import { getDeptTree, getPlatformCompanySelect } from '@/api/system/dept'; import { ElCascader } from 'element-plus'; import { h } from 'vue'; /** * 所属组织 统一搜索 mixin * ------------------------------------------------------------------ * 全站「所属组织」搜索栏统一为级联选择器(el-cascader,单选),风格与客商档案 * (vehicle/customer-archive.vue)保持一致。 * * 接入步骤(在使用了 avue-crud 的页面中): * 1. 混入本 mixin:mixins: [organizationSearch] * 2. option 中「所属组织」列加上 searchslot: true(prop 为 organizationName 或 deptName 均可) * 3. 在 created / 初始化时调用 this.loadOrganizationOptions() * 4. 需要「未分配组织」筛选项的页面在 data 里置 enableUnassignedOrgFilter: true; * 该项只追加进搜索树 organizationSearchTreeOptions,表单级联 organizationTreeOptions 不含哨兵 * 5. 表单「所属组织」只允许选平台公司的页面(运力管理)在 data 里置 * restrictFormOrgToPlatformCompany: true,表单级联样式改用 * buildFormOrganizationTree(...) 的结果,并在打开表单时把回显的组织名写进 * formOrganizationLegacyName;只影响表单,搜索栏仍是全量组织 * * 说明: * - 自动识别 organizationName / deptName 两种 prop,并挂载 column.renderSearch * - 树节点标识统一用 id,级联值为「根 → 选中节点」的 id 路径数组 * - checkStrictly 打开,任意层级(含非末级)都可直接选中 * - 页面若已有 excludeExternalOrganization / normalizeOrgTree 等方法, * 组件自身方法优先级更高,会覆盖本 mixin 的默认实现 * - 搜索值需在各自的 searchChange / normalizeSearch 中按后端要求转换为部门名称: * 级联值是路径数组,统一用 findOrgById(路径)?.rawLabel 取末级名称 * - 表单区(新增/编辑)需要回显时,可用 findOrgPath(deptId) 把 id 转成路径, * 参考 customer-archive 的 deptCascaderValue 计算属性 */ const ORG_PROPS = ['organizationName', 'deptName']; // 「未分配组织」筛选项的哨兵值:不是真实组织名,选中后提交给后端的就是这串字面量。 // 后端 TransportBusiConstant.UNASSIGNED_ORG_FILTER、DriverMapper.xml / TransportVehicleMapper.xml // 的 organizationName 分支以及司机、车辆导出查询都按它改判为 organization_name = ''; // 后端换成空串会让整段组织条件不生效,筛选静默失效。前端与后端这几处必须同步修改。 const UNASSIGNED_ORG_FILTER = '__UNASSIGNED_ORG__'; // 表单「所属组织」限定只能选平台公司时,存量记录里的旧组织名在组织树里已经找不到 // (节点被删或改名),用这个 id 补一个只读节点,否则编辑老数据时所属组织显示为空、 // 保存即把原值丢掉。 const FORM_LEGACY_ORG_ID = '__LEGACY_ORG__'; export default { data() { return { // 树形组织数据,仅供搜索栏 el-cascader 使用; // 与页面自身 organizationOptions(多为表单扁平列表)隔离,避免相互覆盖 organizationTreeOptions: [], organizationTreeFlatOptions: [], organizationLoading: false, // 组织筛选是否追加「未分配组织」一项;页面在 data 里覆盖为 true 才生效 enableUnassignedOrgFilter: false, // 表单「所属组织」是否只允许选平台公司;页面在 data 里覆盖为 true 才生效 restrictFormOrgToPlatformCompany: false, // 平台公司部门 id(字符串),loadOrganizationOptions 时一并拉取 platformCompanyOrgIds: [], // 表单当前回显的组织名,树里没有同名节点时用它补一个只读选项 formOrganizationLegacyName: '', // 进行中的组织数据请求,并发调用共用(不是响应式状态,只做去重) organizationOptionsPromise: null, }; }, computed: { // 搜索栏级联的选项:开关打开时在真实组织最前面追加「未分配组织」。 // 不能直接改 organizationTreeOptions——司机/车辆页的编辑表单也用这棵树选组织。 organizationSearchTreeOptions() { return this.enableUnassignedOrgFilter ? [this.unassignedOrgOption(), ...this.organizationTreeOptions] : this.organizationTreeOptions; }, // 所属组织级联配置:checkStrictly 允许选中任意层级,不必选到末级。 // 限定只能选平台公司时用 disabled 函数只放开平台公司:非平台公司的非末级节点 // 在 checkStrictly 下仍可展开(el-cascader 的 expandable 判断),末级节点点击无效, // 两者都选不中。disabled 在节点渲染时求值,平台公司 id 到手后节点会自行重算。 organizationCascaderProps() { const props = { value: 'id', label: 'label', children: 'children', emitPath: true, checkStrictly: true, expandTrigger: 'click', }; if (this.restrictFormOrgToPlatformCompany) { props.disabled = data => this.platformCompanyOrgDisabled(data); } return props; }, }, methods: { loadOrganizationOptions() { // 并发调用共用同一次请求,返回的 Promise 在数据就绪后才 resolve if (this.organizationOptionsPromise) return this.organizationOptionsPromise; this.organizationLoading = true; const tenantId = this.userInfo?.tenantId; // 平台公司下拉失败不能拖垮组织树,单独兜住,失败时保持上一次的可选范围 const platformCompanyRequest = this.restrictFormOrgToPlatformCompany ? getPlatformCompanySelect().catch(() => null) : Promise.resolve(null); this.organizationOptionsPromise = Promise.all([getDeptTree(tenantId), platformCompanyRequest]) .then(([res, platformRes]) => { this.applyPlatformCompanyOptions(platformRes); const source = res.data?.data || res.data || []; const tree = this.excludeExternalOrganization ? this.excludeExternalOrganization(source) : source; this.organizationTreeOptions = this.normalizeOrgTree(tree); this.organizationTreeFlatOptions = this.flattenOrgTree(this.organizationSearchTreeOptions); const option = this.tableOption || this.option; if (!option) return; ORG_PROPS.forEach(prop => { const column = this.findColumn?.(option.column, prop); if (column) { column.renderSearch = scope => this.renderOrganizationSearch(scope, prop); } }); }) .finally(() => { this.organizationLoading = false; this.organizationOptionsPromise = null; }); return this.organizationOptionsPromise; }, // 组织树与平台公司列表就绪后执行回调;已加载过则立即执行 whenOrganizationOptionsReady(callback) { const pending = this.organizationTreeFlatOptions.length ? Promise.resolve() : this.loadOrganizationOptions(); return Promise.resolve(pending).then(callback, () => callback()); }, applyPlatformCompanyOptions(res) { if (!res) return; const list = res.data?.data || res.data || []; this.platformCompanyOrgIds = (Array.isArray(list) ? list : []).map(item => String(item.id)); }, // 默认排除「外部组织」节点;页面可覆盖 excludeExternalOrganization(tree = []) { return (tree || []).reduce((result, item) => { const name = item.title || item.deptName || item.name || item.label || ''; if (String(name).trim() === '外部组织') return result; result.push({ ...item, children: this.excludeExternalOrganization(item.children || []), }); return result; }, []); }, normalizeOrgTree(tree = []) { return (tree || []).map(item => { const label = item.title || item.deptName || item.name || item.label || ''; const children = this.normalizeOrgTree(item.children || []); return { ...item, id: String(item.id), label, rawLabel: label, children: children.length ? children : undefined, }; }); }, // 哨兵节点的 id 与 rawLabel 都取哨兵值,resolveOrgName 取到的仍是哨兵而不是某个真实组织名 unassignedOrgOption() { return { id: UNASSIGNED_ORG_FILTER, label: '未分配组织', rawLabel: UNASSIGNED_ORG_FILTER }; }, flattenOrgTree(tree = []) { return (tree || []).flatMap(item => [ { id: String(item.id), label: item.label, rawLabel: item.rawLabel }, ...this.flattenOrgTree(item.children || []), ]); }, // 组织列渲染用:空串或纯空白即「未分配组织」(组织列 NOT NULL,无组织存空串) isBlankOrganizationName(value) { return String(value ?? '').trim() === ''; }, // 兼容两种入参:级联路径数组(取末级)或单个部门 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; // 必须显式传 node.children || []:叶节点的 children 是 undefined, // 传 undefined 会触发默认参数重新拿到整棵树,逐层重复扫描直到爆栈 const matched = this.findOrgPath(target, node.children || [], path); if (matched) return matched; } return null; }, // 由部门 id 取组织名称(末级),供表单提交 findOrgName(id) { return this.findOrgById(id)?.rawLabel || ''; }, // 级联节点的 disabled 判定:只有平台公司可选。页面自建 props 时也复用它 platformCompanyOrgDisabled(data) { return !this.platformCompanyOrgIds.includes(String(data?.id ?? '')); }, // 组织名对应的节点是否为平台公司;名称在树里不存在时按「不是」处理 isPlatformCompanyOrgName(name) { const value = String(name ?? '').trim(); if (!value) return false; const node = this.organizationTreeFlatOptions.find(item => item.rawLabel === value); return !!node && this.platformCompanyOrgIds.includes(String(node.id)); }, // 新增表单的默认组织:只有落在平台公司上才预填,否则留空(不能让用户从默认值 // 直接带出一个选不中的组织名) defaultFormOrganizationName(name) { return this.isPlatformCompanyOrgName(name) ? String(name).trim() : ''; }, // 表单级联的选项树:限定平台公司时,把回显的原组织名补成一个只读节点, // 否则存量记录的组织在树里找不到,编辑时显示为空、保存即丢原值 buildFormOrganizationTree(tree = this.organizationTreeOptions) { const source = tree || []; if (!this.restrictFormOrgToPlatformCompany) return source; const name = String(this.formOrganizationLegacyName || '').trim(); if (!name) return source; if (this.organizationTreeFlatOptions.some(item => item.rawLabel === name)) return source; return [...source, { id: FORM_LEGACY_ORG_ID, label: name, rawLabel: name }]; }, // 表单级联回显用的路径:树里有该组织就用真实路径,否则落到原组织兜底节点 resolveFormOrgPath(name) { const path = this.resolveOrgPath(name); if (path.length) return path; const value = String(name ?? '').trim(); if (!value) return []; return value === String(this.formOrganizationLegacyName || '').trim() ? [FORM_LEGACY_ORG_ID] : []; }, // 记录表单当前回显的组织名:能匹配真实节点(或已清空)就不需要兜底节点, // 否则记下来,由 buildFormOrganizationTree 补成只读选项 syncFormOrganizationLegacy(name) { const value = String(name ?? '').trim(); if (!value || this.organizationTreeFlatOptions.some(item => item.rawLabel === value)) { this.formOrganizationLegacyName = ''; return; } this.formOrganizationLegacyName = value; }, // 组织名称 → 级联路径(表单回显用;部门树异步加载完成后会自动重算) 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 !== '') : []; if (!value.length) return ''; const node = this.findOrgById(value); if (node) return node.rawLabel || ''; // 原组织兜底节点不在扁平表里,直接取它代表的历史名称 return String(value[value.length - 1]) === FORM_LEGACY_ORG_ID ? String(this.formOrganizationLegacyName || '').trim() : ''; }, // 搜索提交前把级联路径数组就地转成组织名称(后端按名称过滤) normalizeOrganizationSearch(params, prop = 'organizationName') { if (Array.isArray(params?.[prop])) { params[prop] = this.resolveOrgName(params[prop]); } return params; }, renderOrganizationSearch(scope, prop = 'organizationName') { return h(ElCascader, { modelValue: Array.isArray(scope.row?.[prop]) ? scope.row[prop] : [], 'onUpdate:modelValue': value => { // 级联单选:modelValue 必须是 id 路径数组,否则控件内部状态会错乱导致无法再次选中 if (scope.row) scope.row[prop] = Array.isArray(value) ? value : []; }, options: this.organizationSearchTreeOptions, props: { label: 'label', value: 'id', children: 'children', checkStrictly: true, expandTrigger: 'click', }, filterable: true, clearable: true, style: 'width: 100%', placeholder: '请选择 所属组织', }); }, }, };