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:
@@ -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();
|
||||
|
||||
|
||||
@@ -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,登录用户身份权限
|
||||
*/
|
||||
|
||||
92
hzhub-portal-dealer/src/api/erp/index.ts
Normal file
92
hzhub-portal-dealer/src/api/erp/index.ts
Normal 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();
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
15
hzhub-portal-dealer/src/hooks/useLoginTenantId.ts
Normal file
15
hzhub-portal-dealer/src/hooks/useLoginTenantId.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,登录用户身份权限
|
||||
*/
|
||||
|
||||
92
hzhub-portal-employee/src/api/erp/index.ts
Normal file
92
hzhub-portal-employee/src/api/erp/index.ts
Normal 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<{ code: number; msg: string; data: { 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();
|
||||
}
|
||||
15
hzhub-portal-employee/src/hooks/useLoginTenantId.ts
Normal file
15
hzhub-portal-employee/src/hooks/useLoginTenantId.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
@@ -1,125 +1,467 @@
|
||||
<!-- Dealer 经销商管理 -->
|
||||
<script setup lang="ts">
|
||||
// Placeholder for dealer management module
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { getCustomerList, getSalesAreas, getBrands, type CustomerVO } from '@/api/erp';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
// 统计
|
||||
const totalDealers = ref(0);
|
||||
|
||||
// 筛选
|
||||
const filters = ref({
|
||||
salesAreaCode: '',
|
||||
brand: '',
|
||||
isStop: '' as '' | '0' | '1',
|
||||
keyword: '',
|
||||
});
|
||||
|
||||
// 下拉选项
|
||||
const salesAreas = ref<CustomerVO[]>([]);
|
||||
const brands = ref<CustomerVO[]>([]);
|
||||
|
||||
// 分页
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(12);
|
||||
const total = ref(0);
|
||||
const dealerList = ref<CustomerVO[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
async function loadDealerList() {
|
||||
loading.value = true;
|
||||
console.log('=== 开始加载经销商列表 ===');
|
||||
console.log('当前页:', currentPage.value, '每页数量:', pageSize.value);
|
||||
console.log('筛选条件:', filters.value);
|
||||
|
||||
try {
|
||||
const res = await getCustomerList({
|
||||
pageNum: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: filters.value.keyword || undefined,
|
||||
salesAreaCode: filters.value.salesAreaCode || undefined,
|
||||
brand: filters.value.brand || undefined,
|
||||
});
|
||||
console.log('API响应:', res);
|
||||
|
||||
// 处理嵌套的响应结构
|
||||
const data = res.data || res; // 兼容两种响应格式
|
||||
console.log('数据对象:', data);
|
||||
console.log('数据行数:', data.rows?.length, '总数:', data.total);
|
||||
|
||||
let rows = data.rows || [];
|
||||
// 状态筛选(后端不支持,前端过滤)
|
||||
if (filters.value.isStop !== '') {
|
||||
const stopFlag = Number(filters.value.isStop);
|
||||
rows = rows.filter((d: CustomerVO) => d.isStop === stopFlag);
|
||||
}
|
||||
dealerList.value = rows;
|
||||
total.value = data.total || 0;
|
||||
totalDealers.value = data.total || 0;
|
||||
console.log('最终显示经销商数量:', dealerList.value.length);
|
||||
} catch (error: any) {
|
||||
console.error('加载经销商列表失败:', error);
|
||||
ElMessage.error(error?.message || '加载经销商列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSalesAreas() {
|
||||
console.log('=== 开始加载销区列表 ===');
|
||||
try {
|
||||
const res = await getSalesAreas();
|
||||
console.log('销区API响应:', res);
|
||||
salesAreas.value = res?.data || [];
|
||||
console.log('销区数量:', salesAreas.value.length);
|
||||
} catch (error) {
|
||||
console.error('加载销区失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadBrands() {
|
||||
try {
|
||||
const res = await getBrands();
|
||||
brands.value = res?.data || [];
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
currentPage.value = 1;
|
||||
loadDealerList();
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
currentPage.value = page;
|
||||
loadDealerList();
|
||||
}
|
||||
|
||||
// 状态标签
|
||||
function getStatusInfo(item: CustomerVO) {
|
||||
if (item.isStop === 1) {
|
||||
return { type: 'danger' as const, text: '已停用' };
|
||||
}
|
||||
return { type: 'success' as const, text: '合作中' };
|
||||
}
|
||||
|
||||
// 手机号脱敏
|
||||
function maskPhone(phone: string) {
|
||||
if (!phone || phone.length < 7) return phone || '--';
|
||||
return phone.slice(0, 3) + '****' + phone.slice(-4);
|
||||
}
|
||||
|
||||
// Logo 渐变色
|
||||
const logoGradients = [
|
||||
'linear-gradient(135deg, #1d5af3, #3378fc)',
|
||||
'linear-gradient(135deg, #8b5cf6, #a78bfa)',
|
||||
'linear-gradient(135deg, #16a34a, #22c55e)',
|
||||
'linear-gradient(135deg, #d97706, #f59e0b)',
|
||||
'linear-gradient(135deg, #dc2626, #ef4444)',
|
||||
'linear-gradient(135deg, #0ea5e9, #38bdf8)',
|
||||
'linear-gradient(135deg, #e11d48, #f43f5e)',
|
||||
'linear-gradient(135deg, #64748b, #94a3b8)',
|
||||
];
|
||||
|
||||
function getLogoBg(item: CustomerVO) {
|
||||
const idx = item.customerCode.charCodeAt(item.customerCode.length - 1) % logoGradients.length;
|
||||
return logoGradients[idx];
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('=== 经销商页面已挂载 ===');
|
||||
loadDealerList();
|
||||
loadSalesAreas();
|
||||
loadBrands();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dealer-container">
|
||||
<div class="module-header">
|
||||
<h1 class="module-title">经销商管理</h1>
|
||||
<p class="module-subtitle">经销商信息与合作关系管理</p>
|
||||
<div class="dealer-page">
|
||||
<!-- Header -->
|
||||
<div class="page-header">
|
||||
<div class="header-stats">
|
||||
<div class="h-stat">
|
||||
<span class="h-stat-value">{{ totalDealers }}</span>
|
||||
<span class="h-stat-label">合作经销商</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="module-content">
|
||||
<div class="placeholder-card">
|
||||
<el-icon class="placeholder-icon" color="#22c55e" :size="64">
|
||||
<Shop />
|
||||
</el-icon>
|
||||
<h2 class="placeholder-title">功能开发中,敬请期待</h2>
|
||||
<p class="placeholder-text">
|
||||
经销商管理模块将提供经销商档案管理、合同管理、业绩跟踪、
|
||||
结算管理等功能,支持经销商分级、区域划分等管理策略。
|
||||
</p>
|
||||
<div class="placeholder-stats">
|
||||
<div class="stat-item">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>经销商档案</span>
|
||||
<!-- Filter -->
|
||||
<div class="filter-bar">
|
||||
<el-select
|
||||
v-model="filters.salesAreaCode"
|
||||
placeholder="选择销区"
|
||||
clearable
|
||||
style="width: 140px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option
|
||||
v-for="area in salesAreas"
|
||||
:key="area.salesAreaCode"
|
||||
:label="area.salesAreaName"
|
||||
:value="area.salesAreaCode"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="filters.brand"
|
||||
placeholder="选择品牌"
|
||||
clearable
|
||||
filterable
|
||||
style="width: 150px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in brands"
|
||||
:key="item.brand"
|
||||
:label="item.brandName"
|
||||
:value="item.brand"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="filters.isStop"
|
||||
placeholder="状态"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="合作中" value="0" />
|
||||
<el-option label="已停用" value="1" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="搜索经销商名称、编码..."
|
||||
clearable
|
||||
style="width: 240px"
|
||||
@clear="handleSearch"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<span class="total-info" v-if="total > 0">共 {{ total }} 家</span>
|
||||
</div>
|
||||
|
||||
<!-- Dealer Cards -->
|
||||
<div class="dealer-grid" v-loading="loading">
|
||||
<div
|
||||
v-for="dealer in dealerList"
|
||||
:key="dealer.customerCode"
|
||||
class="dealer-card"
|
||||
>
|
||||
<div class="card-header">
|
||||
<div class="dealer-logo" :style="{ background: getLogoBg(dealer) }">
|
||||
{{ dealer.customerName.charAt(0) }}
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>合同管理</span>
|
||||
<div class="dealer-identity">
|
||||
<span class="dealer-name">{{ dealer.customerName }}</span>
|
||||
<span class="dealer-code">{{ dealer.customerCode }}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
<span>业绩跟踪</span>
|
||||
<el-tag :type="getStatusInfo(dealer).type" size="small" effect="light" round>
|
||||
{{ getStatusInfo(dealer).text }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">销区</span>
|
||||
<span class="detail-value">{{ dealer.salesAreaName || '--' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">品牌</span>
|
||||
<span class="detail-value">{{ dealer.brandName || '--' }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">联系人</span>
|
||||
<span class="detail-value">{{ dealer.contactName || '--' }} · {{ maskPhone(dealer.phone) }}</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">地址</span>
|
||||
<span class="detail-value">{{ [dealer.province, dealer.city].filter(Boolean).join(' ') || '--' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="dealer-kpi">
|
||||
<div class="kpi-item">
|
||||
<span class="kpi-value">{{ dealer.sdOrgName || '--' }}</span>
|
||||
<span class="kpi-label">经销组织</span>
|
||||
</div>
|
||||
<div class="kpi-item">
|
||||
<span class="kpi-value">{{ dealer.pricePlanName || '--' }}</span>
|
||||
<span class="kpi-label">价格方案</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-if="!loading && dealerList.length === 0" class="empty-state">
|
||||
<el-empty description="暂无经销商数据" />
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="pagination-bar" v-if="total > 0">
|
||||
<span class="pagination-info">共 {{ total }} 家经销商</span>
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dealer-container {
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
background-color: var(--color-bg);
|
||||
.dealer-page {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.module-header {
|
||||
margin-bottom: 24px;
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: left;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.module-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 8px 0;
|
||||
}
|
||||
.header-stats {
|
||||
display: flex;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.module-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.h-stat-value {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.module-content {
|
||||
.placeholder-card {
|
||||
background-color: #ffffff;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 48px;
|
||||
border: 1px solid var(--color-border);
|
||||
text-align: center;
|
||||
.h-stat-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
.placeholder-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
margin: 0 0 16px 0;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
line-height: 1.6;
|
||||
margin: 0 0 32px 0;
|
||||
max-width: 600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.placeholder-stats {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
justify-content: center;
|
||||
margin-top: 32px;
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--color-bg-light);
|
||||
|
||||
.el-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 32px;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.total-info {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
.dealer-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.dealer-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dealer-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.dealer-card {
|
||||
background: #fff;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
transition: all 0.25s ease;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 18px 20px 14px;
|
||||
}
|
||||
|
||||
.dealer-logo {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 12px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dealer-identity {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dealer-name {
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
color: var(--color-text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dealer-code {
|
||||
font-family: 'SF Mono', Menlo, monospace;
|
||||
font-size: 11.5px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-primary);
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 60%;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
margin-top: 14px;
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid #f5f3f0;
|
||||
}
|
||||
|
||||
.dealer-kpi {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.kpi-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.pagination-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
|
||||
.pagination-info {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
<script lang="ts" setup>
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import type { LoginDTO } from '@/api/auth/types';
|
||||
import { computed, reactive, ref } from 'vue';
|
||||
import { computed, reactive, ref, watch, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { login } from '@/api';
|
||||
import { useUserStore } from '@/stores';
|
||||
import { useLoginTenantId } from '@/hooks/useLoginTenantId';
|
||||
|
||||
// 定义 emits
|
||||
const emit = defineEmits<{
|
||||
@@ -16,9 +17,12 @@ const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
// 使用全局租户ID状态
|
||||
const { loginTenantId } = useLoginTenantId();
|
||||
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
// 租户列表(从配置或API获取)
|
||||
// 租户列表(从配置或API获取)- 不包含系统租户000000
|
||||
const tenantList = ref([
|
||||
{ label: '集团总部', value: '000001' },
|
||||
{ label: '汇亚公司', value: '000002' },
|
||||
@@ -26,36 +30,112 @@ const tenantList = ref([
|
||||
{ label: '玛缇公司', value: '000004' },
|
||||
]);
|
||||
|
||||
// 从URL参数获取租户ID,如果没有则默认集团总部
|
||||
// 从URL参数获取租户ID
|
||||
const urlTenantId = computed(() => {
|
||||
const tenantParam = route.query.tenant as string;
|
||||
// 验证是否在租户列表中
|
||||
if (tenantParam && tenantList.value.some(t => t.value === tenantParam)) {
|
||||
return tenantParam;
|
||||
}
|
||||
return '000001'; // 默认集团总部
|
||||
return null;
|
||||
});
|
||||
|
||||
// 是否显示租户选择框(URL有参数则隐藏)
|
||||
const showTenantSelect = computed(() => {
|
||||
return !route.query.tenant;
|
||||
return !urlTenantId.value;
|
||||
});
|
||||
|
||||
// 根据租户ID获取租户名称
|
||||
const tenantName = computed(() => {
|
||||
const tenant = tenantList.value.find(t => t.value === urlTenantId.value);
|
||||
const currentTenantId = urlTenantId.value || formModel.tenantId;
|
||||
const tenant = tenantList.value.find(t => t.value === currentTenantId);
|
||||
return tenant?.label || '集团总部';
|
||||
});
|
||||
|
||||
const formModel = reactive<LoginDTO & { tenantId: string }>({
|
||||
username: '',
|
||||
password: '',
|
||||
tenantId: urlTenantId.value, // 使用URL参数或默认值
|
||||
tenantId: urlTenantId.value || loginTenantId.value || '000001', // URL参数优先,否则使用全局状态或默认值
|
||||
clientId: import.meta.env.VITE_CLIENT_ID,
|
||||
grantType: 'password',
|
||||
uuid: 'a5705def96be468f80e4b8bde3127c31',
|
||||
});
|
||||
|
||||
// 监听表单中的租户ID变化,同步到全局状态
|
||||
watch(() => formModel.tenantId, (newTenantId, oldTenantId) => {
|
||||
if (newTenantId !== oldTenantId && newTenantId !== loginTenantId.value) {
|
||||
// console.log('租户ID变化:', oldTenantId, '->', newTenantId);
|
||||
loginTenantId.value = newTenantId;
|
||||
// console.log('全局状态已同步:', loginTenantId.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 记住登录功能
|
||||
const rememberMe = ref(false);
|
||||
const REMEMBER_KEY = 'hzhub_employee_remember_me';
|
||||
|
||||
// 加载记住的登录信息
|
||||
function loadRemembered() {
|
||||
try {
|
||||
const saved = localStorage.getItem(REMEMBER_KEY);
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
if (data.tenantId && data.username) {
|
||||
formModel.tenantId = data.tenantId;
|
||||
formModel.username = data.username;
|
||||
formModel.password = data.password || '';
|
||||
rememberMe.value = true;
|
||||
// 同步到全局状态
|
||||
loginTenantId.value = data.tenantId;
|
||||
// console.log('已恢复记住的登录信息,租户ID:', data.tenantId);
|
||||
}
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
// 保存记住的登录信息
|
||||
function saveRemembered() {
|
||||
if (rememberMe.value) {
|
||||
localStorage.setItem(REMEMBER_KEY, JSON.stringify({
|
||||
tenantId: formModel.tenantId,
|
||||
username: formModel.username,
|
||||
password: formModel.password,
|
||||
}));
|
||||
} else {
|
||||
localStorage.removeItem(REMEMBER_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时,初始化租户ID和记住的登录信息
|
||||
onMounted(() => {
|
||||
// console.log('=== 登录页面初始化 ===');
|
||||
// console.log('URL租户参数:', urlTenantId.value);
|
||||
// console.log('全局租户ID:', loginTenantId.value);
|
||||
|
||||
// 先尝试加载记住的登录信息
|
||||
loadRemembered();
|
||||
|
||||
// URL有参数时,使用URL参数并更新全局状态(优先级最高)
|
||||
if (urlTenantId.value) {
|
||||
// console.log('使用URL参数租户ID:', urlTenantId.value);
|
||||
formModel.tenantId = urlTenantId.value;
|
||||
loginTenantId.value = urlTenantId.value;
|
||||
}
|
||||
// 否则,如果全局状态为默认值(000000)且没有记住的登录信息,设置为第一个租户
|
||||
else if (loginTenantId.value === '000000' && !rememberMe.value) {
|
||||
const firstTenantId = tenantList.value[0].value;
|
||||
// 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);
|
||||
});
|
||||
|
||||
const rules = reactive<FormRules>({
|
||||
tenantId: [{ required: true, message: '请选择公司', trigger: 'change' }],
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
@@ -67,16 +147,19 @@ const loading = ref(false);
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
loading.value = true;
|
||||
// console.log('=== 开始登录流程 ===');
|
||||
// console.log('表单租户ID:', formModel.tenantId);
|
||||
// console.log('全局租户ID:', loginTenantId.value);
|
||||
|
||||
// 表单验证
|
||||
await formRef.value?.validate();
|
||||
|
||||
// 确保使用正确的租户ID
|
||||
formModel.tenantId = urlTenantId.value;
|
||||
// ❌ 删除强制覆盖:formModel.tenantId = urlTenantId.value;
|
||||
// 使用用户选择的租户ID(已在表单中)
|
||||
|
||||
// 发起登录请求
|
||||
const res = await login(formModel);
|
||||
console.log('登录响应:', res);
|
||||
// console.log('登录响应:', res);
|
||||
|
||||
// 登录成功
|
||||
if (res.data?.access_token) {
|
||||
@@ -105,8 +188,14 @@ async function handleSubmit() {
|
||||
});
|
||||
}
|
||||
|
||||
// 保存记住登录状态
|
||||
saveRemembered();
|
||||
|
||||
ElMessage.success(`登录成功 - ${tenantName.value}`);
|
||||
|
||||
// 触发成功事件
|
||||
emit('success');
|
||||
|
||||
// 跳转到首页或 redirect 页面
|
||||
const redirect = router.currentRoute.value.query.redirect as string;
|
||||
const targetPath = redirect || '/dashboard';
|
||||
@@ -115,7 +204,7 @@ async function handleSubmit() {
|
||||
}
|
||||
catch (error: any) {
|
||||
// 错误已经在 request.ts 的拦截器中处理并显示了
|
||||
console.error('登录失败:', error);
|
||||
// console.error('登录失败:', error);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
@@ -180,6 +269,9 @@ async function handleSubmit() {
|
||||
登录
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-checkbox v-model="rememberMe">记住登录</el-checkbox>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 切换公司的链接(仅在有URL参数时显示) -->
|
||||
|
||||
Reference in New Issue
Block a user