西藏巴青项目

index.vue 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. <template>
  2. <view :key="layoutKey" :class="pageRootClass" class="login-page">
  3. <!-- 顶部品牌区 -->
  4. <view class="login-hero">
  5. <view class="login-hero__orb login-hero__orb--1" />
  6. <view class="login-hero__orb login-hero__orb--2" />
  7. <view class="login-hero__orb login-hero__orb--3" />
  8. <view class="login-hero__brand">
  9. <!-- <view class="login-hero__logo">
  10. <up-icon name="home-fill" color="#ffffff" :size="44" />
  11. </view> -->
  12. <text class="login-hero__app text-body">{{ $t('app.name') }}</text>
  13. <text class="login-hero__welcome text-body">{{ $t('loginPage.welcome') }}</text>
  14. </view>
  15. </view>
  16. <!-- 表单卡片 -->
  17. <view class="login-body">
  18. <view class="login-card">
  19. <text class="login-card__title text-body">{{ $t('loginPage.title') }}</text>
  20. <text class="login-card__sub text-body">{{ $t('loginPage.subtitle') }}</text>
  21. <view class="login-field">
  22. <view class="login-field__box">
  23. <view class="login-field__icon">
  24. <up-icon name="account" color="#6b7f72" :size="22" />
  25. </view>
  26. <input
  27. v-model="form.username"
  28. class="login-field__input text-body"
  29. type="text"
  30. :placeholder="$t('loginPage.usernamePlaceholder')"
  31. placeholder-class="login-field__placeholder"
  32. confirm-type="next"
  33. />
  34. </view>
  35. </view>
  36. <view class="login-field">
  37. <view class="login-field__box">
  38. <view class="login-field__icon">
  39. <up-icon name="lock" color="#6b7f72" :size="22" />
  40. </view>
  41. <input
  42. v-model="form.password"
  43. class="login-field__input text-body"
  44. type="password"
  45. :placeholder="$t('loginPage.passwordPlaceholder')"
  46. placeholder-class="login-field__placeholder"
  47. confirm-type="done"
  48. @confirm="handleLogin"
  49. />
  50. </view>
  51. </view>
  52. <view v-if="captchaEnabled" class="login-field login-captcha">
  53. <view class="login-field__box login-captcha__input-wrap">
  54. <view class="login-field__icon">
  55. <up-icon name="order" color="#6b7f72" :size="22" />
  56. </view>
  57. <input
  58. v-model="form.code"
  59. class="login-field__input text-body"
  60. type="text"
  61. :placeholder="$t('loginPage.codePlaceholder')"
  62. placeholder-class="login-field__placeholder"
  63. />
  64. </view>
  65. <image
  66. v-if="codeUrl"
  67. class="login-captcha__img"
  68. :src="codeUrl"
  69. mode="aspectFit"
  70. @click="loadCaptcha"
  71. />
  72. </view>
  73. <view class="login-remember" @click="form.rememberMe = !form.rememberMe">
  74. <view :class="['login-remember__check', { 'login-remember__check--on': form.rememberMe }]">
  75. <up-icon v-if="form.rememberMe" name="checkmark" color="#ffffff" :size="14" />
  76. </view>
  77. <text class="login-remember__txt text-body">{{ $t('loginPage.rememberMe') }}</text>
  78. </view>
  79. <view
  80. :class="['login-btn', { 'login-btn--loading': loading }]"
  81. role="button"
  82. @click="handleLogin"
  83. >
  84. <text class="login-btn__txt text-body">
  85. {{ loading ? $t('loginPage.submitting') : $t('loginPage.submit') }}
  86. </text>
  87. </view>
  88. </view>
  89. <text class="login-footer text-body">{{ $t('loginPage.footerHint') }}</text>
  90. </view>
  91. </view>
  92. </template>
  93. <script>
  94. import UIcon from 'uview-plus/components/u-icon/u-icon.vue'
  95. import { getCodeImg } from '@/api/login'
  96. import { getToken } from '@/utils/auth'
  97. import { useUserStore } from '@/store/user'
  98. import { REMEMBER_USERNAME_KEY } from '@/config'
  99. const HOME_URL = '/pages/home/index'
  100. export default {
  101. components: {
  102. 'up-icon': UIcon
  103. },
  104. data() {
  105. return {
  106. layoutKey: 0,
  107. loading: false,
  108. captchaEnabled: false,
  109. codeUrl: '',
  110. form: {
  111. username: '',
  112. password: '',
  113. code: '',
  114. uuid: '',
  115. rememberMe: false
  116. }
  117. }
  118. },
  119. computed: {
  120. lang() {
  121. return this.$i18n.locale
  122. },
  123. pageRootClass() {
  124. return this.lang === 'bo' ? 'lang-bo' : 'lang-zh'
  125. }
  126. },
  127. onLoad() {
  128. if (getToken()) {
  129. this.goHome()
  130. return
  131. }
  132. this.loadRememberedUsername()
  133. this.loadCaptcha()
  134. },
  135. methods: {
  136. loadCaptcha() {
  137. getCodeImg()
  138. .then((res) => {
  139. this.captchaEnabled =
  140. res.captchaEnabled === undefined ? true : !!res.captchaEnabled
  141. if (this.captchaEnabled && res.img) {
  142. this.codeUrl = 'data:image/gif;base64,' + res.img
  143. this.form.uuid = res.uuid || ''
  144. }
  145. })
  146. .catch(() => {
  147. this.captchaEnabled = false
  148. })
  149. },
  150. loadRememberedUsername() {
  151. const saved = uni.getStorageSync(REMEMBER_USERNAME_KEY)
  152. if (saved) {
  153. this.form.username = saved
  154. this.form.rememberMe = true
  155. }
  156. },
  157. goHome() {
  158. uni.switchTab({ url: HOME_URL })
  159. },
  160. validateForm() {
  161. if (!(this.form.username || '').trim()) {
  162. uni.showToast({ title: this.$t('loginPage.errUsername'), icon: 'none' })
  163. return false
  164. }
  165. if (!this.form.password) {
  166. uni.showToast({ title: this.$t('loginPage.errPassword'), icon: 'none' })
  167. return false
  168. }
  169. if (this.captchaEnabled && !(this.form.code || '').trim()) {
  170. uni.showToast({ title: this.$t('loginPage.errCode'), icon: 'none' })
  171. return false
  172. }
  173. return true
  174. },
  175. handleLogin() {
  176. if (!this.validateForm() || this.loading) {
  177. return
  178. }
  179. if (this.form.rememberMe) {
  180. uni.setStorageSync(REMEMBER_USERNAME_KEY, this.form.username.trim())
  181. } else {
  182. uni.removeStorageSync(REMEMBER_USERNAME_KEY)
  183. }
  184. this.loading = true
  185. const userStore = useUserStore()
  186. userStore.fedLogOut()
  187. const payload = {
  188. username: this.form.username.trim(),
  189. password: this.form.password,
  190. code: this.captchaEnabled ? (this.form.code || '').trim() : '',
  191. uuid: this.captchaEnabled ? this.form.uuid : ''
  192. }
  193. userStore
  194. .login(payload)
  195. .then(() => userStore.fetchUserInfo())
  196. .then(() => {
  197. this.goHome()
  198. })
  199. .catch(() => {
  200. if (this.captchaEnabled) {
  201. this.loadCaptcha()
  202. }
  203. })
  204. .finally(() => {
  205. this.loading = false
  206. })
  207. }
  208. }
  209. }
  210. </script>
  211. <style lang="scss" scoped>
  212. @import '@/styles/morandi.scss';
  213. .login-page {
  214. min-height: 100%;
  215. display: flex;
  216. flex-direction: column;
  217. min-width: 0;
  218. box-sizing: border-box;
  219. background: $morandi-bg-page;
  220. }
  221. /* ---------- 顶部渐变品牌区 ---------- */
  222. .login-hero {
  223. position: relative;
  224. flex-shrink: 0;
  225. min-height: 420rpx;
  226. padding: 80rpx 48rpx 120rpx;
  227. box-sizing: border-box;
  228. overflow: hidden;
  229. background: linear-gradient(145deg, #3d9b6e 0%, #22c55e 42%, #86efac 100%);
  230. }
  231. .login-hero__orb {
  232. position: absolute;
  233. border-radius: 50%;
  234. background: rgba(255, 255, 255, 0.12);
  235. }
  236. .login-hero__orb--1 {
  237. width: 320rpx;
  238. height: 320rpx;
  239. top: -80rpx;
  240. right: -60rpx;
  241. }
  242. .login-hero__orb--2 {
  243. width: 200rpx;
  244. height: 200rpx;
  245. bottom: 40rpx;
  246. left: -40rpx;
  247. }
  248. .login-hero__orb--3 {
  249. width: 120rpx;
  250. height: 120rpx;
  251. top: 120rpx;
  252. left: 60%;
  253. background: rgba(255, 255, 255, 0.08);
  254. }
  255. .login-hero__brand {
  256. position: relative;
  257. z-index: 1;
  258. display: flex;
  259. flex-direction: column;
  260. align-items: flex-start;
  261. gap: 16rpx;
  262. min-width: 0;
  263. }
  264. .login-hero__logo {
  265. display: flex;
  266. align-items: center;
  267. justify-content: center;
  268. padding: 24rpx;
  269. border-radius: 28rpx;
  270. background: rgba(255, 255, 255, 0.22);
  271. border: 1rpx solid rgba(255, 255, 255, 0.35);
  272. box-sizing: border-box;
  273. }
  274. .login-hero__app {
  275. font-size: 44rpx;
  276. font-weight: 700;
  277. color: #ffffff;
  278. line-height: 1.3;
  279. word-break: break-word;
  280. overflow-wrap: anywhere;
  281. }
  282. .login-hero__welcome {
  283. font-size: 28rpx;
  284. color: rgba(255, 255, 255, 0.88);
  285. line-height: 1.5;
  286. word-break: break-word;
  287. overflow-wrap: anywhere;
  288. }
  289. /* ---------- 表单区(卡片上浮) ---------- */
  290. .login-body {
  291. flex: 1;
  292. min-width: 0;
  293. margin-top: 24px;
  294. padding: 0 40rpx 48rpx;
  295. box-sizing: border-box;
  296. }
  297. .login-card {
  298. display: flex;
  299. flex-direction: column;
  300. gap: 28rpx;
  301. min-width: 0;
  302. padding: 48rpx 40rpx 44rpx;
  303. border-radius: 32rpx;
  304. background: $morandi-bg-card;
  305. border: 1rpx solid rgba(255, 255, 255, 0.8);
  306. box-shadow: 0 16rpx 48rpx rgba(74, 69, 66, 0.08);
  307. box-sizing: border-box;
  308. }
  309. .login-card__title {
  310. font-size: 36rpx;
  311. font-weight: 600;
  312. color: $morandi-text;
  313. line-height: 1.35;
  314. }
  315. .login-card__sub {
  316. font-size: 26rpx;
  317. color: $morandi-text-muted;
  318. line-height: 1.55;
  319. margin-top: -12rpx;
  320. margin-bottom: 8rpx;
  321. word-break: break-word;
  322. overflow-wrap: anywhere;
  323. }
  324. .login-field {
  325. min-width: 0;
  326. }
  327. .login-field__box {
  328. display: flex;
  329. flex-direction: row;
  330. align-items: center;
  331. gap: 16rpx;
  332. min-width: 0;
  333. padding: 8rpx 24rpx 8rpx 20rpx;
  334. border-radius: 20rpx;
  335. background: $morandi-composer;
  336. border: 1rpx solid $morandi-border-soft;
  337. box-sizing: border-box;
  338. }
  339. .login-field__icon {
  340. flex-shrink: 0;
  341. display: flex;
  342. align-items: center;
  343. justify-content: center;
  344. }
  345. .login-field__input {
  346. flex: 1;
  347. min-width: 0;
  348. min-height: 72rpx;
  349. font-size: 30rpx;
  350. color: $morandi-text;
  351. line-height: 1.5;
  352. }
  353. .login-field__placeholder {
  354. color: $morandi-text-soft;
  355. font-size: 28rpx;
  356. }
  357. .login-captcha {
  358. display: flex;
  359. flex-direction: row;
  360. align-items: center;
  361. gap: 16rpx;
  362. min-width: 0;
  363. }
  364. .login-captcha__input-wrap {
  365. flex: 1;
  366. min-width: 0;
  367. }
  368. .login-captcha__img {
  369. flex-shrink: 0;
  370. width: 200rpx;
  371. height: 72rpx;
  372. border-radius: 12rpx;
  373. border: 1rpx solid $morandi-border-soft;
  374. background: $morandi-bg-card-inner;
  375. }
  376. .login-remember {
  377. display: flex;
  378. flex-direction: row;
  379. align-items: center;
  380. gap: 16rpx;
  381. min-width: 0;
  382. padding: 4rpx 0;
  383. }
  384. .login-remember__check {
  385. flex-shrink: 0;
  386. width: 36rpx;
  387. height: 36rpx;
  388. border-radius: 10rpx;
  389. border: 2rpx solid $morandi-border-strong;
  390. background: $morandi-bg-card-inner;
  391. display: flex;
  392. align-items: center;
  393. justify-content: center;
  394. box-sizing: border-box;
  395. }
  396. .login-remember__check--on {
  397. background: #22c55e;
  398. border-color: #22c55e;
  399. }
  400. .login-remember__txt {
  401. font-size: 28rpx;
  402. color: $morandi-text-secondary;
  403. line-height: 1.45;
  404. }
  405. .login-btn {
  406. margin-top: 12rpx;
  407. min-width: 0;
  408. padding: 28rpx 32rpx;
  409. border-radius: 48rpx;
  410. background: linear-gradient(90deg, #16a34a 0%, #22c55e 50%, #4ade80 100%);
  411. box-shadow: 0 12rpx 32rpx rgba(34, 197, 94, 0.35);
  412. box-sizing: border-box;
  413. display: flex;
  414. align-items: center;
  415. justify-content: center;
  416. }
  417. .login-btn--loading {
  418. opacity: 0.75;
  419. }
  420. .login-btn__txt {
  421. font-size: 32rpx;
  422. font-weight: 600;
  423. color: #ffffff;
  424. letter-spacing: 4rpx;
  425. }
  426. .login-footer {
  427. display: block;
  428. margin-top: 40rpx;
  429. text-align: center;
  430. font-size: 24rpx;
  431. color: $morandi-text-soft;
  432. line-height: 1.5;
  433. word-break: break-word;
  434. overflow-wrap: anywhere;
  435. }
  436. </style>