feat: 新增班优浏览器登录导入

This commit is contained in:
JSR
2026-07-11 18:47:36 +08:00
parent ff6cb694d2
commit 3081bcf283
9 changed files with 345 additions and 20 deletions
+1
View File
@@ -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",
+29
View File
@@ -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:
+46
View File
@@ -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)
}
}
})()
+139
View File
@@ -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,
+1
View File
@@ -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,
+103 -16
View File
@@ -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)" }}>
+11 -4
View File
@@ -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}}",
+7
View File
@@ -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": "点击表头选择姓名列",
+8
View File
@@ -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<{