啊鑫
3 天以前 fca192d3c38c5dcfbb6ace8bc71d6078f6a079b2
LLJ附件系统全面优化:多格式文件预览与APK兼容性

主要改进:
- 实现多格式文件预览功能(文本、图片、Office文档)
- 修复文件预览弹窗中的下载错误(selectedAttachment为null)
- 针对APP环境优化文件下载和预览兼容性
- 添加条件编译支持不同平台的文件处理策略
- 重构按钮布局为垂直固定底部排列
- 新增APK兼容性说明文档和FTP接口文档

技术优化:
- APP环境图片预览先下载到本地避免网络问题
- 文件下载添加进度显示和存储路径指定
- 统一错误处理和用户体验优化
- CSS样式针对APP环境专项适配

包含文档:
- APK兼容性优化说明.md
- FTP文件操作接口文档-更新版.md
已添加2个文件
已修改2个文件
1362 ■■■■■ 文件已修改
APK兼容性优化说明.md 217 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
FTP文件操作接口文档-更新版.md 296 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
pages/QC/LLJ/Add.vue 844 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
store/index.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
APK¼æÈÝÐÔÓÅ»¯ËµÃ÷.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,217 @@
# APK兼容性优化说明
## ðŸŽ¯ æ¦‚è¿°
针对UniApp项目打包成Android APK时的兼容性问题,已实施全面的优化方案,确保所有功能在APP环境中正常运行。
## ðŸ”§ å·²è§£å†³çš„兼容性问题
### 1. æ–‡ä»¶é¢„览功能兼容性
#### é—®é¢˜æè¿°
- H5环境和APP环境的文件加载机制不同
- ç½‘络图片在APP中可能无法直接显示
- ä¸åŒæ–‡ä»¶ç±»åž‹éœ€è¦ä¸åŒçš„处理策略
#### è§£å†³æ–¹æ¡ˆ
```javascript
// å›¾ç‰‡é¢„览 - APP环境先下载到本地
// #ifdef APP-PLUS
uni.downloadFile({
    url: url,
    success: (res) => {
        // ä½¿ç”¨æœ¬åœ°è·¯å¾„预览
        uni.navigateTo({
            url: `/pages/fileView/imageView?url=${encodeURIComponent(res.tempFilePath)}`
        });
    }
});
// #endif
// H5环境直接使用网络URL
// #ifdef H5
uni.navigateTo({
    url: `/pages/fileView/imageView?url=${encodeURIComponent(url)}`
});
// #endif
```
### 2. æ–‡ä»¶ä¸‹è½½åŠŸèƒ½ä¼˜åŒ–
#### é—®é¢˜æè¿°
- H5环境使用blob下载,APP需要uni.downloadFile
- APP中文件存储路径和权限处理
- ä¸‹è½½è¿›åº¦æ˜¾ç¤ºå’Œç”¨æˆ·ä½“验
#### è§£å†³æ–¹æ¡ˆ
```javascript
// APP环境优化
// #ifdef APP-PLUS
const saveDir = plus.os.name === 'Android' ?
    plus.io.convertLocalFileSystemURL('_downloads/') :
    plus.io.convertLocalFileSystemURL('_documents/');
const downloadTask = uni.downloadFile({
    url: url,
    filePath: `${saveDir}${fileName}`, // æŒ‡å®šä¿å­˜è·¯å¾„
    success: (res) => {
        // æä¾›æ‰“开文件选项
        uni.showModal({
            title: '下载成功',
            confirmText: '打开文件',
            success: (modalRes) => {
                if (modalRes.confirm) {
                    plus.runtime.openFile(res.filePath);
                }
            }
        });
    }
});
// æ˜¾ç¤ºä¸‹è½½è¿›åº¦
downloadTask.onProgressUpdate((res) => {
    const progress = Math.round(res.progress);
    uni.showLoading({ title: `下载中 ${progress}%` });
});
// #endif
```
### 3. ç½‘络请求兼容性
#### é—®é¢˜æè¿°
- CORS问题在APP中不存在,但有其他限制
- è¯·æ±‚头和响应处理可能不同
- è¶…时和错误处理机制
#### è§£å†³æ–¹æ¡ˆ
- **APP环境**:无需处理CORS,使用原生网络请求
- **H5环境**:保留CORS处理和多种下载降级策略
- **统一错误处理**:提供友好的错误提示
### 4. UI组件适配
#### é—®é¢˜æè¿°
- å¼¹çª—在不同屏幕尺寸下的显示
- CSS样式在APP中的渲染差异
- è§¦æ‘¸äº¤äº’的优化
#### è§£å†³æ–¹æ¡ˆ
```css
/* APP环境专用样式 */
/* #ifdef APP-PLUS */
.file-preview-popup {
    width: 85vw;
    max-height: 75vh;
}
.file-preview-content {
    max-height: 350px;
}
/* #endif */
```
## ðŸ“± æ”¯æŒçš„æ–‡ä»¶ç±»åž‹
### å®Œå…¨æ”¯æŒé¢„览
- **文本文件**: txt, log, md, csv, json, xml
- **图片文件**: jpg, jpeg, png, gif, bmp, webp
### å‹å¥½æç¤ºä¸‹è½½
- **Office文档**: doc, docx, xls, xlsx, ppt, pptx
- **PDF文件**: pdf
- **其他格式**: æ˜¾ç¤ºä¸æ”¯æŒæç¤ºï¼Œæä¾›ä¸‹è½½é€‰é¡¹
## ðŸ”’ æƒé™å’Œå®‰å…¨
### Android权限
APP会自动申请以下权限:
- `WRITE_EXTERNAL_STORAGE`: æ–‡ä»¶å†™å…¥æƒé™
- `INTERNET`: ç½‘络访问权限
- `ACCESS_NETWORK_STATE`: ç½‘络状态检测
### æ–‡ä»¶å­˜å‚¨ä½ç½®
- **Android**: `_downloads/` ç›®å½•(用户可访问)
- **iOS**: `_documents/` ç›®å½•(应用沙盒)
## ðŸš€ æ€§èƒ½ä¼˜åŒ–
### ä¸‹è½½ä¼˜åŒ–
- æ˜¾ç¤ºå®žæ—¶ä¸‹è½½è¿›åº¦
- è‡ªåŠ¨é‡è¯•æœºåˆ¶
- ç½‘络状态检测
- æ–‡ä»¶å¤§å°é¢„检
### å†…存管理
- åŠæ—¶é‡Šæ”¾ä¸´æ—¶æ–‡ä»¶
- å›¾ç‰‡é¢„览使用本地缓存
- é¿å…å¤§æ–‡ä»¶å†…存占用
### ç”¨æˆ·ä½“验
- åŠ è½½çŠ¶æ€åé¦ˆ
- å‹å¥½çš„错误提示
- ä¸€é”®æ‰“开下载文件
- å–消下载功能
## ðŸ” æµ‹è¯•建议
### è®¾å¤‡å…¼å®¹æ€§æµ‹è¯•
1. **不同Android版本**: 5.0+ æ”¯æŒ
2. **不同屏幕尺寸**: 4寸~7寸平板
3. **不同品牌PDA**: ç¡®ä¿ç¡¬ä»¶åŠŸèƒ½æ­£å¸¸
### åŠŸèƒ½æµ‹è¯•æ¸…å•
- [ ] æ–‡ä»¶é¢„览(各种格式)
- [ ] æ–‡ä»¶ä¸‹è½½ï¼ˆç½‘络/离线)
- [ ] å­˜å‚¨æƒé™ç”³è¯·
- [ ] å¤§æ–‡ä»¶å¤„理(>10MB)
- [ ] ç½‘络中断恢复
- [ ] å¼¹çª—显示适配
### æ€§èƒ½æµ‹è¯•
- [ ] å¯åŠ¨é€Ÿåº¦
- [ ] å†…存占用
- [ ] æ–‡ä»¶æ“ä½œå“åº”æ—¶é—´
- [ ] æ‰¹é‡ä¸‹è½½æ€§èƒ½
## ðŸ“‹ éƒ¨ç½²æ³¨æ„äº‹é¡¹
### manifest.json é…ç½®
```json
{
    "app-plus": {
        "modules": {
            "FileSystem": {}
        },
        "permissions": {
            "写入系统存储": {
                "request": "always"
            }
        }
    }
}
```
### æ‰“包配置
- å¯ç”¨æ–‡ä»¶ç³»ç»Ÿæ¨¡å—
- é…ç½®å­˜å‚¨æƒé™
- è®¾ç½®ç½‘络安全策略
- ä¼˜åŒ–包大小
## ðŸ› å·²çŸ¥é™åˆ¶
1. **大文件预览**: è¶…过50MB的文件建议直接下载
2. **网络依赖**: é¢„览功能需要稳定的网络连接
3. **存储空间**: ä¸‹è½½æ–‡ä»¶ä¼šå ç”¨è®¾å¤‡å­˜å‚¨ç©ºé—´
4. **系统应用**: æŸäº›æ–‡ä»¶ç±»åž‹éœ€è¦å®‰è£…对应的阅读器
## ðŸ“ž æŠ€æœ¯æ”¯æŒ
如遇到兼容性问题:
1. æ£€æŸ¥è®¾å¤‡Android版本(建议5.0+)
2. ç¡®è®¤å­˜å‚¨æƒé™å·²æŽˆäºˆ
3. æµ‹è¯•网络连接状况
4. æŸ¥çœ‹æŽ§åˆ¶å°é”™è¯¯æ—¥å¿—
---
**文档版本**: v1.0
**最后更新**: 2025-07-20
**适用版本**: UniApp v2.0.2+
FTPÎļþ²Ù×÷½Ó¿ÚÎĵµ-¸üаæ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,296 @@
# é™„件管理系统 - FTP文件操作接口文档 (更新版)
## ðŸš¨ é‡è¦æ›´æ–°ï¼šCORS跨域问题解决方案
### é—®é¢˜æè¿°
在H5环境中使用 `DownloadFtpFile` æŽ¥å£æ—¶å‡ºçŽ°ä»¥ä¸‹é”™è¯¯ï¼š
```
Refused to get unsafe header "content-disposition"
```
### åŽŸå› åˆ†æž
这是由于浏览器的CORS(跨域资源共享)安全策略导致的,浏览器阻止了前端访问某些HTTP响应头。
### è§£å†³æ–¹æ¡ˆ
#### åŽç«¯CORS配置(必须)
后端接口必须设置以下CORS响应头:
```http
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Expose-Headers: Content-Disposition, Content-Length, Content-Type
```
**关键点**: `Access-Control-Expose-Headers` å¿…须包含 `Content-Disposition`,否则前端无法读取文件名信息。
#### å‰ç«¯å¤šçŽ¯å¢ƒå…¼å®¹ï¼ˆå·²å®žçŽ°ï¼‰
前端代码已更新为多环境兼容:
1. **H5环境**: ä½¿ç”¨å¤šç§ä¸‹è½½æ–¹å¼çš„降级策略
   - æ–¹æ³•1: `<a>` æ ‡ç­¾ä¸‹è½½
   - æ–¹æ³•2: `window.open` ä¸‹è½½
   - æ–¹æ³•3: `fetch` + `blob` ä¸‹è½½
