Compare commits

..

No commits in common. "929" and "粤公学标字〔2026〕5号" have entirely different histories.

66 changed files with 5146 additions and 9666 deletions

554
bun.lock
View File

@ -18,20 +18,18 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"decimal.js": "^10.6.0",
"docxtemplater": "^3.68.5",
"exceljs": "^4.4.0",
"file-saver": "^2.0.5",
"localforage": "^1.10.0",
"lucide-vue-next": "^0.563.0",
"motion-v": "^2.0.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"pizzip": "^3.2.0",
"reka-ui": "^2.8.0",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"vue": "^3.5.25",
"vue-i18n": "^11.3.0",
"vue-router": "^4.6.4",
"vuedraggable": "^4.1.0",
},
"devDependencies": {
@ -47,554 +45,548 @@
},
},
"packages": {
"@ag-grid-community/locale": ["@ag-grid-community/locale@35.1.0", "", {}, "sha512-Tez1imtqfipMT3O1Ay+dyDcFJIj6H6gXBp45s44pwkzWQzxO20IBpZUrmAPTNRMYVZNXqCVbNsozWrPaVFAgeQ=="],
"@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=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@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-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
"@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/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="],
"@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/types": ["@babel/types@7.29.0", "", { "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", "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=="],
"@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/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/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@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/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@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=="],
"@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/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/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=="],
"@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=="],
"@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="],
"@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/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/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/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
"@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/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=="],
"@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=="],
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
"@iconify/types": ["@iconify/types@2.0.0", "https://registry.npmmirror.com/@iconify/types/-/types-2.0.0.tgz", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
"@iconify/vue": ["@iconify/vue@5.0.0", "", { "dependencies": { "@iconify/types": "^2.0.0" }, "peerDependencies": { "vue": ">=3" } }, "sha512-C+KuEWIF5nSBrobFJhT//JS87OZ++QDORB6f2q2Wm6fl2mueSTpFBeBsveK0KW9hWiZ4mNiPjsh6Zs4jjdROSg=="],
"@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=="],
"@internationalized/date": ["@internationalized/date@3.12.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-/PyIMzK29jtXaGU23qTvNZxvBXRtKbNnGDFD+PY6CZw/Y8Ex8pFUzkuCJCG9aOqmShjqhS9mPqP6Dk5onQY8rQ=="],
"@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/number": ["@internationalized/number@3.6.5", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g=="],
"@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=="],
"@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/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/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/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/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/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/shared": ["@intlify/shared@11.4.6", "", {}, "sha512-m1p1HHAMLhqSpTRH7VnXdrN0CQ4y+9vunFkpLkbD8soIuBsnQdawZXqMCgvwI2UVF9Ww7sVaw7g9tV2VO7shoA=="],
"@intlify/shared": ["@intlify/shared@11.3.1", "https://registry.npmmirror.com/@intlify/shared/-/shared-11.3.1.tgz", {}, "sha512-9GWc5PKuRdeWkT7FJN43c/+rD6xpSB3WtizewkfFCK/0XzYqCk4gQBWWcTdfKo8ylEcHwqYsR2Z3HRE3XhEHrQ=="],
"@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/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/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@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/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@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/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@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/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "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", "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=="],
"@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=="],
"@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=="],
"@noble/ciphers": ["@noble/ciphers@2.2.0", "", {}, "sha512-Z6pjIZ/8IJcCGzb2S/0Px5J81yij85xASuk1teLNeg75bfT07MV3a/O2Mtn1I2se43k3lkVEcFaR10N4cgQcZA=="],
"@noble/ciphers": ["@noble/ciphers@2.1.1", "https://registry.npmmirror.com/@noble/ciphers/-/ciphers-2.1.1.tgz", {}, "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw=="],
"@noble/hashes": ["@noble/hashes@2.2.0", "", {}, "sha512-IYqDGiTXab6FniAgnSdZwgWbomxpy9FtYvLKs7wCUs2a8RkITG+DFGO1DM9cr+E3/RgADRpFjrKVaJ1z6sjtEg=="],
"@noble/hashes": ["@noble/hashes@2.0.1", "https://registry.npmmirror.com/@noble/hashes/-/hashes-2.0.1.tgz", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="],
"@oxc-project/runtime": ["@oxc-project/runtime@0.114.0", "", {}, "sha512-mVGQvr/uFJGQ3hsvgQ1sJfh79t5owyZZZtw+VaH+WhtvsmtgjT6imznB9sz2Q67Q0/4obM9mOOtQscU4aJteSg=="],
"@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/types": ["@oxc-project/types@0.114.0", "", {}, "sha512-//nBfbzHQHvJs8oFIjv6coZ6uxQ4alLfiPe6D5vit6c4pmxATHHlVwgB1k+Hv4yoAMyncdxgRBF5K4BYWUCzvA=="],
"@oxc-project/types": ["@oxc-project/types@0.114.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.114.0.tgz", {}, "sha512-//nBfbzHQHvJs8oFIjv6coZ6uxQ4alLfiPe6D5vit6c4pmxATHHlVwgB1k+Hv4yoAMyncdxgRBF5K4BYWUCzvA=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.5", "", { "os": "android", "cpu": "arm64" }, "sha512-zCEmUrt1bggwgBgeKLxNj217J1OrChrp3jJt24VK9jAharSTeVaHODNL+LpcQVhRz+FktYWfT9cjo5oZ99ZLpg=="],
"@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-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ZP9xb9lPAex36pvkNWCjSEJW/Gfdm9I3ssiqOFLmpZ/vosPXgpoGxCmh+dX1Qs+/bWQE6toNFXWWL8vYoKoK9Q=="],
"@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-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-7IdrPunf6dp9mywMgTOKMMGDnMHQ6+h5gRl6LW8rhD8WK2kXX0IwzcM5Zc0B5J7xQs8QWOlKjv8BJsU/1CD3pg=="],
"@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-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-o/JCk+dL0IN68EBhZ4DqfsfvxPfMeoM6cJtxORC1YYoxGHZyth2Kb2maXDb4oddw2wu8iIbnYXYPEzBtAF5CAg=="],
"@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-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-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-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-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-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-WFljyDkxtXRlWxMjxeegf7xMYXxUr8u7JdXlOEWKYgDqEgxUnSEsVDxBiNWQ1D5kQKwf8Wo4sVKEYPRhCdsjwA=="],
"@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-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.5", "", { "os": "linux", "cpu": "x64" }, "sha512-CUlplTujmbDWp2gamvrqVKi2Or8lmngXT1WxsizJfts7JrvfGhZObciaY/+CbdbS9qNnskvwMZNEhTPrn7b+WA=="],
"@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-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.5", "", { "os": "linux", "cpu": "x64" }, "sha512-wdf7g9NbVZCeAo2iGhsjJb7I8ZFfs6X8bumfrWg82VK+8P6AlLXwk48a1ASiJQDTS7Svq2xVzZg3sGO2aXpHRA=="],
"@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-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.5", "", { "os": "none", "cpu": "arm64" }, "sha512-0CWY7ubu12nhzz+tkpHjoG3IRSTlWYe0wrfJRf4qqjqQSGtAYgoL9kwzdvlhaFdZ5ffVeyYw9qLsChcjUMEloQ=="],
"@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-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-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-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-jUct1XVeGtyjqJXEAfvdFa8xoigYZ2rge7nYEm70ppQxpfH9ze2fbIrpHmP2tNM2vL/F6Dd0CpXhpjPbC6bSxQ=="],
"@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-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.5", "", { "os": "win32", "cpu": "x64" }, "sha512-VQ8F9ld5gw29epjnVGdrx8ugiLTe8BMqmhDYy7nGbdeDo4HAt4bgdZvLbViEhg7DZyHLpiEUlO5/jPSUrIuxRQ=="],
"@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/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.2", "", {}, "sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw=="],
"@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=="],
"@swc/helpers": ["@swc/helpers@0.5.18", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ=="],
"@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=="],
"@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/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/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": ["@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-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.0", "", { "os": "android", "cpu": "arm64" }, "sha512-F0QkHAVaW/JNBWl4CEKWdZ9PMb0khw5DCELAOnu+RtjAfx5Zgw+gqCHFvqg3AirU1IAd181fwOtJQ5I8Yx5wtw=="],
"@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-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-I0QylkXsBsJMZ4nkUNSR04p6+UptjcwhcVo3Zu828ikiEqHjVmQL9RuQ6uT/cVIiKpvtVA25msu/eRV97JeNSA=="],
"@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-x64": ["@tailwindcss/oxide-darwin-x64@4.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-6TmQIn4p09PBrmnkvbYQ0wbZhLtbaksCDx7Y7R3FYYx0yxNA7xg5KP7dowmQ3d2JVdabIHvs3Hx4K3d5uCf8xg=="],
"@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-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-qBudxDvAa2QwGlq9y7VIzhTvp2mLJ6nD/G8/tI70DCDoneaUeLWBJaPcbfzqRIWraj+o969aDQKvKW9dvkUizw=="],
"@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-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-7XKkitpy5NIjFZNUQPeUyNJNJn1CJeV7rmMR+exHfTuOsg8rxIO9eNV5TSEnqRcaOK77zQpsyUkBWmPy8FgdSg=="],
"@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-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Mff5a5Q3WoQR01pGU1gr29hHM1N93xYrKkGXfPw/aRtK4bOc331Ho4Tgfsm5WDGvpevqMpdlkCojT3qlCQbCpA=="],
"@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-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-XKcSStleEVnbH6W/9DHzZv1YhjE4eSS6zOu2eRtYAIh7aV4o3vIBs+t/B15xlqoxt6ef/0uiqJVB6hkHjWD/0A=="],
"@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-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-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-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-lKUaygq4G7sWkhQbfdRRBkaq4LY39IriqBQ+Gk6l5nKq6Ay2M2ZZb1tlIyRNgZKS8cbErTwuYSor0IIULC0SHw=="],
"@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-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-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-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-2UU/15y1sWDEDNJXxEIrfWKC2Yb4YgIW5Xz2fKFqGzFWfoMHWFlfa1EJlGO2Xzjkq/tvSarh9ZTjvbxqWvLLXA=="],
"@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-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.0", "", { "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", "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/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=="],
"@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=="],
"@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.18", "", {}, "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg=="],
"@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/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=="],
"@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=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@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=="],
"@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
"@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/node": ["@types/node@24.10.13", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg=="],
"@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/web-bluetooth": ["@types/web-bluetooth@0.0.21", "", {}, "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA=="],
"@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=="],
"@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=="],
"@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=="],
"@volar/language-core": ["@volar/language-core@2.4.28", "", { "dependencies": { "@volar/source-map": "2.4.28" } }, "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ=="],
"@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/source-map": ["@volar/source-map@2.4.28", "", {}, "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ=="],
"@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/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=="],
"@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=="],
"@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-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-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-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-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-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-ssr": ["@vue/compiler-ssr@3.5.28", "", { "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", "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/devtools-api": ["@vue/devtools-api@7.7.9", "", { "dependencies": { "@vue/devtools-kit": "^7.7.9" } }, "sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g=="],
"@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-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-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-shared": ["@vue/devtools-shared@7.7.9", "", { "dependencies": { "rfdc": "^1.4.1" } }, "sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA=="],
"@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/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/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/reactivity": ["@vue/reactivity@3.5.28", "", { "dependencies": { "@vue/shared": "3.5.28" } }, "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw=="],
"@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/runtime-core": ["@vue/runtime-core@3.5.28", "", { "dependencies": { "@vue/reactivity": "3.5.28", "@vue/shared": "3.5.28" } }, "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ=="],
"@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-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/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/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/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/shared": ["@vue/shared@3.5.28", "", {}, "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ=="],
"@vue/shared": ["@vue/shared@3.5.28", "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.28.tgz", {}, "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ=="],
"@vue/tsconfig": ["@vue/tsconfig@0.8.1", "", { "peerDependencies": { "typescript": "5.x", "vue": "^3.4.0" }, "optionalPeers": ["typescript", "vue"] }, "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g=="],
"@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=="],
"@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/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/metadata": ["@vueuse/metadata@14.2.1", "", {}, "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw=="],
"@vueuse/metadata": ["@vueuse/metadata@14.2.1", "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-14.2.1.tgz", {}, "sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw=="],
"@vueuse/shared": ["@vueuse/shared@14.2.1", "", { "peerDependencies": { "vue": "^3.5.0" } }, "sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw=="],
"@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=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="],
"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=="],
"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-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-core": ["ag-charts-core@13.1.0", "", { "dependencies": { "ag-charts-types": "13.1.0" } }, "sha512-mLHJZ8oU5CPeLRURescdISCtMsiiA/m4d1iBr6aQBEgiTVogRMGpFpsYNtQiYtoW2sRh+62I9sN8fhC3JQjX/g=="],
"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-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-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-locale": ["ag-charts-locale@13.1.0", "", {}, "sha512-mPgJnVsOI4Cf17CAlRh8BvLz19e165sdQJeUXNaB7M+DPB+pxODOcfx4oqZlR4Wc8Zu++TGb/2ueHa/aeV2qeQ=="],
"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-types": ["ag-charts-types@13.1.0", "", {}, "sha512-DytRM3CXli+Y013SC1Mr8lQBrhVTACK+11ilDHOhwUM0sRpmGuR51XFGcBKOliW1Vas1AycP31Cm3Pp0jx3hqw=="],
"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-grid-community": ["ag-grid-community@35.1.0", "", { "dependencies": { "ag-charts-types": "13.1.0" } }, "sha512-yWFQfRNjv3KUBkHHzFdDOYGjPcDMU0B8Up4qG651diFlGRUGEGVs94SK73niWvk1FDZdpV9oWrwq3f30/qAoVg=="],
"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-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=="],
"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-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=="],
"alien-signals": ["alien-signals@3.1.2", "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.1.2.tgz", {}, "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw=="],
"alien-signals": ["alien-signals@3.1.2", "", {}, "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw=="],
"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=="],
"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=="],
"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-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=="],
"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=="],
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"async": ["async@3.2.6", "https://registry.npmmirror.com/async/-/async-3.2.6.tgz", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"base64-js": ["base64-js@1.5.1", "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"big-integer": ["big-integer@1.6.52", "https://registry.npmmirror.com/big-integer/-/big-integer-1.6.52.tgz", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="],
"big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="],
"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=="],
"binary": ["binary@0.3.0", "", { "dependencies": { "buffers": "~0.1.1", "chainsaw": "~0.1.0" } }, "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg=="],
"birpc": ["birpc@2.9.0", "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
"birpc": ["birpc@2.9.0", "", {}, "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw=="],
"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=="],
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"bluebird": ["bluebird@3.4.7", "https://registry.npmmirror.com/bluebird/-/bluebird-3.4.7.tgz", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="],
"bluebird": ["bluebird@3.4.7", "", {}, "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="],
"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=="],
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
"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=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "https://registry.npmmirror.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"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-indexof-polyfill": ["buffer-indexof-polyfill@1.0.2", "", {}, "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A=="],
"buffers": ["buffers@0.1.1", "https://registry.npmmirror.com/buffers/-/buffers-0.1.1.tgz", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="],
"buffers": ["buffers@0.1.1", "", {}, "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="],
"bun-types": ["bun-types@1.3.9", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.9.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
"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=="],
"chainsaw": ["chainsaw@0.1.0", "", { "dependencies": { "traverse": ">=0.3.0 <0.4" } }, "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ=="],
"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=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"clsx": ["clsx@2.1.1", "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"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=="],
"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=="],
"concat-map": ["concat-map@0.0.1", "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"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=="],
"copy-anything": ["copy-anything@4.0.5", "", { "dependencies": { "is-what": "^5.2.0" } }, "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA=="],
"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=="],
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
"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=="],
"crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
"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=="],
"crc32-stream": ["crc32-stream@4.0.3", "", { "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" } }, "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw=="],
"csstype": ["csstype@3.2.3", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"dayjs": ["dayjs@1.11.19", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
"dayjs": ["dayjs@1.11.19", "", {}, "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw=="],
"decimal.js": ["decimal.js@10.6.0", "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
"defu": ["defu@6.1.4", "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
"detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"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=="],
"docxtemplater": ["docxtemplater@3.69.0", "", { "dependencies": { "@xmldom/xmldom": "^0.9.10" } }, "sha512-l1zDGXj4CHdBCkGPvmVOsEzc4DDpMxLXgnNd1zllEck9gxCGkkV5vv1tOD5JhudaM73nTIgymy4wil2u9O/uhQ=="],
"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=="],
"duplexer2": ["duplexer2@0.1.4", "", { "dependencies": { "readable-stream": "^2.0.2" } }, "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA=="],
"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=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"entities": ["entities@7.0.1", "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
"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=="],
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"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=="],
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
"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=="],
"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=="],
"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=="],
"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=="],
"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=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"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=="],
"file-saver": ["file-saver@2.0.5", "", {}, "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="],
"fs.realpath": ["fs.realpath@1.0.0", "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"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=="],
"fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
"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.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"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=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"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=="],
"fstream": ["fstream@1.0.12", "", { "dependencies": { "graceful-fs": "^4.1.2", "inherits": "~2.0.0", "mkdirp": ">=0.5 0", "rimraf": "2" } }, "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg=="],
"hey-listen": ["hey-listen@1.0.8", "https://registry.npmmirror.com/hey-listen/-/hey-listen-1.0.8.tgz", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
"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=="],
"hookable": ["hookable@5.5.3", "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"ieee754": ["ieee754@1.2.1", "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
"immediate": ["immediate@3.0.6", "https://registry.npmmirror.com/immediate/-/immediate-3.0.6.tgz", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
"hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="],
"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=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"immediate": ["immediate@3.0.6", "", {}, "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="],
"is-what": ["is-what@5.5.0", "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"isarray": ["isarray@1.0.0", "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"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=="],
"is-what": ["is-what@5.5.0", "", {}, "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw=="],
"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=="],
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
"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=="],
"jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
"lie": ["lie@3.1.1", "https://registry.npmmirror.com/lie/-/lie-3.1.1.tgz", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw=="],
"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": ["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=="],
"lazystream": ["lazystream@1.0.1", "", { "dependencies": { "readable-stream": "^2.0.5" } }, "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw=="],
"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=="],
"lie": ["lie@3.1.1", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw=="],
"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=="],
"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-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-android-arm64": ["lightningcss-android-arm64@1.31.1", "", { "os": "android", "cpu": "arm64" }, "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg=="],
"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-darwin-arm64": ["lightningcss-darwin-arm64@1.31.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg=="],
"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-x64": ["lightningcss-darwin-x64@1.31.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA=="],
"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-freebsd-x64": ["lightningcss-freebsd-x64@1.31.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A=="],
"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-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.31.1", "", { "os": "linux", "cpu": "arm" }, "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g=="],
"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-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg=="],
"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-musl": ["lightningcss-linux-arm64-musl@1.31.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg=="],
"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-x64-gnu": ["lightningcss-linux-x64-gnu@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA=="],
"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-musl": ["lightningcss-linux-x64-musl@1.31.1", "", { "os": "linux", "cpu": "x64" }, "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA=="],
"listenercount": ["listenercount@1.0.1", "https://registry.npmmirror.com/listenercount/-/listenercount-1.0.1.tgz", {}, "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.31.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w=="],
"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-x64-msvc": ["lightningcss-win32-x64-msvc@1.31.1", "", { "os": "win32", "cpu": "x64" }, "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw=="],
"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=="],
"listenercount": ["listenercount@1.0.1", "", {}, "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="],
"lodash.difference": ["lodash.difference@4.5.0", "https://registry.npmmirror.com/lodash.difference/-/lodash.difference-4.5.0.tgz", {}, "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="],
"localforage": ["localforage@1.10.0", "", { "dependencies": { "lie": "3.1.1" } }, "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg=="],
"lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "https://registry.npmmirror.com/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
"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.difference": ["lodash.difference@4.5.0", "", {}, "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA=="],
"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.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="],
"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.flatten": ["lodash.flatten@4.4.0", "", {}, "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g=="],
"lodash.isequal": ["lodash.isequal@4.5.0", "https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="],
"lodash.groupby": ["lodash.groupby@4.6.0", "", {}, "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw=="],
"lodash.isfunction": ["lodash.isfunction@3.0.9", "https://registry.npmmirror.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz", {}, "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="],
"lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="],
"lodash.isnil": ["lodash.isnil@4.0.0", "https://registry.npmmirror.com/lodash.isnil/-/lodash.isnil-4.0.0.tgz", {}, "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng=="],
"lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="],
"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.isfunction": ["lodash.isfunction@3.0.9", "", {}, "sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw=="],
"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.isnil": ["lodash.isnil@4.0.0", "", {}, "sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng=="],
"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.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="],
"lodash.uniq": ["lodash.uniq@4.5.0", "https://registry.npmmirror.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="],
"lodash.isundefined": ["lodash.isundefined@3.0.1", "", {}, "sha512-MXB1is3s899/cD8jheYYE2V9qTHwKvt+npCwpD+1Sxm3Q3cECXCiYHjeHWXNwr6Q0SOBPrYUDxendrO6goVTEA=="],
"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.union": ["lodash.union@4.6.0", "", {}, "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw=="],
"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.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="],
"minimatch": ["minimatch@5.1.9", "https://registry.npmmirror.com/minimatch/-/minimatch-5.1.9.tgz", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
"lucide-vue-next": ["lucide-vue-next@0.563.0", "", { "peerDependencies": { "vue": ">=3.0.1" } }, "sha512-zsE/lCKtmaa7bGfhSpN84br1K9YoQ5pCN+2oKWjQQG3Lo6ufUUKBuHSjNFI6RvUevxaajNXb8XwFUKeTXG3sIA=="],
"mitt": ["mitt@3.0.1", "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
"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=="],
"minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="],
"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=="],
"mitt": ["mitt@3.0.1", "", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
"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=="],
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
"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=="],
"motion-dom": ["motion-dom@12.34.3", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ=="],
"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-utils": ["motion-utils@12.29.2", "", {}, "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A=="],
"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-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=="],
"normalize-path": ["normalize-path@3.0.0", "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
"ohash": ["ohash@2.0.11", "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"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=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"pako": ["pako@1.0.11", "https://registry.npmmirror.com/pako/-/pako-1.0.11.tgz", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
"path-browserify": ["path-browserify@1.0.1", "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"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=="],
"pako": ["pako@2.2.0", "", {}, "sha512-zJq6RP/5q+TO2OpFV3FHzlPnFjmkb7Nc99a5SNjJE+uu/PkpChs+NIZSSzbBoD+6kjiISXjfYdwj1ZRQ81dz/w=="],
"perfect-debounce": ["perfect-debounce@1.0.0", "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"picomatch": ["picomatch@4.0.3", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="],
"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=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"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=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"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=="],
"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=="],
"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-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=="],
"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=="],
"pizzip": ["pizzip@3.2.0", "", { "dependencies": { "pako": "^2.1.0" } }, "sha512-X4NPNICxCfIK8VYhF6wbksn81vTiziyLbvKuORVAmolvnUzl1A1xmz9DAWKxPRq9lZg84pJOOAMq3OE61bD8IQ=="],
"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=="],
"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=="],
"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=="],
"process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="],
"rfdc": ["rfdc@1.4.1", "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"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=="],
"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=="],
"readdir-glob": ["readdir-glob@1.1.3", "", { "dependencies": { "minimatch": "^5.1.0" } }, "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA=="],
"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=="],
"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=="],
"safe-buffer": ["safe-buffer@5.1.2", "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.1.2.tgz", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"saxes": ["saxes@5.0.1", "https://registry.npmmirror.com/saxes/-/saxes-5.0.1.tgz", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw=="],
"rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="],
"setimmediate": ["setimmediate@1.0.5", "https://registry.npmmirror.com/setimmediate/-/setimmediate-1.0.5.tgz", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
"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=="],
"sortablejs": ["sortablejs@1.14.0", "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.14.0.tgz", {}, "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="],
"safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"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=="],
"saxes": ["saxes@5.0.1", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw=="],
"speakingurl": ["speakingurl@14.0.1", "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
"setimmediate": ["setimmediate@1.0.5", "", {}, "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="],
"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=="],
"sortablejs": ["sortablejs@1.14.0", "", {}, "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="],
"superjson": ["superjson@2.2.6", "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"tailwind-merge": ["tailwind-merge@3.5.0", "https://registry.npmmirror.com/tailwind-merge/-/tailwind-merge-3.5.0.tgz", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
"speakingurl": ["speakingurl@14.0.1", "", {}, "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ=="],
"tailwindcss": ["tailwindcss@4.2.0", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.2.0.tgz", {}, "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"tapable": ["tapable@2.3.0", "https://registry.npmmirror.com/tapable/-/tapable-2.3.0.tgz", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"superjson": ["superjson@2.2.6", "", { "dependencies": { "copy-anything": "^4" } }, "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA=="],
"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=="],
"tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
"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=="],
"tailwindcss": ["tailwindcss@4.2.0", "", {}, "sha512-yYzTZ4++b7fNYxFfpnberEEKu43w44aqDMNM9MHMmcKuCH7lL8jJ4yJ7LGHv7rSwiqM0nkiobF9I6cLlpS2P7Q=="],
"tmp": ["tmp@0.2.5", "https://registry.npmmirror.com/tmp/-/tmp-0.2.5.tgz", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"traverse": ["traverse@0.3.9", "https://registry.npmmirror.com/traverse/-/traverse-0.3.9.tgz", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="],
"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=="],
"tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"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=="],
"tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="],
"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=="],
"traverse": ["traverse@0.3.9", "", {}, "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="],
"undici-types": ["undici-types@7.16.0", "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"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=="],
"tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="],
"util-deprecate": ["util-deprecate@1.0.2", "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"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=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
"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=="],
"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=="],
"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=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"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=="],
"uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="],
"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=="],
"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-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=="],
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
"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=="],
"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=="],
"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-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=="],
"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-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=="],
"wrappy": ["wrappy@1.0.2", "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"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=="],
"xmlchars": ["xmlchars@2.2.0", "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
"vuedraggable": ["vuedraggable@4.1.0", "", { "dependencies": { "sortablejs": "1.14.0" }, "peerDependencies": { "vue": "^3.0.1" } }, "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww=="],
"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=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"@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=="],
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
"@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=="],
"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/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=="],
"@fast-csv/format/@types/node": ["@types/node@14.18.63", "", {}, "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="],
"@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/parse/@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=="],
"@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/@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/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
"@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/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@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/@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=="],
"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/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"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/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"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=="],
"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/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=="],
"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=="],
"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=="],
"glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
"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=="],
"jszip/lie": ["lie@3.3.0", "", { "dependencies": { "immediate": "~3.0.5" } }, "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ=="],
"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/pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
"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/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=="],
"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=="],
"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=="],
"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=="],
"reka-ui/@internationalized/date": ["@internationalized/date@3.11.0", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-BOx5huLAWhicM9/ZFs84CzP+V3gBW6vlpM02yzsdYC7TGlZJX1OJiEEHcSayF00Z+3jLlm4w79amvSt6RqKN3Q=="],
"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=="],
"rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.5", "", {}, "sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw=="],
"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=="],
"string_decoder/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"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=="],
"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=="],
"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=="],
"vue-i18n/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"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=="],
"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=="],
"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=="],
"archiver-utils/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="],
"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=="],
"duplexer2/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=="],
"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=="],
"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=="],
}
}

