fix(mini): preserve taskbar and restore login
This commit is contained in:
@@ -9,5 +9,5 @@ npm run desktop:dev
|
|||||||
```
|
```
|
||||||
|
|
||||||
- Web 开发端口:`5180`
|
- Web 开发端口:`5180`
|
||||||
- 桌面窗口默认宽度:`420`;启动后高度自动适配当前屏幕全高,并停靠在屏幕右侧
|
- 桌面窗口默认宽度:`420`;启动后高度自动适配当前屏幕可用工作区,不遮挡 Windows 任务栏,并停靠在屏幕右侧
|
||||||
- 左右停靠状态保存在本地;在 Tauri 桌面环境中会移动实际窗口。
|
- 左右停靠状态保存在本地;在 Tauri 桌面环境中会移动实际窗口。
|
||||||
|
|||||||
@@ -58,9 +58,8 @@ await page.route('http://localhost:9150/api/v1/**', async (route) => {
|
|||||||
|
|
||||||
await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: 'networkidle' })
|
await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: 'networkidle' })
|
||||||
await page.screenshot({ path: 'test-results/mini-login.png', fullPage: true })
|
await page.screenshot({ path: 'test-results/mini-login.png', fullPage: true })
|
||||||
await page.locator('.login-note input').nth(1).fill('demo@senlin.ai')
|
const loginDefaults = await page.locator('.login-note input').evaluateAll((inputs) => inputs.map((input) => input.value))
|
||||||
await page.locator('.login-note input').nth(2).fill('password123')
|
await page.locator('.login-note input').nth(2).press('Enter')
|
||||||
await page.getByRole('button', { name: '连接工作台' }).click()
|
|
||||||
await page.waitForSelector('.mini-shell')
|
await page.waitForSelector('.mini-shell')
|
||||||
await page.waitForTimeout(400)
|
await page.waitForTimeout(400)
|
||||||
await page.screenshot({ path: 'test-results/mini-home-right.png', fullPage: true })
|
await page.screenshot({ path: 'test-results/mini-home-right.png', fullPage: true })
|
||||||
@@ -97,6 +96,7 @@ await browser.close()
|
|||||||
await server.close()
|
await server.close()
|
||||||
|
|
||||||
const failures = []
|
const failures = []
|
||||||
|
if (loginDefaults[1] !== 'demo@senlin.ai' || loginDefaults[2] !== 'password123') failures.push(`login defaults failed: ${JSON.stringify(loginDefaults)}`)
|
||||||
if (errors.length) failures.push(`console errors: ${errors.join('; ')}`)
|
if (errors.length) failures.push(`console errors: ${errors.join('; ')}`)
|
||||||
if (home.width !== 420) failures.push(`expected 420px shell, got ${home.width}`)
|
if (home.width !== 420) failures.push(`expected 420px shell, got ${home.width}`)
|
||||||
if (home.navCount !== 5) failures.push(`expected five top navigation items, got ${home.navCount}`)
|
if (home.navCount !== 5) failures.push(`expected five top navigation items, got ${home.navCount}`)
|
||||||
|
|||||||
@@ -7,20 +7,19 @@ fn dock_window(window: Window, side: String) -> Result<(), String> {
|
|||||||
.map_err(|error| error.to_string())?
|
.map_err(|error| error.to_string())?
|
||||||
.ok_or_else(|| "无法识别当前屏幕".to_string())?;
|
.ok_or_else(|| "无法识别当前屏幕".to_string())?;
|
||||||
let window_size = window.outer_size().map_err(|error| error.to_string())?;
|
let window_size = window.outer_size().map_err(|error| error.to_string())?;
|
||||||
let monitor_position = monitor.position();
|
let work_area = monitor.work_area();
|
||||||
let monitor_size = monitor.size();
|
let dock_size = PhysicalSize::new(window_size.width, work_area.size.height);
|
||||||
let dock_size = PhysicalSize::new(window_size.width, monitor_size.height);
|
|
||||||
let x = if side == "left" {
|
let x = if side == "left" {
|
||||||
monitor_position.x
|
work_area.position.x
|
||||||
} else {
|
} else {
|
||||||
monitor_position.x + monitor_size.width as i32 - dock_size.width as i32
|
work_area.position.x + work_area.size.width as i32 - dock_size.width as i32
|
||||||
};
|
};
|
||||||
|
|
||||||
window
|
window
|
||||||
.set_size(dock_size)
|
.set_size(dock_size)
|
||||||
.map_err(|error| error.to_string())?;
|
.map_err(|error| error.to_string())?;
|
||||||
window
|
window
|
||||||
.set_position(PhysicalPosition::new(x, monitor_position.y))
|
.set_position(PhysicalPosition::new(x, work_area.position.y))
|
||||||
.map_err(|error| error.to_string())?;
|
.map_err(|error| error.to_string())?;
|
||||||
window
|
window
|
||||||
.set_always_on_top(true)
|
.set_always_on_top(true)
|
||||||
|
|||||||
@@ -307,13 +307,27 @@ function MoreView({ workspace, openAction }: ViewProps) {
|
|||||||
|
|
||||||
function Login({ onLogin }: { onLogin: (session: ApiSession) => void }) {
|
function Login({ onLogin }: { onLogin: (session: ApiSession) => void }) {
|
||||||
const [server, setServer] = useState('http://localhost:9150')
|
const [server, setServer] = useState('http://localhost:9150')
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('demo@senlin.ai')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('password123')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
|
async function submitLogin() {
|
||||||
|
if (loading) return
|
||||||
|
setLoading(true)
|
||||||
|
setError('')
|
||||||
|
try {
|
||||||
|
onLogin(await login(server, email.trim(), password))
|
||||||
|
} catch (requestError) {
|
||||||
|
setError(errorMessage(requestError))
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mini-login">
|
<div className="mini-login">
|
||||||
<div className="login-note">
|
<form className="login-note" onSubmit={(event) => { event.preventDefault(); void submitLogin() }}>
|
||||||
<img src="/senlinai-icon.svg" alt="" />
|
<img src="/senlinai-icon.svg" alt="" />
|
||||||
<Title heading={3}>森林AI Mini</Title>
|
<Title heading={3}>森林AI Mini</Title>
|
||||||
<Text type="secondary">停靠在屏幕一侧,随时记录与推进项目</Text>
|
<Text type="secondary">停靠在屏幕一侧,随时记录与推进项目</Text>
|
||||||
@@ -321,9 +335,9 @@ function Login({ onLogin }: { onLogin: (session: ApiSession) => void }) {
|
|||||||
<label>邮箱<Input value={email} onChange={setEmail} /></label>
|
<label>邮箱<Input value={email} onChange={setEmail} /></label>
|
||||||
<label>密码<Input.Password value={password} onChange={setPassword} /></label>
|
<label>密码<Input.Password value={password} onChange={setPassword} /></label>
|
||||||
{error ? <Alert type="error" content={error} /> : null}
|
{error ? <Alert type="error" content={error} /> : null}
|
||||||
<Button type="primary" long loading={loading} onClick={async () => { setLoading(true); setError(''); try { onLogin(await login(server, email, password)) } catch (requestError) { setError(errorMessage(requestError)) } finally { setLoading(false) } }}>连接工作台</Button>
|
<Button type="primary" htmlType="submit" long loading={loading}>连接工作台</Button>
|
||||||
<small>v1.0 完整版</small>
|
<small>v1.0 完整版</small>
|
||||||
</div>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -192,7 +192,9 @@ function useSessionBase(session: ApiSession) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeBaseUrl(value: string) {
|
function normalizeBaseUrl(value: string) {
|
||||||
return value.trim().replace(/\/+$/, '')
|
const normalized = value.trim().replace(/\/+$/, '')
|
||||||
|
if (!normalized || /^https?:\/\//i.test(normalized)) return normalized
|
||||||
|
return `http://${normalized}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function slugify(value: string) {
|
function slugify(value: string) {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ ai_key_encryption_secret: "development-ai-key-secret-change-me"
|
|||||||
allowed_origins:
|
allowed_origins:
|
||||||
- "http://localhost:5173"
|
- "http://localhost:5173"
|
||||||
- "http://127.0.0.1:5173"
|
- "http://127.0.0.1:5173"
|
||||||
|
- "http://localhost:5180"
|
||||||
|
- "http://127.0.0.1:5180"
|
||||||
- "http://localhost:4173"
|
- "http://localhost:4173"
|
||||||
- "http://127.0.0.1:4173"
|
- "http://127.0.0.1:4173"
|
||||||
- "http://localhost:4174"
|
- "http://localhost:4174"
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ func TestDevelopmentConfigMatchesLocalWorkspaceContract(t *testing.T) {
|
|||||||
require.ElementsMatch(t, []string{
|
require.ElementsMatch(t, []string{
|
||||||
"http://localhost:5173",
|
"http://localhost:5173",
|
||||||
"http://127.0.0.1:5173",
|
"http://127.0.0.1:5173",
|
||||||
|
"http://localhost:5180",
|
||||||
|
"http://127.0.0.1:5180",
|
||||||
"http://localhost:4173",
|
"http://localhost:4173",
|
||||||
"http://127.0.0.1:4173",
|
"http://127.0.0.1:4173",
|
||||||
"http://localhost:4174",
|
"http://localhost:4174",
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ func cors(cfg config.Config) gin.HandlerFunc {
|
|||||||
origins = []string{
|
origins = []string{
|
||||||
"http://localhost:5173",
|
"http://localhost:5173",
|
||||||
"http://127.0.0.1:5173",
|
"http://127.0.0.1:5173",
|
||||||
|
"http://localhost:5180",
|
||||||
|
"http://127.0.0.1:5180",
|
||||||
"http://tauri.localhost",
|
"http://tauri.localhost",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,18 @@ func TestRouterAddsCORSHeadersForLocalWebClient(t *testing.T) {
|
|||||||
require.Contains(t, rec.Header().Get("Access-Control-Allow-Headers"), "Authorization")
|
require.Contains(t, rec.Header().Get("Access-Control-Allow-Headers"), "Authorization")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRouterAddsCORSHeadersForMiniClient(t *testing.T) {
|
||||||
|
router := NewRouter(config.Config{Env: "test"}, testRegistrar{})
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/ping", nil)
|
||||||
|
req.Header.Set("Origin", "http://localhost:5180")
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusOK, rec.Code)
|
||||||
|
require.Equal(t, "http://localhost:5180", rec.Header().Get("Access-Control-Allow-Origin"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestAPIV1LoginRemainsPublic(t *testing.T) {
|
func TestAPIV1LoginRemainsPublic(t *testing.T) {
|
||||||
router := NewProtectedRouter(config.Config{Env: "test"}, func(token string) (uint, error) {
|
router := NewProtectedRouter(config.Config{Env: "test"}, func(token string) (uint, error) {
|
||||||
return 0, http.ErrNoCookie
|
return 0, http.ErrNoCookie
|
||||||
|
|||||||
Reference in New Issue
Block a user