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