wangcy пре 6 месеци
родитељ
комит
67a09e434b
50 измењених фајлова са 2621 додато и 35 уклоњено
  1. 62 1
      commons/basic/Index.ets
  2. 6 1
      commons/basic/oh-package.json5
  3. 49 0
      commons/basic/src/main/ets/ads/BannerAd.ets
  4. 175 0
      commons/basic/src/main/ets/ads/RewardAd.ets
  5. 109 0
      commons/basic/src/main/ets/ads/StartAd.ets
  6. 22 0
      commons/basic/src/main/ets/components/ReviseImgHeaderBuilder.ets
  7. 60 0
      commons/basic/src/main/ets/components/ShowBannerAd.ets
  8. 24 0
      commons/basic/src/main/ets/components/ShowJHRewardAd.ets
  9. 52 0
      commons/basic/src/main/ets/components/YTHeader.ets
  10. 29 0
      commons/basic/src/main/ets/components/YtButton.ets
  11. 10 0
      commons/basic/src/main/ets/constants/index.ets
  12. 125 0
      commons/basic/src/main/ets/models/UserInfo.ets
  13. 19 0
      commons/basic/src/main/ets/models/index.ets
  14. 1 1
      commons/basic/src/main/ets/pages/Index.ets
  15. 0 3
      commons/basic/src/main/ets/utils/Calc.ets
  16. 24 0
      commons/basic/src/main/ets/utils/CopyText.ets
  17. 10 0
      commons/basic/src/main/ets/utils/DebounceLoad.ets
  18. 10 0
      commons/basic/src/main/ets/utils/FormatDate.ets
  19. 79 0
      commons/basic/src/main/ets/utils/HuaWeiAuthPlugin.ets
  20. 45 0
      commons/basic/src/main/ets/utils/PermissionControl.ets
  21. 22 0
      commons/basic/src/main/ets/utils/TakePicture.ets
  22. 70 0
      commons/basic/src/main/ets/utils/UploadFile.ets
  23. 40 0
      commons/basic/src/main/ets/utils/YTAvoid.ets
  24. 54 0
      commons/basic/src/main/ets/utils/YTBreakPoint.ets
  25. 10 0
      commons/basic/src/main/ets/utils/YTLog.ets
  26. 233 0
      commons/basic/src/main/ets/utils/YTRequest.ets
  27. 29 0
      commons/basic/src/main/ets/utils/YTRouter.ets
  28. 124 0
      commons/basic/src/main/ets/utils/YTToast.ets
  29. 10 2
      commons/basic/src/main/resources/base/element/color.json
  30. 38 2
      commons/basic/src/main/resources/base/element/float.json
  31. BIN
      commons/basic/src/main/resources/base/media/ic_back.png
  32. 2 0
      features/sketch/oh-package.json5
  33. 1 1
      features/user/Index.ets
  34. 1 0
      features/user/oh-package.json5
  35. 94 0
      features/user/src/main/ets/components/LoginInput.ets
  36. 45 0
      features/user/src/main/ets/components/OtherLoginMethods.ets
  37. 66 0
      features/user/src/main/ets/components/Terms.ets
  38. 4 0
      features/user/src/main/ets/models/index.ets
  39. 41 0
      features/user/src/main/ets/pages/AgreementPage.ets
  40. 137 0
      features/user/src/main/ets/pages/LoginPage.ets
  41. 251 0
      features/user/src/main/ets/pages/SettingPage.ets
  42. 138 0
      features/user/src/main/ets/pages/SuggestionPage.ets
  43. 0 3
      features/user/src/main/ets/utils/Calc.ets
  44. 152 0
      features/user/src/main/ets/views/Mine.ets
  45. BIN
      features/user/src/main/resources/base/media/copy.png
  46. BIN
      features/user/src/main/resources/base/media/default_img.png
  47. BIN
      features/user/src/main/resources/base/media/right_arrow.png
  48. 5 1
      products/entry/oh-package.json5
  49. 76 5
      products/entry/src/main/ets/entryability/EntryAbility.ets
  50. 67 15
      products/entry/src/main/ets/pages/Index.ets

+ 62 - 1
commons/basic/Index.ets

@@ -1 +1,62 @@
-export { add } from './src/main/ets/utils/Calc';
+export * from '@ibestservices/ibest-ui'
+
+export { reviseImgHeaderBuilder } from './src/main/ets/components/ReviseImgHeaderBuilder'
+
+export { takePicture } from './src/main/ets/utils/TakePicture'
+
+export { copyText } from './src/main/ets/utils/CopyText'
+
+export { Log as YTLog } from '@abner/log'
+
+export { YtDebounce } from "./src/main/ets/utils/DebounceLoad"
+
+export { ShowBannerAd } from "./src/main/ets/components/ShowBannerAd"
+
+export { BannerAd } from "./src/main/ets/ads/BannerAd"
+
+export { ShowJHRewardAd } from "./src/main/ets/components/ShowJHRewardAd"
+
+export { jhStartAd } from "./src/main/ets/ads/StartAd"
+
+export { AdStatus } from "./src/main/ets/constants"
+
+export { permissionControl } from "./src/main/ets/utils/PermissionControl"
+
+export { yTRequest } from "./src/main/ets/utils/YTRequest"
+
+export { huaweiAuthPlugin } from "./src/main/ets/utils/HuaWeiAuthPlugin"
+
+export { userInfo, UserInfo } from "./src/main/ets/models/UserInfo"
+
+export { yTToast } from "./src/main/ets/utils/YTToast"
+
+export { YtButton } from "./src/main/ets/components/YtButton"
+
+export { YTHeader } from "./src/main/ets/components/YTHeader"
+
+export { Upload } from "./src/main/ets/utils/UploadFile"
+
+// export * from "@ibestservices/ibest-ui"
+
+export { YtAvoid } from "./src/main/ets/utils/YTAvoid"
+
+export { yTRouter } from "./src/main/ets/utils/YTRouter"
+
+export * from "./src/main/ets/utils/YTBreakPoint"
+
+export { BarType } from './src/main/ets/models/index'
+
+export * from './src/main/ets/models'
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 6 - 1
commons/basic/oh-package.json5

@@ -7,5 +7,10 @@
   "license": "Apache-2.0",
   "packageType": "InterfaceHar",
   "dependencies": {
-  }
+    "@ohos/axios": "^2.2.4",
+    "@abner/log": "^1.0.5",
+    "@ibestservices/ibest-ui": "2.0.6"
+  },
+  "devDependencies": {},
+  "dynamicDependencies": {}
 }

+ 49 - 0
commons/basic/src/main/ets/ads/BannerAd.ets

@@ -0,0 +1,49 @@
+import { advertising } from "@kit.AdsKit";
+
+
+interface BannerParam {
+  adWidth: number,
+  adHeight: number
+}
+
+export class BannerAd {
+  adParam: advertising.AdRequestParams = {
+    // 广告类型:横幅广告
+    adType: 8,
+    // 'testw6vs28auh3'为测试专用的广告位ID,App正式发布时需要改为正式的广告位ID
+    adId: 'testw6vs28auh3',
+    // 广告位宽
+    adWidth: 360,
+    // 广告位高
+    adHeight: 57,
+  };
+  adOptions: advertising.AdOptions = {
+    // 设置广告内容分级上限
+    adContentClassification: 'A'
+  };
+  adDisplayOptions: advertising.AdDisplayOptions = {
+    // 是否静音,默认不静音
+    mute: false,
+    // 广告轮播的时间间隔,单位ms,取值范围[30000, 120000]
+    refreshTime: 30000
+  }
+  ratio: number = 1;
+  adWidth: number = -1;
+  adHeight: number = -1;
+
+  constructor(param?: BannerParam) {
+    if (param && param.adWidth > 0 && param.adHeight > 0) {
+      this.adParam.adWidth = param.adWidth
+      this.adParam.adHeight = param.adHeight
+    }
+    if (this.adParam?.adWidth && typeof (this.adParam?.adWidth) === 'number' && this.adParam?.adWidth > 0) {
+      this.adWidth = this.adParam?.adWidth;
+    }
+    if (this.adParam?.adHeight && typeof (this.adParam?.adHeight) === 'number' && this.adParam?.adHeight > 0) {
+      this.adHeight = this.adParam?.adHeight;
+    }
+    if (this.adWidth > 0 && this.adHeight > 0) {
+      this.ratio = this.adWidth / this.adHeight;
+    }
+  }
+}

+ 175 - 0
commons/basic/src/main/ets/ads/RewardAd.ets

