mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
Merge pull request #12 from NanGua-QWQ/main
refactoring: update AutoScoreManager
This commit is contained in:
+1
-1
@@ -31,8 +31,8 @@
|
||||
"mica-electron": "^1.5.16",
|
||||
"os": "^0.1.2",
|
||||
"pinyin-pro": "^3.27.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"react-router-dom": "^6.28.0",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"tdesign-react": "^1.16.3",
|
||||
"typeorm": "^0.3.27",
|
||||
|
||||
Generated
+3
-179
@@ -33,12 +33,12 @@ importers:
|
||||
pinyin-pro:
|
||||
specifier: ^3.27.0
|
||||
version: 3.27.0
|
||||
prismjs:
|
||||
specifier: ^1.30.0
|
||||
version: 1.30.0
|
||||
react-router-dom:
|
||||
specifier: ^6.28.0
|
||||
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:
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2
|
||||
@@ -211,10 +211,6 @@ packages:
|
||||
resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/runtime@7.28.6':
|
||||
resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
@@ -915,9 +911,6 @@ packages:
|
||||
'@types/fs-extra@9.0.13':
|
||||
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':
|
||||
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
||||
|
||||
@@ -936,9 +929,6 @@ packages:
|
||||
'@types/plist@3.0.5':
|
||||
resolution: {integrity: sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA==}
|
||||
|
||||
'@types/prismjs@1.26.5':
|
||||
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
|
||||
|
||||
'@types/react-dom@19.2.3':
|
||||
resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
|
||||
peerDependencies:
|
||||
@@ -956,12 +946,6 @@ packages:
|
||||
'@types/triple-beam@1.3.5':
|
||||
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':
|
||||
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.
|
||||
@@ -1278,15 +1262,6 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
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:
|
||||
resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
@@ -1366,9 +1341,6 @@ packages:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
comma-separated-tokens@2.0.3:
|
||||
resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
|
||||
|
||||
commander@5.1.0:
|
||||
resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==}
|
||||
engines: {node: '>= 6'}
|
||||
@@ -1451,9 +1423,6 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
decode-named-character-reference@1.3.0:
|
||||
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
|
||||
|
||||
decompress-response@6.0.0:
|
||||
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1776,9 +1745,6 @@ packages:
|
||||
fast-levenshtein@2.0.6:
|
||||
resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
|
||||
|
||||
fault@1.0.4:
|
||||
resolution: {integrity: sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==}
|
||||
|
||||
fd-slicer@1.1.0:
|
||||
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
|
||||
|
||||
@@ -1837,10 +1803,6 @@ packages:
|
||||
resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
format@0.2.2:
|
||||
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
|
||||
engines: {node: '>=0.4.x'}
|
||||
|
||||
forwarded@0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -1999,24 +1961,12 @@ packages:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
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:
|
||||
resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==}
|
||||
|
||||
hermes-parser@0.25.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==}
|
||||
|
||||
@@ -2097,12 +2047,6 @@ packages:
|
||||
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
|
||||
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:
|
||||
resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2135,9 +2079,6 @@ packages:
|
||||
resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-decimal@2.0.1:
|
||||
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
|
||||
|
||||
is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2158,9 +2099,6 @@ packages:
|
||||
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
is-hexadecimal@2.0.1:
|
||||
resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==}
|
||||
|
||||
is-interactive@1.0.0:
|
||||
resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -2339,9 +2277,6 @@ packages:
|
||||
resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
lowlight@1.20.0:
|
||||
resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
@@ -2620,9 +2555,6 @@ packages:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
parse-entities@4.0.2:
|
||||
resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -2723,9 +2655,6 @@ packages:
|
||||
prop-types@15.8.1:
|
||||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
|
||||
|
||||
property-information@7.1.0:
|
||||
resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
|
||||
engines: {node: '>= 0.10'}
|
||||
@@ -2791,12 +2720,6 @@ packages:
|
||||
peerDependencies:
|
||||
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:
|
||||
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
||||
peerDependencies:
|
||||
@@ -2826,9 +2749,6 @@ packages:
|
||||
resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
refractor@5.0.0:
|
||||
resolution: {integrity: sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw==}
|
||||
|
||||
regenerator-runtime@0.14.1:
|
||||
resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==}
|
||||
|
||||
@@ -3035,9 +2955,6 @@ packages:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
space-separated-tokens@2.0.2:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
sprintf-js@1.1.3:
|
||||
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
|
||||
|
||||
@@ -3623,8 +3540,6 @@ snapshots:
|
||||
dependencies:
|
||||
regenerator-runtime: 0.14.1
|
||||
|
||||
'@babel/runtime@7.28.6': {}
|
||||
|
||||
'@babel/template@7.27.2':
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
@@ -4199,10 +4114,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/node': 22.19.5
|
||||
|
||||
'@types/hast@3.0.4':
|
||||
dependencies:
|
||||
'@types/unist': 3.0.3
|
||||
|
||||
'@types/http-cache-semantics@4.0.4': {}
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
@@ -4223,8 +4134,6 @@ snapshots:
|
||||
xmlbuilder: 15.1.1
|
||||
optional: true
|
||||
|
||||
'@types/prismjs@1.26.5': {}
|
||||
|
||||
'@types/react-dom@19.2.3(@types/react@19.2.8)':
|
||||
dependencies:
|
||||
'@types/react': 19.2.8
|
||||
@@ -4241,10 +4150,6 @@ snapshots:
|
||||
|
||||
'@types/triple-beam@1.3.5': {}
|
||||
|
||||
'@types/unist@2.0.11': {}
|
||||
|
||||
'@types/unist@3.0.3': {}
|
||||
|
||||
'@types/uuid@11.0.0':
|
||||
dependencies:
|
||||
uuid: 13.0.0
|
||||
@@ -4687,12 +4592,6 @@ snapshots:
|
||||
ansi-styles: 4.3.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:
|
||||
dependencies:
|
||||
readdirp: 5.0.0
|
||||
@@ -4760,8 +4659,6 @@ snapshots:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
||||
comma-separated-tokens@2.0.3: {}
|
||||
|
||||
commander@5.1.0: {}
|
||||
|
||||
commander@9.5.0:
|
||||
@@ -4828,10 +4725,6 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
decode-named-character-reference@1.3.0:
|
||||
dependencies:
|
||||
character-entities: 2.0.2
|
||||
|
||||
decompress-response@6.0.0:
|
||||
dependencies:
|
||||
mimic-response: 3.1.0
|
||||
@@ -5367,10 +5260,6 @@ snapshots:
|
||||
|
||||
fast-levenshtein@2.0.6: {}
|
||||
|
||||
fault@1.0.4:
|
||||
dependencies:
|
||||
format: 0.2.2
|
||||
|
||||
fd-slicer@1.1.0:
|
||||
dependencies:
|
||||
pend: 1.2.0
|
||||
@@ -5437,8 +5326,6 @@ snapshots:
|
||||
hasown: 2.0.2
|
||||
mime-types: 2.1.35
|
||||
|
||||
format@0.2.2: {}
|
||||
|
||||
forwarded@0.2.0: {}
|
||||
|
||||
frac@1.1.2: {}
|
||||
@@ -5621,28 +5508,12 @@ snapshots:
|
||||
dependencies:
|
||||
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-parser@0.25.1:
|
||||
dependencies:
|
||||
hermes-estree: 0.25.1
|
||||
|
||||
highlight.js@10.7.3: {}
|
||||
|
||||
highlightjs-vue@1.0.0: {}
|
||||
|
||||
hoist-non-react-statics@3.3.2:
|
||||
dependencies:
|
||||
react-is: 16.13.1
|
||||
@@ -5726,13 +5597,6 @@ snapshots:
|
||||
|
||||
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:
|
||||
dependencies:
|
||||
call-bind: 1.0.8
|
||||
@@ -5773,8 +5637,6 @@ snapshots:
|
||||
call-bound: 1.0.4
|
||||
has-tostringtag: 1.0.2
|
||||
|
||||
is-decimal@2.0.1: {}
|
||||
|
||||
is-extglob@2.1.1: {}
|
||||
|
||||
is-finalizationregistry@1.1.1:
|
||||
@@ -5795,8 +5657,6 @@ snapshots:
|
||||
dependencies:
|
||||
is-extglob: 2.1.1
|
||||
|
||||
is-hexadecimal@2.0.1: {}
|
||||
|
||||
is-interactive@1.0.0: {}
|
||||
|
||||
is-map@2.0.3: {}
|
||||
@@ -5965,11 +5825,6 @@ snapshots:
|
||||
|
||||
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@5.1.1:
|
||||
@@ -6253,16 +6108,6 @@ snapshots:
|
||||
dependencies:
|
||||
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: {}
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
@@ -6351,8 +6196,6 @@ snapshots:
|
||||
object-assign: 4.1.1
|
||||
react-is: 16.13.1
|
||||
|
||||
property-information@7.1.0: {}
|
||||
|
||||
proxy-addr@2.0.7:
|
||||
dependencies:
|
||||
forwarded: 0.2.0
|
||||
@@ -6416,16 +6259,6 @@ snapshots:
|
||||
'@remix-run/router': 1.23.2
|
||||
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):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.10
|
||||
@@ -6464,13 +6297,6 @@ snapshots:
|
||||
get-proto: 1.0.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: {}
|
||||
|
||||
regexp.prototype.flags@1.5.4:
|
||||
@@ -6749,8 +6575,6 @@ snapshots:
|
||||
|
||||
source-map@0.6.1: {}
|
||||
|
||||
space-separated-tokens@2.0.2: {}
|
||||
|
||||
sprintf-js@1.1.3:
|
||||
optional: true
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Service } from '../../shared/kernel'
|
||||
import { MainContext } from '../context'
|
||||
import { student } from '../repos/StudentRepository'
|
||||
import { getTriggerLogic } from '../../shared/triggers'
|
||||
|
||||
interface AutoScoreRule {
|
||||
id: number
|
||||
@@ -129,7 +130,6 @@ export class AutoScoreService extends Service {
|
||||
const data = await fs.readJsonFile<AutoScoreRulesFile>(RULES_FILE_NAME, 'automatic')
|
||||
if (data && data.rules) {
|
||||
this.rules = data.rules.map((rule: any) => {
|
||||
// 数据迁移:将旧格式转换为新格式
|
||||
const migratedRule = this.migrateRule(rule)
|
||||
return {
|
||||
...migratedRule,
|
||||
@@ -138,7 +138,6 @@ export class AutoScoreService extends Service {
|
||||
: undefined
|
||||
}
|
||||
})
|
||||
// 如果有数据迁移,保存新格式
|
||||
if (
|
||||
data.rules.some(
|
||||
(rule: any) => rule.intervalMinutes !== undefined || rule.scoreValue !== undefined
|
||||
@@ -159,12 +158,10 @@ export class AutoScoreService extends Service {
|
||||
}
|
||||
|
||||
private migrateRule(rule: any): AutoScoreRule {
|
||||
// 如果已经是新格式,直接返回
|
||||
if (!rule.intervalMinutes && !rule.scoreValue) {
|
||||
return rule
|
||||
}
|
||||
|
||||
// 迁移旧格式到新格式
|
||||
const migratedRule: AutoScoreRule = {
|
||||
id: rule.id,
|
||||
enabled: rule.enabled,
|
||||
@@ -175,7 +172,6 @@ export class AutoScoreService extends Service {
|
||||
actions: rule.actions || []
|
||||
}
|
||||
|
||||
// 将intervalMinutes迁移到triggers
|
||||
if (
|
||||
rule.intervalMinutes &&
|
||||
!migratedRule.triggers?.find((t) => t.event === 'interval_time_passed')
|
||||
@@ -187,7 +183,6 @@ export class AutoScoreService extends Service {
|
||||
})
|
||||
}
|
||||
|
||||
// 将scoreValue和reason迁移到actions
|
||||
if (
|
||||
rule.scoreValue !== undefined &&
|
||||
!migratedRule.actions?.find((a) => a.event === 'add_score')
|
||||
@@ -280,25 +275,28 @@ export class AutoScoreService extends Service {
|
||||
this.timers.delete(rule.id)
|
||||
}
|
||||
|
||||
// 从triggers中读取间隔时间
|
||||
const intervalTrigger = rule.triggers?.find((t) => t.event === 'interval_time_passed')
|
||||
const intervalMinutes = intervalTrigger?.value ? parseInt(intervalTrigger.value, 10) : 0
|
||||
const now = new Date()
|
||||
let delayMs = 0
|
||||
let primaryTrigger: { event: string; value?: string } | undefined
|
||||
|
||||
if (!intervalMinutes || intervalMinutes <= 0) {
|
||||
this.logger.warn(`Rule ${rule.name} has no valid interval time, skipping timer`)
|
||||
for (const trigger of rule.triggers || []) {
|
||||
const logic = getTriggerLogic(trigger.event)
|
||||
if (logic?.calculateNextTime) {
|
||||
const result = logic.calculateNextTime(trigger.value || '', rule.lastExecuted, now)
|
||||
if (delayMs === 0 || result.delayMs < delayMs) {
|
||||
delayMs = result.delayMs
|
||||
primaryTrigger = trigger
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!primaryTrigger) {
|
||||
this.logger.warn(`Rule ${rule.name} has no valid triggers with timing logic, skipping`)
|
||||
return
|
||||
}
|
||||
|
||||
const now = new Date()
|
||||
const intervalMs = intervalMinutes * 60 * 1000
|
||||
|
||||
let delayMs = intervalMs
|
||||
if (rule.lastExecuted) {
|
||||
const timeSinceLastExecution = now.getTime() - rule.lastExecuted.getTime()
|
||||
delayMs = intervalMs - (timeSinceLastExecution % intervalMs)
|
||||
if (timeSinceLastExecution >= intervalMs) {
|
||||
delayMs = 0
|
||||
}
|
||||
if (delayMs < 0) {
|
||||
delayMs = 0
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
@@ -307,21 +305,32 @@ export class AutoScoreService extends Service {
|
||||
}, delayMs)
|
||||
|
||||
this.timers.set(rule.id, timer)
|
||||
this.logger.info(`Rule ${rule.name} scheduled to execute in ${delayMs}ms`)
|
||||
}
|
||||
|
||||
private setRuleInterval(rule: AutoScoreRule) {
|
||||
// 从triggers中读取间隔时间
|
||||
const intervalTrigger = rule.triggers?.find((t) => t.event === 'interval_time_passed')
|
||||
const intervalMinutes = intervalTrigger?.value ? parseInt(intervalTrigger.value, 10) : 0
|
||||
const now = new Date()
|
||||
let minDelayMs = Infinity
|
||||
let primaryTrigger: { event: string; value?: string } | undefined
|
||||
|
||||
if (!intervalMinutes || intervalMinutes <= 0) {
|
||||
for (const trigger of rule.triggers || []) {
|
||||
const logic = getTriggerLogic(trigger.event)
|
||||
if (logic?.calculateNextTime) {
|
||||
const result = logic.calculateNextTime(trigger.value || '', rule.lastExecuted, now)
|
||||
if (result.delayMs < minDelayMs) {
|
||||
minDelayMs = result.delayMs
|
||||
primaryTrigger = trigger
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!primaryTrigger || minDelayMs === Infinity) {
|
||||
return
|
||||
}
|
||||
|
||||
const intervalMs = intervalMinutes * 60 * 1000
|
||||
const timer = setInterval(() => {
|
||||
this.executeRule(rule)
|
||||
}, intervalMs)
|
||||
}, minDelayMs)
|
||||
|
||||
this.timers.set(rule.id, timer)
|
||||
}
|
||||
@@ -331,7 +340,6 @@ export class AutoScoreService extends Service {
|
||||
this.logger.info(`Executing auto score rule: ${rule.name}`)
|
||||
|
||||
const studentRepo = this.mainCtx.students
|
||||
const eventRepo = this.mainCtx.events
|
||||
|
||||
let studentsToScore: student[] = []
|
||||
if (rule.studentNames.length === 0) {
|
||||
@@ -347,17 +355,30 @@ export class AutoScoreService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
// 从actions中读取分数和理由
|
||||
const scoreAction = rule.actions?.find((a) => a.event === 'add_score')
|
||||
const scoreValue = scoreAction?.value ? parseInt(scoreAction.value, 10) : 0
|
||||
const reason = scoreAction?.reason || `自动化加分 - ${rule.name}`
|
||||
for (const trigger of rule.triggers || []) {
|
||||
const logic = getTriggerLogic(trigger.event)
|
||||
if (logic?.check) {
|
||||
const context = {
|
||||
students: studentsToScore,
|
||||
events: [],
|
||||
rule: {
|
||||
id: rule.id,
|
||||
name: rule.name,
|
||||
studentNames: rule.studentNames,
|
||||
triggers: rule.triggers,
|
||||
actions: rule.actions
|
||||
},
|
||||
now: new Date()
|
||||
}
|
||||
const result = logic.check(context, trigger.value || '')
|
||||
if (result.matchedStudents && result.matchedStudents.length > 0) {
|
||||
studentsToScore = result.matchedStudents
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const student of studentsToScore) {
|
||||
await eventRepo.create({
|
||||
student_name: student.name,
|
||||
reason_content: reason,
|
||||
delta: scoreValue
|
||||
})
|
||||
for (const action of rule.actions || []) {
|
||||
await this.executeAction(action, studentsToScore, rule.name)
|
||||
}
|
||||
|
||||
rule.lastExecuted = new Date()
|
||||
@@ -371,6 +392,54 @@ export class AutoScoreService extends Service {
|
||||
}
|
||||
}
|
||||
|
||||
private async executeAction(
|
||||
action: { event: string; value?: string; reason?: string },
|
||||
students: student[],
|
||||
ruleName: string
|
||||
) {
|
||||
const eventRepo = this.mainCtx.events
|
||||
|
||||
switch (action.event) {
|
||||
case 'add_score': {
|
||||
const scoreValue = action.value ? parseInt(action.value, 10) : 0
|
||||
const reason = action.reason || `自动化加分 - ${ruleName}`
|
||||
for (const student of students) {
|
||||
await eventRepo.create({
|
||||
student_name: student.name,
|
||||
reason_content: reason,
|
||||
delta: scoreValue
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'add_tag': {
|
||||
const tagName = action.value
|
||||
if (tagName) {
|
||||
const studentRepo = this.mainCtx.students
|
||||
for (const student of students) {
|
||||
const currentTags = student.tags || []
|
||||
if (!currentTags.includes(tagName)) {
|
||||
await studentRepo.update(student.id, {
|
||||
tags: [...currentTags, tagName]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'send_notification': {
|
||||
this.logger.info(`Notification action: ${action.value}`)
|
||||
break
|
||||
}
|
||||
case 'set_student_status': {
|
||||
this.logger.info(`Set student status action: ${action.value} (not implemented - student type has no status field)`)
|
||||
break
|
||||
}
|
||||
default:
|
||||
this.logger.warn(`Unknown action event: ${action.event}`)
|
||||
}
|
||||
}
|
||||
|
||||
private stopRules() {
|
||||
for (const [timer] of this.timers) {
|
||||
clearTimeout(timer)
|
||||
|
||||
@@ -7,10 +7,12 @@ import { Wizard } from './components/Wizard'
|
||||
import { ThemeProvider } from './contexts/ThemeContext'
|
||||
import { ThemeEditorProvider } from './contexts/ThemeEditorContext'
|
||||
import { ThemeEditor } from './components/ThemeEditor'
|
||||
import { useTheme } from './contexts/ThemeContext';
|
||||
|
||||
function MainContent(): React.JSX.Element {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
if (!(window as any).api) return
|
||||
@@ -153,9 +155,9 @@ function MainContent(): React.JSX.Element {
|
||||
>
|
||||
<p
|
||||
style={{
|
||||
color: '#de2611',
|
||||
color: '#df0000',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '13px',
|
||||
fontSize: '14px',
|
||||
pointerEvents: 'none'
|
||||
}}
|
||||
>
|
||||
@@ -163,7 +165,7 @@ function MainContent(): React.JSX.Element {
|
||||
</p>
|
||||
<p
|
||||
style={{
|
||||
color: '#44474b',
|
||||
color: currentTheme?.mode === 'dark' ? '#fff' : '#44474b',
|
||||
fontWeight: 'bold',
|
||||
fontSize: '13px',
|
||||
paddingLeft: '5px'
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 { allTriggers, allActions, TriggerItem, ActionItem } from '../services/AutoScoreService'
|
||||
import { AddIcon, MoveIcon } from 'tdesign-icons-react'
|
||||
import { allTriggers, allActions, triggerRegistry, actionRegistry } from './com.automatically'
|
||||
import type { TriggerItem, ActionItem } from './com.automatically/types'
|
||||
import TriggerItemComponent from './com.automatically/TriggerItem'
|
||||
import ActionItemComponent from './com.automatically/ActionItem'
|
||||
import {
|
||||
Card,
|
||||
Form,
|
||||
@@ -14,10 +15,10 @@ import {
|
||||
Space,
|
||||
Switch,
|
||||
Popconfirm,
|
||||
Radio,
|
||||
Select,
|
||||
TooltipLite
|
||||
} from 'tdesign-react'
|
||||
import Code from './Code'
|
||||
|
||||
interface AutoScoreRule {
|
||||
id: number
|
||||
@@ -42,6 +43,8 @@ export const AutoScoreManager: React.FC = () => {
|
||||
const [pageSize, setPageSize] = useState<number>(50)
|
||||
const [form] = Form.useForm()
|
||||
const [editingRuleId, setEditingRuleId] = useState<number | null>(null)
|
||||
const [triggerList, setTriggerList] = useState<TriggerItem[]>([])
|
||||
const [actionList, setActionList] = useState<ActionItem[]>([])
|
||||
|
||||
const fetchRules = async () => {
|
||||
if (!(window as any).api) return
|
||||
@@ -93,35 +96,16 @@ export const AutoScoreManager: React.FC = () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证触发器必填项
|
||||
if (triggerList.length === 0) {
|
||||
MessagePlugin.warning('请至少添加一个触发器')
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 验证行动必填项
|
||||
if (actionList.length === 0) {
|
||||
MessagePlugin.warning('请至少添加一个行动')
|
||||
return
|
||||
}
|
||||
|
||||
for (const a of actionList) {
|
||||
const def = allActions.find((action) => action.eventName === a.eventName)
|
||||
if (def?.valueType && (!a.value || String(a.value).trim() === '')) {
|
||||
MessagePlugin.warning(`行动 ${def.label} 需要填写 value`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 确保 studentNames 是数组类型
|
||||
const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []
|
||||
|
||||
const triggersPayload = triggerList.map((t) => ({ event: t.eventName, value: t.value }))
|
||||
@@ -139,7 +123,6 @@ export const AutoScoreManager: React.FC = () => {
|
||||
actions: actionsPayload
|
||||
}
|
||||
|
||||
// 权限检查:仅管理员可创建/更新自动化
|
||||
try {
|
||||
const authRes = await (window as any).api.authGetStatus()
|
||||
if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') {
|
||||
@@ -153,19 +136,16 @@ export const AutoScoreManager: React.FC = () => {
|
||||
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: '',
|
||||
studentNames: ''
|
||||
@@ -173,7 +153,7 @@ export const AutoScoreManager: React.FC = () => {
|
||||
setEditingRuleId(null)
|
||||
setTriggerList([])
|
||||
setActionList([])
|
||||
fetchRules() // 刷新自动化列表
|
||||
fetchRules()
|
||||
} else {
|
||||
MessagePlugin.error(
|
||||
res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')
|
||||
@@ -184,39 +164,30 @@ export const AutoScoreManager: React.FC = () => {
|
||||
MessagePlugin.error(editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = (rule: AutoScoreRule) => {
|
||||
setEditingRuleId(rule.id)
|
||||
form.setFieldsValue({
|
||||
name: rule.name,
|
||||
studentNames: rule.studentNames.join(', ')
|
||||
})
|
||||
// 如果后端返回了 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 ?? ''
|
||||
}
|
||||
})
|
||||
const mapped = rule.triggers.map((t, idx) => ({
|
||||
id: idx + 1,
|
||||
eventName: t.event,
|
||||
value: t.value ?? ''
|
||||
}))
|
||||
setTriggerList(mapped)
|
||||
} else {
|
||||
setTriggerList([])
|
||||
}
|
||||
// 如果后端返回了 actions 字段,把它加载到 actionList
|
||||
if (rule.actions && Array.isArray(rule.actions)) {
|
||||
const mapped = rule.actions.map((a, idx) => {
|
||||
const found = allActions.find((action) => action.eventName === a.event)
|
||||
return {
|
||||
id: idx + 1,
|
||||
eventName: a.event,
|
||||
valueType: found?.valueType,
|
||||
value: a.value ?? '',
|
||||
reason: a.reason
|
||||
}
|
||||
})
|
||||
const mapped = rule.actions.map((a, idx) => ({
|
||||
id: idx + 1,
|
||||
eventName: a.event,
|
||||
value: a.value ?? '',
|
||||
reason: a.reason ?? ''
|
||||
}))
|
||||
setActionList(mapped)
|
||||
} else {
|
||||
setActionList([])
|
||||
@@ -225,7 +196,6 @@ export const AutoScoreManager: React.FC = () => {
|
||||
|
||||
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') {
|
||||
@@ -240,7 +210,7 @@ export const AutoScoreManager: React.FC = () => {
|
||||
const res = await (window as any).api.invoke('auto-score:deleteRule', ruleId)
|
||||
if (res.success) {
|
||||
MessagePlugin.success('自动化删除成功')
|
||||
fetchRules() // 刷新自动化列表
|
||||
fetchRules()
|
||||
} else {
|
||||
MessagePlugin.error(res.message || '删除自动化失败')
|
||||
}
|
||||
@@ -252,7 +222,6 @@ export const AutoScoreManager: React.FC = () => {
|
||||
|
||||
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') {
|
||||
@@ -267,7 +236,7 @@ export const AutoScoreManager: React.FC = () => {
|
||||
const res = await (window as any).api.invoke('auto-score:toggleRule', { ruleId, enabled })
|
||||
if (res.success) {
|
||||
MessagePlugin.success(enabled ? '自动化已启用' : '自动化已禁用')
|
||||
fetchRules() // 刷新自动化列表
|
||||
fetchRules()
|
||||
} else {
|
||||
MessagePlugin.error(res.message || (enabled ? '启用自动化失败' : '禁用自动化失败'))
|
||||
}
|
||||
@@ -278,7 +247,6 @@ export const AutoScoreManager: React.FC = () => {
|
||||
}
|
||||
|
||||
const handleResetForm = () => {
|
||||
// 手动清空表单字段,避免 form.reset() 导致的栈溢出
|
||||
form.setFieldsValue({
|
||||
name: '',
|
||||
studentNames: ''
|
||||
@@ -288,6 +256,73 @@ export const AutoScoreManager: React.FC = () => {
|
||||
setActionList([])
|
||||
}
|
||||
|
||||
const handleAddTrigger = () => {
|
||||
const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1
|
||||
const defaultTrigger = allTriggers.list[0]
|
||||
if (!defaultTrigger) {
|
||||
MessagePlugin.error('没有可用的触发器类型,请检查配置')
|
||||
return
|
||||
}
|
||||
setTriggerList((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: nextId,
|
||||
eventName: defaultTrigger.eventName,
|
||||
value: ''
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
const handleDeleteTrigger = (id: number) => {
|
||||
setTriggerList((prev) => prev.filter((t) => t.id !== id))
|
||||
}
|
||||
|
||||
const handleTriggerChange = (id: number, eventName: string) => {
|
||||
setTriggerList((prev) =>
|
||||
prev.map((t) => (t.id === id ? { ...t, eventName, value: '' } : t))
|
||||
)
|
||||
}
|
||||
|
||||
const handleTriggerValueChange = (id: number, value: string) => {
|
||||
setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, value } : t)))
|
||||
}
|
||||
|
||||
const handleAddAction = () => {
|
||||
const nextId = actionList.length ? Math.max(...actionList.map((a) => a.id)) + 1 : 1
|
||||
const defaultAction = allActions.list[0]
|
||||
if (!defaultAction) {
|
||||
MessagePlugin.error('没有可用的行动类型,请检查配置')
|
||||
return
|
||||
}
|
||||
setActionList((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: nextId,
|
||||
eventName: defaultAction.eventName,
|
||||
value: '',
|
||||
reason: ''
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
const handleDeleteAction = (id: number) => {
|
||||
setActionList((prev) => prev.filter((a) => a.id !== id))
|
||||
}
|
||||
|
||||
const handleActionChange = (id: number, eventName: string) => {
|
||||
setActionList((prev) =>
|
||||
prev.map((a) => (a.id === id ? { ...a, eventName, value: '' } : a))
|
||||
)
|
||||
}
|
||||
|
||||
const handleActionValueChange = (id: number, value: string) => {
|
||||
setActionList((prev) => prev.map((a) => (a.id === id ? { ...a, value } : a)))
|
||||
}
|
||||
|
||||
const handleActionReasonChange = (id: number, reason: string) => {
|
||||
setActionList((prev) => prev.map((a) => (a.id === id ? { ...a, reason } : a)))
|
||||
}
|
||||
|
||||
const columns: PrimaryTableCol<AutoScoreRule>[] = [
|
||||
{
|
||||
colKey: 'drag',
|
||||
@@ -317,7 +352,7 @@ export const AutoScoreManager: React.FC = () => {
|
||||
return <span>无</span>
|
||||
}
|
||||
const triggerLabels = row.triggers.map((t) => {
|
||||
const def = allTriggers.find((tr) => tr.eventName === t.event)
|
||||
const def = triggerRegistry.get(t.event)
|
||||
return def?.label || t.event
|
||||
})
|
||||
return (
|
||||
@@ -341,7 +376,7 @@ export const AutoScoreManager: React.FC = () => {
|
||||
return <span>无</span>
|
||||
}
|
||||
const actionLabels = row.actions.map((a) => {
|
||||
const def = allActions.find((ac) => ac.eventName === a.event)
|
||||
const def = actionRegistry.get(a.event)
|
||||
return def?.label || a.event
|
||||
})
|
||||
return (
|
||||
@@ -404,179 +439,32 @@ export const AutoScoreManager: React.FC = () => {
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
const onDragSort = (params: any) => setRules(params.newData)
|
||||
|
||||
const triggerOptions = allTriggers.map((t) => ({ label: t.label, value: t.eventName }))
|
||||
|
||||
const [triggerList, setTriggerList] = useState<TriggerItem[]>([])
|
||||
const [actionList, setActionList] = useState<ActionItem[]>([])
|
||||
|
||||
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 handleActionChange = (id: number, value: string) => {
|
||||
const found = allActions.find((a) => a.eventName === value)
|
||||
setActionList((prev) =>
|
||||
prev.map((t) =>
|
||||
t.id === id ? { ...t, eventName: value, valueType: found?.valueType, value: '' } : t
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const handleActionValueChange = (id: number, val: string) => {
|
||||
setActionList((prev) => prev.map((t) => (t.id === id ? { ...t, value: val } : t)))
|
||||
}
|
||||
|
||||
const handleDeleteAction = (id: number) => {
|
||||
setActionList((prev) => prev.filter((t) => t.id !== id))
|
||||
}
|
||||
|
||||
const handleAddAction = () => {
|
||||
const nextId = actionList.length ? Math.max(...actionList.map((t) => t.id)) + 1 : 1
|
||||
const defaultAction = allActions[0]
|
||||
setActionList((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: nextId,
|
||||
eventName: defaultAction.eventName,
|
||||
valueType: defaultAction.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
|
||||
? React.createElement(triggerTest.valueType, {
|
||||
placeholder:
|
||||
triggerTest.eventName === 'interval_time_passed'
|
||||
? '请选择日期'
|
||||
: '请输入时间间隔(天)',
|
||||
style: { width: '150px' },
|
||||
value:
|
||||
triggerTest.eventName === 'interval_time_passed'
|
||||
? triggerTest.value
|
||||
? new Date(triggerTest.value)
|
||||
: undefined
|
||||
: String(triggerTest.value ?? ''),
|
||||
onChange: (v: any) => handleValueChange(triggerTest.id, v ? String(v) : '')
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
.map((item) => (
|
||||
<TriggerItemComponent
|
||||
key={item.id}
|
||||
item={item}
|
||||
onDelete={handleDeleteTrigger}
|
||||
onChange={handleTriggerChange}
|
||||
onValueChange={handleTriggerValueChange}
|
||||
/>
|
||||
))
|
||||
|
||||
const actionOptions = allActions.map((a) => ({ label: a.label, value: a.eventName }))
|
||||
|
||||
const actionItems = actionList
|
||||
.filter((a) => a.eventName !== null)
|
||||
.map((action) => (
|
||||
<div key={action.id} style={{ display: 'flex', gap: 5, alignItems: 'center' }}>
|
||||
<Button
|
||||
theme="default"
|
||||
variant="text"
|
||||
icon={<Delete1Icon strokeWidth={2.4} />}
|
||||
onClick={() => handleDeleteAction(action.id)}
|
||||
/>
|
||||
<Select
|
||||
value={action.eventName}
|
||||
style={{ width: '200px' }}
|
||||
options={actionOptions}
|
||||
placeholder="请选择触发行动"
|
||||
onChange={(value) => handleActionChange(action.id, value as string)}
|
||||
/>
|
||||
{(() => {
|
||||
const actionDef = allActions.find((a) => a.eventName === action.eventName)
|
||||
const renderConfig = actionDef?.renderConfig
|
||||
|
||||
// 特殊处理Radio组件
|
||||
if (action.valueType === Radio || renderConfig?.component === Radio) {
|
||||
return (
|
||||
<Radio.Group
|
||||
value={action.value || 'email'}
|
||||
onChange={(v: any) => handleActionValueChange(action.id, v ? String(v) : '')}
|
||||
{...renderConfig?.props}
|
||||
>
|
||||
{renderConfig?.props?.options?.map((option: any) => (
|
||||
<Radio.Button key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Radio.Button>
|
||||
))}
|
||||
</Radio.Group>
|
||||
)
|
||||
} else if (renderConfig?.component) {
|
||||
return React.createElement(renderConfig.component, {
|
||||
value: String(action.value ?? ''),
|
||||
onChange: (v: any) => handleActionValueChange(action.id, v ? String(v) : ''),
|
||||
...renderConfig.props
|
||||
})
|
||||
} else if (action.valueType) {
|
||||
return React.createElement(action.valueType, {
|
||||
placeholder: '请输入Value',
|
||||
style: { width: '150px' },
|
||||
value: String(action.value ?? ''),
|
||||
onChange: (v: any) => handleActionValueChange(action.id, v ? String(v) : '')
|
||||
})
|
||||
}
|
||||
return null
|
||||
})()}
|
||||
{action.eventName === 'add_score' && (
|
||||
<Input
|
||||
placeholder="请输入理由"
|
||||
style={{ width: '150px' }}
|
||||
value={action.reason || ''}
|
||||
onChange={(v: any) => {
|
||||
setActionList((prev) =>
|
||||
prev.map((a) => (a.id === action.id ? { ...a, reason: v } : a))
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
.map((item) => (
|
||||
<ActionItemComponent
|
||||
key={item.id}
|
||||
item={item}
|
||||
onDelete={handleDeleteAction}
|
||||
onChange={handleActionChange}
|
||||
onValueChange={handleActionValueChange}
|
||||
onReasonChange={handleActionReasonChange}
|
||||
/>
|
||||
))
|
||||
|
||||
return (
|
||||
@@ -671,21 +559,36 @@ export const AutoScoreManager: React.FC = () => {
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
||||
<SyntaxHighlighter language="javascript" style={prism} showLineNumbers>
|
||||
println("这是一个示例代码块,展示如何使用自动化加分功能的API接口")
|
||||
</SyntaxHighlighter>
|
||||
<Code
|
||||
code={(() => {
|
||||
if (editingRuleId !== null) {
|
||||
const values = form.getFieldsValue(true) as unknown as AutoScoreRuleFormValues
|
||||
const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []
|
||||
const triggersPayload = triggerList.map((t) => ({ event: t.eventName, value: t.value }))
|
||||
const actionsPayload = actionList.map((a) => ({
|
||||
event: a.eventName,
|
||||
value: a.value,
|
||||
reason: a.reason
|
||||
}))
|
||||
|
||||
const currentRule = {
|
||||
id: editingRuleId,
|
||||
enabled: true,
|
||||
name: values.name || '',
|
||||
studentNames,
|
||||
triggers: triggersPayload,
|
||||
actions: actionsPayload
|
||||
}
|
||||
|
||||
return JSON.stringify(currentRule, null, 2)
|
||||
} else {
|
||||
return JSON.stringify(rules, null, 2)
|
||||
}
|
||||
})()}
|
||||
language={'json'}
|
||||
/>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
import Prism from "prismjs";
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
// 预先导入所有主题
|
||||
import "prismjs/themes/prism-coy.min.css";
|
||||
import "prismjs/themes/prism-okaidia.min.css";
|
||||
|
||||
const Code = ({ code, language }) => {
|
||||
const { currentTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
// 重新高亮代码
|
||||
Prism.highlightAll();
|
||||
}, [currentTheme, code, language]);
|
||||
|
||||
// 根据主题设置不同的类名
|
||||
const isDark = currentTheme?.mode === 'dark';
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
opacity: "0.6",
|
||||
// 根据主题动态设置样式
|
||||
backgroundColor: isDark ? '#282c34' : '#f5f2f0',
|
||||
color: isDark ? '#abb2bf' : '#383a42',
|
||||
padding: '16px',
|
||||
borderRadius: '8px',
|
||||
overflow: 'auto'
|
||||
}}
|
||||
>
|
||||
<pre className={isDark ? 'prism-okaidia' : 'prism-coy'}>
|
||||
<code className={`language-${language}`}>{code}</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Code;
|
||||
@@ -0,0 +1,52 @@
|
||||
import React from 'react'
|
||||
import { Button, Select } from 'tdesign-react'
|
||||
import { Delete1Icon } from 'tdesign-icons-react'
|
||||
import { actionRegistry, allActions } from './registry'
|
||||
import type { ActionItem as ActionItemType } from './types'
|
||||
|
||||
interface ActionItemProps {
|
||||
item: ActionItemType
|
||||
onDelete: (id: number) => void
|
||||
onChange: (id: number, eventName: string) => void
|
||||
onValueChange: (id: number, value: string) => void
|
||||
onReasonChange: (id: number, reason: string) => void
|
||||
}
|
||||
|
||||
const ActionItem: React.FC<ActionItemProps> = ({
|
||||
item,
|
||||
onDelete,
|
||||
onChange,
|
||||
onValueChange,
|
||||
onReasonChange
|
||||
}) => {
|
||||
const definition = actionRegistry.get(item.eventName)
|
||||
const Component = definition?.component
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 5, alignItems: 'center' }}>
|
||||
<Button
|
||||
theme="default"
|
||||
variant="text"
|
||||
icon={<Delete1Icon strokeWidth={2.4} />}
|
||||
onClick={() => onDelete(item.id)}
|
||||
/>
|
||||
<Select
|
||||
value={item.eventName}
|
||||
style={{ width: '200px' }}
|
||||
options={allActions.options}
|
||||
placeholder="请选择触发行动"
|
||||
onChange={(value) => onChange(item.id, value as string)}
|
||||
/>
|
||||
{Component && (
|
||||
<Component
|
||||
value={item.value}
|
||||
reason={item.reason}
|
||||
onChange={(value) => onValueChange(item.id, value)}
|
||||
onReasonChange={(reason) => onReasonChange(item.id, reason)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ActionItem
|
||||
@@ -0,0 +1,43 @@
|
||||
import React from 'react'
|
||||
import { Button, Select } from 'tdesign-react'
|
||||
import { Delete1Icon } from 'tdesign-icons-react'
|
||||
import { triggerRegistry, allTriggers } from './registry'
|
||||
import type { TriggerItem as TriggerItemType } from './types'
|
||||
|
||||
interface TriggerItemProps {
|
||||
item: TriggerItemType
|
||||
onDelete: (id: number) => void
|
||||
onChange: (id: number, eventName: string) => void
|
||||
onValueChange: (id: number, value: string) => void
|
||||
}
|
||||
|
||||
const TriggerItem: React.FC<TriggerItemProps> = ({ item, onDelete, onChange, onValueChange }) => {
|
||||
const definition = triggerRegistry.get(item.eventName)
|
||||
const Component = definition?.component
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 5 }}>
|
||||
<Button
|
||||
theme="default"
|
||||
variant="text"
|
||||
icon={<Delete1Icon strokeWidth={2.4} />}
|
||||
onClick={() => onDelete(item.id)}
|
||||
/>
|
||||
<Select
|
||||
value={item.eventName}
|
||||
style={{ width: '200px' }}
|
||||
options={allTriggers.options}
|
||||
placeholder="请选择触发规则"
|
||||
onChange={(value) => onChange(item.id, value as string)}
|
||||
/>
|
||||
{Component && (
|
||||
<Component
|
||||
value={item.value}
|
||||
onChange={(value) => onValueChange(item.id, value)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TriggerItem
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react'
|
||||
import { Input } from 'tdesign-react'
|
||||
import type { ActionComponentProps } from '../types'
|
||||
|
||||
export const eventName = 'add_score'
|
||||
export const label = '添加分数'
|
||||
export const description = '为学生添加分数'
|
||||
export const hasReason = true
|
||||
|
||||
const AddScoreAction: React.FC<ActionComponentProps> = ({ value, reason, onChange, onReasonChange }) => {
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
placeholder="请输入分数"
|
||||
style={{ width: '150px' }}
|
||||
value={value ?? ''}
|
||||
onChange={(v: any) => onChange(v ? String(v) : '')}
|
||||
/>
|
||||
<Input
|
||||
placeholder="请输入理由"
|
||||
style={{ width: '150px' }}
|
||||
value={reason ?? ''}
|
||||
onChange={(v: any) => onReasonChange?.(v ? String(v) : '')}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddScoreAction
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import { Input } from 'tdesign-react'
|
||||
import type { ActionComponentProps } from '../types'
|
||||
|
||||
export const eventName = 'add_tag'
|
||||
export const label = '添加标签'
|
||||
export const description = '为学生添加标签'
|
||||
export const hasReason = false
|
||||
|
||||
const AddTagAction: React.FC<ActionComponentProps> = ({ value, onChange }) => {
|
||||
return (
|
||||
<Input
|
||||
placeholder="请输入标签"
|
||||
style={{ width: '150px' }}
|
||||
value={value ?? ''}
|
||||
onChange={(v: any) => onChange(v ? String(v) : '')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddTagAction
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
import { Input } from 'tdesign-react'
|
||||
import type { ActionComponentProps } from '../types'
|
||||
|
||||
export const eventName = 'send_notification'
|
||||
export const label = '发送通知'
|
||||
export const description = '向学生发送通知'
|
||||
export const hasReason = false
|
||||
|
||||
const SendNotificationAction: React.FC<ActionComponentProps> = ({ value, onChange }) => {
|
||||
return (
|
||||
<Input
|
||||
placeholder="请输入通知内容"
|
||||
style={{ width: '150px' }}
|
||||
value={value ?? ''}
|
||||
onChange={(v: any) => onChange(v ? String(v) : '')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default SendNotificationAction
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
import { Select } from 'tdesign-react'
|
||||
import type { ActionComponentProps } from '../types'
|
||||
|
||||
export const eventName = 'set_student_status'
|
||||
export const label = '设置学生状态'
|
||||
export const description = '设置学生的状态'
|
||||
export const hasReason = false
|
||||
|
||||
const statusOptions = [
|
||||
{ label: '活跃', value: 'active' },
|
||||
{ label: '不活跃', value: 'inactive' },
|
||||
{ label: '请假', value: 'leave' }
|
||||
]
|
||||
|
||||
const SetStudentStatusAction: React.FC<ActionComponentProps> = ({ value, onChange }) => {
|
||||
return (
|
||||
<Select
|
||||
placeholder="请选择状态"
|
||||
style={{ width: '150px' }}
|
||||
value={value ?? ''}
|
||||
onChange={(v: any) => onChange(v ? String(v) : '')}
|
||||
options={statusOptions}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default SetStudentStatusAction
|
||||
@@ -0,0 +1,70 @@
|
||||
import { actionRegistry } from '../registry'
|
||||
import type { ActionDefinition } from '../types'
|
||||
|
||||
import AddScoreAction, {
|
||||
eventName as addScoreEventName,
|
||||
label as addScoreLabel,
|
||||
description as addScoreDescription,
|
||||
hasReason as addScoreHasReason
|
||||
} from './AddScoreAction'
|
||||
|
||||
import AddTagAction, {
|
||||
eventName as addTagEventName,
|
||||
label as addTagLabel,
|
||||
description as addTagDescription,
|
||||
hasReason as addTagHasReason
|
||||
} from './AddTagAction'
|
||||
|
||||
import SendNotificationAction, {
|
||||
eventName as sendNotificationEventName,
|
||||
label as sendNotificationLabel,
|
||||
description as sendNotificationDescription,
|
||||
hasReason as sendNotificationHasReason
|
||||
} from './SendNotificationAction'
|
||||
|
||||
import SetStudentStatusAction, {
|
||||
eventName as setStudentStatusEventName,
|
||||
label as setStudentStatusLabel,
|
||||
description as setStudentStatusDescription,
|
||||
hasReason as setStudentStatusHasReason
|
||||
} from './SetStudentStatusAction'
|
||||
|
||||
const actionDefinitions: ActionDefinition[] = [
|
||||
{
|
||||
eventName: addScoreEventName,
|
||||
label: addScoreLabel,
|
||||
description: addScoreDescription,
|
||||
component: AddScoreAction,
|
||||
hasReason: addScoreHasReason
|
||||
},
|
||||
{
|
||||
eventName: addTagEventName,
|
||||
label: addTagLabel,
|
||||
description: addTagDescription,
|
||||
component: AddTagAction,
|
||||
hasReason: addTagHasReason
|
||||
},
|
||||
{
|
||||
eventName: sendNotificationEventName,
|
||||
label: sendNotificationLabel,
|
||||
description: sendNotificationDescription,
|
||||
component: SendNotificationAction,
|
||||
hasReason: sendNotificationHasReason
|
||||
},
|
||||
{
|
||||
eventName: setStudentStatusEventName,
|
||||
label: setStudentStatusLabel,
|
||||
description: setStudentStatusDescription,
|
||||
component: SetStudentStatusAction,
|
||||
hasReason: setStudentStatusHasReason
|
||||
}
|
||||
]
|
||||
|
||||
actionDefinitions.forEach((def) => actionRegistry.register(def))
|
||||
|
||||
export {
|
||||
AddScoreAction,
|
||||
AddTagAction,
|
||||
SendNotificationAction,
|
||||
SetStudentStatusAction
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import './triggers'
|
||||
import './actions'
|
||||
|
||||
export { triggerRegistry, actionRegistry, allTriggers, allActions } from './registry'
|
||||
export type {
|
||||
TriggerComponentProps,
|
||||
ActionComponentProps,
|
||||
TriggerDefinition,
|
||||
ActionDefinition,
|
||||
TriggerItem,
|
||||
ActionItem
|
||||
} from './types'
|
||||
export { default as TriggerItemComponent } from './TriggerItem'
|
||||
export { default as ActionItemComponent } from './ActionItem'
|
||||
|
||||
export { IntervalTimeTrigger, StudentTagTrigger, RandomTimeTrigger } from './triggers'
|
||||
export { AddScoreAction, AddTagAction, SendNotificationAction, SetStudentStatusAction } from './actions'
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { TriggerDefinition, ActionDefinition, TriggerLogic } from './types'
|
||||
|
||||
class TriggerRegistry {
|
||||
private triggers: Map<string, TriggerDefinition> = new Map()
|
||||
|
||||
register(definition: TriggerDefinition) {
|
||||
this.triggers.set(definition.eventName, definition)
|
||||
}
|
||||
|
||||
get(eventName: string): TriggerDefinition | undefined {
|
||||
return this.triggers.get(eventName)
|
||||
}
|
||||
|
||||
getAll(): TriggerDefinition[] {
|
||||
return Array.from(this.triggers.values())
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this.getAll().map((t) => ({ label: t.label, value: t.eventName }))
|
||||
}
|
||||
|
||||
getLogic(eventName: string): TriggerLogic | undefined {
|
||||
return this.triggers.get(eventName)?.triggerLogic
|
||||
}
|
||||
}
|
||||
|
||||
class ActionRegistry {
|
||||
private actions: Map<string, ActionDefinition> = new Map()
|
||||
|
||||
register(definition: ActionDefinition) {
|
||||
this.actions.set(definition.eventName, definition)
|
||||
}
|
||||
|
||||
get(eventName: string): ActionDefinition | undefined {
|
||||
return this.actions.get(eventName)
|
||||
}
|
||||
|
||||
getAll(): ActionDefinition[] {
|
||||
return Array.from(this.actions.values())
|
||||
}
|
||||
|
||||
getOptions() {
|
||||
return this.getAll().map((a) => ({ label: a.label, value: a.eventName }))
|
||||
}
|
||||
}
|
||||
|
||||
export const triggerRegistry = new TriggerRegistry()
|
||||
export const actionRegistry = new ActionRegistry()
|
||||
|
||||
export const allTriggers = {
|
||||
get list() {
|
||||
return triggerRegistry.getAll()
|
||||
},
|
||||
get options() {
|
||||
return triggerRegistry.getOptions()
|
||||
}
|
||||
}
|
||||
|
||||
export const allActions = {
|
||||
get list() {
|
||||
return actionRegistry.getAll()
|
||||
},
|
||||
get options() {
|
||||
return actionRegistry.getOptions()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react'
|
||||
import { InputNumber } from 'tdesign-react'
|
||||
import type { TriggerComponentProps } from '../types'
|
||||
|
||||
export const eventName = 'interval_time_passed'
|
||||
export const label = '根据间隔时间触发'
|
||||
export const description = '当间隔时间到达时触发自动化'
|
||||
export const triggerLogic = {
|
||||
eventName,
|
||||
label,
|
||||
description,
|
||||
validate: (value: string) => {
|
||||
const minutes = parseInt(value, 10)
|
||||
if (isNaN(minutes) || minutes <= 0) {
|
||||
return { valid: false, message: '请输入有效的时间间隔(分钟)' }
|
||||
}
|
||||
return { valid: true }
|
||||
}
|
||||
}
|
||||
|
||||
const IntervalTimeTrigger: React.FC<TriggerComponentProps> = ({ value, onChange }) => {
|
||||
const numValue = value ? parseInt(value, 10) : undefined
|
||||
|
||||
const handleChange = (v: any) => {
|
||||
const numV = typeof v === 'number' ? v : (v ? Number(v) : undefined)
|
||||
onChange(numV !== undefined && numV !== null && !isNaN(numV) ? String(numV) : '')
|
||||
}
|
||||
|
||||
return (
|
||||
<InputNumber
|
||||
placeholder="请输入时间间隔(分钟)"
|
||||
style={{ width: '150px' }}
|
||||
value={numValue}
|
||||
onChange={handleChange}
|
||||
min={1}
|
||||
theme="column"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default IntervalTimeTrigger
|
||||
@@ -0,0 +1,77 @@
|
||||
import React from 'react'
|
||||
import { InputNumber, Row, Col } from 'tdesign-react'
|
||||
import type { TriggerComponentProps } from '../types'
|
||||
|
||||
export const eventName = 'random_time'
|
||||
export const label = '随机时间触发'
|
||||
export const description = '在指定时间范围内随机触发自动化'
|
||||
export const triggerLogic = {
|
||||
eventName,
|
||||
label,
|
||||
description,
|
||||
validate: (value: string) => {
|
||||
const minutes = parseInt(value, 10)
|
||||
if (isNaN(minutes) || minutes <= 0) {
|
||||
return { valid: false, message: '请输入有效的时间范围(分钟)' }
|
||||
}
|
||||
return { valid: true }
|
||||
}
|
||||
}
|
||||
|
||||
interface RandomTimeConfig {
|
||||
minHour: number
|
||||
maxHour: number
|
||||
}
|
||||
|
||||
const RandomTimeTrigger: React.FC<TriggerComponentProps> = ({ value, onChange }) => {
|
||||
let config: RandomTimeConfig = { minHour: 9, maxHour: 18 }
|
||||
try {
|
||||
if (value) {
|
||||
const parsed = JSON.parse(value)
|
||||
config = { ...config, ...parsed }
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const handleChange = (key: keyof RandomTimeConfig, v: any) => {
|
||||
const numV = typeof v === 'number' ? v : (v ? Number(v) : undefined)
|
||||
const newConfig = { ...config, [key]: numV ?? (key === 'minHour' ? 0 : 23) }
|
||||
onChange(JSON.stringify(newConfig))
|
||||
}
|
||||
|
||||
return (
|
||||
<Row gutter={8}>
|
||||
<Col>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<span style={{ fontSize: '12px', color: 'var(--ss-text-secondary)' }}>从</span>
|
||||
<InputNumber
|
||||
placeholder="最小小时"
|
||||
style={{ width: '70px' }}
|
||||
value={config.minHour}
|
||||
onChange={(v) => handleChange('minHour', v)}
|
||||
min={0}
|
||||
max={23}
|
||||
theme="column"
|
||||
/>
|
||||
<span style={{ fontSize: '12px', color: 'var(--ss-text-secondary)' }}>时</span>
|
||||
</div>
|
||||
</Col>
|
||||
<Col>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<span style={{ fontSize: '12px', color: 'var(--ss-text-secondary)' }}>到</span>
|
||||
<InputNumber
|
||||
placeholder="最大小时"
|
||||
style={{ width: '70px' }}
|
||||
value={config.maxHour}
|
||||
onChange={(v) => handleChange('maxHour', v)}
|
||||
min={0}
|
||||
max={23}
|
||||
theme="column"
|
||||
/>
|
||||
<span style={{ fontSize: '12px', color: 'var(--ss-text-secondary)' }}>时</span>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
||||
|
||||
export default RandomTimeTrigger
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Select } from 'tdesign-react'
|
||||
import type { TriggerComponentProps } from '../types'
|
||||
|
||||
export const eventName = 'student_tag_added'
|
||||
export const label = '当学生添加标签时触发'
|
||||
export const description = '当学生添加特定标签时触发自动化'
|
||||
export const triggerLogic = {
|
||||
eventName,
|
||||
label,
|
||||
description,
|
||||
validate: (value: string) => {
|
||||
if (!value || value.trim() === '') {
|
||||
return { valid: false, message: '请输入标签名称' }
|
||||
}
|
||||
return { valid: true }
|
||||
}
|
||||
}
|
||||
|
||||
const StudentTagTrigger: React.FC<TriggerComponentProps> = ({ value, onChange }) => {
|
||||
const [tags, setTags] = useState<{ label: string; value: string }[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTags = async () => {
|
||||
try {
|
||||
const res = await (window as any).api.invoke('tag:getAll', {})
|
||||
if (res.success && res.data) {
|
||||
setTags(res.data.map((tag: any) => ({ label: tag.name, value: tag.name })))
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch tags', e)
|
||||
}
|
||||
}
|
||||
fetchTags()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Select
|
||||
placeholder="请选择标签"
|
||||
style={{ width: '150px' }}
|
||||
value={value ?? ''}
|
||||
onChange={(v: any) => onChange(v ? String(v) : '')}
|
||||
options={tags}
|
||||
filterable
|
||||
clearable
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default StudentTagTrigger
|
||||
@@ -0,0 +1,55 @@
|
||||
import { triggerRegistry } from '../registry'
|
||||
import type { TriggerDefinition } from '../types'
|
||||
|
||||
import IntervalTimeTrigger, {
|
||||
eventName as intervalEventName,
|
||||
label as intervalLabel,
|
||||
description as intervalDescription,
|
||||
triggerLogic as intervalTriggerLogic
|
||||
} from './IntervalTimeTrigger'
|
||||
|
||||
import StudentTagTrigger, {
|
||||
eventName as studentTagEventName,
|
||||
label as studentTagLabel,
|
||||
description as studentTagDescription,
|
||||
triggerLogic as studentTagTriggerLogic
|
||||
} from './StudentTagTrigger'
|
||||
|
||||
import RandomTimeTrigger, {
|
||||
eventName as randomTimeEventName,
|
||||
label as randomTimeLabel,
|
||||
description as randomTimeDescription,
|
||||
triggerLogic as randomTimeTriggerLogic
|
||||
} from './RandomTimeTrigger'
|
||||
|
||||
const triggerDefinitions: TriggerDefinition[] = [
|
||||
{
|
||||
eventName: intervalEventName,
|
||||
label: intervalLabel,
|
||||
description: intervalDescription,
|
||||
component: IntervalTimeTrigger,
|
||||
triggerLogic: intervalTriggerLogic
|
||||
},
|
||||
{
|
||||
eventName: studentTagEventName,
|
||||
label: studentTagLabel,
|
||||
description: studentTagDescription,
|
||||
component: StudentTagTrigger,
|
||||
triggerLogic: studentTagTriggerLogic
|
||||
},
|
||||
{
|
||||
eventName: randomTimeEventName,
|
||||
label: randomTimeLabel,
|
||||
description: randomTimeDescription,
|
||||
component: RandomTimeTrigger,
|
||||
triggerLogic: randomTimeTriggerLogic
|
||||
}
|
||||
]
|
||||
|
||||
triggerDefinitions.forEach((def) => triggerRegistry.register(def))
|
||||
|
||||
export {
|
||||
IntervalTimeTrigger,
|
||||
StudentTagTrigger,
|
||||
RandomTimeTrigger
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react'
|
||||
|
||||
export interface TriggerComponentProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
}
|
||||
|
||||
export interface ActionComponentProps {
|
||||
value: string
|
||||
reason: string
|
||||
onChange: (value: string) => void
|
||||
onReasonChange: (reason: string) => void
|
||||
}
|
||||
|
||||
export interface TriggerLogic {
|
||||
eventName: string
|
||||
label: string
|
||||
description: string
|
||||
validate: (value: string) => { valid: boolean; message?: string }
|
||||
calculateNextTime?: (value: string, lastExecuted: Date | undefined, now: Date) => { delayMs: number; nextExecuteTime: Date }
|
||||
check?: (context: any, value: string) => { shouldExecute: boolean; message?: string }
|
||||
}
|
||||
|
||||
export interface TriggerDefinition {
|
||||
eventName: string
|
||||
label: string
|
||||
description: string
|
||||
component: React.FC<TriggerComponentProps>
|
||||
triggerLogic?: TriggerLogic
|
||||
}
|
||||
|
||||
export interface ActionDefinition {
|
||||
eventName: string
|
||||
label: string
|
||||
description: string
|
||||
component: React.FC<ActionComponentProps>
|
||||
hasReason?: boolean
|
||||
}
|
||||
|
||||
export interface TriggerItem {
|
||||
id: number
|
||||
eventName: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface ActionItem {
|
||||
id: number
|
||||
eventName: string
|
||||
value: string
|
||||
reason: string
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { Service } from '../../../shared/kernel'
|
||||
import { ClientContext } from '../ClientContext'
|
||||
import { Input, Select, Radio } from 'tdesign-react'
|
||||
import { allTriggers, allActions } from '../components/com.automatically'
|
||||
import type { TriggerItem, ActionItem } from '../components/com.automatically/types'
|
||||
|
||||
export interface AutoScoreRule {
|
||||
id: number
|
||||
@@ -27,123 +27,8 @@ declare module '../../../shared/kernel' {
|
||||
}
|
||||
}
|
||||
|
||||
type RenderConfig = {
|
||||
component?: React.ElementType
|
||||
props?: Record<string, any>
|
||||
render?: (props: any) => React.ReactNode
|
||||
}
|
||||
|
||||
type TriggerDef = {
|
||||
id: number
|
||||
label: string
|
||||
description: string
|
||||
eventName: string
|
||||
valueType?: React.ElementType
|
||||
renderConfig?: RenderConfig
|
||||
}
|
||||
|
||||
export const allTriggers: TriggerDef[] = [
|
||||
{
|
||||
id: 1,
|
||||
label: '根据间隔时间触发',
|
||||
description: '当学生注册时触发自动化',
|
||||
eventName: 'interval_time_passed',
|
||||
valueType: Input
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: '按照学生标签触发',
|
||||
description: '当学生完成作业时触发自动化',
|
||||
eventName: 'student_tag_matched',
|
||||
valueType: Input
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: '随机时间触发',
|
||||
description: '当随机时间到达时触发自动化',
|
||||
eventName: 'random_time_reached'
|
||||
}
|
||||
]
|
||||
|
||||
// 触发器和行动项类型定义
|
||||
export type TriggerItem = {
|
||||
id: number
|
||||
eventName: string
|
||||
value?: string
|
||||
valueType?: React.ElementType
|
||||
}
|
||||
|
||||
export type ActionItem = {
|
||||
id: number
|
||||
eventName: string
|
||||
value?: string
|
||||
reason?: string
|
||||
valueType?: React.ElementType
|
||||
}
|
||||
|
||||
export const allActions: TriggerDef[] = [
|
||||
{
|
||||
id: 1,
|
||||
label: '添加分数',
|
||||
description: '为学生添加分数',
|
||||
eventName: 'add_score',
|
||||
valueType: Input,
|
||||
renderConfig: {
|
||||
component: Input,
|
||||
props: {
|
||||
placeholder: '请输入分数',
|
||||
style: { width: '150px' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
label: '添加标签',
|
||||
description: '为学生添加标签',
|
||||
eventName: 'add_tag',
|
||||
valueType: Input,
|
||||
renderConfig: {
|
||||
component: Input,
|
||||
props: {
|
||||
placeholder: '请输入标签',
|
||||
style: { width: '150px' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
label: '发送通知',
|
||||
description: '向学生发送通知',
|
||||
eventName: 'send_notification',
|
||||
valueType: Input,
|
||||
renderConfig: {
|
||||
component: Input,
|
||||
props: {
|
||||
placeholder: '请输入通知内容',
|
||||
style: { width: '150px' }
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
label: '设置学生状态',
|
||||
description: '设置学生的状态',
|
||||
eventName: 'set_student_status',
|
||||
valueType: Select,
|
||||
renderConfig: {
|
||||
component: Select,
|
||||
props: {
|
||||
placeholder: '请选择状态',
|
||||
style: { width: '150px' },
|
||||
options: [
|
||||
{ label: '活跃', value: 'active' },
|
||||
{ label: '不活跃', value: 'inactive' },
|
||||
{ label: '请假', value: 'leave' }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
export { allTriggers, allActions }
|
||||
export type { TriggerItem, ActionItem }
|
||||
|
||||
export class AutoScoreService extends Service {
|
||||
constructor(ctx: ClientContext) {
|
||||
@@ -183,22 +68,19 @@ export class AutoScoreService extends Service {
|
||||
return await (window as any).api.invoke('auto-score:getStatus', {})
|
||||
}
|
||||
|
||||
// 触发器管理相关函数
|
||||
createTriggerItem(triggerList: TriggerItem[]): TriggerItem {
|
||||
const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1
|
||||
const defaultTrigger = allTriggers[0]
|
||||
const defaultTrigger = allTriggers.list[0]
|
||||
return {
|
||||
id: nextId,
|
||||
eventName: defaultTrigger.eventName,
|
||||
valueType: defaultTrigger.valueType,
|
||||
value: ''
|
||||
}
|
||||
}
|
||||
|
||||
updateTriggerEvent(triggerList: TriggerItem[], id: number, eventName: string): TriggerItem[] {
|
||||
const found = allTriggers.find((a) => a.eventName === eventName)
|
||||
return triggerList.map((t) =>
|
||||
t.id === id ? { ...t, eventName, valueType: found?.valueType, value: '' } : t
|
||||
t.id === id ? { ...t, eventName, value: '' } : t
|
||||
)
|
||||
}
|
||||
|
||||
@@ -210,22 +92,20 @@ export class AutoScoreService extends Service {
|
||||
return triggerList.filter((t) => t.id !== id)
|
||||
}
|
||||
|
||||
// 行动管理相关函数
|
||||
createActionItem(actionList: ActionItem[]): ActionItem {
|
||||
const nextId = actionList.length ? Math.max(...actionList.map((a) => a.id)) + 1 : 1
|
||||
const defaultAction = allActions[0]
|
||||
const defaultAction = allActions.list[0]
|
||||
return {
|
||||
id: nextId,
|
||||
eventName: defaultAction.eventName,
|
||||
valueType: defaultAction.valueType,
|
||||
value: ''
|
||||
value: '',
|
||||
reason: ''
|
||||
}
|
||||
}
|
||||
|
||||
updateActionEvent(actionList: ActionItem[], id: number, eventName: string): ActionItem[] {
|
||||
const found = allActions.find((a) => a.eventName === eventName)
|
||||
return actionList.map((a) =>
|
||||
a.id === id ? { ...a, eventName, valueType: found?.valueType, value: '' } : a
|
||||
a.id === id ? { ...a, eventName, value: '' } : a
|
||||
)
|
||||
}
|
||||
|
||||
@@ -233,6 +113,10 @@ export class AutoScoreService extends Service {
|
||||
return actionList.map((a) => (a.id === id ? { ...a, value } : a))
|
||||
}
|
||||
|
||||
updateActionReason(actionList: ActionItem[], id: number, reason: string): ActionItem[] {
|
||||
return actionList.map((a) => (a.id === id ? { ...a, reason } : a))
|
||||
}
|
||||
|
||||
deleteActionItem(actionList: ActionItem[], id: number): ActionItem[] {
|
||||
return actionList.filter((a) => a.id !== id)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { TriggerLogic } from './types'
|
||||
|
||||
export const intervalTimeTrigger: TriggerLogic = {
|
||||
eventName: 'interval_time_passed',
|
||||
label: '根据间隔时间触发',
|
||||
description: '当间隔时间到达时触发自动化',
|
||||
|
||||
validate: (value: string) => {
|
||||
const minutes = parseInt(value, 10)
|
||||
if (isNaN(minutes) || minutes <= 0) {
|
||||
return { valid: false, message: '请输入有效的时间间隔(分钟)' }
|
||||
}
|
||||
return { valid: true }
|
||||
},
|
||||
|
||||
calculateNextTime: (value: string, lastExecuted: Date | undefined, now: Date) => {
|
||||
const intervalMinutes = parseInt(value, 10)
|
||||
if (isNaN(intervalMinutes) || intervalMinutes <= 0) {
|
||||
return { delayMs: 0, nextExecuteTime: now }
|
||||
}
|
||||
|
||||
const intervalMs = intervalMinutes * 60 * 1000
|
||||
|
||||
let delayMs = intervalMs
|
||||
if (lastExecuted) {
|
||||
const timeSinceLastExecution = now.getTime() - lastExecuted.getTime()
|
||||
delayMs = intervalMs - (timeSinceLastExecution % intervalMs)
|
||||
if (timeSinceLastExecution >= intervalMs) {
|
||||
delayMs = 0
|
||||
}
|
||||
}
|
||||
|
||||
const nextExecuteTime = new Date(now.getTime() + delayMs)
|
||||
return { delayMs, nextExecuteTime }
|
||||
},
|
||||
|
||||
check: (context, value) => {
|
||||
const result = intervalTimeTrigger.calculateNextTime!(
|
||||
value,
|
||||
context.rule.triggers?.find((t) => t.event === 'interval_time_passed')
|
||||
? context.now
|
||||
: undefined,
|
||||
context.now
|
||||
)
|
||||
return {
|
||||
shouldExecute: result.delayMs === 0,
|
||||
matchedStudents: context.students,
|
||||
nextExecuteTime: result.nextExecuteTime,
|
||||
delayMs: result.delayMs
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { TriggerLogic } from './types'
|
||||
|
||||
export const randomTimeTrigger: TriggerLogic = {
|
||||
eventName: 'random_time_reached',
|
||||
label: '随机时间触发',
|
||||
description: '当随机时间到达时触发自动化',
|
||||
|
||||
validate: (value: string) => {
|
||||
if (!value) {
|
||||
return { valid: true }
|
||||
}
|
||||
try {
|
||||
const config = JSON.parse(value)
|
||||
if (config.minHour !== undefined || config.maxHour !== undefined) {
|
||||
const minHour = config.minHour ?? 0
|
||||
const maxHour = config.maxHour ?? 23
|
||||
if (minHour < 0 || minHour > 23 || maxHour < 0 || maxHour > 23) {
|
||||
return { valid: false, message: '小时范围必须在0-23之间' }
|
||||
}
|
||||
if (minHour > maxHour) {
|
||||
return { valid: false, message: '最小小时不能大于最大小时' }
|
||||
}
|
||||
}
|
||||
return { valid: true }
|
||||
} catch {
|
||||
return { valid: false, message: '配置格式错误' }
|
||||
}
|
||||
},
|
||||
|
||||
calculateNextTime: (value: string, _lastExecuted: Date | undefined, now: Date) => {
|
||||
let config = { minHour: 9, maxHour: 18 }
|
||||
try {
|
||||
if (value) {
|
||||
const parsed = JSON.parse(value)
|
||||
config = { ...config, ...parsed }
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const minHour = config.minHour ?? 0
|
||||
const maxHour = config.maxHour ?? 23
|
||||
|
||||
const randomHour = Math.floor(Math.random() * (maxHour - minHour + 1)) + minHour
|
||||
const randomMinute = Math.floor(Math.random() * 60)
|
||||
|
||||
let targetDate = new Date(now)
|
||||
targetDate.setHours(randomHour, randomMinute, 0, 0)
|
||||
|
||||
if (targetDate.getTime() <= now.getTime()) {
|
||||
targetDate.setDate(targetDate.getDate() + 1)
|
||||
}
|
||||
|
||||
const delayMs = targetDate.getTime() - now.getTime()
|
||||
return { delayMs, nextExecuteTime: targetDate }
|
||||
},
|
||||
|
||||
check: (context, value) => {
|
||||
const result = randomTimeTrigger.calculateNextTime!(value, undefined, context.now)
|
||||
return {
|
||||
shouldExecute: result.delayMs === 0,
|
||||
matchedStudents: context.students,
|
||||
nextExecuteTime: result.nextExecuteTime,
|
||||
delayMs: result.delayMs
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { TriggerLogic } from './types'
|
||||
|
||||
export const studentTagTrigger: TriggerLogic = {
|
||||
eventName: 'student_tag_matched',
|
||||
label: '按照学生标签触发',
|
||||
description: '当学生标签匹配时触发自动化',
|
||||
|
||||
validate: (value: string) => {
|
||||
if (!value || value.trim() === '') {
|
||||
return { valid: false, message: '请输入标签名称' }
|
||||
}
|
||||
return { valid: true }
|
||||
},
|
||||
|
||||
check: (context, value) => {
|
||||
const tagName = value.trim().toLowerCase()
|
||||
const matchedStudents = context.students.filter((student: any) => {
|
||||
if (student.tags && Array.isArray(student.tags)) {
|
||||
return student.tags.some((tag: string) => tag.toLowerCase() === tagName)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
return {
|
||||
shouldExecute: matchedStudents.length > 0,
|
||||
matchedStudents,
|
||||
nextExecuteTime: context.now
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { TriggerLogic } from './types'
|
||||
import { intervalTimeTrigger } from './IntervalTimeTrigger'
|
||||
import { studentTagTrigger } from './StudentTagTrigger'
|
||||
import { randomTimeTrigger } from './RandomTimeTrigger'
|
||||
|
||||
export * from './types'
|
||||
export { intervalTimeTrigger, studentTagTrigger, randomTimeTrigger }
|
||||
|
||||
export const allTriggerLogics: TriggerLogic[] = [
|
||||
intervalTimeTrigger,
|
||||
studentTagTrigger,
|
||||
randomTimeTrigger
|
||||
]
|
||||
|
||||
export function getTriggerLogic(eventName: string): TriggerLogic | undefined {
|
||||
return allTriggerLogics.find((t) => t.eventName === eventName)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
export interface TriggerContext {
|
||||
students: any[]
|
||||
events: any[]
|
||||
rule: {
|
||||
id: number
|
||||
name: string
|
||||
studentNames: string[]
|
||||
triggers?: { event: string; value?: string }[]
|
||||
actions?: { event: string; value?: string; reason?: string }[]
|
||||
}
|
||||
now: Date
|
||||
}
|
||||
|
||||
export interface TriggerResult {
|
||||
shouldExecute: boolean
|
||||
matchedStudents?: any[]
|
||||
nextExecuteTime?: Date
|
||||
delayMs?: number
|
||||
}
|
||||
|
||||
export interface TriggerLogic {
|
||||
eventName: string
|
||||
label: string
|
||||
description: string
|
||||
validate: (value: string) => { valid: boolean; message?: string }
|
||||
calculateNextTime?: (
|
||||
value: string,
|
||||
lastExecuted: Date | undefined,
|
||||
now: Date
|
||||
) => { delayMs: number; nextExecuteTime: Date }
|
||||
check?: (context: TriggerContext, value: string) => TriggerResult
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
||||
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*"],
|
||||
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/shared/**/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["electron-vite/node"],
|
||||
|
||||
Reference in New Issue
Block a user