586 lines
17 KiB
Vue
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> |