index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  1. <route lang="json5" type="page">
  2. {
  3. style: {
  4. navigationBarTitleText: '登录',
  5. navigationStyle: 'custom',
  6. },
  7. }
  8. </route>
  9. <template>
  10. <view class="login-container">
  11. <!-- 背景装饰元素 -->
  12. <view class="bg-decoration bg-circle-1"></view>
  13. <view class="bg-decoration bg-circle-2"></view>
  14. <view class="bg-decoration bg-circle-3"></view>
  15. <view class="login-header">
  16. <image class="login-logo" :src="appLogo" mode="aspectFit"></image>
  17. <view class="login-title">{{ appTitle }}</view>
  18. </view>
  19. <view class="login-form">
  20. <view class="welcome-text">欢迎登录</view>
  21. <view class="login-desc">请输入您的账号和密码</view>
  22. <view class="login-input-group">
  23. <view class="input-wrapper">
  24. <wd-input
  25. v-model="loginForm.username"
  26. prefix-icon="user"
  27. placeholder="请输入用户名"
  28. clearable
  29. class="login-input"
  30. :border="false"
  31. required
  32. ></wd-input>
  33. <view class="input-bottom-line"></view>
  34. </view>
  35. <view class="input-wrapper">
  36. <wd-input
  37. v-model="loginForm.password"
  38. prefix-icon="lock-on"
  39. placeholder="请输入密码"
  40. clearable
  41. show-password
  42. class="login-input"
  43. :border="false"
  44. required
  45. ></wd-input>
  46. <view class="input-bottom-line"></view>
  47. </view>
  48. <!-- 验证码区域 -->
  49. <view class="input-wrapper captcha-wrapper">
  50. <wd-input
  51. v-if="captcha.captchaEnabled"
  52. v-model="loginForm.code"
  53. prefix-icon="secured"
  54. placeholder="请输入验证码"
  55. clearable
  56. class="login-input captcha-input"
  57. :border="false"
  58. required
  59. >
  60. <template #suffix>
  61. <image
  62. class="captcha-image"
  63. :src="'data:image/gif;base64,' + captcha.image"
  64. mode="aspectFit"
  65. @click="refreshCaptcha"
  66. ></image>
  67. </template>
  68. </wd-input>
  69. <view class="input-bottom-line"></view>
  70. </view>
  71. </view>
  72. <!-- 登录按钮组 -->
  73. <view class="login-buttons">
  74. <!-- 账号密码登录按钮 -->
  75. <wd-button
  76. type="primary"
  77. size="large"
  78. block
  79. @click="handleAccountLogin"
  80. class="account-login-btn"
  81. >
  82. <wd-icon name="right" size="18px" class="login-icon"></wd-icon>
  83. 登录
  84. </wd-button>
  85. <!-- 微信小程序一键登录按钮 -->
  86. <!-- #ifdef MP-WEIXIN -->
  87. <view class="divider">
  88. <view class="divider-line"></view>
  89. <view class="divider-text">或</view>
  90. <view class="divider-line"></view>
  91. </view>
  92. <wd-button
  93. type="info"
  94. size="large"
  95. block
  96. plain
  97. @click="handleWechatLogin"
  98. class="wechat-login-btn"
  99. >
  100. 微信一键登录
  101. </wd-button>
  102. <!-- #endif -->
  103. </view>
  104. </view>
  105. <!-- 隐私协议勾选 -->
  106. <view class="privacy-agreement">
  107. <wd-checkbox
  108. v-model="agreePrivacy"
  109. shape="square"
  110. class="privacy-checkbox"
  111. active-color="var(--wot-color-theme, #1989fa)"
  112. >
  113. <view class="agreement-text">
  114. 我已阅读并同意
  115. <text class="agreement-link" @click.stop="handleAgreement('user')">《用户协议》</text>
  116. <text class="agreement-link" @click.stop="handleAgreement('privacy')">《隐私政策》</text>
  117. </view>
  118. </wd-checkbox>
  119. </view>
  120. <view class="login-footer"></view>
  121. </view>
  122. </template>
  123. <script setup lang="ts">
  124. import { ref } from 'vue'
  125. import { useUserStore } from '@/store/user'
  126. import { isMpWeixin } from '@/utils/platform'
  127. import { getCode, ILoginForm } from '@/api/login'
  128. import { toast } from '@/utils/toast'
  129. import { isTableBar } from '@/utils/index'
  130. import { ICaptcha } from '@/api/login.typings'
  131. const redirectRoute = ref('')
  132. // 获取环境变量
  133. const appTitle = ref(import.meta.env.VITE_APP_TITLE || 'Unibest Login')
  134. const appLogo = ref(import.meta.env.VITE_APP_LOGO || '/static/logo.svg')
  135. // 初始化store
  136. const userStore = useUserStore()
  137. // 路由位置
  138. // 验证码图片
  139. const captcha = ref<ICaptcha>({
  140. captchaEnabled: false,
  141. uuid: '',
  142. image: '',
  143. })
  144. // 登录表单数据
  145. const loginForm = ref<ILoginForm>({
  146. username: 'admin',
  147. password: '123456',
  148. code: '',
  149. uuid: '',
  150. })
  151. // 隐私协议勾选状态
  152. const agreePrivacy = ref(true)
  153. // 页面加载完毕时触发
  154. onLoad((option) => {
  155. // 一进来就刷新验证码
  156. captcha.value.captchaEnabled && refreshCaptcha()
  157. // 获取跳转路由
  158. if (option.redirect) {
  159. redirectRoute.value = option.redirect
  160. }
  161. })
  162. // 账号密码登录
  163. const handleAccountLogin = async () => {
  164. if (!agreePrivacy.value) {
  165. toast.error('请阅读同意协议')
  166. return
  167. }
  168. // 表单验证
  169. if (!loginForm.value.username) {
  170. toast.error('请输入用户名')
  171. return
  172. }
  173. if (!loginForm.value.password) {
  174. toast.error('请输入密码')
  175. return
  176. }
  177. if (captcha.value.captchaEnabled && !loginForm.value.code) {
  178. toast.error('请输入验证码')
  179. return
  180. }
  181. // 执行登录
  182. await userStore.login(loginForm.value)
  183. // 跳转到首页或重定向页面
  184. const targetUrl = redirectRoute.value || '/pages/index/index'
  185. if (isTableBar(targetUrl)) {
  186. uni.switchTab({ url: targetUrl })
  187. } else {
  188. uni.redirectTo({ url: targetUrl })
  189. }
  190. }
  191. // 微信登录
  192. const handleWechatLogin = async () => {
  193. if (!isMpWeixin) {
  194. toast.info('请在微信小程序中使用此功能')
  195. return
  196. }
  197. // 验证是否同意隐私协议
  198. if (!agreePrivacy.value) {
  199. toast.error('请先阅读并同意用户协议和隐私政策')
  200. return
  201. }
  202. // 微信登录
  203. await userStore.wxLogin()
  204. // 跳转到首页或重定向页面
  205. const targetUrl = redirectRoute.value || '/pages/index/index'
  206. if (isTableBar(targetUrl)) {
  207. uni.switchTab({ url: targetUrl })
  208. } else {
  209. uni.redirectTo({ url: targetUrl })
  210. }
  211. }
  212. // 刷新验证码
  213. const refreshCaptcha = () => {
  214. // 获取验证码
  215. getCode().then((res) => {
  216. const { data } = res
  217. loginForm.value.uuid = data.uuid
  218. captcha.value = data
  219. })
  220. }
  221. // 处理协议点击
  222. const handleAgreement = (type: 'user' | 'privacy') => {
  223. const title = type === 'user' ? '用户协议' : '隐私政策'
  224. // showToast(`查看${title}`)
  225. // 实际项目中可以跳转到对应的协议页面
  226. // uni.navigateTo({
  227. // url: `/pages/agreement/${type}`
  228. // })
  229. }
  230. </script>
  231. <style lang="scss" scoped>
  232. /* 验证码输入框样式 */
  233. .captcha-wrapper {
  234. .captcha-input {
  235. :deep(.wd-input__suffix) {
  236. margin-right: 0;
  237. padding-right: 0;
  238. }
  239. }
  240. .captcha-image {
  241. width: 100px;
  242. height: 36px;
  243. margin-left: 10px;
  244. border-radius: 8px;
  245. cursor: pointer;
  246. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  247. transition: all 0.3s ease;
  248. position: relative;
  249. overflow: hidden;
  250. &::after {
  251. content: '';
  252. position: absolute;
  253. top: 0;
  254. left: 0;
  255. right: 0;
  256. bottom: 0;
  257. background: linear-gradient(to bottom, rgba(255, 255, 255, 0.1), transparent);
  258. pointer-events: none;
  259. }
  260. &:active {
  261. opacity: 0.8;
  262. transform: scale(0.96);
  263. box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
  264. }
  265. }
  266. }
  267. .login-container {
  268. box-sizing: border-box;
  269. display: flex;
  270. flex-direction: column;
  271. min-height: 100vh;
  272. padding: 0 70rpx;
  273. background-color: #ffffff;
  274. background-image: linear-gradient(
  275. 135deg,
  276. rgba(25, 137, 250, 0.05) 0%,
  277. rgba(255, 255, 255, 0) 100%
  278. );
  279. position: relative;
  280. overflow: hidden;
  281. }
  282. /* 背景装饰元素 */
  283. .bg-decoration {
  284. position: absolute;
  285. border-radius: 50%;
  286. background: linear-gradient(135deg, rgba(25, 137, 250, 0.05), rgba(25, 137, 250, 0.1));
  287. z-index: 0;
  288. pointer-events: none;
  289. }
  290. .bg-circle-1 {
  291. width: 500rpx;
  292. height: 500rpx;
  293. top: -200rpx;
  294. right: -200rpx;
  295. opacity: 0.6;
  296. }
  297. .bg-circle-2 {
  298. width: 400rpx;
  299. height: 400rpx;
  300. bottom: 10%;
  301. left: -200rpx;
  302. opacity: 0.4;
  303. }
  304. .bg-circle-3 {
  305. width: 300rpx;
  306. height: 300rpx;
  307. bottom: -100rpx;
  308. right: 10%;
  309. opacity: 0.3;
  310. background: linear-gradient(135deg, rgba(7, 193, 96, 0.05), rgba(7, 193, 96, 0.1));
  311. }
  312. .login-header {
  313. display: flex;
  314. flex-direction: column;
  315. align-items: center;
  316. justify-content: center;
  317. margin-top: 120rpx;
  318. animation: fadeInDown 0.8s ease-out;
  319. .login-logo {
  320. width: 200rpx;
  321. height: 200rpx;
  322. border-radius: 36rpx;
  323. box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.12);
  324. transition: all 0.3s ease;
  325. &:active {
  326. transform: scale(0.95);
  327. box-shadow: 0 6rpx 15rpx rgba(0, 0, 0, 0.1);
  328. }
  329. }
  330. .login-title {
  331. margin-top: 30rpx;
  332. font-size: 46rpx;
  333. font-weight: bold;
  334. color: #333333;
  335. letter-spacing: 3rpx;
  336. text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
  337. }
  338. }
  339. .login-form {
  340. flex: 1;
  341. margin-top: 70rpx;
  342. animation: fadeIn 0.8s ease-out 0.2s both;
  343. .welcome-text {
  344. margin-bottom: 16rpx;
  345. font-size: 48rpx;
  346. font-weight: bold;
  347. color: #333333;
  348. text-align: center;
  349. letter-spacing: 1rpx;
  350. }
  351. .login-desc {
  352. margin-bottom: 70rpx;
  353. font-size: 28rpx;
  354. color: #888888;
  355. text-align: center;
  356. }
  357. .login-input-group {
  358. margin-bottom: 60rpx;
  359. position: relative;
  360. z-index: 1;
  361. .input-wrapper {
  362. position: relative;
  363. margin-bottom: 50rpx;
  364. transition: all 0.3s ease;
  365. border-radius: 16rpx;
  366. overflow: hidden;
  367. &:last-child {
  368. margin-bottom: 0;
  369. }
  370. .login-input {
  371. padding: 12rpx 20rpx;
  372. background-color: rgba(245, 247, 250, 0.7);
  373. border-radius: 16rpx;
  374. transition: all 0.3s ease;
  375. :deep(.wd-input__inner) {
  376. font-size: 30rpx;
  377. color: #333333;
  378. }
  379. :deep(.wd-input__placeholder) {
  380. font-size: 28rpx;
  381. color: #aaaaaa;
  382. }
  383. &:focus-within {
  384. background-color: rgba(245, 247, 250, 0.95);
  385. box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.06);
  386. transform: translateY(-3rpx);
  387. }
  388. }
  389. .input-bottom-line {
  390. position: absolute;
  391. bottom: -2rpx;
  392. left: 5%;
  393. width: 90%;
  394. height: 2rpx;
  395. background: linear-gradient(
  396. to right,
  397. transparent,
  398. var(--wot-color-theme, #1989fa),
  399. transparent
  400. );
  401. transition: transform 0.4s ease;
  402. transform: scaleX(0);
  403. opacity: 0.8;
  404. }
  405. &:focus-within .input-bottom-line {
  406. transform: scaleX(1);
  407. }
  408. .input-icon {
  409. margin-right: 16rpx;
  410. color: #666666;
  411. transition: color 0.3s ease;
  412. }
  413. &:focus-within .input-icon {
  414. color: var(--wot-color-theme, #1989fa);
  415. }
  416. }
  417. }
  418. .login-buttons {
  419. display: flex;
  420. flex-direction: column;
  421. gap: 36rpx;
  422. .account-login-btn {
  423. height: 96rpx;
  424. margin-top: 20rpx;
  425. font-size: 32rpx;
  426. font-weight: 500;
  427. letter-spacing: 2rpx;
  428. border-radius: 48rpx;
  429. box-shadow: 0 10rpx 20rpx rgba(25, 137, 250, 0.25);
  430. transition: all 0.3s ease;
  431. display: flex;
  432. align-items: center;
  433. justify-content: center;
  434. .login-icon {
  435. margin-right: 8rpx;
  436. opacity: 0.8;
  437. transition: all 0.3s ease;
  438. }
  439. &:active {
  440. box-shadow: 0 5rpx 10rpx rgba(25, 137, 250, 0.2);
  441. transform: scale(0.98);
  442. .login-icon {
  443. transform: translateX(3rpx);
  444. }
  445. }
  446. }
  447. .divider {
  448. display: flex;
  449. align-items: center;
  450. margin: 24rpx 0;
  451. .divider-line {
  452. flex: 1;
  453. height: 1px;
  454. background-color: #eeeeee;
  455. }
  456. .divider-text {
  457. padding: 0 24rpx;
  458. font-size: 24rpx;
  459. color: #999999;
  460. }
  461. }
  462. .wechat-login-btn {
  463. height: 96rpx;
  464. font-size: 32rpx;
  465. color: #07c160;
  466. border-color: #07c160;
  467. border-radius: 48rpx;
  468. transition: all 0.3s ease;
  469. .wechat-icon {
  470. margin-right: 12rpx;
  471. }
  472. &:active {
  473. background-color: rgba(7, 193, 96, 0.08);
  474. transform: scale(0.98);
  475. }
  476. }
  477. }
  478. }
  479. .privacy-agreement {
  480. display: flex;
  481. justify-content: center;
  482. margin: 30rpx 0 40rpx;
  483. animation: fadeIn 0.8s ease-out 0.4s both;
  484. .privacy-checkbox {
  485. display: flex;
  486. align-items: center;
  487. }
  488. .agreement-text {
  489. font-size: 26rpx;
  490. line-height: 1.6;
  491. color: #666666;
  492. .agreement-link {
  493. padding: 0 4rpx;
  494. font-weight: 500;
  495. color: var(--wot-color-theme, #1989fa);
  496. transition: all 0.3s ease;
  497. &:active {
  498. opacity: 0.8;
  499. transform: scale(0.98);
  500. }
  501. }
  502. }
  503. }
  504. .login-footer {
  505. padding: 50rpx 0;
  506. margin-top: auto;
  507. }
  508. /* 添加动画效果 */
  509. @keyframes fadeIn {
  510. from {
  511. opacity: 0;
  512. }
  513. to {
  514. opacity: 1;
  515. }
  516. }
  517. @keyframes fadeInDown {
  518. from {
  519. opacity: 0;
  520. transform: translateY(-20px);
  521. }
  522. to {
  523. opacity: 1;
  524. transform: translateY(0);
  525. }
  526. }
  527. </style>