@@ -0,0 +1,175 @@
+import { advertising } from '@kit.AdsKit';
+import { common } from '@kit.AbilityKit';
+import { hilog } from '@kit.PerformanceAnalysisKit';
+import { BusinessError, commonEventManager, emitter } from '@kit.BasicServicesKit';
+import { AdStatus } from '../constants';
+import { YTLog, yTToast } from '../../../../Index';
+import { IBestToast } from '@ibestservices/ibest-ui';
+
+const KEY_REWARD_DATA = "reward_ad_data";
+const KEY_REWARD_STATUS = "reward_ad_status";
+
+export class RewardAd {
+  private ads: Array<advertising.Advertisement> = [];
+  private uIContext: common.UIAbilityContext
+  private load: advertising.AdLoader
+  // 用于保存创建成功的订阅者对象,后续使用其完成订阅及退订的动作
+  private subscriber: commonEventManager.CommonEventSubscriber | null = null;
+  private displayOptions: advertising.AdDisplayOptions = {
+    // 激励广告视频播放是否静音
+    mute: true
+  };
+  private isReward = false
+
+  constructor(uIContext: common.UIAbilityContext) {
+    this.uIContext = uIContext
+    this.load = new advertising.AdLoader(this.uIContext);
+    this.requestAndShowAd(this.load)
+  }
+
+  private requestAndShowAd(adLoader: advertising.AdLoader): void {
+    const adRequestParam: advertising.AdRequestParams = {
+      // 广告类型:激励广告
+      adType: 7,
+      // 'testx9dtjwj8hp'为测试专用的广告位ID,应用正式发布时需要改为正式的广告位ID
+      adId: 'testx9dtjwj8hp',
+    };
+    const adOption: advertising.AdOptions = {
+      // 设置是否请求非个性化广告
+      nonPersonalizedAd: 0,
+      // 是否允许流量下载0:不允许,1:允许,不设置以广告主设置为准
+      allowMobileTraffic: 0,
+      // 是否希望根据 COPPA 的规定将您的内容视为面向儿童的内容: -1默认值,不确定 0不希望 1希望
+      tagForChildProtection: -1,
+      // 是否希望按适合未达到法定承诺年龄的欧洲经济区 (EEA) 用户的方式处理该广告请求: -1默认值,不确定 0不希望 1希望
+      tagForUnderAgeOfPromise: -1,
+      // 设置广告内容分级上限: W: 3+,所有受众 PI: 7+,家长指导 J:12+,青少年 A: 16+/18+,成人受众
+      adContentClassification: 'A'
+    };
+    const adLoaderListener: advertising.AdLoadListener = {
+      onAdLoadFailure: (errorCode: number, errorMsg: string) => {
+        setTimeout(() => {
+          IBestToast.hide()
+          setTimeout(() => {
+            IBestToast.show({ message: '加载失败', type: 'fail' })
+          }, 100)
+        }, 1000)
+      },
+      onAdLoadSuccess: (ads: Array<advertising.Advertisement>) => {
+        hilog.info(0x0000, 'testTag', '%{public}s', `Succeeded in requesting ad`);
+        this.ads.push(...ads);
+        this.showAd()
+      },
+    };
+    adLoader.loadAd(adRequestParam, adOption, adLoaderListener);
+  }
+
+  // 订阅方法,需要在每次展示广告前调用
+  public registerPPSReceiver(): void {
+    if (this.subscriber) {
+      this.unRegisterPPSReceiver();
+    }
+    // 订阅者信息
+    const subscribeInfo: commonEventManager.CommonEventSubscribeInfo = {
+      events: ["com.huawei.hms.pps.action.PPS_REWARD_STATUS_CHANGED"],
+      // publisherBundleName被设置为"com.huawei.hms.adsservice",这意味着只有来自该包名的事件才会被订阅者接受和处理。
+      // 如果没有明确声明publisherBundleName,那么订阅者可能会收到来自其它包名的伪造事件,从而导致安全性问题或误导。
+      publisherBundleName: "com.huawei.hms.adsservice"
+    };
+    // 创建订阅者回调
+    commonEventManager.createSubscriber(subscribeInfo, (err: BusinessError, commonEventSubscriber:
+      commonEventManager.CommonEventSubscriber) => {
+      if (err) {
+        hilog.error(0x0000, 'testTag', '%{public}s',
+          `createSubscriber error, code: ${err.code}, message: ${err.message}`);
+        return;
+      }
+      hilog.info(0x0000, 'testTag', '%{public}s', `Succeeded in creating subscriber`);
+      this.subscriber = commonEventSubscriber;
+      // 订阅公共事件回调
+      if (!this.subscriber) {
+        hilog.warn(0x0000, 'testTag', '%{public}s', `Need create subscriber`);
+        return;
+      }
+      commonEventManager.subscribe(this.subscriber, (err: BusinessError, commonEventSubscriber:
+        commonEventManager.CommonEventData) => {
+        if (err) {
+          hilog.error(0x0000, 'testTag', '%{public}s', `Subscribe error, code: ${err.code}, message: ${err.message}`);
+        } else {
+          hilog.info(0x0000, 'testTag', '%{public}s', 'Subscribe data');
+          const status: string = commonEventSubscriber?.parameters?.[KEY_REWARD_STATUS];
+          switch (status) {
+            case AdStatus.AD_OPEN:
+              hilog.info(0x0000, 'testTag', '%{public}s', `onAdOpen`);
+              //关闭弹窗
+              IBestToast.hide()
+              yTToast.hide()
+              break;
+            case AdStatus.AD_CLICKED:
+              hilog.info(0x0000, 'testTag', '%{public}s', `onAdClick`);
+              break;
+            case AdStatus.AD_CLOSED:
+              hilog.info(0x0000, 'testTag', '%{public}s', `onAdClose`);
+              this.unRegisterPPSReceiver();
+
+              if (this.isReward) {
+                this.isReward = false
+                if (AppStorage.get<number>('RewardNum')! < 5) {
+                  setTimeout(() => {
+                    // yTToast.getReward({ number: 500 })
+                  }, 500)
+                }
+              }
+
+
+              break;
+            case AdStatus.AD_REWARDED:
+              const rewardData: Record<string, string | number> = commonEventSubscriber?.parameters?.[KEY_REWARD_DATA];
+              const rewardType: string = rewardData?.rewardType as string;
+              const rewardAmount: number = rewardData?.rewardAmount as number;
+              //奖励位置
+              this.isReward = true
+              YTLog.info('发送奖励')
+              emitter.emit('getReward')
+
+              break;
+            case AdStatus.AD_VIDEO_START:
+              hilog.info(0x0000, 'testTag', '%{public}s', `onAdVideoStart`);
+              break;
+            case AdStatus.AD_COMPLETED:
+              hilog.info(0x0000, 'testTag', '%{public}s', `onAdCompleted`);
+              break;
+
+            default:
+              IBestToast.hide()
+              IBestToast.show({ message: '加载失败', type: 'fail' })
+              break;
+          }
+        }
+      });
+    });
+  }
+
+  // 取消订阅
+  public unRegisterPPSReceiver(): void {
+    commonEventManager.unsubscribe(this.subscriber, (err: BusinessError) => {
+      if (err) {
+        hilog.error(0x0000, 'testTag', '%{public}s', `Unsubscribe error, code: ${err.code}, message: ${err.message}`);
+      } else {
+        hilog.info(0x0000, 'testTag', '%{public}s', `Succeeded in unsubscribing`);
+        this.subscriber = null;
+      }
+    });
+  }
+
+  //展示广告
+  private showAd() {
+    // 请在此处自行增加步骤2中的,注册激励广告状态监听器
+    this.registerPPSReceiver()
+
+    // 此处ads[0]表示请求到的第一个广告,用户根据实际情况选择
+    advertising.showAd(this.ads[0], this.displayOptions, this.uIContext);
+  }
+}
+
+

+ 109 - 0
commons/basic/src/main/ets/ads/StartAd.ets

@@ -0,0 +1,109 @@
+import { common } from '@kit.AbilityKit';
+import { advertising } from '@kit.AdsKit';
+import { router } from '@kit.ArkUI';
+import { hilog } from '@kit.PerformanceAnalysisKit';
+import { emitter } from '@kit.BasicServicesKit';
+import { YTLog } from '../../../../Index';
+
+export enum AdType {
+  // 开屏广告的类型
+  SPLASH_AD = 1
+}
+
+class JhStartAd {
+  private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
+  private oaid: string = '';
+  private isTimeOut: boolean = false;
+  // 超时时间(单位毫秒),开发者可根据实际情况修改
+  private timeOutDuration: number = 1 * 1000;
+  // 超时index
+  private timeOutIndex: number = -1;
+  // 广告展示参数
+  declare ads: Array<advertising.Advertisement>
+  adDisplayOptions: advertising.AdDisplayOptions = {
+    // 是否静音,默认不静音
+    mute: false
+  }
+  // 广告配置
+  private adOptions: advertising.AdOptions = {
+    // 是否允许流量下载0:不允许,1:允许,不设置以广告主设置为准
+    allowMobileTraffic: 0,
+    // 是否希望根据 COPPA 的规定将您的内容视为面向儿童的内容: -1默认值,不确定 0不希望 1希望
+    tagForChildProtection: -1,
+    // 是否希望按适合未达到法定承诺年龄的欧洲经济区 (EEA) 用户的方式处理该广告请求: -1默认值,不确定 0不希望 1希望
+    tagForUnderAgeOfPromise: -1,
+    // 设置广告内容分级上限: W: 3+,所有受众 PI: 7+,家长指导 J:12+,青少年 A: 16+/18+,成人受众
+    adContentClassification: 'A'
+  }
+  // 开屏视频广告请求参数
+  private splashVideoAdReqParams: advertising.AdRequestParams = {
+    // 'testd7c5cewoj6'为测试专用的广告位ID,App正式发布时需要改为正式的广告位ID
+    adId: 'testd7c5cewoj6',
+    adType: AdType.SPLASH_AD,
+    adCount: 1,
+    oaid: this.oaid
+  }
+  // 开屏图片广告请求参数
+  private splashImageAdReqParams: advertising.AdRequestParams = {
+    // 'testq6zq98hecj'为测试专用的广告位ID,App正式发布时需要改为正式的广告位ID
+    adId: 'testq6zq98hecj',
+    adType: AdType.SPLASH_AD,
+    adCount: 1,
+    oaid: this.oaid
+  }
+
+  init(UIContext: common.UIAbilityContext, oaid: string) {
+    this.context = UIContext
+    this.oaid = oaid
+    this.requestAd(this.splashImageAdReqParams, this.adOptions)
+  }
+
+  private requestAd(adReqParams: advertising.AdRequestParams, adOptions: advertising.AdOptions): void {
+    // 广告请求回调监听
+    const adLoaderListener: advertising.AdLoadListener = {
+      // 广告请求失败回调
+      onAdLoadFailure: (errorCode: number, errorMsg: string) => {
+        clearTimeout(this.timeOutIndex);
+        YTLog.error(errorCode + ':' + errorMsg)
+
+        if (this.isTimeOut) {
+          return;
+        }
+        emitter.emit('adLoadFail')
+      },
+      // 广告请求成功回调
+      onAdLoadSuccess: (ads: Array<advertising.Advertisement>) => {
+        clearTimeout(this.timeOutIndex);
+        if (this.isTimeOut) {
+          return;
+        }
+        YTLog.info('广告加载成功')
+        if (canIUse("SystemCapability.Advertising.Ads")) {
+          this.ads = ads
+          AppStorage.setOrCreate('isLoaded', true)
+        }
+      }
+    };
+    // 创建AdLoader广告对象
+    const load: advertising.AdLoader = new advertising.AdLoader(this.context);
+    // 调用广告请求接口
+    this.timeOutHandler();
+    load.loadAd(adReqParams, adOptions, adLoaderListener);
+  }
+
+  private timeOutHandler(): void {
+    this.isTimeOut = false;
+    // 超时处理
+    this.timeOutIndex = setTimeout(() => {
+      this.isTimeOut = true;
+      const options: router.RouterOptions = {
+        // 开发者可根据项目实际情况修改超时之后要跳转的目标页面
+        url: 'pages/Index',
+      };
+      router.pushUrl(options);
+      hilog.error(0x0000, 'testTag', '%{public}s', 'load ad time out');
+    }, this.timeOutDuration);
+  }
+}
+
+export const jhStartAd = new JhStartAd()

+ 22 - 0
commons/basic/src/main/ets/components/ReviseImgHeaderBuilder.ets

@@ -0,0 +1,22 @@
+import { BarType } from '../models'
+
+@Builder
+export function reviseImgHeaderBuilder(items: BarType<undefined>[]) {
+  Column() {
+    ForEach(items, (item: BarType<undefined>, index) => {
+      Text(item.text)
+        .width('100%')
+        .height(43)
+        .textAlign(TextAlign.Center)
+        .onClick(item.click)
+      if (index < items.length - 1) {
+        Text('')
+          .width('100%')
+          .height(index == items.length - 2 ? 6 : 1)
+          .backgroundColor('#F8F8F8')
+      }
+    })
+  }
+  .height('100%')
+  .padding({ top: 4 })
+}

+ 60 - 0
commons/basic/src/main/ets/components/ShowBannerAd.ets

@@ -0,0 +1,60 @@
+import { advertising, AutoAdComponent } from '@kit.AdsKit';
+import { BannerAd } from '../ads/BannerAd';
+import { hilog } from '@kit.PerformanceAnalysisKit';
+import { AdStatus } from '../constants';
+
+@Component
+export struct ShowBannerAd {
+  private bannerAd = new BannerAd()
+  @State visibilityState: Visibility = Visibility.Hidden;
+
+  build() {
+    Column() {
+      this.buildBannerView()
+    }
+    .borderRadius(4)
+    // .backgroundColor($r('app.color.main_green'))
+    .height(54)
+    .width('100%')
+    .clip(true)
+  }
+
+  @Builder
+  buildBannerView() {
+    Column() {
+      AutoAdComponent({
+        adParam: this.bannerAd.adParam,
+        adOptions: this.bannerAd.adOptions,
+        displayOptions: this.bannerAd.adDisplayOptions,
+        interactionListener: {
+          onStatusChanged: (status: string, ad: advertising.Advertisement, data: string) => {
+            hilog.info(0x0000, 'testTag', '%{public}s', `status is ${status}`);
+            switch (status) {
+              case AdStatus.AD_OPEN:
+                hilog.info(0x0000, 'testTag', '%{public}s', 'Status is onAdOpen');
+                break;
+              case AdStatus.AD_CLICKED:
+                hilog.info(0x0000, 'testTag', '%{public}s', 'Status is onAdClick');
+                break;
+              case AdStatus.AD_CLOSED:
+                hilog.info(0x0000, 'testTag', '%{public}s', 'Status is onAdClose');
+                this.visibilityState = Visibility.None;
+                break;
+              case AdStatus.AD_LOAD:
+                hilog.info(0x0000, 'testTag', '%{public}s', 'Status is onAdLoad');
+                this.visibilityState = Visibility.Visible;
+                break;
+              case AdStatus.AD_FAIL:
+                hilog.error(0x0000, 'testTag', '%{public}s', 'Status is onAdFail');
+                this.visibilityState = Visibility.None;
+                break;
+            }
+          }
+        }
+      })
+    }
+    .width('100%')
+    .aspectRatio(this.bannerAd.ratio)
+    .visibility(this.visibilityState)
+  }
+}

+ 24 - 0
commons/basic/src/main/ets/components/ShowJHRewardAd.ets

