hook.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824
  1. import dayjs from "dayjs";
  2. import { message } from "@/utils/message";
  3. import { addDialog } from "@/components/ReDialog";
  4. import {
  5. getCouponList,
  6. getCouponById,
  7. createCoupon,
  8. updateCouponStatus,
  9. deleteCoupon,
  10. distributeCoupon
  11. } from "@/api/coupon";
  12. import { distributeCoupons, getCouponDistributeRecords } from "@/api/marketing";
  13. import { getUserList } from "@/api/user";
  14. import ShopSelector from "../../components/ShopSelector.vue";
  15. import ProductSelector from "../../components/ProductSelector.vue";
  16. import DeviceSelector from "../../components/DeviceSelector.vue";
  17. import type { PaginationProps } from "@pureadmin/table";
  18. import { deviceDetection } from "@pureadmin/utils";
  19. import { onMounted, reactive, ref, toRaw } from "vue";
  20. import {
  21. ElForm,
  22. ElInput,
  23. ElFormItem,
  24. ElSelect,
  25. ElOption,
  26. ElDatePicker,
  27. ElInputNumber,
  28. ElTag,
  29. ElRadioGroup,
  30. ElRadioButton,
  31. ElAlert,
  32. ElTable,
  33. ElTableColumn
  34. } from "element-plus";
  35. import {
  36. initPagination,
  37. handlePageSizeChange,
  38. handleCurrentPageChange,
  39. resetPagination,
  40. updatePaginationData
  41. } from "@/utils/paginationHelper";
  42. interface SearchFormProps {
  43. name: string;
  44. type: number | undefined;
  45. status: number | undefined;
  46. receiveType: string | undefined;
  47. }
  48. interface CouponFormData {
  49. id?: number;
  50. name: string;
  51. activityId: string;
  52. receiveType: string; // Collect-主动领取,Release-系统发放
  53. type: number | undefined;
  54. value: number | null;
  55. totalCount: number | null;
  56. shopIds: number[];
  57. deviceIds: number[];
  58. productIds: number[];
  59. validPeriod: string[];
  60. description: string;
  61. applyScope: number;
  62. deviceScope: number;
  63. productScope: number;
  64. }
  65. export function useCoupon() {
  66. const form = reactive<SearchFormProps>({
  67. name: "",
  68. type: undefined,
  69. status: undefined,
  70. receiveType: undefined
  71. });
  72. const loading = ref(true);
  73. const dataList = ref([]);
  74. const pagination = reactive<PaginationProps>(initPagination());
  75. type TagType = "success" | "primary" | "warning" | "info" | "danger";
  76. const typeMap: Record<number, { text: string; type: TagType }> = {
  77. 1: { text: "满减券", type: "primary" },
  78. 2: { text: "折扣券", type: "success" },
  79. 3: { text: "立减券", type: "warning" },
  80. 4: { text: "商品券", type: "info" }
  81. };
  82. const statusMap: Record<number, { text: string; type: TagType }> = {
  83. 0: { text: "未启用", type: "info" },
  84. 1: { text: "已启用", type: "success" },
  85. 2: { text: "已过期", type: "danger" }
  86. };
  87. const columns: TableColumnList = [
  88. {
  89. label: "优惠券ID",
  90. prop: "id",
  91. width: 200
  92. },
  93. {
  94. label: "优惠券名称",
  95. prop: "couponName",
  96. minWidth: 150
  97. },
  98. {
  99. label: "关联活动",
  100. prop: "activityName",
  101. minWidth: 120,
  102. formatter: ({ activityName }) => activityName || "无关联活动"
  103. },
  104. {
  105. label: "类型",
  106. prop: "couponType",
  107. minWidth: 100,
  108. cellRenderer: ({ row }) => {
  109. const item = typeMap[row.couponType] || { text: "未知", type: "info" };
  110. return <ElTag type={item.type}>{item.text}</ElTag>;
  111. }
  112. },
  113. {
  114. label: "发放类型",
  115. prop: "receiveType",
  116. minWidth: 80,
  117. cellRenderer: ({ row }) => {
  118. if (row.receiveType === 'Collect') {
  119. return <ElTag type="success">主动领取</ElTag>;
  120. }
  121. return <ElTag type="primary">系统发放</ElTag>;
  122. }
  123. },
  124. {
  125. label: "优惠值",
  126. prop: "discountValue",
  127. minWidth: 80,
  128. cellRenderer: ({ row }) => {
  129. if (row.couponType === 2) {
  130. return `${row.discountValue}折`;
  131. }
  132. return `¥${row.discountValue}`;
  133. }
  134. },
  135. {
  136. label: "发放总量",
  137. prop: "totalCount",
  138. minWidth: 90
  139. },
  140. {
  141. label: "已领取",
  142. prop: "receivedCount",
  143. minWidth: 90
  144. },
  145. {
  146. label: "已使用",
  147. prop: "usedCount",
  148. minWidth: 90
  149. },
  150. {
  151. label: "有效期",
  152. minWidth: 200,
  153. cellRenderer: ({ row }) => {
  154. const start = row.startTime ? dayjs(row.startTime).format("YYYY-MM-DD") : "";
  155. const end = row.endTime ? dayjs(row.endTime).format("YYYY-MM-DD") : "";
  156. return `${start} ~ ${end}`;
  157. }
  158. },
  159. {
  160. label: "状态",
  161. prop: "status",
  162. minWidth: 100,
  163. cellRenderer: ({ row }) => {
  164. const item = statusMap[row.status] || { text: "未知", type: "info" };
  165. return <ElTag type={item.type}>{item.text}</ElTag>;
  166. }
  167. },
  168. {
  169. label: "操作",
  170. fixed: "right",
  171. width: 200,
  172. slot: "operation"
  173. }
  174. ];
  175. async function onSearch() {
  176. loading.value = true;
  177. try {
  178. const searchParams: any = {
  179. page: pagination.currentPage,
  180. pageSize: pagination.pageSize
  181. };
  182. if (form.name) searchParams.name = form.name;
  183. if (form.type !== undefined) searchParams.type = form.type;
  184. if (form.status !== undefined) searchParams.status = form.status;
  185. if (form.receiveType) searchParams.receiveType = form.receiveType;
  186. const { data } = await getCouponList(searchParams);
  187. if (data) {
  188. dataList.value = data.list || [];
  189. updatePaginationData(pagination, data);
  190. }
  191. } finally {
  192. loading.value = false;
  193. }
  194. }
  195. function resetForm(formEl) {
  196. if (!formEl) return;
  197. formEl.resetFields();
  198. resetPagination(pagination, onSearch);
  199. }
  200. async function handleDelete(row) {
  201. const { code } = await deleteCoupon(row.id);
  202. if (code === 200) {
  203. message("删除成功", { type: "success" });
  204. onSearch();
  205. }
  206. }
  207. async function handleToggleStatus(row) {
  208. const newStatus = row.status === 1 ? 0 : 1;
  209. const { code } = await updateCouponStatus(row.id, newStatus);
  210. if (code === 200) {
  211. message("状态更新成功", { type: "success" });
  212. onSearch();
  213. }
  214. }
  215. function handleDistribute(row) {
  216. const distributeForm = reactive({
  217. targetType: "all", // all-全部用户, specific-指定用户
  218. userIds: [],
  219. distributeCount: 1 // 发放数量
  220. });
  221. // 用户列表状态
  222. const userList = ref([]);
  223. const userLoading = ref(false);
  224. const userSearchKeyword = ref("");
  225. // 加载用户列表
  226. const loadUserList = async (keyword = "") => {
  227. userLoading.value = true;
  228. try {
  229. const { data } = await getUserList({
  230. page: 1,
  231. pageSize: 100,
  232. phone: keyword || undefined,
  233. nickname: keyword || undefined
  234. });
  235. if (data) {
  236. userList.value = data.list || [];
  237. }
  238. } catch (error) {
  239. console.error("加载用户列表失败:", error);
  240. message("加载用户列表失败", { type: "error" });
  241. } finally {
  242. userLoading.value = false;
  243. }
  244. };
  245. // 用户搜索
  246. const handleUserSearch = (query: string) => {
  247. userSearchKeyword.value = query;
  248. if (query) {
  249. loadUserList(query);
  250. } else {
  251. loadUserList();
  252. }
  253. };
  254. addDialog({
  255. title: `发放优惠券 - ${row.couponName || '未知优惠券'}`,
  256. width: "700px",
  257. draggable: true,
  258. fullscreen: deviceDetection(),
  259. contentRenderer: () => (
  260. <ElForm label-width="100px" size="default">
  261. <ElFormItem label="优惠券">
  262. <ElInput modelValue={row.couponName} disabled />
  263. </ElFormItem>
  264. <ElFormItem label="发放总量">
  265. <ElInput modelValue={`${row.receivedCount || 0} / ${row.totalCount || 0}`} disabled />
  266. </ElFormItem>
  267. <ElFormItem label="目标用户" required>
  268. <ElSelect
  269. v-model={distributeForm.targetType}
  270. placeholder="请选择目标用户"
  271. class="w-full"
  272. onChange={() => {
  273. // 切换时清空用户选择
  274. if (distributeForm.targetType === 'all') {
  275. distributeForm.userIds = [];
  276. }
  277. }}
  278. >
  279. <ElOption label="全部用户" value="all" />
  280. <ElOption label="指定用户" value="specific" />
  281. </ElSelect>
  282. </ElFormItem>
  283. {distributeForm.targetType === 'specific' && (
  284. <ElFormItem label="选择用户" required>
  285. {loadUserList()}
  286. <ElSelect
  287. v-model={distributeForm.userIds}
  288. class="w-full"
  289. placeholder="请输入用户名或手机号搜索"
  290. >
  291. {userList.value.map((user: any) => (
  292. <ElOption
  293. key={user.id}
  294. label={`${user.nickname || user.phone || '未知用户'} (${user.phone || '无手机号'})`}
  295. value={user.id}
  296. />
  297. ))}
  298. </ElSelect>
  299. <div style="font-size: 12px; color: #9ca3af; margin-top: 4px;">
  300. 已选择 {distributeForm.userIds.length} 个用户
  301. </div>
  302. </ElFormItem>
  303. )}
  304. <ElFormItem label="发放数量" required>
  305. <ElInputNumber
  306. v-model={distributeForm.distributeCount}
  307. placeholder="发放数量"
  308. class="w-full"
  309. min={1}
  310. max={row.totalCount - (row.receivedCount || 0)}
  311. />
  312. </ElFormItem>
  313. <ElAlert
  314. type="info"
  315. closable={false}
  316. showIcon={true}
  317. style={{marginTop: '8px'}}
  318. >
  319. {distributeForm.targetType === 'all'
  320. ? '将向平台所有用户发放该优惠券'
  321. : `将向已选择的 ${distributeForm.userIds.length} 个用户发放该优惠券`}
  322. </ElAlert>
  323. </ElForm>
  324. ),
  325. beforeSure: async done => {
  326. // 验证
  327. if (distributeForm.targetType === 'specific' && distributeForm.userIds.length === 0) {
  328. message("请选择至少一个用户", { type: "warning" });
  329. return;
  330. }
  331. try {
  332. // 调用后端发放接口
  333. const { code, message: errorMsg } = await distributeCoupon({
  334. templateId: row.id, // 现在后端返回的是字符串,无需转换
  335. targetType: distributeForm.targetType === 'all' ? 1 : 3, // 1-全部用户,3-指定用户
  336. userIds: distributeForm.targetType === 'specific' ? distributeForm.userIds : undefined
  337. });
  338. if (code === 200) {
  339. message("发放成功", { type: "success" });
  340. done();
  341. onSearch();
  342. } else {
  343. message(errorMsg || "发放失败", { type: "error" });
  344. }
  345. } catch (error) {
  346. const errorMsg = error?.response?.data?.message || "发放失败,请重试";
  347. message(errorMsg, { type: "error" });
  348. }
  349. }
  350. });
  351. }
  352. function handleViewRecords(row) {
  353. addDialog({
  354. title: "发放记录",
  355. width: "800px",
  356. draggable: true,
  357. fullscreen: deviceDetection(),
  358. contentRenderer: () => (
  359. <div>
  360. <ElTable data={[]} style="width: 100%">
  361. <ElTableColumn prop="userName" label="用户名" width={120} />
  362. <ElTableColumn prop="phone" label="手机号" width={120} />
  363. <ElTableColumn prop="distributeTime" label="发放时间" width={160} />
  364. <ElTableColumn prop="status" label="状态" width={100} />
  365. </ElTable>
  366. </div>
  367. )
  368. });
  369. }
  370. const formStyles = `
  371. .coupon-form {
  372. padding: 0;
  373. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  374. }
  375. .form-block {
  376. margin-bottom: 20px;
  377. }
  378. .block-title {
  379. font-size: 14px;
  380. font-weight: 600;
  381. color: #374151;
  382. margin-bottom: 14px;
  383. padding-bottom: 8px;
  384. border-bottom: 1px solid #e5e7eb;
  385. }
  386. .form-grid-2 {
  387. display: grid;
  388. grid-template-columns: repeat(2, 1fr);
  389. gap: 20px 24px;
  390. }
  391. .form-grid-3 {
  392. display: grid;
  393. grid-template-columns: repeat(3, 1fr);
  394. gap: 20px 24px;
  395. }
  396. .type-selector {
  397. display: grid;
  398. grid-template-columns: repeat(4, 1fr);
  399. gap: 12px;
  400. margin-bottom: 18px;
  401. }
  402. .type-btn {
  403. padding: 12px 16px;
  404. border: 1px solid #e5e7eb;
  405. border-radius: 6px;
  406. background: #fff;
  407. cursor: pointer;
  408. transition: all 0.15s ease;
  409. text-align: center;
  410. }
  411. .type-btn:hover {
  412. border-color: #3b82f6;
  413. background: #f8fafc;
  414. }
  415. .type-btn.active {
  416. border-color: #3b82f6;
  417. background: #eff6ff;
  418. }
  419. .type-btn .name {
  420. font-size: 14px;
  421. font-weight: 600;
  422. color: #374151;
  423. margin-bottom: 4px;
  424. }
  425. .type-btn .hint {
  426. font-size: 12px;
  427. color: #9ca3af;
  428. }
  429. .type-btn.active .name {
  430. color: #3b82f6;
  431. }
  432. .scope-grid {
  433. display: grid;
  434. grid-template-columns: repeat(3, 1fr);
  435. gap: 16px;
  436. }
  437. .scope-card {
  438. background: #f9fafb;
  439. border: 1px solid #e5e7eb;
  440. border-radius: 8px;
  441. padding: 14px 16px;
  442. }
  443. .scope-card .card-header {
  444. display: flex;
  445. align-items: center;
  446. justify-content: space-between;
  447. margin-bottom: 12px;
  448. }
  449. .scope-card .card-label {
  450. font-size: 13px;
  451. font-weight: 500;
  452. color: #374151;
  453. }
  454. .scope-card .card-body {
  455. min-height: 32px;
  456. }
  457. .coupon-form .el-form-item {
  458. margin-bottom: 0;
  459. align-items: center;
  460. }
  461. .coupon-form .el-form-item__label {
  462. font-size: 13px;
  463. color: #606266;
  464. padding-right: 8px;
  465. }
  466. .coupon-form .el-input__wrapper,
  467. .coupon-form .el-textarea__inner {
  468. font-size: 13px;
  469. }
  470. .coupon-form .form-label-90 .el-form-item__label {
  471. width: 90px !important;
  472. }
  473. .coupon-form .form-label-100 .el-form-item__label {
  474. width: 100px !important;
  475. }
  476. .preview-box {
  477. display: flex;
  478. align-items: center;
  479. justify-content: center;
  480. gap: 8px;
  481. padding: 12px 16px;
  482. background: #f9fafb;
  483. border-radius: 4px;
  484. margin-top: 12px;
  485. font-size: 14px;
  486. color: #374151;
  487. }
  488. .preview-box .value {
  489. font-weight: 600;
  490. }
  491. @media (max-width: 1200px) {
  492. .form-grid-3,
  493. .scope-grid {
  494. grid-template-columns: repeat(2, 1fr);
  495. }
  496. .type-selector {
  497. grid-template-columns: repeat(2, 1fr);
  498. }
  499. }
  500. @media (max-width: 768px) {
  501. .form-grid-2,
  502. .form-grid-3,
  503. .scope-grid {
  504. grid-template-columns: 1fr;
  505. }
  506. .type-selector {
  507. grid-template-columns: 1fr;
  508. }
  509. }
  510. `;
  511. function renderForm(formData: CouponFormData) {
  512. return (
  513. <div class="coupon-form">
  514. <style>{formStyles}</style>
  515. <div class="form-block">
  516. <div class="block-title">基本信息</div>
  517. <ElForm label-width="90px" size="default">
  518. <div class="form-grid-2">
  519. <ElFormItem label="优惠券名称" required>
  520. <ElInput v-model={formData.name} placeholder="请输入优惠券名称" clearable maxlength={50} showWordLimit />
  521. </ElFormItem>
  522. <ElFormItem label="关联活动">
  523. <ElSelect v-model={formData.activityId} placeholder="请选择关联的营销活动(可选)" class="w-full" clearable filterable>
  524. <ElOption label="春节大促活动" value="1" />
  525. <ElOption label="新品上市活动" value="2" />
  526. <ElOption label="会员专享活动" value="3" />
  527. <ElOption label="不关联活动" value="" />
  528. </ElSelect>
  529. </ElFormItem>
  530. <ElFormItem label="发放类型" required>
  531. <ElRadioGroup v-model={formData.receiveType}>
  532. <ElRadioButton value="Release">系统发放</ElRadioButton>
  533. <ElRadioButton value="Collect">主动领取</ElRadioButton>
  534. </ElRadioGroup>
  535. </ElFormItem>
  536. </div>
  537. <ElAlert
  538. type="info"
  539. closable={false}
  540. showIcon={true}
  541. style={{marginTop: '12px'}}
  542. >
  543. {formData.receiveType === 'Collect'
  544. ? '主动领取:将显示在用户端小程序领券中心,用户可自行领取'
  545. : '系统发放:创建后可在列表中点击发放,选择指定用户或全部用户'}
  546. </ElAlert>
  547. </ElForm>
  548. </div>
  549. <div class="form-block">
  550. <div class="block-title">优惠券类型</div>
  551. <div class="type-selector">
  552. {[1, 2, 3, 4].map(type => (
  553. <div
  554. class={`type-btn ${formData.type === type ? 'active' : ''}`}
  555. onClick={() => { formData.type = type; }}
  556. >
  557. <div class="name">{typeMap[type].text}</div>
  558. <div class="hint">
  559. {type === 1 && "满额减免"}
  560. {type === 2 && "折扣优惠"}
  561. {type === 3 && "直接抵扣"}
  562. {type === 4 && "免费兑换"}
  563. </div>
  564. </div>
  565. ))}
  566. </div>
  567. {formData.type && (
  568. <ElForm label-width="90px" size="default">
  569. <div class="form-grid-3">
  570. <ElFormItem label="优惠值" required>
  571. <ElInputNumber
  572. v-model={formData.value}
  573. min={0}
  574. precision={2}
  575. class="w-full!"
  576. placeholder={formData.type === 2 ? "折扣值如8表示8折" : "优惠金额"}
  577. {...(formData.type !== 2 ? { "data-prefix": "¥" as any } : {})}
  578. />
  579. </ElFormItem>
  580. <ElFormItem label="发放总量" required>
  581. <ElInputNumber
  582. v-model={formData.totalCount}
  583. min={1}
  584. class="w-full!"
  585. placeholder="发放总数量"
  586. />
  587. </ElFormItem>
  588. <ElFormItem label="有效期" required>
  589. <ElDatePicker
  590. v-model={formData.validPeriod}
  591. type="daterange"
  592. range-separator="至"
  593. start-placeholder="开始日期"
  594. end-placeholder="结束日期"
  595. class="w-full!"
  596. />
  597. </ElFormItem>
  598. </div>
  599. </ElForm>
  600. )}
  601. </div>
  602. <div class="form-block">
  603. <div class="block-title">适用范围</div>
  604. <div class="scope-grid">
  605. <div class="scope-card">
  606. <div class="card-header">
  607. <span class="card-label">门店范围</span>
  608. <ElRadioGroup v-model={formData.applyScope} size="small">
  609. <ElRadioButton value={1}>全部</ElRadioButton>
  610. <ElRadioButton value={2}>指定</ElRadioButton>
  611. </ElRadioGroup>
  612. </div>
  613. <div class="card-body">
  614. {formData.applyScope === 2 && <ShopSelector v-model={formData.shopIds} />}
  615. </div>
  616. </div>
  617. <div class="scope-card">
  618. <div class="card-header">
  619. <span class="card-label">设备范围</span>
  620. <ElRadioGroup v-model={formData.deviceScope} size="small">
  621. <ElRadioButton value={1}>全部</ElRadioButton>
  622. <ElRadioButton value={2}>指定</ElRadioButton>
  623. </ElRadioGroup>
  624. </div>
  625. <div class="card-body">
  626. {formData.deviceScope === 2 && <DeviceSelector v-model={formData.deviceIds} />}
  627. </div>
  628. </div>
  629. <div class="scope-card">
  630. <div class="card-header">
  631. <span class="card-label">商品范围</span>
  632. <ElRadioGroup v-model={formData.productScope} size="small">
  633. <ElRadioButton value={1}>全部</ElRadioButton>
  634. <ElRadioButton value={2}>指定</ElRadioButton>
  635. </ElRadioGroup>
  636. </div>
  637. <div class="card-body">
  638. {formData.productScope === 2 && <ProductSelector v-model={formData.productIds} />}
  639. </div>
  640. </div>
  641. </div>
  642. </div>
  643. <div class="form-block">
  644. <div class="block-title">使用说明</div>
  645. <ElForm size="default">
  646. <ElFormItem label="">
  647. <ElInput v-model={formData.description} type="textarea" rows={2} placeholder="请输入使用说明(可选)" maxlength={500} showWordLimit />
  648. </ElFormItem>
  649. </ElForm>
  650. </div>
  651. </div>
  652. );
  653. }
  654. function validateForm(formData: CouponFormData): boolean {
  655. if (!formData.name) {
  656. message("请输入优惠券名称", { type: "warning" });
  657. return false;
  658. }
  659. if (!formData.type) {
  660. message("请选择优惠券类型", { type: "warning" });
  661. return false;
  662. }
  663. if (!formData.value || formData.value <= 0) {
  664. message("请输入有效的优惠值", { type: "warning" });
  665. return false;
  666. }
  667. if (!formData.totalCount || formData.totalCount <= 0) {
  668. message("请输入发放总量", { type: "warning" });
  669. return false;
  670. }
  671. return true;
  672. }
  673. function handleAdd() {
  674. const formData = reactive<CouponFormData>({
  675. name: "",
  676. activityId: "",
  677. receiveType: "Release", // 默认为系统发放
  678. type: undefined,
  679. value: null,
  680. totalCount: null,
  681. shopIds: [],
  682. deviceIds: [],
  683. productIds: [],
  684. validPeriod: [],
  685. description: "",
  686. applyScope: 1,
  687. deviceScope: 1,
  688. productScope: 1
  689. });
  690. addDialog({
  691. title: "新增优惠券",
  692. width: "1100px",
  693. draggable: true,
  694. fullscreen: deviceDetection(),
  695. contentRenderer: () => renderForm(formData),
  696. beforeSure: async (done) => {
  697. if (!validateForm(formData)) return;
  698. try {
  699. // 将前端表单数据转换为后端 DTO 格式
  700. const submitData = {
  701. couponName: formData.name,
  702. couponType: formData.type,
  703. receiveType: formData.receiveType,
  704. discountValue: formData.value,
  705. totalCount: formData.totalCount,
  706. validType: 1, // 固定时间
  707. validStartTime: formData.validPeriod?.[0],
  708. validEndTime: formData.validPeriod?.[1],
  709. applyScope: formData.applyScope,
  710. productScope: formData.productScope,
  711. activityId: formData.activityId ? Number(formData.activityId) : null,
  712. shopIds: formData.shopIds,
  713. productIds: formData.productIds,
  714. couponDesc: formData.description,
  715. receiveLimit: 1
  716. };
  717. const { code, message: errorMsg } = await createCoupon(submitData);
  718. if (code === 200) {
  719. message("新增成功", { type: "success" });
  720. done();
  721. onSearch();
  722. } else {
  723. message(errorMsg || "新增失败", { type: "error" });
  724. }
  725. } catch (error: any) {
  726. // 展示后端返回的具体错误信息
  727. const errorMsg = error?.response?.data?.message || error?.message || "新增失败,请重试";
  728. message(errorMsg, { type: "error" });
  729. }
  730. }
  731. });
  732. }
  733. function handleSizeChange(val: number) {
  734. handlePageSizeChange(val, pagination, onSearch);
  735. }
  736. function handleCurrentChange(val: number) {
  737. handleCurrentPageChange(val, pagination, onSearch);
  738. }
  739. onMounted(() => {
  740. onSearch();
  741. });
  742. return {
  743. form,
  744. loading,
  745. columns,
  746. dataList,
  747. pagination,
  748. typeMap,
  749. statusMap,
  750. onSearch,
  751. resetForm,
  752. handleAdd,
  753. handleDelete,
  754. handleToggleStatus,
  755. handleDistribute,
  756. handleViewRecords,
  757. handleSizeChange,
  758. handleCurrentChange
  759. };
  760. }