mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-21 18:19:03 +08:00
@@ -6,6 +6,7 @@ out
|
|||||||
*.log*
|
*.log*
|
||||||
logs
|
logs
|
||||||
docs
|
docs
|
||||||
|
configs
|
||||||
|
|
||||||
build/*
|
build/*
|
||||||
!build/entitlements.mac.plist
|
!build/entitlements.mac.plist
|
||||||
|
|||||||
@@ -28,8 +28,10 @@
|
|||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"mica-electron": "^1.5.16",
|
"mica-electron": "^1.5.16",
|
||||||
|
"os": "^0.1.2",
|
||||||
"pinyin-pro": "^3.27.0",
|
"pinyin-pro": "^3.27.0",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
|
"react-syntax-highlighter": "^16.1.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"tdesign-react": "^1.16.3",
|
"tdesign-react": "^1.16.3",
|
||||||
"typeorm": "^0.3.27",
|
"typeorm": "^0.3.27",
|
||||||
|
|||||||
Generated
+191
@@ -26,12 +26,16 @@ importers:
|
|||||||
mica-electron:
|
mica-electron:
|
||||||
specifier: ^1.5.16
|
specifier: ^1.5.16
|
||||||
version: 1.5.16
|
version: 1.5.16
|
||||||
|
os: {specifier: ^0.1.2, version: 0.1.2}
|
||||||
pinyin-pro:
|
pinyin-pro:
|
||||||
specifier: ^3.27.0
|
specifier: ^3.27.0
|
||||||
version: 3.27.0
|
version: 3.27.0
|
||||||
react-router-dom:
|
react-router-dom:
|
||||||
specifier: ^6.28.0
|
specifier: ^6.28.0
|
||||||
version: 6.30.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
version: 6.30.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
|
react-syntax-highlighter:
|
||||||
|
specifier: ^16.1.0
|
||||||
|
version: 16.1.0(react@19.2.3)
|
||||||
reflect-metadata:
|
reflect-metadata:
|
||||||
specifier: ^0.2.2
|
specifier: ^0.2.2
|
||||||
version: 0.2.2
|
version: 0.2.2
|
||||||
@@ -204,6 +208,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==}
|
resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
|
'@babel/runtime@7.28.6':
|
||||||
|
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/template@7.27.2':
|
'@babel/template@7.27.2':
|
||||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -904,6 +912,9 @@ packages:
|
|||||||
'@types/fs-extra@9.0.13':
|
'@types/fs-extra@9.0.13':
|
||||||
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
|
resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==}
|
||||||
|
|
||||||
|
'@types/hast@3.0.4':
|
||||||
|
resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==}
|
||||||
|
|
||||||
'@types/http-cache-semantics@4.0.4':
|
'@types/http-cache-semantics@4.0.4':
|
||||||
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
||||||
|
|
||||||
@@ -922,6 +933,9 @@ packages:
|
|||||||
'@types/plist@3.0.5':
|
'@types/plist@3.0.5':
|
||||||
resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==}
|
resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==}
|
||||||
|
|
||||||
|
'@types/prismjs@1.26.5':
|
||||||
|
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
|
||||||
|
|
||||||
'@types/react-dom@19.2.3':
|
'@types/react-dom@19.2.3':
|
||||||
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -939,6 +953,12 @@ packages:
|
|||||||
'@types/triple-beam@1.3.5':
|
'@types/triple-beam@1.3.5':
|
||||||
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
|
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
|
||||||
|
|
||||||
|
'@types/unist@2.0.11':
|
||||||
|
resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==}
|
||||||
|
|
||||||
|
'@types/unist@3.0.3':
|
||||||
|
resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==}
|
||||||
|
|
||||||
'@types/uuid@11.0.0':
|
'@types/uuid@11.0.0':
|
||||||
resolution: {integrity: sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==}
|
resolution: {integrity: sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==}
|
||||||
deprecated: This is a stub types definition. uuid provides its own type definitions, so you do not need this installed.
|
deprecated: This is a stub types definition. uuid provides its own type definitions, so you do not need this installed.
|
||||||
@@ -1255,6 +1275,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
character-entities-legacy@3.0.0:
|
||||||
|
resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
|
||||||
|
|
||||||
|
character-entities@2.0.2:
|
||||||
|
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
|
||||||
|
|
||||||
|
character-reference-invalid@2.0.1:
|
||||||
|
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
|
||||||
|
|
||||||
chokidar@5.0.0:
|
chokidar@5.0.0:
|
||||||
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
||||||
engines: {node: '>= 20.19.0'}
|
engines: {node: '>= 20.19.0'}
|
||||||
@@ -1334,6 +1363,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
comma-separated-tokens@2.0.3:
|
||||||
|
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||||
|
|
||||||
commander@5.1.0:
|
commander@5.1.0:
|
||||||
resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
|
resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -1416,6 +1448,9 @@ packages:
|
|||||||
supports-color:
|
supports-color:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
decode-named-character-reference@1.3.0:
|
||||||
|
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
|
||||||
|
|
||||||
decompress-response@6.0.0:
|
decompress-response@6.0.0:
|
||||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1738,6 +1773,9 @@ packages:
|
|||||||
fast-levenshtein@2.0.6:
|
fast-levenshtein@2.0.6:
|
||||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||||
|
|
||||||
|
fault@1.0.4:
|
||||||
|
resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==}
|
||||||
|
|
||||||
fd-slicer@1.1.0:
|
fd-slicer@1.1.0:
|
||||||
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
|
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
|
||||||
|
|
||||||
@@ -1796,6 +1834,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
format@0.2.2:
|
||||||
|
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||||
|
engines: {node: '>=0.4.x'}
|
||||||
|
|
||||||
forwarded@0.2.0:
|
forwarded@0.2.0:
|
||||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -1954,12 +1996,24 @@ packages:
|
|||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
hast-util-parse-selector@4.0.0:
|
||||||
|
resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==}
|
||||||
|
|
||||||
|
hastscript@9.0.1:
|
||||||
|
resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==}
|
||||||
|
|
||||||
hermes-estree@0.25.1:
|
hermes-estree@0.25.1:
|
||||||
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
|
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
|
||||||
|
|
||||||
hermes-parser@0.25.1:
|
hermes-parser@0.25.1:
|
||||||
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
|
resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==}
|
||||||
|
|
||||||
|
highlight.js@10.7.3:
|
||||||
|
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
|
||||||
|
|
||||||
|
highlightjs-vue@1.0.0:
|
||||||
|
resolution: {integrity: sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==}
|
||||||
|
|
||||||
hoist-non-react-statics@3.3.2:
|
hoist-non-react-statics@3.3.2:
|
||||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||||
|
|
||||||
@@ -2040,6 +2094,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
is-alphabetical@2.0.1:
|
||||||
|
resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==}
|
||||||
|
|
||||||
|
is-alphanumerical@2.0.1:
|
||||||
|
resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==}
|
||||||
|
|
||||||
is-array-buffer@3.0.5:
|
is-array-buffer@3.0.5:
|
||||||
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
|
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2072,6 +2132,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
|
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
is-decimal@2.0.1:
|
||||||
|
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
|
||||||
|
|
||||||
is-extglob@2.1.1:
|
is-extglob@2.1.1:
|
||||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -2092,6 +2155,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
is-hexadecimal@2.0.1:
|
||||||
|
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
|
||||||
|
|
||||||
is-interactive@1.0.0:
|
is-interactive@1.0.0:
|
||||||
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
|
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2270,6 +2336,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
|
resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
lowlight@1.20.0:
|
||||||
|
resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==}
|
||||||
|
|
||||||
lru-cache@10.4.3:
|
lru-cache@10.4.3:
|
||||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||||
|
|
||||||
@@ -2518,6 +2587,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
|
resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
os@0.1.2:
|
||||||
|
resolution: {integrity: sha512-ZoXJkvAnljwvc56MbvhtKVWmSkzV712k42Is2mA0+0KTSRakq5XXuXpjZjgAt9ctzl51ojhQWakQQpmOvXWfjQ==}
|
||||||
|
|
||||||
own-keys@1.0.1:
|
own-keys@1.0.1:
|
||||||
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2545,6 +2617,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
parse-entities@4.0.2:
|
||||||
|
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
||||||
|
|
||||||
parseurl@1.3.3:
|
parseurl@1.3.3:
|
||||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -2626,6 +2701,10 @@ packages:
|
|||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
prismjs@1.30.0:
|
||||||
|
resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
proc-log@5.0.0:
|
proc-log@5.0.0:
|
||||||
resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==}
|
resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==}
|
||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
@@ -2641,6 +2720,9 @@ packages:
|
|||||||
prop-types@15.8.1:
|
prop-types@15.8.1:
|
||||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||||
|
|
||||||
|
property-information@7.1.0:
|
||||||
|
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
@@ -2706,6 +2788,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: '>=16.8'
|
react: '>=16.8'
|
||||||
|
|
||||||
|
react-syntax-highlighter@16.1.0:
|
||||||
|
resolution: {integrity: sha512-E40/hBiP5rCNwkeBN1vRP+xow1X0pndinO+z3h7HLsHyjztbyjfzNWNKuAsJj+7DLam9iT4AaaOZnueCU+Nplg==}
|
||||||
|
engines: {node: '>= 16.20.2'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>= 0.14.0'
|
||||||
|
|
||||||
react-transition-group@4.4.5:
|
react-transition-group@4.4.5:
|
||||||
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2735,6 +2823,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
refractor@5.0.0:
|
||||||
|
resolution: {integrity: sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==}
|
||||||
|
|
||||||
regenerator-runtime@0.14.1:
|
regenerator-runtime@0.14.1:
|
||||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||||
|
|
||||||
@@ -2941,6 +3032,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
space-separated-tokens@2.0.2:
|
||||||
|
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||||
|
|
||||||
sprintf-js@1.1.3:
|
sprintf-js@1.1.3:
|
||||||
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
|
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
|
||||||
|
|
||||||
@@ -3526,6 +3620,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime: 0.14.1
|
regenerator-runtime: 0.14.1
|
||||||
|
|
||||||
|
'@babel/runtime@7.28.6': {}
|
||||||
|
|
||||||
'@babel/template@7.27.2':
|
'@babel/template@7.27.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.27.1
|
||||||
@@ -4100,6 +4196,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.19.5
|
'@types/node': 22.19.5
|
||||||
|
|
||||||
|
'@types/hast@3.0.4':
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 3.0.3
|
||||||
|
|
||||||
'@types/http-cache-semantics@4.0.4': {}
|
'@types/http-cache-semantics@4.0.4': {}
|
||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
@@ -4120,6 +4220,8 @@ snapshots:
|
|||||||
xmlbuilder: 15.1.1
|
xmlbuilder: 15.1.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@types/prismjs@1.26.5': {}
|
||||||
|
|
||||||
'@types/react-dom@19.2.3(@types/react@19.2.8)':
|
'@types/react-dom@19.2.3(@types/react@19.2.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/react': 19.2.8
|
'@types/react': 19.2.8
|
||||||
@@ -4136,6 +4238,10 @@ snapshots:
|
|||||||
|
|
||||||
'@types/triple-beam@1.3.5': {}
|
'@types/triple-beam@1.3.5': {}
|
||||||
|
|
||||||
|
'@types/unist@2.0.11': {}
|
||||||
|
|
||||||
|
'@types/unist@3.0.3': {}
|
||||||
|
|
||||||
'@types/uuid@11.0.0':
|
'@types/uuid@11.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
uuid: 13.0.0
|
uuid: 13.0.0
|
||||||
@@ -4578,6 +4684,12 @@ snapshots:
|
|||||||
ansi-styles: 4.3.0
|
ansi-styles: 4.3.0
|
||||||
supports-color: 7.2.0
|
supports-color: 7.2.0
|
||||||
|
|
||||||
|
character-entities-legacy@3.0.0: {}
|
||||||
|
|
||||||
|
character-entities@2.0.2: {}
|
||||||
|
|
||||||
|
character-reference-invalid@2.0.1: {}
|
||||||
|
|
||||||
chokidar@5.0.0:
|
chokidar@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 5.0.0
|
readdirp: 5.0.0
|
||||||
@@ -4645,6 +4757,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream: 1.0.0
|
delayed-stream: 1.0.0
|
||||||
|
|
||||||
|
comma-separated-tokens@2.0.3: {}
|
||||||
|
|
||||||
commander@5.1.0: {}
|
commander@5.1.0: {}
|
||||||
|
|
||||||
commander@9.5.0:
|
commander@9.5.0:
|
||||||
@@ -4711,6 +4825,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
|
|
||||||
|
decode-named-character-reference@1.3.0:
|
||||||
|
dependencies:
|
||||||
|
character-entities: 2.0.2
|
||||||
|
|
||||||
decompress-response@6.0.0:
|
decompress-response@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
mimic-response: 3.1.0
|
mimic-response: 3.1.0
|
||||||
@@ -5246,6 +5364,10 @@ snapshots:
|
|||||||
|
|
||||||
fast-levenshtein@2.0.6: {}
|
fast-levenshtein@2.0.6: {}
|
||||||
|
|
||||||
|
fault@1.0.4:
|
||||||
|
dependencies:
|
||||||
|
format: 0.2.2
|
||||||
|
|
||||||
fd-slicer@1.1.0:
|
fd-slicer@1.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pend: 1.2.0
|
pend: 1.2.0
|
||||||
@@ -5312,6 +5434,8 @@ snapshots:
|
|||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
|
|
||||||
|
format@0.2.2: {}
|
||||||
|
|
||||||
forwarded@0.2.0: {}
|
forwarded@0.2.0: {}
|
||||||
|
|
||||||
frac@1.1.2: {}
|
frac@1.1.2: {}
|
||||||
@@ -5494,12 +5618,28 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
||||||
|
hast-util-parse-selector@4.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
|
||||||
|
hastscript@9.0.1:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
comma-separated-tokens: 2.0.3
|
||||||
|
hast-util-parse-selector: 4.0.0
|
||||||
|
property-information: 7.1.0
|
||||||
|
space-separated-tokens: 2.0.2
|
||||||
|
|
||||||
hermes-estree@0.25.1: {}
|
hermes-estree@0.25.1: {}
|
||||||
|
|
||||||
hermes-parser@0.25.1:
|
hermes-parser@0.25.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
hermes-estree: 0.25.1
|
hermes-estree: 0.25.1
|
||||||
|
|
||||||
|
highlight.js@10.7.3: {}
|
||||||
|
|
||||||
|
highlightjs-vue@1.0.0: {}
|
||||||
|
|
||||||
hoist-non-react-statics@3.3.2:
|
hoist-non-react-statics@3.3.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
react-is: 16.13.1
|
react-is: 16.13.1
|
||||||
@@ -5583,6 +5723,13 @@ snapshots:
|
|||||||
|
|
||||||
ipaddr.js@1.9.1: {}
|
ipaddr.js@1.9.1: {}
|
||||||
|
|
||||||
|
is-alphabetical@2.0.1: {}
|
||||||
|
|
||||||
|
is-alphanumerical@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
is-alphabetical: 2.0.1
|
||||||
|
is-decimal: 2.0.1
|
||||||
|
|
||||||
is-array-buffer@3.0.5:
|
is-array-buffer@3.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
call-bind: 1.0.8
|
call-bind: 1.0.8
|
||||||
@@ -5623,6 +5770,8 @@ snapshots:
|
|||||||
call-bound: 1.0.4
|
call-bound: 1.0.4
|
||||||
has-tostringtag: 1.0.2
|
has-tostringtag: 1.0.2
|
||||||
|
|
||||||
|
is-decimal@2.0.1: {}
|
||||||
|
|
||||||
is-extglob@2.1.1: {}
|
is-extglob@2.1.1: {}
|
||||||
|
|
||||||
is-finalizationregistry@1.1.1:
|
is-finalizationregistry@1.1.1:
|
||||||
@@ -5643,6 +5792,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-extglob: 2.1.1
|
is-extglob: 2.1.1
|
||||||
|
|
||||||
|
is-hexadecimal@2.0.1: {}
|
||||||
|
|
||||||
is-interactive@1.0.0: {}
|
is-interactive@1.0.0: {}
|
||||||
|
|
||||||
is-map@2.0.3: {}
|
is-map@2.0.3: {}
|
||||||
@@ -5811,6 +5962,11 @@ snapshots:
|
|||||||
|
|
||||||
lowercase-keys@2.0.0: {}
|
lowercase-keys@2.0.0: {}
|
||||||
|
|
||||||
|
lowlight@1.20.0:
|
||||||
|
dependencies:
|
||||||
|
fault: 1.0.4
|
||||||
|
highlight.js: 10.7.3
|
||||||
|
|
||||||
lru-cache@10.4.3: {}
|
lru-cache@10.4.3: {}
|
||||||
|
|
||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
@@ -6068,6 +6224,8 @@ snapshots:
|
|||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
wcwidth: 1.0.1
|
wcwidth: 1.0.1
|
||||||
|
|
||||||
|
os@0.1.2: {}
|
||||||
|
|
||||||
own-keys@1.0.1:
|
own-keys@1.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
get-intrinsic: 1.3.0
|
get-intrinsic: 1.3.0
|
||||||
@@ -6092,6 +6250,16 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
callsites: 3.1.0
|
callsites: 3.1.0
|
||||||
|
|
||||||
|
parse-entities@4.0.2:
|
||||||
|
dependencies:
|
||||||
|
'@types/unist': 2.0.11
|
||||||
|
character-entities-legacy: 3.0.0
|
||||||
|
character-reference-invalid: 2.0.1
|
||||||
|
decode-named-character-reference: 1.3.0
|
||||||
|
is-alphanumerical: 2.0.1
|
||||||
|
is-decimal: 2.0.1
|
||||||
|
is-hexadecimal: 2.0.1
|
||||||
|
|
||||||
parseurl@1.3.3: {}
|
parseurl@1.3.3: {}
|
||||||
|
|
||||||
path-exists@4.0.0: {}
|
path-exists@4.0.0: {}
|
||||||
@@ -6163,6 +6331,8 @@ snapshots:
|
|||||||
|
|
||||||
prettier@3.7.4: {}
|
prettier@3.7.4: {}
|
||||||
|
|
||||||
|
prismjs@1.30.0: {}
|
||||||
|
|
||||||
proc-log@5.0.0: {}
|
proc-log@5.0.0: {}
|
||||||
|
|
||||||
progress@2.0.3: {}
|
progress@2.0.3: {}
|
||||||
@@ -6178,6 +6348,8 @@ snapshots:
|
|||||||
object-assign: 4.1.1
|
object-assign: 4.1.1
|
||||||
react-is: 16.13.1
|
react-is: 16.13.1
|
||||||
|
|
||||||
|
property-information@7.1.0: {}
|
||||||
|
|
||||||
proxy-addr@2.0.7:
|
proxy-addr@2.0.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
forwarded: 0.2.0
|
forwarded: 0.2.0
|
||||||
@@ -6241,6 +6413,16 @@ snapshots:
|
|||||||
'@remix-run/router': 1.23.2
|
'@remix-run/router': 1.23.2
|
||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
|
|
||||||
|
react-syntax-highlighter@16.1.0(react@19.2.3):
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.28.6
|
||||||
|
highlight.js: 10.7.3
|
||||||
|
highlightjs-vue: 1.0.0
|
||||||
|
lowlight: 1.20.0
|
||||||
|
prismjs: 1.30.0
|
||||||
|
react: 19.2.3
|
||||||
|
refractor: 5.0.0
|
||||||
|
|
||||||
react-transition-group@4.4.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
react-transition-group@4.4.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.26.10
|
'@babel/runtime': 7.26.10
|
||||||
@@ -6279,6 +6461,13 @@ snapshots:
|
|||||||
get-proto: 1.0.1
|
get-proto: 1.0.1
|
||||||
which-builtin-type: 1.2.1
|
which-builtin-type: 1.2.1
|
||||||
|
|
||||||
|
refractor@5.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@types/hast': 3.0.4
|
||||||
|
'@types/prismjs': 1.26.5
|
||||||
|
hastscript: 9.0.1
|
||||||
|
parse-entities: 4.0.2
|
||||||
|
|
||||||
regenerator-runtime@0.14.1: {}
|
regenerator-runtime@0.14.1: {}
|
||||||
|
|
||||||
regexp.prototype.flags@1.5.4:
|
regexp.prototype.flags@1.5.4:
|
||||||
@@ -6557,6 +6746,8 @@ snapshots:
|
|||||||
|
|
||||||
source-map@0.6.1: {}
|
source-map@0.6.1: {}
|
||||||
|
|
||||||
|
space-separated-tokens@2.0.2: {}
|
||||||
|
|
||||||
sprintf-js@1.1.3:
|
sprintf-js@1.1.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,15 @@ export class DbManager extends Service {
|
|||||||
this.dataSource = new DataSource({
|
this.dataSource = new DataSource({
|
||||||
type: 'better-sqlite3',
|
type: 'better-sqlite3',
|
||||||
database: dbPath,
|
database: dbPath,
|
||||||
entities: [StudentEntity, ReasonEntity, ScoreEventEntity, SettlementEntity, SettingEntity, TagEntity, StudentTagEntity],
|
entities: [
|
||||||
|
StudentEntity,
|
||||||
|
ReasonEntity,
|
||||||
|
ScoreEventEntity,
|
||||||
|
SettlementEntity,
|
||||||
|
SettingEntity,
|
||||||
|
TagEntity,
|
||||||
|
StudentTagEntity
|
||||||
|
],
|
||||||
migrations,
|
migrations,
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
logging: false
|
logging: false
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ export {
|
|||||||
WindowManagerToken,
|
WindowManagerToken,
|
||||||
TrayServiceToken,
|
TrayServiceToken,
|
||||||
AutoScoreServiceToken,
|
AutoScoreServiceToken,
|
||||||
HttpServerServiceToken
|
HttpServerServiceToken,
|
||||||
|
FileSystemServiceToken
|
||||||
} from './tokens'
|
} from './tokens'
|
||||||
export type {
|
export type {
|
||||||
appRuntimeContext,
|
appRuntimeContext,
|
||||||
|
|||||||
@@ -16,3 +16,4 @@ export const WindowManagerToken = Symbol.for('secscore.windowManager')
|
|||||||
export const TrayServiceToken = Symbol.for('secscore.trayService')
|
export const TrayServiceToken = Symbol.for('secscore.trayService')
|
||||||
export const AutoScoreServiceToken = Symbol.for('secscore.autoScoreService')
|
export const AutoScoreServiceToken = Symbol.for('secscore.autoScoreService')
|
||||||
export const HttpServerServiceToken = Symbol.for('secscore.httpServerService')
|
export const HttpServerServiceToken = Symbol.for('secscore.httpServerService')
|
||||||
|
export const FileSystemServiceToken = Symbol.for('secscore.fileSystemService')
|
||||||
|
|||||||
+29
-8
@@ -17,6 +17,7 @@ import { HttpServerService } from './services/HttpServerService'
|
|||||||
import { WindowManager, type windowManagerOptions } from './services/WindowManager'
|
import { WindowManager, type windowManagerOptions } from './services/WindowManager'
|
||||||
import { TrayService } from './services/TrayService'
|
import { TrayService } from './services/TrayService'
|
||||||
import { AutoScoreService } from './services/AutoScoreService'
|
import { AutoScoreService } from './services/AutoScoreService'
|
||||||
|
import { FileSystemService } from './services/FileSystemService'
|
||||||
import { StudentRepository } from './repos/StudentRepository'
|
import { StudentRepository } from './repos/StudentRepository'
|
||||||
import { ReasonRepository } from './repos/ReasonRepository'
|
import { ReasonRepository } from './repos/ReasonRepository'
|
||||||
import { EventRepository } from './repos/EventRepository'
|
import { EventRepository } from './repos/EventRepository'
|
||||||
@@ -39,13 +40,15 @@ import {
|
|||||||
WindowManagerToken,
|
WindowManagerToken,
|
||||||
TrayServiceToken,
|
TrayServiceToken,
|
||||||
AutoScoreServiceToken,
|
AutoScoreServiceToken,
|
||||||
HttpServerServiceToken
|
HttpServerServiceToken,
|
||||||
|
FileSystemServiceToken
|
||||||
} from './hosting'
|
} from './hosting'
|
||||||
|
|
||||||
type mainAppConfig = {
|
type mainAppConfig = {
|
||||||
isDev: boolean
|
isDev: boolean
|
||||||
appRoot: string
|
appRoot: string
|
||||||
dataRoot: string
|
dataRoot: string
|
||||||
|
configDir: string
|
||||||
logDir: string
|
logDir: string
|
||||||
themeDir: string
|
themeDir: string
|
||||||
dbPath: string
|
dbPath: string
|
||||||
@@ -184,6 +187,7 @@ app.whenReady().then(async () => {
|
|||||||
: ensureWritableDir(join(appRoot, 'data'), join(app.getPath('userData'), 'secscore-data'))
|
: ensureWritableDir(join(appRoot, 'data'), join(app.getPath('userData'), 'secscore-data'))
|
||||||
|
|
||||||
const logDir = is.dev ? join(process.cwd(), 'logs') : join(dataRoot, 'logs')
|
const logDir = is.dev ? join(process.cwd(), 'logs') : join(dataRoot, 'logs')
|
||||||
|
const configDir = is.dev ? join(process.cwd(), 'configs') : join(dataRoot, 'configs')
|
||||||
const themeDir = is.dev
|
const themeDir = is.dev
|
||||||
? join(process.cwd(), 'themes')
|
? join(process.cwd(), 'themes')
|
||||||
: ensureWritableDir(join(appRoot, 'themes'), join(dataRoot, 'themes'))
|
: ensureWritableDir(join(appRoot, 'themes'), join(dataRoot, 'themes'))
|
||||||
@@ -194,6 +198,7 @@ app.whenReady().then(async () => {
|
|||||||
appRoot,
|
appRoot,
|
||||||
dataRoot,
|
dataRoot,
|
||||||
logDir,
|
logDir,
|
||||||
|
configDir,
|
||||||
themeDir,
|
themeDir,
|
||||||
dbPath,
|
dbPath,
|
||||||
window: {
|
window: {
|
||||||
@@ -232,7 +237,10 @@ app.whenReady().then(async () => {
|
|||||||
(p) => new PermissionService(p.get(MainContext))
|
(p) => new PermissionService(p.get(MainContext))
|
||||||
)
|
)
|
||||||
services.addSingleton(AuthService, (p) => new AuthService(p.get(MainContext)))
|
services.addSingleton(AuthService, (p) => new AuthService(p.get(MainContext)))
|
||||||
services.addSingleton(DataService, (p) => new DataService(p.get(MainContext), p.get(TagRepositoryToken)))
|
services.addSingleton(
|
||||||
|
DataService,
|
||||||
|
(p) => new DataService(p.get(MainContext), p.get(TagRepositoryToken))
|
||||||
|
)
|
||||||
|
|
||||||
services.addSingleton(
|
services.addSingleton(
|
||||||
StudentRepositoryToken,
|
StudentRepositoryToken,
|
||||||
@@ -261,14 +269,15 @@ app.whenReady().then(async () => {
|
|||||||
TrayServiceToken,
|
TrayServiceToken,
|
||||||
(p) => new TrayService(p.get(MainContext), config.window)
|
(p) => new TrayService(p.get(MainContext), config.window)
|
||||||
)
|
)
|
||||||
services.addSingleton(
|
services.addSingleton(AutoScoreServiceToken, (p) => new AutoScoreService(p.get(MainContext)))
|
||||||
AutoScoreServiceToken,
|
|
||||||
(p) => new AutoScoreService(p.get(MainContext))
|
|
||||||
)
|
|
||||||
services.addSingleton(
|
services.addSingleton(
|
||||||
HttpServerServiceToken,
|
HttpServerServiceToken,
|
||||||
(p) => new HttpServerService(p.get(MainContext))
|
(p) => new HttpServerService(p.get(MainContext))
|
||||||
)
|
)
|
||||||
|
services.addSingleton(
|
||||||
|
FileSystemServiceToken,
|
||||||
|
(p) => new FileSystemService(p.get(MainContext), config.configDir)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.configure(async (_builderContext, appCtx) => {
|
.configure(async (_builderContext, appCtx) => {
|
||||||
const services = appCtx.services
|
const services = appCtx.services
|
||||||
@@ -285,13 +294,19 @@ app.whenReady().then(async () => {
|
|||||||
services.get(ReasonRepositoryToken)
|
services.get(ReasonRepositoryToken)
|
||||||
services.get(EventRepositoryToken)
|
services.get(EventRepositoryToken)
|
||||||
services.get(SettlementRepositoryToken)
|
services.get(SettlementRepositoryToken)
|
||||||
services.get(ThemeServiceToken)
|
const theme = services.get(
|
||||||
|
ThemeServiceToken
|
||||||
|
) as import('./services/ThemeService').ThemeService
|
||||||
|
await theme.init()
|
||||||
|
if (!process.env.HEADLESS) {
|
||||||
services.get(WindowManagerToken)
|
services.get(WindowManagerToken)
|
||||||
const tray = services.get(TrayServiceToken) as TrayService
|
const tray = services.get(TrayServiceToken) as TrayService
|
||||||
tray.initialize()
|
tray.initialize()
|
||||||
|
}
|
||||||
const autoScore = services.get(AutoScoreServiceToken) as AutoScoreService
|
const autoScore = services.get(AutoScoreServiceToken) as AutoScoreService
|
||||||
autoScore.initialize?.()
|
autoScore.initialize?.()
|
||||||
services.get(HttpServerServiceToken)
|
services.get(HttpServerServiceToken)
|
||||||
|
services.get(FileSystemServiceToken)
|
||||||
})
|
})
|
||||||
.configure(async (_builderContext, appCtx) => {
|
.configure(async (_builderContext, appCtx) => {
|
||||||
const services = appCtx.services
|
const services = appCtx.services
|
||||||
@@ -308,10 +323,16 @@ app.whenReady().then(async () => {
|
|||||||
services.get(ReasonRepositoryToken)
|
services.get(ReasonRepositoryToken)
|
||||||
services.get(EventRepositoryToken)
|
services.get(EventRepositoryToken)
|
||||||
services.get(SettlementRepositoryToken)
|
services.get(SettlementRepositoryToken)
|
||||||
services.get(ThemeServiceToken)
|
const theme = services.get(
|
||||||
|
ThemeServiceToken
|
||||||
|
) as import('./services/ThemeService').ThemeService
|
||||||
|
await theme.init()
|
||||||
|
if (!process.env.HEADLESS) {
|
||||||
services.get(WindowManagerToken)
|
services.get(WindowManagerToken)
|
||||||
const tray = services.get(TrayServiceToken) as TrayService
|
const tray = services.get(TrayServiceToken) as TrayService
|
||||||
tray.initialize()
|
tray.initialize()
|
||||||
|
}
|
||||||
|
services.get(FileSystemServiceToken)
|
||||||
})
|
})
|
||||||
|
|
||||||
const host = await builder.build()
|
const host = await builder.build()
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export class StudentRepository extends Service {
|
|||||||
order: { score: 'DESC', name: 'ASC' }
|
order: { score: 'DESC', name: 'ASC' }
|
||||||
})
|
})
|
||||||
|
|
||||||
return entities.map(entity => {
|
return entities.map((entity) => {
|
||||||
let tags: string[] = []
|
let tags: string[] = []
|
||||||
try {
|
try {
|
||||||
tags = Array.isArray(entity.tags) ? entity.tags : JSON.parse(entity.tags || '[]')
|
tags = Array.isArray(entity.tags) ? entity.tags : JSON.parse(entity.tags || '[]')
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class TagRepository {
|
|||||||
.orderBy('st.created_at', 'ASC')
|
.orderBy('st.created_at', 'ASC')
|
||||||
.getMany()
|
.getMany()
|
||||||
|
|
||||||
return relations.map(r => r.tag).filter(Boolean)
|
return relations.map((r) => r.tag).filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
async addTagToStudent(studentId: number, tagId: number): Promise<void> {
|
async addTagToStudent(studentId: number, tagId: number): Promise<void> {
|
||||||
|
|||||||
@@ -0,0 +1,554 @@
|
|||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { AddIcon, Delete1Icon, MoveIcon } from 'tdesign-icons-react'
|
||||||
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||||
|
import { prism } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Form,
|
||||||
|
Input,
|
||||||
|
InputNumber,
|
||||||
|
Button,
|
||||||
|
MessagePlugin,
|
||||||
|
Table,
|
||||||
|
PrimaryTableCol,
|
||||||
|
Tag,
|
||||||
|
Space,
|
||||||
|
Switch,
|
||||||
|
Popconfirm,
|
||||||
|
Radio,
|
||||||
|
Select,
|
||||||
|
TooltipLite
|
||||||
|
} from 'tdesign-react'
|
||||||
|
|
||||||
|
interface AutoScoreRule {
|
||||||
|
id: number
|
||||||
|
enabled: boolean
|
||||||
|
name: string
|
||||||
|
intervalMinutes: number
|
||||||
|
studentNames: string[]
|
||||||
|
scoreValue: number
|
||||||
|
reason: string
|
||||||
|
lastExecuted?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AutoScoreRuleFormValues {
|
||||||
|
name: string
|
||||||
|
intervalMinutes: number
|
||||||
|
studentNames: string
|
||||||
|
scoreValue: number
|
||||||
|
reason: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AutoScoreManager: React.FC = () => {
|
||||||
|
const [rules, setRules] = useState<AutoScoreRule[]>([])
|
||||||
|
const [students, setStudents] = useState<{ id: number; name: string }[]>([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
const [pageSize, setPageSize] = useState<number>(50)
|
||||||
|
const [form] = Form.useForm()
|
||||||
|
const [editingRuleId, setEditingRuleId] = useState<number | null>(null)
|
||||||
|
|
||||||
|
const fetchRules = async () => {
|
||||||
|
if (!(window as any).api) return
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
// 权限检查:确保当前为 admin
|
||||||
|
try {
|
||||||
|
const authRes = await (window as any).api.authGetStatus()
|
||||||
|
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
||||||
|
MessagePlugin.error('需要管理员权限以查看自动加分自动化,请先登录管理员账号')
|
||||||
|
setLoading(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// 如果权限检查失败,继续让后端返回更明确的错误
|
||||||
|
console.warn('Auth check failed', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const [rulesRes, studentsRes] = await Promise.all([
|
||||||
|
(window as any).api.invoke('auto-score:getRules', {}),
|
||||||
|
(window as any).api.queryStudents({})
|
||||||
|
])
|
||||||
|
if (rulesRes.success) {
|
||||||
|
setRules(rulesRes.data)
|
||||||
|
} else {
|
||||||
|
MessagePlugin.error(rulesRes.message || '获取自动化失败')
|
||||||
|
}
|
||||||
|
if (studentsRes.success) {
|
||||||
|
setStudents(studentsRes.data)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch auto score rules:', error)
|
||||||
|
MessagePlugin.error('获取自动化失败')
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchRules()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!(window as any).api) return
|
||||||
|
|
||||||
|
const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues & {
|
||||||
|
timeUnit: string
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!values.name || values.intervalMinutes == null || values.scoreValue == null) {
|
||||||
|
MessagePlugin.warning('请填写完整信息')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据单位转换间隔时间
|
||||||
|
const intervalMinutes =
|
||||||
|
values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes
|
||||||
|
|
||||||
|
// 确保 studentNames 是数组类型
|
||||||
|
const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []
|
||||||
|
|
||||||
|
const ruleData = {
|
||||||
|
enabled: true,
|
||||||
|
name: values.name,
|
||||||
|
intervalMinutes,
|
||||||
|
studentNames,
|
||||||
|
scoreValue: values.scoreValue,
|
||||||
|
reason: values.reason || `自动化加分 - ${values.name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限检查:仅管理员可创建/更新自动化
|
||||||
|
try {
|
||||||
|
const authRes = await (window as any).api.authGetStatus()
|
||||||
|
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
||||||
|
MessagePlugin.error('需要管理员权限以创建或更新自动加分自动化')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Auth check failed', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let res
|
||||||
|
if (editingRuleId !== null) {
|
||||||
|
// 更新现有自动化
|
||||||
|
res = await (window as any).api.invoke('auto-score:updateRule', {
|
||||||
|
id: editingRuleId,
|
||||||
|
...ruleData
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 创建新自动化
|
||||||
|
res = await (window as any).api.invoke('auto-score:addRule', ruleData)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (res.success) {
|
||||||
|
MessagePlugin.success(editingRuleId !== null ? '自动化更新成功' : '自动化创建成功')
|
||||||
|
// 手动清空表单字段,避免 form.reset() 导致的栈溢出
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: '',
|
||||||
|
intervalMinutes: undefined,
|
||||||
|
studentNames: '',
|
||||||
|
scoreValue: undefined,
|
||||||
|
reason: '',
|
||||||
|
timeUnit: 'minutes'
|
||||||
|
})
|
||||||
|
setEditingRuleId(null)
|
||||||
|
fetchRules() // 刷新自动化列表
|
||||||
|
} else {
|
||||||
|
MessagePlugin.error(
|
||||||
|
res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to submit auto score rule:', error)
|
||||||
|
MessagePlugin.error(editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handleEdit = (rule: AutoScoreRule) => {
|
||||||
|
setEditingRuleId(rule.id)
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: rule.name,
|
||||||
|
intervalMinutes: rule.intervalMinutes,
|
||||||
|
studentNames: rule.studentNames.join(', '),
|
||||||
|
scoreValue: rule.scoreValue,
|
||||||
|
reason: rule.reason
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (ruleId: number) => {
|
||||||
|
if (!(window as any).api) return
|
||||||
|
// 权限检查
|
||||||
|
try {
|
||||||
|
const authRes = await (window as any).api.authGetStatus()
|
||||||
|
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
||||||
|
MessagePlugin.error('需要管理员权限以删除自动加分自动化')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Auth check failed', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await (window as any).api.invoke('auto-score:deleteRule', ruleId)
|
||||||
|
if (res.success) {
|
||||||
|
MessagePlugin.success('自动化删除成功')
|
||||||
|
fetchRules() // 刷新自动化列表
|
||||||
|
} else {
|
||||||
|
MessagePlugin.error(res.message || '删除自动化失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete auto score rule:', error)
|
||||||
|
MessagePlugin.error('删除自动化失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggle = async (ruleId: number, enabled: boolean) => {
|
||||||
|
if (!(window as any).api) return
|
||||||
|
// 权限检查
|
||||||
|
try {
|
||||||
|
const authRes = await (window as any).api.authGetStatus()
|
||||||
|
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
||||||
|
MessagePlugin.error('需要管理员权限以启用/禁用自动加分自动化')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Auth check failed', e)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await (window as any).api.invoke('auto-score:toggleRule', { ruleId, enabled })
|
||||||
|
if (res.success) {
|
||||||
|
MessagePlugin.success(enabled ? '自动化已启用' : '自动化已禁用')
|
||||||
|
fetchRules() // 刷新自动化列表
|
||||||
|
} else {
|
||||||
|
MessagePlugin.error(res.message || (enabled ? '启用自动化失败' : '禁用自动化失败'))
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to toggle auto score rule:', error)
|
||||||
|
MessagePlugin.error(enabled ? '启用自动化失败' : '禁用自动化失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResetForm = () => {
|
||||||
|
// 手动清空表单字段,避免 form.reset() 导致的栈溢出
|
||||||
|
form.setFieldsValue({
|
||||||
|
name: '',
|
||||||
|
intervalMinutes: undefined,
|
||||||
|
studentNames: '',
|
||||||
|
scoreValue: undefined,
|
||||||
|
reason: ''
|
||||||
|
})
|
||||||
|
setEditingRuleId(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns: PrimaryTableCol<AutoScoreRule>[] = [
|
||||||
|
{
|
||||||
|
colKey: 'drag',
|
||||||
|
title: '排序',
|
||||||
|
cell: () => <MoveIcon />,
|
||||||
|
width: 60
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colKey: 'enabled',
|
||||||
|
title: '状态',
|
||||||
|
width: 80,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Switch
|
||||||
|
value={row.enabled}
|
||||||
|
onChange={(value) => handleToggle(row.id, value)}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{ colKey: 'name', title: '自动化名称', width: 150 },
|
||||||
|
{
|
||||||
|
colKey: 'intervalMinutes',
|
||||||
|
title: '间隔',
|
||||||
|
width: 100,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const isDays = row.intervalMinutes >= 1440
|
||||||
|
const value = isDays ? row.intervalMinutes / 1440 : row.intervalMinutes
|
||||||
|
const unit = isDays ? '天' : '分钟'
|
||||||
|
return `${value} ${unit}`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colKey: 'scoreValue',
|
||||||
|
title: '分值',
|
||||||
|
width: 80,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Tag theme={row.scoreValue > 0 ? 'success' : 'danger'} variant="light">
|
||||||
|
{row.scoreValue > 0 ? `+${row.scoreValue}` : row.scoreValue}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colKey: 'studentNames',
|
||||||
|
title: '适用学生',
|
||||||
|
width: 130,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
if (row.studentNames.length === 0) {
|
||||||
|
return <span>所有学生</span>
|
||||||
|
}
|
||||||
|
const studentList = row.studentNames.join(',\n')
|
||||||
|
return (
|
||||||
|
<TooltipLite content={studentList} showArrow placement="mouse" theme="default">
|
||||||
|
{row.studentNames.length} 名学生
|
||||||
|
</TooltipLite>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ colKey: 'reason', title: '理由', width: 130, ellipsis: true },
|
||||||
|
{
|
||||||
|
colKey: 'lastExecuted',
|
||||||
|
title: '最后执行',
|
||||||
|
width: 180,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
if (!row.lastExecuted) return <span>未执行</span>
|
||||||
|
try {
|
||||||
|
const date = new Date(row.lastExecuted)
|
||||||
|
return date.toLocaleString()
|
||||||
|
} catch {
|
||||||
|
return <span>无效时间</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
colKey: 'operation',
|
||||||
|
title: '操作',
|
||||||
|
width: 150,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Space>
|
||||||
|
<Button size="small" variant="outline" onClick={() => handleEdit(row)}>
|
||||||
|
编辑
|
||||||
|
</Button>
|
||||||
|
<Popconfirm content="确定要删除这条自动化吗?" onConfirm={() => handleDelete(row.id)}>
|
||||||
|
<Button size="small" variant="outline" theme="danger">
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
</Space>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const onDragSort = (params: any) => setRules(params.newData)
|
||||||
|
|
||||||
|
// 定义触发规则选项
|
||||||
|
const triggerOptions = [
|
||||||
|
{ label: '学生注册', value: 'student_registered' },
|
||||||
|
{ label: '学生登录', value: 'student_logged_in' },
|
||||||
|
{ label: '完成作业', value: 'homework_completed' },
|
||||||
|
{ label: '考试通过', value: 'exam_passed' },
|
||||||
|
{ label: '参与活动', value: 'event_participated' },
|
||||||
|
{ label: '签到', value: 'check_in' },
|
||||||
|
{ label: '其他自定义事件', value: 'custom_event' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const initialTriggers = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
triggerEvent: triggerOptions[1],
|
||||||
|
description: '当学生登录时触发自动化',
|
||||||
|
haveValue: true,
|
||||||
|
value: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
triggerEvent: triggerOptions[2],
|
||||||
|
description: '当学生完成作业时触发自动化',
|
||||||
|
haveValue: true,
|
||||||
|
value: '12'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
triggerEvent: triggerOptions[3],
|
||||||
|
description: '当学生考试通过时触发自动化',
|
||||||
|
haveValue: false,
|
||||||
|
value: null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const [triggerList, setTriggerList] = useState(initialTriggers)
|
||||||
|
|
||||||
|
const handleTriggerChange = (id: number, value: string) => {
|
||||||
|
setTriggerList((prev) =>
|
||||||
|
prev.map((t) =>
|
||||||
|
t.id === id
|
||||||
|
? { ...t, triggerEvent: triggerOptions.find((o) => o.value === value) || t.triggerEvent }
|
||||||
|
: t
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleValueChange = (id: number, val: string) => {
|
||||||
|
setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, value: val } : t)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteTrigger = (id: number) => {
|
||||||
|
setTriggerList((prev) => prev.filter((t) => t.id !== id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddTrigger = () => {
|
||||||
|
const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1
|
||||||
|
setTriggerList((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ id: nextId, triggerEvent: triggerOptions[0], description: '', haveValue: false, value: '' }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerItems = triggerList
|
||||||
|
.filter((t) => t.description !== null)
|
||||||
|
.map((triggerTest) => (
|
||||||
|
<div key={triggerTest.id} style={{ display: 'flex', gap: 5 }}>
|
||||||
|
<Button
|
||||||
|
theme="default"
|
||||||
|
variant="text"
|
||||||
|
icon={<Delete1Icon strokeWidth={2.4} />}
|
||||||
|
onClick={() => handleDeleteTrigger(triggerTest.id)}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={triggerTest.triggerEvent.value}
|
||||||
|
style={{ width: '200px' }}
|
||||||
|
options={triggerOptions}
|
||||||
|
placeholder="请选择触发规则"
|
||||||
|
onChange={(value) => handleTriggerChange(triggerTest.id, value as string)}
|
||||||
|
/>
|
||||||
|
{triggerTest.haveValue === true ? (
|
||||||
|
<Input
|
||||||
|
placeholder="请输入Value"
|
||||||
|
style={{ width: '150px' }}
|
||||||
|
value={String(triggerTest.value)}
|
||||||
|
onChange={(v) =>
|
||||||
|
handleValueChange(triggerTest.id, String((v as any).target?.value ?? v))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '24px' }}>
|
||||||
|
<h2 style={{ marginBottom: '24px', color: 'var(--ss-text-main)' }}>自动化加分管理</h2>
|
||||||
|
|
||||||
|
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
||||||
|
<Form form={form} labelWidth={100} onReset={handleResetForm}>
|
||||||
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
|
||||||
|
<Form.FormItem
|
||||||
|
label="自动化名称"
|
||||||
|
name="name"
|
||||||
|
rules={[{ required: true, message: '请输入自动化名称' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="例如:每日签到加分" />
|
||||||
|
</Form.FormItem>
|
||||||
|
|
||||||
|
<Form.FormItem>
|
||||||
|
<Space>
|
||||||
|
<Form.FormItem
|
||||||
|
label="间隔时间"
|
||||||
|
name="intervalMinutes"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: '请输入间隔时间' },
|
||||||
|
{ min: 1, message: '间隔时间至少为1分钟' }
|
||||||
|
]}
|
||||||
|
style={{ marginBottom: 0 }}
|
||||||
|
>
|
||||||
|
<InputNumber min={1} placeholder="例如:1(每隔1分钟/天执行一次)" />
|
||||||
|
</Form.FormItem>
|
||||||
|
<Form.FormItem name="timeUnit" initialData="minutes" style={{ marginBottom: 0 }}>
|
||||||
|
<Radio.Group variant="default-filled">
|
||||||
|
<Radio.Button value="days">天</Radio.Button>
|
||||||
|
<Radio.Button value="minutes">分钟</Radio.Button>
|
||||||
|
</Radio.Group>
|
||||||
|
</Form.FormItem>
|
||||||
|
</Space>
|
||||||
|
</Form.FormItem>
|
||||||
|
|
||||||
|
<Form.FormItem
|
||||||
|
label="加分值"
|
||||||
|
name="scoreValue"
|
||||||
|
rules={[{ required: true, message: '请输入加分值' }]}
|
||||||
|
>
|
||||||
|
<InputNumber placeholder="例如:1(每次加1分)" />
|
||||||
|
</Form.FormItem>
|
||||||
|
|
||||||
|
<Form.FormItem label="适用学生" name="studentNames">
|
||||||
|
<Select
|
||||||
|
filterable
|
||||||
|
multiple
|
||||||
|
placeholder="请选择或搜索学生(留空表示所有学生)"
|
||||||
|
options={students.map((student) => ({ label: student.name, value: student.name }))}
|
||||||
|
/>
|
||||||
|
</Form.FormItem>
|
||||||
|
|
||||||
|
<Form.FormItem label="加分理由" name="reason">
|
||||||
|
<Input placeholder="例如:每日签到奖励" />
|
||||||
|
</Form.FormItem>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
|
||||||
|
<Button theme="primary" onClick={handleSubmit}>
|
||||||
|
{editingRuleId !== null ? '更新自动化' : '添加自动化'}
|
||||||
|
</Button>
|
||||||
|
<Button type="reset" variant="outline">
|
||||||
|
{editingRuleId !== null ? '取消编辑' : '重置表单'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
||||||
|
<Table
|
||||||
|
data={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
|
||||||
|
columns={columns}
|
||||||
|
rowKey="id"
|
||||||
|
resizable
|
||||||
|
loading={loading}
|
||||||
|
dragSort="row-handler"
|
||||||
|
onDragSort={onDragSort}
|
||||||
|
pagination={{
|
||||||
|
current: currentPage,
|
||||||
|
pageSize,
|
||||||
|
total: rules.length,
|
||||||
|
onChange: (pageInfo) => setCurrentPage(pageInfo.current),
|
||||||
|
onPageSizeChange: (size) => setPageSize(size)
|
||||||
|
}}
|
||||||
|
style={{ color: 'var(--ss-text-main)' }}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
||||||
|
title="当以下事件触发时"
|
||||||
|
headerBordered
|
||||||
|
>
|
||||||
|
<Space style={{ display: 'grid' }}>
|
||||||
|
{triggerItems}
|
||||||
|
<Button
|
||||||
|
theme="default"
|
||||||
|
variant="text"
|
||||||
|
style={{ fontWeight: 'bolder', fontSize: 15 }}
|
||||||
|
icon={<AddIcon strokeWidth={3} />}
|
||||||
|
onClick={handleAddTrigger}
|
||||||
|
>
|
||||||
|
添加触发器
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
||||||
|
<SyntaxHighlighter language="javascript" style={prism} showLineNumbers>
|
||||||
|
println("这是一个示例代码块,展示如何使用自动化加分功能的API接口")
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</Card>
|
||||||
|
{/* <div style={{ marginTop: '24px', padding: '16px', backgroundColor: 'var(--ss-card-bg)', borderRadius: '8px' }}>
|
||||||
|
<h3 style={{ marginBottom: '12px', color: 'var(--ss-text-main)' }}>使用说明</h3>
|
||||||
|
<ul style={{ color: 'var(--ss-text-secondary)', lineHeight: '1.6' }}>
|
||||||
|
<li>自动化加分功能会按照设定的时间间隔自动为学生加分</li>
|
||||||
|
<li>间隔时间以分钟为单位,例如1440表示每24小时(一天)执行一次</li>
|
||||||
|
<li>如果"适用学生"字段为空,则自动化适用于所有学生</li>
|
||||||
|
<li>可以随时启用/禁用自动化,不会影响已保存的自动化配置</li>
|
||||||
|
</ul>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -201,7 +201,7 @@ export class AutoScoreService extends Service {
|
|||||||
// 否则只对指定的学生加分
|
// 否则只对指定的学生加分
|
||||||
const allStudents = await studentRepo.findAll()
|
const allStudents = await studentRepo.findAll()
|
||||||
for (const name of rule.studentNames) {
|
for (const name of rule.studentNames) {
|
||||||
const student = allStudents.find(s => s.name === name)
|
const student = allStudents.find((s) => s.name === name)
|
||||||
if (student) {
|
if (student) {
|
||||||
studentsToScore.push(student)
|
studentsToScore.push(student)
|
||||||
}
|
}
|
||||||
@@ -236,7 +236,7 @@ export class AutoScoreService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addRule(rule: Omit<AutoScoreRule, 'id' | 'lastExecuted'>): Promise<number> {
|
async addRule(rule: Omit<AutoScoreRule, 'id' | 'lastExecuted'>): Promise<number> {
|
||||||
const newId = this.rules.length > 0 ? Math.max(...this.rules.map(r => r.id)) + 1 : 1
|
const newId = this.rules.length > 0 ? Math.max(...this.rules.map((r) => r.id)) + 1 : 1
|
||||||
const newRule: AutoScoreRule = {
|
const newRule: AutoScoreRule = {
|
||||||
...rule,
|
...rule,
|
||||||
id: newId,
|
id: newId,
|
||||||
@@ -254,7 +254,7 @@ export class AutoScoreService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateRule(rule: Partial<AutoScoreRule> & { id: number }): Promise<boolean> {
|
async updateRule(rule: Partial<AutoScoreRule> & { id: number }): Promise<boolean> {
|
||||||
const index = this.rules.findIndex(r => r.id === rule.id)
|
const index = this.rules.findIndex((r) => r.id === rule.id)
|
||||||
if (index === -1) return false
|
if (index === -1) return false
|
||||||
|
|
||||||
// 停止旧的定时器
|
// 停止旧的定时器
|
||||||
@@ -280,7 +280,7 @@ export class AutoScoreService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteRule(ruleId: number): Promise<boolean> {
|
async deleteRule(ruleId: number): Promise<boolean> {
|
||||||
const index = this.rules.findIndex(r => r.id === ruleId)
|
const index = this.rules.findIndex((r) => r.id === ruleId)
|
||||||
if (index === -1) return false
|
if (index === -1) return false
|
||||||
|
|
||||||
// 停止定时器
|
// 停止定时器
|
||||||
@@ -298,7 +298,7 @@ export class AutoScoreService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async toggleRule(ruleId: number, enabled: boolean): Promise<boolean> {
|
async toggleRule(ruleId: number, enabled: boolean): Promise<boolean> {
|
||||||
const rule = this.rules.find(r => r.id === ruleId)
|
const rule = this.rules.find((r) => r.id === ruleId)
|
||||||
if (!rule) return false
|
if (!rule) return false
|
||||||
|
|
||||||
rule.enabled = enabled
|
rule.enabled = enabled
|
||||||
@@ -324,7 +324,7 @@ export class AutoScoreService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isEnabled(): boolean {
|
isEnabled(): boolean {
|
||||||
return this.rules.some(rule => rule.enabled)
|
return this.rules.some((rule) => rule.enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
async restart() {
|
async restart() {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class DataService extends Service {
|
|||||||
const tags = await this.tagRepo.findAll()
|
const tags = await this.tagRepo.findAll()
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: tags.map(t => ({ id: t.id, name: t.name }))
|
data: tags.map((t) => ({ id: t.id, name: t.name }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ export class DataService extends Service {
|
|||||||
const tags = await this.tagRepo.findByStudent(studentId)
|
const tags = await this.tagRepo.findByStudent(studentId)
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: tags.map(t => ({ id: t.id, name: t.name }))
|
data: tags.map((t) => ({ id: t.id, name: t.name }))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -90,12 +90,15 @@ export class DataService extends Service {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
this.mainCtx.handle('tags:updateStudentTags', async (event, studentId: number, tagIds: number[]) => {
|
this.mainCtx.handle(
|
||||||
|
'tags:updateStudentTags',
|
||||||
|
async (event, studentId: number, tagIds: number[]) => {
|
||||||
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
if (!this.mainCtx.permissions.requirePermission(event, 'admin'))
|
||||||
return { success: false, message: 'Permission denied' }
|
return { success: false, message: 'Permission denied' }
|
||||||
|
|
||||||
await this.tagRepo.updateStudentTags(studentId, tagIds)
|
await this.tagRepo.updateStudentTags(studentId, tagIds)
|
||||||
return { success: true }
|
return { success: true }
|
||||||
})
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
import { Service } from '../../shared/kernel'
|
||||||
|
import { MainContext } from '../context'
|
||||||
|
import { join } from 'path'
|
||||||
|
import fs from 'fs/promises'
|
||||||
|
|
||||||
|
declare module '../../shared/kernel' {
|
||||||
|
interface Context {
|
||||||
|
fileSystem: FileSystemService
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigFileInfo {
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
size: number
|
||||||
|
modified: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigFolderStructure {
|
||||||
|
configRoot: string
|
||||||
|
automatic: string
|
||||||
|
script: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileSystemService extends Service {
|
||||||
|
private configRoot: string
|
||||||
|
private automaticDir: string
|
||||||
|
private scriptDir: string
|
||||||
|
private initPromise: Promise<void> | null = null
|
||||||
|
|
||||||
|
constructor(ctx: MainContext, ConfigRoot: string) {
|
||||||
|
super(ctx, 'fileSystem')
|
||||||
|
this.configRoot = ConfigRoot
|
||||||
|
this.automaticDir = join(this.configRoot, 'automatic')
|
||||||
|
this.scriptDir = join(this.configRoot, 'script')
|
||||||
|
this.initPromise = this.initialize()
|
||||||
|
this.registerIpc()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async initialize(): Promise<void> {
|
||||||
|
await this.ensureDirectories()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureDirectories(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.mkdir(this.configRoot, { recursive: true })
|
||||||
|
await fs.mkdir(this.automaticDir, { recursive: true })
|
||||||
|
await fs.mkdir(this.scriptDir, { recursive: true })
|
||||||
|
} catch (error) {
|
||||||
|
this.ctx.logger.warn('FileSystemService: Failed to create config directories', { error })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfigStructure(): ConfigFolderStructure {
|
||||||
|
return {
|
||||||
|
configRoot: this.configRoot,
|
||||||
|
automatic: this.automaticDir,
|
||||||
|
script: this.scriptDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async readJsonFile<T = any>(
|
||||||
|
relativePath: string,
|
||||||
|
folder: 'automatic' | 'script' = 'automatic'
|
||||||
|
): Promise<T | null> {
|
||||||
|
await this.initPromise
|
||||||
|
const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir
|
||||||
|
const filePath = join(baseDir, relativePath)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = await fs.readFile(filePath, 'utf-8')
|
||||||
|
return JSON.parse(content) as T
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
this.ctx.logger.warn('FileSystemService: Failed to read JSON file', { path: filePath, error })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeJsonFile(
|
||||||
|
relativePath: string,
|
||||||
|
data: any,
|
||||||
|
folder: 'automatic' | 'script' = 'automatic'
|
||||||
|
): Promise<boolean> {
|
||||||
|
await this.initPromise
|
||||||
|
const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir
|
||||||
|
const filePath = join(baseDir, relativePath)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = JSON.stringify(data, null, 2)
|
||||||
|
await fs.writeFile(filePath, content, 'utf-8')
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
this.ctx.logger.warn('FileSystemService: Failed to write JSON file', {
|
||||||
|
path: filePath,
|
||||||
|
error
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async readTextFile(
|
||||||
|
relativePath: string,
|
||||||
|
folder: 'automatic' | 'script' = 'automatic'
|
||||||
|
): Promise<string | null> {
|
||||||
|
await this.initPromise
|
||||||
|
const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir
|
||||||
|
const filePath = join(baseDir, relativePath)
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await fs.readFile(filePath, 'utf-8')
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
this.ctx.logger.warn('FileSystemService: Failed to read text file', { path: filePath, error })
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async writeTextFile(
|
||||||
|
content: string,
|
||||||
|
relativePath: string,
|
||||||
|
folder: 'automatic' | 'script' = 'automatic'
|
||||||
|
): Promise<boolean> {
|
||||||
|
await this.initPromise
|
||||||
|
const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir
|
||||||
|
const filePath = join(baseDir, relativePath)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(filePath, content, 'utf-8')
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
this.ctx.logger.warn('FileSystemService: Failed to write text file', {
|
||||||
|
path: filePath,
|
||||||
|
error
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteFile(
|
||||||
|
relativePath: string,
|
||||||
|
folder: 'automatic' | 'script' = 'automatic'
|
||||||
|
): Promise<boolean> {
|
||||||
|
await this.initPromise
|
||||||
|
const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir
|
||||||
|
const filePath = join(baseDir, relativePath)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.unlink(filePath)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
this.ctx.logger.warn('FileSystemService: Failed to delete file', { path: filePath, error })
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async listFiles(folder: 'automatic' | 'script' = 'automatic'): Promise<ConfigFileInfo[]> {
|
||||||
|
await this.initPromise
|
||||||
|
const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = await fs.readdir(baseDir, { withFileTypes: true })
|
||||||
|
const files: ConfigFileInfo[] = []
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isFile()) {
|
||||||
|
const filePath = join(baseDir, entry.name)
|
||||||
|
const stats = await fs.stat(filePath)
|
||||||
|
files.push({
|
||||||
|
name: entry.name,
|
||||||
|
path: filePath,
|
||||||
|
size: stats.size,
|
||||||
|
modified: stats.mtime
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && 'code' in error && (error as any).code === 'ENOENT') {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
this.ctx.logger.warn('FileSystemService: Failed to list files', { path: baseDir, error })
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fileExists(
|
||||||
|
relativePath: string,
|
||||||
|
folder: 'automatic' | 'script' = 'automatic'
|
||||||
|
): Promise<boolean> {
|
||||||
|
await this.initPromise
|
||||||
|
const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir
|
||||||
|
const filePath = join(baseDir, relativePath)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.access(filePath)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private get mainCtx() {
|
||||||
|
return this.ctx as MainContext
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerIpc(): void {
|
||||||
|
this.mainCtx.handle('fs:getConfigStructure', async () => {
|
||||||
|
await this.initPromise
|
||||||
|
return { success: true, data: this.getConfigStructure() }
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mainCtx.handle(
|
||||||
|
'fs:readJson',
|
||||||
|
async (_event, relativePath: string, folder: 'automatic' | 'script') => {
|
||||||
|
await this.initPromise
|
||||||
|
const data = await this.readJsonFile(relativePath, folder)
|
||||||
|
return { success: true, data }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.mainCtx.handle(
|
||||||
|
'fs:writeJson',
|
||||||
|
async (_event, relativePath: string, data: any, folder: 'automatic' | 'script') => {
|
||||||
|
await this.initPromise
|
||||||
|
const success = await this.writeJsonFile(relativePath, data, folder)
|
||||||
|
return { success }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.mainCtx.handle(
|
||||||
|
'fs:readText',
|
||||||
|
async (_event, relativePath: string, folder: 'automatic' | 'script') => {
|
||||||
|
await this.initPromise
|
||||||
|
const content = await this.readTextFile(relativePath, folder)
|
||||||
|
return { success: true, data: content }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.mainCtx.handle(
|
||||||
|
'fs:writeText',
|
||||||
|
async (_event, content: string, relativePath: string, folder: 'automatic' | 'script') => {
|
||||||
|
await this.initPromise
|
||||||
|
const success = await this.writeTextFile(content, relativePath, folder)
|
||||||
|
return { success }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.mainCtx.handle(
|
||||||
|
'fs:deleteFile',
|
||||||
|
async (_event, relativePath: string, folder: 'automatic' | 'script') => {
|
||||||
|
await this.initPromise
|
||||||
|
const success = await this.deleteFile(relativePath, folder)
|
||||||
|
return { success }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
this.mainCtx.handle('fs:listFiles', async (_event, folder: 'automatic' | 'script') => {
|
||||||
|
await this.initPromise
|
||||||
|
const files = await this.listFiles(folder)
|
||||||
|
return { success: true, data: files }
|
||||||
|
})
|
||||||
|
|
||||||
|
this.mainCtx.handle(
|
||||||
|
'fs:fileExists',
|
||||||
|
async (_event, relativePath: string, folder: 'automatic' | 'script') => {
|
||||||
|
await this.initPromise
|
||||||
|
const exists = await this.fileExists(relativePath, folder)
|
||||||
|
return { success: true, data: exists }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -116,7 +116,9 @@ export class HttpServerService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (attempts === maxAttempts) {
|
if (attempts === maxAttempts) {
|
||||||
throw new Error(`Could not find available port after ${maxAttempts} attempts starting from ${originalPort}`)
|
throw new Error(
|
||||||
|
`Could not find available port after ${maxAttempts} attempts starting from ${originalPort}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attempts > 0) {
|
if (attempts > 0) {
|
||||||
|
|||||||
@@ -88,6 +88,11 @@ export class SettingsService extends Service {
|
|||||||
kind: 'json',
|
kind: 'json',
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
writePermission: 'admin'
|
writePermission: 'admin'
|
||||||
|
},
|
||||||
|
current_theme_id: {
|
||||||
|
kind: 'string',
|
||||||
|
defaultValue: 'light-default',
|
||||||
|
writePermission: 'admin'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,31 @@ export class ThemeService extends Service {
|
|||||||
return this.ctx as MainContext
|
return this.ctx as MainContext
|
||||||
}
|
}
|
||||||
|
|
||||||
public init() {
|
public async init() {
|
||||||
// Already inited in constructor
|
await this.loadSavedTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadSavedTheme() {
|
||||||
|
try {
|
||||||
|
const savedThemeId = await this.mainCtx.settings.getValue('current_theme_id' as any)
|
||||||
|
if (savedThemeId && typeof savedThemeId === 'string') {
|
||||||
|
const themes = this.getThemeList()
|
||||||
|
const exists = themes.some((t) => t.id === savedThemeId)
|
||||||
|
if (exists) {
|
||||||
|
this.currentThemeId = savedThemeId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.mainCtx.logger.warn('Failed to load saved theme', { meta: e })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveCurrentTheme() {
|
||||||
|
try {
|
||||||
|
await this.mainCtx.settings.setValue('current_theme_id' as any, this.currentThemeId)
|
||||||
|
} catch (e) {
|
||||||
|
this.mainCtx.logger.warn('Failed to save theme', { meta: e })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupWatcher() {
|
private setupWatcher() {
|
||||||
@@ -83,6 +106,7 @@ export class ThemeService extends Service {
|
|||||||
return { success: false, message: 'Permission denied' }
|
return { success: false, message: 'Permission denied' }
|
||||||
}
|
}
|
||||||
this.currentThemeId = themeId
|
this.currentThemeId = themeId
|
||||||
|
await this.saveCurrentTheme()
|
||||||
this.applyMicaEffect(themeId)
|
this.applyMicaEffect(themeId)
|
||||||
this.notifyThemeUpdate()
|
this.notifyThemeUpdate()
|
||||||
return { success: true }
|
return { success: true }
|
||||||
@@ -139,6 +163,7 @@ export class ThemeService extends Service {
|
|||||||
return { success: false, message: 'Permission denied' }
|
return { success: false, message: 'Permission denied' }
|
||||||
|
|
||||||
this.currentThemeId = 'custom'
|
this.currentThemeId = 'custom'
|
||||||
|
await this.saveCurrentTheme()
|
||||||
this.applyMicaConfig(config)
|
this.applyMicaConfig(config)
|
||||||
this.notifyThemeUpdate()
|
this.notifyThemeUpdate()
|
||||||
return { success: true }
|
return { success: true }
|
||||||
|
|||||||
@@ -119,6 +119,23 @@ const api = {
|
|||||||
httpServerStop: () => ipcRenderer.invoke('http:server:stop'),
|
httpServerStop: () => ipcRenderer.invoke('http:server:stop'),
|
||||||
httpServerStatus: () => ipcRenderer.invoke('http:server:status'),
|
httpServerStatus: () => ipcRenderer.invoke('http:server:status'),
|
||||||
|
|
||||||
|
// File System
|
||||||
|
fsGetConfigStructure: () => ipcRenderer.invoke('fs:getConfigStructure'),
|
||||||
|
fsReadJson: (relativePath: string, folder?: 'automatic' | 'script') =>
|
||||||
|
ipcRenderer.invoke('fs:readJson', relativePath, folder ?? 'automatic'),
|
||||||
|
fsWriteJson: (relativePath: string, data: any, folder?: 'automatic' | 'script') =>
|
||||||
|
ipcRenderer.invoke('fs:writeJson', relativePath, data, folder ?? 'automatic'),
|
||||||
|
fsReadText: (relativePath: string, folder?: 'automatic' | 'script') =>
|
||||||
|
ipcRenderer.invoke('fs:readText', relativePath, folder ?? 'automatic'),
|
||||||
|
fsWriteText: (content: string, relativePath: string, folder?: 'automatic' | 'script') =>
|
||||||
|
ipcRenderer.invoke('fs:writeText', content, relativePath, folder ?? 'automatic'),
|
||||||
|
fsDeleteFile: (relativePath: string, folder?: 'automatic' | 'script') =>
|
||||||
|
ipcRenderer.invoke('fs:deleteFile', relativePath, folder ?? 'automatic'),
|
||||||
|
fsListFiles: (folder?: 'automatic' | 'script') =>
|
||||||
|
ipcRenderer.invoke('fs:listFiles', folder ?? 'automatic'),
|
||||||
|
fsFileExists: (relativePath: string, folder?: 'automatic' | 'script') =>
|
||||||
|
ipcRenderer.invoke('fs:fileExists', relativePath, folder ?? 'automatic'),
|
||||||
|
|
||||||
// Generic invoke wrapper for backward compatibility with callers using `api.invoke`
|
// Generic invoke wrapper for backward compatibility with callers using `api.invoke`
|
||||||
invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args)
|
invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args)
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-1
@@ -31,10 +31,24 @@ export type settingsSpec = {
|
|||||||
window_radius: 'rounded' | 'small' | 'square'
|
window_radius: 'rounded' | 'small' | 'square'
|
||||||
auto_score_enabled: boolean
|
auto_score_enabled: boolean
|
||||||
auto_score_rules: any[]
|
auto_score_rules: any[]
|
||||||
|
current_theme_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type settingsKey = keyof settingsSpec
|
export type settingsKey = keyof settingsSpec
|
||||||
|
|
||||||
|
export interface ConfigFileInfo {
|
||||||
|
name: string
|
||||||
|
path: string
|
||||||
|
size: number
|
||||||
|
modified: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigFolderStructure {
|
||||||
|
configRoot: string
|
||||||
|
automatic: string
|
||||||
|
sscript: string
|
||||||
|
}
|
||||||
|
|
||||||
export type settingChange<K extends settingsKey = settingsKey> = {
|
export type settingChange<K extends settingsKey = settingsKey> = {
|
||||||
key: K
|
key: K
|
||||||
value: settingsSpec[K]
|
value: settingsSpec[K]
|
||||||
@@ -163,7 +177,11 @@ export interface electronApi {
|
|||||||
registerUrlProtocol: () => Promise<ipcResponse<{ registered?: boolean }>>
|
registerUrlProtocol: () => Promise<ipcResponse<{ registered?: boolean }>>
|
||||||
|
|
||||||
// HTTP Server
|
// HTTP Server
|
||||||
httpServerStart: (config?: { port?: number; host?: string; corsOrigin?: string }) => Promise<
|
httpServerStart: (config?: {
|
||||||
|
port?: number
|
||||||
|
host?: string
|
||||||
|
corsOrigin?: string
|
||||||
|
}) => Promise<
|
||||||
ipcResponse<{ url: string; config: { port: number; host: string; corsOrigin?: string } }>
|
ipcResponse<{ url: string; config: { port: number; host: string; corsOrigin?: string } }>
|
||||||
>
|
>
|
||||||
httpServerStop: () => Promise<ipcResponse<void>>
|
httpServerStop: () => Promise<ipcResponse<void>>
|
||||||
@@ -175,6 +193,33 @@ export interface electronApi {
|
|||||||
}>
|
}>
|
||||||
>
|
>
|
||||||
|
|
||||||
|
// File System
|
||||||
|
fsGetConfigStructure: () => Promise<ipcResponse<ConfigFolderStructure>>
|
||||||
|
fsReadJson: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise<ipcResponse<any>>
|
||||||
|
fsWriteJson: (
|
||||||
|
relativePath: string,
|
||||||
|
data: any,
|
||||||
|
folder?: 'automatic' | 'sscript'
|
||||||
|
) => Promise<ipcResponse<void>>
|
||||||
|
fsReadText: (
|
||||||
|
relativePath: string,
|
||||||
|
folder?: 'automatic' | 'sscript'
|
||||||
|
) => Promise<ipcResponse<string | null>>
|
||||||
|
fsWriteText: (
|
||||||
|
content: string,
|
||||||
|
relativePath: string,
|
||||||
|
folder?: 'automatic' | 'sscript'
|
||||||
|
) => Promise<ipcResponse<void>>
|
||||||
|
fsDeleteFile: (
|
||||||
|
relativePath: string,
|
||||||
|
folder?: 'automatic' | 'sscript'
|
||||||
|
) => Promise<ipcResponse<void>>
|
||||||
|
fsListFiles: (folder?: 'automatic' | 'sscript') => Promise<ipcResponse<ConfigFileInfo[]>>
|
||||||
|
fsFileExists: (
|
||||||
|
relativePath: string,
|
||||||
|
folder?: 'automatic' | 'sscript'
|
||||||
|
) => Promise<ipcResponse<boolean>>
|
||||||
|
|
||||||
// Generic invoke wrapper (minimal compatibility API)
|
// Generic invoke wrapper (minimal compatibility API)
|
||||||
invoke?: (channel: string, ...args: any[]) => Promise<any>
|
invoke?: (channel: string, ...args: any[]) => Promise<any>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,10 +139,72 @@ function MainContent(): React.JSX.Element {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'fixed',
|
||||||
|
display: 'flex',
|
||||||
|
bottom: '2px',
|
||||||
|
left: '20px',
|
||||||
|
opacity: 0.6,
|
||||||
|
zIndex: 9999
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
color: '#de2611',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '13px',
|
||||||
|
pointerEvents: 'none'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
开发中画面,不代表最终品质
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
color: '#44474b',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
fontSize: '13px',
|
||||||
|
paddingLeft: '5px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
SecScore Dev ({getPlatform()}-{getArchitecture()})
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getArchitecture(): string {
|
||||||
|
// 尝试从 userAgent 中获取架构信息
|
||||||
|
const userAgent = navigator.userAgent.toLowerCase()
|
||||||
|
|
||||||
|
if (userAgent.includes('arm64') || userAgent.includes('aarch64')) {
|
||||||
|
return 'ARM64'
|
||||||
|
} else if (userAgent.includes('x64') || userAgent.includes('amd64')) {
|
||||||
|
return 'x64'
|
||||||
|
} else if (userAgent.includes('i386') || userAgent.includes('i686')) {
|
||||||
|
return 'x86'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认返回未知架构
|
||||||
|
return userAgent
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlatform(): string {
|
||||||
|
// 尝试从 userAgent 中获取平台信息
|
||||||
|
const userAgent = navigator.userAgent.toLowerCase()
|
||||||
|
|
||||||
|
if (userAgent.includes('windows')) {
|
||||||
|
return 'Windows'
|
||||||
|
} else if (userAgent.includes('mac')) {
|
||||||
|
return 'Mac'
|
||||||
|
} else if (userAgent.includes('linux')) {
|
||||||
|
return 'Linux'
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unknown'
|
||||||
|
}
|
||||||
function App(): React.JSX.Element {
|
function App(): React.JSX.Element {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { MoveIcon } from 'tdesign-icons-react'
|
import { AddIcon, Delete1Icon, MoveIcon } from 'tdesign-icons-react'
|
||||||
|
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
||||||
|
import { prism } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Form,
|
Form,
|
||||||
@@ -15,7 +17,8 @@ import {
|
|||||||
Popconfirm,
|
Popconfirm,
|
||||||
Radio,
|
Radio,
|
||||||
Select,
|
Select,
|
||||||
TooltipLite
|
TooltipLite,
|
||||||
|
DatePicker
|
||||||
} from 'tdesign-react'
|
} from 'tdesign-react'
|
||||||
|
|
||||||
interface AutoScoreRule {
|
interface AutoScoreRule {
|
||||||
@@ -27,6 +30,7 @@ interface AutoScoreRule {
|
|||||||
scoreValue: number
|
scoreValue: number
|
||||||
reason: string
|
reason: string
|
||||||
lastExecuted?: string
|
lastExecuted?: string
|
||||||
|
triggers?: { event: string; value?: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AutoScoreRuleFormValues {
|
interface AutoScoreRuleFormValues {
|
||||||
@@ -55,7 +59,7 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const authRes = await (window as any).api.authGetStatus()
|
const authRes = await (window as any).api.authGetStatus()
|
||||||
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
||||||
MessagePlugin.error('需要管理员权限以查看自动加分规则,请先登录管理员账号')
|
MessagePlugin.error('需要管理员权限以查看自动加分自动化,请先登录管理员账号')
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -71,14 +75,14 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
if (rulesRes.success) {
|
if (rulesRes.success) {
|
||||||
setRules(rulesRes.data)
|
setRules(rulesRes.data)
|
||||||
} else {
|
} else {
|
||||||
MessagePlugin.error(rulesRes.message || '获取规则失败')
|
MessagePlugin.error(rulesRes.message || '获取自动化失败')
|
||||||
}
|
}
|
||||||
if (studentsRes.success) {
|
if (studentsRes.success) {
|
||||||
setStudents(studentsRes.data)
|
setStudents(studentsRes.data)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch auto score rules:', error)
|
console.error('Failed to fetch auto score rules:', error)
|
||||||
MessagePlugin.error('获取规则失败')
|
MessagePlugin.error('获取自动化失败')
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
@@ -89,20 +93,25 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!(window as any).api) return;
|
if (!(window as any).api) return
|
||||||
|
|
||||||
const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues & { timeUnit: string };
|
const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues & {
|
||||||
|
timeUnit: string
|
||||||
|
}
|
||||||
|
|
||||||
if (!values.name || values.intervalMinutes == null || values.scoreValue == null) {
|
if (!values.name || values.intervalMinutes == null || values.scoreValue == null) {
|
||||||
MessagePlugin.warning('请填写完整信息');
|
MessagePlugin.warning('请填写完整信息')
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据单位转换间隔时间
|
// 根据单位转换间隔时间
|
||||||
const intervalMinutes = values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes;
|
const intervalMinutes =
|
||||||
|
values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes
|
||||||
|
|
||||||
// 确保 studentNames 是数组类型
|
// 确保 studentNames 是数组类型
|
||||||
const studentNames = Array.isArray(values.studentNames) ? values.studentNames : [];
|
const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []
|
||||||
|
|
||||||
|
const triggersPayload = triggerList.map((t) => ({ event: t.eventName, value: t.value }))
|
||||||
|
|
||||||
const ruleData = {
|
const ruleData = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -111,13 +120,23 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
studentNames,
|
studentNames,
|
||||||
scoreValue: values.scoreValue,
|
scoreValue: values.scoreValue,
|
||||||
reason: values.reason || `自动化加分 - ${values.name}`,
|
reason: values.reason || `自动化加分 - ${values.name}`,
|
||||||
};
|
triggers: triggersPayload
|
||||||
|
}
|
||||||
|
|
||||||
// 权限检查:仅管理员可创建/更新规则
|
// 权限检查:仅管理员可创建/更新自动化
|
||||||
try {
|
try {
|
||||||
|
// 验证触发器必填项(如果某个触发器需要 value,则确保已填写)
|
||||||
|
for (const t of triggerList) {
|
||||||
|
const def = allTriggers.find((a) => a.eventName === t.eventName)
|
||||||
|
if (def?.valueType && (!t.value || String(t.value).trim() === '')) {
|
||||||
|
MessagePlugin.warning(`触发器 ${def.label} 需要填写 value`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const authRes = await (window as any).api.authGetStatus()
|
const authRes = await (window as any).api.authGetStatus()
|
||||||
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
||||||
MessagePlugin.error('需要管理员权限以创建或更新自动加分规则')
|
MessagePlugin.error('需要管理员权限以创建或更新自动加分自动化')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -127,18 +146,18 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
let res
|
let res
|
||||||
if (editingRuleId !== null) {
|
if (editingRuleId !== null) {
|
||||||
// 更新现有规则
|
// 更新现有自动化
|
||||||
res = await (window as any).api.invoke('auto-score:updateRule', {
|
res = await (window as any).api.invoke('auto-score:updateRule', {
|
||||||
id: editingRuleId,
|
id: editingRuleId,
|
||||||
...ruleData
|
...ruleData
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// 创建新规则
|
// 创建新自动化
|
||||||
res = await (window as any).api.invoke('auto-score:addRule', ruleData)
|
res = await (window as any).api.invoke('auto-score:addRule', ruleData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
MessagePlugin.success(editingRuleId !== null ? '规则更新成功' : '规则创建成功')
|
MessagePlugin.success(editingRuleId !== null ? '自动化更新成功' : '自动化创建成功')
|
||||||
// 手动清空表单字段,避免 form.reset() 导致的栈溢出
|
// 手动清空表单字段,避免 form.reset() 导致的栈溢出
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -149,13 +168,16 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
timeUnit: 'minutes'
|
timeUnit: 'minutes'
|
||||||
})
|
})
|
||||||
setEditingRuleId(null)
|
setEditingRuleId(null)
|
||||||
fetchRules() // 刷新规则列表
|
setTriggerList([])
|
||||||
|
fetchRules() // 刷新自动化列表
|
||||||
} else {
|
} else {
|
||||||
MessagePlugin.error(res.message || (editingRuleId !== null ? '更新规则失败' : '创建规则失败'))
|
MessagePlugin.error(
|
||||||
|
res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to submit auto score rule:', error)
|
console.error('Failed to submit auto score rule:', error)
|
||||||
MessagePlugin.error(editingRuleId !== null ? '更新规则失败' : '创建规则失败')
|
MessagePlugin.error(editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const handleEdit = (rule: AutoScoreRule) => {
|
const handleEdit = (rule: AutoScoreRule) => {
|
||||||
@@ -167,6 +189,21 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
scoreValue: rule.scoreValue,
|
scoreValue: rule.scoreValue,
|
||||||
reason: rule.reason
|
reason: rule.reason
|
||||||
})
|
})
|
||||||
|
// 如果后端返回了 triggers 字段,把它加载到 triggerList
|
||||||
|
if (rule.triggers && Array.isArray(rule.triggers)) {
|
||||||
|
const mapped = rule.triggers.map((t, idx) => {
|
||||||
|
const found = allTriggers.find((a) => a.eventName === t.event)
|
||||||
|
return {
|
||||||
|
id: idx + 1,
|
||||||
|
eventName: t.event,
|
||||||
|
haveValue: !!found?.valueType,
|
||||||
|
value: t.value ?? ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setTriggerList(mapped)
|
||||||
|
} else {
|
||||||
|
setTriggerList([])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async (ruleId: number) => {
|
const handleDelete = async (ruleId: number) => {
|
||||||
@@ -175,7 +212,7 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const authRes = await (window as any).api.authGetStatus()
|
const authRes = await (window as any).api.authGetStatus()
|
||||||
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
||||||
MessagePlugin.error('需要管理员权限以删除自动加分规则')
|
MessagePlugin.error('需要管理员权限以删除自动加分自动化')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -185,14 +222,14 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const res = await (window as any).api.invoke('auto-score:deleteRule', ruleId)
|
const res = await (window as any).api.invoke('auto-score:deleteRule', ruleId)
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
MessagePlugin.success('规则删除成功')
|
MessagePlugin.success('自动化删除成功')
|
||||||
fetchRules() // 刷新规则列表
|
fetchRules() // 刷新自动化列表
|
||||||
} else {
|
} else {
|
||||||
MessagePlugin.error(res.message || '删除规则失败')
|
MessagePlugin.error(res.message || '删除自动化失败')
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete auto score rule:', error)
|
console.error('Failed to delete auto score rule:', error)
|
||||||
MessagePlugin.error('删除规则失败')
|
MessagePlugin.error('删除自动化失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +239,7 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const authRes = await (window as any).api.authGetStatus()
|
const authRes = await (window as any).api.authGetStatus()
|
||||||
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
||||||
MessagePlugin.error('需要管理员权限以启用/禁用自动加分规则')
|
MessagePlugin.error('需要管理员权限以启用/禁用自动加分自动化')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -212,14 +249,14 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const res = await (window as any).api.invoke('auto-score:toggleRule', { ruleId, enabled })
|
const res = await (window as any).api.invoke('auto-score:toggleRule', { ruleId, enabled })
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
MessagePlugin.success(enabled ? '规则已启用' : '规则已禁用')
|
MessagePlugin.success(enabled ? '自动化已启用' : '自动化已禁用')
|
||||||
fetchRules() // 刷新规则列表
|
fetchRules() // 刷新自动化列表
|
||||||
} else {
|
} else {
|
||||||
MessagePlugin.error(res.message || (enabled ? '启用规则失败' : '禁用规则失败'))
|
MessagePlugin.error(res.message || (enabled ? '启用自动化失败' : '禁用自动化失败'))
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to toggle auto score rule:', error)
|
console.error('Failed to toggle auto score rule:', error)
|
||||||
MessagePlugin.error(enabled ? '启用规则失败' : '禁用规则失败')
|
MessagePlugin.error(enabled ? '启用自动化失败' : '禁用自动化失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +291,7 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{ colKey: 'name', title: '规则名称', width: 150 },
|
{ colKey: 'name', title: '自动化名称', width: 150 },
|
||||||
{
|
{
|
||||||
colKey: 'intervalMinutes',
|
colKey: 'intervalMinutes',
|
||||||
title: '间隔',
|
title: '间隔',
|
||||||
@@ -286,12 +323,7 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
}
|
}
|
||||||
const studentList = row.studentNames.join(',\n')
|
const studentList = row.studentNames.join(',\n')
|
||||||
return (
|
return (
|
||||||
<TooltipLite
|
<TooltipLite content={studentList} showArrow placement="mouse" theme="default">
|
||||||
content={studentList}
|
|
||||||
showArrow
|
|
||||||
placement="mouse"
|
|
||||||
theme="default"
|
|
||||||
>
|
|
||||||
{row.studentNames.length} 名学生
|
{row.studentNames.length} 名学生
|
||||||
</TooltipLite>
|
</TooltipLite>
|
||||||
)
|
)
|
||||||
@@ -318,17 +350,10 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
width: 150,
|
width: 150,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Space>
|
<Space>
|
||||||
<Button
|
<Button size="small" variant="outline" onClick={() => handleEdit(row)}>
|
||||||
size="small"
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => handleEdit(row)}
|
|
||||||
>
|
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Popconfirm
|
<Popconfirm content="确定要删除这条自动化吗?" onConfirm={() => handleDelete(row.id)}>
|
||||||
content="确定要删除这条规则吗?"
|
|
||||||
onConfirm={() => handleDelete(row.id)}
|
|
||||||
>
|
|
||||||
<Button size="small" variant="outline" theme="danger">
|
<Button size="small" variant="outline" theme="danger">
|
||||||
删除
|
删除
|
||||||
</Button>
|
</Button>
|
||||||
@@ -337,24 +362,131 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
const onDragSort = (params: any) => setRules(params.newData)
|
||||||
|
|
||||||
const onDragSort = (params: any) => setRules(params.newData);
|
type TriggerDef = {
|
||||||
|
id: number
|
||||||
|
label: string
|
||||||
|
description: string
|
||||||
|
eventName: string
|
||||||
|
valueType?: 'DatePicker' | 'Input'
|
||||||
|
}
|
||||||
|
|
||||||
|
const allTriggers: TriggerDef[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
label: '根据间隔时间触发',
|
||||||
|
description: '当学生注册时触发自动化',
|
||||||
|
eventName: 'interval_time_passed',
|
||||||
|
valueType: 'DatePicker'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
label: '按照学生标签触发',
|
||||||
|
description: '当学生完成作业时触发自动化',
|
||||||
|
eventName: 'student_tag_matched',
|
||||||
|
valueType: 'Input'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
label: '随机时间触发',
|
||||||
|
description: '当随机时间到达时触发自动化',
|
||||||
|
eventName: 'random_time_reached'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const triggerOptions = allTriggers.map((t) => ({ label: t.label, value: t.eventName }))
|
||||||
|
|
||||||
|
// triggerList items only need id, eventName, haveValue and value
|
||||||
|
type TriggerItem = {
|
||||||
|
id: number
|
||||||
|
eventName: string
|
||||||
|
value?: string
|
||||||
|
valueType?: 'DatePicker' | 'Input'
|
||||||
|
}
|
||||||
|
const [triggerList, setTriggerList] = useState<TriggerItem[]>([])
|
||||||
|
|
||||||
|
const handleTriggerChange = (id: number, value: string) => {
|
||||||
|
const found = allTriggers.find((a) => a.eventName === value)
|
||||||
|
setTriggerList((prev) =>
|
||||||
|
prev.map((t) =>
|
||||||
|
t.id === id ? { ...t, eventName: value, valueType: found?.valueType, value: '' } : t
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleValueChange = (id: number, val: string) => {
|
||||||
|
setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, value: val } : t)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteTrigger = (id: number) => {
|
||||||
|
setTriggerList((prev) => prev.filter((t) => t.id !== id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAddTrigger = () => {
|
||||||
|
const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1
|
||||||
|
const defaultTrigger = allTriggers[0]
|
||||||
|
setTriggerList((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
id: nextId,
|
||||||
|
eventName: defaultTrigger.eventName,
|
||||||
|
valueType: defaultTrigger.valueType,
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerItems = triggerList
|
||||||
|
.filter((t) => t.eventName !== null)
|
||||||
|
.map((triggerTest) => (
|
||||||
|
<div key={triggerTest.id} style={{ display: 'flex', gap: 5 }}>
|
||||||
|
<Button
|
||||||
|
theme="default"
|
||||||
|
variant="text"
|
||||||
|
icon={<Delete1Icon strokeWidth={2.4} />}
|
||||||
|
onClick={() => handleDeleteTrigger(triggerTest.id)}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={triggerTest.eventName}
|
||||||
|
style={{ width: '200px' }}
|
||||||
|
options={triggerOptions}
|
||||||
|
placeholder="请选择触发规则"
|
||||||
|
onChange={(value) => handleTriggerChange(triggerTest.id, value as string)}
|
||||||
|
/>
|
||||||
|
{triggerTest.valueType ? (
|
||||||
|
triggerTest.eventName === 'interval_time_passed' ? (
|
||||||
|
<DatePicker
|
||||||
|
placeholder="请选择日期"
|
||||||
|
style={{ width: '150px' }}
|
||||||
|
value={triggerTest.value ? new Date(triggerTest.value) : undefined}
|
||||||
|
onChange={(v) => handleValueChange(triggerTest.id, v ? String(v) : '')}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Input
|
||||||
|
placeholder="请输入Value"
|
||||||
|
style={{ width: '150px' }}
|
||||||
|
value={String(triggerTest.value ?? '')}
|
||||||
|
onChange={(v) =>
|
||||||
|
handleValueChange(triggerTest.id, String((v as any).target?.value ?? v))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '24px' }}>
|
<div style={{ padding: '24px' }}>
|
||||||
<h2 style={{ marginBottom: '24px', color: 'var(--ss-text-main)' }}>自动化加分管理</h2>
|
<h2 style={{ marginBottom: '24px', color: 'var(--ss-text-main)' }}>自动化加分管理</h2>
|
||||||
|
|
||||||
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
||||||
<Form
|
<Form form={form} labelWidth={100} onReset={handleResetForm}>
|
||||||
form={form}
|
|
||||||
labelWidth={100}
|
|
||||||
onReset={handleResetForm}
|
|
||||||
>
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
|
||||||
<Form.FormItem
|
<Form.FormItem
|
||||||
label="规则名称"
|
label="自动化名称"
|
||||||
name="name"
|
name="name"
|
||||||
rules={[{ required: true, message: '请输入规则名称' }]}
|
rules={[{ required: true, message: '请输入自动化名称' }]}
|
||||||
>
|
>
|
||||||
<Input placeholder="例如:每日签到加分" />
|
<Input placeholder="例如:每日签到加分" />
|
||||||
</Form.FormItem>
|
</Form.FormItem>
|
||||||
@@ -366,7 +498,7 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
name="intervalMinutes"
|
name="intervalMinutes"
|
||||||
rules={[
|
rules={[
|
||||||
{ required: true, message: '请输入间隔时间' },
|
{ required: true, message: '请输入间隔时间' },
|
||||||
{ min: 1, message: '间隔时间至少为1分钟' }
|
{ min: 1, message: '间隔时间至少为1分钟/天' }
|
||||||
]}
|
]}
|
||||||
style={{ marginBottom: 0 }}
|
style={{ marginBottom: 0 }}
|
||||||
>
|
>
|
||||||
@@ -389,10 +521,7 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
<InputNumber placeholder="例如:1(每次加1分)" />
|
<InputNumber placeholder="例如:1(每次加1分)" />
|
||||||
</Form.FormItem>
|
</Form.FormItem>
|
||||||
|
|
||||||
<Form.FormItem
|
<Form.FormItem label="适用学生" name="studentNames">
|
||||||
label="适用学生"
|
|
||||||
name="studentNames"
|
|
||||||
>
|
|
||||||
<Select
|
<Select
|
||||||
filterable
|
filterable
|
||||||
multiple
|
multiple
|
||||||
@@ -401,32 +530,22 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Form.FormItem>
|
</Form.FormItem>
|
||||||
|
|
||||||
<Form.FormItem
|
<Form.FormItem label="加分理由" name="reason">
|
||||||
label="加分理由"
|
|
||||||
name="reason"
|
|
||||||
>
|
|
||||||
<Input placeholder="例如:每日签到奖励" />
|
<Input placeholder="例如:每日签到奖励" />
|
||||||
</Form.FormItem>
|
</Form.FormItem>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
|
<div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
|
||||||
<Button
|
<Button theme="primary" onClick={handleSubmit}>
|
||||||
theme="primary"
|
{editingRuleId !== null ? '更新自动化' : '添加自动化'}
|
||||||
onClick={handleSubmit}
|
|
||||||
>
|
|
||||||
{editingRuleId !== null ? '更新规则' : '添加规则'}
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button type="reset" variant="outline">
|
||||||
type="reset"
|
|
||||||
variant="outline"
|
|
||||||
>
|
|
||||||
{editingRuleId !== null ? '取消编辑' : '重置表单'}
|
{editingRuleId !== null ? '取消编辑' : '重置表单'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
||||||
<Card style={{ backgroundColor: 'var(--ss-card-bg)' }}>
|
|
||||||
<Table
|
<Table
|
||||||
data={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
|
data={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -440,19 +559,43 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
pageSize,
|
pageSize,
|
||||||
total: rules.length,
|
total: rules.length,
|
||||||
onChange: (pageInfo) => setCurrentPage(pageInfo.current),
|
onChange: (pageInfo) => setCurrentPage(pageInfo.current),
|
||||||
onPageSizeChange: (size) => setPageSize(size),
|
onPageSizeChange: (size) => setPageSize(size)
|
||||||
}}
|
}}
|
||||||
style={{ color: 'var(--ss-text-main)' }}
|
style={{ color: 'var(--ss-text-main)' }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* <div style={{ marginTop: '24px', padding: '16px', backgroundColor: 'var(--ss-card-bg)', borderRadius: '8px' }}>
|
<Card
|
||||||
|
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
||||||
|
title="当以下事件触发时"
|
||||||
|
headerBordered
|
||||||
|
>
|
||||||
|
<Space style={{ display: 'grid' }}>
|
||||||
|
{triggerItems}
|
||||||
|
<Button
|
||||||
|
theme="default"
|
||||||
|
variant="text"
|
||||||
|
style={{ fontWeight: 'bolder', fontSize: 15 }}
|
||||||
|
icon={<AddIcon strokeWidth={3} />}
|
||||||
|
onClick={handleAddTrigger}
|
||||||
|
>
|
||||||
|
添加触发器
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
||||||
|
<SyntaxHighlighter language="javascript" style={prism} showLineNumbers>
|
||||||
|
println("这是一个示例代码块,展示如何使用自动化加分功能的API接口")
|
||||||
|
</SyntaxHighlighter>
|
||||||
|
</Card>
|
||||||
|
{/* <div style={{ marginTop: '24px', padding: '16px', backgroundColor: 'var(--ss-card-bg)', borderRadius: '8px' }}>
|
||||||
<h3 style={{ marginBottom: '12px', color: 'var(--ss-text-main)' }}>使用说明</h3>
|
<h3 style={{ marginBottom: '12px', color: 'var(--ss-text-main)' }}>使用说明</h3>
|
||||||
<ul style={{ color: 'var(--ss-text-secondary)', lineHeight: '1.6' }}>
|
<ul style={{ color: 'var(--ss-text-secondary)', lineHeight: '1.6' }}>
|
||||||
<li>自动化加分功能会按照设定的时间间隔自动为学生加分</li>
|
<li>自动化加分功能会按照设定的时间间隔自动为学生加分</li>
|
||||||
<li>间隔时间以分钟为单位,例如1440表示每24小时(一天)执行一次</li>
|
<li>间隔时间以分钟为单位,例如1440表示每24小时(一天)执行一次</li>
|
||||||
<li>如果"适用学生"字段为空,则规则适用于所有学生</li>
|
<li>如果"适用学生"字段为空,则自动化适用于所有学生</li>
|
||||||
<li>可以随时启用/禁用规则,不会影响已保存的规则配置</li>
|
<li>可以随时启用/禁用自动化,不会影响已保存的自动化配置</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div> */}
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -140,7 +140,9 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
const values = form.getFieldsValue(true) as any
|
const values = form.getFieldsValue(true) as any
|
||||||
|
|
||||||
// 支持多选学生
|
// 支持多选学生
|
||||||
const studentNames = Array.isArray(values.student_name) ? values.student_name : [values.student_name]
|
const studentNames = Array.isArray(values.student_name)
|
||||||
|
? values.student_name
|
||||||
|
: [values.student_name]
|
||||||
if (!studentNames || studentNames.length === 0 || !values.reason_content) {
|
if (!studentNames || studentNames.length === 0 || !values.reason_content) {
|
||||||
MessagePlugin.warning('请填写完整信息')
|
MessagePlugin.warning('请填写完整信息')
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -111,7 +111,8 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
|||||||
if (change?.key === 'is_wizard_completed')
|
if (change?.key === 'is_wizard_completed')
|
||||||
return { ...prev, is_wizard_completed: change.value }
|
return { ...prev, is_wizard_completed: change.value }
|
||||||
if (change?.key === 'window_zoom') return { ...prev, window_zoom: change.value }
|
if (change?.key === 'window_zoom') return { ...prev, window_zoom: change.value }
|
||||||
if (change?.key === 'auto_score_enabled') return { ...prev, auto_score_enabled: change.value }
|
if (change?.key === 'auto_score_enabled')
|
||||||
|
return { ...prev, auto_score_enabled: change.value }
|
||||||
return prev
|
return prev
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -636,10 +637,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
|||||||
<Form labelWidth={120}>
|
<Form labelWidth={120}>
|
||||||
<Form.FormItem label="服务器状态">
|
<Form.FormItem label="服务器状态">
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<Tag
|
<Tag theme={httpServerStatus?.isRunning ? 'success' : 'default'} variant="light">
|
||||||
theme={httpServerStatus?.isRunning ? 'success' : 'default'}
|
|
||||||
variant="light"
|
|
||||||
>
|
|
||||||
{httpServerStatus?.isRunning ? '运行中' : '已停止'}
|
{httpServerStatus?.isRunning ? '运行中' : '已停止'}
|
||||||
</Tag>
|
</Tag>
|
||||||
{httpServerStatus?.isRunning && (
|
{httpServerStatus?.isRunning && (
|
||||||
@@ -678,7 +676,9 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
|||||||
style={{ width: '200px' }}
|
style={{ width: '200px' }}
|
||||||
disabled={!canAdmin || httpLoading}
|
disabled={!canAdmin || httpLoading}
|
||||||
/>
|
/>
|
||||||
<div style={{ marginTop: '4px', fontSize: '12px', color: 'var(--ss-text-secondary)' }}>
|
<div
|
||||||
|
style={{ marginTop: '4px', fontSize: '12px', color: 'var(--ss-text-secondary)' }}
|
||||||
|
>
|
||||||
* 表示允许所有来源,建议在生产环境中设置具体域名
|
* 表示允许所有来源,建议在生产环境中设置具体域名
|
||||||
</div>
|
</div>
|
||||||
</Form.FormItem>
|
</Form.FormItem>
|
||||||
@@ -713,7 +713,9 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission
|
|||||||
</Form.FormItem>
|
</Form.FormItem>
|
||||||
|
|
||||||
<Form.FormItem label="API端点">
|
<Form.FormItem label="API端点">
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px', fontSize: '12px' }}>
|
<div
|
||||||
|
style={{ display: 'flex', flexDirection: 'column', gap: '4px', fontSize: '12px' }}
|
||||||
|
>
|
||||||
<div>• GET /health - 健康检查</div>
|
<div>• GET /health - 健康检查</div>
|
||||||
<div>• GET /api/students/scores - 获取所有学生分数</div>
|
<div>• GET /api/students/scores - 获取所有学生分数</div>
|
||||||
<div>• GET /api/students/:id/score - 获取单个学生分数</div>
|
<div>• GET /api/students/:id/score - 获取单个学生分数</div>
|
||||||
|
|||||||
@@ -68,7 +68,9 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps):
|
|||||||
|
|
||||||
<div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}>
|
<div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}>
|
||||||
<Menu value={activeMenu} onChange={onMenuChange} style={{ width: '100%', border: 'none' }}>
|
<Menu value={activeMenu} onChange={onMenuChange} style={{ width: '100%', border: 'none' }}>
|
||||||
<Menu.MenuItem value="home" icon={<HomeIcon />}> 主页
|
<Menu.MenuItem value="home" icon={<HomeIcon />}>
|
||||||
|
{' '}
|
||||||
|
主页
|
||||||
</Menu.MenuItem>
|
</Menu.MenuItem>
|
||||||
<Menu.MenuItem value="students" icon={<UserIcon />} disabled={permission !== 'admin'}>
|
<Menu.MenuItem value="students" icon={<UserIcon />} disabled={permission !== 'admin'}>
|
||||||
学生管理
|
学生管理
|
||||||
@@ -76,11 +78,14 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps):
|
|||||||
<Menu.MenuItem value="score" icon={<HistoryIcon />}>
|
<Menu.MenuItem value="score" icon={<HistoryIcon />}>
|
||||||
积分管理
|
积分管理
|
||||||
</Menu.MenuItem>
|
</Menu.MenuItem>
|
||||||
<Menu.MenuItem value="leaderboard" icon={<ViewListIcon />}> 排行榜
|
<Menu.MenuItem value="auto-score" icon={<ReplayIcon />}>
|
||||||
|
自动加分
|
||||||
</Menu.MenuItem>
|
</Menu.MenuItem>
|
||||||
<Menu.MenuItem value="auto-score" icon={<ReplayIcon />}> 自动加分
|
<Menu.MenuItem value="leaderboard" icon={<ViewListIcon />}>
|
||||||
|
排行榜
|
||||||
</Menu.MenuItem>
|
</Menu.MenuItem>
|
||||||
<Menu.MenuItem value="settlements" icon={<HistoryIcon />}> 结算历史
|
<Menu.MenuItem value="settlements" icon={<HistoryIcon />}>
|
||||||
|
结算历史
|
||||||
</Menu.MenuItem>
|
</Menu.MenuItem>
|
||||||
<Menu.MenuItem value="reasons" icon={<RootListIcon />} disabled={permission !== 'admin'}>
|
<Menu.MenuItem value="reasons" icon={<RootListIcon />} disabled={permission !== 'admin'}>
|
||||||
理由管理
|
理由管理
|
||||||
|
|||||||
@@ -382,7 +382,7 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
{tags.length === 0 ? (
|
{tags.length === 0 ? (
|
||||||
<span style={{ color: 'var(--ss-text-secondary)' }}>无标签</span>
|
<span style={{ color: 'var(--ss-text-secondary)' }}>无标签</span>
|
||||||
) : (
|
) : (
|
||||||
tags.slice(0, 3).map(tag => (
|
tags.slice(0, 3).map((tag) => (
|
||||||
<Tag key={tag} theme="primary" size="small">
|
<Tag key={tag} theme="primary" size="small">
|
||||||
{tag}
|
{tag}
|
||||||
</Tag>
|
</Tag>
|
||||||
@@ -449,7 +449,7 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
pageSize,
|
pageSize,
|
||||||
total: data.length,
|
total: data.length,
|
||||||
onChange: (pageInfo) => setCurrentPage(pageInfo.current),
|
onChange: (pageInfo) => setCurrentPage(pageInfo.current),
|
||||||
onPageSizeChange: (size) => setPageSize(size),
|
onPageSizeChange: (size) => setPageSize(size)
|
||||||
}}
|
}}
|
||||||
style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}
|
style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { Dialog, Input, Button, Space, Tag, MessagePlugin } from 'tdesign-react'
|
import { Dialog, Input, Button, Space, Tag, MessagePlugin } from 'tdesign-react'
|
||||||
|
import { useTheme } from '../contexts/ThemeContext' // 导入主题上下文
|
||||||
|
|
||||||
interface Tag {
|
interface Tag {
|
||||||
id: number
|
id: number
|
||||||
@@ -24,7 +25,9 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
|||||||
const [inputValue, setInputValue] = useState('')
|
const [inputValue, setInputValue] = useState('')
|
||||||
const [allTags, setAllTags] = useState<Tag[]>([])
|
const [allTags, setAllTags] = useState<Tag[]>([])
|
||||||
const [selectedTagIds, setSelectedTagIds] = useState<Set<number>>(new Set(initialTagIds))
|
const [selectedTagIds, setSelectedTagIds] = useState<Set<number>>(new Set(initialTagIds))
|
||||||
const [loading, setLoading] = useState(false)
|
const { currentTheme } = useTheme() // 获取当前主题
|
||||||
|
|
||||||
|
const themeMode = currentTheme?.mode || 'light' // 默认为 light
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
@@ -36,7 +39,6 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
|||||||
|
|
||||||
const fetchAllTags = async () => {
|
const fetchAllTags = async () => {
|
||||||
if (!(window as any).api) return
|
if (!(window as any).api) return
|
||||||
setLoading(true)
|
|
||||||
try {
|
try {
|
||||||
const res = await (window as any).api.tagsGetAll()
|
const res = await (window as any).api.tagsGetAll()
|
||||||
if (res.success && res.data) {
|
if (res.success && res.data) {
|
||||||
@@ -45,8 +47,6 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to fetch tags:', e)
|
console.error('Failed to fetch tags:', e)
|
||||||
MessagePlugin.error('获取标签列表失败')
|
MessagePlugin.error('获取标签列表失败')
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,15 +54,15 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
|||||||
const trimmed = inputValue.trim()
|
const trimmed = inputValue.trim()
|
||||||
if (!trimmed) return
|
if (!trimmed) return
|
||||||
|
|
||||||
if (trimmed.length > 50) {
|
if (trimmed.length > 30) {
|
||||||
MessagePlugin.error('标签名称不能超过 50 个字符')
|
MessagePlugin.error('标签名称不能超过 30 个字符')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allTags.some(t => t.name === trimmed)) {
|
if (allTags.some((t) => t.name === trimmed)) {
|
||||||
const existingTag = allTags.find(t => t.name === trimmed)
|
const existingTag = allTags.find((t) => t.name === trimmed)
|
||||||
if (existingTag) {
|
if (existingTag) {
|
||||||
setSelectedTagIds(prev => new Set([...prev, existingTag.id]))
|
setSelectedTagIds((prev) => new Set([...prev, existingTag.id]))
|
||||||
}
|
}
|
||||||
setInputValue('')
|
setInputValue('')
|
||||||
return
|
return
|
||||||
@@ -71,8 +71,8 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
|||||||
try {
|
try {
|
||||||
const res = await (window as any).api.tagsCreate(trimmed)
|
const res = await (window as any).api.tagsCreate(trimmed)
|
||||||
if (res.success && res.data) {
|
if (res.success && res.data) {
|
||||||
setAllTags(prev => [...prev, res.data])
|
setAllTags((prev) => [...prev, res.data])
|
||||||
setSelectedTagIds(prev => new Set([...prev, res.data.id]))
|
setSelectedTagIds((prev) => new Set([...prev, res.data.id]))
|
||||||
setInputValue('')
|
setInputValue('')
|
||||||
} else {
|
} else {
|
||||||
MessagePlugin.error(res.message || '添加标签失败')
|
MessagePlugin.error(res.message || '添加标签失败')
|
||||||
@@ -84,7 +84,7 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleToggleTag = (tagId: number) => {
|
const handleToggleTag = (tagId: number) => {
|
||||||
setSelectedTagIds(prev => {
|
setSelectedTagIds((prev) => {
|
||||||
const newSet = new Set(prev)
|
const newSet = new Set(prev)
|
||||||
if (newSet.has(tagId)) {
|
if (newSet.has(tagId)) {
|
||||||
newSet.delete(tagId)
|
newSet.delete(tagId)
|
||||||
@@ -99,8 +99,8 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
|||||||
try {
|
try {
|
||||||
const res = await (window as any).api.tagsDelete(tagId)
|
const res = await (window as any).api.tagsDelete(tagId)
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setAllTags(prev => prev.filter(t => t.id !== tagId))
|
setAllTags((prev) => prev.filter((t) => t.id !== tagId))
|
||||||
setSelectedTagIds(prev => {
|
setSelectedTagIds((prev) => {
|
||||||
const newSet = new Set(prev)
|
const newSet = new Set(prev)
|
||||||
newSet.delete(tagId)
|
newSet.delete(tagId)
|
||||||
return newSet
|
return newSet
|
||||||
@@ -115,20 +115,20 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = (value: string, context: { e: React.KeyboardEvent<HTMLInputElement> }) => {
|
/* const handleKeyDown = (value: string, context: { e: React.KeyboardEvent<HTMLInputElement> }) => {
|
||||||
if (context.e.key === 'Enter') {
|
if (context.e.key === 'Enter') {
|
||||||
context.e.preventDefault()
|
context.e.preventDefault()
|
||||||
handleAddTag()
|
handleAddTag()
|
||||||
}
|
}
|
||||||
}
|
} */
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
onConfirm(Array.from(selectedTagIds))
|
onConfirm(Array.from(selectedTagIds))
|
||||||
onClose()
|
onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedTags = allTags.filter(t => selectedTagIds.has(t.id))
|
const selectedTags = allTags.filter((t) => selectedTagIds.has(t.id))
|
||||||
const availableTags = allTags.filter(t => !selectedTagIds.has(t.id))
|
const availableTags = allTags.filter((t) => !selectedTagIds.has(t.id))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -149,14 +149,10 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
|||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={setInputValue}
|
onChange={setInputValue}
|
||||||
onEnter={handleAddTag}
|
onEnter={handleAddTag}
|
||||||
/* maxLength={50} */
|
/* maxLength={50} */
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button theme="primary" onClick={handleAddTag} disabled={!inputValue.trim()}>
|
||||||
theme="primary"
|
|
||||||
onClick={handleAddTag}
|
|
||||||
disabled={!inputValue.trim()}
|
|
||||||
>
|
|
||||||
添加
|
添加
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -166,7 +162,14 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
|||||||
<div style={{ fontSize: '14px', color: 'var(--ss-text-secondary)', marginBottom: '8px' }}>
|
<div style={{ fontSize: '14px', color: 'var(--ss-text-secondary)', marginBottom: '8px' }}>
|
||||||
已选标签(点击取消)
|
已选标签(点击取消)
|
||||||
</div>
|
</div>
|
||||||
<div style={{ minHeight: '50px', padding: '12px', backgroundColor: 'var(--ss-card-bg)', borderRadius: '4px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
minHeight: '50px',
|
||||||
|
padding: '12px',
|
||||||
|
backgroundColor: 'var(--ss-card-bg)',
|
||||||
|
borderRadius: '4px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{selectedTags.length === 0 ? (
|
{selectedTags.length === 0 ? (
|
||||||
<span style={{ color: 'var(--ss-text-secondary)' }}>未选择标签</span>
|
<span style={{ color: 'var(--ss-text-secondary)' }}>未选择标签</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -175,7 +178,7 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
|||||||
<Tag
|
<Tag
|
||||||
key={tag.id}
|
key={tag.id}
|
||||||
theme="primary"
|
theme="primary"
|
||||||
variant="light"
|
variant={themeMode === 'dark' ? 'outline' : 'light'} // 根据主题模式动态设置变体
|
||||||
closable
|
closable
|
||||||
onClose={() => handleToggleTag(tag.id)}
|
onClose={() => handleToggleTag(tag.id)}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
@@ -193,7 +196,14 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
|||||||
<div style={{ fontSize: '14px', color: 'var(--ss-text-secondary)', marginBottom: '8px' }}>
|
<div style={{ fontSize: '14px', color: 'var(--ss-text-secondary)', marginBottom: '8px' }}>
|
||||||
可选标签(点击选择)
|
可选标签(点击选择)
|
||||||
</div>
|
</div>
|
||||||
<div style={{ minHeight: '50px', padding: '12px', backgroundColor: 'var(--ss-card-bg)', borderRadius: '4px' }}>
|
<div
|
||||||
|
style={{
|
||||||
|
minHeight: '50px',
|
||||||
|
padding: '12px',
|
||||||
|
backgroundColor: 'var(--ss-card-bg)',
|
||||||
|
borderRadius: '4px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
{availableTags.length === 0 ? (
|
{availableTags.length === 0 ? (
|
||||||
<span style={{ color: 'var(--ss-text-secondary)' }}>无可用标签</span>
|
<span style={{ color: 'var(--ss-text-secondary)' }}>无可用标签</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -202,7 +212,7 @@ export const TagEditorDialog: React.FC<TagEditorDialogProps> = ({
|
|||||||
<Tag
|
<Tag
|
||||||
key={tag.id}
|
key={tag.id}
|
||||||
theme="default"
|
theme="default"
|
||||||
variant="outline"
|
variant={themeMode === 'dark' ? 'light' : 'outline'} // 根据主题模式动态设置变体
|
||||||
closable
|
closable
|
||||||
onClose={() => handleDeleteTag(tag.id)}
|
onClose={() => handleDeleteTag(tag.id)}
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
|
|||||||
Reference in New Issue
Block a user