审核管理:三个子菜单收敛为「待办审核 + 审批规则」(变体 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 request from '@/utils/request';
|
||||||
import type { ApiResult, PageResult } from '@/api';
|
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';
|
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));
|
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 {
|
export interface ReviewList {
|
||||||
//
|
//
|
||||||
id?: number;
|
id?: number;
|
||||||
// 模块
|
// 模块
|
||||||
module?: string;
|
module?: string;
|
||||||
@@ -39,6 +39,20 @@ export interface ReviewList {
|
|||||||
reviewFlowList?: 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;
|
keywords?: string;
|
||||||
module?: string;
|
module?: string;
|
||||||
pk?: number;
|
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>
|
<template>
|
||||||
<div class="page">
|
<div class="rr">
|
||||||
<div class="ele-body">
|
<header class="rr-head">
|
||||||
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
<div>
|
||||||
<ele-pro-table
|
<h2 class="rr-head__title">审批规则</h2>
|
||||||
ref="tableRef"
|
<p class="rr-head__sub">
|
||||||
row-key="id"
|
一个模块对应一条审批链。链里每一级写「谁审」——按学院角色解析时,一条链覆盖全部学院。
|
||||||
:columns="columns"
|
</p>
|
||||||
:datasource="datasource"
|
</div>
|
||||||
:customRow="customRow"
|
<a-button type="primary" @click="openEditor(undefined)"
|
||||||
tool-class="ele-toolbar-form"
|
>新建审批链</a-button
|
||||||
class="sys-org-table"
|
>
|
||||||
>
|
</header>
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 编辑弹窗 -->
|
<a-alert
|
||||||
<ReviewFlowEdit v-model:visible="showEdit" :data="current" @done="reload" />
|
v-if="conflicts.length"
|
||||||
</div>
|
type="warning"
|
||||||
</div>
|
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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { createVNode, ref } from "vue";
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { message, Modal } from "ant-design-vue";
|
import { message } from 'ant-design-vue';
|
||||||
import { ExclamationCircleOutlined } from "@ant-design/icons-vue";
|
import type { ColumnItem } from 'ele-admin-pro/es/ele-pro-table/types';
|
||||||
import type { EleProTable } from "ele-admin-pro";
|
import ReviewFlowEdit from '../reviewFlow/components/reviewFlowEdit.vue';
|
||||||
import { toDateString } from "ele-admin-pro";
|
import type { ReviewFlow } from '@/api/gxmu/reviewFlow/model';
|
||||||
import type {
|
import { updateReviewFlowConfig } from '@/api/gxmu/reviewFlowConfig';
|
||||||
DatasourceFunction,
|
import type { ReviewFlowConfig } from '@/api/gxmu/reviewFlowConfig/model';
|
||||||
ColumnItem
|
import { loadModuleRegistry } from '@/utils/module-registry';
|
||||||
} from "ele-admin-pro/es/ele-pro-table/types";
|
import { toAllModuleOptions } from '@/utils/module-registry-core';
|
||||||
import Search from "./components/search.vue";
|
import type { ModuleOption } from '@/utils/module-registry-core';
|
||||||
import ReviewFlowEdit from "./components/reviewFlowEdit.vue";
|
import {
|
||||||
import { pageReviewFlow, removeReviewFlow, removeBatchReviewFlow } from "@/api/gxmu/reviewFlow";
|
chainOf,
|
||||||
import type { ReviewFlow, ReviewFlowParam } from "@/api/gxmu/reviewFlow/model";
|
configsOf,
|
||||||
|
levelName,
|
||||||
|
loadReviewChains,
|
||||||
|
ruleText,
|
||||||
|
useReviewChains
|
||||||
|
} from '@/views/gxmu/utils/reviewChains';
|
||||||
|
|
||||||
// 表格实例
|
interface RuleRow {
|
||||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
module: string;
|
||||||
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
configId?: number;
|
||||||
|
configCount: number;
|
||||||
|
chain?: ReturnType<typeof chainOf>;
|
||||||
|
}
|
||||||
|
|
||||||
// 表格选中数据
|
const { flowGroups, loading } = useReviewChains();
|
||||||
const selection = ref<ReviewFlow[]>([]);
|
const modules = ref<ModuleOption[]>([]);
|
||||||
// 当前编辑数据
|
const editorOpen = ref(false);
|
||||||
const current = ref<ReviewFlow | null>(null);
|
const editing = ref<ReviewFlow | null>(null);
|
||||||
// 是否显示编辑弹窗
|
const rows = ref<RuleRow[]>([]);
|
||||||
const showEdit = ref(false);
|
|
||||||
// 是否显示批量移动弹窗
|
|
||||||
const showMove = ref(false);
|
|
||||||
// 加载状态
|
|
||||||
const loading = ref(true);
|
|
||||||
|
|
||||||
// 表格数据源
|
const columns: ColumnItem[] = [
|
||||||
const datasource: DatasourceFunction = ({
|
{ title: '模块', key: 'module', width: 260 },
|
||||||
page,
|
{ title: '生效审批链(按顺序)', key: 'chain' },
|
||||||
limit,
|
{ title: '操作', key: 'action', width: 190 }
|
||||||
where,
|
];
|
||||||
orders,
|
|
||||||
filters
|
const flowOptions = computed(() =>
|
||||||
}) => {
|
flowGroups.value
|
||||||
if (filters) {
|
.filter((f) => !!f.title)
|
||||||
where.status = filters.status;
|
.map((f) => ({ label: f.title as string, value: f.id as number }))
|
||||||
}
|
);
|
||||||
where.withGroup = true;
|
|
||||||
return pageReviewFlow({
|
const conflicts = computed(() => rows.value.filter((r) => r.configCount > 1));
|
||||||
...where,
|
|
||||||
...orders,
|
const buildRows = () => {
|
||||||
page,
|
rows.value = modules.value.map((m) => {
|
||||||
limit
|
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 load = async (force = false) => {
|
||||||
const columns = ref<ColumnItem[]>([
|
try {
|
||||||
{
|
await loadReviewChains(force);
|
||||||
title: "ID",
|
// 含停用模块:停用模块的存量审批链仍要能看到、能改
|
||||||
dataIndex: "id",
|
modules.value = toAllModuleOptions(await loadModuleRegistry());
|
||||||
key: "id",
|
buildRows();
|
||||||
align: "center",
|
} catch (e: any) {
|
||||||
width: 90
|
message.error(e.message || '审批规则加载失败');
|
||||||
},
|
|
||||||
{
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
]);
|
};
|
||||||
|
|
||||||
/* 搜索 */
|
onMounted(() => load());
|
||||||
const reload = (where?: ReviewFlowParam) => {
|
|
||||||
selection.value = [];
|
|
||||||
tableRef?.value?.reload({ where: where });
|
|
||||||
};
|
|
||||||
|
|
||||||
/* 打开编辑弹窗 */
|
const openEditor = (flowId?: number) => {
|
||||||
const openEdit = (row?: ReviewFlow) => {
|
if (flowId == null) {
|
||||||
current.value = row ?? null;
|
editing.value = null;
|
||||||
showEdit.value = true;
|
editorOpen.value = true;
|
||||||
};
|
return;
|
||||||
|
|
||||||
/* 打开批量移动弹窗 */
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
Modal.confirm({
|
const group = flowGroups.value.find((g) => g.id === flowId);
|
||||||
title: "提示",
|
if (!group) {
|
||||||
content: "确定要删除选中的记录吗?",
|
message.warning('这条审批链已不存在,请刷新后重试');
|
||||||
icon: createVNode(ExclamationCircleOutlined),
|
return;
|
||||||
maskClosable: true,
|
}
|
||||||
onOk: () => {
|
editing.value = group;
|
||||||
const hide = message.loading("请求中..", 0);
|
editorOpen.value = true;
|
||||||
removeBatchReviewFlow(selection.value.map((d) => d.id))
|
};
|
||||||
.then((msg) => {
|
|
||||||
hide();
|
|
||||||
message.success(msg);
|
|
||||||
reload();
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
hide();
|
|
||||||
message.error(e.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/* 查询 */
|
const changeChain = async (row: RuleRow, flowId: string | number) => {
|
||||||
const query = () => {
|
const target = configsOf(row.module)[0];
|
||||||
loading.value = true;
|
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 onSaved = async () => {
|
||||||
const customRow = (record: ReviewFlow) => {
|
// 链改完,分组的 title / 层级都变了,必须强制重取
|
||||||
return {
|
await load(true);
|
||||||
// 行点击事件
|
};
|
||||||
onClick: () => {
|
|
||||||
// console.log(record);
|
|
||||||
},
|
|
||||||
// 行双击事件
|
|
||||||
onDblclick: () => {
|
|
||||||
openEdit(record);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
query();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<style lang="less" scoped>
|
||||||
export default {
|
.rr-head {
|
||||||
name: "ReviewFlow"
|
display: flex;
|
||||||
};
|
align-items: flex-start;
|
||||||
</script>
|
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>
|
<template>
|
||||||
<div>
|
<div class="review-search">
|
||||||
<module-registry-notice
|
<module-registry-notice
|
||||||
:error="moduleLoadError"
|
:error="moduleLoadError"
|
||||||
@retry="loadModuleOptions(true)"
|
@retry="loadModuleOptions(true)"
|
||||||
/>
|
/>
|
||||||
<a-space :size="10" style="flex-wrap: wrap">
|
<a-input-search
|
||||||
<a-select
|
v-model:value="keywords"
|
||||||
v-model:value="where.module"
|
allow-clear
|
||||||
allow-clear
|
placeholder="搜索申报名称 / 审核意见"
|
||||||
placeholder="请选择模块"
|
style="width: 300px"
|
||||||
style="width: 220px"
|
@search="search"
|
||||||
:options="moduleOptions"
|
@pressEnter="search"
|
||||||
@change="search"
|
/>
|
||||||
/>
|
|
||||||
<a-input-search
|
|
||||||
v-model:value="where.keywords"
|
|
||||||
allow-clear
|
|
||||||
placeholder="请输入名称/审核意见"
|
|
||||||
style="width: 260px"
|
|
||||||
@search="search"
|
|
||||||
@pressEnter="search"
|
|
||||||
/>
|
|
||||||
</a-space>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, reactive, ref, watch } from 'vue';
|
import { onMounted, ref } from 'vue';
|
||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import type { ReviewListParam } from '@/api/gxmu/reviewList/model';
|
import type { ReviewListParam } from '@/api/gxmu/reviewList/model';
|
||||||
import {
|
import {
|
||||||
allModuleOptions,
|
|
||||||
isModuleRegistryStale,
|
isModuleRegistryStale,
|
||||||
loadModuleRegistry
|
loadModuleRegistry
|
||||||
} from '@/utils/module-registry';
|
} from '@/utils/module-registry';
|
||||||
import type { ModuleOption } from '@/utils/module-registry-core';
|
|
||||||
import ModuleRegistryNotice from '@/views/gxmu/components/moduleRegistryNotice.vue';
|
import ModuleRegistryNotice from '@/views/gxmu/components/moduleRegistryNotice.vue';
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
selection?: [];
|
|
||||||
}>(),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'search', where?: ReviewListParam): void;
|
(e: 'search', where?: ReviewListParam): void;
|
||||||
(e: 'add'): void;
|
|
||||||
(e: 'remove'): void;
|
|
||||||
(e: 'batchMove'): void;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const where = reactive<ReviewListParam>({
|
const keywords = ref<string>();
|
||||||
module: undefined,
|
|
||||||
keywords: undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
// 模块筛选项来自模块注册表的全量清单(含停用模块,标题带「(已停用)」):
|
|
||||||
// 审核列表是「查存量」的入口,停用模块的历史记录仍要能按模块筛出来
|
|
||||||
const moduleOptions = ref<ModuleOption[]>([]);
|
|
||||||
const moduleLoadError = ref<string>();
|
const moduleLoadError = ref<string>();
|
||||||
|
|
||||||
|
// 模块标题(列表里的模块名、左侧导航都来自注册表);拉不到时保留上次成功的快照并提示重试
|
||||||
const loadModuleOptions = async (force = false) => {
|
const loadModuleOptions = async (force = false) => {
|
||||||
moduleLoadError.value = undefined;
|
moduleLoadError.value = undefined;
|
||||||
try {
|
try {
|
||||||
const list = await loadModuleRegistry(force);
|
await loadModuleRegistry(force);
|
||||||
moduleOptions.value = allModuleOptions(list);
|
|
||||||
// 命中「上次成功」的兜底快照:只给顶部提示 + 重试,不重复弹 message
|
|
||||||
if (isModuleRegistryStale()) {
|
if (isModuleRegistryStale()) {
|
||||||
moduleLoadError.value = '模块清单加载失败,已保留上次成功加载的清单';
|
moduleLoadError.value = '模块清单加载失败,已保留上次成功加载的清单';
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// 拉取失败:保留上次成功的清单(内存 / localStorage),顶部提示 + 重试
|
moduleLoadError.value = '模块清单加载失败,请重试';
|
||||||
moduleOptions.value = allModuleOptions();
|
|
||||||
moduleLoadError.value = moduleOptions.value.length
|
|
||||||
? '模块清单加载失败,已保留上次成功加载的清单'
|
|
||||||
: '模块清单加载失败,请重试';
|
|
||||||
message.warning(moduleLoadError.value);
|
message.warning(moduleLoadError.value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -83,14 +49,15 @@
|
|||||||
onMounted(() => loadModuleOptions());
|
onMounted(() => loadModuleOptions());
|
||||||
|
|
||||||
const search = () => {
|
const search = () => {
|
||||||
emit('search', {
|
emit('search', { keywords: keywords.value?.trim() || undefined });
|
||||||
...where,
|
|
||||||
keywords: where.keywords?.trim() || undefined
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
|
||||||
() => props.selection,
|
|
||||||
() => {}
|
|
||||||
);
|
|
||||||
</script>
|
</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>
|
<template>
|
||||||
<div class="page">
|
<div class="rw">
|
||||||
<div class="ele-body">
|
<!-- 左:模块导航。审核人每天先看「哪个模块有活」 -->
|
||||||
|
<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' }">
|
<a-card :bordered="false" :body-style="{ padding: '16px' }">
|
||||||
<ele-pro-table
|
<ele-pro-table
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
@@ -8,61 +82,138 @@
|
|||||||
:columns="columns"
|
:columns="columns"
|
||||||
:datasource="datasource"
|
:datasource="datasource"
|
||||||
tool-class="ele-toolbar-form"
|
tool-class="ele-toolbar-form"
|
||||||
class="sys-org-table"
|
|
||||||
>
|
>
|
||||||
<template #toolbar>
|
<template #toolbar>
|
||||||
<search
|
<search @search="onSearch" />
|
||||||
@search="reload"
|
|
||||||
:selection="selection"
|
|
||||||
@add="openEdit"
|
|
||||||
@remove="removeBatch"
|
|
||||||
@batchMove="openMove"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
<template v-if="column.key === 'status'">
|
<template v-if="column.key === 'businessName'">
|
||||||
<a-tag v-if="record.status === 0">待审核</a-tag>
|
<div class="rw-name">
|
||||||
<a-tag v-else-if="record.status === 1" color="green">通过</a-tag>
|
<b>{{ record.businessName || '未命名申报 #' + record.pk }}</b>
|
||||||
<a-tag v-else-if="record.status === 2" color="red">驳回</a-tag>
|
<span>{{ record.moduleName || record.module }}</span>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="column.key === 'action'">
|
<template v-else-if="column.key === 'level'">
|
||||||
<a-space>
|
<span :class="['rw-level', levelClass(record)]">
|
||||||
<a @click="openDataModal(record)">查看数据</a>
|
{{ levelLabel(record) }}
|
||||||
<a-divider type="vertical" />
|
</span>
|
||||||
<a @click="openFlowModal(record)">查看审核流</a>
|
</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>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</ele-pro-table>
|
</ele-pro-table>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
</section>
|
||||||
|
|
||||||
<ReviewListEdit
|
<!-- 详情:先看审批走到哪,再看申报内容 -->
|
||||||
v-model:visible="showEdit"
|
<a-drawer
|
||||||
:data="current"
|
:visible="detailOpen"
|
||||||
@done="reload"
|
: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
|
</a-modal>
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { createVNode, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { message, Modal } from 'ant-design-vue';
|
import { useRouter } from 'vue-router';
|
||||||
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import type { EleProTable } from 'ele-admin-pro';
|
import type { EleProTable } from 'ele-admin-pro';
|
||||||
import { toDateString } from 'ele-admin-pro';
|
import { toDateString } from 'ele-admin-pro';
|
||||||
import type {
|
import type {
|
||||||
@@ -70,167 +221,553 @@
|
|||||||
ColumnItem
|
ColumnItem
|
||||||
} from 'ele-admin-pro/es/ele-pro-table/types';
|
} from 'ele-admin-pro/es/ele-pro-table/types';
|
||||||
import Search from './components/search.vue';
|
import Search from './components/search.vue';
|
||||||
import ReviewListEdit from './components/reviewListEdit.vue';
|
|
||||||
import ModuleDataModal from './components/moduleDataModal.vue';
|
import ModuleDataModal from './components/moduleDataModal.vue';
|
||||||
import ReviewFlowModal from './components/reviewFlowModal.vue';
|
|
||||||
import {
|
import {
|
||||||
|
getPendingSummary,
|
||||||
pageGroupReviewList,
|
pageGroupReviewList,
|
||||||
removeBatchReviewList
|
pageReviewList,
|
||||||
|
updateReviewList
|
||||||
} from '@/api/gxmu/reviewList';
|
} from '@/api/gxmu/reviewList';
|
||||||
import type {
|
import type {
|
||||||
ReviewList,
|
ReviewList,
|
||||||
ReviewListParam
|
ReviewListParam,
|
||||||
|
ReviewPendingSummary
|
||||||
} from '@/api/gxmu/reviewList/model';
|
} 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 tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||||
const selection = ref<ReviewList[]>([]);
|
const scope = ref<'mine' | 'all'>('mine');
|
||||||
const current = ref<ReviewList | null>(null);
|
const activeModule = ref<string>('all');
|
||||||
const dataCurrent = ref<ReviewList | null>(null);
|
const keywords = ref<string>();
|
||||||
const flowCurrent = ref<ReviewList | null>(null);
|
const summary = ref<ReviewPendingSummary>({ total: 0, modules: [] });
|
||||||
const showEdit = ref(false);
|
const moduleOptions = ref<ModuleOption[]>([]);
|
||||||
const showDataVisible = ref(false);
|
const detail = ref<ReviewList | null>(null);
|
||||||
const showFlowVisible = ref(false);
|
const detailOpen = ref(false);
|
||||||
const showMove = ref(false);
|
const dataVisible = ref(false);
|
||||||
const loading = ref(true);
|
const rejectOpen = ref(false);
|
||||||
|
const rejectTarget = ref<ReviewList | null>(null);
|
||||||
|
const rejectReason = ref('');
|
||||||
|
const rejecting = ref(false);
|
||||||
|
|
||||||
const datasource: DatasourceFunction = ({
|
const myUserId = computed(() => userStore.info?.userId);
|
||||||
page,
|
|
||||||
limit,
|
// 只有拿到「审批规则」菜单的人才能看到入口:改链是校团委的事,
|
||||||
where,
|
// 学院角色能进工作台、但不该进规则页
|
||||||
orders,
|
const canSeeRules = computed(() => {
|
||||||
filters
|
const walk = (list?: any[]): boolean =>
|
||||||
}) => {
|
(list || []).some(
|
||||||
if (filters) {
|
(node) =>
|
||||||
where.status = filters.status;
|
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({
|
if (activeModule.value === 'all') {
|
||||||
...where,
|
return '左侧按模块看谁有活;每条申报都显示它当前停在哪一级。';
|
||||||
...orders,
|
}
|
||||||
page,
|
if (!activeChain.value) {
|
||||||
limit
|
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[]>([
|
const columns = ref<ColumnItem[]>([
|
||||||
{
|
{ title: '申报名称', key: 'businessName', ellipsis: true },
|
||||||
title: 'ID',
|
{ title: '当前环节', key: 'level', width: 210 },
|
||||||
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: '提交时间',
|
title: '提交时间',
|
||||||
dataIndex: 'createTime',
|
dataIndex: 'createTime',
|
||||||
key: 'createTime',
|
key: 'createTime',
|
||||||
align: 'center',
|
width: 160,
|
||||||
sorter: true,
|
sorter: true,
|
||||||
ellipsis: true,
|
customRender: ({ text }) => fmt(text)
|
||||||
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '当前审核人',
|
|
||||||
dataIndex: ['user', 'realName'],
|
|
||||||
key: 'userId',
|
|
||||||
align: 'center'
|
|
||||||
},
|
},
|
||||||
|
{ title: '状态', key: 'status', width: 110 },
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 220,
|
width: 160,
|
||||||
fixed: 'right',
|
fixed: 'right',
|
||||||
align: 'center',
|
|
||||||
hideInSetting: true
|
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) => {
|
const reload = (where?: ReviewListParam) => {
|
||||||
selection.value = [];
|
if (where) {
|
||||||
tableRef?.value?.reload({ where });
|
keywords.value = where.keywords;
|
||||||
|
}
|
||||||
|
tableRef.value?.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
const openEdit = (row?: ReviewList) => {
|
const onSearch = (where?: ReviewListParam) => {
|
||||||
current.value = row ?? null;
|
keywords.value = where?.keywords;
|
||||||
showEdit.value = true;
|
tableRef.value?.reload({ where: {} });
|
||||||
};
|
};
|
||||||
|
|
||||||
const openMove = () => {
|
const selectModule = (module: string) => {
|
||||||
showMove.value = true;
|
activeModule.value = module;
|
||||||
|
tableRef.value?.reload({ where: {} });
|
||||||
};
|
};
|
||||||
|
|
||||||
const openDataModal = (row: ReviewList) => {
|
const goRules = () => {
|
||||||
dataCurrent.value = row;
|
router.push('/review/gxmu/reviewFlow');
|
||||||
showDataVisible.value = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openFlowModal = (row: ReviewList) => {
|
const loadSummary = async () => {
|
||||||
flowCurrent.value = row;
|
try {
|
||||||
showFlowVisible.value = true;
|
summary.value = await getPendingSummary();
|
||||||
|
} catch (e: any) {
|
||||||
|
// 统计失败不该挡住列表,导航不显示数字即可
|
||||||
|
summary.value = { total: 0, modules: [] };
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeBatch = () => {
|
const loadModules = async () => {
|
||||||
if (!selection.value.length) {
|
try {
|
||||||
message.error('请至少选择一条数据');
|
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;
|
return;
|
||||||
}
|
}
|
||||||
Modal.confirm({
|
if (!rejectTarget.value?.id) {
|
||||||
title: '提示',
|
return;
|
||||||
content: '确定要删除选中的记录吗?',
|
}
|
||||||
icon: createVNode(ExclamationCircleOutlined),
|
rejecting.value = true;
|
||||||
maskClosable: true,
|
try {
|
||||||
onOk: () => {
|
await updateReviewList({
|
||||||
const hide = message.loading('请求中..', 0);
|
id: rejectTarget.value.id,
|
||||||
removeBatchReviewList(selection.value.map((d) => d.id))
|
status: 2,
|
||||||
.then((msg) => {
|
content: rejectReason.value.trim()
|
||||||
hide();
|
});
|
||||||
message.success(msg);
|
message.success('已驳回');
|
||||||
reload();
|
rejectOpen.value = false;
|
||||||
})
|
detailOpen.value = false;
|
||||||
.catch((e) => {
|
tableRef.value?.reload();
|
||||||
hide();
|
await loadSummary();
|
||||||
message.error(e.message);
|
} catch (e: any) {
|
||||||
});
|
message.error(e.message);
|
||||||
}
|
} finally {
|
||||||
});
|
rejecting.value = false;
|
||||||
};
|
}
|
||||||
|
|
||||||
const query = () => {
|
|
||||||
loading.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
query();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
export default {
|
|
||||||
name: 'ReviewList'
|
|
||||||
};
|
};
|
||||||
</script>
|
</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