feat/optimization-and-audio #1

Merged
zhilv merged 4 commits from feat/optimization-and-audio into main 2026-04-26 20:51:23 +08:00
4 changed files with 362 additions and 388 deletions
Showing only changes of commit a182c64f82 - Show all commits

View File

@@ -1,7 +1,7 @@
{
"name": "frontend",
"private": true,
"version": "0.1.3",
"version": "0.1.4",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -137,9 +137,6 @@ const renderInlineLinks = (text: string): JSX.Element[] => {
const App: ParentComponent = (props) => {
const location = useLocation();
const [version] = createResource(versionApi);
const [copyState, setCopyState] = createSignal<"idle" | "done" | "error">(
"idle",
);
const [updateDialogOpen, setUpdateDialogOpen] = createSignal(false);
const [updateCheckState, setUpdateCheckState] =
createSignal<UpdateCheckState>("idle");
@@ -197,31 +194,6 @@ const App: ParentComponent = (props) => {
return error instanceof Error ? error.message : "版本信息获取失败";
});
const versionPayloadText = createMemo(() =>
[
`Version: ${versionText()}`,
`Mode: ${modeText()}`,
`Commit: ${commitText()}`,
`Build: ${buildText()}`,
`Author: ${authorText()}`,
`Email: ${emailText()}`,
].join("\n"),
);
const updateSummaryText = createMemo(() => {
if (updateCheckState() === "checking") {
return "更新检查中...";
}
if (updateCheckState() === "available") {
return `发现新版本:${latestRelease()?.tag_name ?? "-"}`;
}
if (updateCheckState() === "latest") {
return "已是最新版本";
}
if (updateCheckState() === "error") {
return updateCheckError() || "更新检查失败";
}
return "未检查更新";
});
const releaseNotesBlocks = createMemo(() =>
parseMarkdownBlocks(latestRelease()?.body ?? ""),
);
@@ -231,6 +203,14 @@ const App: ParentComponent = (props) => {
const releaseLink = createMemo(
() => latestRelease()?.html_url || RELEASES_PAGE_URL,
);
const safeValue = (value: string) => (value === "unknown" ? "-" : value);
const hasUpdateBadge = createMemo(() => updateCheckState() === "available");
const updateDialogTitle = createMemo(() => {
if (updateCheckState() === "available") {
return `发现更新 ${latestRelease()?.tag_name ?? ""}`;
}
return "更新信息";
});
onMount(() => {
const unsubscribe = settingsStore.subscribe((state) => {
@@ -242,16 +222,6 @@ const App: ParentComponent = (props) => {
});
});
const handleCopyVersion = async () => {
try {
await navigator.clipboard.writeText(versionPayloadText());
setCopyState("done");
} catch {
setCopyState("error");
}
window.setTimeout(() => setCopyState("idle"), 1800);
};
const performUpdateCheck = async (manual = false) => {
if (updateCheckState() === "checking") {
return;
@@ -276,17 +246,17 @@ const App: ParentComponent = (props) => {
setRuntimeTarget(target);
const hasNewVersion = isRemoteVersionNewer(versionText(), release.tag_name);
setLatestRelease(release);
setMatchedAsset(resolveAssetForRuntime(release.assets, target));
if (!hasNewVersion) {
setUpdateCheckState("latest");
if (manual) {
setLatestRelease(release);
setMatchedAsset(resolveAssetForRuntime(release.assets, target));
setUpdateDialogOpen(true);
}
return;
}
setLatestRelease(release);
setMatchedAsset(resolveAssetForRuntime(release.assets, target));
setUpdateCheckState("available");
setUpdateDialogOpen(true);
} catch (error) {
@@ -425,48 +395,48 @@ const App: ParentComponent = (props) => {
<p class="mt-1 text-sm text-zinc-500">
{asideList().find((item) => isActive(item.url))?.label ?? "账号"}
</p>
<p class="mt-1 text-xs text-zinc-500">
: {modeText()}
</p>
<p class="mt-1 text-xs text-zinc-500">
: {isDebugMode() ? "已开启" : "已关闭"}
</p>
<p class="mt-3 text-xs font-medium tracking-[0.18em] text-cyan-700/75 uppercase">
Runtime
</p>
<div class="mt-2 grid gap-1 text-xs text-zinc-500 xl:block">
<p>Version: {versionText()}</p>
<p>Commit: {commitText()}</p>
<p>Build: {buildText()}</p>
<p>Author: {authorText()}</p>
<p>Email: {emailText()}</p>
</div>
<p
class={`mt-2 text-xs ${updateCheckState() === "error" ? "text-rose-500" : "text-zinc-500"}`}
>
: {updateSummaryText()}
</p>
<div class="mt-3 flex flex-wrap items-center gap-2">
<div class="mt-3 border-t border-zinc-200/80 pt-3">
<button
type="button"
class="rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-xs text-zinc-700 transition active:bg-zinc-200 hover:bg-zinc-100"
onClick={() => void handleCopyVersion()}
>
{copyState() === "done"
? "已复制"
: copyState() === "error"
? "复制失败"
: "复制版本信息"}
</button>
<button
type="button"
class="rounded-lg border border-cyan-200 bg-cyan-50 px-3 py-1.5 text-xs text-cyan-700 transition active:bg-cyan-200 hover:bg-cyan-100 disabled:cursor-not-allowed disabled:opacity-60"
class="flex w-full min-w-0 items-center gap-2 rounded-lg border border-zinc-200 bg-white/80 px-2.5 py-1.5 text-left text-xs text-zinc-500 transition hover:bg-white disabled:cursor-not-allowed disabled:opacity-60"
disabled={updateCheckState() === "checking"}
onClick={() => void performUpdateCheck(true)}
title="点击查看更新内容"
>
{updateCheckState() === "checking" ? "检查中..." : "检查更新"}
<span class="text-zinc-400"></span>
<span class="shrink-0 rounded-full border border-zinc-200 bg-zinc-100 px-2 py-0.5 text-[11px] font-medium text-zinc-700">
{safeValue(versionText())}
</span>
{hasUpdateBadge() ? (
<span
class="h-2 w-2 shrink-0 rounded-full bg-rose-500"
title={`新版本:${latestRelease()?.tag_name ?? "-"}`}
/>
) : null}
<span class="ml-auto text-[11px] text-zinc-400">
{updateCheckState() === "checking" ? "检查中..." : "查看更新"}
</span>
</button>
<details class="mt-2 rounded-lg border border-zinc-200/80 bg-white/70 px-2.5 py-2">
<summary class="cursor-pointer select-none text-[11px] text-zinc-500">
</summary>
<div class="mt-2 space-y-1 text-xs">
<p class={isDebugMode() ? "text-amber-600" : "text-zinc-600"}>
Mode: {safeValue(modeText())}
</p>
<p class="truncate text-zinc-600">Commit: {safeValue(commitText())}</p>
<p class="truncate text-zinc-600">Build: {safeValue(buildText())}</p>
<p class="truncate text-zinc-600">Author: {safeValue(authorText())}</p>
<p class="truncate text-zinc-600">Email: {safeValue(emailText())}</p>
</div>
</details>
</div>
{updateCheckState() === "error" ? (
<p class="mt-2 text-xs text-rose-500">
{updateCheckError() || "更新检查失败"}
</p>
) : null}
{versionErrorText() ? (
<p class="mt-2 text-xs text-rose-500">{versionErrorText()}</p>
) : null}
@@ -488,7 +458,7 @@ const App: ParentComponent = (props) => {
}
setUpdateDialogOpen(false);
}}
title={`发现更新 ${latestRelease()?.tag_name ?? ""}`}
title={updateDialogTitle()}
widthClass="max-w-3xl"
closeOnOverlay={downloadState() !== "downloading"}
footer={

View File

@@ -283,7 +283,13 @@ const CourseWorkspace = (props: CourseWorkspaceProps) => {
>
<For each={props.recordTypeOptions}>
{(item) => (
<option value={item.value}>{item.label}</option>
<option
value={item.value}
disabled={item.value === "/discuss"}
class={item.value === "/discuss" ? "text-zinc-400" : ""}
>
{item.label}
</option>
)}
</For>
</select>

View File

@@ -1,4 +1,4 @@
import { createMemo, createSignal, For, onCleanup, onMount } from "solid-js";
import { createMemo, createSignal, For, onCleanup, onMount } from "solid-js";
import { fetchDebugConfig, updateDebugConfig } from "~/service/debugLog";
import { hostApi } from "~/service/wk";
import { getMergedHosts, settingsStore } from "~/store/settings";
@@ -117,367 +117,365 @@ const Setting = () => {
void refreshDebugConfig();
});
const panelClass =
"rounded-[28px] border border-white/80 bg-white/85 p-5 shadow-[0_18px_50px_-28px_rgba(15,23,42,0.25)]";
const sectionCardClass = "rounded-2xl border border-zinc-200 bg-zinc-50/80 p-4";
return (
<div class="flex h-full min-h-0 flex-col overflow-hidden">
<div class="flex shrink-0 items-center justify-between gap-4 rounded-[28px] border border-white/80 bg-[linear-gradient(135deg,rgba(255,255,255,0.92),rgba(240,249,255,0.96))] px-5 py-4 shadow-[0_18px_45px_-28px_rgba(15,23,42,0.18)]">
<div class="flex shrink-0 items-center justify-between gap-4 rounded-[28px] border border-white/80 bg-[linear-gradient(125deg,rgba(255,255,255,0.96),rgba(240,249,255,0.95)_55%,rgba(236,254,255,0.9))] px-6 py-5 shadow-[0_22px_50px_-32px_rgba(15,23,42,0.22)]">
<div>
<p class="text-xs font-medium tracking-[0.26em] text-cyan-700/75 uppercase">
Settings Center
<p class="text-xs font-semibold tracking-[0.28em] text-cyan-700/80 uppercase">
Preference Center
</p>
<h1 class="mt-2 text-2xl font-semibold text-zinc-900"></h1>
<p class="mt-1 text-sm text-zinc-500">
Host
Host
</p>
</div>
<div class="rounded-2xl border border-zinc-200 bg-white/90 px-4 py-3 text-right shadow-sm">
<p class="text-sm font-medium text-zinc-800"></p>
<p class="mt-1 text-xs text-zinc-500">
<div class="rounded-2xl border border-zinc-200/80 bg-white/90 px-4 py-3 text-right shadow-sm">
<p class="text-sm font-medium text-zinc-800"></p>
<p class="mt-1 text-xs leading-5 text-zinc-500">
Host
</p>
</div>
</div>
<div class="mt-4 min-h-0 flex-1 overflow-y-auto pr-1">
<div class="grid gap-4 xl:grid-cols-[minmax(0,1.1fr)_minmax(0,0.9fr)]">
<div class="flex flex-col gap-4">
<div class="rounded-[28px] border border-white/80 bg-white/85 p-4 shadow-[0_18px_50px_-28px_rgba(15,23,42,0.25)]">
<div class="mt-5 min-h-0 flex-1 overflow-hidden">
<div class="grid h-full min-h-full w-full gap-5 lg:grid-cols-2">
<div class="flex min-h-0 h-full flex-col gap-5 overflow-y-auto pr-1">
<section class={panelClass}>
<div class="flex items-center justify-between gap-4">
<div>
<p class="text-base font-semibold text-zinc-900"></p>
<p class="mt-0.5 text-xs text-zinc-500">
</p>
<p class="text-lg font-semibold text-zinc-900"></p>
<p class="mt-1 text-xs text-zinc-500"></p>
</div>
<button
type="button"
class="rounded-lg border border-rose-200 bg-rose-50 px-2.5 py-1.5 text-xs text-rose-600 transition hover:bg-rose-100 active:bg-rose-200"
class="rounded-lg border border-rose-200 bg-rose-50 px-3 py-1.5 text-xs text-rose-600 transition hover:bg-rose-100 active:bg-rose-200"
onClick={() => settingsStore.getState().clearAllPersistedData()}
>
</button>
</div>
<div class="mt-3 grid gap-2">
<div class="flex items-center justify-between gap-2 rounded-xl border border-zinc-200 bg-zinc-50/80 px-3 py-2">
<div>
<p class="text-sm font-medium text-zinc-900"></p>
<p class="text-xs text-zinc-500"></p>
<div class="mt-4 grid gap-3">
<div class={sectionCardClass}>
<div class="flex items-center justify-between gap-3">
<div>
<p class="text-sm font-medium text-zinc-900"></p>
<p class="mt-1 text-xs text-zinc-500"></p>
</div>
<button
type="button"
class="rounded-lg border border-zinc-200 bg-white px-2.5 py-1.5 text-xs text-zinc-700 transition hover:bg-zinc-100 active:bg-zinc-200"
onClick={() =>
settingsStore.getState().clearPersistedSection("accounts")
}
>
</button>
</div>
<button
type="button"
class="rounded-lg border border-zinc-200 bg-white px-2 py-1 text-xs text-zinc-700 transition hover:bg-zinc-100 active:bg-zinc-200"
onClick={() =>
settingsStore.getState().clearPersistedSection("accounts")
}
>
</button>
</div>
<div class="flex items-center justify-between gap-2 rounded-xl border border-zinc-200 bg-zinc-50/80 px-3 py-2">
<div>
<p class="text-sm font-medium text-zinc-900"></p>
<p class="text-xs text-zinc-500"></p>
<div class={sectionCardClass}>
<div class="flex items-center justify-between gap-3">
<div>
<p class="text-sm font-medium text-zinc-900"></p>
<p class="mt-1 text-xs text-zinc-500"></p>
</div>
<button
type="button"
class="rounded-lg border border-zinc-200 bg-white px-2.5 py-1.5 text-xs text-zinc-700 transition hover:bg-zinc-100 active:bg-zinc-200"
onClick={() =>
settingsStore.getState().clearPersistedSection("records")
}
>
</button>
</div>
<button
type="button"
class="rounded-lg border border-zinc-200 bg-white px-2 py-1 text-xs text-zinc-700 transition hover:bg-zinc-100 active:bg-zinc-200"
onClick={() =>
settingsStore.getState().clearPersistedSection("records")
}
>
</button>
</div>
<div class="flex items-center justify-between gap-2 rounded-xl border border-zinc-200 bg-zinc-50/80 px-3 py-2">
<div>
<p class="text-sm font-medium text-zinc-900"></p>
<p class="text-xs text-zinc-500"></p>
<div class={sectionCardClass}>
<div class="flex items-center justify-between gap-3">
<div>
<p class="text-sm font-medium text-zinc-900"></p>
<p class="mt-1 text-xs text-zinc-500"></p>
</div>
<button
type="button"
class="rounded-lg border border-zinc-200 bg-white px-2.5 py-1.5 text-xs text-zinc-700 transition hover:bg-zinc-100 active:bg-zinc-200"
onClick={() =>
settingsStore.getState().clearPersistedSection("logs")
}
>
</button>
</div>
<button
type="button"
class="rounded-lg border border-zinc-200 bg-white px-2 py-1 text-xs text-zinc-700 transition hover:bg-zinc-100 active:bg-zinc-200"
onClick={() =>
settingsStore.getState().clearPersistedSection("logs")
}
>
</button>
</div>
</div>
</div>
</section>
<div class="rounded-[28px] border border-white/80 bg-white/85 p-5 shadow-[0_18px_50px_-28px_rgba(15,23,42,0.25)]">
<p class="text-lg font-semibold text-zinc-900"></p>
<p class="mt-1 text-sm text-zinc-500">
使
</p>
</div>
<section class={panelClass}>
<div class="border-b border-zinc-200/70 pb-4">
<p class="text-lg font-semibold text-zinc-900"></p>
<p class="mt-1 text-sm text-zinc-500">
使
</p>
</div>
<div class="mt-4 grid gap-4">
<div class="rounded-2xl border border-zinc-200 bg-zinc-50/80 p-4">
<div class="flex items-center justify-between gap-3">
<div>
<p class="font-medium text-zinc-900"></p>
<p class="mt-1 text-sm text-zinc-500">
/ SSL
<div class="mt-4 grid gap-4 md:grid-cols-2">
<div class={`${sectionCardClass} md:col-span-2`}>
<div class="flex items-center justify-between gap-3">
<div>
<p class="font-medium text-zinc-900"></p>
<p class="mt-1 text-sm text-zinc-500">
/ SSL
</p>
</div>
<input
type="checkbox"
checked={state().debugEnabled}
disabled={debugSyncing()}
onChange={(event) =>
void handleDebugToggle(event.currentTarget.checked)
}
/>
</div>
<div class="mt-3 rounded-xl border border-zinc-200 bg-white px-4 py-3 text-sm text-zinc-600">
<p>{backendDebugState()?.enabled ? "已开启" : "已关闭"}</p>
<p class="mt-1">{backendDebugState()?.buildMode ?? "-"}</p>
<p class="mt-1">
{backendDebugState()?.proxyConfigured
? backendDebugState()?.proxy
: "未配置"}
</p>
<p class="mt-1">
SSL
{backendDebugState()?.skipSSLVerify ? "已配置" : "未配置"}
</p>
</div>
{debugError() ? (
<div class="mt-3 rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-600">
{debugError()}
</div>
) : null}
</div>
<div class={sectionCardClass}>
<div class="flex items-center justify-between gap-3">
<div>
<p class="font-medium text-zinc-900"></p>
<p class="mt-1 text-sm text-zinc-500"></p>
</div>
<input
type="checkbox"
checked={state().autoScrollLogs}
onChange={(event) =>
settingsStore
.getState()
.setAutoScrollLogs(event.currentTarget.checked)
}
/>
</div>
</div>
<div class={sectionCardClass}>
<div class="flex items-center justify-between gap-3">
<div>
<p class="font-medium text-zinc-900"></p>
<p class="mt-1 text-sm text-zinc-500"></p>
</div>
<input
type="checkbox"
checked={state().showLogTimestamps}
onChange={(event) =>
settingsStore
.getState()
.setShowLogTimestamps(event.currentTarget.checked)
}
/>
</div>
</div>
<div class={sectionCardClass}>
<label class="block">
<p class="font-medium text-zinc-900"></p>
<p class="mt-1 text-sm text-zinc-500"></p>
<select
class="mt-3 w-full rounded-xl border border-zinc-200 bg-white px-4 py-3 transition outline-none focus:border-cyan-400"
value={state().densityMode}
onChange={(event) =>
settingsStore
.getState()
.setDensityMode(
event.currentTarget.value as
| "comfortable"
| "compact",
)
}
>
<option value="comfortable"></option>
<option value="compact"></option>
</select>
</label>
</div>
<div class={sectionCardClass}>
<label class="block">
<div class="flex items-center justify-between gap-3">
<div>
<p class="font-medium text-zinc-900"></p>
<p class="mt-1 text-sm text-zinc-500">{state().logFontSize}px</p>
</div>
</div>
<input
class="mt-4 w-full"
type="range"
min="11"
max="16"
value={state().logFontSize}
onInput={(event) =>
settingsStore
.getState()
.setLogFontSize(Number(event.currentTarget.value))
}
/>
</label>
</div>
<div class={`${sectionCardClass} md:col-span-2`}>
<label class="block">
<div class="flex items-center justify-between gap-3">
<div>
<p class="font-medium text-zinc-900"></p>
<p class="mt-1 text-sm text-zinc-500">{state().sidebarWidth}px</p>
</div>
</div>
<input
class="mt-4 w-full"
type="range"
min="280"
max="380"
step="10"
value={state().sidebarWidth}
onInput={(event) =>
settingsStore
.getState()
.setSidebarWidth(Number(event.currentTarget.value))
}
/>
</label>
</div>
</div>
</section>
</div>
<div class="flex min-h-0 h-full flex-col gap-5 overflow-y-auto pr-1">
<section class={panelClass}>
<div class="border-b border-zinc-200 pb-4">
<p class="text-lg font-semibold text-zinc-900">Host </p>
<p class="mt-1 text-sm text-zinc-500">
Host Host
</p>
</div>
<div class={`${sectionCardClass} mt-4`}>
<div class="flex items-center justify-between gap-3">
<p class="font-medium text-zinc-900"> Host</p>
<button
type="button"
class="rounded-xl border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-700 transition hover:bg-zinc-100 active:bg-zinc-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={isLoadingRemoteHosts()}
onClick={() => void loadRemoteHosts()}
>
{isLoadingRemoteHosts() ? "获取中..." : "获取远程 Host"}
</button>
</div>
<div class="mt-3 grid gap-3 md:grid-cols-2">
<input
type="checkbox"
checked={state().debugEnabled}
disabled={debugSyncing()}
onChange={(event) =>
void handleDebugToggle(event.currentTarget.checked)
}
class="rounded-xl border border-zinc-200 bg-white px-4 py-3 transition outline-none focus:border-cyan-400"
value={hostLabel()}
onInput={(event) => setHostLabel(event.currentTarget.value)}
placeholder="名称,如:校内测试"
/>
<input
class="rounded-xl border border-zinc-200 bg-white px-4 py-3 transition outline-none focus:border-cyan-400"
value={hostValue()}
onInput={(event) => setHostValue(event.currentTarget.value)}
placeholder="Hostexample.com"
/>
</div>
<div class="mt-3 rounded-xl border border-zinc-200 bg-white px-4 py-3 text-sm text-zinc-600">
<p>{backendDebugState()?.enabled ? "已开启" : "已关闭"}</p>
<p class="mt-1">
{backendDebugState()?.buildMode ?? "-"}
</p>
<p class="mt-1">
{backendDebugState()?.proxyConfigured
? backendDebugState()?.proxy
: "未配置"}
</p>
<p class="mt-1">
SSL
{backendDebugState()?.skipSSLVerify ? "已配置" : "未配置"}
</p>
</div>
{debugError() ? (
<button
type="button"
class="mt-3 rounded-xl bg-cyan-500 px-4 py-2 text-sm text-white transition hover:bg-cyan-600 active:bg-cyan-700"
onClick={addLocalHost}
>
Host
</button>
{hostError() ? (
<div class="mt-3 rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-600">
{debugError()}
{hostError()}
</div>
) : null}
</div>
</section>
<div class="rounded-2xl border border-zinc-200 bg-zinc-50/80 p-4">
<div class="flex items-center justify-between gap-3">
<div>
<p class="font-medium text-zinc-900"></p>
<p class="mt-1 text-sm text-zinc-500">
</p>
</div>
<input
type="checkbox"
checked={state().autoScrollLogs}
onChange={(event) =>
settingsStore
.getState()
.setAutoScrollLogs(event.currentTarget.checked)
}
/>
</div>
<section class={panelClass}>
<div class="flex items-center justify-between border-b border-zinc-200 pb-4">
<p class="text-lg font-semibold text-zinc-900">Host </p>
<p class="text-xs text-zinc-500"></p>
</div>
<div class="rounded-2xl border border-zinc-200 bg-zinc-50/80 p-4">
<div class="flex items-center justify-between gap-3">
<div>
<p class="font-medium text-zinc-900"></p>
<p class="mt-1 text-sm text-zinc-500">
</p>
</div>
<input
type="checkbox"
checked={state().showLogTimestamps}
onChange={(event) =>
settingsStore
.getState()
.setShowLogTimestamps(event.currentTarget.checked)
}
/>
</div>
</div>
<div class="rounded-2xl border border-zinc-200 bg-zinc-50/80 p-4">
<label class="block">
<p class="font-medium text-zinc-900"></p>
<p class="mt-1 text-sm text-zinc-500">
</p>
<select
class="mt-3 w-full rounded-xl border border-zinc-200 bg-white px-4 py-3 transition outline-none focus:border-cyan-400"
value={state().densityMode}
onChange={(event) =>
settingsStore
.getState()
.setDensityMode(
event.currentTarget.value as
| "comfortable"
| "compact",
)
}
>
<option value="comfortable"></option>
<option value="compact"></option>
</select>
</label>
</div>
<div class="rounded-2xl border border-zinc-200 bg-zinc-50/80 p-4">
<label class="block">
<div class="flex items-center justify-between gap-3">
<div>
<p class="font-medium text-zinc-900"></p>
<p class="mt-1 text-sm text-zinc-500">
{state().logFontSize}px
</p>
</div>
</div>
<input
class="mt-4 w-full"
type="range"
min="11"
max="16"
value={state().logFontSize}
onInput={(event) =>
settingsStore
.getState()
.setLogFontSize(Number(event.currentTarget.value))
}
/>
</label>
</div>
<div class="rounded-2xl border border-zinc-200 bg-zinc-50/80 p-4">
<label class="block">
<div class="flex items-center justify-between gap-3">
<div>
<p class="font-medium text-zinc-900"></p>
<p class="mt-1 text-sm text-zinc-500">
{state().sidebarWidth}px
</p>
</div>
</div>
<input
class="mt-4 w-full"
type="range"
min="280"
max="380"
step="10"
value={state().sidebarWidth}
onInput={(event) =>
settingsStore
.getState()
.setSidebarWidth(Number(event.currentTarget.value))
}
/>
</label>
</div>
</div>
</div>
</div>
<div class="flex flex-col gap-4">
<div class="rounded-[28px] border border-white/80 bg-white/85 p-5 shadow-[0_18px_50px_-28px_rgba(15,23,42,0.25)]">
<div class="border-b border-zinc-200 pb-4">
<p class="text-lg font-semibold text-zinc-900">Host </p>
<p class="mt-1 text-sm text-zinc-500">
Host Host
</p>
</div>
<div class="mt-4 rounded-2xl border border-zinc-200 bg-zinc-50/80 p-4">
<div class="flex items-center justify-between gap-3">
<p class="font-medium text-zinc-900"> Host</p>
<button
type="button"
class="rounded-xl border border-zinc-200 bg-white px-3 py-2 text-sm text-zinc-700 transition hover:bg-zinc-100 active:bg-zinc-200 disabled:cursor-not-allowed disabled:opacity-60"
disabled={isLoadingRemoteHosts()}
onClick={() => void loadRemoteHosts()}
>
{isLoadingRemoteHosts() ? "获取中..." : "获取远程 Host"}
</button>
</div>
<div class="mt-3 grid gap-3 md:grid-cols-2">
<input
class="rounded-xl border border-zinc-200 bg-white px-4 py-3 transition outline-none focus:border-cyan-400"
value={hostLabel()}
onInput={(event) => setHostLabel(event.currentTarget.value)}
placeholder="名称,如:校内测试"
/>
<input
class="rounded-xl border border-zinc-200 bg-white px-4 py-3 transition outline-none focus:border-cyan-400"
value={hostValue()}
onInput={(event) => setHostValue(event.currentTarget.value)}
placeholder="Hostexample.com"
/>
</div>
<button
type="button"
class="mt-3 rounded-xl bg-cyan-500 px-4 py-2 text-sm text-white transition hover:bg-cyan-600 active:bg-cyan-700"
onClick={addLocalHost}
>
Host
</button>
{hostError() ? (
<div class="mt-3 rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-600">
{hostError()}
</div>
) : null}
</div>
<div class="mt-4 grid gap-4 xl:grid-cols-2">
<div class="rounded-2xl border border-zinc-200 bg-zinc-50/80 p-4">
<p class="font-medium text-zinc-900"> Host</p>
<div class="mt-3 flex flex-col gap-3">
<For each={state().localHosts}>
{(item) => (
<div class="rounded-xl border border-zinc-200 bg-white px-4 py-3">
<div class="flex items-start justify-between gap-3">
<div>
<p class="font-medium text-zinc-900">
{item.label}
</p>
<p class="mt-1 text-sm text-zinc-500">
{item.host}
</p>
<div class="mt-4 grid gap-4 xl:grid-cols-2">
<div class={sectionCardClass}>
<p class="font-medium text-zinc-900"> Host</p>
<div class="mt-3 flex flex-col gap-3">
<For each={state().localHosts}>
{(item) => (
<div class="rounded-xl border border-zinc-200 bg-white px-4 py-3">
<div class="flex items-start justify-between gap-3">
<div>
<p class="font-medium text-zinc-900">{item.label}</p>
<p class="mt-1 text-sm text-zinc-500">{item.host}</p>
</div>
<button
type="button"
class="rounded-lg border border-rose-200 px-3 py-1 text-xs text-rose-600 transition hover:bg-rose-50 active:bg-rose-100"
onClick={() =>
settingsStore.getState().removeLocalHost(item.host)
}
>
</button>
</div>
<button
type="button"
class="rounded-lg border border-rose-200 px-3 py-1 text-xs text-rose-600 transition hover:bg-rose-50 active:bg-rose-100"
onClick={() =>
settingsStore
.getState()
.removeLocalHost(item.host)
}
>
</button>
</div>
</div>
)}
</For>
)}
</For>
</div>
</div>
</div>
<div class="rounded-2xl border border-zinc-200 bg-zinc-50/80 p-4">
<p class="font-medium text-zinc-900"></p>
<div class="mt-3 flex flex-col gap-3">
<For each={mergedHosts()}>
{(item) => (
<div class="rounded-xl border border-zinc-200 bg-white px-4 py-3">
<p class="font-medium text-zinc-900">{item.label}</p>
<p class="mt-1 text-sm text-zinc-500">{item.host}</p>
<p class="mt-2 text-xs text-cyan-700">
{item.source === "local" ? "本地优先" : "远端"}
</p>
</div>
)}
</For>
<div class={sectionCardClass}>
<p class="font-medium text-zinc-900"></p>
<div class="mt-3 flex flex-col gap-3">
<For each={mergedHosts()}>
{(item) => (
<div class="rounded-xl border border-zinc-200 bg-white px-4 py-3">
<p class="font-medium text-zinc-900">{item.label}</p>
<p class="mt-1 text-sm text-zinc-500">{item.host}</p>
<p class="mt-2 text-xs text-cyan-700">
{item.source === "local" ? "本地优先" : "远端"}
</p>
</div>
)}
</For>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
</div>