2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-06 14:47:18 +08:00

feat: 提交部分前端翻译

This commit is contained in:
2025-04-13 01:14:11 +08:00
parent db0679cd92
commit df32b3b5de
25 changed files with 5589 additions and 1294 deletions

View File

@@ -1,10 +1,12 @@
<script setup lang="ts">
import { NButton, NCheckbox, NDataTable, NFlex, NInput, NPopconfirm, NTag } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import container from '@/api/panel/container'
import { useFileStore } from '@/store'
import { formatDateTime } from '@/utils'
const { $gettext } = useGettext()
const fileStore = useFileStore()
const router = useRouter()
@@ -26,14 +28,14 @@ const updateModal = ref(false)
const columns: any = [
{
title: '名称',
title: $gettext('Name'),
key: 'name',
minWidth: 150,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: '目录',
title: $gettext('Directory'),
key: 'path',
minWidth: 150,
resizable: true,
@@ -53,14 +55,14 @@ const columns: any = [
}
},
{
title: '状态',
title: $gettext('Status'),
key: 'status',
width: 150,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: '创建时间',
title: $gettext('Creation Time'),
key: 'created_at',
width: 200,
resizable: true,
@@ -69,7 +71,7 @@ const columns: any = [
}
},
{
title: '操作',
title: $gettext('Actions'),
key: 'actions',
width: 280,
align: 'center',
@@ -92,7 +94,7 @@ const columns: any = [
}
},
{
default: () => '编辑'
default: () => $gettext('Edit')
}
),
h(
@@ -100,14 +102,14 @@ const columns: any = [
{
showIcon: false,
onPositiveClick: () => {
const messageReactive = window.$message.loading('启动中...', {
const messageReactive = window.$message.loading($gettext('Starting...'), {
duration: 0
})
useRequest(container.composeUp(row.name, forcePush.value))
.onSuccess(() => {
refresh()
forcePush.value = false
window.$message.success('启动成功')
window.$message.success($gettext('Start successful'))
})
.onComplete(() => {
messageReactive?.destroy()
@@ -123,14 +125,14 @@ const columns: any = [
},
{
default: () => [
h('strong', {}, { default: () => `确定启动编排 ${row.name} 吗?` }),
h('strong', {}, { default: () => $gettext(`Are you sure you want to start compose %{ name }?`, { name: row.name }) }),
h(
NCheckbox,
{
checked: forcePush.value,
onUpdateChecked: (v) => (forcePush.value = v)
},
{ default: () => '强制拉取镜像' }
{ default: () => $gettext('Force pull images') }
)
]
}
@@ -145,7 +147,7 @@ const columns: any = [
type: 'success'
},
{
default: () => '启动'
default: () => $gettext('Start')
}
)
}
@@ -157,13 +159,13 @@ const columns: any = [
onPositiveClick: () => {
useRequest(container.composeDown(row.name)).onSuccess(() => {
refresh()
window.$message.success('停止成功')
window.$message.success($gettext('Stop successful'))
})
}
},
{
default: () => {
return `确定停止编排 ${row.name} 吗?`
return $gettext(`Are you sure you want to stop compose %{ name }?`, { name: row.name })
},
trigger: () => {
return h(
@@ -174,7 +176,7 @@ const columns: any = [
type: 'warning'
},
{
default: () => '停止'
default: () => $gettext('Stop')
}
)
}
@@ -186,13 +188,13 @@ const columns: any = [
onPositiveClick: () => {
useRequest(container.composeRemove(row.name)).onSuccess(() => {
refresh()
window.$message.success('删除成功')
window.$message.success($gettext('Delete successful'))
})
}
},
{
default: () => {
return `确定删除编排 ${row.name} 吗?`
return $gettext(`Are you sure you want to delete compose %{ name }?`, { name: row.name })
},
trigger: () => {
return h(
@@ -203,7 +205,7 @@ const columns: any = [
type: 'error'
},
{
default: () => '删除'
default: () => $gettext('Delete')
}
)
}
@@ -229,7 +231,7 @@ const handleCreate = () => {
useRequest(container.composeCreate(createModel.value))
.onSuccess(() => {
refresh()
window.$message.success('创建成功')
window.$message.success($gettext('Created successfully'))
})
.onComplete(() => {
loading.value = false
@@ -247,7 +249,7 @@ const handleUpdate = () => {
useRequest(container.composeUpdate(updateModel.value.name, updateModel.value))
.onSuccess(() => {
refresh()
window.$message.success('更新成功')
window.$message.success($gettext('Update successful'))
})
.onComplete(() => {
loading.value = false
@@ -268,7 +270,7 @@ onMounted(() => {
<template>
<n-flex vertical :size="20">
<n-flex>
<n-button type="primary" @click="createModal = true">创建编排</n-button>
<n-button type="primary" @click="createModal = true">{{ $gettext('Create Compose') }}</n-button>
</n-flex>
<n-data-table
striped
@@ -294,64 +296,64 @@ onMounted(() => {
<n-modal
v-model:show="createModal"
preset="card"
title="创建编排"
:title="$gettext('Create Compose')"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
>
<n-form :model="createModel">
<n-form-item path="name" label="编排名">
<n-form-item path="name" :label="$gettext('Compose Name')">
<n-input v-model:value="createModel.name" type="text" />
</n-form-item>
<n-form-item path="compose" label="编排">
<n-form-item path="compose" :label="$gettext('Compose')">
<n-input
v-model:value="createModel.compose"
type="textarea"
:autosize="{ minRows: 10, maxRows: 20 }"
/>
</n-form-item>
<n-form-item path="envs" label="环境变量">
<n-form-item path="envs" :label="$gettext('Environment Variables')">
<n-dynamic-input
v-model:value="createModel.envs"
preset="pair"
key-placeholder="变量名"
value-placeholder="变量值"
:key-placeholder="$gettext('Variable Name')"
:value-placeholder="$gettext('Variable Value')"
/>
</n-form-item>
</n-form>
<n-button type="info" block :loading="loading" :disabled="loading" @click="handleCreate">
提交
{{ $gettext('Submit') }}
</n-button>
</n-modal>
<n-modal
v-model:show="updateModal"
preset="card"
title="编辑编排"
:title="$gettext('Edit Compose')"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
>
<n-form :model="updateModel">
<n-form-item path="compose" label="编排">
<n-form-item path="compose" :label="$gettext('Compose')">
<n-input
v-model:value="updateModel.compose"
type="textarea"
:autosize="{ minRows: 10, maxRows: 20 }"
/>
</n-form-item>
<n-form-item path="envs" label="环境变量">
<n-form-item path="envs" :label="$gettext('Environment Variables')">
<n-dynamic-input
v-model:value="updateModel.envs"
preset="pair"
key-placeholder="变量名"
value-placeholder="变量值"
:key-placeholder="$gettext('Variable Name')"
:value-placeholder="$gettext('Variable Value')"
/>
</n-form-item>
</n-form>
<n-button type="info" block :loading="loading" :disabled="loading" @click="handleUpdate">
提交
{{ $gettext('Submit') }}
</n-button>
</n-modal>
</template>

View File

@@ -1,5 +1,8 @@
<script setup lang="ts">
import container from '@/api/panel/container'
import { useGettext } from 'vue3-gettext'
const { $gettext } = useGettext()
const props = defineProps({
show: {
@@ -49,10 +52,10 @@ const createModel = reactive({
const networks = ref<any>({})
const restartPolicyOptions = [
{ label: '无', value: 'no' },
{ label: '始终', value: 'always' },
{ label: '失败时(默认重启 5 次)', value: 'on-failure' },
{ label: '未手动停止则重启', value: 'unless-stopped' }
{ 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 addPortRow = () => {
@@ -100,7 +103,7 @@ const handleSubmit = () => {
doSubmit.value = true
useRequest(container.containerCreate(createModel))
.onSuccess(() => {
window.$message.success('创建成功')
window.$message.success($gettext('Created successfully'))
handleClose()
})
.onComplete(() => {
@@ -121,7 +124,7 @@ onMounted(() => {
<template>
<n-modal
title="创建容器"
:title="$gettext('Create Container')"
preset="card"
style="width: 60vw"
size="huge"
@@ -131,40 +134,40 @@ onMounted(() => {
@close="handleClose"
>
<n-form :model="createModel">
<n-form-item path="name" label="容器名">
<n-form-item path="name" :label="$gettext('Container Name')">
<n-input v-model:value="createModel.name" type="text" @keydown.enter.prevent />
</n-form-item>
<n-form-item path="name" label="镜像">
<n-form-item path="name" :label="$gettext('Image')">
<n-input v-model:value="createModel.image" type="text" @keydown.enter.prevent />
</n-form-item>
<n-form-item path="exposedAll" label="端口">
<n-form-item path="exposedAll" :label="$gettext('Ports')">
<n-radio
:checked="!createModel.publish_all_ports"
:value="false"
@change="createModel.publish_all_ports = !$event.target.value"
>
映射端口
{{ $gettext('Map Ports') }}
</n-radio>
<n-radio
:checked="createModel.publish_all_ports"
:value="true"
@change="createModel.publish_all_ports = !!$event.target.value"
>
暴露所有
{{ $gettext('Expose All') }}
</n-radio>
</n-form-item>
<n-form-item path="ports" label="端口映射" v-if="!createModel.publish_all_ports">
<n-form-item path="ports" :label="$gettext('Port Mapping')" v-if="!createModel.publish_all_ports">
<n-space vertical>
<n-table striped>
<thead>
<tr>
<th>IP</th>
<th>主机起始</th>
<th>主机结束</th>
<th>容器起始</th>
<th>容器结束</th>
<th>协议</th>
<th>操作</th>
<th>{{ $gettext('Host (Start)') }}</th>
<th>{{ $gettext('Host (End)') }}</th>
<th>{{ $gettext('Container (Start)') }}</th>
<th>{{ $gettext('Container (End)') }}</th>
<th>{{ $gettext('Protocol') }}</th>
<th>{{ $gettext('Actions') }}</th>
</tr>
</thead>
<tbody>
@@ -174,7 +177,7 @@ onMounted(() => {
v-model:value="item.host"
type="text"
@keydown.enter.prevent
placeholder="可留空"
:placeholder="$gettext('Optional')"
/>
</td>
<td>
@@ -223,25 +226,25 @@ onMounted(() => {
UDP
</n-radio>
</td>
<td><n-button @click="removePortRow(index)" size="small">删除</n-button></td>
<td><n-button @click="removePortRow(index)" size="small">{{ $gettext('Delete') }}</n-button></td>
</tr>
</tbody>
</n-table>
<n-button @click="addPortRow">添加</n-button>
<n-button @click="addPortRow">{{ $gettext('Add') }}</n-button>
</n-space>
</n-form-item>
<n-form-item path="network" label="网络">
<n-form-item path="network" :label="$gettext('Network')">
<n-select v-model:value="createModel.network" :options="networks" />
</n-form-item>
<n-form-item path="mount" label="挂载">
<n-form-item path="mount" :label="$gettext('Mount')">
<n-space vertical>
<n-table striped>
<thead>
<tr>
<th>主机目录</th>
<th>容器目录</th>
<th>权限</th>
<th>操作</th>
<th>{{ $gettext('Host Directory') }}</th>
<th>{{ $gettext('Container Directory') }}</th>
<th>{{ $gettext('Permission') }}</th>
<th>{{ $gettext('Actions') }}</th>
</tr>
</thead>
<tbody>
@@ -259,7 +262,7 @@ onMounted(() => {
name="mode"
@change="item.mode = $event.target.value"
>
读写
{{ $gettext('Read-Write') }}
</n-radio>
<n-radio
:checked="item.mode === 'ro'"
@@ -267,25 +270,25 @@ onMounted(() => {
name="mode"
@change="item.mode = $event.target.value"
>
只读
{{ $gettext('Read-Only') }}
</n-radio>
</td>
<td><n-button @click="removeVolumeRow(index)" size="small">删除</n-button></td>
<td><n-button @click="removeVolumeRow(index)" size="small">{{ $gettext('Delete') }}</n-button></td>
</tr>
</tbody>
</n-table>
<n-button @click="addVolumeRow">添加</n-button>
<n-button @click="addVolumeRow">{{ $gettext('Add') }}</n-button>
</n-space>
</n-form-item>
<n-form-item path="command" label="启动命令">
<n-dynamic-input v-model:value="createModel.command" placeholder="命令" />
<n-form-item path="command" :label="$gettext('Command')">
<n-dynamic-input v-model:value="createModel.command" :placeholder="$gettext('Command')" />
</n-form-item>
<n-form-item path="entrypoint" label="入口点">
<n-dynamic-input v-model:value="createModel.entrypoint" placeholder="入口点" />
<n-form-item path="entrypoint" :label="$gettext('Entrypoint')">
<n-dynamic-input v-model:value="createModel.entrypoint" :placeholder="$gettext('Entrypoint')" />
</n-form-item>
<n-row :gutter="[0, 24]">
<n-col :span="8">
<n-form-item path="memory" label="内存">
<n-form-item path="memory" :label="$gettext('Memory')">
<n-input-number v-model:value="createModel.memory" />
</n-form-item>
</n-col>
@@ -295,61 +298,61 @@ onMounted(() => {
</n-form-item>
</n-col>
<n-col :span="8">
<n-form-item path="cpu_shares" label="CPU 权重">
<n-form-item path="cpu_shares" :label="$gettext('CPU Shares')">
<n-input-number v-model:value="createModel.cpu_shares" />
</n-form-item>
</n-col>
</n-row>
<n-row :gutter="[0, 24]">
<n-col :span="6">
<n-form-item path="tty" label="伪终端(-t">
<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="标准输入(-i">
<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="退出后自动删除">
<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="特权模式">
<n-form-item path="privileged" :label="$gettext('Privileged Mode')">
<n-switch v-model:value="createModel.privileged" />
</n-form-item>
</n-col>
</n-row>
<n-form-item path="restart_policy" label="重启策略">
<n-form-item path="restart_policy" :label="$gettext('Restart Policy')">
<n-select
v-model:value="createModel.restart_policy"
placeholder="选择重启策略"
:placeholder="$gettext('Select restart policy')"
:options="restartPolicyOptions"
>
{{ createModel.restart_policy || '选择重启策略' }}
{{ createModel.restart_policy || $gettext('Select restart policy') }}
</n-select>
</n-form-item>
<n-form-item path="env" label="环境变量">
<n-form-item path="env" :label="$gettext('Environment Variables')">
<n-dynamic-input
v-model:value="createModel.env"
preset="pair"
key-placeholder="变量名"
value-placeholder="变量值"
:key-placeholder="$gettext('Variable Name')"
:value-placeholder="$gettext('Variable Value')"
/>
</n-form-item>
<n-form-item path="labels" label="标签">
<n-form-item path="labels" :label="$gettext('Labels')">
<n-dynamic-input
v-model:value="createModel.labels"
preset="pair"
key-placeholder="标签名"
value-placeholder="标签值"
:key-placeholder="$gettext('Label Name')"
:value-placeholder="$gettext('Label Value')"
/>
</n-form-item>
</n-form>
<n-button type="info" block :loading="doSubmit" :disabled="doSubmit" @click="handleSubmit">
提交
{{ $gettext('Submit') }}
</n-button>
</n-modal>
</template>

View File

@@ -1,10 +1,13 @@
<script setup lang="ts">
import Editor from '@guolao/vue-monaco-editor'
import { NButton, NDataTable, NDropdown, NFlex, NInput, NSwitch, NTag } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import container from '@/api/panel/container'
import ContainerCreate from '@/views/container/ContainerCreate.vue'
const { $gettext } = useGettext()
const logModal = ref(false)
const logs = ref('')
const renameModal = ref(false)
@@ -19,14 +22,14 @@ const selectedRowKeys = ref<any>([])
const columns: any = [
{ type: 'selection', fixed: 'left' },
{
title: '容器名',
title: $gettext('Container Name'),
key: 'name',
minWidth: 150,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: '状态',
title: $gettext('Status'),
key: 'state',
width: 100,
resizable: true,
@@ -46,7 +49,7 @@ const columns: any = [
}
},
{
title: '镜像',
title: $gettext('Image'),
key: 'image',
minWidth: 300,
resizable: true,
@@ -57,7 +60,7 @@ const columns: any = [
}
},
{
title: '端口(主机->容器)',
title: $gettext('Ports (Host->Container)'),
key: 'ports',
minWidth: 200,
resizable: true,
@@ -74,14 +77,14 @@ const columns: any = [
}
},
{
title: '运行状态',
title: $gettext('Running Status'),
key: 'status',
width: 300,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: '操作',
title: $gettext('Actions'),
key: 'actions',
width: 250,
align: 'center',
@@ -97,7 +100,7 @@ const columns: any = [
onClick: () => handleShowLog(row)
},
{
default: () => '日志'
default: () => $gettext('Logs')
}
),
h(
@@ -113,7 +116,7 @@ const columns: any = [
}
},
{
default: () => '重命名'
default: () => $gettext('Rename')
}
),
h(
@@ -121,37 +124,37 @@ const columns: any = [
{
options: [
{
label: '启动',
label: $gettext('Start'),
key: 'start',
disabled: row.state === 'running'
},
{
label: '停止',
label: $gettext('Stop'),
key: 'stop',
disabled: row.state !== 'running'
},
{
label: '重启',
label: $gettext('Restart'),
key: 'restart',
disabled: row.state !== 'running'
},
{
label: '强制停止',
label: $gettext('Force Stop'),
key: 'forceStop',
disabled: row.state !== 'running'
},
{
label: '暂停',
label: $gettext('Pause'),
key: 'pause',
disabled: row.state !== 'running'
},
{
label: '恢复',
label: $gettext('Resume'),
key: 'unpause',
disabled: row.state === 'running'
},
{
label: '删除',
label: $gettext('Delete'),
key: 'delete'
}
],
@@ -191,7 +194,7 @@ const columns: any = [
style: 'margin-left: 15px;'
},
{
default: () => '更多'
default: () => $gettext('More')
}
)
}
@@ -224,7 +227,7 @@ const handleRename = () => {
() => {
refresh()
renameModal.value = false
window.$message.success('重命名成功')
window.$message.success($gettext('Rename successful'))
}
)
}
@@ -232,62 +235,62 @@ const handleRename = () => {
const handleStart = (id: string) => {
useRequest(container.containerStart(id)).onSuccess(() => {
refresh()
window.$message.success('启动成功')
window.$message.success($gettext('Start successful'))
})
}
const handleStop = (id: string) => {
useRequest(container.containerStop(id)).onSuccess(() => {
refresh()
window.$message.success('停止成功')
window.$message.success($gettext('Stop successful'))
})
}
const handleRestart = (id: string) => {
useRequest(container.containerRestart(id)).onSuccess(() => {
refresh()
window.$message.success('重启成功')
window.$message.success($gettext('Restart successful'))
})
}
const handleForceStop = (id: string) => {
useRequest(container.containerKill(id)).onSuccess(() => {
refresh()
window.$message.success('强制停止成功')
window.$message.success($gettext('Force stop successful'))
})
}
const handlePause = (id: string) => {
useRequest(container.containerPause(id)).onSuccess(() => {
refresh()
window.$message.success('暂停成功')
window.$message.success($gettext('Pause successful'))
})
}
const handleUnpause = (id: string) => {
useRequest(container.containerUnpause(id)).onSuccess(() => {
refresh()
window.$message.success('恢复成功')
window.$message.success($gettext('Resume successful'))
})
}
const handleDelete = (id: string) => {
useRequest(container.containerRemove(id)).onSuccess(() => {
refresh()
window.$message.success('删除成功')
window.$message.success($gettext('Delete successful'))
})
}
const handlePrune = () => {
useRequest(container.containerPrune()).onSuccess(() => {
refresh()
window.$message.success('清理成功')
window.$message.success($gettext('Cleanup successful'))
})
}
const bulkStart = async () => {
if (selectedRowKeys.value.length === 0) {
window.$message.info('请选择要启动的容器')
window.$message.info($gettext('Please select containers to start'))
return
}
@@ -296,12 +299,12 @@ const bulkStart = async () => {
selectedRowKeys.value = []
refresh()
window.$message.success('启动成功')
window.$message.success($gettext('Start successful'))
}
const bulkStop = async () => {
if (selectedRowKeys.value.length === 0) {
window.$message.info('请选择要停止的容器')
window.$message.info($gettext('Please select containers to stop'))
return
}
@@ -310,12 +313,12 @@ const bulkStop = async () => {
selectedRowKeys.value = []
refresh()
window.$message.success('停止成功')
window.$message.success($gettext('Stop successful'))
}
const bulkRestart = async () => {
if (selectedRowKeys.value.length === 0) {
window.$message.info('请选择要重启的容器')
window.$message.info($gettext('Please select containers to restart'))
return
}
@@ -324,12 +327,12 @@ const bulkRestart = async () => {
selectedRowKeys.value = []
refresh()
window.$message.success('重启成功')
window.$message.success($gettext('Restart successful'))
}
const bulkForceStop = async () => {
if (selectedRowKeys.value.length === 0) {
window.$message.info('请选择要强制停止的容器')
window.$message.info($gettext('Please select containers to force stop'))
return
}
@@ -338,12 +341,12 @@ const bulkForceStop = async () => {
selectedRowKeys.value = []
refresh()
window.$message.success('强制停止成功')
window.$message.success($gettext('Force stop successful'))
}
const bulkDelete = async () => {
if (selectedRowKeys.value.length === 0) {
window.$message.info('请选择要删除的容器')
window.$message.info($gettext('Please select containers to delete'))
return
}
@@ -352,12 +355,12 @@ const bulkDelete = async () => {
selectedRowKeys.value = []
refresh()
window.$message.success('删除成功')
window.$message.success($gettext('Delete successful'))
}
const bulkPause = async () => {
if (selectedRowKeys.value.length === 0) {
window.$message.info('请选择要暂停的容器')
window.$message.info($gettext('Please select containers to pause'))
return
}
@@ -366,12 +369,12 @@ const bulkPause = async () => {
selectedRowKeys.value = []
refresh()
window.$message.success('暂停成功')
window.$message.success($gettext('Pause successful'))
}
const bulkUnpause = async () => {
if (selectedRowKeys.value.length === 0) {
window.$message.info('请选择要恢复的容器')
window.$message.info($gettext('Please select containers to resume'))
return
}
@@ -380,7 +383,7 @@ const bulkUnpause = async () => {
selectedRowKeys.value = []
refresh()
window.$message.success('恢复成功')
window.$message.success($gettext('Resume successful'))
}
const closeContainerCreateModal = () => {
@@ -396,16 +399,16 @@ onMounted(() => {
<template>
<n-flex vertical :size="20">
<n-flex>
<n-button type="primary" @click="containerCreateModal = true">创建容器</n-button>
<n-button type="primary" @click="handlePrune" ghost>清理容器</n-button>
<n-button type="primary" @click="containerCreateModal = true">{{ $gettext('Create Container') }}</n-button>
<n-button type="primary" @click="handlePrune" ghost>{{ $gettext('Cleanup Containers') }}</n-button>
<n-button-group>
<n-button @click="bulkStart">启动</n-button>
<n-button @click="bulkStop">停止</n-button>
<n-button @click="bulkRestart">重启</n-button>
<n-button @click="bulkForceStop">强制停止</n-button>
<n-button @click="bulkPause">暂停</n-button>
<n-button @click="bulkUnpause">恢复</n-button>
<n-button @click="bulkDelete">删除</n-button>
<n-button @click="bulkStart">{{ $gettext('Start') }}</n-button>
<n-button @click="bulkStop">{{ $gettext('Stop') }}</n-button>
<n-button @click="bulkRestart">{{ $gettext('Restart') }}</n-button>
<n-button @click="bulkForceStop">{{ $gettext('Force Stop') }}</n-button>
<n-button @click="bulkPause">{{ $gettext('Pause') }}</n-button>
<n-button @click="bulkUnpause">{{ $gettext('Resume') }}</n-button>
<n-button @click="bulkDelete">{{ $gettext('Delete') }}</n-button>
</n-button-group>
</n-flex>
<n-data-table
@@ -433,7 +436,7 @@ onMounted(() => {
<n-modal
v-model:show="logModal"
preset="card"
title="日志"
:title="$gettext('Logs')"
style="width: 80vw"
size="huge"
:bordered="false"
@@ -456,23 +459,23 @@ onMounted(() => {
<n-modal
v-model:show="renameModal"
preset="card"
title="重命名"
:title="$gettext('Rename')"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
>
<n-form :model="renameModel">
<n-form-item path="name" label="新名称">
<n-form-item path="name" :label="$gettext('New Name')">
<n-input
v-model:value="renameModel.name"
type="text"
@keydown.enter.prevent
placeholder="输入新名称"
:placeholder="$gettext('Enter new name')"
/>
</n-form-item>
</n-form>
<n-button type="info" block @click="handleRename">提交</n-button>
<n-button type="info" block @click="handleRename">{{ $gettext('Submit') }}</n-button>
</n-modal>
<ContainerCreate :show="containerCreateModal" @close="closeContainerCreateModal" />
</template>

View File

@@ -1,9 +1,12 @@
<script setup lang="ts">
import { NButton, NDataTable, NFlex, NInput, NPopconfirm, NTag } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import container from '@/api/panel/container'
import { formatDateTime } from '@/utils'
const { $gettext } = useGettext()
const pullModel = ref({
name: '',
auth: false,
@@ -23,14 +26,14 @@ const columns: any = [
ellipsis: { tooltip: true }
},
{
title: '容器数',
title: $gettext('Container Count'),
key: 'containers',
width: 100,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: '镜像',
title: $gettext('Image'),
key: 'repo_tags',
minWidth: 200,
resizable: true,
@@ -47,14 +50,14 @@ const columns: any = [
}
},
{
title: '大小',
title: $gettext('Size'),
key: 'size',
width: 150,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: '创建时间',
title: $gettext('Creation Time'),
key: 'created_at',
width: 200,
resizable: true,
@@ -63,7 +66,7 @@ const columns: any = [
}
},
{
title: '操作',
title: $gettext('Actions'),
key: 'actions',
width: 120,
align: 'center',
@@ -79,7 +82,7 @@ const columns: any = [
},
{
default: () => {
return '确定删除吗?'
return $gettext('Are you sure you want to delete?')
},
trigger: () => {
return h(
@@ -89,7 +92,7 @@ const columns: any = [
type: 'error'
},
{
default: () => '删除'
default: () => $gettext('Delete')
}
)
}
@@ -113,14 +116,14 @@ const { loading, data, page, total, pageSize, pageCount, refresh } = usePaginati
const handleDelete = async (row: any) => {
useRequest(container.imageRemove(row.id)).onSuccess(() => {
refresh()
window.$message.success('删除成功')
window.$message.success($gettext('Delete successful'))
})
}
const handlePrune = () => {
useRequest(container.imagePrune()).onSuccess(() => {
refresh()
window.$message.success('清理成功')
window.$message.success($gettext('Cleanup successful'))
})
}
@@ -129,7 +132,7 @@ const handlePull = () => {
useRequest(container.imagePull(pullModel.value))
.onSuccess(() => {
refresh()
window.$message.success('拉取成功')
window.$message.success($gettext('Pull successful'))
})
.onComplete(() => {
loading.value = false
@@ -145,8 +148,8 @@ onMounted(() => {
<template>
<n-flex vertical :size="20">
<n-flex>
<n-button type="primary" @click="pullModal = true">拉取镜像</n-button>
<n-button type="primary" @click="handlePrune" ghost>清理镜像</n-button>
<n-button type="primary" @click="pullModal = true">{{ $gettext('Pull Image') }}</n-button>
<n-button type="primary" @click="handlePrune" ghost>{{ $gettext('Cleanup Images') }}</n-button>
</n-flex>
<n-data-table
striped
@@ -173,44 +176,44 @@ onMounted(() => {
<n-modal
v-model:show="pullModal"
preset="card"
title="拉取镜像"
:title="$gettext('Pull Image')"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
>
<n-form :model="pullModel">
<n-form-item path="name" label="镜像名">
<n-form-item path="name" :label="$gettext('Image Name')">
<n-input
v-model:value="pullModel.name"
type="text"
@keydown.enter.prevent
placeholder="docker.io/php:8.3-fpm"
:placeholder="$gettext('docker.io/php:8.3-fpm')"
/>
</n-form-item>
<n-form-item path="auth" label="验证">
<n-form-item path="auth" :label="$gettext('Authentication')">
<n-switch v-model:value="pullModel.auth" />
</n-form-item>
<n-form-item v-if="pullModel.auth" path="username" label="用户名">
<n-form-item v-if="pullModel.auth" path="username" :label="$gettext('Username')">
<n-input
v-model:value="pullModel.username"
type="text"
@keydown.enter.prevent
placeholder="输入用户名"
:placeholder="$gettext('Enter username')"
/>
</n-form-item>
<n-form-item v-if="pullModel.auth" path="password" label="密码">
<n-form-item v-if="pullModel.auth" path="password" :label="$gettext('Password')">
<n-input
v-model:value="pullModel.password"
type="password"
show-password-on="click"
@keydown.enter.prevent
placeholder="输入密码"
:placeholder="$gettext('Enter password')"
/>
</n-form-item>
</n-form>
<n-button type="info" block :loading="loading" :disabled="loading" @click="handlePull">
提交
{{ $gettext('Submit') }}
</n-button>
</n-modal>
</template>

View File

@@ -1,5 +1,6 @@
<script setup lang="ts">
import ComposeView from '@/views/container/ComposeView.vue'
import { useGettext } from 'vue3-gettext'
defineOptions({
name: 'container-index'
@@ -10,25 +11,26 @@ import ImageView from '@/views/container/ImageView.vue'
import NetworkView from '@/views/container/NetworkView.vue'
import VolumeView from '@/views/container/VolumeView.vue'
const { $gettext } = useGettext()
const current = ref('container')
</script>
<template>
<common-page show-footer>
<n-tabs v-model:value="current" type="line" animated>
<n-tab-pane name="container" tab="容器">
<n-tab-pane name="container" :tab="$gettext('Containers')">
<container-view />
</n-tab-pane>
<n-tab-pane name="compose" tab="编排">
<n-tab-pane name="compose" :tab="$gettext('Compose')">
<compose-view />
</n-tab-pane>
<n-tab-pane name="image" tab="镜像">
<n-tab-pane name="image" :tab="$gettext('Images')">
<image-view />
</n-tab-pane>
<n-tab-pane name="network" tab="网络">
<n-tab-pane name="network" :tab="$gettext('Networks')">
<network-view />
</n-tab-pane>
<n-tab-pane name="volume" tab="">
<n-tab-pane name="volume" :tab="$gettext('Volumes')">
<volume-view />
</n-tab-pane>
</n-tabs>

View File

@@ -1,9 +1,12 @@
<script setup lang="ts">
import { NButton, NDataTable, NFlex, NInput, NPopconfirm, NTag } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import container from '@/api/panel/container'
import { formatDateTime } from '@/utils'
const { $gettext } = useGettext()
const createModel = ref({
name: '',
driver: 'bridge',
@@ -39,28 +42,28 @@ const selectedRowKeys = ref<any>([])
const columns: any = [
{ type: 'selection', fixed: 'left' },
{
title: '名称',
title: $gettext('Name'),
key: 'name',
minWidth: 150,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: '驱动',
title: $gettext('Driver'),
key: 'driver',
width: 100,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: '范围',
title: $gettext('Scope'),
key: 'scope',
width: 100,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: '子网',
title: $gettext('Subnet'),
key: 'subnet',
minWidth: 150,
resizable: true,
@@ -77,7 +80,7 @@ const columns: any = [
}
},
{
title: '网关',
title: $gettext('Gateway'),
key: 'gateway',
width: 150,
resizable: true,
@@ -94,7 +97,7 @@ const columns: any = [
}
},
{
title: '创建时间',
title: $gettext('Creation Time'),
key: 'created_at',
width: 200,
resizable: true,
@@ -103,7 +106,7 @@ const columns: any = [
}
},
{
title: '操作',
title: $gettext('Actions'),
key: 'actions',
width: 120,
align: 'center',
@@ -119,7 +122,7 @@ const columns: any = [
},
{
default: () => {
return '确定删除吗?'
return $gettext('Are you sure you want to delete?')
},
trigger: () => {
return h(
@@ -129,7 +132,7 @@ const columns: any = [
type: 'error'
},
{
default: () => '删除'
default: () => $gettext('Delete')
}
)
}
@@ -153,14 +156,14 @@ const { loading, data, page, total, pageSize, pageCount, refresh } = usePaginati
const handleDelete = (row: any) => {
useRequest(container.networkRemove(row.id)).onSuccess(() => {
refresh()
window.$message.success('删除成功')
window.$message.success($gettext('Delete successful'))
})
}
const handlePrune = () => {
useRequest(container.networkPrune()).onSuccess(() => {
refresh()
window.$message.success('清理成功')
window.$message.success($gettext('Cleanup successful'))
})
}
@@ -169,7 +172,7 @@ const handleCreate = () => {
useRequest(container.networkCreate(createModel.value))
.onSuccess(() => {
refresh()
window.$message.success('创建成功')
window.$message.success($gettext('Created successfully'))
})
.onComplete(() => {
loading.value = false
@@ -185,8 +188,8 @@ onMounted(() => {
<template>
<n-flex vertical :size="20">
<n-flex>
<n-button type="primary" @click="createModal = true">创建网络</n-button>
<n-button type="primary" @click="handlePrune" ghost>清理网络</n-button>
<n-button type="primary" @click="createModal = true">{{ $gettext('Create Network') }}</n-button>
<n-button type="primary" @click="handlePrune" ghost>{{ $gettext('Cleanup Networks') }}</n-button>
</n-flex>
<n-data-table
striped
@@ -213,17 +216,17 @@ onMounted(() => {
<n-modal
v-model:show="createModal"
preset="card"
title="创建网络"
:title="$gettext('Create Network')"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
>
<n-form :model="createModel">
<n-form-item path="name" label="网络名">
<n-form-item path="name" :label="$gettext('Network Name')">
<n-input v-model:value="createModel.name" type="text" @keydown.enter.prevent />
</n-form-item>
<n-form-item path="driver" label="驱动">
<n-form-item path="driver" :label="$gettext('Driver')">
<n-select
:options="options"
v-model:value="createModel.driver"
@@ -235,76 +238,76 @@ onMounted(() => {
<n-form-item path="ipv4" label="IPV4">
<n-switch v-model:value="createModel.ipv4.enabled" />
</n-form-item>
<n-form-item v-if="createModel.ipv4.enabled" path="subnet" label="子网">
<n-form-item v-if="createModel.ipv4.enabled" path="subnet" :label="$gettext('Subnet')">
<n-input
v-model:value="createModel.ipv4.subnet"
type="text"
@keydown.enter.prevent
placeholder="172.16.10.0/24"
:placeholder="$gettext('172.16.10.0/24')"
/>
</n-form-item>
<n-form-item v-if="createModel.ipv4.enabled" path="gateway" label="网关">
<n-form-item v-if="createModel.ipv4.enabled" path="gateway" :label="$gettext('Gateway')">
<n-input
v-model:value="createModel.ipv4.gateway"
type="text"
@keydown.enter.prevent
placeholder="172.16.10.254"
:placeholder="$gettext('172.16.10.254')"
/>
</n-form-item>
<n-form-item v-if="createModel.ipv4.enabled" path="ip_range" label="IP范围">
<n-form-item v-if="createModel.ipv4.enabled" path="ip_range" :label="$gettext('IP Range')">
<n-input
v-model:value="createModel.ipv4.ip_range"
type="text"
@keydown.enter.prevent
placeholder="172.16.10.0/24"
:placeholder="$gettext('172.16.10.0/24')"
/>
</n-form-item>
<n-form-item path="ipv6" label="IPV6">
<n-switch v-model:value="createModel.ipv6.enabled" />
</n-form-item>
<n-form-item v-if="createModel.ipv6.enabled" path="subnet" label="子网">
<n-form-item v-if="createModel.ipv6.enabled" path="subnet" :label="$gettext('Subnet')">
<n-input
v-model:value="createModel.ipv6.subnet"
type="text"
@keydown.enter.prevent
placeholder="2408:400e::/48"
:placeholder="$gettext('2408:400e::/48')"
/>
</n-form-item>
<n-form-item v-if="createModel.ipv6.enabled" path="gateway" label="网关">
<n-form-item v-if="createModel.ipv6.enabled" path="gateway" :label="$gettext('Gateway')">
<n-input
v-model:value="createModel.ipv6.gateway"
type="text"
@keydown.enter.prevent
placeholder="2408:400e::1"
:placeholder="$gettext('2408:400e::1')"
/>
</n-form-item>
<n-form-item v-if="createModel.ipv6.enabled" path="ip_range" label="IP范围">
<n-form-item v-if="createModel.ipv6.enabled" path="ip_range" :label="$gettext('IP Range')">
<n-input
v-model:value="createModel.ipv6.ip_range"
type="text"
@keydown.enter.prevent
placeholder="2408:400e::/64"
:placeholder="$gettext('2408:400e::/64')"
/>
</n-form-item>
<n-form-item path="env" label="标签">
<n-form-item path="env" :label="$gettext('Labels')">
<n-dynamic-input
v-model:value="createModel.labels"
preset="pair"
key-placeholder="标签名"
value-placeholder="标签值"
:key-placeholder="$gettext('Label Name')"
:value-placeholder="$gettext('Label Value')"
/>
</n-form-item>
<n-form-item path="env" label="选项">
<n-form-item path="env" :label="$gettext('Options')">
<n-dynamic-input
v-model:value="createModel.options"
preset="pair"
key-placeholder="选项名"
value-placeholder="选项值"
:key-placeholder="$gettext('Option Name')"
:value-placeholder="$gettext('Option Value')"
/>
</n-form-item>
</n-form>
<n-button type="info" block :loading="loading" :disabled="loading" @click="handleCreate">
提交
{{ $gettext('Submit') }}
</n-button>
</n-modal>
</template>

View File

@@ -1,9 +1,12 @@
<script setup lang="ts">
import { NButton, NDataTable, NInput, NPopconfirm } from 'naive-ui'
import { useGettext } from 'vue3-gettext'
import container from '@/api/panel/container'
import { formatDateTime } from '@/utils'
const { $gettext } = useGettext()
const createModel = ref({
name: '',
driver: 'local',
@@ -20,35 +23,35 @@ const selectedRowKeys = ref<any>([])
const columns: any = [
{ type: 'selection', fixed: 'left' },
{
title: '名称',
title: $gettext('Name'),
key: 'name',
minWidth: 150,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: '驱动',
title: $gettext('Driver'),
key: 'driver',
width: 100,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: '范围',
title: $gettext('Scope'),
key: 'scope',
width: 100,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: '挂载点',
title: $gettext('Mount Point'),
key: 'mount_point',
resizable: true,
minWidth: 150,
ellipsis: { tooltip: true }
},
{
title: '创建时间',
title: $gettext('Creation Time'),
key: 'created_at',
width: 200,
resizable: true,
@@ -57,7 +60,7 @@ const columns: any = [
}
},
{
title: '操作',
title: $gettext('Actions'),
key: 'actions',
width: 120,
align: 'center',
@@ -73,7 +76,7 @@ const columns: any = [
},
{
default: () => {
return '确定删除吗?'
return $gettext('Are you sure you want to delete?')
},
trigger: () => {
return h(
@@ -83,7 +86,7 @@ const columns: any = [
type: 'error'
},
{
default: () => '删除'
default: () => $gettext('Delete')
}
)
}
@@ -107,14 +110,14 @@ const { loading, data, page, total, pageSize, pageCount, refresh } = usePaginati
const handleDelete = async (row: any) => {
useRequest(container.volumeRemove(row.id)).onSuccess(() => {
refresh()
window.$message.success('删除成功')
window.$message.success($gettext('Delete successful'))
})
}
const handlePrune = () => {
useRequest(container.volumePrune()).onSuccess(() => {
refresh()
window.$message.success('清理成功')
window.$message.success($gettext('Cleanup successful'))
})
}
@@ -123,7 +126,7 @@ const handleCreate = () => {
useRequest(container.volumeCreate(createModel.value))
.onSuccess(() => {
refresh()
window.$message.success('创建成功')
window.$message.success($gettext('Created successfully'))
})
.onComplete(() => {
loading.value = false
@@ -139,8 +142,8 @@ onMounted(() => {
<template>
<n-flex vertical :size="20">
<n-flex>
<n-button type="primary" @click="createModal = true">创建卷</n-button>
<n-button type="primary" @click="handlePrune" ghost>清理卷</n-button>
<n-button type="primary" @click="createModal = true">{{ $gettext('Create Volume') }}</n-button>
<n-button type="primary" @click="handlePrune" ghost>{{ $gettext('Cleanup Volumes') }}</n-button>
</n-flex>
<n-data-table
striped
@@ -167,17 +170,17 @@ onMounted(() => {
<n-modal
v-model:show="createModal"
preset="card"
title="创建卷"
:title="$gettext('Create Volume')"
style="width: 60vw"
size="huge"
:bordered="false"
:segmented="false"
>
<n-form :model="createModel">
<n-form-item path="name" label="卷名">
<n-form-item path="name" :label="$gettext('Volume Name')">
<n-input v-model:value="createModel.name" type="text" @keydown.enter.prevent />
</n-form-item>
<n-form-item path="driver" label="驱动">
<n-form-item path="driver" :label="$gettext('Driver')">
<n-select
:options="options"
v-model:value="createModel.driver"
@@ -186,25 +189,25 @@ onMounted(() => {
>
</n-select>
</n-form-item>
<n-form-item path="env" label="标签">
<n-form-item path="env" :label="$gettext('Labels')">
<n-dynamic-input
v-model:value="createModel.labels"
preset="pair"
key-placeholder="标签名"
value-placeholder="标签值"
:key-placeholder="$gettext('Label Name')"
:value-placeholder="$gettext('Label Value')"
/>
</n-form-item>
<n-form-item path="env" label="选项">
<n-form-item path="env" :label="$gettext('Options')">
<n-dynamic-input
v-model:value="createModel.options"
preset="pair"
key-placeholder="选项名"
value-placeholder="选项值"
:key-placeholder="$gettext('Option Name')"
:value-placeholder="$gettext('Option Value')"
/>
</n-form-item>
</n-form>
<n-button type="info" block :loading="loading" :disabled="loading" @click="handleCreate">
提交
{{ $gettext('Submit') }}
</n-button>
</n-modal>
</template>

View File

@@ -1,3 +1,4 @@
import { $gettext } from '@/utils/gettext'
import type { RouteType } from '~/types/router'
const Layout = () => import('@/layout/IndexView.vue')
@@ -15,7 +16,7 @@ export default {
path: '',
component: () => import('./IndexView.vue'),
meta: {
title: 'containerIndex.title',
title: $gettext('Container'),
icon: 'mdi:layers-outline',
role: ['admin'],
requireAuth: true

View File

@@ -14,7 +14,7 @@ import {
import { use } from 'echarts/core'
import { CanvasRenderer } from 'echarts/renderers'
import { NButton, NPopconfirm } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { useGettext } from 'vue3-gettext'
import dashboard from '@/api/panel/dashboard'
import { router } from '@/router'
@@ -34,7 +34,7 @@ use([
DataZoomComponent
])
const { locale } = useI18n()
const { current: locale, $gettext } = useGettext()
const tabStore = useTabStore()
const realtime = ref<Realtime | null>(null)
@@ -126,13 +126,13 @@ const statusColor = (percentage: number) => {
const statusText = (percentage: number) => {
if (percentage >= 90) {
return '运行堵塞'
return $gettext('Running blocked')
} else if (percentage >= 80) {
return '运行缓慢'
return $gettext('Running slowly')
} else if (percentage >= 70) {
return '运行正常'
return $gettext('Running normally')
}
return '运行流畅'
return $gettext('Running smoothly')
}
const chartOptions = computed(() => {
@@ -318,9 +318,9 @@ const fetchCurrent = () => {
const handleRestartPanel = () => {
clearInterval(homeInterval)
window.$message.loading('面板重启中...')
window.$message.loading($gettext('Panel restarting...'))
useRequest(dashboard.restart()).onSuccess(() => {
window.$message.success('面板重启成功')
window.$message.success($gettext('Panel restarted successfully'))
setTimeout(() => {
tabStore.reloadTab(tabStore.active)
}, 3000)
@@ -332,7 +332,7 @@ const handleUpdate = () => {
if (data.update) {
router.push({ name: 'dashboard-update' })
} else {
window.$message.success('当前已是最新版本')
window.$message.success($gettext('Current version is the latest'))
}
})
}
@@ -376,7 +376,14 @@ const clearCurrent = () => {
}
const quantifier = computed(() => {
return locale.value === 'en' ? '' : ' 个'
switch (locale) {
case 'zh_CN':
return '个'
case 'zh_TW':
return '個'
default:
return ''
}
})
let homeInterval: any = null
@@ -408,40 +415,48 @@ if (import.meta.hot) {
<n-page-header :subtitle="systemInfo?.panel_version">
<n-grid :cols="4" pb-10>
<n-gi>
<n-statistic label="网站" :value="countInfo.website + quantifier" />
<n-statistic :label="$gettext('Website')" :value="countInfo.website + quantifier" />
</n-gi>
<n-gi>
<n-statistic label="数据库" :value="countInfo.database + quantifier" />
<n-statistic
:label="$gettext('Database')"
:value="countInfo.database + quantifier"
/>
</n-gi>
<n-gi>
<n-statistic label="FTP" :value="countInfo.ftp + quantifier" />
</n-gi>
<n-gi>
<n-statistic label="计划任务" :value="countInfo.cron + quantifier" />
<n-statistic
:label="$gettext('Scheduled Tasks')"
:value="countInfo.cron + quantifier"
/>
</n-gi>
</n-grid>
<template #title>耗子面板</template>
<template #title>{{ $gettext('Rat Panel') }}</template>
<template #extra>
<n-flex>
<n-button type="primary" @click="toSponsor"> 赞助支持 </n-button>
<n-button type="primary" @click="toSponsor">
{{ $gettext('Sponsor Support') }}
</n-button>
<n-popconfirm @positive-click="handleRestartPanel">
<template #trigger>
<n-button type="warning"> 重启 </n-button>
<n-button type="warning"> {{ $gettext('Restart') }} </n-button>
</template>
确定要重启面板吗
{{ $gettext('Are you sure you want to restart the panel?') }}
</n-popconfirm>
<n-button type="success" @click="handleUpdate"> 更新 </n-button>
<n-button type="success" @click="handleUpdate"> {{ $gettext('Update') }} </n-button>
</n-flex>
</template>
</n-page-header>
</n-card>
<n-card :segmented="true" size="small" title="资源总览">
<n-card :segmented="true" size="small" :title="$gettext('Resource Overview')">
<n-flex v-if="realtime" size="large">
<n-popover placement="bottom" trigger="hover">
<template #trigger>
<n-flex vertical flex items-center p-20 pl-40 pr-40>
<p>负载状态</p>
<p>{{ $gettext('Load Status') }}</p>
<n-progress
type="dashboard"
:percentage="Math.round(formatPercent((realtime.load.load1 / cores) * 100))"
@@ -453,21 +468,21 @@ if (import.meta.hot) {
</template>
<n-table :single-line="false" striped>
<tr>
<th>最近 1 分钟</th>
<th>{{ $gettext('Last 1 minute') }}</th>
<td>
{{ formatPercent((realtime.load.load1 / cores) * 100) }}% /
{{ realtime.load.load1 }}
</td>
</tr>
<tr>
<th>最近 5 分钟</th>
<th>{{ $gettext('Last 5 minutes') }}</th>
<td>
{{ formatPercent((realtime.load.load5 / cores) * 100) }}% /
{{ realtime.load.load5 }}
</td>
</tr>
<tr>
<th>最近 15 分钟</th>
<th>{{ $gettext('Last 15 minutes') }}</th>
<td>
{{ formatPercent((realtime.load.load15 / cores) * 100) }}% /
{{ realtime.load.load15 }}
@@ -485,25 +500,26 @@ if (import.meta.hot) {
:color="statusColor(realtime.percent)"
>
</n-progress>
<p>{{ cores }} 核心</p>
<p>{{ cores }} {{ $gettext('cores') }}</p>
</n-flex>
</template>
<n-table :single-line="false" striped>
<tr>
<th>型号</th>
<th>{{ $gettext('Model') }}</th>
<td>{{ realtime.cpus[0].modelName }}</td>
</tr>
<tr>
<th>参数</th>
<th>{{ $gettext('Parameters') }}</th>
<td>
{{ realtime.cpus.length }} CPU {{ cores }} 核心
{{ formatBytes(realtime.cpus[0].cacheSize * 1024) }} 缓存
{{ realtime.cpus.length }} CPU {{ cores }} {{ $gettext('cores') }}
{{ formatBytes(realtime.cpus[0].cacheSize * 1024) }} {{ $gettext('cache') }}
</td>
</tr>
<tr v-for="item in realtime.cpus" :key="item.modelName">
<th>CPU-{{ item.cpu }}</th>
<td>
使用率 {{ formatPercent(realtime.percents[item.cpu]) }}% 频率 {{ item.mhz }} MHz
{{ $gettext('Usage') }} {{ formatPercent(realtime.percents[item.cpu]) }}%
{{ $gettext('Frequency') }} {{ item.mhz }} MHz
</td>
</tr>
</n-table>
@@ -511,7 +527,7 @@ if (import.meta.hot) {
<n-popover placement="bottom" trigger="hover">
<template #trigger>
<n-flex vertical flex items-center p-20 pl-40 pr-40>
<p>内存</p>
<p>{{ $gettext('Memory') }}</p>
<n-progress
type="dashboard"
:percentage="realtime.mem.usedPercent"
@@ -523,73 +539,73 @@ if (import.meta.hot) {
</template>
<n-table :single-line="false" striped>
<tr>
<th>活跃</th>
<th>{{ $gettext('Active') }}</th>
<td>
{{ formatBytes(realtime.mem.active) }}
</td>
</tr>
<tr>
<th>不活跃</th>
<th>{{ $gettext('Inactive') }}</th>
<td>
{{ formatBytes(realtime.mem.inactive) }}
</td>
</tr>
<tr>
<th>空闲</th>
<th>{{ $gettext('Free') }}</th>
<td>
{{ formatBytes(realtime.mem.free) }}
</td>
</tr>
<tr>
<th>共享</th>
<th>{{ $gettext('Shared') }}</th>
<td>
{{ formatBytes(realtime.mem.shared) }}
</td>
</tr>
<tr>
<th>已提交</th>
<th>{{ $gettext('Committed') }}</th>
<td>
{{ formatBytes(realtime.mem.committedas) }}
</td>
</tr>
<tr>
<th>提交限制</th>
<th>{{ $gettext('Commit Limit') }}</th>
<td>
{{ formatBytes(realtime.mem.commitlimit) }}
</td>
</tr>
<tr>
<th>SWAP大小</th>
<th>{{ $gettext('SWAP Size') }}</th>
<td>
{{ formatBytes(realtime.mem.swaptotal) }}
</td>
</tr>
<tr>
<th>SWAP已用</th>
<th>{{ $gettext('SWAP Used') }}</th>
<td>
{{ formatBytes(realtime.mem.swapcached) }}
</td>
</tr>
<tr>
<th>SWAP可用</th>
<th>{{ $gettext('SWAP Available') }}</th>
<td>
{{ formatBytes(realtime.mem.swapfree) }}
</td>
</tr>
<tr>
<th>物理内存大小</th>
<th>{{ $gettext('Physical Memory Size') }}</th>
<td>
{{ formatBytes(realtime.mem.total) }}
</td>
</tr>
<tr>
<th>物理内存已用</th>
<th>{{ $gettext('Physical Memory Used') }}</th>
<td>
{{ formatBytes(realtime.mem.used) }}
</td>
</tr>
<tr>
<th>物理内存可用</th>
<th>{{ $gettext('Physical Memory Available') }}</th>
<td>
{{ formatBytes(realtime.mem.available) }}
</td>
@@ -622,27 +638,27 @@ if (import.meta.hot) {
</template>
<n-table :single-line="false">
<tr>
<th>挂载点</th>
<th>{{ $gettext('Mount Point') }}</th>
<td>{{ item.path }}</td>
</tr>
<tr>
<th>文件系统</th>
<th>{{ $gettext('File System') }}</th>
<td>{{ item.fstype }}</td>
</tr>
<tr>
<th>Inodes 使用率</th>
<th>{{ $gettext('Inodes Usage') }}</th>
<td>{{ formatPercent(item.inodesUsedPercent) }}%</td>
</tr>
<tr>
<th>Inodes 总数</th>
<th>{{ $gettext('Inodes Total') }}</th>
<td>{{ item.inodesTotal }}</td>
</tr>
<tr>
<th>Inodes 已用</th>
<th>{{ $gettext('Inodes Used') }}</th>
<td>{{ item.inodesUsed }}</td>
</tr>
<tr>
<th>Inodes 可用</th>
<th>{{ $gettext('Inodes Available') }}</th>
<td>{{ item.inodesFree }}</td>
</tr>
</n-table>
@@ -659,7 +675,7 @@ if (import.meta.hot) {
>
<n-gi>
<n-flex vertical>
<n-card :segmented="true" size="small" title="快捷应用" min-h-340>
<n-card :segmented="true" size="small" :title="$gettext('Quick Apps')" min-h-340>
<n-scrollbar max-h-270>
<n-grid
v-if="!homeAppsLoading"
@@ -702,57 +718,60 @@ if (import.meta.hot) {
</n-grid>
</n-scrollbar>
<n-text v-if="!homeAppsLoading && !homeApps.length">
您还没有设置任何应用在此显示
{{ $gettext('You have not set any apps to display here!') }}
</n-text>
<n-skeleton v-if="homeAppsLoading" text :repeat="12" />
</n-card>
<n-card :segmented="true" size="small" title="环境信息">
<n-card :segmented="true" size="small" :title="$gettext('Environment Information')">
<n-table v-if="systemInfo" :single-line="false">
<tr>
<th>系统主机名</th>
<th>{{ $gettext('System Hostname') }}</th>
<td>
{{ systemInfo?.hostname || '加载中...' }}
{{ systemInfo?.hostname || $gettext('Loading...') }}
</td>
</tr>
<tr>
<th>系统版本号</th>
<th>{{ $gettext('System Version') }}</th>
<td>
{{ `${systemInfo?.os_name} ${systemInfo?.kernel_arch}` || '加载中...' }}
{{
`${systemInfo?.os_name} ${systemInfo?.kernel_arch}` ||
$gettext('Loading...')
}}
</td>
</tr>
<tr>
<th>系统内核版本</th>
<th>{{ $gettext('System Kernel Version') }}</th>
<td>
{{ systemInfo?.kernel_version || '加载中...' }}
{{ systemInfo?.kernel_version || $gettext('Loading...') }}
</td>
</tr>
<tr>
<th>系统运行时间</th>
<th>{{ $gettext('System Uptime') }}</th>
<td>
{{ formatDuration(Number(systemInfo?.uptime)) || '加载中...' }}
{{ formatDuration(Number(systemInfo?.uptime)) || $gettext('Loading...') }}
</td>
</tr>
<tr>
<th>面板内部版本</th>
<th>{{ $gettext('Panel Internal Version') }}</th>
<td>
{{
systemInfo?.commit_hash +
' ' +
systemInfo?.go_version +
' ' +
systemInfo?.build_time || '加载中...'
systemInfo?.build_time || $gettext('Loading...')
}}
</td>
</tr>
<tr>
<th>面板编译信息</th>
<th>{{ $gettext('Panel Compile Information') }}</th>
<td>
{{
systemInfo?.build_id +
' ' +
systemInfo?.build_user +
'/' +
systemInfo?.build_host || '加载中...'
systemInfo?.build_host || $gettext('Loading...')
}}
</td>
</tr>
@@ -762,7 +781,7 @@ if (import.meta.hot) {
</n-flex>
</n-gi>
<n-gi>
<n-card :segmented="true" size="small" title="实时监控">
<n-card :segmented="true" size="small" :title="$gettext('Real-time Monitoring')">
<n-flex vertical v-if="systemInfo">
<n-form
inline
@@ -772,11 +791,11 @@ if (import.meta.hot) {
>
<n-form-item>
<n-radio-group v-model:value="chartType">
<n-radio-button value="net" label="网络" />
<n-radio-button value="disk" label="硬盘" />
<n-radio-button value="net" :label="$gettext('Network')" />
<n-radio-button value="disk" :label="$gettext('Disk')" />
</n-radio-group>
</n-form-item>
<n-form-item label="单位" ml-auto>
<n-form-item :label="$gettext('Unit')" ml-auto>
<n-select
v-model:value="unitType"
:options="units"
@@ -784,7 +803,7 @@ if (import.meta.hot) {
w-80
></n-select>
</n-form-item>
<n-form-item v-if="chartType == 'net'" label="网卡">
<n-form-item v-if="chartType == 'net'" :label="$gettext('Network Card')">
<n-select
multiple
v-model:value="nets"
@@ -793,7 +812,7 @@ if (import.meta.hot) {
w-200
></n-select>
</n-form-item>
<n-form-item v-if="chartType == 'disk'" label="硬盘">
<n-form-item v-if="chartType == 'disk'" :label="$gettext('Disk')">
<n-select
multiple
v-model:value="disks"
@@ -804,16 +823,26 @@ if (import.meta.hot) {
</n-form-item>
</n-form>
<n-flex v-if="chartType == 'net'">
<n-tag>总发送 {{ formatBytes(total.netBytesSent) }}</n-tag>
<n-tag>总接收 {{ formatBytes(total.netBytesRecv) }}</n-tag>
<n-tag>实时发送 {{ formatBytes(current.netBytesSent) }}/s</n-tag>
<n-tag>实时接收 {{ formatBytes(current.netBytesRecv) }}/s</n-tag>
<n-tag>{{ $gettext('Total Sent') }} {{ formatBytes(total.netBytesSent) }} </n-tag>
<n-tag>
{{ $gettext('Total Received') }} {{ formatBytes(total.netBytesRecv) }}
</n-tag>
<n-tag>
{{ $gettext('Real-time Sent') }}
{{ formatBytes(current.netBytesSent) }}/s
</n-tag>
<n-tag
>{{ $gettext('Real-time Received') }} {{ formatBytes(current.netBytesRecv) }}/s
</n-tag>
</n-flex>
<n-flex v-if="chartType == 'disk'">
<n-tag>读取 {{ formatBytes(total.diskReadBytes) }}</n-tag>
<n-tag>写入 {{ formatBytes(total.diskWriteBytes) }}</n-tag>
<n-tag>实时读写 {{ formatBytes(current.diskRWBytes) }}/s</n-tag>
<n-tag>读写延迟 {{ current.diskRWTime }}ms</n-tag>
<n-tag>{{ $gettext('Read') }} {{ formatBytes(total.diskReadBytes) }}</n-tag>
<n-tag>{{ $gettext('Write') }} {{ formatBytes(total.diskWriteBytes) }}</n-tag>
<n-tag
>{{ $gettext('Real-time Read/Write') }}
{{ formatBytes(current.diskRWBytes) }}/s</n-tag
>
<n-tag>{{ $gettext('Read/Write Latency') }} {{ current.diskRWTime }}ms</n-tag>
</n-flex>
<n-card :bordered="false" h-530 pt-10>
<v-chart class="chart" :option="chartOptions" autoresize />

View File

@@ -1,3 +1,4 @@
import { $gettext } from '@/utils/gettext'
import type { RouteType } from '~/types/router'
const Layout = () => import('@/layout/IndexView.vue')
@@ -16,7 +17,7 @@ export default {
path: 'dashboard',
component: () => import('./IndexView.vue'),
meta: {
title: '仪表盘',
title: $gettext('Dashboard'),
icon: 'mdi:gauge',
role: ['admin'],
requireAuth: true
@@ -28,7 +29,7 @@ export default {
component: () => import('./UpdateView.vue'),
isHidden: true,
meta: {
title: 'homeUpdate.title',
title: $gettext('Update'),
icon: 'mdi:archive-arrow-up-outline',
role: ['admin'],
requireAuth: true

View File

@@ -5,7 +5,7 @@ defineOptions({
import Editor from '@guolao/vue-monaco-editor'
import { NButton, NCheckbox, NDataTable, NFlex, NInput, NPopconfirm, NSwitch, NTag } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { useGettext } from 'vue3-gettext'
import dashboard from '@/api/panel/dashboard'
import website from '@/api/panel/website'
@@ -13,21 +13,21 @@ import { useFileStore } from '@/store'
import { generateRandomString, isNullOrUndef, renderIcon } from '@/utils'
const fileStore = useFileStore()
const { t } = useI18n()
const { $gettext } = useGettext()
const router = useRouter()
const selectedRowKeys = ref<any>([])
const columns: any = [
{ type: 'selection', fixed: 'left' },
{
title: t('websiteIndex.columns.name'),
title: $gettext('Website Name'),
key: 'name',
width: 200,
resizable: true,
ellipsis: { tooltip: true }
},
{
title: t('websiteIndex.columns.status'),
title: $gettext('Running'),
key: 'status',
width: 150,
align: 'center',
@@ -41,7 +41,7 @@ const columns: any = [
}
},
{
title: t('websiteIndex.columns.path'),
title: $gettext('Directory'),
key: 'path',
minWidth: 200,
resizable: true,
@@ -75,7 +75,7 @@ const columns: any = [
}
},
{
title: t('websiteIndex.columns.remark'),
title: $gettext('Remark'),
key: 'remark',
minWidth: 200,
resizable: true,
@@ -92,7 +92,7 @@ const columns: any = [
}
},
{
title: t('websiteIndex.columns.actions'),
title: $gettext('Actions'),
key: 'actions',
width: 220,
align: 'center',
@@ -108,7 +108,7 @@ const columns: any = [
onClick: () => handleEdit(row)
},
{
default: () => '修改',
default: () => $gettext('Edit'),
icon: renderIcon('material-symbols:edit-outline', { size: 14 })
}
),
@@ -127,14 +127,23 @@ const columns: any = [
},
{
default: () => [
h('strong', {}, { default: () => `确定删除网站 ${row.name} 吗?` }),
h(
'strong',
{},
{
default: () =>
$gettext('Are you sure you want to delete website %{ name }?', {
name: row.name
})
}
),
h(
NCheckbox,
{
checked: deleteModel.value.path,
onUpdateChecked: (v) => (deleteModel.value.path = v)
},
{ default: () => '删除网站目录' }
{ default: () => $gettext('Delete website directory') }
),
h(
NCheckbox,
@@ -142,7 +151,7 @@ const columns: any = [
checked: deleteModel.value.db,
onUpdateChecked: (v) => (deleteModel.value.db = v)
},
{ default: () => '删除本地同名数据库' }
{ default: () => $gettext('Delete local database with the same name') }
)
]
}
@@ -157,7 +166,7 @@ const columns: any = [
style: 'margin-left: 15px;'
},
{
default: () => '删除',
default: () => $gettext('Delete'),
icon: renderIcon('material-symbols:delete-outline', { size: 14 })
}
)
@@ -198,7 +207,7 @@ const { data: installedDbAndPhp } = useRequest(dashboard.installedDbAndPhp, {
initialData: {
php: [
{
label: '不使用',
label: $gettext('Not used'),
value: 0
}
],
@@ -227,7 +236,11 @@ const handleStatusChange = (row: any) => {
useRequest(website.status(row.id, !row.status)).onSuccess(() => {
row.status = !row.status
window.$message.success('已' + (row.status ? '启动' : '停止'))
window.$message.success(
$gettext('Already %{ status }', {
status: row.status ? $gettext('started') : $gettext('stopped')
})
)
})
}
@@ -237,7 +250,7 @@ const getDefaultPage = async () => {
const handleRemark = (row: any) => {
useRequest(website.updateRemark(row.id, row.remark)).onSuccess(() => {
window.$message.success('修改成功')
window.$message.success($gettext('Modified successfully'))
})
}
@@ -254,7 +267,7 @@ const handleDelete = (id: number) => {
useRequest(website.delete(id, deleteModel.value.path, deleteModel.value.db)).onSuccess(() => {
refresh()
deleteModel.value.path = true
window.$message.success('删除成功')
window.$message.success($gettext('Deleted successfully'))
})
}
@@ -263,7 +276,7 @@ const handleSaveDefaultPage = () => {
website.saveDefaultConfig(editDefaultPageModel.value.index, editDefaultPageModel.value.stop)
).onSuccess(() => {
editDefaultPageModal.value = false
window.$message.success('修改成功')
window.$message.success($gettext('Modified successfully'))
})
}
@@ -293,13 +306,13 @@ const handleCreate = async () => {
path: '',
remark: ''
}
window.$message.success('创建成功')
window.$message.success($gettext('Created successfully'))
})
}
const bulkDelete = async () => {
if (selectedRowKeys.value.length === 0) {
window.$message.info('请选择要删除的网站')
window.$message.info($gettext('Please select the websites to delete'))
return
}
@@ -308,7 +321,7 @@ const bulkDelete = async () => {
selectedRowKeys.value = []
refresh()
window.$message.success('删除成功')
window.$message.success($gettext('Deleted successfully'))
}
const formatDbValue = (value: string) => {
@@ -332,16 +345,20 @@ onMounted(() => {
<n-flex vertical :size="20">
<n-flex>
<n-button type="primary" @click="createModal = true">
{{ $t('websiteIndex.create.trigger') }}
{{ $gettext('Create Website') }}
</n-button>
<n-popconfirm @positive-click="bulkDelete">
<template #trigger>
<n-button type="error"> 批量删除 </n-button>
<n-button type="error"> {{ $gettext('Batch Delete') }} </n-button>
</template>
这会删除网站目录但不会删除同名数据库确定删除选中的网站吗
{{
$gettext(
'This will delete the website directory but not the database with the same name. Are you sure you want to delete the selected websites?'
)
}}
</n-popconfirm>
<n-button type="warning" @click="editDefaultPageModal = true">
{{ $t('websiteIndex.edit.trigger') }}
{{ $gettext('Modify Default Page') }}
</n-button>
</n-flex>
<n-data-table
@@ -369,7 +386,7 @@ onMounted(() => {
</common-page>
<n-modal
v-model:show="createModal"
:title="$t('websiteIndex.create.title')"
:title="$gettext('Create Website')"
preset="card"
style="width: 60vw"
size="huge"
@@ -378,17 +395,21 @@ onMounted(() => {
@close="createModal = false"
>
<n-form :model="createModel">
<n-form-item path="name" :label="$t('websiteIndex.create.fields.name.label')">
<n-form-item path="name" :label="$gettext('Website Name')">
<n-input
v-model:value="createModel.name"
type="text"
@keydown.enter.prevent
:placeholder="$t('websiteIndex.create.fields.name.placeholder')"
:placeholder="
$gettext(
'Recommended to use English for the website name, it cannot be modified after setting'
)
"
/>
</n-form-item>
<n-row :gutter="[0, 24]">
<n-col :span="11">
<n-form-item :label="$t('websiteIndex.create.fields.domains.label')">
<n-form-item :label="$gettext('Domain')">
<n-dynamic-input
v-model:value="createModel.domains"
placeholder="example.com"
@@ -399,7 +420,7 @@ onMounted(() => {
</n-col>
<n-col :span="2"></n-col>
<n-col :span="11">
<n-form-item :label="$t('websiteIndex.create.fields.port.label')">
<n-form-item :label="$gettext('Port')">
<n-dynamic-input
v-model:value="createModel.listens"
placeholder="80"
@@ -411,11 +432,11 @@ onMounted(() => {
</n-row>
<n-row :gutter="[0, 24]">
<n-col :span="11">
<n-form-item path="php" :label="$t('websiteIndex.create.fields.phpVersion.label')">
<n-form-item path="php" :label="$gettext('PHP Version')">
<n-select
v-model:value="createModel.php"
:options="installedDbAndPhp.php"
:placeholder="$t('websiteIndex.create.fields.phpVersion.placeholder')"
:placeholder="$gettext('Select PHP Version')"
@keydown.enter.prevent
>
</n-select>
@@ -423,11 +444,11 @@ onMounted(() => {
</n-col>
<n-col :span="2"></n-col>
<n-col :span="11">
<n-form-item path="db" :label="$t('websiteIndex.create.fields.db.label')">
<n-form-item path="db" :label="$gettext('Database')">
<n-select
v-model:value="createModel.db_type"
:options="installedDbAndPhp.db"
:placeholder="$t('websiteIndex.create.fields.db.placeholder')"
:placeholder="$gettext('Select Database')"
@keydown.enter.prevent
@update:value="
() => {
@@ -444,31 +465,23 @@ onMounted(() => {
</n-row>
<n-row :gutter="[0, 24]">
<n-col :span="7">
<n-form-item
v-if="createModel.db"
path="db_name"
:label="$t('websiteIndex.create.fields.dbName.label')"
>
<n-form-item v-if="createModel.db" path="db_name" :label="$gettext('Database Name')">
<n-input
v-model:value="createModel.db_name"
type="text"
@keydown.enter.prevent
:placeholder="$t('websiteIndex.create.fields.dbName.placeholder')"
:placeholder="$gettext('Database Name')"
/>
</n-form-item>
</n-col>
<n-col :span="1"></n-col>
<n-col :span="7">
<n-form-item
v-if="createModel.db"
path="db_user"
:label="$t('websiteIndex.create.fields.dbUser.label')"
>
<n-form-item v-if="createModel.db" path="db_user" :label="$gettext('Database User')">
<n-input
v-model:value="createModel.db_user"
type="text"
@keydown.enter.prevent
:placeholder="$t('websiteIndex.create.fields.dbUser.placeholder')"
:placeholder="$gettext('Database User')"
/>
</n-form-item>
</n-col>
@@ -477,42 +490,46 @@ onMounted(() => {
<n-form-item
v-if="createModel.db"
path="db_password"
:label="$t('websiteIndex.create.fields.dbPassword.label')"
:label="$gettext('Database Password')"
>
<n-input
v-model:value="createModel.db_password"
type="text"
@keydown.enter.prevent
:placeholder="$t('websiteIndex.create.fields.dbPassword.placeholder')"
:placeholder="$gettext('Database Password')"
/>
</n-form-item>
</n-col>
</n-row>
<n-form-item path="path" :label="$t('websiteIndex.create.fields.path.label')">
<n-form-item path="path" :label="$gettext('Directory')">
<n-input
v-model:value="createModel.path"
type="text"
@keydown.enter.prevent
:placeholder="$t('websiteIndex.create.fields.path.placeholder')"
:placeholder="
$gettext(
'Website root directory (if left empty, defaults to website directory/website name)'
)
"
/>
</n-form-item>
<n-form-item path="remark" :label="$t('websiteIndex.create.fields.remark.label')">
<n-form-item path="remark" :label="$gettext('Remark')">
<n-input
v-model:value="createModel.remark"
type="textarea"
@keydown.enter.prevent
:placeholder="$t('websiteIndex.create.fields.remark.placeholder')"
:placeholder="$gettext('Remark')"
/>
</n-form-item>
</n-form>
<n-button type="info" block @click="handleCreate">
{{ $t('websiteIndex.create.actions.submit') }}
{{ $gettext('Create') }}
</n-button>
</n-modal>
<n-modal
v-model:show="editDefaultPageModal"
preset="card"
title="修改默认页"
:title="$gettext('Modify Default Page')"
style="width: 80vw"
size="huge"
:bordered="false"
@@ -520,7 +537,7 @@ onMounted(() => {
@close="handleSaveDefaultPage"
>
<n-tabs type="line" animated>
<n-tab-pane name="index" tab="默认页">
<n-tab-pane :name="$gettext('Default Page')" :tab="$gettext('Default Page')">
<Editor
v-model:value="editDefaultPageModel.index"
language="html"
@@ -534,7 +551,7 @@ onMounted(() => {
}"
/>
</n-tab-pane>
<n-tab-pane name="stop" tab="停止页">
<n-tab-pane :name="$gettext('Stop Page')" :tab="$gettext('Stop Page')">
<Editor
v-model:value="editDefaultPageModel.stop"
language="html"