feat: 新增门店列表页面并修复全局类型错误
1. 新增门店列表页(筛选/统计/表格/抽屉编辑),使用 mockjs 提供接口数据 2. 新增门店相关枚举、API 定义、路由配置 3. 修复 auth.ts loginApi 参数类型不匹配 4. 修复 merchant-center stores 属性路径错误及 merchant prop 类型不兼容 5. 修复 merchant-setting showSubmitButton 不存在于 VbenFormProps
This commit is contained in:
@@ -43,8 +43,12 @@
|
||||
"@vueuse/core": "catalog:",
|
||||
"ant-design-vue": "catalog:",
|
||||
"dayjs": "catalog:",
|
||||
"mockjs": "catalog:",
|
||||
"pinia": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"vue-router": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/mockjs": "catalog:"
|
||||
}
|
||||
}
|
||||
|
||||
105
apps/web-antd/src/api/store/index.ts
Normal file
105
apps/web-antd/src/api/store/index.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import type {
|
||||
ServiceType,
|
||||
StoreAuditStatus,
|
||||
StoreBusinessStatus,
|
||||
} from '#/enums/storeEnum';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export * from '#/enums/storeEnum';
|
||||
|
||||
/** 门店列表项 */
|
||||
export interface StoreListItemDto {
|
||||
/** 门店ID */
|
||||
id: string;
|
||||
/** 门店名称 */
|
||||
name: string;
|
||||
/** 门店编码 */
|
||||
code: string;
|
||||
/** 联系电话 */
|
||||
contactPhone: string;
|
||||
/** 店长/负责人 */
|
||||
managerName: string;
|
||||
/** 门店地址 */
|
||||
address: string;
|
||||
/** 门店封面图 */
|
||||
coverImage?: string;
|
||||
/** 营业状态 */
|
||||
businessStatus: StoreBusinessStatus;
|
||||
/** 审核状态 */
|
||||
auditStatus: StoreAuditStatus;
|
||||
/** 服务方式 */
|
||||
serviceTypes: ServiceType[];
|
||||
/** 创建时间 */
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** 门店统计 */
|
||||
export interface StoreStatsDto {
|
||||
/** 门店总数 */
|
||||
total: number;
|
||||
/** 营业中 */
|
||||
operating: number;
|
||||
/** 休息中 */
|
||||
resting: number;
|
||||
/** 待审核 */
|
||||
pendingAudit: number;
|
||||
}
|
||||
|
||||
/** 门店列表查询参数 */
|
||||
export interface StoreListQuery {
|
||||
keyword?: string;
|
||||
businessStatus?: StoreBusinessStatus;
|
||||
auditStatus?: StoreAuditStatus;
|
||||
serviceType?: ServiceType;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/** 分页结果 */
|
||||
export interface PaginatedResult<T> {
|
||||
items: T[];
|
||||
total: number;
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/** 创建/编辑门店参数 */
|
||||
export interface SaveStoreDto {
|
||||
id?: string;
|
||||
name: string;
|
||||
code: string;
|
||||
contactPhone: string;
|
||||
managerName: string;
|
||||
address: string;
|
||||
coverImage?: string;
|
||||
businessStatus?: StoreBusinessStatus;
|
||||
serviceTypes?: ServiceType[];
|
||||
}
|
||||
|
||||
/** 获取门店列表 */
|
||||
export async function getStoreListApi(params: StoreListQuery) {
|
||||
return requestClient.get<PaginatedResult<StoreListItemDto>>('/store/list', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
/** 获取门店统计 */
|
||||
export async function getStoreStatsApi() {
|
||||
return requestClient.get<StoreStatsDto>('/store/stats');
|
||||
}
|
||||
|
||||
/** 创建门店 */
|
||||
export async function createStoreApi(data: SaveStoreDto) {
|
||||
return requestClient.post('/store/create', data);
|
||||
}
|
||||
|
||||
/** 更新门店 */
|
||||
export async function updateStoreApi(data: SaveStoreDto) {
|
||||
return requestClient.post('/store/update', data);
|
||||
}
|
||||
|
||||
/** 删除门店 */
|
||||
export async function deleteStoreApi(id: string) {
|
||||
return requestClient.post('/store/delete', { id });
|
||||
}
|
||||
29
apps/web-antd/src/enums/storeEnum.ts
Normal file
29
apps/web-antd/src/enums/storeEnum.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/** 门店营业状态 */
|
||||
export enum StoreBusinessStatus {
|
||||
/** 强制关闭 */
|
||||
ForceClosed = 3,
|
||||
/** 营业中 */
|
||||
Operating = 1,
|
||||
/** 休息中 */
|
||||
Resting = 2,
|
||||
}
|
||||
|
||||
/** 门店审核状态 */
|
||||
export enum StoreAuditStatus {
|
||||
/** 已通过 */
|
||||
Approved = 1,
|
||||
/** 待审核 */
|
||||
Pending = 0,
|
||||
/** 已拒绝 */
|
||||
Rejected = 2,
|
||||
}
|
||||
|
||||
/** 服务方式 */
|
||||
export enum ServiceType {
|
||||
/** 外卖配送 */
|
||||
Delivery = 1,
|
||||
/** 堂食 */
|
||||
DineIn = 3,
|
||||
/** 到店自提 */
|
||||
Pickup = 2,
|
||||
}
|
||||
@@ -3,6 +3,11 @@ import { unmountGlobalLoading } from '@vben/utils';
|
||||
|
||||
import { overridesPreferences } from './preferences';
|
||||
|
||||
// 开发环境启用 Mock 数据
|
||||
if (import.meta.env.DEV) {
|
||||
import('./mock');
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用初始化完成之后再进行页面加载渲染
|
||||
*/
|
||||
|
||||
4
apps/web-antd/src/mock/index.ts
Normal file
4
apps/web-antd/src/mock/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// Mock 数据入口,仅在开发环境下使用
|
||||
import './store';
|
||||
|
||||
console.warn('[Mock] Mock 数据已启用');
|
||||
253
apps/web-antd/src/mock/store.ts
Normal file
253
apps/web-antd/src/mock/store.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
import Mock from 'mockjs';
|
||||
|
||||
const Random = Mock.Random;
|
||||
|
||||
/** mockjs 请求回调参数 */
|
||||
interface MockRequestOptions {
|
||||
url: string;
|
||||
type: string;
|
||||
body: null | string;
|
||||
}
|
||||
|
||||
/** 门店筛选参数 */
|
||||
interface StoreFilterParams {
|
||||
keyword?: string;
|
||||
businessStatus?: string;
|
||||
auditStatus?: string;
|
||||
serviceType?: string;
|
||||
page?: string;
|
||||
pageSize?: string;
|
||||
}
|
||||
|
||||
// 预定义门店数据,保证每次请求返回一致的数据
|
||||
const storePool = generateStores(23);
|
||||
|
||||
function generateStores(count: number) {
|
||||
const districts = [
|
||||
'朝阳区建国路88号',
|
||||
'海淀区中关村大街66号',
|
||||
'朝阳区望京西路50号',
|
||||
'通州区新华大街120号',
|
||||
'丰台区丰台路18号',
|
||||
'西城区西单北大街100号',
|
||||
'东城区王府井大街200号',
|
||||
'大兴区黄村镇兴华路30号',
|
||||
'昌平区回龙观东大街15号',
|
||||
'顺义区府前街8号',
|
||||
'石景山区石景山路22号',
|
||||
'房山区良乡拱辰大街55号',
|
||||
'密云区鼓楼东大街10号',
|
||||
'怀柔区青春路6号',
|
||||
'平谷区府前街12号',
|
||||
'门头沟区新桥大街3号',
|
||||
'延庆区妫水北街9号',
|
||||
'亦庄经济开发区荣华南路1号',
|
||||
'望京SOHO T1-2层',
|
||||
'三里屯太古里南区B1',
|
||||
'国贸商城3层',
|
||||
'五道口华联商厦1层',
|
||||
'中关村食宝街B1层',
|
||||
];
|
||||
|
||||
const managerNames = [
|
||||
'张伟',
|
||||
'李娜',
|
||||
'王磊',
|
||||
'赵敏',
|
||||
'刘洋',
|
||||
'陈静',
|
||||
'杨帆',
|
||||
'周杰',
|
||||
'吴芳',
|
||||
'孙涛',
|
||||
'马丽',
|
||||
'朱军',
|
||||
'胡明',
|
||||
'郭强',
|
||||
'何欢',
|
||||
'林峰',
|
||||
'徐婷',
|
||||
'高远',
|
||||
'罗斌',
|
||||
'梁宇',
|
||||
'宋佳',
|
||||
'唐亮',
|
||||
'韩雪',
|
||||
];
|
||||
|
||||
const storeNames = [
|
||||
'老三家外卖(朝阳店)',
|
||||
'老三家外卖(海淀店)',
|
||||
'老三家外卖(望京店)',
|
||||
'老三家外卖(通州店)',
|
||||
'老三家外卖(丰台店)',
|
||||
'老三家外卖(西单店)',
|
||||
'老三家外卖(王府井店)',
|
||||
'老三家外卖(大兴店)',
|
||||
'老三家外卖(回龙观店)',
|
||||
'老三家外卖(顺义店)',
|
||||
'老三家外卖(石景山店)',
|
||||
'老三家外卖(良乡店)',
|
||||
'老三家外卖(密云店)',
|
||||
'老三家外卖(怀柔店)',
|
||||
'老三家外卖(平谷店)',
|
||||
'老三家外卖(门头沟店)',
|
||||
'老三家外卖(延庆店)',
|
||||
'老三家外卖(亦庄店)',
|
||||
'老三家外卖(望京SOHO店)',
|
||||
'老三家外卖(三里屯店)',
|
||||
'老三家外卖(国贸店)',
|
||||
'老三家外卖(五道口店)',
|
||||
'老三家外卖(中关村店)',
|
||||
];
|
||||
|
||||
const avatarColors = [
|
||||
'#3b82f6',
|
||||
'#f59e0b',
|
||||
'#8b5cf6',
|
||||
'#ef4444',
|
||||
'#22c55e',
|
||||
'#06b6d4',
|
||||
'#ec4899',
|
||||
'#f97316',
|
||||
'#14b8a6',
|
||||
'#6366f1',
|
||||
];
|
||||
|
||||
const stores = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
// 1. 按索引分配营业状态,模拟真实分布
|
||||
let businessStatus = 1;
|
||||
if (i >= 21) {
|
||||
businessStatus = Random.pick([1, 2, 3]);
|
||||
} else if (i >= 18) {
|
||||
businessStatus = 3;
|
||||
} else if (i >= 14) {
|
||||
businessStatus = 2;
|
||||
}
|
||||
|
||||
// 2. 按索引分配审核状态
|
||||
let auditStatus = 2;
|
||||
if (i < 20) {
|
||||
auditStatus = 1;
|
||||
} else if (i < 22) {
|
||||
auditStatus = 0;
|
||||
}
|
||||
|
||||
// 3. 循环分配服务方式组合
|
||||
const serviceTypeCombos = [[1], [1, 2], [1, 2, 3], [1, 3], [2, 3]];
|
||||
|
||||
stores.push({
|
||||
id: Random.guid(),
|
||||
name: storeNames[i] || `老三家外卖(分店${i + 1})`,
|
||||
code: `ST2025${String(i + 1).padStart(4, '0')}`,
|
||||
contactPhone: `138****${String(8001 + i).slice(-4)}`,
|
||||
managerName: managerNames[i] || Random.cname(),
|
||||
address: `北京市${districts[i] || `朝阳区某路${i + 1}号`}`,
|
||||
coverImage: '',
|
||||
businessStatus,
|
||||
auditStatus,
|
||||
serviceTypes: serviceTypeCombos[i % serviceTypeCombos.length],
|
||||
createdAt: Random.datetime('yyyy-MM-dd'),
|
||||
_avatarColor: avatarColors[i % avatarColors.length],
|
||||
});
|
||||
}
|
||||
return stores;
|
||||
}
|
||||
|
||||
function filterStores(params: StoreFilterParams) {
|
||||
let list = [...storePool];
|
||||
|
||||
// 1. 关键词模糊匹配(名称/编码/电话)
|
||||
if (params.keyword) {
|
||||
const kw = params.keyword.toLowerCase();
|
||||
list = list.filter(
|
||||
(s) =>
|
||||
s.name.toLowerCase().includes(kw) ||
|
||||
s.code.toLowerCase().includes(kw) ||
|
||||
s.contactPhone.includes(kw),
|
||||
);
|
||||
}
|
||||
|
||||
// 2. 营业状态筛选
|
||||
if (params.businessStatus) {
|
||||
const status = Number(params.businessStatus);
|
||||
list = list.filter((s) => s.businessStatus === status);
|
||||
}
|
||||
|
||||
// 3. 审核状态筛选
|
||||
if (params.auditStatus !== undefined && params.auditStatus !== '') {
|
||||
const status = Number(params.auditStatus);
|
||||
list = list.filter((s) => s.auditStatus === status);
|
||||
}
|
||||
|
||||
// 4. 服务方式筛选
|
||||
if (params.serviceType) {
|
||||
const type = Number(params.serviceType);
|
||||
list = list.filter((s) => (s.serviceTypes ?? []).includes(type));
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 从 URL 中解析查询参数 */
|
||||
function parseUrlParams(url: string): StoreFilterParams {
|
||||
const parsed = new URL(url, 'http://localhost');
|
||||
const params: Record<string, string> = {};
|
||||
parsed.searchParams.forEach((value, key) => {
|
||||
params[key] = value;
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
// 门店列表
|
||||
Mock.mock(/\/store\/list/, 'get', (options: MockRequestOptions) => {
|
||||
const params = parseUrlParams(options.url);
|
||||
|
||||
const page = Number(params.page) || 1;
|
||||
const pageSize = Number(params.pageSize) || 10;
|
||||
const filtered = filterStores(params);
|
||||
const start = (page - 1) * pageSize;
|
||||
const items = filtered.slice(start, start + pageSize);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
items,
|
||||
total: filtered.length,
|
||||
page,
|
||||
pageSize,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// 门店统计
|
||||
Mock.mock(/\/store\/stats/, 'get', () => {
|
||||
return {
|
||||
code: 200,
|
||||
data: {
|
||||
total: storePool.length,
|
||||
operating: storePool.filter((s) => s.businessStatus === 1).length,
|
||||
resting: storePool.filter((s) => s.businessStatus === 2).length,
|
||||
pendingAudit: storePool.filter((s) => s.auditStatus === 0).length,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// 创建门店
|
||||
Mock.mock(/\/store\/create/, 'post', () => {
|
||||
return { code: 200, data: null };
|
||||
});
|
||||
|
||||
// 更新门店
|
||||
Mock.mock(/\/store\/update/, 'post', () => {
|
||||
return { code: 200, data: null };
|
||||
});
|
||||
|
||||
// 删除门店
|
||||
Mock.mock(/\/store\/delete/, 'post', () => {
|
||||
return { code: 200, data: null };
|
||||
});
|
||||
|
||||
// 设置 mock 响应延迟
|
||||
Mock.setup({ timeout: '200-400' });
|
||||
26
apps/web-antd/src/router/routes/modules/store.ts
Normal file
26
apps/web-antd/src/router/routes/modules/store.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
meta: {
|
||||
icon: 'lucide:store',
|
||||
order: 10,
|
||||
title: '门店管理',
|
||||
},
|
||||
name: 'Store',
|
||||
path: '/store',
|
||||
children: [
|
||||
{
|
||||
name: 'StoreList',
|
||||
path: '/store/list',
|
||||
component: () => import('#/views/store/list/index.vue'),
|
||||
meta: {
|
||||
icon: 'lucide:list',
|
||||
title: '门店列表',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Recordable, UserInfo } from '@vben/types';
|
||||
|
||||
import type { AuthApi } from '#/api';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
@@ -33,7 +35,9 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
let userInfo: null | UserInfo = null;
|
||||
try {
|
||||
loginLoading.value = true;
|
||||
const { accessToken, refreshToken } = await loginApi(params);
|
||||
const { accessToken, refreshToken } = await loginApi(
|
||||
params as AuthApi.LoginParams,
|
||||
);
|
||||
|
||||
// 如果成功获取到 accessToken
|
||||
if (accessToken) {
|
||||
|
||||
@@ -1,404 +1,475 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
CurrentMerchantCenterDto,
|
||||
MerchantAuditLogDto,
|
||||
MerchantChangeLogDto,
|
||||
MerchantContractDto,
|
||||
MerchantDocumentDto,
|
||||
MerchantStaffDto,
|
||||
MerchantStoreDto,
|
||||
} from '#/api/merchant';
|
||||
import type { CurrentMerchantCenterDto } from '#/api/merchant';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { Profile } from '@vben/common-ui';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import { Alert, Empty, List, message, Spin, Tag } from 'ant-design-vue';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Empty,
|
||||
Image,
|
||||
Modal,
|
||||
Spin,
|
||||
Table,
|
||||
TabPane,
|
||||
Tabs,
|
||||
Tag,
|
||||
Timeline,
|
||||
} from 'ant-design-vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { getMerchantInfoApi } from '#/api/merchant';
|
||||
import {
|
||||
ContractStatus,
|
||||
MerchantAuditAction,
|
||||
MerchantDocumentStatus,
|
||||
MerchantDocumentType,
|
||||
MerchantStatus,
|
||||
OperatingMode,
|
||||
StaffRoleType,
|
||||
StaffStatus,
|
||||
StoreStatus,
|
||||
} from '#/enums/merchantEnum';
|
||||
|
||||
import MerchantSetting from './merchant-setting.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
const tabsValue = ref('basic');
|
||||
const loading = ref(false);
|
||||
const merchantCenter = ref<CurrentMerchantCenterDto | null>(null);
|
||||
const activeTab = ref('documents');
|
||||
const isEditModalVisible = ref(false);
|
||||
|
||||
const tabs = [
|
||||
{ label: '基本信息', value: 'basic' },
|
||||
{ label: '资质合同', value: 'qualification' },
|
||||
{ label: '门店信息', value: 'stores' },
|
||||
{ label: '员工信息', value: 'staffs' },
|
||||
{ label: '日志记录', value: 'logs' },
|
||||
];
|
||||
const merchant = computed(() => merchantCenter.value?.merchant);
|
||||
const documents = computed(() => merchantCenter.value?.documents ?? []);
|
||||
const contracts = computed(() => merchantCenter.value?.contracts ?? []);
|
||||
const stores = computed(() => merchantCenter.value?.merchant?.stores ?? []);
|
||||
const staffs = computed(() => merchantCenter.value?.staffs ?? []);
|
||||
const auditLogs = computed(() => merchantCenter.value?.auditLogs ?? []);
|
||||
const changeLogs = computed(() => merchantCenter.value?.changeLogs ?? []);
|
||||
|
||||
const merchantStores = computed<MerchantStoreDto[]>(() => {
|
||||
return merchantCenter.value?.merchant.stores ?? [];
|
||||
});
|
||||
|
||||
const merchantDocuments = computed<MerchantDocumentDto[]>(() => {
|
||||
return merchantCenter.value?.documents ?? [];
|
||||
});
|
||||
|
||||
const merchantContracts = computed<MerchantContractDto[]>(() => {
|
||||
return merchantCenter.value?.contracts ?? [];
|
||||
});
|
||||
|
||||
const merchantStaffs = computed<MerchantStaffDto[]>(() => {
|
||||
return merchantCenter.value?.staffs ?? [];
|
||||
});
|
||||
|
||||
const merchantAuditLogs = computed<MerchantAuditLogDto[]>(() => {
|
||||
return merchantCenter.value?.auditLogs ?? [];
|
||||
});
|
||||
|
||||
const merchantChangeLogs = computed<MerchantChangeLogDto[]>(() => {
|
||||
return merchantCenter.value?.changeLogs ?? [];
|
||||
});
|
||||
|
||||
function formatDateTime(value?: null | string) {
|
||||
if (!value) {
|
||||
return '--';
|
||||
}
|
||||
|
||||
return new Date(value).toLocaleString('zh-CN', { hour12: false });
|
||||
}
|
||||
|
||||
function resolveStoreStatus(status: number) {
|
||||
if (status === 1) {
|
||||
return '营业中';
|
||||
}
|
||||
|
||||
if (status === 2) {
|
||||
return '停业中';
|
||||
}
|
||||
|
||||
return `状态${status}`;
|
||||
}
|
||||
|
||||
function resolveContractStatus(status: number) {
|
||||
if (status === 1) {
|
||||
return '生效中';
|
||||
}
|
||||
|
||||
if (status === 2) {
|
||||
return '已终止';
|
||||
}
|
||||
|
||||
if (status === 3) {
|
||||
return '已过期';
|
||||
}
|
||||
|
||||
return `状态${status}`;
|
||||
}
|
||||
|
||||
function resolveDocumentType(documentType: number) {
|
||||
if (documentType === 1) {
|
||||
return '营业执照';
|
||||
}
|
||||
|
||||
if (documentType === 2) {
|
||||
return '食品经营许可证';
|
||||
}
|
||||
|
||||
if (documentType === 3) {
|
||||
return '法人身份证';
|
||||
}
|
||||
|
||||
if (documentType === 4) {
|
||||
return '门头照';
|
||||
}
|
||||
|
||||
return `类型${documentType}`;
|
||||
}
|
||||
|
||||
function resolveDocumentStatus(status: number) {
|
||||
if (status === 1) {
|
||||
return '待审核';
|
||||
}
|
||||
|
||||
if (status === 2) {
|
||||
return '已通过';
|
||||
}
|
||||
|
||||
if (status === 3) {
|
||||
return '已驳回';
|
||||
}
|
||||
|
||||
return `状态${status}`;
|
||||
}
|
||||
|
||||
function resolveStaffRole(roleType: number) {
|
||||
if (roleType === 1) {
|
||||
return '店长';
|
||||
}
|
||||
|
||||
if (roleType === 2) {
|
||||
return '店员';
|
||||
}
|
||||
|
||||
if (roleType === 3) {
|
||||
return '财务';
|
||||
}
|
||||
|
||||
return `角色${roleType}`;
|
||||
}
|
||||
|
||||
function resolveStaffStatus(status: number) {
|
||||
if (status === 1) {
|
||||
return '在职';
|
||||
}
|
||||
|
||||
if (status === 2) {
|
||||
return '离职';
|
||||
}
|
||||
|
||||
return `状态${status}`;
|
||||
}
|
||||
|
||||
async function loadMerchantCenter() {
|
||||
async function loadData() {
|
||||
loading.value = true;
|
||||
|
||||
try {
|
||||
merchantCenter.value = await getMerchantInfoApi();
|
||||
} catch {
|
||||
message.error('商户中心信息加载失败');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadMerchantCenter();
|
||||
});
|
||||
function handleEditSuccess() {
|
||||
isEditModalVisible.value = false;
|
||||
loadData();
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr?: null | string) {
|
||||
if (!dateStr) return '--';
|
||||
return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
function formatDate(dateStr?: null | string) {
|
||||
if (!dateStr) return '--';
|
||||
return dayjs(dateStr).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
// Enum Resolvers
|
||||
function resolveOperatingMode(mode?: OperatingMode) {
|
||||
const map: Record<number, string> = {
|
||||
[OperatingMode.Direct]: '直营',
|
||||
[OperatingMode.Franchise]: '加盟',
|
||||
};
|
||||
return mode ? (map[mode] ?? mode) : '--';
|
||||
}
|
||||
|
||||
function resolveMerchantStatus(status?: MerchantStatus) {
|
||||
const map: Record<number, { color: string; text: string }> = {
|
||||
[MerchantStatus.Pending]: { color: 'orange', text: '待审核' },
|
||||
[MerchantStatus.Active]: { color: 'green', text: '营业中' },
|
||||
[MerchantStatus.Suspended]: { color: 'red', text: '已暂停' },
|
||||
[MerchantStatus.Closed]: { color: 'default', text: '已关闭' },
|
||||
};
|
||||
if (status === undefined) {
|
||||
return { color: 'default', text: '--' };
|
||||
}
|
||||
return map[status] ?? { color: 'default', text: `未知(${status})` };
|
||||
}
|
||||
|
||||
function resolveDocumentType(type: MerchantDocumentType) {
|
||||
const map: Record<number, string> = {
|
||||
[MerchantDocumentType.BusinessLicense]: '营业执照',
|
||||
[MerchantDocumentType.Permit]: '许可证',
|
||||
[MerchantDocumentType.Other]: '其他',
|
||||
};
|
||||
return map[type] ?? `类型${type}`;
|
||||
}
|
||||
|
||||
function resolveDocumentStatus(status: MerchantDocumentStatus) {
|
||||
const map: Record<
|
||||
number,
|
||||
{
|
||||
status: 'default' | 'error' | 'processing' | 'success' | 'warning';
|
||||
text: string;
|
||||
}
|
||||
> = {
|
||||
[MerchantDocumentStatus.Pending]: { status: 'processing', text: '审核中' },
|
||||
[MerchantDocumentStatus.Approved]: { status: 'success', text: '有效' },
|
||||
[MerchantDocumentStatus.Rejected]: { status: 'error', text: '已驳回' },
|
||||
[MerchantDocumentStatus.Expired]: { status: 'warning', text: '已过期' },
|
||||
};
|
||||
return map[status] ?? { status: 'default', text: `状态${status}` };
|
||||
}
|
||||
|
||||
function resolveContractStatus(status: ContractStatus) {
|
||||
const map: Record<number, { color: string; text: string }> = {
|
||||
[ContractStatus.Draft]: { color: 'default', text: '草稿' },
|
||||
[ContractStatus.Active]: { color: 'green', text: '生效中' },
|
||||
[ContractStatus.Expired]: { color: 'orange', text: '已过期' },
|
||||
[ContractStatus.Terminated]: { color: 'red', text: '已终止' },
|
||||
};
|
||||
return map[status] ?? { color: 'default', text: `状态${status}` };
|
||||
}
|
||||
|
||||
function resolveStoreStatus(status: StoreStatus) {
|
||||
const map: Record<number, { color: string; text: string }> = {
|
||||
[StoreStatus.Operating]: { color: 'green', text: '营业中' },
|
||||
[StoreStatus.Closed]: { color: 'red', text: '已关店' },
|
||||
[StoreStatus.Renovating]: { color: 'orange', text: '装修中' },
|
||||
};
|
||||
return map[status] ?? { color: 'default', text: `状态${status}` };
|
||||
}
|
||||
|
||||
function resolveStaffRole(role: StaffRoleType) {
|
||||
const map: Record<number, string> = {
|
||||
[StaffRoleType.StoreManager]: '店长',
|
||||
[StaffRoleType.Financial]: '财务',
|
||||
[StaffRoleType.Operator]: '操作员',
|
||||
};
|
||||
return map[role] ?? `角色${role}`;
|
||||
}
|
||||
|
||||
function resolveStaffStatus(status: StaffStatus) {
|
||||
const map: Record<number, { color: string; text: string }> = {
|
||||
[StaffStatus.Active]: { color: 'green', text: '在职' },
|
||||
[StaffStatus.Resigned]: { color: 'default', text: '离职' },
|
||||
};
|
||||
return map[status] ?? { color: 'default', text: `状态${status}` };
|
||||
}
|
||||
|
||||
function resolveAuditAction(action: MerchantAuditAction) {
|
||||
const map: Record<number, string> = {
|
||||
[MerchantAuditAction.Unknown]: '未知',
|
||||
[MerchantAuditAction.Create]: '创建',
|
||||
[MerchantAuditAction.Update]: '更新',
|
||||
[MerchantAuditAction.Delete]: '删除',
|
||||
[MerchantAuditAction.Audit]: '审核通过',
|
||||
[MerchantAuditAction.Reject]: '审核驳回',
|
||||
[MerchantAuditAction.Freeze]: '冻结',
|
||||
[MerchantAuditAction.Unfreeze]: '解冻',
|
||||
[MerchantAuditAction.Close]: '关闭',
|
||||
[MerchantAuditAction.Reopen]: '重开',
|
||||
};
|
||||
return map[action] ?? `动作${action}`;
|
||||
}
|
||||
|
||||
// Table Columns
|
||||
const storeColumns = [
|
||||
{ title: '门店名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '营业执照号', dataIndex: 'licenseNumber', key: 'licenseNumber' },
|
||||
{ title: '联系电话', dataIndex: 'contactPhone', key: 'contactPhone' },
|
||||
{ title: '地址', dataIndex: 'address', key: 'address' },
|
||||
{ title: '状态', key: 'status' },
|
||||
];
|
||||
|
||||
const staffColumns = [
|
||||
{ title: '姓名', dataIndex: 'name', key: 'name' },
|
||||
{ title: '角色', key: 'roleType' },
|
||||
{ title: '电话', dataIndex: 'phone', key: 'phone' },
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
||||
{ title: '状态', key: 'status' },
|
||||
];
|
||||
|
||||
const contractColumns = [
|
||||
{ title: '合同编号', dataIndex: 'contractNumber', key: 'contractNumber' },
|
||||
{ title: '状态', key: 'status' },
|
||||
{ title: '开始日期', key: 'startDate' },
|
||||
{ title: '结束日期', key: 'endDate' },
|
||||
{ title: '签署时间', key: 'signedAt' },
|
||||
{ title: '操作', key: 'action' },
|
||||
];
|
||||
|
||||
onMounted(loadData);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-5">
|
||||
<Profile
|
||||
v-model:model-value="tabsValue"
|
||||
:title="$t('page.merchant.center')"
|
||||
:user-info="{
|
||||
realName: userStore.userInfo?.realName || '',
|
||||
avatar: userStore.userInfo?.avatar || '',
|
||||
username: userStore.userInfo?.username || '',
|
||||
}"
|
||||
:tabs="tabs"
|
||||
>
|
||||
<template #content>
|
||||
<div class="rounded-lg bg-card p-4">
|
||||
<Spin :spinning="loading">
|
||||
<MerchantSetting v-if="tabsValue === 'basic'" />
|
||||
<Page title="商户中心">
|
||||
<template #extra>
|
||||
<Button type="primary" @click="isEditModalVisible = true">
|
||||
编辑基本信息
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<template v-else-if="tabsValue === 'qualification'">
|
||||
<div class="mb-3 text-base font-medium">资质证照</div>
|
||||
<Empty
|
||||
v-if="merchantDocuments.length === 0"
|
||||
description="暂无资质证照"
|
||||
/>
|
||||
<List
|
||||
v-else
|
||||
:data-source="merchantDocuments"
|
||||
item-layout="vertical"
|
||||
<div v-if="loading" class="p-10 text-center">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="!merchant" class="p-10 text-center">
|
||||
<Empty description="暂无商户数据" />
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<!-- 基本信息卡片 -->
|
||||
<Card :bordered="false" title="基本信息">
|
||||
<Descriptions
|
||||
:column="{ xxl: 3, xl: 3, lg: 2, md: 2, sm: 1, xs: 1 }"
|
||||
bordered
|
||||
>
|
||||
<Descriptions.Item label="商户名称">
|
||||
<span class="text-lg font-bold">{{ merchant.name }}</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag :color="resolveMerchantStatus(merchant.status).color">
|
||||
{{ resolveMerchantStatus(merchant.status).text }}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="经营模式">
|
||||
<Tag color="blue">
|
||||
{{ resolveOperatingMode(merchant.operatingMode) }}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="营业执照号">
|
||||
{{ merchant.licenseNumber || '--' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="法人/负责人">
|
||||
{{ merchant.legalRepresentative || '--' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="联系电话">
|
||||
{{ merchant.contactPhone || '--' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="联系邮箱">
|
||||
{{ merchant.contactEmail || '--' }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">
|
||||
{{ formatDateTime(merchant.createdAt) }}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="注册地址" :span="3">
|
||||
{{ merchant.registeredAddress || '--' }}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
<!-- 详细信息 Tabs -->
|
||||
<Card :bordered="false">
|
||||
<Tabs v-model:active-key="activeTab">
|
||||
<!-- 资质证照 -->
|
||||
<TabPane key="documents" tab="资质证照">
|
||||
<Empty v-if="documents.length === 0" description="暂无资质证照" />
|
||||
<div
|
||||
v-else
|
||||
class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"
|
||||
>
|
||||
<Card
|
||||
v-for="doc in documents"
|
||||
:key="doc.id"
|
||||
hoverable
|
||||
class="overflow-hidden"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<List.Item>
|
||||
<div class="mb-2 flex flex-wrap items-center gap-2">
|
||||
<Tag color="blue">
|
||||
{{ resolveDocumentType(item.documentType) }}
|
||||
</Tag>
|
||||
<Tag>{{ resolveDocumentStatus(item.status) }}</Tag>
|
||||
<span class="text-gray-500">
|
||||
编号:{{ item.documentNumber || '--' }}
|
||||
<template #cover>
|
||||
<div class="flex h-48 items-center justify-center bg-gray-50">
|
||||
<Image
|
||||
v-if="doc.fileUrl"
|
||||
:src="doc.fileUrl"
|
||||
alt="证照预览"
|
||||
class="max-h-full max-w-full object-contain"
|
||||
height="100%"
|
||||
/>
|
||||
<div v-else class="text-gray-400">无预览图</div>
|
||||
</div>
|
||||
</template>
|
||||
<Card.Meta :title="resolveDocumentType(doc.documentType)">
|
||||
<template #description>
|
||||
<div class="space-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span>状态:</span>
|
||||
<Badge
|
||||
:status="resolveDocumentStatus(doc.status).status"
|
||||
:text="resolveDocumentStatus(doc.status).text"
|
||||
/>
|
||||
</div>
|
||||
<div>证号: {{ doc.documentNumber || '--' }}</div>
|
||||
<div>有效期: {{ formatDate(doc.expiresAt) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card.Meta>
|
||||
</Card>
|
||||
</div>
|
||||
</TabPane>
|
||||
|
||||
<!-- 门店信息 -->
|
||||
<TabPane key="stores" tab="门店信息">
|
||||
<Table
|
||||
:columns="storeColumns"
|
||||
:data-source="stores"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<Tag :color="resolveStoreStatus(record.status).color">
|
||||
{{ resolveStoreStatus(record.status).text }}
|
||||
</Tag>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</TabPane>
|
||||
|
||||
<!-- 员工信息 -->
|
||||
<TabPane key="staffs" tab="员工信息">
|
||||
<Table
|
||||
:columns="staffColumns"
|
||||
:data-source="staffs"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'roleType'">
|
||||
<Tag>{{ resolveStaffRole(record.roleType) }}</Tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<Tag :color="resolveStaffStatus(record.status).color">
|
||||
{{ resolveStaffStatus(record.status).text }}
|
||||
</Tag>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</TabPane>
|
||||
|
||||
<!-- 合同信息 -->
|
||||
<TabPane key="contracts" tab="合同信息">
|
||||
<Table
|
||||
:columns="contractColumns"
|
||||
:data-source="contracts"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<Tag :color="resolveContractStatus(record.status).color">
|
||||
{{ resolveContractStatus(record.status).text }}
|
||||
</Tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'startDate'">
|
||||
{{ formatDate(record.startDate) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'endDate'">
|
||||
{{ formatDate(record.endDate) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'signedAt'">
|
||||
{{ formatDateTime(record.signedAt) }}
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<Button
|
||||
v-if="record.fileUrl"
|
||||
type="link"
|
||||
:href="record.fileUrl"
|
||||
target="_blank"
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</TabPane>
|
||||
|
||||
<!-- 操作日志 -->
|
||||
<TabPane key="logs" tab="操作日志">
|
||||
<div class="flex gap-8">
|
||||
<div class="flex-1">
|
||||
<Alert class="mb-4" message="审核日志" type="info" />
|
||||
<Empty
|
||||
v-if="auditLogs.length === 0"
|
||||
description="暂无审核日志"
|
||||
/>
|
||||
<Timeline v-else>
|
||||
<Timeline.Item
|
||||
v-for="log in auditLogs"
|
||||
:key="log.id"
|
||||
color="blue"
|
||||
>
|
||||
<div class="font-medium">
|
||||
<span class="mr-2">
|
||||
[{{ resolveAuditAction(log.action) }}]
|
||||
</span>
|
||||
{{ log.title }}
|
||||
<span class="ml-2 text-xs text-gray-400">
|
||||
{{ formatDateTime(log.createdAt) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
签发时间:{{ formatDateTime(item.issuedAt) }}
|
||||
<div class="text-sm text-gray-600">
|
||||
{{ log.description || '无描述' }}
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
到期时间:{{ formatDateTime(item.expiresAt) }}
|
||||
<div class="text-xs text-gray-500">
|
||||
操作人: {{ log.operatorName || '--' }} (IP:
|
||||
{{ log.ipAddress || '--' }})
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
文件地址:
|
||||
<a
|
||||
:href="item.fileUrl"
|
||||
class="text-blue-500"
|
||||
target="_blank"
|
||||
>
|
||||
{{ item.fileUrl }}
|
||||
</a>
|
||||
</Timeline.Item>
|
||||
</Timeline>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<Alert class="mb-4" message="变更日志" type="info" />
|
||||
<Empty
|
||||
v-if="changeLogs.length === 0"
|
||||
description="暂无变更日志"
|
||||
/>
|
||||
<Timeline v-else>
|
||||
<Timeline.Item
|
||||
v-for="log in changeLogs"
|
||||
:key="log.id"
|
||||
color="orange"
|
||||
>
|
||||
<div class="font-medium">
|
||||
变更字段: {{ log.fieldName }}
|
||||
<span class="ml-2 text-xs text-gray-400">
|
||||
{{ formatDateTime(log.changedAt) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
备注:{{ item.remarks || '--' }}
|
||||
<div class="text-sm">
|
||||
<span class="text-red-500 line-through">
|
||||
{{ log.oldValue || '(空)' }}
|
||||
</span>
|
||||
<span class="mx-2">-></span>
|
||||
<span class="text-green-500">
|
||||
{{ log.newValue || '(空)' }}
|
||||
</span>
|
||||
</div>
|
||||
</List.Item>
|
||||
</template>
|
||||
</List>
|
||||
<div class="text-xs text-gray-500">
|
||||
操作人: {{ log.changedByName || '--' }}
|
||||
</div>
|
||||
</Timeline.Item>
|
||||
</Timeline>
|
||||
</div>
|
||||
</div>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 mt-6 text-base font-medium">合同信息</div>
|
||||
<Empty
|
||||
v-if="merchantContracts.length === 0"
|
||||
description="暂无合同"
|
||||
/>
|
||||
<List
|
||||
v-else
|
||||
:data-source="merchantContracts"
|
||||
item-layout="vertical"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<List.Item>
|
||||
<div class="mb-2 flex flex-wrap items-center gap-2">
|
||||
<Tag color="purple">
|
||||
合同号:{{ item.contractNumber }}
|
||||
</Tag>
|
||||
<Tag>{{ resolveContractStatus(item.status) }}</Tag>
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
生效日期:{{ formatDateTime(item.startDate) }}
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
截止日期:{{ formatDateTime(item.endDate) }}
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
签署时间:{{ formatDateTime(item.signedAt) }}
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
终止时间:{{ formatDateTime(item.terminatedAt) }}
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
终止原因:{{ item.terminationReason || '--' }}
|
||||
</div>
|
||||
</List.Item>
|
||||
</template>
|
||||
</List>
|
||||
</template>
|
||||
|
||||
<template v-else-if="tabsValue === 'stores'">
|
||||
<Empty
|
||||
v-if="merchantStores.length === 0"
|
||||
description="暂无门店信息"
|
||||
/>
|
||||
<List v-else :data-source="merchantStores" item-layout="vertical">
|
||||
<template #renderItem="{ item }">
|
||||
<List.Item>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<Tag color="green">{{ item.name }}</Tag>
|
||||
<Tag>{{ resolveStoreStatus(item.status) }}</Tag>
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
门店地址:{{ item.address }}
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
联系电话:{{ item.contactPhone || '--' }}
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
营业执照号:{{ item.licenseNumber || '--' }}
|
||||
</div>
|
||||
</List.Item>
|
||||
</template>
|
||||
</List>
|
||||
</template>
|
||||
|
||||
<template v-else-if="tabsValue === 'staffs'">
|
||||
<Empty
|
||||
v-if="merchantStaffs.length === 0"
|
||||
description="暂无员工信息"
|
||||
/>
|
||||
<List v-else :data-source="merchantStaffs" item-layout="vertical">
|
||||
<template #renderItem="{ item }">
|
||||
<List.Item>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<Tag color="cyan">{{ item.name }}</Tag>
|
||||
<Tag>{{ resolveStaffRole(item.roleType) }}</Tag>
|
||||
<Tag>{{ resolveStaffStatus(item.status) }}</Tag>
|
||||
</div>
|
||||
<div class="text-gray-600">联系电话:{{ item.phone }}</div>
|
||||
<div class="text-gray-600">
|
||||
联系邮箱:{{ item.email || '--' }}
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
所属门店ID:{{ item.storeId || '--' }}
|
||||
</div>
|
||||
</List.Item>
|
||||
</template>
|
||||
</List>
|
||||
</template>
|
||||
|
||||
<template v-else-if="tabsValue === 'logs'">
|
||||
<Alert class="mb-4" message="审核日志" type="info" />
|
||||
<Empty
|
||||
v-if="merchantAuditLogs.length === 0"
|
||||
description="暂无审核日志"
|
||||
/>
|
||||
<List
|
||||
v-else
|
||||
:data-source="merchantAuditLogs"
|
||||
item-layout="vertical"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<List.Item>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<Tag color="orange">{{ item.title }}</Tag>
|
||||
<Tag>动作{{ item.action }}</Tag>
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
操作人:{{ item.operatorName || '--' }}
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
操作描述:{{ item.description || '--' }}
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
操作IP:{{ item.ipAddress || '--' }}
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
操作时间:{{ formatDateTime(item.createdAt) }}
|
||||
</div>
|
||||
</List.Item>
|
||||
</template>
|
||||
</List>
|
||||
|
||||
<Alert class="mb-4 mt-6" message="变更日志" type="info" />
|
||||
<Empty
|
||||
v-if="merchantChangeLogs.length === 0"
|
||||
description="暂无变更日志"
|
||||
/>
|
||||
<List
|
||||
v-else
|
||||
:data-source="merchantChangeLogs"
|
||||
item-layout="vertical"
|
||||
>
|
||||
<template #renderItem="{ item }">
|
||||
<List.Item>
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<Tag color="gold">{{ item.fieldName }}</Tag>
|
||||
<Tag>{{ item.changedByName || '--' }}</Tag>
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
旧值:{{ item.oldValue || '--' }}
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
新值:{{ item.newValue || '--' }}
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
变更原因:{{ item.changeReason || '--' }}
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
变更时间:{{ formatDateTime(item.changedAt) }}
|
||||
</div>
|
||||
</List.Item>
|
||||
</template>
|
||||
</List>
|
||||
</template>
|
||||
</Spin>
|
||||
</div>
|
||||
</template>
|
||||
</Profile>
|
||||
</div>
|
||||
<!-- Edit Modal -->
|
||||
<Modal
|
||||
v-model:open="isEditModalVisible"
|
||||
title="编辑商户信息"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
destroy-on-close
|
||||
>
|
||||
<MerchantSetting
|
||||
:merchant="merchant ?? null"
|
||||
@success="handleEditSuccess"
|
||||
/>
|
||||
</Modal>
|
||||
</Page>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.bg-card {
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,23 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { MerchantDetailDto, UpdateMerchantDto } from '#/api/merchant';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { ProfileBaseSetting } from '@vben/common-ui';
|
||||
import { watch } from 'vue';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { getMerchantInfoApi, updateMerchantInfoApi } from '#/api/merchant';
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import { updateMerchantInfoApi } from '#/api/merchant';
|
||||
import { OperatingMode } from '#/enums/merchantEnum';
|
||||
|
||||
const profileBaseSettingRef = ref();
|
||||
const props = defineProps<{
|
||||
merchant: MerchantDetailDto | null;
|
||||
}>();
|
||||
|
||||
const formSchema = computed((): VbenFormSchema[] => {
|
||||
return [
|
||||
const emit = defineEmits<{
|
||||
(e: 'success'): void;
|
||||
}>();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
// Use a grid layout with 2 columns
|
||||
wrapperClass: 'grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-2',
|
||||
commonConfig: {
|
||||
// Label width
|
||||
labelWidth: 100,
|
||||
},
|
||||
showDefaultActions: true,
|
||||
submitButtonOptions: {
|
||||
content: '保存更改',
|
||||
},
|
||||
resetButtonOptions: { show: false },
|
||||
// The submit button is usually at the end. We might want it full width or placed differently.
|
||||
// With grid, it might be in the last cell.
|
||||
actionWrapperClass: 'col-span-1 md:col-span-2 text-right',
|
||||
|
||||
schema: [
|
||||
{
|
||||
fieldName: 'name',
|
||||
component: 'Input',
|
||||
label: '商户名称',
|
||||
rules: 'required',
|
||||
formItemClass: 'col-span-1',
|
||||
},
|
||||
{
|
||||
fieldName: 'operatingMode',
|
||||
@@ -25,61 +47,87 @@ const formSchema = computed((): VbenFormSchema[] => {
|
||||
label: '经营模式',
|
||||
componentProps: {
|
||||
options: [
|
||||
{ label: '直营', value: 1 },
|
||||
{ label: '加盟', value: 2 },
|
||||
{ label: '直营', value: OperatingMode.Direct },
|
||||
{ label: '加盟', value: OperatingMode.Franchise },
|
||||
],
|
||||
},
|
||||
rules: 'required',
|
||||
formItemClass: 'col-span-1',
|
||||
},
|
||||
{
|
||||
fieldName: 'licenseNumber',
|
||||
component: 'Input',
|
||||
label: '营业执照号',
|
||||
formItemClass: 'col-span-1',
|
||||
},
|
||||
{
|
||||
fieldName: 'legalRepresentative',
|
||||
component: 'Input',
|
||||
label: '法人/负责人',
|
||||
formItemClass: 'col-span-1',
|
||||
},
|
||||
{
|
||||
fieldName: 'contactPhone',
|
||||
component: 'Input',
|
||||
label: '联系电话',
|
||||
formItemClass: 'col-span-1',
|
||||
},
|
||||
{
|
||||
fieldName: 'contactEmail',
|
||||
component: 'Input',
|
||||
label: '联系邮箱',
|
||||
formItemClass: 'col-span-1',
|
||||
},
|
||||
{
|
||||
fieldName: 'registeredAddress',
|
||||
component: 'Textarea',
|
||||
label: '注册地址',
|
||||
formItemClass: 'col-span-1 md:col-span-2',
|
||||
componentProps: {
|
||||
rows: 3,
|
||||
},
|
||||
},
|
||||
];
|
||||
],
|
||||
handleSubmit: onSubmit,
|
||||
});
|
||||
|
||||
async function handleSubmit(values: Record<string, unknown>) {
|
||||
async function onSubmit(values: Record<string, any>) {
|
||||
if (!props.merchant?.id) return;
|
||||
|
||||
try {
|
||||
await updateMerchantInfoApi(values);
|
||||
const payload: UpdateMerchantDto = {
|
||||
id: props.merchant.id,
|
||||
name: values.name,
|
||||
operatingMode: values.operatingMode,
|
||||
licenseNumber: values.licenseNumber,
|
||||
legalRepresentative: values.legalRepresentative,
|
||||
contactPhone: values.contactPhone,
|
||||
contactEmail: values.contactEmail,
|
||||
registeredAddress: values.registeredAddress,
|
||||
};
|
||||
|
||||
await updateMerchantInfoApi(payload);
|
||||
message.success('商户信息更新成功');
|
||||
} catch {
|
||||
emit('success');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message.error('更新失败');
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const data = await getMerchantInfoApi();
|
||||
// 假设接口返回的是 CurrentMerchantCenterDto,我们需要取其中的 merchant 对象
|
||||
if (data && data.merchant) {
|
||||
profileBaseSettingRef.value.getFormApi().setValues(data.merchant);
|
||||
}
|
||||
});
|
||||
watch(
|
||||
() => props.merchant,
|
||||
(merchant) => {
|
||||
if (merchant) {
|
||||
formApi.setValues(merchant);
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProfileBaseSetting
|
||||
ref="profileBaseSettingRef"
|
||||
:form-schema="formSchema"
|
||||
@submit="handleSubmit"
|
||||
/>
|
||||
<div class="p-4">
|
||||
<Form />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
557
apps/web-antd/src/views/store/list/index.vue
Normal file
557
apps/web-antd/src/views/store/list/index.vue
Normal file
@@ -0,0 +1,557 @@
|
||||
<script setup lang="ts">
|
||||
import type { StoreListItemDto, StoreStatsDto } from '#/api/store';
|
||||
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { Page } from '@vben/common-ui';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Drawer,
|
||||
Form,
|
||||
FormItem,
|
||||
Input,
|
||||
InputSearch,
|
||||
message,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Select,
|
||||
SelectOption,
|
||||
Statistic,
|
||||
Table,
|
||||
Tag,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
createStoreApi,
|
||||
deleteStoreApi,
|
||||
getStoreListApi,
|
||||
getStoreStatsApi,
|
||||
ServiceType,
|
||||
StoreAuditStatus,
|
||||
StoreBusinessStatus,
|
||||
updateStoreApi,
|
||||
} from '#/api/store';
|
||||
|
||||
/** 表格分页变更参数 */
|
||||
interface TablePagination {
|
||||
current?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
// 语义色值常量(后续可迁移至全局主题配置)
|
||||
const THEME_COLORS = {
|
||||
text: '#1f1f1f',
|
||||
success: '#22c55e',
|
||||
warning: '#f59e0b',
|
||||
danger: '#ef4444',
|
||||
} as const;
|
||||
|
||||
// 头像装饰色板
|
||||
const AVATAR_COLORS = [
|
||||
'#3b82f6',
|
||||
'#f59e0b',
|
||||
'#8b5cf6',
|
||||
'#ef4444',
|
||||
'#22c55e',
|
||||
'#06b6d4',
|
||||
'#ec4899',
|
||||
'#f97316',
|
||||
'#14b8a6',
|
||||
'#6366f1',
|
||||
] as const;
|
||||
|
||||
// ========== 1. 列表状态 ==========
|
||||
const isLoading = ref(false);
|
||||
const storeList = ref<StoreListItemDto[]>([]);
|
||||
const stats = ref<StoreStatsDto>({
|
||||
total: 0,
|
||||
operating: 0,
|
||||
resting: 0,
|
||||
pendingAudit: 0,
|
||||
});
|
||||
const pagination = reactive({ current: 1, pageSize: 10, total: 0 });
|
||||
const filters = reactive({
|
||||
keyword: '',
|
||||
businessStatus: undefined as StoreBusinessStatus | undefined,
|
||||
auditStatus: undefined as StoreAuditStatus | undefined,
|
||||
serviceType: undefined as ServiceType | undefined,
|
||||
});
|
||||
|
||||
// ========== 2. 抽屉状态 ==========
|
||||
const isDrawerVisible = ref(false);
|
||||
const drawerMode = ref<'create' | 'edit'>('create');
|
||||
const drawerTitle = computed(() =>
|
||||
drawerMode.value === 'edit' ? '编辑门店' : '添加门店',
|
||||
);
|
||||
const formState = reactive({
|
||||
id: '',
|
||||
name: '',
|
||||
code: '',
|
||||
contactPhone: '',
|
||||
managerName: '',
|
||||
address: '',
|
||||
coverImage: '',
|
||||
businessStatus: StoreBusinessStatus.Operating as StoreBusinessStatus,
|
||||
serviceTypes: [ServiceType.Delivery] as ServiceType[],
|
||||
});
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
// ========== 3. 枚举映射 ==========
|
||||
const businessStatusMap: Record<number, { color: string; text: string }> = {
|
||||
[StoreBusinessStatus.Operating]: { color: 'green', text: '营业中' },
|
||||
[StoreBusinessStatus.Resting]: { color: 'default', text: '休息中' },
|
||||
[StoreBusinessStatus.ForceClosed]: { color: 'red', text: '强制关闭' },
|
||||
};
|
||||
|
||||
const auditStatusMap: Record<number, { color: string; text: string }> = {
|
||||
[StoreAuditStatus.Pending]: { color: 'orange', text: '待审核' },
|
||||
[StoreAuditStatus.Approved]: { color: 'green', text: '已通过' },
|
||||
[StoreAuditStatus.Rejected]: { color: 'red', text: '已拒绝' },
|
||||
};
|
||||
|
||||
const serviceTypeMap: Record<number, { color: string; text: string }> = {
|
||||
[ServiceType.Delivery]: { color: 'blue', text: '外卖' },
|
||||
[ServiceType.Pickup]: { color: 'green', text: '自提' },
|
||||
[ServiceType.DineIn]: { color: 'orange', text: '堂食' },
|
||||
};
|
||||
|
||||
const businessStatusOptions = [
|
||||
{ label: '营业中', value: StoreBusinessStatus.Operating },
|
||||
{ label: '休息中', value: StoreBusinessStatus.Resting },
|
||||
{ label: '强制关闭', value: StoreBusinessStatus.ForceClosed },
|
||||
];
|
||||
|
||||
const auditStatusOptions = [
|
||||
{ label: '待审核', value: StoreAuditStatus.Pending },
|
||||
{ label: '已通过', value: StoreAuditStatus.Approved },
|
||||
{ label: '已拒绝', value: StoreAuditStatus.Rejected },
|
||||
];
|
||||
|
||||
const serviceTypeOptions = [
|
||||
{ label: '外卖配送', value: ServiceType.Delivery },
|
||||
{ label: '到店自提', value: ServiceType.Pickup },
|
||||
{ label: '堂食', value: ServiceType.DineIn },
|
||||
];
|
||||
|
||||
function getAvatarColor(index: number) {
|
||||
return AVATAR_COLORS[index % AVATAR_COLORS.length];
|
||||
}
|
||||
|
||||
// ========== 4. 表格列定义 ==========
|
||||
const columns = [
|
||||
{ title: '门店信息', key: 'storeInfo', width: 240 },
|
||||
{
|
||||
title: '联系电话',
|
||||
dataIndex: 'contactPhone',
|
||||
key: 'contactPhone',
|
||||
width: 120,
|
||||
},
|
||||
{ title: '店长', dataIndex: 'managerName', key: 'managerName', width: 80 },
|
||||
{ title: '地址', dataIndex: 'address', key: 'address', ellipsis: true },
|
||||
{ title: '服务方式', key: 'serviceTypes', width: 180 },
|
||||
{ title: '营业状态', key: 'businessStatus', width: 100 },
|
||||
{ title: '审核状态', key: 'auditStatus', width: 100 },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', width: 120 },
|
||||
{ title: '操作', key: 'action', width: 180, fixed: 'right' as const },
|
||||
];
|
||||
|
||||
// ========== 5. 数据加载 ==========
|
||||
async function loadList() {
|
||||
// 1. 开启加载态
|
||||
isLoading.value = true;
|
||||
try {
|
||||
// 2. 请求门店列表
|
||||
const res = await getStoreListApi({
|
||||
keyword: filters.keyword || undefined,
|
||||
businessStatus: filters.businessStatus,
|
||||
auditStatus: filters.auditStatus,
|
||||
serviceType: filters.serviceType,
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
});
|
||||
|
||||
// 3. 更新列表与分页
|
||||
storeList.value = res.items;
|
||||
pagination.total = res.total;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
stats.value = await getStoreStatsApi();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
// 1. 重置到第一页
|
||||
pagination.current = 1;
|
||||
// 2. 重新加载
|
||||
loadList();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
// 1. 清空筛选条件
|
||||
filters.keyword = '';
|
||||
filters.businessStatus = undefined;
|
||||
filters.auditStatus = undefined;
|
||||
filters.serviceType = undefined;
|
||||
|
||||
// 2. 重置分页并加载
|
||||
pagination.current = 1;
|
||||
loadList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: TablePagination) {
|
||||
// 1. 同步分页参数
|
||||
pagination.current = pag.current ?? 1;
|
||||
pagination.pageSize = pag.pageSize ?? 10;
|
||||
|
||||
// 2. 重新加载
|
||||
loadList();
|
||||
}
|
||||
|
||||
// ========== 6. 抽屉操作 ==========
|
||||
function openDrawer(mode: 'create' | 'edit', record?: unknown) {
|
||||
drawerMode.value = mode;
|
||||
|
||||
if (mode === 'edit' && record) {
|
||||
// 1. 编辑模式:回填表单数据
|
||||
const store = record as StoreListItemDto;
|
||||
Object.assign(formState, {
|
||||
id: store.id,
|
||||
name: store.name,
|
||||
code: store.code,
|
||||
contactPhone: store.contactPhone,
|
||||
managerName: store.managerName,
|
||||
address: store.address,
|
||||
coverImage: store.coverImage || '',
|
||||
businessStatus: store.businessStatus,
|
||||
serviceTypes: [...store.serviceTypes],
|
||||
});
|
||||
} else {
|
||||
// 2. 新增模式:重置表单
|
||||
Object.assign(formState, {
|
||||
id: '',
|
||||
name: '',
|
||||
code: '',
|
||||
contactPhone: '',
|
||||
managerName: '',
|
||||
address: '',
|
||||
coverImage: '',
|
||||
businessStatus: StoreBusinessStatus.Operating,
|
||||
serviceTypes: [ServiceType.Delivery],
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 打开抽屉
|
||||
isDrawerVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
// 1. 开启提交加载态
|
||||
isSubmitting.value = true;
|
||||
try {
|
||||
// 2. 根据模式调用对应接口
|
||||
const data = { ...formState };
|
||||
if (drawerMode.value === 'edit') {
|
||||
await updateStoreApi(data);
|
||||
message.success('更新成功');
|
||||
} else {
|
||||
await createStoreApi(data);
|
||||
message.success('创建成功');
|
||||
}
|
||||
|
||||
// 3. 关闭抽屉并刷新数据
|
||||
isDrawerVisible.value = false;
|
||||
loadList();
|
||||
loadStats();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 7. 删除操作 ==========
|
||||
async function handleDelete(record: unknown) {
|
||||
try {
|
||||
// 1. 调用删除接口
|
||||
const store = record as StoreListItemDto;
|
||||
await deleteStoreApi(store.id);
|
||||
message.success('删除成功');
|
||||
|
||||
// 2. 刷新列表与统计
|
||||
loadList();
|
||||
loadStats();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 8. 统计卡片配置 ==========
|
||||
const statCards = computed(() => [
|
||||
{ title: '门店总数', value: stats.value.total, color: THEME_COLORS.text },
|
||||
{
|
||||
title: '营业中',
|
||||
value: stats.value.operating,
|
||||
color: THEME_COLORS.success,
|
||||
},
|
||||
{ title: '休息中', value: stats.value.resting, color: THEME_COLORS.warning },
|
||||
{
|
||||
title: '待审核',
|
||||
value: stats.value.pendingAudit,
|
||||
color: THEME_COLORS.danger,
|
||||
},
|
||||
]);
|
||||
|
||||
onMounted(() => {
|
||||
loadList();
|
||||
loadStats();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Page title="门店列表" content-class="space-y-4">
|
||||
<template #extra>
|
||||
<div class="flex gap-2">
|
||||
<Button @click="() => message.info('导出功能开发中')">导出</Button>
|
||||
<Button type="primary" @click="openDrawer('create')">新增门店</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 筛选栏 -->
|
||||
<Card :bordered="false" size="small">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<InputSearch
|
||||
v-model:value="filters.keyword"
|
||||
placeholder="搜索门店名称 / 编号 / 电话"
|
||||
style="width: 260px"
|
||||
allow-clear
|
||||
@search="handleSearch"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="filters.businessStatus"
|
||||
placeholder="营业状态"
|
||||
style="width: 130px"
|
||||
allow-clear
|
||||
:options="businessStatusOptions"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="filters.auditStatus"
|
||||
placeholder="审核状态"
|
||||
style="width: 130px"
|
||||
allow-clear
|
||||
:options="auditStatusOptions"
|
||||
/>
|
||||
<Select
|
||||
v-model:value="filters.serviceType"
|
||||
placeholder="服务方式"
|
||||
style="width: 130px"
|
||||
allow-clear
|
||||
:options="serviceTypeOptions"
|
||||
/>
|
||||
<Button type="primary" @click="handleSearch">查询</Button>
|
||||
<Button @click="handleReset">重置</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<Row :gutter="16">
|
||||
<Col v-for="item in statCards" :key="item.title" :span="6">
|
||||
<Card :bordered="false" hoverable>
|
||||
<Statistic
|
||||
:title="item.title"
|
||||
:value="item.value"
|
||||
:value-style="{ color: item.color, fontWeight: 700 }"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<!-- 表格 -->
|
||||
<Card :bordered="false">
|
||||
<Table
|
||||
:columns="columns"
|
||||
:data-source="storeList"
|
||||
:loading="isLoading"
|
||||
:pagination="{
|
||||
current: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`,
|
||||
}"
|
||||
row-key="id"
|
||||
:scroll="{ x: 1200 }"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record, index }">
|
||||
<!-- 门店信息 -->
|
||||
<template v-if="column.key === 'storeInfo'">
|
||||
<div class="flex items-center gap-3">
|
||||
<div
|
||||
class="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg text-white"
|
||||
:style="{ background: getAvatarColor(index) }"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="m2 7 4.41-4.41A2 2 0 0 1 7.83 2h8.34a2 2 0 0 1 1.42.59L22 7"
|
||||
/>
|
||||
<path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8" />
|
||||
<path d="M15 22v-4a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2v4" />
|
||||
<path d="M2 7h20" />
|
||||
<path
|
||||
d="M22 7v3a2 2 0 0 1-2 2a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 16 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 12 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 8 12a2.7 2.7 0 0 1-1.59-.63.7.7 0 0 0-.82 0A2.7 2.7 0 0 1 4 12a2 2 0 0 1-2-2V7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-medium">{{ record.name }}</div>
|
||||
<div class="text-xs text-gray-400">{{ record.code }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 服务方式 -->
|
||||
<template v-if="column.key === 'serviceTypes'">
|
||||
<Tag
|
||||
v-for="st in record.serviceTypes"
|
||||
:key="st"
|
||||
:color="serviceTypeMap[st]?.color"
|
||||
>
|
||||
{{ serviceTypeMap[st]?.text }}
|
||||
</Tag>
|
||||
</template>
|
||||
|
||||
<!-- 营业状态 -->
|
||||
<template v-if="column.key === 'businessStatus'">
|
||||
<Tag :color="businessStatusMap[record.businessStatus]?.color">
|
||||
{{ businessStatusMap[record.businessStatus]?.text }}
|
||||
</Tag>
|
||||
</template>
|
||||
|
||||
<!-- 审核状态 -->
|
||||
<template v-if="column.key === 'auditStatus'">
|
||||
<Tag :color="auditStatusMap[record.auditStatus]?.color">
|
||||
{{ auditStatusMap[record.auditStatus]?.text }}
|
||||
</Tag>
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template v-if="column.key === 'action'">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
@click="openDrawer('edit', record)"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title="确定要删除该门店吗?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDelete(record)"
|
||||
>
|
||||
<Button type="link" size="small" danger>删除</Button>
|
||||
</Popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
<!-- 新增/编辑抽屉 -->
|
||||
<Drawer
|
||||
v-model:open="isDrawerVisible"
|
||||
:title="drawerTitle"
|
||||
:width="520"
|
||||
:body-style="{ paddingBottom: '80px' }"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<div
|
||||
class="mb-4 border-l-[3px] border-blue-500 pl-2.5 text-[15px] font-semibold"
|
||||
>
|
||||
基本信息
|
||||
</div>
|
||||
<FormItem label="门店名称" required>
|
||||
<Input v-model:value="formState.name" placeholder="请输入门店名称" />
|
||||
</FormItem>
|
||||
<FormItem label="门店编码" required>
|
||||
<Input v-model:value="formState.code" placeholder="如:ST20250006" />
|
||||
</FormItem>
|
||||
<FormItem label="联系电话" required>
|
||||
<Input
|
||||
v-model:value="formState.contactPhone"
|
||||
placeholder="请输入联系电话"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="负责人" required>
|
||||
<Input
|
||||
v-model:value="formState.managerName"
|
||||
placeholder="请输入负责人姓名"
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem label="门店地址" required>
|
||||
<Input
|
||||
v-model:value="formState.address"
|
||||
placeholder="请输入详细地址"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<div
|
||||
class="mb-4 mt-6 border-l-[3px] border-blue-500 pl-2.5 text-[15px] font-semibold"
|
||||
>
|
||||
营业设置
|
||||
</div>
|
||||
<FormItem label="营业状态">
|
||||
<Select v-model:value="formState.businessStatus">
|
||||
<SelectOption
|
||||
v-for="opt in businessStatusOptions"
|
||||
:key="opt.value"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</FormItem>
|
||||
<FormItem label="服务方式">
|
||||
<Select
|
||||
v-model:value="formState.serviceTypes"
|
||||
mode="multiple"
|
||||
placeholder="请选择服务方式"
|
||||
:options="serviceTypeOptions"
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex justify-end gap-2">
|
||||
<Button @click="isDrawerVisible = false">取消</Button>
|
||||
<Button type="primary" :loading="isSubmitting" @click="handleSubmit">
|
||||
确认
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Drawer>
|
||||
</Page>
|
||||
</template>
|
||||
688
pnpm-lock.yaml
generated
688
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,7 @@ catalog:
|
||||
'@types/html-minifier-terser': ^7.0.2
|
||||
'@types/json-bigint': ^1.0.4
|
||||
'@types/lodash.clonedeep': ^4.5.9
|
||||
'@types/mockjs': ^1.0.10
|
||||
'@types/node': ^24.10.9
|
||||
'@types/nprogress': ^0.2.3
|
||||
'@types/postcss-import': ^14.0.3
|
||||
@@ -125,6 +126,7 @@ catalog:
|
||||
lodash.clonedeep: ^4.5.0
|
||||
lucide-vue-next: ^0.553.0
|
||||
medium-zoom: ^1.1.0
|
||||
mockjs: ^1.1.0
|
||||
nitropack: ^2.13.1
|
||||
nprogress: ^0.2.0
|
||||
ora: ^8.2.0
|
||||
|
||||
Reference in New Issue
Block a user