feat: add svelte workspace feature panels
This commit is contained in:
18
apps/web/src/features/ai/AISessionCards.svelte
Normal file
18
apps/web/src/features/ai/AISessionCards.svelte
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export type AISessionCard = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
updatedAt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export let sessions: AISessionCard[] = [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section aria-label="AI 会话" class="ai-session-grid">
|
||||||
|
{#each sessions as session}
|
||||||
|
<button class="ai-session-card" type="button">
|
||||||
|
<strong>{session.title}</strong>
|
||||||
|
<span>{session.updatedAt}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
24
apps/web/src/features/inbox/InboxPanel.svelte
Normal file
24
apps/web/src/features/inbox/InboxPanel.svelte
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let onCapture: (input: { sourceType: string; body: string }) => void = () => {};
|
||||||
|
|
||||||
|
let body = '';
|
||||||
|
let sourceType = 'text';
|
||||||
|
|
||||||
|
function submit() {
|
||||||
|
onCapture({ sourceType, body });
|
||||||
|
body = '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section aria-label="项目 Inbox" class="inbox-panel">
|
||||||
|
<select aria-label="收集类型" bind:value={sourceType}>
|
||||||
|
<option value="text">文本</option>
|
||||||
|
<option value="link">链接</option>
|
||||||
|
<option value="file">文件</option>
|
||||||
|
</select>
|
||||||
|
<textarea aria-label="收集内容" bind:value={body}></textarea>
|
||||||
|
<div class="toolbar">
|
||||||
|
<button type="button" on:click={submit}>保存到 Inbox</button>
|
||||||
|
<button type="button">分析/整理</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
41
apps/web/src/features/inbox/SuggestionList.svelte
Normal file
41
apps/web/src/features/inbox/SuggestionList.svelte
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export type Suggestion = {
|
||||||
|
kind: 'task' | 'note' | 'source';
|
||||||
|
title: string;
|
||||||
|
body: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export let suggestions: Suggestion[] = [];
|
||||||
|
export let onConfirm: (selected: Suggestion[]) => void;
|
||||||
|
|
||||||
|
let selectedIndexes: number[] = [];
|
||||||
|
|
||||||
|
function toggle(index: number) {
|
||||||
|
selectedIndexes = selectedIndexes.includes(index)
|
||||||
|
? selectedIndexes.filter((item) => item !== index)
|
||||||
|
: [...selectedIndexes, index];
|
||||||
|
}
|
||||||
|
|
||||||
|
function confirm() {
|
||||||
|
onConfirm(selectedIndexes.map((index) => suggestions[index]));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section aria-label="AI 整理建议" class="suggestions">
|
||||||
|
{#each suggestions as suggestion, index}
|
||||||
|
<label class="suggestion">
|
||||||
|
<input
|
||||||
|
aria-label={`选择 ${suggestion.title}`}
|
||||||
|
type="checkbox"
|
||||||
|
checked={selectedIndexes.includes(index)}
|
||||||
|
on:change={() => toggle(index)}
|
||||||
|
/>
|
||||||
|
<span class="suggestion-body">
|
||||||
|
<strong>{suggestion.title}</strong>
|
||||||
|
<small>{suggestion.kind}</small>
|
||||||
|
<span>{suggestion.body}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
<button type="button" on:click={confirm}>创建选中项</button>
|
||||||
|
</section>
|
||||||
25
apps/web/src/features/inbox/SuggestionList.test.ts
Normal file
25
apps/web/src/features/inbox/SuggestionList.test.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import '@testing-library/jest-dom/vitest';
|
||||||
|
import { fireEvent, render, screen } from '@testing-library/svelte';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
import SuggestionList from './SuggestionList.svelte';
|
||||||
|
|
||||||
|
describe('SuggestionList', () => {
|
||||||
|
it('requires explicit selection before confirmation', async () => {
|
||||||
|
const onConfirm = vi.fn();
|
||||||
|
render(SuggestionList, {
|
||||||
|
props: {
|
||||||
|
suggestions: [{ kind: 'task', title: '跟进报价', body: '联系客户' }],
|
||||||
|
onConfirm,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await fireEvent.click(screen.getByText('创建选中项'));
|
||||||
|
expect(onConfirm).toHaveBeenCalledWith([]);
|
||||||
|
|
||||||
|
await fireEvent.click(screen.getByLabelText('选择 跟进报价'));
|
||||||
|
await fireEvent.click(screen.getByText('创建选中项'));
|
||||||
|
expect(onConfirm).toHaveBeenLastCalledWith([
|
||||||
|
{ kind: 'task', title: '跟进报价', body: '联系客户' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
12
apps/web/src/features/notes/MarkdownEditor.svelte
Normal file
12
apps/web/src/features/notes/MarkdownEditor.svelte
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let value = '';
|
||||||
|
export let onChange: (value: string) => void = () => {};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<textarea
|
||||||
|
aria-label="Markdown 编辑器"
|
||||||
|
class="markdown-editor"
|
||||||
|
spellcheck="false"
|
||||||
|
bind:value
|
||||||
|
on:input={() => onChange(value)}
|
||||||
|
></textarea>
|
||||||
22
apps/web/src/features/tasks/TaskList.svelte
Normal file
22
apps/web/src/features/tasks/TaskList.svelte
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export type TaskListItem = {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
status: 'open' | 'doing' | 'done';
|
||||||
|
assignee?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export let tasks: TaskListItem[] = [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section aria-label="任务列表" class="task-list">
|
||||||
|
{#each tasks as task}
|
||||||
|
<article class="task-row">
|
||||||
|
<strong>{task.title}</strong>
|
||||||
|
<span>{task.status}</span>
|
||||||
|
{#if task.assignee}
|
||||||
|
<span>{task.assignee}</span>
|
||||||
|
{/if}
|
||||||
|
</article>
|
||||||
|
{/each}
|
||||||
|
</section>
|
||||||
Reference in New Issue
Block a user