add store
This commit is contained in:
+18
-10
@@ -1,10 +1,11 @@
|
||||
import { axios, type Page, type PageResult } from "timi-web";
|
||||
import type { Customer, CustomerDailyStatView, CustomerGenderStatView, CustomerTrendStatReq } from "@/types/Customer";
|
||||
import { getCurrentStoreId, withCurrentStoreId, withCurrentStorePage } from "@/utils/store";
|
||||
|
||||
const BASE_URI = "/gao/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> {
|
||||
@@ -14,11 +15,11 @@ async function detail(id: string): Promise<Customer> {
|
||||
}
|
||||
|
||||
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> {
|
||||
return await axios.post(`${BASE_URI}/update`, page);
|
||||
return await axios.post(`${BASE_URI}/update`, withCurrentStoreId(page));
|
||||
}
|
||||
|
||||
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> {
|
||||
return await axios.post(`${BASE_URI}/find/code`, null, {
|
||||
params: { code }
|
||||
params: {
|
||||
storeId: getCurrentStoreId(),
|
||||
code
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function findByName(name: string): Promise<Customer | undefined> {
|
||||
return await axios.post(`${BASE_URI}/find/name`, null, {
|
||||
params: { name }
|
||||
})
|
||||
params: {
|
||||
storeId: getCurrentStoreId(),
|
||||
name
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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"
|
||||
});
|
||||
}
|
||||
@@ -73,11 +80,13 @@ async function deleteInvite(customerId: string): Promise<void> {
|
||||
}
|
||||
|
||||
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[]> {
|
||||
return await axios.post(`${BASE_URI}/state/gender`);
|
||||
return await axios.post(`${BASE_URI}/state/gender`, undefined, {
|
||||
params: { storeId: getCurrentStoreId() }
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
@@ -96,4 +105,3 @@ export default {
|
||||
dailyStat,
|
||||
genderStat
|
||||
};
|
||||
|
||||
|
||||
+7
-4
@@ -1,14 +1,17 @@
|
||||
import { axios, type Page, type PageResult } from "timi-web";
|
||||
import type { EventItem } from "@/types/EventItem";
|
||||
import { getCurrentStoreId, withCurrentStoreId, withCurrentStorePage } from "@/utils/store";
|
||||
|
||||
const BASE_URI = "/gao/event";
|
||||
|
||||
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[]> {
|
||||
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> {
|
||||
@@ -18,11 +21,11 @@ async function detail(id: string): Promise<EventItem> {
|
||||
}
|
||||
|
||||
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> {
|
||||
return await axios.post(`${BASE_URI}/update`, event);
|
||||
return await axios.post(`${BASE_URI}/update`, withCurrentStoreId(event));
|
||||
}
|
||||
|
||||
async function sort(idList: string[]): Promise<void> {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { axios, type PageResult } from "timi-web";
|
||||
import type { CustomerEventRankView, EventRecord, EventRecordPage, EventRecordRankPage } from "@/types/EventRecord";
|
||||
import type { Customer } from "@/types/Customer.ts";
|
||||
import { withCurrentStoreId } from "@/utils/store";
|
||||
|
||||
const BASE_URI = "/gao/event/record";
|
||||
|
||||
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> {
|
||||
@@ -15,19 +16,19 @@ async function detail(id: string): Promise<EventRecord> {
|
||||
}
|
||||
|
||||
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> {
|
||||
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> {
|
||||
await axios.post(`${BASE_URI}/create/customer`, customer);
|
||||
await axios.post(`${BASE_URI}/create/customer`, withCurrentStoreId(customer));
|
||||
}
|
||||
|
||||
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> {
|
||||
@@ -37,11 +38,11 @@ async function remove(id: string): Promise<void> {
|
||||
}
|
||||
|
||||
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> {
|
||||
return await axios.post(`${BASE_URI}/export`, page, {
|
||||
return await axios.post(`${BASE_URI}/export`, withCurrentStoreId(page), {
|
||||
responseType: "blob"
|
||||
});
|
||||
}
|
||||
@@ -57,4 +58,3 @@ export default {
|
||||
remove,
|
||||
eventRank
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { axios, type Page, type PageResult, type Role } from "timi-web";
|
||||
import type { ManagedUser } from "@/types/ManagedUser";
|
||||
import { withCurrentStoreId, withCurrentStorePage } from "@/utils/store";
|
||||
|
||||
const BASE_URI = "/gao/user";
|
||||
|
||||
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> {
|
||||
@@ -14,11 +15,11 @@ async function detail(id: string): 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> {
|
||||
return await axios.post(`${BASE_URI}/update`, user);
|
||||
return await axios.post(`${BASE_URI}/update`, withCurrentStoreId(user));
|
||||
}
|
||||
|
||||
async function remove(id: string): Promise<void> {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
+2
-1
@@ -4,7 +4,7 @@ import MainLayout from "@/layout/MainLayout.vue";
|
||||
import Login from "@/pages/Login.vue";
|
||||
import chartRoutes from "@/router/routes/chartRoutes";
|
||||
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 registrationRoutes from "@/router/routes/registrationRoutes";
|
||||
import tabRoutes from "@/router/routes/tabRoutes";
|
||||
@@ -17,6 +17,7 @@ const detailRoutes = [
|
||||
...userRoutes,
|
||||
...registrationRoutes,
|
||||
...managementSettingRoutes,
|
||||
...storeRoutes,
|
||||
...permissionRoutes,
|
||||
...chartRoutes,
|
||||
...managementAccountRoutes
|
||||
|
||||
@@ -3,6 +3,68 @@ import ManagementSettings from "@/pages/management/ManagementSettings.vue";
|
||||
import ManagementProfile from "@/pages/management/ManagementProfile.vue";
|
||||
import ManagementPassword from "@/pages/management/ManagementPassword.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[] = [
|
||||
{
|
||||
@@ -67,6 +129,7 @@ export const managementAccountRoutes: RouteRecordRaw[] = [
|
||||
];
|
||||
|
||||
const managementRoutes: RouteRecordRaw[] = [
|
||||
...storeRoutes,
|
||||
...managementSettingRoutes,
|
||||
...managementAccountRoutes
|
||||
];
|
||||
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ export type BodyAreaValue = {
|
||||
};
|
||||
|
||||
export type Customer = {
|
||||
storeId?: string;
|
||||
code?: string;
|
||||
name?: string;
|
||||
gender?: CustomerGender;
|
||||
@@ -37,6 +38,7 @@ export type CustomerSearchReq = {
|
||||
};
|
||||
|
||||
export type CustomerTrendStatReq = {
|
||||
storeId?: string;
|
||||
beginCreatedAt: number;
|
||||
endCreatedAt: number;
|
||||
};
|
||||
|
||||
@@ -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 EventItem = {
|
||||
storeId?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
color?: string;
|
||||
@@ -17,4 +18,3 @@ export type EventItem = {
|
||||
sort?: number;
|
||||
roleCodeList?: string[];
|
||||
} & Model;
|
||||
|
||||
|
||||
@@ -3,12 +3,15 @@ import type { Customer } from "@/types/Customer";
|
||||
import type { EventItem } from "@/types/EventItem";
|
||||
|
||||
export type EventRecord = {
|
||||
storeId?: string;
|
||||
customerId?: string;
|
||||
customerIdList?: string[];
|
||||
eventId?: string;
|
||||
operatorUserId?: string;
|
||||
remark?: string;
|
||||
registeredAt?: number;
|
||||
registeredDateValue?: number;
|
||||
businessDayNo?: number;
|
||||
|
||||
customer?: Customer;
|
||||
event?: EventItem;
|
||||
@@ -27,6 +30,7 @@ export type EventRecordListItemView = {
|
||||
};
|
||||
|
||||
export type EventRecordPage = {
|
||||
storeId?: string;
|
||||
customerId?: string;
|
||||
eventIdList?: string[];
|
||||
keyword?: string;
|
||||
@@ -35,6 +39,7 @@ export type EventRecordPage = {
|
||||
} & BasePage;
|
||||
|
||||
export type EventRecordRankPage = {
|
||||
storeId?: string;
|
||||
customerId?: string;
|
||||
eventIdList?: string[];
|
||||
beginAt?: number;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Model, Role, User } from "timi-web";
|
||||
|
||||
export type ManagedUser = {
|
||||
storeId?: string;
|
||||
userId?: string;
|
||||
enabled?: boolean;
|
||||
remark?: string;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
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: {
|
||||
create: "GAO:CUSTOMER:CREATE",
|
||||
read: "GAO:CUSTOMER:READ",
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user