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
+19 -3
View File
@@ -1,10 +1,26 @@
<template> <template>
<div v-can="permissionCodes.product.read" class="product-list"> <div v-can="permissionCodes.product.read" class="product-list">
<div class="filters bg-white"> <div class="filters bg-white">
<t-input v-model.trim="keyword" class="keyword" type="search" borderless clearable placeholder="搜索产品名称或编码" /> <t-input
v-model="keyword"
class="keyword"
type="search"
borderless
placeholder="搜索产品名称或编码"
/>
<t-dropdown-menu class="menus"> <t-dropdown-menu class="menus">
<t-dropdown-item v-model="status" label="状态" :options="statusOptions" @change="selectStatus" /> <t-dropdown-item
<t-dropdown-item v-model="productType" label="类型" :options="productTypeOptions" @change="selectProductType" /> v-model="status"
label="状态"
:options="statusOptions"
@change="selectStatus"
/>
<t-dropdown-item
v-model="productType"
label="类型"
:options="productTypeOptions"
@change="selectProductType"
/>
<t-dropdown-item <t-dropdown-item
:model-value="categoryIdList" :model-value="categoryIdList"
label="分类" label="分类"
+154 -65
View File
@@ -2,29 +2,37 @@
<div v-can="permissionCodes.salesOrder.read" class="consumption-record-list"> <div v-can="permissionCodes.salesOrder.read" class="consumption-record-list">
<div class="filters"> <div class="filters">
<t-input <t-input
class="keyword"
:value="customerDisplayText" :value="customerDisplayText"
class="customer-input"
readonly readonly
borderless borderless
placeholder="选择或扫码客户" placeholder="选择或扫码客户"
@click="customerSelectorVisible = true" @click="customerSelectorVisible = true"
> >
<template #suffix-icon> <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> </template>
</t-input> </t-input>
<t-dropdown-menu> <t-dropdown-menu class="menus">
<t-dropdown-item v-model="sourceType" label="消费来源" :options="sourceTypeOptions" @change="reloadRecords" /> <t-dropdown-item
<t-dropdown-item v-model="status" label="消费状态" :options="statusOptions" @change="reloadRecords" /> v-model="sourceType"
</t-dropdown-menu> label="消费来源"
<code-scanner :options="sourceTypeOptions"
v-if="scannerVisible" @change="reloadRecords"
visible
:status-text="scanStatus"
active-text="正在识别客户编码,请将条码或二维码移入取景框。"
@decode="handleDecodedText"
@status-change="handleScanStatusChange"
/> />
<t-dropdown-item
v-model="status"
label="消费状态"
:options="statusOptions"
@change="reloadRecords"
/>
</t-dropdown-menu>
</div> </div>
<t-pull-down-refresh <t-pull-down-refresh
v-model="isRefreshing" v-model="isRefreshing"
@@ -33,33 +41,64 @@
@scrolltolower="loadRecords" @scrolltolower="loadRecords"
> >
<div class="list-wrap"> <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-group class="summary" theme="card">
<t-cell title="记录数" :note="`共 ${total} 条`" /> <t-cell title="记录数" :note="`共 ${total} 条`" />
<t-cell title="消费金额合计" :note="`¥${formatPackageAmount(amountTotal)}`" /> <t-cell title="消费金额合计" :note="`¥${formatPackageAmount(amountTotal)}`" />
</t-cell-group> </t-cell-group>
<t-cell-group v-if="recordList.length" theme="card"> <t-cell-group v-if="recordList.length" theme="card">
<t-cell <t-cell
class="item"
v-for="(item, index) in recordList" v-for="(item, index) in recordList"
:key="item.id" :key="item.id"
v-entrance-transition="index * 30"
:title="getRecordTitle(item)"
:note="getRecordNote(item)"
:arrow="canOpenSource(item)" :arrow="canOpenSource(item)"
v-entrance-transition="index * 30"
@click="openSource(item)" @click="openSource(item)"
> >
<template #description> <template #leftIcon>
<div class="description"> <t-image
<span v-text="getCustomerText(item)" /> v-if="getCustomerPhotoURL(item.customer, 64)"
<span v-text="getEffectiveAtText(item)" /> :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> </div>
</template> </template>
<template #rightIcon> <template #description>
<div class="right"> <div class="description">
<t-tag :content="getSourceText(item.sourceType)" size="small" theme="primary" variant="light-outline" /> <t-tag
<t-tag :content="getStatusText(item.status)" size="small" :theme="item.status === 'ACTIVE' ? 'success' : 'danger'" variant="light-outline" /> size="small"
<strong v-text="`¥${formatPackageAmount(item.amount)}`" /> :theme="item.status === 'ACTIVE' ? 'success' : 'danger'"
variant="light-outline"
:content="getStatusText(item.status)"
/>
<pass-time-label class="time" :timestamp="item.effectiveAt" underline />
</div> </div>
</template> </template>
<template #note>
<strong class="red" v-text="`¥${formatPackageAmount(item.amount)}`" />
</template>
</t-cell> </t-cell>
</t-cell-group> </t-cell-group>
<div v-else class="center-tips" v-text="isLoading ? '正在加载消费记录' : '暂无消费记录'" /> <div v-else class="center-tips" v-text="isLoading ? '正在加载消费记录' : '暂无消费记录'" />
@@ -76,7 +115,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { DropdownValue } from "tdesign-mobile-vue"; import type { DropdownValue } from "tdesign-mobile-vue";
import { Time } from "timi-web"; import { PassTimeLabel } from "timi-web";
import { useUserStore } from "timi-web-api"; import { useUserStore } from "timi-web-api";
import ConsumptionRecordAPI from "@/api/ConsumptionRecordAPI"; import ConsumptionRecordAPI from "@/api/ConsumptionRecordAPI";
import CustomerAPI from "@/api/CustomerAPI"; import CustomerAPI from "@/api/CustomerAPI";
@@ -84,8 +123,14 @@ import CodeScanner from "@/components/CodeScanner.vue";
import CustomerSelector from "@/components/CustomerSelector.vue"; import CustomerSelector from "@/components/CustomerSelector.vue";
import { useNavBarStore } from "@/store/navBarStore"; import { useNavBarStore } from "@/store/navBarStore";
import type { Customer } from "@/types/Customer"; 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 { getErrorMessage } from "@/utils/error";
import { getCustomerPhotoURL } from "@/utils/customer";
import { Message } from "@/utils/message"; import { Message } from "@/utils/message";
import { formatPackageAmount } from "@/utils/package"; import { formatPackageAmount } from "@/utils/package";
import { permissionCodes } from "@/utils/permission"; import { permissionCodes } from "@/utils/permission";
@@ -220,30 +265,15 @@ async function loadRecords(): Promise<void> {
function getRecordTitle(record: ConsumptionRecord): string { function getRecordTitle(record: ConsumptionRecord): string {
if (record.sourceType === "LEGACY_EVENT") { if (record.sourceType === "LEGACY_EVENT") {
return record.sourcePayload?.eventName || "历史消费事件"; return record.sourcePayload?.eventName || "消费事件";
} }
return record.orderType === "PACKAGE" ? "套餐订单消费" : "产品订单消费"; return record.orderType === "PACKAGE" ? "套餐订单" : "产品订单";
}
function getRecordNote(record: ConsumptionRecord): string {
if (record.status === "REVOKED") {
return record.revokeReason || "消费事实已失效";
}
return record.sourceType === "LEGACY_EVENT" ? "查看原始登记记录" : "查看销售订单";
} }
function getCustomerText(record: ConsumptionRecord): string { function getCustomerText(record: ConsumptionRecord): string {
return record.customer?.name || record.customer?.code || record.customerId || "未知客户"; 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 { function getStatusText(value?: ConsumptionRecordStatus): string {
return value === "REVOKED" ? "已失效" : "有效"; return value === "REVOKED" ? "已失效" : "有效";
} }
@@ -280,44 +310,103 @@ onMounted(async () => {
.filters { .filters {
top: var(--app-nav-height); top: var(--app-nav-height);
z-index: 1; left: 0;
right: 0;
z-index: 10;
display: flex;
padding: .75rem 1rem;
position: sticky; position: sticky;
background: var(--td-bg-color-page); background: #F5F5F8;
box-shadow: var(--td-shadow-1); box-shadow: var(--td-shadow-1);
flex-direction: column;
.customer-input { .keyword {
--td-input-vertical-padding: .5rem 1rem; --td-input-vertical-padding: 8px 14px;
margin: .5rem 1rem;
background: var(--td-bg-color-container);
border: 1px solid var(--td-component-border); border: 1px solid var(--td-component-border);
z-index: 1;
background: var(--td-bg-color-container);
border-radius: 1rem; 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 { .list-wrap {
padding: 1rem 0; gap: 1rem;
display: flex;
padding-top: 1rem;
flex-direction: column;
.scanner-wrap {
margin: 0 1rem;
}
.item {
padding: 10px;
.avatar {
width: 3.5rem;
height: 3.5rem;
}
.title {
gap: .5rem;
display: flex;
align-items: center;
.customer {
font-size: .875rem;
}
}
.description { .description {
gap: .5rem; gap: .5rem;
display: flex; display: flex;
color: var(--td-text-color-secondary); align-items: center;
.time {
font-size: .75rem;
} }
.summary {
margin-bottom: 1rem;
}
.right {
gap: .35rem;
display: flex;
align-items: flex-end;
flex-direction: column;
strong {
color: var(--td-error-color);
} }
} }
} }
} }
: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> </style>