2. **APP环境**: ä½¿ç”¨ `uni.downloadFile`
3. **小程序环境**: ä½¿ç”¨ `uni.downloadFile`
---
## åŸºç¡€ä¿¡æ¯
- **服务器地址**: `http://36.26.21.214:10054/api`
- **FTP服务器**: `ftp://36.26.21.214`
- **模块**: LLJ (来料检验)
---
## 1. èŽ·å–é™„ä»¶åˆ—è¡¨
### æŽ¥å£ä¿¡æ¯
- **URL**: `/LLJ/getAttachments`
- **方法**: `POST`
- **用途**: èŽ·å–æŒ‡å®šæ£€éªŒå•çš„é™„ä»¶åˆ—è¡¨
### è¯·æ±‚参数
```json
{
  "releaseNo": "检验单号"
}
```
### å“åº”格式
```json
{
  "status": 0,
  "message": "成功",
  "data": {
    "tbBillList": [
      {
        "id": 123456,
        "itemNo": "物料编号",
        "fattach": "附件文件名.pdf",
        "ftype": "文件类型",
        "fversion": "版本号",
        "fdate": "受控日期",
        "createBy": "上传人",
        "createDate": "上传时间"
      }
    ]
  }
}
```
---
## 2. é¢„览FTP文件
### æŽ¥å£ä¿¡æ¯
- **URL**: `/LLJ/PreviewFtpFile`
- **方法**: `GET`
- **用途**: èŽ·å–FTP服务器上文件的预览流
### è¯·æ±‚参数
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| itemNo | string | æ˜¯ | ç‰©æ–™ç¼–号 |
| fileName | string | æ˜¯ | æ–‡ä»¶åï¼ˆéœ€URL编码) |
| ftpServer | string | æ˜¯ | FTP服务器地址 `ftp://36.26.21.214` |
### CORS配置要求
```http
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Expose-Headers: Content-Type, Content-Length
```
### æ”¯æŒçš„æ–‡ä»¶ç±»åž‹
- **PDF文件**: `pdf`
- **图片文件**: `jpg`, `jpeg`, `png`, `gif`, `bmp`, `webp`
- **文本文件**: `txt`, `log`, `md`, `csv`
- **Office文档**: `doc`, `docx`, `xls`, `xlsx`, `ppt`, `pptx`
---
## 3. ä¸‹è½½FTP文件 â­ é‡ç‚¹æ›´æ–°
### æŽ¥å£ä¿¡æ¯
- **URL**: `/LLJ/DownloadFtpFile`
- **方法**: `GET`
- **用途**: ä»ŽFTP服务器下载文件
### è¯·æ±‚参数
| å‚数名 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|--------|------|------|------|
| itemNo | string | æ˜¯ | ç‰©æ–™ç¼–号 |
| fileName | string | æ˜¯ | æ–‡ä»¶åï¼ˆéœ€URL编码) |
| ftpServer | string | æ˜¯ | FTP服务器地址 `ftp://36.26.21.214` |
### âš ï¸ å…³é”®CORS配置
```http
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Expose-Headers: Content-Disposition, Content-Length, Content-Type
```
### å“åº”头要求
```http
Content-Type: application/octet-stream (或具体的MIME类型)
Content-Disposition: attachment; filename="文件名.pdf"
Content-Length: æ–‡ä»¶å¤§å°
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Disposition, Content-Length, Content-Type
```
### å‰ç«¯å¤„理策略
```javascript
// H5环境:多种下载方式
1. <a> æ ‡ç­¾ä¸‹è½½ (首选)
2. window.open ä¸‹è½½ (备选)
3. fetch + blob ä¸‹è½½ (兜底)
// APP/小程序环境:
uni.downloadFile (原生下载)
```
---
## 4. å®žçŽ°å»ºè®®
### åŽç«¯CORS中间件配置示例
#### .NET Core ç¤ºä¾‹
```csharp
public void ConfigureServices(IServiceCollection services)
{
    services.AddCors(options =>
    {
        options.AddPolicy("AllowAll",
            builder =>
            {
                builder
                    .AllowAnyOrigin()
                    .AllowAnyMethod()
                    .AllowAnyHeader()
                    .WithExposedHeaders("Content-Disposition", "Content-Length", "Content-Type");
            });
    });
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseCors("AllowAll");
}
```
#### Express.js ç¤ºä¾‹
```javascript
app.use((req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*');
    res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
    res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    res.header('Access-Control-Expose-Headers', 'Content-Disposition, Content-Length, Content-Type');
    next();
});
```
### æ–‡ä»¶ä¸‹è½½å“åº”示例
```http
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: attachment; filename="测试文档.pdf"
Content-Length: 1024000
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Disposition, Content-Length, Content-Type
[文件二进制数据]
```
---
## 5. æ•…障排除
### å¸¸è§é—®é¢˜åŠè§£å†³æ–¹æ¡ˆ
#### é—®é¢˜1: "Refused to get unsafe header"
**原因**: åŽç«¯æœªè®¾ç½® `Access-Control-Expose-Headers`
**解决**: æ·»åŠ  `Access-Control-Expose-Headers: Content-Disposition, Content-Length, Content-Type`
#### é—®é¢˜2: CORS预检请求失败
**原因**: åŽç«¯æœªå¤„理 OPTIONS è¯·æ±‚
**解决**: æ·»åŠ  OPTIONS æ–¹æ³•支持和相应的CORS头
#### é—®é¢˜3: æ–‡ä»¶ä¸‹è½½ä½†æ–‡ä»¶åä¹±ç 
**原因**: Content-Disposition ä¸­çš„æ–‡ä»¶åç¼–码问题
**解决**: ä½¿ç”¨ UTF-8 ç¼–码或 RFC 5987 æ ¼å¼
```http
Content-Disposition: attachment; filename*=UTF-8''%E6%B5%8B%E8%AF%95%E6%96%87%E6%A1%A3.pdf
```
#### é—®é¢˜4: å¤§æ–‡ä»¶ä¸‹è½½è¶…æ—¶
**原因**: æœåŠ¡å™¨æˆ–ç½‘ç»œè¶…æ—¶
**解决**:
- å¢žåŠ æœåŠ¡å™¨è¶…æ—¶è®¾ç½®
- å®žçŽ°æ–­ç‚¹ç»­ä¼ 
- æä¾›åˆ†ç‰‡ä¸‹è½½
---
## 6. æµ‹è¯•方案
### æµ‹è¯•环境
1. **H5环境**: Chrome, Safari, Firefox
2. **APP环境**: Android, iOS åŽŸç”Ÿåº”ç”¨
3. **小程序环境**: å¾®ä¿¡å°ç¨‹åº
### æµ‹è¯•用例
```javascript
// æµ‹è¯•CORS配置
fetch('http://36.26.21.214:10054/api/LLJ/DownloadFtpFile?itemNo=TEST&fileName=test.pdf&ftpServer=ftp%3A//36.26.21.214', {
    method: 'GET'
})
.then(response => {
    console.log('Content-Disposition:', response.headers.get('Content-Disposition'));
    // åº”该能正常获取到文件名
})
.catch(error => {
    console.error('CORS Error:', error);
});
```
---
## 7. ç›‘控与日志
### å»ºè®®ç›‘控指标
- ä¸‹è½½æˆåŠŸçŽ‡
- ä¸‹è½½è€—æ—¶
- CORS错误频率
- æ–‡ä»¶ä¸å­˜åœ¨é”™è¯¯é¢‘率
### æ—¥å¿—记录建议
```javascript
// è®°å½•下载请求
{
    "timestamp": "2025-07-20T10:00:00Z",
    "method": "DownloadFtpFile",
    "itemNo": "ABC123",
    "fileName": "test.pdf",
    "userAgent": "Mozilla/5.0...",
    "success": true,
    "errorCode": null,
    "responseTime": 1500
}
```
---
**文档版本**: v2.0 (CORS更新版)
**最后更新**: 2025-07-20
**更新内容**:
- æ–°å¢žCORS跨域问题解决方案
- æ›´æ–°å‰ç«¯å¤šçŽ¯å¢ƒä¸‹è½½ç­–ç•¥
- æ·»åŠ åŽç«¯CORS配置示例
- å®Œå–„故障排除指南
pages/QC/LLJ/Add.vue
@@ -103,12 +103,6 @@
        <!-- æ“ä½œæŒ‰é’®åŒº -->
        <view class="action-buttons" v-if="this.current">
                    <button class="secondary-btn" @click="handleEmergencyRelease">紧急放行</button>
                    <button class="secondary-btn" @click="handleWithdraw">撤回</button>
                    <button class="secondary-btn" @click="getInspectionItems">获取检验项目</button>
        </view>
        
