审核管理:三个子菜单收敛为「待办审核 + 审批规则」(变体 A 落地)
按任务切菜单,不再按表切(见 docs/adr/0007): - 待办审核(reviewList):左侧模块导航带待办数,右侧该模块申报清单, 行内「通过 / 驳回」,顶部常驻该模块审批链(第几级、每级谁审), 详情抽屉里看审批进度,再进「查看申报内容」 - 审批规则(reviewFlow):一个模块一行,右边是生效审批链的逐级 chip, 可换链、可编辑链;同一模块多条绑定显式告警(后端 limit 1 只认一条) - 删除「审核流分配管理」页与其编辑弹窗、审核列表的裸字段编辑弹窗 - 新增 utils/reviewChains.ts:把 模块 → 链 → 层级 的 join 收成一处, 两个页面共用,避免各写一套 - 模块清单继续以注册表为唯一来源
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import request from '@/utils/request';
|
||||
import type { ApiResult, PageResult } from '@/api';
|
||||
import type { ReviewList, ReviewListParam } from './model';
|
||||
import type {
|
||||
ReviewList,
|
||||
ReviewListParam,
|
||||
ReviewPendingSummary
|
||||
} from './model';
|
||||
import { MODULES_API_URL } from '@/config/setting';
|
||||
|
||||
/**
|
||||
@@ -35,6 +39,19 @@ export async function pageGroupReviewList(params: ReviewListParam) {
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户的待办统计(按模块)
|
||||
*/
|
||||
export async function getPendingSummary() {
|
||||
const res = await request.get<ApiResult<ReviewPendingSummary>>(
|
||||
MODULES_API_URL + '/gxmu/review-list/pending-summary'
|
||||
);
|
||||
if (res.data.code === 0 && res.data.data) {
|
||||
return res.data.data;
|
||||
}
|
||||
return Promise.reject(new Error(res.data.message));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询审核列表列表
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { PageParam } from '@/api';
|
||||
* 审核列表
|
||||
*/
|
||||
export interface ReviewList {
|
||||
//
|
||||
//
|
||||
id?: number;
|
||||
// 模块
|
||||
module?: string;
|
||||
@@ -39,6 +39,20 @@ export interface ReviewList {
|
||||
reviewFlowList?: ReviewList[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前用户待办统计(按模块)
|
||||
*/
|
||||
export interface ReviewPendingSummary {
|
||||
// 待办总数
|
||||
total: number;
|
||||
// 按模块分组的待办数
|
||||
modules: {
|
||||
module: string;
|
||||
moduleName: string;
|
||||
count: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 审核列表搜索条件
|
||||
*/
|
||||
@@ -48,4 +62,8 @@ export interface ReviewListParam extends PageParam {
|
||||
keywords?: string;
|
||||
module?: string;
|
||||
pk?: number;
|
||||
// 状态(0待审核 1通过 2驳回)
|
||||
status?: number;
|
||||
// 审批人
|
||||
userId?: number;
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<!-- 搜索表单 -->
|
||||
<template>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-button type="primary" class="ele-btn-icon" @click="add">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
<span>添加</span>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import type { GradeParam } from '@/api/user/grade/model';
|
||||
import { watch } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// 选中的角色
|
||||
selection?: [];
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: GradeParam): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'remove'): void;
|
||||
(e: 'batchMove'): void;
|
||||
}>();
|
||||
|
||||
// 新增
|
||||
const add = () => {
|
||||
emit('add');
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.selection,
|
||||
() => {}
|
||||
);
|
||||
</script>
|
||||
+298
-203
@@ -1,222 +1,317 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.image" :width="50" />
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 1" color="green">显示</a-tag>
|
||||
<a-tag v-if="record.status === 0" color="red">隐藏</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
<div class="rr">
|
||||
<header class="rr-head">
|
||||
<div>
|
||||
<h2 class="rr-head__title">审批规则</h2>
|
||||
<p class="rr-head__sub">
|
||||
一个模块对应一条审批链。链里每一级写「谁审」——按学院角色解析时,一条链覆盖全部学院。
|
||||
</p>
|
||||
</div>
|
||||
<a-button type="primary" @click="openEditor(undefined)"
|
||||
>新建审批链</a-button
|
||||
>
|
||||
</header>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ReviewFlowEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
</div>
|
||||
</div>
|
||||
<a-alert
|
||||
v-if="conflicts.length"
|
||||
type="warning"
|
||||
show-icon
|
||||
class="rr-alert"
|
||||
:message="
|
||||
conflicts.length +
|
||||
' 个模块绑定了多条审批链,只有 id 最小的一条生效,请清理'
|
||||
"
|
||||
/>
|
||||
|
||||
<a-card :bordered="false" :body-style="{ padding: '8px 16px 16px' }">
|
||||
<a-spin :spinning="loading">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="rows"
|
||||
:pagination="false"
|
||||
row-key="module"
|
||||
size="middle"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'module'">
|
||||
<div class="rr-module">
|
||||
<b>{{ record.label }}</b>
|
||||
<span>{{ record.module }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'chain'">
|
||||
<div v-if="record.chain" class="rr-chain">
|
||||
<div class="rr-chain__head">
|
||||
<span class="rr-chain__name">{{ record.chain.title }}</span>
|
||||
<a-tag v-if="record.chain.conflict" color="orange">
|
||||
{{ record.configCount }} 条绑定
|
||||
</a-tag>
|
||||
</div>
|
||||
<div class="rr-chain__levels">
|
||||
<span
|
||||
v-for="(lv, i) in record.chain.levels"
|
||||
:key="lv.id || i"
|
||||
class="rr-lv"
|
||||
>
|
||||
<em>{{ i + 1 }}</em>
|
||||
<b>{{ levelName(lv) }}</b>
|
||||
<u v-if="ruleText(lv.reviewerRule)">{{
|
||||
ruleText(lv.reviewerRule)
|
||||
}}</u>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="rr-empty">
|
||||
未绑定审批链,沿用旧版固定审批人(该模块的申报仍会出现在待办里)
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space :size="12">
|
||||
<a v-if="record.chain" @click="openEditor(record.chain.flowId)">
|
||||
编辑链
|
||||
</a>
|
||||
<a-dropdown v-if="record.configId">
|
||||
<a @click.prevent>换链</a>
|
||||
<template #overlay>
|
||||
<a-menu @click="(e: any) => changeChain(record, e.key)">
|
||||
<a-menu-item
|
||||
v-for="f in flowOptions"
|
||||
:key="String(f.value)"
|
||||
:disabled="f.value === record.chain?.flowId"
|
||||
>
|
||||
{{ f.label }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-tooltip
|
||||
v-else
|
||||
title="该模块还没有绑定记录,暂不支持在这里新建绑定"
|
||||
>
|
||||
<span class="rr-dim">换链</span>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-spin>
|
||||
</a-card>
|
||||
|
||||
<review-flow-edit
|
||||
v-model:visible="editorOpen"
|
||||
:data="editing"
|
||||
@done="onSaved"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from "vue";
|
||||
import { message, Modal } from "ant-design-vue";
|
||||
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
|
||||
import type { EleProTable } from "ele-admin-pro";
|
||||
import { toDateString } from "ele-admin-pro";
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from "ele-admin-pro/es/ele-pro-table/types";
|
||||
import Search from "./components/search.vue";
|
||||
import ReviewFlowEdit from "./components/reviewFlowEdit.vue";
|
||||
import { pageReviewFlow, removeReviewFlow, removeBatchReviewFlow } from "@/api/gxmu/reviewFlow";
|
||||
import type { ReviewFlow, ReviewFlowParam } from "@/api/gxmu/reviewFlow/model";
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { ColumnItem } from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import ReviewFlowEdit from '../reviewFlow/components/reviewFlowEdit.vue';
|
||||
import type { ReviewFlow } from '@/api/gxmu/reviewFlow/model';
|
||||
import { updateReviewFlowConfig } from '@/api/gxmu/reviewFlowConfig';
|
||||
import type { ReviewFlowConfig } from '@/api/gxmu/reviewFlowConfig/model';
|
||||
import { loadModuleRegistry } from '@/utils/module-registry';
|
||||
import { toAllModuleOptions } from '@/utils/module-registry-core';
|
||||
import type { ModuleOption } from '@/utils/module-registry-core';
|
||||
import {
|
||||
chainOf,
|
||||
configsOf,
|
||||
levelName,
|
||||
loadReviewChains,
|
||||
ruleText,
|
||||
useReviewChains
|
||||
} from '@/views/gxmu/utils/reviewChains';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
interface RuleRow {
|
||||
module: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
configId?: number;
|
||||
configCount: number;
|
||||
chain?: ReturnType<typeof chainOf>;
|
||||
}
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ReviewFlow[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ReviewFlow | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
const { flowGroups, loading } = useReviewChains();
|
||||
const modules = ref<ModuleOption[]>([]);
|
||||
const editorOpen = ref(false);
|
||||
const editing = ref<ReviewFlow | null>(null);
|
||||
const rows = ref<RuleRow[]>([]);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
where.withGroup = true;
|
||||
return pageReviewFlow({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
const columns: ColumnItem[] = [
|
||||
{ title: '模块', key: 'module', width: 260 },
|
||||
{ title: '生效审批链(按顺序)', key: 'chain' },
|
||||
{ title: '操作', key: 'action', width: 190 }
|
||||
];
|
||||
|
||||
const flowOptions = computed(() =>
|
||||
flowGroups.value
|
||||
.filter((f) => !!f.title)
|
||||
.map((f) => ({ label: f.title as string, value: f.id as number }))
|
||||
);
|
||||
|
||||
const conflicts = computed(() => rows.value.filter((r) => r.configCount > 1));
|
||||
|
||||
const buildRows = () => {
|
||||
rows.value = modules.value.map((m) => {
|
||||
const list = configsOf(m.value);
|
||||
return {
|
||||
module: m.value,
|
||||
label: m.label,
|
||||
configId: list.length ? list[0].id : undefined,
|
||||
configCount: list.length,
|
||||
chain: chainOf(m.value)
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: "ID",
|
||||
dataIndex: "id",
|
||||
key: "id",
|
||||
align: "center",
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: "名称",
|
||||
dataIndex: "title",
|
||||
key: "title",
|
||||
align: "center"
|
||||
},
|
||||
{
|
||||
title: "创建时间",
|
||||
dataIndex: "createTime",
|
||||
key: "createTime",
|
||||
align: "center",
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, "yyyy-MM-dd HH:mm:ss")
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "action",
|
||||
width: 180,
|
||||
fixed: "right",
|
||||
align: "center",
|
||||
hideInSetting: true
|
||||
const load = async (force = false) => {
|
||||
try {
|
||||
await loadReviewChains(force);
|
||||
// 含停用模块:停用模块的存量审批链仍要能看到、能改
|
||||
modules.value = toAllModuleOptions(await loadModuleRegistry());
|
||||
buildRows();
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '审批规则加载失败');
|
||||
}
|
||||
]);
|
||||
};
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ReviewFlowParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
onMounted(() => load());
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ReviewFlow) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: ReviewFlow) => {
|
||||
const hide = message.loading("请求中..", 0);
|
||||
removeReviewFlow(row.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error("请至少选择一条数据");
|
||||
return;
|
||||
const openEditor = (flowId?: number) => {
|
||||
if (flowId == null) {
|
||||
editing.value = null;
|
||||
editorOpen.value = true;
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: "提示",
|
||||
content: "确定要删除选中的记录吗?",
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading("请求中..", 0);
|
||||
removeBatchReviewFlow(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
const group = flowGroups.value.find((g) => g.id === flowId);
|
||||
if (!group) {
|
||||
message.warning('这条审批链已不存在,请刷新后重试');
|
||||
return;
|
||||
}
|
||||
editing.value = group;
|
||||
editorOpen.value = true;
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
const changeChain = async (row: RuleRow, flowId: string | number) => {
|
||||
const target = configsOf(row.module)[0];
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload: ReviewFlowConfig = { ...target, flowId: Number(flowId) };
|
||||
await updateReviewFlowConfig(payload);
|
||||
message.success('已切换审批链,此后新提交的申报按新链走');
|
||||
await load(true);
|
||||
} catch (e: any) {
|
||||
message.error(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ReviewFlow) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
const onSaved = async () => {
|
||||
// 链改完,分组的 title / 层级都变了,必须强制重取
|
||||
await load(true);
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: "ReviewFlow"
|
||||
};
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.rr-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
.rr-head__title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rr-head__sub {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
max-width: 720px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.rr-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.rr-module {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.rr-module span {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.rr-tag {
|
||||
align-self: flex-start;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.rr-chain__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.rr-chain__name {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.rr-chain__levels {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.rr-lv {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 3px 9px;
|
||||
border-radius: 6px;
|
||||
background: #f5f7fa;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rr-lv em {
|
||||
font-style: normal;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #1590ff;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rr-lv u {
|
||||
text-decoration: none;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.rr-empty {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.rr-dim {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,334 +0,0 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="800"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:confirm-loading="loading"
|
||||
:title="isUpdate ? '编辑' : '添加'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<module-registry-notice
|
||||
:error="moduleLoadError"
|
||||
@retry="loadModuleOptions(form.module, form.moduleName, true)"
|
||||
/>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="名称" name="title">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入名称"
|
||||
v-model:value="form.title"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="审核流" name="flowId">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
:options="reviewFlowOptions"
|
||||
:loading="reviewFlowLoading"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择审核流"
|
||||
v-model:value="form.flowId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="所属部门" name="organizationId">
|
||||
<a-tree-select
|
||||
allow-clear
|
||||
show-search
|
||||
tree-default-expand-all
|
||||
:tree-data="organizationTreeData"
|
||||
:loading="organizationLoading"
|
||||
:dropdown-style="{ maxHeight: '360px', overflow: 'auto' }"
|
||||
placeholder="请选择所属部门"
|
||||
v-model:value="form.organizationId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="模块" name="module">
|
||||
<a-select
|
||||
allow-clear
|
||||
show-search
|
||||
:options="moduleOptions"
|
||||
:loading="moduleLoading"
|
||||
:filter-option="filterSelectOption"
|
||||
placeholder="请选择模块"
|
||||
v-model:value="form.module"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject } from 'ele-admin-pro';
|
||||
import { toTreeData } from 'ele-admin-pro/es';
|
||||
import {
|
||||
addReviewFlowConfig,
|
||||
updateReviewFlowConfig
|
||||
} from '@/api/gxmu/reviewFlowConfig';
|
||||
import { ReviewFlowConfig } from '@/api/gxmu/reviewFlowConfig/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { listReviewFlow } from '@/api/gxmu/reviewFlow';
|
||||
import type { ReviewFlow } from '@/api/gxmu/reviewFlow/model';
|
||||
import { listOrganizations } from '@/api/system/organization';
|
||||
import type { Organization } from '@/api/system/organization/model';
|
||||
import {
|
||||
enabledModuleOptions,
|
||||
isModuleRegistryStale,
|
||||
loadModuleRegistry,
|
||||
withLegacyModuleOption
|
||||
} from '@/utils/module-registry';
|
||||
import type { ModuleOption } from '@/utils/module-registry-core';
|
||||
import ModuleRegistryNotice from '@/views/gxmu/components/moduleRegistryNotice.vue';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ReviewFlowConfig | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
const reviewFlowLoading = ref(false);
|
||||
const reviewFlowOptions = ref<{ label: string; value: number }[]>([]);
|
||||
const organizationLoading = ref(false);
|
||||
const organizationTreeData = ref<Organization[]>([]);
|
||||
// 模块下拉来自模块注册表(只含启用模块),不再维护前端硬编码清单
|
||||
const moduleLoading = ref(false);
|
||||
const moduleOptions = ref<ModuleOption[]>([]);
|
||||
const moduleLoadError = ref<string>();
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<ReviewFlowConfig>({
|
||||
id: undefined,
|
||||
title: undefined,
|
||||
organizationId: undefined,
|
||||
flowId: undefined,
|
||||
module: undefined,
|
||||
deleted: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
userId: undefined
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
title: [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
whitespace: true,
|
||||
message: '请输入名称',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
flowId: [
|
||||
{
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请选择审核流',
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
organizationId: [
|
||||
{
|
||||
required: true,
|
||||
type: 'number',
|
||||
message: '请选择所属部门',
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
module: [
|
||||
{
|
||||
required: true,
|
||||
message: '请选择模块',
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
const loadReviewFlowOptions = async () => {
|
||||
reviewFlowLoading.value = true;
|
||||
try {
|
||||
const result = await listReviewFlow({ withGroup: true });
|
||||
const optionMap = new Map<number, string>();
|
||||
result.forEach((item: ReviewFlow) => {
|
||||
if (item.id && item.title && !optionMap.has(item.id)) {
|
||||
optionMap.set(item.id, item.title);
|
||||
}
|
||||
});
|
||||
reviewFlowOptions.value = Array.from(optionMap.entries()).map(
|
||||
([value, label]) => ({
|
||||
label,
|
||||
value
|
||||
})
|
||||
);
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '审核流列表加载失败');
|
||||
} finally {
|
||||
reviewFlowLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 加载模块下拉(审批链配置的下拉只含启用模块)。
|
||||
*
|
||||
* 注册表快照显式传入,不再读模块级可变缓存;编辑停用模块的历史审批链时把
|
||||
* 当前值补成「标题(已停用)」,保证回显不空白。拉取失败时保留上次成功的
|
||||
* 清单并在顶部提示 + 重试,不静默留空下拉。
|
||||
*/
|
||||
const loadModuleOptions = async (
|
||||
module?: string,
|
||||
moduleName?: string,
|
||||
force = false
|
||||
) => {
|
||||
moduleLoading.value = true;
|
||||
moduleLoadError.value = undefined;
|
||||
try {
|
||||
const list = await loadModuleRegistry(force);
|
||||
moduleOptions.value = withLegacyModuleOption(
|
||||
enabledModuleOptions(list),
|
||||
module,
|
||||
moduleName
|
||||
);
|
||||
// 命中「上次成功」的兜底快照:只给顶部提示 + 重试,不重复弹 message
|
||||
if (isModuleRegistryStale()) {
|
||||
moduleLoadError.value = '模块清单加载失败,已保留上次成功加载的清单';
|
||||
}
|
||||
} catch (e: any) {
|
||||
moduleOptions.value = withLegacyModuleOption(
|
||||
enabledModuleOptions(),
|
||||
module,
|
||||
moduleName
|
||||
);
|
||||
moduleLoadError.value = moduleOptions.value.length
|
||||
? '模块清单加载失败,已保留上次成功加载的清单'
|
||||
: '模块清单加载失败,请重试';
|
||||
message.warning(moduleLoadError.value);
|
||||
} finally {
|
||||
moduleLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadOrganizations = async () => {
|
||||
organizationLoading.value = true;
|
||||
try {
|
||||
const result = await listOrganizations();
|
||||
organizationTreeData.value = toTreeData({
|
||||
data: result.map((item) => ({
|
||||
...item,
|
||||
key: item.organizationId,
|
||||
value: item.organizationId,
|
||||
title: item.organizationName
|
||||
})),
|
||||
idField: 'organizationId',
|
||||
parentIdField: 'parentId'
|
||||
}) as Organization[];
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '部门列表加载失败');
|
||||
} finally {
|
||||
organizationLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const filterSelectOption = (input: string, option: any) => {
|
||||
return String(option?.label || '')
|
||||
.toLowerCase()
|
||||
.includes(input.toLowerCase());
|
||||
};
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value
|
||||
? updateReviewFlowConfig
|
||||
: addReviewFlowConfig;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
async (visible) => {
|
||||
if (visible) {
|
||||
if (!organizationTreeData.value.length) {
|
||||
await loadOrganizations();
|
||||
}
|
||||
if (!reviewFlowOptions.value.length) {
|
||||
await loadReviewFlowOptions();
|
||||
}
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
await loadModuleOptions(props.data.module, props.data.moduleName);
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
resetFields();
|
||||
await loadModuleOptions();
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
@@ -1,42 +0,0 @@
|
||||
<!-- 搜索表单 -->
|
||||
<template>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-button type="primary" class="ele-btn-icon" @click="add">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
<span>添加</span>
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import type { GradeParam } from '@/api/user/grade/model';
|
||||
import { watch } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// 选中的角色
|
||||
selection?: [];
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: GradeParam): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'remove'): void;
|
||||
(e: 'batchMove'): void;
|
||||
}>();
|
||||
|
||||
// 新增
|
||||
const add = () => {
|
||||
emit('add');
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.selection,
|
||||
() => {}
|
||||
);
|
||||
</script>
|
||||
@@ -1,233 +0,0 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="id"
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
:customRow="customRow"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<a-image :src="record.image" :width="50" />
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 1" color="green">显示</a-tag>
|
||||
<a-tag v-if="record.status === 0" color="red">隐藏</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openEdit(record)">修改</a>
|
||||
<a-divider type="vertical" />
|
||||
<a-popconfirm
|
||||
title="确定要删除此记录吗?"
|
||||
@confirm="remove(record)"
|
||||
>
|
||||
<a class="ele-text-danger">删除</a>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 编辑弹窗 -->
|
||||
<ReviewFlowConfigEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
DatasourceFunction,
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import ReviewFlowConfigEdit from './components/reviewFlowConfigEdit.vue';
|
||||
import { pageReviewFlowConfig, removeReviewFlowConfig, removeBatchReviewFlowConfig } from '@/api/gxmu/reviewFlowConfig';
|
||||
import type { ReviewFlowConfig, ReviewFlowConfigParam } from '@/api/gxmu/reviewFlowConfig/model';
|
||||
|
||||
// 表格实例
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
|
||||
// 表格选中数据
|
||||
const selection = ref<ReviewFlowConfig[]>([]);
|
||||
// 当前编辑数据
|
||||
const current = ref<ReviewFlowConfig | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
// 是否显示批量移动弹窗
|
||||
const showMove = ref(false);
|
||||
// 加载状态
|
||||
const loading = ref(true);
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
}
|
||||
return pageReviewFlowConfig({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列配置
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 90,
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'title',
|
||||
key: 'title',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '审核流',
|
||||
dataIndex: 'flowName',
|
||||
key: 'flowId',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '模块',
|
||||
dataIndex: 'moduleName',
|
||||
key: 'module',
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
/* 搜索 */
|
||||
const reload = (where?: ReviewFlowConfigParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where: where });
|
||||
};
|
||||
|
||||
/* 打开编辑弹窗 */
|
||||
const openEdit = (row?: ReviewFlowConfig) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
};
|
||||
|
||||
/* 打开批量移动弹窗 */
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
};
|
||||
|
||||
/* 删除单个 */
|
||||
const remove = (row: ReviewFlowConfig) => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeReviewFlowConfig(row.id)
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
};
|
||||
|
||||
/* 批量删除 */
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchReviewFlowConfig(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/* 查询 */
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
/* 自定义行属性 */
|
||||
const customRow = (record: ReviewFlowConfig) => {
|
||||
return {
|
||||
// 行点击事件
|
||||
onClick: () => {
|
||||
// console.log(record);
|
||||
},
|
||||
// 行双击事件
|
||||
onDblclick: () => {
|
||||
openEdit(record);
|
||||
}
|
||||
};
|
||||
};
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ReviewFlowConfig'
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
@@ -1,191 +0,0 @@
|
||||
<!-- 编辑弹窗 -->
|
||||
<template>
|
||||
<ele-modal
|
||||
:width="800"
|
||||
:visible="visible"
|
||||
:maskClosable="false"
|
||||
:maxable="maxable"
|
||||
:title="isUpdate ? '编辑审核列表' : '添加审核列表'"
|
||||
:body-style="{ paddingBottom: '28px' }"
|
||||
@update:visible="updateVisible"
|
||||
@ok="save"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
|
||||
:wrapper-col="
|
||||
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
|
||||
"
|
||||
>
|
||||
<a-form-item label="模块" name="module">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入模块"
|
||||
v-model:value="form.module"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="主键" name="pk">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入主键"
|
||||
v-model:value="form.pk"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="状态(0驳回 1通过)" name="status">
|
||||
<a-radio-group v-model:value="form.status">
|
||||
<a-radio :value="1">显示</a-radio>
|
||||
<a-radio :value="0">隐藏</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="审核意见" name="content">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入审核意见"
|
||||
v-model:value="form.content"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="是否删除, 0否, 1是" name="deleted">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入是否删除, 0否, 1是"
|
||||
v-model:value="form.deleted"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="修改时间" name="updateTime">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入修改时间"
|
||||
v-model:value="form.updateTime"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="userId">
|
||||
<a-input
|
||||
allow-clear
|
||||
placeholder="请输入"
|
||||
v-model:value="form.userId"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="" name="sortNumber">
|
||||
<a-input-number
|
||||
:min="0"
|
||||
:max="9999"
|
||||
class="ele-fluid"
|
||||
placeholder="请输入排序号"
|
||||
v-model:value="form.sortNumber"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</ele-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import { Form, message } from 'ant-design-vue';
|
||||
import { assignObject, uuid } from 'ele-admin-pro';
|
||||
import { addReviewList, updateReviewList } from '@/api/gxmu/reviewList';
|
||||
import { ReviewList } from '@/api/gxmu/reviewList/model';
|
||||
import { useThemeStore } from '@/store/modules/theme';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { FormInstance } from 'ant-design-vue/es/form';
|
||||
import { FileRecord } from '@/api/system/file/model';
|
||||
|
||||
// 是否是修改
|
||||
const isUpdate = ref(false);
|
||||
const useForm = Form.useForm;
|
||||
// 是否开启响应式布局
|
||||
const themeStore = useThemeStore();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
|
||||
const props = defineProps<{
|
||||
// 弹窗是否打开
|
||||
visible: boolean;
|
||||
// 修改回显的数据
|
||||
data?: ReviewList | null;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'done'): void;
|
||||
(e: 'update:visible', visible: boolean): void;
|
||||
}>();
|
||||
|
||||
// 提交状态
|
||||
const loading = ref(false);
|
||||
// 是否显示最大化切换按钮
|
||||
const maxable = ref(true);
|
||||
// 表格选中数据
|
||||
const formRef = ref<FormInstance | null>(null);
|
||||
|
||||
// 用户信息
|
||||
const form = reactive<ReviewList>({
|
||||
id: undefined,
|
||||
module: undefined,
|
||||
pk: undefined,
|
||||
status: undefined,
|
||||
content: undefined,
|
||||
deleted: undefined,
|
||||
tenantId: undefined,
|
||||
createTime: undefined,
|
||||
updateTime: undefined,
|
||||
userId: undefined,
|
||||
sortNumber: undefined,
|
||||
});
|
||||
|
||||
/* 更新visible */
|
||||
const updateVisible = (value: boolean) => {
|
||||
emit('update:visible', value);
|
||||
};
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
|
||||
});
|
||||
|
||||
const { resetFields } = useForm(form, rules);
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
if (!formRef.value) {
|
||||
return;
|
||||
}
|
||||
formRef.value
|
||||
.validate()
|
||||
.then(() => {
|
||||
loading.value = true;
|
||||
const formData = {
|
||||
...form
|
||||
};
|
||||
const saveOrUpdate = isUpdate.value ? updateReviewList : addReviewList;
|
||||
saveOrUpdate(formData)
|
||||
.then((msg) => {
|
||||
loading.value = false;
|
||||
message.success(msg);
|
||||
updateVisible(false);
|
||||
emit('done');
|
||||
})
|
||||
.catch((e) => {
|
||||
loading.value = false;
|
||||
message.error(e.message);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.visible,
|
||||
(visible) => {
|
||||
if (visible) {
|
||||
if (props.data) {
|
||||
assignObject(form, props.data);
|
||||
isUpdate.value = true;
|
||||
} else {
|
||||
isUpdate.value = false;
|
||||
}
|
||||
} else {
|
||||
resetFields();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
@@ -1,81 +1,47 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="review-search">
|
||||
<module-registry-notice
|
||||
:error="moduleLoadError"
|
||||
@retry="loadModuleOptions(true)"
|
||||
/>
|
||||
<a-space :size="10" style="flex-wrap: wrap">
|
||||
<a-select
|
||||
v-model:value="where.module"
|
||||
allow-clear
|
||||
placeholder="请选择模块"
|
||||
style="width: 220px"
|
||||
:options="moduleOptions"
|
||||
@change="search"
|
||||
/>
|
||||
<a-input-search
|
||||
v-model:value="where.keywords"
|
||||
allow-clear
|
||||
placeholder="请输入名称/审核意见"
|
||||
style="width: 260px"
|
||||
@search="search"
|
||||
@pressEnter="search"
|
||||
/>
|
||||
</a-space>
|
||||
<a-input-search
|
||||
v-model:value="keywords"
|
||||
allow-clear
|
||||
placeholder="搜索申报名称 / 审核意见"
|
||||
style="width: 300px"
|
||||
@search="search"
|
||||
@pressEnter="search"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, reactive, ref, watch } from 'vue';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { ReviewListParam } from '@/api/gxmu/reviewList/model';
|
||||
import {
|
||||
allModuleOptions,
|
||||
isModuleRegistryStale,
|
||||
loadModuleRegistry
|
||||
} from '@/utils/module-registry';
|
||||
import type { ModuleOption } from '@/utils/module-registry-core';
|
||||
import ModuleRegistryNotice from '@/views/gxmu/components/moduleRegistryNotice.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
selection?: [];
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: ReviewListParam): void;
|
||||
(e: 'add'): void;
|
||||
(e: 'remove'): void;
|
||||
(e: 'batchMove'): void;
|
||||
}>();
|
||||
|
||||
const where = reactive<ReviewListParam>({
|
||||
module: undefined,
|
||||
keywords: undefined
|
||||
});
|
||||
|
||||
// 模块筛选项来自模块注册表的全量清单(含停用模块,标题带「(已停用)」):
|
||||
// 审核列表是「查存量」的入口,停用模块的历史记录仍要能按模块筛出来
|
||||
const moduleOptions = ref<ModuleOption[]>([]);
|
||||
const keywords = ref<string>();
|
||||
const moduleLoadError = ref<string>();
|
||||
|
||||
// 模块标题(列表里的模块名、左侧导航都来自注册表);拉不到时保留上次成功的快照并提示重试
|
||||
const loadModuleOptions = async (force = false) => {
|
||||
moduleLoadError.value = undefined;
|
||||
try {
|
||||
const list = await loadModuleRegistry(force);
|
||||
moduleOptions.value = allModuleOptions(list);
|
||||
// 命中「上次成功」的兜底快照:只给顶部提示 + 重试,不重复弹 message
|
||||
await loadModuleRegistry(force);
|
||||
if (isModuleRegistryStale()) {
|
||||
moduleLoadError.value = '模块清单加载失败,已保留上次成功加载的清单';
|
||||
}
|
||||
} catch (e: any) {
|
||||
// 拉取失败:保留上次成功的清单(内存 / localStorage),顶部提示 + 重试
|
||||
moduleOptions.value = allModuleOptions();
|
||||
moduleLoadError.value = moduleOptions.value.length
|
||||
? '模块清单加载失败,已保留上次成功加载的清单'
|
||||
: '模块清单加载失败,请重试';
|
||||
moduleLoadError.value = '模块清单加载失败,请重试';
|
||||
message.warning(moduleLoadError.value);
|
||||
}
|
||||
};
|
||||
@@ -83,14 +49,15 @@
|
||||
onMounted(() => loadModuleOptions());
|
||||
|
||||
const search = () => {
|
||||
emit('search', {
|
||||
...where,
|
||||
keywords: where.keywords?.trim() || undefined
|
||||
});
|
||||
emit('search', { keywords: keywords.value?.trim() || undefined });
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.selection,
|
||||
() => {}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.review-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
+698
-161
@@ -1,6 +1,80 @@
|
||||
<template>
|
||||
<div class="page">
|
||||
<div class="ele-body">
|
||||
<div class="rw">
|
||||
<!-- 左:模块导航。审核人每天先看「哪个模块有活」 -->
|
||||
<aside class="rw-side">
|
||||
<div class="rw-side__title">审核管理</div>
|
||||
<button
|
||||
class="rw-mod"
|
||||
:class="{ 'is-active': activeModule === 'all' }"
|
||||
@click="selectModule('all')"
|
||||
>
|
||||
<span class="rw-mod__name">全部申报</span>
|
||||
<a-badge
|
||||
v-if="summary.total"
|
||||
:count="summary.total"
|
||||
:overflow-count="99"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
v-for="m in moduleList"
|
||||
:key="m.value"
|
||||
class="rw-mod"
|
||||
:class="{ 'is-active': activeModule === m.value }"
|
||||
@click="selectModule(m.value)"
|
||||
>
|
||||
<span class="rw-mod__name">{{ m.label }}</span>
|
||||
<a-badge
|
||||
v-if="pendingCount(m.value)"
|
||||
:count="pendingCount(m.value)"
|
||||
:overflow-count="99"
|
||||
/>
|
||||
</button>
|
||||
<div class="rw-side__foot">
|
||||
<a-button v-if="canSeeRules" block @click="goRules">审批规则</a-button>
|
||||
<div class="rw-side__tip">
|
||||
规则决定「谁在第几级审」。改这里比逐条催单管用。
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 右:该模块的申报清单,就地审批 -->
|
||||
<section class="rw-main">
|
||||
<header class="rw-head">
|
||||
<div>
|
||||
<h2 class="rw-head__title">{{ title }}</h2>
|
||||
<p class="rw-head__sub">{{ subtitle }}</p>
|
||||
</div>
|
||||
<a-radio-group
|
||||
v-model:value="scope"
|
||||
size="small"
|
||||
button-style="solid"
|
||||
@change="reload()"
|
||||
>
|
||||
<a-radio-button value="mine">待我审</a-radio-button>
|
||||
<a-radio-button value="all">全部</a-radio-button>
|
||||
</a-radio-group>
|
||||
</header>
|
||||
|
||||
<div v-if="activeChain" class="rw-chain">
|
||||
<span class="rw-chain__label">审批链</span>
|
||||
<span class="rw-chain__name">{{ activeChain.title }}</span>
|
||||
<span class="rw-chain__dot">·</span>
|
||||
<template v-for="(lv, i) in activeChain.levels" :key="lv.id || i">
|
||||
<span class="rw-chain__node">
|
||||
{{ i + 1 }} {{ levelName(lv) }}
|
||||
<em v-if="ruleText(lv.reviewerRule)">{{
|
||||
ruleText(lv.reviewerRule)
|
||||
}}</em>
|
||||
</span>
|
||||
<span v-if="i < activeChain.levels.length - 1" class="rw-chain__arrow"
|
||||
>→</span
|
||||
>
|
||||
</template>
|
||||
<a v-if="activeChain.conflict" class="rw-chain__warn" @click="goRules">
|
||||
该模块有多条绑定,仅一条生效
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
@@ -8,61 +82,138 @@
|
||||
:columns="columns"
|
||||
:datasource="datasource"
|
||||
tool-class="ele-toolbar-form"
|
||||
class="sys-org-table"
|
||||
>
|
||||
<template #toolbar>
|
||||
<search
|
||||
@search="reload"
|
||||
:selection="selection"
|
||||
@add="openEdit"
|
||||
@remove="removeBatch"
|
||||
@batchMove="openMove"
|
||||
/>
|
||||
<search @search="onSearch" />
|
||||
</template>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag v-if="record.status === 0">待审核</a-tag>
|
||||
<a-tag v-else-if="record.status === 1" color="green">通过</a-tag>
|
||||
<a-tag v-else-if="record.status === 2" color="red">驳回</a-tag>
|
||||
<template v-if="column.key === 'businessName'">
|
||||
<div class="rw-name">
|
||||
<b>{{ record.businessName || '未命名申报 #' + record.pk }}</b>
|
||||
<span>{{ record.moduleName || record.module }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a @click="openDataModal(record)">查看数据</a>
|
||||
<a-divider type="vertical" />
|
||||
<a @click="openFlowModal(record)">查看审核流</a>
|
||||
<template v-else-if="column.key === 'level'">
|
||||
<span :class="['rw-level', levelClass(record)]">
|
||||
{{ levelLabel(record) }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'status'">
|
||||
<a-badge
|
||||
:status="statusBadge(record.status)"
|
||||
:text="statusText(record.status)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space :size="12">
|
||||
<template v-if="canHandle(record)">
|
||||
<a @click="approve(record)">通过</a>
|
||||
<a class="rw-danger" @click="openReject(record)">驳回</a>
|
||||
</template>
|
||||
<a @click="openDetail(record)">详情</a>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</ele-pro-table>
|
||||
</a-card>
|
||||
</section>
|
||||
|
||||
<ReviewListEdit
|
||||
v-model:visible="showEdit"
|
||||
:data="current"
|
||||
@done="reload"
|
||||
<!-- 详情:先看审批走到哪,再看申报内容 -->
|
||||
<a-drawer
|
||||
:visible="detailOpen"
|
||||
:width="560"
|
||||
title="申报详情"
|
||||
@close="detailOpen = false"
|
||||
>
|
||||
<template v-if="detail">
|
||||
<div class="rw-detail__head">
|
||||
<h3>{{ detail.businessName || '未命名申报 #' + detail.pk }}</h3>
|
||||
<a-badge
|
||||
:status="statusBadge(detail.status)"
|
||||
:text="statusText(detail.status)"
|
||||
/>
|
||||
</div>
|
||||
<a-descriptions :column="2" size="small" bordered>
|
||||
<a-descriptions-item label="模块">
|
||||
{{ detail.moduleName || detail.module }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="提交时间">
|
||||
{{ fmt(detail.createTime) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="当前审批人">
|
||||
{{ detail.user?.realName || '—' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="当前环节">
|
||||
{{ levelLabel(detail) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item v-if="detail.content" label="最新意见" :span="2">
|
||||
{{ detail.content }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<h4 class="rw-detail__h4">审批进度</h4>
|
||||
<div
|
||||
v-if="detailChain && detailChain.levels.length"
|
||||
class="rw-timeline"
|
||||
>
|
||||
<div
|
||||
v-for="(lv, i) in detailChain.levels"
|
||||
:key="lv.id || i"
|
||||
class="rw-tl"
|
||||
:class="tlClass(detail, i)"
|
||||
>
|
||||
<span class="rw-tl__no">{{ i + 1 }}</span>
|
||||
<div class="rw-tl__body">
|
||||
<b>{{ levelName(lv) }}</b>
|
||||
<span v-if="ruleText(lv.reviewerRule)">
|
||||
{{ ruleText(lv.reviewerRule) }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="rw-tl__state">{{ tlState(detail, i) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<a-empty v-else description="该模块未绑定审批链,沿用旧版固定审批人" />
|
||||
|
||||
<div class="rw-detail__foot">
|
||||
<a-button @click="openModuleData">查看申报内容</a-button>
|
||||
<a-space v-if="canHandle(detail)">
|
||||
<a-button @click="openReject(detail)">驳回</a-button>
|
||||
<a-button type="primary" @click="approve(detail)">通过</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
</a-drawer>
|
||||
|
||||
<module-data-modal
|
||||
v-model:visible="dataVisible"
|
||||
:module="detail?.module"
|
||||
:module-name="detail?.moduleName"
|
||||
:pk="detail?.pk"
|
||||
@done="reload()"
|
||||
/>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="rejectOpen"
|
||||
title="驳回申报"
|
||||
ok-text="确认驳回"
|
||||
cancel-text="取消"
|
||||
:confirm-loading="rejecting"
|
||||
@ok="doReject"
|
||||
>
|
||||
<p class="rw-reject__tip"> 驳回原因会直接发给申报人,写清楚要改什么。 </p>
|
||||
<a-textarea
|
||||
v-model:value="rejectReason"
|
||||
:rows="4"
|
||||
placeholder="例如:缺少学院推荐意见,请补充后重新提交"
|
||||
/>
|
||||
<ModuleDataModal
|
||||
v-model:visible="showDataVisible"
|
||||
:module="dataCurrent?.module"
|
||||
:module-name="dataCurrent?.moduleName"
|
||||
:pk="dataCurrent?.pk"
|
||||
@done="reload"
|
||||
/>
|
||||
<ReviewFlowModal
|
||||
v-model:visible="showFlowVisible"
|
||||
:module="flowCurrent?.module"
|
||||
:module-name="flowCurrent?.moduleName"
|
||||
:pk="flowCurrent?.pk"
|
||||
@done="reload"
|
||||
/>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { createVNode, ref } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import type { EleProTable } from 'ele-admin-pro';
|
||||
import { toDateString } from 'ele-admin-pro';
|
||||
import type {
|
||||
@@ -70,167 +221,553 @@
|
||||
ColumnItem
|
||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||
import Search from './components/search.vue';
|
||||
import ReviewListEdit from './components/reviewListEdit.vue';
|
||||
import ModuleDataModal from './components/moduleDataModal.vue';
|
||||
import ReviewFlowModal from './components/reviewFlowModal.vue';
|
||||
import {
|
||||
getPendingSummary,
|
||||
pageGroupReviewList,
|
||||
removeBatchReviewList
|
||||
pageReviewList,
|
||||
updateReviewList
|
||||
} from '@/api/gxmu/reviewList';
|
||||
import type {
|
||||
ReviewList,
|
||||
ReviewListParam
|
||||
ReviewListParam,
|
||||
ReviewPendingSummary
|
||||
} from '@/api/gxmu/reviewList/model';
|
||||
import {
|
||||
enabledModuleOptions,
|
||||
loadModuleRegistry
|
||||
} from '@/utils/module-registry';
|
||||
import type { ModuleOption } from '@/utils/module-registry-core';
|
||||
import {
|
||||
chainOf,
|
||||
levelName,
|
||||
levelOf,
|
||||
ruleText,
|
||||
useReviewChains
|
||||
} from '@/views/gxmu/utils/reviewChains';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const { load: loadChains, loading: chainsLoading } = useReviewChains();
|
||||
|
||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||
const selection = ref<ReviewList[]>([]);
|
||||
const current = ref<ReviewList | null>(null);
|
||||
const dataCurrent = ref<ReviewList | null>(null);
|
||||
const flowCurrent = ref<ReviewList | null>(null);
|
||||
const showEdit = ref(false);
|
||||
const showDataVisible = ref(false);
|
||||
const showFlowVisible = ref(false);
|
||||
const showMove = ref(false);
|
||||
const loading = ref(true);
|
||||
const scope = ref<'mine' | 'all'>('mine');
|
||||
const activeModule = ref<string>('all');
|
||||
const keywords = ref<string>();
|
||||
const summary = ref<ReviewPendingSummary>({ total: 0, modules: [] });
|
||||
const moduleOptions = ref<ModuleOption[]>([]);
|
||||
const detail = ref<ReviewList | null>(null);
|
||||
const detailOpen = ref(false);
|
||||
const dataVisible = ref(false);
|
||||
const rejectOpen = ref(false);
|
||||
const rejectTarget = ref<ReviewList | null>(null);
|
||||
const rejectReason = ref('');
|
||||
const rejecting = ref(false);
|
||||
|
||||
const datasource: DatasourceFunction = ({
|
||||
page,
|
||||
limit,
|
||||
where,
|
||||
orders,
|
||||
filters
|
||||
}) => {
|
||||
if (filters) {
|
||||
where.status = filters.status;
|
||||
const myUserId = computed(() => userStore.info?.userId);
|
||||
|
||||
// 只有拿到「审批规则」菜单的人才能看到入口:改链是校团委的事,
|
||||
// 学院角色能进工作台、但不该进规则页
|
||||
const canSeeRules = computed(() => {
|
||||
const walk = (list?: any[]): boolean =>
|
||||
(list || []).some(
|
||||
(node) =>
|
||||
String(node?.path || '').indexOf('reviewFlow') >= 0 ||
|
||||
walk(node?.children)
|
||||
);
|
||||
return walk(userStore.menus || []);
|
||||
});
|
||||
|
||||
const moduleList = computed(() => moduleOptions.value);
|
||||
|
||||
const pendingCount = (module: string) => {
|
||||
const hit = summary.value.modules.find((m) => m.module === module);
|
||||
return hit ? hit.count : 0;
|
||||
};
|
||||
|
||||
const title = computed(() =>
|
||||
activeModule.value === 'all' ? '全部申报' : labelOf(activeModule.value)
|
||||
);
|
||||
|
||||
const activeChain = computed(() =>
|
||||
activeModule.value === 'all' ? undefined : chainOf(activeModule.value)
|
||||
);
|
||||
|
||||
const detailChain = computed(() =>
|
||||
detail.value ? chainOf(detail.value.module) : undefined
|
||||
);
|
||||
|
||||
const subtitle = computed(() => {
|
||||
if (chainsLoading.value) {
|
||||
return '审批链加载中…';
|
||||
}
|
||||
return pageGroupReviewList({
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit
|
||||
});
|
||||
if (activeModule.value === 'all') {
|
||||
return '左侧按模块看谁有活;每条申报都显示它当前停在哪一级。';
|
||||
}
|
||||
if (!activeChain.value) {
|
||||
return '该模块未绑定审批链,仍沿用旧版固定审批人。';
|
||||
}
|
||||
return (
|
||||
'共 ' +
|
||||
activeChain.value.levels.length +
|
||||
' 级 · 待我审 ' +
|
||||
pendingCount(activeModule.value) +
|
||||
' 条'
|
||||
);
|
||||
});
|
||||
|
||||
const labelOf = (module: string) => {
|
||||
const hit = moduleOptions.value.find((m) => m.value === module);
|
||||
return hit ? hit.label : module;
|
||||
};
|
||||
|
||||
const columns = ref<ColumnItem[]>([
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
align: 'center',
|
||||
width: 90
|
||||
},
|
||||
{
|
||||
title: '模块',
|
||||
dataIndex: 'moduleName',
|
||||
key: 'module',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'businessName',
|
||||
key: 'businessName',
|
||||
align: 'center',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '最新审核意见',
|
||||
dataIndex: 'content',
|
||||
key: 'content',
|
||||
align: 'center',
|
||||
ellipsis: true
|
||||
},
|
||||
{ title: '申报名称', key: 'businessName', ellipsis: true },
|
||||
{ title: '当前环节', key: 'level', width: 210 },
|
||||
{
|
||||
title: '提交时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
align: 'center',
|
||||
width: 160,
|
||||
sorter: true,
|
||||
ellipsis: true,
|
||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
||||
},
|
||||
{
|
||||
title: '当前审核人',
|
||||
dataIndex: ['user', 'realName'],
|
||||
key: 'userId',
|
||||
align: 'center'
|
||||
customRender: ({ text }) => fmt(text)
|
||||
},
|
||||
{ title: '状态', key: 'status', width: 110 },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 220,
|
||||
width: 160,
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
hideInSetting: true
|
||||
}
|
||||
]);
|
||||
|
||||
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
|
||||
const base: ReviewListParam = {
|
||||
...where,
|
||||
...orders,
|
||||
page,
|
||||
limit,
|
||||
keywords: keywords.value,
|
||||
module: activeModule.value === 'all' ? undefined : activeModule.value
|
||||
};
|
||||
if (scope.value === 'mine') {
|
||||
// 待我审:按当前登录人取待办节点,后端仍会做归属校验
|
||||
return pageReviewList({ ...base, status: 0, userId: myUserId.value });
|
||||
}
|
||||
return pageGroupReviewList(base);
|
||||
};
|
||||
|
||||
const reload = (where?: ReviewListParam) => {
|
||||
selection.value = [];
|
||||
tableRef?.value?.reload({ where });
|
||||
if (where) {
|
||||
keywords.value = where.keywords;
|
||||
}
|
||||
tableRef.value?.reload();
|
||||
};
|
||||
|
||||
const openEdit = (row?: ReviewList) => {
|
||||
current.value = row ?? null;
|
||||
showEdit.value = true;
|
||||
const onSearch = (where?: ReviewListParam) => {
|
||||
keywords.value = where?.keywords;
|
||||
tableRef.value?.reload({ where: {} });
|
||||
};
|
||||
|
||||
const openMove = () => {
|
||||
showMove.value = true;
|
||||
const selectModule = (module: string) => {
|
||||
activeModule.value = module;
|
||||
tableRef.value?.reload({ where: {} });
|
||||
};
|
||||
|
||||
const openDataModal = (row: ReviewList) => {
|
||||
dataCurrent.value = row;
|
||||
showDataVisible.value = true;
|
||||
const goRules = () => {
|
||||
router.push('/review/gxmu/reviewFlow');
|
||||
};
|
||||
|
||||
const openFlowModal = (row: ReviewList) => {
|
||||
flowCurrent.value = row;
|
||||
showFlowVisible.value = true;
|
||||
const loadSummary = async () => {
|
||||
try {
|
||||
summary.value = await getPendingSummary();
|
||||
} catch (e: any) {
|
||||
// 统计失败不该挡住列表,导航不显示数字即可
|
||||
summary.value = { total: 0, modules: [] };
|
||||
}
|
||||
};
|
||||
|
||||
const removeBatch = () => {
|
||||
if (!selection.value.length) {
|
||||
message.error('请至少选择一条数据');
|
||||
const loadModules = async () => {
|
||||
try {
|
||||
const list = await loadModuleRegistry();
|
||||
moduleOptions.value = enabledModuleOptions(list);
|
||||
} catch (e: any) {
|
||||
moduleOptions.value = enabledModuleOptions();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadModules(), loadSummary(), loadChains()]);
|
||||
tableRef.value?.reload();
|
||||
});
|
||||
|
||||
const fmt = (text?: string) =>
|
||||
text ? toDateString(text, 'yyyy-MM-dd HH:mm') : '—';
|
||||
|
||||
const statusText = (status?: number) =>
|
||||
status === 1 ? '已通过' : status === 2 ? '已驳回' : '待审核';
|
||||
|
||||
const statusBadge = (status?: number) =>
|
||||
status === 1 ? 'success' : status === 2 ? 'error' : 'processing';
|
||||
|
||||
const canHandle = (row?: ReviewList | null) =>
|
||||
!!row && row.status === 0 && row.userId === myUserId.value;
|
||||
|
||||
const levelLabel = (row: ReviewList) => {
|
||||
if (row.status === 1) return '已通过';
|
||||
if (row.status === 2) return '已驳回';
|
||||
const lv = levelOf(row.module, row.sortNumber);
|
||||
const index = (row.sortNumber ?? 0) + 1;
|
||||
return lv ? '第 ' + index + ' 级 · ' + levelName(lv) : '待审核';
|
||||
};
|
||||
|
||||
const levelClass = (row: ReviewList) =>
|
||||
row.status === 0 ? 'rw-level--pending' : 'rw-level--done';
|
||||
|
||||
const tlState = (row: ReviewList, i: number) => {
|
||||
const current = row.sortNumber ?? 0;
|
||||
if (row.status === 2) {
|
||||
if (i < current) return '通过';
|
||||
return i === current ? '驳回' : '待流转';
|
||||
}
|
||||
if (row.status === 1 || i < current) return '通过';
|
||||
return i === current ? '审核中' : '待流转';
|
||||
};
|
||||
|
||||
const tlClass = (row: ReviewList, i: number) => {
|
||||
const state = tlState(row, i);
|
||||
return state === '通过'
|
||||
? 'is-done'
|
||||
: state === '审核中'
|
||||
? 'is-now'
|
||||
: state === '驳回'
|
||||
? 'is-reject'
|
||||
: '';
|
||||
};
|
||||
|
||||
const openDetail = (row: ReviewList) => {
|
||||
detail.value = row;
|
||||
detailOpen.value = true;
|
||||
};
|
||||
|
||||
const openModuleData = () => {
|
||||
dataVisible.value = true;
|
||||
};
|
||||
|
||||
const approve = async (row: ReviewList) => {
|
||||
try {
|
||||
await updateReviewList({ id: row.id, status: 1 });
|
||||
message.success('已通过');
|
||||
detailOpen.value = false;
|
||||
tableRef.value?.reload();
|
||||
await loadSummary();
|
||||
} catch (e: any) {
|
||||
message.error(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const openReject = (row: ReviewList) => {
|
||||
rejectTarget.value = row;
|
||||
rejectReason.value = '';
|
||||
rejectOpen.value = true;
|
||||
};
|
||||
|
||||
const doReject = async () => {
|
||||
if (!rejectReason.value.trim()) {
|
||||
message.warning('驳回必须填写意见');
|
||||
return;
|
||||
}
|
||||
Modal.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除选中的记录吗?',
|
||||
icon: createVNode(ExclamationCircleOutlined),
|
||||
maskClosable: true,
|
||||
onOk: () => {
|
||||
const hide = message.loading('请求中..', 0);
|
||||
removeBatchReviewList(selection.value.map((d) => d.id))
|
||||
.then((msg) => {
|
||||
hide();
|
||||
message.success(msg);
|
||||
reload();
|
||||
})
|
||||
.catch((e) => {
|
||||
hide();
|
||||
message.error(e.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const query = () => {
|
||||
loading.value = true;
|
||||
};
|
||||
|
||||
query();
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'ReviewList'
|
||||
if (!rejectTarget.value?.id) {
|
||||
return;
|
||||
}
|
||||
rejecting.value = true;
|
||||
try {
|
||||
await updateReviewList({
|
||||
id: rejectTarget.value.id,
|
||||
status: 2,
|
||||
content: rejectReason.value.trim()
|
||||
});
|
||||
message.success('已驳回');
|
||||
rejectOpen.value = false;
|
||||
detailOpen.value = false;
|
||||
tableRef.value?.reload();
|
||||
await loadSummary();
|
||||
} catch (e: any) {
|
||||
message.error(e.message);
|
||||
} finally {
|
||||
rejecting.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
<style lang="less" scoped>
|
||||
.rw {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 12px 16px 24px;
|
||||
min-height: calc(100vh - 96px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.rw-side {
|
||||
width: 216px;
|
||||
flex: 0 0 216px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 16px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.rw-side__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
padding: 0 8px 10px;
|
||||
}
|
||||
|
||||
.rw-mod {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.72);
|
||||
}
|
||||
|
||||
.rw-mod:hover {
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
.rw-mod.is-active {
|
||||
background: #e8f4ff;
|
||||
color: #1590ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rw-mod__name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rw-side__foot {
|
||||
margin-top: auto;
|
||||
padding-top: 14px;
|
||||
border-top: 1px dashed #eef0f4;
|
||||
}
|
||||
|
||||
.rw-side__tip {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.rw-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rw-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.rw-head__title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rw-head__sub {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.rw-chain {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rw-chain__label {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.rw-chain__name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rw-chain__dot,
|
||||
.rw-chain__arrow {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.rw-chain__node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
background: #f5f7fa;
|
||||
color: rgba(0, 0, 0, 0.72);
|
||||
}
|
||||
|
||||
.rw-chain__node em {
|
||||
font-style: normal;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.rw-chain__warn {
|
||||
margin-left: auto;
|
||||
color: #fa8c16;
|
||||
}
|
||||
|
||||
.rw-name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.rw-name span {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.rw-level {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rw-level--pending {
|
||||
color: #1590ff;
|
||||
}
|
||||
|
||||
.rw-level--done {
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.rw-danger {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.rw-detail__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.rw-detail__head h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rw-detail__h4 {
|
||||
margin: 22px 0 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.rw-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.rw-tl {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.rw-tl__no {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex: 0 0 22px;
|
||||
border-radius: 50%;
|
||||
background: #e6e8ec;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rw-tl__body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.rw-tl__body span {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.rw-tl__state {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.rw-tl.is-done .rw-tl__no {
|
||||
background: #52c41a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rw-tl.is-now {
|
||||
background: #e8f4ff;
|
||||
}
|
||||
|
||||
.rw-tl.is-now .rw-tl__no {
|
||||
background: #1590ff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rw-tl.is-now .rw-tl__state {
|
||||
color: #1590ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rw-tl.is-reject .rw-tl__no {
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rw-detail__foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.rw-reject__tip {
|
||||
margin: 0 0 10px;
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
import { ref } from 'vue';
|
||||
import { pageReviewFlow } from '@/api/gxmu/reviewFlow';
|
||||
import { listReviewFlowConfig } from '@/api/gxmu/reviewFlowConfig';
|
||||
import type { ReviewFlow } from '@/api/gxmu/reviewFlow/model';
|
||||
import type { ReviewFlowConfig } from '@/api/gxmu/reviewFlowConfig/model';
|
||||
|
||||
/**
|
||||
* 审批链读取(模块 -> 链 -> 层级)。
|
||||
*
|
||||
* 后端把「链」存成一行一级(gxmu_review_flow.level),用 title 归组;
|
||||
* 模块到链的绑定在 gxmu_review_flow_config.flow_id。两个页面(待办审核 / 审批规则)
|
||||
* 都要把这条关系翻成人话,所以集中在这里,避免各页面各写一套 join。
|
||||
*
|
||||
* 注意:同一模块若存在多条配置,后端 getByModule 用 limit 1,谁生效取决于数据库返回顺序。
|
||||
* 这里按 id 升序取第一条,与「最老的那条生效」保持可解释;规则页会把多条配置标成冲突。
|
||||
*/
|
||||
|
||||
const flowGroups = ref<ReviewFlow[]>([]);
|
||||
const configs = ref<ReviewFlowConfig[]>([]);
|
||||
const loading = ref(false);
|
||||
const error = ref<string>();
|
||||
let loaded = false;
|
||||
|
||||
const levelIndex = new Map<number, ReviewFlow>();
|
||||
|
||||
const rebuildIndex = () => {
|
||||
levelIndex.clear();
|
||||
flowGroups.value.forEach((group) => {
|
||||
(group.flows || []).forEach((level) => {
|
||||
if (level.id != null) {
|
||||
levelIndex.set(level.id, group);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const sortLevels = (levels?: ReviewFlow[]) =>
|
||||
[...(levels || [])].sort((a, b) => (a.level || 0) - (b.level || 0));
|
||||
|
||||
export async function loadReviewChains(force = false) {
|
||||
if (loaded && !force) {
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
error.value = undefined;
|
||||
try {
|
||||
// 只有分页接口会按 title 归组并回填 organizationName / reviewUser,
|
||||
// 列表接口是平铺的层级行;这里取一页足够大的分组结果,避免链被拆散
|
||||
const [page, configList] = await Promise.all([
|
||||
pageReviewFlow({ withGroup: true, page: 1, limit: 200 }),
|
||||
listReviewFlowConfig()
|
||||
]);
|
||||
flowGroups.value = page?.list || [];
|
||||
configs.value = configList || [];
|
||||
rebuildIndex();
|
||||
loaded = true;
|
||||
} catch (e: any) {
|
||||
error.value = e?.message || '审批链加载失败';
|
||||
throw e;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 该模块的所有绑定配置(正常 1 条;多于 1 条说明历史数据有冲突) */
|
||||
export function configsOf(module?: string) {
|
||||
return configs.value
|
||||
.filter((c) => c.module === module)
|
||||
.sort((a, b) => (a.id || 0) - (b.id || 0));
|
||||
}
|
||||
|
||||
/** 该模块当前生效的配置(与后端 limit 1 对齐:取 id 最小的一条) */
|
||||
export function effectiveConfigOf(module?: string) {
|
||||
const list = configsOf(module);
|
||||
return list.length ? list[0] : undefined;
|
||||
}
|
||||
|
||||
export interface ChainInfo {
|
||||
flowId?: number;
|
||||
title: string;
|
||||
levels: ReviewFlow[];
|
||||
conflict: boolean;
|
||||
}
|
||||
|
||||
/** 该模块生效的审批链;未配置返回 undefined(沿用旧版固定审批人) */
|
||||
export function chainOf(module?: string): ChainInfo | undefined {
|
||||
if (!module) {
|
||||
return undefined;
|
||||
}
|
||||
const all = configsOf(module);
|
||||
const config = all.length ? all[0] : undefined;
|
||||
if (!config || config.flowId == null) {
|
||||
return undefined;
|
||||
}
|
||||
const group = levelIndex.get(config.flowId);
|
||||
if (!group) {
|
||||
return {
|
||||
flowId: config.flowId,
|
||||
title: '审批链已失效',
|
||||
levels: [],
|
||||
conflict: all.length > 1
|
||||
};
|
||||
}
|
||||
return {
|
||||
flowId: group.id,
|
||||
title: group.title || '未命名审批链',
|
||||
levels: sortLevels(group.flows),
|
||||
conflict: all.length > 1
|
||||
};
|
||||
}
|
||||
|
||||
/** 该模块生效链的完整分组(含 flows),用于打开链编辑器 */
|
||||
export function groupOf(module?: string): ReviewFlow | undefined {
|
||||
if (!module) {
|
||||
return undefined;
|
||||
}
|
||||
const config = effectiveConfigOf(module);
|
||||
if (!config || config.flowId == null) {
|
||||
return undefined;
|
||||
}
|
||||
return levelIndex.get(config.flowId);
|
||||
}
|
||||
|
||||
/** 第 sortNumber 级(0 起)的层级定义 */
|
||||
export function levelOf(module?: string, sortNumber?: number | null) {
|
||||
const chain = chainOf(module);
|
||||
if (!chain || !chain.levels.length || sortNumber == null) {
|
||||
return undefined;
|
||||
}
|
||||
return chain.levels[sortNumber];
|
||||
}
|
||||
|
||||
/** 层级的人话名字:机构名优先,其次规则,旧链只写了 reviewUserId 时显示「指定人员」 */
|
||||
export function levelName(level?: ReviewFlow) {
|
||||
if (!level) {
|
||||
return '';
|
||||
}
|
||||
return (
|
||||
level.organizationName ||
|
||||
ruleText(level.reviewerRule) ||
|
||||
(level.reviewUserId ? '指定人员' : '未配置审批人')
|
||||
);
|
||||
}
|
||||
|
||||
const ROLE_LABEL: Record<string, string> = {
|
||||
YouthLeagueCommitteeOfSecondaryColleges: '各学院团委书记',
|
||||
PartyCommitteesOfSecondaryColleges: '各学院党委书记',
|
||||
YouthLeagueBranchesOfSecondaryColleges: '各班团支部书记',
|
||||
SchoolYouthLeagueCommittee: '校团委'
|
||||
};
|
||||
|
||||
/** reviewer_rule 的人话渲染 */
|
||||
export function ruleText(rule?: string) {
|
||||
if (!rule || !rule.trim()) {
|
||||
return '';
|
||||
}
|
||||
return rule
|
||||
.split(';')
|
||||
.map((one) => one.trim())
|
||||
.filter((one) => !!one)
|
||||
.map((one) => {
|
||||
if (one.indexOf('USER:') === 0) {
|
||||
return '指定人员 ' + one.slice(5);
|
||||
}
|
||||
if (one.indexOf('COLLEGE_ROLE:') === 0) {
|
||||
const code = one.slice(13);
|
||||
return '学院角色 · ' + (ROLE_LABEL[code] || code);
|
||||
}
|
||||
return one;
|
||||
})
|
||||
.join('、');
|
||||
}
|
||||
|
||||
export function useReviewChains() {
|
||||
return {
|
||||
flowGroups,
|
||||
configs,
|
||||
loading,
|
||||
error,
|
||||
load: loadReviewChains
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user