visuddhinanda 1 месяц назад
Родитель
Сommit
f63a3026fb

+ 82 - 0
dashboard-v6/src/api/greetings.ts

@@ -0,0 +1,82 @@
+export type GreetingPeriod = "lateNight" | "morning" | "afternoon" | "evening";
+
+export type Greeting = {
+  zh: string;   // 主标题
+  sub: string;  // 副标题
+  en: string;   // 顶部小标签
+};
+
+const greetings: Record<GreetingPeriod, Greeting[]> = {
+  // 深夜 0–4 时
+  lateNight: [
+    { zh: "深夜仍在用功", sub: "精进不懈,是修行者的庄严。", en: "Still practicing at midnight" },
+    { zh: "夜深心不散", sub: "于寂静中,观照诸法生灭。", en: "A quiet mind in the deep night" },
+    { zh: "夜半修观", sub: "睡眠盖轻,正念正知常相随。", en: "Late night contemplation" },
+    { zh: "深夜清醒", sub: "不放逸者,如明灯照暗室。", en: "Wakeful in the stillness" },
+    { zh: "万籁俱寂", sub: "此刻最宜止观,心随息而定。", en: "Silence and stillness await" },
+    { zh: "夜深精进", sub: "佛陀亦于夜分证悟,愿此夜有所得。", en: "Diligent through the night" },
+    { zh: "不眠者的清明", sub: "以法为灯,以戒为足,行于黑暗中。", en: "Clarity in the small hours" },
+    { zh: "深夜共修", sub: "持续用功,善根日益增长。", en: "Continued effort, continued growth" },
+    { zh: "静夜观心", sub: "心若澄澈,烦恼无处可藏。", en: "Observing the mind at night" },
+    { zh: "夜分不放逸", sub: "诸天赞叹不放逸者,愿你精进。", en: "Heedful through the night" },
+  ],
+
+  // 早晨 5–11 时
+  morning: [
+    { zh: "清晨吉祥", sub: "以清净心迎接新的一天。", en: "Auspicious morning" },
+    { zh: "晨起修行", sub: "日出之前发愿,日落之前无悔。", en: "Morning practice begins" },
+    { zh: "早安,精进者", sub: "晨露未晞,正念已起,善哉。", en: "Good morning, diligent one" },
+    { zh: "朝阳初升", sub: "愿今日所学,回向一切众生。", en: "As the sun rises" },
+    { zh: "晨课吉时", sub: "诵经、禅修,从此刻开始。", en: "The auspicious hour of morning study" },
+    { zh: "新的一天", sub: "昨日已过,今日因缘具足,莫空过。", en: "A new day, a new opportunity" },
+    { zh: "晨光中正念", sub: "正念正知,于行住坐卧中修习。", en: "Mindfulness in the morning light" },
+    { zh: "清晨发心", sub: "愿以此身此心,奉行佛法。", en: "Setting intention at dawn" },
+    { zh: "破晓精进", sub: "黎明属于不放逸者,善加利用。", en: "Dawn belongs to the diligent" },
+    { zh: "晨起观身", sub: "于清醒之初,观此身如实知见。", en: "Observing the body at first light" },
+  ],
+
+  // 下午 12–17 时
+  afternoon: [
+    { zh: "午后仍精进", sub: "莫因日中而懈怠,法义常相续。", en: "Diligent through the afternoon" },
+    { zh: "日中已过", sub: "上午所学,午后深化,善加思惟。", en: "The day continues" },
+    { zh: "午后禅思", sub: "食后少动,正是思择法义之时。", en: "Afternoon reflection" },
+    { zh: "下午吉祥", sub: "以闻思修,令善法增长。", en: "Auspicious afternoon" },
+    { zh: "持续用功", sub: "修行无假期,每一刻皆是道场。", en: "Continuing the practice" },
+    { zh: "日午不放逸", sub: "懈怠是修行的障碍,精进是解脱的资粮。", en: "Heedful in the afternoon" },
+    { zh: "午后清明", sub: "心平气和,于法义中深入思择。", en: "Clear-minded this afternoon" },
+    { zh: "日照当下", sub: "此刻因缘,是过去善业之果,善加珍惜。", en: "Present in this moment" },
+    { zh: "下午共学", sub: "独学而无友,则孤陋而寡闻,善哉同行。", en: "Learning together this afternoon" },
+    { zh: "法喜充满", sub: "以法为食,以定为饮,身心安乐。", en: "Nourished by the Dhamma" },
+  ],
+
+  // 傍晚 18–23 时
+  evening: [
+    { zh: "晚安,善修者", sub: "一日将尽,回顾今日,有否精进?", en: "Good evening, practitioner" },
+    { zh: "暮色修观", sub: "夜幕降临,正是内观的好时机。", en: "Evening contemplation" },
+    { zh: "日暮不放逸", sub: "一日将终,善法若有增长,甚为值得。", en: "Heedful as evening falls" },
+    { zh: "傍晚回向", sub: "愿今日修学功德,回向法界一切众生。", en: "Evening dedication of merit" },
+    { zh: "夜课时分", sub: "灯火初上,诵经坐禅,法喜油然而生。", en: "The evening study session" },
+    { zh: "静心入夜", sub: "放下日间纷扰,于法中安住。", en: "Settling into the evening" },
+    { zh: "晚间思择", sub: "以智慧观照今日身口意,有所进益否?", en: "Evening reflection" },
+    { zh: "暮色中精进", sub: "修行者无论晨昏,正念常相续。", en: "Diligent as dusk falls" },
+    { zh: "夜灯长明", sub: "以法为灯,照亮前行之路。", en: "The lamp of Dhamma burns bright" },
+    { zh: "晚上吉祥", sub: "愿今夜禅修安定,明日再接再厉。", en: "An auspicious evening" },
+  ],
+};
+
+function getPeriod(hour: number): GreetingPeriod {
+  if (hour < 5) return "lateNight";
+  if (hour < 12) return "morning";
+  if (hour < 18) return "afternoon";
+  return "evening";
+}
+
+function getRandom<T>(arr: T[]): T {
+  return arr[Math.floor(Math.random() * arr.length)];
+}
+
+export function getGreeting(): Greeting {
+  const hour = new Date().getHours();
+  const period = getPeriod(hour);
+  return getRandom(greetings[period]);
+}

