From f7eea8894539f74fa4b016746b02d821ad329893 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=B5=B5=E5=BF=A0=E6=9E=97?= <170083662@qq.com>
Date: Fri, 28 Aug 2026 15:41:01 +0800
Subject: [PATCH] =?UTF-8?q?feat(login):=20=E6=9B=B4=E6=8D=A2=E7=99=BB?=
=?UTF-8?q?=E5=BD=95=E9=AA=8C=E8=AF=81=E7=A0=81=E4=B8=BA=E5=9B=BE=E5=BD=A2?=
=?UTF-8?q?=E5=AD=97=E7=AC=A6=E9=AA=8C=E8=AF=81=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 配置文件 website.js 中将 captchaType 从 'behavior' 改为 'image'
- 修改 userlogin.vue,图形验证码图片由输入框内联改为输入框旁独立显示
- 重写 login.scss 样式,实现验证码图片区域带边框和高亮效果
- 确保验证码图片点击可刷新,且支持从后端接口获取数据
- 登录页仍默认显示 IAM 登录,需切换「账号密码登录」显示验证码区域
feat(contract-manage): 优化所属组织搜索栏为树形下拉选择框
- 引入并应用 organization-search mixin 统一管理所属组织搜索
- contract-manage.js、driver.js、transport-ship.js、transport-vehicle.js 中所属组织搜索改为 searchslot 模式
- contract-manage.vue 中新增 renderOrganizationSearch 方法,渲染基于 ElTreeSelect 的树形下拉组件
- 组织树数据加载时自动排除「外部组织」节点,保持搜索选项清晰
- 所有相关所属组织搜索统一样式与交互,提升用户体验
feat(system): 用户和部门视图样式及操作改进
- user.vue 和 userinfo.vue 中 el-tabs 组件添加 border-card 类型,提升标签页视觉效果
- dept.vue 中添加查看、编辑、删除操作入口,绑定对应方法,通过权限控制显示权限按钮
- dept.vue 菜单宽度、对话框宽度及行为配置优化
style(login): 调整登录样式及验证码布局细节
- 调整 .login-code-wrap 及内部元素的 flex 布局,输入框与验证码图片并排显示
- 设置验证码图片容器尺寸、边框及 hover 高亮样式
- 确保验证码图片撑满容器并禁止用户选中
refactor(mixins): 新增 organization-search 统一搜索 mixin
- 实现组织树数据加载、排除外部节点、归一化及扁平化处理
- 提供 renderOrganizationSearch 方法用于渲染统一树形选择搜索框
- 减少重复代码,方便各页面快速接入和统一维护
chore(vehicle): 车辆页面引入 organization-search mixin
- 在车辆视图 vehicle.vue 中混入 organization-search,增强所属组织搜索功能一致性
---
.workbuddy/memory/2026-08-28.md | 10 ++
src/config/website.js | 2 +-
src/mixins/organization-search.js | 115 ++++++++++++++++++
src/option/business/contract-manage.js | 13 +-
src/option/transportCapacity/driver.js | 4 +-
.../transportCapacity/transport-ship.js | 4 +-
.../transportCapacity/transport-vehicle.js | 4 +-
src/page/login/userlogin.vue | 31 ++---
src/styles/login.scss | 41 ++++---
src/views/business/contract-manage.vue | 34 +++++-
src/views/system/dept.vue | 22 +++-
src/views/system/user.vue | 2 +-
src/views/system/userinfo.vue | 2 +-
src/views/transportCapacity/vehicle.vue | 3 +
14 files changed, 231 insertions(+), 56 deletions(-)
create mode 100644 src/mixins/organization-search.js
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 {