diff --git a/.workbuddy/memory/2026-08-28.md b/.workbuddy/memory/2026-08-28.md index 900be52..ce01358 100644 --- a/.workbuddy/memory/2026-08-28.md +++ b/.workbuddy/memory/2026-08-28.md @@ -33,3 +33,13 @@ - 样式:`.el-select { width: 180px }` 已成 dead code,移除。 - `:deep(...)` 调整:去掉 `margin-right:auto`,只留 `order:-1`。`.avue-form__menu` 本身 `flex / flex-end`,check-tag 组排首项后,整体仍紧靠右侧,与 `[查询][重置][收起]` 形成 `[快捷筛选] [查询] [重置] [收起]` 右对齐序列。 +## 登录验证码:滑块(behavior) → 图形字符验证码(image) +- 文件:`src/config/website.js`(第14行 `captchaType: 'behavior'` → `'image'`)、`src/page/login/userlogin.vue`、`src/styles/login.scss` +- 背景:登录页原 `captchaType:'behavior'`(滑块/旋转行为验证)。需求改成截图中的「输入框 + 右侧独立彩色字符验证码图片」。 +- 改动: + 1. `website.js` 改 `captchaType: 'image'` 启用图形验证码分支(代码原本就有 image 模式的 UI 与 `getCaptcha()` 逻辑,仅未启用)。 + 2. `userlogin.vue` image 分支模板:验证码图片从 `el-input` 的 `#append` 内联改为 form-item 内 `.login-code-wrap` 并排独立方块(输入框 flex:1 + 右侧 130px 图片块),点击刷新。 + 3. `login.scss` 重写 `.login-code/.login-code-wrap/.login-code-box/.login-code-img`:flex 并排、图片块带 1px 边框 + hover 主色高亮、`object-fit:fill` 撑满。 +- 验证:`vite build --outDir /tmp/tms-verify-build` 全量编译通过(exit 0,无报错)。注意原 `pnpm build` 因 WorkBuddy safe-delete 钩子拦截清空 dist/(741 文件)而中断,非代码问题。 +- 注意:登录页 `activeName` 默认 `'iam'`(IAM 统一登录),图形验证码在 `userlogin.vue`,需点「账号密码登录」才可见;验证码图片依赖后端 `/blade-auth/oauth/captcha` 返回的 `data.key` + `data.image`。 + diff --git a/src/config/website.js b/src/config/website.js index 5172e82..9580713 100644 --- a/src/config/website.js +++ b/src/config/website.js @@ -11,7 +11,7 @@ export default { tenantMode: true, // 是否开启租户模式 tenantId: '000000', // 管理组租户编号 captchaMode: true, // 是否开启验证码模式 - captchaType: 'behavior', // 验证码类型(image:首屏图形验证码 behavior:登录时弹出点选/滑块/旋转随机行为验证) + captchaType: 'image', // 验证码类型(image:首屏图形验证码 behavior:登录时弹出点选/滑块/旋转随机行为验证) switchMode: false, // 是否开启登录切换角色部门 lockPage: '/lock', // 锁屏页面地址 tokenTime: 3000, // 定时刷新token间隔(单位:毫秒) diff --git a/src/mixins/organization-search.js b/src/mixins/organization-search.js new file mode 100644 index 0000000..10f3061 --- /dev/null +++ b/src/mixins/organization-search.js @@ -0,0 +1,115 @@ +import { getDeptTree } from '@/api/system/dept'; +import { ElTreeSelect } from 'element-plus'; +import { h } from 'vue'; + +/** + * 所属组织 统一搜索 mixin + * ------------------------------------------------------------------ + * 全站「所属组织」搜索栏统一为树形下拉(el-tree-select),风格与客户档案 + * (vehicle/customer-archive.vue)保持一致。 + * + * 接入步骤(在使用了 avue-crud 的页面中): + * 1. 混入本 mixin:mixins: [organizationSearch] + * 2. option 中「所属组织」列加上 searchslot: true(prop 为 organizationName 或 deptName 均可) + * 3. 在 created / 初始化时调用 this.loadOrganizationOptions() + * + * 说明: + * - 自动识别 organizationName / deptName 两种 prop,并挂载 column.renderSearch + * - 树节点标识统一用 id(node-key='id'),返回值为部门 id + * - 页面若已有 excludeExternalOrganization / normalizeOrgTree 等方法, + * 组件自身方法优先级更高,会覆盖本 mixin 的默认实现 + * - 搜索值(id)需在各自的 searchChange / normalizeSearch 中按后端要求 + * 转换为部门名称(参考 contract-manage 的 normalizeSearch) + */ +const ORG_PROPS = ['organizationName', 'deptName']; + +export default { + data() { + return { + // 树形组织数据,仅供搜索栏 el-tree-select 使用; + // 与页面自身 organizationOptions(多为表单扁平列表)隔离,避免相互覆盖 + organizationTreeOptions: [], + organizationTreeFlatOptions: [], + organizationLoading: false, + }; + }, + methods: { + loadOrganizationOptions() { + if (this.organizationLoading) return; + this.organizationLoading = true; + const tenantId = this.userInfo?.tenantId; + getDeptTree(tenantId) + .then(res => { + 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.organizationTreeOptions); + 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; + }); + }, + // 默认排除「外部组织」节点;页面可覆盖 + 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, + }; + }); + }, + flattenOrgTree(tree = []) { + return (tree || []).flatMap(item => [ + { id: String(item.id), label: item.label, rawLabel: item.rawLabel }, + ...this.flattenOrgTree(item.children || []), + ]); + }, + findOrgById(id) { + const value = Array.isArray(id) ? id.at(-1) : id; + return this.organizationTreeFlatOptions.find(item => String(item.id) === String(value)); + }, + renderOrganizationSearch(scope, prop = 'organizationName') { + return h(ElTreeSelect, { + modelValue: scope.row?.[prop] || '', + 'onUpdate:modelValue': value => { + if (scope.row) scope.row[prop] = value; + }, + 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/business/contract-manage.js b/src/option/business/contract-manage.js index 77bfd77..b416f07 100644 --- a/src/option/business/contract-manage.js +++ b/src/option/business/contract-manage.js @@ -286,20 +286,11 @@ export const option = createCrudOption([ prop: 'organizationName', search: true, searchOrder: 3, - searchType: 'cascader', - searchPlaceholder: '全部', + searchslot: true, + searchPlaceholder: '请选择 所属组织', formslot: true, order: 120, dicData: [], - props: { - label: 'label', - value: 'id', - children: 'children', - }, - checkStrictly: true, - emitPath: false, - showAllLevels: false, - filterable: true, minWidth: 150, rules: selectRule('所属组织'), }, diff --git a/src/option/transportCapacity/driver.js b/src/option/transportCapacity/driver.js index 0027a84..25c004c 100644 --- a/src/option/transportCapacity/driver.js +++ b/src/option/transportCapacity/driver.js @@ -81,9 +81,7 @@ export const option = { prop: 'organizationName', search: true, searchOrder: 1, - type: 'select', - filterable: true, - dicData: [], + searchslot: true, slot: true, minWidth: 160, }, diff --git a/src/option/transportCapacity/transport-ship.js b/src/option/transportCapacity/transport-ship.js index dced0f4..8e4c44a 100644 --- a/src/option/transportCapacity/transport-ship.js +++ b/src/option/transportCapacity/transport-ship.js @@ -37,9 +37,7 @@ export const option = { prop: 'organizationName', search: true, searchOrder: 1, - type: 'select', - filterable: true, - dicData: [], + searchslot: true, slot: true, minWidth: 160, }, diff --git a/src/option/transportCapacity/transport-vehicle.js b/src/option/transportCapacity/transport-vehicle.js index 99f9e47..50247ad 100644 --- a/src/option/transportCapacity/transport-vehicle.js +++ b/src/option/transportCapacity/transport-vehicle.js @@ -46,9 +46,7 @@ export const option = { prop: 'organizationName', search: true, searchOrder: 1, - type: 'select', - filterable: true, - dicData: [], + searchslot: true, slot: true, minWidth: 150, }, diff --git a/src/page/login/userlogin.vue b/src/page/login/userlogin.vue index 5972557..83b7910 100644 --- a/src/page/login/userlogin.vue +++ b/src/page/login/userlogin.vue @@ -46,21 +46,22 @@ - - - - +
+ + + + +
this.renderOrganizationSearch(scope); + } this.syncCurrentOrganizationOption(); }) .finally(() => { @@ -1748,6 +1751,24 @@ export default { this.organizationOptions.unshift(item); this.organizationFlatOptions.unshift(item); }, + renderOrganizationSearch(scope) { + // 搜索区所属组织:树形下拉(与客户档案统一) + return h(ElTreeSelect, { + modelValue: scope.row?.organizationName || '', + 'onUpdate:modelValue': value => { + if (scope.row) scope.row.organizationName = value; + }, + 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) { this.form[prop] = String(value || '').replace(/\D/g, ''); }, @@ -2089,6 +2110,15 @@ export default {