HarmonyOS 鸿蒙Next中各位大神,帮我看看为什么这段代码预览的总是同一份内容?感谢!感谢!
HarmonyOS 鸿蒙Next中各位大神,帮我看看为什么这段代码预览的总是同一份内容?感谢!感谢!
import { AxiosError, AxiosProgressEvent, AxiosResponse, FormData } from "@ohos/axios";
import http, { initializeCSRF } from "../common/HttpUtil";
import { promptAction } from "@kit.ArkUI";
import { picker } from '@kit.CoreFileKit';
import { fileIo as fs } from '@kit.CoreFileKit'
import { common, Want } from '@kit.AbilityKit'
import { fileUri } from '@kit.CoreFileKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { filePreview } from "@kit.PreviewKit";
// 在组件外部定义接口
interface DocumentInfo {
uri: string;
name: string;
mimeType: string;
}
@Builder
export function AddFiles() {
addfiles()
}
const MIME_MAP: Record<string, string> = {
'pdf': 'application/pdf',
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
}
@Component
struct addfiles {
@Consume pathStack: NavPathStack
@State space: number = 8;
@State arrowPosition: ArrowPosition = ArrowPosition.END;
@State standard_no: string = ''
@State standard_name: string = ''
@State category: string = '请选择文件类型'
@State category_index: number = 0
@State professional: string = '请选择专业'
@State profess_index: number = 0
@State item: string = '请选择检测项目'
@State item_index: number = 0
@State fileUri: string = ''
@State fullFileName: string = ''
@State mimeType: string = ''
@State fileUrl: string = '' //上传成功后服务器返回的URL
@State fileName: string = '' //保存文件名,用于预览时获取MIME
@State isUploading: boolean = false
@State isPreviewing: boolean = false // ✅ 添加预览状态
// 存储选中的文档信息
@State selectedDocument: DocumentInfo | null = null
async aboutToAppear() {
await initializeCSRF()
}
//根据文件名获取MIME类型
getMimeType(fileName: string): string {
//提取文件扩展名例如:.pdf
const ext = fileName.split('.').pop()?.toLowerCase()
//从映射表中查找对应的MIME类型
if (ext && MIME_MAP[ext]) {
return MIME_MAP[ext]
}
//默认返回通用二进制流类型
return 'application/octet-stream'
}
// 解码文件名(处理URL编码)
decodeFileName(encodedName: string): string {
try {
return decodeURIComponent(encodedName);
} catch (e) {
return encodedName;
}
}
// 从URI中提取文件名(处理特殊格式)
extractFileNameFromUri(uri: string): string {
// URI格式:file://docs/storage/Users/currentUser/Documents/文件名.pdf
// 提取最后一个斜杠后的部分
let fileName = uri.split('/').pop() || '未知文件';
// 解码URL编码的字符(如 %20 转为空格)
return this.decodeFileName(fileName);
}
async selectAndUpload() {
try {
//获取上下文
let context = getContext(this) as common.Context
//创建文档选择器
let documentPicker = new picker.DocumentViewPicker(context)
//配置选择选项(限制文件类型)
const options = new picker.DocumentSelectOptions()
options.maxSelectNumber = 1 //一次只能选择1个文件
//限制显示的文件类型
options.fileSuffixFilters = ['.pdf', '.doc', '.docx']
const uris = await documentPicker.select(options)
if (uris && uris.length > 0) {
this.fileUri = uris[0]
console.info('原始URI:', this.fileUri)
// 提取并解码文件名
this.fullFileName = this.extractFileNameFromUri(this.fileUri);
this.mimeType = this.getMimeType(this.fullFileName);
// 7. 保存选中的文档信息
//先给fileName赋值,这样就能实时看到文件名
this.fileName = this.fullFileName
this.selectedDocument = null
this.selectedDocument = {
uri: this.fileUri,
name: this.fileName,
mimeType: this.mimeType,
};
// 更新界面显示
console.info(`选择文件成功:${this.fullFileName}`);
console.info(`MIME类型:${this.mimeType}`);
//开始上传
// await this.uploadFile(fileUri,fullFileName,mimeType)
} else {
console.info('用户取消了选择')
}
} catch (err) {
let error = err as BusinessError
console.error(`选择文件失败:${error.message}`)
promptAction.showToast({ message: '选择文件失败,请重试' })
}
}
// 读取文件内容的辅助函数
async readFileContent(uri: string): Promise<ArrayBuffer> {
try {
// 方法1:尝试直接使用 fs.openSync
let file = fs.openSync(uri, fs.OpenMode.READ_ONLY);
let stat = fs.statSync(file.fd);
let buffer = new ArrayBuffer(stat.size);
fs.readSync(file.fd, buffer);
fs.closeSync(file);
return buffer;
} catch (err) {
console.error('直接读取失败,尝试其他方法:', err);
// 方法2:对于 file://docs/ 格式的URI,需要先复制到应用沙箱
// 但这里已经是URI格式,需要特殊处理
throw new Error('无法读取文件内容,请确保文件存在且可访问');
}
}
async uploadFile(uri: string, fileName: string, mimeType: string) {
this.isUploading = true;
let sandboxPath = '';
try {
// 1. 获取应用上下文
let uiContext = getContext(this) as common.UIAbilityContext;
// 2. 处理文件名(移除可能的路径分隔符)
let safeFileName = fileName.replace(/[\\/]/g, '_');
sandboxPath = `${uiContext.cacheDir}/${safeFileName}`;
console.info('源URI:', uri);
console.info('目标沙箱路径:', sandboxPath);
// 3. 复制文件到应用沙箱(关键修复点)
try {
// 对于 file://docs/ 格式的URI,需要先通过 fs.open 获取文件描述符
// 然后复制内容
let srcFile = fs.openSync(uri, fs.OpenMode.READ_ONLY);
let destFile = fs.openSync(sandboxPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
// 分批复制文件内容
const bufferSize = 8192; // 8KB 缓冲区
let buffer = new ArrayBuffer(bufferSize);
let totalBytes = 0;
while (true) {
let bytesRead = fs.readSync(srcFile.fd, buffer);
if (bytesRead <= 0) {
break;
}
// 只写入实际读取的字节数
let writeBuffer = buffer.slice(0, bytesRead);
fs.writeSync(destFile.fd, writeBuffer);
totalBytes += bytesRead;
}
fs.closeSync(srcFile);
fs.closeSync(destFile);
console.info(`文件复制成功,大小:${totalBytes} 字节`);
} catch (copyErr) {
console.error('文件复制失败:', copyErr);
// 尝试使用 fileUri 模块的 getUriFromPath 方法
try {
let fileInfo = await fileUri.getUriFromPath(uri);
console.info('通过 fileUri 获取文件信息:', fileInfo);
// 如果获取成功,使用返回的路径
if (fileInfo && fileInfo) {
fs.copyFileSync(fileInfo, sandboxPath);
console.info('使用 fileUri 方式复制成功');
}
} catch (uriErr) {
console.error('fileUri 方式也失败:', uriErr);
throw new Error('无法读取选择的文件');
}
}
// 4. 读取文件内容为 ArrayBuffer
let file = fs.openSync(sandboxPath, fs.OpenMode.READ_ONLY);
let stat = fs.statSync(file.fd);
let buffer = new ArrayBuffer(stat.size);
fs.readSync(file.fd, buffer);
fs.closeSync(file);
console.info(`文件读取成功,大小:${stat.size} 字节`);
// 5. 创建 FormData 并添加文件
let formData = new FormData();
formData.append('file', buffer, safeFileName);
formData.append('standard_no', this.standard_no);
formData.append('standard_name', this.standard_name);
formData.append('category', this.category);
formData.append('professional', this.professional);
formData.append('item', this.item);
// 6. 发送上传请求
const response: AxiosResponse = await http.post(
'addfiles',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
},
timeout: 60000, // 60秒超时,大文件需要更长时间
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
if (progressEvent.total) {
let percent = Math.ceil((progressEvent.loaded / progressEvent.total) * 100);
console.info(`上传进度:${percent}%`);
}
}
}
);
// 7. 处理响应
if (response.status === 200) {
console.info('上传成功,响应数据:', response.data);
this.fileUrl = response.data.file_url || response.data;
promptAction.showToast({ message: `${fileName} 上传成功` });
// 可选:上传成功后返回上一页
// setTimeout(() => {
// this.pathStack.pop();
// }, 1500);
} else {
console.error('上传失败,状态码:', response.status);
promptAction.showToast({ message: `上传失败:${response.status}` });
}
} catch (err) {
console.error('上传失败:', err);
let errorMessage = err instanceof Error ? err.message : '未知错误';
promptAction.showToast({ message: `上传失败:${errorMessage}` });
} finally {
this.isUploading = false;
// 清理临时文件
if (sandboxPath) {
try {
fs.accessSync(sandboxPath);
fs.unlinkSync(sandboxPath);
console.info('临时文件已清理');
} catch (e) {
console.info('临时文件不存在或清理失败');
}
}
}
}
async previewDocument() {
console.info('========== 预览前 selectedDocument ==========');
console.info(JSON.stringify(this.selectedDocument));
if (!this.selectedDocument) {
promptAction.showToast({ message: '请先选择文件' });
return;
}
if (this.isPreviewing) {
promptAction.showToast({ message: '正在预览中,请稍后' });
return;
}
this.isPreviewing = true;
let sandboxPath = '';
try {
let context = getContext(this) as common.Context;
let uiContext = getContext(this) as common.UIAbilityContext;
let fileName = this.selectedDocument.name;
let fileUri = this.selectedDocument.uri;
let mimeType = this.selectedDocument.mimeType;
console.info(`========== 开始预览文档 ==========`);
console.info(`文件名:${fileName}`);
console.info(`文件URI:${fileUri}`);
console.info(`MIME类型:${mimeType}`);
// 清理文件名中的特殊字符
let cleanFileName = fileName
.replace(/[\\/]/g, '_')
.replace(/\s/g, '_')
.replace(/[::]/g, '_')
.replace(/[??]/g, '_')
.replace(/[()()]/g, '_');
let timestamp = Date.now();
sandboxPath = `${uiContext.cacheDir}/preview_${timestamp}_${cleanFileName}`;
console.info(`临时文件路径:${sandboxPath}`);
// 复制文件到沙箱
let srcFile: fs.File | null = null;
let destFile: fs.File | null = null;
try {
srcFile = fs.openSync(fileUri, fs.OpenMode.READ_ONLY);
console.info('源文件打开成功,fd:', srcFile.fd);
destFile = fs.openSync(sandboxPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
const bufferSize = 8192;
let buffer = new ArrayBuffer(bufferSize);
let totalBytes = 0;
while (true) {
let bytesRead = fs.readSync(srcFile.fd, buffer);
if (bytesRead <= 0) break;
let writeBuffer = buffer.slice(0, bytesRead);
fs.writeSync(destFile.fd, writeBuffer);
totalBytes += bytesRead;
}
console.info(`文件复制成功,大小:${totalBytes} 字节`);
} catch (copyErr) {
console.error('文件复制失败:', copyErr);
throw new Error(`文件复制失败:${copyErr.message}`);
} finally {
if (srcFile) try { fs.closeSync(srcFile); } catch (e) {}
if (destFile) try { fs.closeSync(destFile); } catch (e) {}
}
// 验证文件
let stat = fs.statSync(sandboxPath);
console.info(`复制后文件大小:${stat.size} 字节`);
if (stat.size === 0) {
throw new Error('复制后的文件大小为0');
}
// ========== 方法1:使用 filePreview ==========
try {
let previewInfo: filePreview.PreviewInfo = {
title: fileName,
uri: sandboxPath,
mimeType: mimeType
};
console.info('调用 canPreview 检查...');
let canPreview = await filePreview.canPreview(context, sandboxPath);
console.info(`canPreview 结果:${canPreview}`);
if (canPreview) {
console.info('调用 openPreview...');
await filePreview.openPreview(context, previewInfo);
console.info('预览器已成功打开');
promptAction.showToast({ message: '正在打开预览...' });
return; // 成功,结束
} else {
console.warn('系统不支持预览此文件类型');
// 继续尝试其他方法
}
} catch (previewErr) {
console.error('filePreview 预览失败:', previewErr);
// 继续尝试其他方法
}
// ========== 方法2:使用系统应用打开 ==========
try {
console.info('尝试使用系统应用打开...');
let fileUriToOpen = `file://${sandboxPath}`;
let want: Want = {
action: 'ohos.want.action.viewData',
uri: fileUriToOpen,
type: mimeType,
flags: 0x10000000
};
await (getContext(this) as common.UIAbilityContext).startAbility(want);
console.info('系统应用已启动');
promptAction.showToast({ message: '正在打开文件...' });
return;
} catch (abilityErr) {
console.error('系统应用打开失败:', abilityErr);
}
// ========== 方法3:提示用户手动打开 ==========
console.error('所有预览方法均失败');
promptAction.showToast({
message: `无法预览,请使用文件管理器打开:${fileName}`,
duration: 3000
});
} catch (err) {
let error = err as BusinessError;
console.error(`预览流程异常:${error.message}`);
promptAction.showToast({ message: `预览失败:${error.message}` });
} finally {
this.isPreviewing = false;
// 清理临时文件(延迟10秒)
if (sandboxPath) {
setTimeout(() => {
try {
if (fs.accessSync(sandboxPath)) {
fs.unlinkSync(sandboxPath);
console.info('临时文件已清理');
}
} catch (e) {}
}, 30000);
}
console.info(`========== 预览流程结束 ==========`);
}
}
build() {
NavDestination() {
Column({ space: 5 }) {
Row() {
Text('新增文件').fontSize(20).fontWeight(FontWeight.Bold).offset({ left: 20 })
}.width('100%').justifyContent(FlexAlign.Start).margin({ bottom: 10 })
Row({ space: 10 }) {
Text('文件类型').width(65).textAlign(TextAlign.Center)
Text('|')
Select([{ value: '国外标准', icon: $r('app.media.wenjianselected') },
{ value: '国家标准', icon: $r('app.media.wenjianselected') },
{ value: '国家标准', icon: $r('app.media.wenjianselected') },
{ value: '行业标准', icon: $r('app.media.wenjianselected') },
{ value: '审核清单', icon: $r('app.media.wenjianselected') },
{ value: '通用文件', icon: $r('app.media.wenjianselected') },
{ value: '一层次文件', icon: $r('app.media.wenjianselected') },
{ value: '二层次文件', icon: $r('app.media.wenjianselected') },
{ value: '三层次文件', icon: $r('app.media.wenjianselected') },
{ value: '其他文件', icon: $r('app.media.wenjianselected') },
])
.selected(this.category_index)
.value(this.category)
.font({ size: 16, weight: 500 })
.fontColor('#182431')
.backgroundColor(Color.Transparent)
.selectedOptionFont({ size: 16, weight: 400 })
.optionFont({ size: 16, weight: 400 })
.space(this.space)
.arrowPosition(this.arrowPosition)
.menuAlign(MenuAlignType.START, { dx: 0, dy: 0 })
.optionWidth(200)
.optionHeight(300)
.onSelect((category_index: number, category?: string | undefined) => {
console.info('Select:' + category_index);
this.category_index = category_index;
if (category) {
this.category = category;
console.log(category)
}
})
.layoutWeight(1)
.avoidance(AvoidanceMode.COVER_TARGET)
}.backgroundColor('#f4f5f6').borderRadius(15).padding({ left: 10 })
Row({ space: 10 }) {
Text('文件编号').width(65).textAlign(TextAlign.Center)
Text('|')
TextInput({ placeholder: '请输入文件编号', text: $$this.standard_no })
.fontColor('#182431')
.layoutWeight(1)
.backgroundColor('#f4f5f6')
}.backgroundColor('#f4f5f6').borderRadius(15).padding({ left: 10 })
Row({ space: 10 }) {
Text('文件名称').width(65).textAlign(TextAlign.Center)
Text('|')
TextInput({ placeholder: '请输入文件名称', text: $$this.standard_name })
.fontColor('#182431')
.layoutWeight(1)
.backgroundColor('#f4f5f6')
}.backgroundColor('#f4f5f6').borderRadius(15).padding({ left: 10 })
Row({ space: 10 }) {
Text('专 业').width(65).textAlign(TextAlign.Center)
Text('|')
Select([{ value: '通用', icon: $r('app.media.zhuanyezhuanyeke') },
{ value: '化学', icon: $r('app.media.zhuanyezhuanyeke') },
{ value: '力学', icon: $r('app.media.zhuanyezhuanyeke') },
{ value: '金相', icon: $r('app.media.zhuanyezhuanyeke') }])
.selected(this.profess_index)
.value(this.professional)
.font({ size: 16, weight: 500 })
.fontColor('#182431')
.backgroundColor(Color.Transparent)
.selectedOptionFont({ size: 16, weight: 400 })
.optionFont({ size: 16, weight: 400 })
.space(this.space)
.arrowPosition(this.arrowPosition)
.menuAlign(MenuAlignType.START, { dx: 0, dy: 0 })
.optionWidth(200)
.optionHeight(300)
.onSelect((profess_index: number, professional?: string | undefined) => {
console.info('Select:' + profess_index)
this.profess_index = profess_index
if (professional) {
this.professional = professional
console.log(this.professional)
}
})
.layoutWeight(1)
.avoidance(AvoidanceMode.COVER_TARGET)
}.backgroundColor('#f4f5f6').padding({ left: 10 }).borderRadius(15)
Row({ space: 10 }) {
Text('检测项目').width(65).textAlign(TextAlign.Center)
Text('|')
Select([{ value: '通用', icon: $r('app.media.xiangmu') },
{ value: '化学成分', icon: $r('app.media.xiangmu') },
{ value: '室温拉伸', icon: $r('app.media.xiangmu') },
{ value: '硬度', icon: $r('app.media.xiangmu') },
{ value: '渗层', icon: $r('app.media.xiangmu') },
{ value: '金相组织', icon: $r('app.media.xiangmu') },
{ value: '失效分析', icon: $r('app.media.xiangmu') }])
.selected(this.item_index)
.value(this.item)
.font({ size: 16, weight: 500 })
.fontColor('#182431')
.backgroundColor(Color.Transparent)
.selectedOptionFont({ size: 16, weight: 400 })
.optionFont({ size: 16, weight: 400 })
.space(this.space)
.arrowPosition(this.arrowPosition)
.menuAlign(MenuAlignType.START, { dx: 0, dy: 0 })
.optionWidth(200)
.optionHeight(300)
.onSelect((item_index: number, item?: string | undefined) => {
console.info('Select:' + item_index)
this.item_index = item_index
if (item) {
this.item = item
console.log(this.item)
}
})
.layoutWeight(1)
.avoidance(AvoidanceMode.COVER_TARGET)
}.backgroundColor('#f4f5f6').padding({ left: 10 }).borderRadius(15)
Row({ space: 10 }) {
Text('选择文件').width(65)
Text('|')
Row() {
Image($r('app.media.wenjianjia')).width(30).height(30)
}.layoutWeight(1).justifyContent(FlexAlign.Center).offset({ left: -10 })
.onClick(() => {
this.selectAndUpload()
})
}
.backgroundColor('#f4f5f6')
.width('100%')
.height(40)
.padding({ left: 10 })
.borderRadius(15)
if (this.selectedDocument){
Row({space:10}){
Text('已选文件').width(65)
Text('|')
Text(this.selectedDocument.name).layoutWeight(1).fontColor(Color.Blue)
.onClick(()=>{
this.previewDocument()
})
}.backgroundColor('#f4f5f6')
.width('100%')
.height(40)
.padding({ left: 10 })
.borderRadius(15)
}
Button('新增文件')
.width('100%')
.type(ButtonType.Capsule)
.borderRadius(10)
.margin({ top: 20 })
.enabled(this.standard_no && this.standard_name ? true : false)
.onClick(() => {
this.uploadFile(this.fileUri, this.fullFileName, this.mimeType)
})
}
.width('100%').height('100%')
.padding({ left: 20, right: 20 })
}.hideTitleBar(true)
}
}
更多关于HarmonyOS 鸿蒙Next中各位大神,帮我看看为什么这段代码预览的总是同一份内容?感谢!感谢!的实战教程也可以访问 https://www.itying.com/category-93-b0.html
开发者你好,
-
你这边的问题是因为在previewDocument方法中, 未将path转换为uri, 需要将 sandboxPath 转换为uri路径, 使用 getUriFromPath 转换后, 使用filePathUri即可正常打开:
let filePathUri: string = fileUri.getUriFromPath(sandboxPath)
-
你的previewDocument方法中参数定义存在问题, 根据参数定位范围就近原则, previewDocument方法中初始化的参数
let fileUri = this.selectedDocument.uri;会覆盖导包中的fileUri类, 导致方法uri转换出现问题, 需要重新定义 this.selectedDocument.uri .
import { fileUri } from '[@kit](/user/kit).CoreFileKit'
更多关于HarmonyOS 鸿蒙Next中各位大神,帮我看看为什么这段代码预览的总是同一份内容?感谢!感谢!的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html
问题已解决,感谢感谢,万分感谢!
好的开发者,后续有任何问题欢迎您随时提问~
开发者你好, 你这边的主要问题是通过 previewDocument 预览的文件每次都是同一个嘛? 你这边的两个方法打开的文件都是同一个嘛?
-
需要麻烦你确认下, 每次使用selectAndUpload选择文件之后, this.selectedDocument 对象值是否发生变化?
-
你这边可以打开应用沙箱路径, 确认下沙箱的catch目录下保存的缓存文件是否不一样.
import { AxiosError, AxiosProgressEvent, AxiosResponse, FormData } from "@ohos/axios";
import http, { initializeCSRF } from "../common/HttpUtil";
import { promptAction } from "@kit.ArkUI";
import { picker } from '@kit.CoreFileKit';
import { fileIo as fs } from '@kit.CoreFileKit'
import { common, Want } from '@kit.AbilityKit'
import { fileUri } from '@kit.CoreFileKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { filePreview } from "@kit.PreviewKit";
// 在组件外部定义接口
interface DocumentInfo {
uri: string;
name: string;
mimeType: string;
}
@Builder
export function AddFiles() {
addfiles()
}
const MIME_MAP: Record<string, string> = {
'pdf': 'application/pdf',
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
}
@Component
struct addfiles {
@Consume pathStack: NavPathStack
@State space: number = 8;
@State arrowPosition: ArrowPosition = ArrowPosition.END;
@State standard_no: string = ''
@State standard_name: string = ''
@State category: string = '请选择文件类型'
@State category_index: number = 0
@State professional: string = '请选择专业'
@State profess_index: number = 0
@State item: string = '请选择检测项目'
@State item_index: number = 0
@State fileUris: string = ''
@State fullFileName: string = ''
@State mimeType: string = ''
@State fileUrl: string = '' //上传成功后服务器返回的URL
@State fileName: string = '' //保存文件名,用于预览时获取MIME
@State isUploading: boolean = false
@State isPreviewing: boolean = false // 添加预览状态
// 存储选中的文档信息
@State selectedDocument: DocumentInfo | null = null
async aboutToAppear() {
await initializeCSRF()
}
//根据文件名获取MIME类型
getMimeType(fileName: string): string {
//提取文件扩展名例如:.pdf
const ext = fileName.split('.').pop()?.toLowerCase()
//从映射表中查找对应的MIME类型
if (ext && MIME_MAP[ext]) {
return MIME_MAP[ext]
}
//默认返回通用二进制流类型
return 'application/octet-stream'
}
// 解码文件名(处理URL编码)
decodeFileName(encodedName: string): string {
try {
return decodeURIComponent(encodedName);
} catch (e) {
return encodedName;
}
}
// 从URI中提取文件名(处理特殊格式)
extractFileNameFromUri(uri: string): string {
// URI格式:file://docs/storage/Users/currentUser/Documents/文件名.pdf
// 提取最后一个斜杠后的部分
let fileName = uri.split('/').pop() || '未知文件';
// 解码URL编码的字符(如 %20 转为空格)
return this.decodeFileName(fileName);
}
async selectAndUpload() {
try {
//获取上下文
let context = getContext(this) as common.Context
//创建文档选择器
let documentPicker = new picker.DocumentViewPicker(context)
//配置选择选项(限制文件类型)
const options = new picker.DocumentSelectOptions()
options.maxSelectNumber = 1 //一次只能选择1个文件
//限制显示的文件类型
options.fileSuffixFilters = ['.pdf', '.doc', '.docx']
const uris = await documentPicker.select(options)
if (uris && uris.length > 0) {
this.fileUris = uris[0]
console.info('原始URI:', this.fileUris)
// 提取并解码文件名
this.fullFileName = this.extractFileNameFromUri(this.fileUris);
this.mimeType = this.getMimeType(this.fullFileName);
// 7. 保存选中的文档信息
//先给fileName赋值,这样就能实时看到文件名
this.fileName = this.fullFileName
this.selectedDocument = null
this.selectedDocument = {
uri: this.fileUris,
name: this.fileName,
mimeType: this.mimeType,
};
// 更新界面显示
console.info(`选择文件成功:${this.fullFileName}`);
console.info(`MIME类型:${this.mimeType}`);
console.info('selectedDocument赋值后:',JSON.stringify(this.selectedDocument))
//开始上传
// await this.uploadFile(fileUri,fullFileName,mimeType)
} else {
console.info('用户取消了选择')
}
} catch (err) {
let error = err as BusinessError
console.error(`选择文件失败:${error.message}`)
promptAction.showToast({ message: '选择文件失败,请重试' })
}
}
async uploadFile(uri: string, fileName: string, mimeType: string) {
this.isUploading = true;
let sandboxPath = '';
try {
// 1. 获取应用上下文
let uiContext = getContext(this) as common.UIAbilityContext;
// 2. 处理文件名(移除可能的路径分隔符)
let safeFileName = fileName.replace(/[\\/]/g, '_');
sandboxPath = `${uiContext.cacheDir}/${safeFileName}`;
console.info('源URI:', uri);
console.info('目标沙箱路径:', sandboxPath);
// 3. 复制文件到应用沙箱(关键修复点)
try {
// 对于 file://docs/ 格式的URI,需要先通过 fs.open 获取文件描述符
// 然后复制内容
let srcFile = fs.openSync(uri, fs.OpenMode.READ_ONLY);
let destFile = fs.openSync(sandboxPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
// 分批复制文件内容
const bufferSize = 8192; // 8KB 缓冲区
let buffer = new ArrayBuffer(bufferSize);
let totalBytes = 0;
while (true) {
let bytesRead = fs.readSync(srcFile.fd, buffer);
if (bytesRead <= 0) {
break;
}
// 只写入实际读取的字节数
let writeBuffer = buffer.slice(0, bytesRead);
fs.writeSync(destFile.fd, writeBuffer);
totalBytes += bytesRead;
}
fs.closeSync(srcFile);
fs.closeSync(destFile);
console.info(`文件复制成功,大小:${totalBytes} 字节`);
} catch (copyErr) {
console.error('文件复制失败:', copyErr);
// 尝试使用 fileUri 模块的 getUriFromPath 方法
try {
let fileInfo = await fileUri.getUriFromPath(uri);
console.info('通过 fileUri 获取文件信息:', fileInfo);
// 如果获取成功,使用返回的路径
if (fileInfo && fileInfo) {
fs.copyFileSync(fileInfo, sandboxPath);
console.info('使用 fileUri 方式复制成功');
}
} catch (uriErr) {
console.error('fileUri 方式也失败:', uriErr);
throw new Error('无法读取选择的文件');
}
}
// 4. 读取文件内容为 ArrayBuffer
let file = fs.openSync(sandboxPath, fs.OpenMode.READ_ONLY);
let stat = fs.statSync(file.fd);
let buffer = new ArrayBuffer(stat.size);
fs.readSync(file.fd, buffer);
fs.closeSync(file);
console.info(`文件读取成功,大小:${stat.size} 字节`);
// 5. 创建 FormData 并添加文件
let formData = new FormData();
formData.append('file', buffer, safeFileName);
formData.append('standard_no', this.standard_no);
formData.append('standard_name', this.standard_name);
formData.append('category', this.category);
formData.append('professional', this.professional);
formData.append('item', this.item);
// 6. 发送上传请求
const response: AxiosResponse = await http.post(
'addfiles',
formData,
{
headers: {
'Content-Type': 'multipart/form-data'
},
timeout: 60000, // 60秒超时,大文件需要更长时间
onUploadProgress: (progressEvent: AxiosProgressEvent) => {
if (progressEvent.total) {
let percent = Math.ceil((progressEvent.loaded / progressEvent.total) * 100);
console.info(`上传进度:${percent}%`);
}
}
}
);
// 7. 处理响应
if (response.status === 200) {
console.info('上传成功,响应数据:', response.data);
this.fileUrl = response.data.file_url || response.data;
promptAction.showToast({ message: `${fileName} 上传成功` });
// 可选:上传成功后返回上一页
// setTimeout(() => {
// this.pathStack.pop();
// }, 1500);
} else {
console.error('上传失败,状态码:', response.status);
promptAction.showToast({ message: `上传失败:${response.status}` });
}
} catch (err) {
console.error('上传失败:', err);
let errorMessage = err instanceof Error ? err.message : '未知错误';
promptAction.showToast({ message: `上传失败:${errorMessage}` });
} finally {
this.isUploading = false;
// 清理临时文件
if (sandboxPath) {
try {
fs.accessSync(sandboxPath);
fs.unlinkSync(sandboxPath);
console.info('临时文件已清理');
} catch (e) {
console.info('临时文件不存在或清理失败');
}
}
}
}
async previewDocument() {
console.info('预览时selectedDocument:',JSON.stringify(this.selectedDocument))
if (!this.selectedDocument) {
promptAction.showToast({ message: '请先选择文件' });
return;
}
if (this.isPreviewing) {
promptAction.showToast({ message: '正在预览中,请稍后' });
return;
}
this.isPreviewing = true;
let sandboxPath = '';
try {
let context = getContext(this) as common.Context;
let uiContext = getContext(this) as common.UIAbilityContext;
let fileName = this.selectedDocument.name;
let fileUri = this.selectedDocument.uri;
let mimeType = this.selectedDocument.mimeType;
console.info(`========== 开始预览文档 ==========`);
console.info(`文件名:${fileName}`);
console.info(`文件URI:${fileUri}`);
console.info(`MIME类型:${mimeType}`);
// 1. 彻底清理文件名:只保留字母、数字、点、下划线
let cleanFileName = fileName
.replace(/[^a-zA-Z0-9._]/g, '_') // 所有非字母数字点下划线替换为下划线
.replace(/_+/g, '_'); // 多个连续下划线合并为一个
// 如果清理后为空或只剩扩展名,则用时间戳命名
if (cleanFileName.length === 0 || cleanFileName === '._') {
cleanFileName = `file_${Date.now()}.${fileName.split('.').pop() || 'pdf'}`;
}
console.info(`清理后文件名:${cleanFileName}`);
let timestamp = Date.now();
sandboxPath = `${uiContext.cacheDir}/preview_${timestamp}_${cleanFileName}`;
console.info(`临时文件路径:${sandboxPath}`);
// 复制文件到沙箱
let srcFile: fs.File | null = null;
let destFile: fs.File | null = null;
try {
srcFile = fs.openSync(fileUri, fs.OpenMode.READ_ONLY);
console.info('源文件打开成功,fd:', srcFile.fd);
destFile = fs.openSync(sandboxPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
const bufferSize = 8192;
let buffer = new ArrayBuffer(bufferSize);
let totalBytes = 0;
while (true) {
let bytesRead = fs.readSync(srcFile.fd, buffer);
if (bytesRead <= 0) break;
let writeBuffer = buffer.slice(0, bytesRead);
fs.writeSync(destFile.fd, writeBuffer);
totalBytes += bytesRead;
}
console.info(`文件复制成功,大小:${totalBytes} 字节`);
} catch (copyErr) {
console.error('文件复制失败:', copyErr);
throw new Error(`文件复制失败:${copyErr.message}`);
} finally {
if (srcFile) try { fs.closeSync(srcFile); } catch (e) {}
if (destFile) try { fs.closeSync(destFile); } catch (e) {}
}
// 验证文件
let stat = fs.statSync(sandboxPath);
console.info(`复制后文件大小:${stat.size} 字节`);
if (stat.size === 0) {
throw new Error('复制后的文件大小为0');
}
await new Promise<void>(resolve => setTimeout(resolve,100))
// ========== 方法1:优先使用系统应用打开 ==========
try {
console.info('尝试使用系统应用打开...');
let fileUriToOpen = `file://${sandboxPath}`;
let want: Want = {
action: 'ohos.want.action.viewData',
uri: fileUriToOpen,
type: mimeType,
flags: 0x10000000
};
await uiContext.startAbility(want)
console.info('系统应用已启动');
promptAction.showToast({ message: '正在打开文件...' });
return;
} catch (abilityErr) {
console.error('系统应用打开失败,尝试filePreview:', abilityErr);
}
// ========== 方法2:使用 filePreview ==========
try {
let previewInfo: filePreview.PreviewInfo = {
title: fileName,
uri: sandboxPath,
mimeType: mimeType
};
console.info('调用 canPreview 检查...');
let canPreview = await filePreview.canPreview(context, sandboxPath);
console.info(`canPreview 结果:${canPreview}`);
if (canPreview) {
console.info('调用 openPreview...');
await filePreview.openPreview(context, previewInfo);
console.info('预览器已成功打开');
promptAction.showToast({ message: '正在打开预览...' });
return; // 成功,结束
} else {
console.warn('filePreview不支持预览此文件类型');
// 继续尝试其他方法
}
} catch (previewErr) {
console.error('filePreview 预览失败:', previewErr);
// 继续尝试其他方法
}
// ========== 方法3:提示用户手动打开 ==========
console.error('所有预览方法均失败');
promptAction.showToast({
message: `无法预览,请使用文件管理器打开:${fileName}`,
duration: 3000
});
} catch (err) {
let error = err as BusinessError;
console.error(`预览流程异常:${error.message}`);
promptAction.showToast({ message: `预览失败:${error.message}` });
} finally {
this.isPreviewing = false;
// 清理临时文件(延迟10秒)
if (sandboxPath) {
setTimeout(() => {
try {
if (fs.accessSync(sandboxPath)) {
fs.unlinkSync(sandboxPath);
console.info('临时文件已清理');
}
} catch (e) {}
}, 50000);
}
console.info(`========== 预览流程结束 ==========`);
}
}
build() {
NavDestination() {
Column({ space: 5 }) {
Row() {
Text('新增文件').fontSize(20).fontWeight(FontWeight.Bold).offset({ left: 20 })
}.width('100%').justifyContent(FlexAlign.Start).margin({ bottom: 10 })
Row({ space: 10 }) {
Text('文件类型').width(65).textAlign(TextAlign.Center)
Text('|')
Select([{ value: '国外标准', icon: $r('app.media.wenjianselected') },
{ value: '国家标准', icon: $r('app.media.wenjianselected') },
{ value: '行业标准', icon: $r('app.media.wenjianselected') },
{ value: '审核清单', icon: $r('app.media.wenjianselected') },
{ value: '通用文件', icon: $r('app.media.wenjianselected') },
{ value: '一层次文件', icon: $r('app.media.wenjianselected') },
{ value: '二层次文件', icon: $r('app.media.wenjianselected') },
{ value: '三层次文件', icon: $r('app.media.wenjianselected') },
{ value: '其他文件', icon: $r('app.media.wenjianselected') },
])
.selected(this.category_index)
.value(this.category)
.font({ size: 16, weight: 500 })
.fontColor('#182431')
.backgroundColor(Color.Transparent)
.selectedOptionFont({ size: 16, weight: 400 })
.optionFont({ size: 16, weight: 400 })
.space(this.space)
.arrowPosition(this.arrowPosition)
.menuAlign(MenuAlignType.START, { dx: 0, dy: 0 })
.optionWidth(200)
.optionHeight(300)
.onSelect((category_index: number, category?: string | undefined) => {
console.info('Select:' + category_index);
this.category_index = category_index;
if (category) {
this.category = category;
console.log(category)
}
})
.layoutWeight(1)
.avoidance(AvoidanceMode.COVER_TARGET)
}.backgroundColor('#f4f5f6').borderRadius(15).padding({ left: 10 })
Row({ space: 10 }) {
Text('文件编号').width(65).textAlign(TextAlign.Center)
Text('|')
TextInput({ placeholder: '请输入文件编号', text: $$this.standard_no })
.fontColor('#182431')
.layoutWeight(1)
.backgroundColor('#f4f5f6')
}.backgroundColor('#f4f5f6').borderRadius(15).padding({ left: 10 })
Row({ space: 10 }) {
Text('文件名称').width(65).textAlign(TextAlign.Center)
Text('|')
TextInput({ placeholder: '请输入文件名称', text: $$this.standard_name })
.fontColor('#182431')
.layoutWeight(1)
.backgroundColor('#f4f5f6')
}.backgroundColor('#f4f5f6').borderRadius(15).padding({ left: 10 })
Row({ space: 10 }) {
Text('专 业').width(65).textAlign(TextAlign.Center)
Text('|')
Select([{ value: '通用', icon: $r('app.media.zhuanyezhuanyeke') },
{ value: '化学', icon: $r('app.media.zhuanyezhuanyeke') },
{ value: '力学', icon: $r('app.media.zhuanyezhuanyeke') },
{ value: '金相', icon: $r('app.media.zhuanyezhuanyeke') }])
.selected(this.profess_index)
.value(this.professional)
.font({ size: 16, weight: 500 })
.fontColor('#182431')
.backgroundColor(Color.Transparent)
.selectedOptionFont({ size: 16, weight: 400 })
.optionFont({ size: 16, weight: 400 })
.space(this.space)
.arrowPosition(this.arrowPosition)
.menuAlign(MenuAlignType.START, { dx: 0, dy: 0 })
.optionWidth(200)
.optionHeight(300)
.onSelect((profess_index: number, professional?: string | undefined) => {
console.info('Select:' + profess_index)
this.profess_index = profess_index
if (professional) {
this.professional = professional
console.log(this.professional)
}
})
.layoutWeight(1)
.avoidance(AvoidanceMode.COVER_TARGET)
}.backgroundColor('#f4f5f6').padding({ left: 10 }).borderRadius(15)
Row({ space: 10 }) {
Text('检测项目').width(65).textAlign(TextAlign.Center)
Text('|')
Select([{ value: '通用', icon: $r('app.media.xiangmu') },
{ value: '化学成分', icon: $r('app.media.xiangmu') },
{ value: '室温拉伸', icon: $r('app.media.xiangmu') },
{ value: '硬度', icon: $r('app.media.xiangmu') },
{ value: '渗层', icon: $r('app.media.xiangmu') },
{ value: '金相组织', icon: $r('app.media.xiangmu') },
{ value: '失效分析', icon: $r('app.media.xiangmu') }])
.selected(this.item_index)
.value(this.item)
.font({ size: 16, weight: 500 })
.fontColor('#182431')
.backgroundColor(Color.Transparent)
.selectedOptionFont({ size: 16, weight: 400 })
.optionFont({ size: 16, weight: 400 })
.space(this.space)
.arrowPosition(this.arrowPosition)
.menuAlign(MenuAlignType.START, { dx: 0, dy: 0 })
.optionWidth(200)
.optionHeight(300)
.onSelect((item_index: number, item?: string | undefined) => {
console.info('Select:' + item_index)
this.item_index = item_index
if (item) {
this.item = item
console.log(this.item)
}
})
.layoutWeight(1)
.avoidance(AvoidanceMode.COVER_TARGET)
}.backgroundColor('#f4f5f6').padding({ left: 10 }).borderRadius(15)
Row({ space: 10 }) {
Text('选择文件').width(65)
Text('|')
Row() {
Image($r('app.media.wenjianjia')).width(30).height(30)
}.layoutWeight(1).justifyContent(FlexAlign.Center).offset({ left: -10 })
.onClick(() => {
this.selectAndUpload()
})
}
.backgroundColor('#f4f5f6')
.width('100%')
.height(40)
.padding({ left: 10 })
.borderRadius(15)
if (this.selectedDocument){
Row({space:10}){
Text('已选文件').width(65)
Text('|')
Text(this.selectedDocument.name).layoutWeight(1).fontColor(Color.Blue)
.onClick(()=>{
this.previewDocument()
})
}.backgroundColor('#f4f5f6')
.width('100%')
.height(40)
.padding({ left: 10 })
.borderRadius(15)
}
Button('新增文件')
.width('100%')
.type(ButtonType.Capsule)
.borderRadius(10)
.margin({ top: 20 })
.enabled(this.standard_no && this.standard_name ? true : false)
.onClick(() => {
this.uploadFile(this.fileUris, this.fullFileName, this.mimeType)
})
}
.width('100%').height('100%')
.padding({ left: 20, right: 20 })
}.hideTitleBar(true)
}
}
03-31 23:12:07.332 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I 原始URI: file://docs/storage/Users/currentUser/Documents/Englishhomework.pdf
03-31 23:12:07.332 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I 选择文件成功:Englishhomework.pdf
03-31 23:12:07.332 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I MIME类型:application/pdf
03-31 23:12:07.332 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I selectedDocument赋值后: {“uri”:“file://docs/storage/Users/currentUser/Documents/Englishhomework.pdf”,“name”:“Englishhomework.pdf”,“mimeType”:“application/pdf”}
03-31 23:12:10.068 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I 预览时selectedDocument: {“uri”:“file://docs/storage/Users/currentUser/Documents/Englishhomework.pdf”,“name”:“Englishhomework.pdf”,“mimeType”:“application/pdf”}
03-31 23:12:10.069 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I ========== 开始预览文档 ==========
03-31 23:12:10.069 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I 文件名:Englishhomework.pdf
03-31 23:12:10.069 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I 文件URI:file://docs/storage/Users/currentUser/Documents/Englishhomework.pdf
03-31 23:12:10.069 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I MIME类型:application/pdf
03-31 23:12:10.069 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I 清理后文件名:Englishhomework.pdf
03-31 23:12:10.069 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I 临时文件路径:/data/storage/el2/base/haps/entry/cache/preview_1774969930069_Englishhomework.pdf
03-31 23:12:10.070 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I 源文件打开成功,fd: 77
03-31 23:12:10.075 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I 文件复制成功,大小:173455 字节
03-31 23:12:10.075 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I 复制后文件大小:173455 字节
03-31 23:12:10.176 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I 尝试使用系统应用打开…
03-31 23:12:10.231 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I 系统应用已启动
03-31 23:12:10.231 25864-25864 A03D00/com.fir…sicdemo/JSAPP com.first…usicdemo I ========== 预览流程结束 ==========
03-31 23:12:10.292 25864-25864 A00000/com.fir…cdemo/testTag com.first…usicdemo I [a92ab1e44a2b76e 259f931 566332]Ability onBackground
各位大神,这是运行日志,代码已经跑通了,但是真机测试就是不能预览,是哪里出现问题了,请指教,感谢感谢!!!
找HarmonyOS工作还需要会Flutter的哦,有需要Flutter教程的可以学学大地老师的教程,很不错,B站免费学的哦:https://www.bilibili.com/video/BV1S4411E7LY/?p=17
预览始终显示同一份内容,通常是因为数据源未触发状态更新。在鸿蒙Next中,请检查:1. 使用的@State、@Prop等装饰器是否作用在可变数据上;2. 数组或对象的修改是否创建了新引用(例如使用this.data = [...this.data, newItem]而非push);3. 列表项是否缺少ForEach中的key参数导致复用错误。
预览总是同一份内容,最常见的原因是 `selectedDocument` 仅在 `selectAndUpload` 中被赋值,而 `selectAndUpload` 末尾又立即将 `selectedDocument` 设为 `null`(第 121 行),导致 `selectedDocument` 始终为空。如果用户在点击预览前没有再次选择文件,则 `selectedDocument` 不会保留上次的选择。
检查代码:
```ts
this.selectedDocument = null; // 此处清空了
this.selectedDocument = { ... };
虽然紧接着又重新赋值,但如果在选择文件后、预览前有其他操作(如路由切换、异步等待)导致状态丢失,或 UI 渲染时机问题,预览时拿到的可能仍是旧数据。
另一个可能原因:复制到沙箱时未区分文件名,若多次选择同名文件,会覆盖同一个临时文件,导致预览看起来是同一份内容。
建议确保:
- 不要在赋值前显式置
null; - 预览用的沙箱文件名加入时间戳,避免缓存;
- 在
previewDocument中直接使用selectedDocument的uri,并检查selectedDocument是否确为最新选择。


