fix ConsumptionRecordList.vue style

This commit is contained in:
Timi
2026-09-07 21:46:08 +08:00
parent c884d87997
commit 904d226943
2 changed files with 173 additions and 68 deletions
+154 -65
View File
@@ -2,29 +2,37 @@
<div v-can="permissionCodes.salesOrder.read" class="consumption-record-list">
<div class="filters">
<t-input
class="keyword"
:value="customerDisplayText"
class="customer-input"
readonly
borderless
placeholder="选择或扫码客户"
@click="customerSelectorVisible = true"
>
<template #suffix-icon>
<t-button size="small" variant="text" :content="scannerVisible ? '关闭扫码' : '扫码'" @click.stop="toggleScanner" />
<t-button
size="extra-small"
theme="primary"
variant="outline"
:content="scannerVisible ? '关闭扫码' : '扫码'"
@click.stop="toggleScanner"
/>
</template>
</t-input>
<t-dropdown-menu>
<t-dropdown-item v-model="sourceType" label="消费来源" :options="sourceTypeOptions" @change="reloadRecords" />
<t-dropdown-item v-model="status" label="消费状态" :options="statusOptions" @change="reloadRecords" />
<t-dropdown-menu class="menus">
<t-dropdown-item
v-model="sourceType"
label="消费来源"
:options="sourceTypeOptions"
@change="reloadRecords"
/>
<t-dropdown-item
v-model="status"
label="消费状态"
:options="statusOptions"
@change="reloadRecords"
/>
</t-dropdown-menu>
<code-scanner
v-if="scannerVisible"
visible
:status-text="scanStatus"
active-text="正在识别客户编码,请将条码或二维码移入取景框。"
@decode="handleDecodedText"
@status-change="handleScanStatusChange"
/>
</div>
<t-pull-down-refresh
v-model="isRefreshing"
@@ -33,33 +41,64 @@
@scrolltolower="loadRecords"
>
<div class="list-wrap">
<div v-if="scannerVisible" class="scanner-wrap">
<code-scanner
visible
:status-text="scanStatus"
active-text="正在识别客户编码,请将条码或二维码移入取景框。"
@decode="handleDecodedText"
@status-change="handleScanStatusChange"
/>
</div>
<t-cell-group class="summary" theme="card">
<t-cell title="记录数" :note="`共 ${total} 条`" />
<t-cell title="消费金额合计" :note="`¥${formatPackageAmount(amountTotal)}`" />
</t-cell-group>
<t-cell-group v-if="recordList.length" theme="card">
<t-cell
class="item"
v-for="(item, index) in recordList"
:key="item.id"
v-entrance-transition="index * 30"
:title="getRecordTitle(item)"
:note="getRecordNote(item)"
:arrow="canOpenSource(item)"
v-entrance-transition="index * 30"
@click="openSource(item)"
>
<template #description>
<div class="description">
<span v-text="getCustomerText(item)" />
<span v-text="getEffectiveAtText(item)" />
<template #leftIcon>
<t-image
v-if="getCustomerPhotoURL(item.customer, 64)"
:src="getCustomerPhotoURL(item.customer, 64)"
:alt="getCustomerText(item)"
fit="cover"
class="avatar"
/>
<div v-else class="avatar fallback" v-text="getCustomerText(item).slice(0, 1)" />
</template>
<template #title>
<div class="title">
<span class="customer" v-text="getCustomerText(item)" />
<t-tag
class="value"
size="small"
:theme="item.sourceType === 'LEGACY_EVENT' ? 'default' : 'primary'"
variant="light"
:content="getRecordTitle(item)"
/>
</div>
</template>
<template #rightIcon>
<div class="right">
<t-tag :content="getSourceText(item.sourceType)" size="small" theme="primary" variant="light-outline" />
<t-tag :content="getStatusText(item.status)" size="small" :theme="item.status === 'ACTIVE' ? 'success' : 'danger'" variant="light-outline" />
<strong v-text="`¥${formatPackageAmount(item.amount)}`" />
<template #description>
<div class="description">
<t-tag
size="small"
:theme="item.status === 'ACTIVE' ? 'success' : 'danger'"
variant="light-outline"
:content="getStatusText(item.status)"
/>
<pass-time-label class="time" :timestamp="item.effectiveAt" underline />
</div>
</template>
<template #note>
<strong class="red" v-text="`¥${formatPackageAmount(item.amount)}`" />
</template>
</t-cell>
</t-cell-group>
<div v-else class="center-tips" v-text="isLoading ? '正在加载消费记录' : '暂无消费记录'" />
@@ -76,7 +115,7 @@
<script setup lang="ts">
import type { DropdownValue } from "tdesign-mobile-vue";
import { Time } from "timi-web";
import { PassTimeLabel } from "timi-web";
import { useUserStore } from "timi-web-api";
import ConsumptionRecordAPI from "@/api/ConsumptionRecordAPI";
import CustomerAPI from "@/api/CustomerAPI";
@@ -84,8 +123,14 @@ import CodeScanner from "@/components/CodeScanner.vue";
import CustomerSelector from "@/components/CustomerSelector.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { Customer } from "@/types/Customer";
import type { ConsumptionRecord, ConsumptionRecordPage, ConsumptionRecordSourceType, ConsumptionRecordStatus } from "@/types/ConsumptionRecord";
import type {
ConsumptionRecord,
ConsumptionRecordPage,
ConsumptionRecordSourceType,
ConsumptionRecordStatus
} from "@/types/ConsumptionRecord";
import { getErrorMessage } from "@/utils/error";
import { getCustomerPhotoURL } from "@/utils/customer";
import { Message } from "@/utils/message";
import { formatPackageAmount } from "@/utils/package";
import { permissionCodes } from "@/utils/permission";
@@ -220,30 +265,15 @@ async function loadRecords(): Promise<void> {
function getRecordTitle(record: ConsumptionRecord): string {
if (record.sourceType === "LEGACY_EVENT") {
return record.sourcePayload?.eventName || "历史消费事件";
return record.sourcePayload?.eventName || "消费事件";
}
return record.orderType === "PACKAGE" ? "套餐订单消费" : "产品订单消费";
}
function getRecordNote(record: ConsumptionRecord): string {
if (record.status === "REVOKED") {
return record.revokeReason || "消费事实已失效";
}
return record.sourceType === "LEGACY_EVENT" ? "查看原始登记记录" : "查看销售订单";
return record.orderType === "PACKAGE" ? "套餐订单" : "产品订单";
}
function getCustomerText(record: ConsumptionRecord): string {
return record.customer?.name || record.customer?.code || record.customerId || "未知客户";
}
function getEffectiveAtText(record: ConsumptionRecord): string {
return record.effectiveAt ? Time.toDateTime(record.effectiveAt) : "未知生效时间";
}
function getSourceText(value?: ConsumptionRecordSourceType): string {
return value === "LEGACY_EVENT" ? "历史消费" : "销售订单";
}
function getStatusText(value?: ConsumptionRecordStatus): string {
return value === "REVOKED" ? "已失效" : "有效";
}
@@ -280,44 +310,103 @@ onMounted(async () => {
.filters {
top: var(--app-nav-height);
z-index: 1;
left: 0;
right: 0;
z-index: 10;
display: flex;
padding: .75rem 1rem;
position: sticky;
background: var(--td-bg-color-page);
background: #F5F5F8;
box-shadow: var(--td-shadow-1);
flex-direction: column;
.customer-input {
--td-input-vertical-padding: .5rem 1rem;
.keyword {
--td-input-vertical-padding: 8px 14px;
margin: .5rem 1rem;
background: var(--td-bg-color-container);
border: 1px solid var(--td-component-border);
z-index: 1;
background: var(--td-bg-color-container);
border-radius: 1rem;
}
.menus {
--td-spacer-1: 6px;
width: 100%;
overflow: hidden;
margin-top: -1rem;
padding-top: .8rem;
background: #fff;
border: 1px solid rgba(148, 163, 184, .18);
border-radius: 0 0 1rem 1rem;
&:after {
display: none;
}
:deep(.t-dropdown-menu__item) {
min-width: 0;
}
:deep(.t-dropdown-menu__title) {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
.list-wrap {
padding: 1rem 0;
gap: 1rem;
display: flex;
padding-top: 1rem;
flex-direction: column;
.description {
gap: .5rem;
display: flex;
color: var(--td-text-color-secondary);
.scanner-wrap {
margin: 0 1rem;
}
.summary {
margin-bottom: 1rem;
}
.item {
padding: 10px;
.right {
gap: .35rem;
display: flex;
align-items: flex-end;
flex-direction: column;
.avatar {
width: 3.5rem;
height: 3.5rem;
}
strong {
color: var(--td-error-color);
.title {
gap: .5rem;
display: flex;
align-items: center;
.customer {
font-size: .875rem;
}
}
.description {
gap: .5rem;
display: flex;
align-items: center;
.time {
font-size: .75rem;
}
}
}
}
}
:deep(.consumption-record-list .avatar .t-image__img) {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: inherit;
}
:global(html.theme-dark) .consumption-record-list {
.fallback {
background: #115e59;
}
}
</style>