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
+18 -10
View File
@@ -1,10 +1,11 @@
import { axios, type Page, type PageResult } from "timi-web"; import { axios, type Page, type PageResult } from "timi-web";
import type { Customer, CustomerDailyStatView, CustomerGenderStatView, CustomerTrendStatReq } from "@/types/Customer"; import type { Customer, CustomerDailyStatView, CustomerGenderStatView, CustomerTrendStatReq } from "@/types/Customer";
import { getCurrentStoreId, withCurrentStoreId, withCurrentStorePage } from "@/utils/store";
const BASE_URI = "/gao/customer"; const BASE_URI = "/gao/customer";
async function list(page: Page<Customer>): Promise<PageResult<Customer>> { async function list(page: Page<Customer>): Promise<PageResult<Customer>> {
return await axios.post(`${BASE_URI}/list`, page); return await axios.post(`${BASE_URI}/list`, withCurrentStorePage(page));
} }
async function detail(id: string): Promise<Customer> { async function detail(id: string): Promise<Customer> {
@@ -14,11 +15,11 @@ async function detail(id: string): Promise<Customer> {
} }
async function create(customer: Customer): Promise<string> { async function create(customer: Customer): Promise<string> {
return await axios.post(`${BASE_URI}/create`, customer); return await axios.post(`${BASE_URI}/create`, withCurrentStoreId(customer));
} }
async function update(page: Customer): Promise<void> { async function update(page: Customer): Promise<void> {
return await axios.post(`${BASE_URI}/update`, page); return await axios.post(`${BASE_URI}/update`, withCurrentStoreId(page));
} }
async function remove(id: string): Promise<void> { async function remove(id: string): Promise<void> {
@@ -35,18 +36,24 @@ async function findById(id: string): Promise<Customer | undefined> {
async function findByCode(code: string): Promise<Customer | undefined> { async function findByCode(code: string): Promise<Customer | undefined> {
return await axios.post(`${BASE_URI}/find/code`, null, { return await axios.post(`${BASE_URI}/find/code`, null, {
params: { code } params: {
storeId: getCurrentStoreId(),
code
}
}); });
} }
async function findByName(name: string): Promise<Customer | undefined> { async function findByName(name: string): Promise<Customer | undefined> {
return await axios.post(`${BASE_URI}/find/name`, null, { return await axios.post(`${BASE_URI}/find/name`, null, {
params: { name } params: {
}) storeId: getCurrentStoreId(),
name
}
});
} }
async function exportXSSF(page: Page<Customer>): Promise<Blob> { async function exportXSSF(page: Page<Customer>): Promise<Blob> {
return await axios.post(`${BASE_URI}/export`, page, { return await axios.post(`${BASE_URI}/export`, withCurrentStorePage(page), {
responseType: "blob" responseType: "blob"
}); });
} }
@@ -73,11 +80,13 @@ async function deleteInvite(customerId: string): Promise<void> {
} }
async function dailyStat(req: CustomerTrendStatReq): Promise<CustomerDailyStatView[]> { async function dailyStat(req: CustomerTrendStatReq): Promise<CustomerDailyStatView[]> {
return await axios.post(`${BASE_URI}/state/daily`, req); return await axios.post(`${BASE_URI}/state/daily`, withCurrentStoreId(req));
} }
async function genderStat(): Promise<CustomerGenderStatView[]> { async function genderStat(): Promise<CustomerGenderStatView[]> {
return await axios.post(`${BASE_URI}/state/gender`); return await axios.post(`${BASE_URI}/state/gender`, undefined, {
params: { storeId: getCurrentStoreId() }
});
} }
export default { export default {
@@ -96,4 +105,3 @@ export default {
dailyStat, dailyStat,
genderStat genderStat
}; };
+7 -4
View File
@@ -1,14 +1,17 @@
import { axios, type Page, type PageResult } from "timi-web"; import { axios, type Page, type PageResult } from "timi-web";
import type { EventItem } from "@/types/EventItem"; import type { EventItem } from "@/types/EventItem";
import { getCurrentStoreId, withCurrentStoreId, withCurrentStorePage } from "@/utils/store";
const BASE_URI = "/gao/event"; const BASE_URI = "/gao/event";
async function list(req: Page<EventItem>): Promise<PageResult<EventItem>> { async function list(req: Page<EventItem>): Promise<PageResult<EventItem>> {
return await axios.post(`${BASE_URI}/list`, req); return await axios.post(`${BASE_URI}/list`, withCurrentStorePage(req));
} }
async function listValid(): Promise<EventItem[]> { async function listValid(): Promise<EventItem[]> {
return await axios.post(`${BASE_URI}/list/valid`); return await axios.post(`${BASE_URI}/list/valid`, undefined, {
params: { storeId: getCurrentStoreId() }
});
} }
async function detail(id: string): Promise<EventItem> { async function detail(id: string): Promise<EventItem> {
@@ -18,11 +21,11 @@ async function detail(id: string): Promise<EventItem> {
} }
async function create(event: EventItem): Promise<void> { async function create(event: EventItem): Promise<void> {
return await axios.post(`${BASE_URI}/create`, event); return await axios.post(`${BASE_URI}/create`, withCurrentStoreId(event));
} }
async function update(event: EventItem): Promise<void> { async function update(event: EventItem): Promise<void> {
return await axios.post(`${BASE_URI}/update`, event); return await axios.post(`${BASE_URI}/update`, withCurrentStoreId(event));
} }
async function sort(idList: string[]): Promise<void> { async function sort(idList: string[]): Promise<void> {
+8 -8
View File
@@ -1,11 +1,12 @@
import { axios, type PageResult } from "timi-web"; import { axios, type PageResult } from "timi-web";
import type { CustomerEventRankView, EventRecord, EventRecordPage, EventRecordRankPage } from "@/types/EventRecord"; import type { CustomerEventRankView, EventRecord, EventRecordPage, EventRecordRankPage } from "@/types/EventRecord";
import type { Customer } from "@/types/Customer.ts"; import type { Customer } from "@/types/Customer.ts";
import { withCurrentStoreId } from "@/utils/store";
const BASE_URI = "/gao/event/record"; const BASE_URI = "/gao/event/record";
async function list(page: EventRecordPage): Promise<PageResult<EventRecord>> { async function list(page: EventRecordPage): Promise<PageResult<EventRecord>> {
return await axios.post(`${BASE_URI}/list`, page); return await axios.post(`${BASE_URI}/list`, withCurrentStoreId(page));
} }
async function detail(id: string): Promise<EventRecord> { async function detail(id: string): Promise<EventRecord> {
@@ -15,19 +16,19 @@ async function detail(id: string): Promise<EventRecord> {
} }
async function create(record: EventRecord): Promise<void> { async function create(record: EventRecord): Promise<void> {
await axios.post(`${BASE_URI}/create`, record); await axios.post(`${BASE_URI}/create`, withCurrentStoreId(record));
} }
async function createBatch(recordList: EventRecord[]): Promise<void> { async function createBatch(recordList: EventRecord[]): Promise<void> {
await axios.post(`${BASE_URI}/create/batch`, recordList); await axios.post(`${BASE_URI}/create/batch`, recordList.map(record => withCurrentStoreId(record)));
} }
async function createWithCustomer(customer: Customer): Promise<void> { async function createWithCustomer(customer: Customer): Promise<void> {
await axios.post(`${BASE_URI}/create/customer`, customer); await axios.post(`${BASE_URI}/create/customer`, withCurrentStoreId(customer));
} }
async function update(record: EventRecord): Promise<void> { async function update(record: EventRecord): Promise<void> {
await axios.post(`${BASE_URI}/update`, record); await axios.post(`${BASE_URI}/update`, withCurrentStoreId(record));
} }
async function remove(id: string): Promise<void> { async function remove(id: string): Promise<void> {
@@ -37,11 +38,11 @@ async function remove(id: string): Promise<void> {
} }
async function eventRank(page: EventRecordRankPage): Promise<CustomerEventRankView> { async function eventRank(page: EventRecordRankPage): Promise<CustomerEventRankView> {
return await axios.post(`${BASE_URI}/rank`, page); return await axios.post(`${BASE_URI}/rank`, withCurrentStoreId(page));
} }
async function exportXSSF(page: EventRecordPage): Promise<Blob> { async function exportXSSF(page: EventRecordPage): Promise<Blob> {
return await axios.post(`${BASE_URI}/export`, page, { return await axios.post(`${BASE_URI}/export`, withCurrentStoreId(page), {
responseType: "blob" responseType: "blob"
}); });
} }
@@ -57,4 +58,3 @@ export default {
remove, remove,
eventRank eventRank
}; };
+4 -3
View File
@@ -1,10 +1,11 @@
import { axios, type Page, type PageResult, type Role } from "timi-web"; import { axios, type Page, type PageResult, type Role } from "timi-web";
import type { ManagedUser } from "@/types/ManagedUser"; import type { ManagedUser } from "@/types/ManagedUser";
import { withCurrentStoreId, withCurrentStorePage } from "@/utils/store";
const BASE_URI = "/gao/user"; const BASE_URI = "/gao/user";
async function list(page: Page<ManagedUser>): Promise<PageResult<ManagedUser>> { async function list(page: Page<ManagedUser>): Promise<PageResult<ManagedUser>> {
return await axios.post(`${BASE_URI}/list`, page); return await axios.post(`${BASE_URI}/list`, withCurrentStorePage(page));
} }
async function detail(id: string): Promise<ManagedUser> { async function detail(id: string): Promise<ManagedUser> {
@@ -14,11 +15,11 @@ async function detail(id: string): Promise<ManagedUser> {
} }
async function create(user: ManagedUser): Promise<ManagedUser> { async function create(user: ManagedUser): Promise<ManagedUser> {
return await axios.post(`${BASE_URI}/create`, user); return await axios.post(`${BASE_URI}/create`, withCurrentStoreId(user));
} }
async function update(user: ManagedUser): Promise<ManagedUser> { async function update(user: ManagedUser): Promise<ManagedUser> {
return await axios.post(`${BASE_URI}/update`, user); return await axios.post(`${BASE_URI}/update`, withCurrentStoreId(user));
} }
async function remove(id: string): Promise<void> { async function remove(id: string): Promise<void> {
+85
View File
@@ -0,0 +1,85 @@
import { axios, type Page, type PageResult } from "timi-web";
import type { Store, StoreBusinessDay } from "@/types/Store";
const STORE_BASE_URI = "/gao/store";
const BUSINESS_DAY_BASE_URI = "/gao/store/business/day";
async function list(page: Page<Store>): Promise<PageResult<Store>> {
return await axios.post(`${STORE_BASE_URI}/list`, page);
}
async function detail(id: string): Promise<Store> {
return await axios.post(`${STORE_BASE_URI}/detail`, undefined, {
params: { id }
});
}
async function create(store: Store): Promise<string> {
return await axios.post(`${STORE_BASE_URI}/create`, store);
}
async function update(store: Store): Promise<void> {
await axios.post(`${STORE_BASE_URI}/update`, store);
}
async function remove(id: string): Promise<void> {
await axios.post(`${STORE_BASE_URI}/delete`, undefined, {
params: { id }
});
}
async function exportXSSF(page: Page<Store>): Promise<Blob> {
return await axios.post(`${STORE_BASE_URI}/export`, page, {
responseType: "blob"
});
}
async function businessDayList(page: Page<StoreBusinessDay>): Promise<PageResult<StoreBusinessDay>> {
return await axios.post(`${BUSINESS_DAY_BASE_URI}/list`, page);
}
async function businessDayCalendar(storeId: string, beginDateValue: number, endDateValue: number): Promise<StoreBusinessDay[]> {
return await axios.post(`${BUSINESS_DAY_BASE_URI}/calendar`, undefined, {
params: {
storeId,
beginDateValue,
endDateValue
}
});
}
async function businessDayDetail(id: string): Promise<StoreBusinessDay> {
return await axios.post(`${BUSINESS_DAY_BASE_URI}/detail`, undefined, {
params: { id }
});
}
async function createBusinessDay(day: StoreBusinessDay): Promise<string> {
return await axios.post(`${BUSINESS_DAY_BASE_URI}/create`, day);
}
async function updateBusinessDay(day: StoreBusinessDay): Promise<void> {
await axios.post(`${BUSINESS_DAY_BASE_URI}/update`, day);
}
async function removeBusinessDay(id: string): Promise<void> {
await axios.post(`${BUSINESS_DAY_BASE_URI}/delete`, undefined, {
params: { id }
});
}
export default {
list,
detail,
create,
update,
remove,
exportXSSF,
businessDayList,
businessDayCalendar,
businessDayDetail,
createBusinessDay,
updateBusinessDay,
removeBusinessDay
};
+1 -2
View File
@@ -190,7 +190,7 @@ async function doSubmit(): Promise<void> {
customerId: customer.id, customerId: customer.id,
eventId: selectedEventId.value, eventId: selectedEventId.value,
remark: "客户快速扫码登记" remark: "客户快速扫码登记"
}) });
scanStatus.value = `登记成功:${customer.name || customer.code || scanCode.value}。`; scanStatus.value = `登记成功:${customer.name || customer.code || scanCode.value}。`;
scanCode.value = ""; scanCode.value = "";
matchedCustomer.value = undefined; matchedCustomer.value = undefined;
@@ -362,4 +362,3 @@ async function doSubmit(): Promise<void> {
} }
} }
</style> </style>
+1 -2
View File
@@ -124,7 +124,7 @@ const fetch = Toolkit.debounce(async () => {
likesExample: { likesExample: {
code: keyword.value, code: keyword.value,
name: keyword.value, name: keyword.value,
telephone: keyword.value, telephone: keyword.value
} }
}); });
pageResult.total = result.total; pageResult.total = result.total;
@@ -336,4 +336,3 @@ async function doExport(): Promise<void> {
} }
} }
</style> </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> </template>
</t-cell> </t-cell>
</t-cell-group> </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-group v-for="group in visibleMenuGroupList" :key="group.title" :title="group.title" theme="card">
<t-cell <t-cell
v-for="item in group.items" v-for="item in group.items"
@@ -42,14 +56,26 @@
<p>技术支持:夜雨</p> <p>技术支持:夜雨</p>
<p>Copyright © 2026 imyeyu.com</p> <p>Copyright © 2026 imyeyu.com</p>
</footer> </footer>
<t-popup v-model="storePickerVisible" placement="bottom">
<t-picker
v-model="storePickerDraft"
:columns="storePickerColumns"
title="当前门店"
@cancel="storePickerVisible = false"
@confirm="confirmStorePicker"
/>
</t-popup>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { UserAPI, useUserStore } from "timi-web"; import { UserAPI, useUserStore } from "timi-web";
import { Toast } from "tdesign-mobile-vue";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { corePermissionCodes, coreRoleCodes, permissionCodes } from "@/utils/permission"; import { corePermissionCodes, coreRoleCodes, permissionCodes } from "@/utils/permission";
type UserAvatarSource = Parameters<typeof UserAPI.getAvatarURL>[0]; type UserAvatarSource = Parameters<typeof UserAPI.getAvatarURL>[0];
type PickerValue = string | number;
type MenuItem = { type MenuItem = {
icon: string; icon: string;
@@ -69,6 +95,7 @@ defineOptions({
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const currentUser = computed(() => userStore.loginUser.user); 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 currentUserDesc = computed(() => currentUser.value?.phoneNo || currentUser.value?.email || "暂无联系方式");
const currentUserAvatar = computed(() => buildUserAvatar(currentUser.value)); const currentUserAvatar = computed(() => buildUserAvatar(currentUser.value));
const canSelectStore = computed(() => userStore.hasPermissions(permissionCodes.store.read));
const canManagePermission = computed(() => ( const canManagePermission = computed(() => (
userStore.hasRoles(coreRoleCodes.admin) || userStore.hasRoles(coreRoleCodes.userRoleAdmin) userStore.hasRoles(coreRoleCodes.admin) || userStore.hasRoles(coreRoleCodes.userRoleAdmin)
) && userStore.hasPermissions([ ) && userStore.hasPermissions([
@@ -90,10 +118,48 @@ const visibleMenuGroupList = computed<MenuGroup[]>(() => menuGroupList.value
})) }))
.filter(group => group.items.length)); .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[]>(() => [ const menuGroupList = computed<MenuGroup[]>(() => [
{ {
title: "客户", title: "客户",
items: [ items: [
{
icon: "shop",
path: "/management/stores",
title: "门店管理",
visible: userStore.hasPermissions(permissionCodes.store.read)
},
{ {
icon: "usergroup", icon: "usergroup",
path: "/management/customers", path: "/management/customers",
@@ -176,6 +242,12 @@ function buildUserAvatar(user?: UserAvatarSource): string {
async function go(path: string): Promise<void> { async function go(path: string): Promise<void> {
await router.push(path); await router.push(path);
} }
onMounted(async () => {
if (canSelectStore.value) {
await currentStoreStore.loadStoreList();
}
});
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
@@ -203,6 +275,13 @@ async function go(path: string): Promise<void> {
} }
} }
.store-card {
.cell {
padding: 10px;
}
}
.footer { .footer {
padding: 1.25rem 1rem calc(1rem + env(safe-area-inset-bottom, 0)); padding: 1.25rem 1rem calc(1rem + env(safe-area-inset-bottom, 0));
text-align: center; text-align: center;
+2 -1
View File
@@ -4,7 +4,7 @@ import MainLayout from "@/layout/MainLayout.vue";
import Login from "@/pages/Login.vue"; import Login from "@/pages/Login.vue";
import chartRoutes from "@/router/routes/chartRoutes"; import chartRoutes from "@/router/routes/chartRoutes";
import customerRoutes from "@/router/routes/customerRoutes"; import customerRoutes from "@/router/routes/customerRoutes";
import { managementAccountRoutes, managementSettingRoutes } from "@/router/routes/managementRoutes"; import { managementAccountRoutes, managementSettingRoutes, storeRoutes } from "@/router/routes/managementRoutes";
import permissionRoutes from "@/router/routes/permissionRoutes"; import permissionRoutes from "@/router/routes/permissionRoutes";
import registrationRoutes from "@/router/routes/registrationRoutes"; import registrationRoutes from "@/router/routes/registrationRoutes";
import tabRoutes from "@/router/routes/tabRoutes"; import tabRoutes from "@/router/routes/tabRoutes";
@@ -17,6 +17,7 @@ const detailRoutes = [
...userRoutes, ...userRoutes,
...registrationRoutes, ...registrationRoutes,
...managementSettingRoutes, ...managementSettingRoutes,
...storeRoutes,
...permissionRoutes, ...permissionRoutes,
...chartRoutes, ...chartRoutes,
...managementAccountRoutes ...managementAccountRoutes
+63
View File
@@ -3,6 +3,68 @@ import ManagementSettings from "@/pages/management/ManagementSettings.vue";
import ManagementProfile from "@/pages/management/ManagementProfile.vue"; import ManagementProfile from "@/pages/management/ManagementProfile.vue";
import ManagementPassword from "@/pages/management/ManagementPassword.vue"; import ManagementPassword from "@/pages/management/ManagementPassword.vue";
import ManagementQrCode from "@/pages/management/ManagementQrCode.vue"; import ManagementQrCode from "@/pages/management/ManagementQrCode.vue";
import StoreBusinessDayList from "@/pages/management/StoreBusinessDayList.vue";
import StoreEdit from "@/pages/management/StoreEdit.vue";
import StoreList from "@/pages/management/StoreList.vue";
export const storeRoutes: RouteRecordRaw[] = [
{
path: "/management/stores",
name: "StoreList",
component: StoreList,
meta: {
depth: 3,
navBarVisible: true,
navBarCanBack: true,
navBarTitle: "门店管理",
tabBarVisible: false,
bodyBackground: "#F5F5F8",
darkBodyBackground: "#10161f"
}
},
{
path: "/management/stores/create",
name: "StoreCreate",
component: StoreEdit,
meta: {
depth: 4,
navBarVisible: true,
navBarCanBack: true,
navBarTitle: "新增门店",
tabBarVisible: false,
bodyBackground: "#F5F5F8",
darkBodyBackground: "#10161f"
}
},
{
path: "/management/stores/:id",
name: "StoreEdit",
component: StoreEdit,
meta: {
depth: 4,
navBarVisible: true,
navBarCanBack: true,
navBarTitle: "编辑门店",
tabBarVisible: false,
bodyBackground: "#F5F5F8",
darkBodyBackground: "#10161f"
}
},
{
path: "/management/stores/:id/business-days",
name: "StoreBusinessDayList",
component: StoreBusinessDayList,
meta: {
depth: 5,
navBarVisible: true,
navBarCanBack: true,
navBarTitle: "营业日管理",
tabBarVisible: false,
bodyBackground: "#F5F5F8",
darkBodyBackground: "#10161f"
}
}
];
export const managementSettingRoutes: RouteRecordRaw[] = [ export const managementSettingRoutes: RouteRecordRaw[] = [
{ {
@@ -67,6 +129,7 @@ export const managementAccountRoutes: RouteRecordRaw[] = [
]; ];
const managementRoutes: RouteRecordRaw[] = [ const managementRoutes: RouteRecordRaw[] = [
...storeRoutes,
...managementSettingRoutes, ...managementSettingRoutes,
...managementAccountRoutes ...managementAccountRoutes
]; ];
+73
View File
@@ -0,0 +1,73 @@
import { Toast } from "tdesign-mobile-vue";
import StoreAPI from "@/api/StoreAPI";
import type { Store } from "@/types/Store";
import { getCurrentStoreId, readStoredCurrentStoreId, setCurrentStoreId } from "@/utils/store";
export const useCurrentStoreStore = defineStore("currentStore", () => {
const storeList = ref<Store[]>([]);
const currentStoreId = ref(readStoredCurrentStoreId());
const isLoading = ref(false);
const currentStore = computed(() => storeList.value.find(item => item.id === currentStoreId.value));
const currentStoreName = computed(() => currentStore.value?.name || "默认门店");
async function loadStoreList(): Promise<void> {
if (isLoading.value) {
return;
}
isLoading.value = true;
try {
const result = await StoreAPI.list({
index: 0,
size: 1000,
equalsExample: {
status: "ACTIVE"
},
orderMap: {
sort: "ASC",
createdAt: "DESC"
}
});
storeList.value = result.list || [];
normalizeCurrentStoreId();
} catch {
Toast.error("门店列表加载失败");
} finally {
isLoading.value = false;
}
}
function selectStore(storeId: string): void {
if (!storeId) {
return;
}
currentStoreId.value = storeId;
setCurrentStoreId(storeId);
}
function normalizeCurrentStoreId(): void {
const storedStoreId = getCurrentStoreId();
if (storeList.value.some(item => item.id === storedStoreId)) {
selectStore(storedStoreId);
return;
}
const firstStoreId = storeList.value[0]?.id;
if (firstStoreId) {
selectStore(firstStoreId);
}
}
return {
storeList,
currentStoreId,
currentStore,
currentStoreName,
isLoading,
loadStoreList,
selectStore
};
});
+2
View File
@@ -9,6 +9,7 @@ export type BodyAreaValue = {
}; };
export type Customer = { export type Customer = {
storeId?: string;
code?: string; code?: string;
name?: string; name?: string;
gender?: CustomerGender; gender?: CustomerGender;
@@ -37,6 +38,7 @@ export type CustomerSearchReq = {
}; };
export type CustomerTrendStatReq = { export type CustomerTrendStatReq = {
storeId?: string;
beginCreatedAt: number; beginCreatedAt: number;
endCreatedAt: number; endCreatedAt: number;
}; };
+1 -1
View File
@@ -5,6 +5,7 @@ export type EventStatus = "DRAFT" | "ACTIVE" | "INACTIVE" | "DELETED";
export type EventLimitType = "NONE" | "DAILY_HALF_DAY" | "NATURAL_DAY" | "INTERVAL_HOUR" | "NATURAL_MONTH" | "TOTAL_LIMIT" | "CUSTOMER_TOTAL_LIMIT"; export type EventLimitType = "NONE" | "DAILY_HALF_DAY" | "NATURAL_DAY" | "INTERVAL_HOUR" | "NATURAL_MONTH" | "TOTAL_LIMIT" | "CUSTOMER_TOTAL_LIMIT";
export type EventItem = { export type EventItem = {
storeId?: string;
name: string; name: string;
description?: string; description?: string;
color?: string; color?: string;
@@ -17,4 +18,3 @@ export type EventItem = {
sort?: number; sort?: number;
roleCodeList?: string[]; roleCodeList?: string[];
} & Model; } & Model;
+5
View File
@@ -3,12 +3,15 @@ import type { Customer } from "@/types/Customer";
import type { EventItem } from "@/types/EventItem"; import type { EventItem } from "@/types/EventItem";
export type EventRecord = { export type EventRecord = {
storeId?: string;
customerId?: string; customerId?: string;
customerIdList?: string[]; customerIdList?: string[];
eventId?: string; eventId?: string;
operatorUserId?: string; operatorUserId?: string;
remark?: string; remark?: string;
registeredAt?: number; registeredAt?: number;
registeredDateValue?: number;
businessDayNo?: number;
customer?: Customer; customer?: Customer;
event?: EventItem; event?: EventItem;
@@ -27,6 +30,7 @@ export type EventRecordListItemView = {
}; };
export type EventRecordPage = { export type EventRecordPage = {
storeId?: string;
customerId?: string; customerId?: string;
eventIdList?: string[]; eventIdList?: string[];
keyword?: string; keyword?: string;
@@ -35,6 +39,7 @@ export type EventRecordPage = {
} & BasePage; } & BasePage;
export type EventRecordRankPage = { export type EventRecordRankPage = {
storeId?: string;
customerId?: string; customerId?: string;
eventIdList?: string[]; eventIdList?: string[];
beginAt?: number; beginAt?: number;
+1
View File
@@ -1,6 +1,7 @@
import type { Model, Role, User } from "timi-web"; import type { Model, Role, User } from "timi-web";
export type ManagedUser = { export type ManagedUser = {
storeId?: string;
userId?: string; userId?: string;
enabled?: boolean; enabled?: boolean;
remark?: string; remark?: string;
+29
View File
@@ -0,0 +1,29 @@
import type { BasePage, Model } from "timi-web";
export type StoreStatus = "ACTIVE" | "INACTIVE" | "DELETED";
export type StoreBusinessDayOpenStatus = "OPEN" | "CLOSED";
export type Store = {
code?: string;
name?: string;
status?: StoreStatus;
telephone?: string;
address?: string;
remark?: string;
sort?: number;
} & Model;
export type StoreBusinessDay = {
storeId?: string;
dateValue?: number;
openStatus?: StoreBusinessDayOpenStatus;
businessDayNo?: number;
reason?: string;
} & Model;
export type StoreBusinessDayPage = {
storeId?: string;
beginDateValue?: number;
endDateValue?: number;
} & BasePage;
+7
View File
@@ -1,4 +1,11 @@
export const permissionCodes = { export const permissionCodes = {
store: {
create: "GAO:STORE:CREATE",
read: "GAO:STORE:READ",
export: "GAO:STORE:EXPORT",
update: "GAO:STORE:UPDATE",
delete: "GAO:STORE:DELETE"
},
customer: { customer: {
create: "GAO:CUSTOMER:CREATE", create: "GAO:CUSTOMER:CREATE",
read: "GAO:CUSTOMER:READ", read: "GAO:CUSTOMER:READ",
+95
View File
@@ -0,0 +1,95 @@
import type { StoreBusinessDayOpenStatus, StoreStatus } from "@/types/Store";
import { permissionCodes } from "@/utils/permission";
import { type Page, useUserStore } from "timi-web";
export const defaultStoreId = "00000000-0000-0000-0000-000000000001";
export const currentStoreIdStorageKey = "gao-customer-h5:current-store-id";
type LoginUserWithStore = {
storeId?: string;
user?: {
storeId?: string;
};
managedUser?: {
storeId?: string;
};
gaoUser?: {
storeId?: string;
};
};
export function getCurrentStoreId(): string {
const userStore = useUserStore();
if (userStore.hasPermissions(permissionCodes.store.read)) {
return readStoredCurrentStoreId() || getLoginUserStoreId() || defaultStoreId;
}
return getLoginUserStoreId() || defaultStoreId;
}
export function setCurrentStoreId(storeId: string): void {
localStorage.setItem(currentStoreIdStorageKey, storeId);
}
export function readStoredCurrentStoreId(): string {
return localStorage.getItem(currentStoreIdStorageKey) || "";
}
export function withCurrentStoreId<T extends { storeId?: string }>(data: T): T {
return {
...data,
storeId: data.storeId || getCurrentStoreId()
};
}
export function withCurrentStorePage<T extends { storeId?: string }>(page: Page<T>): Page<T> {
return {
...page,
equalsExample: withCurrentStoreId(page.equalsExample || {} as Partial<T>) as Partial<T>
};
}
function getLoginUserStoreId(): string {
const userStore = useUserStore();
const loginUser = userStore.loginUser as LoginUserWithStore;
return loginUser.storeId || loginUser.user?.storeId || loginUser.managedUser?.storeId || loginUser.gaoUser?.storeId || "";
}
export function getStoreStatusText(status?: StoreStatus): string {
const map: Record<StoreStatus, string> = {
ACTIVE: "启用",
INACTIVE: "停用",
DELETED: "已删除"
};
return status ? map[status] : "未知";
}
export function getStoreStatusTheme(status?: StoreStatus): "success" | "warning" | "danger" | "default" {
if (status === "ACTIVE") {
return "success";
}
if (status === "INACTIVE") {
return "warning";
}
if (status === "DELETED") {
return "danger";
}
return "default";
}
export function getBusinessDayStatusText(status?: StoreBusinessDayOpenStatus): string {
const map: Record<StoreBusinessDayOpenStatus, string> = {
OPEN: "营业",
CLOSED: "休息"
};
return status ? map[status] : "未配置";
}
export function getBusinessDayStatusTheme(status?: StoreBusinessDayOpenStatus): "success" | "warning" | "default" {
if (status === "OPEN") {
return "success";
}
if (status === "CLOSED") {
return "warning";
}
return "default";
}