|
@@ -1,8 +1,8 @@
|
|
|
-import { Component, OnInit, Input } from '@angular/core';
|
|
|
+import { Component, OnInit, Input, ViewChild, ElementRef } from '@angular/core';
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
import { FormsModule } from '@angular/forms';
|
|
|
import { ActivatedRoute } from '@angular/router';
|
|
|
-import { FmodeObject, FmodeParse } from 'fmode-ng/parse';
|
|
|
+import { FmodeObject, FmodeParse, NovaStorage, NovaFile } from 'fmode-ng/core';
|
|
|
import {
|
|
|
QUOTATION_PRICE_TABLE,
|
|
|
STYLE_LEVELS,
|
|
@@ -151,7 +151,108 @@ export class StageOrderComponent implements OnInit {
|
|
|
homeDefaultRooms = HOME_DEFAULT_ROOMS;
|
|
|
adjustmentRules = ADJUSTMENT_RULES;
|
|
|
|
|
|
- constructor(private route: ActivatedRoute) {}
|
|
|
+ // 文件上传相关
|
|
|
+ @ViewChild('fileInput') fileInput!: ElementRef<HTMLInputElement>;
|
|
|
+ @ViewChild('dropZone') dropZone!: ElementRef<HTMLDivElement>;
|
|
|
+
|
|
|
+ private storage: NovaStorage | null = null;
|
|
|
+ isUploading: boolean = false;
|
|
|
+ uploadProgress: number = 0;
|
|
|
+ projectFiles: Array<{
|
|
|
+ id: string;
|
|
|
+ name: string;
|
|
|
+ url: string;
|
|
|
+ type: string;
|
|
|
+ size: number;
|
|
|
+ uploadedBy: string;
|
|
|
+ uploadedAt: Date;
|
|
|
+ }> = [];
|
|
|
+
|
|
|
+ // 企业微信拖拽相关
|
|
|
+ dragOver: boolean = false;
|
|
|
+ wxFileDropSupported: boolean = false;
|
|
|
+
|
|
|
+ constructor(private route: ActivatedRoute) {
|
|
|
+ this.initStorage();
|
|
|
+ this.checkWxWorkSupport();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化 NovaStorage
|
|
|
+ private async initStorage(): Promise<void> {
|
|
|
+ try {
|
|
|
+ const cid = localStorage.getItem('company') || this.cid || 'cDL6R1hgSi';
|
|
|
+ this.storage = await NovaStorage.withCid(cid);
|
|
|
+ console.log('✅ Stage-order NovaStorage 初始化成功, cid:', cid);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ Stage-order NovaStorage 初始化失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查企业微信拖拽支持
|
|
|
+ private checkWxWorkSupport(): void {
|
|
|
+ // 检查是否在企业微信环境中
|
|
|
+ if (typeof window !== 'undefined' && (window as any).wx) {
|
|
|
+ this.wxFileDropSupported = true;
|
|
|
+ console.log('✅ 企业微信环境,支持文件拖拽');
|
|
|
+
|
|
|
+ // 监听企业微信文件拖拽事件
|
|
|
+ this.initWxWorkFileDrop();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化企业微信文件拖拽
|
|
|
+ private initWxWorkFileDrop(): void {
|
|
|
+ if (!this.wxFileDropSupported) return;
|
|
|
+
|
|
|
+ // 监听企业微信的文件拖拽事件
|
|
|
+ document.addEventListener('dragover', (e) => {
|
|
|
+ if (this.isWxWorkFileDrop(e)) {
|
|
|
+ e.preventDefault();
|
|
|
+ this.dragOver = true;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ document.addEventListener('dragleave', (e) => {
|
|
|
+ if (this.isWxWorkFileDrop(e)) {
|
|
|
+ e.preventDefault();
|
|
|
+ this.dragOver = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ document.addEventListener('drop', (e) => {
|
|
|
+ if (this.isWxWorkFileDrop(e)) {
|
|
|
+ e.preventDefault();
|
|
|
+ this.dragOver = false;
|
|
|
+ this.handleWxWorkFileDrop(e);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 判断是否为企业微信文件拖拽
|
|
|
+ private isWxWorkFileDrop(event: DragEvent): boolean {
|
|
|
+ if (!this.wxFileDropSupported) return false;
|
|
|
+
|
|
|
+ // 检查拖拽的数据是否包含企业微信文件信息
|
|
|
+ const dataTransfer = event.dataTransfer;
|
|
|
+ if (dataTransfer && dataTransfer.types) {
|
|
|
+ return dataTransfer.types.includes('Files') ||
|
|
|
+ dataTransfer.types.includes('application/x-wx-work-file');
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理企业微信文件拖拽
|
|
|
+ private async handleWxWorkFileDrop(event: DragEvent): Promise<void> {
|
|
|
+ if (!this.project || !this.storage) return;
|
|
|
+
|
|
|
+ const files = Array.from(event.dataTransfer?.files || []);
|
|
|
+ if (files.length === 0) return;
|
|
|
+
|
|
|
+ console.log('🎯 接收到企业微信拖拽文件:', files.map(f => f.name));
|
|
|
+
|
|
|
+ await this.uploadFiles(files, '企业微信拖拽');
|
|
|
+ }
|
|
|
|
|
|
async ngOnInit() {
|
|
|
if (!this.project || !this.customer || !this.currentUser) {
|
|
@@ -237,6 +338,9 @@ export class StageOrderComponent implements OnInit {
|
|
|
// 加载已分配的项目团队
|
|
|
await this.loadProjectTeams();
|
|
|
|
|
|
+ // 加载项目文件
|
|
|
+ await this.loadProjectFiles();
|
|
|
+
|
|
|
} catch (err) {
|
|
|
console.error('加载失败:', err);
|
|
|
} finally {
|
|
@@ -797,4 +901,254 @@ export class StageOrderComponent implements OnInit {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 加载项目文件
|
|
|
+ */
|
|
|
+ private async loadProjectFiles(): Promise<void> {
|
|
|
+ if (!this.project) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const query = new Parse.Query('ProjectFile');
|
|
|
+ query.equalTo('project', this.project.toPointer());
|
|
|
+ query.include('uploadedBy');
|
|
|
+ query.descending('uploadedAt');
|
|
|
+ query.limit(50);
|
|
|
+
|
|
|
+ const files = await query.find();
|
|
|
+
|
|
|
+ this.projectFiles = files.map((file: FmodeObject) => ({
|
|
|
+ id: file.id || '',
|
|
|
+ name: file.get('name') || file.get('originalName') || '',
|
|
|
+ url: file.get('url') || '',
|
|
|
+ type: file.get('type') || '',
|
|
|
+ size: file.get('size') || 0,
|
|
|
+ uploadedBy: file.get('uploadedBy')?.get('name') || '未知用户',
|
|
|
+ uploadedAt: file.get('uploadedAt') || file.createdAt
|
|
|
+ }));
|
|
|
+
|
|
|
+ console.log(`✅ 加载了 ${this.projectFiles.length} 个项目文件`);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 加载项目文件失败:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 触发文件选择
|
|
|
+ */
|
|
|
+ triggerFileSelect(): void {
|
|
|
+ if (!this.canEdit || !this.project) return;
|
|
|
+ this.fileInput.nativeElement.click();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理文件选择
|
|
|
+ */
|
|
|
+ onFileSelect(event: Event): void {
|
|
|
+ const target = event.target as HTMLInputElement;
|
|
|
+ const files = Array.from(target.files || []);
|
|
|
+
|
|
|
+ if (files.length > 0) {
|
|
|
+ this.uploadFiles(files, '手动选择');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清空input值,允许重复选择同一文件
|
|
|
+ target.value = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 上传文件到 ProjectFile 表
|
|
|
+ */
|
|
|
+ private async uploadFiles(files: File[], source: string): Promise<void> {
|
|
|
+ if (!this.project || !this.storage || !this.currentUser) {
|
|
|
+ console.error('❌ 缺少必要信息,无法上传文件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.isUploading = true;
|
|
|
+ this.uploadProgress = 0;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const results = [];
|
|
|
+
|
|
|
+ for (let i = 0; i < files.length; i++) {
|
|
|
+ const file = files[i];
|
|
|
+
|
|
|
+ // 更新进度
|
|
|
+ this.uploadProgress = ((i + 1) / files.length) * 100;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 使用 NovaStorage 上传文件,指定项目路径前缀
|
|
|
+ const uploaded: NovaFile = await this.storage.upload(file, {
|
|
|
+ prefixKey: `projects/${this.projectId}/`,
|
|
|
+ onProgress: (p) => {
|
|
|
+ const fileProgress = (i / files.length) * 100 + (p.total.percent / files.length);
|
|
|
+ this.uploadProgress = fileProgress;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 保存文件信息到 ProjectFile 表
|
|
|
+ const projectFile = new Parse.Object('ProjectFile');
|
|
|
+ projectFile.set('project', this.project.toPointer());
|
|
|
+ projectFile.set('name', file.name);
|
|
|
+ projectFile.set('originalName', file.name);
|
|
|
+ projectFile.set('url', uploaded.url);
|
|
|
+ projectFile.set('key', uploaded.key);
|
|
|
+ projectFile.set('type', file.type);
|
|
|
+ projectFile.set('size', file.size);
|
|
|
+ projectFile.set('uploadedBy', this.currentUser.toPointer());
|
|
|
+ projectFile.set('uploadedAt', new Date());
|
|
|
+ projectFile.set('source', source); // 标记来源:企业微信拖拽或手动选择
|
|
|
+ projectFile.set('md5', uploaded.md5);
|
|
|
+ projectFile.set('metadata', uploaded.metadata);
|
|
|
+
|
|
|
+ const savedFile = await projectFile.save();
|
|
|
+
|
|
|
+ // 添加到本地列表
|
|
|
+ this.projectFiles.unshift({
|
|
|
+ id: savedFile.id || '',
|
|
|
+ name: file.name,
|
|
|
+ url: uploaded.url || '',
|
|
|
+ type: file.type,
|
|
|
+ size: file.size,
|
|
|
+ uploadedBy: this.currentUser.get('name'),
|
|
|
+ uploadedAt: new Date()
|
|
|
+ });
|
|
|
+
|
|
|
+ results.push({
|
|
|
+ success: true,
|
|
|
+ file: uploaded,
|
|
|
+ name: file.name
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log('✅ 文件上传成功:', file.name, uploaded.key);
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 文件上传失败:', file.name, error);
|
|
|
+ results.push({
|
|
|
+ success: false,
|
|
|
+ error: error instanceof Error ? error.message : '上传失败',
|
|
|
+ name: file.name
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示上传结果
|
|
|
+ const successCount = results.filter(r => r.success).length;
|
|
|
+ const failCount = results.filter(r => !r.success).length;
|
|
|
+
|
|
|
+ if (failCount === 0) {
|
|
|
+ console.log(`🎉 所有 ${successCount} 个文件上传成功`);
|
|
|
+ // 可以显示成功提示
|
|
|
+ } else {
|
|
|
+ console.warn(`⚠️ ${successCount} 个文件成功,${failCount} 个文件失败`);
|
|
|
+ // 可以显示部分失败的提示
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 批量上传过程中发生错误:', error);
|
|
|
+ } finally {
|
|
|
+ this.isUploading = false;
|
|
|
+ this.uploadProgress = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理拖拽事件
|
|
|
+ */
|
|
|
+ onDragOver(event: DragEvent): void {
|
|
|
+ event.preventDefault();
|
|
|
+ event.stopPropagation();
|
|
|
+ this.dragOver = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ onDragLeave(event: DragEvent): void {
|
|
|
+ event.preventDefault();
|
|
|
+ event.stopPropagation();
|
|
|
+ this.dragOver = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ onDrop(event: DragEvent): void {
|
|
|
+ event.preventDefault();
|
|
|
+ event.stopPropagation();
|
|
|
+ this.dragOver = false;
|
|
|
+
|
|
|
+ if (!this.canEdit || !this.project) return;
|
|
|
+
|
|
|
+ const files = Array.from(event.dataTransfer?.files || []);
|
|
|
+ if (files.length > 0) {
|
|
|
+ console.log('🎯 接收到拖拽文件:', files.map(f => f.name));
|
|
|
+ this.uploadFiles(files, '拖拽上传');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除项目文件
|
|
|
+ */
|
|
|
+ async deleteProjectFile(fileId: string): Promise<void> {
|
|
|
+ if (!this.canEdit) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const query = new Parse.Query('ProjectFile');
|
|
|
+ const file = await query.get(fileId);
|
|
|
+
|
|
|
+ if (file) {
|
|
|
+ await file.destroy();
|
|
|
+
|
|
|
+ // 从本地列表中移除
|
|
|
+ this.projectFiles = this.projectFiles.filter(f => f.id !== fileId);
|
|
|
+
|
|
|
+ console.log('✅ 文件删除成功:', fileId);
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('❌ 文件删除失败:', error);
|
|
|
+ alert('删除失败,请稍后重试');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化文件大小
|
|
|
+ */
|
|
|
+ formatFileSize(bytes: number): string {
|
|
|
+ if (bytes === 0) return '0 Bytes';
|
|
|
+ const k = 1024;
|
|
|
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
|
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
|
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取文件图标
|
|
|
+ */
|
|
|
+ getFileIcon(type: string): string {
|
|
|
+ if (type.startsWith('image/')) return '🖼️';
|
|
|
+ if (type.startsWith('video/')) return '🎬';
|
|
|
+ if (type.startsWith('audio/')) return '🎵';
|
|
|
+ if (type.includes('pdf')) return '📄';
|
|
|
+ if (type.includes('word') || type.includes('document')) return '📝';
|
|
|
+ if (type.includes('excel') || type.includes('spreadsheet')) return '📊';
|
|
|
+ if (type.includes('powerpoint') || type.includes('presentation')) return '📑';
|
|
|
+ if (type.includes('zip') || type.includes('rar') || type.includes('7z')) return '📦';
|
|
|
+ return '📎';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查是否为图片文件
|
|
|
+ */
|
|
|
+ isImageFile(type: string): boolean {
|
|
|
+ return type.startsWith('image/');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 下载文件
|
|
|
+ */
|
|
|
+ downloadFile(url: string, name: string): void {
|
|
|
+ const link = document.createElement('a');
|
|
|
+ link.href = url;
|
|
|
+ link.download = name;
|
|
|
+ link.target = '_blank';
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click();
|
|
|
+ document.body.removeChild(link);
|
|
|
+ }
|
|
|
+
|
|
|
}
|