From 07d96ff5b56b43fa8ceaf0096c4d88b8dbd3d4db Mon Sep 17 00:00:00 2001 From: NanGua-QWQ Date: Sat, 7 Mar 2026 20:00:34 +0800 Subject: [PATCH] =?UTF-8?q?FEFACTARY=EF=BC=9A=E8=87=AA=E5=8A=A8=E5=8C=96?= =?UTF-8?q?=E5=8A=A0=E5=88=86beta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 + pnpm-lock.yaml | 232 +++++++++++++ src/main/repos/EventRepository.ts | 23 ++ src/main/services/AutoScoreService.ts | 279 +++++----------- .../src/components/AutoScoreManager.tsx | 234 ++----------- src/renderer/src/components/Sidebar.tsx | 2 +- .../components/autoScore/actionComponent.tsx | 94 ++++++ .../autoScore/ruleBuilderOverride.css | 307 ++++++++++++++++++ .../components/autoScore/ruleBuilderUtils.ts | 109 +++++++ .../components/autoScore/ruleComponent.tsx | 82 +++++ .../com.automatically/ActionItem.tsx | 46 --- .../com.automatically/TriggerItem.tsx | 53 --- .../actions/AddScoreAction.tsx | 33 -- .../actions/AddTagAction.tsx | 20 -- .../actions/SendNotificationAction.tsx | 20 -- .../com.automatically/actions/index.ts | 51 --- .../src/components/com.automatically/index.ts | 17 - .../components/com.automatically/registry.ts | 66 ---- .../triggers/IntervalTimeTrigger.tsx | 64 ---- .../triggers/RandomTimeTrigger.tsx | 72 ---- .../triggers/StudentTagTrigger.tsx | 50 --- .../com.automatically/triggers/index.ts | 51 --- .../src/components/com.automatically/types.ts | 56 ---- src/renderer/src/services/AutoScoreService.ts | 58 +--- src/shared/autoScore/AutoScoreRuleEngine.ts | 244 ++++++++++++++ src/shared/autoScore/index.ts | 4 + .../autoScore/triggers/IntervalTimeTrigger.ts | 88 +++++ .../autoScore/triggers/StudentTagTrigger.ts | 69 ++++ src/shared/triggers/IntervalTimeTrigger.ts | 52 --- src/shared/triggers/RandomTimeTrigger.ts | 68 ---- src/shared/triggers/StudentTagTrigger.ts | 30 -- src/shared/triggers/index.ts | 27 -- src/shared/triggers/types.ts | 32 -- 33 files changed, 1373 insertions(+), 1262 deletions(-) create mode 100644 src/renderer/src/components/autoScore/actionComponent.tsx create mode 100644 src/renderer/src/components/autoScore/ruleBuilderOverride.css create mode 100644 src/renderer/src/components/autoScore/ruleBuilderUtils.ts create mode 100644 src/renderer/src/components/autoScore/ruleComponent.tsx delete mode 100644 src/renderer/src/components/com.automatically/ActionItem.tsx delete mode 100644 src/renderer/src/components/com.automatically/TriggerItem.tsx delete mode 100644 src/renderer/src/components/com.automatically/actions/AddScoreAction.tsx delete mode 100644 src/renderer/src/components/com.automatically/actions/AddTagAction.tsx delete mode 100644 src/renderer/src/components/com.automatically/actions/SendNotificationAction.tsx delete mode 100644 src/renderer/src/components/com.automatically/actions/index.ts delete mode 100644 src/renderer/src/components/com.automatically/index.ts delete mode 100644 src/renderer/src/components/com.automatically/registry.ts delete mode 100644 src/renderer/src/components/com.automatically/triggers/IntervalTimeTrigger.tsx delete mode 100644 src/renderer/src/components/com.automatically/triggers/RandomTimeTrigger.tsx delete mode 100644 src/renderer/src/components/com.automatically/triggers/StudentTagTrigger.tsx delete mode 100644 src/renderer/src/components/com.automatically/triggers/index.ts delete mode 100644 src/renderer/src/components/com.automatically/types.ts create mode 100644 src/shared/autoScore/AutoScoreRuleEngine.ts create mode 100644 src/shared/autoScore/index.ts create mode 100644 src/shared/autoScore/triggers/IntervalTimeTrigger.ts create mode 100644 src/shared/autoScore/triggers/StudentTagTrigger.ts delete mode 100644 src/shared/triggers/IntervalTimeTrigger.ts delete mode 100644 src/shared/triggers/RandomTimeTrigger.ts delete mode 100644 src/shared/triggers/StudentTagTrigger.ts delete mode 100644 src/shared/triggers/index.ts delete mode 100644 src/shared/triggers/types.ts diff --git a/package.json b/package.json index 0932995..44a3feb 100644 --- a/package.json +++ b/package.json @@ -40,10 +40,12 @@ "chokidar": "^5.0.0", "es-object-atoms": "^1.1.1", "express": "^5.2.1", + "json-rules-engine": "^7.3.1", "mica-electron": "^1.5.16", "os": "^0.1.2", "pg": "^8.19.0", "pinyin-pro": "^3.27.0", + "react-querybuilder": "^8.14.0", "react-router-dom": "^6.28.0", "reflect-metadata": "^0.2.2", "typeorm": "^0.3.27", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b2d514..df89c03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: express: specifier: ^5.2.1 version: 5.2.1 + json-rules-engine: + specifier: ^7.3.1 + version: 7.3.1 mica-electron: specifier: ^1.5.16 version: 1.5.16 @@ -57,6 +60,9 @@ importers: pinyin-pro: specifier: ^3.27.0 version: 3.27.0 + react-querybuilder: + specifier: ^8.14.0 + version: 8.14.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1) react-router-dom: specifier: ^6.28.0 version: 6.30.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -819,6 +825,18 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jsep-plugin/assignment@1.3.0': + resolution: {integrity: sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + + '@jsep-plugin/regex@1.0.4': + resolution: {integrity: sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==} + engines: {node: '>= 10.16.0'} + peerDependencies: + jsep: ^0.4.0||^1.0.0 + '@malept/cross-spawn-promise@2.0.0': resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} engines: {node: '>= 12.13.0'} @@ -1126,6 +1144,20 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' + '@react-querybuilder/core@8.14.0': + resolution: {integrity: sha512-j1pIY0Yyn/dXu9ZST/DVY7TqRmIO1hY/mZ8653DaeHaDzUF37tOdkm/NQDU9RfM0KXIWsJY5zlvYAR1DypZ+7g==} + peerDependencies: + drizzle-orm: '>=0.38.0' + json-logic-js: '>=2' + sequelize: '>=6' + peerDependenciesMeta: + drizzle-orm: + optional: true + json-logic-js: + optional: true + sequelize: + optional: true + '@react-spring/animated@9.6.1': resolution: {integrity: sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==} peerDependencies: @@ -1153,6 +1185,17 @@ packages: react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@reduxjs/toolkit@2.11.2': + resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@remix-run/router@1.23.2': resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==} engines: {node: '>=14.0.0'} @@ -1194,66 +1237,79 @@ packages: resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.55.1': resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.55.1': resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.55.1': resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.55.1': resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.55.1': resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.55.1': resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.55.1': resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.55.1': resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.55.1': resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.55.1': resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.55.1': resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.55.1': resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.55.1': resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==} @@ -1295,10 +1351,25 @@ packages: '@sqltools/formatter@1.2.5': resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@szmarczak/http-timer@4.0.6': resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} + '@ts-jison/common@0.4.1-alpha.1': + resolution: {integrity: sha512-SDbHzq+UMD+V3ciKVBHwCEgVqSeyQPTCjOsd/ZNTGySUVg4x3EauR9ZcEfdVFAsYRR38XWgDI+spq5LDY46KvQ==} + + '@ts-jison/lexer@0.4.1-alpha.1': + resolution: {integrity: sha512-5C1Wr+wixAzn2MOFtgy7KbT6N6j9mhmbjAtyvOqZKsikKtNOQj22MM5HxT+ooRexG2NbtxnDSXYdhHR1Lg58ow==} + + '@ts-jison/parser@0.4.1-alpha.1': + resolution: {integrity: sha512-xNj+qOez/7dju44LlYiTlCjxMzW5oek9EckUAElfln/GBK9vgMSk0swWcnacMr0TYbGjUQuXvL2wEgmDf5WajQ==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1370,6 +1441,9 @@ packages: '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@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. @@ -1782,6 +1856,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -2202,6 +2280,9 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + eventemitter2@6.4.9: + resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -2450,6 +2531,9 @@ packages: resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} engines: {node: '>= 0.4'} + hash-it@6.0.1: + resolution: {integrity: sha512-qhl8+l4Zwi1eLlL3lja5ywmDQnBzLEJxd0QJoAVIgZpgQbdtVZrN5ypB0y3VHwBlvAalpcbM2/A6x7oUks5zNg==} + hasown@2.0.2: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} @@ -2507,6 +2591,9 @@ packages: resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} engines: {node: '>= 4'} + immer@11.1.4: + resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -2717,6 +2804,10 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsep@1.4.0: + resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==} + engines: {node: '>= 10.16.0'} + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -2725,6 +2816,10 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-rules-engine@7.3.1: + resolution: {integrity: sha512-NyRTQZllvAt7AQ3g9P7/t4nIwlEB+EyZV7y8/WgXfZWSlpcDryt1UH9CsoU+Z+MDvj8umN9qqEcbE6qnk9JAHw==} + engines: {node: '>=18.0.0'} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -2748,6 +2843,11 @@ packages: jsonfile@6.2.0: resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + jsonpath-plus@10.4.0: + resolution: {integrity: sha512-T92WWatJXmhBbKsgH/0hl+jxjdXrifi5IKeMY02DWggRxX0UElcbVzPlmgLTbvsPeW1PasQ6xE2Q75stkhGbsA==} + engines: {node: '>=18.0.0'} + hasBin: true + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -3010,6 +3110,10 @@ packages: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} + numeric-quantity@2.1.0: + resolution: {integrity: sha512-oDkQ8nFuNVA+unEg1jd6dAS+O7eLXWWzsa4ViI0S0yFi6654GK0s74o8bF8uLRQdWIz/qFF1GABNFPfwAGQUsg==} + engines: {node: '>=16'} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3319,6 +3423,23 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + react-querybuilder@8.14.0: + resolution: {integrity: sha512-uwJn1XT4A6reuxjPmRLUnvewhC4PZksZU4XSxCJgqwR37r2A1/REvxEgv+zVQGVFcd4dUIZs1E++WDabOWVlmA==} + peerDependencies: + react: '>=18' + + react-redux@9.2.0: + resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + react-refresh@0.18.0: resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==} engines: {node: '>=0.10.0'} @@ -3352,6 +3473,14 @@ packages: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} @@ -3371,6 +3500,9 @@ packages: resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} engines: {node: '>=12', npm: '>=6'} + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -4779,6 +4911,14 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jsep-plugin/assignment@1.3.0(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + + '@jsep-plugin/regex@1.0.4(jsep@1.4.0)': + dependencies: + jsep: 1.4.0 + '@malept/cross-spawn-promise@2.0.0': dependencies: cross-spawn: 7.0.6 @@ -5158,6 +5298,13 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + '@react-querybuilder/core@8.14.0': + dependencies: + '@ts-jison/lexer': 0.4.1-alpha.1 + '@ts-jison/parser': 0.4.1-alpha.1 + immer: 11.1.4 + numeric-quantity: 2.1.0 + '@react-spring/animated@9.6.1(react@19.2.3)': dependencies: '@react-spring/shared': 9.6.1(react@19.2.3) @@ -5191,6 +5338,18 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1))(react@19.2.3)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.4 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.1.1 + optionalDependencies: + react: 19.2.3 + react-redux: 9.2.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1) + '@remix-run/router@1.23.2': {} '@rolldown/pluginutils@1.0.0-beta.53': {} @@ -5279,10 +5438,25 @@ snapshots: '@sqltools/formatter@1.2.5': {} + '@standard-schema/spec@1.1.0': {} + + '@standard-schema/utils@0.3.0': {} + '@szmarczak/http-timer@4.0.6': dependencies: defer-to-connect: 2.0.1 + '@ts-jison/common@0.4.1-alpha.1': {} + + '@ts-jison/lexer@0.4.1-alpha.1': + dependencies: + '@ts-jison/common': 0.4.1-alpha.1 + + '@ts-jison/parser@0.4.1-alpha.1': + dependencies: + '@ts-jison/common': 0.4.1-alpha.1 + '@ts-jison/lexer': 0.4.1-alpha.1 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.28.5 @@ -5373,6 +5547,8 @@ snapshots: '@types/triple-beam@1.3.5': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/uuid@11.0.0': dependencies: uuid: 13.0.0 @@ -5974,6 +6150,8 @@ snapshots: clone@1.0.4: {} + clone@2.1.2: {} + clsx@2.1.1: {} codepage@1.15.0: {} @@ -6551,6 +6729,8 @@ snapshots: etag@1.8.1: {} + eventemitter2@6.4.9: {} + expand-template@2.0.3: {} exponential-backoff@3.1.3: {} @@ -6859,6 +7039,8 @@ snapshots: dependencies: has-symbols: 1.1.0 + hash-it@6.0.1: {} + hasown@2.0.2: dependencies: function-bind: 1.1.2 @@ -6922,6 +7104,8 @@ snapshots: ignore@7.0.5: {} + immer@11.1.4: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -7117,10 +7301,19 @@ snapshots: dependencies: argparse: 2.0.1 + jsep@1.4.0: {} + jsesc@3.1.0: {} json-buffer@3.0.1: {} + json-rules-engine@7.3.1: + dependencies: + clone: 2.1.2 + eventemitter2: 6.4.9 + hash-it: 6.0.1 + jsonpath-plus: 10.4.0 + json-schema-traverse@0.4.1: {} json-stable-stringify-without-jsonify@1.0.1: {} @@ -7144,6 +7337,12 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonpath-plus@10.4.0: + dependencies: + '@jsep-plugin/assignment': 1.3.0(jsep@1.4.0) + '@jsep-plugin/regex': 1.0.4(jsep@1.4.0) + jsep: 1.4.0 + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.9 @@ -7402,6 +7601,8 @@ snapshots: normalize-url@6.1.0: {} + numeric-quantity@2.1.0: {} + object-assign@4.1.1: {} object-hash@3.0.0: {} @@ -7725,6 +7926,29 @@ snapshots: react-is@18.3.1: {} + react-querybuilder@8.14.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1): + dependencies: + '@react-querybuilder/core': 8.14.0 + '@reduxjs/toolkit': 2.11.2(react-redux@9.2.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1))(react@19.2.3) + immer: 11.1.4 + react: 19.2.3 + react-redux: 9.2.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1) + transitivePeerDependencies: + - '@types/react' + - drizzle-orm + - json-logic-js + - redux + - sequelize + + react-redux@9.2.0(@types/react@19.2.8)(react@19.2.3)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.3 + use-sync-external-store: 1.6.0(react@19.2.3) + optionalDependencies: + '@types/react': 19.2.8 + redux: 5.0.1 + react-refresh@0.18.0: {} react-router-dom@6.30.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3): @@ -7755,6 +7979,12 @@ snapshots: readdirp@5.0.0: {} + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + reflect-metadata@0.2.2: {} reflect.getprototypeof@1.0.10: @@ -7783,6 +8013,8 @@ snapshots: dependencies: pe-library: 0.4.1 + reselect@5.1.1: {} + resize-observer-polyfill@1.5.1: {} resolve-alpn@1.2.1: {} diff --git a/src/main/repos/EventRepository.ts b/src/main/repos/EventRepository.ts index a7b59a0..324d28d 100644 --- a/src/main/repos/EventRepository.ts +++ b/src/main/repos/EventRepository.ts @@ -224,4 +224,27 @@ export class EventRepository extends Service { const rows = await qb.getRawMany() return { startTime, rows } } + + async getLastScoreTimeByStudents( + studentNames: string[] + ): Promise> { + if (studentNames.length === 0) return new Map() + + const repo = this.ctx.db.dataSource.getRepository(ScoreEventEntity) + const results = await repo + .createQueryBuilder('e') + .select('e.student_name', 'student_name') + .addSelect('MAX(e.event_time)', 'last_time') + .where('e.student_name IN (:...studentNames)', { studentNames }) + .groupBy('e.student_name') + .getRawMany() + + const map = new Map() + for (const row of results) { + if (row.last_time) { + map.set(row.student_name, new Date(row.last_time)) + } + } + return map + } } diff --git a/src/main/services/AutoScoreService.ts b/src/main/services/AutoScoreService.ts index bcc2a6f..5893702 100644 --- a/src/main/services/AutoScoreService.ts +++ b/src/main/services/AutoScoreService.ts @@ -1,16 +1,10 @@ import { Service } from '../../shared/kernel' import { MainContext } from '../context' -import { student } from '../repos/StudentRepository' -import { getTriggerLogic } from '../../shared/triggers' +import { AutoScoreRuleEngine, type RuleConfig, type AutoScoreContext } from '../../shared/autoScore/AutoScoreRuleEngine' -interface AutoScoreRule { +interface AutoScoreRule extends RuleConfig { id: number - enabled: boolean - name: string - studentNames: string[] lastExecuted?: Date - triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[] - actions?: { event: string; value?: string; reason?: string }[] } interface AutoScoreRuleFileData { @@ -19,8 +13,8 @@ interface AutoScoreRuleFileData { name: string studentNames: string[] lastExecuted?: string - triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[] - actions?: { event: string; value?: string; reason?: string }[] + triggers?: Array<{ event: string; value?: string; relation?: 'AND' | 'OR' }> + actions?: Array<{ event: string; value?: string; reason?: string }> } interface AutoScoreRulesFile { @@ -41,9 +35,11 @@ export class AutoScoreService extends Service { private rules: AutoScoreRule[] = [] private timers: Map = new Map() private initialized = false + private ruleEngine: AutoScoreRuleEngine constructor(ctx: MainContext) { super(ctx, 'autoScore') + this.ruleEngine = new AutoScoreRuleEngine() this.registerIpc() } @@ -116,6 +112,17 @@ export class AutoScoreService extends Service { return { success: false, message: err.message } } }) + + this.mainCtx.handle('auto-score:sortRules', async (event, ruleIds) => { + try { + if (!this.mainCtx.permissions.requirePermission(event, 'admin')) + return { success: false, message: 'Permission denied' } + const success = await this.sortRules(ruleIds) + return { success, data: success } + } catch (err: any) { + return { success: false, message: err.message } + } + }) } private async loadRulesFromFile(): Promise { @@ -260,8 +267,11 @@ export class AutoScoreService extends Service { this.stopRules() } + this.ruleEngine = new AutoScoreRuleEngine() + for (const rule of this.rules) { if (rule.enabled) { + await this.ruleEngine.addRule(rule) this.startRuleTimer(rule) } } @@ -280,11 +290,20 @@ export class AutoScoreService extends Service { let primaryTrigger: { event: string; value?: string } | undefined for (const trigger of rule.triggers || []) { - const logic = getTriggerLogic(trigger.event) - if (logic?.calculateNextTime) { - const result = logic.calculateNextTime(trigger.value || '', rule.lastExecuted, now) - if (delayMs === 0 || result.delayMs < delayMs) { - delayMs = result.delayMs + if (trigger.event === 'interval_time_passed') { + const minutes = parseInt(trigger.value || '30', 10) + const intervalMs = minutes * 60 * 1000 + + let nextExecuteTime: Date + if (rule.lastExecuted) { + nextExecuteTime = new Date(rule.lastExecuted.getTime() + intervalMs) + } else { + nextExecuteTime = new Date(now.getTime() + intervalMs) + } + + const triggerDelayMs = nextExecuteTime.getTime() - now.getTime() + if (delayMs === 0 || triggerDelayMs < delayMs) { + delayMs = triggerDelayMs primaryTrigger = trigger } } @@ -314,11 +333,20 @@ export class AutoScoreService extends Service { let primaryTrigger: { event: string; value?: string } | undefined for (const trigger of rule.triggers || []) { - const logic = getTriggerLogic(trigger.event) - if (logic?.calculateNextTime) { - const result = logic.calculateNextTime(trigger.value || '', rule.lastExecuted, now) - if (result.delayMs < minDelayMs) { - minDelayMs = result.delayMs + if (trigger.event === 'interval_time_passed') { + const minutes = parseInt(trigger.value || '30', 10) + const intervalMs = minutes * 60 * 1000 + + let nextExecuteTime: Date + if (rule.lastExecuted) { + nextExecuteTime = new Date(rule.lastExecuted.getTime() + intervalMs) + } else { + nextExecuteTime = new Date(now.getTime() + intervalMs) + } + + const triggerDelayMs = nextExecuteTime.getTime() - now.getTime() + if (triggerDelayMs < minDelayMs) { + minDelayMs = triggerDelayMs primaryTrigger = trigger } } @@ -340,60 +368,25 @@ export class AutoScoreService extends Service { this.logger.info(`Executing auto score rule: ${rule.name}`) const studentRepo = this.mainCtx.students + const eventRepo = this.mainCtx.events - let studentsToScore: student[] = [] - if (rule.studentNames.length === 0) { - const allStudents = await studentRepo.findAll() - studentsToScore = allStudents - } else { - const allStudents = await studentRepo.findAll() - for (const name of rule.studentNames) { - const student = allStudents.find((s) => s.name === name) - if (student) { - studentsToScore.push(student) - } - } + const allStudents = await studentRepo.findAll() + + let studentsToScore = allStudents + if (rule.studentNames.length > 0) { + studentsToScore = allStudents.filter((s) => rule.studentNames.includes(s.name)) } - // 使用AND/OR逻辑评估触发器 - const matchedStudents = this.evaluateTriggersWithLogic(rule, studentsToScore) + const studentNames = studentsToScore.map((s) => s.name) + const lastScoreTimeMap = await eventRepo.getLastScoreTimeByStudents(studentNames) - if (matchedStudents.length > 0) { - for (const action of rule.actions || []) { - await this.executeAction(action, matchedStudents, rule.name) - } - } - - rule.lastExecuted = new Date() - await this.saveRulesToFile() - - this.logger.info( - `Auto score rule executed successfully for ${matchedStudents.length} students` - ) - } catch (error) { - this.logger.error(`Failed to execute auto score rule ${rule.name}:`, { error }) - } - } - - private evaluateTriggersWithLogic(rule: AutoScoreRule, initialStudents: student[]): student[] { - if (!rule.triggers || rule.triggers.length === 0) { - return [] - } - - let resultStudents: student[] = [...initialStudents] - let currentGroup: student[] = [...initialStudents] - let currentRelation: 'AND' | 'OR' = 'AND' - - for (let i = 0; i < rule.triggers.length; i++) { - const trigger = rule.triggers[i] - const logic = getTriggerLogic(trigger.event) - - if (!logic?.check) { - continue - } - - const context = { - students: currentGroup, + const context: AutoScoreContext = { + students: studentsToScore.map((s) => ({ + id: s.id, + name: s.name, + tags: s.tags || [], + lastScoreTime: lastScoreTimeMap.get(s.name) || new Date(0), + })), events: [], rule: { id: rule.id, @@ -405,138 +398,39 @@ export class AutoScoreService extends Service { now: new Date() } - const result = logic.check(context, trigger.value || '') + const results = await this.ruleEngine.run(context) - if (!result.matchedStudents || result.matchedStudents.length === 0) { - // 当前触发器没有匹配的学生 - if (currentRelation === 'AND') { - // AND关系下,任何一个触发器不匹配,整个组就不匹配 - currentGroup = [] - } - // OR关系下,继续评估下一个触发器 - } else { - // 当前触发器有匹配的学生 - if (currentRelation === 'AND') { - // AND关系下,取交集 - currentGroup = currentGroup.filter((student) => - result.matchedStudents!.some((matched) => matched.id === student.id) - ) - } else { - // OR关系下,取并集 - const newStudents = result.matchedStudents.filter( - (matched) => !currentGroup.some((student) => student.id === matched.id) - ) - currentGroup = [...currentGroup, ...newStudents] - } - } - - // 处理下一个关系(如果存在) - if (i < rule.triggers.length - 1) { - const nextRelation = rule.triggers[i + 1].relation || 'AND' - - if (nextRelation !== currentRelation) { - // 关系发生变化,处理当前组的结果 - if (currentRelation === 'AND') { - // AND组结束,如果当前组不为空,则合并到结果 - if (currentGroup.length > 0) { - resultStudents = resultStudents.filter((student) => - currentGroup.some((groupStudent) => groupStudent.id === student.id) - ) - } else { - // AND组为空,整个规则不匹配 - return [] - } - } else { - // OR组结束,合并当前组到结果 - const newStudents = currentGroup.filter( - (groupStudent) => - !resultStudents.some((resultStudent) => resultStudent.id === groupStudent.id) + for (const result of results) { + if (result.matchedStudents.length > 0) { + for (const action of result.actions) { + await this.ruleEngine.executeAction( + action.event, + result.matchedStudents, + { + ...action, + ruleId: rule.id, + ruleName: rule.name, + studentRepo + }, + eventRepo ) - resultStudents = [...resultStudents, ...newStudents] } - - // 重置当前组为所有学生,开始新的关系组 - currentGroup = [...initialStudents] - currentRelation = nextRelation } } - } - // 处理最后一组的结果 - if (currentRelation === 'AND') { - // AND组结束,如果当前组不为空,则合并到结果 - if (currentGroup.length > 0) { - resultStudents = resultStudents.filter((student) => - currentGroup.some((groupStudent) => groupStudent.id === student.id) - ) - } else { - // AND组为空,整个规则不匹配 - return [] - } - } else { - // OR组结束,合并当前组到结果 - const newStudents = currentGroup.filter( - (groupStudent) => - !resultStudents.some((resultStudent) => resultStudent.id === groupStudent.id) + rule.lastExecuted = new Date() + await this.saveRulesToFile() + + this.logger.info( + `Auto score rule executed successfully for ${results.reduce((sum, r) => sum + r.matchedStudents.length, 0)} students` ) - resultStudents = [...resultStudents, ...newStudents] - } - - return resultStudents - } - - private async executeAction( - action: { event: string; value?: string; reason?: string }, - students: student[], - ruleName: string - ) { - const eventRepo = this.mainCtx.events - - switch (action.event) { - case 'add_score': { - const scoreValue = action.value ? parseInt(action.value, 10) : 0 - const reason = action.reason || `自动化加分 - ${ruleName}` - for (const student of students) { - await eventRepo.create({ - student_name: student.name, - reason_content: reason, - delta: scoreValue - }) - } - break - } - case 'add_tag': { - const tagName = action.value - if (tagName) { - const studentRepo = this.mainCtx.students - for (const student of students) { - const currentTags = student.tags || [] - if (!currentTags.includes(tagName)) { - await studentRepo.update(student.id, { - tags: [...currentTags, tagName] - }) - } - } - } - break - } - case 'send_notification': { - this.logger.info(`Notification action: ${action.value}`) - break - } - case 'set_student_status': { - this.logger.info( - `Set student status action: ${action.value} (not implemented - student type has no status field)` - ) - break - } - default: - this.logger.warn(`Unknown action event: ${action.event}`) + } catch (error) { + this.logger.error(`Failed to execute auto score rule ${rule.name}:`, { error }) } } private stopRules() { - for (const [timer] of this.timers) { + for (const timer of this.timers.values()) { clearTimeout(timer) clearInterval(timer) } @@ -555,6 +449,7 @@ export class AutoScoreService extends Service { await this.saveRulesToFile() if (rule.enabled) { + await this.ruleEngine.addRule(newRule) this.startRuleTimer(newRule) } @@ -577,6 +472,7 @@ export class AutoScoreService extends Service { await this.saveRulesToFile() if (updatedRule.enabled) { + await this.ruleEngine.addRule(updatedRule) this.startRuleTimer(updatedRule) } @@ -612,6 +508,7 @@ export class AutoScoreService extends Service { } if (enabled) { + await this.ruleEngine.addRule(rule) this.startRuleTimer(rule) } diff --git a/src/renderer/src/components/AutoScoreManager.tsx b/src/renderer/src/components/AutoScoreManager.tsx index 67a11ea..d4e7990 100644 --- a/src/renderer/src/components/AutoScoreManager.tsx +++ b/src/renderer/src/components/AutoScoreManager.tsx @@ -1,9 +1,4 @@ import { useState, useEffect } from 'react' -import { PlusOutlined, HolderOutlined } from '@ant-design/icons' -import { allTriggers, allActions, triggerRegistry, actionRegistry } from './com.automatically' -import type { TriggerItem, ActionItem } from './com.automatically/types' -import TriggerItemComponent from './com.automatically/TriggerItem' -import ActionItemComponent from './com.automatically/ActionItem' import { Card, Form, @@ -20,6 +15,9 @@ import { } from 'antd' import type { ColumnsType } from 'antd/es/table' +import { RuleComponent } from './autoScore/ruleComponent' +import type { AutoScoreRuleData } from './autoScore/ruleBuilderUtils' + interface AutoScoreRule { id: number enabled: boolean @@ -43,8 +41,10 @@ export const AutoScoreManager: React.FC = () => { const [pageSize, setPageSize] = useState(50) const [form] = Form.useForm() const [editingRuleId, setEditingRuleId] = useState(null) - const [triggerList, setTriggerList] = useState([]) - const [actionList, setActionList] = useState([]) + const [ruleData, setRuleData] = useState({ + triggers: [], + actions: [] + }) const [messageApi, contextHolder] = message.useMessage() const fetchRules = async () => { @@ -97,35 +97,24 @@ export const AutoScoreManager: React.FC = () => { return } - if (triggerList.length === 0) { + if (ruleData.triggers.length === 0) { messageApi.warning('请至少添加一个触发器') return } - if (actionList.length === 0) { + if (ruleData.actions.length === 0) { messageApi.warning('请至少添加一个行动') return } const studentNames = Array.isArray(values.studentNames) ? values.studentNames : [] - const triggersPayload = triggerList.map((t) => ({ - event: t.eventName, - value: t.value, - relation: t.relation - })) - const actionsPayload = actionList.map((a) => ({ - event: a.eventName, - value: a.value, - reason: a.reason - })) - - const ruleData = { + const ruleDataToSubmit = { enabled: true, name: values.name, studentNames, - triggers: triggersPayload, - actions: actionsPayload + triggers: ruleData.triggers, + actions: ruleData.actions } try { @@ -139,14 +128,14 @@ export const AutoScoreManager: React.FC = () => { } try { - let res + let res: { success: boolean; message?: string; data?: any } if (editingRuleId !== null) { res = await (window as any).api.invoke('auto-score:updateRule', { id: editingRuleId, - ...ruleData + ...ruleDataToSubmit }) } else { - res = await (window as any).api.invoke('auto-score:addRule', ruleData) + res = await (window as any).api.invoke('auto-score:addRule', ruleDataToSubmit) } if (res.success) { @@ -156,8 +145,7 @@ export const AutoScoreManager: React.FC = () => { studentNames: '' }) setEditingRuleId(null) - setTriggerList([]) - setActionList([]) + setRuleData({ triggers: [], actions: [] }) fetchRules() } else { messageApi.error( @@ -174,30 +162,12 @@ export const AutoScoreManager: React.FC = () => { setEditingRuleId(rule.id) form.setFieldsValue({ name: rule.name, - studentNames: rule.studentNames.join(', ') + studentNames: rule.studentNames + }) + setRuleData({ + triggers: rule.triggers || [], + actions: rule.actions || [] }) - if (rule.triggers && Array.isArray(rule.triggers)) { - const mapped = rule.triggers.map((t, idx) => ({ - id: idx + 1, - eventName: t.event, - value: t.value ?? '', - relation: t.relation ?? 'AND' - })) - setTriggerList(mapped) - } else { - setTriggerList([]) - } - if (rule.actions && Array.isArray(rule.actions)) { - const mapped = rule.actions.map((a, idx) => ({ - id: idx + 1, - eventName: a.event, - value: a.value ?? '', - reason: a.reason ?? '' - })) - setActionList(mapped) - } else { - setActionList([]) - } } const handleDelete = async (ruleId: number) => { @@ -258,85 +228,10 @@ export const AutoScoreManager: React.FC = () => { studentNames: '' }) setEditingRuleId(null) - setTriggerList([]) - setActionList([]) - } - - const handleAddTrigger = () => { - const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1 - const defaultTrigger = allTriggers.list[0] - if (!defaultTrigger) { - messageApi.error('没有可用的触发器类型,请检查配置') - return - } - setTriggerList((prev) => [ - ...prev, - { - id: nextId, - eventName: defaultTrigger.eventName, - value: '', - relation: 'AND' - } - ]) - } - - const handleDeleteTrigger = (id: number) => { - setTriggerList((prev) => prev.filter((t) => t.id !== id)) - } - - const handleTriggerChange = (id: number, eventName: string) => { - setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, eventName, value: '' } : t))) - } - - const handleTriggerValueChange = (id: number, value: string) => { - setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, value } : t))) - } - - const handleTriggerRelationChange = (id: number, relation: 'AND' | 'OR') => { - setTriggerList((prev) => prev.map((t) => (t.id === id ? { ...t, relation } : t))) - } - - const handleAddAction = () => { - const nextId = actionList.length ? Math.max(...actionList.map((a) => a.id)) + 1 : 1 - const defaultAction = allActions.list[0] - if (!defaultAction) { - messageApi.error('没有可用的行动类型,请检查配置') - return - } - setActionList((prev) => [ - ...prev, - { - id: nextId, - eventName: defaultAction.eventName, - value: '', - reason: '' - } - ]) - } - - const handleDeleteAction = (id: number) => { - setActionList((prev) => prev.filter((a) => a.id !== id)) - } - - const handleActionChange = (id: number, eventName: string) => { - setActionList((prev) => prev.map((a) => (a.id === id ? { ...a, eventName, value: '' } : a))) - } - - const handleActionValueChange = (id: number, value: string) => { - setActionList((prev) => prev.map((a) => (a.id === id ? { ...a, value } : a))) - } - - const handleActionReasonChange = (id: number, reason: string) => { - setActionList((prev) => prev.map((a) => (a.id === id ? { ...a, reason } : a))) + setRuleData({ triggers: [], actions: [] }) } const columns: ColumnsType = [ - { - key: 'drag', - title: '排序', - width: 60, - render: () => - }, { title: '状态', dataIndex: 'enabled', @@ -356,15 +251,7 @@ export const AutoScoreManager: React.FC = () => { if (!triggers || triggers.length === 0) { return } - const triggerLabels = triggers.map((t) => { - const def = triggerRegistry.get(t.event) - return def?.label || t.event - }) - return ( - - {triggers.length} 个触发器 - - ) + return {triggers.length} 个触发器 } }, { @@ -376,15 +263,7 @@ export const AutoScoreManager: React.FC = () => { if (!actions || actions.length === 0) { return } - const actionLabels = actions.map((a) => { - const def = actionRegistry.get(a.event) - return def?.label || a.event - }) - return ( - - {actions.length} 个行动 - - ) + return {actions.length} 个行动 } }, { @@ -438,33 +317,6 @@ export const AutoScoreManager: React.FC = () => { } ] - const triggerItems = triggerList - .filter((t) => t.eventName !== null) - .map((item, idx) => ( - - )) - - const actionItems = actionList - .filter((a) => a.eventName !== null) - .map((item) => ( - - )) - return (
{contextHolder} @@ -491,6 +343,10 @@ export const AutoScoreManager: React.FC = () => {
+
+ setRuleData(data)} /> +
+
- - - - - - {actionItems} - - - - {contextHolder} - +
diff --git a/src/renderer/src/components/autoScore/actionComponent.tsx b/src/renderer/src/components/autoScore/actionComponent.tsx new file mode 100644 index 0000000..6b4579b --- /dev/null +++ b/src/renderer/src/components/autoScore/actionComponent.tsx @@ -0,0 +1,94 @@ +import { Space, Input, InputNumber, Select, Button } from 'antd' +import { PlusOutlined, DeleteOutlined } from '@ant-design/icons' +import type { AutoScoreAction } from './ruleBuilderUtils' + +interface ActionComponentProps { + actions: AutoScoreAction[] + onAdd: () => void + onRemove: (index: number) => void + onChange: (index: number, field: keyof AutoScoreAction, value: any) => void +} + +export const ActionComponent: React.FC = ({ + actions, + onAdd, + onRemove, + onChange +}) => { + const handleActionChange = (index: number, field: keyof AutoScoreAction, value: any) => { + onChange(index, field, value) + } + + return ( +
+ + {actions.map((action, index) => ( +
+ + handleActionChange(index, 'value', e.target.value)} + placeholder="标签名称" + style={{ width: 200 }} + /> + )} + + handleActionChange(index, 'reason', e.target.value)} + placeholder="操作说明(可选)" + style={{ flex: 1, minWidth: 200 }} + /> + +
+ ))} + + +
+
+ ) +} + +export default ActionComponent diff --git a/src/renderer/src/components/autoScore/ruleBuilderOverride.css b/src/renderer/src/components/autoScore/ruleBuilderOverride.css new file mode 100644 index 0000000..3a41518 --- /dev/null +++ b/src/renderer/src/components/autoScore/ruleBuilderOverride.css @@ -0,0 +1,307 @@ +.queryBuilder { + --querybuilder-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; + --querybuilder-background: var(--ss-card-bg, #ffffff); + --querybuilder-color: var(--ss-text-main, rgba(0, 0, 0, 0.85)); + --querybuilder-border-radius: 6px; + --querybuilder-border-color: #d9d9d9; + --querybuilder-disabled-color: rgba(0, 0, 0, 0.25); + font-family: var(--querybuilder-font-family); + font-size: 14px; + line-height: 1.5715; + color: var(--querybuilder-color); + background: var(--querybuilder-background); + padding: 0; + border-radius: var(--querybuilder-border-radius); +} + +.queryBuilder .ruleGroup { + background: var(--querybuilder-background); + border: 1px solid var(--querybuilder-border-color); + border-radius: var(--querybuilder-border-radius); + padding: 8px; + margin: 0; +} + +.queryBuilder .ruleGroup-header { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + margin-bottom: 8px; +} + +.queryBuilder .ruleGroup-body { + padding-left: 20px; + border-left: 2px solid var(--querybuilder-border-color); + margin-left: 0; +} + +.queryBuilder .rule { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + padding: 4px 8px; + margin: 0; + background: rgba(0, 0, 0, 0.02); + border-radius: 4px; + border: 1px solid transparent; + transition: all 0.3s; +} + +.queryBuilder .rule:hover { + background: rgba(0, 0, 0, 0.04); + border-color: var(--querybuilder-border-color); +} + +.queryBuilder select, +.queryBuilder .ruleGroup-combinators select { + appearance: none; + background: var(--querybuilder-background); + border: 1px solid var(--querybuilder-border-color); + border-radius: var(--querybuilder-border-radius); + padding: 4px 28px 4px 11px; + height: 32px; + font-size: 14px; + color: var(--querybuilder-color); + cursor: pointer; + transition: all 0.3s; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='rgba(0,0,0,0.25)' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 8px center; +} + +.queryBuilder select:hover, +.queryBuilder .ruleGroup-combinators select:hover { + border-color: #40a9ff; +} + +.queryBuilder select:focus, +.queryBuilder .ruleGroup-combinators select:focus { + outline: none; + border-color: #40a9ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); +} + +.queryBuilder input[type='text'], +.queryBuilder input[type='number'], +.queryBuilder input:not([type]) { + background: var(--querybuilder-background); + border: 1px solid var(--querybuilder-border-color); + border-radius: var(--querybuilder-border-radius); + padding: 4px 11px; + height: 32px; + font-size: 14px; + color: var(--querybuilder-color); + transition: all 0.3s; + min-width: 120px; +} + +.queryBuilder input[type='text']:hover, +.queryBuilder input[type='number']:hover, +.queryBuilder input:not([type]):hover { + border-color: #40a9ff; +} + +.queryBuilder input[type='text']:focus, +.queryBuilder input[type='number']:focus, +.queryBuilder input:not([type]):focus { + outline: none; + border-color: #40a9ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); +} + +.queryBuilder input::placeholder { + color: #bfbfbf; +} + +.queryBuilder button { + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 400; + white-space: nowrap; + text-align: center; + background-image: none; + border: 1px solid transparent; + cursor: pointer; + transition: all 0.3s; + height: 32px; + padding: 4px 15px; + font-size: 14px; + border-radius: var(--querybuilder-border-radius); + color: var(--querybuilder-color); + background: var(--querybuilder-background); + border-color: var(--querybuilder-border-color); +} + +.queryBuilder button:hover { + color: #40a9ff; + border-color: #40a9ff; +} + +.queryBuilder button:focus { + outline: none; + color: #40a9ff; + border-color: #40a9ff; +} + +.queryBuilder button.ruleGroup-addRule, +.queryBuilder button.ruleGroup-addGroup { + color: #fff; + background: var(--ant-color-primary, #1677ff); + border-color: var(--ant-color-primary, #1677ff); + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12); + box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045); +} + +.queryBuilder button.ruleGroup-addRule:hover, +.queryBuilder button.ruleGroup-addGroup:hover { + color: #fff; + background: var(--ant-color-primary-hover, #4096ff); + border-color: var(--ant-color-primary-hover, #4096ff); +} + +.queryBuilder button.ruleGroup-addRule:focus, +.queryBuilder button.ruleGroup-addGroup:focus { + color: #fff; + background: var(--ant-color-primary-hover, #4096ff); + border-color: var(--ant-color-primary-hover, #4096ff); +} + +.queryBuilder button.rule-remove, +.queryBuilder button.ruleGroup-remove { + color: #ff4d4f; + border-color: #ff4d4f; + background: transparent; +} + +.queryBuilder button.rule-remove:hover, +.queryBuilder button.ruleGroup-remove:hover { + color: #ff7875; + border-color: #ff7875; +} + +.queryBuilder button.rule-remove:focus, +.queryBuilder button.ruleGroup-remove:focus { + color: #ff7875; + border-color: #ff7875; +} + +.queryBuilder .ruleGroup-combinators { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.queryBuilder .betweenRules { + display: inline-flex; + align-items: center; + gap: 4px; +} + +.queryBuilder .betweenRules .operator-joiner { + color: var(--querybuilder-color); + padding: 0 4px; +} + +.queryBuilder .ruleGroup-notToggle { + display: inline-flex; + align-items: center; + gap: 4px; + margin-left: 8px; + color: var(--querybuilder-color); +} + +.queryBuilder .ruleGroup-notToggle input[type='checkbox'] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: #1890ff; +} + +.queryBuilder .dragHandle { + cursor: move; + color: var(--querybuilder-disabled-color); + padding: 4px; +} + +.queryBuilder .dragHandle:hover { + color: var(--querybuilder-color); +} + +.queryBuilder .shiftActions { + display: inline-flex; + gap: 4px; + margin-left: 8px; +} + +.queryBuilder .shiftActions button { + padding: 4px 8px; + min-width: auto; +} + +.queryBuilder .valueEditor-caseSensitive, +.queryBuilder .valueEditor-caseSensitive input[type='checkbox'] { + cursor: pointer; +} + +.queryBuilder .valueEditor-caseSensitive input[type='checkbox'] { + width: 14px; + height: 14px; + accent-color: #1890ff; +} + +.queryBuilder .valueSource-dropdown { + min-width: 100px; +} + +.queryBuilder:disabled, +.queryBuilder .disabled { + opacity: 0.65; + cursor: not-allowed; + pointer-events: none; +} + +.queryBuilder .validation-error { + color: #ff4d4f; + font-size: 12px; + margin-top: 4px; +} + +.queryBuilder .rule-with-error { + border-color: #ff4d4f !important; +} + +.queryBuilder .rule-with-error input, +.queryBuilder .rule-with-error select { + border-color: #ff4d4f !important; +} + +.queryBuilder .rule-with-error input:focus, +.queryBuilder .rule-with-error select:focus { + box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.2) !important; +} + +@media (prefers-color-scheme: dark) { + .queryBuilder { + --querybuilder-border-color: #434343; + --querybuilder-disabled-color: rgba(255, 255, 255, 0.3); + } + + .queryBuilder .rule { + background: rgba(255, 255, 255, 0.04); + } + + .queryBuilder .rule:hover { + background: rgba(255, 255, 255, 0.08); + } + + .queryBuilder select { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='rgba(255,255,255,0.3)' d='M6 8L1 3h10z'/%3E%3C/svg%3E"); + } + + .queryBuilder input::placeholder { + color: rgba(255, 255, 255, 0.3); + } +} diff --git a/src/renderer/src/components/autoScore/ruleBuilderUtils.ts b/src/renderer/src/components/autoScore/ruleBuilderUtils.ts new file mode 100644 index 0000000..d521d88 --- /dev/null +++ b/src/renderer/src/components/autoScore/ruleBuilderUtils.ts @@ -0,0 +1,109 @@ +import type { RuleGroupType, Field, Operator } from 'react-querybuilder' + +export interface AutoScoreTrigger { + event: string + value?: string + relation?: 'AND' | 'OR' +} + +export interface AutoScoreAction { + event: string + value?: string + reason?: string +} + +export interface AutoScoreRuleData { + triggers: AutoScoreTrigger[] + actions: AutoScoreAction[] +} + +export const TRIGGER_TYPES = { + interval_time_passed: { + label: '间隔时间', + description: '每隔指定时间自动触发' + }, + student_has_tag: { + label: '学生标签', + description: '当学生拥有指定标签时触发' + } +} + +export const ACTION_TYPES = { + add_score: { + label: '加分', + description: '为学生增加分数' + }, + add_tag: { + label: '添加标签', + description: '为学生添加标签' + } +} + +export const fields: Field[] = [ + { name: 'interval_time_passed', label: '间隔时间(分钟)', placeholder: '例如:1440' }, + { name: 'student_has_tag', label: '学生标签', placeholder: '例如:优秀学生,班干部' } +] + +export const operators: Operator[] = [ + { name: '=', label: '等于' }, + { name: 'contains', label: '包含' } +] + +export const defaultQuery: RuleGroupType = { + combinator: 'and', + rules: [{ field: 'interval_time_passed', operator: '=', value: '1440' }] +} + +export function queryToAutoScoreRule(query: RuleGroupType): AutoScoreRuleData { + const triggers: AutoScoreTrigger[] = [] + + const processRuleGroup = (group: RuleGroupType, relation: 'AND' | 'OR' = 'AND') => { + group.rules.forEach((rule, index) => { + if ('rules' in rule) { + processRuleGroup(rule, group.combinator === 'and' ? 'AND' : 'OR') + } else { + const trigger: AutoScoreTrigger = { + event: rule.field, + value: rule.value as string + } + + if (index > 0) { + trigger.relation = relation + } + + triggers.push(trigger) + } + }) + } + + processRuleGroup(query) + + return { + triggers, + actions: [] + } +} + +export function autoScoreRuleToQuery(ruleData: AutoScoreRuleData): RuleGroupType { + const rules: any[] = [] + let currentCombinator: 'and' | 'or' = 'and' + + ruleData.triggers.forEach((trigger, index) => { + if (index === 0) { + currentCombinator = 'and' + } else if (trigger.relation === 'OR') { + currentCombinator = 'or' + } + + rules.push({ + field: trigger.event, + operator: '=', + value: trigger.value || '' + }) + }) + + return { + combinator: currentCombinator, + rules: rules.length > 0 ? rules : defaultQuery.rules + } +} diff --git a/src/renderer/src/components/autoScore/ruleComponent.tsx b/src/renderer/src/components/autoScore/ruleComponent.tsx new file mode 100644 index 0000000..9dc137e --- /dev/null +++ b/src/renderer/src/components/autoScore/ruleComponent.tsx @@ -0,0 +1,82 @@ +import { formatQuery, QueryBuilder, RuleGroupType } from 'react-querybuilder' +import { useState, useEffect } from 'react' +import { + fields, + operators, + defaultQuery, + queryToAutoScoreRule, + autoScoreRuleToQuery, + type AutoScoreRuleData, + AutoScoreAction +} from './ruleBuilderUtils' +import { Card } from 'antd' + +import './ruleBuilderOverride.css' +import { ActionComponent } from './actionComponent' + +interface RuleComponentProps { + initialData?: AutoScoreRuleData + onChange?: (data: AutoScoreRuleData) => void +} + +export const RuleComponent: React.FC = ({ initialData, onChange }) => { + const [query, setQuery] = useState( + initialData ? autoScoreRuleToQuery(initialData) : defaultQuery + ) + const [actions, setActions] = useState(initialData?.actions || []) + + useEffect(() => { + if (onChange) { + const ruleData = queryToAutoScoreRule(query) + onChange({ + ...ruleData, + actions + }) + } + }, [query, actions]) + + const handleQueryChange = (newQuery: RuleGroupType) => { + setQuery(newQuery) + } + + const handleAddAction = () => { + setActions([...actions, { event: 'add_score', value: '5', reason: '' }]) + } + + const handleRemoveAction = (index: number) => { + setActions(actions.filter((_, i) => i !== index)) + } + + const handleActionChange = (index: number, field: keyof AutoScoreAction, value: any) => { + const newActions = [...actions] + newActions[index] = { ...newActions[index], [field]: value } + setActions(newActions) + } + + return ( +
+ + +
+
{formatQuery(query, 'json')}
+
+
+ + + + +
+ ) +} + +export default RuleComponent diff --git a/src/renderer/src/components/com.automatically/ActionItem.tsx b/src/renderer/src/components/com.automatically/ActionItem.tsx deleted file mode 100644 index 7289657..0000000 --- a/src/renderer/src/components/com.automatically/ActionItem.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Button, Select } from 'antd' -import { DeleteOutlined } from '@ant-design/icons' -import { actionRegistry, allActions } from './registry' -import type { ActionItem as ActionItemType } from './types' - -interface ActionItemProps { - item: ActionItemType - onDelete: (id: number) => void - onChange: (id: number, eventName: string) => void - onValueChange: (id: number, value: string) => void - onReasonChange: (id: number, reason: string) => void -} - -const ActionItem: React.FC = ({ - item, - onDelete, - onChange, - onValueChange, - onReasonChange -}) => { - const definition = actionRegistry.get(item.eventName) - const Component = definition?.component - - return ( -
-
- ) -} - -export default TriggerItem diff --git a/src/renderer/src/components/com.automatically/actions/AddScoreAction.tsx b/src/renderer/src/components/com.automatically/actions/AddScoreAction.tsx deleted file mode 100644 index 7865bd7..0000000 --- a/src/renderer/src/components/com.automatically/actions/AddScoreAction.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { Input } from 'antd' -import type { ActionComponentProps } from '../types' - -export const eventName = 'add_score' -export const label = '添加分数' -export const description = '为学生添加分数' -export const hasReason = true - -const AddScoreAction: React.FC = ({ - value, - reason, - onChange, - onReasonChange -}) => { - return ( - <> - onChange(e.target.value ? String(e.target.value) : '')} - /> - onReasonChange?.(e.target.value ? String(e.target.value) : '')} - /> - - ) -} - -export default AddScoreAction diff --git a/src/renderer/src/components/com.automatically/actions/AddTagAction.tsx b/src/renderer/src/components/com.automatically/actions/AddTagAction.tsx deleted file mode 100644 index 0006148..0000000 --- a/src/renderer/src/components/com.automatically/actions/AddTagAction.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Input } from 'antd' -import type { ActionComponentProps } from '../types' - -export const eventName = 'add_tag' -export const label = '添加标签' -export const description = '为学生添加标签' -export const hasReason = false - -const AddTagAction: React.FC = ({ value, onChange }) => { - return ( - onChange(e.target.value ? String(e.target.value) : '')} - /> - ) -} - -export default AddTagAction diff --git a/src/renderer/src/components/com.automatically/actions/SendNotificationAction.tsx b/src/renderer/src/components/com.automatically/actions/SendNotificationAction.tsx deleted file mode 100644 index 4e2acbb..0000000 --- a/src/renderer/src/components/com.automatically/actions/SendNotificationAction.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Input } from 'antd' -import type { ActionComponentProps } from '../types' - -export const eventName = 'send_notification' -export const label = '发送通知' -export const description = '向学生发送通知' -export const hasReason = false - -const SendNotificationAction: React.FC = ({ value, onChange }) => { - return ( - onChange(e.target.value ? String(e.target.value) : '')} - /> - ) -} - -export default SendNotificationAction diff --git a/src/renderer/src/components/com.automatically/actions/index.ts b/src/renderer/src/components/com.automatically/actions/index.ts deleted file mode 100644 index c37133b..0000000 --- a/src/renderer/src/components/com.automatically/actions/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { actionRegistry } from '../registry' -import type { ActionDefinition } from '../types' - -import AddScoreAction, { - eventName as addScoreEventName, - label as addScoreLabel, - description as addScoreDescription, - hasReason as addScoreHasReason -} from './AddScoreAction' - -import AddTagAction, { - eventName as addTagEventName, - label as addTagLabel, - description as addTagDescription, - hasReason as addTagHasReason -} from './AddTagAction' - -import SendNotificationAction, { - eventName as sendNotificationEventName, - label as sendNotificationLabel, - description as sendNotificationDescription, - hasReason as sendNotificationHasReason -} from './SendNotificationAction' - -const actionDefinitions: ActionDefinition[] = [ - { - eventName: addScoreEventName, - label: addScoreLabel, - description: addScoreDescription, - component: AddScoreAction, - hasReason: addScoreHasReason - }, - { - eventName: addTagEventName, - label: addTagLabel, - description: addTagDescription, - component: AddTagAction, - hasReason: addTagHasReason - }, - { - eventName: sendNotificationEventName, - label: sendNotificationLabel, - description: sendNotificationDescription, - component: SendNotificationAction, - hasReason: sendNotificationHasReason - } -] - -actionDefinitions.forEach((def) => actionRegistry.register(def)) - -export { AddScoreAction, AddTagAction, SendNotificationAction } diff --git a/src/renderer/src/components/com.automatically/index.ts b/src/renderer/src/components/com.automatically/index.ts deleted file mode 100644 index b1f962c..0000000 --- a/src/renderer/src/components/com.automatically/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import './triggers' -import './actions' - -export { triggerRegistry, actionRegistry, allTriggers, allActions } from './registry' -export type { - TriggerComponentProps, - ActionComponentProps, - TriggerDefinition, - ActionDefinition, - TriggerItem, - ActionItem -} from './types' -export { default as TriggerItemComponent } from './TriggerItem' -export { default as ActionItemComponent } from './ActionItem' - -export { IntervalTimeTrigger, StudentTagTrigger, RandomTimeTrigger } from './triggers' -export { AddScoreAction, AddTagAction, SendNotificationAction } from './actions' diff --git a/src/renderer/src/components/com.automatically/registry.ts b/src/renderer/src/components/com.automatically/registry.ts deleted file mode 100644 index 687c35a..0000000 --- a/src/renderer/src/components/com.automatically/registry.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { TriggerDefinition, ActionDefinition, TriggerLogic } from './types' - -class TriggerRegistry { - private triggers: Map = new Map() - - register(definition: TriggerDefinition) { - this.triggers.set(definition.eventName, definition) - } - - get(eventName: string): TriggerDefinition | undefined { - return this.triggers.get(eventName) - } - - getAll(): TriggerDefinition[] { - return Array.from(this.triggers.values()) - } - - getOptions() { - return this.getAll().map((t) => ({ label: t.label, value: t.eventName })) - } - - getLogic(eventName: string): TriggerLogic | undefined { - return this.triggers.get(eventName)?.triggerLogic - } -} - -class ActionRegistry { - private actions: Map = new Map() - - register(definition: ActionDefinition) { - this.actions.set(definition.eventName, definition) - } - - get(eventName: string): ActionDefinition | undefined { - return this.actions.get(eventName) - } - - getAll(): ActionDefinition[] { - return Array.from(this.actions.values()) - } - - getOptions() { - return this.getAll().map((a) => ({ label: a.label, value: a.eventName })) - } -} - -export const triggerRegistry = new TriggerRegistry() -export const actionRegistry = new ActionRegistry() - -export const allTriggers = { - get list() { - return triggerRegistry.getAll() - }, - get options() { - return triggerRegistry.getOptions() - } -} - -export const allActions = { - get list() { - return actionRegistry.getAll() - }, - get options() { - return actionRegistry.getOptions() - } -} diff --git a/src/renderer/src/components/com.automatically/triggers/IntervalTimeTrigger.tsx b/src/renderer/src/components/com.automatically/triggers/IntervalTimeTrigger.tsx deleted file mode 100644 index e8d4e8f..0000000 --- a/src/renderer/src/components/com.automatically/triggers/IntervalTimeTrigger.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useState } from 'react' -import { InputNumber, Space, Radio } from 'antd' -import type { TriggerComponentProps } from '../types' - -export const eventName = 'interval_time_passed' -export const label = '根据间隔时间触发' -export const description = '当间隔时间到达时触发自动化' -export const triggerLogic = { - eventName, - label, - description, - validate: (value: string) => { - const minutes = parseInt(value, 10) - if (isNaN(minutes) || minutes <= 0) { - return { valid: false, message: '请输入有效的时间间隔(分钟)' } - } - return { valid: true } - } -} - -const IntervalTimeTrigger: React.FC = ({ value, onChange }) => { - const numValue = value ? parseInt(value, 10) : undefined - const [unit, setUnit] = useState<'minutes' | 'days'>('minutes') - - const handleChange = (v: number | null) => { - const numV = v - if (numV === undefined || numV === null || isNaN(numV)) { - onChange('') - return - } - const minutes = unit === 'minutes' ? numV : numV * 1440 - onChange(String(Math.round(minutes))) - } - - const displayValue = - numValue === undefined || isNaN(numValue) - ? undefined - : unit === 'minutes' - ? numValue - : Math.max(1, Math.round(numValue / 1440)) - - return ( - - - setUnit(e.target.value as 'minutes' | 'days')} - optionType="button" - buttonStyle="solid" - > - - 分钟 - - - ) -} - -export default IntervalTimeTrigger diff --git a/src/renderer/src/components/com.automatically/triggers/RandomTimeTrigger.tsx b/src/renderer/src/components/com.automatically/triggers/RandomTimeTrigger.tsx deleted file mode 100644 index e69e006..0000000 --- a/src/renderer/src/components/com.automatically/triggers/RandomTimeTrigger.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { InputNumber, Space } from 'antd' -import type { TriggerComponentProps } from '../types' - -export const eventName = 'random_time_reached' -export const label = '随机时间触发' -export const description = '在指定时间范围内随机触发自动化' -export const triggerLogic = { - eventName, - label, - description, - validate: (value: string) => { - const minutes = parseInt(value, 10) - if (isNaN(minutes) || minutes <= 0) { - return { valid: false, message: '请输入有效的时间范围(分钟)' } - } - return { valid: true } - } -} - -interface RandomTimeConfig { - minHour: number - maxHour: number -} - -const RandomTimeTrigger: React.FC = ({ value, onChange }) => { - let config: RandomTimeConfig = { minHour: 9, maxHour: 18 } - try { - if (value) { - const parsed = JSON.parse(value) - config = { ...config, ...parsed } - } - } catch (e) { - console.debug('RandomTimeTrigger parse error', e) - } - - const handleChange = (key: keyof RandomTimeConfig, v: number | null) => { - const numV = v ?? (key === 'minHour' ? 0 : 23) - const newConfig = { ...config, [key]: numV } - onChange(JSON.stringify(newConfig)) - } - - return ( - -
- - handleChange('minHour', v)} - min={0} - max={23} - /> - -
-
- - handleChange('maxHour', v)} - min={0} - max={23} - /> - -
-
- ) -} - -export default RandomTimeTrigger diff --git a/src/renderer/src/components/com.automatically/triggers/StudentTagTrigger.tsx b/src/renderer/src/components/com.automatically/triggers/StudentTagTrigger.tsx deleted file mode 100644 index dc09ea9..0000000 --- a/src/renderer/src/components/com.automatically/triggers/StudentTagTrigger.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import { useState, useEffect } from 'react' -import { Select } from 'antd' -import type { TriggerComponentProps } from '../types' - -export const eventName = 'student_tag_matched' -export const label = '当学生匹配标签时触发' -export const description = '当学生匹配特定标签时触发自动化' -export const triggerLogic = { - eventName, - label, - description, - validate: (value: string) => { - if (!value || value.trim() === '') { - return { valid: false, message: '请输入标签名称' } - } - return { valid: true } - } -} - -const StudentTagTrigger: React.FC = ({ value, onChange }) => { - const [tags, setTags] = useState<{ label: string; value: string }[]>([]) - - useEffect(() => { - const fetchTags = async () => { - try { - const res = await (window as any).api.tagsGetAll() - if (res.success && res.data) { - setTags(res.data.map((tag: any) => ({ label: tag.name, value: tag.name }))) - } - } catch (e) { - console.error('Failed to fetch tags', e) - } - } - fetchTags() - }, []) - - return ( -