Jelajahi Sumber

首页初始化

haiyang 1 bulan lalu
induk
melakukan
1eef111199
39 mengubah file dengan 1632 tambahan dan 963 penghapusan
  1. 0 3
      .commitlintrc.cjs
  2. 2 2
      env/.env
  3. 1 1
      env/.env.development
  4. 2 0
      package.json
  5. 28 28
      pages.config.ts
  6. 22 11
      pnpm-lock.yaml
  7. 28 17
      src/api/login.ts
  8. 41 36
      src/api/types/login.ts
  9. 148 0
      src/components/discountCoupon.vue
  10. 91 0
      src/components/spendAndSaveCoupon.vue
  11. 84 84
      src/http/alova.ts
  12. 133 132
      src/http/http.ts
  13. 403 14
      src/pages/index/index.vue
  14. TEMPAT SAMPAH
      src/static/images/index/coupon-bg.png
  15. TEMPAT SAMPAH
      src/static/images/index/coupon1.png
  16. TEMPAT SAMPAH
      src/static/images/index/coupon2.png
  17. TEMPAT SAMPAH
      src/static/images/index/icon1.png
  18. TEMPAT SAMPAH
      src/static/images/index/icon2.png
  19. TEMPAT SAMPAH
      src/static/images/index/index-bg.png
  20. TEMPAT SAMPAH
      src/static/images/index/lock.png
  21. TEMPAT SAMPAH
      src/static/tabbar/example.png
  22. TEMPAT SAMPAH
      src/static/tabbar/exampleHL.png
  23. TEMPAT SAMPAH
      src/static/tabbar/home.png
  24. TEMPAT SAMPAH
      src/static/tabbar/homeHL.png
  25. TEMPAT SAMPAH
      src/static/tabbar/home_select.png
  26. TEMPAT SAMPAH
      src/static/tabbar/income.png
  27. TEMPAT SAMPAH
      src/static/tabbar/income_select.png
  28. TEMPAT SAMPAH
      src/static/tabbar/me.png
  29. TEMPAT SAMPAH
      src/static/tabbar/me_selected.png
  30. TEMPAT SAMPAH
      src/static/tabbar/personal.png
  31. TEMPAT SAMPAH
      src/static/tabbar/personalHL.png
  32. TEMPAT SAMPAH
      src/static/tabbar/scan.png
  33. 6 6
      src/store/index.ts
  34. 252 248
      src/store/token.ts
  35. 47 47
      src/store/user.ts
  36. 95 86
      src/tabbar/config.ts
  37. 81 82
      src/tabbar/index.vue
  38. 3 3
      src/utils/index.ts
  39. 165 163
      vite.config.ts

+ 0 - 3
.commitlintrc.cjs

@@ -1,3 +0,0 @@
-module.exports = {
-  extends: ['@commitlint/config-conventional'],
-}

+ 2 - 2
env/.env

@@ -10,7 +10,7 @@ VITE_WX_APPID = 'wx6196e8e71383a3b0'
 VITE_APP_PUBLIC_BASE=/
 
 # 后台请求地址
-VITE_SERVER_BASEURL = 'https://ukw0y1.laf.run'
+VITE_SERVER_BASEURL = 'http://192.168.1.28:8082/jeecg-boot'
 # 备注:如果后台带统一前缀,则也要加到后面,eg: https://ukw0y1.laf.run/api
 
 # 注意,如果是微信小程序,还有一套请求地址的配置,根据 develop、trial、release 分别设置上传地址,见 `src/utils/index.ts`。
@@ -21,7 +21,7 @@ VITE_APP_PROXY_ENABLE = false
 VITE_APP_PROXY_PREFIX = '/fg-api'
 
 # 第二个请求地址 (目前alova中可以使用)
-VITE_SERVER_BASEURL_SECONDARY = 'https://ukw0y1.laf.run'
+VITE_SERVER_BASEURL_SECONDARY = 'http://192.168.1.28:8082/jeecg-boot'
 
 # 认证模式,'single' | 'double' ==> 单token | 双token
 VITE_AUTH_MODE = 'single'

+ 1 - 1
env/.env.development

@@ -6,4 +6,4 @@ VITE_DELETE_CONSOLE = false
 VITE_SHOW_SOURCEMAP = false
 
 # 后台请求地址
-# VITE_SERVER_BASEURL = 'https://dev.xxx.com'
+VITE_SERVER_BASEURL = 'http://192.168.1.28:8082/jeecg-boot'

+ 2 - 0
package.json

@@ -119,6 +119,7 @@
     "alova": "^3.3.3",
     "clipboard": "^2.0.11",
     "dayjs": "1.11.10",
+    "image-tools": "^1.4.0",
     "js-cookie": "^3.0.5",
     "pinia": "2.0.36",
     "pinia-plugin-persistedstate": "3.2.1",
@@ -170,6 +171,7 @@
     "postcss": "^8.4.49",
     "postcss-html": "^1.8.0",
     "postcss-scss": "^4.0.9",
+    "prettier": "^3.7.4",
     "rollup-plugin-visualizer": "^6.0.3",
     "sass": "1.63.2",
     "std-env": "^3.9.0",

+ 28 - 28
pages.config.ts

@@ -2,34 +2,34 @@ import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
 import { tabBar } from './src/tabbar/config'
 
 export default defineUniPages({
-  'globalStyle': {
-    navigationStyle: 'default',
-    navigationBarTitleText: 'unibest',
-    navigationBarBackgroundColor: '#f8f8f8',
-    navigationBarTextStyle: 'black',
-    backgroundColor: '#FFFFFF',
-  },
-  'easycom': {
-    autoscan: true,
-    custom: {
-      '^fg-(.*)': '@/components/fg-$1/fg-$1.vue',
-      '^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)':
-      'z-paging/components/z-paging$1/z-paging$1.vue',
-      '^u--(.*)': 'uview-plus/components/u-$1/u-$1.vue',
-      '^up-(.*)': 'uview-plus/components/u-$1/u-$1.vue',
-      '^u-([^-].*)': 'uview-plus/components/u-$1/u-$1.vue',
+    'globalStyle': {
+        navigationStyle: 'default',
+        navigationBarTitleText: 'unibest',
+        navigationBarBackgroundColor: '#f8f8f8',
+        navigationBarTextStyle: 'black',
+        backgroundColor: '#FFFFFF',
     },
-  },
-  // 平台特定配置
-  'mp-weixin': {
-    setting: {
-      urlCheck: false,
-      es6: true,
-      enhance: true,
-      postcss: true,
+    'easycom': {
+        autoscan: true,
+        custom: {
+            '^fg-(.*)': '@/components/fg-$1/fg-$1.vue',
+            '^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)':
+                'z-paging/components/z-paging$1/z-paging$1.vue',
+            '^u--(.*)': 'uview-plus/components/u-$1/u-$1.vue',
+            '^up-(.*)': 'uview-plus/components/u-$1/u-$1.vue',
+            '^u-([^-].*)': 'uview-plus/components/u-$1/u-$1.vue',
+        },
     },
-    lazyCodeLoading: 'requiredComponents',
-  },
-  // tabbar 的配置统一在 “./src/tabbar/config.ts” 文件中
-  'tabBar': tabBar as any,
+    // 平台特定配置
+    'mp-weixin': {
+        setting: {
+            urlCheck: false,
+            es6: true,
+            enhance: true,
+            postcss: true,
+        },
+        lazyCodeLoading: 'requiredComponents',
+    },
+    // tabbar 的配置统一在 “./src/tabbar/config.ts” 文件中
+    'tabBar': tabBar as any,
 })

+ 22 - 11
pnpm-lock.yaml

@@ -78,6 +78,9 @@ importers:
       dayjs:
         specifier: 1.11.10
         version: 1.11.10
+      image-tools:
+        specifier: ^1.4.0
+        version: 1.4.0
       js-cookie:
         specifier: ^3.0.5
         version: 3.0.5
@@ -226,6 +229,9 @@ importers:
       postcss-scss:
         specifier: ^4.0.9
         version: 4.0.9(postcss@8.5.6)
+      prettier:
+        specifier: ^3.7.4
+        version: 3.7.4
       rollup-plugin-visualizer:
         specifier: ^6.0.3
         version: 6.0.3(rollup@4.50.0)
@@ -4001,6 +4007,9 @@ packages:
     resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
     engines: {node: '>= 4'}
 
+  image-tools@1.4.0:
+    resolution: {integrity: sha512-TKtvJ6iUwM0mfaD4keMnk1ENHFC470QEjBfA3IlvKdEOufzvWbjbaoNcoyYq6HlViF8+d5tOS1ooE6j7CHf1lQ==}
+
   immutable@4.3.7:
     resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==}
 
@@ -5262,8 +5271,8 @@ packages:
     resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==}
     engines: {node: '>=6.0.0'}
 
-  prettier@3.6.2:
-    resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
+  prettier@3.7.4:
+    resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
     engines: {node: '>=14'}
     hasBin: true
 
@@ -8811,10 +8820,10 @@ snapshots:
 
   '@polka/url@1.0.0-next.29': {}
 
-  '@prettier/sync@0.6.1(prettier@3.6.2)':
+  '@prettier/sync@0.6.1(prettier@3.7.4)':
     dependencies:
       make-synchronized: 0.8.0
-      prettier: 3.6.2
+      prettier: 3.7.4
 
   '@quansync/fs@0.1.5':
     dependencies:
@@ -8916,7 +8925,7 @@ snapshots:
 
   '@tootallnate/once@1.1.2': {}
 
-  '@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.22)(prettier@3.6.2)':
+  '@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.22)(prettier@3.7.4)':
     dependencies:
       '@babel/generator': 7.28.3
       '@babel/parser': 7.28.4
@@ -8924,7 +8933,7 @@ snapshots:
       '@babel/types': 7.28.4
       javascript-natural-sort: 0.7.1
       lodash: 4.17.21
-      prettier: 3.6.2
+      prettier: 3.7.4
     optionalDependencies:
       '@vue/compiler-sfc': 3.5.22
     transitivePeerDependencies:
@@ -10677,7 +10686,7 @@ snapshots:
       eslint: 9.34.0(jiti@2.6.1)
       eslint-formatting-reporter: 0.0.0(eslint@9.34.0(jiti@2.6.1))
       eslint-parser-plain: 0.1.1
-      prettier: 3.6.2
+      prettier: 3.7.4
       synckit: 0.9.3
 
   eslint-plugin-import-lite@0.3.0(eslint@9.34.0(jiti@2.6.1))(typescript@5.8.3):
@@ -11357,6 +11366,8 @@ snapshots:
 
   ignore@7.0.5: {}
 
+  image-tools@1.4.0: {}
+
   immutable@4.3.7: {}
 
   import-fresh@3.3.1:
@@ -12678,8 +12689,8 @@ snapshots:
   openapi-ts-request@1.10.0(@types/node@20.19.11)(@vue/compiler-sfc@3.5.22)(chokidar@3.6.0)(typescript@5.8.3):
     dependencies:
       '@clack/prompts': 0.11.0
-      '@prettier/sync': 0.6.1(prettier@3.6.2)
-      '@trivago/prettier-plugin-sort-imports': 5.2.2(@vue/compiler-sfc@3.5.22)(prettier@3.6.2)
+      '@prettier/sync': 0.6.1(prettier@3.7.4)
+      '@trivago/prettier-plugin-sort-imports': 5.2.2(@vue/compiler-sfc@3.5.22)(prettier@3.7.4)
       axios: 1.11.0
       bing-translate-api: 4.1.0
       chalk: 4.1.2
@@ -12693,7 +12704,7 @@ snapshots:
       minimatch: 9.0.5
       mockjs: 1.1.0
       nunjucks: 3.2.4(chokidar@3.6.0)
-      prettier: 3.6.2
+      prettier: 3.7.4
       reserved-words: 0.1.2
       rimraf: 5.0.10
       swagger2openapi: 7.0.8
@@ -12975,7 +12986,7 @@ snapshots:
     dependencies:
       fast-diff: 1.3.0
 
-  prettier@3.6.2: {}
+  prettier@3.7.4: {}
 
   pretty-format@27.5.1:
     dependencies:

+ 28 - 17
src/api/login.ts

