Browse Source

refactor(scaffold): 完成 TODO 第 0 章脚手架清理

- 后端入口改为读 HAVEN_* 环境变量,缺失必填项拒绝启动,接 log/slog
- 前端移除 Vite 模板(HelloWorld、demo 样式、模板资源),RouterView 外壳并接入 vue-router + pinia
- index.html 改为 zh-CN / Haven
- 新增根 .gitignore,修正 backend/.gitignore 误吞 cmd/haven 的问题
- 新增根 opencode.json(instructions 仅加载 conventions 与 overview)
- TODO 第 0 章全部勾选,AGENTS 当前代码状态同步
kairo 4 giờ trước cách đây
mục cha
commit
ca1ab8074c

+ 48 - 0
.gitignore

@@ -0,0 +1,48 @@
+# ===== 环境与配置 =====
+# 环境变量统一由进程环境提供(HAVEN_*),不使用 .env 文件;这里只兜底本地误建的文件
+.env
+.env.*
+!.env.example
+*.local
+
+# ===== 后端(Go)=====
+# 编译产物。只忽略可执行文件本身,不要写 `backend/haven`——
+# 那里是 cmd/haven 的源码目录
+/backend/haven
+*.exe
+*.test
+*.out
+coverage.*
+go.work.sum
+
+# ===== 前端(Vite / Node)=====
+frontend/node_modules/
+frontend/dist/
+frontend/dist-ssr/
+frontend/.vite/
+*.tsbuildinfo
+pnpm-debug.log*
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# ===== 部署产物 =====
+deploy/build/
+deploy/out/
+
+# ===== 编辑器与 IDE =====
+.vscode/*
+!.vscode/extensions.json
+!.vscode/settings.json
+.idea/
+*.swp
+*.swo
+*~
+
+# ===== 操作系统 =====
+.DS_Store
+Thumbs.db
+
+# ===== 日志与临时文件 =====
+*.log
+/tmp/

+ 7 - 6
AGENTS.md

@@ -21,7 +21,7 @@
 - 永远:`docs/overview.md`、`docs/architecture.md`、`docs/conventions.md`(第四节是硬性禁令清单)
 - 后端:再加 `docs/api.md`、`docs/backend/{data-model,auth,audit}.md`
 - 前端:再加 `docs/api.md`、`docs/frontend/frontend.md`
-- `README.md` 只是索引,具体规则以它指向的文档为准;README 里写的 `deploy/` 目录当前不存在。
+- `AGENTS.md` 已落初版,措辞可复核;根 `opencode.json` 只把 `docs/conventions.md` 与 `docs/overview.md` 作为常驻 instructions,其余文档按任务读。
 
 ## 硬性禁令(全部来自 `docs/conventions.md` 第四节,只摘最容易违反的)
 
@@ -35,12 +35,13 @@
 
 ## 当前代码状态(先看这里再动手,别假设已实现)
 
-仓库处于脚手架阶段:
+仓库处于脚手架阶段(TODO.md 第 0 章):
 
-- `backend/cmd/haven/main.go` 只有 `GET /api/health` 和硬编码 `r.Run(":8080")`,与文档要求的 `HAVEN_LISTEN_ADDR` 等环境变量配置不符——按 `docs/architecture.md` 第四节重写,不要参考现有写法。
-- `backend/internal/*` 全是空目录;`backend/migrations/` 为空。
-- `frontend/` 是 Vite `create-vue` 模板(`HelloWorld.vue`、`src/views|router|stores|api` 空目录);缺 `vue-router`、`pinia` 的实际接线。
-- `frontend/dist/` 是上次构建产物,被 gitignore,不要手改、不要提交。
+- `backend/cmd/haven/` 已有可启动入口:`main.go` / `config.go` / `logger.go`,读 `HAVEN_*` 环境变量、缺失即拒绝启动,只注册了 `GET /api/health`。**暂未接入 `internal/config`**(第 1 章会迁移)。
+- `backend/internal/*` 全是空目录;`backend/migrations/` 为空;`deploy/` 尚不存在。
+- `frontend/` 是清理过的 Vite 模板:`main.ts` 已接 `vue-router`(History,路由表暂空)与 `pinia`,`App.vue` 只渲染 `RouterView`;`src/{api,views,stores}` 是空目录。
+- `frontend/src/style.css` 只剩全局基础样式,页面样式未开始。
+- `frontend/dist/` 是构建产物,已被 gitignore,不要手改、不要提交。
 
 ## 常用命令
 

+ 9 - 9
TODO.md

@@ -3,7 +3,7 @@
 > 执行清单,不是事实来源。所有实现细节以 `docs/` 为准;本文件与 `docs/` 冲突时以 `docs/` 为准。
 > `docs/` 只读,需求变更请先改文档,再勾选这里的条目。
 
-当前阶段:**脚手架已建,业务代码未开始**(`main.go` 只有 `/api/health`,`internal/*` 为空目录,前端仍是 Vite 模板)。
+当前阶段:**第 0 章(脚手架清理)已完成**,下一步进入第 1 章(配置与数据库基座)。
 
 图例:`[ ]` 未做 · `[x]` 已完成 · `[~]` 部分完成
 
@@ -11,15 +11,15 @@
 
 ## 0. 清理脚手架(最先做,避免旧写法被模仿)
 
-- [ ] `backend/cmd/haven/main.go` 重写为读环境变量的入口(见 `architecture.md` 第四节、5.2/5.3):删除硬编码 `:8080`,改用 `HAVEN_LISTEN_ADDR`,接 `log/slog`
-- [ ] 删除 `frontend/src/components/HelloWorld.vue` 与模板样式(`style.css` 里的 demo 变量、`src/assets/{vue.svg,vite.svg,hero.png}`)
-- [ ] 删除 `frontend/README.md`(Vite 模板说明)与 `frontend/public/icons.svg` 等模板残留,保留 `favicon.svg` 或替换
-- [ ] `frontend/index.html`:`lang="zh-CN"`,标题改为 Haven
-- [ ] 接入 `vue-router`(History 模式)与 `pinia` 到 `main.ts`
-- [ ] 根目录 `.gitignore`(当前只有子目录里的,`backend/.gitignore` 里 `/backend/...` 这类路径是按仓库根写的,实际匹配不到)
+- [x] `backend/cmd/haven/main.go` 重写为读环境变量的入口(见 `architecture.md` 第四节、5.2/5.3):删除硬编码 `:8080`,改用 `HAVEN_LISTEN_ADDR`,接 `log/slog`
+- [x] 删除 `frontend/src/components/HelloWorld.vue` 与模板样式(`style.css` 里的 demo 变量、`src/assets/{vue.svg,vite.svg,hero.png}`)
+- [x] 删除 `frontend/README.md`(Vite 模板说明)与 `frontend/public/icons.svg` 等模板残留,保留 `favicon.svg` 或替换
+- [x] `frontend/index.html`:`lang="zh-CN"`,标题改为 Haven
+- [x] 接入 `vue-router`(History 模式)与 `pinia` 到 `main.ts`
+- [x] 根目录 `.gitignore`(当前只有子目录里的,`backend/.gitignore` 里 `/backend/...` 这类路径是按仓库根写的,实际匹配不到)
 
-- [ ] 根目录 `AGENTS.md`:写文档驱动规则、Git 规范、TODO 边界、技术栈锁定(**已有初版**,剩余是人工复核:重点核对“AI 只读不写 docs”和“禁止自动提交”两条措辞没被软化,并补上 TODO 边界)
-- [ ] 根目录 `opencode.json`:`instructions` 只加载 `docs/conventions.md` 和 `docs/overview.md`,其余文档按需手动喂,不全量加载(**尚未创建**)
+- [x] 根目录 `AGENTS.md`:写文档驱动规则、Git 规范、TODO 边界、技术栈锁定(已含“AI 只读不写 docs”“禁止自动提交”“按 TODO 顺序执行”三条)
+- [x] 根目录 `opencode.json`:`instructions` 只加载 `docs/conventions.md` 和 `docs/overview.md`,其余文档按需手动喂,不全量加载
 
 ## 1. 配置与数据库基座
 

+ 6 - 46
backend/.gitignore

@@ -1,49 +1,9 @@
-# ===== 环境与配置 =====
-.env
-.env.*
-!.env.example
-*.local
-
-# ===== 后端(Go)=====
-# 编译产物
-/backend/haven
-/backend/*.exe
-/backend/*.test
-/backend/cmd/**/*.test
+# 后端产物与临时文件(路径均相对 backend/)
+# 只忽略可执行文件。不要写裸 `haven`——那会连 cmd/haven 源码目录一起忽略
+/haven
+*.exe
+*.test
+cmd/**/*.test
 # 测试与覆盖率
 *.out
 coverage.*
