Compare commits

...

1 Commits

Author SHA1 Message Date
a2ae47fc01 Add 929 project changes 2026-06-25 09:28:28 +08:00
66 changed files with 9687 additions and 5167 deletions

554
bun.lock
View File

@ -18,18 +18,20 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"decimal.js": "^10.6.0", "decimal.js": "^10.6.0",
"docxtemplater": "^3.68.5",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"file-saver": "^2.0.5",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lucide-vue-next": "^0.563.0", "lucide-vue-next": "^0.563.0",
"motion-v": "^2.0.0", "motion-v": "^2.0.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1", "pinia-plugin-persistedstate": "^4.7.1",
"pizzip": "^3.2.0",
"reka-ui": "^2.8.0", "reka-ui": "^2.8.0",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"vue": "^3.5.25", "vue": "^3.5.25",
"vue-i18n": "^11.3.0", "vue-i18n": "^11.3.0",
"vue-router": "^4.6.4",
"vuedraggable": "^4.1.0", "vuedraggable": "^4.1.0",
}, },
"devDependencies": { "devDependencies": {
@ -45,548 +47,554 @@
}, },
}, },
"packages": { "packages": {
"@ag-grid-community/locale": ["@ag-grid-community/locale@35.1.0", "https://registry.npmmirror.com/@ag-grid-community/locale/-/locale-35.1.0.tgz", {}, "sha512-Tez1imtqfipMT3O1Ay+dyDcFJIj6H6gXBp45s44pwkzWQzxO20IBpZUrmAPTNRMYVZNXqCVbNsozWrPaVFAgeQ=="], "@ag-grid-community/locale": ["@ag-grid-community/locale@35.1.0", "", {}, "sha512-Tez1imtqfipMT3O1Ay+dyDcFJIj6H6gXBp45s44pwkzWQzxO20IBpZUrmAPTNRMYVZNXqCVbNsozWrPaVFAgeQ=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@babel/parser": ["@babel/parser@7.29.0", "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.0.tgz", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], "@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
"@babel/types": ["@babel/types@7.29.0", "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@emnapi/core": ["@emnapi/core@1.8.1", "https://registry.npmmirror.com/@emnapi/core/-/core-1.8.1.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@fast-csv/format": ["@fast-csv/format@4.3.5", "https://registry.npmmirror.com/@fast-csv/format/-/format-4.3.5.tgz", { "dependencies": { "@types/node": "^14.0.1", "lodash.escaperegexp": "^4.1.2", "lodash.isboolean": "^3.0.3", "lodash.isequal": "^4.5.0", "lodash.isfunction": "^3.0.9", "lodash.isnil": "^4.0.0" } }, "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A=="], "@fast-csv/format": ["@fast-csv/format@4.3.5", "", { "dependencies": { "@types/node": "^14.0.1", "lodash.escaperegexp": "^4.1.2", "lodash.isboolean": "^3.0.3", "lodash.isequal": "^4.5.0", "lodash.isfunction": "^3.0.9", "lodash.isnil": "^4.0.0" } }, "sha512-8iRn6QF3I8Ak78lNAa+Gdl5MJJBM5vRHivFtMRUWINdevNo00K7OXxS2PshawLKTejVwieIlPmK5YlLu6w4u8A=="],
"@fast-csv/parse": ["@fast-csv/parse@4.3.6", "https://registry.npmmirror.com/@fast-csv/parse/-/parse-4.3.6.tgz", { "dependencies": { "@types/node": "^14.0.1", "lodash.escaperegexp": "^4.1.2", "lodash.groupby": "^4.6.0", "lodash.isfunction": "^3.0.9", "lodash.isnil": "^4.0.0", "lodash.isundefined": "^3.0.1", "lodash.uniq": "^4.5.0" } }, "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA=="], "@fast-csv/parse": ["@fast-csv/parse@4.3.6", "", { "dependencies": { "@types/node": "^14.0.1", "lodash.escaperegexp": "^4.1.2", "lodash.groupby": "^4.6.0", "lodash.isfunction": "^3.0.9", "lodash.isnil": "^4.0.0", "lodash.isundefined": "^3.0.1", "lodash.uniq": "^4.5.0" } }, "sha512-uRsLYksqpbDmWaSmzvJcuApSEe38+6NQZBUsuAyMZKqHxH0g1wcJgsKUvN3WC8tewaqFjBMMGrkHmC+T7k8LvA=="],
"@floating-ui/core": ["@floating-ui/core@1.7.4", "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.4.tgz", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.5", "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.5.tgz", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.10.tgz", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
"@floating-ui/vue": ["@floating-ui/vue@1.1.10", "https://registry.npmmirror.com/@floating-ui/vue/-/vue-1.1.10.tgz", { "dependencies": { "@floating-ui/dom": "^1.7.5", "@floating-ui/utils": "^0.2.10", "vue-demi": ">=0.13.0" } }, "sha512-vdf8f6rHnFPPLRsmL4p12wYl+Ux4mOJOkjzKEMYVnwdf7UFdvBtHlLvQyx8iKG5vhPRbDRgZxdtpmyigDPjzYg=="], "@floating-ui/vue": ["@floating-ui/vue@1.1.10", "", { "dependencies": { "@floating-ui/dom": "^1.7.5", "@floating-ui/utils": "^0.2.10", "vue-demi": ">=0.13.0" } }, "sha512-vdf8f6rHnFPPLRsmL4p12wYl+Ux4mOJOkjzKEMYVnwdf7UFdvBtHlLvQyx8iKG5vhPRbDRgZxdtpmyigDPjzYg=="],
"@iconify/types": ["@iconify/types@2.0.0", "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
"@iconify/vue": ["@iconify/vue@5.0.0", "https://registry.npmmirror.com/@iconify/vue/-/vue-5.0.0.tgz", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "vue": ">=3" } }, "sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg=="], "@iconify/vue": ["@iconify/vue@5.0.0", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "vue": ">=3" } }, "sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg=="],
"@internationalized/date": ["@internationalized/date@3.12.0", "https://registry.npmmirror.com/@internationalized/date/-/date-3.12.0.tgz", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ=="], "@internationalized/date": ["@internationalized/date@3.12.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ=="],
"@internationalized/number": ["@internationalized/number@3.6.5", "https://registry.npmmirror.com/@internationalized/number/-/number-3.6.5.tgz", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g=="], "@internationalized/number": ["@internationalized/number@3.6.5", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g=="],
"@intlify/core-base": ["@intlify/core-base@11.3.1", "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.3.1.tgz", { "dependencies": { "@intlify/devtools-types": "11.3.1", "@intlify/message-compiler": "11.3.1", "@intlify/shared": "11.3.1" } }, "sha512-9nG3ItSD5ApZHmTbv2UFqvJSy3m+u6C/orMohukNKoT/Yuwiz8tPtlNw6ylLuPqSP2kP7ZF4Cdqwp6V1m3BQgw=="], "@intlify/core-base": ["@intlify/core-base@11.4.6", "", { "dependencies": { "@intlify/devtools-types": "11.4.6", "@intlify/message-compiler": "11.4.6", "@intlify/shared": "11.4.6" } }, "sha512-EOeHO95XESK9IFHgHeZXunsM/WBAoCA0DlaWODvx14vKmetAuS97t+l6Xe9hTUqntPpF93vtVSjjUDafw3wXMw=="],
"@intlify/devtools-types": ["@intlify/devtools-types@11.3.1", "https://registry.npmmirror.com/@intlify/devtools-types/-/devtools-types-11.3.1.tgz", { "dependencies": { "@intlify/core-base": "11.3.1", "@intlify/shared": "11.3.1" } }, "sha512-qrOknIx294W4YYVYBgldDOeOnv2NlpabW+aYGjMuXMSrY36f7GCAPlEBE2G+qTC5x0oAWDBSY5BmvLlPUx1exg=="], "@intlify/devtools-types": ["@intlify/devtools-types@11.4.6", "", { "dependencies": { "@intlify/core-base": "11.4.6", "@intlify/shared": "11.4.6" } }, "sha512-wowQPpNem56b2d43IJmqbrzG2FeBKe5f/kUGlpNuBmXs6OSqncF8m1+1lxHuW8ISZJF0ma2RkW3iLkw0g0G4VA=="],
"@intlify/message-compiler": ["@intlify/message-compiler@11.3.1", "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.3.1.tgz", { "dependencies": { "@intlify/shared": "11.3.1", "source-map-js": "^1.0.2" } }, "sha512-uIa4YurbphU+4Cl5CoL6nq/c7uQhVNRowEelgboNmXNs+UEcyFLQBESwaUjMvdtYxzA2qh+vGim080KZ84ruDA=="], "@intlify/message-compiler": ["@intlify/message-compiler@11.4.6", "", { "dependencies": { "@intlify/shared": "11.4.6", "source-map-js": "^1.0.2" } }, "sha512-5nj3jULqeTAC1WovwMs1LQWgatTa2pM/rXN9T3XW8rdOtXW9ZF6/GLSNFTKDQmPLwclhPdgUWLJ/4w3fMeeC/Q=="],
"@intlify/shared": ["@intlify/shared@11.3.1", "https://registry.npmmirror.com/@intlify/shared/-/shared-11.3.1.tgz", {}, "sha512-9GWc5PKuRdeWkT7FJN43c/+rD6xpSB3WtizewkfFCK/0XzYqCk4gQBWWcTdfKo8ylEcHwqYsR2Z3HRE3XhEHrQ=="], "@intlify/shared": ["@intlify/shared@11.4.6", "", {}, "sha512-m1p1HHAMLhqSpTRH7VnXdrN0CQ4y+9vunFkpLkbD8soIuBsnQdawZXqMCgvwI2UVF9Ww7sVaw7g9tV2VO7shoA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"@noble/ciphers": ["@noble/ciphers@2.1.1", "https://registry.npmmirror.com/@noble/ciphers/-/ciphers-2.1.1.tgz", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="], "@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="],
"@noble/hashes": ["@noble/hashes@2.0.1", "https://registry.npmmirror.com/@noble/hashes/-/hashes-2.0.1.tgz", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], "@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
"@oxc-project/runtime": ["@oxc-project/runtime@0.114.0", "https://registry.npmmirror.com/@oxc-project/runtime/-/runtime-0.114.0.tgz", {}, "sha512-mVGQvr/uFJGQ3hsvgQ1sJfh79t5owyZZZtw+VaH+WhtvsmtgjT6imznB9sz2Q67Q0/4obM9mOOtQscU4aJteSg=="], "@oxc-project/runtime": ["@oxc-project/runtime@0.114.0", "", {}, "sha512-mVGQvr/uFJGQ3hsvgQ1sJfh79t5owyZZZtw+VaH+WhtvsmtgjT6imznB9sz2Q67Q0/4obM9mOOtQscU4aJteSg=="],
"@oxc-project/types": ["@oxc-project/types@0.114.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.114.0.tgz", {}, "sha512-//nBfbzHQHvJs8oFIjv6coZ6uxQ4alLfiPe6D5vit6c4pmxATHHlVwgB1k+Hv4yoAMyncdxgRBF5K4BYWUCzvA=="], "@oxc-project/types": ["@oxc-project/types@0.114.0", "", {}, "sha512-//nBfbzHQHvJs8oFIjv6coZ6uxQ4alLfiPe6D5vit6c4pmxATHHlVwgB1k+Hv4yoAMyncdxgRBF5K4BYWUCzvA=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.5", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.5.tgz", { "os": "android", "cpu": "arm64" }, "sha512-zCEmUrt1bggwgBgeKLxNj217J1OrChrp3jJt24VK9jAharSTeVaHODNL+LpcQVhRz+FktYWfT9cjo5oZ99ZLpg=="], "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.5", "", { "os": "android", "cpu": "arm64" }, "sha512-zCEmUrt1bggwgBgeKLxNj217J1OrChrp3jJt24VK9jAharSTeVaHODNL+LpcQVhRz+FktYWfT9cjo5oZ99ZLpg=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.5", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.5.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-ZP9xb9lPAex36pvkNWCjSEJW/Gfdm9I3ssiqOFLmpZ/vosPXgpoGxCmh+dX1Qs+/bWQE6toNFXWWL8vYoKoK9Q=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZP9xb9lPAex36pvkNWCjSEJW/Gfdm9I3ssiqOFLmpZ/vosPXgpoGxCmh+dX1Qs+/bWQE6toNFXWWL8vYoKoK9Q=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.5", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.5.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-7IdrPunf6dp9mywMgTOKMMGDnMHQ6+h5gRl6LW8rhD8WK2kXX0IwzcM5Zc0B5J7xQs8QWOlKjv8BJsU/1CD3pg=="], "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-7IdrPunf6dp9mywMgTOKMMGDnMHQ6+h5gRl6LW8rhD8WK2kXX0IwzcM5Zc0B5J7xQs8QWOlKjv8BJsU/1CD3pg=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.5", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.5.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-o/JCk+dL0IN68EBhZ4DqfsfvxPfMeoM6cJtxORC1YYoxGHZyth2Kb2maXDb4oddw2wu8iIbnYXYPEzBtAF5CAg=="], "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-o/JCk+dL0IN68EBhZ4DqfsfvxPfMeoM6cJtxORC1YYoxGHZyth2Kb2maXDb4oddw2wu8iIbnYXYPEzBtAF5CAg=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.5", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.5.tgz", { "os": "linux", "cpu": "arm" }, "sha512-IIBwTtA6VwxQLcEgq2mfrUgam7VvPZjhd/jxmeS1npM+edWsrrpRLHUdze+sk4rhb8/xpP3flemgcZXXUW6ukw=="], "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.5", "", { "os": "linux", "cpu": "arm" }, "sha512-IIBwTtA6VwxQLcEgq2mfrUgam7VvPZjhd/jxmeS1npM+edWsrrpRLHUdze+sk4rhb8/xpP3flemgcZXXUW6ukw=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.5", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.5.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-KSol1De1spMZL+Xg7K5IBWXIvRWv7+pveaxFWXpezezAG7CS6ojzRjtCGCiLxQricutTAi/LkNWKMsd2wNhMKQ=="], "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-KSol1De1spMZL+Xg7K5IBWXIvRWv7+pveaxFWXpezezAG7CS6ojzRjtCGCiLxQricutTAi/LkNWKMsd2wNhMKQ=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.5", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.5.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-WFljyDkxtXRlWxMjxeegf7xMYXxUr8u7JdXlOEWKYgDqEgxUnSEsVDxBiNWQ1D5kQKwf8Wo4sVKEYPRhCdsjwA=="], "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-WFljyDkxtXRlWxMjxeegf7xMYXxUr8u7JdXlOEWKYgDqEgxUnSEsVDxBiNWQ1D5kQKwf8Wo4sVKEYPRhCdsjwA=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.5", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.5.tgz", { "os": "linux", "cpu": "x64" }, "sha512-CUlplTujmbDWp2gamvrqVKi2Or8lmngXT1WxsizJfts7JrvfGhZObciaY/+CbdbS9qNnskvwMZNEhTPrn7b+WA=="], "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.5", "", { "os": "linux", "cpu": "x64" }, "sha512-CUlplTujmbDWp2gamvrqVKi2Or8lmngXT1WxsizJfts7JrvfGhZObciaY/+CbdbS9qNnskvwMZNEhTPrn7b+WA=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.5", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.5.tgz", { "os": "linux", "cpu": "x64" }, "sha512-wdf7g9NbVZCeAo2iGhsjJb7I8ZFfs6X8bumfrWg82VK+8P6AlLXwk48a1ASiJQDTS7Svq2xVzZg3sGO2aXpHRA=="], "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.5", "", { "os": "linux", "cpu": "x64" }, "sha512-wdf7g9NbVZCeAo2iGhsjJb7I8ZFfs6X8bumfrWg82VK+8P6AlLXwk48a1ASiJQDTS7Svq2xVzZg3sGO2aXpHRA=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.5", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.5.tgz", { "os": "none", "cpu": "arm64" }, "sha512-0CWY7ubu12nhzz+tkpHjoG3IRSTlWYe0wrfJRf4qqjqQSGtAYgoL9kwzdvlhaFdZ5ffVeyYw9qLsChcjUMEloQ=="], "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.5", "", { "os": "none", "cpu": "arm64" }, "sha512-0CWY7ubu12nhzz+tkpHjoG3IRSTlWYe0wrfJRf4qqjqQSGtAYgoL9kwzdvlhaFdZ5ffVeyYw9qLsChcjUMEloQ=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.5", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.5.tgz", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-LztXnGzv6t2u830mnZrFLRVqT/DPJ9DL4ZTz/y93rqUVkeHjMMYIYaFj+BUthiYxbVH9dH0SZYufETspKY/NhA=="], "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.5", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-LztXnGzv6t2u830mnZrFLRVqT/DPJ9DL4ZTz/y93rqUVkeHjMMYIYaFj+BUthiYxbVH9dH0SZYufETspKY/NhA=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.5", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.5.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-jUct1XVeGtyjqJXEAfvdFa8xoigYZ2rge7nYEm70ppQxpfH9ze2fbIrpHmP2tNM2vL/F6Dd0CpXhpjPbC6bSxQ=="], "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-jUct1XVeGtyjqJXEAfvdFa8xoigYZ2rge7nYEm70ppQxpfH9ze2fbIrpHmP2tNM2vL/F6Dd0CpXhpjPbC6bSxQ=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.5", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-VQ8F9ld5gw29epjnVGdrx8ugiLTe8BMqmhDYy7nGbdeDo4HAt4bgdZvLbViEhg7DZyHLpiEUlO5/jPSUrIuxRQ=="], "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.5", "", { "os": "win32", "cpu": "x64" }, "sha512-VQ8F9ld5gw29epjnVGdrx8ugiLTe8BMqmhDYy7nGbdeDo4HAt4bgdZvLbViEhg7DZyHLpiEUlO5/jPSUrIuxRQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="],
"@swc/helpers": ["@swc/helpers@0.5.18", "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.18.tgz", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="], "@swc/helpers": ["@swc/helpers@0.5.18", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="],
"@tailwindcss/node": ["@tailwindcss/node@4.2.0", "https://registry.npmmirror.com/@tailwindcss/node/-/node-4.2.0.tgz", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.0" } }, "sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q=="], "@tailwindcss/node": ["@tailwindcss/node@4.2.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.0" } }, "sha512-Yv+fn/o2OmL5fh/Ir62VXItdShnUxfpkMA4Y7jdeC8O81WPB8Kf6TT6GSHvnqgSwDzlB5iT7kDpeXxLsUS0T6Q=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.0", "https://registry.npmmirror.com/@tailwindcss/oxide/-/oxide-4.2.0.tgz", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.0", "@tailwindcss/oxide-darwin-arm64": "4.2.0", "@tailwindcss/oxide-darwin-x64": "4.2.0", "@tailwindcss/oxide-freebsd-x64": "4.2.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.0", "@tailwindcss/oxide-linux-arm64-musl": "4.2.0", "@tailwindcss/oxide-linux-x64-gnu": "4.2.0", "@tailwindcss/oxide-linux-x64-musl": "4.2.0", "@tailwindcss/oxide-wasm32-wasi": "4.2.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.0", "@tailwindcss/oxide-win32-x64-msvc": "4.2.0" } }, "sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.0", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.0", "@tailwindcss/oxide-darwin-arm64": "4.2.0", "@tailwindcss/oxide-darwin-x64": "4.2.0", "@tailwindcss/oxide-freebsd-x64": "4.2.0", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.0", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.0", "@tailwindcss/oxide-linux-arm64-musl": "4.2.0", "@tailwindcss/oxide-linux-x64-gnu": "4.2.0", "@tailwindcss/oxide-linux-x64-musl": "4.2.0", "@tailwindcss/oxide-wasm32-wasi": "4.2.0", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.0", "@tailwindcss/oxide-win32-x64-msvc": "4.2.0" } }, "sha512-AZqQzADaj742oqn2xjl5JbIOzZB/DGCYF/7bpvhA8KvjUj9HJkag6bBuwZvH1ps6dfgxNHyuJVlzSr2VpMgdTQ=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.0", "https://registry.npmmirror.com/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw=="], "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.0", "", { "os": "android", "cpu": "arm64" }, "sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.0", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA=="], "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.0", "https://registry.npmmirror.com/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg=="], "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.0", "https://registry.npmmirror.com/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.0.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw=="], "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.0", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.0.tgz", { "os": "linux", "cpu": "arm" }, "sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg=="], "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.0", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-Mff5a5Q3WoQR01pGU1gr29hHM1N93xYrKkGXfPw/aRtK4bOc331Ho4Tgfsm5WDGvpevqMpdlkCojT3qlCQbCpA=="], "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Mff5a5Q3WoQR01pGU1gr29hHM1N93xYrKkGXfPw/aRtK4bOc331Ho4Tgfsm5WDGvpevqMpdlkCojT3qlCQbCpA=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.0", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A=="], "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.0", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA=="], "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-/hlXCBqn9K6fi7eAM0RsobHwJYa5V/xzWspVTzxnX+Ft9v6n+30Pz8+RxCn7sQL/vRHHLS30iQPrHQunu6/vJA=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.0", "https://registry.npmmirror.com/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw=="], "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.0", "https://registry.npmmirror.com/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.0.tgz", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw=="], "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.0", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-xuDjhAsFdUuFP5W9Ze4k/o4AskUtI8bcAGU4puTYprr89QaYFmhYOPfP+d1pH+k9ets6RoE23BXZM1X1jJqoyw=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.0", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.0.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-2UU/15y1sWDEDNJXxEIrfWKC2Yb4YgIW5Xz2fKFqGzFWfoMHWFlfa1EJlGO2Xzjkq/tvSarh9ZTjvbxqWvLLXA=="], "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-2UU/15y1sWDEDNJXxEIrfWKC2Yb4YgIW5Xz2fKFqGzFWfoMHWFlfa1EJlGO2Xzjkq/tvSarh9ZTjvbxqWvLLXA=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.0", "https://registry.npmmirror.com/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-CrFadmFoc+z76EV6LPG1jx6XceDsaCG3lFhyLNo/bV9ByPrE+FnBPckXQVP4XRkN76h3Fjt/a+5Er/oA/nCBvQ=="], "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.0", "", { "os": "win32", "cpu": "x64" }, "sha512-CrFadmFoc+z76EV6LPG1jx6XceDsaCG3lFhyLNo/bV9ByPrE+FnBPckXQVP4XRkN76h3Fjt/a+5Er/oA/nCBvQ=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.2.0", "https://registry.npmmirror.com/@tailwindcss/vite/-/vite-4.2.0.tgz", { "dependencies": { "@tailwindcss/node": "4.2.0", "@tailwindcss/oxide": "4.2.0", "tailwindcss": "4.2.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-da9mFCaHpoOgtQiWtDGIikTrSpUFBtIZCG3jy/u2BGV+l/X1/pbxzmIUxNt6JWm19N3WtGi4KlJdSH/Si83WOA=="], "@tailwindcss/vite": ["@tailwindcss/vite@4.2.0", "", { "dependencies": { "@tailwindcss/node": "4.2.0", "@tailwindcss/oxide": "4.2.0", "tailwindcss": "4.2.0" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-da9mFCaHpoOgtQiWtDGIikTrSpUFBtIZCG3jy/u2BGV+l/X1/pbxzmIUxNt6JWm19N3WtGi4KlJdSH/Si83WOA=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.18", "https://registry.npmmirror.com/@tanstack/virtual-core/-/virtual-core-3.13.18.tgz", {}, "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg=="], "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.18", "", {}, "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg=="],
"@tanstack/vue-virtual": ["@tanstack/vue-virtual@3.13.18", "https://registry.npmmirror.com/@tanstack/vue-virtual/-/vue-virtual-3.13.18.tgz", { "dependencies": { "@tanstack/virtual-core": "3.13.18" }, "peerDependencies": { "vue": "^2.7.0 || ^3.0.0" } }, "sha512-6pT8HdHtTU5Z+t906cGdCroUNA5wHjFXsNss9gwk7QAr1VNZtz9IQCs2Nhx0gABK48c+OocHl2As+TMg8+Hy4A=="], "@tanstack/vue-virtual": ["@tanstack/vue-virtual@3.13.18", "", { "dependencies": { "@tanstack/virtual-core": "3.13.18" }, "peerDependencies": { "vue": "^2.7.0 || ^3.0.0" } }, "sha512-6pT8HdHtTU5Z+t906cGdCroUNA5wHjFXsNss9gwk7QAr1VNZtz9IQCs2Nhx0gABK48c+OocHl2As+TMg8+Hy4A=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/bun": ["@types/bun@1.3.9", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.9.tgz", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@types/node": ["@types/node@24.10.13", "https://registry.npmmirror.com/@types/node/-/node-24.10.13.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="], "@types/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="],
"@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="], "@types/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
"@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="], "@vitejs/plugin-vue": ["@vitejs/plugin-vue@6.0.4", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.2" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0", "vue": "^3.2.25" } }, "sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ=="],
"@volar/language-core": ["@volar/language-core@2.4.28", "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.28.tgz", { "dependencies": { "@volar/source-map": "2.4.28" } }, "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ=="], "@volar/language-core": ["@volar/language-core@2.4.28", "", { "dependencies": { "@volar/source-map": "2.4.28" } }, "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ=="],
"@volar/source-map": ["@volar/source-map@2.4.28", "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.28.tgz", {}, "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ=="], "@volar/source-map": ["@volar/source-map@2.4.28", "", {}, "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ=="],
"@volar/typescript": ["@volar/typescript@2.4.28", "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.28.tgz", { "dependencies": { "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw=="], "@volar/typescript": ["@volar/typescript@2.4.28", "", { "dependencies": { "@volar/language-core": "2.4.28", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw=="],
"@vue/compiler-core": ["@vue/compiler-core@3.5.28", "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.28.tgz", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.28", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ=="], "@vue/compiler-core": ["@vue/compiler-core@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/shared": "3.5.28", "entities": "^7.0.1", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ=="],
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.28", "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz", { "dependencies": { "@vue/compiler-core": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA=="], "@vue/compiler-dom": ["@vue/compiler-dom@3.5.28", "", { "dependencies": { "@vue/compiler-core": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA=="],
"@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.28", "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.28", "@vue/compiler-dom": "3.5.28", "@vue/compiler-ssr": "3.5.28", "@vue/shared": "3.5.28", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g=="], "@vue/compiler-sfc": ["@vue/compiler-sfc@3.5.28", "", { "dependencies": { "@babel/parser": "^7.29.0", "@vue/compiler-core": "3.5.28", "@vue/compiler-dom": "3.5.28", "@vue/compiler-ssr": "3.5.28", "@vue/shared": "3.5.28", "estree-walker": "^2.0.2", "magic-string": "^0.30.21", "postcss": "^8.5.6", "source-map-js": "^1.2.1" } }, "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g=="],
"@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.28", "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz", { "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g=="], "@vue/compiler-ssr": ["@vue/compiler-ssr@3.5.28", "", { "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g=="],
"@vue/devtools-api": ["@vue/devtools-api@7.7.9", "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.9.tgz", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="], "@vue/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="],
"@vue/devtools-kit": ["@vue/devtools-kit@7.7.9", "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz", { "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA=="], "@vue/devtools-kit": ["@vue/devtools-kit@7.7.9", "", { "dependencies": { "@vue/devtools-shared": "^7.7.9", "birpc": "^2.3.0", "hookable": "^5.5.3", "mitt": "^3.0.1", "perfect-debounce": "^1.0.0", "speakingurl": "^14.0.1", "superjson": "^2.2.2" } }, "sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA=="],
"@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="], "@vue/devtools-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="],
"@vue/language-core": ["@vue/language-core@3.2.5", "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.2.5.tgz", { "dependencies": { "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" } }, "sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g=="], "@vue/language-core": ["@vue/language-core@3.2.5", "", { "dependencies": { "@volar/language-core": "2.4.28", "@vue/compiler-dom": "^3.5.0", "@vue/shared": "^3.5.0", "alien-signals": "^3.0.0", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1", "picomatch": "^4.0.2" } }, "sha512-d3OIxN/+KRedeM5wQ6H6NIpwS3P5gC9nmyaHgBk+rO6dIsjY+tOh4UlPpiZbAh3YtLdCGEX4M16RmsBqPmJV+g=="],
"@vue/reactivity": ["@vue/reactivity@3.5.28", "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.28.tgz", { "dependencies": { "@vue/shared": "3.5.28" } }, "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw=="], "@vue/reactivity": ["@vue/reactivity@3.5.28", "", { "dependencies": { "@vue/shared": "3.5.28" } }, "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw=="],
"@vue/runtime-core": ["@vue/runtime-core@3.5.28", "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.28.tgz", { "dependencies": { "@vue/reactivity": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ=="], "@vue/runtime-core": ["@vue/runtime-core@3.5.28", "", { "dependencies": { "@vue/reactivity": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ=="],
"@vue/runtime-dom": ["@vue/runtime-dom@3.5.28", "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz", { "dependencies": { "@vue/reactivity": "3.5.28", "@vue/runtime-core": "3.5.28", "@vue/shared": "3.5.28", "csstype": "^3.2.3" } }, "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA=="], "@vue/runtime-dom": ["@vue/runtime-dom@3.5.28", "", { "dependencies": { "@vue/reactivity": "3.5.28", "@vue/runtime-core": "3.5.28", "@vue/shared": "3.5.28", "csstype": "^3.2.3" } }, "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA=="],
"@vue/server-renderer": ["@vue/server-renderer@3.5.28", "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.28.tgz", { "dependencies": { "@vue/compiler-ssr": "3.5.28", "@vue/shared": "3.5.28" }, "peerDependencies": { "vue": "3.5.28" } }, "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg=="], "@vue/server-renderer": ["@vue/server-renderer@3.5.28", "", { "dependencies": { "@vue/compiler-ssr": "3.5.28", "@vue/shared": "3.5.28" }, "peerDependencies": { "vue": "3.5.28" } }, "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg=="],
"@vue/shared": ["@vue/shared@3.5.28", "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.28.tgz", {}, "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ=="], "@vue/shared": ["@vue/shared@3.5.28", "", {}, "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ=="],
"@vue/tsconfig": ["@vue/tsconfig@0.8.1", "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.8.1.tgz", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g=="], "@vue/tsconfig": ["@vue/tsconfig@0.8.1", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g=="],
"@vueuse/core": ["@vueuse/core@14.2.1", "https://registry.npmmirror.com/@vueuse/core/-/core-14.2.1.tgz", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.2.1", "@vueuse/shared": "14.2.1" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ=="], "@vueuse/core": ["@vueuse/core@14.2.1", "", { "dependencies": { "@types/web-bluetooth": "^0.0.21", "@vueuse/metadata": "14.2.1", "@vueuse/shared": "14.2.1" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ=="],
"@vueuse/metadata": ["@vueuse/metadata@14.2.1", "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.2.1.tgz", {}, "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw=="], "@vueuse/metadata": ["@vueuse/metadata@14.2.1", "", {}, "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw=="],
"@vueuse/shared": ["@vueuse/shared@14.2.1", "https://registry.npmmirror.com/@vueuse/shared/-/shared-14.2.1.tgz", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw=="], "@vueuse/shared": ["@vueuse/shared@14.2.1", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw=="],
"ag-charts-community": ["ag-charts-community@13.1.0", "https://registry.npmmirror.com/ag-charts-community/-/ag-charts-community-13.1.0.tgz", { "dependencies": { "ag-charts-core": "13.1.0", "ag-charts-locale": "13.1.0", "ag-charts-types": "13.1.0" } }, "sha512-w+uFTjxlAoTq1+8tgUORtB/zr9jm38ibXzbbWnkBP9Dep9yahi5a1jZL7yExAX35uq3g9QtjTh0Oj/QPDBQ9Ew=="], "@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="],
"ag-charts-core": ["ag-charts-core@13.1.0", "https://registry.npmmirror.com/ag-charts-core/-/ag-charts-core-13.1.0.tgz", { "dependencies": { "ag-charts-types": "13.1.0" } }, "sha512-mLHJZ8oU5CPeLRURescdISCtMsiiA/m4d1iBr6aQBEgiTVogRMGpFpsYNtQiYtoW2sRh+62I9sN8fhC3JQjX/g=="], "ag-charts-community": ["ag-charts-community@13.1.0", "", { "dependencies": { "ag-charts-core": "13.1.0", "ag-charts-locale": "13.1.0", "ag-charts-types": "13.1.0" } }, "sha512-w+uFTjxlAoTq1+8tgUORtB/zr9jm38ibXzbbWnkBP9Dep9yahi5a1jZL7yExAX35uq3g9QtjTh0Oj/QPDBQ9Ew=="],
"ag-charts-enterprise": ["ag-charts-enterprise@13.1.0", "https://registry.npmmirror.com/ag-charts-enterprise/-/ag-charts-enterprise-13.1.0.tgz", { "dependencies": { "ag-charts-community": "13.1.0", "ag-charts-core": "13.1.0" } }, "sha512-WyKIqvkOdtdvEJxq76hjTacXTCpIR2lq1JDMYc5MtoHYtiVt1KHApsxS0nbutp/CxGKRgdOqJtxUF+3r33pgPw=="], "ag-charts-core": ["ag-charts-core@13.1.0", "", { "dependencies": { "ag-charts-types": "13.1.0" } }, "sha512-mLHJZ8oU5CPeLRURescdISCtMsiiA/m4d1iBr6aQBEgiTVogRMGpFpsYNtQiYtoW2sRh+62I9sN8fhC3JQjX/g=="],
"ag-charts-locale": ["ag-charts-locale@13.1.0", "https://registry.npmmirror.com/ag-charts-locale/-/ag-charts-locale-13.1.0.tgz", {}, "sha512-mPgJnVsOI4Cf17CAlRh8BvLz19e165sdQJeUXNaB7M+DPB+pxODOcfx4oqZlR4Wc8Zu++TGb/2ueHa/aeV2qeQ=="], "ag-charts-enterprise": ["ag-charts-enterprise@13.1.0", "", { "dependencies": { "ag-charts-community": "13.1.0", "ag-charts-core": "13.1.0" } }, "sha512-WyKIqvkOdtdvEJxq76hjTacXTCpIR2lq1JDMYc5MtoHYtiVt1KHApsxS0nbutp/CxGKRgdOqJtxUF+3r33pgPw=="],
"ag-charts-types": ["ag-charts-types@13.1.0", "https://registry.npmmirror.com/ag-charts-types/-/ag-charts-types-13.1.0.tgz", {}, "sha512-DytRM3CXli+Y013SC1Mr8lQBrhVTACK+11ilDHOhwUM0sRpmGuR51XFGcBKOliW1Vas1AycP31Cm3Pp0jx3hqw=="], "ag-charts-locale": ["ag-charts-locale@13.1.0", "", {}, "sha512-mPgJnVsOI4Cf17CAlRh8BvLz19e165sdQJeUXNaB7M+DPB+pxODOcfx4oqZlR4Wc8Zu++TGb/2ueHa/aeV2qeQ=="],
"ag-grid-community": ["ag-grid-community@35.1.0", "https://registry.npmmirror.com/ag-grid-community/-/ag-grid-community-35.1.0.tgz", { "dependencies": { "ag-charts-types": "13.1.0" } }, "sha512-yWFQfRNjv3KUBkHHzFdDOYGjPcDMU0B8Up4qG651diFlGRUGEGVs94SK73niWvk1FDZdpV9oWrwq3f30/qAoVg=="], "ag-charts-types": ["ag-charts-types@13.1.0", "", {}, "sha512-DytRM3CXli+Y013SC1Mr8lQBrhVTACK+11ilDHOhwUM0sRpmGuR51XFGcBKOliW1Vas1AycP31Cm3Pp0jx3hqw=="],
"ag-grid-enterprise": ["ag-grid-enterprise@35.1.0", "https://registry.npmmirror.com/ag-grid-enterprise/-/ag-grid-enterprise-35.1.0.tgz", { "dependencies": { "ag-grid-community": "35.1.0" }, "optionalDependencies": { "ag-charts-community": "13.1.0", "ag-charts-enterprise": "13.1.0" } }, "sha512-Zhod3fpgWa9KE0JNFkkkb8/3Qv66UR9KF3wFyCz++wQUtQm5wdExul4UA8wm1ukvBmD6QyBLQ5Cs9zDnIEb0uQ=="], "ag-grid-community": ["ag-grid-community@35.1.0", "", { "dependencies": { "ag-charts-types": "13.1.0" } }, "sha512-yWFQfRNjv3KUBkHHzFdDOYGjPcDMU0B8Up4qG651diFlGRUGEGVs94SK73niWvk1FDZdpV9oWrwq3f30/qAoVg=="],
"ag-grid-vue3": ["ag-grid-vue3@35.1.0", "https://registry.npmmirror.com/ag-grid-vue3/-/ag-grid-vue3-35.1.0.tgz", { "dependencies": { "ag-grid-community": "35.1.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-BvM7yrFxRB/r5hZ4xSyE6T2lU2Rj+Ls6RH5tTu/n8DmhCTmLj4QCEkoU7EuaE0/Az3uEHOubYMaCX4jcDf181A=="], "ag-grid-enterprise": ["ag-grid-enterprise@35.1.0", "", { "dependencies": { "ag-grid-community": "35.1.0" }, "optionalDependencies": { "ag-charts-community": "13.1.0", "ag-charts-enterprise": "13.1.0" } }, "sha512-Zhod3fpgWa9KE0JNFkkkb8/3Qv66UR9KF3wFyCz++wQUtQm5wdExul4UA8wm1ukvBmD6QyBLQ5Cs9zDnIEb0uQ=="],
"alien-signals": ["alien-signals@3.1.2", "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.1.2.tgz", {}, "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw=="], "ag-grid-vue3": ["ag-grid-vue3@35.1.0", "", { "dependencies": { "ag-grid-community": "35.1.0" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-BvM7yrFxRB/r5hZ4xSyE6T2lU2Rj+Ls6RH5tTu/n8DmhCTmLj4QCEkoU7EuaE0/Az3uEHOubYMaCX4jcDf181A=="],
"archiver": ["archiver@5.3.2", "https://registry.npmmirror.com/archiver/-/archiver-5.3.2.tgz", { "dependencies": { "archiver-utils": "^2.1.0", "async": "^3.2.4", "buffer-crc32": "^0.2.1", "readable-stream": "^3.6.0", "readdir-glob": "^1.1.2", "tar-stream": "^2.2.0", "zip-stream": "^4.1.0" } }, "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw=="], "alien-signals": ["alien-signals@3.1.2", "", {}, "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw=="],
"archiver-utils": ["archiver-utils@2.1.0", "https://registry.npmmirror.com/archiver-utils/-/archiver-utils-2.1.0.tgz", { "dependencies": { "glob": "^7.1.4", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^2.0.0" } }, "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw=="], "archiver": ["archiver@5.3.2", "", { "dependencies": { "archiver-utils": "^2.1.0", "async": "^3.2.4", "buffer-crc32": "^0.2.1", "readable-stream": "^3.6.0", "readdir-glob": "^1.1.2", "tar-stream": "^2.2.0", "zip-stream": "^4.1.0" } }, "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw=="],
"aria-hidden": ["aria-hidden@1.2.6", "https://registry.npmmirror.com/aria-hidden/-/aria-hidden-1.2.6.tgz", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], "archiver-utils": ["archiver-utils@2.1.0", "", { "dependencies": { "glob": "^7.1.4", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^2.0.0" } }, "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw=="],
"async": ["async@3.2.6", "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"base64-js": ["base64-js@1.5.1", "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"big-integer": ["big-integer@1.6.52", "https://registry.npmmirror.com/big-integer/-/big-integer-1.6.52.tgz", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"binary": ["binary@0.3.0", "https://registry.npmmirror.com/binary/-/binary-0.3.0.tgz", { "dependencies": { "buffers": "~0.1.1", "chainsaw": "~0.1.0" } }, "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg=="], "big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="],
"birpc": ["birpc@2.9.0", "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="], "binary": ["binary@0.3.0", "", { "dependencies": { "buffers": "~0.1.1", "chainsaw": "~0.1.0" } }, "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg=="],
"bl": ["bl@4.1.0", "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
"bluebird": ["bluebird@3.4.7", "https://registry.npmmirror.com/bluebird/-/bluebird-3.4.7.tgz", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="], "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"brace-expansion": ["brace-expansion@2.0.2", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="],
"buffer": ["buffer@5.7.1", "https://registry.npmmirror.com/buffer/-/buffer-5.7.1.tgz", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], "brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buffer-indexof-polyfill": ["buffer-indexof-polyfill@1.0.2", "https://registry.npmmirror.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", {}, "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A=="], "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"buffers": ["buffers@0.1.1", "https://registry.npmmirror.com/buffers/-/buffers-0.1.1.tgz", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="], "buffer-indexof-polyfill": ["buffer-indexof-polyfill@1.0.2", "", {}, "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A=="],
"bun-types": ["bun-types@1.3.9", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.9.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], "buffers": ["buffers@0.1.1", "", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="],
"chainsaw": ["chainsaw@0.1.0", "https://registry.npmmirror.com/chainsaw/-/chainsaw-0.1.0.tgz", { "dependencies": { "traverse": ">=0.3.0 <0.4" } }, "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ=="], "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "chainsaw": ["chainsaw@0.1.0", "", { "dependencies": { "traverse": ">=0.3.0 <0.4" } }, "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ=="],
"clsx": ["clsx@2.1.1", "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"compress-commons": ["compress-commons@4.1.2", "https://registry.npmmirror.com/compress-commons/-/compress-commons-4.1.2.tgz", { "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"concat-map": ["concat-map@0.0.1", "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "compress-commons": ["compress-commons@4.1.2", "", { "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg=="],
"copy-anything": ["copy-anything@4.0.5", "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.0.5.tgz", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="], "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"core-util-is": ["core-util-is@1.0.3", "https://registry.npmmirror.com/core-util-is/-/core-util-is-1.0.3.tgz", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], "copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="],
"crc-32": ["crc-32@1.2.2", "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="], "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"crc32-stream": ["crc32-stream@4.0.3", "https://registry.npmmirror.com/crc32-stream/-/crc32-stream-4.0.3.tgz", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" } }, "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw=="], "crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
"csstype": ["csstype@3.2.3", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "crc32-stream": ["crc32-stream@4.0.3", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" } }, "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw=="],
"dayjs": ["dayjs@1.11.19", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"decimal.js": ["decimal.js@10.6.0", "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], "dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
"defu": ["defu@6.1.4", "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
"detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"duplexer2": ["duplexer2@0.1.4", "https://registry.npmmirror.com/duplexer2/-/duplexer2-0.1.4.tgz", { "dependencies": { "readable-stream": "^2.0.2" } }, "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"end-of-stream": ["end-of-stream@1.4.5", "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.5.tgz", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], "docxtemplater": ["docxtemplater@3.69.0", "", { "dependencies": { "@xmldom/xmldom": "^0.9.10" } }, "sha512-l1zDGXj4CHdBCkGPvmVOsEzc4DDpMxLXgnNd1zllEck9gxCGkkV5vv1tOD5JhudaM73nTIgymy4wil2u9O/uhQ=="],
"enhanced-resolve": ["enhanced-resolve@5.19.0", "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="], "duplexer2": ["duplexer2@0.1.4", "", { "dependencies": { "readable-stream": "^2.0.2" } }, "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA=="],
"entities": ["entities@7.0.1", "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"estree-walker": ["estree-walker@2.0.2", "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
"exceljs": ["exceljs@4.4.0", "https://registry.npmmirror.com/exceljs/-/exceljs-4.4.0.tgz", { "dependencies": { "archiver": "^5.0.0", "dayjs": "^1.8.34", "fast-csv": "^4.3.1", "jszip": "^3.10.1", "readable-stream": "^3.6.0", "saxes": "^5.0.1", "tmp": "^0.2.0", "unzipper": "^0.10.11", "uuid": "^8.3.0" } }, "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg=="], "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"fast-csv": ["fast-csv@4.3.6", "https://registry.npmmirror.com/fast-csv/-/fast-csv-4.3.6.tgz", { "dependencies": { "@fast-csv/format": "4.3.5", "@fast-csv/parse": "4.3.6" } }, "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw=="], "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "exceljs": ["exceljs@4.4.0", "", { "dependencies": { "archiver": "^5.0.0", "dayjs": "^1.8.34", "fast-csv": "^4.3.1", "jszip": "^3.10.1", "readable-stream": "^3.6.0", "saxes": "^5.0.1", "tmp": "^0.2.0", "unzipper": "^0.10.11", "uuid": "^8.3.0" } }, "sha512-XctvKaEMaj1Ii9oDOqbW/6e1gXknSY4g/aLCDicOXqBE4M0nRWkUu0PTp++UPNzoFY12BNHMfs/VadKIS6llvg=="],
"framer-motion": ["framer-motion@12.34.3", "https://registry.npmmirror.com/framer-motion/-/framer-motion-12.34.3.tgz", { "dependencies": { "motion-dom": "^12.34.3", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q=="], "fast-csv": ["fast-csv@4.3.6", "", { "dependencies": { "@fast-csv/format": "4.3.5", "@fast-csv/parse": "4.3.6" } }, "sha512-2RNSpuwwsJGP0frGsOmTb9oUF+VkFSM4SyLTDgwf2ciHWTarN0lQTC+F2f/t5J9QjW+c65VFIAAu85GsvMIusw=="],
"fs-constants": ["fs-constants@1.0.0", "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fs.realpath": ["fs.realpath@1.0.0", "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], "file-saver": ["file-saver@2.0.5", "", {}, "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="],
"fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "framer-motion": ["framer-motion@12.34.3", "", { "dependencies": { "motion-dom": "^12.34.3", "motion-utils": "^12.29.2", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q=="],
"fstream": ["fstream@1.0.12", "https://registry.npmmirror.com/fstream/-/fstream-1.0.12.tgz", { "dependencies": { "graceful-fs": "^4.1.2", "inherits": "~2.0.0", "mkdirp": ">=0.5 0", "rimraf": "2" } }, "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg=="], "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
"glob": ["glob@7.2.3", "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"graceful-fs": ["graceful-fs@4.2.11", "https://registry.npmmirror.com/graceful-fs/-/graceful-fs-4.2.11.tgz", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"hey-listen": ["hey-listen@1.0.8", "https://registry.npmmirror.com/hey-listen/-/hey-listen-1.0.8.tgz", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="], "fstream": ["fstream@1.0.12", "", { "dependencies": { "graceful-fs": "^4.1.2", "inherits": "~2.0.0", "mkdirp": ">=0.5 0", "rimraf": "2" } }, "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg=="],
"hookable": ["hookable@5.5.3", "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"ieee754": ["ieee754@1.2.1", "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"immediate": ["immediate@3.0.6", "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="], "hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
"inflight": ["inflight@1.0.6", "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
"inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"is-what": ["is-what@5.5.0", "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="], "immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
"isarray": ["isarray@1.0.0", "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"jiti": ["jiti@2.6.1", "https://registry.npmmirror.com/jiti/-/jiti-2.6.1.tgz", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"jszip": ["jszip@3.10.1", "https://registry.npmmirror.com/jszip/-/jszip-3.10.1.tgz", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="], "is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="],
"lazystream": ["lazystream@1.0.1", "https://registry.npmmirror.com/lazystream/-/lazystream-1.0.1.tgz", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="], "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"lie": ["lie@3.1.1", "https://registry.npmmirror.com/lie/-/lie-3.1.1.tgz", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"lightningcss": ["lightningcss@1.31.1", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.31.1.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="], "jszip": ["jszip@3.10.1", "", { "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", "readable-stream": "~2.3.6", "setimmediate": "^1.0.5" } }, "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="], "lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="], "lie": ["lie@3.1.1", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="], "lightningcss": ["lightningcss@1.31.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.31.1", "lightningcss-darwin-arm64": "1.31.1", "lightningcss-darwin-x64": "1.31.1", "lightningcss-freebsd-x64": "1.31.1", "lightningcss-linux-arm-gnueabihf": "1.31.1", "lightningcss-linux-arm64-gnu": "1.31.1", "lightningcss-linux-arm64-musl": "1.31.1", "lightningcss-linux-x64-gnu": "1.31.1", "lightningcss-linux-x64-musl": "1.31.1", "lightningcss-win32-arm64-msvc": "1.31.1", "lightningcss-win32-x64-msvc": "1.31.1" } }, "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="], "lightningcss-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="], "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="], "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="], "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="], "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="], "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="], "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="], "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
"listenercount": ["listenercount@1.0.1", "https://registry.npmmirror.com/listenercount/-/listenercount-1.0.1.tgz", {}, "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="], "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
"localforage": ["localforage@1.10.0", "https://registry.npmmirror.com/localforage/-/localforage-1.10.0.tgz", { "dependencies": { "lie": "3.1.1" } }, "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg=="], "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
"lodash.defaults": ["lodash.defaults@4.2.0", "https://registry.npmmirror.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="], "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
"lodash.difference": ["lodash.difference@4.5.0", "https://registry.npmmirror.com/lodash.difference/-/lodash.difference-4.5.0.tgz", {}, "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="], "listenercount": ["listenercount@1.0.1", "", {}, "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="],
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "https://registry.npmmirror.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], "localforage": ["localforage@1.10.0", "", { "dependencies": { "lie": "3.1.1" } }, "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg=="],
"lodash.flatten": ["lodash.flatten@4.4.0", "https://registry.npmmirror.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz", {}, "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="], "lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
"lodash.groupby": ["lodash.groupby@4.6.0", "https://registry.npmmirror.com/lodash.groupby/-/lodash.groupby-4.6.0.tgz", {}, "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw=="], "lodash.difference": ["lodash.difference@4.5.0", "", {}, "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="],
"lodash.isboolean": ["lodash.isboolean@3.0.3", "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
"lodash.isequal": ["lodash.isequal@4.5.0", "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], "lodash.flatten": ["lodash.flatten@4.4.0", "", {}, "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="],
"lodash.isfunction": ["lodash.isfunction@3.0.9", "https://registry.npmmirror.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", {}, "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="], "lodash.groupby": ["lodash.groupby@4.6.0", "", {}, "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw=="],
"lodash.isnil": ["lodash.isnil@4.0.0", "https://registry.npmmirror.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz", {}, "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng=="], "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
"lodash.isplainobject": ["lodash.isplainobject@4.0.6", "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="],
"lodash.isundefined": ["lodash.isundefined@3.0.1", "https://registry.npmmirror.com/lodash.isundefined/-/lodash.isundefined-3.0.1.tgz", {}, "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA=="], "lodash.isfunction": ["lodash.isfunction@3.0.9", "", {}, "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="],
"lodash.union": ["lodash.union@4.6.0", "https://registry.npmmirror.com/lodash.union/-/lodash.union-4.6.0.tgz", {}, "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="], "lodash.isnil": ["lodash.isnil@4.0.0", "", {}, "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng=="],
"lodash.uniq": ["lodash.uniq@4.5.0", "https://registry.npmmirror.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="], "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
"lucide-vue-next": ["lucide-vue-next@0.563.0", "https://registry.npmmirror.com/lucide-vue-next/-/lucide-vue-next-0.563.0.tgz", { "peerDependencies": { "vue": ">=3.0.1" } }, "sha512-zsE/lCKtmaa7bGfhSpN84br1K9YoQ5pCN+2oKWjQQG3Lo6ufUUKBuHSjNFI6RvUevxaajNXb8XwFUKeTXG3sIA=="], "lodash.isundefined": ["lodash.isundefined@3.0.1", "", {}, "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA=="],
"magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "lodash.union": ["lodash.union@4.6.0", "", {}, "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="],
"minimatch": ["minimatch@5.1.9", "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.9.tgz", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="],
"mitt": ["mitt@3.0.1", "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="], "lucide-vue-next": ["lucide-vue-next@0.563.0", "", { "peerDependencies": { "vue": ">=3.0.1" } }, "sha512-zsE/lCKtmaa7bGfhSpN84br1K9YoQ5pCN+2oKWjQQG3Lo6ufUUKBuHSjNFI6RvUevxaajNXb8XwFUKeTXG3sIA=="],
"mkdirp": ["mkdirp@3.0.1", "https://registry.npmmirror.com/mkdirp/-/mkdirp-3.0.1.tgz", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"motion-dom": ["motion-dom@12.34.3", "https://registry.npmmirror.com/motion-dom/-/motion-dom-12.34.3.tgz", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ=="], "minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
"motion-utils": ["motion-utils@12.29.2", "https://registry.npmmirror.com/motion-utils/-/motion-utils-12.29.2.tgz", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="], "mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
"motion-v": ["motion-v@2.0.0", "https://registry.npmmirror.com/motion-v/-/motion-v-2.0.0.tgz", { "dependencies": { "framer-motion": "^12.29.2", "hey-listen": "^1.0.8", "motion-dom": "^12.29.2", "motion-utils": "^12.29.2" }, "peerDependencies": { "@vueuse/core": ">=10.0.0", "vue": ">=3.0.0" } }, "sha512-oQuQMrPhti+Zps6OosOaW3b/eqzaGAuwI54XHJKq/dIWtQWcNzfyhTo4VB5xmp7yLN+3BE9FKF6skLsynfgbHQ=="], "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
"muggle-string": ["muggle-string@0.4.1", "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="], "motion-dom": ["motion-dom@12.34.3", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ=="],
"nanoid": ["nanoid@3.3.11", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "motion-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="],
"normalize-path": ["normalize-path@3.0.0", "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "motion-v": ["motion-v@2.0.0", "", { "dependencies": { "framer-motion": "^12.29.2", "hey-listen": "^1.0.8", "motion-dom": "^12.29.2", "motion-utils": "^12.29.2" }, "peerDependencies": { "@vueuse/core": ">=10.0.0", "vue": ">=3.0.0" } }, "sha512-oQuQMrPhti+Zps6OosOaW3b/eqzaGAuwI54XHJKq/dIWtQWcNzfyhTo4VB5xmp7yLN+3BE9FKF6skLsynfgbHQ=="],
"ohash": ["ohash@2.0.11", "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], "muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
"once": ["once@1.4.0", "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"pako": ["pako@1.0.11", "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"path-browserify": ["path-browserify@1.0.1", "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="], "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"perfect-debounce": ["perfect-debounce@1.0.0", "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], "pako": ["pako@2.2.0", "", {}, "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w=="],
"picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"picomatch": ["picomatch@4.0.3", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"pinia": ["pinia@3.0.4", "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz", { "dependencies": { "@vue/devtools-api": "^7.7.7" }, "peerDependencies": { "typescript": ">=4.5.0", "vue": "^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw=="], "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"pinia-plugin-persistedstate": ["pinia-plugin-persistedstate@4.7.1", "https://registry.npmmirror.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.7.1.tgz", { "dependencies": { "defu": "^6.1.4" }, "peerDependencies": { "@nuxt/kit": ">=3.0.0", "@pinia/nuxt": ">=0.10.0", "pinia": ">=3.0.0" }, "optionalPeers": ["@nuxt/kit", "@pinia/nuxt", "pinia"] }, "sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"postcss": ["postcss@8.5.6", "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "https://registry.npmmirror.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], "pinia": ["pinia@3.0.4", "", { "dependencies": { "@vue/devtools-api": "^7.7.7" }, "peerDependencies": { "typescript": ">=4.5.0", "vue": "^3.5.11" }, "optionalPeers": ["typescript"] }, "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw=="],
"readable-stream": ["readable-stream@3.6.2", "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "pinia-plugin-persistedstate": ["pinia-plugin-persistedstate@4.7.1", "", { "dependencies": { "defu": "^6.1.4" }, "peerDependencies": { "@nuxt/kit": ">=3.0.0", "@pinia/nuxt": ">=0.10.0", "pinia": ">=3.0.0" }, "optionalPeers": ["@nuxt/kit", "@pinia/nuxt", "pinia"] }, "sha512-WHOqh2esDlR3eAaknPbqXrkkj0D24h8shrDPqysgCFR6ghqP/fpFfJmMPJp0gETHsvrh9YNNg6dQfo2OEtDnIQ=="],
"readdir-glob": ["readdir-glob@1.1.3", "https://registry.npmmirror.com/readdir-glob/-/readdir-glob-1.1.3.tgz", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="], "pizzip": ["pizzip@3.2.0", "", { "dependencies": { "pako": "^2.1.0" } }, "sha512-X4NPNICxCfIK8VYhF6wbksn81vTiziyLbvKuORVAmolvnUzl1A1xmz9DAWKxPRq9lZg84pJOOAMq3OE61bD8IQ=="],
"reka-ui": ["reka-ui@2.8.2", "https://registry.npmmirror.com/reka-ui/-/reka-ui-2.8.2.tgz", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^14.1.0", "@vueuse/shared": "^14.1.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.2.0" } }, "sha512-8lTKcJhmG+D3UyJxhBnNnW/720sLzm0pbA9AC1MWazmJ5YchJAyTSl+O00xP/kxBmEN0fw5JqWVHguiFmsGjzA=="], "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"rfdc": ["rfdc@1.4.1", "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"rimraf": ["rimraf@2.7.1", "https://registry.npmmirror.com/rimraf/-/rimraf-2.7.1.tgz", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"rolldown": ["rolldown@1.0.0-rc.5", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.5.tgz", { "dependencies": { "@oxc-project/types": "=0.114.0", "@rolldown/pluginutils": "1.0.0-rc.5" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.5", "@rolldown/binding-darwin-arm64": "1.0.0-rc.5", "@rolldown/binding-darwin-x64": "1.0.0-rc.5", "@rolldown/binding-freebsd-x64": "1.0.0-rc.5", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.5", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.5", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.5", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.5", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.5", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.5", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.5", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.5", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.5" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-0AdalTs6hNTioaCYIkAa7+xsmHBfU5hCNclZnM/lp7lGGDuUOb6N4BVNtwiomybbencDjq/waKjTImqiGCs5sw=="], "readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="],
"safe-buffer": ["safe-buffer@5.1.2", "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], "reka-ui": ["reka-ui@2.8.2", "", { "dependencies": { "@floating-ui/dom": "^1.6.13", "@floating-ui/vue": "^1.1.6", "@internationalized/date": "^3.5.0", "@internationalized/number": "^3.5.0", "@tanstack/vue-virtual": "^3.12.0", "@vueuse/core": "^14.1.0", "@vueuse/shared": "^14.1.0", "aria-hidden": "^1.2.4", "defu": "^6.1.4", "ohash": "^2.0.11" }, "peerDependencies": { "vue": ">= 3.2.0" } }, "sha512-8lTKcJhmG+D3UyJxhBnNnW/720sLzm0pbA9AC1MWazmJ5YchJAyTSl+O00xP/kxBmEN0fw5JqWVHguiFmsGjzA=="],
"saxes": ["saxes@5.0.1", "https://registry.npmmirror.com/saxes/-/saxes-5.0.1.tgz", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw=="], "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"setimmediate": ["setimmediate@1.0.5", "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="], "rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="],
"sortablejs": ["sortablejs@1.14.0", "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.14.0.tgz", {}, "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="], "rolldown": ["rolldown@1.0.0-rc.5", "", { "dependencies": { "@oxc-project/types": "=0.114.0", "@rolldown/pluginutils": "1.0.0-rc.5" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.5", "@rolldown/binding-darwin-arm64": "1.0.0-rc.5", "@rolldown/binding-darwin-x64": "1.0.0-rc.5", "@rolldown/binding-freebsd-x64": "1.0.0-rc.5", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.5", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.5", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.5", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.5", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.5", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.5", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.5", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.5", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.5" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-0AdalTs6hNTioaCYIkAa7+xsmHBfU5hCNclZnM/lp7lGGDuUOb6N4BVNtwiomybbencDjq/waKjTImqiGCs5sw=="],
"source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], "safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"speakingurl": ["speakingurl@14.0.1", "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="], "saxes": ["saxes@5.0.1", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw=="],
"string_decoder": ["string_decoder@1.3.0", "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
"superjson": ["superjson@2.2.6", "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="], "sortablejs": ["sortablejs@1.14.0", "", {}, "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="],
"tailwind-merge": ["tailwind-merge@3.5.0", "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.5.0.tgz", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"tailwindcss": ["tailwindcss@4.2.0", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.2.0.tgz", {}, "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q=="], "speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
"tapable": ["tapable@2.3.0", "https://registry.npmmirror.com/tapable/-/tapable-2.3.0.tgz", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"tar-stream": ["tar-stream@2.2.0", "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], "superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="],
"tinyglobby": ["tinyglobby@0.2.15", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
"tmp": ["tmp@0.2.5", "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], "tailwindcss": ["tailwindcss@4.2.0", "", {}, "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q=="],
"traverse": ["traverse@0.3.9", "https://registry.npmmirror.com/traverse/-/traverse-0.3.9.tgz", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
"tw-animate-css": ["tw-animate-css@1.4.0", "https://registry.npmmirror.com/tw-animate-css/-/tw-animate-css-1.4.0.tgz", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
"undici-types": ["undici-types@7.16.0", "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="],
"unzipper": ["unzipper@0.10.14", "https://registry.npmmirror.com/unzipper/-/unzipper-0.10.14.tgz", { "dependencies": { "big-integer": "^1.6.17", "binary": "~0.3.0", "bluebird": "~3.4.1", "buffer-indexof-polyfill": "~1.0.0", "duplexer2": "~0.1.4", "fstream": "^1.0.12", "graceful-fs": "^4.2.2", "listenercount": "~1.0.1", "readable-stream": "~2.3.6", "setimmediate": "~1.0.4" } }, "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"util-deprecate": ["util-deprecate@1.0.2", "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
"uuid": ["uuid@8.3.2", "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"vite": ["vite@8.0.0-beta.15", "https://registry.npmmirror.com/vite/-/vite-8.0.0-beta.15.tgz", { "dependencies": { "@oxc-project/runtime": "0.114.0", "lightningcss": "^1.31.1", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-rc.5", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-RHX7IvsJlEfjyA1rS7MY0UsmF91etdLAamslHR5lfuO3W/BXRdXm2tRE64ztpSPZbKqB4wAAZ0AwtF6QzfKZLA=="], "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"vscode-uri": ["vscode-uri@3.1.0", "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], "unzipper": ["unzipper@0.10.14", "", { "dependencies": { "big-integer": "^1.6.17", "binary": "~0.3.0", "bluebird": "~3.4.1", "buffer-indexof-polyfill": "~1.0.0", "duplexer2": "~0.1.4", "fstream": "^1.0.12", "graceful-fs": "^4.2.2", "listenercount": "~1.0.1", "readable-stream": "~2.3.6", "setimmediate": "~1.0.4" } }, "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g=="],
"vue": ["vue@3.5.28", "https://registry.npmmirror.com/vue/-/vue-3.5.28.tgz", { "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/compiler-sfc": "3.5.28", "@vue/runtime-dom": "3.5.28", "@vue/server-renderer": "3.5.28", "@vue/shared": "3.5.28" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vue-demi": ["vue-demi@0.14.10", "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="], "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
"vue-i18n": ["vue-i18n@11.3.1", "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-11.3.1.tgz", { "dependencies": { "@intlify/core-base": "11.3.1", "@intlify/devtools-types": "11.3.1", "@intlify/shared": "11.3.1", "@vue/devtools-api": "^6.5.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-azq8fhVnCwJAw0iXW7i44h9P+Bj+snNuevBAaJ9bxn0I3YVsRU3deVFPNnTfZ2uxVJefGp83JUmL68ddCPw5Pw=="], "vite": ["vite@8.0.0-beta.15", "", { "dependencies": { "@oxc-project/runtime": "0.114.0", "lightningcss": "^1.31.1", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rolldown": "1.0.0-rc.5", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-RHX7IvsJlEfjyA1rS7MY0UsmF91etdLAamslHR5lfuO3W/BXRdXm2tRE64ztpSPZbKqB4wAAZ0AwtF6QzfKZLA=="],
"vue-router": ["vue-router@4.6.4", "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz", { "dependencies": { "@vue/devtools-api": "^6.6.4" }, "peerDependencies": { "vue": "^3.5.0" } }, "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg=="], "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
"vue-tsc": ["vue-tsc@3.2.5", "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.2.5.tgz", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.2.5" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA=="], "vue": ["vue@3.5.28", "", { "dependencies": { "@vue/compiler-dom": "3.5.28", "@vue/compiler-sfc": "3.5.28", "@vue/runtime-dom": "3.5.28", "@vue/server-renderer": "3.5.28", "@vue/shared": "3.5.28" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg=="],
"vuedraggable": ["vuedraggable@4.1.0", "https://registry.npmmirror.com/vuedraggable/-/vuedraggable-4.1.0.tgz", { "dependencies": { "sortablejs": "1.14.0" }, "peerDependencies": { "vue": "^3.0.1" } }, "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww=="], "vue-demi": ["vue-demi@0.14.10", "", { "peerDependencies": { "@vue/composition-api": "^1.0.0-rc.1", "vue": "^3.0.0-0 || ^2.6.0" }, "optionalPeers": ["@vue/composition-api"], "bin": { "vue-demi-fix": "bin/vue-demi-fix.js", "vue-demi-switch": "bin/vue-demi-switch.js" } }, "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg=="],
"wrappy": ["wrappy@1.0.2", "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "vue-i18n": ["vue-i18n@11.4.6", "", { "dependencies": { "@intlify/core-base": "11.4.6", "@intlify/devtools-types": "11.4.6", "@intlify/shared": "11.4.6", "@vue/devtools-api": "^6.5.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-l0gE7Rfy0phCa5ChKYkOq543Wgd39BCK6hkktfr1Ed4D99oRkgPK9ffShASZdeC8OJxGfdWmpYoAaAH6iLEuIg=="],
"xmlchars": ["xmlchars@2.2.0", "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], "vue-tsc": ["vue-tsc@3.2.5", "", { "dependencies": { "@volar/typescript": "2.4.28", "@vue/language-core": "3.2.5" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "bin/vue-tsc.js" } }, "sha512-/htfTCMluQ+P2FISGAooul8kO4JMheOTCbCy4M6dYnYYjqLe3BExZudAua6MSIKSFYQtFOYAll7XobYwcpokGA=="],
"zip-stream": ["zip-stream@4.1.1", "https://registry.npmmirror.com/zip-stream/-/zip-stream-4.1.1.tgz", { "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", "readable-stream": "^3.6.0" } }, "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ=="], "vuedraggable": ["vuedraggable@4.1.0", "", { "dependencies": { "sortablejs": "1.14.0" }, "peerDependencies": { "vue": "^3.0.1" } }, "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww=="],
"@fast-csv/format/@types/node": ["@types/node@14.18.63", "https://registry.npmmirror.com/@types/node/-/node-14.18.63.tgz", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"@fast-csv/parse/@types/node": ["@types/node@14.18.63", "https://registry.npmmirror.com/@types/node/-/node-14.18.63.tgz", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="], "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "https://registry.npmmirror.com/@emnapi/core/-/core-1.8.1.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "zip-stream": ["zip-stream@4.1.1", "", { "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", "readable-stream": "^3.6.0" } }, "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], "@fast-csv/format/@types/node": ["@types/node@14.18.63", "", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], "@fast-csv/parse/@types/node": ["@types/node@14.18.63", "", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"archiver-utils/readable-stream": ["readable-stream@2.3.8", "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" }, "bundled": true }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
"duplexer2/readable-stream": ["readable-stream@2.3.8", "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"glob/minimatch": ["minimatch@3.1.5", "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"jszip/lie": ["lie@3.3.0", "https://registry.npmmirror.com/lie/-/lie-3.3.0.tgz", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="], "archiver-utils/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"jszip/readable-stream": ["readable-stream@2.3.8", "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "duplexer2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"lazystream/readable-stream": ["readable-stream@2.3.8", "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"reka-ui/@internationalized/date": ["@internationalized/date@3.11.0", "https://registry.npmmirror.com/@internationalized/date/-/date-3.11.0.tgz", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q=="], "jszip/lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.5", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.5.tgz", {}, "sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw=="], "jszip/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], "jszip/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"unzipper/readable-stream": ["readable-stream@2.3.8", "https://registry.npmmirror.com/readable-stream/-/readable-stream-2.3.8.tgz", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "lazystream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"vue-i18n/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], "reka-ui/@internationalized/date": ["@internationalized/date@3.11.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q=="],
"vue-router/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="], "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.5", "", {}, "sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw=="],
"zip-stream/archiver-utils": ["archiver-utils@3.0.4", "https://registry.npmmirror.com/archiver-utils/-/archiver-utils-3.0.4.tgz", { "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw=="], "string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"archiver-utils/readable-stream/string_decoder": ["string_decoder@1.1.1", "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "unzipper/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="],
"duplexer2/readable-stream/string_decoder": ["string_decoder@1.1.1", "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "vue-i18n/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "zip-stream/archiver-utils": ["archiver-utils@3.0.4", "", { "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash.defaults": "^4.2.0", "lodash.difference": "^4.5.0", "lodash.flatten": "^4.4.0", "lodash.isplainobject": "^4.0.6", "lodash.union": "^4.6.0", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" } }, "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw=="],
"jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "archiver-utils/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "duplexer2/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"unzipper/readable-stream/string_decoder": ["string_decoder@1.1.1", "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.1.1.tgz", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], "glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"jszip/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"lazystream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"unzipper/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
} }
} }

117
data.js
View File

@ -1,117 +0,0 @@
const DEFAULT_FILE_NAME = 'data'
const DEFAULT_MIME_TYPE = 'application/octet-stream'
const encoder = new TextEncoder()
const decoder = new TextDecoder()
const normalizeSuffix = (suffix) => {
const value = String(suffix || '').trim()
if (!value) throw new Error('INVALID_SUFFIX')
return value.startsWith('.') ? value : `.${value}`
}
const sanitizeFileNamePart = (value) => {
const cleaned = String(value || '')
.replace(/[\\/:*?"<>|]/g, '_')
.replace(/\s+/g, ' ')
.trim()
return cleaned || DEFAULT_FILE_NAME
}
const formatTimestamp = (date = new Date()) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hour = String(date.getHours()).padStart(2, '0')
const minute = String(date.getMinutes()).padStart(2, '0')
const second = String(date.getSeconds()).padStart(2, '0')
return `${year}${month}${day}-${hour}${minute}${second}`
}
const encodeData = (data) => {
const json = JSON.stringify(data)
return encoder.encode(json)
}
const decodeData = (bytes) => {
const text = decoder.decode(bytes)
return JSON.parse(text)
}
const downloadBlob = (blob, fileName) => {
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = fileName
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
URL.revokeObjectURL(url)
}
const pickFile = (accept) => {
return new Promise((resolve, reject) => {
const input = document.createElement('input')
input.type = 'file'
input.accept = accept
input.style.display = 'none'
const cleanup = () => {
input.removeEventListener('change', handleChange)
input.remove()
}
const handleChange = () => {
const file = input.files?.[0] || null
cleanup()
if (!file) {
reject(new Error('FILE_NOT_SELECTED'))
return
}
resolve(file)
}
input.addEventListener('change', handleChange, { once: true })
document.body.appendChild(input)
input.click()
})
}
export const exportData = async (data, suffix, options = {}) => {
const normalizedSuffix = normalizeSuffix(suffix)
const bytes = encodeData(data)
const blob = new Blob([bytes], {
type: options.mimeType || DEFAULT_MIME_TYPE
})
const baseName = sanitizeFileNamePart(options.fileName)
const fileName = `${baseName}-${formatTimestamp()}${normalizedSuffix}`
const shouldDownload = options.download !== false
if (shouldDownload) {
downloadBlob(blob, fileName)
}
return {
blob,
fileName,
bytes
}
}
export const importData = async (suffix) => {
const normalizedSuffix = normalizeSuffix(suffix)
const file = await pickFile(normalizedSuffix)
const fileName = String(file.name || '')
if (!fileName.toLowerCase().endsWith(normalizedSuffix.toLowerCase())) {
throw new Error('INVALID_FILE_SUFFIX')
}
const bytes = new Uint8Array(await file.arrayBuffer())
return decodeData(bytes)
}

View File

@ -4,19 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" /> <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>交通运输工程造价咨询服务预算编制规范</title> <title>联众咨询</title>
<!-- 👇 企微 / 微信分享专用 meta 👇 -->
<meta name="description" content="交通运输工程造价咨询服务预算编制规范工具,依据 T/GDHS 017-2026 标准,提供专业预算编制、计算、导出功能。" />
<!-- 微信开放平台标签(解决不显示封面/标题问题) -->
<meta property="og:title" content="交通运输工程造价咨询服务预算编制工具" />
<meta property="og:description" content="依据 T/GDHS 017-2026 标准,专业工程造价预算编制工具。" />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://jtzjfw.lianzhong.com.cn/logo.jpg" /> <!-- 必须替换成你自己的在线图片 -->
<!-- 企微专用优化 -->
<meta name="wx:cover" content="https://jtzjfw.lianzhong.com.cn/logo.jpg" />
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>

67
package-lock.json generated
View File

@ -21,18 +21,20 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"decimal.js": "^10.6.0", "decimal.js": "^10.6.0",
"docxtemplater": "^3.68.5",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"file-saver": "^2.0.5",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lucide-vue-next": "^0.563.0", "lucide-vue-next": "^0.563.0",
"motion-v": "^2.0.0", "motion-v": "^2.0.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1", "pinia-plugin-persistedstate": "^4.7.1",
"pizzip": "^3.2.0",
"reka-ui": "^2.8.0", "reka-ui": "^2.8.0",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"vue": "^3.5.25", "vue": "^3.5.25",
"vue-i18n": "^11.3.0", "vue-i18n": "^11.3.0",
"vue-router": "^4.6.4",
"vuedraggable": "^4.1.0" "vuedraggable": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
@ -657,6 +659,15 @@
"vue": "^3.5.0" "vue": "^3.5.0"
} }
}, },
"node_modules/@xmldom/xmldom": {
"version": "0.9.9",
"resolved": "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.9.tgz",
"integrity": "sha512-qycIHAucxy/LXAYIjmLmtQ8q9GPnMbnjG1KXhWm9o5sCr6pOYDATkMPiTNa6/v8eELyqOQ2FsEqeoFYmgv/gJg==",
"license": "MIT",
"engines": {
"node": ">=14.6"
}
},
"node_modules/ag-charts-community": { "node_modules/ag-charts-community": {
"version": "13.1.0", "version": "13.1.0",
"license": "MIT", "license": "MIT",
@ -1013,6 +1024,18 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/docxtemplater": {
"version": "3.68.5",
"resolved": "https://registry.npmmirror.com/docxtemplater/-/docxtemplater-3.68.5.tgz",
"integrity": "sha512-2xcHvTXjMA0jdX6PRh1BUTLrcRQ86Re/QJKWCUCX/vv5RKzntjNNkpR/O4AUoJY1TdoqxA+d04L4xgoAUNf/kw==",
"license": "MIT",
"dependencies": {
"@xmldom/xmldom": "^0.9.8"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/duplexer2": { "node_modules/duplexer2": {
"version": "0.1.4", "version": "0.1.4",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
@ -1117,6 +1140,12 @@
} }
} }
}, },
"node_modules/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==",
"license": "MIT"
},
"node_modules/framer-motion": { "node_modules/framer-motion": {
"version": "12.34.3", "version": "12.34.3",
"license": "MIT", "license": "MIT",
@ -1638,6 +1667,21 @@
} }
} }
}, },
"node_modules/pizzip": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/pizzip/-/pizzip-3.2.0.tgz",
"integrity": "sha512-X4NPNICxCfIK8VYhF6wbksn81vTiziyLbvKuORVAmolvnUzl1A1xmz9DAWKxPRq9lZg84pJOOAMq3OE61bD8IQ==",
"license": "(MIT OR GPL-3.0)",
"dependencies": {
"pako": "^2.1.0"
}
},
"node_modules/pizzip/node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.6",
"funding": [ "funding": [
@ -2128,27 +2172,6 @@
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/vue-router": {
"version": "4.6.4",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz",
"integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.5.0"
}
},
"node_modules/vue-router/node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/vue-tsc": { "node_modules/vue-tsc": {
"version": "3.2.5", "version": "3.2.5",
"dev": true, "dev": true,

View File

@ -1,14 +1,13 @@
{ {
"name": "ZWJJ2026", "name": "my-vue-app",
"private": true, "private": true,
"version": "1.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "bunx --bun vite", "dev": "bunx --bun vite",
"build": "bunx vue-tsc -b && bunx --bun vite build", "build": " bunx --bun vite build",
"preview": "bunx --bun vite preview", "preview": "bunx --bun vite preview",
"type-check": "bunx vue-tsc --noEmit", "type-check": "bunx vue-tsc --noEmit"
"dockerPush": "bun run build && docker build -f Dockerfile.dist -t wintsa/zwzjjstool2026:latest . && docker push wintsa/zwzjjstool2026:latest"
}, },
"dependencies": { "dependencies": {
"@ag-grid-community/locale": "^35.1.0", "@ag-grid-community/locale": "^35.1.0",
@ -24,18 +23,20 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"decimal.js": "^10.6.0", "decimal.js": "^10.6.0",
"docxtemplater": "^3.68.5",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
"file-saver": "^2.0.5",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lucide-vue-next": "^0.563.0", "lucide-vue-next": "^0.563.0",
"motion-v": "^2.0.0", "motion-v": "^2.0.0",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1", "pinia-plugin-persistedstate": "^4.7.1",
"pizzip": "^3.2.0",
"reka-ui": "^2.8.0", "reka-ui": "^2.8.0",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18", "tailwindcss": "^4.1.18",
"vue": "^3.5.25", "vue": "^3.5.25",
"vue-i18n": "^11.3.0", "vue-i18n": "^11.3.0",
"vue-router": "^4.6.4",
"vuedraggable": "^4.1.0" "vuedraggable": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {

BIN
public/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

1
public/bg-home.png Normal file
View File

@ -0,0 +1 @@
iVBORw0KGgoAAAANSUhEUgAAAfQAAAH0CAYAAADl8HLGAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAKT2lDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjanVNnVFPpFj333vRCS4iAlEtvUhUIIFJCi4AUkSYqIQkQSoghodkVUcERRUUEG8igiAOOjoCMFVEsDIoK2AfkIaKOg6OIisr74Xuja9a89+bN/rXXPues852zzwfACAyWSDNRNYAMqUIeEeCDx8TG4eQuQIEKJHAAEAizZCFz/SMBAPh+PDwrIsAHvgABeNMLCADATZvAMByH/w/qQplcAYCEAcB0kThLCIAUAEB6jkKmAEBGAYCdmCZTAKAEAGDLY2LjAFAtAGAnf+bTAICd+Jl7AQBblCEVAaCRACATZYhEAGg7AKzPVopFAFgwABRmS8Q5ANgtADBJV2ZIALC3AMDOEAuyAAgMADBRiIUpAAR7AGDIIyN4AISZABRG8lc88SuuEOcqAAB4mbI8uSQ5RYFbCC1xB1dXLh4ozkkXKxQ2YQJhmkAuwnmZGTKBNA/g88wAAKCRFRHgg/P9eM4Ors7ONo62Dl8t6r8G/yJiYuP+5c+rcEAAAOF0ftH+LC+zGoA7BoBt/qIl7gRoXgugdfeLZrIPQLUAoOnaV/Nw+H48PEWhkLnZ2eXk5NhKxEJbYcpXff5nwl/AV/1s+X48/Pf14L7iJIEyXYFHBPjgwsz0TKUcz5IJhGLc5o9H/LcL//wd0yLESWK5WCoU41EScY5EmozzMqUiiUKSKcUl0v9k4t8s+wM+3zUAsGo+AXuRLahdYwP2SycQWHTA4vcAAPK7b8HUKAgDgGiD4c93/+8//UegJQCAZkmScQAAXkQkLlTKsz/HCAAARKCBKrBBG/TBGCzABhzBBdzBC/xgNoRCJMTCQhBCCmSAHHJgKayCQiiGzbAdKmAv1EAdNMBRaIaTcA4uwlW4Dj1wD/phCJ7BKLyBCQRByAgTYSHaiAFiilgjjggXmYX4IcFIBBKLJCDJiBRRIkuRNUgxUopUIFVIHfI9cgI5h1xGupE7yAAygvyGvEcxlIGyUT3UDLVDuag3GoRGogvQZHQxmo8WoJvQcrQaPYw2oefQq2gP2o8+Q8cwwOgYBzPEbDAuxsNCsTgsCZNjy7EirAyrxhqwVqwDu4n1Y8+xdwQSgUXACTYEd0IgYR5BSFhMWE7YSKggHCQ0EdoJNwkDhFHCJyKTqEu0JroR+cQYYjIxh1hILCPWEo8TLxB7iEPENyQSiUMyJ7mQAkmxpFTSEtJG0m5SI+ksqZs0SBojk8naZGuyBzmULCAryIXkneTD5DPkG+Qh8lsKnWJAcaT4U+IoUspqShnlEOU05QZlmDJBVaOaUt2ooVQRNY9aQq2htlKvUYeoEzR1mjnNgxZJS6WtopXTGmgXaPdpr+h0uhHdlR5Ol9BX0svpR+iX6AP0dwwNhhWDx4hnKBmbGAcYZxl3GK+YTKYZ04sZx1QwNzHrmOeZD5lvVVgqtip8FZHKCpVKlSaVGyovVKmqpqreqgtV81XLVI+pXlN9rkZVM1PjqQnUlqtVqp1Q61MbU2epO6iHqmeob1Q/pH5Z/YkGWcNMw09DpFGgsV/jvMYgC2MZs3gsIWsNq4Z1gTXEJrHN2Xx2KruY/R27iz2qqaE5QzNKM1ezUvOUZj8H45hx+Jx0TgnnKKeX836K3hTvKeIpG6Y0TLkxZVxrqpaXllirSKtRq0frvTau7aedpr1Fu1n7gQ5Bx0onXCdHZ4/OBZ3nU9lT3acKpxZNPTr1ri6qa6UbobtEd79up+6Ynr5egJ5Mb6feeb3n+hx9L/1U/W36p/VHDFgGswwkBtsMzhg8xTVxbzwdL8fb8VFDXcNAQ6VhlWGX4SU

Binary file not shown.

Before

Width:  |  Height:  |  Size: 856 B

BIN
public/template-doc.doc Normal file

Binary file not shown.

BIN
public/template-doc.docx Normal file

Binary file not shown.

BIN
public/template-excel.xlsx Normal file

Binary file not shown.

BIN
public/组 4@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
public/组 5@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/组 6@2x.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,3 +1,323 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useTabStore } from '@/pinia/tab'
import HomeEntryView from '@/features/workbench/components/HomeEntryView.vue'
import Tab from '@/layout/tab.vue'
import { waitForHydration } from '@/pinia/Plugin/indexdb'
import localforage from 'localforage'
import {
buildProjectUrl,
DEFAULT_PROJECT_ID,
ensureProjectIdInUrl,
FORCE_HOME_QUERY_KEY,
getProjectDbName,
NEW_PROJECT_QUERY_KEY,
PROJECT_TAB_ID,
QUICK_PROJECT_ID
} from '@/lib/workspace'
import { collectActiveProjectSessionLocks, initProjectSessionLock } from '@/lib/projectSessionLock'
import { listenProjectDeleted, listenResetAll } from '@/lib/projectEvents'
import { listProjects, type ProjectMeta } from '@/lib/projectRegistry'
import { closePage } from './lib/utils'
const tabStore = useTabStore()
const { t } = useI18n()
const isReady = ref(false)
const lockConflict = ref(false)
const currentProjectId = ref('')
const currentProjectName = ref('')
const conflictProjectList = ref<ProjectMeta[]>([])
const openedProjectIds = ref<string[]>([])
const closeCountdown = ref(10)
const isNewProjectRequest = ref(false)
const isForceHomeRequest = ref(false)
let closeCountdownTimer: ReturnType<typeof setInterval> | null = null
let releaseLock: (() => void) | null = null
let stopProjectDeletedListener: (() => void) | null = null
let stopResetAllListener: (() => void) | null = null
let isHandlingDeletedProject = false
let isHandlingGlobalReset = false
const showHomeEntry = computed(() => !tabStore.hasCompletedSetup)
const handleImportComplete = () => {
tabStore.hasCompletedSetup = true
}
const initCurrentProjectLock = () => {
if (releaseLock) return
const projectId = String(currentProjectId.value || '').trim()
if (!projectId || projectId === QUICK_PROJECT_ID) {
lockConflict.value = false
return
}
const lock = initProjectSessionLock({
projectId,
onConflict: (next) => {
lockConflict.value = next
if (next) {
refreshConflictProjectList()
startCloseCountdown()
} else {
clearCloseCountdown()
}
}
})
releaseLock = lock.release
}
const refreshConflictProjectList = () => {
void (async () => {
const projects = listProjects()
const enriched = await Promise.all(
projects.map(async (project) => {
try {
const kvStoreInstance = localforage.createInstance({
name: getProjectDbName(project.id),
storeName: 'pinia-kv'
})
const kvState = await kvStoreInstance.getItem<any>('pinia-kv')
const entries = kvState?.entries && typeof kvState.entries === 'object' ? kvState.entries : null
const projectInfo = entries?.['xm-base-info-v1']
const projectName =
projectInfo && typeof projectInfo === 'object' && typeof projectInfo.projectName === 'string'
? projectInfo.projectName.trim()
: ''
return {
...project,
name: projectName || project.name
}
} catch {
return project
}
})
)
conflictProjectList.value = enriched
const hit = enriched.find(item => item.id === currentProjectId.value)
currentProjectName.value = hit?.name || currentProjectId.value
openedProjectIds.value = Array.from(collectActiveProjectSessionLocks(enriched.map(item => item.id)))
})()
}
const clearCloseCountdown = () => {
if (!closeCountdownTimer) return
clearInterval(closeCountdownTimer)
closeCountdownTimer = null
}
const startCloseCountdown = () => {
clearCloseCountdown()
closeCountdown.value = 10
closeCountdownTimer = setInterval(() => {
closeCountdown.value -= 1
if (closeCountdown.value <= 0) {
clearCloseCountdown()
try {
closePage()
} catch {
//
}
}
}, 1000)
}
const isConflictProjectOpen = (projectId: string) => openedProjectIds.value.includes(projectId)
const openProjectInNewTab = (projectId: string, options?: { newProject?: boolean }) => {
if (isConflictProjectOpen(projectId)) return
const href = buildProjectUrl(projectId, options)
window.open(href, '_blank', 'noopener')
}
const createProjectAndOpen = () => {
refreshConflictProjectList()
openProjectInNewTab(DEFAULT_PROJECT_ID, { newProject: true })
}
const syncRouteRequestFlags = () => {
try {
const url = new URL(window.location.href)
isNewProjectRequest.value = url.searchParams.get(NEW_PROJECT_QUERY_KEY) === '1'
isForceHomeRequest.value = url.searchParams.get(FORCE_HOME_QUERY_KEY) === '1'
} catch {
isNewProjectRequest.value = false
isForceHomeRequest.value = false
}
}
const formatProjectEditedTime = (value: string) => {
const date = new Date(value)
if (Number.isNaN(date.getTime())) return '-'
const pad = (num: number) => String(num).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
}
const handleReleaseProjectLock = () => {
if (!releaseLock) return
releaseLock()
releaseLock = null
lockConflict.value = false
}
const handleProjectDeleted = (deletedProjectId: string) => {
if (String(deletedProjectId || '').trim() !== currentProjectId.value) return
if (isHandlingDeletedProject) return
isHandlingDeletedProject = true
handleReleaseProjectLock()
tabStore.resetTabs()
tabStore.hasCompletedSetup = false
const href = buildProjectUrl(DEFAULT_PROJECT_ID, { forceHome: true })
try {
window.close()
} catch {
// ignore and fallback to redirect
}
window.setTimeout(() => {
window.location.href = href
}, 120)
}
const handleResetAll = () => {
if (isHandlingGlobalReset) return
isHandlingGlobalReset = true
handleReleaseProjectLock()
tabStore.resetTabs()
tabStore.hasCompletedSetup = false
const href = buildProjectUrl(DEFAULT_PROJECT_ID, { forceHome: true })
try {
window.close()
} catch {
// ignore and fallback to redirect
}
window.setTimeout(() => {
window.location.href = href
}, 120)
}
onMounted(() => {
currentProjectId.value = ensureProjectIdInUrl()
syncRouteRequestFlags()
refreshConflictProjectList()
if (!isForceHomeRequest.value && currentProjectId.value !== QUICK_PROJECT_ID && currentProjectId.value !== DEFAULT_PROJECT_ID) {
initCurrentProjectLock()
}
window.addEventListener('home-import-selected', handleImportComplete)
window.addEventListener('jgjs-release-project-lock', handleReleaseProjectLock)
stopProjectDeletedListener = listenProjectDeleted(handleProjectDeleted)
stopResetAllListener = listenResetAll(handleResetAll)
waitForHydration('tabs').then(() => {
if (isForceHomeRequest.value) {
tabStore.resetTabs()
tabStore.hasCompletedSetup = false
}
if (!tabStore.hasCompletedSetup && !isNewProjectRequest.value && !isForceHomeRequest.value) {
const hasProjects = listProjects().length > 0
if (hasProjects && currentProjectId.value !== DEFAULT_PROJECT_ID) {
if (Array.isArray(tabStore.tabs) && tabStore.tabs.length > 0) {
tabStore.hasCompletedSetup = true
} else {
tabStore.enterWorkspace({
id: PROJECT_TAB_ID,
title: t('home.cards.projectBudget'),
componentName: 'ProjectCalcView'
})
tabStore.hasCompletedSetup = true
}
}
}
if (tabStore.hasCompletedSetup && Array.isArray(tabStore.tabs) && tabStore.tabs.length > 0) {
const activeId = typeof tabStore.activeTabId === 'string' ? tabStore.activeTabId : ''
const hasActive = Boolean(activeId) && tabStore.tabs.some(tab => tab.id === activeId)
if (!hasActive) {
tabStore.activeTabId = tabStore.tabs[0]?.id
}
}
if (!releaseLock) {
lockConflict.value = false
clearCloseCountdown()
}
isReady.value = true
})
})
onBeforeUnmount(() => {
clearCloseCountdown()
window.removeEventListener('home-import-selected', handleImportComplete)
window.removeEventListener('jgjs-release-project-lock', handleReleaseProjectLock)
if (stopProjectDeletedListener) {
stopProjectDeletedListener()
stopProjectDeletedListener = null
}
if (stopResetAllListener) {
stopResetAllListener()
stopResetAllListener = null
}
if (releaseLock) {
releaseLock()
releaseLock = null
}
})
</script>
<template> <template>
<RouterView /> <template v-if="isReady">
<div
v-if="lockConflict"
class="flex min-h-screen items-center justify-center bg-slate-50 px-4"
>
<div class="w-full max-w-lg rounded-xl border border-red-200 bg-white p-6 shadow-sm">
<h2 class="text-lg font-semibold text-slate-900">{{ t('app.projectConflict.title') }}</h2>
<p class="mt-2 text-sm leading-6 text-slate-600">
{{ t('app.projectConflict.desc', { name: currentProjectName }) }}
</p>
<p class="mt-2 text-xs text-slate-500">
{{ t('app.projectConflict.countdown', { seconds: closeCountdown }) }}
</p>
<div class="mt-4 max-h-52 space-y-2 overflow-auto rounded-md border border-slate-200 bg-slate-50 p-2">
<button
v-for="project in conflictProjectList"
:key="project.id"
type="button"
class="flex w-full items-center justify-between rounded-md border border-transparent bg-white px-3 py-2 text-left text-sm transition"
:class="isConflictProjectOpen(project.id) ? 'cursor-not-allowed opacity-60' : 'cursor-pointer hover:border-slate-200 hover:bg-slate-100'"
:disabled="isConflictProjectOpen(project.id)"
@click="openProjectInNewTab(project.id)"
>
<span class="font-medium text-slate-700">
{{ project.name }}
<span v-if="isConflictProjectOpen(project.id)" class="ml-1 text-xs text-slate-500">{{ t('app.projectConflict.opened') }}</span>
</span>
<span class="text-xs text-slate-500">{{ t('app.projectConflict.lastEdited', { time: formatProjectEditedTime(project.updatedAt) }) }}</span>
</button>
</div>
<div class="mt-4 flex items-center gap-2">
<button
type="button"
class="cursor-pointer rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-700 hover:bg-slate-100"
@click="createProjectAndOpen"
>
{{ t('app.projectConflict.createAndOpen') }}
</button>
<button
type="button"
class="rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-700"
:class="isConflictProjectOpen('default') ? 'cursor-not-allowed opacity-60' : 'cursor-pointer hover:bg-slate-100'"
:disabled="isConflictProjectOpen('default')"
@click="openProjectInNewTab('default')"
>
{{ t('app.projectConflict.openDefault') }}
</button>
</div>
</div>
</div>
<template v-else>
<HomeEntryView v-if="showHomeEntry" />
<Tab v-else />
</template>
</template>
</template> </template>

View File

@ -1,318 +0,0 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useTabStore } from '@/pinia/tab'
import HomeEntryView from '@/features/workbench/components/HomeEntryView.vue'
import Tab from '@/layout/tab.vue'
import { waitForHydration } from '@/pinia/Plugin/indexdb'
import localforage from 'localforage'
import {
buildProjectUrl,
DEFAULT_PROJECT_ID,
ensureProjectIdInUrl,
FORCE_HOME_QUERY_KEY,
getProjectDbName,
NEW_PROJECT_QUERY_KEY,
PROJECT_TAB_ID,
QUICK_PROJECT_ID
} from '@/lib/workspace'
import { collectActiveProjectSessionLocks, initProjectSessionLock } from '@/lib/projectSessionLock'
import { listenProjectDeleted, listenResetAll } from '@/lib/projectEvents'
import { listProjects, type ProjectMeta } from '@/lib/projectRegistry'
const tabStore = useTabStore()
const { t } = useI18n()
const isReady = ref(false)
const lockConflict = ref(false)
const currentProjectId = ref('')
const currentProjectName = ref('')
const conflictProjectList = ref<ProjectMeta[]>([])
const openedProjectIds = ref<string[]>([])
const closeCountdown = ref(10)
const isNewProjectRequest = ref(false)
const isForceHomeRequest = ref(false)
let closeCountdownTimer: ReturnType<typeof setInterval> | null = null
let releaseLock: (() => void) | null = null
let stopProjectDeletedListener: (() => void) | null = null
let stopResetAllListener: (() => void) | null = null
let isHandlingDeletedProject = false
let isHandlingGlobalReset = false
const showHomeEntry = computed(() => !tabStore.hasCompletedSetup)
const handleImportComplete = () => {
tabStore.hasCompletedSetup = true
}
const initCurrentProjectLock = () => {
if (releaseLock) return
const projectId = String(currentProjectId.value || '').trim()
if (!projectId || projectId === QUICK_PROJECT_ID) {
lockConflict.value = false
return
}
const lock = initProjectSessionLock({
projectId,
onConflict: (next) => {
lockConflict.value = next
if (next) {
refreshConflictProjectList()
startCloseCountdown()
} else {
clearCloseCountdown()
}
}
})
releaseLock = lock.release
}
const refreshConflictProjectList = () => {
void (async () => {
const projects = listProjects()
const enriched = await Promise.all(
projects.map(async (project) => {
try {
const kvStoreInstance = localforage.createInstance({
name: getProjectDbName(project.id),
storeName: 'pinia-kv'
})
const kvState = await kvStoreInstance.getItem<any>('pinia-kv')
const entries = kvState?.entries && typeof kvState.entries === 'object' ? kvState.entries : null
const projectInfo = entries?.['xm-base-info-v1']
const projectName =
projectInfo && typeof projectInfo === 'object' && typeof projectInfo.projectName === 'string'
? projectInfo.projectName.trim()
: ''
return {
...project,
name: projectName || project.name
}
} catch {
return project
}
})
)
conflictProjectList.value = enriched
const hit = enriched.find(item => item.id === currentProjectId.value)
currentProjectName.value = hit?.name || currentProjectId.value
openedProjectIds.value = Array.from(collectActiveProjectSessionLocks(enriched.map(item => item.id)))
})()
}
const clearCloseCountdown = () => {
if (!closeCountdownTimer) return
clearInterval(closeCountdownTimer)
closeCountdownTimer = null
}
const startCloseCountdown = () => {
clearCloseCountdown()
closeCountdown.value = 10
closeCountdownTimer = setInterval(() => {
closeCountdown.value -= 1
if (closeCountdown.value <= 0) {
clearCloseCountdown()
backToHome()
}
}, 1000)
}
const isConflictProjectOpen = (projectId: string) => openedProjectIds.value.includes(projectId)
const openProjectInNewTab = (projectId: string, options?: { newProject?: boolean }) => {
if (isConflictProjectOpen(projectId)) return
const href = buildProjectUrl(projectId, options)
window.open(href, '_blank', 'noopener')
}
const createProjectAndOpen = () => {
refreshConflictProjectList()
openProjectInNewTab(DEFAULT_PROJECT_ID, { newProject: true })
}
const backToHome = () => {
window.location.href = buildProjectUrl(DEFAULT_PROJECT_ID, { forceHome: true })
}
const syncRouteRequestFlags = () => {
try {
const url = new URL(window.location.href)
isNewProjectRequest.value = url.searchParams.get(NEW_PROJECT_QUERY_KEY) === '1'
isForceHomeRequest.value = url.searchParams.get(FORCE_HOME_QUERY_KEY) === '1'
} catch {
isNewProjectRequest.value = false
isForceHomeRequest.value = false
}
}
const formatProjectEditedTime = (value: string) => {
const date = new Date(value)
if (Number.isNaN(date.getTime())) return '-'
const pad = (num: number) => String(num).padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
}
const handleReleaseProjectLock = () => {
if (!releaseLock) return
releaseLock()
releaseLock = null
lockConflict.value = false
}
const handleProjectDeleted = (deletedProjectId: string) => {
if (String(deletedProjectId || '').trim() !== currentProjectId.value) return
if (isHandlingDeletedProject) return
isHandlingDeletedProject = true
handleReleaseProjectLock()
tabStore.resetTabs()
tabStore.hasCompletedSetup = false
const href = buildProjectUrl(DEFAULT_PROJECT_ID, { forceHome: true })
try {
window.close()
} catch {
// ignore and fallback to redirect
}
window.setTimeout(() => {
window.location.href = href
}, 120)
}
const handleResetAll = () => {
if (isHandlingGlobalReset) return
isHandlingGlobalReset = true
handleReleaseProjectLock()
tabStore.resetTabs()
tabStore.hasCompletedSetup = false
const href = buildProjectUrl(DEFAULT_PROJECT_ID, { forceHome: true })
try {
window.close()
} catch {
// ignore and fallback to redirect
}
window.setTimeout(() => {
window.location.href = href
}, 120)
}
onMounted(() => {
currentProjectId.value = ensureProjectIdInUrl()
syncRouteRequestFlags()
refreshConflictProjectList()
if (!isForceHomeRequest.value && currentProjectId.value !== QUICK_PROJECT_ID && currentProjectId.value !== DEFAULT_PROJECT_ID) {
initCurrentProjectLock()
}
window.addEventListener('home-import-selected', handleImportComplete)
window.addEventListener('jgjs-release-project-lock', handleReleaseProjectLock)
stopProjectDeletedListener = listenProjectDeleted(handleProjectDeleted)
stopResetAllListener = listenResetAll(handleResetAll)
waitForHydration('tabs').then(() => {
if (isForceHomeRequest.value) {
tabStore.resetTabs()
tabStore.hasCompletedSetup = false
}
if (!tabStore.hasCompletedSetup && !isNewProjectRequest.value && !isForceHomeRequest.value) {
const hasProjects = listProjects().length > 0
if (hasProjects && currentProjectId.value !== DEFAULT_PROJECT_ID) {
if (Array.isArray(tabStore.tabs) && tabStore.tabs.length > 0) {
tabStore.hasCompletedSetup = true
} else {
tabStore.enterWorkspace({
id: PROJECT_TAB_ID,
title: t('home.cards.projectBudget'),
componentName: 'ProjectCalcView'
})
tabStore.hasCompletedSetup = true
}
}
}
if (tabStore.hasCompletedSetup && Array.isArray(tabStore.tabs) && tabStore.tabs.length > 0) {
const activeId = typeof tabStore.activeTabId === 'string' ? tabStore.activeTabId : ''
const hasActive = Boolean(activeId) && tabStore.tabs.some(tab => tab.id === activeId)
if (!hasActive) {
tabStore.activeTabId = tabStore.tabs[0]?.id
}
}
if (!releaseLock) {
lockConflict.value = false
clearCloseCountdown()
}
isReady.value = true
})
})
onBeforeUnmount(() => {
clearCloseCountdown()
window.removeEventListener('home-import-selected', handleImportComplete)
window.removeEventListener('jgjs-release-project-lock', handleReleaseProjectLock)
if (stopProjectDeletedListener) {
stopProjectDeletedListener()
stopProjectDeletedListener = null
}
if (stopResetAllListener) {
stopResetAllListener()
stopResetAllListener = null
}
if (releaseLock) {
releaseLock()
releaseLock = null
}
})
</script>
<template>
<template v-if="isReady">
<div
v-if="lockConflict"
class="flex min-h-screen items-center justify-center bg-slate-50 px-4"
>
<div class="w-full max-w-lg rounded-xl border border-red-200 bg-white p-6 shadow-sm">
<h2 class="text-lg font-semibold text-slate-900">{{ t('app.projectConflict.title') }}</h2>
<p class="mt-2 text-sm leading-6 text-slate-600">
{{ t('app.projectConflict.desc', { name: currentProjectName }) }}
</p>
<p class="mt-2 text-xs text-slate-500">
{{ t('app.projectConflict.countdown', { seconds: closeCountdown }) }}
</p>
<div class="mt-4 max-h-52 space-y-2 overflow-auto rounded-md border border-slate-200 bg-slate-50 p-2">
<button
v-for="project in conflictProjectList"
:key="project.id"
type="button"
class="flex w-full items-center justify-between rounded-md border border-transparent bg-white px-3 py-2 text-left text-sm transition"
:class="isConflictProjectOpen(project.id) ? 'cursor-not-allowed opacity-60' : 'cursor-pointer hover:border-slate-200 hover:bg-slate-100'"
:disabled="isConflictProjectOpen(project.id)"
@click="openProjectInNewTab(project.id)"
>
<span class="font-medium text-slate-700">
{{ project.name }}
<span v-if="isConflictProjectOpen(project.id)" class="ml-1 text-xs text-slate-500">{{ t('app.projectConflict.opened') }}</span>
</span>
<span class="text-xs text-slate-500">{{ t('app.projectConflict.lastEdited', { time: formatProjectEditedTime(project.updatedAt) }) }}</span>
</button>
</div>
<div class="mt-4 flex items-center gap-2">
<button
type="button"
class="cursor-pointer rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-700 hover:bg-slate-100"
@click="createProjectAndOpen"
>
{{ t('app.projectConflict.createAndOpen') }}
</button>
<button
type="button"
class="rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-700"
:class="'cursor-pointer hover:bg-slate-100'"
@click="backToHome"
>
{{ t('app.projectConflict.openDefault') }}
</button>
</div>
</div>
</div>
<template v-else>
<HomeEntryView v-if="showHomeEntry" />
<Tab v-else />
</template>
</template>
</template>

View File

@ -1,197 +0,0 @@
<script setup lang="ts">
import { computed, ref, watchEffect } from 'vue'
import { useI18n } from 'vue-i18n'
import { Languages } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { DEFAULT_LOCALE, setAppLocale, type AppLocale } from '@/i18n'
import {
DISCLAIMER_ENTRY_QUERY_KEY,
DISCLAIMER_RETURN_URL_QUERY_KEY,
hasAcceptedRestrictedDisclaimer,
persistRestrictedDisclaimerAcceptance,
readRestrictedEntryCodeFromUrl
} from '@/lib/workspace'
const { t, locale } = useI18n()
const accepted = ref(false)
const parsedParams = (() => {
try {
const url = new URL(window.location.href)
const returnUrl = String(url.searchParams.get(DISCLAIMER_RETURN_URL_QUERY_KEY) || '').trim()
const parsedReturnUrl = returnUrl ? new URL(returnUrl, window.location.href) : null
const entryFromReturnUrl = String(parsedReturnUrl?.searchParams.get(DISCLAIMER_ENTRY_QUERY_KEY) || '').trim()
return {
returnUrl,
entry: entryFromReturnUrl || readRestrictedEntryCodeFromUrl(parsedReturnUrl)
}
} catch {
return {
returnUrl: '',
entry: readRestrictedEntryCodeFromUrl()
}
}
})()
const hasRestrictedEntry = computed(() => Boolean(parsedParams.entry))
const backHref = computed(() => parsedParams.returnUrl || './?projectId=default')
const sections = computed(() => [
{
title: t('disclaimerPage.sections.standardBasisTitle'),
paragraphs: [
t('disclaimerPage.sections.standardBasisP1'),
t('disclaimerPage.sections.standardBasisP2'),
t('disclaimerPage.sections.standardBasisP3')
]
},
{
title: t('disclaimerPage.sections.referenceOnlyTitle'),
paragraphs: [t('disclaimerPage.sections.referenceOnlyP1')]
},
{
title: t('disclaimerPage.sections.accuracyTitle'),
paragraphs: [t('disclaimerPage.sections.accuracyP1')]
},
{
title: t('disclaimerPage.sections.riskTitle'),
paragraphs: [t('disclaimerPage.sections.riskP1')]
},
{
title: t('disclaimerPage.sections.liabilityTitle'),
paragraphs: [t('disclaimerPage.sections.liabilityP1'), t('disclaimerPage.sections.liabilityP2')]
},
{
title: t('disclaimerPage.sections.interruptionTitle'),
paragraphs: [t('disclaimerPage.sections.interruptionP1')]
},
{
title: t('disclaimerPage.sections.externalTitle'),
paragraphs: [t('disclaimerPage.sections.externalP1')]
},
{
title: t('disclaimerPage.sections.lawTitle'),
paragraphs: [t('disclaimerPage.sections.lawP1')]
}
])
const toggleLocale = () => {
const next = locale.value === 'en-US' ? 'zh-CN' : 'en-US'
setAppLocale(next as AppLocale)
}
const handleContinue = () => {
if (!accepted.value) return
if (parsedParams.entry) {
persistRestrictedDisclaimerAcceptance(parsedParams.entry)
}
window.location.href = backHref.value
}
watchEffect(() => {
const lang = locale.value || DEFAULT_LOCALE
document.documentElement.lang = lang
document.title = t('disclaimerPage.documentTitle')
})
accepted.value = parsedParams.entry ? hasAcceptedRestrictedDisclaimer(parsedParams.entry) : false
</script>
<template>
<main class="min-h-screen bg-[linear-gradient(180deg,#f7fbff_0%,#eef4f8_44%,#e4edf3_100%)] text-slate-900">
<div class="mx-auto w-full max-w-5xl px-4 py-6 sm:px-6 lg:px-8 lg:py-10">
<div class="mb-4 flex justify-end">
<Button
variant="outline"
size="sm"
class="h-9 cursor-pointer gap-2 rounded-full border-slate-300/80 bg-white/85 px-4 text-xs text-slate-700 shadow-sm backdrop-blur"
@click="toggleLocale"
>
<Languages class="h-3.5 w-3.5" />
<span>{{ locale === 'en-US' ? 'EN' : '中' }}</span>
<span class="hidden sm:inline">{{ t('disclaimerPage.actions.switchLocale') }}</span>
</Button>
</div>
<section class="overflow-hidden rounded-[28px] border border-slate-200/70 bg-white/85 shadow-[0_24px_80px_rgba(15,23,42,0.10)] backdrop-blur">
<div class="relative overflow-hidden px-5 py-6 sm:px-8 sm:py-8 lg:px-10">
<div class="pointer-events-none absolute inset-x-0 top-0 h-40 bg-[radial-gradient(circle_at_top,rgba(14,116,144,0.18),transparent_68%)]" />
<div class="relative">
<p class="text-xs font-bold tracking-[0.24em] text-teal-700">{{ t('disclaimerPage.eyebrow') }}</p>
<h1 class="mt-3 max-w-4xl text-2xl font-semibold leading-tight tracking-tight sm:text-3xl">
{{ t('disclaimerPage.pageTitle') }}
</h1>
<p class="mt-4 text-sm text-slate-500">
<span>{{ t('disclaimerPage.lastUpdatedLabel') }}</span>
<span>{{ t('disclaimerPage.lastUpdatedValue') }}</span>
</p>
<p class="mt-5 max-w-4xl text-sm leading-7 text-slate-600 sm:text-[15px]">
{{ t('disclaimerPage.leadText') }}
</p>
</div>
</div>
<div class="h-px bg-[linear-gradient(90deg,rgba(148,163,184,0),rgba(148,163,184,0.7),rgba(148,163,184,0))]" />
<div class="px-5 py-6 sm:px-8 lg:px-10 lg:py-8">
<section
v-for="section in sections"
:key="section.title"
class="mb-6 border-b border-slate-100 pb-6 last:mb-0 last:border-b-0 last:pb-0"
>
<h2 class="text-lg font-semibold leading-7 text-slate-900 sm:text-[20px]">{{ section.title }}</h2>
<p
v-for="paragraph in section.paragraphs"
:key="paragraph"
class="mt-3 text-sm leading-7 text-slate-600 sm:text-[15px]"
>
{{ paragraph }}
</p>
</section>
<section
v-if="hasRestrictedEntry"
class="mt-6 rounded-[24px] border border-teal-200/70 bg-[linear-gradient(135deg,rgba(15,118,110,0.08),rgba(14,165,233,0.08))] p-5 sm:p-6"
>
<h2 class="text-base font-semibold text-slate-900">{{ t('disclaimerPage.confirm.title') }}</h2>
<p class="mt-3 text-sm leading-7 text-slate-600">{{ t('disclaimerPage.confirm.desc1') }}</p>
<p class="mt-1 text-sm leading-7 text-slate-600">{{ t('disclaimerPage.confirm.desc2') }}</p>
<label class="mt-4 flex cursor-pointer items-start gap-3 rounded-2xl border border-slate-200/80 bg-white/80 px-4 py-3 text-sm leading-6 text-slate-700">
<input v-model="accepted" type="checkbox" class="mt-1 h-4 w-4 accent-teal-700" />
<span>{{ t('disclaimerPage.confirm.checkbox') }}</span>
</label>
<div class="mt-5 flex flex-col gap-3 sm:flex-row">
<button
type="button"
class="inline-flex min-h-11 items-center justify-center rounded-full bg-teal-700 px-5 text-sm font-semibold text-white shadow-[0_12px_24px_rgba(15,118,110,0.18)] transition hover:bg-teal-800 disabled:cursor-not-allowed disabled:opacity-55 disabled:shadow-none"
:disabled="!accepted"
@click="handleContinue"
>
{{ t('disclaimerPage.confirm.continue') }}
</button>
<a
:href="backHref"
class="inline-flex min-h-11 items-center justify-center rounded-full border border-slate-300 bg-white px-5 text-sm font-semibold text-slate-700 transition hover:bg-slate-50"
>
{{ t('disclaimerPage.actions.back') }}
</a>
</div>
<p class="mt-4 text-xs leading-5 text-slate-500">{{ t('disclaimerPage.confirm.hint') }}</p>
</section>
<div v-else class="mt-6 flex">
<a
:href="backHref"
class="inline-flex min-h-11 items-center justify-center rounded-full border border-slate-300 bg-white px-5 text-sm font-semibold text-slate-700 transition hover:bg-slate-50"
>
{{ t('disclaimerPage.actions.back') }}
</a>
</div>
</div>
</section>
</div>
</main>
</template>

View File

@ -74,6 +74,7 @@ import {
ToastTitle, ToastTitle,
ToastViewport ToastViewport
} from 'reka-ui' } from 'reka-ui'
import {useDataStore} from '@/pinia/zx'
const STORAGE_KEY = 'ht-card-v1' const STORAGE_KEY = 'ht-card-v1'
const tabStore = useTabStore() const tabStore = useTabStore()
@ -333,8 +334,18 @@ const loadContractBudgetFee = async (contractId: string) => {
loadHtMainTotalFee(`htExtraFee-${contractId}-additional-work`), loadHtMainTotalFee(`htExtraFee-${contractId}-additional-work`),
loadHtMainTotalFee(`htExtraFee-${contractId}-reserve`) loadHtMainTotalFee(`htExtraFee-${contractId}-reserve`)
]) ])
const parts = [serviceFee, additionalFee, reserveFee]
const total = sumNullableNumbers(parts) const zxRows = await useDataStore().query([
{ field: 'type', value: `${contractId}-zxFw`, operator: 'eq' }
])
const totalFinalFee = zxRows.reduce((sum, row) => {
return sum + (Number(row.finalFee) || 0)
}, 0)
const total = totalFinalFee;
/*const parts = [serviceFee, additionalFee, reserveFee]
const total = sumNullableNumbers(parts)*/
return total == null ? null : roundTo(total, 2) return total == null ? null : roundTo(total, 2)
} }

View File

@ -1,4 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch, type PropType } from 'vue' import { computed, defineComponent, h, nextTick, onActivated, onBeforeUnmount, onMounted, ref, shallowRef, watch, type PropType } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { AgGridVue } from 'ag-grid-vue3' import { AgGridVue } from 'ag-grid-vue3'
@ -60,12 +60,23 @@ const rowData = ref<SummaryRow[]>([])
const explanationText = ref('') const explanationText = ref('')
let reloadTimer: ReturnType<typeof setTimeout> | null = null let reloadTimer: ReturnType<typeof setTimeout> | null = null
/**
* 对数值数组求和保留3位小数
* @param values 数值数组可包含 null/undefined
* @returns 求和结果无有效值时返回 null
*/
const sum3 = (values: Array<number | null | undefined>) => { const sum3 = (values: Array<number | null | undefined>) => {
const valid = values.filter((v): v is number => toFiniteNumberOrNull(v) != null) const valid = values.filter((v): v is number => toFiniteNumberOrNull(v) != null)
if (valid.length === 0) return null if (valid.length === 0) return null
return roundTo(valid.reduce((a, b) => a + b, 0), 3) return roundTo(valid.reduce((a, b) => a + b, 0), 3)
} }
/**
* 计算时工法的总费用
* 遍历时工法明细行优先使用 serviceBudget否则通过 adoptedBudgetUnitPrice × personnelCount × workdayCount 计算
* @param state 时工法状态对象
* @returns 总费用保留3位小数无有效值时返回 null
*/
const sumHourlyMethodFee = (state: HourlyMethodStateLike | null): number | null => { const sumHourlyMethodFee = (state: HourlyMethodStateLike | null): number | null => {
const rows = Array.isArray(state?.detailRows) ? state.detailRows : [] const rows = Array.isArray(state?.detailRows) ? state.detailRows : []
if (rows.length === 0) return null if (rows.length === 0) return null
@ -88,6 +99,13 @@ const sumHourlyMethodFee = (state: HourlyMethodStateLike | null): number | null
return hasValid ? roundTo(total, 3) : null return hasValid ? roundTo(total, 3) : null
} }
/**
* 计算工程量清单法的总费用
* 遍历工程量清单明细行优先使用 budgetFee否则通过 quantity × unitPrice 计算
* 跳过固定小计行fee-subtotal-fixed
* @param state 工程量清单法状态对象
* @returns 总费用保留3位小数无有效值时返回 null
*/
const sumQuantityMethodFee = (state: QuantityMethodStateLike | null): number | null => { const sumQuantityMethodFee = (state: QuantityMethodStateLike | null): number | null => {
const rows = Array.isArray(state?.detailRows) ? state.detailRows : [] const rows = Array.isArray(state?.detailRows) ? state.detailRows : []
if (rows.length === 0) return null if (rows.length === 0) return null
@ -110,6 +128,12 @@ const sumQuantityMethodFee = (state: QuantityMethodStateLike | null): number | n
return hasValid ? roundTo(total, 3) : null return hasValid ? roundTo(total, 3) : null
} }
/**
* 加载合同段某行的三种计费方式费率法时工法工程量清单法并汇总
* @param mainStorageKey 主存储键 htExtraFee-{contractId}-additional-work
* @param rowId 行ID
* @returns 包含subtotal小计m0费率法详情m4时工法详情m5工程量清单法详情的对象
*/
const loadHtMethodSummaryByRow = async (mainStorageKey: string, rowId: string): Promise<{ const loadHtMethodSummaryByRow = async (mainStorageKey: string, rowId: string): Promise<{
subtotal: number | null subtotal: number | null
m0: { coe: string; fee: number } | null m0: { coe: string; fee: number } | null
@ -139,6 +163,12 @@ const loadHtMethodSummaryByRow = async (mainStorageKey: string, rowId: string):
} }
} }
/**
* 构建附加工作或预备费的汇总行数据
* @param rowType 行类型'additional' 附加工作 'reserve' 预备费
* @param list 费用项目列表包含 idnamecode
* @returns 包含 rows汇总行数组 explainLines说明文本数组的对象
*/
const buildFeeRows = async ( const buildFeeRows = async (
rowType: 'additional' | 'reserve', rowType: 'additional' | 'reserve',
list: Array<{ id: string | number; name: string; code: unknown }> list: Array<{ id: string | number; name: string; code: unknown }>
@ -182,6 +212,11 @@ const buildFeeRows = async (
return { rows, explainLines } return { rows, explainLines }
} }
/**
* 构建咨询服务汇总行数据
* 从合同状态中筛选已选中的服务项转换为 SummaryRow 格式
* @returns 咨询服务汇总行数组
*/
const buildServiceRows = (): SummaryRow[] => { const buildServiceRows = (): SummaryRow[] => {
const contractState = zxFwPricingStore.getContractState(props.contractId) const contractState = zxFwPricingStore.getContractState(props.contractId)
const selectedSet = new Set((contractState?.selectedIds || []).map(id => String(id))) const selectedSet = new Set((contractState?.selectedIds || []).map(id => String(id)))
@ -202,6 +237,11 @@ const buildServiceRows = (): SummaryRow[] => {
})) }))
} }
/**
* 重新加载所有汇总行数据
* 包括咨询服务附加工作预备费
* 同时生成说明文本
*/
const reloadRows = async () => { const reloadRows = async () => {
await zxFwPricingStore.loadContract(props.contractId) await zxFwPricingStore.loadContract(props.contractId)
const [additionalResult, reserveResult] = await Promise.all([ const [additionalResult, reserveResult] = await Promise.all([
@ -219,6 +259,10 @@ const reloadRows = async () => {
explanationText.value = lines.join('\n') explanationText.value = lines.join('\n')
} }
/**
* 延迟重新加载汇总数据防抖 80ms
* 避免频繁触发数据加载
*/
const scheduleReload = () => { const scheduleReload = () => {
if (reloadTimer) clearTimeout(reloadTimer) if (reloadTimer) clearTimeout(reloadTimer)
reloadTimer = setTimeout(() => { reloadTimer = setTimeout(() => {
@ -226,6 +270,10 @@ const scheduleReload = () => {
}, 80) }, 80)
} }
/**
* 刷新签名用于 watch 监听数据变化
* 监听合同数据附加工作预备费的主状态和方法状态变化
*/
const refreshSignature = computed(() => { const refreshSignature = computed(() => {
const additionalKey = `htExtraFee-${props.contractId}-additional-work` const additionalKey = `htExtraFee-${props.contractId}-additional-work`
const reserveKey = `htExtraFee-${props.contractId}-reserve` const reserveKey = `htExtraFee-${props.contractId}-reserve`
@ -238,6 +286,10 @@ const refreshSignature = computed(() => {
}) })
}) })
/**
* 计算总计行数据
* 对所有汇总行的各字段进行求和
*/
const totalRow = computed<SummaryRow>(() => { const totalRow = computed<SummaryRow>(() => {
const sumField = (pick: (row: SummaryRow) => number | null | undefined) => const sumField = (pick: (row: SummaryRow) => number | null | undefined) =>
sum3(rowData.value.map(pick)) sum3(rowData.value.map(pick))
@ -302,9 +354,6 @@ const columnDefs: ColDef<SummaryRow>[] = [
minWidth: 90, minWidth: 90,
maxWidth: 140, maxWidth: 140,
colSpan: params => (params.data?.rowType === 'total' ? 2 : 1), colSpan: params => (params.data?.rowType === 'total' ? 2 : 1),
cellClassRules: {
'ag-summary-label-cell': params => params.data?.rowType === 'total'
},
valueFormatter: params => { valueFormatter: params => {
if (params.data?.rowType === 'total') return params.data.name || t('htSummary.total') if (params.data?.rowType === 'total') return params.data.name || t('htSummary.total')
return typeof params.value === 'string' ? params.value : '' return typeof params.value === 'string' ? params.value : ''
@ -422,14 +471,28 @@ const summaryGridOptions: GridOptions<SummaryRow> = {
getRowClass: params => (params.data?.rowType === 'additional' || params.data?.rowType === 'reserve' ? 'ht-summary-fee-row' : '') getRowClass: params => (params.data?.rowType === 'additional' || params.data?.rowType === 'reserve' ? 'ht-summary-fee-row' : '')
} }
/**
* AG Grid 准备就绪回调
* 初始化 gridApi 并同步自动行高
* @param event Grid 准备就绪事件
*/
const onGridReady = (event: GridReadyEvent<SummaryRow>) => { const onGridReady = (event: GridReadyEvent<SummaryRow>) => {
gridApi.value = event.api gridApi.value = event.api
void syncAutoRowHeights() void syncAutoRowHeights()
} }
/**
* 检查 Grid API 是否可用
* @param api Grid API 实例
* @returns API 是否有效
*/
const isGridApiAlive = (api: GridApi<SummaryRow> | null | undefined): api is GridApi<SummaryRow> => const isGridApiAlive = (api: GridApi<SummaryRow> | null | undefined): api is GridApi<SummaryRow> =>
Boolean(api && !api.isDestroyed?.()) Boolean(api && !api.isDestroyed?.())
/**
* 同步自动行高
* 触发 AG Grid 重新计算行高并刷新单元格
*/
const syncAutoRowHeights = async () => { const syncAutoRowHeights = async () => {
await nextTick() await nextTick()
const api = gridApi.value const api = gridApi.value
@ -438,10 +501,20 @@ const syncAutoRowHeights = async () => {
api.refreshCells({ force: true }) api.refreshCells({ force: true })
} }
/**
* 首次数据渲染完成回调
* 同步自动行高以确保显示正确
* @param _event 首次数据渲染事件
*/
const onFirstDataRendered = (_event: FirstDataRenderedEvent<SummaryRow>) => { const onFirstDataRendered = (_event: FirstDataRenderedEvent<SummaryRow>) => {
void syncAutoRowHeights() void syncAutoRowHeights()
} }
/**
* 行数据更新回调
* 同步自动行高以适应新数据
* @param _event 行数据更新事件
*/
const onRowDataUpdated = (_event: RowDataUpdatedEvent<SummaryRow>) => { const onRowDataUpdated = (_event: RowDataUpdatedEvent<SummaryRow>) => {
void syncAutoRowHeights() void syncAutoRowHeights()
} }

View File

@ -19,6 +19,7 @@ import TypeLine from '@/layout/typeLine.vue';
import { useZxFwPricingStore } from '@/pinia/zxFwPricing' import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { roundTo, sumNullableNumbers, toFiniteNumber } from '@/lib/decimal' import { roundTo, sumNullableNumbers, toFiniteNumber } from '@/lib/decimal'
import { formatThousands } from '@/lib/numberFormat' import { formatThousands } from '@/lib/numberFormat'
import {useDataStore} from '@/pinia/zx'
// 1. Props + // 1. Props +
const props = defineProps<{ const props = defineProps<{
@ -152,7 +153,15 @@ const refreshContractBudget = async () => {
]) ])
const parts = [serviceFee, additionalFee, reserveFee] const parts = [serviceFee, additionalFee, reserveFee]
const total = sumNullableNumbers(parts) const total = sumNullableNumbers(parts)
contractBudget.value = total == null ? null : roundTo(total, 2)
const zxRows = await useDataStore().query([
{ field: 'type', value: `${props.contractId}-zxFw`, operator: 'eq' }
])
const totalFinalFee = zxRows.reduce((sum, row) => {
return sum + (Number(row.finalFee) || 0)
}, 0)
contractBudget.value = totalFinalFee == null ? null : roundTo(totalFinalFee, 2)
} }
const budgetRefreshSignature = computed(() => { const budgetRefreshSignature = computed(() => {
@ -334,11 +343,11 @@ const xmCategories = computed<XmCategoryItem[]>(() => [
{ key: 'base-info', label: t('htCard.categories.baseInfo'), component: htBaseInfoView }, { key: 'base-info', label: t('htCard.categories.baseInfo'), component: htBaseInfoView },
{ key: 'info', label: t('htCard.categories.scaleInfo'), component: htView }, { key: 'info', label: t('htCard.categories.scaleInfo'), component: htView },
{ key: 'contract', label: t('htCard.categories.services'), component: zxfwView }, { key: 'contract', label: t('htCard.categories.services'), component: zxfwView },
{ key: 'consult-category-factor', label: t('htCard.categories.consultFactor'), component: consultCategoryFactorView }, // { key: 'consult-category-factor', label: t('htCard.categories.consultFactor'), component: consultCategoryFactorView },
{ key: 'major-factor', label: t('htCard.categories.majorFactor'), component: majorFactorView }, // { key: 'major-factor', label: t('htCard.categories.majorFactor'), component: majorFactorView },
{ key: 'additional-work-fee', label: t('htCard.categories.additionalFee'), component: additionalWorkFeeView }, // { key: 'additional-work-fee', label: t('htCard.categories.additionalFee'), component: additionalWorkFeeView },
{ key: 'reserve-fee', label: t('htCard.categories.reserveFee'), component: reserveFeeView }, // { key: 'reserve-fee', label: t('htCard.categories.reserveFee'), component: reserveFeeView },
{ key: 'all', label: t('htCard.categories.summary'), component: summaryView }, // { key: 'all', label: t('htCard.categories.summary'), component: summaryView },
]); ]);
watch(budgetRefreshSignature, (next, prev) => { watch(budgetRefreshSignature, (next, prev) => {

View File

@ -16,16 +16,10 @@ const XM_DB_KEY = computed(() => {
}) })
const BASE_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1') const BASE_INFO_KEY = computed(() => props.projectInfoKey || 'xm-base-info-v1')
const { t } = useI18n() const { t } = useI18n()
const titleText = computed(() => `${t('htInfo.scaleDetailTitle')}${t('htInfo.scaleDetailHint')}`)
</script> </script>
<template> <template>
<CommonAgGrid <CommonAgGrid :title="t('htInfo.scaleDetailTitle')" :dbKey="DB_KEY" :xmInfoKey="XM_DB_KEY" :base-info-key="BASE_INFO_KEY"/>
:title="titleText"
:dbKey="DB_KEY"
:xmInfoKey="XM_DB_KEY"
:base-info-key="BASE_INFO_KEY"
/>
</template> </template>

File diff suppressed because it is too large Load Diff

View File

@ -310,7 +310,7 @@ const buildDefaultRows = (projectCountValue = getTargetProjectCount()): DetailRo
consultCategoryFactor: null, consultCategoryFactor: null,
majorFactor: null, majorFactor: null,
workStageFactor: 1, workStageFactor: 1,
workRatio: 1, workRatio: 100,
budgetFee: null, budgetFee: null,
budgetFeeBasic: null, budgetFeeBasic: null,
budgetFeeOptional: null, budgetFeeOptional: null,

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@ import MethodUnavailableNotice from '@/features/shared/components/MethodUnavaila
import { buildProjectScopedSessionKey } from '@/lib/pricingPersistControl' import { buildProjectScopedSessionKey } from '@/lib/pricingPersistControl'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'; import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale';
import {useDataStore} from "@/pinia/zx";
interface DetailRow { interface DetailRow {
id: string id: string
@ -30,9 +31,12 @@ interface DetailRow {
workload: number | null workload: number | null
basicFee: number | null basicFee: number | null
budgetBase: string budgetBase: string
budgetReferenceUnitPrice: string budgetReferenceUnitPrice: string | number | null
budgetAdoptedUnitPrice: number | null budgetAdoptedUnitPrice: number | null
consultCategoryFactor: number | null consultCategoryFactor: number | null
cLow: number | null
cMid: number | null
cHigh: number | null
serviceFee: number | null serviceFee: number | null
remark: string remark: string
path: string[] path: string[]
@ -45,7 +49,7 @@ interface XmInfoState {
const props = defineProps<{ const props = defineProps<{
contractId: string, contractId: string,
contractName: string,
serviceId: string | number serviceId: string | number
}>() }>()
const zxFwPricingStore = useZxFwPricingStore() const zxFwPricingStore = useZxFwPricingStore()
@ -60,15 +64,28 @@ let factorDefaultsLoaded = false
const paneInstanceCreatedAt = Date.now() const paneInstanceCreatedAt = Date.now()
const gridApi = ref<GridApi<DetailRow> | null>(null) const gridApi = ref<GridApi<DetailRow> | null>(null)
/**
* 获取服务对应的咨询服务分类因子
* @returns 咨询服务分类因子值如果没有则返回 null
*/
const getDefaultConsultCategoryFactor = () => const getDefaultConsultCategoryFactor = () =>
consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null consultCategoryFactorMap.value.get(String(props.serviceId)) ?? null
/**
* 确保咨询服务分类因子默认值已加载
* 从合同段的咨询因子配置中加载避免重复加载
*/
const ensureFactorDefaultsLoaded = async () => { const ensureFactorDefaultsLoaded = async () => {
if (factorDefaultsLoaded) return if (factorDefaultsLoaded) return
consultCategoryFactorMap.value = await loadConsultCategoryFactorMap(HT_CONSULT_FACTOR_KEY.value) consultCategoryFactorMap.value = await loadConsultCategoryFactorMap(HT_CONSULT_FACTOR_KEY.value)
factorDefaultsLoaded = true factorDefaultsLoaded = true
} }
/**
* 检查是否应该跳过数据持久化
* 用于在清空操作后的短时间内避免旧数据回填
* @returns 是否应该跳过持久化
*/
const shouldSkipPersist = () => { const shouldSkipPersist = () => {
const storageKey = buildProjectScopedSessionKey(PRICING_CLEAR_SKIP_PREFIX, DB_KEY.value) const storageKey = buildProjectScopedSessionKey(PRICING_CLEAR_SKIP_PREFIX, DB_KEY.value)
const raw = sessionStorage.getItem(storageKey) const raw = sessionStorage.getItem(storageKey)
@ -92,6 +109,11 @@ const shouldSkipPersist = () => {
return false return false
} }
/**
* 检查是否应该强制加载默认数据
* 用于在恢复默认操作后强制使用初始值
* @returns 是否应该强制加载默认数据
*/
const shouldForceDefaultLoad = () => { const shouldForceDefaultLoad = () => {
const storageKey = buildProjectScopedSessionKey(PRICING_FORCE_DEFAULT_PREFIX, DB_KEY.value) const storageKey = buildProjectScopedSessionKey(PRICING_FORCE_DEFAULT_PREFIX, DB_KEY.value)
const raw = sessionStorage.getItem(storageKey) const raw = sessionStorage.getItem(storageKey)
@ -101,8 +123,12 @@ const shouldForceDefaultLoad = () => {
return Number.isFinite(forceUntil) && Date.now() <= forceUntil return Number.isFinite(forceUntil) && Date.now() <= forceUntil
} }
/**
* 获取当前服务工作量法的状态
* @returns 工作量法状态对象包含明细行数据
*/
const getMethodState = () => const getMethodState = () =>
zxFwPricingStore.getServicePricingMethodState<DetailRow>(props.contractId, props.serviceId, 'workload') zxFwPricingStore.getServicePricingMethodState<DetailRow>(props.contractId, props.serviceId, 'serviceFee')
const detailRows = computed<DetailRow[]>({ const detailRows = computed<DetailRow[]>({
get: () => { get: () => {
@ -110,7 +136,7 @@ const detailRows = computed<DetailRow[]>({
return Array.isArray(rows) ? rows : [] return Array.isArray(rows) ? rows : []
}, },
set: rows => { set: rows => {
zxFwPricingStore.setServicePricingMethodState(props.contractId, props.serviceId, 'workload', { zxFwPricingStore.setServicePricingMethodState(props.contractId, props.serviceId, 'serviceFee', {
detailRows: rows detailRows: rows
}) })
} }
@ -127,9 +153,17 @@ type taskLite = {
maxPrice: number | null maxPrice: number | null
minPrice: number | null minPrice: number | null
defPrice: number | null defPrice: number | null
midPrice: number | null
highPrice: number | null
desc: string | null desc: string | null
} }
/**
* 获取任务显示名称
* 根据当前语言环境返回中文或英文名称
* @param task 任务对象
* @returns 任务显示名称
*/
const getTaskDisplayName = (task: taskLite | undefined) => { const getTaskDisplayName = (task: taskLite | undefined) => {
if (!task) return '' if (!task) return ''
return String(locale.value).toLowerCase().startsWith('en') return String(locale.value).toLowerCase().startsWith('en')
@ -137,6 +171,12 @@ const getTaskDisplayName = (task: taskLite | undefined) => {
: task.name : task.name
} }
/**
* 格式化任务参考单价
* 根据任务的最小/最大价格生成价格区间字符串
* @param task 任务对象
* @returns 格式化后的价格区间字符串 "100元-200元"
*/
const formatTaskReferenceUnitPrice = (task: taskLite) => { const formatTaskReferenceUnitPrice = (task: taskLite) => {
const unit = task.unit || '' const unit = task.unit || ''
const hasMin = typeof task.minPrice === 'number' && Number.isFinite(task.minPrice) const hasMin = typeof task.minPrice === 'number' && Number.isFinite(task.minPrice)
@ -147,8 +187,16 @@ const formatTaskReferenceUnitPrice = (task: taskLite) => {
return '' return ''
} }
/**
* 获取当前服务对应的工作任务 ID 列表
* taskList 中筛选出属于当前服务的任务并按 ID 排序
* @returns 任务 ID 数组
*/
const getSourceTaskIds = () => { const getSourceTaskIds = () => {
const currentServiceId = Number(props.serviceId) // serviceId ct-1776069559338-f621-zx-9 -> 9
const serviceIdStr = String(props.serviceId)
const match = serviceIdStr.match(/-(\d+)$/)
const currentServiceId = match ? Number(match[1]) : NaN
return Object.entries(taskList as Record<string, taskLite>) return Object.entries(taskList as Record<string, taskLite>)
.filter(([, task]) => Number(task.serviceID) === currentServiceId) .filter(([, task]) => Number(task.serviceID) === currentServiceId)
.map(([key]) => Number(key)) .map(([key]) => Number(key))
@ -158,6 +206,11 @@ const getSourceTaskIds = () => {
const isWorkloadMethodApplicable = computed(() => getSourceTaskIds().length > 0) const isWorkloadMethodApplicable = computed(() => getSourceTaskIds().length > 0)
/**
* 构建默认明细行数据
* 根据当前服务的任务列表生成初始行数据
* @returns 默认明细行数组
*/
const buildDefaultRows = (): DetailRow[] => { const buildDefaultRows = (): DetailRow[] => {
const rows: DetailRow[] = [] const rows: DetailRow[] = []
const sourceTaskIds = getSourceTaskIds() const sourceTaskIds = getSourceTaskIds()
@ -166,22 +219,24 @@ const buildDefaultRows = (): DetailRow[] => {
const task = (taskList as Record<string, taskLite | undefined>)[String(taskId)] const task = (taskList as Record<string, taskLite | undefined>)[String(taskId)]
const taskCode = task?.code || task?.ref || '' const taskCode = task?.code || task?.ref || ''
if (!taskCode || !task?.name) continue if (!taskCode || !task?.name) continue
const rowId = `task-${taskId}-${order}` const contractId = props.contractId;
const rowId = `${contractId}-task-${taskId}`
rows.push({ rows.push({
id: rowId, id: rowId,
taskCode, taskCode,
taskName: getTaskDisplayName(task), taskName: getTaskDisplayName(task),
unit: task.unit || '', unit: task.unit || '',
conversion: typeof task.conversion === 'number' && Number.isFinite(task.conversion) ? task.conversion : null, conversion: typeof task.conversion === 'number' && Number.isFinite(task.conversion) ? task.conversion : null,
workload: null, workload: typeof task.midPrice === 'number' && Number.isFinite(task.midPrice) ? task.midPrice : null,
basicFee: null, basicFee: null,
budgetBase: task.basicParam || '', budgetBase: task.basicParam || '',
budgetReferenceUnitPrice: formatTaskReferenceUnitPrice(task), budgetReferenceUnitPrice: null,
budgetAdoptedUnitPrice: budgetAdoptedUnitPrice:
typeof task.defPrice === 'number' && Number.isFinite(task.defPrice) ? task.defPrice : null, typeof task.defPrice === 'number' && Number.isFinite(task.defPrice) ? task.defPrice : null,
consultCategoryFactor: getDefaultConsultCategoryFactor(), consultCategoryFactor: typeof task.highPrice === 'number' && Number.isFinite(task.highPrice) ? task.highPrice : null,
serviceFee: null, serviceFee: null,
remark: task.desc|| '', remark: task.desc|| '',
type: 'task',
path: [rowId] path: [rowId]
}) })
} }
@ -191,6 +246,12 @@ const buildDefaultRows = (): DetailRow[] => {
const isNoTaskRow = (row: DetailRow | undefined) => row?.id?.startsWith('task-none-') ?? false const isNoTaskRow = (row: DetailRow | undefined) => row?.id?.startsWith('task-none-') ?? false
/**
* 合并数据库中的行数据与默认行数据
* 保留用户编辑的值工作量单价因子等缺失时使用默认值
* @param rowsFromDb 从数据库加载的行数据
* @returns 合并后的明细行数组
*/
const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] => { const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] => {
const dbValueMap = new Map<string, DetailRow>() const dbValueMap = new Map<string, DetailRow>()
for (const row of rowsFromDb || []) { for (const row of rowsFromDb || []) {
@ -202,20 +263,38 @@ const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] =>
if (!fromDb) return row if (!fromDb) return row
const hasRemark = Object.prototype.hasOwnProperty.call(fromDb, 'remark') const hasRemark = Object.prototype.hasOwnProperty.call(fromDb, 'remark')
const hasConsultCategoryFactor = Object.prototype.hasOwnProperty.call(fromDb, 'consultCategoryFactor') const hasConsultCategoryFactor = Object.prototype.hasOwnProperty.call(fromDb, 'consultCategoryFactor')
// cLow
let cLowValue: number | null = null
let cMidValue: number | null = null
let cHighValue: number | null = null
const refPrice = fromDb.budgetReferenceUnitPrice
const lowPrice = row.budgetAdoptedUnitPrice
const midPrice = row.workload
const highPrice = row.consultCategoryFactor
cLowValue = refPrice * lowPrice
cMidValue = refPrice * midPrice
cHighValue = refPrice * highPrice
let serviceFee = cMidValue
if (fromDb.serviceFee != null && fromDb.serviceFee != 0) {
serviceFee = fromDb.serviceFee
}
return { return {
...row, ...row,
workload: typeof fromDb.workload === 'number' ? fromDb.workload : null, workload: typeof fromDb.workload === 'number' ? fromDb.workload : null,
basicFee: typeof fromDb.basicFee === 'number' ? fromDb.basicFee : null, basicFee: typeof fromDb.basicFee === 'number' ? fromDb.basicFee : null,
budgetAdoptedUnitPrice: budgetReferenceUnitPrice: fromDb.budgetReferenceUnitPrice ?? row.budgetReferenceUnitPrice,
typeof fromDb.budgetAdoptedUnitPrice === 'number' ? fromDb.budgetAdoptedUnitPrice : null, budgetAdoptedUnitPrice: 1, // 1
cLow: cLowValue,
cMid: cMidValue,
cHigh: cHighValue,
consultCategoryFactor: consultCategoryFactor:
typeof fromDb.consultCategoryFactor === 'number' typeof fromDb.consultCategoryFactor === 'number'
? fromDb.consultCategoryFactor ? fromDb.consultCategoryFactor
: hasConsultCategoryFactor : hasConsultCategoryFactor
? null ? null
: getDefaultConsultCategoryFactor(), : getDefaultConsultCategoryFactor(),
serviceFee: typeof fromDb.serviceFee === 'number' ? fromDb.serviceFee : null, serviceFee: serviceFee,
remark: typeof fromDb.remark === 'string' ? fromDb.remark : hasRemark ? '' : row.remark remark: typeof fromDb.remark === 'string' ? fromDb.remark : hasRemark ? '' : row.remark
} }
}) })
@ -227,6 +306,12 @@ const parseSanitizedNumberOrNull = (value: unknown) =>
const parseSanitizedAdoptedPriceOrNull = (value: unknown) => const parseSanitizedAdoptedPriceOrNull = (value: unknown) =>
parseNumberOrNull(value, { sanitize: true, precision: 6 }) parseNumberOrNull(value, { sanitize: true, precision: 6 })
/**
* 计算基础费用
* 公式基础费用 = 采用单价 × 换算系数 × 工作量
* @param row 明细行数据
* @returns 基础费用保留2位小数如果数据不完整则返回 null
*/
const calcBasicFee = (row: DetailRow | undefined) => { const calcBasicFee = (row: DetailRow | undefined) => {
if (!row || isNoTaskRow(row)) return null if (!row || isNoTaskRow(row)) return null
const price = row.budgetAdoptedUnitPrice const price = row.budgetAdoptedUnitPrice
@ -245,6 +330,12 @@ const calcBasicFee = (row: DetailRow | undefined) => {
return roundTo(toDecimal(price).mul(conversion).mul(workload), 2) return roundTo(toDecimal(price).mul(conversion).mul(workload), 2)
} }
/**
* 计算服务费用
* 公式服务费用 = 基础费用 × 咨询服务分类因子
* @param row 明细行数据
* @returns 服务费用保留2位小数如果数据不完整则返回 null
*/
const calcServiceFee = (row: DetailRow | undefined) => { const calcServiceFee = (row: DetailRow | undefined) => {
if (!row || isNoTaskRow(row)) return null if (!row || isNoTaskRow(row)) return null
const factor = row.consultCategoryFactor const factor = row.consultCategoryFactor
@ -259,6 +350,12 @@ const calcServiceFee = (row: DetailRow | undefined) => {
return roundTo(toDecimal(basicFee).mul(factor), 2) return roundTo(toDecimal(basicFee).mul(factor), 2)
} }
/**
* 格式化可编辑数字单元格
* 根据行类型和值状态返回相应的显示文本
* @param params AG Grid 单元格参数
* @returns 格式化后的显示文本
*/
const formatEditableNumber = (params: any) => { const formatEditableNumber = (params: any) => {
if (isNoTaskRow(params.data)) return t('workloadPricing.none') if (isNoTaskRow(params.data)) return t('workloadPricing.none')
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
@ -268,6 +365,12 @@ const formatEditableNumber = (params: any) => {
return formatThousandsFlexible(params.value, 3) return formatThousandsFlexible(params.value, 3)
} }
/**
* 判断两行是否应该合并显示按任务名称和预算基数
* 用于 AG Grid spanRows 功能相同任务和预算基数的行合并显示
* @param params AG Grid 行比较参数
* @returns 是否应该合并
*/
const spanRowsByTaskName = (params: any) => { const spanRowsByTaskName = (params: any) => {
const rowA = params?.nodeA?.data as DetailRow | undefined const rowA = params?.nodeA?.data as DetailRow | undefined
const rowB = params?.nodeB?.data as DetailRow | undefined const rowB = params?.nodeB?.data as DetailRow | undefined
@ -285,9 +388,6 @@ const columnDefs: ColDef<DetailRow>[] = [
width: 120, width: 120,
pinned: 'left', pinned: 'left',
colSpan: params => (params.node?.rowPinned ? 2 : 1), colSpan: params => (params.node?.rowPinned ? 2 : 1),
cellClassRules: {
'ag-summary-label-cell': params => Boolean(params.node?.rowPinned)
},
valueFormatter: params => (params.node?.rowPinned ? t('workloadPricing.total') : params.value || '') valueFormatter: params => (params.node?.rowPinned ? t('workloadPricing.total') : params.value || '')
}, },
{ {
@ -308,7 +408,6 @@ const columnDefs: ColDef<DetailRow>[] = [
field: 'budgetBase', field: 'budgetBase',
minWidth: 150, minWidth: 150,
autoHeight: true, autoHeight: true,
width: 180, width: 180,
colSpan: params => (params.node?.rowPinned ? 3 : 1), colSpan: params => (params.node?.rowPinned ? 3 : 1),
spanRows: spanRowsByTaskName, spanRows: spanRowsByTaskName,
@ -319,15 +418,16 @@ const columnDefs: ColDef<DetailRow>[] = [
field: 'budgetReferenceUnitPrice', field: 'budgetReferenceUnitPrice',
minWidth: 170, minWidth: 170,
flex: 1, flex: 1,
valueFormatter: params => params.value || '' editable: params => !params.node?.group && !params.node?.rowPinned && !isNoTaskRow(params.data),
valueFormatter: formatEditableNumber
}, },
{ /*{
headerName: t('workloadPricing.columns.budgetAdoptedUnitPrice'), headerName: t('workloadPricing.columns.budgetAdoptedUnitPrice'),
field: 'budgetAdoptedUnitPrice', field: 'budgetAdoptedUnitPrice',
headerClass: 'ag-right-aligned-header', headerClass: 'ag-right-aligned-header',
minWidth: 170, minWidth: 170,
flex: 1, flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned && !isNoTaskRow(params.data), // editable: params => !params.node?.group && !params.node?.rowPinned && !isNoTaskRow(params.data),
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''), cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
cellClassRules: { cellClassRules: {
'ag-right-aligned-cell': () => true, 'ag-right-aligned-cell': () => true,
@ -347,8 +447,58 @@ const columnDefs: ColDef<DetailRow>[] = [
const unit = params.data?.unit || '' const unit = params.data?.unit || ''
return `${formatThousandsFlexible(params.value, 6)}${unit}` return `${formatThousandsFlexible(params.value, 6)}${unit}`
} }
},*/
{
headerName: t('workloadPricing.columns.budgetAdoptedUnitPrice'),
field: 'budgetAdoptedUnitPrice',
headerClass: 'ag-right-aligned-header',
minWidth: 170,
flex: 1,
// editable false
editable: false,
//
cellClass: 'ag-right-aligned-cell',
// cellClassRules editable-cell
cellClassRules: {
'ag-right-aligned-cell': () => true
},
// 1
valueFormatter: params => {
const unit = params.data?.unit || ''
return `1`
// return `1${unit}`
},
//
valueParser: () => 1,
//
cellEditor: undefined
}, },
{ {
headerName: t('workloadPricing.columns.workload'),
field: 'workload',
headerClass: 'ag-right-aligned-header',
minWidth: 170,
flex: 1,
// editable false
editable: false,
//
cellClass: 'ag-right-aligned-cell',
// cellClassRules editable-cell
cellClassRules: {
'ag-right-aligned-cell': () => true
},
// 1
valueFormatter: params => {
const unit = params.data?.unit || ''
return `2`
// return `1${unit}`
},
//
valueParser: () => 1,
//
cellEditor: undefined
},
/*{
headerName: t('workloadPricing.columns.workload'), headerName: t('workloadPricing.columns.workload'),
field: 'workload', field: 'workload',
minWidth: 140, minWidth: 140,
@ -367,8 +517,33 @@ const columnDefs: ColDef<DetailRow>[] = [
aggFunc: decimalAggSum, aggFunc: decimalAggSum,
valueParser: params => parseSanitizedNumberOrNull(params.newValue), valueParser: params => parseSanitizedNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber valueFormatter: formatEditableNumber
}, },*/
{ {
headerName: t('workloadPricing.columns.consultCategoryFactor'),
field: 'consultCategoryFactor',
headerClass: 'ag-right-aligned-header',
minWidth: 170,
flex: 1,
// editable false
editable: false,
//
cellClass: 'ag-right-aligned-cell',
// cellClassRules editable-cell
cellClassRules: {
'ag-right-aligned-cell': () => true
},
// 1
valueFormatter: params => {
const unit = params.data?.unit || ''
return `3`
// return `1${unit}`
},
//
valueParser: () => 1,
//
cellEditor: undefined
},
/*{
headerName: t('workloadPricing.columns.consultCategoryFactor'), headerName: t('workloadPricing.columns.consultCategoryFactor'),
field: 'consultCategoryFactor', field: 'consultCategoryFactor',
width: 80, width: 80,
@ -387,6 +562,54 @@ const columnDefs: ColDef<DetailRow>[] = [
}, },
valueParser: params => parseSanitizedNumberOrNull(params.newValue), valueParser: params => parseSanitizedNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber valueFormatter: formatEditableNumber
},*/
{
headerName: t('workloadPricing.columns.cLow'),
field: 'cLow',
width: 100,
minWidth: 70,
maxWidth: 120,
//
editable: false,
//
cellClass: 'ag-right-aligned-cell',
cellClassRules: {
'ag-right-aligned-cell': () => true
}/*,
valueParser: params => parseSanitizedNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber*/
},
{
headerName: t('workloadPricing.columns.cMid'),
field: 'cMid',
width: 100,
minWidth: 70,
maxWidth: 120,
//
editable: false,
//
cellClass: 'ag-right-aligned-cell',
cellClassRules: {
'ag-right-aligned-cell': () => true
}/*,
valueParser: params => parseSanitizedNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber*/
},
{
headerName: t('workloadPricing.columns.cHigh'),
field: 'cHigh',
width: 100,
minWidth: 70,
maxWidth: 120,
//
editable: false,
//
cellClass: 'ag-right-aligned-cell',
cellClassRules: {
'ag-right-aligned-cell': () => true
}/*,
valueParser: params => parseSanitizedNumberOrNull(params.newValue),
valueFormatter: formatEditableNumber*/
}, },
{ {
headerName: t('workloadPricing.columns.serviceFee'), headerName: t('workloadPricing.columns.serviceFee'),
@ -394,17 +617,19 @@ const columnDefs: ColDef<DetailRow>[] = [
headerClass: 'ag-right-aligned-header', headerClass: 'ag-right-aligned-header',
minWidth: 150, minWidth: 150,
flex: 1, flex: 1,
editable: false, editable: params => !params.node?.group && !params.node?.rowPinned && !isNoTaskRow(params.data),
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
cellClassRules: { cellClassRules: {
'ag-right-aligned-cell': () => true 'ag-right-aligned-cell': () => true,
'editable-cell-empty': params =>
!params.node?.group &&
!params.node?.rowPinned &&
!isNoTaskRow(params.data) &&
(params.value == null || params.value === '')
}, },
valueGetter: params => (params.node?.rowPinned ? params.data?.serviceFee ?? null : calcServiceFee(params.data)), valueParser: params => parseSanitizedNumberOrNull(params.newValue),
aggFunc: decimalAggSum, valueFormatter: formatEditableNumber
valueFormatter: params => {
if (isNoTaskRow(params.data)) return t('workloadPricing.none')
if (params.value == null || params.value === '') return ''
return formatThousandsFlexible(roundTo(params.value, 3), 3)
}
}, },
{ {
headerName: t('workloadPricing.columns.remark'), headerName: t('workloadPricing.columns.remark'),
@ -433,7 +658,9 @@ const columnDefs: ColDef<DetailRow>[] = [
] ]
const gridColumnDefs = computed(() => withReadonlyAutoHeight(columnDefs)) const gridColumnDefs = computed(() => withReadonlyAutoHeight(columnDefs))
const totalWorkload = computed(() => sumByNumber(detailRows.value, row => row.workload)) const totalWorkload = computed(() => {
return 1;
})
const totalBasicFee = computed(() => sumByNumber(detailRows.value, row => calcBasicFee(row))) const totalBasicFee = computed(() => sumByNumber(detailRows.value, row => calcBasicFee(row)))
const totalServiceFee = computed(() => sumNullableBy(detailRows.value, row => calcServiceFee(row))) const totalServiceFee = computed(() => sumNullableBy(detailRows.value, row => calcServiceFee(row)))
const pinnedTopRowData = computed(() => const pinnedTopRowData = computed(() =>
@ -457,44 +684,63 @@ const pinnedTopRowData = computed(() =>
/**
* 构建用于持久化的明细行数据
* 计算并附加基础费用和服务费用字段
* @returns 包含计算字段的明细行数组
*/
const buildPersistDetailRows = () => const buildPersistDetailRows = () =>
detailRows.value.map(row => ({ detailRows.value.map(row => ({
...row, ...row,
basicFee: calcBasicFee(row), basicFee: calcBasicFee(row),
serviceFee: calcServiceFee(row) serviceFee: row.serviceFee
})) }))
/**
* 保存数据到 IndexedDB 并同步到 zxFw store
* 保存时机单元格编辑批量操作结束组件失活/卸载时
* 同步工作量法的总服务费用到咨询服务汇总表
*/
const saveToIndexedDB = async () => { const saveToIndexedDB = async () => {
if (!isWorkloadMethodApplicable.value) return
if (shouldSkipPersist()) return
try { try {
const payload = { const rows = detailRows.value.map(row => ({ ...row, type: `${props.contractId}-task`}))
detailRows: JSON.parse(JSON.stringify(buildPersistDetailRows())) const stats = await useDataStore().upsertBatch(rows)
} console.log('💾 数据保存成功:', stats)
zxFwPricingStore.setServicePricingMethodState( /*zxFwPricingStore.setServicePricingMethodState(
props.contractId, props.contractId,
props.serviceId, props.serviceId,
'workload', 'serviceFee',
payload, payload,
{ force: true } { force: true }
) )*/
const synced = await syncPricingTotalToZxFw({ // const synced = await syncPricingTotalToZxFw({
contractId: props.contractId, // contractId: props.contractId,
serviceId: props.serviceId, // serviceId: props.serviceId,
field: 'workload', // field: 'serviceFee',
value: totalServiceFee.value // value: totalWorkload.value
}) // })
if (!synced) return // if (!synced) return
} catch (error) { } catch (error) {
console.error('saveToIndexedDB failed:', error) console.error('saveToIndexedDB failed:', error)
} }
} }
/**
* 判断两个可空数值是否相等考虑精度
* @param left 第一个数值
* @param right 第二个数值
* @returns 是否相等
*/
const isSameNullableNumber = (left: number | null | undefined, right: number | null | undefined) => { const isSameNullableNumber = (left: number | null | undefined, right: number | null | undefined) => {
if (left == null && right == null) return true if (left == null && right == null) return true
if (left == null || right == null) return false if (left == null || right == null) return false
return roundTo(left, 6) === roundTo(right, 6) return roundTo(left, 6) === roundTo(right, 6)
} }
/**
* 从合同段同步咨询服务分类因子
* 当合同段的咨询因子配置变化时更新当前工作量的因子默认值
*/
const syncLinkedConsultFactorFromHt = async () => { const syncLinkedConsultFactorFromHt = async () => {
if (!isWorkloadMethodApplicable.value || detailRows.value.length === 0) return if (!isWorkloadMethodApplicable.value || detailRows.value.length === 0) return
consultCategoryFactorMap.value = await loadConsultCategoryFactorMap(HT_CONSULT_FACTOR_KEY.value) consultCategoryFactorMap.value = await loadConsultCategoryFactorMap(HT_CONSULT_FACTOR_KEY.value)
@ -520,23 +766,30 @@ const linkedConsultFactorSignature = computed(() => JSON.stringify({
?? null ?? null
})) }))
/**
* IndexedDB 加载工作量法数据
* 加载时机组件挂载激活storageKey 变化时
* 优先加载历史数据没有则使用默认数据
*/
const loadFromIndexedDB = async () => { const loadFromIndexedDB = async () => {
try { try {
if (!isWorkloadMethodApplicable.value) { if (!isWorkloadMethodApplicable.value) {
detailRows.value = [] detailRows.value = []
return return
} }
const taskRows = await useDataStore().query([
await ensureFactorDefaultsLoaded() { field: 'type', value: `${props.contractId}-task`, operator: 'eq' }
])
/*await ensureFactorDefaultsLoaded()
if (shouldForceDefaultLoad()) { if (shouldForceDefaultLoad()) {
detailRows.value = buildDefaultRows() detailRows.value = buildDefaultRows()
return return
} }*/
const data = await zxFwPricingStore.loadServicePricingMethodState<DetailRow>(props.contractId, props.serviceId, 'workload') // const data = await zxFwPricingStore.loadServicePricingMethodState<DetailRow>(props.contractId, props.serviceId, 'serviceFee')
if (data) { if (taskRows) {
detailRows.value = mergeWithDictRows(data.detailRows) detailRows.value = mergeWithDictRows(taskRows)
return return
} }
@ -547,6 +800,10 @@ const loadFromIndexedDB = async () => {
} }
} }
/**
* 根据任务词典更新行标签
* 当语言切换或任务词典更新时同步更新任务名称单位等信息
*/
const relabelRowsFromTaskDict = async () => { const relabelRowsFromTaskDict = async () => {
if (!isWorkloadMethodApplicable.value || detailRows.value.length === 0) return if (!isWorkloadMethodApplicable.value || detailRows.value.length === 0) return
let changed = false let changed = false
@ -584,24 +841,46 @@ const relabelRowsFromTaskDict = async () => {
let isBulkClipboardMutation = false let isBulkClipboardMutation = false
/**
* 提交网格变更并保存
* 在单元格编辑完成后调用触发数据持久化
*/
const commitGridChanges = () => { const commitGridChanges = () => {
void saveToIndexedDB() void saveToIndexedDB()
} }
/**
* 处理单元格值变化事件
* 触发时机用户编辑完单元格后
* 批量操作期间跳过避免重复保存
*/
const handleCellValueChanged = () => { const handleCellValueChanged = () => {
if (isBulkClipboardMutation) return if (isBulkClipboardMutation) return
commitGridChanges() commitGridChanges()
} }
/**
* 处理批量粘贴/填充操作开始
* 设置标志位避免在批量操作过程中频繁保存
*/
const handleBulkMutationStart = () => { const handleBulkMutationStart = () => {
isBulkClipboardMutation = true isBulkClipboardMutation = true
} }
/**
* 处理批量粘贴/填充操作结束
* 清除标志位并保存所有变更
*/
const handleBulkMutationEnd = () => { const handleBulkMutationEnd = () => {
isBulkClipboardMutation = false isBulkClipboardMutation = false
commitGridChanges() commitGridChanges()
} }
/**
* AG Grid 初始化完成回调
* 记录 gridApi 实例用于后续刷新单元格等操作
* @param event AG Grid 初始化事件
*/
const handleGridReady = (event: GridReadyEvent<DetailRow>) => { const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
gridApi.value = event.api gridApi.value = event.api
} }
@ -619,6 +898,21 @@ watch(
void relabelRowsFromTaskDict() void relabelRowsFromTaskDict()
} }
) )
watch(
() => useDataStore().items,
async (newItems) => {
if (Object.keys(newItems).length > 0) {
loadFromIndexedDB();
}
},
{ immediate: true, deep: true }
)
/**
* 处理复制到剪贴板的单元格数据
* 将数组类型的数据转换为 JSON 字符串
* @param params AG Grid 剪贴板参数
* @returns 处理后的单元格值
*/
const processCellForClipboard = (params: any) => { const processCellForClipboard = (params: any) => {
if (Array.isArray(params.value)) { if (Array.isArray(params.value)) {
return JSON.stringify(params.value); // return JSON.stringify(params.value); //
@ -626,6 +920,12 @@ const processCellForClipboard = (params: any) => {
return params.value; return params.value;
}; };
/**
* 处理从剪贴板粘贴的单元格数据
* 根据字段类型解析数值单价工作量因子等
* @param params AG Grid 剪贴板参数
* @returns 解析后的单元格值
*/
const processCellFromClipboard = (params: any) => { const processCellFromClipboard = (params: any) => {
const field = String(params.column?.getColDef?.().field || '') const field = String(params.column?.getColDef?.().field || '')
if (field === 'budgetAdoptedUnitPrice') { if (field === 'budgetAdoptedUnitPrice') {
@ -668,7 +968,7 @@ const mydiyTheme = myTheme.withParams({
</div> </div>
<div v-if="isWorkloadMethodApplicable" :class="agGridWrapClass"> <div v-if="isWorkloadMethodApplicable" :class="agGridWrapClass">
<AgGridVue :style="agGridStyle" :rowData="detailRows" :pinnedTopRowData="pinnedTopRowData" <AgGridVue :style="agGridStyle" :rowData="detailRows"
:columnDefs="gridColumnDefs" :gridOptions="gridOptions" :theme="mydiyTheme" :treeData="false" :columnDefs="gridColumnDefs" :gridOptions="gridOptions" :theme="mydiyTheme" :treeData="false"
:animateRows="true" :animateRows="true"
:enableCellSpan="true" :enableCellSpan="true"

View File

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue' import {computed, nextTick, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch} from 'vue'
import { useI18n } from 'vue-i18n' import {useI18n} from 'vue-i18n'
import { AgGridVue } from 'ag-grid-vue3' import {AgGridVue} from 'ag-grid-vue3'
import type { import type {
ColDef, ColDef,
ColGroupDef, ColGroupDef,
@ -10,17 +10,24 @@ import type {
GridReadyEvent, GridReadyEvent,
RowDataUpdatedEvent RowDataUpdatedEvent
} from 'ag-grid-community' } from 'ag-grid-community'
import { expertList } from '@/sql' import {expertList} from '@/sql'
import { myTheme, gridOptions, agGridWrapClass, agGridStyle } from '@/lib/diyAgGridOptions' import {myTheme, gridOptions, agGridWrapClass, agGridStyle} from '@/lib/diyAgGridOptions'
import { decimalAggSum, roundTo, sumByNumber, sumNullableNumbers, toDecimal } from '@/lib/decimal' import {decimalAggSum, roundTo, sumByNumber, sumNullableNumbers, toDecimal} from '@/lib/decimal'
import { formatThousandsFlexible } from '@/lib/numberFormat' import {formatThousandsFlexible} from '@/lib/numberFormat'
import { parseNumberOrNull } from '@/lib/number' import {parseNumberOrNull} from '@/lib/number'
import { syncPricingTotalToZxFw, type ZxFwPricingField } from '@/lib/zxFwPricingSync' import {syncPricingTotalToZxFw, type ZxFwPricingField} from '@/lib/zxFwPricingSync'
import { useZxFwPricingStore, type HtFeeMethodType, type ServicePricingMethod } from '@/pinia/zxFwPricing' import {useZxFwPricingStore, type HtFeeMethodType, type ServicePricingMethod} from '@/pinia/zxFwPricing'
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale' import {AG_GRID_LOCALE_CN} from '@ag-grid-community/locale'
import { buildProjectScopedSessionKey } from '@/lib/pricingPersistControl' import {buildProjectScopedSessionKey} from '@/lib/pricingPersistControl'
import { withReadonlyAutoHeight } from '@/lib/agGridReadonlyAutoHeight' import {withReadonlyAutoHeight} from '@/lib/agGridReadonlyAutoHeight'
import {formatScaleEditableNumber, formatScaleReadonlyMoney} from "@/lib/pricingScaleGrid";
import {ValueParserParams, ValueFormatterParams} from 'ag-grid-community';
import {useKvStore} from "@/pinia/kv";
import {useDataStore} from '@/pinia/zx'
/**
* 数据行接口定义
* 注意此接口需与 columnDefs 中的字段保持一致
*/
interface DetailRow { interface DetailRow {
id: string id: string
expertCode: string expertCode: string
@ -29,15 +36,35 @@ interface DetailRow {
compositeBudgetUnitPrice: string compositeBudgetUnitPrice: string
adoptedBudgetUnitPrice: number | null adoptedBudgetUnitPrice: number | null
personnelCount: number | null personnelCount: number | null
workdayCount: number | null workdayCount: number | string | null
serviceBudget: number | null serviceBudget: number | string | null
remark: string remark: string
path: string[] path: string[]
// columnDefs
unitPrice?: number | string | null
workdayCount1?: number | string | null
feeSubtotal?: number | string | null
unitPrice2?: number | string | null
workdayCount2?: number | string | null
feeSubtotal2?: number | string | null
unitPrice3?: number | string | null
workdayCount3?: number | string | null
feeSubtotal3?: number | string | null
unitPrice4?: number | string | null
workdayCount4?: number | string | null
feeSubtotal4?: number | string | null
unitPrice5?: number | string | null
workdayCount5?: number | string | null
feeSubtotal5?: number | string | null
workdayCount6?: number | string | null
feeSubtotal6?: number | string | null,
avgUnitPrice: number | string | null
} }
interface GridState { interface GridState {
detailRows: DetailRow[] detailRows: DetailRow[]
} }
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
storageKey: string storageKey: string
@ -57,7 +84,7 @@ const props = withDefaults(
} }
) )
const zxFwPricingStore = useZxFwPricingStore() const zxFwPricingStore = useZxFwPricingStore()
const { t, locale } = useI18n() const {t, locale} = useI18n()
const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:' const PRICING_CLEAR_SKIP_PREFIX = 'pricing-clear-skip:'
const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:' const PRICING_FORCE_DEFAULT_PREFIX = 'pricing-force-default:'
@ -124,34 +151,9 @@ const getHtMethodState = () => {
} }
const detailRows = computed<DetailRow[]>({ const detailRows = computed<DetailRow[]>({
get: () => { get: () => {
if (useServicePricingState.value) {
const rows = getServiceMethodState()?.detailRows
return Array.isArray(rows) ? rows : []
}
if (useHtMethodState.value) {
const rows = getHtMethodState()?.detailRows
return Array.isArray(rows) ? rows : []
}
return fallbackDetailRows.value return fallbackDetailRows.value
}, },
set: rows => { set: rows => {
if (useServicePricingState.value && serviceMethod.value) {
const currentState = getServiceMethodState()
zxFwPricingStore.setServicePricingMethodState(props.contractId!, props.serviceId!, serviceMethod.value, {
detailRows: rows,
projectCount: currentState?.projectCount ?? null
})
return
}
if (useHtMethodState.value) {
zxFwPricingStore.setHtFeeMethodState(
props.htMainStorageKey!,
props.htRowId!,
props.htMethodType!,
{ detailRows: rows }
)
return
}
fallbackDetailRows.value = rows fallbackDetailRows.value = rows
} }
}) })
@ -213,12 +215,18 @@ const getDefaultAdoptedBudgetUnitPrice = (expert: ExpertLite) => {
return roundTo(toDecimal(expert.defPrice).mul(expert.manageCoe), 2) return roundTo(toDecimal(expert.defPrice).mul(expert.manageCoe), 2)
} }
const buildDefaultRows = (): DetailRow[] => { const buildDefaultRows = async (): Promise<DetailRow[]> => {
const rows: DetailRow[] = [] const rows: DetailRow[] = []
for (const [expertId, expert] of expertEntries) { const rowsToMap = await useDataStore().query([
const rowId = `expert-${expertId}` { field: 'type', value: `${props.contractId}-zxFw`, operator: 'eq' }
])
for (const expert of rowsToMap) {
const expertId = expert.id || expert._id //
const contractId = props.contractId
const rowId = `${contractId}-hourly-${expertId}`
rows.push({ rows.push({
id: rowId, id: rowId,
type: `${props.contractId}-hourly`,
expertCode: expert.code, expertCode: expert.code,
expertName: getExpertDisplayName(expert), expertName: getExpertDisplayName(expert),
laborBudgetUnitPrice: formatPriceRange(expert.minPrice, expert.maxPrice), laborBudgetUnitPrice: formatPriceRange(expert.minPrice, expert.maxPrice),
@ -234,30 +242,55 @@ const buildDefaultRows = (): DetailRow[] => {
return rows return rows
} }
const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] => { /**
* 合并数据库中的行数据与默认行数据
* 保留用户编辑的所有字段值包括新增的职称分类字段
*/
const mergeWithDictRows = async (rowsFromDb: DetailRow[] | undefined): Promise<DetailRow[]> => {
const dbValueMap = new Map<string, DetailRow>() const dbValueMap = new Map<string, DetailRow>()
for (const row of rowsFromDb || []) { for (const row of rowsFromDb || []) {
dbValueMap.set(row.id, row) dbValueMap.set(row.id, row)
} }
return buildDefaultRows().map(row => { const defaultRows = await buildDefaultRows()
return defaultRows.map(row => {
const fromDb = dbValueMap.get(row.id) const fromDb = dbValueMap.get(row.id)
if (!fromDb) return row if (!fromDb) return row
// 使
return { return {
...row, ...row,
adoptedBudgetUnitPrice: //
typeof fromDb.adoptedBudgetUnitPrice === 'number' ? fromDb.adoptedBudgetUnitPrice : null, adoptedBudgetUnitPrice: typeof fromDb.adoptedBudgetUnitPrice === 'number' ? fromDb.adoptedBudgetUnitPrice : null,
personnelCount: typeof fromDb.personnelCount === 'number' ? fromDb.personnelCount : null, personnelCount: typeof fromDb.personnelCount === 'number' ? fromDb.personnelCount : null,
workdayCount: typeof fromDb.workdayCount === 'number' ? fromDb.workdayCount : null, workdayCount: typeof fromDb.workdayCount === 'number' ? fromDb.workdayCount : null,
serviceBudget: typeof fromDb.serviceBudget === 'number' ? fromDb.serviceBudget : null, serviceBudget: typeof fromDb.serviceBudget === 'number' ? fromDb.serviceBudget : null,
remark: typeof fromDb.remark === 'string' ? fromDb.remark : '' remark: typeof fromDb.remark === 'string' ? fromDb.remark : '',
//
unitPrice: typeof fromDb.unitPrice === 'number' ? fromDb.unitPrice : null,
workdayCount1: typeof fromDb.workdayCount1 === 'number' ? fromDb.workdayCount1 : null,
feeSubtotal: typeof fromDb.feeSubtotal === 'number' ? fromDb.feeSubtotal : null,
unitPrice2: typeof fromDb.unitPrice2 === 'number' ? fromDb.unitPrice2 : null,
workdayCount2: typeof fromDb.workdayCount2 === 'number' ? fromDb.workdayCount2 : null,
feeSubtotal2: typeof fromDb.feeSubtotal2 === 'number' ? fromDb.feeSubtotal2 : null,
unitPrice3: typeof fromDb.unitPrice3 === 'number' ? fromDb.unitPrice3 : null,
workdayCount3: typeof fromDb.workdayCount3 === 'number' ? fromDb.workdayCount3 : null,
feeSubtotal3: typeof fromDb.feeSubtotal3 === 'number' ? fromDb.feeSubtotal3 : null,
unitPrice4: typeof fromDb.unitPrice4 === 'number' ? fromDb.unitPrice4 : null,
workdayCount4: typeof fromDb.workdayCount4 === 'number' ? fromDb.workdayCount4 : null,
feeSubtotal4: typeof fromDb.feeSubtotal4 === 'number' ? fromDb.feeSubtotal4 : null,
unitPrice5: typeof fromDb.unitPrice5 === 'number' ? fromDb.unitPrice5 : null,
workdayCount5: typeof fromDb.workdayCount5 === 'number' ? fromDb.workdayCount5 : null,
feeSubtotal5: typeof fromDb.feeSubtotal5 === 'number' ? fromDb.feeSubtotal5 : null,
feeSubtotal6: typeof fromDb.feeSubtotal6 === 'number' ? fromDb.feeSubtotal6 : null,
avgUnitPrice: typeof fromDb.avgUnitPrice === 'number' ? fromDb.avgUnitPrice : null,
} }
}) })
} }
const parseNonNegativeIntegerOrNull = (value: unknown) => { const parseNonNegativeIntegerOrNull = (value: unknown) => {
if (value === '' || value == null) return null if (value === '' || value == null) return null
const parsed = parseNumberOrNull(value, { sanitize: true, precision: 0 }) const parsed = parseNumberOrNull(value, {sanitize: true, precision: 0})
if (parsed == null) return null if (parsed == null) return null
if (!Number.isSafeInteger(parsed) || parsed < 0) return null if (!Number.isSafeInteger(parsed) || parsed < 0) return null
return parsed return parsed
@ -298,7 +331,258 @@ const calcServiceBudget = (row: DetailRow | undefined) => {
const syncServiceBudgetToRows = () => { const syncServiceBudgetToRows = () => {
for (const row of detailRows.value) { for (const row of detailRows.value) {
row.serviceBudget = calcServiceBudget(row) // row.serviceBudget = calcServiceBudget(row)
// ×
row.feeSubtotal = Math.round((row.unitPrice ?? 0) * (row.workdayCount ?? 0))
row.feeSubtotal2 = Math.round((row.unitPrice2 ?? 0) * (row.workdayCount2 ?? 0))
row.feeSubtotal3 = Math.round((row.unitPrice3 ?? 0) * (row.workdayCount3 ?? 0))
row.feeSubtotal4 = Math.round((row.unitPrice4 ?? 0) * (row.workdayCount4 ?? 0))
row.feeSubtotal5 = Math.round((row.unitPrice5 ?? 0) * (row.workdayCount5 ?? 0))
//
row.workdayCount6 = (row.workdayCount ?? 0) +
(row.workdayCount2 ?? 0) +
(row.workdayCount3 ?? 0) +
(row.workdayCount4 ?? 0) +
(row.workdayCount5 ?? 0)
//
row.feeSubtotal6 = Math.round(
(row.workdayCount ?? 0) * (row.unitPrice ?? 0) +
(row.workdayCount2 ?? 0) * (row.unitPrice2 ?? 0) +
(row.workdayCount3 ?? 0) * (row.unitPrice3 ?? 0) +
(row.workdayCount4 ?? 0) * (row.unitPrice4 ?? 0) +
(row.workdayCount5 ?? 0) * (row.unitPrice5 ?? 0)
)
//
const totalWorkday = row.workdayCount6 ?? 0
const totalFee = row.feeSubtotal6 ?? 0
row.avgUnitPrice = Math.round(totalWorkday * totalFee)
}
}
function editableNumberCol2(
field: string,
headerName: string,
options: {
decimals?: 0 | 1; // 0=1=1
aggFunc?: string;
} = {}
): ColDef {
const {decimals = 0, aggFunc = 'sum'} = options;
/*const valueParser = (params: ValueParserParams) => {
const val = params.newValue?.trim();
if (!val) return null;
const num = parseFloat(val);
return isNaN(num) || num < 0 ? null : num;
};*/
const valueFormatter = (params: ValueFormatterParams) => {
if (params.node?.rowPinned === 'bottom') {
return '/';
}
const val = params.value;
if (val === null || val === undefined) return 0;
if (decimals === 0) {
return Math.round(val).toString(); //
} else {
return Number(val).toFixed(1); // 1
}
};
return {
field,
headerName,
editable: (params) => {
if (params.node?.rowPinned === 'bottom') {
return false;
} else {
return true;
}
},
cellDataType: 'number',
valueFormatter,
aggFunc,
};
}
function editableNumberCol3(
field: string,
headerName: string,
options: {
decimals?: 0 | 1; // 0=1=1
aggFunc?: string;
} = {}
): ColDef {
const {decimals = 0, aggFunc = 'sum'} = options;
const valueParser = (params: ValueParserParams) => {
const val = params.newValue?.trim();
if (!val) return null;
const num = parseFloat(val);
return isNaN(num) || num < 0 ? null : num;
};
const valueFormatter = (params: ValueFormatterParams) => {
const val = params.value;
if (val === null || val === undefined) return 0;
if (decimals === 0) {
return Math.round(val).toString(); //
} else {
return Number(val).toFixed(1); // 1
}
};
return {
field,
headerName,
editable: (params) => {
if (params.node?.rowPinned === 'bottom') {
return false;
} else {
return true;
}
},
cellDataType: 'number',
valueParser,
valueFormatter,
aggFunc,
};
}
//
function calculatedFeeSubtotalCol(
field: string,
headerName: string,
unitPriceField: string = 'unitPrice',
workdayCountField: string = 'workdayCount'
): ColDef {
return {
field,
headerName,
editable: false,
cellDataType: 'number',
valueGetter: (params) => {
const unitPrice = params.data?.[unitPriceField] ?? 0;
const workdayCount = params.data?.[workdayCountField] ?? 0;
const subtotal = unitPrice * workdayCount;
if (params.node?.rowPinned === 'bottom') {
//
// 使
const allRows = fallbackDetailRows.value;
const total = allRows.reduce((sum, row) => {
return sum + ((row[unitPriceField as keyof DetailRow] as number || 0) *
(row[workdayCountField as keyof DetailRow] as number || 0));
}, 0);
return Math.round(total); //
}
return Math.round(subtotal); //
},
/*valueFormatter: (params: ValueFormatterParams) => {
const unitPrice = params.data?.[unitPriceField] ?? 0;
const workdayCount = params.data?.[workdayCountField] ?? 0;
const subtotal = unitPrice * workdayCount;
return Math.round(subtotal); //
},*/
aggFunc: 'sum'
};
}
function calculatedFeeSubtotalCol2(
field: string,
headerName: string,
unitPriceField: string = 'unitPrice',
workdayCountField: string = 'workdayCount'
): ColDef {
return {
field,
headerName,
editable: false,
cellDataType: 'number',
valueGetter: (params) => {
const unitPrice = params.data?.[unitPriceField] ?? 0;
const workdayCount = params.data?.[workdayCountField] ?? 0;
const subtotal = unitPrice * workdayCount;
return Math.round(subtotal); //
},
valueFormatter: (params: ValueFormatterParams) => {
const allRows = fallbackDetailRows.value;
const total = allRows.reduce((sum, row) => {
return sum + ((row[unitPriceField as keyof DetailRow] as number || 0) *
(row[workdayCountField as keyof DetailRow] as number || 0));
}, 0);
return Math.round(total); //
},
aggFunc: 'sum'
};
}
function calculatedWorkdayTotalCol(
field: string,
headerName: string,
options: {
decimals?: 0 | 1; // 0=1=1
aggFunc?: string;
}): ColDef {
const {decimals = 0, aggFunc = 'sum'} = options;
return {
field,
headerName,
editable: false,
cellDataType: 'number',
valueGetter: (params) => {
const workdayCount1 = params.data?.['workdayCount'] ?? 0;
const workdayCount2 = params.data?.['workdayCount2'] ?? 0;
const workdayCount3 = params.data?.['workdayCount3'] ?? 0;
const workdayCount4 = params.data?.['workdayCount4'] ?? 0;
const workdayCount5 = params.data?.['workdayCount5'] ?? 0;
const count = workdayCount1 + workdayCount2 + workdayCount3 + workdayCount4 + workdayCount5;
return count; //
},
valueFormatter: (params: ValueFormatterParams) => {
const val = params.value;
if (val === null || val === undefined) return '';
if (decimals === 0) {
return Math.round(val).toString(); //
} else {
return Number(val).toFixed(1); // 1
}
}
}
}
function calculatedAvgUnitPriceCol(
field: string,
headerName: string,
options: {
decimals?: 0 | 1; // 0=1=1
aggFunc?: string;
}): ColDef {
const {decimals = 0, aggFunc = 'sum'} = options;
return {
field,
headerName,
editable: false,
cellDataType: 'number'
}
}
function calculatedSubTotalCol(
field: string,
headerName: string,
options: {
decimals?: 0 | 1; // 0=1=1
aggFunc?: string;
}): ColDef {
const {decimals = 0, aggFunc = 'sum'} = options;
return {
field,
headerName,
editable: false,
cellDataType: 'number'
} }
} }
@ -314,11 +598,11 @@ const editableNumberCol = <K extends keyof DetailRow>(
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params => !params.node?.group && !params.node?.rowPinned,
cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''), cellClass: params => (!params.node?.group && !params.node?.rowPinned ? 'editable-cell-line' : ''),
cellClassRules: { cellClassRules: {
'ag-right-aligned-cell':()=>true, 'ag-right-aligned-cell': () => true,
'editable-cell-empty': params => 'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
}, },
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueParser: params => parseNumberOrNull(params.newValue, {precision: 3}),
valueFormatter: formatEditableNumber, valueFormatter: formatEditableNumber,
...extra ...extra
}) })
@ -343,7 +627,7 @@ const editableMoneyCol = <K extends keyof DetailRow>(
'editable-cell-empty': params => 'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '') !params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')
}, },
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }), valueParser: params => parseNumberOrNull(params.newValue, {precision: 3}),
valueFormatter: params => { valueFormatter: params => {
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return t('hourlyFeeGrid.clickToInput') return t('hourlyFeeGrid.clickToInput')
@ -376,10 +660,7 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
width: 100, width: 100,
pinned: 'left', pinned: 'left',
colSpan: params => (params.node?.rowPinned ? 2 : 1), colSpan: params => (params.node?.rowPinned ? 2 : 1),
cellClassRules: { valueFormatter: params => (params.node?.rowPinned ? t('hourlyFeeGrid.total') : params.value || 0)
'ag-summary-label-cell': params => Boolean(params.node?.rowPinned)
},
valueFormatter: params => (params.node?.rowPinned ? t('hourlyFeeGrid.total') : params.value || '')
}, },
{ {
headerName: t('hourlyFeeGrid.columns.name'), headerName: t('hourlyFeeGrid.columns.name'),
@ -391,27 +672,87 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
wrapText: true, wrapText: true,
autoHeight: true, autoHeight: true,
cellClass: 'hourly-fee-name-cell', cellClass: 'hourly-fee-name-cell',
cellStyle: { whiteSpace: 'normal', lineHeight: '1.2' }, cellStyle: {whiteSpace: 'normal', lineHeight: '1.2'},
valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '') valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '')
}, },
{ {
headerName: t('hourlyFeeGrid.columns.referenceUnitPrice'), headerName: t('hourlyFeeGrid.columns.technician'),
marryChildren: true, marryChildren: true,
children: [ children: [
readonlyTextCol('laborBudgetUnitPrice', t('hourlyFeeGrid.columns.laborBudgetUnitPrice'), { // /
colSpan: params => (params.node?.rowPinned ? 3 : 1), editableNumberCol2('unitPrice', t('hourlyFeeGrid.columns.unitPrice'), {decimals: 0}),
valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '') // 1
}), editableNumberCol3('workdayCount', t('hourlyFeeGrid.columns.workdayCount'), {decimals: 1}),
readonlyTextCol('compositeBudgetUnitPrice', t('hourlyFeeGrid.columns.compositeBudgetUnitPrice')) //
calculatedFeeSubtotalCol('feeSubtotal', '费用小计(元)', 'unitPrice', 'workdayCount')
] ]
}, },
editableMoneyCol('adoptedBudgetUnitPrice', t('hourlyFeeGrid.columns.adoptedBudgetUnitPrice')), {
headerName: t('hourlyFeeGrid.columns.assistantEngineer'),
marryChildren: true,
children: [
// /
editableNumberCol2('unitPrice2', t('hourlyFeeGrid.columns.unitPrice'), {decimals: 0}),
// 1
editableNumberCol3('workdayCount2', t('hourlyFeeGrid.columns.workdayCount'), {decimals: 1}),
//
calculatedFeeSubtotalCol('feeSubtotal2', '费用小计(元)', 'unitPrice2', 'workdayCount2')
]
},
{
headerName: t('hourlyFeeGrid.columns.midEngineer'),
marryChildren: true,
children: [
// /
editableNumberCol2('unitPrice3', t('hourlyFeeGrid.columns.unitPrice'), {decimals: 0}),
// 1
editableNumberCol3('workdayCount3', t('hourlyFeeGrid.columns.workdayCount'), {decimals: 1}),
//
calculatedFeeSubtotalCol('feeSubtotal3', '费用小计(元)', 'unitPrice3', 'workdayCount3')
]
},
{
headerName: t('hourlyFeeGrid.columns.seniorEngineer'),
marryChildren: true,
children: [
// /
editableNumberCol2('unitPrice4', t('hourlyFeeGrid.columns.unitPrice'), {decimals: 0}),
// 1
editableNumberCol3('workdayCount4', t('hourlyFeeGrid.columns.workdayCount'), {decimals: 1}),
//
calculatedFeeSubtotalCol('feeSubtotal4', '费用小计(元)', 'unitPrice4', 'workdayCount4')
]
},
{
headerName: t('hourlyFeeGrid.columns.profSeniorEngineer'),
marryChildren: true,
children: [
// /
editableNumberCol2('unitPrice5', t('hourlyFeeGrid.columns.unitPrice'), {decimals: 0}),
// 1
editableNumberCol3('workdayCount5', t('hourlyFeeGrid.columns.workdayCount'), {decimals: 1}),
//
calculatedFeeSubtotalCol('feeSubtotal5', '费用小计(元)', 'unitPrice5', 'workdayCount5')
]
},
{
headerName: t('hourlyFeeGrid.columns.total'),
marryChildren: true,
children: [
/*editableNumberCol2('workdayCount6', t('hourlyFeeGrid.columns.workdayCount'), {decimals: 1}),
editableNumberCol2('subtotal6', t('hourlyFeeGrid.columns.subtotal'), {decimals: 0}),*/
calculatedWorkdayTotalCol('workdayCount6', t('hourlyFeeGrid.columns.workdayCount'), {decimals: 1}),
calculatedSubTotalCol('feeSubtotal6', t('hourlyFeeGrid.columns.subtotal'), {decimals: 0}),
calculatedAvgUnitPriceCol('avgUnitPrice', t('hourlyFeeGrid.columns.avgUnitPrice'), {decimals: 0})
]
},
/*editableMoneyCol('adoptedBudgetUnitPrice', t('hourlyFeeGrid.columns.adoptedBudgetUnitPrice')),
editableNumberCol('personnelCount', t('hourlyFeeGrid.columns.personnelCount'), { editableNumberCol('personnelCount', t('hourlyFeeGrid.columns.personnelCount'), {
aggFunc: decimalAggSum, aggFunc: decimalAggSum,
valueParser: params => parseNonNegativeIntegerOrNull(params.newValue), valueParser: params => parseNonNegativeIntegerOrNull(params.newValue),
valueFormatter: formatEditableInteger valueFormatter: formatEditableInteger
}), }),
editableNumberCol('workdayCount', t('hourlyFeeGrid.columns.workdayCount'), { aggFunc: decimalAggSum }), editableNumberCol('workdayCount', t('hourlyFeeGrid.columns.workdayCount'), {aggFunc: decimalAggSum}),
{ {
headerName: t('hourlyFeeGrid.columns.serviceBudget'), headerName: t('hourlyFeeGrid.columns.serviceBudget'),
field: 'serviceBudget', field: 'serviceBudget',
@ -428,7 +769,7 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
if (params.value == null || params.value === '') return '' if (params.value == null || params.value === '') return ''
return formatThousandsFlexible(params.value, 3) return formatThousandsFlexible(params.value, 3)
} }
}, },*/
{ {
headerName: t('hourlyFeeGrid.columns.remark'), headerName: t('hourlyFeeGrid.columns.remark'),
field: 'remark', field: 'remark',
@ -437,7 +778,7 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
cellEditor: 'agLargeTextCellEditor', cellEditor: 'agLargeTextCellEditor',
wrapText: true, wrapText: true,
autoHeight: true, autoHeight: true,
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' }, cellStyle: {whiteSpace: 'normal', lineHeight: '1.4'},
editable: params => !params.node?.group && !params.node?.rowPinned, editable: params => !params.node?.group && !params.node?.rowPinned,
valueFormatter: params => { valueFormatter: params => {
if (!params.node?.group && !params.node?.rowPinned && !params.value) return t('hourlyFeeGrid.clickToInput') if (!params.node?.group && !params.node?.rowPinned && !params.value) return t('hourlyFeeGrid.clickToInput')
@ -452,10 +793,30 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
] ]
const gridColumnDefs = computed(() => withReadonlyAutoHeight(columnDefs)) const gridColumnDefs = computed(() => withReadonlyAutoHeight(columnDefs))
const totalPersonnelCount = computed(() => sumByNumber(detailRows.value, row => row.personnelCount))
const totalWorkdayCount = computed(() => sumByNumber(detailRows.value, row => row.workdayCount)) const totalWorkdayCount = computed(() => sumByNumber(detailRows.value, row => row.workdayCount))
const totalWorkdayCount2 = computed(() => sumByNumber(detailRows.value, row => row.workdayCount2))
const totalWorkdayCount3 = computed(() => sumByNumber(detailRows.value, row => row.workdayCount3))
const totalWorkdayCount4 = computed(() => sumByNumber(detailRows.value, row => row.workdayCount4))
const totalWorkdayCount5 = computed(() => sumByNumber(detailRows.value, row => row.workdayCount5))
const totalFeeSubtotal = computed(() => {
return sumByNumber(detailRows.value, row => row.feeSubtotal)
})
const totalFeeSubtotal2 = computed(() => sumByNumber(detailRows.value, row => row.feeSubtotal2))
const totalFeeSubtotal3 = computed(() => sumByNumber(detailRows.value, row => row.feeSubtotal3))
const totalFeeSubtotal4 = computed(() => sumByNumber(detailRows.value, row => row.feeSubtotal4))
const totalFeeSubtotal5 = computed(() => sumByNumber(detailRows.value, row => row.feeSubtotal5))
const totalFeeSubtotal6 = computed(() => {
const allRows = fallbackDetailRows.value;
return allRows.reduce((sum, row) => {
const value = Number(row?.feeSubtotal6) || 0
return sum + value
}, 0);
})
const totalAvgUnitPrice = computed(() => sumByNumber(detailRows.value, row => row.avgUnitPrice))
const totalPersonnelCount = computed(() => sumByNumber(detailRows.value, row => row.personnelCount))
const totalServiceBudget = computed(() => sumNullableNumbers(detailRows.value.map(row => calcServiceBudget(row)))) const totalServiceBudget = computed(() => sumNullableNumbers(detailRows.value.map(row => calcServiceBudget(row))))
const pinnedTopRowData = computed(() => [ const pinnedTopRowData = computed(() => {
const result = [
{ {
id: 'pinned-total-row', id: 'pinned-total-row',
expertCode: t('hourlyFeeGrid.total'), expertCode: t('hourlyFeeGrid.total'),
@ -463,84 +824,110 @@ const pinnedTopRowData = computed(() => [
laborBudgetUnitPrice: '', laborBudgetUnitPrice: '',
compositeBudgetUnitPrice: '', compositeBudgetUnitPrice: '',
adoptedBudgetUnitPrice: null, adoptedBudgetUnitPrice: null,
personnelCount: totalPersonnelCount.value, // personnelCount: totalPersonnelCount.value,
unitPrice: '/',
workdayCount: totalWorkdayCount.value, workdayCount: totalWorkdayCount.value,
workdayCount2: totalWorkdayCount2.value,
workdayCount3: totalWorkdayCount3.value,
workdayCount4: totalWorkdayCount4.value,
workdayCount5: totalWorkdayCount5.value,
avgUnitPrice: totalAvgUnitPrice.value,
feeSubtotal2: totalFeeSubtotal2.value,
feeSubtotal3: totalFeeSubtotal3.value,
feeSubtotal4: totalFeeSubtotal4.value,
feeSubtotal5: totalFeeSubtotal5.value,
feeSubtotal6: totalFeeSubtotal6.value,
serviceBudget: totalServiceBudget.value, serviceBudget: totalServiceBudget.value,
remark: '', remark: '',
path: ['TOTAL'] path: ['TOTAL']
} }
]) ]
const saveToIndexedDB = async () => { return result
if (shouldSkipPersist()) return })
try {
syncServiceBudgetToRows()
const payload: GridState = {
detailRows: JSON.parse(JSON.stringify(detailRows.value))
}
if (useServicePricingState.value && serviceMethod.value) { /**
zxFwPricingStore.setServicePricingMethodState( * 保存数据到 IndexedDB
props.contractId!, * 保存时机
props.serviceId!, * 1. 单元格值变化时handleCellValueChanged
serviceMethod.value, * 2. 组件失活时onDeactivated- 切换页面
payload, * 3. 组件卸载前onBeforeUnmount- 关闭页面
{ force: true } */
)
} else if (useHtMethodState.value) {
zxFwPricingStore.setHtFeeMethodState(
props.htMainStorageKey!,
props.htRowId!,
props.htMethodType!,
payload,
{ force: true }
)
} else {
zxFwPricingStore.setKeyState(props.storageKey, payload)
}
if (props.enableZxFwSync && props.contractId && props.serviceId != null) { const recalculateServiceFees = () => {
const synced = await syncPricingTotalToZxFw({ for (const row of fallbackDetailRows.value) {
contractId: props.contractId, row.workdayCount6 = row.workdayCount + row.workdayCount2 + row.workdayCount3 + row.workdayCount4 + row.workdayCount5
serviceId: props.serviceId, row.feeSubtotal6 = row.feeSubtotal + row.feeSubtotal2 + row.feeSubtotal3 + row.feeSubtotal4 + row.feeSubtotal5
field: props.syncField, row.avgUnitPrice = row.feeSubtotal6 * row.workdayCount6
value: totalServiceBudget.value
})
if (!synced) return
}
} catch (error) {
console.error('saveToIndexedDB failed:', error)
} }
} }
const saveToIndexedDB = async () => {
try {
// syncServiceBudgetToRows()
syncServiceBudgetToRows();
// 使 upsertBatch
const rows = detailRows.value.map(row => ({ ...row }))
const stats = await useDataStore().upsertBatch(rows)
console.log('💾 数据保存成功:', stats)
} catch (error) {
console.error('❌ saveToIndexedDB 失败:', error)
}
}
/**
* IndexedDB 加载数据
* 加载时机
* 1. 组件挂载时onMounted
* 2. 组件激活时onActivated- 切换回页面
* 3. storageKey 变化时watch
*/
const loadFromIndexedDB = async () => { const loadFromIndexedDB = async () => {
try { try {
if (shouldForceDefaultLoad()) {
detailRows.value = buildDefaultRows() // type 'hourly'
syncServiceBudgetToRows() const hourlyRows = await useDataStore().query([
return { field: 'type', value: `${props.contractId}-hourly`, operator: 'eq' }
} ])
const data = useServicePricingState.value && serviceMethod.value // DataItem[] DetailRow[]
? await zxFwPricingStore.loadServicePricingMethodState<DetailRow>(props.contractId!, props.serviceId!, serviceMethod.value) const detailRowsFromStore: DetailRow[] = hourlyRows.map(row => ({
: useHtMethodState.value id: String(row.id || ''),
? await zxFwPricingStore.loadHtFeeMethodState<GridState>( expertCode: String(row.expertCode || ''),
props.htMainStorageKey!, expertName: String(row.expertName || ''),
props.htRowId!, laborBudgetUnitPrice: String(row.laborBudgetUnitPrice || ''),
props.htMethodType! compositeBudgetUnitPrice: String(row.compositeBudgetUnitPrice || ''),
) adoptedBudgetUnitPrice: row.adoptedBudgetUnitPrice != null ? Number(row.adoptedBudgetUnitPrice) : null,
: await zxFwPricingStore.loadKeyState<GridState>(props.storageKey) personnelCount: row.personnelCount != null ? Number(row.personnelCount) : null,
if (data) { workdayCount: row.workdayCount != null ? Number(row.workdayCount) : null,
detailRows.value = mergeWithDictRows(data.detailRows) serviceBudget: row.serviceBudget != null ? Number(row.serviceBudget) : null,
syncServiceBudgetToRows() remark: String(row.remark || ''),
return path: Array.isArray(row.path) ? row.path : [],
} unitPrice: row.unitPrice != null ? Number(row.unitPrice) : null,
detailRows.value = buildDefaultRows() workdayCount1: row.workdayCount1 != null ? Number(row.workdayCount1) : null,
syncServiceBudgetToRows() feeSubtotal: row.feeSubtotal != null ? Number(row.feeSubtotal) : null,
unitPrice2: row.unitPrice2 != null ? Number(row.unitPrice2) : null,
workdayCount2: row.workdayCount2 != null ? Number(row.workdayCount2) : null,
feeSubtotal2: row.feeSubtotal2 != null ? Number(row.feeSubtotal2) : null,
unitPrice3: row.unitPrice3 != null ? Number(row.unitPrice3) : null,
workdayCount3: row.workdayCount3 != null ? Number(row.workdayCount3) : null,
feeSubtotal3: row.feeSubtotal3 != null ? Number(row.feeSubtotal3) : null,
unitPrice4: row.unitPrice4 != null ? Number(row.unitPrice4) : null,
workdayCount4: row.workdayCount4 != null ? Number(row.workdayCount4) : null,
feeSubtotal4: row.feeSubtotal4 != null ? Number(row.feeSubtotal4) : null,
unitPrice5: row.unitPrice5 != null ? Number(row.unitPrice5) : null,
workdayCount5: row.workdayCount5 != null ? Number(row.workdayCount5) : null,
feeSubtotal5: row.feeSubtotal5 != null ? Number(row.feeSubtotal5) : null,
workdayCount6: row.workdayCount6 != null ? Number(row.workdayCount6) : null,
feeSubtotal6: row.feeSubtotal6 != null ? Number(row.feeSubtotal6) : null,
avgUnitPrice: row.avgUnitPrice != null ? Number(row.avgUnitPrice) : null
}))
fallbackDetailRows.value = await mergeWithDictRows(detailRowsFromStore)
console.log('✅ 转换后的 DetailRow 数据:', detailRowsFromStore)
} catch (error) { } catch (error) {
console.error('loadFromIndexedDB failed:', error) detailRows.value = await buildDefaultRows()
detailRows.value = buildDefaultRows()
syncServiceBudgetToRows()
} }
} }
@ -560,23 +947,36 @@ const relabelRowsFromExpertDict = async () => {
expertName: nextName expertName: nextName
} }
}) })
gridApi.value?.refreshCells({ force: true }) gridApi.value?.refreshCells({force: true})
if (!changed) return if (!changed) return
await saveToIndexedDB() await saveToIndexedDB()
} }
let isBulkClipboardMutation = false let isBulkClipboardMutation = false
/**
* 提交网格变更同步计算值刷新UI保存到数据库
* @param source 变更来源用于日志追踪
*/
const commitGridChanges = (source: string) => { const commitGridChanges = (source: string) => {
console.log('🔄 提交网格变更:', source)
syncServiceBudgetToRows() syncServiceBudgetToRows()
gridApi.value?.refreshCells({ force: true }) gridApi.value?.refreshCells({force: true})
scheduleAutoRowHeights() scheduleAutoRowHeights()
void saveToIndexedDB() void saveToIndexedDB()
} }
/**
* 处理单元格值变化事件
* 触发时机用户编辑完单元格后
*/
const handleCellValueChanged = (event?: any) => { const handleCellValueChanged = (event?: any) => {
if (isBulkClipboardMutation) return /*if (isBulkClipboardMutation) {
commitGridChanges('cell-value-changed') return
}
commitGridChanges('cell-value-changed')*/
saveToIndexedDB()
} }
const handleBulkMutationStart = () => { const handleBulkMutationStart = () => {
@ -584,8 +984,8 @@ const handleBulkMutationStart = () => {
} }
const handleBulkMutationEnd = (event?: any) => { const handleBulkMutationEnd = (event?: any) => {
isBulkClipboardMutation = false // isBulkClipboardMutation = false
commitGridChanges(event?.type || 'bulk-end') // commitGridChanges(event?.type || 'bulk-end')
} }
const handleGridReady = (event: GridReadyEvent<DetailRow>) => { const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
@ -602,7 +1002,7 @@ const forceRefreshCellsOnLiveApi = () => {
setTimeout(() => { setTimeout(() => {
const liveApi = gridApi.value const liveApi = gridApi.value
if (!isGridApiAlive(liveApi)) return if (!isGridApiAlive(liveApi)) return
liveApi.refreshCells({ force: true }) liveApi.refreshCells({force: true})
liveApi.redrawRows() liveApi.redrawRows()
}, 16) }, 16)
} }
@ -612,7 +1012,7 @@ const syncAutoRowHeights = async () => {
const api = gridApi.value const api = gridApi.value
if (!isGridApiAlive(api)) return if (!isGridApiAlive(api)) return
api.onRowHeightChanged() api.onRowHeightChanged()
api.refreshCells({ force: true }) api.refreshCells({force: true})
api.redrawRows() api.redrawRows()
forceRefreshCellsOnLiveApi() forceRefreshCellsOnLiveApi()
} }
@ -652,7 +1052,7 @@ const processCellFromClipboard = (params: any) => {
return parseNonNegativeIntegerOrNull(params.value) return parseNonNegativeIntegerOrNull(params.value)
} }
if (field === 'adoptedBudgetUnitPrice' || field === 'workdayCount') { if (field === 'adoptedBudgetUnitPrice' || field === 'workdayCount') {
return parseNumberOrNull(params.value, { precision: 3 }) return parseNumberOrNull(params.value, {precision: 3})
} }
try { try {
const parsed = JSON.parse(params.value) const parsed = JSON.parse(params.value)
@ -663,16 +1063,34 @@ const processCellFromClipboard = (params: any) => {
return params.value return params.value
} }
/**
* 组件挂载时加载数据
*/
onMounted(async () => { onMounted(async () => {
console.log('🚀 组件挂载')
await loadFromIndexedDB() await loadFromIndexedDB()
scheduleAutoRowHeights() scheduleAutoRowHeights()
}) })
/**
* 组件激活时重新加载数据从其他页面切换回来时
*/
onActivated(async () => { onActivated(async () => {
console.log('🔙 组件激活')
await loadFromIndexedDB() await loadFromIndexedDB()
scheduleAutoRowHeights() scheduleAutoRowHeights()
}) })
watch(
() => useDataStore().items,
async (newItems) => {
if (Object.keys(newItems).length > 0) {
loadFromIndexedDB();
}
},
{ immediate: true, deep: true }
)
watch( watch(
() => props.storageKey, () => props.storageKey,
() => { () => {
@ -695,12 +1113,21 @@ watch(
} }
) )
/**
* 组件失活时保存数据切换页面时触发
* 配合 Vue <keep-alive> 使用
*/
onDeactivated(() => { onDeactivated(() => {
console.log('🔄 组件失活,保存数据')
gridApi.value?.stopEditing() gridApi.value?.stopEditing()
void saveToIndexedDB() void saveToIndexedDB()
}) })
/**
* 组件卸载前保存数据关闭页面时触发
*/
onBeforeUnmount(() => { onBeforeUnmount(() => {
console.log('💔 组件卸载,保存数据')
gridApi.value?.stopEditing() gridApi.value?.stopEditing()
gridApi.value = null gridApi.value = null
if (autoHeightSyncTimer) { if (autoHeightSyncTimer) {
@ -723,7 +1150,7 @@ onBeforeUnmount(() => {
<AgGridVue <AgGridVue
:style="agGridStyle" :style="agGridStyle"
:rowData="detailRows" :rowData="detailRows"
:pinnedTopRowData="pinnedTopRowData" :pinnedBottomRowData="pinnedTopRowData"
:columnDefs="gridColumnDefs" :columnDefs="gridColumnDefs"
:gridOptions="gridOptions" :gridOptions="gridOptions"
:theme="myTheme" :theme="myTheme"

View File

@ -279,9 +279,6 @@ const columnDefs: ColDef<FeeRow>[] = [
: typeof params.node?.rowIndex === 'number' : typeof params.node?.rowIndex === 'number'
? params.node.rowIndex + 1 ? params.node.rowIndex + 1
: '', : '',
cellClassRules: {
'ag-summary-label-cell': params => isSubtotalRow(params.data)
},
colSpan: params => (isSubtotalRow(params.data) ? 2 : 1) colSpan: params => (isSubtotalRow(params.data) ? 2 : 1)
}, },
{ {

View File

@ -375,12 +375,6 @@ const saveToIndexedDB = async (force = false) => {
const snapshot = JSON.stringify(payload.detailRows) const snapshot = JSON.stringify(payload.detailRows)
if (!force && snapshot === lastSavedSnapshot.value) return if (!force && snapshot === lastSavedSnapshot.value) return
zxFwPricingStore.setHtFeeMainState(props.storageKey, payload, { force }) zxFwPricingStore.setHtFeeMainState(props.storageKey, payload, { force })
if (String(props.storageKey || '').includes('-additional-work')) {
const contractId = String(props.contractId || '').trim()
if (contractId) {
await zxFwPricingStore.syncHtExtraFeeByContractBase(contractId)
}
}
lastSavedSnapshot.value = snapshot lastSavedSnapshot.value = snapshot
} catch (error) { } catch (error) {
console.error('saveToIndexedDB failed:', error) console.error('saveToIndexedDB failed:', error)
@ -504,7 +498,6 @@ const columnDefs: ColDef<FeeMethodRow>[] = [
? '' ? ''
: 'editable-cell-line', : 'editable-cell-line',
cellClassRules: { cellClassRules: {
'ag-summary-label-cell': params => isSummaryRow(params.data),
'editable-cell-empty': params => params.value == null || params.value === '' 'editable-cell-empty': params => params.value == null || params.value === ''
} }
}, },

View File

@ -1,5 +1,6 @@
<!--
<script setup lang="ts"> <script setup lang="ts">
import { computed, watch } from 'vue' import { computed } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
interface ServiceItem { interface ServiceItem {
@ -10,7 +11,6 @@ interface ServiceItem {
const props = defineProps<{ const props = defineProps<{
services: ServiceItem[] services: ServiceItem[]
serviceRows?: string[][]
modelValue: string[] modelValue: string[]
}>() }>()
@ -19,67 +19,68 @@ const emit = defineEmits<{
}>() }>()
const { t } = useI18n() const { t } = useI18n()
/** 互斥规则配置 */
const MUTUAL_EXCLUSION_RULES: Record<string, string[]> = {
'D1': ['D2-1', 'D2-2', 'D3-1', 'D3-2', 'D3-3', 'D3-4', 'D3-6-1', 'D3-7'],
'D2-1': ['D1', 'D2-2', 'D3-4', 'D3-6-1', 'D3-7'],
'D2-2': ['D2-1', 'D3-1', 'D3-2', 'D3-3']
}
/** 当前已选服务的 code 集合 */
const selectedCodes = computed(() => {
return props.services
.filter(s => selectedSet.value.has(s.id))
.map(s => s.code)
})
/** 获取某服务被哪些服务互斥 */
const getBlockedBy = (targetCode: string): string[] => {
const blockers: string[] = []
for (const [blockerCode, blockedCodes] of Object.entries(MUTUAL_EXCLUSION_RULES)) {
if (selectedCodes.value.includes(blockerCode) && blockedCodes.includes(targetCode)) {
blockers.push(blockerCode)
}
}
return blockers
}
/** 判断某服务是否被禁用 */
const isServiceDisabled = (item: ServiceItem): { disabled: boolean; reason: string } => {
const blockers = getBlockedBy(item.code)
if (blockers.length > 0) {
const blockerServices = blockers.map(bc => {
const s = props.services.find(s => s.code === bc)
return s ? `${s.code}${s.name}` : bc
})
return { disabled: true, reason: `与已选的${blockerServices.join('、')}互斥` }
}
return { disabled: false, reason: '' }
}
const selectedSet = computed(() => new Set(props.modelValue)) const selectedSet = computed(() => new Set(props.modelValue))
const serviceById = computed(() => new Map(props.services.map(item => [item.id, item])))
const firstRowIds = computed(() => {
const rows = Array.isArray(props.serviceRows) ? props.serviceRows : []
if (rows.length === 0) return [] as string[]
const firstRow = Array.isArray(rows[0]) ? rows[0] : []
return firstRow.filter(id => serviceById.value.has(id))
})
const firstRowIdSet = computed(() => new Set(firstRowIds.value))
const groupedRows = computed(() => {
const rows = Array.isArray(props.serviceRows) ? props.serviceRows : []
if (rows.length === 0) return [] as ServiceItem[][]
const used = new Set<string>() const toggleService = (item: ServiceItem, checked: boolean) => {
const grouped = rows const { disabled } = isServiceDisabled(item)
.map(row => row.map(id => serviceById.value.get(id)).filter((item): item is ServiceItem => Boolean(item))) //
.map(row => row.filter(item => { if (checked && disabled) return
if (used.has(item.id)) return false
used.add(item.id)
return true
}))
.filter(row => row.length > 0)
const leftovers = props.services.filter(item => !used.has(item.id))
if (leftovers.length > 0) grouped.push(leftovers)
return grouped
})
const toggleService = (id: string, checked: boolean) => {
const next = new Set(props.modelValue) const next = new Set(props.modelValue)
if (checked) { if (checked) {
if (firstRowIdSet.value.has(id)) { next.add(item.id)
firstRowIds.value.forEach(firstId => next.delete(firstId)) //
const blockedCodes = MUTUAL_EXCLUSION_RULES[item.code] || []
for (const blockedCode of blockedCodes) {
const blockedService = props.services.find(s => s.code === blockedCode)
if (blockedService) {
next.delete(blockedService.id)
}
} }
next.add(id)
} else { } else {
next.delete(id) next.delete(item.id)
} }
emit('update:modelValue', props.services.map(item => item.id).filter(itemId => next.has(itemId))) emit('update:modelValue', props.services.map(s => s.id).filter(id => next.has(id)))
} }
const isFirstRowDisabled = (id: string) => {
if (!firstRowIdSet.value.has(id)) return false
for (const selectedId of props.modelValue) {
if (selectedId !== id && firstRowIdSet.value.has(selectedId)) return true
}
return false
}
watch(
() => [props.modelValue, firstRowIds.value] as const,
() => {
const firstSelected = props.modelValue.filter(id => firstRowIdSet.value.has(id))
if (firstSelected.length <= 1) return
const keepId = firstRowIds.value.find(id => firstSelected.includes(id)) || firstSelected[0]
const next = props.modelValue.filter(id => !firstRowIdSet.value.has(id) || id === keepId)
emit('update:modelValue', next)
},
{ immediate: true, deep: true }
)
const clearAll = () => { const clearAll = () => {
emit('update:modelValue', []) emit('update:modelValue', [])
} }
@ -88,10 +89,7 @@ const clearAll = () => {
<template> <template>
<div class="rounded-lg border bg-card p-2.5 shadow-sm"> <div class="rounded-lg border bg-card p-2.5 shadow-sm">
<div class="mb-1 flex items-center justify-between gap-2"> <div class="mb-1 flex items-center justify-between gap-2">
<div class="flex min-w-0 items-center gap-1.5"> <label class="block text-[11px] font-medium text-foreground leading-none">{{ t('serviceSelector.title') }}</label>
<label class="block shrink-0 text-sm font-semibold leading-none text-slate-900">{{ t('serviceSelector.title') }}</label>
<span class="min-w-0 text-xs leading-5 text-muted-foreground">{{ t('serviceSelector.titleHint') }}</span>
</div>
<button <button
type="button" type="button"
class="cursor-pointer h-6 rounded-md border px-2 text-[13px] text-muted-foreground transition hover:bg-accent" class="cursor-pointer h-6 rounded-md border px-2 text-[13px] text-muted-foreground transition hover:bg-accent"
@ -101,37 +99,102 @@ const clearAll = () => {
</button> </button>
</div> </div>
<div class="rounded-md border p-1.5"> <div class="rounded-md border p-1.5">
<div v-if="groupedRows.length > 0" class="flex flex-col gap-1.5"> <div class="flex flex-wrap items-start gap-1">
<div
v-for="(row, rowIndex) in groupedRows"
:key="`service-row-${rowIndex}`"
class="flex flex-wrap items-start gap-1 border-b border-slate-200 pb-1.5 last:border-b-0 last:pb-0"
>
<label <label
v-for="item in row" v-for="item in props.services"
:key="item.id" :key="item.id"
:class="[ :class="[
'inline-flex w-fit max-w-full items-start gap-1.5 rounded-md border px-2 py-1 text-[11px] leading-4 transition', 'inline-flex w-fit max-w-full cursor-pointer items-start gap-1.5 rounded-md border px-2 py-1 text-[11px] leading-4',
isFirstRowDisabled(item.id) isServiceDisabled(item).disabled
? 'cursor-not-allowed border-slate-300 bg-slate-100/80 text-slate-400 opacity-80' ? 'opacity-50 cursor-not-allowed hover:bg-transparent'
: 'cursor-pointer hover:bg-muted/60' : 'hover:bg-muted/60'
]" ]"
:title="isServiceDisabled(item).reason"
> >
<input <input
type="checkbox" type="checkbox"
:class="[ class="mt-0.5"
'mt-0.5',
isFirstRowDisabled(item.id) ? 'cursor-not-allowed accent-slate-300' : 'cursor-pointer'
]"
:checked="selectedSet.has(item.id)" :checked="selectedSet.has(item.id)"
:disabled="isFirstRowDisabled(item.id)" :disabled="isServiceDisabled(item).disabled"
@change="toggleService(item.id, ($event.target as HTMLInputElement).checked)" @change="toggleService(item, ($event.target as HTMLInputElement).checked)"
/>
<span class="text-muted-foreground shrink-0">{{ item.code }}</span>
<span class="text-foreground break-words">{{ item.name }}</span>
</label>
</div>
<div v-if="props.services.length === 0" class="px-2 py-4 text-center text-xs text-muted-foreground">
{{ t('serviceSelector.empty') }}
</div>
</div>
</div>
</template>
-->
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
interface ServiceItem {
id: string
code: string
name: string
}
const props = defineProps<{
services: ServiceItem[]
modelValue: string[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string[]): void
}>()
const { t } = useI18n()
const selectedSet = computed(() => new Set(props.modelValue))
const toggleService = (item: ServiceItem, checked: boolean) => {
const next = new Set(props.modelValue)
if (checked) {
next.add(item.id)
} else {
next.delete(item.id)
}
emit('update:modelValue', props.services.map(s => s.id).filter(id => next.has(id)))
}
const clearAll = () => {
emit('update:modelValue', [])
}
</script>
<template>
<div class="rounded-lg border bg-card p-2.5 shadow-sm">
<div class="mb-1 flex items-center justify-between gap-2">
<label class="block text-[11px] font-medium text-foreground leading-none">{{ t('serviceSelector.title') }}</label>
<button
type="button"
class="cursor-pointer h-6 rounded-md border px-2 text-[13px] text-muted-foreground transition hover:bg-accent"
@click="clearAll"
>
{{ t('serviceSelector.clear') }}
</button>
</div>
<div class="rounded-md border p-1.5">
<div class="flex flex-wrap items-start gap-1">
<label
v-for="item in props.services"
:key="item.id"
class="inline-flex w-fit max-w-full cursor-pointer items-start gap-1.5 rounded-md border px-2 py-1 text-[11px] leading-4 hover:bg-muted/60"
>
<input
type="checkbox"
class="mt-0.5"
:checked="selectedSet.has(item.id)"
@change="toggleService(item, ($event.target as HTMLInputElement).checked)"
/> />
<span class="text-muted-foreground shrink-0">{{ item.code }}</span> <span class="text-muted-foreground shrink-0">{{ item.code }}</span>
<span class="text-foreground break-words">{{ item.name }}</span> <span class="text-foreground break-words">{{ item.name }}</span>
</label> </label>
</div> </div>
</div>
<div v-if="props.services.length === 0" class="px-2 py-4 text-center text-xs text-muted-foreground"> <div v-if="props.services.length === 0" class="px-2 py-4 text-center text-xs text-muted-foreground">
{{ t('serviceSelector.empty') }} {{ t('serviceSelector.empty') }}
</div> </div>

View File

@ -31,6 +31,7 @@ import { getServiceDictItemById, getWorkListEntries, wholeProcessTasks } from '@
import { WorkType } from '@/sql' import { WorkType } from '@/sql'
import { useZxFwPricingStore } from '@/pinia/zxFwPricing' import { useZxFwPricingStore } from '@/pinia/zxFwPricing'
import { useKvStore } from '@/pinia/kv' import { useKvStore } from '@/pinia/kv'
import { useDataStore } from '@/pinia/zx'
import { Trash2 } from 'lucide-vue-next' import { Trash2 } from 'lucide-vue-next'
interface WorkContentRow { interface WorkContentRow {
@ -149,16 +150,131 @@ const loadProjectIndustryId = async () => {
const buildDefaultRowsFromDict = async (): Promise<WorkContentRow[]> => { const buildDefaultRowsFromDict = async (): Promise<WorkContentRow[]> => {
const rows: WorkContentRow[] = [] const rows: WorkContentRow[] = []
const entries = getWorkListEntries(locale.value) as Array<{ text: string; serviceid: number; order: number; type: number }> const contentRows = await useDataStore().query([
{ field: 'type', value: `${props.contractId}-content`, operator: 'eq' }
])
let contentArray = [];
if (contentRows.length > 0) {
contentArray = contentRows[0].value;
}
const entries = getWorkListEntries(locale.value) as Array<{
text: string
serviceid: number
order: number
type: number
code: string
leaf?: boolean
textEn?: string
checked: boolean
custom: boolean
remark: string
}>;
let filtered: typeof entries = [] // code contentArray entries
const contentMap = new Map<string, string>()
if (Array.isArray(contentArray)) {
for (const item of contentArray) {
if (item?.code != null) {
contentMap.set(`${item.code}-${item.content}`, item)
}
}
}
for (let entry of entries) {
const item = contentMap.get(`${entry.code}-${entry.text}`)
if (item != null && `${entry.code}-${entry.text}` == `${item.code}-${item.content}`) {
entry.checked = item.checked
entry.custom = item.custom
entry.remark = item.remark
}
}
//
let filtered = [...entries]
// order
filtered.sort((a, b) => a.order - b.order)
// code ->
const parentMap = new Map<string, { serviceGroup: string; content: string }>()
// leaf: false
let i = 1;
for (const entry of filtered) {
if (entry.leaf === false) {
/*const serviceItem = getServiceDictItemById(entry.serviceid) as { code?: string; name?: string } | undefined
const serviceGroup = serviceItem
? `${String(serviceItem.code || '').trim()} ${String(serviceItem.name || '').trim()}`.trim()
: entry.code*/
parentMap.set(entry.code, {
serviceGroup: `${entry.text}`,
content: String(entry.text || '').trim()
})
i++;
}
}
// leaf: true
for (const entry of filtered) {
// leaf: true
if (entry.leaf !== true) {
continue
}
const content = String(entry.text || '').trim()
if (!content) continue
const typeLabel = ((): WorkType => {
if (entry.type === 1) return t('workContent.type.optional') as WorkType
if (entry.type === 2) return t('workContent.type.daily') as WorkType
if (entry.type === 3) return t('workContent.type.special') as WorkType
if (entry.type === 4) return t('workContent.type.additional') as WorkType
return t('workContent.type.basic') as WorkType
})()
/*const serviceItem = getServiceDictItemById(entry.serviceid) as { code?: string; name?: string } | undefined
const serviceGroup = serviceItem
? `${String(serviceItem.code || '').trim()} ${String(serviceItem.name || '').trim()}`.trim()
: entry.code*/
//
const parent = parentMap.get(entry.code)
const parentName = parent?.serviceGroup
// path: [, ]
const path = [parentName, content]
rows.push({
id: `dict-${entry.serviceid}-${entry.code}-${entry.order}` ||
`dict-${entry.serviceid}-${entry.content.substring(0, 8)}-${entry.order}`,
code: entry.code,
content,
type: typeLabel,
dictOrder: entry.order,
serviceGroup: parentName, // serviceGroup
serviceid: toServiceId(entry.serviceid),
remark: entry.remark == null ? '' : entry.remark,
checked: entry.checked == null ? true : entry.checked,
custom: entry.custom == null ? false : entry.custom,
leaf: entry.leaf,
path // 使 [, ]
})
}
return rows
}
const buildDefaultRowsFromDict2 = async (): Promise<WorkContentRow[]> => {
const rows: WorkContentRow[] = []
const entries = getWorkListEntries(locale.value) as Array<{ text: string; serviceid: number; order: number; type: number }>
const filtered = [...entries]
/*let filtered: typeof entries = []
let groupedServiceIds: number[] = [] let groupedServiceIds: number[] = []
let groupedBy: 'fid' | 'sid' | null = null let groupedBy: 'fid' | 'sid' | null = null
let matchedWholeProcessGroup: { fid: number; industry: number; sid: number[] } | null = null let matchedWholeProcessGroup: { fid: number; industry: number; sid: number[] } | null = null
isWholeProcessGroupedMode.value = false isWholeProcessGroupedMode.value = false
groupedServiceGroups.value = [] groupedServiceGroups.value = []
if (props.dictMode === 'service') { if (props.dictMode === 'service') {
const sid = Number(props.serviceId) const sid = Number(props.serviceId.split('-').pop())
const industryId = await loadProjectIndustryId() const industryId = await loadProjectIndustryId()
const wholeProcessGroupByFid = wholeProcessTasks.find( const wholeProcessGroupByFid = wholeProcessTasks.find(
item => Number(item.fid) === sid && Number(item.industry) === industryId item => Number(item.fid) === sid && Number(item.industry) === industryId
@ -204,7 +320,7 @@ const buildDefaultRowsFromDict = async (): Promise<WorkContentRow[]> => {
}) })
} else { } else {
filtered.sort((a, b) => a.order - b.order) filtered.sort((a, b) => a.order - b.order)
} }*/
for (const entry of filtered) { for (const entry of filtered) {
const content = String(entry.text || '').trim() const content = String(entry.text || '').trim()
@ -263,20 +379,67 @@ const emitCheckedChange = () => {
emit('checkedChange', [...checkedIds.value]) emit('checkedChange', [...checkedIds.value])
} }
const saveToStore = () => { const saveToStore = async () => {
const payload: WorkContentState = { /*const payload: WorkContentState = {
detailRows: getPersistableRows(rowData.value).map(item => ({ ...item })) detailRows: getPersistableRows(rowData.value).map(item => ({ ...item }))
} }
zxFwPricingStore.setKeyState(props.storageKey, payload) zxFwPricingStore.setKeyState(props.storageKey, payload)
emitCheckedChange() emitCheckedChange()*/
const rows = { value: [...rowData.value], type: `${props.contractId}-content`, id: `${props.contractId}-content` }
const stats = await useDataStore().upsert(rows)
console.log('💾 数据保存成功:', rows)
} }
const loadFromStore = async () => { const loadFromStore = async () => {
const defaultRows = // TODO
props.dictMode === 'none' let defaultRows = await buildDefaultRowsFromDict()
? [] const zxRows = await useDataStore().query([
: await buildDefaultRowsFromDict() { field: 'type', value: `${props.contractId}-zxFw`, operator: 'eq' }
const state = await zxFwPricingStore.loadKeyState<WorkContentState>(props.storageKey) ])
const zxRowCodes = [
...new Set(
zxRows
.map(row => row.code)
.filter(Boolean)
.flatMap(code => {
const mapping = {
'D1': ['D3-1', 'D3-2', 'D3-3', 'D3-4', 'D3-6-1', 'D3-7', 'D4-6', 'D5-1'],
'D2-1': ['D3-1', 'D3-2', 'D3-3'],
'D2-2': ['D3-4', 'D3-5', 'D3-6'],
'D3-1': ['D3-1'],
'D3-2': ['D3-2'],
'D3-3': ['D3-3'],
'D3-4': ['D3-4'],
'D3-6-1': ['D3-6-1'],
'D3-7': ['D3-7'],
'D4-6': ['D4-6'],
'D5-1': ['D5-1']
}
return mapping[code] || []
})
),
'D0-1',
'D0'
]
// defaultRows code zxRowCodes
rowData.value = defaultRows.filter(row =>
row.code && zxRowCodes.includes(row.code)
)
/*const contentRows = await useDataStore().query([
{ field: 'type', value: `${props.contractId}-content`, operator: 'eq' }
])
if (contentRows.length > 0) {
rowData.value = contentRows[0].value;
} else {
}*/
/*console.log(rowData.value)
console.log('zxRowCodes', zxRowCodes)*/
/*const state = await zxFwPricingStore.loadKeyState<WorkContentState>(props.storageKey)
if (Array.isArray(state?.detailRows) && state.detailRows.length > 0) { if (Array.isArray(state?.detailRows) && state.detailRows.length > 0) {
const persistedRows = state.detailRows.map(item => ({ const persistedRows = state.detailRows.map(item => ({
...item, ...item,
@ -331,9 +494,9 @@ const loadFromStore = async () => {
} else { } else {
rowData.value = withAddTriggerRows(defaultRows) rowData.value = withAddTriggerRows(defaultRows)
saveToStore() saveToStore()
} }*/
emitCheckedChange() // emitCheckedChange()
await syncGroupedRowsRender() // await syncGroupedRowsRender()
} }
const handleCheckedToggle = (id: string, checked: boolean) => { const handleCheckedToggle = (id: string, checked: boolean) => {
@ -376,24 +539,11 @@ const groupRowRendererParams = computed<IGroupCellRendererParams<WorkContentRow>
innerRenderer: (params: ICellRendererParams<WorkContentRow>) => { innerRenderer: (params: ICellRendererParams<WorkContentRow>) => {
const wrapper = document.createElement('div') const wrapper = document.createElement('div')
wrapper.className = 'work-content-group-row' wrapper.className = 'work-content-group-row'
const checkbox = document.createElement('input') //
checkbox.type = 'checkbox'
checkbox.className = 'work-content-group-check'
const rows = getGroupCheckableRows(params.node)
const checkedCount = rows.filter(item => item.checked).length
checkbox.checked = rows.length > 0 && checkedCount === rows.length
checkbox.indeterminate = checkedCount > 0 && checkedCount < rows.length
checkbox.disabled = rows.length === 0
checkbox.addEventListener('mousedown', event => event.stopPropagation())
checkbox.addEventListener('click', event => event.stopPropagation())
checkbox.addEventListener('change', event => {
event.stopPropagation()
handleGroupCheckedToggle(params.node, checkbox.checked)
})
const label = document.createElement('span') const label = document.createElement('span')
label.className = 'work-content-group-label' label.className = 'work-content-group-label'
label.textContent = String(params.valueFormatted || params.value || params.node.key || '') label.textContent = String(params.valueFormatted || params.value || params.node.key || '')
wrapper.append(checkbox, label) wrapper.appendChild(label)
return wrapper return wrapper
} }
} }
@ -416,8 +566,19 @@ const contentCellRenderer = (params: ICellRendererParams<WorkContentRow>) => {
wrapper.appendChild(label) wrapper.appendChild(label)
return wrapper return wrapper
} }
// checkbox placeholder
if (data.custom) { if (!isAddTriggerRow(data) && !data.custom) {
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.checked = data.checked
checkbox.className = 'work-content-check'
checkbox.addEventListener('change', (event: Event) => {
const target = event.target as HTMLInputElement
handleCheckedToggle(data.id, target.checked)
})
wrapper.appendChild(checkbox)
}
const label = document.createElement('span') const label = document.createElement('span')
if (!data.content) { if (!data.content) {
label.className = 'work-content-placeholder' label.className = 'work-content-placeholder'
@ -428,20 +589,6 @@ const contentCellRenderer = (params: ICellRendererParams<WorkContentRow>) => {
} }
wrapper.appendChild(label) wrapper.appendChild(label)
return wrapper return wrapper
}
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.className = 'work-content-check'
checkbox.checked = Boolean(data.checked)
checkbox.addEventListener('change', () => {
handleCheckedToggle(data.id, checkbox.checked)
})
const label = document.createElement('span')
label.className = 'work-content-text'
label.textContent = String(data.content || '')
wrapper.appendChild(checkbox)
wrapper.appendChild(label)
return wrapper
} }
const columnDefs: ColDef<WorkContentRow>[] = [ const columnDefs: ColDef<WorkContentRow>[] = [
@ -452,13 +599,45 @@ const columnDefs: ColDef<WorkContentRow>[] = [
suppressMovable: true, suppressMovable: true,
editable: false, editable: false,
colSpan: params => (isAddTriggerRow(params.data) ? 5 : 1), colSpan: params => (isAddTriggerRow(params.data) ? 5 : 1),
valueGetter: params => { /*valueGetter: params => {
if (!params.node || params.node.group || isAddTriggerRow(params.data)) return '' if (!params.node || params.node.group || isAddTriggerRow(params.data)) return ''
if (!isWholeProcessGroupedMode.value) return (params.node.rowIndex ?? 0) + 1 if (!isWholeProcessGroupedMode.value) return (params.node.rowIndex ?? 0) + 1
const siblings = params.node.parent?.childrenAfterSort || [] const siblings = params.node.parent?.childrenAfterSort || []
const visibleLeafSiblings = siblings.filter(node => !node.group && !isAddTriggerRow(node.data as WorkContentRow)) const visibleLeafSiblings = siblings.filter(node => !node.group && !isAddTriggerRow(node.data as WorkContentRow))
const index = visibleLeafSiblings.findIndex(node => node.id === params.node?.id) const index = visibleLeafSiblings.findIndex(node => node.id === params.node?.id)
return index >= 0 ? index + 1 : '' return index >= 0 ? index + 1 : ''
},*/
valueGetter: params => {
// 1.
if (isAddTriggerRow(params.data)) return ''
// 2.
if (params.node.group) {
//
const allGroups = params.api.getRowNode(params.node.id)?.allChildren
?.filter(node => node.group && !isAddTriggerRow(node.data))
const groupIndex = allGroups?.indexOf(params.node) + 1 || 1
return String(groupIndex)
}
// 3.
const parentNode = params.node.parent
if (!parentNode) return ''
//
const parentGroupIndex = parentNode.allChildren
?.filter(node => node.group && !isAddTriggerRow(node.data))
.indexOf(parentNode) + 1 || 1
//
const siblings = parentNode.childrenAfterSort || []
const visibleLeafSiblings = siblings.filter(node =>
!node.group && !isAddTriggerRow(node.data as WorkContentRow)
)
const childIndex = visibleLeafSiblings.findIndex(node => node.id === params.node?.id) + 1
return `${childIndex}`
// return `${parentGroupIndex}.${childIndex}`
}, },
cellRenderer: (params: ICellRendererParams<WorkContentRow>) => { cellRenderer: (params: ICellRendererParams<WorkContentRow>) => {
const row = params.data const row = params.data
@ -491,7 +670,7 @@ const columnDefs: ColDef<WorkContentRow>[] = [
cellStyle: { whiteSpace: 'normal', lineHeight: '1.5' }, cellStyle: { whiteSpace: 'normal', lineHeight: '1.5' },
cellRenderer: contentCellRenderer cellRenderer: contentCellRenderer
}, },
{ /*{
headerName: t('workContent.columns.type'), headerName: t('workContent.columns.type'),
field: 'type', field: 'type',
minWidth: 100, minWidth: 100,
@ -499,7 +678,7 @@ const columnDefs: ColDef<WorkContentRow>[] = [
editable: false, editable: false,
valueFormatter: (params: ValueFormatterParams<WorkContentRow>) => valueFormatter: (params: ValueFormatterParams<WorkContentRow>) =>
isAddTriggerRow(params.data) ? '' : String(params.value || '') isAddTriggerRow(params.data) ? '' : String(params.value || '')
}, },*/
{ {
headerName: t('workContent.columns.remark'), headerName: t('workContent.columns.remark'),
field: 'remark', field: 'remark',
@ -516,7 +695,7 @@ const columnDefs: ColDef<WorkContentRow>[] = [
}, },
valueFormatter: params => (isAddTriggerRow(params.data) ? '' : (params.value || t('workContent.clickToInput'))) valueFormatter: params => (isAddTriggerRow(params.data) ? '' : (params.value || t('workContent.clickToInput')))
}, },
{ /*{
headerName: t('workContent.columns.actions'), headerName: t('workContent.columns.actions'),
colId: 'actions', colId: 'actions',
minWidth: 92, minWidth: 92,
@ -556,7 +735,7 @@ const columnDefs: ColDef<WorkContentRow>[] = [
} }
} }
}) })
} }*/
] ]
const gridColumnDefs = computed(() => withReadonlyAutoHeight(columnDefs)) const gridColumnDefs = computed(() => withReadonlyAutoHeight(columnDefs))
@ -579,7 +758,7 @@ const createAddTriggerRow = (groupName?: string): WorkContentRow => {
} }
} }
const withAddTriggerRows = (rows: WorkContentRow[]) => { const withAddTriggerRows2 = (rows: WorkContentRow[]) => {
const pureRows = getPersistableRows(rows) const pureRows = getPersistableRows(rows)
if (isWholeProcessGroupedMode.value) { if (isWholeProcessGroupedMode.value) {
const groupedMap = new Map<string, WorkContentRow[]>() const groupedMap = new Map<string, WorkContentRow[]>()
@ -608,6 +787,11 @@ const withAddTriggerRows = (rows: WorkContentRow[]) => {
return [...pureRows, createAddTriggerRow()] return [...pureRows, createAddTriggerRow()]
} }
const withAddTriggerRows = (rows: WorkContentRow[]) => {
const pureRows = getPersistableRows(rows)
return pureRows
}
const getDataPath = (data: WorkContentRow) => { const getDataPath = (data: WorkContentRow) => {
const path = Array.isArray(data?.path) const path = Array.isArray(data?.path)
? data.path.map(segment => String(segment || '').trim()).filter(Boolean) ? data.path.map(segment => String(segment || '').trim()).filter(Boolean)
@ -685,11 +869,21 @@ watch(isWholeProcessGroupedMode, () => {
}) })
watch( watch(
() => useDataStore().items,
async (newItems) => {
if (Object.keys(newItems).length > 0) {
loadFromStore();
}
},
{ immediate: true, deep: true }
)
/*watch(
() => rowData.value.length, () => rowData.value.length,
() => { () => {
void syncGroupedRowsRender() void syncGroupedRowsRender()
} }
) )*/
watch(locale, () => { watch(locale, () => {
void loadFromStore() void loadFromStore()
@ -732,10 +926,10 @@ const confirmDeleteRow = () => {
:columnDefs="gridColumnDefs" :columnDefs="gridColumnDefs"
:theme="myTheme" :theme="myTheme"
:getRowId="(params: { data: WorkContentRow }) => params.data.id" :getRowId="(params: { data: WorkContentRow }) => params.data.id"
:treeData="isWholeProcessGroupedMode" :treeData="true"
:getDataPath="getDataPath" :getDataPath="getDataPath"
:groupDefaultExpanded="isWholeProcessGroupedMode ? -1 : 0" :groupDefaultExpanded="true ? -1 : 0"
:groupDisplayType="isWholeProcessGroupedMode ? 'groupRows' : undefined" :groupDisplayType="true ? 'groupRows' : undefined"
:groupRowRendererParams="groupRowRendererParams" :groupRowRendererParams="groupRowRendererParams"
:animateRows="true" :animateRows="true"
:localeText="AG_GRID_LOCALE_CN" :localeText="AG_GRID_LOCALE_CN"
@ -824,6 +1018,11 @@ const confirmDeleteRow = () => {
gap: 8px; gap: 8px;
} }
/* 隐藏一级分组行的复选框,但保留折叠箭头 */
:deep(.ag-group-cell .ag-cell-value .ag-cell-wrapper > input[type='checkbox']) {
display: none !important;
}
:deep(.work-content-group-check) { :deep(.work-content-group-check) {
width: 14px; width: 14px;
height: 14px; height: 14px;

View File

@ -138,16 +138,6 @@ const hasMeaningfulFactorValue = (rows: SourceRow[] | undefined) =>
return hasBudgetValue || hasRemark return hasBudgetValue || hasRemark
}) })
const hasUsablePersistedRows = (state: GridState | null | undefined) =>
Array.isArray(state?.detailRows) &&
state.detailRows.some(row => {
const hasFactor =
typeof row?.budgetValue === 'number' ||
typeof row?.standardFactor === 'number'
const hasRemark = typeof row?.remark === 'string' && row.remark.trim() !== ''
return hasFactor || hasRemark || String(row?.id || '').trim() !== ''
})
const mergeWithDictRows = (rowsFromDb: SourceRow[] | undefined): FactorRow[] => { const mergeWithDictRows = (rowsFromDb: SourceRow[] | undefined): FactorRow[] => {
const dbValueMap = new Map<string, SourceRow>() const dbValueMap = new Map<string, SourceRow>()
for (const row of rowsFromDb || []) { for (const row of rowsFromDb || []) {
@ -318,7 +308,7 @@ const saveFactorChangeState = async (changedRowIds: string[]) => {
const loadGridState = async (storageKey: string): Promise<GridState | null> => { const loadGridState = async (storageKey: string): Promise<GridState | null> => {
if (!storageKey) return null if (!storageKey) return null
const piniaData = await zxFwPricingStore.loadKeyState<GridState>(storageKey) const piniaData = await zxFwPricingStore.loadKeyState<GridState>(storageKey)
if (hasUsablePersistedRows(piniaData)) return piniaData if (piniaData?.detailRows && Array.isArray(piniaData.detailRows)) return piniaData
// kvStore pinia keyed state // kvStore pinia keyed state
const legacyData = await kvStore.getItem<GridState>(storageKey) const legacyData = await kvStore.getItem<GridState>(storageKey)

View File

@ -9,7 +9,7 @@ import { withReadonlyAutoHeight } from '@/lib/agGridReadonlyAutoHeight'
import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal' import { decimalAggSum, roundTo, sumByNumber } from '@/lib/decimal'
import { parseNumberOrNull } from '@/lib/number' import { parseNumberOrNull } from '@/lib/number'
import { formatThousandsFlexible } from '@/lib/numberFormat' import { formatThousandsFlexible } from '@/lib/numberFormat'
import { getIndustryDisplayName, getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql' import { getIndustryDisplayName, getMajorDictEntries, isMajorIdInIndustryScope, xmProjectConfig } from '@/sql'
import { syncContractScaleToPricing, type ContractScaleSyncResult } from '@/lib/zxFwPricingSync' import { syncContractScaleToPricing, type ContractScaleSyncResult } from '@/lib/zxFwPricingSync'
import { SwitchRoot, SwitchThumb } from 'reka-ui' import { SwitchRoot, SwitchThumb } from 'reka-ui'
import { useKvStore } from '@/pinia/kv' import { useKvStore } from '@/pinia/kv'
@ -22,8 +22,6 @@ import {
ToastViewport ToastViewport
} from 'reka-ui' } from 'reka-ui'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { CircleHelp } from 'lucide-vue-next'
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
@ -112,14 +110,35 @@ const buildDetailDict = (entries: Array<[string, MajorLite]>) => {
hasArea: item.hasArea !== false hasArea: item.hasArea !== false
}) })
} }
const result = groupOrder.map(code => groupMap.get(code)).filter((group): group is DictGroup => Boolean(group))
return groupOrder.map(code => groupMap.get(code)).filter((group): group is DictGroup => Boolean(group)) return result
} }
const buildDefaultRows = (): DetailRow[] => { const buildDefaultRows = (): DetailRow[] => {
const rows: DetailRow[] = [] const rows: DetailRow[] = []
// 1. 使
for (const group of detailDict.value) {
if (group.code != 'C0') {
rows.push({
id: group.id,
groupCode: group.code,
groupName: group.name,
majorCode: group.code,
majorName: group.name,
hasCost: true,
hasArea: true,
amount: null,
landArea: null,
path: [`${group.code} ${group.name}`],
isGroupRow: true,
hide: false
})
}
}
for (const group of detailDict.value) { for (const group of detailDict.value) {
for (const child of group.children) { for (const child of group.children) {
@ -326,25 +345,61 @@ interface ContractScaleChangeState {
updatedAt: number updatedAt: number
} }
const XM_PROJECT_PHASE_KEY = 'xm-project-phase-v1'
interface ProjectPhaseState {
feePhase?: string
feeStage?: string
}
const props = defineProps<{ const props = defineProps<{
title: string title: string
dbKey: string dbKey: string
xmInfoKey?: string | null xmInfoKey?: string | null
baseInfoKey?: string baseInfoKey?: string
titleHint?: string
titleHintAria?: string
}>() }>()
let persistTimer: ReturnType<typeof setTimeout> | null = null let persistTimer: ReturnType<typeof setTimeout> | null = null
const gridApi = ref<GridApi<DetailRow> | null>(null) const gridApi = ref<GridApi<DetailRow> | null>(null)
const activeIndustryId = ref('') const activeIndustryId = ref('')
const totalLabel = computed(() => { const totalLabel = ref(xmProjectConfig.pinnedTotalLabel)
const industryName = getIndustryDisplayName(activeIndustryId.value.trim(), locale.value)
return industryName ? t('pricingScale.totalInvestmentByIndustry', { industryName }) : t('pricingScale.totalInvestment')
})
const roughCalcEnabled = ref(false) const roughCalcEnabled = ref(false)
const visibleRowData = computed(() => { return detailRows.value.filter(row => !row.hide) }) const visibleRowData = computed(() => { return detailRows.value.filter(row => !row.hide) })
const feePhaseOptions = xmProjectConfig.feePhaseOptions
const feeStageOptions = xmProjectConfig.feeStageOptions
const feePhase = ref('')
const feeStage = ref('')
const loadPhaseState = async () => {
try {
const state = await kvStore.getItem<ProjectPhaseState>(XM_PROJECT_PHASE_KEY)
if (state) {
feePhase.value = state.feePhase || ''
feeStage.value = state.feeStage || ''
}
} catch (error) {
console.error('load phase state failed:', error)
}
}
const savePhaseState = async () => {
try {
await kvStore.setItem<ProjectPhaseState>(XM_PROJECT_PHASE_KEY, {
feePhase: feePhase.value,
feeStage: feeStage.value
})
} catch (error) {
console.error('save phase state failed:', error)
}
}
watch([feePhase, feeStage], () => {
void savePhaseState()
})
void loadPhaseState()
const refreshPinnedTotalLabelCell = () => { const refreshPinnedTotalLabelCell = () => {
if (!gridApi.value) return if (!gridApi.value) return
const pinnedTopNode = gridApi.value.getPinnedTopRow(0) const pinnedTopNode = gridApi.value.getPinnedTopRow(0)
@ -398,34 +453,26 @@ const columnDefs: ColDef<DetailRow>[] = [
} }
}, },
{ {
headerName: t('pricingScale.columns.landArea'), headerName: '造价金额(元)',
field: 'landArea', field: 'amountYuan',
headerClass: 'ag-right-aligned-header', headerClass: 'ag-right-aligned-header',
minWidth: 100, minWidth: 120,
flex: 1, flex: 1,
editable: params => !roughCalcEnabled.value && !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea), editable: false,
cellClass: params => aggFunc: decimalAggSum,
!params.node?.group && !params.node?.rowPinned && params.data?.hasArea valueGetter: params => params.data?.amount,
? 'editable-cell-line'
: '',
cellClassRules: { cellClassRules: {
'ag-right-aligned-cell': () => true, 'ag-right-aligned-cell': () => true
'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea) && (params.value == null || params.value === '')
}, },
valueParser: params => parseNumberOrNull(params.newValue, { precision: 3 }),
valueFormatter: params => { valueFormatter: params => {
if (roughCalcEnabled.value) { if (roughCalcEnabled.value) {
return '' return ''
} }
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasArea) { const amount = params.value
if (typeof amount !== 'number' || !Number.isFinite(amount)) {
return '' return ''
} }
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) { return formatThousandsFlexible(roundTo(amount * 10000, 0), 0)
return t('pricingScale.clickToInput')
}
if (params.value == null) return ''
return formatThousandsFlexible(params.value, 3)
} }
} }
] ]
@ -435,9 +482,6 @@ const autoGroupColumnDef: ColDef = {
headerName: t('pricingScale.columns.majorGroup'), headerName: t('pricingScale.columns.majorGroup'),
minWidth: 200, minWidth: 200,
flex: 2, flex: 2,
cellClassRules: {
'ag-summary-label-cell': params => Boolean(params.node?.rowPinned)
},
cellRendererParams: { cellRendererParams: {
suppressCount: true suppressCount: true
}, },
@ -763,24 +807,10 @@ onMounted(() => {
<div class="h-full"> <div class="h-full">
<div class="rounded-lg border bg-card xmMx scroll-mt-3 flex flex-col overflow-hidden h-full"> <div class="rounded-lg border bg-card xmMx scroll-mt-3 flex flex-col overflow-hidden h-full">
<div class="flex items-center justify-between border-b px-4 py-3"> <div class="flex items-center justify-between border-b px-4 py-3">
<div class="flex items-center gap-1.5">
<h3 <h3
class="text-sm font-semibold text-foreground cursor-pointer select-none transition-colors hover:text-primary"> class="text-sm font-semibold text-foreground cursor-pointer select-none transition-colors hover:text-primary">
{{ props.title }} {{ props.title }}
</h3> </h3>
<TooltipRoot v-if="props.titleHint">
<TooltipTrigger as-child>
<button
type="button"
:aria-label="props.titleHintAria || props.titleHint"
class="inline-flex h-5 w-5 items-center justify-center text-muted-foreground/90 transition hover:text-foreground"
>
<CircleHelp class="h-4 w-4" />
</button>
</TooltipTrigger>
<TooltipContent side="top">{{ props.titleHint }}</TooltipContent>
</TooltipRoot>
</div>
<!-- <div class="flex items-center gap-2"> <!-- <div class="flex items-center gap-2">
<span class=" text-xs text-muted-foreground">简要计算</span> <span class=" text-xs text-muted-foreground">简要计算</span>
<SwitchRoot <SwitchRoot
@ -792,6 +822,42 @@ onMounted(() => {
</div> --> </div> -->
</div> </div>
<!-- Project Phase Selection -->
<div class="border-b px-4 py-3 space-y-3 bg-card">
<div class="flex items-center gap-4">
<span class="shrink-0 text-sm font-medium text-foreground w-28">项目费用阶段</span>
<div class="flex-1 flex flex-wrap gap-2">
<button
v-for="option in feePhaseOptions"
:key="option"
class="px-3 py-1.5 text-xs rounded-lg border transition-colors"
:class="feePhase === option
? 'bg-blue-600 border-blue-600 text-white font-medium'
: 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'"
@click="feePhase = option"
>
{{ option }}
</button>
</div>
</div>
<div class="flex items-center gap-4">
<span class="shrink-0 text-sm font-medium text-foreground w-28">项目费用环节</span>
<div class="flex-1 flex flex-wrap gap-2">
<button
v-for="option in feeStageOptions"
:key="option"
class="px-3 py-1.5 text-xs rounded-lg border transition-colors"
:class="feeStage === option
? 'bg-blue-600 border-blue-600 text-white font-medium'
: 'bg-white border-slate-200 text-slate-600 hover:bg-slate-50'"
@click="feeStage = option"
>
{{ option }}
</button>
</div>
</div>
</div>
<div :class="agGridWrapClass"> <div :class="agGridWrapClass">
<AgGridVue :style="agGridStyle" :rowData="visibleRowData" :pinnedTopRowData="pinnedTopRowData" <AgGridVue :style="agGridStyle" :rowData="visibleRowData" :pinnedTopRowData="pinnedTopRowData"
:columnDefs="gridColumnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="detailGridOptions" :theme="myTheme" :columnDefs="gridColumnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="detailGridOptions" :theme="myTheme"

View File

@ -44,3 +44,24 @@
opacity: 0; opacity: 0;
transform: translateY(-6px) scale(0.98); transform: translateY(-6px) scale(0.98);
} }
/* Sidebar slide transitions */
.slide-in-left-enter-active,
.slide-in-left-leave-active {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-in-left-enter-from,
.slide-in-left-leave-to {
transform: translateX(100%);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}

View File

@ -1,41 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { useTabStore } from '@/pinia/tab' import { useTabStore } from '@/pinia/tab'
import { useKvStore } from '@/pinia/kv' import { useKvStore } from '@/pinia/kv'
import { useUiPrefsStore } from '@/pinia/uiPrefs' import { useUiPrefsStore } from '@/pinia/uiPrefs'
import { import {
BarChart3,
Calculator, Calculator,
Check, Check,
ChevronDown,
Languages, Languages,
X X
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { getIndustryDisplayName, industryTypeList } from '@/sql' import { getIndustryDisplayName, industryTypeList } from '@/sql'
import { initializeProjectFactorStates, initializeProjectScaleState } from '@/lib/projectWorkspace' import { initializeProjectFactorStates, initializeProjectScaleState } from '@/lib/projectWorkspace'
import { import {
SelectContent,
SelectIcon,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectPortal,
SelectRoot,
SelectTrigger,
SelectViewport
} from 'reka-ui'
import {
buildDisclaimerUrl,
buildProjectUrl, buildProjectUrl,
consumePendingDisclaimerAction,
DEFAULT_PROJECT_ID, DEFAULT_PROJECT_ID,
hasAcceptedRestrictedDisclaimer,
FORCE_HOME_QUERY_KEY, FORCE_HOME_QUERY_KEY,
isDisclaimerAcceptanceRequired,
NEW_PROJECT_QUERY_KEY, NEW_PROJECT_QUERY_KEY,
OPEN_PROJECT_DIALOG_QUERY_KEY, OPEN_PROJECT_DIALOG_QUERY_KEY,
setPendingDisclaimerAction,
PROJECT_TAB_ID, PROJECT_TAB_ID,
QUICK_PROJECT_ID, QUICK_PROJECT_ID,
QUICK_CONSULT_CATEGORY_FACTOR_KEY, QUICK_CONSULT_CATEGORY_FACTOR_KEY,
@ -81,13 +65,12 @@ const PROJECT_INFO_KEY = 'xm-base-info-v1'
const PROJECT_CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1' const PROJECT_CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1'
const PROJECT_MAJOR_FACTOR_KEY = 'xm-major-factor-v1' const PROJECT_MAJOR_FACTOR_KEY = 'xm-major-factor-v1'
const PROJECT_SCALE_KEY = 'xm-info-v3' const PROJECT_SCALE_KEY = 'xm-info-v3'
const FILE_LEDGER_URL = 'https://www.lianzhong.com.cn/file?fileNo=24'
const getActiveProjectId = () => readCurrentProjectId() const getActiveProjectId = () => readCurrentProjectId()
const tabStore = useTabStore() const tabStore = useTabStore()
const kvStore = useKvStore() const kvStore = useKvStore()
const uiPrefsStore = useUiPrefsStore() const uiPrefsStore = useUiPrefsStore()
const { t, tm, locale } = useI18n() const { t, locale } = useI18n()
const projectDialogOpen = ref(false) const projectDialogOpen = ref(false)
const projectIndustry = ref(String(industryTypeList[0]?.id || '')) const projectIndustry = ref(String(industryTypeList[0]?.id || ''))
const projectSubmitting = ref(false) const projectSubmitting = ref(false)
@ -99,24 +82,21 @@ const homeImportConfirmOpen = ref(false)
const pendingHomeImportFile = ref<File | null>(null) const pendingHomeImportFile = ref<File | null>(null)
const pendingHomeImportFileName = ref('') const pendingHomeImportFileName = ref('')
const existingProjectDialogOpen = ref(false) const existingProjectDialogOpen = ref(false)
const disclaimerRequired = ref(false)
const existingProjects = ref<Array<{ id: string; name: string; updatedAt: string }>>([]) const existingProjects = ref<Array<{ id: string; name: string; updatedAt: string }>>([])
const existingProjectLoading = ref(false) const existingProjectLoading = ref(false)
const hasExistingProjects = ref(false) const hasExistingProjects = ref(false)
const openedProjectIds = ref<string[]>([]) const openedProjectIds = ref<string[]>([])
let existingProjectPollTimer: ReturnType<typeof setInterval> | null = null let existingProjectPollTimer: ReturnType<typeof setInterval> | null = null
const heroTitleIndex = ref(0) const RELATED_FILES_URL = 'https://www.lianzhong.com.cn/file'
const heroDescIndex = ref(0)
const projectIndustryLabel = computed(() => {
const target = String(projectIndustry.value || '').trim()
if (!target) return ''
return getIndustryDisplayName(target, locale.value) || ''
})
const localeBadge = computed(() => (locale.value === 'en-US' ? 'EN' : '中')) const localeBadge = computed(() => (locale.value === 'en-US' ? 'EN' : '中'))
const toggleLocale = () => { const toggleLocale = () => {
const next = locale.value === 'en-US' ? 'zh-CN' : 'en-US' const next = locale.value === 'en-US' ? 'zh-CN' : 'en-US'
uiPrefsStore.setLocale(next as 'zh-CN' | 'en-US') uiPrefsStore.setLocale(next as 'zh-CN' | 'en-US')
} }
const openRelatedFiles = () => {
window.open(RELATED_FILES_URL, '_blank', 'noopener')
}
const resolveProjectRegistryName = (projectIdRaw: string) => { const resolveProjectRegistryName = (projectIdRaw: string) => {
const projectId = String(projectIdRaw || '').trim() const projectId = String(projectIdRaw || '').trim()
if (projectId !== DEFAULT_PROJECT_ID) return undefined if (projectId !== DEFAULT_PROJECT_ID) return undefined
@ -182,35 +162,8 @@ const loadProjectDefaults = async () => {
} }
const openProjectCalc = async () => { const openProjectCalc = async () => {
await runWithDisclaimerGuard({ type: 'project' }, async () => {
await loadProjectDefaults()
projectDialogOpen.value = true projectDialogOpen.value = true
})
}
const openRelatedFiles = () => {
window.open(FILE_LEDGER_URL, '_blank', 'noopener')
}
const redirectToDisclaimerPage = () => {
const returnUrl = window.location.href
window.location.href = buildDisclaimerUrl(returnUrl)
}
const runWithDisclaimerGuard = async (
pendingAction: { type: 'project' | 'quick' | 'import' | 'existing-project'; projectId?: string },
action: () => void | Promise<void>
) => {
if (!disclaimerRequired.value || hasAcceptedRestrictedDisclaimer()) {
await action()
return
}
setPendingDisclaimerAction(pendingAction)
redirectToDisclaimerPage()
}
const syncDisclaimerRequirement = () => {
disclaimerRequired.value = isDisclaimerAcceptanceRequired()
} }
const syncExistingProjectOpenedState = (projectIds: string[]) => { const syncExistingProjectOpenedState = (projectIds: string[]) => {
@ -263,11 +216,10 @@ const startExistingProjectPolling = () => {
} }
const openExistingProjectDialog = async () => { const openExistingProjectDialog = async () => {
await runWithDisclaimerGuard({ type: 'existing-project' }, async () => { projectDialogOpen.value = false
existingProjectDialogOpen.value = true existingProjectDialogOpen.value = true
await refreshExistingProjects() await refreshExistingProjects()
startExistingProjectPolling() startExistingProjectPolling()
})
} }
const closeExistingProjectDialog = () => { const closeExistingProjectDialog = () => {
@ -358,38 +310,8 @@ const enterQuickCalc = (contractName: string) => {
tabStore.hasCompletedSetup = true tabStore.hasCompletedSetup = true
} }
const openQuickCalc = async () => { const openQuickCalc = () => {
await runWithDisclaimerGuard({ type: 'quick' }, async () => { window.alert(t('home.cards.developing'))
await loadQuickDefaults()
const contractName = quickContractName.value.trim() || QUICK_CONTRACT_FALLBACK_NAME
const industry = quickIndustry.value.trim()
quickSubmitting.value = true
try {
const currentInfo = await kvStore.getItem<QuickProjectInfoState>(QUICK_PROJECT_INFO_KEY)
await kvStore.setItem(QUICK_PROJECT_INFO_KEY, {
...currentInfo,
projectIndustry: industry,
projectName: t('quickCalc.projectName')
})
await kvStore.setItem(QUICK_CONTRACT_META_KEY, {
id: QUICK_CONTRACT_ID,
name: contractName,
updatedAt: new Date().toISOString()
})
if (industry) {
await initializeProjectFactorStates(
kvStore,
industry,
QUICK_CONSULT_CATEGORY_FACTOR_KEY,
QUICK_MAJOR_FACTOR_KEY
)
}
enterQuickCalc(contractName)
} finally {
quickSubmitting.value = false
}
})
} }
const handleHomeImportChange = (event: Event) => { const handleHomeImportChange = (event: Event) => {
@ -403,33 +325,7 @@ const handleHomeImportChange = (event: Event) => {
} }
const openHomeImport = () => { const openHomeImport = () => {
void runWithDisclaimerGuard({ type: 'import' }, () => {
homeImportInputRef.value?.click() homeImportInputRef.value?.click()
})
}
const replayPendingDisclaimerAction = async () => {
if (!disclaimerRequired.value || !hasAcceptedRestrictedDisclaimer()) return
const pendingAction = consumePendingDisclaimerAction()
if (!pendingAction) return
if (pendingAction.type === 'project') {
await loadProjectDefaults()
projectDialogOpen.value = true
return
}
if (pendingAction.type === 'quick') {
await openQuickCalc()
return
}
if (pendingAction.type === 'import') {
homeImportInputRef.value?.click()
return
}
if (pendingAction.type === 'existing-project') {
existingProjectDialogOpen.value = true
await refreshExistingProjects()
startExistingProjectPolling()
}
} }
const cancelHomeImportConfirm = () => { const cancelHomeImportConfirm = () => {
@ -458,85 +354,10 @@ const handleHomeVisibilityChange = () => {
handleHomeWindowFocus() handleHomeWindowFocus()
} }
const openDisclaimerPage = () => {
const href = buildDisclaimerUrl(window.location.href)
window.open(href, '_blank', 'noopener')
}
const resolveLocalizedStringArray = (value: unknown) => {
if (!Array.isArray(value)) return []
return value.filter((item): item is string => typeof item === 'string' && item.trim().length > 0)
}
const heroTitleOptions = computed(() =>
resolveLocalizedStringArray(tm('home.cards.heroTitles'))
)
const heroDescOptions = computed(() =>
resolveLocalizedStringArray(tm('home.cards.heroDescs'))
)
const heroTitleText = computed(() => heroTitleOptions.value[heroTitleIndex.value] || '')
const heroDescText = computed(() => heroDescOptions.value[heroDescIndex.value] || '')
const pickRandomIndex = (length: number) => {
if (length <= 1) return 0
return Math.floor(Math.random() * length)
}
const refreshHeroCopy = () => {
heroTitleIndex.value = pickRandomIndex(heroTitleOptions.value.length)
heroDescIndex.value = pickRandomIndex(heroDescOptions.value.length)
}
const homeActionCards = [
{
key: 'projectBudget',
desc: 'projectBudgetDesc',
action: 'enter',
icon: 'project',
iconWrapClass: 'border-blue-100 bg-blue-50/80 text-blue-600',
iconClass: 'h-5 w-5',
clickFunc: openProjectCalc,
showExistingAction: true
},
{
key: 'quickCalc',
desc: 'quickCalcDesc',
action: 'enter',
icon: 'quick',
iconWrapClass: 'border-amber-100 bg-amber-50/80 text-amber-600',
iconClass: 'h-10 w-10',
clickFunc: openQuickCalc,
showExistingAction: false
},
{
key: 'importData',
desc: 'importDataDesc',
action: 'pickFile',
icon: 'import',
iconWrapClass: 'border-emerald-100 bg-emerald-50/80 text-emerald-600',
iconClass: 'h-5 w-5',
clickFunc: openHomeImport,
showExistingAction: false
},
{
key: 'file',
desc: 'fileDataDesc',
action: 'openFileSystem',
icon: 'files',
iconWrapClass: 'border-emerald-100 bg-emerald-50/80 text-emerald-600',
iconClass: 'h-5 w-5',
clickFunc: openRelatedFiles,
showExistingAction: false
}
] as const
onMounted(() => { onMounted(() => {
syncDisclaimerRequirement()
void refreshExistingProjects() void refreshExistingProjects()
void loadProjectDefaults() void loadProjectDefaults()
void loadQuickDefaults() void loadQuickDefaults()
void replayPendingDisclaimerAction()
refreshHeroCopy()
window.addEventListener('focus', handleHomeWindowFocus) window.addEventListener('focus', handleHomeWindowFocus)
document.addEventListener('visibilitychange', handleHomeVisibilityChange) document.addEventListener('visibilitychange', handleHomeVisibilityChange)
try { try {
@ -564,109 +385,127 @@ onBeforeUnmount(() => {
window.removeEventListener('focus', handleHomeWindowFocus) window.removeEventListener('focus', handleHomeWindowFocus)
document.removeEventListener('visibilitychange', handleHomeVisibilityChange) document.removeEventListener('visibilitychange', handleHomeVisibilityChange)
}) })
watch(
() => locale.value,
() => {
refreshHeroCopy()
}
)
</script> </script>
<template> <template>
<input ref="homeImportInputRef" type="file" accept=".zw" class="sr-only" @change="handleHomeImportChange" /> <input ref="homeImportInputRef" type="file" accept=".zw" class="sr-only" @change="handleHomeImportChange" />
<div class="home-entry relative flex min-h-screen items-center justify-center px-4 py-8 lg:py-10"> <div class="home-entry relative flex min-h-full items-center justify-center overflow-hidden px-4 py-8 lg:py-10">
<div class="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_80%_60%_at_50%_-10%,rgba(59,130,246,0.06),transparent_70%)]" /> <div class="pointer-events-none absolute inset-0 bg-[url('/background.png')] bg-cover bg-center bg-no-repeat" />
<div class="pointer-events-none absolute inset-0 bg-white/78" />
<div
class="pointer-events-none absolute inset-0 bg-[radial-gradient(ellipse_80%_60%_at_50%_-10%,rgba(59,130,246,0.08),transparent_70%)]"
/>
<div class="relative w-full max-w-[1240px]"> <div class="relative w-full max-w-[1240px]">
<div class="absolute right-0 top-0 z-10"> <div class="absolute right-0 top-0 z-10">
<Button <button
variant="outline" type="button"
size="sm" class="inline-flex h-8 cursor-pointer items-center justify-center gap-1.5 rounded-full border border-slate-200/80 bg-white/85 px-3 text-xs text-slate-600 shadow-sm backdrop-blur transition hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-200"
class="h-8 cursor-pointer gap-1.5 rounded-full border-slate-200/80 bg-white/85 px-3 text-xs text-slate-600 shadow-sm backdrop-blur transition hover:bg-white"
@click="toggleLocale" @click="toggleLocale"
> >
<Languages class="h-3.5 w-3.5" /> <Languages class="h-3.5 w-3.5" />
<span>{{ localeBadge }}</span> <span>{{ localeBadge }}</span>
</Button> </button>
</div> </div>
<div class="home-title text-center" :style="{ maxWidth: '800px' }">
<div class="home-title text-center">
<h1 <h1
class="text-2xl tracking-tight text-slate-900 lg:text-3xl" class="text-2xl font-semibold tracking-tight text-slate-900 lg:text-3xl"
:style="{ whiteSpace: 'pre-line', fontWeight: '200', fontFamily: 'HarmonyOS_Sans_SC' }" style="font-family: HarmonyOS_Sans_SC, 'Microsoft YaHei', sans-serif; font-weight: 300; white-space: pre-line;"
> >
{{ t('home.title') }} {{ t('home.title') }}
</h1> </h1>
<p class="mt-1.5 text-sm text-slate-500">{{ t('home.subtitle') }}</p> <p class="mt-1.5 text-sm text-slate-500">{{ t('home.subtitle') }}</p>
</div> </div>
<div class="mt-5 grid items-stretch gap-4 md:grid-cols-2 xl:grid-cols-5">
<div <div class="mt-5 grid items-stretch gap-4 md:grid-cols-2 xl:grid-cols-4">
class="home-hero home-card-base home-entry-item home-entry-item--1 relative overflow-hidden rounded-2xl bg-[#dc2626] p-7 text-white shadow-[0_24px_60px_rgba(153,27,27,0.35)]" <section
class="home-card-base home-entry-item home-entry-item--1 relative overflow-hidden rounded-2xl bg-[#dc2626] p-7 text-white shadow-[0_24px_60px_rgba(153,27,27,0.35)]"
> >
<div class="pointer-events-none absolute -right-20 -top-16 h-56 w-56 rounded-full bg-white/12 blur-2xl" /> <div class="pointer-events-none absolute -right-20 -top-16 h-56 w-56 rounded-full bg-white/12 blur-2xl" />
<div class="pointer-events-none absolute -left-10 -bottom-10 h-40 w-40 rounded-full bg-white/8 blur-3xl" /> <div class="pointer-events-none absolute -bottom-10 -left-10 h-40 w-40 rounded-full bg-white/8 blur-3xl" />
<div v-for="index in 10" :key="index" :class="`home-hero-meteor home-hero-meteor--${index}`" /> <div v-for="index in 10" :key="index" :class="`home-hero-meteor home-hero-meteor--${index}`" />
<div class="relative inline-flex h-11 w-11 items-center justify-center rounded-xl bg-white/15 ring-1 ring-white/35"> <div class="relative inline-flex h-11 w-11 items-center justify-center rounded-xl bg-white/15 ring-1 ring-white/35">
<Calculator class="h-5 w-5" /> <BarChart3 class="h-5 w-5" />
</div> </div>
<h2 class="relative mt-8 text-2xl font-semibold leading-tight tracking-tight lg:text-3xl" :style="{ whiteSpace: 'pre-line', fontSize: '20px' }">{{ heroTitleText }}</h2> <h2 class="relative mt-8 whitespace-pre-line text-xl font-semibold leading-tight tracking-tight">
{{ t('home.cards.heroTitle') }}
</h2>
<p class="relative mt-2 text-sm text-red-200/90">{{ t('home.cards.heroSubTitle') }}</p> <p class="relative mt-2 text-sm text-red-200/90">{{ t('home.cards.heroSubTitle') }}</p>
<div class="relative mt-6 h-px bg-white/20" /> <div class="relative mt-6 h-px bg-white/20" />
<p class="relative mt-4 whitespace-pre-line text-xs leading-5 text-red-200/60">{{ heroDescText }}</p> <p class="relative mt-4 whitespace-pre-line text-xs leading-5 text-red-200/80">{{ t('home.cards.heroDesc') }}</p>
</div> </section>
<article <article
v-for="(card, index) in homeActionCards"
:key="card.key"
role="button" role="button"
tabindex="0" tabindex="0"
:class="[ class="home-card-base home-entry-item home-entry-item--2 group flex cursor-pointer flex-col justify-between rounded-xl border border-slate-200/80 bg-white/95 px-5 py-5 shadow-[0_4px_20px_rgba(15,23,42,0.06)] backdrop-blur-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_12px_32px_rgba(15,23,42,0.12)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-200"
'home-card home-card-base home-entry-item group flex cursor-pointer flex-col justify-between rounded-xl border border-slate-200/80 bg-white/95 px-5 py-5 shadow-[0_4px_20px_rgba(15,23,42,0.06)] backdrop-blur-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_12px_32px_rgba(15,23,42,0.12)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-200', @click="openProjectCalc"
`home-entry-item--${index + 2}` @keydown.enter.prevent="openProjectCalc"
]" @keydown.space.prevent="openProjectCalc"
@click="card.clickFunc"
@keydown.enter.prevent="card.clickFunc"
@keydown.space.prevent="card.clickFunc"
> >
<div> <div>
<div <div class="inline-flex h-11 w-11 items-center justify-center rounded-xl border border-blue-100 bg-blue-50/80 text-blue-600 shadow-sm transition-transform duration-200 group-hover:scale-105">
:class="[ <BarChart3 class="h-5 w-5" />
'inline-flex h-11 w-11 items-center justify-center rounded-xl border shadow-sm transition-transform duration-200 group-hover:scale-105', </div>
card.iconWrapClass <h3 class="mt-4 text-base font-semibold text-slate-900">{{ t('home.cards.projectBudget') }}</h3>
]" <p class="mt-1.5 text-xs leading-5 text-slate-500">{{ t('home.cards.projectBudgetDesc') }}</p>
</div>
<div class="mt-4 flex flex-wrap items-center justify-end gap-2">
<button
v-if="hasExistingProjects"
type="button"
class="cursor-pointer rounded-md border border-slate-200 px-3 py-1.5 text-xs font-medium text-slate-600 transition hover:border-slate-300 hover:bg-slate-50 hover:text-slate-700"
@click.stop="openExistingProjectDialog"
> >
<svg v-if="card.icon === 'project'" viewBox="0 0 1024 1024" :class="card.iconClass" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> {{ t('home.cards.pickExisting') }}
<path </button>
fill="currentColor" <button
d="M938.666667 874.666667c0 11.733333-9.6 21.333333-21.333334 21.333333H106.666667c-11.733333 0-21.333333-9.6-21.333334-21.333333s9.6-21.333333 21.333334-21.333334h42.666666V490.666667c0-11.733333 9.6-21.333333 21.333334-21.333334h170.666666c11.733333 0 21.333333 9.6 21.333334 21.333334v362.666666h42.666666V320c0-11.733333 9.6-21.333333 21.333334-21.333333h170.666666c11.733333 0 21.333333 9.6 21.333334 21.333333v533.333333h42.666666V149.333333c0-11.733333 9.6-21.333333 21.333334-21.333333h170.666666c11.733333 0 21.333333 9.6 21.333334 21.333333v704h42.666666c11.733333 0 21.333333 9.6 21.333334 21.333334z" type="button"
/> class="cursor-pointer rounded-md bg-blue-600 px-3 py-1.5 text-xs font-medium text-white transition hover:bg-blue-700"
</svg> @click.stop="openProjectCalc"
<svg v-else-if="card.icon === 'quick'" viewBox="0 0 800 800" :class="card.iconClass" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> >
<path <span class="flex items-center gap-1">
fill="currentColor" {{ t('home.cards.enter') }}
d="M5245 5891c-11-5-47-38-80-73-33-36-269-281-525-544-256-263-514-530-575-592-90-93-113-123-130-170-20-54-79-170-200-392-70-129-81-164-61-194 22-35 59-40 114-15 26 11 119 47 207 79 88 33 185 69 215 81 30 12 83 31 118 44 58 20 82 40 300 246 130 123 291 276 357 339 66 63 165 158 219 210 55 52 105 100 111 106 57 58 205 194 212 194 4 0 7-520 5-1155l-2-1155-553 0c-343 0-576-4-613-11-86-15-175-46-227-79-25-15-48-26-51-23-3 4-6 154-6 335l0 328-80 0-80 0 0-336 0-336-32 22c-44 29-106 56-176 77-50 15-130 17-647 22l-590 6 0 1165 0 1165 440 0c467 0 528-4 702-50 105-28 200-69 261-114 41-31 42-32 42-91l0-60 80 0 80 0 1 33c3 69 8 82 40 110 19 16 63 43 99 59 36 16 71 33 79 37 13 7 56 169 47 178-6 6-170-49-221-74-27-14-66-38-86-54l-35-28-32 25c-51 40-181 101-274 128-188 54-249 59-840 63l-548 4 0-1330 0-1331 619 0c669 0 715-3 826-54 63-29 138-94 155-136 12-30 13-30 91-30l78 0 17 35c20 44 70 87 137 122 114 58 98 56 807 62l655 6 3 1313c2 1296 2 1314 22 1339 34 43 21 153-29 252-35 67-147 173-224 211-70 34-179 50-222 31z m171-190c72-42 154-148 154-200 0-12-47-64-125-138-69-65-129-119-133-121-4-1-60 51-125 116l-117 118 121 127c136 144 141 146 225 98z m-341-468l110-114-65-62c-36-33-110-104-165-157-385-367-609-575-618-575-7 0-54 44-106 97l-94 97 335 343c184 189 366 376 403 416 38 39 73 71 79 71 6-1 61-53 121-116z m-905-994c0-7-193-79-211-79-5 0 14 46 42 101l51 102 59-59c32-32 59-61 59-65z" <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
transform="translate(0,800) scale(0.1,-0.1)" </span>
/> </button>
<path </div>
fill="currentColor" </article>
d="M1897 4983c-9-8-9-2599-1-2630l6-23 787 0c432 0 791-4 797-8 6-4 18-21 27-38 24-46 102-113 170-144 169-79 482-78 640 1 96 49 141 90 193 177 6 9 97 11 822 11l782 1 0 1330 0 1330-85 0-85 0 0-1250 0-1250-773 0-772 0-18-51c-24-66-84-131-146-158-131-56-369-51-491 11-69 35-121 96-138 162l-8 31-775 5-774 5-2 1150c-2 633-3 1194-3 1248l0 97-73 0c-41 0-77-3-80-7z"
transform="translate(0,800) scale(0.1,-0.1)" <article
/> role="button"
<path tabindex="0"
fill="currentColor" class="home-card-base home-entry-item home-entry-item--3 group flex cursor-pointer flex-col justify-between rounded-xl border border-slate-200/80 bg-white/95 px-5 py-5 shadow-[0_4px_20px_rgba(15,23,42,0.06)] backdrop-blur-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_12px_32px_rgba(15,23,42,0.12)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-200"
d="M3391 3872c-261-2-347-5-353-15-4-6-8-38-8-69 0-47 4-59 19-68 13-6 214-10 584-10 474 0 566 2 576 14 7 9 11 40 9 78l-3 63-240 5c-132 3-395 4-584 2z" @click="openQuickCalc"
transform="translate(0,800) scale(0.1,-0.1)" @keydown.enter.prevent="openQuickCalc"
/> @keydown.space.prevent="openQuickCalc"
</svg> >
<svg v-else-if="card.icon === 'import'" viewBox="0 0 1024 1024" :class="card.iconClass" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> <div>
<path <div class="inline-flex h-11 w-11 items-center justify-center rounded-xl border border-amber-100 bg-amber-50/80 text-amber-600 shadow-sm transition-transform duration-200 group-hover:scale-105">
fill="currentColor" <Calculator class="h-5 w-5" />
d="M154.579478 1001.73913v-332.844521h89.043479V912.695652H912.695652V369.530435h-234.896695V111.304348H243.890087v349.184h-89.043478V22.26087h585.683478l261.431652 263.924869V1001.73913z m612.173913-721.252173h104.314435l-104.314435-105.293914z m-416.857043 411.469913l79.026087-79.026087H22.26087v-89.043479h406.661565L349.94087 444.861217l41.138087-41.22713 123.592347 123.592348 41.227131 41.182608-41.227131 41.138087-123.592347 123.592348z m123.013565-123.013566l0.489739-0.534261-0.489739-0.489739z" </div>
/> <h3 class="mt-4 text-base font-semibold text-slate-900">{{ t('home.cards.quickCalc') }}</h3>
</svg> <p class="mt-1.5 text-xs leading-5 text-slate-500">{{ t('home.cards.quickCalcDesc') }}</p>
</div>
<div class="mt-4 flex items-center text-xs font-medium text-slate-400 transition-colors group-hover:text-slate-600">
<span>{{ t('home.cards.developing') }}</span>
<svg class="ml-1 h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
</div>
</article>
<article
role="button"
tabindex="0"
class="home-card-base home-entry-item home-entry-item--4 group flex cursor-pointer flex-col justify-between rounded-xl border border-slate-200/80 bg-white/95 px-5 py-5 shadow-[0_4px_20px_rgba(15,23,42,0.06)] backdrop-blur-sm transition-all duration-200 hover:-translate-y-0.5 hover:shadow-[0_12px_32px_rgba(15,23,42,0.12)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-200"
@click="openRelatedFiles"
@keydown.enter.prevent="openRelatedFiles"
@keydown.space.prevent="openRelatedFiles"
>
<div>
<div class="inline-flex h-11 w-11 items-center justify-center rounded-xl border border-emerald-100 bg-emerald-50/80 shadow-sm transition-transform duration-200 group-hover:scale-105">
<svg <svg
v-else
viewBox="0 0 1024 1024" viewBox="0 0 1024 1024"
:class="card.iconClass" class="h-6 w-6"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
aria-hidden="true" aria-hidden="true"
> >
@ -676,37 +515,15 @@ watch(
/> />
</svg> </svg>
</div> </div>
<h3 class="mt-4 text-base font-semibold text-slate-900">{{ t(`home.cards.${card.key}`) }}</h3> <h3 class="mt-4 text-base font-semibold text-slate-900">{{ t('home.cards.relatedFiles') }}</h3>
<p class="mt-1.5 text-xs leading-5 text-slate-500">{{ t(`home.cards.${card.desc}`) }}</p> <p class="mt-1.5 text-xs leading-5 text-slate-500">{{ t('home.cards.relatedFilesDesc') }}</p>
</div> </div>
<div class="mt-4 flex items-center justify-between gap-2"> <div class="mt-4 flex items-center text-xs font-medium text-slate-400 transition-colors group-hover:text-slate-600">
<button <span>{{ t('home.cards.openRelatedFiles') }}</span>
v-if="card.showExistingAction && hasExistingProjects"
type="button"
class="cursor-pointer rounded-md border border-slate-200 px-2.5 py-1 text-xs font-medium text-slate-500 transition hover:border-slate-300 hover:bg-slate-50 hover:text-slate-700"
@click.stop="openExistingProjectDialog"
>
{{ t('home.cards.pickExisting') }}
</button>
<div class="flex items-center text-xs font-medium text-slate-400 transition-colors group-hover:text-slate-600">
<span>{{ t(`home.cards.${card.action}`) }}</span>
<svg class="ml-1 h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg> <svg class="ml-1 h-3.5 w-3.5 transition-transform group-hover:translate-x-0.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
</div> </div>
</div>
</article> </article>
</div> </div>
<div class="home-disclaimer mt-5 text-center">
<button
type="button"
class="inline-flex cursor-pointer items-center justify-center rounded-2xl border border-slate-300/60 bg-white/55 px-5 py-2 text-base font-semibold text-slate-700 shadow-[0_8px_24px_rgba(15,23,42,0.06)] backdrop-blur-sm underline decoration-slate-300 underline-offset-4 transition hover:border-slate-400/70 hover:bg-white/70 hover:text-slate-900 hover:decoration-slate-500"
@click="openDisclaimerPage"
>
{{ t('home.disclaimer.link') }}
</button>
<p class="mt-2 text-xs leading-5 text-slate-500">
{{ t('home.disclaimer.supportText') }}
</p>
</div>
</div> </div>
</div> </div>
@ -715,18 +532,22 @@ watch(
class="fixed inset-0 z-[90] flex items-center justify-center bg-black/45 p-4" class="fixed inset-0 z-[90] flex items-center justify-center bg-black/45 p-4"
@click.self="closeExistingProjectDialog" @click.self="closeExistingProjectDialog"
> >
<div class="w-full max-w-lg rounded-xl border bg-background shadow-2xl"> <div class="w-full max-w-lg rounded-3xl border border-slate-200/60 bg-white shadow-2xl">
<div class="flex items-center justify-between border-b px-5 py-4"> <div class="flex items-start justify-between border-b border-slate-100 px-6 pt-6 pb-4">
<div> <div>
<h3 class="text-base font-semibold text-foreground">{{ t('home.dialog.chooseExistingProject') }}</h3> <h3 class="text-xl font-bold text-[#1a1a1a]">{{ t('home.dialog.chooseExistingProject') }}</h3>
<p class="mt-1 text-sm text-muted-foreground">{{ t('home.dialog.chooseExistingProjectDesc') }}</p> <p class="mt-1.5 text-base text-[#666]">{{ t('home.dialog.chooseExistingProjectDesc') }}</p>
</div> </div>
<Button variant="ghost" size="icon" class="h-8 w-8" @click="closeExistingProjectDialog"> <button
<X class="h-4 w-4" /> type="button"
</Button> class="flex h-8 w-8 items-center justify-center rounded-full text-slate-400 transition hover:bg-slate-100 hover:text-slate-600"
@click="closeExistingProjectDialog"
>
<X class="h-5 w-5" />
</button>
</div> </div>
<div class="max-h-80 space-y-2 overflow-auto px-5 py-4"> <div class="max-h-80 space-y-3 overflow-auto px-6 py-5">
<div <div
v-if="!existingProjectLoading && existingProjects.length === 0" v-if="!existingProjectLoading && existingProjects.length === 0"
class="rounded-lg border border-dashed border-slate-200 bg-slate-50 px-3 py-5 text-center text-sm text-slate-500" class="rounded-lg border border-dashed border-slate-200 bg-slate-50 px-3 py-5 text-center text-sm text-slate-500"
@ -738,29 +559,29 @@ watch(
:key="project.id" :key="project.id"
type="button" type="button"
:disabled="isExistingProjectOpened(project.id)" :disabled="isExistingProjectOpened(project.id)"
class="flex w-full items-center justify-between rounded-lg border border-slate-200 px-3 py-2 text-left transition" class="flex w-full items-center justify-between rounded-xl border border-slate-200 bg-white px-4 py-3.5 text-left transition hover:border-slate-300 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-60"
:class="isExistingProjectOpened(project.id)
? 'cursor-not-allowed border-slate-200 bg-slate-100/80 opacity-70'
: 'cursor-pointer hover:border-slate-300 hover:bg-slate-50'"
@click="enterExistingProject(project.id)" @click="enterExistingProject(project.id)"
> >
<div class="min-w-0"> <div class="min-w-0 flex-1">
<div class="truncate text-sm font-medium text-slate-800"> <div class="text-base font-medium text-[#1a1a1a]">
{{ project.name }} {{ project.name }}
<span v-if="isExistingProjectOpened(project.id)" class="ml-1 text-xs text-slate-500">
{{ t('tab.toolbar.opened') }}
</span>
</div> </div>
<div class="mt-0.5 text-xs text-slate-500">{{ project.id }}</div> <div class="mt-1 text-sm text-[#888]">{{ project.id }}</div>
</div> </div>
<div class="shrink-0 pl-2 text-xs text-slate-500"> <div class="shrink-0 pl-4 text-sm text-[#888]">
{{ t('tab.toolbar.lastEdited', { time: formatProjectEditedTime(project.updatedAt) }) }} {{ t('tab.toolbar.lastEdited', { time: formatProjectEditedTime(project.updatedAt) }) }}
</div> </div>
</button> </button>
</div> </div>
<div class="flex items-center justify-end gap-2 border-t px-5 py-4"> <div class="flex items-center justify-end gap-3 border-t border-slate-100 px-6 pt-4 pb-6">
<Button variant="outline" @click="closeExistingProjectDialog">{{ t('common.cancel') }}</Button> <button
type="button"
class="cursor-pointer rounded-lg border border-slate-200 bg-white px-5 py-2.5 text-base font-medium text-[#666] transition hover:border-slate-300 hover:bg-slate-50"
@click="closeExistingProjectDialog"
>
{{ t('common.cancel') }}
</button>
</div> </div>
</div> </div>
</div> </div>
@ -770,61 +591,67 @@ watch(
class="fixed inset-0 z-[90] flex items-center justify-center bg-black/45 p-4" class="fixed inset-0 z-[90] flex items-center justify-center bg-black/45 p-4"
@click.self="closeProjectCalcDialog" @click.self="closeProjectCalcDialog"
> >
<div class="w-full max-w-md rounded-xl border bg-background shadow-2xl"> <div class="w-full max-w-lg rounded-3xl border border-slate-200/60 bg-white shadow-2xl">
<div class="flex items-center justify-between border-b px-5 py-4"> <div class="flex items-start justify-between border-b border-slate-100 px-6 pt-6 pb-4">
<div> <div>
<h3 class="text-base font-semibold text-foreground">{{ t('home.dialog.newProject') }}</h3> <h3 class="text-xl font-bold text-[#1a1a1a]">{{ t('home.dialog.newProject') }}</h3>
<p class="mt-1 text-sm text-muted-foreground">{{ t('home.dialog.chooseIndustryDesc') }}</p> <p class="mt-1.5 text-base text-[#666]">选择工程行业后进入项目计算页面</p>
</div> </div>
<Button variant="ghost" size="icon" class="h-8 w-8" @click="closeProjectCalcDialog"> <button
<X class="h-4 w-4" /> type="button"
</Button> class="flex h-8 w-8 items-center justify-center rounded-full text-slate-400 transition hover:bg-slate-100 hover:text-slate-600"
@click="closeProjectCalcDialog"
>
<X class="h-5 w-5" />
</button>
</div> </div>
<div class="space-y-4 px-5 py-4"> <div class="px-6 pt-5 pb-4">
<label class="block space-y-2"> <h4 class="mb-3 text-base font-semibold text-[#1a1a1a]">{{ t('home.dialog.industry') }}</h4>
<span class="text-sm font-medium text-foreground">{{ t('home.dialog.industry') }}</span> <div class="space-y-3">
<SelectRoot v-model="projectIndustry"> <button
<SelectTrigger
class="inline-flex h-11 w-full items-center justify-between rounded-xl border border-border/70 bg-[linear-gradient(180deg,rgba(248,250,252,0.98),rgba(241,245,249,0.92))] px-4 text-sm text-foreground shadow-sm outline-none transition hover:border-border focus-visible:ring-2 focus-visible:ring-slate-300"
>
<span :class="projectIndustryLabel ? 'text-foreground' : 'text-muted-foreground'">
{{ projectIndustryLabel || t('home.dialog.selectIndustry') }}
</span>
<SelectIcon as-child>
<ChevronDown class="h-4 w-4 text-muted-foreground" />
</SelectIcon>
</SelectTrigger>
<SelectPortal>
<SelectContent
:side-offset="6"
position="popper"
class="z-[120] w-[var(--reka-select-trigger-width)] overflow-hidden rounded-xl border border-border bg-popover text-popover-foreground shadow-xl"
>
<SelectViewport class="p-1">
<SelectItem
v-for="item in industryTypeList" v-for="item in industryTypeList"
:key="`project-${item.id}`" :key="`project-${item.id}`"
:value="String(item.id)" type="button"
class="relative flex h-9 w-full cursor-default select-none items-center rounded-md pl-3 pr-8 text-sm outline-none data-[highlighted]:bg-muted data-[highlighted]:text-foreground data-[state=checked]:bg-slate-100" :class="[
'flex w-full items-center justify-between rounded-xl border px-4 py-3.5 text-left transition',
projectIndustry === String(item.id)
? 'border-[#1a1a1a] bg-white text-[#1a1a1a] shadow-sm'
: 'border-slate-200 bg-white text-[#1a1a1a] hover:border-slate-300'
]"
@click="projectIndustry = String(item.id)"
> >
<SelectItemText>{{ getIndustryDisplayName(item.id, locale) }}</SelectItemText> <span class="text-base">{{ getIndustryDisplayName(item.id, locale) }}</span>
<SelectItemIndicator class="absolute right-2 inline-flex items-center text-slate-700"> <div
<Check class="h-4 w-4" /> :class="[
</SelectItemIndicator> 'flex h-5 w-5 items-center justify-center rounded-md border transition',
</SelectItem> projectIndustry === String(item.id)
</SelectViewport> ? 'border-[#1a1a1a] bg-[#1a1a1a] text-white'
</SelectContent> : 'border-slate-300 bg-white'
</SelectPortal> ]"
</SelectRoot> >
</label> <Check v-if="projectIndustry === String(item.id)" class="h-3.5 w-3.5" />
</div>
</button>
</div>
</div> </div>
<div class="flex items-center justify-end gap-2 border-t px-5 py-4"> <div class="flex items-center justify-end gap-3 border-t border-slate-100 px-6 pt-4 pb-6">
<Button variant="outline" @click="closeProjectCalcDialog">{{ t('common.cancel') }}</Button> <button
<Button :disabled="projectSubmitting || !projectIndustry" @click="confirmProjectCalc"> type="button"
class="cursor-pointer rounded-lg border border-slate-200 bg-white px-5 py-2.5 text-base font-medium text-[#666] transition hover:border-slate-300 hover:bg-slate-50"
@click="closeProjectCalcDialog; openExistingProjectDialog()"
>
{{ t('home.cards.pickExisting') }}
</button>
<button
type="button"
:disabled="projectSubmitting || !projectIndustry"
class="cursor-pointer rounded-lg bg-[#1a1a1a] px-5 py-2.5 text-base font-medium text-white transition hover:bg-[#2a2a2a] disabled:cursor-not-allowed disabled:opacity-50"
@click="confirmProjectCalc"
>
{{ projectSubmitting ? t('home.dialog.entering') : t('home.dialog.enterProjectCalc') }} {{ projectSubmitting ? t('home.dialog.entering') : t('home.dialog.enterProjectCalc') }}
</Button> </button>
</div> </div>
</div> </div>
</div> </div>
@ -870,17 +697,11 @@ watch(
.home-entry-item { .home-entry-item {
animation: fade-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) both; animation: fade-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) both;
} }
.home-slogan-row {
animation: fade-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) 0.6s both;
}
.home-entry-item--1 { animation-delay: 0.2s; } .home-entry-item--1 { animation-delay: 0.2s; }
.home-entry-item--2 { animation-delay: 0.3s; } .home-entry-item--2 { animation-delay: 0.3s; }
.home-entry-item--3 { animation-delay: 0.4s; } .home-entry-item--3 { animation-delay: 0.4s; }
.home-entry-item--4 { animation-delay: 0.5s; } .home-entry-item--4 { animation-delay: 0.5s; }
.home-entry-item--5 { animation-delay: 0.6s; } .home-entry-item--5 { animation-delay: 0.6s; }
.home-disclaimer {
animation: fade-up 0.45s cubic-bezier(0.22, 1, 0.36, 1) 0.6s both;
}
@keyframes hero-in { @keyframes hero-in {
from { opacity: 0; transform: translateX(-20px) scale(0.97); } from { opacity: 0; transform: translateX(-20px) scale(0.97); }

View File

@ -17,7 +17,6 @@ import { getIndustryMajorEntry } from '@/lib/pricingScaleCalc'
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee' import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
import { QUICK_PROJECT_INFO_KEY } from '@/lib/workspace' import { QUICK_PROJECT_INFO_KEY } from '@/lib/workspace'
import { initializeProjectFactorStates } from '@/lib/projectWorkspace' import { initializeProjectFactorStates } from '@/lib/projectWorkspace'
import { resolveServicePricingCapabilities } from '@/lib/servicePricing'
const props = defineProps<{ const props = defineProps<{
contractId: string contractId: string
@ -37,9 +36,8 @@ type DictFactorItem = {
defCoe: number | null defCoe: number | null
hasCost?: boolean | null hasCost?: boolean | null
hasArea?: boolean | null hasArea?: boolean | null
enableInvestScale?: boolean | null scale?: boolean | null
enableLandScale?: boolean | null onlyCostScale?: boolean | null
investScaleSingleTotal?: boolean | null
} }
type QuickCalcScaleMode = 'cost' | 'area' type QuickCalcScaleMode = 'cost' | 'area'
@ -71,9 +69,8 @@ const mapDictItemToFactorItem = (id: string, item: Record<string, unknown> | und
defCoe: typeof item.defCoe === 'number' ? item.defCoe : null, defCoe: typeof item.defCoe === 'number' ? item.defCoe : null,
hasCost: item.hasCost === true, hasCost: item.hasCost === true,
hasArea: item.hasArea === true, hasArea: item.hasArea === true,
enableInvestScale: item.enableInvestScale === true, scale: item.scale === true,
enableLandScale: item.enableLandScale === true, onlyCostScale: item.onlyCostScale === true
investScaleSingleTotal: item.investScaleSingleTotal === true
} }
} }
@ -198,22 +195,10 @@ const preferLandScaleForDualMajor = computed(() => majorSupportsCostScale.value
const workEnvCoefficient = computed(() => const workEnvCoefficient = computed(() =>
parseNumberOrNull(workEnvFactor.value, { sanitize: true, precision: 3 }) parseNumberOrNull(workEnvFactor.value, { sanitize: true, precision: 3 })
) )
const consultPricingCapabilities = computed(() => const consultSupportsScale = computed(() => selectedConsultDictItem.value?.scale === true)
resolveServicePricingCapabilities(selectedConsultDictItem.value, { const consultOnlySupportsCostScale = computed(() => selectedConsultDictItem.value?.onlyCostScale === true)
investScaleEnabled: false,
landScaleEnabled: false,
investScaleSingleTotal: false,
workloadEnabled: false,
hourlyEnabled: false
})
)
const consultSupportsScale = computed(() => consultPricingCapabilities.value.investScaleEnabled || consultPricingCapabilities.value.landScaleEnabled)
const consultOnlySupportsCostScale = computed(() =>
consultPricingCapabilities.value.investScaleEnabled
&& !consultPricingCapabilities.value.landScaleEnabled
)
const canUseInvestScale = computed(() => const canUseInvestScale = computed(() =>
consultPricingCapabilities.value.investScaleEnabled && consultSupportsScale.value &&
hasResolvedMajor.value && hasResolvedMajor.value &&
( (
consultOnlySupportsCostScale.value || consultOnlySupportsCostScale.value ||
@ -221,8 +206,9 @@ const canUseInvestScale = computed(() =>
) )
) )
const canUseLandScale = computed(() => const canUseLandScale = computed(() =>
consultPricingCapabilities.value.landScaleEnabled && consultSupportsScale.value &&
hasResolvedMajor.value && hasResolvedMajor.value &&
!consultOnlySupportsCostScale.value &&
majorSupportsLandScale.value majorSupportsLandScale.value
) )
const investScalePlaceholder = computed(() => { const investScalePlaceholder = computed(() => {
@ -707,8 +693,8 @@ watch(canUseLandScale, enabled => {
.quick-calc-layout { .quick-calc-layout {
display: grid; display: grid;
grid-template-columns: minmax(0, 1.02fr) minmax(420px, 1.34fr); grid-template-columns: minmax(0, 1.45fr) minmax(340px, 0.95fr);
gap: 12px; gap: 10px;
height: 100%; height: 100%;
min-height: 0; min-height: 0;
} }
@ -724,34 +710,6 @@ watch(canUseLandScale, enabled => {
overflow: hidden; overflow: hidden;
} }
.quick-calc-panel--form {
position: relative;
border-color: var(--qc-border);
background:
radial-gradient(circle at 84% -12%, color-mix(in srgb, hsl(var(--destructive)) 22%, transparent) 0%, transparent 46%),
radial-gradient(circle at -8% 108%, color-mix(in srgb, hsl(var(--destructive)) 14%, transparent) 0%, transparent 44%),
linear-gradient(160deg, color-mix(in srgb, var(--card) 95%, white) 0%, color-mix(in srgb, var(--card) 86%, var(--muted)) 100%);
box-shadow:
0 22px 44px color-mix(in srgb, hsl(var(--destructive)) 12%, transparent),
0 12px 28px color-mix(in srgb, var(--foreground) 10%, transparent);
}
.quick-calc-panel--form::before {
content: '';
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: linear-gradient(
90deg,
color-mix(in srgb, hsl(var(--destructive)) 58%, white) 0%,
color-mix(in srgb, hsl(var(--destructive)) 26%, transparent) 35%,
transparent 100%
);
pointer-events: none;
}
.quick-calc-panel__header { .quick-calc-panel__header {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@ -1194,7 +1152,7 @@ watch(canUseLandScale, enabled => {
.quick-calc-panel--form .quick-calc-form { .quick-calc-panel--form .quick-calc-form {
overflow: auto; overflow: auto;
padding: 10px; padding: 8px;
font-size: 14px; font-size: 14px;
} }
@ -1221,11 +1179,8 @@ watch(canUseLandScale, enabled => {
.quick-calc-panel--form .quick-calc-form-section { .quick-calc-panel--form .quick-calc-form-section {
gap: 6px; gap: 6px;
padding: 10px 12px; padding: 8px 10px;
border-radius: 12px; border-radius: 12px;
background:
linear-gradient(180deg, color-mix(in srgb, white 30%, transparent) 0%, transparent 100%),
color-mix(in srgb, var(--card) 94%, var(--muted));
} }
.quick-calc-form-section__header { .quick-calc-form-section__header {
@ -1366,11 +1321,7 @@ watch(canUseLandScale, enabled => {
} }
.quick-calc-panel--form .quick-calc-field__readonly--emphasis { .quick-calc-panel--form .quick-calc-field__readonly--emphasis {
font-size: 18px; font-size: 14px;
font-weight: 800;
color: color-mix(in srgb, hsl(var(--destructive)) 88%, var(--foreground));
letter-spacing: 0.015em;
text-shadow: 0 1px 0 color-mix(in srgb, white 65%, transparent);
} }
.quick-calc-form-hint { .quick-calc-form-hint {

View File

@ -16,12 +16,10 @@ import { useI18n } from 'vue-i18n'
import TypeLine from '@/layout/typeLine.vue' import TypeLine from '@/layout/typeLine.vue'
import MethodUnavailableNotice from '@/features/shared/components/MethodUnavailableNotice.vue' import MethodUnavailableNotice from '@/features/shared/components/MethodUnavailableNotice.vue'
import ScaleFormulaReadonlyPane from '@/features/pricing/components/ScaleFormulaReadonlyPane.vue' import ScaleFormulaReadonlyPane from '@/features/pricing/components/ScaleFormulaReadonlyPane.vue'
import { resolveServicePricingCapabilities } from '@/lib/servicePricing'
interface ServiceMethodType { interface ServiceMethodType {
enableInvestScale?: boolean | null scale?: boolean | null
enableLandScale?: boolean | null onlyCostScale?: boolean | null
investScaleSingleTotal?: boolean | null
amount?: boolean | null amount?: boolean | null
workDay?: boolean | null workDay?: boolean | null
} }
@ -42,19 +40,20 @@ interface PricingCategoryItem {
component: Component component: Component
} }
const resolveMethodEnabled = (value: boolean | null | undefined, fallback = true) =>
typeof value === 'boolean' ? value : fallback
const methodAvailability = computed(() => { const methodAvailability = computed(() => {
const capability = resolveServicePricingCapabilities(props.type, { const scale = resolveMethodEnabled(props.type?.scale, true)
investScaleEnabled: true, const onlyCostScale = resolveMethodEnabled(props.type?.onlyCostScale, false)
landScaleEnabled: true, const amount = resolveMethodEnabled(props.type?.amount, true)
investScaleSingleTotal: false, const workDay = resolveMethodEnabled(props.type?.workDay, true)
workloadEnabled: true,
hourlyEnabled: true
})
return { return {
investmentScale: capability.investScaleEnabled, investmentScale: scale,
landScale: capability.landScaleEnabled, landScale: scale && !onlyCostScale,
workload: capability.workloadEnabled, workload: amount,
hourly: capability.hourlyEnabled hourly: workDay
} }
}) })
@ -72,6 +71,7 @@ const createPricingPane = (name: string) =>
return () => h(AsyncPricingView, { return () => h(AsyncPricingView, {
contractId: props.contractId, contractId: props.contractId,
contractName: props.contractName,
serviceId: props.serviceId, serviceId: props.serviceId,
projectInfoKey: props.projectInfoKey projectInfoKey: props.projectInfoKey
}) })
@ -93,6 +93,7 @@ const investmentScaleView = createPricingPane('InvestmentScalePricingPane')
const landScaleView = createPricingPane('LandScalePricingPane') const landScaleView = createPricingPane('LandScalePricingPane')
const workloadView = createPricingPane('WorkloadPricingPane') const workloadView = createPricingPane('WorkloadPricingPane')
const hourlyView = createPricingPane('HourlyPricingPane') const hourlyView = createPricingPane('HourlyPricingPane')
const otherService = createPricingPane('OtherService')
const createScaleFormulaPane = ( const createScaleFormulaPane = (
method: 'investScale' | 'landScale', method: 'investScale' | 'landScale',
@ -131,7 +132,8 @@ const workContentPane = markRaw(
contractId: props.contractId, contractId: props.contractId,
projectInfoKey: props.projectInfoKey, projectInfoKey: props.projectInfoKey,
serviceId: props.serviceId, serviceId: props.serviceId,
dictMode: 'service' dictMode: 'service',
"show-no-column": true
}) })
} }
}) })
@ -161,7 +163,7 @@ const pricingCategories = computed<PricingCategoryItem[]>(() => [
label: t('zxFwView.categories.investmentScale'), label: t('zxFwView.categories.investmentScale'),
component: methodAvailability.value.investmentScale ? investmentScaleView : investmentScaleUnavailableView component: methodAvailability.value.investmentScale ? investmentScaleView : investmentScaleUnavailableView
}, },
{ /*{
key: 'investment-scale-formula', key: 'investment-scale-formula',
label: t('zxFwView.categories.investmentScaleFormula'), label: t('zxFwView.categories.investmentScaleFormula'),
component: methodAvailability.value.investmentScale ? investmentScaleFormulaView : investmentScaleUnavailableView component: methodAvailability.value.investmentScale ? investmentScaleFormulaView : investmentScaleUnavailableView
@ -175,7 +177,7 @@ const pricingCategories = computed<PricingCategoryItem[]>(() => [
key: 'land-scale-formula', key: 'land-scale-formula',
label: t('zxFwView.categories.landScaleFormula'), label: t('zxFwView.categories.landScaleFormula'),
component: methodAvailability.value.landScale ? landScaleFormulaView : landScaleUnavailableView component: methodAvailability.value.landScale ? landScaleFormulaView : landScaleUnavailableView
}, },*/
{ {
key: 'workload-method', key: 'workload-method',
label: t('zxFwView.categories.workload'), label: t('zxFwView.categories.workload'),
@ -191,6 +193,11 @@ const pricingCategories = computed<PricingCategoryItem[]>(() => [
label: t('zxFwView.categories.workContent'), label: t('zxFwView.categories.workContent'),
component: workContentPane component: workContentPane
}, },
{
key: 'other-service',
label: t('zxFwView.categories.otherService'),
component: otherService
},
]) ])
const defaultCategory = computed(() => { const defaultCategory = computed(() => {

View File

@ -53,10 +53,6 @@ const { t, locale } = useI18n()
const DEFAULT_PROJECT_NAME = t('xmInfo.defaultProjectName') const DEFAULT_PROJECT_NAME = t('xmInfo.defaultProjectName')
const DEFAULT_DESC = t('xmInfo.defaultDesc') const DEFAULT_DESC = t('xmInfo.defaultDesc')
const INDUSTRY_HINT_TEXT = computed(() => t('xmInfo.industryHint')) const INDUSTRY_HINT_TEXT = computed(() => t('xmInfo.industryHint'))
const REPORT_CONTENT_HINT_TEXT = computed(() => t('xmInfo.reportContentHint'))
const OTHER_DESC_HINT_TEXT = computed(() => t('xmInfo.otherDescHint'))
const FIELD_HINT_BUTTON_CLASS = 'inline-flex h-5 w-5 items-center justify-center text-muted-foreground/90 transition hover:text-foreground'
const FIELD_HINT_ICON_CLASS = 'h-4 w-4'
const getTodayDateString = () => { const getTodayDateString = () => {
const now = new Date() const now = new Date()
const year = String(now.getFullYear()) const year = String(now.getFullYear())
@ -235,21 +231,7 @@ onMounted(async () => {
<div v-else class="rounded-xl border bg-card p-4 shadow-sm shrink-0 md:p-5"> <div v-else class="rounded-xl border bg-card p-4 shadow-sm shrink-0 md:p-5">
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4"> <div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<div class="md:col-span-2 xl:col-span-4"> <div class="md:col-span-2 xl:col-span-4">
<div class="flex items-center gap-1.5">
<label class="block text-sm font-medium text-foreground">{{ t('xmInfo.fields.projectName') }}</label> <label class="block text-sm font-medium text-foreground">{{ t('xmInfo.fields.projectName') }}</label>
<TooltipRoot>
<TooltipTrigger as-child>
<button
type="button"
:aria-label="t('xmInfo.reportContentHintAria')"
:class="FIELD_HINT_BUTTON_CLASS"
>
<CircleHelp :class="FIELD_HINT_ICON_CLASS" />
</button>
</TooltipTrigger>
<TooltipContent side="top">{{ REPORT_CONTENT_HINT_TEXT }}</TooltipContent>
</TooltipRoot>
</div>
<input <input
v-model="projectName" v-model="projectName"
type="text" type="text"
@ -268,9 +250,9 @@ onMounted(async () => {
<button <button
type="button" type="button"
:aria-label="t('xmInfo.industryHintAria')" :aria-label="t('xmInfo.industryHintAria')"
:class="FIELD_HINT_BUTTON_CLASS" class="inline-flex h-5 w-5 items-center justify-center text-muted-foreground/90 transition hover:text-foreground"
> >
<CircleHelp :class="FIELD_HINT_ICON_CLASS" /> <CircleHelp class="h-5 w-5" />
</button> </button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top">{{ INDUSTRY_HINT_TEXT }}</TooltipContent> <TooltipContent side="top">{{ INDUSTRY_HINT_TEXT }}</TooltipContent>
@ -445,25 +427,10 @@ onMounted(async () => {
<div class="md:col-span-2 xl:col-span-4"> <div class="md:col-span-2 xl:col-span-4">
<div class="flex items-center gap-1.5">
<label class="block text-sm font-medium text-foreground">{{ t('xmInfo.fields.desc') }}</label> <label class="block text-sm font-medium text-foreground">{{ t('xmInfo.fields.desc') }}</label>
<TooltipRoot>
<TooltipTrigger as-child>
<button
type="button"
:aria-label="t('xmInfo.otherDescHintAria')"
:class="FIELD_HINT_BUTTON_CLASS"
>
<CircleHelp :class="FIELD_HINT_ICON_CLASS" />
</button>
</TooltipTrigger>
<TooltipContent side="top">{{ OTHER_DESC_HINT_TEXT }}</TooltipContent>
</TooltipRoot>
</div>
<textarea <textarea
v-model="desc" v-model="desc"
rows="4" rows="4"
:placeholder="t('xmInfo.placeholders.desc')"
class="mt-2 w-full rounded-lg border bg-background px-4 py-2 text-sm outline-none ring-offset-background shadow-sm transition placeholder:text-muted-foreground/70 focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-ring resize-none" class="mt-2 w-full rounded-lg border bg-background px-4 py-2 text-sm outline-none ring-offset-background shadow-sm transition placeholder:text-muted-foreground/70 focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-ring resize-none"
/> />
</div> </div>

View File

@ -26,8 +26,8 @@ const majorFactorView = markRaw(
const xmCategories = computed(() => [ const xmCategories = computed(() => [
{ key: 'info', label: t('xmCard.categories.info'), component: infoView }, { key: 'info', label: t('xmCard.categories.info'), component: infoView },
{ key: 'scale-info', label: t('xmCard.categories.scaleInfo'), component: scaleInfoView }, { key: 'scale-info', label: t('xmCard.categories.scaleInfo'), component: scaleInfoView },
{ key: 'consult-category-factor', label: t('xmCard.categories.consultCategoryFactor'), component: consultCategoryFactorView }, // { key: 'consult-category-factor', label: t('xmCard.categories.consultCategoryFactor'), component: consultCategoryFactorView },
{ key: 'major-factor', label: t('xmCard.categories.majorFactor'), component: majorFactorView }, // { key: 'major-factor', label: t('xmCard.categories.majorFactor'), component: majorFactorView },
{ key: 'contract', label: t('xmCard.categories.contract'), component: htView } { key: 'contract', label: t('xmCard.categories.contract'), component: htView }
]) ])
</script> </script>

View File

@ -14,49 +14,32 @@ export const enUS = {
countdown: 'This page will try to close automatically in {seconds} seconds. You can open another project in a new tab first.', countdown: 'This page will try to close automatically in {seconds} seconds. You can open another project in a new tab first.',
opened: '(Opened)', opened: '(Opened)',
lastEdited: 'Last edited: {time}', lastEdited: 'Last edited: {time}',
openDefault: 'Back to Home', openDefault: 'Open Default Project',
createAndOpen: 'Create and Open' createAndOpen: 'Create and Open'
} }
}, },
home: { home: {
title: 'Yue Gong Xue Biao Zi [2026] No. 5\nSpecification for Budget Preparation of Cost Consulting Services for Transportation Engineering', title: 'Engineering Calculation Entry',
subtitle: 'Project Budget · Quick Calc · Import Data · Related Files', subtitle: 'Project Budget · Quick Calc · Import Data',
projectCalcTab: 'Project Calculation', projectCalcTab: 'Project Calculation',
quickCalcTab: 'Quick Calculation', quickCalcTab: 'Quick Calculation',
cards: { cards: {
heroTitle: 'ZonghuiyiBilling Made Simple', heroTitle: 'One-Click Smart Budget',
heroTitles: [ heroSubTitle: 'Accelerate standards adoption',
'Zonghuiyi | Billing Made Simple', heroDesc: 'Cost consulting fee calculator for transport construction projects',
'Zonghuiyi | No Late Nights for Billing',
'Zonghuiyi | Effortless Billing'
],
heroSubTitle: '',
heroDesc: 'Instant Pricing, Instant Results, Leave Time for Creation',
heroDescs: [
'Instant Pricing, Instant Results, Leave Time for Creation.',
'Smart Pricing, One Click to Results, Leave Time for Creation.',
'Smart Pricing, One Click to Generate, Leave Time for Creation.',
'Zonghuiyi. Simple, No Late Nights, Effortless.'
],
projectBudget: 'Project Budget', projectBudget: 'Project Budget',
projectBudgetDesc: 'For full project-level calculation across multiple contracts with import/export support', projectBudgetDesc: 'For full project-level calculation across multiple contracts with import/export support',
quickCalc: 'Quick Calc', quickCalc: 'Quick Calc',
quickCalcDesc: 'Suitable for single service trial, select industry, consultation type, engineering specialty, input base number and get results in seconds', quickCalcDesc: 'Suitable for single service trial, select industry, consultation type, engineering specialty, input base number and get results in seconds',
importData: 'Import Data', importData: 'Import Data',
importDataDesc: 'Import a ".zw" package and create a new project automatically to restore the data without overriding existing projects', importDataDesc: 'Import a ".zw" package and create a new project automatically to restore the data without overriding existing projects',
relatedFiles: 'Related Files',
relatedFilesDesc: 'View, print, and download the fee documents, related bidding documents, and contract templates used by this calculator',
viewFiles: 'View Files',
enter: 'Enter', enter: 'Enter',
developing: 'In Development',
pickFile: 'Choose File', pickFile: 'Choose File',
pickExisting: 'Choose Existing', pickExisting: 'Choose Existing',
openFileSystem: 'Open File System', relatedFiles: 'Related Files',
file: 'File System', relatedFilesDesc: 'View related fee documents, tender files, contract files, service content, and work requirements',
fileDataDesc: 'The file system provides related fee documents, bidding documents, contract documents, service contents, and work requirements' openRelatedFiles: 'Open Page'
},
disclaimer: {
link: 'View Disclaimer',
supportText: 'This calculator is provided with free technical support by Zhongwei Engineering Consulting Co., Ltd.'
}, },
dialog: { dialog: {
newProject: 'New Project', newProject: 'New Project',
@ -73,47 +56,6 @@ export const enUS = {
noProjectYet: 'No project available. Create a new project first.' noProjectYet: 'No project available. Create a new project first.'
} }
}, },
disclaimerPage: {
documentTitle: 'Budget Tool Disclaimer',
eyebrow: 'DISCLAIMER',
pageTitle: 'Disclaimer for Budget Preparation Tool under (T/GDHS017-2026) Cost Consulting Services for Transportation Engineering as Specifications for Budget Compilation',
lastUpdatedLabel: 'Last updated: ',
lastUpdatedValue: 'April 16, 2026',
leadText: 'Thank you for using the cost consulting budget preparation tool for the Specification for Budget Preparation of Cost Consulting Services for Transportation Engineering (T/GDHS 017-2026) provided on this website. Before using this tool, please read the following disclaimer carefully. By continuing to use this tool, you acknowledge that you have read, understood, and agreed to all terms of this disclaimer.',
sections: {
standardBasisTitle: '1. Standard Basis',
standardBasisP1: '1.1 This tool is based on the methodology set out in the group standard Specification for Budget Preparation of Cost Consulting Services for Transportation Engineering (T/GDHS 017-2026) issued by the Guangdong Province Highway Society "GDHS". Users must independently determine whether the standard applies to their specific projects and the regulatory requirements of their local authorities.',
standardBasisP2: '1.2 The standard version used by this tool is indicated on the interface as T/GDHS 017-2026. Should the standard be subsequently revised, supplemented, or replaced, this tool may not be updated in a timely manner. Users must independently confirm that the referenced version remains the latest valid version before use.',
standardBasisP3: '1.3 The calculation results generated by this tool are based on the budgeting methods, cost structure, and preparation rules in specified the standard. Requirements and calculation approaches may still vary across regions and project owners. This tool does not guarantee that its results will meet the review requirements of any specific project or authority.',
referenceOnlyTitle: '2. Results Are for Reference Only',
referenceOnlyP1: 'All results generated by this tool, including but not limited to values, breakdowns, summaries and preparation instructions, are produced automatically based on the parameters you provide (such as engineering industry, project scale, consulting category, engineering discipline, job responsibilities, adjustment factor, etc.) and the mathematical models and formulas in the standard. They are for reference purposes only and do not constitute professional advice or any official or mandatory basis for budget approval.',
accuracyTitle: '3. No Warranties of Accuracy or Completeness',
accuracyP1: 'Although we make reasonable efforts to ensure the tool\'s availability, it is provided on an as-is basis without any express or implied warranty. We do not guarantee that the results will always be accurate, error-free, or complete. Differences may arise due to input errors, formula selection, rounding, or system delays.',
riskTitle: '4. Users Bear Their Own Risks',
riskP1: 'You should independently assess the reliability of the calculation results and bear all risks and responsibilities arising from any decisions made based thereon. The output of this tool should not replace professional calculation or review. Before making important decisions, you should consult professionals holding a registered certificate in transportation engineering cost or perform manual verification based on the specific project context.',
liabilityTitle: '5. Limitation of Liability',
liabilityP1: 'To the fullest extent permitted by applicable law, the developers, administrators, publishers, and their affiliates shall not be liable for any direct, indirect, accidental, special, or consequential losses arising from the use of or inability to use this tool, even if advised of the possibility of such losses.',
liabilityP2: 'In particular, if any cost consulting enterprise or individual issues deliverables from the results generated by this tool, the issuing party bears sole responsibility for their quality. This tool assumes no responsibility for the accuracy, compliance, or disputes arising from any third-party deliverables.',
interruptionTitle: '6. Service Interruption and Changes',
interruptionP1: 'We reserve the right to modify, suspend, or terminate part or all of this tool at any time, with or without notice. We are not responsible for unavailability, data loss, or changes in calculation results caused by maintenance, network failures, third-party service interruptions, or updates to the standard.',
externalTitle: '7. External Links and Third-Party Content',
externalP1: 'If this tool references or links to the National Group Standards Information Platform, the Guangdong Highway Society website, or other third-party websites, such links are provided only for convenience and do not imply endorsement of their accuracy, timeliness, or completeness. We assume no responsibility for any information, services, or content provided by third parties.',
lawTitle: '8. Governing Law',
lawP1: 'This disclaimer shall be governed by the laws of the People\'s Republic of China. If any provision of this disclaimer is held invalid or unenforceable, the remaining provisions shall remain in effect.'
},
confirm: {
title: 'User Confirmation',
desc1: 'I have read, understood, and agree to the full contents of this disclaimer.',
desc2: 'You must check the box before continuing to use this tool.',
checkbox: 'I have read, understood, and agree to the full contents of this disclaimer.',
continue: 'Agree and Continue',
hint: 'Once checked, your acceptance will be recorded in this browser so the same restricted entry will not prompt you again.'
},
actions: {
back: 'Back to Home',
switchLocale: 'Switch Language'
}
},
tab: { tab: {
toolbar: { toolbar: {
light: 'Light', light: 'Light',
@ -227,7 +169,9 @@ export const enUS = {
toast: { toast: {
export: 'Export Report', export: 'Export Report',
success: 'Export Success', success: 'Export Success',
failed: 'Export Failed' failed: 'Export Failed',
saveSuccess: 'Save Success',
saveFailed: 'Save Failed'
}, },
messages: { messages: {
defaultProjectLabel: 'Default Project', defaultProjectLabel: 'Default Project',
@ -361,9 +305,7 @@ export const enUS = {
reserveTitle: 'Reserve Fee' reserveTitle: 'Reserve Fee'
}, },
htInfo: { htInfo: {
scaleDetailTitle: 'Contract Scale Details', scaleDetailTitle: 'Contract Scale Details'
scaleDetailHint: 'When the scale data in this table differs from the project scale data, pricing will use this table.',
scaleDetailHintAria: 'Contract scale details hint'
}, },
htFeeRate: { htFeeRate: {
baseLabel: 'Base (total budget of all service fees)', baseLabel: 'Base (total budget of all service fees)',
@ -378,7 +320,7 @@ export const enUS = {
title: 'Consulting Service Details', title: 'Consulting Service Details',
warning: 'Please review and adjust recommended limits/special values in the specification, then update final fee if needed.', warning: 'Please review and adjust recommended limits/special values in the specification, then update final fee if needed.',
editTabTitle: 'Service Edit-{name}', editTabTitle: 'Service Edit-{name}',
subtotal: 'Total', subtotal: 'Subtotal',
edit: 'Edit', edit: 'Edit',
resetDefault: 'Reset', resetDefault: 'Reset',
delete: 'Remove', delete: 'Remove',
@ -428,7 +370,7 @@ export const enUS = {
} }
}, },
htFeeGrid: { htFeeGrid: {
subtotal: 'Total', subtotal: 'Subtotal',
currentRow: 'Current Row', currentRow: 'Current Row',
unnamed: 'Unnamed', unnamed: 'Unnamed',
edit: 'Edit', edit: 'Edit',
@ -460,8 +402,6 @@ export const enUS = {
}, },
serviceSelector: { serviceSelector: {
title: 'Select Services', title: 'Select Services',
titleHint: 'Some selectable services have been added beyond those listed in the specification. These additions do not conflict with the specification and are only included to support fee calculation under the specification.',
titleHintAria: 'Select services hint',
clear: 'Clear', clear: 'Clear',
empty: 'No services' empty: 'No services'
}, },
@ -497,7 +437,7 @@ export const enUS = {
} }
}, },
htFeeDetail: { htFeeDetail: {
subtotal: 'Total', subtotal: 'Subtotal',
currentRow: 'Current Row', currentRow: 'Current Row',
clickToInput: 'Click to input', clickToInput: 'Click to input',
addRow: 'Add Row', addRow: 'Add Row',
@ -636,7 +576,7 @@ export const enUS = {
pricingScale: { pricingScale: {
totalInvestmentByIndustry: '{industryName} Total Investment', totalInvestmentByIndustry: '{industryName} Total Investment',
totalInvestment: 'Total Investment', totalInvestment: 'Total Investment',
clickToInput: 'Click to input', clickToInput: 'Optional, enter manually, numeric, 4 decimals',
projectLabel: 'Project {index}', projectLabel: 'Project {index}',
columns: { columns: {
investAmount: 'Cost Amount (10k CNY)', investAmount: 'Cost Amount (10k CNY)',
@ -649,7 +589,7 @@ export const enUS = {
consultCategoryFactor: 'Consult Category Factor', consultCategoryFactor: 'Consult Category Factor',
majorFactor: 'Major Factor', majorFactor: 'Major Factor',
workStageFactor: 'Work Stage Factor (Draft/Review)', workStageFactor: 'Work Stage Factor (Draft/Review)',
workRatio: 'Service Budget Composition Ratio and Quantity Ratio', workRatio: 'Work Ratio (%)',
total: 'Total', total: 'Total',
remark: 'Remark', remark: 'Remark',
majorGroup: 'Major Code and Major Name' majorGroup: 'Major Code and Major Name'
@ -658,8 +598,7 @@ export const enUS = {
resetInvestAmount: 'Click ↻ to restore default cost amount for this column', resetInvestAmount: 'Click ↻ to restore default cost amount for this column',
resetLandArea: 'Click ↻ to restore default land area for this column', resetLandArea: 'Click ↻ to restore default land area for this column',
resetConsultCategoryFactor: 'Click ↻ to restore default consult category factor for this column', resetConsultCategoryFactor: 'Click ↻ to restore default consult category factor for this column',
resetMajorFactor: 'Click ↻ to restore default major factor for this column', resetMajorFactor: 'Click ↻ to restore default major factor for this column'
workRatio: 'This coefficient applies in two cases: service budget composition ratio, for cases where only part of the entrusted work in Appendix D Tables D.2 to D.7 of the specification is included; and quantity ratio, for cases where the calculation base uses the scale of each deliverable, batched task, single project, or unit project rather than the total amount, with quantity representing multiple items, copies, or units.'
} }
}, },
pricingPane: { pricingPane: {
@ -671,8 +610,6 @@ export const enUS = {
confirmOverride: 'Confirm Override', confirmOverride: 'Confirm Override',
investment: { investment: {
title: 'Investment Scale Details', title: 'Investment Scale Details',
titleHint: 'Budget amount values in this pane follow whichever was operated later between the contract scale table and this pane.',
titleHintAria: 'Investment scale details hint',
clearDesc: 'This will clear current investment scale details. Continue?', clearDesc: 'This will clear current investment scale details. Continue?',
overrideDesc: 'Use contract default data to override current investment scale details. Continue?' overrideDesc: 'Use contract default data to override current investment scale details. Continue?'
}, },
@ -707,14 +644,16 @@ export const enUS = {
total: 'Grand Total', total: 'Grand Total',
columns: { columns: {
code: 'Code', code: 'Code',
name: 'Personnel Name', name: 'Name',
referenceUnitPrice: 'Budget Reference Unit Price', technician: 'Technician',
laborBudgetUnitPrice: 'Labor Budget Unit Price (CNY/workday)', assistantEngineer: 'Assistant Engineer',
compositeBudgetUnitPrice: 'Composite Budget Unit Price (CNY/workday)', midEngineer: 'Intermediate Engineer (or Level 2 Cost Engineer)',
adoptedBudgetUnitPrice: 'Adopted Budget Unit Price (CNY/workday)', seniorEngineer: 'Senior Engineer (or Level 1 Cost Engineer)',
personnelCount: 'Personnel Count', profSeniorEngineer: 'Professor-level Senior Engineer',
unitPrice: 'Unit Price (CNY/workday)',
workdayCount: 'Workday Count', workdayCount: 'Workday Count',
serviceBudget: 'Service Budget (CNY)', subtotal: 'Subtotal (CNY)',
avgUnitPrice: 'Average Unit Price (CNY/workday)',
remark: 'Remark' remark: 'Remark'
} }
}, },
@ -727,10 +666,6 @@ export const enUS = {
defaultDesc: 'When providing cost consulting services, penalties should be graded by service quality. For scores >=85 and <90, penalty is 10% of budget fee; >=80 and <85: 20%; >=75 and <80: 30%; >=70 and <75: 40%; <70: 50% or above.', defaultDesc: 'When providing cost consulting services, penalties should be graded by service quality. For scores >=85 and <90, penalty is 10% of budget fee; >=80 and <85: 20%; >=75 and <80: 30%; >=70 and <75: 40%; <70: 50% or above.',
industryHint: 'Changing industry requires reset and re-selection', industryHint: 'Changing industry requires reset and re-selection',
industryHintAria: 'Industry hint', industryHintAria: 'Industry hint',
reportContentHint: 'This field is optional and is only used to auto-generate report content.',
reportContentHintAria: 'Report content hint',
otherDescHint: 'This field is optional. The current content is only a sample. The preparer may fill it in according to actual needs or leave it blank.',
otherDescHintAria: 'Other notes hint',
createFromHomeFirst: 'Please create a project from Home before entering this page.', createFromHomeFirst: 'Please create a project from Home before entering this page.',
fields: { fields: {
projectName: 'Project Name', projectName: 'Project Name',
@ -744,10 +679,9 @@ export const enUS = {
}, },
placeholders: { placeholders: {
overview: 'Enter project overview', overview: 'Enter project overview',
desc: 'Other Notes', preparedBy: 'Enter preparer',
preparedBy: 'XXX', reviewedBy: 'Enter reviewer',
reviewedBy: 'XXX', preparedCompany: 'Enter prepared company'
preparedCompany: 'XXX'
} }
} }
} as const } as const

View File

@ -14,49 +14,32 @@ export const zhCN = {
countdown: '本页将在 {seconds} 秒后自动尝试关闭。你也可以先在新标签页打开其他项目。', countdown: '本页将在 {seconds} 秒后自动尝试关闭。你也可以先在新标签页打开其他项目。',
opened: '(已打开)', opened: '(已打开)',
lastEdited: '最后编辑:{time}', lastEdited: '最后编辑:{time}',
openDefault: '返回首页', openDefault: '打开默认项目',
createAndOpen: '新建项目并打开' createAndOpen: '新建项目并打开'
} }
}, },
home: { home: {
title: '粤公学标字20265号\n交通运输工程造价咨询服务预算编制规范', title: '粤价函2008929号\n《广东省交通工程造价技术中介服务收费项目及标准表》',
subtitle: '项目计算 · 单项速算 · 导入数据 · 相关文件', subtitle: '项目计算 · 单项速算 · 导入数据',
projectCalcTab: '项目计算', projectCalcTab: '项目计算',
quickCalcTab: '快速计算', quickCalcTab: '快速计算',
cards: { cards: {
heroTitle: '众会易|算费真容易', heroTitle: '智能预算一键生成',
heroTitles: [ heroSubTitle: '助力《规范》高效落地',
'众会易 | 算费真容易', heroDesc: '交通建设项目工程造价咨询服务费计算',
'众会易 | 算费不熬夜',
'众会易 | 算费不费力'
],
heroSubTitle: '',
heroDesc: '智算费用 即点即出 您的时间留给创造',
heroDescs: [
'智算费用 即点即出 您的时间留给创造。',
'智算费用 一键即出 您的时间留给创造。',
'智算费用 一键生成 您的时间留给创造。',
'众会易 真容易 不熬夜 不费力'
],
projectBudget: '项目预算', projectBudget: '项目预算',
projectBudgetDesc: '适用于多合同段、项目级整体计算,支持导出/导入完整项目数据', projectBudgetDesc: '适用于多合同段、项目级整体计算,支持导出/导入完整项目数据',
quickCalc: '单项速算', quickCalc: '单项速算',
quickCalcDesc: '适用于单项服务试算,选择行业、咨询类型、工程专业,输入基数秒出结果', quickCalcDesc: '适用于单项服务试算,选择行业、咨询类型、工程专业,输入基数秒出结果',
importData: '导入数据', importData: '导入数据',
importDataDesc: '导入".zw"数据包,并自动新建一个项目用于恢复数据,不会覆盖现有项目', importDataDesc: '导入".zw"数据包,并自动新建一个项目用于恢复数据,不会覆盖现有项目',
relatedFiles: '相关文件',
relatedFilesDesc: '在线查看、打印和下载与该计算器依据的收费文件、相关招标文件与合同文件范本',
viewFiles: '查看文件',
enter: '进入计算', enter: '进入计算',
developing: '正在开发',
pickFile: '选择文件', pickFile: '选择文件',
pickExisting: '选择已有项目', pickExisting: '选择已有项目',
openFileSystem: '打开文件系统', relatedFiles: '相关文件',
file: '文件系统', relatedFilesDesc: '可查看相关收费文件、招标文件、合同文件、服务内容、工作要求',
fileDataDesc: '文件系统可查看相关收费文件、招标文件、合同文件、服务内容、工作要求' openRelatedFiles: '打开页面'
},
disclaimer: {
link: '查看免责声明',
supportText: '本计算工具由众为工程咨询有限公司提供免费技术支持'
}, },
dialog: { dialog: {
newProject: '新建项目', newProject: '新建项目',
@ -73,47 +56,6 @@ export const zhCN = {
noProjectYet: '当前暂无可进入的项目,请先新建项目。' noProjectYet: '当前暂无可进入的项目,请先新建项目。'
} }
}, },
disclaimerPage: {
documentTitle: '预算编制工具免责声明',
eyebrow: 'DISCLAIMER',
pageTitle: '《交通运输工程造价咨询服务预算编制规范》T/GDHS 017-2026预算编制工具免责声明',
lastUpdatedLabel: '最后更新日期:',
lastUpdatedValue: '2026年04月16日',
leadText: '感谢您使用本网站提供的《交通运输工程造价咨询服务预算编制规范》T/GDHS 017-2026造价咨询服务预算编制工具。在您使用本工具前请仔细阅读以下免责声明条款。您继续使用本工具即视为您已阅读、理解并同意接受本声明的全部内容。',
sections: {
standardBasisTitle: '1. 标准依据说明',
standardBasisP1: '1.1 本工具依据广东省公路学会发布的团体标准《交通运输工程造价咨询服务预算编制规范》T/GDHS 017-2026设定编制方法。使用者应自行判断该标准是否适用于其具体项目及所在地主管部门要求。',
standardBasisP2: '1.2 本工具所依据的规范版本已在工具界面标注为 T/GDHS 017-2026。如该规范后续发布修订内容、补充规定或被新版本替代本工具可能无法及时同步更新。使用者有责任在使用前确认所依据规范是否仍为最新有效版本。',
standardBasisP3: '1.3 本工具的计算结果基于本规范中的预算编制方法、费用组成及编制规则,但不同地区、不同项目法人对造价咨询服务预算编制的具体要求和计算方法可能存在差异。本工具不保证其计算结果符合任何特定项目或特定主管部门的审核要求。',
referenceOnlyTitle: '2. 计算结果仅供参考',
referenceOnlyP1: '本工具所提供的所有计算结果,包括但不限于数值、明细表、汇总报表及编制说明,均基于您输入的参数(如工程行业、项目规模、咨询类别、工程专业、工作内容、调整系数等)以及本规范中的数学模型与公式自动生成,仅供参考使用。这些结果不构成任何形式的专业建议,也不代表任何官方或强制性的预算审批依据。',
accuracyTitle: '3. 不保证准确性与完整性',
accuracyP1: '尽管我们尽力确保本工具可用,但本工具按现状提供,不附带任何明示或暗示的保证。我们无法保证计算结果在任何情况下均准确、无误或完整。由于数据输入错误、公式取舍、四舍五入或系统延迟等原因,结果可能与实际情况存在偏差。',
riskTitle: '4. 用户自行承担风险',
riskP1: '您应独立判断计算结果的可信性,并承担将其用于任何决策所产生的全部风险与责任。您不应依赖本工具替代专业人士的具体计算或复核。在作出重大决定前,建议咨询持有交通运输工程造价工程师注册证书的专业人员,或结合项目具体情况进行人工验证与复核。',
liabilityTitle: '5. 责任限制',
liabilityP1: '在适用法律允许的最大范围内,本工具的开发方、管理方、发布方及其关联方,不对因使用或无法使用本工具而导致的任何直接、间接、偶然、特殊或后果性损失承担法律责任,即使已被告知可能发生此类损失。',
liabilityP2: '特别声明:任何造价咨询企业或个人依据本工具计算结果出具的成果文件,其质量责任由出具方自行承担。本工具不对任何第三方成果文件的准确性、合规性或由此引发的争议承担责任。',
interruptionTitle: '6. 服务中断与修改',
interruptionP1: '我们保留随时修改、暂停或终止本工具部分或全部功能的权利,且可能不另行通知。对于因技术维护、网络故障、第三方服务中断、规范版本变更等原因导致的工具不可用、数据丢失或计算结果变化,我们不承担责任。',
externalTitle: '7. 外部链接与第三方内容',
externalP1: '如果本工具引用或链接至全国团体标准信息平台、广东省公路学会官网或其他第三方网站,该等链接仅为方便用户查阅规范原文而提供,不代表我们认可其内容的准确性、时效性或完整性。对于任何第三方网站或工具的信息、服务或内容,我们不承担责任。',
lawTitle: '8. 适用法律',
lawP1: '本声明的解释、效力及争议解决均适用中华人民共和国法律。若本声明任何条款被认定为无效或不可执行,不影响其余条款的效力。'
},
confirm: {
title: '用户确认',
desc1: '我已阅读、理解并同意本免责声明的全部内容。',
desc2: '勾选后方可继续使用本工具。',
checkbox: '我已阅读、理解并同意本免责声明的全部内容。',
continue: '同意并继续',
hint: '勾选后将记录当前浏览器的同意状态,后续从同一受限入口访问时不再重复提示。'
},
actions: {
back: '返回入口',
switchLocale: '切换语言'
}
},
tab: { tab: {
toolbar: { toolbar: {
light: '浅色', light: '浅色',
@ -127,7 +69,7 @@ export const zhCN = {
reset: '重置', reset: '重置',
resetting: '重置中...', resetting: '重置中...',
projectList: '项目列表', projectList: '项目列表',
projectCount: '项目数量:{count}', projectCount: '项目数量:{count}/{max}',
createProject: '新建项目', createProject: '新建项目',
backHome: '返回入口', backHome: '返回入口',
resetAll: '清除全部项目', resetAll: '清除全部项目',
@ -227,7 +169,9 @@ export const zhCN = {
toast: { toast: {
export: '导出报表', export: '导出报表',
success: '导出成功', success: '导出成功',
failed: '导出失败' failed: '导出失败',
saveSuccess: '保存成功',
saveFailed: '保存失败'
}, },
messages: { messages: {
defaultProjectLabel: '默认项目', defaultProjectLabel: '默认项目',
@ -252,7 +196,7 @@ export const zhCN = {
copied: '已复制', copied: '已复制',
copyFailed: '复制失败', copyFailed: '复制失败',
brandAlt: '众为咨询', brandAlt: '众为咨询',
supportText: '本计算工具由众为工程咨询有限公司提供免费技术支持', supportText: '本网站由众为工程咨询有限公司提供免费技术支持',
aboutTitle: '关于我们', aboutTitle: '关于我们',
companyName: '众为工程咨询有限公司', companyName: '众为工程咨询有限公司',
openOfficialSiteAria: '跳转到官网首页', openOfficialSiteAria: '跳转到官网首页',
@ -334,9 +278,9 @@ export const zhCN = {
metaBudget: '合同段预算金额:{amount}', metaBudget: '合同段预算金额:{amount}',
currencySuffix: '元', currencySuffix: '元',
categories: { categories: {
baseInfo: '基础信息', baseInfo: '合同基础信息',
scaleInfo: '规模信息', scaleInfo: '合同规模',
services: '咨询服务', services: '合同费用汇总表',
consultFactor: '咨询分类系数', consultFactor: '咨询分类系数',
majorFactor: '工程专业系数', majorFactor: '工程专业系数',
additionalFee: '附加工作费', additionalFee: '附加工作费',
@ -361,9 +305,7 @@ export const zhCN = {
reserveTitle: '预备费' reserveTitle: '预备费'
}, },
htInfo: { htInfo: {
scaleDetailTitle: '合同规模明细', scaleDetailTitle: '合同规模明细'
scaleDetailHint: '当本表规模与项目规模数据不一致时,计费以本表规模为准',
scaleDetailHintAria: '合同规模明细提示'
}, },
htFeeRate: { htFeeRate: {
baseLabel: '基数(所有服务费预算合计)', baseLabel: '基数(所有服务费预算合计)',
@ -378,7 +320,7 @@ export const zhCN = {
title: '咨询服务明细', title: '咨询服务明细',
warning: '※ 请注意检查并修改《规范》建议的限值或特殊值,并在确认金额栏修改', warning: '※ 请注意检查并修改《规范》建议的限值或特殊值,并在确认金额栏修改',
editTabTitle: '服务编辑-{name}', editTabTitle: '服务编辑-{name}',
subtotal: '计', subtotal: '计',
edit: '编辑', edit: '编辑',
resetDefault: '恢复默认', resetDefault: '恢复默认',
delete: '删除', delete: '删除',
@ -408,7 +350,7 @@ export const zhCN = {
}, },
htSummary: { htSummary: {
title: '合同段汇总', title: '合同段汇总',
total: '计', total: '计',
remark: '说明', remark: '说明',
placeholder: '请先填咨询服务/附加工作费/预备费的数据', placeholder: '请先填咨询服务/附加工作费/预备费的数据',
additionalPrefix: '附加工作费', additionalPrefix: '附加工作费',
@ -428,7 +370,7 @@ export const zhCN = {
} }
}, },
htFeeGrid: { htFeeGrid: {
subtotal: '计', subtotal: '计',
currentRow: '当前行', currentRow: '当前行',
unnamed: '未命名', unnamed: '未命名',
edit: '编辑', edit: '编辑',
@ -460,8 +402,6 @@ export const zhCN = {
}, },
serviceSelector: { serviceSelector: {
title: '选择服务', title: '选择服务',
titleHint: '本选择项较《规范》列明的服务项有所增加。此增加与《规范》无冲突,只为满足《规范》费用计算之需',
titleHintAria: '选择服务提示',
clear: '清空', clear: '清空',
empty: '暂无服务' empty: '暂无服务'
}, },
@ -477,7 +417,8 @@ export const zhCN = {
landScaleFormula: '用地规模法计算公式', landScaleFormula: '用地规模法计算公式',
workload: '工作量法', workload: '工作量法',
hourly: '工时法', hourly: '工时法',
workContent: '工作内容' workContent: '工作内容',
otherService: '其他服务计算'
}, },
formulaColumns: { formulaColumns: {
subtitle: '直接展示当前计价法 store 的最新明细,随数据变更自动同步。', subtitle: '直接展示当前计价法 store 的最新明细,随数据变更自动同步。',
@ -497,7 +438,7 @@ export const zhCN = {
} }
}, },
htFeeDetail: { htFeeDetail: {
subtotal: '计', subtotal: '计',
currentRow: '当前行', currentRow: '当前行',
clickToInput: '点击输入', clickToInput: '点击输入',
addRow: '添加行', addRow: '添加行',
@ -618,8 +559,8 @@ export const zhCN = {
}, },
xmCard: { xmCard: {
categories: { categories: {
info: '基础信息', info: '项目基础信息',
scaleInfo: '规模信息', scaleInfo: '项目规模',
consultCategoryFactor: '咨询分类系数', consultCategoryFactor: '咨询分类系数',
majorFactor: '工程专业系数', majorFactor: '工程专业系数',
contract: '合同段管理' contract: '合同段管理'
@ -635,11 +576,12 @@ export const zhCN = {
pricingScale: { pricingScale: {
totalInvestmentByIndustry: '{industryName}总投资', totalInvestmentByIndustry: '{industryName}总投资',
totalInvestment: '总投资', totalInvestment: '总投资',
clickToInput: '点击输入', clickToInput: '非必填手动录入数字4位',
projectLabel: '项目{index}', projectLabel: '项目{index}',
columns: { columns: {
code: '编码',
investAmount: '造价金额(万元)', investAmount: '造价金额(万元)',
landArea: '用地面积(亩)', landArea: '造价金额(元)',
benchmarkBudget: '基准预算(元)', benchmarkBudget: '基准预算(元)',
basicWork: '基本工作', basicWork: '基本工作',
optionalWork: '可选工作', optionalWork: '可选工作',
@ -647,18 +589,25 @@ export const zhCN = {
budgetFee: '预算费用', budgetFee: '预算费用',
consultCategoryFactor: '咨询分类系数', consultCategoryFactor: '咨询分类系数',
majorFactor: '专业系数', majorFactor: '专业系数',
workStageFactor: '工作环节系数(编审系数)', workStageFactor: '工作环节系数',
workRatio: '服务预算构成比率与数量比', workRatio: '工作占比',
total: '合计', total: '合计',
remark: '说明', remark: '说明',
majorGroup: '专业编码以及工程专业名称' majorGroup: '项目明细费用',
name: '名称',
number: '编码',
base: '计算基础',
base2: '计算基数F万元',
formula: '计算公式',
calculationAmount: '计算金额(元)',
calculationGroup: '计算公式',
serviceFee: '服务费用(元)'
}, },
tooltip: { tooltip: {
resetInvestAmount: '点击右侧↻本列造价金额数值恢复为本合同规模数值', resetInvestAmount: '点击右侧↻恢复本列默认造价金额',
resetLandArea: '点击右侧↻本列用地面积数值恢复为本合同规模数值', resetLandArea: '点击右侧↻恢复本列默认用地面积',
resetConsultCategoryFactor: '点击右侧↻本列咨询分类系数值恢复为本项目的相应的咨询分类系数值', resetConsultCategoryFactor: '点击右侧↻恢复本列默认咨询分类系数',
resetMajorFactor: '点击右侧↻本列专业系数值恢复为本项目的相应的专业系数值', resetMajorFactor: '点击右侧↻恢复本列默认专业系数'
workRatio: '本列系数适用于以下两种情形服务预算构成比率适用于《规范》附录D表D.2~D.7中委托工作内容仅为部分工作时的情形;数量比,适用于计算基数按每份成果、分批次任务、单项工程或单位工程的规模(非总额)的情形,数量表示有多个个、份或项'
} }
}, },
pricingPane: { pricingPane: {
@ -670,8 +619,6 @@ export const zhCN = {
confirmOverride: '确认覆盖', confirmOverride: '确认覆盖',
investment: { investment: {
title: '投资规模明细', title: '投资规模明细',
titleHint: '本表造价金额取值规则:以本合同规模表与本表造价金额中操作时间较晚的值为准',
titleHintAria: '投资规模明细提示',
clearDesc: '将清空当前投资规模明细,是否继续?', clearDesc: '将清空当前投资规模明细,是否继续?',
overrideDesc: '将使用合同默认数据覆盖当前投资规模明细,是否继续?' overrideDesc: '将使用合同默认数据覆盖当前投资规模明细,是否继续?'
}, },
@ -687,34 +634,78 @@ export const zhCN = {
unavailableMessage: '当前服务没有关联工作量法任务,无需填写此部分内容。', unavailableMessage: '当前服务没有关联工作量法任务,无需填写此部分内容。',
clickToInput: '点击输入', clickToInput: '点击输入',
none: '无', none: '无',
total: '总计', total: '总计',
columns: { columns: {
code: '编码', code: '编码',
name: '名称', name: '名称',
budgetBase: '预算基数', budgetBase: '计算基础',
budgetReferenceUnitPrice: '预算参考单价', budgetReferenceUnitPrice: '计算基数(份)',
budgetAdoptedUnitPrice: '预算采用单价', budgetAdoptedUnitPrice: '最低单价(万元/份)',
workload: '工作量', workload: '中值单价(万元/份)',
consultCategoryFactor: '咨询分类系数', consultCategoryFactor: '最高单价(万元/份)',
serviceFee: '服务费用(元)', cLow: '计算最低值(元)',
cMid: '计算中值(元)',
cHigh: '计算最高值(元)',
serviceFee: '本计算取值(元)',
remark: '说明' remark: '说明'
} }
}, },
hourlyFeeGrid: { hourlyFeeGrid: {
title: '工时法明细', title: '工时法明细',
clickToInput: '点击输入', clickToInput: '点击输入',
total: '总计', total: '总计',
columns: { columns: {
code: '编码', code: '编码',
name: '人员名称', name: '名称',
referenceUnitPrice: '预算参考单价', technician: '技术员',
laborBudgetUnitPrice: '人工预算单价(元/工日)', assistantEngineer: '助理工程师',
compositeBudgetUnitPrice: '综合预算单价(元/工日)', midEngineer: '中级工程师(或二级造价工程师)',
adoptedBudgetUnitPrice: '预算采用单价(元/工日)', seniorEngineer: '高级工程师(或一级造价工程师)',
personnelCount: '人员数量(人)', profSeniorEngineer: '正高级工程师',
unitPrice: '单价(元/工日)',
workdayCount: '工日数量(工日)', workdayCount: '工日数量(工日)',
serviceBudget: '服务预算(元)', subtotal: '费用小计(元)',
remark: '说明' unitPrice2: '单价(元/工日)',
workdayCount2: '工日数量(工日)',
subtotal2: '费用小计(元)',
unitPrice3: '单价(元/工日)',
workdayCount3: '工日数量(工日)',
subtotal3: '费用小计(元)',
unitPrice4: '单价(元/工日)',
workdayCount4: '工日数量(工日)',
subtotal4: '费用小计(元)',
unitPrice5: '单价(元/工日)',
workdayCount5: '工日数量(工日)',
subtotal5: '费用小计(元)',
unitPrice6: '单价(元/工日)',
workdayCount6: '工日数量(工日)',
subtotal6: '费用小计(元)',
avgUnitPrice: '折算单价(元/工日)',
remark: '说明',
total: '合计',
referenceUnitPrice: '参考单价(元/工日)',
laborBudgetUnitPrice: '劳动预算单价(元/工日)',
compositeBudgetUnitPrice: '综合预算单价(元/工日)',
adoptedBudgetUnitPrice: '采用单价(元/工日)',
personnelCount: '人员数量(人)',
serviceBudget: '服务费用(元)',
}
},
otherService: {
title: '其他服务计算',
clickToInput: '点击输入',
total: '小计',
columns: {
num: '序号',
code: '编码',
name: '名称',
feeItem: '费用项',
unit: '单位',
quantity: '数量',
unitPrice: '单价',
serviceFee: '服务费用(元)',
remark: '说明',
actions: '操作'
} }
}, },
xmScaleGrid: { xmScaleGrid: {
@ -726,10 +717,6 @@ export const zhCN = {
defaultDesc: '在履行造价咨询服务时宜根据咨询服务质量情况分级确定相应的处罚金额。其中考评得分在大于及等于85和小于90分时处罚金额为预算费用的10%其中考评得分在大于及等于80和小于85分时处罚金额为预算费用的20%其中考评得分在大于及等于75和小于80分时处罚金额为预算费用的30%其中考评得分在大于及等于70和小于75分时处罚金额为预算费用的40%其中考评得分小于70分时处罚金额为预算费用的50%以上。', defaultDesc: '在履行造价咨询服务时宜根据咨询服务质量情况分级确定相应的处罚金额。其中考评得分在大于及等于85和小于90分时处罚金额为预算费用的10%其中考评得分在大于及等于80和小于85分时处罚金额为预算费用的20%其中考评得分在大于及等于75和小于80分时处罚金额为预算费用的30%其中考评得分在大于及等于70和小于75分时处罚金额为预算费用的40%其中考评得分小于70分时处罚金额为预算费用的50%以上。',
industryHint: '变更需要重置后重新选择', industryHint: '变更需要重置后重新选择',
industryHintAria: '工程行业提示', industryHintAria: '工程行业提示',
reportContentHint: '本内容为选择性填写,填写内容仅用于自动生成编制报告内容',
reportContentHintAria: '说明内容提示',
otherDescHint: '本内容为选择性填写。当前显示内容仅为示意,编制人可根据实际情况填写,亦可不填。',
otherDescHintAria: '其他说明提示',
createFromHomeFirst: '请从首页先新建项目后再进入此页面。', createFromHomeFirst: '请从首页先新建项目后再进入此页面。',
fields: { fields: {
projectName: '项目名称', projectName: '项目名称',
@ -743,10 +730,9 @@ export const zhCN = {
}, },
placeholders: { placeholders: {
overview: '请输入项目概况', overview: '请输入项目概况',
desc: '其他说明', preparedBy: '请输入编制人',
preparedBy: 'XXX', reviewedBy: '请输入复核人',
reviewedBy: 'XXX', preparedCompany: '请输入编制单位'
preparedCompany: 'XXX'
} }
} }
} as const } as const

File diff suppressed because it is too large Load Diff

View File

@ -96,14 +96,10 @@ const activeComponent = computed(() => {
const sideWidthStyle = computed(() => ({ width: 'var(--app-typeline-side-w)' })) const sideWidthStyle = computed(() => ({ width: 'var(--app-typeline-side-w)' }))
const itemGapStyle = computed(() => ({ gap: 'var(--app-typeline-gap)' })) const itemGapStyle = computed(() => ({ gap: 'var(--app-typeline-gap)' }))
const axisColStyle = computed(() => ({ width: 'var(--app-typeline-dot)' }))
const dotStyle = computed(() => ({ width: 'var(--app-typeline-dot)', height: 'var(--app-typeline-dot)' })) const dotStyle = computed(() => ({ width: 'var(--app-typeline-dot)', height: 'var(--app-typeline-dot)' }))
const dotInnerStyle = computed(() => ({ width: 'var(--app-typeline-dot-inner)', height: 'var(--app-typeline-dot-inner)' })) const dotInnerStyle = computed(() => ({ width: 'var(--app-typeline-dot-inner)', height: 'var(--app-typeline-dot-inner)' }))
const labelStyle = computed(() => ({ fontSize: 'var(--app-typeline-label-font)', lineHeight: 'var(--app-typeline-label-line)' })) const labelStyle = computed(() => ({ fontSize: 'var(--app-typeline-label-font)', lineHeight: 'var(--app-typeline-label-line)' }))
const connectorStyle = computed(() => ({ const lineStyle = computed(() => ({ left: 'var(--app-typeline-line-left)' }))
height: 'var(--app-typeline-arrow-shaft-h)',
width: 'var(--app-typeline-arrow-line-w)'
}))
const copyBtnText = ref(t('typeLine.copy')) const copyBtnText = ref(t('typeLine.copy'))
const sheetOpen = ref(false) const sheetOpen = ref(false)
@ -111,34 +107,6 @@ const sheetOpen = ref(false)
let copyBtnTimer: ReturnType<typeof setTimeout> | null = null let copyBtnTimer: ReturnType<typeof setTimeout> | null = null
let titleOverflowRafId: number | null = null let titleOverflowRafId: number | null = null
const copyTextWithFallback = async (text: string) => {
if (!text) return false
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text)
return true
}
if (typeof document === 'undefined') return false
const textarea = document.createElement('textarea')
textarea.value = text
textarea.setAttribute('readonly', 'true')
textarea.style.position = 'fixed'
textarea.style.top = '-9999px'
textarea.style.left = '-9999px'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
try {
textarea.focus()
textarea.select()
textarea.setSelectionRange(0, textarea.value.length)
return document.execCommand('copy')
} finally {
document.body.removeChild(textarea)
}
}
@ -150,8 +118,8 @@ const handleCopySubtitle = async () => {
if (!text) return if (!text) return
try { try {
const copied = await copyTextWithFallback(text) await navigator.clipboard.writeText(text)
copyBtnText.value = copied ? t('typeLine.copied') : t('typeLine.copyFailed') copyBtnText.value = t('typeLine.copied')
} catch (error) { } catch (error) {
console.error('copy failed:', error) console.error('copy failed:', error)
copyBtnText.value = t('typeLine.copyFailed') copyBtnText.value = t('typeLine.copyFailed')
@ -288,32 +256,24 @@ useMotionValueEvent(
</div> </div>
</div> </div>
<div :class="['flex flex-col gap-2 relative ', (props.title || props.subtitle || props.metaText) ? 'mt-3' : 'mt-6']"> <div :class="['flex flex-col gap-6 relative ', (props.title || props.subtitle || props.metaText) ? 'mt-3' : 'mt-6']">
<div v-for="(item, index) in props.categories" :key="item.key" <div :style="lineStyle" class="absolute top-3 bottom-3 w-[1.5px] bg-border/60"></div>
:style="itemGapStyle" class="flex items-start cursor-pointer group" @click="switchCategory(item.key)">
<div :style="axisColStyle" class="flex shrink-0 flex-col items-center"> <div v-for="item in props.categories" :key="item.key"
:style="itemGapStyle" class="relative flex items-center cursor-pointer group" @click="switchCategory(item.key)">
<div :class="[ <div :class="[
'z-10 rounded-full border-2 flex shrink-0 items-center justify-center transition-all duration-200', 'z-10 rounded-full border-2 flex items-center justify-center transition-all duration-200',
activeCategory === item.key activeCategory === item.key
? 'bg-primary border-primary shadow-[0_0_0_3px_rgba(var(--primary),0.15)]' ? 'bg-blue-600 border-blue-600'
: 'bg-background border-muted-foreground/40 group-hover:border-muted-foreground/70' : 'bg-background border-slate-300 group-hover:border-slate-400'
]" :style="dotStyle"> ]" :style="dotStyle">
<div v-if="activeCategory === item.key" class="bg-background rounded-full" :style="dotInnerStyle"></div> <div v-if="activeCategory === item.key" class="bg-white rounded-full" :style="dotInnerStyle"></div>
</div>
<div
v-if="index < props.categories.length - 1"
class="flex flex-col items-center"
:style="{ paddingTop: 'var(--app-typeline-arrow-offset)' }"
aria-hidden="true"
>
<div :style="connectorStyle" class="typeline-arrow-connector"></div>
</div>
</div> </div>
<span :class="[ <span :class="[
'pt-px transition-colors duration-200', 'transition-colors duration-200',
activeCategory === item.key activeCategory === item.key
? 'font-semibold text-primary' ? 'font-semibold text-blue-600'
: 'text-muted-foreground group-hover:text-foreground' : 'text-slate-500 group-hover:text-slate-700'
]" :style="labelStyle"> ]" :style="labelStyle">
{{ item.label }} {{ item.label }}
</span> </span>
@ -431,23 +391,4 @@ useMotionValueEvent(
overflow: hidden; overflow: hidden;
word-break: break-word; word-break: break-word;
} }
.typeline-arrow-connector {
position: relative;
background: color-mix(in oklab, var(--foreground) 22%, var(--border));
border-radius: 9999px;
}
.typeline-arrow-connector::after {
content: '';
position: absolute;
left: 50%;
top: 100%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: calc(var(--app-typeline-arrow-head-w) / 2) solid transparent;
border-right: calc(var(--app-typeline-arrow-head-w) / 2) solid transparent;
border-top: var(--app-typeline-arrow-head-h) solid color-mix(in oklab, var(--foreground) 22%, var(--border));
}
</style> </style>

View File

@ -45,7 +45,7 @@ export class AgGridResetHeader implements IHeaderComp {
eButton.style.height = '18px' eButton.style.height = '18px'
eButton.style.border = '1px solid #d1d5db' eButton.style.border = '1px solid #d1d5db'
eButton.style.borderRadius = '999px' eButton.style.borderRadius = '999px'
eButton.style.background = '#edff87' eButton.style.background = '#fff'
eButton.style.color = '#4b5563' eButton.style.color = '#4b5563'
eButton.style.cursor = 'pointer' eButton.style.cursor = 'pointer'
eButton.style.fontSize = '12px' eButton.style.fontSize = '12px'
@ -69,38 +69,8 @@ export class AgGridResetHeader implements IHeaderComp {
this.params = params this.params = params
this.eLabel.textContent = params.displayName || params.column?.getColDef().headerName || '' this.eLabel.textContent = params.displayName || params.column?.getColDef().headerName || ''
const fallbackResetTitle = i18n.global.t('agGrid.resetDefault') const fallbackResetTitle = i18n.global.t('agGrid.resetDefault')
const hintText = String(params.column?.getColDef().headerTooltip || '').trim()
const hasReset = Boolean(params.onReset)
const hasHint = Boolean(hintText)
if (hasReset) {
this.eButton.textContent = '↻'
this.eButton.title = ''
this.eButton.setAttribute('aria-label', params.resetTitle || fallbackResetTitle) this.eButton.setAttribute('aria-label', params.resetTitle || fallbackResetTitle)
this.eButton.style.visibility = 'visible' this.eButton.style.visibility = params.onReset ? 'visible' : 'hidden'
this.eButton.style.border = '1px solid #d1d5db'
this.eButton.style.background = '#edff87'
this.eButton.style.color = '#4b5563'
this.eButton.style.cursor = 'pointer'
return true
}
if (hasHint) {
this.eButton.textContent = '?'
this.eButton.title = hintText
this.eButton.setAttribute('aria-label', hintText)
this.eButton.style.visibility = 'visible'
this.eButton.style.border = '1px solid #cbd5e1'
this.eButton.style.background = '#ffffff'
this.eButton.style.color = '#64748b'
this.eButton.style.cursor = 'help'
return true
}
this.eButton.textContent = ''
this.eButton.title = ''
this.eButton.setAttribute('aria-label', params.resetTitle || fallbackResetTitle)
this.eButton.style.visibility = 'hidden'
return true return true
} }

View File

@ -8,7 +8,6 @@ import {
import { roundTo, sumByNumber, toDecimal, toFiniteNumberOrNull } from '@/lib/decimal' import { roundTo, sumByNumber, toDecimal, toFiniteNumberOrNull } from '@/lib/decimal'
import { getScaleBudgetFee } from '@/lib/pricingScaleFee' import { getScaleBudgetFee } from '@/lib/pricingScaleFee'
import { getScaleBudgetFeeByRow } from '@/lib/pricingScaleDetail' import { getScaleBudgetFeeByRow } from '@/lib/pricingScaleDetail'
import { isInvestScaleSingleTotalService, resolveServicePricingCapabilities } from '@/lib/servicePricing'
import { useZxFwPricingStore, type ServicePricingMethod } from '@/pinia/zxFwPricing' import { useZxFwPricingStore, type ServicePricingMethod } from '@/pinia/zxFwPricing'
import { useKvStore } from '@/pinia/kv' import { useKvStore } from '@/pinia/kv'
@ -50,8 +49,6 @@ const getOnlyCostScaleSummaryAmount = (
interface ScaleRow { interface ScaleRow {
id: string id: string
hasCost?: boolean
hasArea?: boolean
amount: number | null amount: number | null
landArea: number | null landArea: number | null
benchmarkBudgetBasicChecked: boolean benchmarkBudgetBasicChecked: boolean
@ -88,10 +85,6 @@ interface MajorLite {
interface ServiceLite { interface ServiceLite {
defCoe: number | null defCoe: number | null
enableInvestScale?: boolean | null
enableLandScale?: boolean | null
investScaleSingleTotal?: boolean | null
scale?: boolean | null
onlyCostScale?: boolean | null onlyCostScale?: boolean | null
} }
@ -228,14 +221,9 @@ const getDefaultConsultCategoryFactor = (serviceId: string | number) => {
return toFiniteNumberOrNull(service?.defCoe) return toFiniteNumberOrNull(service?.defCoe)
} }
const usesInvestScaleSingleTotal = (serviceId: string | number) => { const isOnlyCostScaleService = (serviceId: string | number) => {
const service = (getServiceDictById() as Record<string, ServiceLite | undefined>)[String(serviceId)] const service = (getServiceDictById() as Record<string, ServiceLite | undefined>)[String(serviceId)]
return isInvestScaleSingleTotalService(service) return service?.onlyCostScale === true
}
const getScaleMethodCapabilities = (serviceId: string | number) => {
const service = (getServiceDictById() as Record<string, ServiceLite | undefined>)[String(serviceId)]
return resolveServicePricingCapabilities(service)
} }
const majorById = new Map(getMajorDictEntries().map(({ id, item }) => [id, item as MajorLite])) const majorById = new Map(getMajorDictEntries().map(({ id, item }) => [id, item as MajorLite]))
@ -338,8 +326,6 @@ const buildDefaultScaleRows = (
consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId) consultCategoryFactorMap?.get(String(serviceId)) ?? getDefaultConsultCategoryFactor(serviceId)
return getMajorLeafIds().map(id => ({ return getMajorLeafIds().map(id => ({
id, id,
hasCost: isCostMajorById(id),
hasArea: isAreaMajorById(id),
amount: null, amount: null,
landArea: null, landArea: null,
benchmarkBudgetBasicChecked: true, benchmarkBudgetBasicChecked: true,
@ -347,7 +333,7 @@ const buildDefaultScaleRows = (
consultCategoryFactor: defaultConsultCategoryFactor, consultCategoryFactor: defaultConsultCategoryFactor,
majorFactor: majorFactorMap?.get(id) ?? getDefaultMajorFactorById(id), majorFactor: majorFactorMap?.get(id) ?? getDefaultMajorFactorById(id),
workStageFactor: 1, workStageFactor: 1,
workRatio: 1 workRatio: 100
})) }))
} }
@ -380,14 +366,6 @@ const mergeScaleRows = (
return { return {
...row, ...row,
hasCost:
typeof (fromDb as { hasCost?: unknown }).hasCost === 'boolean'
? Boolean((fromDb as { hasCost?: unknown }).hasCost)
: row.hasCost,
hasArea:
typeof (fromDb as { hasArea?: unknown }).hasArea === 'boolean'
? Boolean((fromDb as { hasArea?: unknown }).hasArea)
: row.hasArea,
amount: toFiniteNumberOrNull(fromDb.amount), amount: toFiniteNumberOrNull(fromDb.amount),
landArea: toFiniteNumberOrNull(fromDb.landArea), landArea: toFiniteNumberOrNull(fromDb.landArea),
benchmarkBudgetBasicChecked: benchmarkBudgetBasicChecked:
@ -416,7 +394,7 @@ const mergeScaleRows = (
const getInvestmentBudgetFee = (row: ScaleRow) => getScaleBudgetFeeByRow(row, 'cost') const getInvestmentBudgetFee = (row: ScaleRow) => getScaleBudgetFeeByRow(row, 'cost')
const getInvestScaleSingleTotalBudgetFee = ( const getOnlyCostScaleBudgetFee = (
serviceId: string, serviceId: string,
rowsFromDb: Array<Record<string, unknown>> | undefined, rowsFromDb: Array<Record<string, unknown>> | undefined,
consultCategoryFactorMap?: Map<string, number | null>, consultCategoryFactorMap?: Map<string, number | null>,
@ -434,7 +412,7 @@ const getInvestScaleSingleTotalBudgetFee = (
toFiniteNumberOrNull(industryMajorEntry?.item?.defCoe) ?? toFiniteNumberOrNull(industryMajorEntry?.item?.defCoe) ??
1 1
// 单行总投资模式支持“按项目行”存储(如 1::majorId、2::majorId每行需独立计费后求和。 // 新版 onlyCostScale 支持“按项目行”存储(如 1::majorId、2::majorId每行需独立计费后求和。
const usePerRowCalculation = sourceRows.some(row => { const usePerRowCalculation = sourceRows.some(row => {
if (typeof row?.projectIndex === 'number' && Number.isFinite(row.projectIndex)) return true if (typeof row?.projectIndex === 'number' && Number.isFinite(row.projectIndex)) return true
const id = String(row?.id || '') const id = String(row?.id || '')
@ -451,7 +429,7 @@ const getInvestScaleSingleTotalBudgetFee = (
majorFactor: getRowNumberOrFallback(row, 'majorFactor', defaultMajorFactor), majorFactor: getRowNumberOrFallback(row, 'majorFactor', defaultMajorFactor),
consultCategoryFactor: getRowNumberOrFallback(row, 'consultCategoryFactor', defaultConsultCategoryFactor), consultCategoryFactor: getRowNumberOrFallback(row, 'consultCategoryFactor', defaultConsultCategoryFactor),
workStageFactor: getRowNumberOrFallback(row, 'workStageFactor', 1), workStageFactor: getRowNumberOrFallback(row, 'workStageFactor', 1),
workRatio: getRowNumberOrFallback(row, 'workRatio', 1) workRatio: getRowNumberOrFallback(row, 'workRatio', 100)
}, 'cost') }, 'cost')
}) })
} }
@ -465,7 +443,7 @@ const getInvestScaleSingleTotalBudgetFee = (
const consultCategoryFactor = getRowNumberOrFallback(onlyRow, 'consultCategoryFactor', defaultConsultCategoryFactor) const consultCategoryFactor = getRowNumberOrFallback(onlyRow, 'consultCategoryFactor', defaultConsultCategoryFactor)
const majorFactor = getRowNumberOrFallback(onlyRow, 'majorFactor', defaultMajorFactor) const majorFactor = getRowNumberOrFallback(onlyRow, 'majorFactor', defaultMajorFactor)
const workStageFactor = getRowNumberOrFallback(onlyRow, 'workStageFactor', 1) const workStageFactor = getRowNumberOrFallback(onlyRow, 'workStageFactor', 1)
const workRatio = getRowNumberOrFallback(onlyRow, 'workRatio', 1) const workRatio = getRowNumberOrFallback(onlyRow, 'workRatio', 100)
return getScaleBudgetFeeByRow({ return getScaleBudgetFeeByRow({
amount: resolvedTotalAmount, amount: resolvedTotalAmount,
benchmarkBudgetBasicChecked: typeof onlyRow?.benchmarkBudgetBasicChecked === 'boolean' ? onlyRow.benchmarkBudgetBasicChecked : true, benchmarkBudgetBasicChecked: typeof onlyRow?.benchmarkBudgetBasicChecked === 'boolean' ? onlyRow.benchmarkBudgetBasicChecked : true,
@ -477,7 +455,7 @@ const getInvestScaleSingleTotalBudgetFee = (
}, 'cost') }, 'cost')
} }
const buildInvestScaleSingleTotalDetailRows = ( const buildOnlyCostScaleDetailRows = (
serviceId: string, serviceId: string,
rowsFromDb: Array<Record<string, unknown>> | undefined, rowsFromDb: Array<Record<string, unknown>> | undefined,
consultCategoryFactorMap?: Map<string, number | null>, consultCategoryFactorMap?: Map<string, number | null>,
@ -506,13 +484,11 @@ const buildInvestScaleSingleTotalDetailRows = (
1 1
) )
const workStageFactor = getRowNumberOrFallback(onlyRow, 'workStageFactor', 1) const workStageFactor = getRowNumberOrFallback(onlyRow, 'workStageFactor', 1)
const workRatio = getRowNumberOrFallback(onlyRow, 'workRatio', 1) const workRatio = getRowNumberOrFallback(onlyRow, 'workRatio', 100)
return [ return [
{ {
id: onlyCostRowId, id: onlyCostRowId,
hasCost: true,
hasArea: false,
amount: resolvedTotalAmount, amount: resolvedTotalAmount,
landArea: null, landArea: null,
consultCategoryFactor, consultCategoryFactor,
@ -680,8 +656,6 @@ const normalizeScopedScaleRows = (
const hasWorkRatio = hasOwn(row, 'workRatio') const hasWorkRatio = hasOwn(row, 'workRatio')
return { return {
id: resolvedMajorId, id: resolvedMajorId,
hasCost: isCostMajorById(resolvedMajorId),
hasArea: isAreaMajorById(resolvedMajorId),
amount: toFiniteNumberOrNull(row.amount), amount: toFiniteNumberOrNull(row.amount),
landArea: toFiniteNumberOrNull(row.landArea), landArea: toFiniteNumberOrNull(row.landArea),
benchmarkBudgetBasicChecked: benchmarkBudgetBasicChecked:
@ -699,7 +673,7 @@ const normalizeScopedScaleRows = (
(hasWorkStageFactor ? null : 1), (hasWorkStageFactor ? null : 1),
workRatio: workRatio:
toFiniteNumberOrNull(row.workRatio) ?? toFiniteNumberOrNull(row.workRatio) ??
(hasWorkRatio ? null : 1) (hasWorkRatio ? null : 100)
} }
}) })
} }
@ -757,8 +731,7 @@ const buildDefaultPricingMethodDetailRows = (
serviceId: string, serviceId: string,
context: PricingMethodDefaultBuildContext context: PricingMethodDefaultBuildContext
): PricingMethodDefaultDetailRows => { ): PricingMethodDefaultDetailRows => {
const capabilities = getScaleMethodCapabilities(serviceId) const onlyCostScale = isOnlyCostScaleService(serviceId)
const investScaleSingleTotal = usesInvestScaleSingleTotal(serviceId)
const scaleRows = resolveScaleRows( const scaleRows = resolveScaleRows(
serviceId, serviceId,
null, null,
@ -767,9 +740,8 @@ const buildDefaultPricingMethodDetailRows = (
context.majorFactorMap context.majorFactorMap
) )
const investScale = capabilities.investScaleEnabled const investScale = onlyCostScale
? (investScaleSingleTotal ? buildOnlyCostScaleDetailRows(
? buildInvestScaleSingleTotalDetailRows(
serviceId, serviceId,
context.htData?.detailRows as Array<Record<string, unknown>> | undefined, context.htData?.detailRows as Array<Record<string, unknown>> | undefined,
context.consultCategoryFactorMap, context.consultCategoryFactorMap,
@ -781,11 +753,8 @@ const buildDefaultPricingMethodDetailRows = (
if (!isCostMajorById(row.id)) return false if (!isCostMajorById(row.id)) return false
if (context.excludeInvestmentCostAndAreaRows && isDualScaleMajorById(row.id)) return false if (context.excludeInvestmentCostAndAreaRows && isDualScaleMajorById(row.id)) return false
return true return true
})) })
: [] const landScale = scaleRows.filter(row => isAreaMajorById(row.id))
const landScale = capabilities.landScaleEnabled
? scaleRows.filter(row => isAreaMajorById(row.id))
: []
return { return {
investScale, investScale,
@ -869,7 +838,7 @@ export const getPricingMethodTotalsForService = async (params: {
const consultCategoryFactorMap = buildConsultCategoryFactorMap(consultFactorData) const consultCategoryFactorMap = buildConsultCategoryFactorMap(consultFactorData)
const majorFactorMap = buildMajorFactorMap(majorFactorData) const majorFactorMap = buildMajorFactorMap(majorFactorData)
const investScaleSingleTotal = usesInvestScaleSingleTotal(serviceId) const onlyCostScale = isOnlyCostScaleService(serviceId)
const industryId = typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : '' const industryId = typeof baseInfo?.projectIndustry === 'string' ? baseInfo.projectIndustry.trim() : ''
// 优先使用对应计费页的数据;不存在时回退合同段规模信息,再回退默认字典行。 // 优先使用对应计费页的数据;不存在时回退合同段规模信息,再回退默认字典行。
@ -882,8 +851,8 @@ export const getPricingMethodTotalsForService = async (params: {
const scopedLandRows = hasScopedScaleRows(landScaleRowsSource) const scopedLandRows = hasScopedScaleRows(landScaleRowsSource)
? normalizeScopedScaleRows(serviceId, landScaleRowsSource, consultCategoryFactorMap, majorFactorMap) ? normalizeScopedScaleRows(serviceId, landScaleRowsSource, consultCategoryFactorMap, majorFactorMap)
: null : null
const investScale = investScaleSingleTotal const investScale = onlyCostScale
? getInvestScaleSingleTotalBudgetFee( ? getOnlyCostScaleBudgetFee(
serviceId, serviceId,
(investData?.detailRows as Array<Record<string, unknown>> | undefined) || (investData?.detailRows as Array<Record<string, unknown>> | undefined) ||
(htData?.detailRows as Array<Record<string, unknown>> | undefined), (htData?.detailRows as Array<Record<string, unknown>> | undefined),
@ -974,49 +943,16 @@ export const ensurePricingMethodDetailRowsForServices = async (params: {
const workloadData = toStoredDetailRowsState(storeWorkloadData) || workloadDataFallback const workloadData = toStoredDetailRowsState(storeWorkloadData) || workloadDataFallback
const hourlyData = toStoredDetailRowsState(storeHourlyData) || hourlyDataFallback const hourlyData = toStoredDetailRowsState(storeHourlyData) || hourlyDataFallback
const shouldInitInvest = !Array.isArray(investData?.detailRows) const shouldInitInvest = !Array.isArray(investData?.detailRows) || investData!.detailRows!.length === 0
const shouldInitLand = !Array.isArray(landData?.detailRows) const shouldInitLand = !Array.isArray(landData?.detailRows) || landData!.detailRows!.length === 0
const shouldInitWorkload = !Array.isArray(workloadData?.detailRows) const shouldInitWorkload = !Array.isArray(workloadData?.detailRows) || workloadData!.detailRows!.length === 0
const shouldInitHourly = !Array.isArray(hourlyData?.detailRows) const shouldInitHourly = !Array.isArray(hourlyData?.detailRows) || hourlyData!.detailRows!.length === 0
console.log('[pricing][ensure-detail-rows][before] ' + JSON.stringify({
contractId: params.contractId,
serviceId,
shouldInit: {
invest: shouldInitInvest,
land: shouldInitLand,
workload: shouldInitWorkload,
hourly: shouldInitHourly
},
existingLengths: {
invest: Array.isArray(investData?.detailRows) ? investData.detailRows.length : -1,
land: Array.isArray(landData?.detailRows) ? landData.detailRows.length : -1,
workload: Array.isArray(workloadData?.detailRows) ? workloadData.detailRows.length : -1,
hourly: Array.isArray(hourlyData?.detailRows) ? hourlyData.detailRows.length : -1
}
}))
const writeTasks: Promise<unknown>[] = [] const writeTasks: Promise<unknown>[] = []
let defaultRows: PricingMethodDefaultDetailRows | null = null let defaultRows: PricingMethodDefaultDetailRows | null = null
const getDefaultRows = () => { const getDefaultRows = () => {
if (!defaultRows) { if (!defaultRows) {
defaultRows = buildDefaultPricingMethodDetailRows(serviceId, context) defaultRows = buildDefaultPricingMethodDetailRows(serviceId, context)
console.log('[pricing][ensure-detail-rows][defaults] ' + JSON.stringify({
contractId: params.contractId,
serviceId,
lengths: {
invest: Array.isArray(defaultRows.investScale) ? defaultRows.investScale.length : -1,
land: Array.isArray(defaultRows.landScale) ? defaultRows.landScale.length : -1,
workload: Array.isArray(defaultRows.workload) ? defaultRows.workload.length : -1,
hourly: Array.isArray(defaultRows.hourly) ? defaultRows.hourly.length : -1
},
sample: {
invest: Array.isArray(defaultRows.investScale) ? defaultRows.investScale[0] : null,
land: Array.isArray(defaultRows.landScale) ? defaultRows.landScale[0] : null,
workload: Array.isArray(defaultRows.workload) ? defaultRows.workload[0] : null,
hourly: Array.isArray(defaultRows.hourly) ? defaultRows.hourly[0] : null
}
}))
} }
return defaultRows return defaultRows
} }
@ -1060,13 +996,6 @@ export const ensurePricingMethodDetailRowsForServices = async (params: {
if (writeTasks.length > 0) { if (writeTasks.length > 0) {
await Promise.all(writeTasks) await Promise.all(writeTasks)
} }
console.log('[pricing][ensure-detail-rows][after] ' + JSON.stringify({
contractId: params.contractId,
serviceId,
wroteAny: writeTasks.length > 0,
writeCount: writeTasks.length
}))
}) })
) )
} }
@ -1090,4 +1019,3 @@ export const getPricingMethodTotalsForServices = async (params: {
) )
return result return result
} }

View File

@ -8,7 +8,6 @@
import { getMajorDictEntries, getMajorIdAliasMap, getServiceDictById } from '@/sql' import { getMajorDictEntries, getMajorIdAliasMap, getServiceDictById } from '@/sql'
import { toFiniteNumberOrNull } from '@/lib/decimal' import { toFiniteNumberOrNull } from '@/lib/decimal'
import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee' import { getBenchmarkBudgetByScale, getScaleBudgetFee } from '@/lib/pricingScaleFee'
import { isInvestScaleSingleTotalService } from '@/lib/servicePricing'
import type { import type {
ScaleCalcRow, ScaleCalcRow,
ScaleType, ScaleType,
@ -81,9 +80,9 @@ export const getDefaultConsultCategoryFactor = (serviceId: string | number): num
} }
/** 判断是否为仅投资规模服务 */ /** 判断是否为仅投资规模服务 */
export const isInvestScaleSingleTotalByService = (serviceId: string | number): boolean => { export const isOnlyCostScaleService = (serviceId: string | number): boolean => {
const service = (getServiceDictById() as Record<string, ServiceLite | undefined>)[String(serviceId)] const service = (getServiceDictById() as Record<string, ServiceLite | undefined>)[String(serviceId)]
return isInvestScaleSingleTotalService(service) return service?.onlyCostScale === true
} }
/* ---------------------------------------------------------------- /* ----------------------------------------------------------------
@ -113,7 +112,7 @@ export const buildDefaultScaleRows = (
consultCategoryFactor: defaultFactor, consultCategoryFactor: defaultFactor,
majorFactor: majorFactorMap?.get(id) ?? getDefaultMajorFactor(id), majorFactor: majorFactorMap?.get(id) ?? getDefaultMajorFactor(id),
workStageFactor: 1, workStageFactor: 1,
workRatio: 1 workRatio: 100
})) }))
} }
@ -217,4 +216,3 @@ export const sumNullableBy = <T>(list: T[], pick: (item: T) => number | null | u
} }
return hasValid ? total : null return hasValid ? total : null
} }

View File

@ -4,7 +4,6 @@ import {
formatScaleReadonlyMoney, formatScaleReadonlyMoney,
getScaleMergeColSpanBeforeTotal getScaleMergeColSpanBeforeTotal
} from '@/lib/pricingScaleGrid' } from '@/lib/pricingScaleGrid'
import { AgGridResetHeader } from '@/lib/agGridResetHeader'
import { i18n } from '@/i18n' import { i18n } from '@/i18n'
type ScaleColumnField<TRow> = Extract<keyof TRow, string> | string type ScaleColumnField<TRow> = Extract<keyof TRow, string> | string
@ -200,8 +199,6 @@ export const createScaleBudgetFeeColumnGroup = <TRow>(options: {
headerName: scaleT('columns.workRatio'), headerName: scaleT('columns.workRatio'),
field: 'workRatio' as any, field: 'workRatio' as any,
colId: 'workRatio', colId: 'workRatio',
headerTooltip: scaleT('tooltip.workRatio'),
headerComponent: AgGridResetHeader,
headerClass: 'ag-right-aligned-header', headerClass: 'ag-right-aligned-header',
minWidth: 80, minWidth: 80,
flex: 1, flex: 1,
@ -261,14 +258,11 @@ export const createScaleAutoGroupColumn = <TRow>(options: {
idLabelMap: Map<string, string> idLabelMap: Map<string, string>
parseProjectIndexFromPathKey: (key: string) => number | null parseProjectIndexFromPathKey: (key: string) => number | null
}) : ColDef<TRow> => ({ }) : ColDef<TRow> => ({
headerName: scaleT('columns.majorGroup'), headerName: scaleT('columns.number'),
minWidth: 250, minWidth: 250,
flex: 2, flex: 2,
wrapText: true, wrapText: true,
autoHeight: true, autoHeight: true,
cellClassRules: {
'ag-summary-label-cell': params => Boolean(params.node?.rowPinned)
},
cellStyle: { cellStyle: {
whiteSpace: 'normal', whiteSpace: 'normal',
lineHeight: '1.4' lineHeight: '1.4'
@ -282,8 +276,8 @@ export const createScaleAutoGroupColumn = <TRow>(options: {
return options.totalLabel return options.totalLabel
} }
const rowData = params.data as any const rowData = params.data as any
if (!params.node?.group && rowData?.majorCode && rowData?.majorName) { if (!params.node?.group && rowData?.majorCode) {
return `${rowData.majorCode} ${rowData.majorName}` return rowData.majorCode
} }
const nodeId = String(params.value || '') const nodeId = String(params.value || '')
const projectIndex = options.parseProjectIndexFromPathKey(nodeId) const projectIndex = options.parseProjectIndexFromPathKey(nodeId)
@ -293,8 +287,8 @@ export const createScaleAutoGroupColumn = <TRow>(options: {
tooltipValueGetter: params => { tooltipValueGetter: params => {
if (params.node?.rowPinned) return options.totalLabel if (params.node?.rowPinned) return options.totalLabel
const rowData = params.data as any const rowData = params.data as any
if (!params.node?.group && rowData?.majorCode && rowData?.majorName) { if (!params.node?.group && rowData?.majorCode) {
return `${rowData.majorCode} ${rowData.majorName}` return rowData.majorCode
} }
const nodeId = String(params.value || '') const nodeId = String(params.value || '')
const projectIndex = options.parseProjectIndexFromPathKey(nodeId) const projectIndex = options.parseProjectIndexFromPathKey(nodeId)
@ -302,3 +296,92 @@ export const createScaleAutoGroupColumn = <TRow>(options: {
return options.idLabelMap.get(nodeId) || nodeId return options.idLabelMap.get(nodeId) || nodeId
} }
}) })
export const createScaleCalculationFormulaColumnGroup = <TRow>(options: {
getCalculationBase?: (row: TRow | undefined) => string | null
getCalculationBaseValue?: (row: TRow | undefined) => number | null
getCalculationFormula?: (row: TRow | undefined) => string | null
getCalculationAmount?: (row: TRow | undefined) => number | null
isBaseEditable?: (row: TRow | undefined) => boolean
parseNumberOrNull: (value: any, options?: any) => any
}) : ColGroupDef<TRow> => ({
headerName: scaleT('columns.calculationGroup'),
marryChildren: true,
children: [
{
headerName: scaleT('columns.base'),
field: 'calculationBase' as any,
colId: 'calculationBase',
minWidth: 120,
flex: 1,
editable: params => !params.node?.group && !params.node?.rowPinned && (options.isBaseEditable?.(params.data) ?? true),
cellClass: params =>
!params.node?.group && !params.node?.rowPinned && (options.isBaseEditable?.(params.data) ?? true)
? 'editable-cell-line'
: '',
cellClassRules: {
'ag-right-aligned-cell': () => true,
'editable-cell-empty': params =>
!params.node?.group && !params.node?.rowPinned && (options.isBaseEditable?.(params.data) ?? true) && (params.value == null || params.value === '')
},
valueGetter: params => (params.node?.rowPinned ? null : options.getCalculationBase?.(params.data) ?? null),
valueParser: params => options.parseNumberOrNull(params.newValue, { precision: 3 }),
valueFormatter: params => {
if (params.node?.rowPinned) return ''
const value = params.value
return value == null || value === '' ? '' : String(value)
}
},
{
headerName: scaleT('columns.base2'),
field: 'calculationBaseValue' as any,
colId: 'calculationBaseValue',
headerClass: 'ag-right-aligned-header',
minWidth: 130,
flex: 1,
cellClassRules: {
'ag-right-aligned-cell': () => true
},
valueGetter: params => (params.node?.rowPinned ? null : options.getCalculationBaseValue?.(params.data) ?? null),
valueFormatter: params => {
if (params.node?.rowPinned) return ''
const value = params.value
if (value == null) return ''
return typeof value === 'number' ? value.toLocaleString() : String(value)
}
},
{
headerName: scaleT('columns.formula'),
field: 'calculationFormula' as any,
colId: 'calculationFormula',
minWidth: 200,
flex: 1.5,
wrapText: true,
autoHeight: true,
cellStyle: { whiteSpace: 'normal', lineHeight: '1.4' },
valueGetter: params => (params.node?.rowPinned ? null : options.getCalculationFormula?.(params.data) ?? null),
valueFormatter: params => {
if (params.node?.rowPinned) return ''
return params.value || ''
}
},
{
headerName: scaleT('columns.calculationAmount'),
field: 'calculationAmount' as any,
colId: 'calculationAmount',
headerClass: 'ag-right-aligned-header',
minWidth: 120,
flex: 1,
cellClassRules: {
'ag-right-aligned-cell': () => true
},
valueGetter: params => (params.node?.rowPinned ? null : options.getCalculationAmount?.(params.data) ?? null),
valueFormatter: params => {
if (params.node?.rowPinned) return ''
const value = params.value
if (value == null) return ''
return typeof value === 'number' ? value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : String(value)
}
}
]
})

View File

@ -54,7 +54,7 @@ export const getBenchmarkBudgetByScale = (value: unknown, mode: ScaleMode) => {
* *
* *
* 1. basic / optional * 1. basic / optional
* 2. * 2.
* 3. / / * 3. / /
* *
* `getBenchmarkBudgetSplitByScale` * `getBenchmarkBudgetSplitByScale`
@ -78,7 +78,7 @@ export const getScaleBudgetFeeSplit = (params: {
const majorFactor = toFiniteNumberOrNull(params.majorFactor) const majorFactor = toFiniteNumberOrNull(params.majorFactor)
const consultCategoryFactor = toFiniteNumberOrNull(params.consultCategoryFactor) const consultCategoryFactor = toFiniteNumberOrNull(params.consultCategoryFactor)
const workStageFactor = hasWorkStageFactor ? toFiniteNumberOrNull(params.workStageFactor) : 1 const workStageFactor = hasWorkStageFactor ? toFiniteNumberOrNull(params.workStageFactor) : 1
const workRatio = hasWorkRatio ? toFiniteNumberOrNull(params.workRatio) : 1 const workRatio = hasWorkRatio ? toFiniteNumberOrNull(params.workRatio) : 100
if ( if (
benchmarkBudgetBasic == null || benchmarkBudgetBasic == null ||
@ -95,6 +95,7 @@ export const getScaleBudgetFeeSplit = (params: {
.mul(majorFactor) .mul(majorFactor)
.mul(workStageFactor) .mul(workStageFactor)
.mul(workRatio) .mul(workRatio)
.div(100)
const roundedBenchmarkBudget = roundTo(addNumbers(benchmarkBudgetBasic, benchmarkBudgetOptional), 2) const roundedBenchmarkBudget = roundTo(addNumbers(benchmarkBudgetBasic, benchmarkBudgetOptional), 2)
const basic = roundTo(toDecimal(benchmarkBudgetBasic).mul(multiplier), 2) const basic = roundTo(toDecimal(benchmarkBudgetBasic).mul(multiplier), 2)
const optional = roundTo(toDecimal(benchmarkBudgetOptional).mul(multiplier), 2) const optional = roundTo(toDecimal(benchmarkBudgetOptional).mul(multiplier), 2)
@ -120,7 +121,7 @@ export const getScaleBudgetFee = (params: {
const majorFactor = toFiniteNumberOrNull(params.majorFactor) const majorFactor = toFiniteNumberOrNull(params.majorFactor)
const consultCategoryFactor = toFiniteNumberOrNull(params.consultCategoryFactor) const consultCategoryFactor = toFiniteNumberOrNull(params.consultCategoryFactor)
const workStageFactor = hasWorkStageFactor ? toFiniteNumberOrNull(params.workStageFactor) : 1 const workStageFactor = hasWorkStageFactor ? toFiniteNumberOrNull(params.workStageFactor) : 1
const workRatio = hasWorkRatio ? toFiniteNumberOrNull(params.workRatio) : 1 const workRatio = hasWorkRatio ? toFiniteNumberOrNull(params.workRatio) : 100
if ( if (
benchmarkBudget == null || benchmarkBudget == null ||
@ -137,5 +138,6 @@ export const getScaleBudgetFee = (params: {
.mul(majorFactor) .mul(majorFactor)
.mul(workStageFactor) .mul(workStageFactor)
.mul(workRatio) .mul(workRatio)
.div(100)
return roundTo(toDecimal(roundedBenchmarkBudget).mul(multiplier), 2) return roundTo(toDecimal(roundedBenchmarkBudget).mul(multiplier), 2)
} }

View File

@ -294,10 +294,10 @@ export const initializeProjectFactorStates = async (
detailRows: buildFactorRowsFromEntries(majorEntries) detailRows: buildFactorRowsFromEntries(majorEntries)
} }
// 新项目初始化走 createProjectKvAdapter 时setItem 是整包读改写,不是原子更新。 await Promise.all([
// 这里并发写两个 key 会互相覆盖,导致咨询系数或专业系数其中一个丢失。 kvStore.setItem(consultCategoryFactorKey, consultPayload),
await kvStore.setItem(consultCategoryFactorKey, consultPayload) kvStore.setItem(majorFactorKey, majorPayload)
await kvStore.setItem(majorFactorKey, majorPayload) ])
} }
export const initializeProjectScaleState = async ( export const initializeProjectScaleState = async (
@ -307,4 +307,3 @@ export const initializeProjectScaleState = async (
) => { ) => {
await kvStore.setItem(projectScaleKey, buildDefaultProjectScaleState(industry)) await kvStore.setItem(projectScaleKey, buildDefaultProjectScaleState(industry))
} }

View File

@ -1,4 +1,4 @@
import { getMajorDictById, getMajorIdAliasMap, serviceList } from '@/sql' import { serviceList } from '@/sql'
import { roundTo, toFiniteNumber, toFiniteNumberOrZero } from '@/lib/decimal' import { roundTo, toFiniteNumber, toFiniteNumberOrZero } from '@/lib/decimal'
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee' import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
export { toFiniteNumber, toFiniteNumberOrZero } export { toFiniteNumber, toFiniteNumberOrZero }
@ -52,7 +52,6 @@ interface ScaleRowLike {
interface WorkloadMethodRowLike { interface WorkloadMethodRowLike {
id: string id: string
conversion?: unknown
budgetAdoptedUnitPrice?: unknown budgetAdoptedUnitPrice?: unknown
workload?: unknown workload?: unknown
basicFee?: unknown basicFee?: unknown
@ -247,18 +246,6 @@ export const toScaleMajorId = (row: ScaleMethodRowLike): number | null => {
return toSafeInteger(parsed.majorPart) return toSafeInteger(parsed.majorPart)
} }
const majorDictById = getMajorDictById() as Record<string, { hasCost?: unknown; hasArea?: unknown } | undefined>
const majorIdAliasMap = getMajorIdAliasMap()
const resolveMajorCapability = (majorId: number | null) => {
if (majorId == null) return null
const key = String(majorId)
const resolvedKey = Object.prototype.hasOwnProperty.call(majorDictById, key)
? key
: (majorIdAliasMap.get(key) || key)
return majorDictById[resolvedKey] || null
}
export const toScaleProNum = (row: ScaleMethodRowLike): number => { export const toScaleProNum = (row: ScaleMethodRowLike): number => {
const parsed = parseScaleScopedRowId(row.id) const parsed = parseScaleScopedRowId(row.id)
return parsed.proNum > 0 ? parsed.proNum : 1 return parsed.proNum > 0 ? parsed.proNum : 1
@ -289,16 +276,7 @@ const isExportableScaleMethodRow = (
mode: 'cost' | 'area' mode: 'cost' | 'area'
) => { ) => {
if (!isScaleLeafRow(row)) return false if (!isScaleLeafRow(row)) return false
if (mode === 'cost') { return mode === 'cost' ? row?.hasCost === true : row?.hasArea === true
if (row?.hasCost === true) return true
if (row?.hasCost === false) return false
} else {
if (row?.hasArea === true) return true
if (row?.hasArea === false) return false
}
const major = row ? resolveMajorCapability(toScaleMajorId(row)) : null
if (!major) return false
return mode === 'cost' ? major.hasCost !== false : major.hasArea !== false
} }
export const normalizeTaskText = (value: unknown): string => String(value || '').trim() export const normalizeTaskText = (value: unknown): string => String(value || '').trim()
@ -324,14 +302,9 @@ export const resolveScaleMethodFee = (row: ScaleMethodRowLike, mode: 'cost' | 'a
workRatio: row.workRatio workRatio: row.workRatio
}) })
: null : null
const basicFee = allUnchecked const basicFee = allUnchecked ? null : (toFiniteNumber(row.budgetFee) ?? computedSplit?.total ?? null)
? null const basicFeeBasic = allUnchecked ? null : (toFiniteNumber(row.budgetFeeBasic) ?? computedSplit?.basic ?? null)
: (benchmarkBudgetBasic != null && benchmarkBudgetOptional != null const basicFeeOptional = allUnchecked ? null : (toFiniteNumber(row.budgetFeeOptional) ?? computedSplit?.optional ?? null)
? roundTo(benchmarkBudgetBasic + benchmarkBudgetOptional, 2)
: null)
const basicFeeBasic = allUnchecked ? null : benchmarkBudgetBasic
const basicFeeOptional = allUnchecked ? null : benchmarkBudgetOptional
const serviceFee = allUnchecked ? null : (toFiniteNumber(row.budgetFee) ?? computedSplit?.total ?? null)
const basicFormula = typeof row.basicFormula === 'string' && row.basicFormula.trim() const basicFormula = typeof row.basicFormula === 'string' && row.basicFormula.trim()
? row.basicFormula ? row.basicFormula
: (basicChecked ? (benchmarkSplit?.basicFormula ?? '') : '') : (basicChecked ? (benchmarkSplit?.basicFormula ?? '') : '')
@ -342,7 +315,6 @@ export const resolveScaleMethodFee = (row: ScaleMethodRowLike, mode: 'cost' | 'a
basicFee, basicFee,
basicFeeBasic, basicFeeBasic,
basicFeeOptional, basicFeeOptional,
serviceFee,
basicFormula, basicFormula,
optionalFormula optionalFormula
} }
@ -468,12 +440,11 @@ export const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined) => {
const cost = toFiniteNumber(row.amount) const cost = toFiniteNumber(row.amount)
const feeResolved = resolveScaleMethodFee(row, 'cost') const feeResolved = resolveScaleMethodFee(row, 'cost')
const basicFee = feeResolved.basicFee const basicFee = feeResolved.basicFee
const serviceFee = feeResolved.serviceFee if (basicFee != null) hasTotalValue = true
if (serviceFee != null) hasTotalValue = true
const basicFeeBasic = feeResolved.basicFeeBasic const basicFeeBasic = feeResolved.basicFeeBasic
const basicFeeOptional = feeResolved.basicFeeOptional const basicFeeOptional = feeResolved.basicFeeOptional
const remark = typeof row.remark === 'string' ? row.remark : '' const remark = typeof row.remark === 'string' ? row.remark : ''
if (basicFee == null && serviceFee == null) return null if (basicFee == null) return null
return { return {
proNum, proNum,
major, major,
@ -487,7 +458,7 @@ export const buildMethod1 = (rows: ScaleMethodRowLike[] | undefined) => {
majorCoe: toFiniteNumberOrZero(row.majorFactor), majorCoe: toFiniteNumberOrZero(row.majorFactor),
processCoe: toFiniteNumber(row.workStageFactor) ?? 1, processCoe: toFiniteNumber(row.workStageFactor) ?? 1,
proportion: toFiniteNumber(row.workRatio) ?? 1, proportion: toFiniteNumber(row.workRatio) ?? 1,
fee: toMoney(serviceFee), fee: toMoney(basicFee),
remark remark
} }
}) })
@ -520,12 +491,11 @@ export const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined) => {
const area = toFiniteNumber(row.landArea) const area = toFiniteNumber(row.landArea)
const feeResolved = resolveScaleMethodFee(row, 'area') const feeResolved = resolveScaleMethodFee(row, 'area')
const basicFee = feeResolved.basicFee const basicFee = feeResolved.basicFee
const serviceFee = feeResolved.serviceFee if (basicFee != null) hasTotalValue = true
if (serviceFee != null) hasTotalValue = true
const basicFeeBasic = feeResolved.basicFeeBasic const basicFeeBasic = feeResolved.basicFeeBasic
const basicFeeOptional = feeResolved.basicFeeOptional const basicFeeOptional = feeResolved.basicFeeOptional
const remark = typeof row.remark === 'string' ? row.remark : '' const remark = typeof row.remark === 'string' ? row.remark : ''
if (basicFee == null && serviceFee == null) return null if (basicFee == null) return null
return { return {
proNum, proNum,
major, major,
@ -539,7 +509,7 @@ export const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined) => {
majorCoe: toFiniteNumberOrZero(row.majorFactor), majorCoe: toFiniteNumberOrZero(row.majorFactor),
processCoe: toFiniteNumber(row.workStageFactor) ?? 1, processCoe: toFiniteNumber(row.workStageFactor) ?? 1,
proportion: toFiniteNumber(row.workRatio) ?? 1, proportion: toFiniteNumber(row.workRatio) ?? 1,
fee: toMoney(serviceFee), fee: toMoney(basicFee),
remark remark
} }
}) })
@ -557,34 +527,16 @@ export const buildMethod2 = (rows: ScaleMethodRowLike[] | undefined) => {
} }
} }
const resolveWorkloadBasicFee = (row: WorkloadMethodRowLike) => {
const basicFee = toFiniteNumber(row.basicFee)
if (basicFee != null) return basicFee
const price = toFiniteNumber(row.budgetAdoptedUnitPrice)
const conversion = toFiniteNumber(row.conversion)
const amount = toFiniteNumber(row.workload)
if (price == null || conversion == null || amount == null) return null
return roundTo(price * conversion * amount, 2)
}
const resolveWorkloadServiceFee = (row: WorkloadMethodRowLike, basicFee: number | null) => {
const fee = toFiniteNumber(row.serviceFee)
if (fee != null) return fee
const factor = toFiniteNumber(row.consultCategoryFactor)
if (basicFee == null || factor == null) return null
return roundTo(basicFee * factor, 2)
}
export const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined) => { export const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined) => {
if (!Array.isArray(rows)) return null if (!Array.isArray(rows)) return null
let hasTotalValue = false let hasTotalValue = false
const det = rows const det = rows
.map(row => { .map(row => {
const task = getTaskIdFromRowId(row.id) const task = getTaskIdFromRowId(row.id)
if (task == null) return null if (task == null || row.basicFee == null) return null
const amount = toFiniteNumber(row.workload) const amount = toFiniteNumber(row.workload)
const basicFee = resolveWorkloadBasicFee(row) const basicFee = toFiniteNumber(row.basicFee)
const fee = resolveWorkloadServiceFee(row, basicFee) const fee = toFiniteNumber(row.serviceFee)
if (fee != null) hasTotalValue = true if (fee != null) hasTotalValue = true
const remark = typeof row.remark === 'string' ? row.remark : '' const remark = typeof row.remark === 'string' ? row.remark : ''
const hasValue = amount != null || basicFee != null || fee != null || isNonEmptyString(remark) const hasValue = amount != null || basicFee != null || fee != null || isNonEmptyString(remark)
@ -609,26 +561,16 @@ export const buildMethod3 = (rows: WorkloadMethodRowLike[] | undefined) => {
} }
} }
const resolveHourlyServiceFee = (row: HourlyMethodRowLike) => {
const fee = toFiniteNumber(row.serviceBudget)
if (fee != null) return fee
const price = toFiniteNumber(row.adoptedBudgetUnitPrice)
const personNum = toFiniteNumber(row.personnelCount)
const workDay = toFiniteNumber(row.workdayCount)
if (price == null || personNum == null || workDay == null) return null
return roundTo(price * personNum * workDay, 2)
}
export const buildMethod4 = (rows: HourlyMethodRowLike[] | undefined) => { export const buildMethod4 = (rows: HourlyMethodRowLike[] | undefined) => {
if (!Array.isArray(rows)) return null if (!Array.isArray(rows)) return null
let hasTotalValue = false let hasTotalValue = false
const det = rows const det = rows
.map(row => { .map(row => {
const expert = getExpertIdFromRowId(row.id) const expert = getExpertIdFromRowId(row.id)
if (expert == null) return null if (expert == null || row.serviceBudget == null) return null
const personNum = toFiniteNumber(row.personnelCount) const personNum = toFiniteNumber(row.personnelCount)
const workDay = toFiniteNumber(row.workdayCount) const workDay = toFiniteNumber(row.workdayCount)
const fee = resolveHourlyServiceFee(row) const fee = toFiniteNumber(row.serviceBudget)
if (fee != null) hasTotalValue = true if (fee != null) hasTotalValue = true
const remark = typeof row.remark === 'string' ? row.remark : '' const remark = typeof row.remark === 'string' ? row.remark : ''
const hasValue = personNum != null || workDay != null || fee != null || isNonEmptyString(remark) const hasValue = personNum != null || workDay != null || fee != null || isNonEmptyString(remark)

View File

@ -1,63 +0,0 @@
import type { ServiceLite } from '@/types/pricing'
type ServicePricingCapabilitySource = Partial<Pick<
ServiceLite,
| 'enableInvestScale'
| 'enableLandScale'
| 'investScaleSingleTotal'
| 'scale'
| 'onlyCostScale'
| 'amount'
| 'workDay'
>>
export interface ServicePricingCapabilities {
investScaleEnabled: boolean
landScaleEnabled: boolean
investScaleSingleTotal: boolean
workloadEnabled: boolean
hourlyEnabled: boolean
}
export const resolveMethodEnabled = (value: unknown, fallback: boolean) => {
if (typeof value === 'boolean') return value
if (typeof value === 'number') return value === 1
if (typeof value === 'string') {
const normalized = value.trim().toLowerCase()
if (normalized === 'true' || normalized === '1') return true
if (normalized === 'false' || normalized === '0') return false
}
return fallback
}
export const resolveServicePricingCapabilities = (
service: ServicePricingCapabilitySource | null | undefined,
defaults: Partial<ServicePricingCapabilities> = {}
): ServicePricingCapabilities => {
const legacyScaleEnabled = resolveMethodEnabled(service?.scale, defaults.investScaleEnabled ?? false)
const legacyOnlyCostScale = resolveMethodEnabled(service?.onlyCostScale, defaults.investScaleSingleTotal ?? false)
const investScaleEnabled = resolveMethodEnabled(
service?.enableInvestScale,
legacyScaleEnabled
)
const landScaleEnabled = resolveMethodEnabled(
service?.enableLandScale,
legacyScaleEnabled && !legacyOnlyCostScale
)
const investScaleSingleTotal = resolveMethodEnabled(
service?.investScaleSingleTotal,
legacyOnlyCostScale
)
return {
investScaleEnabled,
landScaleEnabled,
investScaleSingleTotal,
workloadEnabled: resolveMethodEnabled(service?.amount, defaults.workloadEnabled ?? false),
hourlyEnabled: resolveMethodEnabled(service?.workDay, defaults.hourlyEnabled ?? false)
}
}
export const isInvestScaleSingleTotalService = (service: ServicePricingCapabilitySource | null | undefined) =>
resolveServicePricingCapabilities(service).investScaleSingleTotal

View File

@ -13,15 +13,9 @@ export const PROJECT_ID_QUERY_KEY = 'projectId'
export const NEW_PROJECT_QUERY_KEY = 'newProject' export const NEW_PROJECT_QUERY_KEY = 'newProject'
export const OPEN_PROJECT_DIALOG_QUERY_KEY = 'openProjectDialog' export const OPEN_PROJECT_DIALOG_QUERY_KEY = 'openProjectDialog'
export const FORCE_HOME_QUERY_KEY = 'forceHome' export const FORCE_HOME_QUERY_KEY = 'forceHome'
export const DISCLAIMER_ENTRY_QUERY_KEY = 'from'
export const DISCLAIMER_ENTRY_QUERY_VALUE = 'gov'
export const DEFAULT_PROJECT_ID = 'default' export const DEFAULT_PROJECT_ID = 'default'
export const QUICK_PROJECT_ID = 'quick' export const QUICK_PROJECT_ID = 'quick'
export const PROJECT_DB_NAME_PREFIX = 'DB' export const PROJECT_DB_NAME_PREFIX = 'DB'
export const DISCLAIMER_ACCEPTANCE_STORAGE_KEY = 'jgjs-disclaimer-accepted-v1'
export const DISCLAIMER_ACCEPTED_EVENT = 'jgjs-disclaimer-accepted'
export const DISCLAIMER_PENDING_ACTION_STORAGE_KEY = 'jgjs-disclaimer-pending-action-v1'
export const DISCLAIMER_RETURN_URL_QUERY_KEY = 'returnUrl'
export const QUICK_CONTRACT_ID = 'quick-contract-default' export const QUICK_CONTRACT_ID = 'quick-contract-default'
export const QUICK_CONTRACT_META_KEY = 'quick-contract-meta-v1' export const QUICK_CONTRACT_META_KEY = 'quick-contract-meta-v1'
@ -48,11 +42,6 @@ export interface QuickContractMeta {
updatedAt: string updatedAt: string
} }
export interface DisclaimerPendingAction {
type: 'project' | 'quick' | 'import' | 'existing-project'
projectId?: string
}
export const readWorkspaceMode = (): WorkspaceMode => { export const readWorkspaceMode = (): WorkspaceMode => {
@ -196,107 +185,6 @@ export const buildProjectUrl = (
} }
} }
const readDisclaimerAcceptedEntries = () => {
try {
const raw = window.localStorage.getItem(DISCLAIMER_ACCEPTANCE_STORAGE_KEY)
if (!raw) return {}
const parsed = JSON.parse(raw) as Record<string, unknown>
if (!parsed || typeof parsed !== 'object') return {}
return parsed
} catch {
return {}
}
}
export const readRestrictedEntryCodeFromUrl = (href?: string | URL | null) => {
try {
const url = href instanceof URL ? href : new URL(href || window.location.href, window.location.href)
const entry = String(url.searchParams.get(DISCLAIMER_ENTRY_QUERY_KEY) || '').trim()
return entry
} catch {
return ''
}
}
export const isRestrictedDisclaimerEntry = (entryRaw: string) =>
String(entryRaw || '').trim() === DISCLAIMER_ENTRY_QUERY_VALUE
export const isDisclaimerAcceptanceRequired = (href?: string | URL | null) =>
isRestrictedDisclaimerEntry(readRestrictedEntryCodeFromUrl(href))
export const hasAcceptedRestrictedDisclaimer = (entryRaw?: string) => {
const entry = String(entryRaw || readRestrictedEntryCodeFromUrl() || '').trim()
if (!isRestrictedDisclaimerEntry(entry)) return false
const acceptedMap = readDisclaimerAcceptedEntries()
return acceptedMap[entry] === true
}
export const persistRestrictedDisclaimerAcceptance = (entryRaw?: string) => {
const entry = String(entryRaw || readRestrictedEntryCodeFromUrl() || '').trim()
if (!isRestrictedDisclaimerEntry(entry)) return false
try {
const acceptedMap = readDisclaimerAcceptedEntries()
acceptedMap[entry] = true
window.localStorage.setItem(DISCLAIMER_ACCEPTANCE_STORAGE_KEY, JSON.stringify(acceptedMap))
window.dispatchEvent(new CustomEvent(DISCLAIMER_ACCEPTED_EVENT, {
detail: {
entry
}
}))
return true
} catch {
return false
}
}
export const setPendingDisclaimerAction = (action: DisclaimerPendingAction | null) => {
try {
if (!action) {
window.sessionStorage.removeItem(DISCLAIMER_PENDING_ACTION_STORAGE_KEY)
return
}
window.sessionStorage.setItem(DISCLAIMER_PENDING_ACTION_STORAGE_KEY, JSON.stringify(action))
} catch {
// ignore session storage errors
}
}
export const consumePendingDisclaimerAction = () => {
try {
const raw = window.sessionStorage.getItem(DISCLAIMER_PENDING_ACTION_STORAGE_KEY)
window.sessionStorage.removeItem(DISCLAIMER_PENDING_ACTION_STORAGE_KEY)
if (!raw) return null
const parsed = JSON.parse(raw) as DisclaimerPendingAction
if (!parsed || typeof parsed !== 'object') return null
const type = String(parsed.type || '').trim()
if (!type) return null
if (!['project', 'quick', 'import', 'existing-project'].includes(type)) return null
return {
type: type as DisclaimerPendingAction['type'],
projectId: typeof parsed.projectId === 'string' ? parsed.projectId.trim() : undefined
}
} catch {
return null
}
}
export const buildDisclaimerUrl = (returnUrl?: string) => {
try {
const url = new URL(window.location.href)
const target = new URL(`${url.pathname}${url.search}`, url.origin)
if (returnUrl) {
target.hash = `#/disclaimer?${new URLSearchParams({
[DISCLAIMER_RETURN_URL_QUERY_KEY]: returnUrl
}).toString()}`
} else {
target.hash = '#/disclaimer'
}
return target.toString()
} catch {
return './#/disclaimer'
}
}
export const getProjectDbName = (projectIdRaw: string) => { export const getProjectDbName = (projectIdRaw: string) => {
const projectId = normalizeProjectId(projectIdRaw) const projectId = normalizeProjectId(projectIdRaw)
return `${PROJECT_DB_NAME_PREFIX}-${projectId}` return `${PROJECT_DB_NAME_PREFIX}-${projectId}`

View File

@ -22,15 +22,6 @@ type FactorDictItem = {
type FactorDict = Record<string, FactorDictItem> type FactorDict = Record<string, FactorDictItem>
const hasUsableFactorRows = (state: XmFactorState | null | undefined) =>
Array.isArray(state?.detailRows) &&
state.detailRows.some(row => {
const hasFactor =
toFiniteNumberOrNull(row?.budgetValue) != null ||
toFiniteNumberOrNull(row?.standardFactor) != null
return hasFactor || String(row?.id || '').trim() !== ''
})
const buildStandardFactorMap = (dict: FactorDict): Map<string, number | null> => { const buildStandardFactorMap = (dict: FactorDict): Map<string, number | null> => {
const map = new Map<string, number | null>() const map = new Map<string, number | null>()
for (const [id, item] of Object.entries(dict)) { for (const [id, item] of Object.entries(dict)) {
@ -77,12 +68,7 @@ const loadFactorMap = async (
const zxFwPricingStore = getZxFwPricingStoreSafely() const zxFwPricingStore = getZxFwPricingStoreSafely()
const kvStore = getKvStoreSafely() const kvStore = getKvStoreSafely()
const piniaData = zxFwPricingStore ? await zxFwPricingStore.loadKeyState<XmFactorState>(storageKey) : null const piniaData = zxFwPricingStore ? await zxFwPricingStore.loadKeyState<XmFactorState>(storageKey) : null
const kvData = kvStore ? await kvStore.getItem<XmFactorState>(storageKey) : null const data = piniaData ?? (kvStore ? await kvStore.getItem<XmFactorState>(storageKey) : null)
const data = hasUsableFactorRows(piniaData)
? piniaData
: hasUsableFactorRows(kvData)
? kvData
: (piniaData ?? kvData)
const map = buildStandardFactorMap(dict) const map = buildStandardFactorMap(dict)
for (const row of data?.detailRows || []) { for (const row of data?.detailRows || []) {
if (!row?.id) continue if (!row?.id) continue

View File

@ -1,6 +1,5 @@
import { roundTo, sumByNumber, toDecimal } from '@/lib/decimal' import { roundTo, sumByNumber, toDecimal } from '@/lib/decimal'
import { ensurePricingMethodDetailRowsForServices } from '@/lib/pricingMethodTotals' import { ensurePricingMethodDetailRowsForServices } from '@/lib/pricingMethodTotals'
import { isInvestScaleSingleTotalService, resolveServicePricingCapabilities } from '@/lib/servicePricing'
import { getServiceDictItemById } from '@/sql' import { getServiceDictItemById } from '@/sql'
import { import {
isSameNullableNumber, isSameNullableNumber,
@ -65,10 +64,6 @@ type WorkloadDetailRow = {
} }
type ServiceLite = { type ServiceLite = {
enableInvestScale?: boolean | null
enableLandScale?: boolean | null
investScaleSingleTotal?: boolean | null
scale?: boolean | null
onlyCostScale?: boolean | null onlyCostScale?: boolean | null
} }
@ -113,14 +108,9 @@ const getWorkloadMethodTotalServiceFee = (rows: WorkloadDetailRow[]) => {
return roundTo(sumByNumber(rows, row => row.serviceFee ?? null), 2) return roundTo(sumByNumber(rows, row => row.serviceFee ?? null), 2)
} }
const usesInvestScaleSingleTotal = (serviceId: string) => { const isOnlyCostScaleService = (serviceId: string) => {
const service = getServiceDictItemById(serviceId) as ServiceLite | undefined const service = getServiceDictItemById(serviceId) as ServiceLite | undefined
return isInvestScaleSingleTotalService(service) return service?.onlyCostScale === true
}
const getScaleMethodCapabilities = (serviceId: string) => {
const service = getServiceDictItemById(serviceId) as ServiceLite | undefined
return resolveServicePricingCapabilities(service)
} }
const matchesChangedScaleRow = ( const matchesChangedScaleRow = (
@ -155,7 +145,7 @@ const syncScaleMethodRows = async (params: {
let changedRowCount = 0 let changedRowCount = 0
const useSummaryScaleValues = const useSummaryScaleValues =
methodState.detailRows.length === 1 || methodState.detailRows.length === 1 ||
(params.method === 'investScale' && usesInvestScaleSingleTotal(params.serviceId)) (params.method === 'investScale' && isOnlyCostScaleService(params.serviceId))
const nextRows = methodState.detailRows.map(rawRow => { const nextRows = methodState.detailRows.map(rawRow => {
const mode = params.method === 'investScale' ? 'cost' : 'area' const mode = params.method === 'investScale' ? 'cost' : 'area'
if (!matchesChangedScaleRow(rawRow, params.changedRowIds, { bypassFilter: useSummaryScaleValues })) return rawRow if (!matchesChangedScaleRow(rawRow, params.changedRowIds, { bypassFilter: useSummaryScaleValues })) return rawRow
@ -253,9 +243,6 @@ export const syncContractScaleToPricing = async (
let updatedRowCount = 0 let updatedRowCount = 0
for (const serviceId of selectedIds) { for (const serviceId of selectedIds) {
const capabilities = getScaleMethodCapabilities(serviceId)
if (capabilities.investScaleEnabled) {
const investChangedCount = await syncScaleMethodRows({ const investChangedCount = await syncScaleMethodRows({
contractId, contractId,
serviceId, serviceId,
@ -270,9 +257,6 @@ export const syncContractScaleToPricing = async (
updatedMethodCount += 1 updatedMethodCount += 1
updatedRowCount += investChangedCount updatedRowCount += investChangedCount
} }
}
if (capabilities.landScaleEnabled) {
const landChangedCount = await syncScaleMethodRows({ const landChangedCount = await syncScaleMethodRows({
contractId, contractId,
serviceId, serviceId,
@ -288,7 +272,6 @@ export const syncContractScaleToPricing = async (
updatedRowCount += landChangedCount updatedRowCount += landChangedCount
} }
} }
}
return { return {
updatedServiceCount: updatedServiceIdSet.size, updatedServiceCount: updatedServiceIdSet.size,
@ -533,4 +516,3 @@ export const syncPricingTotalToZxFw = async (params: {
const store = useZxFwPricingStore() const store = useZxFwPricingStore()
return store.updatePricingField(params) return store.updatePricingField(params)
} }

View File

@ -27,8 +27,6 @@ import './style.css'
import { i18n } from '@/i18n' import { i18n } from '@/i18n'
import { ensureProjectIdInUrl, getProjectDbName } from '@/lib/workspace' import { ensureProjectIdInUrl, getProjectDbName } from '@/lib/workspace'
import { listProjects } from '@/lib/projectRegistry' import { listProjects } from '@/lib/projectRegistry'
import { collectActiveProjectSessionLocks } from '@/lib/projectSessionLock'
import { router } from '@/router'
LicenseManager.setLicenseKey( LicenseManager.setLicenseKey(
'[v3][RELEASE][0102]_NDg2Njc4MzY3MDgzNw==16d78ca762fb5d2ff740aed081e2af7b' '[v3][RELEASE][0102]_NDg2Njc4MzY3MDgzNw==16d78ca762fb5d2ff740aed081e2af7b'
@ -53,20 +51,14 @@ const AG_GRID_MODULES = [
LocaleModule,ValidationModule ,CellSpanModule ,RowStyleModule ,RowSelectionModule ,ServerSideRowModelApiModule LocaleModule,ValidationModule ,CellSpanModule ,RowStyleModule ,RowSelectionModule ,ServerSideRowModelApiModule
] ]
const isDisclaimerRoute = () => String(window.location.hash || '').startsWith('#/disclaimer')
const pickBootstrapProjectId = () => { const pickBootstrapProjectId = () => {
if (isDisclaimerRoute()) return 'default'
try { try {
const url = new URL(window.location.href) const url = new URL(window.location.href)
const explicit = String(url.searchParams.get('projectId') || '').trim() const explicit = String(url.searchParams.get('projectId') || '').trim()
if (explicit) return ensureProjectIdInUrl() if (explicit) return ensureProjectIdInUrl()
const projects = listProjects() const projects = listProjects()
const openedProjectIds = collectActiveProjectSessionLocks(projects.map(project => project.id)) if (projects.length > 0) {
const availableProjects = projects.filter(project => !openedProjectIds.has(project.id)) const lastEdited = projects[0]
if (availableProjects.length > 0) {
const lastEdited = availableProjects[0]
url.searchParams.set('projectId', lastEdited.id) url.searchParams.set('projectId', lastEdited.id)
window.history.replaceState({}, '', `${url.pathname}${url.search}${url.hash}`) window.history.replaceState({}, '', `${url.pathname}${url.search}${url.hash}`)
} }
@ -91,4 +83,4 @@ uiPrefsStore.initFromStorage()
// 在应用启动时一次性注册 AG Grid 运行所需模块。 // 在应用启动时一次性注册 AG Grid 运行所需模块。
ModuleRegistry.registerModules(AG_GRID_MODULES) ModuleRegistry.registerModules(AG_GRID_MODULES)
createApp(App).use(pinia).use(i18n).use(router).mount('#app') createApp(App).use(pinia).use(i18n).mount('#app')

522
src/pinia/zx.ts Normal file
View File

@ -0,0 +1,522 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
/**
*
*
*/
export interface DataItem {
id: string
[key: string]: any
}
/**
*
*/
export interface QueryCondition {
field?: string
value?: any
operator?: 'eq' | 'neq' | 'gt' | 'lt' | 'gte' | 'lte' | 'includes'
}
/**
* Store
* state
*/
export const useDataStore = defineStore('zx', () => {
// ==================== State ====================
/**
*
* { [id: string]: DataItem }
*/
const items = ref<Record<string, DataItem>>({})
// ==================== Getters ====================
/**
*
*/
const allItems = computed<DataItem[]>(() => {
return Object.values(items.value)
})
/**
*
*/
const count = computed<number>(() => {
return Object.keys(items.value).length
})
/**
* ID
*/
const allIds = computed<string[]>(() => {
return Object.keys(items.value)
})
// ==================== Actions ====================
/**
* /
* @param item id
* @returns
*
* @example
* ```ts
* const store = useDataStore()
* await store.create({ id: '1', name: '测试', value: 100 })
* ```
*/
const create = async (item: DataItem): Promise<boolean> => {
if (!item || !item.id) {
console.warn('[DataStore] 创建失败:缺少 id 字段')
return false
}
const id = String(item.id)
// 检查是否已存在
if (items.value[id]) {
console.warn(`[DataStore] 创建失败ID "${id}" 已存在`)
return false
}
// 深拷贝后存储
items.value[id] = JSON.parse(JSON.stringify(item))
console.log(`[DataStore] 创建成功:${id}`)
return true
}
/**
*
* @param itemList
* @returns
*
* @example
* ```ts
* await store.createBatch([
* { id: '1', name: '项目1' },
* { id: '2', name: '项目2' }
* ])
* ```
*/
const createBatch = async (itemList: DataItem[]): Promise<number> => {
let successCount = 0
for (const item of itemList) {
const result = await create(item)
if (result) successCount++
}
console.log(`[DataStore] 批量创建完成:成功 ${successCount}/${itemList.length}`)
return successCount
}
/**
* Upsert
* ID
* @param item id
* @returns 'created' | 'updated'
*
* @example
* ```ts
* const result = await store.upsert({ id: '1', name: '测试', value: 100 })
* console.log(result) // 'created' 或 'updated'
* ```
*/
const upsert = async (item: DataItem): Promise<'created' | 'updated'> => {
if (!item || !item.id) {
console.warn('[DataStore] Upsert 失败:缺少 id 字段')
throw new Error('数据项必须包含 id 字段')
}
const id = String(item.id)
const exists = Object.prototype.hasOwnProperty.call(items.value, id)
// 深拷贝后存储
items.value[id] = JSON.parse(JSON.stringify(item))
if (exists) {
console.log(`[DataStore] Upsert 更新:${id}`)
return 'updated'
} else {
console.log(`[DataStore] Upsert 创建:${id}`)
return 'created'
}
}
/**
* Batch Upsert
* @param itemList
* @returns { created: number, updated: number }
*
* @example
* ```ts
* const stats = await store.upsertBatch([
* { id: '1', name: '项目1' }, // 如果存在则更新,否则创建
* { id: '2', name: '项目2' }
* ])
* console.log(stats) // { created: 1, updated: 1 }
* ```
*/
const upsertBatch = async (itemList: DataItem[]): Promise<{ created: number; updated: number }> => {
let createdCount = 0
let updatedCount = 0
for (const item of itemList) {
const result = await upsert(item)
if (result === 'created') {
createdCount++
} else {
updatedCount++
}
}
console.log(`[DataStore] 批量 Upsert 完成:创建 ${createdCount} 条,更新 ${updatedCount} 条,共 ${itemList.length}`)
return { created: createdCount, updated: updatedCount }
}
/**
*
* @param id ID
* @returns null
*
* @example
* ```ts
* const item = await store.read('1')
* if (item) {
* console.log(item.name)
* }
* ```
*/
const read = async (id: string): Promise<DataItem | null> => {
const key = String(id)
if (!items.value[key]) {
return null
}
// 返回深拷贝,避免外部修改影响 store
return JSON.parse(JSON.stringify(items.value[key]))
}
/**
*
* @param ids ID
* @returns
*
* @example
* ```ts
* const items = await store.readBatch(['1', '2', '3'])
* ```
*/
const readBatch = async (ids: string[]): Promise<DataItem[]> => {
const results: DataItem[] = []
for (const id of ids) {
const item = await read(id)
if (item) {
results.push(item)
}
}
return results
}
/**
*
* @returns
*/
const readAll = async (): Promise<DataItem[]> => {
return JSON.parse(JSON.stringify(allItems.value))
}
/**
*
* @param id ID
* @param newItem id
* @returns
*
* @example
* ```ts
* await store.update('1', { id: '1', name: '新名称', value: 200 })
* ```
*/
const update = async (id: string, newItem: DataItem): Promise<boolean> => {
const key = String(id)
if (!items.value[key]) {
console.warn(`[DataStore] 更新失败ID "${key}" 不存在`)
return false
}
if (!newItem || newItem.id !== id) {
console.warn('[DataStore] 更新失败ID 不匹配')
return false
}
// 全量替换
items.value[key] = JSON.parse(JSON.stringify(newItem))
console.log(`[DataStore] 更新成功:${key}`)
return true
}
/**
*
* @param id ID
* @param partialData id
* @returns
*
* @example
* ```ts
* await store.patch('1', { name: '新名称' }) // 只更新 name 字段
* ```
*/
const patch = async (id: string, partialData: Partial<DataItem>): Promise<boolean> => {
const key = String(id)
if (!items.value[key]) {
console.warn(`[DataStore] 部分更新失败ID "${key}" 不存在`)
return false
}
// 合并更新
items.value[key] = {
...items.value[key],
...partialData,
id: key // 确保 id 不被修改
}
console.log(`[DataStore] 部分更新成功:${key}`)
return true
}
/**
*
* @param updates [{ id, data }]
* @returns
*
* @example
* ```ts
* await store.updateBatch([
* { id: '1', data: { name: '新名称1' } },
* { id: '2', data: { value: 200 } }
* ])
* ```
*/
const updateBatch = async (updates: Array<{ id: string; data: Partial<DataItem> }>): Promise<number> => {
let successCount = 0
for (const { id, data } of updates) {
const result = await patch(id, data)
if (result) successCount++
}
console.log(`[DataStore] 批量更新完成:成功 ${successCount}/${updates.length}`)
return successCount
}
/**
*
* @param id ID
* @returns
*
* @example
* ```ts
* await store.delete('1')
* ```
*/
const deleteItem = async (id: string): Promise<boolean> => {
const key = String(id)
if (!items.value[key]) {
console.warn(`[DataStore] 删除失败ID "${key}" 不存在`)
return false
}
delete items.value[key]
console.log(`[DataStore] 删除成功:${key}`)
return true
}
/**
*
* @param ids ID
* @returns
*
* @example
* ```ts
* await store.deleteBatch(['1', '2', '3'])
* ```
*/
const deleteBatch = async (ids: string[]): Promise<number> => {
let successCount = 0
for (const id of ids) {
const result = await deleteItem(id)
if (result) successCount++
}
console.log(`[DataStore] 批量删除完成:成功 ${successCount}/${ids.length}`)
return successCount
}
/**
*
*/
const clear = async (): Promise<void> => {
items.value = {}
console.log('[DataStore] 已清空所有数据')
}
/**
*
* @param conditions
* @returns
*
* @example
* ```ts
* // 查询 name 为 "测试" 且 value > 50 的数据
* const results = await store.query([
* { field: 'name', value: '测试', operator: 'eq' },
* { field: 'value', value: 50, operator: 'gt' }
* ])
* ```
*/
const query = async (conditions: QueryCondition[]): Promise<DataItem[]> => {
const results = allItems.value.filter(item => {
return conditions.every(condition => {
if (!condition.field) return true
const fieldValue = item[condition.field]
const targetValue = condition.value
const operator = condition.operator || 'eq'
switch (operator) {
case 'eq':
return fieldValue === targetValue
case 'neq':
return fieldValue !== targetValue
case 'gt':
return fieldValue > targetValue
case 'lt':
return fieldValue < targetValue
case 'gte':
return fieldValue >= targetValue
case 'lte':
return fieldValue <= targetValue
case 'includes':
return Array.isArray(fieldValue)
? fieldValue.includes(targetValue)
: String(fieldValue).includes(String(targetValue))
default:
return true
}
})
})
return JSON.parse(JSON.stringify(results))
}
/**
*
* @param field
* @param value
* @returns null
*
* @example
* ```ts
* const item = await store.findByField('name', '测试')
* ```
*/
const findByField = async (field: string, value: any): Promise<DataItem | null> => {
const results = await query([{ field, value, operator: 'eq' }])
return results.length > 0 ? results[0] : null
}
/**
*
* @param id ID
* @returns
*/
const exists = async (id: string): Promise<boolean> => {
const key = String(id)
return Object.prototype.hasOwnProperty.call(items.value, key)
}
/**
*
* @returns
*/
const exportData = async (): Promise<DataItem[]> => {
return JSON.parse(JSON.stringify(allItems.value))
}
/**
*
* @param dataList
* @param options
* - replace: 是否替换现有数据 false
*
* @example
* ```ts
* // 合并导入
* await store.importData(dataArray)
*
* // 替换导入
* await store.importData(dataArray, { replace: true })
* ```
*/
const importData = async (
dataList: DataItem[],
options?: { replace?: boolean }
): Promise<void> => {
if (options?.replace) {
await clear()
}
for (const item of dataList) {
if (item && item.id) {
items.value[String(item.id)] = JSON.parse(JSON.stringify(item))
}
}
console.log(`[DataStore] 导入完成:${dataList.length} 条数据`)
}
// ==================== Return ====================
return {
// State
items,
// Getters
allItems,
count,
allIds,
// CRUD Actions
create,
createBatch,
upsert,
upsertBatch,
read,
readBatch,
readAll,
update,
patch,
updateBatch,
deleteItem,
deleteBatch,
clear,
// Query Actions
query,
findByField,
exists,
// Import/Export
exportData,
importData
}
}, {
persist: true // 启用持久化
})

View File

@ -15,7 +15,7 @@ import {
} from '@/pinia/zxFwPricingHtFee' } from '@/pinia/zxFwPricingHtFee'
import { useZxFwPricingKeysStore } from '@/pinia/zxFwPricingKeys' import { useZxFwPricingKeysStore } from '@/pinia/zxFwPricingKeys'
export type ZxFwPricingField = 'investScale' | 'landScale' | 'workload' | 'hourly' export type ZxFwPricingField = 'investScale' | 'landScale' | 'serviceFee' | 'hourly'
export type ServicePricingMethod = ZxFwPricingField export type ServicePricingMethod = ZxFwPricingField
export interface ZxFwDetailRow { export interface ZxFwDetailRow {
@ -26,7 +26,7 @@ export interface ZxFwDetailRow {
process?: number | null process?: number | null
investScale: number | null investScale: number | null
landScale: number | null landScale: number | null
workload: number | null serviceFee: number | null
hourly: number | null hourly: number | null
subtotal?: number | null subtotal?: number | null
finalFee?: number | null finalFee?: number | null
@ -47,7 +47,7 @@ export interface ServicePricingMethodState<TRow = unknown> {
export interface ServicePricingState { export interface ServicePricingState {
investScale?: ServicePricingMethodState investScale?: ServicePricingMethodState
landScale?: ServicePricingMethodState landScale?: ServicePricingMethodState
workload?: ServicePricingMethodState serviceFee?: ServicePricingMethodState
hourly?: ServicePricingMethodState hourly?: ServicePricingMethodState
} }
@ -63,7 +63,7 @@ const FIXED_ROW_ID = 'fixed-budget-c'
const METHOD_STORAGE_PREFIX_MAP: Record<ServicePricingMethod, string> = { const METHOD_STORAGE_PREFIX_MAP: Record<ServicePricingMethod, string> = {
investScale: 'tzGMF', investScale: 'tzGMF',
landScale: 'ydGMF', landScale: 'ydGMF',
workload: 'gzlF', serviceFee: 'gzlF',
hourly: 'hourlyPricing' hourly: 'hourlyPricing'
} }
const STORAGE_PREFIX_METHOD_MAP = new Map<string, ServicePricingMethod>( const STORAGE_PREFIX_METHOD_MAP = new Map<string, ServicePricingMethod>(
@ -131,7 +131,7 @@ const normalizeRows = (rows: unknown): ZxFwDetailRow[] =>
process: normalizeProcessValue(row.process, rowId), process: normalizeProcessValue(row.process, rowId),
investScale: toFiniteNumberOrNull(row.investScale), investScale: toFiniteNumberOrNull(row.investScale),
landScale: toFiniteNumberOrNull(row.landScale), landScale: toFiniteNumberOrNull(row.landScale),
workload: toFiniteNumberOrNull(row.workload), serviceFee: toFiniteNumberOrNull(row.serviceFee),
hourly: toFiniteNumberOrNull(row.hourly), hourly: toFiniteNumberOrNull(row.hourly),
subtotal: toFiniteNumberOrNull(row.subtotal), subtotal: toFiniteNumberOrNull(row.subtotal),
finalFee: toFiniteNumberOrNull(row.finalFee), finalFee: toFiniteNumberOrNull(row.finalFee),
@ -824,7 +824,7 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
const rowSubtotal = sumNullableNumbers([ const rowSubtotal = sumNullableNumbers([
toFiniteNumberOrNull(row.investScale), toFiniteNumberOrNull(row.investScale),
toFiniteNumberOrNull(row.landScale), toFiniteNumberOrNull(row.landScale),
toFiniteNumberOrNull(row.workload), toFiniteNumberOrNull(row.serviceFee),
toFiniteNumberOrNull(row.hourly) toFiniteNumberOrNull(row.hourly)
]) ])
return { return {
@ -945,8 +945,7 @@ export const useZxFwPricingStore = defineStore('zxFwPricing', () => {
getHtFeeMethodState, getHtFeeMethodState,
setHtFeeMethodState, setHtFeeMethodState,
loadHtFeeMethodState, loadHtFeeMethodState,
removeHtFeeMethodState, removeHtFeeMethodState
syncHtExtraFeeByContractBase
} }
}, { }, {
persist: true persist: true

View File

@ -1,19 +0,0 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import DisclaimerPage from '@/features/disclaimer/components/DisclaimerPage.vue'
import WorkspaceShell from '@/features/app/components/WorkspaceShell.vue'
export const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: '/',
name: 'workspace',
component: WorkspaceShell
},
{
path: '/disclaimer',
name: 'disclaimer',
component: DisclaimerPage
}
]
})

1040
src/sql.ts

File diff suppressed because it is too large Load Diff

View File

@ -57,17 +57,12 @@ html {
--app-grid-row-h: 2.25rem; --app-grid-row-h: 2.25rem;
--app-grid-font-size: 0.875rem; --app-grid-font-size: 0.875rem;
--app-typeline-side-w: 12.5rem; --app-typeline-side-w: 12.5rem;
--app-typeline-gap: 0.625rem; --app-typeline-gap: 0.75rem;
--app-typeline-label-font: 0.8125rem; --app-typeline-label-font: 0.8125rem;
--app-typeline-label-line: 1rem; --app-typeline-label-line: 1rem;
--app-typeline-dot: 1.125rem; --app-typeline-dot: 1.25rem;
--app-typeline-dot-inner: 0.3125rem; --app-typeline-dot-inner: 0.375rem;
--app-typeline-line-left: 0.5625rem; --app-typeline-line-left: 0.5625rem;
--app-typeline-arrow-offset: 0.125rem;
--app-typeline-arrow-shaft-h: 0.9375rem;
--app-typeline-arrow-line-w: 0.09375rem;
--app-typeline-arrow-head-w: 0.5rem;
--app-typeline-arrow-head-h: 0.375rem;
--radius: 0.625rem; --radius: 0.625rem;
--background: oklch(1 0 0); --background: oklch(1 0 0);
--foreground: oklch(0.145 0 0); --foreground: oklch(0.145 0 0);
@ -186,23 +181,6 @@ input[inputmode='numeric'] {
text-align: right; text-align: right;
} }
.ag-theme-quartz .ag-cell.ag-summary-label-cell {
justify-content: center;
text-align: center;
}
.ag-theme-quartz .ag-cell.ag-summary-label-cell .ag-cell-wrapper {
width: 100%;
justify-content: center;
}
.ag-theme-quartz .ag-cell.ag-summary-label-cell .ag-cell-value {
display: flex;
width: 100%;
justify-content: center;
text-align: center;
}
/* Global AG Grid header alignment: center all header text. */ /* Global AG Grid header alignment: center all header text. */
.ag-theme-quartz .ag-header-cell-label, .ag-theme-quartz .ag-header-cell-label,
.ag-theme-quartz .ag-header-group-cell-label { .ag-theme-quartz .ag-header-group-cell-label {
@ -440,17 +418,12 @@ input[inputmode='numeric'] {
--app-grid-row-h: 2.125rem; --app-grid-row-h: 2.125rem;
--app-grid-font-size: 0.8125rem; --app-grid-font-size: 0.8125rem;
--app-typeline-side-w: 11.5rem; --app-typeline-side-w: 11.5rem;
--app-typeline-gap: 0.5625rem; --app-typeline-gap: 0.625rem;
--app-typeline-label-font: 0.75rem; --app-typeline-label-font: 0.75rem;
--app-typeline-label-line: 0.95rem; --app-typeline-label-line: 0.95rem;
--app-typeline-dot: 1rem; --app-typeline-dot: 1.125rem;
--app-typeline-dot-inner: 0.25rem; --app-typeline-dot-inner: 0.3125rem;
--app-typeline-line-left: 0.5rem; --app-typeline-line-left: 0.5rem;
--app-typeline-arrow-offset: 0.125rem;
--app-typeline-arrow-shaft-h: 0.8125rem;
--app-typeline-arrow-line-w: 0.09375rem;
--app-typeline-arrow-head-w: 0.4375rem;
--app-typeline-arrow-head-h: 0.3125rem;
} }
} }
@ -466,17 +439,12 @@ input[inputmode='numeric'] {
--app-grid-row-h: 2.25rem; --app-grid-row-h: 2.25rem;
--app-grid-font-size: 0.875rem; --app-grid-font-size: 0.875rem;
--app-typeline-side-w: 12rem; --app-typeline-side-w: 12rem;
--app-typeline-gap: 0.59375rem; --app-typeline-gap: 0.6875rem;
--app-typeline-label-font: 0.8125rem; --app-typeline-label-font: 0.8125rem;
--app-typeline-label-line: 1rem; --app-typeline-label-line: 1rem;
--app-typeline-dot: 1.0625rem; --app-typeline-dot: 1.1875rem;
--app-typeline-dot-inner: 0.3125rem; --app-typeline-dot-inner: 0.375rem;
--app-typeline-line-left: 0.5625rem; --app-typeline-line-left: 0.5625rem;
--app-typeline-arrow-offset: 0.125rem;
--app-typeline-arrow-shaft-h: 0.875rem;
--app-typeline-arrow-line-w: 0.09375rem;
--app-typeline-arrow-head-w: 0.46875rem;
--app-typeline-arrow-head-h: 0.34375rem;
} }
} }
@ -492,17 +460,12 @@ input[inputmode='numeric'] {
--app-grid-row-h: 2.25rem; --app-grid-row-h: 2.25rem;
--app-grid-font-size: 0.875rem; --app-grid-font-size: 0.875rem;
--app-typeline-side-w: 12.5rem; --app-typeline-side-w: 12.5rem;
--app-typeline-gap: 0.625rem; --app-typeline-gap: 0.75rem;
--app-typeline-label-font: 0.875rem; --app-typeline-label-font: 0.875rem;
--app-typeline-label-line: 1.1rem; --app-typeline-label-line: 1.1rem;
--app-typeline-dot: 1.125rem; --app-typeline-dot: 1.25rem;
--app-typeline-dot-inner: 0.375rem; --app-typeline-dot-inner: 0.4375rem;
--app-typeline-line-left: 0.625rem; --app-typeline-line-left: 0.625rem;
--app-typeline-arrow-offset: 0.125rem;
--app-typeline-arrow-shaft-h: 0.9375rem;
--app-typeline-arrow-line-w: 0.09375rem;
--app-typeline-arrow-head-w: 0.5rem;
--app-typeline-arrow-head-h: 0.375rem;
} }
} }
@ -518,17 +481,12 @@ input[inputmode='numeric'] {
--app-grid-row-h: 2.5rem; --app-grid-row-h: 2.5rem;
--app-grid-font-size: 0.9375rem; --app-grid-font-size: 0.9375rem;
--app-typeline-side-w: 13rem; --app-typeline-side-w: 13rem;
--app-typeline-gap: 0.6875rem; --app-typeline-gap: 0.8125rem;
--app-typeline-label-font: 0.9375rem; --app-typeline-label-font: 0.9375rem;
--app-typeline-label-line: 1.2rem; --app-typeline-label-line: 1.2rem;
--app-typeline-dot: 1.25rem; --app-typeline-dot: 1.375rem;
--app-typeline-dot-inner: 0.4375rem; --app-typeline-dot-inner: 0.5rem;
--app-typeline-line-left: 0.6875rem; --app-typeline-line-left: 0.6875rem;
--app-typeline-arrow-offset: 0.15625rem;
--app-typeline-arrow-shaft-h: 1rem;
--app-typeline-arrow-line-w: 0.125rem;
--app-typeline-arrow-head-w: 0.5625rem;
--app-typeline-arrow-head-h: 0.40625rem;
} }
} }
@ -544,17 +502,12 @@ input[inputmode='numeric'] {
--app-grid-row-h: 2.625rem; --app-grid-row-h: 2.625rem;
--app-grid-font-size: 1rem; --app-grid-font-size: 1rem;
--app-typeline-side-w: 13.5rem; --app-typeline-side-w: 13.5rem;
--app-typeline-gap: 0.75rem; --app-typeline-gap: 0.875rem;
--app-typeline-label-font: 1rem; --app-typeline-label-font: 1rem;
--app-typeline-label-line: 1.25rem; --app-typeline-label-line: 1.25rem;
--app-typeline-dot: 1.375rem; --app-typeline-dot: 1.5rem;
--app-typeline-dot-inner: 0.5rem; --app-typeline-dot-inner: 0.5625rem;
--app-typeline-line-left: 0.75rem; --app-typeline-line-left: 0.75rem;
--app-typeline-arrow-offset: 0.1875rem;
--app-typeline-arrow-shaft-h: 1.125rem;
--app-typeline-arrow-line-w: 0.125rem;
--app-typeline-arrow-head-w: 0.625rem;
--app-typeline-arrow-head-h: 0.4375rem;
} }
} }
@ -570,16 +523,11 @@ input[inputmode='numeric'] {
--app-grid-row-h: 2.875rem; --app-grid-row-h: 2.875rem;
--app-grid-font-size: 1.0625rem; --app-grid-font-size: 1.0625rem;
--app-typeline-side-w: 14.5rem; --app-typeline-side-w: 14.5rem;
--app-typeline-gap: 0.875rem; --app-typeline-gap: 1rem;
--app-typeline-label-font: 1.0625rem; --app-typeline-label-font: 1.0625rem;
--app-typeline-label-line: 1.35rem; --app-typeline-label-line: 1.35rem;
--app-typeline-dot: 1.5rem; --app-typeline-dot: 1.625rem;
--app-typeline-dot-inner: 0.5625rem; --app-typeline-dot-inner: 0.625rem;
--app-typeline-line-left: 0.8125rem; --app-typeline-line-left: 0.8125rem;
--app-typeline-arrow-offset: 0.21875rem;
--app-typeline-arrow-shaft-h: 1.25rem;
--app-typeline-arrow-line-w: 0.125rem;
--app-typeline-arrow-head-w: 0.6875rem;
--app-typeline-arrow-head-h: 0.5rem;
} }
} }

View File

@ -213,15 +213,8 @@ export interface MajorLite {
/** 咨询服务字典精简信息 */ /** 咨询服务字典精简信息 */
export interface ServiceLite { export interface ServiceLite {
defCoe: number | null defCoe: number | null
enableInvestScale?: boolean | null
enableLandScale?: boolean | null
investScaleSingleTotal?: boolean | null
scale?: boolean | null
/** legacy fallback; new code should use investScaleSingleTotal */
onlyCostScale?: boolean | null onlyCostScale?: boolean | null
mutiple?: boolean | null mutiple?: boolean | null
amount?: boolean | null
workDay?: boolean | null
} }
/** 工作量法任务字典 */ /** 工作量法任务字典 */

View File

@ -1 +1 @@
{"root":["./src/main.ts","./src/router.ts","./src/sql.ts","./src/components/ui/button/index.ts","./src/components/ui/card/index.ts","./src/components/ui/scroll-area/index.ts","./src/components/ui/tooltip/index.ts","./src/features/ht/contracts.ts","./src/features/ht/importexport.ts","./src/features/ht/types.ts","./src/features/tab/importexport.ts","./src/features/tab/types.ts","./src/i18n/dictionary-en.ts","./src/i18n/index.ts","./src/i18n/locales/en-us.ts","./src/i18n/locales/zh-cn.ts","./src/lib/aggridreadonlyautoheight.ts","./src/lib/aggridresetheader.ts","./src/lib/contractsegment.ts","./src/lib/decimal.ts","./src/lib/diyaggridoptions.ts","./src/lib/number.ts","./src/lib/numberformat.ts","./src/lib/pricinghourlycalc.ts","./src/lib/pricingmethodtotals.ts","./src/lib/pricingpersistcontrol.ts","./src/lib/pricingpinnedrows.ts","./src/lib/pricingscalecalc.ts","./src/lib/pricingscalecolumns.ts","./src/lib/pricingscaledetail.ts","./src/lib/pricingscaledict.ts","./src/lib/pricingscalefee.ts","./src/lib/pricingscalegrid.ts","./src/lib/pricingscalelink.ts","./src/lib/pricingscalepanedata.ts","./src/lib/pricingscalepanelifecycle.ts","./src/lib/pricingscaleproject.ts","./src/lib/pricingscalerowmap.ts","./src/lib/pricingworkloadcalc.ts","./src/lib/projectevents.ts","./src/lib/projectkvstore.ts","./src/lib/projectregistry.ts","./src/lib/projectsessionlock.ts","./src/lib/projectworkspace.ts","./src/lib/reportexportbuilders.ts","./src/lib/servicepricing.ts","./src/lib/utils.ts","./src/lib/workspace.ts","./src/lib/xmfactordefaults.ts","./src/lib/zwarchive.ts","./src/lib/zxfwpricingsync.ts","./src/pinia/kv.ts","./src/pinia/tab.ts","./src/pinia/uiprefs.ts","./src/pinia/zxfwpricing.ts","./src/pinia/zxfwpricinghtfee.ts","./src/pinia/zxfwpricingkeys.ts","./src/pinia/plugin/indexdb.ts","./src/pinia/plugin/types.d.ts","./src/types/pricing.ts","./src/app.vue","./src/components/ui/button/button.vue","./src/components/ui/card/card.vue","./src/components/ui/card/cardaction.vue","./src/components/ui/card/cardcontent.vue","./src/components/ui/card/carddescription.vue","./src/components/ui/card/cardfooter.vue","./src/components/ui/card/cardheader.vue","./src/components/ui/card/cardtitle.vue","./src/components/ui/scroll-area/scrollarea.vue","./src/components/ui/scroll-area/scrollbar.vue","./src/components/ui/tooltip/tooltipcontent.vue","./src/features/app/components/workspaceshell.vue","./src/features/disclaimer/components/disclaimerpage.vue","./src/features/ht/components/ht.vue","./src/features/ht/components/htadditionalworkfee.vue","./src/features/ht/components/htbaseinfo.vue","./src/features/ht/components/htconsultcategoryfactor.vue","./src/features/ht/components/htcontractsummary.vue","./src/features/ht/components/htfeeratemethodform.vue","./src/features/ht/components/htmajorfactor.vue","./src/features/ht/components/htreservefee.vue","./src/features/ht/components/htcard.vue","./src/features/ht/components/htinfo.vue","./src/features/ht/components/zxfw.vue","./src/features/pricing/components/hourlypricingpane.vue","./src/features/pricing/components/investmentscalepricingpane.vue","./src/features/pricing/components/landscalepricingpane.vue","./src/features/pricing/components/scaleformulareadonlypane.vue","./src/features/pricing/components/workloadpricingpane.vue","./src/features/shared/components/hourlyfeegrid.vue","./src/features/shared/components/htfeegrid.vue","./src/features/shared/components/htfeemethodgrid.vue","./src/features/shared/components/methodunavailablenotice.vue","./src/features/shared/components/servicecheckboxselector.vue","./src/features/shared/components/workcontentgrid.vue","./src/features/shared/components/xmfactorgrid.vue","./src/features/shared/components/xmcommonaggrid.vue","./src/features/workbench/components/homeentryview.vue","./src/features/workbench/components/htfeemethodtypelineview.vue","./src/features/workbench/components/quickcalcworkbenchview.vue","./src/features/workbench/components/zxfwview.vue","./src/features/xm/components/xmconsultcategoryfactor.vue","./src/features/xm/components/xmmajorfactor.vue","./src/features/xm/components/info.vue","./src/features/xm/components/xmcard.vue","./src/features/xm/components/xminfo.vue","./src/layout/tab.vue","./src/layout/typeline.vue"],"version":"5.9.3"} {"root":["./src/main.ts","./src/sql.ts","./src/components/ui/button/index.ts","./src/components/ui/card/index.ts","./src/components/ui/scroll-area/index.ts","./src/components/ui/tooltip/index.ts","./src/features/ht/contracts.ts","./src/features/ht/importexport.ts","./src/features/ht/types.ts","./src/features/tab/importexport.ts","./src/features/tab/types.ts","./src/i18n/dictionary-en.ts","./src/i18n/index.ts","./src/i18n/locales/en-us.ts","./src/i18n/locales/zh-cn.ts","./src/lib/aggridreadonlyautoheight.ts","./src/lib/aggridresetheader.ts","./src/lib/contractsegment.ts","./src/lib/decimal.ts","./src/lib/diyaggridoptions.ts","./src/lib/number.ts","./src/lib/numberformat.ts","./src/lib/pricinghourlycalc.ts","./src/lib/pricingmethodtotals.ts","./src/lib/pricingpersistcontrol.ts","./src/lib/pricingpinnedrows.ts","./src/lib/pricingscalecalc.ts","./src/lib/pricingscalecolumns.ts","./src/lib/pricingscaledetail.ts","./src/lib/pricingscaledict.ts","./src/lib/pricingscalefee.ts","./src/lib/pricingscalegrid.ts","./src/lib/pricingscalelink.ts","./src/lib/pricingscalepanedata.ts","./src/lib/pricingscalepanelifecycle.ts","./src/lib/pricingscaleproject.ts","./src/lib/pricingscalerowmap.ts","./src/lib/pricingworkloadcalc.ts","./src/lib/projectevents.ts","./src/lib/projectkvstore.ts","./src/lib/projectregistry.ts","./src/lib/projectsessionlock.ts","./src/lib/projectworkspace.ts","./src/lib/reportexportbuilders.ts","./src/lib/utils.ts","./src/lib/workspace.ts","./src/lib/xmfactordefaults.ts","./src/lib/zwarchive.ts","./src/lib/zxfwpricingsync.ts","./src/pinia/kv.ts","./src/pinia/tab.ts","./src/pinia/uiprefs.ts","./src/pinia/zx.ts","./src/pinia/zxfwpricing.ts","./src/pinia/zxfwpricinghtfee.ts","./src/pinia/zxfwpricingkeys.ts","./src/pinia/plugin/indexdb.ts","./src/pinia/plugin/types.d.ts","./src/types/pricing.ts","./src/app.vue","./src/components/ui/button/button.vue","./src/components/ui/card/card.vue","./src/components/ui/card/cardaction.vue","./src/components/ui/card/cardcontent.vue","./src/components/ui/card/carddescription.vue","./src/components/ui/card/cardfooter.vue","./src/components/ui/card/cardheader.vue","./src/components/ui/card/cardtitle.vue","./src/components/ui/scroll-area/scrollarea.vue","./src/components/ui/scroll-area/scrollbar.vue","./src/components/ui/tooltip/tooltipcontent.vue","./src/features/ht/components/ht.vue","./src/features/ht/components/htadditionalworkfee.vue","./src/features/ht/components/htbaseinfo.vue","./src/features/ht/components/htconsultcategoryfactor.vue","./src/features/ht/components/htcontractsummary.vue","./src/features/ht/components/htfeeratemethodform.vue","./src/features/ht/components/htmajorfactor.vue","./src/features/ht/components/htreservefee.vue","./src/features/ht/components/htcard.vue","./src/features/ht/components/htinfo.vue","./src/features/ht/components/zxfw.vue","./src/features/pricing/components/hourlypricingpane.vue","./src/features/pricing/components/investmentscalepricingpane.vue","./src/features/pricing/components/landscalepricingpane.vue","./src/features/pricing/components/otherservice.vue","./src/features/pricing/components/scaleformulareadonlypane.vue","./src/features/pricing/components/workloadpricingpane.vue","./src/features/shared/components/hourlyfeegrid.vue","./src/features/shared/components/htfeegrid.vue","./src/features/shared/components/htfeemethodgrid.vue","./src/features/shared/components/methodunavailablenotice.vue","./src/features/shared/components/servicecheckboxselector.vue","./src/features/shared/components/workcontentgrid.vue","./src/features/shared/components/xmfactorgrid.vue","./src/features/shared/components/xmcommonaggrid.vue","./src/features/workbench/components/homeentryview.vue","./src/features/workbench/components/htfeemethodtypelineview.vue","./src/features/workbench/components/quickcalcworkbenchview.vue","./src/features/workbench/components/zxfwview.vue","./src/features/xm/components/xmconsultcategoryfactor.vue","./src/features/xm/components/xmmajorfactor.vue","./src/features/xm/components/info.vue","./src/features/xm/components/xmcard.vue","./src/features/xm/components/xminfo.vue","./src/layout/tab.vue","./src/layout/typeline.vue"],"errors":true,"version":"5.9.3"}