Merge remote-tracking branch 'websoft/master'

# Conflicts:
#	src/views/settlement/receivable-payable-detail.vue
This commit is contained in:
2026-08-31 19:30:16 +08:00
25 changed files with 493 additions and 187 deletions
+12
View File
@@ -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 转部门名称。
</content>
</invoke>
+72 -14
View File
@@ -1,11 +1,11 @@
import { getDeptTree } from '@/api/system/dept'; import { getDeptTree } from '@/api/system/dept';
import { ElTreeSelect } from 'element-plus'; import { ElCascader } from 'element-plus';
import { h } from 'vue'; import { h } from 'vue';
/** /**
* 所属组织 统一搜索 mixin * 所属组织 统一搜索 mixin
* ------------------------------------------------------------------ * ------------------------------------------------------------------
* 全站「所属组织」搜索栏统一为树形下拉(el-tree-select),风格与客档案 * 全站「所属组织」搜索栏统一为级联选择器(el-cascader,单选),风格与客档案
* vehicle/customer-archive.vue)保持一致。 * vehicle/customer-archive.vue)保持一致。
* *
* 接入步骤(在使用了 avue-crud 的页面中): * 接入步骤(在使用了 avue-crud 的页面中):
@@ -15,24 +15,40 @@ import { h } from 'vue';
* *
* 说明: * 说明:
* - 自动识别 organizationName / deptName 两种 prop,并挂载 column.renderSearch * - 自动识别 organizationName / deptName 两种 prop,并挂载 column.renderSearch
* - 树节点标识统一用 idnode-key='id'),返回值为部门 id * - 树节点标识统一用 id,级联值为「根 → 选中节点」的 id 路径数组
* - checkStrictly 打开,任意层级(含非末级)都可直接选中
* - 页面若已有 excludeExternalOrganization / normalizeOrgTree 等方法, * - 页面若已有 excludeExternalOrganization / normalizeOrgTree 等方法,
* 组件自身方法优先级更高,会覆盖本 mixin 的默认实现 * 组件自身方法优先级更高,会覆盖本 mixin 的默认实现
* - 搜索值id需在各自的 searchChange / normalizeSearch 中按后端要求 * - 搜索值需在各自的 searchChange / normalizeSearch 中按后端要求转换为部门名称:
* 转换为部门名称(参考 contract-manage 的 normalizeSearch * 级联值是路径数组,统一用 findOrgById(路径)?.rawLabel 取末级名称
* - 表单区(新增/编辑)需要回显时,可用 findOrgPath(deptId) 把 id 转成路径,
* 参考 customer-archive 的 deptCascaderValue 计算属性
*/ */
const ORG_PROPS = ['organizationName', 'deptName']; const ORG_PROPS = ['organizationName', 'deptName'];
export default { export default {
data() { data() {
return { return {
// 树形组织数据,仅供搜索栏 el-tree-select 使用; // 树形组织数据,仅供搜索栏 el-cascader 使用;
// 与页面自身 organizationOptions(多为表单扁平列表)隔离,避免相互覆盖 // 与页面自身 organizationOptions(多为表单扁平列表)隔离,避免相互覆盖
organizationTreeOptions: [], organizationTreeOptions: [],
organizationTreeFlatOptions: [], organizationTreeFlatOptions: [],
organizationLoading: false, organizationLoading: false,
}; };
}, },
computed: {
// 所属组织级联配置:checkStrictly 允许选中任意层级,不必选到末级
organizationCascaderProps() {
return {
value: 'id',
label: 'label',
children: 'children',
emitPath: true,
checkStrictly: true,
expandTrigger: 'click',
};
},
},
methods: { methods: {
loadOrganizationOptions() { loadOrganizationOptions() {
if (this.organizationLoading) return; if (this.organizationLoading) return;
@@ -90,25 +106,67 @@ export default {
...this.flattenOrgTree(item.children || []), ...this.flattenOrgTree(item.children || []),
]); ]);
}, },
// 兼容两种入参:级联路径数组(取末级)或单个部门 id
findOrgById(id) { findOrgById(id) {
const value = Array.isArray(id) ? id.at(-1) : id; const value = Array.isArray(id) ? id.at(-1) : id;
return this.organizationTreeFlatOptions.find(item => String(item.id) === String(value)); 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') { renderOrganizationSearch(scope, prop = 'organizationName') {
return h(ElTreeSelect, { return h(ElCascader, {
modelValue: scope.row?.[prop] || '', modelValue: Array.isArray(scope.row?.[prop]) ? scope.row[prop] : [],
'onUpdate:modelValue': value => { '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, filterable: true,
clearable: true, clearable: true,
'render-after-expand': false,
style: 'width: 100%', style: 'width: 100%',
placeholder: '请选择 所属组织', placeholder: '请选择 所属组织',
props: { label: 'label', value: 'id', children: 'children' },
}); });
}, },
}, },
@@ -38,7 +38,7 @@ export const formalSettlementSearchFields = [
{ label: '结算单号', prop: 'formalSettlementNo', type: 'input' }, { label: '结算单号', prop: 'formalSettlementNo', type: 'input' },
{ label: '预结算单号', prop: 'preSettlementNo', type: 'input' }, { label: '预结算单号', prop: 'preSettlementNo', type: 'input' },
{ label: '项目', prop: 'projectName', type: 'input' }, { label: '项目', prop: 'projectName', type: 'input' },
{ label: '所属组织', prop: 'deptName', type: 'input' }, { label: '所属组织', prop: 'deptName', type: 'cascader' },
{ label: '合同编号', prop: 'contractNo', type: 'input' }, { label: '合同编号', prop: 'contractNo', type: 'input' },
{ label: '付款方', prop: 'payerName', type: 'input' }, { label: '付款方', prop: 'payerName', type: 'input' },
{ label: '收款方', prop: 'payeeName', type: 'input' }, { label: '收款方', prop: 'payeeName', type: 'input' },
+1 -1
View File
@@ -10,7 +10,7 @@ export const preSettlementSearchFields = [
{ label: '预结算单号', prop: 'preSettlementNo', type: 'input' }, { label: '预结算单号', prop: 'preSettlementNo', type: 'input' },
{ label: '预付单号', prop: 'advanceNo', type: 'input' }, { label: '预付单号', prop: 'advanceNo', type: 'input' },
{ label: '项目名称', prop: 'projectName', type: 'input' }, { label: '项目名称', prop: 'projectName', type: 'input' },
{ label: '所属组织', prop: 'deptName', type: 'input' }, { label: '所属组织', prop: 'deptName', type: 'cascader' },
{ label: '合同名称', prop: 'contractName', type: 'input' }, { label: '合同名称', prop: 'contractName', type: 'input' },
{ label: '合同编号', prop: 'contractNo', type: 'input' }, { label: '合同编号', prop: 'contractNo', type: 'input' },
{ label: '收款方', prop: 'payeeName', type: 'input' }, { label: '收款方', prop: 'payeeName', type: 'input' },
@@ -14,7 +14,7 @@ export const searchFields = [
{ label: '单据号', prop: 'documentNo', type: 'input' }, { label: '单据号', prop: 'documentNo', type: 'input' },
{ label: '生成日期', prop: 'generateDateRange', type: 'daterange' }, { label: '生成日期', prop: 'generateDateRange', type: 'daterange' },
{ label: '客户名称', prop: 'customerName', type: 'input' }, { label: '客户名称', prop: 'customerName', type: 'input' },
{ label: '所属组织', prop: 'deptName', type: 'input' }, { label: '所属组织', prop: 'deptName', type: 'cascader' },
{ label: '项目名称', prop: 'projectName', type: 'input' }, { label: '项目名称', prop: 'projectName', type: 'input' },
{ label: '货物类型', prop: 'cargoType', type: 'input' }, { label: '货物类型', prop: 'cargoType', type: 'input' },
{ label: '货物名称', prop: 'cargoName', type: 'input' }, { label: '货物名称', prop: 'cargoName', type: 'input' },
@@ -5,7 +5,7 @@ export const settlementAdjustmentSearchFields = [
{ label: '日期', prop: 'createDateRange', type: 'daterange' }, { label: '日期', prop: 'createDateRange', type: 'daterange' },
{ label: '客户名称', prop: 'customerName', type: 'input' }, { label: '客户名称', prop: 'customerName', type: 'input' },
{ label: '项目名称', prop: 'projectName', type: 'input' }, { label: '项目名称', prop: 'projectName', type: 'input' },
{ label: '所属组织', prop: 'deptName', type: 'input' }, { label: '所属组织', prop: 'deptName', type: 'cascader' },
{ label: '关联结算单', prop: 'formalSettlementNo', type: 'input' }, { label: '关联结算单', prop: 'formalSettlementNo', type: 'input' },
{ {
label: '结算单类型', label: '结算单类型',
@@ -15,7 +15,7 @@ export const transportReconciliationSearchFields = [
{ prop: 'reconciliationNo', label: '对账单号', type: 'input' }, { prop: 'reconciliationNo', label: '对账单号', type: 'input' },
{ prop: 'preSettlementNos', label: '预结算单号', type: 'input' }, { prop: 'preSettlementNos', label: '预结算单号', type: 'input' },
{ prop: 'projectName', label: '项目', type: 'input' }, { prop: 'projectName', label: '项目', type: 'input' },
{ prop: 'deptName', label: '所属组织', type: 'input' }, { prop: 'deptName', label: '所属组织', type: 'cascader' },
{ prop: 'contractNo', label: '合同编号', type: 'input' }, { prop: 'contractNo', label: '合同编号', type: 'input' },
{ prop: 'payerName', label: '付款方', type: 'input' }, { prop: 'payerName', label: '付款方', type: 'input' },
{ prop: 'payeeName', label: '收款方', type: 'input' }, { prop: 'payeeName', label: '收款方', type: 'input' },
+4 -3
View File
@@ -135,9 +135,10 @@
//white-space: nowrap; //white-space: nowrap;
} }
// 全宽兜底:搜索表单内树形下拉占满(覆盖默认搜索项宽度) // 全宽兜底:搜索表单内树形下拉 / 级联选择器占满(覆盖默认搜索项宽度)
// 注意:搜索 form 内的 el-tree-select走该规则;新增/编辑表单用 span=6 的栅格自然分开,不受影响。 // 注意:搜索 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-tree-select,
.avue-crud__search .el-form-item__content > .el-cascader {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
} }
+51 -19
View File
@@ -216,18 +216,14 @@
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="所属组织" prop="organizationName"> <el-form-item label="所属组织" prop="organizationName">
<el-tree-select <el-cascader
v-model="selectedOrganizationId" v-model="organizationCascaderValue"
:data="organizationOptions" :options="organizationOptions"
:props="organizationTreeSelectProps" :props="organizationCascaderProps"
node-key="id"
placeholder="请选择" placeholder="请选择"
check-strictly
clearable clearable
filterable filterable
:render-after-expand="false"
@visible-change="visible => visible && loadOrganizationOptions()" @visible-change="visible => visible && loadOrganizationOptions()"
@change="handleOrganizationChange"
/> />
</el-form-item> </el-form-item>
<el-form-item label="经办人"> <el-form-item label="经办人">
@@ -794,7 +790,7 @@ import { getToken } from '@/utils/auth';
import { downloadFileByUrl, downloadXls } from '@/utils/util'; import { downloadFileByUrl, downloadXls } from '@/utils/util';
import BillingPlanEditor from './components/billing-plan-editor.vue'; import BillingPlanEditor from './components/billing-plan-editor.vue';
import PdfPreview from '@/components/pdf-preview/main.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 { OpenFileViewer } from '@open-file-viewer/vue';
import { fallbackPlugin, imagePlugin, officePlugin, textPlugin } from '@open-file-viewer/core'; import { fallbackPlugin, imagePlugin, officePlugin, textPlugin } from '@open-file-viewer/core';
import '@open-file-viewer/core/style.css'; import '@open-file-viewer/core/style.css';
@@ -1230,13 +1226,32 @@ export default {
contractFormatOptions() { contractFormatOptions() {
return this.columnOptions('contractFormat'); return this.columnOptions('contractFormat');
}, },
organizationTreeSelectProps() { organizationCascaderProps() {
return { return {
label: 'label', label: 'label',
value: 'id', value: 'id',
children: 'children', 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() { settlementRuleEnabled() {
return Number(this.settlementRuleForm.autoGenerate) === 1; return Number(this.settlementRuleForm.autoGenerate) === 1;
}, },
@@ -1728,6 +1743,18 @@ export default {
const value = Array.isArray(id) ? id.at(-1) : id; const value = Array.isArray(id) ? id.at(-1) : id;
return this.organizationFlatOptions.find(item => String(item.id) === String(value)); 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) { handleOrganizationChange(id) {
const organization = this.findOrganization(id); const organization = this.findOrganization(id);
Object.assign( Object.assign(
@@ -1752,21 +1779,26 @@ export default {
this.organizationFlatOptions.unshift(item); this.organizationFlatOptions.unshift(item);
}, },
renderOrganizationSearch(scope) { renderOrganizationSearch(scope) {
// 搜索区所属组织:树形下拉(与客户档案统一 // 搜索区所属组织:单选级联(与新增/编辑表单一致
return h(ElTreeSelect, { // 级联返回 id 路径数组,提交时由 normalizeSearch 取末级 id 转组织名称
modelValue: scope.row?.organizationName || '', return h(ElCascader, {
modelValue: Array.isArray(scope.row?.organizationName) ? scope.row.organizationName : [],
'onUpdate:modelValue': value => { '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, filterable: true,
clearable: true, clearable: true,
'render-after-expand': false,
style: 'width: 100%', style: 'width: 100%',
placeholder: '请选择 所属组织', placeholder: '请选择 所属组织',
props: { label: 'label', value: 'id', children: 'children' },
}); });
}, },
integerInput(prop, value) { integerInput(prop, value) {
+64 -23
View File
@@ -166,33 +166,25 @@
/> />
</el-form-item> </el-form-item>
<el-form-item label="业务部门" prop="businessDeptId"> <el-form-item label="业务部门" prop="businessDeptId">
<el-tree-select <el-cascader
v-model="form.businessDeptId" v-model="businessDeptCascaderValue"
:data="businessDeptTreeOptions" :options="businessDeptTreeOptions"
:props="deptTreeSelectProps" :props="deptCascaderProps"
node-key="value"
placeholder="请选择业务部门" placeholder="请选择业务部门"
check-strictly
filterable filterable
clearable clearable
:render-after-expand="false"
:disabled="isBasicInfoReadonly" :disabled="isBasicInfoReadonly"
@change="handleBusinessDeptChange"
/> />
</el-form-item> </el-form-item>
<el-form-item label="承办部门" prop="undertakeDeptId"> <el-form-item label="承办部门" prop="undertakeDeptId">
<el-tree-select <el-cascader
v-model="form.undertakeDeptId" v-model="undertakeDeptCascaderValue"
:data="deptTreeOptions" :options="deptTreeOptions"
:props="deptTreeSelectProps" :props="deptCascaderProps"
node-key="value"
placeholder="请选择承办部门" placeholder="请选择承办部门"
check-strictly
clearable clearable
filterable filterable
:render-after-expand="false"
:disabled="isBasicInfoReadonly" :disabled="isBasicInfoReadonly"
@change="handleUndertakeDeptChange"
/> />
</el-form-item> </el-form-item>
<el-form-item label="项目由来" prop="projectSource"> <el-form-item label="项目由来" prop="projectSource">
@@ -1047,11 +1039,6 @@ export default {
deptOptions: [], deptOptions: [],
businessDeptTreeOptions: [], businessDeptTreeOptions: [],
deptTreeOptions: [], deptTreeOptions: [],
deptTreeSelectProps: {
label: 'label',
value: 'value',
children: 'children',
},
cargoTypeOptions: [], cargoTypeOptions: [],
selectedCustomerId: [], selectedCustomerId: [],
selectedCarrierIds: [], selectedCarrierIds: [],
@@ -1110,6 +1097,45 @@ export default {
}, },
computed: { computed: {
...mapGetters(['permission', 'userInfo']), ...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() { permissionList() {
return { return {
addBtn: this.canCreate, addBtn: this.canCreate,
@@ -1843,15 +1869,30 @@ export default {
return `${base}-BG${this.$dayjs().format('YYYYMMDD')}`; return `${base}-BG${this.$dayjs().format('YYYYMMDD')}`;
}, },
handleBusinessDeptChange(value) { 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.form.businessDeptName = dept ? dept.rawLabel : '';
this.$refs.projectForm?.validateField('businessDeptId'); this.$refs.projectForm?.validateField('businessDeptId');
}, },
handleUndertakeDeptChange(value) { 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.form.undertakeDeptName = dept ? dept.rawLabel : '';
this.$refs.projectForm?.validateField('undertakeDeptId'); 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() { handleProjectTypeChange() {
this.normalizeAttachmentFileTypes(); this.normalizeAttachmentFileTypes();
this.$refs.projectForm?.validateField('projectType'); this.$refs.projectForm?.validateField('projectType');
+1
View File
@@ -48,6 +48,7 @@ import BillPaymentTable from './components/bill-payment-table.vue';
const emptyQuery = () => ({ const emptyQuery = () => ({
paymentNo: '', paymentNo: '',
deptName: '', deptName: '',
deptPath: [],
paymentDateRange: [], paymentDateRange: [],
approvalStatus: '', approvalStatus: '',
}); });
@@ -6,18 +6,15 @@
<el-input v-model="query.paymentNo" clearable placeholder="请输入" /> <el-input v-model="query.paymentNo" clearable placeholder="请输入" />
</el-form-item> </el-form-item>
<el-form-item label="使用部门"> <el-form-item label="使用部门">
<el-tree-select <el-cascader
:model-value="query.deptName" v-model="query.deptPath"
:data="organizationTreeOptions" :options="organizationTreeOptions"
node-key="id" :props="organizationCascaderProps"
check-strictly
filterable filterable
clearable clearable
render-after-expand
style="width: 100%" style="width: 100%"
placeholder="请选择 所属组织" placeholder="请选择 所属组织"
:props="{ label: 'label', value: 'id', children: 'children' }" @change="val => { query.deptName = findOrgById(val)?.rawLabel || '' }"
@update:model-value="val => { query.deptName = findOrgById(val)?.rawLabel || val }"
/> />
</el-form-item> </el-form-item>
<el-form-item label="付款日期"> <el-form-item label="付款日期">
+6 -8
View File
@@ -10,18 +10,15 @@
<el-input v-model="query.projectName" clearable placeholder="请输入" /> <el-input v-model="query.projectName" clearable placeholder="请输入" />
</el-form-item> </el-form-item>
<el-form-item label="所属组织"> <el-form-item label="所属组织">
<el-tree-select <el-cascader
:model-value="query.deptName" v-model="query.deptPath"
:data="organizationTreeOptions" :options="organizationTreeOptions"
node-key="id" :props="organizationCascaderProps"
check-strictly
filterable filterable
clearable clearable
render-after-expand
style="width: 100%" style="width: 100%"
placeholder="请选择 所属组织" placeholder="请选择 所属组织"
:props="{ label: 'label', value: 'id', children: 'children' }" @change="val => { query.deptName = findOrgById(val)?.rawLabel || '' }"
@update:model-value="val => { query.deptName = findOrgById(val)?.rawLabel || val }"
/> />
</el-form-item> </el-form-item>
<el-form-item class="invoice-kingdee-item" label="金蝶单据状态"> <el-form-item class="invoice-kingdee-item" label="金蝶单据状态">
@@ -252,6 +249,7 @@ const emptyQuery = () => ({
applicationNo: '', applicationNo: '',
projectName: '', projectName: '',
deptName: '', deptName: '',
deptPath: [],
kingdeeStatus: '', kingdeeStatus: '',
approvalStatus: '', approvalStatus: '',
}); });
+6 -8
View File
@@ -19,18 +19,15 @@
<el-input v-model="query.projectName" clearable placeholder="请输入" /> <el-input v-model="query.projectName" clearable placeholder="请输入" />
</el-form-item> </el-form-item>
<el-form-item label="所属组织"> <el-form-item label="所属组织">
<el-tree-select <el-cascader
:model-value="query.deptName" v-model="query.deptPath"
:data="organizationTreeOptions" :options="organizationTreeOptions"
node-key="id" :props="organizationCascaderProps"
check-strictly
filterable filterable
clearable clearable
render-after-expand
style="width: 100%" style="width: 100%"
placeholder="请选择 所属组织" placeholder="请选择 所属组织"
:props="{ label: 'label', value: 'id', children: 'children' }" @change="val => { query.deptName = findOrgById(val)?.rawLabel || '' }"
@update:model-value="val => { query.deptName = findOrgById(val)?.rawLabel || val }"
/> />
</el-form-item> </el-form-item>
<el-form-item v-if="searchExpanded" label="审核状态"> <el-form-item v-if="searchExpanded" label="审核状态">
@@ -245,6 +242,7 @@ const emptyQuery = () => ({
invoiceDate: '', invoiceDate: '',
projectName: '', projectName: '',
deptName: '', deptName: '',
deptPath: [],
approvalStatus: '', approvalStatus: '',
kingdeeStatus: '', kingdeeStatus: '',
}); });
+6 -8
View File
@@ -23,18 +23,15 @@
/></el-form-item> /></el-form-item>
<template v-if="searchExpanded"> <template v-if="searchExpanded">
<el-form-item label="所属组织" <el-form-item label="所属组织"
><el-tree-select ><el-cascader
:model-value="query.deptName" v-model="query.deptPath"
:data="organizationTreeOptions" :options="organizationTreeOptions"
node-key="id" :props="organizationCascaderProps"
check-strictly
filterable filterable
clearable clearable
render-after-expand
style="width: 100%" style="width: 100%"
placeholder="请选择 所属组织" placeholder="请选择 所属组织"
:props="{ label: 'label', value: 'id', children: 'children' }" @change="val => { query.deptName = findOrgById(val)?.rawLabel || '' }"
@update:model-value="val => { query.deptName = findOrgById(val)?.rawLabel || val }"
/></el-form-item> /></el-form-item>
<el-form-item label="关联结算单" <el-form-item label="关联结算单"
><el-input v-model="query.settlementNo" clearable ><el-input v-model="query.settlementNo" clearable
@@ -243,6 +240,7 @@ const emptyQuery = () => ({
payeeName: '', payeeName: '',
projectName: '', projectName: '',
deptName: '', deptName: '',
deptPath: [],
settlementNo: '', settlementNo: '',
paymentType: '', paymentType: '',
approvalStatus: '', approvalStatus: '',
+16 -2
View File
@@ -27,6 +27,16 @@
:value="item.value" :value="item.value"
/> />
</el-select> </el-select>
<el-cascader
v-else-if="field.type === 'cascader'"
v-model="query[field.prop]"
:options="organizationTreeOptions"
:props="organizationCascaderProps"
clearable
filterable
style="width: 100%"
placeholder="请选择"
/>
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" /> <el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
</el-form-item> </el-form-item>
<div class="formal-page__search-actions"> <div class="formal-page__search-actions">
@@ -168,6 +178,7 @@ import {
formalSettlementSearchFields, formalSettlementSearchFields,
paymentStatusOptions, paymentStatusOptions,
} from '@/option/settlement/formalSettlementSearch'; } from '@/option/settlement/formalSettlementSearch';
import organizationSearch from '@/mixins/organization-search';
import { formalSettlementTableColumns } from '@/option/settlement/formalSettlementTable'; import { formalSettlementTableColumns } from '@/option/settlement/formalSettlementTable';
import FormalSettlementEditor from './components/formal-settlement-editor.vue'; import FormalSettlementEditor from './components/formal-settlement-editor.vue';
import FormalSettlementTablePanel from './components/formal-settlement-table-panel.vue'; import FormalSettlementTablePanel from './components/formal-settlement-table-panel.vue';
@@ -176,7 +187,7 @@ const emptyQuery = () => ({
formalSettlementNo: '', formalSettlementNo: '',
preSettlementNo: '', preSettlementNo: '',
projectName: '', projectName: '',
deptName: '', deptName: [],
contractNo: '', contractNo: '',
payerName: '', payerName: '',
payeeName: '', payeeName: '',
@@ -190,6 +201,7 @@ const emptyQuery = () => ({
export default { export default {
name: 'FormalSettlement', name: 'FormalSettlement',
components: { FormalSettlementEditor, FormalSettlementTablePanel }, components: { FormalSettlementEditor, FormalSettlementTablePanel },
mixins: [organizationSearch],
data() { data() {
return { return {
ArrowDown, ArrowDown,
@@ -231,6 +243,7 @@ export default {
}, },
}, },
mounted() { mounted() {
this.loadOrganizationOptions();
this.loadTable(); this.loadTable();
this.openRouteDetail(); this.openRouteDetail();
}, },
@@ -517,7 +530,8 @@ export default {
XLSX.writeFile(workbook, `正式结算单${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`); XLSX.writeFile(workbook, `正式结算单${this.$dayjs().format('YYYY-MM-DD HH-mm-ss')}.xlsx`);
}, },
buildQueryParams() { buildQueryParams() {
const params = { ...this.query }; // 所属组织级联返回 id 路径,转成组织名称传给后端(用副本,避免污染搜索框回显)
const params = this.normalizeOrganizationSearch({ ...this.query }, 'deptName');
params.settlementType = this.activeSettlementType; params.settlementType = this.activeSettlementType;
const range = params.createDateRange || []; const range = params.createDateRange || [];
delete params.createDateRange; delete params.createDateRange;
+17 -2
View File
@@ -29,6 +29,16 @@
:value="item.value" :value="item.value"
/> />
</el-select> </el-select>
<el-cascader
v-else-if="field.type === 'cascader'"
v-model="query[field.prop]"
:options="organizationTreeOptions"
:props="organizationCascaderProps"
clearable
filterable
style="width: 100%"
placeholder="请选择"
/>
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" /> <el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
</el-form-item> </el-form-item>
</template> </template>
@@ -283,6 +293,7 @@ import {
voidBill, voidBill,
} from '@/api/settlement/preSettlement'; } from '@/api/settlement/preSettlement';
import { preSettlementSearchFields } from '@/option/settlement/preSettlementSearch'; import { preSettlementSearchFields } from '@/option/settlement/preSettlementSearch';
import organizationSearch from '@/mixins/organization-search';
import { preSettlementTableColumns } from '@/option/settlement/preSettlementTable'; import { preSettlementTableColumns } from '@/option/settlement/preSettlementTable';
import { createSettlementTransfer } from '@/utils/settlement-transfer'; import { createSettlementTransfer } from '@/utils/settlement-transfer';
import { downloadFile } from '@/utils/util'; import { downloadFile } from '@/utils/util';
@@ -292,7 +303,7 @@ const emptyQuery = () => ({
preSettlementNo: '', preSettlementNo: '',
advanceNo: '', advanceNo: '',
projectName: '', projectName: '',
deptName: '', deptName: [],
contractName: '', contractName: '',
contractNo: '', contractNo: '',
payeeName: '', payeeName: '',
@@ -304,6 +315,7 @@ const emptyQuery = () => ({
export default { export default {
name: 'PreSettlement', name: 'PreSettlement',
components: { PreSettlementEditor }, components: { PreSettlementEditor },
mixins: [organizationSearch],
data() { data() {
return { return {
ArrowDown, ArrowDown,
@@ -357,6 +369,7 @@ export default {
}, },
}, },
created() { created() {
this.loadOrganizationOptions();
this.loadTable(); this.loadTable();
this.openRouteDetail(); this.openRouteDetail();
}, },
@@ -377,8 +390,10 @@ export default {
}, },
buildQuery() { buildQuery() {
const range = this.query.createDateRange || []; const range = this.query.createDateRange || [];
// 所属组织级联返回 id 路径,转成组织名称传给后端(用副本,避免污染搜索框回显)
const query = this.normalizeOrganizationSearch({ ...this.query }, 'deptName');
return { return {
...this.query, ...query,
createDateRange: undefined, createDateRange: undefined,
createStartDate: range[0], createStartDate: range[0],
createEndDate: range[1], createEndDate: range[1],
+16 -2
View File
@@ -25,6 +25,16 @@
:label="item.label" :label="item.label"
:value="item.value" :value="item.value"
/></el-select> /></el-select>
<el-cascader
v-else-if="field.type === 'cascader'"
v-model="query[field.prop]"
:options="organizationTreeOptions"
:props="organizationCascaderProps"
clearable
filterable
style="width: 100%"
placeholder="请选择"
/>
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" /> <el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
</el-form-item> </el-form-item>
<div class="settlement-adjustment-page__search-actions"> <div class="settlement-adjustment-page__search-actions">
@@ -159,6 +169,7 @@ import { ArrowDown, ArrowUp, Refresh } from '@element-plus/icons-vue';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import * as api from '@/api/settlement/settlementAdjustment'; import * as api from '@/api/settlement/settlementAdjustment';
import { settlementAdjustmentSearchFields } from '@/option/settlement/settlementAdjustmentSearch'; import { settlementAdjustmentSearchFields } from '@/option/settlement/settlementAdjustmentSearch';
import organizationSearch from '@/mixins/organization-search';
import { settlementAdjustmentTableColumns } from '@/option/settlement/settlementAdjustmentTable'; import { settlementAdjustmentTableColumns } from '@/option/settlement/settlementAdjustmentTable';
import SettlementAdjustmentEditor from './components/settlement-adjustment-editor.vue'; import SettlementAdjustmentEditor from './components/settlement-adjustment-editor.vue';
@@ -167,7 +178,7 @@ const emptyQuery = () => ({
createDateRange: [], createDateRange: [],
customerName: '', customerName: '',
projectName: '', projectName: '',
deptName: '', deptName: [],
formalSettlementNo: '', formalSettlementNo: '',
settlementType: '', settlementType: '',
approvalStatus: '', approvalStatus: '',
@@ -175,6 +186,7 @@ const emptyQuery = () => ({
export default { export default {
name: 'SettlementAdjustment', name: 'SettlementAdjustment',
components: { SettlementAdjustmentEditor }, components: { SettlementAdjustmentEditor },
mixins: [organizationSearch],
data: () => ({ data: () => ({
ArrowDown, ArrowDown,
ArrowUp, ArrowUp,
@@ -199,6 +211,7 @@ export default {
}, },
}, },
created() { created() {
this.loadOrganizationOptions();
this.loadTable(); this.loadTable();
}, },
methods: { methods: {
@@ -210,7 +223,8 @@ export default {
try { try {
const range = this.query.createDateRange || []; const range = this.query.createDateRange || [];
const { data } = await api.getList(this.page.current, this.page.size, { const { data } = await api.getList(this.page.current, this.page.size, {
...this.query, // 所属组织级联返回 id 路径,转成组织名称传给后端(用副本,避免污染搜索框回显)
...this.normalizeOrganizationSearch({ ...this.query }, 'deptName'),
createDateRange: undefined, createDateRange: undefined,
createStartDate: range[0], createStartDate: range[0],
createEndDate: range[1], createEndDate: range[1],
@@ -17,6 +17,16 @@
:value="item.value" :value="item.value"
/> />
</el-select> </el-select>
<el-cascader
v-else-if="field.type === 'cascader'"
v-model="query[field.prop]"
:options="organizationTreeOptions"
:props="organizationCascaderProps"
clearable
filterable
style="width: 100%"
placeholder="请选择"
/>
<el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" /> <el-input v-else v-model="query[field.prop]" clearable placeholder="请输入" />
</el-form-item> </el-form-item>
</div> </div>
@@ -87,6 +97,7 @@ import * as XLSX from 'xlsx';
import * as api from '@/api/settlement/transportReconciliation'; import * as api from '@/api/settlement/transportReconciliation';
import { transportReconciliationSearchFields } from '@/option/settlement/transportReconciliationSearch'; import { transportReconciliationSearchFields } from '@/option/settlement/transportReconciliationSearch';
import { transportReconciliationTableColumns } from '@/option/settlement/transportReconciliationTable'; import { transportReconciliationTableColumns } from '@/option/settlement/transportReconciliationTable';
import organizationSearch from '@/mixins/organization-search';
import TransportReconciliationEditor from './components/transport-reconciliation-editor.vue'; import TransportReconciliationEditor from './components/transport-reconciliation-editor.vue';
import TransportReconciliationTablePanel from './components/transport-reconciliation-table-panel.vue'; import TransportReconciliationTablePanel from './components/transport-reconciliation-table-panel.vue';
@@ -94,7 +105,7 @@ const emptyQuery = () => ({
reconciliationNo: '', reconciliationNo: '',
preSettlementNos: '', preSettlementNos: '',
projectName: '', projectName: '',
deptName: '', deptName: [],
contractNo: '', contractNo: '',
payerName: '', payerName: '',
payeeName: '', payeeName: '',
@@ -105,6 +116,7 @@ const emptyQuery = () => ({
export default { export default {
name: 'TransportReconciliation', name: 'TransportReconciliation',
components: { TransportReconciliationEditor, TransportReconciliationTablePanel }, components: { TransportReconciliationEditor, TransportReconciliationTablePanel },
mixins: [organizationSearch],
data() { data() {
return { return {
ArrowDown, ArrowDown,
@@ -129,6 +141,7 @@ export default {
}, },
}, },
mounted() { mounted() {
this.loadOrganizationOptions();
this.loadTable(); this.loadTable();
}, },
methods: { methods: {
@@ -139,7 +152,8 @@ export default {
this.loading = true; this.loading = true;
try { try {
const response = await api.getList(this.page.current, this.page.size, { const response = await api.getList(this.page.current, this.page.size, {
...this.query, // 所属组织级联返回 id 路径,转成组织名称传给后端(用副本,避免污染搜索框回显)
...this.normalizeOrganizationSearch({ ...this.query }, 'deptName'),
settlementType: this.settlementType, settlementType: this.settlementType,
}); });
const data = response?.data?.data || response?.data || response || {}; const data = response?.data?.data || response?.data || response || {};
+43 -1
View File
@@ -195,7 +195,7 @@
<el-col :span="6"><el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item></el-col> <el-col :span="6"><el-form-item label="邮箱"><el-input v-model="form.email" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="性别"><el-select v-model="form.sex"><el-option label="男" :value="1" /><el-option label="女" :value="2" /><el-option label="未知" :value="3" /></el-select></el-form-item></el-col> <el-col :span="6"><el-form-item label="性别"><el-select v-model="form.sex"><el-option label="男" :value="1" /><el-option label="女" :value="2" /><el-option label="未知" :value="3" /></el-select></el-form-item></el-col>
<el-col :span="6"><el-form-item label="岗位" prop="postId"><el-tree-select v-model="form.postId" :data="postList" :props="{ label: 'postName', value: 'id' }" check-strictly /></el-form-item></el-col> <el-col :span="6"><el-form-item label="岗位" prop="postId"><el-tree-select v-model="form.postId" :data="postList" :props="{ label: 'postName', value: 'id' }" check-strictly /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="所属组织" prop="deptId"><el-tree-select v-model="form.deptId" :data="permissionDeptTree" :props="{ label: 'title', value: 'id' }" multiple check-strictly show-checkbox /></el-form-item></el-col> <el-col :span="6"><el-form-item label="所属组织" prop="deptId"><el-cascader v-model="deptCascaderValue" :options="permissionDeptTree" :props="deptCascaderProps" clearable filterable placeholder="请选择 所属组织" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="工号"><el-input v-model="form.code" /></el-form-item></el-col> <el-col :span="6"><el-form-item label="工号"><el-input v-model="form.code" /></el-form-item></el-col>
<el-col :span="6"><el-form-item label="人员类别"><el-select v-model="form.personCategory"><el-option label="内部员工" :value="1" /><el-option label="承运商" :value="2" /></el-select></el-form-item></el-col> <el-col :span="6"><el-form-item label="人员类别"><el-select v-model="form.personCategory"><el-option label="内部员工" :value="1" /><el-option label="承运商" :value="2" /></el-select></el-form-item></el-col>
<el-col :span="18"><el-form-item label="数据权限范围"><el-radio-group v-model="form.dataScopeRange"><el-radio :value="1">仅所属组织</el-radio><el-radio :value="2">全部组织</el-radio><el-radio :value="3">自定义</el-radio></el-radio-group></el-form-item></el-col> <el-col :span="18"><el-form-item label="数据权限范围"><el-radio-group v-model="form.dataScopeRange"><el-radio :value="1">仅所属组织</el-radio><el-radio :value="2">全部组织</el-radio><el-radio :value="3">自定义</el-radio></el-radio-group></el-form-item></el-col>
@@ -443,6 +443,36 @@ export default {
}, },
computed: { computed: {
...mapGetters(['userInfo', 'permission']), ...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() { permissionList() {
return { return {
addBtn: this.validData(this.permission.user_add, false), addBtn: this.validData(this.permission.user_add, false),
@@ -565,6 +595,18 @@ export default {
this.customerList = res.data.data || []; 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() { submitRole() {
const roleList = this.$refs.treeRole.getCheckedKeys().join(','); const roleList = this.$refs.treeRole.getCheckedKeys().join(',');
if (this.roleFormMode) { if (this.roleFormMode) {
+19 -8
View File
@@ -490,14 +490,14 @@
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="所属组织" prop="organizationName"> <el-form-item label="所属组织" prop="organizationName">
<el-select v-model="driverForm.organizationName" clearable filterable> <el-cascader
<el-option v-model="organizationCascaderValue"
v-for="item in organizationOptions" :options="organizationTreeOptions"
:key="item.value" :props="organizationCascaderProps"
:label="item.label" filterable
:value="item.value" clearable
/> placeholder="请选择 所属组织"
</el-select> />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@@ -815,6 +815,15 @@ export default {
}, },
computed: { computed: {
...mapGetters(['permission', 'userInfo']), ...mapGetters(['permission', 'userInfo']),
// 所属组织级联:表单存组织名称,级联控件用 id 路径,两者在此双向转换
organizationCascaderValue: {
get() {
return this.resolveOrgPath(this.driverForm.organizationName);
},
set(path) {
this.driverForm.organizationName = this.resolveOrgName(path);
},
},
isAdmin() { isAdmin() {
const authority = this.userInfo.authority || ''; const authority = this.userInfo.authority || '';
return authority.includes('admin'); return authority.includes('admin');
@@ -1613,6 +1622,8 @@ export default {
this.onLoad(this.page); this.onLoad(this.page);
}, },
searchChange(params, done) { searchChange(params, done) {
// 所属组织级联返回的是 id 路径,转成组织名称传给后端
this.normalizeOrganizationSearch(params);
this.searchForm = params; this.searchForm = params;
this.page.currentPage = 1; this.page.currentPage = 1;
this.onLoad(this.page, params); this.onLoad(this.page, params);
+17 -11
View File
@@ -145,19 +145,14 @@
<el-row :gutter="18"> <el-row :gutter="18">
<el-col :span="6"> <el-col :span="6">
<el-form-item label="船舶所属组织" prop="organizationName"> <el-form-item label="船舶所属组织" prop="organizationName">
<el-select <el-cascader
v-model="shipForm.organizationName" v-model="organizationCascaderValue"
:options="organizationTreeOptions"
:props="organizationCascaderProps"
filterable filterable
clearable clearable
placeholder="请选择" placeholder="请选择 船舶所属组织"
> />
<el-option
v-for="item in organizationOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
@@ -707,6 +702,15 @@ export default {
}, },
computed: { computed: {
...mapGetters(['permission', 'userInfo']), ...mapGetters(['permission', 'userInfo']),
// 船舶所属组织级联:表单存组织名称,级联控件用 id 路径,两者在此双向转换
organizationCascaderValue: {
get() {
return this.resolveOrgPath(this.shipForm.organizationName);
},
set(path) {
this.shipForm.organizationName = this.resolveOrgName(path);
},
},
isAdmin() { isAdmin() {
const authority = this.userInfo.authority || ''; const authority = this.userInfo.authority || '';
return authority.includes('admin'); return authority.includes('admin');
@@ -1100,6 +1104,8 @@ export default {
this.onLoad(this.page); this.onLoad(this.page);
}, },
searchChange(params, done) { searchChange(params, done) {
// 所属组织级联返回的是 id 路径,转成组织名称传给后端
this.normalizeOrganizationSearch(params);
this.searchForm = params; this.searchForm = params;
this.page.currentPage = 1; this.page.currentPage = 1;
this.onLoad(this.page, params); this.onLoad(this.page, params);
+17 -11
View File
@@ -159,19 +159,14 @@
<el-row :gutter="18"> <el-row :gutter="18">
<el-col :span="24"> <el-col :span="24">
<el-form-item label="所有组织" prop="organizationName"> <el-form-item label="所有组织" prop="organizationName">
<el-select <el-cascader
v-model="vehicleForm.organizationName" v-model="organizationCascaderValue"
:options="organizationTreeOptions"
:props="organizationCascaderProps"
filterable filterable
clearable clearable
placeholder="请选择" placeholder="请选择 所属组织"
> />
<el-option
v-for="item in organizationOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row> </el-row>
@@ -709,6 +704,15 @@ export default {
}, },
computed: { computed: {
...mapGetters(['permission', 'userInfo']), ...mapGetters(['permission', 'userInfo']),
// 所属组织级联:表单存组织名称,级联控件用 id 路径,两者在此双向转换
organizationCascaderValue: {
get() {
return this.resolveOrgPath(this.vehicleForm.organizationName);
},
set(path) {
this.vehicleForm.organizationName = this.resolveOrgName(path);
},
},
isAdmin() { isAdmin() {
const authority = this.userInfo.authority || ''; const authority = this.userInfo.authority || '';
return authority.includes('admin'); return authority.includes('admin');
@@ -1275,6 +1279,8 @@ export default {
this.onLoad(this.page); this.onLoad(this.page);
}, },
searchChange(params, done) { searchChange(params, done) {
// 所属组织级联返回的是 id 路径,转成组织名称传给后端
this.normalizeOrganizationSearch(params);
this.searchForm = params; this.searchForm = params;
this.page.currentPage = 1; this.page.currentPage = 1;
this.onLoad(this.page, params); this.onLoad(this.page, params);
+87 -39
View File
@@ -335,17 +335,13 @@
</el-col> </el-col>
<el-col :span="6"> <el-col :span="6">
<el-form-item label="所属组织" prop="deptName"> <el-form-item label="所属组织" prop="deptName">
<el-tree-select <el-cascader
v-model="archiveForm.deptId" v-model="deptCascaderValue"
:data="deptTree" :options="deptTree"
:props="{ label: 'label', value: 'value', children: 'children' }" :props="deptCascaderProps"
multiple
node-key="value"
check-strictly
filterable
clearable clearable
:render-after-expand="false" filterable
@change="onDeptChange" placeholder="请选择 所属组织"
/> />
</el-form-item> </el-form-item>
</el-col> </el-col>
@@ -1367,7 +1363,7 @@
<script> <script>
import { h } from 'vue'; import { h } from 'vue';
import { ElTreeSelect } from 'element-plus'; import { ElCascader } from 'element-plus';
import { import {
getList, getList,
getDetail, getDetail,
@@ -1875,6 +1871,34 @@ export default {
emitPath: true, emitPath: true,
}; };
}, },
deptCascaderProps() {
return {
value: 'value',
label: 'label',
children: 'children',
emitPath: true,
checkStrictly: true,
expandTrigger: 'click',
};
},
// 所属组织级联值:archiveForm.deptId 只存选中的部门 ID(单选),
// 级联控件需要根到选中节点的完整路径,两者在此做双向转换——
// 部门树是异步加载的,依赖 deptTree 可保证树加载完成后回显自动刷新。
deptCascaderValue: {
get() {
const ids = Array.isArray(this.archiveForm.deptId)
? this.archiveForm.deptId
: this.archiveForm.deptId
? [this.archiveForm.deptId]
: [];
const currentId = ids.length ? String(ids[ids.length - 1]) : '';
if (!currentId) return [];
return this.findDeptPath(currentId) || [currentId];
},
set(path) {
this.onDeptChange(path);
},
},
ids() { ids() {
let ids = []; let ids = [];
this.selectionList.forEach(ele => { this.selectionList.forEach(ele => {
@@ -2645,38 +2669,57 @@ export default {
this.onDeptChange([firstTopLevelDept.value]); this.onDeptChange([firstTopLevelDept.value]);
}, },
onDeptChange(value) { onDeptChange(value) {
const deptIds = Array.isArray(value) ? value : value ? [value] : []; // 级联单选:value 为根到选中节点的 ID 路径,取末级作为客商所属组织
const deptNames = deptIds const path = Array.isArray(value)
.map(id => this.deptOptions.find(item => String(item.value) === String(id))?.rawLabel) ? value.filter(item => item !== undefined && item !== null && item !== '')
.filter(Boolean); : [];
this.archiveForm.deptId = deptIds; const deptId = path.length ? String(path[path.length - 1]) : '';
this.archiveForm.deptIds = deptIds.join(','); this.archiveForm.deptId = deptId ? [deptId] : [];
this.archiveForm.deptName = deptNames.join(','); this.archiveForm.deptIds = deptId;
this.archiveForm.deptName = deptId ? this.findDeptLabel(deptId) : '';
this.$refs.archiveForm?.validateField('deptName'); this.$refs.archiveForm?.validateField('deptName');
}, },
findDeptPath(id, tree = this.deptTree, parents = []) {
for (const node of tree || []) {
const path = [...parents, String(node.value)];
if (String(node.value) === String(id)) return path;
const children = node.children || [];
if (children.length) {
const matched = this.findDeptPath(id, children, path);
if (matched) return matched;
}
}
return null;
},
findDeptLabel(id, tree = this.deptTree) {
for (const node of tree || []) {
if (String(node.value) === String(id)) return node.label;
const matched = this.findDeptLabel(id, node.children || []);
if (matched) return matched;
}
return '';
},
renderDeptSearch(scope) { renderDeptSearch(scope) {
// 搜索区所属组织:多选树形下拉(与新增/编辑表单一致) // 搜索区所属组织:单选级联(与新增/编辑表单一致)
// 多选返回 ID 数组,转换为部门名称按逗号拼接写入 searchForm.deptName // 级联返回的是根到选中节点的 ID 路径数组,提交时取末级 ID 转部门名称
return h(ElTreeSelect, { return h(ElCascader, {
modelValue: Array.isArray(scope.row?.deptName) ? scope.row.deptName : [], modelValue: Array.isArray(scope.row?.deptName) ? scope.row.deptName : [],
'onUpdate:modelValue': value => { 'onUpdate:modelValue': value => {
// 多选模式modelValue 必须是 ID 数组,否则 el-tree-select 内部状态会错乱导致无法再次选中 // 级联单选modelValue 必须是 ID 路径数组,否则控件内部状态会错乱导致无法再次选中
const ids = Array.isArray(value) ? value : value ? [value] : []; if (scope.row) scope.row.deptName = Array.isArray(value) ? value : [];
if (scope.row) scope.row.deptName = ids; },
options: this.deptTree,
props: {
label: 'label',
value: 'value',
children: 'children',
checkStrictly: true,
expandTrigger: 'click',
}, },
data: this.deptTree,
'node-key': 'value',
'check-strictly': true,
multiple: true,
filterable: true, filterable: true,
clearable: true, clearable: true,
'render-after-expand': false,
collapseTags: true,
'collapse-tags-tooltip': true,
'default-expand-all': false,
style: 'width: 100%', style: 'width: 100%',
placeholder: '请选择 所属组织', placeholder: '请选择 所属组织',
props: { label: 'label', value: 'value', children: 'children' },
}); });
}, },
handleNoFixedTermChange(checked) { handleNoFixedTermChange(checked) {
@@ -2717,13 +2760,15 @@ export default {
: []; : [];
}, },
normalizeDeptIds(detail) { normalizeDeptIds(detail) {
// 所属组织为单选级联,只保留第一个部门 ID(历史多选数据自动收敛)
const value = detail.deptIds || detail.deptId; const value = detail.deptIds || detail.deptId;
return value const ids = value
? String(value) ? String(value)
.split(',') .split(',')
.map(item => item.trim()) .map(item => item.trim())
.filter(item => item !== '') .filter(item => item !== '')
: []; : [];
return ids.length ? [ids[0]] : [];
}, },
formatArchiveAddress(archive = {}) { formatArchiveAddress(archive = {}) {
return [archive.registeredRegionName, archive.registeredDetailAddress] return [archive.registeredRegionName, archive.registeredDetailAddress]
@@ -4539,12 +4584,15 @@ export default {
this.onLoad(this.page); this.onLoad(this.page);
}, },
searchChange(params, done) { searchChange(params, done) {
// 搜索区所属组织多选返回的是部门 ID 数组,转成部门名称逗号串传给后端(与列表接口 deptName 字段一致) // 搜索区所属组织级联返回的是 ID 路径数组,取末级 ID 转成部门名称传给后端(与列表接口 deptName 字段一致)
if (Array.isArray(params.deptName)) { if (Array.isArray(params.deptName)) {
params.deptName = params.deptName const path = params.deptName.filter(
.map(id => this.deptOptions.find(item => String(item.value) === String(id))?.rawLabel) item => item !== undefined && item !== null && item !== ''
.filter(Boolean) );
.join(','); const deptId = path.length ? path[path.length - 1] : '';
params.deptName = deptId
? this.deptOptions.find(item => String(item.value) === String(deptId))?.rawLabel || ''
: '';
} }
this.query = this.buildQuery(params); this.query = this.buildQuery(params);
this.page.currentPage = 1; this.page.currentPage = 1;
+13 -13
View File
@@ -47,26 +47,26 @@ export default ({ mode, command }) => {
__VUE_I18N_LEGACY_API__: true, __VUE_I18N_LEGACY_API__: true,
__INTLIFY_PROD_DEVTOOLS__: false, __INTLIFY_PROD_DEVTOOLS__: false,
}, },
server: {
port: 2888,
proxy: {
'/api': {
target: 'http://localhost',
//target: 'https://saber3.bladex.cn/api',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, ''),
},
},
},
// server: { // server: {
// port: 2889, // port: 2888,
// proxy: { // proxy: {
// '/api': { // '/api': {
// target: 'http://172.16.203.228:8000', // target: 'http://localhost',
// //target: 'https://saber3.bladex.cn/api',
// changeOrigin: true, // changeOrigin: true,
// rewrite: path => path.replace(/^\/api/, ''),
// }, // },
// }, // },
// }, // },
server: {
port: 2889,
proxy: {
'/api': {
target: 'http://172.16.203.228:8000',
changeOrigin: true,
},
},
},
resolve: { resolve: {
alias: { alias: {
'~': resolve(__dirname, './'), '~': resolve(__dirname, './'),