login.vue 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. <template>
  2. <view class="login-page">
  3. <view class="login-content">
  4. <!-- Logo -->
  5. <view class="brand">
  6. <view class="brand-mark"></view>
  7. <text class="brand-name">超级进化</text>
  8. <text class="brand-sub">运营管理平台</text>
  9. </view>
  10. <!-- Form -->
  11. <view class="form">
  12. <view class="field">
  13. <view class="field-header">
  14. <AppIcon name="smartphone" size="18" color="#999999" />
  15. <text class="field-label">手机号</text>
  16. </view>
  17. <input
  18. v-model="loginForm.mobilePhone"
  19. type="number"
  20. placeholder="请输入手机号"
  21. maxlength="11"
  22. class="field-input"
  23. />
  24. </view>
  25. <view class="field">
  26. <view class="field-header">
  27. <AppIcon name="lock" size="18" color="#999999" />
  28. <text class="field-label">密码</text>
  29. </view>
  30. <input
  31. v-model="loginForm.password"
  32. type="password"
  33. placeholder="请输入密码"
  34. @confirm="handleLogin"
  35. class="field-input"
  36. />
  37. </view>
  38. <view class="remember-row" @click="rememberMe = !rememberMe">
  39. <view class="remember-check" :class="{ checked: rememberMe }">
  40. <text v-if="rememberMe" class="remember-tick">✓</text>
  41. </view>
  42. <text class="remember-label">记住密码</text>
  43. </view>
  44. <button class="submit-btn" @click="handleLogin" :disabled="loading">
  45. {{ loading ? '登录中...' : '登录' }}
  46. </button>
  47. </view>
  48. </view>
  49. </view>
  50. </template>
  51. <script setup>
  52. import { ref, onMounted } from 'vue'
  53. import { login } from '../../api/auth.js'
  54. import { loadDicts } from '../../utils/dict.js'
  55. import { storage, showToast } from '../../utils/index.js'
  56. import JSEncrypt from 'jsencrypt'
  57. const loading = ref(false)
  58. const rememberMe = ref(false)
  59. const loginForm = ref({
  60. mobilePhone: '',
  61. password: ''
  62. })
  63. const encryptData = (str) => {
  64. let encryptor = new JSEncrypt()
  65. let publicKey = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNc4Zrvk3E0mUkO8NOeNYOOaPI4uLoBAuDt9Rp0urX7y0wq7vvQzytvwzXXeM9Xp89j7g4ZLR7qBLBCj3QNPH0SUjE1yy9KVBKdjkPre7WT+plS74s2rJz/hygKiJ3Vxa+Z15v6JEHy/3/+i9gW3p/bCLaMQtvGemNvDXwCTwINQIDAQAB'
  66. encryptor.setPublicKey(publicKey)
  67. return encryptor.encrypt(str)
  68. }
  69. const REMEMBER_KEY = 'remembered_login'
  70. onMounted(() => {
  71. const saved = storage.get(REMEMBER_KEY)
  72. if (saved) {
  73. loginForm.value.mobilePhone = saved.mobilePhone || ''
  74. loginForm.value.password = saved.password || ''
  75. rememberMe.value = true
  76. }
  77. })
  78. const loadDictionaries = async () => {
  79. await loadDicts()
  80. }
  81. const handleLogin = async () => {
  82. const phone = loginForm.value.mobilePhone.trim()
  83. if (!phone) {
  84. showToast('请输入手机号')
  85. return
  86. }
  87. if (!/^1[3-9]\d{9}$/.test(phone)) {
  88. showToast('请输入正确的手机号')
  89. return
  90. }
  91. if (!loginForm.value.password) {
  92. showToast('请输入密码')
  93. return
  94. }
  95. loading.value = true
  96. try {
  97. const encryptedPassword = encryptData(loginForm.value.password)
  98. const res = await login(phone, encryptedPassword)
  99. if (res && res.code === 200) {
  100. if (rememberMe.value) {
  101. storage.set(REMEMBER_KEY, {
  102. mobilePhone: phone,
  103. password: loginForm.value.password
  104. })
  105. } else {
  106. storage.remove(REMEMBER_KEY)
  107. }
  108. storage.set('token', res.data.accessToken)
  109. storage.set('userInfo', {
  110. id: res.data.id,
  111. name: res.data.username || phone,
  112. mobilePhone: phone
  113. })
  114. await loadDictionaries()
  115. showToast('登录成功', 'success')
  116. setTimeout(() => {
  117. uni.switchTab({
  118. url: '/pages/index/index'
  119. })
  120. }, 800)
  121. } else {
  122. showToast(res.msg || '登录失败')
  123. }
  124. } catch (error) {
  125. console.error('登录失败:', error)
  126. showToast(error.msg || '网络错误,请稍后重试')
  127. } finally {
  128. loading.value = false
  129. }
  130. }
  131. </script>
  132. <style scoped>
  133. .login-page {
  134. min-height: 100vh;
  135. background: #F5F7FA;
  136. display: flex;
  137. align-items: center;
  138. justify-content: center;
  139. padding: 60rpx 40rpx;
  140. box-sizing: border-box;
  141. }
  142. .login-content {
  143. width: 100%;
  144. max-width: 600rpx;
  145. }
  146. /* Brand */
  147. .brand {
  148. margin-bottom: 80rpx;
  149. }
  150. .brand-mark {
  151. width: 8rpx;
  152. height: 40rpx;
  153. background: #C6171E;
  154. border-radius: 4rpx;
  155. margin-bottom: 24rpx;
  156. }
  157. .brand-name {
  158. display: block;
  159. font-size: 48rpx;
  160. font-weight: 700;
  161. color: #1A1A1A;
  162. letter-spacing: 2rpx;
  163. line-height: 1.2;
  164. }
  165. .brand-sub {
  166. display: block;
  167. font-size: 26rpx;
  168. color: #999999;
  169. margin-top: 12rpx;
  170. font-weight: 400;
  171. }
  172. /* Form */
  173. .form {
  174. display: flex;
  175. flex-direction: column;
  176. gap: 32rpx;
  177. }
  178. .field {
  179. display: flex;
  180. flex-direction: column;
  181. gap: 16rpx;
  182. }
  183. .field-header {
  184. display: flex;
  185. align-items: center;
  186. gap: 10rpx;
  187. }
  188. .field-label {
  189. font-size: 26rpx;
  190. font-weight: 500;
  191. color: #666666;
  192. }
  193. .field-input {
  194. height: 96rpx;
  195. padding: 0 28rpx;
  196. background: #F0F0F0;
  197. border: none;
  198. border-radius: 12rpx;
  199. font-size: 28rpx;
  200. color: #1A1A1A;
  201. box-sizing: border-box;
  202. transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  203. }
  204. .field-input:focus {
  205. background: #FFFFFF;
  206. box-shadow: 0 0 0 3px rgba(198, 23, 30, 0.2);
  207. outline: none;
  208. }
  209. .field-input::placeholder {
  210. color: #B0B0B0;
  211. }
  212. /* Remember Me */
  213. .remember-row {
  214. display: flex;
  215. align-items: center;
  216. gap: 14rpx;
  217. cursor: pointer;
  218. }
  219. .remember-check {
  220. width: 36rpx;
  221. height: 36rpx;
  222. border-radius: 8rpx;
  223. border: 2rpx solid #CCCCCC;
  224. display: flex;
  225. align-items: center;
  226. justify-content: center;
  227. transition: all 0.2s;
  228. }
  229. .remember-check.checked {
  230. background: #C6171E;
  231. border-color: #C6171E;
  232. }
  233. .remember-tick {
  234. font-size: 22rpx;
  235. color: #FFFFFF;
  236. font-weight: 700;
  237. line-height: 1;
  238. }
  239. .remember-label {
  240. font-size: 26rpx;
  241. color: #666666;
  242. }
  243. /* Button */
  244. .submit-btn {
  245. width: 100%;
  246. height: 96rpx;
  247. background: #C6171E;
  248. color: #FFFFFF;
  249. border: none;
  250. border-radius: 48rpx;
  251. font-size: 30rpx;
  252. font-weight: 600;
  253. margin-top: 16rpx;
  254. display: flex;
  255. align-items: center;
  256. justify-content: center;
  257. transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
  258. }
  259. .submit-btn:active {
  260. background: #A81212;
  261. transform: scale(0.97);
  262. }
  263. .submit-btn:disabled {
  264. background: #CCCCCC;
  265. opacity: 0.6;
  266. transform: none;
  267. }
  268. </style>