feat: 优化当日收益计算方式
This commit is contained in:
123
AGENTS.md
Normal file
123
AGENTS.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# PROJECT KNOWLEDGE BASE
|
||||||
|
|
||||||
|
**Generated:** 2026-03-21T03:22:46Z
|
||||||
|
**Commit:** 270bc3a
|
||||||
|
**Branch:** main
|
||||||
|
|
||||||
|
## OVERVIEW
|
||||||
|
|
||||||
|
Real-time mutual fund valuation tracker (基估宝). Next.js 16 App Router, pure JavaScript (JSX, no TypeScript), static export to GitHub Pages. Glassmorphism UI with heavy custom CSS variables (3557-line globals.css). All data via JSONP/script injection to external Chinese financial APIs (天天基金, 东方财富, 腾讯财经). localStorage as primary database; Supabase for optional cloud sync.
|
||||||
|
|
||||||
|
## STRUCTURE
|
||||||
|
|
||||||
|
```
|
||||||
|
real-time-fund/
|
||||||
|
├── app/ # Next.js App Router root
|
||||||
|
│ ├── page.jsx # MONOLITHIC SPA entry (~3000+ lines) — ALL state + logic here
|
||||||
|
│ ├── layout.jsx # Root layout (theme init, PWA, GA, Toaster)
|
||||||
|
│ ├── globals.css # Tailwind v4 + glassmorphism CSS variables (~3557 lines)
|
||||||
|
│ ├── api/fund.js # ALL external data fetching (~954 lines, JSONP + script injection)
|
||||||
|
│ ├── components/ # 47 app-specific UI components (modals, cards, tables, charts)
|
||||||
|
│ ├── lib/ # Core utilities: supabase, cacheRequest, tradingCalendar, valuationTimeseries
|
||||||
|
│ ├── hooks/ # Custom hooks: useBodyScrollLock, useFundFuzzyMatcher
|
||||||
|
│ └── assets/ # Static images (GitHub SVG, donation QR codes)
|
||||||
|
├── components/ui/ # 15 shadcn/ui primitives (accordion, button, dialog, drawer, etc.)
|
||||||
|
├── lib/utils.js # cn() helper only (clsx + tailwind-merge)
|
||||||
|
├── public/ # Static: allFund.json, PWA manifest, service worker, icon
|
||||||
|
├── doc/ # Documentation: localStorage schema, Supabase SQL, dev group QR
|
||||||
|
├── .github/workflows/ # CI/CD: nextjs.yml (GitHub Pages), docker-ci.yml (Docker build)
|
||||||
|
├── .husky/ # Pre-commit: lint-staged → ESLint
|
||||||
|
├── Dockerfile # Multi-stage: Node 22 build → Nginx Alpine serve
|
||||||
|
├── docker-compose.yml # Docker Compose config
|
||||||
|
├── entrypoint.sh # Runtime env var placeholder replacement
|
||||||
|
├── nginx.conf # Nginx config (port 3000, SPA fallback)
|
||||||
|
├── next.config.js # Static export, reactStrictMode, reactCompiler
|
||||||
|
├── jsconfig.json # Path aliases: @/* → ./*
|
||||||
|
├── eslint.config.mjs # ESLint flat config: next/core-web-vitals
|
||||||
|
├── postcss.config.mjs # Tailwind v4 PostCSS plugin
|
||||||
|
├── components.json # shadcn/ui config (new-york, JSX, RSC)
|
||||||
|
└── package.json # Node >= 20.9.0, lint-staged, husky
|
||||||
|
```
|
||||||
|
|
||||||
|
## WHERE TO LOOK
|
||||||
|
|
||||||
|
| Task | Location | Notes |
|
||||||
|
|------|----------|-------|
|
||||||
|
| Fund valuation logic | `app/api/fund.js` | JSONP to 天天基金, script injection to 腾讯财经 |
|
||||||
|
| Main UI orchestration | `app/page.jsx` | Monolithic — all useState, business logic, rendering |
|
||||||
|
| Fund card display | `app/components/FundCard.jsx` | Individual fund card with holdings |
|
||||||
|
| Desktop table | `app/components/PcFundTable.jsx` | PC-specific table layout |
|
||||||
|
| Mobile table | `app/components/MobileFundTable.jsx` | Mobile-specific layout, swipe actions |
|
||||||
|
| Holding calculations | `app/page.jsx` (getHoldingProfit) | Profit/loss computation |
|
||||||
|
| Cloud sync | `app/lib/supabase.js` + page.jsx sync functions | Supabase auth + data sync |
|
||||||
|
| Trading/DCA | `app/components/TradeModal.jsx`, `DcaModal.jsx` | Buy/sell, dollar-cost averaging |
|
||||||
|
| Fund fuzzy search | `app/hooks/useFundFuzzyMatcher.js` | Fuse.js based name/code matching |
|
||||||
|
| OCR import | `app/page.jsx` (processFiles) | Tesseract.js + LLM parsing |
|
||||||
|
| Valuation intraday chart | `app/lib/valuationTimeseries.js` | localStorage time-series |
|
||||||
|
| Trading calendar | `app/lib/tradingCalendar.js` | Chinese holiday detection via CDN |
|
||||||
|
| Request caching | `app/lib/cacheRequest.js` | In-memory cache with dedup |
|
||||||
|
| UI primitives | `components/ui/` | shadcn/ui — accordion, dialog, drawer, select, etc. |
|
||||||
|
| Global styles | `app/globals.css` | CSS variables, glassmorphism, responsive |
|
||||||
|
| CI/CD | `.github/workflows/nextjs.yml` | Build + deploy to GitHub Pages |
|
||||||
|
| Docker | `Dockerfile`, `docker-compose.yml` | Multi-stage build with runtime env injection |
|
||||||
|
| localStorage schema | `doc/localStorage 数据结构.md` | Full documentation of stored data shapes |
|
||||||
|
| Supabase schema | `doc/supabase.sql` | Database tables for cloud sync |
|
||||||
|
|
||||||
|
## CONVENTIONS
|
||||||
|
|
||||||
|
- **JavaScript only** — no TypeScript. `tsx: false` in shadcn config.
|
||||||
|
- **No src/ directory** — app/, components/, lib/ at root level.
|
||||||
|
- **Static export** — `output: 'export'` in next.config.js. No server-side runtime.
|
||||||
|
- **JSONP + script injection** — all external API calls bypass CORS via `<script>` tags, not fetch().
|
||||||
|
- **localStorage-first** — all user data stored locally; Supabase sync is optional/secondary.
|
||||||
|
- **Monolithic page.jsx** — entire app state and logic in one file (~3000+ lines). No state management library.
|
||||||
|
- **Dual responsive layouts** — `PcFundTable` and `MobileFundTable` switch at 640px breakpoint.
|
||||||
|
- **shadcn/ui conventions** — new-york style, CSS variables enabled, Lucide icons, path aliases (`@/components`, `@/lib/utils`).
|
||||||
|
- **Linting only** — ESLint + lint-staged on pre-commit. No Prettier, no auto-formatting.
|
||||||
|
- **React Compiler** — `reactCompiler: true` in next.config.js (experimental auto-memoization).
|
||||||
|
|
||||||
|
## ANTI-PATTERNS (THIS PROJECT)
|
||||||
|
|
||||||
|
- **No test infrastructure** — zero test files, no test framework, no test scripts.
|
||||||
|
- **Dual ESLint configs** — both `.eslintrc.json` (legacy) and `eslint.config.mjs` (flat) exist. Flat config is active.
|
||||||
|
- **`--legacy-peer-deps`** — Dockerfile uses this flag, indicating peer dependency conflicts.
|
||||||
|
- **Console statements** — 20 console.error/warn/log across codebase (mostly error logging in page.jsx).
|
||||||
|
- **2 eslint-disable comments** — `no-await-in-loop` in MobileFundTable, `react-hooks/exhaustive-deps` in HoldingEditModal.
|
||||||
|
- **Hardcoded API keys** — `app/api/fund.js` lines 911-914 contain plaintext API keys for LLM service.
|
||||||
|
- **Empty catch blocks** — several `catch (e) {}` blocks that swallow errors silently.
|
||||||
|
|
||||||
|
## UNIQUE STYLES
|
||||||
|
|
||||||
|
- **Glassmorphism design** — frosted glass effect via `backdrop-filter: blur()` + semi-transparent backgrounds.
|
||||||
|
- **CSS variable system** — 50+ CSS custom properties for colors, spacing, transitions in globals.css.
|
||||||
|
- **Runtime env injection** — Docker entrypoint replaces `__PLACEHOLDER__` strings in static JS/HTML at container start.
|
||||||
|
- **JSONP everywhere** — financial APIs (天天基金, 腾讯财经) accessed via script tag injection, not fetch().
|
||||||
|
- **OCR + LLM import** — Tesseract.js OCR → LLM text parsing → fund code extraction.
|
||||||
|
- **Multiple IDE configs** — .cursor/, .qoder/, .trae/ directories suggest active AI-assisted development.
|
||||||
|
|
||||||
|
## COMMANDS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Development
|
||||||
|
npm run dev # Start dev server (localhost:3000)
|
||||||
|
npm run build # Static export to out/
|
||||||
|
npm run lint # ESLint check
|
||||||
|
npm run lint:fix # ESLint auto-fix
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
docker build -t real-time-fund .
|
||||||
|
docker run -d -p 3000:3000 --env-file .env real-time-fund
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
cp env.example .env.local # Copy template, fill NEXT_PUBLIC_* values
|
||||||
|
```
|
||||||
|
|
||||||
|
## NOTES
|
||||||
|
|
||||||
|
- **Fund code format**: 6-digit numeric codes (e.g., 110022). Stored in localStorage key `localFunds`.
|
||||||
|
- **Data sources**: 天天基金 (valuation JSONP), 东方财富 (holdings HTML parsing), 腾讯财经 (stock quotes script injection).
|
||||||
|
- **Deployment**: GitHub Actions auto-deploys main → GitHub Pages. Also supports Vercel, Cloudflare Pages, Docker.
|
||||||
|
- **Node requirement**: >= 20.9.0 (enforced in package.json engines).
|
||||||
|
- **License**: AGPL-3.0 — derivative works must be open-sourced under same license.
|
||||||
|
- **Chinese UI** — all user-facing text is Chinese (zh-CN). README is bilingual (Chinese primary).
|
||||||
38
app/api/AGENTS.md
Normal file
38
app/api/AGENTS.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# app/api/ — Data Fetching Layer
|
||||||
|
|
||||||
|
## OVERVIEW
|
||||||
|
|
||||||
|
Single file (`fund.js`, ~954 lines) containing ALL external data fetching for the entire application. Pure client-side: JSONP + script tag injection to bypass CORS.
|
||||||
|
|
||||||
|
## WHERE TO LOOK
|
||||||
|
|
||||||
|
| Function | Purpose |
|
||||||
|
|----------|---------|
|
||||||
|
| `fetchFundData(code)` | Main fund data (valuation + NAV + holdings). Uses 天天基金 JSONP |
|
||||||
|
| `fetchFundDataFallback(code)` | Backup data source when primary fails |
|
||||||
|
| `fetchSmartFundNetValue(code, date)` | Smart NAV lookup with date fallback |
|
||||||
|
| `searchFunds(val)` | Fund search by name/code (东方财富) |
|
||||||
|
| `fetchFundHistory(code, range)` | Historical NAV data via pingzhongdata |
|
||||||
|
| `fetchFundPingzhongdata(code)` | Raw eastmoney pingzhongdata (trend, grand total) |
|
||||||
|
| `fetchMarketIndices()` | 24 A-share/HK/US indices via 腾讯财经 |
|
||||||
|
| `fetchShanghaiIndexDate()` | Shanghai index date for trading day check |
|
||||||
|
| `parseFundTextWithLLM(text)` | OCR text → fund codes via LLM (apis.iflow.cn) |
|
||||||
|
| `loadScript(url)` | JSONP helper — creates script tag, waits for global var |
|
||||||
|
| `fetchRelatedSectors(code)` | Fund sector/track info (unused in main UI) |
|
||||||
|
|
||||||
|
## CONVENTIONS
|
||||||
|
|
||||||
|
- **JSONP pattern**: `loadScript(url)` → sets global callback → script.onload → reads `window.XXX` → cleanup
|
||||||
|
- **All functions return Promises** — async/await throughout
|
||||||
|
- **Cached via `cachedRequest()`** from `app/lib/cacheRequest.js`
|
||||||
|
- **Error handling**: try/catch returning null/empty — never throws to UI
|
||||||
|
- **Market indices**: `MARKET_INDEX_KEYS` array defines 24 indices with `code`, `varKey`, `name`
|
||||||
|
- **Stock code normalization**: `normalizeTencentCode()` handles A-share (6-digit), HK (5-digit), US (letter codes)
|
||||||
|
|
||||||
|
## ANTI-PATTERNS (THIS DIRECTORY)
|
||||||
|
|
||||||
|
- **Hardcoded API keys** (lines 911-914) — plaintext LLM service keys in source
|
||||||
|
- **Empty catch blocks** — several `catch (e) {}` silently swallowing errors
|
||||||
|
- **Global window pollution** — JSONP callbacks assigned to `window.jsonpgz`, `window.SuggestData_*`, etc.
|
||||||
|
- **No retry logic** — failed requests return null, no exponential backoff
|
||||||
|
- **Script cleanup race conditions** — scripts removed from DOM after onload/onerror, but timeout may trigger after removal
|
||||||
@@ -155,6 +155,38 @@ const parseLatestNetValueFromLsjzContent = (content) => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析历史净值数据(支持多条记录)
|
||||||
|
* 返回按日期升序排列的净值数组
|
||||||
|
*/
|
||||||
|
const parseNetValuesFromLsjzContent = (content) => {
|
||||||
|
if (!content || content.includes('暂无数据')) return [];
|
||||||
|
const rowMatches = content.match(/<tr[\s\S]*?<\/tr>/gi) || [];
|
||||||
|
const results = [];
|
||||||
|
for (const row of rowMatches) {
|
||||||
|
const cells = row.match(/<td[^>]*>(.*?)<\/td>/gi) || [];
|
||||||
|
if (!cells.length) continue;
|
||||||
|
const getText = (td) => td.replace(/<[^>]+>/g, '').trim();
|
||||||
|
const dateStr = getText(cells[0] || '');
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) continue;
|
||||||
|
const navStr = getText(cells[1] || '');
|
||||||
|
const nav = parseFloat(navStr);
|
||||||
|
if (!Number.isFinite(nav)) continue;
|
||||||
|
let growth = null;
|
||||||
|
for (const c of cells) {
|
||||||
|
const txt = getText(c);
|
||||||
|
const m = txt.match(/([-+]?\d+(?:\.\d+)?)\s*%/);
|
||||||
|
if (m) {
|
||||||
|
growth = parseFloat(m[1]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results.push({ date: dateStr, nav, growth });
|
||||||
|
}
|
||||||
|
// 返回按日期升序排列的结果(API返回的是倒序,需要反转)
|
||||||
|
return results.reverse();
|
||||||
|
};
|
||||||
|
|
||||||
const extractHoldingsReportDate = (html) => {
|
const extractHoldingsReportDate = (html) => {
|
||||||
if (!html) return null;
|
if (!html) return null;
|
||||||
|
|
||||||
@@ -316,16 +348,19 @@ export const fetchFundData = async (c) => {
|
|||||||
gszzl: Number.isFinite(gszzlNum) ? gszzlNum : json.gszzl
|
gszzl: Number.isFinite(gszzlNum) ? gszzlNum : json.gszzl
|
||||||
};
|
};
|
||||||
const lsjzPromise = new Promise((resolveT) => {
|
const lsjzPromise = new Promise((resolveT) => {
|
||||||
const url = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${c}&page=1&per=1&sdate=&edate=`;
|
const url = `https://fundf10.eastmoney.com/F10DataApi.aspx?type=lsjz&code=${c}&page=1&per=2&sdate=&edate=`;
|
||||||
loadScript(url)
|
loadScript(url)
|
||||||
.then((apidata) => {
|
.then((apidata) => {
|
||||||
const content = apidata?.content || '';
|
const content = apidata?.content || '';
|
||||||
const latest = parseLatestNetValueFromLsjzContent(content);
|
const navList = parseNetValuesFromLsjzContent(content);
|
||||||
if (latest && latest.nav) {
|
if (navList.length > 0) {
|
||||||
|
const latest = navList[navList.length - 1];
|
||||||
|
const previousNav = navList.length > 1 ? navList[navList.length - 2] : null;
|
||||||
resolveT({
|
resolveT({
|
||||||
dwjz: String(latest.nav),
|
dwjz: String(latest.nav),
|
||||||
zzl: Number.isFinite(latest.growth) ? latest.growth : null,
|
zzl: Number.isFinite(latest.growth) ? latest.growth : null,
|
||||||
jzrq: latest.date
|
jzrq: latest.date,
|
||||||
|
lastNav: previousNav ? String(previousNav.nav) : null
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
resolveT(null);
|
resolveT(null);
|
||||||
@@ -506,6 +541,7 @@ export const fetchFundData = async (c) => {
|
|||||||
gzData.dwjz = tData.dwjz;
|
gzData.dwjz = tData.dwjz;
|
||||||
gzData.jzrq = tData.jzrq;
|
gzData.jzrq = tData.jzrq;
|
||||||
gzData.zzl = tData.zzl;
|
gzData.zzl = tData.zzl;
|
||||||
|
gzData.lastNav = tData.lastNav;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resolve({
|
resolve({
|
||||||
|
|||||||
56
app/components/AGENTS.md
Normal file
56
app/components/AGENTS.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# app/components/ — UI Components
|
||||||
|
|
||||||
|
## OVERVIEW
|
||||||
|
|
||||||
|
47 app-specific React components (all client-side). Modals dominate (~26). Core display: FundCard, PcFundTable, MobileFundTable.
|
||||||
|
|
||||||
|
## STRUCTURE
|
||||||
|
|
||||||
|
```
|
||||||
|
app/components/
|
||||||
|
├── Core Display (6)
|
||||||
|
│ ├── FundCard.jsx # Individual fund card (valuation + holdings)
|
||||||
|
│ ├── PcFundTable.jsx # Desktop table layout
|
||||||
|
│ ├── MobileFundTable.jsx # Mobile list with swipe actions
|
||||||
|
│ ├── MobileFundCardDrawer.jsx# Mobile fund detail drawer
|
||||||
|
│ ├── GroupSummary.jsx # Group portfolio summary
|
||||||
|
│ └── MarketIndexAccordion.jsx# Market indices (24 A/HK/US)
|
||||||
|
├── Modals (26)
|
||||||
|
│ ├── Fund ops: AddFundToGroupModal, GroupManageModal, GroupModal, AddResultModal
|
||||||
|
│ ├── Trading: TradeModal, HoldingEditModal, HoldingActionModal, TransactionHistoryModal, PendingTradesModal, DcaModal, AddHistoryModal
|
||||||
|
│ ├── Settings: SettingsModal, MarketSettingModal, MobileSettingModal, PcTableSettingModal, SortSettingModal
|
||||||
|
│ ├── Auth: LoginModal, CloudConfigModal
|
||||||
|
│ ├── Scan: ScanPickModal, ScanProgressModal, ScanImportConfirmModal, ScanImportProgressModal
|
||||||
|
│ └── Misc: ConfirmModal, SuccessModal, DonateModal, FeedbackModal, WeChatModal, UpdatePromptModal, FundHistoryNetValueModal
|
||||||
|
├── Charts (3)
|
||||||
|
│ ├── FundIntradayChart.jsx # Intraday valuation chart (localStorage data)
|
||||||
|
│ ├── FundTrendChart.jsx # Fund trend chart (pingzhongdata)
|
||||||
|
│ └── FundHistoryNetValue.jsx # Historical NAV display
|
||||||
|
└── Utilities (7)
|
||||||
|
├── Icons.jsx # Custom SVG icons (Close, Eye, Moon, Sun, etc.)
|
||||||
|
├── Common.jsx # Shared UI helpers
|
||||||
|
├── FitText.jsx # Auto-fit text sizing
|
||||||
|
├── RefreshButton.jsx # Manual refresh control
|
||||||
|
├── EmptyStateCard.jsx # Empty state placeholder
|
||||||
|
├── Announcement.jsx # Banner announcement
|
||||||
|
├── ThemeColorSync.jsx # Theme meta tag sync
|
||||||
|
├── PwaRegister.jsx # Service worker registration
|
||||||
|
└── AnalyticsGate.jsx # Conditional GA loader
|
||||||
|
```
|
||||||
|
|
||||||
|
## CONVENTIONS
|
||||||
|
|
||||||
|
- **All client components** — `'use client'` at top, no server components
|
||||||
|
- **State from parent** — page.jsx manages ALL state; components receive props only
|
||||||
|
- **shadcn/ui primitives** — imported from `@/components/ui/*`
|
||||||
|
- **Mobile/Desktop switching** — parent passes `isMobile` prop; 640px breakpoint
|
||||||
|
- **Modals**: use `useBodyScrollLock(open)` hook for scroll prevention
|
||||||
|
- **Icons**: mix of custom SVG (Icons.jsx) + lucide-react
|
||||||
|
- **Styling**: glassmorphism via CSS variables (globals.css), no component-level CSS
|
||||||
|
|
||||||
|
## ANTI-PATTERNS (THIS DIRECTORY)
|
||||||
|
|
||||||
|
- **No prop drilling avoidance** — all state flows from page.jsx via props (30+ prop holes in FundCard)
|
||||||
|
- **Modal sprawl** — 26 modals could benefit from a modal manager/context
|
||||||
|
- **Swipe gesture duplication** — MobileFundTable and MobileFundCardDrawer both implement swipe logic
|
||||||
|
- **No loading skeletons** — components show spinners, not skeleton placeholders
|
||||||
@@ -458,7 +458,7 @@ export default function MobileFundTable({
|
|||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const item = queue.shift();
|
const item = queue.shift();
|
||||||
if (item == null) continue;
|
if (item == null) continue;
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await worker(item);
|
await worker(item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -859,7 +859,6 @@ export default function MobileFundTable({
|
|||||||
const cls = hasProfit ? (value > 0 ? 'up' : value < 0 ? 'down' : '') : 'muted';
|
const cls = hasProfit ? (value > 0 ? 'up' : value < 0 ? 'down' : '') : 'muted';
|
||||||
const amountStr = hasProfit ? (info.getValue() ?? '') : '—';
|
const amountStr = hasProfit ? (info.getValue() ?? '') : '—';
|
||||||
const percentStr = original.todayProfitPercent ?? '';
|
const percentStr = original.todayProfitPercent ?? '';
|
||||||
const isUpdated = original.isUpdated;
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '100%' }}>
|
<div style={{ width: '100%' }}>
|
||||||
<span className={cls} style={{ display: 'block', width: '100%', fontWeight: 700 }}>
|
<span className={cls} style={{ display: 'block', width: '100%', fontWeight: 700 }}>
|
||||||
@@ -867,7 +866,7 @@ export default function MobileFundTable({
|
|||||||
{masked && hasProfit ? <span className="mask-text">******</span> : amountStr}
|
{masked && hasProfit ? <span className="mask-text">******</span> : amountStr}
|
||||||
</FitText>
|
</FitText>
|
||||||
</span>
|
</span>
|
||||||
{percentStr && !isUpdated && !masked ? (
|
{percentStr && !masked ? (
|
||||||
<span className={`${cls} today-profit-percent`} style={{ display: 'block', width: '100%', fontSize: '0.75em', opacity: 0.9, fontWeight: 500 }}>
|
<span className={`${cls} today-profit-percent`} style={{ display: 'block', width: '100%', fontSize: '0.75em', opacity: 0.9, fontWeight: 500 }}>
|
||||||
<FitText maxFontSize={11} minFontSize={9}>
|
<FitText maxFontSize={11} minFontSize={9}>
|
||||||
{percentStr}
|
{percentStr}
|
||||||
|
|||||||
@@ -867,7 +867,7 @@ export default function PcFundTable({
|
|||||||
<FitText className={cls} style={{ fontWeight: 700, display: 'block' }} maxFontSize={14} minFontSize={10}>
|
<FitText className={cls} style={{ fontWeight: 700, display: 'block' }} maxFontSize={14} minFontSize={10}>
|
||||||
{masked && hasProfit ? <span className="mask-text">******</span> : amountStr}
|
{masked && hasProfit ? <span className="mask-text">******</span> : amountStr}
|
||||||
</FitText>
|
</FitText>
|
||||||
{percentStr && !isUpdated && !masked ? (
|
{percentStr && !masked ? (
|
||||||
<span className={`${cls} today-profit-percent`} style={{ display: 'block', fontSize: '0.75em', opacity: 0.9, fontWeight: 500 }}>
|
<span className={`${cls} today-profit-percent`} style={{ display: 'block', fontSize: '0.75em', opacity: 0.9, fontWeight: 500 }}>
|
||||||
<FitText maxFontSize={11} minFontSize={9}>
|
<FitText maxFontSize={11} minFontSize={9}>
|
||||||
{percentStr}
|
{percentStr}
|
||||||
|
|||||||
28
app/lib/AGENTS.md
Normal file
28
app/lib/AGENTS.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# app/lib/ — Core Utilities
|
||||||
|
|
||||||
|
## OVERVIEW
|
||||||
|
|
||||||
|
4 utility modules: Supabase client, request cache, trading calendar, valuation time-series.
|
||||||
|
|
||||||
|
## WHERE TO LOOK
|
||||||
|
|
||||||
|
| File | Exports | Purpose |
|
||||||
|
|------|---------|---------|
|
||||||
|
| `supabase.js` | `supabase`, `isSupabaseConfigured` | Supabase client (or noop fallback). Auth + DB + realtime |
|
||||||
|
| `cacheRequest.js` | `cachedRequest()`, `clearCachedRequest()` | In-memory request dedup + TTL cache |
|
||||||
|
| `tradingCalendar.js` | `loadHolidaysForYear()`, `loadHolidaysForYears()`, `isTradingDay()` | Chinese stock market holiday detection via CDN |
|
||||||
|
| `valuationTimeseries.js` | `recordValuation()`, `getValuationSeries()`, `clearFund()`, `getAllValuationSeries()` | Fund valuation time-series (localStorage) |
|
||||||
|
|
||||||
|
## CONVENTIONS
|
||||||
|
|
||||||
|
- **supabase.js**: creates `createNoopSupabase()` when env vars missing — all auth/DB methods return safe defaults
|
||||||
|
- **cacheRequest.js**: deduplicates concurrent requests for same key; default 10s TTL
|
||||||
|
- **tradingCalendar.js**: downloads `chinese-days` JSON from cdn.jsdelivr.net; caches per-year in Map
|
||||||
|
- **valuationTimeseries.js**: localStorage key `fundValuationTimeseries`; auto-clears old dates on new data
|
||||||
|
|
||||||
|
## ANTI-PATTERNS (THIS DIRECTORY)
|
||||||
|
|
||||||
|
- **No error reporting** — all modules silently fail (console.warn at most)
|
||||||
|
- **localStorage quota not handled** — valuationTimeseries writes without checking available space
|
||||||
|
- **Cache only in-memory** — cacheRequest lost on page reload; no persistent cache
|
||||||
|
- **No request cancellation** — JSONP scripts can't be aborted once injected
|
||||||
@@ -600,8 +600,11 @@ export default function HomePage() {
|
|||||||
|
|
||||||
if (canCalcTodayProfit) {
|
if (canCalcTodayProfit) {
|
||||||
const amount = holding.share * currentNav;
|
const amount = holding.share * currentNav;
|
||||||
// 优先用 zzl (真实涨跌幅), 降级用 gszzl
|
// 优先使用昨日净值直接计算(更精确,避免涨跌幅四舍五入误差)
|
||||||
// 若 gztime 日期 > jzrq,说明估值更新晚于净值日期,优先使用 gszzl 计算当日盈亏
|
const lastNav = fund.lastNav != null && fund.lastNav !== '' ? Number(fund.lastNav) : null;
|
||||||
|
if (lastNav && Number.isFinite(lastNav) && lastNav > 0) {
|
||||||
|
profitToday = (currentNav - lastNav) * holding.share;
|
||||||
|
} else {
|
||||||
const gz = isString(fund.gztime) ? toTz(fund.gztime) : null;
|
const gz = isString(fund.gztime) ? toTz(fund.gztime) : null;
|
||||||
const jz = isString(fund.jzrq) ? toTz(fund.jzrq) : null;
|
const jz = isString(fund.jzrq) ? toTz(fund.jzrq) : null;
|
||||||
const preferGszzl =
|
const preferGszzl =
|
||||||
@@ -620,6 +623,7 @@ export default function HomePage() {
|
|||||||
}
|
}
|
||||||
if (!Number.isFinite(rate)) rate = 0;
|
if (!Number.isFinite(rate)) rate = 0;
|
||||||
profitToday = amount - (amount / (1 + rate / 100));
|
profitToday = amount - (amount / (1 + rate / 100));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
profitToday = null;
|
profitToday = null;
|
||||||
}
|
}
|
||||||
|
|||||||
28
components/ui/AGENTS.md
Normal file
28
components/ui/AGENTS.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# components/ui/ — shadcn/ui Primitives
|
||||||
|
|
||||||
|
## OVERVIEW
|
||||||
|
|
||||||
|
15 shadcn/ui components (new-york style, JSX). Low-level primitives — do NOT modify manually.
|
||||||
|
|
||||||
|
## WHERE TO LOOK
|
||||||
|
|
||||||
|
```
|
||||||
|
accordion.jsx button.jsx dialog.jsx drawer.jsx
|
||||||
|
field.jsx input-otp.jsx label.jsx progress.jsx
|
||||||
|
radio-group.jsx select.jsx separator.jsx sonner.jsx
|
||||||
|
spinner.jsx switch.jsx tabs.jsx
|
||||||
|
```
|
||||||
|
|
||||||
|
## CONVENTIONS
|
||||||
|
|
||||||
|
- **Add via CLI**: `npx shadcn@latest add <component>` — never copy-paste manually
|
||||||
|
- **Style**: new-york, CSS variables enabled, neutral base color
|
||||||
|
- **Icons**: lucide-react
|
||||||
|
- **Path aliases**: `@/components/ui/*`, `@/lib/utils` (cn helper)
|
||||||
|
- **forwardRef pattern** — all components use React.forwardRef
|
||||||
|
- **Styling**: tailwind-merge via `cn()` in `lib/utils.js`
|
||||||
|
|
||||||
|
## ANTI-PATTERNS (THIS DIRECTORY)
|
||||||
|
|
||||||
|
- **Do not edit** — manual changes will be overwritten by shadcn CLI updates
|
||||||
|
- **No custom components here** — app-specific components belong in `app/components/`
|
||||||
Reference in New Issue
Block a user