feat(workbench): 四页签加「按应用筛选」(appKey)+页签重排(我提交的→3、抄送我的→4)
build / build (push) Successful in 1m52s

列宽/展示:待办办件标题 minWidth 220→160、四页签时间列 170→190、已办意见列
ellipsis+tooltip(长意见单行截断 hover 看全文);应用管理停用态 Tag 改用 iView
默认样式(原 gray 非预设色)。应用筛选下拉数据源同流程定义页(flw_app enabled=1)。

门禁:vite build ✓;vue-tsc 本机不可用(TS 5.9.3 与 vue-tsc 1.8.27 不兼容)
This commit is contained in:
chenxing
2026-10-12 11:25:58 +08:00
parent 955c1e55f6
commit 2a1ec48b25
3 changed files with 86 additions and 43 deletions
+6 -6
View File
@@ -59,20 +59,20 @@ export const TASK_STATUS_COLOR: Record<number, string> = {
class FlowApi { class FlowApi {
// ==================== 用户面 ==================== // ==================== 用户面 ====================
/** 我的待办 */ /** 我的待办(appKey=按应用筛选,可空) */
todo(params: { page?: number; size?: number }) { todo(params: { page?: number; size?: number; appKey?: string }) {
return get(`${ROOT}/todo`, params); return get(`${ROOT}/todo`, params);
} }
/** 我的已办 */ /** 我的已办(appKey=按应用筛选,可空) */
done(params: { page?: number; size?: number }) { done(params: { page?: number; size?: number; appKey?: string }) {
return get(`${ROOT}/done`, params); return get(`${ROOT}/done`, params);
} }
/** 我的抄送(NOTIFY 节点落库;知会不占待办,独立页签) */ /** 我的抄送(NOTIFY 节点落库;知会不占待办,独立页签) */
ccMy(params: { page?: number; size?: number }) { ccMy(params: { page?: number; size?: number; appKey?: string }) {
return get(`${ROOT}/cc/my`, params); return get(`${ROOT}/cc/my`, params);
} }
/** 我提交的办件 */ /** 我提交的办件 */
my(params: { status?: number; page?: number; size?: number }) { my(params: { status?: number; page?: number; size?: number; appKey?: string }) {
return get(`${ROOT}/my`, params); return get(`${ROOT}/my`, params);
} }
/** 办件详情(提交人/办理人/本校管理员可见) */ /** 办件详情(提交人/办理人/本校管理员可见) */
+78 -35
View File
@@ -1,7 +1,8 @@
<script setup lang="ts"> <script setup lang="ts">
// 工作台:我的待办 / 我的已办 / 抄送我的 / 我提交的(概览 §9.4 主路径查询,走 assignee 索引)。 // 工作台:我的待办 / 我的已办 / 我提交的 / 抄送我的(概览 §9.4 主路径查询,走 assignee 索引)。
// 规范骨架(bsch应用页规范 §1):tabs-wrapper + Tabs type=card,Tab 切换只 v-show 内容区,分页状态各自独立 // 四页签均有「按应用筛选」(办件定义归属 flw_def.app_key)。
import { ref } from "vue"; // 规范骨架(bsch应用页规范 §1):tabs-wrapper + Tabs type=card,Tab 切换只 v-show 内容区,分页与筛选状态各自独立
import { reactive, ref, onMounted } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { Message, Tag } from "view-ui-plus"; import { Message, Tag } from "view-ui-plus";
import MyConfirm from "@/components/MyConfirm"; import MyConfirm from "@/components/MyConfirm";
@@ -11,8 +12,10 @@ import { OperatorEnum } from "@/types";
import { flowApi, TASK_STATUS, TASK_STATUS_COLOR, INSTANCE_STATUS, INSTANCE_STATUS_COLOR } from "@/api/module/flow"; import { flowApi, TASK_STATUS, TASK_STATUS_COLOR, INSTANCE_STATUS, INSTANCE_STATUS_COLOR } from "@/api/module/flow";
import { actionColumn } from "@/utils/tableActions"; import { actionColumn } from "@/utils/tableActions";
type TabKey = "todo" | "done" | "cc" | "my";
const router = useRouter(); const router = useRouter();
const tab = ref<"my" | "todo" | "done" | "cc">("todo"); const tab = ref<TabKey>("todo");
const drawerVisible = ref(false); const drawerVisible = ref(false);
const drawerInstanceId = ref<string | number>(""); const drawerInstanceId = ref<string | number>("");
const todoTableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null); const todoTableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
@@ -20,13 +23,19 @@ const doneTableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
const ccTableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null); const ccTableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
const myTableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null); const myTableRef = ref<InstanceType<typeof TablePageAlovaNew> | null>(null);
const myParams = ref<{ status?: number }>({ status: undefined }); const myParams = ref<{ status?: number }>({ status: undefined });
// 应用筛选(2026-10-12):后端按办件定义归属 flw_def.app_key 过滤;四页签筛选状态各自独立
const apps = ref<{ appKey: string; appName: string }[]>([]);
const filters = reactive<Record<TabKey, string>>({ todo: "", done: "", cc: "", my: "" });
const tableRefs: Record<TabKey, typeof todoTableRef> = {
todo: todoTableRef, done: doneTableRef, cc: ccTableRef, my: myTableRef,
};
const todoColumns = [ const todoColumns = [
{ title: "序号", type: "index", width: 70, align: "center" }, { title: "序号", type: "index", width: 70, align: "center" },
{ title: "办件标题", key: "bizTitle", minWidth: 220 }, { title: "办件标题", key: "bizTitle", minWidth: 160 },
{ title: "当前节点", key: "nodeName", width: 160 }, { title: "当前节点", key: "nodeName", width: 160 },
{ title: "提交人", width: 100, render: (h: any, p: any) => h("span", p.row.submitterName || "-") }, { title: "提交人", width: 100, render: (h: any, p: any) => h("span", p.row.submitterName || "-") },
{ title: "到达时间", width: 170, render: (h: any, p: any) => h("span", (p.row.arriveTime || "").replace("T", " ")) }, { title: "到达时间", width: 190, render: (h: any, p: any) => h("span", (p.row.arriveTime || "").replace("T", " ")) },
actionColumn([{ key: "handle", label: "办理", icon: "icon-edit", on: (r: any) => goHandle(r) }], { mode: "text", width: 100, fixed: "right" }), actionColumn([{ key: "handle", label: "办理", icon: "icon-edit", on: (r: any) => goHandle(r) }], { mode: "text", width: 100, fixed: "right" }),
]; ];
@@ -46,8 +55,9 @@ const doneColumns = [
}, },
}, },
{ title: "结果", width: 90, align: "center", render: (h: any, p: any) => h(Tag, { color: TASK_STATUS_COLOR[p.row.status] || "default" }, () => TASK_STATUS[p.row.status] || p.row.status) }, { title: "结果", width: 90, align: "center", render: (h: any, p: any) => h(Tag, { color: TASK_STATUS_COLOR[p.row.status] || "default" }, () => TASK_STATUS[p.row.status] || p.row.status) },
{ title: "意见", key: "opinion", minWidth: 120 }, // 意见可能很长(退回理由/审批批语),单行截断 + hover 全文
{ title: "办理时间", width: 170, render: (h: any, p: any) => h("span", (p.row.handleTime || "").replace("T", " ")) }, { title: "意见", key: "opinion", minWidth: 120, ellipsis: true, tooltip: true },
{ title: "办理时间", width: 190, render: (h: any, p: any) => h("span", (p.row.handleTime || "").replace("T", " ")) },
{ title: "办件状态", width: 100, render: (h: any, p: any) => h("span", INSTANCE_STATUS[p.row.instanceStatus] || p.row.instanceStatus) }, { title: "办件状态", width: 100, render: (h: any, p: any) => h("span", INSTANCE_STATUS[p.row.instanceStatus] || p.row.instanceStatus) },
actionColumn([{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => openDetail(r.instanceId) }], { mode: "text", width: 100, fixed: "right" }), actionColumn([{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => openDetail(r.instanceId) }], { mode: "text", width: 100, fixed: "right" }),
]; ];
@@ -57,7 +67,7 @@ const ccColumns = [
{ title: "序号", type: "index", width: 70, align: "center" }, { title: "序号", type: "index", width: 70, align: "center" },
{ title: "办件标题", key: "bizTitle", minWidth: 220 }, { title: "办件标题", key: "bizTitle", minWidth: 220 },
{ title: "抄送节点", key: "nodeName", width: 160 }, { title: "抄送节点", key: "nodeName", width: 160 },
{ title: "抄送时间", width: 170, render: (h: any, p: any) => h("span", (p.row.sentTime || "").replace("T", " ")) }, { title: "抄送时间", width: 190, render: (h: any, p: any) => h("span", (p.row.sentTime || "").replace("T", " ")) },
{ title: "办件状态", width: 100, render: (h: any, p: any) => h("span", INSTANCE_STATUS[p.row.instanceStatus] || p.row.instanceStatus || "-") }, { title: "办件状态", width: 100, render: (h: any, p: any) => h("span", INSTANCE_STATUS[p.row.instanceStatus] || p.row.instanceStatus || "-") },
actionColumn([{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => openDetail(r.instanceId) }], { mode: "text", width: 100, fixed: "right" }), actionColumn([{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => openDetail(r.instanceId) }], { mode: "text", width: 100, fixed: "right" }),
]; ];
@@ -70,8 +80,8 @@ const myColumns = [
{ title: "状态", width: 100, align: "center", render: (h: any, p: any) => { title: "状态", width: 100, align: "center", render: (h: any, p: any) =>
h(Tag, { color: INSTANCE_STATUS_COLOR[p.row.status] || "gray" }, () => INSTANCE_STATUS[p.row.status] || p.row.status) }, h(Tag, { color: INSTANCE_STATUS_COLOR[p.row.status] || "gray" }, () => INSTANCE_STATUS[p.row.status] || p.row.status) },
{ title: "当前节点", key: "currentNodes", width: 150 }, { title: "当前节点", key: "currentNodes", width: 150 },
{ title: "提交时间", width: 170, render: (h: any, p: any) => h("span", (p.row.submitTime || "").replace("T", " ")) }, { title: "提交时间", width: 190, render: (h: any, p: any) => h("span", (p.row.submitTime || "").replace("T", " ")) },
{ title: "办结时间", width: 170, render: (h: any, p: any) => h("span", (p.row.finishTime || "").replace("T", " ") || "-") }, { title: "办结时间", width: 190, render: (h: any, p: any) => h("span", (p.row.finishTime || "").replace("T", " ") || "-") },
actionColumn( actionColumn(
[ [
{ key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => openDetail(r.instanceId) }, { key: "detail", label: "详情", icon: "icon-browse", on: (r: any) => openDetail(r.instanceId) },
@@ -83,16 +93,36 @@ const myColumns = [
]; ];
// dataApi 返回 Method(alova usePagination 负责发送与解包),不能调 .send() // dataApi 返回 Method(alova usePagination 负责发送与解包),不能调 .send()
const todoApi = (p: { pageIndex: number; pageSize: number }) => flowApi.todo({ page: p.pageIndex, size: p.pageSize }); const todoApi = (p: { pageIndex: number; pageSize: number }) =>
const doneApi = (p: { pageIndex: number; pageSize: number }) => flowApi.done({ page: p.pageIndex, size: p.pageSize }); flowApi.todo({ appKey: filters.todo || undefined, page: p.pageIndex, size: p.pageSize });
const ccApi = (p: { pageIndex: number; pageSize: number }) => flowApi.ccMy({ page: p.pageIndex, size: p.pageSize }); const doneApi = (p: { pageIndex: number; pageSize: number }) =>
flowApi.done({ appKey: filters.done || undefined, page: p.pageIndex, size: p.pageSize });
const ccApi = (p: { pageIndex: number; pageSize: number }) =>
flowApi.ccMy({ appKey: filters.cc || undefined, page: p.pageIndex, size: p.pageSize });
const myApi = (p: { pageIndex: number; pageSize: number }) => const myApi = (p: { pageIndex: number; pageSize: number }) =>
flowApi.my({ status: myParams.value.status || undefined, page: p.pageIndex, size: p.pageSize }); flowApi.my({ status: myParams.value.status || undefined, appKey: filters.my || undefined, page: p.pageIndex, size: p.pageSize });
function onMySearch() { function onMySearch() {
myTableRef.value?.refreshData(); myTableRef.value?.refreshData();
} }
/** 应用下拉数据源:应用设置白名单(flw_app enabled=1),失败降级为空下拉 */
async function loadApps() {
try {
const rows: any[] = (await flowApi.appList().send()) || [];
apps.value = rows.filter((r) => r.enabled === 1)
.map((r) => ({ appKey: String(r.appKey), appName: r.appName || String(r.appKey) }));
} catch {
apps.value = [];
}
}
/** 筛选变更:回到第一页重查;未加载过的页签标记为已加载,避免切页签时重复请求 */
function onFilterChange(name: TabKey) {
loaded[name] = true;
tableRefs[name].value?.refreshData();
}
async function onWithdraw(row: any) { async function onWithdraw(row: any) {
await MyConfirm({ await MyConfirm({
title: "撤回", title: "撤回",
@@ -129,24 +159,15 @@ function openDetail(instanceId: string | number) {
} }
// 各内容区 v-show 共存,非首个页签首次切换才拉取(保持原惰性加载行为) // 各内容区 v-show 共存,非首个页签首次切换才拉取(保持原惰性加载行为)
const doneLoaded = ref(false); const loaded = reactive<Record<TabKey, boolean>>({ todo: true, done: false, cc: false, my: false });
const ccLoaded = ref(false);
const myLoaded = ref(false);
function switchTab(name: string) { function switchTab(name: string) {
tab.value = name as any; tab.value = name as TabKey;
if (name === "done" && !doneLoaded.value) { if (loaded[name as TabKey]) return;
doneLoaded.value = true; loaded[name as TabKey] = true;
doneTableRef.value?.reloadData(OperatorEnum.sel); tableRefs[name as TabKey].value?.reloadData(OperatorEnum.sel);
}
if (name === "cc" && !ccLoaded.value) {
ccLoaded.value = true;
ccTableRef.value?.reloadData(OperatorEnum.sel);
}
if (name === "my" && !myLoaded.value) {
myLoaded.value = true;
myTableRef.value?.reloadData(OperatorEnum.sel);
}
} }
onMounted(loadApps);
</script> </script>
<template> <template>
@@ -155,19 +176,28 @@ function switchTab(name: string) {
<Tabs class="tabs-panel" :value="tab" @on-click="switchTab"> <Tabs class="tabs-panel" :value="tab" @on-click="switchTab">
<TabPane label="我的待办" name="todo" /> <TabPane label="我的待办" name="todo" />
<TabPane label="我的已办" name="done" /> <TabPane label="我的已办" name="done" />
<TabPane label="抄送我的" name="cc" />
<TabPane label="我提交的" name="my" /> <TabPane label="我提交的" name="my" />
<TabPane label="抄送我的" name="cc" />
</Tabs> </Tabs>
<div class="tab-body" v-show="tab === 'todo'"> <div class="tab-body" v-show="tab === 'todo'">
<div class="search-wrap fix">
<Select v-model="filters.todo" class="search-wrap_sel" clearable filterable placeholder="按应用筛选"
@on-change="onFilterChange('todo')">
<Option v-for="a in apps" :key="a.appKey" :value="a.appKey">{{ a.appName }}</Option>
</Select>
</div>
<TablePageAlovaNew ref="todoTableRef" :dataApi="todoApi" :columns="todoColumns" /> <TablePageAlovaNew ref="todoTableRef" :dataApi="todoApi" :columns="todoColumns" />
</div> </div>
<div class="tab-body" v-show="tab === 'done'"> <div class="tab-body" v-show="tab === 'done'">
<div class="search-wrap fix">
<Select v-model="filters.done" class="search-wrap_sel" clearable filterable placeholder="按应用筛选"
@on-change="onFilterChange('done')">
<Option v-for="a in apps" :key="a.appKey" :value="a.appKey">{{ a.appName }}</Option>
</Select>
</div>
<TablePageAlovaNew ref="doneTableRef" :dataApi="doneApi" :columns="doneColumns" :autoLoad="false" /> <TablePageAlovaNew ref="doneTableRef" :dataApi="doneApi" :columns="doneColumns" :autoLoad="false" />
</div> </div>
<div class="tab-body" v-show="tab === 'cc'">
<TablePageAlovaNew ref="ccTableRef" :dataApi="ccApi" :columns="ccColumns" :autoLoad="false" />
</div>
<div class="tab-body" v-show="tab === 'my'"> <div class="tab-body" v-show="tab === 'my'">
<div class="search-wrap fix"> <div class="search-wrap fix">
<Select v-model="myParams.status" class="search-wrap_sel" clearable placeholder="状态" @on-change="onMySearch"> <Select v-model="myParams.status" class="search-wrap_sel" clearable placeholder="状态" @on-change="onMySearch">
@@ -176,9 +206,22 @@ function switchTab(name: string) {
<Option :value="30" label="已拒绝" /> <Option :value="30" label="已拒绝" />
<Option :value="40" label="已撤回" /> <Option :value="40" label="已撤回" />
</Select> </Select>
<Select v-model="filters.my" class="search-wrap_sel" clearable filterable placeholder="按应用筛选"
@on-change="onFilterChange('my')">
<Option v-for="a in apps" :key="a.appKey" :value="a.appKey">{{ a.appName }}</Option>
</Select>
</div> </div>
<TablePageAlovaNew ref="myTableRef" :dataApi="myApi" :columns="myColumns" :autoLoad="false" /> <TablePageAlovaNew ref="myTableRef" :dataApi="myApi" :columns="myColumns" :autoLoad="false" />
</div> </div>
<div class="tab-body" v-show="tab === 'cc'">
<div class="search-wrap fix">
<Select v-model="filters.cc" class="search-wrap_sel" clearable filterable placeholder="按应用筛选"
@on-change="onFilterChange('cc')">
<Option v-for="a in apps" :key="a.appKey" :value="a.appKey">{{ a.appName }}</Option>
</Select>
</div>
<TablePageAlovaNew ref="ccTableRef" :dataApi="ccApi" :columns="ccColumns" :autoLoad="false" />
</div>
</div> </div>
<InstanceDrawer v-model="drawerVisible" :instance-id="drawerInstanceId" /> <InstanceDrawer v-model="drawerVisible" :instance-id="drawerInstanceId" />
</div> </div>
+2 -2
View File
@@ -42,9 +42,9 @@ const columns = [
title: "状态", title: "状态",
width: 90, width: 90,
align: "center", align: "center",
// 停用态走 iView 默认 Tag(不指定 color),避免自定义灰底与默认样式不一致
render: (h: any, p: any) => render: (h: any, p: any) =>
h(Tag, { color: p.row.enabled === 1 ? "green" : "gray" }, p.row.enabled === 1 ? h(Tag, { color: "green" }, () => "已开启") : h(Tag, () => "已停用"),
() => (p.row.enabled === 1 ? "已开启" : "已停用")),
}, },
actionColumn( actionColumn(
[ [