瀏覽代碼

优化YTHeader YTDate 新增工具类 YTPhotoHelper

丶等天黑 2 月之前
父節點
當前提交
564e6fa223

+ 2 - 0
commons/basic/Index.ets

@@ -1,3 +1,5 @@
+export { YTPhotoHelper } from './src/main/ets/utils/YTPhotoHelper'
+
 export { permissionController } from './src/main/ets/utils/PermissionControl'
 
 export { YTUserRequest } from './src/main/ets/apis/YTUserRequest'

+ 75 - 11
commons/basic/src/main/ets/components/generalComp/YTHeader.ets

@@ -1,17 +1,60 @@
 import { YTAvoid } from '../../utils/YTAvoid'
 import { yTRouter } from '../../utils/YTRouter'
 
+interface Font {
+  fontWeight?: string | number | FontWeight,
+  fontSize?: string | number | Resource,
+  fontColor?: ResourceColor
+}
+
+interface DefaultStyle {
+  backArrow?: boolean,
+  title?: string,
+  titleFont?: Font,
+  click?: () => void
+}
+
+
 @Component
 export struct YTHeader {
   @BuilderParam leftComp?: () => void
   @BuilderParam rightComp?: () => void
   @BuilderParam centerComp?: () => void
   @StorageProp(YTAvoid.SAFE_TOP_KEY) safeTop: number = 0
-  backArrow: boolean = true
-  title: string = ''
-  bgc: ResourceColor = Color.White
-  click = () => {
-    yTRouter.routerBack()
+  bgc: ResourceColor = Color.Transparent
+  headerPadding?: Length | Padding | LocalizedPadding
+  headerHeight: Length = this.safeTop + 44
+  /**
+   * @description 该属性为默认结构样式,传入对应结构后失效
+   * @param backArrow 是否显示返回箭头
+   * @param title 标题
+   * @param titleFont 标题样式
+   * @param click 点击返回按钮的回调
+   *
+   */
+  defaultStyle?: DefaultStyle = {}
+  private backArrow: boolean = true
+  private title: string = ''
+  private titleFont: Font = { fontSize: 18, fontWeight: 700, fontColor: Color.Black }
+
+  aboutToAppear(): void {
+    if (this.headerPadding == undefined) {
+      this.headerPadding = { top: this.safeTop }
+    }
+    if (this.defaultStyle) {
+      if (this.defaultStyle.backArrow !== undefined) {
+        this.backArrow = this.defaultStyle.backArrow
+      }
+      if (this.defaultStyle.title !== undefined) {
+        this.title = this.defaultStyle.title
+      }
+      if (this.defaultStyle.titleFont !== undefined) {
+        this.titleFont = this.defaultStyle.titleFont
+      }
+      if (this.defaultStyle.click !== undefined) {
+        this.click = this.defaultStyle.click
+      }
+    }
   }
 
   build() {
@@ -32,7 +75,7 @@ export struct YTHeader {
         }
       }
       .width('100%')
-      .justifyContent(this.rightComp ? FlexAlign.SpaceBetween : FlexAlign.Start)
+      .justifyContent(this.calcSort())
 
 
       Row() {
@@ -41,18 +84,39 @@ export struct YTHeader {
         }
         if (this.title && !this.centerComp) {
           Text(this.title)
-            .fontSize(18)
-            .fontWeight(700)
-            .fontColor(Color.Black)
+            .fontSize(this.titleFont?.fontSize)
+            .fontWeight(this.titleFont?.fontWeight)
+            .fontColor(this.titleFont?.fontColor)
         }
       }
       .width('100%')
       .justifyContent(FlexAlign.Center)
       .hitTestBehavior(HitTestMode.None)
     }
-    .height(44 + this.safeTop)
-    .padding({ top: this.safeTop })
+    .height(this.headerHeight)
+    .padding(this.headerPadding)
     .backgroundColor(this.bgc)
 
   }