+ 109 - 0
dashboard-v6/src/api/workspace.ts

@@ -0,0 +1,109 @@
+export type ModuleItem = {
+  key: string;
+  title: string;
+  titleZh: string;
+  description: string;
+  icon: string; // icon name, rendered by caller
+  path: string;
+  color: string;
+  bg: string;
+  accent: string;
+  stats: string;
+};
+
+export type RecentItem = {
+  id: number;
+  title: string;
+  subtitle: string;
+  time: string;
+  type: "tipitaka" | "article" | "task";
+  emoji: string;
+};
+
+// TODO: replace with real fetch
+export async function fetchModules(): Promise<ModuleItem[]> {
+  return [
+    {
+      key: "tipitaka",
+      title: "Tipitaka",
+      titleZh: "大藏经",
+      description: "浏览与研读巴利文三藏经典,包含律藏、经藏与论藏。",
+      icon: "BookOutlined",
+      path: "/workspace/tipitaka",
+      color: "#b5854a",
+      bg: "linear-gradient(135deg, #fdf6ec 0%, #f5e6cc 100%)",
+      accent: "#8c6320",
+      stats: "3 部 · 律经论",
+    },
+    {
+      key: "article",
+      title: "Article",
+      titleZh: "文章",
+      description: "撰写、整理与发布法义文章、学习笔记及研究报告。",
+      icon: "FileTextOutlined",
+      path: "/workspace/edit/article",
+      color: "#4a7fb5",
+      bg: "linear-gradient(135deg, #ecf3fd 0%, #ccddf5 100%)",
+      accent: "#20508c",
+      stats: "24 篇文章",
+    },
+    {
+      key: "task",
+      title: "Task",
+      titleZh: "任务",
+      description: "管理个人修学计划、法务安排与日常待办事项。",
+      icon: "CheckSquareOutlined",
+      path: "/workspace/channel",
+      color: "#4ab58a",
+      bg: "linear-gradient(135deg, #ecfdf6 0%, #ccf0e0 100%)",
+      accent: "#1a7a56",
+      stats: "5 项进行中",
+    },
+  ];
+}
+
+// TODO: replace with real fetch
+export async function fetchRecentItems(): Promise<RecentItem[]> {
+  return [
+    {
+      id: 1,
+      title: "巴利文大藏经",
+      subtitle: "Tipitaka · 律藏",
+      time: "今天",
+      type: "tipitaka",
+      emoji: "📜",
+    },
+    {
+      id: 2,
+      title: "比库戒学习笔记",
+      subtitle: "Article · 学习",
+      time: "昨天",
+      type: "article",
+      emoji: "📝",
+    },
+    {
+      id: 3,
+      title: "161101伍波萨他的准备工作",
+      subtitle: "Article · 法务",
+      time: "Jan 1",
+      type: "article",
+      emoji: "📄",
+    },
+    {
+      id: 4,
+      title: "本周学习任务",
+      subtitle: "Task · 进行中",
+      time: "Feb 20",
+      type: "task",
+      emoji: "✅",
+    },
+    {
+      id: 5,
+      title: "阿毗达磨注释",
+      subtitle: "Tipitaka · 论藏",
+      time: "Feb 18",
+      type: "tipitaka",
+      emoji: "📚",
+    },
+  ];
+}

