refactor: 重命名 CRUD 组件为 Crud 并简化字典标签类型

- CURD 组件 PageModal、PageSearch、PageContent、usePage 重命名为 CrudModal、CrudSearch、CrudTable、useCrudPage
- 字典项标签类型改用 primary/success/warning/danger/info,保留历史单字母值兼容读取
- 字典项页面拆分为独立子组件,移除 GithubCorner 组件
- 新增 icon 工具与 screen 样式
- 新增路由缓存、路由跳转、大屏与表格 CRUD 示例页
This commit is contained in:
Ray.Hao committed 2026-09-25 21:10:54 +08:00
1 parent 3ccbdb7c06
commit 89d45da839
224 files changed
+7926 -3052

No files matched your search

@@ -58,10 +58,8 @@ defineOptions({
/**
* 审批流程时间线(流程走向 + 审批记录融合)
*
* @description 以设计环节为骨架按编排顺序展示,实际办理记录按 activityId 归位到环节:
* 已办(办理人/意见/时间)、进行中(待认领或等待办理)、未到(运行中位于当前环节之后)、
* 未经过(网关分支跳过或流程结束仍未到达),驳回重办的多轮记录全部保留
* 以设计环节为骨架按编排顺序展示,实际办理记录按 activityId 归位到环节:
* 已办(办理人/意见/时间)、进行中(待认领或等待办理)、未到(运行中位于当前环节之后)、 未经过(网关分支跳过或流程结束仍未到达),驳回重办的多轮记录全部保留
*/
const props = defineProps<{
/** 审批环节(按 BPMN 编排顺序,含分支未走节点) */
@@ -74,7 +72,7 @@ const props = defineProps<{
status: InstanceStatus;
}>();
/** 节点ID -> 办理记录(驳回重办为多条) */
// 节点 ID -> 办理记录(驳回重办为多条)
const recordMap = computed(() => {
const map = new Map<string, ApprovalHistoryItem[]>();
props.history.forEach((item) => {
@@ -86,25 +84,37 @@ const recordMap = computed(() => {
return map;
});
/**
* 取节点对应的审批记录
*/
function recordsOf(nodeId: string): ApprovalHistoryItem[] {
return recordMap.value.get(nodeId) ?? [];
}
/**
* 判断节点是否已有审批记录
*/
function hasRecord(nodeId: string): boolean {
return recordsOf(nodeId).length > 0;
}
/** 是否"未到":流程运行中且位于当前环节之后 */
/**
* 是否"未到":流程运行中且位于当前环节之后
*/
function isUpcoming(index: number): boolean {
return props.status === "running" && index >= props.active;
}
/** 无记录环节状态标签:未到(后续环节)/ 未经过(分支跳过或已结束) */
/**
* 无记录环节状态标签:未到(后续环节)/ 未经过(分支跳过或已结束)
*/
function pendingLabel(index: number): string {
return isUpcoming(index) ? "未到" : "未经过";
}
/** 环节办理人描述:角色(成员账号);发起人办理环节标注"发起人" */
/**
* 环节办理人描述:角色(成员账号);发起人办理环节标注"发起人"
*/
function describe(stage: ProcessStageItem): string {
if (stage.initiator) {
return "发起人";
+21 -14
View File
@@ -186,8 +186,7 @@ defineOptions({
/**
* BPMN 流程设计器
*
* @description bpmn-js 建模器 + 轻量属性面板(名称/表单标识/办理人/候选组/条件表达式),
* bpmn-js 建模器 + 轻量属性面板(名称/表单标识/办理人/候选组/条件表达式),
* 扩展 flowable 命名空间属性,保存时产出引擎可直接部署的 BPMN 2.0 XML
*/
const props = defineProps<{
@@ -205,10 +204,10 @@ const emit = defineEmits<{
const canvasRef = ref<HTMLElement | null>(null);
const modeler = shallowRef<Modeler>();
/** 脏标记:画布有未保存的修改(承载页据此拦截离开) */
// 脏标记:画布有未保存的修改(承载页据此拦截离开)
const dirty = ref(false);
/** 当前选中元素(Shape/Connection 统称) */
// 当前选中元素(Shape/Connection 统称)
const selectedElement = shallowRef<Shape | null>(null);
const isUserTask = computed(
@@ -217,7 +216,7 @@ const isUserTask = computed(
const isSequenceFlow = computed(
() => !!selectedElement.value && is(selectedElement.value, "bpmn:SequenceFlow")
);
/** 仅开始节点可绑定表单(后端只读开始节点 formKey 渲染发起表单,审批展示复用同一份数据) */
// 仅开始节点可绑定表单(后端只读开始节点 formKey 渲染发起表单,审批展示复用同一份数据)
const isFormBindable = computed(
() => !!selectedElement.value && is(selectedElement.value, "bpmn:StartEvent")
);
@@ -227,20 +226,20 @@ const selectedFormKey = computed(() => selectedElement.value?.businessObject?.ge
const selectedAssignee = computed(
() => selectedElement.value?.businessObject?.get("assignee") ?? ""
);
/** 候选组(角色编码,逗号分隔)与办理人二选一配置,均空发布时会被后端拦截 */
// 候选组(角色编码,逗号分隔)与办理人二选一配置,均空发布时会被后端拦截
const selectedCandidateGroups = computed(
() => selectedElement.value?.businessObject?.get("candidateGroups") ?? ""
);
/** 候选组多选值:XML 逗号串 ↔ 选项数组互转,数据结构保持引擎兼容 */
// 候选组多选值:XML 逗号串 ↔ 选项数组互转,数据结构保持引擎兼容
const candidateGroupCodes = computed(() =>
String(selectedCandidateGroups.value)
.split(",")
.map((code) => code.trim())
.filter(Boolean)
);
/** 角色编码选项(label 角色名 / value 角色编码) */
// 角色编码选项(label 角色名 / value 角色编码)
const roleCodeOptions = ref<OptionItem[]>([]);
/** 发起表单选项(label 表单名 / value formKey,仅已发布 workflow 类型) */
// 发起表单选项(label 表单名 / value formKey,仅已发布 workflow 类型)
const workflowFormOptions = ref<OptionItem[]>([]);
const selectedCondition = computed(
() => selectedElement.value?.businessObject?.conditionExpression?.body ?? ""
@@ -250,7 +249,7 @@ const selectedCondition = computed(
* 更新元素普通属性(名称/formKey/assignee/candidateGroups)
*
* @param property 属性名
* @param value 属性值(空值传 undefined 移除属性,避免序列化出空串属性干扰引擎解析)
* @param value 属性值(空值传 undefined 移除属性,避免序列化出空串属性干扰引擎解析)
*/
function updateProperty(property: string, value: string): void {
const element = selectedElement.value;
@@ -273,12 +272,16 @@ function updateCandidateGroups(codes: string[]): void {
updateProperty("candidateGroups", codes.join(","));
}
/** 加载角色编码选项(候选组下拉数据源) */
/**
* 加载角色编码选项(候选组下拉数据源)
*/
async function loadRoleCodeOptions(): Promise<void> {
roleCodeOptions.value = await RoleAPI.getCodeOptions();
}
/** 加载发起表单选项(formKey 下拉数据源) */
/**
* 加载发起表单选项(formKey 下拉数据源)
*/
async function loadWorkflowFormOptions(): Promise<void> {
workflowFormOptions.value = await FormAPI.getWorkflowOptions();
}
@@ -319,12 +322,16 @@ function handleZoom(delta: number): void {
canvas.zoom(canvas.zoom() + delta);
}
/** 适应画布(完整展示流程图) */
/**
* 适应画布(完整展示流程图)
*/
function handleFitViewport(): void {
(modeler.value!.get("canvas") as CanvasLike).zoom("fit-viewport");
}
/** 导入 XML 并选中态复位 */
/**
* 导入 XML 并选中态复位
*/
async function importXml(xml: string): Promise<void> {
if (!xml || !modeler.value) return;
try {
+5 -6
View File
@@ -18,16 +18,15 @@ defineOptions({
/**
* BPMN 流程图查看器
*
* @description 渲染 BPMN XML 并按节点状态高亮:已办节点标记走过路径、
* 渲染 BPMN XML 并按节点状态高亮:已办节点标记走过路径、
* 进行中节点标记当前待办;缩放平移由 NavigatedViewer 自带滚轮/空格拖拽支持
*/
const props = defineProps<{
/** BPMN 2.0 XML */
xml: string;
/** 已办节点ID列表(走过路径高亮) */
/** 已办节点 ID 列表(走过路径高亮) */
executedActivityIds?: string[];
/** 进行中节点ID列表(当前待办高亮) */
/** 进行中节点 ID 列表(当前待办高亮) */
activeActivityIds?: string[];
}>();
@@ -38,8 +37,8 @@ const viewer = shallowRef<Viewer>();
* 导入 XML 并叠加高亮标记
*
* @param xml BPMN XML
* @param executedIds 已办节点ID列表
* @param activeIds 进行中节点ID列表
* @param executedIds 已办节点 ID 列表
* @param activeIds 进行中节点 ID 列表
*/
async function renderDiagram(
xml: string,
+1 -3
View File
@@ -12,8 +12,7 @@ defineOptions({
/**
* 表单只读回显
*
* @description 按提交时快照规则渲染表单并禁用全部控件,
* 按提交时快照规则渲染表单并禁用全部控件,
* 供审批办理、实例详情等场景回看发起数据
*/
const props = defineProps<{
@@ -45,7 +44,6 @@ watch(
/**
* 递归禁用规则中的全部控件
*
* 同时移除校验规则,只读态不展示必填星号
*
* @param rules form-create 规则(JSON 解析值)
@@ -69,7 +69,7 @@ defineOptions({
name: "InstanceDetailDrawer",
});
/** 实例状态展示映射(标签文案 + 标签色) */
// 实例状态展示映射(标签文案 + 标签色)
const statusOptions: Record<
InstanceStatus,
{ label: string; tag: "primary" | "success" | "danger" }
@@ -84,7 +84,7 @@ const loading = ref(false);
const detail = ref<InstanceDetailData>();
/** 流程图数据(详情抽屉"流程图"页签) */
// 流程图数据(详情抽屉"流程图"页签)
const diagram = ref<ProcessDiagramData>({
bpmnXml: "",
executedActivityIds: [],
+5 -5
View File
@@ -33,15 +33,15 @@ defineOptions({
const route = useRoute();
/** 模型 ID(设计器始终由列表页"设计"按钮带参进入) */
// 模型 ID(设计器始终由列表页"设计"按钮带参进入)
const modelId = computed(() => String(route.query.modelId ?? ""));
/** 页面标题(列表页携带,如【请假审批】流程设计) */
// 页面标题(列表页携带,如【请假审批】流程设计)
const title = computed(() => String(route.query.title ?? "流程设计"));
const designerRef = ref<InstanceType<typeof BpmnDesigner>>();
/** 回显的 BPMN XML(新模型为空串,画布空白可自由拖拽) */
// 回显的 BPMN XML(新模型为空串,画布空白可自由拖拽)
const xml = ref("");
const loading = ref(false);
@@ -58,7 +58,7 @@ onMounted(() => {
});
// 菜单 keep_alive=1 缓存页面,换模型再次进入需按 modelId 重载画布;
// 离开页面时 query 置空,跳过重载避免缓存页误报"缺少模型ID参数"
// 离开页面时 query 置空,跳过重载避免缓存页误报"缺少模型 ID 参数"
watch(modelId, (val) => {
if (val) loadXml();
});
@@ -101,7 +101,7 @@ async function handleSave(xml: string): Promise<void> {
}
/**
* 返回流程设计列表(有未保存修改时二次确认)
* @returns 流程设计列表(有未保存修改时二次确认)
*/
async function handleBack(): Promise<void> {
if (designerRef.value?.isDirty()) {
+11 -13
View File
@@ -104,10 +104,10 @@ defineOptions({
inheritAttrs: false,
});
/** 演示引导提示条可见状态(关闭后本次会话不再显示) */
// 演示引导提示条可见状态(关闭后本次会话不再显示)
const demoTipVisible = ref(true);
/** 可发起流程列表 */
// 可发起流程列表
const processList = ref<StartableProcessItem[]>([]);
const selectedId = ref("");
@@ -116,28 +116,25 @@ const selectedProcess = computed(() =>
processList.value.find((process) => process.id === selectedId.value)
);
/** 审批流程走向预览 */
// 审批流程走向预览
const stages = ref<ProcessStageItem[]>([]);
const userStore = useUserStore();
/**
* 流程实例名称:默认按"流程-姓名-时间"自动拼接,切换流程或手动修改后重置,
* 用户可编辑以获得可读标题(同一流程多次发起也能从列表一眼区分)
*/
// 流程实例名称:默认按"流程-姓名-时间"自动拼接,切换流程或手动修改后重置, 用户可编辑以获得可读标题(同一流程多次发起也能从列表一眼区分)
const processName = ref("");
const autoNameValue = ref("");
/** 名称被手动修改过:区别于自动值,展示"恢复自动命名"入口 */
// 名称被手动修改过:区别于自动值,展示"恢复自动命名"入口
const autoNameDirty = ref(false);
/**
* 生成默认流程名称:流程名-昵称-MM月DD日
*
* <p>精确到日即可:同日多次发起由列表的"发起时间"列区分,名称保持简洁;特殊情况可手动编辑</p>
* 生成默认流程名称:流程名-昵称-MM 月 DD 日
* 精确到日即可:同日多次发起由列表的"发起时间"列区分,名称保持简洁;特殊情况可手动编辑
*/
function buildAutoName(): string {
const nickname = userStore.userInfo?.nickname || userStore.userInfo?.username || "";
const now = new Date();
// 数值补零为两位
const pad = (num: number) => String(num).padStart(2, "0");
return selectedProcess.value
? `${selectedProcess.value.name}-${nickname}-${pad(now.getMonth() + 1)}月${pad(now.getDate())}日`
@@ -168,7 +165,9 @@ function resetAutoName(): void {
autoNameDirty.value = false;
}
/** 发起表单渲染状态(复用动态表单渲染管线,保证与填写页一致) */
/**
* 发起表单渲染状态(复用动态表单渲染管线,保证与填写页一致)
*/
const { rule, option, loading, submitted, load, refill } = useFormRenderer(() =>
FormAPI.getRender(selectedProcess.value?.formKey ?? "")
);
@@ -181,7 +180,6 @@ onMounted(async () => {
/**
* 切换流程时按 formKey 加载发起表单规则与审批走向
*
* 未绑定表单的流程走确认直发,不加载规则
*/
async function handleProcessChange(): Promise<void> {
+7 -5
View File
@@ -257,10 +257,12 @@ const { toggle: toggleFullscreen } = useFullscreen(tableWrapperRef);
const queryFormRef = ref<FormInstance>();
const modelFormRef = ref<FormInstance>();
/** 启用开关权限(el-switch 不支持 v-hasPerm 指令移除,改 v-if 控制) */
/**
* 启用开关权限(el-switch 不支持 v-hasPerm 指令移除,改 v-if 控制)
*/
const hasStatePerm = hasPerm("workflow:definition:update");
/** 分页表格数据管理 */
// 分页表格数据管理
const { loading, list, total, params, fetchData, handleQuery, handleResetQuery } = usePageTable<
WorkflowModelItem,
WorkflowModelQueryParams
@@ -297,7 +299,7 @@ const rules: FormRules<WorkflowModelFormData> = {
],
};
/** 流程图弹窗状态 */
// 流程图弹窗状态
const diagramState = reactive({
title: "",
visible: false,
@@ -305,7 +307,7 @@ const diagramState = reactive({
xml: "",
});
/** 正在切换启用状态的流程 ID(该行开关转圈,其余行禁用防并发) */
// 正在切换启用状态的流程 ID(该行开关转圈,其余行禁用防并发)
const switchingId = ref<string | null>(null);
/**
@@ -465,7 +467,7 @@ async function handleResetDemo(): Promise<void> {
}
}
/** 是否正在重置演示流程(重置请求期间按钮转圈并防重复点击) */
// 是否正在重置演示流程(重置请求期间按钮转圈并防重复点击)
const resetting = ref(false);
/**
@@ -112,7 +112,7 @@ const { toggle: toggleFullscreen } = useFullscreen(tableWrapperRef);
const queryFormRef = ref<FormInstance>();
/** 分页表格数据管理 */
// 分页表格数据管理
const { loading, list, total, params, fetchData, handleQuery, handleResetQuery } = usePageTable<
DoneTaskItem,
WorkflowTaskQueryParams
@@ -126,10 +126,12 @@ const { loading, list, total, params, fetchData, handleQuery, handleResetQuery }
onBeforeReset: () => queryFormRef.value?.resetFields(),
});
/** 暴露刷新能力:审批中心页签切换时父组件调用 */
/**
* 暴露刷新能力:审批中心页签切换时父组件调用
*/
defineExpose({ fetchData });
/** 实例详情抽屉 */
// 实例详情抽屉
const detailDrawerRef = ref<InstanceType<typeof InstanceDetailDrawer>>();
/**
@@ -146,7 +146,7 @@ const { toggle: toggleFullscreen } = useFullscreen(tableWrapperRef);
const queryFormRef = ref<FormInstance>();
/** 实例状态展示映射(标签文案 + 标签色) */
// 实例状态展示映射(标签文案 + 标签色)
const statusOptions: Record<
InstanceStatus,
{ label: string; tag: "primary" | "success" | "danger" }
@@ -156,7 +156,7 @@ const statusOptions: Record<
terminated: { label: "已终止", tag: "danger" },
};
/** 分页表格数据管理 */
// 分页表格数据管理
const { loading, list, total, params, fetchData, handleQuery, handleResetQuery } = usePageTable<
WorkflowInstanceItem,
WorkflowInstanceQueryParams
@@ -170,10 +170,12 @@ const { loading, list, total, params, fetchData, handleQuery, handleResetQuery }
onBeforeReset: () => queryFormRef.value?.resetFields(),
});
/** 暴露刷新能力:审批中心页签切换时父组件调用 */
/**
* 暴露刷新能力:审批中心页签切换时父组件调用
*/
defineExpose({ fetchData });
/** 实例详情抽屉 */
// 实例详情抽屉
const detailDrawerRef = ref<InstanceType<typeof InstanceDetailDrawer>>();
/**
+16 -11
View File
@@ -92,8 +92,8 @@
<el-descriptions-item label="当前任务">{{ detail.taskName }}</el-descriptions-item>
</el-descriptions>
<!-- AI 摘要:按需生成,未开启 AI 时接口不存在,失败不影响审批 -->
<div class="todo-approve__ai">
<!-- AI 摘要:按需生成,未开启 AI 时入口不显示;生成失败不影响审批 -->
<div v-if="appConfig.aiEnabled" class="todo-approve__ai">
<div class="todo-approve__ai-header">
<span>AI 摘要</span>
<el-button link type="primary" :loading="aiLoading" @click="handleAiSummary">
@@ -187,6 +187,7 @@ import type {
WorkflowTaskQueryParams,
} from "@/api/workflow";
import { usePageTable } from "@/composables";
import { appConfig } from "@/settings";
import ApprovalTimeline from "../../components/ApprovalTimeline.vue";
import FormDetail from "../../components/FormDetail.vue";
import ProcessStages from "../../components/ProcessStages.vue";
@@ -195,7 +196,9 @@ defineOptions({
name: "WorkflowTodoTab",
});
/** 待办数量回传(驱动审批中心页签角标) */
/**
* 待办数量回传(驱动审批中心页签角标)
*/
const emit = defineEmits<{
totalChange: [total: number];
}>();
@@ -205,7 +208,7 @@ const { toggle: toggleFullscreen } = useFullscreen(tableWrapperRef);
const queryFormRef = ref<FormInstance>();
/** 分页表格数据管理 */
// 分页表格数据管理
const { loading, list, total, params, fetchData, handleQuery, handleResetQuery } = usePageTable<
TodoTaskItem,
WorkflowTaskQueryParams
@@ -221,27 +224,29 @@ const { loading, list, total, params, fetchData, handleQuery, handleResetQuery }
watch(total, (value) => emit("totalChange", value), { immediate: true });
/** 暴露刷新能力:审批中心页签切换时父组件调用 */
/**
* 暴露刷新能力:审批中心页签切换时父组件调用
*/
defineExpose({ fetchData });
/** 办理弹窗状态 */
// 办理弹窗状态
const approveState = reactive({
visible: false,
loading: false,
title: "审批办理",
});
/** 当前任务详情 */
// 当前任务详情
const detail = ref<TaskDetailData>();
/** 审批意见(通过/驳回共用) */
// 审批意见(通过/驳回共用)
const comment = ref("");
/** AI 摘要与生成状态 */
// AI 摘要与生成状态
const aiSummary = ref("");
const aiLoading = ref(false);
/** 当前任务在流程走向中的位置(节点ID精确匹配,当前环节高亮进行中,之前为已完成;未匹配时不高亮) */
// 当前任务在流程走向中的位置(节点 ID 精确匹配,当前环节高亮进行中,之前为已完成;未匹配时不高亮)
const currentStageIndex = computed(() => {
const stages = detail.value?.stages;
if (!stages?.length || !detail.value?.taskDefinitionKey) {
@@ -250,7 +255,7 @@ const currentStageIndex = computed(() => {
return stages.findIndex((stage) => stage.nodeId === detail.value?.taskDefinitionKey);
});
/** 驳回模式状态 */
// 驳回模式状态
const rejectMode = ref(false);
const rejectTargets = ref<RejectTargetItem[]>([]);
const rejectTargetId = ref("");
+4 -5
View File
@@ -37,21 +37,20 @@ defineOptions({
const route = useRoute();
/** 当前页签;发起成功跳转携带 ?tab=mine 直达「我发起的」查看进度 */
// 当前页签;发起成功跳转携带 ?tab=mine 直达「我发起的」查看进度
const activeTab = ref((route.query.tab as string) || "todo");
/** 待办数量(TodoTab 查询后回传,驱动页签角标) */
// 待办数量(TodoTab 查询后回传,驱动页签角标)
const todoTotal = ref(0);
/** 各页签组件引用(调用其暴露的刷新能力) */
// 各页签组件引用(调用其暴露的刷新能力)
const todoTabRef = ref<InstanceType<typeof TodoTab>>();
const doneTabRef = ref<InstanceType<typeof DoneTab>>();
const mineTabRef = ref<InstanceType<typeof MineTab>>();
/**
* 页签切换刷新对应列表
*
* @description lazy 页签首次激活时 ref 尚未就绪,由子组件 onMounted 自行首查,此处跳过不重复请求;
* lazy 页签首次激活时 ref 尚未就绪,由子组件 onMounted 自行首查,此处跳过不重复请求;
* 再次切换时调用子组件刷新,保证办理/发起后切回页签即见最新数据
*/
function handleTabChange(name: string | number): void {