feat: PC 审批 flowCode 筛选、通用详情与价格表查看页(#46)
This commit is contained in:
@@ -6,6 +6,8 @@ import type { PageParam } from '@/api';
|
|||||||
export interface ApprovalQueryParam extends PageParam {
|
export interface ApprovalQueryParam extends PageParam {
|
||||||
// 单据类型 completion/coordination
|
// 单据类型 completion/coordination
|
||||||
docType?: string;
|
docType?: string;
|
||||||
|
// 流程编码,如 coordination_external(按流程类型筛选)
|
||||||
|
flowCode?: string;
|
||||||
// 单号
|
// 单号
|
||||||
bizNo?: string;
|
bizNo?: string;
|
||||||
// 关键词:单号、名称或标题、实施单位
|
// 关键词:单号、名称或标题、实施单位
|
||||||
@@ -36,6 +38,8 @@ export interface ApprovalItem {
|
|||||||
bizNo?: string;
|
bizNo?: string;
|
||||||
docType?: string;
|
docType?: string;
|
||||||
docTypeName?: string;
|
docTypeName?: string;
|
||||||
|
// 流程编码
|
||||||
|
flowCode?: string;
|
||||||
status?: string;
|
status?: string;
|
||||||
statusName?: string;
|
statusName?: string;
|
||||||
// 名称或标题
|
// 名称或标题
|
||||||
@@ -157,7 +161,11 @@ export interface ApprovalInstance {
|
|||||||
confirms?: ApprovalConfirm[];
|
confirms?: ApprovalConfirm[];
|
||||||
myTask?: ApprovalMyTask | null;
|
myTask?: ApprovalMyTask | null;
|
||||||
pendingTasks?: ApprovalPendingTask[];
|
pendingTasks?: ApprovalPendingTask[];
|
||||||
permissions?: Record<string, boolean>;
|
// 业务内容(业务字段、明细、会签进度)
|
||||||
|
document?: ApprovalDocument | null;
|
||||||
|
// 字段改值历史
|
||||||
|
fieldChanges?: ApprovalFieldChange[];
|
||||||
|
permissions?: ApprovalPermissions;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -204,6 +212,8 @@ export interface ApprovalTodoParam extends PageParam {
|
|||||||
status?: string;
|
status?: string;
|
||||||
// 单据类型
|
// 单据类型
|
||||||
docType?: string;
|
docType?: string;
|
||||||
|
// 流程编码,如 settlement(按流程类型筛选)
|
||||||
|
flowCode?: string;
|
||||||
// 查询范围:mine(默认本人待办)/all(全租户待办,需 assign 权限)
|
// 查询范围:mine(默认本人待办)/all(全租户待办,需 assign 权限)
|
||||||
scope?: 'mine' | 'all';
|
scope?: 'mine' | 'all';
|
||||||
// 关键词:单号、名称或标题、实施单位
|
// 关键词:单号、名称或标题、实施单位
|
||||||
@@ -232,3 +242,126 @@ export interface ApprovalAssignResult {
|
|||||||
assigneeName?: string;
|
assigneeName?: string;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务字段(有序,后端已格式化展示值)
|
||||||
|
*/
|
||||||
|
export interface ApprovalField {
|
||||||
|
key?: string;
|
||||||
|
label?: string;
|
||||||
|
value?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 明细列元数据(类型 TEXT/DECIMAL/INT/DATE/BOOL/ENUM)
|
||||||
|
*/
|
||||||
|
export interface ApprovalLineColumn {
|
||||||
|
key?: string;
|
||||||
|
label?: string;
|
||||||
|
type?: string;
|
||||||
|
required?: boolean;
|
||||||
|
// 数值列小数位
|
||||||
|
scale?: number;
|
||||||
|
// 文本列最大长度
|
||||||
|
maxLength?: number;
|
||||||
|
// 是否金额列(服务端按数量 × 单价重算)
|
||||||
|
money?: boolean;
|
||||||
|
// ENUM 列可选值
|
||||||
|
options?: { value?: string; label?: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 明细行:以列 key 为键
|
||||||
|
*/
|
||||||
|
export type ApprovalLine = Record<string, any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 明细汇总
|
||||||
|
*/
|
||||||
|
export interface ApprovalLineSummary {
|
||||||
|
lineCount?: number;
|
||||||
|
amountTotal?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会签组成员
|
||||||
|
*/
|
||||||
|
export interface ApprovalGroupMember {
|
||||||
|
userId?: number;
|
||||||
|
userName?: string;
|
||||||
|
status?: string;
|
||||||
|
statusName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 会签节点进度(按组已签 N/M)
|
||||||
|
*/
|
||||||
|
export interface ApprovalGroupProgress {
|
||||||
|
nodeKey?: string;
|
||||||
|
nodeName?: string;
|
||||||
|
groupKey?: string;
|
||||||
|
groupName?: string;
|
||||||
|
groupSeq?: number;
|
||||||
|
signed?: number;
|
||||||
|
total?: number;
|
||||||
|
members?: ApprovalGroupMember[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 单据业务内容
|
||||||
|
*/
|
||||||
|
export interface ApprovalDocument {
|
||||||
|
// 业务字段(有序)
|
||||||
|
fields?: ApprovalField[];
|
||||||
|
// 明细行
|
||||||
|
lines?: ApprovalLine[];
|
||||||
|
// 明细列元数据;无明细为空
|
||||||
|
lineColumns?: ApprovalLineColumn[];
|
||||||
|
// 明细汇总;无明细为 null
|
||||||
|
lineSummary?: ApprovalLineSummary | null;
|
||||||
|
// 会签节点进度,非会签单据为空
|
||||||
|
groupProgress?: ApprovalGroupProgress[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字段改值历史
|
||||||
|
*/
|
||||||
|
export interface ApprovalFieldChange {
|
||||||
|
id?: number;
|
||||||
|
nodeKey?: string;
|
||||||
|
nodeName?: string;
|
||||||
|
fieldKey?: string;
|
||||||
|
fieldLabel?: string;
|
||||||
|
oldValue?: string | null;
|
||||||
|
newValue?: string | null;
|
||||||
|
operatorName?: string;
|
||||||
|
round?: number;
|
||||||
|
createTime?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前用户可回填字段
|
||||||
|
*/
|
||||||
|
export interface ApprovalWritableField {
|
||||||
|
field?: string;
|
||||||
|
label?: string;
|
||||||
|
required?: boolean;
|
||||||
|
value?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 详情动作权限
|
||||||
|
*/
|
||||||
|
export interface ApprovalPermissions {
|
||||||
|
canEdit?: boolean;
|
||||||
|
canSubmit?: boolean;
|
||||||
|
canWithdraw?: boolean;
|
||||||
|
canApprove?: boolean;
|
||||||
|
canReject?: boolean;
|
||||||
|
canConfirm?: boolean;
|
||||||
|
canArchive?: boolean;
|
||||||
|
canDispatch?: boolean;
|
||||||
|
canAssign?: boolean;
|
||||||
|
// 当前用户在该节点可回填的字段;非节点处理人为空
|
||||||
|
writableFields?: ApprovalWritableField[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import request from '@/utils/request';
|
||||||
|
import type { ApiResult } from '@/api';
|
||||||
|
import { THINK_API_URL } from '@/config/setting';
|
||||||
|
import type { ApprovalPriceView } from './model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设施价格表:当前生效版本 + 历史版本(只读,权限点 approval:price:view)
|
||||||
|
*/
|
||||||
|
export async function getApprovalPrice() {
|
||||||
|
const res = await request.get<ApiResult<ApprovalPriceView>>(
|
||||||
|
THINK_API_URL + '/approval/price'
|
||||||
|
);
|
||||||
|
if (res.data.code === 0 && res.data.data) {
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type {
|
||||||
|
ApprovalField,
|
||||||
|
ApprovalLine,
|
||||||
|
ApprovalLineColumn
|
||||||
|
} from '../../instance/model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 价格表版本:审批实例即版本(ADR 0012)
|
||||||
|
*/
|
||||||
|
export interface ApprovalPriceVersion {
|
||||||
|
instanceId?: number;
|
||||||
|
// 单号
|
||||||
|
bizNo?: string;
|
||||||
|
// 生效日期(yyyy-MM-dd)
|
||||||
|
effectiveDate?: string;
|
||||||
|
// 归档时间
|
||||||
|
archiveTime?: string;
|
||||||
|
// 报送部门名
|
||||||
|
reportOrgName?: string;
|
||||||
|
// 业务字段(含生效日期等)
|
||||||
|
fields?: ApprovalField[];
|
||||||
|
// 明细行
|
||||||
|
lines?: ApprovalLine[];
|
||||||
|
// 明细列元数据
|
||||||
|
lineColumns?: ApprovalLineColumn[];
|
||||||
|
// 合计金额
|
||||||
|
amountTotal?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 价格表查看页数据:current 为当前生效版本,history 为历史版本(按生效日期倒序)
|
||||||
|
*/
|
||||||
|
export interface ApprovalPriceView {
|
||||||
|
current?: ApprovalPriceVersion | null;
|
||||||
|
history?: ApprovalPriceVersion[];
|
||||||
|
}
|
||||||
@@ -8,12 +8,33 @@ import type {
|
|||||||
} from '../instance/model';
|
} from '../instance/model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通过任务
|
* 通过任务(fields 为当前节点可回填字段,无回填时省略)
|
||||||
*/
|
*/
|
||||||
export async function approveApprovalTask(id: number, opinion?: string) {
|
export async function approveApprovalTask(
|
||||||
|
id: number,
|
||||||
|
opinion?: string,
|
||||||
|
fields?: Record<string, string>
|
||||||
|
) {
|
||||||
const res = await request.post<ApiResult<unknown>>(
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
THINK_API_URL + '/approval/task/' + id + '/approve',
|
THINK_API_URL + '/approval/task/' + id + '/approve',
|
||||||
{ opinion }
|
{ opinion, fields }
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 回填(只写字段,不推进节点)
|
||||||
|
*/
|
||||||
|
export async function backfillApprovalTask(
|
||||||
|
id: number,
|
||||||
|
fields: Record<string, string>
|
||||||
|
) {
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
THINK_API_URL + '/approval/task/' + id + '/backfill',
|
||||||
|
{ fields }
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.message;
|
return res.data.message;
|
||||||
|
|||||||
@@ -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>
|
<div class="ele-text-break">{{ data.serviceContent || '-' }}</div>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else-if="data.docType === 'coordination'">
|
||||||
<a-descriptions-item label="标题" :span="2">
|
<a-descriptions-item label="标题" :span="2">
|
||||||
{{ data.title || '-' }}
|
{{ data.title || '-' }}
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
@@ -80,6 +80,13 @@
|
|||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
</a-descriptions>
|
</a-descriptions>
|
||||||
|
|
||||||
|
<!-- 二期通用业务内容:业务字段、明细、会签进度、改值历史 -->
|
||||||
|
<document-view
|
||||||
|
v-if="hasDocument"
|
||||||
|
:document="data.document"
|
||||||
|
:field-changes="data.fieldChanges"
|
||||||
|
/>
|
||||||
|
|
||||||
<a-divider orientation="left">附件</a-divider>
|
<a-divider orientation="left">附件</a-divider>
|
||||||
<div v-if="data.attachments && data.attachments.length">
|
<div v-if="data.attachments && data.attachments.length">
|
||||||
<div
|
<div
|
||||||
@@ -159,11 +166,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import { message } from 'ant-design-vue/es';
|
import { message } from 'ant-design-vue/es';
|
||||||
import { toDateString } from 'ele-admin-pro/es';
|
import { toDateString } from 'ele-admin-pro/es';
|
||||||
import { getInstance } from '@/api/approval/instance';
|
import { getInstance } from '@/api/approval/instance';
|
||||||
import type { ApprovalInstance } from '@/api/approval/instance/model';
|
import type { ApprovalInstance } from '@/api/approval/instance/model';
|
||||||
|
import DocumentView from '../../components/document-view.vue';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:visible', visible: boolean): void;
|
(e: 'update:visible', visible: boolean): void;
|
||||||
@@ -181,6 +189,20 @@
|
|||||||
// 加载状态
|
// 加载状态
|
||||||
const loading = ref(false);
|
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) => {
|
const statusColor = (status?: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|||||||
@@ -12,7 +12,16 @@
|
|||||||
<a-col v-for="field in basicFields" :key="field.prop" v-bind="colSpan">
|
<a-col v-for="field in basicFields" :key="field.prop" v-bind="colSpan">
|
||||||
<a-form-item :label="field.label">
|
<a-form-item :label="field.label">
|
||||||
<a-select
|
<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"
|
v-model:value="form.docType"
|
||||||
placeholder="全部"
|
placeholder="全部"
|
||||||
allow-clear
|
allow-clear
|
||||||
@@ -108,6 +117,7 @@
|
|||||||
ApprovalQueryParam,
|
ApprovalQueryParam,
|
||||||
ApprovalUser
|
ApprovalUser
|
||||||
} from '@/api/approval/instance/model';
|
} from '@/api/approval/instance/model';
|
||||||
|
import { APPROVAL_FLOW_OPTIONS } from '../../format';
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'search', where?: ApprovalQueryParam): void;
|
(e: 'search', where?: ApprovalQueryParam): void;
|
||||||
@@ -129,6 +139,7 @@
|
|||||||
// 基础字段
|
// 基础字段
|
||||||
const basicFields: {
|
const basicFields: {
|
||||||
prop:
|
prop:
|
||||||
|
| 'flowCode'
|
||||||
| 'docType'
|
| 'docType'
|
||||||
| 'bizNo'
|
| 'bizNo'
|
||||||
| 'keywords'
|
| 'keywords'
|
||||||
@@ -138,6 +149,7 @@
|
|||||||
| 'status';
|
| 'status';
|
||||||
label: string;
|
label: string;
|
||||||
}[] = [
|
}[] = [
|
||||||
|
{ prop: 'flowCode', label: '流程类型' },
|
||||||
{ prop: 'docType', label: '单据类型' },
|
{ prop: 'docType', label: '单据类型' },
|
||||||
{ prop: 'bizNo', label: '单号' },
|
{ prop: 'bizNo', label: '单号' },
|
||||||
{ prop: 'keywords', label: '标题或名称' },
|
{ prop: 'keywords', label: '标题或名称' },
|
||||||
@@ -147,6 +159,9 @@
|
|||||||
{ prop: 'status', label: '状态' }
|
{ prop: 'status', label: '状态' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 流程类型选项(后端暂未提供字典接口,见 format.ts)
|
||||||
|
const flowOptions = APPROVAL_FLOW_OPTIONS;
|
||||||
|
|
||||||
// 单据类型选项
|
// 单据类型选项
|
||||||
const docTypeOptions = [
|
const docTypeOptions = [
|
||||||
{ label: '业务完成确认单', value: 'completion' },
|
{ label: '业务完成确认单', value: 'completion' },
|
||||||
@@ -165,6 +180,7 @@
|
|||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const { form, resetFields } = useFormData<ApprovalQueryParam>({
|
const { form, resetFields } = useFormData<ApprovalQueryParam>({
|
||||||
|
flowCode: undefined,
|
||||||
docType: undefined,
|
docType: undefined,
|
||||||
bizNo: '',
|
bizNo: '',
|
||||||
keywords: '',
|
keywords: '',
|
||||||
|
|||||||
@@ -30,7 +30,10 @@
|
|||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
<template #bodyCell="{ column, record }">
|
<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'">
|
<a-tag :color="record.docType === 'completion' ? 'blue' : 'purple'">
|
||||||
{{ record.docTypeName }}
|
{{ record.docTypeName }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
@@ -74,6 +77,7 @@
|
|||||||
ApprovalItem,
|
ApprovalItem,
|
||||||
ApprovalQueryParam
|
ApprovalQueryParam
|
||||||
} from '@/api/approval/instance/model';
|
} from '@/api/approval/instance/model';
|
||||||
|
import { approvalFlowName } from '../format';
|
||||||
|
|
||||||
// 表格实例
|
// 表格实例
|
||||||
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
|
||||||
@@ -106,6 +110,13 @@
|
|||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
customRender: ({ text }) => text || '-'
|
customRender: ({ text }) => text || '-'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '流程类型',
|
||||||
|
key: 'flowCode',
|
||||||
|
dataIndex: 'flowCode',
|
||||||
|
width: 180,
|
||||||
|
ellipsis: true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '单据类型',
|
title: '单据类型',
|
||||||
key: 'docType',
|
key: 'docType',
|
||||||
@@ -174,6 +185,12 @@
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 流程类型显示名:优先 flowCode,降级为单据类型名
|
||||||
|
const flowName = (row: ApprovalItem) =>
|
||||||
|
row.flowCode
|
||||||
|
? approvalFlowName(row.flowCode)
|
||||||
|
: row.docTypeName || row.docType || '-';
|
||||||
|
|
||||||
// 状态颜色
|
// 状态颜色
|
||||||
const statusColor = (status?: string) => {
|
const statusColor = (status?: string) => {
|
||||||
switch (status) {
|
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>
|
||||||
|
<a-button
|
||||||
|
v-if="writableFields.length"
|
||||||
|
v-permission="'approval:instance:approve'"
|
||||||
|
:loading="submitting"
|
||||||
|
@click="backfill"
|
||||||
|
>
|
||||||
|
回填
|
||||||
|
</a-button>
|
||||||
<a-button
|
<a-button
|
||||||
v-if="detail.permissions?.canDispatch"
|
v-if="detail.permissions?.canDispatch"
|
||||||
v-permission="'approval:instance:approve'"
|
v-permission="'approval:instance:approve'"
|
||||||
@@ -66,6 +74,39 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</div>
|
</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 bordered size="small" :column="2" title="基本信息">
|
||||||
<a-descriptions-item label="单号">
|
<a-descriptions-item label="单号">
|
||||||
{{ detail.bizNo }}
|
{{ detail.bizNo }}
|
||||||
@@ -144,6 +185,13 @@
|
|||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
</a-descriptions>
|
</a-descriptions>
|
||||||
|
|
||||||
|
<!-- 二期通用业务内容:业务字段、明细、会签进度、改值历史 -->
|
||||||
|
<document-view
|
||||||
|
v-if="hasDocument"
|
||||||
|
:document="detail.document"
|
||||||
|
:field-changes="detail.fieldChanges"
|
||||||
|
/>
|
||||||
|
|
||||||
<a-descriptions bordered size="small" :column="2" title="附件">
|
<a-descriptions bordered size="small" :column="2" title="附件">
|
||||||
<a-descriptions-item label="附件" :span="2">
|
<a-descriptions-item label="附件" :span="2">
|
||||||
<a-empty
|
<a-empty
|
||||||
@@ -326,6 +374,7 @@
|
|||||||
approveApprovalTask,
|
approveApprovalTask,
|
||||||
archiveApprovalTask,
|
archiveApprovalTask,
|
||||||
assignApprovalTask,
|
assignApprovalTask,
|
||||||
|
backfillApprovalTask,
|
||||||
confirmApprovalTask,
|
confirmApprovalTask,
|
||||||
dispatchApprovalTask,
|
dispatchApprovalTask,
|
||||||
listDispatchCandidates,
|
listDispatchCandidates,
|
||||||
@@ -335,7 +384,8 @@
|
|||||||
approvalStatusColor,
|
approvalStatusColor,
|
||||||
formatApprovalTime,
|
formatApprovalTime,
|
||||||
formatFileSize
|
formatFileSize
|
||||||
} from '../format';
|
} from '../../format';
|
||||||
|
import DocumentView from '../../components/document-view.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
// 抽屉是否打开
|
// 抽屉是否打开
|
||||||
@@ -365,6 +415,37 @@
|
|||||||
const pendingTasks = computed<ApprovalPendingTask[]>(
|
const pendingTasks = computed<ApprovalPendingTask[]>(
|
||||||
() => detail.value?.pendingTasks ?? []
|
() => 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 () => {
|
const load = async () => {
|
||||||
if (!props.instanceId) {
|
if (!props.instanceId) {
|
||||||
@@ -374,6 +455,7 @@
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
detail.value = await getInstance(props.instanceId);
|
detail.value = await getInstance(props.instanceId);
|
||||||
|
resetWritableValues();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
detail.value = null;
|
detail.value = null;
|
||||||
message.error(e.message);
|
message.error(e.message);
|
||||||
@@ -420,6 +502,53 @@
|
|||||||
opinionVisible.value = true;
|
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 submitOpinion = async () => {
|
||||||
const taskId = detail.value?.myTask?.id;
|
const taskId = detail.value?.myTask?.id;
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
@@ -431,10 +560,21 @@
|
|||||||
message.error('请填写驳回意见');
|
message.error('请填写驳回意见');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let fields: Record<string, string> | undefined;
|
||||||
|
if (opinionMode.value === 'approve') {
|
||||||
|
fields = collectWritableFields();
|
||||||
|
if (!fields && writableFields.value.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
submitting.value = true;
|
submitting.value = true;
|
||||||
try {
|
try {
|
||||||
if (opinionMode.value === 'approve') {
|
if (opinionMode.value === 'approve') {
|
||||||
const msg = await approveApprovalTask(taskId, text || undefined);
|
const msg = await approveApprovalTask(
|
||||||
|
taskId,
|
||||||
|
text || undefined,
|
||||||
|
fields
|
||||||
|
);
|
||||||
if (msg && msg !== '审批成功') {
|
if (msg && msg !== '审批成功') {
|
||||||
message.warning(msg);
|
message.warning(msg);
|
||||||
} else {
|
} else {
|
||||||
@@ -668,6 +808,13 @@
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.approval-backfill {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.approval-section-title {
|
.approval-section-title {
|
||||||
margin: 24px 0 16px;
|
margin: 24px 0 16px;
|
||||||
font-size: 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>
|
<template #toolbar>
|
||||||
<a-space :size="10" style="flex-wrap: wrap">
|
<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
|
<a-select
|
||||||
v-model:value="where.status"
|
v-model:value="where.status"
|
||||||
allow-clear
|
allow-clear
|
||||||
@@ -81,7 +89,11 @@
|
|||||||
ApprovalTodoParam
|
ApprovalTodoParam
|
||||||
} from '@/api/approval/instance/model';
|
} from '@/api/approval/instance/model';
|
||||||
import { pageApprovalTodo } from '@/api/approval/instance';
|
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';
|
import DetailDrawer from './components/detail-drawer.vue';
|
||||||
|
|
||||||
// 表格实例
|
// 表格实例
|
||||||
@@ -96,10 +108,14 @@
|
|||||||
|
|
||||||
// 搜索条件
|
// 搜索条件
|
||||||
const { where, resetFields } = useSearch<ApprovalTodoParam>({
|
const { where, resetFields } = useSearch<ApprovalTodoParam>({
|
||||||
|
flowCode: undefined,
|
||||||
status: undefined,
|
status: undefined,
|
||||||
keywords: undefined
|
keywords: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 流程类型选项(后端暂未提供字典接口,见 format.ts)
|
||||||
|
const flowOptions = APPROVAL_FLOW_OPTIONS;
|
||||||
|
|
||||||
// 状态选项
|
// 状态选项
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{ label: '审批中', value: 'PENDING' },
|
{ label: '审批中', value: 'PENDING' },
|
||||||
@@ -180,6 +196,7 @@
|
|||||||
// 表格数据源
|
// 表格数据源
|
||||||
const datasource: DatasourceFunction = ({ page, limit }) => {
|
const datasource: DatasourceFunction = ({ page, limit }) => {
|
||||||
return pageApprovalTodo({
|
return pageApprovalTodo({
|
||||||
|
flowCode: where.flowCode,
|
||||||
status: where.status,
|
status: where.status,
|
||||||
keywords: where.keywords?.trim() || undefined,
|
keywords: where.keywords?.trim() || undefined,
|
||||||
scope: allScope.value ? 'all' : undefined,
|
scope: allScope.value ? 'all' : undefined,
|
||||||
|
|||||||
Reference in New Issue
Block a user