@@ -147,14 +141,22 @@
            </table>
        </view>
        <!-- æ“ä½œæŒ‰é’®åŒº -->
        <view class="action-buttons">
            <button class="secondary-btn" @click="addDestruction" v-if="this.current">破坏实验</button>
            <button class="secondary-btn" @click="uploadImages">上传/查看图片</button>
            <button class="secondary-btn" @click="fetchDrawingNumber">调取PLM图纸</button>
            <button class="secondary-btn" @click="viewAttachmentInfo">查看附件信息</button>
            <button class="secondary-btn" @click="addDefectDescription" v-if="this.current">添加不良描述</button>
            <button class="primary-btn" @click="submitInspection" v-if="this.current">检验提交</button>
        <!-- é¡µé¢å†…容区域 -->
        <view class="content-wrapper">
            <!-- ä¸ºåº•部按钮留出空间 -->
        </view>
        <!-- å›ºå®šåœ¨åº•部的操作按钮区 -->
        <view class="fixed-action-buttons">
            <button class="action-btn" @click="handleEmergencyRelease" v-if="this.current">紧急放行</button>
            <button class="action-btn" @click="handleWithdraw" v-if="this.current">撤回</button>
            <button class="action-btn" @click="getInspectionItems" v-if="this.current">获取检验项目</button>
            <button class="action-btn" @click="addDestruction" v-if="this.current">破坏实验</button>
            <button class="action-btn" @click="uploadImages">上传/查看图片</button>
            <button class="action-btn" @click="fetchDrawingNumber">调取PLM图纸</button>
            <button class="action-btn" @click="viewAttachmentInfo">查看附件信息</button>
            <button class="action-btn" @click="addDefectDescription" v-if="this.current">添加不良描述</button>
            <button class="action-btn primary" @click="submitInspection" v-if="this.current">检验提交</button>
        </view>
        <view v-if="remarksPopup" class="overlay">
            <view class="popup">