-# go work 摘要(如不用 workspace 可不加)
-go.work.sum
-
-# ===== 前端(Vite / Node)=====
-node_modules/
-dist/
-dist-ssr/
-.vite/
-*.tsbuildinfo
-pnpm-debug.log*
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# ===== 构建与部署产物 =====
-/deploy/build/
-/deploy/out/
-
-# ===== 编辑器与 IDE =====
-.vscode/*
-!.vscode/extensions.json
-!.vscode/settings.json
-.idea/
-*.swp
-*.swo
-*~
-
-# ===== 操作系统 =====
-.DS_Store
-Thumbs.db
-
-# ===== 日志与临时文件 =====
-*.log
-/tmp/

+ 89 - 0
backend/cmd/haven/config.go

@@ -0,0 +1,89 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+	"strings"
+)
+
+// config 是后端启动所需的全部配置。
+//
+// 来源见 docs/architecture.md 第四节:全部通过环境变量提供,
+// 缺失必填项则拒绝启动。cookie 名、有效期、bcrypt cost 等常量
+// 不在配置里暴露,直接写在代码中。
+type config struct {
+	// 必填
+	ListenAddr   string // HAVEN_LISTEN_ADDR
+	DBDSN        string // HAVEN_DB_DSN
+	FrontendDir  string // HAVEN_FRONTEND_DIR
+	GogsBaseURL  string // HAVEN_GOGS_BASE_URL
+	CookieDomain string // HAVEN_COOKIE_DOMAIN
+
+	// 可选,带默认值
+	CookieSecure bool   // HAVEN_COOKIE_SECURE,默认 true
+	TotpIssuer   string // HAVEN_TOTP_ISSUER,默认 Haven
+	LogLevel     string // HAVEN_LOG_LEVEL,默认 info
+}
+
+// loadConfig 从环境变量读取并校验配置。
+// 任一必填项缺失或取值非法都返回错误,由调用方拒绝启动。
+func loadConfig() (config, error) {
+	var cfg config
+	var missing []string
+
+	cfg.ListenAddr = strings.TrimSpace(os.Getenv("HAVEN_LISTEN_ADDR"))
+	if cfg.ListenAddr == "" {
+		missing = append(missing, "HAVEN_LISTEN_ADDR")
+	}
+
+	cfg.DBDSN = strings.TrimSpace(os.Getenv("HAVEN_DB_DSN"))
+	if cfg.DBDSN == "" {
+		missing = append(missing, "HAVEN_DB_DSN")
+	}
+
+	cfg.FrontendDir = strings.TrimSpace(os.Getenv("HAVEN_FRONTEND_DIR"))
+	if cfg.FrontendDir == "" {
+		missing = append(missing, "HAVEN_FRONTEND_DIR")
+	}
+
+	cfg.GogsBaseURL = strings.TrimSpace(strings.TrimSuffix(os.Getenv("HAVEN_GOGS_BASE_URL"), "/"))
+	if cfg.GogsBaseURL == "" {
+		missing = append(missing, "HAVEN_GOGS_BASE_URL")
+	}
+
+	cfg.CookieDomain = strings.TrimSpace(os.Getenv("HAVEN_COOKIE_DOMAIN"))
+	if cfg.CookieDomain == "" {
+		missing = append(missing, "HAVEN_COOKIE_DOMAIN")
+	}
+
+	if len(missing) > 0 {
+		return config{}, fmt.Errorf("缺少必填环境变量: %s", strings.Join(missing, ", "))
+	}
+
+	cfg.CookieSecure = true
+	if raw := strings.TrimSpace(os.Getenv("HAVEN_COOKIE_SECURE")); raw != "" {
+		v, err := strconv.ParseBool(raw)
+		if err != nil {
+			return config{}, fmt.Errorf("HAVEN_COOKIE_SECURE 取值非法 %q,应为 true 或 false", raw)
+		}
+		cfg.CookieSecure = v
+	}
+
+	cfg.TotpIssuer = strings.TrimSpace(os.Getenv("HAVEN_TOTP_ISSUER"))
+	if cfg.TotpIssuer == "" {
+		cfg.TotpIssuer = "Haven"
+	}
+
+	cfg.LogLevel = strings.ToLower(strings.TrimSpace(os.Getenv("HAVEN_LOG_LEVEL")))
+	if cfg.LogLevel == "" {
+		cfg.LogLevel = "info"
+	}
+	switch cfg.LogLevel {
+	case "debug", "info", "warn", "error":
+	default:
+		return config{}, fmt.Errorf("HAVEN_LOG_LEVEL 取值非法 %q,应为 debug / info / warn / error", cfg.LogLevel)
+	}
+
+	return cfg, nil
+}

+ 26 - 0
backend/cmd/haven/logger.go

@@ -0,0 +1,26 @@
+package main
+
+import (
+	"log/slog"
+	"os"
+)
+
+// newLogger 按配置的日志级别构造结构化日志器,输出到 stderr。
+//
+// 见 docs/architecture.md 5.7:日志走标准库 log/slog,
+// 不得记录请求体、笔记内容、密码或 Token。
+func newLogger(level string) *slog.Logger {
+	var lv slog.Level
+	switch level {
+	case "debug":
+		lv = slog.LevelDebug
+	case "warn":
+		lv = slog.LevelWarn
+	case "error":
+		lv = slog.LevelError
+	default:
+		lv = slog.LevelInfo
+	}
+
+	return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: lv}))
+}

+ 36 - 6
backend/cmd/haven/main.go

@@ -1,11 +1,41 @@
 package main
 
-import "github.com/gin-gonic/gin"
+import (
+	"log/slog"
+	"os"
 
+	"github.com/gin-gonic/gin"
+)
+
+// main 是后端入口。
+//
+// 配置全部来自环境变量,见 docs/architecture.md 第四节;
+// 本期只完成启动骨架:读取配置、初始化日志、注册健康检查。
+// 数据库连接、鉴权中间件与各领域路由按 TODO.md 后续条目接入。
 func main() {
-    r := gin.Default()
-    r.GET("/api/health", func(c *gin.Context) {
-        c.JSON(200, gin.H{"status": "ok"})
-    })
-    r.Run(":8080")
+	if err := run(); err != nil {
+		slog.Error("服务启动失败", "err", err)
+		os.Exit(1)
+	}
+}
+
+func run() error {
+	cfg, err := loadConfig()
+	if err != nil {
+		return err
+	}
+
+	logger := newLogger(cfg.LogLevel)
+	slog.SetDefault(logger)
+
+	gin.SetMode(gin.ReleaseMode)
+	r := gin.New()
+	r.Use(gin.Recovery())
+
+	r.GET("/api/health", func(c *gin.Context) {
+		c.JSON(200, gin.H{"status": "ok"})
+	})
+
+	logger.Info("后端启动", "listen_addr", cfg.ListenAddr)
+	return r.Run(cfg.ListenAddr)
 }

+ 0 - 5
frontend/README.md

@@ -1,5 +0,0 @@
-# Vue 3 + TypeScript + Vite
-
-This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
-
-Learn more about the recommended Project Setup and IDE Support in the [Vue Docs TypeScript Guide](https://vuejs.org/guide/typescript/overview.html#project-setup).

+ 2 - 2
frontend/index.html

@@ -1,10 +1,10 @@
 <!doctype html>
-<html lang="en">
+<html lang="zh-CN">
   <head>
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>frontend</title>
+    <title>Haven</title>
   </head>
   <body>
     <div id="app"></div>

+ 0 - 24
frontend/public/icons.svg

@@ -1,24 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg">
-  <symbol id="bluesky-icon" viewBox="0 0 16 17">
-    <g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
-    <defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
-  </symbol>
-  <symbol id="discord-icon" viewBox="0 0 20 19">
-    <path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
-  </symbol>
-  <symbol id="documentation-icon" viewBox="0 0 21 20">
-    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
-    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
-    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
-  </symbol>
-  <symbol id="github-icon" viewBox="0 0 19 19">
-    <path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
-  </symbol>
-  <symbol id="social-icon" viewBox="0 0 20 20">
-    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
-    <path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
-  </symbol>
-  <symbol id="x-icon" viewBox="0 0 19 19">
-    <path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
-  </symbol>
-</svg>

+ 2 - 2
frontend/src/App.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
-import HelloWorld from './components/HelloWorld.vue'
+import { RouterView } from 'vue-router'
 </script>
 
 <template>
-  <HelloWorld />
+  <RouterView />
 </template>

BIN
frontend/src/assets/hero.png


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 0 - 0
frontend/src/assets/vite.svg


+ 0 - 1
frontend/src/assets/vue.svg

@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

+ 0 - 95
frontend/src/components/HelloWorld.vue

@@ -1,95 +0,0 @@
-<script setup lang="ts">
-import { ref } from 'vue'
-import heroImg from '../assets/hero.png'
-import viteLogo from '../assets/vite.svg'
-import vueLogo from '../assets/vue.svg'
-
-const count = ref(0)
-</script>
-
-<template>
-  <section id="center">
-    <div class="hero">
-      <img :src="heroImg" class="base" width="170" height="179" alt="" />
-      <img :src="vueLogo" class="framework" alt="Vue logo" />
-      <img :src="viteLogo" class="vite" alt="Vite logo" />
-    </div>
-    <div>
-      <h1>Get started</h1>
-      <p>Edit <code>src/App.vue</code> and save to test <code>HMR</code></p>
-    </div>
-    <button type="button" class="counter" @click="count++">
-      Count is {{ count }}
-    </button>
-  </section>
-
-  <div class="ticks"></div>
-
-  <section id="next-steps">
-    <div id="docs">
-      <svg class="icon" role="presentation" aria-hidden="true">
-        <use href="/icons.svg#documentation-icon"></use>
-      </svg>
-      <h2>Documentation</h2>
-      <p>Your questions, answered</p>
-      <ul>
-        <li>
-          <a href="https://vite.dev/" target="_blank">
-            <img class="logo" :src="viteLogo" alt="" />
-            Explore Vite
-          </a>
-        </li>
-        <li>
-          <a href="https://vuejs.org/" target="_blank">
-            <img class="button-icon" :src="vueLogo" alt="" />
-            Learn more
-          </a>
-        </li>
-      </ul>
-    </div>
-    <div id="social">
-      <svg class="icon" role="presentation" aria-hidden="true">
-        <use href="/icons.svg#social-icon"></use>
-      </svg>
-      <h2>Connect with us</h2>
-      <p>Join the Vite community</p>
-      <ul>
-        <li>
-          <a href="https://github.com/vitejs/vite" target="_blank">
-            <svg class="button-icon" role="presentation" aria-hidden="true">
-              <use href="/icons.svg#github-icon"></use>
-            </svg>
-            GitHub
-          </a>
-        </li>
-        <li>
-          <a href="https://chat.vite.dev/" target="_blank">
-            <svg class="button-icon" role="presentation" aria-hidden="true">
-              <use href="/icons.svg#discord-icon"></use>
-            </svg>
-            Discord
-          </a>
-        </li>
-        <li>
-          <a href="https://x.com/vite_js" target="_blank">
-            <svg class="button-icon" role="presentation" aria-hidden="true">
-              <use href="/icons.svg#x-icon"></use>
-            </svg>
-            X.com
-          </a>
-        </li>
-        <li>
-          <a href="https://bsky.app/profile/vite.dev" target="_blank">
-            <svg class="button-icon" role="presentation" aria-hidden="true">
-              <use href="/icons.svg#bluesky-icon"></use>
-            </svg>
-            Bluesky
-          </a>
-        </li>
-      </ul>
-    </div>
-  </section>
-
-  <div class="ticks"></div>
-  <section id="spacer"></section>
-</template>

+ 7 - 1
frontend/src/main.ts

@@ -1,5 +1,11 @@
 import { createApp } from 'vue'
+import { createPinia } from 'pinia'
 import './style.css'
 import App from './App.vue'
+import router from './router'
 
-createApp(App).mount('#app')
+const app = createApp(App)
+
+app.use(createPinia())
+app.use(router)
+app.mount('#app')

+ 12 - 0
frontend/src/router/index.ts

@@ -0,0 +1,12 @@
+import { createRouter, createWebHistory, type RouteRecordRaw } from 'vue-router'
+
+export const routes: RouteRecordRaw[] = []
+
+// History 模式,见 docs/architecture.md 第一节与 docs/frontend/frontend.md。
+// 公开域与私有域的页面在后续任务中按 window.location.hostname 分表加入。
+const router = createRouter({
+  history: createWebHistory(),
+  routes,
+})
+
+export default router

+ 7 - 287
frontend/src/style.css

@@ -1,296 +1,16 @@
-:root {
-  --text: #6b6375;
-  --text-h: #08060d;
-  --bg: #fff;
-  --border: #e5e4e7;
-  --code-bg: #f4f3ec;
-  --accent: #aa3bff;
-  --accent-bg: rgba(170, 59, 255, 0.1);
-  --accent-border: rgba(170, 59, 255, 0.5);
-  --social-bg: rgba(244, 243, 236, 0.5);
-  --shadow:
-    rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
+/* Haven 全局样式。具体页面样式在后续任务中补充。 */
 
