Преглед изворни кода

Merge branch 'master' of http://git.fmode.cn:3000/nkkj/yss-project

0235711 пре 1 дан
родитељ
комит
fe1e38925a

+ 7 - 1
.trae/rules/project_rules.md

@@ -1,2 +1,8 @@
+<<<<<<< HEAD
 
-模板页面显示的条件和循环if和for统一用控制流的@if和@for
+模板页面显示的条件和循环if和for统一用控制流的@if和@for
+=======
+# 项目规范
+
+//模板页面显示的条件和循环采用控制流的@if和@for
+>>>>>>> c955fbffd90c3e0d8285002bb4c199a356069c1a

+ 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",

+ 2 - 2
src/app/app.routes.ts

@@ -39,7 +39,7 @@ import { HrLayout } from './pages/hr/hr-layout/hr-layout';
 import { Dashboard as HrDashboard } from './pages/hr/dashboard/dashboard';
 import { EmployeeRecords } from './pages/hr/employee-records/employee-records';
 import { Attendance } from './pages/hr/attendance/attendance';
-import { Assets } from './pages/hr/assets/assets';
+
 import { DesignerProfile } from './pages/hr/designer-profile/designer-profile';
 
 // 管理员页面
@@ -121,7 +121,7 @@ export const routes: Routes = [
       { path: 'dashboard', component: HrDashboard, title: '人事看板' },
       { path: 'employee-records', component: EmployeeRecords, title: '花名册与档案库' },
       { path: 'attendance', component: Attendance, title: '考勤统计' },
-      { path: 'assets', component: Assets, title: '资产管理' },
+
       { path: 'designer-profile/:id', component: DesignerProfile, title: '设计师详情' }
     ]
   },

+ 1 - 1
src/app/pages/auth/login/login.ts

@@ -40,7 +40,7 @@ export class LoginPage {
       'designer': '/designer/dashboard',
       'team-leader': '/team-leader/dashboard',
       'finance': '/finance/dashboard',
-      'hr': '/hr/assets',
+      'hr': '/hr/dashboard',
       'admin': '/admin/dashboard'
     };
 

+ 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();
   }

+ 0 - 408
src/app/pages/hr/assets/assets-stats.html

