index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599
  1. <template>
  2. <div class="app-container h-full flex flex-1 flex-col">
  3. <!-- 搜索 -->
  4. <page-search
  5. ref="searchRef"
  6. :search-config="searchConfig"
  7. @query-click="handleQueryClick"
  8. @reset-click="handleResetClick"
  9. ></page-search>
  10. <!-- 列表 -->
  11. <page-content
  12. ref="contentRef"
  13. :content-config="contentConfig"
  14. @add-click="handleAddClick"
  15. @export-click="handleExportClick"
  16. @search-click="handleSearchClick"
  17. @toolbar-click="handleToolbarClick"
  18. @operate-click="handleOperateClick"
  19. @filter-change="handleFilterChange"
  20. @sort-change="handleSortChange"
  21. >
  22. <template #balance="scope">
  23. <span class="money-text primary">¥ {{ scope.row.balance }}</span>
  24. </template>
  25. <template #totalConsumption="scope">
  26. <span class="money-text warning">¥ {{ formatMoney(scope.row.totalConsumption) }}</span>
  27. </template>
  28. </page-content>
  29. <!-- 详情 -->
  30. <page-modal
  31. ref="editModalRef"
  32. :modal-config="editModalConfig"
  33. @submit-click="handleSubmitClick"
  34. ></page-modal>
  35. <!-- 退款记录弹窗 -->
  36. <el-dialog
  37. v-model="refundRecordDialogVisible"
  38. title="退款记录"
  39. width="80%"
  40. :close-on-click-modal="false"
  41. >
  42. <div class="refund-record-container">
  43. <!-- 搜索区域 -->
  44. <el-form :inline="true" :model="refundQueryParams" class="search-form">
  45. <el-form-item label="创建时间">
  46. <el-date-picker
  47. v-model="refundDateRange"
  48. type="daterange"
  49. range-separator="至"
  50. start-placeholder="开始日期"
  51. end-placeholder="结束日期"
  52. value-format="YYYY-MM-DD HH:mm:ss"
  53. :default-time="[new Date(0, 0, 0, 0, 0, 0), new Date(0, 0, 0, 23, 59, 59)]"
  54. @change="handleRefundDateChange"
  55. />
  56. </el-form-item>
  57. <el-form-item>
  58. <el-button type="primary" @click="handleRefundQuery">查询</el-button>
  59. <el-button @click="handleRefundReset">重置</el-button>
  60. </el-form-item>
  61. </el-form>
  62. <!-- 表格 -->
  63. <el-table
  64. v-loading="refundTableLoading"
  65. :data="refundTableData"
  66. border
  67. style="width: 100%"
  68. >
  69. <el-table-column prop="orderId" label="订单ID" width="100" />
  70. <el-table-column prop="orderNo" label="商户订单号" width="180" />
  71. <el-table-column prop="type" label="退款类型" width="120">
  72. <template #default="{ row }">
  73. <el-tag :type="row.type === 1 ? 'warning' : 'info'">
  74. {{ row.type === 1 ? "主动退款" : "用户申请退款" }}
  75. </el-tag>
  76. </template>
  77. </el-table-column>
  78. <el-table-column prop="outRefundNo" label="商户退款单号" width="180" />
  79. <el-table-column prop="refundId" label="微信支付退款单号" width="200" />
  80. <el-table-column prop="transactionId" label="微信支付订单号" width="200" />
  81. <el-table-column prop="reason" label="退款原因" width="150" show-overflow-tooltip />
  82. <el-table-column prop="acceptedTime" label="退款受理时间" width="180" />
  83. <el-table-column prop="successTime" label="退款成功时间" width="180" />
  84. <el-table-column prop="amount" label="退款金额(元)" width="130">
  85. <template #default="{ row }">
  86. <span class="money-text danger">¥ {{ formatMoney(row.amount) }}</span>
  87. </template>
  88. </el-table-column>
  89. <el-table-column prop="status" label="退款状态" width="120" />
  90. <el-table-column prop="createTime" label="创建时间" width="180" />
  91. <el-table-column label="操作" width="100">
  92. <template #default="{ row }">
  93. <el-button type="info" link @click="handleRefundRecordClick(row)">退款日志</el-button>
  94. </template>
  95. </el-table-column>
  96. </el-table>
  97. <!-- 分页 -->
  98. <el-pagination
  99. v-model:current-page="refundQueryParams.pageNum"
  100. v-model:page-size="refundQueryParams.pageSize"
  101. :total="refundTotal"
  102. :page-sizes="[10, 20, 30, 50]"
  103. layout="total, sizes, prev, pager, next, jumper"
  104. background
  105. @size-change="handleRefundSizeChange"
  106. @current-change="handleRefundCurrentChange"
  107. />
  108. </div>
  109. </el-dialog>
  110. <!-- 导出日期区间弹窗 -->
  111. <el-dialog
  112. v-model="exportDateDialogVisible"
  113. title="退款导出"
  114. width="420px"
  115. :close-on-click-modal="false"
  116. :align-center="true"
  117. >
  118. <el-form label-width="90px" style="padding: 8px 16px 0">
  119. <el-form-item label="区间选择:">
  120. <el-date-picker
  121. v-model="exportDateRange"
  122. type="daterange"
  123. range-separator="至"
  124. start-placeholder="开始日期"
  125. end-placeholder="结束日期"
  126. value-format="YYYY-MM-DD"
  127. style="width: 100%"
  128. />
  129. </el-form-item>
  130. </el-form>
  131. <template #footer>
  132. <el-button @click="exportDateDialogVisible = false">取 消</el-button>
  133. <el-button type="primary" :loading="exportBtnLoading" @click="handleExportConfirm">
  134. 确 定
  135. </el-button>
  136. </template>
  137. </el-dialog>
  138. <!-- 退款日志抽屉 -->
  139. <el-dialog
  140. v-model="refundLogDrawerVisible"
  141. title="退款日志"
  142. :size="200"
  143. :close-on-click-modal="false"
  144. >
  145. <el-form label-width="120px">
  146. <el-form-item label="通知请求">
  147. <el-input
  148. v-model="currentNotifyRequest"
  149. type="textarea"
  150. :rows="20"
  151. readonly
  152. placeholder="暂无数据"
  153. />
  154. </el-form-item>
  155. </el-form>
  156. </el-dialog>
  157. </div>
  158. </template>
  159. <script setup lang="ts">
  160. import UserOrderInfoAPI from "@/api/orderManage/user-order-info-api";
  161. defineOptions({ name: "UserInfo" });
  162. import UserInfoAPI, {
  163. UserInfoForm,
  164. UserInfoPageQuery,
  165. UserRefundOrderPageQuery,
  166. UserRefundOrderPageVO,
  167. } from "@/api/userManage/user-info-api";
  168. import UserFirmAPI from "@/api/toBManage/user-firm-api";
  169. import type { IObject, IModalConfig, IContentConfig, ISearchConfig } from "@/components/CURD/types";
  170. import usePage from "@/components/CURD/usePage";
  171. // 组合式 CRUD
  172. const {
  173. searchRef,
  174. contentRef,
  175. editModalRef,
  176. handleQueryClick,
  177. handleResetClick,
  178. handleAddClick,
  179. handleEditClick,
  180. handleSubmitClick,
  181. handleSearchClick,
  182. handleFilterChange,
  183. } = usePage();
  184. // 金额格式化函数
  185. const formatMoney = (value: number | undefined | null): string => {
  186. if (value === null || value === undefined) return "0.00";
  187. return Math.abs(value).toLocaleString("zh-CN", {
  188. minimumFractionDigits: 2,
  189. maximumFractionDigits: 2,
  190. });
  191. };
  192. // 搜索配置
  193. const searchConfig: ISearchConfig = reactive({
  194. permPrefix: "business:user-info",
  195. formItems: [
  196. {
  197. type: "input",
  198. label: "手机号",
  199. prop: "phone",
  200. attrs: {
  201. placeholder: "手机号",
  202. clearable: true,
  203. style: { width: "200px" },
  204. },
  205. },
  206. {
  207. type: "select",
  208. label: "所属企业",
  209. prop: "firmId",
  210. attrs: {
  211. placeholder: "请选择企业",
  212. clearable: true,
  213. filterable: true,
  214. style: { width: "200px" },
  215. },
  216. options: [],
  217. async initFn(formItem) {
  218. try {
  219. const response = await UserFirmAPI.getFirmList();
  220. if (Array.isArray(response)) {
  221. formItem.options = response.map((item: any) => ({
  222. label: item.name,
  223. value: item.id,
  224. }));
  225. }
  226. } catch (error) {
  227. console.error("Failed to fetch firm list:", error);
  228. }
  229. },
  230. },
  231. ],
  232. });
  233. // 列表配置
  234. const contentConfig: IContentConfig<UserInfoPageQuery> = reactive({
  235. // 权限前缀
  236. permPrefix: "business:user-info",
  237. table: {
  238. border: true,
  239. highlightCurrentRow: true,
  240. },
  241. // 主键
  242. pk: "id",
  243. // 列表查询接口
  244. indexAction: UserInfoAPI.getPage,
  245. //退款导出
  246. exportAction: UserInfoAPI.exportExcel,
  247. // 数据解析函数
  248. parseData(res: any) {
  249. return {
  250. total: res.total,
  251. list: res.list,
  252. };
  253. },
  254. // 分页配置
  255. pagination: {
  256. background: true,
  257. layout: "total, sizes, prev, pager, next, jumper",
  258. pageSize: 20,
  259. pageSizes: [10, 20, 30, 50],
  260. },
  261. // 工具栏配置
  262. toolbar: ["export"],
  263. defaultToolbar: ["refresh", "filter"],
  264. // 表格列配置
  265. cols: [
  266. { type: "selection", width: 50, align: "center" },
  267. { label: "用户ID", prop: "id", width: 100 },
  268. { label: "手机号", prop: "phone" },
  269. { label: "昵称", prop: "nickName" },
  270. {
  271. label: "当前余额",
  272. prop: "balance",
  273. sortable: "custom",
  274. templet: "custom",
  275. slotName: "balance",
  276. },
  277. {
  278. label: "累计消费",
  279. prop: "totalConsumption",
  280. sortable: "custom",
  281. templet: "custom",
  282. slotName: "totalConsumption",
  283. },
  284. { label: "创建时间", prop: "createTime" },
  285. { label: "所属企业", prop: "firmName" },
  286. {
  287. label: "操作",
  288. prop: "operation",
  289. templet: "tool",
  290. operat: [
  291. {
  292. name: "detail",
  293. text: "详情",
  294. attrs: {
  295. type: "primary",
  296. icon: "view",
  297. link: true,
  298. size: "small",
  299. },
  300. },
  301. {
  302. name: "refundRecord",
  303. text: "退款记录",
  304. attrs: {
  305. type: "info",
  306. link: true,
  307. size: "small",
  308. },
  309. },
  310. {
  311. name: "refund",
  312. text: "退款",
  313. attrs: {
  314. type: "danger",
  315. size: "small",
  316. },
  317. render: (row: any) => row.balance > 0,
  318. },
  319. ],
  320. },
  321. ],
  322. });
  323. // 详情配置
  324. const editModalConfig: IModalConfig<UserInfoForm> = reactive({
  325. permPrefix: "business:user-info",
  326. component: "drawer",
  327. drawer: {
  328. title: "用户详情",
  329. size: 500,
  330. },
  331. pk: "id",
  332. formItems: [
  333. {
  334. type: "input",
  335. attrs: {
  336. placeholder: "用户ID",
  337. disabled: true,
  338. },
  339. label: "用户ID",
  340. prop: "id",
  341. },
  342. {
  343. type: "input",
  344. attrs: {
  345. placeholder: "手机号",
  346. disabled: true,
  347. },
  348. label: "手机号",
  349. prop: "phone",
  350. },
  351. {
  352. type: "input",
  353. attrs: {
  354. placeholder: "昵称",
  355. disabled: true,
  356. },
  357. label: "昵称",
  358. prop: "nickName",
  359. },
  360. {
  361. type: "input",
  362. attrs: {
  363. placeholder: "微信openid",
  364. disabled: true,
  365. },
  366. label: "微信openid",
  367. prop: "openid",
  368. },
  369. ],
  370. });
  371. // 处理操作按钮点击
  372. const handleOperateClick = (data: IObject) => {
  373. if (data.name === "detail") {
  374. handleEditClick(data.row, async () => {
  375. return await UserInfoAPI.getFormData(data.row.id);
  376. });
  377. } else if (data.name === "refundRecord") {
  378. // 打开退款记录弹窗
  379. openRefundRecordDialog(data.row);
  380. } else if (data.name === "refund") {
  381. ElMessageBox.confirm("确认要为该用户执行退款操作吗?", "退款确认", {
  382. confirmButtonText: "确认",
  383. cancelButtonText: "取消",
  384. type: "warning",
  385. beforeClose: async (action, instance, done) => {
  386. if (action === "confirm") {
  387. instance.confirmButtonLoading = true;
  388. instance.confirmButtonText = "退款中...";
  389. try {
  390. await UserInfoAPI.refund(data.row.id);
  391. ElMessage.success("退款成功");
  392. done();
  393. contentRef.value?.fetchPageData();
  394. } catch (error) {
  395. instance.confirmButtonLoading = false;
  396. instance.confirmButtonText = "确认";
  397. }
  398. } else {
  399. done();
  400. }
  401. },
  402. });
  403. }
  404. };
  405. // ==================== 导出日期区间弹窗相关 ====================
  406. const exportDateDialogVisible = ref(false);
  407. const exportDateRange = ref<[string, string] | null>(null);
  408. const exportBtnLoading = ref(false);
  409. // 覆盖 usePage 默认的 handleExportClick,先弹窗选择日期
  410. const handleExportClick = () => {
  411. exportDateRange.value = null;
  412. exportDateDialogVisible.value = true;
  413. };
  414. // 确认导出
  415. const handleExportConfirm = () => {
  416. const queryParams = searchRef.value?.getQueryParams() ?? {};
  417. const filteredQuery: Record<string, any> = {};
  418. for (const key in queryParams) {
  419. const value = queryParams[key];
  420. if (value !== "" && value !== null && value !== undefined) {
  421. filteredQuery[key] = value;
  422. }
  423. }
  424. if (exportDateRange.value && exportDateRange.value.length === 2) {
  425. filteredQuery.startTime = `${exportDateRange.value[0]} 00:00:00`;
  426. filteredQuery.endTime = `${exportDateRange.value[1]} 23:59:59`;
  427. }
  428. exportBtnLoading.value = true;
  429. contentRef.value?.exportPageData(filteredQuery);
  430. // exportPageData 内部异步处理,此处延迟关闭弹窗
  431. setTimeout(() => {
  432. exportBtnLoading.value = false;
  433. exportDateDialogVisible.value = false;
  434. }, 1500);
  435. };
  436. // 处理工具栏按钮点击
  437. const handleToolbarClick = (name: string) => {
  438. console.log(name);
  439. };
  440. // 排序参数
  441. const sortParams = reactive({
  442. sortField: "",
  443. sortOrder: "",
  444. });
  445. // 处理排序变化
  446. const handleSortChange = (data: { prop: string; order: string | null }) => {
  447. sortParams.sortField = data.prop || "";
  448. sortParams.sortOrder = data.order || "";
  449. // 带着排序参数重新查询
  450. contentRef.value?.fetchPageData({ ...sortParams });
  451. };
  452. // ==================== 退款记录弹窗相关 ====================
  453. const refundRecordDialogVisible = ref(false);
  454. const refundTableLoading = ref(false);
  455. const refundTableData = ref<UserRefundOrderPageVO[]>([]);
  456. const refundTotal = ref(0);
  457. const refundDateRange = ref<[string, string]>();
  458. const currentUserId = ref<number>(0);
  459. // 退款查询参数
  460. const refundQueryParams = reactive<UserRefundOrderPageQuery>({
  461. pageNum: 1,
  462. pageSize: 10,
  463. userId: 0,
  464. startTime: undefined,
  465. endTime: undefined,
  466. });
  467. // 打开退款记录弹窗
  468. const openRefundRecordDialog = (row: any) => {
  469. currentUserId.value = row.id;
  470. refundQueryParams.userId = row.id;
  471. refundQueryParams.pageNum = 1;
  472. refundQueryParams.startTime = undefined;
  473. refundQueryParams.endTime = undefined;
  474. refundDateRange.value = undefined;
  475. refundRecordDialogVisible.value = true;
  476. fetchRefundRecordList();
  477. };
  478. // 获取退款记录列表
  479. const fetchRefundRecordList = async () => {
  480. refundTableLoading.value = true;
  481. try {
  482. const response = await UserInfoAPI.getUserRefundOrderList(refundQueryParams);
  483. refundTableData.value = response.list;
  484. refundTotal.value = response.total;
  485. } catch (error) {
  486. console.error("获取退款记录失败:", error);
  487. ElMessage.error("获取退款记录失败");
  488. } finally {
  489. refundTableLoading.value = false;
  490. }
  491. };
  492. // 处理日期范围变化
  493. const handleRefundDateChange = (value: [string, string] | null) => {
  494. if (value && value.length === 2) {
  495. refundQueryParams.startTime = value[0];
  496. refundQueryParams.endTime = value[1];
  497. } else {
  498. refundQueryParams.startTime = undefined;
  499. refundQueryParams.endTime = undefined;
  500. }
  501. };
  502. // 查询退款记录
  503. const handleRefundQuery = () => {
  504. refundQueryParams.pageNum = 1;
  505. fetchRefundRecordList();
  506. };
  507. // 重置退款查询
  508. const handleRefundReset = () => {
  509. refundQueryParams.pageNum = 1;
  510. refundQueryParams.startTime = undefined;
  511. refundQueryParams.endTime = undefined;
  512. refundDateRange.value = undefined;
  513. fetchRefundRecordList();
  514. };
  515. // 分页大小变化
  516. const handleRefundSizeChange = (size: number) => {
  517. refundQueryParams.pageSize = size;
  518. refundQueryParams.pageNum = 1;
  519. fetchRefundRecordList();
  520. };
  521. // 当前页变化
  522. const handleRefundCurrentChange = (page: number) => {
  523. refundQueryParams.pageNum = page;
  524. fetchRefundRecordList();
  525. };
  526. // ==================== 退款日志抽屉相关 ====================
  527. const refundLogDrawerVisible = ref(false);
  528. const currentNotifyRequest = ref("");
  529. // 处理退款记录点击
  530. const handleRefundRecordClick = (row: UserRefundOrderPageVO) => {
  531. currentNotifyRequest.value = row.notifyRequest || "";
  532. refundLogDrawerVisible.value = true;
  533. };
  534. </script>
  535. <style scoped>
  536. .money-text {
  537. font-weight: bold;
  538. font-size: 13px;
  539. }
  540. .money-text.primary {
  541. color: #409eff;
  542. }
  543. .money-text.success {
  544. color: #67c23a;
  545. }
  546. .money-text.danger {
  547. color: #f56c6c;
  548. }
  549. .money-text.warning {
  550. color: #e6a23c;
  551. }
  552. .refund-record-container {
  553. padding: 0;
  554. }
  555. .search-form {
  556. margin-bottom: 16px;
  557. }
  558. .el-pagination {
  559. margin-top: 16px;
  560. justify-content: flex-end;
  561. }
  562. </style>