index.vue 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. <template>
  2. <!-- 单根节点(含下单/详情弹层):AppMain 的 <transition mode="out-in"> 要求组件根为元素 -->
  3. <div class="har-order">
  4. <el-card>
  5. <template #header>
  6. <div class="card-header">
  7. <span>订单管理</span>
  8. <el-button
  9. v-hasPerm="['module_payment:har:order:create']"
  10. type="primary"
  11. icon="Plus"
  12. @click="openCreate"
  13. >
  14. 创建订单
  15. </el-button>
  16. </div>
  17. </template>
  18. <div class="mb-4">
  19. <el-form :inline="true" :model="searchForm">
  20. <el-form-item label="企业">
  21. <el-select
  22. v-model="searchForm.enterprise_id"
  23. placeholder="全部企业"
  24. clearable
  25. filterable
  26. style="width: 190px"
  27. >
  28. <el-option
  29. v-for="e in enterpriseStore.getEnterpriseList"
  30. :key="e.enterprise_id"
  31. :label="e.name || e.enterprise_id"
  32. :value="e.enterprise_id"
  33. />
  34. </el-select>
  35. </el-form-item>
  36. <el-form-item label="商户编号">
  37. <el-input
  38. v-model="searchForm.merchant_code"
  39. placeholder="商户编号"
  40. clearable
  41. style="width: 170px"
  42. @keyup.enter="handleSearch"
  43. />
  44. </el-form-item>
  45. <el-form-item label="订单号">
  46. <el-input
  47. v-model="searchForm.order_no"
  48. placeholder="我方订单号"
  49. clearable
  50. style="width: 180px"
  51. @keyup.enter="handleSearch"
  52. />
  53. </el-form-item>
  54. <el-form-item label="商户订单号">
  55. <el-input
  56. v-model="searchForm.out_biz_no"
  57. placeholder="企业自己的订单号"
  58. clearable
  59. style="width: 180px"
  60. @keyup.enter="handleSearch"
  61. />
  62. </el-form-item>
  63. <el-form-item label="交易状态">
  64. <el-select
  65. v-model="searchForm.trade_status"
  66. placeholder="全部"
  67. clearable
  68. style="width: 140px"
  69. >
  70. <el-option
  71. v-for="(text, value) in HAR_TRADE_STATUS_TEXT"
  72. :key="value"
  73. :label="text"
  74. :value="value"
  75. />
  76. </el-select>
  77. </el-form-item>
  78. <el-form-item label="交易方式">
  79. <el-select
  80. v-model="searchForm.trade_type"
  81. placeholder="全部"
  82. clearable
  83. style="width: 140px"
  84. >
  85. <el-option
  86. v-for="t in HAR_TRADE_TYPE_OPTIONS"
  87. :key="t.value"
  88. :label="t.label"
  89. :value="t.value"
  90. />
  91. </el-select>
  92. </el-form-item>
  93. <el-form-item label="创建日期">
  94. <el-date-picker
  95. v-model="dateRange"
  96. type="daterange"
  97. range-separator="至"
  98. start-placeholder="开始日期"
  99. end-placeholder="结束日期"
  100. value-format="YYYY-MM-DD"
  101. style="width: 250px"
  102. @change="handleDateChange"
  103. />
  104. </el-form-item>
  105. <el-form-item>
  106. <el-button type="primary" @click="handleSearch">查询</el-button>
  107. <el-button @click="handleReset">重置</el-button>
  108. </el-form-item>
  109. </el-form>
  110. </div>
  111. <el-table v-loading="loading" :data="list" border stripe>
  112. <template #empty>
  113. <el-empty description="暂无数据" />
  114. </template>
  115. <el-table-column prop="order_no" label="订单号" min-width="190" show-overflow-tooltip />
  116. <el-table-column prop="out_biz_no" label="商户订单号" min-width="160" show-overflow-tooltip>
  117. <template #default="{ row }">{{ row.out_biz_no || "-" }}</template>
  118. </el-table-column>
  119. <el-table-column prop="title" label="商品名称" min-width="140" show-overflow-tooltip>
  120. <template #default="{ row }">{{ row.title || "-" }}</template>
  121. </el-table-column>
  122. <el-table-column label="金额(元)" width="110">
  123. <template #default="{ row }">¥{{ row.price }}</template>
  124. </el-table-column>
  125. <el-table-column label="实收金额(元)" width="120">
  126. <template #default="{ row }">{{ row.rcv_amount != null ? "¥" + row.rcv_amount : "-" }}</template>
  127. </el-table-column>
  128. <el-table-column label="手续费(元)" width="110">
  129. <template #default="{ row }">{{ row.fee_amount != null ? "¥" + row.fee_amount : "-" }}</template>
  130. </el-table-column>
  131. <el-table-column label="渠道成本(元)" width="120">
  132. <template #default="{ row }">{{ row.channel_amount != null ? "¥" + row.channel_amount : "-" }}</template>
  133. </el-table-column>
  134. <el-table-column label="交易状态" width="100">
  135. <template #default="{ row }">
  136. <el-tag :type="HAR_TRADE_STATUS_TAG[row.trade_status] || 'info'">
  137. {{ HAR_TRADE_STATUS_TEXT[row.trade_status] || row.trade_status }}
  138. </el-tag>
  139. </template>
  140. </el-table-column>
  141. <el-table-column label="交易方式" width="110">
  142. <template #default="{ row }">{{ tradeTypeText(row.trade_type) }}</template>
  143. </el-table-column>
  144. <el-table-column prop="merchant_code" label="商户编号" min-width="140">
  145. <template #default="{ row }">{{ row.merchant_code || "-" }}</template>
  146. </el-table-column>
  147. <el-table-column prop="third_shop_code" label="门店编码" min-width="130">
  148. <template #default="{ row }">{{ row.third_shop_code || "-" }}</template>
  149. </el-table-column>
  150. <el-table-column prop="platform_trade_no" label="支付宝交易号" min-width="200" show-overflow-tooltip>
  151. <template #default="{ row }">{{ row.platform_trade_no || "-" }}</template>
  152. </el-table-column>
  153. <el-table-column label="交易时间" width="170">
  154. <template #default="{ row }">
  155. {{ row.trade_time ? dayjs(row.trade_time).format("YYYY-MM-DD HH:mm:ss") : "-" }}
  156. </template>
  157. </el-table-column>
  158. <el-table-column label="退款时间" width="170">
  159. <template #default="{ row }">
  160. {{ row.refund_time ? dayjs(row.refund_time).format("YYYY-MM-DD HH:mm:ss") : "-" }}
  161. </template>
  162. </el-table-column>
  163. <el-table-column prop="refund_reason" label="退款原因" min-width="160" show-overflow-tooltip>
  164. <template #default="{ row }">{{ row.refund_reason || "-" }}</template>
  165. </el-table-column>
  166. <el-table-column label="操作" width="150" fixed="right">
  167. <template #default="{ row }">
  168. <el-button
  169. v-hasPerm="['module_payment:har:order:detail']"
  170. size="small"
  171. @click="openDetail(row)"
  172. >
  173. 详情
  174. </el-button>
  175. <el-button
  176. v-hasPerm="['module_payment:har:order:sync']"
  177. size="small"
  178. type="primary"
  179. :loading="syncingId === row.id"
  180. @click="handleSync(row)"
  181. >
  182. 同步
  183. </el-button>
  184. </template>
  185. </el-table-column>
  186. </el-table>
  187. <div class="mt-4 flex justify-end">
  188. <el-pagination
  189. v-model:current-page="pageNo"
  190. v-model:page-size="pageSize"
  191. :total="total"
  192. :page-sizes="[10, 20, 50, 100]"
  193. layout="total, sizes, prev, pager, next, jumper"
  194. @size-change="load"
  195. @current-change="load"
  196. />
  197. </div>
  198. </el-card>
  199. <!-- 创建订单 -->
  200. <el-dialog v-model="createVisible" title="创建订单" width="640px" @closed="handleCreateClosed">
  201. <el-alert
  202. class="mb-4"
  203. type="info"
  204. :closable="false"
  205. show-icon
  206. title="填写付款码即为被扫收款(用户出示付款码);留空则生成收款链接,由用户扫码支付。"
  207. />
  208. <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
  209. <el-form-item label="商户" prop="merchant_code">
  210. <!-- 可清空 + 可输入:本租户配了多个商户时这里就是「钱进哪一家」的开关;
  211. 列表拉不到时(未授权/接口异常)仍可手工敲编号,归属由后端按本租户校验 -->
  212. <el-select
  213. v-model="form.merchant_code"
  214. placeholder="留空即按企业与租户自动匹配"
  215. clearable
  216. filterable
  217. allow-create
  218. default-first-option
  219. style="width: 100%"
  220. >
  221. <el-option
  222. v-for="m in merchantOptions"
  223. :key="m.merchant_code"
  224. :label="merchantLabel(m)"
  225. :value="m.merchant_code"
  226. />
  227. </el-select>
  228. <div v-if="merchantOptions.length > 1" class="form-tip">
  229. 本租户配置了多个商户,请选择这笔款项收进哪一个商户
  230. </div>
  231. </el-form-item>
  232. <el-form-item label="企业" prop="enterprise_id">
  233. <el-select
  234. v-model="form.enterprise_id"
  235. placeholder="留空即按租户下单"
  236. clearable
  237. filterable
  238. style="width: 100%"
  239. >
  240. <el-option
  241. v-for="e in enterpriseStore.getEnterpriseList"
  242. :key="e.enterprise_id"
  243. :label="e.name || e.enterprise_id"
  244. :value="e.enterprise_id"
  245. />
  246. </el-select>
  247. </el-form-item>
  248. <el-form-item label="商户订单号" prop="out_biz_no">
  249. <el-input
  250. v-model="form.out_biz_no"
  251. placeholder="企业自己的订单号,便于对账,最长 64 位"
  252. maxlength="64"
  253. show-word-limit
  254. />
  255. </el-form-item>
  256. <el-form-item label="金额(元)" prop="price">
  257. <el-input-number
  258. v-model="form.price"
  259. :min="0.01"
  260. :precision="2"
  261. :step="1"
  262. controls-position="right"
  263. style="width: 200px"
  264. />
  265. </el-form-item>
  266. <el-form-item label="商品名称" prop="title">
  267. <el-input v-model="form.title" maxlength="64" show-word-limit />
  268. </el-form-item>
  269. <el-form-item label="交易方式" prop="trade_type">
  270. <el-select v-model="form.trade_type" style="width: 100%" @change="handleTradeTypeChange">
  271. <el-option
  272. v-for="t in HAR_TRADE_TYPE_OPTIONS"
  273. :key="t.value"
  274. :label="t.label"
  275. :value="t.value"
  276. />
  277. </el-select>
  278. </el-form-item>
  279. <template v-if="form.trade_type === 'alipay_stage'">
  280. <el-form-item label="分期数" prop="stage_num">
  281. <el-select v-model="form.stage_num" placeholder="请选择分期数" style="width: 100%">
  282. <el-option v-for="n in STAGE_NUM_OPTIONS" :key="n" :label="`${n} 期`" :value="n" />
  283. </el-select>
  284. </el-form-item>
  285. <el-form-item label="手续费承担方" prop="percent">
  286. <el-select v-model="form.percent" style="width: 100%">
  287. <el-option label="商户承担" value="100" />
  288. <el-option label="用户承担" value="0" />
  289. </el-select>
  290. </el-form-item>
  291. </template>
  292. <el-form-item label="门店编码" prop="third_shop_code">
  293. <el-input
  294. v-model="form.third_shop_code"
  295. placeholder="商户侧门店编码,最长 32 位"
  296. maxlength="32"
  297. show-word-limit
  298. />
  299. </el-form-item>
  300. <el-form-item label="平台门店编号" prop="shop_code">
  301. <el-input v-model="form.shop_code" placeholder="上游返回的平台门店编号" />
  302. </el-form-item>
  303. <el-form-item label="付款码" prop="auth_code">
  304. <el-input
  305. v-model="form.auth_code"
  306. placeholder="扫用户付款码得到,填写即走被扫收款"
  307. clearable
  308. />
  309. </el-form-item>
  310. <el-form-item label="支付超时" prop="pay_timeout">
  311. <el-input-number
  312. v-model="form.pay_timeout"
  313. :min="2"
  314. :max="60"
  315. controls-position="right"
  316. style="width: 200px"
  317. />
  318. <span class="ml-2 text-gray-400">分钟,仅主扫下单有效</span>
  319. </el-form-item>
  320. <el-form-item label="扩展参数" prop="extra_params">
  321. <el-input
  322. v-model="form.extra_params"
  323. type="textarea"
  324. :rows="2"
  325. placeholder="上游透传字段,原样提交"
  326. />
  327. </el-form-item>
  328. </el-form>
  329. <template #footer>
  330. <el-button @click="createVisible = false">取消</el-button>
  331. <el-button type="primary" :loading="saving" @click="handleCreate">提交</el-button>
  332. </template>
  333. </el-dialog>
  334. <!-- 订单详情 -->
  335. <el-dialog v-model="detailVisible" title="订单详情" width="720px">
  336. <el-descriptions v-if="detail" :column="2" border>
  337. <el-descriptions-item label="订单号" :span="2">{{ detail.order_no }}</el-descriptions-item>
  338. <el-descriptions-item label="商户订单号">
  339. {{ detail.out_biz_no || "-" }}
  340. </el-descriptions-item>
  341. <el-descriptions-item label="交易状态">
  342. <el-tag :type="HAR_TRADE_STATUS_TAG[detail.trade_status] || 'info'">
  343. {{ HAR_TRADE_STATUS_TEXT[detail.trade_status] || detail.trade_status }}
  344. </el-tag>
  345. </el-descriptions-item>
  346. <el-descriptions-item label="商品名称">{{ detail.title || "-" }}</el-descriptions-item>
  347. <el-descriptions-item label="交易方式">
  348. {{ tradeTypeText(detail.trade_type) }}
  349. </el-descriptions-item>
  350. <el-descriptions-item label="订单金额">¥{{ detail.price }}</el-descriptions-item>
  351. <el-descriptions-item label="实收金额">
  352. {{ detail.rcv_amount != null ? `¥${detail.rcv_amount}` : "-" }}
  353. </el-descriptions-item>
  354. <el-descriptions-item label="手续费">
  355. {{ detail.fee_amount != null ? `¥${detail.fee_amount}` : "-" }}
  356. </el-descriptions-item>
  357. <el-descriptions-item label="渠道成本">
  358. {{ detail.channel_amount != null ? `¥${detail.channel_amount}` : "-" }}
  359. </el-descriptions-item>
  360. <el-descriptions-item label="商户编号">
  361. {{ detail.merchant_code || "-" }}
  362. </el-descriptions-item>
  363. <el-descriptions-item label="门店编码">
  364. {{ detail.third_shop_code || "-" }}
  365. </el-descriptions-item>
  366. <el-descriptions-item label="平台门店编号">
  367. {{ detail.shop_code || "-" }}
  368. </el-descriptions-item>
  369. <el-descriptions-item label="上游订单号" :span="2">
  370. {{ detail.upstream_order_no || "-" }}
  371. </el-descriptions-item>
  372. <el-descriptions-item label="支付宝交易号" :span="2">
  373. {{ detail.platform_trade_no || "-" }}
  374. </el-descriptions-item>
  375. <el-descriptions-item v-if="detail.trade_type === 'alipay_stage'" label="分期数">
  376. {{ detail.stage_num ? `${detail.stage_num} 期` : "-" }}
  377. </el-descriptions-item>
  378. <el-descriptions-item v-if="detail.trade_type === 'alipay_stage'" label="手续费承担方">
  379. {{ detail.percent === "100" ? "商户承担" : detail.percent === "0" ? "用户承担" : "-" }}
  380. </el-descriptions-item>
  381. <el-descriptions-item label="支付超时">
  382. {{ detail.pay_timeout != null ? `${detail.pay_timeout} 分钟` : "-" }}
  383. </el-descriptions-item>
  384. <el-descriptions-item label="企业回调">
  385. {{ HAR_NOTIFY_STATUS_TEXT[detail.notify_status || ""] || detail.notify_status || "-" }}
  386. </el-descriptions-item>
  387. <el-descriptions-item label="下单时间">
  388. {{ formatTime(detail.created_time) }}
  389. </el-descriptions-item>
  390. <el-descriptions-item label="支付时间">
  391. {{ formatTime(detail.trade_time) }}
  392. </el-descriptions-item>
  393. <el-descriptions-item label="退款时间">
  394. {{ formatTime(detail.refund_time) }}
  395. </el-descriptions-item>
  396. <el-descriptions-item label="退款原因" :span="2">
  397. {{ detail.refund_reason || "-" }}
  398. </el-descriptions-item>
  399. <el-descriptions-item label="上游创建时间">
  400. {{ formatTime(detail.upstream_create_time) }}
  401. </el-descriptions-item>
  402. <el-descriptions-item v-if="detail.pay_url" label="收款链接" :span="2">
  403. <div class="pay-url">
  404. <span class="pay-url__text">{{ detail.pay_url }}</span>
  405. <el-button size="small" type="primary" @click="openPayUrl(detail.pay_url)">
  406. 打开
  407. </el-button>
  408. <el-button size="small" @click="copyPayUrl(detail.pay_url)">复制</el-button>
  409. </div>
  410. </el-descriptions-item>
  411. <el-descriptions-item v-if="detail.extra_params" label="扩展参数" :span="2">
  412. {{ detail.extra_params }}
  413. </el-descriptions-item>
  414. </el-descriptions>
  415. <template #footer>
  416. <el-button
  417. v-hasPerm="['module_payment:har:order:sync']"
  418. :loading="syncingId === detail?.id"
  419. @click="detail && handleSync(detail)"
  420. >
  421. 同步上游状态
  422. </el-button>
  423. <el-button @click="detailVisible = false">关闭</el-button>
  424. </template>
  425. </el-dialog>
  426. </div>
  427. </template>
  428. <script setup lang="ts">
  429. import { onMounted, reactive, ref } from "vue";
  430. import { ElMessage, type FormInstance, type FormRules } from "element-plus";
  431. import dayjs from "dayjs";
  432. import HarAPI, {
  433. HAR_BIZ_UN_STAGE,
  434. HAR_NOTIFY_STATUS_TEXT,
  435. HAR_TRADE_STATUS_TAG,
  436. HAR_TRADE_STATUS_TEXT,
  437. HAR_TRADE_TYPE_OPTIONS,
  438. type HarMerchantVO,
  439. type HarOrderCreateParams,
  440. type HarOrderVO,
  441. } from "@/api/module_payment/har";
  442. import { useEnterpriseStore } from "@/store/modules/enterprise.store";
  443. const enterpriseStore = useEnterpriseStore();
  444. /** 本租户可见的商户配置 —— 下单时用哪一条由这里选 */
  445. const merchantOptions = ref<HarMerchantVO[]>([]);
  446. /** 规范 5.1 给定的分期期数 */
  447. const STAGE_NUM_OPTIONS = ["3", "6", "12", "24"];
  448. const list = ref<HarOrderVO[]>([]);
  449. const total = ref(0);
  450. const pageNo = ref(1);
  451. const pageSize = ref(20);
  452. const loading = ref(false);
  453. const syncingId = ref<string | undefined>();
  454. const createVisible = ref(false);
  455. const saving = ref(false);
  456. const formRef = ref<FormInstance>();
  457. const detailVisible = ref(false);
  458. const detail = ref<HarOrderVO | null>(null);
  459. const dateRange = ref<string[] | null>(null);
  460. const searchForm = reactive({
  461. enterprise_id: undefined as string | undefined,
  462. merchant_code: "",
  463. order_no: "",
  464. out_biz_no: "",
  465. trade_status: "",
  466. trade_type: "",
  467. start_date: undefined as string | undefined,
  468. end_date: undefined as string | undefined,
  469. });
  470. /** price 在后端入参里是 BigDecimal(数字或字符串皆可),表单里固定为数字,提交时再断言 */
  471. type OrderForm = Omit<HarOrderCreateParams, "price"> & { price?: number };
  472. const defaultForm = (): OrderForm => ({
  473. // 用 undefined 而不是空串:空串对 enterprise_id 是有含义的(=租户级),
  474. // 对 merchant_code 不是 —— 留空只表示「没指定」,后端按 StrUtil.isNotBlank 分流
  475. merchant_code: undefined,
  476. enterprise_id: "",
  477. out_biz_no: "",
  478. price: undefined,
  479. title: "",
  480. trade_type: "alipay_un_stage",
  481. // 非分期要带业务类型(规范 260924)。后端纯透传、不推导,所以这里就得给上;
  482. // 分期那个取值规范没写,留空 —— 真缺了上游会点名报出来。
  483. biz: HAR_BIZ_UN_STAGE,
  484. stage_num: "",
  485. percent: "",
  486. third_shop_code: "",
  487. shop_code: "",
  488. auth_code: "",
  489. pay_timeout: undefined,
  490. extra_params: "",
  491. });
  492. const form = reactive<OrderForm>(defaultForm());
  493. const rules: FormRules = {
  494. // 企业**非必填**:留空即按租户下单(用 enterprise_id IS NULL 的那条租户级商户映射),
  495. // 与 HarOrderCreateDTO 上「选填」的注释、后端 HarOrderService#create 的分流一致。
  496. // 商户同理非必填:本租户只有一条配置时留空即为那条;有多条时后端会 400 要求指定。
  497. price: [{ required: true, message: "请填写金额", trigger: "blur" }],
  498. title: [{ required: true, message: "请填写商品名称", trigger: "blur" }],
  499. trade_type: [{ required: true, message: "请选择交易方式", trigger: "change" }],
  500. };
  501. const merchantLabel = (m: HarMerchantVO) =>
  502. m.merchant_name ? `${m.merchant_name}(${m.merchant_code})` : m.merchant_code;
  503. async function loadMerchantOptions() {
  504. try {
  505. const res = await HarAPI.merchantList({ page_size: 100 });
  506. merchantOptions.value = res.data.data?.items || res.data.data?.list || [];
  507. } catch {
  508. // 拉不到(未授权/接口异常)不阻塞下单:下拉留空,仍可手工输入编号,
  509. // 归属由后端按本租户校验(跨租户的会被 404 挡掉)
  510. merchantOptions.value = [];
  511. }
  512. }
  513. const tradeTypeText = (v?: string) =>
  514. HAR_TRADE_TYPE_OPTIONS.find((t) => t.value === v)?.label || v || "-";
  515. const formatTime = (v?: string) => (v ? dayjs(v).format("YYYY-MM-DD HH:mm:ss") : "-");
  516. async function load() {
  517. loading.value = true;
  518. try {
  519. const res = await HarAPI.orderList({
  520. enterprise_id: searchForm.enterprise_id || undefined,
  521. merchant_code: searchForm.merchant_code || undefined,
  522. order_no: searchForm.order_no || undefined,
  523. out_biz_no: searchForm.out_biz_no || undefined,
  524. trade_status: searchForm.trade_status || undefined,
  525. trade_type: searchForm.trade_type || undefined,
  526. start_date: searchForm.start_date || undefined,
  527. end_date: searchForm.end_date || undefined,
  528. page_no: pageNo.value,
  529. page_size: pageSize.value,
  530. });
  531. list.value = res.data.data?.items || res.data.data?.list || [];
  532. total.value = res.data.data?.total || 0;
  533. } finally {
  534. loading.value = false;
  535. }
  536. }
  537. function handleSearch() {
  538. pageNo.value = 1;
  539. load();
  540. }
  541. function handleDateChange() {
  542. if (dateRange.value && dateRange.value.length === 2) {
  543. searchForm.start_date = dateRange.value[0];
  544. searchForm.end_date = dateRange.value[1];
  545. } else {
  546. searchForm.start_date = undefined;
  547. searchForm.end_date = undefined;
  548. }
  549. }
  550. function handleReset() {
  551. searchForm.enterprise_id = undefined;
  552. searchForm.merchant_code = "";
  553. searchForm.order_no = "";
  554. searchForm.out_biz_no = "";
  555. searchForm.trade_status = "";
  556. searchForm.trade_type = "";
  557. searchForm.start_date = undefined;
  558. searchForm.end_date = undefined;
  559. dateRange.value = null;
  560. handleSearch();
  561. }
  562. function openCreate() {
  563. Object.assign(form, defaultForm());
  564. createVisible.value = true;
  565. }
  566. function handleCreateClosed() {
  567. formRef.value?.clearValidate();
  568. Object.assign(form, defaultForm());
  569. }
  570. function handleTradeTypeChange(v: string) {
  571. // 非分期时清掉分期字段,避免把无关参数透传给上游
  572. if (v !== "alipay_stage") {
  573. form.stage_num = "";
  574. form.percent = "";
  575. // 切回非分期要把业务类型补回来:清掉它再切回来仍是「非分期却不带 biz」
  576. form.biz = HAR_BIZ_UN_STAGE;
  577. } else {
  578. // 分期的业务类型取值规范没给,不猜
  579. form.biz = "";
  580. if (!form.percent) {
  581. form.percent = "100";
  582. }
  583. }
  584. }
  585. async function handleCreate() {
  586. if (form.trade_type === "alipay_stage" && (!form.stage_num || !form.percent)) {
  587. ElMessage.warning("花呗分期必须选择分期数与手续费承担方");
  588. return;
  589. }
  590. const valid = await formRef.value?.validate().catch(() => false);
  591. if (!valid) {
  592. return;
  593. }
  594. saving.value = true;
  595. try {
  596. const res = await HarAPI.orderCreate({
  597. ...form,
  598. // 已过表单必填校验,此处 price 必然有值
  599. price: form.price as number,
  600. out_biz_no: form.out_biz_no || undefined,
  601. biz: form.biz || undefined,
  602. stage_num: form.stage_num || undefined,
  603. percent: form.percent || undefined,
  604. third_shop_code: form.third_shop_code || undefined,
  605. shop_code: form.shop_code || undefined,
  606. auth_code: form.auth_code || undefined,
  607. extra_params: form.extra_params || undefined,
  608. });
  609. const created = res.data.data;
  610. createVisible.value = false;
  611. detail.value = created || null;
  612. detailVisible.value = true;
  613. ElMessage.success(
  614. created?.pay_url ? "下单成功,请在详情中打开收款链接" : "下单成功,等待用户付款"
  615. );
  616. load();
  617. } finally {
  618. saving.value = false;
  619. }
  620. }
  621. async function openDetail(row: HarOrderVO) {
  622. const res = await HarAPI.orderDetail(row.id as string);
  623. detail.value = res.data.data;
  624. detailVisible.value = true;
  625. }
  626. async function handleSync(row: HarOrderVO) {
  627. syncingId.value = row.id;
  628. try {
  629. const res = await HarAPI.orderSync(row.id as string);
  630. const updated = res.data.data;
  631. ElMessage.success(
  632. `已同步,当前状态:${HAR_TRADE_STATUS_TEXT[updated.trade_status] || updated.trade_status}`
  633. );
  634. // 详情打开着就同步刷新,避免弹层里看到的还是旧状态
  635. if (detailVisible.value && detail.value?.id === row.id) {
  636. detail.value = updated;
  637. }
  638. load();
  639. } finally {
  640. syncingId.value = undefined;
  641. }
  642. }
  643. function openPayUrl(url?: string) {
  644. if (!url) {
  645. return;
  646. }
  647. const w = window.open(url, "_blank");
  648. if (!w) {
  649. ElMessage.warning("收款链接弹窗被浏览器拦截,请允许弹窗后重试");
  650. }
  651. }
  652. async function copyPayUrl(url?: string) {
  653. if (!url) {
  654. return;
  655. }
  656. try {
  657. await navigator.clipboard.writeText(url);
  658. ElMessage.success("已复制收款链接");
  659. } catch {
  660. ElMessage.warning("浏览器拒绝了复制操作,请手动选中复制");
  661. }
  662. }
  663. onMounted(async () => {
  664. load();
  665. loadMerchantOptions();
  666. try {
  667. await enterpriseStore.loadEnterpriseList();
  668. } catch {
  669. // 企业列表加载失败不阻塞列表查询
  670. }
  671. });
  672. </script>
  673. <style lang="scss" scoped>
  674. .card-header {
  675. display: flex;
  676. justify-content: space-between;
  677. align-items: center;
  678. }
  679. .form-tip {
  680. font-size: 12px;
  681. line-height: 1.6;
  682. color: var(--el-text-color-secondary);
  683. }
  684. .pay-url {
  685. display: flex;
  686. align-items: center;
  687. gap: 8px;
  688. &__text {
  689. flex: 1;
  690. word-break: break-all;
  691. color: var(--el-text-color-regular);
  692. }
  693. }
  694. </style>