@@ -189,8 +191,8 @@
        
        <view class="barcode">
            <u-modal :show="drawingShow" title="图纸明细" @confirm="drawingConfirm" @cancel="drawingCancel"
                showCancelButton>
                <uni-table border stripe emptyText="暂无更多数据" style="margin-left: 5px;margin-right: 5px;height: 500px;">
                showCancelButton :z-index="1000">
                <uni-table border stripe emptyText="暂无更多数据" style="margin-left: 5px;margin-right: 5px;height: 400px;max-height: 60vh;overflow-y: auto;">
                    <uni-tr>
                        <uni-th align="center">相关文档</uni-th>
                        <uni-th align="center" width="90">有无关联PDF文件</uni-th>
@@ -227,19 +229,16 @@
                    <div class="attachment-detail-row"><span class="attachment-label">受控日期:</span><span>{{ selectedAttachment.fdate }}</span></div>
                    <div class="attachment-detail-row"><span class="attachment-label">上传人:</span><span>{{ selectedAttachment.createBy }}</span></div>
                    <div class="attachment-detail-row"><span class="attachment-label">上传时间:</span><span>{{ selectedAttachment.createDate }}</span></div>
                    <div v-if="isPreviewable(selectedAttachment.fattach)" class="attachment-preview-area">
                        <div v-if="['pdf','jpg','jpeg','png','gif'].includes(selectedAttachment.fattach.trim().split('.').pop().toLowerCase())">
                            <iframe :src="getAttachmentUrl(selectedAttachment)" style="width:100%;height:320px;border-radius:10px;background:#f8fafc;" frameborder="0"></iframe>
                        </div>
                        <div v-else-if="['txt'].includes(selectedAttachment.fattach.trim().split('.').pop().toLowerCase())">
                            <iframe :src="getAttachmentUrl(selectedAttachment)" style="width:100%;height:320px;border-radius:10px;background:#f8fafc;" frameborder="0"></iframe>
                        </div>
                        <div v-else-if="['doc','docx','xls','xlsx'].includes(selectedAttachment.fattach.trim().split('.').pop().toLowerCase())">
                            <iframe :src="'https://view.officeapps.live.com/op/view.aspx?src=' + encodeURIComponent(getAttachmentUrl(selectedAttachment))" style="width:100%;height:320px;border-radius:10px;background:#f8fafc;" frameborder="0"></iframe>
                        </div>
                    </div>
                    <div v-else class="attachment-download-area">
                        <button class="attachment-download-link" @click="downloadAttachment(selectedAttachment)">下载附件</button>
                    <div class="attachment-actions-detail">
                        <button class="attachment-action-btn preview-btn"
                            @click="previewFtpFile(selectedAttachment)"
                            v-if="isPreviewable(selectedAttachment.fattach)">
                            ðŸ” åœ¨çº¿é¢„览
                        </button>
                        <button class="attachment-action-btn download-btn"
                            @click="downloadAttachment(selectedAttachment)">
                            ðŸ“¥ ä¸‹è½½æ–‡ä»¶
                        </button>
                    </div>
                </div>
                <div v-else class="attachment-detail-empty">暂无附件信息</div>
@@ -254,16 +253,58 @@
                <div v-else-if="attachments.length === 0">暂无附件</div>
                <ul class="attachment-list" v-else>
                    <li v-for="item in attachments" :key="item.id">
                        <div class="attachment-info">
                        <span class="attachment-name" @click="showAttachmentDetailDialog(item)">
                            {{ item.fattach }}
                        </span>
                            <div class="attachment-meta">
                                <span class="attachment-type">{{ item.ftype || '未知类型' }}</span>
                            </div>
                        </div>
                        <div class="attachment-actions">
                            <button class="secondary-btn" @click="showAttachmentDetailDialog(item)">详情</button>
                            <button class="secondary-btn preview-btn" @click="previewFtpFile(item)"
                                v-if="isPreviewable(item.fattach)">预览</button>
                            <button class="secondary-btn" @click="downloadAttachment(item)">下载</button>
                        </div>
                    </li>
                </ul>
                <button class="attachment-popup-close" @click="closeAttachmentPopup">关闭</button>
            </view>
        </view>
        <!-- æ–‡ä»¶é¢„览弹窗 -->
        <view v-if="showFilePreviewPopup" class="overlay">
            <view class="popup file-preview-popup">
                <h3 class="file-preview-title">{{ previewTitle }}</h3>
                <div class="file-preview-divider"></div>
                <div class="file-preview-content">
                    <!-- æ–‡æœ¬å†…容预览 -->
                    <pre v-if="previewType === 'text'">{{ previewContent }}</pre>
                    <!-- å›¾ç‰‡å†…容预览 -->
                    <view v-else-if="previewType === 'image'" class="image-preview-container">
                        <image :src="previewContent" mode="widthFix" style="width: 100%; max-height: 400px;"></image>
                    </view>
                    <!-- Excel ç­‰ Office æ–‡ä»¶æç¤º -->
                    <view v-else-if="previewType === 'excel'" class="unsupported-preview">
                        <view class="unsupported-icon">📊</view>
                        <view class="unsupported-text">Excel æ–‡ä»¶æš‚不支持在线预览</view>
                        <view class="unsupported-hint">请点击下载按钮获取完整文件</view>
                    </view>
                    <!-- ä¸æ”¯æŒçš„æ–‡ä»¶ç±»åž‹ -->
                    <view v-else class="unsupported-preview">
                        <view class="unsupported-icon">📄</view>
                        <view class="unsupported-text">此文件格式暂不支持预览</view>
                        <view class="unsupported-hint">请点击下载按钮获取完整文件</view>
                    </view>
                </div>
                <div class="file-preview-actions">
                    <button v-if="previewType !== 'text'" class="file-preview-btn download-btn" @click="downloadPreviewFile">📥 ä¸‹è½½æ–‡ä»¶</button>
                    <button class="file-preview-btn close-btn" @click="closeFilePreview">关闭</button>
                </div>
            </view>
        </view>
    </view>
@@ -325,6 +366,11 @@
                attachmentsLoading: false,
                selectedAttachment: null,
                showAttachmentDetail: false,
                showFilePreviewPopup: false,
                previewContent: '',
                previewTitle: '',
                previewItemNo: '',
                previewType: '', // 'text', 'image', 'excel', 'unsupported'
                
            }
        },