@@ -5,8 +5,8 @@ import { http } from '@/http/http'
  * 登录表单
  */
 export interface ILoginForm {
-  username: string
-  password: string
+    username: string
+    password: string
 }
 
 /**
@@ -14,7 +14,7 @@ export interface ILoginForm {
  * @returns ICaptcha 验证码
  */
 export function getCode() {
-  return http.get<ICaptcha>('/user/getCode')
+    return http.get<ICaptcha>('/user/getCode')
 }
 
 /**
@@ -22,7 +22,7 @@ export function getCode() {
  * @param loginForm 登录表单
  */
 export function login(loginForm: ILoginForm) {
-  return http.post<IAuthLoginRes>('/auth/login', loginForm)
+    return http.post<IAuthLoginRes>('/auth/login', loginForm)
 }
 
 /**
@@ -30,35 +30,35 @@ export function login(loginForm: ILoginForm) {
  * @param refreshToken 刷新token
  */
 export function refreshToken(refreshToken: string) {
-  return http.post<IDoubleTokenRes>('/auth/refreshToken', { refreshToken })
+    return http.post<IDoubleTokenRes>('/auth/refreshToken', { refreshToken })
 }
 
 /**
  * 获取用户信息
  */
 export function getUserInfo() {
-  return http.get<IUserInfoRes>('/user/info')
+    return http.get<IUserInfoRes>('/user/info')
 }
 
 /**
  * 退出登录
  */
 export function logout() {
-  return http.get<void>('/auth/logout')
+    return http.get<void>('/auth/logout')
 }
 
 /**
  * 修改用户信息
  */
 export function updateInfo(data: IUpdateInfo) {
-  return http.post('/user/updateInfo', data)
+    return http.post('/user/updateInfo', data)
 }
 
 /**
  * 修改用户密码
  */
 export function updateUserPassword(data: IUpdatePassword) {
-  return http.post('/user/updatePassword', data)
+    return http.post('/user/updatePassword', data)
 }
 
 /**
@@ -66,13 +66,24 @@ export function updateUserPassword(data: IUpdatePassword) {
  * @returns Promise 包含微信登录凭证(code)
  */
 export function getWxCode() {
-  return new Promise<UniApp.LoginRes>((resolve, reject) => {
-    uni.login({
-      provider: 'weixin',
-      success: res => resolve(res),
-      fail: err => reject(new Error(err)),
+    return new Promise<UniApp.LoginRes>((resolve, reject) => {
+        uni.login({
+            provider: 'weixin',
+            success: res => resolve(res),
+            fail: err => reject(new Error(err)),
+        })
+    })
+}
+
+export function getUserProfile() {
+    return new Promise<UniApp.GetUserProfileRes>((resolve, reject) => {
+        uni.getUserInfo({
+            provider: 'weixin',
+            desc: '用于获取登录用户信息',
+            success: res => resolve(res),
+            fail: err => reject(new Error(err)),
+        })
     })
-  })
 }
 
 /**
@@ -80,6 +91,6 @@ export function getWxCode() {
  * @param params 微信登录参数,包含code
  * @returns Promise 包含登录结果
  */
-export function wxLogin(data: { code: string }) {
-  return http.post<IAuthLoginRes>('/auth/wxLogin', data)
+export function wxLogin(code: string) {
+    return http.post<IAuthLoginRes>('/couponCenter/APP/wx/login', { code })
 }

+ 41 - 36
src/api/types/login.ts

@@ -3,79 +3,84 @@ export type AuthMode = 'single' | 'double'
 
 // 单Token响应类型
 export interface ISingleTokenRes {
-  token: string
-  expiresIn: number // 有效期(秒)
+    token: string
+    expiresIn: number // 有效期(秒)
+}
+
+export interface ISingleWXTokenRes {
+    openId: string
+    token: string
 }
 
 // 双Token响应类型
 export interface IDoubleTokenRes {
-  accessToken: string
-  refreshToken: string
-  accessExpiresIn: number // 访问令牌有效期(秒)
-  refreshExpiresIn: number // 刷新令牌有效期(秒)
+    accessToken: string
+    refreshToken: string
+    accessExpiresIn: number // 访问令牌有效期(秒)
+    refreshExpiresIn: number // 刷新令牌有效期(秒)
 }
 
 /**
  * 登录返回的信息,其实就是 token 信息
  */
-export type IAuthLoginRes = ISingleTokenRes | IDoubleTokenRes
+export type IAuthLoginRes = ISingleTokenRes | IDoubleTokenRes | ISingleWXTokenRes
 
 /**
  * 用户信息
  */
 export interface IUserInfoRes {
-  userId: number
-  username: string
-  nickname: string
-  avatar?: string
-  [key: string]: any // 允许其他扩展字段
+    userId: number
+    username: string
+    nickname: string
+    avatar?: string
+    [key: string]: any // 允许其他扩展字段
 }
 
 // 认证存储数据结构
 export interface AuthStorage {
-  mode: AuthMode
-  tokens: ISingleTokenRes | IDoubleTokenRes
-  userInfo?: IUserInfoRes
-  loginTime: number // 登录时间戳
+    mode: AuthMode
+    tokens: ISingleTokenRes | IDoubleTokenRes
+    userInfo?: IUserInfoRes
+    loginTime: number // 登录时间戳
 }
 
 /**
  * 获取验证码
  */
 export interface ICaptcha {
-  captchaEnabled: boolean
-  uuid: string
-  image: string
+    captchaEnabled: boolean
+    uuid: string
+    image: string
 }
 /**
  * 上传成功的信息
  */
 export interface IUploadSuccessInfo {
-  fileId: number
-  originalName: string
-  fileName: string
-  storagePath: string
-  fileHash: string
-  fileType: string
-  fileBusinessType: string
-  fileSize: number
+    fileId: number
+    originalName: string
+    fileName: string
+    storagePath: string
+    fileHash: string
+    fileType: string
+    fileBusinessType: string
+    fileSize: number
 }
 /**
  * 更新用户信息
  */
 export interface IUpdateInfo {
-  id: number
-  name: string
-  sex: string
+    id: number
+    name: string
+    sex: string
 }
 /**
  * 更新用户信息
  */
 export interface IUpdatePassword {
-  id: number
-  oldPassword: string
-  newPassword: string
-  confirmPassword: string
+    id: number
+    oldPassword: string
+    newPassword: string
+    confirmPassword: string
 }
 
 /**
@@ -84,7 +89,7 @@ export interface IUpdatePassword {
  * @returns 是否为单Token响应
  */
 export function isSingleTokenRes(tokenRes: IAuthLoginRes): tokenRes is ISingleTokenRes {
-  return 'token' in tokenRes && !('refreshToken' in tokenRes)
+    return 'token' in tokenRes && !('refreshToken' in tokenRes)
 }
 
 /**
@@ -93,5 +98,5 @@ export function isSingleTokenRes(tokenRes: IAuthLoginRes): tokenRes is ISingleTo
  * @returns 是否为双Token响应
  */
 export function isDoubleTokenRes(tokenRes: IAuthLoginRes): tokenRes is IDoubleTokenRes {
-  return 'accessToken' in tokenRes && 'refreshToken' in tokenRes
+    return 'accessToken' in tokenRes && 'refreshToken' in tokenRes
 }

+ 148 - 0
src/components/discountCoupon.vue

@@ -0,0 +1,148 @@
+<script lang="ts" setup>
+import CouponImg from '@img/index/coupon2.png'
+import { pathToBase64 } from 'image-tools'
+import { ref } from 'vue'
+
+const couponBgBase64 = ref('')
+
+// 将图片路径转换为base64格式
+async function convertImageToBase64() {
+    try {
+        // 转换首页背景图
+        couponBgBase64.value = await pathToBase64(CouponImg)
+
+        console.log('图片转换base64成功')
+    }
+    catch (error) {
+        console.error('图片转换base64失败:', error)
+    }
+}
+
+onLoad(async () => {
+    console.log('测试 uni API 自动引入: onLoad')
+    await convertImageToBase64()
+})
+</script>
+
+<template>
+    <view class="discount-coupon" :style="{ backgroundImage: `url(${couponBgBase64})` }">
+        <view class="discount-content">
+            <view class="discount-amount">
+                <view class="discount-amount-number">
+                    8
+                </view>
+                <view class="discount-amount-unit">
+                    折
+                </view>
+            </view>
+            <view class="discount-desc">
+                <view class="discount-desc-text">
+                    满100元可用
+                </view>
+                <view class="discount-desc-type">
+                    限汽车分类商品使用
+                </view>
+            </view>
+        </view>
+        <view class="discount-desc-time">
+            自领取之日起七日内使用,最高优惠50元
+        </view>
+        <view class="discount-btn">
+            <view>立即</view>
+            <view>发券</view>
+        </view>
+    </view>
+</template>
+
+<style lang="scss" scoped>
+.discount-coupon {
+    width: 670rpx;
+    height: 209rpx;
+    background-repeat: no-repeat;
+    background-position: right top;
+    background-size: cover;
+    position: relative;
+
+    .discount-content {
+        height: 95rpx;
+        position: absolute;
+        top: 38%;
+        left: 40%;
+        transform: translate(-50%, -50%);
+        display: flex;
+        flex-direction: row;
+        justify-content: center;
+        align-items: center;
+        gap: 30rpx;
+
+        .discount-amount {
+            display: flex;
+            flex-direction: row;
+            flex-wrap: nowrap;
+            align-items: flex-end;
+            gap: 6rpx;
+
+            .discount-amount-number {
+                flex: 1;
+                font-weight: 500;
+                font-size: 132rpx;
+                color: #f8204b;
+                margin-bottom: -16rpx;
+            }
+
+            .discount-amount-unit {
+                flex: 1;
+                font-weight: 500;
+                font-size: 56rpx;
+                color: #f8204b;
+            }
+        }
+
+        .discount-desc {
+            display: flex;
+            flex-direction: column;
+            gap: 13rpx;
+            justify-content: center;
+            padding-top: 17rpx;
+
+            .discount-desc-text {
+                font-weight: bold;
+                font-size: 30rpx;
+                color: #333333;
+            }
+
+            .discount-desc-type {
+                font-weight: 400;
+                font-size: 22rpx;
+                color: #333333;
+            }
+        }
+    }
+
+    .discount-desc-time {
+        position: absolute;
+        bottom: 5%;
+        left: 29%;
+        transform: translate(-50%, -50%);
+        font-weight: 400;
+        font-size: 18rpx;
+        color: #666666;
+    }
+
+    .discount-btn {
+        position: absolute;
+        font-weight: 500;
+        font-size: 28rpx;
+        color: #ffffff;
+        top: 50%;
+        right: 20rpx;
+        transform: translate(-51%, -49%);
+
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        gap: 10rpx;
+    }
+}
+</style>

+ 91 - 0
src/components/spendAndSaveCoupon.vue

@@ -0,0 +1,91 @@
+<script lang="ts" setup>
+
+</script>
+
+<template>
+    <view class="spend-and-save-coupon">
+        <view class="coupon-content">
+            <view class="coupon-amount">
+                <view class="coupon-amount-text">
+                    50
+                </view>
+                <view class="coupon-amount-icon">
+                    ¥
+                </view>
+            </view>
+            <view class="coupon-desc">
+                满699元可用
+            </view>
+        </view>
+        <view class="coupon-btn">
+            去发券
+        </view>
+    </view>
+</template>
+
+<style lang="scss" scoped>
+.spend-and-save-coupon {
+    width: 210rpx;
+    height: 205rpx;
+    background: url('@img/index/coupon1.png') no-repeat right top;
+    background-size: cover;
+    position: relative;
+
+    .coupon-content {
+        position: absolute;
+        top: 35%;
+        left: 51%;
+        transform: translate(-50%, -50%);
+        width: 162rpx;
+        height: 158rpx;
+        display: flex;
+        flex-direction: column;
+        justify-content: center;
+        align-items: center;
+        gap: 27rpx;
+
+        .coupon-amount {
+            position: relative;
+
+            .coupon-amount-text {
+                font-weight: 500;
+                font-size: 66rpx;
+                color: #ff4f3b;
+            }
+
+            .coupon-amount-icon {
+                position: absolute;
+                width: 24rpx;
+                height: 24rpx;
+                background-color: #ffffff;
+                border-radius: 50%;
+                bottom: 0;
+                right: 0;
+                transform: translate(55%, -20%);
+                font-weight: 500;
+                font-size: 20rpx;
+                color: #ff1a00;
+                display: flex;
+                justify-content: center;
+                align-items: center;
+            }
+        }
+
+        .coupon-desc {
+            font-weight: 400;
+            font-size: 22rpx;
+            color: #ff1a00;
+        }
+    }
+
+    .coupon-btn {
+        position: absolute;
+        font-weight: 500;
+        font-size: 22rpx;
+        color: #f26e5f;
+        bottom: 2rpx;
+        left: 50%;
+        transform: translate(-54%, -55%);
+    }
+}
+</style>

+ 84 - 84
src/http/alova.ts

@@ -9,111 +9,111 @@ import { ContentTypeEnum, ResultEnum, ShowMessage } from './tools/enum'
 
 // 配置动态Tag
 export const API_DOMAINS = {
-  DEFAULT: import.meta.env.VITE_SERVER_BASEURL,
-  SECONDARY: import.meta.env.VITE_SERVER_BASEURL_SECONDARY,
+    DEFAULT: import.meta.env.VITE_SERVER_BASEURL,
+    SECONDARY: import.meta.env.VITE_SERVER_BASEURL_SECONDARY,
 }
 
 /**
  * 创建请求实例
  */
 const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication<
-  typeof VueHook,
-  typeof uniappRequestAdapter
+    typeof VueHook,
+    typeof uniappRequestAdapter
 >({
-  // 如果下面拦截不到,请使用 refreshTokenOnSuccess by 群友@琛
-  refreshTokenOnError: {
-    isExpired: (error) => {
-      return error.response?.status === ResultEnum.Unauthorized
+    // 如果下面拦截不到,请使用 refreshTokenOnSuccess by 群友@琛
+    refreshTokenOnError: {
+        isExpired: (error) => {
+            return error.response?.status === ResultEnum.Unauthorized
+        },
+        handler: async () => {
+            try {
+                // await authLogin();
+            }
+            catch (error) {
+                // 切换到登录页
+                toLoginPage({ mode: 'reLaunch' })
+                throw error
+            }
+        },
     },
-    handler: async () => {
-      try {
-        // await authLogin();
-      }
-      catch (error) {
-        // 切换到登录页
-        toLoginPage({ mode: 'reLaunch' })
-        throw error
-      }
-    },
-  },
 })
 
 /**
  * alova 请求实例
  */
 const alovaInstance = createAlova({
-  baseURL: API_DOMAINS.DEFAULT,
-  ...AdapterUniapp(),
-  timeout: 5000,
-  statesHook: VueHook,
+    baseURL: API_DOMAINS.DEFAULT,
+    ...AdapterUniapp(),
+    timeout: 5000,
+    statesHook: VueHook,
 
-  beforeRequest: onAuthRequired((method) => {
-    // 设置默认 Content-Type
-    method.config.headers = {
-      ContentType: ContentTypeEnum.JSON,
-      Accept: 'application/json, text/plain, */*',
-      ...method.config.headers,
-    }
+    beforeRequest: onAuthRequired((method) => {
+        // 设置默认 Content-Type
+        method.config.headers = {
+            ContentType: ContentTypeEnum.JSON,
+            Accept: 'application/json, text/plain, */*',
+            ...method.config.headers,
+        }
 
-    const { config } = method
-    const ignoreAuth = !config.meta?.ignoreAuth
-    console.log('ignoreAuth===>', ignoreAuth)
-    // 处理认证信息   自行处理认证问题
-    if (ignoreAuth) {
-      const token = 'getToken()'
-      if (!token) {
-        throw new Error('[请求错误]:未登录')
-      }
-      // method.config.headers.token = token;
-    }
+        const { config } = method
+        const ignoreAuth = !config.meta?.ignoreAuth
+        console.log('ignoreAuth===>', ignoreAuth)
+        // 处理认证信息   自行处理认证问题
+        if (ignoreAuth) {
+            const token = 'getToken()'
+            if (!token) {
+                throw new Error('[请求错误]:未登录')
+            }
+            // method.config.headers.token = token;
+        }
 
-    // 处理动态域名
-    if (config.meta?.domain) {
-      method.baseURL = config.meta.domain
-      console.log('当前域名', method.baseURL)
-    }
-  }),
+        // 处理动态域名
+        if (config.meta?.domain) {
+            method.baseURL = config.meta.domain
+            console.log('当前域名', method.baseURL)
+        }
+    }),
 
-  responded: onResponseRefreshToken((response, method) => {
-    const { config } = method
-    const { requestType } = config
-    const {
-      statusCode,
-      data: rawData,
-      errMsg,
-    } = response as UniNamespace.RequestSuccessCallbackResult
+    responded: onResponseRefreshToken((response, method) => {
+        const { config } = method
+        const { requestType } = config
+        const {
+            statusCode,
+            data: rawData,
+            errMsg,
+        } = response as UniNamespace.RequestSuccessCallbackResult
 
-    // 处理特殊请求类型(上传/下载)
-    if (requestType === 'upload' || requestType === 'download') {
-      return response
-    }
+        // 处理特殊请求类型(上传/下载)
+        if (requestType === 'upload' || requestType === 'download') {
+            return response
+        }
 
-    // 处理 HTTP 状态码错误
-    if (statusCode !== 200) {
-      const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`
-      console.error('errorMessage===>', errorMessage)
-      uni.showToast({
-        title: errorMessage,
-        icon: 'error',
-      })
-      throw new Error(`${errorMessage}:${errMsg}`)
-    }
+        // 处理 HTTP 状态码错误
+        if (statusCode !== 200) {
+            const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]`
+            console.error('errorMessage===>', errorMessage)
+            uni.showToast({
+                title: errorMessage,
+                icon: 'error',
+            })
+            throw new Error(`${errorMessage}:${errMsg}`)
+        }
 
-    // 处理业务逻辑错误
-    const { code, message, data } = rawData as IResponse
-    // 0和200当做成功都很普遍,这里直接兼容两者,见 ResultEnum
-    if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
-      if (config.meta?.toast !== false) {
-        uni.showToast({
-          title: message,
-          icon: 'none',
-        })
-      }
-      throw new Error(`请求错误[${code}]:${message}`)
-    }
-    // 处理成功响应,返回业务数据
-    return data
-  }),
+        // 处理业务逻辑错误
+        const { code, message, data } = rawData as IResponse
+        // 0和200当做成功都很普遍,这里直接兼容两者,见 ResultEnum
+        if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
+            if (config.meta?.toast !== false) {
+                uni.showToast({
+                    title: message,
+                    icon: 'none',
+                })
+            }
+            throw new Error(`请求错误[${code}]:${message}`)
+        }
+        // 处理成功响应,返回业务数据
+        return data
+    }),
 })
 
 export const http = alovaInstance

+ 133 - 132
src/http/http.ts

@@ -11,117 +11,118 @@ let refreshing = false // 防止重复刷新 token 标识
 let taskQueue: (() => void)[] = [] // 刷新 token 请求队列
 
 export function http<T>(options: CustomRequestOptions) {
-  // 1. 返回 Promise 对象
-  return new Promise<T>((resolve, reject) => {
-    uni.request({
-      ...options,
-      dataType: 'json',
-      // #ifndef MP-WEIXIN
-      responseType: 'json',
-      // #endif
-      // 响应成功
-      success: async (res) => {
-        const responseData = res.data as IResponse<T>
-        const { code } = responseData
+    // 1. 返回 Promise 对象
+    return new Promise<T>((resolve, reject) => {
+        uni.request({
+            ...options,
+            dataType: 'json',
+            // #ifndef MP-WEIXIN
+            responseType: 'json',
+            // #endif
+            // 响应成功
+            success: async (res) => {
+                console.log('http-success: ', res)
+                const responseData = res.data as IResponse<T>
+                const { code } = responseData
 
-        // 检查是否是401错误(包括HTTP状态码401或业务码401)
-        const isTokenExpired = res.statusCode === 401 || code === 401
+                // 检查是否是401错误(包括HTTP状态码401或业务码401)
+                const isTokenExpired = res.statusCode === 401 || code === 401
 
-        if (isTokenExpired) {
-          const tokenStore = useTokenStore()
-          if (!isDoubleTokenMode) {
-            // 未启用双token策略,清理用户信息,跳转到登录页
-            tokenStore.logout()
-            toLoginPage()
-            return reject(res)
-          }
+                if (isTokenExpired) {
+                    const tokenStore = useTokenStore()
+                    if (!isDoubleTokenMode) {
+                        // 未启用双token策略,清理用户信息,跳转到登录页
+                        tokenStore.logout()
+                        toLoginPage()
+                        return reject(res)
+                    }
 
-          /* -------- 无感刷新 token ----------- */
-          const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
-          // token 失效的,且有刷新 token 的,才放到请求队列里
-          if (refreshToken) {
-            taskQueue.push(() => {
-              resolve(http<T>(options))
-            })
-          }
+                    /* -------- 无感刷新 token ----------- */
+                    const { refreshToken } = tokenStore.tokenInfo as IDoubleTokenRes || {}
+                    // token 失效的,且有刷新 token 的,才放到请求队列里
+                    if (refreshToken) {
+                        taskQueue.push(() => {
+                            resolve(http<T>(options))
+                        })
+                    }
 
-          // 如果有 refreshToken 且未在刷新中,发起刷新 token 请求
-          if (refreshToken && !refreshing) {
-            refreshing = true
-            try {
-              // 发起刷新 token 请求(使用 store 的 refreshToken 方法)
-              await tokenStore.refreshToken()
-              // 刷新 token 成功
-              refreshing = false
-              nextTick(() => {
-                // 关闭其他弹窗
-                uni.hideToast()
-                uni.showToast({
-                  title: 'token 刷新成功',
-                  icon: 'none',
-                })
-              })
-              // 将任务队列的所有任务重新请求
-              taskQueue.forEach(task => task())
-            }
-            catch (refreshErr) {
-              console.error('刷新 token 失败:', refreshErr)
-              refreshing = false
-              // 刷新 token 失败,跳转到登录页
-              nextTick(() => {
-                // 关闭其他弹窗
-                uni.hideToast()
-                uni.showToast({
-                  title: '登录已过期,请重新登录',
-                  icon: 'none',
-                })
-              })
-              // 清除用户信息
-              await tokenStore.logout()
-              // 跳转到登录页
-              setTimeout(() => {
-                toLoginPage()
-              }, 2000)
-            }
-            finally {
-              // 不管刷新 token 成功与否,都清空任务队列
-              taskQueue = []
-            }
-          }
+                    // 如果有 refreshToken 且未在刷新中,发起刷新 token 请求
+                    if (refreshToken && !refreshing) {
+                        refreshing = true
+                        try {
+                            // 发起刷新 token 请求(使用 store 的 refreshToken 方法)
+                            await tokenStore.refreshToken()
+                            // 刷新 token 成功
+                            refreshing = false
+                            nextTick(() => {
+                                // 关闭其他弹窗
+                                uni.hideToast()
+                                uni.showToast({
+                                    title: 'token 刷新成功',
+                                    icon: 'none',
+                                })
+                            })
+                            // 将任务队列的所有任务重新请求
+                            taskQueue.forEach(task => task())
+                        }
+                        catch (refreshErr) {
+                            console.error('刷新 token 失败:', refreshErr)
+                            refreshing = false
+                            // 刷新 token 失败,跳转到登录页
+                            nextTick(() => {
+                                // 关闭其他弹窗
+                                uni.hideToast()
+                                uni.showToast({
+                                    title: '登录已过期,请重新登录',
+                                    icon: 'none',
+                                })
+                            })
+                            // 清除用户信息
+                            await tokenStore.logout()
+                            // 跳转到登录页
+                            setTimeout(() => {
+                                toLoginPage()
+                            }, 2000)
+                        }
+                        finally {
+                            // 不管刷新 token 成功与否,都清空任务队列
+                            taskQueue = []
+                        }
+                    }
 
-          return reject(res)
-        }
+                    return reject(res)
+                }
 
-        // 处理其他成功状态(HTTP状态码200-299)
-        if (res.statusCode >= 200 && res.statusCode < 300) {
-          // 处理业务逻辑错误
-          if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
-            uni.showToast({
-              icon: 'none',
-              title: responseData.msg || responseData.message || '请求错误',
-            })
-          }
-          return resolve(responseData.data)
-        }
+                // 处理其他成功状态(HTTP状态码200-299)
+                if (res.statusCode >= 200 && res.statusCode < 300) {
+                    // 处理业务逻辑错误
+                    if (code !== ResultEnum.Success0 && code !== ResultEnum.Success200) {
+                        uni.showToast({
+                            icon: 'none',
+                            title: responseData.msg || responseData.message || '请求错误',
+                        })
+                    }
+                    return resolve(responseData.result)
+                }
 
-        // 处理其他错误
-        !options.hideErrorToast
-                && uni.showToast({
-          icon: 'none',
-          title: (res.data as any).msg || '请求错误',
-        })
-        reject(res)
-      },
-      // 响应失败
-      fail(err) {
-        uni.showToast({
-          icon: 'none',
-          title: '网络错误,换个网络试试',
+                // 处理其他错误
+                !options.hideErrorToast
+                    && uni.showToast({
+                        icon: 'none',
+                        title: (res.data as any).msg || '请求错误',
+                    })
+                reject(res)
+            },
+            // 响应失败
+            fail(err) {
+                uni.showToast({
+                    icon: 'none',
+                    title: '网络错误,换个网络试试',
+                })
+                reject(err)
+            },
         })
-        reject(err)
-      },
     })
-  })
 }
 
 /**
@@ -132,13 +133,13 @@ export function http<T>(options: CustomRequestOptions) {
  * @returns
  */
 export function httpGet<T>(url: string, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
-  return http<T>({
-    url,
-    query,
-    method: 'GET',
-    header,
-    ...options,
-  })
+    return http<T>({
+        url,
+        query,
+        method: 'GET',
+        header,
+        ...options,
+    })
 }
 
 /**
@@ -150,40 +151,40 @@ export function httpGet<T>(url: string, query?: Record<string, any>, header?: Re
  * @returns
  */
 export function httpPost<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
-  return http<T>({
-    url,
-    query,
-    data,
-    method: 'POST',
-    header,
-    ...options,
-  })
+    return http<T>({
+        url,
+        query,
+        data,
+        method: 'POST',
+        header,
+        ...options,
+    })
 }
 /**
  * PUT 请求
  */
 export function httpPut<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
-  return http<T>({
-    url,
-    data,
-    query,
-    method: 'PUT',
-    header,
-    ...options,
-  })
+    return http<T>({
+        url,
+        data,
+        query,
+        method: 'PUT',
+        header,
+        ...options,
+    })
 }
 
 /**
  * DELETE 请求(无请求体,仅 query)
  */
 export function httpDelete<T>(url: string, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
-  return http<T>({
-    url,
-    query,
-    method: 'DELETE',
-    header,
-    ...options,
-  })
+    return http<T>({
+        url,
+        query,
+        method: 'DELETE',
+        header,
+        ...options,
+    })
 }
 
 // 支持与 axios 类似的API调用

+ 403 - 14
src/pages/index/index.vue

@@ -1,4 +1,12 @@
 <script lang="ts" setup>
+import couponBg from '@img/index/coupon-bg.png'
+import indexBg from '@img/index/index-bg.png'
+import { pathToBase64 } from 'image-tools'
+import { ref } from 'vue'
+import DiscountCoupon from '@/components/discountCoupon.vue'
+import SpendAndSaveCoupon from '@/components/spendAndSaveCoupon.vue'
+import { useTokenStore } from '@/store/token'
+
 defineOptions({
     name: 'Home',
 })
@@ -11,26 +19,407 @@ definePage({
         navigationBarTitleText: '首页',
     },
 })
+const couponBgBase64 = ref(couponBg)
+const indexBgBase64 = ref(indexBg)
+
+const tokenStore = useTokenStore()
+const { hasLogin, wxLogin } = tokenStore
+
+console.log('hasLogin:', hasLogin)
+
+// 将图片路径转换为base64格式
+async function convertImageToBase64() {
+    try {
+        // 转换首页背景图
+        indexBgBase64.value = await pathToBase64(indexBg)
+
+        // 转换优惠券背景图
+        couponBgBase64.value = await pathToBase64(couponBg)
+
+        console.log('图片转换base64成功')
+    }
+    catch (error) {
+        console.error('图片转换base64失败:', error)
+    }
+}
 
-onLoad(() => {
+onLoad(async () => {
     console.log('测试 uni API 自动引入: onLoad')
+    await convertImageToBase64()
 })
+
+// #ifdef MP-WEIXIN
+async function login() {
+    await tokenStore.wxLogin()
+}
+// #endif
+
+// 顶部导航栏高度,设置banner位置
+const navigationBarHeight = ref(0)
+// #ifdef MP-WEIXIN
+function getNavigationBarHeight() {
+    uni.getSystemInfo({
+        success: (res) => {
+            const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
+            console.log('顶部导航栏高度:', res.statusBarHeight, menuButtonInfo)
+            // 顶部导航栏高度 = 状态栏高度 + 胶囊的高度
+            navigationBarHeight.value = res.statusBarHeight + menuButtonInfo.height + 12
+        },
+    })
+}
+getNavigationBarHeight()
+// #endif
 </script>
 
 <template>
-    <view class="bg-white px-4 pt-safe">
-        <view class="mt-10">
-            <up-coupon
-                :amount="200"
-                unit="¥"
-                title="大额优惠券"
-                desc="仅限VIP用户"
-                limit="满500可用"
-                time="有效期至2023-12-31"
-                size="large"
-                type="error"
-            />
-            <up-input border="surround" />
+    <view class="home-container">
+        <!-- 顶部区域 -->
+        <view class="home-header" :style="{ backgroundImage: `url('${indexBgBase64}')` }">
+            <view class="home-header-avatar-info" :style="{ paddingTop: `${navigationBarHeight + 10}px` }">
+                <view class="absolute left-5 z-1 text-xl c-white" :style="{ top: `${navigationBarHeight - 39}px` }">
+                    券中心
+                </view>
+                <view class="home-header-balance">
+                    我的收益(元)
+                </view>
+                <view class="home-header-balance-num">
+                    <view class="home-header-balance-num-amount">
+                        7297491.08
+                    </view>
+                </view>
+            </view>
+            <view class="home-header-tips">
+                <view class="home-header-tips-item">
+                    <view class="home-header-tips-item-num">
+                        115张
+                    </view>
+                    <view class="home-header-tips-item-des">
+                        已核销
+                    </view>
+                </view>
+                <view class="home-header-tips-item">
+                    <view class="home-header-tips-item-num">
+                        68张
+                    </view>
+                    <view class="home-header-tips-item-des">
+                        未核销
+                    </view>
+                </view>
+                <view class="home-header-tips-item">
+                    <view class="home-header-tips-item-num">
+                        68张
+                    </view>
+                    <view class="home-header-tips-item-des">
+                        已发放
+                    </view>
+                </view>
+            </view>
+            <view v-if="!hasLogin" class="home-hidden" @click="login">
+                <image class="home-hidden-img" src="@img/index/lock.png" mode="scaleToFill" />
+                <view class="home-hidden-text">
+                    请登录,查看更多内容~
+                </view>
+            </view>
+        </view>
+        <!-- 满减券 -->
+        <view class="home-header-coupon" :style="{ backgroundImage: `url('${couponBgBase64}')` }">
+            <view class="home-header-coupon-title">
+                <image class="home-header-coupon-title-icon" src="@img/index/icon1.png" mode="scaleToFill" />
+                <view class="home-header-coupon-title-text">
+                    满减券
+                </view>
+                <view class="home-header-coupon-title-des">
+                    平台满减&nbsp;&nbsp;乐享不停
+                </view>
+            </view>
+            <view class="home-header-coupon-content">
+                <spend-and-save-coupon />
+                <spend-and-save-coupon />
+                <spend-and-save-coupon />
+            </view>
+            <view class="home-header-coupon-btn">
+                <up-button class="home-header-coupon-btn-text" text="查看更多优惠券"
+                    color="linear-gradient(0deg, #FFE8CE 0%, #FBB8A0 100%)" />
+            </view>
+        </view>
+        <!-- 折扣券 -->
+        <view class="home-coupon">
+            <view class="home-coupon-title">
+                <image class="home-coupon-title-icon" src="@img/index/icon2.png" mode="scaleToFill" />
+                <view class="home-coupon-title-text">
+                    折扣券
+                </view>
+                <view class="home-coupon-title-des">
+                    分享折扣&nbsp;&nbsp;立享优惠
+                </view>
+                <view class="home-coupon-title-more">
+                    <view class="home-coupon-title-more-text">
+                        更多
+                    </view>
+                    <up-icon size="14" name="arrow-right" />
+                </view>
+            </view>
+            <view class="home-coupon-content">
+                <discount-coupon />
+                <discount-coupon />
+                <discount-coupon />
+            </view>
         </view>
     </view>
 </template>
+
+<style lang="scss" scoped>
+.home-container {
+    font-family: Alibaba PuHuiTi;
+    min-height: 100vh;
+    background-color: #f5f5f5;
+    line-height: 1;
+    position: relative;
+
+    .home-header {
+        height: 550rpx;
+        // background-image: url('@img/index/index-bg.png');
+        background-repeat: no-repeat;
+        background-position: -80px top;
+        background-size: 125% 200%;
+        box-sizing: border-box;
+
+        .home-header-avatar-info {
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            text-align: center;
+            gap: 30rpx;
+            padding: 0 24rpx;
+
+            .home-header-balance {
+                font-weight: 400;
+                font-size: 26rpx;
+                color: #ffffff;
+            }
+
+            .home-header-balance-num {
+                display: flex;
+                justify-content: space-between;
+                align-items: center;
+
+                .home-header-balance-num-amount {
+                    width: 100%;
+                    font-weight: 500;
+                    font-size: 65rpx;
+                    color: #ffffff;
+                }
+
+                .home-header-balance-num-btns {
+                    display: flex;
+                    gap: 15rpx;
+
+                    .home-header-balance-num-btn {
+                        padding: 20rpx 40rpx;
+                        border-radius: 33rpx;
+                        font-weight: 400;
+                        font-size: 26rpx;
+
+                        &.js {
+                            background: #da4c47;
+                            color: #ffffff;
+                        }
+
+                        &.tx {
+                            background: #bfbfbf;
+                            color: #747474;
+                        }
+                    }
+                }
+            }
+        }
+
+        .home-header-tips {
+            height: 124rpx;
+            display: flex;
+            // background: linear-gradient(114deg, #f67873, #fb847f, #f67873);
+            border-radius: 10rpx;
+            margin: 47rpx 24rpx 0 24rpx;
+
+            .home-header-tips-item {
+                flex: 1;
+                display: flex;
+                flex-direction: column;
+                justify-content: center;
+                align-items: center;
+                gap: 15rpx;
+                font-weight: 500;
+                font-size: 34rpx;
+                color: #ffffff;
+                line-height: 1;
+                position: relative;
+
+                &:not(:last-child):after {
+                    content: '';
+                    position: absolute;
+                    top: 50%;
+                    right: 0;
+                    transform: translateY(-50%);
+                    width: 2px;
+                    height: 40rpx;
+                    background: #ffffff;
+                }
+
+                .home-header-tips-item-num {
+                    font-weight: bold;
+                }
+
+                .home-header-tips-item-des {
+                    font-weight: 400;
+                    font-size: 24rpx;
+                    color: #ffffff;
+                }
+            }
+        }
+
+        .home-hidden {
+            height: 550rpx;
+            width: 100%;
+            position: absolute;
+            top: 0;
+            right: 0;
+            display: flex;
+            flex-direction: column;
+            justify-content: center;
+            align-items: center;
+            background-color: rgba(255, 255, 255, 0.1);
+            backdrop-filter: blur(25rpx);
+            -webkit-backdrop-filter: blur(25rpx);
+
+            .home-hidden-img {
+                width: 209rpx;
+                height: 209rpx;
+                object-fit: cover;
+            }
+
+            .home-hidden-text {
+                font-weight: 400;
+                font-size: 26rpx;
+                color: #ffffff;
+                margin-top: -25rpx;
+            }
+        }
+    }
+
+    // 优惠券
+    .home-header-coupon {
+        position: relative;
+        background-repeat: no-repeat;
+        background-position: right top;
+        background-size: 100% auto;
+        height: 419rpx;
+        margin: -75rpx 20rpx 20rpx 20rpx;
+        gap: 20rpx;
+        padding: 28rpx 20rpx;
+
+        .home-header-coupon-title {
+            display: flex;
+            flex-direction: row;
+
+            .home-header-coupon-title-icon {
+                width: 38rpx;
+                height: 38rpx;
+                object-fit: cover;
+                margin-right: 13rpx;
+            }
+
+            .home-header-coupon-title-text {
+                font-weight: 500;
+                font-size: 30rpx;
+                color: #333333;
+                margin-right: 21rpx;
+            }
+
+            .home-header-coupon-title-des {
+                display: inline-flex;
+                align-items: center;
+                font-size: 24rpx;
+                font-weight: 400;
+                color: #888888;
+            }
+        }
+
+        .home-header-coupon-content {
+            height: 238rpx;
+            display: flex;
+            flex-direction: row;
+            align-items: flex-end;
+            gap: 20rpx;
+        }
+
+        .home-header-coupon-btn {
+            padding: 21rpx 70rpx 22px 69rpx;
+
+            .home-header-coupon-btn-text {
+                box-shadow: 0rpx 4rpx 7rpx 0rpx rgba(230, 77, 13, 0.17);
+                border-radius: 37rpx;
+                color: #651e03 !important;
+                font-size: 26rpx;
+                font-weight: 400;
+            }
+        }
+    }
+
+    // 折扣券
+    .home-coupon {
+        margin: -20rpx 20rpx 20rpx 20rpx;
+        padding: 28rpx 20rpx;
+        background-color: #ffffff;
+        border-radius: 15rpx;
+
+        .home-coupon-title {
+            display: flex;
+            flex-direction: row;
+
+            .home-coupon-title-icon {
+                width: 38rpx;
+                height: 38rpx;
+                object-fit: cover;
+                margin-right: 13rpx;
+            }
+
+            .home-coupon-title-text {
+                font-weight: 500;
+                font-size: 30rpx;
+                color: #333333;
+                margin-right: 21rpx;
+            }
+
+            .home-coupon-title-des {
+                display: inline-flex;
+                align-items: center;
+                font-size: 24rpx;
+                font-weight: 400;
+                color: #888888;
+            }
+
+            .home-coupon-title-more {
+                margin-left: auto;
+                display: flex;
+                flex-direction: row;
+                align-items: center;
+
+                .home-coupon-title-more-text {
+                    font-weight: 400;
+                    font-size: 24rpx;
+                    color: #666666;
+                    height: 23rpx;
+                    line-height: 23rpx;
+                }
+            }
+        }
+
+        .home-coupon-content {
+            padding-top: 28rpx;
+            display: flex;
+            flex-direction: column;
+            gap: 20rpx;
+        }
+    }
+}
+</style>

TEMPAT SAMPAH
src/static/images/index/coupon-bg.png


TEMPAT SAMPAH
src/static/images/index/coupon1.png


TEMPAT SAMPAH
src/static/images/index/coupon2.png


TEMPAT SAMPAH
src/static/images/index/icon1.png


TEMPAT SAMPAH
src/static/images/index/icon2.png


TEMPAT SAMPAH
src/static/images/index/index-bg.png


TEMPAT SAMPAH
src/static/images/index/lock.png


TEMPAT SAMPAH
src/static/tabbar/example.png


TEMPAT SAMPAH
src/static/tabbar/exampleHL.png


TEMPAT SAMPAH
src/static/tabbar/home.png


TEMPAT SAMPAH
src/static/tabbar/homeHL.png


TEMPAT SAMPAH
src/static/tabbar/home_select.png


TEMPAT SAMPAH
src/static/tabbar/income.png


TEMPAT SAMPAH
src/static/tabbar/income_select.png


TEMPAT SAMPAH
src/static/tabbar/me.png


TEMPAT SAMPAH
src/static/tabbar/me_selected.png


TEMPAT SAMPAH
src/static/tabbar/personal.png


TEMPAT SAMPAH
src/static/tabbar/personalHL.png


TEMPAT SAMPAH
src/static/tabbar/scan.png


+ 6 - 6
src/store/index.ts

@@ -3,12 +3,12 @@ import { createPersistedState } from 'pinia-plugin-persistedstate' // 数据持
 
 const store = createPinia()
 store.use(
-  createPersistedState({
-    storage: {
-      getItem: uni.getStorageSync,
-      setItem: uni.setStorageSync,
-    },
-  }),
+    createPersistedState({
+        storage: {
+            getItem: uni.getStorageSync,
+            setItem: uni.setStorageSync,
+        },
+    }),
 )
 // 立即激活 Pinia 实例, 这样即使在 app.use(store)之前调用 store 也能正常工作 (解决APP端白屏问题)
 setActivePinia(store)

+ 252 - 248
src/store/token.ts

@@ -1,15 +1,16 @@
 import type {
-  ILoginForm,
+    ILoginForm,
 } from '@/api/login'
 import type { IAuthLoginRes } from '@/api/types/login'
 import { defineStore } from 'pinia'
 import { computed, ref } from 'vue' // 修复:导入 computed
 import {
-  login as _login,
-  logout as _logout,
-  refreshToken as _refreshToken,
-  wxLogin as _wxLogin,
-  getWxCode,
+    login as _login,
+    logout as _logout,
+    refreshToken as _refreshToken,
+    wxLogin as _wxLogin,
+    getUserProfile,
+    getWxCode,
 } from '@/api/login'
 import { isDoubleTokenRes, isSingleTokenRes } from '@/api/types/login'
 import { isDoubleTokenMode } from '@/utils'
@@ -17,274 +18,277 @@ import { useUserStore } from './user'
 
 // 初始化状态
 const tokenInfoState = isDoubleTokenMode
-  ? {
-      accessToken: '',
-      accessExpiresIn: 0,
-      refreshToken: '',
-      refreshExpiresIn: 0,
+    ? {
+        accessToken: '',
+        accessExpiresIn: 0,
+        refreshToken: '',
+        refreshExpiresIn: 0,
     }
-  : {
-      token: '',
-      expiresIn: 0,
+    : {
+        token: '',
+        expiresIn: 0,
     }
 
 export const useTokenStore = defineStore(
-  'token',
-  () => {
-    // 定义用户信息
-    const tokenInfo = ref<IAuthLoginRes>({ ...tokenInfoState })
-    // 设置用户信息
-    const setTokenInfo = (val: IAuthLoginRes) => {
-      tokenInfo.value = val
+    'token',
+    () => {
+        // 定义用户信息
+        const tokenInfo = ref<IAuthLoginRes>({ ...tokenInfoState })
+        // 设置用户信息
+        const setTokenInfo = (val: IAuthLoginRes) => {
+            tokenInfo.value = val
 
-      // 计算并存储过期时间
-      const now = Date.now()
-      if (isSingleTokenRes(val)) {
-        // 单token模式
-        const expireTime = now + val.expiresIn * 1000
-        uni.setStorageSync('accessTokenExpireTime', expireTime)
-      }
-      else if (isDoubleTokenRes(val)) {
-        // 双token模式
-        const accessExpireTime = now + val.accessExpiresIn * 1000
-        const refreshExpireTime = now + val.refreshExpiresIn * 1000
-        uni.setStorageSync('accessTokenExpireTime', accessExpireTime)
-        uni.setStorageSync('refreshTokenExpireTime', refreshExpireTime)
-      }
-    }
-
-    /**
-     * 判断token是否过期
-     */
-    const isTokenExpired = computed(() => {
-      if (!tokenInfo.value) {
-        return true
-      }
-
-      const now = Date.now()
-      const expireTime = uni.getStorageSync('accessTokenExpireTime')
+            // 计算并存储过期时间
+            const now = Date.now()
+            if (isSingleTokenRes(val)) {
+                // 单token模式
+                const expireTime = now + val.expiresIn * 1000
+                uni.setStorageSync('accessTokenExpireTime', expireTime)
+            }
+            else if (isDoubleTokenRes(val)) {
+                // 双token模式
+                const accessExpireTime = now + val.accessExpiresIn * 1000
+                const refreshExpireTime = now + val.refreshExpiresIn * 1000
+                uni.setStorageSync('accessTokenExpireTime', accessExpireTime)
+                uni.setStorageSync('refreshTokenExpireTime', refreshExpireTime)
+            }
+        }
 
-      if (!expireTime)
-        return true
-      return now >= expireTime
-    })
+        /**
+                                                     * 判断token是否过期
+                                                     */
+        const isTokenExpired = computed(() => {
+            if (!tokenInfo.value) {
+                return true
+            }
 
-    /**
-     * 判断refreshToken是否过期
-     */
-    const isRefreshTokenExpired = computed(() => {
-      if (!isDoubleTokenMode)
-        return true
+            const now = Date.now()
+            const expireTime = uni.getStorageSync('accessTokenExpireTime')
 
-      const now = Date.now()
-      const refreshExpireTime = uni.getStorageSync('refreshTokenExpireTime')
+            if (!expireTime)
+                return true
+            return now >= expireTime
+        })
 
-      if (!refreshExpireTime)
-        return true
-      return now >= refreshExpireTime
-    })
+        /**
+                                                     * 判断refreshToken是否过期
+                                                     */
+        const isRefreshTokenExpired = computed(() => {
+            if (!isDoubleTokenMode)
+                return true
 
-    /**
-     * 登录成功后处理逻辑
-     * @param tokenInfo 登录返回的token信息
-     */
-    async function _postLogin(tokenInfo: IAuthLoginRes) {
-      setTokenInfo(tokenInfo)
-      const userStore = useUserStore()
-      await userStore.fetchUserInfo()
-    }
+            const now = Date.now()
+            const refreshExpireTime = uni.getStorageSync('refreshTokenExpireTime')
 
-    /**
-     * 用户登录
-     * 有的时候后端会用一个接口返回token和用户信息,有的时候会分开2个接口,一个获取token,一个获取用户信息
-     * (各有利弊,看业务场景和系统复杂度),这里使用2个接口返回的来模拟
-     * @param loginForm 登录参数
-     * @returns 登录结果
-     */
-    const login = async (loginForm: ILoginForm) => {
-      try {
-        const res = await _login(loginForm)
-        console.log('普通登录-res: ', res)
-        await _postLogin(res)
-        uni.showToast({
-          title: '登录成功',
-          icon: 'success',
-        })
-        return res
-      }
-      catch (error) {
-        console.error('登录失败:', error)
-        uni.showToast({
-          title: '登录失败,请重试',
-          icon: 'error',
+            if (!refreshExpireTime)
+                return true
+            return now >= refreshExpireTime
         })
-        throw error
-      }
-    }
 
-    /**
-     * 微信登录
-     * 有的时候后端会用一个接口返回token和用户信息,有的时候会分开2个接口,一个获取token,一个获取用户信息
-     * (各有利弊,看业务场景和系统复杂度),这里使用2个接口返回的来模拟
-     * @returns 登录结果
-     */
-    const wxLogin = async () => {
-      try {
-        // 获取微信小程序登录的code
-        const code = await getWxCode()
-        console.log('微信登录-code: ', code)
-        const res = await _wxLogin(code)
-        console.log('微信登录-res: ', res)
-        await _postLogin(res)
-        uni.showToast({
-          title: '登录成功',
-          icon: 'success',
-        })
-        return res
-      }
-      catch (error) {
-        console.error('微信登录失败:', error)
-        uni.showToast({
-          title: '微信登录失败,请重试',
-          icon: 'error',
-        })
-        throw error
-      }
-    }
+        /**
+                                                     * 登录成功后处理逻辑
+                                                     * @param tokenInfo 登录返回的token信息
+                                                     */
+        async function _postLogin(tokenInfo: IAuthLoginRes) {
+            setTokenInfo(tokenInfo)
+            // const userStore = useUserStore()
+            // await userStore.fetchUserInfo()
+        }
 
-    /**
-     * 退出登录 并 删除用户信息
-     */
-    const logout = async () => {
-      try {
-        // TODO 实现自己的退出登录逻辑
-        await _logout()
-      }
-      catch (error) {
-        console.error('退出登录失败:', error)
-      }
-      finally {
-        // 无论成功失败,都需要清除本地token信息
-        // 清除存储的过期时间
-        uni.removeStorageSync('accessTokenExpireTime')
-        uni.removeStorageSync('refreshTokenExpireTime')
-        console.log('退出登录-清除用户信息')
-        tokenInfo.value = { ...tokenInfoState }
-        uni.removeStorageSync('token')
-        const userStore = useUserStore()
-        userStore.clearUserInfo()
-      }
-    }
+        /**
+                                                     * 用户登录
+                                                     * 有的时候后端会用一个接口返回token和用户信息,有的时候会分开2个接口,一个获取token,一个获取用户信息
+                                                     * (各有利弊,看业务场景和系统复杂度),这里使用2个接口返回的来模拟
+                                                     * @param loginForm 登录参数
+                                                     * @returns 登录结果
+                                                     */
+        const login = async (loginForm: ILoginForm) => {
+            try {
+                const res = await _login(loginForm)
+                console.log('普通登录-res: ', res)
+                await _postLogin(res)
+                uni.showToast({
+                    title: '登录成功',
+                    icon: 'success',
+                })
+                return res
+            }
+            catch (error) {
+                console.error('登录失败:', error)
+                uni.showToast({
+                    title: '登录失败,请重试',
+                    icon: 'error',
+                })
+                throw error
+            }
+        }
 
-    /**
-     * 刷新token
-     * @returns 刷新结果
-     */
-    const refreshToken = async () => {
-      if (!isDoubleTokenMode) {
-        console.error('单token模式不支持刷新token')
-        throw new Error('单token模式不支持刷新token')
-      }
+        /**
+                                                     * 微信登录
+                                                     * 有的时候后端会用一个接口返回token和用户信息,有的时候会分开2个接口,一个获取token,一个获取用户信息
+                                                     * (各有利弊,看业务场景和系统复杂度),这里使用2个接口返回的来模拟
+                                                     * @returns 登录结果
+                                                     */
+        const wxLogin = async () => {
+            try {
+                // 获取用户信息
+                const profile = await getUserProfile()
+                console.log('微信登录-profile: ', profile)
+                // 获取微信小程序登录的code
+                const code = await getWxCode()
+                console.log('微信登录-code: ', code.code)
+                const res = await _wxLogin(code.code)
+                console.log('微信登录-res: ', res)
+                await _postLogin(res)
+                uni.showToast({
+                    title: '登录成功',
+                    icon: 'success',
+                })
+                return res
+            }
+            catch (error) {
+                console.error('微信登录失败:', error)
+                uni.showToast({
+                    title: '微信登录失败,请重试',
+                    icon: 'error',
+                })
+                throw error
+            }
+        }
 
-      try {
-        // 安全检查,确保refreshToken存在
-        if (!isDoubleTokenRes(tokenInfo.value) || !tokenInfo.value.refreshToken) {
-          throw new Error('无效的refreshToken')
+        /**
+                                                     * 退出登录 并 删除用户信息
+                                                     */
+        const logout = async () => {
+            try {
+                // TODO 实现自己的退出登录逻辑
+                await _logout()
+            }
+            catch (error) {
+                console.error('退出登录失败:', error)
+            }
+            finally {
+                // 无论成功失败,都需要清除本地token信息
+                // 清除存储的过期时间
+                uni.removeStorageSync('accessTokenExpireTime')
+                uni.removeStorageSync('refreshTokenExpireTime')
+                console.log('退出登录-清除用户信息')
+                tokenInfo.value = { ...tokenInfoState }
+                uni.removeStorageSync('token')
+                const userStore = useUserStore()
+                userStore.clearUserInfo()
+            }
         }
 
-        const refreshToken = tokenInfo.value.refreshToken
-        const res = await _refreshToken(refreshToken)
-        console.log('刷新token-res: ', res)
-        setTokenInfo(res)
-        return res
-      }
-      catch (error) {
-        console.error('刷新token失败:', error)
-        throw error
-      }
-    }
+        /**
+                                                     * 刷新token
+                                                     * @returns 刷新结果
+                                                     */
+        const refreshToken = async () => {
+            if (!isDoubleTokenMode) {
+                console.error('单token模式不支持刷新token')
+                throw new Error('单token模式不支持刷新token')
+            }
 
-    /**
-     * 获取有效的token
-     * 注意:在computed中不直接调用异步函数,只做状态判断
-     * 实际的刷新操作应由调用方处理
-     */
-    const getValidToken = computed(() => {
-      // token已过期,返回空
-      if (isTokenExpired.value) {
-        return ''
-      }
+            try {
+                // 安全检查,确保refreshToken存在
+                if (!isDoubleTokenRes(tokenInfo.value) || !tokenInfo.value.refreshToken) {
+                    throw new Error('无效的refreshToken')
+                }
 
-      if (!isDoubleTokenMode) {
-        return isSingleTokenRes(tokenInfo.value) ? tokenInfo.value.token : ''
-      }
-      else {
-        return isDoubleTokenRes(tokenInfo.value) ? tokenInfo.value.accessToken : ''
-      }
-    })
+                const refreshToken = tokenInfo.value.refreshToken
+                const res = await _refreshToken(refreshToken)
+                console.log('刷新token-res: ', res)
+                setTokenInfo(res)
+                return res
+            }
+            catch (error) {
+                console.error('刷新token失败:', error)
+                throw error
+            }
+        }
 
-    /**
-     * 检查是否有登录信息(不考虑token是否过期)
-     */
-    const hasLoginInfo = computed(() => {
-      if (!tokenInfo.value) {
-        return false
-      }
-      if (isDoubleTokenMode) {
-        return isDoubleTokenRes(tokenInfo.value) && !!tokenInfo.value.accessToken
-      }
-      else {
-        return isSingleTokenRes(tokenInfo.value) && !!tokenInfo.value.token
-      }
-    })
+        /**
+                                                     * 获取有效的token
+                                                     * 注意:在computed中不直接调用异步函数,只做状态判断
+                                                     * 实际的刷新操作应由调用方处理
+                                                     */
+        const getValidToken = computed(() => {
+            // token已过期,返回空
+            if (isTokenExpired.value) {
+                return ''
+            }
 
-    /**
-     * 检查是否已登录且token有效
-     */
-    const hasValidLogin = computed(() => {
-      console.log('hasValidLogin', hasLoginInfo.value, !isTokenExpired.value)
-      return hasLoginInfo.value && !isTokenExpired.value
-    })
+            if (!isDoubleTokenMode) {
+                return isSingleTokenRes(tokenInfo.value) ? tokenInfo.value.token : ''
+            }
+            else {
+                return isDoubleTokenRes(tokenInfo.value) ? tokenInfo.value.accessToken : ''
+            }
+        })
 
-    /**
-     * 尝试获取有效的token,如果过期且可刷新,则刷新token
-     * @returns 有效的token或空字符串
-     */
-    const tryGetValidToken = async (): Promise<string> => {
-      if (!getValidToken.value && isDoubleTokenMode && !isRefreshTokenExpired.value) {
-        try {
-          await refreshToken()
-          return getValidToken.value
-        }
-        catch (error) {
-          console.error('尝试刷新token失败:', error)
-          return ''
+        /**
+                                                     * 检查是否有登录信息(不考虑token是否过期)
+                                                     */
+        const hasLoginInfo = computed(() => {
+            if (!tokenInfo.value) {
+                return false
+            }
+            if (isDoubleTokenMode) {
+                return isDoubleTokenRes(tokenInfo.value) && !!tokenInfo.value.accessToken
+            }
+            else {
+                return isSingleTokenRes(tokenInfo.value) && !!tokenInfo.value.token
+            }
+        })
+
+        /**
+                                                     * 检查是否已登录且token有效
+                                                     */
+        const hasValidLogin = computed(() => {
+            console.log('hasValidLogin', hasLoginInfo.value, !isTokenExpired.value)
+            return hasLoginInfo.value && !isTokenExpired.value
+        })
+
+        /**
+                                                     * 尝试获取有效的token,如果过期且可刷新,则刷新token
+                                                     * @returns 有效的token或空字符串
+                                                     */
+        const tryGetValidToken = async (): Promise<string> => {
+            if (!getValidToken.value && isDoubleTokenMode && !isRefreshTokenExpired.value) {
+                try {
+                    await refreshToken()
+                    return getValidToken.value
+                }
+                catch (error) {
+                    console.error('尝试刷新token失败:', error)
+                    return ''
+                }
+            }
+            return getValidToken.value
         }
-      }
-      return getValidToken.value
-    }
 
-    return {
-      // 核心API方法
-      login,
-      wxLogin,
-      logout,
+        return {
+            // 核心API方法
+            login,
+            wxLogin,
+            logout,
 
-      // 认证状态判断(最常用的)
-      hasLogin: hasValidLogin,
+            // 认证状态判断(最常用的)
+            hasLogin: hasValidLogin,
 
-      // 内部系统使用的方法
-      refreshToken,
-      tryGetValidToken,
-      validToken: getValidToken,
+            // 内部系统使用的方法
+            refreshToken,
+            tryGetValidToken,
+            validToken: getValidToken,
 
-      // 调试或特殊场景可能需要直接访问的信息
-      tokenInfo,
-      setTokenInfo,
-    }
-  },
-  {
-    // 添加持久化配置,确保刷新页面后token信息不丢失
-    persist: true,
-  },
+            // 调试或特殊场景可能需要直接访问的信息
+            tokenInfo,
+            setTokenInfo,
+        }
+    },
+    {
+        // 添加持久化配置,确保刷新页面后token信息不丢失
+        persist: true,
+    },
 )

+ 47 - 47
src/store/user.ts

@@ -2,60 +2,60 @@ import type { IUserInfoRes } from '@/api/types/login'
 import { defineStore } from 'pinia'
 import { ref } from 'vue'
 import {
-  getUserInfo,
+    getUserInfo,
 } from '@/api/login'
 
 // 初始化状态
 const userInfoState: IUserInfoRes = {
-  userId: -1,
-  username: '',
-  nickname: '',
-  avatar: '/static/images/default-avatar.png',
+    userId: -1,
+    username: '',
+    nickname: '',
+    avatar: '/static/images/default-avatar.png',
 }
 
 export const useUserStore = defineStore(
-  'user',
-  () => {
-    // 定义用户信息
-    const userInfo = ref<IUserInfoRes>({ ...userInfoState })
-    // 设置用户信息
-    const setUserInfo = (val: IUserInfoRes) => {
-      console.log('设置用户信息', val)
-      // 若头像为空 则使用默认头像
-      if (!val.avatar) {
-        val.avatar = userInfoState.avatar
-      }
-      userInfo.value = val
-    }
-    const setUserAvatar = (avatar: string) => {
-      userInfo.value.avatar = avatar
-      console.log('设置用户头像', avatar)
-      console.log('userInfo', userInfo.value)
-    }
-    // 删除用户信息
-    const clearUserInfo = () => {
-      userInfo.value = { ...userInfoState }
-      uni.removeStorageSync('user')
-    }
+    'user',
+    () => {
+        // 定义用户信息
+        const userInfo = ref<IUserInfoRes>({ ...userInfoState })
+        // 设置用户信息
+        const setUserInfo = (val: IUserInfoRes) => {
+            console.log('设置用户信息', val)
+            // 若头像为空 则使用默认头像
+            if (!val.avatar) {
+                val.avatar = userInfoState.avatar
+            }
+            userInfo.value = val
+        }
+        const setUserAvatar = (avatar: string) => {
+            userInfo.value.avatar = avatar
+            console.log('设置用户头像', avatar)
+            console.log('userInfo', userInfo.value)
+        }
+        // 删除用户信息
+        const clearUserInfo = () => {
+            userInfo.value = { ...userInfoState }
+            uni.removeStorageSync('user')
+        }
 
-    /**
-     * 获取用户信息
-     */
-    const fetchUserInfo = async () => {
-      const res = await getUserInfo()
-      setUserInfo(res)
-      return res
-    }
+        /**
+             * 获取用户信息
+             */
+        const fetchUserInfo = async () => {
+            const res = await getUserInfo()
+            setUserInfo(res)
+            return res
+        }
 
-    return {
-      userInfo,
-      clearUserInfo,
-      fetchUserInfo,
-      setUserInfo,
-      setUserAvatar,
-    }
-  },
-  {
-    persist: true,
-  },
+        return {
+            userInfo,
+            clearUserInfo,
+            fetchUserInfo,
+            setUserInfo,
+            setUserAvatar,
+        }
+    },
+    {
+        persist: true,
+    },
 )

+ 95 - 86
src/tabbar/config.ts

@@ -11,10 +11,10 @@ import type { CustomTabBarItem, NativeTabBarItem } from './types'
  * 温馨提示:本文件的任何代码更改了之后,都需要重新运行,否则 pages.json 不会更新导致配置不生效
  */
 export const TABBAR_STRATEGY_MAP = {
-  NO_TABBAR: 0,
-  NATIVE_TABBAR: 1,
-  CUSTOM_TABBAR_WITH_CACHE: 2,
-  CUSTOM_TABBAR_WITHOUT_CACHE: 3,
+    NO_TABBAR: 0,
+    NATIVE_TABBAR: 1,
+    CUSTOM_TABBAR_WITH_CACHE: 2,
+    CUSTOM_TABBAR_WITHOUT_CACHE: 3,
 }
 
 // TODO: 1/3. 通过这里切换使用tabbar的策略
@@ -25,80 +25,89 @@ export const selectedTabbarStrategy = TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CAC
 
 // TODO: 2/3. 使用 NATIVE_TABBAR 时,更新下面的 tabbar 配置
 export const nativeTabbarList: NativeTabBarItem[] = [
-  {
-    iconPath: 'static/tabbar/home.png',
-    selectedIconPath: 'static/tabbar/homeHL.png',
-    pagePath: 'pages/index/index',
-    text: '首页',
-  },
-  {
-    iconPath: 'static/tabbar/personal.png',
-    selectedIconPath: 'static/tabbar/personalHL.png',
-    pagePath: 'pages/me/me',
-    text: '个人',
-  },
+    {
+        iconPath: 'static/tabbar/home.png',
+        selectedIconPath: 'static/tabbar/home_select.png',
+        pagePath: 'pages/index/index',
+        text: '首页',
+    },
+    {
+        iconPath: 'static/tabbar/income.png',
+        selectedIconPath: 'static/tabbar/income_select.png',
+        pagePath: 'pages/income/income',
+        text: '收益',
+    },
+    {
+        iconPath: 'static/tabbar/me.png',
+        selectedIconPath: 'static/tabbar/me_selected.png',
+        pagePath: 'pages/me/me',
+        text: '个人',
+    },
 ]
 
 // TODO: 3/3. 使用 CUSTOM_TABBAR(2,3) 时,更新下面的 tabbar 配置
 // 如果需要配置鼓包,需要在 'tabbar/store.ts' 里面设置,最后在 `tabbar/index.vue` 里面更改鼓包的图片
 export const customTabbarList: CustomTabBarItem[] = [
-  {
-    text: '首页',
-    pagePath: 'pages/index/index',
-    // 注意 unocss 图标需要如下处理:(二选一)
-    // 1)在fg-tabbar.vue页面上引入一下并注释掉(见tabbar/index.vue代码第2行)
-    // 2)配置到 unocss.config.ts 的 safelist 中
-    iconType: 'unocss',
-    icon: 'i-carbon-home',
-    // badge: 'dot',
-  },
-  {
-    pagePath: 'pages/income/income',
-    text: '收益',
-    iconType: 'unocss',
-    icon: 'i-carbon-chart-line',
-    // badge: 10,
-  },
-  {
-    pagePath: 'pages/me/me',
-    text: '我的',
-    // 1)在fg-tabbar.vue页面上引入一下并注释掉(见tabbar/index.vue代码第2行)
-    // 2)配置到 unocss.config.ts 的 safelist 中
-    iconType: 'unocss',
-    icon: 'i-carbon-user',
-    // badge: 10,
-  },
-  // {
-  //   pagePath: 'pages/income/income',
-  //   text: '收益',
-  //   iconType: 'unocss',
-  //   icon: 'i-carbon-chart-line',
-  // },
-  // 其他类型演示
-  // 1、uiLib
-  // {
-  //   pagePath: 'pages/index/index',
-  //   text: '首页',
-  //   iconType: 'uiLib',
-  //   icon: 'home',
-  // },
-  // 2、iconfont
-  // {
-  //   pagePath: 'pages/index/index',
-  //   text: '首页',
-  //   // 注意 iconfont 图标需要额外加上 'iconfont',如下
-  //   iconType: 'iconfont',
-  //   icon: 'iconfont icon-my',
-  // },
-  // 3、image
-  // {
-  //   pagePath: 'pages/index/index',
-  //   text: '首页',
-  //   // 使用 ‘image’时,需要配置 icon + iconActive 2张图片
-  //   iconType: 'image',
-  //   icon: '/static/tabbar/home.png',
-  //   iconActive: '/static/tabbar/homeHL.png',
-  // },
+    {
+        text: '首页',
+        pagePath: 'pages/index/index',
+        // 注意 unocss 图标需要如下处理:(二选一)
+        // 1)在fg-tabbar.vue页面上引入一下并注释掉(见tabbar/index.vue代码第2行)
+        // 2)配置到 unocss.config.ts 的 safelist 中
+        iconType: 'image',
+        icon: '/static/tabbar/home.png',
+        iconActive: '/static/tabbar/home_select.png',
+        // badge: 'dot',
+    },
+    {
+        pagePath: 'pages/income/income',
+        text: '收益',
+        iconType: 'image',
+        icon: '/static/tabbar/income.png',
+        iconActive: '/static/tabbar/income_select.png',
+        // badge: 10,
+    },
+    {
+        pagePath: 'pages/me/me',
+        text: '我的',
+        // 1)在fg-tabbar.vue页面上引入一下并注释掉(见tabbar/index.vue代码第2行)
+        // 2)配置到 unocss.config.ts 的 safelist 中
+        iconType: 'image',
+        icon: '/static/tabbar/me.png',
+        iconActive: '/static/tabbar/me_selected.png',
+        // badge: 10,
+    },
+    // {
+    //   pagePath: 'pages/income/income',
+    //   text: '收益',
+    //   iconType: 'unocss',
+    //   icon: 'i-carbon-chart-line',
+    // },
+    // 其他类型演示
+    // 1、uiLib
+    // {
+    //   pagePath: 'pages/index/index',
+    //   text: '首页',
+    //   iconType: 'uiLib',
+    //   icon: 'home',
+    // },
+    // 2、iconfont
+    // {
+    //   pagePath: 'pages/index/index',
+    //   text: '首页',
+    //   // 注意 iconfont 图标需要额外加上 'iconfont',如下
+    //   iconType: 'iconfont',
+    //   icon: 'iconfont icon-my',
+    // },
+    // 3、image
+    // {
+    //   pagePath: 'pages/index/index',
+    //   text: '首页',
+    //   // 使用 ‘image’时,需要配置 icon + iconActive 2张图片
+    //   iconType: 'image',
+    //   icon: '/static/tabbar/home.png',
+    //   iconActive: '/static/tabbar/homeHL.png',
+    // },
 ]
 
 /**
@@ -106,14 +115,14 @@ export const customTabbarList: CustomTabBarItem[] = [
  * NATIVE_TABBAR(1) 和 CUSTOM_TABBAR_WITH_CACHE(2) 时,需要tabbar缓存
  */
 export const tabbarCacheEnable
-  = [TABBAR_STRATEGY_MAP.NATIVE_TABBAR, TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE].includes(selectedTabbarStrategy)
+    = [TABBAR_STRATEGY_MAP.NATIVE_TABBAR, TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE].includes(selectedTabbarStrategy)
 
 /**
  * 是否启用自定义 tabbar
  * CUSTOM_TABBAR(2,3) 时,启用自定义tabbar
  */
 export const customTabbarEnable
-  = [TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE, TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITHOUT_CACHE].includes(selectedTabbarStrategy)
+    = [TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE, TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITHOUT_CACHE].includes(selectedTabbarStrategy)
 
 /**
  * 是否需要隐藏原生 tabbar
@@ -125,17 +134,17 @@ const _tabbarList = customTabbarEnable ? customTabbarList.map(item => ({ text: i
 export const tabbarList = customTabbarEnable ? customTabbarList : nativeTabbarList
 
 const _tabbar: TabBar = {
-  // 只有微信小程序支持 custom。App 和 H5 不生效
-  custom: selectedTabbarStrategy === TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE,
-  color: '#999999',
-  selectedColor: '#018d71',
-  backgroundColor: '#F8F8F8',
-  borderStyle: 'black',
-  height: '50px',
-  fontSize: '10px',
-  iconWidth: '24px',
-  spacing: '3px',
-  list: _tabbarList as unknown as TabBar['list'],
+    // 只有微信小程序支持 custom。App 和 H5 不生效
+    custom: selectedTabbarStrategy === TABBAR_STRATEGY_MAP.CUSTOM_TABBAR_WITH_CACHE,
+    color: '#999999',
+    selectedColor: '#FF654F',
+    backgroundColor: '#FFFFFF',
+    borderStyle: 'black',
+    height: '120px',
+    fontSize: '22px',
+    iconWidth: '53px',
+    spacing: '15px',
+    list: _tabbarList as unknown as TabBar['list'],
 }
 
 export const tabBar = tabbarCacheEnable ? _tabbar : undefined

+ 81 - 82
src/tabbar/index.vue

@@ -7,7 +7,7 @@ import { tabbarList, tabbarStore } from './store'
 // #ifdef MP-WEIXIN
 // 将自定义节点设置成虚拟的(去掉自定义组件包裹层),更加接近Vue组件的表现,能更好的使用flex属性
 defineOptions({
-  virtualHost: true,
+    virtualHost: true,
 })
 // #endif
 
@@ -15,85 +15,82 @@ defineOptions({
  * 中间的鼓包tabbarItem的点击事件
  */
 function handleClickBulge() {
-  uni.showToast({
-    title: '点击了中间的鼓包tabbarItem',
-    icon: 'none',
-  })
+    uni.showToast({
+        title: '点击了中间的鼓包tabbarItem',
+        icon: 'none',
+    })
 }
 
 function handleClick(index: number) {
-  // 点击原来的不做操作
-  if (index === tabbarStore.curIdx) {
-    return
-  }
-  if (tabbarList[index].isBulge) {
-    handleClickBulge()
-    return
-  }
-  const url = tabbarList[index].pagePath
-  tabbarStore.setCurIdx(index)
-  if (tabbarCacheEnable) {
-    uni.switchTab({ url })
-  }
-  else {
-    uni.navigateTo({ url })
-  }
+    // 点击原来的不做操作
+    if (index === tabbarStore.curIdx) {
+        return
+    }
+    if (tabbarList[index].isBulge) {
+        handleClickBulge()
+        return
+    }
+    const url = tabbarList[index].pagePath
+    tabbarStore.setCurIdx(index)
+    if (tabbarCacheEnable) {
+        uni.switchTab({ url })
+    }
+    else {
+        uni.navigateTo({ url })
+    }
 }
 // #ifndef MP-WEIXIN || MP-ALIPAY
 // 因为有了 custom:true, 微信里面不需要多余的hide操作
 onLoad(() => {
-  // 解决原生 tabBar 未隐藏导致有2个 tabBar 的问题
-  needHideNativeTabbar
-    && uni.hideTabBar({
-    fail(err) {
-      console.log('hideTabBar fail: ', err)
-    },
-    success(res) {
-      // console.log('hideTabBar success: ', res)
-    },
-  })
+    // 解决原生 tabBar 未隐藏导致有2个 tabBar 的问题
+    needHideNativeTabbar
+        && uni.hideTabBar({
+            fail(err) {
+                console.log('hideTabBar fail: ', err)
+            },
+            success(res) {
+                // console.log('hideTabBar success: ', res)
+            },
+        })
 })
 // #endif
 
 // #ifdef MP-ALIPAY
 onMounted(() => {
-  // 解决支付宝自定义tabbar 未隐藏导致有2个 tabBar 的问题; 注意支付宝很特别,需要在 onMounted 钩子调用
-  customTabbarEnable // 另外,支付宝里面,只要是 customTabbar 都需要隐藏
-    && uni.hideTabBar({
-    fail(err) {
-      console.log('hideTabBar fail: ', err)
-    },
-    success(res) {
-      // console.log('hideTabBar success: ', res)
-    },
-  })
+    // 解决支付宝自定义tabbar 未隐藏导致有2个 tabBar 的问题; 注意支付宝很特别,需要在 onMounted 钩子调用
+    customTabbarEnable // 另外,支付宝里面,只要是 customTabbar 都需要隐藏
+        && uni.hideTabBar({
+            fail(err) {
+                console.log('hideTabBar fail: ', err)
+            },
+            success(res) {
+                // console.log('hideTabBar success: ', res)
+            },
+        })
 })
 // #endif
-const activeColor = 'var(--wot-color-theme, #1890ff)'
-const inactiveColor = '#666'
+const activeColor = 'var(--wot-color-theme, #FF654F )'
+const inactiveColor = '#888888'
 function getColorByIndex(index: number) {
-  return tabbarStore.curIdx === index ? activeColor : inactiveColor
+    return tabbarStore.curIdx === index ? activeColor : inactiveColor
 }
 
 function getImageByIndex(index: number, item: CustomTabBarItem) {
-  if (!item.iconActive) {
-    console.warn('image 模式下,需要配置 iconActive (高亮时的图片),否则无法切换高亮图片')
-    return item.icon
-  }
-  return tabbarStore.curIdx === index ? item.iconActive : item.icon
+    if (!item.iconActive) {
+        console.warn('image 模式下,需要配置 iconActive (高亮时的图片),否则无法切换高亮图片')
+        return item.icon
+    }
+    return tabbarStore.curIdx === index ? item.iconActive : item.icon
 }
 </script>
 
 <template>
-    <view v-if="customTabbarEnable" class="h-50px pb-safe">
+    <view v-if="customTabbarEnable" class="h-100rpx pb-safe">
         <view class="border-and-fixed bg-white" @touchmove.stop.prevent>
-            <view class="h-50px flex items-center">
-                <view
-                    v-for="(item, index) in tabbarList" :key="index"
-                    class="flex flex-1 flex-col items-center justify-center"
-                    :style="{ color: getColorByIndex(index) }"
-                    @click="handleClick(index)"
-                >
+            <view class="h-100rpx flex items-center pt-1">
+                <view v-for="(item, index) in tabbarList" :key="index"
+                    class="flex flex-1 flex-col items-center justify-center" :style="{ color: getColorByIndex(index) }"
+                    @click="handleClick(index)">
                     <view v-if="item.isBulge" class="relative">
                         <!-- 中间一个鼓包tabbarItem的处理 -->
                         <view class="bulge">
@@ -114,9 +111,9 @@ function getImageByIndex(index: number, item: CustomTabBarItem) {
                             <view :class="item.icon" class="text-20px" />
                         </template>
                         <template v-if="item.iconType === 'image'">
-                            <image :src="getImageByIndex(index, item)" mode="scaleToFill" class="h-20px w-20px" />
+                            <image :src="getImageByIndex(index, item)" mode="scaleToFill" class="h-41rpx w-42rpx" />
                         </template>
-                        <view class="mt-2px text-12px">
+                        <view class="mt-15rpx text-22rpx">
                             {{ item.text }}
                         </view>
                         <!-- 角标显示 -->
@@ -125,7 +122,8 @@ function getImageByIndex(index: number, item: CustomTabBarItem) {
                                 <view class="absolute right-0 top-0 h-2 w-2 rounded-full bg-#f56c6c" />
                             </template>
                             <template v-else>
-                                <view class="absolute top-0 box-border h-5 min-w-5 center rounded-full bg-#f56c6c px-1 text-center text-xs text-white -right-3">
+                                <view
+                                    class="absolute top-0 box-border h-5 min-w-5 center rounded-full bg-#f56c6c px-1 text-center text-xs text-white -right-3">
                                     {{ item.badge > 99 ? '99+' : item.badge }}
                                 </view>
                             </template>
@@ -164,32 +162,33 @@ function getImageByIndex(index: number, item: CustomTabBarItem) {
 
 <style scoped lang="scss">
 .border-and-fixed {
-  position: fixed;
-  bottom: 0;
-  left: 0;
-  right: 0;
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    right: 0;
 
-  border-top: 1px solid #eee;
-  box-sizing: border-box;
+    border-top: 1px solid #eee;
+    box-sizing: border-box;
 }
+
 // 中间鼓包的样式
 .bulge {
-  position: absolute;
-  top: -20px;
-  left: 50%;
-  transform-origin: top center;
-  transform: translateX(-50%) scale(0.5) translateY(-33%);
-  display: flex;
-  justify-content: center;
-  align-items: center;
-  width: 250rpx;
-  height: 250rpx;
-  border-radius: 50%;
-  background-color: #fff;
-  box-shadow: inset 0 0 0 1px #fefefe;
+    position: absolute;
+    top: -20px;
+    left: 50%;
+    transform-origin: top center;
+    transform: translateX(-50%) scale(0.5) translateY(-33%);
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 250rpx;
+    height: 250rpx;
+    border-radius: 50%;
+    background-color: #fff;
+    box-shadow: inset 0 0 0 1px #fefefe;
 
-  &:active {
-    // opacity: 0.8;
-  }
+    &:active {
+        // opacity: 0.8;
+    }
 }
 </style>

+ 3 - 3
src/utils/index.ts

@@ -122,9 +122,9 @@ export function getEnvBaseUrl() {
     let baseUrl = import.meta.env.VITE_SERVER_BASEURL
 
     // # 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
-    const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run'
-    const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run'
-    const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run'
+    const VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'http://192.168.1.28:8082/jeecg-boot'
+    const VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'http://192.168.1.28:8082/jeecg-boot'
+    const VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'http://192.168.1.28:8082/jeecg-boot'
 
     // 微信小程序端环境区分
     if (isMpWeixin) {

+ 165 - 163
vite.config.ts

@@ -30,178 +30,180 @@ import syncManifestPlugin from './vite-plugins/sync-manifest-plugins'
 
 // https://vitejs.dev/config/
 export default defineConfig(({ command, mode }) => {
-  // @see https://unocss.dev/
-  // const UnoCSS = (await import('unocss/vite')).default
-  // console.log(mode === process.env.NODE_ENV) // true
+    // @see https://unocss.dev/
+    // const UnoCSS = (await import('unocss/vite')).default
+    // console.log(mode === process.env.NODE_ENV) // true
 
-  // mode: 区分生产环境还是开发环境
-  console.log('command, mode -> ', command, mode)
-  // pnpm dev:h5 时得到 => serve development
-  // pnpm build:h5 时得到 => build production
-  // pnpm dev:mp-weixin 时得到 => build development (注意区别,command为build)
-  // pnpm build:mp-weixin 时得到 => build production
-  // pnpm dev:app 时得到 => build development (注意区别,command为build)
-  // pnpm build:app 时得到 => build production
-  // dev 和 build 命令可以分别使用 .env.development 和 .env.production 的环境变量
+    // mode: 区分生产环境还是开发环境
+    console.log('command, mode -> ', command, mode)
+    // pnpm dev:h5 时得到 => serve development
+    // pnpm build:h5 时得到 => build production
+    // pnpm dev:mp-weixin 时得到 => build development (注意区别,command为build)
+    // pnpm build:mp-weixin 时得到 => build production
+    // pnpm dev:app 时得到 => build development (注意区别,command为build)
+    // pnpm build:app 时得到 => build production
+    // dev 和 build 命令可以分别使用 .env.development 和 .env.production 的环境变量
 
-  const { UNI_PLATFORM } = process.env
-  console.log('UNI_PLATFORM -> ', UNI_PLATFORM) // 得到 mp-weixin, h5, app 等
+    const { UNI_PLATFORM } = process.env
+    console.log('UNI_PLATFORM -> ', UNI_PLATFORM) // 得到 mp-weixin, h5, app 等
 
-  const env = loadEnv(mode, path.resolve(process.cwd(), 'env'))
-  const {
-    VITE_APP_PORT,
-    VITE_SERVER_BASEURL,
-    VITE_APP_TITLE,
-    VITE_DELETE_CONSOLE,
-    VITE_APP_PUBLIC_BASE,
-    VITE_APP_PROXY_ENABLE,
-    VITE_APP_PROXY_PREFIX,
-    VITE_COPY_NATIVE_RES_ENABLE,
-  } = env
-  console.log('环境变量 env -> ', env)
+    const env = loadEnv(mode, path.resolve(process.cwd(), 'env'))
+    const {
+        VITE_APP_PORT,
+        VITE_SERVER_BASEURL,
+        VITE_APP_TITLE,
+        VITE_DELETE_CONSOLE,
+        VITE_APP_PUBLIC_BASE,
+        VITE_APP_PROXY_ENABLE,
+        VITE_APP_PROXY_PREFIX,
+        VITE_COPY_NATIVE_RES_ENABLE,
+    } = env
+    console.log('环境变量 env -> ', env)
 
-  return defineConfig({
-    envDir: './env', // 自定义env目录
-    base: VITE_APP_PUBLIC_BASE,
-    plugins: [
-      UniLayouts(),
-      UniPlatform(),
-      UniManifest(),
-      UniPages({
-        exclude: ['**/components/**/**.*'],
-        // pages 目录为 src/pages,分包目录不能配置在pages目录下!!
-        // 是个数组,可以配置多个,但是不能为pages里面的目录!!
-        subPackages: [
-          'src/pages-fg', // 这个是相对必要的路由,尽量留着(登录页、注册页、404页等)
-          'src/pages-A',
+    return defineConfig({
+        envDir: './env', // 自定义env目录
+        base: VITE_APP_PUBLIC_BASE,
+        plugins: [
+            UniLayouts(),
+            UniPlatform(),
+            UniManifest(),
+            UniPages({
+                exclude: ['**/components/**/**.*'],
+                // pages 目录为 src/pages,分包目录不能配置在pages目录下!!
+                // 是个数组,可以配置多个,但是不能为pages里面的目录!!
+                subPackages: [
+                    'src/pages-fg', // 这个是相对必要的路由,尽量留着(登录页、注册页、404页等)
+                    'src/pages-A',
+                ],
+                dts: 'src/types/uni-pages.d.ts',
+            }),
+            // Optimization 插件需要 page.json 文件,故应在 UniPages 插件之后执行
+            Optimization({
+                enable: {
+                    'optimization': true,
+                    'async-import': true,
+                    'async-component': true,
+                },
+                dts: {
+                    base: 'src/types',
+                },
+                logger: false,
+            }),
+            // UniXXX 需要在 Uni 之前引入
+            // 若存在改变 pages.json 的插件,请将 UniKuRoot 放置其后
+            UniKuRoot({
+                excludePages: ['**/components/**/**.*'],
+            }),
+            Uni(),
+            {
+                // 临时解决 dcloudio 官方的 @dcloudio/uni-mp-compiler 出现的编译 BUG
+                // 参考 github issue: https://github.com/dcloudio/uni-app/issues/4952
+                // 自定义插件禁用 vite:vue 插件的 devToolsEnabled,强制编译 vue 模板时 inline 为 true
+                name: 'fix-vite-plugin-vue',
+                configResolved(config) {
+                    const plugin = config.plugins.find(p => p.name === 'vite:vue')
+                    if (plugin && plugin.api && plugin.api.options) {
+                        plugin.api.options.devToolsEnabled = false
+                    }
+                },
+            },
+            UnoCSS(),
+            AutoImport({
+                imports: ['vue', 'uni-app'],
+                dts: 'src/types/auto-import.d.ts',
+                dirs: ['src/hooks'], // 自动导入 hooks
+                vueTemplate: true, // default false
+            }),
+            ViteRestart({
+                // 通过这个插件,在修改vite.config.js文件则不需要重新运行也生效配置
+                restart: ['vite.config.js'],
+            }),
+            // h5环境增加 BUILD_TIME 和 BUILD_BRANCH
+            UNI_PLATFORM === 'h5' && {
+                name: 'html-transform',
+                transformIndexHtml(html) {
+                    return html.replace('%BUILD_TIME%', dayjs().format('YYYY-MM-DD HH:mm:ss')).replace('%VITE_APP_TITLE%', VITE_APP_TITLE)
+                },
+            },
+            // 打包分析插件,h5 + 生产环境才弹出
+            UNI_PLATFORM === 'h5'
+            && mode === 'production'
+            && visualizer({
+                filename: './node_modules/.cache/visualizer/stats.html',
+                open: true,
+                gzipSize: true,
+                brotliSize: true,
+            }),
+            // 原生插件资源复制插件 - 仅在 app 平台且启用时生效
+            createCopyNativeResourcesPlugin(
+                UNI_PLATFORM === 'app' && VITE_COPY_NATIVE_RES_ENABLE === 'true',
+                {
+                    verbose: mode === 'development', // 开发模式显示详细日志
+                },
+            ),
+            syncManifestPlugin(),
+            Components({
+                extensions: ['vue'],
+                deep: true, // 是否递归扫描子目录,
+                directoryAsNamespace: false, // 是否把目录名作为命名空间前缀,true 时组件名为 目录名+组件名,
+                dts: 'src/types/components.d.ts', // 自动生成的组件类型声明文件路径(用于 TypeScript 支持)
+            }),
+            // 自动打开开发者工具插件 (必须修改 .env 文件中的 VITE_WX_APPID)
+            openDevTools(),
         ],
-        dts: 'src/types/uni-pages.d.ts',
-      }),
-      // Optimization 插件需要 page.json 文件,故应在 UniPages 插件之后执行
-      Optimization({
-        enable: {
-          'optimization': true,
-          'async-import': true,
-          'async-component': true,
+        define: {
+            __VITE_APP_PROXY__: JSON.stringify(VITE_APP_PROXY_ENABLE),
         },
-        dts: {
-          base: 'src/types',
+        css: {
+            postcss: {
+                preprocessorOptions: {
+                    scss: {
+                        silenceDeprecations: ['legacy-js-api', 'color-functions', 'import'],
+                    },
+                },
+                plugins: [
+                    // autoprefixer({
+                    //   // 指定目标浏览器
+                    //   overrideBrowserslist: ['> 1%', 'last 2 versions'],
+                    // }),
+                ],
+            },
         },
-        logger: false,
-      }),
-      // UniXXX 需要在 Uni 之前引入
-      // 若存在改变 pages.json 的插件,请将 UniKuRoot 放置其后
-      UniKuRoot({
-        excludePages: ['**/components/**/**.*'],
-      }),
-      Uni(),
-      {
-        // 临时解决 dcloudio 官方的 @dcloudio/uni-mp-compiler 出现的编译 BUG
-        // 参考 github issue: https://github.com/dcloudio/uni-app/issues/4952
-        // 自定义插件禁用 vite:vue 插件的 devToolsEnabled,强制编译 vue 模板时 inline 为 true
-        name: 'fix-vite-plugin-vue',
-        configResolved(config) {
-          const plugin = config.plugins.find(p => p.name === 'vite:vue')
-          if (plugin && plugin.api && plugin.api.options) {
-            plugin.api.options.devToolsEnabled = false
-          }
+
+        resolve: {
+            alias: {
+                '@': path.join(process.cwd(), './src'),
+                '@img': path.join(process.cwd(), './src/static/images'),
+            },
         },
-      },
-      UnoCSS(),
-      AutoImport({
-        imports: ['vue', 'uni-app'],
-        dts: 'src/types/auto-import.d.ts',
-        dirs: ['src/hooks'], // 自动导入 hooks
-        vueTemplate: true, // default false
-      }),
-      ViteRestart({
-        // 通过这个插件,在修改vite.config.js文件则不需要重新运行也生效配置
-        restart: ['vite.config.js'],
-      }),
-      // h5环境增加 BUILD_TIME 和 BUILD_BRANCH
-      UNI_PLATFORM === 'h5' && {
-        name: 'html-transform',
-        transformIndexHtml(html) {
-          return html.replace('%BUILD_TIME%', dayjs().format('YYYY-MM-DD HH:mm:ss')).replace('%VITE_APP_TITLE%', VITE_APP_TITLE)
+        server: {
+            host: '0.0.0.0',
+            hmr: true,
+            port: Number.parseInt(VITE_APP_PORT, 10),
+            // 仅 H5 端生效,其他端不生效(其他端走build,不走devServer)
+            proxy: JSON.parse(VITE_APP_PROXY_ENABLE)
+                ? {
+                    [VITE_APP_PROXY_PREFIX]: {
+                        target: VITE_SERVER_BASEURL,
+                        changeOrigin: true,
+                        // 后端有/api前缀则不做处理,没有则需要去掉
+                        rewrite: path => path.replace(new RegExp(`^${VITE_APP_PROXY_PREFIX}`), ''),
+                    },
+                }
+                : undefined,
         },
-      },
-      // 打包分析插件,h5 + 生产环境才弹出
-      UNI_PLATFORM === 'h5'
-            && mode === 'production'
-      && visualizer({
-              filename: './node_modules/.cache/visualizer/stats.html',
-              open: true,
-              gzipSize: true,
-              brotliSize: true,
-            }),
-      // 原生插件资源复制插件 - 仅在 app 平台且启用时生效
-      createCopyNativeResourcesPlugin(
-        UNI_PLATFORM === 'app' && VITE_COPY_NATIVE_RES_ENABLE === 'true',
-        {
-          verbose: mode === 'development', // 开发模式显示详细日志
+        esbuild: {
+            drop: VITE_DELETE_CONSOLE === 'true' ? ['console', 'debugger'] : [],
         },
-      ),
-      syncManifestPlugin(),
-      Components({
-        extensions: ['vue'],
-        deep: true, // 是否递归扫描子目录,
-        directoryAsNamespace: false, // 是否把目录名作为命名空间前缀,true 时组件名为 目录名+组件名,
-        dts: 'src/types/components.d.ts', // 自动生成的组件类型声明文件路径(用于 TypeScript 支持)
-      }),
-      // 自动打开开发者工具插件 (必须修改 .env 文件中的 VITE_WX_APPID)
-      openDevTools(),
-    ],
-    define: {
-      __VITE_APP_PROXY__: JSON.stringify(VITE_APP_PROXY_ENABLE),
-    },
-    css: {
-      postcss: {
-        preprocessorOptions: {
-          scss: {
-            silenceDeprecations: ['legacy-js-api', 'color-functions', 'import'],
-          },
+        assetsInclude: ['**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.gif', '**/*.svg'],
+        build: {
+            sourcemap: false,
+            // 方便非h5端调试
+            // sourcemap: VITE_SHOW_SOURCEMAP === 'true', // 默认是false
+            target: 'es6',
+            // 开发环境不用压缩
+            minify: mode === 'development' ? false : 'esbuild',
+            assetsInlineLimit: 2 * 1024 * 1024,
         },
-        plugins: [
-          // autoprefixer({
-          //   // 指定目标浏览器
-          //   overrideBrowserslist: ['> 1%', 'last 2 versions'],
-          // }),
-        ],
-      },
-    },
-
-    resolve: {
-      alias: {
-        '@': path.join(process.cwd(), './src'),
-        '@img': path.join(process.cwd(), './src/static/images'),
-      },
-    },
-    server: {
-      host: '0.0.0.0',
-      hmr: true,
-      port: Number.parseInt(VITE_APP_PORT, 10),
-      // 仅 H5 端生效,其他端不生效(其他端走build,不走devServer)
-      proxy: JSON.parse(VITE_APP_PROXY_ENABLE)
-        ? {
-            [VITE_APP_PROXY_PREFIX]: {
-              target: VITE_SERVER_BASEURL,
-              changeOrigin: true,
-              // 后端有/api前缀则不做处理,没有则需要去掉
-              rewrite: path => path.replace(new RegExp(`^${VITE_APP_PROXY_PREFIX}`), ''),
-            },
-          }
-        : undefined,
-    },
-    esbuild: {
-      drop: VITE_DELETE_CONSOLE === 'true' ? ['console', 'debugger'] : [],
-    },
-    build: {
-      sourcemap: false,
-      // 方便非h5端调试
-      // sourcemap: VITE_SHOW_SOURCEMAP === 'true', // 默认是false
-      target: 'es6',
-      // 开发环境不用压缩
-      minify: mode === 'development' ? false : 'esbuild',
-    },
-  })
+    })
 })