Просмотр исходного кода

接口对接了一大半;扫描正在解决当中

Huanyi 3 месяцев назад
Родитель
Сommit
8c36e87316

+ 0 - 118
LANGUAGE_SWITCH_OPTIMIZATION.md

@@ -1,118 +0,0 @@
-# 语言切换优化说明
-
-## 优化目标
-点击语言切换时,只切换文本语言环境,避免不必要的页面重绘。
-
-## 实施的优化措施
-
-### 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. 实现增量更新策略(仅更新可见区域)

+ 11 - 0
apis/auth.js

@@ -50,3 +50,14 @@ export const logout = () => {
     method: 'DELETE'
   })
 }
+
+/**
+ * 获取我的任务数量
+ * @returns {Promise}
+ */
+export const getTaskCount = () => {
+  return request({
+    url: '/applet/mine/count',
+    method: 'GET'
+  })
+}

+ 27 - 0
apis/setting.js

@@ -14,3 +14,30 @@ export const getAgreementContent = (type) => {
     method: 'GET'
   })
 }
+
+/**
+ * 获取小程序轮播图列表
+ * @returns {Promise}
+ */
+export const getCarouselList = () => {
+  return request({
+    url: '/setting/carousel/listOnApplet',
+    method: 'GET'
+  })
+}
+
+/**
+ * 获取首页最近文档列表
+ * @param {Object} params - 查询参数
+ * @param {String} params.name - 文档名称(模糊搜索)
+ * @param {Number} params.pageNum - 页码
+ * @param {Number} params.pageSize - 每页数量
+ * @returns {Promise}
+ */
+export const getRecentDocuments = (params) => {
+  return request({
+    url: '/applet/home/listRecent',
+    method: 'GET',
+    data: params
+  })
+}

+ 8 - 9
components/TabBar/index.vue

@@ -9,7 +9,8 @@
     >
       <image 
         class="tab-icon" 
-        :src="currentTab === item.name ? item.activeIcon : item.icon"
+        :class="{ 'icon-active': currentTab === item.name }"
+        :src="item.icon"
         mode="aspectFit"
       />
       <text class="tab-text">{{ item.label }}</text>
@@ -37,21 +38,18 @@ const tabList = computed(() => [
     name: 'home',
     label: t('common.page.home'),
     icon: '/static/pages/tabbar/home.png',
-    activeIcon: '/static/pages/tabbar/home-active.png',
     url: '/pages/home/index'
   },
   {
     name: 'scan',
     label: t('common.page.scan'),
     icon: '/static/pages/tabbar/scan.png',
-    activeIcon: '/static/pages/tabbar/scan-active.png',
     url: '/pages/scan/index'
   },
   {
     name: 'mine',
     label: t('common.page.mine'),
     icon: '/static/pages/tabbar/mine.png',
-    activeIcon: '/static/pages/tabbar/mine-active.png',
     url: '/pages/my/index'
   }
 ])
