diff --git a/src/api/PointAPI.ts b/src/api/PointAPI.ts new file mode 100644 index 0000000..262b42f --- /dev/null +++ b/src/api/PointAPI.ts @@ -0,0 +1,96 @@ +import { axios, type Page, type PageResult } from "timi-web"; +import type { + PointAccount, + PointAdjustRequest, + PointLedger, + PointLedgerPage, + PointRule +} from "@/types/Point"; +import { getCurrentStoreId, withCurrentStoreId, withCurrentStorePage } from "@/utils/store"; + +const BASE_URI = "/gao/customer/point"; + +type ListOptions = { + allStores?: boolean; + storeId?: string; +}; + +/** 查询指定客户的积分账户。账户不存在表示客户尚未产生过积分流水。 */ +async function accountDetail(customerId: string, storeId?: string): Promise { + return await axios.post(`${BASE_URI}/account/detail`, undefined, { + params: { + customerId, + storeId: storeId || getCurrentStoreId() || undefined + } + }); +} + +/** 分页查询积分流水。全局管理员不传门店时允许查询全部门店。 */ +async function ledgerList(page: PointLedgerPage, options?: ListOptions): Promise> { + if (options?.allStores) { + return await axios.post(`${BASE_URI}/ledger/list`, page); + } + + if (options?.storeId) { + return await axios.post(`${BASE_URI}/ledger/list`, { + ...page, + storeId: options.storeId + }); + } + + return await axios.post(`${BASE_URI}/ledger/list`, withCurrentStoreId(page)); +} + +/** 人工调整积分,后端使用 adjustRequestId 保证幂等。 */ +async function adjust(request: PointAdjustRequest): Promise { + await axios.post(`${BASE_URI}/adjust`, withCurrentStoreId(request)); +} + +async function ruleList(page: Page, options?: ListOptions): Promise> { + if (options?.allStores) { + return await axios.post(`${BASE_URI}/rule/list`, page); + } + + if (options?.storeId) { + return await axios.post(`${BASE_URI}/rule/list`, { + ...page, + equalsExample: { + ...(page.equalsExample || {}), + storeId: options.storeId + } + }); + } + + return await axios.post(`${BASE_URI}/rule/list`, withCurrentStorePage(page)); +} + +async function ruleDetail(id: string): Promise { + return await axios.post(`${BASE_URI}/rule/detail`, undefined, { + params: { id } + }); +} + +async function ruleCreate(rule: PointRule): Promise { + return await axios.post(`${BASE_URI}/rule/create`, withCurrentStoreId(rule)); +} + +async function ruleUpdate(rule: PointRule): Promise { + await axios.post(`${BASE_URI}/rule/update`, withCurrentStoreId(rule)); +} + +async function ruleRemove(id: string): Promise { + await axios.post(`${BASE_URI}/rule/delete`, undefined, { + params: { id } + }); +} + +export default { + accountDetail, + ledgerList, + adjust, + ruleList, + ruleDetail, + ruleCreate, + ruleUpdate, + ruleRemove +}; diff --git a/src/components/EventSelector.vue b/src/components/EventSelector.vue index 4717065..252e561 100644 --- a/src/components/EventSelector.vue +++ b/src/components/EventSelector.vue @@ -6,6 +6,7 @@ v-for="item in eventList" :key="item.id" :value="item.id" + :disabled="disabled" class="item" borderless > @@ -23,11 +24,13 @@ interface Props { eventList: EventItem[]; label?: string; requiredMark?: boolean; + disabled?: boolean; } withDefaults(defineProps(), { label: "登记事件", - requiredMark: false + requiredMark: false, + disabled: false }); const modelValue = defineModel({ required: true }); diff --git a/src/layout/MainLayout.vue b/src/layout/MainLayout.vue index ce926f2..aaa4fe3 100644 --- a/src/layout/MainLayout.vue +++ b/src/layout/MainLayout.vue @@ -116,7 +116,7 @@ const navBarHeight = computed(() => { const tabVal = ref(route.path); const list = [ { value: "/customers", icon: "usergroup", text: "客户" }, - { value: "/store", icon: "shop", text: "门店" }, + { value: "/store", icon: "shop-3", text: "门店" }, { value: "/management", icon: "setting-1", text: "管理" } ]; diff --git a/src/pages/customer/CustomerDetail.vue b/src/pages/customer/CustomerDetail.vue index 4dc1e31..4c65cc3 100644 --- a/src/pages/customer/CustomerDetail.vue +++ b/src/pages/customer/CustomerDetail.vue @@ -30,6 +30,9 @@ +
调理部位 @@ -147,12 +150,14 @@ import { DialogPlugin, Toast } from "tdesign-mobile-vue"; import { Time, useUserStore } from "timi-web"; import CustomerAPI from "@/api/CustomerAPI"; +import PointAPI from "@/api/PointAPI"; import BodyAreaSelector from "@/components/BodyAreaSelector.vue"; import CodeScanner from "@/components/CodeScanner.vue"; import CustomerProfileCard from "@/components/CustomerProfileCard.vue"; import CustomerSelector from "@/components/CustomerSelector.vue"; import { useSettingStore } from "@/store/settingStore"; import type { BodyAreaValue, Customer } from "@/types/Customer"; +import type { PointAccount } from "@/types/Point"; import { getCustomerPhotoURL, normalizeBodyAreaValue } from "@/utils/customer"; import { permissionCodes } from "@/utils/permission"; import { LunarCalendar } from "@/utils/LunarCalendar"; @@ -167,6 +172,7 @@ const settingStore = useSettingStore(); const userStore = useUserStore(); const customer = ref(); +const pointAccount = ref(); const invitees = ref([]); const selectedInviteeId = ref(""); const selectorVisible = ref(false); @@ -179,6 +185,8 @@ let latestDecodedText = ""; const inviter = computed(() => customer.value?.introducerCustomer); const canReadStat = computed(() => userStore.hasPermissions(permissionCodes.stat.read)); +const canReadPointAccount = computed(() => userStore.hasPermissions(permissionCodes.point.accountRead)); +const pointBalanceText = computed(() => pointAccount.value ? `${(pointAccount.value.balance ?? 0).toLocaleString("zh-CN")} 分` : "尚未产生积分流水"); const creatorName = computed(() => { const creatorUser = customer.value?.creatorUser; return creatorUser?.nick || creatorUser?.name || customer.value?.creatorUserId || "无"; @@ -209,6 +217,7 @@ async function loadCustomer(): Promise { try { showSolarBirthdate.value = false; customer.value = await CustomerAPI.detail(id); + await loadPointAccount(); await loadInvitees(); } catch (err) { customer.value = undefined; @@ -218,6 +227,18 @@ async function loadCustomer(): Promise { } } +async function loadPointAccount(): Promise { + pointAccount.value = undefined; + if (!customer.value?.id || !canReadPointAccount.value) { + return; + } + try { + pointAccount.value = await PointAPI.accountDetail(customer.value.id, customer.value.storeId); + } catch { + pointAccount.value = undefined; + } +} + async function loadInvitees(): Promise { const customerId = customer.value?.id; if (!customerId) { @@ -279,6 +300,16 @@ async function toEdit(): Promise { await router.push(`/management/customers/${customer.value.id}/edit`); } +async function toPointAccount(): Promise { + if (!customer.value?.id) { + return; + } + await router.push({ + name: "PointAccount", + query: { customerId: customer.value.id, storeId: customer.value.storeId || "" } + }); +} + function formatBirthdate(value?: Customer): string { if (!value?.birthdate) { return "无"; diff --git a/src/pages/event/EventRecordEdit.vue b/src/pages/event/EventRecordEdit.vue index 84444ec..200f664 100644 --- a/src/pages/event/EventRecordEdit.vue +++ b/src/pages/event/EventRecordEdit.vue @@ -8,9 +8,9 @@ readonly borderless placeholder="选择或扫码客户" - @click="openSelectedCustomerList" + @click="!isEditMode && openSelectedCustomerList()" > -