mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 05:31:44 +08:00
552 lines
18 KiB
Vue
552 lines
18 KiB
Vue
<script setup lang="ts">
|
|
import container from '@/api/panel/container'
|
|
import { useGettext } from 'vue3-gettext'
|
|
import ImagePullModal from './ImagePullModal.vue'
|
|
|
|
const { $gettext } = useGettext()
|
|
|
|
const show = defineModel<boolean>('show', { type: Boolean, required: true })
|
|
|
|
const doSubmit = ref(false)
|
|
const currentTab = ref('basic')
|
|
|
|
// 镜像拉取
|
|
const showPullModal = ref(false)
|
|
|
|
const createModel = reactive({
|
|
name: '',
|
|
image: '',
|
|
publish_all_ports: false,
|
|
ports: [] as {
|
|
container_start: number
|
|
container_end: number
|
|
host_start: number
|
|
host_end: number
|
|
host: string
|
|
protocol: string
|
|
}[],
|
|
network: '',
|
|
volumes: [] as {
|
|
host: string
|
|
container: string
|
|
mode: string
|
|
}[],
|
|
cpus: 0,
|
|
memory: 0,
|
|
cpu_shares: 1024,
|
|
env: [] as { key: string; value: string }[],
|
|
labels: [] as { key: string; value: string }[],
|
|
command: [] as string[],
|
|
entrypoint: [] as string[],
|
|
restart_policy: 'no',
|
|
tty: false,
|
|
open_stdin: false,
|
|
auto_remove: false,
|
|
privileged: false
|
|
})
|
|
|
|
const networks = ref<{ label: string; value: string }[]>([])
|
|
|
|
const restartPolicyOptions = [
|
|
{ label: $gettext('None'), value: 'no' },
|
|
{ label: $gettext('Always'), value: 'always' },
|
|
{ label: $gettext('On failure (default 5 retries)'), value: 'on-failure' },
|
|
{ label: $gettext('Unless stopped'), value: 'unless-stopped' }
|
|
]
|
|
|
|
const protocolOptions = [
|
|
{ label: 'TCP', value: 'tcp' },
|
|
{ label: 'UDP', value: 'udp' }
|
|
]
|
|
|
|
const volumeModeOptions = [
|
|
{ label: $gettext('Read-Write'), value: 'rw' },
|
|
{ label: $gettext('Read-Only'), value: 'ro' }
|
|
]
|
|
|
|
// 端口映射操作
|
|
const onCreatePort = () => ({
|
|
container_start: 80,
|
|
container_end: 80,
|
|
host_start: 80,
|
|
host_end: 80,
|
|
host: '',
|
|
protocol: 'tcp'
|
|
})
|
|
|
|
// 挂载卷操作
|
|
const onCreateVolume = () => ({
|
|
host: '/www',
|
|
container: '/www',
|
|
mode: 'rw'
|
|
})
|
|
|
|
// 环境变量操作
|
|
const onCreateEnv = () => ({ key: '', value: '' })
|
|
|
|
// 标签操作
|
|
const onCreateLabel = () => ({ key: '', value: '' })
|
|
|
|
const getNetworks = () => {
|
|
useRequest(container.networkList(1, 1000)).onSuccess(({ data }) => {
|
|
networks.value = data.items.map((item: any) => ({
|
|
label: item.name,
|
|
value: item.id
|
|
}))
|
|
if (networks.value.length > 0) {
|
|
createModel.network = networks.value[0]?.value ?? ''
|
|
}
|
|
})
|
|
}
|
|
|
|
// 创建容器
|
|
const createContainer = () => {
|
|
doSubmit.value = true
|
|
useRequest(container.containerCreate(createModel))
|
|
.onSuccess(() => {
|
|
window.$message.success($gettext('Created successfully'))
|
|
show.value = false
|
|
})
|
|
.onComplete(() => {
|
|
doSubmit.value = false
|
|
})
|
|
}
|
|
|
|
// 镜像拉取成功后创建容器
|
|
const onPullSuccess = () => {
|
|
createContainer()
|
|
}
|
|
|
|
// 提交处理
|
|
const handleSubmit = () => {
|
|
if (!createModel.image) {
|
|
window.$message.warning($gettext('Please enter image name'))
|
|
return
|
|
}
|
|
|
|
doSubmit.value = true
|
|
|
|
// 检查镜像是否存在
|
|
useRequest(container.imageExist(createModel.image))
|
|
.onSuccess(({ data }) => {
|
|
if (data) {
|
|
// 镜像存在,直接创建容器
|
|
createContainer()
|
|
} else {
|
|
// 镜像不存在,显示拉取弹窗
|
|
showPullModal.value = true
|
|
}
|
|
})
|
|
.onComplete(() => {
|
|
if (!showPullModal.value) {
|
|
doSubmit.value = false
|
|
}
|
|
})
|
|
}
|
|
|
|
const resetForm = () => {
|
|
createModel.name = ''
|
|
createModel.image = ''
|
|
createModel.publish_all_ports = false
|
|
createModel.ports = []
|
|
createModel.volumes = []
|
|
createModel.cpus = 0
|
|
createModel.memory = 0
|
|
createModel.cpu_shares = 1024
|
|
createModel.env = []
|
|
createModel.labels = []
|
|
createModel.command = []
|
|
createModel.entrypoint = []
|
|
createModel.restart_policy = 'no'
|
|
createModel.tty = false
|
|
createModel.open_stdin = false
|
|
createModel.auto_remove = false
|
|
createModel.privileged = false
|
|
currentTab.value = 'basic'
|
|
showPullModal.value = false
|
|
}
|
|
|
|
watch(show, (val) => {
|
|
if (val) {
|
|
resetForm()
|
|
getNetworks()
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<n-modal
|
|
v-model:show="show"
|
|
:title="$gettext('Create Container')"
|
|
preset="card"
|
|
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="createModel" label-placement="left" label-width="120">
|
|
<n-form-item path="name" :label="$gettext('Container Name')">
|
|
<n-input
|
|
v-model:value="createModel.name"
|
|
type="text"
|
|
@keydown.enter.prevent
|
|
:placeholder="$gettext('Optional, auto-generated if empty')"
|
|
/>
|
|
</n-form-item>
|
|
|
|
<n-form-item path="image" :label="$gettext('Image')">
|
|
<n-input
|
|
v-model:value="createModel.image"
|
|
type="text"
|
|
@keydown.enter.prevent
|
|
:placeholder="$gettext('e.g., nginx, mysql:8.4, your_username/your_image:tag')"
|
|
/>
|
|
</n-form-item>
|
|
|
|
<n-form-item path="network" :label="$gettext('Network')">
|
|
<n-select
|
|
v-model:value="createModel.network"
|
|
:options="networks"
|
|
:placeholder="$gettext('Select network')"
|
|
/>
|
|
</n-form-item>
|
|
|
|
<n-form-item path="restart_policy" :label="$gettext('Restart Policy')">
|
|
<n-select
|
|
v-model:value="createModel.restart_policy"
|
|
:options="restartPolicyOptions"
|
|
:placeholder="$gettext('Select restart policy')"
|
|
/>
|
|
</n-form-item>
|
|
|
|
<n-divider title-placement="left">{{ $gettext('Container Options') }}</n-divider>
|
|
|
|
<n-row :gutter="[24, 0]">
|
|
<n-col :span="6">
|
|
<n-form-item path="tty" :label="$gettext('TTY (-t)')">
|
|
<n-switch v-model:value="createModel.tty" />
|
|
</n-form-item>
|
|
</n-col>
|
|
<n-col :span="6">
|
|
<n-form-item path="open_stdin" :label="$gettext('STDIN (-i)')">
|
|
<n-switch v-model:value="createModel.open_stdin" />
|
|
</n-form-item>
|
|
</n-col>
|
|
<n-col :span="6">
|
|
<n-form-item path="auto_remove" :label="$gettext('Auto Remove')">
|
|
<n-switch v-model:value="createModel.auto_remove" />
|
|
</n-form-item>
|
|
</n-col>
|
|
<n-col :span="6">
|
|
<n-form-item path="privileged" :label="$gettext('Privileged')">
|
|
<n-switch v-model:value="createModel.privileged" />
|
|
</n-form-item>
|
|
</n-col>
|
|
</n-row>
|
|
</n-form>
|
|
</n-tab-pane>
|
|
|
|
<!-- 端口映射 -->
|
|
<n-tab-pane name="ports" :tab="$gettext('Port Mapping')">
|
|
<n-form :model="createModel" label-placement="left" label-width="120">
|
|
<n-form-item :label="$gettext('Port Mode')">
|
|
<n-radio-group v-model:value="createModel.publish_all_ports">
|
|
<n-radio-button :value="false">{{ $gettext('Map Ports') }}</n-radio-button>
|
|
<n-radio-button :value="true">{{ $gettext('Expose All') }}</n-radio-button>
|
|
</n-radio-group>
|
|
</n-form-item>
|
|
|
|
<n-form-item
|
|
v-if="!createModel.publish_all_ports"
|
|
:label="$gettext('Port Mapping')"
|
|
:show-label="false"
|
|
>
|
|
<n-dynamic-input
|
|
v-model:value="createModel.ports"
|
|
:on-create="onCreatePort"
|
|
show-sort-button
|
|
>
|
|
<template #default="{ value }">
|
|
<n-flex align="center" :wrap="false" style="width: 100%">
|
|
<n-input
|
|
v-model:value="value.host"
|
|
:placeholder="$gettext('IP (optional)')"
|
|
style="width: 120px"
|
|
/>
|
|
<span>:</span>
|
|
<n-input-number
|
|
v-model:value="value.host_start"
|
|
:min="1"
|
|
:max="65535"
|
|
:show-button="false"
|
|
:placeholder="$gettext('Host Start')"
|
|
style="width: 90px"
|
|
/>
|
|
<span>-</span>
|
|
<n-input-number
|
|
v-model:value="value.host_end"
|
|
:min="1"
|
|
:max="65535"
|
|
:show-button="false"
|
|
:placeholder="$gettext('Host End')"
|
|
style="width: 90px"
|
|
/>
|
|
<span>:</span>
|
|
<n-input-number
|
|
v-model:value="value.container_start"
|
|
:min="1"
|
|
:max="65535"
|
|
:show-button="false"
|
|
:placeholder="$gettext('Container Start')"
|
|
style="width: 90px"
|
|
/>
|
|
<span>-</span>
|
|
<n-input-number
|
|
v-model:value="value.container_end"
|
|
:min="1"
|
|
:max="65535"
|
|
:show-button="false"
|
|
:placeholder="$gettext('Container End')"
|
|
style="width: 90px"
|
|
/>
|
|
<n-select
|
|
v-model:value="value.protocol"
|
|
:options="protocolOptions"
|
|
style="width: 90px"
|
|
/>
|
|
</n-flex>
|
|
</template>
|
|
</n-dynamic-input>
|
|
</n-form-item>
|
|
|
|
<n-alert v-if="createModel.publish_all_ports" type="info">
|
|
{{
|
|
$gettext(
|
|
'All exposed ports in the image will be automatically mapped to random host ports.'
|
|
)
|
|
}}
|
|
</n-alert>
|
|
</n-form>
|
|
</n-tab-pane>
|
|
|
|
<!-- 存储挂载 -->
|
|
<n-tab-pane name="volumes" :tab="$gettext('Volumes')">
|
|
<n-form :model="createModel" label-placement="left" label-width="120">
|
|
<n-form-item :label="$gettext('Volume Mounts')" :show-label="false">
|
|
<n-dynamic-input
|
|
v-model:value="createModel.volumes"
|
|
:on-create="onCreateVolume"
|
|
show-sort-button
|
|
>
|
|
<template #default="{ value }">
|
|
<n-flex align="center" :wrap="false" style="width: 100%">
|
|
<n-input
|
|
v-model:value="value.host"
|
|
:placeholder="$gettext('Host path')"
|
|
style="flex: 1"
|
|
/>
|
|
<span>:</span>
|
|
<n-input
|
|
v-model:value="value.container"
|
|
:placeholder="$gettext('Container path')"
|
|
style="flex: 1"
|
|
/>
|
|
<n-select
|
|
v-model:value="value.mode"
|
|
:options="volumeModeOptions"
|
|
style="width: 120px"
|
|
/>
|
|
</n-flex>
|
|
</template>
|
|
</n-dynamic-input>
|
|
</n-form-item>
|
|
|
|
<n-alert type="info" style="margin-top: 16px">
|
|
{{
|
|
$gettext(
|
|
'Mount host directories or volumes into the container. Use absolute paths for host directories.'
|
|
)
|
|
}}
|
|
</n-alert>
|
|
</n-form>
|
|
</n-tab-pane>
|
|
|
|
<!-- 资源限制 -->
|
|
<n-tab-pane name="resources" :tab="$gettext('Resource Limits')">
|
|
<n-form :model="createModel" label-placement="left" label-width="120">
|
|
<n-alert type="info" style="margin-bottom: 16px">
|
|
{{
|
|
$gettext(
|
|
'Set resource limits to prevent the container from consuming too many system resources. Set to 0 for no limit.'
|
|
)
|
|
}}
|
|
</n-alert>
|
|
|
|
<n-row :gutter="[24, 0]">
|
|
<n-col :span="8">
|
|
<n-form-item path="memory" :label="$gettext('Memory (MB)')">
|
|
<n-input-number
|
|
v-model:value="createModel.memory"
|
|
:min="0"
|
|
style="width: 100%"
|
|
:placeholder="$gettext('0 = no limit')"
|
|
/>
|
|
</n-form-item>
|
|
</n-col>
|
|
<n-col :span="8">
|
|
<n-form-item path="cpus" :label="$gettext('CPU Cores')">
|
|
<n-input-number
|
|
v-model:value="createModel.cpus"
|
|
:min="0"
|
|
:precision="2"
|
|
:step="0.5"
|
|
style="width: 100%"
|
|
:placeholder="$gettext('0 = no limit')"
|
|
/>
|
|
</n-form-item>
|
|
</n-col>
|
|
<n-col :span="8">
|
|
<n-form-item path="cpu_shares" :label="$gettext('CPU Shares')">
|
|
<n-input-number
|
|
v-model:value="createModel.cpu_shares"
|
|
:min="0"
|
|
:max="262144"
|
|
style="width: 100%"
|
|
/>
|
|
</n-form-item>
|
|
</n-col>
|
|
</n-row>
|
|
|
|
<n-collapse style="margin-top: 16px">
|
|
<n-collapse-item :title="$gettext('Resource Limit Description')">
|
|
<n-descriptions :column="1" label-placement="left">
|
|
<n-descriptions-item :label="$gettext('Memory')">
|
|
{{ $gettext('Maximum memory the container can use, in MB. 0 means no limit.') }}
|
|
</n-descriptions-item>
|
|
<n-descriptions-item :label="$gettext('CPU Cores')">
|
|
{{
|
|
$gettext(
|
|
'Number of CPU cores the container can use. 0.5 means half a core, 2 means 2 cores.'
|
|
)
|
|
}}
|
|
</n-descriptions-item>
|
|
<n-descriptions-item :label="$gettext('CPU Shares')">
|
|
{{
|
|
$gettext(
|
|
'Relative CPU weight. Default is 1024. Higher values get more CPU time when competing.'
|
|
)
|
|
}}
|
|
</n-descriptions-item>
|
|
</n-descriptions>
|
|
</n-collapse-item>
|
|
</n-collapse>
|
|
</n-form>
|
|
</n-tab-pane>
|
|
|
|
<!-- 环境与命令 -->
|
|
<n-tab-pane name="environment" :tab="$gettext('Environment')">
|
|
<n-form :model="createModel" label-placement="left" label-width="140">
|
|
<n-form-item :label="$gettext('Environment Variables')">
|
|
<n-dynamic-input
|
|
v-model:value="createModel.env"
|
|
:on-create="onCreateEnv"
|
|
show-sort-button
|
|
>
|
|
<template #default="{ value }">
|
|
<n-flex align="center" :wrap="false" style="width: 100%">
|
|
<n-input
|
|
v-model:value="value.key"
|
|
:placeholder="$gettext('Variable name')"
|
|
style="flex: 1"
|
|
/>
|
|
<span>=</span>
|
|
<n-input
|
|
v-model:value="value.value"
|
|
:placeholder="$gettext('Variable value')"
|
|
style="flex: 2"
|
|
/>
|
|
</n-flex>
|
|
</template>
|
|
</n-dynamic-input>
|
|
</n-form-item>
|
|
|
|
<n-divider title-placement="left">{{ $gettext('Startup Commands') }}</n-divider>
|
|
|
|
<n-form-item path="command" :label="$gettext('Command')">
|
|
<n-dynamic-input
|
|
v-model:value="createModel.command"
|
|
:placeholder="$gettext('Command argument')"
|
|
/>
|
|
<template #feedback>
|
|
<span class="text-gray-400">
|
|
{{ $gettext('Override the default CMD of the image') }}
|
|
</span>
|
|
</template>
|
|
</n-form-item>
|
|
|
|
<n-form-item path="entrypoint" :label="$gettext('Entrypoint')">
|
|
<n-dynamic-input
|
|
v-model:value="createModel.entrypoint"
|
|
:placeholder="$gettext('Entrypoint argument')"
|
|
/>
|
|
<template #feedback>
|
|
<span class="text-gray-400">
|
|
{{ $gettext('Override the default ENTRYPOINT of the image') }}
|
|
</span>
|
|
</template>
|
|
</n-form-item>
|
|
|
|
<n-divider title-placement="left">{{ $gettext('Labels') }}</n-divider>
|
|
|
|
<n-form-item :label="$gettext('Container Labels')">
|
|
<n-dynamic-input
|
|
v-model:value="createModel.labels"
|
|
:on-create="onCreateLabel"
|
|
show-sort-button
|
|
>
|
|
<template #default="{ value }">
|
|
<n-flex align="center" :wrap="false" style="width: 100%">
|
|
<n-input
|
|
v-model:value="value.key"
|
|
:placeholder="$gettext('Label name')"
|
|
style="flex: 1"
|
|
/>
|
|
<span>=</span>
|
|
<n-input
|
|
v-model:value="value.value"
|
|
:placeholder="$gettext('Label value')"
|
|
style="flex: 2"
|
|
/>
|
|
</n-flex>
|
|
</template>
|
|
</n-dynamic-input>
|
|
</n-form-item>
|
|
</n-form>
|
|
</n-tab-pane>
|
|
</n-tabs>
|
|
|
|
<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('Create') }}
|
|
</n-button>
|
|
</n-flex>
|
|
</template>
|
|
</n-modal>
|
|
|
|
<!-- 镜像拉取弹窗 -->
|
|
<image-pull-modal
|
|
v-model:show="showPullModal"
|
|
:image="createModel.image"
|
|
@success="onPullSuccess"
|
|
/>
|
|
</template>
|