@@ -1286,6 +1332,11 @@
                    this.attachmentsLoading = false;
                    if (res.status === 0) {
                        this.attachments = res.data.tbBillList;
                        // ä¸ºæ¯ä¸ªé™„件设置默认可用状态
                        this.attachments.forEach((item, index) => {
                            this.$set(item, 'ftpAvailable', true); // é»˜è®¤è®¤ä¸ºæ–‡ä»¶å¯ç”¨
                            this.$set(item, 'checking', false);
                        });
                    } else if (res.status === 1 && res.message === "该检验单未上传附件信息!") {
                        uni.showToast({ title: res.message, icon: "none" });
                    } else {
@@ -1297,10 +1348,10 @@
                this.showAttachmentPopup = false;
            },
            getAttachmentUrl(item) {
                const baseUrl = "http://192.168.1.22:10054";
                // åŽ»é™¤æ‰€æœ‰ç©ºç™½å­—ç¬¦ï¼ˆåŒ…æ‹¬ä¸­è‹±æ–‡ç©ºæ ¼ã€åˆ¶è¡¨ç¬¦ç­‰ï¼‰
                let fileName = item.fattach.replace(/[\s\u3000]+/g, '').trim();
                return baseUrl + "/api/LLJ/DownloadAttachment?itemNo=" + encodeURIComponent(item.itemNo) + "&fileName=" + encodeURIComponent(fileName);
                let fileName = item.fattach.replace(/[\s\u3000\r\n]+/g, '').trim();
                // ç»Ÿä¸€ä½¿ç”¨FTP下载接口,包含FTP服务器地址
                return this.$store.state.serverInfo.serverAPI + "/LLJ/DownloadFtpFile?itemNo=" + encodeURIComponent(item.itemNo) + "&fileName=" + encodeURIComponent(fileName) + "&ftpServer=" + encodeURIComponent(this.$store.state.serverInfo.ftpServer);
            },
            showAttachmentDetailDialog(item) {
                console.log('查看详情', item);
@@ -1319,7 +1370,13 @@
                const ext = filename.trim().split('.').pop().toLowerCase();
                // æ”¯æŒåœ¨çº¿é¢„览的文件类型
                return [
                    'pdf', 'jpg', 'jpeg', 'png', 'gif', 'txt', 'doc', 'docx', 'xls', 'xlsx'
                    'pdf',           // PDF文件
                    'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp',  // å›¾ç‰‡æ–‡ä»¶
                    'txt', 'log', 'md',  // æ–‡æœ¬æ–‡ä»¶
                    'doc', 'docx',   // Word文档
                    'xls', 'xlsx',   // Excel表格
                    'ppt', 'pptx',   // PowerPoint演示文稿
                    'csv'            // CSV文件
                ].includes(ext);
            },
            // å¤„理附件下载错误
