index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. <template>
  2. <view class="container">
  3. <!-- 自定义头部 -->
  4. <view class="custom-header">
  5. <view class="header-left" @click="navBack">
  6. <image class="back-icon" src="/static/icons/chevron_right_dark.svg" style="transform: rotate(180deg);">
  7. </image>
  8. </view>
  9. <text class="header-title">个人资料</text>
  10. <view class="header-right"></view>
  11. </view>
  12. <view class="header-placeholder"></view>
  13. <view class="group-card">
  14. <view class="list-item" @click="changeAvatar">
  15. <text class="item-title">头像</text>
  16. <view class="item-right">
  17. <image class="user-avatar" :src="userInfo.avatar" mode="aspectFill"></image>
  18. <image class="arrow-icon" src="/static/icons/chevron_right.svg"></image>
  19. </view>
  20. </view>
  21. <view class="list-item" @click="editName">
  22. <text class="item-title">真实姓名</text>
  23. <view class="item-right">
  24. <text class="item-value">{{ userInfo.name }}</text>
  25. <image class="arrow-icon" src="/static/icons/chevron_right.svg"></image>
  26. </view>
  27. </view>
  28. </view>
  29. <view class="group-card">
  30. <view class="list-item" @click="showStatusPicker">
  31. <text class="item-title">工作状态</text>
  32. <view class="item-right">
  33. <text class="item-value-black">{{ userInfo.workStatus }}</text>
  34. <image class="arrow-icon" src="/static/icons/chevron_right.svg"></image>
  35. </view>
  36. </view>
  37. </view>
  38. <view class="group-card">
  39. <view class="list-item no-border" @click="showCityPicker">
  40. <text class="item-title">所属站点</text>
  41. <view class="item-right">
  42. <text class="item-value">{{ userInfo.stationFullName || '未分配站点' }}</text>
  43. <image class="arrow-icon" src="/static/icons/chevron_right.svg"></image>
  44. </view>
  45. </view>
  46. </view>
  47. <!-- 工作状态选择弹窗 -->
  48. <view class="popup-mask" v-if="isStatusPickerShow" @click="closeStatusPicker">
  49. <view class="popup-content" @click.stop>
  50. <view class="popup-title">选择工作状态</view>
  51. <view class="popup-item" @click="selectStatus('接单中')">接单中</view>
  52. <view class="popup-item" @click="selectStatus('休息中')">休息中</view>
  53. <view class="popup-cancel" @click="closeStatusPicker">取消</view>
  54. </view>
  55. </view>
  56. <!-- 城市站点选择弹窗 (级联版树形结构) -->
  57. <view class="popup-mask" v-if="isCityPickerShow" @click="closeCityPicker">
  58. <view class="popup-content" @click.stop>
  59. <view class="popup-header-row">
  60. <text class="popup-btn-cancel" @click="closeCityPicker">取消</text>
  61. <text class="popup-title-text">请选择工作城市和站点</text>
  62. <text class="popup-btn-confirm" @click="confirmCity">确定</text>
  63. </view>
  64. <view class="picker-body">
  65. <!-- 左侧:垂直路径 -->
  66. <view class="timeline-area">
  67. <view class="timeline-item" v-for="(item, index) in selectedPathway" :key="index"
  68. @click="jumpToStep(index)">
  69. <view class="timeline-dot"></view>
  70. <text>{{ item.name }}</text>
  71. </view>
  72. <view class="timeline-item active" v-if="selectStep === selectedPathway.length">
  73. <view class="timeline-dot"></view>
  74. <text>请选择</text>
  75. </view>
  76. </view>
  77. <!-- 右侧:待选项列表 -->
  78. <scroll-view scroll-y class="list-area">
  79. <view class="list-item" v-for="item in currentCityList" :key="item.id"
  80. @click="selectCityItem(item)">
  81. {{ item.name }}
  82. </view>
  83. <view v-if="currentCityList.length === 0" style="padding:20rpx;color:#999">
  84. 无数据
  85. </view>
  86. </scroll-view>
  87. </view>
  88. </view>
  89. </view>
  90. </view>
  91. </template>
  92. <script>
  93. // 引入 API
  94. import { getMyProfile, updateAvatar, updateStatus, updateCity } from '@/api/fulfiller/fulfiller'
  95. import { uploadFile } from '@/api/fulfiller/app'
  96. import { getAreaStationList } from '@/api/system/areaStation'
  97. export default {
  98. data() {
  99. return {
  100. userInfo: {
  101. name: '',
  102. workType: '',
  103. workStatus: '',
  104. city: '',
  105. avatar: '/static/touxiang.png',
  106. stationName: '',
  107. stationFullName: ''
  108. },
  109. isStatusPickerShow: false,
  110. isCityPickerShow: false,
  111. // 城市站点级联选择器
  112. selectStep: 0,
  113. selectedPathway: [],
  114. currentCityList: [],
  115. selectedCityId: null,
  116. fullTree: [] // 树形结构数据
  117. }
  118. },
  119. onLoad() {
  120. this.loadUserInfo();
  121. uni.$on('updateName', (newName) => {
  122. this.userInfo.name = newName;
  123. });
  124. },
  125. onUnload() {
  126. uni.$off('updateName');
  127. },
  128. methods: {
  129. // 加载用户信息
  130. async loadUserInfo() {
  131. uni.showLoading({ title: '加载中...' });
  132. try {
  133. const res = await getMyProfile();
  134. const data = res.data;
  135. this.userInfo = {
  136. name: data.realName || data.name,
  137. workType: data.workType === 'full_time' ? '全职' : '兼职',
  138. workStatus: this.formatStatus(data.status),
  139. city: data.cityName || '',
  140. avatar: data.avatarUrl || '/static/touxiang.png',
  141. stationName: data.stationName || '',
  142. stationFullName: '加载中...'
  143. };
  144. // 异步查找完整路径
  145. if (data.stationId) {
  146. if (this.fullTree.length === 0) {
  147. await this.loadAreaStationTree();
  148. }
  149. this.userInfo.stationFullName = this.findStationFullName(data.stationId, this.fullTree) || data.stationName || '未分配站点';
  150. } else {
  151. this.userInfo.stationFullName = '未分配站点';
  152. }
  153. } catch (error) {
  154. console.error('加载用户信息失败:', error);
  155. uni.showToast({ title: error.message || error.msg || '加载信息失败', icon: 'none' });
  156. } finally {
  157. uni.hideLoading();
  158. }
  159. },
  160. // 格式化状态
  161. formatStatus(status) {
  162. const statusMap = {
  163. 'busy': '接单中',
  164. 'resting': '休息中',
  165. 'disabled': '已禁用'
  166. };
  167. return statusMap[status] || status;
  168. },
  169. // 查找站点完整路径 (城市/区域/站点)
  170. findStationFullName(stationId, tree) {
  171. if (!stationId || !tree || tree.length === 0) return '';
  172. let path = [];
  173. const dfs = (nodes, targetId, currentPath) => {
  174. for (let node of nodes) {
  175. if (node.id === targetId) {
  176. path = [...currentPath, node.name];
  177. return true;
  178. }
  179. if (node.children && node.children.length > 0) {
  180. if (dfs(node.children, targetId, [...currentPath, node.name])) {
  181. return true;
  182. }
  183. }
  184. }
  185. return false;
  186. };
  187. dfs(tree, stationId, []);
  188. return path.join('/');
  189. },
  190. navBack() {
  191. uni.navigateBack({ delta: 1 });
  192. },
  193. // 修改头像
  194. changeAvatar() {
  195. uni.chooseImage({
  196. count: 1,
  197. success: async (res) => {
  198. const tempFilePath = res.tempFilePaths[0];
  199. // 上传图片到服务器
  200. uni.showLoading({ title: '上传中...' });
  201. try {
  202. const uploadRes = await uploadFile(tempFilePath);
  203. const { url, ossId } = uploadRes.data;
  204. // 调用接口更新头像
  205. await updateAvatar(ossId);
  206. this.userInfo.avatar = url;
  207. uni.showToast({ title: '修改成功', icon: 'success' });
  208. } catch (error) {
  209. console.error('修改头像失败:', error);
  210. uni.showToast({ title: error.message || error.msg || '上传失败', icon: 'none' });
  211. } finally {
  212. uni.hideLoading();
  213. }
  214. }
  215. });
  216. },
  217. editName() {
  218. uni.navigateTo({
  219. url: `/pages/mine/settings/profile/edit-name/index?name=${this.userInfo.name}`
  220. });
  221. },
  222. showStatusPicker() {
  223. this.isStatusPickerShow = true;
  224. },
  225. closeStatusPicker() {
  226. this.isStatusPickerShow = false;
  227. },
  228. // 选择状态
  229. async selectStatus(statusText) {
  230. const statusMap = {
  231. '接单中': 'busy',
  232. '休息中': 'resting'
  233. };
  234. const status = statusMap[statusText];
  235. try {
  236. await updateStatus(status);
  237. this.userInfo.workStatus = statusText;
  238. uni.showToast({ title: '状态已更新', icon: 'success' });
  239. } catch (error) {
  240. console.error('修改状态失败:', error);
  241. uni.showToast({ title: error.message || error.msg || '状态更新失败', icon: 'none' });
  242. } finally {
  243. this.closeStatusPicker();
  244. }
  245. },
  246. // 城市和站点级联选择器
  247. async showCityPicker() {
  248. this.isCityPickerShow = true;
  249. if (this.fullTree.length === 0) {
  250. await this.loadAreaStationTree();
  251. }
  252. if (this.selectedPathway.length === 0) {
  253. this.resetCityPicker();
  254. }
  255. },
  256. async loadAreaStationTree() {
  257. try {
  258. uni.showLoading({ title: '加载中...' });
  259. const res = await getAreaStationList();
  260. const list = res.data || [];
  261. // 列表转树形结构
  262. let map = {};
  263. let roots = [];
  264. list.forEach(node => {
  265. map[node.id] = { ...node, children: [] };
  266. });
  267. list.forEach(node => {
  268. if (node.parentId === 0 || !map[node.parentId]) {
  269. roots.push(map[node.id]);
  270. } else {
  271. map[node.parentId].children.push(map[node.id]);
  272. }
  273. });
  274. this.fullTree = roots;
  275. } catch (err) {
  276. console.error('加载站点数据失败:', err);
  277. this.fullTree = [];
  278. uni.showToast({ title: err.message || err.msg || '获取站点失败', icon: 'none' });
  279. } finally {
  280. uni.hideLoading();
  281. }
  282. },
  283. resetCityPicker() {
  284. this.selectStep = 0;
  285. this.selectedPathway = [];
  286. this.currentCityList = this.fullTree;
  287. },
  288. closeCityPicker() {
  289. this.isCityPickerShow = false;
  290. },
  291. selectCityItem(item) {
  292. this.selectedPathway[this.selectStep] = item;
  293. // 如果有下级(如城市下的区域,区域下的站点)则继续选
  294. if (item.children && item.children.length > 0) {
  295. this.selectStep++;
  296. this.selectedPathway = this.selectedPathway.slice(0, this.selectStep);
  297. this.currentCityList = item.children;
  298. } else {
  299. // 没有下级,直接确认
  300. this.selectedCityId = item.id;
  301. this.confirmCity();
  302. }
  303. },
  304. jumpToStep(step) {
  305. this.selectStep = step;
  306. if (step === 0) {
  307. this.currentCityList = this.fullTree;
  308. } else {
  309. const parent = this.selectedPathway[step - 1];
  310. this.currentCityList = parent ? parent.children : [];
  311. }
  312. },
  313. // 确认城市与站点选择
  314. async confirmCity() {
  315. if (this.selectedPathway.length === 0) {
  316. uni.showToast({ title: '请选择站点', icon: 'none' });
  317. return;
  318. }
  319. // 找出最后选中的节点 (站点)
  320. let stationNode = this.selectedPathway[this.selectedPathway.length - 1];
  321. // 将路径名称用 / 加起来
  322. const fullName = this.selectedPathway.map(i => i.name).join('/');
  323. const reqData = {
  324. stationId: stationNode.id
  325. };
  326. try {
  327. await updateCity(reqData);
  328. this.userInfo.stationFullName = fullName;
  329. uni.showToast({ title: '修改成功', icon: 'success' });
  330. this.closeCityPicker();
  331. // 重置以便下一次打开
  332. this.selectedPathway = [];
  333. } catch (error) {
  334. console.error('修改失败:', error);
  335. uni.showToast({ title: error.message || error.msg || '站点更新失败', icon: 'none' });
  336. }
  337. }
  338. }
  339. }
  340. </script>
  341. <style>
  342. page {
  343. background-color: #F8F8F8;
  344. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  345. }
  346. .custom-header {
  347. position: fixed;
  348. top: 0;
  349. left: 0;
  350. width: 100%;
  351. height: 88rpx;
  352. padding-top: var(--status-bar-height);
  353. background-color: #fff;
  354. display: flex;
  355. align-items: center;
  356. justify-content: space-between;
  357. padding-left: 30rpx;
  358. padding-right: 30rpx;
  359. box-sizing: content-box;
  360. z-index: 100;
  361. }
  362. .header-placeholder {
  363. height: calc(88rpx + var(--status-bar-height));
  364. }
  365. .back-icon {
  366. width: 40rpx;
  367. height: 40rpx;
  368. }
  369. .header-title {
  370. font-size: 28rpx;
  371. font-weight: bold;
  372. color: #333;
  373. }
  374. .header-right {
  375. width: 40rpx;
  376. }
  377. .container {
  378. padding: 20rpx 30rpx;
  379. }
  380. .group-card {
  381. background-color: #fff;
  382. border-radius: 20rpx;
  383. padding: 0 30rpx;
  384. margin-bottom: 30rpx;
  385. }
  386. .list-item {
  387. display: flex;
  388. justify-content: space-between;
  389. align-items: center;
  390. height: 100rpx;
  391. border-bottom: 1px solid #F5F5F5;
  392. }
  393. .list-item.no-border {
  394. border-bottom: none;
  395. }
  396. .item-title {
  397. font-size: 28rpx;
  398. color: #333;
  399. }
  400. .item-right {
  401. display: flex;
  402. align-items: center;
  403. }
  404. .arrow-icon {
  405. width: 28rpx;
  406. height: 28rpx;
  407. opacity: 0.5;
  408. margin-left: 10rpx;
  409. }
  410. .user-avatar {
  411. width: 64rpx;
  412. height: 64rpx;
  413. border-radius: 50%;
  414. }
  415. .item-value {
  416. font-size: 28rpx;
  417. color: #999;
  418. }
  419. .item-value-black {
  420. font-size: 28rpx;
  421. color: #333;
  422. font-weight: 500;
  423. }
  424. /* Popup Styles */
  425. .popup-mask {
  426. position: fixed;
  427. top: 0;
  428. left: 0;
  429. width: 100%;
  430. height: 100%;
  431. background-color: rgba(0, 0, 0, 0.5);
  432. z-index: 999;
  433. display: flex;
  434. align-items: flex-end;
  435. }
  436. .popup-content {
  437. width: 100%;
  438. background-color: #fff;
  439. border-top-left-radius: 20rpx;
  440. border-top-right-radius: 20rpx;
  441. padding-bottom: 30rpx;
  442. }
  443. .popup-title {
  444. text-align: center;
  445. font-size: 28rpx;
  446. color: #999;
  447. padding: 30rpx 0;
  448. border-bottom: 1px solid #eee;
  449. }
  450. .popup-item {
  451. text-align: center;
  452. font-size: 32rpx;
  453. color: #333;
  454. padding: 30rpx 0;
  455. border-bottom: 1px solid #eee;
  456. }
  457. .popup-cancel {
  458. text-align: center;
  459. font-size: 32rpx;
  460. color: #333;
  461. padding: 30rpx 0;
  462. margin-top: 20rpx;
  463. border-top: 10rpx solid #f5f5f5;
  464. }
  465. .popup-header-row {
  466. display: flex;
  467. justify-content: space-between;
  468. padding: 30rpx;
  469. border-bottom: 1px solid #eee;
  470. }
  471. .popup-btn-cancel {
  472. font-size: 28rpx;
  473. color: #666;
  474. }
  475. .popup-title-text {
  476. font-size: 32rpx;
  477. font-weight: bold;
  478. color: #333;
  479. }
  480. .popup-btn-confirm {
  481. font-size: 28rpx;
  482. color: #FF5722;
  483. font-weight: bold;
  484. }
  485. /* 级联城市选择器 */
  486. .picker-body {
  487. display: flex;
  488. height: 500rpx;
  489. }
  490. .timeline-area {
  491. width: 240rpx;
  492. padding: 20rpx;
  493. background: #f8f8f8;
  494. border-right: 1px solid #eee;
  495. overflow-y: auto;
  496. }
  497. .timeline-item {
  498. display: flex;
  499. align-items: center;
  500. padding: 16rpx 0;
  501. font-size: 26rpx;
  502. color: #666;
  503. }
  504. .timeline-item.active {
  505. color: #FF5722;
  506. font-weight: bold;
  507. }
  508. .timeline-dot {
  509. width: 16rpx;
  510. height: 16rpx;
  511. border-radius: 50%;
  512. background: #ccc;
  513. margin-right: 12rpx;
  514. flex-shrink: 0;
  515. }
  516. .timeline-item.active .timeline-dot {
  517. background: #FF5722;
  518. }
  519. .list-area {
  520. flex: 1;
  521. height: 100%;
  522. }
  523. .list-area .list-item {
  524. padding: 24rpx 30rpx;
  525. font-size: 28rpx;
  526. color: #333;
  527. border-bottom: 1px solid #f5f5f5;
  528. }
  529. </style>