add point account list
This commit is contained in:
+30
-5
@@ -1,6 +1,7 @@
|
|||||||
import { axios, type Page, type PageResult } from "timi-web";
|
import { axios, type Page, type PageResult } from "timi-web";
|
||||||
import type {
|
import type {
|
||||||
PointAccount,
|
PointAccount,
|
||||||
|
PointAccountPage,
|
||||||
PointAdjustRequest,
|
PointAdjustRequest,
|
||||||
PointLedger,
|
PointLedger,
|
||||||
PointLedgerPage,
|
PointLedgerPage,
|
||||||
@@ -15,9 +16,21 @@ type ListOptions = {
|
|||||||
storeId?: string;
|
storeId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 分页查询积分账户。账户列表由后端关联返回客户资料。 */
|
||||||
|
async function list(page: PointAccountPage, options?: ListOptions): Promise<PageResult<PointAccount>> {
|
||||||
|
if (options?.allStores) {
|
||||||
|
return await axios.post(`${BASE_URI}/account/list`, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await axios.post(`${BASE_URI}/account/list`, {
|
||||||
|
...page,
|
||||||
|
storeId: options?.storeId || getCurrentStoreId()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 查询指定客户的积分账户。账户不存在表示客户尚未产生过积分流水。 */
|
/** 查询指定客户的积分账户。账户不存在表示客户尚未产生过积分流水。 */
|
||||||
async function accountDetail(customerId: string, storeId?: string): Promise<PointAccount> {
|
async function detailByCustomerId(customerId: string, storeId?: string): Promise<PointAccount> {
|
||||||
return await axios.post(`${BASE_URI}/account/detail`, undefined, {
|
return await axios.post(`${BASE_URI}/account/detail/customer`, undefined, {
|
||||||
params: {
|
params: {
|
||||||
customerId,
|
customerId,
|
||||||
storeId: storeId || getCurrentStoreId() || undefined
|
storeId: storeId || getCurrentStoreId() || undefined
|
||||||
@@ -25,8 +38,18 @@ async function accountDetail(customerId: string, storeId?: string): Promise<Poin
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 按账户 ID 查询积分账户详情。 */
|
||||||
|
async function detail(accountId: string, storeId?: string): Promise<PointAccount> {
|
||||||
|
return await axios.post(`${BASE_URI}/account/detail`, undefined, {
|
||||||
|
params: {
|
||||||
|
id: accountId,
|
||||||
|
storeId: storeId || getCurrentStoreId() || undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 分页查询积分流水。全局管理员不传门店时允许查询全部门店。 */
|
/** 分页查询积分流水。全局管理员不传门店时允许查询全部门店。 */
|
||||||
async function ledgerList(page: PointLedgerPage, options?: ListOptions): Promise<PageResult<PointLedger>> {
|
async function listLedger(page: PointLedgerPage, options?: ListOptions): Promise<PageResult<PointLedger>> {
|
||||||
if (options?.allStores) {
|
if (options?.allStores) {
|
||||||
return await axios.post(`${BASE_URI}/ledger/list`, page);
|
return await axios.post(`${BASE_URI}/ledger/list`, page);
|
||||||
}
|
}
|
||||||
@@ -89,8 +112,10 @@ async function ruleRemove(id: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
accountDetail,
|
list,
|
||||||
ledgerList,
|
detailByCustomerId,
|
||||||
|
detail,
|
||||||
|
listLedger,
|
||||||
adjust,
|
adjust,
|
||||||
ruleList,
|
ruleList,
|
||||||
ruleDetail,
|
ruleDetail,
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<t-cell class="customer-cell" :arrow="arrow" :hover="hover">
|
||||||
|
<template #leftIcon>
|
||||||
|
<t-image
|
||||||
|
v-if="photoURL"
|
||||||
|
:src="photoURL"
|
||||||
|
:alt="customerName"
|
||||||
|
fit="cover"
|
||||||
|
class="avatar"
|
||||||
|
/>
|
||||||
|
<div v-else class="avatar fallback" v-text="customerName.slice(0, 1) || '客'" />
|
||||||
|
</template>
|
||||||
|
<template #title>
|
||||||
|
<div class="title">
|
||||||
|
<div class="main">
|
||||||
|
<span class="name" v-text="customerName || '未命名客户'" />
|
||||||
|
<gender-icon :value="customer?.gender" size="1.125rem" />
|
||||||
|
</div>
|
||||||
|
<span class="code" v-text="customer?.code || ''" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="$slots.description" #description>
|
||||||
|
<slot name="description" />
|
||||||
|
</template>
|
||||||
|
<template v-if="$slots.note" #note>
|
||||||
|
<slot name="note" />
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Customer } from "@/types/Customer";
|
||||||
|
import { getCustomerPhotoURL } from "@/utils/customer";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "CustomerCell"
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
customer?: Customer;
|
||||||
|
arrow?: boolean;
|
||||||
|
hover?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
arrow: false,
|
||||||
|
hover: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const customer = computed(() => props.customer);
|
||||||
|
const customerName = computed(() => customer.value?.name || customer.value?.code || "");
|
||||||
|
const photoURL = computed(() => customer.value ? getCustomerPhotoURL(customer.value, 64) : "");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.customer-cell {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
:deep(.t-cell__title) {
|
||||||
|
margin-right: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-cell__note) {
|
||||||
|
flex: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
gap: .5rem;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.main {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.code {
|
||||||
|
color: #52607a;
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.avatar .t-image__img) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.theme-dark) {
|
||||||
|
.customer-cell {
|
||||||
|
.code {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fallback {
|
||||||
|
background: #115e59;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -378,7 +378,7 @@ async function loadPointAccount(): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
pointAccount.value = await PointAPI.accountDetail(customer.value.id, customer.value.storeId);
|
pointAccount.value = await PointAPI.detailByCustomerId(customer.value.id, customer.value.storeId);
|
||||||
} catch {
|
} catch {
|
||||||
pointAccount.value = undefined;
|
pointAccount.value = undefined;
|
||||||
}
|
}
|
||||||
@@ -388,10 +388,14 @@ async function toPointAccount(): Promise<void> {
|
|||||||
if (!customer.value?.id) {
|
if (!customer.value?.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!pointAccount.value?.id) {
|
||||||
|
await router.push({ name: "PointAccountList" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
await router.push({
|
await router.push({
|
||||||
name: "PointAccount",
|
name: "PointAccountDetail",
|
||||||
|
params: { id: pointAccount.value.id },
|
||||||
query: {
|
query: {
|
||||||
customerId: customer.value.id,
|
|
||||||
storeId: customer.value.storeId || "",
|
storeId: customer.value.storeId || "",
|
||||||
transitionDepth: String(Number(route.meta.depth ?? 0) + 1)
|
transitionDepth: String(Number(route.meta.depth ?? 0) + 1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,9 @@
|
|||||||
:key="item.id || item.code || index"
|
:key="item.id || item.code || index"
|
||||||
class="item"
|
class="item"
|
||||||
>
|
>
|
||||||
<t-cell
|
<customer-cell
|
||||||
class="cell"
|
class="cell"
|
||||||
:title="item.name || item.code || '-'"
|
:customer="item"
|
||||||
arrow
|
arrow
|
||||||
hover
|
hover
|
||||||
@click="toCustomerDetail(item)"
|
@click="toCustomerDetail(item)"
|
||||||
@@ -40,17 +40,7 @@
|
|||||||
<template #description>
|
<template #description>
|
||||||
<telephone-link :value="item.telephone" placeholder="未填写电话" />
|
<telephone-link :value="item.telephone" placeholder="未填写电话" />
|
||||||
</template>
|
</template>
|
||||||
<template #leftIcon>
|
</customer-cell>
|
||||||
<t-image
|
|
||||||
v-if="getCustomerPhotoURL(item, 64)"
|
|
||||||
class="avatar"
|
|
||||||
:src="getCustomerPhotoURL(item, 64)"
|
|
||||||
:alt="item.name"
|
|
||||||
fit="cover"
|
|
||||||
/>
|
|
||||||
<div v-else class="avatar fallback" v-text="(item.name || '客').slice(0, 1)" />
|
|
||||||
</template>
|
|
||||||
</t-cell>
|
|
||||||
<template #right>
|
<template #right>
|
||||||
<div v-can="permissionCodes.customer.update" class="remove bg-red white" @click="removeInvitee(item)">删除</div>
|
<div v-can="permissionCodes.customer.update" class="remove bg-red white" @click="removeInvitee(item)">删除</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -105,7 +95,6 @@ import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
|||||||
import CustomerSelector from "@/components/CustomerSelector.vue";
|
import CustomerSelector from "@/components/CustomerSelector.vue";
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
import { useSettingStore } from "@/store/settingStore";
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import { getCustomerPhotoURL } from "@/utils/customer";
|
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -322,11 +311,6 @@ watch(() => route.params.id, loadCustomer);
|
|||||||
|
|
||||||
.cell {
|
.cell {
|
||||||
padding: 6px 10px 6px 6px;
|
padding: 6px 10px 6px 6px;
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 3.25rem;
|
|
||||||
height: 3.25rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,36 +38,18 @@
|
|||||||
@scrolltolower="fetch"
|
@scrolltolower="fetch"
|
||||||
>
|
>
|
||||||
<t-cell-group class="items" theme="card">
|
<t-cell-group class="items" theme="card">
|
||||||
<t-cell
|
<customer-cell
|
||||||
class="item"
|
class="item"
|
||||||
v-for="(item, index) in pageResult.list"
|
v-for="(item, index) in pageResult.list"
|
||||||
:key="item.id || item.code || index"
|
:key="item.id || item.code || index"
|
||||||
|
:customer="item"
|
||||||
arrow
|
arrow
|
||||||
@click="router.push(`/management/customers/${item.id}`)"
|
@click="router.push(`/management/customers/${item.id}`)"
|
||||||
>
|
>
|
||||||
<template #left-icon>
|
|
||||||
<t-image
|
|
||||||
v-if="getCustomerPhotoURL(item, 64)"
|
|
||||||
class="avatar"
|
|
||||||
:src="getCustomerPhotoURL(item, 64)"
|
|
||||||
:alt="item.name"
|
|
||||||
fit="cover"
|
|
||||||
/>
|
|
||||||
<div v-else class="avatar fallback" v-text="(item.name || item.code || '客').slice(0, 1)" />
|
|
||||||
</template>
|
|
||||||
<template #title>
|
|
||||||
<div class="header">
|
|
||||||
<div class="left">
|
|
||||||
<div class="text-bold" v-text="item.name" />
|
|
||||||
<gender-icon :value="item.gender" size="1.125rem" />
|
|
||||||
</div>
|
|
||||||
<span class="gray" v-text="item.code || ''" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template #description>
|
<template #description>
|
||||||
<telephone-link :value="item.telephone" />
|
<telephone-link :value="item.telephone" />
|
||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</customer-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<div v-if="!pageResult.list.length" class="load-state" v-text="isLoading ? '正在加载客户' : '暂无客户数据'" />
|
<div v-if="!pageResult.list.length" class="load-state" v-text="isLoading ? '正在加载客户' : '暂无客户数据'" />
|
||||||
<div v-if="pageResult.list.length" class="load-state" v-text="isLoading ? '正在加载更多' : isNoMore ? '已经到底了' : '上拉加载更多'" />
|
<div v-if="pageResult.list.length" class="load-state" v-text="isLoading ? '正在加载更多' : isNoMore ? '已经到底了' : '上拉加载更多'" />
|
||||||
@@ -85,14 +67,12 @@
|
|||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { type Page, type PageResult, Time, Toolkit, useUserStore } from "timi-web";
|
import { type Page, type PageResult, Time, Toolkit, useUserStore } from "timi-web";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
import GenderIcon from "@/components/GenderIcon.vue";
|
|
||||||
import TagSelector from "@/components/TagSelector.vue";
|
import TagSelector from "@/components/TagSelector.vue";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import type { NavBarAction } from "@/types/NavBarAction";
|
import type { NavBarAction } from "@/types/NavBarAction";
|
||||||
import type { Tag } from "@/types/Tag";
|
import type { Tag } from "@/types/Tag";
|
||||||
import { getCustomerPhotoURL } from "@/utils/customer";
|
|
||||||
import { corePermissionCodes, gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
import { corePermissionCodes, gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
@@ -304,26 +284,6 @@ async function doExport(): Promise<void> {
|
|||||||
|
|
||||||
.item {
|
.item {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 3.5rem;
|
|
||||||
height: 3.5rem;
|
|
||||||
border-radius: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header {
|
|
||||||
gap: .5rem;
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.left {
|
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,27 +47,16 @@
|
|||||||
class="list"
|
class="list"
|
||||||
theme="card"
|
theme="card"
|
||||||
>
|
>
|
||||||
<t-cell
|
<customer-cell
|
||||||
class="item"
|
class="item"
|
||||||
:class="{ 'in-top3': index < 3}"
|
:class="{ 'in-top3': index < 3}"
|
||||||
v-for="(item, index) in pageResult.list"
|
v-for="(item, index) in pageResult.list"
|
||||||
:key="item.customerId"
|
:key="item.customerId"
|
||||||
arrow
|
arrow
|
||||||
@click="toCustomerDetail(item.customerId || '')"
|
@click="toCustomerDetail(item.customerId || '')"
|
||||||
|
:customer="item.customer"
|
||||||
>
|
>
|
||||||
<template #left-icon>
|
<template #note>
|
||||||
<t-image
|
|
||||||
v-if="getCustomerPhotoURL(item.customer, 64)"
|
|
||||||
:src="getCustomerPhotoURL(item.customer, 64)"
|
|
||||||
:alt="getCustomerName(item)"
|
|
||||||
fit="cover"
|
|
||||||
class="avatar"
|
|
||||||
/>
|
|
||||||
<div v-else class="avatar fallback" v-text="getCustomerInitial(item)" />
|
|
||||||
</template>
|
|
||||||
<template #title>
|
|
||||||
<div class="title">
|
|
||||||
<div class="left">
|
|
||||||
<div
|
<div
|
||||||
class="rank"
|
class="rank"
|
||||||
:class="{
|
:class="{
|
||||||
@@ -77,10 +66,6 @@
|
|||||||
}"
|
}"
|
||||||
v-text="index + 1"
|
v-text="index + 1"
|
||||||
/>
|
/>
|
||||||
<div class="name" v-text="getCustomerName(item)" />
|
|
||||||
</div>
|
|
||||||
<t-tag size="small" variant="light-outline" :content="item.customer?.code" />
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<template #description>
|
<template #description>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
@@ -110,7 +95,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</customer-cell>
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<div v-else class="load-state" v-text="emptyStateText" />
|
<div v-else class="load-state" v-text="emptyStateText" />
|
||||||
<div v-if="pageResult.list.length" class="load-state" v-text="loadStateText" />
|
<div v-if="pageResult.list.length" class="load-state" v-text="loadStateText" />
|
||||||
@@ -145,7 +130,6 @@ import EventAPI from "@/api/EventAPI";
|
|||||||
import EventRecordAPI from "@/api/EventRecordAPI";
|
import EventRecordAPI from "@/api/EventRecordAPI";
|
||||||
import type { CustomerEventRankView, EventRecordRankPage, EventRecordRankType } from "@/types/EventRecord";
|
import type { CustomerEventRankView, EventRecordRankPage, EventRecordRankType } from "@/types/EventRecord";
|
||||||
import type { EventItem } from "@/types/EventItem";
|
import type { EventItem } from "@/types/EventItem";
|
||||||
import { getCustomerPhotoURL } from "@/utils/customer";
|
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { formatEventNumberValue } from "@/utils/eventValue";
|
import { formatEventNumberValue } from "@/utils/eventValue";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
@@ -376,14 +360,6 @@ async function toCustomerDetail(customerId: string): Promise<void> {
|
|||||||
|
|
||||||
// ---------- 格式化 ----------
|
// ---------- 格式化 ----------
|
||||||
|
|
||||||
function getCustomerName(item: CustomerEventRankView): string {
|
|
||||||
return item.customer?.name || "未知客户";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCustomerInitial(item: CustomerEventRankView): string {
|
|
||||||
return getCustomerName(item).slice(0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTotalNumberValueText(item: CustomerEventRankView): string {
|
function getTotalNumberValueText(item: CustomerEventRankView): string {
|
||||||
return formatEventNumberValue(selectedNumberEvent.value, item.totalNumberValue);
|
return formatEventNumberValue(selectedNumberEvent.value, item.totalNumberValue);
|
||||||
}
|
}
|
||||||
@@ -543,36 +519,6 @@ onMounted(async () => {
|
|||||||
.list {
|
.list {
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
--td-spacer-1: 8px;
|
|
||||||
--td-spacer-2: 4px;
|
|
||||||
padding: 10px;
|
|
||||||
|
|
||||||
&.in-top3 {
|
|
||||||
|
|
||||||
.title {
|
|
||||||
|
|
||||||
.name {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
width: 3.5rem;
|
|
||||||
height: 3.5rem;
|
|
||||||
border-radius: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.left {
|
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.rank {
|
.rank {
|
||||||
flex: 0 0 1.5rem;
|
flex: 0 0 1.5rem;
|
||||||
@@ -603,16 +549,6 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
|
||||||
color: #0F172A;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: .875rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,40 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-can="permissionCodes.point.accountRead" class="point-account">
|
<div v-can="permissionCodes.point.accountRead" class="point-account-detail">
|
||||||
<div class="header bg-white">
|
<card-container v-if="customer">
|
||||||
<div class="buttons">
|
|
||||||
<t-button
|
|
||||||
class="selector"
|
|
||||||
size="small"
|
|
||||||
theme="primary"
|
|
||||||
variant="outline"
|
|
||||||
@click="selectorVisible = true"
|
|
||||||
>
|
|
||||||
<template #icon>
|
|
||||||
<t-icon name="user-search" />
|
|
||||||
</template>
|
|
||||||
<span>选择客户</span>
|
|
||||||
</t-button>
|
|
||||||
<t-button size="small" theme="primary" @click="toggleScanner">
|
|
||||||
<template #icon>
|
|
||||||
<t-icon name="scan" />
|
|
||||||
</template>
|
|
||||||
<span v-text="scannerVisible ? '关闭扫码' : '扫码'" />
|
|
||||||
</t-button>
|
|
||||||
</div>
|
|
||||||
<div v-if="scannerVisible" class="line scanner-wrapper">
|
|
||||||
<code-scanner
|
|
||||||
visible
|
|
||||||
:status-text="scanStatus"
|
|
||||||
active-text="正在识别客户编码,请将二维码移入取景框"
|
|
||||||
@decode="handleDecodedText"
|
|
||||||
@status-change="handleScanStatusChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<card-container>
|
|
||||||
<customer-profile-card :customer="customer" />
|
<customer-profile-card :customer="customer" />
|
||||||
</card-container>
|
</card-container>
|
||||||
<section v-if="customer && account" class="account-card">
|
<section v-if="account" class="account-card">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div>积分卡</div>
|
<div>积分卡</div>
|
||||||
<div class="time dark-white" v-text="formatTimestamp(account.createdAt!)" />
|
<div class="time dark-white" v-text="formatTimestamp(account.createdAt!)" />
|
||||||
@@ -46,7 +15,7 @@
|
|||||||
<div class="id" v-text="account.id?.toUpperCase()" />
|
<div class="id" v-text="account.id?.toUpperCase()" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<t-cell-group v-if="customer && account" title="积分统计" theme="card">
|
<t-cell-group v-if="account" title="积分统计" theme="card">
|
||||||
<t-cell title="累计获得" :note="formatPoints(account.totalEarned)" />
|
<t-cell title="累计获得" :note="formatPoints(account.totalEarned)" />
|
||||||
<t-cell title="累计消耗" :note="formatPoints(account.totalConsumed)" />
|
<t-cell title="累计消耗" :note="formatPoints(account.totalConsumed)" />
|
||||||
<t-cell title="累计撤销" :note="formatPoints(account.totalRevoked)" />
|
<t-cell title="累计撤销" :note="formatPoints(account.totalRevoked)" />
|
||||||
@@ -72,17 +41,7 @@
|
|||||||
</t-button>
|
</t-button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<t-empty
|
<t-empty v-if="!account" :description="isLoading ? '正在加载积分账户' : '积分账户不存在或已被删除'" />
|
||||||
v-if="customer && !account"
|
|
||||||
:description="isLoading ? '正在加载积分账户' : '该客户还没有积分流水,积分账户会在首次产生积分时自动创建。'"
|
|
||||||
/>
|
|
||||||
<t-empty v-if="!customer" description="请选择客户查看积分账户" />
|
|
||||||
|
|
||||||
<customer-selector
|
|
||||||
v-model="selectedCustomerId"
|
|
||||||
v-model:visible="selectorVisible"
|
|
||||||
@change="handleCustomerChange"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -91,25 +50,20 @@ import { Message } from "@/utils/message";
|
|||||||
import { useUserStore } from "timi-web";
|
import { useUserStore } from "timi-web";
|
||||||
import CustomerAPI from "@/api/CustomerAPI";
|
import CustomerAPI from "@/api/CustomerAPI";
|
||||||
import PointAPI from "@/api/PointAPI";
|
import PointAPI from "@/api/PointAPI";
|
||||||
import CodeScanner from "@/components/CodeScanner.vue";
|
|
||||||
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
|
||||||
import CustomerSelector from "@/components/CustomerSelector.vue";
|
|
||||||
import { useSettingStore } from "@/store/settingStore";
|
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import type { PointAccount as PointAccountData } from "@/types/Point";
|
import type { PointAccount as PointAccountData } from "@/types/Point";
|
||||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: "PointAccount"
|
name: "PointAccountDetail"
|
||||||
});
|
});
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const currentStoreStore = useCurrentStoreStore();
|
const currentStoreStore = useCurrentStoreStore();
|
||||||
const settingStore = useSettingStore();
|
|
||||||
|
|
||||||
// ---------- 门店 ----------
|
// ---------- 门店 ----------
|
||||||
|
|
||||||
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
|
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
|
||||||
@@ -122,52 +76,17 @@ async function prepareStoreScope(): Promise<void> {
|
|||||||
await currentStoreStore.loadLoginStore();
|
await currentStoreStore.loadLoginStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 客户与账户 ----------
|
// ---------- 账户 ----------
|
||||||
|
|
||||||
const selectedCustomerId = ref(String(route.query.customerId || ""));
|
const accountId = String(route.params.id || "");
|
||||||
const customer = ref<Customer>();
|
const customer = ref<Customer>();
|
||||||
const account = ref<PointAccountData>();
|
const account = ref<PointAccountData>();
|
||||||
const selectorVisible = ref(false);
|
|
||||||
const isLoading = ref(false);
|
const isLoading = ref(false);
|
||||||
|
|
||||||
const balanceText = computed(() => formatPoints(account.value?.balance));
|
const balanceText = computed(() => formatPoints(account.value?.balance));
|
||||||
|
|
||||||
async function handleCustomerChange(value: string | string[]): Promise<void> {
|
|
||||||
selectedCustomerId.value = Array.isArray(value) ? String(value[0] || "") : value;
|
|
||||||
await router.replace({
|
|
||||||
name: "PointAccount",
|
|
||||||
query: selectedCustomerId.value ? { customerId: selectedCustomerId.value } : undefined
|
|
||||||
});
|
|
||||||
await loadAccount();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadCustomerByCode(code: string): Promise<void> {
|
|
||||||
const normalizedCode = code.trim();
|
|
||||||
if (!normalizedCode) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const matched = await CustomerAPI.findByCode(normalizedCode);
|
|
||||||
if (!matched?.id) {
|
|
||||||
Message.warning("未找到客户");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
selectedCustomerId.value = matched.id;
|
|
||||||
customer.value = matched;
|
|
||||||
await router.replace({
|
|
||||||
name: "PointAccount",
|
|
||||||
query: { customerId: selectedCustomerId.value }
|
|
||||||
});
|
|
||||||
await loadAccount();
|
|
||||||
} catch {
|
|
||||||
Message.error("客户信息加载失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadAccount(): Promise<void> {
|
async function loadAccount(): Promise<void> {
|
||||||
const customerId = selectedCustomerId.value;
|
if (!accountId) {
|
||||||
if (!customerId) {
|
|
||||||
customer.value = undefined;
|
customer.value = undefined;
|
||||||
account.value = undefined;
|
account.value = undefined;
|
||||||
return;
|
return;
|
||||||
@@ -175,62 +94,35 @@ async function loadAccount(): Promise<void> {
|
|||||||
|
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
customer.value = await CustomerAPI.detail(customerId);
|
account.value = await PointAPI.detail(
|
||||||
try {
|
accountId,
|
||||||
account.value = await PointAPI.accountDetail(customerId, currentStoreStore.currentStoreId || undefined);
|
String(route.query.storeId || currentStoreStore.currentStoreId || "")
|
||||||
} catch {
|
);
|
||||||
account.value = undefined;
|
if (!account.value) {
|
||||||
|
customer.value = undefined;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
customer.value = account.value.customer || (account.value.customerId ? await CustomerAPI.detail(account.value.customerId) : undefined);
|
||||||
} catch {
|
} catch {
|
||||||
customer.value = undefined;
|
customer.value = undefined;
|
||||||
account.value = undefined;
|
account.value = undefined;
|
||||||
Message.error("客户信息加载失败");
|
Message.error("积分账户加载失败");
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- 扫码 ----------
|
|
||||||
|
|
||||||
const scannerVisible = ref(false);
|
|
||||||
const scanStatus = ref("点击扫码开始识别");
|
|
||||||
let latestDecodedText = "";
|
|
||||||
|
|
||||||
async function toggleScanner(): Promise<void> {
|
|
||||||
scannerVisible.value = !scannerVisible.value;
|
|
||||||
if (scannerVisible.value) {
|
|
||||||
latestDecodedText = "";
|
|
||||||
scanStatus.value = "正在启动扫码预览";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleDecodedText(rawText: string): Promise<void> {
|
|
||||||
const normalizedText = rawText.trim();
|
|
||||||
if (!normalizedText || normalizedText === latestDecodedText) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
latestDecodedText = normalizedText;
|
|
||||||
await settingStore.playScanSuccessSound();
|
|
||||||
scanStatus.value = `已识别 ${normalizedText},正在加载客户档案`;
|
|
||||||
scannerVisible.value = false;
|
|
||||||
await loadCustomerByCode(normalizedText);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleScanStatusChange(value: string): void {
|
|
||||||
scanStatus.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- 导航 ----------
|
// ---------- 导航 ----------
|
||||||
|
|
||||||
async function toLedger(): Promise<void> {
|
async function toLedger(): Promise<void> {
|
||||||
if (!selectedCustomerId.value) {
|
if (!account.value?.customerId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await router.push({
|
await router.push({
|
||||||
name: "PointLedgerList",
|
name: "PointLedgerList",
|
||||||
query: {
|
query: {
|
||||||
customerId: selectedCustomerId.value,
|
customerId: account.value.customerId,
|
||||||
accountId: account.value?.id || "",
|
accountId: account.value.id || "",
|
||||||
transitionDepth: String(getCurrentRouteDepth() + 1)
|
transitionDepth: String(getCurrentRouteDepth() + 1)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -248,12 +140,12 @@ function getCurrentRouteDepth(): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function toAdjust(): Promise<void> {
|
async function toAdjust(): Promise<void> {
|
||||||
if (!selectedCustomerId.value) {
|
if (!account.value?.customerId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await router.push({
|
await router.push({
|
||||||
name: "PointAdjust",
|
name: "PointAdjust",
|
||||||
query: { customerId: selectedCustomerId.value }
|
query: { customerId: account.value.customerId }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +155,10 @@ function formatPoints(value?: number): string {
|
|||||||
return `${(value ?? 0).toLocaleString("zh-CN")} 分`;
|
return `${(value ?? 0).toLocaleString("zh-CN")} 分`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTimestamp(timestamp: number) {
|
function formatTimestamp(timestamp?: number): string {
|
||||||
|
if (!timestamp) {
|
||||||
|
return "未知时间";
|
||||||
|
}
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
const year = date.getFullYear();
|
const year = date.getFullYear();
|
||||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
@@ -276,50 +171,23 @@ function formatSignedPoints(value?: number): string {
|
|||||||
return pointValue < 0 ? `扣除 ${Math.abs(pointValue).toLocaleString("zh-CN")} 分` : `增加 ${pointValue.toLocaleString("zh-CN")} 分`;
|
return pointValue < 0 ? `扣除 ${Math.abs(pointValue).toLocaleString("zh-CN")} 分` : `增加 ${pointValue.toLocaleString("zh-CN")} 分`;
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => route.query.customerId, async customerIdValue => {
|
|
||||||
const customerId = String(customerIdValue || "");
|
|
||||||
if (customerId !== selectedCustomerId.value) {
|
|
||||||
selectedCustomerId.value = customerId;
|
|
||||||
await loadAccount();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
await prepareStoreScope();
|
await prepareStoreScope();
|
||||||
await loadAccount();
|
await loadAccount();
|
||||||
|
} catch {
|
||||||
|
Message.error("积分账户页面加载失败");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
.point-account {
|
.point-account-detail {
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: calc(var(--app-nav-height) + 4rem) 0 calc(6rem + env(safe-area-inset-bottom, 0));
|
padding: calc(var(--app-nav-height) + 1rem) 0 calc(6rem + env(safe-area-inset-bottom, 0));
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
> .header {
|
|
||||||
top: var(--app-nav-height);
|
|
||||||
width: 100%;
|
|
||||||
z-index: 10;
|
|
||||||
padding: .5rem 1rem;
|
|
||||||
position: fixed;
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
gap: 1rem;
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.selector {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.scanner-wrapper {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.account-card {
|
.account-card {
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
margin: 0 1rem;
|
margin: 0 1rem;
|
||||||
@@ -379,11 +247,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:global(html.theme-dark) {
|
:global(html.theme-dark) {
|
||||||
.point-account {
|
.point-account-detail {
|
||||||
.selector {
|
|
||||||
background: #10161f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.account-card {
|
.account-card {
|
||||||
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
<template>
|
||||||
|
<div v-can="permissionCodes.point.accountRead" class="point-account-list">
|
||||||
|
<div class="filter bg-white">
|
||||||
|
<t-input
|
||||||
|
v-model.trim="keyword"
|
||||||
|
class="search"
|
||||||
|
placeholder="搜索客户编码、姓名、拼音或电话"
|
||||||
|
borderless
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<t-pull-down-refresh
|
||||||
|
class="list-wrapper"
|
||||||
|
v-model="isRefreshing"
|
||||||
|
:refresh-timeout="30000"
|
||||||
|
@refresh="reload"
|
||||||
|
@scrolltolower="load"
|
||||||
|
>
|
||||||
|
<t-cell-group v-if="pageResult.list.length" class="list" theme="card">
|
||||||
|
<customer-cell
|
||||||
|
v-for="item in pageResult.list"
|
||||||
|
:key="item.id"
|
||||||
|
:customer="item.customer"
|
||||||
|
class="item"
|
||||||
|
arrow
|
||||||
|
hover
|
||||||
|
@click="toDetail(item)"
|
||||||
|
>
|
||||||
|
<template #description>
|
||||||
|
<span>余额:</span>
|
||||||
|
<span
|
||||||
|
class="balance"
|
||||||
|
:class="getBalanceTheme(item.balance)"
|
||||||
|
v-text="formatPoints(item.balance)" />
|
||||||
|
</template>
|
||||||
|
</customer-cell>
|
||||||
|
</t-cell-group>
|
||||||
|
<div class="load-state" v-text="loadStateText" />
|
||||||
|
</t-pull-down-refresh>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { type PageResult, useUserStore } from "timi-web";
|
||||||
|
import PointAPI from "@/api/PointAPI";
|
||||||
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
|
import type { PointAccount, PointAccountPage } from "@/types/Point";
|
||||||
|
import { Message } from "@/utils/message";
|
||||||
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
|
defineOptions({
|
||||||
|
name: "PointAccountList"
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const currentStoreStore = useCurrentStoreStore();
|
||||||
|
|
||||||
|
// ---------- 门店 ----------
|
||||||
|
|
||||||
|
const isGlobalManager = computed(() => userStore.hasRoles(gaoRoleCodes.globalManager));
|
||||||
|
|
||||||
|
async function prepareStoreScope(): Promise<void> {
|
||||||
|
if (isGlobalManager.value) {
|
||||||
|
await currentStoreStore.loadStoreList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await currentStoreStore.loadLoginStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 筛选 ----------
|
||||||
|
|
||||||
|
const keyword = ref("");
|
||||||
|
let keywordTimer: number | undefined;
|
||||||
|
|
||||||
|
// ---------- 分页 ----------
|
||||||
|
|
||||||
|
const page = reactive<PointAccountPage>({
|
||||||
|
index: 0,
|
||||||
|
size: 20
|
||||||
|
});
|
||||||
|
const pageResult = reactive<PageResult<PointAccount>>({
|
||||||
|
total: 0,
|
||||||
|
list: []
|
||||||
|
});
|
||||||
|
const isLoading = ref(false);
|
||||||
|
const isRefreshing = ref(false);
|
||||||
|
const isNoMore = ref(false);
|
||||||
|
let loadToken = 0;
|
||||||
|
let reloadRequested = false;
|
||||||
|
|
||||||
|
const loadStateText = computed(() => {
|
||||||
|
if (isLoading.value) {
|
||||||
|
return "正在加载积分账户";
|
||||||
|
}
|
||||||
|
if (!pageResult.list.length) {
|
||||||
|
return "暂无积分账户";
|
||||||
|
}
|
||||||
|
if (isNoMore.value) {
|
||||||
|
return `共 ${pageResult.total} 个积分账户`;
|
||||||
|
}
|
||||||
|
return "上拉加载更多";
|
||||||
|
});
|
||||||
|
|
||||||
|
async function reload(): Promise<void> {
|
||||||
|
if (isLoading.value) {
|
||||||
|
reloadRequested = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isRefreshing.value = true;
|
||||||
|
try {
|
||||||
|
page.index = 0;
|
||||||
|
pageResult.total = 0;
|
||||||
|
pageResult.list = [];
|
||||||
|
isNoMore.value = false;
|
||||||
|
await load();
|
||||||
|
} finally {
|
||||||
|
isRefreshing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load(): Promise<void> {
|
||||||
|
if (isLoading.value || isNoMore.value || !currentStoreStore.currentStoreId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const token = loadToken + 1;
|
||||||
|
loadToken = token;
|
||||||
|
isLoading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await PointAPI.list({
|
||||||
|
...page,
|
||||||
|
keyword: keyword.value || undefined
|
||||||
|
}, { storeId: currentStoreStore.currentStoreId });
|
||||||
|
if (token !== loadToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = result.list || [];
|
||||||
|
pageResult.list = pageResult.list.concat(list);
|
||||||
|
pageResult.total = result.total || 0;
|
||||||
|
page.index += 1;
|
||||||
|
isNoMore.value = pageResult.list.length >= pageResult.total || list.length < page.size;
|
||||||
|
} catch {
|
||||||
|
Message.error("积分账户加载失败");
|
||||||
|
} finally {
|
||||||
|
if (token === loadToken) {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
if (reloadRequested && token === loadToken) {
|
||||||
|
reloadRequested = false;
|
||||||
|
await reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 展示 ----------
|
||||||
|
|
||||||
|
function formatPoints(value?: number): string {
|
||||||
|
return `${(value ?? 0).toLocaleString("zh-CN")} 分`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBalanceTheme(value?: number): string {
|
||||||
|
return value && 0 < value ? "positive" : "zero";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 导航 ----------
|
||||||
|
|
||||||
|
async function toDetail(item: PointAccount): Promise<void> {
|
||||||
|
if (!item.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await router.push({
|
||||||
|
name: "PointAccountDetail",
|
||||||
|
params: { id: item.id },
|
||||||
|
query: item.storeId ? { storeId: item.storeId } : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 生命周期 ----------
|
||||||
|
|
||||||
|
watch(keyword, () => {
|
||||||
|
window.clearTimeout(keywordTimer);
|
||||||
|
keywordTimer = window.setTimeout(async () => {
|
||||||
|
await reload();
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.clearTimeout(keywordTimer);
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
await prepareStoreScope();
|
||||||
|
await reload();
|
||||||
|
} catch {
|
||||||
|
Message.error("积分账户页面加载失败");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.point-account-list {
|
||||||
|
padding: calc(var(--app-nav-height) + 4.5rem) 0 1rem;
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
top: var(--app-nav-height);
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 10;
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
position: fixed;
|
||||||
|
box-shadow: var(--td-shadow-1);
|
||||||
|
|
||||||
|
.search {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--td-component-border);
|
||||||
|
padding: .5rem 1rem;
|
||||||
|
border-radius: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-wrapper {
|
||||||
|
min-height: 70vh;
|
||||||
|
|
||||||
|
.list {
|
||||||
|
.item {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: #52607a;
|
||||||
|
font-size: .8125rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance {
|
||||||
|
font-size: .875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positive {
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zero {
|
||||||
|
color: #52607a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-state {
|
||||||
|
color: #52607a;
|
||||||
|
padding: 1rem;
|
||||||
|
font-size: .8125rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(html.theme-dark) {
|
||||||
|
.point-account-list {
|
||||||
|
.description,
|
||||||
|
.zero,
|
||||||
|
.load-state {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.positive {
|
||||||
|
color: #5eead4;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -165,7 +165,7 @@ async function loadCustomerAccount(): Promise<void> {
|
|||||||
selectedCustomer.value = await CustomerAPI.detail(selectedCustomerId.value);
|
selectedCustomer.value = await CustomerAPI.detail(selectedCustomerId.value);
|
||||||
if (userStore.hasPermissions(permissionCodes.point.accountRead)) {
|
if (userStore.hasPermissions(permissionCodes.point.accountRead)) {
|
||||||
try {
|
try {
|
||||||
account.value = await PointAPI.accountDetail(selectedCustomerId.value, currentStoreStore.currentStoreId || undefined);
|
account.value = await PointAPI.detailByCustomerId(selectedCustomerId.value, currentStoreStore.currentStoreId || undefined);
|
||||||
} catch {
|
} catch {
|
||||||
account.value = undefined;
|
account.value = undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ async function load(): Promise<void> {
|
|||||||
loadToken = token;
|
loadToken = token;
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const result = await PointAPI.ledgerList(buildRequest(), buildListOptions());
|
const result = await PointAPI.listLedger(buildRequest(), buildListOptions());
|
||||||
if (token !== loadToken) {
|
if (token !== loadToken) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { RouteRecordRaw } from "vue-router";
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
import PointAccount from "@/pages/point/PointAccount.vue";
|
import PointAccountDetail from "@/pages/point/PointAccountDetail.vue";
|
||||||
|
import PointAccountList from "@/pages/point/PointAccountList.vue";
|
||||||
import PointAdjust from "@/pages/point/PointAdjust.vue";
|
import PointAdjust from "@/pages/point/PointAdjust.vue";
|
||||||
import PointLedgerDetail from "@/pages/point/PointLedgerDetail.vue";
|
import PointLedgerDetail from "@/pages/point/PointLedgerDetail.vue";
|
||||||
import PointLedgerList from "@/pages/point/PointLedgerList.vue";
|
import PointLedgerList from "@/pages/point/PointLedgerList.vue";
|
||||||
@@ -10,8 +11,8 @@ import PointRuleSort from "@/pages/point/PointRuleSort.vue";
|
|||||||
const pointRoutes: RouteRecordRaw[] = [
|
const pointRoutes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
path: "/management/points/account",
|
path: "/management/points/account",
|
||||||
name: "PointAccount",
|
name: "PointAccountList",
|
||||||
component: PointAccount,
|
component: PointAccountList,
|
||||||
meta: {
|
meta: {
|
||||||
depth: 3,
|
depth: 3,
|
||||||
navBarVisible: true,
|
navBarVisible: true,
|
||||||
@@ -22,6 +23,20 @@ const pointRoutes: RouteRecordRaw[] = [
|
|||||||
darkBodyBackground: "#10161f"
|
darkBodyBackground: "#10161f"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/management/points/account/:id",
|
||||||
|
name: "PointAccountDetail",
|
||||||
|
component: PointAccountDetail,
|
||||||
|
meta: {
|
||||||
|
depth: 4,
|
||||||
|
navBarVisible: true,
|
||||||
|
navBarCanBack: true,
|
||||||
|
navBarTitle: "积分账户详情",
|
||||||
|
tabBarVisible: false,
|
||||||
|
bodyBackground: "#F5F5F8",
|
||||||
|
darkBodyBackground: "#10161f"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/management/points/rules/sort",
|
path: "/management/points/rules/sort",
|
||||||
name: "PointRuleSort",
|
name: "PointRuleSort",
|
||||||
|
|||||||
@@ -47,8 +47,14 @@ export type PointAccount = {
|
|||||||
totalRevoked?: number;
|
totalRevoked?: number;
|
||||||
totalAdjusted?: number;
|
totalAdjusted?: number;
|
||||||
version?: number;
|
version?: number;
|
||||||
|
customer?: Customer;
|
||||||
} & Model;
|
} & Model;
|
||||||
|
|
||||||
|
export type PointAccountPage = {
|
||||||
|
storeId?: string;
|
||||||
|
keyword?: string;
|
||||||
|
} & BasePage;
|
||||||
|
|
||||||
export type PointLedger = {
|
export type PointLedger = {
|
||||||
accountId?: string;
|
accountId?: string;
|
||||||
storeId?: string;
|
storeId?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user