From 57368f0179bd54050fe88a5f7820219e2cb61f1a Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Mon, 21 Sep 2026 11:14:51 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E8=AF=95mk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/mk-process.js | 23 ++ src/api/system/business-process.js | 13 + src/router/page/index.js | 66 +++++ src/utils/mk-approval.js | 78 +++++ .../components/waybill-manage-page.vue | 49 +++- src/views/business/contract-manage-change.vue | 3 +- src/views/business/contract-manage.vue | 43 ++- src/views/business/project-apply.vue | 56 +++- src/views/mk/public-biz-view.vue | 271 ++++++++++++++++++ .../payment/payment-application-form.vue | 7 + src/views/payment/payment-application.vue | 7 + .../components/pre-settlement-editor.vue | 7 + src/views/settlement/formal-settlement.vue | 7 + src/views/system/user.vue | 24 +- src/views/vehicle/customer-archive.vue | 32 ++- 15 files changed, 647 insertions(+), 39 deletions(-) create mode 100644 src/api/mk-process.js create mode 100644 src/utils/mk-approval.js create mode 100644 src/views/mk/public-biz-view.vue diff --git a/src/api/mk-process.js b/src/api/mk-process.js new file mode 100644 index 0000000..c70ffc6 --- /dev/null +++ b/src/api/mk-process.js @@ -0,0 +1,23 @@ +import request from '@/axios'; + +export const getMkPublicDetail = (bizType, id) => { + return request({ + url: `/blade-transport/mk-process/public/${bizType}/detail`, + method: 'get', + meta: { + isToken: false, + }, + params: { id }, + }); +}; + +export const postMkPublicProcessMessage = (bizType, data) => { + return request({ + url: `/blade-transport/mk-process/public/${bizType}/process-message`, + method: 'post', + meta: { + isToken: false, + }, + data, + }); +}; diff --git a/src/api/system/business-process.js b/src/api/system/business-process.js index 3cfcd67..44f9526 100644 --- a/src/api/system/business-process.js +++ b/src/api/system/business-process.js @@ -16,3 +16,16 @@ export const processSubmit = data => { data, }); }; + +/** + * 调用 MK processDelete 删除审核流(驳回后重新提交前使用) + * @param {Object} data + * @param {string} data.formInstanceId 业务表单实例 id + */ +export const processDelete = data => { + return request({ + url: '/blade-system/businessProcess/processDelete', + method: 'post', + data, + }); +}; diff --git a/src/router/page/index.js b/src/router/page/index.js index 3b8f319..cbb2e72 100644 --- a/src/router/page/index.js +++ b/src/router/page/index.js @@ -84,6 +84,72 @@ export default [ isAuth: false, }, }, + { + path: '/business/project-apply/public-view', + name: '查看项目信息', + component: () => import('@/views/mk/public-biz-view.vue'), + meta: { + keepAlive: false, + isTab: false, + isAuth: false, + bizType: 'project-apply', + }, + }, + { + path: '/business/contract-manage/public-view', + name: '查看合同信息', + component: () => import('@/views/mk/public-biz-view.vue'), + meta: { + keepAlive: false, + isTab: false, + isAuth: false, + bizType: 'contract-manage', + }, + }, + { + path: '/business/waybill-manage/public-view', + name: '查看运单信息', + component: () => import('@/views/mk/public-biz-view.vue'), + meta: { + keepAlive: false, + isTab: false, + isAuth: false, + bizType: 'waybill-manage', + }, + }, + { + path: '/settlement/pre-settlement/public-view', + name: '查看预结算信息', + component: () => import('@/views/mk/public-biz-view.vue'), + meta: { + keepAlive: false, + isTab: false, + isAuth: false, + bizType: 'pre-settlement', + }, + }, + { + path: '/settlement/formal-settlement/public-view', + name: '查看正式结算信息', + component: () => import('@/views/mk/public-biz-view.vue'), + meta: { + keepAlive: false, + isTab: false, + isAuth: false, + bizType: 'formal-settlement', + }, + }, + { + path: '/payment/payment-application/public-view', + name: '查看付款申请信息', + component: () => import('@/views/mk/public-biz-view.vue'), + meta: { + keepAlive: false, + isTab: false, + isAuth: false, + bizType: 'payment-application', + }, + }, { path: '/', name: '主页', diff --git a/src/utils/mk-approval.js b/src/utils/mk-approval.js new file mode 100644 index 0000000..a8544e7 --- /dev/null +++ b/src/utils/mk-approval.js @@ -0,0 +1,78 @@ +import { getDictionary } from '@/api/system/dictbiz'; +import { processDelete, processSubmit } from '@/api/system/business-process'; +import { ElMessage } from 'element-plus'; + +export const MK_BIZ = { + 'customer-archive': { + dictName: '提交客商审核流', + subjectPrefix: '客商准入审批', + rejected: ['rejected'], + }, + 'project-apply': { + dictName: '提交项目审核流', + subjectPrefix: '项目审批', + rejected: ['rejected', 'change_rejected', 'withdrawn'], + }, + 'contract-manage': { + dictName: '提交合同审核流', + subjectPrefix: '合同审批', + rejected: ['rejected', 'change_rejected', 'withdrawn'], + }, + 'waybill-manage': { + dictName: '提交运单审核流', + subjectPrefix: '运单审批', + rejected: ['rejected', 'returned'], + }, + 'pre-settlement': { + dictName: '提交预结算审核流', + subjectPrefix: '预结算审批', + rejected: ['returned'], + }, + 'formal-settlement': { + dictName: '提交正式结算审核流', + subjectPrefix: '正式结算审批', + rejected: ['returned'], + }, + 'payment-application': { + dictName: '提交付款审核流', + subjectPrefix: '付款审批', + rejected: ['returned'], + }, +}; + +export async function resolveMkTemplateCode(dictName) { + const res = await getDictionary({ code: 'mk_template' }); + const list = res?.data?.data || []; + const matched = list.find(item => String(item.dictValue || '').trim() === dictName); + const templateCode = matched?.dictKey; + if (!templateCode) { + ElMessage.warning(`未配置业务字典 mk_template「${dictName}」,无法提交审核流`); + return Promise.reject(new Error(`未配置业务字典 mk_template「${dictName}」`)); + } + return String(templateCode); +} + +export async function submitMkApprovalFlow({ + bizType, + formInstanceId, + subjectName = '', + approvalStatus = '', +}) { + const conf = MK_BIZ[bizType]; + if (!conf) { + return Promise.reject(new Error(`不支持的MK业务类型:${bizType}`)); + } + if ((conf.rejected || []).includes(approvalStatus)) { + await processDelete({ formInstanceId: String(formInstanceId) }); + } + const templateCode = await resolveMkTemplateCode(conf.dictName); + const subject = subjectName + ? `${conf.subjectPrefix}:${subjectName}` + : `${conf.subjectPrefix}:${formInstanceId}`; + return processSubmit({ + templateCode, + formInstanceId: String(formInstanceId), + subject, + bizType, + }); +} diff --git a/src/views/business/components/waybill-manage-page.vue b/src/views/business/components/waybill-manage-page.vue index cc64540..67d0c7d 100644 --- a/src/views/business/components/waybill-manage-page.vue +++ b/src/views/business/components/waybill-manage-page.vue @@ -3128,6 +3128,7 @@ import { getDictionary as getSystemDictionary } from '@/api/system/dict'; import { addressTypeOptions } from '@/option/base/common-address'; import { packageOptions } from '@/option/business/common'; import { formatUpdateUserName } from '@/utils/audit'; +import { submitMkApprovalFlow } from '@/utils/mk-approval'; import { getToken } from '@/utils/auth'; import { openImportDialog } from '@/utils/import-excel'; import { applyTableMenuWidth } from '@/utils/table-menu'; @@ -5521,11 +5522,23 @@ export default { return; } this.getSaveRequest()(submitRow).then( - () => { - this.onLoad(this.page); - this.$message({ type: 'success', message: '操作成功!' }); - done(); - if (this.isStandaloneWaybillPage) this.closeCrudDialog(); + res => { + const data = res.data?.data || {}; + const id = data.id || submitRow.id; + const after = id + ? submitMkApprovalFlow({ + bizType: 'waybill-manage', + formInstanceId: id, + subjectName: data.waybillNo || submitRow.waybillNo || '', + approvalStatus: data.approvalStatus || submitRow.approvalStatus || '', + }) + : Promise.resolve(); + return after.then(() => { + this.onLoad(this.page); + this.$message({ type: 'success', message: '提交成功!' }); + done(); + if (this.isStandaloneWaybillPage) this.closeCrudDialog(); + }); }, error => { window.console.log(error); @@ -5544,12 +5557,26 @@ export default { ? this.api.updateAttachments : this.getSaveRequest(); request(submitRow).then( - () => { - this.attachmentUploadMode = false; - this.onLoad(this.page); - this.$message({ type: 'success', message: '操作成功!' }); - done(); - if (this.isStandaloneWaybillPage) this.closeCrudDialog(); + res => { + const skipMk = this.attachmentUploadMode; + const data = res.data?.data || {}; + const id = data.id || submitRow.id; + const after = + skipMk || !id + ? Promise.resolve() + : submitMkApprovalFlow({ + bizType: 'waybill-manage', + formInstanceId: id, + subjectName: data.waybillNo || submitRow.waybillNo || '', + approvalStatus: data.approvalStatus || submitRow.approvalStatus || '', + }); + return after.then(() => { + this.attachmentUploadMode = false; + this.onLoad(this.page); + this.$message({ type: 'success', message: skipMk ? '操作成功!' : '提交成功!' }); + done(); + if (this.isStandaloneWaybillPage) this.closeCrudDialog(); + }); }, error => { this.attachmentUploadMode = false; diff --git a/src/views/business/contract-manage-change.vue b/src/views/business/contract-manage-change.vue index c325a2f..b6fd16f 100644 --- a/src/views/business/contract-manage-change.vue +++ b/src/views/business/contract-manage-change.vue @@ -302,6 +302,7 @@ diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index 044a401..88bcde9 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -1035,6 +1035,7 @@ import { getDictionary as getBizDictionary } from '@/api/system/dictbiz'; import { config, option } from '@/option/business/contract-manage'; import { getToken } from '@/utils/auth'; import { downloadFileByUrl, downloadXls } from '@/utils/util'; +import { submitMkApprovalFlow } from '@/utils/mk-approval'; import BillingPlanEditor from './components/billing-plan-editor.vue'; import ContractAttachmentSection, { attachmentName as sharedAttachmentName, @@ -1824,9 +1825,17 @@ export default { const id = saved.id || this.form.id; if (action === 'temporary' || action === 'formal') { if (!id) throw new Error('合同保存成功但未返回主键,无法提交合同'); - return action === 'temporary' + return (action === 'temporary' ? this.api.toTemporary(id) - : this.api.submitFormal(id); + : this.api.submitFormal(id) + ).then(() => + submitMkApprovalFlow({ + bizType: 'contract-manage', + formInstanceId: id, + subjectName: saved.contractName || submit.contractName || '', + approvalStatus: saved.approvalStatus || submit.approvalStatus || '', + }) + ); } return null; }) @@ -1853,8 +1862,20 @@ export default { : this.api.submit(payload); request .then(() => { - this.$message.success(isChangeRejected ? '已重新提交变更审批' : '修改成功'); - this.closeForm(); + if (!isChangeRejected) { + this.$message.success('修改成功'); + this.closeForm(); + return; + } + return submitMkApprovalFlow({ + bizType: 'contract-manage', + formInstanceId: payload.id || this.form.id, + subjectName: payload.contractName || this.form.contractName || '', + approvalStatus: this.form.approvalStatus || 'change_rejected', + }).then(() => { + this.$message.success('已重新提交变更审批'); + this.closeForm(); + }); }) .finally(() => { this.submitAction = ''; @@ -2698,7 +2719,19 @@ export default { } const run = value => { const args = operation.prompt ? [row.id, value] : [row.id]; - this.api[operation.action](...args).then(() => { + const request = () => this.api[operation.action](...args); + const afterMk = + operation.action === 'submitFormal' + ? request().then(() => + submitMkApprovalFlow({ + bizType: 'contract-manage', + formInstanceId: row.id, + subjectName: row.contractName || '', + approvalStatus: row.approvalStatus || '', + }) + ) + : request(); + afterMk.then(() => { this.$message.success(operation.successMessage || `${operation.label}成功`); this.onLoad(this.page, this.query); }); diff --git a/src/views/business/project-apply.vue b/src/views/business/project-apply.vue index fe2b8e2..12d2e22 100644 --- a/src/views/business/project-apply.vue +++ b/src/views/business/project-apply.vue @@ -925,6 +925,7 @@ import { getDictionary as getBizDictionary } from '@/api/system/dictbiz'; import { getList as getUserList } from '@/api/system/user'; import { getToken } from '@/utils/auth'; import { downloadFileByUrl, downloadXls } from '@/utils/util'; +import { submitMkApprovalFlow } from '@/utils/mk-approval'; import { ElImageViewer } from 'element-plus'; import { OpenFileViewer } from '@open-file-viewer/vue'; import PdfPreview from '@/components/pdf-preview/main.vue'; @@ -1589,7 +1590,17 @@ export default { } const run = value => { const args = operation.prompt ? [row.id, value] : [row.id]; - this.api[operation.action](...args).then(res => { + const request = () => this.api[operation.action](...args); + const afterMk = + operation.action === 'submitApproval' + ? submitMkApprovalFlow({ + bizType: 'project-apply', + formInstanceId: row.id, + subjectName: row.projectName || '', + approvalStatus: row.approvalStatus || '', + }).then(() => request()) + : request(); + afterMk.then(res => { if (res.data?.success === false || res.data?.data === false) { this.$message.error(res.data?.msg || `${operation.label}失败,请联系管理员`); return; @@ -1757,9 +1768,26 @@ export default { this.$message.error(res.data?.msg || '保存失败,请联系管理员'); return; } - this.$message.success('操作成功!'); - this.closeProjectForm(); - this.onLoad(this.page, this.query); + const data = res.data?.data || {}; + const id = data.id || this.form.id; + const name = data.projectName || this.form.projectName || ''; + const status = data.approvalStatus || this.form.approvalStatus || ''; + if (!id) { + this.$message.success('操作成功!'); + this.closeProjectForm(); + this.onLoad(this.page, this.query); + return; + } + return submitMkApprovalFlow({ + bizType: 'project-apply', + formInstanceId: id, + subjectName: name, + approvalStatus: status, + }).then(() => { + this.$message.success('提交成功!'); + this.closeProjectForm(); + this.onLoad(this.page, this.query); + }); }, error => { window.console.log(error); @@ -1827,9 +1855,23 @@ export default { ); return; } - this.$message.success(`${needSubmit ? '提交' : '保存'}成功!`); - this.closeProjectForm(); - this.onLoad(this.page, this.query); + if (!needSubmit) { + this.$message.success('保存成功!'); + this.closeProjectForm(); + this.onLoad(this.page, this.query); + return; + } + const id = this.form.id; + return submitMkApprovalFlow({ + bizType: 'project-apply', + formInstanceId: id, + subjectName: this.form.projectName || '', + approvalStatus: this.form.approvalStatus || 'change_rejected', + }).then(() => { + this.$message.success('提交成功!'); + this.closeProjectForm(); + this.onLoad(this.page, this.query); + }); }) .finally(() => { this.submitLoading = false; diff --git a/src/views/mk/public-biz-view.vue b/src/views/mk/public-biz-view.vue new file mode 100644 index 0000000..1dcd457 --- /dev/null +++ b/src/views/mk/public-biz-view.vue @@ -0,0 +1,271 @@ + + + + + + + diff --git a/src/views/payment/payment-application-form.vue b/src/views/payment/payment-application-form.vue index 13cee87..6adff7c 100644 --- a/src/views/payment/payment-application-form.vue +++ b/src/views/payment/payment-application-form.vue @@ -608,6 +608,7 @@ import { } from '@/api/vehicle/customer-archive'; import { readSettlementTransfer, removeSettlementTransfer } from '@/utils/settlement-transfer'; import { downloadFileByUrl } from '@/utils/util'; +import { submitMkApprovalFlow } from '@/utils/mk-approval'; import PdfPreview from '@/components/pdf-preview/main.vue'; const attachmentViewerPlugins = [ @@ -2354,6 +2355,12 @@ export default { const saved = await this.saveDraft(true); if (!saved) return; await api.submit({ id: this.form.id }); + await submitMkApprovalFlow({ + bizType: 'payment-application', + formInstanceId: this.form.id, + subjectName: this.form.paymentNo || '', + approvalStatus: this.form.approvalStatus || '', + }); this.$message.success('提交成功'); this.goBack(); }, diff --git a/src/views/payment/payment-application.vue b/src/views/payment/payment-application.vue index 9fb193b..c18379f 100644 --- a/src/views/payment/payment-application.vue +++ b/src/views/payment/payment-application.vue @@ -233,6 +233,7 @@ import { mapGetters } from 'vuex'; import * as api from '@/api/payment/paymentApplication'; import { paymentApplicationTableColumns } from '@/option/payment/paymentApplication'; import organizationSearch from '@/mixins/organization-search'; +import { submitMkApprovalFlow } from '@/utils/mk-approval'; const emptyQuery = () => ({ paymentNo: '', @@ -361,6 +362,12 @@ export default { }, async handleSubmit(row) { await api.submit({ id: row.id }); + await submitMkApprovalFlow({ + bizType: 'payment-application', + formInstanceId: row.id, + subjectName: row.paymentNo || '', + approvalStatus: row.approvalStatus || '', + }); this.$message.success('提交成功'); this.loadTable(); }, diff --git a/src/views/settlement/components/pre-settlement-editor.vue b/src/views/settlement/components/pre-settlement-editor.vue index 61a80e3..c90624f 100644 --- a/src/views/settlement/components/pre-settlement-editor.vue +++ b/src/views/settlement/components/pre-settlement-editor.vue @@ -955,6 +955,7 @@ import { settlementSummaryColumns, } from '@/option/settlement/preSettlementTable'; import { downloadFileByUrl } from '@/utils/util'; +import { submitMkApprovalFlow } from '@/utils/mk-approval'; import * as XLSX from 'xlsx'; export default { @@ -1553,6 +1554,12 @@ export default { await this.persistPendingAdjustments(); if (shouldSubmit) { await submit({ id: this.form.id }); + await submitMkApprovalFlow({ + bizType: 'pre-settlement', + formInstanceId: this.form.id, + subjectName: this.form.preSettlementNo || this.form.contractName || '', + approvalStatus: this.form.approvalStatus || '', + }); this.$message.success('审批流程已发起'); this.visible = false; this.$emit('success', this.form.id); diff --git a/src/views/settlement/formal-settlement.vue b/src/views/settlement/formal-settlement.vue index 002b5d5..985e1a0 100644 --- a/src/views/settlement/formal-settlement.vue +++ b/src/views/settlement/formal-settlement.vue @@ -182,6 +182,7 @@ import organizationSearch from '@/mixins/organization-search'; import { formalSettlementTableColumns } from '@/option/settlement/formalSettlementTable'; import FormalSettlementEditor from './components/formal-settlement-editor.vue'; import FormalSettlementTablePanel from './components/formal-settlement-table-panel.vue'; +import { submitMkApprovalFlow } from '@/utils/mk-approval'; const emptyQuery = () => ({ formalSettlementNo: '', @@ -337,6 +338,12 @@ export default { async handleSubmit(row) { await this.$confirm('提交后来源预结算将保持锁定,确认提交?', '提示', { type: 'warning' }); await api.submit({ id: row.id }); + await submitMkApprovalFlow({ + bizType: 'formal-settlement', + formInstanceId: row.id, + subjectName: row.formalSettlementNo || '', + approvalStatus: row.approvalStatus || '', + }); this.$message.success('提交成功'); this.loadTable(); }, diff --git a/src/views/system/user.vue b/src/views/system/user.vue index 70247da..107f5be 100644 --- a/src/views/system/user.vue +++ b/src/views/system/user.vue @@ -198,6 +198,17 @@ + + + + + @@ -544,19 +555,16 @@ export default { } return undefined; }, - // 所属组织级联配置:checkStrictly 为 false 时只能选择最后一级(叶子节点) + // 所属组织级联配置:checkStrictly 为 true 时任意一级都可选择 deptCascaderProps() { return { label: 'title', value: 'id', children: 'children', emitPath: false, - checkStrictly: false, + checkStrictly: true, expandTrigger: 'click', - leaf: (data, node) => { - // 判断是否为叶子节点:没有 children 或 children 为空数组 - return !data.children || data.children.length === 0; - }, + leaf: data => !data.children || data.children.length === 0, }; }, // 所属组织为单选级联:form.deptId 为数组(提交时 join),级联返回单个 id 值 @@ -835,6 +843,7 @@ export default { if (mode === 'add') { this.form = { account: '', + password: '', realName: '', phone: '', email: '', @@ -890,6 +899,9 @@ export default { roleId: func.join(this.form.roleId), leaderId: func.join(this.form.leaderId), }; + if (this.userDialogMode !== 'add') { + delete row.password; + } const request = this.userDialogMode === 'add' ? add(row) : update(this.sensitiveManager.getSubmitData(row)); request.then(() => { this.userDialog = false; diff --git a/src/views/vehicle/customer-archive.vue b/src/views/vehicle/customer-archive.vue index fcf3d08..e6e507d 100644 --- a/src/views/vehicle/customer-archive.vue +++ b/src/views/vehicle/customer-archive.vue @@ -1500,7 +1500,7 @@ import { getDetail as getCreditScoreQuantificationDetail, } from '@/api/vehicle/credit-score-quantification'; import { getDictionary } from '@/api/system/dictbiz'; -import { processSubmit } from '@/api/system/business-process'; +import { processDelete, processSubmit } from '@/api/system/business-process'; import { getDeptTree } from '@/api/system/dept'; import { getLazyTree } from '@/api/base/region'; import { exportBlob } from '@/api/common'; @@ -4152,7 +4152,11 @@ export default { archive.fullName || this.archiveForm.fullName || ''; - this.submitCustomerApproval(id, fullName).then(() => { + this.submitCustomerApproval( + id, + fullName, + archive.approvalStatus || this.archiveForm.approvalStatus + ).then(() => { this.closeArchive(); this.onLoad(this.page); this.$message({ type: 'success', message: '提交成功!' }); @@ -4163,15 +4167,19 @@ export default { /** * 列表 / 新增 / 编辑提交共用:提交 MK 审核流并更新客商审批状态 */ - submitCustomerApproval(id, fullName = '') { - return this.submitMkApprovalFlow(id, fullName).then(() => submitApproval(id)); + submitCustomerApproval(id, fullName = '', approvalStatus = '') { + return this.submitMkApprovalFlow(id, fullName, approvalStatus).then(() => submitApproval(id)); }, /** - * 提交 MK 审核流:templateCode 取业务字典 mk_template 中「提交审核流」的键值 + * 提交 MK 审核流:templateCode 取业务字典 mk_template 中「提交客商审核流」的键值 * 提交人手机号由后端从用户表读取真实值(前端接口会脱敏) + * MK 驳回后再提交时,先删除旧流程再创建新流程 */ - async submitMkApprovalFlow(formInstanceId, subjectName = '') { - const templateCode = await this.resolveMkTemplateCode('提交审核流'); + async submitMkApprovalFlow(formInstanceId, subjectName = '', approvalStatus = '') { + if (approvalStatus === 'rejected') { + await processDelete({ formInstanceId: String(formInstanceId) }); + } + const templateCode = await this.resolveMkTemplateCode('提交客商审核流'); const subject = subjectName ? `客商准入审批:${subjectName}` : `客商准入审批:${formInstanceId}`; @@ -4181,7 +4189,7 @@ export default { subject, }); }, - async resolveMkTemplateCode(dictName = '提交审核流') { + async resolveMkTemplateCode(dictName = '提交客商审核流') { const res = await getDictionary({ code: 'mk_template' }); const list = res?.data?.data || []; const matched = list.find(item => String(item.dictValue || '').trim() === dictName); @@ -4782,7 +4790,13 @@ export default { cancelButtonText: '取消', type: 'warning', }) - .then(() => this.submitCustomerApproval(row.id, archive.fullName || row.fullName)) + .then(() => + this.submitCustomerApproval( + row.id, + archive.fullName || row.fullName, + archive.approvalStatus || row.approvalStatus + ) + ) .then(() => { this.onLoad(this.page); this.$message({ type: 'success', message: '提交成功!' });