@@ -93,7 +91,12 @@ const handleTabClick = (item) => {
       width: 48rpx;
       height: 48rpx;
       margin-bottom: 4rpx;
-      transition: transform 0.3s ease;
+      transition: all 0.3s ease;
+      
+      &.icon-active {
+        filter: brightness(0) saturate(100%) invert(68%) sepia(48%) saturate(1234%) hue-rotate(134deg) brightness(91%) contrast(92%);
+        transform: scale(1.1);
+      }
     }
     
     .tab-text {
@@ -103,10 +106,6 @@ const handleTabClick = (item) => {
     }
     
     &.active {
-      .tab-icon {
-        transform: scale(1.1);
-      }
-      
       .tab-text {
         color: #1ec9c9;
         font-weight: 500;

+ 20 - 4
locales/index.js

@@ -5,10 +5,18 @@ import commonEnUS from './common/en_US'
 // 导入页面模块
 import loginZhCN from './pages/login/zh_CN'
 import loginEnUS from './pages/login/en_US'
+import homeZhCN from './pages/home/zh_CN'
+import homeEnUS from './pages/home/en_US'
+import scanZhCN from './pages/scan/zh_CN'
+import scanEnUS from './pages/scan/en_US'
+import myZhCN from './pages/my/zh_CN'
+import myEnUS from './pages/my/en_US'
+import aggreementZhCN from './pages/my/aggreement/zh_CN'
+import aggreementEnUS from './pages/my/aggreement/en_US'
 
 // 导入pages-content模块
-import myZhCN from './pages-content/my/zh_CN'
-import myEnUS from './pages-content/my/en_US'
+import myContentZhCN from './pages-content/my/zh_CN'
+import myContentEnUS from './pages-content/my/en_US'
 
 // 导入组件模块
 import languageSwitcherZhCN from './components/languageSwitcher/zh_CN'
@@ -19,8 +27,12 @@ export const messages = {
   'zh-CN': {
     common: commonZhCN,
     login: loginZhCN,
+    home: homeZhCN,
+    scan: scanZhCN,
+    my: myZhCN,
+    aggreement: aggreementZhCN,
     pagesContent: {
-      my: myZhCN
+      my: myContentZhCN
     },
     components: {
       languageSwitcher: languageSwitcherZhCN
@@ -29,8 +41,12 @@ export const messages = {
   'en-US': {
     common: commonEnUS,
     login: loginEnUS,
+    home: homeEnUS,
+    scan: scanEnUS,
+    my: myEnUS,
+    aggreement: aggreementEnUS,
     pagesContent: {
-      my: myEnUS
+      my: myContentEnUS
     },
     components: {
       languageSwitcher: languageSwitcherEnUS

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

@@ -0,0 +1,10 @@
+export default {
+  title: 'Intelligent eTMF Mini Program',
+  searchPlaceholder: 'Enter document name to search',
+  searchKeywordEmpty: 'Please enter search keyword',
+  intelligentScan: 'Intelligent Scan',
+  scanDesc: 'Quickly scan and upload documents for submission',
+  recentDocuments: 'Recent Documents',
+  viewMore: 'View More',
+  pages: 'pages'
+}

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

@@ -0,0 +1,10 @@
+export default {
+  title: '智能eTMF小程序',
+  searchPlaceholder: '输入要搜索的文档名称',
+  searchKeywordEmpty: '请输入搜索关键词',
+  intelligentScan: '智能扫描',
+  scanDesc: '快速的扫描上传文档进行递交文档',
+  recentDocuments: '最近文档',
+  viewMore: '查看更多',
+  pages: '页'
+}

+ 11 - 0
locales/pages/my/aggreement/en_US.js

@@ -0,0 +1,11 @@
+export default {
+  title: 'User Agreement',
+  userAgreement: 'User Agreement',
+  userAgreementDesc: 'Learn about the terms of using this application',
+  privacyPolicy: 'Privacy Policy',
+  privacyPolicyDesc: 'Learn how we protect your privacy',
+  userAgreementTitle: 'User Agreement',
+  privacyPolicyTitle: 'Privacy Policy',
+  loading: 'Loading...',
+  loadFailed: 'Failed to get agreement content'
+}

+ 11 - 0
locales/pages/my/aggreement/zh_CN.js

@@ -0,0 +1,11 @@
+export default {
+  title: '用户协议',
+  userAgreement: '用户协议',
+  userAgreementDesc: '了解使用本应用的相关条款',
+  privacyPolicy: '隐私政策',
+  privacyPolicyDesc: '了解我们如何保护您的隐私',
+  userAgreementTitle: '用户协议',
+  privacyPolicyTitle: '隐私政策',
+  loading: '加载中...',
+  loadFailed: '获取协议内容失败'
+}

+ 20 - 0
locales/pages/my/en_US.js

@@ -0,0 +1,20 @@
+export default {
+  title: 'eTMF',
+  normalMember: 'Normal Member',
+  memberFeature: 'Member Feature',
+  myTasks: 'My Tasks',
+  toSubmit: 'To Submit',
+  toAudit: 'To Audit',
+  userAgreement: 'User Agreement',
+  languageSwitch: 'Language',
+  aboutUs: 'About Us',
+  logout: 'Logout',
+  logoutConfirm: 'Are you sure you want to logout?',
+  loggingOut: 'Logging out...',
+  logoutSuccess: 'Logged out successfully',
+  languageSwitchSuccess: 'Language switched successfully',
+  defaultNickname: 'No nickname set',
+  confirm: 'Confirm',
+  cancel: 'Cancel',
+  tip: 'Tip'
+}

+ 20 - 0
locales/pages/my/zh_CN.js

@@ -0,0 +1,20 @@
+export default {
+  title: 'eTMF',
+  normalMember: '普通会员',
+  memberFeature: '会员功能',
+  myTasks: '我的任务',
+  toSubmit: '待递交',
+  toAudit: '待审核',
+  userAgreement: '用户协议',
+  languageSwitch: '语言切换',
+  aboutUs: '关于我们',
+  logout: '退出登录',
+  logoutConfirm: '确定要退出登录吗?',
+  loggingOut: '退出中...',
+  logoutSuccess: '已退出登录',
+  languageSwitchSuccess: '语言切换成功',
+  defaultNickname: '未设置昵称',
+  confirm: '确定',
+  cancel: '取消',
+  tip: '提示'
+}

+ 16 - 0
locales/pages/scan/en_US.js

@@ -0,0 +1,16 @@
+export default {
+  title: 'Mini Program Scan',
+  cameraError: 'Camera failed to start',
+  singlePage: 'Single',
+  multiplePage: 'Multiple',
+  switchToSingle: 'Switched to single page mode',
+  switchToMultiple: 'Switched to multiple page mode',
+  captureSuccess: 'Photo taken successfully',
+  captureFailed: 'Failed to take photo',
+  processing: 'Processing...',
+  importImage: 'Import Image',
+  capture: 'Capture',
+  importDocument: 'Import Document',
+  imageImportSuccess: 'Image imported successfully',
+  documentImportSuccess: 'Document imported successfully'
+}

+ 16 - 0
locales/pages/scan/zh_CN.js

@@ -0,0 +1,16 @@
+export default {
+  title: '小程序扫描',
+  cameraError: '相机启动失败',
+  singlePage: '单页',
+  multiplePage: '多页',
+  switchToSingle: '已切换到单页模式',
+  switchToMultiple: '已切换到多页模式',
+  captureSuccess: '拍照成功',
+  captureFailed: '拍照失败',
+  processing: '处理中...',
+  importImage: '导入图片',
+  capture: '拍照',
+  importDocument: '导入文档',
+  imageImportSuccess: '图片导入成功',
+  documentImportSuccess: '文档导入成功'
+}

+ 218 - 87
pages/home/index.vue

@@ -1,20 +1,26 @@
 <template>
   <view class="home-page">
-    <!-- 顶部渐变背景区域 -->
+    <!-- 顶部渐变背景区域 - 固定 -->
     <view class="header-bg" :style="{ paddingTop: statusBarHeight + 'px' }">
       <view class="header-content">
-        <text class="header-title">智能eTMF小程序</text>
+        <text class="header-title">{{ t('home.title') }}</text>
       </view>
     </view>
     
-    <!-- 页面内容 -->
-    <view class="page-content">
+    <!-- 页面内容 - 可滚动 -->
+    <scroll-view 
+      scroll-y 
+      class="page-scroll"
+      :style="{ paddingTop: (statusBarHeight + 60) + 'px' }"
+      @scrolltolower="handleLoadMore"
+    >
+      <view class="page-content">
       <!-- 搜索框 -->
       <view class="search-box">
         <image class="search-icon" src="/static/pages/home/search.png" mode="aspectFit" />
         <input 
           class="search-input" 
-          placeholder="输入要搜索的文档名称"
+          :placeholder="t('home.searchPlaceholder')"
           placeholder-class="search-placeholder"
           v-model="searchKeyword"
           @confirm="handleSearch"
@@ -44,17 +50,17 @@
           <image class="scan-icon" src="/static/pages/home/scan-icon.png" mode="aspectFit" />
         </view>
         <view class="scan-info">
-          <text class="scan-title">智能扫描</text>
-          <text class="scan-desc">快速的扫描上传文档进行递交文档</text>
+          <text class="scan-title">{{ t('home.intelligentScan') }}</text>
+          <text class="scan-desc">{{ t('home.scanDesc') }}</text>
         </view>
       </view>
       
       <!-- 最近文档 -->
       <view class="recent-section">
         <view class="section-header">
-          <text class="section-title">最近文档</text>
+          <text class="section-title">{{ t('home.recentDocuments') }}</text>
           <view class="more-btn" @click="handleViewMore">
-            <text class="more-text">查看更多</text>
+            <text class="more-text">{{ t('home.viewMore') }}</text>
             <text class="more-arrow">›</text>
           </view>
         </view>
@@ -62,19 +68,35 @@
         <view class="document-list">
           <view 
             v-for="(doc, index) in recentDocuments" 
-            :key="index"
+            :key="doc.id"
             class="document-item"
             @click="handleDocumentClick(doc)"
           >
-            <image class="doc-thumbnail" :src="doc.thumbnail" mode="aspectFill" />
+            <image class="doc-thumbnail" :src="doc.thumbnail || '/static/pages/home/doc-thumb.png'" mode="aspectFill" />
             <view class="doc-info">
               <text class="doc-name">{{ doc.name }}</text>
-              <text class="doc-meta">{{ doc.date }} | 共{{ doc.pages }}页</text>
+              <text class="doc-meta">{{ doc.createTime }}</text>
             </view>
           </view>
+          
+          <!-- 加载状态 -->
+          <view v-if="loading" class="loading-more">
+            <text class="loading-text">{{ t('common.message.loading') }}</text>
+          </view>
+          
+          <!-- 没有更多数据 -->
+          <view v-if="!hasMore && recentDocuments.length > 0" class="no-more">
+            <text class="no-more-text">没有更多了</text>
+          </view>
+          
+          <!-- 空状态 -->
+          <view v-if="!loading && recentDocuments.length === 0" class="empty-state">
+            <text class="empty-text">{{ t('common.message.noData') }}</text>
+          </view>
         </view>
       </view>
     </view>
+    </scroll-view>
     
     <!-- 底部导航栏 -->
     <TabBar current-tab="home" />
@@ -85,6 +107,7 @@
 import { ref, onMounted } from 'vue'
 import { useI18n } from 'vue-i18n'
 import TabBar from '@/components/TabBar/index.vue'
+import { getCarouselList, getRecentDocuments } from '@/apis/setting'
 
 const { t } = useI18n()
 
@@ -95,75 +118,141 @@ const statusBarHeight = ref(0)
 const searchKeyword = ref('')
 
 // 轮播图数据
-const bannerList = ref([
-  {
-    image: '/static/pages/home/banner1.png'
-  },
-  {
-    image: '/static/pages/home/banner2.png'
-  }
-])
+const bannerList = ref([])
 
 // 最近文档数据
-const recentDocuments = ref([
-  {
-    id: 1,
-    name: 'HFSLKD_SKD_测试文档名称1',
-    date: '2025/11/12 11:12',
-    pages: 5,
-    thumbnail: '/static/pages/home/doc-thumb.png'
-  },
-  {
-    id: 2,
-    name: 'HFSLKD_SKD_测试文档名称1',
-    date: '2025/11/12 11:12',
-    pages: 5,
-    thumbnail: '/static/pages/home/doc-thumb.png'
-  },
-  {
-    id: 3,
-    name: 'HFSLKD_SKD_测试文档名称1',
-    date: '2025/11/12 11:12',
-    pages: 5,
-    thumbnail: '/static/pages/home/doc-thumb.png'
-  },
-  {
-    id: 4,
-    name: 'HFSLKD_SKD_测试文档名称1',
-    date: '2025/11/12 11:12',
-    pages: 5,
-    thumbnail: '/static/pages/home/doc-thumb.png'
-  },
-  {
-    id: 5,
-    name: 'HFSLKD_SKD_测试文档名称1',
-    date: '2025/11/12 11:12',
-    pages: 5,
-    thumbnail: '/static/pages/home/doc-thumb.png'
-  }
-])
+const recentDocuments = ref([])
+
+// 分页参数
+const pageNum = ref(1)
+const pageSize = ref(10)
+const total = ref(0)
+const loading = ref(false)
+const hasMore = ref(true)
 
 onMounted(() => {
   // 获取系统信息
   const windowInfo = uni.getWindowInfo()
   statusBarHeight.value = windowInfo.statusBarHeight || 0
+  
+  // 获取轮播图数据
+  fetchCarouselList()
+  
+  // 获取最近文档列表
+  fetchRecentDocuments()
 })
 
+// 获取轮播图列表
+const fetchCarouselList = async () => {
+  try {
+    const response = await getCarouselList()
+    
+    if (response && response.code === 200 && response.data) {
+      // 按 sort 字段排序
+      const sortedData = response.data.sort((a, b) => a.sort - b.sort)
+      
+      // 转换为轮播图所需的格式
+      bannerList.value = sortedData.map(item => ({
+        id: item.id,
+        image: item.ossidUrl,
+        note: item.note
+      }))
+      
+      console.log('轮播图加载成功:', bannerList.value.length, '张')
+    }
+  } catch (error) {
+    console.error('获取轮播图失败:', error)
+    // 失败时使用默认图片
+    bannerList.value = [
+      {
+        id: 1,
+        image: '/static/pages/home/banner1.png',
+        note: '默认轮播图1'
+      },
+      {
+        id: 2,
+        image: '/static/pages/home/banner2.png',
+        note: '默认轮播图2'
+      }
+    ]
+  }
+}
+
+// 获取最近文档列表
+const fetchRecentDocuments = async (isLoadMore = false) => {
+  if (loading.value) return
+  
+  try {
+    loading.value = true
+    
+    // 构建请求参数,只在有搜索关键词时才添加 name 参数
+    const params = {
+      pageNum: pageNum.value,
+      pageSize: pageSize.value
+    }
+    
+    // 只有当搜索关键词不为空时才添加 name 参数
+    if (searchKeyword.value && searchKeyword.value.trim()) {
+      params.name = searchKeyword.value.trim()
+    }
+    
+    console.log('请求参数:', params)
+    
+    const response = await getRecentDocuments(params)
+    
+    if (response && response.code === 200) {
+      const { rows, total: totalCount } = response
+      
+      total.value = totalCount
+      
+      if (isLoadMore) {
+        // 加载更多,追加数据
+        recentDocuments.value = [...recentDocuments.value, ...rows]
+      } else {
+        // 首次加载或搜索,替换数据
+        recentDocuments.value = rows
+      }
+      
+      // 判断是否还有更多数据
+      hasMore.value = recentDocuments.value.length < total.value
+      
+      console.log('最近文档加载成功:', rows.length, '条')
+    }
+  } catch (error) {
+    console.error('获取最近文档失败:', error)
+    
+    if (!isLoadMore) {
+      recentDocuments.value = []
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+// 加载更多
+const handleLoadMore = () => {
+  if (!hasMore.value || loading.value) return
+  
+  pageNum.value++
+  fetchRecentDocuments(true)
+}
+
 // 搜索
 const handleSearch = () => {
   if (!searchKeyword.value.trim()) {
     uni.showToast({
-      title: '请输入搜索关键词',
+      title: t('home.searchKeywordEmpty'),
       icon: 'none'
     })
     return
   }
   
-  uni.showToast({
-    title: `搜索: ${searchKeyword.value}`,
-    icon: 'none'
-  })
-  // TODO: 实现搜索功能
+  // 重置分页
+  pageNum.value = 1
+  hasMore.value = true
+  
+  // 重新加载数据
+  fetchRecentDocuments()
 }
 
 // 跳转到扫描页面
@@ -195,15 +284,19 @@ const handleDocumentClick = (doc) => {
 <style lang="scss" scoped>
 .home-page {
   width: 100%;
-  min-height: 100vh;
-  display: flex;
-  flex-direction: column;
+  height: 100vh;
   background-color: #f5f5f5;
-  padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
+  position: relative;
+  overflow: hidden;
   
-  // 顶部渐变背景
+  // 顶部渐变背景 - 固定
   .header-bg {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
     background: linear-gradient(180deg, #1ec9c9 0%, #1eb8b8 100%);
+    z-index: 100;
     
     .header-content {
       padding: 0 24rpx 16rpx;
@@ -219,14 +312,18 @@ const handleDocumentClick = (doc) => {
     }
   }
   
-  // 页面内容
-  .page-content {
-    flex: 1;
-    padding: 0 24rpx 24rpx;
-    margin-top: -12rpx;
+  // 可滚动区域
+  .page-scroll {
+    width: 100%;
+    height: 100%;
     
-    // 搜索框
-    .search-box {
+    // 页面内容
+    .page-content {
+      padding: 0 24rpx 24rpx;
+      padding-bottom: calc(124rpx + env(safe-area-inset-bottom));
+      
+      // 搜索框
+      .search-box {
       background: #ffffff;
       border-radius: 16rpx;
       padding: 20rpx 24rpx;
@@ -249,11 +346,11 @@ const handleDocumentClick = (doc) => {
       
       .search-placeholder {
         color: #999999;
+        }
       }
-    }
-    
-    // 轮播图
-    .banner-section {
+      
+      // 轮播图
+      .banner-section {
       margin-bottom: 24rpx;
       
       .banner-swiper {
@@ -266,11 +363,11 @@ const handleDocumentClick = (doc) => {
           width: 100%;
           height: 100%;
         }
+        }
       }
-    }
-    
-    // 智能扫描卡片
-    .scan-card {
+      
+      // 智能扫描卡片
+      .scan-card {
       background: #ffffff;
       border-radius: 16rpx;
       padding: 32rpx 24rpx;
@@ -316,11 +413,11 @@ const handleDocumentClick = (doc) => {
           color: #999999;
           line-height: 1.4;
         }
+        }
       }
-    }
-    
-    // 最近文档
-    .recent-section {
+      
+      // 最近文档
+      .recent-section {
       .section-header {
         display: flex;
         align-items: center;
@@ -393,6 +490,40 @@ const handleDocumentClick = (doc) => {
               color: #999999;
             }
           }
+          }
+        }
+      }
+      
+      // 加载更多
+      .loading-more {
+        padding: 32rpx;
+        text-align: center;
+        
+        .loading-text {
+          font-size: 24rpx;
+          color: #999999;
+        }
+      }
+      
+      // 没有更多
+      .no-more {
+        padding: 32rpx;
+        text-align: center;
+        
+        .no-more-text {
+          font-size: 24rpx;
+          color: #999999;
+        }
+      }
+      
+      // 空状态
+      .empty-state {
+        padding: 120rpx 32rpx;
+        text-align: center;
+        
+        .empty-text {
+          font-size: 28rpx;
+          color: #999999;
         }
       }
     }

+ 1 - 1
pages/login/login.vue

@@ -404,7 +404,7 @@ const handleLogin = async () => {
   width: 40rpx;
   height: 40rpx;
   margin-right: 16rpx;
-  opacity: 0.6;
+  filter: brightness(0) saturate(100%) invert(68%) sepia(48%) saturate(1234%) hue-rotate(134deg) brightness(91%) contrast(92%);
 }
 
 .input-field {

+ 9 - 5
pages/my/aggreement/index.vue

@@ -6,7 +6,7 @@
         <view class="back-btn" @click="handleBack">
           <text class="back-icon">‹</text>
         </view>
-        <text class="header-title">用户协议</text>
+        <text class="header-title">{{ t('aggreement.title') }}</text>
         <view class="placeholder"></view>
       </view>
     </view>
@@ -20,8 +20,8 @@
           <view class="item-left">
             <image class="item-icon" src="/static/pages/my/agreement/user.png" mode="aspectFit" />
             <view class="item-info">
-              <text class="item-title">用户协议</text>
-              <text class="item-desc">了解使用本应用的相关条款</text>
+              <text class="item-title">{{ t('aggreement.userAgreement') }}</text>
+              <text class="item-desc">{{ t('aggreement.userAgreementDesc') }}</text>
             </view>
           </view>
           <text class="arrow">›</text>
@@ -32,8 +32,8 @@
           <view class="item-left">
             <image class="item-icon" src="/static/pages/my/agreement/privacy.png" mode="aspectFit" />
             <view class="item-info">
-              <text class="item-title">隐私政策</text>
-              <text class="item-desc">了解我们如何保护您的隐私</text>
+              <text class="item-title">{{ t('aggreement.privacyPolicy') }}</text>
+              <text class="item-desc">{{ t('aggreement.privacyPolicyDesc') }}</text>
             </view>
           </view>
           <text class="arrow">›</text>
@@ -45,6 +45,9 @@
 
 <script setup>
 import { ref, onMounted } from 'vue'
+import { useI18n } from 'vue-i18n'
+
+const { t } = useI18n()
 
 // 状态栏高度
 const statusBarHeight = ref(0)
@@ -156,6 +159,7 @@ const handlePrivacyPolicy = () => {
             width: 40rpx;
             height: 40rpx;
             margin-right: 24rpx;
+            filter: brightness(0) saturate(100%) invert(68%) sepia(48%) saturate(1234%) hue-rotate(134deg) brightness(91%) contrast(92%);
           }
           
           .item-info {

+ 111 - 75
pages/my/index.vue

@@ -4,15 +4,20 @@
   
   <!-- 我的主页 -->
   <view v-else class="my-page">
-    <!-- 顶部渐变背景区域 -->
+    <!-- 顶部渐变背景区域 - 固定 -->
     <view class="header-bg" :style="{ paddingTop: statusBarHeight + 'px' }">
-      <text class="header-title">eTMF</text>
+      <text class="header-title">{{ t('my.title') }}</text>
     </view>
     
-    <!-- 页面内容 -->
-    <view class="page-content">
+    <!-- 页面内容 - 可滚动 -->
+    <scroll-view 
+      scroll-y 
+      class="page-scroll"
+      :style="{ paddingTop: (statusBarHeight + 60) + 'px' }"
+    >
+      <view class="page-content">
       <!-- 用户信息卡片 -->
-      <view class="user-card">
+      <view class="user-card" @click="handleUserCardClick">
         <view class="user-info">
           <image 
             class="avatar" 
@@ -25,8 +30,8 @@
             <text class="phone">{{ displayPhone }}</text>
           </view>
         </view>
-        <view class="vip-badge" @click="handleVipClick">
-          <text class="vip-text">普通会员</text>
+        <view class="vip-badge" @click.stop="handleVipClick">
+          <text class="vip-text">{{ t('my.normalMember') }}</text>
           <text class="arrow">›</text>
         </view>
       </view>
@@ -34,7 +39,7 @@
       <!-- 我的任务 -->
       <view class="task-section">
         <view class="section-header">
-          <text class="section-title">我的任务</text>
+          <text class="section-title">{{ t('my.myTasks') }}</text>
           <text class="arrow">›</text>
         </view>
         <view class="task-grid">
@@ -43,26 +48,14 @@
               <image class="task-icon" src="/static/pages/my/task-pending.png" mode="aspectFit" />
               <view v-if="taskCounts.pending > 0" class="task-badge">{{ taskCounts.pending }}</view>
             </view>
-            <text class="task-label">待传递</text>
-          </view>
-          <view class="task-item" @click="handleTaskClick('delivered')">
-            <view class="task-icon-wrapper">
-              <image class="task-icon" src="/static/pages/my/task-delivered.png" mode="aspectFit" />
-            </view>
-            <text class="task-label">已传递</text>
+            <text class="task-label">{{ t('my.toSubmit') }}</text>
           </view>
           <view class="task-item" @click="handleTaskClick('reviewing')">
             <view class="task-icon-wrapper">
               <image class="task-icon" src="/static/pages/my/task-reviewing.png" mode="aspectFit" />
               <view v-if="taskCounts.reviewing > 0" class="task-badge">{{ taskCounts.reviewing }}</view>
             </view>
-            <text class="task-label">待审核</text>
-          </view>
-          <view class="task-item" @click="handleTaskClick('approved')">
-            <view class="task-icon-wrapper">
-              <image class="task-icon" src="/static/pages/my/task-approved.png" mode="aspectFit" />
-            </view>
-            <text class="task-label">已审核</text>
+            <text class="task-label">{{ t('my.toAudit') }}</text>
           </view>
         </view>
       </view>
@@ -72,7 +65,7 @@
         <view class="menu-item" @click="handleProtocol">
           <view class="menu-left">
             <image class="menu-icon" src="/static/pages/my/icon-protocol.png" mode="aspectFit" />
-            <text class="menu-label">用户协议</text>
+            <text class="menu-label">{{ t('my.userAgreement') }}</text>
           </view>
           <text class="arrow">›</text>
         </view>
@@ -80,7 +73,7 @@
         <view class="menu-item">
           <view class="menu-left">
             <image class="menu-icon" src="/static/pages/my/icon-language.png" mode="aspectFit" />
-            <text class="menu-label">语言切换</text>
+            <text class="menu-label">{{ t('my.languageSwitch') }}</text>
           </view>
           <view class="language-switcher" @click="handleLanguageChange">
             <text class="language-text" :class="{ active: locale === 'zh-CN' }">中</text>
@@ -92,7 +85,7 @@
         <view class="menu-item" @click="handleAbout">
           <view class="menu-left">
             <image class="menu-icon" src="/static/pages/my/icon-about.png" mode="aspectFit" />
-            <text class="menu-label">关于我们</text>
+            <text class="menu-label">{{ t('my.aboutUs') }}</text>
           </view>
           <text class="arrow">›</text>
         </view>
@@ -100,9 +93,10 @@
       
       <!-- 退出登录按钮 -->
       <view class="logout-section">
-        <button class="logout-btn" @click="handleLogout">退出登录</button>
+        <button class="logout-btn" @click="handleLogout">{{ t('my.logout') }}</button>
       </view>
     </view>
+    </scroll-view>
     
     <!-- 底部导航栏 -->
     <TabBar current-tab="mine" />
@@ -115,7 +109,7 @@ 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, logout as logoutAPI } from '@/apis/auth'
+import { getUserInfo as getUserInfoAPI, logout as logoutAPI, getTaskCount } from '@/apis/auth'
 import TabBar from '@/components/TabBar/index.vue'
 
 const { t, locale } = useI18n()
@@ -137,15 +131,13 @@ const userInfo = ref({
 
 // 任务数量
 const taskCounts = ref({
-  pending: 6,
-  delivered: 0,
-  reviewing: 8,
-  approved: 0
+  pending: 0,
+  reviewing: 0
 })
 
 // 显示的昵称
 const displayNickname = computed(() => {
-  return userInfo.value.nickname || '未设置昵称'
+  return userInfo.value.nickname || t('my.defaultNickname')
 })
 
 // 加密手机号
@@ -176,6 +168,9 @@ onMounted(() => {
   
   // 获取用户信息
   fetchUserInfo()
+  
+  // 获取任务数量
+  fetchTaskCount()
 })
 
 // 头像加载失败处理
@@ -211,15 +206,41 @@ const fetchUserInfo = async () => {
   }
 }
 
+// 获取任务数量
+const fetchTaskCount = async () => {
+  try {
+    const response = await getTaskCount()
+    
+    if (response && response.code === 200 && response.data) {
+      taskCounts.value = {
+        pending: response.data.toSubmit || 0,
+        reviewing: response.data.toAudit || 0
+      }
+      
+      console.log('任务数量:', taskCounts.value)
+    }
+  } catch (error) {
+    console.error('获取任务数量失败:', error)
+    // 失败时保持默认值 0
+  }
+}
+
 // 从基本信息返回主页
 const handleBackToMain = () => {
   showBasicInfo.value = false
 }
 
+// 点击用户卡片
+const handleUserCardClick = () => {
+  uni.navigateTo({
+    url: '/pages/my/info/index'
+  })
+}
+
 // VIP点击
 const handleVipClick = () => {
   uni.showToast({
-    title: '会员功能',
+    title: t('my.memberFeature'),
     icon: 'none'
   })
 }
@@ -245,7 +266,7 @@ const handleLanguageChange = () => {
   
   setTimeout(() => {
     uni.showToast({
-      title: '语言切换成功',
+      title: t('my.languageSwitchSuccess'),
       icon: 'success',
       duration: 1500
     })
@@ -255,7 +276,7 @@ const handleLanguageChange = () => {
 // 关于我们
 const handleAbout = () => {
   uni.showToast({
-    title: '关于我们',
+    title: t('my.aboutUs'),
     icon: 'none'
   })
 }
@@ -263,15 +284,15 @@ const handleAbout = () => {
 // 退出登录
 const handleLogout = () => {
   uni.showModal({
-    title: '提示',
-    content: '确定要退出登录吗?',
-    confirmText: '确定',
-    cancelText: '取消',
+    title: t('my.tip'),
+    content: t('my.logoutConfirm'),
+    confirmText: t('my.confirm'),
+    cancelText: t('my.cancel'),
     success: async (res) => {
       if (res.confirm) {
         try {
           uni.showLoading({
-            title: '退出中...',
+            title: t('my.loggingOut'),
             mask: true
           })
           
@@ -281,7 +302,7 @@ const handleLogout = () => {
           uni.hideLoading()
           
           uni.showToast({
-            title: '已退出登录',
+            title: t('my.logoutSuccess'),
             icon: 'success',
             duration: 1500
           })
@@ -299,7 +320,7 @@ const handleLogout = () => {
           userStore.logout()
           
           uni.showToast({
-            title: '已退出登录',
+            title: t('my.logoutSuccess'),
             icon: 'success',
             duration: 1500
           })
@@ -319,16 +340,21 @@ const handleLogout = () => {
 <style lang="scss" scoped>
 .my-page {
   width: 100%;
-  min-height: 100vh;
+  height: 100vh;
   background-color: #f5f5f5;
-  padding-bottom: calc(100rpx + env(safe-area-inset-bottom));
+  position: relative;
+  overflow: hidden;
   
-  // 顶部渐变背景
+  // 顶部渐变背景 - 固定
   .header-bg {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
     background: linear-gradient(180deg, #1ec9c9 0%, #1eb8b8 100%);
-    height: 200rpx;
+    z-index: 100;
     display: flex;
-    align-items: center;
+    align-items: flex-start;
     justify-content: center;
     
     .header-title {
@@ -336,24 +362,34 @@ const handleLogout = () => {
       font-weight: 600;
       color: #ffffff;
       letter-spacing: 4rpx;
+      padding: 16rpx 0 24rpx;
     }
   }
   
-  // 页面内容
-  .page-content {
-    padding: 0 24rpx;
-    margin-top: -80rpx;
+  // 可滚动区域
+  .page-scroll {
+    width: 100%;
+    height: 100%;
     
-    // 用户信息卡片
-    .user-card {
-      background: #ffffff;
-      border-radius: 16rpx;
-      padding: 32rpx 24rpx;
-      margin-bottom: 24rpx;
-      display: flex;
-      align-items: center;
-      justify-content: space-between;
-      box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
+    // 页面内容
+    .page-content {
+      padding: 0 24rpx 24rpx;
+      padding-bottom: calc(124rpx + env(safe-area-inset-bottom));
+    
+      // 用户信息卡片
+      .user-card {
+        background: #ffffff;
+        border-radius: 16rpx;
+        padding: 32rpx 24rpx;
+        margin-bottom: 24rpx;
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
+        
+        &:active {
+          background-color: #f8f8f8;
+        }
       
       .user-info {
         display: flex;
@@ -405,11 +441,11 @@ const handleLogout = () => {
           color: #ffffff;
           font-weight: 300;
         }
+        }
       }
-    }
-    
-    // 我的任务
-    .task-section {
+      
+      // 我的任务
+      .task-section {
       background: #ffffff;
       border-radius: 16rpx;
       padding: 32rpx 24rpx;
@@ -480,11 +516,11 @@ const handleLogout = () => {
             color: #666666;
           }
         }
+        }
       }
-    }
-    
-    // 功能列表
-    .menu-list {
+      
+      // 功能列表
+      .menu-list {
       background: #ffffff;
       border-radius: 16rpx;
       overflow: hidden;
@@ -562,11 +598,11 @@ const handleLogout = () => {
             font-weight: 300;
           }
         }
+        }
       }
-    }
-    
-    // 退出登录
-    .logout-section {
+      
+      // 退出登录
+      .logout-section {
       padding: 32rpx 0 60rpx;
       
       .logout-btn {
@@ -587,9 +623,9 @@ const handleLogout = () => {
         
         &::after {
           border: none;
+          }
         }
       }
     }
   }
-}
-</style>
+}</style>

+ 8 - 1
pages/my/info/index.vue

@@ -167,7 +167,14 @@ const fetchBasicInfo = async () => {
 
 // 返回
 const handleBack = () => {
-  emit('back')
+  uni.navigateBack({
+    fail: () => {
+      // 如果返回失败(比如没有上一页),则跳转到我的页面
+      uni.reLaunch({
+        url: '/pages/my/index'
+      })
+    }
+  })
 }
 </script>
 

+ 373 - 63
pages/scan/index.vue

@@ -19,15 +19,35 @@
         class="camera"
         @error="handleCameraError"
       >
-        <!-- 扫描框 -->
-        <view class="scan-frame">
-          <view class="corner corner-tl"></view>
-          <view class="corner corner-tr"></view>
-          <view class="corner corner-bl"></view>
-          <view class="corner corner-br"></view>
-        </view>
+        <!-- 检测边框覆盖层 -->
+        <canvas 
+          v-if="detectedBorders.length > 0"
+          canvas-id="borderCanvas" 
+          class="border-canvas"
+          :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
+        ></canvas>
       </camera>
       
+      <!-- 模式切换按钮 - 在底部操作栏上方 -->
+      <view class="mode-switch-wrapper">
+        <view class="mode-switch">
+          <view 
+            class="mode-btn" 
+            :class="{ active: scanMode === 'single' }"
+            @click="switchToSingle"
+          >
+            <text class="mode-text">单页</text>
+          </view>
+          <view 
+            class="mode-btn" 
+            :class="{ active: scanMode === 'multiple' }"
+            @click="switchToMultiple"
+          >
+            <text class="mode-text">多页</text>
+          </view>
+        </view>
+      </view>
+      
       <!-- 底部操作按钮 -->
       <view class="action-buttons">
         <view class="action-btn" @click="handleSelectImage">
@@ -49,7 +69,7 @@
 </template>
 
 <script setup>
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, onUnmounted } from 'vue'
 import { useI18n } from 'vue-i18n'
 
 const { t } = useI18n()
@@ -57,20 +77,92 @@ const { t } = useI18n()
 // 状态栏高度
 const statusBarHeight = ref(0)
 
+// 扫描模式:single-单页,multiple-多页
+const scanMode = ref('single')
+
+// 检测到的边框
+const detectedBorders = ref([])
+
+// Canvas尺寸
+const canvasWidth = ref(0)
+const canvasHeight = ref(0)
+
+// 相机上下文
+let cameraContext = null
+
+// Canvas上下文
+let canvasContext = null
+
+// 扫描定时器
+let scanTimer = null
+
 onMounted(() => {
-  // 获取系统信息 - 使用新API
+  // 获取系统信息
   const windowInfo = uni.getWindowInfo()
   statusBarHeight.value = windowInfo.statusBarHeight || 0
+  
+  // 获取屏幕尺寸
+  const systemInfo = uni.getSystemInfoSync()
+  canvasWidth.value = systemInfo.windowWidth
+  canvasHeight.value = systemInfo.windowHeight - statusBarHeight.value - 88 - 200 // 减去头部和底部按钮高度
+  
+  // 初始化相机上下文
+  cameraContext = uni.createCameraContext()
+  
+  // 初始化Canvas上下文
+  canvasContext = uni.createCanvasContext('borderCanvas')
+  
+  // 开始实物检测
+  startDetection()
+})
+
+onUnmounted(() => {
+  // 停止检测
+  stopDetection()
 })
 
 // 返回上一页
 const handleBack = () => {
-  // 返回到home页面
   uni.reLaunch({
     url: '/pages/home/index'
   })
 }
 
+// 切换扫描模式
+const toggleScanMode = () => {
+  scanMode.value = scanMode.value === 'single' ? 'multiple' : 'single'
+  
+  uni.showToast({
+    title: scanMode.value === 'single' ? '已切换到单页模式' : '已切换到多页模式',
+    icon: 'none',
+    duration: 1500
+  })
+}
+
+// 切换到单页模式
+const switchToSingle = () => {
+  if (scanMode.value === 'single') return
+  scanMode.value = 'single'
+  
+  uni.showToast({
+    title: '已切换到单页模式',
+    icon: 'none',
+    duration: 1500
+  })
+}
+
+// 切换到多页模式
+const switchToMultiple = () => {
+  if (scanMode.value === 'multiple') return
+  scanMode.value = 'multiple'
+  
+  uni.showToast({
+    title: '已切换到多页模式',
+    icon: 'none',
+    duration: 1500
+  })
+}
+
 // 相机错误处理
 const handleCameraError = (e) => {
   console.error('相机错误:', e)
@@ -80,18 +172,199 @@ const handleCameraError = (e) => {
   })
 }
 
+// 开始实物检测
+const startDetection = () => {
+  // 每隔一段时间进行一次实物检测
+  scanTimer = setInterval(() => {
+    detectObject()
+  }, 300) // 每300ms检测一次
+}
+
+// 停止检测
+const stopDetection = () => {
+  if (scanTimer) {
+    clearInterval(scanTimer)
+    scanTimer = null
+  }
+}
+
+// 检测实物
+const detectObject = () => {
+  if (!cameraContext) return
+  
+  // 拍摄临时照片用于检测
+  cameraContext.takePhoto({
+    quality: 'low', // 使用低质量以提高检测速度
+    success: (res) => {
+      // 分析图片检测实物
+      analyzeImage(res.tempImagePath)
+    },
+    fail: () => {
+      // 静默失败,继续下一次检测
+    }
+  })
+}
+
+// 分析图片中的实物
+const analyzeImage = (imagePath) => {
+  // TODO: 调用后端API或图像识别服务检测实物
+  // 这里需要接入实际的物体检测API,例如:
+  // - 百度AI文字识别OCR
+  // - 腾讯云文档识别
+  // - 阿里云文档检测
+  
+  // 模拟检测结果 - 检测到1-3个实物
+  const hasObject = Math.random() > 0.3 // 70%概率检测到实物
+  
+  if (hasObject) {
+    const objectCount = Math.floor(Math.random() * 3) + 1 // 1-3个实物
+    const borders = []
+    
+    for (let i = 0; i < objectCount; i++) {
+      // 生成随机位置的实物边框(模拟检测结果)
+      const x = Math.random() * (canvasWidth.value - 200) + 50
+      const y = Math.random() * (canvasHeight.value - 300) + 50
+      const width = Math.random() * 200 + 150
+      const height = Math.random() * 250 + 200
+      
+      borders.push({
+        x,
+        y,
+        width,
+        height,
+        // 四个角点坐标(用于绘制更精确的边框)
+        points: [
+          { x, y }, // 左上
+          { x: x + width, y }, // 右上
+          { x: x + width, y: y + height }, // 右下
+          { x, y: y + height } // 左下
+        ]
+      })
+    }
+    
+    detectedBorders.value = borders
+    
+    // 绘制边框
+    drawBorders(borders)
+    
+    // 自动对焦到第一个检测到的实物
+    if (borders.length > 0) {
+      autoFocus(borders[0])
+    }
+  } else {
+    // 没有检测到实物,清空边框
+    detectedBorders.value = []
+    clearCanvas()
+  }
+}
+
+// 绘制检测到的边框
+const drawBorders = (borders) => {
+  if (!canvasContext || !borders || borders.length === 0) return
+  
+  // 清空画布
+  canvasContext.clearRect(0, 0, canvasWidth.value, canvasHeight.value)
+  
+  // 绘制每个检测到的边框
+  borders.forEach((border, index) => {
+    // 设置边框样式
+    canvasContext.setStrokeStyle('#1ec9c9')
+    canvasContext.setLineWidth(3)
+    canvasContext.setLineDash([10, 5], 0) // 虚线效果
+    
+    // 绘制四边形边框
+    canvasContext.beginPath()
+    canvasContext.moveTo(border.points[0].x, border.points[0].y)
+    border.points.forEach((point, i) => {
+      if (i > 0) {
+        canvasContext.lineTo(point.x, point.y)
+      }
+    })
+    canvasContext.closePath()
+    canvasContext.stroke()
+    
+    // 绘制四个角点
+    border.points.forEach(point => {
+      canvasContext.beginPath()
+      canvasContext.arc(point.x, point.y, 8, 0, 2 * Math.PI)
+      canvasContext.setFillStyle('#1ec9c9')
+      canvasContext.fill()
+      
+      // 角点外圈
+      canvasContext.beginPath()
+      canvasContext.arc(point.x, point.y, 12, 0, 2 * Math.PI)
+      canvasContext.setStrokeStyle('#ffffff')
+      canvasContext.setLineWidth(2)
+      canvasContext.stroke()
+    })
+    
+    // 添加半透明填充
+    canvasContext.setFillStyle('rgba(30, 201, 201, 0.1)')
+    canvasContext.beginPath()
+    canvasContext.moveTo(border.points[0].x, border.points[0].y)
+    border.points.forEach((point, i) => {
+      if (i > 0) {
+        canvasContext.lineTo(point.x, point.y)
+      }
+    })
+    canvasContext.closePath()
+    canvasContext.fill()
+  })
+  
+  // 绘制到屏幕
+  canvasContext.draw()
+}
+
+// 清空画布
+const clearCanvas = () => {
+  if (!canvasContext) return
+  canvasContext.clearRect(0, 0, canvasWidth.value, canvasHeight.value)
+  canvasContext.draw()
+}
+
+// 自动对焦到实物
+const autoFocus = (position) => {
+  if (!cameraContext || !position) return
+  
+  // 计算实物中心点(相对于屏幕的比例)
+  const centerX = (position.x + position.width / 2) / 750
+  const centerY = (position.y + position.height / 2) / 1334
+  
+  // 设置对焦点
+  console.log('自动对焦到实物:', centerX, centerY)
+  
+  // 注意:实际对焦效果取决于设备和平台支持
+}
+
 // 拍照
 const handleCapture = () => {
-  const ctx = uni.createCameraContext()
-  ctx.takePhoto({
+  if (!cameraContext) return
+  
+  // 暂停检测
+  stopDetection()
+  
+  cameraContext.takePhoto({
     quality: 'high',
     success: (res) => {
       console.log('拍照成功:', res.tempImagePath)
-      uni.showToast({
-        title: '拍照成功',
-        icon: 'success'
+      
+      uni.showLoading({
+        title: '处理中...',
+        mask: true
       })
-      // TODO: 处理拍照后的图片
+      
+      // TODO: 上传图片到服务器
+      setTimeout(() => {
+        uni.hideLoading()
+        
+        uni.showToast({
+          title: '拍照成功',
+          icon: 'success'
+        })
+        
+        // 恢复检测
+        startDetection()
+      }, 1000)
     },
     fail: (err) => {
       console.error('拍照失败:', err)
@@ -99,45 +372,76 @@ const handleCapture = () => {
         title: '拍照失败',
         icon: 'none'
       })
+      
+      // 恢复检测
+      startDetection()
     }
   })
 }
 
 // 导入图片
 const handleSelectImage = () => {
+  stopDetection()
+  
   uni.chooseImage({
     count: 1,
     sizeType: ['original', 'compressed'],
     sourceType: ['album'],
     success: (res) => {
       console.log('选择图片成功:', res.tempFilePaths)
-      uni.showToast({
-        title: '图片导入成功',
-        icon: 'success'
+      
+      uni.showLoading({
+        title: '处理中...',
+        mask: true
       })
-      // TODO: 处理选择的图片
+      
+      // TODO: 上传图片到服务器
+      setTimeout(() => {
+        uni.hideLoading()
+        
+        uni.showToast({
+          title: '图片导入成功',
+          icon: 'success'
+        })
+        
+        startDetection()
+      }, 1000)
     },
-    fail: (err) => {
-      console.error('选择图片失败:', err)
+    fail: () => {
+      startDetection()
     }
   })
 }
 
 // 导入文档
 const handleSelectFile = () => {
+  stopDetection()
+  
   uni.chooseMessageFile({
     count: 1,
     type: 'file',
     success: (res) => {
       console.log('选择文件成功:', res.tempFiles)
-      uni.showToast({
-        title: '文档导入成功',
-        icon: 'success'
+      
+      uni.showLoading({
+        title: '处理中...',
+        mask: true
       })
-      // TODO: 处理选择的文件
+      
+      // TODO: 上传文件到服务器
+      setTimeout(() => {
+        uni.hideLoading()
+        
+        uni.showToast({
+          title: '文档导入成功',
+          icon: 'success'
+        })
+        
+        startDetection()
+      }, 1000)
     },
-    fail: (err) => {
-      console.error('选择文件失败:', err)
+    fail: () => {
+      startDetection()
     }
   })
 }
@@ -204,48 +508,54 @@ const handleSelectFile = () => {
       width: 100%;
       position: relative;
       
-      // 扫描框
-      .scan-frame {
+      // 边框检测画布
+      .border-canvas {
         position: absolute;
-        top: 50%;
-        left: 50%;
-        transform: translate(-50%, -50%);
-        width: 500rpx;
-        height: 500rpx;
+        top: 0;
+        left: 0;
+        z-index: 10;
+        pointer-events: none;
+      }
+    }
+    
+    // 模式切换按钮 - 在底部操作栏上方
+    .mode-switch-wrapper {
+      position: absolute;
+      bottom: 280rpx;
+      left: 50%;
+      transform: translateX(-50%);
+      z-index: 10;
+      
+      .mode-switch {
+        display: flex;
+        background: rgba(0, 0, 0, 0.6);
+        border-radius: 40rpx;
+        padding: 6rpx;
+        backdrop-filter: blur(10rpx);
         
-        .corner {
-          position: absolute;
-          width: 60rpx;
-          height: 60rpx;
-          border-color: #1ec9c9;
-          border-style: solid;
-          
-          &.corner-tl {
-            top: 0;
-            left: 0;
-            border-width: 6rpx 0 0 6rpx;
-            border-radius: 12rpx 0 0 0;
-          }
+        .mode-btn {
+          padding: 12rpx 32rpx;
+          border-radius: 34rpx;
+          transition: all 0.3s;
           
-          &.corner-tr {
-            top: 0;
-            right: 0;
-            border-width: 6rpx 6rpx 0 0;
-            border-radius: 0 12rpx 0 0;
+          .mode-text {
+            font-size: 26rpx;
+            color: rgba(255, 255, 255, 0.6);
+            font-weight: 500;
+            transition: all 0.3s;
           }
           
-          &.corner-bl {
-            bottom: 0;
-            left: 0;
-            border-width: 0 0 6rpx 6rpx;
-            border-radius: 0 0 0 12rpx;
+          &.active {
+            background: rgba(255, 255, 255, 0.9);
+            
+            .mode-text {
+              color: #333333;
+              font-weight: 600;
+            }
           }
           
-          &.corner-br {
-            bottom: 0;
-            right: 0;
-            border-width: 0 6rpx 6rpx 0;
-            border-radius: 0 0 12rpx 0;
+          &:active {
+            transform: scale(0.95);
           }
         }
       }


+ 14 - 1
utils/request.js

@@ -40,10 +40,23 @@ const request = (options) => {
     // 获取当前语言环境
     const language = getLanguage()
     
+    // 清理请求参数,移除 undefined、null 和空字符串
+    const cleanData = {}
+    if (options.data) {
+      Object.keys(options.data).forEach(key => {
+        const value = options.data[key]
+        // 只添加有效值(非 undefined、非 null、非空字符串)
+        if (value !== undefined && value !== null && value !== '') {
+          cleanData[key] = value
+        }
+      })
+      console.log('清理后的请求参数:', cleanData)
+    }
+    
     uni.request({
       url: BASE_URL + options.url,
       method: options.method || 'GET',
-      data: options.data || {},
+      data: cleanData,
       header: {
         'Content-Type': 'application/json',
         'Content-Language': language,