UserCenter.ets 39 KB

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