fix OrderFulfillmentList.vue style

This commit is contained in:
Timi
2026-09-05 21:28:12 +08:00
parent 1bf987490c
commit 12285308dd
3 changed files with 412 additions and 49 deletions
@@ -0,0 +1,250 @@
<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">
<t-tag
v-for="(item, index) in itemList"
:key="item.id || `${item.productId}-${index}`"
:content="getProductText(item)"
theme="primary"
variant="light"
/>
<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 type { OrderFulfillment, OrderFulfillmentItem } 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 getProductText(item: OrderFulfillmentItem): string {
const productName = item.productNameSnapshot || "未命名产品";
const quantity = getPackageQuantityText(item.quantity);
const unit = item.unitSnapshot ? ` ${item.unitSnapshot}` : "";
return `${productName} × ${quantity}${unit}`;
}
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;
: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>