docs: 新增优惠券和礼品卡相关文档

- 新增 Vue 模板标签错误修复总结文档
- 新增 优惠券列表页面优化说明文档
- 新增 优惠券和礼品卡弹窗优化说明文档
- 新增 商品关联功能修复说明文档
This commit is contained in:
2025-08-14 19:39:56 +08:00
parent 23b0bee982
commit 24e1958bcd
13 changed files with 3736 additions and 579 deletions

View File

@@ -1,11 +1,11 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:width="1000"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑礼品卡' : '添加礼品卡'"
:title="isUpdate ? '编辑礼品卡' : '新增礼品卡'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
@@ -14,97 +14,216 @@
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<a-form-item label="名称" name="name">
<a-input
allow-clear
placeholder="请输入"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="秘钥" name="code">
<a-input
allow-clear
placeholder="请输入秘钥"
v-model:value="form.code"
/>
</a-form-item>
<a-form-item label="商品ID" name="goodsId">
<a-input
allow-clear
placeholder="请输入商品ID"
v-model:value="form.goodsId"
/>
</a-form-item>
<a-form-item label="领取时间" name="takeTime">
<a-input
allow-clear
placeholder="请输入领取时间"
v-model:value="form.takeTime"
/>
</a-form-item>
<a-form-item label="操作人" name="operatorUserId">
<a-input
allow-clear
placeholder="请输入操作人"
v-model:value="form.operatorUserId"
/>
</a-form-item>
<a-form-item label="是否展示" name="isShow">
<a-input
allow-clear
placeholder="请输入是否展示"
v-model:value="form.isShow"
/>
</a-form-item>
<a-form-item label="状态, 0上架 1待上架 2待审核 3审核不通过" name="status">
<a-radio-group v-model:value="form.status">
<a-radio :value="0">显示</a-radio>
<a-radio :value="1">隐藏</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="备注" name="comments">
<!-- 基本信息 -->
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600;">基本信息</span>
</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="礼品卡名称" name="name">
<a-input
placeholder="请输入礼品卡名称"
v-model:value="form.name"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="礼品卡密钥" name="code">
<a-input
placeholder="请输入礼品卡密钥"
v-model:value="form.code"
>
<template #suffix>
<a-button type="link" size="small" @click="generateCode">
生成
</a-button>
</template>
</a-input>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="关联商品" name="goodsId">
<a-select
v-model:value="form.goodsId"
placeholder="请选择关联商品"
show-search
:filter-option="false"
:loading="goodsLoading"
@search="searchGoods"
@change="onGoodsChange"
@dropdown-visible-change="onDropdownVisibleChange"
>
<a-select-option v-for="goods in goodsList" :key="goods.goodsId" :value="goods.goodsId">
<div class="goods-option">
<span>{{ goods.name }}</span>
<a-tag color="blue" style="margin-left: 8px;">¥{{ goods.price || 0 }}</a-tag>
</div>
</a-select-option>
<a-select-option v-if="goodsList.length === 0" disabled>
<div style="text-align: center; color: #999;">
{{ goodsLoading ? '加载中...' : '暂无商品数据' }}
</div>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="生成数量" name="num">
<a-input-number
:min="1"
:max="1000"
placeholder="请输入生成数量"
v-model:value="form.num"
style="width: 100%"
>
<template #addonAfter></template>
</a-input-number>
</a-form-item>
</a-col>
</a-row>
<!-- 状态设置 -->
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600;">状态设置</span>
</a-divider>
<a-row :gutter="16">
<a-col :span="8">
<a-form-item label="上架状态" name="status">
<a-select v-model:value="form.status" placeholder="请选择上架状态">
<a-select-option :value="0">
<div class="status-option">
<a-tag color="success">已上架</a-tag>
<span>正常销售</span>
</div>
</a-select-option>
<a-select-option :value="1">
<div class="status-option">
<a-tag color="warning">待上架</a-tag>
<span>准备上架</span>
</div>
</a-select-option>
<a-select-option :value="2">
<div class="status-option">
<a-tag color="processing">待审核</a-tag>
<span>等待审核</span>
</div>
</a-select-option>
<a-select-option :value="3">
<div class="status-option">
<a-tag color="error">审核不通过</a-tag>
<span>审核失败</span>
</div>
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="展示状态" name="isShow">
<a-switch
v-model:checked="form.isShow"
checked-children="展示"
un-checked-children="隐藏"
/>
</a-form-item>
</a-col>
<a-col :span="8">
<a-form-item label="排序" name="sortNumber">
<a-input-number
:min="0"
placeholder="数字越小越靠前"
v-model:value="form.sortNumber"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<!-- 使用信息 -->
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600;">使用信息</span>
</a-divider>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="领取时间" name="takeTime">
<a-date-picker
v-model:value="form.takeTime"
placeholder="请选择领取时间"
show-time
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="领取用户ID" name="userId">
<a-input-number
:min="1"
placeholder="请输入领取用户ID"
v-model:value="form.userId"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<!-- <a-form-item label="操作人ID" name="operatorUserId">-->
<!-- <a-input-number-->
<!-- :min="1"-->
<!-- placeholder="请输入操作人用户ID"-->
<!-- v-model:value="form.operatorUserId"-->
<!-- style="width: 300px"-->
<!-- />-->
<!-- </a-form-item>-->
<a-form-item label="备注信息" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
placeholder="请输入备注信息"
:rows="3"
:maxlength="200"
show-count
/>
</a-form-item>
<a-form-item label="排序号" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="用户ID" name="userId">
<a-input
allow-clear
placeholder="请输入用户ID"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="是否删除, 0否, 1是" name="deleted">
<a-input
allow-clear
placeholder="请输入是否删除, 0否, 1是"
v-model:value="form.deleted"
/>
</a-form-item>
<a-form-item label="修改时间" name="updateTime">
<a-input
allow-clear
placeholder="请输入修改时间"
v-model:value="form.updateTime"
/>
</a-form-item>
<!-- 礼品卡预览 -->
<div class="gift-card-preview" v-if="form.name">
<a-divider orientation="left">
<span style="color: #1890ff; font-weight: 600;">礼品卡预览</span>
</a-divider>
<div class="gift-card">
<div class="gift-card-header">
<div class="gift-card-title">{{ form.name }}</div>
<div class="gift-card-status">
<a-tag :color="getStatusColor()">{{ getStatusText() }}</a-tag>
</div>
</div>
<div class="gift-card-body">
<div class="gift-card-code">
<span class="code-label">卡密</span>
<span class="code-value">{{ form.code || '未设置' }}</span>
</div>
<div class="gift-card-goods" v-if="selectedGoods">
<span class="goods-label">关联商品</span>
<span class="goods-name">{{ selectedGoods.name }}</span>
<a-tag color="blue" style="margin-left: 8px;">¥{{ selectedGoods.price }}</a-tag>
</div>
</div>
<div class="gift-card-footer">
<div class="gift-card-info">
<span v-if="form.takeTime">领取时间{{ formatTime(form.takeTime) }}</span>
<span v-else>未领取</span>
</div>
</div>
</div>
</div>
</a-form>
</ele-modal>
</template>
@@ -112,21 +231,17 @@
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { assignObject } from 'ele-admin-pro';
import { addShopGift, updateShopGift } from '@/api/shop/shopGift';
import { ShopGift } from '@/api/shop/shopGift/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
import { listShopGoods } from '@/api/shop/shopGoods';
import { ShopGoods } from '@/api/shop/shopGoods/model';
import dayjs from 'dayjs';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
@@ -146,32 +261,34 @@
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 用户信息
// 表单数据
const form = reactive<ShopGift>({
id: undefined,
name: undefined,
code: undefined,
name: '',
code: '',
goodsId: undefined,
takeTime: undefined,
operatorUserId: undefined,
isShow: undefined,
status: undefined,
comments: undefined,
sortNumber: undefined,
isShow: true,
status: 0,
comments: '',
sortNumber: 100,
userId: undefined,
deleted: undefined,
deleted: 0,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
shopGiftId: undefined,
shopGiftName: '',
status: 0,
comments: '',
sortNumber: 100
num: 1
});
// 商品列表
const goodsList = ref<ShopGoods[]>([]);
// 商品加载状态
const goodsLoading = ref(false);
// 选中的商品
const selectedGoods = ref<ShopGoods | null>(null);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
@@ -179,28 +296,131 @@
// 表单验证规则
const rules = reactive({
shopGiftName: [
name: [
{
required: true,
type: 'string',
message: '请填写礼品卡名称',
message: '请输入礼品卡名称',
trigger: 'blur'
},
{
min: 2,
max: 50,
message: '礼品卡名称长度应在2-50个字符之间',
trigger: 'blur'
}
],
code: [
{
required: true,
message: '请输入礼品卡密钥',
trigger: 'blur'
},
{
min: 6,
max: 32,
message: '密钥长度应在6-32个字符之间',
trigger: 'blur'
}
],
goodsId: [
{
required: true,
message: '请选择关联商品',
trigger: 'change'
}
],
num: [
{
required: true,
message: '请输入生成数量',
trigger: 'blur'
},
{
validator: (rule: any, value: any) => {
if (value && (value < 1 || value > 1000)) {
return Promise.reject('生成数量必须在1-1000之间');
}
return Promise.resolve();
},
trigger: 'blur'
}
],
status: [
{
required: true,
message: '请选择上架状态',
trigger: 'change'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
/* 生成密钥 */
const generateCode = () => {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let result = '';
for (let i = 0; i < 8; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
form.code = result;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
/* 搜索商品 */
const searchGoods = async (value: string) => {
if (value && value.trim()) {
goodsLoading.value = true;
try {
const res = await listShopGoods({ keywords: value.trim() });
goodsList.value = res || [];
console.log('搜索到的商品:', goodsList.value);
} catch (e) {
console.error('搜索商品失败:', e);
goodsList.value = [];
} finally {
goodsLoading.value = false;
}
}
};
/* 下拉框显示状态改变 */
const onDropdownVisibleChange = (open: boolean) => {
if (open && goodsList.value.length === 0) {
// 当下拉框打开且没有数据时,加载默认商品列表
getGoodsList();
}
};
/* 商品选择改变 */
const onGoodsChange = (goodsId: number) => {
selectedGoods.value = goodsList.value.find(goods => goods.goodsId === goodsId) || null;
console.log('选中的商品:', selectedGoods.value);
};
/* 获取状态颜色 */
const getStatusColor = () => {
const colorMap = {
0: 'success',
1: 'warning',
2: 'processing',
3: 'error'
};
return colorMap[form.status] || 'default';
};
/* 获取状态文本 */
const getStatusText = () => {
const textMap = {
0: '已上架',
1: '待上架',
2: '待审核',
3: '审核不通过'
};
return textMap[form.status] || '未知状态';
};
/* 格式化时间 */
const formatTime = (time: any) => {
if (!time) return '';
return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
};
const { resetFields } = useForm(form, rules);
@@ -217,6 +437,19 @@
const formData = {
...form
};
// 处理时间字段转换
if (formData.takeTime && dayjs.isDayjs(formData.takeTime)) {
formData.takeTime = formData.takeTime.format('YYYY-MM-DD HH:mm:ss');
}
// 处理数据类型转换
if (formData.isShow !== undefined) {
formData.isShow = formData.isShow === '1' || formData.isShow === true;
}
console.log('提交的礼品卡数据:', formData);
const saveOrUpdate = isUpdate.value ? updateShopGift : addShopGift;
saveOrUpdate(formData)
.then((msg) => {
@@ -228,27 +461,73 @@
.catch((e) => {
loading.value = false;
message.error(e.message);
console.error('保存失败:', e);
});
})
.catch(() => {});
.catch((errors) => {
console.error('表单验证失败:', errors);
});
};
/* 获取商品列表 */
const getGoodsList = async () => {
if (goodsLoading.value) return; // 防止重复加载
goodsLoading.value = true;
try {
const res = await listShopGoods({ pageSize: 50 }); // 限制返回数量
goodsList.value = res || [];
console.log('获取到的商品列表:', goodsList.value);
} catch (e) {
console.error('获取商品列表失败:', e);
goodsList.value = [];
} finally {
goodsLoading.value = false;
}
};
watch(
() => props.visible,
(visible) => {
async (visible) => {
if (visible) {
images.value = [];
await getGoodsList();
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
// 处理时间字段转换
if (props.data.takeTime) {
form.takeTime = dayjs(props.data.takeTime);
}
// 设置选中的商品
if (props.data.goodsId) {
selectedGoods.value = goodsList.value.find(goods => goods.goodsId === props.data.goodsId) || null;
}
isUpdate.value = true;
} else {
// 重置为默认值
Object.assign(form, {
id: undefined,
name: '',
code: '',
goodsId: undefined,
takeTime: undefined,
operatorUserId: undefined,
isShow: true,
status: 0,
comments: '',
sortNumber: 100,
userId: undefined,
deleted: 0,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
num: 1
});
selectedGoods.value = null;
isUpdate.value = false;
}
} else {
@@ -258,3 +537,129 @@
{ immediate: true }
);
</script>
<style lang="less" scoped>
.goods-option,
.status-option {
display: flex;
align-items: center;
justify-content: space-between;
.ant-tag {
margin-left: 8px;
}
span {
color: #666;
font-size: 12px;
}
}
.gift-card-preview {
margin-top: 24px;
.gift-card {
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 50%, #fecfef 100%);
border-radius: 12px;
padding: 20px;
color: #333;
position: relative;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
&::before {
content: '';
position: absolute;
top: -50px;
right: -50px;
width: 100px;
height: 100px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
}
.gift-card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.gift-card-title {
font-size: 20px;
font-weight: bold;
color: #333;
}
}
.gift-card-body {
margin-bottom: 16px;
.gift-card-code {
margin-bottom: 12px;
.code-label {
font-weight: 600;
color: #666;
}
.code-value {
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
background: rgba(255, 255, 255, 0.8);
padding: 4px 8px;
border-radius: 4px;
margin-left: 8px;
}
}
.gift-card-goods {
.goods-label {
font-weight: 600;
color: #666;
}
.goods-name {
margin-left: 8px;
color: #333;
}
}
}
.gift-card-footer {
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.3);
.gift-card-info {
font-size: 12px;
color: #666;
}
}
}
}
:deep(.ant-divider-horizontal.ant-divider-with-text-left) {
margin: 24px 0 16px 0;
.ant-divider-inner-text {
padding: 0 16px 0 0;
}
}
:deep(.ant-form-item) {
margin-bottom: 16px;
}
:deep(.ant-select-selection-item) {
display: flex;
align-items: center;
}
:deep(.ant-input-number) {
width: 100%;
}
:deep(.ant-alert) {
.ant-alert-message {
font-weight: 600;
}
}
</style>