Files
TakeoutSaaS.TenantUI/apps/web-antd/src/views/product/detail/index.vue

304 lines
8.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
/**
* 文件职责:商品详情编辑页。
* 1. 根据路由参数加载商品详情与分类。
* 2. 提供商品基础信息编辑与保存能力。
*/
import type { ProductDetailDto } from '#/api/product';
import { computed, reactive, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { Page } from '@vben/common-ui';
import {
Button,
Card,
Empty,
Form,
Input,
InputNumber,
message,
Select,
Space,
Spin,
Tag,
} from 'ant-design-vue';
import {
getProductCategoryListApi,
getProductDetailApi,
saveProductApi,
} from '#/api/product';
import {
formatCurrency,
tagsToText,
textToTags,
} from '../list/composables/product-list-page/helpers';
const route = useRoute();
const router = useRouter();
const isLoading = ref(false);
const isSubmitting = ref(false);
const detail = ref<null | ProductDetailDto>(null);
const categoryOptions = ref<Array<{ label: string; value: string }>>([]);
const form = reactive({
id: '',
name: '',
subtitle: '',
categoryId: '',
kind: 'single' as 'combo' | 'single',
description: '',
price: 0,
originalPrice: null as null | number,
stock: 0,
status: 'off_shelf' as 'off_shelf' | 'on_sale',
tagsText: '',
});
const storeId = computed(() => String(route.query.storeId || ''));
const productId = computed(() => String(route.query.productId || ''));
const statusMeta = computed(() => {
if (form.status === 'on_sale') {
return { color: 'green', label: '在售' };
}
return { color: 'default', label: '下架' };
});
const originalPriceInput = computed<number | undefined>({
get: () => form.originalPrice ?? undefined,
set: (value) => {
form.originalPrice = value ?? null;
},
});
/** 将详情映射到编辑表单。 */
function patchForm(data: ProductDetailDto) {
form.id = data.id;
form.name = data.name;
form.subtitle = data.subtitle;
form.categoryId = data.categoryId;
form.kind = data.kind;
form.description = data.description;
form.price = data.price;
form.originalPrice = data.originalPrice;
form.stock = data.stock;
form.status = data.status === 'on_sale' ? 'on_sale' : 'off_shelf';
form.tagsText = tagsToText(data.tags);
}
/** 加载商品详情与分类。 */
async function loadDetail() {
if (!storeId.value || !productId.value) {
detail.value = null;
categoryOptions.value = [];
return;
}
isLoading.value = true;
try {
const [detailData, categories] = await Promise.all([
getProductDetailApi({
storeId: storeId.value,
productId: productId.value,
}),
getProductCategoryListApi(storeId.value),
]);
detail.value = detailData;
categoryOptions.value = categories.map((item) => ({
label: item.name,
value: item.id,
}));
patchForm(detailData);
} catch (error) {
console.error(error);
detail.value = null;
categoryOptions.value = [];
} finally {
isLoading.value = false;
}
}
/** 保存商品详情。 */
async function saveDetail() {
if (!storeId.value || !form.id) return;
if (!form.name.trim()) {
message.warning('请输入商品名称');
return;
}
if (!form.categoryId) {
message.warning('请选择商品分类');
return;
}
isSubmitting.value = true;
try {
const saved = await saveProductApi({
storeId: storeId.value,
id: form.id,
name: form.name.trim(),
subtitle: form.subtitle.trim(),
categoryId: form.categoryId,
kind: form.kind,
description: form.description.trim(),
price: Number(form.price || 0),
originalPrice:
form.originalPrice && Number(form.originalPrice) > 0
? Number(form.originalPrice)
: null,
stock: Math.max(0, Math.floor(Number(form.stock || 0))),
status: form.status,
shelfMode: form.status === 'on_sale' ? 'now' : 'draft',
tags: textToTags(form.tagsText),
});
detail.value = saved;
patchForm(saved);
message.success('商品详情已保存');
} catch (error) {
console.error(error);
} finally {
isSubmitting.value = false;
}
}
/** 返回商品列表页。 */
function goBack() {
router.push('/product/list');
}
watch([storeId, productId], loadDetail, { immediate: true });
</script>
<template>
<Page title="商品详情" content-class="space-y-4 page-product-detail">
<Card :bordered="false">
<Space>
<Button @click="goBack">返回商品列表</Button>
<Button type="primary" :loading="isSubmitting" @click="saveDetail">
保存商品
</Button>
</Space>
</Card>
<Spin :spinning="isLoading">
<Card v-if="detail" :bordered="false">
<div class="product-detail-header">
<div class="product-detail-cover">
{{ detail.name.slice(0, 1) }}
</div>
<div class="product-detail-title-wrap">
<div class="title">{{ detail.name }}</div>
<div class="sub">{{ detail.subtitle || '--' }}</div>
<div class="spu">{{ detail.spuCode }}</div>
</div>
<Tag class="product-detail-status" :color="statusMeta.color">
{{ statusMeta.label }}
</Tag>
</div>
<Form layout="vertical">
<Space style="display: flex; width: 100%">
<Form.Item label="商品名称" required style="flex: 1">
<Input v-model:value="form.name" :maxlength="30" show-count />
</Form.Item>
<Form.Item label="副标题" style="flex: 1">
<Input v-model:value="form.subtitle" :maxlength="60" show-count />
</Form.Item>
</Space>
<Space style="display: flex; width: 100%">
<Form.Item label="分类" style="flex: 1">
<Select
v-model:value="form.categoryId"
:options="categoryOptions"
placeholder="请选择分类"
/>
</Form.Item>
<Form.Item label="商品类型" style="flex: 1">
<Select
v-model:value="form.kind"
:options="[
{ label: '单品', value: 'single' },
{ label: '套餐', value: 'combo' },
]"
/>
</Form.Item>
<Form.Item label="状态" style="flex: 1">
<Select
v-model:value="form.status"
:options="[
{ label: '在售', value: 'on_sale' },
{ label: '下架', value: 'off_shelf' },
]"
/>
</Form.Item>
</Space>
<Space style="display: flex; width: 100%">
<Form.Item label="售价" style="flex: 1">
<InputNumber
v-model:value="form.price"
:min="0"
:step="0.5"
style="width: 100%"
/>
</Form.Item>
<Form.Item label="原价" style="flex: 1">
<InputNumber
v-model:value="originalPriceInput"
:min="0"
:step="0.5"
style="width: 100%"
/>
</Form.Item>
<Form.Item label="库存" style="flex: 1">
<InputNumber
v-model:value="form.stock"
:min="0"
:step="1"
style="width: 100%"
/>
</Form.Item>
</Space>
<Form.Item label="标签(逗号分隔)">
<Input
v-model:value="form.tagsText"
placeholder="例如:招牌,新品,辣"
/>
</Form.Item>
<Form.Item label="商品描述">
<Input.TextArea
v-model:value="form.description"
:rows="4"
:maxlength="500"
show-count
/>
</Form.Item>
</Form>
<div class="detail-extra">
<span>商品ID{{ detail.id }}</span>
<span>SPU{{ detail.spuCode }}</span>
<span>月销{{ detail.salesMonthly }}</span>
<span>当前售价{{ formatCurrency(detail.price) }}</span>
</div>
</Card>
<Card v-else :bordered="false">
<Empty description="未找到商品详情" />
</Card>
</Spin>
</Page>
</template>
<style lang="less">
@import './styles/index.less';
</style>