25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

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