No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 

411 líneas
9.5 KiB

  1. <template>
  2. <view class="login">
  3. <view class="logo-section">
  4. <image
  5. class="logo-img"
  6. src="https://cdn.aionline.cc/logo/logo_aionline_black.svg"
  7. mode="widthFix"
  8. ></image>
  9. </view>
  10. <view class="login-actions">
  11. <button class="login-btn wechat" hover-class="login-btn-hover" :loading="postLoading" @click="onLoginClick">
  12. <up-icon class="btn-icon" name="weixin-fill" size="24" color="#ffffff"></up-icon>
  13. <text>微信登录</text>
  14. </button>
  15. <!-- #ifdef MP-WEIXIN -->
  16. <button
  17. class="login-btn phone"
  18. hover-class="login-btn-hover"
  19. :open-type="agree ? 'getPhoneNumber' : ''"
  20. @getphonenumber="getPhoneNumber"
  21. @click="onPhoneLoginClick"
  22. >
  23. <up-icon class="btn-icon" name="phone-fill" size="24" color="#0b8cff"></up-icon>
  24. <text>手机号登录</text>
  25. </button>
  26. <!-- #endif -->
  27. <!-- #ifndef MP-WEIXIN -->
  28. <button class="login-btn phone" hover-class="login-btn-hover" @click="onPhoneLoginClick">
  29. <up-icon class="btn-icon" name="phone-fill" size="24" color="#0b8cff"></up-icon>
  30. <text>手机号登录</text>
  31. </button>
  32. <!-- #endif -->
  33. <!-- #ifdef APP-PLUS -->
  34. <view v-if="isIOS" class="more-actions">
  35. <button class="mini-login-btn apple" hover-class="mini-login-btn-hover" :loading="postLoading" @click="onAppleLoginClick">
  36. <up-icon name="apple-fill" size="20" color="#172033"></up-icon>
  37. <text>Apple 登录</text>
  38. </button>
  39. <button class="mini-login-btn visitor" hover-class="mini-login-btn-hover" :loading="postLoading" @click="onVisitorLoginClick">
  40. <up-icon name="account-fill" size="20" color="#172033"></up-icon>
  41. <text>游客登录</text>
  42. </button>
  43. </view>
  44. <!-- #endif -->
  45. </view>
  46. <view class="agreement" @click="agree = !agree">
  47. <view class="checkbox" :class="{ checked: agree }">
  48. <up-icon v-if="agree" name="checkbox-mark" size="14" color="#ffffff"></up-icon>
  49. </view>
  50. <view class="agreement-text">
  51. <text>请阅读并同意</text>
  52. <text class="link" @click.stop="onAgreementClick('service')">《服务条款》</text>
  53. <text class="link" @click.stop="onAgreementClick('privacy')">《隐私政策》</text>
  54. </view>
  55. </view>
  56. </view>
  57. </template>
  58. <script setup>
  59. import { ref } from 'vue'
  60. import { onLoad } from '@dcloudio/uni-app'
  61. import { userApi } from '@/api/index.js'
  62. import { useUserStore } from '@/store/user.js'
  63. import { useApp } from '@/utils/useApp.js'
  64. import { isInWechat } from '@/utils/util.js'
  65. import { rememberLoginSource, returnAfterLogin } from '@/utils/login.js'
  66. import CONFIG from '@/utils/config.js'
  67. const userStore = useUserStore()
  68. const { toast, showLoading, hideLoading } = useApp()
  69. const postLoading = ref(false)
  70. const agree = ref(false)
  71. const type = ref('1')
  72. const source = ref('')
  73. // 平台标识
  74. let isIOS = false
  75. // #ifdef APP-PLUS
  76. isIOS = uni.getSystemInfoSync().platform === 'ios'
  77. // #endif
  78. onLoad((options = {}) => {
  79. type.value = options.type || '1'
  80. source.value = options.source || ''
  81. const redirect = options.redirect || options.url || ''
  82. rememberLoginSource(redirect)
  83. // #ifdef H5
  84. if (options.code) postLogin(options.code) // 公众号授权回跳
  85. // #endif
  86. // #ifdef MP-WEIXIN
  87. if (options.source === 'scheme') {
  88. // 深度链接进入,自动登录
  89. if (userStore.isLogin) {
  90. uni.switchTab({ url: '/pages/toolbox/toolbox' })
  91. } else {
  92. agree.value = true
  93. onLoginClick()
  94. }
  95. }
  96. // #endif
  97. })
  98. function checkAgree() {
  99. if (!agree.value) {
  100. toast('请阅读并同意服务政策')
  101. return false
  102. }
  103. return true
  104. }
  105. function onAgreementClick(key) {
  106. uni.navigateTo({ url: `/pages/agreement/agreement?key=${key}` })
  107. }
  108. /* ============ 微信登录 ============ */
  109. function onLoginClick() {
  110. // #ifndef H5
  111. if (!checkAgree()) return
  112. // #endif
  113. // #ifdef APP-PLUS
  114. wechatLogin()
  115. // #endif
  116. // #ifdef MP-WEIXIN
  117. uni.login({
  118. success: (res) => postLogin(res.code)
  119. })
  120. // #endif
  121. // #ifdef H5
  122. if (isInWechat()) {
  123. location.replace(getH5AuthUrl())
  124. } else {
  125. getMiniScheme()
  126. }
  127. // #endif
  128. }
  129. function wechatLogin() {
  130. uni.login({
  131. provider: 'weixin',
  132. onlyAuthorize: true,
  133. success: (event) => postLogin(event.code)
  134. })
  135. }
  136. function getH5AuthUrl() {
  137. const mUrl = CONFIG.M_URL
  138. const appid = CONFIG.WX_MP_APPID
  139. return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${mUrl}/pages/login/login&response_type=code&scope=snsapi_userinfo&state=23_4_${appid}#wechat_redirect`
  140. }
  141. async function getMiniScheme() {
  142. try {
  143. showLoading('加载中...')
  144. const res = await userApi.getMiniScheme({})
  145. hideLoading()
  146. location.href = res.data.openlink
  147. } catch (e) {
  148. hideLoading()
  149. toast(e.msg || '登录失败,请重试')
  150. }
  151. }
  152. /* ============ 手机号登录 ============ */
  153. function onPhoneLoginClick() {
  154. if (!checkAgree()) return
  155. // #ifndef MP-WEIXIN
  156. // 非小程序端跳验证码登录页
  157. uni.navigateTo({ url: '/pages/login/phone' })
  158. // #endif
  159. // 小程序端由 button open-type=getPhoneNumber 触发 getPhoneNumber
  160. }
  161. function getPhoneNumber(event) {
  162. if (event.detail.errMsg !== 'getPhoneNumber:ok') {
  163. toast('已取消')
  164. return
  165. }
  166. loginByPhoneCode(event.detail.code)
  167. }
  168. async function loginByPhoneCode(code) {
  169. try {
  170. postLoading.value = true
  171. const res = await userApi.loginByPhoneMp({ code })
  172. afterLogin(res)
  173. } catch (e) {
  174. toast(e.msg || '登录失败,请重试!')
  175. } finally {
  176. postLoading.value = false
  177. }
  178. }
  179. /* ============ Apple / 游客登录 ============ */
  180. function onAppleLoginClick() {
  181. if (!checkAgree()) return
  182. uni.login({
  183. provider: 'apple',
  184. success: (res) => {
  185. const open_id = res.appleInfo.user
  186. let user_name = ''
  187. if (res.appleInfo.fullName) {
  188. user_name = `${res.appleInfo.fullName.familyName || ''}${res.appleInfo.fullName.givenName || ''}`
  189. }
  190. signWithApple({ open_id, user_name })
  191. }
  192. })
  193. }
  194. function onVisitorLoginClick() {
  195. if (!checkAgree()) return
  196. const open_id = uni.getSystemInfoSync().deviceId
  197. signWithApple({ open_id, user_name: '', is_visitor: 1 })
  198. }
  199. async function signWithApple(params) {
  200. try {
  201. postLoading.value = true
  202. const res = await userApi.signWithApple(params)
  203. afterLogin(res)
  204. } catch (e) {
  205. toast(e.msg || '登录失败,请重试!')
  206. } finally {
  207. postLoading.value = false
  208. }
  209. }
  210. /* ============ 通用登录处理 ============ */
  211. async function postLogin(code) {
  212. try {
  213. postLoading.value = true
  214. const res = await userApi.loginByCode({ code })
  215. afterLogin(res)
  216. } catch (e) {
  217. toast(e.msg || '登录失败,请重试!')
  218. } finally {
  219. postLoading.value = false
  220. }
  221. }
  222. function afterLogin(res) {
  223. const inviteCode = uni.getStorageSync('invite_code')
  224. userStore.setLogin({ token: res.data.token, userInfo: res.data })
  225. if (res.data.is_register) {
  226. toast(inviteCode ? '登录并领取成功!' : '登录成功!')
  227. } else {
  228. toast('登录成功!')
  229. }
  230. if (inviteCode) uni.removeStorageSync('invite_code')
  231. // 返回发起登录的页面,navigateBack/reLaunch 都会触发目标页重新读取登录态
  232. setTimeout(() => {
  233. returnAfterLogin()
  234. }, 800)
  235. }
  236. </script>
  237. <style lang="scss" scoped>
  238. .login {
  239. min-height: 100vh;
  240. padding: 0 48rpx calc(46rpx + env(safe-area-inset-bottom));
  241. box-sizing: border-box;
  242. background: #ffffff;
  243. text-align: center;
  244. }
  245. .logo-section {
  246. padding-top: 12vh;
  247. display: flex;
  248. flex-direction: column;
  249. align-items: center;
  250. }
  251. .logo-img {
  252. width: 300rpx;
  253. height: auto;
  254. display: block;
  255. }
  256. .login-actions {
  257. margin-top: 92rpx;
  258. display: flex;
  259. flex-direction: column;
  260. align-items: center;
  261. gap: 30rpx;
  262. }
  263. .login-btn {
  264. width: 100%;
  265. max-width: 560rpx;
  266. height: 92rpx;
  267. margin: 0;
  268. padding: 0 28rpx;
  269. border-radius: 48rpx;
  270. display: flex;
  271. align-items: center;
  272. justify-content: center;
  273. box-sizing: border-box;
  274. font-size: 30rpx;
  275. font-weight: 600;
  276. line-height: 1;
  277. &::after {
  278. border: none;
  279. }
  280. .btn-icon {
  281. margin-right: 14rpx;
  282. }
  283. &.wechat {
  284. color: #fff;
  285. background: #0b8cff;
  286. box-shadow: 0 14rpx 28rpx rgba(11, 140, 255, 0.2);
  287. }
  288. &.phone {
  289. color: #0b8cff;
  290. background: #ffffff;
  291. border: 2rpx solid rgba(11, 140, 255, 0.48);
  292. }
  293. }
  294. .login-btn-hover,
  295. .mini-login-btn-hover {
  296. opacity: 0.86;
  297. }
  298. .more-actions {
  299. width: 100%;
  300. max-width: 560rpx;
  301. display: grid;
  302. grid-template-columns: 1fr 1fr;
  303. gap: 16rpx;
  304. }
  305. .mini-login-btn {
  306. height: 82rpx;
  307. margin: 0;
  308. padding: 0;
  309. border-radius: 42rpx;
  310. display: flex;
  311. align-items: center;
  312. justify-content: center;
  313. gap: 10rpx;
  314. color: #172033;
  315. font-size: 26rpx;
  316. font-weight: 700;
  317. line-height: 1;
  318. background: #ffffff;
  319. border: 1rpx solid rgba(89, 105, 128, 0.12);
  320. &::after {
  321. border: none;
  322. }
  323. }
  324. .agreement {
  325. margin-top: 60rpx;
  326. padding: 0 6rpx;
  327. display: flex;
  328. align-items: center;
  329. justify-content: center;
  330. font-size: 24rpx;
  331. line-height: 1.45;
  332. color: #7f8896;
  333. .checkbox {
  334. width: 32rpx;
  335. height: 32rpx;
  336. margin-right: 12rpx;
  337. border-radius: 50%;
  338. border: 2rpx solid #b8c2d1;
  339. display: flex;
  340. align-items: center;
  341. justify-content: center;
  342. flex-shrink: 0;
  343. &.checked {
  344. background: #0b8cff;
  345. border-color: #0b8cff;
  346. }
  347. }
  348. .agreement-text {
  349. min-width: 0;
  350. display: flex;
  351. align-items: center;
  352. justify-content: center;
  353. flex-wrap: wrap;
  354. }
  355. .link {
  356. color: #0b8cff;
  357. font-weight: 600;
  358. }
  359. }
  360. </style>