徐福静0235668 1 dzień temu
rodzic
commit
0c38a6de6f

+ 3 - 0
.trae/rules/project_rules.md

@@ -0,0 +1,3 @@
+# 项目规范
+
+//模板页面显示的条件和循环采用控制流的@if和@for

+ 243 - 12
package-lock.json

@@ -18,6 +18,7 @@
         "@angular/router": "^20.1.0",
         "chart.js": "^4.5.0",
         "echarts": "^6.0.0",
+        "qrcode": "^1.5.4",
         "roboto-fontface": "^0.10.0",
         "rxjs": "~7.8.0",
         "tslib": "^2.3.0",
@@ -28,6 +29,7 @@
         "@angular/cli": "^20.1.5",
         "@angular/compiler-cli": "^20.1.0",
         "@types/jasmine": "~5.1.0",
+        "@types/qrcode": "^1.5.5",
         "angular-eslint": "20.2.0",
         "eslint": "^9.33.0",
         "jasmine-core": "~5.8.0",
@@ -4212,6 +4214,16 @@
         "undici-types": "~7.10.0"
       }
     },
+    "node_modules/@types/qrcode": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.5.tgz",
+      "integrity": "sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@typescript-eslint/eslint-plugin": {
       "version": "8.40.0",
       "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.40.0.tgz",
@@ -5584,6 +5596,15 @@
         "node": ">=6"
       }
     },
+    "node_modules/camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/caniuse-lite": {
       "version": "1.0.30001741",
       "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz",
@@ -5756,7 +5777,6 @@
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
       "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "color-name": "~1.1.4"
@@ -5769,7 +5789,6 @@
       "version": "1.1.4",
       "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
       "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/colorette": {
@@ -6015,6 +6034,15 @@
         }
       }
     },
+    "node_modules/decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/deep-is": {
       "version": "0.1.4",
       "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -6061,6 +6089,12 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/dijkstrajs": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+      "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
+      "license": "MIT"
+    },
     "node_modules/dom-serialize": {
       "version": "2.2.1",
       "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz",
@@ -7144,7 +7178,6 @@
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
       "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
-      "dev": true,
       "license": "ISC",
       "engines": {
         "node": "6.* || 8.* || >= 10.*"
@@ -9661,6 +9694,15 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/package-json-from-dist": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
@@ -9828,7 +9870,6 @@
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
       "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -9939,6 +9980,15 @@
         "node": ">=16.20.0"
       }
     },
+    "node_modules/pngjs": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+      "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
     "node_modules/postcss": {
       "version": "8.5.6",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -10040,6 +10090,177 @@
         "node": ">=0.9"
       }
     },
+    "node_modules/qrcode": {
+      "version": "1.5.4",
+      "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+      "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+      "license": "MIT",
+      "dependencies": {
+        "dijkstrajs": "^1.0.1",
+        "pngjs": "^5.0.0",
+        "yargs": "^15.3.1"
+      },
+      "bin": {
+        "qrcode": "bin/qrcode"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/qrcode/node_modules/ansi-regex": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+      "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/cliui": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+      "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^6.2.0"
+      }
+    },
+    "node_modules/qrcode/node_modules/emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+      "license": "MIT"
+    },
+    "node_modules/qrcode/node_modules/find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "license": "MIT",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/qrcode/node_modules/p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/string-width": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/strip-ansi": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/y18n": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+      "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+      "license": "ISC"
+    },
+    "node_modules/qrcode/node_modules/yargs": {
+      "version": "15.4.1",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+      "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+      "license": "MIT",
+      "dependencies": {
+        "cliui": "^6.0.0",
+        "decamelize": "^1.2.0",
+        "find-up": "^4.1.0",
+        "get-caller-file": "^2.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^2.0.0",
+        "set-blocking": "^2.0.0",
+        "string-width": "^4.2.0",
+        "which-module": "^2.0.0",
+        "y18n": "^4.0.0",
+        "yargs-parser": "^18.1.2"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/qrcode/node_modules/yargs-parser": {
+      "version": "18.1.3",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+      "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+      "license": "ISC",
+      "dependencies": {
+        "camelcase": "^5.0.0",
+        "decamelize": "^1.2.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
     "node_modules/qs": {
       "version": "6.14.0",
       "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
@@ -10145,7 +10366,6 @@
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
       "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
@@ -10161,6 +10381,12 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/require-main-filename": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+      "license": "ISC"
+    },
     "node_modules/requires-port": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
@@ -10509,6 +10735,12 @@
         "node": ">= 18"
       }
     },
+    "node_modules/set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+      "license": "ISC"
+    },
     "node_modules/setprototypeof": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -11870,6 +12102,12 @@
         "node": ">= 8"
       }
     },
+    "node_modules/which-module": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+      "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+      "license": "ISC"
+    },
     "node_modules/word-wrap": {
       "version": "1.2.5",
       "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -11884,7 +12122,6 @@
       "version": "6.2.0",
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
       "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "ansi-styles": "^4.0.0",
@@ -11989,7 +12226,6 @@
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -11999,7 +12235,6 @@
       "version": "4.3.0",
       "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
       "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "color-convert": "^2.0.1"
@@ -12015,14 +12250,12 @@
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
       "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-      "dev": true,
       "license": "MIT"
     },
     "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
       "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
