Browse Source

重构整个项目:自定义header和tabbar,完成我的页面的基本配置

Huanyi 1 week ago
parent
commit
651fe1a02c

+ 320 - 0
ARCHITECTURE.md

@@ -0,0 +1,320 @@
+# 项目架构说明
+
+## 页面结构
+
+### 单页应用架构
+
+项目采用**单页面 + 组件切换**的架构,实现类似原生TabBar的效果,同时保持头部和底部固定不变。
+
+```
+pages/
+├── login/
+│   └── login.vue           # 登录页面(独立页面)
+└── index.vue               # 主容器页面(核心)
+
+pages-content/              # 内容组件目录(独立)
+├── home/
+│   └── index.vue           # 首页内容组件
+├── scan/
+│   └── index.vue           # 扫描内容组件
+└── my/
+    └── index.vue           # 我的内容组件
+```
+
+## 主容器页面 (pages/index.vue)
+
+### 结构组成
+
+```
+┌─────────────────────────────┐
+│   自定义头部(固定)          │  ← 白色背景,显示当前标题
+│   - 自适应状态栏高度         │     根据currentTab自动切换
+├─────────────────────────────┤
+│                             │
+│   主内容区(动态切换)        │  ← 使用 v-if 切换组件
+│   - HomePage 组件           │     - currentTab = 'home'
+│   - ScanPage 组件           │     - currentTab = 'scan'
+│   - MyPage 组件             │     - currentTab = 'mine'
+│                             │
+├─────────────────────────────┤
+│   自定义底部导航栏(固定)    │  ← 白色背景,三个菜单
+│   🏠 首页 | 📷 扫描 | 👤 我的 │     点击切换 currentTab
+└─────────────────────────────┘
+```
+
+### 核心代码逻辑
+
+```javascript
+// 当前激活的tab
+const currentTab = ref('home')
+
+// 切换tab - 只需要改变currentTab值
+const switchTab = (name) => {
+  if (currentTab.value === name) return
+  currentTab.value = name
+}
+
+// 标题自动跟随当前tab
+const currentPageTitle = computed(() => {
+  const titles = {
+    home: '首页',
+    scan: '扫描',
+    mine: '我的'
+  }
+  return titles[currentTab.value] || '首页'
+})
+```
+
+### 组件切换
+
+```vue
+<view class="main-content">
+  <!-- 通过 v-if 实现组件切换 -->
+  <HomePage v-if="currentTab === 'home'" />
+  <ScanPage v-else-if="currentTab === 'scan'" />
+  <MyPage v-else-if="currentTab === 'mine'" />
+</view>
+```
+
+## 内容组件
+
+### 首页组件 (pages-content/home/index.vue)
+
+```vue
+<template>
+  <view class="home-page">
+    <view class="page-header">
+      <text class="title">首页内容</text>
+    </view>
+    <view class="page-body">
+      <text class="placeholder">首页主体内容区域</text>
+    </view>
+  </view>
+</template>
+```
+
+**特点**:
+- 纯内容组件,无头部和底部
+- 有自己的内部结构和样式
+- 可独立开发和维护
+
+### 扫描组件 (pages-content/scan/index.vue)
+
+```vue
+<template>
+  <view class="scan-page">
+    <view class="page-header">
+      <text class="title">扫描内容</text>
+    </view>
+    <view class="page-body">
+      <text class="placeholder">扫描主体内容区域</text>
+    </view>
+  </view>
+</template>
+```
+
+**特点**:
+- 用于实现扫描相关功能
+- 可以调用相机API、扫码API等
+
+### 我的组件 (pages-content/my/index.vue)
+
+```vue
+<template>
+  <view class="my-page">
+    <view class="page-header">
+      <text class="title">我的内容</text>
+    </view>
+    <view class="page-body">
+      <text class="placeholder">我的主体内容区域</text>
+    </view>
+  </view>
+</template>
+```
+
+**特点**:
+- 用于个人中心相关功能
+- 可以显示用户信息、设置等
+
+## 页面跳转流程
+
+### 登录成功后跳转
+
+```javascript
+// pages/login/login.vue
+uni.navigateTo({
+  url: '/pages/index'  // 跳转到主容器页面
+})
+```
+
+### 主容器内切换
+
+```javascript
+// pages/index.vue
+switchTab('scan')  // 切换到扫描页
+// ↓
+currentTab.value = 'scan'
+// ↓
+<ScanPage /> 组件显示
+// ↓
+头部标题变为"扫描"
+// ↓
+底部导航高亮"扫描"
+```
+
+## 架构优势
+
+### 1. 性能优化
+
+| 操作 | 重新渲染 | 说明 |
+|------|---------|------|
+| 初始加载 | 完整渲染 | 加载主容器 + 首页组件 |
+| 切换tab | 只渲染内容 | 头部和底部不变 |
+| 返回登录 | 完整渲染 | 卸载主容器 |
+
+### 2. 开发便利
+
+- ✅ 每个内容组件独立开发
+- ✅ 头部和底部统一管理
+- ✅ 切换逻辑简单清晰
+- ✅ 易于维护和扩展
+
+### 3. 用户体验
+
+- ✅ 切换流畅,无闪烁
+- ✅ 头部标题自动更新
+- ✅ 底部高亮自动切换
+- ✅ 类似原生TabBar体验
+
+## 扩展指南
+
+### 添加新的Tab页面
+
+**1. 创建内容组件**
+
+```bash
+pages-content/newpage/index.vue
+```
+
+**2. 在主容器中引入**
+
+```vue
+<!-- pages/index.vue -->
+<script setup>
+import NewPage from '../pages-content/newpage/index.vue'
+</script>
+
+<template>
+  <NewPage v-else-if="currentTab === 'newpage'" />
+</template>
+```
+
+**3. 添加底部导航配置**
+
+```javascript
+const tabList = ref([
+  // ...现有配置
+  {
+    name: 'newpage',
+    label: '新页面',
+    icon: '🎯'
+  }
+])
+```
+
+**4. 添加标题映射**
+
+```javascript
+const currentPageTitle = computed(() => {
+  const titles = {
+    // ...
+    newpage: '新页面'
+  }
+  return titles[currentTab.value]
+})
+```
+
+### 自定义头部
+
+如果需要不同页面有不同的头部样式:
+
+```vue
+<!-- 方案1:使用 computed 动态样式 -->
+<view 
+  class="custom-header" 
+  :style="{ 
+    background: headerBackground,
+    paddingTop: statusBarHeight + 'px' 
+  }"
+>
+  <text>{{ currentPageTitle }}</text>
+</view>
+
+<script>
+const headerBackground = computed(() => {
+  const colors = {
+    home: '#ffffff',
+    scan: '#667eea',
+    mine: '#ffffff'
+  }
+  return colors[currentTab.value]
+})
+</script>
+```
+
+### 组件通信
+
+如果需要在内容组件间共享数据:
+
+**使用 Pinia Store**
+```javascript
+// store/tab.js
+export const useTabStore = defineStore('tab', {
+  state: () => ({
+    sharedData: null
+  })
+})
+
+// 在任意组件中使用
+import { useTabStore } from '@/store/tab'
+const tabStore = useTabStore()
+```
+
+## 配置文件
+
+### pages.json
+
+```json
+{
+  "pages": [
+    {
+      "path": "pages/login/login",
+      "style": {
+        "navigationStyle": "custom"
+      }
+    },
+    {
+      "path": "pages/index",
+      "style": {
+        "navigationStyle": "custom"  // 使用自定义导航栏
+      }
+    }
+  ]
+}
+```
+
+**注意**:
+- 不需要配置 `tabBar`
+- 所有tab功能都由 `pages/index.vue` 实现
+- `pages-content/*` 下的组件不需要在 `pages.json` 中注册
+
+## 总结
+
+这是一个**轻量级、高性能**的单页应用架构:
+
+- 🎯 **简单** - 只有一个主页面 + 三个内容组件
+- ⚡ **快速** - 切换tab只更新内容区域
+- 🎨 **灵活** - 每个组件独立开发维护
+- 📦 **可扩展** - 易于添加新的tab页面
+
+适合需要底部导航的小程序项目!

