update point account style

This commit is contained in:
Timi
2026-08-13 00:04:57 +08:00
parent b4ba0062a9
commit 206d2cbc81
3 changed files with 172 additions and 67 deletions
+1 -1
View File
@@ -52,9 +52,9 @@ defineOptions({
.tips {
color: #52607a;
padding: .25rem 1rem;
font-size: .8125rem;
line-height: 1.5;
padding: .25rem 1rem;
}
}
-1
View File
@@ -35,7 +35,6 @@ defineOptions({
</script>
<style lang="less" scoped>
.card-title {
margin: 1rem;
.title {
margin: 0;
+160 -54
View File
@@ -1,33 +1,61 @@
<template>
<div v-can="permissionCodes.point.accountRead" class="point-account">
<section class="selector-panel">
<div class="section-title">选择客户</div>
<div class="header bg-white">
<div class="buttons">
<t-button
v-if="isGlobalManager"
class="customer-trigger"
class="selector"
size="small"
theme="primary"
variant="outline"
:content="selectedStoreName"
@click="openStorePicker"
/>
<t-button
class="customer-trigger"
class="selector"
size="small"
theme="primary"
variant="outline"
:content="customerText"
@click="selectorVisible = true"
/>
<customer-profile-card v-if="customer" :customer="customer" />
</section>
<section v-if="customer && account" class="account-card">
<div class="account-caption">当前可用积分</div>
<div class="balance" v-text="balanceText" />
<div class="account-meta">
<span v-text="`账户版本 ${account.version ?? '-'}`" />
<span v-text="account.storeId || '当前门店'" />
>
<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" />
</card-container>
<section v-if="customer && account" class="account-card">
<div class="header">
<div>积分卡</div>
<div class="time dark-white" v-text="formatTimestamp(account.createdAt!)" />
</div>
<div class="balance">
<t-icon name="money" size="2rem" />
<div class="value" v-text="balanceText" />
</div>
<div class="id" v-text="account.id?.toUpperCase()" />
</section>
<t-cell-group v-if="customer && account" class="statistics" title="积分统计" theme="card">
<t-cell-group v-if="customer && account" title="积分统计" theme="card">
<t-cell title="累计获得" :note="formatPoints(account.totalEarned)" />
<t-cell title="累计撤销" :note="formatPoints(account.totalRevoked)" />
<t-cell title="人工调整" :note="formatSignedPoints(account.totalAdjusted)" />
@@ -36,6 +64,8 @@
<section v-if="customer" class="actions">
<t-button
v-can="permissionCodes.point.ledgerRead"
class="bg-white"
theme="primary"
variant="outline"
@click="toLedger"
>
@@ -78,8 +108,10 @@ import { Toast } from "tdesign-mobile-vue";
import { useUserStore } from "timi-web";
import CustomerAPI from "@/api/CustomerAPI";
import PointAPI from "@/api/PointAPI";
import CodeScanner from "@/components/CodeScanner.vue";
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
import CustomerSelector from "@/components/CustomerSelector.vue";
import { useSettingStore } from "@/store/settingStore";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import type { Customer } from "@/types/Customer";
import type { PointAccount as PointAccountData } from "@/types/Point";
@@ -95,6 +127,7 @@ const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const settingStore = useSettingStore();
// ---------- 门店 ----------
@@ -155,7 +188,6 @@ const account = ref<PointAccountData>();
const selectorVisible = ref(false);
const isLoading = ref(false);
const customerText = computed(() => customer.value?.name || customer.value?.code || "请选择客户");
const balanceText = computed(() => formatPoints(account.value?.balance));
async function handleCustomerChange(value: string | string[]): Promise<void> {
@@ -167,6 +199,33 @@ async function handleCustomerChange(value: string | string[]): Promise<void> {
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) {
Toast.warning("未找到客户");
return;
}
selectedCustomerId.value = matched.id;
customer.value = matched;
await router.replace({
name: "PointAccount",
query: {
customerId: selectedCustomerId.value,
storeId: selectedStoreId.value
}
});
await loadAccount();
} catch {
Toast.error("客户信息加载失败");
}
}
async function loadAccount(): Promise<void> {
const customerId = selectedCustomerId.value;
if (!customerId) {
@@ -192,6 +251,36 @@ async function loadAccount(): Promise<void> {
}
}
// ---------- 扫码 ----------
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> {
@@ -224,6 +313,14 @@ function formatPoints(value?: number): string {
return `${(value ?? 0).toLocaleString("zh-CN")} 分`;
}
function formatTimestamp(timestamp: number) {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}${month}${day}`;
}
function formatSignedPoints(value?: number): string {
const pointValue = value ?? 0;
return `${0 < pointValue ? "+" : ""}${pointValue.toLocaleString("zh-CN")} 分`;
@@ -249,10 +346,30 @@ onMounted(async () => {
.point-account {
gap: 1rem;
display: flex;
padding: calc(var(--app-nav-height) + 1rem) 0 2rem;
padding: var(--app-nav-height) 0 calc(6rem + env(safe-area-inset-bottom, 0));
flex-direction: column;
.selector-panel,
> .header {
top: var(--app-nav-height);
z-index: 10;
padding: .5rem 1rem;
position: sticky;
.buttons {
gap: 1rem;
display: flex;
.selector {
flex: 1;
min-width: 0;
}
}
.scanner-wrapper {
margin-top: 1rem;
}
}
.account-card {
margin: 0 1rem;
padding: 1rem;
@@ -261,58 +378,48 @@ onMounted(async () => {
border-radius: 1.4rem;
}
.selector-panel {
gap: .75rem;
display: flex;
flex-direction: column;
.section-title {
font-size: 1rem;
font-weight: 700;
}
.customer-trigger {
width: 100%;
}
:deep(.customer-profile-card) {
margin: 0;
}
}
.account-card {
color: #fff;
color: #FFF;
background: linear-gradient(135deg, #0f766e, #0f4c5c);
.account-caption {
opacity: .78;
font-size: .875rem;
.header {
display: flex;
justify-content: space-between;
}
.balance {
gap: 1rem;
margin: .5rem 0;
font-size: 2.5rem;
display: flex;
align-items: center;
justify-content: end;
.value {
font-size: 2rem;
text-align: right;
font-weight: 700;
letter-spacing: .03em;
}
}
.account-meta {
gap: 1rem;
display: flex;
.id {
color: rgba(255, 255, 255, .72);
font-size: .75rem;
justify-content: space-between;
}
}
.statistics {
margin: 0 1rem;
.time {
text-align: right;
}
}
.actions {
left: 1rem;
right: 1rem;
gap: .75rem;
z-index: 20;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
display: flex;
margin: 0 1rem;
position: fixed;
.t-button {
flex: 1;
@@ -326,9 +433,8 @@ onMounted(async () => {
:global(html.theme-dark) {
.point-account {
.selector-panel {
background: rgba(15, 23, 42, .88);
box-shadow: 0 .75rem 2rem rgba(2, 6, 23, .32);
.selector {
background: #10161f;
}
.account-card {