jcloud/dashboard/src2/components/JsiteServerOverview.vue

772 lines
24 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div
v-if="$jsiteServer?.pg"
class="grid grid-cols-1 items-start gap-5 lg:grid-cols-2"
>
<!-- 左侧区块基本信息 -->
<div class="col-span-1 space-y-5">
<!-- 当前套餐卡片 -->
<div class="rounded-md border">
<div class="h-12 border-b px-5 py-4">
<h2 class="text-lg font-medium text-gray-900">当前套餐</h2>
</div>
<div class="p-5">
<div class="flex h-full flex-col sm:flex-row sm:items-center sm:justify-between">
<div class="mb-4 sm:mb-0">
<div v-if="$jsiteServer.pg.plan_price" class="text-lg font-bold text-green-600">
¥{{ $jsiteServer.pg.plan_price }}/
</div>
<div v-if="$jsiteServer.pg.end_date" class="mt-2 inline-flex items-center rounded-full bg-amber-50 px-4 py-2 text-sm font-medium text-amber-800">
<ClockIcon class="mr-1.5 h-4 w-4 text-amber-500" />
到期时间{{ $format.date($jsiteServer.pg.end_date) }}
</div>
</div>
<div class="flex gap-2">
<Button
@click="renewServer"
:loading="$jsiteServer.renew?.loading"
class="px-5 !bg-[#1fc76f] !hover:bg-[#1bb85f] !text-white"
>
续费
</Button>
<Button
@click="upgradeServer"
:loading="upgradeLoading"
class="px-5 !bg-[#3b82f6] !hover:bg-[#2563eb] !text-white"
>
升级
</Button>
</div>
</div>
</div>
</div>
<!-- 服务器配置 -->
<div class="rounded-md border">
<div class="h-12 border-b px-5 py-4">
<h2 class="text-lg font-medium text-gray-900">服务器配置</h2>
</div>
<div class="p-5">
<div class="space-y-3">
<div class="flex justify-between items-center">
<span class="text-gray-600">CPU:</span>
<span class="text-lg font-semibold text-blue-600">{{ $jsiteServer.pg.cpu || '未知' }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600">内存:</span>
<span class="text-lg font-semibold text-green-600">{{ $jsiteServer.pg.memory || '未知' }}GB</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600">系统盘:</span>
<span class="text-lg font-semibold text-purple-600">{{ $jsiteServer.pg.disk_size || '未知' }}GB</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600">带宽:</span>
<span class="text-lg font-semibold text-orange-600">{{ $jsiteServer.pg.bandwidth || '未知' }}Mbps</span>
</div>
</div>
</div>
</div>
<!-- 服务器信息 -->
<div class="rounded-md border">
<div class="h-12 border-b px-5 py-4">
<h2 class="text-lg font-medium text-gray-900">服务器信息</h2>
</div>
<div>
<div
v-for="info in serverInformation"
:key="info.label"
class="flex items-center px-5 py-3 last:pb-5 even:bg-gray-50/70"
>
<div class="w-1/3 text-base text-gray-600">{{ info.label }}</div>
<div
class="flex w-2/3 items-center space-x-2 text-base text-gray-900"
>
<div v-if="info.prefix">
<component :is="info.prefix" />
</div>
<!-- 状态字段使用Badge组件 -->
<div v-if="info.label === '状态'">
<Badge :label="info.value" />
</div>
<!-- 服务器名称特殊处理 - 支持内联编辑 -->
<div v-else-if="info.label === '服务器名称'" class="flex-1 min-w-0">
<div v-if="!editingServerName"
@click="startEditServerName"
class="group flex items-center cursor-pointer rounded-md px-2 py-1 -mx-2 -my-1 hover:bg-gray-100 transition-colors duration-200"
:title="'点击编辑服务器名称'"
>
<span class="truncate">{{ info.value }}</span>
<svg class="ml-2 h-4 w-4 text-gray-400 opacity-0 group-hover:opacity-100 transition-opacity duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path>
</svg>
</div>
<div v-else class="flex items-center space-x-2">
<input
ref="serverNameInput"
v-model="editServerNameValue"
@keyup.enter="saveServerName"
@keyup.escape="cancelEditServerName"
@blur="handleInputBlur"
class="flex-1 px-2 py-1 text-sm border border-blue-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent shadow-sm"
:disabled="saveServerNameLoading"
maxlength="100"
placeholder="请输入服务器名称"
/>
<button
@click="saveServerName"
:disabled="saveServerNameLoading"
class="p-1 text-green-600 hover:text-green-700 disabled:opacity-50 transition-colors duration-200"
title="保存"
>
<svg v-if="!saveServerNameLoading" class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
<svg v-else class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="m12 6a6 6 0 0 1 6 6h4a10 10 0 0 0-10-10v4z"></path>
</svg>
</button>
<button
@click="cancelEditServerName"
:disabled="saveServerNameLoading"
class="p-1 text-gray-500 hover:text-gray-700 disabled:opacity-50 transition-colors duration-200"
title="取消"
>
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
</div>
<!-- 其他信息正常显示 -->
<span v-else>
{{ info.value }}
</span>
<div v-if="info.suffix">
<component :is="info.suffix" />
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右侧区块配置信息和操作 -->
<div class="col-span-1 space-y-5">
<!-- 操作 -->
<div class="rounded-md border">
<div class="h-12 border-b px-5 py-4">
<h2 class="text-lg font-medium text-gray-900">操作</h2>
</div>
<div class="p-5">
<div class="flex flex-wrap gap-2">
<Button
@click="restartServer"
:loading="restartLoading"
variant="outline"
class="bg-gray-100 text-gray-700 hover:bg-gray-200"
>
重启
</Button>
<Button
@click="forceRestartServer"
:loading="forceRestartLoading"
variant="outline"
class="bg-orange-50 text-orange-700 hover:bg-orange-100 border-orange-200"
>
强制重启
</Button>
<Button
@click="resetPassword"
:loading="resetPasswordLoading"
variant="outline"
class="bg-gray-100 text-gray-700 hover:bg-gray-200"
>
重置密码
</Button>
<Button
@click="resetKeyPair"
:loading="resetKeyPairLoading"
variant="outline"
class="bg-gray-100 text-gray-700 hover:bg-gray-200"
>
重置密钥对
</Button>
<Button
@click="deleteKeyPair"
:loading="deleteKeyPairLoading"
variant="outline"
class="bg-red-50 text-red-700 hover:bg-red-100 border-red-200"
>
删除密钥对
</Button>
<Button
@click="resetSystem"
:loading="resetSystemLoading"
variant="outline"
class="bg-red-50 text-red-700 hover:bg-red-100 border-red-200"
>
重置系统
</Button>
</div>
</div>
</div>
<!-- SSH连接信息 -->
<div class="rounded-md border">
<div class="h-12 border-b px-5 py-4">
<h2 class="text-lg font-medium text-gray-900">SSH连接</h2>
</div>
<div class="p-5">
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-gray-600">SSH端口:</span>
<span class="font-mono text-gray-900">{{ $jsiteServer.pg.ssh_port || '22' }}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-600">SSH用户:</span>
<span class="font-mono text-gray-900">{{ $jsiteServer.pg.ssh_user || 'root' }}</span>
</div>
<div class="flex justify-between items-center">
<span class="text-gray-600">服务器密码:</span>
<div class="flex items-center space-x-2">
<span class="font-mono text-gray-900">
{{ showPassword ? decryptedPassword : ($jsiteServer.pg.password || '未设置') }}
</span>
<button
v-if="$jsiteServer.pg.password"
@click="togglePassword"
class="text-gray-500 hover:text-gray-700 transition-colors"
:title="showPassword ? '隐藏密码' : '显示密码'"
>
<EyeIcon v-if="!showPassword" class="h-4 w-4" />
<EyeOffIcon v-else class="h-4 w-4" />
</button>
</div>
</div>
<div class="flex justify-between">
<span class="text-gray-600">密钥对名称:</span>
<span class="font-mono text-gray-900">{{ $jsiteServer.pg.key_pair_name || '未设置' }}</span>
</div>
<div class="flex flex-col">
<span class="text-gray-600 mb-2">私钥:</span>
<div v-if="$jsiteServer.pg.private_key" class="bg-gray-50 p-3 rounded border relative">
<button
@click="copyPrivateKey"
class="absolute top-2 right-2 p-1 text-gray-500 hover:text-gray-700 transition-colors rounded"
:title="copySuccess ? '已复制' : '复制私钥'"
>
<CopyIcon class="h-4 w-4" />
</button>
<pre class="font-mono text-xs text-gray-900 whitespace-pre-wrap break-all pr-8">{{ $jsiteServer.pg.private_key }}</pre>
</div>
<span v-else class="font-mono text-gray-900">未设置</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import { getCachedDocumentResource, Badge, Tooltip, Button, createResource } from 'jingrow-ui';
import { h, defineAsyncComponent } from 'vue';
import { toast } from 'vue-sonner';
import { getToastErrorMessage } from '../utils/toast';
import { renderDialog, confirmDialog } from '../utils/components';
import EyeIcon from '~icons/lucide/eye';
import EyeOffIcon from '~icons/lucide/eye-off';
import ClockIcon from '~icons/lucide/clock';
import InfoIcon from '~icons/lucide/info';
import CopyIcon from '~icons/lucide/copy';
export default {
name: 'JsiteServerOverview',
props: ['server'],
components: {
Badge,
Button,
EyeIcon,
EyeOffIcon,
ClockIcon,
InfoIcon,
CopyIcon,
},
data() {
return {
showPassword: false,
decryptedPassword: null,
restartLoading: false,
forceRestartLoading: false,
resetPasswordLoading: false,
resetKeyPairLoading: false,
deleteKeyPairLoading: false,
resetSystemLoading: false,
upgradeLoading: false,
copySuccess: false,
editingServerName: false,
editServerNameValue: '',
saveServerNameLoading: false,
};
},
methods: {
getStatusText(status) {
const statusMap = {
'Pending': '准备中',
'Starting': '启动中',
'Running': '运行中',
'Stopping': '停止中',
'Stopped': '已停止',
'Resetting': '重置中',
'Upgrading': '升级中',
'Disabled': '已禁用'
};
return statusMap[status] || status;
},
getStatusVariant(status) {
const variantMap = {
'Pending': 'warning',
'Starting': 'warning',
'Running': 'success',
'Stopping': 'warning',
'Stopped': 'danger',
'Resetting': 'warning',
'Upgrading': 'warning',
'Disabled': 'danger'
};
return variantMap[status] || 'default';
},
getRegionText(region) {
const regionMap = {
'cn-qingdao': '华北1青岛',
'cn-beijing': '华北2北京',
'cn-zhangjiakou': '华北3张家口',
'cn-huhehaote': '华北5呼和浩特',
'cn-hangzhou': '华东1杭州',
'cn-shanghai': '华东2上海',
'cn-shenzhen': '华南1深圳',
'cn-heyuan': '华南2河源',
'cn-chengdu': '西南1成都',
'cn-guangzhou': '华南3广州',
'cn-wulanchabu': '华北6乌兰察布',
'cn-nanjing': '华东5南京',
'cn-fuzhou': '华东6福州',
'cn-wuhan-lr': '华中1武汉',
'cn-hongkong': '中国香港',
'ap-southeast-1': '新加坡',
'ap-southeast-3': '马来西亚(吉隆坡)',
'ap-southeast-5': '印度尼西亚(雅加达)',
'ap-northeast-1': '日本(东京)',
'us-west-1': '美国(硅谷)',
'us-east-1': '美国(弗吉尼亚)',
'eu-central-1': '德国(法兰克福)',
'eu-west-1': '英国(伦敦)',
'ap-southeast-6': '菲律宾(马尼拉)',
'ap-southeast-7': '泰国(曼谷)',
'ap-northeast-2': '韩国(首尔)'
};
return regionMap[region] || region;
},
renewServer() {
const JsiteServerRenewalDialog = defineAsyncComponent(() => import('./JsiteServerRenewalDialog.vue'));
renderDialog(h(JsiteServerRenewalDialog, {
server: this.server,
serverDoc: this.$jsiteServer.pg,
onSuccess: this.onRenewalSuccess
}));
},
upgradeServer() {
const JsiteServerUpgradeDialog = defineAsyncComponent(() => import('./JsiteServerUpgradeDialog.vue'));
renderDialog(h(JsiteServerUpgradeDialog, {
server: this.server,
serverDoc: this.$jsiteServer.pg,
onSuccess: this.onUpgradeSuccess
}));
},
async restartServer() {
if (!this.$jsiteServer.pg.instance_id) {
toast.error('服务器实例ID不存在');
return;
}
confirmDialog({
title: '重启',
message: '确定要重启服务器吗?重启过程中服务器将暂时不可用。',
primaryAction: {
label: '确定',
onClick: ({ hide }) => {
// 立即显示成功提示并关闭弹窗
toast.success('重启请求已提交');
hide();
// 异步提交请求
this.restartLoading = true;
const restartRequest = createResource({
url: '/api/action/jcloud.api.aliyun_server_light.reboot_aliyun_instance',
params: {
instance_id: this.$jsiteServer.pg.instance_id,
region_id: this.$jsiteServer.pg.region
},
onSuccess: () => {
this.restartLoading = false;
},
onError: (error) => {
toast.error(getToastErrorMessage(error));
this.restartLoading = false;
}
});
restartRequest.submit();
}
}
});
},
async forceRestartServer() {
if (!this.$jsiteServer.pg.instance_id) {
toast.error('服务器实例ID不存在');
return;
}
confirmDialog({
title: '强制重启',
message: '确定要强制重启服务器吗?该操作可能会导致未保存的数据丢失。',
primaryAction: {
label: '确定',
onClick: ({ hide }) => {
toast.success('强制重启请求已提交');
hide();
this.forceRestartLoading = true;
const req = createResource({
url: '/api/action/jcloud.api.aliyun_server_light.force_reboot_aliyun_instance',
params: {
instance_id: this.$jsiteServer.pg.instance_id,
region_id: this.$jsiteServer.pg.region
},
onSuccess: () => {
this.forceRestartLoading = false;
},
onError: (error) => {
toast.error(getToastErrorMessage(error));
this.forceRestartLoading = false;
}
});
req.submit();
}
}
});
},
async resetPassword() {
if (!this.$jsiteServer.pg.instance_id) {
toast.error('服务器实例ID不存在');
return;
}
// 弹出密码输入对话框
const PasswordDialog = defineAsyncComponent(() => import('../dialogs/PasswordDialog.vue'));
renderDialog(h(PasswordDialog, {
title: '重置服务器密码',
description: '长度为 8 至 30 个字符,必须同时包含大小写英文字母、数字和特殊符号。',
onConfirm: (password) => {
// 立即显示成功提示
toast.success('密码重置请求已提交');
// 异步提交请求
this.resetPasswordLoading = true;
const resetPasswordRequest = createResource({
url: '/api/action/jcloud.api.aliyun_server_light.reset_aliyun_instance_password',
params: {
instance_id: this.$jsiteServer.pg.instance_id,
password: password,
region_id: this.$jsiteServer.pg.region
},
onSuccess: () => {
this.resetPasswordLoading = false;
},
onError: (error) => {
toast.error(getToastErrorMessage(error));
this.resetPasswordLoading = false;
}
});
resetPasswordRequest.submit();
}
}));
},
async resetKeyPair() {
if (!this.$jsiteServer.pg.instance_id) {
toast.error('服务器实例ID不存在');
return;
}
confirmDialog({
title: '重置密钥对',
message: '确定要重置密钥对吗?这将删除旧的密钥对并创建新的密钥对。重置后需要使用新的私钥才能连接服务器。',
primaryAction: {
label: '确定',
onClick: ({ hide }) => {
// 立即显示成功提示并关闭弹窗
toast.success('密钥对重置请求已提交');
hide();
// 异步提交请求
this.resetKeyPairLoading = true;
const resetKeyPairRequest = createResource({
url: '/api/action/jcloud.api.aliyun_server_light.reset_server_key_pair',
params: {
instance_id: this.$jsiteServer.pg.instance_id
},
onSuccess: () => {
this.resetKeyPairLoading = false;
},
onError: (error) => {
toast.error(getToastErrorMessage(error));
this.resetKeyPairLoading = false;
}
});
resetKeyPairRequest.submit();
}
}
});
},
async deleteKeyPair() {
if (!this.$jsiteServer.pg.instance_id) {
toast.error('服务器实例ID不存在');
return;
}
confirmDialog({
title: '删除密钥对',
message: '确定要删除密钥对吗?删除后将无法使用私钥连接服务器,建议先设置服务器密码。',
primaryAction: {
label: '确定',
onClick: ({ hide }) => {
// 立即显示成功提示并关闭弹窗
toast.success('密钥对删除请求已提交');
hide();
// 异步提交请求
this.deleteKeyPairLoading = true;
const deleteKeyPairRequest = createResource({
url: '/api/action/jcloud.api.aliyun_server_light.delete_aliyun_instance_key_pair',
params: {
instance_id: this.$jsiteServer.pg.instance_id
},
onSuccess: () => {
this.deleteKeyPairLoading = false;
// 刷新服务器数据以更新界面
this.$jsiteServer.reload();
},
onError: (error) => {
toast.error(getToastErrorMessage(error));
this.deleteKeyPairLoading = false;
}
});
deleteKeyPairRequest.submit();
}
}
});
},
async resetSystem() {
if (!this.$jsiteServer.pg.instance_id) {
toast.error('服务器实例ID不存在');
return;
}
confirmDialog({
title: '重置系统',
message: '确定要重置系统吗?这将清除所有数据并重新安装系统,操作不可逆!',
primaryAction: {
label: '确定',
onClick: ({ hide }) => {
// 立即显示成功提示并关闭弹窗
toast.success('系统重置请求已提交');
hide();
// 异步提交请求
this.resetSystemLoading = true;
const resetSystemRequest = createResource({
url: '/api/action/jcloud.api.aliyun_server_light.reset_aliyun_instance_system',
params: {
instance_id: this.$jsiteServer.pg.instance_id,
region_id: this.$jsiteServer.pg.region
},
onSuccess: () => {
this.resetSystemLoading = false;
},
onError: (error) => {
toast.error(getToastErrorMessage(error));
this.resetSystemLoading = false;
}
});
resetSystemRequest.submit();
}
}
});
},
togglePassword() {
if (this.showPassword) {
this.showPassword = false;
this.decryptedPassword = null;
} else {
const getPasswordRequest = createResource({
url: '/api/action/jcloud.api.aliyun_server_light.get_password',
params: {
pagetype: 'Jsite Server',
name: this.$jsiteServer.pg.name,
fieldname: 'password'
},
onSuccess: (response) => {
if (response && response !== '') {
this.decryptedPassword = response;
this.showPassword = true;
} else {
toast.warning('当前没有保存的密码');
}
},
onError: (error) => {
toast.error('获取密码失败');
}
});
getPasswordRequest.submit();
}
},
copyPrivateKey() {
if (this.$jsiteServer.pg.private_key) {
navigator.clipboard.writeText(this.$jsiteServer.pg.private_key).then(() => {
this.copySuccess = true;
toast.success('私钥已复制到剪贴板');
setTimeout(() => {
this.copySuccess = false;
}, 2000);
}).catch(() => {
toast.error('复制失败,请手动复制');
});
}
},
onRenewalSuccess(data) {
toast.success('服务器续费成功!');
// 刷新服务器数据
this.$jsiteServer.reload();
},
onUpgradeSuccess(data) {
toast.success('服务器升级成功!');
// 刷新服务器数据
this.$jsiteServer.reload();
},
startEditServerName() {
this.editServerNameValue = this.$jsiteServer.pg?.title || this.$jsiteServer.pg?.name || '';
this.editingServerName = true;
this.$nextTick(() => {
const input = this.$refs.serverNameInput;
if (input && typeof input.focus === 'function') {
input.focus();
if (typeof input.select === 'function') {
input.select();
}
}
});
},
cancelEditServerName() {
this.editingServerName = false;
this.editServerNameValue = '';
},
handleInputBlur() {
// 延迟处理,防止点击按钮时触发
setTimeout(() => {
if (this.editingServerName && !this.saveServerNameLoading) {
this.saveServerName();
}
}, 150);
},
async saveServerName() {
if (this.saveServerNameLoading) return;
const newName = this.editServerNameValue.trim();
if (!newName) {
toast.error('服务器名称不能为空');
return;
}
if (newName.length < 2) {
toast.error('服务器名称至少需要2个字符');
return;
}
if (newName.length > 100) {
toast.error('服务器名称不能超过100个字符');
return;
}
if (newName === (this.$jsiteServer.pg?.title || this.$jsiteServer.pg?.name)) {
this.cancelEditServerName();
return;
}
this.saveServerNameLoading = true;
try {
this.$jsiteServer.setValue.submit(
{ title: newName },
{
onSuccess: () => {
toast.success('服务器名称已更新');
this.editingServerName = false;
this.editServerNameValue = '';
this.saveServerNameLoading = false;
},
onError: (error) => {
toast.error(getToastErrorMessage(error));
this.saveServerNameLoading = false;
}
}
);
} catch (error) {
toast.error('更新失败,请重试');
this.saveServerNameLoading = false;
}
},
},
computed: {
serverInformation() {
return [
{
label: '状态',
value: this.getStatusText(this.$jsiteServer.pg?.status),
},
{
label: '服务器名称',
value: this.$jsiteServer.pg?.title || this.$jsiteServer.pg?.name,
},
{
label: '实例ID',
value: this.$jsiteServer.pg?.instance_id || '',
},
{
label: '公网IP',
value: this.$jsiteServer.pg?.public_ip || '',
},
{
label: '内网IP',
value: this.$jsiteServer.pg?.private_ip || '',
},
{
label: '区域',
value: this.getRegionText(this.$jsiteServer.pg?.region),
},
{
label: '系统',
value: this.$jsiteServer.pg?.system || '',
},
];
},
$jsiteServer() {
return getCachedDocumentResource('Jsite Server', this.server);
},
},
};
</script>