Files
lilishop-ui/manager/src/views/promotions/gift-card-cash/gift-card-cash-activity-add.vue
pikachu1995@126.com c0cc8d8598 feat(manager): 优化列表操作列规范、会员分组统计、漏斗图重构与订单交互
1. 订单交互:移除 orderList 缓存机制修复默认带入订单号问题,支持 query 联动;

2. 会员模块:会员分组新增人数统计与联动,全量规范会员提现、充值、日志等操作列样式;

3. 商品与销量:虚拟销量优化首列缩略图与对齐方式,规范品牌与商品分组操作;

4. 数据统计:重构交易转化漏斗图为自适应 SVG 渐变图与指标卡片;

5. 系统与营销:统一菜单/部门操作列样式,优化 OSS 模块、营销活动与首页待办对齐。
2026-10-30 06:51:48 +08:00

633 lines
17 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>
<div class="gift-card-activity-page">
<!-- 顶部标题与返回栏 -->
<el-card shadow="never" class="header-card">
<div class="page-header-row">
<div class="header-left">
<div class="header-title">
<span>{{ pageTitle }}</span>
<el-tag
size="small"
effect="plain"
:type="onlyView ? 'info' : (isEditMode ? 'warning' : 'primary')"
class="mode-tag"
>
{{ onlyView ? "查看模式" : (isEditMode ? "编辑模式" : "新建模式") }}
</el-tag>
<el-tag
v-if="form.cardType"
size="small"
effect="dark"
type="primary"
class="type-tag"
>
{{ form.cardType === "CASH" ? "现金卡" : "提货卡" }}
</el-tag>
<span v-if="route.query.id" class="activity-id font-mono">
ID: {{ route.query.id }}
</span>
</div>
</div>
<div class="header-right">
<el-button @click="closeCurrentPage">返回礼品卡列表</el-button>
</div>
</div>
</el-card>
<!-- 礼品卡基础信息卡片 -->
<el-card shadow="never" v-loading="detailLoading" class="info-card mt_10">
<div class="section-header">
<span class="section-indicator"></span>
<span class="section-title">礼品卡配置信息</span>
</div>
<!-- 查看模式:以结构化描述列表呈现 -->
<template v-if="onlyView">
<el-descriptions :column="2" border class="custom-descriptions">
<el-descriptions-item label="礼品卡名称">
<span class="font-semibold text-dark">{{ form.giftCardName || "-" }}</span>
</el-descriptions-item>
<el-descriptions-item label="礼品卡类型">
<el-tag size="small" effect="plain" type="primary">
{{ form.cardType === "CASH" ? "现金卡" : "提货卡" }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="礼品卡面值">
<span class="price-highlight font-mono font-semibold">
{{ form.faceValue ? $filters.unitPrice(form.faceValue, "¥") : "-" }}
</span>
</el-descriptions-item>
<el-descriptions-item label="总库存">
<span class="font-mono text-dark font-semibold">
{{ form.stockTotal != null ? `${form.stockTotal} 张` : "-" }}
</span>
<span class="hint-inline">(最多可制发凭证张数)</span>
</el-descriptions-item>
<el-descriptions-item label="有效期限" :span="2">
<span class="font-mono text-dark">{{ validityDisplayText }}</span>
</el-descriptions-item>
<el-descriptions-item v-if="activityDetail.createTime" label="创建时间">
<span class="font-mono text-secondary">{{ activityDetail.createTime }}</span>
</el-descriptions-item>
<el-descriptions-item v-if="route.query.id" label="活动ID">
<span class="font-mono text-secondary">{{ route.query.id }}</span>
</el-descriptions-item>
</el-descriptions>
</template>
<!-- 编辑 / 新增模式:表单输入 -->
<template v-else>
<el-form
ref="formRef"
:model="form"
label-width="120px"
:rules="formRule"
class="edit-form"
>
<el-form-item label="礼品卡名称" prop="giftCardName">
<el-input
v-model="form.giftCardName"
type="text"
placeholder="请输入礼品卡名称"
clearable
maxlength="64"
show-word-limit
style="width: 380px"
/>
</el-form-item>
<el-form-item label="礼品卡类型" prop="cardType">
<el-radio-group v-model="form.cardType">
<el-radio value="CASH">现金卡</el-radio>
<el-radio value="PICKUP" disabled>提货卡(暂未开放)</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="面值" prop="faceValue">
<el-input
v-model="form.faceValue"
type="number"
placeholder="请填写面值金额"
clearable
style="width: 260px"
>
<template #append>元</template>
</el-input>
</el-form-item>
<el-form-item label="总库存" prop="stockTotal">
<el-input-number
v-model="form.stockTotal"
:min="1"
:precision="0"
placeholder="最多可制发凭证张数"
style="width: 220px"
/>
<span class="input-tip-text">张(最多可制发凭证张数)</span>
</el-form-item>
<el-form-item label="有效期限" required class="validity-form-item">
<el-radio-group v-model="form.validityType" class="validity-radio-group">
<div class="validity-row">
<el-radio value="LONG_TERM">长期有效</el-radio>
</div>
<div class="validity-row">
<el-radio value="AFTER_ACTIVATE_MONTHS">
<span class="radio-inline-text">自激活后</span>
<el-input-number
v-model="form.validMonthsAfterActivate"
:min="1"
:max="99"
:precision="0"
placeholder="1-99"
:disabled="form.validityType !== 'AFTER_ACTIVATE_MONTHS'"
class="validity-input-num"
/>
<span class="radio-inline-text">个月内有效</span>
</el-radio>
</div>
<div class="validity-row">
<el-radio value="FIXED_UNTIL">
<span class="radio-inline-text">指定有效期至</span>
<el-date-picker
v-model="form.validEndTime"
type="datetime"
format="YYYY-MM-DD HH:mm:ss"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择有效截止时间"
clearable
:disabled="form.validityType !== 'FIXED_UNTIL'"
class="validity-date-picker"
style="width: 220px"
/>
</el-radio>
</div>
</el-radio-group>
</el-form-item>
</el-form>
</template>
</el-card>
<!-- 底部操作栏 -->
<el-card shadow="never" class="footer-card mt_10">
<div class="footer-actions">
<el-button @click="closeCurrentPage">
{{ onlyView ? "返回" : "取消" }}
</el-button>
<el-button
v-if="!onlyView"
type="primary"
:loading="submitLoading"
@click="handleSubmit"
>
{{ isEditMode ? "保存修改" : "提交创建" }}
</el-button>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive, computed, watch, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import {
saveGiftCardCashActivity,
getGiftCardCashActivity,
updateGiftCardCashActivity,
} from "@/api/promotion";
import { regular } from "@/utils";
import { unixToDate } from "@/utils/filters";
import { Message } from "@/utils/message";
import { useAppStore } from "@/stores/app";
defineOptions({ name: "addGiftCardCashActivity" });
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
const detailLoading = ref(false);
const submitLoading = ref(false);
const activityDetail = ref({});
const form = reactive({
giftCardName: "",
cardType: "CASH",
faceValue: "",
stockTotal: null,
validityType: "LONG_TERM",
validEndTime: null,
validMonthsAfterActivate: null,
});
const formRef = ref(null);
const onlyView = computed(() => {
const q = route.query.onlyView;
return q === "true" || q === true;
});
const isEditMode = computed(() => {
return !!route.query.id && !onlyView.value;
});
const pageTitle = computed(() => {
if (onlyView.value) {
return "查看礼品卡";
}
if (isEditMode.value) {
return "编辑礼品卡";
}
return "添加礼品卡";
});
const validityDisplayText = computed(() => {
const type = String(form.validityType || "").toUpperCase();
if (type === "LONG_TERM") {
return "长期有效";
}
if (type === "AFTER_ACTIVATE_MONTHS") {
const m = form.validMonthsAfterActivate;
return m != null && m !== "" ? `自激活后 ${m} 个月内有效` : "自激活后指定月数有效";
}
if (type === "FIXED_UNTIL") {
return form.validEndTime ? `指定有效期至 ${form.validEndTime}` : "指定时间有效";
}
return "-";
});
const checkFaceValue = (rule, value, callback) => {
if (!value && value !== 0) {
return callback(new Error("面额不能为空"));
}
if (!regular.money.test(value)) {
callback(new Error("请输入正整数或者两位小数"));
} else if (parseFloat(value) > 99999999) {
callback(new Error("面额设置超过上限值"));
} else if (parseFloat(value) < 0.01) {
callback(new Error("面值必须大于 0"));
} else {
callback();
}
};
const validateStock = (rule, value, callback) => {
if (value === null || value === undefined || value === "") {
callback(new Error("请填写总库存"));
return;
}
if (Number(value) < 1) {
callback(new Error("活动库存至少为 1"));
return;
}
callback();
};
const formRule = {
giftCardName: [
{ required: true, message: "活动名称不能为空", trigger: "blur" },
{ type: "string", max: 64, message: "活动名称过长", trigger: "blur" },
],
faceValue: [{ required: true, message: "请输入面额" }, { validator: checkFaceValue }],
stockTotal: [{ required: true, validator: validateStock, trigger: "blur" }],
};
const parseDetailDate = (val) => {
if (!val) {
return null;
}
if (val instanceof Date) {
return val;
}
const d = new Date(val);
return Number.isNaN(d.getTime()) ? null : d;
};
const loadDetail = (id) => {
detailLoading.value = true;
getGiftCardCashActivity(id)
.then((res) => {
if (!res.success || !res.result) {
return;
}
activityDetail.value = res.result;
const d = res.result;
form.giftCardName = d.giftCardName || "";
form.cardType = d.cardType || "CASH";
form.faceValue =
d.faceValue !== null && d.faceValue !== undefined ? String(d.faceValue) : "";
form.stockTotal =
d.stockTotal !== null && d.stockTotal !== undefined ? d.stockTotal : null;
form.validityType = d.validityType || "LONG_TERM";
const endDate = parseDetailDate(d.validEndTime);
form.validEndTime = endDate ? unixToDate(endDate.getTime() / 1000) : null;
form.validMonthsAfterActivate =
d.validMonthsAfterActivate !== null &&
d.validMonthsAfterActivate !== undefined &&
d.validMonthsAfterActivate !== ""
? d.validMonthsAfterActivate
: null;
})
.finally(() => {
detailLoading.value = false;
});
};
const closeCurrentPage = () => {
const name = route.name;
if (name && appStore?.removeTag) {
appStore.removeTag(name);
}
if (localStorage?.pageOpenedList && appStore?.pageOpenedList) {
localStorage.pageOpenedList = JSON.stringify(appStore.pageOpenedList);
}
if (window.history.length > 1) {
router.go(-1);
} else {
router.push({ name: "manager-gift-card-cash" });
}
};
const validateByValidityType = () => {
const t = form.validityType;
if (t === "FIXED_UNTIL") {
if (!form.validEndTime) {
Message.warning("请选择结束时间");
return false;
}
}
if (t === "AFTER_ACTIVATE_MONTHS") {
const m = form.validMonthsAfterActivate;
if (m === null || m === undefined || m === "" || Number(m) < 1 || Number(m) > 99) {
Message.warning("请输入1-99的整数");
return false;
}
}
return true;
};
const buildPayload = () => {
const payload = {
giftCardName: (form.giftCardName || "").trim(),
cardType: form.cardType || "CASH",
faceValue: parseFloat(form.faceValue),
validityType: form.validityType || "LONG_TERM",
stockTotal: form.stockTotal,
};
if (payload.validityType === "FIXED_UNTIL") {
const endDate = new Date(form.validEndTime);
payload.validEndTime = Number.isNaN(endDate.getTime())
? form.validEndTime
: endDate.getTime();
}
if (payload.validityType === "AFTER_ACTIVATE_MONTHS") {
payload.validMonthsAfterActivate = form.validMonthsAfterActivate;
}
return payload;
};
const handleSubmit = () => {
if (onlyView.value) {
return;
}
formRef.value.validate((valid) => {
if (!valid) {
return;
}
if (!validateByValidityType()) {
return;
}
submitLoading.value = true;
const req = isEditMode.value
? updateGiftCardCashActivity(route.query.id, buildPayload())
: saveGiftCardCashActivity(buildPayload());
req
.then((res) => {
if (res.success) {
Message.success(isEditMode.value ? "礼品卡活动已更新" : "礼品卡活动创建成功");
closeCurrentPage();
}
})
.finally(() => {
submitLoading.value = false;
});
});
};
watch(
() => form.validityType,
(v) => {
if (v !== "FIXED_UNTIL") {
form.validEndTime = null;
}
if (v !== "AFTER_ACTIVATE_MONTHS") {
form.validMonthsAfterActivate = null;
}
}
);
watch(
() => route.query.id,
(id) => {
if (id) {
loadDetail(id);
}
}
);
onMounted(() => {
if (route.query.id) {
loadDetail(route.query.id);
}
});
</script>
<style lang="scss" scoped>
.gift-card-activity-page {
padding: 16px 20px;
background-color: #f0f2f5;
min-height: 100vh;
box-sizing: border-box;
.mt_10 {
margin-top: 12px;
}
.font-mono {
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
}
.font-semibold {
font-weight: 600;
}
.text-dark {
color: #303133;
}
.text-secondary {
color: #606266;
}
.header-card {
border: none;
border-radius: 4px;
:deep(.el-card__body) {
padding: 16px 20px;
}
.page-header-row {
display: flex;
align-items: center;
justify-content: space-between;
.header-left {
.header-title {
display: flex;
align-items: center;
gap: 12px;
font-size: 18px;
font-weight: 600;
color: #1f2d3d;
.mode-tag,
.type-tag {
font-weight: normal;
}
.activity-id {
font-size: 13px;
color: #909399;
font-weight: normal;
}
}
}
}
}
.info-card {
border: none;
border-radius: 4px;
:deep(.el-card__body) {
padding: 20px;
}
.section-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
.section-indicator {
width: 4px;
height: 16px;
background-color: #409eff;
border-radius: 2px;
}
.section-title {
font-size: 15px;
font-weight: 600;
color: #303133;
}
}
.custom-descriptions {
:deep(.el-descriptions__label) {
width: 130px;
background-color: #f8f9fb;
color: #606266;
font-weight: 500;
}
:deep(.el-descriptions__content) {
color: #303133;
}
.price-highlight {
color: #f56c6c;
font-size: 16px;
}
.hint-inline {
font-size: 12px;
color: #909399;
margin-left: 6px;
}
}
.edit-form {
padding: 8px 12px;
.input-tip-text {
font-size: 13px;
color: #909399;
margin-left: 10px;
}
.validity-radio-group {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
width: 100%;
.validity-row {
display: flex;
align-items: center;
:deep(.el-radio) {
margin-right: 0;
display: inline-flex;
align-items: center;
}
:deep(.el-radio__label) {
display: inline-flex;
align-items: center;
}
.radio-inline-text {
font-size: 14px;
}
.validity-input-num {
width: 130px;
margin: 0 10px;
}
.validity-date-picker {
margin-left: 10px;
}
}
}
}
}
.footer-card {
border: none;
border-radius: 4px;
:deep(.el-card__body) {
padding: 16px 20px;
}
.footer-actions {
display: flex;
justify-content: center;
gap: 16px;
}
}
}
</style>