mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 19:04:23 +08:00
FEFACTARY:自动化加分beta
This commit is contained in:
@@ -40,10 +40,12 @@
|
|||||||
"chokidar": "^5.0.0",
|
"chokidar": "^5.0.0",
|
||||||
"es-object-atoms": "^1.1.1",
|
"es-object-atoms": "^1.1.1",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
|
"json-rules-engine": "^7.3.1",
|
||||||
"mica-electron": "^1.5.16",
|
"mica-electron": "^1.5.16",
|
||||||
"os": "^0.1.2",
|
"os": "^0.1.2",
|
||||||
"pg": "^8.19.0",
|
"pg": "^8.19.0",
|
||||||
"pinyin-pro": "^3.27.0",
|
"pinyin-pro": "^3.27.0",
|
||||||
|
"react-querybuilder": "^8.14.0",
|
||||||
"react-router-dom": "^6.28.0",
|
"react-router-dom": "^6.28.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"typeorm": "^0.3.27",
|
"typeorm": "^0.3.27",
|
||||||
|
|||||||
Generated
+232
@@ -47,6 +47,9 @@ importers:
|
|||||||
express:
|
express:
|
||||||
specifier: ^5.2.1
|
specifier: ^5.2.1
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
|
json-rules-engine:
|
||||||
|
specifier: ^7.3.1
|
||||||
|
version: 7.3.1
|
||||||
mica-electron:
|
mica-electron:
|
||||||
specifier: ^1.5.16
|
specifier: ^1.5.16
|
||||||
version: 1.5.16
|
version: 1.5.16
|
||||||
@@ -57,6 +60,9 @@ importers:
|
|||||||
pinyin-pro:
|
pinyin-pro:
|
||||||
specifier: ^3.27.0
|
specifier: ^3.27.0
|
||||||
version: 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:
|
react-router-dom:
|
||||||
specifier: ^6.28.0
|
specifier: ^6.28.0
|
||||||
version: 6.30.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
version: 6.30.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||||
@@ -819,6 +825,18 @@ packages:
|
|||||||
'@jridgewell/trace-mapping@0.3.31':
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
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':
|
'@malept/cross-spawn-promise@2.0.0':
|
||||||
resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==}
|
resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==}
|
||||||
engines: {node: '>= 12.13.0'}
|
engines: {node: '>= 12.13.0'}
|
||||||
@@ -1126,6 +1144,20 @@ packages:
|
|||||||
react: '>=16.9.0'
|
react: '>=16.9.0'
|
||||||
react-dom: '>=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':
|
'@react-spring/animated@9.6.1':
|
||||||
resolution: {integrity: sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==}
|
resolution: {integrity: sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1153,6 +1185,17 @@ packages:
|
|||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
react-dom: ^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':
|
'@remix-run/router@1.23.2':
|
||||||
resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==}
|
resolution: {integrity: sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@@ -1194,66 +1237,79 @@ packages:
|
|||||||
resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==}
|
resolution: {integrity: sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.55.1':
|
'@rollup/rollup-linux-arm-musleabihf@4.55.1':
|
||||||
resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==}
|
resolution: {integrity: sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.55.1':
|
'@rollup/rollup-linux-arm64-gnu@4.55.1':
|
||||||
resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==}
|
resolution: {integrity: sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.55.1':
|
'@rollup/rollup-linux-arm64-musl@4.55.1':
|
||||||
resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==}
|
resolution: {integrity: sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-gnu@4.55.1':
|
'@rollup/rollup-linux-loong64-gnu@4.55.1':
|
||||||
resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==}
|
resolution: {integrity: sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-loong64-musl@4.55.1':
|
'@rollup/rollup-linux-loong64-musl@4.55.1':
|
||||||
resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==}
|
resolution: {integrity: sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-gnu@4.55.1':
|
'@rollup/rollup-linux-ppc64-gnu@4.55.1':
|
||||||
resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==}
|
resolution: {integrity: sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-ppc64-musl@4.55.1':
|
'@rollup/rollup-linux-ppc64-musl@4.55.1':
|
||||||
resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==}
|
resolution: {integrity: sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.55.1':
|
'@rollup/rollup-linux-riscv64-gnu@4.55.1':
|
||||||
resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==}
|
resolution: {integrity: sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.55.1':
|
'@rollup/rollup-linux-riscv64-musl@4.55.1':
|
||||||
resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==}
|
resolution: {integrity: sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.55.1':
|
'@rollup/rollup-linux-s390x-gnu@4.55.1':
|
||||||
resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==}
|
resolution: {integrity: sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.55.1':
|
'@rollup/rollup-linux-x64-gnu@4.55.1':
|
||||||
resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==}
|
resolution: {integrity: sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.55.1':
|
'@rollup/rollup-linux-x64-musl@4.55.1':
|
||||||
resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==}
|
resolution: {integrity: sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@rollup/rollup-openbsd-x64@4.55.1':
|
'@rollup/rollup-openbsd-x64@4.55.1':
|
||||||
resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==}
|
resolution: {integrity: sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==}
|
||||||
@@ -1295,10 +1351,25 @@ packages:
|
|||||||
'@sqltools/formatter@1.2.5':
|
'@sqltools/formatter@1.2.5':
|
||||||
resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==}
|
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':
|
'@szmarczak/http-timer@4.0.6':
|
||||||
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
|
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
|
||||||
engines: {node: '>=10'}
|
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':
|
'@types/babel__core@7.20.5':
|
||||||
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
|
||||||
|
|
||||||
@@ -1370,6 +1441,9 @@ packages:
|
|||||||
'@types/triple-beam@1.3.5':
|
'@types/triple-beam@1.3.5':
|
||||||
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
|
resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==}
|
||||||
|
|
||||||
|
'@types/use-sync-external-store@0.0.6':
|
||||||
|
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
||||||
|
|
||||||
'@types/uuid@11.0.0':
|
'@types/uuid@11.0.0':
|
||||||
resolution: {integrity: sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==}
|
resolution: {integrity: sha512-HVyk8nj2m+jcFRNazzqyVKiZezyhDKrGUA3jlEcg/nZ6Ms+qHwocba1Y/AaVaznJTAM9xpdFSh+ptbNrhOGvZA==}
|
||||||
deprecated: This is a stub types definition. uuid provides its own type definitions, so you do not need this installed.
|
deprecated: This is a stub types definition. uuid provides its own type definitions, so you do not need this installed.
|
||||||
@@ -1782,6 +1856,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
|
|
||||||
|
clone@2.1.2:
|
||||||
|
resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
|
||||||
|
engines: {node: '>=0.8'}
|
||||||
|
|
||||||
clsx@2.1.1:
|
clsx@2.1.1:
|
||||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2202,6 +2280,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
eventemitter2@6.4.9:
|
||||||
|
resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==}
|
||||||
|
|
||||||
expand-template@2.0.3:
|
expand-template@2.0.3:
|
||||||
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2450,6 +2531,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
hash-it@6.0.1:
|
||||||
|
resolution: {integrity: sha512-qhl8+l4Zwi1eLlL3lja5ywmDQnBzLEJxd0QJoAVIgZpgQbdtVZrN5ypB0y3VHwBlvAalpcbM2/A6x7oUks5zNg==}
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -2507,6 +2591,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
|
||||||
engines: {node: '>= 4'}
|
engines: {node: '>= 4'}
|
||||||
|
|
||||||
|
immer@11.1.4:
|
||||||
|
resolution: {integrity: sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==}
|
||||||
|
|
||||||
import-fresh@3.3.1:
|
import-fresh@3.3.1:
|
||||||
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2717,6 +2804,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jsep@1.4.0:
|
||||||
|
resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==}
|
||||||
|
engines: {node: '>= 10.16.0'}
|
||||||
|
|
||||||
jsesc@3.1.0:
|
jsesc@3.1.0:
|
||||||
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2725,6 +2816,10 @@ packages:
|
|||||||
json-buffer@3.0.1:
|
json-buffer@3.0.1:
|
||||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
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:
|
json-schema-traverse@0.4.1:
|
||||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||||
|
|
||||||
@@ -2748,6 +2843,11 @@ packages:
|
|||||||
jsonfile@6.2.0:
|
jsonfile@6.2.0:
|
||||||
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
|
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:
|
jsx-ast-utils@3.3.5:
|
||||||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||||
engines: {node: '>=4.0'}
|
engines: {node: '>=4.0'}
|
||||||
@@ -3010,6 +3110,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
|
resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
numeric-quantity@2.1.0:
|
||||||
|
resolution: {integrity: sha512-oDkQ8nFuNVA+unEg1jd6dAS+O7eLXWWzsa4ViI0S0yFi6654GK0s74o8bF8uLRQdWIz/qFF1GABNFPfwAGQUsg==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
object-assign@4.1.1:
|
object-assign@4.1.1:
|
||||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -3319,6 +3423,23 @@ packages:
|
|||||||
react-is@18.3.1:
|
react-is@18.3.1:
|
||||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
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:
|
react-refresh@0.18.0:
|
||||||
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
|
resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -3352,6 +3473,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
|
resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==}
|
||||||
engines: {node: '>= 20.19.0'}
|
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:
|
reflect-metadata@0.2.2:
|
||||||
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||||
|
|
||||||
@@ -3371,6 +3500,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==}
|
resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==}
|
||||||
engines: {node: '>=12', npm: '>=6'}
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
|
|
||||||
|
reselect@5.1.1:
|
||||||
|
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
|
||||||
|
|
||||||
resize-observer-polyfill@1.5.1:
|
resize-observer-polyfill@1.5.1:
|
||||||
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||||
|
|
||||||
@@ -4779,6 +4911,14 @@ snapshots:
|
|||||||
'@jridgewell/resolve-uri': 3.1.2
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@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':
|
'@malept/cross-spawn-promise@2.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
@@ -5158,6 +5298,13 @@ snapshots:
|
|||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
react-dom: 19.2.3(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)':
|
'@react-spring/animated@9.6.1(react@19.2.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@react-spring/shared': 9.6.1(react@19.2.3)
|
'@react-spring/shared': 9.6.1(react@19.2.3)
|
||||||
@@ -5191,6 +5338,18 @@ snapshots:
|
|||||||
react: 19.2.3
|
react: 19.2.3
|
||||||
react-dom: 19.2.3(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': {}
|
'@remix-run/router@1.23.2': {}
|
||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.53': {}
|
'@rolldown/pluginutils@1.0.0-beta.53': {}
|
||||||
@@ -5279,10 +5438,25 @@ snapshots:
|
|||||||
|
|
||||||
'@sqltools/formatter@1.2.5': {}
|
'@sqltools/formatter@1.2.5': {}
|
||||||
|
|
||||||
|
'@standard-schema/spec@1.1.0': {}
|
||||||
|
|
||||||
|
'@standard-schema/utils@0.3.0': {}
|
||||||
|
|
||||||
'@szmarczak/http-timer@4.0.6':
|
'@szmarczak/http-timer@4.0.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
defer-to-connect: 2.0.1
|
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':
|
'@types/babel__core@7.20.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/parser': 7.28.5
|
'@babel/parser': 7.28.5
|
||||||
@@ -5373,6 +5547,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/triple-beam@1.3.5': {}
|
'@types/triple-beam@1.3.5': {}
|
||||||
|
|
||||||
|
'@types/use-sync-external-store@0.0.6': {}
|
||||||
|
|
||||||
'@types/uuid@11.0.0':
|
'@types/uuid@11.0.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
uuid: 13.0.0
|
uuid: 13.0.0
|
||||||
@@ -5974,6 +6150,8 @@ snapshots:
|
|||||||
|
|
||||||
clone@1.0.4: {}
|
clone@1.0.4: {}
|
||||||
|
|
||||||
|
clone@2.1.2: {}
|
||||||
|
|
||||||
clsx@2.1.1: {}
|
clsx@2.1.1: {}
|
||||||
|
|
||||||
codepage@1.15.0: {}
|
codepage@1.15.0: {}
|
||||||
@@ -6551,6 +6729,8 @@ snapshots:
|
|||||||
|
|
||||||
etag@1.8.1: {}
|
etag@1.8.1: {}
|
||||||
|
|
||||||
|
eventemitter2@6.4.9: {}
|
||||||
|
|
||||||
expand-template@2.0.3: {}
|
expand-template@2.0.3: {}
|
||||||
|
|
||||||
exponential-backoff@3.1.3: {}
|
exponential-backoff@3.1.3: {}
|
||||||
@@ -6859,6 +7039,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
has-symbols: 1.1.0
|
has-symbols: 1.1.0
|
||||||
|
|
||||||
|
hash-it@6.0.1: {}
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
@@ -6922,6 +7104,8 @@ snapshots:
|
|||||||
|
|
||||||
ignore@7.0.5: {}
|
ignore@7.0.5: {}
|
||||||
|
|
||||||
|
immer@11.1.4: {}
|
||||||
|
|
||||||
import-fresh@3.3.1:
|
import-fresh@3.3.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
parent-module: 1.0.1
|
parent-module: 1.0.1
|
||||||
@@ -7117,10 +7301,19 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
argparse: 2.0.1
|
argparse: 2.0.1
|
||||||
|
|
||||||
|
jsep@1.4.0: {}
|
||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
|
|
||||||
json-buffer@3.0.1: {}
|
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-schema-traverse@0.4.1: {}
|
||||||
|
|
||||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||||
@@ -7144,6 +7337,12 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
graceful-fs: 4.2.11
|
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:
|
jsx-ast-utils@3.3.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
@@ -7402,6 +7601,8 @@ snapshots:
|
|||||||
|
|
||||||
normalize-url@6.1.0: {}
|
normalize-url@6.1.0: {}
|
||||||
|
|
||||||
|
numeric-quantity@2.1.0: {}
|
||||||
|
|
||||||
object-assign@4.1.1: {}
|
object-assign@4.1.1: {}
|
||||||
|
|
||||||
object-hash@3.0.0: {}
|
object-hash@3.0.0: {}
|
||||||
@@ -7725,6 +7926,29 @@ snapshots:
|
|||||||
|
|
||||||
react-is@18.3.1: {}
|
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-refresh@0.18.0: {}
|
||||||
|
|
||||||
react-router-dom@6.30.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
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: {}
|
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-metadata@0.2.2: {}
|
||||||
|
|
||||||
reflect.getprototypeof@1.0.10:
|
reflect.getprototypeof@1.0.10:
|
||||||
@@ -7783,6 +8013,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
pe-library: 0.4.1
|
pe-library: 0.4.1
|
||||||
|
|
||||||
|
reselect@5.1.1: {}
|
||||||
|
|
||||||
resize-observer-polyfill@1.5.1: {}
|
resize-observer-polyfill@1.5.1: {}
|
||||||
|
|
||||||
resolve-alpn@1.2.1: {}
|
resolve-alpn@1.2.1: {}
|
||||||
|
|||||||
@@ -224,4 +224,27 @@ export class EventRepository extends Service {
|
|||||||
const rows = await qb.getRawMany()
|
const rows = await qb.getRawMany()
|
||||||
return { startTime, rows }
|
return { startTime, rows }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getLastScoreTimeByStudents(
|
||||||
|
studentNames: string[]
|
||||||
|
): Promise<Map<string, Date>> {
|
||||||
|
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<string, Date>()
|
||||||
|
for (const row of results) {
|
||||||
|
if (row.last_time) {
|
||||||
|
map.set(row.student_name, new Date(row.last_time))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
import { Service } from '../../shared/kernel'
|
import { Service } from '../../shared/kernel'
|
||||||
import { MainContext } from '../context'
|
import { MainContext } from '../context'
|
||||||
import { student } from '../repos/StudentRepository'
|
import { AutoScoreRuleEngine, type RuleConfig, type AutoScoreContext } from '../../shared/autoScore/AutoScoreRuleEngine'
|
||||||
import { getTriggerLogic } from '../../shared/triggers'
|
|
||||||
|
|
||||||
interface AutoScoreRule {
|
interface AutoScoreRule extends RuleConfig {
|
||||||
id: number
|
id: number
|
||||||
enabled: boolean
|
|
||||||
name: string
|
|
||||||
studentNames: string[]
|
|
||||||
lastExecuted?: Date
|
lastExecuted?: Date
|
||||||
triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[]
|
|
||||||
actions?: { event: string; value?: string; reason?: string }[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AutoScoreRuleFileData {
|
interface AutoScoreRuleFileData {
|
||||||
@@ -19,8 +13,8 @@ interface AutoScoreRuleFileData {
|
|||||||
name: string
|
name: string
|
||||||
studentNames: string[]
|
studentNames: string[]
|
||||||
lastExecuted?: string
|
lastExecuted?: string
|
||||||
triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[]
|
triggers?: Array<{ event: string; value?: string; relation?: 'AND' | 'OR' }>
|
||||||
actions?: { event: string; value?: string; reason?: string }[]
|
actions?: Array<{ event: string; value?: string; reason?: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AutoScoreRulesFile {
|
interface AutoScoreRulesFile {
|
||||||
@@ -41,9 +35,11 @@ export class AutoScoreService extends Service {
|
|||||||
private rules: AutoScoreRule[] = []
|
private rules: AutoScoreRule[] = []
|
||||||
private timers: Map<number, NodeJS.Timeout> = new Map()
|
private timers: Map<number, NodeJS.Timeout> = new Map()
|
||||||
private initialized = false
|
private initialized = false
|
||||||
|
private ruleEngine: AutoScoreRuleEngine
|
||||||
|
|
||||||
constructor(ctx: MainContext) {
|
constructor(ctx: MainContext) {
|
||||||
super(ctx, 'autoScore')
|
super(ctx, 'autoScore')
|
||||||
|
this.ruleEngine = new AutoScoreRuleEngine()
|
||||||
this.registerIpc()
|
this.registerIpc()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +112,17 @@ export class AutoScoreService extends Service {
|
|||||||
return { success: false, message: err.message }
|
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<void> {
|
private async loadRulesFromFile(): Promise<void> {
|
||||||
@@ -260,8 +267,11 @@ export class AutoScoreService extends Service {
|
|||||||
this.stopRules()
|
this.stopRules()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.ruleEngine = new AutoScoreRuleEngine()
|
||||||
|
|
||||||
for (const rule of this.rules) {
|
for (const rule of this.rules) {
|
||||||
if (rule.enabled) {
|
if (rule.enabled) {
|
||||||
|
await this.ruleEngine.addRule(rule)
|
||||||
this.startRuleTimer(rule)
|
this.startRuleTimer(rule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,11 +290,20 @@ export class AutoScoreService extends Service {
|
|||||||
let primaryTrigger: { event: string; value?: string } | undefined
|
let primaryTrigger: { event: string; value?: string } | undefined
|
||||||
|
|
||||||
for (const trigger of rule.triggers || []) {
|
for (const trigger of rule.triggers || []) {
|
||||||
const logic = getTriggerLogic(trigger.event)
|
if (trigger.event === 'interval_time_passed') {
|
||||||
if (logic?.calculateNextTime) {
|
const minutes = parseInt(trigger.value || '30', 10)
|
||||||
const result = logic.calculateNextTime(trigger.value || '', rule.lastExecuted, now)
|
const intervalMs = minutes * 60 * 1000
|
||||||
if (delayMs === 0 || result.delayMs < delayMs) {
|
|
||||||
delayMs = result.delayMs
|
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
|
primaryTrigger = trigger
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,11 +333,20 @@ export class AutoScoreService extends Service {
|
|||||||
let primaryTrigger: { event: string; value?: string } | undefined
|
let primaryTrigger: { event: string; value?: string } | undefined
|
||||||
|
|
||||||
for (const trigger of rule.triggers || []) {
|
for (const trigger of rule.triggers || []) {
|
||||||
const logic = getTriggerLogic(trigger.event)
|
if (trigger.event === 'interval_time_passed') {
|
||||||
if (logic?.calculateNextTime) {
|
const minutes = parseInt(trigger.value || '30', 10)
|
||||||
const result = logic.calculateNextTime(trigger.value || '', rule.lastExecuted, now)
|
const intervalMs = minutes * 60 * 1000
|
||||||
if (result.delayMs < minDelayMs) {
|
|
||||||
minDelayMs = result.delayMs
|
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
|
primaryTrigger = trigger
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -340,60 +368,25 @@ export class AutoScoreService extends Service {
|
|||||||
this.logger.info(`Executing auto score rule: ${rule.name}`)
|
this.logger.info(`Executing auto score rule: ${rule.name}`)
|
||||||
|
|
||||||
const studentRepo = this.mainCtx.students
|
const studentRepo = this.mainCtx.students
|
||||||
|
const eventRepo = this.mainCtx.events
|
||||||
|
|
||||||
let studentsToScore: student[] = []
|
const allStudents = await studentRepo.findAll()
|
||||||
if (rule.studentNames.length === 0) {
|
|
||||||
const allStudents = await studentRepo.findAll()
|
let studentsToScore = allStudents
|
||||||
studentsToScore = allStudents
|
if (rule.studentNames.length > 0) {
|
||||||
} else {
|
studentsToScore = allStudents.filter((s) => rule.studentNames.includes(s.name))
|
||||||
const allStudents = await studentRepo.findAll()
|
|
||||||
for (const name of rule.studentNames) {
|
|
||||||
const student = allStudents.find((s) => s.name === name)
|
|
||||||
if (student) {
|
|
||||||
studentsToScore.push(student)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用AND/OR逻辑评估触发器
|
const studentNames = studentsToScore.map((s) => s.name)
|
||||||
const matchedStudents = this.evaluateTriggersWithLogic(rule, studentsToScore)
|
const lastScoreTimeMap = await eventRepo.getLastScoreTimeByStudents(studentNames)
|
||||||
|
|
||||||
if (matchedStudents.length > 0) {
|
const context: AutoScoreContext = {
|
||||||
for (const action of rule.actions || []) {
|
students: studentsToScore.map((s) => ({
|
||||||
await this.executeAction(action, matchedStudents, rule.name)
|
id: s.id,
|
||||||
}
|
name: s.name,
|
||||||
}
|
tags: s.tags || [],
|
||||||
|
lastScoreTime: lastScoreTimeMap.get(s.name) || new Date(0),
|
||||||
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,
|
|
||||||
events: [],
|
events: [],
|
||||||
rule: {
|
rule: {
|
||||||
id: rule.id,
|
id: rule.id,
|
||||||
@@ -405,138 +398,39 @@ export class AutoScoreService extends Service {
|
|||||||
now: new Date()
|
now: new Date()
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = logic.check(context, trigger.value || '')
|
const results = await this.ruleEngine.run(context)
|
||||||
|
|
||||||
if (!result.matchedStudents || result.matchedStudents.length === 0) {
|
for (const result of results) {
|
||||||
// 当前触发器没有匹配的学生
|
if (result.matchedStudents.length > 0) {
|
||||||
if (currentRelation === 'AND') {
|
for (const action of result.actions) {
|
||||||
// AND关系下,任何一个触发器不匹配,整个组就不匹配
|
await this.ruleEngine.executeAction(
|
||||||
currentGroup = []
|
action.event,
|
||||||
}
|
result.matchedStudents,
|
||||||
// OR关系下,继续评估下一个触发器
|
{
|
||||||
} else {
|
...action,
|
||||||
// 当前触发器有匹配的学生
|
ruleId: rule.id,
|
||||||
if (currentRelation === 'AND') {
|
ruleName: rule.name,
|
||||||
// AND关系下,取交集
|
studentRepo
|
||||||
currentGroup = currentGroup.filter((student) =>
|
},
|
||||||
result.matchedStudents!.some((matched) => matched.id === student.id)
|
eventRepo
|
||||||
)
|
|
||||||
} 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)
|
|
||||||
)
|
)
|
||||||
resultStudents = [...resultStudents, ...newStudents]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置当前组为所有学生,开始新的关系组
|
|
||||||
currentGroup = [...initialStudents]
|
|
||||||
currentRelation = nextRelation
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// 处理最后一组的结果
|
rule.lastExecuted = new Date()
|
||||||
if (currentRelation === 'AND') {
|
await this.saveRulesToFile()
|
||||||
// AND组结束,如果当前组不为空,则合并到结果
|
|
||||||
if (currentGroup.length > 0) {
|
this.logger.info(
|
||||||
resultStudents = resultStudents.filter((student) =>
|
`Auto score rule executed successfully for ${results.reduce((sum, r) => sum + r.matchedStudents.length, 0)} students`
|
||||||
currentGroup.some((groupStudent) => groupStudent.id === student.id)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// AND组为空,整个规则不匹配
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// OR组结束,合并当前组到结果
|
|
||||||
const newStudents = currentGroup.filter(
|
|
||||||
(groupStudent) =>
|
|
||||||
!resultStudents.some((resultStudent) => resultStudent.id === groupStudent.id)
|
|
||||||
)
|
)
|
||||||
resultStudents = [...resultStudents, ...newStudents]
|
} catch (error) {
|
||||||
}
|
this.logger.error(`Failed to execute auto score rule ${rule.name}:`, { error })
|
||||||
|
|
||||||
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}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private stopRules() {
|
private stopRules() {
|
||||||
for (const [timer] of this.timers) {
|
for (const timer of this.timers.values()) {
|
||||||
clearTimeout(timer)
|
clearTimeout(timer)
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
}
|
}
|
||||||
@@ -555,6 +449,7 @@ export class AutoScoreService extends Service {
|
|||||||
await this.saveRulesToFile()
|
await this.saveRulesToFile()
|
||||||
|
|
||||||
if (rule.enabled) {
|
if (rule.enabled) {
|
||||||
|
await this.ruleEngine.addRule(newRule)
|
||||||
this.startRuleTimer(newRule)
|
this.startRuleTimer(newRule)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -577,6 +472,7 @@ export class AutoScoreService extends Service {
|
|||||||
await this.saveRulesToFile()
|
await this.saveRulesToFile()
|
||||||
|
|
||||||
if (updatedRule.enabled) {
|
if (updatedRule.enabled) {
|
||||||
|
await this.ruleEngine.addRule(updatedRule)
|
||||||
this.startRuleTimer(updatedRule)
|
this.startRuleTimer(updatedRule)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,6 +508,7 @@ export class AutoScoreService extends Service {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
await this.ruleEngine.addRule(rule)
|
||||||
this.startRuleTimer(rule)
|
this.startRuleTimer(rule)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react'
|
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 {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Form,
|
Form,
|
||||||
@@ -20,6 +15,9 @@ import {
|
|||||||
} from 'antd'
|
} from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
|
|
||||||
|
import { RuleComponent } from './autoScore/ruleComponent'
|
||||||
|
import type { AutoScoreRuleData } from './autoScore/ruleBuilderUtils'
|
||||||
|
|
||||||
interface AutoScoreRule {
|
interface AutoScoreRule {
|
||||||
id: number
|
id: number
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
@@ -43,8 +41,10 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
const [pageSize, setPageSize] = useState<number>(50)
|
const [pageSize, setPageSize] = useState<number>(50)
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
const [editingRuleId, setEditingRuleId] = useState<number | null>(null)
|
const [editingRuleId, setEditingRuleId] = useState<number | null>(null)
|
||||||
const [triggerList, setTriggerList] = useState<TriggerItem[]>([])
|
const [ruleData, setRuleData] = useState<AutoScoreRuleData>({
|
||||||
const [actionList, setActionList] = useState<ActionItem[]>([])
|
triggers: [],
|
||||||
|
actions: []
|
||||||
|
})
|
||||||
const [messageApi, contextHolder] = message.useMessage()
|
const [messageApi, contextHolder] = message.useMessage()
|
||||||
|
|
||||||
const fetchRules = async () => {
|
const fetchRules = async () => {
|
||||||
@@ -97,35 +97,24 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (triggerList.length === 0) {
|
if (ruleData.triggers.length === 0) {
|
||||||
messageApi.warning('请至少添加一个触发器')
|
messageApi.warning('请至少添加一个触发器')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionList.length === 0) {
|
if (ruleData.actions.length === 0) {
|
||||||
messageApi.warning('请至少添加一个行动')
|
messageApi.warning('请至少添加一个行动')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []
|
const studentNames = Array.isArray(values.studentNames) ? values.studentNames : []
|
||||||
|
|
||||||
const triggersPayload = triggerList.map((t) => ({
|
const ruleDataToSubmit = {
|
||||||
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 = {
|
|
||||||
enabled: true,
|
enabled: true,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
studentNames,
|
studentNames,
|
||||||
triggers: triggersPayload,
|
triggers: ruleData.triggers,
|
||||||
actions: actionsPayload
|
actions: ruleData.actions
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -139,14 +128,14 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let res
|
let res: { success: boolean; message?: string; data?: any }
|
||||||
if (editingRuleId !== null) {
|
if (editingRuleId !== null) {
|
||||||
res = await (window as any).api.invoke('auto-score:updateRule', {
|
res = await (window as any).api.invoke('auto-score:updateRule', {
|
||||||
id: editingRuleId,
|
id: editingRuleId,
|
||||||
...ruleData
|
...ruleDataToSubmit
|
||||||
})
|
})
|
||||||
} else {
|
} 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) {
|
if (res.success) {
|
||||||
@@ -156,8 +145,7 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
studentNames: ''
|
studentNames: ''
|
||||||
})
|
})
|
||||||
setEditingRuleId(null)
|
setEditingRuleId(null)
|
||||||
setTriggerList([])
|
setRuleData({ triggers: [], actions: [] })
|
||||||
setActionList([])
|
|
||||||
fetchRules()
|
fetchRules()
|
||||||
} else {
|
} else {
|
||||||
messageApi.error(
|
messageApi.error(
|
||||||
@@ -174,30 +162,12 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
setEditingRuleId(rule.id)
|
setEditingRuleId(rule.id)
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
name: rule.name,
|
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) => {
|
const handleDelete = async (ruleId: number) => {
|
||||||
@@ -258,85 +228,10 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
studentNames: ''
|
studentNames: ''
|
||||||
})
|
})
|
||||||
setEditingRuleId(null)
|
setEditingRuleId(null)
|
||||||
setTriggerList([])
|
setRuleData({ triggers: [], actions: [] })
|
||||||
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)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: ColumnsType<AutoScoreRule> = [
|
const columns: ColumnsType<AutoScoreRule> = [
|
||||||
{
|
|
||||||
key: 'drag',
|
|
||||||
title: '排序',
|
|
||||||
width: 60,
|
|
||||||
render: () => <HolderOutlined style={{ cursor: 'move' }} />
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'enabled',
|
dataIndex: 'enabled',
|
||||||
@@ -356,15 +251,7 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
if (!triggers || triggers.length === 0) {
|
if (!triggers || triggers.length === 0) {
|
||||||
return <span>无</span>
|
return <span>无</span>
|
||||||
}
|
}
|
||||||
const triggerLabels = triggers.map((t) => {
|
return <span>{triggers.length} 个触发器</span>
|
||||||
const def = triggerRegistry.get(t.event)
|
|
||||||
return def?.label || t.event
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<Tooltip title={triggerLabels.join(', ')}>
|
|
||||||
<span>{triggers.length} 个触发器</span>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -376,15 +263,7 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
if (!actions || actions.length === 0) {
|
if (!actions || actions.length === 0) {
|
||||||
return <span>无</span>
|
return <span>无</span>
|
||||||
}
|
}
|
||||||
const actionLabels = actions.map((a) => {
|
return <span>{actions.length} 个行动</span>
|
||||||
const def = actionRegistry.get(a.event)
|
|
||||||
return def?.label || a.event
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<Tooltip title={actionLabels.join(', ')}>
|
|
||||||
<span>{actions.length} 个行动</span>
|
|
||||||
</Tooltip>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -438,33 +317,6 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const triggerItems = triggerList
|
|
||||||
.filter((t) => t.eventName !== null)
|
|
||||||
.map((item, idx) => (
|
|
||||||
<TriggerItemComponent
|
|
||||||
key={item.id}
|
|
||||||
item={item}
|
|
||||||
onDelete={handleDeleteTrigger}
|
|
||||||
onChange={handleTriggerChange}
|
|
||||||
onValueChange={handleTriggerValueChange}
|
|
||||||
onRelationChange={handleTriggerRelationChange}
|
|
||||||
isFirst={idx === 0}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
const actionItems = actionList
|
|
||||||
.filter((a) => a.eventName !== null)
|
|
||||||
.map((item) => (
|
|
||||||
<ActionItemComponent
|
|
||||||
key={item.id}
|
|
||||||
item={item}
|
|
||||||
onDelete={handleDeleteAction}
|
|
||||||
onChange={handleActionChange}
|
|
||||||
onValueChange={handleActionValueChange}
|
|
||||||
onReasonChange={handleActionReasonChange}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: '24px' }}>
|
<div style={{ padding: '24px' }}>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
@@ -491,6 +343,10 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginTop: '16px' }}>
|
||||||
|
<RuleComponent initialData={ruleData} onChange={(data) => setRuleData(data)} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
|
<div style={{ marginTop: '24px', display: 'flex', gap: '12px' }}>
|
||||||
<Button type="primary" onClick={handleSubmit}>
|
<Button type="primary" onClick={handleSubmit}>
|
||||||
{editingRuleId !== null ? '更新自动化' : '添加自动化'}
|
{editingRuleId !== null ? '更新自动化' : '添加自动化'}
|
||||||
@@ -502,40 +358,6 @@ export const AutoScoreManager: React.FC = () => {
|
|||||||
</Form>
|
</Form>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card
|
|
||||||
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
|
||||||
title="当以下规则触发时"
|
|
||||||
>
|
|
||||||
<Space orientation="vertical" style={{ width: '100%' }}>
|
|
||||||
{triggerItems}
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
onClick={handleAddTrigger}
|
|
||||||
style={{ fontWeight: 'bolder', fontSize: 15 }}
|
|
||||||
>
|
|
||||||
添加规则
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}
|
|
||||||
title="满足规则时触发的行动"
|
|
||||||
>
|
|
||||||
<Space orientation="vertical" style={{ width: '100%' }}>
|
|
||||||
{actionItems}
|
|
||||||
<Button
|
|
||||||
type="dashed"
|
|
||||||
icon={<PlusOutlined />}
|
|
||||||
onClick={handleAddAction}
|
|
||||||
style={{ fontWeight: 'bolder', fontSize: 15 }}
|
|
||||||
>
|
|
||||||
添加行动
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
<Card style={{ marginBottom: '24px', backgroundColor: 'var(--ss-card-bg)' }}>
|
||||||
<Table
|
<Table
|
||||||
dataSource={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
|
dataSource={rules.slice((currentPage - 1) * pageSize, currentPage * pageSize)}
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export function Sidebar({ activeMenu, permission, onMenuChange }: SidebarProps):
|
|||||||
bodyStyle={{ padding: '12px' }}
|
bodyStyle={{ padding: '12px' }}
|
||||||
>
|
>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
<Space orientation="vertical" size={4} style={{ width: '100%' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
<CloudOutlined style={{ fontSize: '12px', color: '#1890ff' }} />
|
<CloudOutlined style={{ fontSize: '12px', color: '#1890ff' }} />
|
||||||
|
|||||||
@@ -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<ActionComponentProps> = ({
|
||||||
|
actions,
|
||||||
|
onAdd,
|
||||||
|
onRemove,
|
||||||
|
onChange
|
||||||
|
}) => {
|
||||||
|
const handleActionChange = (index: number, field: keyof AutoScoreAction, value: any) => {
|
||||||
|
onChange(index, field, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Space direction="vertical" style={{ width: '100%' }}>
|
||||||
|
{actions.map((action, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
padding: '12px',
|
||||||
|
border: '1px solid #d9d9d9',
|
||||||
|
borderRadius: '6px',
|
||||||
|
backgroundColor: '#fafafa'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Space style={{ width: '100%' }} wrap>
|
||||||
|
<Select
|
||||||
|
value={action.event}
|
||||||
|
onChange={(value) => handleActionChange(index, 'event', value)}
|
||||||
|
style={{ width: 150 }}
|
||||||
|
options={Object.entries({
|
||||||
|
add_score: { label: '加分', description: '为学生增加分数' },
|
||||||
|
add_tag: { label: '添加标签', description: '为学生添加标签' }
|
||||||
|
}).map(([key, val]) => ({
|
||||||
|
label: val.label,
|
||||||
|
value: key
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{action.event === 'add_score' && (
|
||||||
|
<InputNumber
|
||||||
|
value={action.value ? parseInt(action.value) : 0}
|
||||||
|
onChange={(value) => handleActionChange(index, 'value', String(value || 0))}
|
||||||
|
placeholder="分数"
|
||||||
|
min={-100}
|
||||||
|
max={100}
|
||||||
|
style={{ width: 120 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{action.event === 'add_tag' && (
|
||||||
|
<Input
|
||||||
|
value={action.value}
|
||||||
|
onChange={(e) => handleActionChange(index, 'value', e.target.value)}
|
||||||
|
placeholder="标签名称"
|
||||||
|
style={{ width: 200 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Input
|
||||||
|
value={action.reason}
|
||||||
|
onChange={(e) => handleActionChange(index, 'reason', e.target.value)}
|
||||||
|
placeholder="操作说明(可选)"
|
||||||
|
style={{ flex: 1, minWidth: 200 }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onClick={() => onRemove(index)}
|
||||||
|
/>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<Button type="dashed" icon={<PlusOutlined />} onClick={onAdd} block>
|
||||||
|
添加操作
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ActionComponent
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<RuleComponentProps> = ({ initialData, onChange }) => {
|
||||||
|
const [query, setQuery] = useState<RuleGroupType>(
|
||||||
|
initialData ? autoScoreRuleToQuery(initialData) : defaultQuery
|
||||||
|
)
|
||||||
|
const [actions, setActions] = useState<AutoScoreRuleData['actions']>(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 (
|
||||||
|
<div>
|
||||||
|
<Card title="触发条件" style={{ marginBottom: '16px' }}>
|
||||||
|
<QueryBuilder
|
||||||
|
fields={fields}
|
||||||
|
operators={operators}
|
||||||
|
query={query}
|
||||||
|
onQueryChange={handleQueryChange}
|
||||||
|
/>
|
||||||
|
<div style={{ marginTop: '8px' }}>
|
||||||
|
<pre style={{ fontSize: '12px', color: '#999' }}>{formatQuery(query, 'json')}</pre>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card title="执行操作">
|
||||||
|
<ActionComponent
|
||||||
|
actions={actions}
|
||||||
|
onAdd={handleAddAction}
|
||||||
|
onRemove={handleRemoveAction}
|
||||||
|
onChange={handleActionChange}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RuleComponent
|
||||||
@@ -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<ActionItemProps> = ({
|
|
||||||
item,
|
|
||||||
onDelete,
|
|
||||||
onChange,
|
|
||||||
onValueChange,
|
|
||||||
onReasonChange
|
|
||||||
}) => {
|
|
||||||
const definition = actionRegistry.get(item.eventName)
|
|
||||||
const Component = definition?.component
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', gap: 5, alignItems: 'center' }}>
|
|
||||||
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => onDelete(item.id)} />
|
|
||||||
<Select
|
|
||||||
value={item.eventName}
|
|
||||||
style={{ width: '200px' }}
|
|
||||||
options={allActions.options}
|
|
||||||
placeholder="请选择触发行动"
|
|
||||||
onChange={(value) => onChange(item.id, value as string)}
|
|
||||||
/>
|
|
||||||
{Component && (
|
|
||||||
<Component
|
|
||||||
value={item.value}
|
|
||||||
reason={item.reason}
|
|
||||||
onChange={(value) => onValueChange(item.id, value)}
|
|
||||||
onReasonChange={(reason) => onReasonChange(item.id, reason)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ActionItem
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { Button, Select } from 'antd'
|
|
||||||
import { DeleteOutlined } from '@ant-design/icons'
|
|
||||||
import { triggerRegistry, allTriggers } from './registry'
|
|
||||||
import type { TriggerItem as TriggerItemType } from './types'
|
|
||||||
|
|
||||||
interface TriggerItemProps {
|
|
||||||
item: TriggerItemType
|
|
||||||
onDelete: (id: number) => void
|
|
||||||
onChange: (id: number, eventName: string) => void
|
|
||||||
onValueChange: (id: number, value: string) => void
|
|
||||||
onRelationChange?: (id: number, relation: 'AND' | 'OR') => void
|
|
||||||
isFirst?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const TriggerItem: React.FC<TriggerItemProps> = ({
|
|
||||||
item,
|
|
||||||
onDelete,
|
|
||||||
onChange,
|
|
||||||
onValueChange,
|
|
||||||
onRelationChange,
|
|
||||||
isFirst = false
|
|
||||||
}) => {
|
|
||||||
const definition = triggerRegistry.get(item.eventName)
|
|
||||||
const Component = definition?.component
|
|
||||||
const relation = item.relation || 'AND'
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ display: 'flex', gap: 5, alignItems: 'center' }}>
|
|
||||||
{!isFirst && (
|
|
||||||
<Button
|
|
||||||
type={relation === 'AND' ? 'primary' : 'default'}
|
|
||||||
danger={relation === 'OR'}
|
|
||||||
onClick={() => onRelationChange?.(item.id, relation === 'AND' ? 'OR' : 'AND')}
|
|
||||||
>
|
|
||||||
{relation === 'AND' ? '并' : '或'}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => onDelete(item.id)} />
|
|
||||||
<Select
|
|
||||||
value={item.eventName}
|
|
||||||
style={{ width: '200px' }}
|
|
||||||
options={allTriggers.options}
|
|
||||||
placeholder="请选择触发规则"
|
|
||||||
onChange={(value) => onChange(item.id, value as string)}
|
|
||||||
/>
|
|
||||||
{Component && (
|
|
||||||
<Component value={item.value} onChange={(value) => onValueChange(item.id, value)} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TriggerItem
|
|
||||||
@@ -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<ActionComponentProps> = ({
|
|
||||||
value,
|
|
||||||
reason,
|
|
||||||
onChange,
|
|
||||||
onReasonChange
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Input
|
|
||||||
placeholder="请输入分数"
|
|
||||||
style={{ width: '150px' }}
|
|
||||||
value={value ?? ''}
|
|
||||||
onChange={(e) => onChange(e.target.value ? String(e.target.value) : '')}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder="请输入理由"
|
|
||||||
style={{ width: '150px' }}
|
|
||||||
value={reason ?? ''}
|
|
||||||
onChange={(e) => onReasonChange?.(e.target.value ? String(e.target.value) : '')}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AddScoreAction
|
|
||||||
@@ -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<ActionComponentProps> = ({ value, onChange }) => {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
placeholder="请输入标签"
|
|
||||||
style={{ width: '150px' }}
|
|
||||||
value={value ?? ''}
|
|
||||||
onChange={(e) => onChange(e.target.value ? String(e.target.value) : '')}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AddTagAction
|
|
||||||
@@ -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<ActionComponentProps> = ({ value, onChange }) => {
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
placeholder="请输入通知内容"
|
|
||||||
style={{ width: '150px' }}
|
|
||||||
value={value ?? ''}
|
|
||||||
onChange={(e) => onChange(e.target.value ? String(e.target.value) : '')}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SendNotificationAction
|
|
||||||
@@ -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 }
|
|
||||||
@@ -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'
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import type { TriggerDefinition, ActionDefinition, TriggerLogic } from './types'
|
|
||||||
|
|
||||||
class TriggerRegistry {
|
|
||||||
private triggers: Map<string, TriggerDefinition> = new Map()
|
|
||||||
|
|
||||||
register(definition: TriggerDefinition) {
|
|
||||||
this.triggers.set(definition.eventName, definition)
|
|
||||||
}
|
|
||||||
|
|
||||||
get(eventName: string): TriggerDefinition | undefined {
|
|
||||||
return this.triggers.get(eventName)
|
|
||||||
}
|
|
||||||
|
|
||||||
getAll(): TriggerDefinition[] {
|
|
||||||
return Array.from(this.triggers.values())
|
|
||||||
}
|
|
||||||
|
|
||||||
getOptions() {
|
|
||||||
return this.getAll().map((t) => ({ label: t.label, value: t.eventName }))
|
|
||||||
}
|
|
||||||
|
|
||||||
getLogic(eventName: string): TriggerLogic | undefined {
|
|
||||||
return this.triggers.get(eventName)?.triggerLogic
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ActionRegistry {
|
|
||||||
private actions: Map<string, ActionDefinition> = new Map()
|
|
||||||
|
|
||||||
register(definition: ActionDefinition) {
|
|
||||||
this.actions.set(definition.eventName, definition)
|
|
||||||
}
|
|
||||||
|
|
||||||
get(eventName: string): ActionDefinition | undefined {
|
|
||||||
return this.actions.get(eventName)
|
|
||||||
}
|
|
||||||
|
|
||||||
getAll(): ActionDefinition[] {
|
|
||||||
return Array.from(this.actions.values())
|
|
||||||
}
|
|
||||||
|
|
||||||
getOptions() {
|
|
||||||
return this.getAll().map((a) => ({ label: a.label, value: a.eventName }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const triggerRegistry = new TriggerRegistry()
|
|
||||||
export const actionRegistry = new ActionRegistry()
|
|
||||||
|
|
||||||
export const allTriggers = {
|
|
||||||
get list() {
|
|
||||||
return triggerRegistry.getAll()
|
|
||||||
},
|
|
||||||
get options() {
|
|
||||||
return triggerRegistry.getOptions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const allActions = {
|
|
||||||
get list() {
|
|
||||||
return actionRegistry.getAll()
|
|
||||||
},
|
|
||||||
get options() {
|
|
||||||
return actionRegistry.getOptions()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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<TriggerComponentProps> = ({ 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 (
|
|
||||||
<Space>
|
|
||||||
<InputNumber
|
|
||||||
placeholder={unit === 'minutes' ? '请输入时间间隔(分钟)' : '请输入时间间隔(天)'}
|
|
||||||
style={{ width: '100px' }}
|
|
||||||
value={displayValue}
|
|
||||||
onChange={handleChange}
|
|
||||||
min={1}
|
|
||||||
/>
|
|
||||||
<Radio.Group
|
|
||||||
value={unit}
|
|
||||||
onChange={(e) => setUnit(e.target.value as 'minutes' | 'days')}
|
|
||||||
optionType="button"
|
|
||||||
buttonStyle="solid"
|
|
||||||
>
|
|
||||||
<Radio.Button value="days">天</Radio.Button>
|
|
||||||
<Radio.Button value="minutes">分钟</Radio.Button>
|
|
||||||
</Radio.Group>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IntervalTimeTrigger
|
|
||||||
@@ -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<TriggerComponentProps> = ({ 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 (
|
|
||||||
<Space>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
||||||
<span style={{ fontSize: '12px', color: 'var(--ss-text-secondary)' }}>从</span>
|
|
||||||
<InputNumber
|
|
||||||
placeholder="最小小时"
|
|
||||||
style={{ width: '70px' }}
|
|
||||||
value={config.minHour}
|
|
||||||
onChange={(v) => handleChange('minHour', v)}
|
|
||||||
min={0}
|
|
||||||
max={23}
|
|
||||||
/>
|
|
||||||
<span style={{ fontSize: '12px', color: 'var(--ss-text-secondary)' }}>时</span>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '4px' }}>
|
|
||||||
<span style={{ fontSize: '12px', color: 'var(--ss-text-secondary)' }}>到</span>
|
|
||||||
<InputNumber
|
|
||||||
placeholder="最大小时"
|
|
||||||
style={{ width: '70px' }}
|
|
||||||
value={config.maxHour}
|
|
||||||
onChange={(v) => handleChange('maxHour', v)}
|
|
||||||
min={0}
|
|
||||||
max={23}
|
|
||||||
/>
|
|
||||||
<span style={{ fontSize: '12px', color: 'var(--ss-text-secondary)' }}>时</span>
|
|
||||||
</div>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RandomTimeTrigger
|
|
||||||
@@ -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<TriggerComponentProps> = ({ 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 (
|
|
||||||
<Select
|
|
||||||
placeholder="请选择标签"
|
|
||||||
style={{ width: '150px' }}
|
|
||||||
value={value ?? undefined}
|
|
||||||
onChange={(v) => onChange(v ? String(v) : '')}
|
|
||||||
options={tags}
|
|
||||||
showSearch
|
|
||||||
allowClear
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StudentTagTrigger
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { triggerRegistry } from '../registry'
|
|
||||||
import type { TriggerDefinition } from '../types'
|
|
||||||
|
|
||||||
import IntervalTimeTrigger, {
|
|
||||||
eventName as intervalEventName,
|
|
||||||
label as intervalLabel,
|
|
||||||
description as intervalDescription,
|
|
||||||
triggerLogic as intervalTriggerLogic
|
|
||||||
} from './IntervalTimeTrigger'
|
|
||||||
|
|
||||||
import StudentTagTrigger, {
|
|
||||||
eventName as studentTagEventName,
|
|
||||||
label as studentTagLabel,
|
|
||||||
description as studentTagDescription,
|
|
||||||
triggerLogic as studentTagTriggerLogic
|
|
||||||
} from './StudentTagTrigger'
|
|
||||||
|
|
||||||
import RandomTimeTrigger, {
|
|
||||||
eventName as randomTimeEventName,
|
|
||||||
label as randomTimeLabel,
|
|
||||||
description as randomTimeDescription,
|
|
||||||
triggerLogic as randomTimeTriggerLogic
|
|
||||||
} from './RandomTimeTrigger'
|
|
||||||
|
|
||||||
const triggerDefinitions: TriggerDefinition[] = [
|
|
||||||
{
|
|
||||||
eventName: intervalEventName,
|
|
||||||
label: intervalLabel,
|
|
||||||
description: intervalDescription,
|
|
||||||
component: IntervalTimeTrigger,
|
|
||||||
triggerLogic: intervalTriggerLogic
|
|
||||||
},
|
|
||||||
{
|
|
||||||
eventName: studentTagEventName,
|
|
||||||
label: studentTagLabel,
|
|
||||||
description: studentTagDescription,
|
|
||||||
component: StudentTagTrigger,
|
|
||||||
triggerLogic: studentTagTriggerLogic
|
|
||||||
},
|
|
||||||
{
|
|
||||||
eventName: randomTimeEventName,
|
|
||||||
label: randomTimeLabel,
|
|
||||||
description: randomTimeDescription,
|
|
||||||
component: RandomTimeTrigger,
|
|
||||||
triggerLogic: randomTimeTriggerLogic
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
triggerDefinitions.forEach((def) => triggerRegistry.register(def))
|
|
||||||
|
|
||||||
export { IntervalTimeTrigger, StudentTagTrigger, RandomTimeTrigger }
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
|
|
||||||
export interface TriggerComponentProps {
|
|
||||||
value: string
|
|
||||||
onChange: (value: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ActionComponentProps {
|
|
||||||
value: string
|
|
||||||
reason: string
|
|
||||||
onChange: (value: string) => void
|
|
||||||
onReasonChange: (reason: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TriggerLogic {
|
|
||||||
eventName: string
|
|
||||||
label: string
|
|
||||||
description: string
|
|
||||||
validate: (value: string) => { valid: boolean; message?: string }
|
|
||||||
calculateNextTime?: (
|
|
||||||
value: string,
|
|
||||||
lastExecuted: Date | undefined,
|
|
||||||
now: Date
|
|
||||||
) => { delayMs: number; nextExecuteTime: Date }
|
|
||||||
check?: (context: any, value: string) => { shouldExecute: boolean; message?: string }
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TriggerDefinition {
|
|
||||||
eventName: string
|
|
||||||
label: string
|
|
||||||
description: string
|
|
||||||
component: React.FC<TriggerComponentProps>
|
|
||||||
triggerLogic?: TriggerLogic
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ActionDefinition {
|
|
||||||
eventName: string
|
|
||||||
label: string
|
|
||||||
description: string
|
|
||||||
component: React.FC<ActionComponentProps>
|
|
||||||
hasReason?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TriggerItem {
|
|
||||||
id: number
|
|
||||||
eventName: string
|
|
||||||
value: string
|
|
||||||
relation?: 'AND' | 'OR'
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ActionItem {
|
|
||||||
id: number
|
|
||||||
eventName: string
|
|
||||||
value: string
|
|
||||||
reason: string
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { Service } from '../../../shared/kernel'
|
import { Service } from '../../../shared/kernel'
|
||||||
import { ClientContext } from '../ClientContext'
|
import { ClientContext } from '../ClientContext'
|
||||||
import { allTriggers, allActions } from '../components/com.automatically'
|
|
||||||
import type { TriggerItem, ActionItem } from '../components/com.automatically/types'
|
|
||||||
|
|
||||||
export interface AutoScoreRule {
|
export interface AutoScoreRule {
|
||||||
id: number
|
id: number
|
||||||
@@ -9,7 +7,7 @@ export interface AutoScoreRule {
|
|||||||
name: string
|
name: string
|
||||||
studentNames: string[]
|
studentNames: string[]
|
||||||
lastExecuted?: string
|
lastExecuted?: string
|
||||||
triggers?: { event: string; value?: string }[]
|
triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[]
|
||||||
actions?: { event: string; value?: string; reason?: string }[]
|
actions?: { event: string; value?: string; reason?: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +15,7 @@ export interface AutoScoreRuleInput {
|
|||||||
enabled: boolean
|
enabled: boolean
|
||||||
name: string
|
name: string
|
||||||
studentNames: string[]
|
studentNames: string[]
|
||||||
triggers?: { event: string; value?: string }[]
|
triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[]
|
||||||
actions?: { event: string; value?: string; reason?: string }[]
|
actions?: { event: string; value?: string; reason?: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,9 +25,6 @@ declare module '../../../shared/kernel' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { allTriggers, allActions }
|
|
||||||
export type { TriggerItem, ActionItem }
|
|
||||||
|
|
||||||
export class AutoScoreService extends Service {
|
export class AutoScoreService extends Service {
|
||||||
constructor(ctx: ClientContext) {
|
constructor(ctx: ClientContext) {
|
||||||
super(ctx, 'autoScore')
|
super(ctx, 'autoScore')
|
||||||
@@ -73,53 +68,4 @@ export class AutoScoreService extends Service {
|
|||||||
async getStatus(): Promise<{ success: boolean; data?: { enabled: boolean }; message?: string }> {
|
async getStatus(): Promise<{ success: boolean; data?: { enabled: boolean }; message?: string }> {
|
||||||
return await (window as any).api.invoke('auto-score:getStatus', {})
|
return await (window as any).api.invoke('auto-score:getStatus', {})
|
||||||
}
|
}
|
||||||
|
|
||||||
createTriggerItem(triggerList: TriggerItem[]): TriggerItem {
|
|
||||||
const nextId = triggerList.length ? Math.max(...triggerList.map((t) => t.id)) + 1 : 1
|
|
||||||
const defaultTrigger = allTriggers.list[0]
|
|
||||||
return {
|
|
||||||
id: nextId,
|
|
||||||
eventName: defaultTrigger.eventName,
|
|
||||||
value: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTriggerEvent(triggerList: TriggerItem[], id: number, eventName: string): TriggerItem[] {
|
|
||||||
return triggerList.map((t) => (t.id === id ? { ...t, eventName, value: '' } : t))
|
|
||||||
}
|
|
||||||
|
|
||||||
updateTriggerValue(triggerList: TriggerItem[], id: number, value: string): TriggerItem[] {
|
|
||||||
return triggerList.map((t) => (t.id === id ? { ...t, value } : t))
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteTriggerItem(triggerList: TriggerItem[], id: number): TriggerItem[] {
|
|
||||||
return triggerList.filter((t) => t.id !== id)
|
|
||||||
}
|
|
||||||
|
|
||||||
createActionItem(actionList: ActionItem[]): ActionItem {
|
|
||||||
const nextId = actionList.length ? Math.max(...actionList.map((a) => a.id)) + 1 : 1
|
|
||||||
const defaultAction = allActions.list[0]
|
|
||||||
return {
|
|
||||||
id: nextId,
|
|
||||||
eventName: defaultAction.eventName,
|
|
||||||
value: '',
|
|
||||||
reason: ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateActionEvent(actionList: ActionItem[], id: number, eventName: string): ActionItem[] {
|
|
||||||
return actionList.map((a) => (a.id === id ? { ...a, eventName, value: '' } : a))
|
|
||||||
}
|
|
||||||
|
|
||||||
updateActionValue(actionList: ActionItem[], id: number, value: string): ActionItem[] {
|
|
||||||
return actionList.map((a) => (a.id === id ? { ...a, value } : a))
|
|
||||||
}
|
|
||||||
|
|
||||||
updateActionReason(actionList: ActionItem[], id: number, reason: string): ActionItem[] {
|
|
||||||
return actionList.map((a) => (a.id === id ? { ...a, reason } : a))
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteActionItem(actionList: ActionItem[], id: number): ActionItem[] {
|
|
||||||
return actionList.filter((a) => a.id !== id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,244 @@
|
|||||||
|
export interface AutoScoreContext {
|
||||||
|
students: Array<{
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
tags: string[]
|
||||||
|
lastScoreTime: Date
|
||||||
|
}>
|
||||||
|
events: Array<{
|
||||||
|
id: number
|
||||||
|
student_name: string
|
||||||
|
delta: number
|
||||||
|
reason_content: string
|
||||||
|
created_at: Date
|
||||||
|
}>
|
||||||
|
rule: {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
studentNames: string[]
|
||||||
|
triggers?: Array<{ event: string; value?: string; relation?: 'AND' | 'OR' }>
|
||||||
|
actions?: Array<{ event: string; value?: string; reason?: string }>
|
||||||
|
}
|
||||||
|
now: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutoScoreActionContext {
|
||||||
|
students: Array<{
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
tags: string[]
|
||||||
|
}>
|
||||||
|
params: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RuleConfig {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
enabled: boolean
|
||||||
|
studentNames: string[]
|
||||||
|
triggers: Array<{
|
||||||
|
event: string
|
||||||
|
value?: string
|
||||||
|
relation?: 'AND' | 'OR'
|
||||||
|
}>
|
||||||
|
actions: Array<{
|
||||||
|
event: string
|
||||||
|
value?: string
|
||||||
|
reason?: string
|
||||||
|
}>
|
||||||
|
lastExecuted?: Date
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TriggerCheckResult {
|
||||||
|
matchedStudents: Array<{ id: number; name: string; tags?: string[]; lastScoreTime?: Date }>
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AutoScoreRuleEngine {
|
||||||
|
private rules: Map<number, RuleConfig> = new Map()
|
||||||
|
|
||||||
|
async addRule(rule: RuleConfig): Promise<void> {
|
||||||
|
this.rules.set(rule.id, rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkIntervalTimeTrigger(
|
||||||
|
students: AutoScoreContext['students'],
|
||||||
|
value: string | undefined,
|
||||||
|
now: Date
|
||||||
|
): Set<number> {
|
||||||
|
const minutes = parseInt(value || '30', 10)
|
||||||
|
if (isNaN(minutes) || minutes <= 0) return new Set()
|
||||||
|
|
||||||
|
const intervalMs = minutes * 60 * 1000
|
||||||
|
|
||||||
|
const matchedIds = new Set<number>()
|
||||||
|
for (const student of students) {
|
||||||
|
if (!student.lastScoreTime) {
|
||||||
|
matchedIds.add(student.id)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeSinceLastScore = now.getTime() - new Date(student.lastScoreTime).getTime()
|
||||||
|
if (timeSinceLastScore >= intervalMs) {
|
||||||
|
matchedIds.add(student.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchedIds
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkStudentTagTrigger(
|
||||||
|
students: AutoScoreContext['students'],
|
||||||
|
value: string | undefined
|
||||||
|
): Set<number> {
|
||||||
|
const requiredTags = value
|
||||||
|
?.split(',')
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean) || []
|
||||||
|
|
||||||
|
if (requiredTags.length === 0) return new Set()
|
||||||
|
|
||||||
|
const matchedIds = new Set<number>()
|
||||||
|
for (const student of students) {
|
||||||
|
const studentTags = student.tags || []
|
||||||
|
if (requiredTags.some((tag) => studentTags.includes(tag))) {
|
||||||
|
matchedIds.add(student.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchedIds
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(context: AutoScoreContext): Promise<
|
||||||
|
{
|
||||||
|
ruleId: number
|
||||||
|
matchedStudents: Array<{ id: number; name: string; tags?: string[] }>
|
||||||
|
actions: Array<{ event: string; value?: string; reason?: string }>
|
||||||
|
}[]
|
||||||
|
> {
|
||||||
|
const results: {
|
||||||
|
ruleId: number
|
||||||
|
matchedStudents: Array<{ id: number; name: string; tags?: string[] }>
|
||||||
|
actions: Array<{ event: string; value?: string; reason?: string }>
|
||||||
|
}[] = []
|
||||||
|
|
||||||
|
for (const [, rule] of this.rules) {
|
||||||
|
if (!rule.enabled) continue
|
||||||
|
|
||||||
|
const triggers = rule.triggers || []
|
||||||
|
if (triggers.length === 0) continue
|
||||||
|
|
||||||
|
const triggerResults: Set<number>[] = []
|
||||||
|
const relations: ('AND' | 'OR')[] = []
|
||||||
|
|
||||||
|
for (let i = 0; i < triggers.length; i++) {
|
||||||
|
const trigger = triggers[i]
|
||||||
|
let matchedIds: Set<number>
|
||||||
|
|
||||||
|
switch (trigger.event) {
|
||||||
|
case 'interval_time_passed':
|
||||||
|
matchedIds = this.checkIntervalTimeTrigger(
|
||||||
|
context.students,
|
||||||
|
trigger.value,
|
||||||
|
context.now
|
||||||
|
)
|
||||||
|
break
|
||||||
|
case 'student_has_tag':
|
||||||
|
matchedIds = this.checkStudentTagTrigger(context.students, trigger.value)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
triggerResults.push(matchedIds)
|
||||||
|
relations.push(trigger.relation || (i === 0 ? 'AND' : 'AND'))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (triggerResults.length === 0) continue
|
||||||
|
|
||||||
|
let finalMatchedIds: Set<number>
|
||||||
|
const firstRelation = triggers[0]?.relation || 'AND'
|
||||||
|
|
||||||
|
if (firstRelation === 'OR') {
|
||||||
|
finalMatchedIds = new Set<number>()
|
||||||
|
for (const ids of triggerResults) {
|
||||||
|
for (const id of ids) {
|
||||||
|
finalMatchedIds.add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finalMatchedIds = new Set(triggerResults[0])
|
||||||
|
for (let i = 1; i < triggerResults.length; i++) {
|
||||||
|
const relation = triggers[i]?.relation || 'AND'
|
||||||
|
if (relation === 'OR') {
|
||||||
|
for (const id of triggerResults[i]) {
|
||||||
|
finalMatchedIds.add(id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const nextSet = new Set<number>()
|
||||||
|
for (const id of finalMatchedIds) {
|
||||||
|
if (triggerResults[i].has(id)) {
|
||||||
|
nextSet.add(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finalMatchedIds = nextSet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalMatchedIds.size > 0) {
|
||||||
|
const matchedStudents = context.students.filter((s) => finalMatchedIds.has(s.id))
|
||||||
|
|
||||||
|
results.push({
|
||||||
|
ruleId: rule.id,
|
||||||
|
matchedStudents: matchedStudents.map((s) => ({
|
||||||
|
id: s.id,
|
||||||
|
name: s.name,
|
||||||
|
tags: s.tags
|
||||||
|
})),
|
||||||
|
actions: rule.actions || []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeAction(
|
||||||
|
actionType: string,
|
||||||
|
students: Array<{ id: number; name: string; tags?: string[] }>,
|
||||||
|
params: any,
|
||||||
|
eventRepo: any
|
||||||
|
): Promise<void> {
|
||||||
|
switch (actionType) {
|
||||||
|
case 'add_score': {
|
||||||
|
const scoreValue = params.value ? parseInt(params.value, 10) : 0
|
||||||
|
const reason = params.reason || `自动化加分 - ${params.ruleName}`
|
||||||
|
for (const student of students) {
|
||||||
|
await eventRepo.create({
|
||||||
|
student_name: student.name,
|
||||||
|
reason_content: reason,
|
||||||
|
delta: scoreValue
|
||||||
|
})
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'add_tag': {
|
||||||
|
const tagName = params.value
|
||||||
|
if (tagName) {
|
||||||
|
const studentRepo: any = params.studentRepo
|
||||||
|
for (const student of students) {
|
||||||
|
const currentTags = student.tags || []
|
||||||
|
if (!currentTags.includes(tagName)) {
|
||||||
|
await studentRepo.update(student.id, {
|
||||||
|
tags: [...currentTags, tagName]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
console.warn(`Unknown action event: ${actionType}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './AutoScoreRuleEngine' // interfaces and engine consolidated
|
||||||
|
|
||||||
|
export * from './triggers/IntervalTimeTrigger'
|
||||||
|
export * from './triggers/StudentTagTrigger'
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import type { AutoScoreContext } from '../AutoScoreRuleEngine'
|
||||||
|
|
||||||
|
export interface IntervalTimeTriggerConfig {
|
||||||
|
intervalMinutes: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createIntervalTimeTrigger() {
|
||||||
|
return {
|
||||||
|
id: 'interval_time_passed',
|
||||||
|
label: '间隔时间',
|
||||||
|
description: '每隔指定时间自动触发',
|
||||||
|
|
||||||
|
validate: (value: string): { valid: boolean; message?: string } => {
|
||||||
|
const minutes = parseInt(value, 10)
|
||||||
|
if (isNaN(minutes) || minutes <= 0) {
|
||||||
|
return { valid: false, message: '请输入有效的分钟数' }
|
||||||
|
}
|
||||||
|
return { valid: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
calculateNextTime: (
|
||||||
|
value: string,
|
||||||
|
lastExecuted: Date | undefined,
|
||||||
|
now: Date
|
||||||
|
): { delayMs: number; nextExecuteTime: Date } => {
|
||||||
|
const minutes = parseInt(value, 10)
|
||||||
|
const intervalMs = minutes * 60 * 1000
|
||||||
|
|
||||||
|
let nextExecuteTime: Date
|
||||||
|
if (lastExecuted) {
|
||||||
|
nextExecuteTime = new Date(lastExecuted.getTime() + intervalMs)
|
||||||
|
} else {
|
||||||
|
nextExecuteTime = new Date(now.getTime() + intervalMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
const delayMs = nextExecuteTime.getTime() - now.getTime()
|
||||||
|
return { delayMs, nextExecuteTime }
|
||||||
|
},
|
||||||
|
|
||||||
|
check: (
|
||||||
|
context: AutoScoreContext,
|
||||||
|
value: string
|
||||||
|
): {
|
||||||
|
shouldExecute: boolean
|
||||||
|
matchedStudents?: Array<{ id: number; name: string; tags: string[] }>
|
||||||
|
nextExecuteTime?: Date
|
||||||
|
delayMs?: number
|
||||||
|
} => {
|
||||||
|
const minutes = parseInt(value, 10)
|
||||||
|
if (isNaN(minutes) || minutes <= 0) {
|
||||||
|
return { shouldExecute: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const intervalMs = minutes * 60 * 1000
|
||||||
|
const now = context.now
|
||||||
|
|
||||||
|
const matchedStudents = context.students.filter((student) => {
|
||||||
|
if (!student.lastScoreTime) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeSinceLastScore = now.getTime() - student.lastScoreTime.getTime()
|
||||||
|
return timeSinceLastScore >= intervalMs
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
shouldExecute: matchedStudents.length > 0,
|
||||||
|
matchedStudents
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toRuleCondition: (value: string): any => {
|
||||||
|
const minutes = parseInt(value, 10)
|
||||||
|
const intervalMs = minutes * 60 * 1000
|
||||||
|
|
||||||
|
return {
|
||||||
|
all: [
|
||||||
|
{
|
||||||
|
fact: 'student',
|
||||||
|
path: '.lastScoreTime',
|
||||||
|
operator: 'lessThan',
|
||||||
|
value: new Date(Date.now() - intervalMs).toISOString()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import type { AutoScoreContext } from '../AutoScoreRuleEngine'
|
||||||
|
|
||||||
|
export interface StudentTagTriggerConfig {
|
||||||
|
requiredTags: string[]
|
||||||
|
matchMode: 'any' | 'all'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createStudentTagTrigger() {
|
||||||
|
return {
|
||||||
|
id: 'student_has_tag',
|
||||||
|
label: '学生标签',
|
||||||
|
description: '当学生拥有指定标签时触发(需要以间隔时间加分为前提)',
|
||||||
|
|
||||||
|
validate: (value: string): { valid: boolean; message?: string } => {
|
||||||
|
if (!value || value.trim() === '') {
|
||||||
|
return { valid: false, message: '请输入标签名称' }
|
||||||
|
}
|
||||||
|
return { valid: true }
|
||||||
|
},
|
||||||
|
|
||||||
|
check: (
|
||||||
|
context: AutoScoreContext,
|
||||||
|
value: string
|
||||||
|
): {
|
||||||
|
shouldExecute: boolean
|
||||||
|
matchedStudents?: Array<{ id: number; name: string; tags: string[] }>
|
||||||
|
} => {
|
||||||
|
const requiredTags = value
|
||||||
|
.split(',')
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
if (requiredTags.length === 0) {
|
||||||
|
return { shouldExecute: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchedStudents = context.students.filter((student) => {
|
||||||
|
const studentTags = student.tags || []
|
||||||
|
return requiredTags.some((tag) => studentTags.includes(tag))
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
shouldExecute: matchedStudents.length > 0,
|
||||||
|
matchedStudents
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
toRuleCondition: (value: string): any => {
|
||||||
|
const requiredTags = value
|
||||||
|
.split(',')
|
||||||
|
.map((t) => t.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
return {
|
||||||
|
all: [
|
||||||
|
{
|
||||||
|
fact: 'student',
|
||||||
|
path: '.tags',
|
||||||
|
operator: 'containsAny',
|
||||||
|
value: requiredTags
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
requiresDependency: () => {
|
||||||
|
return 'interval_time_passed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import type { TriggerLogic } from './types'
|
|
||||||
|
|
||||||
export const intervalTimeTrigger: TriggerLogic = {
|
|
||||||
eventName: 'interval_time_passed',
|
|
||||||
label: '根据间隔时间触发',
|
|
||||||
description: '当间隔时间到达时触发自动化',
|
|
||||||
|
|
||||||
validate: (value: string) => {
|
|
||||||
const minutes = parseInt(value, 10)
|
|
||||||
if (isNaN(minutes) || minutes <= 0) {
|
|
||||||
return { valid: false, message: '请输入有效的时间间隔(分钟)' }
|
|
||||||
}
|
|
||||||
return { valid: true }
|
|
||||||
},
|
|
||||||
|
|
||||||
calculateNextTime: (value: string, lastExecuted: Date | undefined, now: Date) => {
|
|
||||||
const intervalMinutes = parseInt(value, 10)
|
|
||||||
if (isNaN(intervalMinutes) || intervalMinutes <= 0) {
|
|
||||||
return { delayMs: 0, nextExecuteTime: now }
|
|
||||||
}
|
|
||||||
|
|
||||||
const intervalMs = intervalMinutes * 60 * 1000
|
|
||||||
|
|
||||||
let delayMs = intervalMs
|
|
||||||
if (lastExecuted) {
|
|
||||||
const timeSinceLastExecution = now.getTime() - lastExecuted.getTime()
|
|
||||||
delayMs = intervalMs - (timeSinceLastExecution % intervalMs)
|
|
||||||
if (timeSinceLastExecution >= intervalMs) {
|
|
||||||
delayMs = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextExecuteTime = new Date(now.getTime() + delayMs)
|
|
||||||
return { delayMs, nextExecuteTime }
|
|
||||||
},
|
|
||||||
|
|
||||||
check: (context, value) => {
|
|
||||||
const result = intervalTimeTrigger.calculateNextTime!(
|
|
||||||
value,
|
|
||||||
context.rule.triggers?.find((t) => t.event === 'interval_time_passed')
|
|
||||||
? context.now
|
|
||||||
: undefined,
|
|
||||||
context.now
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
shouldExecute: result.delayMs === 0,
|
|
||||||
matchedStudents: context.students,
|
|
||||||
nextExecuteTime: result.nextExecuteTime,
|
|
||||||
delayMs: result.delayMs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import type { TriggerLogic } from './types'
|
|
||||||
|
|
||||||
export const randomTimeTrigger: TriggerLogic = {
|
|
||||||
eventName: 'random_time_reached',
|
|
||||||
label: '随机时间触发',
|
|
||||||
description: '当随机时间到达时触发自动化',
|
|
||||||
|
|
||||||
validate: (value: string) => {
|
|
||||||
if (!value) {
|
|
||||||
return { valid: true }
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const config = JSON.parse(value)
|
|
||||||
if (config.minHour !== undefined || config.maxHour !== undefined) {
|
|
||||||
const minHour = config.minHour ?? 0
|
|
||||||
const maxHour = config.maxHour ?? 23
|
|
||||||
if (minHour < 0 || minHour > 23 || maxHour < 0 || maxHour > 23) {
|
|
||||||
return { valid: false, message: '小时范围必须在0-23之间' }
|
|
||||||
}
|
|
||||||
if (minHour > maxHour) {
|
|
||||||
return { valid: false, message: '最小小时不能大于最大小时' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { valid: true }
|
|
||||||
} catch {
|
|
||||||
return { valid: false, message: '配置格式错误' }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
calculateNextTime: (value: string, _lastExecuted: Date | undefined, now: Date) => {
|
|
||||||
let config = { minHour: 9, maxHour: 18 }
|
|
||||||
try {
|
|
||||||
if (value) {
|
|
||||||
const parsed = JSON.parse(value)
|
|
||||||
config = { ...config, ...parsed }
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// swallow errors, use default config
|
|
||||||
console.debug('randomTimeTrigger calculateNextTime parse error', e)
|
|
||||||
}
|
|
||||||
|
|
||||||
const minHour = config.minHour ?? 0
|
|
||||||
const maxHour = config.maxHour ?? 23
|
|
||||||
|
|
||||||
const randomHour = Math.floor(Math.random() * (maxHour - minHour + 1)) + minHour
|
|
||||||
const randomMinute = Math.floor(Math.random() * 60)
|
|
||||||
|
|
||||||
const targetDate = new Date(now)
|
|
||||||
targetDate.setHours(randomHour, randomMinute, 0, 0)
|
|
||||||
|
|
||||||
if (targetDate.getTime() <= now.getTime()) {
|
|
||||||
targetDate.setDate(targetDate.getDate() + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const delayMs = targetDate.getTime() - now.getTime()
|
|
||||||
return { delayMs, nextExecuteTime: targetDate }
|
|
||||||
},
|
|
||||||
|
|
||||||
check: (context, value) => {
|
|
||||||
const result = randomTimeTrigger.calculateNextTime!(value, undefined, context.now)
|
|
||||||
return {
|
|
||||||
shouldExecute: result.delayMs === 0,
|
|
||||||
matchedStudents: context.students,
|
|
||||||
nextExecuteTime: result.nextExecuteTime,
|
|
||||||
delayMs: result.delayMs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import type { TriggerLogic } from './types'
|
|
||||||
|
|
||||||
export const studentTagTrigger: TriggerLogic = {
|
|
||||||
eventName: 'student_tag_matched',
|
|
||||||
label: '按照学生标签触发',
|
|
||||||
description: '当学生标签匹配时触发自动化',
|
|
||||||
|
|
||||||
validate: (value: string) => {
|
|
||||||
if (!value || value.trim() === '') {
|
|
||||||
return { valid: false, message: '请输入标签名称' }
|
|
||||||
}
|
|
||||||
return { valid: true }
|
|
||||||
},
|
|
||||||
|
|
||||||
check: (context, value) => {
|
|
||||||
const tagName = value.trim().toLowerCase()
|
|
||||||
const matchedStudents = context.students.filter((student: any) => {
|
|
||||||
if (student.tags && Array.isArray(student.tags)) {
|
|
||||||
return student.tags.some((tag: string) => tag.toLowerCase() === tagName)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
shouldExecute: matchedStudents.length > 0,
|
|
||||||
matchedStudents,
|
|
||||||
nextExecuteTime: context.now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import type { TriggerLogic } from './types'
|
|
||||||
import { intervalTimeTrigger } from './IntervalTimeTrigger'
|
|
||||||
import { studentTagTrigger } from './StudentTagTrigger'
|
|
||||||
import { randomTimeTrigger } from './RandomTimeTrigger'
|
|
||||||
|
|
||||||
export * from './types'
|
|
||||||
export { intervalTimeTrigger, studentTagTrigger, randomTimeTrigger }
|
|
||||||
|
|
||||||
export const allTriggerLogics: TriggerLogic[] = [
|
|
||||||
intervalTimeTrigger,
|
|
||||||
studentTagTrigger,
|
|
||||||
randomTimeTrigger
|
|
||||||
]
|
|
||||||
|
|
||||||
export function getTriggerLogic(eventName: string): TriggerLogic | undefined {
|
|
||||||
const direct = allTriggerLogics.find((t) => t.eventName === eventName)
|
|
||||||
if (direct) return direct
|
|
||||||
const aliasMap: Record<string, string> = {
|
|
||||||
random_time: 'random_time_reached',
|
|
||||||
student_tag_added: 'student_tag_matched'
|
|
||||||
}
|
|
||||||
const mapped = aliasMap[eventName]
|
|
||||||
if (mapped) {
|
|
||||||
return allTriggerLogics.find((t) => t.eventName === mapped)
|
|
||||||
}
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
export interface TriggerContext {
|
|
||||||
students: any[]
|
|
||||||
events: any[]
|
|
||||||
rule: {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
studentNames: string[]
|
|
||||||
triggers?: { event: string; value?: string; relation?: 'AND' | 'OR' }[]
|
|
||||||
actions?: { event: string; value?: string; reason?: string }[]
|
|
||||||
}
|
|
||||||
now: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TriggerResult {
|
|
||||||
shouldExecute: boolean
|
|
||||||
matchedStudents?: any[]
|
|
||||||
nextExecuteTime?: Date
|
|
||||||
delayMs?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TriggerLogic {
|
|
||||||
eventName: string
|
|
||||||
label: string
|
|
||||||
description: string
|
|
||||||
validate: (value: string) => { valid: boolean; message?: string }
|
|
||||||
calculateNextTime?: (
|
|
||||||
value: string,
|
|
||||||
lastExecuted: Date | undefined,
|
|
||||||
now: Date
|
|
||||||
) => { delayMs: number; nextExecuteTime: Date }
|
|
||||||
check?: (context: TriggerContext, value: string) => TriggerResult
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user