Prechádzať zdrojové kódy

fix: alert with window.fmode.alert

Future 1 deň pred
rodič
commit
ac4b3e0dc5

+ 112 - 0
rules/alert.md

@@ -0,0 +1,112 @@
+# 全局提示与输入调用规范(window.fmode.alert / window.fmode.input)
+
+本文定义统一的 UI 提示与输入接口,避免在各页面中分散使用浏览器 `alert()` 或事件派发。通过在 `app.ts` 中挂载的 `window.fmode`,可直接以函数方式调用。
+
+## 总览
+
+- `window.fmode.alert(detail)`:信息提示或确认弹窗
+- `window.fmode.input(detail)`:输入弹窗(含 `inputs`)
+ - 简化输入用法(与 `prompt()` 等价):`await window.fmode.input(message: string, defaultValue?: string)` 返回输入的字符串或 `null`
+
+二者均返回一个结果对象(等同于 Ionic `AlertController.onDidDismiss()` 的结果),可被 `await`。如果在 `detail` 中提供了 `callback(result)`,系统会优先回调并同时派发 `globalAlert:result` 事件,含 `detail.requestId`(可选)。
+
+## 参数 detail 结构
+
+通用字段(两者均支持):
+- `header?: string` 标题,默认:`提示` / `请输入`
+- `subHeader?: string` 副标题
+- `message?: string` 消息正文(支持换行)
+- `buttons?: Array<{ text: string; role?: 'cancel'|'confirm'|string }>` 按钮定义,默认:
+  - alert:`[{ text: '确定', role: 'confirm' }]`
+  - input:`[{ text: '取消', role: 'cancel' }, { text: '确定', role: 'confirm' }]`
+- `callback?: (result: any) => void` 结果回调函数(优先级高于结果事件派发)
+- `requestId?: string` 请求 ID(用于在结果事件中关联)
+
+仅 `window.fmode.input` 支持:
+- `inputs?: Array<{ name: string; type?: string; placeholder?: string; value?: any }>` 输入项,默认:`[{ name: 'value', type: 'text', placeholder: '请输入内容' }]`
+
+## 返回结果
+
+- Promise 解析为 `result` 对象,包含:
+  - `role`: 按钮角色,如 `'confirm' | 'cancel' | string`
+  - 可能包含输入值(参考 Ionic Alert 返回的 `data` 格式)
+- 若定义了 `detail.callback`,会被调用:`detail.callback(result)`
+- 同时派发:`window.dispatchEvent(new CustomEvent('globalAlert:result', { detail: { requestId, result } }))`
+
+## 使用示例
+
+### 信息提示(alert)
+
+```ts
+await window.fmode.alert({
+  header: '报表导出',
+  message: '✅ 报表导出成功!',
+  buttons: [{ text: '确定', role: 'confirm' }]
+});
+```
+
+### 确认交互(await 结果)
+
+```ts
+const result = await window.fmode.alert({
+  header: '删除确认',
+  message: '确定要删除该记录吗?',
+  buttons: [
+    { text: '取消', role: 'cancel' },
+    { text: '确定', role: 'confirm' }
+  ]
+});
+if (result?.role === 'confirm') {
+  // 执行删除
+}
+```
+
+### 输入弹窗
+
+```ts
+const result = await window.fmode.input({
+  header: '请输入标签',
+  inputs: [
+    { name: 'tag', type: 'text', placeholder: '标签名称' }
+  ],
+  buttons: [
+    { text: '取消', role: 'cancel' },
+    { text: '确定', role: 'confirm' }
+  ]
+});
+if (result?.role === 'confirm') {
+  const value = result?.data?.values?.tag;
+  // 使用输入值
+}
+```
+
+### 简化输入用法(prompt 等价)
+
+```ts
+// 直接传入描述与默认值,返回字符串或 null
+const name = await window.fmode.input('请输入项目名称', '默认项目');
+if (name !== null) {
+  // 用户点击了“确定”,拿到输入字符串
+  console.log('新项目名称:', name);
+} else {
+  // 用户取消
+}
+```
+
+## 迁移规则
+
+- 禁止使用浏览器 `alert()`,统一改为 `window.fmode.alert()`
+- 禁止派发 `globalAlert/globalPrompt` 事件,统一改为直接函数调用
+- 若需要跨组件监听结果,使用 `requestId` + `globalAlert:result` 事件进行关联
+
+## 实现位置
+
+- `src/app/app.ts`:挂载 `window.fmode.alert` / `window.fmode.input`,并通过 Ionic `AlertController` 实现 UI 弹窗与结果返回
+  - 字符串简化用法由 `presentPromptSimple(message, defaultValue?)` 提供,直接返回字符串或 `null`
+- `src/fmode-ng-augmentation.d.ts`:为 `window.fmode` 添加 TypeScript 类型声明
+
+## 注意事项
+
+- 保持按钮的 `role` 与业务语义一致,用于结果分支判断
+- 输入项的 `name` 建议使用业务含义清晰的键名,避免与其它输入重复
+- 在需要异步结果通知的场景下,务必传入 `requestId` 以便事件监听端进行匹配

