add product
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div v-can="permissionCodes.product.read" class="product-detail">
|
||||
<t-loading v-if="isLoading" class="loading" size="32px" />
|
||||
<template v-else-if="product">
|
||||
<section class="hero" v-entrance-transition>
|
||||
<span class="eyebrow" v-text="'产品库'" />
|
||||
<div class="hero-title">
|
||||
<h1 v-text="product.name || '未命名产品'" />
|
||||
<t-tag :content="getProductStatusText(product.status)" variant="light-outline" :theme="getProductStatusTheme(product.status)" />
|
||||
</div>
|
||||
<div class="hero-meta">
|
||||
<span v-text="`${product.code || '-'} · ${getProductTypeText(product.productType)}`" />
|
||||
<span v-text="`${product.unitList?.length || 0} 个业务单位`" />
|
||||
</div>
|
||||
</section>
|
||||
<card-container v-if="product.attachmentList?.length" title="产品附件">
|
||||
<photo-scroller
|
||||
:model-value="attachmentUrlList"
|
||||
:photo-items="attachmentItemList"
|
||||
title="附件列表"
|
||||
:show-add="false"
|
||||
:show-remove="false"
|
||||
:show-viewer-actions="false"
|
||||
/>
|
||||
</card-container>
|
||||
<card-container title="基本信息">
|
||||
<t-cell-group theme="card" class="info">
|
||||
<t-cell title="产品名称" :note="product.name || '未命名产品'" />
|
||||
<t-cell title="产品编码" :note="product.code || '-'" />
|
||||
<t-cell title="产品类型" :note="getProductTypeText(product.productType)" />
|
||||
<t-cell title="产品分类" :note="product.category?.name || '未分类'" />
|
||||
<t-cell title="基础单位" :note="product.baseUnit || '-'" />
|
||||
<t-cell title="参考成本" :note="formatProductAmount(product.referenceCost)" />
|
||||
<t-cell title="库存管理" :note="product.stockEnabled ? '已开启' : '未开启'" />
|
||||
<t-cell title="状态" :note="getProductStatusText(product.status)" />
|
||||
</t-cell-group>
|
||||
</card-container>
|
||||
<card-container title="业务单位" :description="product.unitList?.length ? '名称、基础单位数量、售价和可用范围由产品库统一维护' : '暂无业务单位'">
|
||||
<t-cell-group theme="card" class="units">
|
||||
<t-cell v-for="unit in product.unitList || []" :key="unit.id || unit.name">
|
||||
<template #title>
|
||||
<div class="unit-title">
|
||||
<strong v-text="unit.name || '未命名单位'" />
|
||||
<t-tag v-if="unit.isBase" size="small" theme="primary" variant="light-outline" content="基础单位" />
|
||||
</div>
|
||||
</template>
|
||||
<template #note>
|
||||
<span v-text="`1 ${unit.name || '单位'} = ${unit.baseQuantityPerUnit} ${product.baseUnit || '基础单位'}`" />
|
||||
</template>
|
||||
<template #description>
|
||||
<div class="unit-description">
|
||||
<span v-text="`建议售价:${formatProductAmount(unit.salePrice)}`" />
|
||||
<span v-text="getUnitUsageText(unit)" />
|
||||
</div>
|
||||
</template>
|
||||
</t-cell>
|
||||
</t-cell-group>
|
||||
</card-container>
|
||||
<card-container v-if="product.remark" title="备注">
|
||||
<div class="remark" v-text="product.remark" />
|
||||
</card-container>
|
||||
</template>
|
||||
<t-empty v-else description="产品不存在或已被删除" />
|
||||
</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, ProductUnit } from "@/types/Product";
|
||||
import { getAttachmentReadURL } from "@/utils/attachment";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import { formatProductAmount, getProductStatusText, getProductStatusTheme, getProductTypeText } from "@/utils/product";
|
||||
import { Message } from "@/utils/message";
|
||||
import { permissionCodes } from "@/utils/permission";
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
function getUnitUsageText(unit: ProductUnit): string {
|
||||
const usage: string[] = [];
|
||||
if (unit.allowSale) {
|
||||
usage.push("可销售");
|
||||
}
|
||||
if (unit.allowPurchase) {
|
||||
usage.push("可采购");
|
||||
}
|
||||
return usage.length ? usage.join("、") : "暂无业务用途";
|
||||
}
|
||||
|
||||
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: "停用后不能用于新订单、套餐或库存操作,历史记录仍会保留。确定停用?",
|
||||
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: grid;
|
||||
padding: calc(var(--app-nav-height) + 1rem) 0 1rem;
|
||||
|
||||
.hero {
|
||||
margin: 0 1rem;
|
||||
padding: 1rem;
|
||||
background: linear-gradient(135deg, #e8fffa, #eff6ff);
|
||||
box-shadow: var(--td-shadow-1);
|
||||
border-radius: 1.25rem;
|
||||
|
||||
.eyebrow {
|
||||
color: #0f766e;
|
||||
display: block;
|
||||
font-size: .6875rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .08em;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
margin-top: .35rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.hero-meta {
|
||||
gap: .75rem;
|
||||
display: flex;
|
||||
margin-top: .6rem;
|
||||
color: var(--td-text-color-secondary);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.loading {
|
||||
margin: 3rem auto;
|
||||
}
|
||||
|
||||
.info,
|
||||
.units {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.unit-title {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.unit-description {
|
||||
gap: .125rem;
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.remark {
|
||||
padding: 1rem;
|
||||
white-space: pre-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>
|
||||
Reference in New Issue
Block a user