mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 21:14:21 +08:00
Merge branch 'main' of https://github.com/SECTL/SecScore
This commit is contained in:
+15
-4
@@ -65,17 +65,27 @@
|
|||||||
9. `reward_redemptions`
|
9. `reward_redemptions`
|
||||||
- `id`, `uuid`, `student_name`, `reward_id`, `reward_name`, `cost_points`, `redeemed_at`
|
- `id`, `uuid`, `student_name`, `reward_id`, `reward_name`, `cost_points`, `redeemed_at`
|
||||||
|
|
||||||
## 5) 看板展示字段命名约定(尽量遵守)
|
## 5) 字段名使用铁律(必须遵守,防止列不存在)
|
||||||
|
1. `SELECT`、`WHERE`、`GROUP BY`、`ORDER BY`、`JOIN ON` 中引用的字段名,必须来自上面的“可用业务表与字段”原样字段,严禁臆造字段。
|
||||||
|
2. “展示名/别名”不等于真实字段名。若需展示为 `student_name`,必须使用别名,不可把别名当字段直接查询。
|
||||||
|
3. 关键映射(高频易错):
|
||||||
|
- `students` 表没有 `student_name`,只有 `name`。正确写法:`students.name AS student_name`(或 `s.name AS student_name`)
|
||||||
|
- `score_events` 表有 `student_name`
|
||||||
|
- `reward_redemptions` 表有 `student_name`
|
||||||
|
4. 错误示例(禁止生成):`SELECT student_name, score FROM students`
|
||||||
|
5. 正确示例(优先生成):`SELECT name AS student_name, score FROM students`
|
||||||
|
|
||||||
|
## 6) 看板展示字段命名约定(尽量遵守)
|
||||||
- 学生名统一命名为:`student_name`
|
- 学生名统一命名为:`student_name`
|
||||||
- 常见指标命名建议:`score`, `reward_points`, `week_change`, `week_deducted`, `answered_count`
|
- 常见指标命名建议:`score`, `reward_points`, `week_change`, `week_deducted`, `answered_count`
|
||||||
|
|
||||||
## 6) 生成策略
|
## 7) 生成策略
|
||||||
1. 排行类需求必须有 `ORDER BY`。
|
1. 排行类需求必须有 `ORDER BY`。
|
||||||
2. 聚合类需求必须有清晰别名。
|
2. 聚合类需求必须有清晰别名。
|
||||||
3. 对可能为空的数据优先使用 `COALESCE`。
|
3. 对可能为空的数据优先使用 `COALESCE`。
|
||||||
4. 默认不写 `LIMIT`(系统外层会限制);除非用户明确要求更小结果集。
|
4. 默认不写 `LIMIT`(系统外层会限制);除非用户明确要求更小结果集。
|
||||||
|
|
||||||
## 7) 自然周规则(允许生成上个自然周 SQL)
|
## 8) 自然周规则(允许生成上个自然周 SQL)
|
||||||
当用户要求“上个自然周/本自然周”时,必须使用模板变量区间表达,不要自行计算数据库日期函数:
|
当用户要求“上个自然周/本自然周”时,必须使用模板变量区间表达,不要自行计算数据库日期函数:
|
||||||
- 上个自然周:`event_time >= '{ {last_week_start} }' AND event_time < '{ {this_week_start} }'`
|
- 上个自然周:`event_time >= '{ {last_week_start} }' AND event_time < '{ {this_week_start} }'`
|
||||||
- 本自然周:`event_time >= '{ {this_week_start} }'`
|
- 本自然周:`event_time >= '{ {this_week_start} }'`
|
||||||
@@ -85,7 +95,7 @@
|
|||||||
2. 不要发明其它周边界变量。
|
2. 不要发明其它周边界变量。
|
||||||
3. 避免使用 SQLite/PostgreSQL 方言日期函数(如 `strftime`、`date_trunc`)以保证跨库兼容。
|
3. 避免使用 SQLite/PostgreSQL 方言日期函数(如 `strftime`、`date_trunc`)以保证跨库兼容。
|
||||||
|
|
||||||
## 8) 输出前自检清单(必须全部为“是”)
|
## 9) 输出前自检清单(必须全部为“是”)
|
||||||
- 是否只用了 `SELECT/WITH`?
|
- 是否只用了 `SELECT/WITH`?
|
||||||
- 是否没有 `;` 和注释?
|
- 是否没有 `;` 和注释?
|
||||||
- 是否没有禁用关键词?
|
- 是否没有禁用关键词?
|
||||||
@@ -93,6 +103,7 @@
|
|||||||
- 模板变量是否都加了单引号?
|
- 模板变量是否都加了单引号?
|
||||||
- 是否没有系统表(如 `sqlite_master`)?
|
- 是否没有系统表(如 `sqlite_master`)?
|
||||||
- 是否只用了给定业务表?
|
- 是否只用了给定业务表?
|
||||||
|
- 是否所有字段都能在对应表字段清单中找到(尤其检查 `students.name` vs `students.student_name`)?
|
||||||
|
|
||||||
用户需求:
|
用户需求:
|
||||||
{ {在这里粘贴用户需求} }
|
{ {在这里粘贴用户需求} }
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use sea_orm::{ConnectionTrait, DatabaseConnection, Statement};
|
||||||
use serde_json::{Map, Value as JsonValue};
|
use serde_json::{Map, Value as JsonValue};
|
||||||
use sqlx::{Column, Row, SqlitePool};
|
use sqlx::{Column, Row, SqlitePool};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
@@ -24,11 +25,23 @@ enum QueryBackend {
|
|||||||
Postgres,
|
Postgres,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_view_permission(state: &Arc<RwLock<AppState>>, sender_id: Option<u32>) -> bool {
|
fn check_permission(
|
||||||
|
state: &Arc<RwLock<AppState>>,
|
||||||
|
sender_id: Option<u32>,
|
||||||
|
level: PermissionLevel,
|
||||||
|
) -> bool {
|
||||||
let state_guard = state.read();
|
let state_guard = state.read();
|
||||||
let mut permissions = state_guard.permissions.write();
|
let mut permissions = state_guard.permissions.write();
|
||||||
let id = sender_id.unwrap_or(0);
|
let id = sender_id.unwrap_or(0);
|
||||||
permissions.require_permission(id, PermissionLevel::View)
|
permissions.require_permission(id, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_view_permission(state: &Arc<RwLock<AppState>>, sender_id: Option<u32>) -> bool {
|
||||||
|
check_permission(state, sender_id, PermissionLevel::View)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_admin_permission(state: &Arc<RwLock<AppState>>, sender_id: Option<u32>) -> bool {
|
||||||
|
check_permission(state, sender_id, PermissionLevel::Admin)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn contains_forbidden_keyword(sql: &str) -> bool {
|
fn contains_forbidden_keyword(sql: &str) -> bool {
|
||||||
@@ -92,6 +105,87 @@ fn parse_limit(limit: Option<u64>) -> u64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn escape_sql(value: &str) -> String {
|
||||||
|
value.replace('\'', "''")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn now_iso() -> String {
|
||||||
|
chrono::Utc::now()
|
||||||
|
.format("%Y-%m-%dT%H:%M:%S%.3fZ")
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_board_configs(value: JsonValue) -> JsonValue {
|
||||||
|
if value.is_array() {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
JsonValue::Array(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ensure_board_configs_table(conn: &DatabaseConnection) -> Result<(), String> {
|
||||||
|
let sql = r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS board_configs (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
config_json TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
)
|
||||||
|
"#
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
conn.execute(Statement::from_string(conn.get_database_backend(), sql))
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_board_configs_raw(conn: &DatabaseConnection) -> Result<Option<String>, String> {
|
||||||
|
let sql = "SELECT config_json FROM board_configs WHERE id = 1".to_string();
|
||||||
|
let row = conn
|
||||||
|
.query_one(Statement::from_string(conn.get_database_backend(), sql))
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
match row {
|
||||||
|
Some(r) => r
|
||||||
|
.try_get("", "config_json")
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|e| e.to_string()),
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn upsert_board_configs_raw(conn: &DatabaseConnection, config_json: &str) -> Result<(), String> {
|
||||||
|
let config_escaped = escape_sql(config_json);
|
||||||
|
let now_escaped = escape_sql(&now_iso());
|
||||||
|
let sql = format!(
|
||||||
|
"INSERT INTO board_configs (id, config_json, updated_at) VALUES (1, '{}', '{}') ON CONFLICT(id) DO UPDATE SET config_json = EXCLUDED.config_json, updated_at = EXCLUDED.updated_at",
|
||||||
|
config_escaped, now_escaped
|
||||||
|
);
|
||||||
|
|
||||||
|
conn.execute(Statement::from_string(conn.get_database_backend(), sql))
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_legacy_board_configs_from_settings(
|
||||||
|
state: &Arc<RwLock<AppState>>,
|
||||||
|
conn: Option<DatabaseConnection>,
|
||||||
|
) -> Result<JsonValue, String> {
|
||||||
|
let state_guard = state.read();
|
||||||
|
let mut settings = state_guard.settings.write();
|
||||||
|
settings.attach_db(conn);
|
||||||
|
settings.initialize().await.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let legacy = match settings.get_value(SettingsKey::DashboardsConfig) {
|
||||||
|
SettingsValue::Json(v) => v,
|
||||||
|
_ => JsonValue::Array(vec![]),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(normalize_board_configs(legacy))
|
||||||
|
}
|
||||||
|
|
||||||
fn sqlite_db_path(app_handle: &AppHandle) -> Result<String, String> {
|
fn sqlite_db_path(app_handle: &AppHandle) -> Result<String, String> {
|
||||||
if cfg!(all(debug_assertions, desktop)) {
|
if cfg!(all(debug_assertions, desktop)) {
|
||||||
return Ok("data.sql".to_string());
|
return Ok("data.sql".to_string());
|
||||||
@@ -200,6 +294,68 @@ async fn query_postgres(sql: &str, pg_url: &str) -> Result<Vec<JsonValue>, Strin
|
|||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn board_get_configs(
|
||||||
|
sender_id: Option<u32>,
|
||||||
|
state: State<'_, Arc<RwLock<AppState>>>,
|
||||||
|
) -> Result<IpcResponse<JsonValue>, String> {
|
||||||
|
if !check_view_permission(&state, sender_id) {
|
||||||
|
return Ok(IpcResponse::error("Permission denied: view required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let db_conn = {
|
||||||
|
let state_guard = state.read();
|
||||||
|
let conn = state_guard.db.read().clone();
|
||||||
|
conn
|
||||||
|
};
|
||||||
|
let Some(conn) = db_conn else {
|
||||||
|
return Ok(IpcResponse::error("Database not connected"));
|
||||||
|
};
|
||||||
|
|
||||||
|
ensure_board_configs_table(&conn).await?;
|
||||||
|
|
||||||
|
if let Some(raw) = get_board_configs_raw(&conn).await? {
|
||||||
|
let parsed: JsonValue = serde_json::from_str(&raw).unwrap_or(JsonValue::Array(vec![]));
|
||||||
|
return Ok(IpcResponse::success(normalize_board_configs(parsed)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let legacy = get_legacy_board_configs_from_settings(&state, Some(conn.clone())).await?;
|
||||||
|
if legacy.as_array().map(|v| !v.is_empty()).unwrap_or(false) {
|
||||||
|
let raw = serde_json::to_string(&legacy).map_err(|e| e.to_string())?;
|
||||||
|
upsert_board_configs_raw(&conn, &raw).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(IpcResponse::success(legacy))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn board_save_configs(
|
||||||
|
configs: JsonValue,
|
||||||
|
sender_id: Option<u32>,
|
||||||
|
state: State<'_, Arc<RwLock<AppState>>>,
|
||||||
|
) -> Result<IpcResponse<()>, String> {
|
||||||
|
if !check_admin_permission(&state, sender_id) {
|
||||||
|
return Ok(IpcResponse::error("Permission denied: admin required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let normalized = normalize_board_configs(configs);
|
||||||
|
let raw = serde_json::to_string(&normalized).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let db_conn = {
|
||||||
|
let state_guard = state.read();
|
||||||
|
let conn = state_guard.db.read().clone();
|
||||||
|
conn
|
||||||
|
};
|
||||||
|
let Some(conn) = db_conn else {
|
||||||
|
return Ok(IpcResponse::error("Database not connected"));
|
||||||
|
};
|
||||||
|
|
||||||
|
ensure_board_configs_table(&conn).await?;
|
||||||
|
upsert_board_configs_raw(&conn, &raw).await?;
|
||||||
|
|
||||||
|
Ok(IpcResponse::success_empty())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn board_query_sql(
|
pub async fn board_query_sql(
|
||||||
params: BoardSqlQueryParams,
|
params: BoardSqlQueryParams,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ impl Migration {
|
|||||||
Self::create_score_events_table(conn, is_sqlite).await?;
|
Self::create_score_events_table(conn, is_sqlite).await?;
|
||||||
Self::create_settlements_table(conn, is_sqlite).await?;
|
Self::create_settlements_table(conn, is_sqlite).await?;
|
||||||
Self::create_settings_table(conn, is_sqlite).await?;
|
Self::create_settings_table(conn, is_sqlite).await?;
|
||||||
|
Self::create_board_configs_table(conn, is_sqlite).await?;
|
||||||
Self::create_tags_table(conn, is_sqlite).await?;
|
Self::create_tags_table(conn, is_sqlite).await?;
|
||||||
Self::create_student_tags_table(conn, is_sqlite).await?;
|
Self::create_student_tags_table(conn, is_sqlite).await?;
|
||||||
Self::create_reward_settings_table(conn, is_sqlite).await?;
|
Self::create_reward_settings_table(conn, is_sqlite).await?;
|
||||||
@@ -77,6 +78,17 @@ impl Migration {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_board_configs_table(
|
||||||
|
conn: &impl ConnectionTrait,
|
||||||
|
sqlite: bool,
|
||||||
|
) -> Result<(), DbErr> {
|
||||||
|
let sql = get_create_board_configs_table_sql(sqlite);
|
||||||
|
conn.execute(Statement::from_string(Self::get_db_backend(sqlite), sql))
|
||||||
|
.await?;
|
||||||
|
info!("Created board_configs table");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_tags_table(conn: &impl ConnectionTrait, sqlite: bool) -> Result<(), DbErr> {
|
async fn create_tags_table(conn: &impl ConnectionTrait, sqlite: bool) -> Result<(), DbErr> {
|
||||||
let sql = get_create_tags_table_sql(sqlite);
|
let sql = get_create_tags_table_sql(sqlite);
|
||||||
conn.execute(Statement::from_string(Self::get_db_backend(sqlite), sql))
|
conn.execute(Statement::from_string(Self::get_db_backend(sqlite), sql))
|
||||||
@@ -315,6 +327,7 @@ impl Migration {
|
|||||||
TABLE_REWARD_REDEMPTIONS,
|
TABLE_REWARD_REDEMPTIONS,
|
||||||
TABLE_REWARD_SETTINGS,
|
TABLE_REWARD_SETTINGS,
|
||||||
TABLE_SETTINGS,
|
TABLE_SETTINGS,
|
||||||
|
TABLE_BOARD_CONFIGS,
|
||||||
];
|
];
|
||||||
|
|
||||||
let db_backend = Self::get_db_backend(sqlite);
|
let db_backend = Self::get_db_backend(sqlite);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ pub const TABLE_REASONS: &str = "reasons";
|
|||||||
pub const TABLE_SCORE_EVENTS: &str = "score_events";
|
pub const TABLE_SCORE_EVENTS: &str = "score_events";
|
||||||
pub const TABLE_SETTLEMENTS: &str = "settlements";
|
pub const TABLE_SETTLEMENTS: &str = "settlements";
|
||||||
pub const TABLE_SETTINGS: &str = "settings";
|
pub const TABLE_SETTINGS: &str = "settings";
|
||||||
|
pub const TABLE_BOARD_CONFIGS: &str = "board_configs";
|
||||||
pub const TABLE_TAGS: &str = "tags";
|
pub const TABLE_TAGS: &str = "tags";
|
||||||
pub const TABLE_STUDENT_TAGS: &str = "student_tags";
|
pub const TABLE_STUDENT_TAGS: &str = "student_tags";
|
||||||
pub const TABLE_REWARD_SETTINGS: &str = "reward_settings";
|
pub const TABLE_REWARD_SETTINGS: &str = "reward_settings";
|
||||||
@@ -57,6 +58,13 @@ pub mod settings {
|
|||||||
pub const VALUE: &str = "value";
|
pub const VALUE: &str = "value";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod board_configs {
|
||||||
|
pub const TABLE: &str = "board_configs";
|
||||||
|
pub const ID: &str = "id";
|
||||||
|
pub const CONFIG_JSON: &str = "config_json";
|
||||||
|
pub const UPDATED_AT: &str = "updated_at";
|
||||||
|
}
|
||||||
|
|
||||||
pub mod tags {
|
pub mod tags {
|
||||||
pub const TABLE: &str = "tags";
|
pub const TABLE: &str = "tags";
|
||||||
pub const ID: &str = "id";
|
pub const ID: &str = "id";
|
||||||
@@ -287,6 +295,28 @@ pub fn get_create_settings_table_sql(sqlite: bool) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_create_board_configs_table_sql(sqlite: bool) -> String {
|
||||||
|
if sqlite {
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS board_configs (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
config_json TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
)
|
||||||
|
"#
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS board_configs (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
config_json TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
)
|
||||||
|
"#
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_create_tags_table_sql(sqlite: bool) -> String {
|
pub fn get_create_tags_table_sql(sqlite: bool) -> String {
|
||||||
if sqlite {
|
if sqlite {
|
||||||
r#"
|
r#"
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ pub fn run() {
|
|||||||
auto_score_toggle_rule,
|
auto_score_toggle_rule,
|
||||||
auto_score_get_status,
|
auto_score_get_status,
|
||||||
auto_score_sort_rules,
|
auto_score_sort_rules,
|
||||||
|
board_get_configs,
|
||||||
|
board_save_configs,
|
||||||
board_query_sql,
|
board_query_sql,
|
||||||
log_query,
|
log_query,
|
||||||
log_clear,
|
log_clear,
|
||||||
|
|||||||
@@ -444,7 +444,7 @@ ORDER BY reward_points DESC, score DESC`,
|
|||||||
|
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
const res = await (window as any).api.setSetting("dashboards_config", nextBoards)
|
const res = await (window as any).api.boardSaveConfigs(nextBoards)
|
||||||
if (!res?.success) {
|
if (!res?.success) {
|
||||||
messageApi.error(res?.message || t("board.saveFailed"))
|
messageApi.error(res?.message || t("board.saveFailed"))
|
||||||
}
|
}
|
||||||
@@ -475,7 +475,7 @@ ORDER BY reward_points DESC, score DESC`,
|
|||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
try {
|
try {
|
||||||
const res = await (window as any).api.getSetting("dashboards_config")
|
const res = await (window as any).api.boardGetConfigs()
|
||||||
if (res?.success) {
|
if (res?.success) {
|
||||||
const normalized = normalizeBoards(res?.data)
|
const normalized = normalizeBoards(res?.data)
|
||||||
setBoards(normalized)
|
setBoards(normalized)
|
||||||
@@ -835,7 +835,14 @@ ORDER BY reward_points DESC, score DESC`,
|
|||||||
>
|
>
|
||||||
{rankBadge && (
|
{rankBadge && (
|
||||||
<div
|
<div
|
||||||
style={{ position: "absolute", top: "-10px", left: "-10px", fontSize: "24px" }}
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 10,
|
||||||
|
right: 10,
|
||||||
|
fontSize: "32px",
|
||||||
|
lineHeight: 1,
|
||||||
|
zIndex: 1,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{rankBadge}
|
{rankBadge}
|
||||||
</div>
|
</div>
|
||||||
@@ -1009,9 +1016,15 @@ ORDER BY reward_points DESC, score DESC`,
|
|||||||
title={<span style={{ fontWeight: 600 }}>{list.name}</span>}
|
title={<span style={{ fontWeight: 600 }}>{list.name}</span>}
|
||||||
extra={
|
extra={
|
||||||
<Space size={6}>
|
<Space size={6}>
|
||||||
<Button size="small" onClick={() => setEditingListId(list.id)} icon={<EditOutlined />}>
|
<Button
|
||||||
{t("board.editList")}
|
size="small"
|
||||||
</Button>
|
type="text"
|
||||||
|
shape="circle"
|
||||||
|
onClick={() => setEditingListId(list.id)}
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
aria-label={t("board.editList")}
|
||||||
|
title={t("board.editList")}
|
||||||
|
/>
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
title={t("board.removeListConfirm")}
|
title={t("board.removeListConfirm")}
|
||||||
onConfirm={() => removeList(board.id, list.id)}
|
onConfirm={() => removeList(board.id, list.id)}
|
||||||
@@ -1019,12 +1032,14 @@ ORDER BY reward_points DESC, score DESC`,
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
|
type="text"
|
||||||
|
shape="circle"
|
||||||
danger
|
danger
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
disabled={!canManage || board.lists.length <= 1}
|
disabled={!canManage || board.lists.length <= 1}
|
||||||
>
|
aria-label={t("board.removeList")}
|
||||||
{t("board.removeList")}
|
title={t("board.removeList")}
|
||||||
</Button>
|
/>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@@ -1292,6 +1307,16 @@ ORDER BY reward_points DESC, score DESC`,
|
|||||||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Typography.Link href="https://doubao.com/bot/uEh3mtxq" target="_blank" rel="noreferrer">
|
||||||
|
豆包智能体一句话生成查询
|
||||||
|
</Typography.Link>
|
||||||
|
<Typography.Link
|
||||||
|
href="https://www.coze.cn/store/agent/7620447018255384610?bot_id=true"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
扣子智能体一句话生成查询
|
||||||
|
</Typography.Link>
|
||||||
</Space>
|
</Space>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -155,6 +155,10 @@ const api = {
|
|||||||
limit?: number
|
limit?: number
|
||||||
}): Promise<{ success: boolean; data: any[]; message?: string }> =>
|
}): Promise<{ success: boolean; data: any[]; message?: string }> =>
|
||||||
invoke("board_query_sql", { params }),
|
invoke("board_query_sql", { params }),
|
||||||
|
boardGetConfigs: (): Promise<{ success: boolean; data: any[]; message?: string }> =>
|
||||||
|
invoke("board_get_configs"),
|
||||||
|
boardSaveConfigs: (configs: any[]): Promise<{ success: boolean; message?: string }> =>
|
||||||
|
invoke("board_save_configs", { configs }),
|
||||||
|
|
||||||
// Settlement
|
// Settlement
|
||||||
querySettlements: (): Promise<{ success: boolean; data: any[] }> => invoke("db_settlement_query"),
|
querySettlements: (): Promise<{ success: boolean; data: any[] }> => invoke("db_settlement_query"),
|
||||||
|
|||||||
Reference in New Issue
Block a user