mirror of
https://github.com/SECTL/SecScore.git
synced 2026-07-19 06:04:22 +08:00
feat: 新增班优浏览器登录导入
This commit is contained in:
@@ -30,6 +30,7 @@
|
|||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"i18next": "^25.8.14",
|
"i18next": "^25.8.14",
|
||||||
"pinyin-pro": "^3.27.0",
|
"pinyin-pro": "^3.27.0",
|
||||||
|
"playwright": "^1.61.1",
|
||||||
"react": "^19.2.1",
|
"react": "^19.2.1",
|
||||||
"react-dom": "^19.2.1",
|
"react-dom": "^19.2.1",
|
||||||
"react-i18next": "^16.5.6",
|
"react-i18next": "^16.5.6",
|
||||||
|
|||||||
Generated
+29
@@ -38,6 +38,9 @@ importers:
|
|||||||
pinyin-pro:
|
pinyin-pro:
|
||||||
specifier: ^3.27.0
|
specifier: ^3.27.0
|
||||||
version: 3.28.0
|
version: 3.28.0
|
||||||
|
playwright:
|
||||||
|
specifier: ^1.61.1
|
||||||
|
version: 1.61.1
|
||||||
react:
|
react:
|
||||||
specifier: ^19.2.1
|
specifier: ^19.2.1
|
||||||
version: 19.2.4
|
version: 19.2.4
|
||||||
@@ -1594,6 +1597,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
|
resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==}
|
||||||
engines: {node: '>=0.8'}
|
engines: {node: '>=0.8'}
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
@@ -2012,6 +2020,16 @@ packages:
|
|||||||
pinyin-pro@3.28.0:
|
pinyin-pro@3.28.0:
|
||||||
resolution: {integrity: sha512-mMRty6RisoyYNphJrTo3pnvp3w8OMZBrXm9YSWkxhAfxKj1KZk2y8T2PDIZlDDRsvZ0No+Hz6FI4sZpA6Ey25g==}
|
resolution: {integrity: sha512-mMRty6RisoyYNphJrTo3pnvp3w8OMZBrXm9YSWkxhAfxKj1KZk2y8T2PDIZlDDRsvZ0No+Hz6FI4sZpA6Ey25g==}
|
||||||
|
|
||||||
|
playwright-core@1.61.1:
|
||||||
|
resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
playwright@1.61.1:
|
||||||
|
resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
possible-typed-array-names@1.1.0:
|
possible-typed-array-names@1.1.0:
|
||||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -4263,6 +4281,9 @@ snapshots:
|
|||||||
|
|
||||||
frac@1.1.2: {}
|
frac@1.1.2: {}
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -4680,6 +4701,14 @@ snapshots:
|
|||||||
|
|
||||||
pinyin-pro@3.28.0: {}
|
pinyin-pro@3.28.0: {}
|
||||||
|
|
||||||
|
playwright-core@1.61.1: {}
|
||||||
|
|
||||||
|
playwright@1.61.1:
|
||||||
|
dependencies:
|
||||||
|
playwright-core: 1.61.1
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.2
|
||||||
|
|
||||||
possible-typed-array-names@1.1.0: {}
|
possible-typed-array-names@1.1.0: {}
|
||||||
|
|
||||||
postcss@8.5.8:
|
postcss@8.5.8:
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
const { chromium } = require("playwright")
|
||||||
|
|
||||||
|
const LOGIN_URL = "https://care.seewo.com/"
|
||||||
|
const LOGIN_BUTTON_SELECTOR = "a#index-login-btn"
|
||||||
|
const USER_PANEL_SELECTOR = "span#user-info-panel"
|
||||||
|
const LOGIN_TIMEOUT_MS = 10 * 60 * 1000
|
||||||
|
|
||||||
|
const toCookieHeader = (cookies) => {
|
||||||
|
return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ")
|
||||||
|
}
|
||||||
|
|
||||||
|
;(async () => {
|
||||||
|
let browser
|
||||||
|
try {
|
||||||
|
browser = await chromium.launch({ headless: false })
|
||||||
|
const context = await browser.newContext()
|
||||||
|
const page = await context.newPage()
|
||||||
|
|
||||||
|
await page.goto(LOGIN_URL, { waitUntil: "domcontentloaded", timeout: 60 * 1000 })
|
||||||
|
await page.locator(LOGIN_BUTTON_SELECTOR).click({ timeout: 60 * 1000 })
|
||||||
|
await page.locator(USER_PANEL_SELECTOR).waitFor({
|
||||||
|
state: "visible",
|
||||||
|
timeout: LOGIN_TIMEOUT_MS,
|
||||||
|
})
|
||||||
|
|
||||||
|
const cookies = await context.cookies(LOGIN_URL)
|
||||||
|
const cookieHeader = toCookieHeader(cookies)
|
||||||
|
if (!cookieHeader) {
|
||||||
|
throw new Error("No care.seewo.com cookies were captured after login")
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(
|
||||||
|
JSON.stringify({
|
||||||
|
cookie: cookieHeader,
|
||||||
|
count: cookies.length,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
process.stderr.write(error && error.stack ? error.stack : String(error))
|
||||||
|
process.exitCode = 1
|
||||||
|
} finally {
|
||||||
|
if (browser) {
|
||||||
|
await browser.close().catch(() => undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
@@ -4,8 +4,11 @@ use sea_orm::{
|
|||||||
};
|
};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
|
use tokio::process::Command;
|
||||||
|
use tokio::time::{timeout, Duration};
|
||||||
|
|
||||||
use crate::db::entities::students;
|
use crate::db::entities::students;
|
||||||
use crate::models::{StudentUpdate, StudentWithTags};
|
use crate::models::{StudentUpdate, StudentWithTags};
|
||||||
@@ -40,6 +43,13 @@ pub struct FetchBanYouClassroomDetailParams {
|
|||||||
pub team_plan_id: Option<i64>,
|
pub team_plan_id: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct BanYouBrowserCookieData {
|
||||||
|
pub cookie: String,
|
||||||
|
pub count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct BanYouClassroom {
|
pub struct BanYouClassroom {
|
||||||
@@ -239,6 +249,135 @@ fn extract_csrf_token_from_html(html: &str) -> Option<String> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_node_project_dir() -> Option<PathBuf> {
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
if let Ok(current_dir) = std::env::current_dir() {
|
||||||
|
candidates.push(current_dir);
|
||||||
|
}
|
||||||
|
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
candidates.push(manifest_dir.clone());
|
||||||
|
if let Some(parent) = manifest_dir.parent() {
|
||||||
|
candidates.push(parent.to_path_buf());
|
||||||
|
}
|
||||||
|
|
||||||
|
for candidate in candidates {
|
||||||
|
for dir in candidate.ancestors() {
|
||||||
|
if dir.join("package.json").is_file()
|
||||||
|
&& dir
|
||||||
|
.join("scripts")
|
||||||
|
.join("banyou-login-playwright.cjs")
|
||||||
|
.is_file()
|
||||||
|
{
|
||||||
|
return Some(dir.to_path_buf());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn node_executable() -> &'static str {
|
||||||
|
if cfg!(windows) {
|
||||||
|
"node.exe"
|
||||||
|
} else {
|
||||||
|
"node"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_banyou_playwright_script(
|
||||||
|
project_dir: &Path,
|
||||||
|
) -> Result<BanYouBrowserCookieData, String> {
|
||||||
|
let script_path = project_dir
|
||||||
|
.join("scripts")
|
||||||
|
.join("banyou-login-playwright.cjs");
|
||||||
|
let output = timeout(
|
||||||
|
Duration::from_secs(10 * 60),
|
||||||
|
Command::new(node_executable())
|
||||||
|
.arg(script_path)
|
||||||
|
.current_dir(project_dir)
|
||||||
|
.output(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|_| "BanYou browser login timed out".to_string())?
|
||||||
|
.map_err(|e| format!("Failed to start Node.js for Playwright: {}", e))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
|
let message = if stderr.is_empty() {
|
||||||
|
"Playwright browser login failed".to_string()
|
||||||
|
} else {
|
||||||
|
stderr
|
||||||
|
};
|
||||||
|
return Err(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
|
if stdout.is_empty() {
|
||||||
|
return Err("Playwright did not return a Cookie".to_string());
|
||||||
|
}
|
||||||
|
serde_json::from_str::<BanYouBrowserCookieData>(&stdout)
|
||||||
|
.map_err(|e| format!("Failed to parse Playwright Cookie output: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn student_fetch_banyou_cookie_with_browser(
|
||||||
|
state: State<'_, Arc<RwLock<AppState>>>,
|
||||||
|
sender_id: Option<u32>,
|
||||||
|
) -> Result<IpcResponse<BanYouBrowserCookieData>, String> {
|
||||||
|
if !check_admin_permission(&state, sender_id) {
|
||||||
|
return Ok(IpcResponse::error("Permission denied: admin required"));
|
||||||
|
}
|
||||||
|
|
||||||
|
log_banyou(
|
||||||
|
state.inner(),
|
||||||
|
LogLevel::Info,
|
||||||
|
"browser cookie fetch started",
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let Some(project_dir) = find_node_project_dir() else {
|
||||||
|
log_banyou(
|
||||||
|
state.inner(),
|
||||||
|
LogLevel::Error,
|
||||||
|
"failed to locate project directory for playwright script",
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
return Ok(IpcResponse::error(
|
||||||
|
"Cannot find Playwright login script in project directory",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
match run_banyou_playwright_script(&project_dir).await {
|
||||||
|
Ok(data) => {
|
||||||
|
if data.cookie.trim().is_empty() {
|
||||||
|
return Ok(IpcResponse::error("Cookie cannot be empty"));
|
||||||
|
}
|
||||||
|
log_banyou(
|
||||||
|
state.inner(),
|
||||||
|
LogLevel::Info,
|
||||||
|
"browser cookie fetch succeeded",
|
||||||
|
Some(serde_json::json!({
|
||||||
|
"cookie_length": data.cookie.len(),
|
||||||
|
"cookie_count": data.count,
|
||||||
|
"contains_uid": data.cookie.contains("uid="),
|
||||||
|
"contains_access_token": data.cookie.contains("accessToken="),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
Ok(IpcResponse::success(data))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log_banyou(
|
||||||
|
state.inner(),
|
||||||
|
LogLevel::Error,
|
||||||
|
"browser cookie fetch failed",
|
||||||
|
Some(serde_json::json!({ "error": err })),
|
||||||
|
);
|
||||||
|
Ok(IpcResponse::error(
|
||||||
|
"通过浏览器登录班优失败,请重试或改用手动输入 Cookie",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_banyou_client_and_csrf(
|
async fn get_banyou_client_and_csrf(
|
||||||
state: &Arc<RwLock<AppState>>,
|
state: &Arc<RwLock<AppState>>,
|
||||||
cookie: &str,
|
cookie: &str,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pub fn run() {
|
|||||||
student_update,
|
student_update,
|
||||||
student_delete,
|
student_delete,
|
||||||
student_import_from_xlsx,
|
student_import_from_xlsx,
|
||||||
|
student_fetch_banyou_cookie_with_browser,
|
||||||
student_fetch_banyou_classrooms,
|
student_fetch_banyou_classrooms,
|
||||||
student_fetch_banyou_classroom_detail,
|
student_fetch_banyou_classroom_detail,
|
||||||
tags_get_all,
|
tags_get_all,
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
} from "antd"
|
} from "antd"
|
||||||
import type { ColumnsType } from "antd/es/table"
|
import type { ColumnsType } from "antd/es/table"
|
||||||
import { UploadOutlined, MoreOutlined, PlusOutlined, CopyOutlined } from "@ant-design/icons"
|
import {
|
||||||
|
UploadOutlined,
|
||||||
|
MoreOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
CopyOutlined,
|
||||||
|
LoginOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
} from "@ant-design/icons"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { TagEditorDialog } from "./TagEditorDialog"
|
import { TagEditorDialog } from "./TagEditorDialog"
|
||||||
import { getAvatarFromExtraJson, setAvatarInExtraJson } from "../utils/studentAvatar"
|
import { getAvatarFromExtraJson, setAvatarInExtraJson } from "../utils/studentAvatar"
|
||||||
@@ -94,7 +101,9 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
const [visible, setVisible] = useState(false)
|
const [visible, setVisible] = useState(false)
|
||||||
const [textImportVisible, setTextImportVisible] = useState(false)
|
const [textImportVisible, setTextImportVisible] = useState(false)
|
||||||
const [xlsxVisible, setXlsxVisible] = useState(false)
|
const [xlsxVisible, setXlsxVisible] = useState(false)
|
||||||
|
const [banYouLoginMethodVisible, setBanYouLoginMethodVisible] = useState(false)
|
||||||
const [banYouVisible, setBanYouVisible] = useState(false)
|
const [banYouVisible, setBanYouVisible] = useState(false)
|
||||||
|
const [banYouImportMode, setBanYouImportMode] = useState<"browser" | "manual">("manual")
|
||||||
const [tagEditVisible, setTagEditVisible] = useState(false)
|
const [tagEditVisible, setTagEditVisible] = useState(false)
|
||||||
const [editingStudent, setEditingStudent] = useState<student | null>(null)
|
const [editingStudent, setEditingStudent] = useState<student | null>(null)
|
||||||
const [groupEditVisible, setGroupEditVisible] = useState(false)
|
const [groupEditVisible, setGroupEditVisible] = useState(false)
|
||||||
@@ -135,6 +144,7 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
const [xlsxSelectedCol, setXlsxSelectedCol] = useState<number | null>(null)
|
const [xlsxSelectedCol, setXlsxSelectedCol] = useState<number | null>(null)
|
||||||
const [textImportValue, setTextImportValue] = useState("")
|
const [textImportValue, setTextImportValue] = useState("")
|
||||||
const [banYouCookie, setBanYouCookie] = useState("")
|
const [banYouCookie, setBanYouCookie] = useState("")
|
||||||
|
const [banYouBrowserLoginLoading, setBanYouBrowserLoginLoading] = useState(false)
|
||||||
const [banYouLoading, setBanYouLoading] = useState(false)
|
const [banYouLoading, setBanYouLoading] = useState(false)
|
||||||
const [banYouDetailLoading, setBanYouDetailLoading] = useState(false)
|
const [banYouDetailLoading, setBanYouDetailLoading] = useState(false)
|
||||||
const [banYouImportLoading, setBanYouImportLoading] = useState(false)
|
const [banYouImportLoading, setBanYouImportLoading] = useState(false)
|
||||||
@@ -1002,15 +1012,21 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenBanYouImport = () => {
|
const handleOpenBanYouImport = () => {
|
||||||
|
setBanYouLoginMethodVisible(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpenManualBanYouImport = () => {
|
||||||
|
setBanYouImportMode("manual")
|
||||||
|
setBanYouLoginMethodVisible(false)
|
||||||
setBanYouVisible(true)
|
setBanYouVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFetchBanYouClassrooms = async () => {
|
const handleFetchBanYouClassrooms = async (cookieOverride?: string) => {
|
||||||
if (!canEdit) {
|
if (!canEdit) {
|
||||||
messageApi.error(t("common.readOnly"))
|
messageApi.error(t("common.readOnly"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const cookie = banYouCookie.trim()
|
const cookie = (cookieOverride ?? banYouCookie).trim()
|
||||||
if (!cookie) {
|
if (!cookie) {
|
||||||
messageApi.warning(t("students.banyouCookieRequired"))
|
messageApi.warning(t("students.banyouCookieRequired"))
|
||||||
return
|
return
|
||||||
@@ -1046,6 +1062,38 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleBanYouBrowserLogin = async () => {
|
||||||
|
if (!canEdit) {
|
||||||
|
messageApi.error(t("common.readOnly"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!(window as any).api?.fetchBanYouCookieWithBrowser) {
|
||||||
|
messageApi.error(t("students.banyouBrowserLoginFailed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setBanYouBrowserLoginLoading(true)
|
||||||
|
try {
|
||||||
|
messageApi.info(t("students.banyouBrowserLoginWaiting"))
|
||||||
|
const res = await (window as any).api.fetchBanYouCookieWithBrowser()
|
||||||
|
const cookie = String(res?.data?.cookie ?? "").trim()
|
||||||
|
if (!res?.success || !cookie) {
|
||||||
|
messageApi.error(res?.message || t("students.banyouBrowserLoginFailed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setBanYouCookie(cookie)
|
||||||
|
setBanYouImportMode("browser")
|
||||||
|
setBanYouLoginMethodVisible(false)
|
||||||
|
setBanYouVisible(true)
|
||||||
|
messageApi.success(t("students.banyouBrowserCookieSuccess"))
|
||||||
|
await handleFetchBanYouClassrooms(cookie)
|
||||||
|
} catch (e: any) {
|
||||||
|
messageApi.error(e?.message || t("students.banyouBrowserLoginFailed"))
|
||||||
|
} finally {
|
||||||
|
setBanYouBrowserLoginLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const medalKey = (item: BanYouMedal, idx: number) => item.key || item.uid || `${item.name}-${idx}`
|
const medalKey = (item: BanYouMedal, idx: number) => item.key || item.uid || `${item.name}-${idx}`
|
||||||
|
|
||||||
const fetchBanYouClassDetail = async (classroom: BanYouClassroom, teamPlanId?: number) => {
|
const fetchBanYouClassDetail = async (classroom: BanYouClassroom, teamPlanId?: number) => {
|
||||||
@@ -1804,6 +1852,37 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={t("students.banyouLoginMethodTitle")}
|
||||||
|
open={banYouLoginMethodVisible}
|
||||||
|
onCancel={() => setBanYouLoginMethodVisible(false)}
|
||||||
|
footer={null}
|
||||||
|
destroyOnHidden
|
||||||
|
>
|
||||||
|
<Space orientation="vertical" style={{ width: "100%" }} size={12}>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
type="primary"
|
||||||
|
icon={<LoginOutlined />}
|
||||||
|
loading={banYouBrowserLoginLoading}
|
||||||
|
onClick={handleBanYouBrowserLogin}
|
||||||
|
>
|
||||||
|
{t("students.banyouBrowserLogin")}
|
||||||
|
</Button>
|
||||||
|
<div style={{ color: "var(--ss-text-secondary)", fontSize: 12 }}>
|
||||||
|
{t("students.banyouBrowserLoginHint")}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
icon={<EditOutlined />}
|
||||||
|
disabled={banYouBrowserLoginLoading}
|
||||||
|
onClick={handleOpenManualBanYouImport}
|
||||||
|
>
|
||||||
|
{t("students.banyouManualCookie")}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={t("students.importByBanyou")}
|
title={t("students.importByBanyou")}
|
||||||
open={banYouVisible}
|
open={banYouVisible}
|
||||||
@@ -1813,19 +1892,27 @@ export const StudentManager: React.FC<{ canEdit: boolean }> = ({ canEdit }) => {
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
>
|
>
|
||||||
<Space orientation="vertical" style={{ width: "100%" }} size={12}>
|
<Space orientation="vertical" style={{ width: "100%" }} size={12}>
|
||||||
<div style={{ color: "var(--ss-text-secondary)", fontSize: 12 }}>
|
{banYouImportMode === "manual" ? (
|
||||||
{t("students.banyouCookieHint")}
|
<>
|
||||||
</div>
|
<div style={{ color: "var(--ss-text-secondary)", fontSize: 12 }}>
|
||||||
<Input.TextArea
|
{t("students.banyouCookieHint")}
|
||||||
value={banYouCookie}
|
</div>
|
||||||
onChange={(e) => setBanYouCookie(e.target.value)}
|
<Input.TextArea
|
||||||
rows={4}
|
value={banYouCookie}
|
||||||
placeholder={t("students.banyouCookiePlaceholder")}
|
onChange={(e) => setBanYouCookie(e.target.value)}
|
||||||
disabled={banYouLoading}
|
rows={4}
|
||||||
/>
|
placeholder={t("students.banyouCookiePlaceholder")}
|
||||||
<Button type="primary" loading={banYouLoading} onClick={handleFetchBanYouClassrooms}>
|
disabled={banYouLoading}
|
||||||
{t("students.banyouFetch")}
|
/>
|
||||||
</Button>
|
<Button
|
||||||
|
type="primary"
|
||||||
|
loading={banYouLoading}
|
||||||
|
onClick={() => handleFetchBanYouClassrooms()}
|
||||||
|
>
|
||||||
|
{t("students.banyouFetch")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<div style={{ marginTop: 8 }}>
|
<div style={{ marginTop: 8 }}>
|
||||||
<h3 style={{ margin: "0 0 12px", color: "var(--ss-text-main)" }}>
|
<h3 style={{ margin: "0 0 12px", color: "var(--ss-text-main)" }}>
|
||||||
|
|||||||
@@ -572,7 +572,14 @@
|
|||||||
"importTextPlaceholder": "Example:\nAlice\nBob\nCharlie",
|
"importTextPlaceholder": "Example:\nAlice\nBob\nCharlie",
|
||||||
"importByText": "Import from Text",
|
"importByText": "Import from Text",
|
||||||
"importByXlsx": "Import via xlsx",
|
"importByXlsx": "Import via xlsx",
|
||||||
"importByBanyou": "Import from BanYou",
|
"importByBanyou": "Import from Easicare",
|
||||||
|
"banyouLoginMethodTitle": "Choose Easicare Login Method",
|
||||||
|
"banyouBrowserLogin": "Login to Easicare in Browser (Recommended)",
|
||||||
|
"banyouBrowserLoginHint": "A Playwright browser window will open. Complete login there, then SecScore will capture the Cookie and close the browser automatically.",
|
||||||
|
"banyouManualCookie": "Enter Cookie Manually",
|
||||||
|
"banyouBrowserLoginWaiting": "Browser opened. Please complete Easicare login.",
|
||||||
|
"banyouBrowserCookieSuccess": "Easicare Cookie captured",
|
||||||
|
"banyouBrowserLoginFailed": "Failed to login to Easicare in browser",
|
||||||
"xlsxPreview": "XLSX Preview & Import",
|
"xlsxPreview": "XLSX Preview & Import",
|
||||||
"file": "File",
|
"file": "File",
|
||||||
"selectNameCol": "Click header to select name column",
|
"selectNameCol": "Click header to select name column",
|
||||||
@@ -584,12 +591,12 @@
|
|||||||
"selectNameColFirst": "Please select \"Name Column\" first",
|
"selectNameColFirst": "Please select \"Name Column\" first",
|
||||||
"noNamesFound": "No importable names found in selected column",
|
"noNamesFound": "No importable names found in selected column",
|
||||||
"importFailed": "Import failed",
|
"importFailed": "Import failed",
|
||||||
"banyouCookieHint": "After signing in to BanYou Web, copy the full Cookie and paste it below.",
|
"banyouCookieHint": "After signing in to Easicare Web, copy the full Cookie and paste it below.",
|
||||||
"banyouCookiePlaceholder": "uid=...; accessToken=...; ...",
|
"banyouCookiePlaceholder": "uid=...; accessToken=...; ...",
|
||||||
"banyouCookieRequired": "Please paste Cookie first",
|
"banyouCookieRequired": "Please paste Cookie first",
|
||||||
"banyouFetch": "Fetch Classrooms",
|
"banyouFetch": "Fetch Classrooms",
|
||||||
"banyouFetchSuccess": "Fetched {{count}} classrooms",
|
"banyouFetchSuccess": "Fetched {{count}} classrooms",
|
||||||
"banyouFetchFailed": "Failed to fetch BanYou classrooms",
|
"banyouFetchFailed": "Failed to fetch Easicare classrooms",
|
||||||
"banyouCreatedClasses": "Classes I Created",
|
"banyouCreatedClasses": "Classes I Created",
|
||||||
"banyouJoinedClasses": "Classes I Joined",
|
"banyouJoinedClasses": "Classes I Joined",
|
||||||
"banyouNoClasses": "No classroom data",
|
"banyouNoClasses": "No classroom data",
|
||||||
@@ -609,7 +616,7 @@
|
|||||||
"banyouSelectTeamPlanFirst": "Please select a group plan first",
|
"banyouSelectTeamPlanFirst": "Please select a group plan first",
|
||||||
"banyouImportSelected": "Import Selected",
|
"banyouImportSelected": "Import Selected",
|
||||||
"banyouNothingSelected": "Please select at least one item to import",
|
"banyouNothingSelected": "Please select at least one item to import",
|
||||||
"banyouImportSummary": "BanYou import done: Students +{{studentsInserted}}/{{studentsSkipped}} skipped; Reasons +{{reasonsInserted}}/{{reasonsSkipped}} skipped; Groups updated {{groupsUpdated}}/{{groupsSkipped}} skipped",
|
"banyouImportSummary": "Easicare import done: Students +{{studentsInserted}}/{{studentsSkipped}} skipped; Reasons +{{reasonsInserted}}/{{reasonsSkipped}} skipped; Groups updated {{groupsUpdated}}/{{groupsSkipped}} skipped",
|
||||||
"editTagTitle": "Edit Tags - {{name}}",
|
"editTagTitle": "Edit Tags - {{name}}",
|
||||||
"editGroup": "Set Group",
|
"editGroup": "Set Group",
|
||||||
"editGroupTitle": "Set Group - {{name}}",
|
"editGroupTitle": "Set Group - {{name}}",
|
||||||
|
|||||||
@@ -573,6 +573,13 @@
|
|||||||
"importByText": "从文本导入",
|
"importByText": "从文本导入",
|
||||||
"importByXlsx": "通过 xlsx 导入",
|
"importByXlsx": "通过 xlsx 导入",
|
||||||
"importByBanyou": "从班优导入",
|
"importByBanyou": "从班优导入",
|
||||||
|
"banyouLoginMethodTitle": "选择班优登录方式",
|
||||||
|
"banyouBrowserLogin": "通过浏览器登录班优(推荐)",
|
||||||
|
"banyouBrowserLoginHint": "会打开一个 Playwright 浏览器窗口,请在窗口中完成登录;登录成功后会自动抓取 Cookie 并关闭浏览器。",
|
||||||
|
"banyouManualCookie": "手动输入 Cookie",
|
||||||
|
"banyouBrowserLoginWaiting": "已打开浏览器,请完成班优登录",
|
||||||
|
"banyouBrowserCookieSuccess": "已获取班优 Cookie",
|
||||||
|
"banyouBrowserLoginFailed": "通过浏览器登录班优失败",
|
||||||
"xlsxPreview": "xlsx 预览与导入",
|
"xlsxPreview": "xlsx 预览与导入",
|
||||||
"file": "文件",
|
"file": "文件",
|
||||||
"selectNameCol": "点击表头选择姓名列",
|
"selectNameCol": "点击表头选择姓名列",
|
||||||
|
|||||||
@@ -173,6 +173,14 @@ const api = {
|
|||||||
names: string[]
|
names: string[]
|
||||||
}): Promise<{ success: boolean; data: { inserted: number; skipped: number; total: number } }> =>
|
}): Promise<{ success: boolean; data: { inserted: number; skipped: number; total: number } }> =>
|
||||||
invoke("student_import_from_xlsx", { params }),
|
invoke("student_import_from_xlsx", { params }),
|
||||||
|
fetchBanYouCookieWithBrowser: (): Promise<{
|
||||||
|
success: boolean
|
||||||
|
data?: {
|
||||||
|
cookie: string
|
||||||
|
count: number
|
||||||
|
}
|
||||||
|
message?: string
|
||||||
|
}> => invoke("student_fetch_banyou_cookie_with_browser"),
|
||||||
fetchBanYouClassrooms: (params: {
|
fetchBanYouClassrooms: (params: {
|
||||||
cookie: string
|
cookie: string
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
|
|||||||
Reference in New Issue
Block a user