diff --git a/.workbuddy/memory/2026-08-31.md b/.workbuddy/memory/2026-08-31.md new file mode 100644 index 0000000..a3896a4 --- /dev/null +++ b/.workbuddy/memory/2026-08-31.md @@ -0,0 +1,12 @@ +# 2026-08-31 + +## 客商档案「所属组织」改为单选级联(customer-archive.vue) +- 表单区 `el-tree-select`(多选勾选树)→ `el-cascader` 单选级联;`deptCascaderProps` 开 `checkStrictly: true`,可任意层级选中(不必选到叶子)。 +- 引入 computed `deptCascaderValue`(get/set)做「部门 ID ↔ 根到节点路径」双向转换: + - `archiveForm.deptId` 仍为选中部门 ID 数组(单选只一个),后端字段 `deptIds` 逗号串 / `deptName` 不变,保存逻辑 `normalizeArchive` 无需改。 + - getter 依赖 `deptTree`,部门树异步加载完成后详情回显自动刷新(解决 created 里 initDeptTree 与 openArchive 并发的时序问题)。 +- `onDeptChange(path)` 改为取路径末级 → 写 `deptId / deptIds / deptName`;新增 `findDeptPath(id)`、`findDeptLabel(id)` 递归查找。 +- `normalizeDeptIds(detail)` 单选只取第一个 ID(历史多选数据自动收敛)。 +- 列表搜索区 `renderDeptSearch` 同步改为 `h(ElCascader, ...)`(import 从 ElTreeSelect 换 ElCascader),`searchChange` 取路径末级 ID 转部门名称。 + + diff --git a/src/mixins/organization-search.js b/src/mixins/organization-search.js index 10f3061..5842986 100644 --- a/src/mixins/organization-search.js +++ b/src/mixins/organization-search.js @@ -1,11 +1,11 @@ import { getDeptTree } from '@/api/system/dept'; -import { ElTreeSelect } from 'element-plus'; +import { ElCascader } from 'element-plus'; import { h } from 'vue'; /** * 所属组织 统一搜索 mixin * ------------------------------------------------------------------ - * 全站「所属组织」搜索栏统一为树形下拉(el-tree-select),风格与客户档案 + * 全站「所属组织」搜索栏统一为级联选择器(el-cascader,单选),风格与客商档案 * (vehicle/customer-archive.vue)保持一致。 * * 接入步骤(在使用了 avue-crud 的页面中): @@ -15,24 +15,40 @@ import { h } from 'vue'; * * 说明: * - 自动识别 organizationName / deptName 两种 prop,并挂载 column.renderSearch - * - 树节点标识统一用 id(node-key='id'),返回值为部门 id + * - 树节点标识统一用 id,级联值为「根 → 选中节点」的 id 路径数组 + * - checkStrictly 打开,任意层级(含非末级)都可直接选中 * - 页面若已有 excludeExternalOrganization / normalizeOrgTree 等方法, * 组件自身方法优先级更高,会覆盖本 mixin 的默认实现 - * - 搜索值(id)需在各自的 searchChange / normalizeSearch 中按后端要求 - * 转换为部门名称(参考 contract-manage 的 normalizeSearch) + * - 搜索值需在各自的 searchChange / normalizeSearch 中按后端要求转换为部门名称: + * 级联值是路径数组,统一用 findOrgById(路径)?.rawLabel 取末级名称 + * - 表单区(新增/编辑)需要回显时,可用 findOrgPath(deptId) 把 id 转成路径, + * 参考 customer-archive 的 deptCascaderValue 计算属性 */ const ORG_PROPS = ['organizationName', 'deptName']; export default { data() { return { - // 树形组织数据,仅供搜索栏 el-tree-select 使用; + // 树形组织数据,仅供搜索栏 el-cascader 使用; // 与页面自身 organizationOptions(多为表单扁平列表)隔离,避免相互覆盖 organizationTreeOptions: [], organizationTreeFlatOptions: [], organizationLoading: false, }; }, + computed: { + // 所属组织级联配置:checkStrictly 允许选中任意层级,不必选到末级 + organizationCascaderProps() { + return { + value: 'id', + label: 'label', + children: 'children', + emitPath: true, + checkStrictly: true, + expandTrigger: 'click', + }; + }, + }, methods: { loadOrganizationOptions() { if (this.organizationLoading) return; @@ -90,25 +106,67 @@ export default { ...this.flattenOrgTree(item.children || []), ]); }, + // 兼容两种入参:级联路径数组(取末级)或单个部门 id findOrgById(id) { const value = Array.isArray(id) ? id.at(-1) : id; return this.organizationTreeFlatOptions.find(item => String(item.id) === String(value)); }, + // 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null) + findOrgPath(id, tree = this.organizationTreeOptions, parents = []) { + const target = Array.isArray(id) ? id.at(-1) : id; + if (target === undefined || target === null || target === '') return null; + for (const node of tree || []) { + const path = [...parents, String(node.id)]; + if (String(node.id) === String(target)) return path; + const matched = this.findOrgPath(target, node.children, path); + if (matched) return matched; + } + return null; + }, + // 由部门 id 取组织名称(末级),供表单提交 + findOrgName(id) { + return this.findOrgById(id)?.rawLabel || ''; + }, + // 组织名称 → 级联路径(表单回显用;部门树异步加载完成后会自动重算) + resolveOrgPath(name) { + const value = String(name ?? '').trim(); + if (!value) return []; + const node = this.organizationTreeFlatOptions.find(item => item.rawLabel === value); + return node ? this.findOrgPath(node.id) || [] : []; + }, + // 级联路径 → 组织名称(表单/搜索提交用,取末级节点名称) + resolveOrgName(path) { + const value = Array.isArray(path) + ? path.filter(item => item !== undefined && item !== null && item !== '') + : []; + return value.length ? this.findOrgById(value)?.rawLabel || '' : ''; + }, + // 搜索提交前把级联路径数组就地转成组织名称(后端按名称过滤) + normalizeOrganizationSearch(params, prop = 'organizationName') { + if (Array.isArray(params?.[prop])) { + params[prop] = this.resolveOrgName(params[prop]); + } + return params; + }, renderOrganizationSearch(scope, prop = 'organizationName') { - return h(ElTreeSelect, { - modelValue: scope.row?.[prop] || '', + return h(ElCascader, { + modelValue: Array.isArray(scope.row?.[prop]) ? scope.row[prop] : [], 'onUpdate:modelValue': value => { - if (scope.row) scope.row[prop] = value; + // 级联单选:modelValue 必须是 id 路径数组,否则控件内部状态会错乱导致无法再次选中 + if (scope.row) scope.row[prop] = Array.isArray(value) ? value : []; + }, + options: this.organizationTreeOptions, + props: { + label: 'label', + value: 'id', + children: 'children', + checkStrictly: true, + expandTrigger: 'click', }, - data: this.organizationTreeOptions, - 'node-key': 'id', - 'check-strictly': true, filterable: true, clearable: true, - 'render-after-expand': false, style: 'width: 100%', placeholder: '请选择 所属组织', - props: { label: 'label', value: 'id', children: 'children' }, }); }, }, diff --git a/src/option/settlement/formalSettlementSearch.js b/src/option/settlement/formalSettlementSearch.js index d516fa6..abe3520 100644 --- a/src/option/settlement/formalSettlementSearch.js +++ b/src/option/settlement/formalSettlementSearch.js @@ -38,7 +38,7 @@ export const formalSettlementSearchFields = [ { label: '结算单号', prop: 'formalSettlementNo', type: 'input' }, { label: '预结算单号', prop: 'preSettlementNo', type: 'input' }, { label: '项目', prop: 'projectName', type: 'input' }, - { label: '所属组织', prop: 'deptName', type: 'input' }, + { label: '所属组织', prop: 'deptName', type: 'cascader' }, { label: '合同编号', prop: 'contractNo', type: 'input' }, { label: '付款方', prop: 'payerName', type: 'input' }, { label: '收款方', prop: 'payeeName', type: 'input' }, diff --git a/src/option/settlement/preSettlementSearch.js b/src/option/settlement/preSettlementSearch.js index 8e57a0e..1165ae9 100644 --- a/src/option/settlement/preSettlementSearch.js +++ b/src/option/settlement/preSettlementSearch.js @@ -10,7 +10,7 @@ export const preSettlementSearchFields = [ { label: '预结算单号', prop: 'preSettlementNo', type: 'input' }, { label: '预付单号', prop: 'advanceNo', type: 'input' }, { label: '项目名称', prop: 'projectName', type: 'input' }, - { label: '所属组织', prop: 'deptName', type: 'input' }, + { label: '所属组织', prop: 'deptName', type: 'cascader' }, { label: '合同名称', prop: 'contractName', type: 'input' }, { label: '合同编号', prop: 'contractNo', type: 'input' }, { label: '收款方', prop: 'payeeName', type: 'input' }, diff --git a/src/option/settlement/receivable-payable-detail.js b/src/option/settlement/receivable-payable-detail.js index 9a5c212..dd91680 100644 --- a/src/option/settlement/receivable-payable-detail.js +++ b/src/option/settlement/receivable-payable-detail.js @@ -14,7 +14,7 @@ export const searchFields = [ { label: '单据号', prop: 'documentNo', type: 'input' }, { label: '生成日期', prop: 'generateDateRange', type: 'daterange' }, { label: '客户名称', prop: 'customerName', type: 'input' }, - { label: '所属组织', prop: 'deptName', type: 'input' }, + { label: '所属组织', prop: 'deptName', type: 'cascader' }, { label: '项目名称', prop: 'projectName', type: 'input' }, { label: '货物类型', prop: 'cargoType', type: 'input' }, { label: '货物名称', prop: 'cargoName', type: 'input' }, diff --git a/src/option/settlement/settlementAdjustmentSearch.js b/src/option/settlement/settlementAdjustmentSearch.js index 5711fce..9208fd2 100644 --- a/src/option/settlement/settlementAdjustmentSearch.js +++ b/src/option/settlement/settlementAdjustmentSearch.js @@ -5,7 +5,7 @@ export const settlementAdjustmentSearchFields = [ { label: '日期', prop: 'createDateRange', type: 'daterange' }, { label: '客户名称', prop: 'customerName', type: 'input' }, { label: '项目名称', prop: 'projectName', type: 'input' }, - { label: '所属组织', prop: 'deptName', type: 'input' }, + { label: '所属组织', prop: 'deptName', type: 'cascader' }, { label: '关联结算单', prop: 'formalSettlementNo', type: 'input' }, { label: '结算单类型', diff --git a/src/option/settlement/transportReconciliationSearch.js b/src/option/settlement/transportReconciliationSearch.js index ad0ee72..5577db4 100644 --- a/src/option/settlement/transportReconciliationSearch.js +++ b/src/option/settlement/transportReconciliationSearch.js @@ -15,7 +15,7 @@ export const transportReconciliationSearchFields = [ { prop: 'reconciliationNo', label: '对账单号', type: 'input' }, { prop: 'preSettlementNos', label: '预结算单号', type: 'input' }, { prop: 'projectName', label: '项目', type: 'input' }, - { prop: 'deptName', label: '所属组织', type: 'input' }, + { prop: 'deptName', label: '所属组织', type: 'cascader' }, { prop: 'contractNo', label: '合同编号', type: 'input' }, { prop: 'payerName', label: '付款方', type: 'input' }, { prop: 'payeeName', label: '收款方', type: 'input' }, diff --git a/src/styles/element-ui.scss b/src/styles/element-ui.scss index 91f3b5e..cba5269 100644 --- a/src/styles/element-ui.scss +++ b/src/styles/element-ui.scss @@ -135,9 +135,10 @@ //white-space: nowrap; } -// 全宽兜底:搜索表单内树形下拉占满(覆盖默认搜索项宽度) -// 注意:搜索 form 内的 el-tree-select 也走该规则;新增/编辑表单用 span=6 的栅格自然分开,不受影响。 -.avue-crud__search .el-form-item__content > .el-tree-select { +// 全宽兜底:搜索表单内树形下拉 / 级联选择器占满(覆盖默认搜索项宽度) +// 注意:搜索 form 内的 el-tree-select、el-cascader 均走该规则;新增/编辑表单用 span=6 的栅格自然分开,不受影响。 +.avue-crud__search .el-form-item__content > .el-tree-select, +.avue-crud__search .el-form-item__content > .el-cascader { width: 100%; max-width: 100%; } diff --git a/src/views/business/contract-manage.vue b/src/views/business/contract-manage.vue index 7095ca7..d11064f 100644 --- a/src/views/business/contract-manage.vue +++ b/src/views/business/contract-manage.vue @@ -216,18 +216,14 @@ - @@ -794,7 +790,7 @@ import { getToken } from '@/utils/auth'; import { downloadFileByUrl, downloadXls } from '@/utils/util'; import BillingPlanEditor from './components/billing-plan-editor.vue'; import PdfPreview from '@/components/pdf-preview/main.vue'; -import { ElImageViewer, ElTreeSelect } from 'element-plus'; +import { ElImageViewer, ElCascader } from 'element-plus'; import { OpenFileViewer } from '@open-file-viewer/vue'; import { fallbackPlugin, imagePlugin, officePlugin, textPlugin } from '@open-file-viewer/core'; import '@open-file-viewer/core/style.css'; @@ -1230,13 +1226,32 @@ export default { contractFormatOptions() { return this.columnOptions('contractFormat'); }, - organizationTreeSelectProps() { + organizationCascaderProps() { return { label: 'label', value: 'id', children: 'children', + emitPath: true, + checkStrictly: true, + expandTrigger: 'click', }; }, + // 所属组织级联:selectedOrganizationId 存部门 id,级联控件用根到节点的 id 路径, + // 两者在此双向转换;依赖 organizationOptions,树加载完成后回显自动刷新 + organizationCascaderValue: { + get() { + const id = this.selectedOrganizationId || this.form.organizationId; + if (!id) return []; + return this.findOrganizationPath(id) || [String(id)]; + }, + set(path) { + const value = Array.isArray(path) + ? path.filter(item => item !== undefined && item !== null && item !== '') + : []; + this.selectedOrganizationId = value.length ? String(value.at(-1)) : ''; + this.handleOrganizationChange(value); + }, + }, settlementRuleEnabled() { return Number(this.settlementRuleForm.autoGenerate) === 1; }, @@ -1728,6 +1743,18 @@ export default { const value = Array.isArray(id) ? id.at(-1) : id; return this.organizationFlatOptions.find(item => String(item.id) === String(value)); }, + // 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null) + findOrganizationPath(id, tree = this.organizationOptions, parents = []) { + const target = Array.isArray(id) ? id.at(-1) : id; + if (target === undefined || target === null || target === '') return null; + for (const node of tree || []) { + const path = [...parents, String(node.id)]; + if (String(node.id) === String(target)) return path; + const matched = this.findOrganizationPath(target, node.children, path); + if (matched) return matched; + } + return null; + }, handleOrganizationChange(id) { const organization = this.findOrganization(id); Object.assign( @@ -1752,21 +1779,26 @@ export default { this.organizationFlatOptions.unshift(item); }, renderOrganizationSearch(scope) { - // 搜索区所属组织:树形下拉(与客户档案统一) - return h(ElTreeSelect, { - modelValue: scope.row?.organizationName || '', + // 搜索区所属组织:单选级联(与新增/编辑表单一致) + // 级联返回 id 路径数组,提交时由 normalizeSearch 取末级 id 转组织名称 + return h(ElCascader, { + modelValue: Array.isArray(scope.row?.organizationName) ? scope.row.organizationName : [], 'onUpdate:modelValue': value => { - if (scope.row) scope.row.organizationName = value; + // 级联单选:modelValue 必须是 id 路径数组,否则控件内部状态会错乱导致无法再次选中 + if (scope.row) scope.row.organizationName = Array.isArray(value) ? value : []; + }, + options: this.organizationOptions, + props: { + label: 'label', + value: 'id', + children: 'children', + checkStrictly: true, + expandTrigger: 'click', }, - data: this.organizationOptions, - 'node-key': 'id', - 'check-strictly': true, filterable: true, clearable: true, - 'render-after-expand': false, style: 'width: 100%', placeholder: '请选择 所属组织', - props: { label: 'label', value: 'id', children: 'children' }, }); }, integerInput(prop, value) { diff --git a/src/views/business/project-apply.vue b/src/views/business/project-apply.vue index 07b74a0..4f5fd18 100644 --- a/src/views/business/project-apply.vue +++ b/src/views/business/project-apply.vue @@ -166,33 +166,25 @@ /> - - @@ -1047,11 +1039,6 @@ export default { deptOptions: [], businessDeptTreeOptions: [], deptTreeOptions: [], - deptTreeSelectProps: { - label: 'label', - value: 'value', - children: 'children', - }, cargoTypeOptions: [], selectedCustomerId: [], selectedCarrierIds: [], @@ -1110,6 +1097,45 @@ export default { }, computed: { ...mapGetters(['permission', 'userInfo']), + // 部门级联配置:checkStrictly 允许选中任意层级,不必选到末级 + deptCascaderProps() { + return { + label: 'label', + value: 'value', + children: 'children', + emitPath: true, + checkStrictly: true, + expandTrigger: 'click', + }; + }, + // 业务部门:form 存部门 id,级联控件用根到节点的 id 路径,两者在此双向转换 + businessDeptCascaderValue: { + get() { + const id = this.form.businessDeptId; + return id ? this.findDeptPath(id, this.businessDeptTreeOptions) || [id] : []; + }, + set(path) { + const value = Array.isArray(path) + ? path.filter(item => item !== undefined && item !== null && item !== '') + : []; + this.form.businessDeptId = value.length ? value.at(-1) : ''; + this.handleBusinessDeptChange(this.form.businessDeptId); + }, + }, + // 承办部门:同上 + undertakeDeptCascaderValue: { + get() { + const id = this.form.undertakeDeptId; + return id ? this.findDeptPath(id, this.deptTreeOptions) || [id] : []; + }, + set(path) { + const value = Array.isArray(path) + ? path.filter(item => item !== undefined && item !== null && item !== '') + : []; + this.form.undertakeDeptId = value.length ? value.at(-1) : ''; + this.handleUndertakeDeptChange(this.form.undertakeDeptId); + }, + }, permissionList() { return { addBtn: this.canCreate, @@ -1843,15 +1869,30 @@ export default { return `${base}-BG${this.$dayjs().format('YYYYMMDD')}`; }, handleBusinessDeptChange(value) { - const dept = this.deptOptions.find(item => item.value === value); + // 兼容级联路径数组(取末级)与单个部门 id + const id = Array.isArray(value) ? value.at(-1) : value; + const dept = this.deptOptions.find(item => String(item.value) === String(id)); this.form.businessDeptName = dept ? dept.rawLabel : ''; this.$refs.projectForm?.validateField('businessDeptId'); }, handleUndertakeDeptChange(value) { - const dept = this.deptOptions.find(item => item.value === value); + const id = Array.isArray(value) ? value.at(-1) : value; + const dept = this.deptOptions.find(item => String(item.value) === String(id)); this.form.undertakeDeptName = dept ? dept.rawLabel : ''; this.$refs.projectForm?.validateField('undertakeDeptId'); }, + // 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null) + findDeptPath(id, tree = [], parents = []) { + const target = Array.isArray(id) ? id.at(-1) : id; + if (target === undefined || target === null || target === '') return null; + for (const node of tree || []) { + const path = [...parents, node.value]; + if (String(node.value) === String(target)) return path; + const matched = this.findDeptPath(target, node.children, path); + if (matched) return matched; + } + return null; + }, handleProjectTypeChange() { this.normalizeAttachmentFileTypes(); this.$refs.projectForm?.validateField('projectType'); diff --git a/src/views/payment/bill-payment.vue b/src/views/payment/bill-payment.vue index 7ce8f8e..d2ae35d 100644 --- a/src/views/payment/bill-payment.vue +++ b/src/views/payment/bill-payment.vue @@ -48,6 +48,7 @@ import BillPaymentTable from './components/bill-payment-table.vue'; const emptyQuery = () => ({ paymentNo: '', deptName: '', + deptPath: [], paymentDateRange: [], approvalStatus: '', }); diff --git a/src/views/payment/components/bill-payment-search.vue b/src/views/payment/components/bill-payment-search.vue index 1d855ca..e395845 100644 --- a/src/views/payment/components/bill-payment-search.vue +++ b/src/views/payment/components/bill-payment-search.vue @@ -6,18 +6,15 @@ - diff --git a/src/views/payment/invoice-application.vue b/src/views/payment/invoice-application.vue index f9d8115..b701546 100644 --- a/src/views/payment/invoice-application.vue +++ b/src/views/payment/invoice-application.vue @@ -10,18 +10,15 @@ - @@ -252,6 +249,7 @@ const emptyQuery = () => ({ applicationNo: '', projectName: '', deptName: '', + deptPath: [], kingdeeStatus: '', approvalStatus: '', }); diff --git a/src/views/payment/invoice-receipt.vue b/src/views/payment/invoice-receipt.vue index 3f4f4b3..55f4acf 100644 --- a/src/views/payment/invoice-receipt.vue +++ b/src/views/payment/invoice-receipt.vue @@ -19,18 +19,15 @@ - @@ -245,6 +242,7 @@ const emptyQuery = () => ({ invoiceDate: '', projectName: '', deptName: '', + deptPath: [], approvalStatus: '', kingdeeStatus: '', }); diff --git a/src/views/payment/payment-application.vue b/src/views/payment/payment-application.vue index d53e918..27715b8 100644 --- a/src/views/payment/payment-application.vue +++ b/src/views/payment/payment-application.vue @@ -23,18 +23,15 @@ /> @@ -283,6 +293,7 @@ import { voidBill, } from '@/api/settlement/preSettlement'; import { preSettlementSearchFields } from '@/option/settlement/preSettlementSearch'; +import organizationSearch from '@/mixins/organization-search'; import { preSettlementTableColumns } from '@/option/settlement/preSettlementTable'; import { createSettlementTransfer } from '@/utils/settlement-transfer'; import { downloadFile } from '@/utils/util'; @@ -292,7 +303,7 @@ const emptyQuery = () => ({ preSettlementNo: '', advanceNo: '', projectName: '', - deptName: '', + deptName: [], contractName: '', contractNo: '', payeeName: '', @@ -304,6 +315,7 @@ const emptyQuery = () => ({ export default { name: 'PreSettlement', components: { PreSettlementEditor }, + mixins: [organizationSearch], data() { return { ArrowDown, @@ -357,6 +369,7 @@ export default { }, }, created() { + this.loadOrganizationOptions(); this.loadTable(); this.openRouteDetail(); }, @@ -377,8 +390,10 @@ export default { }, buildQuery() { const range = this.query.createDateRange || []; + // 所属组织级联返回 id 路径,转成组织名称传给后端(用副本,避免污染搜索框回显) + const query = this.normalizeOrganizationSearch({ ...this.query }, 'deptName'); return { - ...this.query, + ...query, createDateRange: undefined, createStartDate: range[0], createEndDate: range[1], diff --git a/src/views/settlement/settlement-adjustment.vue b/src/views/settlement/settlement-adjustment.vue index c715186..752e7a2 100644 --- a/src/views/settlement/settlement-adjustment.vue +++ b/src/views/settlement/settlement-adjustment.vue @@ -25,6 +25,16 @@ :label="item.label" :value="item.value" /> +
@@ -159,6 +169,7 @@ import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue'; import { mapGetters } from 'vuex'; import * as api from '@/api/settlement/settlementAdjustment'; import { settlementAdjustmentSearchFields } from '@/option/settlement/settlementAdjustmentSearch'; +import organizationSearch from '@/mixins/organization-search'; import { settlementAdjustmentTableColumns } from '@/option/settlement/settlementAdjustmentTable'; import SettlementAdjustmentEditor from './components/settlement-adjustment-editor.vue'; @@ -167,7 +178,7 @@ const emptyQuery = () => ({ createDateRange: [], customerName: '', projectName: '', - deptName: '', + deptName: [], formalSettlementNo: '', settlementType: '', approvalStatus: '', @@ -175,6 +186,7 @@ const emptyQuery = () => ({ export default { name: 'SettlementAdjustment', components: { SettlementAdjustmentEditor }, + mixins: [organizationSearch], data: () => ({ ArrowDown, ArrowUp, @@ -199,6 +211,7 @@ export default { }, }, created() { + this.loadOrganizationOptions(); this.loadTable(); }, methods: { @@ -210,7 +223,8 @@ export default { try { const range = this.query.createDateRange || []; const { data } = await api.getList(this.page.current, this.page.size, { - ...this.query, + // 所属组织级联返回 id 路径,转成组织名称传给后端(用副本,避免污染搜索框回显) + ...this.normalizeOrganizationSearch({ ...this.query }, 'deptName'), createDateRange: undefined, createStartDate: range[0], createEndDate: range[1], diff --git a/src/views/settlement/transport-reconciliation.vue b/src/views/settlement/transport-reconciliation.vue index 4ad2554..0a87b48 100644 --- a/src/views/settlement/transport-reconciliation.vue +++ b/src/views/settlement/transport-reconciliation.vue @@ -17,6 +17,16 @@ :value="item.value" /> +
@@ -87,6 +97,7 @@ import * as XLSX from 'xlsx'; import * as api from '@/api/settlement/transportReconciliation'; import { transportReconciliationSearchFields } from '@/option/settlement/transportReconciliationSearch'; import { transportReconciliationTableColumns } from '@/option/settlement/transportReconciliationTable'; +import organizationSearch from '@/mixins/organization-search'; import TransportReconciliationEditor from './components/transport-reconciliation-editor.vue'; import TransportReconciliationTablePanel from './components/transport-reconciliation-table-panel.vue'; @@ -94,7 +105,7 @@ const emptyQuery = () => ({ reconciliationNo: '', preSettlementNos: '', projectName: '', - deptName: '', + deptName: [], contractNo: '', payerName: '', payeeName: '', @@ -105,6 +116,7 @@ const emptyQuery = () => ({ export default { name: 'TransportReconciliation', components: { TransportReconciliationEditor, TransportReconciliationTablePanel }, + mixins: [organizationSearch], data() { return { ArrowDown, @@ -129,6 +141,7 @@ export default { }, }, mounted() { + this.loadOrganizationOptions(); this.loadTable(); }, methods: { @@ -139,7 +152,8 @@ export default { this.loading = true; try { const response = await api.getList(this.page.current, this.page.size, { - ...this.query, + // 所属组织级联返回 id 路径,转成组织名称传给后端(用副本,避免污染搜索框回显) + ...this.normalizeOrganizationSearch({ ...this.query }, 'deptName'), settlementType: this.settlementType, }); const data = response?.data?.data || response?.data || response || {}; diff --git a/src/views/system/user.vue b/src/views/system/user.vue index 0bc6c36..933f6a0 100644 --- a/src/views/system/user.vue +++ b/src/views/system/user.vue @@ -195,7 +195,7 @@ - + 仅所属组织全部组织自定义 @@ -443,6 +443,36 @@ export default { }, computed: { ...mapGetters(['userInfo', 'permission']), + // 所属组织级联配置:checkStrictly 允许选中任意层级,不必选到末级 + deptCascaderProps() { + return { + label: 'title', + value: 'id', + children: 'children', + emitPath: true, + checkStrictly: true, + expandTrigger: 'click', + }; + }, + // 所属组织为单选级联:form.deptId 仍为数组(提交时 join),此处与 id 路径双向转换 + deptCascaderValue: { + get() { + const ids = Array.isArray(this.form.deptId) + ? this.form.deptId + : this.form.deptId + ? [this.form.deptId] + : []; + const id = ids.find(item => item !== undefined && item !== null && item !== ''); + return id === undefined ? [] : this.findDeptPath(id) || [id]; + }, + set(path) { + const value = Array.isArray(path) + ? path.filter(item => item !== undefined && item !== null && item !== '') + : []; + this.form.deptId = value.length ? [value.at(-1)] : []; + this.$refs.userFormRef?.validateField('deptId'); + }, + }, permissionList() { return { addBtn: this.validData(this.permission.user_add, false), @@ -565,6 +595,18 @@ export default { this.customerList = res.data.data || []; }); }, + // 由部门 id 反查「根 → 该节点」的 id 路径,供级联回显(找不到返回 null) + findDeptPath(id, tree = this.permissionDeptTree, parents = []) { + const target = Array.isArray(id) ? id.at(-1) : id; + if (target === undefined || target === null || target === '') return null; + for (const node of tree || []) { + const path = [...parents, node.id]; + if (String(node.id) === String(target)) return path; + const matched = this.findDeptPath(target, node.children, path); + if (matched) return matched; + } + return null; + }, submitRole() { const roleList = this.$refs.treeRole.getCheckedKeys().join(','); if (this.roleFormMode) { diff --git a/src/views/transportCapacity/driver.vue b/src/views/transportCapacity/driver.vue index 6a63be0..94259fc 100644 --- a/src/views/transportCapacity/driver.vue +++ b/src/views/transportCapacity/driver.vue @@ -490,14 +490,14 @@ - - - + @@ -815,6 +815,15 @@ export default { }, computed: { ...mapGetters(['permission', 'userInfo']), + // 所属组织级联:表单存组织名称,级联控件用 id 路径,两者在此双向转换 + organizationCascaderValue: { + get() { + return this.resolveOrgPath(this.driverForm.organizationName); + }, + set(path) { + this.driverForm.organizationName = this.resolveOrgName(path); + }, + }, isAdmin() { const authority = this.userInfo.authority || ''; return authority.includes('admin'); @@ -1613,6 +1622,8 @@ export default { this.onLoad(this.page); }, searchChange(params, done) { + // 所属组织级联返回的是 id 路径,转成组织名称传给后端 + this.normalizeOrganizationSearch(params); this.searchForm = params; this.page.currentPage = 1; this.onLoad(this.page, params); diff --git a/src/views/transportCapacity/ship.vue b/src/views/transportCapacity/ship.vue index 255570f..07b1912 100644 --- a/src/views/transportCapacity/ship.vue +++ b/src/views/transportCapacity/ship.vue @@ -145,19 +145,14 @@ - - - + placeholder="请选择 船舶所属组织" + /> @@ -707,6 +702,15 @@ export default { }, computed: { ...mapGetters(['permission', 'userInfo']), + // 船舶所属组织级联:表单存组织名称,级联控件用 id 路径,两者在此双向转换 + organizationCascaderValue: { + get() { + return this.resolveOrgPath(this.shipForm.organizationName); + }, + set(path) { + this.shipForm.organizationName = this.resolveOrgName(path); + }, + }, isAdmin() { const authority = this.userInfo.authority || ''; return authority.includes('admin'); @@ -1100,6 +1104,8 @@ export default { this.onLoad(this.page); }, searchChange(params, done) { + // 所属组织级联返回的是 id 路径,转成组织名称传给后端 + this.normalizeOrganizationSearch(params); this.searchForm = params; this.page.currentPage = 1; this.onLoad(this.page, params); diff --git a/src/views/transportCapacity/vehicle.vue b/src/views/transportCapacity/vehicle.vue index 6c8dbca..8026573 100644 --- a/src/views/transportCapacity/vehicle.vue +++ b/src/views/transportCapacity/vehicle.vue @@ -159,19 +159,14 @@ - - - + placeholder="请选择 所属组织" + /> @@ -709,6 +704,15 @@ export default { }, computed: { ...mapGetters(['permission', 'userInfo']), + // 所属组织级联:表单存组织名称,级联控件用 id 路径,两者在此双向转换 + organizationCascaderValue: { + get() { + return this.resolveOrgPath(this.vehicleForm.organizationName); + }, + set(path) { + this.vehicleForm.organizationName = this.resolveOrgName(path); + }, + }, isAdmin() { const authority = this.userInfo.authority || ''; return authority.includes('admin'); @@ -1275,6 +1279,8 @@ export default { this.onLoad(this.page); }, searchChange(params, done) { + // 所属组织级联返回的是 id 路径,转成组织名称传给后端 + this.normalizeOrganizationSearch(params); this.searchForm = params; this.page.currentPage = 1; this.onLoad(this.page, params); diff --git a/src/views/vehicle/customer-archive.vue b/src/views/vehicle/customer-archive.vue index 4211c7b..aad44d4 100644 --- a/src/views/vehicle/customer-archive.vue +++ b/src/views/vehicle/customer-archive.vue @@ -335,17 +335,13 @@ - @@ -1367,7 +1363,7 @@