index.vue 17 KB

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