2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 09:13:49 +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

@@ -53,7 +53,6 @@
"remove": "^0.1.5",
"vue": "^3.5.13",
"vue-echarts": "^7.0.3",
"vue-i18n": "^11.0.1",
"vue-router": "^4.5.0",
"vue3-gettext": "4.0.0-alpha.8"
},

40
web/pnpm-lock.yaml generated
View File

@@ -92,9 +92,6 @@ importers:
vue-echarts:
specifier: ^7.0.3
version: 7.0.3(@vue/runtime-core@3.5.13)(echarts@5.6.0)(vue@3.5.13(typescript@5.8.3))
vue-i18n:
specifier: ^11.0.1
version: 11.1.3(vue@3.5.13(typescript@5.8.3))
vue-router:
specifier: ^4.5.0
version: 4.5.0(vue@3.5.13(typescript@5.8.3))
@@ -709,18 +706,6 @@ packages:
peerDependencies:
vue: '>=3'
'@intlify/core-base@11.1.3':
resolution: {integrity: sha512-cMuHunYO7LE80azTitcvEbs1KJmtd6g7I5pxlApV3Jo547zdO3h31/0uXpqHc+Y3RKt1wo2y68RGSx77Z1klyA==}
engines: {node: '>= 16'}
'@intlify/message-compiler@11.1.3':
resolution: {integrity: sha512-7rbqqpo2f5+tIcwZTAG/Ooy9C8NDVwfDkvSeDPWUPQW+Dyzfw2o9H103N5lKBxO7wxX9dgCDjQ8Umz73uYw3hw==}
engines: {node: '>= 16'}
'@intlify/shared@11.1.3':
resolution: {integrity: sha512-pTFBgqa/99JRA2H1qfyqv97MKWJrYngXBA/I0elZcYxvJgcCw3mApAoPW3mJ7vx3j+Ti0FyKUFZ4hWxdjKaxvA==}
engines: {node: '>= 16'}
'@isaacs/cliui@8.0.2':
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
engines: {node: '>=12'}
@@ -3450,12 +3435,6 @@ packages:
peerDependencies:
vue: ^3.4.37
vue-i18n@11.1.3:
resolution: {integrity: sha512-Pcylh9z9S5+CJAqgbRZ3EKxFIBIrtY5YUppU722GIT65+Nukm0TCqiQegZnNLCZkXGthxe0cpqj0AoM51H+6Gw==}
engines: {node: '>= 16'}
peerDependencies:
vue: ^3.0.0
vue-router@4.5.0:
resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==}
peerDependencies:
@@ -4185,18 +4164,6 @@ snapshots:
'@iconify/types': 2.0.0
vue: 3.5.13(typescript@5.8.3)
'@intlify/core-base@11.1.3':
dependencies:
'@intlify/message-compiler': 11.1.3
'@intlify/shared': 11.1.3
'@intlify/message-compiler@11.1.3':
dependencies:
'@intlify/shared': 11.1.3
source-map-js: 1.2.1
'@intlify/shared@11.1.3': {}
'@isaacs/cliui@8.0.2':
dependencies:
string-width: 5.1.2
@@ -7255,13 +7222,6 @@ snapshots:
dependencies:
vue: 3.5.13(typescript@5.8.3)
vue-i18n@11.1.3(vue@3.5.13(typescript@5.8.3)):
dependencies:
'@intlify/core-base': 11.1.3
'@intlify/shared': 11.1.3
'@vue/devtools-api': 6.6.4
vue: 3.5.13(typescript@5.8.3)
vue-router@4.5.0(vue@3.5.13(typescript@5.8.3)):
dependencies:
'@vue/devtools-api': 6.6.4

View File

