complaint-card.html 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726
  1. <div class="complaint-card">
  2. <!-- 企业微信监控控制区域 -->
  3. <div class="wechat-monitoring-section">
  4. <div class="monitoring-header">
  5. <h4>企业微信投诉监控</h4>
  6. <div class="monitoring-status" [class]="'status-' + realTimeTaskStatus()">
  7. {{ realTimeTaskStatus() === 'idle' ? '未启动' :
  8. realTimeTaskStatus() === 'processing' ? '监控中' : '已启动' }}
  9. </div>
  10. </div>
  11. <div class="monitoring-controls">
  12. <button class="btn btn-primary"
  13. (click)="startWeChatMonitoring()"
  14. [disabled]="realTimeTaskStatus() === 'processing'">
  15. <i class="fas fa-play"></i>
  16. 启动监控
  17. </button>
  18. <button class="btn btn-secondary"
  19. (click)="setupKeywordMonitoring()">
  20. <i class="fas fa-cog"></i>
  21. 关键词设置
  22. </button>
  23. <div class="monitoring-stats">
  24. <span class="stat-item">
  25. <i class="fas fa-eye"></i>
  26. 监控群组: {{ monitoredGroups || 5 }}
  27. </span>
  28. <span class="stat-item">
  29. <i class="fas fa-bell"></i>
  30. 今日检测: {{ todayDetections || 3 }}
  31. </span>
  32. </div>
  33. </div>
  34. </div>
  35. <!-- 实时代办项处理区域 -->
  36. <div class="realtime-tasks-section" *ngIf="realTimeTaskStatus() !== 'idle'">
  37. <div class="section-header">
  38. <h4>实时代办项</h4>
  39. <span class="task-count">{{ urgentComplaints().length }} 项待处理</span>
  40. </div>
  41. <div class="realtime-task-list">
  42. <div class="task-item"
  43. *ngFor="let complaint of urgentComplaints()"
  44. [class.processing]="processingTaskId() === complaint.id">
  45. <div class="task-info">
  46. <div class="task-title">{{ complaint.description }}</div>
  47. <div class="task-meta">
  48. <span class="customer">{{ complaint.customerName }}</span>
  49. <span class="time">{{ complaint.submitTime | date:'HH:mm' }}</span>
  50. <span class="urgency" [class]="'urgency-' + complaint.urgencyLevel">
  51. {{ complaint.urgencyLevel === 'critical' ? '紧急' : '重要' }}
  52. </span>
  53. </div>
  54. </div>
  55. <div class="task-actions">
  56. <button class="btn btn-sm btn-primary"
  57. (click)="processRealTimeTask(complaint.id)"
  58. [disabled]="processingTaskId() === complaint.id">
  59. <i class="fas fa-play" *ngIf="processingTaskId() !== complaint.id"></i>
  60. <i class="fas fa-spinner fa-spin" *ngIf="processingTaskId() === complaint.id"></i>
  61. {{ processingTaskId() === complaint.id ? '处理中' : '立即处理' }}
  62. </button>
  63. <button class="btn btn-sm btn-warning"
  64. (click)="escalateComplaint(complaint)">
  65. <i class="fas fa-arrow-up"></i>
  66. 升级
  67. </button>
  68. </div>
  69. </div>
  70. </div>
  71. </div>
  72. <!-- 企业微信监控控制面板 -->
  73. <div class="wechat-monitoring-panel">
  74. <h4>企业微信投诉监控</h4>
  75. <div class="monitoring-controls">
  76. <button class="control-btn start-monitoring" (click)="startWeChatMonitoring()">
  77. <span class="btn-icon">📱</span>
  78. 开始微信监控
  79. </button>
  80. <button class="control-btn setup-keywords" (click)="setupKeywordMonitoring()">
  81. <span class="btn-icon">🔍</span>
  82. 设置关键词
  83. </button>
  84. <div class="monitoring-status">
  85. <span class="status-label">监控状态:</span>
  86. <span class="status-indicator active">运行中</span>
  87. </div>
  88. </div>
  89. <div class="keyword-display">
  90. <span class="keyword-label">监控关键词:</span>
  91. <div class="keyword-tags">
  92. @for (keyword of monitorKeywords; track keyword) {
  93. <span class="keyword-tag">{{ keyword }}</span>
  94. }
  95. </div>
  96. </div>
  97. </div>
  98. <!-- 实时代办项状态 -->
  99. @if (realTimeTaskStatus() !== 'idle') {
  100. <div class="real-time-task-status">
  101. <div class="task-status-header">
  102. <h5>实时代办项处理</h5>
  103. <span class="task-status-badge" [class]="realTimeTaskStatus()">
  104. {{ realTimeTaskStatus() === 'processing' ? '处理中...' : '已完成' }}
  105. </span>
  106. </div>
  107. @if (realTimeTaskStatus() === 'processing') {
  108. <div class="task-progress">
  109. <div class="progress-bar">
  110. <div class="progress-fill"></div>
  111. </div>
  112. <span class="progress-text">正在创建代办项:{{ processingTaskId() }}</span>
  113. </div>
  114. }
  115. </div>
  116. }
  117. <!-- 优先级统计 -->
  118. <div class="priority-stats">
  119. <h5>优先级分布</h5>
  120. <div class="priority-cards-grid">
  121. @for (priority of priorities; track priority.value) {
  122. <div class="priority-card" [style.border-color]="priority.color">
  123. <div class="priority-card-header" [style.background-color]="priority.color">
  124. <span class="priority-card-label">{{ priority.label }}</span>
  125. </div>
  126. <div class="priority-card-body">
  127. <div class="priority-card-count">{{ stats().priorityStats[priority.value] || 0 }}</div>
  128. <div class="priority-card-suffix">个</div>
  129. </div>
  130. </div>
  131. }
  132. </div>
  133. </div>
  134. <!-- 类型统计 -->
  135. <div class="type-stats">
  136. <h5>问题类型统计</h5>
  137. <div class="type-grid">
  138. @for (type of complaintTypes; track type.value) {
  139. <div class="type-item">
  140. <span class="type-label">{{ type.label }}</span>
  141. <span class="type-count">{{ stats().typeStats[type.value] || 0 }}</span>
  142. </div>
  143. }
  144. </div>
  145. </div>
  146. <!-- 筛选区域 -->
  147. <div class="filter-section">
  148. <div class="search-row">
  149. <div class="search-group">
  150. <label>搜索:</label>
  151. <input
  152. type="text"
  153. class="search-input"
  154. placeholder="搜索投诉内容、客户姓名..."
  155. [value]="searchKeyword()"
  156. (input)="updateSearchKeyword($event.target.value)">
  157. </div>
  158. </div>
  159. <div class="filter-row">
  160. <div class="filter-group">
  161. <label>状态筛选:</label>
  162. <div class="filter-buttons">
  163. <button
  164. class="filter-btn"
  165. [class.active]="statusFilter() === 'all'"
  166. (click)="updateStatusFilter('all')">
  167. 全部
  168. </button>
  169. <button
  170. class="filter-btn pending"
  171. [class.active]="statusFilter() === 'pending'"
  172. (click)="updateStatusFilter('pending')">
  173. 待处理
  174. </button>
  175. <button
  176. class="filter-btn processing"
  177. [class.active]="statusFilter() === 'processing'"
  178. (click)="updateStatusFilter('processing')">
  179. 处理中
  180. </button>
  181. <button
  182. class="filter-btn resolved"
  183. [class.active]="statusFilter() === 'resolved'"
  184. (click)="updateStatusFilter('resolved')">
  185. 已解决
  186. </button>
  187. </div>
  188. </div>
  189. </div>
  190. <div class="filter-row">
  191. <div class="filter-group">
  192. <label>优先级筛选:</label>
  193. <select
  194. class="filter-select"
  195. [value]="priorityFilter()"
  196. (change)="updatePriorityFilter($event)">
  197. <option value="all">全部优先级</option>
  198. @for (priority of priorities; track priority.value) {
  199. <option [value]="priority.value">{{ priority.label }}</option>
  200. }
  201. </select>
  202. </div>
  203. <div class="filter-group">
  204. <label>类型筛选:</label>
  205. <select
  206. class="filter-select"
  207. [value]="typeFilter()"
  208. (change)="updateTypeFilter($event)">
  209. <option value="all">全部类型</option>
  210. @for (type of complaintTypes; track type.value) {
  211. <option [value]="type.value">{{ type.label }}</option>
  212. }
  213. </select>
  214. </div>
  215. <!-- 新增筛选项 -->
  216. <div class="filter-group">
  217. <label>来源筛选:</label>
  218. <select
  219. class="filter-select"
  220. [value]="sourceFilter()"
  221. (change)="updateSourceFilter($event)">
  222. <option value="all">全部来源</option>
  223. @for (source of complaintSources; track source.value) {
  224. <option [value]="source.value">{{ source.icon }} {{ source.label }}</option>
  225. }
  226. </select>
  227. </div>
  228. <div class="filter-group">
  229. <label>紧急程度:</label>
  230. <select
  231. class="filter-select"
  232. [value]="urgencyFilter()"
  233. (change)="updateUrgencyFilter($event)">
  234. <option value="all">全部级别</option>
  235. @for (urgency of urgencyLevels; track urgency.value) {
  236. <option [value]="urgency.value">{{ urgency.label }}</option>
  237. }
  238. </select>
  239. </div>
  240. </div>
  241. </div>
  242. <!-- 投诉列表 -->
  243. <div class="complaints-list">
  244. @if (filteredComplaints() && filteredComplaints().length > 0) {
  245. <div class="complaints-grid">
  246. @for (complaint of filteredComplaints(); track complaint.id) {
  247. <div class="complaint-card-item" [class]="getStatusClass(complaint)" [class.overdue]="isOverdue(complaint)">
  248. <!-- 卡片头部 -->
  249. <div class="card-header">
  250. <div class="header-left">
  251. <span class="type-tag" [class]="getComplaintType(complaint)">{{ getTypeLabel(getComplaintType(complaint)) }}</span>
  252. <div class="priority-badge" [class]="getPriorityClass(complaint.priority || 'low')" [style.background-color]="getPriorityInfo(complaint.priority || 'low').color">
  253. {{ getPriorityInfo(complaint.priority || 'low').label }}优先级
  254. </div>
  255. <!-- 新增来源标识 -->
  256. <div class="source-badge" [class]="complaint.source">
  257. {{ getComplaintSourceInfo(complaint).icon }} {{ getComplaintSourceInfo(complaint).label }}
  258. </div>
  259. <!-- 紧急程度标识 -->
  260. @if (complaint.urgencyLevel && complaint.urgencyLevel !== 'normal') {
  261. <div class="urgency-badge" [style.background-color]="getUrgencyInfo(complaint).color">
  262. {{ getUrgencyInfo(complaint).label }}
  263. </div>
  264. }
  265. <!-- 自动标注标识 -->
  266. @if (isAutoTagged(complaint)) {
  267. <span class="auto-tag-badge">🤖 自动</span>
  268. }
  269. <!-- 升级标识 -->
  270. @if (complaint.escalationLevel && complaint.escalationLevel > 0) {
  271. <span class="escalation-badge">⬆️ {{ getEscalationDisplay(complaint.escalationLevel) }}</span>
  272. }
  273. </div>
  274. <div class="header-right">
  275. <span class="status-badge" [class]="getStatusClass(complaint)">
  276. {{ complaint.status }}
  277. </span>
  278. @if (isOverdue(complaint)) {
  279. <span class="overdue-badge">超时</span>
  280. }
  281. @if (complaint.followUpRequired) {
  282. <span class="follow-up-badge">需跟进</span>
  283. }
  284. </div>
  285. </div>
  286. <!-- 卡片主体 -->
  287. <div class="card-body">
  288. @if (complaint.customerName) {
  289. <div class="customer-info">
  290. <span class="customer-label">客户:</span>
  291. <span class="customer-name">{{ complaint.customerName }}</span>
  292. </div>
  293. }
  294. <!-- 微信相关信息 -->
  295. @if (complaint.source === 'wechat_auto' && complaint.wechatGroupName) {
  296. <div class="wechat-info">
  297. <span class="wechat-label">📱 微信群:</span>
  298. <span class="wechat-group">{{ complaint.wechatGroupName }}</span>
  299. @if (complaint.keywordMatched && complaint.keywordMatched.length > 0) {
  300. <div class="matched-keywords">
  301. <span class="keyword-label">匹配关键词:</span>
  302. @for (keyword of complaint.keywordMatched; track keyword) {
  303. <span class="matched-keyword">{{ keyword }}</span>
  304. }
  305. </div>
  306. }
  307. </div>
  308. }
  309. <!-- 分配信息 -->
  310. @if (complaint.assignedTo) {
  311. <div class="assignment-info">
  312. <span class="assignment-label">👤 处理人:</span>
  313. <span class="assignee-name">{{ complaint.assignedTo }}</span>
  314. </div>
  315. }
  316. <!-- 标签显示 -->
  317. @if (complaint.tags && complaint.tags.length > 0) {
  318. <div class="tags-section">
  319. <span class="tags-label">🏷️ 标签:</span>
  320. <div class="tags-list">
  321. @for (tag of complaint.tags; track tag) {
  322. <span class="complaint-tag">{{ tag }}</span>
  323. }
  324. </div>
  325. </div>
  326. }
  327. <div class="complaint-description">
  328. <h4>投诉内容</h4>
  329. <p>{{ complaint.description }}</p>
  330. </div>
  331. @if (complaint.images && complaint.images.length > 0) {
  332. <div class="complaint-images">
  333. <h5>相关图片</h5>
  334. <div class="images-grid">
  335. @for (image of complaint.images; track $index) {
  336. <img [src]="image" [alt]="'投诉图片' + ($index + 1)" class="complaint-image">
  337. }
  338. </div>
  339. </div>
  340. }
  341. <div class="time-section">
  342. <div class="time-item">
  343. <span class="time-label">提交时间:</span>
  344. <span class="time-value">{{ complaint.submittedAt | date:'yyyy-MM-dd HH:mm' }}</span>
  345. </div>
  346. <div class="time-item">
  347. <span class="time-label">处理天数:</span>
  348. <span class="time-value">{{ getDaysInProgress(complaint) }} 天</span>
  349. </div>
  350. @if (complaint.resolvedAt) {
  351. <div class="time-item">
  352. <span class="time-label">解决时间:</span>
  353. <span class="time-value">{{ complaint.resolvedAt | date:'yyyy-MM-dd HH:mm' }}</span>
  354. </div>
  355. }
  356. </div>
  357. @if (complaint.handlerComment) {
  358. <div class="handler-section">
  359. <h5>处理意见</h5>
  360. <p class="handler-comment">{{ complaint.handlerComment }}</p>
  361. @if (complaint.handlerName) {
  362. <div class="handler-info">
  363. <span class="handler-label">处理人:</span>
  364. <span class="handler-name">{{ complaint.handlerName }}</span>
  365. </div>
  366. }
  367. </div>
  368. }
  369. @if (complaint.solution) {
  370. <div class="solution-section">
  371. <h5>解决方案</h5>
  372. <p class="solution-text">{{ complaint.solution }}</p>
  373. </div>
  374. }
  375. </div>
  376. <!-- 卡片底部操作按钮 -->
  377. <div class="card-footer">
  378. @if (complaint.status === '待处理') {
  379. <button class="action-btn process-btn" (click)="startProcessing(complaint)">
  380. <span class="btn-icon">🔧</span>
  381. 开始处理
  382. </button>
  383. <button class="action-btn task-btn" (click)="createRealTimeTask(complaint)">
  384. <span class="btn-icon">📋</span>
  385. 创建代办项
  386. </button>
  387. } @else if (complaint.status === '处理中') {
  388. <button class="action-btn complete-btn" (click)="completeProcessing(complaint)">
  389. <span class="btn-icon">✅</span>
  390. 完成处理
  391. </button>
  392. } @else if (complaint.status === '已解决') {
  393. <div class="completed-status">
  394. <span class="completed-icon">✓</span>
  395. <span class="completed-text">处理完成</span>
  396. </div>
  397. }
  398. @if (complaint.status !== '已解决') {
  399. <button class="action-btn detail-btn" (click)="viewDetails(complaint)">
  400. <span class="btn-icon">👁️</span>
  401. 查看详情
  402. </button>
  403. <!-- 新增操作按钮 -->
  404. @if (complaint.escalationLevel !== undefined && complaint.escalationLevel < 3) {
  405. <button class="action-btn escalate-btn" (click)="escalateComplaint(complaint)">
  406. <span class="btn-icon">⬆️</span>
  407. 升级处理
  408. </button>
  409. }
  410. <button class="action-btn assign-btn" (click)="assignComplaint(complaint, '处理员A')">
  411. <span class="btn-icon">👤</span>
  412. 分配处理
  413. </button>
  414. <button class="action-btn tag-btn" (click)="addComplaintTag(complaint)">
  415. <span class="btn-icon">🏷️</span>
  416. 添加标签
  417. </button>
  418. @if (!complaint.followUpRequired) {
  419. <button class="action-btn follow-up-btn" (click)="setFollowUpRequired(complaint, true)">
  420. <span class="btn-icon">📞</span>
  421. 设置跟进
  422. </button>
  423. } @else {
  424. <button class="action-btn follow-up-btn active" (click)="setFollowUpRequired(complaint, false)">
  425. <span class="btn-icon">✓</span>
  426. 取消跟进
  427. </button>
  428. }
  429. }
  430. </div>
  431. </div>
  432. }
  433. </div>
  434. } @else {
  435. <div class="empty-state">
  436. <div class="empty-icon">📋</div>
  437. <div class="empty-title">暂无投诉记录</div>
  438. <div class="empty-description">当前没有符合筛选条件的投诉记录</div>
  439. </div>
  440. }
  441. </div>
  442. <!-- 投诉详情弹窗 -->
  443. @if (showDetailModal() && selectedComplaint()) {
  444. <div class="modal-overlay" (click)="closeDetailModal()">
  445. <div class="modal-content complaint-detail-modal" (click)="$event.stopPropagation()">
  446. <div class="modal-header">
  447. <h3>📋 投诉详情</h3>
  448. <button class="close-btn" (click)="closeDetailModal()">
  449. <span>✕</span>
  450. </button>
  451. </div>
  452. <div class="modal-body">
  453. @if (selectedComplaint(); as complaint) {
  454. <!-- 基本信息 -->
  455. <div class="detail-section">
  456. <h4 class="section-title">基本信息</h4>
  457. <div class="info-grid">
  458. <div class="info-item">
  459. <span class="label">投诉ID:</span>
  460. <span class="value">{{ complaint.id }}</span>
  461. </div>
  462. <div class="info-item">
  463. <span class="label">客户姓名:</span>
  464. <span class="value">{{ complaint.customerName || '未提供' }}</span>
  465. </div>
  466. <div class="info-item">
  467. <span class="label">投诉类型:</span>
  468. <span class="value type-badge" [class]="getComplaintType(complaint)">
  469. {{ getTypeLabel(getComplaintType(complaint)) }}
  470. </span>
  471. </div>
  472. <div class="info-item">
  473. <span class="label">优先级:</span>
  474. <span class="value priority-badge" [style.background-color]="getPriorityInfo(complaint.priority || 'low').color">
  475. {{ getPriorityInfo(complaint.priority || 'low').label }}
  476. </span>
  477. </div>
  478. <div class="info-item">
  479. <span class="label">状态:</span>
  480. <span class="value status-badge" [class]="getStatusClass(complaint)">
  481. {{ complaint.status }}
  482. </span>
  483. </div>
  484. <div class="info-item">
  485. <span class="label">来源:</span>
  486. <span class="value source-badge">
  487. {{ getComplaintSourceInfo(complaint).icon }} {{ getComplaintSourceInfo(complaint).label }}
  488. </span>
  489. </div>
  490. </div>
  491. </div>
  492. <!-- 投诉内容 -->
  493. <div class="detail-section">
  494. <h4 class="section-title">投诉内容</h4>
  495. <div class="complaint-content-box">
  496. <p>{{ complaint.description }}</p>
  497. </div>
  498. </div>
  499. <!-- 微信相关信息 -->
  500. @if (complaint.source === 'wechat_auto' && complaint.wechatGroupName) {
  501. <div class="detail-section">
  502. <h4 class="section-title">📱 微信信息</h4>
  503. <div class="info-grid">
  504. <div class="info-item">
  505. <span class="label">微信群:</span>
  506. <span class="value">{{ complaint.wechatGroupName }}</span>
  507. </div>
  508. @if (complaint.keywordMatched && complaint.keywordMatched.length > 0) {
  509. <div class="info-item full-width">
  510. <span class="label">匹配关键词:</span>
  511. <div class="keyword-tags">
  512. @for (keyword of complaint.keywordMatched; track keyword) {
  513. <span class="keyword-tag">{{ keyword }}</span>
  514. }
  515. </div>
  516. </div>
  517. }
  518. </div>
  519. </div>
  520. }
  521. <!-- 标签 -->
  522. @if (complaint.tags && complaint.tags.length > 0) {
  523. <div class="detail-section">
  524. <h4 class="section-title">🏷️ 标签</h4>
  525. <div class="tags-display">
  526. @for (tag of complaint.tags; track tag) {
  527. <span class="tag-item">
  528. {{ tag }}
  529. <button class="tag-remove" (click)="removeTag(complaint, tag)">×</button>
  530. </span>
  531. }
  532. </div>
  533. </div>
  534. }
  535. <!-- 相关图片 -->
  536. @if (complaint.images && complaint.images.length > 0) {
  537. <div class="detail-section">
  538. <h4 class="section-title">📷 相关图片</h4>
  539. <div class="images-gallery">
  540. @for (image of complaint.images; track $index) {
  541. <img [src]="image" [alt]="'投诉图片' + ($index + 1)" class="gallery-image">
  542. }
  543. </div>
  544. </div>
  545. }
  546. <!-- 处理信息 -->
  547. <div class="detail-section">
  548. <h4 class="section-title">处理信息</h4>
  549. <div class="info-grid">
  550. <div class="info-item">
  551. <span class="label">提交时间:</span>
  552. <span class="value">{{ complaint.submittedAt | date:'yyyy-MM-dd HH:mm' }}</span>
  553. </div>
  554. <div class="info-item">
  555. <span class="label">处理天数:</span>
  556. <span class="value" [class.warning]="isOverdue(complaint)">
  557. {{ getDaysInProgress(complaint) }} 天
  558. </span>
  559. </div>
  560. @if (complaint.assignedTo) {
  561. <div class="info-item">
  562. <span class="label">处理人:</span>
  563. <span class="value">{{ complaint.assignedTo }}</span>
  564. </div>
  565. }
  566. @if (complaint.resolvedAt) {
  567. <div class="info-item">
  568. <span class="label">解决时间:</span>
  569. <span class="value">{{ complaint.resolvedAt | date:'yyyy-MM-dd HH:mm' }}</span>
  570. </div>
  571. }
  572. </div>
  573. </div>
  574. <!-- 处理意见 -->
  575. @if (complaint.handlerComment) {
  576. <div class="detail-section">
  577. <h4 class="section-title">处理意见</h4>
  578. <div class="comment-box">
  579. <p>{{ complaint.handlerComment }}</p>
  580. @if (complaint.handlerName) {
  581. <div class="comment-author">—— {{ complaint.handlerName }}</div>
  582. }
  583. </div>
  584. </div>
  585. }
  586. <!-- 解决方案 -->
  587. @if (complaint.solution) {
  588. <div class="detail-section">
  589. <h4 class="section-title">✅ 解决方案</h4>
  590. <div class="solution-box">
  591. <p>{{ complaint.solution }}</p>
  592. </div>
  593. </div>
  594. }
  595. }
  596. </div>
  597. <div class="modal-footer">
  598. <button class="btn btn-secondary" (click)="closeDetailModal()">关闭</button>
  599. @if (selectedComplaint() && selectedComplaint()!.status !== '已解决') {
  600. <button class="btn btn-primary" (click)="addComplaintTag(selectedComplaint()!); closeDetailModal()">
  601. <i class="fas fa-tag"></i>
  602. 添加标签
  603. </button>
  604. }
  605. </div>
  606. </div>
  607. </div>
  608. }
  609. <!-- 添加标签弹窗 -->
  610. @if (showTagModal() && tagModalComplaint()) {
  611. <div class="modal-overlay" (click)="closeTagModal()">
  612. <div class="modal-content tag-modal" (click)="$event.stopPropagation()">
  613. <div class="modal-header">
  614. <h3>🏷️ 添加标签</h3>
  615. <button class="close-btn" (click)="closeTagModal()">
  616. <span>✕</span>
  617. </button>
  618. </div>
  619. <div class="modal-body">
  620. @if (tagModalComplaint(); as complaint) {
  621. <!-- 当前标签 -->
  622. @if (complaint.tags && complaint.tags.length > 0) {
  623. <div class="current-tags-section">
  624. <h4 class="section-title">当前标签</h4>
  625. <div class="current-tags">
  626. @for (tag of complaint.tags; track tag) {
  627. <span class="tag-item">
  628. {{ tag }}
  629. <button class="tag-remove" (click)="removeTag(complaint, tag)">×</button>
  630. </span>
  631. }
  632. </div>
  633. </div>
  634. }
  635. <!-- 预设标签 -->
  636. <div class="preset-tags-section">
  637. <h4 class="section-title">预设标签</h4>
  638. <div class="preset-tags-grid">
  639. @for (tag of presetTags; track tag) {
  640. <button
  641. class="preset-tag-btn"
  642. [class.selected]="complaint.tags?.includes(tag)"
  643. (click)="selectPresetTag(tag)">
  644. {{ tag }}
  645. </button>
  646. }
  647. </div>
  648. </div>
  649. <!-- 自定义标签 -->
  650. <div class="custom-tag-section">
  651. <h4 class="section-title">自定义标签</h4>
  652. <div class="custom-tag-input-group">
  653. <input
  654. type="text"
  655. class="custom-tag-input"
  656. placeholder="输入自定义标签..."
  657. [value]="newTagInput()"
  658. (input)="newTagInput.set($any($event.target).value)"
  659. (keyup.enter)="addCustomTag()">
  660. <button class="btn btn-primary" (click)="addCustomTag()">
  661. 添加
  662. </button>
  663. </div>
  664. </div>
  665. }
  666. </div>
  667. <div class="modal-footer">
  668. <button class="btn btn-secondary" (click)="closeTagModal()">取消</button>
  669. <button class="btn btn-primary" (click)="closeTagModal()">
  670. <i class="fas fa-check"></i>
  671. 完成
  672. </button>
  673. </div>
  674. </div>
  675. </div>
  676. }
  677. </div>