先提交上去

This commit is contained in:
Fox_block
2026-03-16 20:49:46 +08:00
parent 86abcb5602
commit 192b48cab0
303 changed files with 30019 additions and 5846 deletions
+332
View File
@@ -0,0 +1,332 @@
import React, { useEffect, useMemo, useState } from "react"
import { Button, ColorPicker, Input, Segmented, Space, Typography, message } from "antd"
import type { Color } from "antd/es/color-picker"
import { useTranslation } from "react-i18next"
import { useTheme } from "../contexts/ThemeContext"
import type { themeConfig } from "../preload/types"
type props = {
compact?: boolean
}
const presetPrimaryColors = [
"#1677FF",
"#2F54EB",
"#722ED1",
"#EB2F96",
"#F5222D",
"#FA8C16",
"#FADB14",
"#52C41A",
"#13C2C2",
]
const presetGradients: {
id: string
labelKey: string
light: string
dark: string
}[] = [
{
id: "blue",
labelKey: "blue",
light: "linear-gradient(180deg, #f7fbff 0%, #f1f7ff 55%, #f8f9fc 100%)",
dark: "linear-gradient(180deg, #0f1220 0%, #101524 55%, #0b0d16 100%)",
},
{
id: "pink",
labelKey: "pink",
light: "linear-gradient(180deg, #fff7f1 0%, #fff1f1 55%, #f7f7fb 100%)",
dark: "linear-gradient(180deg, #1a0f14 0%, #1d1218 55%, #120c10 100%)",
},
{
id: "cyan",
labelKey: "cyan",
light: "linear-gradient(180deg, #f0f9ff 0%, #e0f2fe 55%, #f0f9ff 100%)",
dark: "linear-gradient(180deg, #050b10 0%, #06121a 55%, #05070a 100%)",
},
{
id: "purple",
labelKey: "purple",
light: "linear-gradient(180deg, #faf5ff 0%, #f3e8ff 55%, #faf5ff 100%)",
dark: "linear-gradient(180deg, #0f0a14 0%, #151020 55%, #0d0910 100%)",
},
]
const deepClone = <T,>(v: T): T => JSON.parse(JSON.stringify(v)) as T
const normalizeHex = (value: string): string | null => {
const s = String(value || "").trim()
if (!s) return null
const m = s.match(/^#?([0-9a-fA-F]{6})$/)
if (!m) return null
return `#${m[1].toUpperCase()}`
}
const buildGradient = (a: string, b: string, dir: "v" | "h" | "d"): string => {
const angle = dir === "h" ? "90deg" : dir === "d" ? "135deg" : "180deg"
return `linear-gradient(${angle}, ${a} 0%, ${b} 100%)`
}
export const ThemeQuickSettings: React.FC<props> = ({ compact }) => {
const { t } = useTranslation()
const { currentTheme, setTheme, themes, applyTheme } = useTheme()
const [messageApi, holder] = message.useMessage()
const [workingTheme, setWorkingTheme] = useState<themeConfig | null>(null)
const [saving, setSaving] = useState(false)
const [primaryInput, setPrimaryInput] = useState("")
const primaryColor = useMemo(() => {
const fromTheme = workingTheme?.config?.tdesign?.brandColor
return normalizeHex(primaryInput) || normalizeHex(fromTheme || "") || "#1677FF"
}, [primaryInput, workingTheme])
const [gradientDir, setGradientDir] = useState<"v" | "h" | "d">("v")
const [g1, setG1] = useState<string>("#f7fbff")
const [g2, setG2] = useState<string>("#f8f9fc")
const currentBg = workingTheme?.config?.custom?.["--ss-bg-color"] || ""
useEffect(() => {
if (!currentTheme) return
const base = deepClone(currentTheme)
const editable =
base.id === "custom-default" || base.id.startsWith("custom-")
? base
: { ...base, id: "custom-default", name: t("theme.myTheme") }
setWorkingTheme(editable)
setPrimaryInput(editable.config?.tdesign?.brandColor || "")
}, [currentTheme])
// Real-time theme preview
useEffect(() => {
if (!workingTheme) return
applyTheme(workingTheme)
}, [workingTheme, applyTheme])
const ensureCustomThemeSelected = async (theme: themeConfig): Promise<boolean> => {
if (!(window as any).api) return false
const exists = themes.some((t) => t.id === theme.id)
if (!exists) {
const res = await (window as any).api.saveTheme(theme)
if (!res?.success) return false
}
if (currentTheme?.id !== theme.id) {
await setTheme(theme.id)
}
return true
}
const saveThemeToDb = async (theme: themeConfig) => {
if (!(window as any).api) return false
try {
const exists = themes.some((t) => t.id === theme.id)
if (!exists) {
const res = await (window as any).api.saveTheme(theme)
if (!res?.success) return false
}
if (currentTheme?.id !== theme.id) {
await setTheme(theme.id)
}
const res = await (window as any).api.saveTheme(theme)
return res?.success
} catch {
return false
}
}
const save = async () => {
if (!(window as any).api) return
if (!workingTheme) return
setSaving(true)
try {
const ok = await ensureCustomThemeSelected(workingTheme)
if (!ok) throw new Error("保存失败")
const res = await (window as any).api.saveTheme(workingTheme)
if (!res?.success) throw new Error(res?.message || "保存失败")
messageApi.success(t("theme.saved"))
} catch (e: any) {
messageApi.error(e?.message || t("theme.saveFailed"))
} finally {
setSaving(false)
}
}
const setPrimary = async (hex: string) => {
setPrimaryInput(hex)
setWorkingTheme((prev: themeConfig | null) => {
if (!prev) return prev
const next = deepClone(prev)
next.config = next.config || ({} as any)
next.config.tdesign = { ...(next.config.tdesign || {}), brandColor: hex }
saveThemeToDb(next)
return next
})
}
const setGradientPreset = async (value: string) => {
setWorkingTheme((prev) => {
if (!prev) return prev
const next = deepClone(prev)
next.config = next.config || ({} as any)
next.config.custom = { ...(next.config.custom || {}), "--ss-bg-color": value }
saveThemeToDb(next)
return next
})
}
const setGradientFromPickers = async () => {
const g = buildGradient(g1, g2, gradientDir)
setWorkingTheme((prev: themeConfig | null) => {
if (!prev) return prev
const next = deepClone(prev)
next.config = next.config || ({} as any)
next.config.custom = { ...(next.config.custom || {}), "--ss-bg-color": g }
saveThemeToDb(next)
return next
})
}
const setMode = async (mode: "light" | "dark") => {
const targetThemeId = mode === "light" ? "light-default" : "dark-default"
await setTheme(targetThemeId)
}
if (!workingTheme) return null
return (
<div style={{ display: "flex", flexDirection: "column", gap: compact ? 12 : 16 }}>
{holder}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Typography.Text strong>{t("theme.colorAndBackground")}</Typography.Text>
<Space>
<Button type="primary" loading={saving} onClick={save}>
{t("common.save")}
</Button>
</Space>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<Typography.Text>{t("theme.mode")}</Typography.Text>
<Segmented
value={workingTheme.mode}
onChange={(v) => setMode(v as any)}
options={[
{ label: t("settings.lightMode"), value: "light" },
{ label: t("settings.darkMode"), value: "dark" },
]}
/>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<Typography.Text>{t("settings.primaryColor")}</Typography.Text>
<div style={{ display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
<Input
value={primaryInput}
onChange={(e) => setPrimaryInput(e.target.value)}
onBlur={() => {
const n = normalizeHex(primaryInput)
if (n) setPrimary(n)
else setPrimary(primaryColor)
}}
style={{ width: 120 }}
/>
{presetPrimaryColors.map((c) => (
<button
key={c}
type="button"
onClick={() => setPrimary(c)}
style={{
width: 18,
height: 18,
borderRadius: 999,
border:
c === primaryColor ? "2px solid rgba(0,0,0,0.35)" : "1px solid rgba(0,0,0,0.18)",
background: c,
cursor: "pointer",
padding: 0,
}}
/>
))}
<ColorPicker
value={primaryColor}
onChange={(color: Color) => setPrimary(color.toHexString().toUpperCase())}
/>
</div>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
<Typography.Text>{t("settings.backgroundGradient")}</Typography.Text>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(140px, 1fr))",
gap: 10,
}}
>
{presetGradients.map((g) => {
const gradientValue = g[workingTheme.mode]
const active = currentBg === gradientValue
return (
<button
key={g.id}
type="button"
onClick={() => setGradientPreset(gradientValue)}
style={{
borderRadius: 10,
border: active
? "2px solid var(--ant-color-primary)"
: "1px solid var(--ss-border-color)",
padding: 10,
background: "transparent",
cursor: "pointer",
textAlign: "left",
}}
>
<div
style={{
height: 56,
borderRadius: 8,
border: "1px solid var(--ss-border-color)",
background: gradientValue,
}}
/>
<div style={{ marginTop: 8, fontSize: 12, color: "var(--ss-text-secondary)" }}>
{t(`theme.gradientLabels.${g.labelKey}`)}
</div>
</button>
)
})}
</div>
<div style={{ display: "flex", gap: 10, alignItems: "center", flexWrap: "wrap" }}>
<Segmented
value={gradientDir}
onChange={(v) => setGradientDir(v as any)}
options={[
{ label: t("theme.gradientDirections.vertical"), value: "v" },
{ label: t("theme.gradientDirections.horizontal"), value: "h" },
{ label: t("theme.gradientDirections.diagonal"), value: "d" },
]}
/>
<Space size="small">
<ColorPicker value={g1} onChange={(c: Color) => setG1(c.toHexString())} />
<ColorPicker value={g2} onChange={(c: Color) => setG2(c.toHexString())} />
<Button onClick={setGradientFromPickers}>{t("theme.generate")}</Button>
</Space>
<div
style={{
width: 160,
height: 32,
borderRadius: 8,
border: "1px solid var(--ss-border-color)",
background: buildGradient(g1, g2, gradientDir),
}}
/>
</div>
</div>
</div>
)
}