index.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  1. <template>
  2. <view class="my-page">
  3. <!-- 顶部背景墙 -->
  4. <view class="header-bg">
  5. <image src="/static/images/my-header.png" class="header-img" mode="widthFix"></image>
  6. <view class="user-block" @click="goToLogin" @longpress="onDebugTap">
  7. <image class="user-avatar" :src="userInfo?.avatarUrl || '/static/images/default-avatar.png'"
  8. mode="aspectFill"></image>
  9. <view class="user-info">
  10. <text class="user-name">{{ userInfo ? userInfo.nickName : '点击登录' }}</text>
  11. <text class="user-desc-text">{{ userInfo ? (userInfo.remark || '这位用户很懒,什么都没写 🐾') : '登录后享受更多权益 🐾'
  12. }}</text>
  13. </view>
  14. <view v-if="!userInfo" class="css-right-arrow-gold"></view>
  15. </view>
  16. </view>
  17. <!-- 我的服务订单区 -->
  18. <view class="cute-card order-wrap">
  19. <view class="card-head">
  20. <text class="card-title">我的服务订单</text>
  21. <view class="card-more" @click="goToOrder('all')">
  22. <text>查看全部</text>
  23. <view class="css-right-arrow-small"></view>
  24. </view>
  25. </view>
  26. <view class="order-nav">
  27. <view class="nav-item" v-for="item in orderItems" :key="item.key" @click="goToOrder(item.key)">
  28. <view class="icon-bulb">
  29. <image class="custom-icon" :src="item.icon" mode="aspectFit"></image>
  30. </view>
  31. <text class="nav-label">{{ item.label }}</text>
  32. </view>
  33. </view>
  34. </view>
  35. <!-- 服务与工具 -->
  36. <view class="cute-card tool-wrap">
  37. <view class="card-head">
  38. <text class="card-title">服务与工具</text>
  39. </view>
  40. <view class="tool-grid">
  41. <view class="tool-item" v-for="item in menuItems" :key="item.path || item.title" @click="goToMenu(item)">
  42. <image class="custom-icon tool-icon" :src="item.icon" mode="aspectFit"></image>
  43. <text class="tool-text">{{ item.title }}</text>
  44. </view>
  45. </view>
  46. </view>
  47. <view class="footer-msg">
  48. <text>~ 感谢您的陪伴 ~</text>
  49. </view>
  50. <!-- 客服中心弹窗 @Author: Antigravity -->
  51. <view class="service-popup-mask" v-if="showServicePopup" @click="closeServicePopup">
  52. <view class="service-popup" @click.stop>
  53. <view class="service-header">
  54. <text class="service-title">联系客服</text>
  55. <view class="css-close-btn" @click="closeServicePopup"></view>
  56. </view>
  57. <view class="qr-section">
  58. <text class="qr-title">客服二维码</text>
  59. <image class="qr-img" :src="customerSetting.qrCodeUrl || '/static/images/logo.png'" mode="aspectFit" @click="previewQRCode">
  60. </image>
  61. <text class="qr-desc">点击查看大图</text>
  62. </view>
  63. <view class="service-list">
  64. <view class="service-row">
  65. <view class="service-icon-box online-bg">
  66. <image class="service-icon-img" src="/static/icon/my/customerservice/online.png" mode="aspectFit"></image>
  67. </view>
  68. <view class="service-info">
  69. <text class="service-name">在线客服</text>
  70. <text class="service-desc">{{ customerSetting.wechatAccount || '企业微信专属客服在线解答' }}</text>
  71. </view>
  72. <view class="call-btn-mini green-btn" @click="openOnlineService">
  73. <text>去咨询</text>
  74. </view>
  75. </view>
  76. <view class="service-row">
  77. <view class="service-icon-box phone-bg">
  78. <image class="service-icon-img" src="/static/icon/my/customerservice/phone.png" mode="aspectFit"></image>
  79. </view>
  80. <view class="service-info">
  81. <text class="service-name">客服电话</text>
  82. <text class="service-desc">{{ customerSetting.phoneNumber || '暂无电话' }}</text>
  83. </view>
  84. <view class="call-btn-mini orange-btn" @click="callServicePhone">
  85. <text>拨打</text>
  86. </view>
  87. </view>
  88. </view>
  89. </view>
  90. </view>
  91. <custom-tabbar></custom-tabbar>
  92. </view>
  93. </template>
  94. <script>
  95. // @Author: Antigravity
  96. import { getInfo } from '@/api/system/user'
  97. import { getCustomerServiceSetting } from '@/api/system/customerServiceSetting'
  98. import customTabbar from '@/components/custom-tabbar/index.vue'
  99. import orderStatusData from '@/json/orderStatus.json'
  100. // 订单状态图标映射
  101. const iconMap = {
  102. 0: '/static/images/my-pendingdispatch.png',
  103. 1: '/static/images/my-pendingaccept.png',
  104. 2: '/static/images/my-inservice.png',
  105. 3: '/static/images/my-pendingservice.png',
  106. 4: '/static/images/my-finished.png',
  107. 5: '/static/images/my-cancel.png'
  108. }
  109. export default {
  110. components: { customTabbar },
  111. data() {
  112. return {
  113. userInfo: null,
  114. showServicePopup: false,
  115. debugTapCount: 0, // 调试触发计数器
  116. customerSetting: {
  117. wechatAccount: '',
  118. phoneNumber: '',
  119. qrCode: '',
  120. qrCodeUrl: '',
  121. enterpriseWechatLink: ''
  122. },
  123. // 订单状态导航
  124. orderItems: orderStatusData.map(item => ({
  125. key: item.value,
  126. label: item.label,
  127. icon: iconMap[item.value] || '/static/images/my-pendingdispatch.png'
  128. })),
  129. // 功能菜单
  130. menuItems: [
  131. { title: '宠物档案', icon: '/static/images/my-pet.png', path: '/pages/my/pet/list/index' },
  132. { title: '用户管理', icon: '/static/images/my-customer.png', path: '/pages/my/user/list/index' },
  133. { title: '投诉管理', icon: '/static/images/my-complaint.png', path: '/pages/my/complaint/list/index' },
  134. { title: '服务费统计', icon: '/static/images/my-fee.png', path: '/pages/my/fee/statistics/index' },
  135. { title: '客服中心', icon: '/static/images/my-customerservice.png', path: '' },
  136. { title: '协议中心', icon: '/static/images/my-agreement.png', path: '/pages/my/agreement/list/index' },
  137. { title: '系统设置', icon: '/static/images/my-systemsetting.png', path: '/pages/my/settings/index' }
  138. ]
  139. }
  140. },
  141. onShow() {
  142. this.fetchUserInfo()
  143. this.fetchCustomerSetting()
  144. },
  145. methods: {
  146. // 获取当前登录用户信息
  147. async fetchUserInfo() {
  148. const token = uni.getStorageSync('token')
  149. if (token) {
  150. try {
  151. const res = await getInfo()
  152. if (res && res.user) {
  153. this.userInfo = res.user
  154. }
  155. } catch (error) {
  156. console.error('获取用户信息失败', error)
  157. }
  158. } else {
  159. this.userInfo = null
  160. }
  161. },
  162. // 获取客服配置 (ID=2)
  163. async fetchCustomerSetting() {
  164. try {
  165. const res = await getCustomerServiceSetting(2)
  166. if (res) {
  167. // 商户端 request 拦截器已处理 res.data,此处 res 即为配置对象
  168. Object.assign(this.customerSetting, res)
  169. }
  170. } catch (error) {
  171. console.error('获取客服配置失败', error)
  172. }
  173. },
  174. // 跳转到登录页(未登录时)
  175. goToLogin() {
  176. if (!this.userInfo) {
  177. uni.navigateTo({ url: '/pages/login/index' })
  178. }
  179. },
  180. // 跳转到订单列表
  181. goToOrder(statusValue) {
  182. if (statusValue === 'all') {
  183. uni.reLaunch({ url: '/pages/order/list/index' })
  184. } else {
  185. uni.reLaunch({ url: `/pages/order/list/index?status=${statusValue}` })
  186. }
  187. },
  188. // 菜单项点击处理
  189. goToMenu(item) {
  190. if (item.title === '客服中心') {
  191. this.showServicePopup = true
  192. return
  193. }
  194. if (item.path) {
  195. uni.navigateTo({ url: item.path })
  196. }
  197. },
  198. // 关闭客服弹窗
  199. closeServicePopup() {
  200. this.showServicePopup = false
  201. },
  202. // 预览客服二维码
  203. previewQRCode() {
  204. if (!this.customerSetting.qrCodeUrl) return
  205. uni.previewImage({
  206. urls: [this.customerSetting.qrCodeUrl]
  207. })
  208. },
  209. // 打开在线客服
  210. openOnlineService() {
  211. if (this.customerSetting.enterpriseWechatLink) {
  212. // #ifdef H5
  213. window.location.href = this.customerSetting.enterpriseWechatLink
  214. // #endif
  215. // #ifndef H5
  216. uni.setClipboardData({
  217. data: this.customerSetting.wechatAccount || this.customerSetting.enterpriseWechatLink,
  218. success: () => {
  219. uni.showToast({ title: '客服账号已复制,请在微信中添加', icon: 'none' })
  220. }
  221. })
  222. // #endif
  223. } else {
  224. uni.showToast({ title: '在线客服暂未配置', icon: 'none' })
  225. }
  226. },
  227. // 拨打客服电话(多级降级策略,兼容 Vite CLI 和 HBuilderX 原生两种打包方式)
  228. callServicePhone() {
  229. const phoneNumber = String(this.customerSetting.phoneNumber || '').trim()
  230. if (!phoneNumber) {
  231. uni.showToast({ title: '暂无客服电话', icon: 'none' })
  232. return
  233. }
  234. try {
  235. // 策略1:App Plus 原生 plus.device.dial(HBuilderX 原生项目可用)
  236. // eslint-disable-next-line no-undef
  237. if (typeof plus !== 'undefined' && plus.device && plus.device.dial) {
  238. // eslint-disable-next-line no-undef
  239. plus.device.dial(phoneNumber, false)
  240. return
  241. }
  242. // 策略2:tel: URI Scheme(适用于 Vite CLI 打包的 WebView App,Android/iOS 均会拦截并跳转拨号盘)
  243. if (typeof window !== 'undefined') {
  244. window.location.href = 'tel:' + phoneNumber
  245. return
  246. }
  247. // 策略3:最终兜底 uni.makePhoneCall
  248. uni.makePhoneCall({
  249. phoneNumber,
  250. fail: (err) => {
  251. uni.showModal({
  252. title: '拨号失败',
  253. content: JSON.stringify(err),
  254. showCancel: false
  255. })
  256. }
  257. })
  258. } catch (e) {
  259. uni.showModal({
  260. title: '拨号异常',
  261. content: e.message || String(e),
  262. showCancel: false
  263. })
  264. }
  265. },
  266. // 长按头像区备触发调试日志查看
  267. onDebugTap() {
  268. this.debugTapCount++
  269. if (this.debugTapCount >= 3) {
  270. this.debugTapCount = 0
  271. this.showDebugLog()
  272. }
  273. },
  274. // 读取并展示全局错误日志
  275. showDebugLog() {
  276. try {
  277. const logs = JSON.parse(uni.getStorageSync('__debug_logs') || '[]')
  278. // eslint-disable-next-line no-undef
  279. const plusAvail = typeof plus !== 'undefined' ? 'yes' : 'no'
  280. // eslint-disable-next-line no-undef
  281. const plusDial = (typeof plus !== 'undefined' && plus.device && plus.device.dial) ? 'yes' : 'no'
  282. const winAvail = typeof window !== 'undefined' ? 'yes' : 'no'
  283. const makePhoneType = typeof uni.makePhoneCall
  284. const baseInfo = `plus:${plusAvail} | plus.device.dial:${plusDial} | window:${winAvail} | makePhoneCall:${makePhoneType} | 日志:${logs.length}条`
  285. const logText = logs.length > 0
  286. ? logs.slice(0, 5).map(l => `${l.t}\n${l.msg}`).join('\n---\n')
  287. : '暂无错误日志'
  288. uni.showModal({
  289. title: '调试信息',
  290. content: `${baseInfo}\n\n${logText}`,
  291. showCancel: true,
  292. cancelText: '清队日志',
  293. confirmText: '关闭',
  294. success: (res) => {
  295. if (res.cancel) {
  296. uni.removeStorageSync('__debug_logs')
  297. uni.showToast({ title: '日志已清队', icon: 'none' })
  298. }
  299. }
  300. })
  301. } catch(e) {
  302. uni.showModal({ title: '读取日志失败', content: e.message || String(e), showCancel: false })
  303. }
  304. }
  305. }
  306. }
  307. </script>
  308. <style lang="scss" scoped>
  309. /* 保持原有样式并添加弹窗样式 @Author: Antigravity */
  310. .my-page {
  311. min-height: 100vh;
  312. background-color: #fcfaf5;
  313. padding-bottom: 140rpx;
  314. }
  315. /* ... 之前的样式保留,下文补充新样式 ... */
  316. .service-popup-mask {
  317. position: fixed;
  318. top: 0;
  319. left: 0;
  320. right: 0;
  321. bottom: 0;
  322. background-color: rgba(0, 0, 0, 0.6);
  323. z-index: 999;
  324. display: flex;
  325. justify-content: center;
  326. align-items: center;
  327. backdrop-filter: blur(4px);
  328. }
  329. .service-popup {
  330. width: 620rpx;
  331. background-color: #fff;
  332. border-radius: 48rpx;
  333. padding: 0;
  334. overflow: hidden;
  335. box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.1);
  336. animation: slide-in 0.3s cubic-bezier(0.18, 0.89, 0.32, 1.28);
  337. }
  338. @keyframes slide-in {
  339. from { transform: scale(0.85); opacity: 0; }
  340. to { transform: scale(1); opacity: 1; }
  341. }
  342. .service-header {
  343. display: flex;
  344. justify-content: space-between;
  345. align-items: center;
  346. padding: 40rpx;
  347. border-bottom: 2rpx solid #EEEEEE;
  348. position: relative;
  349. }
  350. .service-title {
  351. font-size: 32rpx;
  352. font-weight: 800;
  353. color: #4a3e2e;
  354. }
  355. /* CSS 关闭叉号 @Author: Antigravity */
  356. .css-close-btn {
  357. width: 44rpx;
  358. height: 44rpx;
  359. position: relative;
  360. &::before, &::after {
  361. content: '';
  362. position: absolute;
  363. top: 20rpx;
  364. left: 8rpx;
  365. width: 28rpx;
  366. height: 4rpx;
  367. background-color: #ccc;
  368. transform: rotate(45deg);
  369. border-radius: 4rpx;
  370. }
  371. &::after { transform: rotate(-45deg); }
  372. &:active { opacity: 0.6; }
  373. }
  374. .qr-section {
  375. padding: 48rpx 0;
  376. display: flex;
  377. flex-direction: column;
  378. align-items: center;
  379. background: linear-gradient(180deg, #fff 0%, #fffbf2 100%);
  380. }
  381. .qr-title {
  382. font-size: 28rpx;
  383. font-weight: 700;
  384. color: #6d5b45;
  385. margin-bottom: 30rpx;
  386. }
  387. .qr-img {
  388. width: 320rpx;
  389. height: 320rpx;
  390. border-radius: 32rpx;
  391. padding: 16rpx;
  392. background: #fff;
  393. box-shadow: 0 8rpx 32rpx rgba(247, 202, 62, 0.15);
  394. margin-bottom: 20rpx;
  395. }
  396. .qr-desc {
  397. font-size: 24rpx;
  398. color: #a39686;
  399. font-weight: 600;
  400. }
  401. .service-list {
  402. padding: 20rpx 40rpx 40rpx;
  403. }
  404. .service-row {
  405. display: flex;
  406. align-items: center;
  407. padding: 32rpx 0;
  408. border-bottom: 2rpx solid #EEEEEE;
  409. &:last-child { border-bottom: none; }
  410. }
  411. .service-icon-box {
  412. width: 88rpx;
  413. height: 88rpx;
  414. border-radius: 28rpx;
  415. display: flex;
  416. align-items: center;
  417. justify-content: center;
  418. margin-right: 24rpx;
  419. &.online-bg {
  420. background: linear-gradient(135deg, #71d192 0%, #4caf50 100%);
  421. box-shadow: 0 8rpx 16rpx rgba(76, 175, 80, 0.25);
  422. }
  423. &.phone-bg {
  424. background: linear-gradient(135deg, #ffcc33 0%, #f7ca3e 100%);
  425. box-shadow: 0 8rpx 16rpx rgba(247, 202, 62, 0.25);
  426. }
  427. }
  428. .service-icon-img {
  429. width: 56rpx;
  430. height: 56rpx;
  431. }
  432. .service-info {
  433. flex: 1;
  434. }
  435. .service-name {
  436. display: block;
  437. font-size: 28rpx;
  438. font-weight: 800;
  439. color: #4a3e2e;
  440. margin-bottom: 4rpx;
  441. }
  442. .service-desc {
  443. display: block;
  444. font-size: 22rpx;
  445. color: #a39686;
  446. font-weight: 600;
  447. }
  448. .call-btn-mini {
  449. padding: 10rpx 26rpx;
  450. border-radius: 20rpx;
  451. font-size: 24rpx;
  452. font-weight: 700;
  453. transition: transform 0.2s;
  454. background: transparent;
  455. border: 2rpx solid transparent;
  456. &:active { transform: scale(0.95); }
  457. &.green-btn { border-color: #5ec686; color: #5ec686; }
  458. &.orange-btn { border-color: #f7ca3e; color: #f7ca3e; }
  459. }
  460. /* ====== 顶部背景图区域(替换原纯色背景) ====== */
  461. .header-bg {
  462. position: relative;
  463. padding-top: calc(var(--status-bar-height, 44px) + 20rpx);
  464. height: 440rpx;
  465. overflow: hidden;
  466. }
  467. .header-img {
  468. width: 100%;
  469. height: 100%;
  470. display: block;
  471. object-fit: cover;
  472. }
  473. .user-block {
  474. display: flex;
  475. align-items: center;
  476. position: absolute;
  477. top: 50%;
  478. transform: translateY(-50%);
  479. left: 40rpx;
  480. right: 40rpx;
  481. z-index: 2;
  482. }
  483. /* CSS 右箭头 (金色) @Author: Antigravity */
  484. .css-right-arrow-gold {
  485. width: 16rpx;
  486. height: 16rpx;
  487. border-right: 4rpx solid rgba(100, 70, 20, 0.4);
  488. border-top: 4rpx solid rgba(100, 70, 20, 0.4);
  489. transform: rotate(45deg);
  490. margin-left: auto;
  491. }
  492. /* CSS 右箭头 (小号) @Author: Antigravity */
  493. .css-right-arrow-small {
  494. width: 12rpx;
  495. height: 12rpx;
  496. border-right: 3rpx solid #a39686;
  497. border-top: 3rpx solid #a39686;
  498. transform: rotate(45deg);
  499. }
  500. .user-avatar {
  501. width: 140rpx;
  502. height: 140rpx;
  503. border: 8rpx solid #fff;
  504. border-radius: 50%;
  505. background-color: #fff;
  506. }
  507. .user-info {
  508. flex: 1;
  509. margin-left: 32rpx;
  510. }
  511. .user-name {
  512. display: block;
  513. font-size: 40rpx;
  514. font-weight: 800;
  515. color: #5c4314;
  516. margin-bottom: 12rpx;
  517. letter-spacing: 2rpx;
  518. }
  519. .user-desc-text {
  520. display: block;
  521. font-size: 24rpx;
  522. color: #5c4314;
  523. opacity: 0.85;
  524. font-weight: 600;
  525. }
  526. .cute-card {
  527. background: #fff;
  528. border-radius: 40rpx;
  529. padding: 40rpx;
  530. margin: 0 32rpx;
  531. box-shadow: 0 12rpx 40rpx rgba(220, 212, 196, 0.4);
  532. position: relative;
  533. z-index: 3;
  534. }
  535. .order-wrap {
  536. margin-top: -60rpx;
  537. border: 4rpx solid #fffdfa;
  538. }
  539. .card-head {
  540. display: flex;
  541. justify-content: space-between;
  542. align-items: center;
  543. margin-bottom: 32rpx;
  544. }
  545. .card-title {
  546. font-size: 30rpx;
  547. font-weight: 800;
  548. color: #4a3e2e;
  549. }
  550. .card-more {
  551. display: flex;
  552. align-items: center;
  553. gap: 10rpx;
  554. font-size: 24rpx;
  555. color: #a39686;
  556. font-weight: 600;
  557. }
  558. .order-nav {
  559. display: flex;
  560. justify-content: space-between;
  561. }
  562. .nav-item {
  563. display: flex;
  564. flex-direction: column;
  565. align-items: center;
  566. gap: 12rpx;
  567. }
  568. .icon-bulb {
  569. width: 84rpx;
  570. height: 84rpx;
  571. background: transparent;
  572. border-radius: 28rpx;
  573. display: flex;
  574. align-items: center;
  575. justify-content: center;
  576. }
  577. /* ====== 服务与工具网格卡片 ====== */
  578. .tool-wrap {
  579. margin-top: 40rpx;
  580. }
  581. .tool-grid {
  582. display: flex;
  583. flex-wrap: wrap;
  584. }
  585. .tool-item {
  586. width: 25%;
  587. display: flex;
  588. flex-direction: column;
  589. align-items: center;
  590. padding: 24rpx 0;
  591. gap: 10rpx;
  592. &:active {
  593. opacity: 0.7;
  594. }
  595. }
  596. .tool-icon {
  597. width: 52rpx;
  598. height: 52rpx;
  599. }
  600. .custom-icon {
  601. width: 48rpx;
  602. height: 48rpx;
  603. }
  604. .tool-text {
  605. font-size: 22rpx;
  606. color: #4a3e2e;
  607. font-weight: 600;
  608. }
  609. .nav-label {
  610. font-size: 22rpx;
  611. font-weight: 600;
  612. }
  613. .footer-msg {
  614. text-align: center;
  615. margin-top: 60rpx;
  616. margin-bottom: 40rpx;
  617. font-size: 24rpx;
  618. font-weight: 600;
  619. color: #d1c5b4;
  620. letter-spacing: 4rpx;
  621. }
  622. </style>