281 lines
6.3 KiB
Vue
281 lines
6.3 KiB
Vue
<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>
|