summaryrefslogtreecommitdiff
path: root/frontend/src/pages/ProjectsPage.svelte
blob: b29a85673ae557cd87956ac702d9680d53d69479 (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
<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>