重构股票信息表单,替换复盘卡片相关逻辑,更新底部导航,移除不再使用的复盘卡片存储,优化 Dockerfile 中的 npm 安装命令
Some checks failed
构建并运行 Docker / build (push) Failing after 2m30s
Some checks failed
构建并运行 Docker / build (push) Failing after 2m30s
This commit is contained in:
@ -10,8 +10,7 @@ WORKDIR /app
|
|||||||
# 安装依赖
|
# 安装依赖
|
||||||
COPY package.json ./
|
COPY package.json ./
|
||||||
# 使用 npm 缓存来加速构建
|
# 使用 npm 缓存来加速构建
|
||||||
RUN npm config set cache /tmp/npm-cache && \
|
RUN npm install --prefer-offline --no-audit --no-fund --production=false
|
||||||
npm install --cache /tmp/npm-cache --prefer-offline --no-audit --no-fund --production=false
|
|
||||||
|
|
||||||
# 构建阶段
|
# 构建阶段
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
|
|||||||
@ -1,119 +1,284 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { REVIEW_TAGS, PRICE_LABELS, type ReviewTag, type PriceLabel, type ReviewCard } from '$lib/types/review';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '$lib/components/ui/dialog';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
import { Label } from '$lib/components/ui/label';
|
||||||
|
import { Plus, X } from 'lucide-svelte';
|
||||||
|
import type { StockInfo } from '$lib/types/review';
|
||||||
|
|
||||||
let { open = false, onClose, onAdd } = $props<{ open?: boolean; onClose: () => void; onAdd: (card: Omit<ReviewCard, 'id' | 'createdAt' | 'updatedAt' | 'starred'>) => void }>();
|
export let open = false;
|
||||||
|
export let onClose: () => void;
|
||||||
|
export let onAdd: (stockInfo: StockInfo) => void;
|
||||||
|
|
||||||
let form = $state({
|
// 默认标签
|
||||||
title: '',
|
const defaultLabels = [
|
||||||
content: '',
|
{ label: '买入价', key: 'buy' },
|
||||||
tags: [] as ReviewTag[],
|
{ label: '目标价', key: 'target' },
|
||||||
stock: {
|
{ label: '止损价', key: 'stop' }
|
||||||
|
];
|
||||||
|
|
||||||
|
let form: StockInfo = {
|
||||||
name: '',
|
name: '',
|
||||||
code: '',
|
code: '',
|
||||||
amount: 0,
|
tags: [],
|
||||||
priceGroups: [
|
priceGroups: []
|
||||||
{ price: 0, label: PRICE_LABELS[0] as PriceLabel }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function addPriceGroup() {
|
|
||||||
form.stock.priceGroups.push({ price: 0, label: PRICE_LABELS[0] });
|
|
||||||
}
|
|
||||||
function removePriceGroup(idx: number) {
|
|
||||||
if (form.stock.priceGroups.length > 1) {
|
|
||||||
form.stock.priceGroups.splice(idx, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function resetForm() {
|
|
||||||
form.title = '';
|
|
||||||
form.content = '';
|
|
||||||
form.tags = [];
|
|
||||||
form.stock = {
|
|
||||||
name: '',
|
|
||||||
code: '',
|
|
||||||
amount: 0,
|
|
||||||
priceGroups: [
|
|
||||||
{ price: 0, label: PRICE_LABELS[0] }
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
}
|
// 保证 price 始终为 number 类型
|
||||||
function handleSubmit() {
|
let addedPriceGroups: { label: string; price: number }[] = [];
|
||||||
if (!form.title.trim() || !form.content.trim()) return;
|
let customLabelInput = '';
|
||||||
onAdd({
|
let customPriceInput: string = '';
|
||||||
title: form.title,
|
let customLabelError = '';
|
||||||
content: form.content,
|
let nameError = '';
|
||||||
tags: [...form.tags],
|
let codeError = '';
|
||||||
stock: {
|
let tagInput = '';
|
||||||
name: form.stock.name,
|
let tagInputRef: any = null;
|
||||||
code: form.stock.code,
|
|
||||||
amount: form.stock.amount,
|
function validate() {
|
||||||
priceGroups: form.stock.priceGroups.map(pg => ({ ...pg }))
|
nameError = form.name.trim() ? '' : '股票名称不能为空';
|
||||||
|
codeError = form.code.trim() ? '' : '股票代码不能为空';
|
||||||
|
customLabelError = '';
|
||||||
|
if (customLabelInput && customPriceInput === '') {
|
||||||
|
customLabelError = '请输入价格';
|
||||||
}
|
}
|
||||||
});
|
return !nameError && !codeError && !customLabelError;
|
||||||
resetForm();
|
}
|
||||||
onClose();
|
|
||||||
}
|
function addTag() {
|
||||||
function handleCancel() {
|
const tag = tagInput.trim();
|
||||||
resetForm();
|
if (tag && !form.tags.includes(tag)) {
|
||||||
onClose();
|
form.tags = [...form.tags, tag];
|
||||||
}
|
}
|
||||||
|
tagInput = '';
|
||||||
|
if (tagInputRef && typeof tagInputRef.focus === 'function') tagInputRef.focus();
|
||||||
|
}
|
||||||
|
function removeTag(idx: number) {
|
||||||
|
form.tags = form.tags.filter((_, i) => i !== idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加自带标签
|
||||||
|
function addDefaultLabel(label: string) {
|
||||||
|
if (!addedPriceGroups.some(pg => pg.label === label)) {
|
||||||
|
addedPriceGroups = [...addedPriceGroups, { label, price: 0 }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 添加自定义标签
|
||||||
|
function addCustomPriceGroup() {
|
||||||
|
const label = customLabelInput.trim();
|
||||||
|
const price = Number(customPriceInput);
|
||||||
|
if (!label) {
|
||||||
|
customLabelError = '请输入标签名';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (customPriceInput === '' || isNaN(price)) {
|
||||||
|
customLabelError = '请输入价格';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
defaultLabels.some(l => l.label === label) ||
|
||||||
|
addedPriceGroups.some(pg => pg.label === label)
|
||||||
|
) {
|
||||||
|
customLabelError = '标签名已存在';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addedPriceGroups = [...addedPriceGroups, { label, price }];
|
||||||
|
customLabelInput = '';
|
||||||
|
customPriceInput = '';
|
||||||
|
customLabelError = '';
|
||||||
|
}
|
||||||
|
// 删除标签
|
||||||
|
function removePriceGroup(idx: number) {
|
||||||
|
addedPriceGroups = addedPriceGroups.filter((_, i) => i !== idx);
|
||||||
|
}
|
||||||
|
function resetForm() {
|
||||||
|
form = {
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
tags: [],
|
||||||
|
priceGroups: []
|
||||||
|
};
|
||||||
|
addedPriceGroups = [];
|
||||||
|
customLabelInput = '';
|
||||||
|
customPriceInput = '';
|
||||||
|
customLabelError = '';
|
||||||
|
nameError = '';
|
||||||
|
codeError = '';
|
||||||
|
tagInput = '';
|
||||||
|
}
|
||||||
|
function handleSubmit(e: Event) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!validate()) return;
|
||||||
|
onAdd({
|
||||||
|
name: form.name,
|
||||||
|
code: form.code,
|
||||||
|
tags: [...form.tags],
|
||||||
|
priceGroups: addedPriceGroups.filter(pg => !isNaN(pg.price))
|
||||||
|
});
|
||||||
|
resetForm();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
function handleCancel() {
|
||||||
|
resetForm();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
function handleTagKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Enter' || e.key === ',') {
|
||||||
|
e.preventDefault();
|
||||||
|
addTag();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: if (open) {
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('stock-name')?.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if open}
|
<Dialog bind:open>
|
||||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/30 backdrop-blur-sm">
|
<DialogContent class="max-w-xl w-full rounded-md p-0 bg-background shadow-2xl">
|
||||||
<div class="bg-white dark:bg-zinc-900 rounded-xl shadow-xl p-6 w-full max-w-md relative animate-in fade-in-0 zoom-in-95">
|
<DialogHeader class="px-8 pt-8 pb-3">
|
||||||
<h2 class="text-lg font-bold mb-4">添加复盘卡片</h2>
|
<DialogTitle class="text-2xl font-bold tracking-tight mb-1">添加股票信息</DialogTitle>
|
||||||
<form on:submit|preventDefault={handleSubmit}>
|
<DialogDescription class="text-base text-muted-foreground mt-1 mb-2">
|
||||||
<div class="mb-3">
|
填写股票基本信息和价格标签(可选)。
|
||||||
<label class="block text-sm font-medium mb-1">标题</label>
|
</DialogDescription>
|
||||||
<input class="input input-bordered w-full" bind:value={form.title} required />
|
</DialogHeader>
|
||||||
</div>
|
<form class="space-y-5 px-8 pb-8 pt-2 font-sans" onsubmit={handleSubmit}>
|
||||||
<div class="mb-3">
|
<!-- 股票信息 -->
|
||||||
<label class="block text-sm font-medium mb-1">内容</label>
|
<div>
|
||||||
<textarea class="input input-bordered w-full min-h-[80px]" bind:value={form.content} required />
|
<div class="text-lg font-semibold mb-3 tracking-tight">股票信息</div>
|
||||||
</div>
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="mb-3">
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1">标签</label>
|
<Label for="stock-name" class="mb-1 block text-sm text-muted-foreground font-medium">名称</Label>
|
||||||
<div class="flex flex-wrap gap-2">
|
<Input
|
||||||
{#each REVIEW_TAGS as tag}
|
id="stock-name"
|
||||||
<label class="flex items-center gap-1 cursor-pointer">
|
bind:value={form.name}
|
||||||
<input type="checkbox" bind:group={form.tags} value={tag} />
|
placeholder="股票名称"
|
||||||
<span class="text-xs">{tag}</span>
|
class="h-10 w-full rounded-md px-4 text-sm placeholder:text-muted-foreground/70 focus-visible:ring-2 focus-visible:ring-primary focus-visible:border-primary border border-input"
|
||||||
</label>
|
onblur={() => (nameError = form.name.trim() ? '' : '股票名称不能为空')}
|
||||||
{/each}
|
autocomplete="off"
|
||||||
</div>
|
/>
|
||||||
</div>
|
{#if nameError}
|
||||||
<div class="mb-3">
|
<span class="text-xs text-red-500 mt-1 block">{nameError}</span>
|
||||||
<label class="block text-sm font-medium mb-1">股票信息</label>
|
{/if}
|
||||||
<div class="flex gap-2 mb-2">
|
|
||||||
<input class="input input-bordered flex-1" placeholder="名称" bind:value={form.stock.name} />
|
|
||||||
<input class="input input-bordered flex-1" placeholder="代码" bind:value={form.stock.code} />
|
|
||||||
<input class="input input-bordered w-20" type="number" min="0" placeholder="数量" bind:value={form.stock.amount} />
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium mb-1">价格组</label>
|
<Label for="stock-code" class="mb-1 block text-sm text-muted-foreground font-medium">代码</Label>
|
||||||
{#each form.stock.priceGroups as pg, i}
|
<Input
|
||||||
<div class="flex items-center gap-2 mb-1">
|
id="stock-code"
|
||||||
<select class="input input-bordered w-24" bind:value={pg.label}>
|
bind:value={form.code}
|
||||||
{#each PRICE_LABELS as label}
|
placeholder="股票代码"
|
||||||
<option value={label}>{label}</option>
|
class="h-10 w-full rounded-md px-4 text-sm placeholder:text-muted-foreground/70 focus-visible:ring-2 focus-visible:ring-primary focus-visible:border-primary border border-input"
|
||||||
{/each}
|
onblur={() => (codeError = form.code.trim() ? '' : '股票代码不能为空')}
|
||||||
</select>
|
autocomplete="off"
|
||||||
<input class="input input-bordered w-24" type="number" min="0" placeholder="价格" bind:value={pg.price} />
|
/>
|
||||||
{#if form.stock.priceGroups.length > 1}
|
{#if codeError}
|
||||||
<button type="button" class="btn btn-xs btn-error" on:click={() => removePriceGroup(i)}>移除</button>
|
<span class="text-xs text-red-500 mt-1 block">{codeError}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
<button type="button" class="btn btn-xs btn-primary mt-1" on:click={addPriceGroup}>添加价格组</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end gap-2 mt-6">
|
<div class="mt-4">
|
||||||
<button type="button" class="btn btn-ghost" on:click={handleCancel}>取消</button>
|
<Label for="stock-tags" class="mb-1 block text-sm text-muted-foreground font-medium">股票标签</Label>
|
||||||
<button type="submit" class="btn btn-primary">添加</button>
|
<div class="flex flex-wrap gap-2 items-center mb-1">
|
||||||
|
{#each form.tags as tag, i}
|
||||||
|
<span class="inline-flex items-center px-2 py-0.5 bg-muted rounded-md shadow-sm text-sm border border-muted-foreground/10 transition hover:bg-primary/20 hover:text-primary-foreground cursor-pointer">
|
||||||
|
{tag}
|
||||||
|
<button type="button" class="ml-1 hover:text-destructive focus:outline-none" tabindex="-1" onclick={() => removeTag(i)}>
|
||||||
|
<X class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Input
|
||||||
|
id="stock-tags"
|
||||||
|
bind:this={tagInputRef}
|
||||||
|
class="w-40 h-8 text-sm px-2 rounded-md focus-visible:ring-2 focus-visible:ring-primary border border-input"
|
||||||
|
placeholder="输入标签后回车"
|
||||||
|
bind:value={tagInput}
|
||||||
|
onkeydown={handleTagKeydown}
|
||||||
|
onblur={addTag}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<span class="text-xs text-muted-foreground">按回车或逗号添加标签</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
<div class="border-t border-muted-foreground/10 my-3"></div>
|
||||||
</div>
|
<!-- 价格标签(可选) -->
|
||||||
{/if}
|
<div>
|
||||||
|
<div class="text-lg font-semibold mb-2 tracking-tight">价格标签 <span class="text-xs text-muted-foreground">(可选)</span></div>
|
||||||
|
<div class="flex flex-wrap gap-2 mb-2 items-center">
|
||||||
|
{#each defaultLabels as l}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant={addedPriceGroups.some(pg => pg.label === l.label) ? 'secondary' : 'outline'}
|
||||||
|
class="rounded-md hover:bg-primary/10 hover:text-primary-foreground transition-colors font-medium h-8 px-3"
|
||||||
|
disabled={addedPriceGroups.some(pg => pg.label === l.label)}
|
||||||
|
onclick={() => addDefaultLabel(l.label)}
|
||||||
|
>
|
||||||
|
{#if addedPriceGroups.some(pg => pg.label === l.label)}
|
||||||
|
已添加
|
||||||
|
{:else}
|
||||||
|
<Plus class="w-4 h-4 mr-1" />{l.label}
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#each addedPriceGroups as pg, i}
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<span class="inline-flex items-center h-8 px-3 bg-muted rounded-md shadow-sm text-sm font-medium leading-none border border-muted-foreground/10 transition hover:bg-primary/20 hover:text-primary-foreground cursor-pointer">
|
||||||
|
{pg.label}
|
||||||
|
<button type="button" class="ml-1 hover:text-destructive focus:outline-none" tabindex="-1" onclick={() => removePriceGroup(i)}>
|
||||||
|
<X class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
placeholder="请输入价格"
|
||||||
|
class="w-32 h-8 text-sm px-2 rounded-md focus-visible:ring-2 focus-visible:ring-primary border border-input"
|
||||||
|
value={pg.price === 0 ? '' : pg.price}
|
||||||
|
oninput={e => pg.price = Number((e.target as HTMLInputElement).value)}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<!-- 自定义标签输入 -->
|
||||||
|
<div class="flex items-center gap-2 mt-2">
|
||||||
|
<Input
|
||||||
|
class="w-32 h-8 text-sm px-2 rounded-md focus-visible:ring-2 focus-visible:ring-primary border border-input"
|
||||||
|
placeholder="自定义标签名"
|
||||||
|
bind:value={customLabelInput}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
class="w-32 h-8 text-sm px-2 rounded-md focus-visible:ring-2 focus-visible:ring-primary border border-input"
|
||||||
|
placeholder="请输入价格"
|
||||||
|
bind:value={customPriceInput}
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<Button type="button" size="sm" class="h-8 rounded-md font-medium" onclick={addCustomPriceGroup}>
|
||||||
|
<Plus class="h-4 w-4" /> 添加
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{#if customLabelError}
|
||||||
|
<span class="text-xs text-red-500 mt-1 block">{customLabelError}</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<DialogFooter class="flex flex-row justify-end gap-4 pt-6 pb-2 px-0">
|
||||||
|
<Button variant="outline" type="button" class="min-w-[90px] h-10 rounded-md font-medium" onclick={handleCancel}>取消</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
class="min-w-[120px] h-10 rounded-md text-base font-semibold"
|
||||||
|
disabled={!validate()}
|
||||||
|
>
|
||||||
|
添加股票
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
@ -3,7 +3,7 @@
|
|||||||
import { Home, Plus, Search, Settings, Minus } from 'lucide-svelte';
|
import { Home, Plus, Search, Settings, Minus } from 'lucide-svelte';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { addReviewCardStore } from '@/lib/stores/ReviewCardStore';
|
import { addStockStore } from '$lib/stores/StockStore';
|
||||||
|
|
||||||
// 路由配置
|
// 路由配置
|
||||||
const navs = [
|
const navs = [
|
||||||
@ -36,7 +36,7 @@
|
|||||||
size="icon"
|
size="icon"
|
||||||
class="rounded-full w-16 h-16 shadow-2xl border-4 border-background bg-gradient-to-br from-primary via-primary to-primary/90 flex items-center justify-center transition-all duration-300 hover:scale-110 hover:shadow-[0_20px_40px_rgba(0,0,0,0.3)] active:scale-95 focus-visible:ring-4 focus-visible:ring-primary/20 absolute left-1/2 -translate-x-1/2 -top-6 z-10"
|
class="rounded-full w-16 h-16 shadow-2xl border-4 border-background bg-gradient-to-br from-primary via-primary to-primary/90 flex items-center justify-center transition-all duration-300 hover:scale-110 hover:shadow-[0_20px_40px_rgba(0,0,0,0.3)] active:scale-95 focus-visible:ring-4 focus-visible:ring-primary/20 absolute left-1/2 -translate-x-1/2 -top-6 z-10"
|
||||||
aria-label="添加"
|
aria-label="添加"
|
||||||
onclick={() => addReviewCardStore.set(true)}
|
onclick={() => addStockStore.set(true)}
|
||||||
>
|
>
|
||||||
<Plus class="w-8 h-8 text-white transition-transform duration-200 group-hover:rotate-90" />
|
<Plus class="w-8 h-8 text-white transition-transform duration-200 group-hover:rotate-90" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,33 +0,0 @@
|
|||||||
import { writable } from 'svelte/store';
|
|
||||||
import type { ReviewCard } from '../types/review';
|
|
||||||
|
|
||||||
// 控制添加卡片弹窗
|
|
||||||
export const addReviewCardStore = writable(false);
|
|
||||||
|
|
||||||
// 复盘卡片列表 store
|
|
||||||
function createReviewCardStore() {
|
|
||||||
const { subscribe, set, update } = writable<ReviewCard[]>([]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe,
|
|
||||||
set,
|
|
||||||
reset: () => set([]),
|
|
||||||
add: (card: Omit<ReviewCard, 'id' | 'createdAt' | 'updatedAt' | 'starred'>) =>
|
|
||||||
update(cards => [
|
|
||||||
...cards,
|
|
||||||
{
|
|
||||||
...card,
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
starred: false,
|
|
||||||
createdAt: new Date(),
|
|
||||||
updatedAt: new Date()
|
|
||||||
}
|
|
||||||
]),
|
|
||||||
remove: (id: string) => update(cards => cards.filter(card => card.id !== id)),
|
|
||||||
toggleStar: (id: string) => update(cards => cards.map(card => card.id === id ? { ...card, starred: !card.starred } : card)),
|
|
||||||
updateCard: (id: string, data: Partial<Omit<ReviewCard, 'id' | 'createdAt' | 'updatedAt'>>) =>
|
|
||||||
update(cards => cards.map(card => card.id === id ? { ...card, ...data, updatedAt: new Date() } : card))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const reviewCardStore = createReviewCardStore();
|
|
||||||
16
src/lib/stores/StockStore.ts
Normal file
16
src/lib/stores/StockStore.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import type { StockInfo } from '$lib/types/review';
|
||||||
|
|
||||||
|
function createStockStore() {
|
||||||
|
const { subscribe, set, update } = writable<StockInfo[]>([]);
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
set,
|
||||||
|
reset: () => set([]),
|
||||||
|
add: (stock: StockInfo) => update(list => [...list, stock]),
|
||||||
|
remove: (idx: number) => update(list => list.filter((_, i) => i !== idx)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const stockStore = createStockStore();
|
||||||
|
export const addStockStore = writable(false);
|
||||||
@ -1,32 +1,13 @@
|
|||||||
// 复盘卡片标签类型
|
// 股票信息类型和自定义价格标签类型
|
||||||
export const REVIEW_TAGS = ['情绪', '策略', '失误', '复盘'] as const;
|
|
||||||
export type ReviewTag = typeof REVIEW_TAGS[number];
|
|
||||||
|
|
||||||
// 价格组类型
|
export type CustomPriceGroup = {
|
||||||
export const PRICE_LABELS = ['买入价', '止损价', '目标价', '卖出价'] as const;
|
label: string; // 价格标签名
|
||||||
export type PriceLabel = typeof PRICE_LABELS[number];
|
price: number; // 价格
|
||||||
|
|
||||||
type PriceGroup = {
|
|
||||||
price: number;
|
|
||||||
label: PriceLabel;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 股票信息类型
|
|
||||||
export interface StockInfo {
|
export interface StockInfo {
|
||||||
name: string;
|
name: string; // 股票名称
|
||||||
code: string;
|
code: string; // 股票代码
|
||||||
amount: number;
|
tags: string[]; // 股票标签
|
||||||
priceGroups: PriceGroup[];
|
priceGroups: CustomPriceGroup[]; // 价格标签+价格数组
|
||||||
}
|
|
||||||
|
|
||||||
// 复盘卡片类型
|
|
||||||
export interface ReviewCard {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
tags: ReviewTag[];
|
|
||||||
stock: StockInfo;
|
|
||||||
starred: boolean;
|
|
||||||
createdAt: Date;
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,91 +1,40 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// @ts-expect-error: svelte-masonry 没有类型声明
|
// @ts-expect-error: svelte-masonry 没有类型声明
|
||||||
import Masonry from 'svelte-masonry';
|
import Masonry from 'svelte-masonry';
|
||||||
import { REVIEW_TAGS, PRICE_LABELS, type ReviewTag, type PriceLabel, type ReviewCard } from '$lib/types/review';
|
|
||||||
import AddReviewCardForm from '$lib/components/AddReviewCardForm.svelte';
|
import AddReviewCardForm from '$lib/components/AddReviewCardForm.svelte';
|
||||||
import BottomBar from '$lib/components/layout/BottomBar.svelte';
|
import BottomBar from '$lib/components/layout/BottomBar.svelte';
|
||||||
import { reviewCardStore, addReviewCardStore } from '@/lib/stores/ReviewCardStore';
|
import { stockStore, addStockStore } from '$lib/stores/StockStore';
|
||||||
|
import type { StockInfo } from '$lib/types/review';
|
||||||
|
|
||||||
let showForm = $state(false);
|
function handleAdd(stock: StockInfo) {
|
||||||
// let openAddForm = $state(false); // 移除 openAddForm 相关代码,表单 open={addReviewCardSignal}
|
stockStore.add(stock);
|
||||||
|
|
||||||
let form = $state({
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
tags: [] as ReviewTag[],
|
|
||||||
stock: {
|
|
||||||
name: '',
|
|
||||||
code: '',
|
|
||||||
amount: 0,
|
|
||||||
priceGroups: [
|
|
||||||
{ price: 0, label: PRICE_LABELS[0] as PriceLabel }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function addPriceGroup() {
|
|
||||||
form.stock.priceGroups.push({ price: 0, label: PRICE_LABELS[0] });
|
|
||||||
}
|
}
|
||||||
function removePriceGroup(idx: number) {
|
|
||||||
if (form.stock.priceGroups.length > 1) {
|
|
||||||
form.stock.priceGroups.splice(idx, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetForm() {
|
|
||||||
form.title = '';
|
|
||||||
form.content = '';
|
|
||||||
form.tags = [];
|
|
||||||
form.stock = {
|
|
||||||
name: '',
|
|
||||||
code: '',
|
|
||||||
amount: 0,
|
|
||||||
priceGroups: [
|
|
||||||
{ price: 0, label: PRICE_LABELS[0] }
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAddCard() {
|
|
||||||
// 简单校验
|
|
||||||
if (!form.title.trim() || !form.content.trim()) return;
|
|
||||||
reviewCardStore.add({
|
|
||||||
title: form.title,
|
|
||||||
content: form.content,
|
|
||||||
tags: [...form.tags],
|
|
||||||
stock: {
|
|
||||||
name: form.stock.name,
|
|
||||||
code: form.stock.code,
|
|
||||||
amount: form.stock.amount,
|
|
||||||
priceGroups: form.stock.priceGroups.map(pg => ({ ...pg }))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resetForm();
|
|
||||||
showForm = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAdd(card: Omit<ReviewCard, 'id' | 'createdAt' | 'updatedAt' | 'starred'>) {
|
|
||||||
reviewCardStore.add(card);
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mx-auto p-4">
|
<div class="mx-auto p-4">
|
||||||
<h1 class="text-2xl font-bold mb-6">瀑布流列表演示</h1>
|
<h1 class="text-2xl font-bold mb-6">股票列表</h1>
|
||||||
|
<button class="mb-4 px-4 py-2 bg-primary text-white rounded" on:click={() => addStockStore.set(true)}>添加股票</button>
|
||||||
<Masonry columns="2" gap="16">
|
<Masonry columns="2" gap="16">
|
||||||
{#each $reviewCardStore as card (card.id)}
|
{#each $stockStore as stock, idx (stock.name + stock.code)}
|
||||||
<div class="bg-white dark:bg-zinc-900 rounded-xl shadow-md overflow-hidden mb-4 border border-border">
|
<div class="bg-white dark:bg-zinc-900 rounded-xl shadow-md overflow-hidden mb-4 border border-border">
|
||||||
<div class="p-3">
|
<div class="p-3">
|
||||||
<div class="font-semibold text-base mb-1">{card.title}</div>
|
<div class="font-semibold text-base mb-1">{stock.name}({stock.code})</div>
|
||||||
|
<div class="mt-2 space-y-1">
|
||||||
|
{#each stock.priceGroups as group}
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="inline-block px-2 py-0.5 bg-gray-100 rounded text-xs">{group.label}</span>
|
||||||
|
<span class="text-sm">¥{group.price}</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
<button class="mt-2 text-xs text-red-500" on:click={() => stockStore.remove(idx)}>删除</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</Masonry>
|
</Masonry>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddReviewCardForm open={$addReviewCardStore} onAdd={handleAdd} onClose={() => addReviewCardStore.set(false)} />
|
<AddReviewCardForm open={$addStockStore} onAdd={handleAdd} onClose={() => addStockStore.set(false)} />
|
||||||
|
|
||||||
<BottomBar />
|
<BottomBar />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -114,4 +63,5 @@
|
|||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
margin-right: 0 !important;
|
margin-right: 0 !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
Reference in New Issue
Block a user