| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- 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<void> {
- 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<void> {
- 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<void> {
- if (!this.selected) {
- return;
- }
- await this.parseApi.updateTicket(this.selected, { title: this.editingTitle });
- await this.loadTickets();
- this.message = '工单标题已更新。';
- }
- async transitionSelected(status: TicketStatus): Promise<void> {
- if (!this.selected) {
- return;
- }
- await this.parseApi.updateTicket(this.selected, { status });
- await this.loadTickets();
- this.message = `工单状态已更新为:${this.statusLabel(status)}。`;
- }
- async deleteSelected(): Promise<void> {
- 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<string, string> = {
- pending: '待分派',
- processing: '处理中',
- confirming: '待客户确认',
- resolved: '已解决',
- closed: '已关闭'
- };
- return labels[status] ?? status;
- }
- priorityLabel(priority: string): string {
- const labels: Record<string, string> = {
- 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' });
- });
- }
- }
|