+
+  private click: () => void = () => {
+    yTRouter.routerBack()
+  }
+
+  private calcSort() {
+    if ((this.leftComp || this.backArrow) && this.rightComp) {
+      return FlexAlign.SpaceBetween
+    }
+    if (this.rightComp) {
+      if (!this.leftComp) {
+        if (this.backArrow) {
+          return FlexAlign.SpaceBetween
+        } else {
+          return FlexAlign.End
+        }
+      }
+
+    }
+    return FlexAlign.Start
+  }
 }

+ 100 - 1
commons/basic/src/main/ets/utils/FormatDate.ets

@@ -1,11 +1,110 @@
 export class YTDate extends Date {
-  formatDate(separator: string = '-', date: Date = this): string {
+  constructor(value?: number | string | Date) {
+    if (!value) {
+      super()
+      return
+    }
+    if (typeof value == 'string') {
+      const separator = value.charAt(4)
+      value = value.split(separator).join('-')
+      const ymdRegex = /^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/;
+      if (!ymdRegex.test(value)) {
+        throw new Error('传入的日期不合法')
+      }
+    }
+    super(value)
+  }
+
+
+  /**
+   * 将数字转换为中文周数表示(如:7 → "第七周")
+   * @param weekNum - 周数数字(正整数)
+   * @returns 中文周数字符串
+   */
+  static convertToChineseWeek(weekNum: number): string {
+    // 1. 参数校验
+    if (!Number.isInteger(weekNum) || weekNum <= 0) {
+      throw new Error('输入必须是正整数');
+    }
+
+    // 2. 中文数字映射表
+    const chineseNumbers: string[] = [
+      '零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十',
+      '十一', '十二', '十三', '十四', '十五', '十六', '十七', '十八', '十九', '二十'
+    ];
+
+    // 3. 处理不同范围的数字
+    let result: string;
+    if (weekNum <= 20) {
+      // 直接使用预定义映射(1-20)
+      result = chineseNumbers[weekNum];
+    } else if (weekNum <= 99) {
+      // 分解十位和个位(21-99)
+      const tens = Math.floor(weekNum / 10);
+      const units = weekNum % 10;
+
+      result = chineseNumbers[tens] + '十';
+      if (units > 0) {
+        result += chineseNumbers[units];
+      }
+    } else {
+      // 超过99的直接返回数字形式
+      result = weekNum.toString();
+    }
+
+    // 4. 返回带序号的周数
+    return `第${result}周`;
+  }
+
+  /**
+   * 根据时间戳获取星期几的中文表示
+   * @param timestamp - 时间戳(毫秒)
+   * @returns 星期几的中文字符串(如"星期一")
+   */
+  static getDayOfWeekFromTimestamp(timestamp: number | string | Date): string {
+    // 1. 创建Date对象
+    const date = new YTDate(timestamp);
+
+    // 2. 获取星期索引(0-6,0=星期日)
+    const dayIndex = date.getDay();
+
+    // 3. 映射到中文星期
+    const weekdays = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
+
+    return weekdays[dayIndex];
+  }
+
+  /**
+   * 替换日期分隔符
+   * @param separator - 新的分隔符
+   * @param targetStr - 目标字符串
+   * @returns 替换分隔符后的字符串
+   */
+  static changeSeparator(separator: string, targetStr: string) {
+    return targetStr.split(targetStr.charAt(4)).join(separator)
+  }
+
+  /**
+   * 格式化日期
+   * @param separator - 日期分隔符
+   * @param date - 日期对象
+   * @param onlyYMD - 是否只返回年月日
+   * @returns 格式化后的日期字符串
+   */
+  formatDate(separator: string = '-', onlyYMD: boolean = false, date: Date = this): string {
+    if (!(date instanceof Date) || isNaN(date.getTime())) {
+      throw new Error('无效的日期对象');
+    }
     const year = date.getFullYear();
     const month = (date.getMonth() + 1).toString().padStart(2, '0');
     const day = date.getDate().toString().padStart(2, '0');
     const hours = date.getHours().toString().padStart(2, '0');
     const minutes = date.getMinutes().toString().padStart(2, '0');
     const seconds = date.getSeconds().toString().padStart(2, '0');
+    if (onlyYMD) {
+      return `${year + separator + month + separator + day}`
+    }
+
     return `${year + separator + month + separator + day} ${hours}:${minutes}:${seconds}`;
   }
 }

+ 181 - 0
commons/basic/src/main/ets/utils/YTPhotoHelper.ets

@@ -0,0 +1,181 @@
+import axios, { AxiosProgressEvent } from '@ohos/axios';
+import { fileIo, fileUri } from '@kit.CoreFileKit';
+import { camera, cameraPicker as picker } from '@kit.CameraKit';
+import { util } from '@kit.ArkTS';
+import { photoAccessHelper } from '@kit.MediaLibraryKit';
+import { image } from '@kit.ImageKit';
+import { BusinessError } from '@kit.BasicServicesKit';
+import { YTLog } from '../../../../Index';
+
+/**
+ * @method cashPhotos 传入需要下载得url数组并下载文件,通过回调函数返回对应cashPaths数组
+ * @method takePicture 拍照获取图片
+ * @method selectImage 从相册选择图片
+ * @method saveByShowAssetsCreationDialog 通过ShowAssetsCreationDialog保存文件(弹窗)
+ * @method getPhotoFileBuffer 将缓存的图片转化为流
+ * @method saveImgToAssets 直接保存图片至相册 需要权限
+ */
+export class YTPhotoHelper {
+  private cashPaths: string[] = []
+  private currentIndex: number = 0
+  private readonly context: Context
+
+  constructor(context: Context) {
+    this.context = context
+  }
+
+  //传入需要下载得url数组并下载文件,通过回调函数返回对应cashPaths数组
+  async cashPhotos(urls: string[], finishDownLoad: (cashPaths: string[]) => void, cashPhotoType: string = '.jpg') {
+    let filePath = this.context.cacheDir + '/' + util.generateRandomUUID() + cashPhotoType
+    // Download the file. If the file already exists, delete the existing one first.
+    try {
+      fileIo.accessSync(filePath);
+      fileIo.unlinkSync(filePath);
+    } catch (err) {
+      YTLog.error(err)
+    }
+
+    await axios({
+      url: urls[this.currentIndex],
+      method: 'get',
+      context: getContext(this),
+      filePath: filePath,
+      onDownloadProgress: (progressEvent: AxiosProgressEvent): void => {
+        YTLog.info("progress: " + progressEvent && progressEvent.loaded && progressEvent.total ?
+        Math.ceil(progressEvent.loaded / progressEvent.total * 100) : 0)
+      }
+    })
+    this.currentIndex++
+    this.cashPaths.unshift(filePath)
+    if (this.currentIndex < urls.length) {
+      this.cashPhotos(urls, finishDownLoad, cashPhotoType)
+    } else {
+      finishDownLoad(this.cashPaths)
+    }
+  }
+
+  //拍照获取图片
+  async takePicture() {
+    let pathDir = this.context.filesDir;
+    let fileName = `${new Date().getTime()}`
+    let filePath = pathDir + `/${fileName}.png`
+    fileIo.createRandomAccessFileSync(filePath, fileIo.OpenMode.CREATE);
+
+    let uri = fileUri.getUriFromPath(filePath);
+    let pickerProfile: picker.PickerProfile = {
+      cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK,
+      saveUri: uri
+    };
+    let result: picker.PickerResult =
+      await picker.pick(this.context, [picker.PickerMediaType.PHOTO], //(如果需要录像可以添加) picker.PickerMediaType.VIDEO
+        pickerProfile);
+    if (!result.resultUri) {
+      return Promise.reject('用户未拍照')
+    }
+    return filePath
+  }
+
+  //从相册选择图片
+  selectImage(success: (fullPath: string) => void) {
+    const option = new photoAccessHelper.PhotoSelectOptions()
+    option.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
+    option.maxSelectNumber = 1
+    const picker = new photoAccessHelper.PhotoViewPicker()
+    picker.select(option)
+      .then(res => {
+        const photoName = util.generateRandomUUID()
+        const photoType = '.' + res.photoUris[0].split('.').pop()
+        const fullPath = this.context.cacheDir + '/' + photoName + photoType
+        const photo = fileIo.openSync(res.photoUris[0])
+        try {
+          fileIo.copyFile(photo.fd, fullPath)
+            .then(() => {
+              YTLog.info(fullPath)
+              success(fullPath)
+            })
+        } catch (err) {
+          YTLog.error(err)
+        }
+      })
+  }
+
+
+  //传入需要保存得cashPaths数组,并通过ShowAssetsCreationDialog保存文件
+  async saveByShowAssetsCreationDialog(cashPaths: string[], success?: () => void) {
+    const phAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.context);
+    const uris = cashPaths.map(item => fileUri.getUriFromPath(item))
+    try {
+      // 指定待保存照片的创建选项,包括文件后缀和照片类型,标题和照片子类型可选。
+      const photoCreationConfigs = uris.map(() => {
+        const photoCreationConfig: photoAccessHelper.PhotoCreationConfig = {
+          fileNameExtension: 'jpg',
+          photoType: photoAccessHelper.PhotoType.IMAGE,
+        }
+        return photoCreationConfig
+      })
+      // 基于弹窗授权的方式获取媒体库的目标uri。
+      let desFileUris: Array<string> =
+        await phAccessHelper.showAssetsCreationDialog(uris, photoCreationConfigs);
+
+      let index = 0
+      while (index < uris.length) {
+        //通过该uri打开图片
+        let file = await fileIo.open(desFileUris[index], fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE)
+        //获取流
+        const buffer = await this.getPhotoFileBuffer(cashPaths[index])
+        //通过流写入相册
+        await fileIo.write(file.fd, buffer)
+        //关流
+        await fileIo.close(file)
+        index++
+      }
+      success?.()
+    } catch (err) {
+      YTLog.error(err)
+    }
+  }
+
+  //传入图片地址,转化为流
+  async getPhotoFileBuffer(filePath: string): Promise<ArrayBuffer> {
+    try {
+      const imageSource = image.createImageSource(filePath)
+      let imagePackerApi = image.createImagePacker();
+      let buffer = await imagePackerApi.packing(imageSource, {
+        quality: 100,
+        format: 'image/jpeg'
+      })
+      return buffer
+    } catch (err) {
+      let error: BusinessError = err as BusinessError;
+      console.error(`read file failed, errCode:${error.code}, errMessage:${error.message}`);
+      return Promise.reject(err)
+    }
+  }
+
+
+  //须先申请权限ohos.permission.WRITE_IMAGEVIDEO 保存图片至相册
+  async saveImgToAssets(cashPaths: string[]) {
+    try {
+      let index = 0;
+      while (index < cashPaths.length) {
+        //获取当前上下文
+        //通过accessHelper模块获取accessHelper对象
+        let accessHelper = photoAccessHelper.getPhotoAccessHelper(getContext())
+        //指定待创建的文件类型、后缀和创建选项,创建图片或视频资源 返回创建的图片和视频的uri
+        let uri = await accessHelper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'jpg')
+        //通过该uri打开图片
+        let file = await fileIo.open(uri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE)
+        //获取流
+        const buffer = await this.getPhotoFileBuffer(cashPaths[index])
+        //通过流写入相册
+        await fileIo.write(file.fd, buffer)
+        //关流
+        await fileIo.close(file)
+        index++
+      }
+
+    } catch (e) {
+      YTLog.error(e)
+    }
+  }
+}

