fix PackageConfig*.vue style
This commit is contained in:
@@ -2,10 +2,23 @@
|
||||
<div v-can="permissionCodes.packageConfig.read" class="package-config-detail">
|
||||
<template v-if="config">
|
||||
<section class="hero" v-entrance-transition>
|
||||
<span class="eyebrow" v-text="'套餐管理'" />
|
||||
<div class="title"><h1 v-text="config.name || '未命名套餐'" /><t-tag :content="getPackageConfigStatusText(config.status)" variant="light-outline" :theme="getStatusTheme(config.status)" /></div>
|
||||
<p v-text="config.description || '暂无套餐说明'" />
|
||||
<div class="hero-meta"><span v-text="`${config.itemList?.length || 0} 项产品权益`" /><strong v-text="formatPackageAmount(config.standardAmount)" /></div>
|
||||
<div class="header">
|
||||
<span class="eyebrow" v-text="'套餐管理'" />
|
||||
<t-tag
|
||||
: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>
|
||||
<t-cell-group title="基本信息" theme="card" v-entrance-transition="70">
|
||||
<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.updater?.name || config.updater?.nick || '未知'" />
|
||||
</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
|
||||
class="item"
|
||||
v-for="item in config.itemList || []"
|
||||
:title="item.productNameSnapshot"
|
||||
:key="item.id || `${item.productId}-${item.productUnitId}`"
|
||||
:title="item.productNameSnapshot || '未命名产品'"
|
||||
: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="'暂无产品权益'" />
|
||||
</t-cell-group>
|
||||
<t-button
|
||||
@@ -48,11 +84,12 @@ import { useUserStore } from "timi-web-api";
|
||||
import PackageAPI from "@/api/PackageAPI";
|
||||
import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||
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 { formatPackageAmount, getPackageConfigStatusText } from "@/utils/package";
|
||||
import { Message } from "@/utils/message";
|
||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||
import { getProductPhotoURL as getProductPhotoUrl, getProductTypeText } from "@/utils/product";
|
||||
|
||||
defineOptions({ name: "PackageConfigDetail" });
|
||||
|
||||
@@ -75,6 +112,10 @@ function getStatusTheme(status?: PackageConfigStatus): "success" | "warning" | "
|
||||
return status === "ACTIVE" ? "success" : status === "INACTIVE" ? "warning" : "default";
|
||||
}
|
||||
|
||||
function getItemPhotoURL(item: PackageConfigItem): string {
|
||||
return getProductPhotoUrl(item.product || {});
|
||||
}
|
||||
|
||||
async function loadData(): Promise<void> {
|
||||
const id = String(route.params.id || "");
|
||||
if (!id) {
|
||||
@@ -157,55 +198,123 @@ onUnmounted(() => navBarStore.clearRight());
|
||||
.hero {
|
||||
margin: 0 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);
|
||||
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 {
|
||||
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;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
p {
|
||||
.description {
|
||||
color: var(--td-text-color-secondary);
|
||||
margin: .5rem 0 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.hero-meta {
|
||||
.items-price {
|
||||
gap: .75rem;
|
||||
display: flex;
|
||||
margin-top: .75rem;
|
||||
color: var(--td-text-color-secondary);
|
||||
align-items: center;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #c2410c;
|
||||
font-size: 1.2rem;
|
||||
white-space: nowrap;
|
||||
.price {
|
||||
|
||||
.value {
|
||||
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 {
|
||||
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 {
|
||||
color: #99f6e4;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div v-can="savePermissionCode" class="package-config-edit">
|
||||
<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-input v-model="form.name" borderless maxlength="100" placeholder="请输入套餐名称" />
|
||||
</t-form-item>
|
||||
@@ -36,33 +33,75 @@
|
||||
label-width="6rem"
|
||||
>
|
||||
<div class="header">
|
||||
<span class="no" v-text="String(index + 1).padStart(2, '0')" />
|
||||
<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>
|
||||
<product-selector
|
||||
:model-value="item.productId && item.productUnitId ? {productId: item.productId, productUnitId: item.productUnitId} : undefined"
|
||||
label="产品单位"
|
||||
:model-value="item.productId ? {productId: item.productId, productUnitId: item.productUnitId || ''} : undefined"
|
||||
:selected-product="getProduct(item)"
|
||||
label="产品"
|
||||
placeholder="请选择产品"
|
||||
required-mark
|
||||
@update:model-value="applyProduct(index, $event)"
|
||||
/>
|
||||
<t-form-item label="默认配额" required-mark>
|
||||
<t-input v-model.trim="item.defaultQuantity" type="number" borderless />
|
||||
<t-form-item v-if="item.productId" label="产品单位" required-mark>
|
||||
<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 label="备注">
|
||||
<t-input v-model="item.remark" borderless />
|
||||
<t-input v-model.trim="item.remark" borderless placeholder="选填" />
|
||||
</t-form-item>
|
||||
<div class="price">
|
||||
<span>预估价(原价)</span>
|
||||
<strong class="red" v-text="getItemOriginalAmountText(item)" />
|
||||
</div>
|
||||
</t-form>
|
||||
<t-button
|
||||
class="cell-button"
|
||||
theme="primary"
|
||||
class="add"
|
||||
type="button"
|
||||
theme="light"
|
||||
variant="dashed"
|
||||
@click="addItem"
|
||||
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>
|
||||
</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-picker v-model="statusPickerDraft" :columns="statusOptions" title="状态" @cancel="statusPickerVisible = false" @confirm="confirmStatus" />
|
||||
</t-popup>
|
||||
@@ -83,13 +122,13 @@ import { useCurrentStoreStore } from "@/store/currentStoreStore";
|
||||
import { useNavBarStore } from "@/store/navBarStore";
|
||||
import type { GaoPackageConfig, PackageConfigItem, PackageConfigStatus } from "@/types/Package";
|
||||
import ProductAPI from "@/api/ProductAPI";
|
||||
import type { Product } from "@/types/Product";
|
||||
import type { Product, ProductUnit } from "@/types/Product";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import { formatPackageAmountInput, getPackageConfigStatusText, normalizePackageQuantity, parsePackageAmount } from "@/utils/package";
|
||||
import { Message } from "@/utils/message";
|
||||
import { gaoRoleCodes, permissionCodes } from "@/utils/permission";
|
||||
import { formatProductAmount, getProductQuantityStep } from "@/utils/product";
|
||||
|
||||
type PickerValue = string | number;
|
||||
type PackageConfigFormItem = Partial<PackageConfigItem> & { clientKey: string };
|
||||
|
||||
defineOptions({ name: "PackageConfigEdit" });
|
||||
@@ -117,10 +156,8 @@ const form = reactive({
|
||||
const standardAmountText = ref("");
|
||||
const beginAtText = ref("");
|
||||
const endAtText = ref("");
|
||||
const storePickerVisible = ref(false);
|
||||
const storePickerDraft = ref<PickerValue[]>([]);
|
||||
const statusPickerVisible = ref(false);
|
||||
const statusPickerDraft = ref<PickerValue[]>([]);
|
||||
const statusPickerDraft = ref<Array<string | number>>([]);
|
||||
const beginPickerVisible = ref(false);
|
||||
const endPickerVisible = ref(false);
|
||||
const beginDraft = ref("");
|
||||
@@ -130,8 +167,6 @@ const statusOptions = [
|
||||
{ label: "启用", value: "ACTIVE" },
|
||||
{ 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 {
|
||||
return {
|
||||
@@ -144,29 +179,87 @@ function createItem(item?: Partial<PackageConfigItem>): PackageConfigFormItem {
|
||||
|
||||
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> {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
const product = productCache.get(value.productId) || await ProductAPI.detail(value.productId);
|
||||
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], {
|
||||
productId: value.productId,
|
||||
productUnitId: value.productUnitId,
|
||||
productUnitId,
|
||||
productNameSnapshot: product.name,
|
||||
unitSnapshot: unit?.name || "",
|
||||
baseQuantityPerUnitSnapshot: unit?.baseQuantityPerUnit
|
||||
});
|
||||
}
|
||||
|
||||
function addItem(): void {
|
||||
form.itemList.push(createItem());
|
||||
function getSaleableUnits(item: PackageConfigFormItem): ProductUnit[] {
|
||||
return (productCache.get(item.productId || "")?.unitList || []).filter(unit => unit.allowSale !== false && unit.id);
|
||||
}
|
||||
|
||||
function openStorePicker(): void {
|
||||
storePickerDraft.value = [form.storeId];
|
||||
storePickerVisible.value = true;
|
||||
function selectProductUnit(index: number, unit: ProductUnit): void {
|
||||
const item = form.itemList[index];
|
||||
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 {
|
||||
@@ -182,14 +275,6 @@ function removeItem(index: number): void {
|
||||
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 {
|
||||
const status = String(value[0] || "");
|
||||
if (status === "DRAFT" || status === "ACTIVE" || status === "INACTIVE") {
|
||||
@@ -227,8 +312,8 @@ function resolveTimestamp(value: string): number | undefined {
|
||||
}
|
||||
|
||||
function validateForm(): boolean {
|
||||
if (isGlobalManager.value && !form.storeId) {
|
||||
Message.warning("请选择所属门店");
|
||||
if (!form.storeId) {
|
||||
Message.warning("当前门店未就绪");
|
||||
return false;
|
||||
}
|
||||
if (!form.name.trim()) {
|
||||
@@ -251,6 +336,10 @@ function validateForm(): boolean {
|
||||
Message.warning("产品单位和正数量不能为空");
|
||||
return false;
|
||||
}
|
||||
if (getProduct(item) && !getProduct(item)!.allowDecimalQuantity && quantity.includes(".")) {
|
||||
Message.warning("该产品不允许小数数量");
|
||||
return false;
|
||||
}
|
||||
if (keySet.has(key)) {
|
||||
Message.warning("产品名称和单位组合不能重复");
|
||||
return false;
|
||||
@@ -311,23 +400,27 @@ async function loadData(): Promise<void> {
|
||||
} else {
|
||||
await currentStoreStore.loadLoginStore();
|
||||
}
|
||||
if (!form.storeId) {
|
||||
form.storeId = currentStoreStore.currentStoreId;
|
||||
}
|
||||
const currentStoreId = currentStoreStore.currentStoreId;
|
||||
if (isCreateMode.value) {
|
||||
form.storeId = currentStoreId;
|
||||
addItem();
|
||||
return;
|
||||
}
|
||||
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, {
|
||||
id: result.id || routeId.value,
|
||||
storeId: result.storeId || form.storeId,
|
||||
storeId: result.storeId || currentStoreId,
|
||||
name: result.name || "",
|
||||
description: result.description || "",
|
||||
status: result.status || "DRAFT",
|
||||
beginAt: result.beginAt || undefined,
|
||||
endAt: result.endAt || undefined,
|
||||
itemList: (result.itemList || []).map(createItem)
|
||||
itemList
|
||||
});
|
||||
standardAmountText.value = formatPackageAmountInput(result.standardAmount);
|
||||
beginAtText.value = result.beginAt ? Time.toDateTime(result.beginAt) : "";
|
||||
@@ -367,28 +460,89 @@ onUnmounted(() => navBarStore.clearRight());
|
||||
}
|
||||
|
||||
.items {
|
||||
padding: 1rem 0;
|
||||
gap: .75rem;
|
||||
display: flex;
|
||||
margin-top: .75rem;
|
||||
flex-direction: column;
|
||||
|
||||
.item {
|
||||
gap: .5rem;
|
||||
border: 1px dashed var(--td-component-border);
|
||||
padding: .5rem 1rem;
|
||||
margin-bottom: .75rem;
|
||||
border-radius: 1rem;
|
||||
border: 1px dashed var(--td-border-level-1-color);
|
||||
padding: .5rem;
|
||||
border-radius: .875rem;
|
||||
|
||||
.header {
|
||||
gap: .625rem;
|
||||
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) {
|
||||
padding: .25rem 0;
|
||||
.unit-list {
|
||||
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>
|
||||
|
||||
@@ -25,33 +25,53 @@
|
||||
>
|
||||
<t-cell-group
|
||||
v-if="configList.length"
|
||||
class="list fix-last-cell-line"
|
||||
class="list fix-last-cell-line empty-cell-title-margin"
|
||||
theme="card"
|
||||
>
|
||||
<t-swipe-cell
|
||||
<div
|
||||
class="item"
|
||||
v-for="(item, index) in configList"
|
||||
:key="item.id"
|
||||
v-entrance-transition="index * 30"
|
||||
>
|
||||
<t-cell :arrow="!!item.id" :hover="!!item.id" @click="toDetail(item)">
|
||||
<t-cell class="cell">
|
||||
<template #title>
|
||||
<div class="title">
|
||||
<strong v-text="item.name || '未命名套餐'" />
|
||||
<t-tag :content="getPackageConfigStatusText(item.status)" variant="light-outline" :theme="getStatusTheme(item.status)" />
|
||||
</div>
|
||||
</template>
|
||||
<template #note>
|
||||
<span class="price orange" v-text="formatPackageAmount(item.standardAmount)" />
|
||||
</template>
|
||||
<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>
|
||||
</t-cell>
|
||||
<template v-if="canDelete && item.id" #right>
|
||||
<t-button class="swipe-cell-button" theme="danger" @click.stop="confirmRemove(item)">删除</t-button>
|
||||
</template>
|
||||
</t-swipe-cell>
|
||||
</div>
|
||||
</t-cell-group>
|
||||
<div v-else class="center-tips" v-text="loading ? '正在加载套餐配置' : '暂无套餐配置'" />
|
||||
<div class="center-tips" v-text="loadStateText" />
|
||||
@@ -61,16 +81,16 @@
|
||||
</template>
|
||||
|
||||
<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 { useUserStore } from "timi-web-api";
|
||||
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 NavBarActionMenu from "@/components/NavBarActionMenu.vue";
|
||||
import { useNavBarStore } from "@/store/navBarStore";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import { formatPackageAmount, getPackageConfigStatusText } from "@/utils/package";
|
||||
import { formatPackageAmount, getPackageConfigStatusText, getPackageQuantityText } from "@/utils/package";
|
||||
import { Message } from "@/utils/message";
|
||||
import { permissionCodes } from "@/utils/permission";
|
||||
|
||||
@@ -80,7 +100,6 @@ const router = useRouter();
|
||||
const navBarStore = useNavBarStore();
|
||||
const userStore = useUserStore();
|
||||
const canCreate = computed(() => userStore.hasPermissions(permissionCodes.packageConfig.create));
|
||||
const canDelete = computed(() => userStore.hasPermissions(permissionCodes.packageConfig.delete));
|
||||
const actionList = computed<NavBarAction[]>(() => canCreate.value ? [{
|
||||
key: "create",
|
||||
text: "新增套餐配置",
|
||||
@@ -172,27 +191,9 @@ function getStatusTheme(statusValue?: PackageConfigStatus): "success" | "warning
|
||||
return statusValue === "ACTIVE" ? "success" : statusValue === "INACTIVE" ? "warning" : "default";
|
||||
}
|
||||
|
||||
function confirmRemove(item: GaoPackageConfig): void {
|
||||
if (!item.id || !canDelete.value) {
|
||||
return;
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
function getProductRightText(item: PackageConfigItem): string {
|
||||
const quantity = getPackageQuantityText(item.defaultQuantity);
|
||||
return `${item.productNameSnapshot || "未命名产品"} × ${quantity}${item.unitSnapshot ? ` ${item.unitSnapshot}` : ""}`;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
@@ -228,17 +229,22 @@ onUnmounted(() => {
|
||||
box-shadow: var(--td-shadow-1);
|
||||
|
||||
.keyword {
|
||||
--td-input-vertical-padding: .25rem 1rem;
|
||||
|
||||
flex: 1;
|
||||
border: 1px solid var(--td-component-border);
|
||||
z-index: 1;
|
||||
padding: .5rem 1rem;
|
||||
background: var(--td-bg-color-container);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
--td-spacer: 4px;
|
||||
--td-spacer-1: 6px;
|
||||
|
||||
border: 1px solid rgba(148, 163, 184, .18);
|
||||
margin-left: -2rem;
|
||||
padding-left: 1.5rem;
|
||||
padding-left: 2rem;
|
||||
border-radius: 1rem;
|
||||
|
||||
&:after {
|
||||
@@ -251,15 +257,60 @@ onUnmounted(() => {
|
||||
padding-top: 1rem;
|
||||
|
||||
.item {
|
||||
background: var(--td-bg-color-container);
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
gap: .5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.cell {
|
||||
padding: 10px;
|
||||
|
||||
.price {
|
||||
font-size: 1.25rem;
|
||||
.title {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user