fix ProductDetail.vue style

This commit is contained in:
Timi
2026-09-07 13:10:45 +08:00
parent 1d70aebd83
commit 62cedb406a
+281 -14
View File
@@ -32,23 +32,37 @@
<t-cell <t-cell
v-for="sku in product.skuList || []" v-for="sku in product.skuList || []"
:key="sku.id || sku.code" :key="sku.id || sku.code"
role="button"
tabindex="0"
arrow
hover
@click="openSkuDetail(sku)"
> >
<template #title> <template #title>
<div class="title"> <div class="title">
<h3 class="name" v-text="getSkuTitle(sku)" /> <t-tag
<t-tag size="small" variant="light-outline" :theme="sku.status === 'ACTIVE' ? 'success' : 'warning'" :content="sku.status === 'ACTIVE' ? '启用' : '停用'" /> class="code"
size="small"
variant="light"
:content="sku.code || '无编码'"
/>
<h5 class="name" v-text="getSkuTitle(sku)" />
</div> </div>
</template> </template>
<template #note> <template #note>
<span class="price"> <t-tag
<span class="value text-bold" v-text="sku.code || '无编码'" /> size="small"
</span> variant="light-outline"
:theme="sku.status === 'ACTIVE' ? 'success' : 'warning'"
:content="sku.status === 'ACTIVE' ? '启用' : '停用'"
/>
</template> </template>
<template #description> <template #description>
<div class="tags"> <div class="tags">
<t-tag <t-tag
v-for="unit in sku.unitList || []" v-for="unit in sku.unitList || []"
:key="unit.id || unit.name" :key="unit.id || unit.name"
size="small"
theme="primary" theme="primary"
variant="light-outline" variant="light-outline"
:content="`${unit.name}${unit.isBase ? '(基础)' : ''}`" :content="`${unit.name}${unit.isBase ? '(基础)' : ''}`"
@@ -61,10 +75,82 @@
</template> </template>
</div> </div>
<nav-bar-action-menu v-if="actionList.length" :actions="actionList" /> <nav-bar-action-menu v-if="actionList.length" :actions="actionList" />
<bottom-popup
v-model="skuDetailVisible"
title="SKU 详情"
height="min(88vh, 48rem)"
left-button-text="关闭"
right-button-text=""
@left-button-click="closeSkuDetail"
>
<div v-if="selectedSku" class="sku-popup">
<div class="sku-popup-header">
<div class="sku-heading">
<strong v-text="getSkuTitle(selectedSku)" />
<span class="code" v-text="selectedSku.code || '无 SKU 编码'" />
</div>
<t-tag
size="small"
variant="light-outline"
:theme="selectedSku.status === 'ACTIVE' ? 'success' : 'warning'"
:content="selectedSku.status === 'ACTIVE' ? '启用' : '停用'"
/>
</div>
<div class="sku-spec-panel">
<div class="section-title">
<strong>销售规格</strong>
<span v-if="selectedSku.specSummary" v-text="`摘要:${selectedSku.specSummary}`" />
</div>
<div v-if="skuSpecEntries.length" class="sku-spec-list">
<div v-for="entry in skuSpecEntries" :key="`${entry.specName}-${entry.valueName}`" class="sku-spec-item">
<span class="spec-name" v-text="entry.specName" />
<strong v-text="entry.valueName" />
</div>
</div>
<div v-else class="sku-spec-empty" v-text="product?.specList?.length ? '暂无匹配的规格值' : '无多规格产品,使用默认 SKU'" />
</div>
<t-cell-group title="基本信息" theme="card">
<t-cell title="明细名称" :note="selectedSku.name || '未命名 SKU'" />
<t-cell title="SKU 编码" :note="selectedSku.code || '无编码'" />
<t-cell v-if="selectedSku.barcode" title="SKU 条码" :note="selectedSku.barcode" />
<t-cell title="库存管理" :note="selectedSku.stockEnabled ? '启用' : '关闭'" />
<t-cell title="允许小数数量" :note="selectedSku.allowDecimalQuantity ? '允许' : '不允许'" />
<t-cell v-if="selectedSku.stockEnabled" title="最低库存" :note="`${getProductQuantityText(selectedSku.minStockQuantity)} 基础单位`" />
<t-cell v-if="selectedSku.remark" title="备注" :note="Text.display(selectedSku.remark)" />
</t-cell-group>
<photo-scroller
v-if="skuPhotoItemList.length"
:model-value="skuPhotoUrlList"
:photo-items="skuPhotoItemList"
title="SKU 图片"
:show-add="false"
:show-remove="false"
:show-viewer-actions="false"
/>
<div class="unit-section">
<div class="unit-heading">
<strong>销售单位</strong>
<span>采购、销售和库存操作共用</span>
</div>
<div v-if="selectedSku.unitList?.length" class="unit-list">
<div v-for="unit in selectedSku.unitList" :key="unit.id || unit.name" class="unit-item">
<div class="unit-name">
<strong v-text="unit.name || '未命名单位'" />
<t-tag v-if="unit.isBase" size="small" theme="primary" variant="light-outline" content="基础" />
</div>
<div class="unit-meta" v-text="getUnitSummary(unit)" />
</div>
</div>
<div v-else class="unit-empty" v-text="'暂无销售单位'" />
</div>
</div>
</bottom-popup>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { DialogPlugin } from "tdesign-mobile-vue"; import { DialogPlugin } from "tdesign-mobile-vue";
import { BottomPopup } from "timi-tdesign-mobile";
import { useUserStore } from "timi-web-api"; import { useUserStore } from "timi-web-api";
import ProductAPI from "@/api/ProductAPI"; import ProductAPI from "@/api/ProductAPI";
import NavBarActionMenu from "@/components/NavBarActionMenu.vue"; import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
@@ -73,10 +159,10 @@ import ProductProfile from "@/pages/product/component/ProductProfile.vue";
import { useNavBarStore } from "@/store/navBarStore"; import { useNavBarStore } from "@/store/navBarStore";
import type { NavBarAction } from "@/types/NavBarAction"; import type { NavBarAction } from "@/types/NavBarAction";
import type { PhotoItem } from "@/types/Photo"; import type { PhotoItem } from "@/types/Photo";
import type { Product } from "@/types/Product"; import type { Product, ProductSku, ProductSkuUnit } 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 } from "@/utils/product"; import { formatProductAmount, getProductQuantityText, getProductSkuSpecEntries, getProductStatusText } 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"; import { Text } from "timi-web";
@@ -89,6 +175,8 @@ const navBarStore = useNavBarStore();
const userStore = useUserStore(); const userStore = useUserStore();
const product = ref<Product>(); const product = ref<Product>();
const isLoading = ref(false); const isLoading = ref(false);
const selectedSku = ref<ProductSku>();
const skuDetailVisible = ref(false);
const attachmentItemList = computed<PhotoItem[]>(() => (product.value?.attachmentList || []) const attachmentItemList = computed<PhotoItem[]>(() => (product.value?.attachmentList || [])
.filter(item => !!item.id) .filter(item => !!item.id)
.map(item => ({ .map(item => ({
@@ -98,6 +186,16 @@ const attachmentItemList = computed<PhotoItem[]>(() => (product.value?.attachmen
url: getAttachmentReadURL(item.id || "", 90) url: getAttachmentReadURL(item.id || "", 90)
}))); })));
const attachmentUrlList = computed(() => attachmentItemList.value.map(item => item.url)); const attachmentUrlList = computed(() => attachmentItemList.value.map(item => item.url));
const skuSpecEntries = computed(() => getProductSkuSpecEntries(selectedSku.value || { specValueIdList: [] }, product.value?.specList || []));
const skuPhotoItemList = computed<PhotoItem[]>(() => (selectedSku.value?.attachmentList || [])
.filter(item => !!item.id)
.map(item => ({
attachment: item,
attachType: item.attachType || "PHOTO",
previewUrl: getAttachmentReadURL(item.id || ""),
url: getAttachmentReadURL(item.id || "", 90)
})));
const skuPhotoUrlList = computed(() => skuPhotoItemList.value.map(item => item.url));
const actionList = computed<NavBarAction[]>(() => { const actionList = computed<NavBarAction[]>(() => {
const list: NavBarAction[] = []; const list: NavBarAction[] = [];
if (userStore.hasPermissions(permissionCodes.product.update)) { if (userStore.hasPermissions(permissionCodes.product.update)) {
@@ -146,6 +244,23 @@ function getSkuTitle(sku: NonNullable<Product["skuList"]>[number]): string {
return sku.specSummary || sku.name || "未命名 SKU"; return sku.specSummary || sku.name || "未命名 SKU";
} }
function openSkuDetail(sku: ProductSku): void {
selectedSku.value = sku;
skuDetailVisible.value = true;
}
function closeSkuDetail(): void {
skuDetailVisible.value = false;
}
function getUnitSummary(unit: ProductSkuUnit): string {
const quantityText = unit.isBase ? "基础单位" : `1 ${unit.name || "单位"} = ${getProductQuantityText(unit.baseQuantityPerUnit)} 基础单位`;
const priceText = unit.salePrice === null || unit.salePrice === undefined ? "未定价" : `¥${formatProductAmount(unit.salePrice)}`;
const saleText = unit.allowSale ? "可销售" : "停售";
const purchaseText = unit.allowPurchase ? "可采购" : "禁采购";
return `${quantityText} · ${priceText} · ${saleText} · ${purchaseText}`;
}
function getCurrentRouteDepth(): number { function getCurrentRouteDepth(): number {
const transitionDepth = Array.isArray(route.query.transitionDepth) const transitionDepth = Array.isArray(route.query.transitionDepth)
? route.query.transitionDepth[0] ? route.query.transitionDepth[0]
@@ -214,13 +329,6 @@ onUnmounted(() => navBarStore.clearRight());
} }
} }
.price {
.value {
font-size: 1.5rem;
}
}
.tags { .tags {
gap: .5rem; gap: .5rem;
display: flex; display: flex;
@@ -229,6 +337,157 @@ onUnmounted(() => navBarStore.clearRight());
} }
} }
.sku-popup {
max-height: 100%;
padding: 1rem 0 calc(1rem + env(safe-area-inset-bottom, 0px));
overflow-y: auto;
.sku-popup-header {
gap: .75rem;
display: flex;
padding: 0 1rem .75rem;
align-items: center;
justify-content: space-between;
}
.sku-heading {
gap: .5rem;
min-width: 0;
display: flex;
flex-direction: column;
strong {
font-size: 1.125rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.code {
color: var(--td-text-color-secondary);
font-size: .75rem;
}
}
.sku-spec-panel {
margin: 0 .75rem .75rem;
padding: .75rem;
background: var(--td-bg-color-secondarycontainer);
border-radius: .875rem;
}
.section-title {
gap: .5rem;
display: flex;
align-items: baseline;
justify-content: space-between;
span {
color: var(--td-text-color-secondary);
font-size: .75rem;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.sku-spec-list {
gap: .5rem;
display: grid;
margin-top: .625rem;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.sku-spec-item {
gap: .375rem;
min-width: 0;
display: flex;
padding: .5rem .625rem;
align-items: baseline;
background: var(--td-bg-color-container);
border-radius: .625rem;
.spec-name {
color: var(--td-text-color-secondary);
font-size: .75rem;
flex: none;
}
strong {
min-width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.sku-spec-empty,
.unit-empty {
color: var(--td-text-color-secondary);
font-size: .75rem;
}
.sku-spec-empty {
margin-top: .5rem;
}
:deep(.t-cell-group) {
margin: 0 0 .75rem;
}
:deep(.photo-scroller) {
margin: 0 .75rem .75rem;
}
.unit-section {
margin-top: .75rem;
border-top: 1px solid var(--td-component-border);
}
.unit-heading {
gap: .5rem;
display: flex;
padding: .75rem 1rem .5rem;
align-items: baseline;
justify-content: space-between;
span {
color: var(--td-text-color-secondary);
font-size: .75rem;
text-align: right;
}
}
.unit-list {
gap: .5rem;
display: flex;
padding: 0 1rem .75rem;
flex-direction: column;
}
.unit-item {
gap: .375rem;
display: flex;
padding: .625rem .75rem;
background: var(--td-bg-color-secondarycontainer);
border-radius: .75rem;
flex-direction: column;
}
.unit-name {
gap: .5rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.unit-meta {
color: var(--td-text-color-secondary);
font-size: .75rem;
line-height: 1.5;
}
}
:global(html.theme-dark) .product-detail .hero { :global(html.theme-dark) .product-detail .hero {
background: linear-gradient(135deg, rgba(15, 118, 110, .2), rgba(37, 99, 235, .18)); background: linear-gradient(135deg, rgba(15, 118, 110, .2), rgba(37, 99, 235, .18));
@@ -237,4 +496,12 @@ onUnmounted(() => navBarStore.clearRight());
} }
} }
:global(html.theme-dark) .sku-popup {
.sku-spec-panel,
.sku-spec-item,
.unit-item {
background: rgba(148, 163, 184, .12);
}
}
</style> </style>