add product sku info

This commit is contained in:
Timi
2026-09-23 00:14:01 +08:00
parent dbf0b2e299
commit 23fc2c3577
3 changed files with 221 additions and 127 deletions
+6 -4
View File
@@ -55,8 +55,8 @@
</div>
</template>
</t-cell>
<t-cell v-if="0 < Number(stock.reservedQuantity || 0)" title="查看预占流水" arrow @click="openReservedRecordList" />
<t-cell title="查看该产品 SKU 流水" arrow @click="openRecordList" />
<t-cell title="查看库存流水" arrow @click="openRecordList" />
<t-cell v-if="0 < Number(stock.reservedQuantity || 0)" title="查看库存预占流水" arrow @click="openReservedRecordList" />
</t-cell-group>
<card-container
v-if="canOperate && sku?.status === 'ACTIVE' && sku.stockEnabled"
@@ -199,11 +199,12 @@ async function loadAll(): Promise<void> {
}
async function openRecordList(): Promise<void> {
if (product.value?.id) {
if (product.value?.id && sku.value?.id) {
await router.push({
name: "ProductStockRecordList",
query: {
productId: product.value.id,
skuId: sku.value.id,
transitionDepth: String(getCurrentRouteDepth() + 1)
}
});
@@ -211,11 +212,12 @@ async function openRecordList(): Promise<void> {
}
async function openReservedRecordList(): Promise<void> {
if (product.value?.id) {
if (product.value?.id && sku.value?.id) {
await router.push({
name: "ProductStockRecordList",
query: {
productId: product.value.id,
skuId: sku.value.id,
operationType: "ORDER_RESERVE",
activeReservationOnly: "true",
transitionDepth: String(getCurrentRouteDepth() + 1)
+214 -123
View File
@@ -20,70 +20,103 @@
@refresh="refresh"
@scrolltolower="loadMore"
>
<t-cell-group v-if="recordList.length" class="list empty-cell-title-margin" theme="card">
<t-cell
v-for="(record, index) in recordList"
:key="record.id || `${record.occurredAt}-${record.requestNo}-${index}`"
class="item"
:arrow="!!record.id"
:hover="!!record.id"
v-entrance-transition="index * 30"
@click="toDetail(record)"
>
<template #leftIcon>
<t-image
v-if="getCustomerPhotoURL(record.customer, 64)"
:src="getCustomerPhotoURL(record.customer, 64)"
:alt="getCustomerText(record)"
fit="cover"
class="avatar"
/>
<div
v-else-if="record.customer"
class="avatar fallback"
v-text="getCustomerText(record).slice(0, 1)"
/>
<t-image
v-else-if="record.operator"
:src="getOperatorAvatar(record)"
:alt="getOperatorText(record)"
fit="cover"
class="avatar"
/>
<span v-else class="icon" :class="getDeltaClass(record.onHandDelta)">
<div class="list-wrap">
<t-cell-group v-if="filteredProduct && filteredSku" theme="card">
<t-cell class="sku">
<template #leftIcon>
<t-image
v-if="filteredSkuPhotoURL"
class="avatar"
:src="filteredSkuPhotoURL"
:alt="filteredProduct.name || '产品图片'"
fit="cover"
/>
<div
v-else
class="fallback"
v-text="(filteredProduct.name || filteredProduct.code || '产').slice(0, 1)"
/>
</template>
<template #title>
<div class="title">
<strong class="text-bold clip-text" v-text="filteredProduct.name || '未命名产品'" />
<span class="clip-text" v-text="filteredSku.specSummary || filteredSku.name || '默认 SKU'" />
</div>
</template>
<template #note>
<t-tag
variant="light-outline"
theme="primary"
:content="getProductTypeText(filteredProduct.productType)"
/>
</template>
</t-cell>
</t-cell-group>
<t-cell-group v-if="recordList.length" class="list empty-cell-title-margin" theme="card">
<t-cell
v-for="(record, index) in recordList"
:key="record.id || `${record.occurredAt}-${record.requestNo}-${index}`"
class="item"
:arrow="!!record.id"
:hover="!!record.id"
v-entrance-transition="index * 30"
@click="toDetail(record)"
>
<template #leftIcon>
<t-image
v-if="getCustomerPhotoURL(record.customer, 64)"
:src="getCustomerPhotoURL(record.customer, 64)"
:alt="getCustomerText(record)"
fit="cover"
class="avatar"
/>
<div
v-else-if="record.customer"
class="avatar fallback"
v-text="getCustomerText(record).slice(0, 1)"
/>
<t-image
v-else-if="record.operator"
:src="getOperatorAvatar(record)"
:alt="getOperatorText(record)"
fit="cover"
class="avatar"
/>
<span v-else class="icon" :class="getDeltaClass(record.onHandDelta)">
<t-icon :name="getOperationIcon(record.operationType)" size="1.25rem" />
</span>
</template>
<template #title>
<div class="title">
<span v-if="record.customer" class="actor" v-text="getCustomerText(record)" />
<span v-else-if="record.operator" class="actor" v-text="getOperatorText(record)" />
<strong class="operation" v-text="getStockOperationText(record.operationType)" />
<strong class="product-name" v-text="record.productNameSnapshot || '未命名产品'" />
<span
v-if="record.quantity !== undefined && record.quantity !== null && record.quantity !== ''"
class="quantity"
v-text="getStockQuantityText(record)"
/>
</div>
</template>
<template #description>
<div class="description">
<pass-time-label :timestamp="record.occurredAt" underline />
<div class="delta">
<span>库存:</span>
</template>
<template #title>
<div class="title">
<span v-if="record.customer" class="actor" v-text="getCustomerText(record)" />
<span v-else-if="record.operator" class="actor" v-text="getOperatorText(record)" />
<strong class="operation" v-text="getStockOperationText(record.operationType)" />
<strong class="product-name" v-text="record.productNameSnapshot || '未命名产品'" />
<span
class="value"
:class="getDeltaClass(record.onHandDelta)"
v-text="getDeltaText(record.onHandDelta)"
v-if="record.quantity !== undefined && record.quantity !== null && record.quantity !== ''"
class="quantity"
v-text="getStockQuantityText(record)"
/>
</div>
</div>
</template>
</t-cell>
</t-cell-group>
<div v-else class="center-tips" v-text="emptyText" />
<div v-if="recordList.length" class="center-tips" v-text="loadStateText" />
</template>
<template #description>
<div class="description">
<pass-time-label :timestamp="record.occurredAt" underline />
<div class="delta">
<span>库存:</span>
<span
class="value"
:class="getDeltaClass(record.onHandDelta)"
v-text="getDeltaText(record.onHandDelta)"
/>
</div>
</div>
</template>
</t-cell>
</t-cell-group>
<div v-else class="center-tips" v-text="emptyText" />
<div v-if="recordList.length" class="center-tips" v-text="loadStateText" />
</div>
</t-pull-down-refresh>
</div>
</template>
@@ -94,9 +127,9 @@ import { PassTimeLabel } from "timi-web";
import { UserAPI } from "timi-web-api";
import ProductAPI from "@/api/ProductAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { StockOperationType, StockRecord, StockRecordDirection } from "@/types/Product";
import type { Product, ProductSku, StockOperationType, StockRecord, StockRecordDirection } from "@/types/Product";
import { getErrorMessage } from "@/utils/error";
import { getProductQuantityText, getStockOperationText } from "@/utils/product";
import { getProductPhotoURL, getProductQuantityText, getProductTypeText, getStockOperationText } from "@/utils/product";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
import { getCustomerPhotoURL } from "@/utils/customer";
@@ -118,6 +151,12 @@ const loading = ref(false);
const isRefreshing = ref(false);
const noMore = ref(false);
const productId = computed(() => getQueryText("productId"));
const skuId = computed(() => getQueryText("skuId"));
const filteredProduct = ref<Product>();
const filteredSku = ref<ProductSku>();
const filteredSkuUnitId = ref("");
const filteredSkuUnitList = computed(() => (filteredSku.value?.unitList || []).filter(unit => !!unit.id));
const filteredSkuPhotoURL = computed(() => filteredSku.value ? getProductPhotoURL(filteredSku.value) || (filteredProduct.value ? getProductPhotoURL(filteredProduct.value) : "") : "");
const activeReservationRequested = getQueryText("activeReservationOnly") === "true";
const operationType = ref<OperationFilter>(getQueryText("operationType") as OperationFilter || (activeReservationRequested ? "ORDER_RESERVE" : "ALL"));
const direction = ref<DirectionFilter>(getQueryText("direction") as DirectionFilter || "ALL");
@@ -132,7 +171,7 @@ const emptyText = computed(() => {
if (activeReservationOnly.value) {
return "暂无有效的预占流水";
}
return keyword.value.trim() || productId.value || operationType.value !== "ALL" || direction.value !== "ALL" ? "未找到匹配的库存流水" : "暂无库存流水";
return keyword.value.trim() || productId.value || skuId.value || operationType.value !== "ALL" || direction.value !== "ALL" ? "未找到匹配的库存流水" : "暂无库存流水";
});
const loadStateText = computed(() => loading.value ? "正在加载" : noMore.value ? "已经到底了" : "上拉加载更多");
const operationOptions = [
@@ -159,6 +198,10 @@ function getQueryText(key: string): string {
return Array.isArray(value) ? String(value[0] || "") : String(value || "");
}
function getDefaultFilteredSkuUnitId(): string {
return filteredSkuUnitList.value.find(unit => unit.isBase)?.id || filteredSkuUnitList.value[0]?.id || "";
}
function getOperationIcon(type?: StockOperationType): string {
return ({
INITIAL_IN: "import",
@@ -235,6 +278,7 @@ async function loadMore(force = false): Promise<void> {
try {
const result = await ProductAPI.recordList({
productId: productId.value || undefined,
skuId: skuId.value || undefined,
keyword: keyword.value.trim() || undefined,
operationType: operationType.value === "ALL" ? undefined : operationType.value,
activeReservationOnly: activeReservationOnly.value || undefined,
@@ -257,6 +301,23 @@ async function loadMore(force = false): Promise<void> {
}
}
async function loadSkuFilter(): Promise<void> {
if (!skuId.value) {
filteredProduct.value = undefined;
filteredSku.value = undefined;
filteredSkuUnitId.value = "";
return;
}
const sku = await ProductAPI.skuDetail(skuId.value);
const product = sku.productId ? await ProductAPI.detail(sku.productId) : undefined;
if (skuId.value !== getQueryText("skuId")) {
return;
}
filteredSku.value = sku;
filteredProduct.value = product;
filteredSkuUnitId.value = getDefaultFilteredSkuUnitId();
}
async function selectOperationType(value: DropdownValue): Promise<void> {
if (!Array.isArray(value)) {
operationType.value = String(value) as OperationFilter;
@@ -292,7 +353,7 @@ async function refresh(): Promise<void> {
onMounted(async () => {
navBarStore.clearRight();
try {
await reload();
await Promise.all([loadSkuFilter(), reload()]);
} catch (error) {
Message.error(getErrorMessage(error, "库存流水加载失败"));
}
@@ -372,83 +433,113 @@ onUnmounted(() => {
}
}
.list {
margin: 0;
.list-wrap {
gap: 1rem;
display: flex;
flex-direction: column;
.sku {
--td-tag-square-border-radius: .5rem;
.item {
padding: 10px;
min-width: 0;
:deep(.t-cell__title) {
min-width: 0;
width: 0;
}
:deep(.t-cell__title-text) {
min-width: 0;
width: 100%;
display: flex;
align-items: center;
}
.avatar {
width: 3.5rem;
height: 3.5rem;
}
.icon {
width: 2.25rem;
height: 2.25rem;
display: inline-flex;
background: var(--td-bg-color-secondarycontainer);
align-items: center;
border-radius: 50%;
justify-content: center;
width: 2rem;
height: 2rem;
border-radius: .5rem !important;
}
.title {
gap: .5rem;
display: flex;
min-width: 0;
max-width: 100%;
flex: 1;
display: flex;
font-size: .75rem;
align-items: center;
white-space: nowrap;
.actor {
color: var(--td-text-color-secondary);
flex: 0 0 auto;
}
.operation {
flex: 0 0 auto;
}
.product-name {
min-width: 0;
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.quantity {
color: var(--td-text-color-secondary);
flex: 0 0 auto;
font-weight: 400;
}
}
}
.description {
gap: .5rem;
display: flex;
font-size: .75rem;
align-items: center;
justify-content: space-between;
.list {
margin: 0;
.delta {
.item {
padding: 10px;
min-width: 0;
.value {
font-size: 1.25rem;
:deep(.t-cell__title) {
min-width: 0;
width: 0;
}
:deep(.t-cell__title-text) {
min-width: 0;
width: 100%;
}
.avatar {
width: 3.5rem;
height: 3.5rem;
}
.icon {
width: 2.25rem;
height: 2.25rem;
display: inline-flex;
background: var(--td-bg-color-secondarycontainer);
align-items: center;
border-radius: 50%;
justify-content: center;
}
.title {
gap: .5rem;
min-width: 0;
max-width: 100%;
flex: 1;
display: flex;
font-size: .75rem;
align-items: center;
white-space: nowrap;
.actor {
color: var(--td-text-color-secondary);
flex: 0 0 auto;
}
.operation {
flex: 0 0 auto;
}
.product-name {
min-width: 0;
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.quantity {
color: var(--td-text-color-secondary);
flex: 0 0 auto;
font-weight: 400;
}
}
.description {
gap: .5rem;
display: flex;
font-size: .75rem;
align-items: center;
justify-content: space-between;
.delta {
.value {
font-size: 1.25rem;
}
}
}
}
+1
View File
@@ -192,6 +192,7 @@ export type StockRecord = {
export type StockRecordPage = {
storeId?: string;
productId?: string;
skuId?: string;
keyword?: string;
bizType?: StockBizType;
bizId?: string;