+ 0 - 15
App.vue

@@ -11,21 +11,6 @@ onLaunch(() => {
   
   // 初始化语言设置
   localeStore.initLocale()
-  
-  // 检测本地 token
-  const token = uni.getStorageSync('token')
-  
-  if (!token) {
-    // 没有 token,跳转到登录页
-    console.log('未检测到 token,跳转到登录页')
-    uni.reLaunch({
-      url: '/pages/login/login'
-    })
-  } else {
-    // 有 token,恢复用户状态
-    console.log('检测到 token,恢复用户状态')
-    userStore.restoreState()
-  }
 })
 
 onShow(() => {

+ 118 - 0
LANGUAGE_SWITCH_OPTIMIZATION.md

@@ -0,0 +1,118 @@
+# 语言切换优化说明
+
+## 优化目标
+点击语言切换时,只切换文本语言环境,避免不必要的页面重绘。
+
+## 实施的优化措施
+
+### 1. **页面级优化** (`pages/login/login.vue`)
+
+#### 添加防抖保护
+- 添加 `isLanguageSwitching` 状态标志
+- 防止300ms内的重复点击
+- 避免频繁触发语言切换
+
+#### 使用 nextTick 优化更新时序
+```javascript
+const switchLanguage = async () => {
+  // 防止频繁切换
+  if (isLanguageSwitching.value) return
+  
+  isLanguageSwitching.value = true
+  
+  try {
+    // 直接切换语言
+    localeStore.toggleLocale()
+    
+    // 等待 DOM 更新完成
+    await nextTick()
+    
+    // 异步更新导航栏标题
+    uni.setNavigationBarTitle({
+      title: t('login.title')
+    })
+  } finally {
+    // 300ms后允许再次切换
+    setTimeout(() => {
+      isLanguageSwitching.value = false
+    }, 300)
+  }
+}
+```
+
+### 2. **Store级优化** (`store/locale.js`)
+
+#### 添加相同语言检查
+```javascript
+if (currentLocale.value === locale) {
+  return true  // 语言相同,直接返回,避免无效更新
+}
+```
+
+#### 异步持久化操作
+- 将 `uni.setStorageSync` 改为异步执行
+- 不阻塞UI线程
+- 优先保证用户体验
+
+#### 优化更新顺序
+```javascript
+// 先更新 store 状态
+currentLocale.value = locale
+// 再更新 i18n
+i18n.global.locale.value = locale
+// 最后异步持久化
+setTimeout(() => {
+  uni.setStorageSync('locale', locale)
+}, 0)
+```
+
+## 优化效果
+
+### 性能提升
+1. **减少不必要的响应式触发**:通过相同语言检查
+2. **批量更新**:使用 nextTick 合并更新
+3. **异步持久化**:不阻塞UI线程
+4. **防抖保护**:避免频繁切换造成的性能损耗
+
+### 用户体验改善
+1. **流畅的文本切换**:语言切换更加平滑
+2. **无卡顿**:异步操作不影响UI响应
+3. **即时反馈**:文本立即更新,存储在后台完成
+
+## 技术原理
+
+### Vue i18n 响应式机制
+- `i18n.global.locale` 是响应式的
+- 改变时会触发所有使用 `$t()` 的组件更新
+- 这是正常的文本更新,不是"重绘整个页面"
+
+### nextTick 的作用
+- 确保状态更新在同一个事件循环中完成
+- 减少多次渲染
+- 在 DOM 更新后执行回调
+
+### 异步策略
+- 将非关键操作(如持久化)延迟执行
+- 优先响应用户交互
+- 提升感知性能
+
+## 注意事项
+
+1. **语言切换本质**:Vue i18n 的设计就是在语言切换时更新所有翻译文本,这是必要的
+2. **不是完全避免重绘**:文本内容变化必然触发文本节点的重新渲染
+3. **优化的是**:减少不必要的重复更新、避免阻塞操作、提升切换流畅度
+
+## 测试建议
+
+1. 快速连续点击语言切换按钮,应该有300ms的节流保护
+2. 切换语言后,所有文本应立即更新
+3. 页面不应出现明显的卡顿或闪烁
+4. 存储操作在后台完成,不影响UI
+
+## 后续优化方向
+
+如果仍需进一步优化,可以考虑:
+1. 使用虚拟滚动减少大量文本节点的影响
+2. 按需加载语言包,减少初始包体积
+3. 使用 Web Worker 处理语言包解析
+4. 实现增量更新策略(仅更新可见区域)

+ 1 - 1
apis/auth.js

@@ -24,7 +24,7 @@ export const login = (data) => {
  */
 export const getUserInfo = () => {
   return request({
-    url: '/api/user/info',
+    url: '/applet/user/getInfo',
     method: 'GET'
   })
 }

+ 26 - 0
locales/common/en_US.js

@@ -26,9 +26,35 @@ export default {
     noData: 'No data',
     networkError: 'Network error, please try again later'
   },
+  error: {
+    unauthorized: 'Login expired, please log in again',
+    serverError: 'Server error',
+    requestFailed: 'Request failed',
+    networkFailed: 'Network request failed'
+  },
   language: {
     zh: '简体中文',
     en: 'English',
     switchSuccess: 'Language switched successfully'
+  },
+  page: {
+    home: 'Home',
+    scan: 'Scan',
+    mine: 'Mine'
+  },
+  mine: {
+    title: 'Mine',
+    loading: 'Loading...',
+    basicInfo: 'Basic Info',
+    fileManage: 'File Management',
+    auditManage: 'Audit Management',
+    languageSwitch: 'Language',
+    protocol: 'Protocol',
+    logout: 'Logout',
+    logoutConfirm: 'Are you sure you want to logout?',
+    logoutSuccess: 'Logged out successfully',
+    getUserInfoFailed: 'Failed to get user info',
+    defaultNickname: 'No nickname',
+    loadingFailed: 'Load failed'
   }
 }

+ 26 - 0
locales/common/zh_CN.js

@@ -26,9 +26,35 @@ export default {
     noData: '暂无数据',
     networkError: '网络错误,请稍后重试'
   },
+  error: {
+    unauthorized: '登录失效,请重新登录',
+    serverError: '系统错误',
+    requestFailed: '请求失败',
+    networkFailed: '网络请求失败'
+  },
   language: {
     zh: '简体中文',
     en: 'English',
     switchSuccess: '语言切换成功'
+  },
+  page: {
+    home: '首页',
+    scan: '扫描',
+    mine: '我的'
+  },
+  mine: {
+    title: '我的',
+    loading: '加载中...',
+    basicInfo: '基本信息',
+    fileManage: '文件管理',
+    auditManage: '审核管理',
+    languageSwitch: '语言切换',
+    protocol: '协议说明',
+    logout: '退出登录',
+    logoutConfirm: '确定要退出登录吗?',
+    logoutSuccess: '已退出登录',
+    getUserInfoFailed: '获取用户信息失败',
+    defaultNickname: '未设置昵称',
+    loadingFailed: '获取失败'
   }
 }

+ 0 - 8
locales/index.js

@@ -3,12 +3,8 @@ import commonZhCN from './common/zh_CN'
 import commonEnUS from './common/en_US'
 
 // 导入页面模块
-import homeZhCN from './pages/home/zh_CN'
-import homeEnUS from './pages/home/en_US'
 import loginZhCN from './pages/login/zh_CN'
 import loginEnUS from './pages/login/en_US'
-import mineZhCN from './pages/mine/zh_CN'
-import mineEnUS from './pages/mine/en_US'
 
 // 导入组件模块
 import languageSwitcherZhCN from './components/languageSwitcher/zh_CN'
@@ -18,18 +14,14 @@ import languageSwitcherEnUS from './components/languageSwitcher/en_US'
 export const messages = {
   'zh-CN': {
     common: commonZhCN,
-    home: homeZhCN,
     login: loginZhCN,
-    mine: mineZhCN,
     components: {
       languageSwitcher: languageSwitcherZhCN
     }
   },
   'en-US': {
     common: commonEnUS,
-    home: homeEnUS,
     login: loginEnUS,
-    mine: mineEnUS,
     components: {
       languageSwitcher: languageSwitcherEnUS
     }

+ 0 - 11
locales/pages/home/en_US.js

@@ -1,11 +0,0 @@
-export default {
-  title: 'Intelligent eTMF System',
-  subtitle: 'Welcome',
-  systemInfo: 'System Information',
-  framework: 'Framework Version',
-  platform: 'Platform',
-  frameworkValue: 'Vue 3 + uni-app',
-  platformValue: 'WeChat Mini Program',
-  startButton: 'Get Started',
-  welcomeMessage: 'Welcome to Intelligent eTMF System'
-}

+ 0 - 11
locales/pages/home/zh_CN.js

@@ -1,11 +0,0 @@
-export default {
-  title: '智能eTMF系统',
-  subtitle: '欢迎使用',
-  systemInfo: '系统信息',
-  framework: '框架版本',
-  platform: '平台',
-  frameworkValue: 'Vue 3 + uni-app',
-  platformValue: '微信小程序',
-  startButton: '开始使用',
-  welcomeMessage: '欢迎使用智能eTMF系统'
-}

+ 0 - 11
locales/pages/mine/en_US.js

@@ -1,11 +0,0 @@
-export default {
-  title: 'Mine',
-  username: 'Username',
-  profile: 'Profile',
-  settings: 'Settings',
-  about: 'About',
-  logout: 'Logout',
-  confirmLogout: 'Confirm Logout',
-  logoutMessage: 'Are you sure you want to logout?',
-  logoutSuccess: 'Logout successful'
-}

+ 0 - 11
locales/pages/mine/zh_CN.js

@@ -1,11 +0,0 @@
-export default {
-  title: '我的',
-  username: '用户昵称',
-  profile: '个人信息',
-  settings: '设置',
-  about: '关于',
-  logout: '退出登录',
-  confirmLogout: '确认退出',
-  logoutMessage: '确定要退出登录吗?',
-  logoutSuccess: '退出成功'
-}

+ 50 - 50
manifest.json

@@ -1,54 +1,54 @@
 {
-  "name": "intelligent-etmf-system-applet",
-  "appid": "__UNI__YOUR_APPID",
-  "description": "智能ETMF系统小程序",
-  "versionName": "1.0.0",
-  "versionCode": "100",
-  "transformPx": false,
-  "app-plus": {
-    "usingComponents": true,
-    "nvueStyleCompiler": "uni-app",
-    "compilerVersion": 3,
-    "splashscreen": {
-      "alwaysShowBeforeRender": true,
-      "waiting": true,
-      "autoclose": true,
-      "delay": 0
+    "name" : "intelligent-etmf-system-applet",
+    "appid" : "__UNI__F09429B",
+    "description" : "智能ETMF系统小程序",
+    "versionName" : "1.0.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueStyleCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        "modules" : {},
+        "distribute" : {
+            "android" : {
+                "permissions" : []
+            },
+            "ios" : {},
+            "sdkConfigs" : {}
+        }
     },
-    "modules": {},
-    "distribute": {
-      "android": {
-        "permissions": []
-      },
-      "ios": {},
-      "sdkConfigs": {}
-    }
-  },
-  "quickapp": {},
-  "mp-weixin": {
-    "appid": "",
-    "setting": {
-      "urlCheck": false,
-      "es6": true,
-      "postcss": true,
-      "minified": true,
-      "newFeature": true
+    "quickapp" : {},
+    "mp-weixin" : {
+        "appid" : "",
+        "setting" : {
+            "urlCheck" : false,
+            "es6" : true,
+            "postcss" : true,
+            "minified" : true,
+            "newFeature" : true
+        },
+        "usingComponents" : true,
+        "permission" : {},
+        "requiredPrivateInfos" : []
     },
-    "usingComponents": true,
-    "permission": {},
-    "requiredPrivateInfos": []
-  },
-  "mp-alipay": {
-    "usingComponents": true
-  },
-  "mp-baidu": {
-    "usingComponents": true
-  },
-  "mp-toutiao": {
-    "usingComponents": true
-  },
-  "uniStatistics": {
-    "enable": false
-  },
-  "vueVersion": "3"
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "uniStatistics" : {
+        "enable" : false
+    },
+    "vueVersion" : "3"
 }

+ 81 - 0
pages-content/home/index.vue

@@ -0,0 +1,81 @@
+<template>
+  <view class="home-page">
+    <!-- 自定义头部 -->
+    <view class="custom-header" :style="{ paddingTop: statusBarHeight + 'px' }">
+      <view class="header-content">
+        <text class="header-title">{{ t('common.page.home') }}</text>
+      </view>
+    </view>
+    
+    <!-- 页面内容 -->
+    <view class="page-body">
+      <text class="placeholder">首页主体内容区域</text>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useI18n } from 'vue-i18n'
+
+const { t } = useI18n()
+
+// 状态栏高度
+const statusBarHeight = ref(0)
+
+onMounted(() => {
+  // 获取系统信息
+  const systemInfo = uni.getSystemInfoSync()
+  statusBarHeight.value = systemInfo.statusBarHeight || 0
+  
+  console.log('首页内容组件已加载')
+})
+</script>
+
+<style lang="scss" scoped>
+.home-page {
+  width: 100%;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  background: linear-gradient(180deg, #fff5f5 0%, #f8f9fa 100%);
+  
+  // 自定义头部
+  .custom-header {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    background-color: #ffffff;
+    border-bottom: 1rpx solid #e5e5e5;
+    z-index: 100;
+    
+    .header-content {
+      height: 88rpx;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      
+      .header-title {
+        font-size: 32rpx;
+        font-weight: 500;
+        color: #000000;
+      }
+    }
+  }
+  
+  // 页面内容
+  .page-body {
+    flex: 1;
+    padding: 40rpx;
+    margin-top: 88rpx;
+    
+    .placeholder {
+      font-size: 28rpx;
+      color: #999999;
+      text-align: center;
+      margin-top: 200rpx;
+    }
+  }
+}
+</style>

+ 498 - 0
pages-content/my/index.vue

@@ -0,0 +1,498 @@
+<template>
+  <view class="my-page">
+    <!-- 自定义头部 -->
+    <view class="custom-header" :style="{ paddingTop: statusBarHeight + 'px' }">
+      <view class="header-content">
+        <text class="header-title">{{ t('common.mine.title') }}</text>
+      </view>
+    </view>
+    
+    <!-- 页面内容 -->
+    <view class="page-body">
+      <!-- 用户名片区域 -->
+      <view class="user-card">
+        <image 
+          class="avatar" 
+          :src="userInfo.avatar" 
+          mode="aspectFill"
+          :class="{ loading: loading }"
+          @error="handleAvatarError"
+        />
+        <text class="nickname" :class="{ loading: loading }">{{ displayNickname }}</text>
+      </view>
+      
+      <!-- 功能列表 -->
+      <view class="function-list">
+        <!-- 基本信息 -->
+        <view class="list-item" @click="handleBasicInfo">
+          <view class="item-left">
+            <text class="item-icon">👤</text>
+            <text class="item-label">{{ t('common.mine.basicInfo') }}</text>
+          </view>
+          <text class="item-arrow">›</text>
+        </view>
+        
+        <!-- 文件管理 -->
+        <view class="list-item" @click="handleFileManage">
+          <view class="item-left">
+            <text class="item-icon">📁</text>
+            <text class="item-label">{{ t('common.mine.fileManage') }}</text>
+          </view>
+          <text class="item-arrow">›</text>
+        </view>
+        
+        <!-- 审核管理 -->
+        <view class="list-item" @click="handleAuditManage">
+          <view class="item-left">
+            <text class="item-icon">📋</text>
+            <text class="item-label">{{ t('common.mine.auditManage') }}</text>
+          </view>
+          <text class="item-arrow">›</text>
+        </view>
+        
+        <!-- 语言切换 -->
+        <view class="list-item">
+          <view class="item-left">
+            <text class="item-icon">🌐</text>
+            <text class="item-label">{{ t('common.mine.languageSwitch') }}</text>
+          </view>
+          <view class="item-right">
+            <text class="language-text">{{ currentLanguage }}</text>
+            <switch 
+              :checked="isEnglish" 
+              @change="handleLanguageChange"
+              color="#007aff"
+            />
+          </view>
+        </view>
+        
+        <!-- 协议说明 -->
+        <view class="list-item" @click="handleProtocol">
+          <view class="item-left">
+            <text class="item-icon">📄</text>
+            <text class="item-label">{{ t('common.mine.protocol') }}</text>
+          </view>
+          <text class="item-arrow">›</text>
+        </view>
+      </view>
+      
+      <!-- 退出登录按钮 -->
+      <view class="logout-section">
+        <button class="logout-btn" @click="handleLogout">{{ t('common.mine.logout') }}</button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { useI18n } from 'vue-i18n'
+import { useUserStore } from '@/store/index'
+import { useLocaleStore } from '@/store/locale'
+import { getUserInfo as getUserInfoAPI } from '@/apis/auth'
+
+const { t, locale } = useI18n()
+const userStore = useUserStore()
+const localeStore = useLocaleStore()
+
+// 状态栏高度
+const statusBarHeight = ref(0)
+
+// 用户信息
+const userInfo = ref({
+  avatar: '/static/default-avatar.svg',
+  nickname: ''
+})
+
+// 加载状态
+const loading = ref(false)
+
+// 当前语言显示
+const currentLanguage = computed(() => {
+  return locale.value === 'zh-CN' ? t('common.language.zh') : t('common.language.en')
+})
+
+// 显示的昵称(带加载状态)
+const displayNickname = computed(() => {
+  if (loading.value) {
+    return t('common.mine.loading')
+  }
+  return userInfo.value.nickname || t('common.mine.defaultNickname')
+})
+
+// 是否英文
+const isEnglish = computed(() => {
+  return locale.value === 'en-US'
+})
+
+onMounted(() => {
+  // 获取系统信息
+  const systemInfo = uni.getSystemInfoSync()
+  statusBarHeight.value = systemInfo.statusBarHeight || 0
+  
+  // 获取用户信息
+  fetchUserInfo()
+  
+  console.log('我的内容组件已加载')
+})
+
+// 头像加载失败处理
+const handleAvatarError = () => {
+  console.log('头像加载失败,使用默认头像')
+  userInfo.value.avatar = '/static/default-avatar.svg'
+}
+
+// 获取用户信息
+const fetchUserInfo = async () => {
+  try {
+    loading.value = true
+    
+    // 调用 API 获取用户信息
+    const response = await getUserInfoAPI()
+    
+    if (response && response.data) {
+      // 更新用户信息
+      userInfo.value = {
+        avatar: response.data.avatar || '/static/default-avatar.svg',
+        nickname: response.data.nickname || ''
+      }
+      
+      // 同步更新 store
+      userStore.setUserInfo(response.data)
+    }
+  } catch (error) {
+    console.error('获取用户信息失败:', error)
+    
+    // 如果API失败,尝试从本地store获取
+    const storedUserInfo = userStore.userInfo
+    if (storedUserInfo && storedUserInfo.nickname) {
+      userInfo.value = {
+        avatar: storedUserInfo.avatar || '/static/default-avatar.svg',
+        nickname: storedUserInfo.nickname
+      }
+    } else {
+      userInfo.value = {
+        avatar: '/static/default-avatar.svg',
+        nickname: ''
+      }
+    }
+    
+    uni.showToast({
+      title: t('common.mine.getUserInfoFailed'),
+      icon: 'none',
+      duration: 2000
+    })
+  } finally {
+    loading.value = false
+  }
+}
+
+// 基本信息
+const handleBasicInfo = () => {
+  uni.showToast({
+    title: '基本信息',
+    icon: 'none'
+  })
+  // TODO: 跳转到基本信息页面
+}
+
+// 文件管理
+const handleFileManage = () => {
+  uni.showToast({
+    title: '文件管理',
+    icon: 'none'
+  })
+  // TODO: 跳转到文件管理页面
+}
+
+// 审核管理
+const handleAuditManage = () => {
+  uni.showToast({
+    title: '审核管理',
+    icon: 'none'
+  })
+  // TODO: 跳转到审核管理页面
+}
+
+// 语言切换
+const handleLanguageChange = (e) => {
+  const isChecked = e.detail.value
+  const newLocale = isChecked ? 'en-US' : 'zh-CN'
+  
+  const success = localeStore.setLocale(newLocale)
+  
+  if (success) {
+    // 延迟一下让语言切换生效后再显示提示
+    setTimeout(() => {
+      uni.showToast({
+        title: t('common.language.switchSuccess'),
+        icon: 'success',
+        duration: 1500
+      })
+    }, 100)
+  }
+}
+
+// 协议说明
+const handleProtocol = () => {
+  uni.showToast({
+    title: '协议说明',
+    icon: 'none'
+  })
+  // TODO: 跳转到协议说明页面
+}
+
+// 退出登录
+const handleLogout = () => {
+  uni.showModal({
+    title: t('common.button.confirm'),
+    content: t('common.mine.logoutConfirm'),
+    confirmText: t('common.button.confirm'),
+    cancelText: t('common.button.cancel'),
+    success: (res) => {
+      if (res.confirm) {
+        // 清除本地token和用户信息缓存
+        userStore.logout()
+        
+        // 显示退出成功提示
+        uni.showToast({
+          title: t('common.mine.logoutSuccess'),
+          icon: 'success',
+          duration: 1500
+        })
+        
+        // 延迟跳转到登录页
+        setTimeout(() => {
+          uni.reLaunch({
+            url: '/pages/login/login'
+          })
+        }, 1500)
+      }
+    }
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+.my-page {
+  width: 100%;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  background: linear-gradient(180deg, #f0f4ff 0%, #f8f9fa 100%);
+  
+  // 自定义头部
+  .custom-header {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    background-color: #ffffff;
+    border-bottom: 1rpx solid #e5e5e5;
+    z-index: 100;
+    
+    .header-content {
+      height: 88rpx;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      
+      .header-title {
+        font-size: 32rpx;
+        font-weight: 500;
+        color: #000000;
+      }
+    }
+  }
+  
+  // 页面内容
+  .page-body {
+    flex: 1;
+    margin-top: 88rpx;
+    padding: 40rpx;
+    
+    // 用户名片区域
+    .user-card {
+      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+      border-radius: 24rpx;
+      padding: 80rpx 40rpx 60rpx;
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      margin-bottom: 40rpx;
+      box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3);
+      position: relative;
+      overflow: hidden;
+      
+      // 背景装饰
+      &::before {
+        content: '';
+        position: absolute;
+        top: -50%;
+        right: -20%;
+        width: 400rpx;
+        height: 400rpx;
+        background: rgba(255, 255, 255, 0.1);
+        border-radius: 50%;
+      }
+      
+      &::after {
+        content: '';
+        position: absolute;
+        bottom: -30%;
+        left: -10%;
+        width: 300rpx;
+        height: 300rpx;
+        background: rgba(255, 255, 255, 0.08);
+        border-radius: 50%;
+      }
+      
+      .avatar {
+        width: 140rpx;
+        height: 140rpx;
+        border-radius: 70rpx;
+        margin-bottom: 24rpx;
+        border: 6rpx solid rgba(255, 255, 255, 0.3);
+        box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.2);
+        position: relative;
+        z-index: 1;
+        transition: opacity 0.3s;
+        
+        &.loading {
+          opacity: 0.5;
+          animation: pulse 1.5s ease-in-out infinite;
+        }
+      }
+      
+      .nickname {
+        font-size: 40rpx;
+        font-weight: bold;
+        color: #ffffff;
+        text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2);
+        position: relative;
+        z-index: 1;
+        transition: opacity 0.3s;
+        
+        &.loading {
+          opacity: 0.7;
+        }
+      }
+    }
+    
+    // 功能列表
+    .function-list {
+      background-color: #ffffff;
+      border-radius: 20rpx;
+      overflow: hidden;
+      margin-bottom: 40rpx;
+      box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
+      
+      .list-item {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        padding: 36rpx 40rpx;
+        border-bottom: 1rpx solid #f0f0f0;
+        transition: all 0.3s ease;
+        position: relative;
+        
+        &:last-child {
+          border-bottom: none;
+        }
+        
+        &:active {
+          background-color: #f8f9ff;
+          transform: scale(0.98);
+        }
+        
+        // 左侧渐变条
+        &::before {
+          content: '';
+          position: absolute;
+          left: 0;
+          top: 50%;
+          transform: translateY(-50%);
+          width: 6rpx;
+          height: 60%;
+          background: linear-gradient(180deg, #667eea 0%, #764ba2 100%);
+          border-radius: 0 6rpx 6rpx 0;
+          opacity: 0;
+          transition: opacity 0.3s;
+        }
+        
+        &:active::before {
+          opacity: 1;
+        }
+        
+        .item-left {
+          display: flex;
+          align-items: center;
+          
+          .item-icon {
+            font-size: 44rpx;
+            margin-right: 28rpx;
+            filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.1));
+          }
+          
+          .item-label {
+            font-size: 30rpx;
+            color: #333333;
+            font-weight: 500;
+          }
+        }
+        
+        .item-right {
+          display: flex;
+          align-items: center;
+          gap: 20rpx;
+          
+          .language-text {
+            font-size: 26rpx;
+            color: #667eea;
+            font-weight: 500;
+          }
+        }
+        
+        .item-arrow {
+          font-size: 48rpx;
+          color: #d0d0d0;
+          font-weight: 300;
+        }
+      }
+    }
+    
+    // 退出登录
+    .logout-section {
+      padding: 0;
+      
+      .logout-btn {
+        width: 100%;
+        height: 96rpx;
+        background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
+        border-radius: 20rpx;
+        border: none;
+        font-size: 32rpx;
+        color: #ffffff;
+        font-weight: 600;
+        box-shadow: 0 6rpx 20rpx rgba(255, 107, 107, 0.3);
+        letter-spacing: 2rpx;
+        
+        &:active {
+          opacity: 0.9;
+          transform: scale(0.98);
+        }
+      }
+    }
+  }
+}
+
+// 加载动画
+@keyframes pulse {
+  0%, 100% {
+    opacity: 0.5;
+  }
+  50% {
+    opacity: 0.8;
+  }
+}
+</style>

+ 81 - 0
pages-content/scan/index.vue

@@ -0,0 +1,81 @@
+<template>
+  <view class="scan-page">
+    <!-- 自定义头部 -->
+    <view class="custom-header" :style="{ paddingTop: statusBarHeight + 'px' }">
+      <view class="header-content">
+        <text class="header-title">{{ t('common.page.scan') }}</text>
+      </view>
+    </view>
+    
+    <!-- 页面内容 -->
+    <view class="page-body">
+      <text class="placeholder">扫描主体内容区域</text>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useI18n } from 'vue-i18n'
+
+const { t } = useI18n()
+
+// 状态栏高度
+const statusBarHeight = ref(0)
+
+onMounted(() => {
+  // 获取系统信息
+  const systemInfo = uni.getSystemInfoSync()
+  statusBarHeight.value = systemInfo.statusBarHeight || 0
+  
+  console.log('扫描内容组件已加载')
+})
+</script>
+
+<style lang="scss" scoped>
+.scan-page {
+  width: 100%;
+  min-height: 100vh;
+  display: flex;
+  flex-direction: column;
+  background: linear-gradient(180deg, #e0f7fa 0%, #f8f9fa 100%);
+  
+  // 自定义头部
+  .custom-header {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    background-color: #ffffff;
+    border-bottom: 1rpx solid #e5e5e5;
+    z-index: 100;
+    
+    .header-content {
+      height: 88rpx;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      
+      .header-title {
+        font-size: 32rpx;
+        font-weight: 500;
+        color: #000000;
+      }
+    }
+  }
+  
+  // 页面内容
+  .page-body {
+    flex: 1;
+    padding: 40rpx;
+    margin-top: 88rpx;
+    
+    .placeholder {
+      font-size: 28rpx;
+      color: #999999;
+      text-align: center;
+      margin-top: 200rpx;
+    }
+  }
+}
+</style>

+ 3 - 26
pages.json

@@ -9,41 +9,18 @@
       }
     },
     {
-      "path": "pages/index/index",
+      "path": "pages/index",
       "style": {
-        "navigationBarTitleText": "首页",
-        "enablePullDownRefresh": false
-      }
-    },
-    {
-      "path": "pages/mine/mine",
-      "style": {
-        "navigationBarTitleText": "我的",
+        "navigationStyle": "custom",
         "enablePullDownRefresh": false
       }
     }
   ],
   "globalStyle": {
     "navigationBarTextStyle": "black",
-    "navigationBarTitleText": "智能ETMF系统",
+    "navigationBarTitleText": "智能eTMF系统",
     "navigationBarBackgroundColor": "#F8F8F8",
     "backgroundColor": "#F8F8F8"
   },
-  "tabBar": {
-    "color": "#7A7E83",
-    "selectedColor": "#3cc51f",
-    "borderStyle": "black",
-    "backgroundColor": "#ffffff",
-    "list": [
-      {
-        "pagePath": "pages/index/index",
-        "text": "首页"
-      },
-      {
-        "pagePath": "pages/mine/mine",
-        "text": "我的"
-      }
-    ]
-  },
   "uniIdRouter": {}
 }