-  --sans: system-ui, 'Segoe UI', Roboto, sans-serif;
-  --heading: system-ui, 'Segoe UI', Roboto, sans-serif;
-  --mono: ui-monospace, Consolas, monospace;
+:root {
+  --font-sans: system-ui, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
+  --font-mono: ui-monospace, Consolas, monospace;
 
-  font: 18px/145% var(--sans);
-  letter-spacing: 0.18px;
-  color-scheme: light dark;
-  color: var(--text);
-  background: var(--bg);
-  font-synthesis: none;
+  font-family: var(--font-sans);
+  font-size: 16px;
+  line-height: 1.5;
   text-rendering: optimizeLegibility;
   -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-
-  @media (max-width: 1024px) {
-    font-size: 16px;
-  }
-}
-
-@media (prefers-color-scheme: dark) {
-  :root {
-    --text: #9ca3af;
-    --text-h: #f3f4f6;
-    --bg: #16171d;
-    --border: #2e303a;
-    --code-bg: #1f2028;
-    --accent: #c084fc;
-    --accent-bg: rgba(192, 132, 252, 0.15);
-    --accent-border: rgba(192, 132, 252, 0.5);
-    --social-bg: rgba(47, 48, 58, 0.5);
-    --shadow:
-      rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
-  }
-
-  #social .button-icon {
-    filter: invert(1) brightness(2);
-  }
 }
 
 body {
   margin: 0;
 }
