logic.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. import { getMyOrders, cancelOrderApi } from '@/api/order/subOrder'
  2. import { listAllService } from '@/api/service/list'
  3. import { reportGps } from '@/utils/gps'
  4. import customTabbar from '@/components/custom-tabbar/index.vue'
  5. export default {
  6. components: {
  7. customTabbar
  8. },
  9. data() {
  10. return {
  11. currentTab: 0,
  12. tabs: ['待接送/服务', '配送/服务中', '已完成', '已取消'],
  13. typeFilterOptions: ['全部类型'],
  14. currentTypeFilterIdx: 0,
  15. activeDropdown: 0,
  16. hasTimeFilter: false,
  17. currentMonth: '2026年2月',
  18. weekDays: ['日', '一', '二', '三', '四', '五', '六'],
  19. calendarDays: [],
  20. selectedDateRange: [],
  21. allOrderList: [],
  22. serviceList: [],
  23. searchContent: '',
  24. startServiceTime: '',
  25. endServiceTime: '',
  26. showPetModal: false,
  27. currentPetInfo: {},
  28. showNavModal: false,
  29. navTargetItem: null,
  30. navTargetPointType: '',
  31. activeCallItem: null,
  32. showRemarkInput: false,
  33. remarkText: ''
  34. }
  35. },
  36. created() {
  37. this.initCalendar();
  38. },
  39. async onLoad() {
  40. await this.loadServiceList()
  41. await this.loadOrders()
  42. // 显式请求一次定位授权
  43. reportGps(true).catch(e => console.log('Init GPS check skipped', e));
  44. },
  45. onShow() {
  46. uni.hideTabBar()
  47. // 此处不需要重复调用,因为逻辑可能在onLoad已处理,
  48. // 或者如果需要每次进入都刷新,可以保留,但需确保顺序
  49. this.loadOrders()
  50. },
  51. async onPullDownRefresh() {
  52. try {
  53. await this.loadServiceList()
  54. await this.loadOrders()
  55. } finally {
  56. uni.stopPullDownRefresh()
  57. }
  58. },
  59. watch: {
  60. currentTab() {
  61. this.loadOrders()
  62. },
  63. currentTypeFilterIdx() {
  64. this.loadOrders()
  65. },
  66. searchContent() {
  67. // 搜索内容变化时,自动重新加载订单
  68. this.loadOrders()
  69. }
  70. },
  71. computed: {
  72. orderList() {
  73. return this.allOrderList;
  74. }
  75. },
  76. methods: {
  77. async loadServiceList() {
  78. try {
  79. const res = await listAllService()
  80. this.serviceList = res.data || []
  81. this.typeFilterOptions = ['全部类型', ...this.serviceList.map(s => s.name)]
  82. } catch (err) {
  83. console.error('获取服务类型失败:', err)
  84. }
  85. },
  86. async loadOrders() {
  87. try {
  88. const statusMap = { 0: 2, 1: 3, 2: 4, 3: 5 }
  89. const serviceId = this.currentTypeFilterIdx > 0 ? this.serviceList[this.currentTypeFilterIdx - 1]?.id : undefined
  90. const params = {
  91. status: statusMap[this.currentTab],
  92. content: this.searchContent || undefined,
  93. service: serviceId,
  94. startServiceTime: this.startServiceTime || undefined,
  95. endServiceTime: this.endServiceTime || undefined
  96. }
  97. console.log('订单列表请求参数:', params)
  98. const res = await getMyOrders(params)
  99. console.log('订单列表响应:', res)
  100. const orders = res.rows || []
  101. console.log('订单数量:', orders.length)
  102. this.allOrderList = orders.map(order => this.transformOrder(order, this.currentTab))
  103. } catch (err) {
  104. console.error('获取订单列表失败:', err)
  105. this.allOrderList = []
  106. }
  107. },
  108. transformOrder(order, tabIndex) {
  109. const service = this.serviceList.find(s => s.id === order.service)
  110. const serviceText = service?.name || '未知'
  111. const serviceIcon = service?.iconUrl || ''
  112. const mode = service?.mode || 0
  113. const isRoundTrip = mode === 1
  114. // 根据 Tab 索引强制指定状态文字,忽略后端缺失的 status 字段
  115. let statusText = '接单'
  116. if (tabIndex === 0) {
  117. statusText = '接单' // 待接送/服务
  118. } else if (tabIndex === 1) {
  119. statusText = isRoundTrip ? '出发' : '开始' // 配送/服务中
  120. } else if (tabIndex === 2) {
  121. statusText = '已完成' // 已完成
  122. } else if (tabIndex === 3) {
  123. statusText = '已拒绝' // 已拒绝
  124. }
  125. return {
  126. id: order.id,
  127. status: order.status, // 保存原始 status 用于判断权限
  128. type: isRoundTrip ? 1 : 2,
  129. typeText: serviceText,
  130. typeIcon: serviceIcon,
  131. statusText: statusText,
  132. price: (order.price / 100).toFixed(2),
  133. timeLabel: '服务时间',
  134. time: order.serviceTime || '',
  135. petAvatar: order.petAvatar || '/static/dog.png',
  136. petAvatarUrl: order.petAvatarUrl || '',
  137. petName: order.petName || '',
  138. petBreed: order.breed || '',
  139. startLocation: order.fromAddress || '暂无起点',
  140. startAddress: order.fromAddress || '',
  141. fromAddress: order.fromAddress || '',
  142. fromLat: order.fromLat,
  143. fromLng: order.fromLng,
  144. startDistance: '0km',
  145. endLocation: (order.customerName || '') + ' ' + (order.customerPhone || ''),
  146. endAddress: order.toAddress || '',
  147. toAddress: order.toAddress || '',
  148. toLat: order.toLat,
  149. toLng: order.toLng,
  150. customerPhone: order.customerPhone || '',
  151. endDistance: '0km',
  152. serviceContent: order.remark || '',
  153. remark: order.remark || ''
  154. }
  155. },
  156. getDisplayStatus(item) {
  157. if (item.statusText === '已完成') return '已完成';
  158. if (item.statusText === '已拒绝') return '已拒绝';
  159. if (item.statusText === '接单') {
  160. return item.type === 1 ? '待接送' : '待服务';
  161. }
  162. return item.type === 1 ? '配送中' : '服务中';
  163. },
  164. getStatusClass(item) {
  165. let display = this.getDisplayStatus(item);
  166. if (display === '已完成') return 'finish';
  167. if (display === '已拒绝') return 'reject';
  168. if (display === '配送中' || display === '服务中') return 'processing';
  169. return 'highlight';
  170. },
  171. goToDetail(item) {
  172. uni.navigateTo({ url: `/pages/orders/detail?id=${item.id}` });
  173. },
  174. showPetProfile(item) {
  175. this.currentPetInfo = {
  176. ...item,
  177. petGender: 'M',
  178. petAge: '2岁',
  179. petWeight: '15kg',
  180. petPersonality: '活泼亲人,精力旺盛',
  181. petHobby: '喜欢追飞盘,爱吃肉干',
  182. petRemark: '肠胃较弱,不能乱喂零食;出门易爆冲,请拉紧牵引绳。',
  183. petTags: ['拉响警报', '不能吃鸡肉', '精力旺盛'],
  184. petLogs: [
  185. { date: '2026-02-09 14:00', content: '今天遛弯拉了两次粑粑,精神状态很好。', recorder: '王阿姨' },
  186. { date: '2026-02-08 10:30', content: '有些挑食,剩了小半碗狗粮。', recorder: '李师傅' },
  187. { date: '2026-02-05 09:00', content: '建档。', recorder: '系统记录' }
  188. ]
  189. };
  190. this.showPetModal = true;
  191. },
  192. closePetProfile() {
  193. this.showPetModal = false;
  194. },
  195. openNavigation(item, pointType) {
  196. this.navTargetItem = item;
  197. this.navTargetPointType = pointType;
  198. this.showNavModal = true;
  199. },
  200. closeNavModal() {
  201. this.showNavModal = false;
  202. },
  203. chooseMap(mapType) {
  204. let item = this.navTargetItem;
  205. let pointType = this.navTargetPointType;
  206. // 起 -> fromAddress ; 终 -> toAddress
  207. let name = pointType === 'start' ? (item.fromAddress || '起点') : (item.toAddress || '终点');
  208. let address = pointType === 'start' ? (item.fromAddress || '起点地址') : (item.toAddress || '终点地址');
  209. let latitude = pointType === 'start' ? Number(item.fromLat) : Number(item.toLat);
  210. let longitude = pointType === 'start' ? Number(item.fromLng) : Number(item.toLng);
  211. this.showNavModal = false;
  212. // 统一定义打开地图的函数
  213. const navigateTo = (lat, lng, addrName, addrDesc) => {
  214. uni.openLocation({
  215. latitude: lat,
  216. longitude: lng,
  217. name: addrName,
  218. address: addrDesc || '无法获取详细地址',
  219. success: function () {
  220. console.log('打开导航成功: ' + mapType);
  221. },
  222. fail: function (err) {
  223. console.error('打开导航失败:', err);
  224. uni.showToast({ title: '打开地图失败', icon: 'none' });
  225. }
  226. });
  227. };
  228. // 如果有目标经纬度,直接打开
  229. if (latitude && longitude && !isNaN(latitude) && !isNaN(longitude)) {
  230. navigateTo(latitude, longitude, name, address);
  231. } else {
  232. // 如果没有经纬度,按照需求:使用自己当前的经纬度,然后搜索 fromAddress 或者 toAddress
  233. uni.showLoading({ title: '获取当前位置...', mask: true });
  234. reportGps(true).then(res => {
  235. uni.hideLoading();
  236. // 使用用户当前经纬度作为锚点打开地图,展示目标地址信息
  237. navigateTo(res.latitude, res.longitude, name, address);
  238. }).catch(err => {
  239. uni.hideLoading();
  240. console.error('获取地理位置失败:', err);
  241. // 具体的授权引导已在 reportGps 内部处理
  242. });
  243. }
  244. },
  245. toggleCallMenu(item) {
  246. if (this.activeCallItem === item) {
  247. this.activeCallItem = null;
  248. } else {
  249. this.activeCallItem = item;
  250. }
  251. },
  252. closeCallMenu() {
  253. this.activeCallItem = null;
  254. },
  255. doCall(type, item) {
  256. let phoneNum = '';
  257. const targetItem = item || this.activeCallItem;
  258. // 1. 获取电话号码
  259. if (type === 'merchant') {
  260. phoneNum = '18900008451';
  261. } else if (type === 'customer') {
  262. phoneNum = targetItem?.customerPhone;
  263. }
  264. // 2. 基础校验
  265. if (!phoneNum) {
  266. uni.showToast({ title: '未找到电话号码', icon: 'none' });
  267. this.activeCallItem = null;
  268. return;
  269. }
  270. // 3. 清洗号码 (去除空格、横杠等非数字字符)
  271. phoneNum = phoneNum.replace(/[^\d]/g, '');
  272. // 二次校验:确保清洗后仍有数字
  273. if (phoneNum.length < 3) {
  274. uni.showToast({ title: '电话号码格式错误', icon: 'none' });
  275. this.activeCallItem = null;
  276. return;
  277. }
  278. console.log('正在发起直接呼叫:', phoneNum);
  279. // 4. 核心逻辑:区分环境处理
  280. // #ifdef APP-PLUS
  281. // App 端:使用 uni.makePhoneCall 直接发起呼叫
  282. uni.makePhoneCall({
  283. phoneNumber: phoneNum,
  284. success: () => {
  285. console.log('成功唤起系统拨号盘');
  286. },
  287. fail: (err) => {
  288. console.error('拨号失败:', err);
  289. // 常见错误:Permission denied (权限被拒) 或 Activity not found
  290. let msg = '拨号失败';
  291. if (err.message && err.message.includes('permission')) {
  292. msg = '请在手机设置中允许"电话"权限';
  293. }
  294. uni.showToast({ title: msg, icon: 'none', duration: 3000 });
  295. // 如果失败,尝试引导用户去设置页 (仅限 Android)
  296. // #ifdef APP-ANDROID
  297. if (err.message && err.message.includes('permission')) {
  298. uni.showModal({
  299. title: '权限提示',
  300. content: '拨打电话需要电话权限,是否前往设置开启?',
  301. success: (res) => {
  302. if (res.confirm) {
  303. plus.runtime.openURL("app-settings:");
  304. }
  305. }
  306. });
  307. }
  308. // #endif
  309. },
  310. complete: () => {
  311. this.activeCallItem = null; // 关闭弹窗
  312. }
  313. });
  314. // #endif
  315. // #ifdef H5
  316. // H5 端:使用 tel: 协议
  317. window.location.href = `tel:${phoneNum}`;
  318. this.activeCallItem = null;
  319. // #endif
  320. // #ifdef MP-WEIXIN
  321. // 小程序端:直接调用 makePhoneCall (微信小程序支持直接弹框确认拨打)
  322. uni.makePhoneCall({
  323. phoneNumber: phoneNum,
  324. fail: () => {
  325. uni.showToast({ title: '拨号失败', icon: 'none' });
  326. },
  327. complete: () => {
  328. this.activeCallItem = null;
  329. }
  330. });
  331. // #endif
  332. },
  333. reportAbnormal(item) {
  334. uni.navigateTo({ url: '/pages/orders/anomaly?orderId=' + (item.id || '') });
  335. },
  336. toggleDropdown(idx) {
  337. if (this.activeDropdown === idx) {
  338. this.activeDropdown = 0;
  339. } else {
  340. this.activeDropdown = idx;
  341. }
  342. },
  343. closeDropdown() {
  344. this.activeDropdown = 0;
  345. },
  346. selectType(index) {
  347. this.currentTypeFilterIdx = index;
  348. this.closeDropdown();
  349. },
  350. initCalendar() {
  351. let days = [];
  352. for (let i = 1; i <= 28; i++) {
  353. days.push(i);
  354. }
  355. this.calendarDays = days;
  356. this.selectedDateRange = [2, 4];
  357. },
  358. prevMonth() { uni.showToast({ title: '上个月', icon: 'none' }); },
  359. nextMonth() { uni.showToast({ title: '下个月', icon: 'none' }); },
  360. selectDateItem(day) {
  361. if (this.selectedDateRange.length === 2) {
  362. this.selectedDateRange = [day];
  363. } else if (this.selectedDateRange.length === 1) {
  364. let start = this.selectedDateRange[0];
  365. if (day > start) {
  366. this.selectedDateRange = [start, day];
  367. } else if (day < start) {
  368. this.selectedDateRange = [day, start];
  369. } else {
  370. this.selectedDateRange = [];
  371. }
  372. } else {
  373. this.selectedDateRange = [day];
  374. }
  375. },
  376. getDateClass(day) {
  377. if (this.selectedDateRange.length === 0) return '';
  378. if (this.selectedDateRange.length === 1) {
  379. return day === this.selectedDateRange[0] ? 'is-start' : '';
  380. }
  381. let start = this.selectedDateRange[0];
  382. let end = this.selectedDateRange[1];
  383. if (day === start) return 'is-start';
  384. if (day === end) return 'is-end';
  385. if (day > start && day < end) return 'is-between';
  386. return '';
  387. },
  388. resetTimeFilter() {
  389. this.hasTimeFilter = false;
  390. this.selectedDateRange = [];
  391. this.startServiceTime = '';
  392. this.endServiceTime = '';
  393. this.closeDropdown();
  394. this.loadOrders();
  395. },
  396. confirmTimeFilter() {
  397. if (this.selectedDateRange.length === 0) {
  398. uni.showToast({ title: '请先选择日期', icon: 'none' });
  399. return;
  400. }
  401. // 构建时间范围参数
  402. const year = this.currentMonth.replace(/[^0-9]/g, '').substring(0, 4);
  403. const month = this.currentMonth.replace(/[^0-9]/g, '').substring(4);
  404. const pad = (n) => String(n).padStart(2, '0');
  405. if (this.selectedDateRange.length === 2) {
  406. this.startServiceTime = `${year}-${pad(month)}-${pad(this.selectedDateRange[0])} 00:00:00`;
  407. this.endServiceTime = `${year}-${pad(month)}-${pad(this.selectedDateRange[1])} 23:59:59`;
  408. } else {
  409. this.startServiceTime = `${year}-${pad(month)}-${pad(this.selectedDateRange[0])} 00:00:00`;
  410. this.endServiceTime = `${year}-${pad(month)}-${pad(this.selectedDateRange[0])} 23:59:59`;
  411. }
  412. this.hasTimeFilter = true;
  413. this.closeDropdown();
  414. this.loadOrders();
  415. },
  416. getMainActionText(item) {
  417. return '查看详情';
  418. },
  419. mainAction(item) {
  420. uni.navigateTo({ url: `/pages/orders/detail?id=${item.id}` });
  421. },
  422. openRemarkInput() {
  423. this.remarkText = '';
  424. this.showRemarkInput = true;
  425. },
  426. closeRemarkInput() {
  427. this.showRemarkInput = false;
  428. this.remarkText = '';
  429. },
  430. submitRemark() {
  431. const text = this.remarkText.trim();
  432. if (!text) {
  433. uni.showToast({ title: '备注内容不能为空', icon: 'none' });
  434. return;
  435. }
  436. const now = new Date();
  437. const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
  438. if (!this.currentPetInfo.petLogs) {
  439. this.$set(this.currentPetInfo, 'petLogs', []);
  440. }
  441. this.currentPetInfo.petLogs.unshift({
  442. date: dateStr,
  443. content: text,
  444. recorder: '我'
  445. });
  446. uni.showToast({ title: '备注已添加', icon: 'success' });
  447. this.closeRemarkInput();
  448. },
  449. /**
  450. * 取消订单处理逻辑
  451. * @param {Object} item - 订单项
  452. */
  453. handleCancelOrder(item) {
  454. uni.showModal({
  455. title: '提示',
  456. content: '确认是否取消这个订单?',
  457. success: async (res) => {
  458. if (res.confirm) {
  459. try {
  460. uni.showLoading({ title: '取消中...', mask: true });
  461. await cancelOrderApi({ orderId: item.id });
  462. uni.showToast({ title: '订单已取消', icon: 'success' });
  463. // 延时刷新列表,防止提示框闪现
  464. setTimeout(() => {
  465. this.loadOrders();
  466. }, 1500);
  467. } catch (err) {
  468. console.error('取消订单失败:', err);
  469. uni.showToast({ title: '取消失败', icon: 'none' });
  470. } finally {
  471. uni.hideLoading();
  472. }
  473. }
  474. }
  475. });
  476. }
  477. }
  478. }