@@ -1,408 +0,0 @@
-<div class="assets-stats-container">
-  <header class="page-header">
-    <h1>资产管理与统计</h1>
-    <p class="page-description">管理企业全类型资产,查看资产使用状态和统计数据</p>
-  </header>
-
-  <!-- 资产分类筛选栏 -->
-  <div class="filter-bar">
-    <div class="search-container">
-      <mat-icon class="search-icon">search</mat-icon>
-      <input 
-        matInput 
-        placeholder="搜索资产名称、序列号、使用人..."
-        [value]="searchTerm()"
-        (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>
-    </div>
-    
-    <div class="filter-controls">
-      <div class="filter-group">
-        <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>
-        </select>
-      </div>
-      
-      <div class="filter-group">
-        <label>资产状态:</label>
-        <select [value]="statusFilter()" (change)="statusFilter.set($any($event.target).value)" class="filter-select">
-          <option value="">全部状态</option>
-          <option value="空闲">空闲</option>
-          <option value="占用">占用</option>
-          <option value="故障">故障</option>
-          <option value="报修中">报修中</option>
-        </select>
-      </div>
-      
-      <div class="filter-group">
-        <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>
-        </select>
-      </div>
-      
-      <button mat-button color="primary" (click)="resetFilters()" class="reset-btn">
-        <mat-icon>refresh</mat-icon>
-        重置筛选
-      </button>
-      
-      <button mat-raised-button color="primary" (click)="exportAssetLedger()" class="export-btn">
-        <mat-icon>file_download</mat-icon>
-        导出台账
-      </button>
-    </div>
-  </div>
-
-  <!-- 视图切换和统计卡片 -->
-  <div class="view-stats-section">
-    <div class="view-toggle">
-      <button 
-        mat-button 
-        [class.active]="selectedView() === 'grid'"
-        (click)="switchView('grid')"
-        class="view-btn"
-      >
-        <mat-icon>grid_view</mat-icon>
-        卡片视图
-      </button>
-      <button 
-        mat-button 
-        [class.active]="selectedView() === 'list'"
-        (click)="switchView('list')"
-        class="view-btn"
-      >
-        <mat-icon>list</mat-icon>
-        列表视图
-      </button>
-      <div class="asset-count">
-        共 {{ filteredAssets().length }} 项资产
-      </div>
-    </div>
-    
-    <div class="stats-cards">
-      <div class="stat-card">
-        <div class="stat-icon">
-          <mat-icon>category</mat-icon>
-        </div>
-        <div class="stat-content">
-          <div class="stat-value">{{ assetStats().total }}</div>
-          <div class="stat-label">资产总数</div>
-        </div>
-      </div>
-      <div class="stat-card occupied">
-        <div class="stat-icon">
-          <mat-icon>person</mat-icon>
-        </div>
-        <div class="stat-content">
-          <div class="stat-value">{{ assetStats().occupied }}</div>
-          <div class="stat-label">占用中</div>
-          <div class="stat-percentage">{{ Math.round(assetStats().occupied / assetStats().total * 100) }}%</div>
-        </div>
-      </div>
-      <div class="stat-card idle">
-        <div class="stat-icon">
-          <mat-icon>schedule</mat-icon>
-        </div>
-        <div class="stat-content">
-          <div class="stat-value">{{ assetStats().idle }}</div>
-          <div class="stat-label">空闲</div>
-          <div class="stat-percentage">{{ Math.round(assetStats().idle / assetStats().total * 100) }}%</div>
-        </div>
-      </div>
-      <div class="stat-card faulty">
-        <div class="stat-icon">
-          <mat-icon>error</mat-icon>
-        </div>
-        <div class="stat-content">
-          <div class="stat-value">{{ assetStats().faulty }}</div>
-          <div class="stat-label">故障</div>
-          <div class="stat-percentage">{{ Math.round(assetStats().faulty / assetStats().total * 100) }}%</div>
-        </div>
-      </div>
-      <div class="stat-card repairing">
-        <div class="stat-icon">
-          <mat-icon>build</mat-icon>
-        </div>
-        <div class="stat-content">
-          <div class="stat-value">{{ assetStats().repairing }}</div>
-          <div class="stat-label">报修中</div>
-          <div class="stat-percentage">{{ Math.round(assetStats().repairing / assetStats().total * 100) }}%</div>
-        </div>
-      </div>
-      <div class="stat-card value">
-        <div class="stat-icon">
-          <mat-icon>monetization_on</mat-icon>
-        </div>
-        <div class="stat-content">
-          <div class="stat-value">{{ formatCurrency(assetStats().totalValue) }}</div>
-          <div class="stat-label">资产总值</div>
-        </div>
-      </div>
-    </div>
-  </div>
-
-  <!-- 资产列表/网格视图 -->
-  <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"
-          >
-            <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">
-                <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>
-              <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>
-          </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 class="stats-section">
-    <!-- 按类型统计 -->
-    <div class="stats-card">
-      <div class="card-header">
-        <h2>资产类型统计</h2>
-      </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>
-            </div>
-            <div class="type-value">{{ formatCurrency(typeStat.value) }}</div>
-          </div>
-        </div>
-      </div>
-    </div>
-    
-    <!-- 按部门统计 -->
-    <div class="stats-card">
-      <div class="card-header">
-        <h2>部门资产统计</h2>
-      </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>
-            </div>
-            <div class="department-value">{{ formatCurrency(deptStat.value) }}</div>
-          </div>
-        </div>
-      </div>
-    </div>
-    
-    <!-- 资产使用时长统计 -->
-    <div class="stats-card">
-      <div class="card-header">
-        <h2>资产平均使用时长(小时/月)</h2>
-      </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>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</div>

+ 0 - 1119
src/app/pages/hr/assets/assets-stats.scss

@@ -1,1119 +0,0 @@
-// 自定义主题
-$primary-color: #1e40af; // 深蓝主色,传递可靠感
-$primary-light: #3b82f6; // 浅蓝色,用于悬停效果
-$secondary-color: #0d9488; // 薄荷绿,作为强调色
-$success-color: #10b981; // 成功色
-$warning-color: #f59e0b; // 警告色
-$error-color: #ef4444; // 错误色
-$info-color: #3b82f6; // 信息色
-$text-primary: #1f2937; // 主要文本色
-$text-secondary: #4b5563; // 次要文本色
-$text-tertiary: #9ca3af; // 辅助文本色
-$bg-primary: #ffffff; // 主背景色
-$bg-secondary: #f9fafb; // 次要背景色
-$bg-tertiary: #f3f4f6; // 辅助背景色
-$border-color: #e5e7eb; // 边框色
-$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
-$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
-$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
-$border-radius: 8px;
-$transition: all 0.2s ease;
-
-// 主容器样式
-.assets-stats-container {
-  padding: 24px;
-  min-height: 100vh;
-  background-color: $bg-secondary;
-  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-}
-
-// 页面标题
-.page-header {
-  margin-bottom: 32px;
-  text-align: center;
-
-  h1 {
-    font-size: 32px;
-    font-weight: 700;
-    color: $text-primary;
-    margin: 0 0 8px 0;
-    letter-spacing: -0.5px;
-  }
-
-  .page-description {
-    font-size: 16px;
-    color: $text-secondary;
-    margin: 0;
-  }
-}
-
-// 资产分类筛选栏
-.filter-bar {
-  background-color: $bg-primary;
-  border-radius: $border-radius;
-  box-shadow: $shadow-sm;
-  padding: 20px;
-  margin-bottom: 24px;
-  transition: $transition;
-
-  &:hover {
-    box-shadow: $shadow-md;
-  }
-
-  .search-container {
-    position: relative;
-    max-width: 400px;
-    margin: 0 auto 20px;
-
-    .search-icon {
-      position: absolute;
-      left: 12px;
-      top: 50%;
-      transform: translateY(-50%);
-      color: $text-tertiary;
-      z-index: 1;
-    }
-
-    .search-input {
-      width: 100%;
-      padding: 12px 12px 12px 40px;
-      border: 1px solid $border-color;
-      border-radius: $border-radius;
-      font-size: 14px;
-      color: $text-primary;
-      background-color: $bg-primary;
-      transition: $transition;
-      box-sizing: border-box;
-
-      &:focus {
-        outline: none;
-        border-color: $primary-color;
-        box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
-        transform: scale(1.01);
-      }
-
-      &::placeholder {
-        color: $text-tertiary;
-      }
-    }
-
-    .clear-search {
-      position: absolute;
-      right: 8px;
-      top: 50%;
-      transform: translateY(-50%);
-      color: $text-tertiary;
-      padding: 4px;
-      transition: $transition;
-
-      &:hover {
-        color: $text-primary;
-        background-color: $bg-tertiary;
-        border-radius: 50%;
-      }
-    }
-  }
-
-  .filter-controls {
-    display: flex;
-    flex-wrap: wrap;
-    gap: 16px;
-    justify-content: center;
-    align-items: end;
-
-    .filter-group {
-      display: flex;
-      flex-direction: column;
-      gap: 6px;
-      min-width: 150px;
-
-      label {
-        font-size: 12px;
-        font-weight: 500;
-        color: $text-secondary;
-        text-transform: uppercase;
-        letter-spacing: 0.5px;
-      }
-
-      .filter-select {
-        padding: 10px 12px;
-        border: 1px solid $border-color;
-        border-radius: $border-radius;
-        font-size: 14px;
-        color: $text-primary;
-        background-color: $bg-primary;
-        transition: $transition;
-        cursor: pointer;
-        min-width: 150px;
-
-        &:focus {
-          outline: none;
-          border-color: $primary-color;
-          box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
-        }
-
-        &:hover {
-          border-color: $primary-light;
-        }
-      }
-    }
-
-    .reset-btn {
-      padding: 8px 16px;
-      border-radius: $border-radius;
-      font-size: 14px;
-      font-weight: 500;
-      color: $primary-color;
-      transition: $transition;
-
-      &:hover {
-        background-color: color-mix(in srgb, $primary-color 5%, transparent);
-      }
-    }
-
-    .export-btn {
-      padding: 8px 16px;
-      border-radius: $border-radius;
-      font-size: 14px;
-      font-weight: 500;
-      background-color: $primary-color;
-      color: white;
-      transition: $transition;
-
-      &:hover {
-        background-color: $primary-light;
-        transform: translateY(-1px);
-        box-shadow: $shadow-md;
-      }
-
-      &:active {
-        transform: scale(0.98);
-        box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.1);
-      }
-    }
-  }
-}
-
-// 视图切换和统计卡片
-.view-stats-section {
-  margin-bottom: 24px;
-}
-
-.view-toggle {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  margin-bottom: 24px;
-  padding: 12px 20px;
-  background-color: $bg-primary;
-  border-radius: $border-radius;
-  box-shadow: $shadow-sm;
-
-  .view-btn {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    padding: 8px 16px;
-    border-radius: $border-radius;
-    font-size: 14px;
-    font-weight: 500;
-    color: $text-secondary;
-    transition: $transition;
-
-    &.active {
-      background-color: $primary-color;
-      color: white;
-      box-shadow: $shadow-sm;
-    }
-
-    &:not(.active):hover {
-      background-color: $bg-tertiary;
-      color: $text-primary;
-    }
-  }
-
-  .asset-count {
-    margin-left: auto;
-    font-size: 14px;
-    color: $text-secondary;
-    font-weight: 500;
-  }
-}
-
-.stats-cards {
-  display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
-  gap: 16px;
-
-  .stat-card {
-    background-color: $bg-primary;
-    border-radius: $border-radius;
-    padding: 20px;
-    box-shadow: $shadow-sm;
-    display: flex;
-    align-items: center;
-    gap: 16px;
-    transition: $transition;
-    position: relative;
-    overflow: hidden;
-
-    &::before {
-      content: '';
-      position: absolute;
-      top: 0;
-      left: 0;
-      width: 4px;
-      height: 100%;
-      background-color: $primary-color;
-    }
-
-    &:hover {
-      transform: translateY(-4px);
-      box-shadow: $shadow-md;
-    }
-
-    .stat-icon {
-      width: 40px;
-      height: 40px;
-      border-radius: 50%;
-      background-color: color-mix(in srgb, $primary-color 10%, transparent);
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      color: $primary-color;
-      flex-shrink: 0;
-    }
-
-    .stat-content {
-      flex: 1;
-
-      .stat-value {
-        font-size: 24px;
-        font-weight: 700;
-        color: $text-primary;
-        margin-bottom: 4px;
-        line-height: 1;
-      }
-
-      .stat-label {
-        font-size: 12px;
-        color: $text-secondary;
-        font-weight: 500;
-        text-transform: uppercase;
-        letter-spacing: 0.5px;
-      }
-
-      .stat-percentage {
-        font-size: 12px;
-        color: $text-tertiary;
-        margin-top: 4px;
-      }
-    }
-
-    &.occupied {
-      &::before {
-        background-color: $primary-color;
-      }
-
-      .stat-icon {
-        background-color: color-mix(in srgb, $primary-color 10%, transparent);
-        color: $primary-color;
-      }
-    }
-
-    &.idle {
-      &::before {
-        background-color: $secondary-color;
-      }
-
-      .stat-icon {
-        background-color: color-mix(in srgb, $secondary-color 10%, transparent);
-        color: $secondary-color;
-      }
-    }
-
-    &.faulty {
-      &::before {
-        background-color: $error-color;
-      }
-
-      .stat-icon {
-        background-color: color-mix(in srgb, $error-color 10%, transparent);
-        color: $error-color;
-      }
-    }
-
-    &.repairing {
-      &::before {
-        background-color: $warning-color;
-      }
-
-      .stat-icon {
-        background-color: color-mix(in srgb, $warning-color 10%, transparent);
-        color: $warning-color;
-      }
-    }
-
-    &.value {
-      &::before {
-        background-color: $success-color;
-      }
-
-      .stat-icon {
-        background-color: color-mix(in srgb, $success-color 10%, transparent);
-        color: $success-color;
-      }
-    }
-  }
-}
-
-// 资产列表/网格视图
-.assets-view {
-  margin-bottom: 32px;
-}
-
-// 网格视图
-.assets-grid {
-  display: grid;
-  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
-  gap: 20px;
-}
-
-.asset-card {
-  background-color: $bg-primary;
-  border-radius: $border-radius;
-  box-shadow: $shadow-sm;
-  padding: 20px;
-  transition: $transition;
-  position: relative;
-  overflow: hidden;
-
-  &:hover {
-    transform: translateY(-4px);
-    box-shadow: $shadow-md;
-  }
-
-  &.faulty {
-    animation: faulty-pulse 2s infinite;
-  }
-
-  .asset-header {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    margin-bottom: 16px;
-
-    .asset-type-icon {
-      width: 48px;
-      height: 48px;
-      border-radius: 50%;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      font-size: 20px;
-      background-color: $bg-tertiary;
-      color: $primary-color;
-      transition: $transition;
-
-      &.电脑 {
-        background-color: color-mix(in srgb, $primary-color 10%, $bg-tertiary);
-        color: $primary-color;
-      }
-
-      &.外设 {
-        background-color: color-mix(in srgb, $secondary-color 10%, $bg-tertiary);
-        color: $secondary-color;
-      }
-
-      &.软件账号 {
-        background-color: color-mix(in srgb, $info-color 10%, $bg-tertiary);
-        color: $info-color;
-      }
-
-      &.域名 {
-        background-color: color-mix(in srgb, $warning-color 10%, $bg-tertiary);
-        color: $warning-color;
-      }
-
-      &.其他 {
-        background-color: color-mix(in srgb, $text-secondary 10%, $bg-tertiary);
-        color: $text-secondary;
-      }
-    }
-
-    .asset-status {
-      padding: 6px 12px;
-      border-radius: 16px;
-      font-size: 12px;
-      font-weight: 500;
-      text-align: center;
-      min-width: 60px;
-      transition: $transition;
-
-      &.status-idle {
-        background-color: color-mix(in srgb, $secondary-color 15%, transparent);
-        color: $secondary-color;
-      }
-
-      &.status-occupied {
-        background-color: color-mix(in srgb, $primary-color 15%, transparent);
-        color: $primary-color;
-      }
-
-      &.status-faulty {
-        background-color: color-mix(in srgb, $error-color 15%, transparent);
-        color: $error-color;
-      }
-
-      &.status-repairing {
-        background-color: color-mix(in srgb, $warning-color 15%, transparent);
-        color: $warning-color;
-      }
-    }
-  }
-
-  .asset-content {
-    margin-bottom: 16px;
-
-    .asset-name {
-      font-size: 18px;
-      font-weight: 600;
-      color: $text-primary;
-      margin: 0 0 12px 0;
-      line-height: 1.3;
-    }
-
-    .asset-info {
-      display: flex;
-      flex-direction: column;
-      gap: 8px;
-
-      .info-item {
-        display: flex;
-        justify-content: space-between;
-        align-items: center;
-        padding: 8px 0;
-        border-bottom: 1px solid $border-color;
-
-        .info-label {
-          font-size: 12px;
-          color: $text-tertiary;
-          font-weight: 500;
-        }
-
-        .info-value {
-          font-size: 12px;
-          color: $text-secondary;
-          font-weight: 500;
-        }
-      }
-
-      .info-item.faulty-notice {
-        background-color: color-mix(in srgb, $error-color 5%, transparent);
-        border-radius: 6px;
-        padding: 8px 12px;
-        border: none;
-
-        .info-value {
-          color: $error-color;
-        }
-      }
-    }
-  }
-
-  .asset-actions {
-    display: flex;
-    justify-content: flex-end;
-    gap: 8px;
-    padding-top: 16px;
-    border-top: 1px solid $border-color;
-
-    .action-btn {
-      width: 36px;
-      height: 36px;
-      border-radius: 50%;
-      color: $text-secondary;
-      background-color: $bg-tertiary;
-      transition: $transition;
-
-      &:hover {
-        background-color: $border-color;
-        color: $text-primary;
-        transform: scale(1.1);
-      }
-
-      &.primary {
-        color: $primary-color;
-        background-color: color-mix(in srgb, $primary-color 10%, $bg-tertiary);
-
-        &:hover {
-          background-color: $primary-color;
-          color: white;
-        }
-      }
-
-      &.warning {
-        color: $error-color;
-        background-color: color-mix(in srgb, $error-color 10%, $bg-tertiary);
-
-        &:hover {
-          background-color: $error-color;
-          color: white;
-        }
-      }
-    }
-  }
-}
-
-// 列表视图
-.assets-list {
-  background-color: $bg-primary;
-  border-radius: $border-radius;
-  box-shadow: $shadow-sm;
-  overflow: hidden;
-}
-
-.assets-table {
-  width: 100%;
-  min-width: 800px;
-
-  .mat-header-row {
-    background-color: $bg-tertiary;
-
-    th {
-      font-size: 12px;
-      font-weight: 600;
-      color: $text-secondary;
-      text-transform: uppercase;
-      letter-spacing: 0.5px;
-      padding: 12px 16px;
-      text-align: left;
-      border-bottom: 1px solid $border-color;
-    }
-  }
-
-  .mat-row {
-    transition: $transition;
-
-    &:hover {
-      background-color: $bg-tertiary;
-      transform: translateX(2px);
-    }
-
-    td {
-      padding: 12px 16px;
-      border-bottom: 1px solid $border-color;
-      font-size: 14px;
-      color: $text-primary;
-    }
-
-    &:last-child td {
-      border-bottom: none;
-    }
-  }
-
-  .asset-name-list {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-
-    .asset-type-icon-small {
-      width: 24px;
-      height: 24px;
-      border-radius: 50%;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      font-size: 14px;
-      background-color: $bg-tertiary;
-      color: $primary-color;
-
-      &.电脑 {
-        background-color: color-mix(in srgb, $primary-color 10%, $bg-tertiary);
-        color: $primary-color;
-      }
-
-      &.外设 {
-        background-color: color-mix(in srgb, $secondary-color 10%, $bg-tertiary);
-        color: $secondary-color;
-      }
-
-      &.软件账号 {
-        background-color: color-mix(in srgb, $info-color 10%, $bg-tertiary);
-        color: $info-color;
-      }
-
-      &.域名 {
-        background-color: color-mix(in srgb, $warning-color 10%, $bg-tertiary);
-        color: $warning-color;
-      }
-
-      &.其他 {
-        background-color: color-mix(in srgb, $text-secondary 10%, $bg-tertiary);
-        color: $text-secondary;
-      }
-    }
-  }
-
-  .status-badge {
-    padding: 4px 12px;
-    border-radius: 16px;
-    font-size: 12px;
-    font-weight: 500;
-    text-align: center;
-    display: inline-block;
-    transition: $transition;
-
-    &.status-idle {
-      background-color: color-mix(in srgb, $secondary-color 15%, transparent);
-      color: $secondary-color;
-    }
-
-    &.status-occupied {
-      background-color: color-mix(in srgb, $primary-color 15%, transparent);
-      color: $primary-color;
-    }
-
-    &.status-faulty {
-      background-color: color-mix(in srgb, $error-color 15%, transparent);
-      color: $error-color;
-    }
-
-    &.status-repairing {
-      background-color: color-mix(in srgb, $warning-color 15%, transparent);
-      color: $warning-color;
-    }
-  }
-
-  .actions-column {
-    text-align: right;
-  }
-
-  .action-buttons-list {
-    display: flex;
-    justify-content: flex-end;
-    gap: 4px;
-
-    .action-btn {
-      width: 32px;
-      height: 32px;
-      border-radius: 50%;
-      color: $text-secondary;
-      background-color: $bg-tertiary;
-      transition: $transition;
-
-      &:hover {
-        background-color: $border-color;
-        color: $text-primary;
-        transform: scale(1.1);
-      }
-
-      &.primary {
-        color: $primary-color;
-        background-color: color-mix(in srgb, $primary-color 10%, $bg-tertiary);
-
-        &:hover {
-          background-color: $primary-color;
-          color: white;
-        }
-      }
-
-      &.warning {
-        color: $error-color;
-        background-color: color-mix(in srgb, $error-color 10%, $bg-tertiary);
-
-        &:hover {
-          background-color: $error-color;
-          color: white;
-        }
-      }
-    }
-  }
-
-  .no-data {
-    text-align: center;
-    padding: 60px 20px;
-
-    .empty-state {
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-
-      mat-icon {
-        font-size: 48px;
-        color: $text-tertiary;
-        margin-bottom: 16px;
-      }
-
-      p {
-        color: $text-secondary;
-        font-size: 16px;
-        margin: 0;
-      }
-    }
-  }
-}
-
-// 统计图表区
-.stats-section {
-  display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
-  gap: 24px;
-}
-
-.stats-card {
-  background-color: $bg-primary;
-  border-radius: $border-radius;
-  box-shadow: $shadow-sm;
-  padding: 24px;
-  transition: $transition;
-
-  &:hover {
-    transform: translateY(-2px);
-    box-shadow: $shadow-md;
-  }
-
-  .card-header {
-    margin-bottom: 24px;
-
-    h2 {
-      font-size: 20px;
-      font-weight: 600;
-      color: $text-primary;
-      margin: 0;
-    }
-  }
-
-  .chart-content {
-    min-height: 200px;
-  }
-
-  // 类型统计
-  .type-stats {
-    display: flex;
-    flex-direction: column;
-    gap: 16px;
-
-    .type-stat-item {
-      display: flex;
-      align-items: center;
-      gap: 12px;
-
-      .type-info {
-        flex: 0 0 120px;
-
-        .type-name {
-          font-size: 14px;
-          font-weight: 500;
-          color: $text-primary;
-          display: block;
-        }
-
-        .type-count {
-          font-size: 12px;
-          color: $text-tertiary;
-          display: block;
-          margin-top: 2px;
-        }
-      }
-
-      .progress-bar {
-        flex: 1;
-        height: 12px;
-        background-color: $bg-tertiary;
-        border-radius: 6px;
-        overflow: hidden;
-        position: relative;
-
-        .progress-fill {
-          height: 100%;
-          background-color: $primary-color;
-          border-radius: 6px;
-          transition: width 0.8s ease-out;
-          position: relative;
-          overflow: hidden;
-
-          &::after {
-            content: '';
-            position: absolute;
-            top: 0;
-            left: 0;
-            right: 0;
-            bottom: 0;
-            background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
-            animation: progressAnimation 1.5s infinite;
-          }
-        }
-      }
-
-      .type-value {
-        font-size: 14px;
-        font-weight: 600;
-        color: $text-primary;
-        min-width: 100px;
-        text-align: right;
-      }
-    }
-  }
-
-  // 部门统计
-  .department-stats {
-    display: flex;
-    flex-direction: column;
-    gap: 16px;
-
-    .department-stat-item {
-      display: flex;
-      align-items: center;
-      gap: 12px;
-
-      .department-info {
-        flex: 0 0 150px;
-
-        .department-name {
-          font-size: 14px;
-          font-weight: 500;
-          color: $text-primary;
-          display: block;
-        }
-
-        .department-count {
-          font-size: 12px;
-          color: $text-tertiary;
-          display: block;
-          margin-top: 2px;
-        }
-      }
-
-      .progress-bar {
-        flex: 1;
-        height: 12px;
-        background-color: $bg-tertiary;
-        border-radius: 6px;
-        overflow: hidden;
-        position: relative;
-
-        .progress-fill {
-          height: 100%;
-          background-color: $secondary-color;
-          border-radius: 6px;
-          transition: width 0.8s ease-out;
-          position: relative;
-          overflow: hidden;
-
-          &::after {
-            content: '';
-            position: absolute;
-            top: 0;
-            left: 0;
-            right: 0;
-            bottom: 0;
-            background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
-            animation: progressAnimation 1.5s infinite;
-          }
-        }
-      }
-
-      .department-value {
-        font-size: 14px;
-        font-weight: 600;
-        color: $text-primary;
-        min-width: 100px;
-        text-align: right;
-      }
-    }
-  }
-
-  // 使用时长统计
-  .usage-stats {
-    display: flex;
-    align-items: flex-end;
-    gap: 16px;
-    height: 200px;
-    padding-top: 20px;
-
-    .usage-stat-item {
-      flex: 1;
-      display: flex;
-      flex-direction: column;
-      align-items: center;
-      gap: 8px;
-
-      .usage-info {
-        text-align: center;
-
-        .usage-type {
-          font-size: 12px;
-          color: $text-secondary;
-          font-weight: 500;
-        }
-      }
-
-      .bar-container {
-        position: relative;
-        width: 100%;
-        height: 100%;
-        display: flex;
-        align-items: flex-end;
-        justify-content: center;
-      }
-
-      .usage-bar {
-        width: 40px;
-        background-color: $primary-color;
-        border-radius: 4px 4px 0 0;
-        position: relative;
-        overflow: hidden;
-        transition: height 1s ease-out;
-        cursor: pointer;
-
-        &::after {
-          content: '';
-          position: absolute;
-          top: 0;
-          left: 0;
-          right: 0;
-          bottom: 0;
-          background: linear-gradient(180deg, rgba(255, 255, 255, 0.2), transparent);
-        }
-
-        &:hover {
-          background-color: $primary-light;
-          transform: scaleY(1.02);
-        }
-
-        .usage-value {
-          position: absolute;
-          top: -24px;
-          left: 50%;
-          transform: translateX(-50%);
-          font-size: 12px;
-          font-weight: 600;
-          color: $text-primary;
-        }
-      }
-    }
-  }
-}
-
-// 动画定义
-@keyframes faulty-pulse {
-  0%, 100% {
-    box-shadow: $shadow-sm, 0 0 0 0 rgba(239, 68, 68, 0);
-  }
-  50% {
-    box-shadow: $shadow-md, 0 0 0 4px rgba(239, 68, 68, 0.3);
-  }
-}
-
-@keyframes progressAnimation {
-  0% {
-    transform: translateX(-100%);
-  }
-  100% {
-    transform: translateX(100%);
-  }
-}
-
-// 响应式设计
-@media (max-width: 1200px) {
-  .assets-stats-container {
-    padding: 16px;
-  }
-
-  .stats-section {
-    grid-template-columns: 1fr;
-  }
-
-  .assets-grid {
-    grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
-  }
-}
-
-@media (max-width: 768px) {
-  .assets-stats-container {
-    padding: 12px;
-  }
-
-  .page-header {
-    margin-bottom: 20px;
-
-    h1 {
-      font-size: 24px;
-    }
-  }
-
-  .filter-bar {
-    padding: 16px;
-  }
-
-  .filter-controls {
-    flex-direction: column;
-    align-items: stretch;
-
-    .filter-group {
-      min-width: auto;
-    }
-  }
-
-  .view-toggle {
-    flex-wrap: wrap;
-    justify-content: center;
-
-    .asset-count {
-      margin-left: 0;
-      width: 100%;
-      text-align: center;
-    }
-  }
-
-  .stats-cards {
-    grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
-    gap: 12px;
-  }
-
-  .asset-card {
-    padding: 16px;
-
-    .asset-header {
-      flex-direction: column;
-      align-items: flex-start;
-      gap: 12px;
-    }
-
-    .asset-actions {
-      justify-content: center;
-    }
-  }
-
-  .assets-list {
-    overflow-x: auto;
-  }
-
-  .stats-section {
-    gap: 16px;
-  }
-
-  .stats-card {
-    padding: 16px;
-  }
-
-  .usage-stats {
-    height: 150px;
-    gap: 8px;
-  }
-
-  .usage-bar {
-    width: 24px;
-  }
-}
-
-@media (max-width: 480px) {
-  .stats-cards {
-    grid-template-columns: 1fr 1fr;
-  }
-
-  .assets-grid {
-    grid-template-columns: 1fr;
-  }
-}

