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:
1
web/.gitignore
vendored
1
web/.gitignore
vendored
@@ -13,6 +13,7 @@ dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
package-lock.json
|
||||
|
||||
|
||||
# Editor directories and files
|
||||
|
||||
@@ -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.') }}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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')" />
|
||||
|
||||
Reference in New Issue
Block a user