+ 143 - 0
pages/index.vue

@@ -0,0 +1,143 @@
+<template>
+  <view class="page-container">
+    <!-- 主内容区 -->
+    <view class="main-content">
+      <!-- 首页内容 -->
+      <HomePage v-if="currentTab === 'home'" />
+      
+      <!-- 扫描内容 -->
+      <ScanPage v-else-if="currentTab === 'scan'" />
+      
+      <!-- 我的内容 -->
+      <MyPage v-else-if="currentTab === 'mine'" />
+    </view>
+    
+    <!-- 自定义底部导航栏 -->
+    <view class="custom-tabbar">
+      <view 
+        v-for="(item, index) in tabList" 
+        :key="index"
+        class="tab-item"
+        :class="{ active: currentTab === item.name }"
+        @click="switchTab(item.name)"
+      >
+        <image 
+          class="tab-icon" 
+          :src="currentTab === item.name ? item.activeIcon : item.icon"
+          mode="aspectFit"
+        />
+        <text class="tab-text">{{ item.label }}</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+import { useI18n } from 'vue-i18n'
+import HomePage from '../pages-content/home/index.vue'
+import ScanPage from '../pages-content/scan/index.vue'
+import MyPage from '../pages-content/my/index.vue'
+
+const { t } = useI18n()
+
+// 当前激活的tab
+const currentTab = ref('home')
+
+// 底部导航配置
+const tabList = computed(() => [
+  {
+    name: 'home',
+    label: t('common.page.home'),
+    icon: '/static/tabbar/home.png',
+    activeIcon: '/static/tabbar/home-active.png'
+  },
+  {
+    name: 'scan',
+    label: t('common.page.scan'),
+    icon: '/static/tabbar/scan.png',
+    activeIcon: '/static/tabbar/scan-active.png'
+  },
+  {
+    name: 'mine',
+    label: t('common.page.mine'),
+    icon: '/static/tabbar/mine.png',
+    activeIcon: '/static/tabbar/mine-active.png'
+  }
+])
+
+// 切换tab
+const switchTab = (name) => {
+  if (currentTab.value === name) return
+  currentTab.value = name
+}
+</script>
+
+<style lang="scss" scoped>
+.page-container {
+  width: 100%;
+  height: 100vh;
+  display: flex;
+  flex-direction: column;
+  background-color: #f5f5f5;
+}
+
+// 主内容区
+.main-content {
+  flex: 1;
+  overflow-y: auto;
+  padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
+}
+
+// 自定义底部导航栏
+.custom-tabbar {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 100rpx;
+  background-color: #ffffff;
+  display: flex;
+  border-top: 1rpx solid #e5e5e5;
+  padding-bottom: env(safe-area-inset-bottom);
+  z-index: 1000;
+  
+  .tab-item {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    padding: 8rpx 0;
+    transition: all 0.3s ease;
+    
+    .tab-icon {
+      width: 48rpx;
+      height: 48rpx;
+      margin-bottom: 4rpx;
+      transition: transform 0.3s ease;
+    }
+    
+    .tab-text {
+      font-size: 20rpx;
+      color: #666666;
+      transition: color 0.3s ease;
+    }
+    
+    &.active {
+      .tab-icon {
+        transform: scale(1.1);
+      }
+      
+      .tab-text {
+        color: #007aff;
+        font-weight: 500;
+      }
+    }
+    
+    &:active {
+      opacity: 0.8;
+    }
+  }
+}
+</style>

