order.uvue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. <template>
  2. <view class="page-container">
  3. <!-- 1. 顶部标签栏 (Scroll View) -->
  4. <scroll-view direction="horizontal" class="tab-scroll" show-scrollbar="false" :enable-flex="true">
  5. <view class="tab-wrapper">
  6. <view v-for="(tab, index) in tabs" :key="tab.value"
  7. :class="['tab-item', currentTab === tab.value ? 'active' : '']" @click="handleTabClick(tab.value)">
  8. <text>
  9. {{ tab.label }}
  10. </text>
  11. <!-- 激活下的黄色短线 -->
  12. <view v-if="currentTab === tab.value" class="tab-indicator">
  13. </view>
  14. </view>
  15. </view>
  16. </scroll-view>
  17. <!-- 2. 订单列表 -->
  18. <scroll-view style="flex:1" v-if="orderList.length > 0" direction="vertical" @scrolltolower="loadMore">
  19. <view class="order-list">
  20. <view v-for="(item, index) in orderList" :key="item.id" class="order-card">
  21. <!-- Card Header: 时间 & 状态 -->
  22. <view class="card-header">
  23. <text class="time-text">
  24. 预约时间:{{ item.service_time }}
  25. </text>
  26. <text class="status-badge paid">
  27. {{ item.state_text }}
  28. </text>
  29. </view>
  30. <!-- Card Body: 服务信息 (左图右文) -->
  31. <view class="service-section" @click="orderDetail(item.id as number)">
  32. <image
  33. :src="(((((item.project as UTSJSONObject).cover_urls ) as UTSArray<UTSJSONObject>)[0]) as UTSJSONObject).medium_url"
  34. class=" service-image" mode="aspectFill" />
  35. <view class="service-details">
  36. <view class="service-title-row">
  37. <text class="service-name">
  38. {{ (item.project as UTSJSONObject)?.title }}
  39. </text>
  40. </view>
  41. <view class="contact-info-row">
  42. <text class="contact-info">
  43. 联系人:{{ item.user_nickname }}
  44. </text>
  45. <text class="tag-pill green" v-if="item.source === 1">
  46. 新客
  47. </text>
  48. </view>
  49. </view>
  50. <text class="service-price">
  51. ¥{{ item.project_amount }}
  52. </text>
  53. </view>
  54. <!-- Card Address: 地址 & 距离 -->
  55. <view class="address-section">
  56. <u-icon name="location" :size="18" />
  57. <text class="address-content">
  58. {{( item.address as UTSJSONObject)?.["address"] }}
  59. </text>
  60. <text class="distance-text">
  61. {{ item.distance }}km
  62. </text>
  63. </view>
  64. <!-- Card Income: 预估收入 -->
  65. <view class="income-section">
  66. <text class="income-label">
  67. 预估收入
  68. </text>
  69. <view class="income-value-group">
  70. <text class="income-main">
  71. ¥{{ item.commission_amount }}
  72. </text>
  73. <text class="income-sub">
  74. (含路费)
  75. </text>
  76. </view>
  77. </view>
  78. <!-- Card Actions: 按钮组 -->
  79. <view class="action-section" v-if="currentTab==1">
  80. <view class="btn btn-nav"
  81. @click.stop="onNavigate((item.address as UTSJSONObject)?.address as String) ">
  82. <u-icon name="navigation" :size="18" />
  83. <text style="font-size:28rpx">
  84. 地址导航
  85. </text>
  86. </view>
  87. <view style="flex-direction:row">
  88. <button class="btn btn-transfer" @click="onTransfer(item.id as number)">
  89. 我要转单
  90. </button>
  91. <button class="btn btn-confirm" @click="onConfirm(item.id as number)">
  92. 确认接单
  93. </button>
  94. </view>
  95. </view>
  96. <view class="action-section" v-else="currentTab==2">
  97. <view class="btn btn-nav"
  98. @click.stop="onNavigate((item.address as UTSJSONObject)?.address as String) ">
  99. <u-icon name="navigation" :size="18" />
  100. <text style="font-size:28rpx">
  101. 地址导航
  102. </text>
  103. </view>
  104. <view style="flex-direction:row">
  105. <button class="btn btn-transfer" @click="onTransfer(item.id as number)">
  106. 我要转单
  107. </button>
  108. <button class="btn btn-confirm" @click="onConfirm(item.id as number)">
  109. 确认接单
  110. </button>
  111. </view>
  112. </view>
  113. </view>
  114. </view>
  115. <!-- 加载状态 -->
  116. <view v-if="loading" class="loading">
  117. <loading style="border-width: 6rpx; border-color:#FFDA59" />
  118. </view>
  119. <view v-if="!hasMore && orderList.length > 0" class="no-more">
  120. <text style="font-size: 28rpx;
  121. color: #999999;
  122. text-align: center;">
  123. 没有更多数据了...
  124. </text>
  125. </view>
  126. </scroll-view>
  127. <!-- 空状态 -->
  128. <view v-else class="ss-flex-2" style="align-items: center;margin-top: 200rpx;">
  129. <image src="/static/other/order-k.png" class="wh">
  130. </image>
  131. <text style="font-size: 30rpx;color: #999999;letter-spacing: 2rpx;text-align: center;font-weight: 600;">
  132. 暂无订单</text>
  133. </view>
  134. </view>
  135. </template>
  136. <script setup lang="ts">
  137. import { ref } from 'vue';
  138. import { getOrderList, transferOrder, acceptOrder } from '@/utils/api/order';
  139. // --- 数据 ---
  140. const tabs = ref([
  141. {
  142. label: '新订单',
  143. value: 1
  144. },
  145. {
  146. label: '进行中',
  147. value: 2
  148. },
  149. {
  150. label: '取消/售后',
  151. value: 5
  152. },
  153. {
  154. label: '已完成',
  155. value: 3
  156. },
  157. {
  158. label: '全部',
  159. value: 0
  160. },
  161. ])
  162. const currentTab = ref(1);
  163. type OrderTag = {
  164. text : string;
  165. type : 'orange' | 'green' | 'red';
  166. }
  167. const orderList = ref<UTSJSONObject[]>([]);
  168. const pageNo = ref(1);
  169. const pageSize = ref(5);
  170. const hasMore = ref(true);
  171. const loading = ref(false);
  172. // --- 方法 ---
  173. // helper used in template to give v-for a typed array source
  174. // function tagList(order : OrderItem) : OrderTag[] {
  175. // return order.tags;
  176. // }
  177. //计算预估收入
  178. const estimateManey = (item) => {
  179. // 技师等级和星级
  180. const coach_level = Number(coachInfo.value?.coach_level) ?? 1; // 加钟等级
  181. const star_level = Number(coachInfo.value?.star_level) ?? 3; // 星星等级
  182. let project_amount = (Number(item?.project_amount) ?? 0) * (item?.num - 0 ?? 1); // 项目金额
  183. const traffic_amount = Number(item?.traffic_amount) ?? 0; // 路费
  184. // 星级系数表(1-5星)
  185. const starArr = [0,
  186. 0.45,
  187. 0.48,
  188. 0.5,
  189. 0.55,
  190. 0.58];
  191. let maneyInfo = 0;
  192. //是否是vip订单
  193. const isVip = item.user?.member_type && item.user?.member_type - 0 > 0 ? true : false;
  194. if (!isVip) {
  195. if (item.click_farming) {
  196. // 刷单
  197. maneyInfo = project_amount * 0.7;
  198. } else if (item?.type === 1) {
  199. // 首钟
  200. const starRate = starArr[star_level] ?? 0.5;
  201. maneyInfo = project_amount * starRate + traffic_amount;
  202. } else if (item?.type === 3) {
  203. // 加钟
  204. maneyInfo = project_amount * (0.5 + coach_level / 10);
  205. } else {
  206. maneyInfo = 0;
  207. }
  208. } else {
  209. if (item.click_farming) {
  210. // 刷单
  211. maneyInfo = project_amount * 0.5;
  212. } else if (item?.type === 1) {
  213. // 首钟
  214. maneyInfo = project_amount * 0.5 + traffic_amount;
  215. } else if (item?.type === 3) {
  216. // 加钟
  217. maneyInfo = project_amount * 0.7;
  218. } else {
  219. maneyInfo = 0;
  220. }
  221. }
  222. // 保留两位小数
  223. return maneyInfo.toFixed(2);
  224. };
  225. // 加载订单列表
  226. const httpGetOrderList = async (isLoadMore : boolean) => {
  227. if (!hasMore.value || loading.value) return;
  228. loading.value = true;
  229. try {
  230. const response = await getOrderList({
  231. type: currentTab.value,
  232. page: pageNo.value,
  233. per_page: pageSize.value
  234. }) as UTSJSONObject;
  235. const code = response["code"] as number;
  236. if (code !== 200) return;
  237. const items = (response.data as UTSJSONObject)["items"] as UTSArray<UTSJSONObject>;
  238. if (isLoadMore) {
  239. orderList.value.push(...items);
  240. } else {
  241. orderList.value = items;
  242. }
  243. // 判断是否还有更多数据
  244. if (items.length < pageSize.value) {
  245. hasMore.value = false;
  246. } else {
  247. pageNo.value++;
  248. }
  249. } catch (err : any) {
  250. console.error('获取订单列表接口异常', err);
  251. } finally {
  252. loading.value = false;
  253. }
  254. };
  255. // 处理tab点击事件
  256. const handleTabClick = (index : number) => {
  257. currentTab.value = index;
  258. // 切换标签时重置分页状态
  259. pageNo.value = 1;
  260. hasMore.value = true;
  261. orderList.value = [];
  262. httpGetOrderList(false);
  263. };
  264. // 滚动到底部加载更多
  265. const loadMore = () => {
  266. httpGetOrderList(true);
  267. };
  268. // 客户接单接口
  269. const onConfirmOrder = async (orderId : number) => {
  270. // uni.showLoading({ title: '处理中...' });
  271. try {
  272. const res = await acceptOrder({
  273. order_id: orderId
  274. }) as UTSJSONObject;
  275. // const code = response["code"] as number
  276. uni.hideLoading();
  277. if (res?.code === 200) {
  278. uni.showToast({ title: '接单成功', icon: 'success' });
  279. // 可选: 自动刷新订单列表或做其他操作
  280. await httpGetOrderList(false);
  281. } else {
  282. uni.showToast({
  283. title: (res?.msg) as String ?? '接单失败',
  284. icon: 'none'
  285. });
  286. }
  287. } catch (err) {
  288. uni.hideLoading();
  289. console.error('接单异常', err);
  290. uni.showToast({
  291. title: '接单失败,请重试',
  292. icon: 'none'
  293. });
  294. }
  295. };
  296. const onConfirm = (orderId : number) => {
  297. uni.showModal({
  298. title: '接单确认',
  299. content: '确定要接受该订单吗?',
  300. success: (res) => { onConfirmOrder(orderId) }
  301. });
  302. };
  303. // 商户转单
  304. const httptransferOrder = async (orderId : number,) => {
  305. try {
  306. const res = await transferOrder({
  307. order_id: orderId,
  308. }) as UTSJSONObject;
  309. if (res?.code === 200) {
  310. uni.showToast({ title: '转单成功', icon: 'success' });
  311. await httpGetOrderList(false);
  312. } else {
  313. uni.showToast({
  314. title: (res?.msg) as String ?? '转单失败',
  315. icon: 'none'
  316. });
  317. }
  318. } catch (err) {
  319. console.error('转单异常', err);
  320. uni.showToast({
  321. title: '转单失败,请重试',
  322. icon: 'none'
  323. });
  324. }
  325. };
  326. const onTransfer = (id : number) => {
  327. uni.showModal({
  328. title: '转单确认',
  329. content: '确定将此订单转给其他技师吗?',
  330. success: (res) => {
  331. httptransferOrder(id);
  332. }
  333. });
  334. };
  335. const orderDetail = (orderId : number) => {
  336. uni.navigateTo({
  337. url: `/pages/order/orderDetail?orderId=${orderId}`
  338. });
  339. };
  340. const onNavigate = (addr : string) => {
  341. uni.showToast({ title: '启动导航', icon: 'none' });
  342. uni.navigateTo({
  343. url: '/pages/map/map'
  344. });
  345. };
  346. onLoad(() => {
  347. // httpGetOrderList();
  348. })
  349. onReady(() => {
  350. httpGetOrderList(false);
  351. })
  352. </script>
  353. <style scoped>
  354. .page-container {
  355. background-color: #f5f6f8;
  356. box-sizing: border-box;
  357. height: 100%;
  358. }
  359. .tab-scroll {
  360. background-color: #ffffff;
  361. height: 88rpx;
  362. width: 100%;
  363. }
  364. .tab-wrapper {
  365. flex-direction: row;
  366. align-items: center;
  367. height: 88rpx;
  368. padding: 0 20rpx;
  369. }
  370. .tab-item {
  371. position: relative;
  372. padding: 0 30rpx;
  373. height: 88rpx;
  374. align-items: center;
  375. justify-content: center;
  376. font-size: 30rpx;
  377. color: #666666;
  378. flex-shrink: 0
  379. }
  380. .tab-item.active {
  381. color: #333333;
  382. font-weight: bold;
  383. }
  384. .tab-indicator {
  385. position: absolute;
  386. bottom: 16rpx;
  387. left: 0;
  388. right: 0;
  389. margin: 0 auto;
  390. width: 40rpx;
  391. height: 6rpx;
  392. background-color: #ffc107;
  393. border-radius: 3rpx;
  394. }
  395. .order-list {
  396. padding: 20rpx;
  397. }
  398. .order-card {
  399. background-color: #ffffff;
  400. border-radius: 16rpx;
  401. padding: 30rpx;
  402. margin-bottom: 20rpx;
  403. }
  404. .card-header {
  405. justify-content: space-between;
  406. align-items: center;
  407. flex-direction: row;
  408. }
  409. .time-text {
  410. font-size: 28rpx;
  411. color: #333333;
  412. font-weight: 400;
  413. }
  414. .status-badge {
  415. font-size: 24rpx;
  416. padding: 6rpx 16rpx;
  417. border-radius: 20rpx;
  418. }
  419. .status-badge.paid {
  420. background-color: #fff7e6;
  421. color: #ff9900;
  422. }
  423. .service-section {
  424. flex-direction: row;
  425. align-items: center;
  426. }
  427. .service-image {
  428. width: 110rpx;
  429. height: 110rpx;
  430. border-radius: 12rpx;
  431. background-color: #f0f0f0;
  432. flex-shrink: 0;
  433. }
  434. .service-details {
  435. flex: 1;
  436. justify-content: space-between;
  437. }
  438. .service-title-row {
  439. flex-direction: row;
  440. align-items: center;
  441. }
  442. .service-name {
  443. font-size: 32rpx;
  444. font-weight: bold;
  445. color: #333333;
  446. }
  447. .service-price {
  448. font-size: 34rpx;
  449. font-weight: bold;
  450. color: #333333;
  451. }
  452. .tags-container {
  453. flex-direction: row;
  454. flex-wrap: wrap;
  455. }
  456. .tag-pill {
  457. font-size: 22rpx;
  458. padding: 4rpx 12rpx;
  459. border-radius: 20rpx;
  460. border-width: 1rpx;
  461. border-style: solid;
  462. line-height: 1.2;
  463. margin-right: 10rpx;
  464. }
  465. .tag-pill.orange {
  466. color: #ff9900;
  467. border-color: #ff9900;
  468. background-color: #fffaf0;
  469. }
  470. .tag-pill.green {
  471. color: #52c41a;
  472. border-color: #52c41a;
  473. background-color: #f6ffed;
  474. }
  475. .tag-pill.red {
  476. color: #ff4d4f;
  477. border-color: #ff4d4f;
  478. background-color: #fff1f0;
  479. }
  480. .contact-info-row {
  481. flex-direction: row;
  482. align-items: center;
  483. }
  484. .contact-info {
  485. font-size: 26rpx;
  486. color: #999999;
  487. margin-left: 10rpx;
  488. }
  489. .address-section {
  490. flex-direction: row;
  491. align-items: flex-start;
  492. padding-bottom: 20rpx;
  493. border-bottom-width: 1rpx;
  494. border-bottom-color: #f5f5f5;
  495. border-bottom-style: solid;
  496. }
  497. .address-content {
  498. flex: 1;
  499. font-size: 26rpx;
  500. color: #666666;
  501. line-height: 1.4;
  502. }
  503. .distance-text {
  504. font-size: 24rpx;
  505. color: #999999;
  506. white-space: nowrap;
  507. margin-left: 10rpx;
  508. }
  509. .income-section {
  510. flex-direction: row;
  511. justify-content: space-between;
  512. align-items: center;
  513. }
  514. .income-label {
  515. font-size: 28rpx;
  516. color: #666666;
  517. }
  518. .income-value-group {
  519. flex-direction: row;
  520. align-items: center;
  521. }
  522. .income-main {
  523. font-size: 36rpx;
  524. font-weight: bold;
  525. color: #ff4d4f;
  526. }
  527. .income-sub {
  528. font-size: 24rpx;
  529. color: #999999;
  530. }
  531. .action-section {
  532. flex-direction: row;
  533. justify-content: space-between;
  534. }
  535. .btn {
  536. height: 72rpx;
  537. border-radius: 36rpx;
  538. width: 170rpx;
  539. padding: 0 10rpx;
  540. }
  541. .btn-nav {
  542. background-color: #FFFBEF;
  543. flex-direction: row;
  544. justify-content: center;
  545. align-items: center;
  546. }
  547. .btn-transfer {
  548. background-color: #ffffff;
  549. color: #ff9900;
  550. font-size: 28rpx;
  551. }
  552. .btn-confirm {
  553. background-color: #ffc107;
  554. color: #333333;
  555. font-size: 28rpx;
  556. margin-left: 20rpx
  557. }
  558. .loading {
  559. text-align: center;
  560. justify-content: center;
  561. align-items: center;
  562. }
  563. </style>