@@ -0,0 +1,24 @@
+import { common } from '@kit.AbilityKit';
+import { RewardAd } from '../ads/RewardAd';
+import { emitter } from '@kit.BasicServicesKit';
+
+
+@Component
+export struct ShowJHRewardAd {
+  private context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
+  @BuilderParam showAdStruct: () => void
+
+  aboutToAppear(): void {
+    emitter.on('showJHAd', () => {
+      new RewardAd(this.context)
+    })
+  }
+
+  aboutToDisappear(): void {
+    emitter.off('showJHAd')
+  }
+
+  build() {
+    this.showAdStruct()
+  }
+}

+ 52 - 0
commons/basic/src/main/ets/components/YTHeader.ets

@@ -0,0 +1,52 @@
+import { yTRouter } from '../utils/YTRouter'
+
+@Component
+export struct YTHeader {
+  @BuilderParam leftComp?: () => void
+  @BuilderParam RightComp?: () => void
+  @BuilderParam CenterComp?: () => void
+  backArrow: boolean = true
+  title: string = ''
+  click = () => {
+    yTRouter.routerBack()
+  }
+
+  build() {
+    Stack() {
+      Row() {
+        if (this.backArrow && !this.leftComp) {
+          Image($r('app.media.ic_back'))
+            .width(24)
+            .margin({ left: 16 })
+            .onClick(this.click)
+        }
+        if (this.leftComp) {
+          this.leftComp()
+        }
+
+        if (this.RightComp) {
+          this.RightComp()
+        }
+      }
+      .width('100%')
+      .justifyContent(this.RightComp ? FlexAlign.SpaceBetween : FlexAlign.Start)
+
+      if (this.CenterComp || this.title)
+      Row() {
+        if (this.CenterComp) {
+          this.CenterComp()
+        }
+        if (this.title && !this.CenterComp) {
+          Text(this.title)
+            .fontSize(18)
+            .fontWeight(700)
+        }
+      }
+      .width('100%')
+      .justifyContent(FlexAlign.Center)
+      .hitTestBehavior(HitTestMode.None)
+    }
+    .height(44)
+    .backgroundColor(Color.White)
+  }
+}

+ 29 - 0
commons/basic/src/main/ets/components/YtButton.ets

@@ -0,0 +1,29 @@
+@Component
+export struct YtButton {
+  btHeight: Length = 37
+  btWidth: Length = '100%'
+  btContent: string = ''
+  click = () => {
+
+  }
+  btPadding?: Length | Padding
+  btFontSize: number = 12
+  bgc: ResourceColor = $r('app.color.main_ac_color_dark')
+  btFontColor: ResourceColor = Color.White
+  btBorder?: BorderOptions
+  btState: boolean = true
+
+  build() {
+    Button(this.btContent)
+      .buttonStyle(ButtonStyleMode.NORMAL)
+      .fontSize($r(`app.float.page_text_font_size_${this.btFontSize}`))
+      .fontColor(this.btFontColor)
+      .backgroundColor(this.bgc)
+      .height(this.btHeight)
+      .width(this.btWidth)
+      .onClick(this.click)
+      .padding(this.btPadding ?? {})
+      .border(this.btBorder)
+      .enabled(this.btState)
+  }
+}

+ 10 - 0
commons/basic/src/main/ets/constants/index.ets

@@ -0,0 +1,10 @@
+export enum AdStatus {
+  AD_LOAD = 'onAdLoad',
+  AD_FAIL = 'onAdFail',
+  AD_OPEN = 'onAdOpen',
+  AD_CLICKED = 'onAdClick',
+  AD_CLOSED = 'onAdClose',
+  AD_REWARDED = 'onAdReward',
+  AD_VIDEO_START = 'onVideoPlayBegin',
+  AD_COMPLETED = 'onVideoPlayEnd'
+}

+ 125 - 0
commons/basic/src/main/ets/models/UserInfo.ets

@@ -0,0 +1,125 @@
+import { emitter } from '@kit.BasicServicesKit'
+
+export class UserInfo {
+  KEY: string = 'UserInfo'
+  isLogin: boolean = false
+  private memberName?: string
+  private memberPhone?: string
+  private memberEmail?: string
+  private memberIcon?: string
+  private aiCount?: number
+  private userId?: number
+  private memberScore?: number
+  private token?: string
+
+  setUserInfoAndLogin(_userInfo: UserInfo) {
+    userInfo.isLogin = true
+    if (_userInfo.token) {
+      userInfo.token = _userInfo.token
+    }
+    userInfo.memberEmail = _userInfo.memberEmail
+    userInfo.memberIcon = _userInfo.memberIcon
+    userInfo.memberName = _userInfo.memberName
+    userInfo.userId = _userInfo.userId
+    userInfo.memberScore = _userInfo.memberScore
+    userInfo.aiCount = _userInfo.aiCount
+    userInfo.memberPhone = _userInfo.memberPhone
+    emitter.emit('refreshList')
+    AppStorage.setOrCreate(userInfo.KEY, userInfo)
+  }
+
+  logout() {
+    userInfo.isLogin = false
+    userInfo.token = undefined
+    userInfo.memberEmail = undefined
+    userInfo.memberIcon = undefined
+    userInfo.memberName = undefined
+    userInfo.userId = undefined
+    userInfo.memberScore = undefined
+    userInfo.aiCount = undefined
+    userInfo.memberPhone = undefined
+    emitter.emit('cleanList')
+    AppStorage.setOrCreate(userInfo.KEY, userInfo)
+    AppStorage.setOrCreate('token', undefined)
+  }
+
+  checkLogin() {
+    return userInfo.isLogin
+  }
+
+  getToken() {
+    return userInfo.token
+  }
+
+
+  setToken(token: string) {
+    userInfo.token = token
+    AppStorage.setOrCreate<string>('token', token)
+    AppStorage.setOrCreate(userInfo.KEY, userInfo)
+  }
+
+  getHeadImg() {
+    return userInfo.memberIcon
+  }
+
+  setHeadImg(headImg: string) {
+    userInfo.memberIcon = headImg
+    AppStorage.setOrCreate(userInfo.KEY, userInfo)
+  }
+
+  getEmail() {
+    return userInfo.memberEmail
+  }
+
+  setEmail(email: string) {
+    userInfo.memberEmail = email
+    AppStorage.setOrCreate(userInfo.KEY, userInfo)
+  }
+
+  getName() {
+    return userInfo.memberName
+  }
+
+  setName(name: string) {
+    userInfo.memberName = name
+    AppStorage.setOrCreate(userInfo.KEY, userInfo)
+  }
+
+  getId() {
+    return userInfo.userId
+  }
+
+  setId(id: number) {
+    userInfo.userId = id
+    AppStorage.setOrCreate(userInfo.KEY, userInfo)
+  }
+
+  getGoldNum() {
+    return userInfo.memberScore
+  }
+
+  setGoldNum(goldNum: number) {
+    userInfo.memberScore = goldNum
+    AppStorage.setOrCreate(userInfo.KEY, userInfo)
+  }
+
+  getAiNum() {
+    return userInfo.aiCount
+  }
+
+  setAiNum(aiNum: number) {
+    userInfo.aiCount = aiNum
+    AppStorage.setOrCreate(userInfo.KEY, userInfo)
+  }
+
+  getPhoneNumber() {
+    return userInfo.memberPhone
+  }
+
+  setPhoneNumber(phoneNumber: string) {
+    userInfo.memberPhone = phoneNumber
+    AppStorage.setOrCreate(userInfo.KEY, userInfo)
+  }
+}
+
+export const userInfo = new UserInfo()

+ 19 - 0
commons/basic/src/main/ets/models/index.ets

@@ -0,0 +1,19 @@
+export interface BarType<T> {
+  src?: ResourceStr,
+  acSrc?: ResourceStr,
+  text?: string,
+  message?: string,
+  click?: () => void,
+  number?: number,
+  state?: T,
+  date?: string,
+  index?: number,
+  finally?: () => void
+}
+
+export type reqString = Record<string, string>
+
+export interface avoidType {
+  top?: number,
+  bottom?: number
+}

+ 1 - 1
commons/basic/src/main/ets/pages/Index.ets

