mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
refactor: 迁移组件库至shadcn/ui并新增版本更新功能
迁移项目内所有HeroUI表单组件至shadcn/ui,统一组件风格并修复HeroUI Switch点击无响应的问题 新增全局版本更新弹窗功能,包含状态管理store、全局弹窗组件与调试页面调试工具 优化配置广播逻辑,添加250ms防抖避免频繁触发页面重渲染 新增开发环境下日志直接输出到终端的能力,完善日志输出规则 调整调试模式设置项位置,从高级设置页移动至关于设置页 为背景层添加pointer-events: none,避免遮挡页面点击事件 新增点击拦截诊断工具至资源调试页面,用于排查控件无法点击的问题 更新Koring.yml配置模板,新增advanced.preLaunchCmd字段
This commit is contained in:
@@ -67,7 +67,7 @@ build/ ← generated by switch-icon.js (gitignored)
|
||||
- `electron/auth.ts`: Auth data persistence (JSON file)
|
||||
- `electron/core/`: @xmcl/* integrations (auth, installer, launcher, modrinth, instance)
|
||||
- `electron/core/background-image.ts`: 背景图处理服务 —— 自选壁纸复制到 userData 并按屏幕尺寸降采样/重编码**落盘**,配置文件只存**文件路径**(不使用 BASE64)
|
||||
- `electron/core/logger.ts`: 统一日志 —— 全局包装 `ipcMain.handle`(channel/耗时/成败);开启 debug 模式(`config.advanced.debugMode`)后写 `userData/koring.log`(5MB 轮转),否则仅控制台;渲染端经 `log:write` 桥汇入
|
||||
- `electron/core/logger.ts`: 统一日志 —— 全局包装 `ipcMain.handle`(channel/耗时/成败);开启 debug 模式(`config.advanced.debugMode`)后写 `userData/koring.log`(5MB 轮转),否则仅控制台;**dev(未打包)运行下日志直接写进程 stdout/stderr 输出到启动终端**;渲染端经 `log:write` 桥汇入
|
||||
- `electron/resource-protocol.ts`: `koring-res://` 特权自定义协议 —— 渲染进程以「资源引用」流式读取本地壁纸;仅服务 userData 内 `background-custom*` 白名单文件(realpath 二次校验,防目录穿越)
|
||||
- `electron/handlers/`: IPC handlers (config, auth, install, launch, mods, instance, background, task, system, window)
|
||||
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
appVersion: 1.2.6
|
||||
oobe: false
|
||||
advanced:
|
||||
preLaunchCmd: '123'
|
||||
|
||||
File diff suppressed because one or more lines are too long
+14
-4
@@ -5,6 +5,7 @@
|
||||
* - 默认(非 debug):warn/error/info 输出到控制台,debug 不输出;
|
||||
* - 用户开启「调试模式」(config.advanced.debugMode,设置→游戏→高级):
|
||||
* debug 也输出控制台,并把全部级别写入 userData/koring.log(超过 5MB 自动轮转为 .old);
|
||||
* - 开发(未打包)运行且开启调试模式时:日志额外/直接输出到启动该进程的终端(stdout/stderr);
|
||||
* - 渲染进程经 `log:write`(ipcRenderer.send)汇入同一套格式/文件。
|
||||
*/
|
||||
|
||||
@@ -25,6 +26,9 @@ export interface Logger {
|
||||
|
||||
const MAX_LOG_BYTES = 5 * 1024 * 1024;
|
||||
|
||||
/** 开发(未打包)运行:直接跑在终端里,日志写 stdout/stderr 用户即可实时看到 */
|
||||
const isDevRun = !app.isPackaged;
|
||||
|
||||
let debugModeProvider: () => boolean = () => false;
|
||||
export function setDebugModeProvider(fn: () => boolean): void {
|
||||
debugModeProvider = fn;
|
||||
@@ -109,10 +113,16 @@ function write(scope: string, level: LogLevel, args: unknown[]): void {
|
||||
const line = `[${timestamp()}][${scope}][${level.toUpperCase()}] ${msg}`;
|
||||
|
||||
const enabled = isDebugMode();
|
||||
// debug 仅在调试模式可见;info/warn/error 始终走控制台
|
||||
const showConsole = level !== 'debug' || enabled;
|
||||
if (showConsole) {
|
||||
if (level === 'error') console.error(line);
|
||||
// debug 仅在调试模式可见;info/warn/error 始终输出
|
||||
const show = level !== 'debug' || enabled;
|
||||
if (show) {
|
||||
if (isDevRun) {
|
||||
// dev(未打包,pnpm dev / electron .):直接写进程 stdout/stderr,
|
||||
// 让详细日志实时出现在启动它的终端里(不依赖外部控制台捕获)。
|
||||
const text = `${line}\n`;
|
||||
if (level === 'error' || level === 'warn') process.stderr.write(text);
|
||||
else process.stdout.write(text);
|
||||
} else if (level === 'error') console.error(line);
|
||||
else if (level === 'warn') console.warn(line);
|
||||
else if (level === 'info') console.info(line);
|
||||
else console.debug(line);
|
||||
|
||||
@@ -26,13 +26,26 @@ export function registerConfigHandlers(win: WinRef) {
|
||||
}
|
||||
});
|
||||
|
||||
// 广播防抖:输入框/滑块每键触发 update 时,不立刻全树广播(避免整页重渲染打断交互),
|
||||
// 合并到 250ms 后只广播一次最新配置;渲染端乐观更新保证即时反馈。
|
||||
let broadcastTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const scheduleBroadcast = () => {
|
||||
if (broadcastTimer) clearTimeout(broadcastTimer);
|
||||
broadcastTimer = setTimeout(() => {
|
||||
broadcastTimer = null;
|
||||
if (win.mainWindow && !win.mainWindow.isDestroyed()) {
|
||||
win.mainWindow.webContents.send('config:changed', getConfig());
|
||||
}
|
||||
}, 250);
|
||||
};
|
||||
|
||||
// 主进程权威更新:渲染进程提交 { section, patch } 补丁,
|
||||
// 主进程深度合并到内存配置 → debounce 稀疏写盘 → 广播完整配置给所有渲染进程
|
||||
// 主进程深度合并到内存配置 → debounce 稀疏写盘 → 防抖广播完整配置给所有渲染进程
|
||||
ipcMain.handle('config:update', (_event, payload: { section: string; patch: unknown }) => {
|
||||
try {
|
||||
const { section, patch } = payload;
|
||||
const config = updateConfig({ [section]: patch } as Record<string, unknown>);
|
||||
win.mainWindow?.webContents.send('config:changed', config);
|
||||
scheduleBroadcast();
|
||||
return { success: true, data: config, error: null };
|
||||
} catch (e: unknown) {
|
||||
return { success: false, data: null, error: String(e) };
|
||||
|
||||
+2
-1
@@ -25,7 +25,8 @@ const { app } = electron;
|
||||
|
||||
const isDev = !app.isPackaged;
|
||||
|
||||
// 统一日志:debug 模式(config.advanced.debugMode)→ 控制台 + userData/koring.log;否则仅控制台
|
||||
// 统一日志:debug 模式(config.advanced.debugMode)→ 控制台 + userData/koring.log;
|
||||
// dev(未打包)运行下直接写进程 stdout/stderr,日志实时输出到启动它的终端;否则仅控制台
|
||||
const log = createLogger('main');
|
||||
setDebugModeProvider(() => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { onUpdateStatus } from "@/api/update";
|
||||
import { useRouteStore } from "@/stores/routeStore";
|
||||
import { useUpdateDialogStore } from "@/stores/updateDialogStore";
|
||||
import { BUILD_MODE } from "@/lib/mode";
|
||||
import { VERSION } from "@/lib/version";
|
||||
import { VersionCard } from "@/components/VersionCard";
|
||||
|
||||
// 与 VersionCard 一致的构建模式配色(dev 橙 / beta 绿 / run 蓝),用于主按钮底色
|
||||
const modeGradients: Record<string, string> = {
|
||||
dev: "linear-gradient(135deg, #F59E0B, #D97706)",
|
||||
beta: "linear-gradient(135deg, #10B981, #059669)",
|
||||
run: "linear-gradient(135deg, #3B82F6, #2563EB)",
|
||||
};
|
||||
|
||||
/**
|
||||
* "发现新版本" 弹窗(全局,RootLayout 挂载):
|
||||
* - 主进程检查到新版本(状态进入 available)且不在版本更新页时自动弹出
|
||||
* - 开发者工具可通过 useUpdateDialogStore.show(version) 手动唤起(用于预览)
|
||||
* - 上半部分直接复用 VersionCard(模式渐变 + Silk + Logo,随构建模式变色)
|
||||
* - 按钮:稍后更新 / 立即更新(跳转版本更新页面)
|
||||
*/
|
||||
export function UpdateAvailableDialog() {
|
||||
const open = useUpdateDialogStore((s) => s.open);
|
||||
const version = useUpdateDialogStore((s) => s.version);
|
||||
const hide = useUpdateDialogStore((s) => s.hide);
|
||||
const show = useUpdateDialogStore((s) => s.show);
|
||||
|
||||
const prevStateRef = useRef<string>("idle");
|
||||
const currentRouteRef = useRef<string>(useRouteStore.getState().current);
|
||||
|
||||
useEffect(() => {
|
||||
// 跟随路由(更新页自身不弹,避免打扰已在该页操作的用户)
|
||||
return useRouteStore.subscribe(() => {
|
||||
currentRouteRef.current = useRouteStore.getState().current;
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const unsub = onUpdateStatus((status) => {
|
||||
// 仅在「进入 available」这一跳变时自动弹出(checking→available / 再次手动检查会重新触发)
|
||||
const alreadyOpen = useUpdateDialogStore.getState().open;
|
||||
if (
|
||||
status.state === "available" &&
|
||||
prevStateRef.current !== "available" &&
|
||||
currentRouteRef.current !== "update" &&
|
||||
!alreadyOpen
|
||||
) {
|
||||
show(status.version);
|
||||
}
|
||||
prevStateRef.current = status.state;
|
||||
});
|
||||
return unsub;
|
||||
}, [show]);
|
||||
|
||||
const goUpdate = () => {
|
||||
hide();
|
||||
// 等关闭动画结束后再切换路由,避免弹窗关闭与 view transition 快照抢帧导致无过渡
|
||||
window.setTimeout(() => {
|
||||
useRouteStore.getState().navigate("update");
|
||||
}, 220);
|
||||
};
|
||||
|
||||
const gradient = modeGradients[BUILD_MODE] ?? modeGradients.run;
|
||||
const targetVersion = version || VERSION;
|
||||
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={(o) => !o && hide()}>
|
||||
<AlertDialogContent
|
||||
className="gap-0 overflow-hidden rounded-2xl p-0"
|
||||
style={{ width: "min(720px, calc(100vw - 2rem))", maxWidth: "min(720px, calc(100vw - 2rem))" }}
|
||||
>
|
||||
{/* 统一内边距容器:卡片与正文共用同一水平宽度(安全区不粘连边框) */}
|
||||
<div className="flex flex-col p-2.5 sm:p-3">
|
||||
{/* 上半部分:直接复用 VersionCard(全宽;关闭共享过渡名,避免打断路由切换动画) */}
|
||||
<VersionCard noViewTransition className="w-full" />
|
||||
|
||||
{/* 下半部分:与版本卡同宽的说明 + 按钮 */}
|
||||
<div className="flex flex-col gap-4 px-1 pt-4 pb-1">
|
||||
<div>
|
||||
<AlertDialogTitle className="font-heading text-base font-semibold text-foreground">
|
||||
版本更新可用
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription className="mt-1.5 text-[13px] leading-relaxed">
|
||||
当前版本 v{VERSION},发现新版本 v{targetVersion}。建议尽快更新以获得最新功能与修复。
|
||||
</AlertDialogDescription>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2.5">
|
||||
<button
|
||||
onClick={hide}
|
||||
className="flex-1 h-10 rounded-lg text-[13px] font-medium bg-foreground/[0.05] hover:bg-foreground/[0.1] text-foreground/70 hover:text-foreground transition-colors cursor-pointer"
|
||||
>
|
||||
稍后更新
|
||||
</button>
|
||||
<button
|
||||
onClick={goUpdate}
|
||||
className="flex-[1.4] h-10 rounded-lg text-[13px] font-semibold text-white transition-colors cursor-pointer"
|
||||
style={{ background: gradient, boxShadow: "0 2px 10px rgba(0,0,0,0.12)" }}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.filter = "brightness(1.08)")}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.filter = "none")}
|
||||
>
|
||||
立即更新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
@@ -40,6 +40,11 @@ interface VersionCardProps {
|
||||
* 并显示「查看更新」小字提示;其他页面为 false
|
||||
*/
|
||||
isSettingPage?: boolean;
|
||||
/**
|
||||
* 关闭共享元素过渡(view-transition-name):弹窗/浮层里复用 VersionCard 时开启,
|
||||
* 避免与页面上的 VersionCard 重名导致路由切换过渡失效
|
||||
*/
|
||||
noViewTransition?: boolean;
|
||||
}
|
||||
|
||||
export function VersionCard({
|
||||
@@ -49,6 +54,7 @@ export function VersionCard({
|
||||
simple = false,
|
||||
oobe = false,
|
||||
isSettingPage = false,
|
||||
noViewTransition = false,
|
||||
}: VersionCardProps) {
|
||||
const color = modeColors[overrideMode ?? BUILD_MODE] ?? modeColors.run;
|
||||
const gradient = modeGradients[overrideMode ?? BUILD_MODE] ?? modeGradients.run;
|
||||
@@ -76,8 +82,13 @@ export function VersionCard({
|
||||
)}
|
||||
onClick={handleCardClick}
|
||||
// 共享元素过渡:路由切换时(startViewTransition),新旧页面中同名 view-transition-name
|
||||
// 的元素会从上一个位置平滑移动/形变到当前页面的位置
|
||||
style={{ viewTransitionName: "version-card" } as React.CSSProperties}
|
||||
// 的元素会从上一个位置平滑移动/形变到当前页面的位置。
|
||||
// 浮层/弹窗复用(noViewTransition)时关闭,避免与页面卡片重名打断过渡。
|
||||
style={
|
||||
noViewTransition
|
||||
? undefined
|
||||
: ({ viewTransitionName: "version-card" } as React.CSSProperties)
|
||||
}
|
||||
>
|
||||
{/* 背景层 */}
|
||||
<div className="absolute inset-0" style={{ background: gradient }} />
|
||||
|
||||
@@ -177,6 +177,7 @@ export function BackgroundLayer() {
|
||||
const imageLayerStyle = (url: string): React.CSSProperties => ({
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
pointerEvents: "none", // 背景层永不接收任何指针事件
|
||||
backgroundImage: `url(${url})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
|
||||
@@ -11,11 +11,11 @@ import {
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Select,
|
||||
Switch,
|
||||
TextArea,
|
||||
} from "@heroui/react";
|
||||
import { Check, ChevronDown, FolderOpen, FolderSearch, Loader2 } from "lucide-react";
|
||||
import { ipcInvoke } from "@/api/ipc";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { SettingRow } from "./SettingRow";
|
||||
|
||||
export interface SettingOption {
|
||||
@@ -51,6 +51,7 @@ export function SettingSelect({
|
||||
return (
|
||||
<SettingRow label={label} desc={desc}>
|
||||
<Select.Root
|
||||
aria-label={label}
|
||||
selectedKey={selectedKey}
|
||||
onSelectionChange={(keys) => {
|
||||
// RAC 单选时可能传 Key | null,也可能传 Set<Key>;两种形状都兼容
|
||||
@@ -119,6 +120,7 @@ export function SettingNumberField({
|
||||
<SettingRow label={label} desc={desc}>
|
||||
<div className={`flex items-center gap-1.5 ${className ?? ""}`}>
|
||||
<NumberField.Root
|
||||
aria-label={label}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
minValue={min}
|
||||
@@ -162,12 +164,12 @@ export function SettingSwitch({
|
||||
}) {
|
||||
return (
|
||||
<SettingRow label={label} desc={desc}>
|
||||
{/* 注:HeroUI 3 基于 react-aria,Switch 使用 onChange 而非 onValueChange */}
|
||||
<Switch isSelected={checked} onChange={onChange}>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
</Switch>
|
||||
{/* shadcn Switch(@base-ui/react/switch):替代 HeroUI 3 Switch(鼠标点击不触发 change) */}
|
||||
<Switch
|
||||
aria-label={label}
|
||||
checked={checked}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
</SettingRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
"use client"
|
||||
|
||||
/**
|
||||
* shadcn 风格 Select(基于 @base-ui/react/select,与项目 switch/radio/slider 同底座)。
|
||||
* 用法:
|
||||
* <Select value={v} onValueChange={setV}>
|
||||
* <SelectTrigger aria-label="x"><SelectValue placeholder="请选择" /></SelectTrigger>
|
||||
* <SelectContent>
|
||||
* <SelectItem value="a">A</SelectItem>
|
||||
* </SelectContent>
|
||||
* </Select>
|
||||
*/
|
||||
|
||||
import { Select as SelectPrimitive } from "@base-ui/react/select"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Select({
|
||||
className,
|
||||
...props
|
||||
}: SelectPrimitive.Root.Props<string>) {
|
||||
return (
|
||||
<SelectPrimitive.Root
|
||||
data-slot="select"
|
||||
className={cn("", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
className,
|
||||
placeholder,
|
||||
...props
|
||||
}: SelectPrimitive.Value.Props & { placeholder?: string }) {
|
||||
if (placeholder) {
|
||||
return (
|
||||
<SelectPrimitive.Value
|
||||
placeholder={placeholder}
|
||||
data-slot="select-value"
|
||||
className={cn("text-[13px] text-foreground data-[placeholder]:text-muted-foreground/60", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<SelectPrimitive.Value
|
||||
data-slot="select-value"
|
||||
className={cn("text-[13px] text-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: SelectPrimitive.Trigger.Props & { size?: "sm" | "default" }) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"inline-flex h-8 w-full shrink-0 items-center justify-between gap-2 rounded-lg border border-border/40 bg-white/60 px-3 text-[13px] text-foreground outline-none transition-colors select-none dark:bg-black/30 dark:border-white/[0.08] focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 hover:border-ring/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectIcon({ className, ...props }: SelectPrimitive.Icon.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Icon
|
||||
data-slot="select-icon"
|
||||
className={cn("shrink-0 text-muted-foreground/60", className)}
|
||||
{...props}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</SelectPrimitive.Icon>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
position = "popper",
|
||||
side = "bottom",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: SelectPrimitive.Popup.Props & {
|
||||
position?: "popper" | "item-aligned"
|
||||
side?: "top" | "bottom" | "left" | "right"
|
||||
sideOffset?: number
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Positioner
|
||||
side={side}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50",
|
||||
position === "popper" &&
|
||||
"w-[var(--anchor-width)] min-w-[10rem]"
|
||||
)}
|
||||
>
|
||||
<SelectPrimitive.Popup
|
||||
data-slot="select-content"
|
||||
className={cn(
|
||||
"relative max-h-72 min-w-[10rem] overflow-y-auto scroll-area rounded-xl border border-border/50 bg-background p-1.5 text-[13px] text-foreground shadow-xl outline-none",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</SelectPrimitive.Positioner>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SelectPrimitive.Item.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"group/item relative flex w-full cursor-pointer items-center gap-2 rounded-lg py-1.5 pr-8 pl-2.5 text-[13px] text-foreground outline-none select-none",
|
||||
"data-highlighted:bg-muted data-selected:bg-primary/10 data-selected:text-primary",
|
||||
"data-disabled:pointer-events-none data-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.ItemText className="min-w-0 flex-1 truncate">
|
||||
{children}
|
||||
</SelectPrimitive.ItemText>
|
||||
<SelectPrimitive.ItemIndicator className="absolute right-2 text-primary">
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden
|
||||
>
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({ className, ...props }: SelectPrimitive.Label.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("px-2.5 py-1.5 text-[12px] text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: SelectPrimitive.Separator.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectIcon,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { type ReactNode } from "react";
|
||||
import { BackgroundLayer } from "@/components/background/BackgroundLayer";
|
||||
import { SystemLayer } from "@/components/system/SystemLayer";
|
||||
import { ConfirmDialog } from "@/components/ui/confirm-dialog";
|
||||
import { UpdateAvailableDialog } from "@/components/UpdateAvailableDialog";
|
||||
import { Toaster } from "sonner";
|
||||
import { useA11yStore } from "@/stores/a11yStore";
|
||||
import clsx from "clsx";
|
||||
@@ -55,6 +56,9 @@ export function RootLayout({
|
||||
{/* Global confirm dialog */}
|
||||
<ConfirmDialog />
|
||||
|
||||
{/* 全局:发现新版本弹窗(检查到新版本时自动弹出) */}
|
||||
<UpdateAvailableDialog />
|
||||
|
||||
{/* Sonner toaster */}
|
||||
<Toaster
|
||||
position="bottom-right"
|
||||
|
||||
+2
-1
@@ -4,7 +4,8 @@
|
||||
* 规则(与主进程 electron/core/logger.ts 对齐):
|
||||
* - 默认(非 debug):error/warn/info 输出到控制台(DevTools),debug 不输出;
|
||||
* - 用户开启「调试模式」(config.advanced.debugMode)后:debug 也输出,
|
||||
* 并经由 electronAPI.log → 主进程 log:write 桥写入 userData/koring.log。
|
||||
* 并经由 electronAPI.log → 主进程 log:write 桥汇入主进程统一日志:
|
||||
* dev(未打包)运行下同步输出到启动终端的 stdout/stderr,同时写入 userData/koring.log。
|
||||
*/
|
||||
|
||||
import { useConfigStore } from "@/stores/configStore";
|
||||
|
||||
@@ -31,6 +31,7 @@ export function ResourceDebug() {
|
||||
const [heap, setHeap] = useState<JsHeapInfo | null>(null);
|
||||
const [domNodes, setDomNodes] = useState(0);
|
||||
const [logInfo, setLogInfo] = useState<{ filePath: string | null; debugMode: boolean } | null>(null);
|
||||
const [hitInfo, setHitInfo] = useState<string[] | null>(null);
|
||||
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
const snapshot = useResourceStore((s) => s.snapshot);
|
||||
@@ -38,6 +39,54 @@ export function ResourceDebug() {
|
||||
const clearFree = useResourceStore((s) => s.clearFree);
|
||||
const resetCounters = useResourceStore((s) => s.resetCounters);
|
||||
|
||||
/** 诊断「控件无法点击」:找出全屏覆盖且 pointer-events≠none 的元素,并采样几个点位的最上层元素 */
|
||||
const runHitTest = () => {
|
||||
const lines: string[] = [];
|
||||
const all = document.querySelectorAll<HTMLElement>("body *");
|
||||
// 1) 疑似全屏拦截层
|
||||
const seen = new Set<HTMLElement>();
|
||||
all.forEach((el) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const cs = getComputedStyle(el);
|
||||
const covers =
|
||||
rect.width >= window.innerWidth * 0.97 && rect.height >= window.innerHeight * 0.97;
|
||||
const clickable = cs.pointerEvents !== "none";
|
||||
if (!covers || !clickable || seen.has(el)) return;
|
||||
seen.add(el);
|
||||
const tag = el.tagName.toLowerCase();
|
||||
lines.push(
|
||||
`[全屏可点] <${tag}${el.id ? `#${el.id}` : ""}> pos=${cs.position} z=${cs.zIndex} class="${String(el.className).slice(0, 120)}"`,
|
||||
);
|
||||
});
|
||||
// 2) 采样几个位置的最上层元素
|
||||
const points: Array<[number, number, string]> = [
|
||||
[0.5, 0.5, "中央"],
|
||||
[0.5, 0.12, "标题栏下沿"],
|
||||
[0.25, 0.6, "内容区"],
|
||||
[0.75, 0.85, "内容区右下"],
|
||||
];
|
||||
for (const [fx, fy, label] of points) {
|
||||
const el = document.elementFromPoint(Math.floor(innerWidth * fx), Math.floor(innerHeight * fy));
|
||||
if (!el || el === document.body) {
|
||||
lines.push(`[${label}] (${fx},${fy}) → 无元素/body`);
|
||||
continue;
|
||||
}
|
||||
const target = el as HTMLElement;
|
||||
const cs = getComputedStyle(target);
|
||||
const chain: string[] = [];
|
||||
let node: HTMLElement | null = target;
|
||||
for (let i = 0; node && i < 5; i++) {
|
||||
chain.push(
|
||||
`${node.tagName.toLowerCase()}${node.id ? `#${node.id}` : ""}${node.className ? `.${String(node.className).split(/\s+/).filter(Boolean).slice(0, 2).join(".")}` : ""}`,
|
||||
);
|
||||
node = node.parentElement;
|
||||
}
|
||||
lines.push(`[${label}] (${fx},${fy}) → ${chain.join(" < ")} | pe=${cs.pointerEvents}`);
|
||||
}
|
||||
if (lines.length === 0) lines.push("未发现明显拦截层(可再多点几个位置)");
|
||||
setHitInfo(lines);
|
||||
};
|
||||
|
||||
const sample = async () => {
|
||||
setHeap(readJsHeap());
|
||||
setDomNodes(document.querySelectorAll("*").length);
|
||||
@@ -140,6 +189,31 @@ export function ResourceDebug() {
|
||||
</GlassCard>
|
||||
</div>
|
||||
|
||||
{/* 点击拦截诊断 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-foreground/40 uppercase tracking-wider mb-3">
|
||||
点击拦截诊断(控件点了没反应时使用)
|
||||
</h3>
|
||||
<GlassCard>
|
||||
<div className="flex items-center justify-between gap-2 mb-2">
|
||||
<p className="text-[13px] text-muted-foreground">
|
||||
找出「覆盖全屏且可接收指针」的元素,并采样 4 个点位的最上层元素
|
||||
</p>
|
||||
<button
|
||||
onClick={runHitTest}
|
||||
className="inline-flex items-center gap-1 rounded-md border border-foreground/10 px-2 py-1 text-[12px] text-muted-foreground hover:text-foreground transition-colors cursor-pointer"
|
||||
>
|
||||
检测
|
||||
</button>
|
||||
</div>
|
||||
{hitInfo && (
|
||||
<pre className="max-h-56 overflow-auto rounded-md bg-foreground/[0.04] p-3 text-[12px] leading-relaxed font-mono whitespace-pre-wrap text-foreground/80">
|
||||
{hitInfo.join("\n")}
|
||||
</pre>
|
||||
)}
|
||||
</GlassCard>
|
||||
</div>
|
||||
|
||||
{/* 进程工作集 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-foreground/40 uppercase tracking-wider mb-3">
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
type UpdateStatusPayload,
|
||||
type VersionCompareResult,
|
||||
} from "@/api/update";
|
||||
import { useUpdateDialogStore } from "@/stores/updateDialogStore";
|
||||
|
||||
/** 更新功能测试:版本识别 / 检查 / 介绍 / 比对 / 下载 */
|
||||
export function UpdateDebug() {
|
||||
@@ -71,7 +72,27 @@ export function UpdateDebug() {
|
||||
</GlassCard>
|
||||
</div>
|
||||
|
||||
{/* 2. 设置版本号 */}
|
||||
{/* 2. 新版本弹窗预览(唤起全局"发现新版本"弹窗) */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-foreground/40 uppercase tracking-wider mb-3">
|
||||
新版本弹窗预览
|
||||
</h3>
|
||||
<GlassCard>
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<Button size="sm" onClick={() => useUpdateDialogStore.getState().show(s?.version || "9.9.9")}>
|
||||
唤起新版本弹窗
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => useUpdateDialogStore.getState().hide()}>
|
||||
关闭弹窗
|
||||
</Button>
|
||||
<span className="text-[12px] text-muted-foreground font-mono ml-1">
|
||||
版本:{s?.version || "9.9.9"}(无可用版本时用 9.9.9 预览)
|
||||
</span>
|
||||
</div>
|
||||
</GlassCard>
|
||||
</div>
|
||||
|
||||
{/* 3. 设置版本号 */}
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-foreground/40 uppercase tracking-wider mb-3">
|
||||
设置测试版本号
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useConfigStore } from "@/stores/configStore";
|
||||
import {
|
||||
SettingCard,
|
||||
SettingSelect,
|
||||
SettingSwitch,
|
||||
SettingNumberField,
|
||||
SettingFilePicker,
|
||||
fieldCls,
|
||||
@@ -28,7 +27,7 @@ export function AdvancedSetting() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader title="高级设置" desc="游戏高级启动参数、调试选项与实验性功能" />
|
||||
<PageHeader title="高级设置" desc="游戏高级启动参数与实验性功能" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* 启动行为 */}
|
||||
@@ -163,20 +162,6 @@ export function AdvancedSetting() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 调试 */}
|
||||
<div>
|
||||
<SectionTitle>调试</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<SettingCard>
|
||||
<SettingSwitch
|
||||
label="调试模式"
|
||||
desc="启用后附加 -Dkoring.debugMode=true 并在控制台输出详细日志,可能影响性能"
|
||||
checked={adv.debugMode}
|
||||
onChange={(v) => setAdvanced({ debugMode: v })}
|
||||
/>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,8 @@ import { BUILD_MODE } from "@/lib/mode";
|
||||
import { BUILD_COMMIT, BUILD_ID } from "@/lib/buildInfo";
|
||||
import { ExternalLink, GitFork, RotateCcw, ChevronDown } from "lucide-react";
|
||||
import { Link, Select, ListBox, ListBoxItem } from "@heroui/react";
|
||||
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||
import { useConfigStore } from "@/stores/configStore";
|
||||
import { SettingCard, SettingRow, SettingSwitch, PageHeader, SectionTitle } from "@/components/setting";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
getUpdateChannels,
|
||||
@@ -47,6 +48,9 @@ export function AboutSetting() {
|
||||
const [countdown, setCountdown] = useState(5);
|
||||
const canConfirm = countdown <= 0;
|
||||
|
||||
const adv = useConfigStore((s) => s.config.advanced);
|
||||
const setAdvanced = useConfigStore((s) => s.setAdvanced);
|
||||
|
||||
// 设备识别码(组合指纹:主板/硬盘/BIOS → 回退系统安装标识)
|
||||
const [device, setDevice] = useState<DeviceIdentity | null>(null);
|
||||
useEffect(() => {
|
||||
@@ -201,6 +205,20 @@ export function AboutSetting() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SectionTitle>调试</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
<SettingCard>
|
||||
<SettingSwitch
|
||||
label="调试模式"
|
||||
desc="启用后附加 -Dkoring.debugMode=true 并在控制台输出详细日志(dev 运行时会同步输出到终端),可能影响性能"
|
||||
checked={adv?.debugMode ?? false}
|
||||
onChange={(v) => setAdvanced({ debugMode: v })}
|
||||
/>
|
||||
</SettingCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<SectionTitle>相关链接</SectionTitle>
|
||||
<div className="space-y-3">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
import { useConfigStore } from "@/stores/configStore";
|
||||
import { Switch, Input } from "@heroui/react";
|
||||
import { Input } from "@heroui/react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { ShieldCheck } from "lucide-react";
|
||||
import { SettingCard, SettingRow, PageHeader, SectionTitle, fieldCls } from "@/components/setting";
|
||||
|
||||
@@ -30,11 +31,7 @@ export function SecurityIdSetting() {
|
||||
label="启用第三方认证"
|
||||
desc="使用自定义认证服务器替代 Microsoft 认证(适用于离线服务器)"
|
||||
>
|
||||
<Switch isSelected={enabled} onChange={handleToggle}>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
</Switch>
|
||||
<Switch aria-label="启用第三方认证" checked={enabled} onCheckedChange={handleToggle} />
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useA11yStore } from "@/stores/a11yStore";
|
||||
import { Switch } from "@heroui/react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||
|
||||
export function A11ySetting() {
|
||||
@@ -15,31 +15,19 @@ export function A11ySetting() {
|
||||
<div className="space-y-3">
|
||||
<SettingCard>
|
||||
<SettingRow label="减少动画" desc="关闭页面切换动画和背景动效">
|
||||
<Switch isSelected={reduceMotion} onChange={setReduceMotion}>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
</Switch>
|
||||
<Switch aria-label="减少动画" checked={reduceMotion} onCheckedChange={setReduceMotion} />
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
|
||||
<SettingCard>
|
||||
<SettingRow label="减少透明度" desc="将磨砂玻璃效果替换为纯色背景,提升可读性">
|
||||
<Switch isSelected={reduceTransparency} onChange={setReduceTransparency}>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
</Switch>
|
||||
<Switch aria-label="减少透明度" checked={reduceTransparency} onCheckedChange={setReduceTransparency} />
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
|
||||
<SettingCard>
|
||||
<SettingRow label="高对比度" desc="增强文字与背景的对比度,改善可读性">
|
||||
<Switch isSelected={highContrast} onChange={setHighContrast}>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
</Switch>
|
||||
<Switch aria-label="高对比度" checked={highContrast} onCheckedChange={setHighContrast} />
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useThemeStore, type DarkMode } from "@/stores/themeStore";
|
||||
import { useBackgroundStore } from "@/stores/backgroundStore";
|
||||
import { Switch, Button, Slider } from "@heroui/react";
|
||||
import { Button, Slider } from "@heroui/react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { DEFAULT_BG } from "@/lib/mode";
|
||||
import clsx from "clsx";
|
||||
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
|
||||
@@ -207,11 +208,7 @@ export function ThemeBgSetting() {
|
||||
|
||||
<SettingCard>
|
||||
<SettingRow label="背景图片视差" desc="背景图片随窗口滚动产生视差位移">
|
||||
<Switch isSelected={parallax} onChange={setParallax}>
|
||||
<Switch.Control>
|
||||
<Switch.Thumb />
|
||||
</Switch.Control>
|
||||
</Switch>
|
||||
<Switch aria-label="背景图片视差" checked={parallax} onCheckedChange={setParallax} />
|
||||
</SettingRow>
|
||||
</SettingCard>
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
/**
|
||||
* "发现新版本"弹窗开关(渲染端共享):
|
||||
* - UpdateAvailableDialog 自动弹窗(状态进入 available)与
|
||||
* 开发者工具的手动唤起都通过这里控制
|
||||
*/
|
||||
interface UpdateDialogState {
|
||||
open: boolean;
|
||||
/** 目标(新)版本号;为空时展示当前版本 */
|
||||
version: string;
|
||||
show: (version?: string) => void;
|
||||
hide: () => void;
|
||||
}
|
||||
|
||||
export const useUpdateDialogStore = create<UpdateDialogState>((set) => ({
|
||||
open: false,
|
||||
version: "",
|
||||
show: (version = "") => set({ open: true, version }),
|
||||
hide: () => set({ open: false }),
|
||||
}));
|
||||
Reference in New Issue
Block a user