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'}> 理由管理