2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-05 13:37:21 +08:00

feat(website): 添加"全部"分类、动态模态框标题和批量创建类型支持 (#1329)

* Initial plan

* feat(website): 添加"全部"分类、动态模态框标题和批量创建类型支持

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* refactor: 修复代码审查问题,提取正则常量和接口定义

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>
This commit is contained in:
Copilot
2026-02-04 20:04:19 +08:00
committed by GitHub
parent f7b63e2f6e
commit 181b262a61
4 changed files with 163 additions and 29 deletions

1
web/.gitignore vendored
View File

@@ -13,6 +13,7 @@ dist
dist-ssr
coverage
*.local
package-lock.json
# Editor directories and files

View File

@@ -5,11 +5,73 @@ import { useGettext } from 'vue3-gettext'
import website from '@/api/panel/website'
const show = defineModel<boolean>('show', { type: Boolean, required: true })
const type = defineModel<string>('type', { type: String, required: true })
const { $gettext } = useGettext()
const bulkCreate = ref('')
// 内部选择的类型(当外部 type 为 'all' 时使用)
const selectedType = ref('proxy')
// 实际使用的网站类型
const effectiveType = computed(() => {
if (type.value === 'all') {
return selectedType.value
}
return type.value
})
// 批量创建网站请求模型
interface BulkCreateModel {
type: string
name: string
listens: Array<string>
domains: Array<string>
path: string
proxy: string
remark: string
}
// 类型选项
const typeOptions = computed(() => [
{ label: $gettext('Reverse Proxy'), value: 'proxy' },
{ label: $gettext('PHP'), value: 'php' },
{ label: $gettext('Pure Static'), value: 'static' }
])
// 获取模态框标题
const modalTitle = computed(() => {
switch (effectiveType.value) {
case 'proxy':
return $gettext('Bulk Create Reverse Proxy Website')
case 'php':
return $gettext('Bulk Create PHP Website')
case 'static':
return $gettext('Bulk Create Pure Static Website')
default:
return $gettext('Bulk Create Website')
}
})
// 获取占位符文本(根据类型不同显示不同格式)
const placeholderText = computed(() => {
if (effectiveType.value === 'proxy') {
return $gettext('name|domain|port|proxy_target|remark')
}
return $gettext('name|domain|port|path|remark')
})
// 获取第四列的说明文本
const fourthColumnHelp = computed(() => {
if (effectiveType.value === 'proxy') {
return $gettext(
'Proxy Target: The target address for reverse proxy (e.g., http://127.0.0.1:3000).'
)
}
return $gettext('Path: The path of the website, can be empty to use the default path.')
})
const handleCreate = async () => {
// 按行分割
const lines = bulkCreate.value.split('\n')
@@ -32,20 +94,20 @@ const handleCreate = async () => {
.trim()
.split(',')
.map((item) => item.trim())
const path = (parts[3] ?? '').trim()
const fourthColumn = (parts[3] ?? '').trim()
const remark = parts[4] ? parts[4].trim() : ''
let model = {
name: '',
listens: [] as Array<string>,
domains: [] as Array<string>,
path: '',
remark: ''
// 构建请求模型
const model: BulkCreateModel = {
type: effectiveType.value,
name: name,
listens: listens,
domains: domains,
path: effectiveType.value === 'proxy' ? '' : fourthColumn,
proxy: effectiveType.value === 'proxy' ? fourthColumn : '',
remark: remark
}
model.name = name
model.domains = domains
model.listens = listens
model.path = path
model.remark = remark
// 去除空的域名和端口
model.domains = model.domains.filter((item) => item !== '')
model.listens = model.listens.filter((item) => item !== '')
@@ -59,13 +121,6 @@ const handleCreate = async () => {
window.$message.success(
$gettext('Website %{ name } created successfully', { name: model.name })
)
model = {
name: '',
domains: [] as Array<string>,
listens: [] as Array<string>,
path: '',
remark: ''
}
window.$bus.emit('website:refresh')
})
}
@@ -75,7 +130,7 @@ const handleCreate = async () => {
<template>
<n-modal
v-model:show="show"
:title="$gettext('Bulk Create Website')"
:title="modalTitle"
preset="card"
style="width: 60vw"
size="huge"
@@ -84,6 +139,13 @@ const handleCreate = async () => {
@close="show = false"
>
<n-flex vertical>
<n-form-item v-if="type === 'all'" :label="$gettext('Website Type')">
<n-select
v-model:value="selectedType"
:options="typeOptions"
:placeholder="$gettext('Select Website Type')"
/>
</n-form-item>
<n-alert type="info">
{{
$gettext(
@@ -94,7 +156,7 @@ const handleCreate = async () => {
<n-input
type="textarea"
:autosize="{ minRows: 10, maxRows: 15 }"
:placeholder="$gettext('name|domain|port|path|remark')"
:placeholder="placeholderText"
v-model:value="bulkCreate"
/>
<n-text>
@@ -119,7 +181,7 @@ const handleCreate = async () => {
}}
</n-text>
<n-text>
{{ $gettext('Path: The path of the website, can be empty to use the default path.') }}
{{ fourthColumnHelp }}
</n-text>
<n-text>
{{ $gettext('Remark: The remark of the website, can be empty.') }}

View File

@@ -12,6 +12,23 @@ const type = defineModel<string>('type', { type: String, required: true })
const { $gettext } = useGettext()
// 内部选择的类型(当外部 type 为 'all' 时使用)
const selectedType = ref('proxy')
// 实际使用的网站类型
const effectiveType = computed(() => {
if (type.value === 'all') {
return selectedType.value
}
return type.value
})
// 类型选项
const typeOptions = computed(() => [
{ label: $gettext('Reverse Proxy'), value: 'proxy' },
{ label: $gettext('PHP'), value: 'php' },
{ label: $gettext('Pure Static'), value: 'static' }
])
const createModel = ref({
type: '',
name: '',
@@ -49,8 +66,42 @@ const { data: installedEnvironment } = useRequest(home.installedEnvironment, {
}
})
// 获取模态框标题
const modalTitle = computed(() => {
switch (effectiveType.value) {
case 'proxy':
return $gettext('Create Reverse Proxy Website')
case 'php':
return $gettext('Create PHP Website')
case 'static':
return $gettext('Create Pure Static Website')
default:
return $gettext('Create Website')
}
})
// 域名分隔符正则表达式(支持逗号、空格、换行分隔)
const DOMAIN_SEPARATORS_REGEX = /[\s,\n\r]+/
// 处理域名粘贴,支持批量添加
const handleDomainCreate = (index: number, value: string) => {
if (DOMAIN_SEPARATORS_REGEX.test(value)) {
// 解析多个域名并去除空白
const domains = value.split(DOMAIN_SEPARATORS_REGEX).map((d) => d.trim()).filter((d) => d !== '')
if (domains.length > 1) {
// 移除当前空输入框
createModel.value.domains.splice(index, 1)
// 过滤掉已存在的域名,避免重复
const existingDomains = new Set(createModel.value.domains.map((d) => d.trim()))
const newDomains = domains.filter((d) => !existingDomains.has(d))
// 将新域名添加到列表
createModel.value.domains.push(...newDomains)
}
}
}
const handleCreate = async () => {
createModel.value.type = type.value
createModel.value.type = effectiveType.value
// 去除空的域名和端口
createModel.value.domains = createModel.value.domains.filter((item) => item !== '')
createModel.value.listens = createModel.value.listens.filter((item) => item !== '')
@@ -111,7 +162,7 @@ watch(showPathSelector, (val) => {
<template>
<n-modal
v-model:show="show"
:title="$gettext('Create Website')"
:title="modalTitle"
preset="card"
style="width: 60vw"
size="huge"
@@ -120,6 +171,13 @@ watch(showPathSelector, (val) => {
@close="show = false"
>
<n-form :model="createModel">
<n-form-item v-if="type === 'all'" path="type" :label="$gettext('Website Type')">
<n-select
v-model:value="selectedType"
:options="typeOptions"
:placeholder="$gettext('Select Website Type')"
/>
</n-form-item>
<n-form-item path="name" :label="$gettext('Name')">
<n-input
v-model:value="createModel.name"
@@ -138,6 +196,18 @@ watch(showPathSelector, (val) => {
placeholder="example.com"
:min="1"
show-sort-button
@update:value="
(value: string[]) => {
// 检查最后一个元素是否包含多个域名
if (value.length > 0) {
const lastIndex = value.length - 1
const lastValue = value[lastIndex]
if (lastValue && DOMAIN_SEPARATORS_REGEX.test(lastValue)) {
handleDomainCreate(lastIndex, lastValue)
}
}
}
"
/>
</n-form-item>
</n-col>
@@ -153,7 +223,7 @@ watch(showPathSelector, (val) => {
</n-form-item>
</n-col>
</n-row>
<n-row v-if="type == 'php'" :gutter="[0, 24]">
<n-row v-if="effectiveType == 'php'" :gutter="[0, 24]">
<n-col :span="11">
<n-form-item path="php" :label="$gettext('PHP Version')">
<n-select
@@ -186,7 +256,7 @@ watch(showPathSelector, (val) => {
</n-form-item>
</n-col>
</n-row>
<n-row v-if="type == 'php'" :gutter="[0, 24]">
<n-row v-if="effectiveType == 'php'" :gutter="[0, 24]">
<n-col :span="7">
<n-form-item v-if="createModel.db" path="db_name" :label="$gettext('Database Name')">
<n-input
@@ -224,7 +294,7 @@ watch(showPathSelector, (val) => {
</n-form-item>
</n-col>
</n-row>
<n-form-item v-if="type != 'proxy'" path="path" :label="$gettext('Directory')">
<n-form-item v-if="effectiveType != 'proxy'" path="path" :label="$gettext('Directory')">
<n-input-group>
<n-input
v-model:value="createModel.path"
@@ -243,7 +313,7 @@ watch(showPathSelector, (val) => {
</n-button>
</n-input-group>
</n-form-item>
<n-form-item v-if="type == 'proxy'" path="path" :label="$gettext('Proxy Target')">
<n-form-item v-if="effectiveType == 'proxy'" path="path" :label="$gettext('Proxy Target')">
<n-input
v-model:value="createModel.proxy"
type="text"

View File

@@ -8,7 +8,7 @@ import CreateModal from '@/views/website/CreateModal.vue'
import ListView from '@/views/website/ListView.vue'
import SettingView from '@/views/website/SettingView.vue'
const currentTab = ref('proxy')
const currentTab = ref('all')
const createModal = ref(false)
const bulkCreateModal = ref(false)
@@ -18,6 +18,7 @@ const bulkCreateModal = ref(false)
<common-page show-header show-footer>
<template #tabbar>
<n-tabs v-model:value="currentTab" animated>
<n-tab name="all" :tab="$gettext('All')" />
<n-tab name="proxy" :tab="$gettext('Reverse Proxy')" />
<n-tab name="php" :tab="$gettext('PHP')" />
<n-tab name="static" :tab="$gettext('Pure Static')" />