Merge remote-tracking branch 'origin/feature_会员模块UI优化' into release_20260831

This commit is contained in:
2026-08-30 14:41:59 +08:00
11 changed files with 2717 additions and 882 deletions
@@ -72,7 +72,8 @@ public class MemberInfoBoardController {
List<Map> result = new ArrayList<>();
int year = DateUtil.thisYear();
for (int i = year - 5; i < year; i++) {
int count = memberInfoService.count(Sqls.create("select count(1) from member_history where year = @year ").setParam("year", year));
// 历史趋势必须按循环中的年份分别统计,避免五个历史节点都查询成当前年份。
int count = memberInfoService.count(Sqls.create("select count(1) from member_history where year = @year ").setParam("year", i));
result.add(Map.of("label", i, "value", count));
}
result.add(Map.of("label", year, "value", memberInfoService.count(Sqls.create("select count(1) from sys_user where member = 1"))));
@@ -245,6 +245,8 @@
//初始化pjax
;(function initPjax() {
if ($.support.pjax) {
// jquery-pjax 的历史缓存读取全局默认值;必须在初始化前关闭,避免返回时恢复缺少 Vue 动态样式的旧 DOM。
$.pjax.defaults.maxCacheLength = 0
$(document).pjax("a[data-pjax]", "#container", {
maxCacheLength: 0,
push: true,
@@ -271,22 +273,9 @@
toggleShowBar()
})
// 监听浏览器的前进后退操作
// PJAX 已内置页面前进后退处理;这里只同步 H5 弹层历史,禁止再次发起 PJAX 请求。
$(window).on("popstate", function (event) {
if (window.h5HistoryLayerManager.handlePopState(event.originalEvent)) {
return
}
if (event.originalEvent.state) {
// 如果存在历史记录状态,触发 Pjax 加载对应内容
$.pjax({
url: event.originalEvent.state.url,
container: "#container",
fragment: "#container",
timeout: 8000,
push: false,
scrollTo: false
})
}
window.h5HistoryLayerManager.handlePopState(event.originalEvent)
})
}
})()
@@ -1,49 +1,326 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar title="入会分工会审核" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<van-sticky>
<van-search
v-model="pageForm.searchKeyword"
placeholder="请输入工号或者姓名进行查询"
show-action
:reverse-color="false"
input-align="left"
@search="doSearch">
<template #action>
<div @click="doSearch">搜索</div>
</template>
</van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item :options="unitList" @change="doSearch" v-model="pageForm.unitId"></van-dropdown-item>
<van-dropdown-item :options="auditList" @change="doSearch" v-model="pageForm.audit"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<style id="style-member-branch-approval-h5">
<!--#include("../../theme.css"){}#-->
<table-list api="/platform/member/apply/branchUnionApproval/pageData" :page_form.sync="pageForm" ref="tableListRef" title="userName" @ready="doSearch">
body {
background-color: #f3f7fd;
}
.member-audit-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.member-audit-page .van-nav-bar {
background-color: #fff;
}
.member-audit-page .van-nav-bar__title {
color: #172033;
font-size: 16px;
font-weight: 600;
}
.member-audit-page .van-nav-bar .van-icon,
.member-audit-page .van-nav-bar__text {
color: #1989fa;
}
.member-audit-sticky {
padding: 22px 12px 1px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.member-audit-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.member-audit-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.member-audit-search .van-field__control {
color: #263548;
font-size: 14px;
}
.member-audit-search .van-field__control::placeholder {
color: #a7b0bf;
}
.member-audit-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.member-audit-filter.is-open {
border-radius: 10px 10px 0 0;
}
.member-audit-filter .van-dropdown-menu__bar,
.member-audit-filter .van-dropdown-menu__item,
.member-audit-filter .van-dropdown-menu__title {
height: 44px;
}
.member-audit-filter .van-dropdown-menu__bar {
box-shadow: none;
}
.member-audit-filter .van-dropdown-menu__title {
display: inline-flex;
align-items: center;
color: #66758a;
font-size: 14px;
line-height: 20px;
}
.member-audit-filter .van-dropdown-menu__title--active,
.member-audit-filter .van-dropdown-item__option--active,
.member-audit-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.member-audit-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
box-sizing: border-box;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
}
.member-audit-filter .van-dropdown-item__option {
display: flex;
min-height: 48px;
padding: 0 16px;
align-items: center;
color: #46566c;
font-size: 14px;
}
.member-audit-content {
padding: 0 12px;
}
.member-audit-list .table-list-container {
margin-top: 0;
}
.member-audit-list .empty-state {
display: none;
}
.member-audit-empty {
display: flex;
min-height: 280px;
padding: 46px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.member-audit-empty img {
display: block;
width: 86%;
max-width: 290px;
height: auto;
object-fit: contain;
}
.member-audit-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.member-audit-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.member-audit-card-header {
display: flex;
margin-bottom: 7px;
align-items: flex-start;
justify-content: space-between;
}
.member-audit-card-heading {
min-width: 0;
padding-right: 10px;
flex: 1;
}
.member-audit-card-title {
overflow: hidden;
color: #253247;
font-size: 15px;
line-height: 22px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.member-audit-card-code {
margin-top: 1px;
color: #9ba6b6;
font-size: 12px;
line-height: 18px;
}
.member-audit-status {
display: inline-flex;
flex: none;
}
.member-audit-status .van-tag {
display: inline-flex;
min-height: 24px;
padding: 3px 9px;
align-items: center;
box-sizing: border-box;
border: 0;
border-radius: 12px;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
}
.member-audit-page .table-list-container .table-list-item .action-btn .van-icon {
margin-right: 6px;
font-size: 15px;
}
.member-audit-page-skeleton {
min-height: calc(100vh - 46px);
padding: 22px 12px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.member-audit-page-skeleton__filter,
.member-audit-page-skeleton__card {
margin-bottom: 10px;
padding: 14px;
border-radius: 12px;
background-color: #fff;
box-sizing: border-box;
box-shadow: 0 5px 16px rgba(43, 73, 112, .07);
}
.member-audit-page-skeleton__filter {
min-height: 96px;
}
.member-audit-page-skeleton__card {
min-height: 154px;
}
.member-audit-page-skeleton .van-skeleton {
padding: 0;
}
@media (max-width: 350px) {
.member-audit-content,
.member-audit-sticky {
padding-right: 10px;
padding-left: 10px;
}
}
</style>
<div id="app" v-cloak>
<div class="member-h5-page member-audit-page">
<van-nav-bar title="分工会审核" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="member-audit-page-skeleton" v-if="pageLoading">
<div class="member-audit-page-skeleton__filter"><van-skeleton title :row="2"></van-skeleton></div>
<div class="member-audit-page-skeleton__cards">
<div class="member-audit-page-skeleton__card" v-for="index in 3" :key="'branch-page-skeleton-' + index">
<van-skeleton title :row="4"></van-skeleton>
</div>
</div>
</div>
<div v-show="!pageLoading">
<van-sticky :offset-top="46">
<div class="member-audit-sticky">
<van-search v-model="pageForm.searchKeyword" class="member-audit-search"
placeholder="请输入工号或姓名搜索" shape="round" clearable
@search="doSearch" @clear="doSearch"></van-search>
<van-dropdown-menu class="member-audit-filter" :class="{'is-open': filterOpened}"
active-color="#1989fa" :close-on-click-outside="false"
:close-on-click-overlay="false">
<van-dropdown-item v-model="pageForm.unitId" :options="unitList" @change="doSearch"
@open="setFilterOpened(true)" @close="setFilterOpened(false)"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.audit" :options="auditList" @change="doSearch"
@open="setFilterOpened(true)" @close="setFilterOpened(false)"></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<main class="member-audit-content">
<div class="member-audit-list">
<div v-if="pageForm.totalCount === 0" class="member-audit-empty">
<img src="/assets/mobile/img/home/v4/app-center-empty-v4.png" alt="暂无分工会审核数据">
<div class="member-audit-empty__title">暂无审核数据</div>
<div class="member-audit-empty__hint">当前筛选条件下没有需要办理的入会申请</div>
</div>
<table-list v-show="!initialLoading" api="/platform/member/apply/branchUnionApproval/pageData" :page_form.sync="pageForm" ref="tableListRef" title="userName" :show_loading_overlay="false" @loading-change="onListLoadingChange" @ready="onReady">
<template #header="{index,row}">
<div class="member-audit-card-header">
<div class="member-audit-card-heading">
<div class="member-audit-card-title">{{row.userName || '--'}}</div>
<div class="member-audit-card-code">工号:{{row.loginName || '--'}}</div>
</div>
<enum-tag class="member-audit-status" v-if="row.instanceState" :value="row.instanceState"
name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</div>
</template>
<template v-slot="{index,row}">
<table-column label="工号">{{row.loginName}}</table-column>
<table-column label="所属工会">{{row.unionName}}</table-column>
<table-column label="工作单位">{{row.unitName}}</table-column>
<table-column label="填报时间">{{row.applyDateTime}}</table-column>
<table-column label="当前节点">{{row.curTaskName}}</table-column>
<table-column label="所属工会">{{row.unionName || '--'}}</table-column>
<table-column label="工作单位">{{row.unitName || '--'}}</table-column>
<table-column label="填报时间">{{row.applyDateTime || '--'}}</table-column>
<table-column label="当前节点">{{row.curTaskName || '--'}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<van-icon name="eye-o"></van-icon>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row)">
<i class="fa fa-check"></i>
<van-icon name="edit"></van-icon>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<van-icon name="revoke"></van-icon>
<span>撤回</span>
</div>
</template>
</table-list>
</div>
</main>
<info ref="infoRef">
<div v-if="showApprovalForm">
@@ -60,13 +337,15 @@ layout("/layouts/platform_h5.html"){
show-word-limit
></van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="primary" block @click="handleTaskAction(1)">同意</van-button>
<van-button type="danger" block @click="handleTaskAction(2)">拒绝</van-button>
<van-button type="danger" block @click="handleTaskAction(6)">退回</van-button>
<div class="member-approval-actions">
<van-button class="member-agree-button" block :loading="formLoading" :disabled="formLoading" @click="handleTaskAction(1)">同意</van-button>
<van-button type="danger" block :disabled="formLoading" @click="handleTaskAction(2)">拒绝</van-button>
<van-button class="member-return-button" block :disabled="formLoading" @click="handleTaskAction(6)">退回</van-button>
</div>
</div>
</info>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
<!--#include("../common/info.js"){}#-->
@@ -83,6 +362,7 @@ layout("/layouts/platform_h5.html"){
unitId: "",
audit: false
},
unionList: [],
unitList: [],
auditList: [
{ text: "已审核", value: true },
@@ -91,27 +371,63 @@ layout("/layouts/platform_h5.html"){
// 审核相关
formData: {},
showApprovalForm: false
showApprovalForm: false,
initialLoading: true,
formLoading: false,
pageLoading: true,
filterOpened: false,
filterReady: false,
tableReady: false,
initialSearchStarted: false
}
},
components: {
"info": INFO
},
methods: {
methods: {
historyBack,
// 同步筛选弹层展开状态,用于保持与提案预审核一致的筛选栏圆角。
setFilterOpened(opened) {
this.$set(this, "filterOpened", opened)
},
onReady() {
this.$set(this, "tableReady", true)
this.tryInitialSearch()
},
// 工会、单位筛选项与列表组件都完成初始化后,只触发一次首次查询。
tryInitialSearch() {
if (!this.filterReady || !this.tableReady || this.initialSearchStarted) return
this.$set(this, "initialSearchStarted", true)
this.doSearch()
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$set(this.pageForm, "pageNumber", 1)
this.$set(this.pageForm, "totalCount", 0)
this.$refs.tableListRef.doSearch()
})
},
onListLoadingChange(state) {
if (state.initial) {
this.$set(this, "initialLoading", state.loading)
if (!state.loading && this.initialSearchStarted) {
this.$set(this, "pageLoading", false)
}
}
},
unionChange() {
this.$set(this.pageForm, "unitId", null)
this.flushUnits().then(() => {
this.doSearch()
})
},
// 查看详情
onView(row) {
this.showApprovalForm = false
@@ -127,18 +443,13 @@ layout("/layouts/platform_h5.html"){
}
},
async handleTaskAction(val) {
await this.$refs.formRef.validate();
this.$dialog.confirm({
handleTaskAction(val) {
if (this.formLoading) return
this.$refs.formRef.validate().then(() => this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
const loading = this.$toast.loading({
message: "加载中...",
forbidClick: true,
overlay: true,
duration: 0
})
})).then(() => {
this.formLoading = true
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
@@ -151,7 +462,7 @@ layout("/layouts/platform_h5.html"){
this.doSearch()
}
}).finally(() => {
loading.close()
this.formLoading = false
})
})
},
@@ -182,7 +493,7 @@ layout("/layouts/platform_h5.html"){
initUnion() {
const hasAdmin = this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
const unionId = hasAdmin ? null : this.$store.state.user.union.id
this.$businessTool.listUnion(unionId).then((data) => {
return this.$businessTool.listUnion(unionId).then((data) => {
this.unionList = data
this.unionList.forEach((v) => {
v.text = v.name
@@ -199,7 +510,7 @@ layout("/layouts/platform_h5.html"){
flushUnits() {
const hasAdmin = this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
const unionId = hasAdmin ? this.pageForm.unionId : this.$store.state.user.union.id
this.$businessTool.listUnit(unionId).then((data) => {
return this.$businessTool.listUnit(unionId).then((data) => {
this.unitList = data
this.unitList.forEach((v) => {
v.text = v.name
@@ -212,9 +523,11 @@ layout("/layouts/platform_h5.html"){
})
}
},
async created() {
await this.initUnion()
await this.flushUnits()
created() {
this.initUnion().then(() => this.flushUnits()).finally(() => {
this.$set(this, "filterReady", true)
this.tryInitialSearch()
})
}
})
</script>
@@ -1,8 +1,17 @@
const INFO = {
template: /*language=HTML*/ `
<van-action-sheet v-model="visible" title="查看详情">
<div class="detail-container">
<van-cell-group title="基本信息">
<van-action-sheet v-model="visible" class="member-info-sheet" title="入会申请详情"
:close-on-click-overlay="false" @close="handleComponentClose">
<div class="member-info-scroll">
<div class="member-list-skeleton" v-if="detailLoading">
<div class="member-list-skeleton__card" v-for="index in 3" :key="'detail-skeleton-' + index">
<van-skeleton title :row="5"></van-skeleton>
</div>
</div>
<template v-else>
<section class="member-detail-card">
<div class="member-detail-card__title">基本信息</div>
<van-cell-group :border="false">
<van-cell title="工号">{{ viewData.loginname }}</van-cell>
<van-cell title="姓名">{{ viewData.username }}</van-cell>
<van-cell title="性别">{{ viewData.sex }}</van-cell>
@@ -73,9 +82,12 @@ const INFO = {
</van-cell>
</van-cell-group>
</section>
<template v-for="task in doneTasks">
<van-cell-group :title="task.displayName" v-if="task.ext.isFirstTaskNode">
<section class="member-detail-card" v-if="task.ext.isFirstTaskNode" :key="'task-' + task.id">
<div class="member-detail-card__title">{{task.displayName}}</div>
<van-cell-group :border="false">
<van-cell title="申请用户">{{ task.ext.initiatorName }}({{task.ext.initiatorAccount}})
</van-cell>
<van-cell title="申请时间">{{ task.finishTime }}</van-cell>
@@ -84,8 +96,11 @@ const INFO = {
:value="task.ext.submitType"></dict-tag>
</van-cell>
</van-cell-group>
</section>
<van-cell-group :title="task.displayName" v-else>
<section class="member-detail-card" v-else :key="'task-' + task.id">
<div class="member-detail-card__title">{{task.displayName}}</div>
<van-cell-group :border="false">
<van-cell title="办理用户">{{ task.taskFormData.userName }}({{task.taskFormData.loginName}})
</van-cell>
<van-cell title="办理时间">{{ task.finishTime }}</van-cell>
@@ -97,6 +112,8 @@ const INFO = {
<div v-html="task.taskFormData.tf_opinion"></div>
</van-cell>
</van-cell-group>
</section>
</template>
</template>
</div>
<slot></slot>
@@ -109,36 +126,68 @@ const INFO = {
row: {},
viewData: {},
doneTasks: [],
detailLoading: false,
historyUnsubscribe: null,
historyLayerName: "member-apply-info"
}
},
methods: {
/**
* 打开入会申请详情
* row 传列表当前申请记录至少包含申请 id无返回值详情和已办任务并行加载
*/
onOpen(row) {
this.row = row
this.visible = true
this.info()
this.getDoneTasks()
this.$set(this, "row", row)
window.h5HistoryLayerManager.ensureRegistered("member-apply-detail")
window.h5HistoryLayerManager.open(this.historyLayerName)
this.detailLoading = true
Promise.all([this.info(), this.getDoneTasks()]).finally(() => {
this.detailLoading = false
})
},
// 关闭
onClose(){
this.visible = false
window.h5HistoryLayerManager.close(this.historyLayerName)
},
handleComponentClose() {
if (window.h5HistoryLayerManager.stack.includes(this.historyLayerName)) {
window.h5HistoryLayerManager.close(this.historyLayerName)
}
},
// id 为入会申请记录主键;接口返回会员申请详情对象。
info() {
this.$axios.post("/platform/member/apply/mine/findMemberApplyRecord", { id: this.row.id }).then((res) => {
return this.$axios.post("/platform/member/apply/mine/findMemberApplyRecord", { id: this.row.id }, {h5SkeletonLoading: true}).then((res) => {
if (res.code === 0) {
this.viewData = res.data
this.$set(this, "viewData", res.data)
}
})
},
// bizId 为入会申请记录主键;接口返回该申请已完成的 WF 任务数组。
getDoneTasks() {
this.$axios.post("/flow/common/doneTasks", { bizId: this.row.id }).then((res) => {
return this.$axios.post("/flow/common/doneTasks", { bizId: this.row.id }, {h5SkeletonLoading: true}).then((res) => {
if (res.code === 0) {
this.doneTasks = res.data
this.$set(this, "doneTasks", res.data)
}
})
},
},
created() {
window.h5HistoryLayerManager.ensureRegistered("member-apply-detail")
this.historyUnsubscribe = window.h5HistoryLayerManager.subscribe((stack) => {
this.$set(this, "visible", stack.includes(this.historyLayerName))
})
},
beforeDestroy() {
if (this.historyUnsubscribe) {
this.historyUnsubscribe()
}
if (window.h5HistoryLayerManager.pageKey === "member-apply-detail") {
window.h5HistoryLayerManager.unregister("member-apply-detail")
}
},
style: /*language=CSS*/ `
.column-cell {
flex-direction: column;
@@ -149,12 +198,11 @@ const INFO = {
border-collapse: collapse;
table-layout: auto;
text-align: center;
font-family: 'Arial', sans-serif;
margin: 20px 0;
margin: 10px 0 0;
}
.table-header {
background-color: gray;
color: white;
background-color: #edf5ff;
color: #4f6f98;
font-weight: bold;
text-transform: uppercase;
}
@@ -166,8 +214,10 @@ const INFO = {
background-color: #f1f1f1;
}
.table-cell {
padding: 10px 20px;
border: 1px solid #ddd;
padding: 8px 6px;
border: 1px solid #e8eef6;
color: #596779;
font-size: 11px;
text-align: center;
}
`
@@ -1,62 +1,382 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style id="style-member-apply-mine-h5">
<!--#include("../../theme.css"){}#-->
body {
background-color: #f3f7fd;
}
.member-mine-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
background-color: #f3f7fd;
}
.member-mine-page .van-nav-bar {
background-color: #fff;
}
.member-mine-page .van-nav-bar__title {
color: #172033;
font-size: 16px;
font-weight: 600;
}
.member-mine-page .van-nav-bar .van-icon,
.member-mine-page .van-nav-bar__text {
color: #1989fa;
}
.member-mine-sticky {
padding: 22px 12px 1px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.member-mine-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.member-mine-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.member-mine-search .van-field__control {
color: #263548;
font-size: 14px;
}
.member-mine-search .van-field__control::placeholder {
color: #a7b0bf;
}
.member-mine-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.member-mine-filter.is-open {
border-radius: 10px 10px 0 0;
}
.member-mine-filter .van-dropdown-menu__bar {
height: 44px;
box-shadow: none;
}
.member-mine-filter .van-dropdown-menu__item {
height: 44px;
align-items: center;
justify-content: center;
}
.member-mine-filter .van-dropdown-menu__title {
display: inline-flex;
height: 44px;
align-items: center;
color: #66758a;
font-size: 14px;
line-height: 20px;
}
.member-mine-filter .van-dropdown-menu__title--active,
.member-mine-filter .van-dropdown-item__option--active,
.member-mine-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.member-mine-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
box-sizing: border-box;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
}
.member-mine-filter .van-dropdown-item__option {
display: flex;
min-height: 48px;
padding: 0 16px;
align-items: center;
color: #46566c;
font-size: 14px;
}
.member-mine-filter .van-dropdown-item__option .van-cell__title {
display: flex;
min-height: 20px;
align-items: center;
line-height: 20px;
}
.member-mine-content {
padding: 0 12px;
}
.member-mine-list {
padding-bottom: 2px;
}
.member-mine-list .table-list-container {
margin-top: 0;
}
.member-mine-list .empty-state {
display: none;
}
.member-mine-empty {
display: flex;
min-height: 280px;
padding: 46px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.member-mine-empty img {
display: block;
width: 86%;
max-width: 290px;
height: auto;
object-fit: contain;
}
.member-mine-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
font-weight: 600;
line-height: 22px;
}
.member-mine-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.member-mine-card-header {
display: flex;
margin-bottom: 7px;
align-items: flex-start;
justify-content: space-between;
}
.member-mine-card-heading {
min-width: 0;
padding-right: 10px;
flex: 1;
}
.member-mine-card-title {
overflow: hidden;
color: #253247;
font-size: 15px;
font-weight: 600;
line-height: 22px;
text-overflow: ellipsis;
white-space: nowrap;
}
.member-mine-card-code {
margin-top: 1px;
color: #9ba6b6;
font-size: 12px;
line-height: 18px;
}
.member-mine-card-status {
display: inline-flex;
flex: none;
}
.member-mine-card-status .van-tag,
.member-mine-card-status--plain {
display: inline-flex;
min-height: 24px;
padding: 3px 9px;
align-items: center;
box-sizing: border-box;
border: 0;
border-radius: 12px;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
}
.member-mine-card-status--plain {
color: #1989fa;
background-color: #eaf4ff;
}
.member-mine-page .table-list-container .table-list-item .action-btn .van-icon {
margin-right: 6px;
font-size: 15px;
}
.member-mine-page-skeleton {
min-height: calc(100vh - 46px);
padding: 22px 12px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.member-mine-page-skeleton__filter,
.member-mine-page-skeleton__card {
margin-bottom: 10px;
padding: 14px;
border-radius: 12px;
background-color: #fff;
box-sizing: border-box;
box-shadow: 0 5px 16px rgba(43, 73, 112, .07);
}
.member-mine-page-skeleton__filter {
min-height: 96px;
}
.member-mine-page-skeleton__card {
min-height: 154px;
}
.member-mine-page-skeleton .van-skeleton {
padding: 0;
}
@media (max-width: 350px) {
.member-mine-content,
.member-mine-sticky {
padding-right: 10px;
padding-left: 10px;
}
}
</style>
<div id="app" v-cloak>
<van-sticky>
<van-nav-bar title="入会填报记录" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<van-search
v-model="pageForm.searchKeyword"
placeholder="请输入工号或者姓名进行查询"
show-action
:reverse-color="false"
input-align="left"
@search="doSearch">
<template #action>
<div @click="doSearch">搜索</div>
</template>
</van-search>
<van-dropdown-menu
v-if="$auth.hasRoleOr('SYSADMIN,SCHOOL_UNION_ADMIN,BRANCH_UNION_CHAIRMAN')"
:close-on-click-outside="false"
:close-on-click-overlay="false"
>
<van-dropdown-item :options="unionList" @change="unionChange" v-model="pageForm.unionId"></van-dropdown-item>
<van-dropdown-item :options="unitList" @change="doSearch" v-model="pageForm.unitId"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<div class="member-h5-page member-mine-page">
<van-nav-bar title="我的入会" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="member-mine-page-skeleton" v-if="pageLoading">
<div class="member-mine-page-skeleton__filter"><van-skeleton title :row="2"></van-skeleton></div>
<div class="member-mine-page-skeleton__cards">
<div class="member-mine-page-skeleton__card" v-for="index in 3" :key="'mine-page-skeleton-' + index">
<van-skeleton title :row="4"></van-skeleton>
</div>
</div>
</div>
<div v-show="!pageLoading">
<van-sticky :offset-top="46">
<div class="member-mine-sticky">
<van-search
v-model="pageForm.searchKeyword"
class="member-mine-search"
placeholder="请输入工号或姓名搜索"
shape="round"
clearable
@search="doSearch"
@clear="doSearch">
</van-search>
<van-dropdown-menu
v-if="$auth.hasRoleOr('SYSADMIN,SCHOOL_UNION_ADMIN,BRANCH_UNION_CHAIRMAN')"
class="member-mine-filter"
:class="{'is-open': filterOpened}"
active-color="#1989fa"
:close-on-click-outside="false"
:close-on-click-overlay="false">
<van-dropdown-item
v-model="pageForm.unionId"
:options="unionList"
@change="unionChange"
@open="setFilterOpened(true)"
@close="setFilterOpened(false)">
</van-dropdown-item>
<van-dropdown-item
v-model="pageForm.unitId"
:options="unitList"
@change="doSearch"
@open="setFilterOpened(true)"
@close="setFilterOpened(false)">
</van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<table-list api="/platform/member/apply/mine/pageData" :page_form.sync="pageForm" ref="tableListRef" title="userName" @ready="doSearch">
<main class="member-mine-content">
<div class="member-mine-list">
<div v-if="pageForm.totalCount === 0" class="member-mine-empty">
<img src="/assets/mobile/img/home/v4/app-center-empty-v4.png" alt="暂无入会申请记录">
<div class="member-mine-empty__title">暂无入会申请记录</div>
<div class="member-mine-empty__hint">当前筛选条件下暂时没有入会申请</div>
</div>
<table-list api="/platform/member/apply/mine/pageData" :page_form.sync="pageForm" ref="tableListRef" title="userName" @loading-change="onListLoadingChange" @ready="onTableReady">
<template #header="{index,row}">
<div class="member-mine-card-header">
<div class="member-mine-card-heading">
<div class="member-mine-card-title">{{row.userName || '--'}}</div>
<div class="member-mine-card-code">工号:{{row.loginName || '--'}}</div>
</div>
<enum-tag
class="member-mine-card-status"
v-if="row.instanceState"
:value="row.instanceState"
name="ProcessInstanceStateEnum"
label_key="message"
size="small">
</enum-tag>
<span v-else class="member-mine-card-status member-mine-card-status--plain">待提交</span>
</div>
</template>
<template v-slot="{index,row}">
<table-column label="工号">{{row.loginName}}</table-column>
<table-column label="所属工会">{{row.unionName}}</table-column>
<table-column label="工作单位">{{row.unitName}}</table-column>
<table-column label="填报时间">{{row.applyDateTime}}</table-column>
<table-column label="当前节点">{{row.taskName}}</table-column>
<table-column label="当前节点">{{row.taskName || '待提交'}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<van-icon name="eye-o"></van-icon>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskKey === 'startTask' || !row.instanceId"
@click="onEdit(row)">
<i class="fa fa-edit"></i>
<van-icon name="edit"></van-icon>
<span>编辑</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<van-icon name="revoke"></van-icon>
<span>撤回</span>
</div>
<div class="action-btn delete" v-if="row.taskKey === 'startTask' || !row.instanceId"
@click="onDelete(row.id)">
<i class="fa fa-trash"></i>
<van-icon name="delete-o"></van-icon>
<span>删除</span>
</div>
</template>
</table-list>
</div>
</main>
<info ref="infoRef"></info>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
<!--#include("../common/info.js"){}#-->
@@ -75,6 +395,13 @@ layout("/layouts/platform_h5.html"){
},
unionList: [],
unitList: [],
pageLoading: true,
filterOpened: false,
filterReady: false,
tableReady: false,
initialSearchStarted: false,
// 标识最近一次单位查询,避免快速切换分工会时旧请求覆盖新选项。
unitRequestSequence: 0
}
},
components: {
@@ -83,10 +410,35 @@ layout("/layouts/platform_h5.html"){
methods: {
historyBack,
// 同步筛选弹层状态,用于切换下拉菜单圆角样式。
setFilterOpened(opened) {
this.$set(this, "filterOpened", opened)
},
onView(row) {
this.$refs.infoRef.onOpen(row)
},
onListLoadingChange(state) {
if (state.initial) {
if (!state.loading && this.initialSearchStarted) {
this.$set(this, "pageLoading", false)
}
}
},
onTableReady() {
this.$set(this, "tableReady", true)
this.tryInitialSearch()
},
// 工会、单位筛选项与列表组件都完成初始化后,只触发一次首次查询。
tryInitialSearch() {
if (!this.filterReady || !this.tableReady || this.initialSearchStarted) return
this.$set(this, "initialSearchStarted", true)
this.doSearch()
},
onEdit(row) {
this.$pjaxReplace('/platform/member/apply/submit/h5?taskId=' + (row.startTaskId || '') + "&bizId=" + row.id)
},
@@ -140,47 +492,66 @@ layout("/layouts/platform_h5.html"){
},
doSearch() {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$refs.tableListRef.doSearch()
// 下拉框先更新父页面筛选值,下一帧再查询,避免 TableList 将旧 unitId 回写覆盖当前选择。
this.$nextTick(() => {
this.$set(this.pageForm, "pageNumber", 1)
this.$set(this.pageForm, "totalCount", 0)
if (this.$refs.tableListRef) {
this.$refs.tableListRef.doSearch()
}
})
},
unionChange() {
// unionId 为本次下拉选择的分工会 ID;切换后只加载该分工会所属单位并刷新列表。
unionChange(unionId) {
this.$set(this.pageForm, "unionId", unionId)
this.$set(this.pageForm, "unitId", null)
this.flushUnits()
this.doSearch()
this.$set(this, "unitList", [{text: "全部单位", value: null}])
this.flushUnits(unionId).then((applied) => {
if (applied) this.doSearch()
})
},
async initUnion() {
initUnion() {
const hasAdmin = this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
const unionId = hasAdmin ? null : this.$store.state.user.union.id
this.unionList = await this.$businessTool.listUnion(unionId)
this.unionList.forEach((v) => {
v.text = v.name
v.value = v.id
return this.$businessTool.listUnion(unionId).then((data) => {
const options = (data || []).map((v) => ({
text: v.name,
value: v.id
}))
if (hasAdmin) {
options.unshift({text: "全部工会", value: null})
}
this.$set(this, "unionList", options)
if (options.length > 0) {
this.$set(this.pageForm, "unionId", options[0].value)
}
})
if (hasAdmin) {
this.unionList.unshift({ text: "全部工会", value: null })
}
if (this.unionList && this.unionList.length > 0) {
this.$set(this.pageForm, "unionId", this.unionList[0].value)
}
},
async flushUnits() {
// selectedUnionId 为选中的分工会 ID;返回 true 表示本次单位查询结果已应用到页面。
flushUnits(selectedUnionId) {
const hasAdmin = this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
const unionId = hasAdmin ? this.pageForm.unionId : this.$store.state.user.union.id
this.unitList = await this.$businessTool.listUnit(unionId)
this.unitList.forEach((v) => {
v.text = v.name
v.value = v.id
const unionId = hasAdmin ? selectedUnionId : this.$store.state.user.union.id
const requestSequence = this.unitRequestSequence + 1
this.$set(this, "unitRequestSequence", requestSequence)
return this.$businessTool.listUnit(unionId).then((data) => {
// 仅接收最后一次分工会切换的查询结果,防止异步返回顺序造成单位串联错误。
if (requestSequence !== this.unitRequestSequence) return false
const options = (data || []).map((v) => ({
text: v.name,
value: v.id
}))
options.unshift({text: "全部单位", value: null})
this.$set(this, "unitList", options)
this.$set(this.pageForm, "unitId", options[0].value)
return true
})
this.unitList.unshift({ text: "全部单位", value: null })
if (this.unitList && this.unitList.length > 0) {
this.$set(this.pageForm, "unitId", this.unitList[0].value)
}
}
},
async created() {
await this.initUnion()
await this.flushUnits()
created() {
this.initUnion().then(() => this.flushUnits(this.pageForm.unionId)).finally(() => {
this.$set(this, "filterReady", true)
this.tryInitialSearch()
})
}
})
</script>
@@ -1,49 +1,326 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<div id="app" v-cloak>
<van-nav-bar title="入会校工会审核" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<van-sticky>
<van-search
v-model="pageForm.searchKeyword"
placeholder="请输入工号或者姓名进行查询"
show-action
:reverse-color="false"
input-align="left"
@search="doSearch">
<template #action>
<div @click="doSearch">搜索</div>
</template>
</van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item :options="unitList" @change="doSearch" v-model="pageForm.unitId"></van-dropdown-item>
<van-dropdown-item :options="auditList" @change="doSearch" v-model="pageForm.audit"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<style id="style-member-school-approval-h5">
<!--#include("../../theme.css"){}#-->
<table-list api="/platform/member/apply/schoolUnionApproval/pageData" :page_form.sync="pageForm" ref="tableListRef" title="userName" @ready="doSearch">
body,
.member-audit-page {
background-color: #f3f7fd;
}
.member-audit-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.member-audit-page .van-nav-bar {
background-color: #fff;
}
.member-audit-page .van-nav-bar__title {
color: #172033;
font-size: 16px;
font-weight: 600;
}
.member-audit-page .van-nav-bar .van-icon,
.member-audit-page .van-nav-bar__text {
color: #1989fa;
}
.member-audit-sticky {
padding: 22px 12px 1px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.member-audit-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.member-audit-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.member-audit-search .van-field__control {
color: #263548;
font-size: 14px;
}
.member-audit-search .van-field__control::placeholder {
color: #a7b0bf;
}
.member-audit-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.member-audit-filter.is-open {
border-radius: 10px 10px 0 0;
}
.member-audit-filter .van-dropdown-menu__bar,
.member-audit-filter .van-dropdown-menu__item,
.member-audit-filter .van-dropdown-menu__title {
height: 44px;
}
.member-audit-filter .van-dropdown-menu__bar {
box-shadow: none;
}
.member-audit-filter .van-dropdown-menu__title {
display: inline-flex;
align-items: center;
color: #66758a;
font-size: 14px;
line-height: 20px;
}
.member-audit-filter .van-dropdown-menu__title--active,
.member-audit-filter .van-dropdown-item__option--active,
.member-audit-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.member-audit-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
box-sizing: border-box;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
}
.member-audit-filter .van-dropdown-item__option {
display: flex;
min-height: 48px;
padding: 0 16px;
align-items: center;
color: #46566c;
font-size: 14px;
}
.member-audit-content {
padding: 0 12px;
}
.member-audit-list .table-list-container {
margin-top: 0;
}
.member-audit-list .empty-state {
display: none;
}
.member-audit-empty {
display: flex;
min-height: 280px;
padding: 46px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.member-audit-empty img {
display: block;
width: 86%;
max-width: 290px;
height: auto;
object-fit: contain;
}
.member-audit-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.member-audit-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.member-audit-card-header {
display: flex;
margin-bottom: 7px;
align-items: flex-start;
justify-content: space-between;
}
.member-audit-card-heading {
min-width: 0;
padding-right: 10px;
flex: 1;
}
.member-audit-card-title {
overflow: hidden;
color: #253247;
font-size: 15px;
line-height: 22px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.member-audit-card-code {
margin-top: 1px;
color: #9ba6b6;
font-size: 12px;
line-height: 18px;
}
.member-audit-status {
display: inline-flex;
flex: none;
}
.member-audit-status .van-tag {
display: inline-flex;
min-height: 24px;
padding: 3px 9px;
align-items: center;
box-sizing: border-box;
border: 0;
border-radius: 12px;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
}
.member-audit-page .table-list-container .table-list-item .action-btn .van-icon {
margin-right: 6px;
font-size: 15px;
}
.member-audit-page-skeleton {
min-height: calc(100vh - 46px);
padding: 22px 12px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.member-audit-page-skeleton__filter,
.member-audit-page-skeleton__card {
margin-bottom: 10px;
padding: 14px;
border-radius: 12px;
background-color: #fff;
box-sizing: border-box;
box-shadow: 0 5px 16px rgba(43, 73, 112, .07);
}
.member-audit-page-skeleton__filter {
min-height: 96px;
}
.member-audit-page-skeleton__card {
min-height: 154px;
}
.member-audit-page-skeleton .van-skeleton {
padding: 0;
}
@media (max-width: 350px) {
.member-audit-content,
.member-audit-sticky {
padding-right: 10px;
padding-left: 10px;
}
}
</style>
<div id="app" v-cloak>
<div class="member-h5-page member-audit-page">
<van-nav-bar title="校工会审核" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="member-audit-page-skeleton" v-if="pageLoading">
<div class="member-audit-page-skeleton__filter"><van-skeleton title :row="2"></van-skeleton></div>
<div class="member-audit-page-skeleton__cards">
<div class="member-audit-page-skeleton__card" v-for="index in 3" :key="'school-page-skeleton-' + index">
<van-skeleton title :row="4"></van-skeleton>
</div>
</div>
</div>
<div v-show="!pageLoading">
<van-sticky :offset-top="46">
<div class="member-audit-sticky">
<van-search v-model="pageForm.searchKeyword" class="member-audit-search"
placeholder="请输入工号或姓名搜索" shape="round" clearable
@search="doSearch" @clear="doSearch"></van-search>
<van-dropdown-menu class="member-audit-filter" :class="{'is-open': filterOpened}"
active-color="#1989fa" :close-on-click-outside="false"
:close-on-click-overlay="false">
<van-dropdown-item v-model="pageForm.unitId" :options="unitList" @change="doSearch"
@open="setFilterOpened(true)" @close="setFilterOpened(false)"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.audit" :options="auditList" @change="doSearch"
@open="setFilterOpened(true)" @close="setFilterOpened(false)"></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<main class="member-audit-content">
<div class="member-audit-list">
<div v-if="pageForm.totalCount === 0" class="member-audit-empty">
<img src="/assets/mobile/img/home/v4/app-center-empty-v4.png" alt="暂无校工会审核数据">
<div class="member-audit-empty__title">暂无审核数据</div>
<div class="member-audit-empty__hint">当前筛选条件下没有需要办理的入会申请</div>
</div>
<table-list v-show="!initialLoading" api="/platform/member/apply/schoolUnionApproval/pageData" :page_form.sync="pageForm" ref="tableListRef" title="userName" :show_loading_overlay="false" @loading-change="onListLoadingChange" @ready="onReady">
<template #header="{index,row}">
<div class="member-audit-card-header">
<div class="member-audit-card-heading">
<div class="member-audit-card-title">{{row.userName || '--'}}</div>
<div class="member-audit-card-code">工号:{{row.loginName || '--'}}</div>
</div>
<enum-tag class="member-audit-status" v-if="row.instanceState" :value="row.instanceState"
name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag>
</div>
</template>
<template v-slot="{index,row}">
<table-column label="工号">{{row.loginName}}</table-column>
<table-column label="所属工会">{{row.unionName}}</table-column>
<table-column label="工作单位">{{row.unitName}}</table-column>
<table-column label="填报时间">{{row.applyDateTime}}</table-column>
<table-column label="当前节点">{{row.curTaskName}}</table-column>
<table-column label="所属工会">{{row.unionName || '--'}}</table-column>
<table-column label="工作单位">{{row.unitName || '--'}}</table-column>
<table-column label="填报时间">{{row.applyDateTime || '--'}}</table-column>
<table-column label="当前节点">{{row.curTaskName || '--'}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="onView(row)">
<i class="fa fa-eye"></i>
<van-icon name="eye-o"></van-icon>
<span>查看</span>
</div>
<div class="action-btn" v-if="row.taskState === 10" @click="onApproval(row)">
<i class="fa fa-check"></i>
<van-icon name="edit"></van-icon>
<span>审核</span>
</div>
<div class="action-btn delete" v-if="row.canRevoke" @click="onRevoke(row)">
<i class="fa fa-undo"></i>
<van-icon name="revoke"></van-icon>
<span>撤回</span>
</div>
</template>
</table-list>
</div>
</main>
<info ref="infoRef">
<div v-if="showApprovalForm">
@@ -60,13 +337,15 @@ layout("/layouts/platform_h5.html"){
show-word-limit
></van-field>
</van-form>
<div style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button type="primary" block @click="handleTaskAction(1)">同意</van-button>
<van-button type="danger" block @click="handleTaskAction(2)">拒绝</van-button>
<van-button type="danger" block @click="handleTaskAction(6)">退回</van-button>
<div class="member-approval-actions">
<van-button class="member-agree-button" block :loading="formLoading" :disabled="formLoading" @click="handleTaskAction(1)">同意</van-button>
<van-button type="danger" block :disabled="formLoading" @click="handleTaskAction(2)">拒绝</van-button>
<van-button class="member-return-button" block :disabled="formLoading" @click="handleTaskAction(6)">退回</van-button>
</div>
</div>
</info>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
<!--#include("../common/info.js"){}#-->
@@ -83,6 +362,7 @@ layout("/layouts/platform_h5.html"){
unitId: "",
audit: false
},
unionList: [],
unitList: [],
auditList: [
{ text: "已审核", value: true },
@@ -91,7 +371,14 @@ layout("/layouts/platform_h5.html"){
// 审核相关
formData: {},
showApprovalForm: false
showApprovalForm: false,
initialLoading: true,
formLoading: false,
pageLoading: true,
filterOpened: false,
filterReady: false,
tableReady: false,
initialSearchStarted: false
}
},
components: {
@@ -100,18 +387,47 @@ layout("/layouts/platform_h5.html"){
methods: {
historyBack,
// 同步筛选弹层展开状态,用于保持与提案预审核一致的筛选栏圆角。
setFilterOpened(opened) {
this.$set(this, "filterOpened", opened)
},
onReady() {
this.$set(this, "tableReady", true)
this.tryInitialSearch()
},
// 工会、单位筛选项与列表组件都完成初始化后,只触发一次首次查询。
tryInitialSearch() {
if (!this.filterReady || !this.tableReady || this.initialSearchStarted) return
this.$set(this, "initialSearchStarted", true)
this.doSearch()
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$set(this.pageForm, "pageNumber", 1)
this.$set(this.pageForm, "totalCount", 0)
this.$refs.tableListRef.doSearch()
})
},
onListLoadingChange(state) {
if (state.initial) {
this.$set(this, "initialLoading", state.loading)
if (!state.loading && this.initialSearchStarted) {
this.$set(this, "pageLoading", false)
}
}
},
unionChange() {
this.$set(this.pageForm, "unitId", null)
this.flushUnits().then(() => {
this.doSearch()
})
},
// 查看详情
onView(row) {
this.showApprovalForm = false
@@ -127,18 +443,13 @@ layout("/layouts/platform_h5.html"){
}
},
async handleTaskAction(val) {
await this.$refs.formRef.validate();
this.$dialog.confirm({
handleTaskAction(val) {
if (this.formLoading) return
this.$refs.formRef.validate().then(() => this.$dialog.confirm({
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
const loading = this.$toast.loading({
message: "加载中...",
forbidClick: true,
overlay: true,
duration: 0
})
})).then(() => {
this.formLoading = true
this.$axios.post("/flow/common/executeTask", {
data: JSON.stringify({
...this.formData,
@@ -151,7 +462,7 @@ layout("/layouts/platform_h5.html"){
this.doSearch()
}
}).finally(() => {
loading.close()
this.formLoading = false
})
})
},
@@ -182,7 +493,7 @@ layout("/layouts/platform_h5.html"){
initUnion() {
const hasAdmin = this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
const unionId = hasAdmin ? null : this.$store.state.user.union.id
this.$businessTool.listUnion(unionId).then((data) => {
return this.$businessTool.listUnion(unionId).then((data) => {
this.unionList = data
this.unionList.forEach((v) => {
v.text = v.name
@@ -199,7 +510,7 @@ layout("/layouts/platform_h5.html"){
flushUnits() {
const hasAdmin = this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
const unionId = hasAdmin ? this.pageForm.unionId : this.$store.state.user.union.id
this.$businessTool.listUnit(unionId).then((data) => {
return this.$businessTool.listUnit(unionId).then((data) => {
this.unitList = data
this.unitList.forEach((v) => {
v.text = v.name
@@ -212,9 +523,11 @@ layout("/layouts/platform_h5.html"){
})
}
},
async created() {
await this.initUnion()
await this.flushUnits()
created() {
this.initUnion().then(() => this.flushUnits()).finally(() => {
this.$set(this, "filterReady", true)
this.tryInitialSearch()
})
}
})
</script>
@@ -1,7 +1,9 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style>
<style id="style-member-apply-submit-h5">
<!--#include("../../theme.css"){}#-->
.up_down > .van-cell {
flex-flow: column;
}
@@ -10,14 +12,79 @@ layout("/layouts/platform_h5.html"){
width: 100%;
}
.van-cell, .van-picker button, .submit .van-button--normal {
font-size: 16px;
.member-family-heading,
.member-family-card__heading {
display: flex;
align-items: center;
justify-content: space-between;
}
.member-family-card {
margin-top: 10px;
padding: 10px;
border: 1px solid #edf1f6;
border-radius: 10px;
background: #f8fbff;
}
.member-family-card__heading {
color: #42546c;
font-size: 13px;
font-weight: 600;
}
.member-family-empty {
padding: 28px 0;
color: #9aa7b8;
text-align: center;
}
.member-family-empty__text {
margin-bottom: 10px;
font-size: 12px;
}
.member-voluntary {
padding: 12px 14px;
color: #596779;
font-size: 13px;
line-height: 1.8;
}
.member-voluntary__paragraph + .member-voluntary__paragraph {
margin-top: 6px;
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="入会申请" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<div class="member-h5-page" :class="{'has-fixed-actions': showActionButtons}">
<van-nav-bar title="入会申请" left-text="返回" left-arrow @click-left="handlePageBack" placeholder fixed></van-nav-bar>
<van-form ref="formRef" class="form-container" :show-error-message="false">
<div class="member-page-skeleton" v-if="pageLoading">
<div class="member-page-skeleton__hero"><van-skeleton title :row="1"></van-skeleton></div>
<div class="member-page-skeleton__cards member-list-skeleton">
<div class="member-form-skeleton__card" v-for="index in 3" :key="'member-form-skeleton-' + index">
<van-skeleton title :row="5"></van-skeleton>
</div>
</div>
</div>
<header class="member-hero" v-show="!pageLoading">
<div class="member-hero__content">
<div class="member-hero__title">会员入会</div>
<div class="member-hero__subtitle">凝聚职工 · 温暖同行</div>
</div>
</header>
<main class="member-page-main" v-show="!pageLoading">
<div class="member-form-intro">
<van-icon name="info-o"></van-icon>
<div>
<div class="member-form-intro__title">入会信息确认</div>
<div class="member-form-intro__text">请核对个人信息并完整填写申请资料,带星号的内容为必填项。</div>
</div>
</div>
<van-form ref="formRef" class="form-container member-application-form" :show-error-message="false">
<van-cell-group title="基础信息" class="form-section">
<van-field v-model="formData.loginname" label="工号" readonly></van-field>
<van-field v-model="formData.username" label="姓名" readonly></van-field>
@@ -28,14 +95,14 @@ layout("/layouts/platform_h5.html"){
label="民族"
readonly
placeholder="请点击选择民族"
@click="showNationPicker = true"
@click="setLayerVisible('showNationPicker', true)"
clickable
></van-field>
<van-popup position="bottom" round v-model:show="showNationPicker">
<van-popup position="bottom" round v-model="showNationPicker" :close-on-click-overlay="false">
<van-picker show-toolbar
:columns="nationColumns"
@cancel="showNationPicker = false"
@confirm="(v)=>{formData.nation = v;showNationPicker = false}"
@cancel="setLayerVisible('showNationPicker', false)"
@confirm="onNationConfirm"
></van-picker>
</van-popup>
<van-field
@@ -43,16 +110,16 @@ layout("/layouts/platform_h5.html"){
label="出生年月"
is-link
readonly
@click="showDatePicker = true"
@click="setLayerVisible('showDatePicker', true)"
:rules="[{ required: true, message: '请填写出生年月' }]"
></van-field>
<van-popup v-model="showDatePicker" position="bottom">
<van-popup v-model="showDatePicker" position="bottom" :close-on-click-overlay="false">
<van-datetime-picker
type="date"
:min-date="new Date(1900, 0, 1)"
:max-date="new Date()"
@confirm="onDateConfirm"
@cancel="showDatePicker=false"
@cancel="setLayerVisible('showDatePicker', false)"
></van-datetime-picker>
</van-popup>
<van-field
@@ -62,14 +129,14 @@ layout("/layouts/platform_h5.html"){
readonly
is-link
placeholder="请选择政治面貌"
@click="showPoliticalPicker = true"
@click="setLayerVisible('showPoliticalPicker', true)"
clickable
></van-field>
<van-popup position="bottom" round v-model:show="showPoliticalPicker">
<van-popup position="bottom" round v-model="showPoliticalPicker" :close-on-click-overlay="false">
<van-picker show-toolbar
:columns="politicalColumns"
@cancel="showPoliticalPicker = false"
@confirm="(v)=>{formData.political = v;showPoliticalPicker = false}"
@cancel="setLayerVisible('showPoliticalPicker', false)"
@confirm="onPoliticalConfirm"
></van-picker>
</van-popup>
<van-field
@@ -79,14 +146,14 @@ layout("/layouts/platform_h5.html"){
readonly
is-link
placeholder="请选择学历"
@click="showEducationPicker = true"
@click="setLayerVisible('showEducationPicker', true)"
clickable
></van-field>
<van-popup position="bottom" round v-model:show="showEducationPicker">
<van-popup position="bottom" round v-model="showEducationPicker" :close-on-click-overlay="false">
<van-picker show-toolbar
:columns="educationColumns"
@cancel="showEducationPicker = false"
@confirm="(v)=>{formData.education = v;showEducationPicker = false}"
@cancel="setLayerVisible('showEducationPicker', false)"
@confirm="onEducationConfirm"
></van-picker>
</van-popup>
<van-field
@@ -96,14 +163,14 @@ layout("/layouts/platform_h5.html"){
readonly
is-link
placeholder="请选择学位"
@click="showAcademicDegreePicker = true"
@click="setLayerVisible('showAcademicDegreePicker', true)"
clickable
></van-field>
<van-popup position="bottom" round v-model:show="showAcademicDegreePicker">
<van-popup position="bottom" round v-model="showAcademicDegreePicker" :close-on-click-overlay="false">
<van-picker show-toolbar
:columns="academicDegreeColumns"
@cancel="showAcademicDegreePicker = false"
@confirm="(v)=>{formData.academicDegree = v;showAcademicDegreePicker = false}"
@cancel="setLayerVisible('showAcademicDegreePicker', false)"
@confirm="onAcademicDegreeConfirm"
></van-picker>
</van-popup>
<van-field
@@ -154,14 +221,14 @@ layout("/layouts/platform_h5.html"){
readonly
is-link
placeholder="请选择婚姻状况"
@click="showMarriagePicker = true"
@click="setLayerVisible('showMarriagePicker', true)"
clickable
></van-field>
<van-popup position="bottom" round v-model:show="showMarriagePicker">
<van-popup position="bottom" round v-model="showMarriagePicker" :close-on-click-overlay="false">
<van-picker show-toolbar
:columns="marriageColumns"
@cancel="showMarriagePicker = false"
@confirm="(v)=>{formData.marriage = v;showMarriagePicker = false}"
@cancel="setLayerVisible('showMarriagePicker', false)"
@confirm="onMarriageConfirm"
></van-picker>
</van-popup>
@@ -215,25 +282,21 @@ layout("/layouts/platform_h5.html"){
<van-cell-group title="家庭信息" class="form-section up_down">
<div class="van-cell">
<div style="display: flex;justify-content: space-between">
<div style="display: flex;">
<div style="width: 100%">
<span>家庭主要成员及联系方式</span>
</div>
</div>
<div class="member-family-heading">
<span>家庭主要成员及联系方式</span>
<van-button v-if="formData.families && formData.families.length>0 && formData.families.length<5"
style="width: 40px"
icon="plus" type="primary" round size="mini"
@click="formData.families.push({})"
@click="addFamilyMember"
native-type="button"></van-button>
</div>
<template v-if="formData.families && formData.families.length>0">
<div v-for="o,i in formData.families" class="mt5">
<div style="font-weight: 700;display: flex;justify-content: space-between">
<div v-for="(o,i) in formData.families" :key="'family-' + i" class="member-family-card">
<div class="member-family-card__heading">
<span>成员{{toChinesNum(i+1)}}</span>
<van-button style="width: 40px"
icon="cross" type="danger" round size="mini"
@click="formData.families.splice(i,1)"
@click="removeFamilyMember(i)"
native-type="button"></van-button>
</div>
<van-field label="关系" v-model="o.relation" placeholder="请填写与本人关系"
@@ -247,10 +310,11 @@ layout("/layouts/platform_h5.html"){
</div>
</template>
<template v-else>
<div style="padding: 50px;text-align: center">
<div class="member-family-empty">
<div class="member-family-empty__text">暂未填写家庭成员</div>
<van-button style="width: 50px"
icon="plus" type="primary" round size="mini"
@click="formData.families.push({})"
@click="addFamilyMember"
native-type="button"></van-button>
</div>
</template>
@@ -266,26 +330,28 @@ layout("/layouts/platform_h5.html"){
</van-cell-group>
<van-cell-group title="入会意愿" class="form-section">
<van-checkbox style="font-size: 17px;padding: 12px" icon-size="24px"
<van-checkbox class="member-voluntary" icon-size="22px"
v-model="formData.isVoluntary" shape="square">
<div style="text-indent: 2.1rem">
<div class="member-voluntary__paragraph">
<span :style="'color:' + (formData.isVoluntary ? '#1989fa' : '#F56C6C') ">
我自愿申请加入学校工会,并委托学校按规定代为扣缴工会会员会费。
</span>
</div>
<div style="text-indent: 2.1rem">
<div class="member-voluntary__paragraph">
<span :style="'color:' + (formData.isVoluntary ? '#1989fa' : '#F56C6C') ">
我将严格遵守工会章程,认真执行工会决议,积极参与工会活动,主动融入“学校健康幸福家”建设,为营造温暖、和谐、奋进的校园氛围贡献力量,以实际行动助力学校各项事业发展。
</span>
</div>
</van-checkbox>
</van-cell-group>
<div v-if="showActionButtons" style="display: flex; justify-content: space-between; column-gap: 10px; padding: 10px">
<van-button block type="primary" @click.submit="onSave">保存</van-button>
<van-button v-if="!taskId" block type="primary" @click.submit="onSubmit">提交</van-button>
<van-button v-else block type="primary" @click.submit="onFinishTask">提交</van-button>
<div v-if="showActionButtons" class="member-form-actions">
<van-button block plain class="member-save-button" :loading="formLoading && loadingAction === 'save'" :disabled="formLoading" @click.submit="onSave">保存草稿</van-button>
<van-button v-if="!taskId" block type="primary" class="member-submit-button" :loading="formLoading && loadingAction === 'submit'" :disabled="formLoading" @click.submit="onSubmit">提交申请</van-button>
<van-button v-else block type="primary" class="member-submit-button" :loading="formLoading && loadingAction === 'submitAgain'" :disabled="formLoading" @click.submit="onFinishTask">重新提交</van-button>
</div>
</van-form>
</main>
</div>
</div>
<script nonce="${cspNonce!}">
new Vue({
@@ -298,6 +364,10 @@ layout("/layouts/platform_h5.html"){
taskId: GetQueryString("taskId"),
// 当前登录人已是会员时,不显示保存和提交按钮。
showActionButtons: true,
pageLoading: true,
formLoading: false,
loadingAction: "",
historyLayerPageKey: "member-apply-submit",
formData: {
families: []
},
@@ -319,21 +389,90 @@ layout("/layouts/platform_h5.html"){
}
},
methods: {
historyBack,
handlePageBack() {
if (window.h5HistoryLayerManager.stack.length) {
window.h5HistoryLayerManager.close()
return
}
historyBack()
},
/**
* 同步入会表单选择器与浏览器历史。
* stateName 为选择器状态字段,visible 为目标显示状态;方法无返回值。
*/
setLayerVisible(stateName, visible) {
const layerName = {
showNationPicker: "nation-picker",
showDatePicker: "birthday-picker",
showPoliticalPicker: "political-picker",
showEducationPicker: "education-picker",
showAcademicDegreePicker: "degree-picker",
showMarriagePicker: "marriage-picker"
}[stateName]
if (visible) {
window.h5HistoryLayerManager.open(layerName)
return
}
window.h5HistoryLayerManager.close(layerName)
},
registerHistoryLayers() {
// 六个选择器共用页面级历史栈,手机返回键只关闭当前最上层选择器。
window.h5HistoryLayerManager.register(this.historyLayerPageKey, (stack) => {
this.$set(this, "showNationPicker", stack.includes("nation-picker"))
this.$set(this, "showDatePicker", stack.includes("birthday-picker"))
this.$set(this, "showPoliticalPicker", stack.includes("political-picker"))
this.$set(this, "showEducationPicker", stack.includes("education-picker"))
this.$set(this, "showAcademicDegreePicker", stack.includes("degree-picker"))
this.$set(this, "showMarriagePicker", stack.includes("marriage-picker"))
})
},
onNationConfirm(value) {
this.$set(this.formData, "nation", value)
this.setLayerVisible("showNationPicker", false)
},
onPoliticalConfirm(value) {
this.$set(this.formData, "political", value)
this.setLayerVisible("showPoliticalPicker", false)
},
onEducationConfirm(value) {
this.$set(this.formData, "education", value)
this.setLayerVisible("showEducationPicker", false)
},
onAcademicDegreeConfirm(value) {
this.$set(this.formData, "academicDegree", value)
this.setLayerVisible("showAcademicDegreePicker", false)
},
onMarriageConfirm(value) {
this.$set(this.formData, "marriage", value)
this.setLayerVisible("showMarriagePicker", false)
},
addFamilyMember() {
const nextIndex = this.formData.families.length
this.$set(this.formData.families, nextIndex, {})
},
removeFamilyMember(index) {
this.formData.families.splice(index, 1)
},
onSave() {
if (this.formLoading) return
this.$dialog.confirm({
title: "提示",
message: "您确定要保存吗?",
confirmButtonText: "确定",
cancelButtonText: "取消"
}).then(() => {
const loading = this.$toast.loading({
message: "加载中...",
forbidClick: true,
overlay: true,
duration: 0
})
this.formLoading = true
this.loadingAction = "save"
this.$axios.post("/platform/member/apply/submit/save", { data: JSON.stringify(this.formData) }).then(res => {
if (res.code === 0) {
this.$toast.success(res.msg)
@@ -343,12 +482,14 @@ layout("/layouts/platform_h5.html"){
console.log(res.msg)
}
}).finally(() => {
loading.close()
this.formLoading = false
this.loadingAction = ""
})
})
},
onSubmit() {
if (this.formLoading) return
this.$refs.formRef.validate().then(() => {
if (!this.formData.personalData) {
this.$toast.fail("请填写个人学习及工作经历")
@@ -362,12 +503,8 @@ layout("/layouts/platform_h5.html"){
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
const loading = this.$toast.loading({
message: "加载中...",
forbidClick: true,
overlay: true,
duration: 0
})
this.formLoading = true
this.loadingAction = "submit"
this.$axios.post("/platform/member/apply/submit/submit", {
data: JSON.stringify(this.formData)
}).then(res => {
@@ -379,13 +516,15 @@ layout("/layouts/platform_h5.html"){
console.log(res.msg)
}
}).finally(() => {
loading.close()
this.formLoading = false
this.loadingAction = ""
})
})
})
},
onFinishTask() {
if (this.formLoading) return
this.$refs.formRef.validate().then(() => {
if (!this.formData.personalData) {
this.$toast.fail("请填写个人学习及工作经历")
@@ -399,12 +538,8 @@ layout("/layouts/platform_h5.html"){
title: "提示",
message: "您确定要提交吗?"
}).then(() => {
const loading = this.$toast.loading({
message: "加载中...",
forbidClick: true,
overlay: true,
duration: 0
})
this.formLoading = true
this.loadingAction = "submitAgain"
this.$axios.post("/platform/member/apply/submit/submitAgain", {
data: JSON.stringify(this.formData),
taskId: this.taskId
@@ -417,39 +552,35 @@ layout("/layouts/platform_h5.html"){
console.log(res.msg)
}
}).finally(() => {
loading.close()
this.formLoading = false
this.loadingAction = ""
})
})
})
},
onDateConfirm(value) {
this.formData.birthday = value
this.showDatePicker = false
this.$set(this.formData, "birthday", value)
this.setLayerVisible("showDatePicker", false)
},
async init() {
let user
const resp = await $.post("/platform/member/apply/submit/getSelfUserInfo")
if (resp.code === 0) {
if (!resp.data) {
user = this.$store.state.user
} else {
user = resp.data
}
} else {
user = this.$store.state.user
}
// 页面按钮是否展示,以当前登录人的会员状态为准。
this.showActionButtons = !(user && user.member)
init() {
// 无参数获取当前登录人资料;有 id 时改查已有申请,接口均返回对应的用户/申请对象。
return this.$axios.post("/platform/member/apply/submit/getSelfUserInfo", {}, {h5SkeletonLoading: true})
.then((resp) => {
const user = resp.code === 0 && resp.data ? resp.data : this.$store.state.user
// 页面按钮是否展示,以当前登录人的会员状态为准。
this.showActionButtons = !(user && user.member)
if (this.id) {
return this.$axios.post("/platform/member/apply/submit/findApplyById", { id: this.id }, {h5SkeletonLoading: true})
.then((res) => {
if (res.code === 0) {
this.$set(this, "formData", res.data)
}
})
}
if (this.id) {
const res = await this.$axios.post("/platform/member/apply/submit/findApplyById", { id: this.id })
debugger
if (res.code === 0) {
this.formData = res.data
}
} else {
const {
id,
username,
@@ -519,7 +650,7 @@ layout("/layouts/platform_h5.html"){
this.$set(this.formData, "campus", "")
this.$set(this.formData, "campusName", "")
}
}
})
},
toChinesNum(num) {
let changeNum = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"]
@@ -545,19 +676,20 @@ layout("/layouts/platform_h5.html"){
this.$set(this.formData, "campus", o.value) // 字典值
this.showCampusPicker = false
},
async initDictOptions() {
// 初始化校区字典数据
const campusData = await this.$businessTool.getDictOptions("USER_CAMPUS")
this.campusColumns = campusData.map(item => {
return { value: item.code, text: item.name }
})
// 初始化完成后,如果已经有用户数据,更新校区显示名称
if (this.formData.campus && this.campusColumns.length > 0) {
const campusItem = this.campusColumns.find(item => item.value === this.formData.campus)
if (campusItem) {
this.$set(this.formData, "campusName", campusItem.text)
}
}
initDictOptions() {
// USER_CAMPUS 返回校区字典数组;完成后同步已有表单的校区显示名称。
return this.$businessTool.getDictOptions("USER_CAMPUS")
.then((campusData) => {
this.campusColumns = campusData.map((item) => {
return { value: item.code, text: item.name }
})
if (this.formData.campus && this.campusColumns.length > 0) {
const campusItem = this.campusColumns.find((item) => item.value === this.formData.campus)
if (campusItem) {
this.$set(this.formData, "campusName", campusItem.text)
}
}
})
}
},
computed: {
@@ -594,8 +726,15 @@ layout("/layouts/platform_h5.html"){
}
},
created() {
this.registerHistoryLayers()
this.initDictOptions()
this.init()
.then(() => this.init())
.finally(() => {
this.pageLoading = false
})
},
beforeDestroy() {
window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
}
})
</script>
@@ -1,5 +1,12 @@
const MOBILE_MEMBER_INFO = {
template: /*language=HTML*/ `
<div class="member-info-scroll">
<div class="member-list-skeleton" v-if="detailLoading">
<div class="member-list-skeleton__card" v-for="index in 3" :key="'member-profile-skeleton-' + index">
<van-skeleton title :row="5"></van-skeleton>
</div>
</div>
<template v-else>
<div v-if="profileMode" class="mine-profile-details">
<section class="mine-detail-card">
<h2>基本信息</h2>
@@ -24,7 +31,9 @@
</section>
</div>
<div v-else class="detail-container">
<van-cell-group title="基本信息">
<section class="member-detail-card">
<div class="member-detail-card__title">会员信息</div>
<van-cell-group :border="false">
<van-cell title="工号">{{ viewData.loginname }}</van-cell>
<van-cell title="姓名">{{ viewData.username }}</van-cell>
<van-cell title="性别">{{ viewData.sex }}</van-cell>
@@ -81,6 +90,9 @@
</van-cell>
</template>
</van-cell-group>
</section>
</div>
</template>
</div>
`,
props: {
@@ -89,12 +101,13 @@
},
data() {
return {
viewData: {}
viewData: {},
detailLoading: false
}
},
watch: {
userId: {
handler: function () {
handler() {
this.findUserInfoByUserId()
},
immediate: true
@@ -113,14 +126,18 @@
}
return mobile.replace(/^(\d{3})\d+(\d{4})$/, "$1****$2")
},
// userId 为系统用户主键;接口返回会员档案详情对象,加载期间显示档案骨架屏。
findUserInfoByUserId() {
if (!this.userId) {
return
}
this.$axios.post("/platform/member/apply/common/findOne", { id: this.userId }).then((res) => {
this.detailLoading = true
this.$axios.post("/platform/member/apply/common/findOne", { id: this.userId }, {h5SkeletonLoading: true}).then((res) => {
if (res.code === 0) {
this.viewData = res.data
this.$set(this, "viewData", res.data)
}
}).finally(() => {
this.detailLoading = false
})
}
}
@@ -2,536 +2,346 @@
layout("/layouts/platform_h5.html"){
#-->
<style>
.top-col {
background-color: white;
position: relative;
box-sizing: border-box;
padding: 20px;
width: calc((100% - 5px) / 2);
}
.top-col:not(:last-child),
.center-col:not(:last-child) {
margin-right: 5px;
}
.col-title {
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
margin-bottom: 10px;
}
.top-col-member-number {
font-size: 30px;
color: rgba(0, 0, 0, 0.85);
}
.top-col-chart {
height: 80px;
width: 100%;
position: relative;
}
.center-col {
background-color: white;
position: relative;
box-sizing: border-box;
padding: 20px;
/*width: calc((100% - (20px * 1)) / 2);*/
}
.idx {
display: flex;
justify-content: center;
align-items: center;
color: rgba(0, 0, 0, 0.85);
width: 20px;
height: 20px;
border-radius: 50%;
background-color: #f0f0f0;
font-size: 12px;
}
.idx-rank {
color: white !important;
background-color: #314659 !important;
}
.el-table__body-wrapper::-webkit-scrollbar {
display: none; /* Chrome Safari */
}
.el-table__body-wrapper {
scrollbar-width: none; /* firefox */
-ms-overflow-style: none; /* IE 10+ */
overflow-x: hidden;
overflow-y: auto;
}
.data-box{
text-align: center;
font-size:13px;
margin:12px 0;
}
.th-row,.td-row{
display:flex;
height:30px;
line-height:30px;
padding:0 12px;
}
.th-row{background:#f8f8f8;}
/* 关键:比例 + 最小宽度0 */
.col-index{flex:0 0 60px;} /* 序号固定60 */
.col-name{flex:1 1 120px;min-width:0;} /* 名称占剩余,最小120 */
.col-num{flex:0 0 80px;text-align:right;} /* 人数固定80 */
.common-ellipsis{
overflow:hidden;
white-space:nowrap;
text-overflow:ellipsis;
body { background: #f3f7fd; }
.member-board-page { min-height: 100vh; padding-bottom: calc(22px + env(safe-area-inset-bottom)); box-sizing: border-box; color: #263548; background: #f3f7fd; }
.member-board-page .van-nav-bar { background: #fff; }
.member-board-page .van-nav-bar__title { color: #1f2937; font-size: 16px; font-weight: 600; }
.member-board-page .van-nav-bar .van-icon, .member-board-page .van-nav-bar__text { color: #1989fa; }
.member-board-main { padding: 12px; box-sizing: border-box; }
.member-board-card { overflow: hidden; border: 1px solid #e8eef6; border-radius: 12px; background: #fff; box-shadow: 0 7px 20px rgba(55, 85, 126, .08); }
.member-board-section { margin-top: 10px; }
.member-board-card__title { color: #2c3b50; font-size: 14px; line-height: 21px; font-weight: 600; }
.member-board-summary { display: flex; min-height: 132px; padding: 16px; align-items: stretch; color: #fff; border: 0; background: #247fe8; box-sizing: border-box; box-shadow: 0 9px 22px rgba(36, 127, 232, .22); }
.member-board-summary__number { display: flex; width: 42%; padding-right: 13px; justify-content: center; flex-direction: column; border-right: 1px solid rgba(255, 255, 255, .26); box-sizing: border-box; }
.member-board-summary__label, .member-board-summary__trend-title { color: rgba(255, 255, 255, .88); font-size: 12px; line-height: 18px; }
.member-board-summary__value { margin-top: 3px; color: #fff; font-size: 30px; line-height: 40px; font-weight: 700; letter-spacing: .5px; white-space: nowrap; }
.member-board-summary__unit { margin-left: 3px; font-size: 13px; font-weight: 500; }
.member-board-summary__ratio { display: flex; margin-top: 6px; align-items: center; color: rgba(255, 255, 255, .9); font-size: 11px; line-height: 18px; white-space: nowrap; }
.member-board-summary__ratio .van-icon { margin-left: 5px; color: #7ee2a8; font-size: 12px; }
.member-board-summary__ratio.is-down .van-icon { color: #ffb6b6; transform: rotate(180deg); }
.member-board-summary__trend { min-width: 0; padding-left: 14px; flex: 1; box-sizing: border-box; }
.member-board-summary__chart { width: 100%; height: 86px; margin-top: 1px; }
.member-board-grid { display: grid; margin-top: 10px; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.member-board-mini-card { min-width: 0; height: 158px; padding: 13px 12px 10px; box-sizing: border-box; }
.member-board-mini-card__content { position: relative; height: 116px; }
.member-board-mini-chart { width: 100%; height: 116px; }
.member-board-ring-value { position: absolute; top: 50%; left: 50%; z-index: 2; color: #263548; font-size: 17px; font-weight: 600; transform: translate(-50%, -50%); pointer-events: none; }
.member-board-sex-layout { display: flex; height: 116px; align-items: center; }
.member-board-sex-chart { width: 58%; height: 112px; flex: none; }
.member-board-sex-legend { min-width: 0; flex: 1; }
.member-board-sex-legend__item { display: flex; margin: 8px 0; align-items: center; color: #56657a; font-size: 11px; line-height: 18px; }
.member-board-sex-legend__dot { width: 7px; height: 7px; margin-right: 6px; flex: none; border-radius: 50%; background: #409eff; }
.member-board-sex-legend__item.is-woman .member-board-sex-legend__dot { background: #f56c6c; }
.member-board-sex-legend__name { flex: none; }
.member-board-sex-legend__value { margin-left: 5px; color: #263548; font-weight: 600; }
.member-board-chart-card { padding: 13px 12px 10px; box-sizing: border-box; }
.member-board-radar-chart { width: 100%; height: 252px; }
.member-board-column-chart { width: 100%; height: 238px; }
.member-board-union-head { display: flex; align-items: center; justify-content: space-between; }
.member-board-union-total { display: inline-flex; height: 28px; padding: 0 10px; align-items: center; border-radius: 14px; color: #397de0; background: #edf5ff; font-size: 11px; }
.member-board-union-chart { width: 100%; height: 178px; margin-top: 4px; }
.member-board-ranking { margin-top: 5px; overflow: hidden; border: 1px solid #edf1f6; border-radius: 8px; }
.member-board-ranking__row { display: grid; min-height: 34px; padding: 0 10px; align-items: center; grid-template-columns: 38px minmax(0, 1fr) 58px; color: #526176; font-size: 11px; line-height: 18px; box-sizing: border-box; }
.member-board-ranking__row:nth-child(odd) { background: #f8fbff; }
.member-board-ranking__row.is-head { min-height: 36px; color: #354255; background: #f4f7fb; font-weight: 600; }
.member-board-ranking__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.member-board-ranking__number { text-align: right; }
.member-board-ranking__footer { display: flex; min-height: 38px; padding: 0 10px; align-items: center; justify-content: space-between; border-top: 1px solid #edf1f6; color: #8491a6; background: #fff; font-size: 11px; box-sizing: border-box; }
.member-board-ranking__more { display: inline-flex; align-items: center; color: #1989fa; }
.member-board-ranking__more .van-icon { margin-left: 3px; transition: transform .18s ease; }
.member-board-ranking__more.is-open .van-icon { transform: rotate(180deg); }
.member-board-update-time { display: flex; padding: 17px 0 2px; align-items: center; justify-content: center; color: #9aa7b8; font-size: 11px; line-height: 18px; }
.member-board-update-time .van-icon { margin-right: 5px; color: #8fa1b8; font-size: 14px; }
.member-board-skeleton { padding: 12px; }
.member-board-skeleton__card { margin-bottom: 10px; padding: 16px; border-radius: 12px; background: #fff; box-sizing: border-box; box-shadow: 0 7px 20px rgba(55, 85, 126, .08); }
.member-board-skeleton__card.is-summary { height: 132px; background: #dcecff; }
.member-board-skeleton__card.is-chart { height: 224px; }
.member-board-skeleton__card .van-skeleton { padding: 0; }
@media (min-width: 600px) { .member-board-main, .member-board-skeleton { max-width: 520px; margin-right: auto; margin-left: auto; } }
@media (max-width: 350px) {
.member-board-main, .member-board-skeleton { padding-right: 10px; padding-left: 10px; }
.member-board-grid { gap: 8px; }
.member-board-mini-card { padding-right: 9px; padding-left: 9px; }
.member-board-summary__value { font-size: 26px; }
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="会员统计" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<van-row style="background-color: #f0f2f5" class="margin-top5" type="flex">
<van-col span="12" class="top-col">
<div class="col-title">会员数</div>
<div class="top-col-member-number">
{{memberNumberData.memberNum}}
<span style="font-size: 14px">&ensp;</span>
<div class="member-board-page">
<van-nav-bar title="会员统计" left-text="返回" left-arrow placeholder fixed @click-left="historyBack"></van-nav-bar>
<div v-if="pageLoading" class="member-board-skeleton">
<div class="member-board-skeleton__card is-summary"><van-skeleton title :row="3"></van-skeleton></div>
<div class="member-board-grid">
<div class="member-board-skeleton__card is-chart"><van-skeleton title :row="4"></van-skeleton></div>
<div class="member-board-skeleton__card is-chart"><van-skeleton title :row="4"></van-skeleton></div>
</div>
<div style="color: rgba(0, 0, 0, 0.85); margin-top: 20px; font-size: 14px; display: flex; align-items: center; justify-content: flex-end">
年同比 {{memberNumberData.ratio}}&ensp;
<svg
aria-hidden="true"
data-icon="caret-up"
fill="currentColor"
focusable="false"
height="1em"
style="color: #67c23a"
v-if="memberNumberData.diff>=0"
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z"
></path>
</svg>
<div class="member-board-skeleton__card is-chart"><van-skeleton title :row="5"></van-skeleton></div>
<div class="member-board-skeleton__card is-chart"><van-skeleton title :row="5"></van-skeleton></div>
</div>
<svg
aria-hidden="true"
data-icon="caret-up"
fill="currentColor"
focusable="false"
height="1em"
style="color: #f56c6c"
v-else
viewBox="0 0 1024 1024"
width="1em"
>
<path
d="M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z"
></path>
</svg>
</div>
</van-col>
<main v-show="!pageLoading" class="member-board-main">
<section class="member-board-card member-board-summary">
<div class="member-board-summary__number">
<div class="member-board-summary__label">会员总数</div>
<div class="member-board-summary__value">{{formatNumber(memberNumberData.memberNum)}}<span class="member-board-summary__unit"></span></div>
<div class="member-board-summary__ratio" :class="{'is-down': memberNumberData.diff < 0}">
年同比 {{memberNumberData.ratio}}<van-icon name="play"></van-icon>
</div>
</div>
<div class="member-board-summary__trend">
<div class="member-board-summary__trend-title">增长趋势</div>
<div id="growthTrendChart" class="member-board-summary__chart"></div>
</div>
</section>
<van-col span="12" class="top-col">
<div class="col-title">增长趋势</div>
<div class="top-col-chart" id="growthTrendChart"></div>
</van-col>
</van-row>
<section class="member-board-grid">
<div class="member-board-card member-board-mini-card">
<div class="member-board-card__title">会员占比</div>
<div class="member-board-mini-card__content">
<div id="memberPercentageChart" class="member-board-mini-chart"></div>
<div class="member-board-ring-value">{{memberPercentageText}}</div>
</div>
</div>
<div class="member-board-card member-board-mini-card">
<div class="member-board-card__title">性别占比</div>
<div class="member-board-sex-layout">
<div id="memberSexPercentageChart" class="member-board-sex-chart"></div>
<div class="member-board-sex-legend">
<div class="member-board-sex-legend__item"><span class="member-board-sex-legend__dot"></span><span class="member-board-sex-legend__name"></span><span class="member-board-sex-legend__value">{{malePercentage}}</span></div>
<div class="member-board-sex-legend__item is-woman"><span class="member-board-sex-legend__dot"></span><span class="member-board-sex-legend__name"></span><span class="member-board-sex-legend__value">{{femalePercentage}}</span></div>
</div>
</div>
</div>
</section>
<van-row style="background-color: #f0f2f5; margin-top: 5px" type="flex">
<van-col span="12" class="top-col">
<div class="col-title">会员占比</div>
<div class="top-col-chart" id="memberPercentageChart"></div>
</van-col>
<van-col span="12" class="top-col">
<div class="col-title">性别占比</div>
<div class="top-col-chart" id="memberSexPercentageChart"></div>
</van-col>
</van-row>
<van-row style="background-color: #f0f2f5; margin-top: 5px" type="flex">
<van-col span="24" class="center-col">
<div class="col-title">在职状态</div>
<div id="userStateMemberChart" style="width: 100%; height: 240px; box-sizing: border-box"></div>
<div class="col-title">人员类型</div>
<div id="personTypeMemberChart" style="width: 100%; height: 240px; box-sizing: border-box"></div>
</van-col>
</van-row>
<van-row style="background-color: #f0f2f5; margin-top: 5px" type="flex">
<van-col span="24" class="center-col">
<div class="col-title">工会会员数</div>
<div id="unionMemberChart" style="width: 100%; height: 200px"></div>
<div class="data-box">
<!-- 表头 -->
<van-row class="th-row">
<van-col class="col-index">序号</van-col>
<van-col class="col-name">工会名称</van-col>
<van-col class="col-num">会员人数</van-col>
</van-row>
<!-- 数据行 -->
<van-row
class="td-row"
v-for="(item,index) in unionMember"
:key="index"
:style="{background:index%2?'#fafafa':'#fff'}"
>
<van-col class="col-index">{{ index+1 }}</van-col>
<van-col class="col-name common-ellipsis" :title="item.unionname">
{{ item.unionname }}
</van-col>
<van-col class="col-num">{{ item.value }}</van-col>
</van-row>
</div>
</van-col>
</van-row>
<section class="member-board-card member-board-chart-card member-board-section">
<div class="member-board-card__title">在职状态分布</div>
<div id="userStateMemberChart" class="member-board-radar-chart"></div>
</section>
<section class="member-board-card member-board-chart-card member-board-section">
<div class="member-board-card__title">人员类型分布</div>
<div id="personTypeMemberChart" class="member-board-column-chart"></div>
</section>
<section class="member-board-card member-board-chart-card member-board-section">
<div class="member-board-union-head">
<div class="member-board-card__title">工会会员数统计(按工会)</div>
<div class="member-board-union-total">共 {{unionMember.length}} 个工会</div>
</div>
<div id="unionMemberChart" class="member-board-union-chart"></div>
<div class="member-board-ranking">
<div class="member-board-ranking__row is-head"><span>排名</span><span>工会名称</span><span class="member-board-ranking__number">会员人数</span></div>
<div v-for="(item, index) in visibleUnionMember" :key="item.unionCode || index" class="member-board-ranking__row">
<span>{{index + 1}}</span><span class="member-board-ranking__name" :title="item.unionname">{{item.unionname}}</span><span class="member-board-ranking__number">{{item.value}}</span>
</div>
<div class="member-board-ranking__footer">
<span>共 {{unionMember.length}} 个工会</span>
<span v-if="unionMember.length > 5" class="member-board-ranking__more" :class="{'is-open': showAllUnion}" @click="toggleUnionRanking">
{{showAllUnion ? "收起" : "查看更多"}}<van-icon name="arrow-down"></van-icon>
</span>
</div>
</div>
</section>
<div class="member-board-update-time"><van-icon name="clock-o"></van-icon>数据更新于 {{updateTime || "--"}}</div>
</main>
</div>
</div>
<script>
const chart = {
growthTrendChart: undefined,
memberPercentageChart: undefined,
memberSexPercentageChart: undefined,
unionMemberChart: undefined,
personTypeMemberChart: undefined,
userStateMemberChart: undefined
<script nonce="${cspNonce!}">
const memberBoardCharts = {
growthTrendChart: null,
memberPercentageChart: null,
memberSexPercentageChart: null,
unionMemberChart: null,
personTypeMemberChart: null,
userStateMemberChart: null
}
new Vue({
el: "#app",
data: {
loading: {
memberNumber: false,
growthTrend: false,
memberPercentage: false,
memberSexPercentage: false,
unionMember: false,
personTypeMember: false,
userStateMember: false
},
memberNumberData: {
memberNum: "--",
lastYearMemberNum: "--",
ratio: "0.0%",
diff: 0
},
memberPercentage: "--",
unionMember: []
data() {
return {
pageLoading: true,
loading: {memberNumber: false, growthTrend: false, memberPercentage: false, memberSexPercentage: false, unionMember: false, personTypeMember: false, userStateMember: false},
memberNumberData: {memberNum: 0, lastYearMemberNum: 0, ratio: "0.00%", diff: 0},
memberPercentage: 0,
growthTrendData: [],
memberSexData: [],
personTypeMemberData: [],
userStateMemberData: [],
unionChartData: [],
unionMember: [],
showAllUnion: false,
updateTime: ""
}
},
computed: {
memberPercentageText() { return (Number(this.memberPercentage || 0) * 100).toFixed(2) + "%" },
malePercentage() { return this.getSexPercentage("男") },
femalePercentage() { return this.getSexPercentage("女") },
visibleUnionMember() { return this.showAllUnion ? this.unionMember : this.unionMember.slice(0, 5) }
},
methods: {
historyBack,
/**
* 会员数
* @returns {Promise<void>}
*/
formatNumber(value) { return Number(value || 0).toLocaleString("zh-CN") },
getSexPercentage(type) {
const total = this.memberSexData.reduce((sum, item) => sum + Number(item.value || 0), 0)
const current = this.memberSexData.find((item) => item.type === type)
return total > 0 ? (Number(current ? current.value : 0) * 100 / total).toFixed(2) + "%" : "0.00%"
},
toggleUnionRanking() { this.$set(this, "showAllUnion", !this.showAllUnion) },
formatUpdateTime(date) {
const pad = (value) => String(value).padStart(2, "0")
return date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + " " + pad(date.getHours()) + ":" + pad(date.getMinutes())
},
setLoading(key, value) { this.$set(this.loading, key, value) },
getMemberNumber() {
this.loading.memberNumber = true
this.$axios.post("/platform/member/info/board/memberNumber").then((resp) => {
this.setLoading("memberNumber", true)
return this.$axios.post("/platform/member/info/board/memberNumber").then((resp) => {
if (resp.code !== 0) { this.$toast.fail(resp.msg); return }
const memberNum = Number(resp.data.memberNum || 0)
const lastYearMemberNum = Number(resp.data.lastYearMemberNum || 0)
const diff = memberNum - lastYearMemberNum
const ratio = lastYearMemberNum > 0 ? Math.abs(diff) * 100 / lastYearMemberNum : 0
this.$set(this, "memberNumberData", {memberNum, lastYearMemberNum, diff, ratio: ratio.toFixed(2) + "%"})
}).finally(() => { this.setLoading("memberNumber", false) })
},
getGrowthTrend() {
this.setLoading("growthTrend", true)
return this.$axios.post("/platform/member/info/board/growthTrend").then((resp) => {
if (resp.code === 0) this.$set(this, "growthTrendData", resp.data || [])
else this.$toast.fail(resp.msg)
}).finally(() => { this.setLoading("growthTrend", false) })
},
getMemberPercentage() {
this.setLoading("memberPercentage", true)
return this.$axios.post("/platform/member/info/board/memberPercentage").then((resp) => {
if (resp.code === 0) this.$set(this, "memberPercentage", Number(resp.data || 0))
else this.$toast.fail(resp.msg)
}).finally(() => { this.setLoading("memberPercentage", false) })
},
getMemberSexPercentage() {
this.setLoading("memberSexPercentage", true)
return this.$axios.post("/platform/member/info/board/memberSexPercentage").then((resp) => {
if (resp.code === 0) this.$set(this, "memberSexData", resp.data || [])
else this.$toast.fail(resp.msg)
}).finally(() => { this.setLoading("memberSexPercentage", false) })
},
getUnionMember() {
this.setLoading("unionMember", true)
return this.$axios.post("/platform/member/info/board/unionMember").then((resp) => {
if (resp.code !== 0) { this.$toast.fail(resp.msg); return }
const data = resp.data || {}
const tableData = (data.tableData || []).slice().sort((first, second) => Number(second.value || 0) - Number(first.value || 0))
this.$set(this, "unionChartData", data.chartData || [])
this.$set(this, "unionMember", tableData)
}).finally(() => { this.setLoading("unionMember", false) })
},
getPersonTypeMember() {
this.setLoading("personTypeMember", true)
return this.$axios.post("/platform/member/info/board/personTypeMember").then((resp) => {
if (resp.code === 0) {
const {
data: { memberNum, lastYearMemberNum }
} = resp
const diff = memberNum - lastYearMemberNum
const ratio = (lastYearMemberNum ? (Math.abs(diff) / lastYearMemberNum) * 100 : (diff / 1) * 10).toFixed(2) + " %"
this.memberNumberData = { memberNum, lastYearMemberNum, diff, ratio }
this.loading.memberNumber = false
} else {
this.$toast.fail(resp.msg)
const data = (resp.data || []).map((item) => ({label: item.name || item.label, value: Number(item.value || 0)}))
this.$set(this, "personTypeMemberData", data)
} else this.$toast.fail(resp.msg)
}).finally(() => { this.setLoading("personTypeMember", false) })
},
getUserStateMember() {
this.setLoading("userStateMember", true)
return this.$axios.post("/platform/member/info/board/userStateMember").then((resp) => {
if (resp.code === 0) {
const data = (resp.data || []).map((item) => ({label: item.name || item.label, value: Number(item.value || 0)}))
this.$set(this, "userStateMemberData", data)
} else this.$toast.fail(resp.msg)
}).finally(() => { this.setLoading("userStateMember", false) })
},
renderGrowthTrendChart() {
memberBoardCharts.growthTrendChart = new G2Plot.Line("growthTrendChart", {
data: this.growthTrendData, autoFit: true, xField: "label", yField: "value", smooth: true, color: "#ffffff", padding: [8, 5, 22, 5],
lineStyle: {lineWidth: 2.5}, point: {size: 2.5, shape: "circle", style: {fill: "#ffffff", stroke: "#ffffff"}},
xAxis: {line: null, tickLine: null, label: {style: {fill: "rgba(255,255,255,.78)", fontSize: 9}}}, yAxis: false,
tooltip: {formatter: (datum) => ({name: datum.label + "年", value: datum.value + " 人"})}
})
memberBoardCharts.growthTrendChart.render()
},
renderMemberPercentageChart() {
const percent = Math.max(0, Math.min(1, Number(this.memberPercentage || 0)))
const data = [{type: "会员", value: percent}, {type: "其他", value: 1 - percent}]
memberBoardCharts.memberPercentageChart = new G2Plot.Pie("memberPercentageChart", {
data, autoFit: true, angleField: "value", colorField: "type", radius: .86, innerRadius: .72, legend: false, label: false, tooltip: false,
color: ["#397ff0", "#edf2f8"], statistic: {title: false, content: false}
})
memberBoardCharts.memberPercentageChart.render()
},
renderMemberSexChart() {
memberBoardCharts.memberSexPercentageChart = new G2Plot.Pie("memberSexPercentageChart", {
data: this.memberSexData, autoFit: true, angleField: "value", colorField: "type", radius: .88, legend: false, label: false,
color: ["#409eff", "#f56c6c"], interactions: [{type: "element-active"}]
})
memberBoardCharts.memberSexPercentageChart.render()
},
renderUserStateChart() {
memberBoardCharts.userStateMemberChart = new G2Plot.Radar("userStateMemberChart", {
data: this.userStateMemberData, autoFit: true, xField: "label", yField: "value", appendPadding: [10, 18, 8, 18], color: "#2f78ed",
meta: {value: {alias: "会员数量", min: 0, nice: true}}, xAxis: {tickLine: null, label: {style: {fill: "#526176", fontSize: 11}}},
yAxis: {label: false, grid: {alternateColor: "#f5f8fd"}}, point: {size: 3}, area: {style: {fillOpacity: .16}}
})
memberBoardCharts.userStateMemberChart.render()
},
// 人员类型名称按每行五个中文字符拆分,保证移动端横轴展示完整内容而不是省略号。
formatPersonTypeAxisLabel(value) {
const text = String(value || "")
const lineLength = 5
const lines = []
for (let index = 0; index < text.length; index += lineLength) {
lines.push(text.substring(index, index + lineLength))
}
return lines.join("\n")
},
renderPersonTypeChart() {
memberBoardCharts.personTypeMemberChart = new G2Plot.Column("personTypeMemberChart", {
data: this.personTypeMemberData, autoFit: true, xField: "label", yField: "value", color: "#397ff0", columnWidthRatio: .42, appendPadding: [8, 4, 36, 4],
label: {position: "top", style: {fill: "#397ff0", fontSize: 10}},
xAxis: {tickLine: null, label: {autoHide: false, autoRotate: false, style: {fill: "#526176", fontSize: 9}, formatter: (value) => this.formatPersonTypeAxisLabel(value)}},
yAxis: {label: {style: {fill: "#8a97a8", fontSize: 9}}, grid: {line: {style: {stroke: "#edf1f6"}}}},
tooltip: {formatter: (datum) => ({name: datum.label, value: datum.value + " 人"})}
})
memberBoardCharts.personTypeMemberChart.render()
},
renderUnionChart() {
memberBoardCharts.unionMemberChart = new G2Plot.Column("unionMemberChart", {
data: this.unionChartData, autoFit: true, xField: "unionname", yField: "value", color: "#397ff0", columnWidthRatio: .56, appendPadding: [10, 2, 4, 2], label: false, xAxis: false,
yAxis: {label: {style: {fill: "#8a97a8", fontSize: 9}}, grid: {line: {style: {stroke: "#edf1f6"}}}},
tooltip: {formatter: (datum) => ({name: datum.unionname, value: datum.value + " 人"})}
})
memberBoardCharts.unionMemberChart.render()
},
renderCharts() {
this.destroyCharts()
this.renderGrowthTrendChart(); this.renderMemberPercentageChart(); this.renderMemberSexChart(); this.renderUserStateChart(); this.renderPersonTypeChart(); this.renderUnionChart()
},
resizeCharts() {
Object.keys(memberBoardCharts).forEach((key) => {
const container = document.getElementById(key)
if (memberBoardCharts[key] && container) {
memberBoardCharts[key].changeSize(container.clientWidth, container.clientHeight)
}
})
},
/**
* 增长趋势
* @returns {Promise<void>}
*/
async getGrowthTrend() {
this.loading.growthTrend = true
this.$axios.post("/platform/member/info/board/growthTrend").then((resp) => {
this.loading.growthTrend = false
if (resp.code === 0) {
const { data } = resp
const labels = data.map((v) => v.label)
const values = data.map((v) => v.value)
if (chart.growthTrendChart) {
chart.growthTrendChart.changeData(values)
return
}
chart.growthTrendChart = new G2Plot.TinyArea("growthTrendChart", {
autoFit: true,
data: values,
smooth: true,
showContent: true,
areaStyle: {
fill: "#d6e3fd"
},
tooltip: {
customContent: function (i, data) {
if (labels[i]) {
return labels[i] + "-" + data[0]?.data?.y + "人"
}
return ""
}
}
})
chart.growthTrendChart.render()
} else {
this.$toast.fail(resp.msg)
}
destroyCharts() {
Object.keys(memberBoardCharts).forEach((key) => {
if (memberBoardCharts[key]) { memberBoardCharts[key].destroy(); memberBoardCharts[key] = null }
})
},
/**
* 会员占比
* @returns {Promise<void>}
*/
async getMemberPercentage() {
this.loading.memberPercentage = true
this.$axios.post("/platform/member/info/board/memberPercentage").then((resp) => {
this.loading.memberPercentage = false
if (resp.code === 0) {
const { data } = resp
this.memberPercentage = data.toFixed(2)
if (chart.memberPercentageChart) {
chart.memberPercentageChart.changeData(data)
return
}
chart.memberPercentageChart = new G2Plot.Liquid("memberPercentageChart", {
outline: {
border: 4,
distance: 8
},
wave: {
length: 128
},
autoFit: true,
percent: data,
statistic: {
content: {
style: {
fontSize: 16
}
}
}
})
chart.memberPercentageChart.render()
} else {
this.$toast.fail(resp.msg)
}
})
},
/**
* 男女占比
* @returns {Promise<void>}
*/
async getMemberSexPercentage() {
this.loading.memberSexPercentage = true
this.$axios.post("/platform/member/info/board/memberSexPercentage").then((resp) => {
this.loading.memberSexPercentage = false
if (resp.code === 0) {
const { data } = resp
if (chart.memberSexPercentageChart) {
chart.memberSexPercentageChart.changeData(data)
return
}
chart.memberSexPercentageChart = new G2Plot.Pie("memberSexPercentageChart", {
data,
angleField: "value",
colorField: "type",
legend: false,
label: {
formatter: () => ""
},
interactions: [{ type: "element-active" }],
color: ["#409EFF", "#F56C6C"]
})
chart.memberSexPercentageChart.render()
} else {
this.$toast.fail(resp.msg)
}
})
},
/**
* 各工会会员数
* @returns {Promise<void>}
*/
async getUnionMember() {
this.loading.unionMember = true
this.$axios.post("/platform/member/info/board/unionMember").then((resp) => {
this.loading.unionMember = false
if (resp.code === 0) {
const { chartData, tableData } = resp.data
this.unionMember = tableData
const labels = chartData.map((v) => v.unionname)
const values = chartData.map((v) => v.value)
if (chart.unionMemberChart) {
chart.unionMemberChart.changeData(values)
return
}
chart.unionMemberChart = new G2Plot.Column("unionMemberChart", {
data: chartData,
autoFit: true,
xField: "unionname",
yField: "value",
label: {
// 可手动配置 label 数据标签位置
position: "middle", // 'top', 'bottom', 'middle',
// 配置样式
style: {
fill: "#FFFFFF",
opacity: 0.6
}
},
xAxis: {
label: {
autoHide: true,
autoRotate: false
}
},
meta: {
label: {
alias: "类型"
},
value: {
alias: "会员数"
}
}
})
chart.unionMemberChart.render()
} else {
this.$message.warning(resp.msg)
}
})
},
/**
* 根据人员类型查找会员数
* @returns {Promise<void>}
*/
async getPersonTypeMember() {
this.loading.personTypeMember = true
this.$axios.post("/platform/member/info/board/personTypeMember").then((resp) => {
this.loading.personTypeMember = false
if (resp.code === 0) {
const { data } = resp
if (chart.personTypeMemberChart) {
chart.personTypeMemberChart.changeData(data)
return
}
chart.personTypeMemberChart = new G2Plot.Column("personTypeMemberChart", {
data,
autoFit: true,
xField: "label",
yField: "value",
label: {
// 可手动配置 label 数据标签位置
position: "middle", // 'top', 'bottom', 'middle',
// 配置样式
style: {
fill: "#FFFFFF",
opacity: 0.6
}
},
xAxis: {
label: {
autoHide: true,
autoRotate: false
}
},
meta: {
label: {
alias: "类型"
},
value: {
alias: "会员数"
}
}
})
chart.personTypeMemberChart.render()
} else {
this.$toast.fail(resp.msg)
}
})
},
/**
* 根据在职状态查找会员数
* @returns {Promise<void>}
*/
async getUserStateMember() {
this.loading.userStateMember = true
this.$axios.post("/platform/member/info/board/userStateMember").then((resp) => {
this.loading.userStateMember = false
if (resp.code === 0) {
const { data } = resp
if (chart.userStateMemberChart) {
chart.userStateMemberChart.changeData(data)
return
}
chart.userStateMemberChart = new G2Plot.Radar("userStateMemberChart", {
data,
xField: "label",
yField: "value",
appendPadding: [0, 10, 0, 10],
meta: {
value: {
alias: "会员数量",
min: 0,
nice: true,
formatter: (v) => parseInt(v)
}
},
xAxis: {
tickLine: null
},
yAxis: {
label: false,
grid: {
alternateColor: "rgba(0, 0, 0, 0.04)"
}
},
// 开启辅助点
point: {
size: 2
},
area: {}
})
chart.userStateMemberChart.render()
} else {
this.$toast.fail(resp.msg)
}
})
},
initData() {
this.getMemberNumber()
this.getGrowthTrend()
this.getMemberPercentage()
this.getMemberSexPercentage()
this.getUnionMember()
this.getPersonTypeMember()
this.getUserStateMember()
const requests = [this.getMemberNumber(), this.getGrowthTrend(), this.getMemberPercentage(), this.getMemberSexPercentage(), this.getUnionMember(), this.getPersonTypeMember(), this.getUserStateMember()]
Promise.all(requests).then(() => { this.$set(this, "updateTime", this.formatUpdateTime(new Date())) }).finally(() => {
this.$set(this, "pageLoading", false)
this.$nextTick(() => { this.renderCharts() })
})
}
},
created() {
this.initData()
}
mounted() { window.addEventListener("resize", this.resizeCharts); this.initData() },
beforeDestroy() { window.removeEventListener("resize", this.resizeCharts); this.destroyCharts() }
})
</script>
<!--#
@@ -1,53 +1,332 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<style></style>
<style id="style-member-info-group-h5">
<!--#include("../../theme.css"){}#-->
body,
.member-archive-page {
background-color: #f3f7fd;
}
.member-archive-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.member-archive-page .van-nav-bar {
background-color: #fff;
}
.member-archive-page .van-nav-bar__title {
color: #172033;
font-size: 16px;
font-weight: 600;
}
.member-archive-page .van-nav-bar .van-icon,
.member-archive-page .van-nav-bar__text {
color: #1989fa;
}
.member-archive-sticky {
padding: 22px 12px 1px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.member-archive-search {
margin: 0 0 8px;
padding: 0;
overflow: hidden;
border-radius: 12px;
background-color: #fff;
box-shadow: 0 5px 16px rgba(43, 73, 112, .1);
}
.member-archive-search .van-search__content {
height: 42px;
padding-left: 12px;
align-items: center;
border-radius: 12px;
background-color: #fff;
}
.member-archive-search .van-field__control {
color: #263548;
font-size: 14px;
}
.member-archive-search .van-field__control::placeholder {
color: #a7b0bf;
}
.member-archive-filter {
margin: 0 0 10px;
overflow: hidden;
border-radius: 10px;
box-shadow: 0 5px 16px rgba(43, 73, 112, .08);
transition: border-radius .18s ease;
}
.member-archive-filter.is-open {
border-radius: 10px 10px 0 0;
}
.member-archive-filter .van-dropdown-menu__bar,
.member-archive-filter .van-dropdown-menu__item,
.member-archive-filter .van-dropdown-menu__title {
height: 44px;
}
.member-archive-filter .van-dropdown-menu__bar {
box-shadow: none;
}
.member-archive-filter .van-dropdown-menu__title {
display: inline-flex;
align-items: center;
color: #66758a;
font-size: 14px;
line-height: 20px;
}
.member-archive-filter .van-dropdown-menu__title--active,
.member-archive-filter .van-dropdown-item__option--active,
.member-archive-filter .van-dropdown-item__option--active .van-dropdown-item__icon {
color: #1989fa;
}
.member-archive-filter .van-dropdown-item__content {
width: calc(100% - 24px);
margin: 0 12px;
overflow: hidden;
box-sizing: border-box;
border-radius: 0 0 12px 12px;
box-shadow: 0 10px 24px rgba(31, 65, 108, .14);
}
.member-archive-filter .van-dropdown-item__option {
display: flex;
min-height: 48px;
padding: 0 16px;
align-items: center;
color: #46566c;
font-size: 14px;
}
.member-archive-content {
padding: 0 12px;
}
.member-archive-summary {
display: inline-flex;
height: 28px;
margin: 0 0 10px;
padding: 0 11px;
align-items: center;
border-radius: 14px;
color: #397de0;
background-color: #e8f2ff;
font-size: 12px;
line-height: 28px;
}
.member-archive-list .table-list-container {
margin-top: 0;
}
.member-archive-list .empty-state {
display: none;
}
.member-archive-empty {
display: flex;
min-height: 280px;
padding: 46px 18px 36px;
align-items: center;
justify-content: center;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.member-archive-empty img {
display: block;
width: 86%;
max-width: 290px;
height: auto;
object-fit: contain;
}
.member-archive-empty__title {
margin-top: 14px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 600;
}
.member-archive-empty__hint {
margin-top: 5px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.member-archive-card-header {
display: flex;
margin-bottom: 7px;
align-items: flex-start;
justify-content: space-between;
}
.member-archive-card-heading {
min-width: 0;
padding-right: 10px;
flex: 1;
}
.member-archive-card-title {
overflow: hidden;
color: #253247;
font-size: 15px;
line-height: 22px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.member-archive-card-code {
margin-top: 1px;
color: #9ba6b6;
font-size: 12px;
line-height: 18px;
}
.member-archive-status {
display: inline-flex;
min-height: 24px;
padding: 3px 9px;
align-items: center;
box-sizing: border-box;
border-radius: 12px;
color: #1989fa;
background-color: #eaf4ff;
font-size: 12px;
line-height: 18px;
white-space: nowrap;
}
.member-archive-page .table-list-container .table-list-item .action-btn .van-icon {
margin-right: 6px;
font-size: 15px;
}
.member-archive-page-skeleton {
min-height: calc(100vh - 46px);
padding: 22px 12px;
box-sizing: border-box;
background-color: #f3f7fd;
}
.member-archive-page-skeleton__filter,
.member-archive-page-skeleton__card {
margin-bottom: 10px;
padding: 14px;
border-radius: 12px;
background-color: #fff;
box-sizing: border-box;
box-shadow: 0 5px 16px rgba(43, 73, 112, .07);
}
.member-archive-page-skeleton__filter {
min-height: 96px;
}
.member-archive-page-skeleton__card {
min-height: 154px;
}
.member-archive-page-skeleton .van-skeleton {
padding: 0;
}
@media (max-width: 350px) {
.member-archive-content,
.member-archive-sticky {
padding-right: 10px;
padding-left: 10px;
}
}
</style>
<div id="app" v-cloak>
<van-nav-bar title="会员查询" left-text="返回" left-arrow @click-left="historyBack" placeholder fixed></van-nav-bar>
<van-sticky offset-top="46px">
<van-search
v-model="pageForm.searchKeyword"
placeholder="请输入工号或者姓名进行查询"
show-action
:reverse-color="false"
input-align="left"
@search="doSearch">
<template #action>
<div @click="doSearch">搜索</div>
</template>
</van-search>
<van-dropdown-menu :close-on-click-outside="false" :close-on-click-overlay="false">
<van-dropdown-item :options="unionList" @change="unionChange"
v-model="pageForm.unionId"></van-dropdown-item>
<van-dropdown-item :options="unitList" @change="doSearch" v-model="pageForm.unitId"></van-dropdown-item>
</van-dropdown-menu>
</van-sticky>
<div class="member-h5-page member-archive-page">
<van-nav-bar title="会员档案" left-text="返回" left-arrow @click-left="handlePageBack" placeholder fixed></van-nav-bar>
<div class="member-archive-page-skeleton" v-if="pageLoading">
<div class="member-archive-page-skeleton__filter"><van-skeleton title :row="2"></van-skeleton></div>
<div class="member-archive-page-skeleton__cards">
<div class="member-archive-page-skeleton__card" v-for="index in 3" :key="'archive-page-skeleton-' + index">
<van-skeleton title :row="4"></van-skeleton>
</div>
</div>
</div>
<div v-show="!pageLoading">
<van-sticky :offset-top="46">
<div class="member-archive-sticky">
<van-search v-model="pageForm.searchKeyword" class="member-archive-search"
placeholder="请输入工号或姓名搜索" shape="round" clearable
@search="doSearch" @clear="doSearch"></van-search>
<van-dropdown-menu class="member-archive-filter" :class="{'is-open': filterOpened}"
active-color="#1989fa" :close-on-click-outside="false"
:close-on-click-overlay="false">
<van-dropdown-item v-model="pageForm.unionId" :options="unionList" @change="unionChange"
@open="setFilterOpened(true)" @close="setFilterOpened(false)"></van-dropdown-item>
<van-dropdown-item v-model="pageForm.unitId" :options="unitList" @change="doSearch"
@open="setFilterOpened(true)" @close="setFilterOpened(false)"></van-dropdown-item>
</van-dropdown-menu>
</div>
</van-sticky>
<table-list api="/platform/member/info/group/pageData" :page_form.sync="pageForm" ref="tableListRef" @ready="onReady">
<main class="member-archive-content">
<div class="member-archive-summary">共 {{pageForm.totalCount}} 名会员</div>
<div class="member-archive-list">
<div v-if="pageForm.totalCount === 0" class="member-archive-empty">
<img src="/assets/mobile/img/home/v4/app-center-empty-v4.png" alt="暂无会员档案">
<div class="member-archive-empty__title">暂无会员档案</div>
<div class="member-archive-empty__hint">请尝试调整工会、单位或搜索关键词</div>
</div>
<table-list v-show="!initialLoading" api="/platform/member/info/group/pageData" :page_form.sync="pageForm" ref="tableListRef" title="username" :show_loading_overlay="false" @loading-change="onListLoadingChange" @ready="onReady">
<template #header="{index,row}">
<div class="member-archive-card-header">
<div class="member-archive-card-heading">
<div class="member-archive-card-title">{{row.username || '--'}}</div>
<div class="member-archive-card-code">工号:{{row.loginname || '--'}}</div>
</div>
<span class="member-archive-status">会员</span>
</div>
</template>
<template v-slot="{index,row}">
<table-column label="姓名">{{row.username}}</table-column>
<table-column label="工号">{{row.loginname}}</table-column>
<table-column label="性别">{{row.sex}}</table-column>
<table-column label="所属工会">{{row.unionName}}</table-column>
<table-column label="所属单位">{{row.unitName}}</table-column>
<table-column label="性别">{{row.sex || '--'}}</table-column>
<table-column label="所属工会">{{row.unionName || '--'}}</table-column>
<table-column label="所属单位">{{row.unitName || '--'}}</table-column>
</template>
<template #actions="{index,row}">
<div class="action-btn" @click="viewInfo(row)">
<i class="fa fa-eye"></i>
<van-icon name="eye-o"></van-icon>
<span>查看</span>
</div>
</template>
</table-list>
</div>
</main>
<div :style="{color:'#1890ff'}" style="text-align: right; position: fixed; bottom: 20px; right: 20px">
<van-button :color="'#1890ff'" hairline size="small" type="primary">总人数:{{pageForm.totalCount}}人
</van-button>
</div>
<van-action-sheet v-model="infoShow" title="会员详细信息">
<van-action-sheet v-model="infoShow" class="member-info-sheet" title="会员详细信息" :close-on-click-overlay="false" @close="handleInfoSheetClose">
<member-info ref="memberInfoRef" :user-id="userId"></member-info>
</van-action-sheet>
</div>
</div>
</div>
<script nonce="${cspNonce!}">
<!--#include("../../common/mobileMemberInfo.js"){}#-->
@@ -67,69 +346,127 @@ layout("/layouts/platform_h5.html"){
unitList: [],
userId: "",
infoShow: false
infoShow: false,
initialLoading: true,
pageLoading: true,
filterOpened: false,
filterReady: false,
tableReady: false,
initialSearchStarted: false,
historyLayerPageKey: "member-info-group"
}
},
components: {
"member-info": MOBILE_MEMBER_INFO
},
methods: {
historyBack,
// 同步筛选弹层展开状态,用于保持与提案预审核一致的筛选栏圆角。
setFilterOpened(opened) {
this.$set(this, "filterOpened", opened)
},
handlePageBack() {
if (window.h5HistoryLayerManager.stack.length) {
window.h5HistoryLayerManager.close()
return
}
historyBack()
},
registerHistoryLayers() {
// 会员详情作为页面唯一弹层,返回键先关闭详情再离开会员档案页面。
window.h5HistoryLayerManager.register(this.historyLayerPageKey, (stack) => {
this.$set(this, "infoShow", stack.includes("member-info"))
})
},
onReady() {
this.$set(this, "tableReady", true)
this.tryInitialSearch()
},
// 工会、单位筛选项与列表组件都完成初始化后,只触发一次首次查询。
tryInitialSearch() {
if (!this.filterReady || !this.tableReady || this.initialSearchStarted) return
this.$set(this, "initialSearchStarted", true)
this.doSearch()
},
viewInfo(row) {
this.userId = row.id
this.infoShow = true
this.$set(this, "userId", row.id)
window.h5HistoryLayerManager.open("member-info")
},
handleInfoSheetClose() {
if (window.h5HistoryLayerManager.stack.includes("member-info")) {
window.h5HistoryLayerManager.close("member-info")
}
},
onListLoadingChange(state) {
// 首屏和重新筛选时以卡片骨架替代旧列表,避免数据切换期间残留。
if (state.initial) {
this.$set(this, "initialLoading", state.loading)
if (!state.loading && this.initialSearchStarted) {
this.$set(this, "pageLoading", false)
}
}
},
unionChange() {
this.$set(this.pageForm, "unitId", null)
this.flushUnits()
this.doSearch()
this.flushUnits().then(() => {
this.doSearch()
})
},
doSearch() {
this.$nextTick(() => {
this.pageForm.pageNumber = 1
this.pageForm.totalCount = 0
this.$set(this.pageForm, "pageNumber", 1)
this.$set(this.pageForm, "totalCount", 0)
this.$refs.tableListRef.doSearch()
})
},
async initUnion() {
initUnion() {
const hasAdmin = this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
const unionId = hasAdmin ? null : this.$store.state.user.union.id
this.unionList = await this.$businessTool.listUnion(unionId)
this.unionList.forEach((v) => {
v.text = v.name
v.value = v.id
return this.$businessTool.listUnion(unionId).then((data) => {
this.unionList = data
this.unionList.forEach((v) => {
v.text = v.name
v.value = v.id
})
if (hasAdmin) {
this.unionList.unshift({ text: "全部工会", value: null })
}
if (this.unionList.length > 0) {
this.$set(this.pageForm, "unionId", this.unionList[0].value)
}
})
if (hasAdmin) {
this.unionList.unshift({ text: "全部工会", value: null })
}
if (this.unionList && this.unionList.length > 0) {
this.$set(this.pageForm, "unionId", this.unionList[0].value)
}
},
async flushUnits() {
flushUnits() {
const hasAdmin = this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
const unionId = hasAdmin ? this.pageForm.unionId : this.$store.state.user.union.id
this.unitList = await this.$businessTool.listUnit(unionId)
this.unitList.forEach((v) => {
v.text = v.name
v.value = v.id
return this.$businessTool.listUnit(unionId).then((data) => {
this.unitList = data
this.unitList.forEach((v) => {
v.text = v.name
v.value = v.id
})
if (hasAdmin && !this.pageForm.unionId) {
this.unitList.unshift({ text: "全部单位", value: null })
}
if (this.unitList.length > 0) {
this.$set(this.pageForm, "unitId", this.unitList[0].value)
}
})
if (hasAdmin && !this.pageForm.unionId) {
this.unitList.unshift({ text: "全部单位", value: null })
}
if (this.unitList && this.unitList.length > 0) {
this.$set(this.pageForm, "unitId", this.unitList[0].value)
}
},
},
created() {
this.initUnion()
this.flushUnits()
this.registerHistoryLayers()
this.initUnion().then(() => this.flushUnits()).finally(() => {
this.$set(this, "filterReady", true)
this.tryInitialSearch()
})
},
beforeDestroy() {
window.h5HistoryLayerManager.unregister(this.historyLayerPageKey)
}
})
</script>
@@ -0,0 +1,495 @@
:root {
--member-primary: #1989fa;
--member-page-bg: #f3f7fd;
--member-text: #263548;
--member-muted: #8c98aa;
--member-border: #e8eef6;
--color-primary: #1989fa;
}
body {
background: var(--member-page-bg);
}
.member-h5-page {
min-height: 100vh;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
box-sizing: border-box;
color: var(--member-text);
background: var(--member-page-bg);
}
.member-h5-page.has-fixed-actions {
padding-bottom: calc(78px + env(safe-area-inset-bottom));
}
.member-h5-page .van-nav-bar {
background: #fff;
}
.member-h5-page .van-nav-bar__title {
color: #1f2937;
font-size: 16px;
font-weight: 600;
}
.member-h5-page .van-nav-bar .van-icon,
.member-h5-page .van-nav-bar__text {
color: var(--member-primary);
}
.member-hero {
position: relative;
height: 116px;
padding: 18px 18px 0;
overflow: hidden;
box-sizing: border-box;
background-color: #edf5ff;
background-image: url("/assets/mobile/img/home/v4/hero-campus.png");
background-position: 66% 46%;
background-size: cover;
}
.member-hero::after {
position: absolute;
inset: 0;
background: rgba(242, 247, 255, .76);
content: "";
}
.member-hero__content {
position: relative;
z-index: 1;
}
.member-hero__title {
color: var(--member-primary);
font-size: 23px;
font-weight: 700;
line-height: 30px;
letter-spacing: 1px;
}
.member-hero__subtitle {
margin-top: 2px;
color: #718096;
font-size: 11px;
line-height: 18px;
}
.member-page-main {
position: relative;
z-index: 2;
margin-top: -34px;
padding: 0 12px;
}
.member-toolbar {
padding: 10px;
border: 1px solid var(--member-border);
border-radius: 12px;
background: rgba(255, 255, 255, .98);
box-shadow: 0 5px 16px rgba(43, 73, 112, .09);
}
.member-toolbar .van-search {
padding: 0;
background: transparent;
}
.member-toolbar .van-search__content {
height: 42px;
align-items: center;
border-radius: 10px;
background: #f5f8fc;
}
.member-toolbar .van-field__control {
color: var(--member-text);
font-size: 14px;
}
.member-toolbar .van-search__action {
padding: 0 4px 0 12px;
color: var(--member-primary);
font-size: 14px;
}
.member-toolbar .van-dropdown-menu {
margin-top: 8px;
/* Vant 下拉选项弹层位于菜单内部,吸顶状态下必须允许弹层超出筛选卡片显示。 */
overflow: visible;
border-top: 1px solid #edf1f6;
}
.member-toolbar .van-dropdown-menu__bar {
height: 42px;
background: transparent;
}
.member-toolbar .van-dropdown-menu__title {
color: #66758a;
font-size: 13px;
}
.member-list-section {
margin-top: 10px;
}
.member-list-section .table-list-container {
margin-top: 0;
}
.member-list-section .table-list-item {
border-color: var(--member-border) !important;
border-radius: 12px !important;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08) !important;
}
.member-list-section .item-title {
color: #263548 !important;
font-size: 15px !important;
}
.member-list-section .action-btn {
color: var(--member-primary) !important;
background: #eaf4ff !important;
}
.member-list-section .action-btn.delete {
color: #e95757 !important;
background: #fff0f0 !important;
}
.member-empty {
display: flex;
min-height: 280px;
padding: 20px 18px 30px;
align-items: center;
justify-content: flex-start;
flex-direction: column;
box-sizing: border-box;
text-align: center;
}
.member-empty img {
display: block;
width: 84%;
max-width: 290px;
height: auto;
object-fit: contain;
}
.member-empty__title {
margin-top: -8px;
color: #50627a;
font-size: 15px;
line-height: 22px;
font-weight: 500;
}
.member-empty__hint {
margin-top: 6px;
color: #9aa7b8;
font-size: 12px;
line-height: 18px;
}
.member-list-skeleton {
padding-top: 10px;
}
.member-list-skeleton__card {
min-height: 154px;
margin-bottom: 10px;
padding: 14px;
border-radius: 12px;
background: #fff;
box-sizing: border-box;
box-shadow: 0 7px 20px rgba(55, 85, 126, .08);
}
.member-list-skeleton__card .van-skeleton {
padding: 0;
}
.member-list-skeleton__card .van-skeleton__title {
height: 17px;
}
.member-list-skeleton__card .van-skeleton__row {
height: 12px;
margin-top: 13px;
}
/* 页面首次进入时统一遮住尚未完成初始化的横幅、筛选栏和列表,避免返回页面时出现空筛选项。 */
.member-page-skeleton {
min-height: calc(100vh - 46px);
padding: 12px;
box-sizing: border-box;
background: var(--member-page-bg);
}
.member-page-skeleton__hero {
height: 82px;
margin-bottom: 10px;
padding: 18px;
border-radius: 12px;
background: linear-gradient(135deg, #e4f1ff 0%, #f2f7ff 100%);
box-sizing: border-box;
}
.member-page-skeleton__toolbar {
min-height: 96px;
margin-bottom: 10px;
padding: 14px;
border-radius: 12px;
background: #fff;
box-sizing: border-box;
box-shadow: 0 5px 16px rgba(43, 73, 112, .07);
}
.member-page-skeleton__toolbar .van-skeleton,
.member-page-skeleton__hero .van-skeleton {
padding: 0;
}
.member-page-skeleton__cards .member-list-skeleton {
padding-top: 0;
}
.member-count-chip {
display: inline-flex;
height: 28px;
margin: 10px 0 0;
padding: 0 11px;
align-items: center;
border-radius: 14px;
color: #397de0;
background: #edf5ff;
font-size: 12px;
}
.member-form-intro {
display: flex;
min-height: 58px;
margin-bottom: 10px;
padding: 11px 12px;
align-items: flex-start;
box-sizing: border-box;
border: 1px solid #dfeafb;
border-radius: 10px;
background: rgba(246, 250, 255, .98);
box-shadow: 0 3px 10px rgba(53, 98, 154, .08);
}
.member-form-intro .van-icon {
margin: 1px 9px 0 0;
color: var(--member-primary);
font-size: 19px;
}
.member-form-intro__title {
color: var(--member-primary);
font-size: 12px;
line-height: 18px;
font-weight: 600;
}
.member-form-intro__text {
margin-top: 2px;
color: #8a97a8;
font-size: 10px;
line-height: 17px;
}
.member-application-form.form-container {
padding-bottom: 0;
background: transparent;
}
.member-application-form .form-section {
margin-bottom: 10px;
overflow: hidden;
border: 1px solid var(--member-border);
border-radius: 12px;
background: #fff;
box-shadow: 0 5px 16px rgba(55, 85, 126, .07);
}
.member-application-form .van-cell-group__title {
padding: 12px 14px 8px 21px;
color: #354255;
background: #fff;
font-size: 13px;
line-height: 20px;
}
.member-application-form .van-cell-group__title::before {
top: 15px;
bottom: 11px;
left: 12px;
width: 2px;
border-radius: 2px;
}
.member-application-form .van-cell {
min-height: 44px;
padding: 10px 14px;
background: #fff;
}
.member-application-form .van-field__label {
width: 84px;
color: #596779;
font-size: 12px;
line-height: 24px;
}
.member-application-form .van-field__control,
.member-application-form .van-field__value {
color: #4f5d70;
font-size: 12px;
line-height: 24px;
}
.member-form-actions,
.member-approval-actions {
position: fixed;
right: 0;
bottom: 0;
left: 0;
z-index: 100;
display: flex;
gap: 9px;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
border-top: 1px solid #edf1f6;
background: rgba(255, 255, 255, .98);
box-sizing: border-box;
box-shadow: 0 -4px 14px rgba(50, 78, 112, .06);
}
.member-form-actions .van-button,
.member-approval-actions .van-button {
height: 42px;
margin: 0;
border-radius: 14px;
font-size: 14px;
}
.member-form-actions .member-save-button {
color: var(--member-primary);
border-color: var(--member-primary);
background: #fff;
}
.member-form-actions .member-submit-button,
.member-approval-actions .member-agree-button {
color: #fff;
border-color: var(--member-primary);
background: var(--member-primary);
}
.member-approval-actions .member-return-button {
color: #fff;
border-color: #e6a23c;
background: #e6a23c;
}
.member-form-skeleton {
padding: 0 12px 24px;
}
.member-form-skeleton__card {
min-height: 200px;
margin-bottom: 10px;
padding: 16px 14px;
border-radius: 12px;
background: #fff;
box-sizing: border-box;
}
.member-form-skeleton__card .van-skeleton {
padding: 0;
}
.member-info-sheet {
height: 88%;
overflow: hidden;
border-radius: 16px 16px 0 0;
background: var(--member-page-bg);
}
.member-info-sheet .van-action-sheet__header {
height: 48px;
color: #1f2937;
background: #fff;
font-size: 16px;
line-height: 48px;
font-weight: 600;
}
.member-info-scroll {
height: calc(100% - 48px);
padding: 12px 12px calc(82px + env(safe-area-inset-bottom));
overflow-y: auto;
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
}
.member-detail-card {
margin-bottom: 10px;
overflow: hidden;
border-radius: 12px;
background: #fff;
box-shadow: 0 5px 16px rgba(55, 85, 126, .07);
}
.member-detail-card__title {
position: relative;
padding: 12px 14px 8px 21px;
color: #354255;
font-size: 13px;
line-height: 20px;
font-weight: 600;
}
.member-detail-card__title::before {
position: absolute;
top: 15px;
bottom: 11px;
left: 12px;
width: 2px;
border-radius: 2px;
background: var(--member-primary);
content: "";
}
.member-detail-card .van-cell {
padding: 10px 14px;
color: #4f5d70;
font-size: 12px;
}
.member-detail-card .van-cell__title {
color: #7b899b;
}
@media (min-width: 600px) {
.member-hero__content,
.member-page-main,
.member-form-skeleton {
max-width: 520px;
margin-right: auto;
margin-left: auto;
box-sizing: border-box;
}
.member-form-actions,
.member-approval-actions {
right: 50%;
left: auto;
width: 520px;
transform: translateX(50%);
}
}