+ 135 - 0
dashboard-v6/src/components/workspace/home/ModuleCard.tsx

@@ -0,0 +1,135 @@
+import type { CSSProperties } from "react";
+import { useNavigate } from "react-router";
+import {
+  BookOutlined,
+  FileTextOutlined,
+  CheckSquareOutlined,
+  ArrowRightOutlined,
+} from "@ant-design/icons";
+import type { ModuleItem } from "../../../api/workspace";
+
+const iconMap: Record<string, React.ReactNode> = {
+  BookOutlined: <BookOutlined />,
+  FileTextOutlined: <FileTextOutlined />,
+  CheckSquareOutlined: <CheckSquareOutlined />,
+};
+
+type ModuleCardProps = ModuleItem;
+
+export default function ModuleCard({
+  title,
+  titleZh,
+  icon,
+  description,
+  stats,
+  color,
+  bg,
+  accent,
+  path,
+}: ModuleCardProps) {
+  const navigate = useNavigate();
+
+  return (
+    <>
+      <div
+        style={{ ...styles.card, background: bg }}
+        className="workspace-module-card"
+        onClick={() => navigate(path)}
+      >
+        <div style={styles.inner}>
+          <div
+            style={{
+              ...styles.icon,
+              color,
+              borderColor: color + "33",
+            }}
+          >
+            {iconMap[icon]}
+          </div>
+          <div style={styles.info}>
+            <div style={styles.titleRow}>
+              <span style={{ ...styles.title, color: accent }}>{title}</span>
+              <span style={styles.titleZh}>{titleZh}</span>
+            </div>
+            <p style={styles.desc}>{description}</p>
+            <span style={{ ...styles.stats, color }}>{stats}</span>
+          </div>
+          <ArrowRightOutlined style={{ ...styles.arrow, color }} />
+        </div>
+      </div>
+
+      <style>{`
+        .workspace-module-card {
+          cursor: pointer;
+          transition: transform 0.2s ease, box-shadow 0.2s ease;
+          box-shadow: 0 1px 4px rgba(0,0,0,0.06);
+        }
+        .workspace-module-card:hover {
+          transform: translateY(-3px);
+          box-shadow: 0 8px 24px rgba(0,0,0,0.1);
+        }
+      `}</style>
+    </>
+  );
+}
+
+const styles: Record<string, CSSProperties> = {
+  card: {
+    borderRadius: 12,
+    padding: 20,
+    border: "1px solid rgba(0,0,0,0.06)",
+  },
+  inner: {
+    display: "flex",
+    flexDirection: "column",
+    gap: 12,
+    position: "relative",
+  },
+  icon: {
+    fontSize: 22,
+    width: 44,
+    height: 44,
+    borderRadius: 10,
+    border: "1.5px solid",
+    display: "flex",
+    alignItems: "center",
+    justifyContent: "center",
+    background: "rgba(255,255,255,0.6)",
+  },
+  info: {
+    flex: 1,
+  },
+  titleRow: {
+    display: "flex",
+    alignItems: "baseline",
+    gap: 8,
+    marginBottom: 6,
+  },
+  title: {
+    fontSize: 16,
+    fontWeight: 700,
+    letterSpacing: "-0.01em",
+    fontFamily: "Georgia, serif",
+  },
+  titleZh: {
+    fontSize: 12,
+    color: "#a09080",
+  },
+  desc: {
+    fontSize: 13,
+    color: "#6b5f52",
+    lineHeight: 1.6,
+    margin: "0 0 8px",
+  },
+  stats: {
+    fontSize: 12,
+    fontWeight: 500,
+  },
+  arrow: {
+    position: "absolute",
+    top: 0,
+    right: 0,
+    fontSize: 14,
+    opacity: 0.5,
+  },
+};

+ 25 - 0
dashboard-v6/src/components/workspace/home/ModuleGrid.tsx

