add product stock preview

This commit is contained in:
Timi
2026-09-16 14:53:36 +08:00
parent 443d0a6190
commit f8a9c0e649
12 changed files with 456 additions and 86 deletions
+57 -12
View File
@@ -203,7 +203,7 @@
<script setup lang="ts">
import { BottomPopup } from "timi-tdesign-mobile";
import type { ColorObject } from "tdesign-mobile-vue";
import { DialogPlugin, type ColorObject } from "tdesign-mobile-vue";
import { type Attachment, AttachmentAPI, useUserStore } from "timi-web-api";
import ProductAPI from "@/api/ProductAPI";
import PhotoScroller from "@/components/PhotoScroller.vue";
@@ -212,7 +212,7 @@ import ProductSkuUnitEditor from "@/pages/product/component/ProductSkuUnitEditor
import ProductSkuUnitBatchEditor from "@/pages/product/component/ProductSkuUnitBatchEditor.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { PhotoItem } from "@/types/Photo";
import type { Product, ProductAttachmentType, ProductCategory, ProductSku, ProductSkuUnit, ProductSpec } from "@/types/Product";
import type { Product, ProductAttachmentType, ProductCategory, ProductSku, ProductSkuUnit, ProductSpec, ProductType } from "@/types/Product";
import type { ProductAttachmentItem, ProductForm, ProductSkuForm as SkuForm, ProductSkuUnitForm as UnitForm, ProductSpecForm as SpecForm, ProductSpecValueForm as SpecValueForm } from "@/types/ProductEditor";
import { compressUploadImage } from "@/utils/imageCompression";
import { getAttachmentReadURL } from "@/utils/attachment";
@@ -464,19 +464,64 @@ function openTypePicker(): void {
typePickerVisible.value = true;
}
function confirmType(value: Array<string | number>): void {
async function confirmType(value: Array<string | number>): Promise<void> {
const type = String(value[0] || "");
if (type === "SERVICE" || type === "GOODS" || type === "CONSUMABLE") {
form.productType = type;
if (type === "SERVICE") {
form.stockEnabled = false;
form.skuList.forEach(sku => {
sku.stockEnabled = false;
sku.unitList.forEach(unit => { unit.allowPurchase = false; });
});
}
if (type !== "SERVICE" && type !== "GOODS" && type !== "CONSUMABLE") {
typePickerVisible.value = false;
return;
}
typePickerVisible.value = false;
if (type !== "SERVICE" || form.productType === "SERVICE") {
applyProductType(type);
return;
}
if (!await canChangeToService()) {
return;
}
const dialog = DialogPlugin.confirm({
title: "切换为服务产品",
content: "切换后将关闭产品及全部 SKU 的库存管理,并清空最低库存设置;已有库存流水会保留。确定继续吗?",
cancelBtn: "取消",
confirmBtn: "确定切换",
onCancel() {
dialog.destroy();
},
onConfirm() {
dialog.destroy();
applyProductType("SERVICE");
}
});
}
function applyProductType(type: ProductType): void {
form.productType = type;
if (type !== "SERVICE") {
return;
}
form.stockEnabled = false;
form.skuList.forEach(sku => {
sku.stockEnabled = false;
sku.minStockQuantityText = "";
sku.unitList.forEach(unit => { unit.allowPurchase = false; });
});
}
async function canChangeToService(): Promise<boolean> {
const skuIdList = form.skuList.map(sku => sku.id).filter((id): id is string => !!id);
if (!skuIdList.length) {
return true;
}
try {
const stockList = await ProductAPI.stockBatchDetail({ skuIdList });
if (stockList.some(stock => 0 < Number(stock.reservedQuantity || 0))) {
Message.warning("当前产品存在预占库存,不能切换为服务产品,请先完成或取消相关订单");
return false;
}
return true;
} catch (error) {
Message.error(getErrorMessage(error, "无法确认预占库存,暂不能切换为服务产品"));
return false;
}
}
function openCategoryPicker(): void {
+9 -2
View File
@@ -29,7 +29,14 @@
@click="toSkuList(item)"
>
<template #leftIcon>
<div class="avatar fallback" v-text="(item.productName || '产').slice(0, 1)" />
<t-image
v-if="getProductPhotoURL(item)"
:src="getProductPhotoURL(item)"
:alt="item.productName || '产品图片'"
fit="cover"
class="avatar"
/>
<div v-else class="avatar fallback" v-text="(item.productName || '产').slice(0, 1)" />
</template>
<template #title>
<div class="title">
@@ -67,7 +74,7 @@ import ProductAPI from "@/api/ProductAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { ProductStockProduct } from "@/types/Product";
import { getErrorMessage } from "@/utils/error";
import { getProductQuantityText } from "@/utils/product";
import { getProductPhotoURL, getProductQuantityText } from "@/utils/product";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
+14 -2
View File
@@ -32,7 +32,14 @@
@click="toDetail(item)"
>
<template #leftIcon>
<div class="avatar fallback" v-text="(item.sku?.name || 'S').slice(0, 1)" />
<t-image
v-if="getSkuPhotoURL(item)"
:src="getSkuPhotoURL(item)"
:alt="item.sku?.name || 'SKU 图片'"
fit="cover"
class="avatar"
/>
<div v-else class="avatar fallback" v-text="(item.sku?.name || 'S').slice(0, 1)" />
</template>
<template #title>
<div class="title">
@@ -71,7 +78,7 @@ import ProductAPI from "@/api/ProductAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { Product, ProductStock } from "@/types/Product";
import { getErrorMessage } from "@/utils/error";
import { getProductQuantityText } from "@/utils/product";
import { getProductPhotoURL, getProductQuantityText } from "@/utils/product";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
@@ -102,6 +109,11 @@ const emptyText = computed(() => {
}
return "该产品暂无启用库存管理的 SKU";
});
function getSkuPhotoURL(item: ProductStock): string {
return item.sku ? getProductPhotoURL(item.sku) : "";
}
let keywordTimer: number | undefined;
let requestToken = 0;
let suppressKeywordFetch = false;