mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 10:17:17 +08:00
feat: 优化容器模版部署
This commit is contained in:
@@ -9,6 +9,7 @@ type TemplateSlug struct {
|
||||
type TemplateCreate struct {
|
||||
Slug string `json:"slug" validate:"required"`
|
||||
Name string `json:"name" validate:"required|regex:^[a-zA-Z0-9_-]+$"`
|
||||
Compose string `json:"compose"`
|
||||
Envs []types.KV `json:"envs"`
|
||||
AutoFirewall bool `json:"auto_firewall"`
|
||||
}
|
||||
|
||||
@@ -65,8 +65,14 @@ func (s *TemplateService) Create(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用用户自定义的 compose 内容,如果没有则使用模版默认内容
|
||||
compose := req.Compose
|
||||
if compose == "" {
|
||||
compose = template.Compose
|
||||
}
|
||||
|
||||
// 创建编排
|
||||
dir, err := s.templateRepo.CreateCompose(req.Name, template.Compose, req.Envs, req.AutoFirewall)
|
||||
dir, err := s.templateRepo.CreateCompose(req.Name, compose, req.Envs, req.AutoFirewall)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
|
||||
@@ -9,6 +9,7 @@ export default {
|
||||
create: (data: {
|
||||
slug: string
|
||||
name: string
|
||||
compose: string
|
||||
envs: { key: string; value: string }[]
|
||||
auto_firewall: boolean
|
||||
}): any => http.Post('/template', data),
|
||||
|
||||
164
web/src/components/common/DiffEditor.vue
Normal file
164
web/src/components/common/DiffEditor.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<script setup lang="ts">
|
||||
import { useThemeStore } from '@/store'
|
||||
import { getMonaco } from '@/utils/monaco'
|
||||
import type * as Monaco from 'monaco-editor'
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
|
||||
const props = defineProps({
|
||||
original: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
lang: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'yaml'
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '60vh'
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const modified = defineModel<string>('modified', { type: String, required: true })
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const editorRef = shallowRef<Monaco.editor.IStandaloneDiffEditor>()
|
||||
const monacoRef = shallowRef<typeof Monaco>()
|
||||
const loading = ref(true)
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
const themeVars = useThemeVars()
|
||||
|
||||
async function initEditor() {
|
||||
if (!containerRef.value) return
|
||||
|
||||
const monaco = await getMonaco(themeStore.locale)
|
||||
monacoRef.value = monaco
|
||||
|
||||
const originalModel = monaco.editor.createModel(props.original, props.lang)
|
||||
const modifiedModel = monaco.editor.createModel(modified.value, props.lang)
|
||||
|
||||
editorRef.value = monaco.editor.createDiffEditor(containerRef.value, {
|
||||
theme: 'vs' + (themeStore.darkMode ? '-dark' : ''),
|
||||
automaticLayout: true,
|
||||
smoothScrolling: true,
|
||||
readOnly: props.readOnly,
|
||||
renderSideBySide: true,
|
||||
enableSplitViewResizing: true,
|
||||
originalEditable: false
|
||||
})
|
||||
|
||||
editorRef.value.setModel({
|
||||
original: originalModel,
|
||||
modified: modifiedModel
|
||||
})
|
||||
|
||||
// 监听修改后的内容变化
|
||||
modifiedModel.onDidChangeContent(() => {
|
||||
const newValue = modifiedModel.getValue()
|
||||
if (newValue !== modified.value) {
|
||||
modified.value = newValue
|
||||
}
|
||||
})
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.original,
|
||||
(newOriginal) => {
|
||||
if (editorRef.value && monacoRef.value) {
|
||||
const model = editorRef.value.getModel()
|
||||
if (model?.original) {
|
||||
model.original.setValue(newOriginal)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(modified, (newModified) => {
|
||||
if (editorRef.value) {
|
||||
const model = editorRef.value.getModel()
|
||||
if (model?.modified && model.modified.getValue() !== newModified) {
|
||||
model.modified.setValue(newModified)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.lang,
|
||||
(newLang) => {
|
||||
if (editorRef.value && monacoRef.value) {
|
||||
const model = editorRef.value.getModel()
|
||||
if (model?.original) {
|
||||
monacoRef.value.editor.setModelLanguage(model.original, newLang)
|
||||
}
|
||||
if (model?.modified) {
|
||||
monacoRef.value.editor.setModelLanguage(model.modified, newLang)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.readOnly,
|
||||
(newReadOnly) => {
|
||||
if (editorRef.value) {
|
||||
editorRef.value.updateOptions({ readOnly: newReadOnly })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
initEditor()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
const model = editorRef.value?.getModel()
|
||||
model?.original?.dispose()
|
||||
model?.modified?.dispose()
|
||||
editorRef.value?.dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="diff-editor" :style="{ height: props.height, borderColor: themeVars.borderColor }">
|
||||
<div v-if="loading" class="editor-loading">
|
||||
<n-spin size="medium" />
|
||||
</div>
|
||||
<div ref="containerRef" class="editor-container" :style="{ height: props.height }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.diff-editor {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
border: 1px solid;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.editor-loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import containerApi from '@/api/panel/container'
|
||||
import templateApi from '@/api/panel/template'
|
||||
import DiffEditor from '@/components/common/DiffEditor.vue'
|
||||
import PtyTerminalModal from '@/components/common/PtyTerminalModal.vue'
|
||||
import type { FormInst, FormItemRule, FormRules } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
@@ -19,9 +21,20 @@ const emit = defineEmits<{
|
||||
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
||||
|
||||
const doSubmit = ref(false)
|
||||
const currentTab = ref('basic')
|
||||
const currentStep = ref(1)
|
||||
const formRef = ref<FormInst | null>(null)
|
||||
|
||||
// 部署模式: create 或 update
|
||||
const deployMode = ref<'create' | 'update'>('create')
|
||||
|
||||
// 编排列表
|
||||
const composeList = ref<{ name: string; path: string }[]>([])
|
||||
const composeListLoading = ref(false)
|
||||
const selectedCompose = ref<string | null>(null)
|
||||
const selectedComposeData = ref<{ compose: string; envs: { key: string; value: string }[] } | null>(
|
||||
null
|
||||
)
|
||||
|
||||
// 启动终端
|
||||
const upModal = ref(false)
|
||||
const upCommand = ref('')
|
||||
@@ -33,6 +46,10 @@ const deployModel = reactive({
|
||||
envs: {} as Record<string, any>
|
||||
})
|
||||
|
||||
// 最终编排内容
|
||||
const finalCompose = ref('')
|
||||
const finalEnvs = ref<{ key: string; value: string }[]>([])
|
||||
|
||||
// 初始化环境变量默认值
|
||||
const initEnvDefaults = () => {
|
||||
if (!props.template?.environments) return
|
||||
@@ -46,7 +63,7 @@ const initEnvDefaults = () => {
|
||||
// 获取 select 选项
|
||||
const getSelectOptions = (env: TemplateEnvironment) => {
|
||||
if (!env.options) return []
|
||||
return Object.entries(env.options).map(([value, label]) => ({
|
||||
return Object.entries(env.options).map(([label, value]) => ({
|
||||
label,
|
||||
value
|
||||
}))
|
||||
@@ -75,54 +92,160 @@ const formRules = computed<FormRules>(() => {
|
||||
return rules
|
||||
})
|
||||
|
||||
// 加载编排列表
|
||||
const loadComposeList = () => {
|
||||
composeListLoading.value = true
|
||||
useRequest(containerApi.composeList(1, 1000))
|
||||
.onSuccess(({ data }) => {
|
||||
composeList.value = data.items || []
|
||||
})
|
||||
.onComplete(() => {
|
||||
composeListLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 加载选中编排的详情并预填充环境变量
|
||||
const loadComposeDetailAndFillEnvs = (name: string) => {
|
||||
useRequest(containerApi.composeGet(name)).onSuccess(({ data }) => {
|
||||
selectedComposeData.value = {
|
||||
compose: data.compose,
|
||||
envs: data.envs || []
|
||||
}
|
||||
// 用旧编排的环境变量预填充表单
|
||||
const oldEnvs = data.envs || []
|
||||
oldEnvs.forEach((env: { key: string; value: string }) => {
|
||||
if (env.key in deployModel.envs) {
|
||||
deployModel.envs[env.key] = env.value
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 进入步骤3
|
||||
const goToStep3 = () => {
|
||||
finalCompose.value = props.template?.compose || ''
|
||||
finalEnvs.value = generateFinalEnvs()
|
||||
currentStep.value = 3
|
||||
}
|
||||
|
||||
// 生成最终的环境变量列表
|
||||
const generateFinalEnvs = () => {
|
||||
return Object.entries(deployModel.envs).map(([key, value]) => ({
|
||||
key,
|
||||
value: String(value)
|
||||
}))
|
||||
}
|
||||
|
||||
// 步骤1:选择部署模式
|
||||
const handleModeSelect = (mode: 'create' | 'update') => {
|
||||
deployMode.value = mode
|
||||
if (mode === 'update') {
|
||||
loadComposeList()
|
||||
}
|
||||
currentStep.value = 2
|
||||
}
|
||||
|
||||
// 选择编排后加载详情并预填充环境变量
|
||||
const handleComposeSelect = (name: string) => {
|
||||
selectedCompose.value = name
|
||||
if (name) {
|
||||
loadComposeDetailAndFillEnvs(name)
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤2:验证并进入下一步
|
||||
const handleStep2Next = async () => {
|
||||
if (deployMode.value === 'create') {
|
||||
// 验证编排名称
|
||||
if (!deployModel.name.trim()) {
|
||||
window.$message.warning($gettext('Please enter compose name'))
|
||||
return
|
||||
}
|
||||
|
||||
// 表单校验
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
goToStep3()
|
||||
} else {
|
||||
// 更新模式:验证是否选择了编排
|
||||
if (!selectedCompose.value) {
|
||||
window.$message.warning($gettext('Please select a compose'))
|
||||
return
|
||||
}
|
||||
|
||||
// 表单校验
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
|
||||
goToStep3()
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤3:进入确认步骤
|
||||
const handleStep3Next = () => {
|
||||
currentStep.value = 4
|
||||
}
|
||||
|
||||
// 提交部署
|
||||
const handleSubmit = async () => {
|
||||
if (!props.template) return
|
||||
|
||||
if (!deployModel.name.trim()) {
|
||||
window.$message.warning($gettext('Please enter compose name'))
|
||||
return
|
||||
}
|
||||
|
||||
// 表单校验
|
||||
try {
|
||||
await formRef.value?.validate()
|
||||
} catch {
|
||||
currentTab.value = 'environment'
|
||||
return
|
||||
}
|
||||
|
||||
doSubmit.value = true
|
||||
|
||||
// 构建环境变量数组
|
||||
const envs = Object.entries(deployModel.envs).map(([key, value]) => ({
|
||||
key,
|
||||
value: String(value)
|
||||
}))
|
||||
|
||||
// 创建 compose
|
||||
useRequest(
|
||||
templateApi.create({
|
||||
slug: props.template.slug,
|
||||
name: deployModel.name,
|
||||
envs,
|
||||
auto_firewall: deployModel.autoFirewall
|
||||
})
|
||||
)
|
||||
.onSuccess(({ data }) => {
|
||||
window.$message.success($gettext('Created successfully'))
|
||||
if (deployModel.autoStart) {
|
||||
// 自动启动
|
||||
upCommand.value = `docker compose -f ${data}/docker-compose.yml up -d`
|
||||
upModal.value = true
|
||||
} else {
|
||||
show.value = false
|
||||
emit('success')
|
||||
}
|
||||
})
|
||||
.onComplete(() => {
|
||||
doSubmit.value = false
|
||||
})
|
||||
if (deployMode.value === 'create') {
|
||||
// 创建新编排
|
||||
useRequest(
|
||||
templateApi.create({
|
||||
slug: props.template.slug,
|
||||
name: deployModel.name,
|
||||
compose: finalCompose.value,
|
||||
envs: finalEnvs.value,
|
||||
auto_firewall: deployModel.autoFirewall
|
||||
})
|
||||
)
|
||||
.onSuccess(({ data }) => {
|
||||
window.$message.success($gettext('Created successfully'))
|
||||
if (deployModel.autoStart) {
|
||||
upCommand.value = `docker compose -f ${data}/docker-compose.yml up -d`
|
||||
upModal.value = true
|
||||
} else {
|
||||
show.value = false
|
||||
emit('success')
|
||||
}
|
||||
})
|
||||
.onComplete(() => {
|
||||
doSubmit.value = false
|
||||
})
|
||||
} else {
|
||||
// 更新已有编排
|
||||
useRequest(
|
||||
containerApi.composeUpdate(selectedCompose.value!, {
|
||||
compose: finalCompose.value,
|
||||
envs: finalEnvs.value
|
||||
})
|
||||
)
|
||||
.onSuccess(() => {
|
||||
window.$message.success($gettext('Update successful'))
|
||||
const composePath = composeList.value.find((c) => c.name === selectedCompose.value)?.path
|
||||
if (deployModel.autoStart && composePath) {
|
||||
upCommand.value = `docker compose -f ${composePath}/docker-compose.yml up -d`
|
||||
upModal.value = true
|
||||
} else {
|
||||
show.value = false
|
||||
emit('success')
|
||||
}
|
||||
})
|
||||
.onComplete(() => {
|
||||
doSubmit.value = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 启动完成
|
||||
@@ -131,12 +254,24 @@ const handleUpComplete = () => {
|
||||
emit('success')
|
||||
}
|
||||
|
||||
// 返回上一步
|
||||
const handlePrev = () => {
|
||||
if (currentStep.value > 1) {
|
||||
currentStep.value--
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
deployModel.name = ''
|
||||
deployModel.autoStart = true
|
||||
deployModel.autoFirewall = false
|
||||
deployModel.envs = {}
|
||||
currentTab.value = 'basic'
|
||||
currentStep.value = 1
|
||||
deployMode.value = 'create'
|
||||
selectedCompose.value = null
|
||||
selectedComposeData.value = null
|
||||
finalCompose.value = ''
|
||||
finalEnvs.value = []
|
||||
initEnvDefaults()
|
||||
}
|
||||
|
||||
@@ -154,6 +289,14 @@ watch(
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 编排选项
|
||||
const composeOptions = computed(() => {
|
||||
return composeList.value.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -161,18 +304,70 @@ watch(
|
||||
v-model:show="show"
|
||||
:title="$gettext('Deploy Template') + (template ? ` - ${template.name}` : '')"
|
||||
preset="card"
|
||||
style="width: 60vw"
|
||||
style="width: 70vw"
|
||||
size="huge"
|
||||
:bordered="false"
|
||||
:segmented="false"
|
||||
:mask-closable="!doSubmit"
|
||||
:closable="!doSubmit"
|
||||
>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<!-- 基本设置 -->
|
||||
<n-tab-pane name="basic" :tab="$gettext('Basic Settings')">
|
||||
<n-form :model="deployModel" label-placement="left" label-width="120">
|
||||
<n-form-item path="name" :label="$gettext('Compose Name')">
|
||||
<!-- 步骤指示器 -->
|
||||
<n-steps :current="currentStep" size="small" class="mb-24">
|
||||
<n-step :title="$gettext('Deploy Mode')" />
|
||||
<n-step :title="$gettext('Configuration')" />
|
||||
<n-step :title="$gettext('Preview & Edit')" />
|
||||
<n-step :title="$gettext('Confirm')" />
|
||||
</n-steps>
|
||||
|
||||
<!-- 步骤1:选择部署模式 -->
|
||||
<div v-if="currentStep === 1">
|
||||
<n-flex justify="center" :size="24" style="padding: 40px 0">
|
||||
<n-card hoverable style="width: 280px; cursor: pointer" @click="handleModeSelect('create')">
|
||||
<n-flex vertical align="center" :size="16">
|
||||
<n-icon size="48" color="#18a058">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" />
|
||||
</svg>
|
||||
</n-icon>
|
||||
<n-text strong style="font-size: 16px">{{ $gettext('Create New Compose') }}</n-text>
|
||||
<n-text depth="3" style="text-align: center">
|
||||
{{ $gettext('Create a new compose from this template') }}
|
||||
</n-text>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
|
||||
<n-card hoverable style="width: 280px; cursor: pointer" @click="handleModeSelect('update')">
|
||||
<n-flex vertical align="center" :size="16">
|
||||
<n-icon size="48" color="#2080f0">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path
|
||||
d="M21 10.12h-6.78l2.74-2.82c-2.73-2.7-7.15-2.8-9.88-.1-2.73 2.71-2.73 7.08 0 9.79s7.15 2.71 9.88 0C18.32 15.65 19 14.08 19 12.1h2c0 1.98-.88 4.55-2.64 6.29-3.51 3.48-9.21 3.48-12.72 0-3.5-3.47-3.53-9.11-.02-12.58s9.14-3.47 12.65 0L21 3v7.12z"
|
||||
/>
|
||||
</svg>
|
||||
</n-icon>
|
||||
<n-text strong style="font-size: 16px">{{
|
||||
$gettext('Update Existing Compose')
|
||||
}}</n-text>
|
||||
<n-text depth="3" style="text-align: center">
|
||||
{{ $gettext('Update an existing compose with this template') }}
|
||||
</n-text>
|
||||
</n-flex>
|
||||
</n-card>
|
||||
</n-flex>
|
||||
</div>
|
||||
|
||||
<!-- 步骤2:配置 -->
|
||||
<div v-else-if="currentStep === 2">
|
||||
<!-- 创建模式 -->
|
||||
<template v-if="deployMode === 'create'">
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="deployModel"
|
||||
:rules="formRules"
|
||||
label-placement="left"
|
||||
label-width="160"
|
||||
>
|
||||
<n-form-item path="name" :label="$gettext('Compose Name')" required>
|
||||
<n-input
|
||||
v-model:value="deployModel.name"
|
||||
type="text"
|
||||
@@ -193,22 +388,66 @@ watch(
|
||||
<n-form-item path="autoFirewall" :label="$gettext('Auto Firewall')">
|
||||
<n-switch v-model:value="deployModel.autoFirewall" />
|
||||
<template #feedback>
|
||||
<span>
|
||||
{{ $gettext('Automatically allow ports defined in compose') }}
|
||||
</span>
|
||||
<span>{{ $gettext('Automatically allow ports defined in compose') }}</span>
|
||||
</template>
|
||||
</n-form-item>
|
||||
</n-col>
|
||||
</n-row>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- 环境变量 -->
|
||||
<n-tab-pane
|
||||
v-if="template?.environments?.length"
|
||||
name="environment"
|
||||
:tab="$gettext('Environment Variables')"
|
||||
>
|
||||
<!-- 环境变量 -->
|
||||
<template v-if="template?.environments?.length">
|
||||
<n-divider title-placement="left">{{ $gettext('Environment Variables') }}</n-divider>
|
||||
|
||||
<n-form-item
|
||||
v-for="env in template.environments"
|
||||
:key="env.name"
|
||||
:path="`envs.${env.name}`"
|
||||
:label="env.description"
|
||||
:required="env.default == ''"
|
||||
>
|
||||
<!-- Select 类型 -->
|
||||
<n-select
|
||||
v-if="env.type === 'select'"
|
||||
v-model:value="deployModel.envs[env.name]"
|
||||
:options="getSelectOptions(env)"
|
||||
:placeholder="$gettext('Select value')"
|
||||
/>
|
||||
<!-- Number/Port 类型 -->
|
||||
<n-input-number
|
||||
v-else-if="env.type === 'number' || env.type === 'port'"
|
||||
v-model:value="deployModel.envs[env.name]"
|
||||
:min="env.type === 'port' ? 1 : undefined"
|
||||
:max="env.type === 'port' ? 65535 : undefined"
|
||||
style="width: 100%"
|
||||
:placeholder="String(env.default)"
|
||||
/>
|
||||
<!-- Password 类型 -->
|
||||
<n-input
|
||||
v-else-if="env.type === 'password'"
|
||||
v-model:value="deployModel.envs[env.name]"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="String(env.default)"
|
||||
/>
|
||||
<!-- URL 类型 -->
|
||||
<n-input
|
||||
v-else-if="env.type === 'url'"
|
||||
v-model:value="deployModel.envs[env.name]"
|
||||
:placeholder="String(env.default)"
|
||||
/>
|
||||
<!-- Text 类型 (默认) -->
|
||||
<n-input
|
||||
v-else
|
||||
v-model:value="deployModel.envs[env.name]"
|
||||
:placeholder="String(env.default)"
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<!-- 更新模式 -->
|
||||
<template v-else>
|
||||
<n-form
|
||||
ref="formRef"
|
||||
:model="deployModel"
|
||||
@@ -216,74 +455,191 @@ watch(
|
||||
label-placement="left"
|
||||
label-width="160"
|
||||
>
|
||||
<n-form-item
|
||||
v-for="env in template.environments"
|
||||
:key="env.name"
|
||||
:path="`envs.${env.name}`"
|
||||
:label="env.description"
|
||||
:required="env.default == ''"
|
||||
>
|
||||
<!-- Select 类型 -->
|
||||
<n-form-item :label="$gettext('Select Compose')" required>
|
||||
<n-select
|
||||
v-if="env.type === 'select'"
|
||||
v-model:value="deployModel.envs[env.name]"
|
||||
:options="getSelectOptions(env)"
|
||||
:placeholder="$gettext('Select value')"
|
||||
/>
|
||||
<!-- Number/Port 类型 -->
|
||||
<n-input-number
|
||||
v-else-if="env.type === 'number' || env.type === 'port'"
|
||||
v-model:value="deployModel.envs[env.name]"
|
||||
:min="env.type === 'port' ? 1 : undefined"
|
||||
:max="env.type === 'port' ? 65535 : undefined"
|
||||
style="width: 100%"
|
||||
:placeholder="env.default || ''"
|
||||
/>
|
||||
<!-- Password 类型 -->
|
||||
<n-input
|
||||
v-else-if="env.type === 'password'"
|
||||
v-model:value="deployModel.envs[env.name]"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="env.default || ''"
|
||||
/>
|
||||
<!-- URL 类型 -->
|
||||
<n-input
|
||||
v-else-if="env.type === 'url'"
|
||||
v-model:value="deployModel.envs[env.name]"
|
||||
:placeholder="env.default || ''"
|
||||
/>
|
||||
<!-- Text 类型 (默认) -->
|
||||
<n-input
|
||||
v-else
|
||||
v-model:value="deployModel.envs[env.name]"
|
||||
:placeholder="env.default || ''"
|
||||
:value="selectedCompose"
|
||||
:options="composeOptions"
|
||||
:loading="composeListLoading"
|
||||
:placeholder="$gettext('Select a compose to update')"
|
||||
filterable
|
||||
@update:value="handleComposeSelect"
|
||||
/>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-tab-pane>
|
||||
|
||||
<!-- Compose 预览 -->
|
||||
<n-tab-pane name="compose" :tab="$gettext('Compose Preview')">
|
||||
<common-editor :value="template?.compose || ''" lang="yaml" height="50vh" read-only />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
<n-divider title-placement="left">{{ $gettext('Deploy Options') }}</n-divider>
|
||||
|
||||
<n-row :gutter="[24, 0]">
|
||||
<n-col :span="8">
|
||||
<n-form-item path="autoStart" :label="$gettext('Auto Start')">
|
||||
<n-switch v-model:value="deployModel.autoStart" />
|
||||
</n-form-item>
|
||||
</n-col>
|
||||
</n-row>
|
||||
|
||||
<!-- 环境变量 -->
|
||||
<template v-if="template?.environments?.length">
|
||||
<n-divider title-placement="left">{{ $gettext('Environment Variables') }}</n-divider>
|
||||
|
||||
<n-form-item
|
||||
v-for="env in template.environments"
|
||||
:key="env.name"
|
||||
:path="`envs.${env.name}`"
|
||||
:label="env.description"
|
||||
:required="env.default == ''"
|
||||
>
|
||||
<!-- Select 类型 -->
|
||||
<n-select
|
||||
v-if="env.type === 'select'"
|
||||
v-model:value="deployModel.envs[env.name]"
|
||||
:options="getSelectOptions(env)"
|
||||
:placeholder="$gettext('Select value')"
|
||||
/>
|
||||
<!-- Number/Port 类型 -->
|
||||
<n-input-number
|
||||
v-else-if="env.type === 'number' || env.type === 'port'"
|
||||
v-model:value="deployModel.envs[env.name]"
|
||||
:min="env.type === 'port' ? 1 : undefined"
|
||||
:max="env.type === 'port' ? 65535 : undefined"
|
||||
style="width: 100%"
|
||||
:placeholder="String(env.default)"
|
||||
/>
|
||||
<!-- Password 类型 -->
|
||||
<n-input
|
||||
v-else-if="env.type === 'password'"
|
||||
v-model:value="deployModel.envs[env.name]"
|
||||
type="password"
|
||||
show-password-on="click"
|
||||
:placeholder="String(env.default)"
|
||||
/>
|
||||
<!-- URL 类型 -->
|
||||
<n-input
|
||||
v-else-if="env.type === 'url'"
|
||||
v-model:value="deployModel.envs[env.name]"
|
||||
:placeholder="String(env.default)"
|
||||
/>
|
||||
<!-- Text 类型 (默认) -->
|
||||
<n-input
|
||||
v-else
|
||||
v-model:value="deployModel.envs[env.name]"
|
||||
:placeholder="String(env.default)"
|
||||
/>
|
||||
</n-form-item>
|
||||
</template>
|
||||
</n-form>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- 步骤3:预览和编辑 -->
|
||||
<div v-else-if="currentStep === 3">
|
||||
<n-tabs type="line" animated>
|
||||
<n-tab-pane name="compose" :tab="$gettext('Compose File')">
|
||||
<!-- 创建模式:普通编辑器 -->
|
||||
<template v-if="deployMode === 'create'">
|
||||
<common-editor v-model:value="finalCompose" lang="yaml" height="50vh" />
|
||||
</template>
|
||||
<!-- 更新模式:差异编辑器 -->
|
||||
<template v-else>
|
||||
<n-alert type="info" style="margin-bottom: 12px">
|
||||
{{
|
||||
$gettext(
|
||||
'Left side shows the original compose, right side shows the new compose. You can edit the right side.'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<diff-editor
|
||||
v-if="selectedComposeData"
|
||||
:original="selectedComposeData.compose"
|
||||
v-model:modified="finalCompose"
|
||||
lang="yaml"
|
||||
height="50vh"
|
||||
/>
|
||||
</template>
|
||||
</n-tab-pane>
|
||||
|
||||
<n-tab-pane name="env" :tab="$gettext('Environment Variables')">
|
||||
<n-dynamic-input
|
||||
v-model:value="finalEnvs"
|
||||
preset="pair"
|
||||
:key-placeholder="$gettext('Variable Name')"
|
||||
:value-placeholder="$gettext('Variable Value')"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 步骤4:确认 -->
|
||||
<div v-else-if="currentStep === 4">
|
||||
<n-descriptions :column="1" label-placement="left" bordered>
|
||||
<n-descriptions-item :label="$gettext('Deploy Mode')">
|
||||
<n-tag :type="deployMode === 'create' ? 'success' : 'info'">
|
||||
{{ deployMode === 'create' ? $gettext('Create New') : $gettext('Update Existing') }}
|
||||
</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item :label="$gettext('Compose Name')">
|
||||
{{ deployMode === 'create' ? deployModel.name : selectedCompose }}
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item :label="$gettext('Auto Start')">
|
||||
<n-tag :type="deployModel.autoStart ? 'success' : 'default'">
|
||||
{{ deployModel.autoStart ? $gettext('Yes') : $gettext('No') }}
|
||||
</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item v-if="deployMode === 'create'" :label="$gettext('Auto Firewall')">
|
||||
<n-tag :type="deployModel.autoFirewall ? 'success' : 'default'">
|
||||
{{ deployModel.autoFirewall ? $gettext('Yes') : $gettext('No') }}
|
||||
</n-tag>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item :label="$gettext('Environment Variables')">
|
||||
{{ finalEnvs.length }} {{ $gettext('variables') }}
|
||||
</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
|
||||
<n-divider />
|
||||
|
||||
<n-collapse>
|
||||
<n-collapse-item :title="$gettext('Compose Content')" name="compose">
|
||||
<common-editor :value="finalCompose" lang="yaml" height="30vh" read-only />
|
||||
</n-collapse-item>
|
||||
</n-collapse>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<n-flex justify="end">
|
||||
<n-button @click="show = false" :disabled="doSubmit">
|
||||
{{ $gettext('Cancel') }}
|
||||
</n-button>
|
||||
<n-button type="primary" :loading="doSubmit" :disabled="doSubmit" @click="handleSubmit">
|
||||
{{ $gettext('Deploy') }}
|
||||
<n-flex justify="space-between">
|
||||
<n-button v-if="currentStep > 1" @click="handlePrev" :disabled="doSubmit">
|
||||
{{ $gettext('Previous') }}
|
||||
</n-button>
|
||||
<div v-else />
|
||||
|
||||
<n-flex>
|
||||
<n-button @click="show = false" :disabled="doSubmit">
|
||||
{{ $gettext('Cancel') }}
|
||||
</n-button>
|
||||
<n-button v-if="currentStep === 2" type="primary" @click="handleStep2Next">
|
||||
{{ $gettext('Next') }}
|
||||
</n-button>
|
||||
<n-button v-else-if="currentStep === 3" type="primary" @click="handleStep3Next">
|
||||
{{ $gettext('Next') }}
|
||||
</n-button>
|
||||
<n-button
|
||||
v-else-if="currentStep === 4"
|
||||
type="primary"
|
||||
:loading="doSubmit"
|
||||
:disabled="doSubmit"
|
||||
@click="handleSubmit"
|
||||
>
|
||||
{{ deployMode === 'create' ? $gettext('Create') : $gettext('Update') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-modal>
|
||||
|
||||
<pty-terminal-modal
|
||||
v-model:show="upModal"
|
||||
:title="$gettext('Starting Compose') + ' - ' + deployModel.name"
|
||||
:title="
|
||||
$gettext('Starting Compose') +
|
||||
' - ' +
|
||||
(deployMode === 'create' ? deployModel.name : selectedCompose)
|
||||
"
|
||||
:command="upCommand"
|
||||
@complete="handleUpComplete"
|
||||
/>
|
||||
|
||||
@@ -114,7 +114,7 @@ onMounted(() => {
|
||||
size="small"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-mdi-open-in-new />
|
||||
<i-mdi-open-in-new />
|
||||
</template>
|
||||
</n-button>
|
||||
</n-flex>
|
||||
|
||||
Reference in New Issue
Block a user