304 lines
8.3 KiB
Vue
304 lines
8.3 KiB
Vue
<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>
|