update point ledger list style

This commit is contained in:
Timi
2026-08-13 00:46:11 +08:00
parent 206d2cbc81
commit 53c60d02cd
3 changed files with 275 additions and 150 deletions
+3 -2
View File
@@ -346,14 +346,15 @@ onMounted(async () => {
.point-account {
gap: 1rem;
display: flex;
padding: var(--app-nav-height) 0 calc(6rem + env(safe-area-inset-bottom, 0));
padding: calc(var(--app-nav-height) + 4rem) 0 calc(6rem + env(safe-area-inset-bottom, 0));
flex-direction: column;
> .header {
top: var(--app-nav-height);
width: 100%;
z-index: 10;
padding: .5rem 1rem;
position: sticky;
position: fixed;
.buttons {
gap: 1rem;
+228 -106
View File
@@ -1,12 +1,36 @@
<template>
<div v-can="permissionCodes.point.ledgerRead" class="point-ledger-list">
<div class="filters">
<div class="header bg-white">
<div class="buttons">
<t-button
class="customer-filter"
class="selector"
size="small"
theme="primary"
variant="outline"
:content="customerFilterText"
@click="selectorVisible = true"
>
<template #icon>
<t-icon name="user-search" />
</template>
<span v-text="customerFilterText" />
</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 class="filters">
<t-dropdown-menu>
<t-dropdown-item
v-if="isGlobalManager"
@@ -30,13 +54,73 @@
</t-dropdown-menu>
</div>
<div v-if="isCustomDate" class="date-range">
<t-button variant="outline" :content="dateRange[0] || '开始时间'" @click="startDateVisible = true" />
<t-button
size="small"
variant="outline"
:content="dateRange[0] || '开始时间'"
@click="startDateVisible = true"
/>
<span>至</span>
<t-button variant="outline" :content="dateRange[1] || '结束时间'" @click="endDateVisible = true" />
<t-button
size="small"
variant="outline"
:content="dateRange[1] || '结束时间'"
@click="endDateVisible = true"
/>
</div>
<section v-if="customer" class="customer-summary">
<div class="customer-name" v-text="customer.name || customer.code || '未命名客户'" />
<div class="customer-id" v-text="`客户 ID:${customer.id}`" />
</div>
<card-container v-if="customer" class="customer">
<customer-profile-card :customer="customer" />
</card-container>
<t-pull-down-refresh
v-model="isRefreshing"
:refresh-timeout="30000"
@refresh="reload"
@scrolltolower="load"
>
<t-cell-group v-if="pageResult.list.length" class="list" theme="card">
<t-cell
v-for="item in pageResult.list"
:key="item.id"
class="item"
:arrow="!!item.eventRecordId"
:hover="!!item.eventRecordId"
@click="toRecord(item)"
>
<template #leftIcon>
<t-image
v-if="getCustomerPhotoURL(item.customer!)"
:src="getCustomerPhotoURL(item.customer!)"
fit="cover"
class="avatar"
/>
<div v-else class="avatar fallback" v-text="(item.customer?.name || item.customer?.code || '客').slice(0, 1)" />
</template>
<template #title>
<div class="title">
<t-tag
class="type-icon"
:theme="item.pointDelta && 0 < item.pointDelta ? 'success' : 'danger'"
:content="getLedgerShortType(item)"
/>
<div class="name" v-text="item.customer?.name" />
</div>
</template>
<template #description>
<div class="description">
<div v-text="`交易时间:${Time.toPassedDateTime(item.occurredAt)}`" />
<div class="remark" v-text="getLedgerDescription(item)" />
</div>
</template>
<template #note>
<div class="delta" :class="getLedgerTheme(item)" v-text="getLedgerDelta(item)" />
</template>
</t-cell>
</t-cell-group>
<div class="load-state" v-text="loadStateText" />
</t-pull-down-refresh>
<section v-if="customer" class="actions">
<t-button
v-can="permissionCodes.point.adjust"
theme="primary"
@@ -46,35 +130,6 @@
人工调整
</t-button>
</section>
<t-pull-down-refresh
v-model="isRefreshing"
:refresh-timeout="30000"
@refresh="reload"
@scrolltolower="load"
>
<t-cell-group v-if="pageResult.list.length" class="ledger-list" theme="card">
<t-cell
v-for="item in pageResult.list"
:key="item.id"
class="ledger-item"
:title="getLedgerTitle(item)"
:description="getLedgerDescription(item)"
:note="getLedgerTime(item)"
:arrow="!!item.eventRecordId"
:hover="!!item.eventRecordId"
@click="toRecord(item)"
>
<template #leftIcon>
<div class="type-icon" :class="getLedgerTheme(item)" v-text="getLedgerShortType(item)" />
</template>
<template #extra>
<div class="delta" :class="getLedgerTheme(item)" v-text="getLedgerDelta(item)" />
</template>
</t-cell>
</t-cell-group>
<div v-else class="empty" v-text="isLoading ? '正在加载积分流水' : '暂无积分流水'" />
<div class="load-state" v-text="loadStateText" />
</t-pull-down-refresh>
<customer-selector
v-model="selectedCustomerId"
v-model:visible="selectorVisible"
@@ -108,10 +163,13 @@ import { Toast, type DropdownValue } from "tdesign-mobile-vue";
import { Time, type PageResult, useUserStore } from "timi-web";
import CustomerAPI from "@/api/CustomerAPI";
import PointAPI from "@/api/PointAPI";
import CodeScanner from "@/components/CodeScanner.vue";
import CustomerSelector from "@/components/CustomerSelector.vue";
import { useCurrentStoreStore } from "@/store/currentStoreStore";
import { useSettingStore } from "@/store/settingStore";
import type { Customer } from "@/types/Customer";
import type { PointLedger, PointLedgerChangeType, PointLedgerPage } from "@/types/Point";
import { getCustomerPhotoURL } from "@/utils/customer";
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
type DateFilter = "all" | "today" | "last7" | "last30" | "custom";
@@ -125,6 +183,7 @@ const route = useRoute();
const router = useRouter();
const userStore = useUserStore();
const currentStoreStore = useCurrentStoreStore();
const settingStore = useSettingStore();
// ---------- 筛选 ----------
@@ -176,6 +235,26 @@ async function handleCustomerChange(value: string | string[]): Promise<void> {
await reload();
}
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 reload();
} catch {
Toast.error("客户信息加载失败");
}
}
async function selectStore(value: DropdownValue): Promise<void> {
if (Array.isArray(value)) {
return;
@@ -215,6 +294,36 @@ async function confirmDateRange(): Promise<void> {
await reload();
}
// ---------- 扫码 ----------
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;
}
// ---------- 分页 ----------
const page = reactive<PointLedgerPage>({
@@ -301,34 +410,27 @@ function buildListOptions(): { allStores?: boolean; storeId?: string } | undefin
// ---------- 展示与导航 ----------
function getLedgerTitle(item: PointLedger): string {
if (item.changeType === "EARN") {
return "登记事件获得积分";
}
if (item.changeType === "REVOKE") {
return "删除登记记录撤销积分";
}
return "人工调整积分";
}
// function getLedgerTitle(item: PointLedger): string {
// if (item.changeType === "EARN") {
// return "登记事件获得积分";
// }
// if (item.changeType === "REVOKE") {
// return "删除登记记录撤销积分";
// }
// return "人工调整积分";
// }
function getLedgerDescription(item: PointLedger): string {
const parts = [`余额 ${item.balanceBefore ?? 0} → ${item.balanceAfter ?? 0}`];
if (item.remark) {
parts.push(item.remark);
}
if (item.eventRecordId) {
parts.push(`记录 ${item.eventRecordId}`);
}
return parts.join("|");
}
function getLedgerTime(item: PointLedger): string {
return item.occurredAt ? Time.toPassedDateTime(item.occurredAt) : "未知时间";
}
function getLedgerDelta(item: PointLedger): string {
const value = item.pointDelta ?? 0;
return `${0 < value ? "+" : ""}${value} 分`;
return `${0 < value ? "+" : ""}${value}`;
}
function getLedgerShortType(item: PointLedger): string {
@@ -421,26 +523,43 @@ onMounted(async () => {
<style scoped lang="less">
.point-ledger-list {
padding: calc(var(--app-nav-height) + 1rem) 0 1rem;
.filters {
gap: .75rem;
padding: 0 1rem .75rem;
gap: 1rem;
display: flex;
padding: calc(var(--app-nav-height) + 7rem) 0 1rem;
flex-direction: column;
.customer-filter {
.header {
top: var(--app-nav-height);
width: 100%;
z-index: 10;
padding: .5rem 1rem 0;
position: fixed;
.buttons {
gap: 1rem;
display: flex;
.selector {
flex: 1;
min-width: 0;
}
}
.filters {
margin: .5rem -1rem 0;
}
.scanner-wrapper {
margin-top: 1rem;
}
:deep(.t-dropdown-menu) {
margin: 0 -1rem;
}
background: transparent;
}
.date-range {
gap: .5rem;
padding: 0 1rem .75rem;
padding: .5rem 0 .75rem;
display: flex;
align-items: center;
@@ -453,48 +572,32 @@ onMounted(async () => {
color: #52607a;
}
}
}
.customer-summary {
.list {
.item {
.avatar {
width: 3.5rem;
height: 3.5rem;
}
.title {
gap: .5rem;
margin: 0 1rem 1rem;
padding: .875rem 1rem;
display: grid;
background: rgba(255, 255, 255, .92);
box-shadow: var(--td-shadow-1);
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
border-radius: 1rem;
.customer-name {
font-weight: 700;
}
.customer-id {
color: #52607a;
font-size: .75rem;
}
.t-button {
grid-column: 2;
grid-row: 1 / 3;
}
}
.ledger-list {
margin: 0 1rem;
.ledger-item {
--td-cell-horizontal-padding: .75rem;
display: flex;
.type-icon {
width: 2rem;
height: 2rem;
display: flex;
border-radius: .75rem;
align-items: center;
font-size: .875rem;
font-size: 1rem;
font-weight: 700;
justify-content: center;
border-radius: .5rem;
}
}
.description {
font-size: .75rem;
line-height: 1.25;
}
.delta {
@@ -503,26 +606,36 @@ onMounted(async () => {
.increase {
color: #059669;
background: #d1fae5;
}
.decrease {
color: #dc2626;
background: #fee2e2;
}
}
}
.empty {
color: #52607a;
padding: 2rem 1rem;
text-align: center;
.actions {
left: 1rem;
right: 1rem;
gap: .75rem;
z-index: 20;
bottom: calc(.75rem + env(safe-area-inset-bottom, 0));
display: flex;
position: fixed;
.t-button {
flex: 1;
}
}
}
:global(html.theme-dark) {
.point-ledger-list {
.date-range span,
.selector {
background: #10161f;
}
.header .date-range span,
.customer-id,
.empty {
color: #94a3b8;
@@ -538,6 +651,15 @@ onMounted(async () => {
background: rgba(6, 78, 59, .56);
}
.name {
color: #e2e8f0;
}
.code,
.remark {
color: #94a3b8;
}
.decrease {
color: #fca5a5;
background: rgba(127, 29, 29, .56);
+2
View File
@@ -1,4 +1,5 @@
import type { BasePage, Model } from "timi-web";
import type { Customer } from "@/types/Customer";
export type PointRuleStatus = "DRAFT" | "ACTIVE" | "INACTIVE" | "DELETED";
export type PointRuleTriggerType = "EVERY_TIME" | "FIRST_TIME" | "AFTER_COUNT_EVERY_TIME" | "SOURCE_VALUE";
@@ -41,6 +42,7 @@ export type PointLedger = {
remark?: string;
operatorUserId?: string;
occurredAt?: number;
customer?: Customer;
} & Model;
export type PointLedgerPage = {