feat: PC 审批 flowCode 筛选、通用详情与价格表查看页(#46)

This commit is contained in:
weicw
2026-10-31 07:24:39 +08:00
parent 76298a145f
commit 7ae4319102
14 changed files with 1190 additions and 57 deletions
@@ -0,0 +1,319 @@
<!-- 审批单据业务内容通用渲染:业务字段、明细行、会签进度、改值历史 -->
<template>
<div class="approval-document">
<!-- 业务字段 -->
<template v-if="fields.length">
<a-descriptions bordered size="small" :column="2" title="单据字段">
<a-descriptions-item
v-for="field in fields"
:key="field.key"
:label="field.label || field.key"
:span="isLongText(field.value) ? 2 : 1"
>
<div v-if="isLongText(field.value)" class="approval-document-text">
{{ field.value }}
</div>
<template v-else>{{ displayValue(field.value) }}</template>
</a-descriptions-item>
</a-descriptions>
</template>
<!-- 明细行 -->
<template v-if="lineColumns.length">
<div class="approval-section-title">明细</div>
<a-table
size="small"
bordered
:columns="tableColumns"
:data-source="lines"
:pagination="false"
:scroll="{ x: 'max-content' }"
row-key="lineNo"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'lineNo'">
{{ record.lineNo }}
</template>
<template v-else>
{{ lineCellText(column, record) }}
</template>
</template>
</a-table>
<div v-if="lineSummaryText" class="approval-line-summary">
{{ lineSummaryText }}
</div>
</template>
<!-- 会签进度 -->
<template v-if="groupProgress.length">
<div class="approval-section-title">会签进度</div>
<div
v-for="group in groupProgress"
:key="(group.nodeKey || '') + '-' + (group.groupKey || '')"
class="approval-group"
>
<div class="approval-group-head">
<span class="approval-group-name">
{{ group.nodeName || '' }}
<template v-if="group.groupName">·{{ group.groupName }}</template>
</span>
<a-tag
:color="
group.total && group.signed === group.total ? 'green' : 'blue'
"
>
已签 {{ group.signed ?? 0 }}/{{ group.total ?? 0 }}
</a-tag>
</div>
<div class="approval-group-members">
<a-tag
v-for="member in group.members ?? []"
:key="member.userId"
:color="memberColor(member.status)"
>
{{ member.userName }}·{{ member.statusName || member.status }}
</a-tag>
<span
v-if="!(group.members && group.members.length)"
class="ele-text-secondary"
>
暂无成员
</span>
</div>
</div>
</template>
<!-- 改值历史 -->
<template v-if="fieldChanges.length">
<div class="approval-section-title">改值历史</div>
<a-table
size="small"
bordered
:columns="changeColumns"
:data-source="fieldChanges"
:pagination="false"
:scroll="{ x: 'max-content' }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'createTime'">
{{ formatApprovalTime(record.createTime) }}
</template>
<template v-else-if="column.key === 'value'">
{{ record.oldValue ?? '-' }} → {{ record.newValue ?? '-' }}
</template>
</template>
</a-table>
</template>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import type {
ApprovalDocument,
ApprovalFieldChange,
ApprovalLineColumn
} from '@/api/approval/instance/model';
import {
formatApprovalDate,
formatApprovalTime,
formatMoney
} from '../format';
// 表格列(ant-design-vue 根包未导出 ColumnType,这里用最小结构)
interface TableColumn {
key: string;
dataIndex?: string;
title: string;
width?: number;
align?: 'left' | 'right' | 'center';
}
const props = defineProps<{
// 业务内容
document?: ApprovalDocument | null;
// 改值历史
fieldChanges?: ApprovalFieldChange[];
}>();
const fields = computed(() => props.document?.fields ?? []);
const lines = computed(() => props.document?.lines ?? []);
const lineColumns = computed(() => props.document?.lineColumns ?? []);
const groupProgress = computed(() => props.document?.groupProgress ?? []);
const fieldChanges = computed(() => props.fieldChanges ?? []);
// 展示值:空值统一显示 -
const displayValue = (value?: string | null) =>
value === undefined || value === null || value === '' ? '-' : value;
// 长文本换行展示
const isLongText = (value?: string | null) =>
!!value && (String(value).length > 40 || String(value).includes('\n'));
// 明细列 → 表格列
const tableColumns = computed<TableColumn[]>(() => {
const columns: TableColumn[] = [
{
key: 'lineNo',
dataIndex: 'lineNo',
title: '行号',
width: 70,
align: 'center'
}
];
lineColumns.value.forEach((spec: ApprovalLineColumn) => {
columns.push({
key: spec.key,
dataIndex: spec.key,
title: spec.label || spec.key,
align: isNumeric(spec) ? 'right' : 'left'
});
});
return columns;
});
const isNumeric = (spec: ApprovalLineColumn) =>
spec.type === 'DECIMAL' || spec.type === 'INT' || !!spec.money;
// 明细单元格格式化
const lineCellText = (column: any, record: any) => {
const key = String(column?.key ?? '');
const value = record?.[key];
const spec = lineColumns.value.find((item) => item.key === key);
if (value === undefined || value === null || value === '') {
return '-';
}
if (!spec) {
return String(value);
}
if (spec.money) {
return formatMoney(value);
}
if (spec.type === 'DATE') {
return formatApprovalDate(value) || String(value);
}
if (spec.type === 'BOOL') {
return value === true || value === 'true' || value === 1 ? '是' : '否';
}
if (spec.type === 'ENUM') {
const option = (spec.options ?? []).find(
(item) => String(item.value) === String(value)
);
return option?.label || String(value);
}
if (spec.type === 'DECIMAL') {
return spec.scale && spec.scale > 0
? Number(value).toFixed(spec.scale)
: String(value);
}
return String(value);
};
// 明细汇总:共 N 行,合计 ¥X
const lineSummaryText = computed(() => {
const summary = props.document?.lineSummary;
if (!summary) {
return '';
}
const parts: string[] = [];
if (summary.lineCount !== undefined && summary.lineCount !== null) {
parts.push('共 ' + summary.lineCount + ' 行');
}
if (summary.amountTotal !== undefined && summary.amountTotal !== null) {
parts.push('合计 ¥' + formatMoney(summary.amountTotal));
}
return parts.join(',');
});
// 会签成员状态颜色
const memberColor = (status?: string) => {
if (status === 'DONE' || status === 'APPROVED') {
return 'green';
}
if (status === 'REJECTED') {
return 'red';
}
return 'default';
};
// 改值历史列
const changeColumns: TableColumn[] = [
{
key: 'fieldLabel',
dataIndex: 'fieldLabel',
title: '字段',
width: 140
},
{ key: 'value', title: '旧值 → 新值' },
{
key: 'operatorName',
dataIndex: 'operatorName',
title: '操作人',
width: 120,
align: 'center'
},
{
key: 'round',
dataIndex: 'round',
title: '轮次',
width: 80,
align: 'center'
},
{ key: 'createTime', title: '时间', width: 170, align: 'center' }
];
</script>
<script lang="ts">
export default {
name: 'ApprovalDocumentView'
};
</script>
<style lang="less" scoped>
.approval-section-title {
margin: 24px 0 16px;
font-size: 16px;
font-weight: 500;
}
.approval-document-text {
white-space: pre-wrap;
word-break: break-all;
}
.approval-line-summary {
margin-top: 8px;
text-align: right;
color: rgba(0, 0, 0, 0.65);
}
.approval-group {
margin-bottom: 12px;
&:last-child {
margin-bottom: 0;
}
}
.approval-group-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.approval-group-name {
font-weight: 500;
}
.approval-group-members {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
:deep(.ant-descriptions) {
margin-bottom: 16px;
}
</style>
+105
View File
@@ -0,0 +1,105 @@
import { toDateString } from 'ele-admin-pro';
/**
* 审批时间统一展示:兼容 ISO 字符串与时间戳
*/
export function formatApprovalTime(value?: string | number | null): string {
if (value === undefined || value === null || value === '') {
return '';
}
if (typeof value === 'number') {
return toDateString(value, 'yyyy-MM-dd HH:mm:ss');
}
return String(value).replace('T', ' ').slice(0, 19);
}
/**
* 日期统一展示(yyyy-MM-dd)
*/
export function formatApprovalDate(value?: string | number | null): string {
if (value === undefined || value === null || value === '') {
return '';
}
if (typeof value === 'number') {
return toDateString(value, 'yyyy-MM-dd');
}
return String(value).replace('T', ' ').slice(0, 10);
}
/**
* 附件大小展示
*/
export function formatFileSize(length?: number): string {
if (!length || length <= 0) {
return '0 KB';
}
if (length < 1024 * 1024) {
return (length / 1024).toFixed(1) + ' KB';
}
return (length / 1024 / 1024).toFixed(1) + ' MB';
}
/**
* 金额展示:保留两位小数,空值显示 -
*/
export function formatMoney(value?: number | string | null): string {
if (value === undefined || value === null || value === '') {
return '-';
}
const num = Number(value);
return isNaN(num) ? '-' : num.toFixed(2);
}
/**
* 单据状态标签颜色
*/
export function approvalStatusColor(status?: string): string {
if (status === 'PENDING' || status === 'DRAFT') {
return 'blue';
}
if (status === 'REJECTED') {
return 'red';
}
if (status === 'WITHDRAWN') {
return 'orange';
}
if (status === 'APPROVED') {
return 'green';
}
return 'default';
}
/**
* 流程类型(flowCode)显示名。
*
* 后端目前没有暴露 flowCode 字典接口,选项在此静态维护;
* 新增单据类型时需同步本文件,后端提供字典接口后可改为远程加载。
*/
export const APPROVAL_FLOW_NAMES: Record<string, string> = {
completion: '业务完成确认单',
coordination_internal: '内部协调单',
coordination_external: '外部协调单',
settlement: '结算单',
receipt: '收据申请单',
vehicle: '车辆免收费登记表',
venue: '场地使用申请表',
coupon: '体验券领用申请单',
facility: '设施损坏修复与租用价格表'
};
/**
* 流程类型下拉选项
*/
export const APPROVAL_FLOW_OPTIONS = Object.keys(APPROVAL_FLOW_NAMES).map(
(value) => ({ label: APPROVAL_FLOW_NAMES[value], value })
);
/**
* 按 flowCode 取流程显示名;未知编码原样返回(降级为 docTypeName)
*/
export function approvalFlowName(flowCode?: string): string {
if (!flowCode) {
return '';
}
return APPROVAL_FLOW_NAMES[flowCode] || flowCode;
}
@@ -55,7 +55,7 @@
<div class="ele-text-break">{{ data.serviceContent || '-' }}</div>
</a-descriptions-item>
</template>
<template v-else>
<template v-else-if="data.docType === 'coordination'">
<a-descriptions-item label="标题" :span="2">
{{ data.title || '-' }}
</a-descriptions-item>
@@ -80,6 +80,13 @@
</a-descriptions-item>
</a-descriptions>
<!-- 二期通用业务内容:业务字段、明细、会签进度、改值历史 -->
<document-view
v-if="hasDocument"
:document="data.document"
:field-changes="data.fieldChanges"
/>
<a-divider orientation="left">附件</a-divider>
<div v-if="data.attachments && data.attachments.length">
<div
@@ -159,11 +166,12 @@
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import { toDateString } from 'ele-admin-pro/es';
import { getInstance } from '@/api/approval/instance';
import type { ApprovalInstance } from '@/api/approval/instance/model';
import DocumentView from '../../components/document-view.vue';
const emit = defineEmits<{
(e: 'update:visible', visible: boolean): void;
@@ -181,6 +189,20 @@
// 加载状态
const loading = ref(false);
// 是否有二期通用业务内容可渲染
const hasDocument = computed(() => {
const document = data.value?.document;
if (!document) {
return false;
}
return Boolean(
document.fields?.length ||
document.lineColumns?.length ||
document.groupProgress?.length ||
data.value?.fieldChanges?.length
);
});
// 状态颜色
const statusColor = (status?: string) => {
switch (status) {
@@ -12,7 +12,16 @@
<a-col v-for="field in basicFields" :key="field.prop" v-bind="colSpan">
<a-form-item :label="field.label">
<a-select
v-if="field.prop === 'docType'"
v-if="field.prop === 'flowCode'"
v-model:value="form.flowCode"
placeholder="全部"
allow-clear
show-search
option-filter-prop="label"
:options="flowOptions"
/>
<a-select
v-else-if="field.prop === 'docType'"
v-model:value="form.docType"
placeholder="全部"
allow-clear
@@ -108,6 +117,7 @@
ApprovalQueryParam,
ApprovalUser
} from '@/api/approval/instance/model';
import { APPROVAL_FLOW_OPTIONS } from '../../format';
const emit = defineEmits<{
(e: 'search', where?: ApprovalQueryParam): void;
@@ -129,6 +139,7 @@
// 基础字段
const basicFields: {
prop:
| 'flowCode'
| 'docType'
| 'bizNo'
| 'keywords'
@@ -138,6 +149,7 @@
| 'status';
label: string;
}[] = [
{ prop: 'flowCode', label: '流程类型' },
{ prop: 'docType', label: '单据类型' },
{ prop: 'bizNo', label: '单号' },
{ prop: 'keywords', label: '标题或名称' },
@@ -147,6 +159,9 @@
{ prop: 'status', label: '状态' }
];
// 流程类型选项(后端暂未提供字典接口,见 format.ts)
const flowOptions = APPROVAL_FLOW_OPTIONS;
// 单据类型选项
const docTypeOptions = [
{ label: '业务完成确认单', value: 'completion' },
@@ -165,6 +180,7 @@
// 表单数据
const { form, resetFields } = useFormData<ApprovalQueryParam>({
flowCode: undefined,
docType: undefined,
bizNo: '',
keywords: '',
+18 -1
View File
@@ -30,7 +30,10 @@
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'docType'">
<template v-if="column.key === 'flowCode'">
{{ flowName(record) }}
</template>
<template v-else-if="column.key === 'docType'">
<a-tag :color="record.docType === 'completion' ? 'blue' : 'purple'">
{{ record.docTypeName }}
</a-tag>
@@ -74,6 +77,7 @@
ApprovalItem,
ApprovalQueryParam
} from '@/api/approval/instance/model';
import { approvalFlowName } from '../format';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
@@ -106,6 +110,13 @@
ellipsis: true,
customRender: ({ text }) => text || '-'
},
{
title: '流程类型',
key: 'flowCode',
dataIndex: 'flowCode',
width: 180,
ellipsis: true
},
{
title: '单据类型',
key: 'docType',
@@ -174,6 +185,12 @@
}
]);
// 流程类型显示名:优先 flowCode,降级为单据类型名
const flowName = (row: ApprovalItem) =>
row.flowCode
? approvalFlowName(row.flowCode)
: row.docTypeName || row.docType || '-';
// 状态颜色
const statusColor = (status?: string) => {
switch (status) {
@@ -0,0 +1,154 @@
<!-- 价格表某一版本的明细(只读) -->
<template>
<div class="price-lines">
<a-empty
v-if="!lines.length"
:image="Empty.PRESENTED_IMAGE_SIMPLE"
description="该版本没有明细行"
/>
<template v-else>
<a-table
:columns="columns"
:data-source="lines"
:pagination="false"
size="small"
bordered
:scroll="{ x: 'max-content' }"
row-key="lineNo"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'lineNo'">
{{ record.lineNo }}
</template>
<template v-else>
{{ cellText(column, record) }}
</template>
</template>
</a-table>
<div v-if="summaryText" class="price-summary">
{{ summaryText }}
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { Empty } from 'ant-design-vue';
import type { ApprovalPriceVersion } from '@/api/approval/price/model';
import type { ApprovalLineColumn } from '@/api/approval/instance/model';
import { formatApprovalDate, formatMoney } from '../../format';
// 表格列(ant-design-vue 根包未导出 ColumnType,这里用最小结构)
interface TableColumn {
key: string;
dataIndex?: string;
title: string;
width?: number;
align?: 'left' | 'right' | 'center';
}
const props = defineProps<{
// 版本数据
version: ApprovalPriceVersion;
}>();
const lines = computed(() => props.version?.lines ?? []);
const lineColumns = computed(() => props.version?.lineColumns ?? []);
const isNumeric = (spec: ApprovalLineColumn) =>
spec.type === 'DECIMAL' || spec.type === 'INT' || !!spec.money;
// 明细列 → 表格列;无列元数据时退化为「金额」单列
const columns = computed<TableColumn[]>(() => {
const list: TableColumn[] = [
{
key: 'lineNo',
dataIndex: 'lineNo',
title: '行号',
width: 70,
align: 'center'
}
];
if (!lineColumns.value.length) {
list.push({ key: 'itemName', dataIndex: 'itemName', title: '名称' });
list.push({
key: 'amount',
dataIndex: 'amount',
title: '金额',
align: 'right'
});
return list;
}
lineColumns.value.forEach((spec) => {
list.push({
key: String(spec.key),
dataIndex: spec.key,
title: spec.label || spec.key,
align: isNumeric(spec) ? 'right' : 'left'
});
});
return list;
});
// 单元格展示
const cellText = (column: any, record: any) => {
const key = String(column?.key ?? '');
const value = record?.[key];
const spec = lineColumns.value.find((item) => item.key === key);
if (value === undefined || value === null || value === '') {
return '-';
}
if (!spec) {
return String(value);
}
if (spec.money) {
return formatMoney(value);
}
if (spec.type === 'DATE') {
return formatApprovalDate(value) || String(value);
}
if (spec.type === 'BOOL') {
return value === true || value === 'true' || value === 1 ? '是' : '否';
}
if (spec.type === 'ENUM') {
const option = (spec.options ?? []).find(
(item) => String(item.value) === String(value)
);
return option?.label || String(value);
}
if (spec.type === 'DECIMAL') {
return spec.scale && spec.scale > 0
? Number(value).toFixed(spec.scale)
: String(value);
}
return String(value);
};
// 共 N 行,合计 ¥X
const summaryText = computed(() => {
const parts: string[] = [];
if (lines.value.length) {
parts.push('共 ' + lines.value.length + ' 行');
}
const total = props.version?.amountTotal;
if (total !== undefined && total !== null) {
parts.push('合计 ¥' + formatMoney(total));
}
return parts.join(',');
});
</script>
<script lang="ts">
export default {
name: 'ApprovalPriceLines'
};
</script>
<style lang="less" scoped>
.price-summary {
margin-top: 8px;
text-align: right;
color: rgba(0, 0, 0, 0.65);
}
</style>
+175
View File
@@ -0,0 +1,175 @@
<!-- 审批管理 - 设施价格表查看(只读) -->
<template>
<div class="ele-body">
<a-card :bordered="false" title="当前生效版本">
<a-spin :spinning="loading">
<template v-if="current">
<a-descriptions size="small" :column="3" bordered>
<a-descriptions-item label="生效日期">
{{ formatApprovalDate(current.effectiveDate) || '-' }}
</a-descriptions-item>
<a-descriptions-item label="单号">
{{ current.bizNo || '-' }}
</a-descriptions-item>
<a-descriptions-item label="归档时间">
{{ formatApprovalTime(current.archiveTime) || '-' }}
</a-descriptions-item>
</a-descriptions>
<price-lines :version="current" />
</template>
<a-empty
v-else-if="!loading"
:image="Empty.PRESENTED_IMAGE_SIMPLE"
description="暂无当前生效的设施价格表"
/>
</a-spin>
</a-card>
<a-card
:bordered="false"
title="历史版本"
style="margin-top: 16px"
class="price-history-card"
>
<a-spin :spinning="loading">
<a-empty
v-if="!loading && !history.length"
:image="Empty.PRESENTED_IMAGE_SIMPLE"
description="暂无历史版本"
/>
<template v-else>
<a-table
:data-source="history"
:columns="historyColumns"
:pagination="false"
size="small"
row-key="instanceId"
:expanded-row-keys="expandedKeys"
@expand="onExpand"
>
<template #expandedRowRender="{ record }">
<price-lines :version="record" />
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'effectiveDate'">
{{ formatApprovalDate(record.effectiveDate) || '-' }}
<a-tag
v-if="record.instanceId === current?.instanceId"
color="green"
style="margin-left: 6px"
>
当前生效
</a-tag>
</template>
<template v-else-if="column.key === 'archiveTime'">
{{ formatApprovalTime(record.archiveTime) || '-' }}
</template>
<template v-else-if="column.key === 'lineCount'">
{{ record.lines?.length ?? 0 }}
</template>
<template v-else-if="column.key === 'amountTotal'">
{{ formatMoney(record.amountTotal) }}
</template>
</template>
</a-table>
</template>
</a-spin>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { Empty, message } from 'ant-design-vue';
import { getApprovalPrice } from '@/api/approval/price';
import type {
ApprovalPriceVersion,
ApprovalPriceView
} from '@/api/approval/price/model';
import {
formatApprovalDate,
formatApprovalTime,
formatMoney
} from '../format';
import PriceLines from './components/price-lines.vue';
// 页面数据
const data = ref<ApprovalPriceView | null>(null);
// 加载状态
const loading = ref(false);
// 历史版本展开行
const expandedKeys = ref<number[]>([]);
const current = computed(() => data.value?.current ?? null);
const history = computed(() => data.value?.history ?? []);
// 历史版本表格列
const historyColumns = [
{
key: 'effectiveDate',
dataIndex: 'effectiveDate',
title: '生效日期',
width: 180
},
{ key: 'bizNo', dataIndex: 'bizNo', title: '单号', width: 180 },
{
key: 'reportOrgName',
dataIndex: 'reportOrgName',
title: '报送部门',
ellipsis: true
},
{
key: 'lineCount',
title: '明细行数',
width: 110,
align: 'right' as const
},
{
key: 'amountTotal',
title: '合计金额',
width: 140,
align: 'right' as const
},
{ key: 'archiveTime', title: '归档时间', width: 180 }
];
// 历史版本展开/收起(同一时间只展开一版,避免长页面)
const onExpand = (expanded: boolean, record: ApprovalPriceVersion) => {
if (expanded) {
expandedKeys.value = record.instanceId ? [record.instanceId] : [];
} else {
expandedKeys.value = [];
}
};
const load = () => {
loading.value = true;
getApprovalPrice()
.then((result) => {
data.value = result;
})
.catch((e: Error) => {
data.value = null;
message.error(e.message);
})
.finally(() => {
loading.value = false;
});
};
onMounted(load);
</script>
<script lang="ts">
export default {
name: 'ApprovalPrice'
};
</script>
<style lang="less" scoped>
.price-history-card {
:deep(.ant-table-expanded-row) {
padding: 8px 16px;
}
}
</style>
@@ -21,6 +21,14 @@
>
通过
</a-button>
<a-button
v-if="writableFields.length"
v-permission="'approval:instance:approve'"
:loading="submitting"
@click="backfill"
>
回填
</a-button>
<a-button
v-if="detail.permissions?.canDispatch"
v-permission="'approval:instance:approve'"
@@ -66,6 +74,39 @@
</a-tag>
</div>
<!-- 可回填字段:紧跟在通过/回填按钮下方 -->
<div v-if="writableFields.length" class="approval-backfill">
<div class="approval-section-title">可回填字段</div>
<a-form layout="vertical">
<a-row :gutter="16">
<a-col
v-for="field in writableFields"
:key="field.field"
:xs="24"
:sm="12"
>
<a-form-item
:label="field.label || field.field"
:required="field.required"
>
<a-input
v-model:value="writableValues[field.field]"
:placeholder="
field.required
? '请输入' + (field.label || field.field)
: '可选'
"
allow-clear
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
<div class="ele-text-secondary">
「回填」只保存字段、不推进节点;「通过」会在推进的同时保存字段。
</div>
</div>
<a-descriptions bordered size="small" :column="2" title="基本信息">
<a-descriptions-item label="单号">
{{ detail.bizNo }}
@@ -144,6 +185,13 @@
</a-descriptions-item>
</a-descriptions>
<!-- 二期通用业务内容:业务字段、明细、会签进度、改值历史 -->
<document-view
v-if="hasDocument"
:document="detail.document"
:field-changes="detail.fieldChanges"
/>
<a-descriptions bordered size="small" :column="2" title="附件">
<a-descriptions-item label="附件" :span="2">
<a-empty
@@ -326,6 +374,7 @@
approveApprovalTask,
archiveApprovalTask,
assignApprovalTask,
backfillApprovalTask,
confirmApprovalTask,
dispatchApprovalTask,
listDispatchCandidates,
@@ -335,7 +384,8 @@
approvalStatusColor,
formatApprovalTime,
formatFileSize
} from '../format';
} from '../../format';
import DocumentView from '../../components/document-view.vue';
const props = defineProps<{
// 抽屉是否打开
@@ -365,6 +415,37 @@
const pendingTasks = computed<ApprovalPendingTask[]>(
() => detail.value?.pendingTasks ?? []
);
// 当前用户可回填字段(过滤掉无 key 的脏数据,便于模板直接索引)
const writableFields = computed(() =>
(detail.value?.permissions?.writableFields ?? [])
.filter((field) => !!field.field)
.map((field) => ({ ...field, field: field.field as string }))
);
// 是否有二期通用业务内容可渲染
const hasDocument = computed(() => {
const document = detail.value?.document;
if (!document) {
return false;
}
return Boolean(
document.fields?.length ||
document.lineColumns?.length ||
document.groupProgress?.length ||
detail.value?.fieldChanges?.length
);
});
// 回填字段输入值(字段 key → 值)
const writableValues = reactive<Record<string, string>>({});
// 用详情里的当前值初始化回填输入框
const resetWritableValues = () => {
Object.keys(writableValues).forEach((key) => {
delete writableValues[key];
});
writableFields.value.forEach((field) => {
writableValues[field.field] = field.value ?? '';
});
};
const load = async () => {
if (!props.instanceId) {
@@ -374,6 +455,7 @@
loading.value = true;
try {
detail.value = await getInstance(props.instanceId);
resetWritableValues();
} catch (e: any) {
detail.value = null;
message.error(e.message);
@@ -420,6 +502,53 @@
opinionVisible.value = true;
};
/* 回填字段收集与校验 */
const collectWritableFields = (): Record<string, string> | undefined => {
if (!writableFields.value.length) {
return undefined;
}
const fields: Record<string, string> = {};
for (const field of writableFields.value) {
const value = (writableValues[field.field] ?? '').trim();
if (field.required && !value) {
message.error('请填写' + (field.label || field.field));
return undefined;
}
if (value) {
fields[field.field] = value;
}
}
return fields;
};
/* 回填:只写字段,不推进节点 */
const backfill = async () => {
const taskId = detail.value?.myTask?.id;
if (!taskId) {
message.error('待办任务不存在,请刷新');
return;
}
const fields = collectWritableFields();
if (!fields) {
return;
}
if (!Object.keys(fields).length) {
message.error('请至少填写一个字段');
return;
}
submitting.value = true;
try {
await backfillApprovalTask(taskId, fields);
message.success('回填成功');
await load();
emit('done');
} catch (e: any) {
message.error(e.message);
} finally {
submitting.value = false;
}
};
const submitOpinion = async () => {
const taskId = detail.value?.myTask?.id;
if (!taskId) {
@@ -431,10 +560,21 @@
message.error('请填写驳回意见');
return;
}
let fields: Record<string, string> | undefined;
if (opinionMode.value === 'approve') {
fields = collectWritableFields();
if (!fields && writableFields.value.length) {
return;
}
}
submitting.value = true;
try {
if (opinionMode.value === 'approve') {
const msg = await approveApprovalTask(taskId, text || undefined);
const msg = await approveApprovalTask(
taskId,
text || undefined,
fields
);
if (msg && msg !== '审批成功') {
message.warning(msg);
} else {
@@ -668,6 +808,13 @@
margin-bottom: 16px;
}
.approval-backfill {
margin-bottom: 16px;
padding: 12px 16px;
background: #fafafa;
border-radius: 4px;
}
.approval-section-title {
margin: 24px 0 16px;
font-size: 16px;
-46
View File
@@ -1,46 +0,0 @@
import { toDateString } from 'ele-admin-pro';
/**
* 审批时间统一展示:兼容 ISO 字符串与时间戳
*/
export function formatApprovalTime(value?: string | number | null): string {
if (value === undefined || value === null || value === '') {
return '';
}
if (typeof value === 'number') {
return toDateString(value, 'yyyy-MM-dd HH:mm:ss');
}
return String(value).replace('T', ' ').slice(0, 19);
}
/**
* 附件大小展示
*/
export function formatFileSize(length?: number): string {
if (!length || length <= 0) {
return '0 KB';
}
if (length < 1024 * 1024) {
return (length / 1024).toFixed(1) + ' KB';
}
return (length / 1024 / 1024).toFixed(1) + ' MB';
}
/**
* 单据状态标签颜色
*/
export function approvalStatusColor(status?: string): string {
if (status === 'PENDING' || status === 'DRAFT') {
return 'blue';
}
if (status === 'REJECTED') {
return 'red';
}
if (status === 'WITHDRAWN') {
return 'orange';
}
if (status === 'APPROVED') {
return 'green';
}
return 'default';
}
+18 -1
View File
@@ -15,6 +15,14 @@
>
<template #toolbar>
<a-space :size="10" style="flex-wrap: wrap">
<a-select
v-model:value="where.flowCode"
allow-clear
placeholder="流程类型"
style="width: 200px"
:options="flowOptions"
@change="reload"
/>
<a-select
v-model:value="where.status"
allow-clear
@@ -81,7 +89,11 @@
ApprovalTodoParam
} from '@/api/approval/instance/model';
import { pageApprovalTodo } from '@/api/approval/instance';
import { approvalStatusColor, formatApprovalTime } from './format';
import {
APPROVAL_FLOW_OPTIONS,
approvalStatusColor,
formatApprovalTime
} from '../format';
import DetailDrawer from './components/detail-drawer.vue';
// 表格实例
@@ -96,10 +108,14 @@
// 搜索条件
const { where, resetFields } = useSearch<ApprovalTodoParam>({
flowCode: undefined,
status: undefined,
keywords: undefined
});
// 流程类型选项(后端暂未提供字典接口,见 format.ts)
const flowOptions = APPROVAL_FLOW_OPTIONS;
// 状态选项
const statusOptions = [
{ label: '审批中', value: 'PENDING' },
@@ -180,6 +196,7 @@
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit }) => {
return pageApprovalTodo({
flowCode: where.flowCode,
status: where.status,
keywords: where.keywords?.trim() || undefined,
scope: allScope.value ? 'all' : undefined,