| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824 |
- import dayjs from "dayjs";
- import { message } from "@/utils/message";
- import { addDialog } from "@/components/ReDialog";
- import {
- getCouponList,
- getCouponById,
- createCoupon,
- updateCouponStatus,
- deleteCoupon,
- distributeCoupon
- } from "@/api/coupon";
- import { distributeCoupons, getCouponDistributeRecords } from "@/api/marketing";
- import { getUserList } from "@/api/user";
- import ShopSelector from "../../components/ShopSelector.vue";
- import ProductSelector from "../../components/ProductSelector.vue";
- import DeviceSelector from "../../components/DeviceSelector.vue";
- import type { PaginationProps } from "@pureadmin/table";
- import { deviceDetection } from "@pureadmin/utils";
- import { onMounted, reactive, ref, toRaw } from "vue";
- import {
- ElForm,
- ElInput,
- ElFormItem,
- ElSelect,
- ElOption,
- ElDatePicker,
- ElInputNumber,
- ElTag,
- ElRadioGroup,
- ElRadioButton,
- ElAlert,
- ElTable,
- ElTableColumn
- } from "element-plus";
- import {
- initPagination,
- handlePageSizeChange,
- handleCurrentPageChange,
- resetPagination,
- updatePaginationData
- } from "@/utils/paginationHelper";
- interface SearchFormProps {
- name: string;
- type: number | undefined;
- status: number | undefined;
- receiveType: string | undefined;
- }
- interface CouponFormData {
- id?: number;
- name: string;
- activityId: string;
- receiveType: string; // Collect-主动领取,Release-系统发放
- type: number | undefined;
- value: number | null;
- totalCount: number | null;
- shopIds: number[];
- deviceIds: number[];
- productIds: number[];
- validPeriod: string[];
- description: string;
- applyScope: number;
- deviceScope: number;
- productScope: number;
- }
- export function useCoupon() {
- const form = reactive<SearchFormProps>({
- name: "",
- type: undefined,
- status: undefined,
- receiveType: undefined
- });
- const loading = ref(true);
- const dataList = ref([]);
- const pagination = reactive<PaginationProps>(initPagination());
- type TagType = "success" | "primary" | "warning" | "info" | "danger";
- const typeMap: Record<number, { text: string; type: TagType }> = {
- 1: { text: "满减券", type: "primary" },
- 2: { text: "折扣券", type: "success" },
- 3: { text: "立减券", type: "warning" },
- 4: { text: "商品券", type: "info" }
- };
- const statusMap: Record<number, { text: string; type: TagType }> = {
- 0: { text: "未启用", type: "info" },
- 1: { text: "已启用", type: "success" },
- 2: { text: "已过期", type: "danger" }
- };
- const columns: TableColumnList = [
- {
- label: "优惠券ID",
- prop: "id",
- width: 200
- },
- {
- label: "优惠券名称",
- prop: "couponName",
- minWidth: 150
- },
- {
- label: "关联活动",
- prop: "activityName",
- minWidth: 120,
- formatter: ({ activityName }) => activityName || "无关联活动"
- },
- {
- label: "类型",
- prop: "couponType",
- minWidth: 100,
- cellRenderer: ({ row }) => {
- const item = typeMap[row.couponType] || { text: "未知", type: "info" };
- return <ElTag type={item.type}>{item.text}</ElTag>;
- }
- },
- {
- label: "发放类型",
- prop: "receiveType",
- minWidth: 80,
- cellRenderer: ({ row }) => {
- if (row.receiveType === 'Collect') {
- return <ElTag type="success">主动领取</ElTag>;
- }
- return <ElTag type="primary">系统发放</ElTag>;
- }
- },
- {
- label: "优惠值",
- prop: "discountValue",
- minWidth: 80,
- cellRenderer: ({ row }) => {
- if (row.couponType === 2) {
- return `${row.discountValue}折`;
- }
- return `¥${row.discountValue}`;
- }
- },
- {
- label: "发放总量",
- prop: "totalCount",
- minWidth: 90
- },
- {
- label: "已领取",
- prop: "receivedCount",
- minWidth: 90
- },
- {
- label: "已使用",
- prop: "usedCount",
- minWidth: 90
- },
- {
- label: "有效期",
- minWidth: 200,
- cellRenderer: ({ row }) => {
- const start = row.startTime ? dayjs(row.startTime).format("YYYY-MM-DD") : "";
- const end = row.endTime ? dayjs(row.endTime).format("YYYY-MM-DD") : "";
- return `${start} ~ ${end}`;
- }
- },
- {
- label: "状态",
- prop: "status",
- minWidth: 100,
- cellRenderer: ({ row }) => {
- const item = statusMap[row.status] || { text: "未知", type: "info" };
- return <ElTag type={item.type}>{item.text}</ElTag>;
- }
- },
- {
- label: "操作",
- fixed: "right",
- width: 200,
- slot: "operation"
- }
- ];
- async function onSearch() {
- loading.value = true;
- try {
- const searchParams: any = {
- page: pagination.currentPage,
- pageSize: pagination.pageSize
- };
- if (form.name) searchParams.name = form.name;
- if (form.type !== undefined) searchParams.type = form.type;
- if (form.status !== undefined) searchParams.status = form.status;
- if (form.receiveType) searchParams.receiveType = form.receiveType;
- const { data } = await getCouponList(searchParams);
- if (data) {
- dataList.value = data.list || [];
- updatePaginationData(pagination, data);
- }
- } finally {
- loading.value = false;
- }
- }
- function resetForm(formEl) {
- if (!formEl) return;
- formEl.resetFields();
- resetPagination(pagination, onSearch);
- }
- async function handleDelete(row) {
- const { code } = await deleteCoupon(row.id);
- if (code === 200) {
- message("删除成功", { type: "success" });
- onSearch();
- }
- }
- async function handleToggleStatus(row) {
- const newStatus = row.status === 1 ? 0 : 1;
- const { code } = await updateCouponStatus(row.id, newStatus);
- if (code === 200) {
- message("状态更新成功", { type: "success" });
- onSearch();
- }
- }
- function handleDistribute(row) {
- const distributeForm = reactive({
- targetType: "all", // all-全部用户, specific-指定用户
- userIds: [],
- distributeCount: 1 // 发放数量
- });
- // 用户列表状态
- const userList = ref([]);
- const userLoading = ref(false);
- const userSearchKeyword = ref("");
- // 加载用户列表
- const loadUserList = async (keyword = "") => {
- userLoading.value = true;
- try {
- const { data } = await getUserList({
- page: 1,
- pageSize: 100,
- phone: keyword || undefined,
- nickname: keyword || undefined
- });
- if (data) {
- userList.value = data.list || [];
- }
- } catch (error) {
- console.error("加载用户列表失败:", error);
- message("加载用户列表失败", { type: "error" });
- } finally {
- userLoading.value = false;
- }
- };
- // 用户搜索
- const handleUserSearch = (query: string) => {
- userSearchKeyword.value = query;
- if (query) {
- loadUserList(query);
- } else {
- loadUserList();
- }
- };
- addDialog({
- title: `发放优惠券 - ${row.couponName || '未知优惠券'}`,
- width: "700px",
- draggable: true,
- fullscreen: deviceDetection(),
- contentRenderer: () => (
- <ElForm label-width="100px" size="default">
- <ElFormItem label="优惠券">
- <ElInput modelValue={row.couponName} disabled />
- </ElFormItem>
- <ElFormItem label="发放总量">
- <ElInput modelValue={`${row.receivedCount || 0} / ${row.totalCount || 0}`} disabled />
- </ElFormItem>
- <ElFormItem label="目标用户" required>
- <ElSelect
- v-model={distributeForm.targetType}
- placeholder="请选择目标用户"
- class="w-full"
- onChange={() => {
- // 切换时清空用户选择
- if (distributeForm.targetType === 'all') {
- distributeForm.userIds = [];
- }
- }}
- >
- <ElOption label="全部用户" value="all" />
- <ElOption label="指定用户" value="specific" />
- </ElSelect>
- </ElFormItem>
- {distributeForm.targetType === 'specific' && (
- <ElFormItem label="选择用户" required>
- {loadUserList()}
- <ElSelect
- v-model={distributeForm.userIds}
- class="w-full"
- placeholder="请输入用户名或手机号搜索"
- >
- {userList.value.map((user: any) => (
- <ElOption
- key={user.id}
- label={`${user.nickname || user.phone || '未知用户'} (${user.phone || '无手机号'})`}
- value={user.id}
- />
- ))}
- </ElSelect>
- <div style="font-size: 12px; color: #9ca3af; margin-top: 4px;">
- 已选择 {distributeForm.userIds.length} 个用户
- </div>
- </ElFormItem>
- )}
- <ElFormItem label="发放数量" required>
- <ElInputNumber
- v-model={distributeForm.distributeCount}
- placeholder="发放数量"
- class="w-full"
- min={1}
- max={row.totalCount - (row.receivedCount || 0)}
- />
- </ElFormItem>
- <ElAlert
- type="info"
- closable={false}
- showIcon={true}
- style={{marginTop: '8px'}}
- >
- {distributeForm.targetType === 'all'
- ? '将向平台所有用户发放该优惠券'
- : `将向已选择的 ${distributeForm.userIds.length} 个用户发放该优惠券`}
- </ElAlert>
- </ElForm>
- ),
- beforeSure: async done => {
- // 验证
- if (distributeForm.targetType === 'specific' && distributeForm.userIds.length === 0) {
- message("请选择至少一个用户", { type: "warning" });
- return;
- }
- try {
- // 调用后端发放接口
- const { code, message: errorMsg } = await distributeCoupon({
- templateId: row.id, // 现在后端返回的是字符串,无需转换
- targetType: distributeForm.targetType === 'all' ? 1 : 3, // 1-全部用户,3-指定用户
- userIds: distributeForm.targetType === 'specific' ? distributeForm.userIds : undefined
- });
- if (code === 200) {
- message("发放成功", { type: "success" });
- done();
- onSearch();
- } else {
- message(errorMsg || "发放失败", { type: "error" });
- }
- } catch (error) {
- const errorMsg = error?.response?.data?.message || "发放失败,请重试";
- message(errorMsg, { type: "error" });
- }
- }
- });
- }
- function handleViewRecords(row) {
- addDialog({
- title: "发放记录",
- width: "800px",
- draggable: true,
- fullscreen: deviceDetection(),
- contentRenderer: () => (
- <div>
- <ElTable data={[]} style="width: 100%">
- <ElTableColumn prop="userName" label="用户名" width={120} />
- <ElTableColumn prop="phone" label="手机号" width={120} />
- <ElTableColumn prop="distributeTime" label="发放时间" width={160} />
- <ElTableColumn prop="status" label="状态" width={100} />
- </ElTable>
- </div>
- )
- });
- }
- const formStyles = `
- .coupon-form {
- padding: 0;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
- }
- .form-block {
- margin-bottom: 20px;
- }
- .block-title {
- font-size: 14px;
- font-weight: 600;
- color: #374151;
- margin-bottom: 14px;
- padding-bottom: 8px;
- border-bottom: 1px solid #e5e7eb;
- }
- .form-grid-2 {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 20px 24px;
- }
- .form-grid-3 {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 20px 24px;
- }
- .type-selector {
- display: grid;
- grid-template-columns: repeat(4, 1fr);
- gap: 12px;
- margin-bottom: 18px;
- }
- .type-btn {
- padding: 12px 16px;
- border: 1px solid #e5e7eb;
- border-radius: 6px;
- background: #fff;
- cursor: pointer;
- transition: all 0.15s ease;
- text-align: center;
- }
- .type-btn:hover {
- border-color: #3b82f6;
- background: #f8fafc;
- }
- .type-btn.active {
- border-color: #3b82f6;
- background: #eff6ff;
- }
- .type-btn .name {
- font-size: 14px;
- font-weight: 600;
- color: #374151;
- margin-bottom: 4px;
- }
- .type-btn .hint {
- font-size: 12px;
- color: #9ca3af;
- }
- .type-btn.active .name {
- color: #3b82f6;
- }
- .scope-grid {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 16px;
- }
- .scope-card {
- background: #f9fafb;
- border: 1px solid #e5e7eb;
- border-radius: 8px;
- padding: 14px 16px;
- }
- .scope-card .card-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 12px;
- }
- .scope-card .card-label {
- font-size: 13px;
- font-weight: 500;
- color: #374151;
- }
- .scope-card .card-body {
- min-height: 32px;
- }
- .coupon-form .el-form-item {
- margin-bottom: 0;
- align-items: center;
- }
- .coupon-form .el-form-item__label {
- font-size: 13px;
- color: #606266;
- padding-right: 8px;
- }
- .coupon-form .el-input__wrapper,
- .coupon-form .el-textarea__inner {
- font-size: 13px;
- }
- .coupon-form .form-label-90 .el-form-item__label {
- width: 90px !important;
- }
- .coupon-form .form-label-100 .el-form-item__label {
- width: 100px !important;
- }
- .preview-box {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- padding: 12px 16px;
- background: #f9fafb;
- border-radius: 4px;
- margin-top: 12px;
- font-size: 14px;
- color: #374151;
- }
- .preview-box .value {
- font-weight: 600;
- }
- @media (max-width: 1200px) {
- .form-grid-3,
- .scope-grid {
- grid-template-columns: repeat(2, 1fr);
- }
- .type-selector {
- grid-template-columns: repeat(2, 1fr);
- }
- }
- @media (max-width: 768px) {
- .form-grid-2,
- .form-grid-3,
- .scope-grid {
- grid-template-columns: 1fr;
- }
- .type-selector {
- grid-template-columns: 1fr;
- }
- }
- `;
- function renderForm(formData: CouponFormData) {
- return (
- <div class="coupon-form">
- <style>{formStyles}</style>
- <div class="form-block">
- <div class="block-title">基本信息</div>
- <ElForm label-width="90px" size="default">
- <div class="form-grid-2">
- <ElFormItem label="优惠券名称" required>
- <ElInput v-model={formData.name} placeholder="请输入优惠券名称" clearable maxlength={50} showWordLimit />
- </ElFormItem>
- <ElFormItem label="关联活动">
- <ElSelect v-model={formData.activityId} placeholder="请选择关联的营销活动(可选)" class="w-full" clearable filterable>
- <ElOption label="春节大促活动" value="1" />
- <ElOption label="新品上市活动" value="2" />
- <ElOption label="会员专享活动" value="3" />
- <ElOption label="不关联活动" value="" />
- </ElSelect>
- </ElFormItem>
- <ElFormItem label="发放类型" required>
- <ElRadioGroup v-model={formData.receiveType}>
- <ElRadioButton value="Release">系统发放</ElRadioButton>
- <ElRadioButton value="Collect">主动领取</ElRadioButton>
- </ElRadioGroup>
- </ElFormItem>
- </div>
- <ElAlert
- type="info"
- closable={false}
- showIcon={true}
- style={{marginTop: '12px'}}
- >
- {formData.receiveType === 'Collect'
- ? '主动领取:将显示在用户端小程序领券中心,用户可自行领取'
- : '系统发放:创建后可在列表中点击发放,选择指定用户或全部用户'}
- </ElAlert>
- </ElForm>
- </div>
- <div class="form-block">
- <div class="block-title">优惠券类型</div>
- <div class="type-selector">
- {[1, 2, 3, 4].map(type => (
- <div
- class={`type-btn ${formData.type === type ? 'active' : ''}`}
- onClick={() => { formData.type = type; }}
- >
- <div class="name">{typeMap[type].text}</div>
- <div class="hint">
- {type === 1 && "满额减免"}
- {type === 2 && "折扣优惠"}
- {type === 3 && "直接抵扣"}
- {type === 4 && "免费兑换"}
- </div>
- </div>
- ))}
- </div>
- {formData.type && (
- <ElForm label-width="90px" size="default">
- <div class="form-grid-3">
- <ElFormItem label="优惠值" required>
- <ElInputNumber
- v-model={formData.value}
- min={0}
- precision={2}
- class="w-full!"
- placeholder={formData.type === 2 ? "折扣值如8表示8折" : "优惠金额"}
- {...(formData.type !== 2 ? { "data-prefix": "¥" as any } : {})}
- />
- </ElFormItem>
- <ElFormItem label="发放总量" required>
- <ElInputNumber
- v-model={formData.totalCount}
- min={1}
- class="w-full!"
- placeholder="发放总数量"
- />
- </ElFormItem>
- <ElFormItem label="有效期" required>
- <ElDatePicker
- v-model={formData.validPeriod}
- type="daterange"
- range-separator="至"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- class="w-full!"
- />
- </ElFormItem>
- </div>
- </ElForm>
- )}
- </div>
- <div class="form-block">
- <div class="block-title">适用范围</div>
- <div class="scope-grid">
- <div class="scope-card">
- <div class="card-header">
- <span class="card-label">门店范围</span>
- <ElRadioGroup v-model={formData.applyScope} size="small">
- <ElRadioButton value={1}>全部</ElRadioButton>
- <ElRadioButton value={2}>指定</ElRadioButton>
- </ElRadioGroup>
- </div>
- <div class="card-body">
- {formData.applyScope === 2 && <ShopSelector v-model={formData.shopIds} />}
- </div>
- </div>
- <div class="scope-card">
- <div class="card-header">
- <span class="card-label">设备范围</span>
- <ElRadioGroup v-model={formData.deviceScope} size="small">
- <ElRadioButton value={1}>全部</ElRadioButton>
- <ElRadioButton value={2}>指定</ElRadioButton>
- </ElRadioGroup>
- </div>
- <div class="card-body">
- {formData.deviceScope === 2 && <DeviceSelector v-model={formData.deviceIds} />}
- </div>
- </div>
- <div class="scope-card">
- <div class="card-header">
- <span class="card-label">商品范围</span>
- <ElRadioGroup v-model={formData.productScope} size="small">
- <ElRadioButton value={1}>全部</ElRadioButton>
- <ElRadioButton value={2}>指定</ElRadioButton>
- </ElRadioGroup>
- </div>
- <div class="card-body">
- {formData.productScope === 2 && <ProductSelector v-model={formData.productIds} />}
- </div>
- </div>
- </div>
- </div>
- <div class="form-block">
- <div class="block-title">使用说明</div>
- <ElForm size="default">
- <ElFormItem label="">
- <ElInput v-model={formData.description} type="textarea" rows={2} placeholder="请输入使用说明(可选)" maxlength={500} showWordLimit />
- </ElFormItem>
- </ElForm>
- </div>
- </div>
- );
- }
- function validateForm(formData: CouponFormData): boolean {
- if (!formData.name) {
- message("请输入优惠券名称", { type: "warning" });
- return false;
- }
- if (!formData.type) {
- message("请选择优惠券类型", { type: "warning" });
- return false;
- }
- if (!formData.value || formData.value <= 0) {
- message("请输入有效的优惠值", { type: "warning" });
- return false;
- }
- if (!formData.totalCount || formData.totalCount <= 0) {
- message("请输入发放总量", { type: "warning" });
- return false;
- }
- return true;
- }
- function handleAdd() {
- const formData = reactive<CouponFormData>({
- name: "",
- activityId: "",
- receiveType: "Release", // 默认为系统发放
- type: undefined,
- value: null,
- totalCount: null,
- shopIds: [],
- deviceIds: [],
- productIds: [],
- validPeriod: [],
- description: "",
- applyScope: 1,
- deviceScope: 1,
- productScope: 1
- });
- addDialog({
- title: "新增优惠券",
- width: "1100px",
- draggable: true,
- fullscreen: deviceDetection(),
- contentRenderer: () => renderForm(formData),
- beforeSure: async (done) => {
- if (!validateForm(formData)) return;
- try {
- // 将前端表单数据转换为后端 DTO 格式
- const submitData = {
- couponName: formData.name,
- couponType: formData.type,
- receiveType: formData.receiveType,
- discountValue: formData.value,
- totalCount: formData.totalCount,
- validType: 1, // 固定时间
- validStartTime: formData.validPeriod?.[0],
- validEndTime: formData.validPeriod?.[1],
- applyScope: formData.applyScope,
- productScope: formData.productScope,
- activityId: formData.activityId ? Number(formData.activityId) : null,
- shopIds: formData.shopIds,
- productIds: formData.productIds,
- couponDesc: formData.description,
- receiveLimit: 1
- };
- const { code, message: errorMsg } = await createCoupon(submitData);
- if (code === 200) {
- message("新增成功", { type: "success" });
- done();
- onSearch();
- } else {
- message(errorMsg || "新增失败", { type: "error" });
- }
- } catch (error: any) {
- // 展示后端返回的具体错误信息
- const errorMsg = error?.response?.data?.message || error?.message || "新增失败,请重试";
- message(errorMsg, { type: "error" });
- }
- }
- });
- }
- function handleSizeChange(val: number) {
- handlePageSizeChange(val, pagination, onSearch);
- }
- function handleCurrentChange(val: number) {
- handleCurrentPageChange(val, pagination, onSearch);
- }
- onMounted(() => {
- onSearch();
- });
- return {
- form,
- loading,
- columns,
- dataList,
- pagination,
- typeMap,
- statusMap,
- onSearch,
- resetForm,
- handleAdd,
- handleDelete,
- handleToggleStatus,
- handleDistribute,
- handleViewRecords,
- handleSizeChange,
- handleCurrentChange
- };
- }
|