mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 07:36:38 +08:00
feat: add mode-specific wallpapers and installer regression coverage
This commit is contained in:
@@ -7,6 +7,18 @@ on:
|
||||
branches: ['**']
|
||||
|
||||
jobs:
|
||||
install-scripts:
|
||||
name: Test Installer Scripts (systemd and Alpine OpenRC)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run installer regression tests
|
||||
run: bash test-install-scripts-proxy.sh
|
||||
|
||||
- name: Test Alpine bootstrap and OpenRC lifecycle
|
||||
run: docker run --rm -v "$PWD:/workspace:ro" alpine:3.22 sh /workspace/test-install-scripts-alpine.sh
|
||||
|
||||
frontend:
|
||||
name: Build Frontend
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -304,6 +304,9 @@ jobs:
|
||||
sed -i "s|^PINNED_VERSION=\"\"|PINNED_VERSION=\"${VERSION}\"|" ./artifacts/install.sh
|
||||
sed -i "s|^PINNED_VERSION=\"\"|PINNED_VERSION=\"${VERSION}\"|" ./artifacts/panel_install.sh
|
||||
|
||||
- name: Verify release installer on Alpine OpenRC
|
||||
run: docker run --rm -v "$PWD:/workspace:ro" alpine:3.22 sh /workspace/test-install-scripts-alpine.sh /workspace/artifacts/install.sh
|
||||
|
||||
- name: Create Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
@@ -19,6 +19,8 @@ func TestPublicConfigGetAllowsBrandKeys(t *testing.T) {
|
||||
seedConfigValue(t, r, "app_logo", "logo-data")
|
||||
seedConfigValue(t, r, "app_favicon", "favicon-data")
|
||||
seedConfigValue(t, r, "app_bg_image", "bg-data")
|
||||
seedConfigValue(t, r, "app_bg_image_light", "light-bg-data")
|
||||
seedConfigValue(t, r, "app_bg_image_dark", "dark-bg-data")
|
||||
seedConfigValue(t, r, "cloudflare_site_key", "site-key")
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/public/config/get", bytes.NewBufferString(`{"name":"app_name"}`))
|
||||
@@ -28,6 +30,16 @@ func TestPublicConfigGetAllowsBrandKeys(t *testing.T) {
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
assertHandlerCode(t, resp, 0)
|
||||
for name, want := range map[string]string{
|
||||
"app_bg_image_light": "light-bg-data",
|
||||
"app_bg_image_dark": "dark-bg-data",
|
||||
} {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/public/config/get", bytes.NewBufferString(`{"name":"`+name+`"}`))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp := httptest.NewRecorder()
|
||||
router.ServeHTTP(resp, req)
|
||||
assertHandlerConfigValue(t, resp, name, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicBrandConfigFallsBackWithoutCommercialLicense(t *testing.T) {
|
||||
|
||||
@@ -14,6 +14,8 @@ var publicConfigKeys = map[string]struct{}{
|
||||
"app_logo": {},
|
||||
"app_favicon": {},
|
||||
"app_bg_image": {},
|
||||
"app_bg_image_light": {},
|
||||
"app_bg_image_dark": {},
|
||||
"cloudflare_site_key": {},
|
||||
"is_commercial": {},
|
||||
"hide_footer_brand": {},
|
||||
|
||||
@@ -12,6 +12,8 @@ func TestConfigPolicy(t *testing.T) {
|
||||
{name: "app_logo is public", key: "app_logo", want: ConfigAccessPublic},
|
||||
{name: "app_favicon is public", key: "app_favicon", want: ConfigAccessPublic},
|
||||
{name: "app_bg_image is public", key: "app_bg_image", want: ConfigAccessPublic},
|
||||
{name: "app_bg_image_light is public", key: "app_bg_image_light", want: ConfigAccessPublic},
|
||||
{name: "app_bg_image_dark is public", key: "app_bg_image_dark", want: ConfigAccessPublic},
|
||||
{name: "cloudflare_site_key is public", key: "cloudflare_site_key", want: ConfigAccessPublic},
|
||||
{name: "is_commercial is public", key: "is_commercial", want: ConfigAccessPublic},
|
||||
{name: "hide_footer_brand is public", key: "hide_footer_brand", want: ConfigAccessPublic},
|
||||
@@ -31,7 +33,7 @@ func TestConfigPolicy(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestConfigPolicyHelpers(t *testing.T) {
|
||||
publicKeys := []string{"app_name", "app_logo", "app_favicon", "app_bg_image", "cloudflare_site_key", "is_commercial", "hide_footer_brand"}
|
||||
publicKeys := []string{"app_name", "app_logo", "app_favicon", "app_bg_image", "app_bg_image_light", "app_bg_image_dark", "cloudflare_site_key", "is_commercial", "hide_footer_brand"}
|
||||
for _, key := range publicKeys {
|
||||
if !IsPublicConfigKey(key) {
|
||||
t.Fatalf("expected %q to be public", key)
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# Run only in a disposable Alpine container: this exercises real OpenRC services.
|
||||
# docker run --rm -v "$PWD:/workspace:ro" alpine:3.22 sh /workspace/test-install-scripts-alpine.sh
|
||||
[ -f /etc/alpine-release ] && [ "$(id -u)" = 0 ] || {
|
||||
echo "Run this test as root in a disposable Alpine container." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
INSTALL_SCRIPT=${1:-"$ROOT_DIR/install.sh"}
|
||||
TEST_DIR=$(mktemp -d)
|
||||
export TEST_DIR
|
||||
|
||||
cleanup() {
|
||||
rc-service flux_agent stop >/dev/null 2>&1 || true
|
||||
rc-update del flux_agent default >/dev/null 2>&1 || true
|
||||
rm -f /etc/init.d/flux_agent
|
||||
rm -rf "$TEST_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Docker supplies the network; an empty interfaces file lets OpenRC register
|
||||
# that dependency without changing the container's network configuration.
|
||||
apk add --no-cache openrc
|
||||
mkdir -p /run/openrc /etc/network
|
||||
touch /run/openrc/softlevel /etc/network/interfaces
|
||||
rc-service networking start
|
||||
|
||||
# Fail immediately if any path accidentally calls systemd on Alpine.
|
||||
mkdir -p "$TEST_DIR/bin"
|
||||
cat > "$TEST_DIR/bin/systemctl" <<'EOF'
|
||||
#!/bin/sh
|
||||
touch "$TEST_DIR/systemctl-called"
|
||||
exit 1
|
||||
EOF
|
||||
chmod +x "$TEST_DIR/bin/systemctl"
|
||||
export PATH="$TEST_DIR/bin:$PATH"
|
||||
|
||||
cat > "$TEST_DIR/agent" <<'EOF'
|
||||
#!/bin/sh
|
||||
if [ "${1:-}" = -V ]; then
|
||||
echo "Alpine installer test agent"
|
||||
exit 0
|
||||
fi
|
||||
pwd > ../working-directory
|
||||
exec sleep 300
|
||||
EOF
|
||||
|
||||
# Keep the installer bootstrap, service detection and all lifecycle operations.
|
||||
# Substitute only the network download and optional tcpkill dependency.
|
||||
sed '/^# 执行主函数$/,$d' "$INSTALL_SCRIPT" > "$TEST_DIR/install.sh"
|
||||
cat >> "$TEST_DIR/install.sh" <<'EOF'
|
||||
INSTALL_DIR="$TEST_DIR/flux_agent"
|
||||
ensure_download_url_initialized() {
|
||||
ensure_alpine_runtime_dependencies || return 1
|
||||
DOWNLOAD_URL="file://$TEST_DIR/agent"
|
||||
}
|
||||
check_and_install_tcpkill() { :; }
|
||||
main
|
||||
EOF
|
||||
chmod +x "$TEST_DIR/install.sh"
|
||||
cp "$TEST_DIR/install.sh" "$TEST_DIR/manage.sh"
|
||||
PROXY_ENABLED=false "$TEST_DIR/install.sh" -a http://127.0.0.1:9 -s alpine-test
|
||||
|
||||
rc-service flux_agent status
|
||||
test -L /etc/runlevels/default/flux_agent
|
||||
test "$(cat "$TEST_DIR/working-directory")" = "$TEST_DIR/flux_agent"
|
||||
test -s "$TEST_DIR/flux_agent/config.json"
|
||||
rc-service flux_agent restart
|
||||
rc-service flux_agent status
|
||||
|
||||
# Update must retain config and restart through OpenRC.
|
||||
cp "$TEST_DIR/flux_agent/config.json" "$TEST_DIR/config-before-update.json"
|
||||
cp "$TEST_DIR/manage.sh" "$TEST_DIR/update.sh"
|
||||
printf '2\n' | PROXY_ENABLED=false "$TEST_DIR/update.sh"
|
||||
cmp "$TEST_DIR/config-before-update.json" "$TEST_DIR/flux_agent/config.json"
|
||||
rc-service flux_agent status
|
||||
|
||||
printf '3\ny\n' | "$TEST_DIR/manage.sh"
|
||||
test ! -e /etc/init.d/flux_agent
|
||||
test ! -e /etc/runlevels/default/flux_agent
|
||||
test ! -e "$TEST_DIR/flux_agent"
|
||||
test ! -e "$TEST_DIR/systemctl-called"
|
||||
echo "Alpine installer lifecycle tests passed"
|
||||
@@ -28,6 +28,7 @@ import { isLoggedIn } from "@/utils/auth";
|
||||
import { siteConfig, updateSiteConfig } from "@/config/site";
|
||||
import { useH5Mode } from "@/hooks/useH5Mode";
|
||||
import { SESSION_UPDATED_EVENT } from "@/utils/session";
|
||||
import { useThemeContext } from "@/themes/context";
|
||||
|
||||
const ProtectedRoute = ({
|
||||
children,
|
||||
@@ -79,6 +80,7 @@ const LoginRoute = () => {
|
||||
function App() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const { effectiveMode } = useThemeContext();
|
||||
|
||||
// 全局登录状态监听,当检测到未登录且不在首页时,跳转到首页
|
||||
useEffect(() => {
|
||||
@@ -98,7 +100,10 @@ function App() {
|
||||
// 处理自定义背景图片
|
||||
useEffect(() => {
|
||||
const updateBg = () => {
|
||||
const customBg = siteConfig.app_bg_image;
|
||||
const customBg =
|
||||
(effectiveMode === "dark"
|
||||
? siteConfig.app_bg_image_dark
|
||||
: siteConfig.app_bg_image_light) || siteConfig.app_bg_image;
|
||||
|
||||
if (customBg) {
|
||||
if (customBg === "theme") {
|
||||
@@ -149,7 +154,7 @@ function App() {
|
||||
return () => {
|
||||
window.removeEventListener("site-config-updated", updateBg);
|
||||
};
|
||||
}, []);
|
||||
}, [effectiveMode]);
|
||||
|
||||
// 立即设置页面标题(使用已从缓存读取的配置)
|
||||
useEffect(() => {
|
||||
|
||||
@@ -14,6 +14,8 @@ const PUBLIC_BRAND_CONFIG_KEYS = [
|
||||
"app_logo",
|
||||
"app_favicon",
|
||||
"app_bg_image",
|
||||
"app_bg_image_light",
|
||||
"app_bg_image_dark",
|
||||
"is_commercial",
|
||||
"hide_footer_brand",
|
||||
] as const;
|
||||
@@ -114,6 +116,8 @@ const getInitialConfig = () => {
|
||||
app_logo: "",
|
||||
app_favicon: "",
|
||||
app_bg_image: "",
|
||||
app_bg_image_light: "",
|
||||
app_bg_image_dark: "",
|
||||
is_commercial: false,
|
||||
hide_footer_brand: false,
|
||||
};
|
||||
@@ -127,6 +131,10 @@ const getInitialConfig = () => {
|
||||
localStorage.getItem(CACHE_PREFIX + "app_favicon") || "";
|
||||
const cachedAppBgImage =
|
||||
localStorage.getItem(CACHE_PREFIX + "app_bg_image") || "";
|
||||
const cachedAppBgImageLight =
|
||||
localStorage.getItem(CACHE_PREFIX + "app_bg_image_light") || "";
|
||||
const cachedAppBgImageDark =
|
||||
localStorage.getItem(CACHE_PREFIX + "app_bg_image_dark") || "";
|
||||
const isCommercial =
|
||||
localStorage.getItem(CACHE_PREFIX + "is_commercial") === "true";
|
||||
const hideFooterBrand =
|
||||
@@ -141,6 +149,8 @@ const getInitialConfig = () => {
|
||||
app_logo: isCommercial ? cachedAppLogo : "",
|
||||
app_favicon: isCommercial ? cachedAppFavicon : "",
|
||||
app_bg_image: cachedAppBgImage,
|
||||
app_bg_image_light: cachedAppBgImageLight,
|
||||
app_bg_image_dark: cachedAppBgImageDark,
|
||||
is_commercial: isCommercial,
|
||||
hide_footer_brand: isCommercial && hideFooterBrand,
|
||||
};
|
||||
@@ -154,6 +164,8 @@ const getInitialConfig = () => {
|
||||
app_logo: isCommercial ? cachedAppLogo : "",
|
||||
app_favicon: isCommercial ? cachedAppFavicon : "",
|
||||
app_bg_image: cachedAppBgImage,
|
||||
app_bg_image_light: cachedAppBgImageLight,
|
||||
app_bg_image_dark: cachedAppBgImageDark,
|
||||
is_commercial: isCommercial,
|
||||
hide_footer_brand: isCommercial && hideFooterBrand,
|
||||
};
|
||||
@@ -382,6 +394,14 @@ export const updateSiteConfig = async (configMap?: Record<string, string>) => {
|
||||
resolvedConfigMap,
|
||||
"app_bg_image",
|
||||
);
|
||||
const hasAppBgImageLight = Object.prototype.hasOwnProperty.call(
|
||||
resolvedConfigMap,
|
||||
"app_bg_image_light",
|
||||
);
|
||||
const hasAppBgImageDark = Object.prototype.hasOwnProperty.call(
|
||||
resolvedConfigMap,
|
||||
"app_bg_image_dark",
|
||||
);
|
||||
|
||||
const resolvedCommercial = Object.prototype.hasOwnProperty.call(
|
||||
resolvedConfigMap,
|
||||
@@ -401,11 +421,19 @@ export const updateSiteConfig = async (configMap?: Record<string, string>) => {
|
||||
const appBgImage = hasAppBgImage
|
||||
? String(resolvedConfigMap.app_bg_image || "").trim()
|
||||
: (siteConfig.app_bg_image || "").trim();
|
||||
const appBgImageLight = hasAppBgImageLight
|
||||
? String(resolvedConfigMap.app_bg_image_light || "").trim()
|
||||
: (siteConfig.app_bg_image_light || "").trim();
|
||||
const appBgImageDark = hasAppBgImageDark
|
||||
? String(resolvedConfigMap.app_bg_image_dark || "").trim()
|
||||
: (siteConfig.app_bg_image_dark || "").trim();
|
||||
|
||||
siteConfig.name = resolvedCommercial && appName ? appName : "FLVX";
|
||||
siteConfig.app_logo = resolvedCommercial ? appLogo : "";
|
||||
siteConfig.app_favicon = resolvedCommercial ? appFavicon : "";
|
||||
siteConfig.app_bg_image = appBgImage;
|
||||
siteConfig.app_bg_image_light = appBgImageLight;
|
||||
siteConfig.app_bg_image_dark = appBgImageDark;
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(resolvedConfigMap, "is_commercial")
|
||||
) {
|
||||
|
||||
@@ -90,6 +90,8 @@ interface ConfigItem {
|
||||
dependsValue?: string; // 依赖的配置项值
|
||||
}
|
||||
|
||||
type BgImageKey = "app_bg_image" | "app_bg_image_light" | "app_bg_image_dark";
|
||||
|
||||
const BRAND_PREVIEW_KEYS = ["app_logo", "app_favicon"] as const;
|
||||
|
||||
type BrandPreviewKey = (typeof BRAND_PREVIEW_KEYS)[number];
|
||||
@@ -107,9 +109,9 @@ const toBrandAssetKind = (key: BrandPreviewKey): BrandAssetKind => {
|
||||
const CONFIG_ITEMS: ConfigItem[] = [
|
||||
{
|
||||
key: "app_bg_image",
|
||||
label: "自定义背景",
|
||||
label: "背景壁纸",
|
||||
description:
|
||||
"上传自定义背景图片(建议使用深色/浅色均可看清的图片,或使用半透明模糊效果)",
|
||||
"默认背景用于未单独设置壁纸的模式。可分别上传亮色和暗色壁纸,保存后随外观模式自动切换。",
|
||||
type: "bg_image",
|
||||
},
|
||||
{
|
||||
@@ -263,6 +265,9 @@ const getInitialConfigs = (): Record<string, string> => {
|
||||
"panel_domain",
|
||||
"app_logo",
|
||||
"app_favicon",
|
||||
"app_bg_image",
|
||||
"app_bg_image_light",
|
||||
"app_bg_image_dark",
|
||||
"github_proxy_enabled",
|
||||
"github_proxy_url",
|
||||
"allow_local_remote_addr",
|
||||
@@ -312,8 +317,12 @@ export default function ConfigPage() {
|
||||
|
||||
const logoFileInputRef = useRef<HTMLInputElement>(null);
|
||||
const faviconFileInputRef = useRef<HTMLInputElement>(null);
|
||||
const bgImageFileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [bgImageUploading, setBgImageUploading] = useState(false);
|
||||
const bgImageFileInputRefs = useRef<
|
||||
Partial<Record<BgImageKey, HTMLInputElement>>
|
||||
>({});
|
||||
const [bgImageUploading, setBgImageUploading] = useState<BgImageKey | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const [announcement, setAnnouncement] = useState<AnnouncementData>({
|
||||
content: "",
|
||||
@@ -689,7 +698,14 @@ export default function ConfigPage() {
|
||||
|
||||
if (
|
||||
changedKeys.some((key) =>
|
||||
["app_name", "app_logo", "app_favicon"].includes(key),
|
||||
[
|
||||
"app_name",
|
||||
"app_logo",
|
||||
"app_favicon",
|
||||
"app_bg_image",
|
||||
"app_bg_image_light",
|
||||
"app_bg_image_dark",
|
||||
].includes(key),
|
||||
)
|
||||
) {
|
||||
await updateSiteConfig(configs);
|
||||
@@ -800,6 +816,7 @@ export default function ConfigPage() {
|
||||
|
||||
const handleBgImageUpload = async (
|
||||
e: React.ChangeEvent<HTMLInputElement>,
|
||||
key: BgImageKey,
|
||||
) => {
|
||||
const file = e.target.files?.[0];
|
||||
|
||||
@@ -811,7 +828,7 @@ export default function ConfigPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
setBgImageUploading(true);
|
||||
setBgImageUploading(key);
|
||||
try {
|
||||
const compressedImage = await new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
@@ -861,18 +878,19 @@ export default function ConfigPage() {
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
handleConfigChange("app_bg_image", compressedImage);
|
||||
toast.success("自定义背景上传成功");
|
||||
handleConfigChange(key, compressedImage);
|
||||
toast.success("壁纸上传成功,保存配置后生效");
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "图片处理失败");
|
||||
} finally {
|
||||
setBgImageUploading(false);
|
||||
setBgImageUploading(null);
|
||||
e.target.value = "";
|
||||
}
|
||||
};
|
||||
|
||||
const renderBgImageUploader = () => {
|
||||
const bgImage = configs["app_bg_image"] || "";
|
||||
const renderBgImageUploader = (key: BgImageKey, label: string) => {
|
||||
const bgImage = configs[key] || "";
|
||||
const isDefault = key === "app_bg_image";
|
||||
const isImage =
|
||||
bgImage.startsWith("http") ||
|
||||
bgImage.startsWith("data:") ||
|
||||
@@ -882,47 +900,64 @@ export default function ConfigPage() {
|
||||
const isSolidColor = bgImage && !isImage && !isTheme;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<div className="flex flex-col gap-3 w-full rounded-xl border border-divider p-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{label}
|
||||
</p>
|
||||
{!isDefault && !bgImage && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
未单独设置,使用默认背景
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<input
|
||||
ref={bgImageFileInputRef}
|
||||
ref={(node) => {
|
||||
if (node) bgImageFileInputRefs.current[key] = node;
|
||||
}}
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
type="file"
|
||||
onChange={handleBgImageUpload}
|
||||
onChange={(event) => void handleBgImageUpload(event, key)}
|
||||
/>
|
||||
<Button
|
||||
color="primary"
|
||||
isLoading={bgImageUploading}
|
||||
isDisabled={bgImageUploading !== null && bgImageUploading !== key}
|
||||
isLoading={bgImageUploading === key}
|
||||
variant="flat"
|
||||
onPress={() => bgImageFileInputRef.current?.click()}
|
||||
onPress={() => bgImageFileInputRefs.current[key]?.click()}
|
||||
>
|
||||
上传图片
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
isDisabled={bgImageUploading || isTheme}
|
||||
variant="flat"
|
||||
onPress={() => handleConfigChange("app_bg_image", "theme")}
|
||||
>
|
||||
自适应纯色 (跟随深色模式)
|
||||
</Button>
|
||||
<Button
|
||||
color="default"
|
||||
isDisabled={bgImageUploading || bgImage === "#ffffff"}
|
||||
variant="flat"
|
||||
onPress={() => handleConfigChange("app_bg_image", "#ffffff")}
|
||||
>
|
||||
白色纯色
|
||||
</Button>
|
||||
{isDefault && (
|
||||
<Button
|
||||
color="secondary"
|
||||
isDisabled={bgImageUploading !== null || isTheme}
|
||||
variant="flat"
|
||||
onPress={() => handleConfigChange(key, "theme")}
|
||||
>
|
||||
自适应纯色 (跟随深色模式)
|
||||
</Button>
|
||||
)}
|
||||
{isDefault && (
|
||||
<Button
|
||||
color="default"
|
||||
isDisabled={bgImageUploading !== null || bgImage === "#ffffff"}
|
||||
variant="flat"
|
||||
onPress={() => handleConfigChange(key, "#ffffff")}
|
||||
>
|
||||
白色纯色
|
||||
</Button>
|
||||
)}
|
||||
{bgImage && (
|
||||
<Button
|
||||
color="danger"
|
||||
isDisabled={bgImageUploading}
|
||||
isDisabled={bgImageUploading !== null}
|
||||
variant="flat"
|
||||
onPress={() => handleConfigChange("app_bg_image", "")}
|
||||
onPress={() => handleConfigChange(key, "")}
|
||||
>
|
||||
恢复默认
|
||||
{isDefault ? "恢复内置背景" : "清除专属壁纸"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -930,7 +965,7 @@ export default function ConfigPage() {
|
||||
{bgImage && isImage && (
|
||||
<div className="relative rounded-xl overflow-hidden border border-divider">
|
||||
<img
|
||||
alt="背景预览"
|
||||
alt={`${label}预览`}
|
||||
className="w-full max-h-48 object-cover opacity-80"
|
||||
src={bgImage}
|
||||
/>
|
||||
@@ -1118,7 +1153,15 @@ export default function ConfigPage() {
|
||||
|
||||
switch (item.type) {
|
||||
case "bg_image":
|
||||
return renderBgImageUploader();
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div className="lg:col-span-2">
|
||||
{renderBgImageUploader("app_bg_image", "默认背景")}
|
||||
</div>
|
||||
{renderBgImageUploader("app_bg_image_light", "☀️ 亮色模式壁纸")}
|
||||
{renderBgImageUploader("app_bg_image_dark", "🌙 暗色模式壁纸")}
|
||||
</div>
|
||||
);
|
||||
|
||||
case "input": {
|
||||
if (isBrandPreviewKey(item.key)) {
|
||||
|
||||
Reference in New Issue
Block a user