重构客户端登录页并更新品牌图标

This commit is contained in:
baiyanyun
2026-06-30 15:09:46 +08:00
parent 44fe160880
commit 5d87c9eee7
16 changed files with 607 additions and 155 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "@qiming-ai/qimingclaw",
"version": "0.9.7",
"description": "启明数字员工",
"description": "陇电数字员工",
"main": "dist/main/main.js",
"author": "Qiming Team",
"license": "MIT",
@@ -128,7 +128,7 @@
},
"build": {
"appId": "com.qiming.qimingclaw",
"productName": "启明数字员工",
"productName": "陇电数字员工",
"afterSign": "scripts/build/after-sign.js",
"electronLanguages": [
"en",
@@ -297,8 +297,8 @@
"hardenedRuntime": true,
"extendInfo": {
"CFBundleIdentifier": "com.qiming.agent",
"CFBundleName": "启明数字员工",
"CFBundleDisplayName": "启明数字员工",
"CFBundleName": "陇电数字员工",
"CFBundleDisplayName": "陇电数字员工",
"NSDesktopFolderUsageDescription": "需要访问桌面文件夹以支持文件操作",
"NSDocumentsFolderUsageDescription": "需要访问文档文件夹以支持文件操作",
"NSDownloadsFolderUsageDescription": "需要访问下载文件夹以支持文件操作"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 672 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -39,6 +39,7 @@ import {
syncConfigToServer,
normalizeServerHost,
loginAndRegister,
isLoggedIn as checkIsLoggedIn,
} from "./services/core/auth";
import {
APP_DISPLAY_NAME,
@@ -49,6 +50,7 @@ import type { QuickInitConfig } from "@shared/types/quickInit";
import type { UpdateState } from "@shared/types/updateTypes";
import { t, getCurrentLang } from "./services/core/i18n";
import SetupWizard from "./components/setup/SetupWizard";
import LoginPage from "./components/setup/LoginPage";
import SetupDependencies from "./components/setup/SetupDependencies";
import ClientPage from "./components/pages/ClientPage";
import SettingsPage from "./components/pages/SettingsPage";
@@ -187,6 +189,9 @@ function App() {
// 初始化向导状态
// ============================================
const [isSetupComplete, setIsSetupComplete] = useState<boolean | null>(null);
const [isLoggedIn, setIsLoggedIn] = useState<boolean | null>(null);
const [loginStep1Config, setLoginStep1Config] =
useState<Step1Config>(DEFAULT_STEP1_CONFIG);
const setupJustCompleted = useRef(false);
// 内存变量:标记服务是否由登录流程启动(不持久化)
const loginStartedRef = useRef(false);
@@ -392,6 +397,10 @@ function App() {
const checkSetup = async () => {
try {
const completed = await setupService.isSetupCompleted();
const step1 = await setupService.getStep1Config();
setLoginStep1Config(step1);
const loggedIn = await checkIsLoggedIn();
setIsLoggedIn(loggedIn);
// 每次启动优先读取 quick init 配置
if (completed) {
@@ -411,6 +420,7 @@ function App() {
} catch (error) {
log.error("Failed to check setup status:", error);
setIsSetupComplete(true);
setIsLoggedIn(false);
}
};
checkSetup();
@@ -445,6 +455,8 @@ function App() {
// ============================================
const handleAuthChange = useCallback(async () => {
const user = await authService.getAuthUser();
const loggedIn = await checkIsLoggedIn();
setIsLoggedIn(loggedIn);
if (user) {
setUsername(
user.displayName || user.username || t("Claw.App.defaultUsername"),
@@ -459,6 +471,29 @@ function App() {
loginStartedRef.current = true;
}, []);
const ensureDefaultLoginConfig = useCallback(async () => {
const currentConfig = await setupService.getStep1Config();
const nextConfig: Step1Config = {
...DEFAULT_STEP1_CONFIG,
...currentConfig,
serverHost: currentConfig.serverHost || DEFAULT_STEP1_CONFIG.serverHost,
workspaceDir:
currentConfig.workspaceDir || DEFAULT_STEP1_CONFIG.workspaceDir,
};
await setupService.saveStep1Config(nextConfig);
setLoginStep1Config(nextConfig);
return nextConfig;
}, []);
const handleFullscreenLoginSuccess = useCallback(async () => {
await setupService.completeStep2();
await setupService.completeSetup();
setupJustCompleted.current = true;
setIsSetupComplete(true);
await handleAuthChange();
setAuthRefreshTrigger((v) => v + 1);
}, [handleAuthChange]);
// ============================================
// 主界面下必需依赖检查:仅当存在「未安装」或「错误」时进入依赖安装
// 版本以当前真实安装为准outdated 不触发(用户可在依赖 Tab 手动升级)
@@ -1106,7 +1141,8 @@ function App() {
// ============================================
if (
isSetupComplete === null ||
(isSetupComplete && needsRequiredDepsReinstall === null)
isLoggedIn === null ||
(isLoggedIn && isSetupComplete && needsRequiredDepsReinstall === null)
) {
return (
<I18nContext.Provider value={i18nContextValue}>
@@ -1120,6 +1156,20 @@ function App() {
);
}
if (!isLoggedIn) {
return (
<I18nContext.Provider value={i18nContextValue}>
<ConfigProvider theme={currentTheme}>
<LoginPage
step1Config={loginStep1Config}
onLoginStart={ensureDefaultLoginConfig}
onLoginSuccess={handleFullscreenLoginSuccess}
/>
</ConfigProvider>
</I18nContext.Provider>
);
}
// ============================================
// 渲染:初始化向导
// ============================================

View File

@@ -0,0 +1,306 @@
.page {
min-height: 100vh;
width: 100%;
display: grid;
grid-template-columns: minmax(360px, 36.1vw) 1fr;
overflow: hidden;
background: #f4f4f1;
color: #171412;
}
.brandPanel {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: #1a1714;
color: #ffffff;
}
.brandContent {
width: 280px;
margin-top: 10px;
text-align: center;
}
.brandIcon {
position: relative;
width: 72px;
height: 72px;
margin: 0 auto 24px;
border-radius: 16px;
background: linear-gradient(135deg, #00a960 0%, #007d73 100%);
}
.brandIconHead {
position: absolute;
top: 25px;
left: 32px;
width: 8px;
height: 8px;
border: 2px solid #ffffff;
border-radius: 50%;
}
.brandIconBody {
position: absolute;
top: 38px;
left: 24px;
width: 24px;
height: 18px;
border: 2px solid #ffffff;
border-bottom: 0;
border-radius: 14px 14px 0 0;
}
.brandTitle {
margin-top: 0;
color: #ffffff;
font-size: 29px;
line-height: 40px;
font-weight: 700;
letter-spacing: 0;
}
.brandSubtitle {
margin-top: 20px;
color: rgba(255, 255, 255, 0.55);
font-size: 14px;
line-height: 20px;
letter-spacing: 8px;
text-indent: 8px;
}
.brandSlogan {
margin-top: 24px;
color: rgba(255, 255, 255, 0.74);
font-size: 15px;
line-height: 22px;
letter-spacing: 0;
}
.brandCopyright {
margin-top: 24px;
color: rgba(255, 255, 255, 0.38);
font-size: 12px;
line-height: 18px;
letter-spacing: 0;
}
.loginPanel {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 48px;
}
.card {
position: relative;
width: 420px;
height: 636px;
display: flex;
flex-direction: column;
background: #ffffff;
border: 1px solid #ededeb;
border-radius: 16px;
box-shadow: 0 18px 40px rgba(26, 23, 20, 0.08);
padding: 46px 48px 46px;
}
.header {
margin-bottom: 24px;
}
.title {
margin: 0;
color: #171412;
font-size: 28px;
line-height: 40px;
font-weight: 700;
letter-spacing: 0;
}
.subtitle {
margin: 20px 0 0;
color: #8c8883;
font-size: 14px;
line-height: 20px;
letter-spacing: 0;
}
.form {
display: flex;
flex-direction: column;
gap: 22px;
}
.field {
display: flex;
flex-direction: column;
gap: 8px;
}
.label {
color: #171412;
font-size: 14px;
line-height: 20px;
letter-spacing: 0;
}
.input,
.input:global(.ant-input-affix-wrapper) {
width: 100%;
}
.input:global(.ant-input-affix-wrapper) {
height: 48px;
padding: 0 13px;
border: 1px solid #d7d3ce;
border-radius: 8px;
background: #fdfdfb;
box-shadow: none;
}
.input:global(.ant-input-affix-wrapper) :global(.ant-input) {
height: 46px;
color: #171412;
background: transparent;
font-size: 14px;
line-height: 46px;
}
.input:global(.ant-input-affix-wrapper) :global(.ant-input-prefix) {
margin-inline-end: 13px;
color: #8c8883;
font-size: 16px;
}
.input:global(.ant-input-affix-wrapper) :global(.ant-input-suffix) {
color: #8c8883;
font-size: 15px;
}
.input:global(.ant-input-affix-wrapper) :global(.ant-input-clear-icon) {
color: #c8c5bf;
font-size: 12px;
}
.input:global(.ant-input-affix-wrapper) :global(.ant-input::placeholder),
.input:global(.ant-input-affix-wrapper)
:global(.ant-input-password input::placeholder) {
color: #b7b3ad;
font-size: 14px;
}
.input:global(.ant-input-affix-wrapper-focused),
.input:global(.ant-input-affix-wrapper:hover) {
border-color: #00846d;
box-shadow: none;
}
.error {
height: 0;
margin-top: -16px;
color: #dc2626;
font-size: 12px;
line-height: 16px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.loginButton {
width: 100%;
height: 48px;
margin-top: 2px;
border: 0;
border-radius: 8px;
background: linear-gradient(90deg, #00a455 0%, #007d73 100%);
color: #ffffff;
font-size: 16px;
font-weight: 600;
letter-spacing: 0;
box-shadow: 0 9px 16px rgba(0, 160, 88, 0.24);
}
.loginButton:hover,
.loginButton:focus {
background: linear-gradient(90deg, #00ae5d 0%, #00887c 100%) !important;
color: #ffffff !important;
}
.loginButton:active {
background: linear-gradient(90deg, #00964e 0%, #006e66 100%) !important;
}
.hint {
width: 100%;
margin: 24px 0 0;
color: #006f68;
font-size: 14px;
line-height: 20px;
font-weight: 600;
letter-spacing: 0;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
}
.hintIcon {
width: 11px;
height: 11px;
border: 1.5px solid currentColor;
border-radius: 2px;
}
.hintArrow {
font-size: 18px;
line-height: 16px;
}
.divider {
width: 100%;
height: 1px;
margin-top: 25px;
background: #e7e1dc;
}
.footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: 25px;
color: #aaa5a0;
font-size: 12px;
line-height: 18px;
}
@media (max-width: 900px) {
.page {
grid-template-columns: 1fr;
}
.brandPanel {
display: none;
}
.loginPanel {
padding: 24px;
}
}
@media (max-height: 720px), (max-width: 520px) {
.loginPanel {
align-items: flex-start;
overflow: auto;
}
.card {
width: min(420px, calc(100vw - 48px));
height: auto;
min-height: 636px;
}
}

View File

@@ -0,0 +1,185 @@
import React, { useEffect, useMemo, useState } from "react";
import { Button, Input, message } from "antd";
import {
GlobalOutlined,
LockOutlined,
UserOutlined,
} from "@ant-design/icons";
import type { Step1Config } from "../../services/core/setup";
import {
getAuthErrorMessage,
loginAndRegister,
} from "../../services/core/auth";
import { t } from "../../services/core/i18n";
import styles from "./LoginPage.module.css";
interface LoginPageProps {
step1Config: Step1Config;
initialUsername?: string;
onLoginStart?: () => Promise<void>;
onLoginSuccess: () => Promise<void>;
}
function LoginPage({
step1Config,
initialUsername = "",
onLoginStart,
onLoginSuccess,
}: LoginPageProps) {
const [domain, setDomain] = useState(step1Config.serverHost || "");
const [username, setUsername] = useState(initialUsername);
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [retryCooldown, setRetryCooldown] = useState(0);
useEffect(() => {
setDomain(step1Config.serverHost || "");
}, [step1Config.serverHost]);
useEffect(() => {
setUsername(initialUsername);
}, [initialUsername]);
useEffect(() => {
if (retryCooldown <= 0) return;
const timer = window.setInterval(() => {
setRetryCooldown((prev) => Math.max(0, prev - 1));
}, 1000);
return () => window.clearInterval(timer);
}, [retryCooldown]);
const loginButtonText = useMemo(() => {
if (retryCooldown > 0) {
return t("Claw.Setup.login.pleaseWait", { seconds: retryCooldown });
}
return t("Claw.Setup.login.loginButton");
}, [retryCooldown]);
const handleLogin = async () => {
const normalizedDomain = domain.trim();
const normalizedUsername = username.trim();
if (!normalizedUsername || !password) {
message.warning(t("Claw.Setup.login.accountAndCodeRequired"));
return;
}
if (!normalizedDomain) {
message.warning(t("Claw.Setup.login.domainRequired"));
return;
}
setLoading(true);
setError("");
try {
await onLoginStart?.();
await loginAndRegister(normalizedUsername, password, {
suppressToast: true,
domain: normalizedDomain,
});
await onLoginSuccess();
} catch (loginError: any) {
const errorMessage = getAuthErrorMessage(loginError);
message.error(errorMessage);
setError(errorMessage);
setPassword("");
setRetryCooldown(3);
} finally {
setLoading(false);
}
};
return (
<div className={styles.page}>
<aside className={styles.brandPanel}>
<div className={styles.brandContent}>
<div className={styles.brandIcon} aria-hidden="true">
<span className={styles.brandIconHead} />
<span className={styles.brandIconBody} />
</div>
<div className={styles.brandTitle}></div>
<div className={styles.brandSubtitle}>DIGITAL EMPLOYEE</div>
<div className={styles.brandSlogan}></div>
<div className={styles.brandCopyright}>© 2026 </div>
</div>
</aside>
<main className={styles.loginPanel}>
<section className={styles.card}>
<div className={styles.header}>
<h1 className={styles.title}></h1>
<p className={styles.subtitle}></p>
</div>
<div className={styles.form}>
<label className={styles.field}>
<span className={styles.label}></span>
<Input
className={styles.input}
prefix={<GlobalOutlined />}
value={domain}
onChange={(event) => setDomain(event.target.value)}
placeholder="请输入认证地址"
autoComplete="off"
allowClear
spellCheck={false}
/>
</label>
<label className={styles.field}>
<span className={styles.label}></span>
<Input
className={styles.input}
prefix={<UserOutlined />}
value={username}
onChange={(event) => setUsername(event.target.value)}
placeholder="请输入用户名"
autoComplete="username"
/>
</label>
<label className={styles.field}>
<span className={styles.label}></span>
<Input.Password
className={styles.input}
prefix={<LockOutlined />}
value={password}
onChange={(event) => setPassword(event.target.value)}
placeholder="请输入密码"
autoComplete="current-password"
onPressEnter={handleLogin}
/>
</label>
<div className={styles.error}>{error}</div>
<Button
className={styles.loginButton}
type="primary"
block
loading={loading}
disabled={retryCooldown > 0}
onClick={handleLogin}
>
{loginButtonText}
</Button>
</div>
<div className={styles.hint}>
<span className={styles.hintIcon} />
<span></span>
<span className={styles.hintArrow}></span>
</div>
<div className={styles.divider} />
<div className={styles.footer}>
<span>?</span>
<span>v2.0.1</span>
</div>
</section>
</main>
</div>
);
}
export default LoginPage;

View File

@@ -33,10 +33,7 @@ import {
SettingOutlined,
UserOutlined,
CheckCircleOutlined,
RobotOutlined,
FolderOpenOutlined,
LockOutlined,
GlobalOutlined,
} from "@ant-design/icons";
import {
setupService,
@@ -46,9 +43,7 @@ import {
import {
loginAndRegister,
normalizeServerHost,
isLoggedIn as checkIsLoggedIn,
getCurrentAuth,
getAuthErrorMessage,
logout,
} from "../../services/core/auth";
import { AUTH_KEYS } from "@shared/constants";
@@ -56,6 +51,7 @@ import type { QuickInitConfig } from "@shared/types/quickInit";
import SetupDependencies, {
type MockDependenciesApi,
} from "./SetupDependencies";
import LoginPage from "./LoginPage";
const { Text } = Typography;
@@ -94,19 +90,15 @@ function SetupWizard({
const [dependenciesReady, setDependenciesReady] = useState<boolean | null>(
null,
);
const [currentStep, setCurrentStep] = useState(1);
const [currentStep, setCurrentStep] = useState(2);
const [loading, setLoading] = useState(true);
const [step1Config, setStep1Config] =
useState<Step1Config>(DEFAULT_STEP1_CONFIG);
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [completed, setCompleted] = useState(false);
const [domain, setDomain] = useState("");
const [loginLoading, setLoginLoading] = useState(false);
const [loginError, setLoginError] = useState("");
const [retryCooldown, setRetryCooldown] = useState(0);
const [isAlreadyLoggedIn, setIsAlreadyLoggedIn] = useState(false);
const [checkingAuth, setCheckingAuth] = useState(false);
const [checkingAuth, setCheckingAuth] = useState(true);
const [quickIniting, setQuickIniting] = useState(false);
const quickInitAttempted = useRef(false);
@@ -117,14 +109,14 @@ function SetupWizard({
if (skipDependencyCheck) {
setDependenciesReady(true);
const state = await setupService.getSetupState();
if (state.completed) {
setCompleted(true);
onCompleteRef.current();
} else {
setCurrentStep(state.step1Completed ? 2 : 1);
}
const config = await setupService.getStep1Config();
setStep1Config(config);
if (state.completed) {
setCompleted(true);
onComplete();
} else {
setCurrentStep(2);
}
setLoading(false);
return;
}
@@ -173,7 +165,7 @@ function SetupWizard({
);
}
}
setCurrentStep(state.step1Completed ? 2 : 1);
setCurrentStep(2);
}
} else {
// 有依赖缺失,进入安装流程
@@ -203,6 +195,7 @@ function SetupWizard({
try {
// 1. 保存 step1 配置
const step1: Step1Config = {
...DEFAULT_STEP1_CONFIG,
serverHost: normalizeServerHost(config.serverHost),
agentPort: config.agentPort,
fileServerPort: config.fileServerPort,
@@ -247,9 +240,7 @@ function SetupWizard({
error,
);
setQuickIniting(false);
// step1 可能已保存,从 step1 或 step2 继续
const state = await setupService.getSetupState();
setCurrentStep(state.step1Completed ? 2 : 1);
setCurrentStep(2);
}
},
[onComplete],
@@ -272,9 +263,23 @@ function SetupWizard({
}
}
setCurrentStep(1);
setCurrentStep(2);
}, [performQuickInit]);
const ensureDefaultStep1Config = useCallback(async () => {
const currentConfig = await setupService.getStep1Config();
const nextConfig: Step1Config = {
...DEFAULT_STEP1_CONFIG,
...currentConfig,
serverHost: currentConfig.serverHost || DEFAULT_STEP1_CONFIG.serverHost,
workspaceDir:
currentConfig.workspaceDir || DEFAULT_STEP1_CONFIG.workspaceDir,
};
await setupService.saveStep1Config(nextConfig);
setStep1Config(nextConfig);
return nextConfig;
}, []);
// Check login status when entering step 2
const checkLoginStatus = useCallback(async () => {
setCheckingAuth(true);
@@ -300,15 +305,6 @@ function SetupWizard({
}
}, [currentStep, checkLoginStatus]);
// Retry cooldown timer
useEffect(() => {
if (retryCooldown <= 0) return;
const timer = setInterval(() => {
setRetryCooldown((prev) => Math.max(0, prev - 1));
}, 1000);
return () => clearInterval(timer);
}, [retryCooldown]);
const handleStep1Submit = async () => {
if (!step1Config.fileServerPort) {
message.warning(t("Claw.Setup.basicConfig.fileServerPortRequired"));
@@ -327,49 +323,13 @@ function SetupWizard({
await setupService.saveStep1Config(step1Config);
setCurrentStep(2);
message.success(t("Claw.Setup.basicConfig.saved"));
} catch (error) {
} catch {
message.error(t("Claw.Setup.basicConfig.saveFailed"));
} finally {
setLoading(false);
}
};
const handleLogin = async () => {
if (!username || !password) {
message.warning(t("Claw.Setup.login.accountAndCodeRequired"));
return;
}
const loginDomain = domain || step1Config.serverHost;
if (!loginDomain) {
message.warning(t("Claw.Setup.login.domainRequired"));
return;
}
setLoginLoading(true);
setLoginError("");
try {
await loginAndRegister(username, password, {
suppressToast: true,
domain: loginDomain,
});
setLoginError("");
await setupService.completeStep2();
await setupService.completeSetup();
setCompleted(true);
message.success(t("Claw.Setup.login.success"));
setTimeout(() => onComplete(), 2000);
} catch (error: any) {
const errorMessage = getAuthErrorMessage(error);
message.error(errorMessage);
setLoginError(errorMessage);
setPassword("");
setRetryCooldown(3);
} finally {
setLoginLoading(false);
}
};
const handleContinueLoggedIn = async () => {
try {
await setupService.completeStep2();
@@ -381,6 +341,15 @@ function SetupWizard({
}
};
const handleLoginSuccess = async () => {
await ensureDefaultStep1Config();
await setupService.completeStep2();
await setupService.completeSetup();
setCompleted(true);
message.success(t("Claw.Setup.login.success"));
setTimeout(() => onComplete(), 2000);
};
const handleLogout = async () => {
try {
await logout();
@@ -551,85 +520,11 @@ function SetupWizard({
}
return (
<Form layout="vertical">
{loginError && (
<Alert
message={t("Claw.Setup.login.failed")}
description={
<Text
copyable={{ text: loginError }}
style={{
fontSize: 12,
color: "var(--color-text-secondary)",
}}
>
{loginError}
</Text>
}
type="error"
showIcon
style={{ marginBottom: 12 }}
/>
)}
<Form.Item label={t("Claw.Setup.login.domain")} required>
<Input
prefix={<GlobalOutlined />}
value={domain || step1Config.serverHost}
onChange={(e) => setDomain(e.target.value)}
placeholder={t("Claw.Setup.login.domainPlaceholder")}
autoComplete="off"
spellCheck={false}
/>
</Form.Item>
<Form.Item label={t("Claw.Setup.login.account")} required>
<Input
prefix={<UserOutlined />}
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder={t("Claw.Setup.login.accountPlaceholder")}
autoComplete="username"
/>
</Form.Item>
<Form.Item label={t("Claw.Setup.login.code")} required>
<Input.Password
prefix={<LockOutlined />}
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder={t("Claw.Setup.login.codePlaceholder")}
autoComplete="current-password"
/>
</Form.Item>
<Space style={{ width: "100%" }} direction="vertical">
<Space>
<Button onClick={() => setCurrentStep(1)}>
{t("Claw.Setup.login.prevStep")}
</Button>
<Button
type="primary"
onClick={handleLogin}
loading={loginLoading}
disabled={retryCooldown > 0}
>
{retryCooldown > 0
? t("Claw.Setup.login.pleaseWait", {
seconds: retryCooldown,
})
: t("Claw.Setup.login.loginButton")}
</Button>
</Space>
<div style={{ textAlign: "center" }}>
<Text
style={{ fontSize: 11, color: "var(--color-text-tertiary)" }}
>
{t("Claw.Setup.login.supportMultipleAccountTypes")}
</Text>
</div>
</Space>
</Form>
<LoginPage
step1Config={step1Config}
initialUsername={username}
onLoginSuccess={handleLoginSuccess}
/>
);
default:
@@ -709,6 +604,22 @@ function SetupWizard({
);
}
if (
dependenciesReady &&
currentStep === 2 &&
!completed &&
!checkingAuth &&
!isAlreadyLoggedIn
) {
return (
<LoginPage
step1Config={step1Config}
initialUsername={username}
onLoginSuccess={handleLoginSuccess}
/>
);
}
// 阶段 2-4: 配置步骤
return (
<div style={styles.container}>

View File

@@ -6,7 +6,7 @@
<link rel="icon" type="image/png" href="/icon.png" />
<!-- 与 @shared/constants APP_DISPLAY_NAME 保持一致 -->
<title>启明</title>
<title>陇电数字员工</title>
</head>
<body>
<div id="root"></div>

View File

@@ -49,7 +49,7 @@ import {
describe("Constants", () => {
describe("App Identity", () => {
it("should have consistent app name", () => {
expect(APP_DISPLAY_NAME).toBe("启明数字员工");
expect(APP_DISPLAY_NAME).toBe("陇电数字员工");
expect(APP_NAME_IDENTIFIER).toBe("qimingclaw");
});

View File

@@ -10,7 +10,7 @@ import type { AgentEngineType } from "@shared/types/electron";
// ==================== 应用名称 ====================
/** 应用对外显示名称(窗口标题、关于、安装包名称等),与 package.json build.productName 保持一致 */
export const APP_DISPLAY_NAME = "启明数字员工";
export const APP_DISPLAY_NAME = "陇电数字员工";
/** 应用技术标识(进程名、目录名等,小写字母),与 appId 等保持一致 */
export const APP_NAME_IDENTIFIER = "qimingclaw";