From 5984b203c380197db8121fb87c73fd84c94e8b20 Mon Sep 17 00:00:00 2001 From: NanGua-QWQ Date: Wed, 11 Feb 2026 22:05:19 +0800 Subject: [PATCH 1/5] =?UTF-8?q?Dev:=E6=96=BD=E5=B7=A5=E4=B8=AD......?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + package.json | 2 + pnpm-lock.yaml | 191 ++++++ src/main/hosting/index.ts | 3 +- src/main/hosting/tokens.ts | 3 +- src/main/index.ts | 35 +- src/main/services/AutoScoreManager.tsx | 547 ++++++++++++++++++ src/main/services/FileSystemService.ts | 248 ++++++++ src/main/services/SettingsService.ts | 5 + src/main/services/ThemeService.ts | 29 +- src/preload/index.ts | 17 + src/preload/types.ts | 24 + src/renderer/src/App.tsx | 33 ++ .../src/components/AutoScoreManager.tsx | 181 +++++- src/renderer/src/components/Sidebar.tsx | 9 +- 15 files changed, 1280 insertions(+), 48 deletions(-) create mode 100644 src/main/services/AutoScoreManager.tsx create mode 100644 src/main/services/FileSystemService.ts diff --git a/.gitignore b/.gitignore index 3b19a82..c5c97f7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ out *.log* logs docs +configs build/* !build/entitlements.mac.plist diff --git a/package.json b/package.json index 8d21411..ad8d215 100644 --- a/package.json +++ b/package.json @@ -28,8 +28,10 @@ "chokidar": "^5.0.0", "express": "^5.2.1", "mica-electron": "^1.5.16", + "os": "^0.1.2", "pinyin-pro": "^3.27.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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5dad6ab..ec08177 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,12 +26,16 @@ importers: mica-electron: specifier: ^1.5.16 version: 1.5.16 + os: {specifier: ^0.1.2, version: 0.1.2} pinyin-pro: specifier: ^3.27.0 version: 3.27.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 @@ -204,6 +208,10 @@ 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'} @@ -904,6 +912,9 @@ 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==} @@ -922,6 +933,9 @@ 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: @@ -939,6 +953,12 @@ 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. @@ -1255,6 +1275,15 @@ 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'} @@ -1334,6 +1363,9 @@ 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'} @@ -1416,6 +1448,9 @@ 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'} @@ -1738,6 +1773,9 @@ 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==} @@ -1796,6 +1834,10 @@ 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'} @@ -1954,12 +1996,24 @@ 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==} @@ -2040,6 +2094,12 @@ 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'} @@ -2072,6 +2132,9 @@ 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'} @@ -2092,6 +2155,9 @@ 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'} @@ -2270,6 +2336,9 @@ 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==} @@ -2518,6 +2587,9 @@ packages: resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} engines: {node: '>=10'} + os@0.1.2: + resolution: {integrity: sha512-ZoXJkvAnljwvc56MbvhtKVWmSkzV712k42Is2mA0+0KTSRakq5XXuXpjZjgAt9ctzl51ojhQWakQQpmOvXWfjQ==} + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -2545,6 +2617,9 @@ 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'} @@ -2626,6 +2701,10 @@ packages: engines: {node: '>=14'} hasBin: true + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + proc-log@5.0.0: resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} engines: {node: ^18.17.0 || >=20.5.0} @@ -2641,6 +2720,9 @@ 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'} @@ -2706,6 +2788,12 @@ 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: @@ -2735,6 +2823,9 @@ 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==} @@ -2941,6 +3032,9 @@ 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==} @@ -3526,6 +3620,8 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 + '@babel/runtime@7.28.6': {} + '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 @@ -4100,6 +4196,10 @@ 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': {} @@ -4120,6 +4220,8 @@ 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 @@ -4136,6 +4238,10 @@ 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 @@ -4578,6 +4684,12 @@ 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 @@ -4645,6 +4757,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + commander@5.1.0: {} commander@9.5.0: @@ -4711,6 +4825,10 @@ 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 @@ -5246,6 +5364,10 @@ 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 @@ -5312,6 +5434,8 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + format@0.2.2: {} + forwarded@0.2.0: {} frac@1.1.2: {} @@ -5494,12 +5618,28 @@ 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 @@ -5583,6 +5723,13 @@ 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 @@ -5623,6 +5770,8 @@ 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: @@ -5643,6 +5792,8 @@ snapshots: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + is-interactive@1.0.0: {} is-map@2.0.3: {} @@ -5811,6 +5962,11 @@ 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: @@ -6068,6 +6224,8 @@ snapshots: strip-ansi: 6.0.1 wcwidth: 1.0.1 + os@0.1.2: {} + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -6092,6 +6250,16 @@ 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: {} @@ -6163,6 +6331,8 @@ snapshots: prettier@3.7.4: {} + prismjs@1.30.0: {} + proc-log@5.0.0: {} progress@2.0.3: {} @@ -6178,6 +6348,8 @@ 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 @@ -6241,6 +6413,16 @@ 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 @@ -6279,6 +6461,13 @@ 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: @@ -6557,6 +6746,8 @@ snapshots: source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} + sprintf-js@1.1.3: optional: true diff --git a/src/main/hosting/index.ts b/src/main/hosting/index.ts index 9a88a30..3529f80 100644 --- a/src/main/hosting/index.ts +++ b/src/main/hosting/index.ts @@ -17,7 +17,8 @@ export { WindowManagerToken, TrayServiceToken, AutoScoreServiceToken, - HttpServerServiceToken + HttpServerServiceToken, + FileSystemServiceToken } from './tokens' export type { appRuntimeContext, diff --git a/src/main/hosting/tokens.ts b/src/main/hosting/tokens.ts index d352cff..7e115b5 100644 --- a/src/main/hosting/tokens.ts +++ b/src/main/hosting/tokens.ts @@ -15,4 +15,5 @@ export const ThemeServiceToken = Symbol.for('secscore.themeService') export const WindowManagerToken = Symbol.for('secscore.windowManager') export const TrayServiceToken = Symbol.for('secscore.trayService') export const AutoScoreServiceToken = Symbol.for('secscore.autoScoreService') -export const HttpServerServiceToken = Symbol.for('secscore.httpServerService') \ No newline at end of file +export const HttpServerServiceToken = Symbol.for('secscore.httpServerService') +export const FileSystemServiceToken = Symbol.for('secscore.fileSystemService') \ No newline at end of file diff --git a/src/main/index.ts b/src/main/index.ts index a9b20ec..129ca4b 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -17,6 +17,7 @@ import { HttpServerService } from './services/HttpServerService' import { WindowManager, type windowManagerOptions } from './services/WindowManager' import { TrayService } from './services/TrayService' import { AutoScoreService } from './services/AutoScoreService' +import { FileSystemService } from './services/FileSystemService' import { StudentRepository } from './repos/StudentRepository' import { ReasonRepository } from './repos/ReasonRepository' import { EventRepository } from './repos/EventRepository' @@ -39,13 +40,15 @@ import { WindowManagerToken, TrayServiceToken, AutoScoreServiceToken, - HttpServerServiceToken + HttpServerServiceToken, + FileSystemServiceToken } from './hosting' type mainAppConfig = { isDev: boolean appRoot: string dataRoot: string + configDir: string logDir: string themeDir: string dbPath: string @@ -184,6 +187,7 @@ app.whenReady().then(async () => { : ensureWritableDir(join(appRoot, 'data'), join(app.getPath('userData'), 'secscore-data')) 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 ? join(process.cwd(), 'themes') : ensureWritableDir(join(appRoot, 'themes'), join(dataRoot, 'themes')) @@ -194,6 +198,7 @@ app.whenReady().then(async () => { appRoot, dataRoot, logDir, + configDir, themeDir, dbPath, window: { @@ -269,6 +274,10 @@ app.whenReady().then(async () => { HttpServerServiceToken, (p) => new HttpServerService(p.get(MainContext)) ) + services.addSingleton( + FileSystemServiceToken, + (p) => new FileSystemService(p.get(MainContext), config.configDir) + ) }) .configure(async (_builderContext, appCtx) => { const services = appCtx.services @@ -285,13 +294,17 @@ app.whenReady().then(async () => { services.get(ReasonRepositoryToken) services.get(EventRepositoryToken) services.get(SettlementRepositoryToken) - services.get(ThemeServiceToken) - services.get(WindowManagerToken) - const tray = services.get(TrayServiceToken) as TrayService - tray.initialize() + const theme = services.get(ThemeServiceToken) as import('./services/ThemeService').ThemeService + await theme.init() + if (!process.env.HEADLESS) { + services.get(WindowManagerToken) + const tray = services.get(TrayServiceToken) as TrayService + tray.initialize() + } const autoScore = services.get(AutoScoreServiceToken) as AutoScoreService autoScore.initialize?.() services.get(HttpServerServiceToken) + services.get(FileSystemServiceToken) }) .configure(async (_builderContext, appCtx) => { const services = appCtx.services @@ -308,10 +321,14 @@ app.whenReady().then(async () => { services.get(ReasonRepositoryToken) services.get(EventRepositoryToken) services.get(SettlementRepositoryToken) - services.get(ThemeServiceToken) - services.get(WindowManagerToken) - const tray = services.get(TrayServiceToken) as TrayService - tray.initialize() + const theme = services.get(ThemeServiceToken) as import('./services/ThemeService').ThemeService + await theme.init() + if (!process.env.HEADLESS) { + services.get(WindowManagerToken) + const tray = services.get(TrayServiceToken) as TrayService + tray.initialize() + } + services.get(FileSystemServiceToken) }) const host = await builder.build() diff --git a/src/main/services/AutoScoreManager.tsx b/src/main/services/AutoScoreManager.tsx new file mode 100644 index 0000000..993ef3c --- /dev/null +++ b/src/main/services/AutoScoreManager.tsx @@ -0,0 +1,547 @@ +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([]) + const [students, setStudents] = useState<{ id: number; name: string }[]>([]) + const [loading, setLoading] = useState(false) + const [currentPage, setCurrentPage] = useState(1) + const [pageSize, setPageSize] = useState(50) + const [form] = Form.useForm() + const [editingRuleId, setEditingRuleId] = useState(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[] = [ + { + colKey: 'drag', + title: '排序', + cell: () => , + width: 60 + }, + { + colKey: 'enabled', + title: '状态', + width: 80, + cell: ({ row }) => ( + 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 }) => ( + 0 ? 'success' : 'danger'} variant="light"> + {row.scoreValue > 0 ? `+${row.scoreValue}` : row.scoreValue} + + ) + }, + { + colKey: 'studentNames', + title: '适用学生', + width: 130, + cell: ({ row }) => { + if (row.studentNames.length === 0) { + return 所有学生 + } + const studentList = row.studentNames.join(',\n') + return ( + + {row.studentNames.length} 名学生 + + ) + } + }, + { colKey: 'reason', title: '理由', width: 130, ellipsis: true }, + { + colKey: 'lastExecuted', + title: '最后执行', + width: 180, + cell: ({ row }) => { + if (!row.lastExecuted) return 未执行 + try { + const date = new Date(row.lastExecuted) + return date.toLocaleString() + } catch { + return 无效时间 + } + } + }, + { + colKey: 'operation', + title: '操作', + width: 150, + cell: ({ row }) => ( + + + handleDelete(row.id)} + > + + + + ) + } + ] + 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) => ( +
+
+ )) + + return ( +
+

自动化加分管理

+ + +
+
+ + + + + + + + + + + + + 分钟 + + + + + + + + + + + + +
+ +
+ + +
+
+
+ + setCurrentPage(pageInfo.current), + onPageSizeChange: (size) => setPageSize(size), + }} + style={{ color: 'var(--ss-text-main)' }} + /> + + + + + {triggerItems} + + + + + + + println("这是一个示例代码块,展示如何使用自动化加分功能的API接口") + + +{/*
+

使用说明

+
    +
  • 自动化加分功能会按照设定的时间间隔自动为学生加分
  • +
  • 间隔时间以分钟为单位,例如1440表示每24小时(一天)执行一次
  • +
  • 如果"适用学生"字段为空,则自动化适用于所有学生
  • +
  • 可以随时启用/禁用自动化,不会影响已保存的自动化配置
  • +
+
*/} + + ) +} \ No newline at end of file diff --git a/src/main/services/FileSystemService.ts b/src/main/services/FileSystemService.ts new file mode 100644 index 0000000..9de74b1 --- /dev/null +++ b/src/main/services/FileSystemService.ts @@ -0,0 +1,248 @@ +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 | 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 { + await this.ensureDirectories() + } + + private async ensureDirectories(): Promise { + 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(relativePath: string, folder: 'automatic' | 'script' = 'automatic'): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 } + }) + } +} diff --git a/src/main/services/SettingsService.ts b/src/main/services/SettingsService.ts index 14ed658..d96a320 100644 --- a/src/main/services/SettingsService.ts +++ b/src/main/services/SettingsService.ts @@ -88,6 +88,11 @@ export class SettingsService extends Service { kind: 'json', defaultValue: [], writePermission: 'admin' + }, + current_theme_id: { + kind: 'string', + defaultValue: 'light-default', + writePermission: 'admin' } } diff --git a/src/main/services/ThemeService.ts b/src/main/services/ThemeService.ts index 0493893..fbcd67f 100644 --- a/src/main/services/ThemeService.ts +++ b/src/main/services/ThemeService.ts @@ -46,8 +46,31 @@ export class ThemeService extends Service { return this.ctx as MainContext } - public init() { - // Already inited in constructor + public async init() { + 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() { @@ -83,6 +106,7 @@ export class ThemeService extends Service { return { success: false, message: 'Permission denied' } } this.currentThemeId = themeId + await this.saveCurrentTheme() this.applyMicaEffect(themeId) this.notifyThemeUpdate() return { success: true } @@ -139,6 +163,7 @@ export class ThemeService extends Service { return { success: false, message: 'Permission denied' } this.currentThemeId = 'custom' + await this.saveCurrentTheme() this.applyMicaConfig(config) this.notifyThemeUpdate() return { success: true } diff --git a/src/preload/index.ts b/src/preload/index.ts index d2aa155..da9b812 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -118,6 +118,23 @@ const api = { ipcRenderer.invoke('http:server:start', config), httpServerStop: () => ipcRenderer.invoke('http:server:stop'), 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` invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args) diff --git a/src/preload/types.ts b/src/preload/types.ts index 4af9e80..52f7b84 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -31,10 +31,24 @@ export type settingsSpec = { window_radius: 'rounded' | 'small' | 'square' auto_score_enabled: boolean auto_score_rules: any[] + current_theme_id: string } 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 = { key: K value: settingsSpec[K] @@ -174,6 +188,16 @@ export interface electronApi { url: string | null }> > + + // File System + fsGetConfigStructure: () => Promise> + fsReadJson: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise> + fsWriteJson: (relativePath: string, data: any, folder?: 'automatic' | 'sscript') => Promise> + fsReadText: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise> + fsWriteText: (content: string, relativePath: string, folder?: 'automatic' | 'sscript') => Promise> + fsDeleteFile: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise> + fsListFiles: (folder?: 'automatic' | 'sscript') => Promise> + fsFileExists: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise> // Generic invoke wrapper (minimal compatibility API) invoke?: (channel: string, ...args: any[]) => Promise diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 75d1618..0ba1f77 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -139,10 +139,43 @@ function MainContent(): React.JSX.Element { /> + {/* 开发中画面水印 */} +

+ 开发中画面,不代表真正品质 ({getArchitecture()}) +

) } +function getArchitecture(): string { + + // 尝试从 userAgent 中获取架构信息 + const userAgent = navigator.userAgent.toLowerCase(); + + if (userAgent.includes('arm64') || userAgent.includes('aarch64')) { + return 'ARM64'; + } else if (userAgent.includes('x86_64') || userAgent.includes('amd64')) { + return 'x86_64'; + } else if (userAgent.includes('i386') || userAgent.includes('i686')) { + return 'x86'; + } + + // 默认返回未知架构 + return 'Unknown'; +} + function App(): React.JSX.Element { return ( diff --git a/src/renderer/src/components/AutoScoreManager.tsx b/src/renderer/src/components/AutoScoreManager.tsx index 2a42a58..38380a0 100644 --- a/src/renderer/src/components/AutoScoreManager.tsx +++ b/src/renderer/src/components/AutoScoreManager.tsx @@ -1,5 +1,7 @@ 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 { Card, Form, @@ -27,6 +29,7 @@ interface AutoScoreRule { scoreValue: number reason: string lastExecuted?: string + triggers?: { event: string; value?: string }[] } interface AutoScoreRuleFormValues { @@ -55,7 +58,7 @@ export const AutoScoreManager: React.FC = () => { try { const authRes = await (window as any).api.authGetStatus() if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') { - MessagePlugin.error('需要管理员权限以查看自动加分规则,请先登录管理员账号') + MessagePlugin.error('需要管理员权限以查看自动加分自动化,请先登录管理员账号') setLoading(false) return } @@ -71,14 +74,14 @@ export const AutoScoreManager: React.FC = () => { if (rulesRes.success) { setRules(rulesRes.data) } else { - MessagePlugin.error(rulesRes.message || '获取规则失败') + MessagePlugin.error(rulesRes.message || '获取自动化失败') } if (studentsRes.success) { setStudents(studentsRes.data) } } catch (error) { console.error('Failed to fetch auto score rules:', error) - MessagePlugin.error('获取规则失败') + MessagePlugin.error('获取自动化失败') } finally { setLoading(false) } @@ -104,6 +107,8 @@ export const AutoScoreManager: React.FC = () => { // 确保 studentNames 是数组类型 const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []; + const triggersPayload = triggerList.map((t) => ({ event: t.eventName, value: t.value })) + const ruleData = { enabled: true, name: values.name, @@ -111,13 +116,23 @@ export const AutoScoreManager: React.FC = () => { studentNames, scoreValue: values.scoreValue, reason: values.reason || `自动化加分 - ${values.name}`, + triggers: triggersPayload }; - // 权限检查:仅管理员可创建/更新规则 + // 权限检查:仅管理员可创建/更新自动化 try { + // 验证触发器必填项(如果某个触发器需要 value,则确保已填写) + for (const t of triggerList) { + const def = allTriggers.find((a) => a.eventName === t.eventName) + if (def?.haveValue && (!t.value || String(t.value).trim() === '')) { + MessagePlugin.warning(`触发器 ${def.label} 需要填写 value`) + return + } + } + const authRes = await (window as any).api.authGetStatus() if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') { - MessagePlugin.error('需要管理员权限以创建或更新自动加分规则') + MessagePlugin.error('需要管理员权限以创建或更新自动加分自动化') return } } catch (e) { @@ -127,18 +142,18 @@ 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 ? '规则更新成功' : '规则创建成功') + MessagePlugin.success(editingRuleId !== null ? '自动化更新成功' : '自动化创建成功') // 手动清空表单字段,避免 form.reset() 导致的栈溢出 form.setFieldsValue({ name: '', @@ -149,13 +164,14 @@ export const AutoScoreManager: React.FC = () => { timeUnit: 'minutes' }) setEditingRuleId(null) - fetchRules() // 刷新规则列表 + setTriggerList([]) + fetchRules() // 刷新自动化列表 } else { - MessagePlugin.error(res.message || (editingRuleId !== null ? '更新规则失败' : '创建规则失败')) + MessagePlugin.error(res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')) } } catch (error) { console.error('Failed to submit auto score rule:', error) - MessagePlugin.error(editingRuleId !== null ? '更新规则失败' : '创建规则失败') + MessagePlugin.error(editingRuleId !== null ? '更新自动化失败' : '创建自动化失败') } } const handleEdit = (rule: AutoScoreRule) => { @@ -167,6 +183,21 @@ export const AutoScoreManager: React.FC = () => { scoreValue: rule.scoreValue, 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?.haveValue, + value: t.value ?? '' + } + }) + setTriggerList(mapped) + } else { + setTriggerList([]) + } } const handleDelete = async (ruleId: number) => { @@ -175,7 +206,7 @@ export const AutoScoreManager: React.FC = () => { try { const authRes = await (window as any).api.authGetStatus() if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') { - MessagePlugin.error('需要管理员权限以删除自动加分规则') + MessagePlugin.error('需要管理员权限以删除自动加分自动化') return } } catch (e) { @@ -185,14 +216,14 @@ export const AutoScoreManager: React.FC = () => { try { const res = await (window as any).api.invoke('auto-score:deleteRule', ruleId) if (res.success) { - MessagePlugin.success('规则删除成功') - fetchRules() // 刷新规则列表 + MessagePlugin.success('自动化删除成功') + fetchRules() // 刷新自动化列表 } else { - MessagePlugin.error(res.message || '删除规则失败') + MessagePlugin.error(res.message || '删除自动化失败') } } catch (error) { console.error('Failed to delete auto score rule:', error) - MessagePlugin.error('删除规则失败') + MessagePlugin.error('删除自动化失败') } } @@ -202,7 +233,7 @@ export const AutoScoreManager: React.FC = () => { try { const authRes = await (window as any).api.authGetStatus() if (!authRes || !authRes.success || authRes.data?.permission !== 'admin') { - MessagePlugin.error('需要管理员权限以启用/禁用自动加分规则') + MessagePlugin.error('需要管理员权限以启用/禁用自动加分自动化') return } } catch (e) { @@ -212,14 +243,14 @@ export const AutoScoreManager: React.FC = () => { try { const res = await (window as any).api.invoke('auto-score:toggleRule', { ruleId, enabled }) if (res.success) { - MessagePlugin.success(enabled ? '规则已启用' : '规则已禁用') - fetchRules() // 刷新规则列表 + MessagePlugin.success(enabled ? '自动化已启用' : '自动化已禁用') + fetchRules() // 刷新自动化列表 } else { - MessagePlugin.error(res.message || (enabled ? '启用规则失败' : '禁用规则失败')) + MessagePlugin.error(res.message || (enabled ? '启用自动化失败' : '禁用自动化失败')) } } catch (error) { console.error('Failed to toggle auto score rule:', error) - MessagePlugin.error(enabled ? '启用规则失败' : '禁用规则失败') + MessagePlugin.error(enabled ? '启用自动化失败' : '禁用自动化失败') } } @@ -254,7 +285,7 @@ export const AutoScoreManager: React.FC = () => { /> ) }, - { colKey: 'name', title: '规则名称', width: 150 }, + { colKey: 'name', title: '自动化名称', width: 150 }, { colKey: 'intervalMinutes', title: '间隔', @@ -326,7 +357,7 @@ export const AutoScoreManager: React.FC = () => { 编辑 handleDelete(row.id)} >
{ /> + + + {triggerItems} + + + + + + + println("这是一个示例代码块,展示如何使用自动化加分功能的API接口") + + {/*

使用说明

  • 自动化加分功能会按照设定的时间间隔自动为学生加分
  • 间隔时间以分钟为单位,例如1440表示每24小时(一天)执行一次
  • -
  • 如果"适用学生"字段为空,则规则适用于所有学生
  • -
  • 可以随时启用/禁用规则,不会影响已保存的规则配置
  • +
  • 如果"适用学生"字段为空,则自动化适用于所有学生
  • +
  • 可以随时启用/禁用自动化,不会影响已保存的自动化配置
*/} diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx index c0a7c1c..b527776 100644 --- a/src/renderer/src/components/Sidebar.tsx +++ b/src/renderer/src/components/Sidebar.tsx @@ -76,11 +76,14 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps): }> 积分管理 - }> 排行榜 + }> + 自动加分 - }> 自动加分 + }> + 排行榜 - }> 结算历史 + }> + 结算历史 } disabled={permission !== 'admin'}> 理由管理 From db292f479cdb6d559e2f0cda2bbfc274337d33bd Mon Sep 17 00:00:00 2001 From: NanGua-QWQ Date: Thu, 12 Feb 2026 17:57:22 +0800 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20=E5=BC=80=E5=8F=91=E7=8E=AF?= =?UTF-8?q?=E5=A2=83=E6=8F=90=E7=A4=BA=E7=9B=AE=E5=89=8D=E6=B2=A1=E6=9C=89?= =?UTF-8?q?isDev=E5=88=A4=E6=96=AD=EF=BC=88=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/App.tsx | 55 ++++++++++++++----- .../src/components/TagEditorDialog.tsx | 12 ++-- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 0ba1f77..04ebe4d 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -139,22 +139,32 @@ function MainContent(): React.JSX.Element { /> - {/* 开发中画面水印 */} -

- 开发中画面,不代表真正品质 ({getArchitecture()}) + opacity: 0.6, + zIndex: 9999 + }}> +

