fix PackageConfig*.vue style
This commit is contained in:
@@ -67,6 +67,7 @@ import { getProductPhotoURL } from "@/utils/product";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue: { productId: string; productUnitId: string } | undefined;
|
modelValue: { productId: string; productUnitId: string } | undefined;
|
||||||
|
selectedProduct?: Product;
|
||||||
label?: string;
|
label?: string;
|
||||||
requiredMark?: boolean;
|
requiredMark?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -82,7 +83,7 @@ const selectedText = computed(() => {
|
|||||||
if (!props.modelValue) {
|
if (!props.modelValue) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
const product = productList.value.find(item => item.id === props.modelValue?.productId);
|
const product = props.selectedProduct || productList.value.find(item => item.id === props.modelValue?.productId);
|
||||||
return product?.name || "";
|
return product?.name || "";
|
||||||
});
|
});
|
||||||
const filteredProductList = computed(() => {
|
const filteredProductList = computed(() => {
|
||||||
|
|||||||
@@ -2,10 +2,23 @@
|
|||||||
<div v-can="permissionCodes.packageConfig.read" class="package-config-detail">
|
<div v-can="permissionCodes.packageConfig.read" class="package-config-detail">
|
||||||
<template v-if="config">
|
<template v-if="config">
|
||||||
<section class="hero" v-entrance-transition>
|
<section class="hero" v-entrance-transition>
|
||||||
<span class="eyebrow" v-text="'套餐管理'" />
|
<div class="header">
|
||||||
<div class="title"><h1 v-text="config.name || '未命名套餐'" /><t-tag :content="getPackageConfigStatusText(config.status)" variant="light-outline" :theme="getStatusTheme(config.status)" /></div>
|
<span class="eyebrow" v-text="'套餐管理'" />
|
||||||
<p v-text="config.description || '暂无套餐说明'" />
|
<t-tag
|
||||||
<div class="hero-meta"><span v-text="`${config.itemList?.length || 0} 项产品权益`" /><strong v-text="formatPackageAmount(config.standardAmount)" /></div>
|
:content="getPackageConfigStatusText(config.status)"
|
||||||
|
variant="light-outline"
|
||||||
|
:theme="getStatusTheme(config.status)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 class="title" v-text="config.name || '未命名套餐'" />
|
||||||
|
<p class="description" v-text="config.description || '暂无套餐说明'" />
|
||||||
|
<div class="items-price">
|
||||||
|
<span v-text="`${config.itemList?.length || 0} 项产品权益`" />
|
||||||
|
<div class="price text-bold">
|
||||||
|
<span class="red">¥</span>
|
||||||
|
<span class="red value" v-text="formatPackageAmount(config.standardAmount)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<t-cell-group title="基本信息" theme="card" v-entrance-transition="70">
|
<t-cell-group title="基本信息" theme="card" v-entrance-transition="70">
|
||||||
<t-cell title="标准价" :note="formatPackageAmount(config.standardAmount)" />
|
<t-cell title="标准价" :note="formatPackageAmount(config.standardAmount)" />
|
||||||
@@ -13,13 +26,36 @@
|
|||||||
<t-cell title="终止日期" :note="config.endAt ? Time.toDateTime(config.endAt) : '不限'" />
|
<t-cell title="终止日期" :note="config.endAt ? Time.toDateTime(config.endAt) : '不限'" />
|
||||||
<t-cell title="最后修改人" :note="config.updater?.name || config.updater?.nick || '未知'" />
|
<t-cell title="最后修改人" :note="config.updater?.name || config.updater?.nick || '未知'" />
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<t-cell-group title="产品权益" theme="card" v-entrance-transition="140">
|
<t-cell-group class="product" title="产品权益" theme="card" v-entrance-transition="140">
|
||||||
<t-cell
|
<t-cell
|
||||||
|
class="item"
|
||||||
v-for="item in config.itemList || []"
|
v-for="item in config.itemList || []"
|
||||||
|
:title="item.productNameSnapshot"
|
||||||
:key="item.id || `${item.productId}-${item.productUnitId}`"
|
:key="item.id || `${item.productId}-${item.productUnitId}`"
|
||||||
:title="item.productNameSnapshot || '未命名产品'"
|
|
||||||
:note="`${item.defaultQuantity} ${item.unitSnapshot || ''}${item.remark ? `|${item.remark}` : ''}`"
|
:note="`${item.defaultQuantity} ${item.unitSnapshot || ''}${item.remark ? `|${item.remark}` : ''}`"
|
||||||
/>
|
>
|
||||||
|
<template #leftIcon>
|
||||||
|
<t-image
|
||||||
|
v-if="getItemPhotoURL(item)"
|
||||||
|
:src="getItemPhotoURL(item)"
|
||||||
|
:alt="item.productNameSnapshot || '产品图片'"
|
||||||
|
fit="cover"
|
||||||
|
class="avatar"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="avatar fallback"
|
||||||
|
v-text="(item.productNameSnapshot || '产').slice(0, 1)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template #description>
|
||||||
|
<t-tag
|
||||||
|
:content="getProductTypeText(item.product?.productType)"
|
||||||
|
theme="primary"
|
||||||
|
variant="light"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
<div v-if="!config.itemList?.length" class="empty" v-text="'暂无产品权益'" />
|
<div v-if="!config.itemList?.length" class="empty" v-text="'暂无产品权益'" />
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<t-button
|
<t-button
|
||||||
@@ -48,11 +84,12 @@ import { useUserStore } from "timi-web-api";
|
|||||||
import PackageAPI from "@/api/PackageAPI";
|
import PackageAPI from "@/api/PackageAPI";
|
||||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { GaoPackageConfig, PackageConfigStatus } from "@/types/Package";
|
import type { GaoPackageConfig, PackageConfigItem, PackageConfigStatus } from "@/types/Package";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { formatPackageAmount, getPackageConfigStatusText } from "@/utils/package";
|
import { formatPackageAmount, getPackageConfigStatusText } from "@/utils/package";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
|
import { getProductPhotoURL as getProductPhotoUrl, getProductTypeText } from "@/utils/product";
|
||||||
|
|
||||||
defineOptions({ name: "PackageConfigDetail" });
|
defineOptions({ name: "PackageConfigDetail" });
|
||||||
|
|
||||||
@@ -75,6 +112,10 @@ function getStatusTheme(status?: PackageConfigStatus): "success" | "warning" | "
|
|||||||
return status === "ACTIVE" ? "success" : status === "INACTIVE" ? "warning" : "default";
|
return status === "ACTIVE" ? "success" : status === "INACTIVE" ? "warning" : "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getItemPhotoURL(item: PackageConfigItem): string {
|
||||||
|
return getProductPhotoUrl(item.product || {});
|
||||||
|
}
|
||||||
|
|
||||||
async function loadData(): Promise<void> {
|
async function loadData(): Promise<void> {
|
||||||
const id = String(route.params.id || "");
|
const id = String(route.params.id || "");
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@@ -157,55 +198,123 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
.hero {
|
.hero {
|
||||||
margin: 0 1rem;
|
margin: 0 1rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background: var(--td-bg-color-container);
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(135deg, #ecfeff 0%, #eff6ff 48%, #fff7ed 100%);
|
||||||
|
border: 1px solid rgba(255, 255, 255, .72);
|
||||||
box-shadow: var(--td-shadow-1);
|
box-shadow: var(--td-shadow-1);
|
||||||
border-radius: 1rem;
|
border-radius: 1.25rem;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
width: 9rem;
|
||||||
|
height: 9rem;
|
||||||
|
right: -3rem;
|
||||||
|
top: -4.5rem;
|
||||||
|
background: rgba(45, 212, 191, .2);
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(1rem);
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
width: 7rem;
|
||||||
|
height: 7rem;
|
||||||
|
left: -3.5rem;
|
||||||
|
bottom: -4rem;
|
||||||
|
background: rgba(96, 165, 250, .16);
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(.75rem);
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
> * {
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
color: var(--td-brand-color);
|
||||||
|
display: block;
|
||||||
|
font-size: .6875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .08em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
gap: .5rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.eyebrow {
|
|
||||||
color: var(--td-brand-color);
|
|
||||||
display: block;
|
|
||||||
font-size: .6875rem;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: .08em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
.description {
|
||||||
color: var(--td-text-color-secondary);
|
color: var(--td-text-color-secondary);
|
||||||
margin: .5rem 0 0;
|
margin: .5rem 0 0;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-meta {
|
.items-price {
|
||||||
gap: .75rem;
|
gap: .75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: .75rem;
|
margin-top: .75rem;
|
||||||
color: var(--td-text-color-secondary);
|
color: var(--td-text-color-secondary);
|
||||||
align-items: center;
|
align-items: end;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
.price {
|
||||||
color: #c2410c;
|
|
||||||
font-size: 1.2rem;
|
.value {
|
||||||
white-space: nowrap;
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product {
|
||||||
|
|
||||||
|
.item {
|
||||||
|
padding: 10px;
|
||||||
|
|
||||||
|
.item-title {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.package-config-detail .avatar .t-image__img) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
:global(html.theme-dark) .package-config-detail .hero {
|
:global(html.theme-dark) .package-config-detail .hero {
|
||||||
|
background: linear-gradient(135deg, rgba(15, 118, 110, .3), rgba(37, 99, 235, .28) 52%, rgba(194, 65, 12, .24));
|
||||||
|
border-color: rgba(153, 246, 228, .16);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background: rgba(45, 212, 191, .14);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background: rgba(96, 165, 250, .12);
|
||||||
|
}
|
||||||
|
|
||||||
.eyebrow {
|
.eyebrow {
|
||||||
color: #99f6e4;
|
color: #99f6e4;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-can="savePermissionCode" class="package-config-edit">
|
<div v-can="savePermissionCode" class="package-config-edit">
|
||||||
<t-form class="form" label-align="left" label-width="6rem" v-entrance-transition>
|
<t-form class="form" label-align="left" label-width="6rem" v-entrance-transition>
|
||||||
<t-form-item v-if="isGlobalManager" label="所属门店" arrow @click="openStorePicker">
|
|
||||||
<span class="picker-text" v-text="selectedStoreName" />
|
|
||||||
</t-form-item>
|
|
||||||
<t-form-item label="套餐名称" required-mark>
|
<t-form-item label="套餐名称" required-mark>
|
||||||
<t-input v-model="form.name" borderless maxlength="100" placeholder="请输入套餐名称" />
|
<t-input v-model="form.name" borderless maxlength="100" placeholder="请输入套餐名称" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
@@ -36,33 +33,75 @@
|
|||||||
label-width="6rem"
|
label-width="6rem"
|
||||||
>
|
>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
|
<span class="no" v-text="String(index + 1).padStart(2, '0')" />
|
||||||
<strong v-text="item.productNameSnapshot || '未选择产品'" />
|
<strong v-text="item.productNameSnapshot || '未选择产品'" />
|
||||||
<t-button theme="danger" variant="text" size="small" @click="removeItem(index)">删除</t-button>
|
<t-button
|
||||||
|
v-if="1 < form.itemList.length"
|
||||||
|
theme="danger"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="removeItem(index)"
|
||||||
|
>删除</t-button>
|
||||||
</div>
|
</div>
|
||||||
<product-selector
|
<product-selector
|
||||||
:model-value="item.productId && item.productUnitId ? {productId: item.productId, productUnitId: item.productUnitId} : undefined"
|
:model-value="item.productId ? {productId: item.productId, productUnitId: item.productUnitId || ''} : undefined"
|
||||||
label="产品单位"
|
:selected-product="getProduct(item)"
|
||||||
|
label="产品"
|
||||||
|
placeholder="请选择产品"
|
||||||
required-mark
|
required-mark
|
||||||
@update:model-value="applyProduct(index, $event)"
|
@update:model-value="applyProduct(index, $event)"
|
||||||
/>
|
/>
|
||||||
<t-form-item label="默认配额" required-mark>
|
<t-form-item v-if="item.productId" label="产品单位" required-mark>
|
||||||
<t-input v-model.trim="item.defaultQuantity" type="number" borderless />
|
<div v-if="getSaleableUnits(item).length" class="unit-list">
|
||||||
|
<t-button
|
||||||
|
v-for="unit in getSaleableUnits(item)"
|
||||||
|
:key="unit.id"
|
||||||
|
:theme="item.productUnitId === unit.id ? 'primary' : 'default'"
|
||||||
|
:variant="item.productUnitId === unit.id ? 'base' : 'outline'"
|
||||||
|
size="extra-small"
|
||||||
|
:content="unit.name"
|
||||||
|
@click="selectProductUnit(index, unit)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span v-else class="unit-empty" v-text="'暂无可售单位'" />
|
||||||
|
</t-form-item>
|
||||||
|
<t-form-item label="默认配额" content-align="right" required-mark>
|
||||||
|
<t-stepper
|
||||||
|
v-model="item.defaultQuantity"
|
||||||
|
size="large"
|
||||||
|
:max="Number.MAX_SAFE_INTEGER"
|
||||||
|
:min="getQuantityStep(item)"
|
||||||
|
:step="getQuantityStep(item)"
|
||||||
|
:integer="!getProduct(item)?.allowDecimalQuantity"
|
||||||
|
theme="outline"
|
||||||
|
/>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="备注">
|
<t-form-item label="备注">
|
||||||
<t-input v-model="item.remark" borderless />
|
<t-input v-model.trim="item.remark" borderless placeholder="选填" />
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
|
<div class="price">
|
||||||
|
<span>预估价(原价)</span>
|
||||||
|
<strong class="red" v-text="getItemOriginalAmountText(item)" />
|
||||||
|
</div>
|
||||||
</t-form>
|
</t-form>
|
||||||
<t-button
|
<t-button
|
||||||
class="cell-button"
|
class="add"
|
||||||
theme="primary"
|
type="button"
|
||||||
|
theme="light"
|
||||||
|
variant="dashed"
|
||||||
@click="addItem"
|
@click="addItem"
|
||||||
content="添加产品权益"
|
content="添加产品权益"
|
||||||
/>
|
>
|
||||||
|
<template #icon>
|
||||||
|
<t-icon name="add" size="1.125rem" />
|
||||||
|
</template>
|
||||||
|
</t-button>
|
||||||
|
<div class="summary">
|
||||||
|
<span>产品权益预估价(原价)</span>
|
||||||
|
<strong class="red" v-text="estimatedOriginalAmountText" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</card-container>
|
</card-container>
|
||||||
<t-popup v-model="storePickerVisible" placement="bottom">
|
|
||||||
<t-picker v-model="storePickerDraft" :columns="storePickerColumns" title="选择门店" @cancel="storePickerVisible = false" @confirm="confirmStore" />
|
|
||||||
</t-popup>
|
|
||||||
<t-popup v-model="statusPickerVisible" placement="bottom">
|
<t-popup v-model="statusPickerVisible" placement="bottom">
|
||||||
<t-picker v-model="statusPickerDraft" :columns="statusOptions" title="状态" @cancel="statusPickerVisible = false" @confirm="confirmStatus" />
|
<t-picker v-model="statusPickerDraft" :columns="statusOptions" title="状态" @cancel="statusPickerVisible = false" @confirm="confirmStatus" />
|
||||||
</t-popup>
|
</t-popup>
|
||||||
@@ -83,13 +122,13 @@ import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
|||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import type { GaoPackageConfig, PackageConfigItem, PackageConfigStatus } from "@/types/Package";
|
import type { GaoPackageConfig, PackageConfigItem, PackageConfigStatus } from "@/types/Package";
|
||||||
import ProductAPI from "@/api/ProductAPI";
|
import ProductAPI from "@/api/ProductAPI";
|
||||||
import type { Product } from "@/types/Product";
|
import type { Product, ProductUnit } from "@/types/Product";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { formatPackageAmountInput, getPackageConfigStatusText, normalizePackageQuantity, parsePackageAmount } from "@/utils/package";
|
import { formatPackageAmountInput, getPackageConfigStatusText, normalizePackageQuantity, parsePackageAmount } from "@/utils/package";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||||
|
import { formatProductAmount, getProductQuantityStep } from "@/utils/product";
|
||||||
|
|
||||||
type PickerValue = string | number;
|
|
||||||
type PackageConfigFormItem = Partial<PackageConfigItem> & { clientKey: string };
|
type PackageConfigFormItem = Partial<PackageConfigItem> & { clientKey: string };
|
||||||
|
|
||||||
defineOptions({ name: "PackageConfigEdit" });
|
defineOptions({ name: "PackageConfigEdit" });
|
||||||
@@ -117,10 +156,8 @@ const form = reactive({
|
|||||||
const standardAmountText = ref("");
|
const standardAmountText = ref("");
|
||||||
const beginAtText = ref("");
|
const beginAtText = ref("");
|
||||||
const endAtText = ref("");
|
const endAtText = ref("");
|
||||||
const storePickerVisible = ref(false);
|
|
||||||
const storePickerDraft = ref<PickerValue[]>([]);
|
|
||||||
const statusPickerVisible = ref(false);
|
const statusPickerVisible = ref(false);
|
||||||
const statusPickerDraft = ref<PickerValue[]>([]);
|
const statusPickerDraft = ref<Array<string | number>>([]);
|
||||||
const beginPickerVisible = ref(false);
|
const beginPickerVisible = ref(false);
|
||||||
const endPickerVisible = ref(false);
|
const endPickerVisible = ref(false);
|
||||||
const beginDraft = ref("");
|
const beginDraft = ref("");
|
||||||
@@ -130,8 +167,6 @@ const statusOptions = [
|
|||||||
{ label: "启用", value: "ACTIVE" },
|
{ label: "启用", value: "ACTIVE" },
|
||||||
{ label: "停用", value: "INACTIVE" }
|
{ label: "停用", value: "INACTIVE" }
|
||||||
];
|
];
|
||||||
const storePickerColumns = computed(() => [[...currentStoreStore.storeList].map(item => ({ label: item.name || item.code || item.id || "未知门店", value: item.id || "" }))]);
|
|
||||||
const selectedStoreName = computed(() => currentStoreStore.storeList.find(item => item.id === form.storeId)?.name || form.storeId || "请选择门店");
|
|
||||||
|
|
||||||
function createItem(item?: Partial<PackageConfigItem>): PackageConfigFormItem {
|
function createItem(item?: Partial<PackageConfigItem>): PackageConfigFormItem {
|
||||||
return {
|
return {
|
||||||
@@ -144,29 +179,87 @@ function createItem(item?: Partial<PackageConfigItem>): PackageConfigFormItem {
|
|||||||
|
|
||||||
const productCache = new Map<string, Product>();
|
const productCache = new Map<string, Product>();
|
||||||
|
|
||||||
|
function getProduct(item: PackageConfigFormItem): Product | undefined {
|
||||||
|
return productCache.get(item.productId || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQuantityStep(item: PackageConfigFormItem): number {
|
||||||
|
return getProductQuantityStep(getProduct(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
const estimatedOriginalAmount = computed<number | undefined>(() => {
|
||||||
|
let amount = 0;
|
||||||
|
for (const item of form.itemList) {
|
||||||
|
const itemAmount = calculateOriginalAmount(item);
|
||||||
|
if (itemAmount === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
amount += itemAmount;
|
||||||
|
}
|
||||||
|
return amount;
|
||||||
|
});
|
||||||
|
const estimatedOriginalAmountText = computed(() => estimatedOriginalAmount.value === undefined ? "待完善" : `¥${formatProductAmount(estimatedOriginalAmount.value)}`);
|
||||||
|
|
||||||
async function applyProduct(index: number, value?: { productId: string; productUnitId: string }): Promise<void> {
|
async function applyProduct(index: number, value?: { productId: string; productUnitId: string }): Promise<void> {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const product = productCache.get(value.productId) || await ProductAPI.detail(value.productId);
|
const product = productCache.get(value.productId) || await ProductAPI.detail(value.productId);
|
||||||
productCache.set(value.productId, product);
|
productCache.set(value.productId, product);
|
||||||
const unit = product.unitList?.find(item => item.id === value.productUnitId);
|
const saleableUnits = (product.unitList || []).filter(unit => unit.allowSale !== false && unit.id);
|
||||||
|
const productUnitId = saleableUnits.length < 2 ? saleableUnits[0]?.id || "" : value.productUnitId;
|
||||||
|
const unit = saleableUnits.find(item => item.id === productUnitId);
|
||||||
Object.assign(form.itemList[index], {
|
Object.assign(form.itemList[index], {
|
||||||
productId: value.productId,
|
productId: value.productId,
|
||||||
productUnitId: value.productUnitId,
|
productUnitId,
|
||||||
productNameSnapshot: product.name,
|
productNameSnapshot: product.name,
|
||||||
unitSnapshot: unit?.name || "",
|
unitSnapshot: unit?.name || "",
|
||||||
baseQuantityPerUnitSnapshot: unit?.baseQuantityPerUnit
|
baseQuantityPerUnitSnapshot: unit?.baseQuantityPerUnit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function addItem(): void {
|
function getSaleableUnits(item: PackageConfigFormItem): ProductUnit[] {
|
||||||
form.itemList.push(createItem());
|
return (productCache.get(item.productId || "")?.unitList || []).filter(unit => unit.allowSale !== false && unit.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function openStorePicker(): void {
|
function selectProductUnit(index: number, unit: ProductUnit): void {
|
||||||
storePickerDraft.value = [form.storeId];
|
const item = form.itemList[index];
|
||||||
storePickerVisible.value = true;
|
const product = productCache.get(item.productId || "");
|
||||||
|
if (!product?.id || !unit.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Object.assign(item, {
|
||||||
|
productUnitId: unit.id,
|
||||||
|
productNameSnapshot: product.name,
|
||||||
|
unitSnapshot: unit.name,
|
||||||
|
baseQuantityPerUnitSnapshot: unit.baseQuantityPerUnit
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateOriginalAmount(item: PackageConfigFormItem): number | undefined {
|
||||||
|
const quantity = normalizePackageQuantity(item.defaultQuantity ?? "");
|
||||||
|
const salePrice = productCache.get(item.productId || "")?.unitList?.find(unit => unit.id === item.productUnitId)?.salePrice;
|
||||||
|
if (!quantity || quantity === "0" || salePrice === undefined || salePrice === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const [integerPart, fractionPart = ""] = quantity.split(".");
|
||||||
|
const quantityScale = 10n ** BigInt(fractionPart.length);
|
||||||
|
const quantityValue = BigInt(`${integerPart}${fractionPart}`);
|
||||||
|
const amount = BigInt(salePrice) * quantityValue;
|
||||||
|
if (amount % quantityScale !== 0n) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const result = Number(amount / quantityScale);
|
||||||
|
return Number.isSafeInteger(result) ? result : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getItemOriginalAmountText(item: PackageConfigFormItem): string {
|
||||||
|
const amount = calculateOriginalAmount(item);
|
||||||
|
return amount === undefined ? "待完善" : `¥${formatProductAmount(amount)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addItem(): void {
|
||||||
|
form.itemList.push(createItem());
|
||||||
}
|
}
|
||||||
|
|
||||||
function openStatusPicker(): void {
|
function openStatusPicker(): void {
|
||||||
@@ -182,14 +275,6 @@ function removeItem(index: number): void {
|
|||||||
Message.warning("套餐至少需要一项产品权益");
|
Message.warning("套餐至少需要一项产品权益");
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmStore(value: Array<string | number>): void {
|
|
||||||
const storeId = String(value[0] || "");
|
|
||||||
if (storeId) {
|
|
||||||
form.storeId = storeId;
|
|
||||||
}
|
|
||||||
storePickerVisible.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function confirmStatus(value: Array<string | number>): void {
|
function confirmStatus(value: Array<string | number>): void {
|
||||||
const status = String(value[0] || "");
|
const status = String(value[0] || "");
|
||||||
if (status === "DRAFT" || status === "ACTIVE" || status === "INACTIVE") {
|
if (status === "DRAFT" || status === "ACTIVE" || status === "INACTIVE") {
|
||||||
@@ -227,8 +312,8 @@ function resolveTimestamp(value: string): number | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateForm(): boolean {
|
function validateForm(): boolean {
|
||||||
if (isGlobalManager.value && !form.storeId) {
|
if (!form.storeId) {
|
||||||
Message.warning("请选择所属门店");
|
Message.warning("当前门店未就绪");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!form.name.trim()) {
|
if (!form.name.trim()) {
|
||||||
@@ -251,6 +336,10 @@ function validateForm(): boolean {
|
|||||||
Message.warning("产品单位和正数量不能为空");
|
Message.warning("产品单位和正数量不能为空");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (getProduct(item) && !getProduct(item)!.allowDecimalQuantity && quantity.includes(".")) {
|
||||||
|
Message.warning("该产品不允许小数数量");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (keySet.has(key)) {
|
if (keySet.has(key)) {
|
||||||
Message.warning("产品名称和单位组合不能重复");
|
Message.warning("产品名称和单位组合不能重复");
|
||||||
return false;
|
return false;
|
||||||
@@ -311,23 +400,27 @@ async function loadData(): Promise<void> {
|
|||||||
} else {
|
} else {
|
||||||
await currentStoreStore.loadLoginStore();
|
await currentStoreStore.loadLoginStore();
|
||||||
}
|
}
|
||||||
if (!form.storeId) {
|
const currentStoreId = currentStoreStore.currentStoreId;
|
||||||
form.storeId = currentStoreStore.currentStoreId;
|
|
||||||
}
|
|
||||||
if (isCreateMode.value) {
|
if (isCreateMode.value) {
|
||||||
|
form.storeId = currentStoreId;
|
||||||
addItem();
|
addItem();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await PackageAPI.configDetail(routeId.value);
|
const result = await PackageAPI.configDetail(routeId.value);
|
||||||
|
const itemList = (result.itemList || []).map(createItem);
|
||||||
|
const productIdList = [...new Set(itemList.map(item => item.productId).filter((id): id is string => !!id))];
|
||||||
|
await Promise.all(productIdList.map(async productId => {
|
||||||
|
productCache.set(productId, await ProductAPI.detail(productId));
|
||||||
|
}));
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
id: result.id || routeId.value,
|
id: result.id || routeId.value,
|
||||||
storeId: result.storeId || form.storeId,
|
storeId: result.storeId || currentStoreId,
|
||||||
name: result.name || "",
|
name: result.name || "",
|
||||||
description: result.description || "",
|
description: result.description || "",
|
||||||
status: result.status || "DRAFT",
|
status: result.status || "DRAFT",
|
||||||
beginAt: result.beginAt || undefined,
|
beginAt: result.beginAt || undefined,
|
||||||
endAt: result.endAt || undefined,
|
endAt: result.endAt || undefined,
|
||||||
itemList: (result.itemList || []).map(createItem)
|
itemList
|
||||||
});
|
});
|
||||||
standardAmountText.value = formatPackageAmountInput(result.standardAmount);
|
standardAmountText.value = formatPackageAmountInput(result.standardAmount);
|
||||||
beginAtText.value = result.beginAt ? Time.toDateTime(result.beginAt) : "";
|
beginAtText.value = result.beginAt ? Time.toDateTime(result.beginAt) : "";
|
||||||
@@ -367,28 +460,89 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
}
|
}
|
||||||
|
|
||||||
.items {
|
.items {
|
||||||
padding: 1rem 0;
|
gap: .75rem;
|
||||||
|
display: flex;
|
||||||
|
margin-top: .75rem;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
gap: .5rem;
|
border: 1px dashed var(--td-border-level-1-color);
|
||||||
border: 1px dashed var(--td-component-border);
|
padding: .5rem;
|
||||||
padding: .5rem 1rem;
|
border-radius: .875rem;
|
||||||
margin-bottom: .75rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
gap: .625rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
align-items: center;
|
||||||
|
|
||||||
|
.no {
|
||||||
|
width: 1.75rem;
|
||||||
|
height: 1.75rem;
|
||||||
|
color: var(--td-brand-color);
|
||||||
|
flex: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--td-brand-color-1);
|
||||||
|
border-radius: .625rem;
|
||||||
|
font-size: .6875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--td-text-color-primary);
|
||||||
|
font-size: .875rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.t-form__item) {
|
.unit-list {
|
||||||
padding: .25rem 0;
|
gap: .5rem;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit-empty {
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
font-size: .75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: end;
|
||||||
|
|
||||||
|
.red {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.add {
|
||||||
|
--td-button-border-radius: 1rem;
|
||||||
|
|
||||||
|
gap: .375rem;
|
||||||
|
color: var(--td-brand-color);
|
||||||
|
margin: .75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
margin: 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.red {
|
||||||
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-item {
|
|
||||||
border-radius: .75rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -25,33 +25,53 @@
|
|||||||
>
|
>
|
||||||
<t-cell-group
|
<t-cell-group
|
||||||
v-if="configList.length"
|
v-if="configList.length"
|
||||||
class="list fix-last-cell-line"
|
class="list fix-last-cell-line empty-cell-title-margin"
|
||||||
theme="card"
|
theme="card"
|
||||||
>
|
>
|
||||||
<t-swipe-cell
|
<div
|
||||||
class="item"
|
class="item"
|
||||||
v-for="(item, index) in configList"
|
v-for="(item, index) in configList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
v-entrance-transition="index * 30"
|
v-entrance-transition="index * 30"
|
||||||
>
|
>
|
||||||
<t-cell :arrow="!!item.id" :hover="!!item.id" @click="toDetail(item)">
|
<t-cell class="cell">
|
||||||
<template #title>
|
<template #title>
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<strong v-text="item.name || '未命名套餐'" />
|
<strong v-text="item.name || '未命名套餐'" />
|
||||||
<t-tag :content="getPackageConfigStatusText(item.status)" variant="light-outline" :theme="getStatusTheme(item.status)" />
|
<t-tag :content="getPackageConfigStatusText(item.status)" variant="light-outline" :theme="getStatusTheme(item.status)" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #note>
|
|
||||||
<span class="price orange" v-text="formatPackageAmount(item.standardAmount)" />
|
|
||||||
</template>
|
|
||||||
<template #description>
|
<template #description>
|
||||||
<span v-text="`${item.itemList?.length || 0} 项权益`" />
|
<div class="description">
|
||||||
|
<div class="products">
|
||||||
|
<t-tag
|
||||||
|
v-for="productItem in item.itemList || []"
|
||||||
|
:key="productItem.id || `${productItem.productId}-${productItem.productUnitId}`"
|
||||||
|
:content="getProductRightText(productItem)"
|
||||||
|
theme="primary"
|
||||||
|
variant="light"
|
||||||
|
/>
|
||||||
|
<t-tag v-if="!item.itemList?.length" content="暂无产品权益" />
|
||||||
|
</div>
|
||||||
|
<div class="price-actions">
|
||||||
|
<div class="price">
|
||||||
|
<span class="red">¥</span>
|
||||||
|
<span class="value red" v-text="formatPackageAmount(item.standardAmount)" />
|
||||||
|
</div>
|
||||||
|
<div v-if="item.id" class="actions" @click.stop>
|
||||||
|
<t-button
|
||||||
|
class="detail"
|
||||||
|
theme="primary"
|
||||||
|
size="extra-small"
|
||||||
|
content="详情"
|
||||||
|
@click.stop="toDetail(item)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</t-cell>
|
</t-cell>
|
||||||
<template v-if="canDelete && item.id" #right>
|
</div>
|
||||||
<t-button class="swipe-cell-button" theme="danger" @click.stop="confirmRemove(item)">删除</t-button>
|
|
||||||
</template>
|
|
||||||
</t-swipe-cell>
|
|
||||||
</t-cell-group>
|
</t-cell-group>
|
||||||
<div v-else class="center-tips" v-text="loading ? '正在加载套餐配置' : '暂无套餐配置'" />
|
<div v-else class="center-tips" v-text="loading ? '正在加载套餐配置' : '暂无套餐配置'" />
|
||||||
<div class="center-tips" v-text="loadStateText" />
|
<div class="center-tips" v-text="loadStateText" />
|
||||||
@@ -61,16 +81,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { DialogPlugin, type DropdownValue } from "tdesign-mobile-vue";
|
import { type DropdownValue } from "tdesign-mobile-vue";
|
||||||
import { Toolkit } from "timi-web";
|
import { Toolkit } from "timi-web";
|
||||||
import { useUserStore } from "timi-web-api";
|
import { useUserStore } from "timi-web-api";
|
||||||
import PackageAPI from "@/api/PackageAPI";
|
import PackageAPI from "@/api/PackageAPI";
|
||||||
import type { GaoPackageConfig, PackageConfigStatus } from "@/types/Package";
|
import type { GaoPackageConfig, PackageConfigItem, PackageConfigStatus } from "@/types/Package";
|
||||||
import type { NavBarAction } from "@/types/NavBarAction";
|
import type { NavBarAction } from "@/types/NavBarAction";
|
||||||
import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
|
import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
|
||||||
import { useNavBarStore } from "@/store/navBarStore";
|
import { useNavBarStore } from "@/store/navBarStore";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { formatPackageAmount, getPackageConfigStatusText } from "@/utils/package";
|
import { formatPackageAmount, getPackageConfigStatusText, getPackageQuantityText } from "@/utils/package";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
|
|
||||||
@@ -80,7 +100,6 @@ const router = useRouter();
|
|||||||
const navBarStore = useNavBarStore();
|
const navBarStore = useNavBarStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const canCreate = computed(() => userStore.hasPermissions(permissionCodes.packageConfig.create));
|
const canCreate = computed(() => userStore.hasPermissions(permissionCodes.packageConfig.create));
|
||||||
const canDelete = computed(() => userStore.hasPermissions(permissionCodes.packageConfig.delete));
|
|
||||||
const actionList = computed<NavBarAction[]>(() => canCreate.value ? [{
|
const actionList = computed<NavBarAction[]>(() => canCreate.value ? [{
|
||||||
key: "create",
|
key: "create",
|
||||||
text: "新增套餐配置",
|
text: "新增套餐配置",
|
||||||
@@ -172,27 +191,9 @@ function getStatusTheme(statusValue?: PackageConfigStatus): "success" | "warning
|
|||||||
return statusValue === "ACTIVE" ? "success" : statusValue === "INACTIVE" ? "warning" : "default";
|
return statusValue === "ACTIVE" ? "success" : statusValue === "INACTIVE" ? "warning" : "default";
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmRemove(item: GaoPackageConfig): void {
|
function getProductRightText(item: PackageConfigItem): string {
|
||||||
if (!item.id || !canDelete.value) {
|
const quantity = getPackageQuantityText(item.defaultQuantity);
|
||||||
return;
|
return `${item.productNameSnapshot || "未命名产品"} × ${quantity}${item.unitSnapshot ? ` ${item.unitSnapshot}` : ""}`;
|
||||||
}
|
|
||||||
const dialog = DialogPlugin.confirm({
|
|
||||||
title: "删除套餐配置",
|
|
||||||
content: `确定删除“${item.name || "未命名套餐"}”?删除后不可再用于购买。`,
|
|
||||||
confirmBtn: { content: "删除", theme: "danger" },
|
|
||||||
onConfirm: async () => {
|
|
||||||
try {
|
|
||||||
await PackageAPI.configRemove(item.id!);
|
|
||||||
configList.value = configList.value.filter(config => config.id !== item.id);
|
|
||||||
total.value = Math.max(0, total.value - 1);
|
|
||||||
Message.success("套餐配置已删除");
|
|
||||||
} catch (error) {
|
|
||||||
Message.error(getErrorMessage(error, "套餐配置删除失败"));
|
|
||||||
} finally {
|
|
||||||
dialog.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@@ -228,17 +229,22 @@ onUnmounted(() => {
|
|||||||
box-shadow: var(--td-shadow-1);
|
box-shadow: var(--td-shadow-1);
|
||||||
|
|
||||||
.keyword {
|
.keyword {
|
||||||
|
--td-input-vertical-padding: .25rem 1rem;
|
||||||
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
border: 1px solid var(--td-component-border);
|
border: 1px solid var(--td-component-border);
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
padding: .5rem 1rem;
|
|
||||||
background: var(--td-bg-color-container);
|
background: var(--td-bg-color-container);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
|
--td-spacer: 4px;
|
||||||
|
--td-spacer-1: 6px;
|
||||||
|
|
||||||
|
border: 1px solid rgba(148, 163, 184, .18);
|
||||||
margin-left: -2rem;
|
margin-left: -2rem;
|
||||||
padding-left: 1.5rem;
|
padding-left: 2rem;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
@@ -251,15 +257,60 @@ onUnmounted(() => {
|
|||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
|
|
||||||
.item {
|
.item {
|
||||||
|
background: var(--td-bg-color-container);
|
||||||
|
border-radius: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
.title {
|
.cell {
|
||||||
gap: .5rem;
|
padding: 10px;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price {
|
.title {
|
||||||
font-size: 1.25rem;
|
gap: .5rem;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.products {
|
||||||
|
gap: .35rem;
|
||||||
|
display: flex;
|
||||||
|
max-width: 100%;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
scrollbar-width: none;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
|
||||||
|
:deep(.t-tag) {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-actions {
|
||||||
|
gap: .5rem;
|
||||||
|
display: flex;
|
||||||
|
margin-top: .5rem;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: end;
|
||||||
|
|
||||||
|
.price {
|
||||||
|
|
||||||
|
.value {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,9 +90,9 @@
|
|||||||
v-model="item.quantity"
|
v-model="item.quantity"
|
||||||
size="large"
|
size="large"
|
||||||
:max="maxQuantity"
|
:max="maxQuantity"
|
||||||
:min="1"
|
:min="getQuantityStep(item)"
|
||||||
:step="1"
|
:step="getQuantityStep(item)"
|
||||||
:integer="false"
|
:integer="!getProduct(item)?.allowDecimalQuantity"
|
||||||
theme="outline"
|
theme="outline"
|
||||||
/>
|
/>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
@@ -184,7 +184,7 @@ import type { Product, ProductUnit } from "@/types/Product";
|
|||||||
import type { PhotoItem } from "@/types/Photo";
|
import type { PhotoItem } from "@/types/Photo";
|
||||||
import type { DirectProductOrderItemRequest } from "@/types/SalesOrder";
|
import type { DirectProductOrderItemRequest } from "@/types/SalesOrder";
|
||||||
import { createPackageRequestNo, normalizePackageQuantity, parsePackageAmount } from "@/utils/package";
|
import { createPackageRequestNo, normalizePackageQuantity, parsePackageAmount } from "@/utils/package";
|
||||||
import { formatProductAmount } from "@/utils/product";
|
import { formatProductAmount, getProductQuantityStep } from "@/utils/product";
|
||||||
import { getErrorMessage } from "@/utils/error";
|
import { getErrorMessage } from "@/utils/error";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { permissionCodes } from "@/utils/permission";
|
import { permissionCodes } from "@/utils/permission";
|
||||||
@@ -219,6 +219,15 @@ const scanCode = ref("");
|
|||||||
const scannerVisible = ref(false);
|
const scannerVisible = ref(false);
|
||||||
const scanStatus = ref("点击扫码开始识别。");
|
const scanStatus = ref("点击扫码开始识别。");
|
||||||
let latestDecodedText = "";
|
let latestDecodedText = "";
|
||||||
|
|
||||||
|
function getProduct(item: FormItem): Product | undefined {
|
||||||
|
return productCache.get(item.productId || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQuantityStep(item: FormItem): number {
|
||||||
|
return getProductQuantityStep(getProduct(item));
|
||||||
|
}
|
||||||
|
|
||||||
const totalAmount = computed(() => {
|
const totalAmount = computed(() => {
|
||||||
let amount = 0;
|
let amount = 0;
|
||||||
for (const item of items.value) {
|
for (const item of items.value) {
|
||||||
@@ -380,6 +389,7 @@ function validate(): boolean {
|
|||||||
const quantity = normalizePackageQuantity(item.quantity ?? "");
|
const quantity = normalizePackageQuantity(item.quantity ?? "");
|
||||||
const key = `${item.productId}\u0000${item.productUnitId}`;
|
const key = `${item.productId}\u0000${item.productUnitId}`;
|
||||||
if (!item.productId || !item.productUnitId || !quantity || quantity === "0") { Message.warning("产品单位和正数量不能为空"); return false; }
|
if (!item.productId || !item.productUnitId || !quantity || quantity === "0") { Message.warning("产品单位和正数量不能为空"); return false; }
|
||||||
|
if (getProduct(item) && !getProduct(item)!.allowDecimalQuantity && quantity.includes(".")) { Message.warning("该产品不允许小数数量"); return false; }
|
||||||
if (keys.has(key)) { Message.warning("产品单位不能重复"); return false; }
|
if (keys.has(key)) { Message.warning("产品单位不能重复"); return false; }
|
||||||
keys.add(key);
|
keys.add(key);
|
||||||
if (canPriceOverride.value && item.unitPriceText && parsePackageAmount(item.unitPriceText) === undefined) { Message.warning("成交单价必须是合法金额"); return false; }
|
if (canPriceOverride.value && item.unitPriceText && parsePackageAmount(item.unitPriceText) === undefined) { Message.warning("成交单价必须是合法金额"); return false; }
|
||||||
@@ -499,7 +509,6 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
gap: .625rem;
|
gap: .625rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: .375rem;
|
|
||||||
|
|
||||||
.no {
|
.no {
|
||||||
width: 1.75rem;
|
width: 1.75rem;
|
||||||
@@ -560,7 +569,6 @@ onUnmounted(() => navBarStore.clearRight());
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Attachment, Model, PageResult, User } from "timi-web-api";
|
import type { Attachment, Model, PageResult, User } from "timi-web-api";
|
||||||
import type { Customer } from "@/types/Customer";
|
import type { Customer } from "@/types/Customer";
|
||||||
import type { OrderFulfillment } from "@/types/Fulfillment";
|
import type { OrderFulfillment } from "@/types/Fulfillment";
|
||||||
|
import type { Product } from "@/types/Product";
|
||||||
|
|
||||||
export type PackageConfigStatus = "DRAFT" | "ACTIVE" | "INACTIVE";
|
export type PackageConfigStatus = "DRAFT" | "ACTIVE" | "INACTIVE";
|
||||||
export type CustomerPackageStatus = "ACTIVE" | "FULFILLED" | "CANCELLED";
|
export type CustomerPackageStatus = "ACTIVE" | "FULFILLED" | "CANCELLED";
|
||||||
@@ -15,6 +16,7 @@ export type PackageConfigItem = {
|
|||||||
defaultQuantity: number | string;
|
defaultQuantity: number | string;
|
||||||
sort?: number;
|
sort?: number;
|
||||||
remark?: string | null;
|
remark?: string | null;
|
||||||
|
product?: Product;
|
||||||
} & Model;
|
} & Model;
|
||||||
|
|
||||||
export type GaoPackageConfig = {
|
export type GaoPackageConfig = {
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ export function normalizeProductQuantity(value: number | string): string | undef
|
|||||||
return fraction ? `${integer}.${fraction}` : integer;
|
return fraction ? `${integer}.${fraction}` : integer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 根据产品数量规则返回数量控件步长。允许小数时固定步进 0.1。 */
|
||||||
|
export function getProductQuantityStep(product?: Pick<Product, "allowDecimalQuantity">): number {
|
||||||
|
return product?.allowDecimalQuantity ? .1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
export function getProductTypeText(type?: ProductType): string {
|
export function getProductTypeText(type?: ProductType): string {
|
||||||
return ({ SERVICE: "服务", GOODS: "实物", CONSUMABLE: "消耗品" } as Record<string, string>)[type || ""] || "未知";
|
return ({ SERVICE: "服务", GOODS: "实物", CONSUMABLE: "消耗品" } as Record<string, string>)[type || ""] || "未知";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user