886 lines
25 KiB
Vue
886 lines
25 KiB
Vue
<template>
|
||
<div class="sticky top-0 z-10 shrink-0">
|
||
<Header>
|
||
<FBreadcrumbs :items="breadcrumbs" />
|
||
</Header>
|
||
</div>
|
||
|
||
<div
|
||
v-if="!$team.pg?.is_desk_user && !$session.hasSiteCreationAccess"
|
||
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 class="mx-auto max-w-2xl px-5">
|
||
<div v-if="$resources.options.loading" class="py-4 text-base text-gray-600">
|
||
加载中...
|
||
</div>
|
||
<div v-if="$route.name === 'NewBenchSite' && !bench">
|
||
<div class="py-4 text-base text-gray-600">出错了</div>
|
||
</div>
|
||
<div v-else-if="options" class="space-y-12 pb-[50vh] pt-12">
|
||
<NewSiteAppSelector
|
||
:availableApps="selectedVersionAppOptions"
|
||
:siteOnPublicBench="!bench"
|
||
v-model="apps"
|
||
/>
|
||
<div v-if="showLocalisationSelector" class="space-y-4">
|
||
<div class="flex space-x-2">
|
||
<FormControl
|
||
label="安装本地合规应用?"
|
||
v-model="showLocalisationOption"
|
||
type="checkbox"
|
||
/>
|
||
<Tooltip
|
||
text="本地合规应用允许根据法定合规性创建交易。它们由社区合作伙伴维护。"
|
||
>
|
||
<i-lucide-info class="h-4 w-4 text-gray-500" />
|
||
</Tooltip>
|
||
</div>
|
||
<FormControl
|
||
class="w-1/2"
|
||
variant="outline"
|
||
:class="{ 'pointer-events-none opacity-50': !showLocalisationOption }"
|
||
label="选择国家"
|
||
v-model="selectedLocalisationCountry"
|
||
type="autocomplete"
|
||
:options="localisationAppCountries"
|
||
/>
|
||
</div>
|
||
<div v-if="!bench">
|
||
<div class="flex items-center justify-between">
|
||
<h2 class="text-base font-medium leading-6 text-gray-900">
|
||
选择 Jingrow 版本
|
||
</h2>
|
||
</div>
|
||
<div class="mt-2">
|
||
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3">
|
||
<component
|
||
v-for="v in availableVersions"
|
||
:key="v.name"
|
||
:is="v.disabled ? 'Tooltip' : 'div'"
|
||
:text="
|
||
v.disabled && versionAppsMap[v.name]
|
||
? `此版本不适用于 ${$format.plural(
|
||
versionAppsMap[v.name].length,
|
||
'app',
|
||
'apps',
|
||
)} ${$format.commaAnd(versionAppsMap[v.name])}`
|
||
: ''
|
||
"
|
||
>
|
||
<button
|
||
:class="[
|
||
version === v.name
|
||
? 'border-gray-900 ring-1 ring-gray-900 hover:bg-gray-100'
|
||
: 'bg-white text-gray-900 hover:bg-gray-50',
|
||
v.disabled && 'opacity-50 hover:cursor-default',
|
||
'flex w-full cursor-pointer items-center justify-between rounded border border-gray-400 p-3 text-sm focus:outline-none',
|
||
]"
|
||
@click="
|
||
() => {
|
||
if (v.disabled) return;
|
||
version = v.name;
|
||
}
|
||
"
|
||
>
|
||
<span class="font-medium">{{ v.name }} </span>
|
||
<div
|
||
v-if="v.status === 'Develop'"
|
||
class="flex items-center gap-2"
|
||
>
|
||
<Tooltip
|
||
text="此版本正在开发中,可能存在错误。请勿用于生产站点。"
|
||
>
|
||
<i-lucide-info class="h-4 w-4 text-gray-500" />
|
||
</Tooltip>
|
||
<span class="ml-1 text-gray-600">
|
||
{{ v.status }}
|
||
</span>
|
||
</div>
|
||
</button>
|
||
</component>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="flex flex-col"
|
||
v-if="selectedVersion?.group?.clusters?.length"
|
||
>
|
||
<h2 class="text-base 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 selectedVersion.group.clusters"
|
||
:key="c.name"
|
||
@click="cluster = c.name"
|
||
:class="[
|
||
cluster === c.name
|
||
? 'border-gray-900 ring-1 ring-gray-900 hover:bg-gray-100'
|
||
: 'bg-white text-gray-900 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 v-if="c.image" :src="c.image" class="h-5 w-5" onerror="this.src='/assets/images/china-flag.png'; this.onerror=null;" />
|
||
<i-lucide-globe class="h-5 w-5 text-gray-600" v-else />
|
||
<span class="text-sm font-medium">
|
||
{{ c.title }}
|
||
</span>
|
||
</div>
|
||
<Badge v-if="c.beta" :label="c.beta ? 'Beta' : ''" />
|
||
</div>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="selectedVersion && cluster">
|
||
<div class="flex items-center justify-between">
|
||
<h2 class="text-base font-medium leading-6 text-gray-900">
|
||
选择网站套餐计划
|
||
</h2>
|
||
</div>
|
||
<div class="mt-2">
|
||
<SitePlansCards
|
||
v-model="plan"
|
||
:isPrivateBenchSite="!!bench"
|
||
:isDedicatedServerSite="selectedVersion.group.is_dedicated_server"
|
||
:selectedCluster="cluster"
|
||
:selectedApps="apps"
|
||
:selectedVersion="version"
|
||
:hideRestrictedPlans="selectedLocalisationCountry"
|
||
/>
|
||
</div>
|
||
<div class="mt-4 text-xs text-gray-700">
|
||
<div
|
||
class="flex items-center rounded bg-gray-50 p-2 text-p-base font-medium text-gray-800"
|
||
>
|
||
<i-lucide-badge-check class="h-4 w-8 text-gray-600" />
|
||
<span class="ml-4">
|
||
安装使用 Jingrow 系统遇到任何问题请提交支持工单或联系在线客服。
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="selectedVersion && plan && cluster">
|
||
<h2 class="text-base font-medium leading-6 text-gray-900">
|
||
输入子域名
|
||
</h2>
|
||
<div class="mt-2 items-center">
|
||
<div class="col-span-2 flex w-full">
|
||
<input
|
||
class="dark:[color-scheme:dark] z-10 h-7 w-full flex-1 rounded rounded-r-none border border-[--surface-gray-2] bg-surface-gray-2 py-1.5 pl-2 pr-2 text-base text-ink-gray-8 placeholder-ink-gray-4 transition-colors hover:border-outline-gray-modals hover:bg-surface-gray-3 focus:border-outline-gray-4 focus:bg-surface-white focus:shadow-sm focus:ring-0 focus-visible:ring-2 focus-visible:ring-outline-gray-3"
|
||
placeholder="子域名"
|
||
v-model="subdomain"
|
||
/>
|
||
<div class="flex items-center rounded-r bg-gray-100 px-4 text-base">
|
||
.{{ options.domain }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="mt-1">
|
||
<div
|
||
v-if="$resources.subdomainExists.loading"
|
||
class="text-base text-gray-600"
|
||
>
|
||
正在检查...
|
||
</div>
|
||
<template
|
||
v-else-if="
|
||
!$resources.subdomainExists.error &&
|
||
$resources.subdomainExists.data != null
|
||
"
|
||
>
|
||
<div
|
||
v-if="$resources.subdomainExists.data"
|
||
class="text-sm text-green-600"
|
||
>
|
||
{{ subdomain }}.{{ options.domain }} 可用
|
||
</div>
|
||
<div v-else class="text-sm text-red-600">
|
||
{{ subdomain }}.{{ options.domain }} 不可用
|
||
</div>
|
||
</template>
|
||
<ErrorMessage :message="$resources.subdomainExists.error" />
|
||
</div>
|
||
</div>
|
||
<Summary
|
||
v-if="selectedVersion && cluster && plan && subdomain"
|
||
:options="siteSummaryOptions"
|
||
/>
|
||
<div
|
||
v-if="selectedVersion && cluster && plan"
|
||
class="flex flex-col space-y-4"
|
||
>
|
||
<FormControl
|
||
type="checkbox"
|
||
v-model="agreedToRegionConsent"
|
||
:label="`我同意我所选地区的法律适用于我和Jingrow。`"
|
||
/>
|
||
<FormControl
|
||
class="checkbox"
|
||
type="checkbox"
|
||
label="我同意将我的信息分享给本地合作伙伴"
|
||
@change="(val) => (shareDetailsConsent = val.target.checked)"
|
||
/>
|
||
<div v-if="formError" class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-4">
|
||
{{ formError }}
|
||
</div>
|
||
</div>
|
||
<div v-if="selectedVersion && cluster && plan && subdomain">
|
||
<Button
|
||
class="w-full"
|
||
variant="solid"
|
||
:disabled="!agreedToRegionConsent || !isSubdomainValid"
|
||
@click="checkBalanceAndCreateSite"
|
||
:loading="$resources.createOrder?.loading || false"
|
||
:loadingText="'正在创建订单...'"
|
||
>
|
||
创建站点
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<Dialog
|
||
v-if="showAddPrepaidCreditsDialog"
|
||
v-model="showAddPrepaidCreditsDialog"
|
||
:options="{ title: '新建网站' }"
|
||
>
|
||
<template #body-content>
|
||
<div v-if="showBalanceMessage" class="mb-5 inline-flex gap-1.5 text-base text-gray-700">
|
||
<i-lucide-info class="h-4 w-4" />
|
||
<span>
|
||
您当前的余额不足,请先充值以创建新站点。至少需要充值 {{ $format.userCurrency(Math.ceil(requiredAmount - userBalance)) }}。
|
||
</span>
|
||
</div>
|
||
<BuyPrepaidCreditsForm
|
||
:minimumAmount="Math.ceil(requiredAmount - userBalance)"
|
||
:isOnboarding="false"
|
||
@success="onCreditAdded"
|
||
@cancel="showAddPrepaidCreditsDialog = false"
|
||
/>
|
||
</template>
|
||
</Dialog>
|
||
|
||
<div v-if="showOrderCheckout && !currentOrder" class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
|
||
<div class="bg-white p-6 rounded-lg shadow-xl">
|
||
<p>正在加载支付界面...</p>
|
||
</div>
|
||
</div>
|
||
|
||
<OrderCheckout
|
||
v-if="currentOrder"
|
||
v-model="showOrderCheckout"
|
||
:order="currentOrder"
|
||
@payment-success="onPaymentSuccess"
|
||
@payment-error="onPaymentError"
|
||
@close="resetCheckoutState"
|
||
/>
|
||
</template>
|
||
<script>
|
||
import {
|
||
Autocomplete,
|
||
ErrorMessage,
|
||
FeatherIcon,
|
||
FormControl,
|
||
TextInput,
|
||
Tooltip,
|
||
debounce,
|
||
Breadcrumbs,
|
||
getCachedDocumentResource,
|
||
Dialog,
|
||
} from 'jingrow-ui';
|
||
import SitePlansCards from '../components/SitePlansCards.vue';
|
||
import { validateSubdomain } from '../utils/site';
|
||
import Header from '../components/Header.vue';
|
||
import router from '../router';
|
||
import { plans } from '../data/plans';
|
||
import NewSiteAppSelector from '../components/site/NewSiteAppSelector.vue';
|
||
import Summary from '../components/Summary.vue';
|
||
import { DashboardError } from '../utils/error';
|
||
import { getCountry } from '../utils/country';
|
||
import BuyPrepaidCreditsForm from '../components/BuyPrepaidCreditsForm.vue';
|
||
import OrderCheckout from '../components/OrderCheckout.vue';
|
||
|
||
export default {
|
||
name: 'NewSite',
|
||
props: ['bench'],
|
||
components: {
|
||
FBreadcrumbs: Breadcrumbs,
|
||
NewSiteAppSelector,
|
||
SitePlansCards,
|
||
Autocomplete,
|
||
ErrorMessage,
|
||
FormControl,
|
||
FeatherIcon,
|
||
TextInput,
|
||
Tooltip,
|
||
Summary,
|
||
Header,
|
||
Dialog,
|
||
BuyPrepaidCreditsForm,
|
||
OrderCheckout,
|
||
},
|
||
data() {
|
||
return {
|
||
version: null,
|
||
subdomain: '',
|
||
cluster: null,
|
||
plan: null,
|
||
apps: [],
|
||
appPlans: {},
|
||
selectedApp: null,
|
||
closestCluster: null,
|
||
selectedLocalisationCountry: null,
|
||
showLocalisationOption: false,
|
||
showAppPlanSelectorDialog: false,
|
||
shareDetailsConsent: false,
|
||
agreedToRegionConsent: false,
|
||
showAddPrepaidCreditsDialog: false,
|
||
showBalanceMessage: false,
|
||
requiredAmount: 0,
|
||
userBalance: 0,
|
||
showOrderCheckout: false,
|
||
currentOrder: null,
|
||
formError: null,
|
||
siteFromOrder: null
|
||
};
|
||
},
|
||
watch: {
|
||
apps() {
|
||
this.version = this.autoSelectVersion();
|
||
this.cluster = null;
|
||
this.agreedToRegionConsent = false;
|
||
},
|
||
showLocalisationOption() {
|
||
if (this.showLocalisationOption) {
|
||
const localisationAppCountries = this.localisationAppCountries.map(
|
||
(c) => c.value,
|
||
);
|
||
|
||
if (
|
||
localisationAppCountries.includes(getCountry()) &&
|
||
!this.selectedLocalisationCountry
|
||
) {
|
||
this.selectedLocalisationCountry = { value: getCountry() };
|
||
}
|
||
} else {
|
||
this.selectedLocalisationCountry = null;
|
||
}
|
||
},
|
||
async version() {
|
||
this.cluster = null;
|
||
this.cluster = await this.getClosestCluster();
|
||
this.agreedToRegionConsent = false;
|
||
},
|
||
cluster() {
|
||
this.plan = null;
|
||
this.agreedToRegionConsent = false;
|
||
},
|
||
subdomain: {
|
||
handler: debounce(function (value) {
|
||
let invalidMessage = validateSubdomain(value);
|
||
this.$resources.subdomainExists.error = invalidMessage;
|
||
if (!invalidMessage) {
|
||
this.$resources.subdomainExists.submit();
|
||
}
|
||
}, 500),
|
||
},
|
||
closestCluster() {
|
||
this.cluster = this.closestCluster;
|
||
},
|
||
},
|
||
resources: {
|
||
options() {
|
||
return {
|
||
url: 'jcloud.api.site.options_for_new',
|
||
makeParams() {
|
||
return { for_bench: this.bench };
|
||
},
|
||
onSuccess() {
|
||
if (this.bench && this.options.versions.length > 0) {
|
||
this.version = this.options.versions[0].name;
|
||
}
|
||
},
|
||
auto: true,
|
||
};
|
||
},
|
||
subdomainExists() {
|
||
return {
|
||
url: 'jcloud.api.site.exists',
|
||
makeParams() {
|
||
return {
|
||
domain: this.options?.domain,
|
||
subdomain: this.subdomain,
|
||
};
|
||
},
|
||
validate() {
|
||
let error = validateSubdomain(this.subdomain);
|
||
if (error) {
|
||
return new DashboardError(error);
|
||
}
|
||
},
|
||
transform(data) {
|
||
return !Boolean(data);
|
||
},
|
||
};
|
||
},
|
||
userBalance() {
|
||
return {
|
||
url: 'jcloud.api.billing.get_balance_credit',
|
||
auto: true,
|
||
onSuccess(data) {
|
||
this.userBalance = data;
|
||
},
|
||
};
|
||
},
|
||
createOrder() {
|
||
return {
|
||
url: 'jcloud.api.billing.create_order',
|
||
makeParams() {
|
||
return {
|
||
title: `${this.subdomain}.${this.options?.domain}`,
|
||
description: `${this.plan.plan_title || this.plan.name}`,
|
||
total_amount: this._totalAmount || 0,
|
||
order_type: `新建网站`
|
||
};
|
||
},
|
||
validate() {
|
||
if (!this.subdomain) {
|
||
throw new DashboardError('请输入子域名');
|
||
}
|
||
if (!this._totalAmount || this._totalAmount <= 0) {
|
||
throw new DashboardError('订单金额必须大于0');
|
||
}
|
||
},
|
||
onSuccess(response) {
|
||
if (!response || !response.order) {
|
||
this.formError = '服务器返回数据格式错误';
|
||
return;
|
||
}
|
||
|
||
this.currentOrder = response.order;
|
||
this.$nextTick(() => {
|
||
this.showOrderCheckout = true;
|
||
});
|
||
},
|
||
onError(error) {
|
||
this.formError = error.message || '创建订单失败';
|
||
}
|
||
};
|
||
},
|
||
createSite() {
|
||
return {
|
||
url: 'jcloud.api.site.new',
|
||
makeParams() {
|
||
let appPlans = {};
|
||
for (let app of this.apps) {
|
||
if (app.plan) {
|
||
appPlans[app.app] = app.plan;
|
||
}
|
||
}
|
||
|
||
return {
|
||
site: {
|
||
name: this.subdomain,
|
||
apps: ['jingrow', ...this.apps.map((app) => app.app)],
|
||
localisation_country: this.showLocalisationSelector
|
||
? this.selectedLocalisationCountry?.value
|
||
: null,
|
||
version: this.selectedVersion.name,
|
||
group: this.selectedVersion.group.name,
|
||
cluster: this.cluster,
|
||
plan: this.plan.name,
|
||
share_details_consent: this.shareDetailsConsent,
|
||
selected_app_plans: appPlans,
|
||
order_id: this.currentOrder?.order_id || null
|
||
},
|
||
};
|
||
},
|
||
validate() {
|
||
if (!this.subdomain) {
|
||
throw new DashboardError('请输入子域名');
|
||
}
|
||
|
||
if (!this.agreedToRegionConsent) {
|
||
throw new DashboardError('请同意上述条款以创建站点');
|
||
}
|
||
|
||
if (this.currentOrder && !this.currentOrder.status) {
|
||
throw new DashboardError('未检测到订单状态,无法创建站点');
|
||
}
|
||
},
|
||
onSuccess: (site) => {
|
||
this.$router.push(`/sites/${this.subdomain}.${this.options?.domain}/overview`);
|
||
},
|
||
onError(error) {
|
||
this.formError = error.message || '站点创建失败,请联系客服';
|
||
}
|
||
};
|
||
},
|
||
},
|
||
computed: {
|
||
options() {
|
||
return this.$resources.options.data;
|
||
},
|
||
selectedVersion() {
|
||
return this.options?.versions.find((v) => v.name === this.version);
|
||
},
|
||
availableVersions() {
|
||
if (!this.apps.length || this.bench)
|
||
return this.options.versions.sort((a, b) =>
|
||
b.name.localeCompare(a.name),
|
||
);
|
||
|
||
let commonVersions = this.apps.reduce((acc, app) => {
|
||
if (!acc) return app.sources.map((s) => s.version);
|
||
return acc.filter((v) => app.sources.map((s) => s.version).includes(v));
|
||
}, null);
|
||
|
||
if (this.selectedLocalisationCountry) {
|
||
// temporary override since we don't have localisation app ready for v14
|
||
// TODO: remove this when localisation app is ready for v14
|
||
commonVersions = ['v1'];
|
||
this.version = 'v1';
|
||
}
|
||
|
||
return this.options.versions.map((v) => ({
|
||
...v,
|
||
disabled: !commonVersions.includes(v.name),
|
||
}));
|
||
},
|
||
selectedClusterTitle() {
|
||
return this.selectedVersion?.group?.clusters?.find(
|
||
(c) => c.name === this.cluster,
|
||
)?.title;
|
||
},
|
||
selectedVersionApps() {
|
||
let apps = [];
|
||
|
||
if (!this.bench)
|
||
apps = this.options.app_source_details.sort((a, b) =>
|
||
a.total_installs !== b.total_installs
|
||
? b.total_installs - a.total_installs
|
||
: a.app.localeCompare(b.app),
|
||
);
|
||
else if (!this.selectedVersion?.group?.bench_app_sources) apps = [];
|
||
else
|
||
apps = this.selectedVersion.group.bench_app_sources.map(
|
||
(app_source) => {
|
||
let app_source_details =
|
||
this.options.app_source_details[app_source];
|
||
|
||
let marketplace_details = app_source_details
|
||
? this.options.marketplace_details[app_sourcedetails.app]
|
||
: {};
|
||
|
||
return {
|
||
app_title: app_source,
|
||
...app_source_details,
|
||
...marketplace_details,
|
||
};
|
||
},
|
||
);
|
||
|
||
// 按总安装量排序,然后按名称排序
|
||
return apps.sort((a, b) => {
|
||
if (a.total_installs > b.total_installs) {
|
||
return -1;
|
||
} else if (a.total_installs < b.total_installs) {
|
||
return 1;
|
||
} else {
|
||
return a.app_title.localeCompare(b.app_title);
|
||
}
|
||
});
|
||
},
|
||
selectedVersionAppOptions() {
|
||
return this.selectedVersionApps.filter(
|
||
(app) => !this.localisationAppNames.includes(app.app),
|
||
);
|
||
},
|
||
showLocalisationSelector() {
|
||
if (
|
||
!this.selectedVersionApps ||
|
||
!this.localisationAppNames.length ||
|
||
!this.apps.length
|
||
)
|
||
return false;
|
||
|
||
const appsThatNeedLocalisation = this.selectedVersionApps.filter(
|
||
(app) => app.localisation_apps.length,
|
||
);
|
||
|
||
if (
|
||
appsThatNeedLocalisation.some((app) =>
|
||
this.apps.map((a) => a.app).includes(app.app),
|
||
)
|
||
)
|
||
return true;
|
||
|
||
return false;
|
||
},
|
||
localisationAppNames() {
|
||
if (!this.selectedVersionApps) return [];
|
||
const localisationAppDetails = this.selectedVersionApps.flatMap(
|
||
(app) => app.localisation_apps,
|
||
);
|
||
|
||
return localisationAppDetails
|
||
.map((app) => app?.marketplace_app)
|
||
.filter(Boolean);
|
||
},
|
||
localisationAppCountries() {
|
||
if (!this.selectedVersionApps) return [];
|
||
const localisationAppDetails = this.selectedVersionApps.flatMap(
|
||
(app) => app.localisation_apps,
|
||
);
|
||
return localisationAppDetails.map((app) => ({
|
||
label: app?.country,
|
||
value: app?.country,
|
||
}));
|
||
},
|
||
selectedPlan() {
|
||
if (!plans?.data) return;
|
||
return plans.data.find((p) => p.name === this.plan.name);
|
||
},
|
||
versionAppsMap() {
|
||
const versions = this.availableVersions.map((v) => v.name);
|
||
let problemAppVersions = {};
|
||
if (!this.bench)
|
||
for (let app of this.apps) {
|
||
const appVersions = app.sources.map((s) => s.version);
|
||
const problemVersions = versions.filter(
|
||
(version) => !appVersions.includes(version),
|
||
);
|
||
for (let version of problemVersions) {
|
||
if (!problemAppVersions[version]) {
|
||
problemAppVersions[version] = [];
|
||
}
|
||
problemAppVersions[version].push(app.app_title);
|
||
}
|
||
}
|
||
return problemAppVersions;
|
||
},
|
||
breadcrumbs() {
|
||
if (this.bench) {
|
||
let group = getCachedDocumentResource('Release Group', this.bench);
|
||
return [
|
||
{ label: '站点分组', route: '/groups' },
|
||
{
|
||
label: group ? group.pg.title : this.bench,
|
||
route: {
|
||
name: 'Release Group Detail',
|
||
params: { name: this.bench },
|
||
},
|
||
},
|
||
{
|
||
label: '新建站点',
|
||
route: {
|
||
name: 'Release Group New Site',
|
||
params: { bench: this.bench },
|
||
},
|
||
},
|
||
];
|
||
}
|
||
return [
|
||
{ label: '站点', route: '/sites' },
|
||
{ label: '新建站点', route: '/sites/new' },
|
||
];
|
||
},
|
||
_totalAmount() {
|
||
let total =
|
||
this.$team.pg.currency == 'CNY'
|
||
? this.selectedPlan.price_cny
|
||
: this.selectedPlan.price_usd;
|
||
|
||
for (let app of this.apps.filter((app) => app.plan)) {
|
||
total +=
|
||
this.$team.pg.currency == 'CNY'
|
||
? app.plan.price_cny
|
||
: app.plan.price_usd;
|
||
}
|
||
return total;
|
||
},
|
||
totalAmount() {
|
||
return this.$format.userCurrency(this._totalAmount);
|
||
},
|
||
totalPerMonth() {
|
||
return this.totalAmount;
|
||
},
|
||
totalPerDay() {
|
||
return this.$format.userCurrency(
|
||
this.$format.pricePerDay(this._totalAmount),
|
||
);
|
||
},
|
||
siteSummaryOptions() {
|
||
let appPlans = [];
|
||
for (let app of this.apps) {
|
||
appPlans.push(
|
||
`${
|
||
this.selectedVersionApps.find((a) => a.app === app.app).app_title
|
||
} ${
|
||
app.plan?.price_cny
|
||
? `- <span class="text-gray-600">${this.$format.userCurrency(
|
||
this.$team.pg.currency == 'CNY'
|
||
? app.plan.price_cny
|
||
: app.plan.price_usd,
|
||
)}</span>`
|
||
: ''
|
||
}`,
|
||
);
|
||
}
|
||
|
||
return [
|
||
{
|
||
label: 'Jingrow 版本',
|
||
value: this.selectedVersion?.name,
|
||
},
|
||
{
|
||
label: '区域',
|
||
value: this.selectedClusterTitle,
|
||
},
|
||
{
|
||
label: '站点 URL',
|
||
value: `${this.subdomain}.${this.options?.domain}`,
|
||
},
|
||
{
|
||
label: '站点计划',
|
||
value: this.selectedPlan.plan_title || this.plan.name,
|
||
},
|
||
{
|
||
label: '总计',
|
||
value: this.totalAmount,
|
||
condition: () => this._totalAmount,
|
||
},
|
||
];
|
||
},
|
||
isSubdomainValid() {
|
||
return this.$resources.subdomainExists.data === true &&
|
||
!this.$resources.subdomainExists.error &&
|
||
this.subdomain;
|
||
},
|
||
},
|
||
methods: {
|
||
async getClosestCluster() {
|
||
if (this.closestCluster) return this.closestCluster;
|
||
|
||
// 优先检查是否有中国地区的集群
|
||
const chinaCluster = this.selectedVersion?.group?.clusters.find(
|
||
c => c.title && c.title.includes('中国')
|
||
);
|
||
|
||
if (chinaCluster) {
|
||
this.closestCluster = chinaCluster.name;
|
||
return this.closestCluster;
|
||
}
|
||
|
||
let proxyServers = this.selectedVersion?.group?.clusters
|
||
.flatMap((c) => c.proxy_server || [])
|
||
.map((server) => server.name);
|
||
|
||
if (proxyServers.length > 0) {
|
||
this.findingClosestServer = true;
|
||
let promises = proxyServers.map((server) => this.getPingTime(server));
|
||
let results = await Promise.allSettled(promises);
|
||
let fastestServer = results.reduce((a, b) =>
|
||
a.value.pingTime < b.value.pingTime ? a : b,
|
||
);
|
||
let closestServer = fastestServer.value.server;
|
||
let closestCluster = this.selectedVersion?.group?.clusters.find(
|
||
(c) => c.proxy_server?.name === closestServer,
|
||
);
|
||
if (!this.closestCluster && closestCluster) {
|
||
this.closestCluster = closestCluster.name;
|
||
} else if (this.selectedVersion?.group?.clusters.length > 0) {
|
||
// 如果找不到最快的服务器,默认选择第一个集群
|
||
this.closestCluster = this.selectedVersion.group.clusters[0].name;
|
||
}
|
||
this.findingClosestServer = false;
|
||
} else if (this.selectedVersion?.group?.clusters.length > 0) {
|
||
this.closestCluster = this.selectedVersion.group.clusters[0].name;
|
||
}
|
||
return this.closestCluster;
|
||
},
|
||
async getPingTime(server) {
|
||
let pingTime = 999999;
|
||
try {
|
||
let t1 = new Date().getTime();
|
||
let r = await fetch(`https://${server}`);
|
||
let t2 = new Date().getTime();
|
||
pingTime = t2 - t1;
|
||
} catch (error) {
|
||
console.warn(error);
|
||
}
|
||
return { server, pingTime };
|
||
},
|
||
autoSelectVersion() {
|
||
if (!this.availableVersions) return null;
|
||
|
||
return this.availableVersions
|
||
.sort((a, b) => b.name.localeCompare(a.name))
|
||
.find((v) => !v.disabled)?.name;
|
||
},
|
||
async checkBalanceAndCreateSite() {
|
||
try {
|
||
this.formError = null;
|
||
|
||
if (!this.agreedToRegionConsent) {
|
||
this.formError = '请同意上述条款以创建站点';
|
||
return;
|
||
}
|
||
|
||
if (!this.subdomain) {
|
||
this.formError = '请输入子域名';
|
||
return;
|
||
}
|
||
|
||
if (!this.isSubdomainValid) {
|
||
this.formError = '子域名无效或已被占用';
|
||
return;
|
||
}
|
||
|
||
this.$resources.createOrder.submit();
|
||
} catch (error) {
|
||
this.formError = `创建订单失败: ${error.message || '请稍后再试'}`;
|
||
}
|
||
},
|
||
onPaymentSuccess(order) {
|
||
this.showOrderCheckout = false;
|
||
this.currentOrder = {
|
||
...this.currentOrder,
|
||
...order,
|
||
status: '已支付'
|
||
};
|
||
this.siteFromOrder = order.site;
|
||
this.$resources.createSite.submit();
|
||
},
|
||
onPaymentError(error) {
|
||
this.formError = "支付失败: " + (error.message || "未知错误");
|
||
|
||
if (error.message && error.message.includes('余额不足')) {
|
||
this.requiredAmount = this._totalAmount || 0;
|
||
this.showBalanceMessage = true;
|
||
this.showAddPrepaidCreditsDialog = true;
|
||
}
|
||
|
||
this.showOrderCheckout = false;
|
||
},
|
||
resetCheckoutState() {
|
||
this.showOrderCheckout = false;
|
||
this.currentOrder = null;
|
||
}
|
||
},
|
||
};
|
||
</script>
|
||
<style scoped>
|
||
.checkbox:deep(label) {
|
||
color: theme('colors.gray.700') !important;
|
||
line-height: 1.5;
|
||
}
|
||
</style> |