mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-03 15:06:36 +08:00
嵌入与邮箱
This commit is contained in:
@@ -240,7 +240,7 @@ export const apiSections: PolicySection[] = [
|
||||
code={`{
|
||||
"error_msg": "",
|
||||
"data": {
|
||||
"site_name": "Antigravity Project",
|
||||
"site_name": "Wavelet",
|
||||
"registration_enabled": true,
|
||||
"password_login_enabled": true,
|
||||
"password_register_enabled": true,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Info,
|
||||
Loader2,
|
||||
Lock,
|
||||
Mail,
|
||||
Pencil,
|
||||
Plus,
|
||||
Server,
|
||||
@@ -26,6 +27,7 @@ import {Switch} from "@/components/ui/switch"
|
||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs"
|
||||
import {Input} from "@/components/ui/input"
|
||||
import {Label} from "@/components/ui/label"
|
||||
import {Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle} from "@/components/ui/dialog"
|
||||
import {useAuth} from "@/components/providers/auth-provider"
|
||||
import {AuthSourceModal} from "@/components/common/settings/auth-source-modal"
|
||||
import {AdminService, apiConfig} from "@/lib/services"
|
||||
@@ -94,6 +96,15 @@ export function SecurityMain() {
|
||||
const [capTokenTTL, setCapTokenTTL] = useState("")
|
||||
const [capAutoSolve, setCapAutoSolve] = useState(true)
|
||||
const [serverAddress, setServerAddress] = useState("")
|
||||
const [smtpHost, setSmtpHost] = useState("")
|
||||
const [smtpPort, setSmtpPort] = useState("")
|
||||
const [smtpUsername, setSmtpUsername] = useState("")
|
||||
const [smtpPassword, setSmtpPassword] = useState("")
|
||||
const [smtpTestOpen, setSmtpTestOpen] = useState(false)
|
||||
const [smtpTestTo, setSmtpTestTo] = useState("")
|
||||
const [smtpTestLog, setSmtpTestLog] = useState("")
|
||||
const [smtpTestSuccess, setSmtpTestSuccess] = useState<boolean | null>(null)
|
||||
const [smtpTestError, setSmtpTestError] = useState("")
|
||||
|
||||
const systemConfigsQuery = useQuery({
|
||||
queryKey: ["admin", "system-configs"],
|
||||
@@ -128,6 +139,10 @@ export function SecurityMain() {
|
||||
setCapTokenTTL(cfgMap["cap_token_ttl_seconds"]?.value || "1200")
|
||||
setCapAutoSolve(cfgMap["cap_auto_solve"]?.value !== "false")
|
||||
setServerAddress(cfgMap["server_address"]?.value || "")
|
||||
setSmtpHost(cfgMap["smtp_host"]?.value || "")
|
||||
setSmtpPort(cfgMap["smtp_port"]?.value || "587")
|
||||
setSmtpUsername(cfgMap["smtp_username"]?.value || "")
|
||||
setSmtpPassword(cfgMap["smtp_password"]?.value || "")
|
||||
}
|
||||
}, [systemConfigsQuery.data])
|
||||
|
||||
@@ -238,6 +253,86 @@ export function SecurityMain() {
|
||||
saveSystemMutation.mutate()
|
||||
}
|
||||
|
||||
const saveSmtpMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const updates = [
|
||||
{ key: "smtp_host", value: smtpHost },
|
||||
{ key: "smtp_port", value: smtpPort },
|
||||
{ key: "smtp_username", value: smtpUsername },
|
||||
{ key: "smtp_password", value: smtpPassword },
|
||||
]
|
||||
|
||||
for (const update of updates) {
|
||||
const currentCfg = configs[update.key]
|
||||
if (update.key === "smtp_password" && (update.value === "" || update.value === "******")) {
|
||||
// If already configured and sent empty or mask, skip updating it (keep existing)
|
||||
if (currentCfg && currentCfg.value === "******") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
await AdminService.updateSystemConfig(update.key, {
|
||||
value: update.value,
|
||||
description: currentCfg?.description || "",
|
||||
})
|
||||
}
|
||||
},
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: ["admin", "system-configs"] })
|
||||
toast.success("SMTP 邮件配置已成功保存")
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
toast.error(error.message || "保存配置失败")
|
||||
},
|
||||
})
|
||||
|
||||
const handleSmtpSave = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
saveSmtpMutation.mutate()
|
||||
}
|
||||
|
||||
const testSmtpMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
setSmtpTestLog("正在发起连接测试...\n")
|
||||
setSmtpTestSuccess(null)
|
||||
setSmtpTestError("")
|
||||
|
||||
const res = await AdminService.testSMTP({
|
||||
smtp_host: smtpHost,
|
||||
smtp_port: parseInt(smtpPort, 10) || 587,
|
||||
smtp_username: smtpUsername,
|
||||
smtp_password: smtpPassword,
|
||||
to: smtpTestTo,
|
||||
})
|
||||
return res
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
setSmtpTestLog(data.log)
|
||||
if (data.success) {
|
||||
setSmtpTestSuccess(true)
|
||||
toast.success("测试邮件发送成功")
|
||||
} else {
|
||||
setSmtpTestSuccess(false)
|
||||
setSmtpTestError(data.error || "发送失败,请检查配置和日志。")
|
||||
toast.error("测试邮件发送失败")
|
||||
}
|
||||
},
|
||||
onError: (error: Error) => {
|
||||
setSmtpTestSuccess(false)
|
||||
setSmtpTestError(error.message || "请求发送失败")
|
||||
setSmtpTestLog((prev) => prev + `\n[请求错误] ${error.message}\n`)
|
||||
toast.error(error.message || "测试请求发送失败")
|
||||
},
|
||||
})
|
||||
|
||||
const handleSmtpTestSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!smtpTestTo) {
|
||||
toast.error("请输入目标邮箱地址")
|
||||
return
|
||||
}
|
||||
testSmtpMutation.mutate()
|
||||
}
|
||||
|
||||
if (loading || !user || !user.is_admin) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
@@ -567,6 +662,7 @@ export function SecurityMain() {
|
||||
<TabsContent value="operation" />
|
||||
<TabsContent value="system" className="pt-4">
|
||||
<div className="space-y-6">
|
||||
{/* 通用设置 */}
|
||||
<Card className="border border-dashed shadow-sm">
|
||||
<CardHeader className="border-b border-dashed pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -614,6 +710,106 @@ export function SecurityMain() {
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* SMTP 邮件设置 */}
|
||||
<Card className="border border-dashed shadow-sm">
|
||||
<CardHeader className="border-b border-dashed pb-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="p-1.5 rounded-lg bg-indigo-500/10 text-indigo-500">
|
||||
<Mail className="size-4" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-base font-semibold">SMTP 邮件设置</CardTitle>
|
||||
<CardDescription className="text-xs">配置系统的邮件发送服务 (SMTP)</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<form onSubmit={handleSmtpSave} className="space-y-6">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="smtp_host" className="text-xs font-semibold">SMTP 服务器地址</Label>
|
||||
<Input
|
||||
id="smtp_host"
|
||||
type="text"
|
||||
value={smtpHost}
|
||||
onChange={(e) => setSmtpHost(e.target.value)}
|
||||
placeholder="例如: smtp.example.com"
|
||||
className="bg-card border-dashed text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="smtp_port" className="text-xs font-semibold">SMTP 端口</Label>
|
||||
<Input
|
||||
id="smtp_port"
|
||||
type="number"
|
||||
value={smtpPort}
|
||||
onChange={(e) => setSmtpPort(e.target.value)}
|
||||
placeholder="例如: 587 或 465"
|
||||
className="bg-card border-dashed text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="smtp_username" className="text-xs font-semibold">SMTP 账户</Label>
|
||||
<Input
|
||||
id="smtp_username"
|
||||
type="text"
|
||||
value={smtpUsername}
|
||||
onChange={(e) => setSmtpUsername(e.target.value)}
|
||||
placeholder="例如: sender@example.com"
|
||||
className="bg-card border-dashed text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="smtp_password" className="text-xs font-semibold">SMTP 访问凭证</Label>
|
||||
<Input
|
||||
id="smtp_password"
|
||||
type="password"
|
||||
value={smtpPassword}
|
||||
onChange={(e) => setSmtpPassword(e.target.value)}
|
||||
placeholder={configs["smtp_password"]?.value === "******" ? "•••••• (已配置,留空或输入新值)" : "输入凭证密码"}
|
||||
className="bg-card border-dashed text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 pt-4 border-t border-dashed">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSmtpTestOpen(true)
|
||||
setSmtpTestTo("")
|
||||
setSmtpTestLog("")
|
||||
setSmtpTestSuccess(null)
|
||||
setSmtpTestError("")
|
||||
}}
|
||||
disabled={saveSmtpMutation.isPending}
|
||||
>
|
||||
测试发件
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
disabled={saveSmtpMutation.isPending}
|
||||
>
|
||||
{saveSmtpMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-1.5 size-3.5 animate-spin" />
|
||||
保存中...
|
||||
</>
|
||||
) : (
|
||||
"保存配置"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</TabsContent>
|
||||
<TabsContent value="status" className="pt-4">
|
||||
@@ -677,6 +873,80 @@ export function SecurityMain() {
|
||||
await authSourcesQuery.refetch()
|
||||
}}
|
||||
/>
|
||||
|
||||
<Dialog open={smtpTestOpen} onOpenChange={setSmtpTestOpen}>
|
||||
<DialogContent className="max-w-lg border border-dashed">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-base font-semibold">SMTP 发件测试</DialogTitle>
|
||||
<DialogDescription className="text-xs">
|
||||
输入接收测试邮件的邮箱地址。系统将使用您在表单中当前填写的 SMTP 配置进行发件测试。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSmtpTestSubmit} className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="smtp_test_to" className="text-xs font-semibold">目标邮箱地址</Label>
|
||||
<Input
|
||||
id="smtp_test_to"
|
||||
type="email"
|
||||
required
|
||||
value={smtpTestTo}
|
||||
onChange={(e) => setSmtpTestTo(e.target.value)}
|
||||
placeholder="例如: receiver@example.com"
|
||||
className="bg-card border-dashed text-xs"
|
||||
disabled={testSmtpMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{smtpTestLog && (
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-semibold">连接与传输日志</Label>
|
||||
<pre className="bg-zinc-950 text-zinc-50 font-mono p-4 rounded-lg text-[10px] h-60 overflow-y-auto whitespace-pre-wrap border border-dashed border-zinc-800 leading-relaxed">
|
||||
{smtpTestLog}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{smtpTestSuccess === true && (
|
||||
<div className="p-3 rounded-lg border border-dashed border-emerald-500/30 bg-emerald-500/5 text-emerald-500 text-xs">
|
||||
测试成功!邮件已顺利发出。
|
||||
</div>
|
||||
)}
|
||||
|
||||
{smtpTestSuccess === false && (
|
||||
<div className="p-3 rounded-lg border border-dashed border-rose-500/30 bg-rose-500/5 text-rose-500 text-xs break-all">
|
||||
测试失败:{smtpTestError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<DialogFooter className="gap-2 sm:gap-0 border-t border-dashed pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setSmtpTestOpen(false)}
|
||||
disabled={testSmtpMutation.isPending}
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
size="sm"
|
||||
disabled={testSmtpMutation.isPending}
|
||||
>
|
||||
{testSmtpMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="mr-1.5 size-3.5 animate-spin" />
|
||||
测试中...
|
||||
</>
|
||||
) : (
|
||||
"开始测试"
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -117,6 +117,16 @@ export class AdminService extends BaseService {
|
||||
return this.put<void>(`/system-configs/${ key }`, request);
|
||||
}
|
||||
|
||||
static async testSMTP(request: {
|
||||
smtp_host: string;
|
||||
smtp_port: number;
|
||||
smtp_username: string;
|
||||
smtp_password: string;
|
||||
to: string;
|
||||
}): Promise<{ success: boolean; log: string; error: string }> {
|
||||
return this.post<{ success: boolean; log: string; error: string }>('/system-configs/smtp/test', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除系统配置
|
||||
* @param key - 配置键
|
||||
|
||||
@@ -1,79 +1,11 @@
|
||||
"use server"
|
||||
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import type { Theme } from "./types"
|
||||
import { THEME_CONFIG } from "./config"
|
||||
|
||||
/**
|
||||
* 从 CSS 片段中解析 CSS 变量
|
||||
* @param cssSection - CSS 代码片段
|
||||
* @returns CSS 变量键值对映射
|
||||
*/
|
||||
function parseCSSVariables(cssSection: string): Record<string, string> {
|
||||
const variables: Record<string, string> = {}
|
||||
const regex = /--([a-z0-9-]+):\s*([^;]+);/g
|
||||
|
||||
let match
|
||||
while ((match = regex.exec(cssSection)) !== null) {
|
||||
variables[match[1]] = match[2].trim()
|
||||
}
|
||||
|
||||
return variables
|
||||
}
|
||||
import type {Theme} from "./types"
|
||||
import themesData from "./themes.json"
|
||||
|
||||
/**
|
||||
* 获取所有可用主题
|
||||
* 通过解析 CSS 文件获取主题列表和颜色配置
|
||||
* @returns 主题列表,按默认主题优先,其余按名称排序
|
||||
* 从预生成的 themes.json 中直接读取主题列表和颜色配置
|
||||
* @returns 主题列表,已按默认主题优先且其余按名称排序
|
||||
*/
|
||||
export async function getAvailableThemes(): Promise<Theme[]> {
|
||||
try {
|
||||
const styleDir = path.join(process.cwd(), THEME_CONFIG.styleDir)
|
||||
const files = await fs.readdir(styleDir)
|
||||
|
||||
const themes = await Promise.all(
|
||||
files
|
||||
.filter((file) => file.endsWith(".css"))
|
||||
.map(async (file) => {
|
||||
/* 生成主题名称 */
|
||||
const name = file === "default.css"
|
||||
? "Default"
|
||||
: file
|
||||
.replace(".css", "")
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ")
|
||||
|
||||
const content = await fs.readFile(path.join(styleDir, file), "utf-8")
|
||||
|
||||
/* 从 :root 解析明亮模式颜色 */
|
||||
const rootSection = content.match(/:root\s*\{([^}]+)\}/s)?.[1] || ""
|
||||
const lightColors = parseCSSVariables(rootSection)
|
||||
|
||||
/* 从 .dark 解析暗色模式颜色 */
|
||||
const darkSection = content.match(/\.dark\s*\{([^}]+)\}/s)?.[1] || ""
|
||||
const darkColors = parseCSSVariables(darkSection)
|
||||
|
||||
return {
|
||||
id: file,
|
||||
name,
|
||||
colors: {
|
||||
light: lightColors,
|
||||
dark: darkColors,
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
/* 默认主题排在最前面,其余按名称排序 */
|
||||
return themes.sort((a, b) => {
|
||||
if (a.id === "default.css") return -1
|
||||
if (b.id === "default.css") return 1
|
||||
return a.name.localeCompare(b.name)
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to parse themes:", error)
|
||||
return []
|
||||
}
|
||||
return themesData as Theme[];
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+35
-31
@@ -1,39 +1,43 @@
|
||||
import type { NextConfig } from "next";
|
||||
import type {NextConfig} from "next";
|
||||
|
||||
const isExport = process.env.NEXT_STANDALONE_EXPORT === 'true';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactCompiler: true,
|
||||
experimental: {
|
||||
},
|
||||
async rewrites() {
|
||||
const backendUrl = process.env.LINUX_DO_CREDIT_BACKEND_URL || 'http://localhost:8000';
|
||||
return [
|
||||
// 易支付兼容接口 - 创建订单
|
||||
{
|
||||
source: '/epay/pay/:path*',
|
||||
destination: `${ backendUrl }/pay/:path*`,
|
||||
},
|
||||
// 易支付兼容接口 - 查询订单和退款
|
||||
{
|
||||
source: '/epay/api.php',
|
||||
destination: `${ backendUrl }/api.php`,
|
||||
},
|
||||
// Credit 协议接口 - 商户分发
|
||||
{
|
||||
source: '/lpay/distribute',
|
||||
destination: `${ backendUrl }/pay/distribute`,
|
||||
},
|
||||
// 上传文件静态资源
|
||||
{
|
||||
source: '/f/:id',
|
||||
destination: `${ backendUrl }/f/:id`,
|
||||
},
|
||||
// 标准 RESTful API 接口
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: `${ backendUrl }/api/:path*`,
|
||||
}
|
||||
];
|
||||
},
|
||||
...(isExport ? { output: 'export' } : {
|
||||
async rewrites() {
|
||||
const backendUrl = process.env.LINUX_DO_CREDIT_BACKEND_URL || 'http://localhost:8000';
|
||||
return [
|
||||
// 易支付兼容接口 - 创建订单
|
||||
{
|
||||
source: '/epay/pay/:path*',
|
||||
destination: `${ backendUrl }/pay/:path*`,
|
||||
},
|
||||
// 易支付兼容接口 - 查询订单和退款
|
||||
{
|
||||
source: '/epay/api.php',
|
||||
destination: `${ backendUrl }/api.php`,
|
||||
},
|
||||
// Credit 协议接口 - 商户分发
|
||||
{
|
||||
source: '/lpay/distribute',
|
||||
destination: `${ backendUrl }/pay/distribute`,
|
||||
},
|
||||
// 上传文件静态资源
|
||||
{
|
||||
source: '/f/:id',
|
||||
destination: `${ backendUrl }/f/:id`,
|
||||
},
|
||||
// 标准 RESTful API 接口
|
||||
{
|
||||
source: '/api/:path*',
|
||||
destination: `${ backendUrl }/api/:path*`,
|
||||
}
|
||||
];
|
||||
},
|
||||
})
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
@@ -4,8 +4,11 @@
|
||||
"buildDate": "2026-04-22 16:00:00",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"predev": "node scripts/generate-themes.js",
|
||||
"prebuild": "node scripts/generate-themes.js",
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"build:embed": "NEXT_STANDALONE_EXPORT=true next build",
|
||||
"start": "next start -p 3010",
|
||||
"lint": "eslint"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const STYLE_DIR = path.join(__dirname, '../public/style');
|
||||
const OUTPUT_FILE = path.join(__dirname, '../lib/theme/themes.json');
|
||||
|
||||
function parseCSSVariables(cssSection) {
|
||||
const variables = {};
|
||||
const regex = /--([a-z0-9-]+):\s*([^;]+);/g;
|
||||
let match;
|
||||
while ((match = regex.exec(cssSection)) !== null) {
|
||||
variables[match[1]] = match[2].trim();
|
||||
}
|
||||
return variables;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(STYLE_DIR)) {
|
||||
console.error(`Style directory not found: ${STYLE_DIR}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(STYLE_DIR);
|
||||
const themes = files
|
||||
.filter((file) => file.endsWith('.css'))
|
||||
.map((file) => {
|
||||
/* 生成主题名称 */
|
||||
const name = file === 'default.css'
|
||||
? 'Default'
|
||||
: file
|
||||
.replace('.css', '')
|
||||
.split('-')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
|
||||
const content = fs.readFileSync(path.join(STYLE_DIR, file), 'utf-8');
|
||||
|
||||
/* 从 :root 解析明亮模式颜色 */
|
||||
const rootSection = content.match(/:root\s*\{([^}]+)\}/s)?.[1] || "";
|
||||
const lightColors = parseCSSVariables(rootSection);
|
||||
|
||||
/* 从 .dark 解析暗色模式颜色 */
|
||||
const darkSection = content.match(/\.dark\s*\{([^}]+)\}/s)?.[1] || "";
|
||||
const darkColors = parseCSSVariables(darkSection);
|
||||
|
||||
return {
|
||||
id: file,
|
||||
name,
|
||||
colors: {
|
||||
light: lightColors,
|
||||
dark: darkColors,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/* 默认主题排在最前面,其余按名称排序 */
|
||||
themes.sort((a, b) => {
|
||||
if (a.id === 'default.css') return -1;
|
||||
if (b.id === 'default.css') return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
// 确保输出文件的父目录存在
|
||||
const outputDir = path.dirname(OUTPUT_FILE);
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(OUTPUT_FILE, JSON.stringify(themes, null, 2), 'utf-8');
|
||||
console.log(`Successfully generated themes.json at ${OUTPUT_FILE}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to generate themes.json:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user