210 lines
6.5 KiB
Markdown
210 lines
6.5 KiB
Markdown
|
|
# 麒麟系统浏览器文件下载兼容性修复方案
|
|||
|
|
|
|||
|
|
## 一、问题现象
|
|||
|
|
|
|||
|
|
**下载成功,但文件打不开**。文件已保存到本地,但用对应软件打开时提示损坏或格式错误。
|
|||
|
|
|
|||
|
|
## 二、根因分析
|
|||
|
|
|
|||
|
|
### 可能原因 1:`URL.revokeObjectURL` 释放过早(文件内容损坏)
|
|||
|
|
|
|||
|
|
当前代码(第300-313行):
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
function downloadFile(url, filename) {
|
|||
|
|
fetch(url)
|
|||
|
|
.then((response) => response.blob())
|
|||
|
|
.then((blob) => {
|
|||
|
|
const link = document.createElement("a");
|
|||
|
|
link.href = URL.createObjectURL(blob);
|
|||
|
|
link.download = filename;
|
|||
|
|
document.body.appendChild(link);
|
|||
|
|
link.click();
|
|||
|
|
document.body.removeChild(link);
|
|||
|
|
URL.revokeObjectURL(link.href); // 同步释放,可能过早
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`link.click()` 在麒麟浏览器中是异步的,同步调用 `revokeObjectURL` 会在 Blob 数据写入磁盘前就销毁它,导致文件内容不完整。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 可能原因 2:文件名无后缀 / 后缀被篡改
|
|||
|
|
|
|||
|
|
分析文件名传递链路:
|
|||
|
|
|
|||
|
|
**上传时**,`handlerSuccess` 保存的是 `{ id, name }`,`name` 来自浏览器原始文件名(带后缀),这部分没问题。
|
|||
|
|
|
|||
|
|
**但回显时**,watch 中第183-188行存在一个关键问题:
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
// 当 modelValue 元素是字符串(非对象)时:
|
|||
|
|
} else {
|
|||
|
|
return {
|
|||
|
|
url: String(`/mosty-api/mosty-base/minio/image/download/` + el || ""),
|
|||
|
|
id: el
|
|||
|
|
// ← 没有 name 属性!
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
此时 `file.name` 为 `undefined`,传入 `downloadFile(file.url, file.name)`:
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
link.download = undefined; // 文件名丢失
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**麒麟浏览器的行为**:当 `download` 属性为空或 `undefined` 时,浏览器会:
|
|||
|
|
- 从 URL 路径提取文件名(如 `/minio/image/download/abc123` → 文件名变成 `abc123`,**无后缀**)
|
|||
|
|
- 或根据 Blob 的 `type` 自动添加后缀(如服务端返回的 Content-Type 是 `application/json` → 强制加 `.json` 后缀)
|
|||
|
|
|
|||
|
|
**结果**:一个 `.docx` 文件下载后变成了 `.json` 或无后缀文件,自然打不开。
|
|||
|
|
|
|||
|
|
**验证方法**:在麒麟系统上下载一个文件,查看下载后的文件名是否和原始文件名一致(包括后缀)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 可能原因 3:麒麟系统安全机制拦截
|
|||
|
|
|
|||
|
|
麒麟系统(基于 Linux)自带安全中心,可能触发以下行为:
|
|||
|
|
|
|||
|
|
| 安全机制 | 行为 | 结果 |
|
|||
|
|
|----------|------|------|
|
|||
|
|
| 文件隔离 | 将下载的文件标记为不可信,移到隔离区 | 文件存在但被锁,其他程序无法读取 |
|
|||
|
|
| 执行权限 | 给文件添加可执行标记,或删除可执行标记 | 程序拒绝打开带危险标记的文件 |
|
|||
|
|
| 杀毒扫描 | 实时扫描下载文件,误报则隔离 | 文件被移动或内容被修改 |
|
|||
|
|
| WINE 兼容层 | 试图用 WINE 打开 Windows 格式文件 | 文件关联错误,打开方式不对 |
|
|||
|
|
|
|||
|
|
**验证方法**:
|
|||
|
|
1. 在麒麟系统终端执行 `ls -la` 查看下载文件是否有特殊权限标记
|
|||
|
|
2. 检查 `/tmp` 或隔离区目录是否有被拦截的文件
|
|||
|
|
3. 暂时关闭麒麟安全中心,重新下载测试
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三、修复方案
|
|||
|
|
|
|||
|
|
### 方案 A:综合修复(推荐)
|
|||
|
|
|
|||
|
|
同时解决原因1和原因2,并在下载失败时给出明确提示:
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
import { saveAs } from 'file-saver'
|
|||
|
|
|
|||
|
|
// 补全文件名后缀
|
|||
|
|
function ensureFilename(file) {
|
|||
|
|
if (file.name) return file.name
|
|||
|
|
// name 丢失时,从 URL 中尝试提取,或使用默认名
|
|||
|
|
const urlId = file.url?.split('/').pop()
|
|||
|
|
return urlId ? `文件_${urlId}` : '未命名文件'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function downloadFile(url, filename) {
|
|||
|
|
fetch(url)
|
|||
|
|
.then((response) => {
|
|||
|
|
if (!response.ok) {
|
|||
|
|
throw new Error(`下载失败: ${response.status}`)
|
|||
|
|
}
|
|||
|
|
return response.blob()
|
|||
|
|
})
|
|||
|
|
.then((blob) => {
|
|||
|
|
// saveAs 内部处理了 Blob 释放时序,不会过早 revoke
|
|||
|
|
saveAs(blob, filename)
|
|||
|
|
})
|
|||
|
|
.catch((error) => {
|
|||
|
|
console.error('下载失败:', error)
|
|||
|
|
ElMessage.error('文件下载失败,请重试')
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleDownload = (file) => {
|
|||
|
|
if (file?.response?.data) {
|
|||
|
|
window.open(file.response.data)
|
|||
|
|
} else if (file?.url) {
|
|||
|
|
const filename = ensureFilename(file)
|
|||
|
|
downloadFile(file.url, filename)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**改动内容**:
|
|||
|
|
1. 引入 `file-saver`(项目已有依赖)→ 解决 Blob 释放过早
|
|||
|
|
2. `ensureFilename` 补全文件名 → 解决后缀丢失
|
|||
|
|
3. 响应状态校验 → 发现服务端错误时提示用户
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 方案 B:仅修复 Blob 释放时序(最小改动)
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
function downloadFile(url, filename) {
|
|||
|
|
fetch(url)
|
|||
|
|
.then((response) => response.blob())
|
|||
|
|
.then((blob) => {
|
|||
|
|
const blobUrl = URL.createObjectURL(blob);
|
|||
|
|
const link = document.createElement("a");
|
|||
|
|
link.href = blobUrl;
|
|||
|
|
link.download = filename;
|
|||
|
|
document.body.appendChild(link);
|
|||
|
|
link.click();
|
|||
|
|
document.body.removeChild(link);
|
|||
|
|
// 延迟释放,确保浏览器完成 Blob 数据拷贝
|
|||
|
|
setTimeout(() => URL.revokeObjectURL(blobUrl), 3000);
|
|||
|
|
})
|
|||
|
|
.catch((error) => console.error("下载失败:", error));
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**改动量**:1 行。但未修复文件名丢失问题。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 方案 C:window.open 直接下载
|
|||
|
|
|
|||
|
|
```javascript
|
|||
|
|
const handleDownload = (file) => {
|
|||
|
|
if (file?.url) {
|
|||
|
|
window.open(file.url, "_blank");
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**前提**:后端接口需设置 `Content-Disposition: attachment; filename="xxx.docx"` 响应头,浏览器才能正确处理文件名和下载行为。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四、排查步骤
|
|||
|
|
|
|||
|
|
建议按以下顺序验证,定位到底是哪个原因:
|
|||
|
|
|
|||
|
|
1. **检查文件名**:在麒麟系统下载后,文件名是否带正确后缀(如 `.docx`、`.pdf`)?
|
|||
|
|
- 后缀丢失/被改 → **原因2(文件名问题)**
|
|||
|
|
- 后缀正确 → 排除原因2
|
|||
|
|
|
|||
|
|
2. **检查文件大小**:下载的文件大小是否和服务端一致?
|
|||
|
|
- 文件明显偏小或0字节 → **原因1(Blob 释放过早)**
|
|||
|
|
- 大小一致 → 排除原因1
|
|||
|
|
|
|||
|
|
3. **关闭安全中心测试**:暂时关闭麒麟安全中心,重新下载
|
|||
|
|
- 能正常打开 → **原因3(安全拦截)**
|
|||
|
|
- 仍打不开 → 排除原因3
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五、方案对比
|
|||
|
|
|
|||
|
|
| | 方案 A 综合修复 | 方案 B 延迟释放 | 方案 C window.open |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| 修复原因1(Blob释放) | ✅ | ✅ | 不涉及 |
|
|||
|
|
| 修复原因2(文件名后缀) | ✅ | ❌ | 取决于后端 |
|
|||
|
|
| 处理原因3(安全拦截) | ❌ 需系统配置 | ❌ | ❌ |
|
|||
|
|
| 改动量 | ~15行 | 1行 | 最小 |
|
|||
|
|
|
|||
|
|
**建议**:先执行排查步骤确认根因,再选择对应方案。如果原因1和2同时存在,直接用方案A。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**文档版本**: v3.0
|
|||
|
|
**创建日期**: 2026-04-24
|