浏览代码

国际化基本完成,登录页面绘制完成

Huanyi 1 周之前
父节点
当前提交
dcb0f6ebef
共有 14 个文件被更改,包括 650 次插入685 次删除
  1. 23 0
      App.vue
  2. 0 185
      CHANGELOG_I18N.md
  3. 0 485
      I18N_GUIDE.md
  4. 41 0
      apis/auth.js
  5. 8 0
      locales/index.js
  6. 12 0
      locales/pages/login/en_US.js
  7. 12 0
      locales/pages/login/zh_CN.js
  8. 11 0
      locales/pages/mine/en_US.js
  9. 11 0
      locales/pages/mine/zh_CN.js
  10. 8 0
      pages.json
  11. 27 8
      pages/index/index.vue
  12. 330 0
      pages/login/login.vue
  13. 143 5
      pages/mine/mine.vue
  14. 24 2
      utils/request.js

+ 23 - 0
App.vue

@@ -1,8 +1,31 @@
 <script setup>
 import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
+import { useUserStore } from '@/store/index'
+import { useLocaleStore } from '@/store/locale'
+
+const userStore = useUserStore()
+const localeStore = useLocaleStore()
 
 onLaunch(() => {
   console.log('App Launch')
+  
+  // 初始化语言设置
+  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(() => {

+ 0 - 185
CHANGELOG_I18N.md

@@ -1,185 +0,0 @@
-# 国际化目录结构变更日志
-
-## 2025-12-01 - 目录结构重组
-
-### 变更说明
-
-将语言包组织方式从**按语言分组**调整为**按功能模块分组**,以提升可维护性和扩展性。
-
-### 变更前后对比
-
-#### 旧结构(按语言分组)
-```
-locales/
-├── zh-CN/
-│   ├── common.js
-│   ├── home.js
-│   └── index.js
-├── en-US/
-│   ├── common.js
-│   ├── home.js
-│   └── index.js
-└── index.js
-```
-
-#### 新结构(按功能模块分组)
-```
-locales/
-├── common/
-│   ├── zh_CN.js
-│   └── en_US.js
-├── pages/
-│   └── home/
-│       ├── zh_CN.js
-│       └── en_US.js
-├── index.js
-├── README.md
-└── STRUCTURE.md
-```
-
-### 主要变更
-
-1. **目录组织**
-   - ✅ 每个功能模块独立成目录
-   - ✅ 同一模块的不同语言文件放在一起
-   - ✅ 文件命名从 `zh-CN` 改为 `zh_CN`(使用下划线)
-
-2. **导入方式**
-   ```javascript
-   // 旧方式
-   import zhCN from './zh-CN'
-   import enUS from './en-US'
-   
-   // 新方式
-   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'
-   ```
-
-3. **导出结构**
-   ```javascript
-   // 旧方式
-   export const messages = {
-     'zh-CN': zhCN,
-     'en-US': enUS
-   }
-   
-   // 新方式
-   export const messages = {
-     'zh-CN': {
-       common: commonZhCN,
-       home: homeZhCN
-     },
-     'en-US': {
-       common: commonEnUS,
-       home: homeEnUS
-     }
-   }
-   ```
-
-### 新结构的优势
-
-1. **更易维护**
-   - 同一模块的不同语言翻译在同一目录下
-   - 可以同时打开对比编辑,确保键的一致性
-   - 便于发现翻译遗漏或不一致
-
-2. **更易扩展**
-   - 添加新语言:只需在每个模块目录下添加新文件
-   - 添加新模块:创建新目录,包含所有语言文件
-   - 不需要维护多个语言目录的平行结构
-
-3. **更清晰的职责**
-   - 每个模块独立管理自己的翻译
-   - 模块化开发更友好
-   - 减少目录层级,结构更扁平
-
-4. **团队协作更友好**
-   - 减少文件冲突(不同模块在不同目录)
-   - 代码审查更容易(对比同目录下的文件)
-   - 新成员更容易理解结构
-
-### 迁移影响
-
-#### 对现有代码的影响
-
-**无影响** - 以下部分保持不变:
-
-- ✅ 组件中的使用方式(`t('home.title')`)
-- ✅ i18n 配置和初始化
-- ✅ store 的使用方式
-- ✅ API 接口(`useI18n`、`useLocaleStore`)
-
-#### 需要更新的部分
-
-- ✅ `locales/index.js` - 已更新导入和导出逻辑
-- ✅ 相关文档 - 已更新所有示例和说明
-- ✅ 旧文件 - 已删除
-
-### 文档更新
-
-已更新以下文档以反映新结构:
-
-- ✅ `I18N_GUIDE.md` - 国际化完整指南
-- ✅ `locales/README.md` - 语言包管理文档
-- ✅ `locales/STRUCTURE.md` - 目录结构说明(新增)
-- ✅ `i18n/README.md` - i18n 使用文档
-
-### 快速上手
-
-#### 添加新模块(如 user 模块)
-
-```bash
-# 1. 创建目录和文件
-mkdir -p locales/pages/user
-touch locales/pages/user/zh_CN.js
-touch locales/pages/user/en_US.js
-
-# 2. 编写翻译内容(略)
-
-# 3. 在 locales/index.js 中注册
-# 导入
-import userZhCN from './pages/user/zh_CN'
-import userEnUS from './pages/user/en_US'
-
-# 注册
-export const messages = {
-  'zh-CN': {
-    // ...
-    user: userZhCN
-  },
-  'en-US': {
-    // ...
-    user: userEnUS
-  }
-}
-```
-
-#### 添加新语言(如日语)
-
-```bash
-# 为每个现有模块添加日语文件
-touch locales/common/ja_JP.js
-touch locales/pages/home/ja_JP.js
-
-# 在 locales/index.js 中注册(参考文档)
-```
-
-### 注意事项
-
-1. **文件命名**:使用下划线 `zh_CN.js`,不是连字符 `zh-CN.js`
-2. **模块键名**:在 `locales/index.js` 中注册时,键名要与翻译键的第一级保持一致
-3. **同步维护**:确保同一模块下所有语言文件的键结构完全相同
-
-### 相关链接
-
-- [目录结构详细说明](./locales/STRUCTURE.md)
-- [语言包管理文档](./locales/README.md)
-- [国际化完整指南](./I18N_GUIDE.md)
-
----
-
-**变更日期**:2025-12-01  
-**变更类型**:目录结构重组  
-**向后兼容**:是(对使用方无影响)

+ 0 - 485
I18N_GUIDE.md

@@ -1,485 +0,0 @@
-# 国际化完整指南
-
-## 📦 已完成的功能
-
-✅ vue-i18n 集成
-✅ 模块化语言包管理
-✅ 中英文双语支持
-✅ 语言切换功能
-✅ 持久化存储
-✅ Pinia 状态管理
-✅ 可复用组件
-✅ 完整文档
-
-## 📁 项目结构
-
-```
-intelligent-etmf-system-applet/
-├── i18n/                          # i18n 配置
-│   ├── index.js                  # i18n 初始化
-│   └── README.md                 # 使用文档
-├── locales/                       # 语言包(按模块分组)
-│   ├── common/                   # 通用模块
-│   │   ├── zh_CN.js             # 中文翻译
-│   │   └── en_US.js             # 英文翻译
-│   ├── pages/                    # 页面模块
-│   │   └── home/                # 首页
-│   │       ├── zh_CN.js         # 中文翻译
-│   │       └── en_US.js         # 英文翻译
-│   ├── components/               # 组件模块
-│   │   └── languageSwitcher/    # 语言切换组件
-│   │       ├── zh_CN.js         # 中文翻译
-│   │       └── en_US.js         # 英文翻译
-│   ├── index.js                  # 语言包总导出
-│   ├── README.md                 # 语言包管理文档
-│   ├── STRUCTURE.md              # 目录结构说明
-│   └── COMPONENT_I18N.md         # 组件国际化指南
-├── store/                         # 状态管理
-│   └── locale.js                 # 语言切换 store
-├── utils/                         # 工具函数
-│   └── i18n.js                   # i18n 工具函数
-├── components/                    # 组件
-│   └── LanguageSwitcher/         # 语言切换组件
-│       └── index.vue
-└── main.js                        # 已集成 i18n
-```
-
-## 🚀 快速开始
-
-### 1. 在页面中使用国际化
-
-```vue
-<template>
-  <view>
-    <!-- 基础用法 -->
-    <text>{{ t('home.title') }}</text>
-    
-    <!-- 嵌套键 -->
-    <text>{{ t('common.button.submit') }}</text>
-    
-    <!-- 带参数 -->
-    <text>{{ t('common.greeting', { name: userName }) }}</text>
-  </view>
-</template>
-
-<script setup>
-import { useI18n } from 'vue-i18n'
-
-const { t } = useI18n()
-const userName = '张三'
-</script>
-```
-
-### 2. 在组件中使用国际化
-
-```vue
-<template>
-  <view class="user-card">
-    <!-- 使用组件专属翻译 -->
-    <text>{{ t('components.userCard.title') }}</text>
-    
-    <!-- 使用通用翻译 -->
-    <button>{{ t('common.button.confirm') }}</button>
-    
-    <!-- 动态键 -->
-    <text>{{ t(`components.userCard.status.${user.status}`) }}</text>
-  </view>
-</template>
-
-<script setup>
-import { useI18n } from 'vue-i18n'
-
-const { t } = useI18n()
-const props = defineProps({
-  user: Object
-})
-</script>
-```
-
-### 3. 使用语言切换组件
-
-```vue
-<template>
-  <view class="header">
-    <!-- 方式1:使用封装的组件 -->
-    <LanguageSwitcher />
-    
-    <!-- 方式2:自定义切换逻辑 -->
-    <button @click="changeLanguage">切换语言</button>
-  </view>
-</template>
-
-<script setup>
-import { useLocaleStore } from '@/store/locale'
-import LanguageSwitcher from '@/components/LanguageSwitcher/index.vue'
-
-const localeStore = useLocaleStore()
-
-const changeLanguage = () => {
-  // 切换到下一个语言
-  localeStore.toggleLocale()
-  
-  // 或切换到指定语言
-  // localeStore.setLocale('en-US')
-}
-</script>
-```
-
-### 4. 在 JS 中使用(非组件环境)
-
-```javascript
-import { t } from '@/utils/i18n'
-
-// 在 API 请求中使用
-const showError = () => {
-  uni.showToast({
-    title: t('common.message.error'),
-    icon: 'none'
-  })
-}
-
-// 在工具函数中使用
-const formatStatus = (status) => {
-  return t(`order.status.${status}`)
-}
-```
-
-## 🎨 为组件添加国际化
-
-### 为什么要为组件创建专门的翻译?
-
-- 组件有特定的业务术语
-- 组件需要在多处复用
-- 便于独立维护和版本管理
-
-### 快速示例
-
-创建 UserCard 组件的翻译:
-
-1. 创建翻译文件:`locales/components/userCard/zh_CN.js` 和 `en_US.js`
-2. 在 `locales/index.js` 中注册到 `components` 下
-3. 在组件中使用:`t('components.userCard.title')`
-
-**详细指南**:请查看 [组件国际化指南](./locales/COMPONENT_I18N.md)
-
-## 📝 添加新的翻译模块
-
-### 页面模块示例
-
-在对应目录下创建语言文件。例如创建产品管理页面模块:
-
-创建 `locales/pages/product/zh_CN.js`:
-```javascript
-export default {
-  title: '产品管理',
-  list: {
-    title: '产品列表',
-    empty: '暂无产品'
-  },
-  detail: {
-    name: '产品名称',
-    price: '价格',
-    stock: '库存'
-  },
-  action: {
-    add: '添加产品',
-    edit: '编辑',
-    delete: '删除'
-  }
-}
-```
-
-创建 `locales/pages/product/en_US.js`:
-```javascript
-export default {
-  title: 'Product Management',
-  list: {
-    title: 'Product List',
-    empty: 'No products'
-  },
-  detail: {
-    name: 'Product Name',
-    price: 'Price',
-    stock: 'Stock'
-  },
-  action: {
-    add: 'Add Product',
-    edit: 'Edit',
-    delete: 'Delete'
-  }
-}
-```
-
-### 步骤 2:注册模块
-
-在 `locales/index.js` 中导入新模块:
-```javascript
-// 导入通用模块
-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 productZhCN from './pages/product/zh_CN'  // 新增
-import productEnUS from './pages/product/en_US'  // 新增
-
-// 导入组件模块
-import languageSwitcherZhCN from './components/languageSwitcher/zh_CN'
-import languageSwitcherEnUS from './components/languageSwitcher/en_US'
-
-export const messages = {
-  'zh-CN': {
-    common: commonZhCN,
-    home: homeZhCN,
-    product: productZhCN,  // 新增
-    components: {
-      languageSwitcher: languageSwitcherZhCN
-    }
-  },
-  'en-US': {
-    common: commonEnUS,
-    home: homeEnUS,
-    product: productEnUS,  // 新增
-    components: {
-      languageSwitcher: languageSwitcherEnUS
-    }
-  }
-}
-```
-
-**注意**:
-- 页面模块直接注册在第一层
-- 组件模块注册在 `components` 对象下
-
-### 步骤 3:使用新模块
-
-```vue
-<template>
-  <view>
-    <text>{{ t('product.title') }}</text>
-    <text>{{ t('product.detail.name') }}</text>
-  </view>
-</template>
-```
-
-## 🌍 添加新语言
-
-### 步骤 1:创建语言文件
-
-为每个现有模块创建新语言的翻译文件:
-
-```
-locales/
-  ├── common/
-  │   ├── zh_CN.js
-  │   ├── en_US.js
-  │   └── ja_JP.js        # 新增日文
-  └── pages/
-      └── home/
-          ├── zh_CN.js
-          ├── en_US.js
-          └── ja_JP.js    # 新增日文
-```
-
-### 步骤 2:更新配置
-
-在 `locales/index.js` 中:
-```javascript
-import commonZhCN from './common/zh_CN'
-import commonEnUS from './common/en_US'
-import commonJaJP from './common/ja_JP'  // 新增
-
-import homeZhCN from './pages/home/zh_CN'
-import homeEnUS from './pages/home/en_US'
-import homeJaJP from './pages/home/ja_JP'  // 新增
-
-export const messages = {
-  'zh-CN': {
-    common: commonZhCN,
-    home: homeZhCN
-  },
-  'en-US': {
-    common: commonEnUS,
-    home: homeEnUS
-  },
-  'ja-JP': {  // 新增
-    common: commonJaJP,
-    home: homeJaJP
-  }
-}
-
-export const localeList = [
-  { label: '简体中文', value: 'zh-CN' },
-  { label: 'English', value: 'en-US' },
-  { label: '日本語', value: 'ja-JP' }  // 新增
-]
-```
-
-## 💡 最佳实践
-
-### 1. 模块划分原则
-
-- **common/**:通用文本(按钮、消息、错误提示等),所有语言文件都在 `locales/common/` 下
-- **pages/**:页面模块,每个页面一个子目录(如 `pages/home/`、`pages/user/`),每个子目录下包含各语言版本
-- **其他业务模块**:可根据需要创建其他模块目录(如 `api/`、`components/` 等)
-
-**优势**:
-- 同一功能的不同语言翻译在同一目录下,便于对比和维护
-- 添加新语言只需在每个模块目录下添加新的语言文件
-- 模块划分清晰,职责明确
-
-### 2. 命名规范
-
-```javascript
-// ✅ 推荐:清晰的层级结构
-{
-  user: {
-    profile: {
-      title: '个人资料',
-      field: {
-        name: '姓名',
-        email: '邮箱'
-      }
-    }
-  }
-}
-
-// ❌ 不推荐:扁平结构
-{
-  userProfileTitle: '个人资料',
-  userProfileFieldName: '姓名'
-}
-```
-
-### 3. 参数化文本
-
-```javascript
-// 定义
-{
-  welcome: '欢迎,{name}!',
-  itemCount: '共 {count} 项'
-}
-
-// 使用
-t('common.welcome', { name: '张三' })
-t('common.itemCount', { count: 10 })
-```
-
-### 4. 保持同步
-
-确保同一模块下所有语言文件的翻译键完全一致:
-```javascript
-// ✅ 正确:locales/pages/user/zh_CN.js 和 locales/pages/user/en_US.js 键名相同
-// zh_CN.js
-{ user: { name: '姓名' } }
-
-// en_US.js
-{ user: { name: 'Name' } }
-
-// ❌ 错误:键名不一致
-// zh_CN.js
-{ user: { name: '姓名' } }
-
-// en_US.js
-{ user: { userName: 'Name' } }  // 键名不同!
-```
-
-**提示**:建议同时打开同一模块的不同语言文件进行对比编辑
-
-## 🔧 API 参考
-
-### useLocaleStore
-
-```javascript
-const localeStore = useLocaleStore()
-
-// 状态
-localeStore.currentLocale        // 当前语言:'zh-CN' | 'en-US'
-localeStore.availableLocales     // 可用语言列表
-
-// 方法
-localeStore.setLocale('en-US')   // 切换到指定语言
-localeStore.toggleLocale()       // 切换到下一个语言
-localeStore.getCurrentLocaleName() // 获取当前语言显示名称
-```
-
-### useI18n
-
-```javascript
-const { t, locale, te } = useI18n()
-
-t('home.title')                  // 翻译文本
-t('common.greeting', { name: 'John' }) // 带参数翻译
-locale.value                     // 当前语言
-te('home.title')                 // 检查键是否存在
-```
-
-### 工具函数
-
-```javascript
-import { t, getLocale, setLocale, hasKey } from '@/utils/i18n'
-
-t('home.title')                  // 翻译文本
-getLocale()                      // 获取当前语言
-setLocale('en-US')              // 设置语言
-hasKey('home.title')            // 检查键是否存在
-```
-
-## 🐛 常见问题
-
-### 问题 1:翻译不生效
-
-**原因**:翻译键不存在或拼写错误
-
-**解决**:
-1. 检查翻译键是否正确
-2. 确认模块是否正确导入
-3. 使用浏览器开发工具查看警告信息
-
-### 问题 2:语言切换后页面未更新
-
-**原因**:使用了非响应式的方式获取翻译
-
-**解决**:
-```javascript
-// ❌ 错误:在 setup 外部获取
-const title = t('home.title')
-
-// ✅ 正确:在模板中使用或使用 computed
-{{ t('home.title') }}
-// 或
-const title = computed(() => t('home.title'))
-```
-
-### 问题 3:小程序环境报错
-
-**原因**:i18n 配置不兼容
-
-**解决**:确保使用以下配置
-```javascript
-createI18n({
-  legacy: false,          // 必须:使用 Composition API
-  globalInjection: true,  // 必须:全局注入
-  // ...
-})
-```
-
-## 📚 相关文档
-
-- [i18n 使用指南](./i18n/README.md)
-- [语言包管理](./locales/README.md)
-- [目录结构说明](./locales/STRUCTURE.md)
-- [组件国际化指南](./locales/COMPONENT_I18N.md)
-- [Vue I18n 官方文档](https://vue-i18n.intlify.dev/)
-
-## 🎯 TODO
-
-- [ ] 添加更多语言支持(日语、韩语等)
-- [ ] 实现语言包懒加载优化
-- [ ] 添加翻译缺失检测工具
-- [ ] 集成在线翻译服务
-
-## 📞 联系方式
-
-如有问题或建议,请联系项目维护者。

+ 41 - 0
apis/auth.js

@@ -0,0 +1,41 @@
+/**
+ * 认证相关 API
+ */
+import request from '@/utils/request'
+
+/**
+ * 用户登录
+ * @param {Object} data - 登录信息
+ * @param {String} data.phoneNumber - 手机号
+ * @param {String} data.password - 密码
+ * @returns {Promise}
+ */
+export const login = (data) => {
+  return request({
+    url: '/applet/auth/login',
+    method: 'POST',
+    data
+  })
+}
+
+/**
+ * 获取用户信息
+ * @returns {Promise}
+ */
+export const getUserInfo = () => {
+  return request({
+    url: '/api/user/info',
+    method: 'GET'
+  })
+}
+
+/**
+ * 退出登录
+ * @returns {Promise}
+ */
+export const logout = () => {
+  return request({
+    url: '/api/logout',
+    method: 'POST'
+  })
+}

+ 8 - 0
locales/index.js

@@ -5,6 +5,10 @@ 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'
@@ -15,6 +19,8 @@ export const messages = {
   'zh-CN': {
     common: commonZhCN,
     home: homeZhCN,
+    login: loginZhCN,
+    mine: mineZhCN,
     components: {
       languageSwitcher: languageSwitcherZhCN
     }
@@ -22,6 +28,8 @@ export const messages = {
   'en-US': {
     common: commonEnUS,
     home: homeEnUS,
+    login: loginEnUS,
+    mine: mineEnUS,
     components: {
       languageSwitcher: languageSwitcherEnUS
     }

+ 12 - 0
locales/pages/login/en_US.js

@@ -0,0 +1,12 @@
+export default {
+  title: 'Login',
+  appTitle: 'Intelligent ETMF System',
+  welcome: 'Welcome',
+  phonePlaceholder: 'Enter phone number',
+  passwordPlaceholder: 'Enter password',
+  loginButton: 'Login',
+  phoneError: 'Please enter a valid phone number',
+  passwordError: 'Password must be at least 6 characters',
+  loginSuccess: 'Login successful',
+  loggingIn: 'Logging in...'
+}

+ 12 - 0
locales/pages/login/zh_CN.js

@@ -0,0 +1,12 @@
+export default {
+  title: '登录',
+  appTitle: '智能ETMF系统',
+  welcome: '欢迎登录',
+  phonePlaceholder: '请输入手机号',
+  passwordPlaceholder: '请输入密码',
+  loginButton: '登录',
+  phoneError: '请输入正确的手机号',
+  passwordError: '密码至少6位',
+  loginSuccess: '登录成功',
+  loggingIn: '登录中...'
+}

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

@@ -0,0 +1,11 @@
+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'
+}

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

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

+ 8 - 0
pages.json

@@ -1,5 +1,13 @@
 {
   "pages": [
+    {
+      "path": "pages/login/login",
+      "style": {
+        "navigationBarTitleText": "登录",
+        "navigationStyle": "custom",
+        "enablePullDownRefresh": false
+      }
+    },
     {
       "path": "pages/index/index",
       "style": {

+ 27 - 8
pages/index/index.vue

@@ -29,16 +29,34 @@
 </template>
 
 <script setup>
-import { computed } from 'vue'
+import { computed, watch } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
 import { useI18n } from 'vue-i18n'
 import { useLocaleStore } from '@/store/locale'
 
-const { t } = useI18n()
+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()
@@ -72,16 +90,17 @@ const handleClick = () => {
   
   .language-switch {
     position: absolute;
-    top: 20rpx;
-    right: 20rpx;
-    background: rgba(255, 255, 255, 0.2);
-    padding: 12rpx 24rpx;
-    border-radius: 30rpx;
+    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: 24rpx;
+      font-size: 28rpx;
       color: #ffffff;
       font-weight: 500;
     }

+ 330 - 0
pages/login/login.vue

@@ -0,0 +1,330 @@
+<template>
+  <view class="login-container">
+    <!-- 语言切换按钮 -->
+    <view class="language-switcher" @click="switchLanguage">
+      <text class="language-icon">🌐</text>
+      <text class="language-text">{{ currentLanguageName }}</text>
+    </view>
+    
+    <view class="login-box">
+      <view class="logo-section">
+        <text class="app-title">{{ $t('login.appTitle') }}</text>
+        <text class="app-subtitle">{{ $t('login.welcome') }}</text>
+      </view>
+      
+      <view class="form-section">
+        <view class="form-item">
+          <view class="input-wrapper">
+            <text class="input-icon">📱</text>
+            <input
+              v-model="phone"
+              type="text"
+              maxlength="11"
+              :placeholder="$t('login.phonePlaceholder')"
+              class="input-field"
+              @input="onPhoneInput"
+            />
+          </view>
+        </view>
+        
+        <view class="form-item">
+          <view class="input-wrapper">
+            <text class="input-icon">🔒</text>
+            <input
+              v-model="password"
+              :password="!showPassword"
+              :placeholder="$t('login.passwordPlaceholder')"
+              class="input-field"
+            />
+            <text 
+              class="toggle-password" 
+              @click="togglePassword"
+            >
+              {{ showPassword ? '👁' : '👁‍🗨' }}
+            </text>
+          </view>
+        </view>
+        
+        <button 
+          class="login-btn"
+          @click="handleLogin"
+        >
+          {{ $t('login.loginButton') }}
+        </button>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script setup>
+import { ref, 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'
+import { login } from '@/apis/auth'
+
+const { t, locale } = useI18n()
+const userStore = useUserStore()
+const localeStore = useLocaleStore()
+
+const phone = ref('')
+const password = ref('')
+const showPassword = ref(false)
+
+// 当前语言名称
+const currentLanguageName = computed(() => {
+  return localeStore.getCurrentLocaleName()
+})
+
+// 设置页面标题
+const setPageTitle = () => {
+  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 isValidPhone = computed(() => {
+  return /^1[3-9]\d{9}$/.test(phone.value)
+})
+
+// 是否可以提交
+const canSubmit = computed(() => {
+  return isValidPhone.value && password.value.length >= 6
+})
+
+// 手机号输入处理
+const onPhoneInput = (e) => {
+  // 限制只能输入数字
+  phone.value = e.detail.value.replace(/[^\d]/g, '')
+}
+
+// 切换密码显示
+const togglePassword = () => {
+  showPassword.value = !showPassword.value
+}
+
+// 登录处理
+const handleLogin = async () => {
+  // 验证手机号
+  if (!isValidPhone.value) {
+    uni.showToast({
+      title: t('login.phoneError'),
+      icon: 'none',
+      duration: 2000
+    })
+    return
+  }
+  
+  // 验证密码
+  if (password.value.length < 6) {
+    uni.showToast({
+      title: t('login.passwordError'),
+      icon: 'none',
+      duration: 2000
+    })
+    return
+  }
+  
+  try {
+    uni.showLoading({
+      title: t('login.loggingIn'),
+      mask: true
+    })
+    
+    const res = await login({
+      phoneNumber: phone.value,
+      password: password.value
+    })
+    
+    // 保存 token
+    userStore.setToken(res.data.token)
+    
+    uni.hideLoading()
+    
+    uni.showToast({
+      title: t('login.loginSuccess'),
+      icon: 'success',
+      duration: 1500
+    })
+    
+    // 延迟跳转到首页
+    setTimeout(() => {
+      uni.switchTab({
+        url: '/pages/index/index'
+      })
+    }, 1500)
+    
+  } catch (error) {
+    uni.hideLoading()
+    console.error('登录失败:', error)
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+.login-container {
+  min-height: 100vh;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: flex-start;
+  padding: 40rpx;
+  padding-top: 270rpx;
+  position: relative;
+}
+
+.language-switcher {
+  align-self: flex-end;
+  margin-bottom: 32rpx;
+  margin-right: 40rpx;
+  display: flex;
+  align-items: center;
+  background: rgba(255, 255, 255, 0.25);
+  backdrop-filter: blur(10rpx);
+  border-radius: 40rpx;
+  padding: 16rpx 28rpx;
+  cursor: pointer;
+  transition: all 0.3s;
+  box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
+  
+  &:active {
+    transform: scale(0.95);
+    background: rgba(255, 255, 255, 0.35);
+  }
+}
+
+.login-box {
+  width: 100%;
+  max-width: 600rpx;
+  background: #ffffff;
+  border-radius: 32rpx;
+  padding: 60rpx 40rpx;
+  box-shadow: 0 20rpx 60rpx rgba(0, 0, 0, 0.15);
+}
+
+.language-icon {
+  font-size: 36rpx;
+  margin-right: 8rpx;
+}
+
+.language-text {
+  font-size: 28rpx;
+  color: #ffffff;
+  font-weight: 500;
+}
+
+.logo-section {
+  text-align: center;
+  margin-bottom: 80rpx;
+}
+
+.app-title {
+  display: block;
+  font-size: 48rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 16rpx;
+}
+
+.app-subtitle {
+  display: block;
+  font-size: 28rpx;
+  color: #999;
+}
+
+.form-section {
+  width: 100%;
+}
+
+.form-item {
+  margin-bottom: 32rpx;
+}
+
+.input-wrapper {
+  display: flex;
+  align-items: center;
+  background: #f5f5f5;
+  border-radius: 16rpx;
+  padding: 0 24rpx;
+  height: 96rpx;
+  transition: all 0.3s;
+  border: 2rpx solid transparent;
+  
+  &:focus-within {
+    background: #fff;
+    border-color: #667eea;
+    box-shadow: 0 0 0 4rpx rgba(102, 126, 234, 0.1);
+  }
+}
+
+.input-icon {
+  font-size: 40rpx;
+  margin-right: 16rpx;
+}
+
+.input-field {
+  flex: 1;
+  font-size: 32rpx;
+  color: #333;
+  
+  &::placeholder {
+    color: #999;
+  }
+}
+
+.toggle-password {
+  font-size: 40rpx;
+  cursor: pointer;
+  padding: 8rpx;
+}
+
+.login-btn {
+  width: 100%;
+  height: 96rpx;
+  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+  color: #ffffff;
+  font-size: 32rpx;
+  font-weight: bold;
+  border-radius: 16rpx;
+  border: none;
+  margin-top: 48rpx;
+  transition: all 0.3s;
+  
+  &:active {
+    opacity: 0.8;
+    transform: scale(0.98);
+  }
+}
+
+.login-btn-disabled {
+  background: #ccc;
+  opacity: 0.6;
+  
+  &:active {
+    transform: none;
+  }
+}
+</style>

+ 143 - 5
pages/mine/mine.vue

@@ -1,43 +1,125 @@
 <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">用户昵称</text>
+      <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">个人信息</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">设置</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">关于</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: `点击了${type}`,
+    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>
@@ -50,6 +132,38 @@ const handleMenuClick = (type) => {
   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;
@@ -116,5 +230,29 @@ const handleMenuClick = (type) => {
       }
     }
   }
+  
+  .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>

+ 24 - 2
utils/request.js

@@ -8,6 +8,21 @@ const CLIENT_ID = '2f847927afb2b3ebeefc870c13d623f2'
 // 基础 URL(根据实际情况修改)
 const BASE_URL = 'http://127.0.0.1:8080'
 
+/**
+ * 获取当前语言环境
+ * @returns {String} 语言代码(转换为下划线格式,如 zh_CN)
+ */
+const getLanguage = () => {
+  try {
+    // 从本地存储获取语言设置,默认为 zh-CN
+    const locale = uni.getStorageSync('locale') || 'zh-CN'
+    // 将连字符转换为下划线格式(zh-CN -> zh_CN)
+    return locale.replace('-', '_')
+  } catch (e) {
+    return 'zh_CN'
+  }
+}
+
 /**
  * 封装的请求方法
  * @param {Object} options - 请求配置
@@ -21,6 +36,8 @@ const request = (options) => {
   return new Promise((resolve, reject) => {
     // 获取存储的 token
     const token = uni.getStorageSync('token') || ''
+    // 获取当前语言环境
+    const language = getLanguage()
     
     uni.request({
       url: BASE_URL + options.url,
@@ -28,6 +45,7 @@ const request = (options) => {
       data: options.data || {},
       header: {
         'Content-Type': 'application/json',
+        'Content-Language': language,
         'clientid': CLIENT_ID,
         'token': token,
         ...options.header
@@ -66,8 +84,12 @@ const request = (options) => {
             })
             // 清除 token
             uni.removeStorageSync('token')
-            // 跳转到登录页(如果有的话)
-            // uni.navigateTo({ url: '/pages/login/login' })
+            // 跳转到登录页
+            setTimeout(() => {
+              uni.reLaunch({ 
+                url: '/pages/login/login' 
+              })
+            }, 2000)
             reject({ code, msg: 'token失效' })
             break