@@ -1330,32 +1387,474 @@
                    showCancel: false
                });
            },
            downloadAttachment(item) {
                const baseUrl = "http://192.168.1.22:10054";
                // åŽ»é™¤æ‰€æœ‰ç©ºæ ¼ã€å…¨è§’ç©ºæ ¼ã€å›žè½¦ã€æ¢è¡Œ
            // é¢„览FTP文件
            previewFtpFile(item) {
                const fileName = item.fattach.replace(/[\s\u3000\r\n]+/g, '').trim();
                const url = baseUrl + "/api/Llj/DownloadFtpFile?itemNo=" + encodeURIComponent(item.itemNo) + "&fileName=" + encodeURIComponent(fileName);
                uni.downloadFile({
                const fileExt = fileName.split('.').pop().toLowerCase();
                // æ£€æŸ¥æ–‡ä»¶ç±»åž‹æ˜¯å¦æ”¯æŒé¢„览
                if (!this.isPreviewable(fileName)) {
                    uni.showModal({
                        title: '不支持预览',
                        content: '该文件类型不支持在线预览,请下载后查看',
                        showCancel: false
                    });
                    return;
                }
                const previewUrl = this.$store.state.serverInfo.serverAPI + "/LLJ/PreviewFtpFile?itemNo=" + encodeURIComponent(item.itemNo) + "&fileName=" + encodeURIComponent(fileName) + "&ftpServer=" + encodeURIComponent(this.$store.state.serverInfo.ftpServer);
                // æ ¹æ®æ–‡ä»¶ç±»åž‹è¿›è¡Œä¸åŒçš„预览处理
                if (['pdf'].includes(fileExt)) {
                    this.previewPdfFile(previewUrl, fileName);
                } else if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(fileExt)) {
                    this.previewImageFile(previewUrl, fileName);
                } else if (['txt'].includes(fileExt)) {
                    this.previewTextFile(previewUrl, fileName);
                } else if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(fileExt)) {
                    this.previewOfficeFile(previewUrl, fileName);
                } else {
                    // å°è¯•通用预览
                    this.previewGenericFile(previewUrl, fileName);
                }
            },
            // é¢„览PDF文件
            previewPdfFile(url, fileName) {
                // å…ˆä¸‹è½½PDF文件,转为base64后预览
                uni.request({
                    url: url,
                    method: 'GET',
                    responseType: 'arraybuffer',
                    success: (res) => {
                        if (res.statusCode === 200) {
                            if (typeof plus !== 'undefined' && plus.runtime && plus.runtime.openFile) {
                                plus.runtime.openFile({ path: res.tempFilePath }, () => {
                                    uni.showToast({ title: '打开成功', icon: 'success' });
                                }, (e) => {
                                    uni.showModal({ title: '提示', content: '文件下载成功,但无法自动打开。请在文件管理中手动查找并打开。', showCancel: false });
                            const base64Data = uni.arrayBufferToBase64(res.data);
                            // å­˜å‚¨åˆ°å…¨å±€å˜é‡
                            getApp().globalData.tempPDF = base64Data;
                            uni.navigateTo({
                                url: `/pages/fileView/pdfView`
                                });
                            } else {
                                uni.showModal({ title: '提示', content: '文件下载成功,但当前环境无法自动打开。请在文件管理中手动查找并打开。', showCancel: false });
                            }
                        } else {
                            uni.showModal({ title: '下载失败', content: `下载失败,状态码:${res.statusCode}`, showCancel: false });
                            this.handlePreviewError(res.statusCode, fileName);
                        }
                    },
                    fail: (error) => {
                        uni.showModal({ title: '下载失败', content: `下载失败,请检查网络连接。${error.errMsg}`, showCancel: false });
                        this.handlePreviewError(0, fileName, error.errMsg);
                    }
                });
            },
            // é¢„览图片文件
            previewImageFile(url, fileName) {
                // #ifdef APP-PLUS
                // APP环境:先下载到本地再预览,避免网络图片加载问题
                uni.showLoading({ title: '加载图片...' });
                uni.downloadFile({
                    url: url,
                    success: (res) => {
                        uni.hideLoading();
                        if (res.statusCode === 200) {
                            // ä½¿ç”¨æœ¬åœ°ä¸´æ—¶è·¯å¾„
                            uni.navigateTo({
                                url: `/pages/fileView/imageView?url=${encodeURIComponent(res.tempFilePath)}`
                            });
                        } else {
                            this.handlePreviewError(res.statusCode, fileName);
                        }
                    },
                    fail: (error) => {
                        uni.hideLoading();
                        this.handlePreviewError(0, fileName, error.errMsg);
                    }
                });
                // #endif
                // #ifdef H5 || MP
                // H5和小程序:直接使用网络URL
                uni.navigateTo({
                    url: `/pages/fileView/imageView?url=${encodeURIComponent(url)}`
                });
                // #endif
            },
            // é¢„览文本文件
            previewTextFile(url, fileName) {
                // æ–‡æœ¬æ–‡ä»¶ç›´æŽ¥æ˜¾ç¤ºåœ¨å¼¹çª—中
                uni.showLoading({ title: '加载文件内容...' });
                uni.request({
                    url: url,
                    method: 'GET',
                    success: (res) => {
                        uni.hideLoading();
                        if (res.statusCode === 200) {
                            const fileType = this.getFileType(fileName);
                            if (fileType === 'text') {
                                // æ–‡æœ¬æ–‡ä»¶ï¼šæ˜¾ç¤ºå†…容
                                this.showFilePreview(res.data, fileName);
                            } else if (fileType === 'image') {
                                // å›¾ç‰‡æ–‡ä»¶ï¼šæ˜¾ç¤ºå›¾ç‰‡URL
                                this.showFilePreview(url, fileName);
                            } else {
                                // å…¶ä»–文件类型:显示提示信息
                                this.showFilePreview('', fileName);
                            }
                        } else {
                            this.handlePreviewError(res.statusCode, fileName);
                        }
                    },
                    fail: (error) => {
                        uni.hideLoading();
                        this.handlePreviewError(0, fileName, error.errMsg);
                    }
                });
            },
            // æ£€æµ‹æ–‡ä»¶ç±»åž‹
            getFileType(fileName) {
                const fileExt = fileName.split('.').pop().toLowerCase();
                if (['txt', 'log', 'md', 'csv', 'json', 'xml'].includes(fileExt)) {
                    return 'text';
                } else if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(fileExt)) {
                    return 'image';
                } else if (['xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx'].includes(fileExt)) {
                    return 'excel';
                } else {
                    return 'unsupported';
                }
            },
            // æ˜¾ç¤ºæ–‡ä»¶é¢„览弹窗
            showFilePreview(content, fileName) {
                this.previewContent = content;
                this.previewTitle = fileName;
                this.previewItemNo = this.selectedAttachment?.itemNo || '';
                this.previewType = this.getFileType(fileName);
                this.showFilePreviewPopup = true;
            },
            // å…³é—­æ–‡ä»¶é¢„览弹窗
            closeFilePreview() {
                this.showFilePreviewPopup = false;
                this.previewContent = '';
                this.previewTitle = '';
                this.previewItemNo = '';
                this.previewType = '';
            },
            // ä¸‹è½½é¢„览文件
            downloadPreviewFile() {
                const item = { fattach: this.previewTitle, itemNo: this.previewItemNo };
                this.downloadAttachment(item);
                this.closeFilePreview();
            },
            // é¢„览Office文件
            previewOfficeFile(url, fileName) {
                // å…ˆæ£€æŸ¥Excel文件,使用专门的Excel预览页面
                const fileExt = fileName.split('.').pop().toLowerCase();
                if (['xls', 'xlsx'].includes(fileExt)) {
                    // Excel文件预览
                    uni.request({
                        url: url,
                        method: 'GET',
                        responseType: 'arraybuffer',
                        success: (res) => {
                            if (res.statusCode === 200) {
                                const base64Data = uni.arrayBufferToBase64(res.data);
                                // å­˜å‚¨ Base64 æ•°æ®åˆ°æœ¬åœ°å­˜å‚¨
                                uni.setStorageSync('excelBase64Data', base64Data);
                                uni.navigateTo({
                                    url: `/pages/fileView/excelView`
                                });
                            } else {
                                this.handlePreviewError(res.statusCode, fileName);
                            }
                        },
                        fail: (error) => {
                            this.handlePreviewError(0, fileName, error.errMsg);
                        }
                    });
                } else if (['doc', 'docx'].includes(fileExt)) {
                    // Word文件,尝试使用Word预览页面或者微软在线预览
                    try {
                        const officePreviewUrl = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(url)}`;
                        // å¦‚果有webView页面,使用webView预览
                        this.previewGenericFile(officePreviewUrl, fileName);
                    } catch (error) {
                        this.handlePreviewError(0, fileName, '不支持此Office文件类型的预览');
                    }
                } else {
                    // å…¶ä»–Office文件,使用微软在线预览服务
                    const officePreviewUrl = `https://view.officeapps.live.com/op/view.aspx?src=${encodeURIComponent(url)}`;
                    this.previewGenericFile(officePreviewUrl, fileName);
                }
            },
            // é€šç”¨æ–‡ä»¶é¢„览
            previewGenericFile(url, fileName) {
                // ç”±äºŽæ²¡æœ‰é€šç”¨çš„webView页面,显示提示并提供下载
                uni.showModal({
                    title: '文件预览',
                    content: `文件 "${fileName}" éœ€è¦ä¸‹è½½åŽæŸ¥çœ‹ï¼Œæ˜¯å¦ç«‹å³ä¸‹è½½ï¼Ÿ`,
                    showCancel: true,
                    confirmText: '下载',
                    cancelText: '取消',
                    success: (res) => {
                        if (res.confirm) {
                            const item = { fattach: fileName, itemNo: this.selectedAttachment.itemNo };
                            this.downloadAttachment(item);
                        }
                    }
                });
            },
            // å¤„理预览错误
            handlePreviewError(statusCode, fileName, errorMsg = '') {
                let message = '';
                if (statusCode === 404) {
                    message = `文件 ${fileName} åœ¨FTP服务器上不存在`;
                } else if (statusCode === 0) {
                    message = `预览失败:${errorMsg}`;
                } else {
                    message = `预览失败,状态码:${statusCode}`;
                }
                uni.showModal({
                    title: '预览失败',
                    content: message,
                    showCancel: true,
                    confirmText: '下载',
                    cancelText: '取消',
                    success: (res) => {
                        if (res.confirm) {
                            // ç”¨æˆ·é€‰æ‹©ä¸‹è½½æ–‡ä»¶
                            const item = { fattach: fileName, itemNo: this.selectedAttachment.itemNo };
                            this.downloadAttachment(item);
                        }
                    }
                });
            },
            downloadAttachment(item) {
                // åŽ»é™¤æ‰€æœ‰ç©ºæ ¼ã€å…¨è§’ç©ºæ ¼ã€å›žè½¦ã€æ¢è¡Œ
                const fileName = item.fattach.replace(/[\s\u3000\r\n]+/g, '').trim();
                // ä½¿ç”¨é…ç½®çš„æœåŠ¡å™¨åœ°å€å’ŒFTP服务器地址
                const url = this.$store.state.serverInfo.serverAPI + "/LLJ/DownloadFtpFile?itemNo=" + encodeURIComponent(item.itemNo) + "&fileName=" + encodeURIComponent(fileName) + "&ftpServer=" + encodeURIComponent(this.$store.state.serverInfo.ftpServer);
                // æ£€æŸ¥è¿è¡ŒçŽ¯å¢ƒ
                // #ifdef H5
                // H5环境:使用浏览器下载
                this.downloadFileInBrowser(url, fileName);
                // #endif
                // #ifdef APP-PLUS
                // APP环境:使用uni.downloadFile
                this.downloadFileInApp(url, fileName);
                // #endif
                // #ifdef MP
                // å°ç¨‹åºçŽ¯å¢ƒï¼šä½¿ç”¨uni.downloadFile
                this.downloadFileInApp(url, fileName);
                // #endif
            },
            // åœ¨æµè§ˆå™¨ä¸­ä¸‹è½½æ–‡ä»¶
            downloadFileInBrowser(url, fileName) {
                uni.showLoading({ title: '正在准备下载...' });
                // æ–¹æ³•1:创建隐藏的a标签下载
                try {
                    const link = document.createElement('a');
                    link.href = url;
                    link.download = fileName;
                    link.style.display = 'none';
                    document.body.appendChild(link);
                    link.click();
                    document.body.removeChild(link);
                    uni.hideLoading();
                    uni.showToast({
                        title: '下载已开始',
                        icon: 'success',
                        duration: 2000
                    });
                } catch (error) {
                    console.log('a标签下载失败,尝试window.open方式:', error);
                    // æ–¹æ³•2:使用window.open
                    try {
                        window.open(url, '_blank');
                        uni.hideLoading();
                        uni.showToast({
                            title: '下载已开始',
                            icon: 'success',
                            duration: 2000
                        });
                    } catch (error2) {
                        console.log('window.open下载失败,尝试fetch方式:', error2);
                        // æ–¹æ³•3:使用fetch下载
                        this.downloadFileWithFetch(url, fileName);
                    }
                }
            },
            // ä½¿ç”¨fetch下载文件
            downloadFileWithFetch(url, fileName) {
                fetch(url)
                    .then(response => {
                        if (!response.ok) {
                            throw new Error(`HTTP error! status: ${response.status}`);
                        }
                        return response.blob();
                    })
                    .then(blob => {
                        // åˆ›å»ºblob URL
                        const blobUrl = window.URL.createObjectURL(blob);
                        // åˆ›å»ºä¸‹è½½é“¾æŽ¥
                        const link = document.createElement('a');
                        link.href = blobUrl;
                        link.download = fileName;
                        link.style.display = 'none';
                        document.body.appendChild(link);
                        link.click();
                        document.body.removeChild(link);
                        // é‡Šæ”¾blob URL
                        window.URL.revokeObjectURL(blobUrl);
                        uni.hideLoading();
                        uni.showToast({
                            title: '下载成功',
                            icon: 'success',
                            duration: 2000
                        });
                    })
                    .catch(error => {
                        console.error('Fetch下载失败:', error);
                        uni.hideLoading();
                        if (error.message.includes('404')) {
                            uni.showModal({
                                title: '文件不存在',
                                content: `该附件在FTP服务器上不存在`,
                                showCancel: false
                            });
                        } else {
                            uni.showModal({
                                title: '下载失败',
                                content: `下载失败: ${error.message}`,
                                showCancel: false
                            });
                        }
                    });
            },
            // åœ¨APP中下载文件
            downloadFileInApp(url, fileName) {
                // #ifdef APP-PLUS
                uni.showLoading({ title: '从FTP服务器下载中...' });
                // Android èŽ·å–å­˜å‚¨è·¯å¾„
                const saveDir = plus.os.name === 'Android' ? plus.io.convertLocalFileSystemURL('_downloads/') : plus.io.convertLocalFileSystemURL('_documents/');
                const filePath = `${saveDir}${fileName}`;
                const downloadTask = uni.downloadFile({
                    url: url,
                    filePath: filePath, // æŒ‡å®šä¿å­˜è·¯å¾„
                    success: (res) => {
                        uni.hideLoading();
                        if (res.statusCode === 200) {
                            const fileInfo = {
                                name: fileName,
                                path: res.filePath || filePath,
                                tempPath: res.tempFilePath
                            };
                            uni.showModal({
                                title: '下载成功',
                                content: `文件已保存到:${fileInfo.path}`,
                                showCancel: true,
                                confirmText: '打开文件',
                                cancelText: '确定',
                                success: (modalRes) => {
                                    if (modalRes.confirm) {
                                        // ç”¨æˆ·é€‰æ‹©æ‰“开文件
                                        this.openFileInApp(fileInfo);
                                    }
                                }
                            });
                        } else if (res.statusCode === 404) {
                            uni.showModal({
                                title: '文件不存在',
                                content: `该附件在FTP服务器上不存在`,
                                showCancel: false
                            });
                        } else {
                            uni.showModal({
                                title: '下载失败',
                                content: `状态码:${res.statusCode}`,
                                showCancel: false
                            });
                        }
                    },
                    fail: (error) => {
                        uni.hideLoading();
                        console.error('下载失败:', error);
                        uni.showModal({
                            title: '下载失败',
                            content: `网络错误:${error.errMsg}`,
                            showCancel: false
                        });
                    }
                });
                // ç›‘听下载进度
                downloadTask.onProgressUpdate((res) => {
                    const progress = Math.round(res.progress);
                    uni.showLoading({
                        title: `下载中 ${progress}%`,
                        mask: true
                    });
                });
                // #endif
                // #ifdef MP
                // å°ç¨‹åºçŽ¯å¢ƒçš„ç®€åŒ–å®žçŽ°
                uni.showLoading({ title: '下载中...' });
                uni.downloadFile({
                    url: url,
                    success: (res) => {
                        uni.hideLoading();
                        if (res.statusCode === 200) {
                            uni.showToast({ title: '下载完成', icon: 'success' });
                        }
                    },
                    fail: (error) => {
                        uni.hideLoading();
                        uni.showModal({ title: '下载失败', content: error.errMsg, showCancel: false });
                    }
                });
                // #endif
            },
            // APP中打开文件
            openFileInApp(fileInfo) {
                // #ifdef APP-PLUS
                if (typeof plus !== 'undefined') {
                    const filePath = fileInfo.path || fileInfo.tempPath;
                    // å°è¯•打开文件
                    plus.runtime.openFile(filePath, {}, (error) => {
                        console.error('打开文件失败:', error);
                        uni.showModal({
                            title: '无法打开',
                            content: '系统中没有找到能打开此文件的应用程序',
                            showCancel: false
                        });
                    });
                }
                // #endif
            },
        }
@@ -1368,9 +1867,11 @@
        font-family: 'Microsoft YaHei', 'Segoe UI', sans-serif;
        max-width: 1000px;
        margin: 0 auto;
        padding: 20px;
        padding: 20px 20px 160px 20px; /* åº•部增加padding为固定按钮留空间 */
        background-color: #fff;
        box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
        min-height: 100vh;
        position: relative;
    }
    /* å¤´éƒ¨æ ·å¼ */
