refactor: 按营业时间规则拆分门店列表页面
This commit is contained in:
@@ -0,0 +1,160 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 门店编辑抽屉:
|
||||||
|
* - 负责新增/编辑表单渲染
|
||||||
|
* - 字段更新通过回调上抛,不直接改父级对象
|
||||||
|
*/
|
||||||
|
import type { SelectOptionItem } from '../types';
|
||||||
|
|
||||||
|
import type { ServiceType, StoreBusinessStatus } from '#/api/store';
|
||||||
|
|
||||||
|
import { Button, Drawer, Form, FormItem, Input, Select } from 'ant-design-vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
address: string;
|
||||||
|
businessStatus: StoreBusinessStatus;
|
||||||
|
businessStatusOptions: SelectOptionItem<StoreBusinessStatus>[];
|
||||||
|
code: string;
|
||||||
|
contactPhone: string;
|
||||||
|
isSubmitting: boolean;
|
||||||
|
managerName: string;
|
||||||
|
name: string;
|
||||||
|
onSetAddress: (value: string) => void;
|
||||||
|
onSetBusinessStatus: (value: StoreBusinessStatus) => void;
|
||||||
|
onSetCode: (value: string) => void;
|
||||||
|
onSetContactPhone: (value: string) => void;
|
||||||
|
onSetManagerName: (value: string) => void;
|
||||||
|
onSetName: (value: string) => void;
|
||||||
|
onSetServiceTypes: (value: ServiceType[]) => void;
|
||||||
|
open: boolean;
|
||||||
|
serviceTypeOptions: SelectOptionItem<ServiceType>[];
|
||||||
|
serviceTypes: ServiceType[];
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'submit'): void;
|
||||||
|
(event: 'update:open', value: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/** 读取字符串输入值。 */
|
||||||
|
function readTextValue(value: string | undefined) {
|
||||||
|
return value ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析下拉枚举值。 */
|
||||||
|
function readEnumValue<T extends number>(value: unknown): T | undefined {
|
||||||
|
if (value === undefined || value === null || value === '') return undefined;
|
||||||
|
const numericValue = Number(value);
|
||||||
|
return Number.isFinite(numericValue) ? (numericValue as T) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析服务方式多选结果。 */
|
||||||
|
function readServiceTypeList(value: unknown): ServiceType[] {
|
||||||
|
if (!Array.isArray(value)) return [];
|
||||||
|
|
||||||
|
return value
|
||||||
|
.map(Number)
|
||||||
|
.filter((item) => Number.isFinite(item))
|
||||||
|
.map((item) => item as ServiceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同步营业状态。 */
|
||||||
|
function handleBusinessStatusChange(value: unknown) {
|
||||||
|
const nextStatus = readEnumValue<StoreBusinessStatus>(value);
|
||||||
|
if (nextStatus === undefined) return;
|
||||||
|
props.onSetBusinessStatus(nextStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同步服务方式。 */
|
||||||
|
function handleServiceTypesChange(value: unknown) {
|
||||||
|
props.onSetServiceTypes(readServiceTypeList(value));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Drawer
|
||||||
|
class="store-editor-drawer-wrap"
|
||||||
|
:open="props.open"
|
||||||
|
:title="props.title"
|
||||||
|
:width="520"
|
||||||
|
:mask-closable="true"
|
||||||
|
@update:open="(value) => emit('update:open', value)"
|
||||||
|
>
|
||||||
|
<Form layout="vertical" class="store-drawer-form">
|
||||||
|
<div class="store-drawer-section-title">基本信息</div>
|
||||||
|
<FormItem label="门店名称" required>
|
||||||
|
<Input
|
||||||
|
:value="props.name"
|
||||||
|
placeholder="请输入门店名称"
|
||||||
|
@update:value="(value) => props.onSetName(readTextValue(value))"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="门店编码" required>
|
||||||
|
<Input
|
||||||
|
:value="props.code"
|
||||||
|
placeholder="如:ST20250006"
|
||||||
|
@update:value="(value) => props.onSetCode(readTextValue(value))"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="联系电话" required>
|
||||||
|
<Input
|
||||||
|
:value="props.contactPhone"
|
||||||
|
placeholder="请输入联系电话"
|
||||||
|
@update:value="
|
||||||
|
(value) => props.onSetContactPhone(readTextValue(value))
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="负责人" required>
|
||||||
|
<Input
|
||||||
|
:value="props.managerName"
|
||||||
|
placeholder="请输入负责人姓名"
|
||||||
|
@update:value="
|
||||||
|
(value) => props.onSetManagerName(readTextValue(value))
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="门店地址" required>
|
||||||
|
<Input
|
||||||
|
:value="props.address"
|
||||||
|
placeholder="请输入详细地址"
|
||||||
|
@update:value="(value) => props.onSetAddress(readTextValue(value))"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<div class="store-drawer-section-title">营业设置</div>
|
||||||
|
<FormItem label="营业状态">
|
||||||
|
<Select
|
||||||
|
:value="props.businessStatus"
|
||||||
|
:options="props.businessStatusOptions"
|
||||||
|
@update:value="handleBusinessStatusChange"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem label="服务方式">
|
||||||
|
<Select
|
||||||
|
:value="props.serviceTypes"
|
||||||
|
mode="multiple"
|
||||||
|
placeholder="请选择服务方式"
|
||||||
|
:options="props.serviceTypeOptions"
|
||||||
|
@update:value="handleServiceTypesChange"
|
||||||
|
/>
|
||||||
|
</FormItem>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="store-drawer-footer">
|
||||||
|
<Button @click="emit('update:open', false)">取消</Button>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
:loading="props.isSubmitting"
|
||||||
|
@click="emit('submit')"
|
||||||
|
>
|
||||||
|
确认
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Drawer>
|
||||||
|
</template>
|
||||||
105
apps/web-antd/src/views/store/list/components/StoreFilterBar.vue
Normal file
105
apps/web-antd/src/views/store/list/components/StoreFilterBar.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 门店列表筛选栏:
|
||||||
|
* - 只负责筛选项渲染
|
||||||
|
* - 通过回调把值同步给父级 composable
|
||||||
|
*/
|
||||||
|
import type { SelectOptionItem } from '../types';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ServiceType,
|
||||||
|
StoreAuditStatus,
|
||||||
|
StoreBusinessStatus,
|
||||||
|
} from '#/api/store';
|
||||||
|
|
||||||
|
import { Button, Card, InputSearch, Select } from 'ant-design-vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
auditStatus?: StoreAuditStatus;
|
||||||
|
auditStatusOptions: SelectOptionItem<StoreAuditStatus>[];
|
||||||
|
businessStatus?: StoreBusinessStatus;
|
||||||
|
businessStatusOptions: SelectOptionItem<StoreBusinessStatus>[];
|
||||||
|
keyword: string;
|
||||||
|
onSetAuditStatus: (value?: StoreAuditStatus) => void;
|
||||||
|
onSetBusinessStatus: (value?: StoreBusinessStatus) => void;
|
||||||
|
onSetKeyword: (value: string) => void;
|
||||||
|
onSetServiceType: (value?: ServiceType) => void;
|
||||||
|
serviceType?: ServiceType;
|
||||||
|
serviceTypeOptions: SelectOptionItem<ServiceType>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'reset'): void;
|
||||||
|
(event: 'search'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/** 解析下拉枚举值(支持清空为 undefined)。 */
|
||||||
|
function readEnumValue<T extends number>(value: unknown): T | undefined {
|
||||||
|
if (value === undefined || value === null || value === '') return undefined;
|
||||||
|
const numericValue = Number(value);
|
||||||
|
return Number.isFinite(numericValue) ? (numericValue as T) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同步关键字。 */
|
||||||
|
function handleKeywordChange(value: string | undefined) {
|
||||||
|
props.onSetKeyword(value ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同步营业状态。 */
|
||||||
|
function handleBusinessStatusChange(value: unknown) {
|
||||||
|
props.onSetBusinessStatus(readEnumValue<StoreBusinessStatus>(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同步审核状态。 */
|
||||||
|
function handleAuditStatusChange(value: unknown) {
|
||||||
|
props.onSetAuditStatus(readEnumValue<StoreAuditStatus>(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 同步服务方式。 */
|
||||||
|
function handleServiceTypeChange(value: unknown) {
|
||||||
|
props.onSetServiceType(readEnumValue<ServiceType>(value));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false" size="small" class="store-filter-card">
|
||||||
|
<div class="store-filter-row">
|
||||||
|
<InputSearch
|
||||||
|
:value="props.keyword"
|
||||||
|
class="filter-search"
|
||||||
|
placeholder="搜索门店名称 / 编号 / 电话"
|
||||||
|
allow-clear
|
||||||
|
@update:value="handleKeywordChange"
|
||||||
|
@search="emit('search')"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
:value="props.businessStatus"
|
||||||
|
class="filter-select"
|
||||||
|
placeholder="营业状态"
|
||||||
|
allow-clear
|
||||||
|
:options="props.businessStatusOptions"
|
||||||
|
@update:value="handleBusinessStatusChange"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
:value="props.auditStatus"
|
||||||
|
class="filter-select"
|
||||||
|
placeholder="审核状态"
|
||||||
|
allow-clear
|
||||||
|
:options="props.auditStatusOptions"
|
||||||
|
@update:value="handleAuditStatusChange"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
:value="props.serviceType"
|
||||||
|
class="filter-select"
|
||||||
|
placeholder="服务方式"
|
||||||
|
allow-clear
|
||||||
|
:options="props.serviceTypeOptions"
|
||||||
|
@update:value="handleServiceTypeChange"
|
||||||
|
/>
|
||||||
|
<Button type="primary" @click="emit('search')">查询</Button>
|
||||||
|
<Button @click="emit('reset')">重置</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
150
apps/web-antd/src/views/store/list/components/StoreListTable.vue
Normal file
150
apps/web-antd/src/views/store/list/components/StoreListTable.vue
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 门店列表表格:
|
||||||
|
* - 负责行内容渲染
|
||||||
|
* - 通过事件抛出分页/编辑/删除动作
|
||||||
|
*/
|
||||||
|
import type {
|
||||||
|
StatusTagMeta,
|
||||||
|
StoreTablePagination,
|
||||||
|
TablePagination,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
import type { StoreListItemDto } from '#/api/store';
|
||||||
|
|
||||||
|
import { Button, Card, Popconfirm, Table, Tag } from 'ant-design-vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
auditStatusMap: Record<number, StatusTagMeta>;
|
||||||
|
businessStatusMap: Record<number, StatusTagMeta>;
|
||||||
|
columns: Array<Record<string, unknown>>;
|
||||||
|
getAvatarColor: (index: number) => string;
|
||||||
|
isLoading: boolean;
|
||||||
|
pagination: StoreTablePagination;
|
||||||
|
serviceTypeMap: Record<number, StatusTagMeta>;
|
||||||
|
storeList: StoreListItemDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'delete', record: StoreListItemDto): void;
|
||||||
|
(event: 'edit', record: StoreListItemDto): void;
|
||||||
|
(event: 'tableChange', pagination: TablePagination): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/** 透传表格分页变化。 */
|
||||||
|
function handleTableChange(pagination: TablePagination) {
|
||||||
|
emit('tableChange', pagination);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 类型守卫:把表格记录收敛为门店对象。 */
|
||||||
|
function isStoreRecord(record: unknown): record is StoreListItemDto {
|
||||||
|
if (!record || typeof record !== 'object') return false;
|
||||||
|
const candidate = record as Partial<StoreListItemDto>;
|
||||||
|
return typeof candidate.id === 'string' && typeof candidate.name === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 安全触发编辑事件。 */
|
||||||
|
function emitEdit(record: unknown) {
|
||||||
|
if (!isStoreRecord(record)) return;
|
||||||
|
emit('edit', record);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 安全触发删除事件。 */
|
||||||
|
function emitDelete(record: unknown) {
|
||||||
|
if (!isStoreRecord(record)) return;
|
||||||
|
emit('delete', record);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Card :bordered="false">
|
||||||
|
<Table
|
||||||
|
class="store-list-table"
|
||||||
|
:columns="props.columns"
|
||||||
|
:data-source="props.storeList"
|
||||||
|
:loading="props.isLoading"
|
||||||
|
:pagination="props.pagination"
|
||||||
|
row-key="id"
|
||||||
|
:scroll="{ x: 1200 }"
|
||||||
|
@change="handleTableChange"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ column, record, index }">
|
||||||
|
<template v-if="column.key === 'storeInfo'">
|
||||||
|
<div class="store-info-cell">
|
||||||
|
<div
|
||||||
|
class="store-avatar"
|
||||||
|
:style="{ background: props.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 class="store-info-text">
|
||||||
|
<div class="store-name">{{ record.name }}</div>
|
||||||
|
<div class="store-code">{{ record.code }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="column.key === 'serviceTypes'">
|
||||||
|
<Tag
|
||||||
|
v-for="serviceType in record.serviceTypes"
|
||||||
|
:key="serviceType"
|
||||||
|
class="service-tag"
|
||||||
|
:color="props.serviceTypeMap[serviceType]?.color"
|
||||||
|
>
|
||||||
|
{{ props.serviceTypeMap[serviceType]?.text }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="column.key === 'businessStatus'">
|
||||||
|
<Tag :color="props.businessStatusMap[record.businessStatus]?.color">
|
||||||
|
{{ props.businessStatusMap[record.businessStatus]?.text }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="column.key === 'auditStatus'">
|
||||||
|
<Tag :color="props.auditStatusMap[record.auditStatus]?.color">
|
||||||
|
{{ props.auditStatusMap[record.auditStatus]?.text }}
|
||||||
|
</Tag>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="column.key === 'action'">
|
||||||
|
<div class="store-action-row">
|
||||||
|
<Button type="link" size="small" @click="emitEdit(record)">
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Popconfirm
|
||||||
|
title="确定要删除该门店吗?"
|
||||||
|
ok-text="确定"
|
||||||
|
cancel-text="取消"
|
||||||
|
@confirm="emitDelete(record)"
|
||||||
|
>
|
||||||
|
<Button type="link" size="small" danger>删除</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
/**
|
||||||
|
* 门店统计卡片区域:
|
||||||
|
* - 负责展示统计结果
|
||||||
|
* - 不包含业务状态变更逻辑
|
||||||
|
*/
|
||||||
|
import type { StatCardItem } from '../types';
|
||||||
|
|
||||||
|
import { Card, Col, Row, Statistic } from 'ant-design-vue';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
items: StatCardItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Row :gutter="16" class="store-stats-row">
|
||||||
|
<Col v-for="item in props.items" :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>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* 门店列表页常量集合:
|
||||||
|
* - 色值与文案映射
|
||||||
|
* - 选项数据
|
||||||
|
* - 默认状态
|
||||||
|
* - 表格列定义
|
||||||
|
*/
|
||||||
|
import type {
|
||||||
|
PagePaginationState,
|
||||||
|
SelectOptionItem,
|
||||||
|
StatusTagMeta,
|
||||||
|
StoreFilterState,
|
||||||
|
StoreFormState,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ServiceType,
|
||||||
|
StoreAuditStatus,
|
||||||
|
StoreBusinessStatus,
|
||||||
|
StoreStatsDto,
|
||||||
|
} from '#/api/store';
|
||||||
|
|
||||||
|
import {
|
||||||
|
ServiceType as ServiceTypeEnum,
|
||||||
|
StoreAuditStatus as StoreAuditStatusEnum,
|
||||||
|
StoreBusinessStatus as StoreBusinessStatusEnum,
|
||||||
|
} from '#/api/store';
|
||||||
|
|
||||||
|
export const THEME_COLORS = {
|
||||||
|
danger: '#ef4444',
|
||||||
|
success: '#22c55e',
|
||||||
|
text: '#1f1f1f',
|
||||||
|
warning: '#f59e0b',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const AVATAR_COLORS = [
|
||||||
|
'#3b82f6',
|
||||||
|
'#f59e0b',
|
||||||
|
'#8b5cf6',
|
||||||
|
'#ef4444',
|
||||||
|
'#22c55e',
|
||||||
|
'#06b6d4',
|
||||||
|
'#ec4899',
|
||||||
|
'#f97316',
|
||||||
|
'#14b8a6',
|
||||||
|
'#6366f1',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export const DEFAULT_STATS: StoreStatsDto = {
|
||||||
|
operating: 0,
|
||||||
|
pendingAudit: 0,
|
||||||
|
resting: 0,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_PAGINATION: PagePaginationState = {
|
||||||
|
current: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_FILTERS: StoreFilterState = {
|
||||||
|
auditStatus: undefined,
|
||||||
|
businessStatus: undefined,
|
||||||
|
keyword: '',
|
||||||
|
serviceType: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_FORM_STATE: StoreFormState = {
|
||||||
|
address: '',
|
||||||
|
businessStatus: StoreBusinessStatusEnum.Operating,
|
||||||
|
code: '',
|
||||||
|
contactPhone: '',
|
||||||
|
coverImage: '',
|
||||||
|
id: '',
|
||||||
|
managerName: '',
|
||||||
|
name: '',
|
||||||
|
serviceTypes: [ServiceTypeEnum.Delivery],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const businessStatusMap: Record<number, StatusTagMeta> = {
|
||||||
|
[StoreBusinessStatusEnum.ForceClosed]: { color: 'red', text: '强制关闭' },
|
||||||
|
[StoreBusinessStatusEnum.Operating]: { color: 'green', text: '营业中' },
|
||||||
|
[StoreBusinessStatusEnum.Resting]: { color: 'default', text: '休息中' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const auditStatusMap: Record<number, StatusTagMeta> = {
|
||||||
|
[StoreAuditStatusEnum.Approved]: { color: 'green', text: '已通过' },
|
||||||
|
[StoreAuditStatusEnum.Pending]: { color: 'orange', text: '待审核' },
|
||||||
|
[StoreAuditStatusEnum.Rejected]: { color: 'red', text: '已拒绝' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const serviceTypeMap: Record<number, StatusTagMeta> = {
|
||||||
|
[ServiceTypeEnum.Delivery]: { color: 'blue', text: '外卖' },
|
||||||
|
[ServiceTypeEnum.DineIn]: { color: 'orange', text: '堂食' },
|
||||||
|
[ServiceTypeEnum.Pickup]: { color: 'green', text: '自提' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const businessStatusOptions: SelectOptionItem<StoreBusinessStatus>[] = [
|
||||||
|
{ label: '营业中', value: StoreBusinessStatusEnum.Operating },
|
||||||
|
{ label: '休息中', value: StoreBusinessStatusEnum.Resting },
|
||||||
|
{ label: '强制关闭', value: StoreBusinessStatusEnum.ForceClosed },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const auditStatusOptions: SelectOptionItem<StoreAuditStatus>[] = [
|
||||||
|
{ label: '待审核', value: StoreAuditStatusEnum.Pending },
|
||||||
|
{ label: '已通过', value: StoreAuditStatusEnum.Approved },
|
||||||
|
{ label: '已拒绝', value: StoreAuditStatusEnum.Rejected },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const serviceTypeOptions: SelectOptionItem<ServiceType>[] = [
|
||||||
|
{ label: '外卖配送', value: ServiceTypeEnum.Delivery },
|
||||||
|
{ label: '到店自提', value: ServiceTypeEnum.Pickup },
|
||||||
|
{ label: '堂食', value: ServiceTypeEnum.DineIn },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const columns = [
|
||||||
|
{ key: 'storeInfo', title: '门店信息', width: 240 },
|
||||||
|
{
|
||||||
|
dataIndex: 'contactPhone',
|
||||||
|
key: 'contactPhone',
|
||||||
|
title: '联系电话',
|
||||||
|
width: 120,
|
||||||
|
},
|
||||||
|
{ dataIndex: 'managerName', key: 'managerName', title: '店长', width: 80 },
|
||||||
|
{ dataIndex: 'address', ellipsis: true, key: 'address', title: '地址' },
|
||||||
|
{ key: 'serviceTypes', title: '服务方式', width: 180 },
|
||||||
|
{ key: 'businessStatus', title: '营业状态', width: 100 },
|
||||||
|
{ key: 'auditStatus', title: '审核状态', width: 100 },
|
||||||
|
{ dataIndex: 'createdAt', key: 'createdAt', title: '创建时间', width: 120 },
|
||||||
|
{ fixed: 'right' as const, key: 'action', title: '操作', width: 180 },
|
||||||
|
];
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* 门店列表数据加载动作:
|
||||||
|
* - 列表查询
|
||||||
|
* - 统计查询
|
||||||
|
*/
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { PagePaginationState, StoreFilterState } from '../../types';
|
||||||
|
|
||||||
|
import type { StoreListItemDto, StoreStatsDto } from '#/api/store';
|
||||||
|
|
||||||
|
import { getStoreListApi, getStoreStatsApi } from '#/api/store';
|
||||||
|
|
||||||
|
interface CreateDataActionsOptions {
|
||||||
|
filters: StoreFilterState;
|
||||||
|
isLoading: Ref<boolean>;
|
||||||
|
pagination: PagePaginationState;
|
||||||
|
stats: Ref<StoreStatsDto>;
|
||||||
|
storeList: Ref<StoreListItemDto[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDataActions(options: CreateDataActionsOptions) {
|
||||||
|
/** 加载门店列表并同步分页总数。 */
|
||||||
|
async function loadList() {
|
||||||
|
// 1. 开启加载态。
|
||||||
|
options.isLoading.value = true;
|
||||||
|
try {
|
||||||
|
// 2. 按当前筛选与分页请求列表。
|
||||||
|
const res = await getStoreListApi({
|
||||||
|
auditStatus: options.filters.auditStatus,
|
||||||
|
businessStatus: options.filters.businessStatus,
|
||||||
|
keyword: options.filters.keyword || undefined,
|
||||||
|
page: options.pagination.current,
|
||||||
|
pageSize: options.pagination.pageSize,
|
||||||
|
serviceType: options.filters.serviceType,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3. 同步结果到页面状态。
|
||||||
|
options.storeList.value = res.items;
|
||||||
|
options.pagination.total = res.total;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
options.isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 加载门店统计数据。 */
|
||||||
|
async function loadStats() {
|
||||||
|
try {
|
||||||
|
options.stats.value = await getStoreStatsApi();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadList,
|
||||||
|
loadStats,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* 门店抽屉与删除动作:
|
||||||
|
* - 打开抽屉(新增/编辑)
|
||||||
|
* - 提交保存
|
||||||
|
* - 删除门店
|
||||||
|
*/
|
||||||
|
import type { Ref } from 'vue';
|
||||||
|
|
||||||
|
import type { DrawerMode, StoreFormState } from '../../types';
|
||||||
|
|
||||||
|
import type { StoreListItemDto } from '#/api/store';
|
||||||
|
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
|
|
||||||
|
import { createStoreApi, deleteStoreApi, updateStoreApi } from '#/api/store';
|
||||||
|
|
||||||
|
import { applyRecordToForm, resetStoreForm, toSavePayload } from './helpers';
|
||||||
|
|
||||||
|
interface CreateDrawerActionsOptions {
|
||||||
|
drawerMode: Ref<DrawerMode>;
|
||||||
|
formState: StoreFormState;
|
||||||
|
isDrawerVisible: Ref<boolean>;
|
||||||
|
isSubmitting: Ref<boolean>;
|
||||||
|
loadList: () => Promise<void>;
|
||||||
|
loadStats: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDrawerActions(options: CreateDrawerActionsOptions) {
|
||||||
|
/** 打开抽屉并准备对应模式的数据。 */
|
||||||
|
function openDrawer(mode: DrawerMode, record?: StoreListItemDto) {
|
||||||
|
// 1. 记录当前抽屉模式。
|
||||||
|
options.drawerMode.value = mode;
|
||||||
|
|
||||||
|
// 2. 根据模式回填或重置表单。
|
||||||
|
if (mode === 'edit' && record) {
|
||||||
|
applyRecordToForm(options.formState, record);
|
||||||
|
} else {
|
||||||
|
resetStoreForm(options.formState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 打开抽屉。
|
||||||
|
options.isDrawerVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交新增/编辑门店。 */
|
||||||
|
async function handleSubmit() {
|
||||||
|
// 1. 开启提交加载态。
|
||||||
|
options.isSubmitting.value = true;
|
||||||
|
try {
|
||||||
|
// 2. 根据模式调用对应保存接口。
|
||||||
|
const payload = toSavePayload(options.formState);
|
||||||
|
if (options.drawerMode.value === 'edit') {
|
||||||
|
await updateStoreApi(payload);
|
||||||
|
message.success('更新成功');
|
||||||
|
} else {
|
||||||
|
await createStoreApi(payload);
|
||||||
|
message.success('创建成功');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 关闭抽屉并刷新页面数据。
|
||||||
|
options.isDrawerVisible.value = false;
|
||||||
|
await Promise.all([options.loadList(), options.loadStats()]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
} finally {
|
||||||
|
options.isSubmitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除门店并刷新数据。 */
|
||||||
|
async function handleDelete(record: StoreListItemDto) {
|
||||||
|
try {
|
||||||
|
// 1. 调用删除接口。
|
||||||
|
await deleteStoreApi(record.id);
|
||||||
|
message.success('删除成功');
|
||||||
|
|
||||||
|
// 2. 同步刷新列表与统计。
|
||||||
|
await Promise.all([options.loadList(), options.loadStats()]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleDelete,
|
||||||
|
handleSubmit,
|
||||||
|
openDrawer,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/**
|
||||||
|
* 门店列表页纯函数工具:
|
||||||
|
* - 颜色计算
|
||||||
|
* - 过滤与表单重置
|
||||||
|
* - 保存参数转换
|
||||||
|
*/
|
||||||
|
import type { StoreFilterState, StoreFormState } from '../../types';
|
||||||
|
|
||||||
|
import type { SaveStoreDto, StoreListItemDto } from '#/api/store';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AVATAR_COLORS,
|
||||||
|
DEFAULT_FILTERS,
|
||||||
|
DEFAULT_FORM_STATE,
|
||||||
|
} from './constants';
|
||||||
|
|
||||||
|
/** 获取门店头像装饰色。 */
|
||||||
|
export function getAvatarColor(index: number) {
|
||||||
|
return AVATAR_COLORS[index % AVATAR_COLORS.length] ?? AVATAR_COLORS[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置筛选条件为默认值。 */
|
||||||
|
export function resetFilters(filters: StoreFilterState) {
|
||||||
|
Object.assign(filters, DEFAULT_FILTERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置抽屉表单为默认值。 */
|
||||||
|
export function resetStoreForm(formState: StoreFormState) {
|
||||||
|
Object.assign(formState, {
|
||||||
|
...DEFAULT_FORM_STATE,
|
||||||
|
serviceTypes: [...DEFAULT_FORM_STATE.serviceTypes],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按门店记录回填抽屉表单。 */
|
||||||
|
export function applyRecordToForm(
|
||||||
|
formState: StoreFormState,
|
||||||
|
store: StoreListItemDto,
|
||||||
|
) {
|
||||||
|
Object.assign(formState, {
|
||||||
|
address: store.address,
|
||||||
|
businessStatus: store.businessStatus,
|
||||||
|
code: store.code,
|
||||||
|
contactPhone: store.contactPhone,
|
||||||
|
coverImage: store.coverImage || '',
|
||||||
|
id: store.id,
|
||||||
|
managerName: store.managerName,
|
||||||
|
name: store.name,
|
||||||
|
serviceTypes: [...store.serviceTypes],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将表单状态转换为保存接口参数。 */
|
||||||
|
export function toSavePayload(formState: StoreFormState): SaveStoreDto {
|
||||||
|
return {
|
||||||
|
address: formState.address.trim(),
|
||||||
|
businessStatus: formState.businessStatus,
|
||||||
|
code: formState.code.trim(),
|
||||||
|
contactPhone: formState.contactPhone.trim(),
|
||||||
|
coverImage: formState.coverImage.trim(),
|
||||||
|
id: formState.id || undefined,
|
||||||
|
managerName: formState.managerName.trim(),
|
||||||
|
name: formState.name.trim(),
|
||||||
|
serviceTypes: [...formState.serviceTypes],
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* 门店列表交互动作:
|
||||||
|
* - 查询
|
||||||
|
* - 重置
|
||||||
|
* - 分页变更
|
||||||
|
*/
|
||||||
|
import type {
|
||||||
|
PagePaginationState,
|
||||||
|
StoreFilterState,
|
||||||
|
TablePagination,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
|
import { resetFilters } from './helpers';
|
||||||
|
|
||||||
|
interface CreateListActionsOptions {
|
||||||
|
filters: StoreFilterState;
|
||||||
|
loadList: () => Promise<void>;
|
||||||
|
pagination: PagePaginationState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createListActions(options: CreateListActionsOptions) {
|
||||||
|
/** 按当前筛选条件查询列表。 */
|
||||||
|
async function handleSearch() {
|
||||||
|
// 1. 查询时回到第一页。
|
||||||
|
options.pagination.current = 1;
|
||||||
|
|
||||||
|
// 2. 刷新列表数据。
|
||||||
|
await options.loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置筛选并刷新列表。 */
|
||||||
|
async function handleReset() {
|
||||||
|
// 1. 清空筛选条件。
|
||||||
|
resetFilters(options.filters);
|
||||||
|
|
||||||
|
// 2. 重置页码并刷新列表。
|
||||||
|
options.pagination.current = 1;
|
||||||
|
await options.loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 分页参数变化后刷新列表。 */
|
||||||
|
async function handleTableChange(pagination: TablePagination) {
|
||||||
|
// 1. 同步分页参数。
|
||||||
|
options.pagination.current = pagination.current ?? 1;
|
||||||
|
options.pagination.pageSize = pagination.pageSize ?? 10;
|
||||||
|
|
||||||
|
// 2. 使用新分页重新加载。
|
||||||
|
await options.loadList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleReset,
|
||||||
|
handleSearch,
|
||||||
|
handleTableChange,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
/**
|
||||||
|
* 门店列表页面主编排:
|
||||||
|
* - 维护页面状态
|
||||||
|
* - 组合数据与动作模块
|
||||||
|
* - 对外暴露组件可消费的数据与方法
|
||||||
|
*/
|
||||||
|
import type {
|
||||||
|
DrawerMode,
|
||||||
|
PagePaginationState,
|
||||||
|
StatCardItem,
|
||||||
|
StoreFilterState,
|
||||||
|
StoreFormState,
|
||||||
|
StoreTablePagination,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ServiceType,
|
||||||
|
StoreAuditStatus,
|
||||||
|
StoreBusinessStatus,
|
||||||
|
StoreListItemDto,
|
||||||
|
StoreStatsDto,
|
||||||
|
} from '#/api/store';
|
||||||
|
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
auditStatusMap,
|
||||||
|
auditStatusOptions,
|
||||||
|
businessStatusMap,
|
||||||
|
businessStatusOptions,
|
||||||
|
columns,
|
||||||
|
DEFAULT_FILTERS,
|
||||||
|
DEFAULT_FORM_STATE,
|
||||||
|
DEFAULT_PAGINATION,
|
||||||
|
DEFAULT_STATS,
|
||||||
|
serviceTypeMap,
|
||||||
|
serviceTypeOptions,
|
||||||
|
THEME_COLORS,
|
||||||
|
} from './store-list-page/constants';
|
||||||
|
import { createDataActions } from './store-list-page/data-actions';
|
||||||
|
import { createDrawerActions } from './store-list-page/drawer-actions';
|
||||||
|
import { getAvatarColor } from './store-list-page/helpers';
|
||||||
|
import { createListActions } from './store-list-page/list-actions';
|
||||||
|
|
||||||
|
export function useStoreListPage() {
|
||||||
|
// 1. 列表与筛选状态。
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const storeList = ref<StoreListItemDto[]>([]);
|
||||||
|
const stats = ref<StoreStatsDto>({ ...DEFAULT_STATS });
|
||||||
|
const pagination = reactive<PagePaginationState>({ ...DEFAULT_PAGINATION });
|
||||||
|
const filters = reactive<StoreFilterState>({ ...DEFAULT_FILTERS });
|
||||||
|
|
||||||
|
// 2. 抽屉与表单状态。
|
||||||
|
const isDrawerVisible = ref(false);
|
||||||
|
const drawerMode = ref<DrawerMode>('create');
|
||||||
|
const formState = reactive<StoreFormState>({
|
||||||
|
...DEFAULT_FORM_STATE,
|
||||||
|
serviceTypes: [...DEFAULT_FORM_STATE.serviceTypes],
|
||||||
|
});
|
||||||
|
const isSubmitting = ref(false);
|
||||||
|
|
||||||
|
// 3. 视图衍生数据。
|
||||||
|
const drawerTitle = computed(() =>
|
||||||
|
drawerMode.value === 'edit' ? '编辑门店' : '添加门店',
|
||||||
|
);
|
||||||
|
|
||||||
|
const statCards = computed<StatCardItem[]>(() => [
|
||||||
|
{ color: THEME_COLORS.text, title: '门店总数', value: stats.value.total },
|
||||||
|
{
|
||||||
|
color: THEME_COLORS.success,
|
||||||
|
title: '营业中',
|
||||||
|
value: stats.value.operating,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: THEME_COLORS.warning,
|
||||||
|
title: '休息中',
|
||||||
|
value: stats.value.resting,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
color: THEME_COLORS.danger,
|
||||||
|
title: '待审核',
|
||||||
|
value: stats.value.pendingAudit,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const tablePagination = computed<StoreTablePagination>(() => ({
|
||||||
|
current: pagination.current,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
showSizeChanger: true,
|
||||||
|
showTotal: (total: number) => `共 ${total} 条`,
|
||||||
|
total: pagination.total,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 4. 组装数据加载动作。
|
||||||
|
const { loadList, loadStats } = createDataActions({
|
||||||
|
filters,
|
||||||
|
isLoading,
|
||||||
|
pagination,
|
||||||
|
stats,
|
||||||
|
storeList,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. 组装列表交互动作。
|
||||||
|
const { handleReset, handleSearch, handleTableChange } = createListActions({
|
||||||
|
filters,
|
||||||
|
loadList,
|
||||||
|
pagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 6. 组装抽屉与删除动作。
|
||||||
|
const { handleDelete, handleSubmit, openDrawer } = createDrawerActions({
|
||||||
|
drawerMode,
|
||||||
|
formState,
|
||||||
|
isDrawerVisible,
|
||||||
|
isSubmitting,
|
||||||
|
loadList,
|
||||||
|
loadStats,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 7. 筛选字段更新方法。
|
||||||
|
function setKeyword(value: string) {
|
||||||
|
filters.keyword = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBusinessStatus(value?: StoreBusinessStatus) {
|
||||||
|
filters.businessStatus = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setAuditStatus(value?: StoreAuditStatus) {
|
||||||
|
filters.auditStatus = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setServiceType(value?: ServiceType) {
|
||||||
|
filters.serviceType = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 抽屉表单字段更新方法。
|
||||||
|
function setDrawerVisible(value: boolean) {
|
||||||
|
isDrawerVisible.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormName(value: string) {
|
||||||
|
formState.name = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormCode(value: string) {
|
||||||
|
formState.code = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormContactPhone(value: string) {
|
||||||
|
formState.contactPhone = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormManagerName(value: string) {
|
||||||
|
formState.managerName = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormAddress(value: string) {
|
||||||
|
formState.address = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormBusinessStatus(value: StoreBusinessStatus) {
|
||||||
|
formState.businessStatus = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setFormServiceTypes(value: ServiceType[]) {
|
||||||
|
formState.serviceTypes = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. 抽屉便捷入口。
|
||||||
|
function openCreateDrawer() {
|
||||||
|
openDrawer('create');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDrawer(record: StoreListItemDto) {
|
||||||
|
openDrawer('edit', record);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 10. 页面初始化。
|
||||||
|
onMounted(() => {
|
||||||
|
loadList();
|
||||||
|
loadStats();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
auditStatusMap,
|
||||||
|
auditStatusOptions,
|
||||||
|
businessStatusMap,
|
||||||
|
businessStatusOptions,
|
||||||
|
columns,
|
||||||
|
drawerTitle,
|
||||||
|
filters,
|
||||||
|
formState,
|
||||||
|
getAvatarColor,
|
||||||
|
handleDeleteStore: handleDelete,
|
||||||
|
handleReset,
|
||||||
|
handleSearch,
|
||||||
|
handleSubmitStore: handleSubmit,
|
||||||
|
handleTableChange,
|
||||||
|
isDrawerVisible,
|
||||||
|
isLoading,
|
||||||
|
isSubmitting,
|
||||||
|
openCreateDrawer,
|
||||||
|
openEditDrawer,
|
||||||
|
pagination,
|
||||||
|
serviceTypeMap,
|
||||||
|
serviceTypeOptions,
|
||||||
|
setAuditStatus,
|
||||||
|
setBusinessStatus,
|
||||||
|
setDrawerVisible,
|
||||||
|
setFormAddress,
|
||||||
|
setFormBusinessStatus,
|
||||||
|
setFormCode,
|
||||||
|
setFormContactPhone,
|
||||||
|
setFormManagerName,
|
||||||
|
setFormName,
|
||||||
|
setFormServiceTypes,
|
||||||
|
setKeyword,
|
||||||
|
setServiceType,
|
||||||
|
statCards,
|
||||||
|
storeList,
|
||||||
|
tablePagination,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,557 +1,129 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { StoreListItemDto, StoreStatsDto } from '#/api/store';
|
/**
|
||||||
|
* 门店列表页面主视图:
|
||||||
import { computed, onMounted, reactive, ref } from 'vue';
|
* - 页面级编排与组件组合
|
||||||
|
* - 业务状态与动作由 composable 提供
|
||||||
|
*/
|
||||||
import { Page } from '@vben/common-ui';
|
import { Page } from '@vben/common-ui';
|
||||||
|
|
||||||
import {
|
import { Button, message } from 'ant-design-vue';
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Col,
|
|
||||||
Drawer,
|
|
||||||
Form,
|
|
||||||
FormItem,
|
|
||||||
Input,
|
|
||||||
InputSearch,
|
|
||||||
message,
|
|
||||||
Popconfirm,
|
|
||||||
Row,
|
|
||||||
Select,
|
|
||||||
SelectOption,
|
|
||||||
Statistic,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
} from 'ant-design-vue';
|
|
||||||
|
|
||||||
import {
|
import StoreEditorDrawer from './components/StoreEditorDrawer.vue';
|
||||||
createStoreApi,
|
import StoreFilterBar from './components/StoreFilterBar.vue';
|
||||||
deleteStoreApi,
|
import StoreListTable from './components/StoreListTable.vue';
|
||||||
getStoreListApi,
|
import StoreStatsCards from './components/StoreStatsCards.vue';
|
||||||
getStoreStatsApi,
|
import { useStoreListPage } from './composables/useStoreListPage';
|
||||||
ServiceType,
|
|
||||||
StoreAuditStatus,
|
|
||||||
StoreBusinessStatus,
|
|
||||||
updateStoreApi,
|
|
||||||
} from '#/api/store';
|
|
||||||
|
|
||||||
/** 表格分页变更参数 */
|
const {
|
||||||
interface TablePagination {
|
auditStatusMap,
|
||||||
current?: number;
|
auditStatusOptions,
|
||||||
pageSize?: number;
|
businessStatusMap,
|
||||||
|
businessStatusOptions,
|
||||||
|
columns,
|
||||||
|
drawerTitle,
|
||||||
|
filters,
|
||||||
|
formState,
|
||||||
|
getAvatarColor,
|
||||||
|
handleDeleteStore,
|
||||||
|
handleReset,
|
||||||
|
handleSearch,
|
||||||
|
handleSubmitStore,
|
||||||
|
handleTableChange,
|
||||||
|
isDrawerVisible,
|
||||||
|
isLoading,
|
||||||
|
isSubmitting,
|
||||||
|
openCreateDrawer,
|
||||||
|
openEditDrawer,
|
||||||
|
serviceTypeMap,
|
||||||
|
serviceTypeOptions,
|
||||||
|
setAuditStatus,
|
||||||
|
setBusinessStatus,
|
||||||
|
setDrawerVisible,
|
||||||
|
setFormAddress,
|
||||||
|
setFormBusinessStatus,
|
||||||
|
setFormCode,
|
||||||
|
setFormContactPhone,
|
||||||
|
setFormManagerName,
|
||||||
|
setFormName,
|
||||||
|
setFormServiceTypes,
|
||||||
|
setKeyword,
|
||||||
|
setServiceType,
|
||||||
|
statCards,
|
||||||
|
storeList,
|
||||||
|
tablePagination,
|
||||||
|
} = useStoreListPage();
|
||||||
|
|
||||||
|
/** 导出能力当前未实现,先给出占位提示。 */
|
||||||
|
function handleExport() {
|
||||||
|
message.info('导出功能开发中');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 语义色值常量(后续可迁移至全局主题配置)
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Page title="门店列表" content-class="space-y-4">
|
<Page title="门店列表" content-class="space-y-4 page-store-list">
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<div class="flex gap-2">
|
<div class="store-list-page-extra">
|
||||||
<Button @click="() => message.info('导出功能开发中')">导出</Button>
|
<Button @click="handleExport">导出</Button>
|
||||||
<Button type="primary" @click="openDrawer('create')">新增门店</Button>
|
<Button type="primary" @click="openCreateDrawer">新增门店</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 筛选栏 -->
|
<StoreFilterBar
|
||||||
<Card :bordered="false" size="small">
|
:keyword="filters.keyword"
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
:business-status="filters.businessStatus"
|
||||||
<InputSearch
|
:audit-status="filters.auditStatus"
|
||||||
v-model:value="filters.keyword"
|
:service-type="filters.serviceType"
|
||||||
placeholder="搜索门店名称 / 编号 / 电话"
|
:business-status-options="businessStatusOptions"
|
||||||
style="width: 260px"
|
:audit-status-options="auditStatusOptions"
|
||||||
allow-clear
|
:service-type-options="serviceTypeOptions"
|
||||||
@search="handleSearch"
|
:on-set-keyword="setKeyword"
|
||||||
/>
|
:on-set-business-status="setBusinessStatus"
|
||||||
<Select
|
:on-set-audit-status="setAuditStatus"
|
||||||
v-model:value="filters.businessStatus"
|
:on-set-service-type="setServiceType"
|
||||||
placeholder="营业状态"
|
@search="handleSearch"
|
||||||
style="width: 130px"
|
@reset="handleReset"
|
||||||
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>
|
|
||||||
|
|
||||||
<!-- 统计卡片 -->
|
<StoreStatsCards :items="statCards" />
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- 表格 -->
|
<StoreListTable
|
||||||
<Card :bordered="false">
|
:columns="columns"
|
||||||
<Table
|
:store-list="storeList"
|
||||||
:columns="columns"
|
:is-loading="isLoading"
|
||||||
:data-source="storeList"
|
:pagination="tablePagination"
|
||||||
:loading="isLoading"
|
:service-type-map="serviceTypeMap"
|
||||||
:pagination="{
|
:business-status-map="businessStatusMap"
|
||||||
current: pagination.current,
|
:audit-status-map="auditStatusMap"
|
||||||
pageSize: pagination.pageSize,
|
:get-avatar-color="getAvatarColor"
|
||||||
total: pagination.total,
|
@table-change="handleTableChange"
|
||||||
showSizeChanger: true,
|
@edit="openEditDrawer"
|
||||||
showTotal: (total: number) => `共 ${total} 条`,
|
@delete="handleDeleteStore"
|
||||||
}"
|
/>
|
||||||
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>
|
|
||||||
|
|
||||||
<!-- 服务方式 -->
|
<StoreEditorDrawer
|
||||||
<template v-if="column.key === 'serviceTypes'">
|
:open="isDrawerVisible"
|
||||||
<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"
|
:title="drawerTitle"
|
||||||
:width="520"
|
:is-submitting="isSubmitting"
|
||||||
:body-style="{ paddingBottom: '80px' }"
|
:name="formState.name"
|
||||||
>
|
:code="formState.code"
|
||||||
<Form layout="vertical">
|
:contact-phone="formState.contactPhone"
|
||||||
<div
|
:manager-name="formState.managerName"
|
||||||
class="mb-4 border-l-[3px] border-blue-500 pl-2.5 text-[15px] font-semibold"
|
:address="formState.address"
|
||||||
>
|
:business-status="formState.businessStatus"
|
||||||
基本信息
|
:service-types="formState.serviceTypes"
|
||||||
</div>
|
:business-status-options="businessStatusOptions"
|
||||||
<FormItem label="门店名称" required>
|
:service-type-options="serviceTypeOptions"
|
||||||
<Input v-model:value="formState.name" placeholder="请输入门店名称" />
|
:on-set-name="setFormName"
|
||||||
</FormItem>
|
:on-set-code="setFormCode"
|
||||||
<FormItem label="门店编码" required>
|
:on-set-contact-phone="setFormContactPhone"
|
||||||
<Input v-model:value="formState.code" placeholder="如:ST20250006" />
|
:on-set-manager-name="setFormManagerName"
|
||||||
</FormItem>
|
:on-set-address="setFormAddress"
|
||||||
<FormItem label="联系电话" required>
|
:on-set-business-status="setFormBusinessStatus"
|
||||||
<Input
|
:on-set-service-types="setFormServiceTypes"
|
||||||
v-model:value="formState.contactPhone"
|
@update:open="setDrawerVisible"
|
||||||
placeholder="请输入联系电话"
|
@submit="handleSubmitStore"
|
||||||
/>
|
/>
|
||||||
</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>
|
</Page>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style src="./styles/index.less"></style>
|
||||||
|
|||||||
35
apps/web-antd/src/views/store/list/styles/base.less
Normal file
35
apps/web-antd/src/views/store/list/styles/base.less
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/* 门店列表页基础布局(头部、筛选、统计)样式。 */
|
||||||
|
.page-store-list {
|
||||||
|
.store-list-page-extra {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-filter-card .ant-card-body {
|
||||||
|
padding: 12px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-filter-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-search {
|
||||||
|
width: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
width: 130px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-stats-row .ant-card {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-stats-row .ant-card:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
apps/web-antd/src/views/store/list/styles/drawer.less
Normal file
50
apps/web-antd/src/views/store/list/styles/drawer.less
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/* 门店编辑抽屉样式。 */
|
||||||
|
.store-editor-drawer-wrap {
|
||||||
|
.ant-drawer-content-wrapper {
|
||||||
|
box-shadow:
|
||||||
|
0 8px 24px rgb(0 0 0 / 8%),
|
||||||
|
0 2px 6px rgb(0 0 0 / 4%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-header {
|
||||||
|
min-height: 56px;
|
||||||
|
padding: 0 24px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f1f1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-body {
|
||||||
|
padding: 20px 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-drawer-footer {
|
||||||
|
padding: 14px 24px;
|
||||||
|
border-top: 1px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-store-list {
|
||||||
|
.store-drawer-form .store-drawer-section-title {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
padding-left: 10px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f1f1f;
|
||||||
|
border-left: 3px solid #1677ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-drawer-form .store-drawer-section-title + .ant-form-item {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-drawer-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
}
|
||||||
5
apps/web-antd/src/views/store/list/styles/index.less
Normal file
5
apps/web-antd/src/views/store/list/styles/index.less
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/* 门店列表页面样式聚合入口(仅负责分片导入)。 */
|
||||||
|
@import './base.less';
|
||||||
|
@import './table.less';
|
||||||
|
@import './drawer.less';
|
||||||
|
@import './responsive.less';
|
||||||
18
apps/web-antd/src/views/store/list/styles/responsive.less
Normal file
18
apps/web-antd/src/views/store/list/styles/responsive.less
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/* 门店列表页响应式样式。 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.page-store-list {
|
||||||
|
.store-list-page-extra {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-search {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-select {
|
||||||
|
width: calc(50% - 6px);
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
50
apps/web-antd/src/views/store/list/styles/table.less
Normal file
50
apps/web-antd/src/views/store/list/styles/table.less
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/* 门店列表表格区样式。 */
|
||||||
|
.page-store-list {
|
||||||
|
.store-list-table .store-info-cell {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-list-table .store-avatar {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-list-table .store-info-text {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-list-table .store-name {
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1f1f1f;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-list-table .store-code {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-list-table .service-tag {
|
||||||
|
margin-inline-end: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-list-table .store-action-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
75
apps/web-antd/src/views/store/list/types.ts
Normal file
75
apps/web-antd/src/views/store/list/types.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* 门店列表页专属类型定义:
|
||||||
|
* - 页面状态(筛选、分页、抽屉表单)
|
||||||
|
* - 组件交互参数
|
||||||
|
*/
|
||||||
|
import type {
|
||||||
|
ServiceType,
|
||||||
|
StoreAuditStatus,
|
||||||
|
StoreBusinessStatus,
|
||||||
|
} from '#/api/store';
|
||||||
|
|
||||||
|
/** 表格分页变更参数。 */
|
||||||
|
export interface TablePagination {
|
||||||
|
current?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 下拉选项通用结构。 */
|
||||||
|
export interface SelectOptionItem<T extends number | string> {
|
||||||
|
label: string;
|
||||||
|
value: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 页面分页状态。 */
|
||||||
|
export interface PagePaginationState {
|
||||||
|
current: number;
|
||||||
|
pageSize: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 页面筛选条件。 */
|
||||||
|
export interface StoreFilterState {
|
||||||
|
auditStatus?: StoreAuditStatus;
|
||||||
|
businessStatus?: StoreBusinessStatus;
|
||||||
|
keyword: string;
|
||||||
|
serviceType?: ServiceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 抽屉表单状态。 */
|
||||||
|
export interface StoreFormState {
|
||||||
|
address: string;
|
||||||
|
businessStatus: StoreBusinessStatus;
|
||||||
|
code: string;
|
||||||
|
contactPhone: string;
|
||||||
|
coverImage: string;
|
||||||
|
id: string;
|
||||||
|
managerName: string;
|
||||||
|
name: string;
|
||||||
|
serviceTypes: ServiceType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 状态标签元信息。 */
|
||||||
|
export interface StatusTagMeta {
|
||||||
|
color: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 统计卡片项。 */
|
||||||
|
export interface StatCardItem {
|
||||||
|
color: string;
|
||||||
|
title: string;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 抽屉模式。 */
|
||||||
|
export type DrawerMode = 'create' | 'edit';
|
||||||
|
|
||||||
|
/** 表格分页展示对象。 */
|
||||||
|
export interface StoreTablePagination {
|
||||||
|
current: number;
|
||||||
|
pageSize: number;
|
||||||
|
showSizeChanger: boolean;
|
||||||
|
showTotal: (total: number) => string;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user