Files
mp-vue/src/views/shop/shopGift/components/shopGiftEdit.vue
赵忠林 35b5b35048 feat(shopGift): 优化礼品卡生成功能
- 新增礼品卡预览功能
- 添加使用地址字段
- 优化商品选择界面
- 增加备注信息字段
- 改进表单样式和布局
2025-08-17 19:29:49 +08:00

708 lines
19 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.

<!-- 编辑弹窗 -->
<template>
<ele-modal
width="65%"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '礼品卡详情' : '礼品卡详情'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<!-- 基本信息 -->
<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="请输入礼品卡名称"
:disabled="isUpdate"
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"
:disabled="isUpdate"
>
<template #suffix>
<a-button v-if="!isUpdate" 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"
:disabled="isUpdate"
@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" v-if="!isUpdate">
<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-col :span="12">
<a-form-item label="使用地址" name="useLocation">
<a-input
placeholder="请输入使用的门店地址"
v-model:value="form.useLocation"
:disabled="isUpdate"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="备注信息" name="comments">
<a-textarea
v-model:value="form.comments"
:disabled="isUpdate"
placeholder="请输入备注信息"
:rows="3"
:maxlength="200"
show-count
/>
</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="请选择领取时间"
:disabled="isUpdate"
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"
:disabled="isUpdate"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="操作人ID" name="operatorUserId">
<a-input-number
:min="1"
placeholder="请输入操作人用户ID"
v-model:value="form.operatorUserId"
:disabled="isUpdate"
style="width: 300px"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="操作员备注" name="userId">
<a-textarea
v-model:value="form.operatorRemarks"
:disabled="isUpdate"
placeholder="请输入备注信息"
:rows="3"
:maxlength="200"
show-count
/>
</a-form-item>
</a-col>
</a-row>
<!-- 礼品卡预览 -->
<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>
<span v-if="form.takeTime">领取时间{{ formatTime(form.takeTime) }}</span>
<span v-else>未领取</span>
</a-tag>
</div>
</div>
<div class="gift-card-body">
<div class="gift-card-code">
<span class="code-label text-gray-50">卡密</span>
<span class="code-value">{{ form.code || '未设置' }}</span>
</div>
<div class="gift-card-goods" v-if="selectedGoods">
<span class="goods-label text-gray-50">关联商品</span>
<span class="goods-name">{{ selectedGoods.name }}</span>
<a-tag color="blue" style="margin-left: 8px;">¥{{ selectedGoods.price }}</a-tag>
</div>
<div class="gift-card-goods py-2" v-if="selectedGoods">
<span class="goods-label">使用地址</span>
<span class="goods-name">{{ form.useLocation }}</span>
</div>
</div>
<div class="gift-card-footer">
<div class="gift-card-info text-gray-50">
备注: {{ form.comments }}
</div>
</div>
</div>
</div>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref, reactive, watch} from 'vue';
import {Form, message} from 'ant-design-vue';
import {assignObject} from 'ele-admin-pro';
import {addShopGift, updateShopGift} from '@/api/shop/shopGift';
import {ShopGift} from '@/api/shop/shopGift/model';
import {FormInstance} from 'ant-design-vue/es/form';
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 props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ShopGift | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
// 表单数据
const form = reactive<ShopGift>({
id: undefined,
name: '',
code: '',
goodsId: undefined,
takeTime: undefined,
operatorUserId: undefined,
operatorUserName: undefined,
operatorRemarks: undefined,
isShow: true,
status: 0,
useLocation: '',
comments: '',
sortNumber: 100,
userId: undefined,
deleted: 0,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
num: 1000
});
// 商品列表
const goodsList = ref<ShopGoods[]>([]);
// 商品加载状态
const goodsLoading = ref(false);
// 选中的商品
const selectedGoods = ref<ShopGoods | null>(null);
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
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 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 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);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
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) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
console.error('保存失败:', e);
});
})
.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,
async (visible) => {
if (visible) {
await getGoodsList();
if (props.data) {
assignObject(form, props.data);
// 处理时间字段转换
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: 1000
});
selectedGoods.value = null;
isUpdate.value = false;
}
} else {
resetFields();
}
},
{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, #667eea 0%, #764ba2 50%, #f093fb 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: #f3f3f3;
}
}
.gift-card-body {
margin-bottom: 16px;
.gift-card-code {
margin-bottom: 12px;
.code-label {
font-weight: 600;
color: #f3f3f3;
}
.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: #f3f3f3;
}
.goods-name {
margin-left: 8px;
color: #f3f3f3;
}
}
}
.gift-card-footer {
padding-top: 16px;
border-top: 1px solid rgba(255, 255, 255, 0.3);
.gift-card-info {
font-size: 12px;
color: #f3f3f3;
}
}
}
}
: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>