fix ProductDetail.vue style

This commit is contained in:
Timi
2026-09-03 15:11:34 +08:00
parent 333999fe21
commit 2acdf46d5d
3 changed files with 177 additions and 120 deletions
+164 -112
View File
@@ -1,66 +1,111 @@
<template> <template>
<div v-can="permissionCodes.product.read" class="product-detail"> <div v-can="permissionCodes.product.read" class="product-detail">
<t-loading v-if="isLoading" class="loading" size="32px" /> <t-loading v-if="isLoading" size="32px" />
<template v-else-if="product"> <template v-if="product">
<section class="hero" v-entrance-transition> <card-container class="info">
<span class="eyebrow" v-text="'产品库'" /> <div class="content">
<div class="hero-title"> <div class="left">
<h1 v-text="product.name || '未命名产品'" /> <t-image
<t-tag :content="getProductStatusText(product.status)" variant="light-outline" :theme="getProductStatusTheme(product.status)" /> 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> </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> </card-container>
<t-cell-group title="基本信息" theme="card">
<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 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">
<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} ${product.baseUnit}`"
/>
</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="primary"
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> </template>
<t-empty v-else description="产品不存在或已被删除" />
</div> </div>
<nav-bar-action-menu v-if="actionList.length" :actions="actionList" /> <nav-bar-action-menu v-if="actionList.length" :actions="actionList" />
</template> </template>
@@ -77,9 +122,16 @@ import type { PhotoItem } from "@/types/Photo";
import type { Product, ProductUnit } from "@/types/Product"; import type { Product, ProductUnit } from "@/types/Product";
import { getAttachmentReadURL } from "@/utils/attachment"; import { getAttachmentReadURL } from "@/utils/attachment";
import { getErrorMessage } from "@/utils/error"; import { getErrorMessage } from "@/utils/error";
import { formatProductAmount, getProductStatusText, getProductStatusTheme, getProductTypeText } from "@/utils/product"; import {
formatProductAmount,
getProductPhotoURL,
getProductStatusText,
getProductStatusTheme,
getProductTypeText
} from "@/utils/product";
import { Message } from "@/utils/message"; import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission"; import { permissionCodes } from "@/utils/permission";
import { Text } from "timi-web";
defineOptions({ name: "ProductDetail" }); defineOptions({ name: "ProductDetail" });
@@ -188,73 +240,73 @@ onUnmounted(() => navBarStore.clearRight());
<style scoped lang="less"> <style scoped lang="less">
.product-detail { .product-detail {
gap: 1rem; gap: 1rem;
display: grid; display: flex;
padding: calc(var(--app-nav-height) + 1rem) 0 1rem; padding: calc(var(--app-nav-height) + 1rem) 0 1rem 0;
flex-direction: column;
.hero { .info {
margin: 0 1rem; margin: 0 1rem;
padding: 1rem;
background: linear-gradient(135deg, #e8fffa, #eff6ff);
box-shadow: var(--td-shadow-1);
border-radius: 1.25rem;
.eyebrow { .content {
color: #0f766e; gap: .5rem;
display: block; padding: .5rem;
font-size: .6875rem; display: flex;
font-weight: 700;
letter-spacing: .08em; .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;
}
}
} }
}
.hero-title { .units {
.title {
gap: .5rem; gap: .5rem;
display: flex; display: flex;
margin-top: .35rem;
align-items: center; align-items: center;
justify-content: space-between;
.name {
margin: 0;
}
} }
h1 { .price {
margin: 0;
min-width: 0; .value {
font-size: 1.25rem; font-size: 1.5rem;
line-height: 1.35; }
} }
.hero-meta { .tags {
gap: .75rem; gap: .5rem;
display: flex; display: flex;
margin-top: .6rem; flex-wrap: wrap;
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;
} }
} }
+6 -7
View File
@@ -33,7 +33,11 @@
fit="cover" fit="cover"
class="avatar" class="avatar"
/> />
<div v-else class="avatar placeholder" v-text="(item.name || item.code || '产').slice(0, 1)" /> <div
v-else
class="avatar placeholder"
v-text="(item.name || item.code || '产').slice(0, 1)"
/>
</template> </template>
<template #title> <template #title>
<div class="text-bold" v-text="item.name || '未命名产品'" /> <div class="text-bold" v-text="item.name || '未命名产品'" />
@@ -79,7 +83,7 @@ import type { NavBarAction } from "@/types/NavBarAction";
import type { Product, ProductCategory, ProductStatus, ProductType } from "@/types/Product"; import type { Product, ProductCategory, ProductStatus, ProductType } from "@/types/Product";
import { getAttachmentReadURL } from "@/utils/attachment"; import { getAttachmentReadURL } from "@/utils/attachment";
import { getErrorMessage } from "@/utils/error"; import { getErrorMessage } from "@/utils/error";
import { getProductStatusText, getProductStatusTheme, getProductTypeText } from "@/utils/product"; import { getProductPhotoURL, getProductStatusText, getProductStatusTheme, getProductTypeText } from "@/utils/product";
import { Message } from "@/utils/message"; import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission"; import { permissionCodes } from "@/utils/permission";
@@ -128,11 +132,6 @@ const categoryOptions = computed(() => [{ label: "全部分类", value: "ALL" },
}))]); }))]);
const emptyText = computed(() => isLoading.value ? "正在加载产品" : keyword.value ? "未找到匹配产品" : "暂无产品"); const emptyText = computed(() => isLoading.value ? "正在加载产品" : keyword.value ? "未找到匹配产品" : "暂无产品");
function getProductPhotoURL(item: Product): string {
const attachment = item.attachmentList?.find(item => item.attachType === "PHOTO" && !!item.id);
return attachment?.id ? getAttachmentReadURL(attachment.id, 128) : "";
}
async function loadCategories(): Promise<void> { async function loadCategories(): Promise<void> {
const result = await ProductAPI.categoryList({ index: 0, size: 1000, status: "ACTIVE" }); const result = await ProductAPI.categoryList({ index: 0, size: 1000, status: "ACTIVE" });
categoryList.value = result.list || []; categoryList.value = result.list || [];
+7 -1
View File
@@ -1,4 +1,5 @@
import type { ProductCategoryStatus, ProductStatus, ProductType, StockOperationType } from "@/types/Product"; import type { Product, ProductCategoryStatus, ProductStatus, ProductType, StockOperationType } from "@/types/Product";
import { getAttachmentReadURL } from "@/utils/attachment.ts";
/** 生成产品域写操作的幂等请求号。一次失败重试应复用调用方已经生成的请求号。 */ /** 生成产品域写操作的幂等请求号。一次失败重试应复用调用方已经生成的请求号。 */
export function createProductRequestNo(): string { export function createProductRequestNo(): string {
@@ -76,6 +77,11 @@ export function getStockOperationText(type?: StockOperationType): string {
} as Record<string, string>)[type || ""] || "未知操作"; } as Record<string, string>)[type || ""] || "未知操作";
} }
export function getProductPhotoURL(item: Product): string {
const attachment = item.attachmentList?.find(item => item.attachType === "PHOTO" && !!item.id);
return attachment?.id ? getAttachmentReadURL(attachment.id, 128) : "";
}
/** 将数量转换成稳定的展示文本,兼容后端 BigDecimal 字符串。 */ /** 将数量转换成稳定的展示文本,兼容后端 BigDecimal 字符串。 */
export function getProductQuantityText(value?: number | string | null): string { export function getProductQuantityText(value?: number | string | null): string {
if (value === undefined || value === null || value === "") { if (value === undefined || value === null || value === "") {