init: 初始化 AssetX 项目仓库

包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、
antdesign(管理后台)、landingpage(营销落地页)、
数据库 SQL 和配置文件。
This commit is contained in:
2026-03-27 11:26:43 +00:00
commit 2ee4553b71
634 changed files with 988255 additions and 0 deletions

View File

@@ -0,0 +1,188 @@
"use client";
import { useState, useEffect } from "react";
import Image from "next/image";
import { useApp } from "@/contexts/AppContext";
import { fetchDailyReturns, DailyReturnPoint } from "@/lib/api/fundmarket";
interface PerformanceAnalysisProps {
productId: number;
}
type DayType = "positive" | "negative" | "neutral" | "current";
interface CalendarDayProps {
day: number | null;
value: string;
type: DayType;
}
function CalendarDay({ day, value, type }: CalendarDayProps) {
if (day === null) return <div className="flex-1" />;
const typeStyles: Record<DayType, string> = {
positive: "bg-[#f2fcf7] dark:bg-green-900/20 border-[#cef3e0] dark:border-green-700/30",
negative: "bg-[#fff8f7] dark:bg-red-900/20 border-[#ffdbd5] dark:border-red-700/30",
neutral: "bg-[#f9fafb] dark:bg-gray-700 border-[#f3f4f6] dark:border-gray-600",
current: "bg-[#111827] dark:bg-[#111827] border-[#111827]",
};
const dayTextStyle = type === "current" ? "text-[#fcfcfd]" : "text-[#9ca1af] dark:text-gray-400";
const valueTextStyle =
type === "current" ? "text-[#10b981]" :
type === "positive" ? "text-[#10b981] dark:text-green-400" :
type === "negative" ? "text-[#dc2626] dark:text-red-400" :
"text-[#9ca1af] dark:text-gray-400";
return (
<div className={`rounded border flex flex-col items-center justify-center flex-1 p-1.5 md:p-3 gap-2 md:gap-6 ${typeStyles[type]}`}>
<div className="w-full flex items-start">
<span className={`text-[10px] font-bold leading-[150%] ${dayTextStyle}`}>{day}</span>
</div>
<div className="w-full flex flex-col items-end gap-1">
<span className={`text-[9px] md:text-body-small font-bold leading-[150%] ${valueTextStyle}`}>{value}</span>
{type === "current" && (
<span className="text-[10px] font-bold leading-[150%] tracking-[0.01em] text-[#9DA1AE]">Today</span>
)}
</div>
</div>
);
}
function buildCalendar(year: number, month: number, dataMap: Map<string, DailyReturnPoint>, today: string) {
const firstDay = new Date(year, month - 1, 1).getDay(); // 0=Sun
const daysInMonth = new Date(year, month, 0).getDate();
type Cell = { day: number | null; value: string; type: DayType };
const cells: Cell[] = [];
// Leading empty cells
for (let i = 0; i < firstDay; i++) cells.push({ day: null, value: "", type: "neutral" });
for (let d = 1; d <= daysInMonth; d++) {
const dateStr = `${year}-${String(month).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
const pt = dataMap.get(dateStr);
const isToday = dateStr === today;
const isFuture = dateStr > today;
if (isFuture) {
cells.push({ day: d, value: "--", type: "neutral" });
} else if (!pt || !pt.hasData) {
cells.push({ day: d, value: "--", type: "neutral" });
} else {
const r = pt.dailyReturn;
const label = r === 0 ? "0.00%" : `${r > 0 ? "+" : ""}${r.toFixed(2)}%`;
const type: DayType = isToday ? "current" : r > 0 ? "positive" : r < 0 ? "negative" : "neutral";
cells.push({ day: d, value: label, type });
}
}
// Trailing empty cells to complete the last row
while (cells.length % 7 !== 0) cells.push({ day: null, value: "", type: "neutral" });
// Split into weeks
const weeks: Cell[][] = [];
for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
return weeks;
}
export default function PerformanceAnalysis({ productId }: PerformanceAnalysisProps) {
const { t } = useApp();
const today = new Date();
const [year, setYear] = useState(today.getFullYear());
const [month, setMonth] = useState(today.getMonth() + 1);
const [dataMap, setDataMap] = useState<Map<string, DailyReturnPoint>>(new Map());
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true);
fetchDailyReturns(productId, year, month).then((pts) => {
const m = new Map<string, DailyReturnPoint>();
pts.forEach((p) => m.set(p.date, p));
setDataMap(m);
setIsLoading(false);
});
}, [productId, year, month]);
const todayStr = today.toISOString().slice(0, 10);
const weekData = buildCalendar(year, month, dataMap, todayStr);
const monthLabel = new Date(year, month - 1, 1).toLocaleString("en-US", { month: "long", year: "numeric" });
const prevMonth = () => {
if (month === 1) { setYear(y => y - 1); setMonth(12); }
else setMonth(m => m - 1);
};
const nextMonth = () => {
const isCurrentMonth = year === today.getFullYear() && month === today.getMonth() + 1;
if (isCurrentMonth) return;
if (month === 12) { setYear(y => y + 1); setMonth(1); }
else setMonth(m => m + 1);
};
const isCurrentMonth = year === today.getFullYear() && month === today.getMonth() + 1;
return (
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 p-4 md:p-8 flex flex-col gap-6 md:gap-8">
{/* Calendar Section */}
<div className="flex flex-col gap-6">
{/* Header */}
<div className="flex items-start justify-between w-full">
<div className="flex items-center" style={{ gap: "8px" }}>
<div className="w-5 h-6 flex-shrink-0">
<Image src="/components/product/icon-performance-chart.svg" alt="" width={20} height={24} />
</div>
<h3 className="text-sm font-bold leading-5 text-[#0f172a] dark:text-white">
{t("performance.dailyNetReturns")}
</h3>
</div>
<div className="flex items-center" style={{ gap: "8px", height: "24px" }}>
<button
onClick={prevMonth}
className="rounded-lg flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
style={{ width: "24px", height: "24px" }}
>
<Image src="/components/common/icon-arrow-left.svg" alt="Previous" width={16} height={16} />
</button>
<span className="text-sm font-bold text-[#0a0a0a] dark:text-white" style={{ letterSpacing: "-0.15px", lineHeight: "20px" }}>
{monthLabel}
</span>
<button
onClick={nextMonth}
disabled={isCurrentMonth}
className="rounded-lg flex items-center justify-center hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors disabled:opacity-30 disabled:cursor-not-allowed"
style={{ width: "24px", height: "24px" }}
>
<Image src="/components/common/icon-arrow-right.svg" alt="Next" width={16} height={16} />
</button>
</div>
</div>
{/* Calendar */}
<div className="flex flex-col gap-4">
<div className="grid grid-cols-7 gap-1 md:gap-2">
{["sun", "mon", "tue", "wed", "thu", "fri", "sat"].map((day) => (
<div key={day} className="flex items-center justify-center">
<span className="text-[10px] font-bold leading-[150%] text-[#94a3b8] dark:text-gray-400">
{t(`performance.weekdays.${day}`)}
</span>
</div>
))}
</div>
{isLoading ? (
<div className="h-40 flex items-center justify-center text-[#9ca1af] text-sm">Loading...</div>
) : (
<div className="flex flex-col gap-1">
{weekData.map((week, wi) => (
<div key={wi} className="grid grid-cols-7 gap-1 md:gap-2">
{week.map((day, di) => (
<CalendarDay key={`${wi}-${di}`} day={day.day} value={day.value} type={day.type} />
))}
</div>
))}
</div>
)}
</div>
</div>
</div>
);
}