-
-h1,
-h2 {
-  font-family: var(--heading);
-  font-weight: 500;
-  color: var(--text-h);
-}
-
-h1 {
-  font-size: 56px;
-  letter-spacing: -1.68px;
-  margin: 32px 0;
-  @media (max-width: 1024px) {
-    font-size: 36px;
-    margin: 20px 0;
-  }
-}
-h2 {
-  font-size: 24px;
-  line-height: 118%;
-  letter-spacing: -0.24px;
-  margin: 0 0 8px;
-  @media (max-width: 1024px) {
-    font-size: 20px;
-  }
-}
-p {
-  margin: 0;
-}
-
-code,
-.counter {
-  font-family: var(--mono);
-  display: inline-flex;
-  border-radius: 4px;
-  color: var(--text-h);
-}
-
-code {
-  font-size: 15px;
-  line-height: 135%;
-  padding: 4px 8px;
-  background: var(--code-bg);
-}
-
-.counter {
-  font-size: 16px;
-  padding: 5px 10px;
-  border-radius: 5px;
-  color: var(--accent);
-  background: var(--accent-bg);
-  border: 2px solid transparent;
-  transition: border-color 0.3s;
-  margin-bottom: 24px;
-
-  &:hover {
-    border-color: var(--accent-border);
-  }
-  &:focus-visible {
-    outline: 2px solid var(--accent);
-    outline-offset: 2px;
-  }
-}
-
-.hero {
-  position: relative;
-
-  .base,
-  .framework,
-  .vite {
-    inset-inline: 0;
-    margin: 0 auto;
-  }
-
-  .base {
-    width: 170px;
-    position: relative;
-    z-index: 0;
-  }
-
-  .framework,
-  .vite {
-    position: absolute;
-  }
-
-  .framework {
-    z-index: 1;
-    top: 34px;
-    height: 28px;
-    transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
-      scale(1.4);
-  }
-
-  .vite {
-    z-index: 0;
-    top: 107px;
-    height: 26px;
-    width: auto;
-    transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
-      scale(0.8);
-  }
-}
-
-#app {
-  width: 1126px;
-  max-width: 100%;
-  margin: 0 auto;
-  text-align: center;
-  border-inline: 1px solid var(--border);
-  min-height: 100svh;
-  display: flex;
-  flex-direction: column;
-  box-sizing: border-box;
-}
-
-#center {
-  display: flex;
-  flex-direction: column;
-  gap: 25px;
-  place-content: center;
-  place-items: center;
-  flex-grow: 1;
-
-  @media (max-width: 1024px) {
-    padding: 32px 20px 24px;
-    gap: 18px;
-  }
-}
-
-#next-steps {
-  display: flex;
-  border-top: 1px solid var(--border);
-  text-align: left;
-
-  & > div {
-    flex: 1 1 0;
-    padding: 32px;
-    @media (max-width: 1024px) {
-      padding: 24px 20px;
-    }
-  }
-
-  .icon {
-    margin-bottom: 16px;
-    width: 22px;
-    height: 22px;
-  }
-
-  @media (max-width: 1024px) {
-    flex-direction: column;
-    text-align: center;
-  }
-}
-
-#docs {
-  border-right: 1px solid var(--border);
-
-  @media (max-width: 1024px) {
-    border-right: none;
-    border-bottom: 1px solid var(--border);
-  }
-}
-
-#next-steps ul {
-  list-style: none;
-  padding: 0;
-  display: flex;
-  gap: 8px;
-  margin: 32px 0 0;
-
-  .logo {
-    height: 18px;
-  }
-
-  a {
-    color: var(--text-h);
-    font-size: 16px;
-    border-radius: 6px;
-    background: var(--social-bg);
-    display: flex;
-    padding: 6px 12px;
-    align-items: center;
-    gap: 8px;
-    text-decoration: none;
-    transition: box-shadow 0.3s;
-
-    &:hover {
-      box-shadow: var(--shadow);
-    }
-    .button-icon {
-      height: 18px;
-      width: 18px;
-    }
-  }
-
-  @media (max-width: 1024px) {
-    margin-top: 20px;
-    flex-wrap: wrap;
-    justify-content: center;
-
-    li {
-      flex: 1 1 calc(50% - 8px);
-    }
-
-    a {
-      width: 100%;
-      justify-content: center;
-      box-sizing: border-box;
-    }
-  }
-}
-
-#spacer {
-  height: 88px;
-  border-top: 1px solid var(--border);
-  @media (max-width: 1024px) {
-    height: 48px;
-  }
-}
-
-.ticks {
-  position: relative;
-  width: 100%;
-
-  &::before,
-  &::after {
-    content: '';
-    position: absolute;
-    top: -4.5px;
-    border: 5px solid transparent;
-  }
-
-  &::before {
-    left: 0;
-    border-left-color: var(--border);
-  }
-  &::after {
-    right: 0;
-    border-right-color: var(--border);
-  }
-}

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác

苏ICP备2026082406号
苏公网安备 待审核