fix: 修复日期筛选与表单参数双写、下线范围预估,并改造列表三列样式
build / build (push) Successful in 13m15s

- v29 删除前端对 notice/ScopePreview(推送范围「已选学员数」预估)的请求:
  api 方法、ScopePreviewVo 类型、dev:mock 桩、ClassScopeInput/PushScopePanel 两个组件的预估 UI 一并移除。
- v30 修复发文部门/联系组名称重复且带逗号(如「校园部,校园部」并逐次翻倍):
  postForm 原先把参数同时拼进 URL query 与 urlencoded body,Servlet 会合并两处取值、
  Spring @RequestParam String 再以「,」拼接。改为只发 body,仅在 dev:mock 下附带 query
  (vite-plugin-mock 不解析 urlencoded body),不影响线上。
- 修复时间筛选失效:DatePicker 的 Date 实例被 alova 直接 encodeURIComponent 成浏览器本地化
  日期串(Fri Aug 21 2026 ... GMT+0800),后端按 yyyy-MM-dd 解析不了。列表侧提交前归一化为
  短日期,alova-core 另加一层 Date → 本地 'yyyy-MM-dd HH:mm:ss' 的兜底归一化。
- 列表「类型 / 状态 / 阅读情况」三列改醒目样式(口径不变,仅样式):
  类型改胶囊(重要=实心红、普通=浅灰弱化);状态改彩色圆点胶囊,已发布绿/定时中蓝/
  待审批橙/已驳回红/草稿灰,已撤回、置顶改实心小徽标;阅读情况改渐变进度条 + 已读/应读·百分比,
  读满转绿。同步微调类型(80→90)、阅读情况(110→180)列宽。
