app.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698
  1. import { Component, OnInit, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { LoginModalComponent } from './login-modal.component';
  4. import QRCode from 'qrcode';
  5. // ═══════════════════════════════════════════════════
  6. // APIG Payment Page — Angular Component
  7. // ═══════════════════════════════════════════════════
  8. const API_BASE = 'https://server.fmode.cn';
  9. const PARSE_BASE = API_BASE + '/parse/functions';
  10. const PAY_COMPANY = '1AiWpTEDH9';
  11. const APP_ID = 'ncloudmaster';
  12. const DEFAULT_FUN_ID = 'HOkkX72PMF';
  13. const ORDER_PAGE_SIZE = 10;
  14. interface PriceTier {
  15. count: number;
  16. price: number;
  17. }
  18. interface ApigData {
  19. objectId: string;
  20. title: string;
  21. content?: string;
  22. count?: number;
  23. priceStep?: PriceTier[];
  24. [key: string]: any;
  25. }
  26. interface OrderData {
  27. objectId: string;
  28. createdAt: string;
  29. isPay?: boolean;
  30. price?: number;
  31. count?: number;
  32. detail?: { addCount?: number; oldCount?: number };
  33. [key: string]: any;
  34. }
  35. @Component({
  36. selector: 'app-root',
  37. imports: [CommonModule, LoginModalComponent],
  38. templateUrl: './app.html',
  39. styleUrl: './app.scss'
  40. })
  41. export class App implements OnInit {
  42. @ViewChild('qrCanvas', { static: false }) qrCanvasRef!: ElementRef<HTMLCanvasElement>;
  43. constructor(private cdr: ChangeDetectorRef) {}
  44. // URL params
  45. authId = '';
  46. userId = '';
  47. apigId = '';
  48. funId = DEFAULT_FUN_ID;
  49. // Login state
  50. needLogin = false;
  51. loggedInUser: string = '';
  52. apigList: ApigData[] = [];
  53. apigListLoading = false;
  54. // State
  55. apig: ApigData | null = null;
  56. selectedIndex = 0;
  57. errorMsg = '';
  58. paying = false;
  59. showQrModal = false;
  60. showSuccess = false;
  61. paySuccess = false;
  62. successDetailHtml = '';
  63. // Order state
  64. order: any = null;
  65. tradeNo = '';
  66. nonceStr = '';
  67. pollTimer: any = null;
  68. // Order history
  69. orders: OrderData[] = [];
  70. ordersLoading = false;
  71. orderError = '';
  72. hasMoreOrders = false;
  73. orderSkip = 0;
  74. get selectedTier(): PriceTier | null {
  75. if (!this.apig?.priceStep || this.apig.priceStep.length === 0) return null;
  76. return this.apig.priceStep[this.selectedIndex] || null;
  77. }
  78. get unitLabel(): string {
  79. if (!this.apig) return '次';
  80. return (this.apig.objectId === 'MYM5zJBKgw' || this.apig.objectId === 'FQtTgjcqIZ') ? 'token' : '次';
  81. }
  82. ngOnInit(): void {
  83. const params = new URLSearchParams(window.location.search);
  84. this.authId = params.get('authid') || '';
  85. this.userId = params.get('user') || params.get('userid') || '';
  86. this.apigId = params.get('apigid') || '';
  87. this.funId = params.get('fun_id') || DEFAULT_FUN_ID;
  88. // 1. URL 已提供足够参数 → 直接加载
  89. if (this.authId || (this.userId && this.apigId)) {
  90. console.log('[AUTH] URL 参数充足,直接加载');
  91. this.loadApig();
  92. return;
  93. }
  94. // 2. URL 没有 user → 检查 localStorage 缓存
  95. const cachedUserId = localStorage.getItem('apig_user_id');
  96. const cachedToken = localStorage.getItem('apig_session_token');
  97. if (cachedUserId && cachedToken) {
  98. console.log('[AUTH] 从 localStorage 恢复用户:', cachedUserId);
  99. this.userId = cachedUserId;
  100. this.loggedInUser = cachedUserId;
  101. // 后台验证 token 是否还有效
  102. this.validateCachedToken(cachedToken, cachedUserId);
  103. if (this.apigId) {
  104. this.loadApig();
  105. } else {
  106. this.loadApigList();
  107. }
  108. return;
  109. }
  110. // 3. 什么都没有 → 显示登录弹窗
  111. console.log('[AUTH] 未检测到用户,显示登录弹窗');
  112. this.needLogin = true;
  113. }
  114. async validateCachedToken(token: string, userId: string): Promise<void> {
  115. try {
  116. const resp = await fetch(`${API_BASE}/parse/users/me`, {
  117. headers: {
  118. 'X-Parse-Application-Id': APP_ID,
  119. 'X-Parse-Session-Token': token
  120. }
  121. });
  122. const data = await resp.json();
  123. if (!data.objectId) {
  124. console.warn('[AUTH] 缓存 token 已失效,需要重新登录');
  125. localStorage.removeItem('apig_user_id');
  126. localStorage.removeItem('apig_session_token');
  127. this.needLogin = true;
  128. }
  129. } catch (e) {
  130. console.warn('[AUTH] 验证 token 失败:', e);
  131. }
  132. }
  133. onLoginSuccess(event: { userId: string; sessionToken: string }): void {
  134. console.log('[AUTH] 登录成功, userId:', event.userId);
  135. this.userId = event.userId;
  136. this.loggedInUser = event.userId;
  137. this.needLogin = false;
  138. this.errorMsg = '';
  139. if (!this.apigId) {
  140. this.loadApigList();
  141. return;
  142. }
  143. this.loadApig();
  144. }
  145. // ─── Load available APIG list (when no apigId) ───
  146. async loadApigList(): Promise<void> {
  147. this.apigListLoading = true;
  148. try {
  149. const query: any = {
  150. where: JSON.stringify({
  151. objectId: { $in: ['Vo3ROWEvDy'] }
  152. }),
  153. keys: 'title,content,priceStep',
  154. limit: '10'
  155. };
  156. const resp = await fetch(API_BASE + '/parse/classes/APIG?' + new URLSearchParams(query), {
  157. headers: { 'X-Parse-Application-Id': APP_ID }
  158. });
  159. const data = await resp.json();
  160. if (data.results?.length > 0) {
  161. this.apigList = data.results;
  162. console.log('[APIG] 获取到', data.results.length, '个可用 API');
  163. } else {
  164. console.log('[APIG] 未查询到可用 API');
  165. }
  166. this.cdr.detectChanges();
  167. } catch (e: any) {
  168. console.warn('[APIG] 查询 API 列表失败:', e.message);
  169. } finally {
  170. this.apigListLoading = false;
  171. }
  172. }
  173. navigateToApig(apigId: string): void {
  174. const url = new URL(window.location.href);
  175. url.searchParams.set('apigid', apigId);
  176. window.location.href = url.toString();
  177. }
  178. // ─── Load APIG Info ───
  179. async loadApig(): Promise<void> {
  180. try {
  181. // 如果没有 authId 但有 user+apigid,先查询/创建 APIGAuth
  182. if (!this.authId && this.userId && this.apigId) {
  183. console.log('[INIT] 无 authId,通过 user+apigid 查询 APIGAuth...');
  184. const resolved = await this.resolveAuthId(this.userId, this.apigId);
  185. if (!resolved) {
  186. this.errorMsg = '无法获取或创建计费账套,请联系管理员。';
  187. return;
  188. }
  189. this.authId = resolved;
  190. console.log('[INIT] 获取到 authId:', this.authId);
  191. }
  192. // 1. Try getApig REST endpoint
  193. const resp = await this.postJSON(API_BASE + '/api/apig/getApig', { authid: this.authId });
  194. console.log('[getApig] response:', JSON.stringify(resp).substring(0, 500));
  195. if (resp.code === 200 && resp.data) {
  196. this.apig = resp.data;
  197. this.applyTestTier();
  198. this.selectTier(0);
  199. this.cdr.detectChanges();
  200. this.loadOrderHistory();
  201. return;
  202. }
  203. // 2. Fallback: cloud function
  204. console.log('getApig returned no data, trying cloud function...');
  205. const params = new URLSearchParams(window.location.search);
  206. const cfFuncName = params.get('cfName') || 'getApigInfo';
  207. try {
  208. const cfResp = await this.parseCloudCall(cfFuncName, { apigId: this.authId });
  209. if (cfResp?.result) {
  210. const cfData = cfResp.result.data || cfResp.result;
  211. if (cfData?.objectId) {
  212. this.apig = cfData;
  213. this.applyTestTier();
  214. this.selectTier(0);
  215. this.cdr.detectChanges();
  216. this.loadOrderHistory();
  217. return;
  218. }
  219. }
  220. } catch (e: any) {
  221. console.warn('cloud function error:', e);
  222. }
  223. this.errorMsg = '无法加载接口信息。请确认参数正确。';
  224. } catch (e: any) {
  225. this.errorMsg = '网络错误: ' + e.message;
  226. }
  227. }
  228. // ─── 通过 user+apigid 查询或创建 APIGAuth ───
  229. async resolveAuthId(user: string, apig: string): Promise<string | null> {
  230. // 1. 查询已有 APIGAuth
  231. try {
  232. const query: any = {
  233. where: JSON.stringify({
  234. api: { __type: 'Pointer', className: 'APIG', objectId: apig },
  235. company: { __type: 'Pointer', className: 'Company', objectId: user }
  236. }),
  237. limit: '1'
  238. };
  239. const resp = await fetch(API_BASE + '/parse/classes/APIGAuth?' + new URLSearchParams(query), {
  240. headers: { 'X-Parse-Application-Id': APP_ID }
  241. });
  242. const data = await resp.json();
  243. if (data.results?.length > 0) {
  244. console.log('[resolveAuthId] 找到已有 APIGAuth:', data.results[0].objectId);
  245. return data.results[0].objectId;
  246. }
  247. } catch (e: any) {
  248. console.warn('[resolveAuthId] 查询失败:', e.message);
  249. }
  250. // 2. 创建新 APIGAuth
  251. try {
  252. console.log('[resolveAuthId] 未找到 APIGAuth,创建新记录...');
  253. const createResp = await fetch(API_BASE + '/parse/classes/APIGAuth', {
  254. method: 'POST',
  255. headers: {
  256. 'X-Parse-Application-Id': APP_ID,
  257. 'Content-Type': 'application/json'
  258. },
  259. body: JSON.stringify({
  260. api: { __type: 'Pointer', className: 'APIG', objectId: apig },
  261. company: { __type: 'Pointer', className: 'Company', objectId: user },
  262. count: 0,
  263. used: 0
  264. })
  265. });
  266. const created = await createResp.json();
  267. if (created.objectId) {
  268. console.log('[resolveAuthId] 创建成功:', created.objectId);
  269. return created.objectId;
  270. }
  271. console.error('[resolveAuthId] 创建失败:', JSON.stringify(created));
  272. } catch (e: any) {
  273. console.error('[resolveAuthId] 创建异常:', e.message);
  274. }
  275. return null;
  276. }
  277. applyTestTier(): void {
  278. if (this.apig?.priceStep) {
  279. // 套餐价格翻倍
  280. this.apig.priceStep = this.apig.priceStep.map(t => ({
  281. ...t,
  282. price: t.price * 2
  283. }));
  284. }
  285. const params = new URLSearchParams(window.location.search);
  286. if (params.get('test') === '1' && this.apig?.priceStep) {
  287. this.apig.priceStep.unshift({ count: 1, price: 0.01 });
  288. }
  289. }
  290. selectTier(idx: number): void {
  291. this.selectedIndex = idx;
  292. }
  293. // ─── Payment Flow ───
  294. async startPayment(): Promise<void> {
  295. if (!this.apig?.priceStep || this.paying) return;
  296. this.paying = true;
  297. this.errorMsg = '';
  298. try {
  299. // 1. Generate trade number
  300. const now = new Date();
  301. this.tradeNo = 'C' + (this.userId || 'U') +
  302. now.getFullYear() +
  303. String(now.getMonth() + 1).padStart(2, '0') +
  304. String(now.getDate()).padStart(2, '0') +
  305. String(now.getHours()).padStart(2, '0') +
  306. String(now.getMinutes()).padStart(2, '0') +
  307. String(now.getSeconds()).padStart(2, '0') +
  308. now.getMilliseconds();
  309. const tier = this.apig.priceStep[this.selectedIndex];
  310. // 2. Create order
  311. console.log('[PAY] Creating order...', { tradeNo: this.tradeNo, tier: tier.price, funId: this.funId });
  312. await this.createOrder(tier);
  313. console.log('[PAY] Order created, order=', this.order);
  314. // 3. Call pay_code2 to get QR URL
  315. const payParams: any = {
  316. _ApplicationId: APP_ID,
  317. company: PAY_COMPANY,
  318. out_trade_no: this.tradeNo,
  319. total_fee: +tier.price,
  320. body: this.apig.title + ' 接口充值'
  321. };
  322. if (this.funId) {
  323. payParams.fun_id = this.funId;
  324. }
  325. console.log('[PAY] Calling pay_code2 with params:', JSON.stringify(payParams));
  326. let payResp: any = null;
  327. try {
  328. const controller = new AbortController();
  329. const timeout = setTimeout(() => controller.abort(), 15000);
  330. const rawResp = await fetch(PARSE_BASE + '/pay_code2', {
  331. method: 'POST',
  332. headers: { 'Content-Type': 'application/json' },
  333. body: JSON.stringify(payParams),
  334. signal: controller.signal
  335. });
  336. clearTimeout(timeout);
  337. payResp = await rawResp.json();
  338. console.log('[PAY] pay_code2 response:', JSON.stringify(payResp));
  339. } catch (fetchErr: any) {
  340. console.error('[PAY] pay_code2 fetch error:', fetchErr.message);
  341. if (payParams.fun_id) {
  342. console.log('[PAY] Retrying pay_code2 WITHOUT fun_id...');
  343. delete payParams.fun_id;
  344. payResp = await this.postJSON(PARSE_BASE + '/pay_code2', payParams);
  345. console.log('[PAY] pay_code2 retry response:', JSON.stringify(payResp));
  346. } else {
  347. throw fetchErr;
  348. }
  349. }
  350. if (!payResp?.result?.code_url) {
  351. throw new Error('获取支付码失败: ' + JSON.stringify(payResp));
  352. }
  353. const codeUrl = payResp.result.code_url[0];
  354. this.nonceStr = payResp.result.nonce_str;
  355. console.log('[PAY] QR code URL:', codeUrl);
  356. // 4. Show QR modal
  357. this.showQrModal = true;
  358. this.paySuccess = false;
  359. this.cdr.detectChanges();
  360. // Wait for DOM update, then render QR
  361. setTimeout(() => this.renderQR(codeUrl), 100);
  362. // 5. Start polling
  363. this.startPolling();
  364. } catch (e: any) {
  365. this.errorMsg = '支付发起失败: ' + e.message;
  366. } finally {
  367. this.paying = false;
  368. }
  369. }
  370. async createOrder(tier: PriceTier): Promise<void> {
  371. try {
  372. const body: any = {
  373. type: 'wxpay',
  374. authid: this.authId,
  375. params: {
  376. out_trade_no: this.tradeNo,
  377. total_fee: +tier.price,
  378. body: this.apig!.title + ' 接口充值'
  379. },
  380. apigid: this.apig!.objectId,
  381. oldCount: this.apig!.count || 0,
  382. count: tier.count,
  383. user: this.userId || this.authId,
  384. fcompany: this.userId || this.authId
  385. };
  386. const resp = await this.postJSON(API_BASE + '/api/apig/created-apigorder', body);
  387. if (resp.code === 200 && resp.data) {
  388. this.order = resp.data;
  389. console.log('Order created:', this.order);
  390. }
  391. } catch (e: any) {
  392. console.warn('Order creation error (non-blocking):', e.message);
  393. }
  394. }
  395. startPolling(): void {
  396. if (this.pollTimer) clearInterval(this.pollTimer);
  397. this.pollTimer = setInterval(async () => {
  398. try {
  399. const resp = await this.postJSON(PARSE_BASE + '/order_status2', {
  400. _ApplicationId: APP_ID,
  401. out_trade_no: this.tradeNo,
  402. nonce_str: this.nonceStr,
  403. company: PAY_COMPANY
  404. });
  405. if (resp.result?.status?.[0] === 'SUCCESS') {
  406. clearInterval(this.pollTimer);
  407. this.pollTimer = null;
  408. this.paySuccess = true;
  409. await this.doRecharge();
  410. }
  411. } catch (e: any) {
  412. console.warn('Poll error:', e.message);
  413. }
  414. }, 3000);
  415. }
  416. async doRecharge(): Promise<void> {
  417. this.showQrModal = false;
  418. this.showSuccess = true;
  419. this.cdr.detectChanges();
  420. const oldCount = this.apig!.count || 0;
  421. const tier = this.apig!.priceStep![this.selectedIndex];
  422. console.log('═══ [RECHARGE] 开始 ═══ oldCount:', oldCount, 'addCount:', tier.count, 'fun_id:', this.funId);
  423. // 步骤1: 等待后端微信回调自动执行云函数
  424. for (let i = 1; i <= 8; i++) {
  425. console.log('[RECHARGE] 步骤1: 等待后端回调... 第' + i + '/8次 (3秒后)');
  426. await this.sleep(3000);
  427. await this.refreshBalance();
  428. if (this.apig!.count! > oldCount) {
  429. console.log('[RECHARGE] ✅ 后端回调充值成功! 余额:', oldCount, '→', this.apig!.count);
  430. this.cdr.detectChanges();
  431. this.loadOrderHistory();
  432. return;
  433. }
  434. }
  435. // 步骤2: 后端回调未生效,调用 saveRecharge 保底
  436. console.log('[RECHARGE] 步骤2: 后端回调未生效,调用 saveRecharge 保底...');
  437. try {
  438. const rechargeBody = {
  439. user: this.userId || this.authId,
  440. authComp: this.userId || this.authId,
  441. authid: this.authId,
  442. apigid: this.apig!.objectId,
  443. oldCount: oldCount,
  444. count: tier.count,
  445. orderid: this.order?.objectId || ''
  446. };
  447. console.log('[RECHARGE] saveRecharge 参数:', JSON.stringify(rechargeBody));
  448. const rechargeResp = await this.postJSON(API_BASE + '/api/apig/saveRecharge', rechargeBody);
  449. console.log('[RECHARGE] saveRecharge 响应:', JSON.stringify(rechargeResp));
  450. } catch (e: any) {
  451. console.warn('[RECHARGE] saveRecharge 失败:', e.message);
  452. }
  453. // 步骤3: 再轮询确认
  454. for (let i = 1; i <= 3; i++) {
  455. await this.sleep(2000);
  456. await this.refreshBalance();
  457. if (this.apig!.count! > oldCount) {
  458. console.log('[RECHARGE] ✅ saveRecharge 充值成功! 余额:', oldCount, '→', this.apig!.count);
  459. break;
  460. }
  461. }
  462. if (this.apig!.count! <= oldCount) {
  463. console.warn('[RECHARGE] ⚠️ 充值可能延迟,请稍后刷新页面查看');
  464. }
  465. console.log('═══ [RECHARGE] 结束 ═══ 最终余额:', this.apig!.count);
  466. this.cdr.detectChanges();
  467. this.loadOrderHistory();
  468. }
  469. async refreshBalance(): Promise<void> {
  470. try {
  471. const resp = await this.postJSON(API_BASE + '/api/apig/getApig', { authid: this.authId });
  472. if (resp.code === 200 && resp.data?.count != null) {
  473. this.apig!.count = resp.data.count;
  474. const tier = this.apig!.priceStep![this.selectedIndex];
  475. this.successDetailHtml =
  476. '已充值 <strong style="color:var(--neon);">' + this.apig!.title + '</strong> 接口<br>' +
  477. '获得 <strong style="color:var(--success);">' + tier.count + ' ' + this.unitLabel + '</strong>,有效期 730 天<br>' +
  478. '<span style="color:var(--text-dim);margin-top:8px;display:inline-block;">当前余额: <strong style="color:var(--neon);">' + this.apig!.count + ' ' + this.unitLabel + '</strong></span>';
  479. console.log('Balance refreshed:', this.apig!.count);
  480. }
  481. } catch (e: any) {
  482. console.warn('Balance refresh failed:', e.message);
  483. }
  484. }
  485. async renderQR(url: string): Promise<void> {
  486. const canvas = this.qrCanvasRef?.nativeElement;
  487. if (!canvas) { console.warn('QR canvas not found'); return; }
  488. canvas.width = 200;
  489. canvas.height = 200;
  490. try {
  491. await QRCode.toCanvas(canvas, url, {
  492. width: 200,
  493. margin: 0,
  494. color: { dark: '#000000', light: '#ffffff' }
  495. });
  496. console.log('QR rendered via canvas');
  497. } catch (e) {
  498. console.warn('QRCode.toCanvas failed, using img fallback:', e);
  499. canvas.style.display = 'none';
  500. const wrapper = canvas.parentElement;
  501. if (wrapper) {
  502. const img = document.createElement('img');
  503. img.src = 'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=' + encodeURIComponent(url);
  504. img.width = 200;
  505. img.height = 200;
  506. img.alt = 'Payment QR Code';
  507. img.style.display = 'block';
  508. wrapper.appendChild(img);
  509. }
  510. }
  511. }
  512. cancelPayment(): void {
  513. if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
  514. this.showQrModal = false;
  515. }
  516. handleDone(): void {
  517. try {
  518. const msg = { type: 'apig-payment-done', authid: this.authId, user: this.userId };
  519. if (window.opener) (window.opener as any).postMessage(msg, '*');
  520. if (window.parent !== window) window.parent.postMessage(msg, '*');
  521. } catch (e) { console.warn('postMessage failed:', e); }
  522. try { window.close(); } catch (e) {}
  523. setTimeout(() => { window.location.reload(); }, 500);
  524. }
  525. // ─── Order History ───
  526. async loadOrderHistory(append = false): Promise<void> {
  527. this.ordersLoading = true;
  528. this.orderError = '';
  529. if (!append) {
  530. this.orderSkip = 0;
  531. this.orders = [];
  532. }
  533. try {
  534. const userVal = this.userId || this.authId;
  535. const where = JSON.stringify({
  536. '$or': [
  537. { fromCompany: { __type: 'Pointer', className: 'Company', objectId: userVal } },
  538. { fromUser: { __type: 'Pointer', className: '_User', objectId: userVal } }
  539. ]
  540. });
  541. const qs = new URLSearchParams({
  542. where,
  543. order: '-createdAt',
  544. limit: String(ORDER_PAGE_SIZE),
  545. skip: String(this.orderSkip)
  546. });
  547. const resp = await fetch(API_BASE + '/parse/classes/APIGOrder?' + qs.toString(), {
  548. headers: { 'X-Parse-Application-Id': APP_ID }
  549. });
  550. if (!resp.ok) throw new Error('HTTP ' + resp.status);
  551. const data = await resp.json();
  552. const newOrders: OrderData[] = data.results || [];
  553. console.log('Order history loaded:', newOrders.length, 'skip:', this.orderSkip);
  554. this.orders = append ? [...this.orders, ...newOrders] : newOrders;
  555. this.hasMoreOrders = newOrders.length >= ORDER_PAGE_SIZE;
  556. if (this.hasMoreOrders) this.orderSkip += ORDER_PAGE_SIZE;
  557. } catch (e: any) {
  558. console.warn('Order history error:', e);
  559. this.orderError = e.message;
  560. }
  561. this.ordersLoading = false;
  562. }
  563. // ─── Order formatting helpers ───
  564. formatTime(createdAt: string): string {
  565. if (!createdAt) return '—';
  566. const dt = new Date(createdAt);
  567. return dt.getFullYear() + '-' +
  568. String(dt.getMonth() + 1).padStart(2, '0') + '-' +
  569. String(dt.getDate()).padStart(2, '0') + ' ' +
  570. String(dt.getHours()).padStart(2, '0') + ':' +
  571. String(dt.getMinutes()).padStart(2, '0');
  572. }
  573. formatCount(o: OrderData): string {
  574. const count = o.detail?.addCount || o.count || null;
  575. if (count == null) return '—';
  576. return count + ' ' + this.unitLabel;
  577. }
  578. formatAmount(o: OrderData): string {
  579. return o.price != null ? '¥' + o.price : '—';
  580. }
  581. getStatusClass(o: OrderData): string {
  582. if (o.isPay === true) {
  583. if (o.detail?.addCount && o.detail?.oldCount != null) return 'status-recharged';
  584. return 'status-paid';
  585. }
  586. if (o.isPay === false) {
  587. const age = Date.now() - new Date(o.createdAt).getTime();
  588. return age > 30 * 60 * 1000 ? 'status-failed' : 'status-pending';
  589. }
  590. return 'status-pending';
  591. }
  592. getStatusText(o: OrderData): string {
  593. if (o.isPay === true) {
  594. if (o.detail?.addCount && o.detail?.oldCount != null) return '已充值';
  595. return '已支付';
  596. }
  597. if (o.isPay === false) {
  598. const age = Date.now() - new Date(o.createdAt).getTime();
  599. return age > 30 * 60 * 1000 ? '已过期' : '待支付';
  600. }
  601. return '未知';
  602. }
  603. // ─── Helpers ───
  604. async postJSON(url: string, data: any): Promise<any> {
  605. const resp = await fetch(url, {
  606. method: 'POST',
  607. headers: { 'Content-Type': 'application/json' },
  608. body: JSON.stringify(data)
  609. });
  610. return resp.json();
  611. }
  612. async parseCloudCall(funcId: string, params: any): Promise<any> {
  613. const url = PARSE_BASE + '/' + funcId;
  614. const body = { _ApplicationId: APP_ID, ...params };
  615. const resp = await fetch(url, {
  616. method: 'POST',
  617. headers: {
  618. 'Content-Type': 'application/json',
  619. 'X-Parse-Application-Id': APP_ID
  620. },
  621. body: JSON.stringify(body)
  622. });
  623. if (!resp.ok) {
  624. console.warn('Cloud function ' + funcId + ' returned ' + resp.status);
  625. return null;
  626. }
  627. return resp.json();
  628. }
  629. private sleep(ms: number): Promise<void> {
  630. return new Promise(resolve => setTimeout(resolve, ms));
  631. }
  632. }