-      "dev": true,
       "license": "MIT",
       "engines": {
         "node": ">=8"
@@ -12032,7 +12265,6 @@
       "version": "4.2.3",
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "emoji-regex": "^8.0.0",
@@ -12047,7 +12279,6 @@
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
-      "dev": true,
       "license": "MIT",
       "dependencies": {
         "ansi-regex": "^5.0.1"

+ 2 - 0
package.json

@@ -31,6 +31,7 @@
     "@angular/router": "^20.1.0",
     "chart.js": "^4.5.0",
     "echarts": "^6.0.0",
+    "qrcode": "^1.5.4",
     "roboto-fontface": "^0.10.0",
     "rxjs": "~7.8.0",
     "tslib": "^2.3.0",
@@ -41,6 +42,7 @@
     "@angular/cli": "^20.1.5",
     "@angular/compiler-cli": "^20.1.0",
     "@types/jasmine": "~5.1.0",
+    "@types/qrcode": "^1.5.5",
     "angular-eslint": "20.2.0",
     "eslint": "^9.33.0",
     "jasmine-core": "~5.8.0",

+ 106 - 12
src/app/pages/customer-service/case-library/case-library.html

@@ -7,7 +7,7 @@
         <p>今天是 {{ currentDate.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long' }) }},为客户提供最佳的设计参考</p>
       </section>
 
-      <!-- 筛选区域 -->
+      <!-- 筛选区域(固定在顶部) -->
       <section class="filter-section">
         <div class="filter-header">
           <button class="filter-toggle-btn" (click)="toggleFilterPanel()">
@@ -64,6 +64,36 @@
               </div>
             </div>
             
+            <div class="filter-row">
+              <div class="filter-group">
+                <label>项目类型</label>
+                <select formControlName="projectType">
+                  <option value="">全部类型</option>
+                  <option *ngFor="let pt of projectTypeOptions" [value]="pt">{{ pt }}</option>
+                </select>
+              </div>
+              <div class="filter-group">
+                <label>细分类型</label>
+                <select formControlName="subType">
+                  <option value="">全部细分</option>
+                  <option *ngFor="let st of subTypeOptions" [value]="st">{{ st }}</option>
+                </select>
+              </div>
+              <div class="filter-group">
+                <label>渲染水平</label>
+                <select formControlName="renderingLevel">
+                  <option value="">全部水平</option>
+                  <option *ngFor="let rl of renderingLevelOptions" [value]="rl">{{ rl }}</option>
+                </select>
+              </div>
+              <div class="filter-group">
+                <label>排序</label>
+                <select formControlName="sortBy">
+                  <option *ngFor="let opt of sortOptions" [value]="opt.value">{{ opt.label }}</option>
+                </select>
+              </div>
+            </div>
+            
             <div class="filter-row">
               <div class="filter-group">
                 <label>房屋面积</label>
@@ -103,7 +133,7 @@
           <h3>精选案例 <span class="cases-count">({{ filteredCases().length }})</span></h3>
         </div>
         
-        <!-- 案例网格 -->
+        <!-- 案例网格(瀑布流布局) -->
         <div class="cases-grid">
           <div *ngIf="filteredCases().length === 0" class="empty-state">
             <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor">
@@ -177,15 +207,19 @@
             </svg>
           </button>
           
-          <button 
-            *ngFor="let page of pageNumbers()"
-            class="page-btn"
-            [class.active]="page === currentPage()"
-            (click)="goToPage(page)"
-            [disabled]="page === -1"
-          >
-            {{ page === -1 ? '...' : page }}
-          </button>
+          <ng-container *ngFor="let page of pageNumbers()">
+            <button 
+              *ngIf="page !== -1; else ellipsis"
+              class="page-btn"
+              [class.active]="page === currentPage()"
+              (click)="goToPage(page)"
+            >
+              {{ page }}
+            </button>
+            <ng-template #ellipsis>
+              <div class="pagination-ellipsis">...</div>
+            </ng-template>
+          </ng-container>
           
           <button class="page-btn" (click)="nextPage()" [disabled]="currentPage() === totalPages()">
             <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor">
@@ -262,9 +296,37 @@
               <span>{{ selectedCase()?.category }}</span>
             </div>
             <div class="info-item">
-              <label>浏览量</label>
+              <label>项目类型</label>
+              <span>{{ selectedCase()?.projectType }}</span>
+            </div>
+            <div class="info-item">
+              <label>细分类型</label>
+              <span>{{ selectedCase()?.subType }}</span>
+            </div>
+            <div class="info-item">
+              <label>渲染水平</label>
+              <span>{{ selectedCase()?.renderingLevel }}</span>
+            </div>
+            <div class="info-item">
+              <label>浏览次数</label>
               <span>{{ selectedCase()?.views }}</span>
             </div>
+            <div class="info-item">
+              <label>收藏次数</label>
+              <span>{{ selectedCase()?.favoriteCount }}</span>
+            </div>
+            <div class="info-item">
+              <label>喜欢次数</label>
+              <span>{{ selectedCase()?.likeCount }}</span>
+            </div>
+            <div class="info-item">
+              <label>分享次数</label>
+              <span>{{ selectedCase()?.shareCount }}</span>
+            </div>
+            <div class="info-item">
+              <label>转化率</label>
+              <span>{{ selectedCase()?.conversionRate }}%</span>
+            </div>
           </div>
         </div>
         
@@ -305,4 +367,36 @@
       </div>
     </div>
   </div>
+
+  <!-- 分享弹窗 -->
+  <div class="share-modal" *ngIf="showShareModal()" (click)="closeShareModal()">
+    <div class="share-modal-content" (click)="$event.stopPropagation()">
+      <button class="close-btn" (click)="closeShareModal()">
+        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+          <path d="M6 6L18 18M18 6L6 18" stroke="#666" stroke-width="2" stroke-linecap="round"/>
+        </svg>
+      </button>
+  
+      <h3>分享案例</h3>
+      <p class="share-tip">复制链接发送给客户,或扫码打开案例页</p>
+  
+      <div class="share-body">
+        <div class="share-link-box">
+          <input class="share-link-input" [value]="shareLink()" readonly />
+          <button class="btn-primary" (click)="copyShareLink()">复制链接</button>
+          <button class="btn-secondary" (click)="openShareLink()">打开</button>
+        </div>
+  
+        <div class="qr-box">
+          <img *ngIf="qrDataUrl(); else qrPlaceholder" [src]="qrDataUrl()" alt="分享二维码" width="160" height="160" />
+          <ng-template #qrPlaceholder>
+            <div class="qr-placeholder">二维码生成中或不可用</div>
+          </ng-template>
+          <div class="qr-actions" *ngIf="qrDataUrl()">
+            <button class="btn-secondary" (click)="downloadQrCode()">下载二维码</button>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
 </div>

+ 139 - 484
src/app/pages/customer-service/case-library/case-library.scss

@@ -402,521 +402,176 @@ $transition: all 0.3s ease;
   }
 }
 
-// 案例展示区域
-.cases-section {
-  .section-header {
-    display: flex;
-    align-items: center;
-    justify-content: space-between;
-    margin-bottom: 16px;
-    h3 {
-      font-size: 18px;
-      font-weight: 600;
-      color: $text-primary;
-      .cases-count {
-        font-size: 14px;
-        font-weight: normal;
-        color: $text-tertiary;
-      }
-    }
-  }
-}
-
-// 案例网格
-.cases-grid {
-  display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
-  gap: 20px;
-  margin-bottom: 24px;
-}
-
-// 案例卡片
-.case-card {
-  background-color: $background-primary;
-  border-radius: $border-radius;
-  overflow: hidden;
-  box-shadow: $shadow-sm;
-  cursor: pointer;
-  transition: $transition;
-  border: 2px solid transparent;
-  
-  &:hover {
-    border-color: $primary-color;
-    transform: translateY(-4px);
-    box-shadow: $shadow-md;
-  }
-}
-
-.case-image-container {
-  position: relative;
-  height: 200px;
-  overflow: hidden;
-}
-
-.case-image {
-  width: 100%;
-  height: 100%;
-  object-fit: cover;
-  transition: transform 0.5s ease;
-}
-
-.case-card:hover .case-image {
-  transform: scale(1.05);
-}
-
-.case-overlay {
-  position: absolute;
+/* 覆盖与新增:筛选区域固定与过渡 */
+.filter-section {
+  position: sticky;
   top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background: linear-gradient(to top, rgba(0, 0, 0, 0.6) 0%, transparent 50%);
-  opacity: 0;
-  transition: opacity 0.3s ease;
-  display: flex;
-  flex-direction: column;
-  justify-content: flex-end;
-  padding: 16px;
-  gap: 8px;
+  z-index: 12;
+  background: #fff;
+  padding-top: 8px;
+  border-bottom: 1px solid rgba(0,0,0,0.06);
+  box-shadow: 0 2px 8px rgba(0,0,0,0.04);
 }
 
-.case-card:hover .case-overlay {
-  opacity: 1;
+.filter-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
 }
 
-.favorite-btn,
-.share-btn {
-  display: flex;
+.filter-toggle-btn, .reset-filter-btn {
+  display: inline-flex;
   align-items: center;
   gap: 6px;
-  padding: 8px 12px;
-  background-color: white;
-  border: none;
-  border-radius: 20px;
-  color: $text-secondary;
+  padding: 6px 12px;
+  color: #1e5eff;
+  background: #f3f7ff;
+  border: 1px solid #dbe6ff;
+  border-radius: 6px;
   cursor: pointer;
-  font-size: 12px;
-  font-weight: 500;
-  transition: $transition;
-  
-  &:hover {
-    background-color: $primary-color;
-    color: white;
-  }
+  transition: all .2s ease;
 }
-
-.favorite-btn.favorited {
-  background-color: $danger-color;
-  color: white;
-}
-
-.case-info {
-  padding: 16px;
+.filter-toggle-btn:hover, .reset-filter-btn:hover {
+  background: #e8f0ff;
+  box-shadow: 0 2px 8px rgba(30,94,255,0.12);
 }
 
-.case-name {
-  font-size: 16px;
-  font-weight: 600;
-  color: $text-primary;
-  margin-bottom: 8px;
-  display: -webkit-box;
-  -webkit-line-clamp: 1;
-  -webkit-box-orient: vertical;
+.filter-panel {
   overflow: hidden;
+  max-height: 0;
+  transition: max-height .28s ease;
 }
-
-.case-meta {
-  display: flex;
-  gap: 12px;
-  margin-bottom: 12px;
-}
-
-.meta-item {
-  display: flex;
-  align-items: center;
-  gap: 4px;
-  font-size: 12px;
-  color: $text-tertiary;
-}
-
-.case-tags {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 6px;
-}
-
-.tag {
-  display: inline-block;
-  padding: 4px 10px;
-  background-color: $background-tertiary;
-  color: $text-secondary;
-  border-radius: 12px;
-  font-size: 11px;
+.filter-panel.show {
+  max-height: 560px; // 兼容动画高度
 }
 
-// 空状态
-.empty-state {
-  grid-column: 1 / -1;
-  text-align: center;
-  padding: 60px 20px;
-  color: $text-tertiary;
-  background-color: $background-primary;
-  border-radius: $border-radius;
-  border: 1px dashed $border-color;
-  
-  svg {
-    margin-bottom: 16px;
-  }
-  
-  p {
-    margin-bottom: 16px;
-    font-size: 14px;
-  }
-  
-  .btn-reset {
-    padding: 8px 20px;
-    background-color: $background-tertiary;
-    border: 1px solid $border-color;
-    border-radius: $border-radius;
-    color: $text-secondary;
-    cursor: pointer;
-    font-size: 14px;
-    transition: $transition;
-    
-    &:hover {
-      background-color: $background-secondary;
-      border-color: $primary-color;
-      color: $primary-color;
-    }
-  }
+.filter-form {
+  padding: 12px 0 8px;
 }
-
-// 分页控件
-.pagination {
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  gap: 4px;
+.filter-row {
+  display: grid;
+  grid-template-columns: repeat(12, 1fr);
+  gap: 12px 16px;
+  margin-bottom: 10px;
 }
+.filter-group { grid-column: span 3; }
+@media (max-width: 1200px) { .filter-group { grid-column: span 4; } }
+@media (max-width: 900px) { .filter-group { grid-column: span 6; } }
+@media (max-width: 640px) { .filter-group { grid-column: span 12; } }
 
-.page-btn {
+.checkbox-group {
   display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 36px;
-  height: 36px;
-  background-color: $background-primary;
-  border: 1px solid $border-color;
-  border-radius: $border-radius;
-  color: $text-secondary;
-  cursor: pointer;
-  transition: $transition;
-  
-  &:hover:not(:disabled) {
-    background-color: $background-tertiary;
-    border-color: $primary-color;
-    color: $primary-color;
-  }
-  
-  &.active {
-    background-color: $primary-color;
-    border-color: $primary-color;
-    color: white;
-  }
-  
-  &:disabled {
-    opacity: 0.5;
-    cursor: not-allowed;
-  }
+  flex-wrap: wrap;
+  gap: 8px 10px;
 }
+.checkbox-item { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
+.checkbox-item input { accent-color: #1e5eff; }
 
-// 案例详情模态框
-.case-modal {
-  position: fixed;
-  top: 0;
-  left: 0;
-  right: 0;
-  bottom: 0;
-  background-color: rgba(0, 0, 0, 0.7);
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  z-index: 2000;
-  padding: 20px;
-  animation: fadeIn 0.3s ease;
-}
+.range-inputs { display: inline-flex; align-items: center; gap: 6px; }
+.range-inputs input { width: 110px; }
 
-@keyframes fadeIn {
-  from {
-    opacity: 0;
-  }
-  to {
-    opacity: 1;
-  }
+/* 瀑布流布局 */
+.cases-grid {
+  column-count: 1;
+  column-gap: 16px;
 }
+@media (min-width: 768px) { .cases-grid { column-count: 2; } }
+@media (min-width: 1200px) { .cases-grid { column-count: 3; } }
+@media (min-width: 1600px) { .cases-grid { column-count: 4; } }
 
-.modal-content {
-  background-color: $background-primary;
-  border-radius: $border-radius;
-  max-width: 900px;
+.case-card {
+  display: inline-block; // 关键:配合 columns
   width: 100%;
-  max-height: 90vh;
-  overflow-y: auto;
-  position: relative;
-  animation: slideUp 0.3s ease;
+  break-inside: avoid;
+  margin: 0 0 16px;
+  border-radius: 12px;
+  background: #fff;
+  box-shadow: 0 6px 18px rgba(0,0,0,0.06);
+  overflow: hidden;
+  transition: transform .18s ease, box-shadow .18s ease;
 }
+.case-card:hover { transform: translateY(-2px); box-shadow: 0 10px 24px rgba(0,0,0,0.08); }
 
-@keyframes slideUp {
-  from {
-    opacity: 0;
-    transform: translateY(20px);
-  }
-  to {
-    opacity: 1;
-    transform: translateY(0);
-  }
-}
+.case-image-container { position: relative; }
+.case-image { width: 100%; height: auto; display: block; }
 
-.close-btn {
+.case-overlay {
   position: absolute;
-  top: 16px;
-  right: 16px;
-  width: 32px;
-  height: 32px;
-  background-color: rgba(0, 0, 0, 0.1);
-  border: none;
-  border-radius: 50%;
-  color: $text-secondary;
-  cursor: pointer;
+  inset: 0;
   display: flex;
-  align-items: center;
-  justify-content: center;
-  transition: $transition;
-  z-index: 10;
-  
-  &:hover {
-    background-color: $danger-color;
-    color: white;
-  }
-}
-
-.case-detail-header {
-  padding: 24px 24px 0;
-  
-  h2 {
-    font-size: 24px;
-    font-weight: 600;
-    color: $text-primary;
-    margin-bottom: 8px;
-  }
-  
-  .case-detail-meta {
-    display: flex;
-    gap: 16px;
-    font-size: 14px;
-    color: $text-secondary;
-  }
-}
-
-.case-image-gallery {
-  margin: 20px 0;
-  
-  .main-image {
-    height: 400px;
-    overflow: hidden;
-    
-    img {
-      width: 100%;
-      height: 100%;
-      object-fit: cover;
-    }
-  }
-  
-  .thumbnails {
-    display: flex;
-    gap: 8px;
-    padding: 12px 24px;
-    overflow-x: auto;
-    
-    .thumbnail {
-      width: 80px;
-      height: 60px;
-      object-fit: cover;
-      border-radius: 4px;
-      cursor: pointer;
-      border: 2px solid transparent;
-      transition: $transition;
-      
-      &:hover {
-        border-color: $primary-color;
-      }
-    }
-  }
-}
-
-.case-detail-info {
-  padding: 0 24px 24px;
-  
-  .info-section {
-    margin-bottom: 24px;
-    
-    h3 {
-      font-size: 18px;
-      font-weight: 600;
-      color: $text-primary;
-      margin-bottom: 12px;
-    }
-    
-    p {
-      font-size: 14px;
-      line-height: 1.6;
-      color: $text-secondary;
-      margin-bottom: 8px;
-    }
-  }
-  
-  .info-grid {
-    display: grid;
-    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
-    gap: 16px;
-    
-    .info-item {
-      
-      label {
-        display: block;
-        font-size: 12px;
-        color: $text-tertiary;
-        margin-bottom: 4px;
-      }
-      
-      span {
-        font-size: 14px;
-        color: $text-primary;
-        font-weight: 500;
-      }
-    }
-  }
-  
-  .tags-container {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 8px;
-  }
-}
-
-.case-actions {
-  display: flex;
-  gap: 12px;
-  padding: 20px 24px;
-  background-color: $background-tertiary;
-  border-top: 1px solid $border-color;
-  
-  .primary-btn,
-  .secondary-btn {
-    display: flex;
-    align-items: center;
-    gap: 6px;
-    padding: 10px 20px;
-    border-radius: $border-radius;
-    font-size: 14px;
-    font-weight: 500;
-    cursor: pointer;
-    transition: $transition;
-    border: none;
-  }
-  
-  .primary-btn {
-    background-color: $primary-color;
-    color: white;
-    
-    &:hover {
-      background-color: $primary-dark;
-      transform: translateY(-1px);
-      box-shadow: $shadow-md;
-    }
-  }
-  
-  .secondary-btn {
-    background-color: $background-primary;
-    color: $text-secondary;
-    border: 1px solid $border-color;
-    
-    &:hover {
-      background-color: $background-secondary;
-      border-color: $primary-color;
-      color: $primary-color;
-    }
-  }
+  align-items: flex-end;
+  justify-content: space-between;
+  padding: 10px;
+  opacity: 0;
+  background: linear-gradient(to top, rgba(0,0,0,0.35), rgba(0,0,0,0.0));
+  transition: opacity .2s ease;
 }
+.case-card:hover .case-overlay { opacity: 1; }
 
-// 响应式设计
-@media (max-width: 1200px) {
-  .cases-grid {
-    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
-  }
+.favorite-btn, .share-btn {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 6px 10px;
+  color: #fff;
+  background: rgba(255,255,255,0.1);
+  border: 1px solid rgba(255,255,255,0.3);
+  border-radius: 8px;
+  backdrop-filter: blur(2px);
+  cursor: pointer;
 }
-
-@media (max-width: 768px) {
-  .dashboard-content {
-    flex-direction: column;
-  }
-  
-  .sidebar {
-    width: 100%;
-    height: auto;
-    border-right: none;
-    border-bottom: 1px solid $border-color;
-    
-    .sidebar-nav {
-      padding: 8px 0;
-      
-      .nav-item {
-        padding: 10px 16px;
-      }
-    }
-  }
-  
-  .content-wrapper {
-    padding: 16px;
-  }
-  
-  .menu-toggle {
-    display: block;
-  }
-  
-  .navbar-center {
-    margin: 0 16px;
-  }
-  
-  .cases-grid {
-    grid-template-columns: 1fr;
-  }
-  
-  .filter-form .filter-row {
-    flex-direction: column;
-    gap: 16px;
-  }
-  
-  .filter-form .filter-group {
-    min-width: 100%;
-  }
-  
-  .modal-content {
-    margin: 10px;
-    max-height: calc(100vh - 20px);
-  }
-  
-  .case-image-gallery .main-image {
-    height: 250px;
-  }
-  
-  .case-actions {
-    flex-direction: column;
-    
-    .primary-btn,
-    .secondary-btn {
-      width: 100%;
-      justify-content: center;
-    }
-  }
+.favorite-btn:hover, .share-btn:hover { background: rgba(255,255,255,0.2); }
+
+.case-info { padding: 12px; }
+.case-name { font-size: 15px; margin: 0 0 6px; }
+.case-meta { display: flex; gap: 12px; color: #667085; font-size: 12px; }
+.case-meta .meta-item { display: inline-flex; align-items: center; gap: 6px; }
+.case-tags { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; }
+.case-tags .tag { padding: 2px 8px; border-radius: 999px; background: #f3f7ff; color: #1e5eff; font-size: 12px; }
+
+/* 空状态 */
+.empty-state { text-align: center; color: #98a2b3; padding: 40px 0; }
+.btn-reset { margin-top: 10px; padding: 6px 12px; border-radius: 6px; border: 1px solid #d0d5dd; background: #fff; cursor: pointer; }
+
+/* 分页省略号 */
+.pagination-ellipsis { padding: 0 6px; color: #98a2b3; display: inline-flex; align-items: center; }
+
+/* 案例详情模态框优化 */
+.case-modal { position: fixed; inset: 0; background: rgba(17,24,39,0.45); backdrop-filter: blur(2px); z-index: 100; display: flex; align-items: center; justify-content: center; padding: 24px; }
+.case-modal .modal-content { width: min(980px, 92vw); max-height: 92vh; overflow: auto; border-radius: 12px; background: #fff; position: relative; box-shadow: 0 20px 48px rgba(0,0,0,0.18); }
+.case-modal .close-btn { position: absolute; right: 12px; top: 12px; background: #fff; border: 1px solid #e5e7eb; border-radius: 999px; width: 36px; height: 36px; display: grid; place-items: center; cursor: pointer; }
+
+.case-image-gallery { padding: 12px; }
+.case-image-gallery .main-image img { width: 100%; border-radius: 10px; }
+.case-image-gallery .thumbnails { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; margin-top: 10px; }
+.case-image-gallery .thumbnail { width: 100%; border-radius: 8px; }
+
+.info-section { padding: 12px; }
+.info-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px 14px; }
+@media (max-width: 900px) { .info-grid { grid-template-columns: repeat(2, 1fr); } }
+.info-item label { color: #98a2b3; font-size: 12px; }
+.info-item span { color: #111827; font-size: 14px; }
+
+.case-actions { padding: 12px; display: flex; gap: 10px; border-top: 1px solid #f2f4f7; }
+.primary-btn, .secondary-btn { display: inline-flex; align-items: center; gap: 8px; border-radius: 8px; padding: 8px 12px; cursor: pointer; border: 1px solid transparent; }
+.primary-btn { background: linear-gradient(135deg, #1e5eff, #4f8cff); color: #fff; box-shadow: 0 6px 16px rgba(30,94,255,0.25); }
+.secondary-btn { background: #f8fafc; color: #1f2937; border-color: #e5e7eb; }
+
+/* 分享弹窗 */
+.share-modal { position: fixed; inset: 0; background: rgba(17,24,39,0.45); z-index: 110; display: flex; align-items: center; justify-content: center; padding: 24px; }
+.share-modal-content { width: min(520px, 92vw); background: #fff; border-radius: 12px; box-shadow: 0 18px 44px rgba(0,0,0,0.16); position: relative; padding: 18px; }
+.share-modal .close-btn { position: absolute; right: 12px; top: 12px; background: #fff; border: 1px solid #e5e7eb; border-radius: 999px; width: 36px; height: 36px; display: grid; place-items: center; cursor: pointer; }
+.share-modal h3 { margin: 0 0 6px; font-size: 18px; }
+.share-tip { color: #667085; font-size: 13px; margin-bottom: 12px; }
+.share-body { display: flex; gap: 12px; align-items: flex-start; }
+.share-link-box { flex: 1; display: flex; gap: 8px; }
+.share-link-input { flex: 1; height: 36px; padding: 0 10px; border: 1px solid #e5e7eb; border-radius: 8px; font-size: 13px; }
+.btn-primary { height: 36px; padding: 0 12px; border-radius: 8px; color: #fff; background: #1e5eff; border: 1px solid #1e5eff; cursor: pointer; }
+.qr-box { width: 160px; height: 160px; border: 1px dashed #e5e7eb; border-radius: 8px; display: grid; place-items: center; color: #98a2b3; font-size: 12px; }
+
+/* 响应式微调 */
+@media (max-width: 720px) {
+  .share-body { flex-direction: column; }
+  .qr-box { width: 100%; height: 140px; }
 }

+ 243 - 76
src/app/pages/customer-service/case-library/case-library.ts

@@ -1,7 +1,8 @@
 import { Component, OnInit, signal, computed } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup } from '@angular/forms';
-import { RouterModule } from '@angular/router';
+import { RouterModule, ActivatedRoute } from '@angular/router';
+import * as QRCode from 'qrcode';
 
 // 定义案例接口
 interface CaseItem {
@@ -20,6 +21,14 @@ interface CaseItem {
   tags: string[];
   views: number;
   description: string;
+  // 新增字段
+  projectType: '工装' | '家装';
+  subType: '平层' | '复式' | '别墅' | '自建房' | '其他';
+  renderingLevel: '高端' | '中端';
+  shareCount: number;
+  favoriteCount: number;
+  likeCount: number;
+  conversionRate: number; // 0-100
 }
 
 @Component({
@@ -36,6 +45,12 @@ export class CaseLibrary implements OnInit {
   // 搜索关键词
   searchTerm = signal('');
   
+  // 分享弹窗
+  showShareModal = signal(false);
+  shareLink = signal('');
+  qrDataUrl = signal('');
+  sharedCaseId = signal<string | null>(null);
+
   // 筛选表单
   filterForm: FormGroup;
   
@@ -58,11 +73,11 @@ export class CaseLibrary implements OnInit {
     }
     
     // 应用表单筛选
-    const filters = this.filterForm.value;
+    const filters = this.filterForm.value as any;
     
     if (filters.style && filters.style.length > 0) {
       result = result.filter(caseItem => 
-        caseItem.style.some(style => filters.style.includes(style))
+        caseItem.style.some((s: string) => filters.style.includes(s))
       );
     }
     
@@ -73,18 +88,51 @@ export class CaseLibrary implements OnInit {
     if (filters.property) {
       result = result.filter(caseItem => caseItem.property === filters.property);
     }
+
+    if (filters.projectType) {
+      result = result.filter(caseItem => caseItem.projectType === filters.projectType);
+    }
+
+    if (filters.subType) {
+      result = result.filter(caseItem => caseItem.subType === filters.subType);
+    }
+
+    if (filters.renderingLevel) {
+      result = result.filter(caseItem => caseItem.renderingLevel === filters.renderingLevel);
+    }
     
     if (filters.minArea) {
-      result = result.filter(caseItem => caseItem.area >= filters.minArea);
+      result = result.filter(caseItem => caseItem.area >= Number(filters.minArea));
     }
     
     if (filters.maxArea) {
-      result = result.filter(caseItem => caseItem.area <= filters.maxArea);
+      result = result.filter(caseItem => caseItem.area <= Number(filters.maxArea));
     }
     
     if (filters.favorite) {
       result = result.filter(caseItem => caseItem.isFavorite);
     }
+
+    // 排序
+    if (filters.sortBy) {
+      switch (filters.sortBy) {
+        case 'views':
+          result.sort((a, b) => b.views - a.views);
+          break;
+        case 'shares':
+          result.sort((a, b) => b.shareCount - a.shareCount);
+          break;
+        case 'conversion':
+          result.sort((a, b) => b.conversionRate - a.conversionRate);
+          break;
+        case 'createdAt':
+          result.sort((a, b) => +b.createdAt - +a.createdAt);
+          break;
+      }
+    } else {
+      // 默认按创建时间倒序
+      result.sort((a, b) => +b.createdAt - +a.createdAt);
+    }
     
     return result;
   });
@@ -114,60 +162,119 @@ export class CaseLibrary implements OnInit {
   styleOptions = ['现代简约', '北欧风', '工业风', '新中式', '法式轻奢', '日式', '美式', '混搭'];
   houseTypeOptions = ['一室一厅', '两室一厅', '两室两厅', '三室一厅', '三室两厅', '四室两厅', '复式', '别墅', '其他'];
   propertyOptions = ['万科', '绿城', '保利', '龙湖', '融创', '中海', '碧桂园', '其他'];
+  projectTypeOptions: Array<CaseItem['projectType']> = ['工装', '家装'];
+  subTypeOptions: Array<CaseItem['subType']> = ['平层', '复式', '别墅', '自建房', '其他'];
+  renderingLevelOptions: Array<CaseItem['renderingLevel']> = ['高端', '中端'];
+  sortOptions = [
+    { label: '最新上传', value: 'createdAt' },
+    { label: '浏览最多', value: 'views' },
+    { label: '分享最多', value: 'shares' },
+    { label: '转化率最高', value: 'conversion' }
+  ];
   
-  constructor(private fb: FormBuilder) {
+  constructor(private fb: FormBuilder, private route: ActivatedRoute) {
     // 初始化筛选表单
     this.filterForm = this.fb.group({
       style: [[]],
       houseType: [''],
       property: [''],
+      projectType: [''],
+      subType: [''],
+      renderingLevel: [''],
       minArea: [''],
       maxArea: [''],
-      favorite: [false]
+      favorite: [false],
+      sortBy: ['createdAt']
     });
   }
   
   ngOnInit(): void {
     // 加载模拟案例数据
     this.loadCases();
+    
+    // 读取分享链接参数并打开对应案例详情
+    this.route.queryParamMap.subscribe(params => {
+      const caseId = params.get('case');
+      if (caseId) {
+        const item = this.cases().find(c => c.id === caseId);
+        if (item) {
+          this.viewCaseDetails(item);
+        }
+      }
+    });
   }
   
   // 加载案例数据
   loadCases(): void {
+    // 本地占位图集合
+    const LOCAL_IMAGES = [
+      '/assets/images/portfolio-1.svg',
+      '/assets/images/portfolio-2.svg',
+      '/assets/images/portfolio-3.svg',
+      '/assets/images/portfolio-4.svg'
+    ];
+
+    const rand = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
+    const pick = <T,>(arr: T[]): T => arr[Math.floor(Math.random() * arr.length)];
+
     // 模拟API请求获取案例数据
-    const mockCases: CaseItem[] = Array.from({ length: 24 }, (_, i) => ({
-      id: `case-${i + 1}`,
-      name: `${this.styleOptions[Math.floor(Math.random() * this.styleOptions.length)]}风格 ${this.houseTypeOptions[Math.floor(Math.random() * this.houseTypeOptions.length)]}设计`,
-      category: ['客厅', '卧室', '厨房', '浴室', '书房', '餐厅'][Math.floor(Math.random() * 6)],
-      style: [this.styleOptions[Math.floor(Math.random() * this.styleOptions.length)]],
-      houseType: this.houseTypeOptions[Math.floor(Math.random() * this.houseTypeOptions.length)],
-      property: this.propertyOptions[Math.floor(Math.random() * this.propertyOptions.length)],
-      designer: ['张设计', '李设计', '王设计', '赵设计', '陈设计'][Math.floor(Math.random() * 5)],
-      area: Math.floor(Math.random() * 100) + 50, // 50-150㎡
-      createdAt: new Date(Date.now() - Math.floor(Math.random() * 365) * 24 * 60 * 60 * 1000),
-      coverImage: `https://picsum.photos/id/${100 + i}/600/400`,
-      detailImages: Array.from({ length: 4 }, (_, j) => `https://picsum.photos/id/${130 + i + j}/800/600`),
-      isFavorite: Math.random() > 0.7,
-      tags: ['热门', '精选', '新上传', '高性价比', '业主好评'].filter(() => Math.random() > 0.5),
-      views: Math.floor(Math.random() * 1000) + 100,
-      description: '这是一个精美的' + ['现代简约', '北欧风', '新中式'][Math.floor(Math.random() * 3)] + '风格设计案例,融合了功能性与美学,为客户打造了舒适宜人的居住环境。'
-    }));
+    const mockCases: CaseItem[] = Array.from({ length: 24 }, (_, i) => {
+      const cover = LOCAL_IMAGES[i % LOCAL_IMAGES.length];
+      const details = Array.from({ length: 4 }, (_, j) => LOCAL_IMAGES[(i + j) % LOCAL_IMAGES.length]);
+      const projectType = pick(this.projectTypeOptions);
+      const subType = pick(this.subTypeOptions);
+      const renderingLevel = pick(this.renderingLevelOptions);
+      const createdAt = new Date(Date.now() - rand(0, 365) * 24 * 60 * 60 * 1000);
+      const views = rand(100, 3000);
+      const shareCount = rand(10, 500);
+      const favoriteCount = rand(5, 400);
+      const likeCount = rand(10, 800);
+      const conversionRate = Number((Math.random() * 30 + 5).toFixed(1)); // 5% - 35%
+
+      return {
+        id: `case-${i + 1}`,
+        name: `${pick(this.styleOptions)}风格 ${pick(this.houseTypeOptions)}设计`,
+        category: pick(['客厅', '卧室', '厨房', '浴室', '书房', '餐厅']),
+        style: [pick(this.styleOptions)],
+        houseType: pick(this.houseTypeOptions),
+        property: pick(this.propertyOptions),
+        designer: pick(['张设计', '李设计', '王设计', '赵设计', '陈设计']),
+        area: rand(50, 150),
+        createdAt,
+        coverImage: cover,
+        detailImages: details,
+        isFavorite: Math.random() > 0.7,
+        tags: ['热门', '精选', '新上传', '高性价比', '业主好评'].filter(() => Math.random() > 0.5),
+        views,
+        description: '这是一个精美的' + pick(['现代简约', '北欧风', '新中式']) + '风格设计案例,融合了功能性与美学,为客户打造了舒适宜人的居住环境。',
+        projectType,
+        subType,
+        renderingLevel,
+        shareCount,
+        favoriteCount,
+        likeCount,
+        conversionRate
+      };
+    });
     
     this.cases.set(mockCases);
   }
   
-  // 切换收藏状态
+  // 切换收藏状态(同时更新收藏计数)
   toggleFavorite(caseId: string): void {
     this.cases.set(
-      this.cases().map(caseItem => 
-        caseItem.id === caseId 
-          ? { ...caseItem, isFavorite: !caseItem.isFavorite }
-          : caseItem
-      )
+      this.cases().map(caseItem => {
+        if (caseItem.id === caseId) {
+          const isFav = !caseItem.isFavorite;
+          const favoriteCount = Math.max(0, caseItem.favoriteCount + (isFav ? 1 : -1));
+          return { ...caseItem, isFavorite: isFav, favoriteCount };
+        }
+        return caseItem;
+      })
     );
   }
   
-  // 查看案例详情
+  // 查看案例详情(增加浏览量)
   viewCaseDetails(caseItem: CaseItem): void {
     this.selectedCase.set(caseItem);
     // 增加浏览量
@@ -185,11 +292,78 @@ export class CaseLibrary implements OnInit {
     this.selectedCase.set(null);
   }
   
-  // 分享案例
-  shareCase(caseId: string): void {
-    console.log('分享案例:', caseId);
-    // 模拟复制到剪贴板
-    alert('案例链接已复制到剪贴板!');
+  // 分享案例:生成链接、复制并展示弹窗,同时更新分享计数
+  async shareCase(caseId: string): Promise<void> {
+    const link = this.getShareLink(caseId);
+    this.shareLink.set(link);
+    this.showShareModal.set(true);
+    this.sharedCaseId.set(caseId);
+
+    // 生成二维码
+    await this.generateQrCode(link);
+
+    // 分享计数 +1
+    this.cases.set(
+      this.cases().map(item => item.id === caseId ? { ...item, shareCount: item.shareCount + 1 } : item)
+    );
+
+    // 尝试自动复制
+    try {
+      await navigator.clipboard.writeText(link);
+    } catch {
+      // 忽略复制失败(例如非安全上下文),用户可手动复制
+    }
+  }
+
+  getShareLink(caseId: string): string {
+    const base = window.location.origin;
+    return `${base}/customer-service/case-library?case=${encodeURIComponent(caseId)}`;
+  }
+
+  async copyShareLink(): Promise<void> {
+    const link = this.shareLink();
+    try {
+      await navigator.clipboard.writeText(link);
+      alert('链接已复制到剪贴板');
+    } catch {
+      alert('复制失败,请手动选择链接复制');
+    }
+  }
+
+  // 生成二维码
+  private async generateQrCode(text: string): Promise<void> {
+    try {
+      const url = await QRCode.toDataURL(text, { width: 160, margin: 1 });
+      this.qrDataUrl.set(url);
+    } catch (e) {
+      console.error('生成二维码失败', e);
+      this.qrDataUrl.set('');
+    }
+  }
+
+  downloadQrCode(): void {
+    const dataUrl = this.qrDataUrl();
+    if (!dataUrl) { return; }
+    const a = document.createElement('a');
+    const name = this.sharedCaseId() ? `${this.sharedCaseId()}-qr.png` : 'case-qr.png';
+    a.href = dataUrl;
+    a.download = name;
+    document.body.appendChild(a);
+    a.click();
+    document.body.removeChild(a);
+  }
+
+  openShareLink(): void {
+    const link = this.shareLink();
+    if (link) {
+      window.open(link, '_blank', 'noopener');
+    }
+  }
+
+  closeShareModal(): void {
+    this.showShareModal.set(false);
+    this.qrDataUrl.set('');
+    this.sharedCaseId.set(null);
   }
   
   // 重置筛选条件
@@ -198,9 +372,13 @@ export class CaseLibrary implements OnInit {
       style: [],
       houseType: '',
       property: '',
+      projectType: '',
+      subType: '',
+      renderingLevel: '',
       minArea: '',
       maxArea: '',
-      favorite: false
+      favorite: false,
+      sortBy: 'createdAt'
     });
     this.searchTerm.set('');
     this.currentPage.set(1);
@@ -237,35 +415,36 @@ export class CaseLibrary implements OnInit {
     });
   }
   // 智能页码生成
-pageNumbers = computed(() => {
-  const pages = [];
-  const total = this.totalPages();
-  const current = this.currentPage();
-  
-  // 显示当前页及前后2页,加上第一页和最后一页
-  const start = Math.max(1, current - 2);
-  const end = Math.min(total, current + 2);
-  
-  if (start > 1) {
-    pages.push(1);
-    if (start > 2) {
-      pages.push(-1); // 用-1表示省略号
+  pageNumbers = computed(() => {
+    const pages = [] as number[];
+    const total = this.totalPages();
+    const current = this.currentPage();
+    
+    // 显示当前页及前后2页,加上第一页和最后一页
+    const start = Math.max(1, current - 2);
+    const end = Math.min(total, current + 2);
+    
+    if (start > 1) {
+      pages.push(1);
+      if (start > 2) {
+        pages.push(-1); // 用-1表示省略号
+      }
     }
-  }
-  
-  for (let i = start; i <= end; i++) {
-    pages.push(i);
-  }
-  
-  if (end < total) {
-    if (end < total - 1) {
-      pages.push(-1); // 用-1表示省略号
+    
+    for (let i = start; i <= end; i++) {
+      pages.push(i);
     }
-    pages.push(total);
-  }
+    
+    if (end < total) {
+      if (end < total - 1) {
+        pages.push(-1); // 用-1表示省略号
+      }
+      pages.push(total);
+    }
+    
+    return pages;
+  });
   
-  return pages;
-});
   // 格式化样式显示的辅助方法
   getStyleDisplay(caseItem: CaseItem | null | undefined): string {
     if (!caseItem || !caseItem.style) {
@@ -274,22 +453,10 @@ pageNumbers = computed(() => {
     return Array.isArray(caseItem.style) ? caseItem.style.join('、') : String(caseItem.style);
   }
 
-  // 如果需要直接获取当前选中案例的样式显示
+  // 获取当前选中案例的样式显示
   getSelectedCaseStyle(): string {
     return this.getStyleDisplay(this.selectedCase());
   }
-
-  // 删除重复的简单分页逻辑
-  // 保留智能分页逻辑(第251-270行)
-  // 移除下面这段代码:
-  //  // 在组件类中添加这个计算属性
-  //  pageNumbers = computed(() => {
-  //    const pages = [];
-  //    for (let i = 1; i <= this.totalPages(); i++) {
-  //      pages.push(i);
-  //    }
-  //    return pages;
-  //  });
   
   // 修复 onStyleChange 方法中的类型安全问题
   onStyleChange(style: string, isChecked: boolean): void {

+ 92 - 0
src/app/pages/customer-service/dashboard/dashboard.html

@@ -71,6 +71,98 @@
         </div>
       </div>
     </div>
+
+    <!-- 新增:核心指标渐变数字卡片 -->
+    <div class="core-metrics-grid">
+      <div class="core-metric-card gradient-green">
+        <div class="metric-icon">
+          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+            <path d="M3 12l2-2 4 4 8-8 2 2-10 10z"></path>
+          </svg>
+        </div>
+        <div class="metric-content">
+          <div class="metric-value">{{ stats.conversionRateToday() }}%</div>
+          <div class="metric-label">当日成交率</div>
+        </div>
+      </div>
+      <div class="core-metric-card gradient-orange">
+        <div class="metric-icon">
+          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+            <circle cx="12" cy="12" r="10"></circle>
+            <line x1="12" y1="8" x2="12" y2="12"></line>
+            <line x1="12" y1="16" x2="12.01" y2="16"></line>
+          </svg>
+        </div>
+        <div class="metric-content">
+          <div class="metric-value">{{ stats.pendingComplaints() }}</div>
+          <div class="metric-label">待处理投诉数</div>
+        </div>
+      </div>
+      <div class="core-metric-card gradient-blue">
+        <div class="metric-icon">
+          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+            <path d="M21 15v4a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h7"></path>
+          </svg>
+        </div>
+        <div class="metric-content">
+          <div class="metric-value">{{ stats.unRepliedConsultations() }}</div>
+          <div class="metric-label">未回复咨询数</div>
+        </div>
+      </div>
+    </div>
+</section>
+
+<!-- 新客户触达 与 老客户回访 -->
+<section class="crm-queues">
+  <div class="crm-grid">
+    <!-- 新客户触达 -->
+    <div class="crm-card">
+      <div class="crm-header">
+        <h3>新客户触达</h3>
+        <a class="view-all-link" (click)="goToConsultationList()">查看全部</a>
+      </div>
+      <div class="crm-list">
+        <div class="crm-item" *ngFor="let c of newReachOutCustomers()" (click)="navigateToConsultation(c.name)">
+          <div class="crm-item-main">
+            <div class="avatar small">{{ c.name.charAt(0) }}</div>
+            <div class="info">
+              <div class="name">{{ c.name }}</div>
+              <div class="meta">
+                <span class="tag">{{ c.demandType }}</span>
+                <span class="time">上次沟通:{{ formatDate(c.lastContactAt) }}</span>
+              </div>
+            </div>
+          </div>
+          <button class="ios-btn mini">触达</button>
+        </div>
+        <div *ngIf="newReachOutCustomers().length === 0" class="empty-state small">暂无待触达客户</div>
+      </div>
+    </div>
+
+    <!-- 老客户回访 -->
+    <div class="crm-card">
+      <div class="crm-header">
+        <h3>老客户回访</h3>
+        <a class="view-all-link" (click)="goToConsultationList()">查看全部</a>
+      </div>
+      <div class="crm-list">
+        <div class="crm-item" *ngFor="let c of oldCustomerFollowUps()" (click)="navigateToConsultation(c.name)">
+          <div class="crm-item-main">
+            <div class="avatar small alt">{{ c.name.charAt(0) }}</div>
+            <div class="info">
+              <div class="name">{{ c.name }}</div>
+              <div class="meta">
+                <span class="tag">{{ c.demandType }}</span>
+                <span class="time">上次沟通:{{ formatDate(c.lastContactAt) }}</span>
+              </div>
+            </div>
+          </div>
+          <button class="ios-btn mini outline">回访</button>
+        </div>
+        <div *ngIf="oldCustomerFollowUps().length === 0" class="empty-state small">暂无待回访客户</div>
+      </div>
+    </div>
+  </div>
 </section>
 
 <!-- 紧急待办和项目动态流 -->

+ 695 - 112
src/app/pages/customer-service/dashboard/dashboard.scss

@@ -984,122 +984,10 @@ $ios-radius-xl: 22px;
     }
   }
 }
-
-/* 按钮样式 */
-.btn-primary {
-  padding: 8px 16px;
-  background-color: $primary-color;
-  color: white;
-  border: none;
-  border-radius: $border-radius;
-  font-size: 14px;
-  font-weight: 500;
-  cursor: pointer;
-  transition: $transition;
-  
-  &:hover {
-    background-color: $primary-dark;
-    transform: translateY(-1px);
-    box-shadow: $shadow-md;
-  }
-  
-  &:active {
-    transform: translateY(0);
-  }
-  
-  &:disabled {
-    background-color: $text-tertiary-dark;
-    cursor: not-allowed;
-    transform: none;
-    box-shadow: none;
-  }
-}
-
-/* 回到顶部按钮 */
-.back-to-top {
-  position: fixed;
-  bottom: 24px;
-  right: 24px;
-  width: 48px;
-  height: 48px;
-  border-radius: 50%;
-  background-color: $primary-color;
-  color: white;
-  border: none;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  cursor: pointer;
-  box-shadow: $shadow-md;
-  transition: $transition;
-  opacity: 0;
-  visibility: hidden;
-  z-index: 1000;
-  
-  &.visible {
-    opacity: 1;
-    visibility: visible;
-  }
-  
-  &:hover {
-    background-color: $primary-dark;
-    transform: translateY(-2px);
-    box-shadow: $shadow-lg;
-  }
-  
-  &:active {
-    transform: translateY(0);
-  }
-}
-
-/* 响应式设计 */
-@media (max-width: 768px) {
-  .content-grid {
-    grid-template-columns: 1fr;
-    gap: 20px;
-  }
-  
-  .stats-grid {
-    grid-template-columns: 1fr;
-    gap: 16px;
-  }
-  
-  .welcome-section {
-    padding: 20px;
-  }
-  
-  .welcome-section h2 {
-    font-size: 24px;
-  }
-  
-  .welcome-section p {
-    font-size: 14px;
-  }
-  
-  .section-header h3 {
-    font-size: 18px;
-  }
-  
-  .task-title,
-  .update-title {
-    font-size: 16px !important;
-  }
-  
-  .task-project,
-  .update-text {
-    font-size: 14px !important;
-  }
-}
-
-@media (max-width: 1024px) {
-  .content-grid {
-    grid-template-columns: 1fr;
-  }
   
   .stats-dashboard .stats-grid {
     grid-template-columns: repeat(2, 1fr);
   }
-}
 
 @media (max-width: 768px) {
   .welcome-section {
@@ -1508,4 +1396,699 @@ input[type="datetime-local"]::-webkit-calendar-picker-indicator {
   color: $text-tertiary-dark;
   cursor: not-allowed;
   transform: none;
+}
+/* 核心指标渐变卡片 */
+.core-metrics-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 16px;
+  margin-top: 16px;
+}
+
+.core-metric-card {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 16px;
+  border-radius: 14px;
+  color: #fff;
+  box-shadow: $shadow-md;
+
+  .metric-icon {
+    width: 40px;
+    height: 40px;
+    border-radius: 12px;
+    background: rgba(255,255,255,0.2);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .metric-content {
+    .metric-value { font-size: 22px; font-weight: 700; }
+    .metric-label { font-size: 13px; opacity: 0.9; }
+  }
+
+  &.gradient-green {
+    background: linear-gradient(135deg, #34d399, #10b981);
+  }
+  &.gradient-orange {
+    background: linear-gradient(135deg, #f59e0b, #f97316);
+  }
+  &.gradient-blue {
+    background: linear-gradient(135deg, #60a5fa, #3b82f6);
+  }
+}
+
+/* CRM队列 */
+.crm-queues {
+  margin: 20px 0 8px;
+}
+
+.crm-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 16px;
+}
+
+.crm-card {
+  background: $ios-card-background;
+  border: 1px solid $ios-border;
+  border-radius: $ios-radius-lg;
+  padding: 16px;
+  box-shadow: $shadow-sm;
+}
+
+.crm-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 12px;
+
+  h3 { margin: 0; font-size: 16px; font-weight: 600; }
+  .view-all-link { color: $primary-color; font-size: 13px; cursor: pointer; }
+}
+
+.crm-list {
+  display: flex;
+  flex-direction: column;
+  gap: 10px;
+}
+
+.crm-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 12px;
+  padding: 10px 12px;
+  border-radius: 10px;
+  border: 1px solid $border-color;
+  background: $background-color;
+  transition: $transition;
+
+  &:hover { background: #f7f7fb; }
+
+  .crm-item-main { display: flex; align-items: center; gap: 12px; }
+  .avatar.small {
+    width: 28px; height: 28px; border-radius: 50%;
+    background: $primary-color; color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700;
+    &.alt { background: $ios-secondary; }
+  }
+  .info {
+    .name { font-weight: 600; color: $text-primary-dark; }
+    .meta { color: $text-tertiary-dark; font-size: 12px; display: flex; gap: 8px; }
+    .tag { background: rgba(0,0,0,0.05); padding: 2px 6px; border-radius: 6px; }
+    .time { }
+  }
+
+  .ios-btn.mini { padding: 6px 10px; font-size: 12px; border-radius: 8px; }
+}
+
+.empty-state.small { color: $text-tertiary-dark; font-size: 13px; text-align: center; padding: 8px 0; }
+
+@media (max-width: 900px) {
+  .core-metrics-grid { grid-template-columns: 1fr; }
+  .crm-grid { grid-template-columns: 1fr; }
+}
+
+.core-metric-card:hover {
+  border-color: $primary-color;
+  box-shadow: 0 4px 12px color-mix(in srgb, $primary-color 10%, transparent);
+  transform: translateY(-2px);
+}
+
+.core-icon {
+  width: 48px;
+  height: 48px;
+  background-color: color-mix(in srgb, $primary-color 15%, transparent);
+  border-radius: 12px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin: 0 auto 12px;
+  color: $primary-color;
+  transition: $transition;
+}
+
+.core-card:nth-child(2) .core-icon {
+  background-color: color-mix(in srgb, $success-color 15%, transparent);
+  color: $success-color;
+}
+
+.core-card:nth-child(3) .core-icon {
+  background-color: color-mix(in srgb, $warning-color 15%, transparent);
+  color: $warning-color;
+}
+
+.core-card:nth-child(4) .core-icon {
+  background-color: color-mix(in srgb, $danger-color 15%, transparent);
+  color: $danger-color;
+}
+
+.core-number {
+  font-size: 24px;
+  font-weight: 700;
+  color: $text-primary-dark;
+  margin-bottom: 6px;
+  line-height: 1.2;
+}
+
+.core-label {
+  font-size: 14px;
+  color: $text-secondary-dark;
+  margin: 0;
+}
+
+/* 内容网格 */
+.content-grid {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 24px;
+}
+
+/* 任务列表区块 */
+.tasks-section {
+  background-color: $card-background;
+  border-radius: $border-radius;
+  padding: 24px;
+  box-shadow: $shadow-md;
+  transition: $transition;
+}
+
+.tasks-section:hover {
+  box-shadow: $shadow-lg;
+}
+
+/* 通用区块头部 */
+.section-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 20px;
+}
+
+.section-header h3 {
+  font-size: 20px;
+  font-weight: 600;
+  color: $text-primary-dark;
+  margin: 0;
+}
+
+.section-header .view-all {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  font-size: 14px;
+  color: $primary-color;
+  text-decoration: none;
+  transition: $transition;
+}
+
+.section-header .view-all:hover {
+  color: $primary-dark;
+}
+
+/* 搜索框样式 */
+.search-box {
+  position: relative;
+}
+
+.search-input {
+  padding: 8px 16px;
+  border: 1px solid $border-color;
+  border-radius: $border-radius;
+  font-size: 14px;
+  color: $text-primary-dark;
+  background-color: $card-background;
+  transition: $transition;
+  outline: none;
+  min-width: 200px;
+}
+
+.search-input:focus {
+  border-color: $primary-color;
+  box-shadow: 0 0 0 3px color-mix(in srgb, $primary-color 10%, transparent);
+}
+
+/* 紧急待办列表 */
+.tasks-list {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.tasks-list.empty {
+  text-align: center;
+  padding: 40px 20px;
+  color: $text-tertiary-dark;
+}
+
+.tasks-list.empty p {
+  font-size: 16px;
+  margin: 0;
+}
+
+/* 任务项目样式 */
+.task-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 16px 20px;
+  border-radius: $border-radius;
+  transition: $transition;
+  margin-bottom: 12px;
+  position: relative;
+}
+
+.task-item.completed {
+  background-color: #e8f5e9;
+  border: 1px solid #c8e6c9;
+}
+
+.task-item.overdue {
+  background-color: #ffebee;
+  border: 1px solid #ffcdd2;
+}
+
+.task-item:hover {
+  transform: translateY(-1px);
+}
+
+/* 任务复选框样式 */
+.task-checkbox {
+  margin-right: 16px;
+}
+
+.task-checkbox input[type="checkbox"] {
+  width: 20px;
+  height: 20px;
+  accent-color: $primary-color;
+  border-radius: 4px;
+  cursor: pointer;
+}
+
+/* 任务状态标记 */
+.task-status {
+  position: absolute;
+  top: 12px;
+  right: 12px;
+  padding: 4px 12px;
+  border-radius: 16px;
+  font-size: 12px;
+  font-weight: 600;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+}
+
+.task-status.completed {
+  background-color: color-mix(in srgb, $success-color 15%, transparent);
+  color: $success-color;
+}
+
+.task-status.pending {
+  background-color: color-mix(in srgb, $warning-color 15%, transparent);
+  color: $warning-color;
+}
+
+.task-status.overdue {
+  background-color: color-mix(in srgb, $danger-color 15%, transparent);
+  color: $danger-color;
+}
+
+/* 任务内容 */
+.task-content {
+  flex: 1;
+}
+
+.task-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: $text-primary-dark;
+  margin-bottom: 4px;
+  line-height: 1.3;
+}
+
+.task-project {
+  font-size: 14px;
+  color: $text-secondary-dark;
+  margin-bottom: 8px;
+}
+
+.task-meta {
+  font-size: 14px;
+  color: $text-tertiary-dark;
+}
+
+/* 任务进度 */
+.task-progress {
+  width: 100%;
+  height: 8px;
+  background-color: $background-color;
+  border-radius: 4px;
+  overflow: hidden;
+  margin-top: 12px;
+  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+
+.progress-bar {
+    height: 100%;
+    background: linear-gradient(90deg, $primary-color, #5e9eff);
+    transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+    position: relative;
+    overflow: hidden;
+    border-radius: 4px;
+  }
+
+.progress-bar::after {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.6), transparent);
+  animation: shimmer 2s infinite;
+}
+
+@keyframes shimmer {
+  0% {
+    transform: translateX(-100%);
+  }
+  100% {
+    transform: translateX(100%);
+  }
+}
+
+.progress-text {
+  font-size: 12px;
+  color: $text-tertiary-dark;
+  text-align: right;
+  margin-top: 4px;
+  font-weight: 500;
+}
+
+/* 任务处理进度条容器 */
+.task-progress-container {
+  width: 100%;
+  height: 12px;
+  background-color: #f5f5f5;
+  border-radius: 6px;
+  overflow: hidden;
+  margin-top: 12px;
+  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
+}
+
+.task-progress-bar {
+  height: 100%;
+  background: linear-gradient(90deg, $primary-color 0%, #5e9eff 100%);
+  transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+  position: relative;
+  overflow: hidden;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  padding-right: 8px;
+  border-radius: 6px;
+}
+
+.task-progress-bar::after {
+  content: '';
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.5), transparent);
+  animation: shimmer 1.5s infinite;
+}
+
+.task-progress-text {
+  font-size: 10px;
+  color: white;
+  font-weight: 600;
+  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
+  z-index: 1;
+}
+
+/* 任务操作按钮 */
+.task-actions {
+  display: flex;
+  align-items: center;
+}
+
+.task-button {
+  padding: 6px 12px;
+  border: 1px solid $border-color;
+  border-radius: $border-radius;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: $transition;
+  background-color: $card-background;
+  color: $text-secondary-dark;
+  min-width: 60px;
+  text-align: center;
+}
+
+.task-button:hover {
+  border-color: $primary-color;
+  color: $primary-color;
+}
+
+.task-button.primary {
+  background-color: $primary-color;
+  color: white;
+  border-color: $primary-color;
+}
+
+.task-button.primary:hover {
+  background-color: $primary-dark;
+  border-color: $primary-dark;
+  color: white;
+}
+
+.task-button:disabled {
+  opacity: 0.6;
+  cursor: not-allowed;
+  transform: none;
+}
+
+/* 搜索框样式 */
+.search-box {
+  position: relative;
+}
+
+.search-input {
+  padding: 8px 16px;
+  border: 1px solid $border-color;
+  border-radius: $border-radius;
+  font-size: 14px;
+  color: $text-primary-dark;
+  background-color: $card-background;
+  transition: $transition;
+  outline: none;
+  min-width: 200px;
+}
+
+.search-input:focus {
+  border-color: $primary-color;
+  box-shadow: 0 0 0 3px color-mix(in srgb, $primary-color 10%, transparent);
+}
+
+/* 项目动态流 */
+.project-updates-section {
+  background-color: $card-background;
+  border-radius: $border-radius;
+  padding: 24px;
+  box-shadow: $shadow-md;
+  transition: $transition;
+}
+
+.project-updates-section:hover {
+  box-shadow: $shadow-lg;
+}
+
+.project-updates-section.empty {
+  text-align: center;
+  padding: 60px 20px;
+  color: $text-tertiary-dark;
+}
+
+.project-updates-section.empty p {
+  font-size: 16px;
+  margin: 0;
+}
+
+.project-updates-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.update-item {
+  display: flex;
+  gap: 16px;
+  padding: 20px;
+  border: 1px solid $border-color;
+  border-radius: $border-radius;
+  transition: $transition;
+  
+  &:hover {
+    border-color: $primary-color;
+    box-shadow: 0 4px 12px color-mix(in srgb, $primary-color 10%, transparent);
+    transform: translateY(-2px);
+  }
+  
+  .update-icon {
+    width: 44px;
+    height: 44px;
+    background-color: $primary-color;
+    border-radius: 12px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: white;
+    flex-shrink: 0;
+    transition: $transition;
+  }
+  
+  .update-content {
+    flex: 1;
+    
+    .update-title {
+      font-size: 18px;
+      font-weight: 600;
+      color: $text-primary-dark;
+      margin-bottom: 6px;
+      line-height: 1.3;
+    }
+    
+    .update-text {
+      font-size: 16px;
+      color: $text-secondary-dark;
+      margin-bottom: 12px;
+      line-height: 1.4;
+    }
+    
+    .update-meta {
+      display: flex;
+      align-items: center;
+      gap: 20px;
+      
+      .update-time {
+        font-size: 14px;
+        color: $text-tertiary-dark;
+      }
+      
+      .update-status {
+        padding: 4px 12px;
+        border-radius: 16px;
+        font-size: 14px;
+        font-weight: 600;
+        transition: $transition;
+        
+        &.status-active,
+        &.status-info {
+          background-color: #e3f2fd;
+          color: $primary-color;
+          border: 1px solid #bbdefb;
+        }
+        
+        &.status-completed,
+        &.status-success {
+          background-color: #e8f5e9;
+          color: $success-color;
+          border: 1px solid #c8e6c9;
+        }
+        
+        &.status-warning {
+          background-color: #fff3e0;
+          color: $warning-color;
+          border: 1px solid #ffe0b2;
+        }
+        
+        &.status-danger {
+          background-color: #ffebee;
+          color: $danger-color;
+          border: 1px solid #ffcdd2;
+        }
+      }
+    }
+  }
+}
+
+/* 回到顶部按钮 */
+.back-to-top {
+  position: fixed;
+  bottom: 24px;
+  right: 24px;
+  width: 48px;
+  height: 48px;
+  border-radius: 50%;
+  background-color: $primary-color;
+  color: white;
+  border: none;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  cursor: pointer;
+  box-shadow: $shadow-md;
+  transition: $transition;
+  opacity: 0;
+  visibility: hidden;
+  z-index: 1000;
+  
+  &.visible {
+    opacity: 1;
+    visibility: visible;
+  }
+  
+  &:hover {
+    background-color: $primary-dark;
+    transform: translateY(-2px);
+    box-shadow: $shadow-lg;
+  }
+  
+  &:active {
+    transform: translateY(0);
+  }
+}
+
+/* 响应式设计 */
+@media (max-width: 768px) {
+  .content-grid {
+    grid-template-columns: 1fr;
+    gap: 20px;
+  }
+  
+  .stats-grid {
+    grid-template-columns: 1fr;
+    gap: 16px;
+  }
+  
+  .welcome-section {
+    padding: 20px;
+  }
+  
+  .welcome-section h2 {
+    font-size: 24px;
+  }
+  
+  .welcome-section p {
+    font-size: 14px;
+  }
+  
+  .section-header h3 {
+    font-size: 18px;
+  }
+  
+  .task-title,
+  .update-title {
+    font-size: 16px !important;
+  }
+  
+  .task-project,
+  .update-text {
+    font-size: 14px !important;
+  }
 }

+ 35 - 2
src/app/pages/customer-service/dashboard/dashboard.ts

@@ -20,10 +20,16 @@ export class Dashboard implements OnInit, OnDestroy {
     newConsultations: signal(12),
     pendingAssignments: signal(5),
     exceptionProjects: signal(2),
-    todayRevenue: signal(28500)
+    todayRevenue: signal(28500),
+    // 新增核心指标
+    conversionRateToday: signal(36), // 当日成交率(%)
+    pendingComplaints: signal(3),   // 待处理投诉数
+    unRepliedConsultations: signal(7) // 未回复咨询数
   };
 
-  // 紧急待办列表
+  // 新增:新客户触达/老客户回访列表
+  newReachOutCustomers = signal<Array<{ name: string; demandType: string; lastContactAt: Date }>>([]);
+  oldCustomerFollowUps = signal<Array<{ name: string; demandType: string; lastContactAt: Date }>>([]);
   urgentTasks = signal<Task[]>([]);
 
   // 任务处理状态
@@ -127,6 +133,7 @@ export class Dashboard implements OnInit, OnDestroy {
   ngOnInit(): void {
     this.loadUrgentTasks();
     this.loadProjectUpdates();
+    this.loadCRMQueues(); // 新增:加载新客户触达与老客户回访队列
     
     // 添加滚动事件监听
     window.addEventListener('scroll', this.onScroll.bind(this));
@@ -169,6 +176,32 @@ export class Dashboard implements OnInit, OnDestroy {
     });
   }
 
+  // 加载新客户触达与老客户回访数据(示例数据,后续可接入接口)
+  private loadCRMQueues(): void {
+    const now = new Date();
+    this.newReachOutCustomers.set([
+      { name: '陈女士', demandType: '全屋定制', lastContactAt: new Date(now.getTime() - 2 * 60 * 60 * 1000) },
+      { name: '赵先生', demandType: '厨房改造', lastContactAt: new Date(now.getTime() - 26 * 60 * 60 * 1000) },
+      { name: '吴先生', demandType: '客厅软装', lastContactAt: new Date(now.getTime() - 5 * 60 * 60 * 1000) }
+    ]);
+
+    this.oldCustomerFollowUps.set([
+      { name: '王女士', demandType: '别墅整装', lastContactAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000) },
+      { name: '李先生', demandType: '卧室升级', lastContactAt: new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000) },
+      { name: '孙女士', demandType: '卫生间翻新', lastContactAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000) }
+    ]);
+  }
+
+  // 便捷跳转到咨询列表并带上客户名称搜索
+  navigateToConsultation(keyword: string): void {
+    this.router.navigate(['/customer-service/consultation-list'], { queryParams: { q: keyword } });
+  }
+
+  // 查看全部咨询列表
+  goToConsultationList(): void {
+    this.router.navigate(['/customer-service/consultation-list']);
+  }
+  
   loadProjectUpdates(): void {
     // 模拟项目更新数据
     this.projectService.getProjects().subscribe(projects => {

+ 98 - 61
src/app/pages/customer-service/dashboard/pages/consultation-list/consultation-list.component.ts

@@ -1,89 +1,126 @@
-import { Component } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
 import { CommonModule } from '@angular/common';
-import { RouterModule } from '@angular/router';
+import { RouterModule, ActivatedRoute } from '@angular/router';
+import { FormsModule } from '@angular/forms';
 
 @Component({
   selector: 'app-consultation-list',
   standalone: true,
-  imports: [CommonModule, RouterModule],
+  imports: [CommonModule, RouterModule, FormsModule],
   template: `
     <div class="ios-container">
       <header class="ios-header">
-        <h1>新咨询列表</h1>
         <button class="ios-back-btn" (click)="goBack()">
           <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
             <path d="M19 12H5M12 19l-7-7 7-7"/>
           </svg>
         </button>
+        <h1>客户咨询记录</h1>
       </header>
-      
+
       <div class="ios-content">
-        <!-- 咨询列表内容 -->
-        <div class="ios-card" *ngFor="let item of consultations">
-          <div class="ios-card-header">
-            <h3>{{item.customer}}</h3>
-            <span class="ios-badge">{{item.time}}</span>
+        <div class="toolbar">
+          <div class="search-bar">
+            <input type="text" placeholder="搜索咨询记录..." [(ngModel)]="keyword" (input)="applyFilters()">
+            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor">
+              <circle cx="11" cy="11" r="8"/>
+              <line x1="21" y1="21" x2="16.65" y2="16.65"/>
+            </svg>
+          </div>
+          <div class="quick-filters">
+            <label>
+              <input type="checkbox" [(ngModel)]="filterUnrepliedOver1h" (change)="applyFilters()"> 未回复超1小时
+            </label>
+            <label>
+              <input type="checkbox" [(ngModel)]="filterSensitive" (change)="applyFilters()"> 含敏感词
+            </label>
+          </div>
+        </div>
+
+        <div class="consultation-list">
+          <div class="consultation-card" *ngFor="let item of filteredConsultations">
+            <div class="card-header">
+              <div class="customer-info">
+                <div class="avatar">{{item.customer.charAt(0)}}</div>
+                <div>
+                  <h3>{{item.customer}}</h3>
+                  <p class="time">{{item.time}}</p>
+                </div>
+              </div>
+              <span class="status-badge" [class.urgent]="item.priority === 'high'">
+                {{item.priority === 'high' ? '紧急' : '普通'}}
+              </span>
+            </div>
+            <p class="content">{{item.content}}</p>
+            <div class="card-footer">
+              <button class="ios-btn" (click)="openWeCom(item)" title="跳转企业微信">同步到企微</button>
+              <button class="ios-btn outline">查看详情</button>
+            </div>
           </div>
-          <p>{{item.content}}</p>
         </div>
       </div>
     </div>
   `,
   styles: [`
-    .ios-container {
-      max-width: 800px;
-      margin: 0 auto;
-      font-family: -apple-system, BlinkMacSystemFont, sans-serif;
-    }
-    
-    .ios-header {
-      display: flex;
-      align-items: center;
-      padding: 16px;
-      background: #f2f2f7;
-      border-bottom: 1px solid #d1d1d6;
-    }
-    
-    .ios-back-btn {
-      background: none;
-      border: none;
-      margin-right: 16px;
-    }
-    
-    .ios-content {
-      padding: 16px;
-    }
-    
-    .ios-card {
-      background: white;
-      border-radius: 12px;
-      padding: 16px;
-      margin-bottom: 16px;
-      box-shadow: 0 1px 3px rgba(0,0,0,0.1);
-    }
-    
-    .ios-card-header {
-      display: flex;
-      justify-content: space-between;
-      margin-bottom: 8px;
-    }
-    
-    .ios-badge {
-      background: #007aff;
-      color: white;
-      padding: 4px 8px;
-      border-radius: 10px;
-      font-size: 12px;
-    }
+    .toolbar { display:flex; justify-content: space-between; align-items:center; gap:12px; margin-bottom: 12px; }
+    .quick-filters { display:flex; gap: 16px; color:#636366; font-size:14px; }
+    .quick-filters input { margin-right:6px; }
+    .search-bar { position: relative; }
+    .search-bar input { width: 260px; padding: 10px 14px 10px 36px; border-radius: 10px; border: 1px solid #d1d1d6; background: #f7f7fb; }
+    .search-bar svg { position:absolute; left: 10px; top: 50%; transform: translateY(-50%); color:#8e8e93; }
   `]
 })
-export class ConsultationListComponent {
+export class ConsultationListComponent implements OnInit {
+  keyword = '';
+  filterUnrepliedOver1h = false;
+  filterSensitive = false;
+
+  private sensitivePatterns = /(发手机|发邮箱|找不到人)/;
+
   consultations = [
-    {customer: '张先生', time: '10:30', content: '咨询关于厨房改造的预算和工期'},
-    {customer: '李女士', time: '11:45', content: '询问客厅设计风格建议'},
-    {customer: '王先生', time: '14:20', content: '需要全屋设计方案咨询'}
+    {customer: '张先生', time: '10:30', content: '咨询关于厨房改造的预算和工期', priority: 'normal', lastReplyMinutes: 30, weComUserId: 'zhangxiansheng'},
+    {customer: '李女士', time: '11:45', content: '询问客厅设计风格建议,发手机给你', priority: 'high', lastReplyMinutes: 120, weComUserId: 'linnvshi'},
+    {customer: '王先生', time: '14:20', content: '需要全屋设计方案咨询,发邮箱吧', priority: 'normal', lastReplyMinutes: 75, weComUserId: 'wangxiansheng'}
   ];
-  
+
+  filteredConsultations = [...this.consultations];
+
+  constructor(private route: ActivatedRoute) {}
+
+  ngOnInit(): void {
+    this.route.queryParams.subscribe(params => {
+      const q = params['q'];
+      if (q) {
+        this.keyword = q;
+      }
+      this.applyFilters();
+    });
+  }
+
+  applyFilters(): void {
+    const kw = this.keyword.trim().toLowerCase();
+    this.filteredConsultations = this.consultations.filter(item => {
+      const matchKw = !kw || item.customer.toLowerCase().includes(kw) || item.content.toLowerCase().includes(kw);
+      const matchUnreplied = !this.filterUnrepliedOver1h || (item.lastReplyMinutes ?? 0) > 60;
+      const matchSensitive = !this.filterSensitive || this.sensitivePatterns.test(item.content);
+      return matchKw && matchUnreplied && matchSensitive;
+    });
+  }
+
+  openWeCom(item: any): void {
+    const userId = item.weComUserId || encodeURIComponent(item.customer);
+    const wecomUrl = `wecom://message?username=${userId}`;
+    const webUrl = `https://work.weixin.qq.com/wework_admin/frame#contacts`;
+    try {
+      window.location.href = wecomUrl;
+      setTimeout(() => {
+        window.open(webUrl, '_blank');
+      }, 800);
+    } catch (_) {
+      window.open(webUrl, '_blank');
+    }
+  }
+
   goBack() {
     history.back();
   }

+ 205 - 184
src/app/pages/hr/assets/assets-stats.html

@@ -15,9 +15,11 @@
         (input)="searchTerm.set($event.target.value)"
         class="search-input"
       >
-      <button mat-icon-button *ngIf="searchTerm()" (click)="searchTerm.set('')" class="clear-search">
-        <mat-icon>close</mat-icon>
-      </button>
+      @if (searchTerm()) {
+        <button mat-icon-button (click)="searchTerm.set('')" class="clear-search">
+          <mat-icon>close</mat-icon>
+        </button>
+      }
     </div>
     
     <div class="filter-controls">
@@ -25,7 +27,9 @@
         <label>资产类型:</label>
         <select [value]="typeFilter()" (change)="typeFilter.set($any($event.target).value)" class="filter-select">
           <option value="">全部类型</option>
-          <option *ngFor="let type of assetTypes()" [value]="type">{{ type }}</option>
+          @for (type of assetTypes(); track type) {
+            <option [value]="type">{{ type }}</option>
+          }
         </select>
       </div>
       
@@ -44,7 +48,9 @@
         <label>所属部门:</label>
         <select [value]="departmentFilter()" (change)="departmentFilter.set($any($event.target).value)" class="filter-select">
           <option value="">全部部门</option>
-          <option *ngFor="let dept of departments()" [value]="dept">{{ dept }}</option>
+          @for (dept of departments(); track dept) {
+            <option [value]="dept">{{ dept }}</option>
+          }
         </select>
       </div>
       
@@ -151,147 +157,66 @@
   <!-- 资产列表/网格视图 -->
   <div class="assets-view">
     <!-- 网格视图 -->
-    <div *ngIf="selectedView() === 'grid'" class="assets-grid">
-      <div 
-        *ngFor="let asset of filteredAssets()"
-        class="asset-card"
-        [class.faulty]="asset.status === '故障' || asset.status === '报修中'"
-      >
-        <div class="asset-header">
-          <div class="asset-type-icon" [class]="asset.type">
-            <mat-icon>{{ getTypeIcon(asset.type) }}</mat-icon>
-          </div>
-          <div class="asset-status" [class]="getStatusClass(asset.status)">
-            {{ asset.status }}
-          </div>
-        </div>
-        <div class="asset-content">
-          <h3 class="asset-name">{{ asset.name }}</h3>
-          <div class="asset-info">
-            <div class="info-item">
-              <span class="info-label">序列号:</span>
-              <span class="info-value">{{ asset.serialNumber }}</span>
-            </div>
-            <div class="info-item">
-              <span class="info-label">购买日期:</span>
-              <span class="info-value">{{ formatDate(asset.purchaseDate) }}</span>
-            </div>
-            <div class="info-item">
-              <span class="info-label">价值:</span>
-              <span class="info-value">{{ formatCurrency(asset.value) }}</span>
-            </div>
-            <div class="info-item">
-              <span class="info-label">部门:</span>
-              <span class="info-value">{{ asset.department }}</span>
-            </div>
-            <div *ngIf="asset.assignedToName" class="info-item">
-              <span class="info-label">使用人:</span>
-              <span class="info-value">{{ asset.assignedToName }}</span>
-            </div>
-            <div *ngIf="asset.status === '故障' || asset.status === '报修中'" class="info-item faulty-notice">
-              <span class="info-label">故障描述:</span>
-              <span class="info-value">需要维修</span>
-            </div>
-          </div>
-        </div>
-        <div class="asset-actions">
-          <button mat-icon-button matTooltip="查看详情" class="action-btn">
-            <mat-icon>visibility</mat-icon>
-          </button>
-          <button 
-            mat-icon-button 
-            matTooltip="编辑信息"
-            class="action-btn"
+    @if (selectedView() === 'grid') {
+      <div class="assets-grid">
+        @for (asset of filteredAssets(); track asset.id) {
+          <div 
+            class="asset-card"
+            [class.faulty]="asset.status === '故障' || asset.status === '报修中'"
           >
-            <mat-icon>edit</mat-icon>
-          </button>
-          <ng-container *ngIf="asset.status === '空闲'">
-            <button 
-              mat-icon-button 
-              matTooltip="分配资产"
-              class="action-btn primary"
-              (click)="openAssignmentDialog(asset)"
-            >
-              <mat-icon>assignment_ind</mat-icon>
-            </button>
-          </ng-container>
-          <ng-container *ngIf="asset.status === '占用'">
-            <button 
-              mat-icon-button 
-              matTooltip="归还资产"
-              class="action-btn primary"
-              (click)="openAssignmentDialog(asset, true)"
-            >
-              <mat-icon>undo</mat-icon>
-            </button>
-          </ng-container>
-          <ng-container *ngIf="asset.status === '故障'">
-            <button 
-              mat-icon-button 
-              matTooltip="申请报修"
-              class="action-btn warning"
-              (click)="openRepairDialog(asset)"
-            >
-              <mat-icon>build</mat-icon>
-            </button>
-          </ng-container>
-        </div>
-      </div>
-    </div>
-    
-    <!-- 列表视图 -->
-    <div *ngIf="selectedView() === 'list'" class="assets-list">
-      <table mat-table [dataSource]="filteredAssets()" class="assets-table">
-        <ng-container matColumnDef="name">
-          <th mat-header-cell *matHeaderCellDef>资产名称</th>
-          <td mat-cell *matCellDef="let asset">
-            <div class="asset-name-list">
-              <div class="asset-type-icon-small" [class]="asset.type">
+            <div class="asset-header">
+              <div class="asset-type-icon" [class]="asset.type">
                 <mat-icon>{{ getTypeIcon(asset.type) }}</mat-icon>
               </div>
-              <span>{{ asset.name }}</span>
+              <div class="asset-status" [class]="getStatusClass(asset.status)">
+                {{ asset.status }}
+              </div>
+            </div>
+            <div class="asset-content">
+              <h3 class="asset-name">{{ asset.name }}</h3>
+              <div class="asset-info">
+                <div class="info-item">
+                  <span class="info-label">序列号:</span>
+                  <span class="info-value">{{ asset.serialNumber }}</span>
+                </div>
+                <div class="info-item">
+                  <span class="info-label">购买日期:</span>
+                  <span class="info-value">{{ formatDate(asset.purchaseDate) }}</span>
+                </div>
+                <div class="info-item">
+                  <span class="info-label">价值:</span>
+                  <span class="info-value">{{ formatCurrency(asset.value) }}</span>
+                </div>
+                <div class="info-item">
+                  <span class="info-label">部门:</span>
+                  <span class="info-value">{{ asset.department }}</span>
+                </div>
+                @if (asset.assignedToName) {
+                  <div class="info-item">
+                    <span class="info-label">使用人:</span>
+                    <span class="info-value">{{ asset.assignedToName }}</span>
+                  </div>
+                }
+                @if (asset.status === '故障' || asset.status === '报修中') {
+                  <div class="info-item faulty-notice">
+                    <span class="info-label">故障描述:</span>
+                    <span class="info-value">需要维修</span>
+                  </div>
+                }
+              </div>
             </div>
-          </td>
-        </ng-container>
-        <ng-container matColumnDef="type">
-          <th mat-header-cell *matHeaderCellDef>类型</th>
-          <td mat-cell *matCellDef="let asset">{{ asset.type }}</td>
-        </ng-container>
-        <ng-container matColumnDef="status">
-          <th mat-header-cell *matHeaderCellDef>状态</th>
-          <td mat-cell *matCellDef="let asset">
-            <span class="status-badge" [class]="getStatusClass(asset.status)">
-              {{ asset.status }}
-            </span>
-          </td>
-        </ng-container>
-        <ng-container matColumnDef="department">
-          <th mat-header-cell *matHeaderCellDef>部门</th>
-          <td mat-cell *matCellDef="let asset">{{ asset.department }}</td>
-        </ng-container>
-        <ng-container matColumnDef="assignedTo">
-          <th mat-header-cell *matHeaderCellDef>使用人</th>
-          <td mat-cell *matCellDef="let asset">{{ asset.assignedToName || '-' }}</td>
-        </ng-container>
-        <ng-container matColumnDef="purchaseDate">
-          <th mat-header-cell *matHeaderCellDef>购买日期</th>
-          <td mat-cell *matCellDef="let asset">{{ formatDate(asset.purchaseDate) }}</td>
-        </ng-container>
-        <ng-container matColumnDef="value">
-          <th mat-header-cell *matHeaderCellDef>价值</th>
-          <td mat-cell *matCellDef="let asset">{{ formatCurrency(asset.value) }}</td>
-        </ng-container>
-        <ng-container matColumnDef="actions">
-          <th mat-header-cell *matHeaderCellDef>操作</th>
-          <td mat-cell *matCellDef="let asset" class="actions-column">
-            <div class="action-buttons-list">
+            <div class="asset-actions">
               <button mat-icon-button matTooltip="查看详情" class="action-btn">
                 <mat-icon>visibility</mat-icon>
               </button>
-              <button mat-icon-button matTooltip="编辑信息" class="action-btn">
+              <button 
+                mat-icon-button 
+                matTooltip="编辑信息"
+                class="action-btn"
+              >
                 <mat-icon>edit</mat-icon>
               </button>
-              <ng-container *ngIf="asset.status === '空闲'">
+              @if (asset.status === '空闲') {
                 <button 
                   mat-icon-button 
                   matTooltip="分配资产"
@@ -300,8 +225,8 @@
                 >
                   <mat-icon>assignment_ind</mat-icon>
                 </button>
-              </ng-container>
-              <ng-container *ngIf="asset.status === '占用'">
+              }
+              @if (asset.status === '占用') {
                 <button 
                   mat-icon-button 
                   matTooltip="归还资产"
@@ -310,8 +235,8 @@
                 >
                   <mat-icon>undo</mat-icon>
                 </button>
-              </ng-container>
-              <ng-container *ngIf="asset.status === '故障'">
+              }
+              @if (asset.status === '故障') {
                 <button 
                   mat-icon-button 
                   matTooltip="申请报修"
@@ -320,24 +245,114 @@
                 >
                   <mat-icon>build</mat-icon>
                 </button>
-              </ng-container>
-            </div>
-          </td>
-        </ng-container>
-        
-        <tr mat-header-row *matHeaderRowDef="['name', 'type', 'status', 'department', 'assignedTo', 'purchaseDate', 'value', 'actions']"></tr>
-        <tr mat-row *matRowDef="let row; columns: ['name', 'type', 'status', 'department', 'assignedTo', 'purchaseDate', 'value', 'actions']"></tr>
-        
-        <tr class="mat-row" *matNoDataRow>
-          <td class="mat-cell" colspan="8" class="no-data">
-            <div class="empty-state">
-              <mat-icon>search_off</mat-icon>
-              <p>没有找到符合条件的资产</p>
+              }
             </div>
-          </td>
-        </tr>
-      </table>
-    </div>
+          </div>
+        }
+      </div>
+    }
+    
+    <!-- 列表视图 -->
+    @if (selectedView() === 'list') {
+      <div class="assets-list">
+        <table mat-table [dataSource]="filteredAssets()" class="assets-table">
+          <ng-container matColumnDef="name">
+            <th mat-header-cell *matHeaderCellDef>资产名称</th>
+            <td mat-cell *matCellDef="let asset">
+              <div class="asset-name-list">
+                <div class="asset-type-icon-small" [class]="asset.type">
+                  <mat-icon>{{ getTypeIcon(asset.type) }}</mat-icon>
+                </div>
+                <span>{{ asset.name }}</span>
+              </div>
+            </td>
+          </ng-container>
+          <ng-container matColumnDef="type">
+            <th mat-header-cell *matHeaderCellDef>类型</th>
+            <td mat-cell *matCellDef="let asset">{{ asset.type }}</td>
+          </ng-container>
+          <ng-container matColumnDef="status">
+            <th mat-header-cell *matHeaderCellDef>状态</th>
+            <td mat-cell *matCellDef="let asset">
+              <span class="status-badge" [class]="getStatusClass(asset.status)">
+                {{ asset.status }}
+              </span>
+            </td>
+          </ng-container>
+          <ng-container matColumnDef="department">
+            <th mat-header-cell *matHeaderCellDef>部门</th>
+            <td mat-cell *matCellDef="let asset">{{ asset.department }}</td>
+          </ng-container>
+          <ng-container matColumnDef="assignedTo">
+            <th mat-header-cell *matHeaderCellDef>使用人</th>
+            <td mat-cell *matCellDef="let asset">{{ asset.assignedToName || '-' }}</td>
+          </ng-container>
+          <ng-container matColumnDef="purchaseDate">
+            <th mat-header-cell *matHeaderCellDef>购买日期</th>
+            <td mat-cell *matCellDef="let asset">{{ formatDate(asset.purchaseDate) }}</td>
+          </ng-container>
+          <ng-container matColumnDef="value">
+            <th mat-header-cell *matHeaderCellDef>价值</th>
+            <td mat-cell *matCellDef="let asset">{{ formatCurrency(asset.value) }}</td>
+          </ng-container>
+          <ng-container matColumnDef="actions">
+            <th mat-header-cell *matHeaderCellDef>操作</th>
+            <td mat-cell *matCellDef="let asset" class="actions-column">
+              <div class="action-buttons-list">
+                <button mat-icon-button matTooltip="查看详情" class="action-btn">
+                  <mat-icon>visibility</mat-icon>
+                </button>
+                <button mat-icon-button matTooltip="编辑信息" class="action-btn">
+                  <mat-icon>edit</mat-icon>
+                </button>
+                @if (asset.status === '空闲') {
+                  <button 
+                    mat-icon-button 
+                    matTooltip="分配资产"
+                    class="action-btn primary"
+                    (click)="openAssignmentDialog(asset)"
+                  >
+                    <mat-icon>assignment_ind</mat-icon>
+                  </button>
+                }
+                @if (asset.status === '占用') {
+                  <button 
+                    mat-icon-button 
+                    matTooltip="归还资产"
+                    class="action-btn primary"
+                    (click)="openAssignmentDialog(asset, true)"
+                  >
+                    <mat-icon>undo</mat-icon>
+                  </button>
+                }
+                @if (asset.status === '故障') {
+                  <button 
+                    mat-icon-button 
+                    matTooltip="申请报修"
+                    class="action-btn warning"
+                    (click)="openRepairDialog(asset)"
+                  >
+                    <mat-icon>build</mat-icon>
+                  </button>
+                }
+              </div>
+            </td>
+          </ng-container>
+          
+          <tr mat-header-row *matHeaderRowDef="['name', 'type', 'status', 'department', 'assignedTo', 'purchaseDate', 'value', 'actions']"></tr>
+          <tr mat-row *matRowDef="let row; columns: ['name', 'type', 'status', 'department', 'assignedTo', 'purchaseDate', 'value', 'actions']"></tr>
+          
+          <tr class="mat-row" *matNoDataRow>
+            <td class="mat-cell" colspan="8" class="no-data">
+              <div class="empty-state">
+                <mat-icon>search_off</mat-icon>
+                <p>没有找到符合条件的资产</p>
+              </div>
+            </td>
+          </tr>
+        </table>
+      </div>
+    }
   </div>
 
   <!-- 统计图表区 -->
@@ -349,16 +364,18 @@
       </div>
       <div class="chart-content">
         <div class="type-stats">
-          <div *ngFor="let typeStat of assetStats().typeStatsArray" class="type-stat-item">
-            <div class="type-info">
-              <span class="type-name">{{ typeStat.type }}</span>
-              <span class="type-count">{{ typeStat.count }}台</span>
-            </div>
-            <div class="progress-bar">
-              <div class="progress-fill" [style.width]="(typeStat.count / assetStats().total * 100) + '%'"></div>
+          @for (typeStat of assetStats().typeStatsArray; track typeStat.type) {
+            <div class="type-stat-item">
+              <div class="type-info">
+                <span class="type-name">{{ typeStat.type }}</span>
+                <span class="type-count">{{ typeStat.count }}台</span>
+              </div>
+              <div class="progress-bar">
+                <div class="progress-fill" [style.width]="(typeStat.count / assetStats().total * 100) + '%'"></div>
+              </div>
+              <div class="type-value">{{ formatCurrency(typeStat.value) }}</div>
             </div>
-            <div class="type-value">{{ formatCurrency(typeStat.value) }}</div>
-          </div>
+          }
         </div>
       </div>
     </div>
@@ -370,16 +387,18 @@
       </div>
       <div class="chart-content">
         <div class="department-stats">
-          <div *ngFor="let deptStat of assetStats().departmentStatsArray" class="department-stat-item">
-            <div class="department-info">
-              <span class="department-name">{{ deptStat.department }}</span>
-              <span class="department-count">{{ deptStat.count }}台</span>
-            </div>
-            <div class="progress-bar">
-              <div class="progress-fill" [style.width]="(deptStat.count / assetStats().total * 100) + '%'"></div>
+          @for (deptStat of assetStats().departmentStatsArray; track deptStat.department) {
+            <div class="department-stat-item">
+              <div class="department-info">
+                <span class="department-name">{{ deptStat.department }}</span>
+                <span class="department-count">{{ deptStat.count }}台</span>
+              </div>
+              <div class="progress-bar">
+                <div class="progress-fill" [style.width]="(deptStat.count / assetStats().total * 100) + '%'" ></div>
+              </div>
+              <div class="department-value">{{ formatCurrency(deptStat.value) }}</div>
             </div>
-            <div class="department-value">{{ formatCurrency(deptStat.value) }}</div>
-          </div>
+          }
         </div>
       </div>
     </div>
@@ -391,16 +410,18 @@
       </div>
       <div class="chart-content">
         <div class="usage-stats">
-          <div *ngFor="let usage of assetStats().usageStats" class="usage-stat-item">
-            <div class="usage-info">
-              <span class="usage-type">{{ usage.type }}</span>
-            </div>
-            <div class="bar-container">
-              <div class="usage-bar" [style.height]="(usage.avgHours / 200 * 100) + '%'">
-                <span class="usage-value">{{ usage.avgHours }}h</span>
+          @for (usage of assetStats().usageStats; track usage.type) {
+            <div class="usage-stat-item">
+              <div class="usage-info">
+                <span class="usage-type">{{ usage.type }}</span>
+              </div>
+              <div class="bar-container">
+                <div class="usage-bar" [style.height]="(usage.avgHours / 200 * 100) + '%'">
+                  <span class="usage-value">{{ usage.avgHours }}h</span>
+                </div>
               </div>
             </div>
-          </div>
+          }
         </div>
       </div>
     </div>

+ 43 - 28
src/app/pages/hr/assets/assets.html

@@ -48,22 +48,28 @@
           (keyup.enter)="applyFilters()"
           class="search-input"
         >
-        <button mat-icon-button *ngIf="searchTerm()" (click)="searchTerm.set('')" class="clear-search">
-          <mat-icon>close</mat-icon>
-        </button>
+        @if (searchTerm()) {
+          <button mat-icon-button (click)="searchTerm.set('')" class="clear-search">
+            <mat-icon>close</mat-icon>
+          </button>
+        }
       </div>
       
       <div class="filter-container">
         <mat-select placeholder="部门" [value]="departmentFilter()" (selectionChange)="departmentFilter.set($event.value); applyFilters()">
           <mat-option value="">全部部门</mat-option>
-          <mat-option *ngFor="let dept of departments" [value]="dept.name">{{ dept.name }}</mat-option>
+          @for (dept of departments; track dept.name) {
+            <mat-option [value]="dept.name">{{ dept.name }}</mat-option>
+          }
         </mat-select>
       </div>
       
       <div class="filter-container">
         <mat-select placeholder="状态" [value]="statusFilter()" (selectionChange)="statusFilter.set($event.value); applyFilters()">
           <mat-option value="">全部状态</mat-option>
-          <mat-option *ngFor="let status of statuses" [value]="status">{{ status }}</mat-option>
+          @for (status of statuses; track status) {
+            <mat-option [value]="status">{{ status }}</mat-option>
+          }
         </mat-select>
       </div>
       
@@ -146,7 +152,9 @@
         <th mat-header-cell *matHeaderCellDef>状态</th>
         <td mat-cell *matCellDef="let employee">
           <mat-select [value]="employee.status" (selectionChange)="changeEmployeeStatus(employee, $event.value)" class="status-select">
-            <mat-option *ngFor="let status of statuses" [value]="status">{{ status }}</mat-option>
+            @for (status of statuses; track status) {
+              <mat-option [value]="status">{{ status }}</mat-option>
+            }
           </mat-select>
         </td>
       </ng-container>
@@ -155,18 +163,23 @@
       <ng-container matColumnDef="contract">
         <th mat-header-cell *matHeaderCellDef>合同</th>
         <td mat-cell *matCellDef="let employee">
-          <div *ngIf="employee.contract" class="contract-info">
-            <div class="contract-date">{{ formatDate(employee.contract.startDate) }} - {{ formatDate(employee.contract.endDate) }}</div>
-            <div *ngIf="employee.contract.isExpiringSoon" class="expiring-soon" matTooltip="合同即将到期">
-              ⚠️ 即将到期
+          @if (employee.contract) {
+            <div class="contract-info">
+              <div class="contract-date">{{ formatDate(employee.contract.startDate) }} - {{ formatDate(employee.contract.endDate) }}</div>
+              @if (employee.contract.isExpiringSoon) {
+                <div class="expiring-soon" matTooltip="合同即将到期">
+                  ⚠️ 即将到期
+                </div>
+              }
+              <button mat-icon-button class="contract-btn" matTooltip="查看合同">
+                <mat-icon>description</mat-icon>
+              </button>
             </div>
-            <button mat-icon-button class="contract-btn" matTooltip="查看合同">
-              <mat-icon>description</mat-icon>
-            </button>
-          </div>
-          <div *ngIf="!employee.contract" class="no-contract">
-            无合同信息
-          </div>
+          } @else {
+            <div class="no-contract">
+              无合同信息
+            </div>
+          }
         </td>
       </ng-container>
 
@@ -192,17 +205,19 @@
       <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
 
       <!-- 空数据状态 -->
-      <tr class="mat-row" *matNoDataRow>
-        <td class="mat-cell empty-state" [attr.colspan]="displayedColumns.length">
-          <div class="empty-icon">
-            <mat-icon>search_off</mat-icon>
-          </div>
-          <p>没有找到符合条件的员工</p>
-          <button mat-button (click)="searchTerm.set(''); departmentFilter.set(''); statusFilter.set(''); applyFilters()">
-            清除筛选条件
-          </button>
-        </td>
-      </tr>
+      @if (filteredEmployees().length === 0) {
+        <tr class="mat-row">
+          <td class="mat-cell empty-state" [attr.colspan]="displayedColumns.length">
+            <div class="empty-icon">
+              <mat-icon>search_off</mat-icon>
+            </div>
+            <p>没有找到符合条件的员工</p>
+            <button mat-button (click)="searchTerm.set(''); departmentFilter.set(''); statusFilter.set(''); applyFilters()">
+              清除筛选条件
+            </button>
+          </td>
+        </tr>
+      }
     </table>
   </div>
 

+ 50 - 41
src/app/pages/hr/attendance/attendance.html

@@ -64,32 +64,37 @@
       
       <!-- 日历表头 -->
       <div class="calendar-header">
-        <div class="weekday" *ngFor="let i of [0, 1, 2, 3, 4, 5, 6]">
-          {{ getWeekdayName(i) }}
-        </div>
+        @for (i of [0, 1, 2, 3, 4, 5, 6]; track i) {
+          <div class="weekday">
+            {{ getWeekdayName(i) }}
+          </div>
+        }
       </div>
       
       <!-- 日历格子 -->
       <div class="calendar-grid">
-        <div 
-          *ngFor="let day of getCalendarDays()"
-          class="calendar-day"
-          [class.current-month]="day.currentMonth"
-          [class.other-month]="!day.currentMonth"
-          [class.today]="isToday(day.date)"
-          [class.has-attendance]="day.attendance"
-          [class.absent]="day.attendance && day.attendance.status === '旷工'"
-          [class.late]="day.attendance && day.attendance.status === '迟到'"
-          [class.early]="day.attendance && day.attendance.status === '早退'"
-          [class.leave]="day.attendance && day.attendance.status === '请假'"
-          [class.normal]="day.attendance && day.attendance.status === '正常'"
-          (click)="selectedDate.set(day.date)"
-          matTooltip="{{ getDayTooltip(day) }}"
-          matTooltipPosition="above"
-        >
-          <span class="day-number">{{ day.dayOfMonth }}</span>
-          <div *ngIf="day.attendance" class="attendance-indicator"></div>
-        </div>
+        @for (day of getCalendarDays(); track day.date) {
+          <div 
+            class="calendar-day"
+            [class.current-month]="day.currentMonth"
+            [class.other-month]="!day.currentMonth"
+            [class.today]="isToday(day.date)"
+            [class.has-attendance]="day.attendance"
+            [class.absent]="day.attendance && day.attendance.status === '旷工'"
+            [class.late]="day.attendance && day.attendance.status === '迟到'"
+            [class.early]="day.attendance && day.attendance.status === '早退'"
+            [class.leave]="day.attendance && day.attendance.status === '请假'"
+            [class.normal]="day.attendance && day.attendance.status === '正常'"
+            (click)="selectedDate.set(day.date)"
+            matTooltip="{{ getDayTooltip(day) }}"
+            matTooltipPosition="above"
+          >
+            <span class="day-number">{{ day.dayOfMonth }}</span>
+            @if (day.attendance) {
+              <div class="attendance-indicator"></div>
+            }
+          </div>
+        }
       </div>
       
       <!-- 图例 -->
@@ -155,21 +160,23 @@
           <h2>部门考勤对比</h2>
         </div>
         <div class="department-chart">
-          <div *ngFor="let dept of departmentAttendanceData()" class="dept-bar-container">
-            <div class="dept-info">
-              <span class="dept-name">{{ dept.department }}</span>
-              <span class="dept-rate">{{ dept.complianceRate }}%</span>
+          @for (dept of departmentAttendanceData(); track dept.department) {
+            <div class="dept-bar-container">
+              <div class="dept-info">
+                <span class="dept-name">{{ dept.department }}</span>
+                <span class="dept-rate">{{ dept.complianceRate }}%</span>
+              </div>
+              <div class="progress-bar">
+                <div 
+                  class="progress-fill"
+                  [style.width]="dept.complianceRate + '%'"
+                ></div>
+              </div>
+              <div class="dept-stats">
+                正常 {{ dept.compliant }} / 总计 {{ dept.total }}
+              </div>
             </div>
-            <div class="progress-bar">
-              <div 
-                class="progress-fill"
-                [style.width]="dept.complianceRate + '%'"
-              ></div>
-            </div>
-            <div class="dept-stats">
-              正常 {{ dept.compliant }} / 总计 {{ dept.total }}
-            </div>
-          </div>
+          }
         </div>
       </div>
       
@@ -223,11 +230,13 @@
             <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
             <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
             
-            <tr class="mat-row" *matNoDataRow>
-              <td class="mat-cell no-data" [attr.colspan]="displayedColumns.length">
-                暂无考勤异常记录
-              </td>
-            </tr>
+            @if (exceptionAttendance().length === 0) {
+              <tr class="mat-row">
+                <td class="mat-cell no-data" [attr.colspan]="displayedColumns.length">
+                  暂无考勤异常记录
+                </td>
+              </tr>
+            }
           </table>
         </div>
       </div>

+ 106 - 82
src/app/pages/hr/dashboard/dashboard.html

@@ -38,39 +38,47 @@
         <div class="card-section">
           <h3>流程进度追踪</h3>
           <div class="process-tracking">
-            <div *ngFor="let process of pendingProcesses()" class="process-item">
-              <div class="process-header">
-                <span class="process-title">
-                  {{process.type === 'hire' ? '待审核入职申请' : '待办理离职手续'}}
-                </span>
-                <span class="process-count">{{process.count}}个</span>
+            @for (process of pendingProcesses(); track process) {
+              <div class="process-item">
+                <div class="process-header">
+                  <span class="process-title">
+                    {{process.type === 'hire' ? '待审核入职申请' : '待办理离职手续'}}
+                  </span>
+                  <span class="process-count">{{process.count}}个</span>
+                </div>
+                <mat-progress-bar [value]="process.progress" color="primary"></mat-progress-bar>
+                <button mat-button color="primary" class="process-action" (click)="navigateToProcess(process.type)">
+                  查看详情
+                </button>
               </div>
-              <mat-progress-bar [value]="process.progress" color="primary"></mat-progress-bar>
-              <button mat-button color="primary" class="process-action" (click)="navigateToProcess(process.type)">
-                查看详情
-              </button>
-            </div>
+            }
           </div>
         </div>
 
         <div class="card-section">
           <h3>风险预警</h3>
           <div class="risk-alerts">
-            <div *ngFor="let alert of riskAlerts()" class="risk-alert">
-              <div class="alert-header">
-                <mat-icon color="warn">warning</mat-icon>
-                <span class="alert-title">{{alert.type}}</span>
-                <span class="alert-count">{{alert.count}}人</span>
-              </div>
-              <div class="alert-employees">
-                <div *ngFor="let employee of alert.employees" class="employee-item">
-                  <span class="employee-name">{{employee.name}}</span>
-                  <span class="employee-position">{{employee.position}}</span>
-                  <span class="employee-department">{{employee.department}}</span>
-                  <span *ngIf="employee.daysLeft" class="days-left">剩余{{employee.daysLeft}}天</span>
+            @for (alert of riskAlerts(); track alert) {
+              <div class="risk-alert">
+                <div class="alert-header">
+                  <mat-icon color="warn">warning</mat-icon>
+                  <span class="alert-title">{{alert.type}}</span>
+                  <span class="alert-count">{{alert.count}}人</span>
+                </div>
+                <div class="alert-employees">
+                  @for (employee of alert.employees; track employee) {
+                    <div class="employee-item">
+                      <span class="employee-name">{{employee.name}}</span>
+                      <span class="employee-position">{{employee.position}}</span>
+                      <span class="employee-department">{{employee.department}}</span>
+                      @if (employee.daysLeft) {
+                        <span class="days-left">剩余{{employee.daysLeft}}天</span>
+                      }
+                    </div>
+                  }
                 </div>
               </div>
-            </div>
+            }
           </div>
         </div>
       </mat-card-content>
@@ -90,19 +98,21 @@
         <div class="card-section">
           <h3>核心指标展示</h3>
           <div class="metrics-container">
-            <div *ngFor="let metric of performanceMetrics()" class="metric-item">
-              <div class="metric-header">
-                <span class="metric-name">{{metric.name}}</span>
-                <div class="metric-values">
-                  <span class="metric-actual">{{metric.actual}}{{metric.unit}}</span>
-                  <span class="metric-target">目标: {{metric.target}}{{metric.unit}}</span>
+            @for (metric of performanceMetrics(); track metric) {
+              <div class="metric-item">
+                <div class="metric-header">
+                  <span class="metric-name">{{metric.name}}</span>
+                  <div class="metric-values">
+                    <span class="metric-actual">{{metric.actual}}{{metric.unit}}</span>
+                    <span class="metric-target">目标: {{metric.target}}{{metric.unit}}</span>
+                  </div>
                 </div>
+                <mat-progress-bar
+                  [value]="(metric.actual / metric.target) * 100"
+                  [color]="getProgressColor(metric.actual, metric.target)">
+                </mat-progress-bar>
               </div>
-              <mat-progress-bar 
-                [value]="(metric.actual / metric.target) * 100" 
-                [color]="getProgressColor(metric.actual, metric.target)">
-              </mat-progress-bar>
-            </div>
+            }
           </div>
 
           <div class="department-performance">
@@ -114,12 +124,14 @@
                 <div class="table-cell">准时交付率</div>
                 <div class="table-cell">客户满意度</div>
               </div>
-              <div *ngFor="let dept of departmentPerformance()" class="table-row">
-                <div class="table-cell">{{dept.name}}</div>
-                <div class="table-cell">{{dept.excellentWorkRate}}%</div>
-                <div class="table-cell">{{dept.deliveryOnTimeRate}}%</div>
-                <div class="table-cell">{{dept.customerSatisfaction}}</div>
-              </div>
+              @for (dept of departmentPerformance(); track dept) {
+                <div class="table-row">
+                  <div class="table-cell">{{dept.name}}</div>
+                  <div class="table-cell">{{dept.excellentWorkRate}}%</div>
+                  <div class="table-cell">{{dept.deliveryOnTimeRate}}%</div>
+                  <div class="table-cell">{{dept.customerSatisfaction}}</div>
+                </div>
+              }
             </div>
           </div>
         </div>
@@ -133,29 +145,35 @@
         <div class="card-section">
           <h3>扣分项预警</h3>
           <div class="penalty-warnings">
-            <div *ngFor="let warning of penaltyWarnings()" class="warning-item">
-              <div class="warning-header">
-                <mat-icon color="warn">error</mat-icon>
-                <span class="warning-issue">{{warning.issue}}</span>
-              </div>
-              <div class="warning-details">
-                <div class="warning-departments">
-                  <h4>高发部门</h4>
-                  <div *ngFor="let dept of warning.departments" class="department-item">
-                    <span class="department-name">{{dept.name}}</span>
-                    <span class="department-count">{{dept.count}}次</span>
-                  </div>
+            @for (warning of penaltyWarnings(); track warning) {
+              <div class="warning-item">
+                <div class="warning-header">
+                  <mat-icon color="warn">error</mat-icon>
+                  <span class="warning-issue">{{warning.issue}}</span>
                 </div>
-                <div class="warning-employees">
-                  <h4>高发个人</h4>
-                  <div *ngFor="let emp of warning.employees" class="employee-item">
-                    <span class="employee-name">{{emp.name}}</span>
-                    <span class="employee-department">{{emp.department}}</span>
-                    <span class="employee-count">{{emp.count}}次</span>
+                <div class="warning-details">
+                  <div class="warning-departments">
+                    <h4>高发部门</h4>
+                    @for (dept of warning.departments; track dept) {
+                      <div class="department-item">
+                        <span class="department-name">{{dept.name}}</span>
+                        <span class="department-count">{{dept.count}}次</span>
+                      </div>
+                    }
+                  </div>
+                  <div class="warning-employees">
+                    <h4>高发个人</h4>
+                    @for (emp of warning.employees; track emp) {
+                      <div class="employee-item">
+                        <span class="employee-name">{{emp.name}}</span>
+                        <span class="employee-department">{{emp.department}}</span>
+                        <span class="employee-count">{{emp.count}}次</span>
+                      </div>
+                    }
                   </div>
                 </div>
               </div>
-            </div>
+            }
           </div>
         </div>
       </mat-card-content>
@@ -176,18 +194,22 @@
           <h3>员工结构分析</h3>
           <div class="structure-analysis">
             <mat-tab-group>
-              <mat-tab *ngFor="let structure of employeeStructures()" [label]="structure.category">
-                <div class="structure-data">
-                  <div *ngFor="let item of structure.data" class="structure-item">
-                    <div class="structure-header">
-                      <span class="structure-name">{{item.name}}</span>
-                      <span class="structure-count">{{item.count}}人</span>
-                    </div>
-                    <mat-progress-bar value="{{item.percentage}}" color="primary"></mat-progress-bar>
-                    <span class="structure-percentage">{{item.percentage}}%</span>
+              @for (structure of employeeStructures(); track structure) {
+                <mat-tab [label]="structure.category">
+                  <div class="structure-data">
+                    @for (item of structure.data; track item) {
+                      <div class="structure-item">
+                        <div class="structure-header">
+                          <span class="structure-name">{{item.name}}</span>
+                          <span class="structure-count">{{item.count}}人</span>
+                        </div>
+                        <mat-progress-bar value="{{item.percentage}}" color="primary"></mat-progress-bar>
+                        <span class="structure-percentage">{{item.percentage}}%</span>
+                      </div>
+                    }
                   </div>
-                </div>
-              </mat-tab>
+                </mat-tab>
+              }
             </mat-tab-group>
           </div>
         </div>
@@ -201,18 +223,20 @@
         <div class="card-section">
           <h3>待办事项提醒</h3>
           <div class="todo-items">
-            <div *ngFor="let item of todoItems()" class="todo-item" [ngClass]="getPriorityClass(item.priority)">
-              <div class="todo-header">
-                <span class="todo-task">{{item.task}}</span>
-                <mat-chip-set>
-                  <mat-chip>{{item.type}}</mat-chip>
-                </mat-chip-set>
-              </div>
-              <div class="todo-footer">
-                <span class="todo-due-date">截止日期: {{item.dueDate | date:'yyyy-MM-dd'}}</span>
-                <span class="todo-priority">{{item.priority === 'high' ? '紧急' : item.priority === 'medium' ? '中等' : '普通'}}</span>
+            @for (item of todoItems(); track item) {
+              <div class="todo-item" [ngClass]="getPriorityClass(item.priority)">
+                <div class="todo-header">
+                  <span class="todo-task">{{item.task}}</span>
+                  <mat-chip-set>
+                    <mat-chip>{{item.type}}</mat-chip>
+                  </mat-chip-set>
+                </div>
+                <div class="todo-footer">
+                  <span class="todo-due-date">截止日期: {{item.dueDate | date:'yyyy-MM-dd'}}</span>
+                  <span class="todo-priority">{{item.priority === 'high' ? '紧急' : item.priority === 'medium' ? '中等' : '普通'}}</span>
+                </div>
               </div>
-            </div>
+            }
           </div>
         </div>
       </mat-card-content>

+ 1 - 1
tsconfig.app.json

@@ -4,7 +4,7 @@
   "extends": "./tsconfig.json",
   "compilerOptions": {
     "outDir": "./out-tsc/app",
-    "types": ["echarts"]
+    "types": ["echarts", "qrcode"]
   },
   "include": [
     "src/**/*.ts"