feat(login): 更换登录验证码为图形字符验证码

- 配置文件 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,增强所属组织搜索功能一致性
This commit is contained in:
2026-08-28 15:41:01 +08:00
parent 99d6284a58
commit f7eea88945
14 changed files with 231 additions and 56 deletions
+115
View File
@@ -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. 混入本 mixinmixins: [organizationSearch]
* 2. option 中「所属组织」列加上 searchslot: trueprop 为 organizationName 或 deptName 均可)
* 3. 在 created / 初始化时调用 this.loadOrganizationOptions()
*
* 说明:
* - 自动识别 organizationName / deptName 两种 prop,并挂载 column.renderSearch
* - 树节点标识统一用 idnode-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' },
});
},
},
};