mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 19:37:18 +08:00
feat: 项目管理完成
This commit is contained in:
@@ -37,7 +37,7 @@ func (r *projectRepo) List(typ types.ProjectType, page, limit uint) ([]*types.Pr
|
||||
var total int64
|
||||
|
||||
query := r.db.Model(&biz.Project{})
|
||||
if typ != "" {
|
||||
if typ != "" && typ != "all" {
|
||||
query = query.Where("type = ?", typ)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@ import file from '@/api/panel/file'
|
||||
import TheIcon from '@/components/custom/TheIcon.vue'
|
||||
import { checkName, checkPath, getExt, getIconByExt } from '@/utils'
|
||||
import type { DataTableColumns, InputInst } from 'naive-ui'
|
||||
import { NButton, NDataTable, NEllipsis, NFlex, NTag } from 'naive-ui'
|
||||
import { NButton, NDataTable, NEllipsis, NFlex, NSpin, NTag, useThemeVars } from 'naive-ui'
|
||||
import type { RowData } from 'naive-ui/es/data-table/src/interface'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const themeVars = useThemeVars()
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const path = defineModel<string>('path', { type: String, required: true })
|
||||
const props = defineProps({
|
||||
@@ -17,12 +18,18 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const currentPath = ref('/')
|
||||
|
||||
// 目录大小计算状态
|
||||
const sizeLoading = ref<Map<string, boolean>>(new Map())
|
||||
const sizeCache = ref<Map<string, string>>(new Map())
|
||||
|
||||
const title = computed(() => (props.dir ? $gettext('Select Directory') : $gettext('Select File')))
|
||||
const isInput = ref(false)
|
||||
const pathInput = ref<InputInst | null>(null)
|
||||
const input = ref('www')
|
||||
const sort = ref<string>('')
|
||||
const selected = defineModel<any[]>('selected', { type: Array, default: () => [] })
|
||||
const selected = ref<any[]>([])
|
||||
const create = ref(false)
|
||||
const createModel = ref({
|
||||
dir: false,
|
||||
@@ -58,9 +65,7 @@ const columns: DataTableColumns<RowData> = [
|
||||
class: 'cursor-pointer hover:opacity-60',
|
||||
onClick: () => {
|
||||
if (row.dir) {
|
||||
path.value = row.full
|
||||
} else {
|
||||
selected.value = [row.full]
|
||||
currentPath.value = row.full
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -106,9 +111,41 @@ const columns: DataTableColumns<RowData> = [
|
||||
{
|
||||
title: $gettext('Size'),
|
||||
key: 'size',
|
||||
minWidth: 80,
|
||||
minWidth: 100,
|
||||
render(row: any): any {
|
||||
return h(NTag, { type: 'info', size: 'small', bordered: false }, { default: () => row.size })
|
||||
// 文件
|
||||
if (!row.dir) {
|
||||
return h(
|
||||
NTag,
|
||||
{ type: 'info', size: 'small', bordered: false },
|
||||
{ default: () => row.size }
|
||||
)
|
||||
}
|
||||
// 目录
|
||||
const cachedSize = sizeCache.value.get(row.full)
|
||||
if (cachedSize) {
|
||||
return h(
|
||||
NTag,
|
||||
{ type: 'info', size: 'small', bordered: false },
|
||||
{ default: () => cachedSize }
|
||||
)
|
||||
}
|
||||
const isLoading = sizeLoading.value.get(row.full)
|
||||
if (isLoading) {
|
||||
return h(NSpin, { size: 16, style: { paddingTop: '4px' } })
|
||||
}
|
||||
return h(
|
||||
'span',
|
||||
{
|
||||
style: { cursor: 'pointer', fontSize: '14px', color: themeVars.value.primaryColor },
|
||||
onClick: (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
calculateDirSize(row.full)
|
||||
}
|
||||
},
|
||||
$gettext('Calculate')
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -125,9 +162,9 @@ const columns: DataTableColumns<RowData> = [
|
||||
}
|
||||
]
|
||||
|
||||
const { loading, data, page, total, pageSize, pageCount, refresh } = usePagination(
|
||||
const { loading, data, page, total, pageSize, pageCount, reload } = usePagination(
|
||||
(page, pageSize) =>
|
||||
file.list(encodeURIComponent(path.value), '', false, sort.value, page, pageSize),
|
||||
file.list(encodeURIComponent(currentPath.value), '', false, sort.value, page, pageSize),
|
||||
{
|
||||
initialData: { total: 0, list: [] },
|
||||
initialPageSize: 100,
|
||||
@@ -151,11 +188,11 @@ const handleBlur = () => {
|
||||
}
|
||||
|
||||
isInput.value = false
|
||||
path.value = '/' + input.value
|
||||
currentPath.value = '/' + input.value
|
||||
}
|
||||
|
||||
const handleUp = () => {
|
||||
const count = splitPath(path.value, '/').length
|
||||
const count = splitPath(currentPath.value, '/').length
|
||||
setPath(count - 2)
|
||||
}
|
||||
|
||||
@@ -167,10 +204,10 @@ const splitPath = (str: string, delimiter: string) => {
|
||||
}
|
||||
|
||||
const setPath = (index: number) => {
|
||||
const newPath = splitPath(path.value, '/')
|
||||
const newPath = splitPath(currentPath.value, '/')
|
||||
.slice(0, index + 1)
|
||||
.join('/')
|
||||
path.value = '/' + newPath
|
||||
currentPath.value = '/' + newPath
|
||||
input.value = newPath
|
||||
}
|
||||
|
||||
@@ -183,15 +220,15 @@ const handleSorterChange = (sorter: {
|
||||
switch (sorter.order) {
|
||||
case 'ascend':
|
||||
sort.value = 'asc'
|
||||
refresh()
|
||||
reload()
|
||||
break
|
||||
case 'descend':
|
||||
sort.value = 'desc'
|
||||
refresh()
|
||||
reload()
|
||||
break
|
||||
default:
|
||||
sort.value = ''
|
||||
refresh()
|
||||
reload()
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -210,32 +247,51 @@ const handleCreate = () => {
|
||||
return
|
||||
}
|
||||
|
||||
const fullPath = path.value + '/' + createModel.value.path
|
||||
const fullPath = currentPath.value + '/' + createModel.value.path
|
||||
useRequest(file.create(fullPath, createModel.value.dir)).onSuccess(() => {
|
||||
create.value = false
|
||||
refresh()
|
||||
reload()
|
||||
window.$message.success($gettext('Created successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const closeWatch = watch(
|
||||
path,
|
||||
(value) => {
|
||||
input.value = value.slice(1)
|
||||
selected.value = []
|
||||
refresh()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
// 计算目录大小
|
||||
const calculateDirSize = (dirPath: string) => {
|
||||
sizeLoading.value.set(dirPath, true)
|
||||
useRequest(file.size(dirPath))
|
||||
.onSuccess(({ data }) => {
|
||||
sizeCache.value.set(dirPath, data)
|
||||
})
|
||||
.onComplete(() => {
|
||||
sizeLoading.value.set(dirPath, false)
|
||||
})
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
closeWatch()
|
||||
if (selected.value.length) {
|
||||
// 打开选择器时,用外部path初始化内部currentPath
|
||||
watch(show, (val) => {
|
||||
if (val) {
|
||||
currentPath.value = path.value || '/'
|
||||
}
|
||||
})
|
||||
|
||||
// 监听内部路径变化,刷新列表
|
||||
watch(currentPath, (value) => {
|
||||
if (!value) return
|
||||
input.value = value.slice(1)
|
||||
selected.value = []
|
||||
sizeCache.value.clear()
|
||||
sizeLoading.value.clear()
|
||||
reload()
|
||||
})
|
||||
|
||||
// 选择后更新外部path并关闭
|
||||
watch(selected, (val) => {
|
||||
if (val.length > 0) {
|
||||
path.value = selected.value[0]
|
||||
selected.value = []
|
||||
show.value = false
|
||||
}
|
||||
show.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -247,8 +303,8 @@ const handleClose = () => {
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="handleClose"
|
||||
@mask-click="handleClose"
|
||||
@close="show = false"
|
||||
@mask-click="show = false"
|
||||
>
|
||||
<n-flex>
|
||||
<n-popselect
|
||||
@@ -270,7 +326,7 @@ const handleClose = () => {
|
||||
{{ $gettext('Root Directory') }}
|
||||
</n-breadcrumb-item>
|
||||
<n-breadcrumb-item
|
||||
v-for="(item, index) in splitPath(path, '/')"
|
||||
v-for="(item, index) in splitPath(currentPath, '/')"
|
||||
:key="index"
|
||||
@click.stop="setPath(index)"
|
||||
>
|
||||
@@ -287,7 +343,7 @@ const handleClose = () => {
|
||||
@blur="handleBlur"
|
||||
/>
|
||||
</n-input-group>
|
||||
<n-button @click="refresh">
|
||||
<n-button @click="reload">
|
||||
<i-mdi-refresh :size="16" />
|
||||
</n-button>
|
||||
</n-flex>
|
||||
|
||||
@@ -1,8 +1,223 @@
|
||||
<script setup lang="ts">
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import home from '@/api/panel/home'
|
||||
import project from '@/api/panel/project'
|
||||
import PathSelector from '@/components/common/PathSelector.vue'
|
||||
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
const type = defineModel<string>('type', { type: String, required: true }) // 项目类型
|
||||
const type = defineModel<string>('type', { type: String, required: true })
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
// PHP 框架预设
|
||||
const phpFrameworks = [
|
||||
{ label: $gettext('Custom'), value: 'custom', command: '' },
|
||||
{ label: 'Laravel Octane', value: 'laravel-octane', command: 'artisan octane:start' },
|
||||
{ label: 'Laravel (Artisan Serve)', value: 'laravel-serve', command: 'artisan serve' },
|
||||
{ label: 'ThinkPHP', value: 'thinkphp', command: 'think run' },
|
||||
{ label: 'Webman', value: 'webman', command: 'start.php start' },
|
||||
{ label: 'Hyperf', value: 'hyperf', command: 'bin/hyperf.php start' },
|
||||
{ label: 'Swoole HTTP', value: 'swoole', command: 'server.php' },
|
||||
{ label: 'RoadRunner', value: 'roadrunner', command: 'vendor/bin/rr serve' }
|
||||
]
|
||||
|
||||
const createModel = ref({
|
||||
name: '',
|
||||
type: '',
|
||||
root_dir: '/opt/ace',
|
||||
working_dir: '',
|
||||
exec_start: '',
|
||||
user: 'www'
|
||||
})
|
||||
|
||||
// PHP 特有字段
|
||||
const phpOptions = ref({
|
||||
version: null as number | null,
|
||||
framework: 'custom'
|
||||
})
|
||||
|
||||
const showPathSelector = ref(false)
|
||||
const pathSelectorPath = ref('/opt/ace')
|
||||
|
||||
const { data: installedEnvironment } = useRequest(home.installedEnvironment, {
|
||||
initialData: {
|
||||
php: []
|
||||
}
|
||||
})
|
||||
|
||||
// PHP 版本选项
|
||||
const phpVersionOptions = computed(() => {
|
||||
return installedEnvironment.value?.php || []
|
||||
})
|
||||
|
||||
// 根据 PHP 版本和框架生成启动命令
|
||||
const generateCommand = () => {
|
||||
if (type.value !== 'php' || !phpOptions.value.version) {
|
||||
return
|
||||
}
|
||||
|
||||
const framework = phpFrameworks.find((f) => f.value === phpOptions.value.framework)
|
||||
if (!framework || framework.value === 'custom') {
|
||||
return
|
||||
}
|
||||
|
||||
const phpBin = `php${phpOptions.value.version}`
|
||||
createModel.value.exec_start = `${phpBin} ${framework.command}`
|
||||
}
|
||||
|
||||
// 监听 PHP 版本和框架变化
|
||||
watch(
|
||||
() => [phpOptions.value.version, phpOptions.value.framework],
|
||||
() => {
|
||||
generateCommand()
|
||||
}
|
||||
)
|
||||
|
||||
// 处理目录选择
|
||||
const handleSelectPath = () => {
|
||||
pathSelectorPath.value = createModel.value.root_dir || '/opt/ace'
|
||||
showPathSelector.value = true
|
||||
}
|
||||
|
||||
// 目录选择完成
|
||||
watch(showPathSelector, (val) => {
|
||||
if (!val && pathSelectorPath.value) {
|
||||
createModel.value.root_dir = pathSelectorPath.value
|
||||
}
|
||||
})
|
||||
|
||||
const handleCreate = async () => {
|
||||
createModel.value.type = type.value == 'all' ? 'general' : type.value
|
||||
|
||||
useRequest(project.create(createModel.value)).onSuccess(() => {
|
||||
window.$bus.emit('project:refresh')
|
||||
window.$message.success($gettext('Project created successfully'))
|
||||
show.value = false
|
||||
// 重置表单
|
||||
createModel.value = {
|
||||
name: '',
|
||||
type: '',
|
||||
root_dir: '/opt/ace',
|
||||
working_dir: '',
|
||||
exec_start: '',
|
||||
user: 'www'
|
||||
}
|
||||
phpOptions.value = {
|
||||
version: null,
|
||||
framework: 'custom'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 根据类型获取标题
|
||||
const modalTitle = computed(() => {
|
||||
const titles: Record<string, string> = {
|
||||
general: $gettext('Create General Project'),
|
||||
php: $gettext('Create PHP Project')
|
||||
}
|
||||
return titles[type.value] || $gettext('Create Project')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template></template>
|
||||
<template>
|
||||
<n-modal
|
||||
v-model:show="show"
|
||||
:title="modalTitle"
|
||||
preset="card"
|
||||
style="width: 60vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
@close="show = false"
|
||||
>
|
||||
<n-form :model="createModel" label-placement="left" label-width="100">
|
||||
<n-form-item path="name" :label="$gettext('Project Name')">
|
||||
<n-input
|
||||
v-model:value="createModel.name"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
:placeholder="$gettext('Project name, used as service identifier')"
|
||||
/>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="root_dir" :label="$gettext('Project Directory')" required>
|
||||
<n-input-group>
|
||||
<n-input
|
||||
v-model:value="createModel.root_dir"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
:placeholder="$gettext('Project root directory')"
|
||||
/>
|
||||
<n-button @click="handleSelectPath">
|
||||
<template #icon>
|
||||
<i-mdi-folder-open />
|
||||
</template>
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
|
||||
<!-- PHP 类型特有字段 -->
|
||||
<template v-if="type === 'php'">
|
||||
<n-row :gutter="[24, 0]">
|
||||
<n-col :span="12">
|
||||
<n-form-item :label="$gettext('PHP Version')">
|
||||
<n-select
|
||||
v-model:value="phpOptions.version"
|
||||
:options="phpVersionOptions"
|
||||
:placeholder="$gettext('Select PHP Version')"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-col>
|
||||
<n-col :span="12">
|
||||
<n-form-item :label="$gettext('Framework')">
|
||||
<n-select
|
||||
v-model:value="phpOptions.framework"
|
||||
:options="phpFrameworks"
|
||||
:placeholder="$gettext('Select Framework')"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-col>
|
||||
</n-row>
|
||||
</template>
|
||||
|
||||
<n-form-item path="user" :label="$gettext('Run User')">
|
||||
<n-select
|
||||
v-model:value="createModel.user"
|
||||
:options="[
|
||||
{ label: 'www', value: 'www' },
|
||||
{ label: 'root', value: 'root' },
|
||||
{ label: 'nobody', value: 'nobody' }
|
||||
]"
|
||||
:placeholder="$gettext('Select User')"
|
||||
@keydown.enter.prevent
|
||||
/>
|
||||
<template #feedback>
|
||||
<span class="text-gray-400">
|
||||
{{ $gettext('Select www user if no special requirements') }}
|
||||
</span>
|
||||
</template>
|
||||
</n-form-item>
|
||||
|
||||
<n-form-item path="exec_start" :label="$gettext('Start Command')" required>
|
||||
<n-input
|
||||
v-model:value="createModel.exec_start"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
:placeholder="$gettext('e.g., php artisan serve, node app.js')"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
|
||||
<n-button type="info" block @click="handleCreate">
|
||||
{{ $gettext('Create') }}
|
||||
</n-button>
|
||||
</n-modal>
|
||||
|
||||
<!-- 目录选择器 -->
|
||||
<path-selector v-model:show="showPathSelector" v-model:path="pathSelectorPath" :dir="true" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { TreeSelectOption } from 'naive-ui'
|
||||
|
||||
import PathSelector from '@/components/common/PathSelector.vue'
|
||||
import { translateTitle } from '@/locales/menu'
|
||||
import { usePermissionStore } from '@/store'
|
||||
import { locales as availableLocales } from '@/utils'
|
||||
@@ -12,6 +13,30 @@ const permissionStore = usePermissionStore()
|
||||
|
||||
const model = defineModel<any>('model', { type: Object, required: true })
|
||||
|
||||
// 目录选择器
|
||||
const showPathSelector = ref(false)
|
||||
const pathSelectorPath = ref('/opt/ace')
|
||||
const pathSelectorTarget = ref<'website' | 'backup'>('website')
|
||||
|
||||
const handleSelectPath = (target: 'website' | 'backup') => {
|
||||
pathSelectorTarget.value = target
|
||||
pathSelectorPath.value =
|
||||
target === 'website'
|
||||
? model.value.website_path || '/opt/ace/sites'
|
||||
: model.value.backup_path || '/opt/ace/backup'
|
||||
showPathSelector.value = true
|
||||
}
|
||||
|
||||
watch(showPathSelector, (val) => {
|
||||
if (!val && pathSelectorPath.value) {
|
||||
if (pathSelectorTarget.value === 'website') {
|
||||
model.value.website_path = pathSelectorPath.value
|
||||
} else {
|
||||
model.value.backup_path = pathSelectorPath.value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const locales = computed(() => {
|
||||
return Object.entries(availableLocales).map(([code, name]: [string, string]) => {
|
||||
return {
|
||||
@@ -86,10 +111,24 @@ const menus = computed<TreeSelectOption[]>(() => {
|
||||
<n-input-number v-model:value="model.port" :placeholder="$gettext('8888')" w-full />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Default Website Directory')">
|
||||
<n-input v-model:value="model.website_path" :placeholder="$gettext('/opt/ace/sites')" />
|
||||
<n-input-group>
|
||||
<n-input v-model:value="model.website_path" :placeholder="$gettext('/opt/ace/sites')" />
|
||||
<n-button @click="handleSelectPath('website')">
|
||||
<template #icon>
|
||||
<i-mdi-folder-open />
|
||||
</template>
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Default Backup Directory')">
|
||||
<n-input v-model:value="model.backup_path" :placeholder="$gettext('/opt/ace/backup')" />
|
||||
<n-input-group>
|
||||
<n-input v-model:value="model.backup_path" :placeholder="$gettext('/opt/ace/backup')" />
|
||||
<n-button @click="handleSelectPath('backup')">
|
||||
<template #icon>
|
||||
<i-mdi-folder-open />
|
||||
</template>
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Custom Logo')">
|
||||
<n-input
|
||||
@@ -109,6 +148,9 @@ const menus = computed<TreeSelectOption[]>(() => {
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-flex>
|
||||
|
||||
<!-- 目录选择器 -->
|
||||
<path-selector v-model:show="showPathSelector" v-model:path="pathSelectorPath" :dir="true" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import home from '@/api/panel/home'
|
||||
import website from '@/api/panel/website'
|
||||
import PathSelector from '@/components/common/PathSelector.vue'
|
||||
import { generateRandomString } from '@/utils'
|
||||
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
@@ -28,6 +29,9 @@ const createModel = ref({
|
||||
proxy: ''
|
||||
})
|
||||
|
||||
const showPathSelector = ref(false)
|
||||
const pathSelectorPath = ref('/opt/ace')
|
||||
|
||||
const { data: installedEnvironment } = useRequest(home.installedEnvironment, {
|
||||
initialData: {
|
||||
php: [
|
||||
@@ -89,6 +93,19 @@ const formatDbValue = (value: string) => {
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// 处理目录选择
|
||||
const handleSelectPath = () => {
|
||||
pathSelectorPath.value = createModel.value.path || '/opt/ace'
|
||||
showPathSelector.value = true
|
||||
}
|
||||
|
||||
// 目录选择完成
|
||||
watch(showPathSelector, (val) => {
|
||||
if (!val && pathSelectorPath.value && pathSelectorPath.value !== '/opt/ace') {
|
||||
createModel.value.path = pathSelectorPath.value
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -208,16 +225,23 @@ const formatDbValue = (value: string) => {
|
||||
</n-col>
|
||||
</n-row>
|
||||
<n-form-item v-if="type != 'proxy'" path="path" :label="$gettext('Directory')">
|
||||
<n-input
|
||||
v-model:value="createModel.path"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
:placeholder="
|
||||
$gettext(
|
||||
'Website root directory (if left empty, defaults to website directory/website name/public)'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<n-input-group>
|
||||
<n-input
|
||||
v-model:value="createModel.path"
|
||||
type="text"
|
||||
@keydown.enter.prevent
|
||||
:placeholder="
|
||||
$gettext(
|
||||
'Website root directory (if left empty, defaults to website directory/website name/public)'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<n-button @click="handleSelectPath">
|
||||
<template #icon>
|
||||
<i-mdi-folder-open />
|
||||
</template>
|
||||
</n-button>
|
||||
</n-input-group>
|
||||
</n-form-item>
|
||||
<n-form-item v-if="type == 'proxy'" path="path" :label="$gettext('Proxy Target')">
|
||||
<n-input
|
||||
@@ -240,6 +264,9 @@ const formatDbValue = (value: string) => {
|
||||
{{ $gettext('Create') }}
|
||||
</n-button>
|
||||
</n-modal>
|
||||
|
||||
<!-- 目录选择器 -->
|
||||
<path-selector v-model:show="showPathSelector" v-model:path="pathSelectorPath" :dir="true" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
Reference in New Issue
Block a user