@@ -1,8 +1,4 @@
<script lang="ts" setup>
import { useI18n } from 'vue-i18n'
const { t } = useI18n()
interface Props {
showFooter?: boolean
showHeader?: boolean
@@ -26,7 +22,7 @@ const route = useRoute()
<div flex items-center>
<slot v-if="$slots['title-prefix']" name="title-prefix" />
<div mr-12 h-16 w-4 rounded-l-2 bg-primary></div>
<h2 font-normal>{{ t(title || String(route.meta.title)) }}</h2>
<h2 font-normal>{{ title ? title : route.meta.title ? route.meta.title : '' }}</h2>
<slot name="title-suffix" />
</div>
<slot name="action" />

View File

@@ -1,218 +0,0 @@
{
"name": "Rat Panel",
"certIndex": {
"title": "Certificates"
},
"containerIndex": {
"title": "Containers"
},
"cronIndex": {
"title": "Crond"
},
"fileIndex": {
"title": "Files"
},
"homeUpdate": {
"title": "Update panel",
"loading": "Loading update information, please wait a moment",
"alerts": {
"success": "Panel update successful",
"info": "The panel is already the latest version"
},
"button": {
"update": "Update"
},
"confirm": {
"update": {
"title": "Update panel",
"content": "Are you sure you want to update the panel?",
"positiveText": "Update",
"negativeText": "Cancel",
"loading": "Panel is being updated..."
}
}
},
"monitorIndex": {
"title": "Monitoring"
},
"appIndex": {
"title": "Apps",
"alerts": {
"cache": "Cache updated successfully",
"warning": "It is strongly recommended to take a backup/snapshot before upgrading the app to avoid being unable to roll back if problems arise!",
"setup": "Setup successful",
"install": "The task has been submitted, please check the task progress later",
"update": "The task has been submitted, please go to the task center to check the task progress",
"uninstall": "The task has been submitted, please go to the task center to check the task progress"
},
"buttons": {
"updateCache": "Update cache",
"install": "Install",
"manage": "Manage",
"update": "Upgrade",
"uninstall": "Uninstall"
},
"confirm": {
"update": "Upgrading the {app} app may reset related configurations to the default state. Are you sure you want to continue?",
"uninstall": "Are you sure you want to uninstall the app {app}?"
},
"columns": {
"name": "Name",
"description": "Description",
"installedVersion": "Installed Version",
"show": "Homepage Display",
"actions": "Actions"
}
},
"settingIndex": {
"title": "Settings",
"info": "Update panel settings / entry, adjust browser address accordingly to access panel!",
"edit": {
"toasts": {
"success": "saved successfully"
},
"fields": {
"name": {
"label": "Panel name",
"placeholder": "Rat Panel"
},
"locale": {
"label": "Language",
"placeholder": "Select language"
},
"username": {
"label": "Username",
"placeholder": "admin"
},
"password": {
"label": "Password",
"placeholder": "admin"
},
"email": {
"label": "Certificate default email",
"placeholder": "admin{'@'}example.com"
},
"port": {
"label": "Port",
"placeholder": "8888"
},
"entrance": {
"label": "Security entrance",
"placeholder": "admin"
},
"offline": {
"label": "Offline mode"
},
"https": {
"label": "Panel HTTPS"
},
"cert": {
"label": "Certificate"
},
"key": {
"label": "Key"
},
"path": {
"label": "Default website directory",
"placeholder": "/www/wwwroot"
},
"backup": {
"label": "Default backup directory",
"placeholder": "/www/backup"
}
},
"actions": {
"submit": "save"
}
}
},
"sshIndex": {
"title": "SSH",
"alerts": {
"save": "saved successfully"
},
"save": {
"fields": {
"host": {
"label": "Host",
"placeholder": "Host"
},
"port": {
"label": "Port",
"placeholder": "Port"
},
"username": {
"label": "User name",
"placeholder": "User name"
},
"password": {
"label": "Password",
"placeholder": "Password"
}
},
"actions": {
"submit": "save"
}
}
},
"websiteIndex": {
"title": "Websites",
"columns": {
"name": "Website Name",
"status": "Status",
"path": "Path",
"remark": "Remark",
"actions": "Actions"
},
"create": {
"trigger": "Create website",
"title": "Create a new website",
"fields": {
"name": {
"label": "Website Name",
"placeholder": "Please use English for the website name. Once set, it cannot be changed."
},
"domains": {
"label": "Domains"
},
"port": {
"label": "Port"
},
"phpVersion": {
"label": "PHP Version",
"placeholder": "Select PHP version"
},
"db": {
"label": "Database",
"placeholder": "Select database"
},
"dbName": {
"label": "Database Name",
"placeholder": "Please enter the database name"
},
"dbUser": {
"label": "Database User",
"placeholder": "Please enter the database user"
},
"dbPassword": {
"label": "Database Password",
"placeholder": "Please enter the database password"
},
"path": {
"label": "Directory",
"placeholder": "Website root directory (leave blank for default site directory/site name)"
},
"remark": {
"label": "Remark",
"placeholder": "Please enter a remark"
}
},
"actions": {
"submit": "Submit"
}
},
"edit": {
"trigger": "edit default page"
}
}
}

View File

@@ -1,25 +0,0 @@
import type { App } from 'vue'
import { createI18n } from 'vue-i18n'
import { useThemeStore } from '@/store'
import en from './en.json'
import zh_CN from './zh_CN.json'
let i18n: ReturnType<typeof createI18n>
export function setupI18n(app: App) {
const themeStore = useThemeStore()
i18n = createI18n({
legacy: false,
globalInjection: true,
locale: themeStore.locale,
missingWarn: false, // TODO 完成 i18n 之后需要去除
fallbackWarn: false, // TODO 完成 i18n 之后需要去除
fallbackLocale: 'zh_CN',
messages: {
en: en,
zh_CN: zh_CN
}
})
app.use(i18n)
}

View File

@@ -3,10 +3,8 @@ import { usePermissionStore, useTabStore, useThemeStore } from '@/store'
import { isUrl, renderIcon } from '@/utils'
import type { MenuInst, MenuOption } from 'naive-ui'
import type { VNodeChild } from 'vue'
import { useI18n } from 'vue-i18n'
import type { Meta, RouteType } from '~/types/router'
const { t } = useI18n()
const router = useRouter()
const currentRoute = useRoute()
const permissionStore = usePermissionStore()
@@ -40,7 +38,7 @@ type MenuItem = MenuOption & {
function getMenuItem(route: RouteType, basePath = ''): MenuItem {
let menuItem: MenuItem = {
label: t(route.meta?.title || route.name),
label: route.meta?.title ? route.meta.title : route.name,
key: route.name,
path: resolvePath(basePath, route.path),
icon: getIcon(route.meta)
@@ -56,7 +54,7 @@ function getMenuItem(route: RouteType, basePath = ''): MenuItem {
// 单个子路由处理
const singleRoute = visibleChildren[0]
menuItem = {
label: t(singleRoute.meta?.title || singleRoute.name),
label: singleRoute.meta?.title ? singleRoute.meta.title : singleRoute.name,
key: singleRoute.name,
path: resolvePath(menuItem.path, singleRoute.path),
icon: getIcon(singleRoute.meta)

View File

@@ -1,13 +1,13 @@
<script lang="ts" setup>
import type { TreeSelectOption } from 'naive-ui'
import { useI18n } from 'vue-i18n'
import { useGettext } from 'vue3-gettext'
import TheIcon from '@/components/custom/TheIcon.vue'
import MenuCollapse from '@/layout/header/components/MenuCollapse.vue'
import { usePermissionStore, useThemeStore } from '@/store'
import type { RouteType } from '~/types/router'
const { t } = useI18n()
const { $gettext } = useGettext()
const themeStore = useThemeStore()
const permissionStore = usePermissionStore()
@@ -15,7 +15,7 @@ const settingModal = ref(false)
const getOption = (route: RouteType): TreeSelectOption => {
let menuItem: TreeSelectOption = {
label: t(route.meta?.title || route.name),
label: route.meta?.title ? route.meta.title : route.name,
key: route.name
}
@@ -28,7 +28,7 @@ const getOption = (route: RouteType): TreeSelectOption => {
if (visibleChildren.length === 1) {
// 单个子路由处理
const singleRoute = visibleChildren[0]
menuItem.label = t(singleRoute.meta?.title || singleRoute.name)
menuItem.label = singleRoute.meta?.title ? singleRoute.meta.title : singleRoute.name
const visibleItems = singleRoute.children
? singleRoute.children.filter((item: RouteType) => item.name && !item.isHidden)
: []
@@ -60,12 +60,12 @@ const menus = computed<TreeSelectOption[]>(() => {
@click="settingModal = true"
/>
</template>
菜单设置
{{ $gettext('Menu Settings') }}
</n-tooltip>
<n-modal
v-model:show="settingModal"
preset="card"
title="菜单设置"
:title="$gettext('Menu Settings')"
style="width: 60vw"
size="huge"
:bordered="false"
@@ -75,11 +75,20 @@ const menus = computed<TreeSelectOption[]>(() => {
>
<n-form>
<n-flex vertical>
<n-alert type="info"> 设置保存在浏览器清空浏览器缓存后将会重置 </n-alert>
<n-form-item label="自定义 Logo">
<n-input v-model:value="themeStore.logo" placeholder="请输入完整 URL" />
<n-alert type="info">
{{
$gettext(
'Settings are saved in the browser and will be reset after clearing the browser cache'
)
}}
</n-alert>
<n-form-item :label="$gettext('Custom Logo')">
<n-input
v-model:value="themeStore.logo"
:placeholder="$gettext('Please enter the complete URL')"
/>
</n-form-item>
<n-form-item label="隐藏菜单">
<n-form-item :label="$gettext('Hide Menu')">
<n-tree-select
cascade
checkable

View File

@@ -64,7 +64,7 @@ async function handleContextMenu(e: MouseEvent, tabItem: TabItem) {
@click="handleTagClick(item.path)"
@contextmenu.prevent="handleContextMenu($event, item)"
>
{{ $t(item.title!) }}
{{ item.title! }}
</n-tab>
</n-tabs>
<ContextMenu

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,6 @@ import 'virtual:uno.css'
import { createApp } from 'vue'
import App from './App.vue'
import { setupI18n } from '@/i18n/i18n'
import { setupRouter } from '@/router'
import { setupStore, useThemeStore } from '@/store'
import { createGettext, setupNaiveDiscreteApi } from '@/utils'
@@ -31,7 +30,6 @@ async function setupApp() {
await setupNaiveDiscreteApi()
await setupPanel().then(() => {
app.use(createGettext)
setupI18n(app)
})
await setupRouter(app)
app.mount('#app')

View File

@@ -1,4 +1,5 @@
import type { RouteModule, RoutesType, RouteType } from '~/types/router'
import { $gettext } from '@/utils/gettext'
export const basicRoutes: RoutesType = [
{
@@ -14,7 +15,7 @@ export const basicRoutes: RoutesType = [
component: () => import('@/views/login/IndexView.vue'),
isHidden: true,
meta: {
title: '登录页'
title: $gettext('Login Page')
}
}
]

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"