diff --git a/Dockerfile b/Dockerfile index c8b8cf9..8e311cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,8 @@ WORKDIR /app # 安装依赖 COPY package.json ./ # 使用 npm 缓存来加速构建 +#换源 +RUN npm config set registry https://registry.npm.taobao.org RUN npm install --prefer-offline --no-audit --no-fund --production=false --legacy-peer-deps # 构建阶段 diff --git a/bun.lock b/bun.lock index 7a62428..5e66d8f 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "name": "watch-stock-list", "dependencies": { "@sveltejs/adapter-node": "^5.2.13", + "idb": "^8.0.3", "lucide-svelte": "^0.532.0", "svelte-masonry": "^0.1.5", }, @@ -398,6 +399,8 @@ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "idb": ["idb@8.0.3", "", {}, "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg=="], + "inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="], "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], diff --git a/package.json b/package.json index 8ba38c1..6d6b5b8 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ }, "dependencies": { "@sveltejs/adapter-node": "^5.2.13", + "idb": "^8.0.3", "lucide-svelte": "^0.532.0", "svelte-masonry": "^0.1.5" } diff --git a/src/lib/components/AddReviewCardForm.svelte b/src/lib/components/AddReviewCardForm.svelte index 6d8a011..a0e3f53 100644 --- a/src/lib/components/AddReviewCardForm.svelte +++ b/src/lib/components/AddReviewCardForm.svelte @@ -5,6 +5,7 @@ import { Label } from '$lib/components/ui/label'; import { Plus, X } from 'lucide-svelte'; import type { StockInfo } from '$lib/types/review'; + import { page } from '$app/stores'; export let open = false; export let onClose: () => void; @@ -21,7 +22,8 @@ name: '', code: '', tags: [], - priceGroups: [] + priceGroups: [], + createdAt: '' }; // 保证 price 始终为 number 类型 let addedPriceGroups: { label: string; price: number }[] = []; @@ -33,13 +35,39 @@ let tagInput = ''; let tagInputRef: any = null; + // 从URL参数填充表单数据 + function fillFormFromUrlParams() { + const urlParams = $page.url.searchParams; + const code = urlParams.get('code'); + const name = urlParams.get('name'); + const tags = urlParams.get('tags'); + + if (code) { + form.code = code; + } + if (name) { + form.name = name; + } + if (tags) { + // 支持逗号分隔的标签 + const tagArray = tags.split(',').map(tag => tag.trim()).filter(tag => tag); + form.tags = [...form.tags, ...tagArray]; + } + } + function validate() { - nameError = form.name.trim() ? '' : '股票名称不能为空'; - codeError = form.code.trim() ? '' : '股票代码不能为空'; + const trimmedName = form.name.trim(); + const trimmedCode = form.code.trim(); + + nameError = trimmedName ? '' : '股票名称不能为空'; + codeError = trimmedCode ? '' : '股票代码不能为空'; customLabelError = ''; - if (customLabelInput && customPriceInput === '') { + + // 只有当自定义标签有输入时才验证价格 + if (customLabelInput.trim() && customPriceInput === '') { customLabelError = '请输入价格'; } + return !nameError && !codeError && !customLabelError; } @@ -94,7 +122,8 @@ name: '', code: '', tags: [], - priceGroups: [] + priceGroups: [], + createdAt: '' }; addedPriceGroups = []; customLabelInput = ''; @@ -104,17 +133,40 @@ codeError = ''; tagInput = ''; } - function handleSubmit(e: Event) { + async 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(); + + const trimmedName = form.name.trim(); + const trimmedCode = form.code.trim(); + + // 确保 code 不为空 + if (!trimmedCode) { + codeError = '股票代码不能为空'; + return; + } + + try { + const stockData = { + name: trimmedName, + code: trimmedCode, + tags: [...form.tags], + priceGroups: addedPriceGroups.filter(pg => !isNaN(pg.price) && pg.price > 0), + createdAt: new Date().toISOString() + }; + + console.log('表单提交的股票数据:', stockData); + await onAdd(stockData); + resetForm(); + onClose(); + } catch (error) { + console.error('表单提交错误:', error); + // 如果是重复股票的错误,显示友好提示 + if (error instanceof Error && error.message.includes('已存在')) { + nameError = '该股票已存在'; + codeError = '该股票已存在'; + } + } } function handleCancel() { resetForm(); @@ -126,7 +178,9 @@ addTag(); } } + // 当对话框打开时,检查URL参数并填充表单 $: if (open) { + fillFormFromUrlParams(); setTimeout(() => { document.getElementById('stock-name')?.focus(); }, 100); @@ -205,7 +259,7 @@
-
价格标签 (可选)
+
价格标签 (完全可选)
{#each defaultLabels as l} diff --git a/src/lib/components/StockCard.svelte b/src/lib/components/StockCard.svelte new file mode 100644 index 0000000..69f5963 --- /dev/null +++ b/src/lib/components/StockCard.svelte @@ -0,0 +1,54 @@ + + +
+
+
+
{stock.name} ({stock.code})
+ {#if stock.createdAt} +
+ + {formatDate(stock.createdAt)} +
+ {/if} +
+ {#if onDelete} + + {/if} +
+ {#if stock.tags && stock.tags.length} +
+ {#each stock.tags as tag} + {tag} + {/each} +
+ {/if} + {#if stock.priceGroups && stock.priceGroups.length} +
+ {#each stock.priceGroups as group} +
+ {group.label} + ¥{group.price} +
+ {/each} +
+ {/if} +
\ No newline at end of file diff --git a/src/lib/stores/StockStore.ts b/src/lib/stores/StockStore.ts index 50e273d..a69cfe7 100644 --- a/src/lib/stores/StockStore.ts +++ b/src/lib/stores/StockStore.ts @@ -1,16 +1,166 @@ import { writable } from 'svelte/store'; +import { openDB, type IDBPDatabase } from 'idb'; import type { StockInfo } from '$lib/types/review'; +// 数据库配置 +const DB_NAME = 'StockWatchDB'; +const DB_VERSION = 1; +const STORE_NAME = 'stocks'; + +// 初始化数据库 +async function initDB() { + console.log('初始化数据库...'); + const db = await openDB(DB_NAME, DB_VERSION, { + upgrade(db) { + console.log('数据库升级,创建对象存储...'); + // 创建 stocks 存储,使用 code 作为主键 + if (!db.objectStoreNames.contains(STORE_NAME)) { + const store = db.createObjectStore(STORE_NAME, { keyPath: 'code' }); + // 创建索引以便按时间排序 + store.createIndex('createdAt', 'createdAt', { unique: false }); + console.log('对象存储创建成功,keyPath:', 'code'); + } + }, + }); + console.log('数据库初始化完成'); + return db; +} + +// 数据库操作函数 +async function loadStocksFromDB(): Promise { + try { + const db = await initDB(); + const tx = db.transaction(STORE_NAME, 'readonly'); + const store = tx.objectStore(STORE_NAME); + const stocks = await store.getAll(); + return stocks; + } catch (error) { + console.error('加载股票数据失败:', error); + return []; + } +} + +async function saveStockToDB(stock: StockInfo): Promise { + try { + console.log('准备保存股票数据:', stock); + + // 验证股票代码是否存在且不为空 + if (!stock.code || stock.code.trim() === '') { + console.error('股票代码为空:', stock); + throw new Error('股票代码不能为空'); + } + + const db = await initDB(); + const tx = db.transaction(STORE_NAME, 'readwrite'); + const store = tx.objectStore(STORE_NAME); + + // 检查是否已存在相同代码的股票 + const existingStock = await store.get(stock.code); + if (existingStock) { + throw new Error(`股票代码 ${stock.code} 已存在`); + } + + console.log('添加到数据库:', stock); + await store.add(stock); + await tx.done; + console.log('股票数据保存成功'); + } catch (error) { + console.error('保存股票数据失败:', error); + console.error('股票数据:', stock); + throw error; + } +} + +async function removeStockFromDB(code: string): Promise { + try { + const db = await initDB(); + const tx = db.transaction(STORE_NAME, 'readwrite'); + const store = tx.objectStore(STORE_NAME); + await store.delete(code); + await tx.done; + } catch (error) { + console.error('删除股票数据失败:', error); + throw error; + } +} + +async function clearAllStocksFromDB(): Promise { + try { + const db = await initDB(); + const tx = db.transaction(STORE_NAME, 'readwrite'); + const store = tx.objectStore(STORE_NAME); + await store.clear(); + await tx.done; + } catch (error) { + console.error('清空股票数据失败:', error); + throw error; + } +} + +// 创建增强的 store function createStockStore() { const { subscribe, set, update } = writable([]); + + // 初始化时从数据库加载数据 + let initialized = false; + return { subscribe, + + // 初始化 store + async init() { + if (!initialized) { + const stocks = await loadStocksFromDB(); + set(stocks); + initialized = true; + } + }, + + // 添加股票 + async add(stock: StockInfo) { + try { + await saveStockToDB(stock); + update(list => [...list, stock]); + } catch (error) { + console.error('添加股票失败:', error); + throw error; + } + }, + + // 删除股票(通过股票代码) + async remove(code: string) { + try { + await removeStockFromDB(code); + update(list => list.filter(stock => stock.code !== code)); + } catch (error) { + console.error('删除股票失败:', error); + throw error; + } + }, + + // 重置所有数据 + async reset() { + try { + await clearAllStocksFromDB(); + set([]); + } catch (error) { + console.error('重置数据失败:', error); + throw error; + } + }, + + // 直接设置数据(用于测试或导入) set, - reset: () => set([]), - add: (stock: StockInfo) => update(list => [...list, stock]), - remove: (idx: number) => update(list => list.filter((_, i) => i !== idx)), + + // 更新数据(用于编辑) + update }; } export const stockStore = createStockStore(); -export const addStockStore = writable(false); \ No newline at end of file +export const addStockStore = writable(false); + +// 在浏览器环境中自动初始化 +if (typeof window !== 'undefined') { + stockStore.init(); +} \ No newline at end of file diff --git a/src/lib/types/review.ts b/src/lib/types/review.ts index 39502d7..c8e23cd 100644 --- a/src/lib/types/review.ts +++ b/src/lib/types/review.ts @@ -7,7 +7,8 @@ export type CustomPriceGroup = { export interface StockInfo { name: string; // 股票名称 - code: string; // 股票代码 + code: string; // 股票代码(唯一标识符) tags: string[]; // 股票标签 priceGroups: CustomPriceGroup[]; // 价格标签+价格数组 + createdAt: string; // 添加时间 } diff --git a/src/routes/list/+page.svelte b/src/routes/list/+page.svelte index b0f2324..d149902 100644 --- a/src/routes/list/+page.svelte +++ b/src/routes/list/+page.svelte @@ -2,34 +2,37 @@ // @ts-expect-error: svelte-masonry 没有类型声明 import Masonry from 'svelte-masonry'; import AddReviewCardForm from '$lib/components/AddReviewCardForm.svelte'; + import StockCard from '$lib/components/StockCard.svelte'; import BottomBar from '$lib/components/layout/BottomBar.svelte'; import { stockStore, addStockStore } from '$lib/stores/StockStore'; import type { StockInfo } from '$lib/types/review'; + import { toast } from 'svelte-sonner'; - function handleAdd(stock: StockInfo) { - stockStore.add(stock); + async function handleAdd(stock: StockInfo) { + try { + console.log('页面组件接收到的股票数据:', stock); + await stockStore.add(stock); + } catch (error) { + console.error('添加股票失败:', error); + toast.error('添加股票失败'); + } + } + + async function handleDelete(stock: StockInfo) { + try { + await stockStore.remove(stock.code); + } catch (error) { + console.error('删除股票失败:', error); + toast.error('删除股票失败'); + } }

股票列表

- {#each $stockStore as stock, idx (stock.name + stock.code)} -
-
-
{stock.name}({stock.code})
-
- {#each stock.priceGroups as group} -
- {group.label} - ¥{group.price} -
- {/each} -
- -
-
+ handleDelete(stock)} /> {/each}