@@ -0,0 +1,25 @@
+import type { CSSProperties } from "react";
+import type { ModuleItem } from "../../../api/workspace";
+import ModuleCard from "./ModuleCard";
+
+type ModuleGridProps = {
+  modules: ModuleItem[];
+};
+
+export default function ModuleGrid({ modules }: ModuleGridProps) {
+  return (
+    <div style={styles.grid}>
+      {modules.map(({ key, ...mod }) => (
+        <ModuleCard key={key} {...mod} />
+      ))}
+    </div>
+  );
+}
+
+const styles: Record<string, CSSProperties> = {
+  grid: {
+    display: "grid",
+    gridTemplateColumns: "repeat(3, 1fr)",
+    gap: 16,
+  },
+};

+ 112 - 0
dashboard-v6/src/components/workspace/home/RecentItem.tsx

@@ -0,0 +1,112 @@
+import type { CSSProperties } from "react";
+import { ClockCircleOutlined } from "@ant-design/icons";
+import type { RecentItem as RecentItemType } from "../../../api/workspace";
+
+const typeColor: Record<string, string> = {
+  tipitaka: "#b5854a",
+  article: "#4a7fb5",
+  task: "#4ab58a",
+};
+
+type RecentItemProps = RecentItemType & {
+  onClick?: () => void;
+};
+
+export default function RecentItem({
+  emoji,
+  title,
+  subtitle,
+  type,
+  time,
+  onClick,
+}: RecentItemProps) {
+  const color = typeColor[type];
+
+  return (
+    <>
+      <div style={styles.row} className="workspace-recent-item" onClick={onClick}>
+        <div style={styles.emoji}>{emoji}</div>
+        <div style={styles.info}>
+          <span style={styles.title}>{title}</span>
+          <span style={styles.subtitle}>{subtitle}</span>
+        </div>
+        <div style={styles.right}>
+          <span style={{ ...styles.tag, color, background: color + "15" }}>
+            {type}
+          </span>
+          <span style={styles.time}>
+            <ClockCircleOutlined style={{ marginRight: 4, fontSize: 11 }} />
+            {time}
+          </span>
+        </div>
+      </div>
+
+      <style>{`
+        .workspace-recent-item {
+          transition: background 0.15s ease;
+          cursor: pointer;
+        }
+        .workspace-recent-item:hover {
+          background: #f7f7f5 !important;
+        }
+      `}</style>
+    </>
+  );
+}
+
+const styles: Record<string, CSSProperties> = {
+  row: {
+    display: "flex",
+    alignItems: "center",
+    gap: 14,
+    padding: "14px 20px",
+    borderBottom: "1px solid #f0ece6",
+    background: "#fff",
+  },
+  emoji: {
+    fontSize: 20,
+    width: 36,
+    textAlign: "center",
+    flexShrink: 0,
+  },
+  info: {
+    flex: 1,
+    display: "flex",
+    flexDirection: "column",
+    gap: 2,
+    minWidth: 0,
+  },
+  title: {
+    fontSize: 14,
+    fontWeight: 500,
+    color: "#2d2416",
+    overflow: "hidden",
+    textOverflow: "ellipsis",
+    whiteSpace: "nowrap",
+  },
+  subtitle: {
+    fontSize: 12,
+    color: "#a09080",
+  },
+  right: {
+    display: "flex",
+    flexDirection: "column",
+    alignItems: "flex-end",
+    gap: 4,
+    flexShrink: 0,
+  },
+  tag: {
+    fontSize: 11,
+    fontWeight: 600,
+    padding: "2px 8px",
+    borderRadius: 4,
+    letterSpacing: "0.04em",
+    textTransform: "uppercase",
+  },
+  time: {
+    fontSize: 11,
+    color: "#b5a898",
+    display: "flex",
+    alignItems: "center",
+  },
+};

+ 26 - 0
dashboard-v6/src/components/workspace/home/RecentList.tsx

@@ -0,0 +1,26 @@
+import type { CSSProperties } from "react";
+import type { RecentItem as RecentItemType } from "../../../api/workspace";
+import RecentItem from "./RecentItem";
+
+type RecentListProps = {
+  items: RecentItemType[];
+};
+
+export default function RecentList({ items }: RecentListProps) {
+  return (
+    <div style={styles.list}>
+      {items.map((item) => (
+        <RecentItem key={item.id} {...item} />
+      ))}
+    </div>
+  );
+}
+
+const styles: Record<string, CSSProperties> = {
+  list: {
+    background: "#fff",
+    borderRadius: 12,
+    border: "1px solid #ede9e3",
+    overflow: "hidden",
+  },
+};

+ 42 - 0
dashboard-v6/src/components/workspace/home/SectionPanel.tsx

