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

10 回复

开发者你好,

  1. 你这边的问题是因为在previewDocument方法中, 未将path转换为uri, 需要将 sandboxPath 转换为uri路径, 使用 getUriFromPath 转换后, 使用filePathUri即可正常打开:

    let filePathUri: string = fileUri.getUriFromPath(sandboxPath)

  2. 你的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 预览的文件每次都是同一个嘛? 你这边的两个方法打开的文件都是同一个嘛?

  1. 需要麻烦你确认下, 每次使用selectAndUpload选择文件之后, this.selectedDocument 对象值是否发生变化?

  2. 你这边可以打开应用沙箱路径, 确认下沙箱的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

可以发下能运行的demo吗

预览始终显示同一份内容,通常是因为数据源未触发状态更新。在鸿蒙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 中直接使用 selectedDocumenturi,并检查 selectedDocument 是否确为最新选择。
回到顶部