Files
gao-customer-h5/src/pages/fulfillment/components/OrderFulfillmentListCell.vue
T
2026-09-07 15:34:02 +08:00

281 lines
6.3 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="fulfillment-cell">
<div class="header">
<div class="left">
<t-image
v-if="photoURL"
:src="photoURL"
:alt="customerName"
fit="cover"
class="avatar"
/>
<div v-else class="avatar fallback" v-text="avatarText" />
</div>
<div class="main">
<div class="customer-status">
<strong class="customer" v-text="customerName" />
<t-tag
:content="statusText"
size="small"
variant="light-outline"
:theme="statusTheme"
/>
</div>
<div>
<pass-time-label class="time" :timestamp="fulfillment.deliveredAt" underline />
</div>
</div>
</div>
<t-divider />
<div class="product">
<div class="items">
<div
v-for="(item, index) in itemList"
:key="item.id || `${item.skuId}-${item.skuUnitId}-${index}`"
class="item"
>
<div class="item-title">
<strong v-text="item.productNameSnapshot || '未命名产品'" />
<span v-text="`× ${getPackageQuantityText(item.quantity)}`" />
</div>
<product-sku-snapshot-tags
:snapshot="item.skuSpecSnapshot"
:fallback="item.skuNameSnapshot || item.specSummarySnapshot || '默认 SKU'"
:unit="item.unitSnapshot"
/>
</div>
<t-tag v-if="!itemList.length" content="暂无产品" />
</div>
</div>
<div class="summary" v-text="productSummaryText" />
<t-divider />
<div class="people-actions">
<div class="person">
<span class="label">操作人</span>
<strong v-text="operatorName" />
</div>
<div v-if="showVerifier" class="person">
<span class="label">核销人</span>
<strong v-text="verifierName" />
</div>
<div class="actions">
<t-button
theme="primary"
size="extra-small"
content="详情"
@click.stop="emit('detail')"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { PassTimeLabel } from "timi-web";
import ProductSkuSnapshotTags from "@/components/ProductSkuSnapshotTags.vue";
import type { OrderFulfillment } from "@/types/Fulfillment";
import { getCustomerPhotoURL } from "@/utils/customer";
import { getPackageQuantityText } from "@/utils/package";
import { getFulfillmentStatusText, getStatusTheme } from "@/utils/salesOrder";
defineOptions({ name: "OrderFulfillmentListCell" });
const props = defineProps<{
fulfillment: OrderFulfillment;
}>();
const emit = defineEmits<{
detail: [];
}>();
const itemList = computed(() => props.fulfillment.itemList || []);
const customerName = computed(() => props.fulfillment.customer?.name || props.fulfillment.customer?.code || props.fulfillment.customerId || "未知客户");
const avatarText = computed(() => customerName.value.slice(0, 1) || "客");
const photoURL = computed(() => getCustomerPhotoURL(props.fulfillment.customer, 64));
const statusText = computed(() => getFulfillmentStatusText(props.fulfillment.status));
const statusTheme = computed(() => getStatusTheme(props.fulfillment.status));
const operatorName = computed(() => getUserName(props.fulfillment.operator, props.fulfillment.operatorUserId, "未知操作人"));
const verifierName = computed(() => getUserName(props.fulfillment.verifier, props.fulfillment.verifierUserId, "未知核销人"));
const showVerifier = computed(() => ["VERIFIED", "REVOKED"].includes(props.fulfillment.status || ""));
const totalProductQuantity = computed(() => {
let total = 0;
for (const item of itemList.value) {
const quantity = Number(item.quantity || 0);
if (Number.isFinite(quantity)) {
total += quantity;
}
}
return getPackageQuantityText(total.toFixed(3));
});
const productSummaryText = computed(() => `共 ${itemList.value.length} 项 · 总数量 ${totalProductQuantity.value}`);
function getUserName(user: OrderFulfillment["operator"], userId: string | undefined, fallback: string): string {
return user?.nick || user?.name || userId || fallback;
}
</script>
<style scoped lang="less">
.fulfillment-cell {
gap: .5rem;
padding: 10px;
display: flex;
background: var(--td-bg-color-container);
flex-direction: column;
border-radius: 1rem;
.header {
gap: .5rem;
display: flex;
.left {
.avatar {
width: 3.5rem;
height: 3.5rem;
}
}
.main {
gap: .5rem;
flex: 1;
display: flex;
min-width: 0;
flex-direction: column;
.customer-status {
gap: .5rem;
display: flex;
align-items: center;
justify-content: space-between;
.customer {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.time {
color: var(--td-text-color-secondary);
font-size: .75rem;
}
}
}
.product {
gap: .5rem;
display: flex;
align-items: flex-start;
.items {
flex: 1;
gap: .35rem;
display: flex;
min-width: 0;
flex-wrap: nowrap;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
scrollbar-width: none;
-webkit-overflow-scrolling: touch;
.item {
gap: .375rem;
display: flex;
min-width: 9rem;
flex: 0 0 auto;
flex-direction: column;
white-space: normal;
.item-title {
gap: .375rem;
display: flex;
align-items: baseline;
strong {
max-width: 9rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
span {
color: var(--td-text-color-secondary);
font-size: .75rem;
white-space: nowrap;
}
}
}
:deep(.t-tag) {
flex: 0 0 auto;
}
&::-webkit-scrollbar {
display: none;
}
}
}
.summary {
color: var(--td-text-color-secondary);
font-size: .75rem;
text-align: right;
line-height: 1.5;
}
.people-actions {
gap: .75rem;
display: flex;
align-items: center;
justify-content: space-between;
.person {
gap: .25rem;
display: flex;
min-width: 0;
font-size: .75rem;
align-items: baseline;
.label {
color: var(--td-text-color-secondary);
flex: 0 0 auto;
}
strong {
max-width: 10rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.actions {
--td-button-border-radius: .5rem;
gap: .5rem;
display: flex;
justify-content: end;
.t-button {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
}
}
}
:deep(.fulfillment-cell .avatar .t-image__img) {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: inherit;
}
:global(html.theme-dark) .fulfillment-cell {
.fallback {
background: #115e59;
}
}
</style>