@@ -0,0 +1,42 @@
+import type { ReactNode, CSSProperties } from "react";
+
+type SectionPanelProps = {
+  title: string;
+  children: ReactNode;
+  style?: CSSProperties;
+};
+
+export default function SectionPanel({ title, children, style }: SectionPanelProps) {
+  return (
+    <section style={{ marginBottom: 40, ...style }}>
+      <h2 style={styles.title}>
+        <span style={styles.dot} />
+        {title}
+      </h2>
+      {children}
+    </section>
+  );
+}
+
+const styles: Record<string, CSSProperties> = {
+  title: {
+    fontSize: 13,
+    fontWeight: 600,
+    color: "#8c7e6e",
+    letterSpacing: "0.1em",
+    textTransform: "uppercase",
+    marginBottom: 16,
+    display: "flex",
+    alignItems: "center",
+    gap: 8,
+    fontFamily: "Georgia, serif",
+  },
+  dot: {
+    display: "inline-block",
+    width: 6,
+    height: 6,
+    borderRadius: "50%",
+    background: "#c4a97a",
+    flexShrink: 0,
+  },
+};

+ 52 - 0
dashboard-v6/src/components/workspace/home/WorkspaceHero.tsx

@@ -0,0 +1,52 @@
+import { useMemo, type CSSProperties } from "react";
+import { getGreeting } from "../../../api/greetings";
+
+export default function WorkspaceHero() {
+  // useMemo 确保同一次渲染内问候语固定,不会因重渲染随机变化
+  const greeting = useMemo(() => getGreeting(), []);
+
+  return (
+    <div style={styles.hero}>
+      <div style={styles.inner}>
+        <p style={styles.label}>{greeting.en}</p>
+        <h1 style={styles.title}>{greeting.zh}</h1>
+        <p style={styles.sub}>{greeting.sub}</p>
+      </div>
+    </div>
+  );
+}
+
+const styles: Record<string, CSSProperties> = {
+  hero: {
+    background: "linear-gradient(160deg, #fff 0%, #f0ede8 100%)",
+    borderBottom: "1px solid #e8e4de",
+    padding: "48px 0 36px",
+  },
+  inner: {
+    maxWidth: 880,
+    margin: "0 auto",
+    padding: "0 32px",
+  },
+  label: {
+    fontSize: 13,
+    color: "#b5a898",
+    letterSpacing: "0.12em",
+    textTransform: "uppercase",
+    marginBottom: 8,
+    fontFamily: "Georgia, serif",
+  },
+  title: {
+    fontSize: 36,
+    fontWeight: 700,
+    color: "#2d2416",
+    margin: "0 0 8px",
+    letterSpacing: "-0.02em",
+    fontFamily: "'Noto Serif SC', 'Source Han Serif CN', Georgia, serif",
+  },
+  sub: {
+    fontSize: 15,
+    color: "#8c7e6e",
+    margin: 0,
+    fontFamily: "'Noto Serif SC', 'Source Han Serif CN', Georgia, serif",
+  },
+};

+ 46 - 0
dashboard-v6/src/layouts/workspace/home.tsx

@@ -0,0 +1,46 @@
+import { useEffect, useState, type CSSProperties } from "react";
+import WorkspaceHero from "../../components/workspace/home/WorkspaceHero";
+import SectionPanel from "../../components/workspace/home/SectionPanel";
+import ModuleGrid from "../../components/workspace/home/ModuleGrid";
+import RecentList from "../../components/workspace/home/RecentList";
+import { fetchModules, fetchRecentItems } from "../../api/workspace";
+import type { ModuleItem, RecentItem } from "../../api/workspace";
+
+export default function WorkspaceHome() {
+  const [modules, setModules] = useState<ModuleItem[]>([]);
+  const [recentItems, setRecentItems] = useState<RecentItem[]>([]);
+
+  useEffect(() => {
+    fetchModules().then(setModules);
+    fetchRecentItems().then(setRecentItems);
+  }, []);
+
+  return (
+    <div style={styles.page}>
+      <WorkspaceHero />
+
+      <div style={styles.content}>
+        <SectionPanel title="主要栏目">
+          <ModuleGrid modules={modules} />
+        </SectionPanel>
+
+        <SectionPanel title="最近访问">
+          <RecentList items={recentItems} />
+        </SectionPanel>
+      </div>
+    </div>
+  );
+}
+
+const styles: Record<string, CSSProperties> = {
+  page: {
+    minHeight: "100vh",
+    background: "#f9f8f6",
+    fontFamily: "'Noto Serif SC', 'Source Han Serif CN', Georgia, serif",
+  },
+  content: {
+    maxWidth: 880,
+    margin: "0 auto",
+    padding: "32px 32px 64px",
+  },
+};