feat: 同步地大代码

This commit is contained in:
2026-10-08 15:33:01 +08:00
parent ae37d48657
commit 4cd992fb28
116 changed files with 5824 additions and 2179 deletions
@@ -0,0 +1,19 @@
-- 系统用户增加福利享受截止时间,字段保持可空以兼容历史人员数据。
-- 脚本支持重复执行,字段已存在时不再修改表结构。
SET @welfare_stop_date_column_count = (
SELECT COUNT(*)
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = 'sys_user'
AND COLUMN_NAME = 'welfareStopDate'
);
SET @welfare_stop_date_ddl = IF(
@welfare_stop_date_column_count = 0,
'ALTER TABLE sys_user ADD COLUMN welfareStopDate DATE NULL COMMENT ''福利享受截止时间'' AFTER retireDate',
'SELECT 1'
);
PREPARE welfare_stop_date_stmt FROM @welfare_stop_date_ddl;
EXECUTE welfare_stop_date_stmt;
DEALLOCATE PREPARE welfare_stop_date_stmt;
@@ -0,0 +1,202 @@
/* 首页服务门户布局单独限定作用域,避免影响应用中心和其他业务页。 */
#v4-home-app.home-portal {
width: 100%;
max-width: none;
margin: 0;
color: #20344d;
background: #f2f6fb;
font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
font-size: 14px;
}
#v4-home-app.home-portal * { box-sizing: border-box; }
#v4-home-app .portal-top { display: grid; grid-template-columns: minmax(0, 1fr) 340px; gap: 18px; max-width: 1720px; margin: auto; padding: 18px 24px 0; }
/* 使用完整不透明背景铺满横幅,会员信息独立成卡,不再覆盖校园景观。 */
#v4-home-app .portal-hero { position: relative; min-width: 0; min-height: 260px; overflow: hidden; border-radius: 12px; background: #e8f4ff url('/assets/platform/img/home/service-portal-banner-campus-v4.png') no-repeat right bottom / cover; }
#v4-home-app .portal-hero::after { content: ""; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(90deg, #e8f4ff 0%, #e8f4ff 28%, rgba(232,244,255,.92) 39%, rgba(232,244,255,.35) 54%, rgba(232,244,255,0) 68%); }
#v4-home-app .portal-hero-inner { position: relative; z-index: 1; display: flex; align-items: center; min-height: 260px; height: 100%; padding: 32px 40px; }
#v4-home-app .portal-hero-copy { max-width: 80%; }
#v4-home-app .portal-hero-eyebrow { margin-bottom: 12px; color: #527ca1; font-size: 14px; line-height: 22px; letter-spacing: 2px; }
#v4-home-app .portal-hero-copy h1 { margin: 0 0 10px; font-size: 36px; line-height: 1.3; font-weight: 700; color: #083765; }
#v4-home-app .portal-hero-copy p { margin: 0; color: #335675; font-size: 17px; line-height: 1.8; }
#v4-home-app .portal-values { display: flex; flex-wrap: wrap; gap: 22px 36px; margin-top: 24px; }
#v4-home-app .portal-values span { display: inline-flex; align-items: center; gap: 12px; }
#v4-home-app .portal-values i { color: #3599f2; font-size: 23px; }
/* 首次渲染时分层淡入文字,只改变透明度和位移,不影响卡片布局。 */
@keyframes portal-text-enter {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
#v4-home-app .portal-hero-eyebrow,
#v4-home-app .portal-hero-copy h1,
#v4-home-app .portal-hero-copy p,
#v4-home-app .portal-values > span,
#v4-home-app .portal-member-date,
#v4-home-app .portal-member-heading,
#v4-home-app .portal-member .meta-row { animation: portal-text-enter .6s cubic-bezier(.22, 1, .36, 1) backwards; }
#v4-home-app .portal-hero-copy h1 { animation-delay: .08s; }
#v4-home-app .portal-hero-copy p { animation-delay: .16s; }
#v4-home-app .portal-values > span:nth-child(1) { animation-delay: .24s; }
#v4-home-app .portal-values > span:nth-child(2) { animation-delay: .30s; }
#v4-home-app .portal-values > span:nth-child(3) { animation-delay: .36s; }
#v4-home-app .portal-values > span:nth-child(4) { animation-delay: .42s; }
#v4-home-app .portal-member-date { animation-delay: .10s; }
#v4-home-app .portal-member-heading { animation-delay: .18s; }
#v4-home-app .portal-member .meta-row:nth-child(1) { animation-delay: .26s; }
#v4-home-app .portal-member .meta-row:nth-child(2) { animation-delay: .34s; }
#v4-home-app .portal-member .meta-row:nth-child(3) { animation-delay: .42s; }
@media (prefers-reduced-motion: reduce) {
#v4-home-app .portal-hero-eyebrow,
#v4-home-app .portal-hero-copy h1,
#v4-home-app .portal-hero-copy p,
#v4-home-app .portal-values > span,
#v4-home-app .portal-member-date,
#v4-home-app .portal-member-heading,
#v4-home-app .portal-member .meta-row { animation: none; }
}
#v4-home-app .portal-content { max-width: 1720px; margin: auto; padding: 18px 24px 28px; }
/* 同行共用网格行高,统一横纵间距;待办使用紧凑两行信息,避免撑出大块留白。 */
#v4-home-app .portal-columns { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 18px; margin-top: 18px; align-items: stretch; }
#v4-home-app .portal-columns > * { min-width: 0; }
#v4-home-app .portal-news-wide { grid-column: 1 / -1; }
#v4-home-app .portal-panel,
#v4-home-app .entry-wrapper,
#v4-home-app .todo-center,
#v4-home-app .work-template-wrapper,
#v4-home-app .jcdt-container { background: #fff; border: 0; border-radius: 12px; padding: 20px; box-shadow: 0 3px 14px rgba(30, 66, 108, .035); }
#v4-home-app .home-section-header,
#v4-home-app .portal-todo-heading,
#v4-home-app .work-template-header,
#v4-home-app .portal-news-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; min-height: 28px; margin: 0 0 16px; }
#v4-home-app .home-section-title,
#v4-home-app .work-template-title,
#v4-home-app .portal-news-title,
#v4-home-app .portal-todo-heading h3 { display: inline-flex; align-items: center; gap: 9px; margin: 0; padding: 0; border: 0; font-size: 20px; line-height: 1.4; font-weight: 600; color: #123356; }
#v4-home-app .home-section-title i,
#v4-home-app .work-template-title i,
#v4-home-app .portal-news-title > i:first-child,
#v4-home-app .portal-todo-heading i { color: #1687f5; font-size: 19px; }
#v4-home-app .portal-more { display: inline-flex; align-items: center; gap: 6px; flex-shrink: 0; border: 0; padding: 4px 0; background: transparent; color: #1687f5; font: inherit; font-size: 13px; cursor: pointer; text-decoration: none; }
#v4-home-app .portal-more:hover { color: #0565c6; }
#v4-home-app button:focus-visible,
#v4-home-app a:focus-visible,
#v4-home-app [tabindex]:focus-visible { outline: 2px solid #1687f5; outline-offset: 3px; }
#v4-home-app .entry-card { min-width: 0; }
#v4-home-app .entry-wrapper { padding-bottom: 0; }
#v4-home-app .portal-entry-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; border-bottom: 1px solid #e9eff6; }
#v4-home-app .portal-entry-tabs { display: flex; gap: 34px; }
#v4-home-app .portal-entry-tabs button { border: 0; border-bottom: 3px solid transparent; padding: 0 0 13px; background: transparent; font: inherit; font-size: 19px; color: #465d78; font-weight: 600; cursor: pointer; }
#v4-home-app .portal-entry-tabs button.active { color: #0784fa; border-bottom-color: #0784fa; }
#v4-home-app .entry-section + .entry-section { margin-top: 0; }
#v4-home-app .app-grid { display: flex; justify-content: flex-start; align-items: center; gap: 12px; min-height: 0; overflow-x: auto; padding: 20px 0; border: 0; border-radius: 0; box-shadow: none; }
#v4-home-app .app-item { flex: 0 0 96px; border: 0; font: inherit; padding: 0 4px; max-width: none; }
#v4-home-app .app-item:hover { background: #f4f9ff; box-shadow: none; transform: translateY(-2px); }
#v4-home-app .app-icon { background: transparent; }
#v4-home-app .app-name { display: block; width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 13px; line-height: 20px; min-height: 0; margin-top: 2px; }
#v4-home-app .app-empty { padding: 10px 0; }
#v4-home-app .act-card { padding: 0; border: 0; border-radius: 0; box-shadow: none; }
#v4-home-app .activity-swiper-container { padding: 0 0 4px; }
#v4-home-app .activity-swiper .swiper-slide .item { display: flex; flex-direction: column; height: 100%; }
/* 加高封面并统一裁剪容器,图片、加载态和占位图都保持相同圆角。 */
#v4-home-app .activity-swiper .swiper-slide .item .img-box { flex-shrink: 0; height: auto; aspect-ratio: 1.5; border-radius: 10px; overflow: hidden; clip-path: inset(0 round 10px); background: #1687f5; isolation: isolate; }
#v4-home-app .img-box .el-image { display: block; width: 100%; height: 100%; position: absolute; inset: 0; border-radius: inherit; overflow: hidden; }
#v4-home-app .activity-swiper .swiper-slide .item .img-box img { height: 100% !important; max-height: none !important; object-fit: cover !important; border-radius: 10px; }
#v4-home-app .activity-swiper .swiper-slide .item .img-box .status { top: 8px; left: 8px; border-radius: 5px; padding: 4px 7px; background: #ed4859; }
#v4-home-app .activity-swiper .swiper-slide .item .img-box .status-ended { background: #8b97a7; }
#v4-home-app .activity-swiper .swiper-slide .item .img-box .status-upcoming { background: #1687f5; }
#v4-home-app .activity-swiper .swiper-slide .item h4 { flex: 1; margin: 10px 0 8px; height: auto; min-height: 22px; max-height: 44px; font-size: 15px; color: #20344d; line-height: 22px; }
#v4-home-app .activity-swiper .swiper-slide .item .time p { white-space: normal; font-size: 12px; color: #7b899d; line-height: 1.6; }
#v4-home-app .activity-swiper-container .swiper-button-next,
#v4-home-app .activity-swiper-container .swiper-button-prev { width: 44px; height: 44px; margin-top: 0; padding: 0; transform: translateY(-50%); border: 1px solid #dbeaff; background: rgba(255,255,255,.96); color: #1687f5; box-shadow: 0 3px 12px rgba(30, 66, 108, .18); }
#v4-home-app .activity-swiper-container .swiper-button-next { right: 8px; }
#v4-home-app .activity-swiper-container .swiper-button-prev { left: 8px; }
#v4-home-app .activity-swiper-container .swiper-button-next:hover,
#v4-home-app .activity-swiper-container .swiper-button-prev:hover { transform: translateY(-50%) scale(1.05); background: #1687f5; color: #fff; }
#v4-home-app .activity-swiper-container .swiper-button-next::after,
#v4-home-app .activity-swiper-container .swiper-button-prev::after { font-size: 18px; font-weight: 600; }
#v4-home-app .portal-cover-empty { height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; gap: 9px; color: #94a7bd; background: #f0f5fb; }
#v4-home-app .portal-cover-empty i { font-size: 28px; }
#v4-home-app .user-wrapper { background: transparent; }
#v4-home-app .user-container { display: block; width: 100%; height: 100%; margin: 0; }
#v4-home-app .todo-center { display: flex; flex-direction: column; min-width: 0; height: 100%; }
#v4-home-app .todo-center .header { display: block; margin: 0; }
#v4-home-app .todo-center .el-tabs { margin: 0 0 8px; }
#v4-home-app .todo-center .el-tabs__header { margin: 0; }
#v4-home-app .todo-center .el-tabs__nav { display: flex; width: 100%; border-color: #e9eff6; border-radius: 6px 6px 0 0; }
#v4-home-app .todo-center .el-tabs__item { flex: 1; min-width: 0; padding: 0 7px; font-size: 12px; height: 36px; line-height: 36px; text-align: center; }
#v4-home-app .todo-center .el-tabs__item.is-active { border-bottom: 2px solid #1687f5; color: #1687f5; }
#v4-home-app .portal-task-list { display: flex; flex-direction: column; justify-content: space-evenly; flex: 1; min-height: 180px; }
#v4-home-app .portal-task-row { display: flex; gap: 10px; align-items: center; padding: 8px 6px; border-bottom: 1px solid #eef3f8; }
#v4-home-app .portal-task-row:last-child { border-bottom: 0; }
#v4-home-app .portal-task-row:hover { background: #f7fbff; }
#v4-home-app .portal-task-icon { width: 34px; height: 40px; display: flex; flex: 0 0 34px; align-items: center; justify-content: center; border-radius: 8px; background: #eef7ff; color: #1687f5; font-size: 22px; }
#v4-home-app .portal-task-content { min-width: 0; flex: 1; }
#v4-home-app .portal-task-title { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: #263548; font-weight: 600; font-size: 14px; line-height: 22px; }
#v4-home-app .portal-task-meta { display: flex; align-items: baseline; gap: 10px; font-size: 12px; color: #8390a2; line-height: 21px; }
#v4-home-app .portal-task-meta span { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
#v4-home-app .portal-task-meta > span:first-child { max-width: 45%; flex-shrink: 0; }
#v4-home-app .portal-task-date { flex-shrink: 0; font-size: 12px; color: #7b899d; }
#v4-home-app .portal-task-node { flex: 1; min-width: 0; color: #8b98a9; font-size: 11px; line-height: 17px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#v4-home-app .portal-task-row .el-button { flex-shrink: 0; padding: 8px 14px; }
#v4-home-app .portal-task-pagination { text-align: right; margin-top: 8px; }
#v4-home-app .portal-empty { min-height: 132px; display: flex; align-items: center; justify-content: center; gap: 10px; color: #8a9aac; }
#v4-home-app .portal-empty i { font-size: 26px; color: #83b9ee; }
#v4-home-app .work-template-wrapper { width: 100%; margin: 0; }
#v4-home-app .work-template-card { min-height: 98px; padding: 0; border: 0; border-radius: 0; box-shadow: none; }
#v4-home-app .work-template-grid { display: flex; align-items: flex-start; gap: 14px; overflow-x: auto; padding: 4px 0 10px; scroll-snap-type: x proximity; }
#v4-home-app .work-template-item { flex: 0 0 88px; width: 88px; padding: 4px; border: 0; font: inherit; scroll-snap-align: start; }
#v4-home-app .work-template-item:hover { background: #f4f9ff; box-shadow: none; }
#v4-home-app .work-template-icon,
#v4-home-app .work-template-icon .el-image { width: 48px; height: 48px; }
#v4-home-app .work-template-icon img { width: 48px !important; height: 48px !important; object-fit: contain; }
#v4-home-app .work-template-icon i { font-size: 30px; line-height: 48px; color: #83b9ee; }
#v4-home-app .work-template-name { font-size: 13px !important; line-height: 20px; min-height: 40px; max-height: 40px; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
#v4-home-app .work-template-wrapper .no-activity-placeholder { padding: 12px; }
#v4-home-app .jcdt-container { width: 100%; max-width: none; height: 100%; margin: 0; }
#v4-home-app .portal-news-title { background: transparent; cursor: pointer; font-family: inherit; }
#v4-home-app .portal-news-title > i:last-child:not(:first-child) { color: #8092a6; font-size: 14px; }
#v4-home-app .portal-news-row { display: flex; align-items: center; gap: 12px; width: 100%; padding: 9px 0; border: 0; background: transparent; color: #42536a; font: inherit; text-align: left; cursor: pointer; }
#v4-home-app .portal-news-row time { flex-shrink: 0; padding: 3px 7px; background: #eff5fc; border-radius: 4px; color: #45698f; font-size: 13px; }
#v4-home-app .portal-news-row span { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
#v4-home-app .portal-news-row:hover span { color: #1687f5; }
@media (min-width: 1600px) {
#v4-home-app .portal-hero-copy h1 { font-size: 42px; }
#v4-home-app .app-item { flex-basis: 104px; }
}
@media (max-width: 1199px) {
#v4-home-app .portal-top { grid-template-columns: minmax(0, 1fr) 300px; padding: 18px 18px 0; }
#v4-home-app .portal-hero-inner { padding-left: 32px; }
#v4-home-app .portal-content { padding: 18px 18px 24px; }
#v4-home-app .portal-columns { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); }
#v4-home-app .portal-values { gap: 16px; }
#v4-home-app .portal-panel, #v4-home-app .todo-center { padding: 16px; }
#v4-home-app .portal-task-row { gap: 8px; padding: 8px 0; }
#v4-home-app .portal-task-icon { display: none; }
}
@media (max-width: 900px) {
#v4-home-app .portal-top { grid-template-columns: minmax(0, 1fr); }
#v4-home-app .portal-columns { grid-template-columns: minmax(0, 1fr); }
#v4-home-app .portal-activities { order: 1; }
#v4-home-app .portal-todo { order: 2; }
#v4-home-app .portal-templates { order: 3; }
#v4-home-app .portal-news { order: 4; }
#v4-home-app .portal-hero-copy { max-width: 68%; }
#v4-home-app .portal-hero-copy h1 { font-size: 30px; }
#v4-home-app .portal-hero-copy p { font-size: 14px; }
#v4-home-app .portal-task-icon { display: flex; }
#v4-home-app .portal-task-meta { flex-wrap: nowrap; }
}
@media (max-width: 600px) {
#v4-home-app .portal-top { padding: 10px 10px 8px; }
#v4-home-app .portal-hero-inner { padding: 24px 20px; }
#v4-home-app .portal-hero-copy { max-width: 100%; }
#v4-home-app .portal-hero-copy h1 { font-size: 28px; }
#v4-home-app .portal-values { gap: 12px 18px; font-size: 12px; }
#v4-home-app .portal-values i { font-size: 17px; }
#v4-home-app .portal-content { padding: 10px; }
#v4-home-app .entry-wrapper { padding: 16px 12px 0; }
#v4-home-app .portal-entry-tabs { gap: 20px; }
#v4-home-app .portal-entry-tabs button { font-size: 16px; }
#v4-home-app .app-item { flex-basis: 84px; }
#v4-home-app .home-section-title, #v4-home-app .portal-todo-heading h3, #v4-home-app .work-template-title, #v4-home-app .portal-news-title { font-size: 18px; }
#v4-home-app .portal-task-row .el-button { padding: 8px 10px; }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

@@ -68,9 +68,9 @@
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vxe/vxe-pc-ui.css" />
<link rel="stylesheet" href="${base!}/assets/platform/plugins/vxe/vxe-table.css" />
<script src="https://vxeui.com/umd/xe-utils@3.5.30/dist/xe-utils.umd.min.js"></script>
<script src="https://vxeui.com/umd/vxe-pc-ui@3.1.25/lib/index.umd.min.js"></script>
<script src="https://vxeui.com/umd/vxe-table@3.9.0/lib/index.umd.min.js"></script>
<script src="${base!}/assets/platform/plugins/vxe/xe-utils.js"></script>
<script src="${base!}/assets/platform/plugins/vxe/vxe-pc-ui.js"></script>
<script src="${base!}/assets/platform/plugins/vxe/vue-table.js"></script>
<!-- 引入 form-create 和 designer -->
<script src="${base!}/assets/platform/plugins/form-create/form-create.min.js"></script>
@@ -436,6 +436,8 @@
.v4-logo {
height: 100%;
width: auto;
/* 保留 Logo 透明轮廓,将所有可见颜色统一转换为白色。 */
filter: brightness(0) invert(1);
}
.v4-nav {
@@ -754,7 +756,8 @@
var currentPath = getBaseSubAppPath()
var footer = $("#v4-footer")
if (currentPath === "/platform/v4/home") {
// 服务门户首页不显示版权栏,PJAX 切换时同步判断当前页面。
if (currentPath === "/platform/v4/home" && !$("#v4-home-app").hasClass("home-portal")) {
footer.show()
} else {
footer.hide()
+43 -18
View File
@@ -10,22 +10,23 @@ const act = {
<div ref="activitySwiper" class="swiper activity-swiper">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="(activity, index) in actList" :key="activity.id">
<div class="item" @click="go(activity)">
<div class="item" role="button" tabindex="0" @click="go(activity)" @keydown.enter="go(activity)" @keydown.space.prevent="go(activity)">
<div class="img-box">
<img :src="activity.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
alt="">
<span class="status">
<el-image :src="activity.cover" fit="cover" :alt="activity.name">
<div slot="error" class="portal-cover-empty"><i class="fa fa-picture-o" aria-hidden="true"></i><span>暂无图片</span></div>
</el-image>
<span class="status" :class="{'status-ended': getStatusText(activity) === '已结束', 'status-upcoming': getStatusText(activity) === '未开始'}">
{{getStatusText(activity)}}
</span>
</div>
<h4 class="title">
<h4 class="title" :title="activity.name">
{{ activity.name}}
</h4>
<!--报名时间-->
<div class="time">
<p>
<p :title="[activity.startDate, activity.endDate].filter(Boolean).join(' 至 ')">
<i class="fa fa-clock-o"></i>
时间:{{ activity.startDate }}至{{ activity.endDate }}
{{ formatActivityDate(activity.startDate) }} 至 {{ formatActivityDate(activity.endDate) }}
</p>
</div>
</div>
@@ -34,15 +35,15 @@ const act = {
</div>
<!-- 导航按钮 -->
<div ref="nextButton" class="swiper-button-next"></div>
<div ref="prevButton" class="swiper-button-prev"></div>
<button type="button" ref="nextButton" class="swiper-button-next" aria-label="下一组活动"></button>
<button type="button" ref="prevButton" class="swiper-button-prev" aria-label="上一组活动"></button>
<!-- 进度条 -->
<div ref="pagination" class="swiper-pagination"></div>
</div>
<div v-else class="no-activity-placeholder">
<div class="icon">📅</div>
<div class="icon"><i class="fa fa-calendar" aria-hidden="true"></i></div>
<p>暂无近期活动</p>
<p class="subtext">敬请关注后续通知</p>
</div>
@@ -57,6 +58,24 @@ const act = {
}
},
methods: {
updateNavigationPosition() {
const swiperElement = this.$refs.activitySwiper
const cover = swiperElement && swiperElement.querySelector('.img-box')
if (!cover) return
// 按实际封面高度定位,避免标题换行或响应式列数变化导致箭头偏移。
const container = swiperElement.parentElement
const top = cover.getBoundingClientRect().top - container.getBoundingClientRect().top + cover.offsetHeight / 2
;[this.$refs.prevButton, this.$refs.nextButton].forEach(button => {
if (button) button.style.top = top + 'px'
})
},
formatActivityDate(value) {
return value ? this.$moment(value).format('YYYY-MM-DD') : '待定'
},
browseNext() {
if (!this.swiper) return
this.swiper.isEnd ? this.swiper.slideTo(0) : this.swiper.slideNext()
},
/*查询活动*/
listAct() {
this.$axios.post("/platform/home/listHomeActivity").then((res) => {
@@ -89,13 +108,22 @@ const act = {
centeredSlides: false,
loop: false,
watchOverflow: true,
// 双栏中的活动按容器宽度排布,避免用整个窗口宽度挤出过多卡片。
breakpointsBase: 'container',
observer: false,
observeParents: false,
resizeObserver: false,
// 页面内容加载后可能出现滚动条,需重新计算列宽,避免末张封面及圆角被裁切。
resizeObserver: true,
roundLengths: true,
on: {
init: () => this.$nextTick(() => this.updateNavigationPosition()),
resize: () => this.$nextTick(() => this.updateNavigationPosition()),
slidesUpdated: () => this.$nextTick(() => this.updateNavigationPosition()),
},
// 鼠标停留在活动区域时可横向浏览,到首尾后释放页面滚动。
mousewheel: {
enabled: true,
forceToAxis: false,
forceToAxis: true,
releaseOnEdges: true,
sensitivity: 0.8,
},
@@ -108,14 +136,11 @@ const act = {
type: 'progressbar',
},
breakpoints: {
768: {
480: {
slidesPerView: 2,
},
1024: {
slidesPerView: 4,
},
1200: {
slidesPerView: 5,
740: {
slidesPerView: 3,
}
}
})
@@ -1,18 +1,19 @@
const entry = {
template: /*language=HTML*/ `
<div class="entry-wrapper">
<div :class="['entry-section', 'entry-section-' + section.key]" v-for="section in entrySections" :key="section.key">
<div class="entry-section-header">
<div class="entry-section-title">
<i :class="section.icon"></i>
<span>{{ section.title }}</span>
<em>/Applications</em>
</div>
<div class="portal-entry-header">
<div class="portal-entry-tabs" role="tablist" aria-label="推荐入口">
<button v-for="section in entrySections" :key="section.key"
type="button" role="tab" :aria-selected="activeSection === section.key"
:class="{active: activeSection === section.key}" @click="activeSection = section.key">{{ section.title }}</button>
</div>
<a class="portal-more" href="/platform/v4/apps">全部应用 <i class="fa fa-arrow-right" aria-hidden="true"></i></a>
</div>
<div :class="['entry-section', 'entry-section-' + section.key]" v-for="section in entrySections" :key="section.key" v-show="activeSection === section.key" role="tabpanel">
<div class="apps-container">
<div class="app-grid">
<div class="app-item"
<div class="app-grid" tabindex="0" :aria-label="section.title + ',可横向滚动浏览'">
<button type="button" class="app-item"
v-for="(item, index) in section.list"
:key="item.id || index"
@click="openService(item, section.key)">
@@ -20,8 +21,8 @@ const entry = {
<img v-if="item.picIcon" class="app-icon-img" :src="item.picIcon" alt="" />
<i v-else class="fa fa-cube app-icon-fallback"></i>
</div>
<div class="app-name">{{ item.name }}</div>
</div>
<div class="app-name" :title="item.name">{{ item.name }}</div>
</button>
<el-empty class="app-empty"
v-if="!section.list || section.list.length === 0"
description="暂无数据"
@@ -34,6 +35,7 @@ const entry = {
`,
data() {
return {
activeSection: 'serv',
hasMore: true,
entries: {
serv: [],
@@ -127,11 +129,6 @@ const entry = {
margin-top: 22px;
}
/* 临时屏蔽“我的收藏”分组,保留原节点和数据逻辑便于恢复 */
.entry-section-fav {
display: none;
}
.entry-section-header {
display: flex;
align-items: center;
@@ -118,42 +118,51 @@ layout("/layouts/v4/baseLayout.html"){
</style>
<div class="v4-container" id="v4-home-app">
<link rel="stylesheet" href="${base!}/assets/platform/css/home-portal.css?v=20260929-1" />
<div class="v4-container home-portal" id="v4-home-app">
<!-- 首页banner -->
<div class="section-banner">
<div class="banner-img">
<img :src="homeBannerList[0] || ''" alt=""/>
<stats></stats>
<div class="portal-top">
<div class="portal-hero">
<div class="portal-hero-inner">
<div class="portal-hero-copy">
<div class="portal-hero-eyebrow">教职工一站式服务平台</div>
<h1>智慧工会系统</h1>
<p>情系教职工,服务零距离</p>
<div class="portal-values">
<span><i class="fa fa-heart" aria-hidden="true"></i>服务职工</span>
<span><i class="fa fa-users" aria-hidden="true"></i>凝聚职工</span>
<span><i class="fa fa-leaf" aria-hidden="true"></i>关爱职工</span>
<span><i class="fa fa-file-text" aria-hidden="true"></i>助力发展</span>
</div>
</div>
</div>
</div>
<user class="portal-member" :profile-only="true"></user>
</div>
<div class="" style="padding-bottom: 20px">
<user></user>
<div class="home-grid">
<div class="portal-content">
<div class="entry-card">
<entry></entry>
</div>
</div>
<div class="home-grid">
<div class="home-section">
<div class="portal-columns">
<div class="home-section portal-panel portal-activities">
<div class="home-section-header">
<div class="home-section-title">
<i class="fa fa-calendar"></i>
<span>最新活动</span>
<em>/Activities</em>
</div>
</div>
<button type="button" class="portal-more" @click="$refs.homeActivities.browseNext()">浏览更多 <i class="fa fa-arrow-right" aria-hidden="true"></i></button>
</div>
<div class="act-card">
<act></act>
<act ref="homeActivities"></act>
</div>
</div>
<user ref="homeTodo" class="portal-todo"></user>
<work-template class="portal-templates" v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"></work-template>
<jcdt class="portal-news" :class="{'portal-news-wide': !$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])}" :list="websiteNews"></jcdt>
</div>
<work-template v-if="$auth.hasRoleOr(['SYSADMIN','SCHOOL_UNION_ADMIN'])"></work-template>
<jcdt :list="websiteNews"></jcdt>
</div>
</div>
@@ -2,42 +2,63 @@ const jcdt = {
template: /*language=HTML*/ `
<div class="jcdt-wrapper">
<div class="jcdt-container">
<el-carousel height="360px" :interval="10000" indicator-position="none">
<el-carousel-item v-for="item,index in list" :key="index">
<div class="jcdt-title">
<span>{{ item.label }}</span>
</div>
<div class="jcdt-content">
<div class="jcdt-list">
<div class="jcdt-item" v-for="o,i in item.value" :key="i" @click="onLink(o)">
<div class="date" v-if="o.time">
<i class="el-icon-alarm-clock"></i>
<span>{{o.time}}</span>
</div>
<div style="display: flex; align-items: center">
<div>
</div>
<div class="title">{{o.text}}</div>
</div>
</div>
</div>
</div>
</el-carousel-item>
</el-carousel>
<div class="portal-news-header">
<el-dropdown @command="selectSection" trigger="click">
<button type="button" class="portal-news-title"><i class="fa fa-bullhorn" aria-hidden="true"></i> {{ currentNews.label }} <i v-if="list.length > 1" class="fa fa-angle-down" aria-hidden="true"></i></button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="(section, index) in list" :key="index" :command="index">{{ section.label }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<div v-if="pageCount > 1" class="portal-news-pager" aria-label="动态分页">
<span class="portal-news-count">共 {{ currentNews.value.length }} 条</span>
<button type="button" :disabled="pageNumber === 1" title="上一页动态" aria-label="上一页动态" @click="pageNumber--"><i class="fa fa-angle-left" aria-hidden="true"></i></button>
<span class="portal-news-page" aria-live="polite">{{ pageNumber }} / {{ pageCount }}</span>
<button type="button" :disabled="pageNumber === pageCount" title="下一页动态" aria-label="下一页动态" @click="pageNumber++"><i class="fa fa-angle-right" aria-hidden="true"></i></button>
</div>
</div>
<div class="portal-news-list">
<button type="button" class="portal-news-row" v-for="(item, index) in visibleNews" :key="index" @click="onLink(item)">
<time v-if="item.time" :title="item.time">{{ item.time.slice(5) }}</time>
<span :title="item.text">{{ item.text }}</span>
</button>
<div v-if="!currentNews.value.length" class="portal-empty">暂无动态</div>
</div>
</div>
</div>
`,
data() {
return {}
return {activeIndex: 0, pageNumber: 1, pageSize: 3}
},
props: {
list: {
type: Array,
default: []
default: () => []
}
},
computed: {
currentNews() {
// 默认遵循接口栏目顺序,不再按栏目名称优先选择。
const section = this.list[this.activeIndex] || this.list[0]
return section ? {label: section.label, value: section.value || []} : {label: '最新动态', value: []}
},
visibleNews() {
const start = (this.pageNumber - 1) * this.pageSize
return this.currentNews.value.slice(start, start + this.pageSize)
},
pageCount() {
return Math.max(1, Math.ceil(this.currentNews.value.length / this.pageSize))
}
},
watch: {
list() {
this.selectSection(0)
}
},
methods: {
selectSection(index) {
this.activeIndex = index
this.pageNumber = 1
},
onLink(item) {
const url = this.isHttpOrHttps(item.href) ? item.href : "https://xgh.cug.edu.cn/" + item.href
window.open(url)
@@ -47,6 +68,15 @@ const jcdt = {
}
},
style: /*language=CSS*/ `
/* 每页固定三行,不因展开或末页条数变化撑高相邻的模板卡片。 */
#v4-home-app .portal-news-list { min-height: 132px; }
#v4-home-app .portal-news-list .portal-news-row { height: 44px; padding: 9px 0; }
#v4-home-app .portal-news-pager { display: flex; flex-shrink: 0; align-items: center; gap: 8px; }
#v4-home-app .portal-news-count { margin-right: 4px; color: #8a9aac; font-size: 12px; }
#v4-home-app .portal-news-page { min-width: 32px; color: #637b96; text-align: center; font-size: 12px; font-variant-numeric: tabular-nums; }
#v4-home-app .portal-news-pager button { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 0; border: 1px solid #e4edf7; border-radius: 6px; background: #fff; color: #1687f5; font-size: 18px; cursor: pointer; }
#v4-home-app .portal-news-pager button:hover:not(:disabled) { border-color: #b9d8fb; background: #f0f7ff; }
#v4-home-app .portal-news-pager button:disabled { border-color: #eef2f7; background: #f8fafc; color: #c3cedb; cursor: default; }
.jcdt-wrapper {
width: 100%;
background-size: cover;
@@ -5,21 +5,22 @@ const workTemplate = {
<div class="work-template-title">
<i class="fa fa-clipboard"></i>
<span>工作模板</span>
<em>/Templates</em>
</div>
<button type="button" class="portal-more" @click="browseTemplates">更多模板 <i class="fa fa-arrow-right" aria-hidden="true"></i></button>
</div>
<div class="work-template-card">
<div v-if="templateList && templateList.length > 0" class="work-template-grid">
<div class="work-template-item"
<div ref="templateGrid" v-if="templateList && templateList.length > 0" class="work-template-grid">
<button type="button" class="work-template-item"
v-for="template in templateList"
:key="template.id"
@click="go(template)">
<div class="work-template-icon">
<img :src="template.templateIcon || template.cover || 'https://www.ncu.edu.cn/__local/8/52/2C/644986B4C9A030F7B65300178A6_8679CB08_18467.jpg'"
alt="">
<el-image :src="template.templateIcon || template.cover" fit="contain">
<i slot="error" class="fa fa-clipboard" aria-hidden="true"></i>
</el-image>
</div>
<div class="work-template-name">{{ template.templateName || template.name }}</div>
</div>
<div class="work-template-name" :title="template.templateName || template.name">{{ template.templateName || template.name }}</div>
</button>
</div>
<div v-else class="no-activity-placeholder">
@@ -36,6 +37,13 @@ const workTemplate = {
}
},
methods: {
browseTemplates() {
const grid = this.$refs.templateGrid
if (!grid) return
// 模板仍使用全部真实数据,横向翻页不截断管理员可用的入口。
const atEnd = grid.scrollLeft + grid.clientWidth >= grid.scrollWidth - 2
grid.scrollTo({left: atEnd ? 0 : grid.scrollLeft + grid.clientWidth, behavior: 'smooth'})
},
listTemplate() {
this.$axios.post("/platform/home/listHomeTemplate").then((res) => {
if (res.code === 0) {
@@ -4,13 +4,17 @@ const user = {
<!-- 用户容器:左右布局 -->
<div class="user-container">
<!-- 左侧:用户信息卡片 -->
<div class="user-info-card">
<div v-if="profileOnly" class="user-info-card">
<div class="portal-member-date"><i class="fa fa-calendar-o" aria-hidden="true"></i>{{ $moment().format('YYYY年MM月DD日') }} {{ ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'][$moment().day()] }}</div>
<div class="user-details">
<div class="user-name">${@auth.getPrincipalProperty('username')}</div>
<div class="portal-member-heading">
<span class="portal-member-avatar"><i class="fa fa-user" aria-hidden="true"></i></span>
<div><div class="portal-member-welcome">欢迎回来</div><div class="user-name">${@auth.getPrincipalProperty('username')}</div></div>
</div>
<div class="user-meta-grid">
<div class="meta-row">
<span class="meta-icon">🆔</span>
<span class="meta-icon"><i class="fa fa-id-card-o" aria-hidden="true"></i></span>
<div>
<span class="meta-label">工号</span>
<span class="meta-value">${@auth.getPrincipalProperty('loginname')}</span>
@@ -18,7 +22,7 @@ const user = {
</div>
<div class="meta-row">
<span class="meta-icon">🏢</span>
<span class="meta-icon"><i class="fa fa-building-o" aria-hidden="true"></i></span>
<div>
<span class="meta-label">单位信息</span>
<span class="meta-value">
@@ -33,11 +37,11 @@ const user = {
</span>
</div>
</div>
<div class="meta-row">
<span class="meta-icon">🎭</span>
<div class="meta-row meta-row-roles">
<span class="meta-icon"><i class="fa fa-shield" aria-hidden="true"></i></span>
<div>
<span class="meta-label">角色</span>
<span class="meta-value">{{ roleNames || '—' }}</span>
<div class="portal-member-roles"><span v-for="(role, index) in roleTags" :key="index" class="portal-member-role">{{ role }}</span><span v-if="!roleTags.length" class="meta-value">—</span></div>
</div>
</div>
</div>
@@ -63,10 +67,13 @@ const user = {
</div>
<!-- 右侧:待办中心 -->
<div class="todo-center">
<div v-if="!profileOnly" class="todo-center">
<!-- 标题和标签 -->
<div class="header">
<h3>待办中心</h3>
<div class="portal-todo-heading">
<h3><i class="fa fa-list-ul" aria-hidden="true"></i> 待办中心</h3>
<a class="portal-more" :href="'/flow/todoCenter?status=' + activeTab" target="_blank">全部待办 <i class="fa fa-arrow-right" aria-hidden="true"></i></a>
</div>
<el-tabs v-model="activeTab" type="card" @tab-click="handleTabClick">
<el-tab-pane :label="tabLabel('待办事宜', taskStats.todoCount)" name="todo"></el-tab-pane>
<el-tab-pane :label="tabLabel('已办事宜', taskStats.doneCount)" name="done"></el-tab-pane>
@@ -75,54 +82,41 @@ const user = {
</el-tabs>
</div>
<!-- 表格 -->
<el-table
:data="todoList"
style="width: 100%"
border
stripe
v-loading="loading"
:cell-style="{ padding: '8px' }"
:header-cell-style="{ background: '#f8f9fa', color: '#444', fontWeight: '500' }"
>
<el-table-column label="序号" type="index" :index="indexMethod" width="100px"></el-table-column>
<el-table-column prop="categoryName" label="流程分类" width="100"></el-table-column>
<el-table-column prop="title" label="流程名称" min-width="300">
<template slot-scope="scope">
<el-tooltip effect="dark" :content="scope.row.instanceName" placement="top">
<span class="table-title">{{ scope.row.instanceName }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="sender" label="申请人" width="120">
<template slot-scope="{row}">{{row.variable.initiatorName}}</template>
</el-table-column>
<el-table-column prop="taskName" label="当前流程节点" width="140" show-overflow-tooltip></el-table-column>
<el-table-column prop="date" label="发起日期" width="120">
<template slot-scope="{row}">
{{$moment(row.createdAt).format('YYYY-MM-DD')}}
</template>
</el-table-column>
<el-table-column label="操作" width="100px" fixed="right">
<template slot-scope="scope">
<el-button type="primary" size="mini" @click="openView(scope.row)">查看</el-button>
</template>
</el-table-column>
</el-table>
<!--#include("/layouts/pagination.html"){}#-->
<div class="portal-task-list" v-loading="loading">
<div class="portal-task-row" v-for="(task, index) in todoList" :key="task.taskId || task.instanceId || index">
<i class="fa fa-file-text-o portal-task-icon" aria-hidden="true"></i>
<div class="portal-task-content">
<div class="portal-task-title" :title="task.instanceName">{{ task.instanceName }}</div>
<div class="portal-task-meta">
<span :title="task.variable && task.variable.initiatorName">申请人:{{ (task.variable && task.variable.initiatorName) || '—' }}</span>
<span class="portal-task-node" :title="[task.categoryName, task.taskName].filter(Boolean).join(' · ')">{{ [task.categoryName, task.taskName].filter(Boolean).join(' · ') }}</span>
</div>
</div>
<time class="portal-task-date">{{ task.createdAt ? $moment(task.createdAt).format('YYYY-MM-DD') : '—' }}</time>
<el-button type="primary" size="mini" @click="openView(task)">查看</el-button>
</div>
<div v-if="!loading && !todoList.length" class="portal-empty"><i class="fa fa-check-circle-o" aria-hidden="true"></i><span>暂无{{ activeTab === 'todo' ? '待办事项' : '相关记录' }}</span></div>
</div>
<el-pagination v-if="pageForm.totalCount > pageForm.pageSize" class="portal-task-pagination"
@current-change="pageNumberChange" :current-page="pageForm.pageNumber" :page-size="pageForm.pageSize"
layout="total, prev, next" :total="pageForm.totalCount"></el-pagination>
</div>
</div>
</div>
`,
mixins: [initTableMixins],
// 同一组件分别承载会员卡和待办卡,各自只加载需要的数据。
props: {
profileOnly: { type: Boolean, default: false }
},
data() {
return {
activeTab: 'todo',
todoList: [],
loading: false,
pageForm: {
pageNum: 1,
pageSize: 5,
pageNumber: 1,
pageSize: 3,
totalCount: 0
},
taskStats: {
@@ -140,6 +134,12 @@ const user = {
};
},
computed: {
roleTags() {
// 兼容角色接口已有的中英文分隔符,仅改变展示,不改变角色权限。
return String(this.roleNames || '').split(/[,,、;;]/).map(role => role.trim()).filter(Boolean)
}
},
methods: {
getWeatherInfo(iconCode, temp) {
const info = {
@@ -268,6 +268,7 @@ const user = {
this.getStatistics()
},
handleTabClick(tab) {
this.pageForm.pageNumber = 1
this.pageData()
console.log('切换到:', tab.name);
},
@@ -291,13 +292,42 @@ const user = {
}
},
created() {
this.pageData()
this.getRoleNames()
if (this.profileOnly) {
this.getRoleNames()
} else {
this.pageData()
}
},
mounted() {
this.fetchWeather()
if (this.profileOnly) this.fetchWeather()
},
style: /*language=CSS*/ `
/* 会员卡样式随组件注入,确保与卡片结构同步加载,避免旧外链样式造成排版缺失。 */
#v4-home-app .portal-member { min-width: 0; }
#v4-home-app .portal-member .user-info-card { height: 100%; padding: 20px; gap: 14px; border: 1px solid #e3edf8; border-radius: 12px; background: linear-gradient(180deg, #f1f7ff 0, #fff 110px); box-shadow: 0 3px 14px rgba(30,66,108,.035); }
#v4-home-app .portal-member-date { display: flex; align-items: center; gap: 8px; color: #71839b; font-size: 12px; line-height: 20px; }
#v4-home-app .portal-member-heading { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; padding-bottom: 14px; border-bottom: 1px solid #eaf0f7; }
#v4-home-app .portal-member-heading > div { min-width: 0; }
#v4-home-app .portal-member-avatar { display: flex; align-items: center; justify-content: center; flex: 0 0 46px; height: 46px; border-radius: 50%; background: #e1eeff; color: #1687f5; font-size: 25px; }
#v4-home-app .portal-member-welcome { margin-bottom: 3px; color: #8797a9; font-size: 12px; line-height: 18px; }
#v4-home-app .portal-member .user-name { margin: 0; color: #163c61; text-align: left; font-size: 18px; line-height: 24px; overflow-wrap: anywhere; }
#v4-home-app .portal-member .user-meta-grid { gap: 12px; margin: 0; }
#v4-home-app .portal-member .meta-row { gap: 8px; }
#v4-home-app .portal-member .meta-icon { min-width: 18px; color: #81a4c9; font-size: 14px; }
#v4-home-app .portal-member .meta-row > div { display: grid; grid-template-columns: 54px minmax(0, 1fr); gap: 8px; min-width: 0; }
#v4-home-app .portal-member .meta-label { margin: 0; color: #8995a5; font-size: 12px; line-height: 20px; }
#v4-home-app .portal-member .meta-value { color: #283c54; font-size: 12px; font-weight: 400; line-height: 20px; overflow-wrap: anywhere; }
#v4-home-app .portal-member .meta-row-roles > div { display: block; }
#v4-home-app .portal-member-roles { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px; }
#v4-home-app .portal-member-role { max-width: 100%; display: inline-block; padding: 3px 8px; border: 1px solid #e1ebf7; border-radius: 5px; background: #f3f7fc; color: #46617f; font-size: 12px; line-height: 18px; overflow-wrap: anywhere; }
#v4-home-app .portal-member .weather-section { margin-top: auto; padding: 8px 10px; border-radius: 8px; background: #f0f7ff; color: #527494; }
#v4-home-app .portal-member .weather-header { gap: 6px; }
#v4-home-app .portal-member .weather-location,
#v4-home-app .portal-member .weather-text { font-size: 12px; }
#v4-home-app .portal-member .weather-emoji { font-size: 20px; }
#v4-home-app .portal-member .weather-temp { font-size: 18px; }
#v4-home-app .portal-member .weather-tip { margin-top: 4px; font-size: 11px; line-height: 18px; }
#v4-home-app .portal-member .weather-error { margin-top: auto; padding: 8px; font-size: 12px; }
.user-wrapper {
width: 100%;
background-color: #f5f7fa;
@@ -54,6 +54,14 @@ layout("/layouts/platform.html"){
<el-tag size="mini" v-else-if="row.isDeployed===0" type="info">否</el-tag>
</template>
</el-table-column>
<el-table-column label="流程中心" width="150" align="center" header-align="center">
<template slot-scope="{row}">
<el-switch :value="row.centerEnabled !== 0"
:loading="row.centerUpdating === true"
active-text="开启" inactive-text="关闭"
@change="updateCenterEnabled(row, $event)"></el-switch>
</template>
</el-table-column>
<el-table-column label="排序编码" prop="sortNo" width="120" align="center" header-align="center" sortable="custom">
<template slot-scope="{row}">
<el-input-number
@@ -266,6 +274,25 @@ layout("/layouts/platform.html"){
}
})
},
// 开关仅控制流程中心是否展示该流程,接口失败时保留原状态。
updateCenterEnabled(row, enabled) {
this.$set(row, "centerUpdating", true)
this.$axios.post("/flow/design/updateCenterEnabled", {
id: row.id,
centerEnabled: enabled ? 1 : 0
}).then((res) => {
if (res.code === 0) {
this.$set(row, "centerEnabled", enabled ? 1 : 0)
this.$message.success(enabled ? "已开启" : "已关闭")
} else {
this.$message.error(res.msg || "更新流程中心显示状态失败")
}
}).catch(() => {
this.$message.error("更新流程中心显示状态失败")
}).finally(() => {
this.$set(row, "centerUpdating", false)
})
},
},
created() {
this.pageData()
@@ -47,11 +47,13 @@ layout("/layouts/platform.html"){
<el-option v-for="item in userStateOptions" :label="item" :value="item"></el-option>
</el-select>
</search-item>
<!-- 暂停用人方式筛选,保留查询控件供后续恢复。
<search-item label="用人方式">
<el-select v-model="pageForm.preparedBy" clearable filterable>
<el-option v-for="item in preparedByOptions" :label="item" :value="item"></el-option>
</el-select>
</search-item>
-->
<search-item label="人员类型">
<el-select v-model="pageForm.personType" clearable filterable>
<el-option v-for="item in personTypeOptions" :label="item" :value="item"></el-option>
@@ -124,14 +126,25 @@ layout("/layouts/platform.html"){
},
methods: {
pull() {
// 拉取未结束时不再打开确认框或发送重复请求。
if (this.pullLoading) return
this.$confirm("确定要拉取数据源吗?", "提示", {
type: "warning",
confirmButtonText: "确定",
cancelButtonText: "取消"
})
.then(() => {
this.pullLoading = true
this.$axios
if (this.pullLoading) return
this.$set(this, "pullLoading", true)
// 全屏提示正在拉取,锁定页面操作,避免仅按钮转圈不易察觉。
const loading = this.$loading({
fullscreen: true,
lock: true,
text: "正在拉取信息中心数据,请稍候",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.5)"
})
return this.$axios
.post("/platform/sys/data/user/pull/pullData")
.then((resp) => {
if (resp.code === 0) {
@@ -141,7 +154,9 @@ layout("/layouts/platform.html"){
}
})
.finally(() => {
this.pullLoading = false
// 成功、业务失败或网络异常均关闭遮罩,并恢复按钮状态。
loading.close()
this.$set(this, "pullLoading", false)
})
})
.catch(() => {})
@@ -249,6 +249,8 @@ layout("/layouts/platform.html"){
})
},
execUpdate() {
// 人员更新未结束时不再打开确认框或提交重复请求。
if (this.updateLoading) return
if (!this.updateFromData.pullTime) {
this.$message.warning("请选择数据源时间")
return
@@ -269,8 +271,17 @@ layout("/layouts/platform.html"){
type: "warning"
})
.then(() => {
this.updateLoading = true
this.$axios
if (this.updateLoading) return
this.$set(this, "updateLoading", true)
// 遮罩覆盖人员更新弹窗及页面,明确提示处理中并阻止重复操作。
const loading = this.$loading({
fullscreen: true,
lock: true,
text: "正在更新人员数据,请稍候",
spinner: "el-icon-loading",
background: "rgba(0, 0, 0, 0.5)"
})
return this.$axios
.post("/platform/sys/data/user/update/updateUserFormSource", { param: JSON.stringify(this.updateFromData) })
.then((resp) => {
if (resp.code === 0) {
@@ -280,9 +291,14 @@ layout("/layouts/platform.html"){
}
})
.finally(() => {
this.updateLoading = false
// 成功、业务失败及网络异常均释放遮罩,恢复人员更新按钮。
loading.close()
this.$set(this, "updateLoading", false)
})
})
.catch(() => {
// 取消确认不提交;请求错误提示继续由全局 axios 拦截器处理。
})
},
listUnit() {
this.$businessTool.listUnit(this.pageForm.unionId).then((res) => {
@@ -124,7 +124,8 @@
}
</script>
<script nonce="${cspNonce!}">
const DISABLE_LOGIN_CAPTCHA = ["localhost", "127.0.0.1", "::1"].includes(window.location.hostname)
// 临时屏蔽指定局域网 IP 的验证码;恢复时删除 192.168.0.108,并同步删除后端对应条件。
const DISABLE_LOGIN_CAPTCHA = ["localhost", "127.0.0.1", "::1", "192.168.0.108"].includes(window.location.hostname)
new Vue({
el: "#app",
@@ -155,14 +155,13 @@ layout("/layouts/platform.html"){
</el-button>
<el-button @click="$refs.sortRef.onOpen()" size="medium" type="primary" icon="el-icon-sort">排序
</el-button>
<el-button v-if="$auth.hasPermission('sys.manager.role.user')" size="medium" type="primary"
icon="el-icon-user" :loading="memberRoleLoading" @click="doSettingMemberRole">
一键设置会员角色
</el-button>
</div>
</el-row>
<!-- <div class="pull-right offscreen-right mt5 ml5">-->
<!-- <el-button size="medium" @click="doSettingMemberRole" type="primary"><i class="el-icon-s-tools"></i>-->
<!-- 一键设会员角色-->
<!-- </el-button>-->
<!-- </div>-->
<!-- <div class="pull-right offscreen-right mt5 ml5">-->
<!-- <el-button size="medium" @click="doPublic" type="primary">一键设置公共角色</el-button>-->
<!-- </div>-->
@@ -516,6 +515,7 @@ layout("/layouts/platform.html"){
roleDialogVisible: false,
menuDialogVisible: false,
userDialogVisible: false,
memberRoleLoading: false,
treeData: {
label: "name",
children: "zones",
@@ -689,18 +689,30 @@ layout("/layouts/platform.html"){
})
},
doSettingMemberRole() {
this.$confirm("是否确定一键设置会员角色?", "提示", {
if (this.memberRoleLoading) {
return
}
// 操作面向全部会员,忽略当前列表筛选;确认期间也锁定按钮,避免重复提交。
this.$set(this, "memberRoleLoading", true)
this.$confirm("确定为全部已标记为会员的人员设置会员角色吗?已有角色将保留。", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(async () => {
const {code, msg} = await this.$axios.post("/platform/sys/role/doSettingMemberRole")
if (code === 0) {
this.$message.success(msg)
}
.then(() => {
return this.$axios.post("/platform/sys/role/doSettingMemberRole").then((res) => {
if (res.code === 0) {
this.$message.success("设置完成:新增 " + res.data.addedCount
+ " 条,恢复启用 " + res.data.enabledCount + " 条会员角色关联")
this.pageData()
}
})
})
.catch(() => {
// 取消确认无需提示;请求异常由全局 axios 拦截器统一提示。
})
.finally(() => {
this.$set(this, "memberRoleLoading", false)
})
},
formatAt(val) {
@@ -5,7 +5,7 @@ const clubUserJoin = {
<div>
<div class="process-title">
申请信息
<el-link type="primary" @click="openChart">点击查看流程图</el-link>
<el-link v-if="row.mode !== 2" type="primary" @click="openChart">点击查看流程图</el-link>
</div>
<el-descriptions border>
<el-descriptions-item label="姓名">
@@ -112,7 +112,7 @@ layout("/layouts/platform.html"){
style="width: 100%"
>
<el-option
v-for="item in clubOptions"
v-for="item in sortedClubOptions"
:key="item.id"
:label="item.clubName"
:value="item.id"
@@ -380,9 +380,14 @@ layout("/layouts/platform.html"){
>
{{(lastClubData.year - 1) + '年度注册会员人数为' + lastClubData.history_member + ',其中拨付人数为' + lastClubData.history_y_givemoney_member}}
</div>
<el-table :data="formData.changeUserNum" max-height="300">
<!-- 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。 -->
<el-alert v-if="memberStatisticsWarnings.length"
:title="memberStatisticsWarnings.join(';')"
type="warning" :closable="false" show-icon></el-alert>
<el-table :data="formData.changeUserNum" max-height="300" v-loading="memberStatisticsLoading">
<el-table-column label="序号" type="index" width="50"></el-table-column>
<el-table-column label="人员类型" prop="userState"></el-table-column>
<!-- 2026年10月2日wxg屏蔽操作:<el-table-column label="人员类型" prop="userState"></el-table-column> -->
<el-table-column label="人员类型" prop="personType"></el-table-column>
<el-table-column label="年初人员" prop="yearFirstNum"></el-table-column>
<el-table-column label="年度增加" prop="yearAddNum"></el-table-column>
<el-table-column label="年度减少" prop="yearReduceNum"></el-table-column>
@@ -651,6 +656,11 @@ layout("/layouts/platform.html"){
memberList: [], // 成员名单
lastClubData: {}, // 上年度社团数据
// 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。统计加载与核查提示只服务于成员变化表。
memberStatisticsLoading: false,
memberStatisticsWarnings: [],
memberStatisticsSequence: 0,
// 核心表单数据
formData: {
year: new Date().getFullYear() + "", // 默认当前年度
@@ -678,6 +688,18 @@ layout("/layouts/platform.html"){
},
// 常量定义(便于维护和修改)
computed: {
// 仅对本页社团选项按编码升序展示;空编码放末尾,保留接口原始数组。
sortedClubOptions() {
return (this.clubOptions || []).slice().sort((left, right) => {
const leftCode = left.clubCode == null ? "" : String(left.clubCode);
const rightCode = right.clubCode == null ? "" : String(right.clubCode);
if (!leftCode && rightCode) return 1;
if (leftCode && !rightCode) return -1;
if (leftCode < rightCode) return -1;
if (leftCode > rightCode) return 1;
return 0;
});
},
// 收支类型选项
INCOME_TYPE_OPTIONS() {
return [
@@ -780,8 +802,9 @@ layout("/layouts/platform.html"){
async fetchClubList() {
const resp = await this.$axios.post("/platform/club/examine/apply/getClubsByRole");
this.clubOptions = resp.data || [];
if (this.clubOptions.length > 0) {
this.formData.clubId = this.clubOptions[0].id;
if (this.sortedClubOptions.length > 0) {
// 新增登记默认选中编码最小的社团;编辑回显仍使用原登记社团。
this.$set(this.formData, "clubId", this.sortedClubOptions[0].id);
await this.handleClubChange(this.formData.clubId);
}
},
@@ -833,17 +856,50 @@ layout("/layouts/platform.html"){
return [...sortedDefaults, ...customItems];
},
// 获取社团成员数量
async fetchClubUserNum(clubId) {
const resp = await this.$axios.post("/platform/club/examine/apply/getClubUserNum", {
clubId,
year: this.formData.year
});
if (resp.code === 0) {
this.formData.changeUserNum = resp.data || [];
} else {
this.$message.warning(resp.msg);
// 2026年10月2日wxg屏蔽操作:原统计请求逐行注释保留。
// // 获取社团成员数量
// async fetchClubUserNum(clubId) {
// const resp = await this.$axios.post("/platform/club/examine/apply/getClubUserNum", {
// clubId,
// year: this.formData.year
// });
// if (resp.code === 0) {
// this.formData.changeUserNum = resp.data || [];
// } else {
// this.$message.warning(resp.msg);
// }
// },
//
// 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。
fetchClubUserNum(clubId) {
const sequence = ++this.memberStatisticsSequence;
this.$set(this.formData, "changeUserNum", []);
this.memberStatisticsWarnings = [];
if (!clubId || !this.formData.year) {
this.memberStatisticsLoading = false;
return Promise.resolve();
}
this.memberStatisticsLoading = true;
return this.$axios.post("/platform/club/examine/apply/getClubUserNum", {
clubId: clubId,
year: this.formData.year
}).then((resp) => {
// 切换年度或社团时只接收最近一次请求,避免旧条件的结果覆盖当前统计。
if (sequence !== this.memberStatisticsSequence) return;
if (resp.code === 0) {
const rows = resp.data || [];
this.$set(this.formData, "changeUserNum", rows);
this.memberStatisticsWarnings = rows.length ? rows[0].statisticsWarnings || [] : [];
} else {
this.memberStatisticsWarnings = [resp.msg || "成员变化统计获取失败,请核查"];
}
}).catch(() => {
if (sequence === this.memberStatisticsSequence) {
this.memberStatisticsWarnings = ["成员变化统计请求失败,请检查后重新选择社团或年度"];
}
}).finally(() => {
if (sequence === this.memberStatisticsSequence) this.memberStatisticsLoading = false;
});
},
// 获取成员名单
async fetchMemberList(clubId) {
@@ -1101,13 +1157,22 @@ layout("/layouts/platform.html"){
},
/************************ 事件处理方法 ************************/
// 年度变化处理
handleYearChange: function() {
// 2026年10月2日wxg屏蔽操作:保留原年度切换事件。
// // 年度变化处理
// handleYearChange: function() {
// this.checkIsRegistered();
// },
// 2026年10月2日22:00点开始新开发的代码;实际整改:2026年10月3日 10:14:04;操作人:wxg。所选年度改变后同步刷新成员变化表。
handleYearChange() {
this.checkIsRegistered();
return this.fetchClubUserNum(this.formData.clubId);
},
// 社团变化处理
async handleClubChange(val) {
if (!val) return;
if (!val) {
this.fetchClubUserNum(val);
return;
}
const club = this.clubOptions.find(item => item.id === val);
// 更新社团基础信息
this.formData.due = club?.due || "";
@@ -25,6 +25,14 @@ layout("/layouts/platform.html"){
<search-item label="在职状态">
<dict-select code="USER_STATE" placeholder="请选择人员状态" clearable v-model="pageForm.userState"></dict-select>
</search-item>
<!-- 2026年10月3日 19:42:45;操作人:wxg:搜索按钮前提供退会日期范围,覆盖审核、系统处理及历史直接移出。 -->
<search-item label="退会时间查询">
<el-date-picker v-model="pageForm.exitDateRange" :picker-options="exitDatePickerOptions"
@change="exitDateRangeChange" type="datetimerange" clearable align="right"
start-placeholder="开始日期" end-placeholder="结束日期" range-separator="-"
format="yyyy-MM-dd" value-format="yyyy-MM-dd" style="width: 100%">
</el-date-picker>
</search-item>
</search>
</el-card>
@@ -76,7 +84,23 @@ layout("/layouts/platform.html"){
},
data() {
return {
clubList: []
clubList: [],
pageForm: { exitDateRange: [], exitDateStart: null, exitDateEnd: null },
// 2026年10月3日 19:42:45;操作人:wxg:复用通用查询和分页,快捷范围按自然月计算。
pageDataUrl: "/platform/club/infoManage/exitManage/pageData",
exitDatePickerOptions: {
shortcuts: [
{ text: "当前年度", onClick: picker => {
picker.$emit("pick", [this.$moment().startOf("year").toDate(), new Date()])
} },
{ text: "近半年", onClick: picker => {
picker.$emit("pick", [this.$moment().subtract(6, "months").startOf("day").toDate(), new Date()])
} },
{ text: "近三个月", onClick: picker => {
picker.$emit("pick", [this.$moment().subtract(3, "months").startOf("day").toDate(), new Date()])
} }
]
}
}
},
methods: {
@@ -85,28 +109,35 @@ layout("/layouts/platform.html"){
this.$refs.infoRef.onOpen(row)
})
},
async getClubList() {
const resp = await this.$axios.post("/platform/club/infoManage/exitManage/getClubList")
if (resp.code === 0) {
this.$set(this, "clubList", resp.data)
}
getClubList() {
return this.$axios.post("/platform/club/infoManage/exitManage/getClubList").then(resp => {
if (resp.code === 0) {
// 仅调整当前页面的社团选项顺序;编码为空时排在有编码的社团之后。
const clubs = (resp.data || []).slice().sort((left, right) => {
const leftCode = left.clubCode == null ? "" : String(left.clubCode)
const rightCode = right.clubCode == null ? "" : String(right.clubCode)
if (!leftCode && rightCode) return 1
if (leftCode && !rightCode) return -1
return leftCode < rightCode ? -1 : leftCode > rightCode ? 1 : 0
})
this.$set(this, "clubList", clubs)
}
})
},
exitTypeName(row) {
// 2026年10月3日 19:42:45;操作人:wxg:系统处理退会独立标识,保留原有两类名称。
if (row.exitType === "SYSTEM_EXIT") return "系统处理"
return row.exitType === "AUDIT_EXIT" ? "审核退会" : "直接移出"
},
async pageData() {
const resp = await this.$axios.post("/platform/club/infoManage/exitManage/pageData", this.pageForm)
if (resp.code === 0) {
this.$set(this, "tableData", resp.data.list)
this.$set(this.pageForm, "totalCount", resp.data.totalCount)
} else {
this.$message.warning(resp.msg)
}
/** 2026年10月3日 19:42:45;操作人:wxg:清空范围时同时清空接口参数,筛选后由 mixin 重置页码。 */
exitDateRangeChange(val) {
this.$set(this.pageForm, "exitDateStart", val && val.length === 2 ? val[0] : null)
this.$set(this.pageForm, "exitDateEnd", val && val.length === 2 ? val[1] : null)
this.doSearch()
}
},
async created() {
await this.getClubList()
await this.pageData()
created() {
this.getClubList().then(() => this.pageData())
}
})
</script>
@@ -86,6 +86,8 @@ const clubExitInfo = {
},
getExitTypeName() {
const exitType = this.row.exitType || this.viewData.exitType
// 2026年10月3日 19:42:45;操作人:wxg:详情与列表保持一致,mode=2 显示系统处理。
if (exitType === "SYSTEM_EXIT") return "系统处理"
return exitType === "AUDIT_EXIT" ? "审核退会" : "直接移出"
},
openChart() {
@@ -1,4 +1,4 @@
const CLUB_INFO_MANAGE_TEMPLATE = {
const CLUB_INFO_MANAGE_TEMPLATE = {
template: `
<div class="club-manage-panel">
<el-card class="club-manage-query-card" shadow="never">
@@ -7,29 +7,44 @@
<el-input placeholder="请输入姓名或工号" clearable v-model="pageForm.searchKeyword"></el-input>
</search-item>
<search-item label="人员类型">
<dict-select code="USER_PERSON_TYPE" placeholder="请选择人员类型" clearable v-model="pageForm.personType"></dict-select>
<dict-select code="USER_PERSON_TYPE" placeholder="请选择人员类型" clearable v-model="pageForm.personType" :disabled="pageForm.focusMembers"></dict-select>
</search-item>
<search-item label="在职状态">
<dict-select clearable code="USER_STATE" placeholder="请选择人员状态" v-model="pageForm.userState"></dict-select>
</search-item>
<!-- 2026年10月3日 19:28:31;操作人:wxg:查询标签明确为入会时间,日期筛选逻辑保持不变。 -->
<search-item label="入会时间查询" class="club-join-date-query">
<el-date-picker v-model="pageForm.joinDateRange" :picker-options="joinDatePickerOptions"
@change="joinDateRangeChange" type="datetimerange" clearable align="right"
start-placeholder="开始日期" end-placeholder="结束日期" range-separator="-"
format="yyyy-MM-dd" value-format="yyyy-MM-dd" style="width: 100%">
</el-date-picker>
</search-item>
</search>
</el-card>
<el-card shadow="never" class="mt10 club-manage-list-card">
<table-tool label="会员信息">
<template slot="content">
<el-tag :effect="pageForm.focusMembers ? 'dark' : 'plain'" @click="toggleFocusMembers"
class="mr10" style="cursor: pointer">重点关注名单</el-tag>
<span class="text-danger">提醒:再次点击可切换</span>
</template>
<template v-if="$auth.hasRoleOr(['SYSADMIN', 'SCHOOL_UNION_ADMIN'])">
<el-button size="small" type="primary" icon="el-icon-download" @click="exportRegistrationDoc">导出登记表</el-button>
<el-button @click="openImport" size="small" type="primary">导入成员</el-button>
<el-button size="small" type="primary" @click="openUserAdd(true)">录入成员</el-button>
<el-button size="small" type="primary" @click="openUserAdd(false)">设置理事机构</el-button>
</template>
<el-radio-group @change="doSearch" style="margin-left: 10px" v-model="pageForm.radioType" size="small">
<el-radio-group @change="memberTabChange" style="margin-left: 10px" v-model="pageForm.radioType" size="small">
<el-radio-button label="3">全部</el-radio-button>
<el-radio-button label="2">社团成员</el-radio-button>
<!-- 修改时间:2026-10-03 12:47:38;操作人:wxg;会员页签统一使用“会员”称谓,保留原筛选值。 -->
<el-radio-button label="2">社团会员</el-radio-button>
<el-radio-button label="1">理事机构</el-radio-button>
<el-radio-button label="4">历史会员</el-radio-button>
</el-radio-group>
</table-tool>
<el-table v-loading="tableLoading" :data="tableData" :height="tableHeight"
<el-table :key="pageForm.radioType" v-loading="tableLoading" :data="tableData" :height="tableHeight"
row-key="id" ref="tableRef" @sort-change='pageOrder'>
<el-table-column :index="indexMethod" label="序号" type="index" width="70"></el-table-column>
<el-table-column label="工号" prop="loginName" show-overflow-tooltip sortable></el-table-column>
@@ -37,11 +52,15 @@
<el-table-column label="性别" prop="sex" show-overflow-tooltip sortable></el-table-column>
<el-table-column label="电话" prop="mobile" show-overflow-tooltip></el-table-column>
<el-table-column label="人员类型" prop="personType" show-overflow-tooltip sortable></el-table-column>
<!-- 展示用户当前人员属性,便于结合人员类型及在职状态核实会员信息。 -->
<el-table-column label="人员属性" prop="userAttribute" show-overflow-tooltip sortable></el-table-column>
<el-table-column label="在职状态" prop="userState" show-overflow-tooltip sortable></el-table-column>
<el-table-column label="所属单位" prop="unitName" sortable show-overflow-tooltip></el-table-column>
<el-table-column label="身份" prop="roleName" show-overflow-tooltip></el-table-column>
<el-table-column label="入会时间" prop="joinTime" show-overflow-tooltip sortable></el-table-column>
<el-table-column label="操作">
<!-- 历史成员仅供查询,退出时间取申请记录,不提供会员关系修改入口。 -->
<el-table-column v-if="pageForm.radioType === '4'" label="退出时间" prop="exitTime" show-overflow-tooltip sortable></el-table-column>
<el-table-column v-if="pageForm.radioType !== '4'" label="操作">
<template slot-scope="{row}">
<el-dropdown>
<el-button size="mini">
@@ -54,8 +73,11 @@
</el-dropdown-item>
<!--<el-dropdown-item @click.native="viewInfo(row)">查看注册信息</el-dropdown-item>
<el-dropdown-item @click.native="exportInfo(row)">导出登记表</el-dropdown-item>-->
<!-- 临时屏蔽删除入口,保留原调用以便后续恢复。
<el-dropdown-item @click.native="userDelete(row)">删除</el-dropdown-item>
<!--<el-dropdown-item @click.native="exitClub(row)">退会</el-dropdown-item>-->
-->
<!-- 管理端直接退会沿用现有接口,提交期间禁用入口以防重复操作。 -->
<el-dropdown-item :disabled="formLoading" @click.native="exitClub(row)">退会</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
@@ -157,6 +179,37 @@
},
data() {
return {
pageForm: {
focusMembers: false,
joinDateRange: [],
joinDateStart: null,
joinDateEnd: null,
pageOrderName: "joinTime",
pageOrderBy: "descending"
},
// 快捷范围按自然月计算,月底由 moment 自动调整为目标月份的有效日期。
joinDatePickerOptions: {
shortcuts: [
{
text: "当前年度",
onClick: picker => {
picker.$emit("pick", [this.$moment().startOf("year").toDate(), new Date()])
}
},
{
text: "近半年",
onClick: picker => {
picker.$emit("pick", [this.$moment().subtract(6, "months").startOf("day").toDate(), new Date()])
}
},
{
text: "近三个月",
onClick: picker => {
picker.$emit("pick", [this.$moment().subtract(3, "months").startOf("day").toDate(), new Date()])
}
}
]
},
roleCodeFormData: {},
roleCodeFormRules: {
userName: [{ required: true, message: "必填", trigger: ["blur"] }],
@@ -183,6 +236,28 @@
"file-import": httpVueLoader("/components/plugins/sysImport/index.vue?v=" + new Date().getTime())
},
methods: {
/** 启用重点关注时同时筛选自聘人员和其他人员,清空单选条件以避免冲突;取消后恢复普通查询。 */
toggleFocusMembers() {
this.$set(this.pageForm, "focusMembers", !this.pageForm.focusMembers)
if (this.pageForm.focusMembers) {
this.$set(this.pageForm, "personType", "")
}
this.doSearch()
},
/** 页签切换只调整业务排序,查询和重置页码继续复用 initTableMixins。 */
memberTabChange() {
this.$set(this.pageForm, "pageOrderName", this.pageForm.radioType === "4" ? "exitTime" : "joinTime")
this.$set(this.pageForm, "pageOrderBy", "descending")
this.doSearch()
},
/** 日期范围始终筛选入会时间;当前会员按入会时间倒序,历史成员按退出时间倒序。 */
joinDateRangeChange(val) {
this.$set(this.pageForm, "joinDateStart", val && val.length === 2 ? val[0] : null)
this.$set(this.pageForm, "joinDateEnd", val && val.length === 2 ? val[1] : null)
this.$set(this.pageForm, "pageOrderName", this.pageForm.radioType === "4" ? "exitTime" : "joinTime")
this.$set(this.pageForm, "pageOrderBy", "descending")
this.doSearch()
},
openImport() {
this.importData = {
isfg: 1,
@@ -316,21 +391,29 @@
}
}
},
async exitClub(row) {
const confirm = await this.$confirm("确定要将其退会吗?", "提示", {
exitClub(row) {
if (this.formLoading) return
return this.$confirm("确定要将其退会吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
if (this.formLoading) return
this.$set(this, "formLoading", true)
return this.$axios.post("/platform/club/infoManage/manage/exitClub", { id: row.id }).then(resp => {
if (resp.code === 0) {
this.$message.success(resp.msg)
return this.pageData()
} else {
this.$message.warning(resp.msg)
}
}).finally(() => {
// 接口或列表刷新异常时也释放提交状态,允许后续操作。
this.$set(this, "formLoading", false)
})
}, () => {
// 取消或关闭确认框时,不发送退会请求。
})
if (confirm === "confirm") {
const resp = await this.$axios.post("/platform/club/infoManage/manage/exitClub", { id: row.id })
if (resp.code === 0) {
this.$message.success(resp.msg)
await this.pageData()
} else {
this.$message.warning(resp.msg)
}
}
},
//导出登记表
@@ -369,4 +452,3 @@
this.pageData()
}
}
@@ -42,6 +42,16 @@ layout("/layouts/platform.html"){
#app .club-manage-query-card {
flex: 0 0 auto;
}
/* 日期范围框预留约 320px,包含标签的查询项同步加宽;窄屏仍可换行并适配容器。 */
#app .club-manage-query-card .search .club-join-date-query {
flex: 0 0 420px;
width: 420px;
max-width: 100%;
}
#app .club-manage-query-card .club-join-date-query .search-item-option {
flex: 1 1 auto;
min-width: 0;
}
#app .club-manage-list-card {
flex: 1 1 auto;
min-height: 0;
@@ -71,6 +81,7 @@ layout("/layouts/platform.html"){
<div class="club-tree-scroll">
<el-tree
:data="treeData"
node-key="id"
ref="treeRef"
:expand-on-click-node="false"
:props="{children: 'children', label: 'clubName'}"
@@ -221,12 +232,24 @@ layout("/layouts/platform.html"){
this.currentTreeData = data
this.currentTreeNode = node
this.$nextTick(() => {
this.$refs.clubInfo.pageData()
// 一级节点展示社团总览,只有具体社团节点才需要刷新会员列表。
if (node.level === 2 && this.$refs.clubInfo) {
this.$refs.clubInfo.pageData()
}
})
},
getTreeData() {
this.$axios.post("/platform/club/infoManage/manage/getClubTreeData").then((res) => {
this.treeData = res.data
this.$nextTick(() => {
// 首次进入时选中一级节点,同步树高亮与右侧总览;后续刷新保留用户选择。
if (!this.currentTreeNode && this.treeData && this.treeData.length > 0) {
const rootData = this.treeData[0]
this.$refs.treeRef.setCurrentKey(rootData.id)
this.$set(this, "currentTreeData", rootData)
this.$set(this, "currentTreeNode", this.$refs.treeRef.getNode(rootData.id))
}
})
})
},
async userRemoteMethod(query) {
@@ -21,8 +21,12 @@ const SCHOOL_CLUB_MANAGE_TEMPLATE = {
</el-card>
<el-card shadow="never" class="mt10 club-manage-list-card">
<table-tool label="社团列表">
<template>
<template slot="content">
<!-- 一级节点展示社团总览时,引导用户从左侧树选择社团查看详细信息。 -->
<span v-if="parentNode.currentTreeNode && parentNode.currentTreeNode.level === 1"
style="display: block; text-align: center; color: #f56c6c;">
温馨提醒:选择左侧树中的某个社团,可查询该社团的详细信息。
</span>
</template>
</table-tool>
<el-table v-loading="tableLoading" :data="tableData" :height="tableHeight"
@@ -28,14 +28,14 @@ layout("/layouts/platform.html"){
@change="doSearch"
></el-date-picker>
</search-item>
<search-item label="社团名称" v-if="$auth.hasRoleOr(['SYSADMIN'])">
<search-item label="社团名称">
<el-select clearable filterable placeholder="请选择社团" @change="doSearch"
style="width: 100%;" v-model="pageForm.clubId">
<el-option :label="item.clubName" :value="item.id"
v-for="item in clubs"></el-option>
</el-select>
</search-item>
<search-item label="关键字">
<search-item label="姓名工号">
<el-input v-model="pageForm.searchKeyword" placeholder="请输入工号或者姓名查询"
clearable @keyup.enter="doSearch"></el-input>
</search-item>
@@ -61,8 +61,14 @@ layout("/layouts/platform.html"){
<el-button @click="batchOperatePay" size="small" type="primary">
批量设置
</el-button>
<!-- 2026年10月3日 14:33:18;操作人:wxg:筛选当前会员状态,按钮风格沿用历史拨付页面,首次进入默认在会。 -->
<el-radio-group v-model="pageForm.memberState" size="small" @change="memberStateChange" class="ml5">
<el-radio-button label="">全部</el-radio-button>
<el-radio-button label="1">在会</el-radio-button>
<el-radio-button label="0">退会</el-radio-button>
</el-radio-group>
</table-tool>
<el-table :data="tableData" :height="tableHeight" row-key="id" @selection-change="handleSelectionChange" ref="tableRef">
<el-table :data="tableData" :height="tableHeight" v-loading="tableLoading" row-key="id" @selection-change="handleSelectionChange" ref="tableRef">
<el-table-column
:reserve-selection="true"
type="selection"
@@ -75,15 +81,16 @@ layout("/layouts/platform.html"){
<el-table-column label="所属单位" prop="unitName" sortable show-overflow-tooltip></el-table-column>
<el-table-column label="加入时间" prop="applyTime" sortable show-overflow-tooltip>
<template v-slot="{ row }">
{{ $moment(row.applyTime).format('YYYY-MM-DD HH:mm:ss') }}
{{ row.applyTime ? $moment(row.applyTime).format('YYYY-MM-DD') : '' }}
</template>
</el-table-column>
<el-table-column label="在职状态" prop="userState" sortable show-overflow-tooltip>
<!-- 离退休人员有退休日期时补充退休年份;其他类型或日期缺失时保留原值。 -->
<el-table-column label="人员类型" prop="personType" min-width="120" sortable show-overflow-tooltip>
<template v-slot="{ row }">
<span v-if="row.userState === '退休' && row.retireDate">
{{ row.userState + '(' + row.retireDate + ')' }}
</span>
<span v-else>{{ row.userState }}</span>
<span v-if="row.personType === '离退休人员' && row.retireDate">
{{ row.personType + '(' + $moment(row.retireDate).format('YYYY') + ')' }}
</span>
<span v-else>{{ row.personType }}</span>
</template>
</el-table-column>
<el-table-column label="当前状态" prop="count" sortable show-overflow-tooltip>
@@ -97,10 +104,12 @@ layout("/layouts/platform.html"){
<el-tooltip placement="bottom">
<div slot="content">
是否具备拨付资格,将按以下规则判定:<br/>
第一步:缴费记录必须是已缴费状态,否则直接判定无资格;<br/>
第二步:如果在职状态不是“退休”,则直接判定有领取资格;<br/>
第三步:如果是“退休”状态但未有退休日期信息,判定无领取资格;<br/>
第四步:如果是“退休”状态且填写了退休日期,当您的退休年份 ≥ 缴费记录年份(无缴费年份则按今年算)时,判定有资格,反之则无。
第一步:当前状态为“退会”,直接判定不拨付;<br/>
第二步:缴费记录必须是已缴费状态,否则判定不拨付;<br/>
第三步:如果人员类型不是“离退休人员”,则直接判定有领取资格;<br/>
第四步:如果人员类型为“离退休人员”但未有退休日期信息,判定无领取资格;<br/>
第五步:如果人员类型为“离退休人员”且填写了退休日期,当您的退休年份 ≥ 缴费记录年份(无缴费年份则按今年算)时,判定有资格,反之则无。<br/>
修改缴费状态时重新计算并保存;退会时保存为不拨付。列表读取已保存的拨付结果。
</div>
<span>拨付状态<i class="el-icon-question"></i></span>
</el-tooltip>
@@ -153,7 +162,7 @@ layout("/layouts/platform.html"){
<span class="dialog-footer" slot="footer">
<el-button @click="row = {}; payDialogVisible = false">取消</el-button>
<el-button @click="onBatchHandle" type="primary">确定</el-button>
<el-button @click="onBatchHandle" :loading="formLoading" type="primary">确定</el-button>
</span>
</el-dialog>
</div>
@@ -170,7 +179,10 @@ layout("/layouts/platform.html"){
return {
pageForm: {
payed: null,
// 2026年10月3日 14:33:18;操作人:wxg:列表默认锚点及首次请求均为“在会”。
memberState: '1',
year: new Date().getFullYear() + '',
clubId: null,
},
clubs: [],
row: {},
@@ -178,9 +190,16 @@ layout("/layouts/platform.html"){
payDialogVisible: false,
payed: false,
multipleSelection: [],
pageRequestSequence: 0,
}
},
methods: {
// 2026年10月3日 14:33:18;操作人:wxg:切换会员状态清空原勾选;复用 mixin 搜索方法回到第一页。
memberStateChange() {
this.$set(this, "multipleSelection", [])
if (this.$refs.tableRef) this.$refs.tableRef.clearSelection()
this.doSearch()
},
handleSelectionChange(val) {
this.multipleSelection = val
},
@@ -221,48 +240,64 @@ layout("/layouts/platform.html"){
}
this.operateRequest(data)
},
// 2026年10月3日 09:51:00;操作人:wxg:缴费设置会同步重算并保存拨付状态;列表刷新读取保存结果。
operateRequest(data) {
let message
if (this.payed === false) {
message = '您确定要设置为未缴费吗,此操作将影响该成员的拨付状态。'
message = '您确定要设置为未缴费吗?此操作将同步保存为不拨付。'
} else {
message = '您确定要设置为缴费吗?'
message = '您确定要设置为已缴费吗?此操作将按退会状态、人员类型及退休年份重新计算并保存拨付状态。'
}
this.$confirm(message, '温馨提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
const resp = await this.$axios.post("/platform/club/pay/handle", {
.then(() => {
this.formLoading = true
return this.$axios.post("/platform/club/pay/handle", {
ids: JSON.stringify(data),
payed: this.payed,
}).then(resp => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.doSearch()
this.payDialogVisible = false
this.multipleSelection = []
this.$refs.tableRef.clearSelection()
} else {
this.$message.error(resp.msg)
}
}).finally(() => {
this.formLoading = false
})
if (resp.code === 0) {
this.$message.success(resp.msg)
this.doSearch()
this.payDialogVisible = false
this.multipleSelection = []
this.$refs.tableRef.clearSelection()
} else {
this.$message.error(resp.msg)
}
})
.catch(() => {})
},
async pageData() {
const resp = await this.$axios.post("/platform/club/pay/pageData", this.pageForm)
if (resp.code === 0) {
this.tableData = resp.data.list
this.pageForm.totalCount = resp.data.totalCount
} else {
this.$message.warning(resp.msg)
}
// 每次只请求当前分页,连续切换条件时仅接收最后一次响应,避免旧页覆盖新结果。
pageData() {
const requestSequence = ++this.pageRequestSequence
this.tableLoading = true
return this.$axios.post("/platform/club/pay/pageData", this.pageForm).then(resp => {
if (requestSequence !== this.pageRequestSequence) return
if (resp.code === 0) {
this.tableData = resp.data.list || []
this.$set(this.pageForm, "totalCount", resp.data.totalCount || 0)
this.updateClubInfoListTableHeight()
} else {
this.$message.warning(resp.msg)
}
}).finally(() => {
if (requestSequence === this.pageRequestSequence) this.tableLoading = false
})
}
},
async created() {
this.clubs = await this.$businessTool.listClubByRole()
await this.pageData()
created() {
// 下拉选项与缴费列表共用后端授权范围,不复用其他业务的社团权限口径。
this.$axios.post("/platform/club/pay/listClubs").then(resp => {
if (resp.code === 0) this.clubs = resp.data || []
})
this.pageData()
}
})
</script>
@@ -56,7 +56,7 @@ layout("/layouts/platform.html"){
<el-table-column prop="clubName" label="申请社团" show-overflow-tooltip></el-table-column>
<el-table-column prop="mode" label="申请模式">
<template slot-scope="scope">
<el-tag size="mini" v-if="scope.row.mode" type="success">加入</el-tag>
<el-tag size="mini" v-if="scope.row.mode === 1" type="success">加入</el-tag>
<el-tag size="mini" v-else type="danger">退出</el-tag>
</template>
</el-table-column>
@@ -90,7 +90,7 @@ layout("/layouts/platform.html"){
{{formData.taskName}}
</div>
<el-form :model="formData" ref="formRef" label-width="80px" :rules="formRules" label-suffix="">
<el-form-item v-if="formData.mode === true" label="缴费状态" prop="payed"
<el-form-item v-if="formData.mode === 1" label="缴费状态" prop="payed"
:rules="[{required:true,message:'必填',trigger:['change','blur']}]">
<span class="form-item-tooltip">
(说明:请确认{{ formData.userName }}当年是否缴费)
@@ -28,8 +28,10 @@ layout("/layouts/platform.html"){
<el-table-column prop="clubName" label="社团名称"></el-table-column>
<el-table-column prop="mode" label="申请模式">
<template v-slot="scope">
<el-tag size="mini" v-if="scope.row.mode" type="success">加入</el-tag>
<el-tag size="mini" v-else type="danger">退出</el-tag>
<el-tag size="mini" v-if="scope.row.mode === 1" type="success">加入</el-tag>
<el-tag size="mini" v-else-if="scope.row.mode === 0" type="danger">退出</el-tag>
<!-- 系统直接退会以 mode=2 标识,不属于待提交的 WF 申请。 -->
<el-tag size="mini" v-else-if="scope.row.mode === 2" type="info">系统处理</el-tag>
</template>
</el-table-column>
<el-table-column prop="applyDate" label="申请时间"></el-table-column>
@@ -42,10 +44,10 @@ layout("/layouts/platform.html"){
<el-table-column label="操作" width="300">
<template v-slot="{row}">
<el-button @click="openView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === true" @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === false" @click="exitSubmitAgain(row)" size="mini" type="primary">提交</el-button>
<el-button v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === 1" @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === 0" @click="exitSubmitAgain(row)" size="mini" type="primary">提交</el-button>
<el-button v-if="row.canRevoke" size="mini" type="danger" @click="onRevoke(row)">撤回</el-button>
<el-button v-if="row.taskKey === 'startTask' || !row.instanceId" @click="onDelete(row.id)" size="mini" type="danger">
<el-button v-if="row.mode !== 2 && (row.taskKey === 'startTask' || !row.instanceId)" @click="onDelete(row.id)" size="mini" type="danger">
删除
</el-button>
</template>
@@ -49,7 +49,7 @@ layout("/layouts/platform.html"){
<el-table-column prop="clubName" label="申请社团" show-overflow-tooltip></el-table-column>
<el-table-column prop="mode" label="申请模式">
<template slot-scope="scope">
<el-tag size="mini" v-if="scope.row.mode" type="success">加入</el-tag>
<el-tag size="mini" v-if="scope.row.mode === 1" type="success">加入</el-tag>
<el-tag size="mini" v-else type="danger">退出</el-tag>
</template>
</el-table-column>
File diff suppressed because it is too large Load Diff
@@ -13,24 +13,24 @@ layout("/layouts/platform.html"){
<el-input placeholder="请输入姓名或工号" clearable v-model="pageForm.searchKeyword"></el-input>
</search-item>
<search-item label="所属工会" v-if="isAdmin">
<el-select v-model="pageForm.unionId" @change="doSearch" style="width: 100%"
placeholder="请选择所属工会" clearable>
<el-option v-for="item in unionOptions"
:value="item.id"
:key="item.id"
:label="item.name"
></el-option>
</el-select>
<el-select v-model="pageForm.unionId" @change="doSearch" style="width: 100%"
placeholder="请选择所属工会" clearable>
<el-option v-for="item in unionOptions"
:value="item.id"
:key="item.id"
:label="item.name"
></el-option>
</el-select>
</search-item>
<search-item label="所属单位" v-if="isAdmin">
<el-select v-model="pageForm.unitId" @change="doSearch" style="width: 100%"
placeholder="请选择所属单位" clearable>
<el-option v-for="item in unitOptions"
:value="item.id"
:key="item.id"
:label="item.name"
></el-option>
</el-select>
<el-select v-model="pageForm.unitId" @change="doSearch" style="width: 100%"
placeholder="请选择所属单位" clearable>
<el-option v-for="item in unitOptions"
:value="item.id"
:key="item.id"
:label="item.name"
></el-option>
</el-select>
</search-item>
<search-item label="报销项目">
<el-select v-model="pageForm.reimburseProject" @change="doSearch" style="width: 100%"
@@ -95,9 +95,13 @@ layout("/layouts/platform.html"){
<el-table-column label="操作" fixed="right" width="350px">
<template slot-scope="{row}">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="isAdmin && row.stateId == 3" @click="openEdit(row)" size="mini" type="primary">实际金额</el-button>
<el-button v-if="row.stateId == 3" @click="doPrint(row)" size="mini" type="primary">打印</el-button>
<el-button v-if="$auth.hasRole('SYSADMIN')" @click="onDelete(row.id)" size="mini" type="danger">删除</el-button>
<el-button v-if="isAdmin && row.stateId == 3" @click="openEdit(row)" size="mini" type="primary">
实际金额
</el-button>
<el-button @click="doPrint(row)" size="mini" type="primary">打印</el-button>
<el-button v-if="$auth.hasRole('SYSADMIN')" @click="onDelete(row.id)" size="mini" type="danger">
删除
</el-button>
</template>
</el-table-column>
</el-table>
@@ -124,13 +128,12 @@ layout("/layouts/platform.html"){
</div>
<script nonce="${cspNonce!}">
<!--#include('../info.js'){}#-->
new Vue({
el: "#app",
store,
dicts: ["UNION_REIMBURSE_TYPE","UNION_REIMBURSE_PROJECT"],
dicts: ["UNION_REIMBURSE_TYPE", "UNION_REIMBURSE_PROJECT"],
//分页数据
mixins: [initTableMixins],
components: {
@@ -149,8 +152,8 @@ layout("/layouts/platform.html"){
},
editFormRules: {
realMoney: [
{ required: true, message: '请输入实际报销金额', trigger: 'blur' },
{ pattern: /^([0-9]+)(\.[0-9]{1,2})?$/, message: '请输入正确的金额格式', trigger: 'blur' }
{required: true, message: '请输入实际报销金额', trigger: 'blur'},
{pattern: /^([0-9]+)(\.[0-9]{1,2})?$/, message: '请输入正确的金额格式', trigger: 'blur'}
]
}
}
@@ -204,7 +207,7 @@ layout("/layouts/platform.html"){
}
},
onView(row) {
this.$refs.guava.edit(()=>{
this.$refs.guava.edit(() => {
this.showApprovalForm = false
this.$refs.unionReimburseInfoRef.onOpen(row)
})
@@ -266,11 +269,8 @@ layout("/layouts/platform.html"){
window.open('/platform/unionReimburse/collect/exportSkr?year=' + year + "&unionId=" + unionId + '&unitId=' + unitId + "&reimburseProject=" + reimburseProject)
},
doPrint(row) {
if (row.reimburseProject != "UNION_REIMBURSE_PROJECT_1") {
window.location.href = "/platform/unionReimburse/mine/ActExport?id=" + (row.id || '') + "&Print=true"
} else {
window.location.href = "/platform/unionReimburse/mine/ConExport?id=" + (row.id || '') + "&Print=true"
}
// 所有状态均可下载审批单 PDF,复用公共下载的 POST 请求、错误提示和 loading 收尾。
this.$downLoad("/platform/unionReimburse/collect/exportPdf", {id: row.id})
},
onExport() {
this.$downLoad('/platform/unionReimburse/collect/onExport', this.pageForm)
@@ -130,38 +130,38 @@ const unionReimburseInfo = {
<span>{{ viewData.condolenceTime | dateFormat }}</span>
</el-descriptions-item>
<el-descriptions-item label="结婚时间" v-if="viewData.condolenceTypeId === 'cf4ce7af322d464f8f86d818fcc00203'">
<el-descriptions-item label="结婚时间" v-if="condolenceTypeCode === 'A'">
<span>{{ viewData.marryTime | dateFormat }}</span>
</el-descriptions-item>
<el-descriptions-item label="生育时间" v-if="viewData.condolenceTypeId === 'c90cb10dce6542e99ae271bee6fe8cc0'">
<el-descriptions-item label="生育时间" v-if="condolenceTypeCode === 'D'">
<span>{{ viewData.fertilityTime | dateFormat }}</span>
</el-descriptions-item>
<el-descriptions-item label="住院病由" v-if="viewData.condolenceTypeId === '4e316737e71047e0b01aea78524c1416'">
<el-descriptions-item label="住院病由" v-if="condolenceTypeCode === 'E'">
<span>{{ viewData.hospitalCausation }}</span>
</el-descriptions-item>
<el-descriptions-item label="住院时间" v-if="viewData.condolenceTypeId === '4e316737e71047e0b01aea78524c1416'">
<el-descriptions-item label="住院时间" v-if="condolenceTypeCode === 'E'">
<span v-if="viewData.hospitalizationStartTime && viewData.hospitalizationEndTime">
{{ $moment(viewData.hospitalizationStartTime).format('YYYY-MM-DD') }} 至 {{ $moment(viewData.hospitalizationEndTime).format('YYYY-MM-DD') }}
</span>
<span v-else>暂无住院时间</span>
</el-descriptions-item>
<el-descriptions-item label="入住医院" v-if="viewData.condolenceTypeId === '4e316737e71047e0b01aea78524c1416'">
<el-descriptions-item label="入住医院" v-if="condolenceTypeCode === 'E'">
<span>{{ viewData.hospital }}</span>
</el-descriptions-item>
<el-descriptions-item label="当年次数" v-if="viewData.condolenceTypeId === '4e316737e71047e0b01aea78524c1416'">
<el-descriptions-item label="当年次数" v-if="condolenceTypeCode === 'E'">
<span>{{ viewData.hospitalCount }}</span>
</el-descriptions-item>
<el-descriptions-item label="去逝时间" v-if="viewData.condolenceTypeId === '60f03a87b62b4823855814afdbf5672a'">
<el-descriptions-item label="去逝时间" v-if="condolenceTypeCode === 'G' || condolenceTypeCode === 'H'">
<span>{{ viewData.deathTime | dateFormat }}</span>
</el-descriptions-item>
<el-descriptions-item label="与被慰问人关系" v-if="viewData.condolenceTypeId === '60f03a87b62b4823855814afdbf5672a'">
<el-descriptions-item label="与被慰问人关系" v-if="condolenceTypeCode === 'H'">
<span>{{ viewData.condolenceRelationship }}</span>
</el-descriptions-item>
@@ -174,10 +174,6 @@ const unionReimburseInfo = {
<span>{{ viewData.paymentNotes }}</span>
</el-descriptions-item>
<el-descriptions-item label="备注" :span="3" v-if="viewData.reimburseProject === 'UNION_REIMBURSE_PROJECT_1'">
<span>{{ viewData.notes }}</span>
</el-descriptions-item>
<el-descriptions-item label="附件" :span="3">
<file-preview v-if="viewData.files && viewData.files.length > 0"
:files="viewData.files"
@@ -204,15 +200,6 @@ const unionReimburseInfo = {
<el-col :span="24">
<el-form-item label="支付内容" label-width="100px">
<el-input :autosize="{ minRows: 4, maxRows: 8}"
:value="viewData.paymentNotes"
readonly
type="textarea"></el-input>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="备注" label-width="100px">
<el-input :autosize="{ minRows: 3, maxRows: 6}"
:value="viewData.notes"
readonly
type="textarea"></el-input>
@@ -240,7 +227,17 @@ const unionReimburseInfo = {
</template>
</el-table-column>
<el-table-column label="发票号码" min-width="140" prop="invoiceNo"></el-table-column>
<el-table-column align="right" label="发票金额" min-width="110">
<el-table-column align="right" label="税前金额" min-width="110">
<template slot-scope="scope">
<span>{{ scope.row.amountWithoutTax == null ? '-' : formatMoney(scope.row.amountWithoutTax) }}</span>
</template>
</el-table-column>
<el-table-column align="right" label="税额" min-width="100">
<template slot-scope="scope">
<span>{{ scope.row.taxAmount == null ? '-' : formatMoney(scope.row.taxAmount) }}</span>
</template>
</el-table-column>
<el-table-column align="right" label="价税合计" min-width="110">
<template slot-scope="scope">
<span>{{ formatMoney(scope.row.invoiceAmount) }}</span>
</template>
@@ -299,6 +296,11 @@ const unionReimburseInfo = {
}
},
computed: {
// 详情接口按关联类型返回业务编码,避免不同环境的类型 ID 影响字段显示。
condolenceTypeCode() {
return this.viewData.reimburseProject === "UNION_REIMBURSE_PROJECT_1"
? (this.viewData.condolenceTypeCode || "") : ""
},
showInvoiceTab() {
return !!this.viewData.reimburseProject
},
@@ -86,10 +86,9 @@ layout("/layouts/platform.html"){
<el-table-column label="操作" fixed="right" width="300px">
<template slot-scope="{row}">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button v-if="[2, 3, 4, 5].includes(Number(row.stateId))" @click="doExport(row)" size="mini" type="primary">导出</el-button>
<el-button v-if="row.stateId == 1 || row.stateId == 5" @click="onEdit(row)" size="mini" type="primary">编辑</el-button>
<el-button v-if="row.stateId == 2" @click="onRevoke(row.id)" size="mini" type="danger">撤回</el-button>
<el-button v-if="row.stateId == 3" @click="doExport(row)" size="mini" type="primary">导出报销表</el-button>
<el-button v-if="row.stateId == 3" @click="doPrint(row)" size="mini" type="primary">打印</el-button>
<el-button v-if="row.stateId == 1" @click="onDelete(row.id)" size="mini" type="danger">删除</el-button>
</template>
</el-table-column>
@@ -171,19 +170,8 @@ layout("/layouts/platform.html"){
},
doExport(row) {
if (row.reimburseProject != "UNION_REIMBURSE_PROJECT_1") {
window.location.href = "/platform/unionReimburse/mine/ActExport?id=" + (row.id || '')
} else {
window.location.href = "/platform/unionReimburse/mine/ConExport?id=" + (row.id || '')
}
},
doPrint(row) {
if (row.reimburseProject != "UNION_REIMBURSE_PROJECT_1") {
window.location.href = "/platform/unionReimburse/mine/ActExport?id=" + (row.id || '') + "&Print=true"
} else {
window.location.href = "/platform/unionReimburse/mine/ConExport?id=" + (row.id || '') + "&Print=true"
}
// 复用公共下载的POST请求、错误提示和loading收尾,由后端选择对应审批单模板。
this.$downLoad("/platform/unionReimburse/mine/exportPdf", {id: row.id})
}
},
@@ -212,7 +212,7 @@ layout("/layouts/platform.html"){
this.showApprovalForm = true
this.formData = {
id: row.id,
reviewOpinion: '通过'
reviewOpinion: '已付款成功'
}
this.$refs.unionReimburseInfoRef.onOpen(row)
})
@@ -136,9 +136,9 @@ layout("/layouts/platform.html"){
this.$set(this.formData, 'isRead', true)
this.$commonUtil.previewFile({
suffix: "pdf",
downloadPath: "/platform/sys/file/download?id=orpgde84uggjmpflbob83tadan",
downloadPath: "/platform/sys/file/download?id=15eblmambgh3hoj7hg20n4obsg",
name: "中国地质大学(武汉)教职工重大疾病互助基金管理办法.pdf",
id: "orpgde84uggjmpflbob83tadan"
id: "15eblmambgh3hoj7hg20n4obsg"
})
},
isReadChange() {
@@ -43,10 +43,15 @@ layout("/layouts/platform.html"){
v-if="$auth.hasPermission('medicalMutualAid.aidFund.manage.createPayList')" class="mr10">
当年未缴费【基金会员】名单
</el-tag>
<el-tag :effect="pageForm.personTypeChanged?'dark':'plain'"
@click="pageForm.personTypeChanged=!pageForm.personTypeChanged;doSearch();"
v-if="$auth.hasPermission('medicalMutualAid.aidFund.manage.createPayList')" class="mr10">
人员类型变化名单
</el-tag>
<el-button @click="batchExit" size="small"
type="danger" v-if="pageForm.notPayedCurrentYear"
v-if="$auth.hasPermission('medicalMutualAid.aidFund.manage.createPayList')">
type="danger"
v-if="$auth.hasPermission('medicalMutualAid.aidFund.manage.createPayList') && (pageForm.notPayedCurrentYear || pageForm.personTypeChanged)">
批量设置为退会
</el-button>
@@ -95,15 +100,18 @@ layout("/layouts/platform.html"){
</el-dropdown>
</table-tool>
<el-table :data="tableData" :size="tableSize" @sort-change="pageOrder"
<!-- 仅名单模式切换时重建表格,清除动态列切换后残留的单元格渲染及 tooltip 宽度。 -->
<el-table :key="pageForm.personTypeChanged ? 'member-type-changed' : 'member-all'"
:data="tableData" :size="tableSize" @sort-change="pageOrder"
ref="table" row-key="id" style="width: 100%" v-loading="tableLoading">
<el-table-column
key="member-selection"
type="selection"
width="55">
</el-table-column>
<el-table-column :index="indexMethod" header-align="center" label="序号"
<el-table-column key="member-index" :index="indexMethod" header-align="center" label="序号"
type="index" width="60px"></el-table-column>
<el-table-column
:label="column.label"
@@ -114,7 +122,7 @@ layout("/layouts/platform.html"){
header-align="center"
min-width="100px"
show-overflow-tooltip
v-for="column in tableColumns"
v-for="column in visibleTableColumns"
>
<template v-slot="{row}" v-if="column.prop=='aidFundMemberUserType'">
<dict-tag :options="dict.type.AIDFUND_MEMBER_USER_TYPE"
@@ -127,7 +135,8 @@ layout("/layouts/platform.html"){
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="300">
<!-- 操作列使用独立标识,避免动态增减人员类型列时复用其他列的渲染状态。 -->
<el-table-column key="member-actions" label="操作" fixed="right" width="300">
<template slot-scope="{row}">
<el-button @click="onView(row)" size="mini" type="primary">查看</el-button>
<el-button @click="onEditMemberType(row)" size="mini" type="primary">修改基金会员类型
@@ -207,7 +216,7 @@ layout("/layouts/platform.html"){
<span class="dialog-footer" slot="footer">
<el-button @click="batchChangeTypeVisible = false">取 消</el-button>
<el-button @click="doBatchChangeType" type="primary">提 交</el-button>
<el-button @click="doBatchChangeType" :loading="formLoading" :disabled="formLoading" type="primary">提 交</el-button>
</span>
</el-dialog>
@@ -238,7 +247,7 @@ layout("/layouts/platform.html"){
</el-form>
<span class="dialog-footer" slot="footer">
<el-button @click="changeTypeVisible = false">取 消</el-button>
<el-button @click="doEditChangeType" type="primary">提 交</el-button>
<el-button @click="doEditChangeType" :loading="formLoading" :disabled="formLoading" type="primary">提 交</el-button>
</span>
</el-dialog>
@@ -258,7 +267,8 @@ layout("/layouts/platform.html"){
pageForm: {
unitId: '',
unionId: '',
notPayedCurrentYear: false
notPayedCurrentYear: false,
personTypeChanged: false
},
tableColumns: [
{prop: "loginname", label: "工号"},
@@ -268,6 +278,7 @@ layout("/layouts/platform.html"){
{prop: "unitName", label: "所属单位"},
{prop: "unionName", label: "所属工会"},
{prop: "aidFundMemberUserType", label: "基金会员类型"},
{prop: "personType", label: "人员类型"},
{prop: "aidFundMemberJoinTime", label: "加入时间"},
],
showApprovalForm: false,
@@ -292,11 +303,29 @@ layout("/layouts/platform.html"){
changeTableData: []
}
},
computed: {
// 仅在筛选人员类型变化名单时展示当前人员类型,取消筛选后恢复原有列表列。
visibleTableColumns() {
return this.tableColumns.filter(column => column.prop !== "personType" || this.pageForm.personTypeChanged)
},
},
watch: {
// 动态列完成渲染后再计算宽度,使右侧固定操作列与表头、表体保持一致。
visibleTableColumns() {
this.$nextTick(() => {
if (this.$refs.table) {
this.$refs.table.doLayout()
}
})
},
},
components: {
'info': AID_FUND_INFO,
},
methods: {
doBatchChangeType() {
// 提交完成前禁止重复点击,后端仍会校验历史重复记录。
if (this.formLoading) return
const selection = this.$refs.table.selection
if (selection.length === 0) {
this.$message.warning('请勾选需要变更的人员')
@@ -312,8 +341,10 @@ layout("/layouts/platform.html"){
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
if (this.formLoading) return
this.formLoading = true
const ids = selection.map(v => v.id)
this.$axios.post('/platform/medicalMutualAid/aidFund/manage/doBatchChangeType', {
return this.$axios.post('/platform/medicalMutualAid/aidFund/manage/doBatchChangeType', {
ids: JSON.stringify(ids),
changeType: this.formData.changeType
}).then(resp => {
@@ -322,9 +353,17 @@ layout("/layouts/platform.html"){
this.doSearch()
this.$refs.table.clearSelection()
this.batchChangeTypeVisible = false
} else {
// 重复校验失败时保留弹框及名单,方便操作员调整后重新提交。
this.$alert(resp.msg, '变更提醒', {type: 'warning'})
}
})
})
}).catch(error => {
// 公共 axios 拦截器会 reject 业务错误,此处保留完整重复名单供操作员核对。
if (error && error.msg) this.$alert(error.msg, '变更提醒', {type: 'warning'})
}).finally(() => {
this.formLoading = false
}).catch(() => {})
}).catch(() => {})
})
@@ -352,12 +391,16 @@ layout("/layouts/platform.html"){
this.changeTypeVisible = true
},
doEditChangeType() {
// 提交完成前禁止重复点击,后端仍会校验历史重复记录。
if (this.formLoading) return
this.$confirm('确定要变更【' + this.formData.username + '】吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$axios.post(loc() + "/doEditChangeType", {
if (this.formLoading) return
this.formLoading = true
return this.$axios.post(loc() + "/doEditChangeType", {
userId: this.formData.id,
changeType: this.formData.changeType
}).then(resp => {
@@ -365,9 +408,17 @@ layout("/layouts/platform.html"){
this.$message.success(resp.msg)
this.doSearch()
this.changeTypeVisible = false
} else {
// 保留当前选择并展示后端返回的重复变更人员及上次操作时间。
this.$alert(resp.msg, '变更提醒', {type: 'warning'})
}
})
})
}).catch(error => {
// 公共 axios 拦截器会 reject 业务错误,此处保留完整重复名单供操作员核对。
if (error && error.msg) this.$alert(error.msg, '变更提醒', {type: 'warning'})
}).finally(() => {
this.formLoading = false
}).catch(() => {})
}).catch(() => {})
},
doEditAidFundMemberUserType() {
this.$confirm('确定要修改【' + this.formData.username + '】的基金会员类型吗?', '提示', {
@@ -14,7 +14,7 @@ layout("/layouts/platform.html"){
style="width: 100%"
v-model="pageForm.year"
value-format="yyyy"
:clearable="false"
clearable
@change="doSearch"
></el-date-picker>
</search-item>
@@ -57,7 +57,8 @@ layout("/layouts/platform.html"){
<el-radio-button :label="0">未缴费</el-radio-button>
</el-radio-group>
<el-button @click="openImport"
<!-- 导入和清空均针对指定年度,清除年度后只允许跨年度查询。 -->
<el-button @click="openImport" :disabled="!pageForm.year"
size="small"
icon="el-icon-upload2"
type="primary">导入缴费记录
@@ -81,10 +82,10 @@ layout("/layouts/platform.html"){
type="primary">导出年度缴费名单
</el-button>
<el-button :disabled="tableData.length==0" :loading="tableLoading" @click="clearPayRecord"
<el-button :disabled="!pageForm.year || tableData.length==0" :loading="tableLoading" @click="clearPayRecord"
icon="el-icon-delete"
size="small"
type="danger">清空{{pageForm.year}}年缴费名单
type="danger">清空{{pageForm.year ? pageForm.year + '年' : ''}}缴费名单
</el-button>
</table-tool>
@@ -140,6 +140,7 @@ const MEMBER_CHANGE = {
<el-descriptions-item label="在职状态">
<el-form-item prop="userState">
<dict-select v-model="formData.userState" code="USER_STATE"
@change="handleRetiredStateChange"
:disabled="allowFields('userState')" style="width: 100%"></dict-select>
</el-form-item>
</el-descriptions-item>
@@ -177,7 +178,6 @@ const MEMBER_CHANGE = {
<el-form-item prop="welfareStopDate">
<el-date-picker v-model="formData.welfareStopDate"
type="date"
:disabled="isView"
placeholder="请选择福利享受截止日期"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
@@ -392,6 +392,18 @@ const MEMBER_CHANGE = {
allowFields(prop) {
return !this.allowChangeFields.map(v => v.code).includes(prop)
},
/** 在职状态选择退休时,存在“离退休人员”字典项才自动设置对应人员类型编码。 */
handleRetiredStateChange(userState) {
if (userState !== "退休") {
return
}
this.$businessTool.getDictOptions("USER_PERSON_TYPE").then((options) => {
const retiredOption = (options || []).find((item) => item.name === "离退休人员")
if (retiredOption) {
this.$set(this.formData, "personType", retiredOption.code)
}
})
},
// 管理页仅查询当前被编辑人员所属二级单位的直属三级单位,避免越级选择其他单位数据。
initThreeUnits() {
this.threeUnits = []
@@ -500,6 +512,8 @@ const MEMBER_CHANGE = {
campus,
threeUnitId,
userState,
retireDate,
welfareStopDate,
personType,
preparedBy,
userAttribute,
@@ -528,6 +542,11 @@ const MEMBER_CHANGE = {
this.$set(this.formData, "campus", campus)
this.$set(this.formData, "threeUnitId", threeUnitId)
this.$set(this.formData, "userState", userState)
// 日期选择器使用 yyyy-MM-dd 字符串,避免接口日期值无法在组件中正确回显。
this.$set(this.formData, "retireDate", retireDate
? this.$moment(retireDate).format("YYYY-MM-DD") : "")
this.$set(this.formData, "welfareStopDate", welfareStopDate
? this.$moment(welfareStopDate).format("YYYY-MM-DD") : "")
this.$set(this.formData, "personType", personType)
this.$set(this.formData, "preparedBy", preparedBy)
this.$set(this.formData, "userAttribute", userAttribute)
@@ -161,6 +161,10 @@ layout("/layouts/platform.html"){
<template scope="{row}" v-if="column.prop=='loginname'">
<el-link @click="openView(row.id)" type="primary">{{row.loginname}}</el-link>
</template>
<!-- 出生年月仅格式化表格显示值,保留原始数据供其他业务使用。 -->
<template scope="{row}" v-if="column.prop=='birthday'">
{{row.birthday ? $moment(row.birthday).format('YYYY-MM-DD') : ''}}
</template>
</el-table-column>
<el-table-column header-align="center" label="异动信息">
<el-table-column align="center" header-align="center" label="更新时间"
@@ -31,6 +31,10 @@ layout("/layouts/platform.html"){
<dict-select v-model="pageForm.personType" placeholder="请选择人员类型" @change="doSearch"
code="USER_PERSON_TYPE"></dict-select>
</search-item>
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
</search-item>
<search-item label="变更类型">
<el-select v-model="pageForm.changeType" placeholder="变更类型" @change="doSearch"
clearable filterable style="width: 100%">
@@ -42,10 +46,6 @@ layout("/layouts/platform.html"){
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
</search-item>-->
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
</search-item>
</search>
</el-card>
@@ -43,7 +43,8 @@
<dict-select v-model="pageForm.aidFundMemberUserType" placeholder="请选择人员分类" @change="doSearch"
code="AIDFUND_MEMBER_USER_TYPE"></dict-select>
</search-item>-->
<search-item label="人员属性">
<!-- 数据管理页使用独立标签筛选;其他页面继续显示人员属性下拉框。 -->
<search-item label="人员属性" v-if="!query_disable">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
</search-item>
@@ -119,6 +119,29 @@ layout("/layouts/platform.html"){
</el-link>
</div>
</div>
<!-- 人员属性按字典编码多选,选中条件同时用于列表查询和批量设置会员。 -->
<div class="search-other mt20">
<div class="search-other-label">人员属性</div>
<div>
<el-tag
v-for="item in dict.type.USER_ATTRIBUTE"
:key="item.code"
:type="item.name"
:effect="pageForm.userAttributes.includes(item.code)?'dark':'plain'"
@click="tagClick('userAttributes',item.code)">
{{ item.name }}
</el-tag>
<el-link type="danger"
v-if="dict.type.USER_ATTRIBUTE?.length && pageForm.userAttributes.length"
:underline="false"
@click="this.$set(pageForm, 'userAttributes', []); doSearch()">清空
</el-link>
<el-link :underline="false"
@click="this.$set(pageForm, 'userAttributes', (dict.type.USER_ATTRIBUTE || []).map(p=>p.code)); doSearch()"
type="success">全部
</el-link>
</div>
</div>
</el-card>
@@ -195,13 +218,14 @@ layout("/layouts/platform.html"){
'member-info': MEMBER_INFO,
'common-query': COMMON_QUERY,
},
dicts: ['USER_PERSON_TYPE', 'USER_STATE'],
dicts: ['USER_PERSON_TYPE', 'USER_STATE', 'USER_ATTRIBUTE'],
data() {
return {
visible: false,
pageForm: {
personTypes: [],
userStates: [],
userAttributes: [],
},
tableColumns: [
{ prop: "loginName", label: "工号", sortable: true },
@@ -246,6 +270,8 @@ layout("/layouts/platform.html"){
const pageForm = clone(this.pageForm)
pageForm.personTypes = JSON.stringify(pageForm.personTypes)
pageForm.userStates = JSON.stringify(pageForm.userStates)
// 人员属性标签保存编码数组,提交时按后端多选参数格式转换为 JSON 字符串。
pageForm.userAttributes = JSON.stringify(pageForm.userAttributes)
this.$axios.post("/platform/member/info/dataManage/setMemberByCnd", {
"pageForm": JSON.stringify(pageForm),
"append": append
@@ -262,6 +288,8 @@ layout("/layouts/platform.html"){
const pageForm = clone(this.pageForm)
pageForm.personTypes = JSON.stringify(pageForm.personTypes)
pageForm.userStates = JSON.stringify(pageForm.userStates)
// 人员属性标签保存编码数组,提交时按后端多选参数格式转换为 JSON 字符串。
pageForm.userAttributes = JSON.stringify(pageForm.userAttributes)
pageForm.append = append
this.$axios.post("/platform/member/info/dataManage/setWelfareMemberByCnd", {
"pageForm": JSON.stringify(pageForm),
@@ -291,13 +319,14 @@ layout("/layouts/platform.html"){
},
search(pageForm) {
if (pageForm) {
// 当前页的在职状态、人员类型使用标签数组筛选,公共查询组件回传的是字符串,
// 这里保留当前页数组,避免选择人员属性后把标签筛选状态覆盖掉。
// 公共查询组件回传 JSON 字符串;保留本页三组标签数组,避免顶部搜索覆盖选中条件。
const currentUserStates = this.pageForm.userStates
const currentPersonTypes = this.pageForm.personTypes
const currentUserAttributes = this.pageForm.userAttributes
this.pageForm = {...this.pageForm, ...pageForm}
this.$set(this.pageForm, "userStates", currentUserStates)
this.$set(this.pageForm, "personTypes", currentPersonTypes)
this.$set(this.pageForm, "userAttributes", currentUserAttributes)
}
this.doSearch()
},
@@ -305,12 +334,10 @@ layout("/layouts/platform.html"){
const pageForm = clone(this.pageForm)
pageForm.personTypes = JSON.stringify(pageForm.personTypes)
pageForm.userStates = JSON.stringify(pageForm.userStates)
this.$axios.post(loc() + "/pageData", pageForm).then((res) => {
if (res.code === 0) {
this.tableData = res.data.list
this.pageForm.totalCount = res.data.totalCount
}
})
// 人员属性标签保存编码数组,提交时按后端多选参数格式转换为 JSON 字符串。
pageForm.userAttributes = JSON.stringify(pageForm.userAttributes)
// 仅转换本页标签参数,查询、分页结果和 loading 处理复用公共 mixin。
return initTableMixins.methods.pageData.call(this, pageForm)
},
},
created() {
@@ -29,6 +29,14 @@ layout("/layouts/platform.html"){
margin-right: 8px;
}
/* 在列表标题旁说明待确认人员的来源及可能情况。 */
.pending-confirm-tip {
color: #f56c6c;
font-size: 14px;
font-weight: normal;
line-height: 1.5;
}
/* 批量变更弹窗固定在当前浏览器可视区域正中间。 */
.el-dialog.pending-batch-change-dialog {
position: absolute;
@@ -100,6 +108,10 @@ layout("/layouts/platform.html"){
<dict-select @change="doSearch" code="USER_PERSON_TYPE" placeholder="请选择人员类型"
v-model="pageForm.personType"></dict-select>
</search-item>
<search-item label="人员属性">
<dict-select v-model="pageForm.userAttributes" placeholder="请选择人员属性" @change="doSearch"
code="USER_ATTRIBUTE" multiple collapse-tags clearable></dict-select>
</search-item>
<search-item label="在职状态">
<dict-select @change="doSearch" code="USER_STATE" placeholder="请选择在职状态"
v-model="pageForm.userState"></dict-select>
@@ -109,7 +121,13 @@ layout("/layouts/platform.html"){
<el-card class="mt10 pending-confirm-table-card" shadow="never">
<table-tool label="待确认人员记录">
<template slot="content">
<span class="pending-confirm-tip">温馨提示:以下人员不存在信息中心数据中台端,而智慧工会端还继续存在的,这类人员可能离职、退休、去世。【劳动服务公司】的人员类型,不在此变更,在会员高级管理中变更。</span>
</template>
<div class="pending-list-toolbar-actions">
<el-button class="pending-batch-change-button" type="primary"
icon="el-icon-download" size="small"
@click="exportExcel">导出</el-button>
<el-button class="pending-batch-change-button" type="primary"
icon="el-icon-edit" size="small"
v-if="pageForm.pendingConfirmStatus === 1"
@@ -142,6 +160,8 @@ layout("/layouts/platform.html"){
prop="unitName" show-overflow-tooltip sortable></el-table-column>
<el-table-column align="center" header-align="center" label="人员类型" min-width="120"
prop="personType" show-overflow-tooltip sortable></el-table-column>
<el-table-column align="center" header-align="center" label="人员属性" min-width="120"
prop="userAttribute" show-overflow-tooltip sortable></el-table-column>
<el-table-column align="center" header-align="center" label="在职状态" min-width="100"
prop="userState" show-overflow-tooltip sortable></el-table-column>
<el-table-column align="center" header-align="center" label="确认状态" min-width="100"
@@ -173,18 +193,56 @@ layout("/layouts/platform.html"){
</template>
</guava>
<el-dialog title="批量变更人员类型" :visible.sync="batchDialogVisible"
<el-dialog title="批量变更" :visible.sync="batchDialogVisible"
append-to-body width="480px" custom-class="pending-batch-change-dialog"
@closed="resetBatchChange">
<el-form :model="batchChangeForm" :rules="batchChangeRules"
ref="batchChangeFormRef" label-width="100px">
<el-form-item label="人员类型" prop="personType">
<el-select v-model="batchChangeForm.personType" filterable
<el-form-item label="人员类型">
<el-select v-model="batchChangeForm.personType" clearable filterable
@change="handleBatchPersonTypeChange"
placeholder="请选择人员类型" style="width: 100%">
<el-option v-for="item in pendingPersonTypeOptions" :key="item.code"
:label="item.name" :value="item.code"></el-option>
</el-select>
</el-form-item>
<el-form-item label="在职状态">
<dict-select v-model="batchChangeForm.userState" clearable code="USER_STATE"
@change="handleBatchUserStateChange"
placeholder="请选择在职状态" style="width: 100%"></dict-select>
</el-form-item>
<el-form-item label="退休时间" prop="retireDate"
v-if="batchShowRetireDate" required>
<el-date-picker v-model="batchChangeForm.retireDate" type="date"
placeholder="请选择退休时间"
format="yyyy-MM-dd" value-format="yyyy-MM-dd"
style="width: 100%"></el-date-picker>
</el-form-item>
<el-form-item label="福利享受截止时间" prop="welfareStopDate"
v-if="batchChangeForm.userState === '退休'">
<el-date-picker v-model="batchChangeForm.welfareStopDate" type="date"
placeholder="请选择福利享受截止时间"
format="yyyy-MM-dd" value-format="yyyy-MM-dd"
style="width: 100%"></el-date-picker>
</el-form-item>
<el-form-item label="人员属性">
<dict-select v-model="batchChangeForm.userAttribute" clearable code="USER_ATTRIBUTE"
placeholder="请选择人员属性" style="width: 100%"></dict-select>
</el-form-item>
<el-form-item label="会员状态">
<el-select v-model="batchChangeForm.member" clearable
placeholder="请选择会员状态" style="width: 100%">
<el-option label="会员" :value="true"></el-option>
<el-option label="非会员" :value="false"></el-option>
</el-select>
</el-form-item>
<el-form-item label="福利会员状态">
<el-select v-model="batchChangeForm.welfareMember" clearable
placeholder="请选择福利会员状态" style="width: 100%">
<el-option label="福利会员" :value="true"></el-option>
<el-option label="非福利会员" :value="false"></el-option>
</el-select>
</el-form-item>
</el-form>
<template slot="footer">
<el-button @click="batchDialogVisible = false">取消</el-button>
@@ -208,6 +266,7 @@ layout("/layouts/platform.html"){
unionId: "",
unitId: "",
personType: "",
userAttributes: [],
userState: "",
pendingConfirmStatus: 1
},
@@ -217,20 +276,50 @@ layout("/layouts/platform.html"){
batchDialogVisible: false,
batchSubmitting: false,
batchChangeForm: {
personType: ""
personType: "",
userState: "",
retireDate: "",
welfareStopDate: "",
userAttribute: "",
member: null,
welfareMember: null
},
batchChangeRules: {
personType: [{required: true, message: "请选择人员类型", trigger: "change"}]
retireDate: [{
// 批量目标为退休状态或离退休人员时,必须为全部勾选人员指定退休时间。
validator: (rule, value, callback) => {
if (this.batchShowRetireDate && !value) {
callback(new Error("请选择退休时间"))
return
}
callback()
},
trigger: ["change", "blur"]
}]
},
pendingTableHeight: 300,
pendingTableResizeObserver: null
}
},
computed: {
/** 按字典编码识别离退休人员,统一批量退休时间的展示、校验和提交条件。 */
batchShowRetireDate() {
const retiredOption = this.pendingPersonTypeOptions.find((item) => item.name === "离退休人员")
return this.batchChangeForm.userState === "退休"
|| (!!retiredOption && this.batchChangeForm.personType === retiredOption.code)
}
},
methods: {
/** 列表与导出均提交人员属性编码的 JSON 数组,保留原表单数组供多选组件回显。 */
buildQueryPageForm(source = this.pageForm) {
const pageForm = clone(source)
pageForm.userAttributes = JSON.stringify(source.userAttributes || [])
return pageForm
},
/** 分页查询待确认或已确认人员。 */
pageData() {
this.selectedPendingUsers = []
return $.post("/platform/sourcechange/pending/pageData", this.pageForm).then((resp) => {
return $.post("/platform/sourcechange/pending/pageData", this.buildQueryPageForm()).then((resp) => {
if (resp.code === 0) {
this.tableData = resp.data.list
this.pageForm.totalCount = resp.data.totalCount
@@ -285,6 +374,10 @@ layout("/layouts/platform.html"){
this.selectedPendingUsers = []
this.doSearch()
},
/** 按当前列表筛选条件和排序导出全部匹配人员,不受当前分页大小限制。 */
exportExcel() {
this.$downLoad("/platform/sourcechange/pending/exportExcel", this.buildQueryPageForm())
},
/** 保存当前页勾选的待确认人员,供批量变更使用。 */
handleSelectionChange(selection) {
this.selectedPendingUsers = (selection || []).filter((item) => item.pendingConfirmStatus === 1)
@@ -295,11 +388,41 @@ layout("/layouts/platform.html"){
this.$message.warning("请先勾选需要变更的人员")
return
}
this.batchChangeForm.personType = ""
this.resetBatchChangeForm()
this.batchDialogVisible = true
},
/** 判断批量表单是否至少填写一个需要变更的字段,布尔 false 也是有效值。 */
hasBatchChangeField() {
return !!this.batchChangeForm.personType
|| !!this.batchChangeForm.userState
|| !!this.batchChangeForm.userAttribute
|| this.batchChangeForm.member !== null && this.batchChangeForm.member !== undefined && this.batchChangeForm.member !== ""
|| this.batchChangeForm.welfareMember !== null && this.batchChangeForm.welfareMember !== undefined && this.batchChangeForm.welfareMember !== ""
},
/** 选择离退休人员时同步设置退休状态;选择其他类型不推断或恢复在职状态。 */
handleBatchPersonTypeChange(personType) {
const retiredOption = this.pendingPersonTypeOptions.find((item) => item.name === "离退休人员")
if (retiredOption && personType === retiredOption.code) {
this.$set(this.batchChangeForm, "userState", "退休")
}
},
/** 退休状态自动匹配人员类型;切换状态保留退休时间,仅清理隐藏的福利截止时间。 */
handleBatchUserStateChange(userState) {
if (userState === "退休") {
const retiredOption = this.pendingPersonTypeOptions.find((item) => item.name === "离退休人员")
if (retiredOption) {
this.$set(this.batchChangeForm, "personType", retiredOption.code)
}
return
}
this.$set(this.batchChangeForm, "welfareStopDate", "")
},
/** 二次确认批量操作,防止误改多名人员。 */
confirmBatchChange() {
if (!this.hasBatchChangeField()) {
this.$message.warning("请至少选择一个需要批量变更的字段")
return
}
this.$refs.batchChangeFormRef.validate((valid) => {
if (!valid) {
return
@@ -315,11 +438,33 @@ layout("/layouts/platform.html"){
/** 调用批量接口,在一个事务中修改人员类型并完成确认。 */
submitBatchChange() {
const userIds = this.selectedPendingUsers.map((item) => item.id)
const params = {
userIds: JSON.stringify(userIds)
}
if (this.batchChangeForm.personType) {
params.personType = this.batchChangeForm.personType
}
if (this.batchChangeForm.userState) {
params.userState = this.batchChangeForm.userState
}
// 退休时间仅在退休状态或离退休人员类型下提交,隐藏时保留表单值但不参与变更。
if (this.batchShowRetireDate && this.batchChangeForm.retireDate) {
params.retireDate = this.batchChangeForm.retireDate
}
if (this.batchChangeForm.userState === "退休" && this.batchChangeForm.welfareStopDate) {
params.welfareStopDate = this.batchChangeForm.welfareStopDate
}
if (this.batchChangeForm.userAttribute) {
params.userAttribute = this.batchChangeForm.userAttribute
}
if (this.batchChangeForm.member !== null && this.batchChangeForm.member !== undefined && this.batchChangeForm.member !== "") {
params.member = this.batchChangeForm.member
}
if (this.batchChangeForm.welfareMember !== null && this.batchChangeForm.welfareMember !== undefined && this.batchChangeForm.welfareMember !== "") {
params.welfareMember = this.batchChangeForm.welfareMember
}
this.batchSubmitting = true
this.$axios.post("/platform/sourcechange/pending/batchChange", {
userIds: JSON.stringify(userIds),
personType: this.batchChangeForm.personType
}).then((resp) => {
this.$axios.post("/platform/sourcechange/pending/batchChange", params).then((resp) => {
if (resp.code === 0) {
this.$message.success(resp.msg)
this.batchDialogVisible = false
@@ -333,10 +478,20 @@ layout("/layouts/platform.html"){
this.batchSubmitting = false
})
},
/** 重置批量表单,空值字段不会参与批量整改。 */
resetBatchChangeForm() {
this.batchChangeForm.personType = ""
this.batchChangeForm.userState = ""
this.$set(this.batchChangeForm, "retireDate", "")
this.$set(this.batchChangeForm, "welfareStopDate", "")
this.batchChangeForm.userAttribute = ""
this.batchChangeForm.member = null
this.batchChangeForm.welfareMember = null
},
/** 关闭弹窗后清理表单,避免保留上次选择的人员类型。 */
resetBatchChange() {
this.batchSubmitting = false
this.batchChangeForm.personType = ""
this.resetBatchChangeForm()
if (this.$refs.batchChangeFormRef) {
this.$refs.batchChangeFormRef.clearValidate()
}
@@ -103,6 +103,7 @@
<el-descriptions-item label="在职状态">
<el-form-item prop="userState">
<dict-select v-model="pendingChangeForm.userState" code="USER_STATE"
@change="handlePendingRetiredStateChange"
:disabled="pendingAllowFields('userState')" style="width: 100%"></dict-select>
</el-form-item>
</el-descriptions-item>
@@ -111,6 +112,7 @@
<el-input v-if="pendingChangeReadonly" v-model="pendingChangeForm.personType"
readonly></el-input>
<el-select v-else v-model="pendingChangeForm.personType" filterable
@change="handlePendingPersonTypeChange"
placeholder="请选择人员类型" style="width: 100%">
<el-option v-for="item in pendingPersonTypeOptions" :key="item.code"
:label="item.name" :value="item.code"></el-option>
@@ -125,15 +127,18 @@
</el-form-item>
</el-descriptions-item>
<template v-if="pendingChangeForm.userState === '退休'">
<!-- 退休日期满足退休状态或离退休人员类型任一条件即显示并要求填写。 -->
<template v-if="pendingShowRetireDate">
<el-descriptions-item label="退休日期">
<el-form-item prop="retireDate">
<el-form-item prop="retireDate" :required="!pendingChangeReadonly">
<el-date-picker v-model="pendingChangeForm.retireDate" type="date"
:disabled="pendingAllowFields('retireDate')"
placeholder="请选择退休日期" format="yyyy-MM-dd"
value-format="yyyy-MM-dd" style="width: 100%"></el-date-picker>
</el-form-item>
</el-descriptions-item>
</template>
<template v-if="pendingChangeForm.userState === '退休'">
<el-descriptions-item label="福利享受截止日期">
<el-form-item prop="welfareStopDate">
<el-date-picker v-model="pendingChangeForm.welfareStopDate" type="date"
@@ -11,7 +11,18 @@ const PENDING_MEMBER_CHANGE = {
},
pendingChangeRules: {
username: [{required: false, message: "必填", trigger: ["change", "blur"]}],
sex: [{required: false, message: "必填", trigger: ["change", "blur"]}]
sex: [{required: false, message: "必填", trigger: ["change", "blur"]}],
retireDate: [{
// 退休状态或离退休人员类型需要填写退休日期,其他人员不受此校验限制。
validator: (rule, value, callback) => {
if (this.pendingShowRetireDate && !value) {
callback(new Error("请选择退休日期"))
return
}
callback()
},
trigger: ["change", "blur"]
}]
},
pendingPersonTypeOptions: [],
pendingThreeUnits: [],
@@ -20,10 +31,38 @@ const PENDING_MEMBER_CHANGE = {
pendingChangeLoadingUserId: ""
}
},
computed: {
/** 满足退休状态或离退休人员类型任一条件时展示退休日期;人员类型按字典编码识别。 */
pendingShowRetireDate() {
const retiredOption = this.pendingPersonTypeOptions.find((item) => item.name === "离退休人员")
return this.pendingChangeForm.userState === "退休"
|| (!!retiredOption && this.pendingChangeForm.personType === retiredOption.code)
}
},
methods: {
/** 编辑模式仅允许修改人员类型;查看模式下所有字段均为只读。 */
/** 手动选择离退休人员时同步设置退休状态;选择其他类型时保留当前状态与日期。 */
handlePendingPersonTypeChange(personType) {
const retiredOption = this.pendingPersonTypeOptions.find((item) => item.name === "离退休人员")
if (retiredOption && personType === retiredOption.code) {
this.$set(this.pendingChangeForm, "userState", "退休")
}
},
/** 编辑模式仅允许修改待确认业务确认所需字段;查看模式下所有字段均为只读。 */
pendingAllowFields(prop) {
return this.pendingChangeReadonly || prop !== "personType"
const editableFields = ["personType", "userState", "userAttribute", "member", "welfareMember",
"retireDate", "welfareStopDate"]
return this.pendingChangeReadonly || !editableFields.includes(prop)
},
/** 在职状态选择退休时,存在“离退休人员”字典项才自动设置对应人员类型编码。 */
handlePendingRetiredStateChange(userState) {
if (userState !== "退休") {
return
}
const retiredOption = this.pendingPersonTypeOptions.find((item) => item.name === "离退休人员")
if (retiredOption) {
this.$set(this.pendingChangeForm, "personType", retiredOption.code)
}
},
/** 从人员类型字典中仅保留待确认业务允许变更的两种类型。 */
@@ -91,8 +130,9 @@ const PENDING_MEMBER_CHANGE = {
preparedBy: user.preparedBy,
userAttribute: user.userAttribute,
aidFundMemberUserType: user.aidFundMemberUserType,
retireDate: user.retireDate,
welfareStopDate: user.welfareStopDate,
retireDate: user.retireDate ? this.$moment(user.retireDate).format("YYYY-MM-DD") : "",
welfareStopDate: user.welfareStopDate
? this.$moment(user.welfareStopDate).format("YYYY-MM-DD") : "",
arrivalAtSchoolDate: user.arrivalAtSchoolDate,
families: user.families || [],
personalData: user.personalData
@@ -97,7 +97,7 @@ layout("/layouts/platform_h5.html"){
<template v-slot="{row}">
<table-column label="社团名称">{{ row.clubName || '--' }}</table-column>
<table-column label="所属工会">{{ row.unionName || '--' }}</table-column>
<table-column label="申请类型">{{ row.mode ? '加入社团' : '退出社团' }}</table-column>
<table-column label="申请类型">{{ row.mode === 1 ? '加入社团' : '退出社团' }}</table-column>
<table-column label="申请时间">{{ row.applyDate || '--' }}</table-column>
<table-column label="所属单位">{{ row.unitName || '--' }}</table-column>
<table-column label="当前节点">{{ row.curTaskName || '--' }}</table-column>
@@ -119,7 +119,7 @@ layout("/layouts/platform_h5.html"){
<section class="club-audit-card">
<h2 class="club-audit-card__title">{{ formData.taskName || '社团审核' }}</h2>
<van-form ref="formRef">
<van-field v-if="formData.mode === true" v-model="formData.tf_payed" class="club-audit-payment-field" name="tf_payed" label="缴费状态" :rules="[{ required: true, message: '请选择缴费状态' }]" required>
<van-field v-if="formData.mode === 1" v-model="formData.tf_payed" class="club-audit-payment-field" name="tf_payed" label="缴费状态" :rules="[{ required: true, message: '请选择缴费状态' }]" required>
<template #input>
<van-radio-group v-model="formData.tf_payed" direction="horizontal"><van-radio name="0">未缴费</van-radio><van-radio name="1">已缴费</van-radio></van-radio-group>
</template>
@@ -67,10 +67,10 @@ layout("/layouts/platform_h5.html"){
<div v-if="row" class="club-card-header"><div class="club-card-header__heading"><div class="club-card-header__title">{{ row.userName || '--' }}</div><div class="club-card-header__sub">工号:{{ row.loginName || '--' }}</div></div><enum-tag class="club-card-header__status" v-if="row.instanceState" :value="row.instanceState" name="ProcessInstanceStateEnum" label_key="message" size="small"></enum-tag></div>
</template>
<template v-slot="{row}">
<template v-if="row"><table-column label="社团名称">{{ row.clubName || '--' }}</table-column><table-column label="申请类型">{{ row.mode ? '加入社团' : '退出社团' }}</table-column><table-column label="申请时间">{{ row.applyDate || '--' }}</table-column><table-column label="当前节点">{{ row.taskName || '--' }}</table-column></template>
<template v-if="row"><table-column label="社团名称">{{ row.clubName || '--' }}</table-column><table-column label="申请类型"><van-tag v-if="row.mode === 2" type="primary">系统处理</van-tag><span v-else>{{ row.mode === 1 ? '加入社团' : '退出社团' }}</span></table-column><table-column label="申请时间">{{ row.applyDate || '--' }}</table-column><table-column label="当前节点">{{ row.taskName || '--' }}</table-column></template>
</template>
<template #actions="{row}">
<template v-if="row"><div class="action-btn" @click="onView(row)"><van-icon name="eye-o"></van-icon><span>查看</span></div><div class="action-btn" @click="onEdit(row)" v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === true"><van-icon name="edit"></van-icon><span>编辑</span></div><div class="action-btn" @click="exitSubmitAgain(row)" v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === false"><van-icon name="passed"></van-icon><span>提交</span></div><div class="action-btn delete" @click="onRevoke(row)" v-if="row.canRevoke"><van-icon name="revoke"></van-icon><span>撤回</span></div><div class="action-btn delete" @click="onDelete(row)" v-if="row.taskKey === 'startTask' || !row.instanceId"><van-icon name="delete-o"></van-icon><span>删除</span></div></template>
<template v-if="row"><div class="action-btn" @click="onView(row)"><van-icon name="eye-o"></van-icon><span>查看</span></div><div class="action-btn" @click="onEdit(row)" v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === 1"><van-icon name="edit"></van-icon><span>编辑</span></div><div class="action-btn" @click="exitSubmitAgain(row)" v-if="(row.taskKey === 'startTask' || !row.instanceId) && row.mode === 0"><van-icon name="passed"></van-icon><span>提交</span></div><div class="action-btn delete" @click="onRevoke(row)" v-if="row.canRevoke"><van-icon name="revoke"></van-icon><span>撤回</span></div><div class="action-btn delete" @click="onDelete(row)" v-if="row.mode !== 2 && (row.taskKey === 'startTask' || !row.instanceId)"><van-icon name="delete-o"></van-icon><span>删除</span></div></template>
</template>
<template #empty>
<div v-if="listLoading" class="club-list-skeleton"><div v-for="item in 2" :key="item" class="club-skeleton-card"><div class="club-skeleton-line club-skeleton-line--title"></div><div class="club-skeleton-line"></div><div class="club-skeleton-line club-skeleton-line--short"></div></div></div>
@@ -34,7 +34,7 @@ layout("/layouts/platform_h5.html"){
<template v-slot="{index,row}">
<table-column label="姓名">{{row.userName}}</table-column>
<table-column label="工号">{{row.loginName}}</table-column>
<table-column label="申请模式">{{row.mode ? '加入' : '退出'}}</table-column>
<table-column label="申请模式">{{row.mode === 1 ? '加入' : '退出'}}</table-column>
<table-column label="申请时间">{{row.applyDate}}</table-column>
<table-column label="当前节点">{{row.curTaskName}}</table-column>
</template>
File diff suppressed because it is too large Load Diff
@@ -2,7 +2,7 @@ const UNION_REIMBURSE_INFO = {
template:
/*language=HTML*/
`
<van-popup v-model="visible" position="right" :style="{width: '100%', height: '100%'}" get-container="#app" :close-on-click-overlay="false" class="union-reimburse-info-sheet">
<van-popup v-model="visible" position="right" :style="{width: '100%', height: '100%'}" :get-container="false" :close-on-click-overlay="false" class="union-reimburse-info-sheet">
<div class="union-reimburse-info-detail">
<van-nav-bar :title="detailTitle" left-text="返回" left-arrow fixed @click-left="onClose"></van-nav-bar>
<div class="detail-container">
@@ -116,15 +116,15 @@ const UNION_REIMBURSE_INFO = {
<div class="union-info-label">慰问时间</div>
<div class="union-info-value">{{ formatDateText(viewData.condolenceTime) }}</div>
</div>
<div class="union-info-row" v-if="isCondolenceType('cf4ce7af322d464f8f86d818fcc00203')">
<div class="union-info-row" v-if="condolenceTypeCode === 'A'">
<div class="union-info-label">结婚时间</div>
<div class="union-info-value">{{ formatDateText(viewData.marryTime) }}</div>
</div>
<div class="union-info-row" v-if="isCondolenceType('c90cb10dce6542e99ae271bee6fe8cc0')">
<div class="union-info-row" v-if="condolenceTypeCode === 'D'">
<div class="union-info-label">生育时间</div>
<div class="union-info-value">{{ formatDateText(viewData.fertilityTime) }}</div>
</div>
<template v-if="isCondolenceType('4e316737e71047e0b01aea78524c1416')">
<template v-if="condolenceTypeCode === 'E'">
<div class="union-info-row">
<div class="union-info-label">住院病由</div>
<div class="union-info-value">{{ viewData.hospitalCausation }}</div>
@@ -146,12 +146,12 @@ const UNION_REIMBURSE_INFO = {
<div class="union-info-value">{{ viewData.hospitalCount }}</div>
</div>
</template>
<template v-if="isCondolenceType('60f03a87b62b4823855814afdbf5672a')">
<template v-if="condolenceTypeCode === 'G' || condolenceTypeCode === 'H'">
<div class="union-info-row">
<div class="union-info-label">去逝时间</div>
<div class="union-info-value">{{ formatDateText(viewData.deathTime) }}</div>
</div>
<div class="union-info-row">
<div class="union-info-row" v-if="condolenceTypeCode === 'H'">
<div class="union-info-label">与被慰问人关系</div>
<div class="union-info-value">{{ viewData.condolenceRelationship }}</div>
</div>
@@ -164,10 +164,6 @@ const UNION_REIMBURSE_INFO = {
<div class="union-info-label">报销事由</div>
<div class="union-info-value">{{ viewData.paymentNotes }}</div>
</div>
<div class="union-info-row">
<div class="union-info-label">备注</div>
<div class="union-info-value">{{ viewData.notes }}</div>
</div>
</div>
<div class="union-info-section" v-if="viewData.reimburseProject && viewData.reimburseProject !== 'UNION_REIMBURSE_PROJECT_1'">
@@ -188,10 +184,6 @@ const UNION_REIMBURSE_INFO = {
<div class="union-info-label">活动时间</div>
<div class="union-info-value">{{ formatDateText(viewData.activityTime) }}</div>
</div>
<div class="union-info-row">
<div class="union-info-label">支付内容</div>
<div class="union-info-value">{{ viewData.paymentNotes }}</div>
</div>
</div>
<div class="union-info-section">
@@ -212,6 +204,10 @@ const UNION_REIMBURSE_INFO = {
<div class="union-info-label">发票张数</div>
<div class="union-info-value">{{ viewData.invoiceNumber }}</div>
</div>
<div class="union-info-row">
<div class="union-info-label">支付内容</div>
<div class="union-info-value">{{ viewData.notes }}</div>
</div>
<div class="union-invoice-subtitle">发票明细表</div>
<div class="union-invoice-card-list" v-if="viewData.invoiceDetails && viewData.invoiceDetails.length > 0">
<div class="union-invoice-card" v-for="(item, index) in viewData.invoiceDetails" :key="index">
@@ -221,7 +217,15 @@ const UNION_REIMBURSE_INFO = {
<div class="union-info-value">{{ item.invoiceNo || '未填写' }}</div>
</div>
<div class="union-info-row">
<div class="union-info-label">发票金额</div>
<div class="union-info-label">税前金额</div>
<div class="union-info-value">{{ item.amountWithoutTax == null ? '未识别' : formatMoney(item.amountWithoutTax) }}</div>
</div>
<div class="union-info-row">
<div class="union-info-label">税额</div>
<div class="union-info-value">{{ item.taxAmount == null ? '未识别' : formatMoney(item.taxAmount) }}</div>
</div>
<div class="union-info-row">
<div class="union-info-label">价税合计</div>
<div class="union-info-value">{{ formatMoney(item.invoiceAmount) }}</div>
</div>
<div class="union-info-row">
@@ -308,6 +312,9 @@ const UNION_REIMBURSE_INFO = {
<slot></slot>
</div>
</div>
<div v-if="$slots.footer" class="union-reimburse-info-footer">
<slot name="footer"></slot>
</div>
</div>
</van-popup>
`,
@@ -330,24 +337,34 @@ const UNION_REIMBURSE_INFO = {
}
},
computed: {
// 详情接口按关联类型返回业务编码,避免不同环境的类型 ID 影响字段显示。
condolenceTypeCode() {
return this.viewData.reimburseProject === "UNION_REIMBURSE_PROJECT_1"
? (this.viewData.condolenceTypeCode || "") : ""
},
paymentInfoRows() {
return this.viewData.paymentDetails || []
}
},
mounted() {
this.injectInfoStyle()
const manager = window.h5HistoryLayerManager
if (manager) {
manager.ensureRegistered("union-reimburse-info")
this.$set(this, "historyLayerUnsubscribe", manager.subscribe((layers) => {
this.$set(this, "visible", layers.includes(this.historyLayerName))
}))
}
this.subscribeHistoryLayer()
},
beforeDestroy() {
if (this.historyLayerUnsubscribe) this.historyLayerUnsubscribe()
},
methods: {
// PJAX 会重置全局历史栈订阅;每次打开前重新订阅,保证返回事件能同步关闭详情。
subscribeHistoryLayer() {
if (this.historyLayerUnsubscribe) this.historyLayerUnsubscribe()
this.$set(this, "historyLayerUnsubscribe", null)
const manager = window.h5HistoryLayerManager
if (!manager) return
manager.ensureRegistered("union-reimburse-info")
this.$set(this, "historyLayerUnsubscribe", manager.subscribe((layers) => {
this.$set(this, "visible", layers.includes(this.historyLayerName))
}))
},
injectInfoStyle() {
// 当前组件是普通 JS 对象注册,Vue 不会自动注入 style 字段,这里只为本详情弹窗注入一次样式。
const styleId = "union-reimburse-info-style"
@@ -365,6 +382,7 @@ const UNION_REIMBURSE_INFO = {
this.$set(this, "detailTitle", title || "报销详情")
this.$set(this, "currentPaymentIndex", 0)
// 详情弹层的显示状态由全局历史栈统一同步,确保系统返回键与页面返回按钮关闭同一层。
this.subscribeHistoryLayer()
const manager = window.h5HistoryLayerManager
if (manager && manager.open(this.historyLayerName)) {
this.getInfo()
@@ -375,10 +393,13 @@ const UNION_REIMBURSE_INFO = {
this.getInfo()
this.getDoneTasks()
},
// 只关闭当前详情对应的历史层,避免误关闭审核确认弹框等上层业务弹框。
// 详情之上存在确认框或预览时先关闭最上层;无历史层时才直接关闭详情。
onClose() {
const manager = window.h5HistoryLayerManager
if (manager && manager.close(this.historyLayerName)) return
if (manager && manager.stack.length) {
manager.close()
return
}
this.$set(this, "visible", false)
},
formatCreateTime(value) {
@@ -394,9 +415,6 @@ const UNION_REIMBURSE_INFO = {
const numberValue = Number(value)
return isNaN(numberValue) ? value : numberValue.toFixed(2)
},
isCondolenceType(typeId) {
return this.viewData.condolenceTypeId === typeId || this.viewData.condolenceType === typeId
},
getPaymentWayText(paymentWay) {
const matchedPaymentWay = this.dict.type.UNION_REIMBURSE_PAYMENT_WAY.find(item => item.code === paymentWay)
return matchedPaymentWay ? matchedPaymentWay.name : ""
@@ -533,12 +551,19 @@ const UNION_REIMBURSE_INFO = {
color: #1989fa;
}
.detail-container {
.union-reimburse-info-detail > .detail-container {
flex: 1;
min-height: 0;
overflow: hidden;
background: #f3f7fd;
}
/* 审核按钮占独立底部区域,意见表单随正文滚动,避免被按钮遮挡或挤出可视区。 */
.union-reimburse-info-footer {
flex-shrink: 0;
background: #fff;
}
.union-info-scroll {
height: 100%;
overflow-y: auto;
@@ -44,7 +44,7 @@ layout("/layouts/platform_h5.html"){
</main>
<union-reimburse-info ref="unionReimburseInfoRef" history-layer-name="union-reimburse-review-info">
<template v-if="showApprovalForm">
<div v-if="showApprovalForm" class="union-reimburse-approval">
<section class="union-reimburse-approval-card">
<h2 class="union-reimburse-approval-card__title">{{formData.taskName || '校工会审核'}}</h2>
<van-form ref="formRef">
@@ -52,7 +52,9 @@ layout("/layouts/platform_h5.html"){
<van-field v-model="formData.reviewOpinion" class="union-reimburse-approval-field" name="reviewOpinion" type="textarea" rows="6" label="" placeholder="请输入审批意见" :rules="[{required: true, message: '请填写审批意见'}]" maxlength="100" show-word-limit></van-field>
</van-form>
</section>
<div class="union-reimburse-approval-actions">
</div>
<template slot="footer">
<div v-if="showApprovalForm" class="union-reimburse-approval-actions">
<van-button :loading="formLoading" type="danger" block @click="handleTaskAction(5)">退回</van-button>
<van-button :loading="formLoading" type="danger" block @click="handleTaskAction(4)">不同意</van-button>
<van-button :loading="formLoading" type="primary" block @click="handleTaskAction(3)">同意</van-button>
@@ -173,9 +175,12 @@ layout("/layouts/platform_h5.html"){
this.$set(this, "formData", {
id: row.id,
taskName: "校工会审核",
reviewOpinion: ""
reviewOpinion: "已付款成功"
})
// 先完成审批意见及插槽渲染,再打开详情,避免首次打开时表单尚未更新。
this.$nextTick(() => {
this.$refs.unionReimburseInfoRef.onOpen(row, "报销审核")
})
this.$refs.unionReimburseInfoRef.onOpen(row, "报销审核")
},
onLoadingChange(state) { this.$set(this, "listLoading", state.loading) },
onNavBack() { const manager = window.h5HistoryLayerManager; if (manager && manager.stack.length) { manager.close(); return } historyBack() },