Não pode escolher mais do que 25 tópicos Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') e podem ter até 35 caracteres.
 
 
 

418 linhas
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.svg"
  7. mode="widthFix"
  8. ></image>
  9. <text class="login-tip">登录后同步作品、点数和创作记录</text>
  10. </view>
  11. <view class="login-actions">
  12. <button class="login-btn wechat" hover-class="login-btn-hover" :loading="postLoading" @click="onLoginClick">
  13. <up-icon class="btn-icon" name="weixin-fill" size="24" color="#ffffff"></up-icon>
  14. <text>微信登录</text>
  15. </button>
  16. <!-- #ifdef MP-WEIXIN -->
  17. <button
  18. class="login-btn phone"
  19. hover-class="login-btn-hover"
  20. :open-type="agree ? 'getPhoneNumber' : ''"
  21. @getphonenumber="getPhoneNumber"
  22. @click="onPhoneLoginClick"
  23. >
  24. <up-icon class="btn-icon" name="phone-fill" size="24" color="#46bb36"></up-icon>
  25. <text>手机号登录</text>
  26. </button>
  27. <!-- #endif -->
  28. <!-- #ifndef MP-WEIXIN -->
  29. <button class="login-btn phone" hover-class="login-btn-hover" @click="onPhoneLoginClick">
  30. <up-icon class="btn-icon" name="phone-fill" size="24" color="#46bb36"></up-icon>
  31. <text>手机号登录</text>
  32. </button>
  33. <!-- #endif -->
  34. <!-- #ifdef APP-PLUS -->
  35. <view v-if="isIOS" class="more-actions">
  36. <button class="mini-login-btn apple" hover-class="mini-login-btn-hover" :loading="postLoading" @click="onAppleLoginClick">
  37. <up-icon name="apple-fill" size="20" color="#172033"></up-icon>
  38. <text>Apple 登录</text>
  39. </button>
  40. <button class="mini-login-btn visitor" hover-class="mini-login-btn-hover" :loading="postLoading" @click="onVisitorLoginClick">
  41. <up-icon name="account-fill" size="20" color="#172033"></up-icon>
  42. <text>游客登录</text>
  43. </button>
  44. </view>
  45. <!-- #endif -->
  46. </view>
  47. <view class="agreement" @click="agree = !agree">
  48. <view class="checkbox" :class="{ checked: agree }">
  49. <up-icon v-if="agree" name="checkbox-mark" size="14" color="#ffffff"></up-icon>
  50. </view>
  51. <view class="agreement-text">
  52. <text>请阅读并同意</text>
  53. <text class="link" @click.stop="onAgreementClick('service')">《服务条款》</text>
  54. <text class="link" @click.stop="onAgreementClick('privacy')">《隐私政策》</text>
  55. </view>
  56. </view>
  57. </view>
  58. </template>
  59. <script setup>
  60. import { ref } from 'vue'
  61. import { onLoad } from '@dcloudio/uni-app'
  62. import { userApi } from '@/api/index.js'
  63. import { useUserStore } from '@/store/user.js'
  64. import { useApp } from '@/utils/useApp.js'
  65. import { isInWechat } from '@/utils/util.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. // #ifdef H5
  83. if (redirect) uni.setStorageSync('login_redirect', decodeURIComponent(redirect))
  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. // 统一跳回首页
  232. setTimeout(() => {
  233. uni.switchTab({ url: '/pages/toolbox/toolbox' })
  234. }, 1200)
  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-tip {
  257. margin-top: 24rpx;
  258. color: #7f8896;
  259. font-size: 26rpx;
  260. line-height: 1.4;
  261. }
  262. .login-actions {
  263. margin-top: 92rpx;
  264. display: flex;
  265. flex-direction: column;
  266. align-items: center;
  267. gap: 30rpx;
  268. }
  269. .login-btn {
  270. width: 100%;
  271. max-width: 560rpx;
  272. height: 92rpx;
  273. margin: 0;
  274. padding: 0 28rpx;
  275. border-radius: 48rpx;
  276. display: flex;
  277. align-items: center;
  278. justify-content: center;
  279. box-sizing: border-box;
  280. font-size: 30rpx;
  281. font-weight: 600;
  282. line-height: 1;
  283. &::after {
  284. border: none;
  285. }
  286. .btn-icon {
  287. margin-right: 14rpx;
  288. }
  289. &.wechat {
  290. color: #fff;
  291. background: #46bb36;
  292. box-shadow: 0 14rpx 28rpx rgba(70, 187, 54, 0.18);
  293. }
  294. &.phone {
  295. color: #46bb36;
  296. background: #ffffff;
  297. border: 2rpx solid rgba(70, 187, 54, 0.55);
  298. }
  299. }
  300. .login-btn-hover,
  301. .mini-login-btn-hover {
  302. opacity: 0.86;
  303. }
  304. .more-actions {
  305. width: 100%;
  306. max-width: 560rpx;
  307. display: grid;
  308. grid-template-columns: 1fr 1fr;
  309. gap: 16rpx;
  310. }
  311. .mini-login-btn {
  312. height: 82rpx;
  313. margin: 0;
  314. padding: 0;
  315. border-radius: 42rpx;
  316. display: flex;
  317. align-items: center;
  318. justify-content: center;
  319. gap: 10rpx;
  320. color: #172033;
  321. font-size: 26rpx;
  322. font-weight: 700;
  323. line-height: 1;
  324. background: #ffffff;
  325. border: 1rpx solid rgba(89, 105, 128, 0.12);
  326. &::after {
  327. border: none;
  328. }
  329. }
  330. .agreement {
  331. margin-top: 60rpx;
  332. padding: 0 6rpx;
  333. display: flex;
  334. align-items: center;
  335. justify-content: center;
  336. font-size: 24rpx;
  337. line-height: 1.45;
  338. color: #7f8896;
  339. .checkbox {
  340. width: 32rpx;
  341. height: 32rpx;
  342. margin-right: 12rpx;
  343. border-radius: 50%;
  344. border: 2rpx solid #b8c2d1;
  345. display: flex;
  346. align-items: center;
  347. justify-content: center;
  348. flex-shrink: 0;
  349. &.checked {
  350. background: #46bb36;
  351. border-color: #46bb36;
  352. }
  353. }
  354. .agreement-text {
  355. min-width: 0;
  356. display: flex;
  357. align-items: center;
  358. justify-content: center;
  359. flex-wrap: wrap;
  360. }
  361. .link {
  362. color: #0b8cff;
  363. font-weight: 600;
  364. }
  365. }
  366. </style>