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}