WechatLogin.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682
  1. <template>
  2. <view class="wechat-login-container">
  3. <view class="content-wrapper">
  4. <view class="action-section">
  5. <button class="main-btn" @click="startLoginFlow">
  6. <image class="btn-icon" :src="assets.wechat" mode="aspectFit"></image>
  7. <text>授权手机号码登录</text>
  8. </button>
  9. <view class="agreement-box">
  10. <label class="checkbox-label" @click="toggleAgreed">
  11. <checkbox :checked="isAgreed" color="#C1001C" style="transform:scale(0.7)" />
  12. <text class="agreement-text">我已阅读并同意
  13. <text class="link" @click.stop="showProtocol('user')">《用户协议》</text> 与
  14. <text class="link" @click.stop="showProtocol('privacy')">《隐私政策》</text>
  15. </text>
  16. </label>
  17. </view>
  18. </view>
  19. <view class="footer-section">
  20. <text>© 2026 ERP Order System. All Rights Reserved.</text>
  21. </view>
  22. </view>
  23. <view class="global-mask" v-if="activeModal" @click="closeAllModals"></view>
  24. <!-- 协议拦截确认弹窗 -->
  25. <view class="confirm-modal center-card" v-if="activeModal === 'confirm'">
  26. <view class="card-title">服务协议提示</view>
  27. <view class="card-body">请您阅读并同意我们的协议内容,以便为您提供更安全的服务体验。</view>
  28. <view class="card-footer-btns">
  29. <view class="btn-item cancel" @click="activeModal = ''">拒绝</view>
  30. <view class="btn-item agree" @click="agreeAndClose">同意并继续</view>
  31. </view>
  32. </view>
  33. <!-- 头像昵称授权弹窗 -->
  34. <view class="simulated-profile-pop bottom-pop" v-if="activeModal === 'profile'">
  35. <view class="pop-header-bar">
  36. <text class="pop-cancel" @click="activeModal = ''">取消</text>
  37. <text class="pop-main-title">获取头像昵称</text>
  38. <text class="pop-done" @click="activeModal = 'phone'">保存</text>
  39. </view>
  40. <view class="profile-edit-content">
  41. <view class="avatar-edit-box">
  42. <button class="avatar-wrapper-btn" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
  43. <image class="current-avatar"
  44. :src="avatarPreviewUrl || 'https://img.icons8.com/color/144/user.png'"></image>
  45. <view class="camera-icon">
  46. <image src="https://img.icons8.com/ios-glyphs/30/999999/camera.png" mode="aspectFit">
  47. </image>
  48. </view>
  49. </button>
  50. <text class="edit-hint">点击修改头像</text>
  51. </view>
  52. <view class="nickname-edit-box">
  53. <text class="label">昵称</text>
  54. <input class="nickname-input" type="nickname" :value="userName" placeholder="请输入昵称"
  55. @blur="onNicknameBlur" @input="onNicknameChange" />
  56. </view>
  57. <view class="auth-notice-box">
  58. <text class="notice-text">授权后,开发者将获得您的头像和昵称,用于展示您的个人资料。</text>
  59. </view>
  60. </view>
  61. <view class="bottom-action">
  62. <button class="confirm-btn-fixed" @click="goToPhoneAuth">确定</button>
  63. </view>
  64. </view>
  65. <!-- 手机号授权弹窗 -->
  66. <view class="phone-auth-pop bottom-pop" v-if="activeModal === 'phone'">
  67. <view class="p-header">
  68. <image class="p-mini-logo" :src="assets.logo" mode="aspectFill"></image>
  69. <text class="p-app-name">ERP 智能下单系统 申请</text>
  70. </view>
  71. <view class="p-body">
  72. <text class="p-title">获取您的手机号</text>
  73. <text class="p-phone-hint">是否允许我们获取您的手机号,用于登录和订单通知?</text>
  74. </view>
  75. <view class="p-footer-btns">
  76. <button class="p-btn-fixed p-deny" @click="activeModal = ''">拒绝</button>
  77. <button class="p-btn-fixed p-allow" open-type="getPhoneNumber"
  78. @getphonenumber="handleGetPhoneNumber">允许</button>
  79. </view>
  80. </view>
  81. <!-- 协议富文本弹窗 -->
  82. <view class="protocol-modal center-card" v-if="activeModal === 'protocol'">
  83. <view class="p-pop-header">
  84. <text class="p-pop-title">{{ currentProtocol.title }}</text>
  85. <text class="p-pop-close" @click="activeModal = ''">×</text>
  86. </view>
  87. <scroll-view scroll-y class="p-pop-scroll">
  88. <view class="rich-text-wrapper">
  89. <rich-text :nodes="currentProtocol.content"></rich-text>
  90. </view>
  91. </scroll-view>
  92. <view class="p-pop-footer">
  93. <button class="p-pop-btn" @click="activeModal = ''">我已了解</button>
  94. </view>
  95. </view>
  96. </view>
  97. </template>
  98. <script>
  99. import assets from '@/utils/assets.js';
  100. import { getAgreement } from '@/api/system/agreement.js';
  101. import { login, getWechatPhone, wechatRegister } from '@/api/auth.js';
  102. import { uploadFile } from '@/api/resource/oss.js';
  103. export default {
  104. name: 'WechatLogin',
  105. emits: ['login-success'],
  106. data() {
  107. return {
  108. assets,
  109. isAgreed: false,
  110. activeModal: '',
  111. avatarOssId: null,
  112. avatarPreviewUrl: '',
  113. userName: '微信用户',
  114. currentProtocol: { title: '', content: '' },
  115. protocols: {
  116. user: { title: '', content: '' },
  117. privacy: { title: '', content: '' }
  118. },
  119. openId: '',
  120. unionId: '',
  121. phoneNumber: ''
  122. }
  123. },
  124. methods: {
  125. toggleAgreed() {
  126. this.isAgreed = !this.isAgreed;
  127. },
  128. startLoginFlow() {
  129. if (!this.isAgreed) {
  130. this.activeModal = 'confirm';
  131. } else {
  132. this.performLogin();
  133. }
  134. },
  135. agreeAndClose() {
  136. this.isAgreed = true;
  137. this.activeModal = '';
  138. },
  139. async performLogin() {
  140. try {
  141. uni.showLoading({ title: '登录中...' });
  142. const loginRes = await new Promise((resolve, reject) => {
  143. wx.login({ success: resolve, fail: reject });
  144. });
  145. if (!loginRes.code) {
  146. uni.hideLoading();
  147. uni.showToast({ title: '获取登录凭证失败', icon: 'none' });
  148. return;
  149. }
  150. const res = await login({ loginCode: loginRes.code, grantType: 'wechatApplet' });
  151. uni.hideLoading();
  152. if (res.data && res.data.access_token) {
  153. uni.setStorageSync('token', res.data.access_token);
  154. uni.setStorageSync('isLogin', true);
  155. uni.showToast({ title: '登录成功', icon: 'success' });
  156. setTimeout(() => {
  157. uni.reLaunch({ url: '/pages/order/index' });
  158. }, 1000);
  159. } else if (res.data && res.data.openid) {
  160. this.openId = res.data.openid;
  161. this.unionId = res.data.unionid || '';
  162. this.activeModal = 'profile';
  163. } else {
  164. uni.showToast({ title: '登录失败', icon: 'none' });
  165. }
  166. } catch (error) {
  167. uni.hideLoading();
  168. console.error('登录错误:', error);
  169. uni.showToast({ title: error || '登录失败', icon: 'none' });
  170. }
  171. },
  172. async onChooseAvatar(e) {
  173. const tempPath = e.detail.avatarUrl;
  174. console.log('[微信信息] 头像临时路径:', tempPath);
  175. this.avatarPreviewUrl = tempPath;
  176. try {
  177. uni.showLoading({ title: '上传头像...' });
  178. const res = await uploadFile(tempPath);
  179. uni.hideLoading();
  180. this.avatarOssId = res.ossId;
  181. this.avatarPreviewUrl = res.url;
  182. console.log('[微信信息] 头像OSS上传成功, ossId:', this.avatarOssId);
  183. } catch (err) {
  184. uni.hideLoading();
  185. console.error('[微信信息] 头像上传失败:', err);
  186. uni.showToast({ title: err || '头像上传失败', icon: 'none' });
  187. }
  188. },
  189. onNicknameBlur(e) {
  190. this.userName = e.detail.value;
  191. console.log('[微信信息] 昵称(blur):', this.userName);
  192. },
  193. onNicknameChange(e) {
  194. this.userName = e.detail.value;
  195. console.log('[微信信息] 昵称(input):', this.userName);
  196. },
  197. async goToPhoneAuth() {
  198. this.phoneNumber = '';
  199. this.activeModal = 'phone';
  200. },
  201. async handleGetPhoneNumber(e) {
  202. if (e.detail.errMsg !== 'getPhoneNumber:ok') {
  203. uni.showToast({ title: '获取手机号失败,请重试', icon: 'none' });
  204. return;
  205. }
  206. try {
  207. uni.showLoading({ title: '获取手机号中...' });
  208. const phoneRes = await getWechatPhone({
  209. phoneCode: e.detail.code,
  210. openId: this.openId
  211. });
  212. uni.hideLoading();
  213. this.phoneNumber = phoneRes.data;
  214. uni.showLoading({ title: '注册中...' });
  215. const registerRes = await wechatRegister({
  216. openId: this.openId,
  217. unionId: this.unionId,
  218. phone: this.phoneNumber,
  219. nickname: this.userName,
  220. avatar: this.avatarOssId
  221. });
  222. uni.hideLoading();
  223. this.performLogin();
  224. } catch (error) {
  225. uni.hideLoading();
  226. console.error('注册错误:', error);
  227. uni.showToast({ title: error || '注册失败', icon: 'none' });
  228. }
  229. },
  230. showProtocol(type) {
  231. this.currentProtocol = this.protocols[type];
  232. this.activeModal = 'protocol';
  233. },
  234. closeAllModals() {
  235. this.activeModal = '';
  236. }
  237. },
  238. async mounted() {
  239. try {
  240. const [userRes, privacyRes] = await Promise.all([
  241. getAgreement(1),
  242. getAgreement(2)
  243. ]);
  244. this.protocols.user = { title: userRes.data.title, content: userRes.data.content };
  245. this.protocols.privacy = { title: privacyRes.data.title, content: privacyRes.data.content };
  246. } catch (e) {
  247. console.error('[协议] 加载失败', e);
  248. uni.showToast({ title: e || '加载协议失败', icon: 'none' });
  249. }
  250. }
  251. }
  252. </script>
  253. <style scoped>
  254. .wechat-login-container {
  255. width: 100%;
  256. position: relative;
  257. display: flex;
  258. flex-direction: column;
  259. }
  260. .content-wrapper {
  261. position: relative;
  262. z-index: 2;
  263. flex: 1;
  264. display: flex;
  265. flex-direction: column;
  266. padding: 40rpx 80rpx 0;
  267. box-sizing: border-box;
  268. }
  269. .main-btn {
  270. width: 100%;
  271. height: 100rpx;
  272. background: linear-gradient(135deg, #C1001C 0%, #FF4D4F 100%);
  273. border-radius: 50rpx;
  274. color: #fff;
  275. display: flex;
  276. align-items: center;
  277. justify-content: center;
  278. font-size: 32rpx;
  279. font-weight: bold;
  280. box-shadow: 0 12rpx 30rpx rgba(193, 0, 28, 0.2);
  281. border: none;
  282. margin-bottom: 40rpx;
  283. }
  284. .btn-icon {
  285. width: 48rpx;
  286. height: 48rpx;
  287. margin-right: 16rpx;
  288. }
  289. .agreement-text {
  290. font-size: 24rpx;
  291. color: #999;
  292. }
  293. .link {
  294. color: #C1001C;
  295. margin: 0 4rpx;
  296. font-weight: 500;
  297. }
  298. .footer-section {
  299. margin-top: auto;
  300. padding-bottom: 60rpx;
  301. text-align: center;
  302. font-size: 20rpx;
  303. color: #dcdcdc;
  304. }
  305. /* ========== 弹窗通用 ========== */
  306. .global-mask {
  307. position: fixed;
  308. top: 0;
  309. left: 0;
  310. right: 0;
  311. bottom: 0;
  312. background: rgba(0, 0, 0, 0.5);
  313. z-index: 998;
  314. }
  315. .center-card {
  316. position: fixed;
  317. top: 50%;
  318. left: 50%;
  319. transform: translate(-50%, -50%);
  320. width: 620rpx;
  321. background: #fff;
  322. border-radius: 32rpx;
  323. z-index: 1000;
  324. box-shadow: 0 30rpx 80rpx rgba(0, 0, 0, 0.15);
  325. padding: 50rpx 40rpx;
  326. display: flex;
  327. flex-direction: column;
  328. }
  329. .bottom-pop {
  330. position: fixed;
  331. bottom: 0;
  332. left: 0;
  333. right: 0;
  334. background: #fff;
  335. border-radius: 40rpx 40rpx 0 0;
  336. z-index: 1001;
  337. padding: 40rpx;
  338. padding-bottom: calc(50rpx + env(safe-area-inset-bottom));
  339. box-shadow: 0 -10rpx 40rpx rgba(0, 0, 0, 0.05);
  340. }
  341. button {
  342. display: flex !important;
  343. align-items: center !important;
  344. justify-content: center !important;
  345. padding: 0 !important;
  346. line-height: normal !important;
  347. }
  348. button::after {
  349. border: none;
  350. }
  351. /* ========== 协议拦截弹窗 ========== */
  352. .card-title {
  353. font-size: 38rpx;
  354. font-weight: bold;
  355. text-align: center;
  356. margin-bottom: 30rpx;
  357. }
  358. .card-body {
  359. font-size: 28rpx;
  360. color: #666;
  361. line-height: 1.6;
  362. text-align: center;
  363. margin-bottom: 50rpx;
  364. }
  365. .card-footer-btns {
  366. display: flex;
  367. gap: 24rpx;
  368. }
  369. .btn-item {
  370. flex: 1;
  371. height: 90rpx;
  372. border-radius: 45rpx;
  373. font-size: 30rpx;
  374. display: flex !important;
  375. align-items: center !important;
  376. justify-content: center !important;
  377. text-align: center;
  378. line-height: 90rpx;
  379. }
  380. .btn-item.cancel {
  381. background: #f8f8f8;
  382. color: #999;
  383. }
  384. .btn-item.agree {
  385. background: #C1001C;
  386. color: #fff;
  387. font-weight: bold;
  388. }
  389. /* ========== 协议内容弹窗 ========== */
  390. .p-pop-header {
  391. display: flex;
  392. justify-content: space-between;
  393. align-items: center;
  394. margin-bottom: 30rpx;
  395. }
  396. .p-pop-title {
  397. font-size: 36rpx;
  398. font-weight: bold;
  399. color: #1a1a1a;
  400. }
  401. .p-pop-close {
  402. font-size: 48rpx;
  403. color: #ccc;
  404. padding: 10rpx;
  405. }
  406. .p-pop-scroll {
  407. max-height: 55vh;
  408. margin-bottom: 30rpx;
  409. }
  410. .rich-text-wrapper {
  411. padding: 10rpx 0;
  412. color: #444;
  413. font-size: 28rpx;
  414. }
  415. .p-pop-footer {
  416. padding-top: 20rpx;
  417. }
  418. .p-pop-btn {
  419. width: 100%;
  420. height: 90rpx;
  421. background: #C1001C;
  422. color: #fff;
  423. border-radius: 45rpx;
  424. font-size: 30rpx;
  425. font-weight: bold;
  426. }
  427. /* ========== 头像授权弹窗 ========== */
  428. .pop-header-bar {
  429. display: flex;
  430. justify-content: space-between;
  431. align-items: center;
  432. margin-bottom: 60rpx;
  433. }
  434. .pop-cancel {
  435. font-size: 30rpx;
  436. color: #999;
  437. }
  438. .pop-main-title {
  439. font-size: 32rpx;
  440. font-weight: bold;
  441. }
  442. .pop-done {
  443. font-size: 30rpx;
  444. color: #C1001C;
  445. font-weight: bold;
  446. }
  447. .profile-edit-content {
  448. display: flex;
  449. flex-direction: column;
  450. align-items: center;
  451. }
  452. .avatar-wrapper-btn {
  453. width: 170rpx;
  454. height: 170rpx;
  455. border-radius: 85rpx;
  456. background: #f8f8f8;
  457. position: relative;
  458. margin-bottom: 24rpx;
  459. padding: 0 !important;
  460. overflow: visible;
  461. display: flex !important;
  462. align-items: center;
  463. justify-content: center;
  464. border: none;
  465. }
  466. .current-avatar {
  467. width: 100%;
  468. height: 100%;
  469. border-radius: 85rpx;
  470. border: 4rpx solid #fff;
  471. box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.05);
  472. }
  473. .camera-icon {
  474. position: absolute;
  475. bottom: 0;
  476. right: 0;
  477. background: #fff;
  478. width: 56rpx;
  479. height: 56rpx;
  480. border-radius: 28rpx;
  481. display: flex;
  482. align-items: center;
  483. justify-content: center;
  484. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
  485. z-index: 5;
  486. }
  487. .camera-icon image {
  488. width: 30rpx;
  489. height: 30rpx;
  490. }
  491. .edit-hint {
  492. font-size: 24rpx;
  493. color: #999;
  494. margin-bottom: 70rpx;
  495. width: 100%;
  496. text-align: center;
  497. display: block;
  498. }
  499. .nickname-edit-box {
  500. width: 100%;
  501. display: flex;
  502. align-items: center;
  503. padding: 36rpx 0;
  504. border-top: 1rpx solid #f0f0f0;
  505. border-bottom: 1rpx solid #f0f0f0;
  506. margin-bottom: 40rpx;
  507. }
  508. .nickname-edit-box .label {
  509. width: 130rpx;
  510. font-size: 32rpx;
  511. }
  512. .nickname-input {
  513. flex: 1;
  514. font-size: 32rpx;
  515. }
  516. .notice-text {
  517. font-size: 24rpx;
  518. color: #bfbfbf;
  519. text-align: center;
  520. display: block;
  521. margin-bottom: 60rpx;
  522. }
  523. .confirm-btn-fixed {
  524. width: 100%;
  525. height: 96rpx;
  526. background: #C1001C;
  527. color: #fff;
  528. border-radius: 16rpx;
  529. font-size: 32rpx;
  530. font-weight: bold;
  531. }
  532. /* ========== 手机号授权弹窗 ========== */
  533. .p-header {
  534. display: flex;
  535. align-items: center;
  536. margin-bottom: 50rpx;
  537. }
  538. .p-mini-logo {
  539. width: 44rpx;
  540. height: 44rpx;
  541. border-radius: 8rpx;
  542. margin-right: 16rpx;
  543. }
  544. .p-app-name {
  545. font-size: 28rpx;
  546. color: #7f7f7f;
  547. }
  548. .p-title {
  549. font-size: 40rpx;
  550. font-weight: bold;
  551. color: #000;
  552. margin-bottom: 44rpx;
  553. display: block;
  554. }
  555. .p-phone-hint {
  556. font-size: 30rpx;
  557. color: #666;
  558. line-height: 1.5;
  559. display: block;
  560. margin-bottom: 60rpx;
  561. }
  562. .p-number-card {
  563. background: #fbfbfb;
  564. padding: 36rpx;
  565. border-radius: 20rpx;
  566. display: flex;
  567. justify-content: space-between;
  568. align-items: center;
  569. margin-bottom: 30rpx;
  570. border: 1rpx solid #f0f0f0;
  571. }
  572. .p-real-num {
  573. font-size: 36rpx;
  574. font-weight: bold;
  575. color: #1a1a1a;
  576. display: block;
  577. }
  578. .p-num-hint {
  579. font-size: 24rpx;
  580. color: #999;
  581. }
  582. .p-other-link {
  583. font-size: 28rpx;
  584. color: #576b95;
  585. display: block;
  586. margin-bottom: 60rpx;
  587. }
  588. .p-footer-btns {
  589. display: flex;
  590. gap: 30rpx;
  591. }
  592. .p-btn-fixed {
  593. flex: 1;
  594. height: 96rpx;
  595. border-radius: 16rpx;
  596. font-size: 32rpx;
  597. border: none;
  598. }
  599. .p-deny {
  600. background: #f2f2f2;
  601. color: #C1001C;
  602. }
  603. .p-allow {
  604. background: #C1001C;
  605. color: #fff;
  606. font-weight: bold;
  607. }
  608. </style>