Files
gao-customer-h5/src/pages/salesOrder/components/SalesOrderSummary.vue
T
2026-09-19 01:21:55 +08:00

374 lines
8.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="sales-order-summary">
<customer-profile-card :customer="props.customer" />
<t-cell-group class="customer-order-summary" theme="card">
<t-cell title="订单总数" :note="getOrderSummaryCount()" />
<t-cell title="总金额">
<template #note>
<span class="red" v-text="getOrderSummaryAmount('saleAmount')" />
</template>
</t-cell>
<t-cell title="已收金额">
<template #note>
<span class="green" v-text="getOrderSummaryAmount('paidAmount')" />
</template>
</t-cell>
<t-cell title="待收金额">
<template #note>
<span class="orange" v-text="getOrderSummaryAmount('unpaidAmount')" />
</template>
</t-cell>
<t-cell class="products">
<template #title>
<div class="title">
<div>已购产品</div>
<div v-if="purchasedProductTagRows.length" class="product-tags">
<div class="rows">
<div v-for="(row, rowIndex) in purchasedProductTagRows" :key="rowIndex" class="row">
<t-tag
v-for="(product, productIndex) in row"
:key="`${getProductKey(product, productIndex)}-${rowIndex}`"
theme="primary"
variant="light-outline"
:content="getProductTagContent(product)"
/>
</div>
</div>
</div>
</div>
</template>
</t-cell>
<t-cell class="products">
<template #title>
<div class="title">
<div>可履约产品</div>
<div v-if="availableProductTagRows.length" class="product-tags">
<div class="rows">
<div v-for="(row, rowIndex) in availableProductTagRows" :key="rowIndex" class="row">
<t-tag
v-for="(product, productIndex) in row"
:key="`${getProductKey(product, productIndex)}-${rowIndex}`"
theme="warning"
variant="light-outline"
:content="getProductTagContent(product)"
/>
</div>
</div>
</div>
</div>
</template>
</t-cell>
</t-cell-group>
</div>
</template>
<script setup lang="ts">
import CustomerProductAPI from "@/api/CustomerProductAPI";
import SalesOrderAPI from "@/api/SalesOrderAPI";
import CustomerProfileCard from "@/components/CustomerProfileCard.vue";
import type { Customer } from "@/types/Customer";
import type { CustomerProduct, CustomerProductSummary } from "@/types/CustomerProduct";
import type { SalesOrderPage, SalesOrderSummary as SalesOrderSummaryData } from "@/types/SalesOrder";
import { getErrorMessage } from "@/utils/error";
import { formatPackageAmount, getPackageQuantityText } from "@/utils/package";
import { Message } from "@/utils/message";
defineOptions({ name: "SalesOrderSummary" });
type OrderSummaryAmountField = "saleAmount" | "paidAmount" | "unpaidAmount";
const props = defineProps<{
customer?: Customer;
}>();
const customerId = computed(() => props.customer?.id || "");
const summary = ref<SalesOrderSummaryData>();
const summaryLoading = ref(false);
const productSummary = ref<CustomerProduct>();
const productLoading = ref(false);
let loadToken = 0;
const maxProductTagRowCount = 2;
const purchasedProductTagRows = computed(() => {
const purchasedProducts = productSummary.value?.purchasedProducts ?? [];
return getProductTagRows(purchasedProducts);
});
const availableProductTagRows = computed(() => getProductTagRows(productSummary.value?.availableProducts));
function getProductTagRows(products?: CustomerProductSummary[]): CustomerProductSummary[][] {
if (!products?.length) {
return [];
}
const rowCount = Math.min(maxProductTagRowCount, products.length);
const rowList = Array.from({ length: rowCount }, () => [] as CustomerProductSummary[]);
products.forEach((product, index) => {
rowList[index % rowCount].push(product);
});
return rowList;
}
function getOrderSummaryCount(): string {
return summaryLoading.value ? "加载中" : String(summary.value?.orderCount ?? 0);
}
function getOrderSummaryAmount(field: OrderSummaryAmountField): string {
if (summaryLoading.value) {
return "加载中";
}
return `¥${formatPackageAmount(summary.value?.[field] ?? 0)}`;
}
function getProductKey(product: CustomerProductSummary, index: number): string {
return `${product.productIdSnapshot || product.productName || "product"}-${product.unit || "unit"}-${index}`;
}
function getProductQuantity(product: CustomerProductSummary): string {
return getPackageQuantityText(product.quantity);
}
function getProductTagContent(product: CustomerProductSummary): string {
return `${product.productName || "未命名产品"} × ${getProductQuantity(product)}${product.unit || "基础单位"}`;
}
async function loadOrderSummary(customerId: string, token: number): Promise<void> {
try {
const request: SalesOrderPage = { customerId, index: 0, size: 1 };
const result = await SalesOrderAPI.summary(request);
if (token === loadToken) {
summary.value = result;
}
} catch (error) {
if (token === loadToken) {
summary.value = undefined;
Message.error(getErrorMessage(error, "订单汇总加载失败"));
}
} finally {
if (token === loadToken) {
summaryLoading.value = false;
}
}
}
async function loadProductSummary(customerId: string, token: number): Promise<void> {
try {
const result = await CustomerProductAPI.list({ customerId, index: 0, size: 1 });
if (token === loadToken) {
productSummary.value = result.list?.[0];
}
} catch (error) {
if (token === loadToken) {
productSummary.value = undefined;
Message.error(getErrorMessage(error, "客户产品汇总加载失败"));
}
} finally {
if (token === loadToken) {
productLoading.value = false;
}
}
}
async function loadData(): Promise<void> {
const token = ++loadToken;
const currentCustomerId = customerId.value;
if (!currentCustomerId) {
summary.value = undefined;
productSummary.value = undefined;
summaryLoading.value = false;
productLoading.value = false;
return;
}
summaryLoading.value = true;
productLoading.value = true;
await Promise.all([
loadOrderSummary(currentCustomerId, token),
loadProductSummary(currentCustomerId, token)
]);
}
async function refresh(): Promise<void> {
await loadData();
}
defineExpose({ refresh });
watch(customerId, async () => {
await loadData();
}, { immediate: true });
</script>
<style scoped lang="less">
.sales-order-summary {
gap: 1rem;
display: flex;
min-width: 0;
flex-direction: column;
.customer-order-summary {
:deep(.t-cell-group) {
margin: 0;
}
}
.products {
.title,
:deep(.t-cell__title) {
width: 100%;
overflow: hidden;
min-width: 0;
max-width: 100%;
margin-right: 0;
}
.product-tags {
gap: .5rem;
width: 100%;
min-width: 0;
max-width: 100%;
display: flex;
padding: .25rem 0 .5rem;
overflow-x: auto;
overflow-y: hidden;
flex-wrap: nowrap;
touch-action: pan-x;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
&::-webkit-scrollbar {
display: none;
}
.rows {
gap: .25rem;
width: max-content;
display: flex;
flex-direction: column;
.row {
gap: .25rem;
display: flex;
white-space: nowrap;
}
}
}
}
}
.customer-product-summary {
min-width: 0;
:deep(.t-cell-group) {
margin: 0;
}
.summary-line {
min-width: 0;
:deep(.t-cell__title) {
min-width: 0;
width: 100%;
}
:deep(.t-cell__description) {
min-width: 0;
width: 100%;
max-width: 100%;
overflow: hidden;
}
.line-label {
color: var(--td-text-color-secondary);
font-weight: 600;
}
.available-label {
color: var(--td-success-color);
}
.product-scroll,
.product-tags {
gap: .5rem;
width: 100%;
min-width: 0;
max-width: 100%;
display: flex;
padding: .25rem 0 .5rem;
overflow-x: auto;
overflow-y: hidden;
flex-wrap: nowrap;
touch-action: pan-x;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
&::-webkit-scrollbar {
display: none;
}
.product {
gap: .25rem;
flex: 0 0 auto;
min-width: 7rem;
max-width: 12rem;
display: flex;
border: 1px solid var(--td-component-border);
padding: .5rem .625rem;
background: var(--td-bg-color-container-hover);
border-radius: .5rem;
flex-direction: column;
align-items: flex-start;
.product-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.quantity {
color: var(--td-text-color-primary);
font-size: .875rem;
}
.unit {
color: var(--td-text-color-secondary);
font-size: .75rem;
}
}
.available-product {
border-color: var(--td-success-color-3);
background: var(--td-success-color-1);
}
.rows {
gap: .5rem;
width: max-content;
display: flex;
flex-direction: column;
}
.row {
gap: .5rem;
display: flex;
white-space: nowrap;
}
:deep(.t-tag) {
flex: 0 0 auto;
}
}
.empty-products {
color: var(--td-text-color-placeholder);
padding: .25rem 0 .5rem;
}
}
}
@media (max-width: 42rem) {
.customer-order-summary {
.order-summary-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
}
</style>