From 6a87066097a598243ae16ba3b51cc21c390824a0 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Tue, 15 Sep 2026 11:59:55 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E6=95=B4=20=E5=90=88=E5=90=8C?= =?UTF-8?q?=E3=80=81=E8=BF=90=E5=8A=9B=E3=80=81=E5=AE=A2=E5=95=86=E3=80=81?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/system/dept.js | 7 + src/option/business/project-apply.js | 6 +- src/option/business/temporary-credit-limit.js | 83 +-- .../transportCapacity/vehicle-dispatch.js | 70 ++- src/permission.js | 24 + src/router/views/index.js | 12 + src/utils/chunk-reload.js | 38 ++ .../contract-attachment-section.vue | 153 ++++- src/views/business/contract-manage-change.vue | 579 +++++++++++++++--- src/views/business/contract-manage.vue | 547 +++++++++-------- src/views/business/project-apply.vue | 130 ++-- src/views/business/temporary-credit-limit.vue | 428 ++++++++++++- src/views/system/dept.vue | 155 ++++- .../transportCapacity/vehicle-dispatch.vue | 23 +- src/views/vehicle/customer-archive.vue | 559 +++++------------ 15 files changed, 1857 insertions(+), 957 deletions(-) create mode 100644 src/utils/chunk-reload.js diff --git a/src/api/system/dept.js b/src/api/system/dept.js index ba96954..fc2f0ad 100644 --- a/src/api/system/dept.js +++ b/src/api/system/dept.js @@ -78,3 +78,10 @@ export const getDeptLazyTree = parentId => { }, }); }; + +export const getPlatformCompanySelect = () => { + return request({ + url: '/blade-system/dept/platform-company-select', + method: 'get', + }); +}; diff --git a/src/option/business/project-apply.js b/src/option/business/project-apply.js index 84a8dcb..ad46f63 100644 --- a/src/option/business/project-apply.js +++ b/src/option/business/project-apply.js @@ -169,16 +169,16 @@ export const option = createCrudOption([ rules: textRule('业务部门', 50, true), }, { - label: '承办部门', + label: '平台公司', prop: 'undertakeDeptName', search: true, searchPlaceholder: '请输入', searchOrder: 8, minWidth: 150, - rules: textRule('承办部门', 50, true), + rules: textRule('平台公司', 50, true), }, { - label: '承办部门ID', + label: '平台公司ID', prop: 'undertakeDeptId', hide: true, }, diff --git a/src/option/business/temporary-credit-limit.js b/src/option/business/temporary-credit-limit.js index 2bed4f2..4ad6fb4 100644 --- a/src/option/business/temporary-credit-limit.js +++ b/src/option/business/temporary-credit-limit.js @@ -2,7 +2,6 @@ import { approvalStatusOptions, auditColumns, createCrudOption, - futureDateRule, nonNegativeRule, textRule, withSearchPlaceholders, @@ -58,30 +57,25 @@ export const config = { detailAlignCenter: true, detailSections: [ { - title: '申请信息', + title: '项目额度信息', fields: [ - ['applicationNo', '申请单号', 1], - ['projectName', '项目', 1], + ['projectName', '项目名称', 1], ['projectCode', '项目编号', 1], ['undertakeDeptName', '承办部门', 1], - ['applyDeptName', '申请部门', 1], - ['applicantName', '申请人', 1], - ['approvalStatus', '审批状态', 1], - ['currentNode', '当前节点', 1], - ['currentProcessor', '当前处理人', 1], - ['validUntil', '申请有效期至', 1], - ['remark', '备注', 1], - ], - }, - { - title: '额度信息', - fields: [ ['projectFundLimit', '项目资金使用额度(万元)', 1], ['usedFundLimit', '已使用项目资金使用额度(万元)', 1], ['remainingFundLimit', '剩余项目资金使用额度(万元)', 1], - ['applyLimit', '申请临时额度(万元)', 1], ], }, + { + title: '临时额度信息', + fields: [ + ['applyLimit', '申请临时额度(万元)', 1], + ['validUntil', '申请有效期至', 1], + ['remark', '备注', 4], + ], + showAttachment: true, + }, ], operations: [ { @@ -140,6 +134,14 @@ export const option = { editDisplay: false, editDisabled: true, }, + { + label: '', + prop: 'projectQuotaInfoTitle', + formslot: true, + span: 24, + hide: true, + labelWidth: 0, + }, { label: '项目名称', prop: 'projectName', @@ -216,6 +218,14 @@ export const option = { value: 0, rules: [nonNegativeRule('剩余项目资金使用额度')], }, + { + label: '', + prop: 'temporaryCreditInfoTitle', + formslot: true, + span: 24, + hide: true, + labelWidth: 0, + }, { label: '申请临时额度(万元)', prop: 'applyLimit', @@ -238,9 +248,27 @@ export const option = { minWidth: 140, rules: [ { required: true, message: '请选择申请有效期至', trigger: 'change' }, - futureDateRule('申请有效期至'), ], }, + { + label: '备注', + prop: 'remark', + type: 'textarea', + span: 24, + minRows: 2, + hide: true, + placeholder: '请输入', + maxlength: 200, + showWordLimit: true, + rules: textRule('备注', 200), + }, + { + label: '附件', + prop: 'attachmentsJson', + formslot: true, + span: 24, + hide: true, + }, { label: '申请部门', prop: 'applyDeptName', @@ -304,25 +332,6 @@ export const option = { addDisplay: false, editDisplay: false, }, - { - label: '临时额度信息', - prop: 'remark', - type: 'textarea', - span: 24, - minRows: 2, - hide: true, - placeholder: '请输入', - maxlength: 200, - showWordLimit: true, - rules: textRule('临时额度信息', 200), - }, - { - label: '附件', - prop: 'attachmentsJson', - formslot: true, - span: 24, - hide: true, - }, ...auditColumns.map(column => ({ ...column, hide: true })), ])), dialogWidth: '96%', diff --git a/src/option/transportCapacity/vehicle-dispatch.js b/src/option/transportCapacity/vehicle-dispatch.js index f8ab8d7..0e50813 100644 --- a/src/option/transportCapacity/vehicle-dispatch.js +++ b/src/option/transportCapacity/vehicle-dispatch.js @@ -36,6 +36,7 @@ export const option = { menuFixed: 'right', menuWidth: 320, column: [ + // ========== 表格列(顺序固定)========== { label: '申请单号', prop: 'applicationNo', @@ -50,14 +51,14 @@ export const option = { placeholder: '保存后自动生成', }, { - label: '调度车辆', + label: '车牌号', prop: 'plateNo', search: true, searchOrder: 4, searchPlaceholder: '请输入', minWidth: 120, formslot: true, - rules: [{ required: true, message: '请输入车牌号', trigger: 'blur' }], + rules: [{ required: true, message: '请选择车牌号', trigger: 'blur' }], }, { label: '所属组织', @@ -67,7 +68,7 @@ export const option = { searchPlaceholder: '请输入', minWidth: 140, disabled: true, - placeholder: '选择调度车辆后自动填充', + placeholder: '选择车牌号后自动填充', rules: [{ required: true, message: '请输入所属组织', trigger: 'blur' }], }, { @@ -79,7 +80,15 @@ export const option = { minWidth: 140, formslot: true, span: 12, - rules: [{ required: true, message: '请输入使用部门', trigger: 'blur' }], + rules: [{ required: true, message: '请选择使用部门', trigger: 'blur' }], + }, + { + label: '车辆类型', + prop: 'vehicleType', + minWidth: 110, + addDisplay: false, + editDisplay: false, + viewDisplay: false, }, { label: '审批状态', @@ -107,31 +116,6 @@ export const option = { addDisplay: false, editDisplay: false, }, - { - label: '申请人', - prop: 'createUserName', - hide: true, - display: true, - addDisplay: true, - editDisplay: true, - viewDisplay: true, - disabled: true, - span: 12, - }, - { - label: '申请时间', - prop: 'createTime', - type: 'datetime', - format: 'YYYY-MM-DD HH:mm:ss', - valueFormat: 'YYYY-MM-DD HH:mm:ss', - hide: true, - display: true, - addDisplay: true, - editDisplay: true, - viewDisplay: true, - disabled: true, - span: 12, - }, { label: '创建人', prop: 'createUserName', @@ -149,10 +133,37 @@ export const option = { addDisplay: false, editDisplay: false, }, + // ========== 新增/编辑表单专用(不进表格)========== + { + label: '申请人', + prop: 'applicantName', + hide: true, + display: true, + addDisplay: true, + editDisplay: true, + viewDisplay: true, + disabled: true, + span: 12, + }, + { + label: '申请时间', + prop: 'applyTime', + type: 'datetime', + format: 'YYYY-MM-DD HH:mm:ss', + valueFormat: 'YYYY-MM-DD HH:mm:ss', + hide: true, + display: true, + addDisplay: true, + editDisplay: true, + viewDisplay: true, + disabled: true, + span: 12, + }, { label: '备注', prop: 'remark', type: 'textarea', + hide: true, span: 24, maxlength: 200, showWordLimit: true, @@ -164,7 +175,6 @@ export const option = { hide: true, span: 24, minWidth: 100, - slot: true, formslot: true, }, ], diff --git a/src/permission.js b/src/permission.js index 387889a..27b2028 100644 --- a/src/permission.js +++ b/src/permission.js @@ -2,11 +2,35 @@ import router from './router/'; import store from './store'; import { tabKeyOf } from '@/router/tab'; import { getToken } from '@/utils/auth'; +import { + isChunkLoadError, + reloadForChunkError, + setPendingRoutePath, +} from '@/utils/chunk-reload'; +import { ElMessage } from 'element-plus'; import NProgress from 'nprogress'; // progress bar import 'nprogress/nprogress.css'; // progress bar style NProgress.configure({ showSpinner: false }); const lockPage = '/lock'; //锁屏页 + +// 懒加载 chunk / CSS 失败时整页刷新,避免菜单点击后卡死 +router.onError(error => { + if (!isChunkLoadError(error)) return; + if (reloadForChunkError()) { + ElMessage.warning('页面资源加载失败,正在重新加载…'); + } +}); + +window.addEventListener('unhandledrejection', event => { + if (!isChunkLoadError(event.reason)) return; + if (reloadForChunkError()) { + event.preventDefault(); + ElMessage.warning('页面资源加载失败,正在重新加载…'); + } +}); + router.beforeEach((to, from, next) => { + setPendingRoutePath(to.fullPath); const meta = to.meta || {}; const isMenu = meta.menu === undefined ? to.query.menu : meta.menu; store.commit('SET_IS_MENU', isMenu === undefined); diff --git a/src/router/views/index.js b/src/router/views/index.js index a22af30..3a380e6 100644 --- a/src/router/views/index.js +++ b/src/router/views/index.js @@ -155,6 +155,18 @@ export default [ }, ], }, + { + path: '/business/contract-manage/detail', + component: Layout, + children: [ + { + path: '', + name: '合同详情', + meta: { keepAlive: false, activeMenu: '/business/contract-manage' }, + component: () => import('@/views/business/contract-manage.vue'), + }, + ], + }, { path: '/business/project-apply/form', component: Layout, diff --git a/src/utils/chunk-reload.js b/src/utils/chunk-reload.js new file mode 100644 index 0000000..2d42f54 --- /dev/null +++ b/src/utils/chunk-reload.js @@ -0,0 +1,38 @@ +/** + * 懒加载 chunk / CSS preload 失败后的整页恢复。 + * 常见于:部署后旧 hash 失效、静态服务空闲断连、代理 CONNECTION_RESET。 + */ + +const RELOAD_FLAG = 'app:chunk-reload-ts'; +const RELOAD_COOLDOWN_MS = 10000; + +const CHUNK_ERROR_RE = + /Failed to fetch dynamically imported module|Importing a module script failed|Unable to preload CSS|error loading dynamically imported module|Loading CSS chunk|Loading chunk .+ failed|ChunkLoadError/i; + +/** 最近一次路由跳转目标,供 onError 时整页落到正确地址 */ +let pendingFullPath = ''; + +export function setPendingRoutePath(fullPath = '') { + pendingFullPath = fullPath || ''; +} + +export function isChunkLoadError(error) { + if (!error) return false; + const message = error.message || String(error); + return CHUNK_ERROR_RE.test(message); +} + +/** + * @returns {boolean} 是否已触发刷新(冷却期内返回 false,避免死循环) + */ +export function reloadForChunkError(targetPath) { + const now = Date.now(); + const last = Number(sessionStorage.getItem(RELOAD_FLAG) || 0); + if (now - last < RELOAD_COOLDOWN_MS) { + return false; + } + sessionStorage.setItem(RELOAD_FLAG, String(now)); + const path = targetPath || pendingFullPath || window.location.pathname + window.location.search + window.location.hash; + window.location.assign(path); + return true; +} diff --git a/src/views/business/components/contract-attachment-section.vue b/src/views/business/components/contract-attachment-section.vue index fdf4225..ef36b9b 100644 --- a/src/views/business/components/contract-attachment-section.vue +++ b/src/views/business/components/contract-attachment-section.vue @@ -103,8 +103,8 @@
附件位置 - - + +
@@ -152,13 +152,16 @@ import { UploadFilled } from '@element-plus/icons-vue'; import { baseUrl } from '@/config/env'; import { getUploadHeaders } from '@/utils/upload'; import { downloadFileByUrl } from '@/utils/util'; +import { getDictionary as getBizDictionary } from '@/api/system/dictbiz'; const CONTRACT_ATTACHMENT_TYPE_OTHER = '其它文件'; -export const contractAttachmentTypeOptions = [ +export const defaultContractAttachmentTypeOptions = [ { label: '合同文件', value: '合同文件' }, { label: '双章归档文件', value: '双章归档文件' }, { label: '其它文件', value: CONTRACT_ATTACHMENT_TYPE_OTHER }, ]; +/** @deprecated 兼容旧引用,实际选项由业务字典 contract_attachment_type 动态加载 */ +export const contractAttachmentTypeOptions = defaultContractAttachmentTypeOptions; export const attachmentFileTypes = [ 'pdf', 'bmp', @@ -204,13 +207,29 @@ const attachmentTypeKeywords = fileType => { return [fileType]; }; -export const resolveContractAttachmentType = (row = {}) => { - const values = contractAttachmentTypeOptions.map(item => item.value); +const resolveOtherAttachmentType = (options = []) => { + const list = options.length ? options : defaultContractAttachmentTypeOptions; + const matched = list.find( + item => + String(item.value) === CONTRACT_ATTACHMENT_TYPE_OTHER || + String(item.label).includes('其它') || + String(item.label).includes('其他') + ); + return matched?.value || list[list.length - 1]?.value || CONTRACT_ATTACHMENT_TYPE_OTHER; +}; + +export const resolveContractAttachmentType = ( + row = {}, + options = defaultContractAttachmentTypeOptions +) => { + const list = options.length ? options : defaultContractAttachmentTypeOptions; + const values = list.map(item => item.value); if (values.includes(row.fileType)) return row.fileType; const fileName = normalizeAttachmentTypeText(attachmentName(row)); - if (!fileName) return CONTRACT_ATTACHMENT_TYPE_OTHER; - const matched = contractAttachmentTypeOptions - .filter(item => item.value !== CONTRACT_ATTACHMENT_TYPE_OTHER) + const otherType = resolveOtherAttachmentType(list); + if (!fileName) return otherType; + const matched = list + .filter(item => item.value !== otherType) .flatMap(item => attachmentTypeKeywords(item.value).map(keyword => ({ value: item.value, @@ -220,16 +239,27 @@ export const resolveContractAttachmentType = (row = {}) => { .filter(item => item.keyword) .sort((a, b) => b.keyword.length - a.keyword.length) .find(item => fileName.includes(item.keyword)); - return matched?.value || CONTRACT_ATTACHMENT_TYPE_OTHER; + return matched?.value || otherType; }; -export const normalizeContractFileRows = rows => +export const normalizeContractFileRows = (rows, options = defaultContractAttachmentTypeOptions) => (rows || []).map(row => ({ ...row, - fileType: resolveContractAttachmentType(row), + fileType: resolveContractAttachmentType(row, options), description: row.description || '', })); +const mapDictOptions = response => { + const data = response?.data?.data || response?.data || []; + const records = Array.isArray(data) ? data : data.records || []; + return records + .map(item => ({ + label: item.dictValue, + value: item.dictKey, + })) + .filter(item => item.label && item.value); +}; + export default { name: 'ContractAttachmentSection', components: { UploadFilled }, @@ -242,6 +272,7 @@ export default { lockApproved: Boolean, markApprovedOnUpload: Boolean, useUploadDialog: Boolean, + attachmentLocation: { type: String, default: '合同文件' }, preview: { type: Function, default: null }, }, emits: ['update:rows'], @@ -249,9 +280,9 @@ export default { return { selected: [], attachmentFileTypes, - contractAttachmentTypeOptions, + contractAttachmentTypeOptions: [...defaultContractAttachmentTypeOptions], uploadDialogVisible: false, - uploadDialogType: CONTRACT_ATTACHMENT_TYPE_OTHER, + uploadDialogType: resolveOtherAttachmentType(defaultContractAttachmentTypeOptions), uploadDialogFiles: [], uploadDialogFileList: [], }; @@ -266,9 +297,32 @@ export default { acceptText() { return this.attachmentFileTypes.map(item => `.${item}`).join(','); }, + defaultAttachmentType() { + return resolveOtherAttachmentType(this.contractAttachmentTypeOptions); + }, + }, + created() { + this.loadAttachmentTypeOptions(); }, methods: { attachmentName, + loadAttachmentTypeOptions() { + return getBizDictionary({ code: 'contract_attachment_type' }) + .then(res => { + const options = mapDictOptions(res); + if (options.length) { + this.contractAttachmentTypeOptions = options; + if ( + !options.some(item => String(item.value) === String(this.uploadDialogType)) + ) { + this.uploadDialogType = this.defaultAttachmentType; + } + } + }) + .catch(() => { + // 字典加载失败时保留默认选项 + }); + }, isRowLocked(row) { return this.lockApproved && isAttachmentApproved(row); }, @@ -286,7 +340,7 @@ export default { this.update([...this.rows]); }, openUploadDialog() { - this.uploadDialogType = CONTRACT_ATTACHMENT_TYPE_OTHER; + this.uploadDialogType = this.defaultAttachmentType; this.uploadDialogFiles = []; this.uploadDialogFileList = []; this.uploadDialogVisible = true; @@ -294,7 +348,7 @@ export default { resetUploadDialog() { this.uploadDialogFiles = []; this.uploadDialogFileList = []; - this.uploadDialogType = CONTRACT_ATTACHMENT_TYPE_OTHER; + this.uploadDialogType = this.defaultAttachmentType; }, beforeUpload(file) { const extension = String(file.name || '') @@ -337,10 +391,13 @@ export default { ...this.uploadDialogFiles.filter(item => String(item.uid) !== String(file.uid)), normalized, ]; - this.uploadDialogType = resolveContractAttachmentType({ - ...normalized, - fileType: '', - }); + this.uploadDialogType = resolveContractAttachmentType( + { + ...normalized, + fileType: '', + }, + this.contractAttachmentTypeOptions + ); this.$message.success('上传成功'); }, handleDialogError() { @@ -359,7 +416,7 @@ export default { this.$message.warning('请先上传附件'); return; } - if (this.attachmentType && !this.uploadDialogType) { + if (!this.uploadDialogType) { this.$message.warning('请选择附件类型'); return; } @@ -367,7 +424,7 @@ export default { const uploadTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss'); const appended = this.uploadDialogFiles.map(row => ({ ...row, - fileType: this.attachmentType ? this.uploadDialogType : row.fileType, + fileType: this.uploadDialogType, description: row.description || '', uploadUserName: row.uploadUserName || uploadUserName, uploadTime: row.uploadTime || uploadTime, @@ -398,10 +455,13 @@ export default { approved: existing?.approved || row.approved || false, }; if (this.attachmentType) { - next.fileType = resolveContractAttachmentType({ - ...next, - fileType: existing?.fileType || row.fileType, - }); + next.fileType = resolveContractAttachmentType( + { + ...next, + fileType: existing?.fileType || row.fileType, + }, + this.contractAttachmentTypeOptions + ); } if (this.markApprovedOnUpload && !existing) next.approved = true; return next; @@ -441,6 +501,49 @@ export default { diff --git a/src/views/system/dept.vue b/src/views/system/dept.vue index 94cb306..aac5268 100644 --- a/src/views/system/dept.vue +++ b/src/views/system/dept.vue @@ -42,12 +42,6 @@ - - - - @@ -225,7 +219,7 @@ export default { }, rowSave(row, done, loading) { if (!this.selectedVehicleValue || row.plateNo !== this.selectedVehicleValue) { - this.$message.warning('请选择调度车辆'); + this.$message.warning('请选择车牌号'); loading(); return; } @@ -247,6 +241,8 @@ export default { .then(res => { this.form = res.data.data || {}; this.form.attachments = this.parseAttachments(this.form.attachments); + this.form.applicantName = this.form.createUserName || ''; + this.form.applyTime = this.form.createTime || ''; this.selectedVehicleValue = this.form.plateNo || ''; this.deptCascaderValue = this.findDepartmentPath(this.form.useDepartment); done(); @@ -258,9 +254,10 @@ export default { this.selectedVehicleValue = ''; this.form.attachments = []; this.form.approvalStatus = 'draft'; - this.form.createUserName = + this.form.vehicleType = ''; + this.form.applicantName = this.userInfo?.realName || this.userInfo?.userName || this.userInfo?.account || ''; - this.form.createTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss'); + this.form.applyTime = this.$dayjs().format('YYYY-MM-DD HH:mm:ss'); done(); } }, @@ -315,6 +312,7 @@ export default { records.map(item => ({ value: item.plateNo, organizationName: item.organizationName, + vehicleType: item.vehicleType, })) ); }) @@ -323,19 +321,22 @@ export default { handleVehicleSelect(vehicle) { this.form.plateNo = vehicle.value; this.form.organizationName = vehicle.organizationName || ''; + this.form.vehicleType = vehicle.vehicleType || ''; this.selectedVehicleValue = vehicle.value; }, handleVehicleInput(value) { if (value !== this.selectedVehicleValue) { this.selectedVehicleValue = ''; this.form.organizationName = ''; + this.form.vehicleType = ''; } }, handleVehicleBlur() { if (this.form.plateNo && this.form.plateNo !== this.selectedVehicleValue) { this.form.plateNo = ''; this.form.organizationName = ''; - this.$message.warning('请从下拉列表中选择调度车辆'); + this.form.vehicleType = ''; + this.$message.warning('请从下拉列表中选择车牌号'); } }, handleSubmitApproval(row) { diff --git a/src/views/vehicle/customer-archive.vue b/src/views/vehicle/customer-archive.vue index 9f438de..9c36d1a 100644 --- a/src/views/vehicle/customer-archive.vue +++ b/src/views/vehicle/customer-archive.vue @@ -79,11 +79,12 @@ 资金使用额度风险 - + + + @@ -927,6 +928,7 @@ width="680px" class="contact-dialog" @closed="resetContact" + @opened="syncContactRegionPath" > @@ -946,30 +948,41 @@ - -
- - -
+ + + + + + + + @@ -983,52 +996,6 @@ - -
- - - 搜索 - -
-
- -
-
-
- {{ contactMapStatus }} - - 行政区划:{{ contactMapSelected.regionName }} - -
- -
- item !== undefined && item !== null && item !== '') + : []; + const deptId = ids.length ? String(ids[ids.length - 1]) : ''; + this.contactForm.branchName = deptId ? this.findDeptLabel(deptId) : ''; + this.$refs.contactForm?.validateField('branchName'); + }, + }, ids() { let ids = []; this.selectionList.forEach(ele => { @@ -2650,11 +2621,9 @@ export default { contactName: '', contactPhone: '', email: '', + regionPath: [], regionName: '', - regionCode: '', detailAddress: '', - longitude: '', - latitude: '', contactAddress: '', branchName: '', positionName: '', @@ -2948,6 +2917,67 @@ export default { } return ''; }, + findDeptPathByName(name, tree = this.deptTree, parents = []) { + const target = String(name || '').trim(); + if (!target) return []; + for (const node of tree || []) { + const path = [...parents, String(node.value)]; + if (String(node.label || '').trim() === target) return path; + const matched = this.findDeptPathByName(name, node.children || [], path); + if (matched.length) return matched; + } + return []; + }, + getCurrentOrganizationName() { + const currentDeptId = String(this.userInfo.deptId || this.userInfo.dept_id || '') + .split(',') + .map(item => item.trim()) + .filter(Boolean)[0]; + if (currentDeptId) { + const label = this.findDeptLabel(currentDeptId); + if (label) return label; + } + return this.userInfo.deptName || this.userInfo.dept_name || ''; + }, + findRegionPathByName(options = [], regionName = '', parents = []) { + const target = String(regionName || '').replace(/\s+/g, ''); + if (!target) return []; + for (const item of options || []) { + const nextParents = [...parents, item]; + const nextName = nextParents.map(region => region.title || region.name || '').join(''); + if (nextName === target) { + return nextParents.map(region => region.id); + } + const childPath = this.findRegionPathByName(item.children, target, nextParents); + if (childPath.length) return childPath; + } + return []; + }, + getContactRegionLabels(value) { + const nodes = this.$refs.contactRegionCascader?.getCheckedNodes?.() || []; + if (nodes.length && nodes[0].pathLabels?.length) { + return nodes[0].pathLabels; + } + return (value || []) + .map(code => this.findRegionOption(this.regionOptions, code)?.title) + .filter(Boolean); + }, + handleContactRegionChange(value) { + if (!value || !value.length) { + this.contactForm.regionName = ''; + return; + } + this.$nextTick(() => { + this.contactForm.regionName = this.getContactRegionLabels(value).join(''); + }); + }, + syncContactRegionPath() { + if (!this.contactForm.regionName || this.contactForm.regionPath?.length) return; + const path = this.findRegionPathByName(this.regionOptions, this.contactForm.regionName); + if (path.length) { + this.contactForm.regionPath = path; + } + }, renderDeptSearch(scope) { // 搜索区所属组织:单选级联(与新增/编辑表单一致) // 级联返回的是根到选中节点的 ID 路径数组,提交时取末级 ID 转部门名称 @@ -3071,11 +3101,20 @@ export default { normalizeContact(contact = {}) { const contactAddress = contact.contactAddress || contact.address || this.formatContactAddress(contact); + const regionPath = Array.isArray(contact.regionPath) + ? contact.regionPath + : this.findRegionPathByName(this.regionOptions, contact.regionName); return { ...this.emptyContact(), ...contact, + regionPath, contactAddress, - branchName: contact.branchName || contact.deptName || this.archiveForm.deptName || '', + branchName: + contact.branchName || + contact.deptName || + this.getCurrentOrganizationName() || + this.archiveForm.deptName || + '', }; }, openContact(row, index = -1) { @@ -3084,9 +3123,10 @@ export default { ? this.normalizeContact(row) : { ...this.emptyContact(), - branchName: this.archiveForm.deptName || '', + branchName: this.getCurrentOrganizationName() || this.archiveForm.deptName || '', }; this.contactBox = true; + this.$nextTick(() => this.syncContactRegionPath()); }, resetContact() { this.contactIndex = -1; @@ -3100,6 +3140,7 @@ export default { ...this.contactForm, contactAddress: this.formatContactAddress(this.contactForm), }); + delete contact.regionPath; if (this.contactIndex > -1) { this.archiveForm.contacts.splice(this.contactIndex, 1, contact); } else { @@ -3121,279 +3162,6 @@ export default { }) .catch(() => {}); }, - handleContactMapPick() { - this.contactMapTarget = 'contact'; - this.contactMapKeyword = this.contactForm.detailAddress || this.contactForm.regionName || ''; - this.contactMapSelected = this.buildContactMapSelection({ - lng: this.contactForm.longitude, - lat: this.contactForm.latitude, - address: this.contactForm.detailAddress, - regionName: this.contactForm.regionName, - regionCode: this.contactForm.regionCode, - }); - this.contactMapStatus = this.contactMapSelected.longitude - ? '已加载当前选点,可重新选点' - : '可搜索地址或点击地图选点'; - this.contactMapBox = true; - }, - loadAmap() { - if (window.AMap && window.AMap.Map) { - return Promise.resolve(); - } - if (!amapLoader) { - amapLoader = new Promise((resolve, reject) => { - window._AMapSecurityConfig = { - securityJsCode: AMAP_SECURITY_CODE, - }; - const script = document.createElement('script'); - script.src = `https://webapi.amap.com/maps?v=2.0&key=${AMAP_KEY}`; - script.async = true; - script.onload = resolve; - script.onerror = () => reject(new Error('高德地图组件加载失败')); - document.body.appendChild(script); - }); - } - return amapLoader; - }, - initContactAmap() { - this.loadAmap() - .then(() => { - this.$nextTick(() => { - if (!this.contactAmap) { - const center = this.toLngLat( - this.contactMapSelected.longitude || 116.40769, - this.contactMapSelected.latitude || 39.89945 - ); - this.contactAmap = new window.AMap.Map(this.$refs.contactAmap, { - center, - zoom: this.contactMapSelected.longitude ? 14 : 11, - }); - window.AMap.plugin(['AMap.ToolBar', 'AMap.Geocoder'], () => { - this.contactAmap.addControl(new window.AMap.ToolBar()); - if (!this.contactAmapGeocoder) { - this.contactAmapGeocoder = new window.AMap.Geocoder(); - } - }); - this.contactAmap.on('click', event => this.pickContactMapPoint(event.lnglat)); - } else if (typeof this.contactAmap.resize === 'function') { - this.contactAmap.resize(); - } - if (this.contactMapSelected.longitude) { - this.renderContactMapMarker( - this.toLngLat(this.contactMapSelected.longitude, this.contactMapSelected.latitude) - ); - } else { - this.clearContactMapMarker(); - } - }); - }) - .catch(() => { - this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗'); - this.contactMapBox = false; - }); - }, - searchContactMapKeyword() { - const keyword = String(this.contactMapKeyword || '').trim(); - if (!keyword) { - this.$message.warning('请输入地址关键词'); - return; - } - this.loadAmap() - .then(() => { - this.contactMapLoading = true; - this.ensureContactAmapGeocoder() - .then(() => this.runAmapGeocode('location', keyword)) - .then(result => { - this.contactMapSearchResults = (result.geocodes || []).map((item, index) => ({ - id: item.id || index, - name: item.formattedAddress || keyword, - address: item.formattedAddress || keyword, - location: item.location, - })); - const point = this.resolveMapPoint(result); - if (!point) { - this.contactMapStatus = '未找到匹配地址'; - this.$message.warning('地图搜索无匹配地址'); - return; - } - this.pickContactMapPoint(point, keyword); - }) - .catch(() => { - this.contactMapStatus = '地图搜索失败'; - this.$message.error('地图搜索失败,请稍后重试'); - }) - .finally(() => { - this.contactMapLoading = false; - }); - }) - .catch(() => { - this.$message.error('高德地图组件加载失败,请稍后重试或重新打开弹窗'); - }); - }, - selectContactMapSearchResult(item) { - if (item && item.location) { - this.pickContactMapPoint(item.location, item.address || item.name || ''); - } - }, - pickContactMapPoint(lnglat, keyword) { - const longitude = this.getPointLng(lnglat); - const latitude = this.getPointLat(lnglat); - if (longitude === undefined || latitude === undefined) { - this.$message.warning('选点坐标无效'); - return; - } - const point = this.toLngLat(longitude, latitude); - this.renderContactMapMarker(point); - this.contactMapSelected = this.buildContactMapSelection({ - lng: longitude, - lat: latitude, - address: keyword, - }); - this.contactMapStatus = '正在反查地址...'; - this.ensureContactAmapGeocoder() - .then(() => this.runAmapGeocode('address', point)) - .then(result => { - const address = this.resolveMapAddress(result); - this.contactMapSelected = { - ...this.contactMapSelected, - detailAddress: address.detailAddress || this.contactMapSelected.detailAddress, - regionName: address.regionName || this.contactMapSelected.regionName, - regionCode: address.regionCode || this.contactMapSelected.regionCode, - }; - this.contactMapKeyword = this.contactMapSelected.detailAddress || this.contactMapKeyword; - this.contactMapStatus = this.contactMapSelected.detailAddress || '已选点,可确认回填'; - }) - .catch(() => { - this.contactMapStatus = '反查地址失败'; - this.$message.error('反查地址失败,请重新选点'); - }); - }, - renderContactMapMarker(point) { - if (!this.contactAmap || !window.AMap) return; - this.clearContactMapMarker(); - this.contactAmapMarker = new window.AMap.Marker({ - position: point, - }); - this.contactAmapMarker.setMap(this.contactAmap); - this.contactAmap.setCenter(point); - }, - clearContactMapMarker() { - if (this.contactAmapMarker) { - this.contactAmapMarker.setMap(null); - this.contactAmapMarker = null; - } - }, - confirmContactMapPick() { - if (!this.contactMapSelected.longitude) { - this.$message.warning('请先搜索或点击地图完成选点'); - return; - } - this.contactForm.detailAddress = - this.contactMapSelected.detailAddress || this.contactForm.detailAddress; - this.contactForm.regionName = - this.contactMapSelected.regionName || this.contactForm.regionName; - this.contactForm.regionCode = - this.contactMapSelected.regionCode || this.contactForm.regionCode; - this.contactForm.longitude = this.contactMapSelected.longitude; - this.contactForm.latitude = this.contactMapSelected.latitude; - this.$refs.contactForm?.validateField('detailAddress'); - this.contactMapBox = false; - }, - buildContactMapSelection({ lng, lat, address, regionName, regionCode }) { - const longitude = this.formatCoordinate(lng); - const latitude = this.formatCoordinate(lat); - return { - longitude, - latitude, - detailAddress: address || '', - regionName: regionName || '', - regionCode: regionCode || '', - }; - }, - formatCoordinate(value) { - if (value === undefined || value === null || value === '') { - return ''; - } - const numberValue = Number(value); - return Number.isFinite(numberValue) ? numberValue.toFixed(6) : ''; - }, - toLngLat(lng, lat) { - return new window.AMap.LngLat(Number(lng), Number(lat)); - }, - getPointLng(point) { - if (!point) return undefined; - if (typeof point.getLng === 'function') return point.getLng(); - return point.lng ?? point.lon; - }, - getPointLat(point) { - if (!point) return undefined; - if (typeof point.getLat === 'function') return point.getLat(); - return point.lat; - }, - ensureContactAmapGeocoder() { - if (this.contactAmapGeocoder) { - return Promise.resolve(this.contactAmapGeocoder); - } - return new Promise((resolve, reject) => { - if (!window.AMap || !window.AMap.plugin) { - reject(new Error('高德地图组件未就绪')); - return; - } - window.AMap.plugin(['AMap.Geocoder'], () => { - try { - this.contactAmapGeocoder = new window.AMap.Geocoder(); - resolve(this.contactAmapGeocoder); - } catch (error) { - reject(error); - } - }); - }); - }, - runAmapGeocode(action, input) { - return new Promise((resolve, reject) => { - const timer = window.setTimeout(() => { - reject(new Error('高德地图请求超时')); - }, 10000); - const done = (status, result) => { - window.clearTimeout(timer); - if (status === 'complete' && result) { - resolve(result); - return; - } - reject(new Error('高德地图请求失败')); - }; - try { - if (action === 'location') { - this.contactAmapGeocoder.getLocation(input, done); - } else { - this.contactAmapGeocoder.getAddress(input, done); - } - } catch (error) { - window.clearTimeout(timer); - reject(error); - } - }); - }, - resolveMapPoint(result) { - if (!result) return null; - const geocode = Array.isArray(result.geocodes) ? result.geocodes[0] : null; - if (geocode && geocode.location) return geocode.location; - if (result.location) return result.location; - if (result.lnglat) return result.lnglat; - if (result.getLng || result.lng || result.lon) return result; - return null; - }, - resolveMapAddress(result = {}) { - const regeocode = result.regeocode || {}; - const component = regeocode.addressComponent || {}; - const city = Array.isArray(component.city) ? '' : component.city; - const regionName = [component.province, city, component.district].filter(Boolean).join(''); - return { - detailAddress: regeocode.formattedAddress || '', - regionName, - regionCode: component.adcode || '', - }; - }, normalizeReceipt(receipt = {}) { return { ...this.emptyReceiptAccount(), @@ -4020,7 +3788,12 @@ export default { contacts: (detail.contacts || []).map(item => this.normalizeContact({ ...item, - branchName: item.branchName || item.deptName || detail.deptName || '', + branchName: + item.branchName || + item.deptName || + this.getCurrentOrganizationName() || + detail.deptName || + '', }) ), receiptAccounts: (detail.receiptAccounts || []).map(item => this.normalizeReceipt(item)), @@ -4226,12 +3999,14 @@ export default { delete archive.registeredRegionPath; archive.qualificationAttachments = this.stringifyAttachments(this.qualificationFiles); archive.contacts = (archive.contacts || []) - .map(item => - this.normalizeContact({ + .map(item => { + const contact = this.normalizeContact({ ...item, contactAddress: this.formatContactAddress(item), - }) - ) + }); + delete contact.regionPath; + return contact; + }) .filter(item => item.contactName || item.contactPhone); archive.receiptAccounts = (archive.receiptAccounts || []) .map(item => this.normalizeReceipt(item)) @@ -5048,6 +4823,12 @@ export default { :deep(.el-table td .cell) { white-space: nowrap; } + + // 表头含问号图标,避免被压缩成省略号 + :deep(.el-table th .cell:has(.fund-use-risk-header)) { + overflow: visible; + text-overflow: clip; + } } .archive-tag--deep-blue { @@ -5073,13 +4854,15 @@ export default { .fund-use-risk-header { display: inline-flex; align-items: center; + gap: 4px; + white-space: nowrap; } .fund-use-risk-help { - margin-left: 4px; color: #a8abb2; cursor: help; font-size: 14px; + flex: none; } .archive-form { @@ -5417,23 +5200,9 @@ export default { } } -.contact-form__address { - display: flex; - gap: 10px; - width: 100%; - - .el-input { - flex: 1; - min-width: 0; - } - - .el-input:first-child { - flex: 0 0 38%; - } -} - .contact-form__compact-field { - :deep(.el-input) { + :deep(.el-input), + :deep(.el-cascader) { width: 60%; } } @@ -5463,32 +5232,6 @@ export default { gap: 12px; } -.contact-form__map-toolbar { - display: flex; - gap: 10px; - margin-bottom: 12px; - - .el-input { - flex: 1; - } -} - -.contact-form__map { - width: 100%; - height: 460px; - border: 1px solid #dcdfe6; -} - -.contact-form__map-info { - display: flex; - justify-content: space-between; - gap: 16px; - min-height: 22px; - margin-top: 10px; - color: #606266; - font-size: 13px; -} - .score-detail-table { width: 100%; margin-top: 12px;