feat: 新增工作流模块与动态表单发布
- src/api/workflow: 工作流模型、实例、任务接口与类型定义 - src/views/workflow: 流程设计、发起、审批中心(待办/已办/我发起)页面与 BPMN 设计器、预览器 - src/views/dynamic-form/components/publish: 发布弹窗拆分为多步(入口配置/方式/结果),原 PublishDialog 大幅精简 - src/api/form: 新增 getWorkflowOptions,表单定义增加 category 区分普通/工作流表单 - src/api/system/role: 新增 getCodeOptions 供流程候选组选择 - src/stores/permission: 路由重载先摘旧路由再注册新路由,避免空窗触发 404 - vite.config: 预构建 bpmn-js,避免设计器首进重优化刷新 - src/views/login、.env.development、LayoutToolbar: 本地开发联调调整(昵称展示、本地接口) - src/api/system/user: 重置密码改用 data 传参 - src/views/codegen: 支持 FormTypeEnum 日期字段范围查询 - package.json: 版本号 4.9.0;删除误存的 dev-output.txt
This commit is contained in:
1 parent
3503a399fb
commit
2ab0b31cfc
37 files changed
+5760
-1326
No files matched your search
@@ -0,0 +1,147 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="page-search" shadow="never">
|
||||
<el-form ref="queryFormRef" :model="params" :inline="true">
|
||||
<el-form-item label="关键字" prop="keywords">
|
||||
<el-input
|
||||
v-model="params.keywords"
|
||||
placeholder="任务名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">搜索</el-button>
|
||||
<el-button @click="handleResetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card ref="tableWrapperRef" class="page-content" shadow="never">
|
||||
<div class="page-toolbar">
|
||||
<div class="page-toolbar__left" />
|
||||
<div class="page-toolbar__right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button class="page-icon-btn" @click="fetchData">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="全屏" placement="top">
|
||||
<el-button class="page-icon-btn" @click="toggleFullscreen">
|
||||
<el-icon><FullScreen /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-table-wrapper">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
highlight-current-row
|
||||
:data="list"
|
||||
class="page-table"
|
||||
border
|
||||
height="100%"
|
||||
>
|
||||
<!-- 流程名称为主列:各流程首节点名相近(部门主管审批),流程名称才是区分项 -->
|
||||
<el-table-column
|
||||
label="流程名称"
|
||||
prop="processName"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="任务名称" prop="name" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column
|
||||
label="流程实例ID"
|
||||
prop="processInstanceId"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="办理时间" prop="endTime" width="170" align="center" />
|
||||
<el-table-column label="处理意见" prop="comment" min-width="160" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
{{ (scope.row as DoneTaskItem).comment || "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" label="操作" align="center" width="90">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click.stop="handleDetail((scope.row as DoneTaskItem).processInstanceId)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<pagination
|
||||
v-if="total > 0"
|
||||
v-model:total="total"
|
||||
v-model:page="params.pageNum"
|
||||
v-model:limit="params.pageSize"
|
||||
@pagination="fetchData"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 实例详情抽屉:表单数据 / 审批流程(走向+记录融合)/ 流程图 -->
|
||||
<InstanceDetailDrawer ref="detailDrawerRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFullscreen } from "@vueuse/core";
|
||||
import type { FormInstance } from "element-plus";
|
||||
import { Refresh } from "@element-plus/icons-vue";
|
||||
|
||||
import WorkflowAPI from "@/api/workflow";
|
||||
import type { DoneTaskItem, WorkflowTaskQueryParams } from "@/api/workflow";
|
||||
import { usePageTable } from "@/composables";
|
||||
import InstanceDetailDrawer from "../../components/InstanceDetailDrawer.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "WorkflowDoneTab",
|
||||
});
|
||||
|
||||
const tableWrapperRef = ref<HTMLElement | null>(null);
|
||||
const { toggle: toggleFullscreen } = useFullscreen(tableWrapperRef);
|
||||
|
||||
const queryFormRef = ref<FormInstance>();
|
||||
|
||||
/** 分页表格数据管理 */
|
||||
const { loading, list, total, params, fetchData, handleQuery, handleResetQuery } = usePageTable<
|
||||
DoneTaskItem,
|
||||
WorkflowTaskQueryParams
|
||||
>({
|
||||
initialParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
keywords: "",
|
||||
},
|
||||
request: WorkflowAPI.task.getDonePage,
|
||||
onBeforeReset: () => queryFormRef.value?.resetFields(),
|
||||
});
|
||||
|
||||
/** 暴露刷新能力:审批中心页签切换时父组件调用 */
|
||||
defineExpose({ fetchData });
|
||||
|
||||
/** 实例详情抽屉 */
|
||||
const detailDrawerRef = ref<InstanceType<typeof InstanceDetailDrawer>>();
|
||||
|
||||
/**
|
||||
* 打开实例详情抽屉
|
||||
*
|
||||
* @param instanceId 流程实例 ID
|
||||
*/
|
||||
function handleDetail(instanceId: string): void {
|
||||
detailDrawerRef.value?.open(instanceId);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleQuery();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="page-search" shadow="never">
|
||||
<el-form ref="queryFormRef" :model="params" :inline="true">
|
||||
<el-form-item label="关键字" prop="keywords">
|
||||
<el-input
|
||||
v-model="params.keywords"
|
||||
placeholder="流程名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">搜索</el-button>
|
||||
<el-button @click="handleResetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card ref="tableWrapperRef" class="page-content" shadow="never">
|
||||
<div class="page-toolbar">
|
||||
<div class="page-toolbar__left" />
|
||||
<div class="page-toolbar__right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button class="page-icon-btn" @click="fetchData">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="全屏" placement="top">
|
||||
<el-button class="page-icon-btn" @click="toggleFullscreen">
|
||||
<el-icon><FullScreen /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-table-wrapper">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
highlight-current-row
|
||||
:data="list"
|
||||
class="page-table"
|
||||
border
|
||||
height="100%"
|
||||
>
|
||||
<el-table-column
|
||||
label="流程名称"
|
||||
prop="processName"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column
|
||||
label="流程标识"
|
||||
prop="processDefinitionKey"
|
||||
min-width="150"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="状态" width="90" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag :type="statusOptions[(scope.row as WorkflowInstanceItem).status].tag">
|
||||
{{ statusOptions[(scope.row as WorkflowInstanceItem).status].label }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="发起时间" prop="startTime" width="170" align="center" />
|
||||
<el-table-column label="结束时间" width="170" align="center">
|
||||
<template #default="scope">
|
||||
{{ (scope.row as WorkflowInstanceItem).endTime || "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="终止原因"
|
||||
prop="deleteReason"
|
||||
min-width="120"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column fixed="right" label="操作" align="center" width="200">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click.stop="openDetail((scope.row as WorkflowInstanceItem).id)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="(scope.row as WorkflowInstanceItem).status === 'running'"
|
||||
type="warning"
|
||||
link
|
||||
size="small"
|
||||
@click.stop="handleCancel((scope.row as WorkflowInstanceItem).id)"
|
||||
>
|
||||
撤销
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="(scope.row as WorkflowInstanceItem).status === 'running'"
|
||||
v-hasPerm="['workflow:instance:terminate']"
|
||||
type="danger"
|
||||
link
|
||||
size="small"
|
||||
@click.stop="handleTerminate((scope.row as WorkflowInstanceItem).id)"
|
||||
>
|
||||
终止
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<pagination
|
||||
v-if="total > 0"
|
||||
v-model:total="total"
|
||||
v-model:page="params.pageNum"
|
||||
v-model:limit="params.pageSize"
|
||||
@pagination="fetchData"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 实例详情抽屉:表单数据 / 审批流程(走向+记录融合)/ 流程图 -->
|
||||
<InstanceDetailDrawer ref="detailDrawerRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFullscreen } from "@vueuse/core";
|
||||
import { ElMessage, ElMessageBox, type FormInstance } from "element-plus";
|
||||
import { Refresh } from "@element-plus/icons-vue";
|
||||
|
||||
import WorkflowAPI from "@/api/workflow";
|
||||
import type {
|
||||
InstanceStatus,
|
||||
WorkflowInstanceItem,
|
||||
WorkflowInstanceQueryParams,
|
||||
} from "@/api/workflow";
|
||||
import { usePageTable } from "@/composables";
|
||||
import InstanceDetailDrawer from "../../components/InstanceDetailDrawer.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "WorkflowMineTab",
|
||||
});
|
||||
|
||||
const tableWrapperRef = ref<HTMLElement | null>(null);
|
||||
const { toggle: toggleFullscreen } = useFullscreen(tableWrapperRef);
|
||||
|
||||
const queryFormRef = ref<FormInstance>();
|
||||
|
||||
/** 实例状态展示映射(标签文案 + 标签色) */
|
||||
const statusOptions: Record<
|
||||
InstanceStatus,
|
||||
{ label: string; tag: "primary" | "success" | "danger" }
|
||||
> = {
|
||||
running: { label: "运行中", tag: "primary" },
|
||||
finished: { label: "已完成", tag: "success" },
|
||||
terminated: { label: "已终止", tag: "danger" },
|
||||
};
|
||||
|
||||
/** 分页表格数据管理 */
|
||||
const { loading, list, total, params, fetchData, handleQuery, handleResetQuery } = usePageTable<
|
||||
WorkflowInstanceItem,
|
||||
WorkflowInstanceQueryParams
|
||||
>({
|
||||
initialParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
keywords: "",
|
||||
},
|
||||
request: WorkflowAPI.instance.getPage,
|
||||
onBeforeReset: () => queryFormRef.value?.resetFields(),
|
||||
});
|
||||
|
||||
/** 暴露刷新能力:审批中心页签切换时父组件调用 */
|
||||
defineExpose({ fetchData });
|
||||
|
||||
/** 实例详情抽屉 */
|
||||
const detailDrawerRef = ref<InstanceType<typeof InstanceDetailDrawer>>();
|
||||
|
||||
/**
|
||||
* 打开实例详情抽屉
|
||||
*
|
||||
* @param instanceId 流程实例 ID
|
||||
*/
|
||||
function openDetail(instanceId: string): void {
|
||||
detailDrawerRef.value?.open(instanceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销运行中的流程(仅发起人)
|
||||
*
|
||||
* @param instanceId 流程实例 ID
|
||||
*/
|
||||
async function handleCancel(instanceId: string): Promise<void> {
|
||||
try {
|
||||
await ElMessageBox.confirm("撤销后流程终止且不可恢复,确定撤销?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
});
|
||||
} catch {
|
||||
ElMessage.info("已取消撤销");
|
||||
return;
|
||||
}
|
||||
await WorkflowAPI.instance.cancel(instanceId);
|
||||
ElMessage.success("撤销成功");
|
||||
fetchData();
|
||||
}
|
||||
|
||||
/**
|
||||
* 终止运行中的流程(管理员)
|
||||
*
|
||||
* @param instanceId 流程实例 ID
|
||||
*/
|
||||
async function handleTerminate(instanceId: string): Promise<void> {
|
||||
let reason: string;
|
||||
try {
|
||||
const { value } = await ElMessageBox.prompt("请输入终止原因", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
inputPlaceholder: "终止原因将记录在流程历史中",
|
||||
inputValidator: (input: string) => !!input?.trim() || "请输入终止原因",
|
||||
type: "warning",
|
||||
});
|
||||
reason = value.trim();
|
||||
} catch {
|
||||
ElMessage.info("已取消终止");
|
||||
return;
|
||||
}
|
||||
await WorkflowAPI.instance.terminate(instanceId, reason);
|
||||
ElMessage.success("终止成功");
|
||||
fetchData();
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleQuery();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-card class="page-search" shadow="never">
|
||||
<el-form ref="queryFormRef" :model="params" :inline="true">
|
||||
<el-form-item label="关键字" prop="keywords">
|
||||
<el-input
|
||||
v-model="params.keywords"
|
||||
placeholder="任务名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleQuery">搜索</el-button>
|
||||
<el-button @click="handleResetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card ref="tableWrapperRef" class="page-content" shadow="never">
|
||||
<div class="page-toolbar">
|
||||
<div class="page-toolbar__left" />
|
||||
<div class="page-toolbar__right">
|
||||
<el-tooltip content="刷新" placement="top">
|
||||
<el-button class="page-icon-btn" @click="fetchData">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="全屏" placement="top">
|
||||
<el-button class="page-icon-btn" @click="toggleFullscreen">
|
||||
<el-icon><FullScreen /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-table-wrapper">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
highlight-current-row
|
||||
:data="list"
|
||||
class="page-table"
|
||||
border
|
||||
height="100%"
|
||||
>
|
||||
<!-- 流程名称为主列:各流程首节点名相近(部门主管审批),流程名称才是区分项 -->
|
||||
<el-table-column
|
||||
label="流程名称"
|
||||
prop="processName"
|
||||
min-width="140"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="任务名称" prop="name" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column
|
||||
label="流程实例ID"
|
||||
prop="processInstanceId"
|
||||
min-width="200"
|
||||
show-overflow-tooltip
|
||||
/>
|
||||
<el-table-column label="创建时间" prop="createTime" width="170" align="center" />
|
||||
<el-table-column fixed="right" label="操作" align="center" width="100">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
size="small"
|
||||
@click.stop="openApprove((scope.row as TodoTaskItem).id)"
|
||||
>
|
||||
办理
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<pagination
|
||||
v-if="total > 0"
|
||||
v-model:total="total"
|
||||
v-model:page="params.pageNum"
|
||||
v-model:limit="params.pageSize"
|
||||
@pagination="fetchData"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 审批办理弹窗:发起表单只读回显 + 审批记录 + 通过/驳回 -->
|
||||
<el-dialog v-model="approveState.visible" :title="approveState.title" width="760px">
|
||||
<el-skeleton v-if="approveState.loading" :rows="6" animated />
|
||||
<template v-else-if="detail">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="流程名称">{{ detail.processName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="当前任务">{{ detail.taskName }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<el-tabs class="todo-approve__tabs">
|
||||
<el-tab-pane label="申请信息">
|
||||
<FormDetail
|
||||
v-if="detail.formJson"
|
||||
:form-json="detail.formJson"
|
||||
:options-json="detail.optionsJson"
|
||||
:data-json="detail.dataJson"
|
||||
/>
|
||||
<el-empty v-else description="该流程未绑定表单" :image-size="64" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="流程走向">
|
||||
<ProcessStages
|
||||
v-if="detail.stages?.length"
|
||||
:stages="detail.stages"
|
||||
:active="currentStageIndex"
|
||||
/>
|
||||
<el-empty v-else description="无可展示的流程走向" :image-size="64" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="审批记录">
|
||||
<ApprovalTimeline :history="detail.history" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="审批意见">
|
||||
<el-input v-model="comment" type="textarea" :rows="2" placeholder="请输入审批意见" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 驳回模式:展开目标节点选择 -->
|
||||
<el-form-item v-if="rejectMode" label="驳回节点">
|
||||
<el-select v-model="rejectTargetId" placeholder="请选择驳回到的节点" class="!w-full">
|
||||
<el-option
|
||||
v-for="target in rejectTargets"
|
||||
:key="target.activityId"
|
||||
:label="target.activityName"
|
||||
:value="target.activityId"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<template v-if="!rejectMode">
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
:disabled="approveState.loading"
|
||||
@click="handleComplete"
|
||||
>
|
||||
通 过
|
||||
</el-button>
|
||||
<el-button type="warning" :disabled="approveState.loading" @click="handleRejectClick">
|
||||
驳 回
|
||||
</el-button>
|
||||
<el-button @click="approveState.visible = false">取 消</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button type="warning" :loading="submitting" @click="handleReject">
|
||||
确认驳回
|
||||
</el-button>
|
||||
<el-button @click="closeRejectMode">返 回</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useFullscreen } from "@vueuse/core";
|
||||
import { ElMessage, type FormInstance } from "element-plus";
|
||||
import { Refresh } from "@element-plus/icons-vue";
|
||||
|
||||
import WorkflowAPI from "@/api/workflow";
|
||||
import type {
|
||||
RejectTargetItem,
|
||||
TaskDetailData,
|
||||
TodoTaskItem,
|
||||
WorkflowTaskQueryParams,
|
||||
} from "@/api/workflow";
|
||||
import { usePageTable } from "@/composables";
|
||||
import ApprovalTimeline from "../../components/ApprovalTimeline.vue";
|
||||
import FormDetail from "../../components/FormDetail.vue";
|
||||
import ProcessStages from "../../components/ProcessStages.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "WorkflowTodoTab",
|
||||
});
|
||||
|
||||
/** 待办数量回传(驱动审批中心页签角标) */
|
||||
const emit = defineEmits<{
|
||||
totalChange: [total: number];
|
||||
}>();
|
||||
|
||||
const tableWrapperRef = ref<HTMLElement | null>(null);
|
||||
const { toggle: toggleFullscreen } = useFullscreen(tableWrapperRef);
|
||||
|
||||
const queryFormRef = ref<FormInstance>();
|
||||
|
||||
/** 分页表格数据管理 */
|
||||
const { loading, list, total, params, fetchData, handleQuery, handleResetQuery } = usePageTable<
|
||||
TodoTaskItem,
|
||||
WorkflowTaskQueryParams
|
||||
>({
|
||||
initialParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
keywords: "",
|
||||
},
|
||||
request: WorkflowAPI.task.getTodoPage,
|
||||
onBeforeReset: () => queryFormRef.value?.resetFields(),
|
||||
});
|
||||
|
||||
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("");
|
||||
|
||||
/** 当前任务在流程走向中的位置(节点ID精确匹配,当前环节高亮进行中,之前为已完成;未匹配时不高亮) */
|
||||
const currentStageIndex = computed(() => {
|
||||
const stages = detail.value?.stages;
|
||||
if (!stages?.length || !detail.value?.taskDefinitionKey) {
|
||||
return 0;
|
||||
}
|
||||
return stages.findIndex((stage) => stage.nodeId === detail.value?.taskDefinitionKey);
|
||||
});
|
||||
|
||||
/** 驳回模式状态 */
|
||||
const rejectMode = ref(false);
|
||||
const rejectTargets = ref<RejectTargetItem[]>([]);
|
||||
const rejectTargetId = ref("");
|
||||
|
||||
const submitting = ref(false);
|
||||
|
||||
/**
|
||||
* 打开办理弹窗并加载任务详情
|
||||
*
|
||||
* @param taskId 任务 ID
|
||||
*/
|
||||
async function openApprove(taskId: string): Promise<void> {
|
||||
approveState.visible = true;
|
||||
approveState.loading = true;
|
||||
closeRejectMode();
|
||||
comment.value = "";
|
||||
try {
|
||||
detail.value = await WorkflowAPI.task.getDetail(taskId);
|
||||
approveState.title = `【${detail.value.taskName}】审批办理`;
|
||||
} finally {
|
||||
approveState.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切入驳回模式并加载可驳回的历史节点
|
||||
*/
|
||||
async function handleRejectClick(): Promise<void> {
|
||||
rejectTargets.value = await WorkflowAPI.task.listRejectTargets(detail.value!.taskId);
|
||||
if (rejectTargets.value.length === 0) {
|
||||
ElMessage.warning("当前任务无可驳回的历史节点");
|
||||
return;
|
||||
}
|
||||
rejectTargetId.value = rejectTargets.value[0].activityId;
|
||||
rejectMode.value = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出驳回模式(保留已填意见)
|
||||
*/
|
||||
function closeRejectMode(): void {
|
||||
rejectMode.value = false;
|
||||
rejectTargets.value = [];
|
||||
rejectTargetId.value = "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 审批通过
|
||||
*/
|
||||
async function handleComplete(): Promise<void> {
|
||||
submitting.value = true;
|
||||
try {
|
||||
await WorkflowAPI.task.complete(detail.value!.taskId, { comment: comment.value });
|
||||
ElMessage.success("审批通过");
|
||||
approveState.visible = false;
|
||||
fetchData();
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 驳回到指定历史节点
|
||||
*/
|
||||
async function handleReject(): Promise<void> {
|
||||
submitting.value = true;
|
||||
try {
|
||||
await WorkflowAPI.task.reject(detail.value!.taskId, {
|
||||
targetActivityId: rejectTargetId.value,
|
||||
comment: comment.value,
|
||||
});
|
||||
ElMessage.success("驳回成功");
|
||||
approveState.visible = false;
|
||||
fetchData();
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleQuery();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.todo-approve {
|
||||
&__tabs {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<!-- 审批中心:待办/已办/我发起的为个人视角三类数据(钉钉审批中心模式),页签聚合免跳页 -->
|
||||
<!-- 切换页签即时刷新对应列表(办理/发起后切页签即见最新数据) -->
|
||||
<el-tabs v-model="activeTab" class="workflow-task__tabs" @tab-change="handleTabChange">
|
||||
<el-tab-pane name="todo">
|
||||
<template #label>
|
||||
<span class="workflow-task__label">
|
||||
待办
|
||||
<el-badge v-if="todoTotal > 0" :value="todoTotal" :max="99" />
|
||||
</span>
|
||||
</template>
|
||||
<TodoTab ref="todoTabRef" class="workflow-task__panel" @total-change="todoTotal = $event" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane name="done" label="已办" lazy>
|
||||
<DoneTab ref="doneTabRef" class="workflow-task__panel" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane name="mine" label="我发起的" lazy>
|
||||
<MineTab ref="mineTabRef" class="workflow-task__panel" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import TodoTab from "./components/TodoTab.vue";
|
||||
import DoneTab from "./components/DoneTab.vue";
|
||||
import MineTab from "./components/MineTab.vue";
|
||||
|
||||
defineOptions({
|
||||
name: "WorkflowTask",
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
/** 当前页签;发起成功跳转携带 ?tab=mine 直达「我发起的」查看进度 */
|
||||
const activeTab = ref((route.query.tab as string) || "todo");
|
||||
|
||||
/** 待办数量(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 自行首查,此处跳过不重复请求;
|
||||
* 再次切换时调用子组件刷新,保证办理/发起后切回页签即见最新数据
|
||||
*/
|
||||
function handleTabChange(name: string | number): void {
|
||||
if (name === "todo") {
|
||||
todoTabRef.value?.fetchData();
|
||||
} else if (name === "done") {
|
||||
doneTabRef.value?.fetchData();
|
||||
} else if (name === "mine") {
|
||||
mineTabRef.value?.fetchData();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.workflow-task {
|
||||
/* 页签栏卡片化,与页面搜索/内容卡片视觉一致 */
|
||||
&__tabs {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
|
||||
:deep(.el-tabs__header) {
|
||||
padding: 0 16px;
|
||||
margin-bottom: var(--page-gap);
|
||||
background: var(--content-bg);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: var(--card-radius);
|
||||
box-shadow: var(--card-shadow);
|
||||
}
|
||||
|
||||
:deep(.el-tabs__nav-wrap::after) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tab-pane) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 页签面板:搜索卡片 + 内容卡片的纵向布局(类注入子组件根节点) */
|
||||
&__panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--page-gap);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user