summaryrefslogtreecommitdiff
path: root/frontend/src/pages/ProjectsPage.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/pages/ProjectsPage.svelte')
-rw-r--r--frontend/src/pages/ProjectsPage.svelte112
1 files changed, 112 insertions, 0 deletions
diff --git a/frontend/src/pages/ProjectsPage.svelte b/frontend/src/pages/ProjectsPage.svelte
new file mode 100644
index 0000000..b29a856
--- /dev/null
+++ b/frontend/src/pages/ProjectsPage.svelte
@@ -0,0 +1,112 @@
+<script lang="ts">
+ import Page from '../components/Page.svelte';
+ import Status from '../components/Status.svelte';
+ import { projectsStore } from '../lib/listsStore.svelte';
+ import { createProject } from '../lib/client';
+ import { extractStatusMessage } from '../lib/state';
+ import { go } from '../lib/router';
+ import { fmtDateTime } from '../lib/format';
+
+ let busy = $state(false);
+ let error = $state<string | null>(null);
+ let creating = $state(false);
+ let showCreate = $state(false);
+ let newName = $state('');
+ let newDesc = $state('');
+ let createErr = $state<string | null>(null);
+
+ async function load() {
+ busy = true;
+ error = null;
+ try {
+ await projectsStore.load();
+ } catch (e) {
+ error = extractStatusMessage(e);
+ } finally {
+ busy = false;
+ }
+ }
+
+ async function create(e: SubmitEvent) {
+ e.preventDefault();
+ if (!newName.trim()) return;
+ creating = true;
+ createErr = null;
+ try {
+ const p = await createProject(newName.trim(), newDesc.trim() || undefined);
+ projectsStore.bump(p);
+ showCreate = false;
+ newName = '';
+ newDesc = '';
+ go(`/projects/${p.id}/data`);
+ } catch (err) {
+ createErr = extractStatusMessage(err);
+ } finally {
+ creating = false;
+ }
+ }
+</script>
+
+<Page title="我的项目" subtitle="每个项目持有独立的策略代码版本、数据集与回测记录">
+ {#snippet actions()}
+ <button class="btn primary" onclick={() => (showCreate = !showCreate)} aria-expanded={showCreate}>
+ {showCreate ? '取消新建' : '新建项目'}
+ </button>
+ {/snippet}
+
+ <div class="stack">
+ <Status {busy} {error} busyText="加载项目列表…" empty={null} />
+ {#if showCreate}
+ <form class="card stack" onsubmit={create}>
+ <h2>新建项目</h2>
+ <div>
+ <label for="pn">项目名称</label>
+ <input id="pn" bind:value={newName} required placeholder="例如:双均线ETF轮动" />
+ </div>
+ <div>
+ <label for="pd">简介(可选)</label>
+ <input id="pd" bind:value={newDesc} placeholder="一句话说明研究思路" />
+ </div>
+ {#if createErr}<div class="banner error" role="alert">{createErr}</div>{/if}
+ <div class="row">
+ <button type="submit" class="btn primary" disabled={creating || !newName.trim()}>
+ {creating ? '创建中…' : '创建项目'}
+ </button>
+ </div>
+ </form>
+ {/if}
+
+ {#if !busy && !error && projectsStore.all.length === 0}
+ <Status busy={false} empty="还没有项目" emptyHint="点击右上角「新建项目」开始你的第一个策略研究" />
+ {/if}
+
+ {#if projectsStore.all.length > 0}
+ <div class="card" style="padding:0.4rem 0.6rem;">
+ <table class="data">
+ <thead>
+ <tr>
+ <th>名称</th>
+ <th>简介</th>
+ <th>版本代</th>
+ <th>最近更新</th>
+ </tr>
+ </thead>
+ <tbody>
+ {#each projectsStore.all as p (p.id)}
+ <tr>
+ <td><a href={`#/projects/${p.id}/data`} class="p-name">{p.name}</a></td>
+ <td class="hint">{p.description ?? '—'}</td>
+ <td class="num">{p.draft_generation ?? '—'}</td>
+ <td>{fmtDateTime(p.updated_at)}</td>
+ </tr>
+ {/each}
+ </tbody>
+ </table>
+ </div>
+ {/if}
+ </div>
+</Page>
+
+<style>
+ .p-name { font-weight: 600; }
+</style>