UserCenter.ets 39 KB

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