feat(email): 初始化自建邮箱 MVP
- 新增 Go + SQLite 后端,支持登录、域名、邮箱、别名、联系人、规则、统计与邮件收发。 - 新增 Webmail 前端,支持多邮箱切换、邮件列表/阅读/写信、附件、搜索、主题切换与个人中心。 - 新增 Docker Compose 部署骨架,补充 Postfix、Dovecot、OpenDKIM、Nginx 配置与部署说明。
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import fs from "node:fs"
|
||||
import path from "node:path"
|
||||
|
||||
const root = process.cwd()
|
||||
const srcDir = path.join(root, "src")
|
||||
|
||||
const ignoredSegments = [
|
||||
`${path.sep}components${path.sep}ui${path.sep}`,
|
||||
`${path.sep}dist${path.sep}`,
|
||||
]
|
||||
|
||||
// Business code must use shadcn/ui wrappers for visible UI primitives.
|
||||
// Semantic/layout tags such as div, span, form, header, main, section and a are allowed.
|
||||
const forbidden = [
|
||||
{ tag: "button", replacement: "@/components/ui/button Button" },
|
||||
{ tag: "input", replacement: "@/components/ui/input Input" },
|
||||
{ tag: "textarea", replacement: "@/components/ui/textarea Textarea" },
|
||||
{ tag: "select", replacement: "@/components/ui/select Select" },
|
||||
{ tag: "table", replacement: "@/components/ui/table Table" },
|
||||
{ tag: "thead", replacement: "@/components/ui/table TableHeader" },
|
||||
{ tag: "tbody", replacement: "@/components/ui/table TableBody" },
|
||||
{ tag: "tr", replacement: "@/components/ui/table TableRow" },
|
||||
{ tag: "th", replacement: "@/components/ui/table TableHead" },
|
||||
{ tag: "td", replacement: "@/components/ui/table TableCell" },
|
||||
{ tag: "dialog", replacement: "@/components/ui/dialog Dialog" },
|
||||
{ tag: "aside", replacement: "@/components/ui/sidebar Sidebar" },
|
||||
]
|
||||
|
||||
// Product UI rule: do not add small explanatory subtitle text under titles.
|
||||
// Keep screens clean: use titles, labels, badges and actionable controls only.
|
||||
const forbiddenComponents = [
|
||||
{ name: "CardDescription", reason: "不要在卡片标题下添加说明性小字" },
|
||||
{ name: "DialogDescription", reason: "不要在弹窗标题下添加说明性小字" },
|
||||
{ name: "SheetDescription", reason: "不要在抽屉标题下添加说明性小字" },
|
||||
]
|
||||
|
||||
const forbiddenClassPatterns = [
|
||||
{ pattern: /\bbg-gradient-[^\s"`'}]+/, reason: "不要使用渐变背景,保持 shadcn neutral 极简风格" },
|
||||
{ pattern: /\b(?:from|via|to)-blue-[^\s"`'}]+/, reason: "不要使用蓝色渐变色阶,保持 neutral/black 视觉" },
|
||||
{ pattern: /\b(?:bg|text|border|ring)-blue-[^\s"`'}]+/, reason: "不要使用蓝色品牌色,保持 neutral/black 视觉" },
|
||||
{ pattern: /\bshadow-(?:lg|xl|2xl|primary[^\s"`'}]*)/, reason: "不要使用重阴影或营销感阴影,保持 shadcn 默认克制风格" },
|
||||
]
|
||||
|
||||
function walk(dir) {
|
||||
const out = []
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) out.push(...walk(full))
|
||||
else if (/\.(tsx|jsx)$/.test(entry.name)) out.push(full)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const violations = []
|
||||
for (const file of walk(srcDir)) {
|
||||
if (ignoredSegments.some((segment) => file.includes(segment))) continue
|
||||
const rel = path.relative(root, file)
|
||||
const content = fs.readFileSync(file, "utf8")
|
||||
const lines = content.split(/\r?\n/)
|
||||
lines.forEach((line, index) => {
|
||||
for (const rule of forbidden) {
|
||||
const re = new RegExp(`<${rule.tag}(\\s|>|/)`)
|
||||
if (re.test(line)) {
|
||||
violations.push({ file: rel, line: index + 1, tag: rule.tag, replacement: rule.replacement, code: line.trim() })
|
||||
}
|
||||
}
|
||||
for (const rule of forbiddenComponents) {
|
||||
const re = new RegExp(`\\b${rule.name}\\b`)
|
||||
if (re.test(line)) {
|
||||
violations.push({ file: rel, line: index + 1, tag: rule.name, replacement: rule.reason, code: line.trim() })
|
||||
}
|
||||
}
|
||||
for (const rule of forbiddenClassPatterns) {
|
||||
if (rule.pattern.test(line)) {
|
||||
violations.push({ file: rel, line: index + 1, tag: "visual-style", replacement: rule.reason, code: line.trim() })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("\nshadcn/ui rule failed: business TSX must not use native UI primitives.\n")
|
||||
for (const v of violations) {
|
||||
console.error(`${v.file}:${v.line} <${v.tag}> -> use ${v.replacement}`)
|
||||
console.error(` ${v.code}`)
|
||||
}
|
||||
console.error("\nAllowed: native semantic/layout tags like div, span, form, header, main, section, a. Official shadcn components under src/components/ui are exempt.\n")
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log("shadcn/ui rule passed: no native UI primitives found in business TSX.")
|
||||
Reference in New Issue
Block a user