summaryrefslogtreecommitdiff
path: root/frontend/src/pages/ProjectsPage.svelte
blob: 13525b622ad051104f0a768a502791dfff2bd75e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
<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';
  import { Button } from '$lib/components/ui/button';
  import * as Card from '$lib/components/ui/card';
  import * as Table from '$lib/components/ui/table';
  import { Input } from '$lib/components/ui/input';
  import { Label } from '$lib/components/ui/label';
  import { Alert } from '$lib/components/ui/alert';

  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;
    }
  }

  // 159399 QA regression (2026-09-17): load() existed but was wired nowhere,
  // so the projects page kept rendering the store's stale/empty state and
  // users saw 还没有项目 despite existing projects on the server.
  $effect(() => {
    void load();
  });

  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 variant={showCreate ? 'outline' : 'default'} 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轮动" class="mt-1" />
        </div>
        <div>
          <Label for="pd">简介(可选)</Label>
          <Input id="pd" bind:value={newDesc} placeholder="一句话说明研究思路" class="mt-1" />
        </div>
        {#if createErr}<Alert variant="destructive" class="banner error" role="alert">{createErr}</Alert>{/if}
        <div class="row">
          <Button type="submit" 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}
      <Card.Root size="sm">
        <Table.Root>
          <Table.Header>
            <Table.Row>
              <Table.Head>名称</Table.Head>
              <Table.Head>简介</Table.Head>
              <Table.Head>版本代</Table.Head>
              <Table.Head>最近更新</Table.Head>
            </Table.Row>
          </Table.Header>
          <Table.Body>
            {#each projectsStore.all as p (p.id)}
              <Table.Row>
                <Table.Cell>
                  <a href={`#/projects/${p.id}/data`} class="p-name">{p.name}</a>
                </Table.Cell>
                <Table.Cell class="hint">{p.description ?? '—'}</Table.Cell>
                <Table.Cell class="num">{p.draft_generation ?? '—'}</Table.Cell>
                <Table.Cell>{fmtDateTime(p.updated_at)}</Table.Cell>
              </Table.Row>
            {/each}
          </Table.Body>
        </Table.Root>
      </Card.Root>
    {/if}
  </div>
</Page>

<style>
  .p-name { font-weight: 600; color: var(--teal-strong); }
  .p-name:hover { text-decoration: underline; }
  form.card h2 { margin-bottom: 0.2rem; }
</style>