+ 1 - 1
features/user/src/main/ets/pages/AboutUS.ets

@@ -26,7 +26,7 @@ export struct AboutUS {
 
   build() {
     Column() {
-      YTHeader({ title: "关于我们" })
+      YTHeader({ defaultStyle: { title: '关于我们' } })
 
       Column() {
         Image($r('[basic].media.app_icon'))

+ 1 - 1
features/user/src/main/ets/pages/Privacy.ets

@@ -19,7 +19,7 @@ export struct Privacy {
   build() {
 
     Column() {
-      YTHeader({ title: "隐私政策" })
+      YTHeader({ defaultStyle: { title: '隐私政策' } })
       Web({
         src: "https://hm-static.ytpm.net/friend/doc/%E9%9A%90%E7%A7%81%E6%94%BF%E7%AD%96.html",
         controller: this.webviewController,

+ 14 - 13
features/user/src/main/ets/pages/SettingPage.ets

@@ -2,14 +2,13 @@ import {
   BasicType,
   IBestToast,
   reviseImgHeaderBuilder,
-  takePicture,
-  Upload,
   userInfo,
   UserInfo,
   YTAvoid,
   YTButton,
   YTHeader,
   YTLog,
+  YTPhotoHelper,
   yTRouter,
   yTToast,
   YTUserRequest
@@ -25,12 +24,13 @@ function settingBuilder() {
 
 @Component
 struct SettingPage {
-  @StorageProp(YTAvoid.SAFE_BOTTOM_KEY) safeBottom: number = 0
-  @StorageProp(UserInfo.KEY) userInfo: UserInfo = userInfo
   @State showReviseName: boolean = false
   @State showHeaderImgRevise: boolean = false
-  @State value: string = ''
-  reviseBuilderArr: Array<BasicType<undefined>> = [
+  @StorageProp(YTAvoid.SAFE_BOTTOM_KEY) private safeBottom: number = 0
+  @StorageProp(UserInfo.KEY) private userInfo: UserInfo = userInfo
+  @State private value: string = ''
+  private yTPhotoHelper = new YTPhotoHelper(this.getUIContext().getHostContext()!)
+  private reviseBuilderArr: Array<BasicType<undefined>> = [
     {
       text: '头像修改',
       src: '',
@@ -45,12 +45,12 @@ struct SettingPage {
       }
     }
   ]
-  options: BasicType<undefined>[] = [
+  private options: BasicType<undefined>[] = [
     {
       text: '拍照',
       click: async () => {
         try {
-          const fullpath = await takePicture(this.getUIContext().getHostContext()!)
+          const fullpath = await this.yTPhotoHelper.takePicture()
           this.showHeaderImgRevise = false
           yTRouter.router2DelPhotoPage({ src: fullpath, type: 'header' })
         } catch (e) {
@@ -61,10 +61,11 @@ struct SettingPage {
     {
       text: '从相册中选择',
       click: () => {
-        Upload.selectImage(this.getUIContext().getHostContext()!, (fullPath) => {
-          this.showHeaderImgRevise = false
-          yTRouter.router2DelPhotoPage({ src: fullPath, type: 'header' })
-        })
+        this.yTPhotoHelper.selectImage(
+          (fullPath) => {
+            this.showHeaderImgRevise = false
+            yTRouter.router2DelPhotoPage({ src: fullPath, type: 'header' })
+          })
       }
     },
     {
@@ -77,7 +78,7 @@ struct SettingPage {
 
   build() {
     Column() {
-      YTHeader({ title: '用户设置' })
+      YTHeader({ defaultStyle: { title: '用户设置' } })
       Column() {
         ForEach(this.reviseBuilderArr, (item: BasicType<undefined>, index) => {
           this.reviseBuilder(item)

+ 1 - 1
features/user/src/main/ets/pages/SuggestionPage.ets

@@ -16,7 +16,7 @@ struct SuggestionPage {
 
   build() {
     Column() {
-      YTHeader({ title: '意见反馈' })
+      YTHeader({ defaultStyle: { title: '意见反馈' } })
       Row() {
         Row() {
           Text()

+ 1 - 1
features/user/src/main/ets/pages/UserAgreement.ets

@@ -15,7 +15,7 @@ struct UserAgreementPage {
 
   build() {
     Column() {
-      YTHeader({ title: '用户协议' })
+      YTHeader({ defaultStyle: { title: '用户协议' } })
       Web({
         src: "https://hm-static.ytpm.net/friend/doc/%E7%94%A8%E6%88%B7%E5%8D%8F%E8%AE%AE.html",
         controller: this.webviewController,