fix ProductEdit.vue style

This commit is contained in:
Timi
2026-09-03 16:09:56 +08:00
parent 2acdf46d5d
commit 1e7441e5d7
11 changed files with 160 additions and 114 deletions
+1 -1
View File
@@ -371,9 +371,9 @@ onUnmounted(() => navBarStore.clearRight());
.item {
gap: .5rem;
border: 1px dashed var(--td-component-border);
padding: .5rem 1rem;
margin-bottom: .75rem;
border: 1px dashed var(--td-component-border);
border-radius: 1rem;
.header {
+8 -19
View File
@@ -1,8 +1,8 @@
<template>
<div v-can="permissionCodes.product.read" class="product-detail">
<t-loading v-if="isLoading" size="32px" />
<t-loading v-if="isLoading" class="loading" size="32px" />
<template v-if="product">
<card-container class="info">
<card-container class="info" v-entrance-transition>
<div class="content">
<div class="left">
<t-image
@@ -38,10 +38,9 @@
</div>
</div>
</card-container>
<t-cell-group title="基本信息" theme="card">
<t-cell-group title="基本信息" theme="card" v-entrance-transition="70">
<t-cell title="产品分类" :note="product.category?.name || '未分类'" />
<t-cell title="基础单位" :note="product.baseUnit || '-'" />
<t-cell title="参考成本" :note="formatProductAmount(product.referenceCost)" />
<t-cell title="基础单位" :note="getProductBaseUnitName(product) || '-'" />
<t-cell title="库存管理" :note="product.stockEnabled ? '启用' : '关闭'" />
<t-cell title="状态" :note="getProductStatusText(product.status)" />
<t-cell title="备注" :note="Text.display(product.remark)"></t-cell>
@@ -56,7 +55,7 @@
/>
</t-cell>
</t-cell-group>
<t-cell-group title="业务单位" theme="card" class="units">
<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"
@@ -68,7 +67,7 @@
v-if="!unit.isBase"
theme="default"
size="small"
:content="`${unit.baseQuantityPerUnit} ${product.baseUnit}`"
:content="`${unit.baseQuantityPerUnit} ${getProductBaseUnitName(product)}`"
/>
</div>
</template>
@@ -119,11 +118,12 @@ 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 type { Product } from "@/types/Product";
import { getAttachmentReadURL } from "@/utils/attachment";
import { getErrorMessage } from "@/utils/error";
import {
formatProductAmount,
getProductBaseUnitName,
getProductPhotoURL,
getProductStatusText,
getProductStatusTheme,
@@ -164,17 +164,6 @@ const actionList = computed<NavBarAction[]>(() => {
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) {
+132 -81
View File
@@ -1,32 +1,46 @@
<template>
<div v-can="savePermissionCode" class="product-edit">
<t-form class="form" label-align="left" label-width="6rem">
<t-form-item label="产品编码" required-mark>
<t-input v-model.trim="form.code" borderless maxlength="64" placeholder="门店内唯一编码" />
<t-form class="form" label-align="left" label-width="6rem" v-entrance-transition>
<t-form-item label="产品编码" required-mark help="使用英语、拼音或唯一数字">
<t-input
v-model="form.code"
borderless
maxlength="64"
placeholder="门店内唯一编码"
/>
</t-form-item>
<t-form-item label="产品名称" required-mark>
<t-input v-model.trim="form.name" borderless maxlength="128" placeholder="请输入产品名称" />
<t-input
v-model="form.name"
borderless
maxlength="128"
placeholder="请输入产品名称"
/>
</t-form-item>
<t-form-item label="产品类型" arrow content-align="right" @click="openTypePicker">
<t-form-item
label="产品类型"
arrow
content-align="right"
@click="openTypePicker"
>
<span class="picker-text" v-text="getProductTypeText(form.productType)" />
</t-form-item>
<t-form-item label="产品分类" arrow content-align="right" @click="openCategoryPicker">
<span class="picker-text" v-text="selectedCategoryName" />
</t-form-item>
<t-form-item label="基础单位" required-mark>
<t-input v-model.trim="form.baseUnit" borderless maxlength="32" placeholder="例如 次、瓶" />
<t-form-item label="库存管理" content-align="right">
<t-switch size="small" v-model="form.stockEnabled" :disabled="form.productType === 'SERVICE'" />
</t-form-item>
<t-form-item label="参考成本(元)">
<t-input v-model.trim="referenceCostText" type="number" align="right" borderless placeholder="非负金额" />
<t-form-item label="允许小数" content-align="right">
<t-switch size="small" v-model="form.allowDecimalQuantity" />
</t-form-item>
<t-form-item label="库存管理">
<t-checkbox v-model="form.stockEnabled" :disabled="form.productType === 'SERVICE'">管理库存</t-checkbox>
</t-form-item>
<t-form-item label="允许小数">
<t-checkbox v-model="form.allowDecimalQuantity">业务数量允许小数</t-checkbox>
</t-form-item>
<t-form-item v-if="form.stockEnabled" label="最低库存">
<t-input v-model.trim="minStockQuantityText" type="number" align="right" borderless placeholder="基础单位数量,最多三位小数" />
<t-form-item v-if="form.stockEnabled" label="最低库存" help="基础单位数量,最多三位小数">
<t-input
v-model="minStockQuantityText"
type="number"
align="right"
borderless
/>
</t-form-item>
<t-form-item label="状态" arrow content-align="right" @click="openStatusPicker">
<span class="picker-text" v-text="getProductStatusText(form.status)" />
@@ -34,55 +48,88 @@
<t-form-item label="备注">
<t-textarea v-model.trim="form.remark" borderless maxlength="255" autosize placeholder="可填写产品说明" />
</t-form-item>
<t-form-item label-width="0">
<photo-scroller
v-model="attachmentUrlList"
v-model:photo-items="attachmentItemList"
title="附件列表"
:edit-mode="true"
:loading="isAttachmentUploading"
:show-viewer-actions="false"
:add-actions="attachmentAddActions"
@add-photo="openAttachmentPicker('PHOTO')"
@add-other="openAttachmentPicker('OTHER')"
/>
</t-form-item>
</t-form>
<card-container title="产品附件" description="可上传产品图片或其他产品文件" v-entrance-transition="70">
<photo-scroller
v-model="attachmentUrlList"
v-model:photo-items="attachmentItemList"
title="附件列表"
:edit-mode="true"
:loading="isAttachmentUploading"
:show-viewer-actions="false"
:add-actions="attachmentAddActions"
@add-photo="openAttachmentPicker('PHOTO')"
@add-other="openAttachmentPicker('OTHER')"
/>
<input ref="attachmentInputRef" class="file-input" type="file" accept="image/*" @change="handleAttachmentSelected" />
</card-container>
<card-container title="产品单位" description="必须保留一个基础单位;销售和库存业务只能选择允许的单位" v-entrance-transition="70">
<div class="unit-list">
<div v-for="(unit, index) in form.unitList" :key="unit.clientKey" class="unit-item">
<div class="unit-header">
<strong v-text="`单位 ${index + 1}`" />
<t-button v-if="1 < form.unitList.length" theme="danger" variant="text" size="small" @click="removeUnit(index)">删除</t-button>
</div>
<t-form label-width="6rem">
<t-form-item label="单位名称" required-mark>
<t-input v-model.trim="unit.name" borderless maxlength="32" placeholder="例如 瓶、盒、次" />
</t-form-item>
<t-form-item label="基础单位数量" required-mark>
<t-input v-model.trim="unit.baseQuantityPerUnit" type="number" align="right" borderless placeholder="每个单位对应的基础单位数量" />
</t-form-item>
<t-form-item label="建议售价(元)">
<t-input v-model.trim="unit.salePriceText" type="number" align="right" borderless placeholder="非负金额" />
</t-form-item>
<t-form-item label="基础单位">
<t-checkbox v-model="unit.isBase" @change="setBaseUnit(index)">基础单位</t-checkbox>
</t-form-item>
<t-form-item label="允许采购">
<t-checkbox v-model="unit.allowPurchase">入库可用</t-checkbox>
</t-form-item>
<t-form-item label="允许销售">
<t-checkbox v-model="unit.allowSale">销售和套餐可用</t-checkbox>
</t-form-item>
</t-form>
</div>
<t-button class="add-unit" theme="primary" variant="dashed" block @click="addUnit">添加业务单位</t-button>
<card-container
title="产品单位"
description="必须有且仅有一个基础单位;销售和库存业务只能选择允许的单位"
v-entrance-transition="70"
>
<div class="units">
<t-form
class="unit"
v-for="(unit, index) in form.unitList"
:key="unit.clientKey"
>
<t-form-item label="单位名称" required-mark>
<t-input v-model.trim="unit.name" borderless maxlength="32" placeholder="如 瓶、次" />
<!-- <t-button v-if="1 < form.unitList.length" theme="danger" variant="text" size="small" @click="removeUnit(index)">删除</t-button>-->
<t-button
theme="danger"
variant="text"
size="extra-small"
content="删除"
@click="removeUnit(index)"
/>
</t-form-item>
<t-form-item label="基础单位" content-align="right">
<t-switch
size="small"
v-model="unit.isBase"
:disabled="form.unitList.length === 1"
@change="setBaseUnit(index)"
/>
</t-form-item>
<t-form-item
label="基础单位数量"
help="每个单位对应的基础单位数量"
label-width="7rem"
required-mark
>
<t-input
v-model="unit.baseQuantityPerUnit"
type="number"
align="right"
borderless
:disabled="form.unitList.length === 1"
/>
</t-form-item>
<t-form-item label="建议售价">
<t-input
v-model="unit.salePriceText"
type="number"
align="right"
borderless
placeholder="金额(元)"
/>
</t-form-item>
<t-form-item label="允许采购" content-align="right">
<t-switch size="small" v-model="unit.allowPurchase" />
</t-form-item>
<t-form-item label="允许销售" content-align="right">
<t-switch size="small" v-model="unit.allowSale" />
</t-form-item>
</t-form>
<t-button
class="cell-button"
theme="primary"
content="添加业务单位"
@click="addUnit"
/>
</div>
</card-container>
<t-popup v-model="typePickerVisible" placement="bottom">
<t-picker v-model="typeDraft" :columns="productTypeOptions" title="产品类型" @cancel="typePickerVisible = false" @confirm="confirmType" />
</t-popup>
@@ -92,6 +139,7 @@
<t-popup v-model="statusPickerVisible" placement="bottom">
<t-picker v-model="statusDraft" :columns="statusOptions" title="产品状态" @cancel="statusPickerVisible = false" @confirm="confirmStatus" />
</t-popup>
<input ref="attachmentInputRef" class="file-input" type="file" accept="image/*" @change="handleAttachmentSelected" />
</div>
</template>
@@ -131,14 +179,12 @@ const form = reactive<Product & { unitList: ProductUnitForm[] }>({
code: "",
name: "",
productType: "GOODS",
baseUnit: "",
stockEnabled: true,
allowDecimalQuantity: false,
status: "ACTIVE",
unitList: []
});
const referenceCostText = ref("");
const minStockQuantityText = ref("");
const minStockQuantityText = ref("0");
const categoryList = ref<ProductCategory[]>([]);
const typePickerVisible = ref(false);
const categoryPickerVisible = ref(false);
@@ -331,19 +377,14 @@ function parseOptionalAmount(value: string): number | null | undefined {
}
function validate(): boolean {
if (!form.code.trim() || !form.name.trim() || !form.baseUnit.trim()) {
Message.warning("产品编码、名称和基础单位不能为空");
if (!form.code.trim() || !form.name.trim()) {
Message.warning("产品编码和名称不能为空");
return false;
}
if (form.productType === "SERVICE" && form.stockEnabled) {
Message.warning("服务产品不能管理库存");
return false;
}
const referenceCost = parseOptionalAmount(referenceCostText.value);
if (referenceCost === undefined) {
Message.warning("价格必须是合法的非负元金额,最多两位小数");
return false;
}
if (form.stockEnabled && !normalizeProductQuantity(minStockQuantityText.value || "0")) {
Message.warning("最低库存必须是非负数量,最多三位小数");
return false;
@@ -377,10 +418,6 @@ function validate(): boolean {
Message.warning("基础单位数量必须为 1");
return false;
}
if (name !== form.baseUnit.trim()) {
Message.warning("基础单位名称必须与产品基础单位一致");
return false;
}
}
}
if (baseCount !== 1) {
@@ -404,11 +441,9 @@ async function save(): Promise<void> {
code: form.code.trim(),
name: form.name.trim(),
productType: form.productType,
baseUnit: form.baseUnit.trim(),
stockEnabled: form.stockEnabled,
allowDecimalQuantity: form.allowDecimalQuantity,
status: form.status,
referenceCost: parseOptionalAmount(referenceCostText.value)!,
minStockQuantity: form.stockEnabled ? minStockQuantity : null,
remark: form.remark?.trim() || null,
attachmentList: buildAttachmentList(),
@@ -456,7 +491,6 @@ async function load(): Promise<void> {
code: result.code || "",
name: result.name || "",
productType: result.productType || "GOODS",
baseUnit: result.baseUnit || "",
stockEnabled: !!result.stockEnabled,
allowDecimalQuantity: !!result.allowDecimalQuantity,
status: result.status || "ACTIVE",
@@ -471,7 +505,6 @@ async function load(): Promise<void> {
previewUrl: getAttachmentReadURL(item.id || ""),
url: getAttachmentReadURL(item.id || "", 90)
}));
referenceCostText.value = result.referenceCost === null || result.referenceCost === undefined ? "" : `${Math.floor(result.referenceCost / 100)}.${String(result.referenceCost % 100).padStart(2, "0")}`;
minStockQuantityText.value = result.minStockQuantity === null || result.minStockQuantity === undefined ? "" : String(result.minStockQuantity);
if (!form.unitList.length) {
addUnit();
@@ -508,6 +541,24 @@ onUnmounted(() => navBarStore.clearRight());
overflow: hidden;
}
.units {
padding: 1rem 0;
.unit {
gap: .5rem;
border: 1px dashed var(--td-component-border);
padding: .5rem 0;
margin-bottom: .75rem;
border-radius: 1rem;
.header {
display: flex;
align-items: center;
justify-content: space-between;
}
}
}
.unit-list {
padding: 1rem;
-1
View File
@@ -81,7 +81,6 @@ import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
import { useNavBarStore } from "@/store/navBarStore";
import type { NavBarAction } from "@/types/NavBarAction";
import type { Product, ProductCategory, ProductStatus, ProductType } from "@/types/Product";
import { getAttachmentReadURL } from "@/utils/attachment";
import { getErrorMessage } from "@/utils/error";
import { getProductPhotoURL, getProductStatusText, getProductStatusTheme, getProductTypeText } from "@/utils/product";
import { Message } from "@/utils/message";
+4 -4
View File
@@ -10,13 +10,13 @@
</div>
<div class="hero-meta">
<span v-text="product.code || '无产品编码'" />
<strong v-text="`${getProductQuantityText(stock.availableQuantity)} ${product.baseUnit || ''}`" />
<strong v-text="`${getProductQuantityText(stock.availableQuantity)} ${getProductBaseUnitName(product)}`" />
</div>
</section>
<card-container title="库存余额">
<t-cell-group theme="card" class="summary">
<t-cell title="产品" :note="product.name || '未命名产品'" />
<t-cell title="基础单位" :note="product.baseUnit || '-'" />
<t-cell title="基础单位" :note="getProductBaseUnitName(product) || '-'" />
<t-cell title="实际库存" :note="getProductQuantityText(stock.onHandQuantity)" />
<t-cell title="预占库存" :note="getProductQuantityText(stock.reservedQuantity)" />
<t-cell title="可用库存" :note="getProductQuantityText(stock.availableQuantity)" />
@@ -43,7 +43,7 @@
</template>
<template #description>
<div class="record-description">
<span v-text="`${record.quantity || 0} ${record.unitSnapshot || product.baseUnit || ''} · 基础单位 ${record.baseQuantity || 0}`" />
<span v-text="`${record.quantity || 0} ${record.unitSnapshot || getProductBaseUnitName(product)} · 基础单位 ${record.baseQuantity || 0}`" />
<span class="gray" v-text="record.reason || record.remark || '无备注'" />
</div>
</template>
@@ -72,7 +72,7 @@ import ProductStockOperationDialog from "@/pages/product/ProductStockOperationDi
import { useNavBarStore } from "@/store/navBarStore";
import type { Product, ProductStock, StockOperationType, StockRecord } from "@/types/Product";
import { getErrorMessage } from "@/utils/error";
import { getProductQuantityText, getStockOperationText } from "@/utils/product";
import { getProductBaseUnitName, getProductQuantityText, getStockOperationText } from "@/utils/product";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
+2 -2
View File
@@ -13,7 +13,7 @@
</div>
</template>
<template #note>
<span v-text="`${getProductQuantityText(item.availableQuantity)} ${item.product?.baseUnit || ''}`" />
<span v-text="`${getProductQuantityText(item.availableQuantity)} ${getProductBaseUnitName(item.product)}`" />
</template>
<template #description>
<div class="description">
@@ -35,7 +35,7 @@ import ProductAPI from "@/api/ProductAPI";
import { useNavBarStore } from "@/store/navBarStore";
import type { ProductStock } from "@/types/Product";
import { getErrorMessage } from "@/utils/error";
import { getProductQuantityText } from "@/utils/product";
import { getProductBaseUnitName, getProductQuantityText } from "@/utils/product";
import { Message } from "@/utils/message";
import { permissionCodes } from "@/utils/permission";
@@ -10,7 +10,7 @@
<div class="operation-dialog">
<div class="summary">
<strong v-text="props.product.name || '未命名产品'" />
<span class="gray" v-text="`当前可用库存:${getProductQuantityText(props.stock?.availableQuantity)} ${props.product.baseUnit || ''}`" />
<span class="gray" v-text="`当前可用库存:${getProductQuantityText(props.stock?.availableQuantity)} ${getProductBaseUnitName(props.product)}`" />
</div>
<t-form label-align="left" label-width="5rem">
<t-form-item label="业务单位" required-mark @click="openUnitPicker">
@@ -39,7 +39,7 @@ import { watch } from "vue";
import ProductAPI from "@/api/ProductAPI";
import type { Product, ProductStock, ProductUnit, ProductStockChangeRequest, StockOperationType } from "@/types/Product";
import { getErrorMessage } from "@/utils/error";
import { createProductRequestNo, getProductQuantityText, getStockOperationText } from "@/utils/product";
import { createProductRequestNo, getProductBaseUnitName, getProductQuantityText, getStockOperationText } from "@/utils/product";
import { Message } from "@/utils/message";
import type { PhotoItem } from "@/types/Photo";