Merge remote-tracking branch 'origin/master'

This commit is contained in:
zhouhefeng
2026-09-29 10:07:19 +08:00
24 changed files with 602 additions and 197 deletions
@@ -243,6 +243,27 @@ public class FlowDesignController {
return Result.success();
}
/**
* 设置流程设计在流程中心的展示状态。
* @param id 流程设计 ID
* @param centerEnabled 传 1 表示显示,传 0 表示隐藏
* @return Result;code 为 0 表示保存成功,非 0 时 msg 说明错误,data 无业务数据
*/
@At
@ApiOperation("设置流程中心显示状态")
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("flow.design")
public Result updateCenterEnabled(@Param("id") Long id, @Param("centerEnabled") Integer centerEnabled) {
if (id == null || centerEnabled == null || (centerEnabled != 0 && centerEnabled != 1)) {
return Result.error("流程设计或显示状态无效");
}
int updated = processDesignService.update(Chain.make("centerEnabled", centerEnabled), Cnd.where(ProcessDesign::getId, "=", id));
if (updated == 0 && processDesignService.fetch(id) == null) {
return Result.error("流程设计不存在");
}
return Result.success();
}
@At
@SaCheckLogin
@ApiOperation("获取流程设计任务参与者处理类")
@@ -68,6 +68,11 @@ public class ProcessDesign extends BaseModel {
@Column
private Integer isDeployed;
@Comment("是否在流程中心显示(1显示,0隐藏)")
@Column
@Default("1")
private Integer centerEnabled;
@Comment("排序编码")
@Column
@Default("0")
@@ -11,6 +11,7 @@ import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql;
import org.nutz.dao.util.cri.Static;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap;
@@ -57,6 +58,8 @@ public class SysV4ServController {
$condition
""");
Cnd cnd = Cnd.NEW();
// 只隐藏流程中心入口;历史流程实例及已有流程定义保持原有状态。
cnd.and(new Static("COALESCE(d.centerEnabled, 1) = 1"));
if (StrUtil.isNotBlank(keyword)) {
cnd.where().andLike("t.disPlayName", keyword);
}
@@ -23,7 +23,6 @@ import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeOrigin;
import com.budwk.app.zhgh.staffmanage.member.constant.MemberChangeType;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
@@ -161,10 +160,6 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
for (Sys_user_source source : sources) {
Sys_user user = userMap.get(source.getLoginname());
source.setMember(null);
String unitId = source.getUnitId();
if (StringUtils.isNotBlank(unitId) && unitId.length() == 5) {
source.setUnitId(unitId.substring(0, 3));
}
// 创建用户对象
Sys_user u = new Sys_user();
@@ -180,6 +175,10 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
} else {
// 修改现有用户
u.setId(user.getId());
// 拉取数据未分配三级单位时写入空字符串,避免 updateIgnoreNull 保留人员原有的三级单位。
if (StrUtil.isBlank(source.getThreeUnitId())) {
u.setThreeUnitId("");
}
/*
* aidFundMember 由基金会员业务维护,数据中心源表默认值为 0。
* 全量更新已有用户时置空,避免 updateIgnoreNull 把已迁移的基金会员标识覆盖为非会员。
@@ -383,14 +382,13 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
Sql resetStatusSql = Sqls.create("""
UPDATE sys_user userInfo
SET userInfo.pendingConfirmStatus = @noneStatus
WHERE userInfo.personType IN ('教职工', '劳务派遣人员')
AND userInfo.pendingConfirmStatus <> @noneStatus
WHERE userInfo.pendingConfirmStatus <> @noneStatus
AND userInfo.member='1'
AND EXISTS (
SELECT 1
FROM sys_user_source sourceInfo
WHERE sourceInfo.pullTime = @pullTime
AND sourceInfo.loginname = userInfo.loginname
AND sourceInfo.personType IN ('教职工', '劳务派遣人员')
)
""");
resetStatusSql.setParam("noneStatus", PENDING_CONFIRM_STATUS_NONE);
@@ -400,14 +398,13 @@ public class SysDataUserAllUpdateServiceImpl implements SysDataUserUpdateService
Sql pendingStatusSql = Sqls.create("""
UPDATE sys_user userInfo
SET userInfo.pendingConfirmStatus = @pendingStatus
WHERE userInfo.personType IN ('教职工', '劳务派遣人员')
AND IFNULL(userInfo.pendingConfirmStatus, @noneStatus) = @noneStatus
WHERE IFNULL(userInfo.pendingConfirmStatus, @noneStatus) = @noneStatus
AND userInfo.member='1'
AND NOT EXISTS (
SELECT 1
FROM sys_user_source sourceInfo
WHERE sourceInfo.pullTime = @pullTime
AND sourceInfo.loginname = userInfo.loginname
AND sourceInfo.personType IN ('教职工', '劳务派遣人员')
)
""");
pendingStatusSql.setParam("pendingStatus", PENDING_CONFIRM_STATUS_PENDING);
@@ -132,6 +132,39 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
Date nowDate = new Date();
// 人员单位号先与单位表批量比对,缺失单位只同步一次,避免逐人查询单位表。
Set<String> sourceUnitIds = rawDataList.stream()
.map(raw -> raw.getStr("SZDWH"))
.filter(StrUtil::isNotBlank)
.collect(Collectors.toSet());
Map<String, Sys_unit> unitMap = dao().query(Sys_unit.class, Cnd.NEW()).stream()
.collect(Collectors.toMap(Sys_unit::getId, unit -> unit));
if (!unitMap.keySet().containsAll(sourceUnitIds)) {
log.info("单位需要更新,正在同步更新");
sysDataUnitPullService.updateUnits(new ArrayList<>(unitMap.keySet()));
unitMap = dao().query(Sys_unit.class, Cnd.NEW()).stream()
.collect(Collectors.toMap(Sys_unit::getId, unit -> unit));
}
// 按单位表的实际等级判断:三级单位为3、直属二级单位为2;单位号位数不参与判断。
Map<String, String> tertiaryParentUnitIds = new HashMap<>();
for (String sourceUnitId : sourceUnitIds) {
Sys_unit unit = unitMap.get(sourceUnitId);
if (unit == null) {
log.warn("人员单位号{}在单位表中不存在,保留信息中心原始单位号", sourceUnitId);
continue;
}
if (!Integer.valueOf(3).equals(unit.getUnitLevel())) {
continue;
}
Sys_unit parentUnit = unitMap.get(unit.getParentId());
if (parentUnit == null || !Integer.valueOf(2).equals(parentUnit.getUnitLevel())) {
log.warn("三级单位{}缺少有效的二级父单位,保留信息中心原始单位号", sourceUnitId);
continue;
}
tertiaryParentUnitIds.put(sourceUnitId, parentUnit.getId());
}
// 赋值
List<Sys_user_source> latestSourceList = rawDataList.stream().map(raw -> {
Sys_user_source sysUser = new Sys_user_source();
@@ -174,7 +207,10 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
}
sysUser.setUnitName(raw.getStr("SZDW"));
sysUser.setUnitId(resolveSourceUnitId(raw.getStr("SZDWH")));
String sourceUnitId = raw.getStr("SZDWH");
String parentUnitId = tertiaryParentUnitIds.get(sourceUnitId);
sysUser.setUnitId(StrUtil.blankToDefault(parentUnitId, sourceUnitId));
sysUser.setThreeUnitId(parentUnitId == null ? null : sourceUnitId);
// 设置拉取时间
sysUser.setPullTime(nowDate);
@@ -188,17 +224,6 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
dao().insert(latestSourceList);
}
// 人员的单位数据和数据库的单位数据比较,如果人员里面有单位不存在,去更新单位数据
List<String> sourceUnitIds = latestSourceList.stream().map(Sys_user_source::getUnitId).filter(StrUtil::isNotBlank).distinct().toList();
Sql sql = Sqls.create("select id from sys_unit group by id");
sql.setCallback(Sqls.callback.strList());
dao().execute(sql);
List<String> unitIds = sql.getList(String.class);
if (!new HashSet<>(unitIds).containsAll(sourceUnitIds)) {
log.info("单位需要更新,正在同步更新");
sysDataUnitPullService.updateUnits(unitIds);
}
return nowDate;
} catch (Exception e) {
throw new BaseException("数据拉取失败,{}", e.getMessage());
@@ -342,22 +367,6 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl<Sys_user_source>
return new ArrayList<>(userMap.values());
}
/**
* 根据接口返回的所在单位号转换数据源用户单位。
* 入参 rawUnitId 对应人员接口 SZDWH:5位表示三级单位,需要写入其二级父单位;其他长度保持接口原值。
* 返回值为 sys_user_source.unitId 使用的单位ID,找不到三级单位或父级为空时保留接口原始单位号,避免人员数据丢失。
*/
private String resolveSourceUnitId(String rawUnitId) {
if (StrUtil.isBlank(rawUnitId) || rawUnitId.length() != 5) {
return rawUnitId;
}
Sys_unit unit = dao().fetch(Sys_unit.class, rawUnitId);
if (unit == null || StrUtil.isBlank(unit.getParentId())) {
return rawUnitId;
}
return unit.getParentId();
}
@Async
private void updateDict(List<Sys_user_source> userSources) {
//判断是否要更新在职状态字典
@@ -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

@@ -436,6 +436,8 @@
.v4-logo {
height: 100%;
width: auto;
/* 保留 Logo 透明轮廓,将所有可见颜色统一转换为白色。 */
filter: brightness(0) invert(1);
}
.v4-nav {
@@ -694,12 +696,10 @@
<i class="fa fa-th-large"></i>
应用中心
</a>
<!--
<a href="/platform/v4/serv" data-pjax class="v4-nav-item">
<!-- <a href="/platform/v4/serv" data-pjax class="v4-nav-item">
<i class="fa fa-th-large"></i>
流程中心
</a>
-->
</a>-->
<a href="/platform/v4/todo" data-pjax class="v4-nav-item">
<i class="fa fa-th-large"></i>
待办中心
@@ -756,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()
@@ -150,7 +150,7 @@ layout("/layouts/platform.html"){
</el-descriptions-item>
<el-descriptions-item label="余额" v-if="formData.reimburseFundSource">
<el-form-item label="余额">
<el-input :value="fundBalanceText" readonly></el-input>
<el-input :value="canViewFundBalance ? fundBalanceText : '******'" readonly></el-input>
</el-form-item>
</el-descriptions-item>
@@ -820,6 +820,8 @@ layout("/layouts/platform.html"){
condolenceUserId: [{required: true, message: "请选择慰问对象", trigger: ["change", "blur"]}],
condolenceTypeId: [{required: true, message: "请选择慰问类型", trigger: ["change", "blur"]}],
// 住院慰问显示入住医院字段时,提交和重新提交均须填写非空白医院名称。
hospital: [{required: true, whitespace: true, message: "请输入入住医院", trigger: ["change", "blur"]}],
way: [{required: true, message: "请选择慰问方式", trigger: ["change", "blur"]}],
condolenceMoney: [{required: true, message: "请输入慰问金额", trigger: ["change", "blur"]}],
participants: [{required: true, message: "请填写参与随行人员", trigger: ["change", "blur"]}],
@@ -865,6 +867,10 @@ layout("/layouts/platform.html"){
}
},
computed: {
// 仅系统管理员和校工会管理员可查看真实余额,其他账号统一显示余额遮罩。
canViewFundBalance() {
return this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
},
showInvoiceTab() {
return !!this.formData.reimburseProject
},
@@ -212,7 +212,7 @@ layout("/layouts/platform.html"){
this.showApprovalForm = true
this.formData = {
id: row.id,
reviewOpinion: '通过'
reviewOpinion: '已付款成功'
}
this.$refs.unionReimburseInfoRef.onOpen(row)
})
@@ -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="更新时间"
@@ -473,7 +473,7 @@ layout("/layouts/platform_h5.html"){
<van-field
v-if="formData.reimburseFundSource"
label="余额"
v-model="formData.fundBalance"
:value="canViewFundBalance ? formData.fundBalance : '******'"
readonly
placeholder="自动查询"
></van-field>
@@ -706,7 +706,10 @@ layout("/layouts/platform_h5.html"){
@cancel="closePopup('showHospitalizationEndTimePicker')"
@confirm="onDateConfirm('hospitalizationEndTime', 'showHospitalizationEndTimePicker', $event)"></van-datetime-picker>
</van-popup>
<van-field label="入住医院" v-model="formData.hospital" placeholder="请输入入住医院"></van-field>
<!-- 住院慰问提交和重新提交时,入住医院不能为空或仅包含空白字符。 -->
<van-field label="入住医院" name="hospital" v-model="formData.hospital" required
:rules="[{ required: true, message: '请输入入住医院' }, { pattern: /\S/, message: '请输入入住医院' }]"
placeholder="请输入入住医院"></van-field>
<van-field label="当年次数" v-model="formData.hospitalCount" placeholder="请填写当年次数" type="number"></van-field>
</template>
@@ -1248,6 +1251,10 @@ layout("/layouts/platform_h5.html"){
}
},
computed: {
// 仅系统管理员和校工会管理员可查看真实余额,其他账号统一显示余额遮罩。
canViewFundBalance() {
return this.$auth.hasRoleOr(["SYSADMIN", "SCHOOL_UNION_ADMIN"])
},
paymentInfoRows() {
return this.formData.paymentDetails || []
},
@@ -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">
@@ -308,6 +308,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>
`,
@@ -336,18 +339,23 @@ const UNION_REIMBURSE_INFO = {
},
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 +373,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 +384,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) {
@@ -533,12 +545,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() },