117
data.js Normal file
View File

@ -0,0 +1,117 @@
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,7 +4,19 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<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>
<body>
<div id="app"></div>

67
package-lock.json generated
View File

@ -21,20 +21,18 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"decimal.js": "^10.6.0",
"docxtemplater": "^3.68.5",
"exceljs": "^4.4.0",
"file-saver": "^2.0.5",
"localforage": "^1.10.0",
"lucide-vue-next": "^0.563.0",
"motion-v": "^2.0.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"pizzip": "^3.2.0",
"reka-ui": "^2.8.0",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"vue": "^3.5.25",
"vue-i18n": "^11.3.0",
"vue-router": "^4.6.4",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
@ -659,15 +657,6 @@
"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": {
"version": "13.1.0",
"license": "MIT",
@ -1024,18 +1013,6 @@
"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": {
"version": "0.1.4",
"license": "BSD-3-Clause",
@ -1140,12 +1117,6 @@
}
}
},
"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": {
"version": "12.34.3",
"license": "MIT",
@ -1667,21 +1638,6 @@
}
}
},
"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": {
"version": "8.5.6",
"funding": [
@ -2172,6 +2128,27 @@
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"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": {
"version": "3.2.5",
"dev": true,

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -1 +0,0 @@
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

BIN
public/related-files.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 856 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,323 +1,3 @@
<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 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>
<RouterView />
</template>

View File

@ -0,0 +1,318 @@
<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

@ -0,0 +1,197 @@
<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,7 +74,6 @@ import {
ToastTitle,
ToastViewport
} from 'reka-ui'
import {useDataStore} from '@/pinia/zx'
const STORAGE_KEY = 'ht-card-v1'
const tabStore = useTabStore()
@ -334,18 +333,8 @@ const loadContractBudgetFee = async (contractId: string) => {
loadHtMainTotalFee(`htExtraFee-${contractId}-additional-work`),
loadHtMainTotalFee(`htExtraFee-${contractId}-reserve`)
])
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)*/
const parts = [serviceFee, additionalFee, reserveFee]
const total = sumNullableNumbers(parts)
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 { useI18n } from 'vue-i18n'
import { AgGridVue } from 'ag-grid-vue3'
@ -60,23 +60,12 @@ const rowData = ref<SummaryRow[]>([])
const explanationText = ref('')
let reloadTimer: ReturnType<typeof setTimeout> | null = null
/**
* 对数值数组求和保留3位小数
* @param values 数值数组可包含 null/undefined
* @returns 求和结果无有效值时返回 null
*/
const sum3 = (values: Array<number | null | undefined>) => {
const valid = values.filter((v): v is number => toFiniteNumberOrNull(v) != null)
if (valid.length === 0) return null
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 rows = Array.isArray(state?.detailRows) ? state.detailRows : []
if (rows.length === 0) return null
@ -99,13 +88,6 @@ const sumHourlyMethodFee = (state: HourlyMethodStateLike | null): number | 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 rows = Array.isArray(state?.detailRows) ? state.detailRows : []
if (rows.length === 0) return null
@ -128,12 +110,6 @@ const sumQuantityMethodFee = (state: QuantityMethodStateLike | null): number | n
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<{
subtotal: number | null
m0: { coe: string; fee: number } | null
@ -163,12 +139,6 @@ const loadHtMethodSummaryByRow = async (mainStorageKey: string, rowId: string):
}
}
/**
* 构建附加工作或预备费的汇总行数据
* @param rowType 行类型'additional' 附加工作 'reserve' 预备费
* @param list 费用项目列表包含 idnamecode
* @returns 包含 rows汇总行数组 explainLines说明文本数组的对象
*/
const buildFeeRows = async (
rowType: 'additional' | 'reserve',
list: Array<{ id: string | number; name: string; code: unknown }>
@ -212,11 +182,6 @@ const buildFeeRows = async (
return { rows, explainLines }
}
/**
* 构建咨询服务汇总行数据
* 从合同状态中筛选已选中的服务项转换为 SummaryRow 格式
* @returns 咨询服务汇总行数组
*/
const buildServiceRows = (): SummaryRow[] => {
const contractState = zxFwPricingStore.getContractState(props.contractId)
const selectedSet = new Set((contractState?.selectedIds || []).map(id => String(id)))
@ -237,11 +202,6 @@ const buildServiceRows = (): SummaryRow[] => {
}))
}
/**
* 重新加载所有汇总行数据
* 包括咨询服务附加工作预备费
* 同时生成说明文本
*/
const reloadRows = async () => {
await zxFwPricingStore.loadContract(props.contractId)
const [additionalResult, reserveResult] = await Promise.all([
@ -259,10 +219,6 @@ const reloadRows = async () => {
explanationText.value = lines.join('\n')
}
/**
* 延迟重新加载汇总数据防抖 80ms
* 避免频繁触发数据加载
*/
const scheduleReload = () => {
if (reloadTimer) clearTimeout(reloadTimer)
reloadTimer = setTimeout(() => {
@ -270,10 +226,6 @@ const scheduleReload = () => {
}, 80)
}
/**
* 刷新签名用于 watch 监听数据变化
* 监听合同数据附加工作预备费的主状态和方法状态变化
*/
const refreshSignature = computed(() => {
const additionalKey = `htExtraFee-${props.contractId}-additional-work`
const reserveKey = `htExtraFee-${props.contractId}-reserve`
@ -286,10 +238,6 @@ const refreshSignature = computed(() => {
})
})
/**
* 计算总计行数据
* 对所有汇总行的各字段进行求和
*/
const totalRow = computed<SummaryRow>(() => {
const sumField = (pick: (row: SummaryRow) => number | null | undefined) =>
sum3(rowData.value.map(pick))
@ -354,6 +302,9 @@ const columnDefs: ColDef<SummaryRow>[] = [
minWidth: 90,
maxWidth: 140,
colSpan: params => (params.data?.rowType === 'total' ? 2 : 1),
cellClassRules: {
'ag-summary-label-cell': params => params.data?.rowType === 'total'
},
valueFormatter: params => {
if (params.data?.rowType === 'total') return params.data.name || t('htSummary.total')
return typeof params.value === 'string' ? params.value : ''
@ -471,28 +422,14 @@ const summaryGridOptions: GridOptions<SummaryRow> = {
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>) => {
gridApi.value = event.api
void syncAutoRowHeights()
}
/**
* 检查 Grid API 是否可用
* @param api Grid API 实例
* @returns API 是否有效
*/
const isGridApiAlive = (api: GridApi<SummaryRow> | null | undefined): api is GridApi<SummaryRow> =>
Boolean(api && !api.isDestroyed?.())
/**
* 同步自动行高
* 触发 AG Grid 重新计算行高并刷新单元格
*/
const syncAutoRowHeights = async () => {
await nextTick()
const api = gridApi.value
@ -501,20 +438,10 @@ const syncAutoRowHeights = async () => {
api.refreshCells({ force: true })
}
/**
* 首次数据渲染完成回调
* 同步自动行高以确保显示正确
* @param _event 首次数据渲染事件
*/
const onFirstDataRendered = (_event: FirstDataRenderedEvent<SummaryRow>) => {
void syncAutoRowHeights()
}
/**
* 行数据更新回调
* 同步自动行高以适应新数据
* @param _event 行数据更新事件
*/
const onRowDataUpdated = (_event: RowDataUpdatedEvent<SummaryRow>) => {
void syncAutoRowHeights()
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -20,14 +20,7 @@ import {useZxFwPricingStore, type HtFeeMethodType, type ServicePricingMethod} fr
import { AG_GRID_LOCALE_CN } from '@ag-grid-community/locale'
import { buildProjectScopedSessionKey } from '@/lib/pricingPersistControl'
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 {
id: string
expertCode: string
@ -36,35 +29,15 @@ interface DetailRow {
compositeBudgetUnitPrice: string
adoptedBudgetUnitPrice: number | null
personnelCount: number | null
workdayCount: number | string | null
serviceBudget: number | string | null
workdayCount: number | null
serviceBudget: number | null
remark: 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 {
detailRows: DetailRow[]
}
const props = withDefaults(
defineProps<{
storageKey: string
@ -151,9 +124,34 @@ const getHtMethodState = () => {
}
const detailRows = computed<DetailRow[]>({
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
},
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
}
})
@ -215,18 +213,12 @@ const getDefaultAdoptedBudgetUnitPrice = (expert: ExpertLite) => {
return roundTo(toDecimal(expert.defPrice).mul(expert.manageCoe), 2)
}
const buildDefaultRows = async (): Promise<DetailRow[]> => {
const buildDefaultRows = (): DetailRow[] => {
const rows: DetailRow[] = []
const rowsToMap = await useDataStore().query([
{ 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}`
for (const [expertId, expert] of expertEntries) {
const rowId = `expert-${expertId}`
rows.push({
id: rowId,
type: `${props.contractId}-hourly`,
expertCode: expert.code,
expertName: getExpertDisplayName(expert),
laborBudgetUnitPrice: formatPriceRange(expert.minPrice, expert.maxPrice),
@ -242,48 +234,23 @@ const buildDefaultRows = async (): Promise<DetailRow[]> => {
return rows
}
/**
* 合并数据库中的行数据与默认行数据
* 保留用户编辑的所有字段值包括新增的职称分类字段
*/
const mergeWithDictRows = async (rowsFromDb: DetailRow[] | undefined): Promise<DetailRow[]> => {
const mergeWithDictRows = (rowsFromDb: DetailRow[] | undefined): DetailRow[] => {
const dbValueMap = new Map<string, DetailRow>()
for (const row of rowsFromDb || []) {
dbValueMap.set(row.id, row)
}
const defaultRows = await buildDefaultRows()
return defaultRows.map(row => {
return buildDefaultRows().map(row => {
const fromDb = dbValueMap.get(row.id)
if (!fromDb) return row
// 使
return {
...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,
workdayCount: typeof fromDb.workdayCount === 'number' ? fromDb.workdayCount : null,
serviceBudget: typeof fromDb.serviceBudget === 'number' ? fromDb.serviceBudget : null,
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,
remark: typeof fromDb.remark === 'string' ? fromDb.remark : ''
}
})
}
@ -331,258 +298,7 @@ const calcServiceBudget = (row: DetailRow | undefined) => {
const syncServiceBudgetToRows = () => {
for (const row of detailRows.value) {
// 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'
row.serviceBudget = calcServiceBudget(row)
}
}
@ -660,7 +376,10 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
width: 100,
pinned: 'left',
colSpan: params => (params.node?.rowPinned ? 2 : 1),
valueFormatter: params => (params.node?.rowPinned ? t('hourlyFeeGrid.total') : params.value || 0)
cellClassRules: {
'ag-summary-label-cell': params => Boolean(params.node?.rowPinned)
},
valueFormatter: params => (params.node?.rowPinned ? t('hourlyFeeGrid.total') : params.value || '')
},
{
headerName: t('hourlyFeeGrid.columns.name'),
@ -676,77 +395,17 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '')
},
{
headerName: t('hourlyFeeGrid.columns.technician'),
headerName: t('hourlyFeeGrid.columns.referenceUnitPrice'),
marryChildren: true,
children: [
// /
editableNumberCol2('unitPrice', t('hourlyFeeGrid.columns.unitPrice'), {decimals: 0}),
// 1
editableNumberCol3('workdayCount', t('hourlyFeeGrid.columns.workdayCount'), {decimals: 1}),
//
calculatedFeeSubtotalCol('feeSubtotal', '费用小计(元)', 'unitPrice', 'workdayCount')
readonlyTextCol('laborBudgetUnitPrice', t('hourlyFeeGrid.columns.laborBudgetUnitPrice'), {
colSpan: params => (params.node?.rowPinned ? 3 : 1),
valueFormatter: params => (params.node?.rowPinned ? '' : params.value || '')
}),
readonlyTextCol('compositeBudgetUnitPrice', t('hourlyFeeGrid.columns.compositeBudgetUnitPrice'))
]
},
{
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')),
editableMoneyCol('adoptedBudgetUnitPrice', t('hourlyFeeGrid.columns.adoptedBudgetUnitPrice')),
editableNumberCol('personnelCount', t('hourlyFeeGrid.columns.personnelCount'), {
aggFunc: decimalAggSum,
valueParser: params => parseNonNegativeIntegerOrNull(params.newValue),
@ -769,7 +428,7 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
if (params.value == null || params.value === '') return ''
return formatThousandsFlexible(params.value, 3)
}
},*/
},
{
headerName: t('hourlyFeeGrid.columns.remark'),
field: 'remark',
@ -793,30 +452,10 @@ const columnDefs: (ColDef<DetailRow> | ColGroupDef<DetailRow>)[] = [
]
const gridColumnDefs = computed(() => withReadonlyAutoHeight(columnDefs))
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 totalWorkdayCount = computed(() => sumByNumber(detailRows.value, row => row.workdayCount))
const totalServiceBudget = computed(() => sumNullableNumbers(detailRows.value.map(row => calcServiceBudget(row))))
const pinnedTopRowData = computed(() => {
const result = [
const pinnedTopRowData = computed(() => [
{
id: 'pinned-total-row',
expertCode: t('hourlyFeeGrid.total'),
@ -824,110 +463,84 @@ const pinnedTopRowData = computed(() => {
laborBudgetUnitPrice: '',
compositeBudgetUnitPrice: '',
adoptedBudgetUnitPrice: null,
// personnelCount: totalPersonnelCount.value,
unitPrice: '/',
personnelCount: totalPersonnelCount.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,
remark: '',
path: ['TOTAL']
}
]
return result
})
/**
* 保存数据到 IndexedDB
* 保存时机
* 1. 单元格值变化时handleCellValueChanged
* 2. 组件失活时onDeactivated- 切换页面
* 3. 组件卸载前onBeforeUnmount- 关闭页面
*/
const recalculateServiceFees = () => {
for (const row of fallbackDetailRows.value) {
row.workdayCount6 = row.workdayCount + row.workdayCount2 + row.workdayCount3 + row.workdayCount4 + row.workdayCount5
row.feeSubtotal6 = row.feeSubtotal + row.feeSubtotal2 + row.feeSubtotal3 + row.feeSubtotal4 + row.feeSubtotal5
row.avgUnitPrice = row.feeSubtotal6 * row.workdayCount6
}
}
])
const saveToIndexedDB = async () => {
if (shouldSkipPersist()) return
try {
// syncServiceBudgetToRows()
syncServiceBudgetToRows();
// 使 upsertBatch
const rows = detailRows.value.map(row => ({ ...row }))
const stats = await useDataStore().upsertBatch(rows)
syncServiceBudgetToRows()
const payload: GridState = {
detailRows: JSON.parse(JSON.stringify(detailRows.value))
}
if (useServicePricingState.value && serviceMethod.value) {
zxFwPricingStore.setServicePricingMethodState(
props.contractId!,
props.serviceId!,
serviceMethod.value,
payload,
{ 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 synced = await syncPricingTotalToZxFw({
contractId: props.contractId,
serviceId: props.serviceId,
field: props.syncField,
value: totalServiceBudget.value
})
if (!synced) return
}
console.log('💾 数据保存成功:', stats)
} catch (error) {
console.error('❌ saveToIndexedDB 失败:', error)
console.error('saveToIndexedDB failed:', error)
}
}
/**
* IndexedDB 加载数据
* 加载时机
* 1. 组件挂载时onMounted
* 2. 组件激活时onActivated- 切换回页面
* 3. storageKey 变化时watch
*/
const loadFromIndexedDB = async () => {
try {
// type 'hourly'
const hourlyRows = await useDataStore().query([
{ field: 'type', value: `${props.contractId}-hourly`, operator: 'eq' }
])
// DataItem[] DetailRow[]
const detailRowsFromStore: DetailRow[] = hourlyRows.map(row => ({
id: String(row.id || ''),
expertCode: String(row.expertCode || ''),
expertName: String(row.expertName || ''),
laborBudgetUnitPrice: String(row.laborBudgetUnitPrice || ''),
compositeBudgetUnitPrice: String(row.compositeBudgetUnitPrice || ''),
adoptedBudgetUnitPrice: row.adoptedBudgetUnitPrice != null ? Number(row.adoptedBudgetUnitPrice) : null,
personnelCount: row.personnelCount != null ? Number(row.personnelCount) : null,
workdayCount: row.workdayCount != null ? Number(row.workdayCount) : null,
serviceBudget: row.serviceBudget != null ? Number(row.serviceBudget) : null,
remark: String(row.remark || ''),
path: Array.isArray(row.path) ? row.path : [],
unitPrice: row.unitPrice != null ? Number(row.unitPrice) : null,
workdayCount1: row.workdayCount1 != null ? Number(row.workdayCount1) : null,
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)
if (shouldForceDefaultLoad()) {
detailRows.value = buildDefaultRows()
syncServiceBudgetToRows()
return
}
const data = useServicePricingState.value && serviceMethod.value
? await zxFwPricingStore.loadServicePricingMethodState<DetailRow>(props.contractId!, props.serviceId!, serviceMethod.value)
: useHtMethodState.value
? await zxFwPricingStore.loadHtFeeMethodState<GridState>(
props.htMainStorageKey!,
props.htRowId!,
props.htMethodType!
)
: await zxFwPricingStore.loadKeyState<GridState>(props.storageKey)
if (data) {
detailRows.value = mergeWithDictRows(data.detailRows)
syncServiceBudgetToRows()
return
}
detailRows.value = buildDefaultRows()
syncServiceBudgetToRows()
} catch (error) {
detailRows.value = await buildDefaultRows()
console.error('loadFromIndexedDB failed:', error)
detailRows.value = buildDefaultRows()
syncServiceBudgetToRows()
}
}
@ -954,29 +567,16 @@ const relabelRowsFromExpertDict = async () => {
let isBulkClipboardMutation = false
/**
* 提交网格变更同步计算值刷新UI保存到数据库
* @param source 变更来源用于日志追踪
*/
const commitGridChanges = (source: string) => {
console.log('🔄 提交网格变更:', source)
syncServiceBudgetToRows()
gridApi.value?.refreshCells({ force: true })
scheduleAutoRowHeights()
void saveToIndexedDB()
}
/**
* 处理单元格值变化事件
* 触发时机用户编辑完单元格后
*/
const handleCellValueChanged = (event?: any) => {
/*if (isBulkClipboardMutation) {
return
}
commitGridChanges('cell-value-changed')*/
saveToIndexedDB()
if (isBulkClipboardMutation) return
commitGridChanges('cell-value-changed')
}
const handleBulkMutationStart = () => {
@ -984,8 +584,8 @@ const handleBulkMutationStart = () => {
}
const handleBulkMutationEnd = (event?: any) => {
// isBulkClipboardMutation = false
// commitGridChanges(event?.type || 'bulk-end')
isBulkClipboardMutation = false
commitGridChanges(event?.type || 'bulk-end')
}
const handleGridReady = (event: GridReadyEvent<DetailRow>) => {
@ -1063,34 +663,16 @@ const processCellFromClipboard = (params: any) => {
return params.value
}
/**
* 组件挂载时加载数据
*/
onMounted(async () => {
console.log('🚀 组件挂载')
await loadFromIndexedDB()
scheduleAutoRowHeights()
})
/**
* 组件激活时重新加载数据从其他页面切换回来时
*/
onActivated(async () => {
console.log('🔙 组件激活')
await loadFromIndexedDB()
scheduleAutoRowHeights()
})
watch(
() => useDataStore().items,
async (newItems) => {
if (Object.keys(newItems).length > 0) {
loadFromIndexedDB();
}
},
{ immediate: true, deep: true }
)
watch(
() => props.storageKey,
() => {
@ -1113,21 +695,12 @@ watch(
}
)
/**
* 组件失活时保存数据切换页面时触发
* 配合 Vue <keep-alive> 使用
*/
onDeactivated(() => {
console.log('🔄 组件失活,保存数据')
gridApi.value?.stopEditing()
void saveToIndexedDB()
})
/**
* 组件卸载前保存数据关闭页面时触发
*/
onBeforeUnmount(() => {
console.log('💔 组件卸载,保存数据')
gridApi.value?.stopEditing()
gridApi.value = null
if (autoHeightSyncTimer) {
@ -1150,7 +723,7 @@ onBeforeUnmount(() => {
<AgGridVue
:style="agGridStyle"
:rowData="detailRows"
:pinnedBottomRowData="pinnedTopRowData"
:pinnedTopRowData="pinnedTopRowData"
:columnDefs="gridColumnDefs"
:gridOptions="gridOptions"
:theme="myTheme"

View File

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

View File

@ -375,6 +375,12 @@ const saveToIndexedDB = async (force = false) => {
const snapshot = JSON.stringify(payload.detailRows)
if (!force && snapshot === lastSavedSnapshot.value) return
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
} catch (error) {
console.error('saveToIndexedDB failed:', error)
@ -498,6 +504,7 @@ const columnDefs: ColDef<FeeMethodRow>[] = [
? ''
: 'editable-cell-line',
cellClassRules: {
'ag-summary-label-cell': params => isSummaryRow(params.data),
'editable-cell-empty': params => params.value == null || params.value === ''
}
},

View File

@ -1,6 +1,5 @@
<!--
<script setup lang="ts">
import { computed } from 'vue'
import { computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
interface ServiceItem {
@ -11,6 +10,7 @@ interface ServiceItem {
const props = defineProps<{
services: ServiceItem[]
serviceRows?: string[][]
modelValue: string[]
}>()
@ -19,68 +19,67 @@ const emit = defineEmits<{
}>()
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 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 toggleService = (item: ServiceItem, checked: boolean) => {
const { disabled } = isServiceDisabled(item)
//
if (checked && disabled) return
const used = new Set<string>()
const grouped = rows
.map(row => row.map(id => serviceById.value.get(id)).filter((item): item is ServiceItem => Boolean(item)))
.map(row => row.filter(item => {
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)
if (checked) {
next.add(item.id)
//
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)
}
if (firstRowIdSet.value.has(id)) {
firstRowIds.value.forEach(firstId => next.delete(firstId))
}
next.add(id)
} else {
next.delete(item.id)
next.delete(id)
}
emit('update:modelValue', props.services.map(s => s.id).filter(id => next.has(id)))
emit('update:modelValue', props.services.map(item => item.id).filter(itemId => next.has(itemId)))
}
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 = () => {
emit('update:modelValue', [])
}
@ -89,7 +88,10 @@ const clearAll = () => {
<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>
<div class="flex min-w-0 items-center gap-1.5">
<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
type="button"
class="cursor-pointer h-6 rounded-md border px-2 text-[13px] text-muted-foreground transition hover:bg-accent"
@ -99,102 +101,37 @@ const clearAll = () => {
</button>
</div>
<div class="rounded-md border p-1.5">
<div class="flex flex-wrap items-start gap-1">
<div v-if="groupedRows.length > 0" class="flex flex-col gap-1.5">
<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
v-for="item in props.services"
v-for="item in row"
: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',
isServiceDisabled(item).disabled
? 'opacity-50 cursor-not-allowed hover:bg-transparent'
: 'hover:bg-muted/60'
'inline-flex w-fit max-w-full items-start gap-1.5 rounded-md border px-2 py-1 text-[11px] leading-4 transition',
isFirstRowDisabled(item.id)
? 'cursor-not-allowed border-slate-300 bg-slate-100/80 text-slate-400 opacity-80'
: 'cursor-pointer hover:bg-muted/60'
]"
:title="isServiceDisabled(item).reason"
>
<input
type="checkbox"
class="mt-0.5"
:class="[
'mt-0.5',
isFirstRowDisabled(item.id) ? 'cursor-not-allowed accent-slate-300' : 'cursor-pointer'
]"
:checked="selectedSet.has(item.id)"
:disabled="isServiceDisabled(item).disabled"
@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)"
:disabled="isFirstRowDisabled(item.id)"
@change="toggleService(item.id, ($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>
<div v-if="props.services.length === 0" class="px-2 py-4 text-center text-xs text-muted-foreground">
{{ t('serviceSelector.empty') }}
</div>

View File

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

View File

@ -138,6 +138,16 @@ const hasMeaningfulFactorValue = (rows: SourceRow[] | undefined) =>
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 dbValueMap = new Map<string, SourceRow>()
for (const row of rowsFromDb || []) {
@ -308,7 +318,7 @@ const saveFactorChangeState = async (changedRowIds: string[]) => {
const loadGridState = async (storageKey: string): Promise<GridState | null> => {
if (!storageKey) return null
const piniaData = await zxFwPricingStore.loadKeyState<GridState>(storageKey)
if (piniaData?.detailRows && Array.isArray(piniaData.detailRows)) return piniaData
if (hasUsablePersistedRows(piniaData)) return piniaData
// kvStore pinia keyed state
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 { parseNumberOrNull } from '@/lib/number'
import { formatThousandsFlexible } from '@/lib/numberFormat'
import { getIndustryDisplayName, getMajorDictEntries, isMajorIdInIndustryScope, xmProjectConfig } from '@/sql'
import { getIndustryDisplayName, getMajorDictEntries, isMajorIdInIndustryScope } from '@/sql'
import { syncContractScaleToPricing, type ContractScaleSyncResult } from '@/lib/zxFwPricingSync'
import { SwitchRoot, SwitchThumb } from 'reka-ui'
import { useKvStore } from '@/pinia/kv'
@ -22,6 +22,8 @@ import {
ToastViewport
} from 'reka-ui'
import { Button } from '@/components/ui/button'
import { CircleHelp } from 'lucide-vue-next'
import { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '@/components/ui/tooltip'
@ -110,35 +112,14 @@ const buildDetailDict = (entries: Array<[string, MajorLite]>) => {
hasArea: item.hasArea !== false
})
}
const result = groupOrder.map(code => groupMap.get(code)).filter((group): group is DictGroup => Boolean(group))
return result
return groupOrder.map(code => groupMap.get(code)).filter((group): group is DictGroup => Boolean(group))
}
const buildDefaultRows = (): 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 child of group.children) {
@ -345,61 +326,25 @@ interface ContractScaleChangeState {
updatedAt: number
}
const XM_PROJECT_PHASE_KEY = 'xm-project-phase-v1'
interface ProjectPhaseState {
feePhase?: string
feeStage?: string
}
const props = defineProps<{
title: string
dbKey: string
xmInfoKey?: string | null
baseInfoKey?: string
titleHint?: string
titleHintAria?: string
}>()
let persistTimer: ReturnType<typeof setTimeout> | null = null
const gridApi = ref<GridApi<DetailRow> | null>(null)
const activeIndustryId = ref('')
const totalLabel = ref(xmProjectConfig.pinnedTotalLabel)
const totalLabel = computed(() => {
const industryName = getIndustryDisplayName(activeIndustryId.value.trim(), locale.value)
return industryName ? t('pricingScale.totalInvestmentByIndustry', { industryName }) : t('pricingScale.totalInvestment')
})
const roughCalcEnabled = ref(false)
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 = () => {
if (!gridApi.value) return
const pinnedTopNode = gridApi.value.getPinnedTopRow(0)
@ -453,26 +398,34 @@ const columnDefs: ColDef<DetailRow>[] = [
}
},
{
headerName: '造价金额(元)',
field: 'amountYuan',
headerName: t('pricingScale.columns.landArea'),
field: 'landArea',
headerClass: 'ag-right-aligned-header',
minWidth: 120,
minWidth: 100,
flex: 1,
editable: false,
aggFunc: decimalAggSum,
valueGetter: params => params.data?.amount,
editable: params => !roughCalcEnabled.value && !params.node?.group && !params.node?.rowPinned && Boolean(params.data?.hasArea),
cellClass: params =>
!params.node?.group && !params.node?.rowPinned && params.data?.hasArea
? 'editable-cell-line'
: '',
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 => {
if (roughCalcEnabled.value) {
return ''
}
const amount = params.value
if (typeof amount !== 'number' || !Number.isFinite(amount)) {
if (!params.node?.group && !params.node?.rowPinned && !params.data?.hasArea) {
return ''
}
return formatThousandsFlexible(roundTo(amount * 10000, 0), 0)
if (!params.node?.group && !params.node?.rowPinned && (params.value == null || params.value === '')) {
return t('pricingScale.clickToInput')
}
if (params.value == null) return ''
return formatThousandsFlexible(params.value, 3)
}
}
]
@ -482,6 +435,9 @@ const autoGroupColumnDef: ColDef = {
headerName: t('pricingScale.columns.majorGroup'),
minWidth: 200,
flex: 2,
cellClassRules: {
'ag-summary-label-cell': params => Boolean(params.node?.rowPinned)
},
cellRendererParams: {
suppressCount: true
},
@ -807,10 +763,24 @@ onMounted(() => {
<div class="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 gap-1.5">
<h3
class="text-sm font-semibold text-foreground cursor-pointer select-none transition-colors hover:text-primary">
{{ props.title }}
</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">
<span class=" text-xs text-muted-foreground">简要计算</span>
<SwitchRoot
@ -822,42 +792,6 @@ onMounted(() => {
</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">
<AgGridVue :style="agGridStyle" :rowData="visibleRowData" :pinnedTopRowData="pinnedTopRowData"
:columnDefs="gridColumnDefs" :autoGroupColumnDef="autoGroupColumnDef" :gridOptions="detailGridOptions" :theme="myTheme"

View File

@ -44,24 +44,3 @@
opacity: 0;
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,25 +1,41 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { Button } from '@/components/ui/button'
import { useTabStore } from '@/pinia/tab'
import { useKvStore } from '@/pinia/kv'
import { useUiPrefsStore } from '@/pinia/uiPrefs'
import {
BarChart3,
Calculator,
Check,
ChevronDown,
Languages,
X
} from 'lucide-vue-next'
import { getIndustryDisplayName, industryTypeList } from '@/sql'
import { initializeProjectFactorStates, initializeProjectScaleState } from '@/lib/projectWorkspace'
import {
SelectContent,
SelectIcon,
SelectItem,
SelectItemIndicator,
SelectItemText,
SelectPortal,
SelectRoot,
SelectTrigger,
SelectViewport
} from 'reka-ui'
import {
buildDisclaimerUrl,
buildProjectUrl,
consumePendingDisclaimerAction,
DEFAULT_PROJECT_ID,
hasAcceptedRestrictedDisclaimer,
FORCE_HOME_QUERY_KEY,
isDisclaimerAcceptanceRequired,
NEW_PROJECT_QUERY_KEY,
OPEN_PROJECT_DIALOG_QUERY_KEY,
setPendingDisclaimerAction,
PROJECT_TAB_ID,
QUICK_PROJECT_ID,
QUICK_CONSULT_CATEGORY_FACTOR_KEY,
@ -65,12 +81,13 @@ const PROJECT_INFO_KEY = 'xm-base-info-v1'
const PROJECT_CONSULT_CATEGORY_FACTOR_KEY = 'xm-consult-category-factor-v1'
const PROJECT_MAJOR_FACTOR_KEY = 'xm-major-factor-v1'
const PROJECT_SCALE_KEY = 'xm-info-v3'
const FILE_LEDGER_URL = 'https://www.lianzhong.com.cn/file?fileNo=24'
const getActiveProjectId = () => readCurrentProjectId()
const tabStore = useTabStore()
const kvStore = useKvStore()
const uiPrefsStore = useUiPrefsStore()
const { t, locale } = useI18n()
const { t, tm, locale } = useI18n()
const projectDialogOpen = ref(false)
const projectIndustry = ref(String(industryTypeList[0]?.id || ''))
const projectSubmitting = ref(false)
@ -82,21 +99,24 @@ const homeImportConfirmOpen = ref(false)
const pendingHomeImportFile = ref<File | null>(null)
const pendingHomeImportFileName = ref('')
const existingProjectDialogOpen = ref(false)
const disclaimerRequired = ref(false)
const existingProjects = ref<Array<{ id: string; name: string; updatedAt: string }>>([])
const existingProjectLoading = ref(false)
const hasExistingProjects = ref(false)
const openedProjectIds = ref<string[]>([])
let existingProjectPollTimer: ReturnType<typeof setInterval> | null = null
const RELATED_FILES_URL = 'https://www.lianzhong.com.cn/file'
const heroTitleIndex = ref(0)
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 toggleLocale = () => {
const next = locale.value === 'en-US' ? '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 projectId = String(projectIdRaw || '').trim()
if (projectId !== DEFAULT_PROJECT_ID) return undefined
@ -162,8 +182,35 @@ const loadProjectDefaults = async () => {
}
const openProjectCalc = async () => {
await runWithDisclaimerGuard({ type: 'project' }, async () => {
await loadProjectDefaults()
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[]) => {
@ -216,10 +263,11 @@ const startExistingProjectPolling = () => {
}
const openExistingProjectDialog = async () => {
projectDialogOpen.value = false
await runWithDisclaimerGuard({ type: 'existing-project' }, async () => {
existingProjectDialogOpen.value = true
await refreshExistingProjects()
startExistingProjectPolling()
})
}
const closeExistingProjectDialog = () => {
@ -310,8 +358,38 @@ const enterQuickCalc = (contractName: string) => {
tabStore.hasCompletedSetup = true
}
const openQuickCalc = () => {
window.alert(t('home.cards.developing'))
const openQuickCalc = async () => {
await runWithDisclaimerGuard({ type: 'quick' }, async () => {
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) => {
@ -325,7 +403,33 @@ const handleHomeImportChange = (event: Event) => {
}
const openHomeImport = () => {
void runWithDisclaimerGuard({ type: 'import' }, () => {
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 = () => {
@ -354,10 +458,85 @@ const handleHomeVisibilityChange = () => {
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(() => {
syncDisclaimerRequirement()
void refreshExistingProjects()
void loadProjectDefaults()
void loadQuickDefaults()
void replayPendingDisclaimerAction()
refreshHeroCopy()
window.addEventListener('focus', handleHomeWindowFocus)
document.addEventListener('visibilitychange', handleHomeVisibilityChange)
try {
@ -385,127 +564,109 @@ onBeforeUnmount(() => {
window.removeEventListener('focus', handleHomeWindowFocus)
document.removeEventListener('visibilitychange', handleHomeVisibilityChange)
})
watch(
() => locale.value,
() => {
refreshHeroCopy()
}
)
</script>
<template>
<input ref="homeImportInputRef" type="file" accept=".zw" class="sr-only" @change="handleHomeImportChange" />
<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-[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="home-entry relative flex min-h-screen items-center justify-center 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="relative w-full max-w-[1240px]">
<div class="absolute right-0 top-0 z-10">
<button
type="button"
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"
<Button
variant="outline"
size="sm"
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"
>
<Languages class="h-3.5 w-3.5" />
<span>{{ localeBadge }}</span>
</button>
</Button>
</div>
<div class="home-title text-center">
<div class="home-title text-center" :style="{ maxWidth: '800px' }">
<h1
class="text-2xl font-semibold tracking-tight text-slate-900 lg:text-3xl"
style="font-family: HarmonyOS_Sans_SC, 'Microsoft YaHei', sans-serif; font-weight: 300; white-space: pre-line;"
class="text-2xl tracking-tight text-slate-900 lg:text-3xl"
:style="{ whiteSpace: 'pre-line', fontWeight: '200', fontFamily: 'HarmonyOS_Sans_SC' }"
>
{{ t('home.title') }}
</h1>
<p class="mt-1.5 text-sm text-slate-500">{{ t('home.subtitle') }}</p>
</div>
<div class="mt-5 grid items-stretch gap-4 md:grid-cols-2 xl:grid-cols-4">
<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="mt-5 grid items-stretch gap-4 md:grid-cols-2 xl:grid-cols-5">
<div
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)]"
>
<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 -bottom-10 -left-10 h-40 w-40 rounded-full bg-white/8 blur-3xl" />
<div class="pointer-events-none absolute -left-10 -bottom-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 class="relative inline-flex h-11 w-11 items-center justify-center rounded-xl bg-white/15 ring-1 ring-white/35">
<BarChart3 class="h-5 w-5" />
</div>
<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>
<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/80">{{ t('home.cards.heroDesc') }}</p>
</section>
<article
role="button"
tabindex="0"
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"
@click="openProjectCalc"
@keydown.enter.prevent="openProjectCalc"
@keydown.space.prevent="openProjectCalc"
>
<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">
<BarChart3 class="h-5 w-5" />
</div>
<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"
>
{{ t('home.cards.pickExisting') }}
</button>
<button
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"
@click.stop="openProjectCalc"
>
<span class="flex items-center gap-1">
{{ t('home.cards.enter') }}
<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>
</span>
</button>
</div>
</article>
<article
role="button"
tabindex="0"
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"
@click="openQuickCalc"
@keydown.enter.prevent="openQuickCalc"
@keydown.space.prevent="openQuickCalc"
>
<div>
<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">
<Calculator class="h-5 w-5" />
</div>
<h3 class="mt-4 text-base font-semibold text-slate-900">{{ t('home.cards.quickCalc') }}</h3>
<p class="mt-1.5 text-xs leading-5 text-slate-500">{{ t('home.cards.quickCalcDesc') }}</p>
<h2 class="relative mt-8 text-2xl font-semibold leading-tight tracking-tight lg:text-3xl" :style="{ whiteSpace: 'pre-line', fontSize: '20px' }">{{ heroTitleText }}</h2>
<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" />
<p class="relative mt-4 whitespace-pre-line text-xs leading-5 text-red-200/60">{{ heroDescText }}</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
v-for="(card, index) in homeActionCards"
:key="card.key"
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"
:class="[
'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',
`home-entry-item--${index + 2}`
]"
@click="card.clickFunc"
@keydown.enter.prevent="card.clickFunc"
@keydown.space.prevent="card.clickFunc"
>
<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">
<div
:class="[
'inline-flex h-11 w-11 items-center justify-center rounded-xl border shadow-sm transition-transform duration-200 group-hover:scale-105',
card.iconWrapClass
]"
>
<svg v-if="card.icon === 'project'" viewBox="0 0 1024 1024" :class="card.iconClass" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path
fill="currentColor"
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"
/>
</svg>
<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
fill="currentColor"
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"
transform="translate(0,800) scale(0.1,-0.1)"
/>
<path
fill="currentColor"
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)"
/>
<path
fill="currentColor"
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"
transform="translate(0,800) scale(0.1,-0.1)"
/>
</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">
<path
fill="currentColor"
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"
/>
</svg>
<svg
v-else
viewBox="0 0 1024 1024"
class="h-6 w-6"
:class="card.iconClass"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
@ -515,15 +676,37 @@ onBeforeUnmount(() => {
/>
</svg>
</div>
<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.relatedFilesDesc') }}</p>
<h3 class="mt-4 text-base font-semibold text-slate-900">{{ t(`home.cards.${card.key}`) }}</h3>
<p class="mt-1.5 text-xs leading-5 text-slate-500">{{ t(`home.cards.${card.desc}`) }}</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.openRelatedFiles') }}</span>
<div class="mt-4 flex items-center justify-between gap-2">
<button
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>
</div>
</div>
</article>
</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>
@ -532,22 +715,18 @@ onBeforeUnmount(() => {
class="fixed inset-0 z-[90] flex items-center justify-center bg-black/45 p-4"
@click.self="closeExistingProjectDialog"
>
<div class="w-full max-w-lg rounded-3xl border border-slate-200/60 bg-white shadow-2xl">
<div class="flex items-start justify-between border-b border-slate-100 px-6 pt-6 pb-4">
<div class="w-full max-w-lg rounded-xl border bg-background shadow-2xl">
<div class="flex items-center justify-between border-b px-5 py-4">
<div>
<h3 class="text-xl font-bold text-[#1a1a1a]">{{ t('home.dialog.chooseExistingProject') }}</h3>
<p class="mt-1.5 text-base text-[#666]">{{ t('home.dialog.chooseExistingProjectDesc') }}</p>
<h3 class="text-base font-semibold text-foreground">{{ t('home.dialog.chooseExistingProject') }}</h3>
<p class="mt-1 text-sm text-muted-foreground">{{ t('home.dialog.chooseExistingProjectDesc') }}</p>
</div>
<button
type="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>
<Button variant="ghost" size="icon" class="h-8 w-8" @click="closeExistingProjectDialog">
<X class="h-4 w-4" />
</Button>
</div>
<div class="max-h-80 space-y-3 overflow-auto px-6 py-5">
<div class="max-h-80 space-y-2 overflow-auto px-5 py-4">
<div
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"
@ -559,29 +738,29 @@ onBeforeUnmount(() => {
:key="project.id"
type="button"
:disabled="isExistingProjectOpened(project.id)"
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="flex w-full items-center justify-between rounded-lg border border-slate-200 px-3 py-2 text-left transition"
: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)"
>
<div class="min-w-0 flex-1">
<div class="text-base font-medium text-[#1a1a1a]">
<div class="min-w-0">
<div class="truncate text-sm font-medium text-slate-800">
{{ project.name }}
<span v-if="isExistingProjectOpened(project.id)" class="ml-1 text-xs text-slate-500">
{{ t('tab.toolbar.opened') }}
</span>
</div>
<div class="mt-1 text-sm text-[#888]">{{ project.id }}</div>
<div class="mt-0.5 text-xs text-slate-500">{{ project.id }}</div>
</div>
<div class="shrink-0 pl-4 text-sm text-[#888]">
<div class="shrink-0 pl-2 text-xs text-slate-500">
{{ t('tab.toolbar.lastEdited', { time: formatProjectEditedTime(project.updatedAt) }) }}
</div>
</button>
</div>
<div class="flex items-center justify-end gap-3 border-t border-slate-100 px-6 pt-4 pb-6">
<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 class="flex items-center justify-end gap-2 border-t px-5 py-4">
<Button variant="outline" @click="closeExistingProjectDialog">{{ t('common.cancel') }}</Button>
</div>
</div>
</div>
@ -591,67 +770,61 @@ onBeforeUnmount(() => {
class="fixed inset-0 z-[90] flex items-center justify-center bg-black/45 p-4"
@click.self="closeProjectCalcDialog"
>
<div class="w-full max-w-lg rounded-3xl border border-slate-200/60 bg-white shadow-2xl">
<div class="flex items-start justify-between border-b border-slate-100 px-6 pt-6 pb-4">
<div class="w-full max-w-md rounded-xl border bg-background shadow-2xl">
<div class="flex items-center justify-between border-b px-5 py-4">
<div>
<h3 class="text-xl font-bold text-[#1a1a1a]">{{ t('home.dialog.newProject') }}</h3>
<p class="mt-1.5 text-base text-[#666]">选择工程行业后进入项目计算页面</p>
<h3 class="text-base font-semibold text-foreground">{{ t('home.dialog.newProject') }}</h3>
<p class="mt-1 text-sm text-muted-foreground">{{ t('home.dialog.chooseIndustryDesc') }}</p>
</div>
<button
type="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>
<Button variant="ghost" size="icon" class="h-8 w-8" @click="closeProjectCalcDialog">
<X class="h-4 w-4" />
</Button>
</div>
<div class="px-6 pt-5 pb-4">
<h4 class="mb-3 text-base font-semibold text-[#1a1a1a]">{{ t('home.dialog.industry') }}</h4>
<div class="space-y-3">
<button
<div class="space-y-4 px-5 py-4">
<label class="block space-y-2">
<span class="text-sm font-medium text-foreground">{{ t('home.dialog.industry') }}</span>
<SelectRoot v-model="projectIndustry">
<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"
:key="`project-${item.id}`"
type="button"
: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)"
:value="String(item.id)"
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"
>
<span class="text-base">{{ getIndustryDisplayName(item.id, locale) }}</span>
<div
:class="[
'flex h-5 w-5 items-center justify-center rounded-md border transition',
projectIndustry === String(item.id)
? 'border-[#1a1a1a] bg-[#1a1a1a] text-white'
: 'border-slate-300 bg-white'
]"
>
<Check v-if="projectIndustry === String(item.id)" class="h-3.5 w-3.5" />
</div>
</button>
</div>
<SelectItemText>{{ getIndustryDisplayName(item.id, locale) }}</SelectItemText>
<SelectItemIndicator class="absolute right-2 inline-flex items-center text-slate-700">
<Check class="h-4 w-4" />
</SelectItemIndicator>
</SelectItem>
</SelectViewport>
</SelectContent>
</SelectPortal>
</SelectRoot>
</label>
</div>
<div class="flex items-center justify-end gap-3 border-t border-slate-100 px-6 pt-4 pb-6">
<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="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"
>
<div class="flex items-center justify-end gap-2 border-t px-5 py-4">
<Button variant="outline" @click="closeProjectCalcDialog">{{ t('common.cancel') }}</Button>
<Button :disabled="projectSubmitting || !projectIndustry" @click="confirmProjectCalc">
{{ projectSubmitting ? t('home.dialog.entering') : t('home.dialog.enterProjectCalc') }}
</button>
</Button>
</div>
</div>
</div>
@ -697,11 +870,17 @@ onBeforeUnmount(() => {
.home-entry-item {
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--2 { animation-delay: 0.3s; }
.home-entry-item--3 { animation-delay: 0.4s; }
.home-entry-item--4 { animation-delay: 0.5s; }
.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 {
from { opacity: 0; transform: translateX(-20px) scale(0.97); }

View File

@ -17,6 +17,7 @@ import { getIndustryMajorEntry } from '@/lib/pricingScaleCalc'
import { getBenchmarkBudgetSplitByScale, getScaleBudgetFeeSplit } from '@/lib/pricingScaleFee'
import { QUICK_PROJECT_INFO_KEY } from '@/lib/workspace'
import { initializeProjectFactorStates } from '@/lib/projectWorkspace'
import { resolveServicePricingCapabilities } from '@/lib/servicePricing'
const props = defineProps<{
contractId: string
@ -36,8 +37,9 @@ type DictFactorItem = {
defCoe: number | null
hasCost?: boolean | null
hasArea?: boolean | null
scale?: boolean | null
onlyCostScale?: boolean | null
enableInvestScale?: boolean | null
enableLandScale?: boolean | null
investScaleSingleTotal?: boolean | null
}
type QuickCalcScaleMode = 'cost' | 'area'
@ -69,8 +71,9 @@ const mapDictItemToFactorItem = (id: string, item: Record<string, unknown> | und
defCoe: typeof item.defCoe === 'number' ? item.defCoe : null,
hasCost: item.hasCost === true,
hasArea: item.hasArea === true,
scale: item.scale === true,
onlyCostScale: item.onlyCostScale === true
enableInvestScale: item.enableInvestScale === true,
enableLandScale: item.enableLandScale === true,
investScaleSingleTotal: item.investScaleSingleTotal === true
}
}
@ -195,10 +198,22 @@ const preferLandScaleForDualMajor = computed(() => majorSupportsCostScale.value
const workEnvCoefficient = computed(() =>
parseNumberOrNull(workEnvFactor.value, { sanitize: true, precision: 3 })
)
const consultSupportsScale = computed(() => selectedConsultDictItem.value?.scale === true)
const consultOnlySupportsCostScale = computed(() => selectedConsultDictItem.value?.onlyCostScale === true)
const consultPricingCapabilities = computed(() =>
resolveServicePricingCapabilities(selectedConsultDictItem.value, {
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(() =>
consultSupportsScale.value &&
consultPricingCapabilities.value.investScaleEnabled &&
hasResolvedMajor.value &&
(
consultOnlySupportsCostScale.value ||
@ -206,9 +221,8 @@ const canUseInvestScale = computed(() =>
)
)
const canUseLandScale = computed(() =>
consultSupportsScale.value &&
consultPricingCapabilities.value.landScaleEnabled &&
hasResolvedMajor.value &&
!consultOnlySupportsCostScale.value &&
majorSupportsLandScale.value
)
const investScalePlaceholder = computed(() => {
@ -693,8 +707,8 @@ watch(canUseLandScale, enabled => {
.quick-calc-layout {
display: grid;
grid-template-columns: minmax(0, 1.45fr) minmax(340px, 0.95fr);
gap: 10px;
grid-template-columns: minmax(0, 1.02fr) minmax(420px, 1.34fr);
gap: 12px;
height: 100%;
min-height: 0;
}
@ -710,6 +724,34 @@ watch(canUseLandScale, enabled => {
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 {
display: flex;
align-items: flex-start;
@ -1152,7 +1194,7 @@ watch(canUseLandScale, enabled => {
.quick-calc-panel--form .quick-calc-form {
overflow: auto;
padding: 8px;
padding: 10px;
font-size: 14px;
}
@ -1179,8 +1221,11 @@ watch(canUseLandScale, enabled => {
.quick-calc-panel--form .quick-calc-form-section {
gap: 6px;
padding: 8px 10px;
padding: 10px 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 {
@ -1321,7 +1366,11 @@ watch(canUseLandScale, enabled => {
}
.quick-calc-panel--form .quick-calc-field__readonly--emphasis {
font-size: 14px;
font-size: 18px;
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 {

View File

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

View File

@ -53,6 +53,10 @@ const { t, locale } = useI18n()
const DEFAULT_PROJECT_NAME = t('xmInfo.defaultProjectName')
const DEFAULT_DESC = t('xmInfo.defaultDesc')
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 now = new Date()
const year = String(now.getFullYear())
@ -231,7 +235,21 @@ onMounted(async () => {
<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="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>
<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
v-model="projectName"
type="text"
@ -250,9 +268,9 @@ onMounted(async () => {
<button
type="button"
:aria-label="t('xmInfo.industryHintAria')"
class="inline-flex h-5 w-5 items-center justify-center text-muted-foreground/90 transition hover:text-foreground"
:class="FIELD_HINT_BUTTON_CLASS"
>
<CircleHelp class="h-5 w-5" />
<CircleHelp :class="FIELD_HINT_ICON_CLASS" />
</button>
</TooltipTrigger>
<TooltipContent side="top">{{ INDUSTRY_HINT_TEXT }}</TooltipContent>
@ -427,10 +445,25 @@ onMounted(async () => {
<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>
<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
v-model="desc"
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"
/>
</div>

View File

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

View File

@ -14,32 +14,49 @@ export const enUS = {
countdown: 'This page will try to close automatically in {seconds} seconds. You can open another project in a new tab first.',
opened: '(Opened)',
lastEdited: 'Last edited: {time}',
openDefault: 'Open Default Project',
openDefault: 'Back to Home',
createAndOpen: 'Create and Open'
}
},
home: {
title: 'Engineering Calculation Entry',
subtitle: 'Project Budget · Quick Calc · Import Data',
title: 'Yue Gong Xue Biao Zi [2026] No. 5\nSpecification for Budget Preparation of Cost Consulting Services for Transportation Engineering',
subtitle: 'Project Budget · Quick Calc · Import Data · Related Files',
projectCalcTab: 'Project Calculation',
quickCalcTab: 'Quick Calculation',
cards: {
heroTitle: 'One-Click Smart Budget',
heroSubTitle: 'Accelerate standards adoption',
heroDesc: 'Cost consulting fee calculator for transport construction projects',
heroTitle: 'ZonghuiyiBilling Made Simple',
heroTitles: [
'Zonghuiyi | Billing Made Simple',
'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',
projectBudgetDesc: 'For full project-level calculation across multiple contracts with import/export support',
quickCalc: 'Quick Calc',
quickCalcDesc: 'Suitable for single service trial, select industry, consultation type, engineering specialty, input base number and get results in seconds',
importData: 'Import Data',
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',
developing: 'In Development',
pickFile: 'Choose File',
pickExisting: 'Choose Existing',
relatedFiles: 'Related Files',
relatedFilesDesc: 'View related fee documents, tender files, contract files, service content, and work requirements',
openRelatedFiles: 'Open Page'
openFileSystem: 'Open File System',
file: 'File System',
fileDataDesc: 'The file system provides related fee documents, bidding documents, contract documents, service contents, and work requirements'
},
disclaimer: {
link: 'View Disclaimer',
supportText: 'This calculator is provided with free technical support by Zhongwei Engineering Consulting Co., Ltd.'
},
dialog: {
newProject: 'New Project',
@ -56,6 +73,47 @@ export const enUS = {
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: {
toolbar: {
light: 'Light',
@ -169,9 +227,7 @@ export const enUS = {
toast: {
export: 'Export Report',
success: 'Export Success',
failed: 'Export Failed',
saveSuccess: 'Save Success',
saveFailed: 'Save Failed'
failed: 'Export Failed'
},
messages: {
defaultProjectLabel: 'Default Project',
@ -305,7 +361,9 @@ export const enUS = {
reserveTitle: 'Reserve Fee'
},
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: {
baseLabel: 'Base (total budget of all service fees)',
@ -320,7 +378,7 @@ export const enUS = {
title: 'Consulting Service Details',
warning: 'Please review and adjust recommended limits/special values in the specification, then update final fee if needed.',
editTabTitle: 'Service Edit-{name}',
subtotal: 'Subtotal',
subtotal: 'Total',
edit: 'Edit',
resetDefault: 'Reset',
delete: 'Remove',
@ -370,7 +428,7 @@ export const enUS = {
}
},
htFeeGrid: {
subtotal: 'Subtotal',
subtotal: 'Total',
currentRow: 'Current Row',
unnamed: 'Unnamed',
edit: 'Edit',
@ -402,6 +460,8 @@ export const enUS = {
},
serviceSelector: {
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',
empty: 'No services'
},
@ -437,7 +497,7 @@ export const enUS = {
}
},
htFeeDetail: {
subtotal: 'Subtotal',
subtotal: 'Total',
currentRow: 'Current Row',
clickToInput: 'Click to input',
addRow: 'Add Row',
@ -576,7 +636,7 @@ export const enUS = {
pricingScale: {
totalInvestmentByIndustry: '{industryName} Total Investment',
totalInvestment: 'Total Investment',
clickToInput: 'Optional, enter manually, numeric, 4 decimals',
clickToInput: 'Click to input',
projectLabel: 'Project {index}',
columns: {
investAmount: 'Cost Amount (10k CNY)',
@ -589,7 +649,7 @@ export const enUS = {
consultCategoryFactor: 'Consult Category Factor',
majorFactor: 'Major Factor',
workStageFactor: 'Work Stage Factor (Draft/Review)',
workRatio: 'Work Ratio (%)',
workRatio: 'Service Budget Composition Ratio and Quantity Ratio',
total: 'Total',
remark: 'Remark',
majorGroup: 'Major Code and Major Name'
@ -598,7 +658,8 @@ export const enUS = {
resetInvestAmount: 'Click ↻ to restore default cost amount for this column',
resetLandArea: 'Click ↻ to restore default land area 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: {
@ -610,6 +671,8 @@ export const enUS = {
confirmOverride: 'Confirm Override',
investment: {
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?',
overrideDesc: 'Use contract default data to override current investment scale details. Continue?'
},
@ -644,16 +707,14 @@ export const enUS = {
total: 'Grand Total',
columns: {
code: 'Code',
name: 'Name',
technician: 'Technician',
assistantEngineer: 'Assistant Engineer',
midEngineer: 'Intermediate Engineer (or Level 2 Cost Engineer)',
seniorEngineer: 'Senior Engineer (or Level 1 Cost Engineer)',
profSeniorEngineer: 'Professor-level Senior Engineer',
unitPrice: 'Unit Price (CNY/workday)',
name: 'Personnel Name',
referenceUnitPrice: 'Budget Reference Unit Price',
laborBudgetUnitPrice: 'Labor Budget Unit Price (CNY/workday)',
compositeBudgetUnitPrice: 'Composite Budget Unit Price (CNY/workday)',
adoptedBudgetUnitPrice: 'Adopted Budget Unit Price (CNY/workday)',
personnelCount: 'Personnel Count',
workdayCount: 'Workday Count',
subtotal: 'Subtotal (CNY)',
avgUnitPrice: 'Average Unit Price (CNY/workday)',
serviceBudget: 'Service Budget (CNY)',
remark: 'Remark'
}
},
@ -666,6 +727,10 @@ 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.',
industryHint: 'Changing industry requires reset and re-selection',
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.',
fields: {
projectName: 'Project Name',
@ -679,9 +744,10 @@ export const enUS = {
},
placeholders: {
overview: 'Enter project overview',
preparedBy: 'Enter preparer',
reviewedBy: 'Enter reviewer',
preparedCompany: 'Enter prepared company'
desc: 'Other Notes',
preparedBy: 'XXX',
reviewedBy: 'XXX',
preparedCompany: 'XXX'
}
}
} as const

View File

@ -14,32 +14,49 @@ export const zhCN = {
countdown: '本页将在 {seconds} 秒后自动尝试关闭。你也可以先在新标签页打开其他项目。',
opened: '(已打开)',
lastEdited: '最后编辑:{time}',
openDefault: '打开默认项目',
openDefault: '返回首页',
createAndOpen: '新建项目并打开'
}
},
home: {
title: '粤价函2008929号\n《广东省交通工程造价技术中介服务收费项目及标准表》',
subtitle: '项目计算 · 单项速算 · 导入数据',
title: '粤公学标字20265号\n交通运输工程造价咨询服务预算编制规范',
subtitle: '项目计算 · 单项速算 · 导入数据 · 相关文件',
projectCalcTab: '项目计算',
quickCalcTab: '快速计算',
cards: {
heroTitle: '智能预算一键生成',
heroSubTitle: '助力《规范》高效落地',
heroDesc: '交通建设项目工程造价咨询服务费计算',
heroTitle: '众会易|算费真容易',
heroTitles: [
'众会易 | 算费真容易',
'众会易 | 算费不熬夜',
'众会易 | 算费不费力'
],
heroSubTitle: '',
heroDesc: '智算费用 即点即出 您的时间留给创造',
heroDescs: [
'智算费用 即点即出 您的时间留给创造。',
'智算费用 一键即出 您的时间留给创造。',
'智算费用 一键生成 您的时间留给创造。',
'众会易 真容易 不熬夜 不费力'
],
projectBudget: '项目预算',
projectBudgetDesc: '适用于多合同段、项目级整体计算,支持导出/导入完整项目数据',
quickCalc: '单项速算',
quickCalcDesc: '适用于单项服务试算,选择行业、咨询类型、工程专业,输入基数秒出结果',
importData: '导入数据',
importDataDesc: '导入".zw"数据包,并自动新建一个项目用于恢复数据,不会覆盖现有项目',
relatedFiles: '相关文件',
relatedFilesDesc: '在线查看、打印和下载与该计算器依据的收费文件、相关招标文件与合同文件范本',
viewFiles: '查看文件',
enter: '进入计算',
developing: '正在开发',
pickFile: '选择文件',
pickExisting: '选择已有项目',
relatedFiles: '相关文件',
relatedFilesDesc: '可查看相关收费文件、招标文件、合同文件、服务内容、工作要求',
openRelatedFiles: '打开页面'
openFileSystem: '打开文件系统',
file: '文件系统',
fileDataDesc: '文件系统可查看相关收费文件、招标文件、合同文件、服务内容、工作要求'
},
disclaimer: {
link: '查看免责声明',
supportText: '本计算工具由众为工程咨询有限公司提供免费技术支持'
},
dialog: {
newProject: '新建项目',
@ -56,6 +73,47 @@ export const zhCN = {
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: {
toolbar: {
light: '浅色',
@ -69,7 +127,7 @@ export const zhCN = {
reset: '重置',
resetting: '重置中...',
projectList: '项目列表',
projectCount: '项目数量:{count}/{max}',
projectCount: '项目数量:{count}',
createProject: '新建项目',
backHome: '返回入口',
resetAll: '清除全部项目',
@ -169,9 +227,7 @@ export const zhCN = {
toast: {
export: '导出报表',
success: '导出成功',
failed: '导出失败',
saveSuccess: '保存成功',
saveFailed: '保存失败'
failed: '导出失败'
},
messages: {
defaultProjectLabel: '默认项目',
@ -196,7 +252,7 @@ export const zhCN = {
copied: '已复制',
copyFailed: '复制失败',
brandAlt: '众为咨询',
supportText: '本网站由众为工程咨询有限公司提供免费技术支持',
supportText: '本计算工具由众为工程咨询有限公司提供免费技术支持',
aboutTitle: '关于我们',
companyName: '众为工程咨询有限公司',
openOfficialSiteAria: '跳转到官网首页',
@ -278,9 +334,9 @@ export const zhCN = {
metaBudget: '合同段预算金额:{amount}',
currencySuffix: '元',
categories: {
baseInfo: '合同基础信息',
scaleInfo: '合同规模',
services: '合同费用汇总表',
baseInfo: '基础信息',
scaleInfo: '规模信息',
services: '咨询服务',
consultFactor: '咨询分类系数',
majorFactor: '工程专业系数',
additionalFee: '附加工作费',
@ -305,7 +361,9 @@ export const zhCN = {
reserveTitle: '预备费'
},
htInfo: {
scaleDetailTitle: '合同规模明细'
scaleDetailTitle: '合同规模明细',
scaleDetailHint: '当本表规模与项目规模数据不一致时,计费以本表规模为准',
scaleDetailHintAria: '合同规模明细提示'
},
htFeeRate: {
baseLabel: '基数(所有服务费预算合计)',
@ -320,7 +378,7 @@ export const zhCN = {
title: '咨询服务明细',
warning: '※ 请注意检查并修改《规范》建议的限值或特殊值,并在确认金额栏修改',
editTabTitle: '服务编辑-{name}',
subtotal: '计',
subtotal: '计',
edit: '编辑',
resetDefault: '恢复默认',
delete: '删除',
@ -350,7 +408,7 @@ export const zhCN = {
},
htSummary: {
title: '合同段汇总',
total: '计',
total: '计',
remark: '说明',
placeholder: '请先填咨询服务/附加工作费/预备费的数据',
additionalPrefix: '附加工作费',
@ -370,7 +428,7 @@ export const zhCN = {
}
},
htFeeGrid: {
subtotal: '计',
subtotal: '计',
currentRow: '当前行',
unnamed: '未命名',
edit: '编辑',
@ -402,6 +460,8 @@ export const zhCN = {
},
serviceSelector: {
title: '选择服务',
titleHint: '本选择项较《规范》列明的服务项有所增加。此增加与《规范》无冲突,只为满足《规范》费用计算之需',
titleHintAria: '选择服务提示',
clear: '清空',
empty: '暂无服务'
},
@ -417,8 +477,7 @@ export const zhCN = {
landScaleFormula: '用地规模法计算公式',
workload: '工作量法',
hourly: '工时法',
workContent: '工作内容',
otherService: '其他服务计算'
workContent: '工作内容'
},
formulaColumns: {
subtitle: '直接展示当前计价法 store 的最新明细,随数据变更自动同步。',
@ -438,7 +497,7 @@ export const zhCN = {
}
},
htFeeDetail: {
subtotal: '计',
subtotal: '计',
currentRow: '当前行',
clickToInput: '点击输入',
addRow: '添加行',
@ -559,8 +618,8 @@ export const zhCN = {
},
xmCard: {
categories: {
info: '项目基础信息',
scaleInfo: '项目规模',
info: '基础信息',
scaleInfo: '规模信息',
consultCategoryFactor: '咨询分类系数',
majorFactor: '工程专业系数',
contract: '合同段管理'
@ -576,12 +635,11 @@ export const zhCN = {
pricingScale: {
totalInvestmentByIndustry: '{industryName}总投资',
totalInvestment: '总投资',
clickToInput: '非必填手动录入数字4位',
clickToInput: '点击输入',
projectLabel: '项目{index}',
columns: {
code: '编码',
investAmount: '造价金额(万元)',
landArea: '造价金额(元)',
landArea: '用地面积(亩)',
benchmarkBudget: '基准预算(元)',
basicWork: '基本工作',
optionalWork: '可选工作',
@ -589,25 +647,18 @@ export const zhCN = {
budgetFee: '预算费用',
consultCategoryFactor: '咨询分类系数',
majorFactor: '专业系数',
workStageFactor: '工作环节系数',
workRatio: '工作占比',
workStageFactor: '工作环节系数(编审系数)',
workRatio: '服务预算构成比率与数量比',
total: '合计',
remark: '说明',
majorGroup: '项目明细费用',
name: '名称',
number: '编码',
base: '计算基础',
base2: '计算基数F万元',
formula: '计算公式',
calculationAmount: '计算金额(元)',
calculationGroup: '计算公式',
serviceFee: '服务费用(元)'
majorGroup: '专业编码以及工程专业名称'
},
tooltip: {
resetInvestAmount: '点击右侧↻恢复本列默认造价金额',
resetLandArea: '点击右侧↻恢复本列默认用地面积',
resetConsultCategoryFactor: '点击右侧↻恢复本列默认咨询分类系数',
resetMajorFactor: '点击右侧↻恢复本列默认专业系数'
resetInvestAmount: '点击右侧↻本列造价金额数值恢复为本合同规模数值',
resetLandArea: '点击右侧↻本列用地面积数值恢复为本合同规模数值',
resetConsultCategoryFactor: '点击右侧↻本列咨询分类系数值恢复为本项目的相应的咨询分类系数值',
resetMajorFactor: '点击右侧↻本列专业系数值恢复为本项目的相应的专业系数值',
workRatio: '本列系数适用于以下两种情形服务预算构成比率适用于《规范》附录D表D.2~D.7中委托工作内容仅为部分工作时的情形;数量比,适用于计算基数按每份成果、分批次任务、单项工程或单位工程的规模(非总额)的情形,数量表示有多个个、份或项'
}
},
pricingPane: {
@ -619,6 +670,8 @@ export const zhCN = {
confirmOverride: '确认覆盖',
investment: {
title: '投资规模明细',
titleHint: '本表造价金额取值规则:以本合同规模表与本表造价金额中操作时间较晚的值为准',
titleHintAria: '投资规模明细提示',
clearDesc: '将清空当前投资规模明细,是否继续?',
overrideDesc: '将使用合同默认数据覆盖当前投资规模明细,是否继续?'
},
@ -634,78 +687,34 @@ export const zhCN = {
unavailableMessage: '当前服务没有关联工作量法任务,无需填写此部分内容。',
clickToInput: '点击输入',
none: '无',
total: '总计',
total: '总计',
columns: {
code: '编码',
name: '名称',
budgetBase: '计算基础',
budgetReferenceUnitPrice: '计算基数(份)',
budgetAdoptedUnitPrice: '最低单价(万元/份)',
workload: '中值单价(万元/份)',
consultCategoryFactor: '最高单价(万元/份)',
cLow: '计算最低值(元)',
cMid: '计算中值(元)',
cHigh: '计算最高值(元)',
serviceFee: '本计算取值(元)',
budgetBase: '预算基数',
budgetReferenceUnitPrice: '预算参考单价',
budgetAdoptedUnitPrice: '预算采用单价',
workload: '工作量',
consultCategoryFactor: '咨询分类系数',
serviceFee: '服务费用(元)',
remark: '说明'
}
},
hourlyFeeGrid: {
title: '工时法明细',
clickToInput: '点击输入',
total: '总计',
total: '总计',
columns: {
code: '编码',
name: '名称',
technician: '技术员',
assistantEngineer: '助理工程师',
midEngineer: '中级工程师(或二级造价工程师)',
seniorEngineer: '高级工程师(或一级造价工程师)',
profSeniorEngineer: '正高级工程师',
unitPrice: '单价(元/工日)',
workdayCount: '工日数量(工日)',
subtotal: '费用小计(元)',
unitPrice2: '单价(元/工日)',
workdayCount2: '工日数量(工日)',
subtotal2: '费用小计(元)',
unitPrice3: '单价(元/工日)',
workdayCount3: '工日数量(工日)',
subtotal3: '费用小计(元)',
unitPrice4: '单价(元/工日)',
workdayCount4: '工日数量(工日)',
subtotal4: '费用小计(元)',
unitPrice5: '单价(元/工日)',
workdayCount5: '工日数量(工日)',
subtotal5: '费用小计(元)',
unitPrice6: '单价(元/工日)',
workdayCount6: '工日数量(工日)',
subtotal6: '费用小计(元)',
avgUnitPrice: '折算单价(元/工日)',
remark: '说明',
total: '合计',
referenceUnitPrice: '参考单价(元/工日)',
laborBudgetUnitPrice: '劳动预算单价(元/工日)',
name: '人员名称',
referenceUnitPrice: '预算参考单价',
laborBudgetUnitPrice: '人工预算单价(元/工日)',
compositeBudgetUnitPrice: '综合预算单价(元/工日)',
adoptedBudgetUnitPrice: '采用单价(元/工日)',
adoptedBudgetUnitPrice: '预算采用单价(元/工日)',
personnelCount: '人员数量(人)',
serviceBudget: '服务费用(元)',
}
},
otherService: {
title: '其他服务计算',
clickToInput: '点击输入',
total: '小计',
columns: {
num: '序号',
code: '编码',
name: '名称',
feeItem: '费用项',
unit: '单位',
quantity: '数量',
unitPrice: '单价',
serviceFee: '服务费用(元)',
remark: '说明',
actions: '操作'
workdayCount: '工日数量(工日)',
serviceBudget: '服务预算(元)',
remark: '说明'
}
},
xmScaleGrid: {
@ -717,6 +726,10 @@ export const zhCN = {
defaultDesc: '在履行造价咨询服务时宜根据咨询服务质量情况分级确定相应的处罚金额。其中考评得分在大于及等于85和小于90分时处罚金额为预算费用的10%其中考评得分在大于及等于80和小于85分时处罚金额为预算费用的20%其中考评得分在大于及等于75和小于80分时处罚金额为预算费用的30%其中考评得分在大于及等于70和小于75分时处罚金额为预算费用的40%其中考评得分小于70分时处罚金额为预算费用的50%以上。',
industryHint: '变更需要重置后重新选择',
industryHintAria: '工程行业提示',
reportContentHint: '本内容为选择性填写,填写内容仅用于自动生成编制报告内容',
reportContentHintAria: '说明内容提示',
otherDescHint: '本内容为选择性填写。当前显示内容仅为示意,编制人可根据实际情况填写,亦可不填。',
otherDescHintAria: '其他说明提示',
createFromHomeFirst: '请从首页先新建项目后再进入此页面。',
fields: {
projectName: '项目名称',
@ -730,9 +743,10 @@ export const zhCN = {
},
placeholders: {
overview: '请输入项目概况',
preparedBy: '请输入编制人',
reviewedBy: '请输入复核人',
preparedCompany: '请输入编制单位'
desc: '其他说明',
preparedBy: 'XXX',
reviewedBy: 'XXX',
preparedCompany: 'XXX'
}
}
} as const

File diff suppressed because it is too large Load Diff

View File

@ -96,10 +96,14 @@ const activeComponent = computed(() => {
const sideWidthStyle = computed(() => ({ width: 'var(--app-typeline-side-w)' }))
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 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 lineStyle = computed(() => ({ left: 'var(--app-typeline-line-left)' }))
const connectorStyle = computed(() => ({
height: 'var(--app-typeline-arrow-shaft-h)',
width: 'var(--app-typeline-arrow-line-w)'
}))
const copyBtnText = ref(t('typeLine.copy'))
const sheetOpen = ref(false)
@ -107,6 +111,34 @@ const sheetOpen = ref(false)
let copyBtnTimer: ReturnType<typeof setTimeout> | 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)
}
}
@ -118,8 +150,8 @@ const handleCopySubtitle = async () => {
if (!text) return
try {
await navigator.clipboard.writeText(text)
copyBtnText.value = t('typeLine.copied')
const copied = await copyTextWithFallback(text)
copyBtnText.value = copied ? t('typeLine.copied') : t('typeLine.copyFailed')
} catch (error) {
console.error('copy failed:', error)
copyBtnText.value = t('typeLine.copyFailed')
@ -256,24 +288,32 @@ useMotionValueEvent(
</div>
</div>
<div :class="['flex flex-col gap-6 relative ', (props.title || props.subtitle || props.metaText) ? 'mt-3' : 'mt-6']">
<div :style="lineStyle" class="absolute top-3 bottom-3 w-[1.5px] bg-border/60"></div>
<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="['flex flex-col gap-2 relative ', (props.title || props.subtitle || props.metaText) ? 'mt-3' : 'mt-6']">
<div v-for="(item, index) in props.categories" :key="item.key"
: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 :class="[
'z-10 rounded-full border-2 flex items-center justify-center transition-all duration-200',
'z-10 rounded-full border-2 flex shrink-0 items-center justify-center transition-all duration-200',
activeCategory === item.key
? 'bg-blue-600 border-blue-600'
: 'bg-background border-slate-300 group-hover:border-slate-400'
? 'bg-primary border-primary shadow-[0_0_0_3px_rgba(var(--primary),0.15)]'
: 'bg-background border-muted-foreground/40 group-hover:border-muted-foreground/70'
]" :style="dotStyle">
<div v-if="activeCategory === item.key" class="bg-white rounded-full" :style="dotInnerStyle"></div>
<div v-if="activeCategory === item.key" class="bg-background 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>
<span :class="[
'transition-colors duration-200',
'pt-px transition-colors duration-200',
activeCategory === item.key
? 'font-semibold text-blue-600'
: 'text-slate-500 group-hover:text-slate-700'
? 'font-semibold text-primary'
: 'text-muted-foreground group-hover:text-foreground'
]" :style="labelStyle">
{{ item.label }}
</span>
@ -391,4 +431,23 @@ useMotionValueEvent(
overflow: hidden;
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>

View File

@ -45,7 +45,7 @@ export class AgGridResetHeader implements IHeaderComp {
eButton.style.height = '18px'
eButton.style.border = '1px solid #d1d5db'
eButton.style.borderRadius = '999px'
eButton.style.background = '#fff'
eButton.style.background = '#edff87'
eButton.style.color = '#4b5563'
eButton.style.cursor = 'pointer'
eButton.style.fontSize = '12px'
@ -69,8 +69,38 @@ export class AgGridResetHeader implements IHeaderComp {
this.params = params
this.eLabel.textContent = params.displayName || params.column?.getColDef().headerName || ''
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.style.visibility = params.onReset ? 'visible' : 'hidden'
this.eButton.style.visibility = 'visible'
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
}

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import {
formatScaleReadonlyMoney,
getScaleMergeColSpanBeforeTotal
} from '@/lib/pricingScaleGrid'
import { AgGridResetHeader } from '@/lib/agGridResetHeader'
import { i18n } from '@/i18n'
type ScaleColumnField<TRow> = Extract<keyof TRow, string> | string
@ -199,6 +200,8 @@ export const createScaleBudgetFeeColumnGroup = <TRow>(options: {
headerName: scaleT('columns.workRatio'),
field: 'workRatio' as any,
colId: 'workRatio',
headerTooltip: scaleT('tooltip.workRatio'),
headerComponent: AgGridResetHeader,
headerClass: 'ag-right-aligned-header',
minWidth: 80,
flex: 1,
@ -258,11 +261,14 @@ export const createScaleAutoGroupColumn = <TRow>(options: {
idLabelMap: Map<string, string>
parseProjectIndexFromPathKey: (key: string) => number | null
}) : ColDef<TRow> => ({
headerName: scaleT('columns.number'),
headerName: scaleT('columns.majorGroup'),
minWidth: 250,
flex: 2,
wrapText: true,
autoHeight: true,
cellClassRules: {
'ag-summary-label-cell': params => Boolean(params.node?.rowPinned)
},
cellStyle: {
whiteSpace: 'normal',
lineHeight: '1.4'
@ -276,8 +282,8 @@ export const createScaleAutoGroupColumn = <TRow>(options: {
return options.totalLabel
}
const rowData = params.data as any
if (!params.node?.group && rowData?.majorCode) {
return rowData.majorCode
if (!params.node?.group && rowData?.majorCode && rowData?.majorName) {
return `${rowData.majorCode} ${rowData.majorName}`
}
const nodeId = String(params.value || '')
const projectIndex = options.parseProjectIndexFromPathKey(nodeId)
@ -287,8 +293,8 @@ export const createScaleAutoGroupColumn = <TRow>(options: {
tooltipValueGetter: params => {
if (params.node?.rowPinned) return options.totalLabel
const rowData = params.data as any
if (!params.node?.group && rowData?.majorCode) {
return rowData.majorCode
if (!params.node?.group && rowData?.majorCode && rowData?.majorName) {
return `${rowData.majorCode} ${rowData.majorName}`
}
const nodeId = String(params.value || '')
const projectIndex = options.parseProjectIndexFromPathKey(nodeId)
@ -296,92 +302,3 @@ export const createScaleAutoGroupColumn = <TRow>(options: {
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
* 2.
* 2.
* 3. / /
*
* `getBenchmarkBudgetSplitByScale`
@ -78,7 +78,7 @@ export const getScaleBudgetFeeSplit = (params: {
const majorFactor = toFiniteNumberOrNull(params.majorFactor)
const consultCategoryFactor = toFiniteNumberOrNull(params.consultCategoryFactor)
const workStageFactor = hasWorkStageFactor ? toFiniteNumberOrNull(params.workStageFactor) : 1
const workRatio = hasWorkRatio ? toFiniteNumberOrNull(params.workRatio) : 100
const workRatio = hasWorkRatio ? toFiniteNumberOrNull(params.workRatio) : 1
if (
benchmarkBudgetBasic == null ||
@ -95,7 +95,6 @@ export const getScaleBudgetFeeSplit = (params: {
.mul(majorFactor)
.mul(workStageFactor)
.mul(workRatio)
.div(100)
const roundedBenchmarkBudget = roundTo(addNumbers(benchmarkBudgetBasic, benchmarkBudgetOptional), 2)
const basic = roundTo(toDecimal(benchmarkBudgetBasic).mul(multiplier), 2)
const optional = roundTo(toDecimal(benchmarkBudgetOptional).mul(multiplier), 2)
@ -121,7 +120,7 @@ export const getScaleBudgetFee = (params: {
const majorFactor = toFiniteNumberOrNull(params.majorFactor)
const consultCategoryFactor = toFiniteNumberOrNull(params.consultCategoryFactor)
const workStageFactor = hasWorkStageFactor ? toFiniteNumberOrNull(params.workStageFactor) : 1
const workRatio = hasWorkRatio ? toFiniteNumberOrNull(params.workRatio) : 100
const workRatio = hasWorkRatio ? toFiniteNumberOrNull(params.workRatio) : 1
if (
benchmarkBudget == null ||
@ -138,6 +137,5 @@ export const getScaleBudgetFee = (params: {
.mul(majorFactor)
.mul(workStageFactor)
.mul(workRatio)
.div(100)
return roundTo(toDecimal(roundedBenchmarkBudget).mul(multiplier), 2)
}

View File

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

View File

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

63
src/lib/servicePricing.ts Normal file
View File

@ -0,0 +1,63 @@
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,9 +13,15 @@ export const PROJECT_ID_QUERY_KEY = 'projectId'
export const NEW_PROJECT_QUERY_KEY = 'newProject'
export const OPEN_PROJECT_DIALOG_QUERY_KEY = 'openProjectDialog'
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 QUICK_PROJECT_ID = 'quick'
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_META_KEY = 'quick-contract-meta-v1'
@ -42,6 +48,11 @@ export interface QuickContractMeta {
updatedAt: string
}
export interface DisclaimerPendingAction {
type: 'project' | 'quick' | 'import' | 'existing-project'
projectId?: string
}
export const readWorkspaceMode = (): WorkspaceMode => {
@ -185,6 +196,107 @@ 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) => {
const projectId = normalizeProjectId(projectIdRaw)
return `${PROJECT_DB_NAME_PREFIX}-${projectId}`

View File

@ -22,6 +22,15 @@ type 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 map = new Map<string, number | null>()
for (const [id, item] of Object.entries(dict)) {
@ -68,7 +77,12 @@ const loadFactorMap = async (
const zxFwPricingStore = getZxFwPricingStoreSafely()
const kvStore = getKvStoreSafely()
const piniaData = zxFwPricingStore ? await zxFwPricingStore.loadKeyState<XmFactorState>(storageKey) : null
const data = piniaData ?? (kvStore ? await kvStore.getItem<XmFactorState>(storageKey) : null)
const kvData = kvStore ? await kvStore.getItem<XmFactorState>(storageKey) : null
const data = hasUsableFactorRows(piniaData)
? piniaData
: hasUsableFactorRows(kvData)
? kvData
: (piniaData ?? kvData)
const map = buildStandardFactorMap(dict)
for (const row of data?.detailRows || []) {
if (!row?.id) continue

View File

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

View File

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

View File

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

19
src/router.ts Normal file
View File

@ -0,0 +1,19 @@
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,12 +57,17 @@ html {
--app-grid-row-h: 2.25rem;
--app-grid-font-size: 0.875rem;
--app-typeline-side-w: 12.5rem;
--app-typeline-gap: 0.75rem;
--app-typeline-gap: 0.625rem;
--app-typeline-label-font: 0.8125rem;
--app-typeline-label-line: 1rem;
--app-typeline-dot: 1.25rem;
--app-typeline-dot-inner: 0.375rem;
--app-typeline-dot: 1.125rem;
--app-typeline-dot-inner: 0.3125rem;
--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;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
@ -181,6 +186,23 @@ input[inputmode='numeric'] {
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. */
.ag-theme-quartz .ag-header-cell-label,
.ag-theme-quartz .ag-header-group-cell-label {
@ -418,12 +440,17 @@ input[inputmode='numeric'] {
--app-grid-row-h: 2.125rem;
--app-grid-font-size: 0.8125rem;
--app-typeline-side-w: 11.5rem;
--app-typeline-gap: 0.625rem;
--app-typeline-gap: 0.5625rem;
--app-typeline-label-font: 0.75rem;
--app-typeline-label-line: 0.95rem;
--app-typeline-dot: 1.125rem;
--app-typeline-dot-inner: 0.3125rem;
--app-typeline-dot: 1rem;
--app-typeline-dot-inner: 0.25rem;
--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;
}
}
@ -439,12 +466,17 @@ input[inputmode='numeric'] {
--app-grid-row-h: 2.25rem;
--app-grid-font-size: 0.875rem;
--app-typeline-side-w: 12rem;
--app-typeline-gap: 0.6875rem;
--app-typeline-gap: 0.59375rem;
--app-typeline-label-font: 0.8125rem;
--app-typeline-label-line: 1rem;
--app-typeline-dot: 1.1875rem;
--app-typeline-dot-inner: 0.375rem;
--app-typeline-dot: 1.0625rem;
--app-typeline-dot-inner: 0.3125rem;
--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;
}
}
@ -460,12 +492,17 @@ input[inputmode='numeric'] {
--app-grid-row-h: 2.25rem;
--app-grid-font-size: 0.875rem;
--app-typeline-side-w: 12.5rem;
--app-typeline-gap: 0.75rem;
--app-typeline-gap: 0.625rem;
--app-typeline-label-font: 0.875rem;
--app-typeline-label-line: 1.1rem;
--app-typeline-dot: 1.25rem;
--app-typeline-dot-inner: 0.4375rem;
--app-typeline-dot: 1.125rem;
--app-typeline-dot-inner: 0.375rem;
--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;
}
}
@ -481,12 +518,17 @@ input[inputmode='numeric'] {
--app-grid-row-h: 2.5rem;
--app-grid-font-size: 0.9375rem;
--app-typeline-side-w: 13rem;
--app-typeline-gap: 0.8125rem;
--app-typeline-gap: 0.6875rem;
--app-typeline-label-font: 0.9375rem;
--app-typeline-label-line: 1.2rem;
--app-typeline-dot: 1.375rem;
--app-typeline-dot-inner: 0.5rem;
--app-typeline-dot: 1.25rem;
--app-typeline-dot-inner: 0.4375rem;
--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;
}
}
@ -502,12 +544,17 @@ input[inputmode='numeric'] {
--app-grid-row-h: 2.625rem;
--app-grid-font-size: 1rem;
--app-typeline-side-w: 13.5rem;
--app-typeline-gap: 0.875rem;
--app-typeline-gap: 0.75rem;
--app-typeline-label-font: 1rem;
--app-typeline-label-line: 1.25rem;
--app-typeline-dot: 1.5rem;
--app-typeline-dot-inner: 0.5625rem;
--app-typeline-dot: 1.375rem;
--app-typeline-dot-inner: 0.5rem;
--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;
}
}
@ -523,11 +570,16 @@ input[inputmode='numeric'] {
--app-grid-row-h: 2.875rem;
--app-grid-font-size: 1.0625rem;
--app-typeline-side-w: 14.5rem;
--app-typeline-gap: 1rem;
--app-typeline-gap: 0.875rem;
--app-typeline-label-font: 1.0625rem;
--app-typeline-label-line: 1.35rem;
--app-typeline-dot: 1.625rem;
--app-typeline-dot-inner: 0.625rem;
--app-typeline-dot: 1.5rem;
--app-typeline-dot-inner: 0.5625rem;
--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,8 +213,15 @@ export interface MajorLite {
/** 咨询服务字典精简信息 */
export interface ServiceLite {
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
mutiple?: boolean | null
amount?: boolean | null
workDay?: boolean | null
}
/** 工作量法任务字典 */

View File

@ -1 +1 @@
{"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"}
{"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"}