fix: 修复员工门户登录租户选择和数据展示问题

## 主要修改

### 1. 登录租户选择修复
- 新增全局租户状态管理(useLoginTenantId hook)
  - 使用 @vueuse/core 的 createGlobalState 持久化租户选择
  - 确保组件重新挂载时租户ID不丢失
- 修复登录时租户自动跳回第一个的问题
  - 删除登录时强制覆盖租户ID的代码
  - 用户选择的租户现在会被正确使用
- 恢复"记住登录"功能
  - 自动恢复上次登录的租户、用户名、密码

### 2. ERP 动态API迁移
- 员工门户和经销商门户的ERP API从硬编码迁移到动态API系统
  - /erp/customer/* → /erp/dynamic/v1/customer/*
  - 新增 customer/list, customer/detail, sales-areas, brands API
- 修复API响应嵌套结构问题
  - 动态API返回数据嵌套在 data 字段中
  - 调整响应类型定义和数据处理逻辑

### 3. 经销商管理数据显示修复
- 处理动态API响应的嵌套数据结构
  - 兼容 res.rows 和 res.data.rows 两种格式
  - 添加详细调试日志便于排查问题

## 文件清单
- 新增文件:
  - hzhub-portal-employee/src/hooks/useLoginTenantId.ts
  - hzhub-portal-dealer/src/hooks/useLoginTenantId.ts
  - hzhub-portal-employee/src/api/erp/index.ts
  - hzhub-portal-dealer/src/api/erp/index.ts

- 修改文件:
  - 登录组件:TenantAccountPassword.vue, AccountPassword.vue
  - API文件:auth/index.ts, auth/types.ts
  - 经销商页面:dealer/index.vue

## 测试验证
-  租户下拉列表正常显示
-  选择租户后不会跳回第一个
-  记住登录功能可用
-  经销商管理页面数据正常显示

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
大壮
2026-05-08 07:47:04 +00:00
parent 278e507e8a
commit e6fc123b1f
11 changed files with 867 additions and 122 deletions

View File

@@ -1,8 +1,11 @@
import type { EmailCodeDTO, LoginDTO, LoginVO, RegisterDTO } from './types';
import { post } from '@/utils/request';
import type { EmailCodeDTO, LoginDTO, LoginVO, RegisterDTO, TenantResp } from './types';
import { post, get } from '@/utils/request';
export const login = (data: LoginDTO) => post<LoginVO>('/auth/login', data).json();
// 获取租户列表
export const tenantList = () => get<TenantResp>('/auth/tenant/list').json();
// 邮箱验证码
export const emailCode = (data: EmailCodeDTO) => post('/resource/email/code', data).json();

View File

@@ -16,6 +16,19 @@ export interface LoginVO {
userInfo?: LoginUser;
}
// 租户选项
export interface TenantOption {
companyName: string;
domain?: string;
tenantId: string;
}
// 租户列表响应
export interface TenantResp {
tenantEnabled: boolean;
voList: TenantOption[];
}
/**
* LoginUser登录用户身份权限
*/

View File

@@ -0,0 +1,92 @@
import request from '@/utils/request';
/**
* 测试 ERP 数据库连接
*/
export function testErpConnection() {
return request.get<{
status: string;
database: string;
version: string;
error?: string;
}>('/erp/test/connection').json();
}
/**
* ERP 健康检查
*/
export function erpHealth() {
return request.get<string>('/erp/test/health').json();
}
/**
* 客户档案接口类型
*/
export interface CustomerVO {
customerCode: string;
customerName: string;
companyCode: string;
companyName: string;
brand: string;
brandName: string;
contactName: string;
salesAreaCode: string;
salesAreaName: string;
salesPersonCode: string;
salesPersonName: string;
saleDocCode: string;
saleDocName: string;
pricePlanCode: string;
pricePlanName: string;
customerType: string;
address: string;
phone: string;
email: string;
sdOrgCode: string;
sdOrgName: string;
province: string;
city: string;
isStop: number;
}
/**
* 分页查询客户列表
* 使用动态API: /erp/dynamic/v1/customer/list
*/
export function getCustomerList(params: {
pageNum: number;
pageSize: number;
keyword?: string;
companyCode?: string;
salesAreaCode?: string;
brand?: string;
}) {
return request.get<{ rows: CustomerVO[]; total: number; code: number; msg: string }>(
'/erp/dynamic/v1/customer/list',
params
).json();
}
/**
* 获取客户详情
* 使用动态API: /erp/dynamic/v1/customer/detail
*/
export function getCustomerDetail(customerCode: string) {
return request.get<{ code: number; msg: string; data: CustomerVO }>(`/erp/dynamic/v1/customer/detail?customerCode=${customerCode}`).json();
}
/**
* 获取销区列表
* 使用动态API: /erp/dynamic/v1/customer/sales-areas
*/
export function getSalesAreas() {
return request.get<{ code: number; msg: string; data: CustomerVO[] }>('/erp/dynamic/v1/customer/sales-areas').json();
}
/**
* 获取品牌列表
* 使用动态API: /erp/dynamic/v1/customer/brands
*/
export function getBrands() {
return request.get<{ code: number; msg: string; data: CustomerVO[] }>('/erp/dynamic/v1/customer/brands').json();
}

