UserCenter.ets 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171
  1. import { router } from '@kit.ArkUI';
  2. import { AppUtil, ToastUtil, PreferencesUtil, StrUtil } from '@pura/harmony-utils';
  3. import { BreakpointTypeEnum } from '../common/util/BreakpointSystem';
  4. import * as wxopensdk from '@tencent/wechat_open_sdk';
  5. import { OnWXResp, WXApi, WXEventHandler } from '../common/util/WXApiWrap';
  6. import { bundleManager, common, ConfigurationConstant } from '@kit.AbilityKit';
  7. import { ErrCode } from '@tencent/wechat_open_sdk';
  8. import { http } from '@kit.NetworkKit';
  9. import { authentication } from '@kit.AccountKit';
  10. import { hilog } from '@kit.PerformanceAnalysisKit';
  11. import { BusinessError } from '@kit.BasicServicesKit';
  12. import { util } from '@kit.ArkTS';
  13. import { DialogHelper } from '@pura/harmony-dialog';
  14. import { Pay } from '@cashier_alipay/cashiersdk';
  15. import { OrderInfoUtil } from '../alipay/OrderInfoUtil';
  16. import { RandomUtil, Base64Util } from '@pura/harmony-utils';
  17. import { CommonConstants } from '../common/constants/CommonConstants';
  18. import { PayReq } from '@tencent/wechat_open_sdk';
  19. import { cryptoFramework } from '@kit.CryptoArchitectureKit';
  20. import json from '@ohos.util.json';
  21. import UserUtil, { VipFeature, VipPlanApi, VipPlanListApiResponse, WeChatPrepayId } from '../common/util/UserUtil';
  22. import { Utility } from '../common/util/Utility';
  23. import { pinyin4js } from '@ohos/pinyin4js';
  24. // 微信支付相关工具方法
  25. async function isHasWX(): Promise<boolean> {
  26. try {
  27. let link = 'weixin://';
  28. let data = bundleManager.canOpenLink(link);
  29. if (data) {
  30. return true;
  31. } else {
  32. ToastUtil.showToast('微信未安装');
  33. return false;
  34. }
  35. } catch (err) {
  36. let message = (err as BusinessError).message;
  37. console.error('canOpenLink failed: %{public}s', message);
  38. return false;
  39. }
  40. }
  41. async function createWeChatOrder(app_id: string, mch_id: string, description: string, amount: number,
  42. out_trade_no_pre: string, uid: number, plan_id: string): Promise<string> {
  43. let requestUrl = CommonConstants.WX_PAY_API
  44. + '?app_id=' + encodeURIComponent(app_id.trim())
  45. + '&mch_id=' + encodeURIComponent(mch_id.trim())
  46. + '&description=' + encodeURIComponent(description.trim())
  47. + '&amount=' + encodeURIComponent(amount)
  48. + '&uid=' + encodeURIComponent(uid)
  49. + '&plan_id=' + encodeURIComponent(plan_id)
  50. + '&out_trade_no_pre=' + encodeURIComponent(out_trade_no_pre);
  51. const httpRequest: http.HttpRequest = http.createHttp();
  52. const options: http.HttpRequestOptions = {
  53. method: http.RequestMethod.GET,
  54. readTimeout: 6000,
  55. connectTimeout: 6000,
  56. };
  57. try {
  58. const response: http.HttpResponse = await httpRequest.request(requestUrl, options);
  59. if (response.responseCode === 200) {
  60. let responseData: WeChatPrepayId;
  61. if (typeof response.result === 'string') {
  62. responseData = JSON.parse(response.result) as WeChatPrepayId;
  63. } else if (response.result instanceof Object) {
  64. responseData = response.result as WeChatPrepayId;
  65. } else {
  66. throw new Error('Unexpected response format');
  67. }
  68. const prepayId: string = responseData.prepay_id;
  69. return prepayId;
  70. } else {
  71. ToastUtil.showToast('微信下单失败');
  72. return '';
  73. }
  74. } catch (error) {
  75. ToastUtil.showToast('微信下单异常');
  76. return '';
  77. }
  78. }
  79. function constructSignatureString(appId: string, timestamp: string, nonceStr: string, prepayId: string): string {
  80. return `${appId}\n${timestamp}\n${nonceStr}\n${prepayId}\n`;
  81. }
  82. async function rsaSign2048(message: string): Promise<Uint8Array> {
  83. try {
  84. let keyGen = cryptoFramework.createAsyKeyGenerator("RSA2048")
  85. let key = keyGen.convertPemKeySync(null, CommonConstants.WX_PAY_RSA_PRIVATE_KEY)
  86. let sign = cryptoFramework.createSign('RSA2048|PKCS1|SHA256');
  87. let msgBlob: cryptoFramework.DataBlob = { data: StrUtil.strToUint8Array(message) }
  88. sign.initSync(key.priKey)
  89. let signature = await sign.sign(msgBlob)
  90. return signature.data
  91. } catch (error) {
  92. console.error(error, `onecold error code: ${error.code}`)
  93. return new Uint8Array()
  94. }
  95. }
  96. // 1. 工具函数:将 #RRGGBB 字符串和透明度转为 'rgba(r,g,b,a)' 字符串,深色模式下可返回深色变体
  97. function themeColorWithAlpha(themeColor: string, alpha: number, isDarkMode: boolean = false): string {
  98. if (isDarkMode) {
  99. // 深色模式下返回更深的灰色或半透明黑色
  100. return `rgba(34,34,34,${alpha + 0.2 > 1 ? 1 : alpha + 0.2})`;
  101. }
  102. const color = themeColor.replace('#', '');
  103. const r = parseInt(color.substring(0, 2), 16);
  104. const g = parseInt(color.substring(2, 4), 16);
  105. const b = parseInt(color.substring(4, 6), 16);
  106. return `rgba(${r},${g},${b},${alpha})`;
  107. }
  108. @Component
  109. @Entry
  110. export struct UserCenter {
  111. @State userName: string = '未登录用户';
  112. @State userId: number = 0;
  113. @State userAvatar: Resource = $r('app.media.icon_person2');
  114. @State userAvatarUrl: string = '';
  115. @State isVip: boolean = false;
  116. @State vipExpire: string = '';
  117. @State isLogin: boolean = false;
  118. @State hasActiveSubscription: boolean = false;
  119. @State subscriptionName: string = '';
  120. @State subscriptionEndDate: string = '';
  121. @StorageProp('currentBreakpoint') currentBreakpoint: string = BreakpointTypeEnum.MD;
  122. @State vipFeatures: VipFeature[] = [
  123. {
  124. icon: $r('app.media.wusunyinzhi'),
  125. title: '无损音质',
  126. description: '专享无损音乐支持,让每个音符都完美呈现',
  127. isVipOnly: true,
  128. hasCome: true
  129. },
  130. // { icon: $r('app.media.lrc'), title: '歌词下载', description: '支持歌词API在线下载LRC歌词,实现完美同步', isVipOnly: true },
  131. // { icon: $r('app.media.cover'), title: '封面API', description: '支持设置自动下载封面', isVipOnly: true },
  132. {
  133. icon: $r('app.media.theme'),
  134. title: '主题定制',
  135. description: '自定义播放器主题,打造独一无二的个性风格',
  136. isVipOnly: true,
  137. hasCome: true
  138. },
  139. {
  140. icon: $r('app.media.no_ad'),
  141. title: '无广告',
  142. description: '清爽界面,无广告播放器',
  143. isVipOnly: true,
  144. hasCome: true
  145. },
  146. {
  147. icon: $r('app.media.skip'),
  148. title: '跳过头尾',
  149. description: '为某个歌单专门定制设置跳过头尾',
  150. isVipOnly: true,
  151. hasCome: true
  152. },
  153. {
  154. icon: $r('app.media.car'),
  155. title: 'HiCAR播放',
  156. description: '专为驾车体验优化的音乐播放模式',
  157. isVipOnly: true,
  158. hasCome: false
  159. },
  160. {
  161. icon: $r('app.media.music_wave'),
  162. title: '调音均衡器',
  163. description: '精准定制音效,带来极致听觉体验',
  164. isVipOnly: true,
  165. hasCome: false
  166. },
  167. ];
  168. @State vipPlans: VipPlanApi[] = [];
  169. @State selectedPayPlan: VipPlanApi | null = null;
  170. @State payType: number = 0; // 0: 微信, 1: 支付宝
  171. @StorageProp('themeColor') themeColor: string = CommonConstants.DEFAULT_THEME_COLOR;
  172. @State isDarkMode: boolean = false
  173. @State webUrl: string = CommonConstants.NEW_MEMBER_AGREEMENTS
  174. @State appName:string = ''
  175. @StorageProp('topRectHeight') topRectHeight: number = px2vp(AppUtil.getStatusBarHeight());
  176. @StorageProp('currentColorMode') @Watch('onColorModeChange') currentMode: number =
  177. ConfigurationConstant.ColorMode.COLOR_MODE_LIGHT;
  178. private wxApi = WXApi
  179. private wxEventHandler = WXEventHandler
  180. constructor() {
  181. super();
  182. }
  183. onColorModeChange() {
  184. this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
  185. }
  186. // 通过code换取用户信息
  187. async getUserInfoByCode(code: string) {
  188. await UserUtil.getUserInfoByCode(code);
  189. // 登录后同步本地会员到线上(仅本地有会员且线上无会员时)
  190. const expireDate = await Utility.getLocalNobleExpireDate();
  191. if (Utility.isNobleForOld() && !this.hasActiveSubscription && expireDate) {
  192. const success = await UserUtil.syncLocalNobleToServer(expireDate);
  193. if (success) {
  194. ToastUtil.showToast('本地会员已同步到账号');
  195. PreferencesUtil.putSync('isNoble', false);
  196. }
  197. }
  198. await this.fetchUserInfo();
  199. }
  200. // 通过华为账号信息换取用户信息和会员状态
  201. async getUserInfoByHuawei(openID: string, unionID: string, nickname: string, avatarUri: string) {
  202. await UserUtil.getUserInfoByHuawei(openID, unionID, nickname, avatarUri);
  203. // 登录后同步本地会员到线上(仅本地有会员且线上无会员时)
  204. const expireDate = await Utility.getLocalNobleExpireDate();
  205. if (Utility.isNobleForOld() && !this.hasActiveSubscription && expireDate) {
  206. const success = await UserUtil.syncLocalNobleToServer(expireDate);
  207. if (success) {
  208. ToastUtil.showToast('本地会员已同步到账号');
  209. }
  210. }
  211. await this.fetchUserInfo();
  212. }
  213. @Builder
  214. syncNobleDialogContentBuilder(): void {
  215. Text('检测到您是本地会员,为保障权益请登录账号,会员权益将自动同步到您的账号。')
  216. .fontSize(16)
  217. .fontColor('#222')
  218. .width('100%')
  219. .textAlign(TextAlign.Center)
  220. .padding(16)
  221. }
  222. aboutToAppear(): void {
  223. Utility.getAppName(getContext(this)).then((appName:string)=>{
  224. this.appName = appName
  225. })
  226. this.topRectHeight = px2vp(AppUtil.getStatusBarHeight());
  227. let themeColor = PreferencesUtil.getStringSync('THEME_COLOR', CommonConstants.DEFAULT_THEME_COLOR);
  228. this.isDarkMode = this.currentMode === ConfigurationConstant.ColorMode.COLOR_MODE_DARK
  229. AppStorage.setOrCreate('themeColor', themeColor);
  230. this.themeColor = themeColor;
  231. this.isLogin = PreferencesUtil.getBooleanSync('isLogin', false);
  232. this.userId = PreferencesUtil.getNumberSync('userId', 0);
  233. this.userName = PreferencesUtil.getStringSync('userName', '未登录用户');
  234. this.userAvatarUrl = PreferencesUtil.getStringSync('userAvatarUrl', '');
  235. this.subscriptionName = PreferencesUtil.getStringSync('subscriptionName', '');
  236. this.subscriptionEndDate = PreferencesUtil.getStringSync('subscriptionEndDate', '');
  237. this.hasActiveSubscription = PreferencesUtil.getBooleanSync('hasActiveSubscription', false);
  238. this.wxEventHandler.registerOnWXRespCallback(this.onWXResp)
  239. this.fetchVipPlans();
  240. if (this.isLogin) {
  241. void this.fetchUserInfo();
  242. }
  243. console.log('Heanup isLogin:' + this.isLogin)
  244. // 只在非同步跳转时弹窗提醒
  245. let params = router.getParams() as Record<string, Object>;
  246. let fromSyncNoble = params && params['fromSyncNoble'] === true;
  247. if (fromSyncNoble) {
  248. // 自动弹出登录框
  249. this.showLoginDialog();
  250. } else if (!this.isLogin && Utility.isNobleForOld()) {
  251. DialogHelper.showCustomContentDialog({
  252. dialogId: 'syncNobleDialog',
  253. title: '会员同步提示',
  254. contentBuilder: () => {
  255. this.syncNobleDialogContentBuilder();
  256. },
  257. buttons: [
  258. { value: '暂不登录' },
  259. {
  260. value: '去登录',
  261. action: () => {
  262. this.showLoginDialog();
  263. }
  264. }
  265. ]
  266. });
  267. }
  268. }
  269. aboutToDisappear() {
  270. this.wxEventHandler.unregisterOnWXRespCallback(this.onWXResp)
  271. }
  272. build() {
  273. Column() {
  274. // 顶部安全区和自定义标题栏
  275. Column() {
  276. // 顶部安全区
  277. Blank()
  278. .height(this.topRectHeight)
  279. .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)
  280. .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP])
  281. // 自定义标题栏(Stack实现绝对居中)
  282. Stack() {
  283. // 居中标题
  284. Text('用户中心')
  285. .fontSize(18)
  286. .fontColor(Color.White)
  287. .align(Alignment.Center)
  288. // 左右按钮
  289. Row() {
  290. Image($r('app.media.left_back_white'))
  291. .width(32)
  292. .height(32)
  293. .margin({ left: 12, right: 8 })
  294. .onClick(() => {
  295. router.back();
  296. })
  297. Blank().flexGrow(1)
  298. Blank().width(32)
  299. }
  300. .height(48)
  301. .width('100%')
  302. .alignItems(VerticalAlign.Center)
  303. }
  304. .height(48)
  305. .width('100%')
  306. .backgroundColor(this.isDarkMode ? $r('app.color.user_center_card_background') : this.themeColor)
  307. }
  308. Scroll() {
  309. Column() {
  310. this.buildUserInfoCard()
  311. this.buildVipPlans()
  312. this.buildVipFeatures()
  313. // this.buildFunctionMenu()
  314. }
  315. .width('100%')
  316. }
  317. .scrollBar(BarState.Off)
  318. .edgeEffect(EdgeEffect.Spring)
  319. .layoutWeight(1)
  320. .padding({
  321. bottom: 45
  322. })
  323. }
  324. .backgroundColor($r('app.color.index_background'))
  325. .width('100%')
  326. .height('100%')
  327. .layoutWeight(1)
  328. .transition(TransitionEffect.move(TransitionEdge.BOTTOM)
  329. .animation({ duration: 380, curve: Curve.Ease,delay:50 }))
  330. }
  331. // 用户信息卡片
  332. @Builder
  333. buildUserInfoCard() {
  334. Column() {
  335. Row() {
  336. Image(this.userAvatarUrl && this.userAvatarUrl.length > 0 ? this.userAvatarUrl : this.userAvatar)
  337. .width(56)
  338. .height(56)
  339. .borderRadius('50%')
  340. .clip(true)
  341. .backgroundColor(this.isDarkMode ? Color.Black : Color.White)
  342. .shadow({
  343. radius: 8,
  344. color: 0x11000000,
  345. offsetX: 0,
  346. offsetY: 2
  347. })
  348. Column() {
  349. Row() {
  350. Text(this.isLogin ? this.userName : '未登录用户')
  351. .fontSize(17)
  352. .fontWeight(FontWeight.Bold)
  353. .fontColor(this.isDarkMode ? Color.White : Color.Black)
  354. .textAlign(TextAlign.Start)
  355. .maxLines(1)
  356. .width(110)
  357. .margin({ right: 12 })
  358. }
  359. .justifyContent(FlexAlign.Start)
  360. .margin({ top: 2 })
  361. if (this.isLogin) {
  362. if (this.hasActiveSubscription) {
  363. Text(this.subscriptionName)
  364. .fontSize(13)
  365. .fontColor(themeColorWithAlpha(this.themeColor, 0.8, false))
  366. .textAlign(TextAlign.Start)
  367. Text('到期:' + this.subscriptionEndDate)
  368. .fontSize(11)
  369. .fontColor(this.isDarkMode ? Color.White : Color.Gray)
  370. .textAlign(TextAlign.Start)
  371. } else {
  372. Text('普通用户')
  373. .fontSize(12)
  374. .fontColor(this.isDarkMode ? Color.White : Color.Grey)
  375. .textAlign(TextAlign.Start)
  376. }
  377. }
  378. }
  379. .alignItems(HorizontalAlign.Start)
  380. .width(155)
  381. .margin({ left: 10 })
  382. Column() {
  383. if (this.isLogin) {
  384. Button('退出')
  385. .fontColor(Color.White)
  386. .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)
  387. .height(36)
  388. .width(66)
  389. .fontSize(13)
  390. .borderRadius(13)
  391. .margin({ left: 0 })
  392. .onClick(() => {
  393. this.isLogin = false
  394. this.userName = '未登录用户'
  395. this.userAvatar = $r('app.media.icon_person2')
  396. this.userAvatarUrl = ''
  397. this.isVip = false
  398. this.vipExpire = ''
  399. this.hasActiveSubscription = false;
  400. this.subscriptionName = '';
  401. this.subscriptionEndDate = '';
  402. PreferencesUtil.putSync('isLogin', false);
  403. PreferencesUtil.putSync('userId', 0);
  404. PreferencesUtil.putSync('userName', '未登录用户');
  405. PreferencesUtil.putSync('userAvatarUrl', '');
  406. PreferencesUtil.putSync('subscriptionName', '');
  407. PreferencesUtil.putSync('subscriptionEndDate', '');
  408. PreferencesUtil.putSync('hasActiveSubscription', false);
  409. PreferencesUtil.putSync('userToken', '');
  410. ToastUtil.showToast('已退出登录')
  411. })
  412. } else {
  413. Button('登录')
  414. .fontColor(Color.White)
  415. .backgroundColor(this.isDarkMode ? $r('app.color.title_bar_bg') : this.themeColor)
  416. .height(36)
  417. .width(66)
  418. .fontSize(13)
  419. .borderRadius(13)
  420. .margin({ left: 0 })
  421. }
  422. }
  423. .alignItems(HorizontalAlign.End)
  424. .margin({ left: 30, right: 10 })
  425. }
  426. .onClick(() => {
  427. if (!this.isLogin) {
  428. this.showLoginDialog();
  429. }
  430. })
  431. .width('100%')
  432. .padding(10)
  433. }
  434. .backgroundColor($r('app.color.user_center_card_background'))
  435. .borderRadius(24)
  436. .margin({ left: 14, right: 14, top: 15 })
  437. .shadow({
  438. radius: 8,
  439. color: 0x11000000,
  440. offsetX: 0,
  441. offsetY: 2
  442. })
  443. }
  444. // 登录方式选择弹窗内容
  445. @Builder
  446. loginDialogContentBuilder(): void {
  447. Column() {
  448. Row() {
  449. // 微信登录图标按钮
  450. Column() {
  451. Button({ type: ButtonType.Normal, stateEffect: true }) {
  452. Column() {
  453. Image($r('app.media.wx_logo'))
  454. .width(56)
  455. .height(56)
  456. .margin({ bottom: 8 })
  457. Text('微信登录')
  458. .fontSize(15)
  459. .fontWeight(FontWeight.Bold)
  460. .fontColor('#333')
  461. }
  462. .alignItems(HorizontalAlign.Center)
  463. }
  464. .width(100)
  465. .height(100)
  466. .visibility(PreferencesUtil.getBooleanSync('isShowWX',false)?Visibility.Visible:Visibility.None)
  467. .backgroundColor(Color.White)
  468. // .border({ color: this.themeColor, width: 2 })
  469. .borderRadius(20)
  470. .onClick(async () => {
  471. this.payType = 0;
  472. // 微信登录
  473. let req = new wxopensdk.SendAuthReq
  474. req.isOption1 = false
  475. req.nonAutomatic = true
  476. req.scope = 'snsapi_userinfo,snsapi_friend,snsapi_message,snsapi_contact'
  477. req.state = 'none'
  478. req.transaction = 'test123'
  479. let finished = await this.wxApi.sendReq(getContext(this) as common.UIAbilityContext, req)
  480. console.log('send request finished: ', finished)
  481. DialogHelper.closeDialog('loginDialog');
  482. })
  483. }
  484. .margin({ right: 24 })
  485. // 华为账号登录图标按钮
  486. Column() {
  487. Button({ type: ButtonType.Normal, stateEffect: true }) {
  488. Column() {
  489. Image($r('app.media.huawei'))
  490. .width(56)
  491. .height(56)
  492. .margin({ bottom: 8 })
  493. Text('华为登录')
  494. .fontSize(15)
  495. .fontWeight(FontWeight.Bold)
  496. .fontColor('#333')
  497. }
  498. .alignItems(HorizontalAlign.Center)
  499. }
  500. .width(100)
  501. .height(100)
  502. .backgroundColor(Color.White)
  503. // .border({ color: this.themeColor, width: 2 })
  504. .borderRadius(20)
  505. .onClick(() => {
  506. this.payType = 1;
  507. this.huaweiQuickLogin();
  508. DialogHelper.closeDialog('loginDialog');
  509. })
  510. }
  511. }
  512. .justifyContent(FlexAlign.Center)
  513. .width('100%')
  514. }
  515. .padding({
  516. left: 24,
  517. right: 24,
  518. top: 8,
  519. bottom: 24
  520. })
  521. .width('100%')
  522. }
  523. showLoginDialog() {
  524. DialogHelper.showCustomContentDialog({
  525. dialogId: 'loginDialog',
  526. title: '',
  527. autoCancel: true,
  528. contentBuilder: () => {
  529. this.loginDialogContentBuilder();
  530. },
  531. buttons: []
  532. });
  533. }
  534. // 会员权益展示
  535. @Builder
  536. buildVipFeatures(): void {
  537. Column() {
  538. Text('会员权益')
  539. .fontSize(18)
  540. .fontWeight(FontWeight.Bold)
  541. .padding({ top: 24, bottom: 16 })
  542. .fontColor(this.isDarkMode ? Color.White : '#191919')
  543. .textAlign(TextAlign.Center);
  544. Grid() {
  545. ForEach(this.vipFeatures, (feature: VipFeature) => {
  546. GridItem() {
  547. Column() {
  548. Row() {
  549. Blank().layoutWeight(1)
  550. if (feature.hasCome === false) {
  551. Text('开发中')
  552. .fontSize(10)
  553. .fontColor(themeColorWithAlpha(this.themeColor, 0.9, false))
  554. .backgroundColor(themeColorWithAlpha(this.themeColor, 0.15, this.isDarkMode))
  555. .borderRadius(8)
  556. .padding({
  557. left: 6,
  558. right: 6,
  559. top: 2,
  560. bottom: 2
  561. })
  562. .margin({ top: 2, right: 2 })
  563. } else {
  564. Text('已开发')
  565. .fontSize(10)
  566. .fontColor(themeColorWithAlpha(this.themeColor, 0.9, false))
  567. .backgroundColor(themeColorWithAlpha(this.themeColor, 0.15, this.isDarkMode))
  568. .borderRadius(8)
  569. .padding({
  570. left: 6,
  571. right: 6,
  572. top: 2,
  573. bottom: 2
  574. })
  575. .margin({ top: 2, right: 2 })
  576. }
  577. }
  578. Row() {
  579. Blank().layoutWeight(1)
  580. Image(feature.icon)
  581. .width(32)
  582. .height(32)
  583. .fillColor(this.themeColor)
  584. Blank().layoutWeight(1)
  585. }
  586. .margin({ bottom: 8 })
  587. Text(feature.title)
  588. .fontSize(15)
  589. .fontWeight(FontWeight.Bold)
  590. .fontColor(this.isDarkMode ? Color.White : '#191919')
  591. .margin({ bottom: 2 })
  592. Text(feature.description)
  593. .fontSize(12)
  594. .fontColor(this.isDarkMode ? Color.Gray : '#888')
  595. .textAlign(TextAlign.Center)
  596. .maxLines(2)
  597. .margin({ bottom: 4 })
  598. if (feature.isVipOnly) {
  599. Text('VIP专享')
  600. .fontSize(10)
  601. .fontColor(this.isDarkMode ? Color.White : this.themeColor)
  602. .backgroundColor(this.isDarkMode ? $r('app.color.user_center_button_background') :
  603. themeColorWithAlpha(this.themeColor, 0.15, false))
  604. .borderRadius(8)
  605. .padding({
  606. left: 6,
  607. right: 6,
  608. top: 2,
  609. bottom: 2
  610. })
  611. .margin({ top: 2, bottom: 6 })
  612. }
  613. }
  614. .backgroundColor($r('app.color.user_center_card_background'))
  615. .borderRadius(16)
  616. .shadow({
  617. radius: 8,
  618. color: 0x11000000,
  619. offsetX: 0,
  620. offsetY: 2
  621. })
  622. .border({
  623. color: (this.isDarkMode ? $r('app.color.user_center_button_background') :
  624. themeColorWithAlpha(this.themeColor, 0.08, false))//未选中时的边框颜色
  625. , width: 2
  626. })
  627. .padding(12)
  628. .width('100%')
  629. .height(145)
  630. }
  631. })
  632. }
  633. .columnsTemplate('1fr 1fr')
  634. .columnsGap(16)
  635. .rowsGap(16)
  636. .margin({
  637. left: 16,
  638. right: 16,
  639. top: 0,
  640. bottom: 15
  641. })
  642. }
  643. .backgroundColor(this.isDarkMode ? Color.Black : Color.White)
  644. .borderRadius(24)
  645. .margin({
  646. left: 16,
  647. right: 16,
  648. top: 15,
  649. bottom: 15
  650. })
  651. }
  652. // 支付方式选择弹窗内容
  653. @Builder
  654. payDialogContentBuilder(): void {
  655. Column() {
  656. Row() {
  657. Image($r('app.media.wx_logo'))
  658. .width(22)
  659. .height(22)
  660. .alignSelf(ItemAlign.Center)
  661. .margin({ left: 25 })
  662. Text('微信支付')
  663. .margin({ left: 10, right: 20 })
  664. .fontSize(15)
  665. .fontColor(Color.Gray)
  666. .fontWeight(480)
  667. Blank()
  668. Checkbox()
  669. .select(this.payType === 0)
  670. .selectedColor(this.themeColor)
  671. .onChange((checked: boolean) => {
  672. if (checked) {
  673. this.payType = 0
  674. }
  675. })
  676. .shape(CheckBoxShape.CIRCLE)
  677. .margin({
  678. left: 20,
  679. top: 8,
  680. bottom: 8,
  681. right: 20
  682. })
  683. .width(22)
  684. .height(22)
  685. }
  686. .onClick(() => {
  687. this.payType = 0
  688. })
  689. .width('100%')
  690. .height(50)
  691. Line().width('100%').height(0.3).backgroundColor($r('app.color.index_tab_unselected_font_color'))
  692. Row() {
  693. Image($r('app.media.alipay'))
  694. .width(22)
  695. .height(22)
  696. .alignSelf(ItemAlign.Center)
  697. .margin({ left: 25 })
  698. Text('支付宝')
  699. .margin({ left: 10, right: 20 })
  700. .fontSize(15)
  701. .fontColor(Color.Gray)
  702. .fontWeight(480)
  703. Blank()
  704. Checkbox()
  705. .select(this.payType === 1)
  706. .selectedColor(this.themeColor)
  707. .onChange((checked: boolean) => {
  708. if (checked) {
  709. this.payType = 1
  710. }
  711. })
  712. .shape(CheckBoxShape.CIRCLE)
  713. .margin({
  714. left: 20,
  715. top: 8,
  716. bottom: 8,
  717. right: 20
  718. })
  719. .width(22)
  720. .height(22)
  721. }
  722. .onClick(() => {
  723. this.payType = 1
  724. })
  725. .width('100%')
  726. .height(50)
  727. }
  728. .padding(24)
  729. .width('100%')
  730. }
  731. // 会员套餐选择
  732. @Builder
  733. buildVipPlans(): void {
  734. Column() {
  735. Text(this.subscriptionName=='永久会员'?'您还可以继续赞助支持我们':'会员套餐')
  736. .textAlign(TextAlign.Start)
  737. .padding({ left: 16, bottom: 15, top: 18 })
  738. .fontSize(18)
  739. .fontColor(this.isDarkMode ? Color.White : '#191919')
  740. .fontWeight(FontWeight.Bold)
  741. .fontFamily('鸿蒙黑体');
  742. // 横向滑动卡片区
  743. List() {
  744. ForEach(this.vipPlans, (plan: VipPlanApi, index: number) => {
  745. ListItem() {
  746. Column() {
  747. Text(this.subscriptionName=='永久会员'?"赞助套餐":plan.name)
  748. .fontSize(15)
  749. .fontWeight(FontWeight.Bold)
  750. .fontColor($r('app.color.user_center_font'))
  751. .margin({ top: 18, bottom: 8 })
  752. .textAlign(TextAlign.Center)
  753. // 价格上下排列
  754. Column() {
  755. Text('¥' + plan.price)
  756. .fontSize(24)
  757. .fontWeight(FontWeight.Bold)
  758. .fontColor(this.themeColor)
  759. .margin({ bottom: (plan.original_price && plan.discount_percent > 0) ? 2 : 0 })
  760. .textAlign(TextAlign.Center)
  761. if (plan.original_price && plan.discount_percent && plan.discount_percent > 0) {
  762. Text('¥' + plan.original_price)
  763. .fontSize(14)
  764. .fontColor(themeColorWithAlpha(this.themeColor, 0.7, false))
  765. .decoration({
  766. type: TextDecorationType.LineThrough,
  767. color: themeColorWithAlpha(this.themeColor, 0.7, false)
  768. })
  769. .fontStyle(FontStyle.Italic)
  770. .textAlign(TextAlign.Center)
  771. }
  772. }
  773. .alignItems(HorizontalAlign.Center)
  774. .margin({ bottom: 6 })
  775. Text(plan.duration === '0' ? '永久会员' : (plan.duration + '天会员'))
  776. .fontSize(13)
  777. .fontColor(this.isDarkMode ? Color.White : Color.Grey)
  778. .maxLines(1)
  779. .textAlign(TextAlign.Center)
  780. }
  781. .backgroundColor(this.selectedPayPlan && this.selectedPayPlan.id === plan.id ?
  782. themeColorWithAlpha(this.themeColor, 0.08, false) : (this.isDarkMode ? Color.Black : '#F7F7F7'))
  783. .borderRadius(18)
  784. .shadow({
  785. radius: 8,
  786. color: 0x11000000,
  787. offsetX: 0,
  788. offsetY: 2
  789. })
  790. .border({
  791. color: this.selectedPayPlan && this.selectedPayPlan.id === plan.id ?
  792. (this.isDarkMode ? Color.White : themeColorWithAlpha(this.themeColor, 0.3, this.isDarkMode))//选中时的边框颜色
  793. :
  794. (this.isDarkMode ? $r('app.color.user_center_button_background') :
  795. themeColorWithAlpha(this.themeColor, 0.08, this.isDarkMode))//未选中时的边框颜色
  796. , width: 2
  797. })
  798. .width(125)
  799. .height(125)
  800. .margin({ left: index === 0 ? 16 : 8, right: index === this.vipPlans.length - 1 ? 16 : 8 })
  801. .onClick(() => {
  802. this.selectedPayPlan = plan;
  803. })
  804. .alignItems(HorizontalAlign.Center)
  805. }
  806. })
  807. }
  808. .listDirection(Axis.Horizontal)
  809. .height(130)
  810. .edgeEffect(EdgeEffect.None)
  811. .scrollBar(BarState.Off)
  812. .margin({ bottom: 18 })
  813. Button(this.isVip ? '升级会员' : '立即开通')
  814. .borderRadius(24)
  815. .backgroundColor(this.isDarkMode ? $r('app.color.user_center_button_background') : this.themeColor)
  816. .width(this.currentBreakpoint !== BreakpointTypeEnum.SM ? '25%' : '60%')
  817. .fontColor(Color.White)
  818. .fontSize(18)
  819. .padding(12)
  820. .fontWeight(FontWeight.Bold)
  821. .margin({ top: 1, bottom: 15 })
  822. .alignSelf(ItemAlign.Center)
  823. .onClick(() => {
  824. if (!this.isLogin) {
  825. ToastUtil.showToast('请先登录');
  826. return;
  827. }
  828. if (!this.selectedPayPlan) {
  829. ToastUtil.showToast('请选择套餐');
  830. return;
  831. }
  832. DialogHelper.showCustomContentDialog({
  833. dialogId: 'payDialog',
  834. title: '请选择支付方式',
  835. autoCancel: true,
  836. contentBuilder: () => {
  837. this.payDialogContentBuilder();
  838. },
  839. buttons: [
  840. { value: '取消' },
  841. {
  842. value: '确认支付',
  843. action: () => {
  844. if (this.selectedPayPlan) {
  845. void this.doPay(this.selectedPayPlan);
  846. }
  847. DialogHelper.closeDialog('payDialog');
  848. }
  849. }
  850. ]
  851. });
  852. });
  853. Row() {
  854. Text('购买前请阅读')
  855. .fontSize(12)
  856. .fontColor(this.isDarkMode ? Color.White : '#888')
  857. Text('《会员协议》')
  858. .fontSize(12)
  859. .fontColor(this.isDarkMode ? Color.Blue : this.themeColor)
  860. .onClick(() => {
  861. router.pushUrl({
  862. url: 'pages/WebIndex',
  863. params: { titleName: '会员协议', webUrl: this.webUrl }
  864. });
  865. // router.pushUrl({ url: this.webUrl });
  866. });
  867. }
  868. .justifyContent(FlexAlign.Center)
  869. .width('100%')
  870. .padding({ bottom: 16 });
  871. }
  872. .margin({ left: 14, right: 14, top: 15 })
  873. .backgroundColor($r('app.color.user_center_card_background'))
  874. .borderRadius(24)
  875. }
  876. // 功能菜单
  877. @Builder
  878. buildFunctionMenu(): void {
  879. Column() {
  880. this.menuItem($r('app.media.icon_favorite'), '我的收藏', () => {
  881. if (!this.isLogin) {
  882. ToastUtil.showToast('请先登录')
  883. return
  884. }
  885. // 跳转到收藏页面
  886. ToastUtil.showToast('敬请期待')
  887. })
  888. Line().width('90%').height(0.5).backgroundColor(0xFFEFEFEF)
  889. this.menuItem($r('app.media.icon_history'), '播放历史', () => {
  890. if (!this.isLogin) {
  891. ToastUtil.showToast('请先登录')
  892. return
  893. }
  894. // 跳转到历史记录页面
  895. ToastUtil.showToast('敬请期待')
  896. })
  897. Line().width('90%').height(0.5).backgroundColor(0xFFEFEFEF)
  898. this.menuItem($r('app.media.setting_red'), '设置', () => {
  899. // 跳转到设置页面
  900. router.pushUrl({
  901. url: 'pages/SettingPage'
  902. });
  903. })
  904. }
  905. .backgroundColor(Color.White)
  906. .borderRadius(24)
  907. .margin({ left: 20, right: 20, top: 24 })
  908. }
  909. // 华为一键登录
  910. async huaweiQuickLogin() {
  911. const logTag = 'UserCenter';
  912. const domainId = 0x0000;
  913. try {
  914. const authRequest = new authentication.HuaweiIDProvider().createAuthorizationWithHuaweiIDRequest();
  915. authRequest.scopes = ['profile'];
  916. authRequest.permissions = ['idtoken'];
  917. authRequest.forceAuthorization = true;
  918. authRequest.state = util.generateRandomUUID();
  919. authRequest.idTokenSignAlgorithm = authentication.IdTokenSignAlgorithm.PS256;
  920. const controller = new authentication.AuthenticationController(getContext(this));
  921. const response: authentication.AuthorizationWithHuaweiIDResponse = await controller.executeRequest(authRequest);
  922. const credential = response.data;
  923. const nickName = credential?.nickName ?? '华为用户';
  924. const avatarUri = credential?.avatarUri ?? '';
  925. const openID = credential?.openID;
  926. const unionID = credential?.unionID ?? '';
  927. // hilog.info(domainId, logTag, `华为登录成功,昵称: ${nickName}, openID: ${openID}`);
  928. // 调用接口获取用户信息和会员状态
  929. if (openID) {
  930. await this.getUserInfoByHuawei(openID, unionID, nickName, avatarUri);
  931. } else {
  932. ToastUtil.showToast('openID获取失败');
  933. }
  934. } catch (error) {
  935. hilog.error(domainId, logTag, `华为登录失败: ${error.code}, ${error.message}`);
  936. this.dealHuaweiLoginError(error as BusinessError);
  937. ToastUtil.showToast('华为登录失败');
  938. }
  939. }
  940. dealHuaweiLoginError(error: BusinessError): void {
  941. if (error.code === 1001) {
  942. ToastUtil.showToast('未登录华为账号');
  943. } else if (error.code === 1002) {
  944. ToastUtil.showToast('网络异常');
  945. } else if (error.code === 1003) {
  946. ToastUtil.showToast('内部错误');
  947. } else if (error.code === 1004) {
  948. ToastUtil.showToast('用户取消授权');
  949. } else if (error.code === 1005) {
  950. ToastUtil.showToast('系统服务异常');
  951. } else if (error.code === 1006) {
  952. ToastUtil.showToast('请求被拒绝');
  953. } else if (error.code === 1007) {
  954. ToastUtil.showToast('无权限');
  955. } else {
  956. ToastUtil.showToast('登录失败');
  957. }
  958. }
  959. // 菜单项构建
  960. @Builder
  961. menuItem(icon: Resource, text: string, onClick: () => void): void {
  962. Row() {
  963. Image(icon)
  964. .width(22)
  965. .height(22)
  966. .alignSelf(ItemAlign.Center)
  967. .margin({ left: 22 })
  968. Text(text)
  969. .margin({ left: 10, right: 20 })
  970. .fontSize(15)
  971. .fontColor(Color.Gray)
  972. .fontWeight(480)
  973. Blank()
  974. Image($r('app.media.arrow_right'))
  975. .width(22)
  976. .height(22)
  977. .margin({ left: 20, right: 20 })
  978. .align(Alignment.Center)
  979. }
  980. .width('100%')
  981. .height(50)
  982. .onClick(onClick)
  983. }
  984. async fetchVipPlans() {
  985. try {
  986. const url = 'https://pay.ss5.xyz/subscription/get_list';
  987. const httpRequest = http.createHttp();
  988. const options: http.HttpRequestOptions = {
  989. method: http.RequestMethod.GET,
  990. readTimeout: 3000,
  991. connectTimeout: 3000,
  992. };
  993. const response: http.HttpResponse = await httpRequest.request(url, options);
  994. if (response.responseCode === 200) {
  995. let res = response.result as string;
  996. let json: VipPlanListApiResponse = JSON.parse(res) as VipPlanListApiResponse;
  997. if (json.code === 0 && json.data && json.data.plans) {
  998. this.vipPlans = json.data.plans;
  999. if (this.vipPlans.length > 0) {
  1000. this.selectedPayPlan = this.vipPlans[0]; // 默认选中第一个套餐
  1001. }
  1002. } else {
  1003. ToastUtil.showToast('套餐数据解析失败');
  1004. }
  1005. } else {
  1006. ToastUtil.showToast('获取套餐失败');
  1007. }
  1008. } catch (e) {
  1009. ToastUtil.showToast('获取套餐异常');
  1010. }
  1011. }
  1012. async doPay(plan: VipPlanApi) {
  1013. if (this.payType === 0) {
  1014. await this.doWxPay(plan);
  1015. } else if (this.payType === 1) {
  1016. await this.doAliPay(plan);
  1017. }
  1018. }
  1019. async doWxPay(plan: VipPlanApi) {
  1020. if (!(await isHasWX())) {
  1021. ToastUtil.showToast('没有安装微信');
  1022. return;
  1023. }
  1024. const appName = PreferencesUtil.getStringSync('appName', 'TTMusic');
  1025. hilog.info(0x0000, 'UserCenter', `doWxPay: current this.userId is ${this.userId}`);
  1026. const prepayId = await createWeChatOrder(CommonConstants.WX_PAY_APPID, CommonConstants.WX_PAY_MCHID,
  1027. appName, Math.round(Number(plan.price) * 100), pinyin4js.getShortPinyin(this.appName), this.userId, plan.id);
  1028. if (prepayId) {
  1029. await this.wechatPay(prepayId);
  1030. } else {
  1031. ToastUtil.showToast('微信下单失败');
  1032. }
  1033. }
  1034. async wechatPay(prepayId: string) {
  1035. let nonceStr = RandomUtil.getRandomStr(16, 'abcdefghijklmnopqrstuvwxyz');
  1036. const timestamp: number = Math.floor(Date.now() / 1000);
  1037. const signString = constructSignatureString(CommonConstants.WX_PAY_APPID, timestamp + '', nonceStr, prepayId);
  1038. let signStr = await rsaSign2048(signString);
  1039. let signStrBase64 = Base64Util.encodeToStrSync(signStr);
  1040. let req = new PayReq();
  1041. req.partnerId = CommonConstants.WX_PAY_MCHID;
  1042. req.appId = CommonConstants.WX_PAY_APPID;
  1043. req.packageValue = 'Sign=WXPay';
  1044. req.prepayId = prepayId;
  1045. req.nonceStr = nonceStr;
  1046. req.timeStamp = timestamp + '';
  1047. req.sign = signStrBase64;
  1048. req.extData = 'extData';
  1049. let finished = await this.wxApi.sendReq(getContext(this) as common.UIAbilityContext, req);
  1050. hilog.info(0x0000, 'UserCenter', 'send request finished: ', finished);
  1051. }
  1052. async doAliPay(plan: VipPlanApi) {
  1053. const appName = PreferencesUtil.getStringSync('appName', 'TTMusic');
  1054. OrderInfoUtil.getOrderInfo(Number(plan.price), appName, this.userId, plan.id).then((orderInfo) => {
  1055. console.log('Heanup: alipay orderInfo = ' + json.stringify(orderInfo))
  1056. new Pay().pay(orderInfo, true).then(async (result) => {
  1057. if (result.get('resultStatus') === '9000') {
  1058. ToastUtil.showToast('支付成功!');
  1059. void this.fetchUserInfo();
  1060. // 可在此处刷新会员状态
  1061. } else {
  1062. ToastUtil.showToast('取消支付!');
  1063. }
  1064. }).catch((error: BusinessError) => {
  1065. ToastUtil.showToast('支付失败!' + error.message);
  1066. });
  1067. });
  1068. }
  1069. // 每次进入页面时刷新用户数据
  1070. async fetchUserInfo() {
  1071. // console.log('Heanup 刷新用户信息:'+token)
  1072. await UserUtil.fetchUserInfo();
  1073. // console.log("Heanup UserInfo:" + json.stringify(user_info))
  1074. this.isLogin = PreferencesUtil.getBooleanSync('isLogin', false);
  1075. this.userId = PreferencesUtil.getNumberSync('userId', 0);
  1076. this.userName = PreferencesUtil.getStringSync('userName', '未登录用户');
  1077. this.userAvatarUrl = PreferencesUtil.getStringSync('userAvatarUrl', '');
  1078. this.subscriptionName = PreferencesUtil.getStringSync('subscriptionName', '');
  1079. this.subscriptionEndDate = PreferencesUtil.getStringSync('subscriptionEndDate', '');
  1080. this.hasActiveSubscription = PreferencesUtil.getBooleanSync('hasActiveSubscription', false);
  1081. // console.log('Heanup hasActiveSubscription:' + this.hasActiveSubscription)
  1082. // 新增:同步线上会员状态到本地,便于 Utility.isNoble 全局判断
  1083. PreferencesUtil.putSync('hasActiveSubscription', this.hasActiveSubscription);
  1084. }
  1085. // 微信回调处理
  1086. private onWXResp: OnWXResp = (resp): void => {
  1087. if (resp.errCode === ErrCode.ERR_OK) {
  1088. // 判断回调类型
  1089. if (resp.type === wxopensdk.Command.kCommandSendAuth) {
  1090. // 微信登录成功回调
  1091. const authResp = resp as wxopensdk.SendAuthResp; // 转换为授权响应类型,以便访问code
  1092. this.getUserInfoByCode(resp?.['code'])
  1093. DialogHelper.closeDialog('loginDialog');
  1094. ToastUtil.showToast('登录成功!');
  1095. } else if (resp.type === wxopensdk.Command.kCommandPay) {
  1096. // 微信支付成功回调
  1097. ToastUtil.showToast('支付成功!');
  1098. // 可在此处刷新会员状态或其他支付成功后的业务逻辑
  1099. // this.fetchVipPlans(); // 刷新会员套餐信息
  1100. this.fetchUserInfo();
  1101. } else {
  1102. // 其他类型的成功回调
  1103. ToastUtil.showToast('操作成功!');
  1104. }
  1105. } else {
  1106. // 处理错误情况
  1107. if (resp.type === wxopensdk.Command.kCommandSendAuth) {
  1108. ToastUtil.showToast('微信登录失败!');
  1109. } else if (resp.type === wxopensdk.Command.kCommandPay) {
  1110. ToastUtil.showToast('支付失败!');
  1111. } else {
  1112. ToastUtil.showToast('操作失败!');
  1113. }
  1114. }
  1115. }
  1116. }