+ 0 - 177
pages/index/index.vue

@@ -1,177 +0,0 @@
-<template>
-  <view class="container">
-    <view class="header">
-      <view class="language-switch" @click="handleLanguageSwitch">
-        <text class="language-text">{{ currentLocaleName }}</text>
-      </view>
-      <text class="title">{{ t('home.title') }}</text>
-      <text class="subtitle">{{ t('home.subtitle') }}</text>
-    </view>
-    
-    <view class="content">
-      <view class="card">
-        <text class="card-title">{{ t('home.systemInfo') }}</text>
-        <view class="info-item">
-          <text class="label">{{ t('home.framework') }}:</text>
-          <text class="value">{{ t('home.frameworkValue') }}</text>
-        </view>
-        <view class="info-item">
-          <text class="label">{{ t('home.platform') }}:</text>
-          <text class="value">{{ t('home.platformValue') }}</text>
-        </view>
-      </view>
-      
-      <view class="action-section">
-        <button type="primary" @click="handleClick">{{ t('home.startButton') }}</button>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script setup>
-import { computed, watch } from 'vue'
-import { onShow } from '@dcloudio/uni-app'
-import { useI18n } from 'vue-i18n'
-import { useLocaleStore } from '@/store/locale'
-
-const { t, locale } = useI18n()
-const localeStore = useLocaleStore()
-
-// 获取当前语言名称
-const currentLocaleName = computed(() => localeStore.getCurrentLocaleName())
-
-// 设置页面标题
-const setPageTitle = () => {
-  uni.setNavigationBarTitle({
-    title: t('home.title')
-  })
-}
-
-// 页面显示时设置标题
-onShow(() => {
-  setPageTitle()
-})
-
-// 监听语言变化,更新标题
-watch(locale, () => {
-  setPageTitle()
-})
-
-// 切换语言
-const handleLanguageSwitch = () => {
-  const success = localeStore.toggleLocale()
-  if (success) {
-    uni.showToast({
-      title: t('common.language.switchSuccess'),
-      icon: 'success'
-    })
-  }
-}
-
-const handleClick = () => {
-  uni.showToast({
-    title: t('home.welcomeMessage'),
-    icon: 'success'
-  })
-}
-</script>
-
-<style lang="scss" scoped>
-.container {
-  min-height: 100vh;
-  padding: 40rpx;
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-}
-
-.header {
-  text-align: center;
-  padding: 60rpx 0;
-  position: relative;
-  
-  .language-switch {
-    position: absolute;
-    top: 40rpx;
-    right: 32rpx;
-    background: rgba(255, 255, 255, 0.25);
-    padding: 16rpx 28rpx;
-    border-radius: 40rpx;
-    backdrop-filter: blur(10rpx);
-    border: 1rpx solid rgba(255, 255, 255, 0.3);
-    box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
-    
-    .language-text {
-      font-size: 28rpx;
-      color: #ffffff;
-      font-weight: 500;
-    }
-  }
-  
-  .title {
-    display: block;
-    font-size: 48rpx;
-    font-weight: bold;
-    color: #ffffff;
-    margin-bottom: 20rpx;
-  }
-  
-  .subtitle {
-    display: block;
-    font-size: 28rpx;
-    color: rgba(255, 255, 255, 0.8);
-  }
-}
-
-.content {
-  .card {
-    background: #ffffff;
-    border-radius: 20rpx;
-    padding: 40rpx;
-    margin-bottom: 40rpx;
-    box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
-    
-    .card-title {
-      display: block;
-      font-size: 32rpx;
-      font-weight: bold;
-      color: #333333;
-      margin-bottom: 30rpx;
-    }
-    
-    .info-item {
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-      padding: 20rpx 0;
-      border-bottom: 1rpx solid #f0f0f0;
-      
-      &:last-child {
-        border-bottom: none;
-      }
-      
-      .label {
-        font-size: 28rpx;
-        color: #666666;
-      }
-      
-      .value {
-        font-size: 28rpx;
-        color: #333333;
-        font-weight: 500;
-      }
-    }
-  }
-  
-  .action-section {
-    padding: 20rpx 0;
-    
-    button {
-      width: 100%;
-      height: 88rpx;
-      line-height: 88rpx;
-      border-radius: 44rpx;
-      font-size: 32rpx;
-      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-    }
-  }
-}
-</style>

