Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/lang/en/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import shares from "./shares.json"
import storages from "./storages.json"
import tasks from "./tasks.json"
import users from "./users.json"
import virtual_hosts from "./virtual_hosts.json"

export const dict = {
br,
Expand All @@ -30,4 +31,5 @@ export const dict = {
storages,
tasks,
users,
virtual_hosts,
}
1 change: 1 addition & 0 deletions src/lang/en/manage.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"storages": "Storages",
"shares": "Shares",
"metas": "Metas",
"virtual_hosts": "Virtual Hosts",
"profile": "Profile",
"about": "About",
"tasks": "Tasks",
Expand Down
9 changes: 9 additions & 0 deletions src/lang/en/virtual_hosts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"domain": "Domain",
"domain_help": "The domain name to bind (e.g. blog.example.com)",
"path": "Path",
"path_help": "The OpenList path to map this domain to",
"enabled": "Enabled",
"web_hosting": "Web Hosting",
"web_hosting_help": "If enabled, HTML files will be served directly instead of the file browser"
}
8 changes: 8 additions & 0 deletions src/pages/manage/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ const hide_routes: Route[] = [
to: "/metas/edit/:id",
component: lazy(() => import("./metas/AddOrEdit")),
},
{
to: "/virtual_hosts/add",
component: lazy(() => import("./virtual_hosts/AddOrEdit")),
},
{
to: "/virtual_hosts/edit/:id",
component: lazy(() => import("./virtual_hosts/AddOrEdit")),
},
{
to: "/2fa",
component: lazy(() => import("./users/2fa")),
Expand Down
7 changes: 7 additions & 0 deletions src/pages/manage/sidemenu_items.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
BsBucket,
BsHddNetwork,
BsArrowLeftRight,
BsGlobe,
} from "solid-icons/bs"
import { FiLogIn } from "solid-icons/fi"
import { SiMetabase } from "solid-icons/si"
Expand Down Expand Up @@ -187,6 +188,12 @@ export const side_menu_items: SideMenuItem[] = [
to: "/@manage/metas",
component: lazy(() => import("./metas/Metas")),
},
{
title: "manage.sidemenu.virtual_hosts",
icon: BsGlobe,
to: "/@manage/virtual_hosts",
component: lazy(() => import("./virtual_hosts/VirtualHosts")),
},
{
title: "manage.sidemenu.indexes",
icon: BsSearch,
Expand Down
127 changes: 127 additions & 0 deletions src/pages/manage/virtual_hosts/AddOrEdit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {
Button,
Switch as HopeSwitch,
FormControl,
FormHelperText,
FormLabel,
Heading,
Input,
VStack,
} from "@hope-ui/solid"
import { MaybeLoading, FolderChooseInput } from "~/components"
import { useFetch, useRouter, useT } from "~/hooks"
import { handleResp, notify, r } from "~/utils"
import { VirtualHost, PEmptyResp, PResp } from "~/types"
import { createStore } from "solid-js/store"

const AddOrEdit = () => {
const t = useT()
const { params, back } = useRouter()
const { id } = params
const [vhost, setVhost] = createStore<VirtualHost>({
id: 0,
enabled: true,
domain: "",
path: "",
web_hosting: false,
})
const [vhostLoading, loadVhost] = useFetch(
(): PResp<VirtualHost> => r.get(`/admin/vhost/get?id=${id}`),
)

const initEdit = async () => {
const resp = await loadVhost()
handleResp<VirtualHost>(resp, setVhost)
}
if (id) {
initEdit()
}
const [okLoading, ok] = useFetch((): PEmptyResp => {
return r.post(`/admin/vhost/${id ? "update" : "create"}`, vhost)
})
return (
<MaybeLoading loading={vhostLoading()}>
<VStack w="$full" alignItems="start" spacing="$4">
<Heading>{t(`global.${id ? "edit" : "add"}`)}</Heading>

{/* 启用开关 */}
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These section-label comments are in Chinese (启用开关, 域名, 路径, Web 托管开关), while the rest of the codebase uses English for all comments. For consistency, these should be in English (e.g., Enabled switch, Domain, Path, Web Hosting switch), or simply removed since the labels and FormLabel text already make the sections self-documenting.

Copilot uses AI. Check for mistakes.
<FormControl
w="$full"
display="flex"
flexDirection="row"
alignItems="center"
gap="$3"
>
<FormLabel for="enabled" mb="0">
{t("virtual_hosts.enabled")}
</FormLabel>
<HopeSwitch
id="enabled"
checked={vhost.enabled}
onChange={(e: any) => setVhost("enabled", e.currentTarget.checked)}
/>
</FormControl>

{/* 域名 */}
<FormControl w="$full" display="flex" flexDirection="column" required>
<FormLabel for="domain">{t("virtual_hosts.domain")}</FormLabel>
<Input
id="domain"
placeholder="example.com"
value={vhost.domain}
onInput={(e) => setVhost("domain", e.currentTarget.value)}
/>
<FormHelperText>{t("virtual_hosts.domain_help")}</FormHelperText>
</FormControl>

{/* 路径 */}
<FormControl w="$full" display="flex" flexDirection="column" required>
<FormLabel for="path">{t("virtual_hosts.path")}</FormLabel>
<FolderChooseInput
id="path"
value={vhost.path}
onChange={(path) => setVhost("path", path)}
/>
<FormHelperText>{t("virtual_hosts.path_help")}</FormHelperText>
</FormControl>

{/* Web 托管开关 */}
<FormControl w="$full" display="flex" flexDirection="column">
<FormControl
display="flex"
flexDirection="row"
alignItems="center"
gap="$3"
>
<FormLabel for="web_hosting" mb="0">
{t("virtual_hosts.web_hosting")}
</FormLabel>
<HopeSwitch
id="web_hosting"
checked={vhost.web_hosting}
onChange={(e: any) =>
setVhost("web_hosting", e.currentTarget.checked)
}
/>
</FormControl>
<FormHelperText>{t("virtual_hosts.web_hosting_help")}</FormHelperText>
</FormControl>

<Button
loading={okLoading()}
onClick={async () => {
const resp = await ok()
handleResp(resp, () => {
notify.success(t("global.save_success"))
back()
})
}}
>
{t(`global.${id ? "save" : "add"}`)}
</Button>
</VStack>
</MaybeLoading>
)
}

export default AddOrEdit
115 changes: 115 additions & 0 deletions src/pages/manage/virtual_hosts/VirtualHosts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {
Box,
Button,
HStack,
Table,
Tbody,
Td,
Th,
Thead,
Tr,
VStack,
} from "@hope-ui/solid"
import { createSignal, For } from "solid-js"
import {
useFetch,
useListFetch,
useManageTitle,
useRouter,
useT,
} from "~/hooks"
import { handleResp, notify, r } from "~/utils"
import { VirtualHost, PEmptyResp, PPageResp } from "~/types"
import { DeletePopover } from "../common/DeletePopover"
import { Wether } from "~/components"

const VirtualHosts = () => {
const t = useT()
useManageTitle("manage.sidemenu.virtual_hosts")
const { to } = useRouter()
const [getVhostsLoading, getVhosts] = useFetch(
(): PPageResp<VirtualHost> => r.get("/admin/vhost/list"),
)
const [vhosts, setVhosts] = createSignal<VirtualHost[]>([])
const refresh = async () => {
const resp = await getVhosts()
handleResp(resp, (data) => setVhosts(data.content))
}
refresh()

const [deleting, deleteVhost] = useListFetch(
(id: number): PEmptyResp => r.post(`/admin/vhost/delete?id=${id}`),
)
return (
<VStack spacing="$2" alignItems="start" w="$full">
<HStack spacing="$2">
<Button
colorScheme="accent"
loading={getVhostsLoading()}
onClick={refresh}
>
{t("global.refresh")}
</Button>
<Button
onClick={() => {
to("/@manage/virtual_hosts/add")
}}
>
{t("global.add")}
</Button>
</HStack>
<Box w="$full" overflowX="auto">
<Table highlightOnHover dense>
<Thead>
<Tr>
<For each={["domain", "path", "enabled", "web_hosting"]}>
{(title) => <Th>{t(`virtual_hosts.${title}`)}</Th>}
</For>
<Th>{t("global.operations")}</Th>
</Tr>
</Thead>
<Tbody>
<For each={vhosts()}>
{(vhost) => (
<Tr>
<Td>{vhost.domain}</Td>
<Td>{vhost.path}</Td>
<Td>
<Wether yes={vhost.enabled} />
</Td>
<Td>
<Wether yes={vhost.web_hosting} />
</Td>
<Td>
<HStack spacing="$2">
<Button
onClick={() => {
to(`/@manage/virtual_hosts/edit/${vhost.id}`)
}}
>
{t("global.edit")}
</Button>
<DeletePopover
name={vhost.domain}
loading={deleting() === vhost.id}
onClick={async () => {
const resp = await deleteVhost(vhost.id)
handleResp(resp, () => {
notify.success(t("global.delete_success"))
refresh()
})
}}
/>
</HStack>
</Td>
</Tr>
)}
</For>
</Tbody>
</Table>
</Box>
</VStack>
)
}

export default VirtualHosts
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from "./item_type"
export * from "./meta"
export * from "./task"
export * from "./share"
export * from "./virtual_host"
7 changes: 7 additions & 0 deletions src/types/virtual_host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface VirtualHost {
id: number
enabled: boolean
domain: string
path: string
web_hosting: boolean
}