feat: PC 审批 flowCode 筛选、通用详情与价格表查看页(#46)
This commit is contained in:
@@ -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>
|
||||
@@ -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: '',
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user