@@ -1491,7 +1992,60 @@
        background-color: #f1f5f9;
    }
    /* æŒ‰é’®æ ·å¼ */
    /* å›ºå®šåº•部按钮样式 */
    .fixed-action-buttons {
        position: fixed;
        bottom: 0;
        left: 0;
        right: 0;
        background-color: #fff;
        box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
        padding: 10px 15px 20px 15px;
        z-index: 100;
        display: flex;
        flex-direction: column;
        gap: 8px;
        max-height: 150px;
        overflow-y: auto;
    }
    .action-btn {
        background-color: #ecf0f1;
        color: #34495e;
        padding: 12px 15px;
        border: none;
        border-radius: 6px;
        cursor: pointer;
        font-size: 14px;
        font-weight: 500;
        transition: all 0.3s ease;
        text-align: center;
        min-height: 44px;
        display: flex;
        align-items: center;
        justify-content: center;
    }
    .action-btn:hover {
        background-color: #d5dbdb;
        transform: translateY(-1px);
    }
    .action-btn.primary {
        background-color: #3498db;
        color: #fff;
    }
    .action-btn.primary:hover {
        background-color: #2980b9;
    }
    /* å†…容包装器,为底部按钮留出空间 */
    .content-wrapper {
        height: 20px; /* é¢å¤–的空白区域 */
    }
    /* åŽŸæœ‰æŒ‰é’®æ ·å¼ä¿æŒå…¼å®¹ */
    .action-buttons {
        display: flex;
        gap: 10px;
@@ -1600,7 +2154,7 @@
        display: flex;
        justify-content: center;
        align-items: center;
        z-index: 10;
        z-index: 1000; /* æé«˜å±‚级,确保在固定按钮上方 */
    }
    /* å¼¹çª—整体美化 */
