logic.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. import { getMyProfile } from '@/api/fulfiller/fulfiller'
  2. import { getPendingOrders, acceptOrder, getOrderCount, rejectOrderApi } from '@/api/order/subOrder'
  3. import { listAllService } from '@/api/service/list'
  4. import { getAreaStationList } from '@/api/system/areaStation'
  5. import { isLoggedIn } from '@/utils/auth'
  6. import { reportGps } from '@/utils/gps'
  7. import customTabbar from '@/components/custom-tabbar/index.vue'
  8. export default {
  9. components: {
  10. customTabbar
  11. },
  12. data() {
  13. return {
  14. taskList: [],
  15. currentFilter: 'default', // default, distance, time
  16. filterCondition: '筛选条件',
  17. sortDistance: 'asc', // asc, desc
  18. sortTime: 'asc',
  19. scrollTop: 0, // Track scroll position
  20. isFilterShow: false,
  21. tempFilter: {
  22. service: null,
  23. distance: '全部',
  24. amount: '全部'
  25. },
  26. activeFilter: {
  27. service: null,
  28. distance: '全部',
  29. amount: '全部'
  30. },
  31. workStatus: 'resting', // resting | busy | disabled
  32. showConfirmModal: false,
  33. showPetModal: false,
  34. currentPetInfo: {},
  35. showRejectModal: false,
  36. rejectReason: '',
  37. currentOrder: null,
  38. showAcceptConfirmModal: false,
  39. showNavModal: false,
  40. navTargetItem: null,
  41. navTargetPointType: '',
  42. profile: null,
  43. profileLoading: false,
  44. serviceList: [],
  45. orderStats: {
  46. total: 0,
  47. reject: 0,
  48. completed: 0,
  49. price: 0
  50. }
  51. }
  52. },
  53. onPageScroll(e) {
  54. this.scrollTop = e.scrollTop;
  55. },
  56. async onLoad() {
  57. // Initial load
  58. this.checkWorkStatus();
  59. await this.loadServiceList();
  60. this.loadTaskList();
  61. // 显式请求一次定位授权
  62. reportGps(true).catch(e => console.log('Init GPS check skipped', e));
  63. },
  64. onShow() {
  65. uni.hideTabBar()
  66. this.checkWorkStatus();
  67. if (isLoggedIn()) {
  68. // 每次进入页面强制刷新所有展示数据
  69. this.loadProfile()
  70. this.loadOrderStats()
  71. this.loadTaskList()
  72. this.loadServiceList() // 确保服务配置也是最新的
  73. }
  74. },
  75. async onPullDownRefresh() {
  76. this.checkWorkStatus();
  77. try {
  78. await this.loadServiceList();
  79. const tasks = [
  80. this.loadTaskList()
  81. ];
  82. if (isLoggedIn()) {
  83. tasks.push(this.loadProfile());
  84. tasks.push(this.loadOrderStats());
  85. }
  86. await Promise.all(tasks);
  87. } catch (err) {
  88. console.error('刷新异常:', err);
  89. } finally {
  90. uni.stopPullDownRefresh();
  91. uni.showToast({ title: '刷新成功', icon: 'success' });
  92. }
  93. },
  94. methods: {
  95. async loadProfile() {
  96. if (this.profileLoading) return
  97. this.profileLoading = true
  98. try {
  99. const res = await getMyProfile()
  100. const data = res.data || null
  101. if (data) {
  102. // 以服务器返回的状态为准进行更新
  103. if (data.status) {
  104. this.workStatus = data.status;
  105. uni.setStorageSync('workStatus', data.status);
  106. }
  107. // 需求:头部的接单城市使用站点往上找到城市,而非直接显示站点
  108. if (data.stationId) {
  109. try {
  110. const stationRes = await getAreaStationList();
  111. const list = stationRes.data || [];
  112. const currentStation = list.find(i => i.id === data.stationId);
  113. if (currentStation) {
  114. // 向上溯源:直到找到 parentId 为 0 的节点(即顶层城市)
  115. let node = currentStation;
  116. while (node && node.parentId !== 0) {
  117. let parent = list.find(i => i.id === node.parentId);
  118. if (parent) {
  119. node = parent;
  120. } else {
  121. break;
  122. }
  123. }
  124. // 将溯源到的节点名称作为显示城市
  125. data.cityName = node.name;
  126. }
  127. } catch (e) {
  128. console.error('溯源城市失败:', e);
  129. }
  130. }
  131. }
  132. this.profile = data
  133. } catch (err) {
  134. console.error('获取个人信息失败:', err)
  135. } finally {
  136. this.profileLoading = false
  137. }
  138. },
  139. async loadServiceList() {
  140. try {
  141. const res = await listAllService()
  142. this.serviceList = res.data || []
  143. } catch (err) {
  144. console.error('获取服务类型失败:', err)
  145. }
  146. },
  147. async loadOrderStats() {
  148. try {
  149. const res = await getOrderCount()
  150. this.orderStats = res.data || { total: 0, reject: 0, completed: 0, price: 0 }
  151. } catch (err) {
  152. console.error('获取订单统计失败:', err)
  153. }
  154. },
  155. checkWorkStatus() {
  156. const status = uni.getStorageSync('workStatus');
  157. if (status) {
  158. this.workStatus = status;
  159. } else {
  160. // 默认状态为休息
  161. this.workStatus = 'resting';
  162. uni.setStorageSync('workStatus', 'resting');
  163. }
  164. },
  165. toggleFilter() {
  166. if (this.workStatus === 'resting') return; // Disable filter when resting? Or keep it? User didn't specify, but usually disabled. Let's keep it enabled for now as they might look at filters before working.
  167. this.isFilterShow = !this.isFilterShow;
  168. },
  169. goToWorkStatus() {
  170. uni.navigateTo({
  171. url: '/pages/home/work-status'
  172. });
  173. },
  174. async handleManualLocation() {
  175. try {
  176. uni.showLoading({ title: '定位获取中...', mask: true });
  177. await reportGps(true);
  178. uni.showToast({ title: '位置已更新', icon: 'success' });
  179. } catch (e) {
  180. console.error('Manual location failed', e);
  181. } finally {
  182. uni.hideLoading();
  183. }
  184. },
  185. startWork() {
  186. this.showConfirmModal = true;
  187. },
  188. confirmStartWork() {
  189. this.workStatus = 'busy';
  190. uni.setStorageSync('workStatus', 'busy');
  191. this.loadTaskList();
  192. this.showConfirmModal = false;
  193. uni.showToast({ title: '已开始接单', icon: 'success' });
  194. },
  195. closeConfirmModal() {
  196. this.showConfirmModal = false;
  197. },
  198. showPetProfile(item) {
  199. this.currentPetInfo = item;
  200. this.showPetModal = true;
  201. },
  202. closePetProfile() {
  203. this.showPetModal = false;
  204. },
  205. openRejectModal(item) {
  206. this.currentOrder = item;
  207. this.rejectReason = '';
  208. this.showRejectModal = true;
  209. },
  210. closeRejectModal() {
  211. this.showRejectModal = false;
  212. this.currentOrder = null;
  213. },
  214. async confirmReject() {
  215. if (!this.rejectReason.trim()) {
  216. uni.showToast({ title: '请输入拒绝理由', icon: 'none' });
  217. return;
  218. }
  219. if (!this.currentOrder?.id) return
  220. try {
  221. uni.showLoading({ title: '提交中...', mask: true });
  222. await rejectOrderApi({
  223. orderId: this.currentOrder.id,
  224. rejectReason: this.rejectReason
  225. });
  226. uni.showToast({ title: '已拒绝接单', icon: 'success' });
  227. this.showRejectModal = false;
  228. this.currentOrder = null;
  229. this.loadTaskList();
  230. this.loadOrderStats();
  231. } catch (err) {
  232. console.error('拒绝接单失败:', err);
  233. uni.showToast({ title: '操作失败', icon: 'none' });
  234. } finally {
  235. uni.hideLoading();
  236. }
  237. },
  238. openAcceptModal(item) {
  239. this.currentOrder = item;
  240. this.showAcceptConfirmModal = true;
  241. },
  242. closeAcceptModal() {
  243. this.showAcceptConfirmModal = false;
  244. this.currentOrder = null;
  245. },
  246. async confirmAccept() {
  247. if (!this.currentOrder?.id) return
  248. try {
  249. await acceptOrder(this.currentOrder.id)
  250. uni.showToast({ title: '接单成功', icon: 'success' })
  251. this.showAcceptConfirmModal = false
  252. this.currentOrder = null
  253. this.loadTaskList()
  254. this.loadProfile()
  255. this.loadOrderStats()
  256. } catch (err) {
  257. console.error('接单失败:', err)
  258. uni.showToast({ title: '接单失败', icon: 'none' })
  259. }
  260. },
  261. openNavigation(item, pointType) {
  262. this.navTargetItem = item;
  263. this.navTargetPointType = pointType;
  264. this.showNavModal = true;
  265. },
  266. closeNavModal() {
  267. this.showNavModal = false;
  268. },
  269. chooseMap(mapType) {
  270. let item = this.navTargetItem;
  271. let pointType = this.navTargetPointType;
  272. // 起 -> fromAddress ; 终 -> toAddress
  273. let name = pointType === 'start' ? (item.fromAddress || '起点') : (item.toAddress || '终点');
  274. let address = pointType === 'start' ? (item.fromAddress || '起点地址') : (item.toAddress || '终点地址');
  275. let latitude = pointType === 'start' ? Number(item.fromLat) : Number(item.toLat);
  276. let longitude = pointType === 'start' ? Number(item.fromLng) : Number(item.toLng);
  277. this.showNavModal = false;
  278. // 统一定义打开地图的函数
  279. const navigateTo = (lat, lng, addrName, addrDesc) => {
  280. uni.openLocation({
  281. latitude: lat,
  282. longitude: lng,
  283. name: addrName,
  284. address: addrDesc || '无法获取详细地址',
  285. success: function () {
  286. console.log('打开导航成功: ' + mapType);
  287. },
  288. fail: function (err) {
  289. console.error('打开导航失败:', err);
  290. uni.showToast({ title: '打开地图失败', icon: 'none' });
  291. }
  292. });
  293. };
  294. // 如果有目标经纬度,直接打开
  295. if (latitude && longitude && !isNaN(latitude) && !isNaN(longitude)) {
  296. navigateTo(latitude, longitude, name, address);
  297. } else {
  298. // 如果没有经纬度,按照需求:使用自己当前的经纬度,然后搜索 fromAddress 或者 toAddress
  299. uni.showLoading({ title: '获取当前位置...', mask: true });
  300. uni.getLocation({
  301. type: 'gcj02',
  302. success: (res) => {
  303. uni.hideLoading();
  304. // 使用用户当前经纬度作为锚点打开地图,展示目标地址信息
  305. navigateTo(res.latitude, res.longitude, name, address);
  306. },
  307. fail: (err) => {
  308. uni.hideLoading();
  309. console.error('获取地理位置失败:', err);
  310. uni.showToast({ title: '无法获取当前位置信息', icon: 'none' });
  311. }
  312. });
  313. }
  314. },
  315. selectService(type) {
  316. this.tempFilter.service = type;
  317. },
  318. selectDistance(type) {
  319. this.tempFilter.distance = type;
  320. },
  321. selectAmount(type) {
  322. this.tempFilter.amount = type;
  323. },
  324. resetFilter() {
  325. this.tempFilter = {
  326. service: null,
  327. distance: '全部',
  328. amount: '全部'
  329. };
  330. },
  331. confirmFilter() {
  332. this.activeFilter = { ...this.tempFilter };
  333. this.isFilterShow = false;
  334. this.loadTaskList();
  335. },
  336. closeFilter() {
  337. this.isFilterShow = false;
  338. },
  339. goToDetail(item) {
  340. console.log('Go to detail', item);
  341. },
  342. async loadTaskList() {
  343. try {
  344. const params = {
  345. service: this.activeFilter.service,
  346. minPrice: this.getMinPrice(),
  347. maxPrice: this.getMaxPrice(),
  348. pageNum: 1,
  349. pageSize: 20
  350. }
  351. const res = await getPendingOrders(params)
  352. this.taskList = (res.rows || []).map(item => this.transformOrder(item))
  353. } catch (err) {
  354. console.error('获取订单列表失败:', err)
  355. uni.showToast({ title: '加载失败', icon: 'none' })
  356. this.taskList = []
  357. }
  358. },
  359. getMinPrice() {
  360. const amount = this.activeFilter.amount
  361. if (amount === '100以下') return 0
  362. if (amount === '100-200') return 10000
  363. if (amount === '200-500') return 20000
  364. if (amount === '500以上') return 50000
  365. return undefined
  366. },
  367. getMaxPrice() {
  368. const amount = this.activeFilter.amount
  369. if (amount === '100以下') return 10000
  370. if (amount === '100-200') return 20000
  371. if (amount === '200-500') return 50000
  372. return undefined
  373. },
  374. transformOrder(item) {
  375. const service = this.serviceList.find(s => s.id === item.service)
  376. const serviceText = service?.name || '未知'
  377. const serviceIcon = service?.iconUrl || ''
  378. const mode = service?.mode || 0
  379. const isRoundTrip = mode === 1
  380. return {
  381. id: item.id,
  382. type: isRoundTrip ? 1 : item.service,
  383. typeText: serviceText,
  384. typeIcon: serviceIcon,
  385. price: (item.price / 100).toFixed(2),
  386. timeLabel: '服务时间',
  387. time: item.serviceTime,
  388. petAvatar: item.petAvatar || '/static/dog.png',
  389. petAvatarUrl: item.petAvatarUrl || '',
  390. petName: item.petName,
  391. petBreed: item.breed,
  392. petGender: 'M',
  393. petAge: '',
  394. petWeight: '',
  395. petPersonality: '',
  396. petHobby: '',
  397. petRemark: '',
  398. petTags: [],
  399. petLogs: [],
  400. startLocation: item.fromAddress || '暂无起点',
  401. startAddress: item.fromAddress || '',
  402. fromAddress: item.fromAddress || '',
  403. fromLat: item.fromLat,
  404. fromLng: item.fromLng,
  405. startDistance: '0km',
  406. endLocation: (item.customerName || item.contact || '') + ' ' + (item.customerPhone || ''),
  407. endAddress: item.toAddress || '',
  408. toAddress: item.toAddress || '',
  409. toLat: item.toLat,
  410. toLng: item.toLng,
  411. endDistance: '0km',
  412. serviceContent: '',
  413. remark: item.remark || ''
  414. }
  415. },
  416. setFilter(type) {
  417. this.currentFilter = type;
  418. if (type === 'distance') {
  419. this.sortDistance = this.sortDistance === 'asc' ? 'desc' : 'asc';
  420. uni.showToast({ title: `按距离${this.sortDistance === 'asc' ? '升序' : '降序'}`, icon: 'none' });
  421. } else if (type === 'time') {
  422. this.sortTime = this.sortTime === 'asc' ? 'desc' : 'asc';
  423. uni.showToast({ title: `按时间${this.sortTime === 'asc' ? '升序' : '降序'}`, icon: 'none' });
  424. }
  425. },
  426. showFilterDropdown() {
  427. this.toggleFilter();
  428. }
  429. }
  430. }