@@ -7,7 +7,7 @@ struct Index {
     Row() {
       Column() {
         Text(this.message)
-          .fontSize($r('app.float.page_text_font_size'))
+          .fontSize(10)
           .fontWeight(FontWeight.Bold)
           .onClick(() => {
             this.message = 'Welcome';

+ 0 - 3
commons/basic/src/main/ets/utils/Calc.ets

@@ -1,3 +0,0 @@
-export function add(a: number, b: number) {
-  return a + b;
-}

+ 24 - 0
commons/basic/src/main/ets/utils/CopyText.ets

@@ -0,0 +1,24 @@
+import { IBestToast } from '@ibestservices/ibest-ui';
+import { pasteboard } from '@kit.BasicServicesKit';
+
+
+export function copyText(text: string, showToast: boolean = true) {
+  const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
+  const systemPasteboard = pasteboard.getSystemPasteboard();
+  systemPasteboard.setData(pasteboardData); // 将数据放入剪切板
+  systemPasteboard.getData().then((data) => {
+    if (data) {
+      if (showToast) {
+        IBestToast.show({
+          type: "success",
+          message: "复制成功"
+        })
+      }
+    } else {
+      IBestToast.show({
+        type: "fail",
+        message: "复制失败"
+      })
+    }
+  })
+}

+ 10 - 0
commons/basic/src/main/ets/utils/DebounceLoad.ets

@@ -0,0 +1,10 @@
+// 防抖函数实现
+export function YtDebounce(del: () => void, delay: number) {
+  let timer: number | null = null;
+  return () => {
+    if (timer) {
+      clearTimeout(timer);
+    }
+    timer = setTimeout(del, delay);
+  };
+}

+ 10 - 0
commons/basic/src/main/ets/utils/FormatDate.ets

@@ -0,0 +1,10 @@
+export function formatDate(date: Date): string {
+  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');
+
+  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+}

+ 79 - 0
commons/basic/src/main/ets/utils/HuaWeiAuthPlugin.ets

@@ -0,0 +1,79 @@
+import { authentication } from '@kit.AccountKit'
+import { util } from '@kit.ArkTS'
+import { hilog } from '@kit.PerformanceAnalysisKit'
+import { BusinessError } from '@kit.BasicServicesKit'
+import { YTLog } from '../../../../Index'
+
+class HuaweiAuthPlugin {
+  // 代替了用户输入用户名密码
+  async requestAuth() {
+    // 1. 创建一个Account Kit授权请求对象,可通过返回值设置请求参数。
+    const huaweiIdProvider = new authentication.HuaweiIDProvider()
+    const authCreateRequest = huaweiIdProvider.createAuthorizationWithHuaweiIDRequest() // 创建华为授权请求
+    // 2. 添加请求参数
+    // authCreateRequest.scopes = ['phone']
+    authCreateRequest.permissions = ['serviceauthcode']
+    authCreateRequest.forceAuthorization = true
+    // 3. 执行授权请求,获取认证码
+    const authController = new authentication.AuthenticationController(getContext())
+    const authResponse: authentication.AuthorizationWithHuaweiIDResponse =
+      await authController.executeRequest(authCreateRequest) // 执行授权请求
+    const authorizationCode = authResponse.data?.authorizationCode // 华为授权code码(有了它 有了用户信息)
+    return authorizationCode
+  }
+
+  async getPhoneNumber() {
+    // 创建授权请求,并设置参数
+    const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();
+    // 获取手机号需要传如下scope,传参数之前需要先申请对应scope权限,才能返回对应数据
+    authRequest.scopes = ['phone'];
+    // 获取code需传如下permission
+    authRequest.permissions = ['serviceauthcode'];
+    //用户是否需要登录授权,该值为true且用户未登录或未授权时,会拉起用户登录或授权页面
+    authRequest.forceAuthorization = true;
+    // 用于防跨站点请求伪造
+    authRequest.state = util.generateRandomUUID();
+
+    // 执行请求
+    try {
+      const controller = new authentication.AuthenticationController(getContext(this));
+      controller.executeRequest(authRequest).then((data) => {
+        const authorizationWithHuaweiIDResponse = data as authentication.AuthorizationWithHuaweiIDResponse;
+        const state = authorizationWithHuaweiIDResponse.state;
+        if (state && authRequest.state !== state) {
+          hilog.error(0x0000, 'testTag', `Failed to authorize. The state is different, response state: ${state}`);
+          return;
+        }
+        hilog.info(0x0000, 'testTag', 'Succeeded in authentication.');
+        const authorizationWithHuaweiIDCredential = authorizationWithHuaweiIDResponse.data!;
+        const code = authorizationWithHuaweiIDCredential.authorizationCode;
+        const unionID = authorizationWithHuaweiIDCredential.unionID;
+        const openID = authorizationWithHuaweiIDCredential.openID;
+        // 开发者处理code、unionID、openID
+      }).catch((err: BusinessError) => {
+        YTLog.error(err)
+      });
+    } catch (error) {
+      YTLog.error(error)
+    }
+  }
+
+  // getHuaweiIDState  api12 支持
+
+  async cancelAuth() {
+    try {
+      // 1. 创建一个Account Kit授权请求对象,可通过返回值设置请求参数。
+      const huaweiIdProvider = new authentication.HuaweiIDProvider()
+      const authCancelRequest = huaweiIdProvider.createCancelAuthorizationRequest()
+      // 2. 取消授权
+      const authController = new authentication.AuthenticationController(getContext())
+      await authController.executeRequest(authCancelRequest)
+      return true
+    } catch (e) {
+      console.log('yt-logger', JSON.stringify(e))
+      return false
+    }
+  }
+}
+
+export const huaweiAuthPlugin = new HuaweiAuthPlugin()

+ 45 - 0
commons/basic/src/main/ets/utils/PermissionControl.ets

@@ -0,0 +1,45 @@
+import { abilityAccessCtrl, bundleManager, PermissionRequestResult, Permissions } from '@kit.AbilityKit';
+
+class PermissionControl {
+  private tokenID: number = 0
+  //记录已申请权限,方便到时候调用checkPermission时传参
+  permissions: Permissions[] = []
+  //上一次使用checkPermission校验的权限
+  private lastPermission?: Permissions
+  private context?: Context
+  private atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
+
+  //TODO 需要在windowStage中初始化
+  async init(context: Context) {
+    this.context = context
+    const res = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION)
+    this.tokenID = res.appInfo.accessTokenId
+  }
+
+  //申请权限
+  askPermission(permissions: Permissions[], callback: (res: PermissionRequestResult) => void) {
+    this.permissions = permissions
+    this.atManager.requestPermissionsFromUser(this.context, permissions, (err, data) => {
+      callback(data)
+    })
+  }
+
+  //二次申请权限 不传参数就是上次校验的权限
+  secondAskPermission(permission: Permissions | undefined = this.lastPermission) {
+    if (permission) {
+      this.atManager.requestPermissionOnSetting(this.context, this.permissions)
+    }
+  }
+
+  //校验权限
+  checkPermission(permission: Permissions) {
+    const isPermission = this.atManager.checkAccessTokenSync(this.tokenID, permission)
+    this.lastPermission = permission
+    if (isPermission == -1) {
+      return false
+    }
+    return true
+  }
+}
+
+export const permissionControl = new PermissionControl()

+ 22 - 0
commons/basic/src/main/ets/utils/TakePicture.ets

@@ -0,0 +1,22 @@
+import { fileIo, fileUri } from '@kit.CoreFileKit';
+import { camera, cameraPicker as picker } from '@kit.CameraKit';
+
+export async function takePicture() {
+  let pathDir = getContext().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(getContext(), [picker.PickerMediaType.PHOTO], //(如果需要录像可以添加) picker.PickerMediaType.VIDEO
+      pickerProfile);
+  if (!result.resultUri) {
+    return Promise.reject('用户未拍照')
+  }
+  return filePath
+}

+ 70 - 0
commons/basic/src/main/ets/utils/UploadFile.ets

@@ -0,0 +1,70 @@
+import { photoAccessHelper } from '@kit.MediaLibraryKit'
+import { util } from '@kit.ArkTS'
+import { fileIo } from '@kit.CoreFileKit'
+import { YTLog } from '../../../../Index'
+
+
+export class Upload {
+  // static uploadImage(context: Context) {
+  //   const option = new photoAccessHelper.PhotoSelectOptions()
+  //   option.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
+  //   option.maxSelectNumber = 5 // 设置最多选择5张
+  //   option.isOriginalSupported = true
+  //
+  //   const picker = new photoAccessHelper.PhotoViewPicker()
+  //   picker.select(option)
+  //     .then(async res => { // 改为async/await语法
+  //       try {
+  //         const formData = new FormData()
+  //
+  //         // 并行处理多图
+  //         await Promise.all(res.photoUris.map(async (uri, index) => {
+  //           const photoName = util.generateRandomUUID()
+  //           const photoType = '.' + uri.split('.').pop()
+  //           const fullPath = context.cacheDir + '/' + photoName + photoType
+  //
+  //           const photo = fileIo.openSync(uri)
+  //           try {
+  //             await fileIo.copyFile(photo.fd, fullPath)
+  //             formData.append('file' + index, 'internal://cache/' + photoName + photoType)
+  //           } finally {
+  //             fileIo.closeSync(photo.fd) // 确保关闭文件描述符
+  //           }
+  //         }))
+  //
+  //
+  //         // 统一提交所有文件
+  //         // 这里添加实际的上传代码,例如:
+  //         // const response = await axios.post('your_api_url', formData)
+  //         // YTLog.info('Upload success:' + JSON.stringify(response.data))
+  //
+  //       } catch (err) {
+  //         YTLog.error('Upload failed:' + JSON.stringify(err))
+  //       }
+  //     })
+  // }
+
+  static selectImage(context: Context, isReady: (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 = context.cacheDir + '/' + photoName + photoType
+        const photo = fileIo.openSync(res.photoUris[0])
+        try {
+          fileIo.copyFile(photo.fd, fullPath)
+            .then(() => {
+              YTLog.info(fullPath)
+              isReady(fullPath)
+            })
+        } catch (err) {
+          YTLog.error(err)
+        }
+      })
+  }
+}
+

+ 40 - 0
commons/basic/src/main/ets/utils/YTAvoid.ets

@@ -0,0 +1,40 @@
+import { window } from '@kit.ArkUI'
+import { avoidType } from '../models'
+
+export class YtAvoid {
+  static readonly safeTopKey: string = 'safeTop'
+  static readonly safeBottomKey: string = 'bottomTop'
+
+ static setAvoid(avoid: avoidType) {
+  if (typeof avoid.top != 'undefined') {
+    AppStorage.setOrCreate<number>(YtAvoid.safeTopKey, px2vp(avoid.top))
+  }
+  if (typeof avoid.bottom != 'undefined') {
+    AppStorage.setOrCreate<number>(YtAvoid.safeBottomKey, px2vp(avoid.bottom))
+  }
+}
+
+  static getTop() {
+    return AppStorage.get<number>(YtAvoid.safeTopKey)
+  }
+
+  static getBottom() {
+
+    return AppStorage.get<number>(YtAvoid.safeBottomKey)
+  }
+
+  static setTop(safeTop: number) {
+    AppStorage.setOrCreate<number>(YtAvoid.safeTopKey, safeTop)
+  }
+
+
+  static setBottom(safeBottom: number) {
+    AppStorage.setOrCreate<number>(YtAvoid.safeBottomKey, safeBottom)
+  }
+
+  static setStatusBarContentColor(color: string, context: Context) {
+    window.getLastWindow(context, (err, windowClass) => {
+      windowClass.setWindowSystemBarProperties({ statusBarContentColor: color })
+    })
+  }
+}

+ 54 - 0
commons/basic/src/main/ets/utils/YTBreakPoint.ets

@@ -0,0 +1,54 @@
+declare interface BreakPointTypeOption<T> {
+  xs?: T
+  sm?: T
+  md?: T
+  lg?: T
+}
+
+export class BreakPointType<T> {
+  options: BreakPointTypeOption<T>
+
+  constructor(option: BreakPointTypeOption<T>) {
+    this.options = option
+  }
+
+  getValue(currentBreakPoint: string) {
+    if (currentBreakPoint === 'xs') {
+      return this.options.xs
+    } else if (currentBreakPoint === 'sm') {
+      return this.options.sm
+    } else if (currentBreakPoint === 'md') {
+      return this.options.md
+    } else if (currentBreakPoint === 'lg') {
+      return this.options.lg
+    } else {
+      return undefined
+    }
+  }
+}
+
+
+class YTBreakPoint {
+  readonly KEY = 'bp'
+
+  setBreakPoint(width: number) {
+    const vpWidth = px2vp(width)
+    let bp = ''
+    if (vpWidth < 320) {
+      bp = 'xs'
+    } else if (vpWidth < 600) {
+      bp = 'sm'
+    } else if (vpWidth < 840) {
+      bp = 'md'
+    } else {
+      bp = 'lg'
+    }
+    AppStorage.setOrCreate<string>(this.KEY, bp)
+  }
+
+  getBreakPoint() {
+    return AppStorage.get<string>(this.KEY)
+  }
+}
+
+export const yTBreakPoint = new YTBreakPoint()

+ 10 - 0
commons/basic/src/main/ets/utils/YTLog.ets

@@ -0,0 +1,10 @@
+import { Log } from '@abner/log'
+
+Log.init({
+  tag: "YTLog",
+  domain: 0x0000,
+  close: false,
+  isHilog: true,
+  showLogLocation: true,
+  logSize: 800
+})

+ 233 - 0
commons/basic/src/main/ets/utils/YTRequest.ets

@@ -0,0 +1,233 @@
+import axios, {
+  AxiosError,
+  AxiosHeaders,
+  AxiosProgressEvent,
+  AxiosRequestConfig,
+  AxiosResponse,
+  FormData,
+  InternalAxiosRequestConfig
+} from '@ohos/axios';
+import { IBestToast, YTLog } from '../../../../Index';
+import { reqString } from '../models';
+import { UserInfo, userInfo } from '../models/UserInfo';
+import { formatDate } from './FormatDate';
+import { huaweiAuthPlugin } from './HuaWeiAuthPlugin';
+import { yTRouter } from './YTRouter';
+
+
+export const baseURL: string = 'https://hm-test.ytpm.net/prod-api'
+
+export const instance = axios.create({
+  baseURL,
+  timeout: 5000
+})
+
+// 添加请求拦截器
+instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
+
+  // 对请求数据做点什么
+  if (AppStorage.get<string>('token')) {
+    config.headers.Authorization = AppStorage.get<string>('token')
+  }
+  return config;
+}, (error: AxiosError) => {
+  // 对请求错误做些什么
+
+  return Promise.reject(error);
+});
+
+
+// 添加响应拦截器
+instance.interceptors.response.use((response: AxiosResponse) => {
+  // 对响应数据做点什么
+  YTLog.info(response)
+  // 对响应错误做点什么
+  if (response.data.code == 401) {
+
+    yTRouter.router2LoginPage()
+    return Promise.reject('401');
+  }
+
+  return response.data.data;
+}, (error: AxiosError) => {
+  YTLog.error(error)
+  setTimeout(() => {
+    IBestToast.hide()
+    setTimeout(() => {
+      IBestToast.show({ message: '请求超时,请检查网络', type: 'fail' })
+    }, 100)
+  }, 1000)
+  return Promise.reject(error);
+});
+
+
+class YtRequest {
+  get<T>(url: string, params?: Record<string, string | number | boolean>,
+    headers?: Record<string, string>): Promise<T> {
+    return instance.get<null, T, null>(url, { params, headers })
+  }
+
+  post<T, D>(url: string, data?: D, params?: Record<string, string | number | boolean>, headers?: AxiosHeaders) {
+    return instance.post<null, T, D>(url, data, { params, headers })
+  }
+
+  upPost<T, D>(url: string, data: D, configs?: AxiosRequestConfig<D>) {
+    return instance.post<string, T, D>(url, data, configs)
+  }
+
+  //获取验证码
+  getCaptcha(phonenumber: string, success: (res: string) => void, fail: (err: Error) => void) {
+    yTRequest.post<reqString, reqString>('/api/aiAccount/member/sendSmsCode',
+      { 'phonenumber': phonenumber })
+      .then(res => {
+        success(res['uuid'])
+      })
+      .catch((err: Error) => {
+        fail(err)
+      })
+  }
+
+  //手机号登录
+  phonenumberLogin(param: reqString) {
+    const uuid = AppStorage.get<string>('uuid')
+    if (uuid !== undefined) {
+      yTRequest.post<reqString, reqString>('/api/aiAccount/member/phoneLogin', {
+        'phonenumber': param['phonenumber'],
+        'smsCode': param['captcha'],
+        'uuid': uuid
+      })
+        .then(res => {
+          userInfo.setToken(res['token'])
+          yTRequest.refreshUserInfo(() => {
+            IBestToast.show({ message: '登录成功', type: 'success' })
+            yTRouter.routerBack()
+          })
+        })
+    }
+  }
+
+  //华为登录
+  huaweiLogin() {
+    try {
+      IBestToast.showLoading()
+      huaweiAuthPlugin.requestAuth()
+        .then(res => {
+          yTRequest.post<reqString, reqString>('/api/aiAccount/member/hmLogin',
+            { 'code': res } as reqString)
+            .then(data => {
+              const token = data['token']
+              userInfo.setToken(token)
+              yTRequest.refreshUserInfo((userInfo) => {
+                // YTLog.info(userInfo)
+                IBestToast.hide()
+                setTimeout(() => {
+                  IBestToast.show({ message: '登录成功', type: 'success' })
+                }, 100)
+
+                yTRouter.routerBack()
+              })
+
+            })
+            .catch((err: Error) => {
+              // AlertDialog.show({ message: JSON.stringify(err, null, 2) })
+              // IBestToast.hide()
+              YTLog.error(err)
+            })
+        })
+        .catch((e: Error) => {
+          // AlertDialog.show({ message: JSON.stringify(e, null, 2) })
+          YTLog.error(e)
+          // IBestToast.hide()
+        })
+    } catch (e) {
+      // AlertDialog.show({ message: JSON.stringify(e, null, 2) })
+      YTLog.error(e)
+      // IBestToast.hide()
+    }
+  }
+
+  //刷新用户信息
+  refreshUserInfo(success?: (res: UserInfo) => void) {
+    yTRequest.post<UserInfo, null>('/api/aiAccount/member/info')
+      .then(res => {
+        userInfo.setUserInfoAndLogin(res)
+        YTLog.info(userInfo)
+        success?.(res)
+      })
+  }
+
+  //上传文件
+  uploadFile(context: Context, fullpath: string, success: (url: string) => void) {
+    const formData = new FormData()
+    formData.append('file', fullpath)
+    yTRequest.upPost<reqString, FormData>('/common/upload', formData, {
+      headers: { 'Content-Type': 'multipart/form-data' },
+      context,
+      onUploadProgress: (progressEvent: AxiosProgressEvent): void => {
+        YTLog.info(progressEvent && progressEvent.loaded && progressEvent.total ?
+          Math.ceil(progressEvent.loaded / progressEvent.total * 100) + '%' : '0%', 'uploadFile');
+      }
+    })
+      .then(res => {
+        const url = res['url']
+        success(url)
+      })
+  }
+
+  // 修改用户头像
+  uploadHeadImg(context: Context, fullpath: string, success: () => void) {
+    const formData = new FormData()
+    formData.append('file', fullpath)
+
+    yTRequest.upPost<reqString, FormData>('/common/upload', formData, {
+      headers: { 'Content-Type': 'multipart/form-data' },
+      context,
+      onUploadProgress: (progressEvent: AxiosProgressEvent): void => {
+        YTLog.info(progressEvent && progressEvent.loaded && progressEvent.total ?
+          Math.ceil(progressEvent.loaded / progressEvent.total * 100) + '%' : '0%', 'uploadFile');
+      }
+    })
+      .then(res => {
+        const url = res['url']
+        yTRequest.post<null, reqString>('/api/aiAccount/member/modifyMemberIcon', { 'memberIcon': url })
+          .then(() => {
+            success()
+          })
+          .catch((e: Error) => {
+            YTLog.error(e)
+            // IBestToast.show({ message: '头像上传失败', type: 'fail' })
+          })
+      })
+      .catch((e: Error) => {
+        YTLog.error(e)
+      })
+  }
+
+  // 修改用户昵称
+  changeNickname(name: string, success: () => void) {
+    yTRequest.post<null, reqString>('/api/aiAccount/member/modifyMemberName', { 'memberName': name })
+      .then(() => {
+        yTRequest.refreshUserInfo(() => {
+          success()
+        })
+      })
+  }
+
+  //问题反馈
+  questionBack(des: string, createBy: string) {
+
+    yTRequest.post<null, reqString>('/api/aiAccount/question/saveQuestion', {
+      'backQuestion': des,
+      'createBy': createBy,
+      'createTime': formatDate(new Date()),
+      'id': userInfo.getId()?.toString()!
+    })
+      .then()
+      .catch((e: Error) => {
+        YTLog.error(e)
+      })
+  }
+}
+
+export const yTRequest = new YtRequest()
+

+ 29 - 0
commons/basic/src/main/ets/utils/YTRouter.ets

@@ -0,0 +1,29 @@
+class YTRouter extends NavPathStack {
+
+
+  router2SettingPage() {
+    yTRouter.pushPathByName('SettingPage', '')
+  }
+
+  router2SuggestionPage() {
+    yTRouter.pushPathByName('SuggestionPage', '')
+  }
+
+  router2AgreementPage(param: '关于我们' | '隐私政策' | '用户协议') {
+    yTRouter.pushPathByName('AgreementPage', param)
+  }
+
+  getAgreementPageParam() {
+    return yTRouter.getParamByName('AgreementPage').pop() as '关于我们' | '隐私政策' | '用户协议'
+  }
+
+  router2LoginPage() {
+    yTRouter.pushPathByName('LoginPage', '')
+  }
+
+  routerBack() {
+    yTRouter.pop()
+  }
+}
+
+export const yTRouter = new YTRouter()

+ 124 - 0
commons/basic/src/main/ets/utils/YTToast.ets

@@ -0,0 +1,124 @@
+import { BusinessError } from '@kit.BasicServicesKit';
+import { ComponentContent, promptAction } from '@kit.ArkUI';
+import { UIContext } from '@ohos.arkui.UIContext';
+import { BarType,  YTLog } from '../../../../Index';
+
+
+interface InitOption {
+  context: UIContext;
+  options: promptAction.BaseDialogOptions;
+}
+
+class Params<T> implements BarType<T> {
+  src?: ResourceStr
+  acSrc?: ResourceStr
+  text?: string
+  index?: number
+  number?: number | undefined
+  message?: string
+  click?: () => void
+
+  constructor(item: BarType<undefined>) {
+    this.src = item.src;
+    this.text = item.text;
+    this.index = item.index;
+    this.number = item.number;
+    this.click = item.click;
+    this.message = item.message;
+    this.acSrc = item.acSrc;
+  }
+}
+
+export class YTToast {
+  private declare ctx: UIContext;
+  private declare contentNode: ComponentContent<Object> | null;
+  private declare options: promptAction.BaseDialogOptions;
+
+  init(initOption: InitOption) {
+    this.ctx = initOption.context;
+    this.options = initOption.options;
+  }
+
+  setContext(context: UIContext) {
+    this.ctx = context;
+  }
+
+  setContentNode(node: ComponentContent<Object>) {
+    this.contentNode = node;
+  }
+
+  setOptions(options: promptAction.BaseDialogOptions) {
+    this.options = options;
+  }
+
+  openToast(builder: WrappedBuilder<[
+    BarType<undefined>
+  ]>, item: BarType<undefined>) {
+    this.contentNode =
+      new ComponentContent(this.ctx, builder, new Params<undefined>(item));
+    this.ctx.getPromptAction()
+      .openCustomDialog(this.contentNode, this.options)
+      .then(() => {
+        console.info('OpenCustomDialog complete.')
+      })
+      .catch((error: BusinessError) => {
+        let message = (error as BusinessError).message;
+        let code = (error as BusinessError).code;
+        console.error(`OpenCustomDialog args error code is ${code}, message is ${message}`);
+      })
+  }
+
+  // doubleConfirm(item: BarType<undefined>) {
+  //   this.openToast(wrapBuilder(yTDoubleConfirm), item)
+  // }
+  //
+  // getReward(item: BarType<undefined>) {
+  //   this.openToast(wrapBuilder(rewardTreasure), item)
+  // }
+  //
+  // agreePrivacy(item: BarType<undefined>) {
+  //   this.openToast(wrapBuilder(AgreePrivacy), item)
+  // }
+  //
+  // showBillImage(item: BarType<undefined>) {
+  //   this.openToast(wrapBuilder(showBillImage), item)
+  // }
+
+
+  hide() {
+    try {
+      if (this.contentNode !== null) {
+        this.ctx.getPromptAction()
+          .closeCustomDialog(this.contentNode)
+          .then(() => {
+            this.contentNode = null
+            console.info('CloseCustomDialog complete.')
+          })
+          .catch((error: BusinessError) => {
+            let message = (error as BusinessError).message;
+            let code = (error as BusinessError).code;
+            console.error(`CloseCustomDialog args error code is ${code}, message is ${message}`);
+          })
+      }
+    } catch (e) {
+      YTLog.warn(e)
+    }
+  }
+
+  updateDialog(options: promptAction.BaseDialogOptions) {
+    if (this.contentNode !== null) {
+      this.ctx.getPromptAction()
+        .updateCustomDialog(this.contentNode, options)
+        .then(() => {
+          console.info('UpdateCustomDialog complete.')
+        })
+        .catch((error: BusinessError) => {
+          let message = (error as BusinessError).message;
+          let code = (error as BusinessError).code;
+          console.error(`UpdateCustomDialog args error code is ${code}, message is ${message}`);
+        })
+    }
+  }
+}
+
+export const yTToast = new YTToast()

+ 10 - 2
commons/basic/src/main/resources/base/element/color.json

@@ -1,13 +1,21 @@
 {
   "color": [
     {
-      "name": "light_orange",
+      "name": "main_ac_color_light",
       "value": "#FFE4943C"
     },
     {
-      "name": "dark_orange",
+      "name": "main_ac_color_dark",
       "value": "#FFEA663E"
     },
+    {
+      "name": "main_na_color",
+      "value": "#FFC4C4C4"
+    },
+    {
+      "name": "mask_color",
+      "value": "#80000000"
+    },
     {
       "name": "main_blank",
       "value": "#FF161718"

+ 38 - 2
commons/basic/src/main/resources/base/element/float.json

@@ -1,8 +1,44 @@
 {
   "float": [
     {
-      "name": "page_text_font_size",
-      "value": "50fp"
+      "name": "page_text_font_size_8",
+      "value": "8vp"
+    },
+    {
+      "name": "page_text_font_size_9",
+      "value": "8.8vp"
+    },
+    {
+      "name": "page_text_font_size_10",
+      "value": "10vp"
+    },
+    {
+      "name": "page_text_font_size_11",
+      "value": "11vp"
+    },
+    {
+      "name": "page_text_font_size_12",
+      "value": "12vp"
+    },
+    {
+      "name": "page_text_font_size_13",
+      "value": "13vp"
+    },
+    {
+      "name": "page_text_font_size_14",
+      "value": "14vp"
+    },
+    {
+      "name": "page_text_font_size_15",
+      "value": "15vp"
+    },
+    {
+      "name": "page_text_font_size_16",
+      "value": "16vp"
+    },
+    {
+      "name": "page_text_font_size_20",
+      "value": "20vp"
     }
   ]
 }

BIN
commons/basic/src/main/resources/base/media/ic_back.png


+ 2 - 0
features/sketch/oh-package.json5

@@ -7,5 +7,7 @@
   "license": "Apache-2.0",
   "packageType": "InterfaceHar",
   "dependencies": {
+    "basic": "file:../../commons/basic",
+    "user": "file:../../features/user"
   }
 }

+ 1 - 1
features/user/Index.ets

@@ -1 +1 @@
-export { add } from './src/main/ets/utils/Calc';
+export { Mine } from './src/main/ets/views/Mine';

+ 1 - 0
features/user/oh-package.json5

@@ -7,5 +7,6 @@
   "license": "Apache-2.0",
   "packageType": "InterfaceHar",
   "dependencies": {
+    "basic": "file:../../commons/basic"
   }
 }

+ 94 - 0
features/user/src/main/ets/components/LoginInput.ets

@@ -0,0 +1,94 @@
+import { IBestToast, yTRequest } from 'basic'
+import { CodeInputType } from '../models'
+
+@Component
+export struct LoginInput {
+  item?: CodeInputType
+  mgBottom: number = 24
+  @Link inputData: string
+  @Prop phoneNumber: string
+  code: string = ''
+  @State time: number = 61
+  private declare sa: number
+  needCode: boolean = true
+
+  aboutToDisappear(): void {
+    clearInterval(this.sa)
+  }
+
+  build() {
+    Row({ space: 8 }) {
+      Text(this.item?.txt)
+        .width(73)
+        .height(31)
+        .fontSize(12)
+        .border({ width: { bottom: 1 }, color: $r('[basic].color.main_na_color') })
+        .textAlign(TextAlign.Start)
+      TextInput({ placeholder: this.item?.placeHolder, text: $$this.inputData })
+        .layoutWeight(1)
+        .type(InputType.PhoneNumber)
+        .borderRadius(0)
+        .border({ width: { bottom: 1 }, color: $r('[basic].color.main_na_color') })
+        .textAlign(TextAlign.Start)
+        .height(31)
+        .placeholderFont({ size: 12 })
+        .placeholderColor($r('[basic].color.main_na_color'))
+        .backgroundColor(Color.Transparent)
+        .padding(0)
+        .caretColor($r('[basic].color.main_ac_color_dark'))
+      if (this.needCode) {
+        Text(this.time == 61 ? '获取验证码' : this.time + '后重新发送')
+          .position({ right: 0 })
+          .height(31)
+          .fontSize(12)
+          .fontColor($r('[basic].color.main_ac_color_dark'))
+          .onClick(() => {
+            const rep = new RegExp('^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\\d{8}$');
+            if (this.phoneNumber.match(rep) && this.time == 61) {
+              this.time = 60
+              this.sa = setInterval(() => {
+                this.time--
+                if (this.time < 1) {
+                  this.time = 61
+                  clearInterval(this.sa)
+                }
+              }, 1000)
+              // TODO  此处放置验证码获取逻辑
+              yTRequest.getCaptcha(this.phoneNumber, (res) => {
+                if (!res) {
+                  IBestToast.show({ type: 'warning', message: '请勿重复发送验证码' })
+                  return
+                }
+
+                AppStorage.setOrCreate<string>('uuid', res)
+              }, (err) => {
+                IBestToast.show({ type: 'fail', message: '验证码发送失败' })
+                this.time = 61
+              })
+              // yTRequest.post<CodeType, AskCodeNum>('/api/friendcase/member/sendSmsCode',
+              //   { phonenumber: this.phoneNumber })
+              //   .then(res => {
+              //     if (!res) {
+              //       IBestToast.show({ type: 'warning', message: '请勿重复发送验证码' })
+              //       return
+              //     }
+              //
+              //     AppStorage.setOrCreate<CodeType>('smsCode', res)
+              //   })
+              //   .catch((err: Error) => {
+              //     IBestToast.show({ type: 'fail', message: '验证码发送失败' })
+              //     this.time = 61
+              //   })
+            } else {
+              if (!this.phoneNumber.match(rep)) {
+                IBestToast.show({ message: '请输入正确的手机号', type: 'warning' })
+              }
+            }
+          })
+          .enabled(this.time == 61)
+      }
+
+    }
+    .margin({ bottom: this.mgBottom })
+  }
+}

+ 45 - 0
features/user/src/main/ets/components/OtherLoginMethods.ets

@@ -0,0 +1,45 @@
+import { BarType } from 'basic'
+
+
+@Component
+export struct OtherLoginMethods {
+  loginMethodArr: BarType<undefined>[] = []
+
+  build() {
+    Column() {
+      Text('其他登入方式')
+        .fontSize($r('[basic].float.page_text_font_size_10'))
+        .margin({ bottom: 24 })
+      Column({ space: 16 }) {
+        ForEach(this.loginMethodArr, (item: BarType<undefined>) => {
+          LoginMethod(item)
+        })
+      }
+
+    }
+    .alignItems(HorizontalAlign.Start)
+  }
+}
+
+
+@Builder
+function LoginMethod(item: BarType<undefined>) {
+  Stack({ alignContent: Alignment.Start }) {
+    Button(item.text)
+      .border({ width: 1, color: $r('[basic].color.main_ac_color_dark') })
+      .backgroundColor(Color.Transparent)
+      .width('100%')
+      .height(37)
+      .fontSize($r('[basic].float.page_text_font_size_12'))
+      .type(ButtonType.Normal)
+      .fontColor($r('[basic].color.main_ac_color_dark'))
+      .borderRadius(32)
+    Image(item.src)
+      .width(24)
+      .height(24)
+      .offset({ left: 12 })
+
+  }
+  .onClick(item.click)
+
+}

+ 66 - 0
features/user/src/main/ets/components/Terms.ets

@@ -0,0 +1,66 @@
+import { yTRouter } from 'basic'
+
+@Extend(Span)
+function spanEx(click: () => void) {
+  .decoration({ type: TextDecorationType.Underline, color: $r('[basic].color.main_ac_color_dark') })
+  .fontColor($r('[basic].color.main_ac_color_dark'))
+  .onClick(click)
+}
+
+@Component
+export struct Terms {
+  @Link isAgree: boolean
+  mg: number = 39
+
+  build() {
+    Column() {
+      Row() {
+        Text() {
+          Span('我已阅读并同意')
+          // Span('《中国认证服务条款》')
+          //   .spanEx(() => {
+          //
+          //   })
+          //
+          // Span('以及')
+          Span('"用户协议"')
+            .spanEx(() => {
+              yTRouter.router2AgreementPage('用户协议')
+
+            })
+          Span('和')
+          Span('"隐私政策"')
+            .spanEx(() => {
+              yTRouter.router2AgreementPage('隐私政策')
+            })
+          Span('。')
+        }
+        .fontColor('#4D000000')
+        .fontSize($r('[basic].float.page_text_font_size_10'))
+        .margin({ bottom: this.mg })
+      }
+      .width('100%')
+
+      Row() {
+        Text()
+          .height('100%')
+          .aspectRatio(1)
+          .backgroundColor(Color.White)
+          .borderRadius(18)
+      }
+      .padding(1)
+      .alignSelf(ItemAlign.End)
+      .width(39)
+      .aspectRatio(2)
+      .borderRadius(18)
+      .justifyContent(this.isAgree ? FlexAlign.End : FlexAlign.Start)
+      .backgroundColor(this.isAgree ? $r('[basic].color.main_ac_color_dark') : '#D9D9D9')
+      .onClick(() => {
+        animateTo({ duration: 200 }, () => {
+          this.isAgree = !this.isAgree
+        })
+      })
+    }
+
+  }
+}

+ 4 - 0
features/user/src/main/ets/models/index.ets

@@ -0,0 +1,4 @@
+export interface CodeInputType {
+  txt: string,
+  placeHolder: string
+}

+ 41 - 0
features/user/src/main/ets/pages/AgreementPage.ets

@@ -0,0 +1,41 @@
+import {YtAvoid, YTHeader, yTRouter } from 'basic'
+import { webview } from '@kit.ArkWeb'
+
+@Builder
+function agreementBuilder() {
+  NavDestination() {
+    AgreementPage()
+  }
+  .hideTitleBar(true)
+}
+
+@Component
+struct AgreementPage {
+  @State title: '隐私政策' | '关于我们' | '用户协议' = '隐私政策'
+  @StorageProp(YtAvoid.safeTopKey) safeTop: number = 0
+  @StorageProp(YtAvoid.safeBottomKey) safeBottom: number = 0
+  controller: webview.WebviewController = new webview.WebviewController()
+
+  aboutToAppear(): void {
+    this.title = yTRouter.getAgreementPageParam()
+    // IBestToast.showLoading({
+    //   message: '加载中...'
+    // })
+  }
+
+  build() {
+    Column({ space: 12 }) {
+      YTHeader({ title: this.title })
+      Column() {
+        Web({ src: 'https://www.baidu.com', controller: this.controller })
+          .width('100%')
+          .layoutWeight(1)
+          .onPageEnd(() => {
+            // IBestToast.hide()
+          })
+      }
+      .padding({ left: 16, right: 16 })
+    }
+    .padding({ top: this.safeTop, bottom: this.safeBottom })
+  }
+}

+ 137 - 0
features/user/src/main/ets/pages/LoginPage.ets

@@ -0,0 +1,137 @@
+import { BarType, YtAvoid, YtButton, yTRequest, yTRouter, yTToast } from 'basic'
+import { CodeInputType } from '../models'
+import { LoginInput } from '../components/LoginInput'
+import { Terms } from '../components/Terms'
+import { OtherLoginMethods } from '../components/OtherLoginMethods'
+
+// import { OtherLoginMethods } from '../views/OtherLoginMethods'
+
+
+@Builder
+function LoginBuilder() {
+  NavDestination() {
+    LoginPage()
+  }
+  .hideTitleBar(true)
+}
+
+@Component
+struct LoginPage {
+  @State isAgree: boolean = false
+  @StorageProp(YtAvoid.safeTopKey) safeTop: number = 0
+  @StorageProp(YtAvoid.safeBottomKey) safeBottom: number = 0
+  @State phoneNumber: string = ''
+  @State Captcha: string = ''
+  inputPhoneNumberType: CodeInputType = {
+    txt: '+86',
+    placeHolder: '请输入手机号'
+  }
+  inputCaptchaType: CodeInputType = {
+    txt: '验证码',
+    placeHolder: '请输入验证码'
+  }
+  loginMethodArr: BarType<undefined>[] = [
+  // {
+  //   src: $r('app.media.wechat'),
+  //   txt: '微信登入',
+  //   click: () => {
+  //     //使用微信sdk实现微信登录
+  //
+  //   }
+  // },
+    {
+      // src: $r('app.media.hua_wei'),
+      text: '华为登录',
+      click: () => {
+        if (!this.isAgree) {
+          // yTToast.agreePrivacy({ text: '华为登录' })
+          return
+        }
+        yTRequest.huaweiLogin()
+      }
+    }
+  ]
+
+  aboutToAppear(): void {
+
+  }
+
+  build() {
+    Column() {
+      Image($r('[basic].media.ic_back'))
+        .width(24)
+        .aspectRatio(1)
+        .margin({ bottom: 66 })
+        .onClick(() => {
+          yTRouter.routerBack()
+        })
+      Text('使用电话号码访问')
+        .fontSize($r('[basic].float.page_text_font_size_10'))
+        .margin({ bottom: 39 })
+
+      LoginInput({ item: this.inputPhoneNumberType, inputData: this.phoneNumber, needCode: false })
+      LoginInput({ item: this.inputCaptchaType, inputData: this.Captcha, phoneNumber: this.phoneNumber })
+
+
+      Terms({ isAgree: this.isAgree, mg: 13 })
+        .margin({ bottom: 74 })
+
+      YtButton({
+        btContent: '登录', click: () => {
+          if (!this.isAgree) {
+            AppStorage.setOrCreate<Record<string, string>>('captchaLogin',
+              {
+                'phonenumber': this.phoneNumber,
+                'captcha': this.Captcha
+              })
+            // yTToast.agreePrivacy({ text: '验证码登录' })
+            return
+          }
+          yTRequest.phonenumberLogin({
+            'phonenumber': this.phoneNumber,
+            'captcha': this.Captcha
+          })
+        }
+      })
+        .margin({ bottom: 81 })
+
+
+      // LoginButton('登录', () => {
+      //   if (!this.isAgree) {
+      //     PromptActionClass.openAgreePrivacy({})
+      //     return
+      //   }
+      //   const code = AppStorage.get<CodeType>('smsCode')
+      //   const askCodeNum: AskCodeNum = {
+      //     phonenumber: this.phoneNumber,
+      //     smsCode: this.Captcha,
+      //     uuid: code?.uuid
+      //   }
+      //
+      //   yTRequest.post<TokenType, AskCodeNum>('/api/friendcase/member/phoneLogin', askCodeNum)
+      //     .then(res => {
+      //       AppStorage.setOrCreate(AppstorageKey.TOKEN, res.token)
+      //       yTRequest.refreshUserInfo()
+      //       yTRouter.routerBack()
+      //       AppStorage.setOrCreate<string>(AppstorageKey.PHONE_NUMBER, this.phoneNumber)
+      //       IBestToast.show('登录成功')
+      //     })
+      //     .catch((err: Error) => {
+      //       YTLog.error(err)
+      //
+      //     })
+      //
+      // })
+
+      OtherLoginMethods({ loginMethodArr: this.loginMethodArr })
+    }
+    .padding({
+      top: 10 + this.safeTop,
+      left: 16,
+      right: 16,
+      bottom: this.safeBottom
+    })
+    .alignItems(HorizontalAlign.Start)
+  }
+}
+

+ 251 - 0
features/user/src/main/ets/pages/SettingPage.ets

@@ -0,0 +1,251 @@
+import {
+  BarType,
+  IBestToast,
+  reviseImgHeaderBuilder,
+  takePicture,
+  Upload,
+  UserInfo,
+  userInfo,
+  YtAvoid,
+  YtButton,
+  YTHeader,
+  YTLog,
+  yTRequest,
+  yTRouter,
+  yTToast
+} from 'basic'
+
+@Builder
+function settingBuilder() {
+  NavDestination() {
+    SettingPage()
+  }
+  .hideTitleBar(true)
+}
+
+@Component
+struct SettingPage {
+  @StorageProp(YtAvoid.safeTopKey) safeTop: number = 0
+  @StorageProp(YtAvoid.safeBottomKey) safeBottom: number = 0
+  @StorageProp(userInfo.KEY) userInfo: UserInfo = new UserInfo()
+  @State showReviseName: boolean = false
+  @State showHeaderImgRevise: boolean = false
+  @State value: string = ''
+  reviseBuilderArr: Array<BarType<undefined>> = [
+    {
+      text: '头像修改',
+      src: '',
+      click: () => {
+        this.showHeaderImgRevise = true
+      }
+    },
+    {
+      text: '昵称修改',
+      click: () => {
+        this.showReviseName = true
+      }
+    }
+  ]
+  options: BarType<undefined>[] = [
+    {
+      text: '拍照',
+      click: async () => {
+        try {
+          const fullpath = await takePicture()
+          this.showHeaderImgRevise = false
+          // yTRouter.router2DelPhotoPage({ src: fullpath, type: 'header' })
+        } catch (e) {
+          YTLog.warn(e)
+        }
+      }
+    },
+    {
+      text: '从相册中选择',
+      click: () => {
+        Upload.selectImage(getContext(), (fullpath) => {
+          this.showHeaderImgRevise = false
+          IBestToast.showLoading({ message: '上传中...' })
+          yTRequest.uploadHeadImg(getContext(), fullpath, () => {
+            yTRequest.refreshUserInfo(() => {
+              IBestToast.hide()
+              setTimeout(() => {
+                IBestToast.show({ message: '头像修改成功', type: 'success' })
+              }, 100)
+            })
+          })
+        })
+      }
+    },
+    {
+      text: '取消',
+      click: () => {
+        this.showHeaderImgRevise = false
+      }
+    },
+  ]
+
+  build() {
+    Column() {
+      YTHeader({ title: '用户设置' })
+      Column() {
+        ForEach(this.reviseBuilderArr, (item: BarType<undefined>, index) => {
+          this.reviseBuilder(item)
+          if (index < this.reviseBuilderArr.length - 1) {
+            Column() {
+              Text('')
+                .height(1)
+                .width('100%')
+                .backgroundColor('#0A000000')
+            }
+            .padding({ left: 12, right: 8 })
+            .bindSheet($$this.showHeaderImgRevise, this.changeBuilder, {
+              height: 204,
+              backgroundColor: Color.White,
+              showClose: false
+            })
+          }
+        })
+        YtButton({
+          btContent: '退出登录', click: () => {
+            this.userInfo.logout()
+            yTRouter.routerBack()
+            IBestToast.show({ message: '退出登录成功', type: 'success' })
+          }
+        })
+          .margin({ top: 426, bottom: 12 })
+        YtButton({
+          btContent: '注销用户',
+          btFontColor: Color.Red,
+          bgc: Color.White,
+          click: () => {
+            // yTToast.doubleConfirm({
+            //   text: '警告⚠', message: '确定要注销吗?\n注销后数据丢失无法恢复!', click: () => {
+            //     AlertDialog.show({ message: JSON.stringify('点击了确定', null, 2) })
+            //   }
+            // })
+          }
+        })
+      }
+      .bindSheet($$this.showReviseName, this.reviseNameBuilder, {
+        height: 275,
+        showClose: false,
+        backgroundColor: Color.White
+      })
+      .padding({
+        top: 29,
+        left: 16,
+        right: 16,
+      })
+    }
+    .padding({ top: this.safeTop, bottom: this.safeBottom })
+
+  }
+
+  @Builder
+  reviseBuilder(item: BarType<undefined>) {
+    Row() {
+      Text(item.text)
+        .fontSize($r('[basic].float.page_text_font_size_12'))
+        .fontColor('#80000000')
+      Row() {
+        if (typeof item.src == 'string') {
+          Image(this.userInfo.getHeadImg() ? this.userInfo.getHeadImg() : $r('app.media.default_img'))
+            .height(24)
+            .width(24)
+            .borderRadius(12)
+        } else {
+          Text(this.userInfo.getName() ?? this.userInfo.getPhoneNumber() ?? this.userInfo.getId()?.toString())
+            .fontSize($r('[basic].float.page_text_font_size_12'))
+            .fontColor('#CC000000')
+        }
+        Image($r('app.media.right_arrow'))
+          .width(24)
+          .width(24)
+      }
+    }
+    .width('100%')
+    .justifyContent(FlexAlign.SpaceBetween)
+    .padding({ left: 12, right: 4 })
+    .height(36)
+    .onClick(item.click)
+  }
+
+  @Builder
+  reviseNameBuilder() {
+    Column() {
+
+      Text('昵称')
+        .width('100%')
+        .height(36)
+        .textAlign(TextAlign.Start)
+        .fontSize(12)
+
+      TextInput({ text: $$this.value, placeholder: '请输入昵称' })
+        .borderRadius(4)
+        .fontSize(12)
+        .placeholderFont({ size: 12 })
+        .placeholderColor('#14000000')
+        .height(36)
+        .width('100%')
+        .backgroundColor(Color.White)
+        .border({ width: 1, color: '#14000000' })
+        .margin({ bottom: 25 })
+
+      Row({ space: 32 }) {
+        YtButton({
+          btContent: '取消',
+          btHeight: 25,
+          btWidth: 78,
+          btBorder: { width: 1, color: '#1A000000' },
+          btPadding: {
+            left: 26,
+            right: 26,
+            top: 4,
+            bottom: 4
+          },
+          bgc: Color.White,
+          btFontColor: $r('sys.color.mask_secondary'),
+          click: () => {
+            this.showReviseName = false
+            this.value = ''
+
+          }
+        })
+        YtButton({
+          btContent: '完成',
+          btHeight: 25,
+          btWidth: 78,
+          btPadding: {
+            left: 26,
+            right: 26,
+            top: 4,
+            bottom: 4
+          },
+          click: () => {
+            if (!this.value) {
+              IBestToast.show({
+                type: "warning",
+                message: "昵称不能为空"
+              })
+              return
+            }
+            yTRequest.changeNickname(this.value, () => {
+              IBestToast.show({ message: '名称修改成功', type: 'success' })
+            })
+            //TODO 发送请求后关闭弹窗
+            this.showReviseName = false
+          }
+        })
+      }
+      .justifyContent(FlexAlign.Center)
+      .width('100%')
+
+    }
+    .padding({ left: 22, right: 22, top: 8 })
+  }
+
+  @Builder
+  changeBuilder() {
+    reviseImgHeaderBuilder(this.options)
+  }
+}

+ 138 - 0
features/user/src/main/ets/pages/SuggestionPage.ets

@@ -0,0 +1,138 @@
+import { IBestToast, YtAvoid, YtButton, YTHeader, yTRequest } from 'basic'
+
+@Builder
+function suggestionBuilder() {
+  NavDestination() {
+    SuggestionPage()
+  }
+  .hideTitleBar(true)
+}
+
+@Component
+struct SuggestionPage {
+  @StorageProp(YtAvoid.safeTopKey) safeTop: number = 0
+  @StorageProp(YtAvoid.safeBottomKey) safeBottom: number = 0
+  @State description: string = ''
+  @State contact: string = ''
+
+  build() {
+    Column() {
+      YTHeader({ title: '意见反馈' })
+      Row() {
+        Row() {
+          Text()
+            .width(3)
+            .height(10)
+            .backgroundColor('#FF95BFBF')
+            .margin({ right: 3 })
+          Text() {
+            Span('问题描述 ')
+              .fontColor('#CC000000')
+
+            Span('(必填)')
+              .fontColor('#FFFD4437')
+          }
+          .fontSize($r('[basic].float.page_text_font_size_12'))
+          .fontWeight(500)
+        }
+      }
+      .width('100%')
+      .alignItems(VerticalAlign.Bottom)
+      .margin({ bottom: 4, top: 26 })
+      .justifyContent(FlexAlign.SpaceBetween)
+      .padding({ left: 16, right: 20 })
+
+
+      Text('请尽量将问题描述详细')
+        .fontSize($r('[basic].float.page_text_font_size_10'))
+        .fontColor('#4D000000')
+        .margin({ bottom: 8 })
+        .width('100%')
+        .textAlign(TextAlign.Start)
+        .padding({ left: 22 })
+
+      Column() {
+        TextArea({ text: $$this.description })
+          .width('100%')
+          .padding(5)
+          .fontSize($r('[basic].float.page_text_font_size_12'))
+          .fontColor('#CC000000')
+          .borderRadius(4)
+          .border({ width: 1, color: '#14000000' })
+          .height(155)
+          .backgroundColor(Color.White)
+      }
+      .padding({ left: 22, right: 22 })
+      .margin({ bottom: 23 })
+
+      Row() {
+        Row() {
+          Text()
+            .width(3)
+            .height(10)
+            .backgroundColor('#FF95BFBF')
+            .margin({ right: 3 })
+          Text() {
+            Span('联系方式 ')
+              .fontColor('#CC000000')
+
+            Span('(选填)')
+              .fontColor('#CC000000')
+          }
+          .fontSize($r('[basic].float.page_text_font_size_12'))
+          .fontWeight(500)
+        }
+      }
+      .width('100%')
+      .alignItems(VerticalAlign.Bottom)
+      .margin({ bottom: 4, top: 26 })
+      .justifyContent(FlexAlign.SpaceBetween)
+      .padding({ left: 16, right: 20 })
+
+
+      Text('请输入手机号或QQ号')
+        .fontSize($r('[basic].float.page_text_font_size_10'))
+        .fontColor('#4D000000')
+        .margin({ bottom: 8 })
+        .width('100%')
+        .textAlign(TextAlign.Start)
+        .padding({ left: 22 })
+
+      Column() {
+        TextArea({ text: $$this.contact })
+          .width('100%')
+          .borderRadius(4)
+          .padding(5)
+          .fontSize($r('[basic].float.page_text_font_size_12'))
+          .fontColor('#CC000000')
+          .border({ width: 1, color: '#14000000' })
+          .height(87)
+          .backgroundColor(Color.White)
+      }
+      .padding({ left: 22, right: 22 })
+      .margin({ bottom: 160 })
+
+      Column() {
+        YtButton({
+          btContent: '提交反馈',
+          click: () => {
+            if (!this.description) {
+              IBestToast.show({
+                type: 'warning',
+                message: '请输入问题描述'
+              })
+              return
+            }
+            yTRequest.questionBack(this.description, this.contact)
+            // //TODO 发请求提交反馈
+            // YTLog.info(this.description + this.contact + '反馈提交了')
+          }
+        })
+      }
+      .padding({ left: 18, right: 18 })
+
+    }
+    .padding({ top: this.safeTop, bottom: this.safeBottom })
+
+  }
+}

+ 0 - 3
features/user/src/main/ets/utils/Calc.ets

@@ -1,3 +0,0 @@
-export function add(a: number, b: number) {
-  return a + b;
-}

+ 152 - 0
features/user/src/main/ets/views/Mine.ets

@@ -0,0 +1,152 @@
+import { BarType, copyText, ShowBannerAd, UserInfo, userInfo, YtAvoid, yTRouter } from 'basic'
+
+@Component
+export struct Mine {
+  @StorageProp(YtAvoid.safeTopKey) safeTop: number = 0
+  @StorageProp(userInfo.KEY) userInfo: UserInfo = new UserInfo()
+  setArr: Array<BarType<undefined>> = [
+    // {
+    //   text: 'AI次数兑换',
+    //   click: () => {
+    //     yTRouter.router2AIExchangePage()
+    //     if (this.userInfo.checkLogin()) {
+    //
+    //     } else {
+    //       //TODO 去登录页
+    //     }
+    //
+    //   },
+    //   src: $r('app.media.right_arrow')
+    // },
+    // {
+    //   text: '意见反馈',
+    //   click: () => {
+    //     yTRouter.router2SuggestionPage()
+    //     if (this.userInfo.checkLogin()) {
+    //
+    //     } else {
+    //       //TODO 去登录页
+    //     }
+    //
+    //   },
+    //   src: $r('app.media.right_arrow')
+    // },
+    // {
+    //   text: '给个好评',
+    //   click: () => {
+    //
+    //   },
+    //   src: $r('app.media.right_arrow')
+    // },
+    // {
+    //   text: '关于我们',
+    //   click: () => {
+    //     yTRouter.router2AgreementPage('关于我们')
+    //   },
+    //   src: $r('app.media.right_arrow')
+    // },
+    // {
+    //   text: '隐私政策',
+    //   click: () => {
+    //     yTRouter.router2AgreementPage('隐私政策')
+    //   },
+    //   src: $r('app.media.right_arrow')
+    // }
+  ]
+
+  aboutToAppear(): void {
+
+  }
+
+  build() {
+    Column() {
+      Row() {
+        Row() {
+          if (this.userInfo.getHeadImg()) {
+            Image(this.userInfo.getHeadImg())
+              .aspectRatio(1)
+              .height(40)
+              .borderRadius(20)
+              .margin({ right: 9 })
+          } else {
+            Image($r('app.media.app_icon'))
+              .width(50)
+              .height(40)
+              .margin({ right: 9 })
+          }
+
+          Column({ space: 7 }) {
+            Text(this.userInfo.getName() ?? this.userInfo.getPhoneNumber() ?? this.userInfo.getId()?.toString() ??
+              '未登录')
+              .fontSize($r('[basic].float.page_text_font_size_16'))
+              .fontColor('#FF000000')
+            Text() {
+              Span('ID:' + (this.userInfo.getId()?.toString().padStart(8, '0') ?? '00000000'))
+              ImageSpan($r('app.media.copy'))
+                .width(7)
+                .height(8)
+                .margin({ left: 4 })
+                .onClick(() => {
+                  copyText((this.userInfo.getId()?.toString().padStart(8, '0') ?? '00000000'))
+                })
+                .offset({ bottom: 4 })
+            }
+            .fontColor('#80000000')
+            .fontSize($r('[basic].float.page_text_font_size_10'))
+          }
+          .alignItems(HorizontalAlign.Start)
+        }
+        .onClick(() => {
+
+          if (this.userInfo.checkLogin()) {
+            yTRouter.router2SettingPage()
+            return
+          }
+          yTRouter.router2LoginPage()
+        })
+      }
+      .width('100%')
+      .margin({ bottom: 30 })
+
+      ShowBannerAd()
+
+      List() {
+        ForEach(this.setArr, (item: BarType<undefined>, index) => {
+          ListItem() {
+            Row() {
+              Text(item.text)
+                .fontColor('#80000000')
+                .fontSize(12)
+              if (!index) {
+                Row() {
+                  Text(this.userInfo.getAiNum()?.toString() ?? '')
+                    .fontWeight(600)
+                    .fontSize($r('[basic].float.page_text_font_size_14'))
+                  Image($r('app.media.right_arrow'))
+                    .width(24)
+                    .aspectRatio(1)
+                }
+              } else {
+                Image($r('app.media.right_arrow'))
+                  .width(24)
+                  .aspectRatio(1)
+              }
+            }
+            .width('100%')
+            .height(36)
+            .justifyContent(FlexAlign.SpaceBetween)
+            .onClick(() => {
+              item.click?.()
+            })
+          }
+        })
+      }
+      .padding({ left: 12, right: 4 })
+      .divider({ strokeWidth: 1, color: '#0A000000', endMargin: 8 })
+      .margin({ top: 30 })
+    }
+    .padding({ top: this.safeTop + 22, left: 16, right: 16 })
+
+    .height('100%')
+  }
+}

BIN
features/user/src/main/resources/base/media/copy.png


BIN
features/user/src/main/resources/base/media/default_img.png


BIN
features/user/src/main/resources/base/media/right_arrow.png


+ 5 - 1
products/entry/oh-package.json5

@@ -5,6 +5,10 @@
   "main": "",
   "author": "",
   "license": "",
-  "dependencies": {}
+  "dependencies": {
+    "basic": "file:../../commons/basic",
+    "sketch": "file:../../features/sketch",
+    "user": "file:../../features/user",
+  }
 }
 

+ 76 - 5
products/entry/src/main/ets/entryability/EntryAbility.ets

@@ -1,6 +1,12 @@
-import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
+import { AbilityConstant, bundleManager, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit';
 import { hilog } from '@kit.PerformanceAnalysisKit';
 import { window } from '@kit.ArkUI';
+import {  jhStartAd, permissionControl, YtAvoid,
+  yTBreakPoint,
+  YTLog, yTRequest } from 'basic';
+import { identifier } from '@kit.AdsKit';
+import { BusinessError } from '@kit.BasicServicesKit';
+import { preferences } from '@kit.ArkData';
 
 const DOMAIN = 0x0000;
 
@@ -15,16 +21,81 @@ export default class EntryAbility extends UIAbility {
   }
 
   onWindowStageCreate(windowStage: window.WindowStage): void {
-    // Main window is created, set main page for this ability
-    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
 
-    windowStage.loadContent('pages/Index', (err) => {
+    windowStage.loadContent('/pages/Index'.slice(1), (err) => {
       if (err.code) {
         hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
         return;
       }
-      hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
+
+      PersistentStorage.persistProp<string>('token', '')
+      if (AppStorage.get<string>('token')) {
+        yTRequest.refreshUserInfo()
+      }
+      // IBestInit(windowStage, this.context)
+      bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_SIGNATURE_INFO)
+        .then(res => {
+          YTLog.info(`signatureInfo:` + JSON.stringify(res.signatureInfo))
+        })
+        .catch((err: Error) => {
+          YTLog.error(err)
+        })
+      permissionControl.init(this.context)
+        .then(() => {
+          permissionControl.askPermission(["ohos.permission.APP_TRACKING_CONSENT"], (res) => {
+            if (res.authResults[0] === 0) {
+              YTLog.info('succeeded in requesting permission');
+              identifier.getOAID((err: BusinessError, data: string) => {
+                if (err.code) {
+                  YTLog.error(`get oaid failed, error: ${err.code} ${err.message}`);
+                } else {
+                  const oaid: string = data;
+                  jhStartAd.init(this.context, oaid)
+                  AppStorage.setOrCreate('OAID', oaid)
+                  YTLog.info(`succeeded in getting oaid by callback , oaid: ${oaid}`);
+                }
+              });
+            } else {
+              YTLog.error('user rejected');
+            }
+          })
+        })
+      // IBestSetGlobalConfig({
+      //   unit: "vp",
+      //   fontUnit: "vp"
+      // })
+      const windowClass = windowStage.getMainWindowSync()
+      windowClass.setWindowLayoutFullScreen(true)
+      const top = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height
+      const bottom = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR).bottomRect.height
+      YtAvoid.setAvoid({
+        top, bottom
+      })
+      windowClass.on('avoidAreaChange', avoidOption => {
+        if (avoidOption.type == window.AvoidAreaType.TYPE_SYSTEM) {
+          YtAvoid.setAvoid({
+            top: avoidOption.area.topRect.height
+          })
+        }
+        if (avoidOption.type == window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR) {
+          YtAvoid.setAvoid({
+            bottom: avoidOption.area.bottomRect.height
+          })
+        }
+      })
+      const windowWidth = windowClass.getWindowProperties().windowRect.width
+      yTBreakPoint.setBreakPoint(windowWidth)
+      windowClass.on('windowSizeChange', size => {
+        yTBreakPoint.setBreakPoint(size.width)
+      })
+      //创建首选项 category
+      let preference: preferences.Preferences | null = null
+      preference = preferences.getPreferencesSync(getContext(), {
+        name: 'category'
+      })
+      AppStorage.setOrCreate<preferences.Preferences>('category', preference)
     });
+
   }
 
   onWindowStageDestroy(): void {

+ 67 - 15
products/entry/src/main/ets/pages/Index.ets

@@ -1,23 +1,75 @@
+import { BarType, YtAvoid, yTRouter, yTToast } from 'basic';
+import { Mine } from 'user';
+
 @Entry
 @Component
 struct Index {
-  @State message: string = 'Hello World';
+  @State currentIndex: number = 0
+  @StorageProp(YtAvoid.safeBottomKey) bottom: number = 0
+
+  aboutToAppear(): void {
+    yTToast.init({
+      context: this.getUIContext(),
+      options: { alignment: DialogAlignment.Center, maskColor: $r('[basic].color.mask_color') }
+    })
+  }
 
   build() {
-    RelativeContainer() {
-      Text(this.message)
-        .id('HelloWorld')
-        .fontSize($r('app.float.page_text_font_size'))
-        .fontWeight(FontWeight.Bold)
-        .alignRules({
-          center: { anchor: '__container__', align: VerticalAlign.Center },
-          middle: { anchor: '__container__', align: HorizontalAlign.Center }
-        })
-        .onClick(() => {
-          this.message = 'Welcome';
-        })
+    Navigation(yTRouter) {
+      Tabs() {
+        TabContent() {
+          Text('首页')
+        }
+        .tabBar(this.barBuilder({
+          text: '首页',
+          src: this.currentIndex == 0 ? $r('app.media.app_icon') : $r('app.media.app_icon'),
+          index: 0
+        }))
+
+        TabContent() {
+          // RewardPage()
+        }
+        .tabBar(this.barBuilder({
+          text: '福利',
+          src: this.currentIndex == 1 ? $r('app.media.app_icon') : $r('app.media.app_icon'),
+          index: 1
+        }))
+
+        TabContent() {
+          Mine()
+        }
+        .tabBar(this.barBuilder({
+          text: '我的',
+          src: this.currentIndex == 2 ? $r('app.media.app_icon') : $r('app.media.app_icon'),
+          index: 2
+        }))
+      }
+      .barBackgroundColor($r('[basic].color.main_blank'))
+      .barPosition(BarPosition.End)
+
+      .onTabBarClick((index) => {
+        this.currentIndex = index
+      })
+      .onChange((index) => {
+        this.currentIndex = index
+      })
+    }
+    .mode(NavigationMode.Stack)
+    .hideToolBar(true)
+  }
+
+  @Builder
+  barBuilder(item: BarType<undefined>) {
+    Column() {
+      Image(item.src)
+        .width(24)
+      Text(item.text)
+        .fontSize($r('[basic].float.page_text_font_size_10'))
+        .lineHeight(16)
+        .fontColor(this.currentIndex == item.index ? $r('[basic].color.main_ac_color_dark') :
+        $r('[basic].color.main_na_color'))
+        .margin({ bottom: this.bottom })
     }
-    .height('100%')
-    .width('100%')
+    .margin({ top: 5 })
   }
 }