@@ -1612,6 +2166,9 @@
        border: none;
        position: relative;
        min-width: 260px;
        z-index: 1001; /* ç¡®ä¿å¼¹çª—内容在最上层 */
        max-height: 80vh; /* é™åˆ¶æœ€å¤§é«˜åº¦ï¼Œé¿å…è¢«åº•部按钮遮挡 */
        overflow-y: auto; /* å†…容过多时可滚动 */
    }
    .attachment-popup-title {
        font-size: 22px;
@@ -1690,6 +2247,164 @@
        background: linear-gradient(90deg,#bdbdbd 0%,#e0e0e0 100%);
        color: #1976d2;
    }
    /* é™„件详情页面的操作按钮 */
    .attachment-actions-detail {
        margin: 20px 0;
        display: flex;
        gap: 12px;
        justify-content: center;
        flex-wrap: wrap;
    }
    .attachment-action-btn {
        padding: 10px 20px;
        border: none;
        border-radius: 8px;
        font-size: 14px;
        font-weight: 600;
        cursor: pointer;
        transition: all 0.3s ease;
        display: flex;
        align-items: center;
        justify-content: center;
        min-width: 120px;
        box-shadow: 0 2px 4px rgba(0,0,0,0.1);
    }
    .attachment-action-btn.preview-btn {
        background: linear-gradient(135deg, #4CAF50, #45a049);
        color: white;
    }
    .attachment-action-btn.preview-btn:hover {
        background: linear-gradient(135deg, #45a049, #3d8b40);
        transform: translateY(-1px);
        box-shadow: 0 4px 8px rgba(0,0,0,0.15);
    }
    .attachment-action-btn.download-btn {
        background: linear-gradient(135deg, #2196F3, #1976D2);
        color: white;
    }
    .attachment-action-btn.download-btn:hover {
        background: linear-gradient(135deg, #1976D2, #1565C0);
        transform: translateY(-1px);
        box-shadow: 0 4px 8px rgba(0,0,0,0.15);
    }
    /* æ–‡ä»¶é¢„览弹窗样式 */
    .file-preview-popup {
        width: 80vw;
        max-width: 600px;
        max-height: 70vh;
        display: flex;
        flex-direction: column;
    }
    /* APP环境适配 */
    /* #ifdef APP-PLUS */
    .file-preview-popup {
        width: 85vw;
        max-height: 75vh;
    }
    .file-preview-content {
        max-height: 350px;
    }
    /* #endif */
    .file-preview-title {
        font-size: 18px;
        font-weight: 700;
        color: #222;
        margin-bottom: 8px;
        text-align: center;
        word-break: break-all;
    }
    .file-preview-divider {
        height: 1px;
        background: linear-gradient(90deg,#e0e7ef 0%,#f5f7fa 100%);
        margin-bottom: 16px;
    }
    .file-preview-content {
        flex: 1;
        max-height: 400px;
        overflow-y: auto;
        background: #f8fafc;
        border-radius: 8px;
        padding: 16px;
        margin-bottom: 16px;
        border: 1px solid #e2e8f0;
    }
    .file-preview-content pre {
        font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
        font-size: 12px;
        line-height: 1.5;
        color: #2d3748;
        white-space: pre-wrap;
        word-wrap: break-word;
        margin: 0;
    }
    /* å›¾ç‰‡é¢„览样式 */
    .image-preview-container {
        display: flex;
        justify-content: center;
        align-items: center;
        min-height: 200px;
    }
    /* ä¸æ”¯æŒæ–‡ä»¶ç±»åž‹çš„æç¤ºæ ·å¼ */
    .unsupported-preview {
        text-align: center;
        padding: 40px 20px;
        color: #666;
    }
    .unsupported-icon {
        font-size: 48px;
        margin-bottom: 16px;
    }
    .unsupported-text {
        font-size: 16px;
        font-weight: 600;
        color: #333;
        margin-bottom: 8px;
    }
    .unsupported-hint {
        font-size: 14px;
        color: #999;
        line-height: 1.4;
    }
    .file-preview-actions {
        display: flex;
        gap: 12px;
        justify-content: center;
    }
    .file-preview-btn {
        padding: 8px 20px;
        border: none;
        border-radius: 6px;
        font-size: 14px;
        font-weight: 600;
        cursor: pointer;
        transition: all 0.3s ease;
        display: flex;
        align-items: center;
        justify-content: center;
        min-width: 120px;
    }
    .file-preview-btn.download-btn {
        background: linear-gradient(135deg, #2196F3, #1976D2);
        color: white;
    }
    .file-preview-btn.download-btn:hover {
        background: linear-gradient(135deg, #1976D2, #1565C0);
        transform: translateY(-1px);
    }
    .file-preview-btn.close-btn {
        background: linear-gradient(135deg, #e0e0e0, #bdbdbd);
        color: #444;
    }
    .file-preview-btn.close-btn:hover {
        background: linear-gradient(135deg, #bdbdbd, #9e9e9e);
        transform: translateY(-1px);
    }
    /* åˆ—表弹窗美化(保留原有) */
    .attachment-list {
        padding: 0;
@@ -1702,24 +2417,36 @@
        display: flex;
        align-items: center;
        justify-content: space-between;
        padding: 8px 0;
        padding: 12px 0;
        border-bottom: 1px solid #f0f0f0;
    }
    .attachment-name {
    .attachment-info {
        flex: 1;
        margin-right: 10px;
    }
    .attachment-name {
        color: #3498db;
        cursor: pointer;
        font-weight: 500;
        transition: color 0.2s;
        margin-right: 10px;
        display: block;
        margin-bottom: 4px;
    }
    .attachment-name:hover {
        color: #217dbb;
        text-decoration: underline;
    }
    .attachment-meta {
        font-size: 12px;
    }
    .attachment-type {
        color: #7f8c8d;
        font-style: italic;
    }
    .attachment-actions {
        display: flex;
        gap: 8px;
        flex-shrink: 0;
    }
    .attachment-list .secondary-btn {
        padding: 4px 10px;
@@ -1734,6 +2461,15 @@
        background: #e6f0fa;
        color: #1976d2;
    }
    .preview-btn {
        background: #e8f5e8 !important;
        color: #2e7d2e !important;
        border-color: #a5d6a5 !important;
    }
    .preview-btn:hover {
        background: #d4eecc !important;
        color: #1e5f1e !important;
    }
    .attachment-popup-close {
        margin-top: 18px;
        width: 100%;
store/index.js
@@ -10,9 +10,10 @@
            networkFlag:'内网', 
            serverURLInt:'http://192.168.11.251:10055',//服务器体检 10.0.1.104:10054
            serverURL:'http://localhost:10055',//本地调试地址
            serverAPI:'http://localhost:5184/api',//当前正在使用的服务器,默认为外网  localhost
            // serverAPI:'http://localhost:5184/api',//当前正在使用的服务器,默认为外网  localhost
            //serverAPI:'http://192.168.1.22:10054/api',//内网 
            //serverAPI:'http://36.26.21.214:10054/api',
            serverAPI:'http://36.26.21.214:10054/api',
            ftpServer:'ftp://36.26.21.214',//FTP服务器地址
        }
    },
    mutations: {