UserCenter.ets 41 KB

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