jcloud/dashboard/src2/pages/NewServer.vue
2025-04-12 17:39:38 +08:00

586 lines
17 KiB
Vue

<template>
<div class="sticky top-0 z-10 shrink-0">
<Header>
<Breadcrumbs
:items="[
{ label: '服务器', route: '/servers' },
{ label: '新建服务器', route: '/servers/new' }
]"
/>
</Header>
</div>
<div
v-if="!$team.pg?.is_desk_user && !$session.hasServerCreationAccess"
class="mx-auto mt-60 w-fit rounded border border-dashed px-12 py-8 text-center text-gray-600"
>
<i-lucide-alert-triangle class="mx-auto mb-4 h-6 w-6 text-red-600" />
<ErrorMessage message="您没有权限创建新服务器" />
</div>
<div v-else-if="serverEnabled" class="mx-auto max-w-2xl px-5">
<div v-if="options" class="space-y-12 pb-[50vh] pt-12">
<div class="flex flex-col">
<h2 class="text-sm font-medium leading-6 text-gray-900">
选择服务器类型
</h2>
<div class="mt-2 w-full space-y-2">
<div class="grid grid-cols-2 gap-3">
<button
v-for="c in options?.server_types"
:key="c.name"
@click="serverType = c.name"
:class="[
serverType === c.name
? 'border-gray-900 ring-1 ring-gray-900 hover:bg-gray-100'
: 'border-gray-400 bg-white text-gray-900 ring-gray-200 hover:bg-gray-50',
'flex w-full items-center rounded border p-3 text-left text-base text-gray-900'
]"
>
<div class="flex w-full items-center justify-between space-x-2">
<span class="text-sm font-medium">
{{ c.title }}
</span>
<Tooltip :text="c.description">
<i-lucide-info class="h-4 w-4 text-gray-500" />
</Tooltip>
</div>
</button>
</div>
</div>
</div>
<div v-if="serverType" class="flex flex-col">
<h2 class="text-sm font-medium leading-6 text-gray-900">
输入服务器名称
</h2>
<div class="mt-2">
<FormControl
v-model="serverTitle"
type="text"
class="block rounded-md border-gray-300 shadow-sm focus:border-gray-900 focus:ring-gray-900 sm:text-sm"
/>
</div>
</div>
<div v-if="serverType === 'dedicated'" class="space-y-12">
<div class="flex flex-col" v-if="options?.regions.length">
<h2 class="text-sm font-medium leading-6 text-gray-900">
选择区域
</h2>
<div class="mt-2 w-full space-y-2">
<div class="grid grid-cols-2 gap-3">
<button
v-for="c in options?.regions"
:key="c.name"
@click="serverRegion = c.name"
:class="[
serverRegion === c.name
? 'border-gray-900 ring-1 ring-gray-900 hover:bg-gray-100'
: 'border-gray-400 bg-white text-gray-900 ring-gray-200 hover:bg-gray-50',
'flex w-full items-center rounded border p-3 text-left text-base text-gray-900'
]"
>
<div class="flex w-full items-center justify-between">
<div class="flex w-full items-center space-x-2">
<img :src="c.image" class="h-5 w-5" />
<span class="text-sm font-medium">
{{ c.title }}
</span>
</div>
<Badge v-if="c.beta" :label="c.beta ? '测试版' : ''" />
</div>
</button>
</div>
</div>
</div>
<div
v-if="serverRegion && options.app_premium_plans.length > 0"
class="flex flex-col"
>
<div class="flex items-center justify-between">
<h2 class="text-sm font-medium leading-6 text-gray-900">
计划类型
</h2>
<div>
<Button
link="https://jingrow.com/pricing#dedicated"
variant="ghost"
>
<template #prefix>
<i-lucide-help-circle class="h-4 w-4 text-gray-700" />
</template>
帮助
</Button>
</div>
</div>
<div class="mt-2 w-full space-y-2">
<div class="grid grid-cols-2 gap-3">
<button
v-for="c in [
{
name: '标准',
description: '包含标准支持和SLA'
},
{
name: '高级',
description: '包含企业支持和SLA'
}
]"
:key="c.name"
@click="planType = c.name"
:class="[
planType === c.name
? 'border-gray-900 ring-1 ring-gray-900 hover:bg-gray-100'
: 'border-gray-400 bg-white text-gray-900 ring-gray-200 hover:bg-gray-50',
'flex w-full items-center rounded border p-3 text-left text-base text-gray-900'
]"
>
<div class="flex w-full items-center justify-between space-x-2">
<span class="text-sm font-medium">
{{ c.name }}
</span>
<Tooltip :text="c.description">
<i-lucide-info class="h-4 w-4 text-gray-500" />
</Tooltip>
</div>
</button>
</div>
</div>
</div>
<div v-if="serverRegion">
<div class="flex flex-col" v-if="options?.app_plans.length">
<h2 class="text-sm font-medium leading-6 text-gray-900">
选择应用服务器方案
</h2>
<div class="mt-2 space-y-2">
<ServerPlansCards
v-model="appServerPlan"
:plans="
(planType === '标准'
? options.app_plans
: options.app_premium_plans
).filter(p => p.cluster === serverRegion)
"
/>
</div>
</div>
</div>
<div v-if="serverRegion">
<div class="flex flex-col" v-if="options?.db_plans.length">
<h2 class="text-sm font-medium leading-6 text-gray-900">
选择数据库服务器方案
</h2>
<div class="mt-2 w-full space-y-2">
<ServerPlansCards
v-if="options.db_plans"
v-model="dbServerPlan"
:plans="
(planType === '标准'
? options.db_plans
: options.db_premium_plans
).filter(p => p.cluster === serverRegion)
"
/>
</div>
</div>
</div>
</div>
<div v-else-if="serverType === 'hybrid'" class="space-y-12">
<div class="flex flex-col space-y-2">
<h2 class="text-sm font-medium leading-6 text-gray-900">
应用服务器IP地址
</h2>
<div class="flex space-x-3">
<FormControl
class="w-full"
v-model="appPublicIP"
label="公网IP"
type="text"
/>
<FormControl
class="w-full"
v-model="appPrivateIP"
label="内网IP"
type="text"
/>
</div>
</div>
<div class="flex flex-col space-y-2">
<h2 class="text-sm font-medium leading-6 text-gray-900">
数据库服务器IP地址
</h2>
<div class="flex space-x-3">
<FormControl
class="w-full"
v-model="dbPublicIP"
label="公网IP"
type="text"
/>
<FormControl
class="w-full"
v-model="dbPrivateIP"
label="内网IP"
type="text"
/>
</div>
</div>
<div class="flex flex-col space-y-2">
<h2 class="text-sm font-medium leading-6 text-gray-900">
添加SSH密钥
</h2>
<span class="text-xs text-gray-600">
将此SSH密钥添加到
<span class="font-mono">~/.ssh/authorized_keys</span>
>应用程序和数据库服务器上的文件</span
>
<ClickToCopy :textContent="$resources.hybridOptions.data.ssh_key" />
</div>
</div>
<Summary
:options="summaryOptions"
v-if="
serverTitle &&
((serverRegion && dbServerPlan && appServerPlan) ||
(appPublicIP && appPrivateIP && dbPublicIP && dbPrivateIP))
"
/>
<div
class="flex flex-col space-y-4"
v-if="
serverTitle &&
((serverRegion && dbServerPlan && appServerPlan) ||
(appPublicIP && appPrivateIP && dbPublicIP && dbPrivateIP))
"
>
<FormControl
type="checkbox"
v-model="agreedToRegionConsent"
:label="`我同意我所选地区的法律适用于我和Jingrow。`"
/>
<ErrorMessage
class="my-2"
:message="
$resources.createServer.error || $resources.createHybridServer.error
"
/>
<Button
variant="solid"
:disabled="!agreedToRegionConsent"
@click="
serverType === 'dedicated'
? $resources.createServer.submit({
server: {
title: serverTitle,
cluster: serverRegion,
app_plan: appServerPlan?.name,
db_plan: dbServerPlan?.name
}
})
: $resources.createHybridServer.submit({
server: {
title: serverTitle,
app_public_ip: appPublicIP,
app_private_ip: appPrivateIP,
db_public_ip: dbPublicIP,
db_private_ip: dbPrivateIP,
plan: $resources.hybridOptions.data.plans[0]
}
})
"
:loading="
$resources.createServer.loading ||
$resources.createHybridServer.loading
"
>
{{ serverType === 'hybrid' ? '添加混合服务器' : '创建服务器' }}
</Button>
</div>
</div>
</div>
<div
v-else
class="mx-auto mt-60 w-fit rounded border-2 border-dashed px-12 py-8 text-center text-gray-600"
>
<LucideServer class="mx-auto mb-4 h-8 w-8" />
<p>您的账户未启用服务器功能。</p>
<p>您需要有价值200美元的积分才能启用此功能。</p>
<p>
请从
<router-link class="underline" :to="{ name: 'BillingOverview' }"
>这里</router-link
>添加。
</p>
<p>
或者您可以
<a
class="underline"
href="https://jingrow.com/support"
target="_blank"
>联系支持</a
>
以启用它。
</p>
</div>
</template>
<script>
import LucideServer from '~icons/lucide/server-off';
import Header from '../components/Header.vue';
import Summary from '../components/Summary.vue';
import ServerPlansCards from '../components/server/ServerPlansCards.vue';
import ClickToCopy from '../components/ClickToCopyField.vue';
import { DashboardError } from '../utils/error';
export default {
components: {
ServerPlansCards,
LucideServer,
ClickToCopy,
Summary,
Header
},
props: ['server'],
data() {
return {
serverTitle: '',
appServerPlan: '',
dbServerPlan: '',
serverRegion: '',
serverType: '',
appPublicIP: '',
appPrivateIP: '',
dbPublicIP: '',
dbPrivateIP: '',
planType: 'Standard',
serverEnabled: true,
agreedToRegionConsent: false
};
},
watch: {
serverType() {
this.appServerPlan = '';
this.dbServerPlan = '';
this.serverRegion = '';
this.appPublicIP = '';
this.appPrivateIP = '';
this.dbPublicIP = '';
this.dbPrivateIP = '';
},
planType() {
this.appServerPlan = '';
this.dbServerPlan = '';
}
},
resources: {
options() {
return {
url: 'jcloud.api.server.options',
auto: true,
transform(data) {
return {
server_types: [
{
name: 'dedicated',
title: '专用服务器',
description:
'由 jingrow 管理和拥有的一对专用服务器'
},
{
name: 'hybrid',
title: '混合服务器',
description:
'由 jingrow 管理并由您拥有/提供的一对专用服务器'
}
],
regions: data.regions,
app_plans: data.app_plans.filter(p => p.premium == 0),
db_plans: data.db_plans.filter(p => p.premium == 0),
app_premium_plans: data.app_plans.filter(p => p.premium == 1),
db_premium_plans: data.db_plans.filter(p => p.premium == 1)
};
},
onError(error) {
if (
error.messages.includes(
'服务器功能尚未在您的账户上启用'
)
) {
this.serverEnabled = false;
}
}
};
},
hybridOptions() {
return {
url: 'jcloud.api.selfhosted.options_for_new',
auto: true
};
},
createServer() {
return {
url: 'jcloud.api.server.new',
validate({ server }) {
if (!server.title) {
throw new DashboardError('服务器名称是必填项');
} else if (!server.cluster) {
throw new DashboardError('请选择一个区域');
} else if (!server.app_plan) {
throw new DashboardError('请选择一个应用服务器计划');
} else if (!server.db_plan) {
throw new DashboardError('请选择一个数据库服务器计划');
} else if (Object.keys(this.$team.pg.billing_details).length === 0) {
throw new DashboardError(
"您尚未添加账单信息。请从设置中添加账单信息以继续。"
);
} else if (
this.$team.pg.servers_enabled == 0 &&
((this.$team.pg.currency == 'USD' &&
this.$team.pg.balance < 200) ||
(this.$team.pg.currency == 'CNY' &&
this.$team.pg.balance < 16000))
) {
throw new DashboardError(
'您需要有价值 $200 的信用额度才能创建服务器。'
);
}
},
onSuccess(server) {
this.$router.push({
name: '服务器详情页面',
params: { name: server.server }
});
}
};
},
createHybridServer() {
return {
url: 'jcloud.api.selfhosted.create_and_verify_selfhosted',
validate() {
if (!this.serverTitle) {
throw new DashboardError('服务器名称是必填项');
} else if (
!this.appPublicIP ||
!this.dbPublicIP ||
!this.appPrivateIP ||
!this.dbPrivateIP
) {
throw new DashboardError('请填写所有 IP 地址');
} else if (this.validateIP(this.appPublicIP)) {
throw new DashboardError(
'请输入有效的应用公共 IP'
);
} else if (this.validateIP(this.appPrivateIP)) {
throw new DashboardError(
'请输入有效的应用私有 IP'
);
} else if (this.validateIP(this.dbPublicIP)) {
throw new DashboardError('请输入有效的数据库公共 IP');
} else if (this.validateIP(this.dbPrivateIP)) {
throw new DashboardError(
'请输入有效的数据库私有 IP'
);
} else if (this.dbPublicIP === this.appPublicIP) {
throw new DashboardError(
"请不要将同一服务器用作应用和数据库服务器"
);
} else if (!this.agreedToRegionConsent) {
throw new DashboardError('请同意区域同意书');
}
},
onSuccess(server) {
this.$router.push({
name: '服务器详情页面',
params: { name: server }
});
}
};
}
},
computed: {
options() {
return this.$resources.options.data;
},
_totalPerMonth() {
let currencyField =
this.$team.pg.currency == 'CNY' ? 'price_cny' : 'price_usd';
if (this.serverType === 'dedicated') {
return (
this.appServerPlan[currencyField] + this.dbServerPlan[currencyField]
);
} else if (this.serverType === 'hybrid') {
return this.$resources.hybridOptions?.data?.plans[0][currencyField] * 2;
}
},
totalPerMonth() {
return this.$format.userCurrency(this._totalPerMonth);
},
totalPerDay() {
return this.$format.userCurrency(
this.$format.pricePerDay(this._totalPerMonth)
);
},
summaryOptions() {
return [
{
label: '服务器名称',
value: this.serverTitle
},
{
label: '区域',
value: this.serverRegion,
condition: () => this.serverType === 'dedicated'
},
{
label: '应用服务器方案',
value: this.$format.planTitle(this.appServerPlan) + ' 每月',
condition: () => this.serverType === 'dedicated'
},
{
label: '数据库服务器方案',
value: this.$format.planTitle(this.dbServerPlan) + ' 每月',
condition: () => this.serverType === 'dedicated'
},
{
label: '应用公网IP',
value: this.appPublicIP,
condition: () => this.serverType === 'hybrid'
},
{
label: '应用内网IP',
value: this.appPrivateIP,
condition: () => this.serverType === 'hybrid'
},
{
label: '数据库公网IP',
value: this.dbPublicIP,
condition: () => this.serverType === 'hybrid'
},
{
label: '数据库内网IP',
value: this.dbPrivateIP,
condition: () => this.serverType === 'hybrid'
},
{
label: '方案',
value: `${this.$format.planTitle(
this.$resources.hybridOptions?.data?.plans[0]
)} 每月`,
condition: () =>
this.serverType === 'hybrid' &&
this.$resources.hybridOptions?.data?.plans[0]
},
{
label: '总计',
value: `${this.totalPerMonth} 每月 <div class="text-gray-600"> ${this.totalPerDay} 每天</div>`,
condition: () => this._totalPerMonth
}
];
}
},
methods: {
validateIP(ip) {
return !ip.match(
/^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/
);
}
}
};
</script>