+ 135 - 1
src/app/app.ts

@@ -1,5 +1,6 @@
 import { Component, signal } from '@angular/core';
 import { Router, RouterModule, RouterOutlet } from '@angular/router';
+import { AlertController } from '@ionic/angular/standalone';
 
 @Component({
   selector: 'app-root',
@@ -13,10 +14,13 @@ export class App {
   protected readonly title = signal('yss-project');
 
   constructor(
-    private router: Router
+    private router: Router,
+    private alertController: AlertController
   ){
     this.initParse();
     this.initAuth();
+    this.initFmodeApi();
+    this.registerGlobalAlertEvents();
   }
 
   // 初始化Parse配置
@@ -43,4 +47,134 @@ export class App {
       console.error('❌ 企业微信认证初始化失败:', error);
     }
   }
+
+  // 注册全局 Alert 事件(信息提示、确认、输入)
+  private registerGlobalAlertEvents(): void {
+    window.addEventListener('globalAlert', (e: Event) => {
+      const evt = e as CustomEvent<any>;
+      this.handleGlobalAlert(evt.detail);
+    });
+    window.addEventListener('globalPrompt', (e: Event) => {
+      const evt = e as CustomEvent<any>;
+      const detail = evt.detail || {};
+      this.presentPrompt(detail);
+    });
+  }
+
+  // 挂载到 window.fmode API,支持直接函数调用
+  private initFmodeApi(): void {
+    try {
+      const fmode: any = (window as any).fmode || {};
+      fmode.alert = (detail: any) => this.handleGlobalAlert(detail);
+      // 支持两种用法:
+      // 1) 对象用法:window.fmode.input({ header, message, inputs, buttons, callback }) 保留原功能
+      // 2) 字符串简化用法:window.fmode.input('描述', '默认值'),行为与 prompt() 一致,返回输入值或 null
+      fmode.input = (detailOrMessage: any, defaultValue?: string) => {
+        if (typeof detailOrMessage === 'string') {
+          return this.presentPromptSimple(detailOrMessage, defaultValue);
+        }
+        return this.presentPrompt(detailOrMessage);
+      };
+      (window as any).fmode = fmode;
+      console.log('✅ window.fmode API 已挂载');
+    } catch (error) {
+      console.error('❌ 挂载 window.fmode 失败:', error);
+    }
+  }
+
+  // 处理全局信息提示/确认
+  private async handleGlobalAlert(detail: any): Promise<any> {
+    const header = detail?.header ?? '提示';
+    const subHeader = detail?.subHeader ?? undefined;
+    const message = detail?.message ?? '';
+    const buttons = detail?.buttons ?? [
+      { text: '确定', role: 'confirm' }
+    ];
+
+    const alert = await this.alertController.create({
+      header,
+      subHeader,
+      message,
+      buttons
+    });
+    await alert.present();
+
+    const result = await alert.onDidDismiss();
+    this.returnResult(detail, result);
+    return result;
+  }
+
+  // 全局输入弹窗
+  private async presentPrompt(detail: any): Promise<any> {
+    const header = detail?.header ?? '请输入';
+    const subHeader = detail?.subHeader ?? undefined;
+    const message = detail?.message ?? '';
+    const inputs = detail?.inputs ?? [
+      { name: 'value', type: 'text', placeholder: '请输入内容' }
+    ];
+    const buttons = detail?.buttons ?? [
+      { text: '取消', role: 'cancel' },
+      { text: '确定', role: 'confirm' }
+    ];
+
+    const alert = await this.alertController.create({
+      header,
+      subHeader,
+      message,
+      inputs,
+      buttons
+    });
+    await alert.present();
+
+    const result = await alert.onDidDismiss();
+    this.returnResult(detail, result);
+    return result;
+  }
+
+  // 字符串简化用法:行为与原生 prompt() 相同,返回输入字符串或 null
+  private async presentPromptSimple(message: string, defaultValue?: string): Promise<string | null> {
+    const alert = await this.alertController.create({
+      header: '请输入',
+      message,
+      inputs: [
+        { name: 'value', type: 'text', placeholder: '请输入内容', value: defaultValue ?? '' }
+      ],
+      buttons: [
+        { text: '取消', role: 'cancel' },
+        {
+          text: '确定',
+          role: 'confirm',
+          handler: (value: any) => {
+            // 主动传递输入值作为 data,确保 onDidDismiss() 可获取字符串
+            const v = value?.value ?? '';
+            alert.dismiss(v, 'confirm');
+            return false; // 阻止默认关闭,使用我们主动 dismiss
+          }
+        }
+      ]
+    });
+    await alert.present();
+
+    const result = await alert.onDidDismiss();
+    if (result.role === 'confirm') {
+      return (result.data as string) ?? '';
+    }
+    return null;
+  }
+
+  // 结果返回:优先调用回调,其次派发结果事件
+  private returnResult(detail: any, result: any): void {
+    try {
+      if (typeof detail?.callback === 'function') {
+        detail.callback(result);
+        return;
+      }
+      const requestId = detail?.requestId ?? undefined;
+      window.dispatchEvent(new CustomEvent('globalAlert:result', {
+        detail: { requestId, result }
+      }));
+    } catch (err) {
+      console.error('全局Alert结果处理异常:', err);
+    }
+  }
 }

+ 20 - 6
src/app/pages/admin/user-management/user-dialog/user-dialog.ts

@@ -83,33 +83,43 @@ export class UserDialogComponent {
   isFormValid(): boolean {
     // 验证用户名
     if (!this.user.username.trim() || this.user.username.length < 3) {
-      alert('用户名至少需要3个字符');
+      window.dispatchEvent(new CustomEvent('globalAlert', {
+        detail: { header: '校验失败', message: '用户名至少需要3个字符', buttons: [{ text: '确定', role: 'confirm' }] }
+      }));
       return false;
     }
 
     // 验证姓名
     if (!this.user.realName.trim()) {
-      alert('请输入真实姓名');
+      window.dispatchEvent(new CustomEvent('globalAlert', {
+        detail: { header: '校验失败', message: '请输入真实姓名', buttons: [{ text: '确定', role: 'confirm' }] }
+      }));
       return false;
     }
 
     // 验证邮箱
     const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
     if (!emailRegex.test(this.user.email)) {
-      alert('请输入有效的邮箱地址');
+      window.dispatchEvent(new CustomEvent('globalAlert', {
+        detail: { header: '校验失败', message: '请输入有效的邮箱地址', buttons: [{ text: '确定', role: 'confirm' }] }
+      }));
       return false;
     }
 
     // 验证手机号
     const phoneRegex = /^1[3-9]\d{9}$/;
     if (!phoneRegex.test(this.user.phone)) {
-      alert('请输入有效的手机号码');
+      window.dispatchEvent(new CustomEvent('globalAlert', {
+        detail: { header: '校验失败', message: '请输入有效的手机号码', buttons: [{ text: '确定', role: 'confirm' }] }
+      }));
       return false;
     }
 
     // 验证角色和部门
     if (!this.user.role || !this.user.department) {
-      alert('请选择角色和部门');
+      window.dispatchEvent(new CustomEvent('globalAlert', {
+        detail: { header: '校验失败', message: '请选择角色和部门', buttons: [{ text: '确定', role: 'confirm' }] }
+      }));
       return false;
     }
 
@@ -122,7 +132,11 @@ export class UserDialogComponent {
     for (let i = 0; i < 12; i++) {
       password += chars.charAt(Math.floor(Math.random() * chars.length));
     }
-    alert(`生成的随机密码:${password}\n请妥善保存!`);
+    window.fmode?.alert?.({
+      header: '生成随机密码',
+      message: `生成的随机密码:${password}\n请妥善保存!`,
+      buttons: [{ text: '确定', role: 'confirm' }]
+    });
   }
 
   onRoleChange(): void {

+ 5 - 1
src/app/pages/designer/dashboard/dashboard.ts

@@ -448,7 +448,11 @@ export class Dashboard implements OnInit {
   openShiftModal(): void {
     // 在实际应用中,这里应该打开一个模态框让用户添加代班任务
     // 这里使用alert模拟
-    alert('将打开添加代班任务的表单');
+    window.fmode?.alert?.({
+      header: '添加代班任务',
+      message: '将打开添加代班任务的表单',
+      buttons: [{ text: '确定', role: 'confirm' }]
+    });
     // 实际实现可能是:this.modalService.openShiftModal();
   }
 

+ 5 - 1
src/app/pages/designer/project-detail/project-detail.ts

@@ -1812,7 +1812,11 @@ export class ProjectDetail implements OnInit, OnDestroy {
 
   // 处理渲染超时预警
   handleRenderTimeout() {
-    alert('已发送渲染超时预警通知');
+    window.fmode?.alert?.({
+      header: '渲染预警',
+      message: '已发送渲染超时预警通知',
+      buttons: [{ text: '确定', role: 'confirm' }]
+    });
   }
 
   // 通知技术支持

+ 5 - 1
src/app/pages/finance/dashboard/dashboard.ts

@@ -545,7 +545,11 @@ export class Dashboard implements OnInit {
     document.body.removeChild(link);
     URL.revokeObjectURL(url);
     
-    alert(`✅ 报表导出成功!\n\n时间维度:${dimension}\n逾期项目数:${overdueCount}个\n平均逾期天数:${avgOverdueDays}天\n\n报表已下载到您的下载文件夹。`);
+    window.fmode?.alert?.({
+      header: '报表导出',
+      message: `✅ 报表导出成功!\n\n时间维度:${dimension}\n逾期项目数:${overdueCount}个\n平均逾期天数:${avgOverdueDays}天\n\n报表已下载到您的下载文件夹。`,
+      buttons: [{ text: '确定', role: 'confirm' }]
+    });
   }
 
   // 新增:查看项目详情

+ 17 - 3
src/app/pages/team-leader/team-management/team-management.ts

@@ -437,7 +437,11 @@ export class TeamManagementComponent implements OnInit {
   // 生成考核报告
   generateReport(): void {
     // 在实际项目中,这里应该生成并下载考核报告
-    alert(`已生成${this.selectedQuarter}考核报告`);
+    window.fmode?.alert?.({
+      header: '考核报告',
+      message: `已生成${this.selectedQuarter}考核报告`,
+      buttons: [{ text: '确定', role: 'confirm' }]
+    });
   }
 
   // 查看绩效报告
@@ -445,7 +449,11 @@ export class TeamManagementComponent implements OnInit {
     // 在实际项目中,这里应该显示该设计师的详细绩效报告
     const designer = this.designers.find(d => d.id === designerId);
     if (designer) {
-      alert(`查看${designer.name}的绩效报告`);
+      window.fmode?.alert?.({
+        header: '绩效报告',
+        message: `查看${designer.name}的绩效报告`,
+        buttons: [{ text: '确定', role: 'confirm' }]
+      });
     }
   }
 
@@ -454,7 +462,13 @@ export class TeamManagementComponent implements OnInit {
     // 在实际项目中,这里应该调用API将绩效数据同步至人事系统
     const designer = this.designers.find(d => d.id === designerId);
     if (designer) {
-      alert(`${designer.name}的绩效数据已同步至人事系统`);
+      window.dispatchEvent(new CustomEvent('globalAlert', {
+        detail: {
+          header: '同步至人事系统',
+          message: `${designer.name}的绩效数据已同步至人事系统`,
+          buttons: [{ text: '确定', role: 'confirm' }]
+        }
+      }));
     }
   }
 

+ 16 - 0
src/fmode-ng-augmentation.d.ts

@@ -54,3 +54,19 @@ declare module 'fmode-ng/social' {
     getCurrentUser?(): Promise<any>;
   }
 }
+
+// 扩展 Window,声明 window.fmode.alert/input
+declare global {
+  interface Window {
+    fmode?: {
+      alert?: (detail: any) => Promise<any> | any;
+      // 保留对象用法:传入配置对象,返回 Alert 的 onDidDismiss 结果
+      input?: ((detail: any) => Promise<any>)
+        // 新增字符串简化用法:与 prompt(message, defaultValue) 一致,返回 string 或 null
+        & ((message: string, defaultValue?: string) => Promise<string | null>);
+      [key: string]: any;
+    };
+  }
+}
+
+export {};