| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698 |
- import { Component, OnInit, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
- import { CommonModule } from '@angular/common';
- import { LoginModalComponent } from './login-modal.component';
- import QRCode from 'qrcode';
- // ═══════════════════════════════════════════════════
- // APIG Payment Page — Angular Component
- // ═══════════════════════════════════════════════════
- const API_BASE = 'https://server.fmode.cn';
- const PARSE_BASE = API_BASE + '/parse/functions';
- const PAY_COMPANY = '1AiWpTEDH9';
- const APP_ID = 'ncloudmaster';
- const DEFAULT_FUN_ID = 'HOkkX72PMF';
- const ORDER_PAGE_SIZE = 10;
- interface PriceTier {
- count: number;
- price: number;
- }
- interface ApigData {
- objectId: string;
- title: string;
- content?: string;
- count?: number;
- priceStep?: PriceTier[];
- [key: string]: any;
- }
- interface OrderData {
- objectId: string;
- createdAt: string;
- isPay?: boolean;
- price?: number;
- count?: number;
- detail?: { addCount?: number; oldCount?: number };
- [key: string]: any;
- }
- @Component({
- selector: 'app-root',
- imports: [CommonModule, LoginModalComponent],
- templateUrl: './app.html',
- styleUrl: './app.scss'
- })
- export class App implements OnInit {
- @ViewChild('qrCanvas', { static: false }) qrCanvasRef!: ElementRef<HTMLCanvasElement>;
- constructor(private cdr: ChangeDetectorRef) {}
- // URL params
- authId = '';
- userId = '';
- apigId = '';
- funId = DEFAULT_FUN_ID;
- // Login state
- needLogin = false;
- loggedInUser: string = '';
- apigList: ApigData[] = [];
- apigListLoading = false;
- // State
- apig: ApigData | null = null;
- selectedIndex = 0;
- errorMsg = '';
- paying = false;
- showQrModal = false;
- showSuccess = false;
- paySuccess = false;
- successDetailHtml = '';
- // Order state
- order: any = null;
- tradeNo = '';
- nonceStr = '';
- pollTimer: any = null;
- // Order history
- orders: OrderData[] = [];
- ordersLoading = false;
- orderError = '';
- hasMoreOrders = false;
- orderSkip = 0;
- get selectedTier(): PriceTier | null {
- if (!this.apig?.priceStep || this.apig.priceStep.length === 0) return null;
- return this.apig.priceStep[this.selectedIndex] || null;
- }
- get unitLabel(): string {
- if (!this.apig) return '次';
- return (this.apig.objectId === 'MYM5zJBKgw' || this.apig.objectId === 'FQtTgjcqIZ') ? 'token' : '次';
- }
- ngOnInit(): void {
- const params = new URLSearchParams(window.location.search);
- this.authId = params.get('authid') || '';
- this.userId = params.get('user') || params.get('userid') || '';
- this.apigId = params.get('apigid') || '';
- this.funId = params.get('fun_id') || DEFAULT_FUN_ID;
- // 1. URL 已提供足够参数 → 直接加载
- if (this.authId || (this.userId && this.apigId)) {
- console.log('[AUTH] URL 参数充足,直接加载');
- this.loadApig();
- return;
- }
- // 2. URL 没有 user → 检查 localStorage 缓存
- const cachedUserId = localStorage.getItem('apig_user_id');
- const cachedToken = localStorage.getItem('apig_session_token');
- if (cachedUserId && cachedToken) {
- console.log('[AUTH] 从 localStorage 恢复用户:', cachedUserId);
- this.userId = cachedUserId;
- this.loggedInUser = cachedUserId;
- // 后台验证 token 是否还有效
- this.validateCachedToken(cachedToken, cachedUserId);
- if (this.apigId) {
- this.loadApig();
- } else {
- this.loadApigList();
- }
- return;
- }
- // 3. 什么都没有 → 显示登录弹窗
- console.log('[AUTH] 未检测到用户,显示登录弹窗');
- this.needLogin = true;
- }
- async validateCachedToken(token: string, userId: string): Promise<void> {
- try {
- const resp = await fetch(`${API_BASE}/parse/users/me`, {
- headers: {
- 'X-Parse-Application-Id': APP_ID,
- 'X-Parse-Session-Token': token
- }
- });
- const data = await resp.json();
- if (!data.objectId) {
- console.warn('[AUTH] 缓存 token 已失效,需要重新登录');
- localStorage.removeItem('apig_user_id');
- localStorage.removeItem('apig_session_token');
- this.needLogin = true;
- }
- } catch (e) {
- console.warn('[AUTH] 验证 token 失败:', e);
- }
- }
- onLoginSuccess(event: { userId: string; sessionToken: string }): void {
- console.log('[AUTH] 登录成功, userId:', event.userId);
- this.userId = event.userId;
- this.loggedInUser = event.userId;
- this.needLogin = false;
- this.errorMsg = '';
- if (!this.apigId) {
- this.loadApigList();
- return;
- }
- this.loadApig();
- }
- // ─── Load available APIG list (when no apigId) ───
- async loadApigList(): Promise<void> {
- this.apigListLoading = true;
- try {
- const query: any = {
- where: JSON.stringify({
- objectId: { $in: ['Vo3ROWEvDy'] }
- }),
- keys: 'title,content,priceStep',
- limit: '10'
- };
- const resp = await fetch(API_BASE + '/parse/classes/APIG?' + new URLSearchParams(query), {
- headers: { 'X-Parse-Application-Id': APP_ID }
- });
- const data = await resp.json();
- if (data.results?.length > 0) {
- this.apigList = data.results;
- console.log('[APIG] 获取到', data.results.length, '个可用 API');
- } else {
- console.log('[APIG] 未查询到可用 API');
- }
- this.cdr.detectChanges();
- } catch (e: any) {
- console.warn('[APIG] 查询 API 列表失败:', e.message);
- } finally {
- this.apigListLoading = false;
- }
- }
- navigateToApig(apigId: string): void {
- const url = new URL(window.location.href);
- url.searchParams.set('apigid', apigId);
- window.location.href = url.toString();
- }
- // ─── Load APIG Info ───
- async loadApig(): Promise<void> {
- try {
- // 如果没有 authId 但有 user+apigid,先查询/创建 APIGAuth
- if (!this.authId && this.userId && this.apigId) {
- console.log('[INIT] 无 authId,通过 user+apigid 查询 APIGAuth...');
- const resolved = await this.resolveAuthId(this.userId, this.apigId);
- if (!resolved) {
- this.errorMsg = '无法获取或创建计费账套,请联系管理员。';
- return;
- }
- this.authId = resolved;
- console.log('[INIT] 获取到 authId:', this.authId);
- }
- // 1. Try getApig REST endpoint
- const resp = await this.postJSON(API_BASE + '/api/apig/getApig', { authid: this.authId });
- console.log('[getApig] response:', JSON.stringify(resp).substring(0, 500));
- if (resp.code === 200 && resp.data) {
- this.apig = resp.data;
- this.applyTestTier();
- this.selectTier(0);
- this.cdr.detectChanges();
- this.loadOrderHistory();
- return;
- }
- // 2. Fallback: cloud function
- console.log('getApig returned no data, trying cloud function...');
- const params = new URLSearchParams(window.location.search);
- const cfFuncName = params.get('cfName') || 'getApigInfo';
- try {
- const cfResp = await this.parseCloudCall(cfFuncName, { apigId: this.authId });
- if (cfResp?.result) {
- const cfData = cfResp.result.data || cfResp.result;
- if (cfData?.objectId) {
- this.apig = cfData;
- this.applyTestTier();
- this.selectTier(0);
- this.cdr.detectChanges();
- this.loadOrderHistory();
- return;
- }
- }
- } catch (e: any) {
- console.warn('cloud function error:', e);
- }
- this.errorMsg = '无法加载接口信息。请确认参数正确。';
- } catch (e: any) {
- this.errorMsg = '网络错误: ' + e.message;
- }
- }
- // ─── 通过 user+apigid 查询或创建 APIGAuth ───
- async resolveAuthId(user: string, apig: string): Promise<string | null> {
- // 1. 查询已有 APIGAuth
- try {
- const query: any = {
- where: JSON.stringify({
- api: { __type: 'Pointer', className: 'APIG', objectId: apig },
- company: { __type: 'Pointer', className: 'Company', objectId: user }
- }),
- limit: '1'
- };
- const resp = await fetch(API_BASE + '/parse/classes/APIGAuth?' + new URLSearchParams(query), {
- headers: { 'X-Parse-Application-Id': APP_ID }
- });
- const data = await resp.json();
- if (data.results?.length > 0) {
- console.log('[resolveAuthId] 找到已有 APIGAuth:', data.results[0].objectId);
- return data.results[0].objectId;
- }
- } catch (e: any) {
- console.warn('[resolveAuthId] 查询失败:', e.message);
- }
- // 2. 创建新 APIGAuth
- try {
- console.log('[resolveAuthId] 未找到 APIGAuth,创建新记录...');
- const createResp = await fetch(API_BASE + '/parse/classes/APIGAuth', {
- method: 'POST',
- headers: {
- 'X-Parse-Application-Id': APP_ID,
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify({
- api: { __type: 'Pointer', className: 'APIG', objectId: apig },
- company: { __type: 'Pointer', className: 'Company', objectId: user },
- count: 0,
- used: 0
- })
- });
- const created = await createResp.json();
- if (created.objectId) {
- console.log('[resolveAuthId] 创建成功:', created.objectId);
- return created.objectId;
- }
- console.error('[resolveAuthId] 创建失败:', JSON.stringify(created));
- } catch (e: any) {
- console.error('[resolveAuthId] 创建异常:', e.message);
- }
- return null;
- }
- applyTestTier(): void {
- if (this.apig?.priceStep) {
- // 套餐价格翻倍
- this.apig.priceStep = this.apig.priceStep.map(t => ({
- ...t,
- price: t.price * 2
- }));
- }
- const params = new URLSearchParams(window.location.search);
- if (params.get('test') === '1' && this.apig?.priceStep) {
- this.apig.priceStep.unshift({ count: 1, price: 0.01 });
- }
- }
- selectTier(idx: number): void {
- this.selectedIndex = idx;
- }
- // ─── Payment Flow ───
- async startPayment(): Promise<void> {
- if (!this.apig?.priceStep || this.paying) return;
- this.paying = true;
- this.errorMsg = '';
- try {
- // 1. Generate trade number
- const now = new Date();
- this.tradeNo = 'C' + (this.userId || 'U') +
- now.getFullYear() +
- String(now.getMonth() + 1).padStart(2, '0') +
- String(now.getDate()).padStart(2, '0') +
- String(now.getHours()).padStart(2, '0') +
- String(now.getMinutes()).padStart(2, '0') +
- String(now.getSeconds()).padStart(2, '0') +
- now.getMilliseconds();
- const tier = this.apig.priceStep[this.selectedIndex];
- // 2. Create order
- console.log('[PAY] Creating order...', { tradeNo: this.tradeNo, tier: tier.price, funId: this.funId });
- await this.createOrder(tier);
- console.log('[PAY] Order created, order=', this.order);
- // 3. Call pay_code2 to get QR URL
- const payParams: any = {
- _ApplicationId: APP_ID,
- company: PAY_COMPANY,
- out_trade_no: this.tradeNo,
- total_fee: +tier.price,
- body: this.apig.title + ' 接口充值'
- };
- if (this.funId) {
- payParams.fun_id = this.funId;
- }
- console.log('[PAY] Calling pay_code2 with params:', JSON.stringify(payParams));
- let payResp: any = null;
- try {
- const controller = new AbortController();
- const timeout = setTimeout(() => controller.abort(), 15000);
- const rawResp = await fetch(PARSE_BASE + '/pay_code2', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(payParams),
- signal: controller.signal
- });
- clearTimeout(timeout);
- payResp = await rawResp.json();
- console.log('[PAY] pay_code2 response:', JSON.stringify(payResp));
- } catch (fetchErr: any) {
- console.error('[PAY] pay_code2 fetch error:', fetchErr.message);
- if (payParams.fun_id) {
- console.log('[PAY] Retrying pay_code2 WITHOUT fun_id...');
- delete payParams.fun_id;
- payResp = await this.postJSON(PARSE_BASE + '/pay_code2', payParams);
- console.log('[PAY] pay_code2 retry response:', JSON.stringify(payResp));
- } else {
- throw fetchErr;
- }
- }
- if (!payResp?.result?.code_url) {
- throw new Error('获取支付码失败: ' + JSON.stringify(payResp));
- }
- const codeUrl = payResp.result.code_url[0];
- this.nonceStr = payResp.result.nonce_str;
- console.log('[PAY] QR code URL:', codeUrl);
- // 4. Show QR modal
- this.showQrModal = true;
- this.paySuccess = false;
- this.cdr.detectChanges();
- // Wait for DOM update, then render QR
- setTimeout(() => this.renderQR(codeUrl), 100);
- // 5. Start polling
- this.startPolling();
- } catch (e: any) {
- this.errorMsg = '支付发起失败: ' + e.message;
- } finally {
- this.paying = false;
- }
- }
- async createOrder(tier: PriceTier): Promise<void> {
- try {
- const body: any = {
- type: 'wxpay',
- authid: this.authId,
- params: {
- out_trade_no: this.tradeNo,
- total_fee: +tier.price,
- body: this.apig!.title + ' 接口充值'
- },
- apigid: this.apig!.objectId,
- oldCount: this.apig!.count || 0,
- count: tier.count,
- user: this.userId || this.authId,
- fcompany: this.userId || this.authId
- };
- const resp = await this.postJSON(API_BASE + '/api/apig/created-apigorder', body);
- if (resp.code === 200 && resp.data) {
- this.order = resp.data;
- console.log('Order created:', this.order);
- }
- } catch (e: any) {
- console.warn('Order creation error (non-blocking):', e.message);
- }
- }
- startPolling(): void {
- if (this.pollTimer) clearInterval(this.pollTimer);
- this.pollTimer = setInterval(async () => {
- try {
- const resp = await this.postJSON(PARSE_BASE + '/order_status2', {
- _ApplicationId: APP_ID,
- out_trade_no: this.tradeNo,
- nonce_str: this.nonceStr,
- company: PAY_COMPANY
- });
- if (resp.result?.status?.[0] === 'SUCCESS') {
- clearInterval(this.pollTimer);
- this.pollTimer = null;
- this.paySuccess = true;
- await this.doRecharge();
- }
- } catch (e: any) {
- console.warn('Poll error:', e.message);
- }
- }, 3000);
- }
- async doRecharge(): Promise<void> {
- this.showQrModal = false;
- this.showSuccess = true;
- this.cdr.detectChanges();
- const oldCount = this.apig!.count || 0;
- const tier = this.apig!.priceStep![this.selectedIndex];
- console.log('═══ [RECHARGE] 开始 ═══ oldCount:', oldCount, 'addCount:', tier.count, 'fun_id:', this.funId);
- // 步骤1: 等待后端微信回调自动执行云函数
- for (let i = 1; i <= 8; i++) {
- console.log('[RECHARGE] 步骤1: 等待后端回调... 第' + i + '/8次 (3秒后)');
- await this.sleep(3000);
- await this.refreshBalance();
- if (this.apig!.count! > oldCount) {
- console.log('[RECHARGE] ✅ 后端回调充值成功! 余额:', oldCount, '→', this.apig!.count);
- this.cdr.detectChanges();
- this.loadOrderHistory();
- return;
- }
- }
- // 步骤2: 后端回调未生效,调用 saveRecharge 保底
- console.log('[RECHARGE] 步骤2: 后端回调未生效,调用 saveRecharge 保底...');
- try {
- const rechargeBody = {
- user: this.userId || this.authId,
- authComp: this.userId || this.authId,
- authid: this.authId,
- apigid: this.apig!.objectId,
- oldCount: oldCount,
- count: tier.count,
- orderid: this.order?.objectId || ''
- };
- console.log('[RECHARGE] saveRecharge 参数:', JSON.stringify(rechargeBody));
- const rechargeResp = await this.postJSON(API_BASE + '/api/apig/saveRecharge', rechargeBody);
- console.log('[RECHARGE] saveRecharge 响应:', JSON.stringify(rechargeResp));
- } catch (e: any) {
- console.warn('[RECHARGE] saveRecharge 失败:', e.message);
- }
- // 步骤3: 再轮询确认
- for (let i = 1; i <= 3; i++) {
- await this.sleep(2000);
- await this.refreshBalance();
- if (this.apig!.count! > oldCount) {
- console.log('[RECHARGE] ✅ saveRecharge 充值成功! 余额:', oldCount, '→', this.apig!.count);
- break;
- }
- }
- if (this.apig!.count! <= oldCount) {
- console.warn('[RECHARGE] ⚠️ 充值可能延迟,请稍后刷新页面查看');
- }
- console.log('═══ [RECHARGE] 结束 ═══ 最终余额:', this.apig!.count);
- this.cdr.detectChanges();
- this.loadOrderHistory();
- }
- async refreshBalance(): Promise<void> {
- try {
- const resp = await this.postJSON(API_BASE + '/api/apig/getApig', { authid: this.authId });
- if (resp.code === 200 && resp.data?.count != null) {
- this.apig!.count = resp.data.count;
- const tier = this.apig!.priceStep![this.selectedIndex];
- this.successDetailHtml =
- '已充值 <strong style="color:var(--neon);">' + this.apig!.title + '</strong> 接口<br>' +
- '获得 <strong style="color:var(--success);">' + tier.count + ' ' + this.unitLabel + '</strong>,有效期 730 天<br>' +
- '<span style="color:var(--text-dim);margin-top:8px;display:inline-block;">当前余额: <strong style="color:var(--neon);">' + this.apig!.count + ' ' + this.unitLabel + '</strong></span>';
- console.log('Balance refreshed:', this.apig!.count);
- }
- } catch (e: any) {
- console.warn('Balance refresh failed:', e.message);
- }
- }
- async renderQR(url: string): Promise<void> {
- const canvas = this.qrCanvasRef?.nativeElement;
- if (!canvas) { console.warn('QR canvas not found'); return; }
- canvas.width = 200;
- canvas.height = 200;
- try {
- await QRCode.toCanvas(canvas, url, {
- width: 200,
- margin: 0,
- color: { dark: '#000000', light: '#ffffff' }
- });
- console.log('QR rendered via canvas');
- } catch (e) {
- console.warn('QRCode.toCanvas failed, using img fallback:', e);
- canvas.style.display = 'none';
- const wrapper = canvas.parentElement;
- if (wrapper) {
- const img = document.createElement('img');
- img.src = 'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=' + encodeURIComponent(url);
- img.width = 200;
- img.height = 200;
- img.alt = 'Payment QR Code';
- img.style.display = 'block';
- wrapper.appendChild(img);
- }
- }
- }
- cancelPayment(): void {
- if (this.pollTimer) { clearInterval(this.pollTimer); this.pollTimer = null; }
- this.showQrModal = false;
- }
- handleDone(): void {
- try {
- const msg = { type: 'apig-payment-done', authid: this.authId, user: this.userId };
- if (window.opener) (window.opener as any).postMessage(msg, '*');
- if (window.parent !== window) window.parent.postMessage(msg, '*');
- } catch (e) { console.warn('postMessage failed:', e); }
- try { window.close(); } catch (e) {}
- setTimeout(() => { window.location.reload(); }, 500);
- }
- // ─── Order History ───
- async loadOrderHistory(append = false): Promise<void> {
- this.ordersLoading = true;
- this.orderError = '';
- if (!append) {
- this.orderSkip = 0;
- this.orders = [];
- }
- try {
- const userVal = this.userId || this.authId;
- const where = JSON.stringify({
- '$or': [
- { fromCompany: { __type: 'Pointer', className: 'Company', objectId: userVal } },
- { fromUser: { __type: 'Pointer', className: '_User', objectId: userVal } }
- ]
- });
- const qs = new URLSearchParams({
- where,
- order: '-createdAt',
- limit: String(ORDER_PAGE_SIZE),
- skip: String(this.orderSkip)
- });
- const resp = await fetch(API_BASE + '/parse/classes/APIGOrder?' + qs.toString(), {
- headers: { 'X-Parse-Application-Id': APP_ID }
- });
- if (!resp.ok) throw new Error('HTTP ' + resp.status);
- const data = await resp.json();
- const newOrders: OrderData[] = data.results || [];
- console.log('Order history loaded:', newOrders.length, 'skip:', this.orderSkip);
- this.orders = append ? [...this.orders, ...newOrders] : newOrders;
- this.hasMoreOrders = newOrders.length >= ORDER_PAGE_SIZE;
- if (this.hasMoreOrders) this.orderSkip += ORDER_PAGE_SIZE;
- } catch (e: any) {
- console.warn('Order history error:', e);
- this.orderError = e.message;
- }
- this.ordersLoading = false;
- }
- // ─── Order formatting helpers ───
- formatTime(createdAt: string): string {
- if (!createdAt) return '—';
- const dt = new Date(createdAt);
- return dt.getFullYear() + '-' +
- String(dt.getMonth() + 1).padStart(2, '0') + '-' +
- String(dt.getDate()).padStart(2, '0') + ' ' +
- String(dt.getHours()).padStart(2, '0') + ':' +
- String(dt.getMinutes()).padStart(2, '0');
- }
- formatCount(o: OrderData): string {
- const count = o.detail?.addCount || o.count || null;
- if (count == null) return '—';
- return count + ' ' + this.unitLabel;
- }
- formatAmount(o: OrderData): string {
- return o.price != null ? '¥' + o.price : '—';
- }
- getStatusClass(o: OrderData): string {
- if (o.isPay === true) {
- if (o.detail?.addCount && o.detail?.oldCount != null) return 'status-recharged';
- return 'status-paid';
- }
- if (o.isPay === false) {
- const age = Date.now() - new Date(o.createdAt).getTime();
- return age > 30 * 60 * 1000 ? 'status-failed' : 'status-pending';
- }
- return 'status-pending';
- }
- getStatusText(o: OrderData): string {
- if (o.isPay === true) {
- if (o.detail?.addCount && o.detail?.oldCount != null) return '已充值';
- return '已支付';
- }
- if (o.isPay === false) {
- const age = Date.now() - new Date(o.createdAt).getTime();
- return age > 30 * 60 * 1000 ? '已过期' : '待支付';
- }
- return '未知';
- }
- // ─── Helpers ───
- async postJSON(url: string, data: any): Promise<any> {
- const resp = await fetch(url, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(data)
- });
- return resp.json();
- }
- async parseCloudCall(funcId: string, params: any): Promise<any> {
- const url = PARSE_BASE + '/' + funcId;
- const body = { _ApplicationId: APP_ID, ...params };
- const resp = await fetch(url, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Parse-Application-Id': APP_ID
- },
- body: JSON.stringify(body)
- });
- if (!resp.ok) {
- console.warn('Cloud function ' + funcId + ' returned ' + resp.status);
- return null;
- }
- return resp.json();
- }
- private sleep(ms: number): Promise<void> {
- return new Promise(resolve => setTimeout(resolve, ms));
- }
- }
|