🔀 合并主干 master 到 dev

This commit is contained in:
2026-10-29 00:16:39 +08:00
12 changed files with 934 additions and 68 deletions
+1
View File
@@ -2,6 +2,7 @@
node_modules
pnpm-lock.yaml
/dist
/dist.zip
/tests/e2e/videos/
/tests/e2e/screenshots/
+3 -2
View File
@@ -4,8 +4,9 @@
"scripts": {
"dev": "vite --host",
"prod": "vite --mode production",
"build": "vite build",
"build:prod": "vite build --mode production",
"build": "vite build && node scripts/zip-dist.mjs",
"build:prod": "vite build --mode production && node scripts/zip-dist.mjs",
"zip:dist": "node scripts/zip-dist.mjs",
"serve": "vite preview --host",
"typecheck": "vue-tsc --noEmit"
},
+25
View File
@@ -0,0 +1,25 @@
import { execFileSync } from 'node:child_process';
import { existsSync, rmSync, statSync } from 'node:fs';
import { resolve } from 'node:path';
const root = process.cwd();
const distDir = resolve(root, 'dist');
const zipPath = resolve(root, 'dist.zip');
if (!existsSync(distDir) || !statSync(distDir).isDirectory()) {
console.error('[zip-dist] dist 目录不存在,请先完成构建');
process.exit(1);
}
if (existsSync(zipPath)) {
rmSync(zipPath);
}
// 压缩 dist 内文件到根级(解压即 index.html / assets,便于直接上传部署)
execFileSync('zip', ['-qr', zipPath, '.', '-x', '*.DS_Store', '-x', '__MACOSX/*'], {
cwd: distDir,
stdio: 'inherit',
});
const sizeMb = (statSync(zipPath).size / (1024 * 1024)).toFixed(2);
console.log(`[zip-dist] 已生成 ${zipPath}(${sizeMb} MB)`);
+61
View File
@@ -0,0 +1,61 @@
import { createCrudApi } from './common';
import request from '@/axios';
const baseUrl = '/blade-transport/contract-change';
const api = createCrudApi(baseUrl);
export const getList = api.getList;
export const getDetail = api.getDetail;
export const submit = api.submit;
export const remove = api.remove;
export const createFromContract = (contractId, changeType) =>
request({
url: `${baseUrl}/create-from-contract`,
method: 'post',
params: { contractId, changeType },
});
export const saveDraft = row =>
request({
url: `${baseUrl}/save-draft`,
method: 'post',
data: row,
});
export const submitApproval = id =>
request({
url: `${baseUrl}/submit-approval`,
method: 'post',
params: { id },
});
export const approve = id =>
request({
url: `${baseUrl}/approve`,
method: 'post',
params: { id },
// 审批通过后同步推送合规(附件转传可达 20s+),覆盖全局 10s 超时
timeout: 120000,
});
export const reject = id =>
request({
url: `${baseUrl}/reject`,
method: 'post',
params: { id },
});
export const withdraw = id =>
request({
url: `${baseUrl}/withdraw`,
method: 'post',
params: { id },
});
export const markSyncStatus = (ids, syncStatus) =>
request({
url: `${baseUrl}/mark-sync-status`,
method: 'post',
params: { ids, syncStatus },
});
+221
View File
@@ -0,0 +1,221 @@
import {
approvalStatusOptions,
contractCategoryOptions,
createCrudOption,
hcsSyncStatusOptions,
signTypeOptions,
} from './common';
const listDicFormatter = res => {
const data = res?.data || res;
if (Array.isArray(data)) return data;
if (Array.isArray(data?.records)) return data.records;
if (Array.isArray(data?.data)) return data.data;
if (Array.isArray(data?.data?.records)) return data.data.records;
return [];
};
const projectNameDicFormatter = res => {
const list = listDicFormatter(res);
return list.filter(item => item && item.projectName && String(item.projectName).trim());
};
export const changeTypeOptions = [
{ label: '合同信息变更', value: '合同信息变更' },
{ label: '结算规则变更', value: '结算规则变更' },
];
export const config = {
title: '合同变更',
permission: 'contract_change',
statusProp: 'approvalStatus',
statusTextProp: 'approvalStatusName',
deleteStatus: ['draft', 'withdrawn', 'rejected'],
editStatus: ['draft', 'withdrawn', 'rejected'],
viewStatus: ['approved'],
operations: [
{
action: 'submitApproval',
label: '提交',
statusProp: 'approvalStatus',
status: ['draft'],
permission: 'contract_change_submit',
},
{
action: 'withdraw',
label: '撤回',
statusProp: 'approvalStatus',
status: ['reviewing'],
permission: 'contract_change_withdraw',
},
{
action: 'mkAudit',
label: '流程',
statusProp: 'approvalStatus',
status: ['reviewing', 'rejected', 'approved'],
permission: 'contract_change_approve',
mkOnly: true,
},
{
action: 'approve',
label: '通过',
type: 'success',
statusProp: 'approvalStatus',
status: ['reviewing'],
permission: 'contract_change_approve',
localOnly: true,
},
{
action: 'reject',
label: '驳回',
type: 'warning',
statusProp: 'approvalStatus',
status: ['reviewing'],
permission: 'contract_change_reject',
localOnly: true,
},
],
};
export const option = createCrudOption([
{
label: '合同编号',
prop: 'contractNo',
search: true,
searchPlaceholder: '请输入',
searchOrder: 20,
minWidth: 160,
},
{
label: '合同名称',
prop: 'contractName',
search: true,
searchPlaceholder: '请输入',
searchOrder: 30,
minWidth: 180,
},
{
label: '所属项目',
prop: 'projectName',
search: true,
searchType: 'select',
searchPlaceholder: '请选择',
searchOrder: 60,
dicUrl: '/blade-transport/project-apply/list?current=1&size=9999&contractSelectable=true',
dicFormatter: projectNameDicFormatter,
props: {
label: 'projectName',
value: 'projectName',
},
filterable: true,
minWidth: 160,
},
{
label: '所属组织',
prop: 'organizationName',
search: true,
searchPlaceholder: '请输入',
searchOrder: 10,
minWidth: 150,
},
{
label: '合同类别',
prop: 'contractCategory',
type: 'select',
search: true,
searchPlaceholder: '请选择',
searchOrder: 70,
dicData: contractCategoryOptions,
minWidth: 130,
},
{
label: '签约类型',
prop: 'signType',
type: 'select',
search: true,
searchPlaceholder: '请选择',
searchOrder: 50,
dicData: signTypeOptions,
minWidth: 120,
},
{
label: '甲方',
prop: 'partyA',
minWidth: 160,
},
{
label: '乙方',
prop: 'partyB',
minWidth: 160,
},
{
label: '开始日期',
prop: 'startDate',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
minWidth: 130,
},
{
label: '结束日期',
prop: 'endDate',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
minWidth: 130,
},
{
label: '临时效力起',
prop: 'temporaryStartDate',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
minWidth: 130,
},
{
label: '临时效力止',
prop: 'temporaryEndDate',
type: 'date',
format: 'YYYY-MM-DD',
valueFormat: 'YYYY-MM-DD',
minWidth: 130,
},
{
label: '经办人',
prop: 'handlerUserName',
minWidth: 120,
},
{
label: '创建时间',
prop: 'createTime',
type: 'datetime',
minWidth: 170,
},
{
label: '审核状态',
prop: 'approvalStatus',
type: 'select',
search: true,
searchPlaceholder: '请选择',
searchOrder: 40,
dicData: approvalStatusOptions,
minWidth: 110,
},
{
label: '当前节点',
prop: 'currentNode',
minWidth: 120,
},
{
label: '当前处理人',
prop: 'currentProcessor',
minWidth: 120,
},
{
label: '同步状态',
prop: 'syncStatus',
type: 'select',
dicData: hcsSyncStatusOptions,
minWidth: 110,
},
]);
+11
View File
@@ -117,6 +117,17 @@ export default [
bizType: 'contract-manage',
},
},
{
path: '/business/contract-change/public-view',
name: '查看合同变更信息',
component: () => import('@/views/business/contract-change-public-view.vue'),
meta: {
keepAlive: false,
isTab: false,
isAuth: false,
bizType: 'contract-change',
},
},
{
path: '/settlement/pre-settlement/public-view',
name: '查看预付结算单信息',
+12
View File
@@ -354,6 +354,18 @@ export default [
},
],
},
{
path: '/business/contract-change/form',
component: Layout,
children: [
{
path: '',
name: '合同变更表单',
meta: { keepAlive: false, activeMenu: '/business/contract-change' },
component: loadView(() => import('@/views/business/contract-manage-change.vue')),
},
],
},
{
path: '/wel',
component: () =>
+6
View File
@@ -27,6 +27,12 @@ export const MK_BIZ = {
rejected: ['rejected', 'change_rejected', 'withdrawn'],
approved: ['approved', 'change_approved'],
},
'contract-change': {
dictName: '提交合同变更审核流',
subjectPrefix: '合同变更审批',
rejected: ['rejected', 'withdrawn'],
approved: ['approved'],
},
'pre-settlement': {
dictName: '提交预结算审核流',
subjectPrefix: '预付结算审批',
@@ -0,0 +1,21 @@
<template>
<mk-public-shell biz-type="contract-change" :get-form="getForm">
<contract-manage-change ref="page" />
</mk-public-shell>
</template>
<script>
import MkPublicShell from '@/views/mk/mk-public-shell.vue';
import ContractManageChange from '@/views/business/contract-manage-change.vue';
export default {
name: 'ContractChangePublicView',
components: { MkPublicShell, ContractManageChange },
methods: {
getForm() {
const row = this.$refs.page?.form || {};
return { ...row, subject: row.contractName || row.contractNo || '' };
},
},
};
</script>
+407
View File
@@ -0,0 +1,407 @@
<template>
<basic-container class="contract-change-page">
<avue-crud
:option="tableOption"
:table-loading="loading"
:data="data"
v-model:page="page"
v-model="tableForm"
ref="crud"
:permission="permissionList"
@search-change="searchChange"
@search-reset="searchReset"
@selection-change="selectionChange"
@current-change="currentChange"
@size-change="sizeChange"
@refresh-change="refreshChange"
@on-load="onLoad"
>
<template #menu-left>
<el-button type="primary" plain :loading="hcsRepushingId === 'batch'" @click="handleHcsBatchRepush">
批量同步
</el-button>
<el-button
type="danger"
icon="el-icon-delete"
plain
v-if="hasPermission(`${config.permission}_delete`)"
@click="handleDelete"
>
批量删除
</el-button>
</template>
<template #approvalStatus="{ row }">
{{ row.approvalStatusName || statusLabel(row.approvalStatus) }}
</template>
<template #syncStatus="{ row }">
<el-tag v-if="row.syncStatus" :type="syncTagType(row.syncStatus)" class="status-text">
{{ syncLabel(row.syncStatus) }}
</el-tag>
<span v-else>-</span>
</template>
<template #menu="{ row }">
<el-link type="primary" v-if="canView(row)" @click="openForm('view', row)"> 查看 </el-link>
<el-link type="primary" v-if="canEdit(row)" @click="openForm('edit', row)"> 编辑 </el-link>
<el-link type="danger" v-if="canDelete(row)" @click="rowDel(row)"> 删除 </el-link>
<el-link
v-for="operation in customOperations(row)"
:key="operation.action"
:type="
operation.type === 'warning'
? 'warning'
: operation.type === 'success'
? 'success'
: 'primary'
"
@click="handleCustomOperation(operation, row)"
>
{{ operation.label }}
</el-link>
</template>
</avue-crud>
</basic-container>
</template>
<script>
import * as api from '@/api/business/contract-change';
import { repushBatch as hcsRepushBatch } from '@/api/business/hcs';
import { getMKApprovalUrl } from '@/api/system/business-process';
import { config, option } from '@/option/business/contract-change';
import { approvalStatusOptions } from '@/option/business/common';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import { isAdministrator } from '@/utils/auth';
import { mapGetters } from 'vuex';
const contractChangeFormRoute = '/business/contract-change/form';
const contractChangeListRefreshKey = 'contract-change-list-refresh';
export default {
name: 'ContractChange',
data() {
return {
api,
config,
tableForm: {},
tableOption: this.buildTableOption(),
loading: true,
data: [],
page: {
pageSize: 10,
currentPage: 1,
total: 0,
},
query: {},
selectionList: [],
mkEnabled: false,
hcsRepushingId: null,
};
},
computed: {
...mapGetters(['permission', 'userInfo', 'website']),
permissionList() {
return {
addBtn: false,
viewBtn: false,
delBtn: false,
editBtn: false,
};
},
isAdmin() {
return isAdministrator(this.userInfo);
},
ids() {
return this.selectionList.map(item => item.id).join(',');
},
},
created() {
loadMkEnabled()
.then(enabled => {
this.mkEnabled = enabled;
})
.catch(() => {
this.mkEnabled = false;
});
},
activated() {
if (sessionStorage.getItem(contractChangeListRefreshKey)) {
sessionStorage.removeItem(contractChangeListRefreshKey);
this.onLoad(this.page, this.query);
}
},
methods: {
buildTableOption() {
return {
...option,
addBtn: false,
viewBtn: false,
editBtn: false,
delBtn: false,
menuWidth: 320,
column: (option.column || []).map(column => ({
...column,
display: false,
align: 'center',
headerAlign: 'center',
})),
};
},
hasPermission(code) {
return this.isAdmin || this.validData(this.permission && this.permission[code], false);
},
statusLabel(status) {
return approvalStatusOptions.find(item => item.value === status)?.label || status || '-';
},
syncLabel(value) {
return { unsynced: '待推送', synced: '已推送', failed: '推送失败' }[value] || value;
},
syncTagType(value) {
return { unsynced: 'info', synced: 'success', failed: 'danger' }[value] || 'info';
},
canHcsRepush(row) {
return row.approvalStatus === 'approved' && row.contractId && row.syncStatus !== 'synced';
},
handleHcsBatchRepush() {
if (this.hcsRepushingId) return;
if (!this.selectionList.length) {
this.$message.warning('请选择至少一条数据');
return;
}
const rows = this.selectionList.filter(item => this.canHcsRepush(item));
if (!rows.length) {
this.$message.warning('请选择审批通过且未同步成功的变更单');
return;
}
this.$confirm(`确定同步选中的${rows.length}条合同变更到合规系统?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
const loading = this.$loading({
lock: true,
text: '正在推送合规,请稍候…',
background: 'rgba(255, 255, 255, 0.6)',
});
this.hcsRepushingId = 'batch';
const contractIds = [...new Set(rows.map(item => item.contractId))];
const changeIds = rows.map(item => item.id).join(',');
hcsRepushBatch('contract', contractIds)
.then(res => {
if (res.data?.success === false) {
this.$message.error(res.data?.msg || '批量同步失败');
return api.markSyncStatus(changeIds, 'failed');
}
return api.markSyncStatus(changeIds, 'synced').then(() => {
this.$message.success(res.data?.data || '批量同步成功');
this.onLoad(this.page, this.query);
});
})
.catch(() => api.markSyncStatus(changeIds, 'failed'))
.finally(() => {
this.hcsRepushingId = null;
loading.close();
});
});
},
inStatus(row, statusList) {
if (!statusList || !statusList.length) return true;
return statusList.includes(row.approvalStatus);
},
canEdit(row) {
return (
this.hasPermission(`${this.config.permission}_edit`) &&
!row.readonly &&
this.inStatus(row, this.config.editStatus)
);
},
canDelete(row) {
return (
this.hasPermission(`${this.config.permission}_delete`) &&
!row.readonly &&
this.inStatus(row, this.config.deleteStatus)
);
},
canView(row) {
return (
this.hasPermission(`${this.config.permission}_view`) &&
this.inStatus(row, this.config.viewStatus)
);
},
canOperation(operation, row) {
if (operation.permission && !this.hasPermission(operation.permission)) return false;
if (
row.readonly &&
['submitApproval', 'withdraw', 'approve', 'reject'].includes(operation.action)
) {
return false;
}
return this.inStatus(row, operation.status);
},
customOperations(row) {
return (this.config.operations || []).filter(operation => {
if (operation.mkOnly && !this.mkEnabled) return false;
if (operation.localOnly && this.mkEnabled) return false;
return this.canOperation(operation, row);
});
},
openForm(mode, row = {}) {
const query = { mode, id: row.id };
if (mode === 'edit') query.name = '编辑合同变更';
else if (mode === 'view') query.name = '查看合同变更';
this.$router.push({ path: contractChangeFormRoute, query });
},
handleCustomOperation(operation, row) {
if (operation.action === 'mkAudit') {
this.handleMkAudit(row);
return;
}
const run = () => {
const request = () => this.api[operation.action](row.id);
const afterMk =
operation.action === 'submitApproval'
? submitMkApprovalFlow({
bizType: 'contract-change',
bizId: row.id,
subjectName: row.contractName || '',
approvalStatus: row.approvalStatus || '',
mkEnabled: this.mkEnabled,
}).then(() => request())
: request();
const loading =
operation.action === 'approve'
? this.$loading({
lock: true,
text: '正在推送合规,请稍候…',
background: 'rgba(255, 255, 255, 0.6)',
})
: null;
afterMk
.then(res => {
if (operation.action === 'approve' && res.data?.data === false) {
this.$message.warning(res.data?.msg);
this.onLoad(this.page, this.query);
return;
}
if (res.data?.success === false) {
this.$message.error(res.data?.msg || `${operation.label}失败,请联系管理员`);
return;
}
this.$message.success(`${operation.label}成功`);
this.onLoad(this.page, this.query);
})
.finally(() => loading && loading.close());
};
this.$confirm(operation.confirm || `确定${operation.label}该${this.config.title}?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => run());
},
handleMkAudit(row) {
if (!row?.id) {
this.$message.warning('变更单ID为空,无法打开审核页');
return;
}
getMKApprovalUrl({ bizId: String(row.id) })
.then(res => {
const url = res?.data?.data;
if (!url) {
this.$message.error('获取审核跳转地址失败');
return;
}
window.open(url, '_blank');
})
.catch(() => {
this.$message.error('获取审核跳转地址失败');
});
},
searchReset() {
this.query = {};
this.onLoad(this.page);
},
searchChange(params, done) {
this.query = params;
this.page.currentPage = 1;
this.onLoad(this.page, params);
done();
},
selectionChange(list) {
this.selectionList = list;
},
selectionClear() {
this.selectionList = [];
this.$refs.crud?.toggleSelection?.();
},
currentChange(currentPage) {
this.page.currentPage = currentPage;
},
sizeChange(pageSize) {
this.page.pageSize = pageSize;
},
refreshChange() {
this.onLoad(this.page, this.query);
},
rowDel(row) {
this.$confirm('确定将选择数据删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.api.remove(row.id).then(() => {
this.$message.success('删除成功!');
this.onLoad(this.page, this.query);
});
});
},
handleDelete() {
if (!this.selectionList.length) {
this.$message.warning('请选择至少一条数据');
return;
}
this.$confirm('确定将选择数据删除?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
this.api.remove(this.ids).then(() => {
this.$message.success('删除成功!');
this.selectionClear();
this.onLoad(this.page, this.query);
});
});
},
onLoad(page, params = {}) {
this.loading = true;
const values = {
...this.query,
...params,
};
this.api
.getList(page.currentPage, page.pageSize, values)
.then(res => {
const data = res.data.data || {};
this.page.total = data.total || 0;
this.data = data.records || [];
this.loading = false;
this.selectionClear();
})
.catch(() => {
this.loading = false;
});
},
},
};
</script>
<style scoped lang="scss">
.contract-change-page {
:deep(.avue-crud__menu) {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
}
</style>
+140 -62
View File
@@ -1,12 +1,20 @@
<template>
<basic-container class="contract-change-page">
<div class="archive-page-form__title">{{ pageTitle }}</div>
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="auto" class="contract-change-form">
<el-form
ref="formRef"
:model="form"
:rules="rules"
:disabled="pageReadonly"
label-position="right"
label-width="auto"
class="contract-change-form"
>
<section class="change-section contract-basic-section">
<div class="dialog-section-title">基本信息</div>
<div class="contract-basic-section__grid">
<el-form-item label="变更类型" class="contract-basic-section__change-type">
<el-radio-group v-model="form.changeType">
<el-radio-group v-model="form.changeType" :disabled="pageReadonly">
<el-radio label="合同信息变更" />
<el-radio label="结算规则变更" />
<el-radio label="终止合同" />
@@ -280,7 +288,7 @@
<div class="section-head">
<div class="dialog-section-title">计费信息</div>
<el-button
v-if="feeGenerationMode !== 'manual'"
v-if="!pageReadonly && feeGenerationMode !== 'manual'"
type="primary"
plain
@click="addPlan"
@@ -301,6 +309,7 @@
row.defaultPlan ? '是' : '否'
}}</template></el-table-column
><el-table-column prop="remark" label="备注" min-width="260" /><el-table-column
v-if="!pageReadonly"
label="操作"
width="180"
><template #default="{ row, $index }"
@@ -416,7 +425,7 @@
:value="item.value"
/></el-select></template
></el-table-column>
<el-table-column label="操作" width="100"
<el-table-column v-if="!pageReadonly" label="操作" width="100"
><template #default="{ $index }"
><el-link v-if="$index === 0" type="primary" @click="addCustomPeriodRow">添加</el-link
><el-link v-else type="danger" @click="removeCustomPeriodRow($index)"
@@ -425,7 +434,7 @@
></el-table-column
>
<template #empty
><el-link type="primary" @click="addCustomPeriodRow">添加</el-link></template
><el-link v-if="!pageReadonly" type="primary" @click="addCustomPeriodRow">添加</el-link></template
>
</el-table>
</section>
@@ -461,7 +470,7 @@
:disabled="isSettlementRuleChange"
/></template
></el-table-column>
<el-table-column v-if="!isSettlementRuleChange" label="操作" width="100"
<el-table-column v-if="!pageReadonly && !isSettlementRuleChange" label="操作" width="100"
><template #default="{ $index }"
><el-link type="danger" @click="paymentRatioRows.splice($index, 1)"
>删除</el-link
@@ -470,7 +479,7 @@
>
</el-table>
<el-button
v-if="!isSettlementRuleChange"
v-if="!pageReadonly && !isSettlementRuleChange"
type="primary"
plain
@click="addPaymentRatioRow"
@@ -525,8 +534,9 @@
@update:rows="changeMaterials = $event"
@upload-to-location="handleAttachmentUploadToLocation"
/>
<div class="page-footer">
<el-button @click="$router.back()">取消</el-button>
<div v-if="!pageReadonly" class="page-footer">
<el-button @click="goBack">取消</el-button>
<el-button v-if="isContractChangeModule" @click="saveDraft">保存草稿</el-button>
<el-button type="primary" @click="submit">提交</el-button>
</div>
</el-form>
@@ -539,7 +549,9 @@
</template>
<script>
import * as api from '@/api/business/contract-manage';
import * as contractApi from '@/api/business/contract-manage';
import * as changeApi from '@/api/business/contract-change';
import { getMkPublicDetail } from '@/api/mk-process';
import { loadMkEnabled, submitMkApprovalFlow } from '@/utils/mk-approval';
import BillingPlanEditor from './components/billing-plan-editor.vue';
import ContractAttachmentSection, {
@@ -577,6 +589,8 @@ const defaultContractFormatOptions = [
{ label: '电子合同', value: '电子合同' },
{ label: '纸质合同', value: '纸质合同' },
];
const contractChangeListRoute = '/business/contract-change';
const contractChangeListRefreshKey = 'contract-change-list-refresh';
const normalizeOptionalPositiveInteger = value => {
if (value === undefined || value === null || value === '') return null;
const number = Number(value);
@@ -713,8 +727,21 @@ export default {
},
computed: {
pageTitle() {
if (this.pageReadonly) return this.$route.query.name || '查看合同变更';
return this.$route.query.name || '合同变更';
},
isContractChangeModule() {
return (
this.$route.path === '/business/contract-change/form' ||
this.$route.path === '/business/contract-change/public-view'
);
},
isPublicViewPage() {
return this.$route.path === '/business/contract-change/public-view';
},
pageReadonly() {
return this.isPublicViewPage || this.$route.query.mode === 'view';
},
// 高风险/黑名单客商提示:按合同推送合规留痕 hcs_risk_level/hcs_black_status 判定(与合同详情口径一致)
riskTipText() {
const riskLevel = String(this.form.hcsRiskLevel || '').trim();
@@ -833,13 +860,25 @@ export default {
async load() {
const id = this.$route.query.id;
if (!id) return;
const res = await api.getDetail(id);
const data = res.data?.data || res.data || {};
let data = {};
if (this.isPublicViewPage) {
const res = await getMkPublicDetail('contract-change', id);
data = res.data?.data || res.data || {};
} else if (this.isContractChangeModule) {
const res = await changeApi.getDetail(id);
data = res.data?.data || res.data || {};
} else {
const res = await contractApi.getDetail(id);
data = res.data?.data || res.data || {};
}
const isChangeRecord = this.isContractChangeModule || this.isPublicViewPage;
this.form = {
...data,
changeContent: '',
changeReason: '',
changeAttachmentsJson: '',
// 独立变更单保留 contractId;旧路由以合同 id 作为主键
contractId: data.contractId || (isChangeRecord ? data.contractId : data.id),
changeContent: isChangeRecord ? data.changeContent || '' : '',
changeReason: isChangeRecord ? data.changeReason || '' : '',
changeAttachmentsJson: data.changeAttachmentsJson || '',
copyCount: normalizeOptionalPositiveInteger(data.copyCount),
invoiceCycle: normalizeOptionalPositiveInteger(data.invoiceCycle),
paymentDays: normalizeOptionalPositiveInteger(data.paymentDays),
@@ -847,14 +886,14 @@ export default {
templateFlag: data.templateFlag ?? 0,
electronicSealFlag: data.electronicSealFlag ?? 0,
archiveStatus: data.archiveStatus || '未归档',
changeType: '合同信息变更',
changeType: data.changeType || '合同信息变更',
};
this.changeMaterials = [];
this.changeMaterials = this.parse(data.changeAttachmentsJson);
this.period = data.startDate && data.endDate ? [data.startDate, data.endDate] : [];
this.plans = this.parse(data.billingPlanJson);
this.attachments = this.mergeChangeAttachments(
this.parse(data.attachmentsJson),
this.parse(data.changeAttachmentsJson)
isChangeRecord ? [] : this.parse(data.changeAttachmentsJson)
);
this.contractFileRows = normalizeContractFileRows(this.parse(data.contractFileJson));
const rules = this.parseObject(data.settlementRuleJson);
@@ -896,6 +935,70 @@ export default {
data.feeGenerationMode || (Number(data.billingEnabled) === 0 ? 'manual' : 'system');
this.paymentRatioRows = this.parse(data.paymentRatioJson);
},
goBack() {
if (this.isContractChangeModule) {
this.$router.push(contractChangeListRoute);
return;
}
this.$router.back();
},
buildChangePayload() {
if (this.settlementConfigTab === 'pre') this.preSettlementConfig = { ...this.settlementRule };
else this.formalSettlementConfig = { ...this.settlementRule };
const settlementRule = {
preSettlementConfig: this.preSettlementConfig,
formalSettlementConfig: this.formalSettlementConfig,
};
if (this.isSettlementRuleChange) {
return {
id: this.form.id,
contractId: this.form.contractId,
changeType: this.form.changeType,
changeContent: this.form.changeContent,
changeReason: this.form.changeReason,
feeGenerationMode: this.feeGenerationMode,
billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0,
billingPlanJson: JSON.stringify(this.plans),
settlementRuleJson: JSON.stringify(settlementRule),
preSettlementConfigJson: JSON.stringify(this.preSettlementConfig),
formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig),
changeAttachmentsJson: JSON.stringify(this.changeMaterials),
};
}
return {
...this.form,
contractId: this.form.contractId,
changeType: this.form.changeType || '合同信息变更',
settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB',
copyCount: normalizeOptionalPositiveInteger(this.form.copyCount),
invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle),
paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays),
contractAmount: normalizeOptionalAmount(this.form.contractAmount),
startDate: this.period[0],
endDate: this.period[1],
feeGenerationMode: this.feeGenerationMode,
billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0,
billingPlanJson: JSON.stringify(this.plans),
settlementRuleJson: JSON.stringify(settlementRule),
preSettlementConfigJson: JSON.stringify(this.preSettlementConfig),
formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig),
paymentRatioJson: JSON.stringify(this.paymentRatioRows),
contractFileJson: JSON.stringify(this.contractFileRows),
attachmentsJson: JSON.stringify(this.attachments),
changeContent: this.form.changeContent,
changeReason: this.form.changeReason,
changeAttachmentsJson: JSON.stringify(this.changeMaterials),
};
},
async saveDraft() {
if (this.form.changeType === '终止合同') {
this.$message.warning('终止合同无需保存草稿,请直接提交');
return;
}
const payload = this.buildChangePayload();
await changeApi.saveDraft(payload);
this.$message.success('草稿已保存');
},
parse(value) {
try {
const result = JSON.parse(value || '[]');
@@ -989,9 +1092,10 @@ export default {
validateSettlementRule(rule, label) { if (Number(rule.autoGenerate) !== 1) return true; if (!rule.billStartDate || !rule.settlementType) { this.$message.warning(`${label}:请完整填写账单起始日和结算类型`); return false; } if (rule.settlementType === '月结' && !rule.billCycleType) { this.$message.warning(`${label}:请选择结算周期`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '固定截单日' && !rule.billCutoffDay) { this.$message.warning(`${label}:请选择账单截单日`); return false; } if (rule.settlementType === '月结' && rule.billCycleType === '自定义多周期') return this.validateCustomPeriods(rule.customPeriods, label); if (rule.settlementType === '固定天数周期结算' && !rule.cycleDays) { this.$message.warning(`${label}:请选择周期天数`); return false; } return true; },
async submit() {
if (this.form.changeType === '终止合同') {
await api.terminate(this.form.id, this.form.changeReason);
const contractId = this.form.contractId || this.form.id;
await contractApi.terminate(contractId, this.form.changeReason);
this.$message.success('合同已终止');
this.$router.back();
this.goBack();
return;
}
await this.$refs.formRef.validate();
@@ -1020,48 +1124,22 @@ export default {
) {
return;
}
const settlementRule = {
preSettlementConfig: this.preSettlementConfig,
formalSettlementConfig: this.formalSettlementConfig,
};
const payload = this.isSettlementRuleChange
? {
id: this.form.id,
changeType: this.form.changeType,
changeContent: this.form.changeContent,
changeReason: this.form.changeReason,
feeGenerationMode: this.feeGenerationMode,
billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0,
billingPlanJson: JSON.stringify(this.plans),
settlementRuleJson: JSON.stringify(settlementRule),
preSettlementConfigJson: JSON.stringify(this.preSettlementConfig),
formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig),
changeAttachmentsJson: JSON.stringify(this.changeMaterials),
}
: {
...this.form,
changeType: this.form.changeType || '合同信息变更',
settlementCurrency: String(this.form.settlementCurrency || '').trim() || 'RMB',
copyCount: normalizeOptionalPositiveInteger(this.form.copyCount),
invoiceCycle: normalizeOptionalPositiveInteger(this.form.invoiceCycle),
paymentDays: normalizeOptionalPositiveInteger(this.form.paymentDays),
contractAmount: normalizeOptionalAmount(this.form.contractAmount),
startDate: this.period[0],
endDate: this.period[1],
feeGenerationMode: this.feeGenerationMode,
billingEnabled: this.feeGenerationMode === 'system' ? 1 : 0,
billingPlanJson: JSON.stringify(this.plans),
settlementRuleJson: JSON.stringify(settlementRule),
preSettlementConfigJson: JSON.stringify(this.preSettlementConfig),
formalSettlementConfigJson: JSON.stringify(this.formalSettlementConfig),
paymentRatioJson: JSON.stringify(this.paymentRatioRows),
contractFileJson: JSON.stringify(this.contractFileRows),
attachmentsJson: JSON.stringify(this.attachments),
changeContent: this.form.changeContent,
changeReason: this.form.changeReason,
changeAttachmentsJson: JSON.stringify(this.changeMaterials),
};
await api.submitChange(payload);
const payload = this.buildChangePayload();
if (this.isContractChangeModule) {
await changeApi.submit(payload);
await submitMkApprovalFlow({
bizType: 'contract-change',
bizId: this.form.id,
subjectName: this.form.contractName || '',
approvalStatus: this.form.approvalStatus || '',
mkEnabled: await loadMkEnabled().catch(() => false),
});
sessionStorage.setItem(contractChangeListRefreshKey, '1');
this.$message.success('变更已提交');
this.$router.push(contractChangeListRoute);
return;
}
await contractApi.submitChange(payload);
await submitMkApprovalFlow({
bizType: 'contract-manage',
bizId: this.form.id,
+26 -4
View File
@@ -1089,6 +1089,7 @@ import NProgress from 'nprogress';
import 'nprogress/nprogress.css';
import { exportBlob } from '@/api/common';
import * as api from '@/api/business/contract-manage';
import { createFromContract } from '@/api/business/contract-change';
import { repush as hcsRepush, repushBatch as hcsRepushBatch, getRisk } from '@/api/business/hcs';
import { Position } from '@element-plus/icons-vue';
import { getMkPublicDetail } from '@/api/mk-process';
@@ -3220,12 +3221,33 @@ export default {
if (value === undefined || value === null || value === '') return '-';
return options.find(item => String(item.value) === String(value))?.label || value;
},
handleStartContractChange(row) {
if (!row?.id) {
this.$message.warning('合同ID为空,无法发起变更');
return;
}
const loading = this.$loading({
lock: true,
text: '正在创建变更单…',
background: 'rgba(255, 255, 255, 0.6)',
});
createFromContract(row.id)
.then(res => {
const data = res?.data?.data || res?.data || {};
if (!data.id) {
this.$message.error(res?.data?.msg || '创建合同变更草稿失败');
return;
}
this.$router.push({
path: '/business/contract-change/form',
query: { mode: 'edit', id: data.id, name: '合同变更' },
});
})
.finally(() => loading.close());
},
handleOperation(operation, row) {
if (operation.action === 'startChange') {
this.$router.push({
path: '/business/contract-manage/change',
query: { id: row.id, name: '合同变更' },
});
this.handleStartContractChange(row);
return;
}
if (operation.action === 'flow') {