+ 0 - 823
src/app/pages/hr/assets/assets-stats.ts

@@ -1,823 +0,0 @@
-import { Component, OnInit, signal, computed, Inject } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { FormsModule } from '@angular/forms';
-import { MatButtonModule } from '@angular/material/button';
-import { MatCardModule } from '@angular/material/card';
-import { MatIconModule } from '@angular/material/icon';
-import { MatDialogModule, MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
-import { MatTabsModule } from '@angular/material/tabs';
-import { MatTooltipModule } from '@angular/material/tooltip';
-import { MatTableModule } from '@angular/material/table';
-import { Asset, AssetType, AssetStatus, AssetAssignment, Employee } from '../../../models/hr.model';
-
-// 资产分配对话框组件
-@Component({
-  selector: 'app-asset-assignment-dialog',
-  standalone: true,
-  imports: [
-    CommonModule,
-    FormsModule,
-    MatButtonModule,
-    MatIconModule
-  ],
-  template: `
-    <div class="dialog-header">
-      <h2>{{ isEdit ? '修改资产分配' : '分配资产' }}</h2>
-      <button class="close-btn" (click)="dialogRef.close()">
-        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-          <line x1="18" y1="6" x2="6" y2="18"></line>
-          <line x1="6" y1="6" x2="18" y2="18"></line>
-        </svg>
-      </button>
-    </div>
-    <div class="dialog-content">
-      <div class="info-item">
-        <label>资产名称:</label>
-        <span>{{ asset.name }}</span>
-      </div>
-      <div class="form-group">
-        <label>选择员工:</label>
-        <input 
-          type="text" 
-          [(ngModel)]="selectedEmployeeName"
-          (input)="filterEmployees($event.target.value)"
-          placeholder="搜索员工姓名或工号..."
-          class="employee-search"
-          [disabled]="isReturning"
-        >
-        <div class="employee-dropdown" *ngIf="showEmployeeDropdown && !isReturning">
-          <div *ngFor="let employee of filteredEmployees" 
-               class="employee-item" 
-               (click)="selectEmployee(employee)">
-            <span class="employee-name">{{ employee.name }}</span>
-            <span class="employee-id">{{ employee.employeeId }}</span>
-          </div>
-        </div>
-      </div>
-      <div class="form-group" *ngIf="!isReturning">
-        <label>分配开始日期:</label>
-        <input type="date" [(ngModel)]="startDate" class="date-input">
-      </div>
-      <div class="form-group" *ngIf="!isReturning">
-        <label>预计归还日期:</label>
-        <input type="date" [(ngModel)]="endDate" class="date-input">
-      </div>
-      <div class="form-group" *ngIf="isReturning">
-        <label>实际归还日期:</label>
-        <input type="date" [(ngModel)]="returnDate" class="date-input">
-      </div>
-      <div class="form-group">
-        <label>备注:</label>
-        <textarea 
-          [(ngModel)]="notes" 
-          placeholder="请输入备注信息..." 
-          rows="3"
-          class="notes-input"
-        ></textarea>
-      </div>
-    </div>
-    <div class="dialog-actions">
-      <button mat-button (click)="dialogRef.close()">取消</button>
-      <button mat-raised-button color="primary" (click)="submit()">
-        {{ isReturning ? '确认归还' : '确认分配' }}
-      </button>
-    </div>
-  `,
-  styles: [`
-    .dialog-header {
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-      margin-bottom: 24px;
-      padding-bottom: 16px;
-      border-bottom: 1px solid #e5e7eb;
-    }
-    .close-btn {
-      background: none;
-      border: none;
-      cursor: pointer;
-      color: #6b7280;
-      padding: 4px;
-    }
-    .dialog-content {
-      max-width: 500px;
-    }
-    .info-item {
-      display: flex;
-      justify-content: space-between;
-      margin-bottom: 16px;
-      padding: 8px 0;
-      border-bottom: 1px solid #f3f4f6;
-    }
-    .form-group {
-      margin-bottom: 20px;
-      position: relative;
-    }
-    label {
-      display: block;
-      margin-bottom: 4px;
-      font-weight: 500;
-      color: #374151;
-    }
-    .employee-search,
-    .date-input {
-      width: 100%;
-      padding: 8px 12px;
-      border: 1px solid #e5e7eb;
-      border-radius: 6px;
-      font-size: 14px;
-    }
-    .employee-dropdown {
-      position: absolute;
-      top: 100%;
-      left: 0;
-      right: 0;
-      background-color: white;
-      border: 1px solid #e5e7eb;
-      border-radius: 6px;
-      max-height: 200px;
-      overflow-y: auto;
-      box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
-      z-index: 1000;
-    }
-    .employee-item {
-      padding: 10px 12px;
-      cursor: pointer;
-      transition: background-color 0.2s;
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-    }
-    .employee-item:hover {
-      background-color: #f3f4f6;
-    }
-    .employee-name {
-      font-weight: 500;
-      color: #1f2937;
-    }
-    .employee-id {
-      font-size: 12px;
-      color: #6b7280;
-    }
-    .notes-input {
-      width: 100%;
-      padding: 8px 12px;
-      border: 1px solid #e5e7eb;
-      border-radius: 6px;
-      font-size: 14px;
-      resize: vertical;
-    }
-    .dialog-actions {
-      display: flex;
-      justify-content: flex-end;
-      gap: 12px;
-      margin-top: 24px;
-    }
-  `]
-}) class AssetAssignmentDialog {
-  asset: Asset;
-  employees: Employee[] = [];
-  filteredEmployees: Employee[] = [];
-  showEmployeeDropdown = false;
-  selectedEmployeeId = '';
-  selectedEmployeeName = '';
-  startDate = new Date().toISOString().split('T')[0];
-  endDate = '';
-  returnDate = new Date().toISOString().split('T')[0];
-  notes = '';
-  isEdit = false;
-  isReturning = false;
-  
-  constructor(
-    public dialogRef: MatDialogRef<AssetAssignmentDialog>,
-    @Inject(MAT_DIALOG_DATA) public data: any
-  ) {
-    this.asset = data.asset;
-    this.employees = data.employees;
-    this.filteredEmployees = [...this.employees];
-    this.isEdit = data.isEdit || false;
-    this.isReturning = data.isReturning || false;
-    
-    if (this.isEdit && data.assignment) {
-      const assignment = data.assignment;
-      const employee = this.employees.find(e => e.id === assignment.employeeId);
-      if (employee) {
-        this.selectedEmployeeId = employee.id;
-        this.selectedEmployeeName = employee.name;
-      }
-      if (assignment.startDate) {
-        this.startDate = new Date(assignment.startDate).toISOString().split('T')[0];
-      }
-      if (assignment.endDate) {
-        this.endDate = new Date(assignment.endDate).toISOString().split('T')[0];
-      }
-    }
-  }
-  
-  filterEmployees(query: string) {
-    if (!query.trim()) {
-      this.filteredEmployees = [...this.employees];
-    } else {
-      const term = query.toLowerCase();
-      this.filteredEmployees = this.employees.filter(emp => 
-        emp.name.toLowerCase().includes(term) ||
-        emp.employeeId.toLowerCase().includes(term)
-      );
-    }
-    this.showEmployeeDropdown = true;
-  }
-  
-  selectEmployee(employee: Employee) {
-    this.selectedEmployeeId = employee.id;
-    this.selectedEmployeeName = employee.name;
-    this.showEmployeeDropdown = false;
-  }
-  
-  submit() {
-    if (!this.selectedEmployeeId && !this.isReturning) {
-      alert('请选择员工');
-      return;
-    }
-    
-    this.dialogRef.close({
-      employeeId: this.selectedEmployeeId,
-      startDate: this.startDate,
-      endDate: this.isReturning ? this.returnDate : this.endDate,
-      notes: this.notes
-    });
-  }
-}
-
-// 报修对话框组件
-@Component({
-  selector: 'app-asset-repair-dialog',
-  standalone: true,
-  imports: [
-    CommonModule,
-    FormsModule,
-    MatButtonModule,
-    MatIconModule
-  ],
-  template: `
-    <div class="dialog-header">
-      <h2>资产报修</h2>
-      <button class="close-btn" (click)="dialogRef.close()">
-        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-          <line x1="18" y1="6" x2="6" y2="18"></line>
-          <line x1="6" y1="6" x2="18" y2="18"></line>
-        </svg>
-      </button>
-    </div>
-    <div class="dialog-content">
-      <div class="info-item">
-        <label>资产名称:</label>
-        <span>{{ asset.name }}</span>
-      </div>
-      <div class="info-item">
-        <label>资产类型:</label>
-        <span>{{ asset.type }}</span>
-      </div>
-      <div class="info-item">
-        <label>序列号:</label>
-        <span>{{ asset.serialNumber || '无' }}</span>
-      </div>
-      <div class="form-group">
-        <label>故障描述 *:</label>
-        <textarea 
-          [(ngModel)]="problemDescription" 
-          placeholder="请详细描述故障情况..." 
-          rows="4"
-          class="description-input"
-          required
-        ></textarea>
-      </div>
-      <div class="form-group">
-        <label>期望修复时间:</label>
-        <input type="date" [(ngModel)]="expectedFixDate" class="date-input">
-      </div>
-      <div class="form-group">
-        <label>联系人:</label>
-        <input type="text" [(ngModel)]="contactPerson" class="contact-input" placeholder="请输入联系人姓名">
-      </div>
-      <div class="form-group">
-        <label>联系电话:</label>
-        <input type="text" [(ngModel)]="contactPhone" class="phone-input" placeholder="请输入联系电话">
-      </div>
-    </div>
-    <div class="dialog-actions">
-      <button mat-button (click)="dialogRef.close()">取消</button>
-      <button mat-raised-button color="primary" (click)="submit()" [disabled]="!problemDescription.trim()">
-        提交报修申请
-      </button>
-    </div>
-  `,
-  styles: [`
-    .dialog-header {
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-      margin-bottom: 24px;
-      padding-bottom: 16px;
-      border-bottom: 1px solid #e5e7eb;
-    }
-    .close-btn {
-      background: none;
-      border: none;
-      cursor: pointer;
-      color: #6b7280;
-      padding: 4px;
-    }
-    .dialog-content {
-      max-width: 500px;
-    }
-    .info-item {
-      display: flex;
-      justify-content: space-between;
-      margin-bottom: 16px;
-      padding: 8px 0;
-      border-bottom: 1px solid #f3f4f6;
-    }
-    .form-group {
-      margin-bottom: 20px;
-    }
-    label {
-      display: block;
-      margin-bottom: 4px;
-      font-weight: 500;
-      color: #374151;
-    }
-    .description-input,
-    .date-input,
-    .contact-input,
-    .phone-input {
-      width: 100%;
-      padding: 8px 12px;
-      border: 1px solid #e5e7eb;
-      border-radius: 6px;
-      font-size: 14px;
-    }
-    .description-input {
-      resize: vertical;
-    }
-    .dialog-actions {
-      display: flex;
-      justify-content: flex-end;
-      gap: 12px;
-      margin-top: 24px;
-    }
-  `]
-}) class AssetRepairDialog {
-  asset: Asset;
-  problemDescription = '';
-  expectedFixDate = new Date().toISOString().split('T')[0];
-  contactPerson = '';
-  contactPhone = '';
-  
-  constructor(
-    public dialogRef: MatDialogRef<AssetRepairDialog>,
-    @Inject(MAT_DIALOG_DATA) public data: any
-  ) {
-    this.asset = data.asset;
-  }
-  
-  submit() {
-    if (!this.problemDescription.trim()) {
-      alert('请填写故障描述');
-      return;
-    }
-    
-    this.dialogRef.close({
-      problemDescription: this.problemDescription,
-      expectedFixDate: this.expectedFixDate,
-      contactPerson: this.contactPerson,
-      contactPhone: this.contactPhone
-    });
-  }
-}
-
-// 生成模拟资产数据
-const generateMockAssets = (): Asset[] => {
-  const assets: Asset[] = [];
-  const types: AssetType[] = ['电脑', '外设', '软件账号', '域名', '其他'];
-  const statuses: AssetStatus[] = ['空闲', '占用', '故障', '报修中'];
-  const departments = ['设计部', '客户服务部', '财务部', '人力资源部'];
-  const computerModels = ['MacBook Pro', 'Dell XPS', 'HP EliteBook', 'Lenovo ThinkPad', 'Surface Laptop'];
-  const peripheralTypes = ['显示器', '键盘', '鼠标', '打印机', '扫描仪', '投影仪'];
-  const softwareTypes = ['Adobe Creative Cloud', 'AutoCAD', 'Office 365', '渲染农场账号', '素材库账号'];
-  const otherAssets = ['办公桌', '办公椅', '服务器', '网络设备', '空调'];
-  
-  for (let i = 1; i <= 30; i++) {
-    const type = types[Math.floor(Math.random() * types.length)];
-    const status = statuses[Math.floor(Math.random() * statuses.length)];
-    const purchaseDate = new Date();
-    purchaseDate.setMonth(purchaseDate.getMonth() - Math.floor(Math.random() * 36));
-    
-    let name = '';
-    let serialNumber = `SN${Math.floor(Math.random() * 1000000)}`;
-    
-    switch (type) {
-      case '电脑':
-        name = `${computerModels[Math.floor(Math.random() * computerModels.length)]} ${2020 + Math.floor(Math.random() * 4)}`;
-        break;
-      case '外设':
-        name = peripheralTypes[Math.floor(Math.random() * peripheralTypes.length)];
-        break;
-      case '软件账号':
-        name = softwareTypes[Math.floor(Math.random() * softwareTypes.length)];
-        serialNumber = '无';
-        break;
-      case '域名':
-        name = `example-${i}.com`;
-        serialNumber = '无';
-        break;
-      case '其他':
-        name = otherAssets[Math.floor(Math.random() * otherAssets.length)];
-        break;
-    }
-    
-    const value = Math.floor(Math.random() * 10000) + 500;
-    const assignedTo = status === '占用' ? `emp-${Math.floor(Math.random() * 10) + 1}` : undefined;
-    const assignedToName = assignedTo ? `员工${Math.floor(Math.random() * 20) + 1}` : undefined;
-    
-    // 随机设置保修截止日期
-    const warrantyExpiry = new Date(purchaseDate);
-    warrantyExpiry.setFullYear(warrantyExpiry.getFullYear() + (Math.random() > 0.5 ? 1 : 2));
-    
-    assets.push({
-      id: `asset-${i}`,
-      name,
-      type,
-      status,
-      purchaseDate,
-      value,
-      assignedTo,
-      assignedToName,
-      department: departments[Math.floor(Math.random() * departments.length)],
-      description: `这是一台${name},用于日常办公和项目开发。`,
-      serialNumber,
-      warrantyExpiry
-    });
-  }
-  
-  return assets;
-};
-
-// 生成模拟员工数据
-const generateMockEmployees = (): Employee[] => {
-  const employees: Employee[] = [];
-  const departments = ['设计部', '客户服务部', '财务部', '人力资源部'];
-  const positions = ['设计师', '客服专员', '财务专员', '人事专员', '经理', '助理'];
-  const names = ['张三', '李四', '王五', '赵六', '钱七', '孙八', '周九', '吴十'];
-  
-  for (let i = 1; i <= 20; i++) {
-    employees.push({
-      id: `emp-${i}`,
-      name: names[i % names.length] + i,
-      department: departments[Math.floor(Math.random() * departments.length)],
-      position: positions[Math.floor(Math.random() * positions.length)],
-      employeeId: `EMP2023${String(i).padStart(3, '0')}`,
-      phone: `138${Math.floor(Math.random() * 100000000)}`,
-      email: `employee${i}@example.com`,
-      gender: i % 2 === 0 ? '女' : '男',
-      birthDate: new Date(1980 + Math.floor(Math.random() * 20), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1),
-      hireDate: new Date(2020 + Math.floor(Math.random() * 3), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1),
-      status: '在职'
-    });
-  }
-  
-  return employees;
-};
-
-// 生成模拟资产分配记录
-const generateMockAssignments = (): AssetAssignment[] => {
-  const assignments: AssetAssignment[] = [];
-  
-  for (let i = 1; i <= 15; i++) {
-    const startDate = new Date();
-    startDate.setMonth(startDate.getMonth() - Math.floor(Math.random() * 6));
-    
-    const endDate = Math.random() > 0.5 ? new Date(startDate) : undefined;
-    if (endDate) {
-      endDate.setMonth(endDate.getMonth() + Math.floor(Math.random() * 3) + 1);
-    }
-    
-    assignments.push({
-      id: `assignment-${i}`,
-      assetId: `asset-${Math.floor(Math.random() * 20) + 1}`,
-      employeeId: `emp-${Math.floor(Math.random() * 15) + 1}`,
-      startDate,
-      endDate,
-      status: endDate ? '已归还' : '进行中'
-    });
-  }
-  
-  return assignments;
-};
-
-// 主组件
-@Component({
-  selector: 'app-assets-stats',
-  standalone: true,
-  imports: [
-    CommonModule,
-    FormsModule,
-    MatButtonModule,
-    MatCardModule,
-    MatIconModule,
-    MatDialogModule,
-    MatTabsModule,
-    MatTooltipModule,
-    MatTableModule
-  ],
-  templateUrl: './assets-stats.html',
-  styleUrl: './assets-stats.scss'
-}) export class AssetsStats implements OnInit {
-  // 暴露Math对象给模板使用
-  readonly Math = Math;
-  
-  // 数据
-  assets = signal<Asset[]>([]);
-  employees = signal<Employee[]>([]);
-  assignments = signal<AssetAssignment[]>([]);
-  selectedView = signal<'grid' | 'list'>('grid');
-  searchTerm = signal('');
-  typeFilter = signal<AssetType | ''>('');
-  statusFilter = signal<AssetStatus | ''>('');
-  departmentFilter = signal('');
-  
-  // 计算属性
-  filteredAssets = computed(() => {
-    let filtered = this.assets();
-    
-    // 按搜索词筛选
-    if (this.searchTerm()) {
-      const term = this.searchTerm().toLowerCase();
-      filtered = filtered.filter(asset => 
-        asset.name.toLowerCase().includes(term) ||
-        (asset.serialNumber && asset.serialNumber.toLowerCase().includes(term)) ||
-        (asset.department && asset.department.toLowerCase().includes(term)) ||
-        (asset.assignedToName && asset.assignedToName.toLowerCase().includes(term))
-      );
-    }
-    
-    // 按类型筛选
-    if (this.typeFilter()) {
-      filtered = filtered.filter(asset => asset.type === this.typeFilter());
-    }
-    
-    // 按状态筛选
-    if (this.statusFilter()) {
-      filtered = filtered.filter(asset => asset.status === this.statusFilter());
-    }
-    
-    // 按部门筛选
-    if (this.departmentFilter()) {
-      filtered = filtered.filter(asset => asset.department === this.departmentFilter());
-    }
-    
-    return filtered;
-  });
-  
-  // 资产统计
-  assetStats = computed(() => {
-    const total = this.assets().length;
-    const occupied = this.assets().filter(a => a.status === '占用').length;
-    const idle = this.assets().filter(a => a.status === '空闲').length;
-    const faulty = this.assets().filter(a => a.status === '故障').length;
-    const repairing = this.assets().filter(a => a.status === '报修中').length;
-    
-    const totalValue = this.assets().reduce((sum, asset) => sum + asset.value, 0);
-    
-    // 按类型统计
-    const typeStats = new Map<AssetType, { count: number, value: number }>();
-    this.assets().forEach(asset => {
-      const current = typeStats.get(asset.type) || { count: 0, value: 0 };
-      current.count++;
-      current.value += asset.value;
-      typeStats.set(asset.type, current);
-    });
-    
-    // 转换为数组格式以便在模板中使用
-    const typeStatsArray = Array.from(typeStats.entries()).map(([key, value]) => ({
-      type: key,
-      ...value
-    }));
-    
-    // 按部门统计
-    const departmentStats = new Map<string, { count: number, value: number }>();
-    this.assets().forEach(asset => {
-      const department = asset.department || '未知部门'; // 提供默认值
-      const current = departmentStats.get(department) || { count: 0, value: 0 };
-      current.count++;
-      current.value += asset.value;
-      departmentStats.set(department, current);
-    });
-    
-    // 转换为数组格式以便在模板中使用
-    const departmentStatsArray = Array.from(departmentStats.entries()).map(([key, value]) => ({
-      department: key,
-      ...value
-    }));
-    
-    // 使用时长统计(模拟数据)
-    const usageStats = [
-      { type: '电脑', avgHours: Math.floor(Math.random() * 40) + 100 },
-      { type: '外设', avgHours: Math.floor(Math.random() * 30) + 80 },
-      { type: '软件账号', avgHours: Math.floor(Math.random() * 50) + 120 },
-      { type: '域名', avgHours: Math.floor(Math.random() * 20) + 60 },
-      { type: '其他', avgHours: Math.floor(Math.random() * 20) + 40 }
-    ];
-    
-    return {
-      total,
-      occupied,
-      idle,
-      faulty,
-      repairing,
-      totalValue,
-      typeStats,
-      typeStatsArray,
-      departmentStats,
-      departmentStatsArray,
-      usageStats
-    };
-  });
-  
-  // 获取资产类型列表
-  assetTypes = computed(() => {
-    return Array.from(new Set(this.assets().map(asset => asset.type)));
-  });
-  
-  // 获取部门列表
-  departments = computed(() => {
-    return Array.from(new Set(this.assets().map(asset => asset.department)));
-  });
-  
-  constructor(private dialog: MatDialog) {}
-  
-  ngOnInit() {
-    // 加载模拟数据
-    this.assets.set(generateMockAssets());
-    this.employees.set(generateMockEmployees());
-    this.assignments.set(generateMockAssignments());
-  }
-  
-  // 切换视图(网格/列表)
-  switchView(view: 'grid' | 'list') {
-    this.selectedView.set(view);
-  }
-  
-  // 格式化日期
-  formatDate(date: Date): string {
-    if (!date) return '';
-    const d = new Date(date);
-    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
-  }
-  
-  // 格式化金额
-  formatCurrency(amount: number): string {
-    return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount);
-  }
-  
-  // 获取状态样式类
-  getStatusClass(status: string): string {
-    switch (status) {
-      case '空闲':
-        return 'status-idle';
-      case '占用':
-        return 'status-occupied';
-      case '故障':
-        return 'status-faulty';
-      case '报修中':
-        return 'status-repairing';
-      default:
-        return '';
-    }
-  }
-  
-  // 获取类型图标
-  getTypeIcon(type: AssetType): string {
-    switch (type) {
-      case '电脑':
-        return 'laptop';
-      case '外设':
-        return 'devices';
-      case '软件账号':
-        return 'cloud';
-      case '域名':
-        return 'link';
-      case '其他':
-        return 'category';
-      default:
-        return 'help';
-    }
-  }
-  
-  // 打开资产分配对话框
-  openAssignmentDialog(asset: Asset, isReturning: boolean = false) {
-    let assignment: AssetAssignment | undefined;
-    if (isReturning) {
-      assignment = this.assignments().find(a => a.assetId === asset.id && a.status === '进行中');
-    }
-    
-    const dialogRef = this.dialog.open(AssetAssignmentDialog, {
-      width: '500px',
-      maxWidth: '90vw',
-      disableClose: true,
-      data: {
-        asset,
-        employees: this.employees(),
-        isEdit: !!assignment,
-        isReturning,
-        assignment
-      }
-    });
-    
-    dialogRef.afterClosed().subscribe(result => {
-      if (result) {
-        if (isReturning && assignment) {
-          // 更新归还信息
-          this.assignments.update(assignments => 
-            assignments.map(a => 
-              a.id === assignment!.id ? { ...a, endDate: result.endDate, status: '已归还' } : a
-            )
-          );
-          
-          // 更新资产状态为空闲
-          this.assets.update(assets => 
-            assets.map(a => 
-              a.id === asset.id ? { ...a, status: '空闲', assignedTo: undefined, assignedToName: undefined } : a
-            )
-          );
-        } else {
-          // 创建新分配记录
-          const newAssignment: AssetAssignment = {
-            id: `assignment-${Date.now()}`,
-            assetId: asset.id,
-            employeeId: result.employeeId,
-            startDate: new Date(result.startDate),
-            endDate: result.endDate ? new Date(result.endDate) : undefined,
-            status: '进行中'
-          };
-          
-          this.assignments.update(assignments => [newAssignment, ...assignments]);
-          
-          // 更新资产状态为占用
-          const employee = this.employees().find(e => e.id === result.employeeId);
-          this.assets.update(assets => 
-            assets.map(a => 
-              a.id === asset.id ? { ...a, status: '占用', assignedTo: result.employeeId, assignedToName: employee?.name } : a
-            )
-          );
-        }
-        alert(isReturning ? '资产归还成功' : '资产分配成功');
-      }
-    });
-  }
-  
-  // 打开报修对话框
-  openRepairDialog(asset: Asset) {
-    const dialogRef = this.dialog.open(AssetRepairDialog, {
-      width: '500px',
-      maxWidth: '90vw',
-      disableClose: true,
-      data: { asset }
-    });
-    
-    dialogRef.afterClosed().subscribe(result => {
-      if (result) {
-        // 更新资产状态为报修中
-        this.assets.update(assets => 
-          assets.map(a => 
-            a.id === asset.id ? { ...a, status: '报修中' } : a
-          )
-        );
-        alert('报修申请已提交');
-      }
-    });
-  }
-  
-  // 导出资产台账
-  exportAssetLedger() {
-    alert('资产台账导出功能待实现');
-  }
-  
-  // 重置筛选条件
-  resetFilters() {
-    this.searchTerm.set('');
-    this.typeFilter.set('');
-    this.statusFilter.set('');
-    this.departmentFilter.set('');
-  }
-  
-  // 获取资产使用情况
-  getAssetUsage(assetId: string): AssetAssignment | undefined {
-    return this.assignments().find(a => a.assetId === assetId && a.status === '进行中');
-  }
-  
-  // 获取员工姓名
-  getEmployeeName(employeeId: string): string {
-    const employee = this.employees().find(e => e.id === employeeId);
-    return employee ? employee.name : '未知员工';
-  }
-}