+ 47 - 26
pages/login/login.vue

@@ -57,12 +57,12 @@
 </template>
 
 <script setup>
-import { ref, computed, watch } from 'vue'
+import { ref, computed, nextTick } from 'vue'
 import { onShow } from '@dcloudio/uni-app'
 import { useI18n } from 'vue-i18n'
 import { useUserStore } from '@/store/index'
 import { useLocaleStore } from '@/store/locale'
-import { login } from '@/apis/auth'
+import request from '@/utils/request.js'
 
 const { t, locale } = useI18n()
 const userStore = useUserStore()
@@ -71,37 +71,47 @@ const localeStore = useLocaleStore()
 const phone = ref('')
 const password = ref('')
 const showPassword = ref(false)
+const isLanguageSwitching = ref(false)
 
 // 当前语言名称
 const currentLanguageName = computed(() => {
   return localeStore.getCurrentLocaleName()
 })
 
-// 设置页面标题
-const setPageTitle = () => {
+// 页面显示时更新标题
+onShow(() => {
+  // 更新导航栏标题
   uni.setNavigationBarTitle({
     title: t('login.title')
   })
-}
-
-// 页面显示时设置标题
-onShow(() => {
-  setPageTitle()
-})
-
-// 监听语言变化,更新标题
-watch(locale, () => {
-  setPageTitle()
 })
 
-// 切换语言
-const switchLanguage = () => {
-  localeStore.toggleLocale()
-  uni.showToast({
-    title: t('common.language.switchSuccess'),
-    icon: 'success',
-    duration: 1500
-  })
+// 切换语言 - 优化版本,避免页面重绘
+const switchLanguage = async () => {
+  // 防止频繁切换
+  if (isLanguageSwitching.value) {
+    return
+  }
+  
+  isLanguageSwitching.value = true
+  
+  try {
+    // 直接切换语言,i18n 会自动处理文本更新
+    localeStore.toggleLocale()
+    
+    // 使用 nextTick 确保在 DOM 更新后再更新导航栏
+    await nextTick()
+    
+    // 异步更新导航栏标题
+    uni.setNavigationBarTitle({
+      title: t('login.title')
+    })
+  } finally {
+    // 短暂延迟后允许再次切换
+    setTimeout(() => {
+      isLanguageSwitching.value = false
+    }, 300)
+  }
 }
 
 // 验证手机号格式
@@ -153,11 +163,22 @@ const handleLogin = async () => {
       mask: true
     })
     
-    const res = await login({
+    // 准备登录参数
+    const loginContent = JSON.stringify({
       phoneNumber: phone.value,
       password: password.value
     })
     
+    // 调用新的登录接口
+    const res = await request({
+      url: '/applet/auth/login/password',
+      method: 'POST',
+      data: {
+        clientId: '2f847927afb2b3ebeefc870c13d623f2',
+        content: loginContent
+      }
+    })
+    
     // 保存 token
     userStore.setToken(res.data.token)
     
@@ -169,10 +190,10 @@ const handleLogin = async () => {
       duration: 1500
     })
     
-    // 延迟跳转到首页
+    // 登录成功后跳转到首页
     setTimeout(() => {
-      uni.switchTab({
-        url: '/pages/index/index'
+      uni.navigateTo({
+        url: '/pages/index'
       })
     }, 1500)
     

+ 0 - 258
pages/mine/mine.vue

@@ -1,258 +0,0 @@
-<template>
-  <view class="container">
-    <view class="header">
-      <!-- 语言切换按钮 -->
-      <view class="language-switcher" @click="switchLanguage">
-        <text class="language-icon">🌐</text>
-        <text class="language-text">{{ currentLanguageName }}</text>
-      </view>
-      
-      <view class="avatar">
-        <image class="avatar-img" src="/static/default-avatar.png" mode="aspectFill"></image>
-      </view>
-      <text class="username">{{ userStore.nickname }}</text>
-    </view>
-    
-    <view class="content">
-      <view class="menu-list">
-        <view class="menu-item" @click="handleMenuClick('profile')">
-          <text class="menu-icon">👤</text>
-          <text class="menu-text">{{ $t('mine.profile') }}</text>
-          <text class="menu-arrow">›</text>
-        </view>
-        
-        <view class="menu-item" @click="handleMenuClick('settings')">
-          <text class="menu-icon">⚙️</text>
-          <text class="menu-text">{{ $t('mine.settings') }}</text>
-          <text class="menu-arrow">›</text>
-        </view>
-        
-        <view class="menu-item" @click="handleMenuClick('about')">
-          <text class="menu-icon">ℹ️</text>
-          <text class="menu-text">{{ $t('mine.about') }}</text>
-          <text class="menu-arrow">›</text>
-        </view>
-      </view>
-      
-      <view class="logout-section">
-        <button class="logout-btn" @click="handleLogout">
-          {{ $t('mine.logout') }}
-        </button>
-      </view>
-    </view>
-  </view>
-</template>
-
-<script setup>
-import { computed, watch } from 'vue'
-import { onShow } from '@dcloudio/uni-app'
-import { useI18n } from 'vue-i18n'
-import { useUserStore } from '@/store/index'
-import { useLocaleStore } from '@/store/locale'
-
-const { t, locale } = useI18n()
-const userStore = useUserStore()
-const localeStore = useLocaleStore()
-
-// 当前语言名称
-const currentLanguageName = computed(() => {
-  return localeStore.getCurrentLocaleName()
-})
-
-// 设置页面标题
-const setPageTitle = () => {
-  uni.setNavigationBarTitle({
-    title: t('mine.title')
-  })
-}
-
-// 页面显示时设置标题
-onShow(() => {
-  setPageTitle()
-})
-
-// 监听语言变化,更新标题
-watch(locale, () => {
-  setPageTitle()
-})
-
-// 切换语言
-const switchLanguage = () => {
-  localeStore.toggleLocale()
-  uni.showToast({
-    title: t('common.language.switchSuccess'),
-    icon: 'success',
-    duration: 1500
-  })
-}
-
-// 菜单点击
-const handleMenuClick = (type) => {
-  uni.showToast({
-    title: `${t('mine.' + type)}`,
-    icon: 'none'
-  })
-}
-
-// 退出登录
-const handleLogout = () => {
-  uni.showModal({
-    title: t('mine.confirmLogout'),
-    content: t('mine.logoutMessage'),
-    success: (res) => {
-      if (res.confirm) {
-        // 清除用户信息
-        userStore.logout()
-        
-        uni.showToast({
-          title: t('mine.logoutSuccess'),
-          icon: 'success',
-          duration: 1500
-        })
-        
-        // 跳转到登录页
-        setTimeout(() => {
-          uni.reLaunch({
-            url: '/pages/login/login'
-          })
-        }, 1500)
-      }
-    }
-  })
-}
-</script>
-
-<style lang="scss" scoped>
-.container {
-  min-height: 100vh;
-  background-color: #f8f8f8;
-}
-
-.header {
-  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-  padding: 60rpx 40rpx 80rpx;
-  text-align: center;
-  position: relative;
-  
-  .language-switcher {
-    position: absolute;
-    top: 60rpx;
-    right: 32rpx;
-    display: flex;
-    align-items: center;
-    background: rgba(255, 255, 255, 0.25);
-    backdrop-filter: blur(10rpx);
-    border-radius: 40rpx;
-    padding: 16rpx 28rpx;
-    transition: all 0.3s;
-    z-index: 10;
-    box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
-    
-    &:active {
-      transform: scale(0.95);
-      background: rgba(255, 255, 255, 0.35);
-    }
-    
-    .language-icon {
-      font-size: 36rpx;
-      margin-right: 8rpx;
-    }
-    
-    .language-text {
-      font-size: 28rpx;
-      color: #ffffff;
-      font-weight: 500;
-    }
-  }
-  
-  .avatar {
-    width: 140rpx;
-    height: 140rpx;
-    margin: 0 auto 20rpx;
-    border-radius: 50%;
-    background-color: #ffffff;
-    overflow: hidden;
-    
-    .avatar-img {
-      width: 100%;
-      height: 100%;
-    }
-  }
-  
-  .username {
-    display: block;
-    font-size: 32rpx;
-    font-weight: bold;
-    color: #ffffff;
-  }
-}
-
-.content {
-  margin-top: -40rpx;
-  padding: 0 40rpx;
-  
-  .menu-list {
-    background-color: #ffffff;
-    border-radius: 20rpx;
-    overflow: hidden;
-    box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
-    
-    .menu-item {
-      display: flex;
-      align-items: center;
-      padding: 30rpx 40rpx;
-      border-bottom: 1rpx solid #f0f0f0;
-      transition: background-color 0.3s;
-      
-      &:last-child {
-        border-bottom: none;
-      }
-      
-      &:active {
-        background-color: #f5f5f5;
-      }
-      
-      .menu-icon {
-        font-size: 40rpx;
-        margin-right: 24rpx;
-      }
-      
-      .menu-text {
-        flex: 1;
-        font-size: 28rpx;
-        color: #333333;
-      }
-      
-      .menu-arrow {
-        font-size: 48rpx;
-        color: #cccccc;
-        font-weight: 300;
-      }
-    }
-  }
-  
-  .logout-section {
-    margin-top: 40rpx;
-    padding: 0 40rpx;
-    
-    .logout-btn {
-      width: 100%;
-      height: 88rpx;
-      line-height: 88rpx;
-      background: linear-gradient(135deg, #ff6b6b 0%, #ee5a6f 100%);
-      color: #ffffff;
-      font-size: 32rpx;
-      font-weight: bold;
-      border-radius: 44rpx;
-      border: none;
-      box-shadow: 0 4rpx 20rpx rgba(238, 90, 111, 0.3);
-      transition: all 0.3s;
-      
-      &:active {
-        opacity: 0.8;
-        transform: scale(0.98);
-      }
-    }
-  }
-}
-</style>

+ 46 - 0
project.config.json

@@ -0,0 +1,46 @@
+{
+  "description": "项目配置文件",
+  "packOptions": {
+    "ignore": [],
+    "include": []
+  },
+  "setting": {
+    "bundle": false,
+    "userConfirmedBundleSwitch": false,
+    "urlCheck": true,
+    "scopeDataCheck": false,
+    "coverView": true,
+    "es6": true,
+    "postcss": true,
+    "compileHotReLoad": true,
+    "lazyloadPlaceholderEnable": false,
+    "preloadBackgroundData": false,
+    "minified": true,
+    "autoAudits": false,
+    "newFeature": false,
+    "uglifyFileName": false,
+    "uploadWithSourceMap": true,
+    "useIsolateContext": true,
+    "nodeModules": false,
+    "enhance": true,
+    "useMultiFrameRuntime": true,
+    "useApiHook": true,
+    "useApiHostProcess": true,
+    "showShadowRootInWxmlPanel": true,
+    "packNpmManually": false,
+    "enableEngineNative": false,
+    "packNpmRelationList": [],
+    "minifyWXSS": true,
+    "showES6CompileOption": false,
+    "minifyWXML": true
+  },
+  "compileType": "miniprogram",
+  "libVersion": "3.11.3",
+  "appid": "touristappid",
+  "projectname": "intelligent-etmf-system-applet",
+  "condition": {},
+  "editorSetting": {
+    "tabIndent": "insertSpaces",
+    "tabSize": 2
+  }
+}

+ 0 - 0
static/default-avatar.png


+ 10 - 0
static/default-avatar.svg

@@ -0,0 +1,10 @@
+<svg width="120" height="120" viewBox="0 0 120 120" xmlns="http://www.w3.org/2000/svg">
+  <!-- 背景圆 -->
+  <circle cx="60" cy="60" r="60" fill="#E0E0E0"/>
+  
+  <!-- 头部 -->
+  <circle cx="60" cy="45" r="20" fill="#BDBDBD"/>
+  
+  <!-- 身体 -->
+  <ellipse cx="60" cy="95" rx="30" ry="25" fill="#BDBDBD"/>
+</svg>

BIN
static/tabbar/home-active.png


BIN
static/tabbar/home.png


BIN
static/tabbar/mine-active.png


BIN
static/tabbar/mine.png


BIN
static/tabbar/scan-active.png


BIN
static/tabbar/scan.png


+ 5 - 2
store/index.js

@@ -14,7 +14,7 @@ export const useUserStore = defineStore('user', {
     // 获取用户昵称
     nickname: (state) => state.userInfo?.nickname || '未登录',
     // 获取用户头像
-    avatar: (state) => state.userInfo?.avatar || '/static/default-avatar.png'
+    avatar: (state) => state.userInfo?.avatar || '/static/default-avatar.svg'
   },
   
   actions: {
@@ -31,11 +31,14 @@ export const useUserStore = defineStore('user', {
       uni.setStorageSync('token', token)
     },
     
-    // 登出
+    // 登出(仅清除本地数据,不调用API)
     logout() {
+      // 清除 store 中的用户信息
       this.userInfo = null
       this.token = ''
       this.isLogin = false
+      
+      // 清除本地存储的 token
       uni.removeStorageSync('token')
     },
     

+ 17 - 5
store/locale.js

@@ -30,13 +30,25 @@ export const useLocaleStore = defineStore('locale', () => {
       return false
     }
     
+    // 如果语言相同,直接返回,避免不必要的更新
+    if (currentLocale.value === locale) {
+      return true
+    }
+    
     try {
-      // 更新 i18n 语言
-      i18n.global.locale.value = locale
-      // 更新 store 状态
+      // 批量更新:先更新 store 状态,再更新 i18n,最后持久化
+      // 这样可以减少响应式触发次数
       currentLocale.value = locale
-      // 持久化到本地存储
-      uni.setStorageSync('locale', locale)
+      i18n.global.locale.value = locale
+      
+      // 异步持久化,不阻塞UI
+      setTimeout(() => {
+        try {
+          uni.setStorageSync('locale', locale)
+        } catch (e) {
+          console.error('Failed to save locale:', e)
+        }
+      }, 0)
       
       return true
     } catch (e) {

+ 10 - 9
utils/request.js

@@ -1,6 +1,7 @@
 /**
  * 网络请求封装
  */
+import { t } from './i18n.js'
 
 // 固定的 clientid
 const CLIENT_ID = '2f847927afb2b3ebeefc870c13d623f2'
@@ -47,7 +48,7 @@ const request = (options) => {
         'Content-Type': 'application/json',
         'Content-Language': language,
         'clientid': CLIENT_ID,
-        'token': token,
+        'Authorization': 'Bearer ' + token,
         ...options.header
       },
       success: (res) => {
@@ -76,9 +77,9 @@ const request = (options) => {
             break
             
           case 401:
-            // token 失效
+            // 登录失效
             uni.showToast({
-              title: 'token失效,请重新登录',
+              title: t('common.error.unauthorized'),
               icon: 'none',
               duration: 2000
             })
@@ -90,23 +91,23 @@ const request = (options) => {
                 url: '/pages/login/login' 
               })
             }, 2000)
-            reject({ code, msg: 'token失效' })
+            reject({ code, msg: t('common.error.unauthorized') })
             break
             
           case 500:
             // 系统错误
             uni.showToast({
-              title: '系统错误',
+              title: t('common.error.serverError'),
               icon: 'none',
               duration: 2000
             })
-            reject({ code, msg: '系统错误' })
+            reject({ code, msg: t('common.error.serverError') })
             break
             
           case 501:
             // 提示返回的 msg
             uni.showToast({
-              title: msg || '请求失败',
+              title: msg || t('common.error.requestFailed'),
               icon: 'none',
               duration: 2000
             })
@@ -116,7 +117,7 @@ const request = (options) => {
           default:
             // 其他状态码
             uni.showToast({
-              title: msg || '请求失败',
+              title: msg || t('common.error.requestFailed'),
               icon: 'none',
               duration: 2000
             })
@@ -126,7 +127,7 @@ const request = (options) => {
       },
       fail: (err) => {
         uni.showToast({
-          title: '网络请求失败',
+          title: t('common.error.networkFailed'),
           icon: 'none',
           duration: 2000
         })