Files
gao-customer-h5/src/pages/product/ProductDetail.vue
T
2026-09-05 22:13:56 +08:00

256 lines
6.9 KiB
Vue

<template>
<div v-can="permissionCodes.product.read" class="product-detail">
<t-loading v-if="isLoading" class="loading" size="32px" />
<template v-if="product">
<product-profile :product="product" v-entrance-transition />
<t-cell-group title="基本信息" theme="card" v-entrance-transition="70">
<t-cell title="产品分类" :note="product.category?.name || '未分类'" />
<t-cell title="库存管理" :note="product.stockEnabled ? '启用' : '关闭'" />
<t-cell title="状态" :note="getProductStatusText(product.status)" />
<t-cell title="备注" :note="Text.display(product.remark)"></t-cell>
<t-cell>
<photo-scroller
:model-value="attachmentUrlList"
:photo-items="attachmentItemList"
title="附件列表"
:show-add="false"
:show-remove="false"
:show-viewer-actions="false"
/>
</t-cell>
</t-cell-group>
<t-cell-group theme="card" v-entrance-transition="140">
<t-cell
v-if="product.stockEnabled && userStore.hasPermissions(permissionCodes.productStock.read)"
title="库存"
note="查看库存详情"
arrow
@click="viewStock"
/>
</t-cell-group>
<t-cell-group title="业务单位" theme="card" class="units" v-entrance-transition="210">
<t-cell
v-for="unit in product.unitList || []"
:key="unit.id || unit.name"
>
<template #title>
<div class="title">
<h3 class="name" v-text="unit.name || '未命名单位'" />
<t-tag
v-if="!unit.isBase"
theme="default"
size="small"
:content="`${unit.baseQuantityPerUnit} ${getProductBaseUnitName(product)}`"
/>
</div>
</template>
<template #note>
<span class="price">
<span class="qute red text-bold">¥</span>
<span
class="value text-bold red"
v-text="formatProductAmount(unit.salePrice)"
/>
</span>
</template>
<template #description>
<div class="tags">
<t-tag
v-if="unit.isBase"
theme="warning"
variant="light-outline"
content="基础单位"
/>
<t-tag
v-if="unit.allowSale"
theme="primary"
variant="light-outline"
content="可销售"
/>
<t-tag
v-if="unit.allowPurchase"
theme="primary"
variant="light-outline"
content="可采购"
/>
</div>
</template>
</t-cell>
</t-cell-group>
</template>
</div>
<nav-bar-action-menu v-if="actionList.length" :actions="actionList" />
</template>
<script setup lang="ts">
import { DialogPlugin } from "tdesign-mobile-vue";
import { useUserStore } from "timi-web-api";
import ProductAPI from "@/api/ProductAPI";
import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
import PhotoScroller from "@/components/PhotoScroller.vue";
import ProductProfile from "@/pages/product/component/ProductProfile.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { NavBarAction } from "@/types/NavBarAction";
import type { PhotoItem } from "@/types/Photo";
import type { Product } from "@/types/Product";
import { getAttachmentReadURL } from "@/utils/attachment";
import { getErrorMessage } from "@/utils/error";
import {
formatProductAmount,
getProductBaseUnitName,
getProductStatusText
} from "@/utils/product";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
import { Text } from "timi-web";
defineOptions({ name: "ProductDetail" });
const route = useRoute();
const router = useRouter();
const navBarStore = useNavBarStore();
const userStore = useUserStore();
const product = ref<Product>();
const isLoading = ref(false);
const attachmentItemList = computed<PhotoItem[]>(() => (product.value?.attachmentList || [])
.filter(item => !!item.id)
.map(item => ({
attachment: item,
attachType: item.attachType || "PHOTO",
previewUrl: getAttachmentReadURL(item.id || ""),
url: getAttachmentReadURL(item.id || "", 90)
})));
const attachmentUrlList = computed(() => attachmentItemList.value.map(item => item.url));
const actionList = computed<NavBarAction[]>(() => {
const list: NavBarAction[] = [];
if (userStore.hasPermissions(permissionCodes.product.update)) {
list.push({ key: "edit", text: "编辑产品", icon: "edit-1", theme: "primary", action: edit });
if (product.value?.status === "ACTIVE") {
list.push({ key: "disable", text: "停用产品", icon: "close", theme: "danger", action: disable });
}
}
return list;
});
async function load(): Promise<void> {
const id = String(route.params.id || "");
if (!id) {
return;
}
isLoading.value = true;
try {
product.value = await ProductAPI.detail(id);
} finally {
isLoading.value = false;
}
}
async function edit(): Promise<void> {
if (product.value?.id) {
await router.push(`/management/products/${product.value.id}/edit`);
}
}
async function viewStock(): Promise<void> {
if (product.value?.id) {
await router.push({
name: "ProductStockDetail",
params: { productId: product.value.id },
query: { transitionDepth: String(getCurrentRouteDepth() + 1) }
});
}
}
function getCurrentRouteDepth(): number {
const transitionDepth = Array.isArray(route.query.transitionDepth)
? route.query.transitionDepth[0]
: route.query.transitionDepth;
const depth = Number(transitionDepth);
if (transitionDepth && Number.isInteger(depth) && 0 <= depth && depth <= 20) {
return depth;
}
return Number(route.meta.depth ?? 0);
}
function disable(): void {
const id = product.value?.id;
if (!id) {
return;
}
const dialog = DialogPlugin.confirm({
title: "停用产品",
content: "停用后不能用于新订单、套餐或库存操作,历史记录仍会保留。确定停用?",
cancelBtn: "取消",
confirmBtn: { content: "停用", theme: "danger" },
onConfirm: async () => {
try {
await ProductAPI.disable(id);
await load();
Message.success("产品已停用");
} catch (error) {
Message.error(getErrorMessage(error, "产品停用失败"));
} finally {
dialog.destroy();
}
}
});
}
onMounted(async () => {
navBarStore.clearRight();
navBarStore.setCustomRightVisible(actionList.value.length > 0);
try {
await load();
navBarStore.setCustomRightVisible(actionList.value.length > 0);
} catch (error) {
Message.error(getErrorMessage(error, "产品详情加载失败"));
}
});
onUnmounted(() => navBarStore.clearRight());
</script>
<style scoped lang="less">
.product-detail {
gap: 1rem;
display: flex;
padding: calc(var(--app-nav-height) + 1rem) 0 1rem 0;
flex-direction: column;
.units {
.title {
gap: .5rem;
display: flex;
align-items: center;
.name {
margin: 0;
}
}
.price {
.value {
font-size: 1.5rem;
}
}
.tags {
gap: .5rem;
display: flex;
flex-wrap: wrap;
}
}
}
:global(html.theme-dark) .product-detail .hero {
background: linear-gradient(135deg, rgba(15, 118, 110, .2), rgba(37, 99, 235, .18));
.eyebrow {
color: #99f6e4;
}
}
</style>