add unit selector

This commit is contained in:
Timi
2026-09-21 21:25:23 +08:00
parent 165524c05c
commit 3b596a6355
+118 -15
View File
@@ -21,16 +21,43 @@
<t-cell-group theme="card"> <t-cell-group theme="card">
<t-cell title="产品" :note="product?.name" /> <t-cell title="产品" :note="product?.name" />
</t-cell-group> </t-cell-group>
<t-cell-group v-if="skuStockList.length" theme="card" class="list fix-last-cell-line"> <t-cell-group
v-if="skuStockList.length"
theme="card"
class="list fix-last-cell-line empty-cell-title-margin"
>
<t-cell <t-cell
class="item" class="item"
v-for="(item, index) in skuStockList" v-for="(item, index) in skuStockList"
:key="item.skuId || item.id" :key="item.skuId || item.id"
:title="item.sku?.name || '未命名 SKU 明细'"
arrow arrow
v-entrance-transition="index * 30" v-entrance-transition="index * 30"
@click="toDetail(item)" @click="toDetail(item)"
> >
<template #title>
<div class="title">
<strong v-text="item.sku?.name || '未命名 SKU 明细'" />
<div
v-if="getUnitList(item).length"
class="unit-options"
role="group"
:aria-label="`切换${item.sku?.name || 'SKU'}库存单位`"
>
<t-button
v-for="unit in getUnitList(item)"
:key="unit.id"
size="extra-small"
:theme="getSelectedUnit(item)?.id === unit.id ? 'primary' : 'default'"
:variant="getSelectedUnit(item)?.id === unit.id ? 'base' : 'outline'"
:aria-label="`切换到${unit.name || '未命名单位'}`"
:aria-pressed="getSelectedUnit(item)?.id === unit.id"
@click.stop="selectUnit(item, unit.id || '')"
>
<span v-text="unit.name || '未命名单位'" />
</t-button>
</div>
</div>
</template>
<template #leftIcon> <template #leftIcon>
<t-image <t-image
v-if="getSkuPhotoURL(item)" v-if="getSkuPhotoURL(item)"
@@ -43,18 +70,18 @@
</template> </template>
<template #description> <template #description>
<div class="stock"> <div class="stock">
<span class="on-hand"> <span class="on-hand">
<span>库存</span> <span>库存</span>
<strong v-text="getProductQuantityText(item.onHandQuantity)" /> <strong v-text="getStockQuantityText(item, item.onHandQuantity)" />
</span> </span>
<span class="available"> <span class="available">
<span>可用</span> <span>可用</span>
<strong v-text="getProductQuantityText(item.availableQuantity)" /> <strong v-text="getStockQuantityText(item, item.availableQuantity)" />
</span> </span>
<span class="reserved"> <span class="reserved">
<span>预占</span> <span>预占</span>
<strong v-text="getProductQuantityText(item.reservedQuantity)" /> <strong v-text="getStockQuantityText(item, item.reservedQuantity)" />
</span> </span>
</div> </div>
</template> </template>
</t-cell> </t-cell>
@@ -70,8 +97,9 @@
import { useUserStore } from "timi-web-api"; import { useUserStore } from "timi-web-api";
import ProductAPI from "@/api/ProductAPI"; import ProductAPI from "@/api/ProductAPI";
import { useNavBarStore } from "@/store/navBarStore"; import { useNavBarStore } from "@/store/navBarStore";
import type { Product, ProductStock } from "@/types/Product"; import type { Product, ProductSkuUnit, ProductStock } from "@/types/Product";
import { getErrorMessage } from "@/utils/error"; import { getErrorMessage } from "@/utils/error";
import { convertBaseQuantityToPackageQuantity } from "@/utils/package";
import { getProductPhotoURL, getProductQuantityText } from "@/utils/product"; import { getProductPhotoURL, getProductQuantityText } from "@/utils/product";
import { Message } from "@/utils/message"; import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission"; import { permissionCodes } from "@/utils/permission";
@@ -90,6 +118,7 @@ const total = ref(0);
const isLoading = ref(false); const isLoading = ref(false);
const isRefreshing = ref(false); const isRefreshing = ref(false);
const isNoMore = ref(false); const isNoMore = ref(false);
const selectedUnitIdMap = ref<Record<string, string>>({});
const productId = computed(() => { const productId = computed(() => {
const value = route.params.productId; const value = route.params.productId;
return Array.isArray(value) ? String(value[0] || "") : String(value || ""); return Array.isArray(value) ? String(value[0] || "") : String(value || "");
@@ -108,6 +137,52 @@ function getSkuPhotoURL(item: ProductStock): string {
return item.sku ? getProductPhotoURL(item.sku) : ""; return item.sku ? getProductPhotoURL(item.sku) : "";
} }
function getStockKey(item: ProductStock): string {
return item.skuId || item.sku?.id || item.id || "";
}
function getUnitList(item: ProductStock): ProductSkuUnit[] {
if (item.sku?.unitList?.length) {
return item.sku.unitList.filter(unit => !!unit.id);
}
return product.value?.skuList?.find(sku => sku.id === item.skuId)?.unitList?.filter(unit => !!unit.id) || [];
}
function getSelectedUnit(item: ProductStock): ProductSkuUnit | undefined {
const unitList = getUnitList(item);
const selectedUnitId = selectedUnitIdMap.value[getStockKey(item)];
return unitList.find(unit => unit.id === selectedUnitId) || unitList.find(unit => unit.isBase) || unitList[0];
}
function getSelectedUnitName(item: ProductStock): string {
return getSelectedUnit(item)?.name || "基础单位";
}
function getStockQuantityText(item: ProductStock, value?: number | string): string {
const quantity = convertBaseQuantityToPackageQuantity(value ?? "0", getSelectedUnit(item)?.baseQuantityPerUnit ?? "1");
return getProductQuantityText(quantity);
}
function selectUnit(item: ProductStock, unitId: string): void {
const stockKey = getStockKey(item);
if (!stockKey || !getUnitList(item).some(unit => unit.id === unitId)) {
return;
}
selectedUnitIdMap.value = { ...selectedUnitIdMap.value, [stockKey]: unitId };
}
function initializeSelectedUnits(list: ProductStock[]): void {
const nextSelectedUnitIdMap = { ...selectedUnitIdMap.value };
for (const item of list) {
const stockKey = getStockKey(item);
const selectedUnit = getSelectedUnit(item);
if (stockKey && selectedUnit?.id) {
nextSelectedUnitIdMap[stockKey] = selectedUnit.id;
}
}
selectedUnitIdMap.value = nextSelectedUnitIdMap;
}
let keywordTimer: number | undefined; let keywordTimer: number | undefined;
let requestToken = 0; let requestToken = 0;
let suppressKeywordFetch = false; let suppressKeywordFetch = false;
@@ -130,6 +205,7 @@ async function fetch(reset = false): Promise<void> {
if (reset) { if (reset) {
pageIndex.value = 0; pageIndex.value = 0;
skuStockList.value = []; skuStockList.value = [];
selectedUnitIdMap.value = {};
total.value = 0; total.value = 0;
isNoMore.value = false; isNoMore.value = false;
} }
@@ -148,6 +224,7 @@ async function fetch(reset = false): Promise<void> {
} }
const list = result.list || []; const list = result.list || [];
skuStockList.value = reset ? list : skuStockList.value.concat(list); skuStockList.value = reset ? list : skuStockList.value.concat(list);
initializeSelectedUnits(list);
total.value = result.total || 0; total.value = result.total || 0;
pageIndex.value += 1; pageIndex.value += 1;
isNoMore.value = list.length < pageSize || skuStockList.value.length === total.value || total.value < skuStockList.value.length; isNoMore.value = list.length < pageSize || skuStockList.value.length === total.value || total.value < skuStockList.value.length;
@@ -167,7 +244,8 @@ async function loadCurrent(): Promise<void> {
return; return;
} }
try { try {
await Promise.all([loadProduct(), fetch(true)]); await loadProduct();
await fetch(true);
} catch (error) { } catch (error) {
Message.error(getErrorMessage(error, "SKU 列表加载失败")); Message.error(getErrorMessage(error, "SKU 列表加载失败"));
} }
@@ -175,7 +253,8 @@ async function loadCurrent(): Promise<void> {
async function refresh(): Promise<void> { async function refresh(): Promise<void> {
try { try {
await Promise.all([loadProduct(), fetch(true)]); await loadProduct();
await fetch(true);
} catch (error) { } catch (error) {
Message.error(getErrorMessage(error, "SKU 列表加载失败")); Message.error(getErrorMessage(error, "SKU 列表加载失败"));
} finally { } finally {
@@ -273,6 +352,16 @@ onUnmounted(() => {
.item { .item {
padding: 10px; padding: 10px;
:deep(.t-cell__title) {
min-width: 0;
width: 0;
}
:deep(.t-cell__title-text) {
min-width: 0;
width: 100%;
}
.avatar { .avatar {
width: 3.5rem; width: 3.5rem;
height: 3.5rem; height: 3.5rem;
@@ -292,6 +381,7 @@ onUnmounted(() => {
.title { .title {
gap: .5rem; gap: .5rem;
min-width: 0; min-width: 0;
max-width: 100%;
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -303,6 +393,19 @@ onUnmounted(() => {
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.unit-options {
gap: .25rem;
max-width: 55%;
display: flex;
flex-shrink: 0;
overflow-x: auto;
:deep(.t-button) {
--td-button-border-radius: .5rem;
flex-shrink: 0;
}
}
} }
.sku-spec { .sku-spec {