index.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. <template>
  2. <div class="app-container coupon-user-page">
  3. <el-card shadow="never" class="search-card">
  4. <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="88px" size="small">
  5. <el-form-item label="会员名称" prop="nickname">
  6. <el-input
  7. v-model="queryParams.nickname"
  8. placeholder="请输入会员名称"
  9. clearable
  10. style="width: 160px"
  11. @keyup.enter.native="handleQuery"
  12. />
  13. </el-form-item>
  14. <el-form-item label="手机号" prop="phone">
  15. <el-input
  16. v-model="queryParams.phone"
  17. placeholder="请输入手机号"
  18. clearable
  19. style="width: 160px"
  20. @keyup.enter.native="handleQuery"
  21. />
  22. </el-form-item>
  23. <el-form-item label="优惠券" prop="couponTitle">
  24. <el-input
  25. v-model="queryParams.couponTitle"
  26. placeholder="请输入优惠券名称"
  27. clearable
  28. style="width: 180px"
  29. @keyup.enter.native="handleQuery"
  30. />
  31. </el-form-item>
  32. <el-form-item label="优惠券类型" prop="couponType">
  33. <el-select v-model="queryParams.couponType" placeholder="全部类型" clearable style="width: 140px">
  34. <el-option
  35. v-for="item in couponTypeOptions"
  36. :key="item.dictValue"
  37. :label="item.dictLabel"
  38. :value="item.dictValue"
  39. />
  40. </el-select>
  41. </el-form-item>
  42. <el-form-item label="状态" prop="status">
  43. <el-select v-model="queryParams.status" placeholder="全部状态" clearable style="width: 150px">
  44. <el-option
  45. v-for="item in statusFilterOptions"
  46. :key="item.value"
  47. :label="item.label"
  48. :value="item.value"
  49. />
  50. </el-select>
  51. </el-form-item>
  52. <el-form-item label="领取时间">
  53. <el-date-picker
  54. v-model="dateRange"
  55. style="width: 240px"
  56. value-format="yyyy-MM-dd"
  57. type="daterange"
  58. range-separator="-"
  59. start-placeholder="开始日期"
  60. end-placeholder="结束日期"
  61. />
  62. </el-form-item>
  63. <el-form-item>
  64. <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
  65. <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
  66. </el-form-item>
  67. </el-form>
  68. </el-card>
  69. <el-card shadow="never" class="table-card">
  70. <div class="toolbar">
  71. <div class="toolbar-left">
  72. <el-button
  73. type="warning"
  74. plain
  75. icon="el-icon-download"
  76. size="mini"
  77. @click="handleExport"
  78. v-hasPermi="['store:storeCouponUser:export', 'live:issue:export']"
  79. >导出</el-button>
  80. <span class="toolbar-tip">普通券状态为待使用/已使用;核销券为未核销/已核销</span>
  81. </div>
  82. <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
  83. </div>
  84. <el-table
  85. height="520"
  86. border
  87. size="mini"
  88. v-loading="loading"
  89. :data="storeCouponUserList"
  90. class="coupon-table"
  91. >
  92. <el-table-column label="会员" min-width="130" show-overflow-tooltip>
  93. <template slot-scope="scope">
  94. <div class="cell-main">{{ scope.row.nickname || '-' }}</div>
  95. <div class="cell-sub">{{ scope.row.phone || '-' }}</div>
  96. </template>
  97. </el-table-column>
  98. <el-table-column label="优惠券" min-width="160" show-overflow-tooltip>
  99. <template slot-scope="scope">
  100. <div class="cell-main">{{ scope.row.couponTitle || '-' }}</div>
  101. <div class="cell-sub">面值 ¥{{ scope.row.couponPrice }} · 满 ¥{{ scope.row.useMinPrice }}</div>
  102. </template>
  103. </el-table-column>
  104. <el-table-column label="类型" width="100" align="center">
  105. <template slot-scope="scope">
  106. <el-tag size="mini" :type="couponTypeTagType(scope.row.couponType)" effect="plain">
  107. {{ couponTypeLabel(scope.row.couponType) }}
  108. </el-tag>
  109. </template>
  110. </el-table-column>
  111. <el-table-column label="领取 / 有效期" min-width="160">
  112. <template slot-scope="scope">
  113. <div class="cell-sub">领 {{ formatTime(scope.row.createTime) }}</div>
  114. <div class="cell-sub">止 {{ formatTime(scope.row.limitTime) }}</div>
  115. </template>
  116. </el-table-column>
  117. <el-table-column label="使用信息" min-width="150">
  118. <template slot-scope="scope">
  119. <template v-if="isVerifyCouponType(scope.row.couponType)">
  120. <div class="cell-main">{{ scope.row.verifyUserName || '-' }}</div>
  121. <div class="cell-sub">{{ formatTime(scope.row.verifyTime || scope.row.useTime) }}</div>
  122. </template>
  123. <template v-else>
  124. <div class="cell-main cell-muted">普通券不可核销</div>
  125. <div class="cell-sub">{{ formatTime(scope.row.useTime) !== '-' ? ('使用 ' + formatTime(scope.row.useTime)) : '-' }}</div>
  126. </template>
  127. </template>
  128. </el-table-column>
  129. <el-table-column label="状态" width="96" align="center">
  130. <template slot-scope="scope">
  131. <el-tag size="mini" :type="statusTagType(scope.row.status)">
  132. {{ rowStatusLabel(scope.row) }}
  133. </el-tag>
  134. </template>
  135. </el-table-column>
  136. </el-table>
  137. <pagination
  138. v-show="total > 0"
  139. :total="total"
  140. :page.sync="queryParams.pageNum"
  141. :limit.sync="queryParams.pageSize"
  142. @pagination="getList"
  143. />
  144. </el-card>
  145. </div>
  146. </template>
  147. <script>
  148. import axios from "axios";
  149. import { getToken } from "@/utils/auth";
  150. import { listStoreCouponUser, exportStoreCouponUser } from "@/api/live/liveCouponUser";
  151. export default {
  152. name: "StoreCouponUser",
  153. data() {
  154. return {
  155. statusOptions: [],
  156. couponTypeOptions: [],
  157. loading: true,
  158. dateRange: [],
  159. showSearch: true,
  160. total: 0,
  161. storeCouponUserList: [],
  162. queryParams: {
  163. pageNum: 1,
  164. pageSize: 10,
  165. couponId: null,
  166. userId: null,
  167. nickname: null,
  168. phone: null,
  169. couponTitle: null,
  170. couponPrice: null,
  171. useMinPrice: null,
  172. endTime: null,
  173. useTime: null,
  174. type: null,
  175. couponType: null,
  176. status: null,
  177. isFail: null,
  178. isDel: null
  179. },
  180. // 筛选用中性文案(同一 status 码,展示因券类型不同)
  181. statusFilterOptions: [
  182. { value: "0", label: "待使用 / 未核销" },
  183. { value: "1", label: "已使用 / 已核销" },
  184. { value: "2", label: "已过期" }
  185. ]
  186. };
  187. },
  188. created() {
  189. this.getDicts("live_coupon_user_status").then((response) => {
  190. this.statusOptions = response.data || [];
  191. });
  192. this.getDicts("store_coupon_type").then((response) => {
  193. this.couponTypeOptions = response.data || [];
  194. });
  195. this.getList();
  196. },
  197. methods: {
  198. couponTypeLabel(type) {
  199. if (type === null || type === undefined || type === "") {
  200. return "-";
  201. }
  202. const hit = (this.couponTypeOptions || []).find(item => String(item.dictValue) === String(type));
  203. return hit ? hit.dictLabel : `类型${type}`;
  204. },
  205. isVerifyCouponType(type) {
  206. const label = this.couponTypeLabel(type) || "";
  207. return label.includes("核销") || label.includes("代金券");
  208. },
  209. couponTypeTagType(type) {
  210. return this.isVerifyCouponType(type) ? "warning" : "";
  211. },
  212. /** 按券类型展示状态:普通券=待使用/已使用;核销券=未核销/已核销 */
  213. rowStatusLabel(row) {
  214. const status = row == null ? null : row.status;
  215. if (status === null || status === undefined || status === "") {
  216. return "-";
  217. }
  218. const code = Number(status);
  219. if (this.isVerifyCouponType(row.couponType)) {
  220. const map = { 0: "未核销", 1: "已核销", 2: "已过期" };
  221. return map[code] != null ? map[code] : String(status);
  222. }
  223. const map = { 0: "待使用", 1: "已使用", 2: "已过期" };
  224. return map[code] != null ? map[code] : String(status);
  225. },
  226. statusTagType(status) {
  227. const map = { 0: "info", 1: "success", 2: "danger" };
  228. return map[Number(status)] || "info";
  229. },
  230. formatTime(val) {
  231. if (!val) {
  232. return "-";
  233. }
  234. const text = String(val).replace("T", " ");
  235. return text.length >= 16 ? text.substring(0, 16) : text;
  236. },
  237. getList() {
  238. this.loading = true;
  239. listStoreCouponUser(this.addDateRange(this.queryParams, this.dateRange)).then(response => {
  240. this.storeCouponUserList = response.rows || [];
  241. this.total = response.total || 0;
  242. this.loading = false;
  243. }).catch(() => {
  244. this.loading = false;
  245. });
  246. },
  247. handleQuery() {
  248. this.queryParams.pageNum = 1;
  249. this.getList();
  250. },
  251. resetQuery() {
  252. this.dateRange = [];
  253. this.resetForm("queryForm");
  254. this.handleQuery();
  255. },
  256. handleExport() {
  257. const queryParams = this.addDateRange({ ...this.queryParams }, this.dateRange);
  258. this.$confirm("是否确认导出所有优惠券发放记录数据项?", "警告", {
  259. confirmButtonText: "确定",
  260. cancelButtonText: "取消",
  261. type: "warning"
  262. }).then(() => {
  263. return exportStoreCouponUser(queryParams);
  264. }).then(response => {
  265. return this.downloadFileWithAuth(response.msg);
  266. }).catch(() => {});
  267. },
  268. downloadFileWithAuth(fileName) {
  269. if (!fileName) {
  270. this.msgError("导出失败,未获取到文件名");
  271. return Promise.reject("empty fileName");
  272. }
  273. return axios({
  274. method: "get",
  275. url: process.env.VUE_APP_BASE_API + "/common/download",
  276. params: { fileName: fileName, delete: false },
  277. responseType: "blob",
  278. headers: {
  279. Authorization: "Bearer " + getToken(),
  280. "X-Frontend-Type": "admin"
  281. }
  282. }).then(res => {
  283. const disposition = res.headers["content-disposition"] || "";
  284. let downloadName = fileName.indexOf("_") > -1 ? fileName.substring(fileName.indexOf("_") + 1) : fileName;
  285. const match = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(disposition);
  286. if (match && match[1]) {
  287. downloadName = decodeURIComponent(match[1].replace(/['"]/g, ""));
  288. }
  289. const blob = new Blob([res.data]);
  290. const link = document.createElement("a");
  291. link.href = window.URL.createObjectURL(blob);
  292. link.setAttribute("download", downloadName);
  293. document.body.appendChild(link);
  294. link.click();
  295. document.body.removeChild(link);
  296. window.URL.revokeObjectURL(link.href);
  297. });
  298. }
  299. }
  300. };
  301. </script>
  302. <style scoped>
  303. .coupon-user-page {
  304. padding-bottom: 12px;
  305. }
  306. .search-card {
  307. margin-bottom: 12px;
  308. border-radius: 8px;
  309. }
  310. .search-card >>> .el-card__body {
  311. padding: 14px 16px 2px;
  312. }
  313. .table-card {
  314. border-radius: 8px;
  315. }
  316. .table-card >>> .el-card__body {
  317. padding: 12px 16px 8px;
  318. }
  319. .toolbar {
  320. display: flex;
  321. align-items: center;
  322. justify-content: space-between;
  323. margin-bottom: 12px;
  324. }
  325. .toolbar-left {
  326. display: flex;
  327. align-items: center;
  328. gap: 12px;
  329. }
  330. .toolbar-tip {
  331. font-size: 12px;
  332. color: #909399;
  333. }
  334. .coupon-table {
  335. width: 100%;
  336. }
  337. .cell-main {
  338. line-height: 18px;
  339. color: #303133;
  340. font-weight: 500;
  341. }
  342. .cell-muted {
  343. color: #909399;
  344. font-weight: 400;
  345. }
  346. .cell-sub {
  347. line-height: 18px;
  348. font-size: 12px;
  349. color: #909399;
  350. margin-top: 2px;
  351. }
  352. </style>