311 lines
7.8 KiB
Vue
311 lines
7.8 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">
|
|
<card-container class="info" v-entrance-transition>
|
|
<div class="content">
|
|
<div class="left">
|
|
<t-image
|
|
v-if="getProductPhotoURL(product)"
|
|
:src="getProductPhotoURL(product)"
|
|
:alt="product.name || '产品图片'"
|
|
fit="cover"
|
|
class="avatar"
|
|
/>
|
|
<div
|
|
v-else
|
|
class="avatar placeholder"
|
|
v-text="(product.name || product.code || '产').slice(0, 1)"
|
|
/>
|
|
</div>
|
|
<div class="main">
|
|
<div class="header">
|
|
<h3 class="name" v-text="product.name || '未命名产品'" />
|
|
<div class="code gray" v-text="product.code || '-'" />
|
|
</div>
|
|
<div class="tags">
|
|
<t-tag
|
|
variant="light-outline"
|
|
theme="primary"
|
|
:content="getProductTypeText(product.productType)"
|
|
/>
|
|
<t-tag
|
|
variant="light-outline"
|
|
:theme="getProductStatusTheme(product.status)"
|
|
:content="getProductStatusText(product.status)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</card-container>
|
|
<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 title="业务单位" theme="card" class="units" v-entrance-transition="140">
|
|
<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 { 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,
|
|
getProductPhotoURL,
|
|
getProductStatusText,
|
|
getProductStatusTheme,
|
|
getProductTypeText
|
|
} 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 });
|
|
}
|
|
}
|
|
if (product.value?.stockEnabled && userStore.hasPermissions(permissionCodes.productStock.read)) {
|
|
list.push({ key: "stock", text: "查看库存", icon: "wallet", theme: "default", action: viewStock });
|
|
}
|
|
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(`/management/product-stocks/${product.value.id}`);
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
.info {
|
|
margin: 0 1rem;
|
|
|
|
.content {
|
|
gap: .5rem;
|
|
padding: .5rem;
|
|
display: flex;
|
|
|
|
.avatar {
|
|
width: 7rem;
|
|
height: 7rem;
|
|
}
|
|
|
|
.main {
|
|
flex: 1 1 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
|
|
.header {
|
|
|
|
.name {
|
|
margin: 0;
|
|
}
|
|
|
|
.code {
|
|
font-size: .875rem;
|
|
margin-top: .25rem;
|
|
}
|
|
}
|
|
|
|
.tags {
|
|
gap: .5rem;
|
|
display: flex;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
.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>
|