123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623 |
- import { Component, OnInit, OnDestroy } from '@angular/core';
- import { CommonModule } from '@angular/common';
- import { ActivatedRoute, Router } from '@angular/router';
- import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
- import { ProjectService } from '../../../services/project.service';
- import {
- Project,
- RenderProgress,
- ModelCheckItem,
- CustomerFeedback,
- DesignerChange,
- Settlement,
- ProjectStage
- } from '../../../models/project.model';
- import { ConsultationOrderPanelComponent } from '../../../shared/components/consultation-order-panel/consultation-order-panel.component';
- import { RequirementsConfirmCardComponent } from '../../../shared/components/requirements-confirm-card/requirements-confirm-card';
- import { SettlementCardComponent } from '../../../shared/components/settlement-card/settlement-card';
- import { CustomerReviewCardComponent } from '../../../shared/components/customer-review-card/customer-review-card';
- import { ComplaintCardComponent } from '../../../shared/components/complaint-card/complaint-card';
- import { VerticalNavComponent } from './components/vertical-nav/vertical-nav.component';
- interface ExceptionHistory {
- id: string;
- type: 'failed' | 'stuck' | 'quality' | 'other';
- description: string;
- submitTime: Date;
- status: '待处理' | '处理中' | '已解决';
- response?: string;
- }
- interface ProjectMember {
- id: string;
- name: string;
- role: string;
- avatar: string;
- skillMatch: number;
- progress: number;
- contribution: number;
- }
- interface ProjectFile {
- id: string;
- name: string;
- type: string;
- size: string;
- date: string;
- url: string;
- }
- interface TimelineEvent {
- id: string;
- time: string;
- title: string;
- action: string;
- description: string;
- }
- // 新增:四大板块键类型(顶层声明,供组件与模板共同使用)
- type SectionKey = 'order' | 'requirements' | 'delivery' | 'aftercare';
- @Component({
- selector: 'app-project-detail',
- standalone: true,
- imports: [CommonModule, FormsModule, ReactiveFormsModule, ConsultationOrderPanelComponent, RequirementsConfirmCardComponent, SettlementCardComponent, CustomerReviewCardComponent, ComplaintCardComponent, VerticalNavComponent],
- templateUrl: './project-detail.html',
- styleUrls: ['./project-detail.scss', './debug-styles.scss']
- })
- export class ProjectDetail implements OnInit, OnDestroy {
- // 项目基本数据
- projectId: string = '';
- project: Project | undefined;
- renderProgress: RenderProgress | undefined;
- modelCheckItems: ModelCheckItem[] = [];
- feedbacks: CustomerFeedback[] = [];
- designerChanges: DesignerChange[] = [];
- settlements: Settlement[] = [];
- requirementChecklist: string[] = [];
- reminderMessage: string = '';
- isLoadingRenderProgress: boolean = false;
- errorLoadingRenderProgress: boolean = false;
- feedbackTimeoutCountdown: number = 0;
- private countdownInterval: any;
- projects: {id: string, name: string, status: string}[] = [];
- showDropdown: boolean = false;
- currentStage: string = '';
- // 新增:10阶段顺序(串式流程)
- stageOrder: ProjectStage[] = ['订单创建', '需求沟通', '方案确认', '建模', '软装', '渲染', '尾款结算', '客户评价', '投诉处理'];
- // 新增:阶段展开状态(默认全部收起,当前阶段在数据加载后自动展开)
- expandedStages: Partial<Record<ProjectStage, boolean>> = {
- '订单创建': false,
- '需求沟通': false,
- '方案确认': false,
- '建模': false,
- '软装': false,
- '渲染': false,
- '后期': false,
- '尾款结算': false,
- '客户评价': false,
- '投诉处理': false,
- };
- // 新增:四大板块定义与展开状态
- sections: Array<{ key: SectionKey; label: string; stages: ProjectStage[] }> = [
- { key: 'order', label: '订单创建', stages: ['订单创建'] },
- { key: 'requirements', label: '确认需求', stages: ['需求沟通', '方案确认'] },
- { key: 'delivery', label: '交付执行', stages: ['建模', '软装', '渲染'] },
- { key: 'aftercare', label: '售后', stages: ['尾款结算', '客户评价', '投诉处理'] }
- ];
- expandedSection: SectionKey | null = null;
- // 渲染异常反馈相关属性
- exceptionType: 'failed' | 'stuck' | 'quality' | 'other' = 'failed';
- exceptionDescription: string = '';
- exceptionScreenshotUrl: string | null = null;
- exceptionHistories: ExceptionHistory[] = [];
- isSubmittingFeedback: boolean = false;
- selectedScreenshot: File | null = null;
- screenshotPreview: string | null = null;
- showExceptionForm: boolean = false;
-
- // 标签页相关
- activeTab: 'progress' | 'members' | 'files' = 'progress';
- tabs = [
- { id: 'progress', name: '项目进度' },
- { id: 'members', name: '项目成员' },
- { id: 'files', name: '项目文件' }
- ];
- // 标准化阶段(视图层映射)
- standardPhases: Array<'待分配' | '需求方案' | '项目执行' | '收尾验收' | '归档'> = ['待分配', '需求方案', '项目执行', '收尾验收', '归档'];
- // 文件上传(通用)
- acceptedFileTypes: string = '.doc,.docx,.pdf,.jpg,.jpeg,.png,.zip,.rar,.max,.obj';
- isUploadingFile: boolean = false;
- projectMembers: ProjectMember[] = [];
-
- // 项目文件数据
- projectFiles: ProjectFile[] = [];
-
- // 团队协作时间轴
- timelineEvents: TimelineEvent[] = [];
- // ============ 阶段图片上传状态(新增) ============
- allowedImageTypes: string = '.jpg,.jpeg,.png';
- // 增加审核状态reviewStatus与是否已同步synced标记(仅由组长操作)
- whiteModelImages: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
- softDecorImages: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
- renderLargeImages: Array<{ id: string; name: string; url: string; size?: string; reviewStatus?: 'pending' | 'approved' | 'rejected'; synced?: boolean }> = [];
- showRenderUploadModal: boolean = false;
- pendingRenderLargeItems: Array<{ id: string; name: string; url: string; file: File }> = [];
- // 视图上下文:根据路由前缀识别角色视角(客服/设计师/组长)
- private roleContext: 'customer-service' | 'designer' | 'team-leader' = 'designer';
- constructor(
- private route: ActivatedRoute,
- private projectService: ProjectService,
- private router: Router,
- private fb: FormBuilder,
- ) {}
- // 切换标签页
- switchTab(tabId: 'progress' | 'members' | 'files') {
- this.activeTab = tabId;
- }
- // 类型安全的标签页检查方法
- isActiveTab(tabId: 'progress' | 'members' | 'files'): boolean {
- return this.activeTab === tabId;
- }
- // 根据事件类型获取作者名称
- getEventAuthor(action: string): string {
- // 根据不同的action类型返回对应的作者名称
- switch(action) {
- case '完成':
- case '更新':
- case '优化':
- return '李设计师';
- case '收到':
- return '李客服';
- case '提交':
- return '赵建模师';
- default:
- return '王组长';
- }
- }
- // 切换项目
- switchProject(projectId: string): void {
- this.projectId = projectId;
- this.loadProjectData();
- this.loadProjectMembers();
- this.loadProjectFiles();
- this.loadTimelineEvents();
- // 更新URL但不触发组件重载
- this.router.navigate([], { relativeTo: this.route, queryParamsHandling: 'merge', queryParams: { id: projectId } });
- }
- // 返回工作台
- backToWorkbench(): void {
- this.router.navigate(['/designer/dashboard']);
- }
-
- // 检查阶段是否已完成
- isStageCompleted(stage: ProjectStage): boolean {
- if (!this.project) return false;
-
- // 定义阶段顺序
- const stageOrder = [
- '订单创建', '需求沟通', '方案确认', '建模', '软装',
- '渲染', '后期', '尾款结算', '客户评价', '投诉处理'
- ];
-
- // 获取当前阶段和检查阶段的索引
- const currentStageIndex = stageOrder.indexOf(this.project.currentStage);
- const checkStageIndex = stageOrder.indexOf(stage);
-
- // 如果检查阶段在当前阶段之前,则已完成
- return checkStageIndex < currentStageIndex;
- }
- // 获取阶段状态:completed/active/pending
- getStageStatus(stage: ProjectStage): 'completed' | 'active' | 'pending' {
- const order = this.stageOrder;
- const current = this.project?.currentStage as ProjectStage | undefined;
- const currentIdx = current ? order.indexOf(current) : -1;
- const idx = order.indexOf(stage);
- if (idx === -1) return 'pending';
- if (currentIdx === -1) return 'pending';
- if (idx < currentIdx) return 'completed';
- if (idx === currentIdx) return 'active';
- return 'pending';
- }
- // 切换阶段展开/收起,并保持单展开
- toggleStage(stage: ProjectStage): void {
- // 已移除所有展开按钮,本方法保留以兼容模板其它引用,如无需可进一步删除调用点和方法
- const exclusivelyOpen = true;
- if (exclusivelyOpen) {
- Object.keys(this.expandedStages).forEach((key) => (this.expandedStages[key as ProjectStage] = false));
- this.expandedStages[stage] = true;
- } else {
- this.expandedStages[stage] = !this.expandedStages[stage];
- }
- }
- // 查看阶段详情(已不再通过按钮触发,保留以兼容日志或未来调用)
- viewStageDetails(stage: ProjectStage): void {
- // 以往这里有 alert/导航行为,现清空用户交互,避免误触
- return;
- }
- ngOnInit(): void {
- this.route.paramMap.subscribe(params => {
- this.projectId = params.get('id') || '';
- // 根据当前URL检测视图上下文
- this.roleContext = this.detectRoleContextFromUrl();
- this.loadProjectData();
- this.loadExceptionHistories();
- this.loadProjectMembers();
- this.loadProjectFiles();
- this.loadTimelineEvents();
- });
- // 新增:监听查询参数,支持通过 activeTab 设置初始标签页
- this.route.queryParamMap.subscribe(qp => {
- const raw = qp.get('activeTab');
- const alias: Record<string, 'progress' | 'members' | 'files'> = {
- requirements: 'progress',
- overview: 'progress'
- };
- const tab = raw && (raw in alias ? alias[raw] : raw);
- if (tab === 'progress' || tab === 'members' || tab === 'files') {
- this.activeTab = tab;
- }
- });
-
- // 添加点击事件监听器,当点击页面其他位置时关闭下拉菜单
- document.addEventListener('click', this.closeDropdownOnClickOutside);
-
- // 初始化客户表单(与客服端保持一致)
- this.customerForm = this.fb.group({
- name: ['', Validators.required],
- phone: ['', [Validators.required, Validators.pattern(/^1[3-9]\d{9}$/)]],
- wechat: [''],
- customerType: ['新客户'],
- source: [''],
- remark: [''],
- demandType: [''],
- followUpStatus: ['']
- });
-
- // 自动生成下单时间
- this.orderTime = new Date().toLocaleString('zh-CN', {
- year: 'numeric', month: '2-digit', day: '2-digit',
- hour: '2-digit', minute: '2-digit', second: '2-digit'
- });
- }
- // 在组件销毁时移除事件监听器和清理资源
- ngOnDestroy(): void {
- if (this.countdownInterval) {
- clearInterval(this.countdownInterval);
- }
- document.removeEventListener('click', this.closeDropdownOnClickOutside);
- // 释放所有 blob 预览 URL
- const revokeList: string[] = [];
- this.whiteModelImages.forEach(i => { if (i.url.startsWith('blob:')) revokeList.push(i.url); });
- this.softDecorImages.forEach(i => { if (i.url.startsWith('blob:')) revokeList.push(i.url); });
- this.renderLargeImages.forEach(i => { if (i.url.startsWith('blob:')) revokeList.push(i.url); });
- this.pendingRenderLargeItems.forEach(i => { if (i.url.startsWith('blob:')) revokeList.push(i.url); });
- revokeList.forEach(u => URL.revokeObjectURL(u));
- }
- // ============ 角色视图与只读控制(新增) ============
- private detectRoleContextFromUrl(): 'customer-service' | 'designer' | 'team-leader' {
- const url = this.router.url || '';
- if (url.includes('/customer-service/')) return 'customer-service';
- if (url.includes('/team-leader/')) return 'team-leader';
- return 'designer';
- }
- isDesignerView(): boolean { return this.roleContext === 'designer'; }
- isTeamLeaderView(): boolean { return this.roleContext === 'team-leader'; }
- isCustomerServiceView(): boolean { return this.roleContext === 'customer-service'; }
- // 只读规则:客服视角为只读
- isReadOnly(): boolean { return this.isCustomerServiceView(); }
- // 计算当前激活板块:优先用户点击的 expandedSection;否则取当前阶段所属板块;再否则回退首个板块
- private getActiveSectionKey(): SectionKey {
- if (this.expandedSection) return this.expandedSection;
- const current = this.project?.currentStage as ProjectStage | undefined;
- return current ? this.getSectionKeyForStage(current) : this.sections[0].key;
- }
- // 返回当前板块的全部阶段(所有角色一致):
- // 设计师也可查看 订单创建/确认需求/售后 板块内容
- getVisibleStages(): ProjectStage[] {
- const activeKey = this.getActiveSectionKey();
- const sec = this.sections.find(s => s.key === activeKey);
- return sec ? sec.stages : [];
- }
- // ============ 组长:同步上传与审核(新增,模拟实现) ============
- syncUploadedImages(phase: 'white' | 'soft' | 'render'): void {
- if (!this.isTeamLeaderView()) return;
- const markSynced = (arr: Array<{ reviewStatus?: 'pending'|'approved'|'rejected'; synced?: boolean }>) => {
- arr.forEach(img => {
- if (!img.synced) img.synced = true;
- if (!img.reviewStatus) img.reviewStatus = 'pending';
- });
- };
- if (phase === 'white') markSynced(this.whiteModelImages);
- if (phase === 'soft') markSynced(this.softDecorImages);
- if (phase === 'render') markSynced(this.renderLargeImages);
- alert('已同步该阶段的图片信息(模拟)');
- }
- reviewImage(imageId: string, phase: 'white' | 'soft' | 'render', status: 'approved' | 'rejected'): void {
- if (!this.isTeamLeaderView()) return;
- const setStatus = (arr: Array<{ id: string; reviewStatus?: 'pending'|'approved'|'rejected'; synced?: boolean }>) => {
- const target = arr.find(i => i.id === imageId);
- if (target) {
- target.reviewStatus = status;
- if (!target.synced) target.synced = true; // 审核时自动视为已同步
- }
- };
- if (phase === 'white') setStatus(this.whiteModelImages);
- if (phase === 'soft') setStatus(this.softDecorImages);
- if (phase === 'render') setStatus(this.renderLargeImages);
- }
- getImageReviewStatusText(img: { reviewStatus?: 'pending'|'approved'|'rejected'; synced?: boolean }): string {
- const synced = img.synced ? '已同步' : '未同步';
- const map: Record<string, string> = {
- 'pending': '待审',
- 'approved': '已通过',
- 'rejected': '已驳回'
- };
- const st = img.reviewStatus ? map[img.reviewStatus] : '未标记';
- return `${st} · ${synced}`;
- }
- // 点击页面其他位置时关闭下拉菜单
- private closeDropdownOnClickOutside = (event: MouseEvent): void => {
- const targetElement = event.target as HTMLElement;
- const projectSwitcher = targetElement.closest('.project-switcher');
-
- if (!projectSwitcher && this.showDropdown) {
- this.showDropdown = false;
- }
- };
- loadProjectData(): void {
- if (this.projectId) {
- this.loadProjectDetails();
- this.loadRenderProgress();
- this.loadModelCheckItems();
- this.loadCustomerFeedbacks();
- this.loadDesignerChanges();
- this.loadSettlements();
- this.loadRequirementChecklist();
- }
-
- // 初始化项目列表数据(模拟)
- this.projects = [
- { id: '1', name: '现代风格客厅设计', status: '进行中' },
- { id: '2', name: '北欧风卧室装修', status: '已完成' },
- { id: '3', name: '新中式书房改造', status: '进行中' },
- { id: '4', name: '工业风餐厅设计', status: '待处理' }
- ];
- }
-
- // 加载项目成员数据
- loadProjectMembers(): void {
- // 模拟API请求获取项目成员数据
- setTimeout(() => {
- this.projectMembers = [
- {
- id: '1',
- name: '李设计师',
- role: '主设计师',
- avatar: '李',
- skillMatch: 95,
- progress: 65,
- contribution: 75
- },
- {
- id: '2',
- name: '陈设计师',
- role: '助理设计师',
- avatar: '陈',
- skillMatch: 88,
- progress: 80,
- contribution: 60
- },
- {
- id: '3',
- name: '王组长',
- role: '项目组长',
- avatar: '王',
- skillMatch: 92,
- progress: 70,
- contribution: 70
- },
- {
- id: '4',
- name: '赵建模师',
- role: '3D建模师',
- avatar: '赵',
- skillMatch: 90,
- progress: 90,
- contribution: 85
- }
- ];
- }, 600);
- }
-
- // 加载项目文件数据
- loadProjectFiles(): void {
- // 模拟API请求获取项目文件数据
- setTimeout(() => {
- this.projectFiles = [
- {
- id: '1',
- name: '客厅设计方案V2.0.pdf',
- type: 'pdf',
- size: '2.5MB',
- date: '2024-02-10',
- url: '#'
- },
- {
- id: '2',
- name: '材质库集合.rar',
- type: 'rar',
- size: '45.8MB',
- date: '2024-02-08',
- url: '#'
- },
- {
- id: '3',
- name: '客厅渲染预览1.jpg',
- type: 'jpg',
- size: '3.2MB',
- date: '2024-02-14',
- url: '#'
- },
- {
- id: '4',
- name: '3D模型文件.max',
- type: 'max',
- size: '87.5MB',
- date: '2024-02-12',
- url: '#'
- },
- {
- id: '5',
- name: '客户需求文档.docx',
- type: 'docx',
- size: '1.2MB',
- date: '2024-01-15',
- url: '#'
- },
- {
- id: '6',
- name: '客厅渲染预览2.jpg',
- type: 'jpg',
- size: '3.8MB',
- date: '2024-02-15',
- url: '#'
- }
- ];
- }, 700);
- }
-
- // 加载团队协作时间轴数据
- loadTimelineEvents(): void {
- // 模拟API请求获取时间轴数据
- setTimeout(() => {
- this.timelineEvents = [
- {
- id: '1',
- time: '2024-02-15 14:30',
- title: '渲染完成',
- action: '完成',
- description: '客厅主视角渲染已完成,等待客户确认'
- },
- {
- id: '2',
- time: '2024-02-14 10:15',
- title: '材质调整',
- action: '更新',
- description: '根据客户反馈调整了沙发和窗帘材质'
- },
- {
- id: '3',
- time: '2024-02-12 16:45',
- title: '模型优化',
- action: '优化',
- description: '优化了模型面数,提高渲染效率'
- },
- {
- id: '4',
- time: '2024-02-10 09:30',
- title: '客户反馈',
- action: '收到',
- description: '收到客户关于颜色和储物空间的反馈意见'
- },
- {
- id: '5',
- time: '2024-02-08 15:20',
- title: '模型提交',
- action: '提交',
- description: '完成3D模型搭建并提交审核'
- }
- ];
- }, 800);
- }
-
- // 加载历史反馈记录
- loadExceptionHistories(): void {
- this.projectService.getExceptionHistories(this.projectId).subscribe(histories => {
- this.exceptionHistories = histories;
- });
- }
- loadProjectDetails(): void {
- this.projectService.getProjectById(this.projectId).subscribe(project => {
- this.project = project;
- // 设置当前阶段
- if (project) {
- this.currentStage = project.currentStage || '';
- // 重置展开状态并默认展开当前阶段
- this.stageOrder.forEach(s => this.expandedStages[s] = false);
- if (this.stageOrder.includes(project.currentStage)) {
- this.expandedStages[project.currentStage] = true;
- }
- // 新增:根据当前阶段默认展开所属板块
- const currentSec = this.getSectionKeyForStage(project.currentStage as ProjectStage);
- this.expandedSection = currentSec;
- }
- // 检查技能匹配度
- this.checkSkillMismatch();
- });
- }
-
- // 整理项目详情
- organizeProject(): void {
- // 模拟整理项目逻辑
- alert('项目详情已整理');
- }
-
- // 检查当前阶段是否显示特定卡片
- shouldShowCard(cardType: string): boolean {
- // 改为始终显示:各阶段详情在看板下方就地展示,不再受当前阶段限制
- return true;
- }
- loadRenderProgress(): void {
- this.isLoadingRenderProgress = true;
- this.errorLoadingRenderProgress = false;
-
- // 模拟API加载过程
- setTimeout(() => {
- this.projectService.getRenderProgress(this.projectId).subscribe(progress => {
- this.renderProgress = progress;
- this.isLoadingRenderProgress = false;
-
- // 模拟API加载失败的情况
- if (!progress) {
- this.errorLoadingRenderProgress = true;
- // 通知技术组长
- this.notifyTeamLeader('render-failed');
- } else {
- // 检查是否需要显示超时预警
- this.checkRenderTimeout();
- }
- });
- }, 1000);
- }
- loadModelCheckItems(): void {
- this.projectService.getModelCheckItems().subscribe(items => {
- this.modelCheckItems = items;
- });
- }
- loadCustomerFeedbacks(): void {
- this.projectService.getCustomerFeedbacks().subscribe(feedbacks => {
- this.feedbacks = feedbacks.filter(f => f.projectId === this.projectId);
- // 为反馈添加分类标签
- this.tagCustomerFeedbacks();
- // 检查是否有需要处理的反馈并启动倒计时
- this.checkFeedbackTimeout();
- });
- }
- loadDesignerChanges(): void {
- // 在实际应用中,这里应该从服务中获取设计师变更记录
- // 这里使用模拟数据
- this.designerChanges = [
- {
- id: 'dc1',
- projectId: this.projectId,
- oldDesignerId: 'designer2',
- oldDesignerName: '设计师B',
- newDesignerId: 'designer1',
- newDesignerName: '设计师A',
- changeTime: new Date('2025-09-05'),
- acceptanceTime: new Date('2025-09-05'),
- historicalAchievements: ['完成初步建模', '确定色彩方案'],
- completedWorkload: 30
- }
- ];
- }
- loadSettlements(): void {
- this.projectService.getSettlements().subscribe(settlements => {
- this.settlements = settlements.filter(s => s.projectId === this.projectId);
- });
- }
- loadRequirementChecklist(): void {
- this.projectService.generateRequirementChecklist(this.projectId).subscribe(checklist => {
- this.requirementChecklist = checklist;
- });
- }
- updateModelCheckItem(itemId: string, isPassed: boolean): void {
- this.projectService.updateModelCheckItem(itemId, isPassed).subscribe(() => {
- this.loadModelCheckItems(); // 重新加载检查项
- });
- }
- updateFeedbackStatus(feedbackId: string, status: '处理中' | '已解决'): void {
- this.projectService.updateFeedbackStatus(feedbackId, status).subscribe(() => {
- this.loadCustomerFeedbacks(); // 重新加载反馈
- // 清除倒计时
- if (this.countdownInterval) {
- clearInterval(this.countdownInterval);
- this.feedbackTimeoutCountdown = 0;
- }
- });
- }
- updateProjectStage(stage: ProjectStage): void {
- if (this.project) {
- this.projectService.updateProjectStage(this.projectId, stage).subscribe(() => {
- this.loadProjectDetails(); // 重新加载项目详情
- });
- }
- }
- // 新增:根据给定阶段跳转到下一阶段
- advanceToNextStage(afterStage: ProjectStage): void {
- const idx = this.stageOrder.indexOf(afterStage);
- if (idx >= 0 && idx < this.stageOrder.length - 1) {
- const next = this.stageOrder[idx + 1];
- this.updateProjectStage(next);
- // 可选:更新展开状态,折叠当前、展开下一阶段,提升体验
- if (this.expandedStages[afterStage] !== undefined) this.expandedStages[afterStage] = false as any;
- if (this.expandedStages[next] !== undefined) this.expandedStages[next] = true as any;
- }
- }
- generateReminderMessage(): void {
- this.projectService.generateReminderMessage('stagnation').subscribe(message => {
- this.reminderMessage = message;
-
- // 3秒后自动清除提醒
- setTimeout(() => {
- this.reminderMessage = '';
- }, 3000);
- });
- }
- // ============ 新增:标准化阶段映射与紧急程度 ============
- // 计算距离截止日期的天数(向下取整)
- getDaysToDeadline(): number | null {
- if (!this.project?.deadline) return null;
- const now = new Date();
- const deadline = new Date(this.project.deadline);
- const diffMs = deadline.getTime() - now.getTime();
- return Math.floor(diffMs / (1000 * 60 * 60 * 24));
- }
- // 是否延期/临期/提示
- getUrgencyBadge(): 'overdue' | 'due_3' | 'due_7' | null {
- const d = this.getDaysToDeadline();
- if (d === null) return null;
- if (d < 0) return 'overdue';
- if (d <= 3) return 'due_3';
- if (d <= 7) return 'due_7';
- return null;
- }
- // 是否存在不满意或待处理投诉/反馈
- hasPendingComplaint(): boolean {
- return this.feedbacks.some(f => !f.isSatisfied || f.status === '待处理');
- }
- // 将现有细分阶段映射为标准化阶段
- mapToStandardPhase(stage: ProjectStage): '待分配' | '需求方案' | '项目执行' | '收尾验收' | '归档' {
- const mapping: Record<ProjectStage, '待分配' | '需求方案' | '项目执行' | '收尾验收' | '归档'> = {
- '订单创建': '待分配',
- '需求沟通': '需求方案',
- '方案确认': '需求方案',
- '建模': '项目执行',
- '软装': '项目执行',
- '渲染': '项目执行',
- '后期': '项目执行',
- '尾款结算': '收尾验收',
- '客户评价': '收尾验收',
- '投诉处理': '收尾验收'
- };
- return mapping[stage] ?? '待分配';
- }
- getStandardPhaseIndex(): number {
- if (!this.project?.currentStage) return 0;
- const phase = this.mapToStandardPhase(this.project.currentStage);
- return this.standardPhases.indexOf(phase);
- }
- isStandardPhaseCompleted(phase: '待分配' | '需求方案' | '项目执行' | '收尾验收' | '归档'): boolean {
- return this.standardPhases.indexOf(phase) < this.getStandardPhaseIndex();
- }
- isStandardPhaseCurrent(phase: '待分配' | '需求方案' | '项目执行' | '收尾验收' | '归档'): boolean {
- return this.standardPhases.indexOf(phase) === this.getStandardPhaseIndex();
- }
- // ============ 新增:项目报告导出 ============
- exportProjectReport(): void {
- if (!this.project) return;
- const lines: string[] = [];
- const d = this.getDaysToDeadline();
- lines.push(`项目名称: ${this.project.name}`);
- lines.push(`当前阶段(细分): ${this.project.currentStage}`);
- lines.push(`当前阶段(标准化): ${this.mapToStandardPhase(this.project.currentStage)}`);
- if (this.project.deadline) {
- lines.push(`截止日期: ${this.formatDate(this.project.deadline)}`);
- lines.push(`剩余天数: ${d !== null ? d : '-'}天`);
- }
- lines.push(`技能需求: ${(this.project.skillsRequired || []).join('、')}`);
- lines.push('—— 渲染进度 ——');
- lines.push(this.renderProgress ? `状态: ${this.renderProgress.status}, 完成度: ${this.renderProgress.completionRate}%` : '无渲染进度数据');
- lines.push('—— 客户反馈 ——');
- lines.push(this.feedbacks.length ? `${this.feedbacks.length} 条` : '暂无');
- lines.push('—— 设计师变更 ——');
- lines.push(this.designerChanges.length ? `${this.designerChanges.length} 条` : '暂无');
- lines.push('—— 交付文件 ——');
- lines.push(this.projectFiles.length ? this.projectFiles.map(f => `• ${f.name} (${f.type}, ${f.size})`).join('\n') : '暂无');
- const content = lines.join('\n');
- const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `${this.project.name || '项目'}-阶段报告.txt`;
- a.click();
- URL.revokeObjectURL(url);
- }
- // ============ 新增:通用文件上传(含4K图片校验) ============
- async onGeneralFilesSelected(event: Event): Promise<void> {
- const input = event.target as HTMLInputElement;
- if (!input.files || input.files.length === 0) return;
- const files = Array.from(input.files);
- this.isUploadingFile = true;
- for (const file of files) {
- // 对图片进行4K校验(最大边 >= 4000px)
- if (/\.(jpg|jpeg|png)$/i.test(file.name)) {
- const ok = await this.validateImage4K(file).catch(() => false);
- if (!ok) {
- alert(`图片不符合4K标准(最大边需≥4000像素):${file.name}`);
- continue;
- }
- }
- // 简化:直接追加到本地列表(实际应上传到服务器)
- const fakeType = (file.name.split('.').pop() || '').toLowerCase();
- const sizeMB = (file.size / (1024 * 1024)).toFixed(1) + 'MB';
- const nowStr = this.formatDate(new Date());
- this.projectFiles.unshift({
- id: `file-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
- name: file.name,
- type: fakeType,
- size: sizeMB,
- date: nowStr,
- url: '#'
- });
- }
- this.isUploadingFile = false;
- // 清空选择
- input.value = '';
- }
- validateImage4K(file: File): Promise<boolean> {
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.onload = () => {
- const img = new Image();
- img.onload = () => {
- const maxSide = Math.max(img.width, img.height);
- resolve(maxSide >= 4000);
- };
- img.onerror = () => reject('image load error');
- img.src = reader.result as string;
- };
- reader.onerror = () => reject('read error');
- reader.readAsDataURL(file);
- });
- }
- // 可选:列表 trackBy,优化渲染
- trackById(_: number, item: { id: string }): string { return item.id; }
- retryLoadRenderProgress(): void {
- this.loadRenderProgress();
- }
- // 检查是否所有模型检查项都已通过
- areAllModelChecksPassed(): boolean {
- return this.modelCheckItems.every(item => item.isPassed);
- }
- // 获取技能匹配度警告
- getSkillMismatchWarning(): string | null {
- if (!this.project) return null;
-
- // 模拟技能匹配度检查
- const designerSkills = ['现代风格', '硬装'];
- const requiredSkills = this.project.skillsRequired;
-
- const mismatchedSkills = requiredSkills.filter(skill => !designerSkills.includes(skill));
-
- if (mismatchedSkills.length > 0) {
- return `警告:您不擅长${mismatchedSkills.join('、')},建议联系组长协调`;
- }
-
- return null;
- }
- // 检查渲染是否超时
- checkRenderTimeout(): void {
- if (!this.renderProgress || !this.project) return;
-
- // 模拟交付前3小时预警
- const deliveryTime = new Date(this.project.deadline);
- const currentTime = new Date();
- const timeDifference = deliveryTime.getTime() - currentTime.getTime();
- const hoursRemaining = Math.floor(timeDifference / (1000 * 60 * 60));
-
- if (hoursRemaining <= 3 && hoursRemaining > 0) {
- // 弹窗预警
- alert('渲染进度预警:交付前3小时,请关注渲染进度');
- }
-
- if (hoursRemaining <= 1 && hoursRemaining > 0) {
- // 更严重的预警
- alert('渲染进度严重预警:交付前1小时,渲染可能无法按时完成!');
- }
- }
- // 为客户反馈添加分类标签
- tagCustomerFeedbacks(): void {
- this.feedbacks.forEach(feedback => {
- // 添加分类标签
- if (feedback.content.includes('色彩') || feedback.problemLocation?.includes('色彩')) {
- (feedback as any).tag = '色彩问题';
- } else if (feedback.content.includes('家具') || feedback.problemLocation?.includes('家具')) {
- (feedback as any).tag = '家具款式问题';
- } else if (feedback.content.includes('光线') || feedback.content.includes('照明')) {
- (feedback as any).tag = '光线问题';
- } else {
- (feedback as any).tag = '其他问题';
- }
- });
- }
-
- // 获取反馈标签的辅助方法
- getFeedbackTag(feedback: CustomerFeedback): string {
- return (feedback as any).tag || '';
- }
- // 检查反馈超时
- checkFeedbackTimeout(): void {
- const pendingFeedbacks = this.feedbacks.filter(f => f.status === '待处理');
- if (pendingFeedbacks.length > 0) {
- // 启动1小时倒计时
- this.feedbackTimeoutCountdown = 3600; // 3600秒 = 1小时
- this.startCountdown();
- }
- }
- // 启动倒计时
- startCountdown(): void {
- this.countdownInterval = setInterval(() => {
- if (this.feedbackTimeoutCountdown > 0) {
- this.feedbackTimeoutCountdown--;
- } else {
- clearInterval(this.countdownInterval);
- // 超时提醒
- alert('客户反馈已超过1小时未响应,请立即处理!');
- this.notifyTeamLeader('feedback-overdue');
- }
- }, 1000);
- }
- // 通知技术组长
- notifyTeamLeader(type: 'render-failed' | 'feedback-overdue' | 'skill-mismatch'): void {
- // 实际应用中应调用消息服务通知组长
- console.log(`通知技术组长:${type} - 项目ID: ${this.projectId}`);
- }
- // 检查技能匹配度并提示
- checkSkillMismatch(): void {
- const warning = this.getSkillMismatchWarning();
- if (warning) {
- // 显示技能不匹配警告
- if (confirm(`${warning}\n是否联系技术组长协调支持?`)) {
- this.notifyTeamLeader('skill-mismatch');
- }
- }
- }
- // 发起设计师变更
- initiateDesignerChange(reason: string): void {
- // 实际应用中应调用API发起变更
- console.log(`发起设计师变更,原因:${reason}`);
- alert('已发起设计师变更申请,请等待新设计师承接');
- }
- // 确认承接变更项目
- acceptDesignerChange(changeId: string): void {
- // 实际应用中应调用API确认承接
- console.log(`确认承接设计师变更:${changeId}`);
- alert('已确认承接项目,系统已记录时间戳和责任人');
- }
- // 格式化倒计时显示
- formatCountdown(seconds: number): string {
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
- const remainingSeconds = seconds % 60;
- return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
- }
- // 提交异常反馈
- submitExceptionFeedback(): void {
- if (!this.exceptionDescription.trim() || this.isSubmittingFeedback) {
- alert('请填写异常类型和描述');
- return;
- }
- this.isSubmittingFeedback = true;
-
- // 模拟提交反馈到服务器
- setTimeout(() => {
- const newException: ExceptionHistory = {
- id: `exception-${Date.now()}`,
- type: this.exceptionType,
- description: this.exceptionDescription,
- submitTime: new Date(),
- status: '待处理'
- };
- // 添加到历史记录中
- this.exceptionHistories.unshift(newException);
-
- // 通知客服和技术支持
- this.notifyTechnicalSupport(newException);
-
- // 清空表单
- this.exceptionDescription = '';
- this.clearExceptionScreenshot();
- this.showExceptionForm = false;
-
- // 显示成功消息
- alert('异常反馈已提交,技术支持将尽快处理');
-
- this.isSubmittingFeedback = false;
- }, 1000);
- }
- // 上传异常截图
- uploadExceptionScreenshot(event: Event): void {
- const input = event.target as HTMLInputElement;
- if (input.files && input.files[0]) {
- const file = input.files[0];
- // 在实际应用中,这里应该上传文件到服务器
- // 这里我们使用FileReader来生成一个预览URL
- const reader = new FileReader();
- reader.onload = (e) => {
- this.exceptionScreenshotUrl = e.target?.result as string;
- };
- reader.readAsDataURL(file);
- }
- }
- // 清除异常截图
- clearExceptionScreenshot(): void {
- this.exceptionScreenshotUrl = null;
- const input = document.getElementById('screenshot-upload') as HTMLInputElement;
- if (input) {
- input.value = '';
- }
- }
- // 联系组长
- contactTeamLeader() {
- alert(`已联系${this.project?.assigneeName || '项目组长'}`);
- }
- // 处理渲染超时预警
- handleRenderTimeout() {
- alert('已发送渲染超时预警通知');
- }
- // 通知技术支持
- notifyTechnicalSupport(exception: ExceptionHistory): void {
- // 实际应用中应调用消息服务通知技术支持和客服
- console.log(`通知技术支持和客服:渲染异常 - 项目ID: ${this.projectId}`);
- console.log(`异常类型: ${this.getExceptionTypeText(exception.type)}, 描述: ${exception.description}`);
- }
- // 获取异常类型文本
- getExceptionTypeText(type: string): string {
- const typeMap: Record<string, string> = {
- 'failed': '渲染失败',
- 'stuck': '渲染卡顿',
- 'quality': '渲染质量问题',
- 'other': '其他问题'
- };
- return typeMap[type] || type;
- }
- // 格式化日期
- formatDate(date: Date | string): string {
- const d = typeof date === 'string' ? new Date(date) : date;
- const year = d.getFullYear();
- const month = String(d.getMonth() + 1).padStart(2, '0');
- const day = String(d.getDate()).padStart(2, '0');
- const hours = String(d.getHours()).padStart(2, '0');
- const minutes = String(d.getMinutes()).padStart(2, '0');
- return `${year}-${month}-${day} ${hours}:${minutes}`;
- }
- // 将字节格式化为易读尺寸
- private formatFileSize(bytes: number): string {
- if (bytes < 1024) return `${bytes}B`;
- const kb = bytes / 1024;
- if (kb < 1024) return `${kb.toFixed(1)}KB`;
- const mb = kb / 1024;
- if (mb < 1024) return `${mb.toFixed(1)}MB`;
- const gb = mb / 1024;
- return `${gb.toFixed(2)}GB`;
- }
- // 生成缩略图条目(并创建本地预览URL)
- private makeImageItem(file: File): { id: string; name: string; url: string; size: string } {
- const id = `img-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
- const url = URL.createObjectURL(file);
- return { id, name: file.name, url, size: this.formatFileSize(file.size) };
- }
- // 释放对象URL
- private revokeUrl(url: string): void {
- try { if (url && url.startsWith('blob:')) URL.revokeObjectURL(url); } catch {}
- }
- // =========== 建模阶段:白模上传 ===========
- onWhiteModelSelected(event: Event): void {
- const input = event.target as HTMLInputElement;
- if (!input.files || input.files.length === 0) return;
- const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
- const items = files.map(f => this.makeImageItem(f));
- this.whiteModelImages.unshift(...items);
- input.value = '';
- }
- removeWhiteModelImage(id: string): void {
- const target = this.whiteModelImages.find(i => i.id === id);
- if (target) this.revokeUrl(target.url);
- this.whiteModelImages = this.whiteModelImages.filter(i => i.id !== id);
- }
- // 新增:建模阶段 确认上传并自动进入下一阶段(软装)
- confirmWhiteModelUpload(): void {
- if (this.whiteModelImages.length === 0) return;
- this.advanceToNextStage('建模');
- }
- // =========== 软装阶段:小图上传(建议≤1MB,不强制) ===========
- onSoftDecorSmallPicsSelected(event: Event): void {
- const input = event.target as HTMLInputElement;
- if (!input.files || input.files.length === 0) return;
- const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
- const warnOversize = files.filter(f => f.size > 1024 * 1024);
- if (warnOversize.length > 0) {
- // 仅提示,不阻断
- console.warn('软装小图建议≤1MB,以下文件较大:', warnOversize.map(f => f.name));
- }
- const items = files.map(f => this.makeImageItem(f));
- this.softDecorImages.unshift(...items);
- input.value = '';
- }
- // 拖拽上传相关属性
- isDragOver: boolean = false;
- // 图片预览相关属性
- showImagePreview: boolean = false;
- previewImageData: any = null;
- // 图片预览方法
- previewImage(img: any): void {
- this.previewImageData = img;
- this.showImagePreview = true;
- }
- closeImagePreview(): void {
- this.showImagePreview = false;
- this.previewImageData = null;
- }
- downloadImage(img: any): void {
- if (img) {
- const link = document.createElement('a');
- link.href = img.url;
- link.download = img.name;
- link.click();
- }
- }
- removeImageFromPreview(): void {
- if (this.previewImageData) {
- // 根据图片类型调用相应的删除方法
- if (this.whiteModelImages.find(i => i.id === this.previewImageData.id)) {
- this.removeWhiteModelImage(this.previewImageData.id);
- } else if (this.softDecorImages.find(i => i.id === this.previewImageData.id)) {
- this.removeSoftDecorImage(this.previewImageData.id);
- } else if (this.renderLargeImages.find(i => i.id === this.previewImageData.id)) {
- this.removeRenderLargeImage(this.previewImageData.id);
- }
- this.closeImagePreview();
- }
- }
- // 拖拽事件处理
- onDragOver(event: DragEvent): void {
- event.preventDefault();
- event.stopPropagation();
- this.isDragOver = true;
- }
- onDragLeave(event: DragEvent): void {
- event.preventDefault();
- event.stopPropagation();
- this.isDragOver = false;
- }
- onFileDrop(event: DragEvent, type: 'whiteModel' | 'softDecor' | 'render'): void {
- event.preventDefault();
- event.stopPropagation();
- this.isDragOver = false;
- const files = event.dataTransfer?.files;
- if (!files || files.length === 0) return;
- // 创建模拟的input事件
- const mockEvent = {
- target: {
- files: files
- }
- } as any;
- // 根据类型调用相应的处理方法
- switch (type) {
- case 'whiteModel':
- this.onWhiteModelSelected(mockEvent);
- break;
- case 'softDecor':
- this.onSoftDecorSmallPicsSelected(mockEvent);
- break;
- case 'render':
- this.onRenderLargePicsSelected(mockEvent);
- break;
- }
- }
- // 触发文件输入框
- triggerFileInput(type: 'whiteModel' | 'softDecor' | 'render'): void {
- let inputId: string;
- switch (type) {
- case 'whiteModel':
- inputId = 'whiteModelFileInput';
- break;
- case 'softDecor':
- inputId = 'softDecorFileInput';
- break;
- case 'render':
- inputId = 'renderFileInput';
- break;
- }
- const input = document.querySelector(`#${inputId}`) as HTMLInputElement;
- if (input) {
- input.click();
- }
- }
- removeSoftDecorImage(id: string): void {
- const target = this.softDecorImages.find(i => i.id === id);
- if (target) this.revokeUrl(target.url);
- this.softDecorImages = this.softDecorImages.filter(i => i.id !== id);
- }
- // 新增:软装阶段 确认上传并自动进入下一阶段(渲染)
- confirmSoftDecorUpload(): void {
- if (this.softDecorImages.length === 0) return;
- this.advanceToNextStage('软装');
- }
- // =========== 渲染阶段:大图上传(弹窗 + 4K校验) ===========
- openRenderUploadModal(): void {
- this.showRenderUploadModal = true;
- this.pendingRenderLargeItems = [];
- }
-
- closeRenderUploadModal(): void {
- // 关闭时释放临时预览URL
- this.pendingRenderLargeItems.forEach(i => this.revokeUrl(i.url));
- this.pendingRenderLargeItems = [];
- this.showRenderUploadModal = false;
- }
-
- async onRenderLargePicsSelected(event: Event): Promise<void> {
- const input = event.target as HTMLInputElement;
- if (!input.files || input.files.length === 0) return;
- const files = Array.from(input.files).filter(f => /\.(jpg|jpeg|png)$/i.test(f.name));
- for (const f of files) {
- const ok = await this.validateImage4K(f).catch(() => false);
- if (!ok) {
- alert(`图片不符合4K标准(最大边需≥4000像素):${f.name}`);
- continue;
- }
- const item = this.makeImageItem(f);
- // 直接添加到正式列表,不再使用待确认列表
- this.renderLargeImages.unshift({
- id: item.id,
- name: item.name,
- url: item.url,
- size: this.formatFileSize(f.size)
- });
- }
- input.value = '';
- }
-
- confirmRenderUpload(): void {
- // 将待确认的图片加入正式列表(此处模拟上传成功)
- const toAdd = this.pendingRenderLargeItems.map(i => ({ id: i.id, name: i.name, url: i.url, size: this.formatFileSize(i.file.size) }));
- this.renderLargeImages.unshift(...toAdd);
- this.closeRenderUploadModal();
- // 新增:渲染阶段确认后,自动进入下一阶段(后期)
- this.advanceToNextStage('渲染');
- }
-
- removeRenderLargeImage(id: string): void {
- const target = this.renderLargeImages.find(i => i.id === id);
- if (target) this.revokeUrl(target.url);
- this.renderLargeImages = this.renderLargeImages.filter(i => i.id !== id);
- }
- // 根据阶段映射所属板块
- getSectionKeyForStage(stage: ProjectStage): SectionKey {
- switch (stage) {
- case '订单创建':
- return 'order';
- case '需求沟通':
- case '方案确认':
- return 'requirements';
- case '建模':
- case '软装':
- case '渲染':
- return 'delivery';
- case '尾款结算':
- case '客户评价':
- case '投诉处理':
- return 'aftercare';
- default:
- return 'order';
- }
- }
- // 获取板块状态:completed | 'active' | 'pending'
- getSectionStatus(key: SectionKey): 'completed' | 'active' | 'pending' {
- const current = this.project?.currentStage as ProjectStage | undefined;
- if (!current) return 'pending';
- const currentSection = this.getSectionKeyForStage(current);
- const sectionOrder = this.sections.map(s => s.key);
- const currentIdx = sectionOrder.indexOf(currentSection);
- const idx = sectionOrder.indexOf(key);
- if (idx === -1 || currentIdx === -1) return 'pending';
- if (idx < currentIdx) return 'completed';
- if (idx === currentIdx) return 'active';
- return 'pending';
- }
- // 切换四大板块(单展开)
- toggleSection(key: SectionKey): void {
- this.expandedSection = key;
- // 点击板块按钮时,滚动到该板块的第一个可见阶段卡片
- const sec = this.sections.find(s => s.key === key);
- if (sec) {
- // 设计师仅滚动到可见的三大执行阶段,否则取该板块第一个阶段
- const candidate = this.isDesignerView()
- ? sec.stages.find(st => ['建模', '软装', '渲染'].includes(st)) || sec.stages[0]
- : sec.stages[0];
- this.scrollToStage(candidate);
- }
- }
- // 阶段到锚点的映射
- stageToAnchor(stage: ProjectStage): string {
- const map: Record<ProjectStage, string> = {
- '订单创建': 'order',
- '需求沟通': 'requirements-talk',
- '方案确认': 'proposal-confirm',
- '建模': 'modeling',
- '软装': 'softdecor',
- '渲染': 'render',
- '后期': 'aftercare',
- '尾款结算': 'settlement',
- '客户评价': 'customer-review',
- '投诉处理': 'complaint'
- };
- return `stage-${map[stage] || 'unknown'}`;
- }
- // 平滑滚动到指定阶段卡片
- scrollToStage(stage: ProjectStage): void {
- const anchor = this.stageToAnchor(stage);
- const el = document.getElementById(anchor);
- if (el) {
- el.scrollIntoView({ behavior: 'smooth', block: 'start' });
- }
- }
- // 订单创建阶段:客户信息(迁移自客服端"客户信息"卡片)
- orderCreationMethod: 'miniprogram' | 'manual' = 'miniprogram';
- isSyncing: boolean = false;
- orderTime: string = '';
- customerForm!: FormGroup;
- customerSearchKeyword: string = '';
- customerSearchResults: Array<{ id: string; name: string; phone: string; wechat?: string; avatar?: string; customerType?: string; source?: string; remark?: string }> = [];
- selectedOrderCustomer: { id: string; name: string; phone: string; wechat?: string; avatar?: string; customerType?: string; source?: string; remark?: string } | null = null;
- demandTypes = [
- { value: 'price', label: '价格敏感' },
- { value: 'quality', label: '质量敏感' },
- { value: 'comprehensive', label: '综合要求' }
- ];
-
- followUpStatus = [
- { value: 'quotation', label: '待报价' },
- { value: 'confirm', label: '待确认需求' },
- { value: 'lost', label: '已失联' }
- ];
- // 需求关键信息同步数据
- requirementKeyInfo = {
- colorAtmosphere: {
- description: '',
- mainColor: '',
- colorTemp: '',
- materials: [] as string[]
- },
- spaceStructure: {
- lineRatio: 0,
- blankRatio: 0,
- flowWidth: 0,
- aspectRatio: 0,
- ceilingHeight: 0
- },
- materialWeights: {
- fabricRatio: 0,
- woodRatio: 0,
- metalRatio: 0,
- smoothness: 0,
- glossiness: 0
- },
- presetAtmosphere: {
- name: '',
- rgb: '',
- colorTemp: '',
- materials: [] as string[]
- }
- };
- // 客户信息:搜索/选择/清空/同步/快速填写 逻辑
- searchCustomer(): void {
- if (this.customerSearchKeyword.trim().length >= 2) {
- this.customerSearchResults = [
- { id: '1', name: '张先生', phone: '138****5678', customerType: '老客户', source: '官网咨询', avatar: "data:image/svg+xml,%3Csvg width='64' height='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='%23E6E6E6'/%3E%3Ctext x='50%25' y='50%25' font-family='Arial' font-size='13.333333333333334' font-weight='bold' text-anchor='middle' fill='%23555555' dy='0.3em'%3EIMG%3C/text%3E%3C/svg%3E" },
- { id: '2', name: '李女士', phone: '139****1234', customerType: 'VIP客户', source: '推荐介绍', avatar: "data:image/svg+xml,%3Csvg width='65' height='40' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='100%25' height='100%25' fill='%23DCDCDC'/%3E%3Ctext x='50%25' y='50%25' font-family='Arial' font-size='13.333333333333334' font-weight='bold' text-anchor='middle' fill='%23555555' dy='0.3em'%3EIMG%3C/text%3E%3C/svg%3E" }
- ];
- } else {
- this.customerSearchResults = [];
- }
- }
- selectCustomer(customer: { id: string; name: string; phone: string; wechat?: string; avatar?: string; customerType?: string; source?: string; remark?: string }): void {
- this.selectedOrderCustomer = customer;
- this.customerForm.patchValue({
- name: customer.name,
- phone: customer.phone,
- wechat: customer.wechat || '',
- customerType: customer.customerType || '新客户',
- source: customer.source || '',
- remark: customer.remark || ''
- });
- this.customerSearchResults = [];
- this.customerSearchKeyword = '';
- }
- clearSelectedCustomer(): void {
- this.selectedOrderCustomer = null;
- this.customerForm.reset({ customerType: '新客户' });
- }
- quickFillCustomerInfo(keyword: string): void {
- const k = (keyword || '').trim();
- if (!k) return;
- // 模拟:若有搜索结果,选择第一条
- if (this.customerSearchResults.length === 0) this.searchCustomer();
- if (this.customerSearchResults.length > 0) {
- this.selectCustomer(this.customerSearchResults[0]);
- }
- }
- syncMiniprogramCustomerInfo(): void {
- if (this.isSyncing) return;
- this.isSyncing = true;
- setTimeout(() => {
- // 模拟从小程序同步到客户表单
- this.customerForm.patchValue({
- name: '小程序用户',
- phone: '13800001234',
- wechat: 'wx_user_001',
- customerType: '新客户',
- source: '小程序下单'
- });
- this.isSyncing = false;
- }, 1000);
- }
- downloadFile(file: ProjectFile): void {
- // 实现文件下载逻辑
- const link = document.createElement('a');
- link.href = file.url;
- link.download = file.name;
- link.click();
- }
- previewFile(file: ProjectFile): void {
- // 预览文件逻辑
- console.log('预览文件:', file.name);
- }
- // 同步需求关键信息到客户信息卡片
- syncRequirementKeyInfo(requirementData: any): void {
- if (requirementData) {
- // 同步色彩氛围信息
- if (requirementData.colorIndicators) {
- this.requirementKeyInfo.colorAtmosphere = {
- description: requirementData.colorIndicators.colorRange || '',
- mainColor: `rgb(${requirementData.colorIndicators.mainColor?.r || 0}, ${requirementData.colorIndicators.mainColor?.g || 0}, ${requirementData.colorIndicators.mainColor?.b || 0})`,
- colorTemp: `${requirementData.colorIndicators.colorTemperature || 0}K`,
- materials: []
- };
- }
- // 同步空间结构信息
- if (requirementData.spaceIndicators) {
- this.requirementKeyInfo.spaceStructure = {
- lineRatio: requirementData.spaceIndicators.lineRatio || 0,
- blankRatio: requirementData.spaceIndicators.blankRatio || 0,
- flowWidth: requirementData.spaceIndicators.flowWidth || 0,
- aspectRatio: requirementData.spaceIndicators.aspectRatio || 0,
- ceilingHeight: requirementData.spaceIndicators.ceilingHeight || 0
- };
- }
- // 同步材质权重信息
- if (requirementData.materialIndicators) {
- this.requirementKeyInfo.materialWeights = {
- fabricRatio: requirementData.materialIndicators.fabricRatio || 0,
- woodRatio: requirementData.materialIndicators.woodRatio || 0,
- metalRatio: requirementData.materialIndicators.metalRatio || 0,
- smoothness: requirementData.materialIndicators.smoothness || 0,
- glossiness: requirementData.materialIndicators.glossiness || 0
- };
- }
- // 同步预设氛围信息
- if (requirementData.selectedPresetAtmosphere) {
- this.requirementKeyInfo.presetAtmosphere = {
- name: requirementData.selectedPresetAtmosphere.name || '',
- rgb: requirementData.selectedPresetAtmosphere.rgb || '',
- colorTemp: requirementData.selectedPresetAtmosphere.colorTemp || '',
- materials: requirementData.selectedPresetAtmosphere.materials || []
- };
- }
- console.log('需求关键信息已同步:', this.requirementKeyInfo);
- } else {
- // 模拟数据用于演示
- this.requirementKeyInfo = {
- colorAtmosphere: {
- description: '温馨暖调',
- mainColor: 'rgb(255, 230, 180)',
- colorTemp: '2700K',
- materials: ['木质', '布艺']
- },
- spaceStructure: {
- lineRatio: 60,
- blankRatio: 30,
- flowWidth: 0.9,
- aspectRatio: 1.6,
- ceilingHeight: 2.8
- },
- materialWeights: {
- fabricRatio: 50,
- woodRatio: 30,
- metalRatio: 20,
- smoothness: 7,
- glossiness: 4
- },
- presetAtmosphere: {
- name: '现代简约',
- rgb: '200,220,240',
- colorTemp: '5000K',
- materials: ['金属', '玻璃']
- }
- };
- }
- }
- // 获取同步的关键信息摘要
- getRequirementSummary(): string[] {
- const summary: string[] = [];
-
- if (this.requirementKeyInfo.colorAtmosphere.description) {
- summary.push(`色彩氛围: ${this.requirementKeyInfo.colorAtmosphere.description}`);
- }
-
- if (this.requirementKeyInfo.spaceStructure.aspectRatio > 0) {
- summary.push(`空间比例: ${this.requirementKeyInfo.spaceStructure.aspectRatio.toFixed(1)}`);
- }
-
- if (this.requirementKeyInfo.materialWeights.woodRatio > 0) {
- summary.push(`木质占比: ${this.requirementKeyInfo.materialWeights.woodRatio}%`);
- }
-
- if (this.requirementKeyInfo.presetAtmosphere.name) {
- summary.push(`预设氛围: ${this.requirementKeyInfo.presetAtmosphere.name}`);
- }
-
- return summary;
- }
- // 处理咨询订单表单提交
- onConsultationOrderSubmit(formData: any): void {
- console.log('咨询订单表单提交:', formData);
- // 这里可以添加处理表单提交的逻辑
- // 例如:保存订单信息、更新项目状态等
- }
- }
|