Files
TakeoutSaaS.TenantUI/apps/web-antd/src/views/merchant/center/index.vue
MSuMshk 4be997df63 feat: 新增门店列表页面并修复全局类型错误
1. 新增门店列表页(筛选/统计/表格/抽屉编辑),使用 mockjs 提供接口数据
2. 新增门店相关枚举、API 定义、路由配置
3. 修复 auth.ts loginApi 参数类型不匹配
4. 修复 merchant-center stores 属性路径错误及 merchant prop 类型不兼容
5. 修复 merchant-setting showSubmitButton 不存在于 VbenFormProps
2026-02-15 16:35:22 +08:00

476 lines
16 KiB
Vue

<script setup lang="ts">
import type { CurrentMerchantCenterDto } from '#/api/merchant';
import { computed, onMounted, ref } from 'vue';
import { Page } from '@vben/common-ui';
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 loading = ref(false);
const merchantCenter = ref<CurrentMerchantCenterDto | null>(null);
const activeTab = ref('documents');
const isEditModalVisible = ref(false);
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 ?? []);
async function loadData() {
loading.value = true;
try {
merchantCenter.value = await getMerchantInfoApi();
} catch (error) {
console.error(error);
} finally {
loading.value = false;
}
}
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>
<Page title="商户中心">
<template #extra>
<Button type="primary" @click="isEditModalVisible = true">
编辑基本信息
</Button>
</template>
<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 #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-sm text-gray-600">
{{ log.description || '无描述' }}
</div>
<div class="text-xs text-gray-500">
操作人: {{ log.operatorName || '--' }} (IP:
{{ log.ipAddress || '--' }})
</div>
</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-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>
<div class="text-xs text-gray-500">
操作人: {{ log.changedByName || '--' }}
</div>
</Timeline.Item>
</Timeline>
</div>
</div>
</TabPane>
</Tabs>
</Card>
</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>