- 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:
@@ -23,8 +23,6 @@ import type {
|
||||
ClassTreeNode,
|
||||
ApprovalQuery,
|
||||
ApprovalTabCountVo,
|
||||
PushScopeItem,
|
||||
ScopePreviewVo,
|
||||
SubmitRequest,
|
||||
} from "@/types/notice";
|
||||
|
||||
@@ -46,7 +44,7 @@ import type {
|
||||
* 变更要点(P1-7 / 校园通知改造):
|
||||
* 7. 全部方法加返回值泛型标注,替换 Record<string, any>
|
||||
* 8. 列表分页 content 字段对齐后端 BasePageResult(非旧 list)
|
||||
* 9. 新增:ScopeTree/ScopePreview 推送范围、SaveDraft/DraftList 草稿闭环、
|
||||
* 9. 新增:ScopeTree 推送范围、SaveDraft/DraftList 草稿闭环、
|
||||
* Submit 提交审批、ReadReceipt 阅读回执、Approve 带 opinion
|
||||
*/
|
||||
|
||||
@@ -88,9 +86,12 @@ function postForm<T = any>(url: string, data?: Record<string, any>): Method<Alov
|
||||
? Object.entries(data).filter(([, v]) => v !== undefined && v !== null)
|
||||
: [];
|
||||
const form = pairs.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
|
||||
// 同时把参数拼到 URL query:后端 @RequestParam 表单/查询参数都能接收,
|
||||
// 且本地 vite-plugin-mock 不解析 urlencoded body,靠 query 才能拿到参数(不影响线上)。
|
||||
const fullUrl = form ? `${url}${url.includes("?") ? "&" : "?"}${form}` : url;
|
||||
// 真后端:参数只放进 x-www-form-urlencoded body,URL 不带 query。
|
||||
// 修复 v30:若 query 与 body 同时携带同名参数,Servlet 会合并两处取值,
|
||||
// Spring @RequestParam String 再以「,」拼接,导致部门/联系组等名称变成「校园部,校园部」,
|
||||
// 且每次改名/启停把已含逗号的值原样回发再翻倍。只发 body 后每个参数只被取到一次。
|
||||
// 本地 dev:mock:vite-plugin-mock 不解析 urlencoded body,仅在此模式下把参数附带进 query(不影响线上)。
|
||||
const fullUrl = conf.isMock && form ? `${url}${url.includes("?") ? "&" : "?"}${form}` : url;
|
||||
return new Method<AlovaGenerics<T>>(
|
||||
"POST",
|
||||
alovaInstance,
|
||||
@@ -194,15 +195,6 @@ class NoticeApi {
|
||||
return postForm<void>(`${NOTICE}/ReceiveUsers`, { serid, users });
|
||||
}
|
||||
|
||||
// ========== 推送范围(改造方案 3.1 · v3 换源) ==========
|
||||
/** 范围树数据源已改 base/getStuTree(见 baseApi.getStuTree);此处仅按已选范围预估学员数 */
|
||||
scopePreview(scopes: PushScopeItem[], receiveList?: string) {
|
||||
return get<ScopePreviewVo>(`${NOTICE}/ScopePreview`, {
|
||||
scopes: JSON.stringify(scopes || []),
|
||||
receiveList: receiveList || "",
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 发布/编辑/删除/草稿 ==========
|
||||
/** 新建通知(JSON body,fileIds 为附件ID逗号串,smsFlag 为短信标记;带 flow 即送审) */
|
||||
create(data: NoticeRequest) {
|
||||
|
||||
@@ -1184,31 +1184,6 @@ export default [
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
// 范围预估:全校=ALL_STUDENTS;年级/班级按 studentCount 汇总;教师按名单数
|
||||
url: "/api/notice/ScopePreview",
|
||||
method: "get",
|
||||
response: ({ query }: any) => {
|
||||
let studentCount = 0;
|
||||
try {
|
||||
const scopes = JSON.parse(query.scopes || "[]");
|
||||
for (const s of scopes) {
|
||||
if (s.scopeType === 1) studentCount += ALL_STUDENTS;
|
||||
else if (s.scopeType === 3) {
|
||||
const g: any = gradeClasses.find((x) => x.gradeId === s.scopeId);
|
||||
if (g) studentCount += g.classes.reduce((sum: number, c: any) => sum + c.studentCount, 0);
|
||||
} else if (s.scopeType === 4) {
|
||||
studentCount += (classMap[String(s.scopeId)] || { studentCount: 0 }).studentCount;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const receive = String(query.receiveList || "").trim();
|
||||
const teacherCount = !receive || /^all$/i.test(receive) ? 0 : receive.split(/[,,\s]+/).filter(Boolean).length;
|
||||
return ok({ studentCount, teacherCount, totalCount: studentCount + teacherCount });
|
||||
},
|
||||
},
|
||||
{
|
||||
// 存为草稿:不圈人,仅内容 + 范围快照 + 附件
|
||||
url: "/api/notice/SaveDraft",
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
<Button icon="md-add" @click="openPicker" :disabled="mode === 0"></Button>
|
||||
<Button icon="md-trash" @click="reset" :disabled="mode === 0"></Button>
|
||||
</div>
|
||||
<div class="scope-preview" v-if="modelValue.length && preview">已选学员 <b>{{ preview.studentCount }}</b> 人</div>
|
||||
<UserCheckModal ref="picker" :sources="sources" :userids="classIds" @on-submit="submit" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -25,8 +24,8 @@
|
||||
* 树数据源 base/getStuTree?toUser=false(后端无 GetClassTree),自底向上归出「年级→班级」两层。
|
||||
*/
|
||||
import UserCheckModal from "@/components/UserCheckModal.vue";
|
||||
import { baseApi, noticeApi } from "@/api/module/notice";
|
||||
import type { PushScopeItem, SchTreeNode, ScopePreviewVo } from "@/types/notice";
|
||||
import { baseApi } from "@/api/module/notice";
|
||||
import type { PushScopeItem, SchTreeNode } from "@/types/notice";
|
||||
|
||||
/** 全部班级范围项(后端 scopeType=1 不解析 scopeId) */
|
||||
const ALL_SCOPE: PushScopeItem = { scopeType: 1, scopeId: 0, scopeName: "全部班级" };
|
||||
@@ -56,8 +55,6 @@ export default {
|
||||
treeFailed: false,
|
||||
/** 树未就绪时的回显缓冲(对齐 PushScopePanel pendingScopes 口径) */
|
||||
pendingScopes: null as PushScopeItem[] | null,
|
||||
preview: null as ScopePreviewVo | null,
|
||||
previewTimer: undefined as number | undefined,
|
||||
sources: [{ name: "班级", loader: () => this.loadTree() }],
|
||||
};
|
||||
},
|
||||
@@ -93,9 +90,8 @@ export default {
|
||||
created() {
|
||||
if (this.modelValue.length) {
|
||||
this.syncFromValue(this.modelValue);
|
||||
this.runPreview();
|
||||
} else {
|
||||
this.applyAll(); // 新建默认全部班级(emit 后由 watch 触发预估)
|
||||
this.applyAll(); // 新建默认全部班级
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -268,24 +264,12 @@ export default {
|
||||
this.classStr = [...extras, ...classes].map((x) => x.scopeName).join("、");
|
||||
}
|
||||
},
|
||||
async runPreview() {
|
||||
if (!this.modelValue.length) {
|
||||
this.preview = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.preview = await noticeApi.scopePreview(this.modelValue as PushScopeItem[]).send();
|
||||
} catch {
|
||||
this.preview = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
modelValue: {
|
||||
handler(val: PushScopeItem[]) {
|
||||
// 自身 emit 的变化只跳过状态回灌,预估仍按最新值刷新
|
||||
// 自身 emit 的变化只跳过状态回灌
|
||||
if (!this.suppressWatch) this.syncFromValue(val || []);
|
||||
this.runPreview();
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
@@ -311,14 +295,5 @@ export default {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.scope-preview {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #808695;
|
||||
b {
|
||||
color: #2d8cf0;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -35,11 +35,6 @@
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="scope-empty">未选择推送范围</div>
|
||||
|
||||
<!-- 选中学员数预估(ScopePreview 按范围展开去重) -->
|
||||
<div class="scope-preview" v-if="modelValue.length">
|
||||
<span>已选学员 <b>{{ preview ? preview.studentCount : "…" }}</b></span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -49,13 +44,12 @@
|
||||
* v-model = PushScopeItem[](最粗粒度勾选:父节点勾选则忽略其子孙)。
|
||||
* getStuTree 组节点不区分学校/学段/年级/班级,toScopeNode 自底向上归类(对齐 PushScopeTypeEnum):
|
||||
* 无组子节点=班级(4),其父=年级(3),再上=学部(2),顶层根强制=全校(1)。
|
||||
* 选中学员数:notice/ScopePreview.studentCount。
|
||||
* 编辑回显:父组件加载详情后调用 setScopes(pushScopes) 勾选对应节点;
|
||||
* 旧链路班级通知(receiveType=class)由父组件映射为 scopeType=4 的班级项。
|
||||
*/
|
||||
import { ref, computed, onMounted, watch } from "vue";
|
||||
import { baseApi, noticeApi } from "@/api/module/notice";
|
||||
import type { PushScopeItem, SchTreeNode, ScopeTreeNode, ScopePreviewVo } from "@/types/notice";
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { baseApi } from "@/api/module/notice";
|
||||
import type { PushScopeItem, SchTreeNode, ScopeTreeNode } from "@/types/notice";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: PushScopeItem[];
|
||||
@@ -232,31 +226,6 @@ function applyScopes(scopes?: PushScopeItem[] | null) {
|
||||
|
||||
defineExpose({ setScopes });
|
||||
|
||||
/* ============ 选中学员数预估(ScopePreview,防抖 350ms) ============ */
|
||||
const preview = ref<ScopePreviewVo | null>(null);
|
||||
let previewTimer: number | undefined;
|
||||
|
||||
async function runPreview() {
|
||||
if (!props.modelValue.length) {
|
||||
preview.value = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
preview.value = await noticeApi.scopePreview(props.modelValue).send();
|
||||
} catch {
|
||||
preview.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
window.clearTimeout(previewTimer);
|
||||
previewTimer = window.setTimeout(runPreview, 350);
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
/** getStuTree 组节点 → 面板范围节点:自底向上归类 scopeType(对齐 api-test toScopeNode,见头注释) */
|
||||
function toScopeNode(node: SchTreeNode, forcedType?: number): ScopeTreeNode | null {
|
||||
if (!node || node.type !== 1) return null; // toUser=false 仅组节点;出现人员叶子防御性跳过
|
||||
@@ -368,18 +337,5 @@ onMounted(async () => {
|
||||
font-size: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.scope-preview {
|
||||
margin-top: 8px;
|
||||
background: #f8fafc;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
b {
|
||||
font-size: 15px;
|
||||
color: #2d8cf0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
+1
-11
@@ -12,7 +12,7 @@
|
||||
* - NoticeStatisticsVo → 后端 model/notice/NoticeStatisticsVo.java
|
||||
* - NoticeRequest → 后端 model/notice/NoticeRequest.java
|
||||
* - NoticeQuery → 后端 model/notice/NoticeQuery.java + BasePageQuery.java
|
||||
* - PushScopeItem / ScopeTreeNode / ScopePreviewVo → 改造方案 3.1 推送范围
|
||||
* - PushScopeItem / ScopeTreeNode → 改造方案 3.1 推送范围
|
||||
* - ReadReceiptVo → 后端 model/notice/ReadReceiptVo.java(阅读回执分组)
|
||||
* - ReadTabCountVo → 后端 model/notice/ReadTabCountVo.java(v17「已收到」三 tab 统计)
|
||||
* - SubmitRequest / ApproverSuggestVo → 审核改造(步骤表 + 审核记录表 + 记忆表)
|
||||
@@ -152,16 +152,6 @@ export interface ScopeTreeNode {
|
||||
children?: ScopeTreeNode[];
|
||||
}
|
||||
|
||||
/** 推送范围预估 VO(notice/ScopePreview) */
|
||||
export interface ScopePreviewVo {
|
||||
/** 范围展开学员数(按账号去重) */
|
||||
studentCount: number;
|
||||
/** 教师定向名单数(receiveList 解析) */
|
||||
teacherCount: number;
|
||||
/** 合计预估接收人数 */
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
// ============================ 阅读回执 ============================
|
||||
|
||||
/** 已发送通知阅读回执(notice/ReadReceipt,按 年级→班级→学生 分组 + 教师组) */
|
||||
|
||||
@@ -33,6 +33,33 @@ const defHooks: Required<RequestHooks> = {
|
||||
onHttpError: () => {},
|
||||
};
|
||||
|
||||
/* ============= query 参数归一化 ============= */
|
||||
const pad2 = (n: number) => String(n).padStart(2, "0");
|
||||
|
||||
/** 本地时间 → 'yyyy-MM-dd HH:mm:ss' */
|
||||
function localDateTime(d: Date): string {
|
||||
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(
|
||||
d.getMinutes()
|
||||
)}:${pad2(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 兜底:把 params 里的 Date 实例转成本地 'yyyy-MM-dd HH:mm:ss'。
|
||||
*
|
||||
* alova 拼 query 时对参数只做 `encodeURIComponent(params[key])`,Date 会被隐式转成
|
||||
* 浏览器本地化串「Fri Aug 21 2026 00:00:00 GMT+0800 (中国标准时间)」,后端无法解析。
|
||||
* 业务侧应显式传字符串(如校园公告列表的 startTime/endTime 用 yyyy-MM-dd 短日期),
|
||||
* 这里只做防止 Date 直接泄漏到 URL 的最后一道保险,不改变已是字符串的参数。
|
||||
*/
|
||||
function normalizeParams(method: Method) {
|
||||
const params: any = method.config?.params;
|
||||
if (!params || typeof params !== "object" || Array.isArray(params)) return;
|
||||
Object.keys(params).forEach((k) => {
|
||||
const v = params[k];
|
||||
if (v instanceof Date && !isNaN(v.getTime())) params[k] = localDateTime(v);
|
||||
});
|
||||
}
|
||||
|
||||
/* ============= 错误处理(原 axios 逻辑平移) ============= */
|
||||
function errorHandle<T>(res: BschResponse<T>, status: number): T {
|
||||
// http 层错误
|
||||
@@ -95,6 +122,8 @@ export function createBschAlova(
|
||||
onLoadingEnd();
|
||||
}
|
||||
});
|
||||
// 0. query 参数兜底归一化(Date → 本地时间串),避免本地化日期串进 URL
|
||||
normalizeParams(method);
|
||||
// 1. 用户注入票据/角色等头
|
||||
injectHeaders(method);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user