app.ts 22 KB

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