add store

This commit is contained in:
Timi
2026-08-07 11:32:48 +08:00
parent 9df6dfaf59
commit fab3cd8891
21 changed files with 1355 additions and 31 deletions
+1 -2
View File
@@ -190,7 +190,7 @@ async function doSubmit(): Promise<void> {
customerId: customer.id,
eventId: selectedEventId.value,
remark: "客户快速扫码登记"
})
});
scanStatus.value = `登记成功:${customer.name || customer.code || scanCode.value}。`;
scanCode.value = "";
matchedCustomer.value = undefined;
@@ -362,4 +362,3 @@ async function doSubmit(): Promise<void> {
}
}
</style>
+1 -2
View File
@@ -124,7 +124,7 @@ const fetch = Toolkit.debounce(async () => {
likesExample: {
code: keyword.value,
name: keyword.value,
telephone: keyword.value,
telephone: keyword.value
}
});
pageResult.total = result.total;
@@ -336,4 +336,3 @@ async function doExport(): Promise<void> {
}
}
</style>
@@ -0,0 +1,406 @@
<template>
<div v-can="permissionCodes.store.read" class="business-day-list">
<header class="summary">
<div>
<strong v-text="storeName" />
<span v-text="storeCodeText" />
</div>
<t-tag theme="default" variant="outline" content="无记录表示未配置" />
</header>
<t-pull-down-refresh
v-model="isRefreshing"
:refresh-timeout="30000"
@refresh="refresh"
@scrolltolower="fetch"
>
<div v-for="item in pageResult.list" :key="item.id" class="item">
<div class="content" @click="openDayEditor(item)">
<div class="header">
<span class="date" v-text="formatDateValue(item.dateValue)" />
<t-tag size="small" :theme="getBusinessDayStatusTheme(item.openStatus)" variant="outline" :content="getBusinessDayStatusText(item.openStatus)" />
</div>
<span class="meta" v-text="item.openStatus === 'OPEN' ? `营业日序号:${item.businessDayNo || '-'}` : `原因:${item.reason || '-'}`" />
</div>
<t-button v-can="permissionCodes.store.delete" class="delete" shape="circle" theme="danger" variant="text" @click="confirmRemoveDay(item)">
<t-icon name="delete" />
</t-button>
</div>
<div v-if="!pageResult.list.length" class="empty-block" v-text="isLoading ? '正在加载营业日' : '暂无已配置营业日'" />
<div v-if="pageResult.list.length" class="load-state" v-text="isNoMore ? '已经到底了' : '上拉加载更多'" />
</t-pull-down-refresh>
<t-button v-can="permissionCodes.store.create" class="add" theme="primary" @click="openDayEditor()">
<template #icon>
<t-icon name="add" />
</template>
新增营业日
</t-button>
<t-popup v-model="editorVisible" placement="bottom">
<div class="editor">
<h3 v-text="editingId ? '编辑营业日' : '新增营业日'" />
<t-form label-align="left">
<t-form-item label="日期">
<button class="picker-btn" type="button" @click="openDatePicker">
<span v-text="formModel.dateText || '选择日期'" />
<t-icon name="calendar" />
</button>
</t-form-item>
<t-form-item label="状态">
<t-radio-group v-model="formModel.openStatus" variant="default-filled">
<t-radio :block="false" borderless value="OPEN">营业</t-radio>
<t-radio :block="false" borderless value="CLOSED">休息</t-radio>
</t-radio-group>
</t-form-item>
<t-form-item label="原因">
<t-input v-model.trim="formModel.reason" placeholder="休息原因或备注" borderless />
</t-form-item>
</t-form>
<p class="hint">切换营业或休息可能影响连续登记统计,营业日序号由后端自动维护。</p>
<t-button v-can="savePermissionCode" theme="primary" block :loading="isSaving" @click="saveDay">保存</t-button>
</div>
</t-popup>
<t-popup v-model="datePickerVisible" placement="bottom">
<t-date-time-picker
v-if="datePickerVisible"
v-model="datePickerDraft"
:mode="['date']"
format="YYYY-MM-DD"
title="营业日期"
@cancel="datePickerVisible = false"
@confirm="confirmDatePicker"
/>
</t-popup>
</div>
</template>
<script setup lang="ts">
import { DialogPlugin, Toast } from "tdesign-mobile-vue";
import { type Page, type PageResult } from "timi-web";
import StoreAPI from "@/api/StoreAPI";
import type { Store, StoreBusinessDay, StoreBusinessDayOpenStatus } from "@/types/Store";
import { permissionCodes } from "@/utils/permission";
import { getBusinessDayStatusText, getBusinessDayStatusTheme } from "@/utils/store";
defineOptions({
name: "StoreBusinessDayList"
});
const route = useRoute();
const storeId = computed(() => String(route.params.id || ""));
// ---------- 门店信息 ----------
const store = ref<Store>();
const storeName = computed(() => store.value?.name || "门店");
const storeCodeText = computed(() => `编码:${store.value?.code || "-"}`);
// ---------- 列表数据 ----------
const isRefreshing = ref(false);
const isLoading = ref(false);
const isNoMore = ref(false);
const page = reactive<Page<StoreBusinessDay>>({
index: 0,
size: 20,
equalsExample: {
storeId: storeId.value
},
orderMap: {
dateValue: "DESC"
}
});
const pageResult = reactive<PageResult<StoreBusinessDay>>({
total: 0,
list: []
});
async function refresh(): Promise<void> {
isRefreshing.value = true;
try {
page.index = 0;
page.equalsExample = {
storeId: storeId.value
};
pageResult.total = 0;
pageResult.list = [];
isNoMore.value = false;
await fetch();
} finally {
isRefreshing.value = false;
}
}
async function fetch(): Promise<void> {
if (isLoading.value || isNoMore.value) {
return;
}
isLoading.value = true;
try {
const result = await StoreAPI.businessDayList(page);
pageResult.total = result.total || 0;
pageResult.list = pageResult.list.concat(result.list || []);
page.index += 1;
isNoMore.value = pageResult.list.length >= pageResult.total || (result.list || []).length < page.size;
} finally {
isLoading.value = false;
}
}
// ---------- 编辑 ----------
const editorVisible = ref(false);
const datePickerVisible = ref(false);
const datePickerDraft = ref("");
const isSaving = ref(false);
const editingId = ref("");
const formModel = reactive({
dateText: "",
openStatus: "OPEN" as StoreBusinessDayOpenStatus,
reason: ""
});
const savePermissionCode = computed(() => editingId.value ? permissionCodes.store.update : permissionCodes.store.create);
function openDayEditor(day?: StoreBusinessDay): void {
editingId.value = day?.id || "";
Object.assign(formModel, {
dateText: day?.dateValue ? formatDateValue(day.dateValue) : formatDateValue(toDateValue(new Date())),
openStatus: day?.openStatus || "OPEN",
reason: day?.reason || ""
});
editorVisible.value = true;
}
function openDatePicker(): void {
datePickerDraft.value = formModel.dateText;
datePickerVisible.value = true;
}
function confirmDatePicker(): void {
formModel.dateText = datePickerDraft.value;
datePickerVisible.value = false;
}
async function saveDay(): Promise<void> {
if (!validateDay()) {
return;
}
isSaving.value = true;
try {
const day: StoreBusinessDay = {
id: editingId.value || undefined,
storeId: storeId.value,
dateValue: parseDateValue(formModel.dateText),
openStatus: formModel.openStatus,
reason: formModel.reason || undefined
};
if (editingId.value) {
await StoreAPI.updateBusinessDay(day);
} else {
await StoreAPI.createBusinessDay(day);
}
Toast.success("营业日已保存");
editorVisible.value = false;
await refresh();
} finally {
isSaving.value = false;
}
}
function validateDay(): boolean {
if (!formModel.dateText) {
Toast.warning("请选择日期");
return false;
}
return true;
}
// ---------- 删除 ----------
async function confirmRemoveDay(day: StoreBusinessDay): Promise<void> {
if (!day.id) {
return;
}
const dayId = day.id;
const dialog = DialogPlugin.confirm({
title: "删除营业日",
content: `确定删除 ${formatDateValue(day.dateValue)} 的营业日配置吗?已有登记记录的营业日后端会拒绝删除。`,
cancelBtn: "取消",
confirmBtn: "删除",
onCancel() {
dialog.destroy();
},
async onConfirm() {
dialog.destroy();
await removeDay(dayId);
}
});
}
async function removeDay(id: string): Promise<void> {
await StoreAPI.removeBusinessDay(id);
Toast.success("营业日已删除");
await refresh();
}
// ---------- 日期 ----------
function formatDateValue(value?: number): string {
if (!value) {
return "";
}
const text = String(value);
return `${text.slice(0, 4)}-${text.slice(4, 6)}-${text.slice(6, 8)}`;
}
function parseDateValue(text: string): number {
return Number(text.replaceAll("-", ""));
}
function toDateValue(date: Date): number {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return Number(`${year}${month}${day}`);
}
onMounted(async () => {
try {
store.value = await StoreAPI.detail(storeId.value);
await fetch();
} catch {
Toast.error("营业日数据加载失败");
}
});
</script>
<style scoped lang="less">
.business-day-list {
gap: .75rem;
display: grid;
padding: calc(var(--app-nav-height) + 1rem) 1rem 6rem 1rem;
.summary {
display: flex;
padding: .75rem;
align-items: center;
background: #fff;
border-radius: .75rem;
justify-content: space-between;
div {
gap: .25rem;
display: flex;
flex-direction: column;
}
span {
color: #52607a;
font-size: .8125rem;
}
}
.item {
gap: .5rem;
display: flex;
padding: .75rem;
align-items: center;
border: 1px solid var(--td-border-level-1-color);
background: #fff;
border-radius: .75rem;
.content {
gap: .375rem;
flex: 1;
display: flex;
min-width: 0;
flex-direction: column;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
.date {
font-size: 1rem;
font-weight: 700;
}
.meta {
color: #52607a;
font-size: .875rem;
}
.delete {
flex: none;
}
}
.add {
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
position: fixed;
}
.editor {
gap: .75rem;
display: flex;
padding: 1rem;
background: #fff;
flex-direction: column;
h3 {
margin: 0;
font-size: 1.125rem;
}
.hint {
margin: 0;
color: #52607a;
font-size: .8125rem;
line-height: 1.6;
}
}
.picker-btn {
gap: .5rem;
width: 100%;
border: 0;
display: flex;
padding: 0;
color: inherit;
background: transparent;
align-items: center;
justify-content: space-between;
font: inherit;
}
}
:global(html.theme-dark) {
.business-day-list {
.summary,
.item,
.editor {
background: #1e293b;
}
.summary span,
.item .meta,
.editor .hint {
color: #94a3b8;
}
.date,
.summary strong,
.editor h3 {
color: #e2e8f0;
}
}
}
</style>
+257
View File
@@ -0,0 +1,257 @@
<template>
<div v-can="savePermissionCode" class="store-edit">
<t-form class="form" label-align="left">
<t-form-item label="编码">
<t-input v-model.trim="formModel.code" placeholder="门店编码" borderless />
</t-form-item>
<t-form-item label="名称">
<t-input v-model.trim="formModel.name" placeholder="门店名称" borderless />
</t-form-item>
<t-form-item label="状态">
<button class="picker-btn" type="button" @click="openStatusPicker">
<span v-text="getStoreStatusText(formModel.status)" />
<t-icon name="chevron-right" />
</button>
</t-form-item>
<t-form-item label="电话">
<t-input v-model.trim="formModel.telephone" type="tel" placeholder="联系电话" borderless />
</t-form-item>
<t-form-item label="地址">
<t-input v-model.trim="formModel.address" placeholder="门店地址" borderless />
</t-form-item>
<t-form-item label="排序">
<t-input v-model.trim="formModel.sortText" type="number" placeholder="排序值" borderless />
</t-form-item>
<t-form-item label="备注">
<t-textarea v-model.trim="formModel.remark" class="textarea" placeholder="备注" autosize borderless />
</t-form-item>
</t-form>
<div v-if="!isCreateMode" class="links">
<t-cell title="营业日管理" note="配置营业或休息日期" arrow hover @click="router.push(`/management/stores/${routeId}/business-days`)" />
</div>
<div class="actions">
<t-button v-if="!isCreateMode" v-can="permissionCodes.store.delete" theme="danger" :loading="isRemoving" @click="confirmRemoveStore">删除</t-button>
<t-button theme="primary" :loading="isSaving" @click="saveStore">保存</t-button>
</div>
<t-popup v-model="statusPickerVisible" placement="bottom">
<t-picker
v-model="statusPickerDraft"
:columns="statusPickerColumns"
title="门店状态"
@cancel="statusPickerVisible = false"
@confirm="confirmStatusPicker"
/>
</t-popup>
</div>
</template>
<script setup lang="ts">
import { DialogPlugin, Toast } from "tdesign-mobile-vue";
import StoreAPI from "@/api/StoreAPI";
import type { Store, StoreStatus } from "@/types/Store";
import { permissionCodes } from "@/utils/permission";
import { getStoreStatusText } from "@/utils/store";
type PickerValue = string | number;
defineOptions({
name: "StoreEdit"
});
const route = useRoute();
const router = useRouter();
const routeId = computed(() => String(route.params.id || ""));
// ---------- 路由权限 ----------
const isCreateMode = computed(() => !routeId.value);
const savePermissionCode = computed(() => isCreateMode.value ? permissionCodes.store.create : permissionCodes.store.update);
// ---------- 表单 ----------
const isSaving = ref(false);
const isRemoving = ref(false);
const formModel = reactive({
id: "",
code: "",
name: "",
status: "ACTIVE" as StoreStatus,
telephone: "",
address: "",
remark: "",
sortText: "0"
});
// ---------- 状态选择 ----------
const statusPickerVisible = ref(false);
const statusPickerDraft = ref<PickerValue[]>(["ACTIVE"]);
const statusPickerColumns = [
[
{ label: "启用", value: "ACTIVE" },
{ label: "停用", value: "INACTIVE" }
]
];
function openStatusPicker(): void {
statusPickerDraft.value = [formModel.status];
statusPickerVisible.value = true;
}
function confirmStatusPicker(): void {
const nextStatus = String(statusPickerDraft.value[0] || "ACTIVE") as StoreStatus;
formModel.status = nextStatus;
statusPickerVisible.value = false;
}
// ---------- 页面数据 ----------
async function fillForm(): Promise<void> {
if (!routeId.value) {
return;
}
const current = await StoreAPI.detail(routeId.value);
Object.assign(formModel, {
id: current.id || "",
code: current.code || "",
name: current.name || "",
status: current.status || "ACTIVE",
telephone: current.telephone || "",
address: current.address || "",
remark: current.remark || "",
sortText: String(current.sort ?? 0)
});
}
// ---------- 保存 ----------
async function saveStore(): Promise<void> {
if (!validateForm()) {
return;
}
isSaving.value = true;
try {
const store: Store = {
id: formModel.id || undefined,
code: formModel.code,
name: formModel.name,
status: formModel.status,
telephone: formModel.telephone || undefined,
address: formModel.address || undefined,
remark: formModel.remark || undefined,
sort: Number(formModel.sortText || 0)
};
const savedId = isCreateMode.value ? await StoreAPI.create(store) : formModel.id;
if (!isCreateMode.value) {
await StoreAPI.update(store);
}
Toast.success("门店已保存");
await router.replace(`/management/stores/${savedId}`);
} finally {
isSaving.value = false;
}
}
function validateForm(): boolean {
if (!formModel.code) {
Toast.warning("请填写门店编码");
return false;
}
if (!formModel.name) {
Toast.warning("请填写门店名称");
return false;
}
if (Number.isNaN(Number(formModel.sortText || 0))) {
Toast.warning("排序值必须是数字");
return false;
}
return true;
}
async function confirmRemoveStore(): Promise<void> {
if (!formModel.id) {
return;
}
const dialog = DialogPlugin.confirm({
title: "删除门店",
content: `确定删除 ${formModel.name} 吗?删除后无法恢复。`,
cancelBtn: "取消",
confirmBtn: "删除",
onCancel() {
dialog.destroy();
},
async onConfirm() {
dialog.destroy();
await removeStore();
}
});
}
async function removeStore(): Promise<void> {
if (isRemoving.value) {
return;
}
isRemoving.value = true;
try {
await StoreAPI.remove(formModel.id);
Toast.success("门店已删除");
await router.replace("/management/stores");
} finally {
isRemoving.value = false;
}
}
onMounted(async () => {
try {
await fillForm();
} catch {
Toast.error("门店数据加载失败");
}
});
</script>
<style scoped lang="less">
.store-edit {
padding: calc(var(--app-nav-height) + 1rem) 0 6rem 0;
.form {
border-radius: .75rem;
}
.textarea {
width: 100%;
}
.picker-btn {
gap: .5rem;
width: 100%;
border: 0;
display: flex;
padding: 0;
color: inherit;
background: transparent;
align-items: center;
justify-content: space-between;
font: inherit;
}
.links {
margin: 1rem 0;
}
.actions {
gap: .75rem;
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
display: grid;
position: fixed;
grid-template-columns: 1fr 1fr;
}
}
</style>
+211
View File
@@ -0,0 +1,211 @@
<template>
<div v-can="permissionCodes.store.read" class="store-list">
<t-pull-down-refresh
v-model="isRefreshing"
:refresh-timeout="30000"
@refresh="refresh"
@scrolltolower="fetch"
>
<div v-for="item in pageResult.list" :key="item.id" class="item" @click="toEdit(item)">
<div class="content">
<div class="header">
<span class="name" v-text="item.name || '未命名门店'" />
<t-tag size="small" :theme="getStoreStatusTheme(item.status)" variant="outline" :content="getStoreStatusText(item.status)" />
</div>
<span class="meta" v-text="`编码:${item.code || '-'}`" />
<span class="meta" v-text="item.telephone ? `电话:${item.telephone}` : '电话:-'" />
<span class="meta" v-text="item.address ? `地址:${item.address}` : '地址:-'" />
</div>
<t-icon name="chevron-right" size="1.25rem" />
</div>
<div v-if="!pageResult.list.length" class="empty-block" v-text="isLoading ? '正在加载门店' : '暂无门店'" />
<div v-if="pageResult.list.length" class="load-state" v-text="isNoMore ? '已经到底了' : '上拉加载更多'" />
</t-pull-down-refresh>
<div class="actions">
<t-button v-can="permissionCodes.store.export" theme="default" :loading="isExporting" @click="exportExcel">
<template #icon>
<t-icon name="download" />
</template>
导出
</t-button>
<t-button v-can="permissionCodes.store.create" theme="primary" @click="router.push('/management/stores/create')">
<template #icon>
<t-icon name="add" />
</template>
新增
</t-button>
</div>
</div>
</template>
<script setup lang="ts">
import { Toast } from "tdesign-mobile-vue";
import { Time, Toolkit, type Page, type PageResult } from "timi-web";
import StoreAPI from "@/api/StoreAPI";
import type { Store } from "@/types/Store";
import { permissionCodes } from "@/utils/permission";
import { getStoreStatusText, getStoreStatusTheme } from "@/utils/store";
defineOptions({
name: "StoreList"
});
const router = useRouter();
// ---------- 列表数据 ----------
const isRefreshing = ref(false);
const isLoading = ref(false);
const isNoMore = ref(false);
const page = reactive<Page<Store>>({
index: 0,
size: 20,
orderMap: {
sort: "ASC",
createdAt: "DESC"
}
});
const pageResult = reactive<PageResult<Store>>({
total: 0,
list: []
});
async function refresh(): Promise<void> {
isRefreshing.value = true;
try {
page.index = 0;
pageResult.total = 0;
pageResult.list = [];
isNoMore.value = false;
await fetch();
} finally {
isRefreshing.value = false;
}
}
async function fetch(): Promise<void> {
if (isLoading.value || isNoMore.value) {
return;
}
isLoading.value = true;
try {
const result = await StoreAPI.list(page);
pageResult.total = result.total || 0;
pageResult.list = pageResult.list.concat(result.list || []);
page.index += 1;
isNoMore.value = pageResult.list.length >= pageResult.total || (result.list || []).length < page.size;
} finally {
isLoading.value = false;
}
}
// ---------- 操作 ----------
const isExporting = ref(false);
async function exportExcel(): Promise<void> {
if (isExporting.value) {
return;
}
isExporting.value = true;
try {
const blob = await StoreAPI.exportXSSF({
index: 0,
size: 10000,
orderMap: page.orderMap
});
Toolkit.downloadBlob(blob, `门店列表-${Time.formatDate(new Date())}.xlsx`);
Toast.success("导出已开始");
} finally {
isExporting.value = false;
}
}
async function toEdit(item: Store): Promise<void> {
if (!item.id) {
return;
}
await router.push(`/management/stores/${item.id}`);
}
onMounted(async () => {
try {
await fetch();
} catch {
Toast.error("门店列表加载失败");
}
});
</script>
<style scoped lang="less">
.store-list {
gap: .75rem;
display: grid;
padding: calc(var(--app-nav-height) + 1rem) 1rem 6rem 1rem;
.item {
gap: .75rem;
display: flex;
padding: .75rem;
align-items: center;
border: 1px solid var(--td-border-level-1-color);
background: #fff;
border-radius: .75rem;
.content {
gap: .375rem;
flex: 1;
display: flex;
min-width: 0;
flex-direction: column;
}
.header {
gap: .5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.name {
font-size: 1rem;
font-weight: 700;
}
.meta {
color: #52607a;
font-size: .875rem;
}
}
.actions {
gap: .75rem;
left: 1rem;
right: 1rem;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
display: grid;
position: fixed;
grid-template-columns: 1fr 1fr;
}
}
:global(html.theme-dark) {
.store-list {
.item {
background: #1e293b;
.name {
color: #e2e8f0;
}
.meta {
color: #94a3b8;
}
}
}
}
</style>
+79
View File
@@ -22,6 +22,20 @@
</template>
</t-cell>
</t-cell-group>
<t-cell-group v-if="canSelectStore" class="store-card" theme="card">
<t-cell
class="cell"
title="当前门店"
:note="currentStoreStore.currentStoreName"
arrow
hover
@click="openStorePicker"
>
<template #leftIcon>
<t-icon name="shop" size="1.75rem" />
</template>
</t-cell>
</t-cell-group>
<t-cell-group v-for="group in visibleMenuGroupList" :key="group.title" :title="group.title" theme="card">
<t-cell
v-for="item in group.items"
@@ -42,14 +56,26 @@
<p>技术支持:夜雨</p>
<p>Copyright © 2026 imyeyu.com</p>
</footer>
<t-popup v-model="storePickerVisible" placement="bottom">
<t-picker
v-model="storePickerDraft"
:columns="storePickerColumns"
title="当前门店"
@cancel="storePickerVisible = false"
@confirm="confirmStorePicker"
/>
</t-popup>
</div>
</template>
<script setup lang="ts">
import { UserAPI, useUserStore } from "timi-web";
import { Toast } from "tdesign-mobile-vue";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { corePermissionCodes, coreRoleCodes, permissionCodes } from "@/utils/permission";
type UserAvatarSource = Parameters<typeof UserAPI.getAvatarURL>[0];
type PickerValue = string | number;
type MenuItem = {
icon: string;
@@ -69,6 +95,7 @@ defineOptions({
const router = useRouter();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const currentUser = computed(() => userStore.loginUser.user);
@@ -77,6 +104,7 @@ const currentUserName = computed(() => currentUser.value?.nick || currentUser.va
const currentUserDesc = computed(() => currentUser.value?.phoneNo || currentUser.value?.email || "暂无联系方式");
const currentUserAvatar = computed(() => buildUserAvatar(currentUser.value));
const canSelectStore = computed(() => userStore.hasPermissions(permissionCodes.store.read));
const canManagePermission = computed(() => (
userStore.hasRoles(coreRoleCodes.admin) || userStore.hasRoles(coreRoleCodes.userRoleAdmin)
) && userStore.hasPermissions([
@@ -90,10 +118,48 @@ const visibleMenuGroupList = computed<MenuGroup[]>(() => menuGroupList.value
}))
.filter(group => group.items.length));
// ---------- 当前门店 ----------
const storePickerVisible = ref(false);
const storePickerDraft = ref<PickerValue[]>([]);
const storePickerColumns = computed(() => [
currentStoreStore.storeList.map(item => ({
label: item.name || item.code || "未命名门店",
value: item.id || ""
})).filter(item => item.value)
]);
function openStorePicker(): void {
if (!currentStoreStore.storeList.length) {
Toast.warning("暂无可选门店");
return;
}
storePickerDraft.value = [currentStoreStore.currentStoreId];
storePickerVisible.value = true;
}
function confirmStorePicker(): void {
const storeId = String(storePickerDraft.value[0] || "");
if (!storeId) {
return;
}
currentStoreStore.selectStore(storeId);
storePickerVisible.value = false;
Toast.success("当前门店已切换");
}
const menuGroupList = computed<MenuGroup[]>(() => [
{
title: "客户",
items: [
{
icon: "shop",
path: "/management/stores",
title: "门店管理",
visible: userStore.hasPermissions(permissionCodes.store.read)
},
{
icon: "usergroup",
path: "/management/customers",
@@ -176,6 +242,12 @@ function buildUserAvatar(user?: UserAvatarSource): string {
async function go(path: string): Promise<void> {
await router.push(path);
}
onMounted(async () => {
if (canSelectStore.value) {
await currentStoreStore.loadStoreList();
}
});
</script>
<style scoped lang="less">
@@ -203,6 +275,13 @@ async function go(path: string): Promise<void> {
}
}
.store-card {
.cell {
padding: 10px;
}
}
.footer {
padding: 1.25rem 1rem calc(1rem + env(safe-area-inset-bottom, 0));
text-align: center;