View File

@@ -1,26 +1,36 @@
<!-- 账号密码登录表单 -->
<script lang="ts" setup>
import type { FormInstance, FormRules } from 'element-plus';
import type { LoginDTO } from '@/api/auth/types';
import { reactive, ref } from 'vue';
import type { LoginDTO, TenantResp } from '@/api/auth/types';
import { reactive, ref, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { login } from '@/api';
import { login, tenantList } from '@/api';
import { useUserStore } from '@/stores';
import { useLoginFormStore } from '@/stores/modules/loginForm';
import { useSessionStore } from '@/stores/modules/session';
import { useLoginTenantId } from '@/hooks/useLoginTenantId';
const userStore = useUserStore();
const sessionStore = useSessionStore();
const loginFromStore = useLoginFormStore();
// 使用全局租户ID状态
const { loginTenantId } = useLoginTenantId();
const formRef = ref<FormInstance>();
// 租户信息
const tenantInfo = ref<TenantResp>({
tenantEnabled: false,
voList: [],
});
const formModel = reactive<LoginDTO>({
username: '',
password: '',
clientId: import.meta.env.VITE_CLIENT_ID,
grantType: 'password',
tenantId: '000000',
tenantId: loginTenantId.value, // 使用全局状态
uuid: 'a5705def96be468f80e4b8bde3127c31',
});
@@ -29,12 +39,55 @@ const rules = reactive<FormRules<LoginDTO>>({
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
});
// 监听表单中的租户ID变化同步到全局状态添加防抖避免循环触发
watch(() => formModel.tenantId, (newTenantId, oldTenantId) => {
// 仅在值真正改变时更新全局状态
if (newTenantId !== oldTenantId && newTenantId !== loginTenantId.value) {
console.log('租户ID变化:', oldTenantId, '->', newTenantId);
loginTenantId.value = newTenantId || '000000';
console.log('全局状态已同步:', loginTenantId.value);
}
});
// 加载租户列表
async function loadTenant() {
try {
console.log('开始加载租户列表当前全局租户ID:', loginTenantId.value);
const resp = await tenantList();
tenantInfo.value = resp;
console.log('租户列表加载完成,启用多租户:', resp.tenantEnabled, '租户数量:', resp.voList.length);
// 仅在全局租户ID为默认值时才设置第一个租户
if (resp.tenantEnabled && resp.voList.length > 0 && loginTenantId.value === '000000') {
const firstTenantId = resp.voList[0].tenantId;
console.log('全局租户ID为默认值设置第一个租户:', firstTenantId);
loginTenantId.value = firstTenantId;
formModel.tenantId = firstTenantId;
} else {
// 如果全局状态已有值,同步到表单
console.log('使用全局租户ID:', loginTenantId.value);
formModel.tenantId = loginTenantId.value;
}
console.log('最终表单租户ID:', formModel.tenantId);
} catch (error) {
console.error('加载租户列表失败:', error);
}
}
onMounted(() => {
loadTenant();
});
const router = useRouter();
async function handleSubmit() {
try {
console.log('=== 开始登录流程 ===');
console.log('表单租户ID:', formModel.tenantId);
console.log('全局租户ID:', loginTenantId.value);
await formRef.value?.validate();
const res = await login(formModel);
console.log(res.data.access_token, 'res');
console.log('登录响应:', res.data.access_token, 'res');
res.data.access_token && userStore.setToken(res.data.access_token);
// res.data.userInfo && userStore.setUserInfo(res.data.userInfo);
ElMessage.success('登录成功');
@@ -44,7 +97,7 @@ async function handleSubmit() {
router.replace('/');
}
catch (error) {
console.error('请求错误:', error);
console.error('登录请求错误:', error);
}
}
</script>
@@ -58,6 +111,18 @@ async function handleSubmit() {
style="width: 230px"
@submit.prevent="handleSubmit"
>
<!-- 租户选择仅当启用多租户时显示 -->
<el-form-item v-if="tenantInfo.tenantEnabled" prop="tenantId">
<el-select v-model="formModel.tenantId" placeholder="请选择租户" style="width: 100%">
<el-option
v-for="tenant in tenantInfo.voList"
:key="tenant.tenantId"
:label="tenant.companyName"
:value="tenant.tenantId"
/>
</el-select>
</el-form-item>
<el-form-item prop="username">
<el-input v-model="formModel.username" placeholder="请输入用户名">
<template #prefix>

View File

@@ -0,0 +1,15 @@
import { ref } from 'vue';
import { createGlobalState } from '@vueuse/core';
/**
* 全局租户ID状态
* 使用 createGlobalState 确保组件重新挂载时状态不会丢失
* @see https://vueuse.org/shared/createGlobalState/
*/
export const useLoginTenantId = createGlobalState(() => {
const loginTenantId = ref('000000');
return {
loginTenantId,
};
});