import { AfterViewInit, Component, OnInit } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RouterLink } from '@angular/router'; import { gsap } from 'gsap'; import { ParseApiService, ParseTicket } from '../../core/parse-api.service'; import { ImagePreviewWrapperComponent } from '../../react-wrappers/image-preview-wrapper.component'; import { RatingWrapperComponent } from '../../react-wrappers/rating-wrapper.component'; import { PreviewImage } from '../../../react-components/ImagePreview'; import { MetricCardComponent } from '../../shared/metric-card.component'; import { StatusBadgeComponent } from '../../shared/status-badge.component'; type TicketStatus = 'pending' | 'processing' | 'confirming' | 'resolved' | 'closed'; @Component({ selector: 'app-tickets', standalone: true, imports: [FormsModule, RouterLink, ImagePreviewWrapperComponent, RatingWrapperComponent, MetricCardComponent, StatusBadgeComponent], templateUrl: './tickets.component.html', styleUrl: './tickets.component.scss' }) export class TicketsComponent implements OnInit, AfterViewInit { tickets: ParseTicket[] = []; newTicket = { title: '', category: '软件问题', priority: 'high', impactScope: '部门', description: '' }; selected: ParseTicket | null = null; editingTitle = ''; searchKeyword = ''; statusFilter = 'all'; priorityFilter = 'all'; loading = false; message = ''; score = 4; previewVisible = false; images: PreviewImage[] = [ { id: 'img-1', name: '客户报错截图.png', url: 'https://dummyimage.com/960x540/e5e7eb/111827&text=Ticket+Screenshot' }, { id: 'img-2', name: '控制台日志.png', url: 'https://dummyimage.com/960x540/dbeafe/1d4ed8&text=Console+Log' } ]; readonly lifecycle: TicketStatus[] = ['pending', 'processing', 'confirming', 'resolved']; constructor(readonly parseApi: ParseApiService) {} get visibleTicketCount(): string { return String(this.tickets.length); } get overdueTicketCount(): string { return String(this.tickets.filter((ticket) => ticket.isOverdue).length); } get resolvedCount(): string { return String(this.tickets.filter((ticket) => ticket.status === 'resolved').length); } get activeRiskCount(): string { return String(this.tickets.filter((ticket) => ticket.isOverdue || ticket.priority === 'urgent').length); } get filteredTickets(): ParseTicket[] { const keyword = this.searchKeyword.trim().toLowerCase(); return this.tickets.filter((ticket) => { const matchesKeyword = !keyword || ticket.title.toLowerCase().includes(keyword) || ticket.ticketNo.toLowerCase().includes(keyword) || ticket.companyName.toLowerCase().includes(keyword); const matchesStatus = this.statusFilter === 'all' || ticket.status === this.statusFilter; const matchesPriority = this.priorityFilter === 'all' || ticket.priority === this.priorityFilter; return matchesKeyword && matchesStatus && matchesPriority; }); } ngOnInit(): void { void this.loadTickets(); } ngAfterViewInit(): void { this.animateWorkbench(); } async loadTickets(): Promise { if (!this.parseApi.isLoggedIn) { this.message = '请先登录测试账号后查看按 ACL 隔离后的工单数据。'; return; } this.loading = true; this.message = '正在从 Parse 数据库读取当前用户可见工单...'; try { this.tickets = await this.parseApi.listTickets(); this.selected = this.tickets[0] ?? null; this.editingTitle = this.selected?.title ?? ''; this.message = `已加载 ${this.tickets.length} 条当前用户可见工单,筛选和状态流转均写回数据库。`; this.animateWorkbench(); } catch (error) { this.message = error instanceof Error ? error.message : '工单读取失败。'; } finally { this.loading = false; } } async createTicket(): Promise { if (!this.newTicket.title.trim()) { this.message = '工单标题不能为空。'; return; } try { await this.parseApi.createTicket({ title: this.newTicket.title, category: this.newTicket.category, priority: this.newTicket.priority, impactScope: this.newTicket.impactScope, description: this.newTicket.description || '由前端页面创建的测试工单。' }); this.newTicket = { title: '', category: '软件问题', priority: 'high', impactScope: '部门', description: '' }; await this.loadTickets(); this.message = '工单创建成功,已按当前用户 ACL 隔离。'; } catch (error) { this.message = error instanceof Error ? error.message : '工单创建失败。'; } } selectTicket(ticket: ParseTicket): void { this.selected = ticket; this.editingTitle = ticket.title; this.animateWorkbench(); } async updateSelected(): Promise { if (!this.selected) { return; } await this.parseApi.updateTicket(this.selected, { title: this.editingTitle }); await this.loadTickets(); this.message = '工单标题已更新。'; } async transitionSelected(status: TicketStatus): Promise { if (!this.selected) { return; } await this.parseApi.updateTicket(this.selected, { status }); await this.loadTickets(); this.message = `工单状态已更新为:${this.statusLabel(status)}。`; } async deleteSelected(): Promise { if (!this.selected) { return; } await this.parseApi.deleteTicket(this.selected); await this.loadTickets(); this.message = '工单已删除。'; } slaText(ticket: ParseTicket): string { return ticket.isOverdue ? '已超时' : 'SLA 正常'; } statusLabel(status: string): string { const labels: Record = { pending: '待分派', processing: '处理中', confirming: '待客户确认', resolved: '已解决', closed: '已关闭' }; return labels[status] ?? status; } priorityLabel(priority: string): string { const labels: Record = { low: '低', medium: '中', high: '高', urgent: '紧急' }; return labels[priority] ?? priority; } statusTone(status: string): 'default' | 'success' | 'warning' { if (status === 'resolved' || status === 'closed') { return 'success'; } if (status === 'confirming') { return 'warning'; } return 'default'; } priorityTone(priority: string): 'default' | 'warning' | 'danger' { if (priority === 'urgent') { return 'danger'; } if (priority === 'high') { return 'warning'; } return 'default'; } nextActions(status: string): Array<{ label: string; status: TicketStatus }> { if (status === 'pending') { return [{ label: '分派处理', status: 'processing' }]; } if (status === 'processing') { return [{ label: '提交客户确认', status: 'confirming' }]; } if (status === 'confirming') { return [ { label: '确认解决', status: 'resolved' }, { label: '退回处理', status: 'processing' } ]; } return [{ label: '重新打开', status: 'processing' }]; } isDoneStep(step: TicketStatus, currentStatus: string): boolean { const currentIndex = this.lifecycle.indexOf(currentStatus as TicketStatus); return currentIndex >= 0 && this.lifecycle.indexOf(step) < currentIndex; } private animateWorkbench(): void { window.setTimeout(() => { gsap.fromTo('.ticket-row', { y: 8, opacity: 0 }, { y: 0, opacity: 1, duration: 0.24, stagger: 0.025, ease: 'power2.out' }); gsap.fromTo('.timeline article', { scale: 0.96, opacity: 0.45 }, { scale: 1, opacity: 1, duration: 0.32, stagger: 0.06, ease: 'back.out(1.4)' }); gsap.fromTo('.detail-panel, .test-panel', { y: 10, opacity: 0.72 }, { y: 0, opacity: 1, duration: 0.28, ease: 'power2.out' }); }); } }