+ 开发中画面,不代表最终品质

+

+ SecScore Dev ({getPlatform()}-{getArchitecture()}) +

+ ) } @@ -166,16 +176,31 @@ function getArchitecture(): string { if (userAgent.includes('arm64') || userAgent.includes('aarch64')) { return 'ARM64'; - } else if (userAgent.includes('x86_64') || userAgent.includes('amd64')) { - return 'x86_64'; + } else if (userAgent.includes('x64') || userAgent.includes('amd64')) { + return 'x64'; } else if (userAgent.includes('i386') || userAgent.includes('i686')) { return 'x86'; } // 默认返回未知架构 - return 'Unknown'; + 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 { return ( diff --git a/src/renderer/src/components/TagEditorDialog.tsx b/src/renderer/src/components/TagEditorDialog.tsx index ae7af62..2bf0a2d 100644 --- a/src/renderer/src/components/TagEditorDialog.tsx +++ b/src/renderer/src/components/TagEditorDialog.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react' import { Dialog, Input, Button, Space, Tag, MessagePlugin } from 'tdesign-react' +import { useTheme } from '../contexts/ThemeContext' // 导入主题上下文 interface Tag { id: number @@ -25,6 +26,9 @@ export const TagEditorDialog: React.FC = ({ const [allTags, setAllTags] = useState([]) const [selectedTagIds, setSelectedTagIds] = useState>(new Set(initialTagIds)) const [loading, setLoading] = useState(false) + const { currentTheme } = useTheme() // 获取当前主题 + + const themeMode = currentTheme?.mode || 'light'; // 默认为 light useEffect(() => { if (visible) { @@ -54,8 +58,8 @@ export const TagEditorDialog: React.FC = ({ const trimmed = inputValue.trim() if (!trimmed) return - if (trimmed.length > 50) { - MessagePlugin.error('标签名称不能超过 50 个字符') + if (trimmed.length > 30) { + MessagePlugin.error('标签名称不能超过 30 个字符') return } @@ -175,7 +179,7 @@ export const TagEditorDialog: React.FC = ({ handleToggleTag(tag.id)} style={{ cursor: 'pointer' }} @@ -202,7 +206,7 @@ export const TagEditorDialog: React.FC = ({ handleDeleteTag(tag.id)} style={{ cursor: 'pointer' }} From 5ca46fde9f22733b161659bd01fd72989743753b Mon Sep 17 00:00:00 2001 From: NanGua-QWQ Date: Thu, 12 Feb 2026 21:50:15 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E7=BC=93=E5=AD=98=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/AutoScoreManager.tsx | 47 +++++++++++-------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/src/renderer/src/components/AutoScoreManager.tsx b/src/renderer/src/components/AutoScoreManager.tsx index 38380a0..22cbe50 100644 --- a/src/renderer/src/components/AutoScoreManager.tsx +++ b/src/renderer/src/components/AutoScoreManager.tsx @@ -17,7 +17,8 @@ import { Popconfirm, Radio, Select, - TooltipLite + TooltipLite, + DatePicker } from 'tdesign-react' interface AutoScoreRule { @@ -124,7 +125,7 @@ export const AutoScoreManager: React.FC = () => { // 验证触发器必填项(如果某个触发器需要 value,则确保已填写) for (const t of triggerList) { const def = allTriggers.find((a) => a.eventName === t.eventName) - if (def?.haveValue && (!t.value || String(t.value).trim() === '')) { + if (def?.valueType && (!t.value || String(t.value).trim() === '')) { MessagePlugin.warning(`触发器 ${def.label} 需要填写 value`) return } @@ -190,7 +191,7 @@ export const AutoScoreManager: React.FC = () => { return { id: idx + 1, eventName: t.event, - haveValue: !!found?.haveValue, + haveValue: !!found?.valueType, value: t.value ?? '' } }) @@ -375,25 +376,24 @@ export const AutoScoreManager: React.FC = () => { label: string description: string eventName: string - haveValue: boolean - valueType?: string + valueType?: "DatePicker" | "Input" } const allTriggers: TriggerDef[] = [ - { id: 1, label: '根据间隔时间触发', description: '当学生注册时触发自动化', eventName: 'interval_time_passed', haveValue: false, valueType: 'number' }, - { id: 2, label: '按照学生标签触发', description: '当学生完成作业时触发自动化', eventName: 'student_tag_matched', haveValue: true, valueType: 'number' }, - { id: 3, label: '随机时间触发', description: '当随机时间到达时触发自动化', eventName: 'random_time_reached', haveValue: false, valueType: 'number' } + { 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; haveValue: boolean; value?: string } + type TriggerItem = { id: number; eventName: string; value?: string; valueType?: "DatePicker" | "Input" } const [triggerList, setTriggerList] = useState([]) 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, haveValue: !!found?.haveValue, value: t.value ?? '' } : t))) + setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, eventName: value, valueType: found?.valueType, value: '' } : t))) } const handleValueChange = (id: number, val: string) => { @@ -407,7 +407,7 @@ export const AutoScoreManager: React.FC = () => { 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, haveValue: defaultTrigger.haveValue, value: '' }]) + setTriggerList((prev) => [...prev, { id: nextId, eventName: defaultTrigger.eventName, valueType: defaultTrigger.valueType, value: '' }]) } const triggerItems = triggerList @@ -427,13 +427,22 @@ export const AutoScoreManager: React.FC = () => { placeholder="请选择触发规则" onChange={(value) => handleTriggerChange(triggerTest.id, value as string)} /> - {triggerTest.haveValue ? ( - handleValueChange(triggerTest.id, String((v as any).target?.value ?? v))} - /> + {triggerTest.valueType ? ( + triggerTest.eventName === 'interval_time_passed' ? ( + handleValueChange(triggerTest.id, v ? String(v) : '')} + /> + ) : ( + handleValueChange(triggerTest.id, String((v as any).target?.value ?? v))} + /> + ) ) : null} )) @@ -465,7 +474,7 @@ export const AutoScoreManager: React.FC = () => { name="intervalMinutes" rules={[ { required: true, message: '请输入间隔时间' }, - { min: 1, message: '间隔时间至少为1分钟' } + { min: 1, message: '间隔时间至少为1分钟/天' } ]} style={{ marginBottom: 0 }} > From c64abc070eb88c0a1cf1d39642290094fe53c617 Mon Sep 17 00:00:00 2001 From: NanGua-QWQ Date: Thu, 12 Feb 2026 22:21:30 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E8=BF=87=E4=B8=8B=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/components/TagEditorDialog.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/components/TagEditorDialog.tsx b/src/renderer/src/components/TagEditorDialog.tsx index 2bf0a2d..83becbf 100644 --- a/src/renderer/src/components/TagEditorDialog.tsx +++ b/src/renderer/src/components/TagEditorDialog.tsx @@ -25,7 +25,6 @@ export const TagEditorDialog: React.FC = ({ const [inputValue, setInputValue] = useState('') const [allTags, setAllTags] = useState([]) const [selectedTagIds, setSelectedTagIds] = useState>(new Set(initialTagIds)) - const [loading, setLoading] = useState(false) const { currentTheme } = useTheme() // 获取当前主题 const themeMode = currentTheme?.mode || 'light'; // 默认为 light @@ -40,7 +39,6 @@ export const TagEditorDialog: React.FC = ({ const fetchAllTags = async () => { if (!(window as any).api) return - setLoading(true) try { const res = await (window as any).api.tagsGetAll() if (res.success && res.data) { @@ -49,8 +47,6 @@ export const TagEditorDialog: React.FC = ({ } catch (e) { console.error('Failed to fetch tags:', e) MessagePlugin.error('获取标签列表失败') - } finally { - setLoading(false) } } @@ -119,12 +115,12 @@ export const TagEditorDialog: React.FC = ({ } } - const handleKeyDown = (value: string, context: { e: React.KeyboardEvent }) => { +/* const handleKeyDown = (value: string, context: { e: React.KeyboardEvent }) => { if (context.e.key === 'Enter') { context.e.preventDefault() handleAddTag() } - } + } */ const handleConfirm = () => { onConfirm(Array.from(selectedTagIds)) From c11c4e958f0e3364564db70cbfc3f00eacfdf2f9 Mon Sep 17 00:00:00 2001 From: NanGua-QWQ Date: Thu, 12 Feb 2026 22:29:33 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E8=BF=87=E4=B8=8Bpnpm=20lint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/db/DbManager.ts | 10 +- src/main/db/entities/StudentEntity.ts | 2 +- src/main/db/entities/StudentTagEntity.ts | 2 +- src/main/db/entities/TagEntity.ts | 2 +- .../migrations/AddTagsSchema2026020600000.ts | 2 +- src/main/hosting/tokens.ts | 2 +- src/main/index.ts | 18 +- src/main/repos/StudentRepository.ts | 6 +- src/main/repos/TagRepository.ts | 4 +- src/main/services/AutoScoreManager.tsx | 179 ++++++++-------- src/main/services/AutoScoreService.ts | 28 +-- src/main/services/DataService.ts | 19 +- src/main/services/FileSystemService.ts | 105 ++++++---- src/main/services/HttpServerService.ts | 18 +- src/main/services/ThemeService.ts | 2 +- src/preload/index.ts | 4 +- src/preload/types.ts | 37 +++- src/renderer/src/App.tsx | 82 ++++---- .../src/components/AutoScoreManager.tsx | 191 ++++++++++-------- src/renderer/src/components/ScoreManager.tsx | 6 +- src/renderer/src/components/Settings.tsx | 40 ++-- src/renderer/src/components/Sidebar.tsx | 10 +- .../src/components/StudentManager.tsx | 4 +- .../src/components/TagEditorDialog.tsx | 54 +++-- 24 files changed, 471 insertions(+), 356 deletions(-) diff --git a/src/main/db/DbManager.ts b/src/main/db/DbManager.ts index 7ffd8cf..f41067c 100644 --- a/src/main/db/DbManager.ts +++ b/src/main/db/DbManager.ts @@ -29,7 +29,15 @@ export class DbManager extends Service { this.dataSource = new DataSource({ type: 'better-sqlite3', database: dbPath, - entities: [StudentEntity, ReasonEntity, ScoreEventEntity, SettlementEntity, SettingEntity, TagEntity, StudentTagEntity], + entities: [ + StudentEntity, + ReasonEntity, + ScoreEventEntity, + SettlementEntity, + SettingEntity, + TagEntity, + StudentTagEntity + ], migrations, synchronize: false, logging: false diff --git a/src/main/db/entities/StudentEntity.ts b/src/main/db/entities/StudentEntity.ts index 57ae2bd..07907d5 100644 --- a/src/main/db/entities/StudentEntity.ts +++ b/src/main/db/entities/StudentEntity.ts @@ -11,7 +11,7 @@ export class StudentEntity { @Column({ type: 'text', default: '[]' }) tags!: string - + @Column({ type: 'integer', default: 0 }) score!: number diff --git a/src/main/db/entities/StudentTagEntity.ts b/src/main/db/entities/StudentTagEntity.ts index dbc5222..e7733e0 100644 --- a/src/main/db/entities/StudentTagEntity.ts +++ b/src/main/db/entities/StudentTagEntity.ts @@ -24,4 +24,4 @@ export class StudentTagEntity { @Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' }) created_at!: string -} \ No newline at end of file +} diff --git a/src/main/db/entities/TagEntity.ts b/src/main/db/entities/TagEntity.ts index ec1371e..6cd78b1 100644 --- a/src/main/db/entities/TagEntity.ts +++ b/src/main/db/entities/TagEntity.ts @@ -13,4 +13,4 @@ export class TagEntity { @Column({ type: 'text', default: () => 'CURRENT_TIMESTAMP' }) updated_at!: string -} \ No newline at end of file +} diff --git a/src/main/db/migrations/AddTagsSchema2026020600000.ts b/src/main/db/migrations/AddTagsSchema2026020600000.ts index 6e4bb3f..7a3275f 100644 --- a/src/main/db/migrations/AddTagsSchema2026020600000.ts +++ b/src/main/db/migrations/AddTagsSchema2026020600000.ts @@ -50,4 +50,4 @@ export class AddTagsSchema2026020600000 implements MigrationInterface { await queryRunner.query(`DROP TABLE IF EXISTS "student_tags"`) await queryRunner.query(`DROP TABLE IF EXISTS "tags"`) } -} \ No newline at end of file +} diff --git a/src/main/hosting/tokens.ts b/src/main/hosting/tokens.ts index 7e115b5..4b37ad8 100644 --- a/src/main/hosting/tokens.ts +++ b/src/main/hosting/tokens.ts @@ -16,4 +16,4 @@ export const WindowManagerToken = Symbol.for('secscore.windowManager') export const TrayServiceToken = Symbol.for('secscore.trayService') export const AutoScoreServiceToken = Symbol.for('secscore.autoScoreService') export const HttpServerServiceToken = Symbol.for('secscore.httpServerService') -export const FileSystemServiceToken = Symbol.for('secscore.fileSystemService') \ No newline at end of file +export const FileSystemServiceToken = Symbol.for('secscore.fileSystemService') diff --git a/src/main/index.ts b/src/main/index.ts index 129ca4b..b7d06ba 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -237,7 +237,10 @@ app.whenReady().then(async () => { (p) => new PermissionService(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( StudentRepositoryToken, @@ -266,10 +269,7 @@ app.whenReady().then(async () => { TrayServiceToken, (p) => new TrayService(p.get(MainContext), config.window) ) - services.addSingleton( - AutoScoreServiceToken, - (p) => new AutoScoreService(p.get(MainContext)) - ) + services.addSingleton(AutoScoreServiceToken, (p) => new AutoScoreService(p.get(MainContext))) services.addSingleton( HttpServerServiceToken, (p) => new HttpServerService(p.get(MainContext)) @@ -294,7 +294,9 @@ app.whenReady().then(async () => { services.get(ReasonRepositoryToken) services.get(EventRepositoryToken) services.get(SettlementRepositoryToken) - const theme = services.get(ThemeServiceToken) as import('./services/ThemeService').ThemeService + const theme = services.get( + ThemeServiceToken + ) as import('./services/ThemeService').ThemeService await theme.init() if (!process.env.HEADLESS) { services.get(WindowManagerToken) @@ -321,7 +323,9 @@ app.whenReady().then(async () => { services.get(ReasonRepositoryToken) services.get(EventRepositoryToken) services.get(SettlementRepositoryToken) - const theme = services.get(ThemeServiceToken) as import('./services/ThemeService').ThemeService + const theme = services.get( + ThemeServiceToken + ) as import('./services/ThemeService').ThemeService await theme.init() if (!process.env.HEADLESS) { services.get(WindowManagerToken) diff --git a/src/main/repos/StudentRepository.ts b/src/main/repos/StudentRepository.ts index feda4d6..a0e8130 100644 --- a/src/main/repos/StudentRepository.ts +++ b/src/main/repos/StudentRepository.ts @@ -80,7 +80,7 @@ export class StudentRepository extends Service { order: { score: 'DESC', name: 'ASC' } }) - return entities.map(entity => { + return entities.map((entity) => { let tags: string[] = [] try { tags = Array.isArray(entity.tags) ? entity.tags : JSON.parse(entity.tags || '[]') @@ -117,8 +117,8 @@ export class StudentRepository extends Service { if (key === 'id') continue if (key === 'tags') { next[key] = JSON.stringify(val || []) - } else { - next[key] = val + } else { + next[key] = val } } next.updated_at = new Date().toISOString() diff --git a/src/main/repos/TagRepository.ts b/src/main/repos/TagRepository.ts index 78ae889..9fc4549 100644 --- a/src/main/repos/TagRepository.ts +++ b/src/main/repos/TagRepository.ts @@ -43,7 +43,7 @@ export class TagRepository { .orderBy('st.created_at', 'ASC') .getMany() - return relations.map(r => r.tag).filter(Boolean) + return relations.map((r) => r.tag).filter(Boolean) } async addTagToStudent(studentId: number, tagId: number): Promise { @@ -76,4 +76,4 @@ export class TagRepository { await this.studentTagRepo.save(relation) } } -} \ No newline at end of file +} diff --git a/src/main/services/AutoScoreManager.tsx b/src/main/services/AutoScoreManager.tsx index 993ef3c..2da6ca6 100644 --- a/src/main/services/AutoScoreManager.tsx +++ b/src/main/services/AutoScoreManager.tsx @@ -50,7 +50,7 @@ export const AutoScoreManager: React.FC = () => { const fetchRules = async () => { if (!(window as any).api) return - + setLoading(true) try { // 权限检查:确保当前为 admin @@ -91,20 +91,23 @@ export const AutoScoreManager: React.FC = () => { }, []) 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) { - MessagePlugin.warning('请填写完整信息'); - return; + MessagePlugin.warning('请填写完整信息') + return } // 根据单位转换间隔时间 - const intervalMinutes = values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes; + const intervalMinutes = + values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes // 确保 studentNames 是数组类型 - const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []; + const studentNames = Array.isArray(values.studentNames) ? values.studentNames : [] const ruleData = { enabled: true, @@ -112,8 +115,8 @@ export const AutoScoreManager: React.FC = () => { intervalMinutes, studentNames, scoreValue: values.scoreValue, - reason: values.reason || `自动化加分 - ${values.name}`, - }; + reason: values.reason || `自动化加分 - ${values.name}` + } // 权限检查:仅管理员可创建/更新自动化 try { @@ -153,7 +156,9 @@ export const AutoScoreManager: React.FC = () => { setEditingRuleId(null) fetchRules() // 刷新自动化列表 } else { - MessagePlugin.error(res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')) + MessagePlugin.error( + res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败') + ) } } catch (error) { console.error('Failed to submit auto score rule:', error) @@ -238,11 +243,11 @@ export const AutoScoreManager: React.FC = () => { } const columns: PrimaryTableCol[] = [ - { - colKey: 'drag', - title: '排序', - cell: () => , - width: 60 + { + colKey: 'drag', + title: '排序', + cell: () => , + width: 60 }, { colKey: 'enabled', @@ -288,12 +293,7 @@ export const AutoScoreManager: React.FC = () => { } const studentList = row.studentNames.join(',\n') return ( - + {row.studentNames.length} 名学生 ) @@ -320,17 +320,10 @@ export const AutoScoreManager: React.FC = () => { width: 150, cell: ({ row }) => ( - - handleDelete(row.id)} - > + handleDelete(row.id)}> @@ -339,7 +332,7 @@ export const AutoScoreManager: React.FC = () => { ) } ] - const onDragSort = (params: any) => setRules(params.newData); + const onDragSort = (params: any) => setRules(params.newData) // 定义触发规则选项 const triggerOptions = [ @@ -350,12 +343,30 @@ export const AutoScoreManager: React.FC = () => { { 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 } + { + 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) @@ -363,7 +374,9 @@ export const AutoScoreManager: React.FC = () => { 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 + t.id === id + ? { ...t, triggerEvent: triggerOptions.find((o) => o.value === value) || t.triggerEvent } + : t ) ) } @@ -378,7 +391,10 @@ export const AutoScoreManager: React.FC = () => { 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: '' }]) + setTriggerList((prev) => [ + ...prev, + { id: nextId, triggerEvent: triggerOptions[0], description: '', haveValue: false, value: '' } + ]) } const triggerItems = triggerList @@ -403,7 +419,9 @@ export const AutoScoreManager: React.FC = () => { placeholder="请输入Value" style={{ width: '150px' }} value={String(triggerTest.value)} - onChange={(v) => handleValueChange(triggerTest.id, String((v as any).target?.value ?? v))} + onChange={(v) => + handleValueChange(triggerTest.id, String((v as any).target?.value ?? v)) + } /> ) : null} @@ -414,11 +432,7 @@ export const AutoScoreManager: React.FC = () => {

自动化加分管理

-
+
{ @@ -458,43 +472,31 @@ export const AutoScoreManager: React.FC = () => { - - ({ label: student.name, value: student.name }))} + /> - +
- -
- +
{ pageSize, total: rules.length, onChange: (pageInfo) => setCurrentPage(pageInfo.current), - onPageSizeChange: (size) => setPageSize(size), + onPageSizeChange: (size) => setPageSize(size) }} style={{ color: 'var(--ss-text-main)' }} /> - - + + {triggerItems} - - + println("这是一个示例代码块,展示如何使用自动化加分功能的API接口") -{/*
+ {/*

使用说明

  • 自动化加分功能会按照设定的时间间隔自动为学生加分
  • @@ -544,4 +551,4 @@ export const AutoScoreManager: React.FC = () => {
*/}
) -} \ No newline at end of file +} diff --git a/src/main/services/AutoScoreService.ts b/src/main/services/AutoScoreService.ts index b7fa94d..a135a43 100644 --- a/src/main/services/AutoScoreService.ts +++ b/src/main/services/AutoScoreService.ts @@ -108,7 +108,7 @@ export class AutoScoreService extends Service { const settings = await this.mainCtx.settings.getAllRaw() const autoScoreRulesStr = settings['auto_score_rules'] || '[]' const rulesFromSettings = JSON.parse(autoScoreRulesStr) - + this.rules = rulesFromSettings.map((rule: any) => ({ ...rule, lastExecuted: rule.lastExecuted ? new Date(rule.lastExecuted) : undefined @@ -155,7 +155,7 @@ export class AutoScoreService extends Service { // 计算下次执行时间 const now = new Date() const intervalMs = rule.intervalMinutes * 60 * 1000 - + // 如果规则之前执行过,计算从上次执行到现在需要等待的时间 let delayMs = intervalMs if (rule.lastExecuted) { @@ -188,7 +188,7 @@ export class AutoScoreService extends Service { private async executeRule(rule: AutoScoreRule) { try { console.log(`Executing auto score rule: ${rule.name}`) - + const studentRepo = this.mainCtx.students const eventRepo = this.mainCtx.events @@ -201,7 +201,7 @@ export class AutoScoreService extends Service { // 否则只对指定的学生加分 const allStudents = await studentRepo.findAll() for (const name of rule.studentNames) { - const student = allStudents.find(s => s.name === name) + const student = allStudents.find((s) => s.name === name) if (student) { studentsToScore.push(student) } @@ -220,7 +220,7 @@ export class AutoScoreService extends Service { // 更新规则的最后执行时间 rule.lastExecuted = new Date() await this.saveRulesToSettings() - + console.log(`Auto score rule executed successfully for ${studentsToScore.length} students`) } catch (error) { console.error(`Failed to execute auto score rule ${rule.name}:`, error) @@ -236,25 +236,25 @@ export class AutoScoreService extends Service { } async addRule(rule: Omit): Promise { - 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 = { ...rule, id: newId, lastExecuted: undefined } - + this.rules.push(newRule) await this.saveRulesToSettings() - + if (rule.enabled) { this.startRuleTimer(newRule) } - + return newId } async updateRule(rule: Partial & { id: number }): Promise { - 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 // 停止旧的定时器 @@ -280,7 +280,7 @@ export class AutoScoreService extends Service { } async deleteRule(ruleId: number): Promise { - const index = this.rules.findIndex(r => r.id === ruleId) + const index = this.rules.findIndex((r) => r.id === ruleId) if (index === -1) return false // 停止定时器 @@ -298,7 +298,7 @@ export class AutoScoreService extends Service { } async toggleRule(ruleId: number, enabled: boolean): Promise { - const rule = this.rules.find(r => r.id === ruleId) + const rule = this.rules.find((r) => r.id === ruleId) if (!rule) return false rule.enabled = enabled @@ -324,7 +324,7 @@ export class AutoScoreService extends Service { } isEnabled(): boolean { - return this.rules.some(rule => rule.enabled) + return this.rules.some((rule) => rule.enabled) } async restart() { @@ -342,4 +342,4 @@ export class AutoScoreService extends Service { async dispose(): Promise { this.stopRules() } -} \ No newline at end of file +} diff --git a/src/main/services/DataService.ts b/src/main/services/DataService.ts index 47442d4..c0b1f0e 100644 --- a/src/main/services/DataService.ts +++ b/src/main/services/DataService.ts @@ -53,7 +53,7 @@ export class DataService extends Service { const tags = await this.tagRepo.findAll() return { 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) return { 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[]) => { - if (!this.mainCtx.permissions.requirePermission(event, 'admin')) - return { success: false, message: 'Permission denied' } + this.mainCtx.handle( + 'tags:updateStudentTags', + async (event, studentId: number, tagIds: number[]) => { + if (!this.mainCtx.permissions.requirePermission(event, 'admin')) + return { success: false, message: 'Permission denied' } - await this.tagRepo.updateStudentTags(studentId, tagIds) - return { success: true } - }) + await this.tagRepo.updateStudentTags(studentId, tagIds) + return { success: true } + } + ) } } diff --git a/src/main/services/FileSystemService.ts b/src/main/services/FileSystemService.ts index 9de74b1..276623f 100644 --- a/src/main/services/FileSystemService.ts +++ b/src/main/services/FileSystemService.ts @@ -59,7 +59,10 @@ export class FileSystemService extends Service { } } - async readJsonFile(relativePath: string, folder: 'automatic' | 'script' = 'automatic'): Promise { + async readJsonFile( + relativePath: string, + folder: 'automatic' | 'script' = 'automatic' + ): Promise { await this.initPromise const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir const filePath = join(baseDir, relativePath) @@ -90,7 +93,10 @@ export class FileSystemService extends Service { await fs.writeFile(filePath, content, 'utf-8') return true } catch (error) { - this.ctx.logger.warn('FileSystemService: Failed to write JSON file', { path: filePath, error }) + this.ctx.logger.warn('FileSystemService: Failed to write JSON file', { + path: filePath, + error + }) return false } } @@ -127,12 +133,18 @@ export class FileSystemService extends Service { await fs.writeFile(filePath, content, 'utf-8') return true } catch (error) { - this.ctx.logger.warn('FileSystemService: Failed to write text file', { path: filePath, 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 { + async deleteFile( + relativePath: string, + folder: 'automatic' | 'script' = 'automatic' + ): Promise { await this.initPromise const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir const filePath = join(baseDir, relativePath) @@ -180,7 +192,10 @@ export class FileSystemService extends Service { } } - async fileExists(relativePath: string, folder: 'automatic' | 'script' = 'automatic'): Promise { + async fileExists( + relativePath: string, + folder: 'automatic' | 'script' = 'automatic' + ): Promise { await this.initPromise const baseDir = folder === 'automatic' ? this.automaticDir : this.scriptDir const filePath = join(baseDir, relativePath) @@ -193,7 +208,7 @@ export class FileSystemService extends Service { } } -private get mainCtx() { + private get mainCtx() { return this.ctx as MainContext } @@ -203,35 +218,50 @@ private get mainCtx() { 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: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: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: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: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: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 @@ -239,10 +269,13 @@ private get mainCtx() { 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 } - }) + 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 } + } + ) } } diff --git a/src/main/services/HttpServerService.ts b/src/main/services/HttpServerService.ts index 8ad3c5d..74e2552 100644 --- a/src/main/services/HttpServerService.ts +++ b/src/main/services/HttpServerService.ts @@ -47,12 +47,12 @@ export class HttpServerService extends Service { try { await this.start(config) - return { - success: true, - data: { + return { + success: true, + data: { url: `http://${this.config.host}:${this.config.port}`, config: this.config - } + } } } catch (error) { const message = error instanceof Error ? error.message : 'Unknown error' @@ -109,14 +109,16 @@ export class HttpServerService extends Service { if (await this.isPortAvailable(this.config.port)) { break } - + // 端口被占用,尝试下一个端口 this.config.port++ attempts++ } 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) { @@ -131,7 +133,7 @@ export class HttpServerService extends Service { // 配置中间件 this.app.use(express.json()) this.app.use(express.urlencoded({ extended: true })) - + // 简单的CORS处理 this.app.use((_: Request, res: Response, next: NextFunction) => { res.header('Access-Control-Allow-Origin', this.config.corsOrigin || '*') @@ -352,4 +354,4 @@ export class HttpServerService extends Service { }) }) } -} \ No newline at end of file +} diff --git a/src/main/services/ThemeService.ts b/src/main/services/ThemeService.ts index fbcd67f..43e11f2 100644 --- a/src/main/services/ThemeService.ts +++ b/src/main/services/ThemeService.ts @@ -55,7 +55,7 @@ export class ThemeService extends Service { 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) + const exists = themes.some((t) => t.id === savedThemeId) if (exists) { this.currentThemeId = savedThemeId } diff --git a/src/preload/index.ts b/src/preload/index.ts index da9b812..920e48c 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -112,7 +112,7 @@ const api = { ipcRenderer.invoke('log:write', payload), registerUrlProtocol: () => ipcRenderer.invoke('app:register-url-protocol'), - + // HTTP Server httpServerStart: (config?: { port?: number; host?: string; corsOrigin?: string }) => ipcRenderer.invoke('http:server:start', config), @@ -135,7 +135,7 @@ const api = { 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` invoke: (channel: string, ...args: any[]) => ipcRenderer.invoke(channel, ...args) } diff --git a/src/preload/types.ts b/src/preload/types.ts index 52f7b84..1204fef 100644 --- a/src/preload/types.ts +++ b/src/preload/types.ts @@ -175,9 +175,13 @@ export interface electronApi { }) => Promise> registerUrlProtocol: () => Promise> - + // 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 } }> > httpServerStop: () => Promise> @@ -192,13 +196,30 @@ export interface electronApi { // File System fsGetConfigStructure: () => Promise> fsReadJson: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise> - fsWriteJson: (relativePath: string, data: any, folder?: 'automatic' | 'sscript') => Promise> - fsReadText: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise> - fsWriteText: (content: string, relativePath: string, folder?: 'automatic' | 'sscript') => Promise> - fsDeleteFile: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise> + fsWriteJson: ( + relativePath: string, + data: any, + folder?: 'automatic' | 'sscript' + ) => Promise> + fsReadText: ( + relativePath: string, + folder?: 'automatic' | 'sscript' + ) => Promise> + fsWriteText: ( + content: string, + relativePath: string, + folder?: 'automatic' | 'sscript' + ) => Promise> + fsDeleteFile: ( + relativePath: string, + folder?: 'automatic' | 'sscript' + ) => Promise> fsListFiles: (folder?: 'automatic' | 'sscript') => Promise> - fsFileExists: (relativePath: string, folder?: 'automatic' | 'sscript') => Promise> - + fsFileExists: ( + relativePath: string, + folder?: 'automatic' | 'sscript' + ) => Promise> + // Generic invoke wrapper (minimal compatibility API) invoke?: (channel: string, ...args: any[]) => Promise } diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 04ebe4d..b207581 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -139,67 +139,71 @@ function MainContent(): React.JSX.Element { /> - -
-

- 开发中画面,不代表最终品质 -

-

- SecScore Dev ({getPlatform()}-{getArchitecture()}) -

+ bottom: '2px', + left: '20px', + opacity: 0.6, + zIndex: 9999 + }} + > +

+ 开发中画面,不代表最终品质 +

+

+ SecScore Dev ({getPlatform()}-{getArchitecture()}) +

) } function getArchitecture(): string { - // 尝试从 userAgent 中获取架构信息 - const userAgent = navigator.userAgent.toLowerCase(); - + const userAgent = navigator.userAgent.toLowerCase() + if (userAgent.includes('arm64') || userAgent.includes('aarch64')) { - return 'ARM64'; + return 'ARM64' } else if (userAgent.includes('x64') || userAgent.includes('amd64')) { - return 'x64'; + return 'x64' } else if (userAgent.includes('i386') || userAgent.includes('i686')) { - return 'x86'; + return 'x86' } - + // 默认返回未知架构 - return userAgent; + return userAgent } function getPlatform(): string { - // 尝试从 userAgent 中获取平台信息 - const userAgent = navigator.userAgent.toLowerCase(); - + const userAgent = navigator.userAgent.toLowerCase() + if (userAgent.includes('windows')) { - return 'Windows'; + return 'Windows' } else if (userAgent.includes('mac')) { - return 'Mac'; + return 'Mac' } else if (userAgent.includes('linux')) { - return 'Linux'; + return 'Linux' } - return 'Unknown'; + return 'Unknown' } function App(): React.JSX.Element { return ( diff --git a/src/renderer/src/components/AutoScoreManager.tsx b/src/renderer/src/components/AutoScoreManager.tsx index 22cbe50..2623030 100644 --- a/src/renderer/src/components/AutoScoreManager.tsx +++ b/src/renderer/src/components/AutoScoreManager.tsx @@ -52,7 +52,7 @@ export const AutoScoreManager: React.FC = () => { const fetchRules = async () => { if (!(window as any).api) return - + setLoading(true) try { // 权限检查:确保当前为 admin @@ -93,20 +93,23 @@ export const AutoScoreManager: React.FC = () => { }, []) 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) { - MessagePlugin.warning('请填写完整信息'); - return; + MessagePlugin.warning('请填写完整信息') + return } // 根据单位转换间隔时间 - const intervalMinutes = values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes; + const intervalMinutes = + values.timeUnit === 'days' ? values.intervalMinutes * 1440 : values.intervalMinutes // 确保 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 })) @@ -118,7 +121,7 @@ export const AutoScoreManager: React.FC = () => { scoreValue: values.scoreValue, reason: values.reason || `自动化加分 - ${values.name}`, triggers: triggersPayload - }; + } // 权限检查:仅管理员可创建/更新自动化 try { @@ -168,7 +171,9 @@ export const AutoScoreManager: React.FC = () => { setTriggerList([]) fetchRules() // 刷新自动化列表 } else { - MessagePlugin.error(res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败')) + MessagePlugin.error( + res.message || (editingRuleId !== null ? '更新自动化失败' : '创建自动化失败') + ) } } catch (error) { console.error('Failed to submit auto score rule:', error) @@ -268,11 +273,11 @@ export const AutoScoreManager: React.FC = () => { } const columns: PrimaryTableCol[] = [ - { - colKey: 'drag', - title: '排序', - cell: () => , - width: 60 + { + colKey: 'drag', + title: '排序', + cell: () => , + width: 60 }, { colKey: 'enabled', @@ -318,12 +323,7 @@ export const AutoScoreManager: React.FC = () => { } const studentList = row.studentNames.join(',\n') return ( - + {row.studentNames.length} 名学生 ) @@ -350,17 +350,10 @@ export const AutoScoreManager: React.FC = () => { width: 150, cell: ({ row }) => ( - - handleDelete(row.id)} - > + handleDelete(row.id)}> @@ -369,31 +362,57 @@ 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" + 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' } + { + 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" } + type TriggerItem = { + id: number + eventName: string + value?: string + valueType?: 'DatePicker' | 'Input' + } const [triggerList, setTriggerList] = useState([]) 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))) + setTriggerList((prev) => + prev.map((t) => + t.id === id ? { ...t, eventName: value, valueType: found?.valueType, value: '' } : t + ) + ) } const handleValueChange = (id: number, val: string) => { @@ -407,7 +426,15 @@ export const AutoScoreManager: React.FC = () => { 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: '' }]) + setTriggerList((prev) => [ + ...prev, + { + id: nextId, + eventName: defaultTrigger.eventName, + valueType: defaultTrigger.valueType, + value: '' + } + ]) } const triggerItems = triggerList @@ -440,24 +467,21 @@ export const AutoScoreManager: React.FC = () => { placeholder="请输入Value" style={{ width: '150px' }} value={String(triggerTest.value ?? '')} - onChange={(v) => handleValueChange(triggerTest.id, String((v as any).target?.value ?? v))} + onChange={(v) => + handleValueChange(triggerTest.id, String((v as any).target?.value ?? v)) + } /> ) ) : null} )) - return (

自动化加分管理

-
+
{ @@ -497,43 +521,31 @@ export const AutoScoreManager: React.FC = () => { - - ({ label: student.name, value: student.name }))} + /> - +
- -
- +
{ pageSize, total: rules.length, onChange: (pageInfo) => setCurrentPage(pageInfo.current), - onPageSizeChange: (size) => setPageSize(size), + onPageSizeChange: (size) => setPageSize(size) }} style={{ color: 'var(--ss-text-main)' }} /> - - + + {triggerItems} - - + println("这是一个示例代码块,展示如何使用自动化加分功能的API接口") -{/*
+ {/*

使用说明

  • 自动化加分功能会按照设定的时间间隔自动为学生加分
  • @@ -583,4 +600,4 @@ export const AutoScoreManager: React.FC = () => {
*/}
) -} \ No newline at end of file +} diff --git a/src/renderer/src/components/ScoreManager.tsx b/src/renderer/src/components/ScoreManager.tsx index 227b741..48f1b5b 100644 --- a/src/renderer/src/components/ScoreManager.tsx +++ b/src/renderer/src/components/ScoreManager.tsx @@ -138,9 +138,11 @@ export const ScoreManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { return } 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) { MessagePlugin.warning('请填写完整信息') return diff --git a/src/renderer/src/components/Settings.tsx b/src/renderer/src/components/Settings.tsx index 4916cd8..a95eff9 100644 --- a/src/renderer/src/components/Settings.tsx +++ b/src/renderer/src/components/Settings.tsx @@ -62,7 +62,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission const [settleDialogVisible, setSettleDialogVisible] = useState(false) const [urlRegisterLoading, setUrlRegisterLoading] = useState(false) - + // HTTP Server状态 const [httpServerStatus, setHttpServerStatus] = useState<{ isRunning: boolean @@ -111,7 +111,8 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission if (change?.key === 'is_wizard_completed') return { ...prev, is_wizard_completed: 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 }) }) @@ -259,7 +260,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission if (!(window as any).api) return setSettleDialogVisible(true) } - + // HTTP Server函数 const refreshHttpServerStatus = async () => { if (!(window as any).api) return @@ -275,7 +276,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission console.error('Failed to refresh HTTP server status:', error) } } - + const handleHttpServerStart = async () => { if (!(window as any).api) return setHttpLoading(true) @@ -291,7 +292,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission if (httpCorsOrigin.trim()) { config.corsOrigin = httpCorsOrigin.trim() } - + const res = await (window as any).api.httpServerStart(config) if (res.success) { MessagePlugin.success(`HTTP服务器已启动: ${res.data.url}`) @@ -306,7 +307,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission setHttpLoading(false) } } - + const handleHttpServerStop = async () => { if (!(window as any).api) return setHttpLoading(true) @@ -632,14 +633,11 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission > 通过HTTP API提供学生分数查询服务,可在局域网内访问。 - +
- + {httpServerStatus?.isRunning ? '运行中' : '已停止'} {httpServerStatus?.isRunning && ( @@ -649,7 +647,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission )}
- + = ({ permission disabled={!canAdmin || httpLoading} /> - + = ({ permission disabled={!canAdmin || httpLoading} /> - + = ({ permission style={{ width: '200px' }} disabled={!canAdmin || httpLoading} /> -
+
* 表示允许所有来源,建议在生产环境中设置具体域名
- + - + -
+
• GET /health - 健康检查
• GET /api/students/scores - 获取所有学生分数
• GET /api/students/:id/score - 获取单个学生分数
@@ -724,7 +726,7 @@ export const Settings: React.FC<{ permission: permissionLevel }> = ({ permission - +
diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx index b527776..e5da4dd 100644 --- a/src/renderer/src/components/Sidebar.tsx +++ b/src/renderer/src/components/Sidebar.tsx @@ -68,7 +68,9 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps):
- }> 主页 + }> + {' '} + 主页 } disabled={permission !== 'admin'}> 学生管理 @@ -76,13 +78,13 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps): }> 积分管理 - }> + }> 自动加分 - }> + }> 排行榜 - }> + }> 结算历史 } disabled={permission !== 'admin'}> diff --git a/src/renderer/src/components/StudentManager.tsx b/src/renderer/src/components/StudentManager.tsx index cd360f3..8fb87fb 100644 --- a/src/renderer/src/components/StudentManager.tsx +++ b/src/renderer/src/components/StudentManager.tsx @@ -382,7 +382,7 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { {tags.length === 0 ? ( 无标签 ) : ( - tags.slice(0, 3).map(tag => ( + tags.slice(0, 3).map((tag) => ( {tag} @@ -449,7 +449,7 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => { pageSize, total: data.length, onChange: (pageInfo) => setCurrentPage(pageInfo.current), - onPageSizeChange: (size) => setPageSize(size), + onPageSizeChange: (size) => setPageSize(size) }} style={{ backgroundColor: 'var(--ss-card-bg)', color: 'var(--ss-text-main)' }} /> diff --git a/src/renderer/src/components/TagEditorDialog.tsx b/src/renderer/src/components/TagEditorDialog.tsx index 83becbf..4a46f44 100644 --- a/src/renderer/src/components/TagEditorDialog.tsx +++ b/src/renderer/src/components/TagEditorDialog.tsx @@ -26,8 +26,8 @@ export const TagEditorDialog: React.FC = ({ const [allTags, setAllTags] = useState([]) const [selectedTagIds, setSelectedTagIds] = useState>(new Set(initialTagIds)) const { currentTheme } = useTheme() // 获取当前主题 - - const themeMode = currentTheme?.mode || 'light'; // 默认为 light + + const themeMode = currentTheme?.mode || 'light' // 默认为 light useEffect(() => { if (visible) { @@ -59,10 +59,10 @@ export const TagEditorDialog: React.FC = ({ return } - if (allTags.some(t => t.name === trimmed)) { - const existingTag = allTags.find(t => t.name === trimmed) + if (allTags.some((t) => t.name === trimmed)) { + const existingTag = allTags.find((t) => t.name === trimmed) if (existingTag) { - setSelectedTagIds(prev => new Set([...prev, existingTag.id])) + setSelectedTagIds((prev) => new Set([...prev, existingTag.id])) } setInputValue('') return @@ -71,8 +71,8 @@ export const TagEditorDialog: React.FC = ({ try { const res = await (window as any).api.tagsCreate(trimmed) if (res.success && res.data) { - setAllTags(prev => [...prev, res.data]) - setSelectedTagIds(prev => new Set([...prev, res.data.id])) + setAllTags((prev) => [...prev, res.data]) + setSelectedTagIds((prev) => new Set([...prev, res.data.id])) setInputValue('') } else { MessagePlugin.error(res.message || '添加标签失败') @@ -84,7 +84,7 @@ export const TagEditorDialog: React.FC = ({ } const handleToggleTag = (tagId: number) => { - setSelectedTagIds(prev => { + setSelectedTagIds((prev) => { const newSet = new Set(prev) if (newSet.has(tagId)) { newSet.delete(tagId) @@ -99,8 +99,8 @@ export const TagEditorDialog: React.FC = ({ try { const res = await (window as any).api.tagsDelete(tagId) if (res.success) { - setAllTags(prev => prev.filter(t => t.id !== tagId)) - setSelectedTagIds(prev => { + setAllTags((prev) => prev.filter((t) => t.id !== tagId)) + setSelectedTagIds((prev) => { const newSet = new Set(prev) newSet.delete(tagId) return newSet @@ -115,7 +115,7 @@ export const TagEditorDialog: React.FC = ({ } } -/* const handleKeyDown = (value: string, context: { e: React.KeyboardEvent }) => { + /* const handleKeyDown = (value: string, context: { e: React.KeyboardEvent }) => { if (context.e.key === 'Enter') { context.e.preventDefault() handleAddTag() @@ -127,8 +127,8 @@ export const TagEditorDialog: React.FC = ({ onClose() } - const selectedTags = allTags.filter(t => selectedTagIds.has(t.id)) - const availableTags = 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)) return ( = ({ value={inputValue} onChange={setInputValue} onEnter={handleAddTag} -/* maxLength={50} */ + /* maxLength={50} */ style={{ flex: 1 }} /> -
@@ -166,7 +162,14 @@ export const TagEditorDialog: React.FC = ({
已选标签(点击取消)
-
+
{selectedTags.length === 0 ? ( 未选择标签 ) : ( @@ -193,7 +196,14 @@ export const TagEditorDialog: React.FC = ({
可选标签(点击选择)
-
+
{availableTags.length === 0 ? ( 无可用标签 ) : ( @@ -218,4 +228,4 @@ export const TagEditorDialog: React.FC = ({
) -} \ No newline at end of file +}