- 同步更新《执行计划任务方案.md》v29 / v30 记录。
This commit is contained in:
lijing
2026-09-22 09:46:33 +08:00
parent b623e18b9b
commit a48c5f6343
8 changed files with 244 additions and 152 deletions
+97 -25
View File
@@ -28,6 +28,22 @@ function fmtDate(d: Date) {
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
}
/**
* 日期筛选值 → 接口短日期(yyyy-MM-dd)。
*
* DatePicker 的 v-model 绑定的是 **Date 实例**(视图内部值),而 alova/fetch 拼 query
* 时对参数只做 encodeURIComponent(String(v)),Date 会退化成浏览器本地化串:
* startTime=Fri%20Aug%2021%202026%2000:00:00%20GMT+0800%20(中国标准时间)
* 后端按 yyyy-MM-dd 解析不了(NoticeQuery.startTime 契约即短日期),时间筛选因此失效。
* 故发请求前统一归一化:Date 走本地日期,字符串(resetFilter 写入的默认值)取其前 10 位。
*/
function shortDate(v: unknown): string {
if (!v) return "";
if (v instanceof Date) return isNaN(v.getTime()) ? "" : fmtDate(v);
const m = String(v).match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
return m ? `${m[1]}-${m[2].padStart(2, "0")}-${m[3].padStart(2, "0")}` : "";
}
/* ============ 分页(规范 §3:customPageCount 页长记忆) ============ */
const pageNum = ref(1);
const pageSize = ref(parseInt(localStorage.getItem("customPageCount") || "10"));
@@ -47,8 +63,10 @@ async function run() {
};
if (statusType.value !== "") params.status = Number(statusType.value);
if (noticeType.value !== "") params.type = Number(noticeType.value);
if (startDt.value) params.startTime = startDt.value;
if (endDt.value) params.endTime = endDt.value;
const startTime = shortDate(startDt.value);
const endTime = shortDate(endDt.value);
if (startTime) params.startTime = startTime;
if (endTime) params.endTime = endTime;
if (!isManage.value) {
params.scope = "user";
}
@@ -226,31 +244,93 @@ function downloadFiles(row: NoticeVo & { UpFile?: string }) {
});
}
/* 状态列:草稿/已发布(定时中)/待审批(步骤进度)/已驳回 + 已撤回/置顶徽标 */
const STATUS_TAG: Record<string, string> = { pending: "warning", approved: "success", rejected: "error", none: "default" };
/* ============ 单元格视觉(纯样式,口径不变;视觉语言对齐家庭活动「我发布的」列表) ============
* render 单元格由 Table 内部渲染,不吃本文件的 scoped 样式,故统一用内联样式。
* 色板:灰 #c5c8ce / 蓝 #2d8cf0 / 橙 #ff9900 / 绿 #19be6b / 红 #ed4014(与参考列表同一套)。
*/
const C_GREY = "#c5c8ce";
const C_BLUE = "#2d8cf0";
const C_ORANGE = "#ff9900";
const C_GREEN = "#19be6b";
const C_RED = "#ed4014";
/** 状态胶囊:彩色圆点 + 文字(参考列表 statusPill 同款) */
function statusPill(h: any, hex: string, text: string) {
return h("span", { style: "display:inline-flex;align-items:center;gap:6px;font-size:13px;color:#515a6e;white-space:nowrap;" }, [
h("i", { style: `width:8px;height:8px;border-radius:50%;display:inline-block;flex:none;background:${hex};` }),
text,
]);
}
/** 实心小徽标:置顶 / 已撤回(参考列表「顶」「已撤回」同款) */
function miniBadge(h: any, hex: string, text: string) {
return h(
"span",
{ style: `display:inline-block;padding:0 5px;margin-left:4px;font-size:11px;line-height:16px;color:#fff;background:${hex};border-radius:3px;vertical-align:1px;white-space:nowrap;` },
text
);
}
/** 类型胶囊:重要=实心红醒目,普通=浅灰弱化(同列对比出重要程度) */
function typeCell(h: any, row: any) {
const important = row.type == 2;
return h(
"span",
{
style: important
? `display:inline-block;padding:0 9px;font-size:12px;line-height:18px;font-weight:600;color:#fff;background:${C_RED};border-radius:9px;`
: "display:inline-block;padding:0 9px;font-size:12px;line-height:18px;color:#808695;background:#f5f7fa;border:1px solid #e8eaec;border-radius:9px;",
},
row.typeDesc || (important ? "重要" : "普通")
);
}
/** 阅读情况:渐变进度条 + 已读/应读 · 百分比(参考列表回执进度同款;读满转绿加粗) */
function readCell(h: any, row: any) {
// 未发布(草稿/待审批/已驳回)无阅读数据,不占进度条
if (row.status !== 1) return h("span", { style: `color:${C_GREY};font-size:13px;` }, "—");
const allow = Number(row.allowCount) || 0;
const read = Number(row.readCount) || 0;
if (!allow) return h("span", { style: "color:#808695;font-size:13px;" }, "暂无应读人");
const pct = Math.min(100, Math.round((read / allow) * 100));
const done = read >= allow;
return h("div", { style: "display:flex;align-items:center;gap:8px;" }, [
h("div", { style: "flex:1;min-width:54px;height:6px;background:#f0f2f5;border-radius:4px;overflow:hidden;" }, [
h("div", {
style: `width:${pct}%;height:100%;border-radius:4px;background:${done ? `linear-gradient(90deg,${C_GREEN},#3fc98a)` : `linear-gradient(90deg,${C_BLUE},#5cadff)`};`,
}),
]),
h(
"span",
{ style: `font-size:12px;white-space:nowrap;color:${done ? C_GREEN : "#515a6e"};font-weight:${done ? 600 : 400};` },
`${read}/${allow} · ${pct}%`
),
]);
}
/* 状态列:草稿/已发布(定时中)/待审批(步骤进度)/已驳回 + 已撤回/置顶徽标 */
function renderStatus(h: any, row: any) {
const parts: any[] = [];
if (row.status === 1) {
// 定时未到点:定时中(已排期)
if (row.timingSendTime && row.timingPublished === 0 && row.isPulled !== 1) {
parts.push(h("Tag", { color: "purple" }, () => "定时中"));
parts.push(statusPill(h, C_BLUE, "定时中"));
} else {
parts.push(h("Tag", { color: "success" }, () => "已发布"));
parts.push(statusPill(h, C_GREEN, "已发布"));
}
} else if (row.status === 2) {
parts.push(h("Tag", { color: "warning" }, () => "待审批"));
parts.push(statusPill(h, C_ORANGE, "待审批"));
// 多步进度文本(如「第1/2步 校级审核·会签 0/2」),与徽标重复时不再显示
if (row.approvalStatusDesc && row.approvalStatusDesc !== "待审批") {
parts.push(h("div", { style: "font-size:12px;color:#808695;line-height:1.4;margin-top:2px" }, String(row.approvalStatusDesc)));
}
} else if (row.status === 3) {
parts.push(h("Tag", { color: "error" }, () => "已驳回"));
parts.push(statusPill(h, C_RED, "已驳回"));
} else {
parts.push(h("Tag", {}, () => row.statusDesc || "草稿"));
parts.push(statusPill(h, C_GREY, row.statusDesc || "草稿"));
}
if (row.isPulled === 1) parts.push(h("Tag", { style: "margin-left:4px" }, () => "已撤回"));
if (row.topmost == 1) parts.push(h("Tag", { color: "orange", style: "margin-left:4px" }, () => "置顶"));
if (row.isPulled === 1) parts.push(miniBadge(h, C_ORANGE, "已撤回"));
if (row.topmost == 1) parts.push(miniBadge(h, C_RED, "置顶"));
return h("div", {}, parts);
}
@@ -268,26 +348,17 @@ const columns = computed(() => [
return h("span", { attrs: { title: t } }, t.length > 8 ? `${t.slice(0, 8)}…` : t);
},
},
{ title: "类型", width: 80, align: "center",
render: (h: any, p: any) => h("Tag", { color: p.row.type == 2 ? "error" : "default" }, () => p.row.typeDesc || (p.row.type == 2 ? "重要" : "普通")) },
{ title: "类型", width: 80, align: "center", render: (h: any, p: any) => typeCell(h, p.row) },
{ title: "来源部门", key: "deptName", width: 110, align: "left" },
{ title: "发布人", key: "publisherName", width: 90, align: "left", tooltip: true },
{ title: "状态", width: 190, align: "left", render: (h: any, p: any) => renderStatus(h, p.row) },
{
title: "发布时间",
width: 150,
width: 160,
align: "left",
render: (h: any, p: any) => h("span", {}, String(p.row.timingSendTime || p.row.createTime || "").replace("T", " ").slice(0, 16)),
},
{
title: "阅读情况",
width: 110,
align: "left",
render: (h: any, p: any) =>
p.row.status === 1
? h("span", {}, `${p.row.readCount || 0} / ${p.row.allowCount || 0} 已读`)
: h("span", { style: "color:#999;" }, "—"),
},
{ title: "阅读情况", width: 180, align: "left", render: (h: any, p: any) => readCell(h, p.row) },
{
title: "附件",
width: 70,
@@ -341,9 +412,10 @@ const tableHeight = computed(() => Utils.useTableHeight({ isSearch: true, isOper
<!-- 2. 搜索区 -->
<div class="search-wrap fix">
<div class="search-wrap_date fl">
<DatePicker type="date" placeholder="开始时间" v-model="startDt" @on-change="doSearch"></DatePicker>
<!-- v-model 拿到的是 Date 实例,提交前由 shortDate() 归一化为 yyyy-MM-dd 短日期 -->
<DatePicker type="date" format="yyyy-MM-dd" placeholder="开始时间" v-model="startDt" @on-change="doSearch"></DatePicker>
<span class="ml-8 mr-8">至</span>
<DatePicker type="date" placeholder="结束时间" v-model="endDt" @on-change="doSearch"></DatePicker>
<DatePicker type="date" format="yyyy-MM-dd" placeholder="结束时间" v-model="endDt" @on-change="doSearch"></DatePicker>
</div>
<Select v-model="statusType" class="search-wrap_sel ml-12" clearable placeholder="全部状态" style="width: 110px" @on-change="doSearch">
<Option value="1">已发布</Option>