|
@@ -1,9 +1,9 @@
|
|
|
import { Component, OnInit, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
|
|
import { Component, OnInit, ViewChild, ElementRef, ChangeDetectorRef } from '@angular/core';
|
|
|
import { CommonModule } from '@angular/common';
|
|
import { CommonModule } from '@angular/common';
|
|
|
|
|
+import { RouterOutlet } from '@angular/router';
|
|
|
import { LoginModalComponent } from './login-modal.component';
|
|
import { LoginModalComponent } from './login-modal.component';
|
|
|
import { SkillInstallComponent } from './skill-install.component';
|
|
import { SkillInstallComponent } from './skill-install.component';
|
|
|
import { QiweiSkillInstallComponent } from './qiwei-skill-install.component';
|
|
import { QiweiSkillInstallComponent } from './qiwei-skill-install.component';
|
|
|
-import { EnterpriseTrialComponent } from './enterprise-trial.component';
|
|
|
|
|
|
|
|
|
|
import QRCode from 'qrcode';
|
|
import QRCode from 'qrcode';
|
|
|
|
|
|
|
@@ -287,7 +287,7 @@ const LAUNCHER_URL_LIST = 'https://repos.fmode.cn/x/launcher/url.txt';
|
|
|
|
|
|
|
|
@Component({
|
|
@Component({
|
|
|
selector: 'app-root',
|
|
selector: 'app-root',
|
|
|
- imports: [CommonModule, LoginModalComponent, SkillInstallComponent, QiweiSkillInstallComponent, EnterpriseTrialComponent],
|
|
|
|
|
|
|
+ imports: [CommonModule, RouterOutlet, LoginModalComponent, SkillInstallComponent, QiweiSkillInstallComponent],
|
|
|
templateUrl: './app.html',
|
|
templateUrl: './app.html',
|
|
|
styleUrl: './app.scss'
|
|
styleUrl: './app.scss'
|
|
|
})
|
|
})
|
|
@@ -295,7 +295,11 @@ export class App implements OnInit {
|
|
|
@ViewChild('qrCanvas', { static: false }) qrCanvasRef!: ElementRef<HTMLCanvasElement>;
|
|
@ViewChild('qrCanvas', { static: false }) qrCanvasRef!: ElementRef<HTMLCanvasElement>;
|
|
|
|
|
|
|
|
constructor(private cdr: ChangeDetectorRef) {}
|
|
constructor(private cdr: ChangeDetectorRef) {}
|
|
|
- private readonly routeChangeHandler = () => this.syncWorkshopRouteFromUrl();
|
|
|
|
|
|
|
+ private readonly routeChangeHandler = () => {
|
|
|
|
|
+ this.detectPageFlags();
|
|
|
|
|
+ this.syncWorkshopRouteFromUrl();
|
|
|
|
|
+ };
|
|
|
|
|
+ private readonly logoutHandler = () => this.logout();
|
|
|
|
|
|
|
|
// URL params
|
|
// URL params
|
|
|
authId = '';
|
|
authId = '';
|
|
@@ -452,27 +456,14 @@ export class App implements OnInit {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
ngOnInit(): void {
|
|
ngOnInit(): void {
|
|
|
- this.isLauncherPage = window.location.pathname.includes('/launcher') || window.location.hash === '#/launcher';
|
|
|
|
|
- this.isSkillInstallPage = window.location.pathname.includes('/skill-install') || window.location.hash.startsWith('#/skill-install');
|
|
|
|
|
- this.isQiweiSkillPage = window.location.pathname.includes('/qiwei-skill') ||
|
|
|
|
|
- window.location.pathname.includes('/qiwei-trial') ||
|
|
|
|
|
- window.location.hash.startsWith('#/qiwei-skill') ||
|
|
|
|
|
- window.location.hash.startsWith('#/qiwei-trial');
|
|
|
|
|
- this.isEnterpriseTrialPage = window.location.pathname.includes('/enterprise-trial') ||
|
|
|
|
|
- window.location.hash.startsWith('#/enterprise-trial');
|
|
|
|
|
- if (this.isEnterpriseTrialPage) {
|
|
|
|
|
- document.title = '领取 7 天企业微服务试用 | Fmode';
|
|
|
|
|
- } else if (this.isQiweiSkillPage) {
|
|
|
|
|
- document.title = '领取 7 天企微数字员工试用 | Fmode';
|
|
|
|
|
- } else if (this.isSkillInstallPage) {
|
|
|
|
|
- document.title = '开启 VOC 数据洞察 | Fmode';
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ window.addEventListener('fmode-logout', this.logoutHandler);
|
|
|
|
|
+ this.detectPageFlags();
|
|
|
if (this.isLauncherPage) {
|
|
if (this.isLauncherPage) {
|
|
|
this.loadLauncherDownloads();
|
|
this.loadLauncherDownloads();
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
|
+ const params = this.urlParams();
|
|
|
this.syncWorkshopRouteFromUrl(false);
|
|
this.syncWorkshopRouteFromUrl(false);
|
|
|
window.addEventListener('hashchange', this.routeChangeHandler);
|
|
window.addEventListener('hashchange', this.routeChangeHandler);
|
|
|
window.addEventListener('popstate', this.routeChangeHandler);
|
|
window.addEventListener('popstate', this.routeChangeHandler);
|
|
@@ -518,6 +509,26 @@ export class App implements OnInit {
|
|
|
ngOnDestroy(): void {
|
|
ngOnDestroy(): void {
|
|
|
window.removeEventListener('hashchange', this.routeChangeHandler);
|
|
window.removeEventListener('hashchange', this.routeChangeHandler);
|
|
|
window.removeEventListener('popstate', this.routeChangeHandler);
|
|
window.removeEventListener('popstate', this.routeChangeHandler);
|
|
|
|
|
+ window.removeEventListener('fmode-logout', this.logoutHandler);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 从当前 URL 重算各页面类型标志与标题(初始加载与 hashchange/popstate 共用) */
|
|
|
|
|
+ private detectPageFlags(): void {
|
|
|
|
|
+ this.isLauncherPage = window.location.pathname.includes('/launcher') || window.location.hash === '#/launcher';
|
|
|
|
|
+ this.isSkillInstallPage = window.location.pathname.includes('/skill-install') || window.location.hash.startsWith('#/skill-install');
|
|
|
|
|
+ this.isQiweiSkillPage = window.location.pathname.includes('/qiwei-skill') ||
|
|
|
|
|
+ window.location.pathname.includes('/qiwei-trial') ||
|
|
|
|
|
+ window.location.hash.startsWith('#/qiwei-skill') ||
|
|
|
|
|
+ window.location.hash.startsWith('#/qiwei-trial');
|
|
|
|
|
+ this.isEnterpriseTrialPage = window.location.pathname.includes('/enterprise-trial') ||
|
|
|
|
|
+ window.location.hash.startsWith('#/enterprise-trial');
|
|
|
|
|
+ if (this.isEnterpriseTrialPage) {
|
|
|
|
|
+ document.title = '领取 7 天企业微服务试用 | Fmode';
|
|
|
|
|
+ } else if (this.isQiweiSkillPage) {
|
|
|
|
|
+ document.title = '领取 7 天企微数字员工试用 | Fmode';
|
|
|
|
|
+ } else if (this.isSkillInstallPage) {
|
|
|
|
|
+ document.title = '开启 VOC 数据洞察 | Fmode';
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async loadLauncherDownloads(): Promise<void> {
|
|
async loadLauncherDownloads(): Promise<void> {
|
|
@@ -613,6 +624,11 @@ export class App implements OnInit {
|
|
|
localStorage.setItem('apig_session_token', token);
|
|
localStorage.setItem('apig_session_token', token);
|
|
|
localStorage.setItem('apig_display_name', this.loggedInUser);
|
|
localStorage.setItem('apig_display_name', this.loggedInUser);
|
|
|
|
|
|
|
|
|
|
+ // 广播登录事件,供路由内独立渲染的 enterprise-trial 组件同步登录态
|
|
|
|
|
+ window.dispatchEvent(new CustomEvent('fmode-auth-success', {
|
|
|
|
|
+ detail: { userId: this.userId, sessionToken: token, displayName: this.loggedInUser }
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
this.cdr.detectChanges();
|
|
this.cdr.detectChanges();
|
|
|
if (!this.isSkillInstallPage && !this.isQiweiSkillPage && !this.isEnterpriseTrialPage) this.loadApig();
|
|
if (!this.isSkillInstallPage && !this.isQiweiSkillPage && !this.isEnterpriseTrialPage) this.loadApig();
|
|
|
} catch (e: any) {
|
|
} catch (e: any) {
|
|
@@ -658,6 +674,10 @@ export class App implements OnInit {
|
|
|
const phone = String(data.mobile || data.mobilePhoneNumber || '').trim();
|
|
const phone = String(data.mobile || data.mobilePhoneNumber || '').trim();
|
|
|
if (phone) localStorage.setItem('apig_phone', phone);
|
|
if (phone) localStorage.setItem('apig_phone', phone);
|
|
|
console.log('[AUTH] token 验证通过, user:', data.objectId, data.username || '', 'company:', this.companyId || '(用户无 Company,仅用 user 指针扣费)');
|
|
console.log('[AUTH] token 验证通过, user:', data.objectId, data.username || '', 'company:', this.companyId || '(用户无 Company,仅用 user 指针扣费)');
|
|
|
|
|
+ // 广播登录事件,供路由内独立渲染的 enterprise-trial 组件刷新(服务端可能返回了更新的 displayName)
|
|
|
|
|
+ window.dispatchEvent(new CustomEvent('fmode-auth-success', {
|
|
|
|
|
+ detail: { userId: this.userId, sessionToken: token, displayName: this.loggedInUser }
|
|
|
|
|
+ }));
|
|
|
this.cdr.detectChanges();
|
|
this.cdr.detectChanges();
|
|
|
if (!this.isSkillInstallPage && !this.isQiweiSkillPage && !this.isEnterpriseTrialPage) this.loadApig();
|
|
if (!this.isSkillInstallPage && !this.isQiweiSkillPage && !this.isEnterpriseTrialPage) this.loadApig();
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
@@ -674,6 +694,7 @@ export class App implements OnInit {
|
|
|
this.sessionToken = event.sessionToken;
|
|
this.sessionToken = event.sessionToken;
|
|
|
this.needLogin = false;
|
|
this.needLogin = false;
|
|
|
this.errorMsg = '';
|
|
this.errorMsg = '';
|
|
|
|
|
+ window.dispatchEvent(new CustomEvent('fmode-auth-success', { detail: event }));
|
|
|
|
|
|
|
|
// 通过 validateAndLoad 统一处理 Company 解析 + 数据加载
|
|
// 通过 validateAndLoad 统一处理 Company 解析 + 数据加载
|
|
|
await this.validateAndLoad(event.sessionToken, event.userId);
|
|
await this.validateAndLoad(event.sessionToken, event.userId);
|
|
@@ -831,7 +852,7 @@ export class App implements OnInit {
|
|
|
|
|
|
|
|
// 2. Fallback: cloud function
|
|
// 2. Fallback: cloud function
|
|
|
console.log('getApig returned no data, trying cloud function...');
|
|
console.log('getApig returned no data, trying cloud function...');
|
|
|
- const params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
|
+ const params = this.urlParams();
|
|
|
const cfFuncName = params.get('cfName') || 'getApigInfo';
|
|
const cfFuncName = params.get('cfName') || 'getApigInfo';
|
|
|
try {
|
|
try {
|
|
|
const cfResp = await this.parseCloudCall(cfFuncName, { apigId: this.authId });
|
|
const cfResp = await this.parseCloudCall(cfFuncName, { apigId: this.authId });
|
|
@@ -916,7 +937,7 @@ export class App implements OnInit {
|
|
|
|
|
|
|
|
applyTestTier(): void {
|
|
applyTestTier(): void {
|
|
|
// 仅本地开发时允许 URL 带 test=1 追加测试套餐(¥0.01 / 1 次),线上不开放。
|
|
// 仅本地开发时允许 URL 带 test=1 追加测试套餐(¥0.01 / 1 次),线上不开放。
|
|
|
- const params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
|
+ const params = this.urlParams();
|
|
|
const isLocalhost = ['localhost', '127.0.0.1'].includes(window.location.hostname);
|
|
const isLocalhost = ['localhost', '127.0.0.1'].includes(window.location.hostname);
|
|
|
const isTihaoEcommerce = this.apig?.objectId === TIHAO_ECOMMERCE_APIG_ID;
|
|
const isTihaoEcommerce = this.apig?.objectId === TIHAO_ECOMMERCE_APIG_ID;
|
|
|
if (isLocalhost && isTihaoEcommerce && params.get('test') === '1' && this.apig?.priceStep && !this.apig.priceStep.some(tier => tier.isTest)) {
|
|
if (isLocalhost && isTihaoEcommerce && params.get('test') === '1' && this.apig?.priceStep && !this.apig.priceStep.some(tier => tier.isTest)) {
|
|
@@ -934,13 +955,25 @@ export class App implements OnInit {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
isStrictCallbackTest(): boolean {
|
|
isStrictCallbackTest(): boolean {
|
|
|
- const params = new URLSearchParams(window.location.search);
|
|
|
|
|
|
|
+ const params = this.urlParams();
|
|
|
const isLocalhost = ['localhost', '127.0.0.1'].includes(window.location.hostname);
|
|
const isLocalhost = ['localhost', '127.0.0.1'].includes(window.location.hostname);
|
|
|
return isLocalhost && params.get('callback_test') === '1' && this.apig?.objectId === TIHAO_ECOMMERCE_APIG_ID;
|
|
return isLocalhost && params.get('callback_test') === '1' && this.apig?.objectId === TIHAO_ECOMMERCE_APIG_ID;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- syncWorkshopRouteFromUrl(shouldDetectChanges = true): void {
|
|
|
|
|
|
|
+ /** 兼容两种带参方式:?a=1#/route 与 #/route?a=1;hash 内参数仅作补充,不覆盖 search 已有键 */
|
|
|
|
|
+ private urlParams(): URLSearchParams {
|
|
|
const params = new URLSearchParams(window.location.search);
|
|
const params = new URLSearchParams(window.location.search);
|
|
|
|
|
+ const hashQuery = window.location.hash.includes('?') ? window.location.hash.split('?')[1] : '';
|
|
|
|
|
+ if (hashQuery) {
|
|
|
|
|
+ for (const [key, value] of new URLSearchParams(hashQuery)) {
|
|
|
|
|
+ if (!params.has(key)) params.set(key, value);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return params;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ syncWorkshopRouteFromUrl(shouldDetectChanges = true): void {
|
|
|
|
|
+ const params = this.urlParams();
|
|
|
const nextPackage = this.resolveWorkshopPackage(params);
|
|
const nextPackage = this.resolveWorkshopPackage(params);
|
|
|
this.workshopPackage = nextPackage;
|
|
this.workshopPackage = nextPackage;
|
|
|
this.workshopPortal = !nextPackage && this.isWorkshopPortal(params);
|
|
this.workshopPortal = !nextPackage && this.isWorkshopPortal(params);
|