fix: complete audit security and frontend gaps
This commit is contained in:
@@ -7,3 +7,11 @@ test('project workbench shell renders', async ({ page }) => {
|
||||
await expect(page.getByLabel('服务器 IP 或域名')).toBeVisible();
|
||||
await expect(page.getByLabel('项目总览')).toBeVisible();
|
||||
});
|
||||
|
||||
test('inbox suggestions require explicit confirmation', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.getByLabel('选择 跟进报价').check();
|
||||
await page.getByRole('button', { name: '创建选中项' }).click();
|
||||
|
||||
await expect(page.getByText('已选择 1 项')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1,12 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import ProjectDashboard from '../features/projects/ProjectDashboard.svelte';
|
||||
import ServerLogin from '../features/auth/ServerLogin.svelte';
|
||||
import SuggestionList from '../features/inbox/SuggestionList.svelte';
|
||||
import type { Suggestion } from '../features/inbox/types';
|
||||
import { createApi, type ProjectDashboardSummary } from '../lib/api';
|
||||
|
||||
const emptyDashboard: ProjectDashboardSummary = {
|
||||
project_id: 1,
|
||||
pending_inbox_count: 0,
|
||||
open_task_count: 0,
|
||||
recent_note_count: 0,
|
||||
recent_session_count: 0,
|
||||
};
|
||||
|
||||
let apiBase = localStorage.getItem('apiBase') ?? 'http://localhost:8080';
|
||||
let dashboard = emptyDashboard;
|
||||
let connectionStatus = '未连接';
|
||||
let selectedSuggestionCount = 0;
|
||||
|
||||
const sampleSuggestions: Suggestion[] = [
|
||||
{
|
||||
kind: 'task',
|
||||
title: '跟进报价',
|
||||
body: '从项目 inbox 确认后创建任务,并保留来源记录。',
|
||||
},
|
||||
{
|
||||
kind: 'note',
|
||||
title: '客户背景',
|
||||
body: '把对话中的背景信息沉淀成项目笔记。',
|
||||
},
|
||||
];
|
||||
|
||||
onMount(() => {
|
||||
void loadDashboard();
|
||||
});
|
||||
|
||||
function handleServerChange(event: CustomEvent<{ apiBase: string }>) {
|
||||
apiBase = event.detail.apiBase;
|
||||
localStorage.setItem('apiBase', apiBase);
|
||||
void loadDashboard();
|
||||
}
|
||||
|
||||
async function loadDashboard() {
|
||||
connectionStatus = '连接中';
|
||||
try {
|
||||
dashboard = await createApi(apiBase).getProjectDashboard(1);
|
||||
connectionStatus = '已连接';
|
||||
} catch {
|
||||
dashboard = emptyDashboard;
|
||||
connectionStatus = '无法连接服务器';
|
||||
}
|
||||
}
|
||||
|
||||
function handleConfirm(selected: Suggestion[]) {
|
||||
selectedSuggestionCount = selected.length;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -14,17 +62,15 @@
|
||||
<aside class="sidebar">
|
||||
<h1>项目工作台</h1>
|
||||
<ServerLogin {apiBase} on:serverChange={handleServerChange} />
|
||||
<p class="connection-status" aria-live="polite">{connectionStatus}</p>
|
||||
</aside>
|
||||
|
||||
<section class="workspace">
|
||||
<ProjectDashboard
|
||||
summary={{
|
||||
project_id: 1,
|
||||
pending_inbox_count: 0,
|
||||
open_task_count: 0,
|
||||
recent_note_count: 0,
|
||||
recent_session_count: 0,
|
||||
}}
|
||||
/>
|
||||
<ProjectDashboard summary={dashboard} />
|
||||
<section class="inbox-review" aria-label="项目 inbox">
|
||||
<h2>AI 整理建议</h2>
|
||||
<SuggestionList suggestions={sampleSuggestions} onConfirm={handleConfirm} />
|
||||
<p class="selection-status" aria-live="polite">已选择 {selectedSuggestionCount} 项</p>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<script lang="ts">
|
||||
export type Suggestion = {
|
||||
kind: 'task' | 'note' | 'source';
|
||||
title: string;
|
||||
body: string;
|
||||
};
|
||||
import type { Suggestion } from './types';
|
||||
|
||||
export let suggestions: Suggestion[] = [];
|
||||
export let onConfirm: (selected: Suggestion[]) => void;
|
||||
|
||||
5
apps/web/src/features/inbox/types.ts
Normal file
5
apps/web/src/features/inbox/types.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type Suggestion = {
|
||||
kind: 'task' | 'note' | 'source';
|
||||
title: string;
|
||||
body: string;
|
||||
};
|
||||
@@ -41,9 +41,18 @@ select {
|
||||
}
|
||||
|
||||
.workspace {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.connection-status,
|
||||
.selection-status {
|
||||
margin: 12px 0 0;
|
||||
color: #5f6673;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.server-login {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -95,6 +104,51 @@ select {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.inbox-review {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.inbox-review h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.suggestion {
|
||||
display: grid;
|
||||
grid-template-columns: 20px 1fr;
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
border: 1px solid #d9dde3;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.suggestion-body {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.suggestion-body small {
|
||||
color: #5f6673;
|
||||
}
|
||||
|
||||
.suggestions button {
|
||||
justify-self: start;
|
||||
border: 1px solid #1f2937;
|
||||
border-radius: 6px;
|
||||
background: #1f2937;
|
||||
color: white;
|
||||
padding: 9px 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.shell {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -8,9 +8,11 @@ export type ProjectDashboardSummary = {
|
||||
recent_session_count: number;
|
||||
};
|
||||
|
||||
export function createApi(apiBase: string) {
|
||||
export function createApi(apiBase: string, token?: string) {
|
||||
async function request<T>(path: string): Promise<T> {
|
||||
const response = await fetch(`${apiBase}${path}`);
|
||||
const response = await fetch(`${apiBase}/api${path}`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
if (!response.ok) throw new Error(`API request failed: ${response.status}`);
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,5 @@
|
||||
"noEmit": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
"include": ["src/**/*.d.ts", "src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user