orders.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. <template>
  2. <view class="container">
  3. <!-- 顶部状态栏 -->
  4. <view class="tab-header">
  5. <view
  6. v-for="tab in tabs"
  7. :key="tab.key"
  8. :class="['tab-item', activeTab === tab.key ? 'active' : '']"
  9. @click="activeTab = tab.key"
  10. >
  11. {{ tab.name }}
  12. </view>
  13. </view>
  14. <!-- 订单列表 -->
  15. <scroll-view class="order-scroll" scroll-y :show-scrollbar="false">
  16. <view class="list-wrap">
  17. <view v-for="(order, index) in filteredOrders" :key="index" class="order-card">
  18. <!-- 卡片头部:编号与状态 -->
  19. <view class="card-header">
  20. <text class="order-no">{{ order.orderNo }}</text>
  21. <view :class="['status-badge', order.statusKey]">
  22. <text>{{ order.statusText }}</text>
  23. </view>
  24. </view>
  25. <!-- 订单主体 -->
  26. <view class="card-body">
  27. <view class="body-top">
  28. <view class="order-info">
  29. <text class="order-name">{{ order.jobName }}</text>
  30. <text class="order-company">{{ order.company }}</text>
  31. </view>
  32. <view class="price-block">
  33. <text class="price-symbol">¥</text>
  34. <text class="price-num">{{ order.price }}</text>
  35. </view>
  36. </view>
  37. <view class="order-meta">
  38. <text class="meta-item">{{ order.createTime }}</text>
  39. <text class="meta-item" v-if="order.isDeposit">定金订单</text>
  40. </view>
  41. </view>
  42. <!-- 底部操作按钮 -->
  43. <view class="card-footer" v-if="order.statusKey === 'unpaid'">
  44. <button class="action-btn pay-btn" @click="handlePay(order)">立即支付</button>
  45. </view>
  46. <view class="card-footer" v-else-if="order.statusKey === 'paid'">
  47. <text class="footer-tip">支付完成,感谢您的购买</text>
  48. </view>
  49. </view>
  50. <!-- 空状态 -->
  51. <view class="empty-state" v-if="filteredOrders.length === 0 && !loading">
  52. <image src="/static/icons/empty-box.svg" class="empty-img"></image>
  53. <text>暂无相关订单记录</text>
  54. </view>
  55. <view v-if="filteredOrders.length > 0" class="no-more">—— 已到底啦 ——</view>
  56. </view>
  57. </scroll-view>
  58. <!-- 微信支付确认弹窗 -->
  59. <view class="modal-mask" v-if="showPayModal" @touchmove.stop.prevent>
  60. <view class="modal-container">
  61. <view class="modal-title">确认支付</view>
  62. <view class="pay-info">
  63. <view class="pay-row">
  64. <text class="pay-label">订单名称</text>
  65. <text class="pay-value">{{ currentOrder?.jobName }}</text>
  66. </view>
  67. <view class="pay-row">
  68. <text class="pay-label">支付金额</text>
  69. <text class="pay-value price">¥{{ currentOrder?.price }}</text>
  70. </view>
  71. </view>
  72. <view class="modal-btns">
  73. <button class="modal-btn cancel" @click="showPayModal = false">取消</button>
  74. <button class="modal-btn confirm" @click="confirmPay" :disabled="payLoading">确认支付</button>
  75. </view>
  76. </view>
  77. </view>
  78. </view>
  79. </template>
  80. <script setup>
  81. import { ref, computed, onMounted } from 'vue';
  82. import { onPullDownRefresh } from '@dcloudio/uni-app';
  83. import { listOrder } from '@/api/order';
  84. import { createWxPayOrder } from '@/api/message';
  85. const activeTab = ref('all');
  86. const tabs = [
  87. { name: '全部', key: 'all' },
  88. { name: '待支付', key: 'unpaid' },
  89. { name: '已支付', key: 'paid' },
  90. ];
  91. const loading = ref(false);
  92. const orders = ref([]);
  93. const showPayModal = ref(false);
  94. const currentOrder = ref(null);
  95. const payLoading = ref(false);
  96. onMounted(() => {
  97. fetchOrders();
  98. });
  99. onPullDownRefresh(async () => {
  100. await fetchOrders();
  101. uni.stopPullDownRefresh();
  102. });
  103. const fetchOrders = async () => {
  104. loading.value = true;
  105. try {
  106. const userInfo = uni.getStorageSync('userInfo');
  107. if (!userInfo || !userInfo.studentId) {
  108. uni.showToast({ title: '请先登录', icon: 'none' });
  109. return;
  110. }
  111. const res = await listOrder({
  112. buyerId: userInfo.studentId,
  113. buyerType: 2
  114. });
  115. if (res && res.rows) {
  116. orders.value = res.rows.map(item => {
  117. const statusInfo = resolveOrderStatus(item.orderStatus, item.payStatus);
  118. return {
  119. ...item,
  120. id: item.id,
  121. orderNo: item.orderNo,
  122. statusKey: statusInfo.key,
  123. statusText: statusInfo.text,
  124. jobName: item.productName || item.remark || '测评服务',
  125. company: item.sellerName || '审计之家',
  126. price: item.totalAmount || '0.00',
  127. isDeposit: item.orderType === 2,
  128. createTime: formatTime(item.createTime),
  129. };
  130. });
  131. }
  132. } catch (err) {
  133. console.error('获取订单列表失败:', err);
  134. uni.showToast({ title: '获取订单失败', icon: 'none' });
  135. } finally {
  136. loading.value = false;
  137. }
  138. };
  139. /**
  140. * 根据 orderStatus + payStatus 综合判断订单状态
  141. *
  142. * orderStatus: 0=待处理 1=已完成 2=部分退款 4=全额退款
  143. * payStatus: 0=未支付 1=微信已支付 2=余额已支付
  144. */
  145. const resolveOrderStatus = (orderStatus, payStatus) => {
  146. // 已退款
  147. if (orderStatus === 4) return { key: 'refunded', text: '已退款' };
  148. if (orderStatus === 2) return { key: 'partial_refund', text: '部分退款' };
  149. // 已支付
  150. if (payStatus === 1 || payStatus === 2) {
  151. if (orderStatus === 1) return { key: 'paid', text: '已完成' };
  152. return { key: 'paid', text: '已支付' };
  153. }
  154. // 未支付
  155. if (orderStatus === 0 && (payStatus === 0 || payStatus === null)) {
  156. return { key: 'unpaid', text: '待支付' };
  157. }
  158. // 兜底
  159. return { key: 'unknown', text: '未知状态' };
  160. };
  161. const filteredOrders = computed(() => {
  162. if (activeTab.value === 'all') return orders.value;
  163. return orders.value.filter(o => o.statusKey === activeTab.value);
  164. });
  165. const formatTime = (time) => {
  166. if (!time) return '--';
  167. const d = new Date(time);
  168. const year = d.getFullYear();
  169. const month = String(d.getMonth() + 1).padStart(2, '0');
  170. const day = String(d.getDate()).padStart(2, '0');
  171. const hour = String(d.getHours()).padStart(2, '0');
  172. const minute = String(d.getMinutes()).padStart(2, '0');
  173. return `${year}-${month}-${day} ${hour}:${minute}`;
  174. };
  175. const handlePay = (order) => {
  176. currentOrder.value = order;
  177. showPayModal.value = true;
  178. };
  179. const confirmPay = async () => {
  180. if (!currentOrder.value || payLoading.value) return;
  181. payLoading.value = true;
  182. try {
  183. const userInfo = uni.getStorageSync('userInfo') || {};
  184. const userId = userInfo.studentId;
  185. if (!userId) {
  186. uni.showToast({ title: '请先登录', icon: 'none' });
  187. return;
  188. }
  189. // 该订单的 businessId 关联的是结算单ID(cs_order_card.id)
  190. const businessId = currentOrder.value.businessId;
  191. if (!businessId) {
  192. // 没有 businessId,说明不是结算单创建的订单,走订单本身的支付流程
  193. uni.showToast({ title: '该订单暂不支持在线支付', icon: 'none' });
  194. return;
  195. }
  196. const payRes = await createWxPayOrder(businessId, userId);
  197. if (!(payRes.code === 200 || payRes.code === 0)) {
  198. uni.showToast({ title: payRes.msg || '创建支付订单失败', icon: 'none' });
  199. return;
  200. }
  201. showPayModal.value = false;
  202. // 检查是否返回了微信支付参数
  203. if (payRes.data && payRes.data.wechatPayParams) {
  204. const wxPayParams = payRes.data.wechatPayParams;
  205. uni.showLoading({ title: '发起微信支付...' });
  206. uni.requestPayment({
  207. provider: 'wxpay',
  208. timeStamp: wxPayParams.timeStamp,
  209. nonceStr: wxPayParams.nonceStr,
  210. package: wxPayParams.package,
  211. signType: wxPayParams.signType || 'RSA',
  212. paySign: wxPayParams.paySign,
  213. success: (res) => {
  214. uni.hideLoading();
  215. uni.showToast({ title: '支付成功', icon: 'success' });
  216. // 刷新订单列表
  217. setTimeout(() => { fetchOrders(); }, 1000);
  218. },
  219. fail: (err) => {
  220. uni.hideLoading();
  221. if (err.errMsg && err.errMsg.includes('cancel')) {
  222. uni.showToast({ title: '已取消支付', icon: 'none' });
  223. } else {
  224. uni.showToast({ title: '支付失败,请重试', icon: 'none' });
  225. }
  226. }
  227. });
  228. } else {
  229. uni.showToast({ title: '支付参数异常', icon: 'none' });
  230. }
  231. } catch (err) {
  232. console.error('支付异常:', err);
  233. uni.showToast({ title: '支付异常,请重试', icon: 'none' });
  234. } finally {
  235. payLoading.value = false;
  236. }
  237. };
  238. </script>
  239. <style lang="scss" scoped>
  240. .container {
  241. min-height: 100vh;
  242. background-color: #F5F6F8;
  243. display: flex;
  244. flex-direction: column;
  245. }
  246. /* Tabs */
  247. .tab-header {
  248. display: flex;
  249. background-color: #FFF;
  250. padding: 0 40rpx;
  251. border-bottom: 2rpx solid #F0F2F5;
  252. position: sticky;
  253. top: 0;
  254. z-index: 10;
  255. .tab-item {
  256. flex: 1;
  257. height: 100rpx;
  258. line-height: 100rpx;
  259. text-align: center;
  260. font-size: 28rpx;
  261. color: #666;
  262. position: relative;
  263. transition: all 0.2s;
  264. &.active {
  265. color: #1F6CFF;
  266. font-weight: bold;
  267. &::after {
  268. content: '';
  269. position: absolute;
  270. bottom: 0;
  271. left: 50%;
  272. transform: translateX(-50%);
  273. width: 48rpx;
  274. height: 6rpx;
  275. background-color: #1F6CFF;
  276. border-radius: 3rpx;
  277. }
  278. }
  279. }
  280. }
  281. /* 列表 */
  282. .order-scroll {
  283. flex: 1;
  284. .list-wrap {
  285. padding: 24rpx 30rpx;
  286. }
  287. }
  288. /* 订单卡片 */
  289. .order-card {
  290. background: #FFF;
  291. border-radius: 24rpx;
  292. padding: 32rpx;
  293. margin-bottom: 24rpx;
  294. box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
  295. .card-header {
  296. display: flex;
  297. justify-content: space-between;
  298. align-items: center;
  299. padding-bottom: 24rpx;
  300. margin-bottom: 24rpx;
  301. border-bottom: 1rpx solid #F0F2F5;
  302. .order-no { font-size: 24rpx; color: #999; }
  303. .status-badge {
  304. padding: 4rpx 16rpx;
  305. border-radius: 16rpx;
  306. font-size: 22rpx;
  307. font-weight: 500;
  308. &.unpaid { background: #FFF7E6; color: #FA8C16; }
  309. &.paid { background: #F0FFF4; color: #52C41A; }
  310. &.refunded { background: #F5F5F5; color: #999; }
  311. &.partial_refund { background: #E6F7FF; color: #1890FF; }
  312. }
  313. }
  314. .card-body {
  315. .body-top {
  316. display: flex;
  317. justify-content: space-between;
  318. align-items: flex-start;
  319. margin-bottom: 16rpx;
  320. .order-info {
  321. flex: 1;
  322. min-width: 0;
  323. .order-name {
  324. font-size: 32rpx;
  325. font-weight: 600;
  326. color: #1A1A1A;
  327. display: block;
  328. margin-bottom: 8rpx;
  329. overflow: hidden;
  330. text-overflow: ellipsis;
  331. white-space: nowrap;
  332. }
  333. .order-company {
  334. font-size: 26rpx;
  335. color: #888;
  336. display: block;
  337. }
  338. }
  339. .price-block {
  340. flex-shrink: 0;
  341. margin-left: 16rpx;
  342. text-align: right;
  343. .price-symbol { font-size: 26rpx; color: #FF4D4F; font-weight: 500; }
  344. .price-num { font-size: 40rpx; color: #FF4D4F; font-weight: bold; }
  345. }
  346. }
  347. .order-meta {
  348. display: flex;
  349. gap: 24rpx;
  350. .meta-item {
  351. font-size: 24rpx;
  352. color: #BBB;
  353. }
  354. }
  355. }
  356. .card-footer {
  357. margin-top: 24rpx;
  358. padding-top: 24rpx;
  359. border-top: 1rpx solid #F0F2F5;
  360. display: flex;
  361. justify-content: flex-end;
  362. align-items: center;
  363. .footer-tip {
  364. font-size: 24rpx;
  365. color: #52C41A;
  366. }
  367. .action-btn {
  368. height: 64rpx;
  369. line-height: 62rpx;
  370. padding: 0 40rpx;
  371. font-size: 26rpx;
  372. font-weight: 600;
  373. border-radius: 32rpx;
  374. margin: 0;
  375. &::after { border: none; }
  376. &.pay-btn {
  377. background: linear-gradient(135deg, #1F6CFF, #4D8FFF);
  378. color: #FFF;
  379. box-shadow: 0 4rpx 16rpx rgba(31, 108, 255, 0.25);
  380. }
  381. }
  382. }
  383. }
  384. /* 支付弹窗 */
  385. .modal-mask {
  386. position: fixed; top: 0; left: 0; right: 0; bottom: 0;
  387. background: rgba(0,0,0,0.6); z-index: 2000;
  388. display: flex; align-items: center; justify-content: center;
  389. }
  390. .modal-container {
  391. width: 580rpx;
  392. background: #FFF;
  393. border-radius: 32rpx;
  394. padding: 48rpx 40rpx 40rpx;
  395. .modal-title {
  396. font-size: 36rpx;
  397. font-weight: bold;
  398. color: #1A1A1A;
  399. text-align: center;
  400. margin-bottom: 40rpx;
  401. }
  402. .pay-info {
  403. background: #F8F9FB;
  404. border-radius: 20rpx;
  405. padding: 32rpx;
  406. margin-bottom: 40rpx;
  407. .pay-row {
  408. display: flex;
  409. justify-content: space-between;
  410. align-items: center;
  411. margin-bottom: 20rpx;
  412. &:last-child { margin-bottom: 0; }
  413. .pay-label { font-size: 28rpx; color: #888; }
  414. .pay-value { font-size: 28rpx; color: #333; font-weight: 500; &.price { color: #FF4D4F; font-size: 34rpx; font-weight: bold; } }
  415. }
  416. }
  417. .modal-btns {
  418. display: flex;
  419. gap: 20rpx;
  420. .modal-btn {
  421. flex: 1;
  422. height: 80rpx;
  423. line-height: 80rpx;
  424. border-radius: 40rpx;
  425. font-size: 28rpx;
  426. font-weight: 600;
  427. &::after { border: none; }
  428. &.cancel { background: #F5F5F7; color: #666; }
  429. &.confirm { background: #1F6CFF; color: #FFF; &:disabled { opacity: 0.5; } }
  430. }
  431. }
  432. }
  433. /* 空状态 & 底部 */
  434. .empty-state {
  435. display: flex; flex-direction: column; align-items: center; padding-top: 150rpx;
  436. .empty-img { width: 240rpx; height: 240rpx; margin-bottom: 20rpx; opacity: 0.5; }
  437. text { font-size: 28rpx; color: #CCC; }
  438. }
  439. .no-more { text-align: center; font-size: 24rpx; color: #CCC; padding: 40rpx 0; }
  440. </style>