+ 0 - 224
src/app/pages/hr/assets/assets.html

@@ -1,224 +0,0 @@
-<div class="assets-container">
-  <header class="page-header">
-    <h1>花名册与档案库</h1>
-    <p class="page-description">管理员工全维度信息,包括基本信息、合同管理和证件管理</p>
-  </header>
-
-  <!-- 顶部操作栏 -->
-  <div class="action-bar">
-    <div class="action-buttons">
-      <button mat-raised-button color="primary" class="add-btn" (click)="openAddEmployeeDialog()">
-        <mat-icon>add</mat-icon>
-        新增员工
-      </button>
-      
-      <div class="batch-actions" [class.hidden]="selectedEmployees().length === 0">
-        <button mat-button color="warn" (click)="batchDelete()" class="batch-btn">
-          <mat-icon>delete</mat-icon>
-          批量删除
-        </button>
-        <div class="export-dropdown">
-          <button mat-button class="batch-btn" [matMenuTriggerFor]="exportMenu">
-            <mat-icon>file_download</mat-icon>
-            导出
-            <mat-icon>expand_more</mat-icon>
-          </button>
-          <mat-menu #exportMenu="matMenu">
-            <button mat-menu-item (click)="exportData('excel')">
-              <mat-icon>insert_drive_file</mat-icon>
-              Excel
-            </button>
-            <button mat-menu-item (click)="exportData('pdf')">
-              <mat-icon>picture_as_pdf</mat-icon>
-              PDF
-            </button>
-          </mat-menu>
-        </div>
-      </div>
-    </div>
-    
-    <div class="search-filters">
-      <div class="search-container">
-        <mat-icon class="search-icon">search</mat-icon>
-        <input 
-          matInput 
-          placeholder="搜索员工姓名、工号、手机号或邮箱..."
-          [value]="searchTerm()"
-          (input)="searchTerm.set($event.target.value)"
-          (keyup.enter)="applyFilters()"
-          class="search-input"
-        >
-        <button mat-icon-button *ngIf="searchTerm()" (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>
-        </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>
-        </mat-select>
-      </div>
-      
-      <button mat-raised-button (click)="applyFilters()" class="filter-btn">
-        <mat-icon>filter_list</mat-icon>
-        筛选
-      </button>
-    </div>
-  </div>
-
-  <!-- 数据表格 -->
-  <div class="table-container">
-    <table mat-table [dataSource]="filteredEmployees()" class="employee-table">
-      <!-- 复选框列 -->
-      <ng-container matColumnDef="select">
-        <th mat-header-cell *matHeaderCellDef>
-          <mat-checkbox 
-            [checked]="isAllSelected()"
-            (change)="toggleSelectAll()"
-            [indeterminate]="selectedEmployees().length > 0 && selectedEmployees().length < filteredEmployees().length"
-          ></mat-checkbox>
-        </th>
-        <td mat-cell *matCellDef="let employee">
-          <mat-checkbox 
-            [checked]="selectedEmployees().includes(employee.id)"
-            (change)="toggleEmployeeSelection(employee.id)"
-          ></mat-checkbox>
-        </td>
-      </ng-container>
-
-      <!-- 姓名列 -->
-      <ng-container matColumnDef="name">
-        <th mat-header-cell *matHeaderCellDef class="name-column">姓名</th>
-        <td mat-cell *matCellDef="let employee" class="name-column">
-          <div class="employee-info">
-            <img [src]="employee.avatar" alt="员工头像" class="employee-avatar">
-            <span class="employee-name">{{ employee.name }}</span>
-          </div>
-        </td>
-      </ng-container>
-
-      <!-- 工号列 -->
-      <ng-container matColumnDef="employeeId">
-        <th mat-header-cell *matHeaderCellDef>工号</th>
-        <td mat-cell *matCellDef="let employee">{{ employee.employeeId }}</td>
-      </ng-container>
-
-      <!-- 部门列 -->
-      <ng-container matColumnDef="department">
-        <th mat-header-cell *matHeaderCellDef>部门</th>
-        <td mat-cell *matCellDef="let employee">{{ employee.department }}</td>
-      </ng-container>
-
-      <!-- 岗位列 -->
-      <ng-container matColumnDef="position">
-        <th mat-header-cell *matHeaderCellDef>岗位</th>
-        <td mat-cell *matCellDef="let employee">{{ employee.position }}</td>
-      </ng-container>
-
-      <!-- 手机号列 -->
-      <ng-container matColumnDef="phone">
-        <th mat-header-cell *matHeaderCellDef>手机号</th>
-        <td mat-cell *matCellDef="let employee">{{ employee.phone }}</td>
-      </ng-container>
-
-      <!-- 邮箱列 -->
-      <ng-container matColumnDef="email">
-        <th mat-header-cell *matHeaderCellDef>邮箱</th>
-        <td mat-cell *matCellDef="let employee">{{ employee.email }}</td>
-      </ng-container>
-
-      <!-- 入职日期列 -->
-      <ng-container matColumnDef="hireDate">
-        <th mat-header-cell *matHeaderCellDef>入职日期</th>
-        <td mat-cell *matCellDef="let employee">{{ formatDate(employee.hireDate) }}</td>
-      </ng-container>
-
-      <!-- 状态列 -->
-      <ng-container matColumnDef="status">
-        <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>
-          </mat-select>
-        </td>
-      </ng-container>
-
-      <!-- 合同列 -->
-      <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="合同即将到期">
-              ⚠️ 即将到期
-            </div>
-            <button mat-icon-button class="contract-btn" matTooltip="查看合同">
-              <mat-icon>description</mat-icon>
-            </button>
-          </div>
-          <div *ngIf="!employee.contract" class="no-contract">
-            无合同信息
-          </div>
-        </td>
-      </ng-container>
-
-      <!-- 操作列 -->
-      <ng-container matColumnDef="actions">
-        <th mat-header-cell *matHeaderCellDef>操作</th>
-        <td mat-cell *matCellDef="let employee" class="actions-column">
-          <div class="action-buttons">
-            <button mat-icon-button class="edit-btn" matTooltip="编辑" (click)="editEmployee(employee)">
-              <mat-icon>edit</mat-icon>
-            </button>
-            <button mat-icon-button class="delete-btn" matTooltip="删除" (click)="deleteEmployee(employee.id)">
-              <mat-icon>delete</mat-icon>
-            </button>
-            <button mat-icon-button class="view-btn" matTooltip="查看详情">
-              <mat-icon>visibility</mat-icon>
-            </button>
-          </div>
-        </td>
-      </ng-container>
-
-      <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 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>
-
-  <!-- 分页组件 -->
-  <div class="pagination">
-    <div class="pagination-info">
-      共 {{ filteredEmployees().length }} 条记录,当前显示第 {{ pageIndex * pageSize + 1 }} - 
-      {{ Math.min((pageIndex + 1) * pageSize, filteredEmployees().length) }} 条
-    </div>
-    <mat-paginator
-      [length]="filteredEmployees().length"
-      [pageSize]="pageSize"
-      [pageSizeOptions]="[10, 20, 50]"
-      [pageIndex]="pageIndex"
-      (page)="onPageChange($event)"
-      showFirstLastButtons
-    ></mat-paginator>
-  </div>
-</div>

+ 0 - 725
src/app/pages/hr/assets/assets.scss

@@ -1,725 +0,0 @@
-// 自定义主题
-$primary-color: #1e40af; // 深蓝主色,传递可靠感
-$primary-light: #3b82f6; // 浅蓝色,用于悬停效果
-$secondary-color: #0d9488; // 薄荷绿,作为强调色
-$success-color: #10b981; // 成功色
-$warning-color: #f59e0b; // 警告色(浅橙)
-$error-color: #ef4444; // 错误色
-$text-primary: #1f2937; // 主要文本色
-$text-secondary: #4b5563; // 次要文本色
-$text-tertiary: #9ca3af; // 辅助文本色
-$bg-primary: #ffffff; // 主背景色
-$bg-secondary: #f9fafb; // 次要背景色
-$bg-tertiary: #f3f4f6; // 辅助背景色
-$border-color: #e5e7eb; // 边框色
-$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
-$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
-$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
-$border-radius: 8px;
-$transition: all 0.2s ease;
-
-// 主容器样式
-.assets-container {
-  padding: 24px;
-  min-height: 100vh;
-  background-color: $bg-secondary;
-  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-}
-
-// 页面标题
-.page-header {
-  margin-bottom: 24px;
-  padding-bottom: 16px;
-  border-bottom: 2px solid $border-color;
-
-  h1 {
-    font-size: 28px;
-    font-weight: 700;
-    color: $text-primary;
-    margin: 0 0 8px 0;
-  }
-
-  .page-description {
-    font-size: 16px;
-    color: $text-secondary;
-    margin: 0;
-  }
-}
-
-// 顶部操作栏
-.action-bar {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 24px;
-  padding: 16px 20px;
-  background-color: $bg-primary;
-  border-radius: $border-radius;
-  box-shadow: $shadow-sm;
-  flex-wrap: wrap;
-  gap: 16px;
-
-  .action-buttons {
-    display: flex;
-    align-items: center;
-    gap: 12px;
-    flex-wrap: wrap;
-  }
-
-  .add-btn {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    padding: 10px 20px;
-    background-color: $primary-color;
-    color: white;
-    border: none;
-    border-radius: $border-radius;
-    font-size: 16px;
-    font-weight: 500;
-    cursor: pointer;
-    transition: $transition;
-
-    &:hover {
-      background-color: $primary-light;
-      transform: translateY(-1px);
-      box-shadow: $shadow-md;
-    }
-
-    &:active {
-      transform: translateY(0);
-      box-shadow: $shadow-sm;
-    }
-  }
-
-  .batch-actions {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-    animation: slideIn 0.3s ease-out;
-
-    &.hidden {
-      display: none;
-    }
-  }
-
-  .batch-btn {
-    display: flex;
-    align-items: center;
-    gap: 6px;
-    padding: 8px 12px;
-    border: 1px solid $border-color;
-    border-radius: $border-radius;
-    background-color: $bg-primary;
-    color: $text-secondary;
-    font-size: 14px;
-    cursor: pointer;
-    transition: $transition;
-
-    &:hover {
-      background-color: $bg-tertiary;
-      border-color: $text-tertiary;
-    }
-
-    &:active {
-      transform: scale(0.98);
-    }
-  }
-
-  .search-filters {
-    display: flex;
-    align-items: center;
-    gap: 12px;
-    flex-wrap: wrap;
-  }
-
-  .search-container {
-    position: relative;
-    display: flex;
-    align-items: center;
-    min-width: 280px;
-
-    .search-icon {
-      position: absolute;
-      left: 12px;
-      color: $text-tertiary;
-      z-index: 1;
-    }
-
-    .search-input {
-      width: 100%;
-      padding: 10px 12px 10px 40px;
-      border: 1px solid $border-color;
-      border-radius: $border-radius;
-      font-size: 14px;
-      color: $text-primary;
-      background-color: $bg-primary;
-      transition: $transition;
-
-      &:focus {
-        outline: none;
-        border-color: $primary-color;
-        box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
-      }
-
-      &::placeholder {
-        color: $text-tertiary;
-      }
-    }
-
-    .clear-search {
-      position: absolute;
-      right: 8px;
-      color: $text-tertiary;
-      padding: 4px;
-      transition: $transition;
-
-      &:hover {
-        color: $text-primary;
-      }
-    }
-  }
-
-  .filter-container {
-    min-width: 150px;
-  }
-
-  .filter-btn {
-    display: flex;
-    align-items: center;
-    gap: 6px;
-    padding: 10px 16px;
-    background-color: $secondary-color;
-    color: white;
-    border: none;
-    border-radius: $border-radius;
-    font-size: 14px;
-    font-weight: 500;
-    cursor: pointer;
-    transition: $transition;
-
-    &:hover {
-      background-color: #0f766e;
-      transform: translateY(-1px);
-      box-shadow: $shadow-md;
-    }
-
-    &:active {
-      transform: translateY(0);
-      box-shadow: $shadow-sm;
-    }
-  }
-}
-
-// 表格容器
-.table-container {
-  background-color: $bg-primary;
-  border-radius: $border-radius;
-  box-shadow: $shadow-sm;
-  overflow: hidden;
-  margin-bottom: 24px;
-}
-
-// 员工表格
-.employee-table {
-  width: 100%;
-
-  .mat-header-row {
-    background-color: $bg-tertiary;
-    font-weight: 600;
-    color: $text-primary;
-
-    .mat-header-cell {
-      padding: 16px;
-      font-size: 14px;
-      color: $text-primary;
-      font-weight: 600;
-      border-bottom: 1px solid $border-color;
-    }
-  }
-
-  .mat-row {
-    transition: $transition;
-    cursor: pointer;
-
-    &:hover {
-      background-color: color-mix(in srgb, $primary-color 2%, transparent);
-      transform: translateY(-1px);
-    }
-
-    .mat-cell {
-      padding: 16px;
-      font-size: 14px;
-      color: $text-secondary;
-      border-bottom: 1px solid $border-color;
-      vertical-align: middle;
-    }
-
-    &:last-child .mat-cell {
-      border-bottom: none;
-    }
-  }
-
-  .name-column {
-    min-width: 150px;
-
-    .employee-info {
-      display: flex;
-      align-items: center;
-      gap: 10px;
-    }
-
-    .employee-avatar {
-      width: 36px;
-      height: 36px;
-      border-radius: 50%;
-      object-fit: cover;
-    }
-
-    .employee-name {
-      font-weight: 500;
-      color: $text-primary;
-    }
-  }
-
-  .actions-column {
-    min-width: 120px;
-
-    .action-buttons {
-      display: flex;
-      align-items: center;
-      gap: 8px;
-    }
-
-    .edit-btn,
-    .delete-btn,
-    .view-btn {
-      color: $text-tertiary;
-      transition: $transition;
-      opacity: 0.8;
-
-      &:hover {
-        opacity: 1;
-        transform: scale(1.1);
-      }
-
-      &.edit-btn:hover {
-        color: $primary-color;
-      }
-
-      &.delete-btn:hover {
-        color: $error-color;
-      }
-
-      &.view-btn:hover {
-        color: $secondary-color;
-      }
-    }
-  }
-
-  // 合同信息样式
-  .contract-info {
-    display: flex;
-    align-items: center;
-    gap: 8px;
-
-    .contract-date {
-      font-size: 12px;
-      color: $text-secondary;
-    }
-
-    .expiring-soon {
-      background-color: color-mix(in srgb, $warning-color 15%, transparent);
-      color: $warning-color;
-      padding: 2px 8px;
-      border-radius: 12px;
-      font-size: 11px;
-      font-weight: 500;
-      animation: pulse 2s infinite;
-    }
-
-    .contract-btn {
-      color: $primary-color;
-      transition: $transition;
-
-      &:hover {
-        color: $primary-light;
-        transform: scale(1.1);
-      }
-    }
-  }
-
-  .no-contract {
-    color: $text-tertiary;
-    font-size: 12px;
-    font-style: italic;
-  }
-
-  // 状态选择器样式
-  .status-select {
-    min-width: 100px;
-    .mat-select-trigger {
-      font-size: 14px;
-    }
-  }
-
-  // 空状态样式
-  .empty-state {
-    text-align: center;
-    padding: 60px 20px;
-
-    .empty-icon {
-      font-size: 48px;
-      color: $text-tertiary;
-      margin-bottom: 16px;
-    }
-
-    p {
-      color: $text-secondary;
-      font-size: 16px;
-      margin-bottom: 24px;
-    }
-  }
-}
-
-// 分页组件
-.pagination {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  padding: 16px;
-  background-color: $bg-primary;
-  border-radius: $border-radius;
-  box-shadow: $shadow-sm;
-
-  .pagination-info {
-    font-size: 14px;
-    color: $text-secondary;
-  }
-
-  .mat-paginator {
-    display: flex;
-    justify-content: flex-end;
-    flex: 1;
-  }
-}
-
-// 自定义对话框样式 - 以深蓝色和白色为主色调
-.add-employee-dialog-container {
-  .mat-dialog-container {
-    background-color: #1e293b;
-    color: white;
-    border-radius: 12px;
-    padding: 0;
-    overflow: hidden;
-    box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
-  }
-
-  .mat-dialog-title {
-    color: white;
-    font-size: 24px;
-    font-weight: 600;
-    padding: 24px 24px 0;
-    margin: 0;
-  }
-
-  .mat-dialog-content {
-    padding: 24px;
-    background-color: white;
-    color: #1e293b;
-    margin: 0;
-  }
-
-  .mat-dialog-actions {
-    padding: 16px 24px;
-    background-color: #1e293b;
-    justify-content: flex-end;
-    margin: 0;
-  }
-}
-
-.custom-dialog {
-  // 对话框容器样式
-  .mat-dialog-container {
-    border-radius: $border-radius;
-    box-shadow: 0 10px 30px -5px rgba(0, 0, 0, 0.15), 0 10px 15px -6px rgba(0, 0, 0, 0.1);
-    overflow: hidden;
-    background-color: $bg-primary; // 使用白色作为主背景
-    border: 2px solid $primary-color; // 深蓝色边框
-    max-width: 700px; // 增加对话框宽度
-    width: 95vw;
-    animation: slideIn 0.3s ease-out; // 添加动画效果
-  }
-  
-  // 对话框标题样式
-  h2 {
-    font-size: 24px; // 增大标题字体
-    font-weight: 700;
-    color: $primary-color; // 使用深蓝色标题
-    margin: 0;
-  }
-  
-  // 输入框样式
-  .mat-input-element {
-    color: $text-primary;
-    font-size: 16px; // 增大输入框字体
-    font-weight: 500;
-  }
-  
-  .mat-form-field {
-    width: 100%;
-    transition: all 0.2s ease;
-  }
-  
-  .mat-form-field-appearance-fill .mat-form-field-flex {
-    background-color: $bg-primary; // 输入框白色背景
-    border-radius: $border-radius;
-    border: 1px solid $border-color;
-    transition: all 0.2s ease;
-  }
-  
-  .mat-form-field-appearance-fill .mat-form-field-outline {
-    border: none;
-  }
-  
-  .mat-form-field-appearance-fill .mat-form-field-underline {
-    display: none;
-  }
-  
-  // 输入框聚焦效果增强
-  .mat-form-field-appearance-fill .mat-form-field-focus-overlay {
-    background-color: rgba(30, 64, 175, 0.08);
-  }
-  
-  .mat-form-field:focus-within .mat-form-field-flex {
-    border-color: $primary-color;
-    box-shadow: 0 0 0 3px rgba(30, 64, 175, 0.1);
-  }
-  
-  // 选择框样式增强
-  .mat-select {
-    color: $text-primary;
-    font-size: 16px;
-  }
-  
-  .mat-select-panel {
-    background-color: $bg-primary;
-    border-radius: $border-radius;
-    box-shadow: $shadow-lg;
-    margin-top: 8px;
-    border: 1px solid $border-color;
-    animation: slideIn 0.2s ease-out;
-  }
-  
-  .mat-option {
-    color: $text-primary;
-    padding: 12px 20px; // 增加选项间距
-    font-size: 16px; // 增大选项字体
-    font-weight: 500;
-    transition: all 0.2s ease;
-    border-radius: $border-radius;
-    margin: 2px 8px;
-    
-    &:hover {
-      background-color: rgba(30, 64, 175, 0.1);
-      transform: translateX(4px); // 悬停时轻微移动
-    }
-    
-    &.mat-selected {
-      background-color: color-mix(in srgb, $primary-color 20%, transparent);
-      color: $primary-color;
-      font-weight: 600;
-    }
-    
-    &:active {
-      transform: scale(0.98);
-    }
-  }
-  
-  // 按钮样式优化
-  button[mat-button] {
-    color: $text-secondary;
-    font-size: 16px; // 增大按钮字体
-    font-weight: 600;
-    padding: 10px 24px;
-    transition: all 0.2s ease;
-    border-radius: $border-radius;
-    
-    &:hover {
-      color: $primary-color;
-      background-color: rgba(30, 64, 175, 0.08);
-      transform: translateY(-1px);
-    }
-    
-    &:active {
-      transform: translateY(0);
-    }
-  }
-  
-  button[mat-raised-button][color="primary"] {
-    background-color: $primary-color;
-    color: white;
-    font-size: 16px; // 增大按钮字体
-    font-weight: 600;
-    border-radius: $border-radius;
-    padding: 12px 32px;
-    transition: all 0.2s ease;
-    box-shadow: 0 4px 12px rgba(30, 64, 175, 0.3);
-    
-    &:hover {
-      background-color: $primary-light;
-      transform: translateY(-2px);
-      box-shadow: 0 6px 16px rgba(30, 64, 175, 0.4);
-    }
-    
-    &:active {
-      transform: translateY(0);
-      box-shadow: 0 4px 10px rgba(30, 64, 175, 0.3);
-    }
-    
-    &:disabled {
-      background-color: $text-tertiary;
-      transform: none;
-      box-shadow: none;
-    }
-  }
-  
-  // 日期选择器样式增强
-  .mat-datepicker-toggle {
-    color: $text-tertiary;
-    transition: all 0.2s ease;
-    
-    &:hover {
-      color: $primary-color;
-      transform: scale(1.1);
-    }
-  }
-  
-  .mat-datepicker-content {
-    border-radius: $border-radius;
-    overflow: hidden;
-    border: 1px solid $border-color;
-  }
-  
-  // 表单标签样式增强
-  label {
-    color: $text-primary;
-    font-weight: 600;
-    font-size: 16px;
-    margin-bottom: 8px;
-    display: block;
-  }
-  
-  // 必填标记样式
-  .required-mark {
-    color: $error-color;
-    font-size: 16px;
-    margin-left: 4px;
-  }
-  
-  // 错误状态样式增强
-  .mat-error {
-    color: $error-color;
-    font-size: 14px;
-    margin-top: 6px;
-    display: block;
-    font-weight: 500;
-  }
-  
-  .mat-form-field-invalid .mat-form-field-flex {
-    border-color: $error-color;
-    background-color: rgba(239, 68, 68, 0.05);
-  }
-  
-  .mat-form-field-invalid .mat-input-element {
-    color: $error-color;
-  }
-}
-
-// 自定义通知消息样式
-.success-snackbar {
-  background-color: $success-color;
-  color: white;
-}
-
-.error-snackbar {
-  background-color: $error-color;
-  color: white;
-}
-
-// 动画定义
-@keyframes slideIn {
-  from {
-    opacity: 0;
-    transform: translateY(-10px);
-  }
-  to {
-    opacity: 1;
-    transform: translateY(0);
-  }
-}
-
-@keyframes pulse {
-  0%, 100% {
-    opacity: 1;
-  }
-  50% {
-    opacity: 0.7;
-  }
-}
-
-// 响应式设计
-@media (max-width: 1200px) {
-  .assets-container {
-    padding: 16px;
-  }
-
-  .action-bar {
-    flex-direction: column;
-    align-items: stretch;
-
-    .search-filters {
-      justify-content: space-between;
-    }
-  }
-}
-
-@media (max-width: 768px) {
-  .assets-container {
-    padding: 12px;
-  }
-
-  .page-header {
-    text-align: center;
-  }
-
-  .action-bar {
-    padding: 12px;
-
-    .search-filters {
-      flex-direction: column;
-      align-items: stretch;
-
-      .search-container,
-      .filter-container,
-      .filter-btn {
-        width: 100%;
-        min-width: auto;
-      }
-    }
-  }
-
-  .table-container {
-    overflow-x: auto;
-  }
-
-  .pagination {
-    flex-direction: column;
-    gap: 12px;
-    align-items: center;
-
-    .mat-paginator {
-      justify-content: center;
-    }
-  }
-}

+ 0 - 522
src/app/pages/hr/assets/assets.ts

@@ -1,522 +0,0 @@
-import { Component, OnInit, signal, Inject } from '@angular/core';
-import { CommonModule } from '@angular/common';
-import { FormsModule, ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
-import { MatButtonModule } from '@angular/material/button';
-import { MatInputModule } from '@angular/material/input';
-import { MatSelectModule } from '@angular/material/select';
-import { MatDatepickerModule } from '@angular/material/datepicker';
-import { MatNativeDateModule } from '@angular/material/core';
-import { MatDialogModule, MatDialog, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
-import { MatTableModule } from '@angular/material/table';
-import { MatPaginatorModule } from '@angular/material/paginator';
-import { MatCheckboxModule } from '@angular/material/checkbox';
-import { MatSnackBarModule, MatSnackBar } from '@angular/material/snack-bar';
-import { MatIconModule } from '@angular/material/icon';
-import { MatTooltipModule } from '@angular/material/tooltip';
-import { MatMenuModule } from '@angular/material/menu';
-import { Employee, Department, Position, Contract, Certificate, EmployeeStatus } from '../../../models/hr.model';
-
-// 创建新增员工对话框组件
-@Component({
-  selector: 'app-add-employee-dialog',
-  standalone: true,
-  imports: [
-    CommonModule,
-    ReactiveFormsModule,
-    MatInputModule,
-    MatSelectModule,
-    MatDatepickerModule,
-    MatNativeDateModule,
-    MatButtonModule
-  ],
-  template: `
-    <div class="dialog-header">
-      <h2>新增员工</h2>
-      <button class="close-btn" (click)="dialogRef.close()">
-        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
-          <line x1="18" y1="6" x2="6" y2="18"></line>
-          <line x1="6" y1="6" x2="18" y2="18"></line>
-        </svg>
-      </button>
-    </div>
-    <form [formGroup]="employeeForm" (ngSubmit)="onSubmit()" class="employee-form">
-      <div class="form-row">
-        <div class="form-group">
-          <label>员工姓名 <span class="required-mark">*</span></label>
-          <input matInput formControlName="name" placeholder="请输入姓名">
-        </div>
-        <div class="form-group">
-          <label>工号 <span class="required-mark">*</span></label>
-          <input matInput formControlName="employeeId" placeholder="请输入工号">
-        </div>
-      </div>
-      <div class="form-row">
-        <div class="form-group">
-          <label>部门 <span class="required-mark">*</span></label>
-          <mat-select formControlName="department">
-            <mat-option *ngFor="let dept of departments" [value]="dept.name">
-              {{ dept.name }}
-            </mat-option>
-          </mat-select>
-        </div>
-        <div class="form-group">
-          <label>岗位 <span class="required-mark">*</span></label>
-          <input matInput formControlName="position" placeholder="请输入岗位">
-        </div>
-      </div>
-      <div class="form-row">
-        <div class="form-group">
-          <label>手机号码 <span class="required-mark">*</span></label>
-          <input matInput formControlName="phone" placeholder="请输入手机号码">
-        </div>
-        <div class="form-group">
-          <label>邮箱 <span class="required-mark">*</span></label>
-          <input matInput formControlName="email" placeholder="请输入邮箱">
-        </div>
-      </div>
-      <div class="form-row">
-        <div class="form-group">
-          <label>性别</label>
-          <mat-select formControlName="gender">
-            <mat-option value="男">男</mat-option>
-            <mat-option value="女">女</mat-option>
-          </mat-select>
-        </div>
-        <div class="form-group">
-          <label>出生日期</label>
-          <input matInput [matDatepicker]="birthDatePicker" formControlName="birthDate">
-          <mat-datepicker-toggle matSuffix [for]="birthDatePicker"></mat-datepicker-toggle>
-          <mat-datepicker #birthDatePicker></mat-datepicker>
-        </div>
-      </div>
-      <div class="form-row">
-        <div class="form-group">
-          <label>入职日期 <span class="required-mark">*</span></label>
-          <input matInput [matDatepicker]="hireDatePicker" formControlName="hireDate">
-          <mat-datepicker-toggle matSuffix [for]="hireDatePicker"></mat-datepicker-toggle>
-          <mat-datepicker #hireDatePicker></mat-datepicker>
-        </div>
-        <div class="form-group">
-          <label>状态 <span class="required-mark">*</span></label>
-          <mat-select formControlName="status">
-            <mat-option value="在职">在职</mat-option>
-            <mat-option value="试用期">试用期</mat-option>
-            <mat-option value="离职">离职</mat-option>
-          </mat-select>
-        </div>
-      </div>
-      <div class="dialog-actions">
-        <button type="button" mat-button (click)="dialogRef.close()">取消</button>
-        <button type="submit" mat-raised-button color="primary" [disabled]="!employeeForm.valid">保存</button>
-      </div>
-    </form>
-  `,
-  styles: [`
-    .dialog-header {
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-      margin-bottom: 32px;
-      padding-bottom: 20px;
-      border-bottom: 2px solid #e5e7eb;
-    }
-    
-    .close-btn {
-      background: none;
-      border: none;
-      cursor: pointer;
-      color: #9ca3af;
-      padding: 6px;
-      border-radius: 50%;
-      transition: all 0.2s ease;
-      
-      &:hover {
-        color: #1f2937;
-        background-color: #f3f4f6;
-        transform: scale(1.15);
-      }
-      
-      &:active {
-        transform: scale(0.95);
-      }
-    }
-    
-    .employee-form {
-      max-width: 100%;
-    }
-    
-    .form-row {
-      display: flex;
-      gap: 24px;
-      margin-bottom: 24px;
-      flex-wrap: wrap;
-    }
-    
-    .form-group {
-      flex: 1;
-      min-width: 250px;
-    }
-    
-    label {
-      display: block;
-      margin-bottom: 8px;
-      font-weight: 600;
-      color: #374151;
-      font-size: 16px;
-    }
-    
-    .required-mark {
-      color: #ef4444;
-      font-size: 16px;
-    }
-    
-    .dialog-actions {
-      display: flex;
-      justify-content: flex-end;
-      gap: 16px;
-      margin-top: 32px;
-      padding-top: 20px;
-      border-top: 1px solid #e5e7eb;
-    }
-    
-    // 表单元素聚焦效果
-    .mat-form-field-appearance-fill .mat-form-field-focus-overlay {
-      background-color: rgba(30, 64, 175, 0.05);
-    }
-    
-    // 错误状态样式
-    .mat-error {
-      color: #ef4444;
-      font-size: 14px;
-      margin-top: 4px;
-      display: block;
-    }
-    
-    .mat-form-field-invalid .mat-input-element {
-      color: #ef4444;
-    }
-  `]
-}) class AddEmployeeDialog {
-  employeeForm: FormGroup;
-  departments = departmentsMock;
-  
-  constructor(
-    private fb: FormBuilder,
-    public dialogRef: MatDialogRef<AddEmployeeDialog>,
-    @Inject(MAT_DIALOG_DATA) public data: any
-  ) {
-    this.employeeForm = this.fb.group({
-      name: ['', Validators.required],
-      employeeId: ['', Validators.required],
-      department: ['', Validators.required],
-      position: ['', Validators.required],
-      phone: ['', [Validators.required, Validators.pattern(/^1[3-9]\d{9}$/)]],
-      email: ['', [Validators.required, Validators.email]],
-      gender: ['男'],
-      birthDate: [null],
-      hireDate: [new Date(), Validators.required],
-      status: ['在职', Validators.required]
-    });
-  }
-  
-  onSubmit() {
-    if (this.employeeForm.valid) {
-      this.dialogRef.close(this.employeeForm.value);
-    }
-  }
-}
-
-// 模拟数据
-const departmentsMock: Department[] = [
-  { id: '1', name: '管理层', employeeCount: 5 },
-  { id: '2', name: '设计部', employeeCount: 20 },
-  { id: '3', name: '客户服务部', employeeCount: 15 },
-  { id: '4', name: '财务部', employeeCount: 8 },
-  { id: '5', name: '人力资源部', employeeCount: 5 }
-];
-
-// 生成模拟员工数据
-const generateMockEmployees = (): Employee[] => {
-  const statuses: EmployeeStatus[] = ['在职', '试用期', '离职'];
-  const genders = ['男', '女'];
-  const departments = departmentsMock.map(d => d.name);
-  const positions = ['经理', '设计师', '客服专员', '财务专员', '人事专员', '助理'];
-  const employees: Employee[] = [];
-  
-  for (let i = 1; i <= 30; i++) {
-    const status = statuses[Math.floor(Math.random() * statuses.length)];
-    const hireDate = new Date();
-    hireDate.setMonth(hireDate.getMonth() - Math.floor(Math.random() * 36));
-    
-    // 随机生成合同信息
-    const contractEndDate = new Date(hireDate);
-    contractEndDate.setFullYear(contractEndDate.getFullYear() + 1);
-    
-    // 判断合同是否即将到期(7天内)
-    const today = new Date();
-    const daysUntilExpiry = Math.ceil((contractEndDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
-    const isExpiringSoon = daysUntilExpiry <= 7 && daysUntilExpiry >= 0;
-    
-    employees.push({
-      id: `emp-${i}`,
-      name: `员工${i}`,
-      employeeId: `EMP${2023}${String(i).padStart(3, '0')}`,
-      department: departments[Math.floor(Math.random() * departments.length)],
-      position: positions[Math.floor(Math.random() * positions.length)],
-      phone: `138${Math.floor(Math.random() * 100000000)}`,
-      email: `employee${i}@example.com`,
-      gender: genders[Math.floor(Math.random() * genders.length)],
-      birthDate: new Date(1980 + Math.floor(Math.random() * 20), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1),
-      hireDate,
-      status,
-      avatar: `https://picsum.photos/seed/emp${i}/40/40`,
-      contract: {
-        id: `contract-${i}`,
-        startDate: hireDate,
-        endDate: contractEndDate,
-        type: '劳动合同',
-        isExpiringSoon
-      },
-      certificates: i % 3 === 0 ? [
-        {
-          id: `cert-${i}-1`,
-          name: '身份证',
-          type: '身份证件',
-          number: `110101${Math.floor(Math.random() * 1000000000000000000)}`,
-          issueDate: new Date(2010 + Math.floor(Math.random() * 10), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1),
-          expiryDate: new Date(2030 + Math.floor(Math.random() * 10), Math.floor(Math.random() * 12), Math.floor(Math.random() * 28) + 1)
-        }
-      ] : undefined
-    });
-  }
-  
-  return employees;
-};
-
-// 主组件
-@Component({
-  selector: 'app-assets',
-  standalone: true,
-  imports: [
-    CommonModule,
-    FormsModule,
-    ReactiveFormsModule,
-    MatButtonModule,
-    MatInputModule,
-    MatSelectModule,
-    MatDialogModule,
-    MatTableModule,
-    MatPaginatorModule,
-    MatCheckboxModule,
-    MatSnackBarModule,
-    MatIconModule,
-    MatTooltipModule,
-    MatMenuModule
-  ],
-  templateUrl: './assets.html',
-  styleUrl: './assets.scss'
-}) export class Assets implements OnInit {
-  // 暴露Math对象给模板使用
-  readonly Math = Math;
-  
-  // 员工数据
-  employees = signal<Employee[]>([]);
-  filteredEmployees = signal<Employee[]>([]);
-  selectedEmployees = signal<string[]>([]);
-  
-  // 搜索和筛选
-  searchTerm = signal('');
-  departmentFilter = signal('');
-  statusFilter = signal('');
-  
-  // 分页
-  pageSize = 10;
-  pageIndex = 0;
-  
-  // 表格列
-  displayedColumns: string[] = ['select', 'name', 'employeeId', 'department', 'position', 'phone', 'email', 'hireDate', 'status', 'contract', 'actions'];
-  
-  // 部门和状态选项
-  departments = departmentsMock;
-  statuses: EmployeeStatus[] = ['在职', '试用期', '离职'];
-  
-  constructor(
-    private dialog: MatDialog,
-    private snackBar: MatSnackBar
-  ) {}
-  
-  ngOnInit() {
-    // 加载模拟数据
-    this.employees.set(generateMockEmployees());
-    this.applyFilters();
-  }
-  
-  // 应用筛选
-  applyFilters() {
-    let filtered = this.employees();
-    
-    // 搜索筛选
-    if (this.searchTerm()) {
-      const term = this.searchTerm().toLowerCase();
-      filtered = filtered.filter(emp => 
-        emp.name.toLowerCase().includes(term) ||
-        emp.employeeId.toLowerCase().includes(term) ||
-        emp.phone.includes(term) ||
-        emp.email.toLowerCase().includes(term)
-      );
-    }
-    
-    // 部门筛选
-    if (this.departmentFilter()) {
-      filtered = filtered.filter(emp => emp.department === this.departmentFilter());
-    }
-    
-    // 状态筛选
-    if (this.statusFilter()) {
-      filtered = filtered.filter(emp => emp.status === this.statusFilter());
-    }
-    
-    this.filteredEmployees.set(filtered);
-  }
-  
-  // 打开新增员工对话框
-  openAddEmployeeDialog() {
-    const dialogRef = this.dialog.open(AddEmployeeDialog, {
-      width: '600px',
-      maxWidth: '90vw',
-      disableClose: true,
-      panelClass: 'custom-dialog'
-    });
-    
-    dialogRef.afterClosed().subscribe(result => {
-      if (result) {
-        const newEmployee: Employee = {
-          id: `emp-${Date.now()}`,
-          ...result,
-          avatar: `https://picsum.photos/seed/emp${Date.now()}/40/40`,
-          contract: {
-            id: `contract-${Date.now()}`,
-            startDate: result.hireDate,
-            endDate: new Date(result.hireDate),
-            type: '劳动合同',
-            isExpiringSoon: false
-          }
-        };
-        
-        // 更新合同结束日期(假设一年合同)
-        if (newEmployee.contract) {
-          newEmployee.contract.endDate.setFullYear(newEmployee.contract.endDate.getFullYear() + 1);
-        }
-        
-        this.employees.update(emps => [newEmployee, ...emps]);
-        this.applyFilters();
-        this.showSuccessMessage('员工添加成功');
-      }
-    });
-  }
-  
-  // 编辑员工
-  editEmployee(employee: Employee) {
-    // 在实际应用中,这里会打开编辑对话框
-    this.showSuccessMessage('编辑功能待实现');
-  }
-  
-  // 删除员工
-  deleteEmployee(id: string) {
-    if (confirm('确定要删除该员工吗?')) {
-      this.employees.update(emps => emps.filter(emp => emp.id !== id));
-      this.applyFilters();
-      this.showSuccessMessage('员工删除成功');
-    }
-  }
-  
-  // 批量删除
-  batchDelete() {
-    if (this.selectedEmployees().length === 0) {
-      this.showErrorMessage('请先选择要删除的员工');
-      return;
-    }
-    
-    if (confirm(`确定要删除选中的 ${this.selectedEmployees().length} 名员工吗?`)) {
-      this.employees.update(emps => emps.filter(emp => !this.selectedEmployees().includes(emp.id)));
-      this.selectedEmployees.set([]);
-      this.applyFilters();
-      this.showSuccessMessage('批量删除成功');
-    }
-  }
-  
-  // 导出数据
-  exportData(format: 'excel' | 'pdf') {
-    this.showSuccessMessage(`${format.toUpperCase()} 导出功能待实现`);
-  }
-  
-  // 切换员工选择
-  toggleEmployeeSelection(id: string) {
-    this.selectedEmployees.update(selected => {
-      if (selected.includes(id)) {
-        return selected.filter(selectedId => selectedId !== id);
-      } else {
-        return [...selected, id];
-      }
-    });
-  }
-  
-  // 全选/取消全选
-  toggleSelectAll() {
-    if (this.isAllSelected()) {
-      this.selectedEmployees.set([]);
-    } else {
-      this.selectedEmployees.set(this.filteredEmployees().map(emp => emp.id));
-    }
-  }
-  
-  // 判断是否全选
-  isAllSelected() {
-    return this.selectedEmployees().length > 0 && 
-           this.selectedEmployees().length === this.filteredEmployees().length;
-  }
-  
-  // 切换员工状态
-  changeEmployeeStatus(employee: Employee, newStatus: EmployeeStatus) {
-    if (employee.status === newStatus) return;
-    
-    this.employees.update(emps => 
-      emps.map(emp => 
-        emp.id === employee.id ? { ...emp, status: newStatus } : emp
-      )
-    );
-    this.applyFilters();
-    this.showSuccessMessage(`员工状态已更新为${newStatus}`);
-  }
-  
-  // 格式化日期
-  formatDate(date: Date | string): string {
-    if (!date) return '';
-    const d = new Date(date);
-    return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
-  }
-  
-  // 显示成功消息
-  showSuccessMessage(message: string) {
-    this.snackBar.open(message, '关闭', {
-      duration: 3000,
-      verticalPosition: 'top',
-      panelClass: 'success-snackbar'
-    });
-  }
-  
-  // 显示错误消息
-  showErrorMessage(message: string) {
-    this.snackBar.open(message, '关闭', {
-      duration: 3000,
-      verticalPosition: 'top',
-      panelClass: 'error-snackbar'
-    });
-  }
-  
-  // 分页事件处理
-  onPageChange(event: any) {
-    this.pageIndex = event.pageIndex;
-    this.pageSize = event.pageSize;
-  }
-}

+ 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 - 5
src/app/pages/hr/hr-layout/hr-layout.html

@@ -37,11 +37,7 @@
           <span matListItemTitle *ngIf="isExpanded">考勤统计</span>
         </a>
 
-        <!-- 资产管理 -->
-        <a mat-list-item routerLink="/hr/assets" routerLinkActive="active-link">
-          <mat-icon matListItemIcon>inventory</mat-icon>
-          <span matListItemTitle *ngIf="isExpanded">资产管理</span>
-        </a>
+
       </mat-nav-list>
 
       <!-- 导航栏底部 -->

+ 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"