2025-06-27 18:50:23 +08:00

427 lines
12 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>
<Card title="个人资料" v-if="user" class="mx-auto max-w-3xl">
<div class="flex items-center border-b pb-3">
<div class="relative">
<Avatar size="2xl" :label="user.first_name" :image="user.user_image" />
<FileUploader
@success="onProfilePhotoChange"
fileTypes="image/*"
:upload-args="{
pagetype: 'User',
docname: user.name,
method: 'jcloud.api.account.update_profile_picture',
}"
>
<template v-slot="{ openFileSelector, uploading, progress, error }">
<div class="ml-4">
<button
@click="openFileSelector()"
class="absolute inset-0 grid h-10 w-full place-items-center rounded-full bg-black text-xs font-medium text-white opacity-0 transition hover:opacity-50 focus:opacity-50 focus:outline-none"
:class="{ 'opacity-50': uploading }"
>
<span v-if="uploading">{{ progress }}%</span>
<span v-else>编辑</span>
</button>
</div>
</template>
</FileUploader>
</div>
<div class="ml-4">
<h3 class="text-base font-semibold">
{{ user.first_name }} {{ user.last_name }}
</h3>
<p class="mt-1 text-base text-gray-600">用户名{{ user.username }}</p>
<p class="mt-1 text-base text-gray-600">手机{{ user.mobile_no }}</p>
<p class="mt-1 text-base text-gray-600">邮箱{{ user.email }}</p>
</div>
<div class="ml-auto">
<Button icon-left="edit" @click="showProfileEditDialog = true">
编辑
</Button>
</div>
</div>
<div>
<ListItem
title="应用市场开发者"
subtitle="开发者可以在应用市场发布自己的应用,供用户付费或免费订阅。"
v-if="!$team.pg.is_developer"
>
<template #actions>
<Button @click="confirmPublisherAccount">
<span>成为开发者</span>
</Button>
</template>
</ListItem>
<ListItem
:title="user.is_2fa_enabled ? '禁用双因素认证' : '启用双因素认证'"
:subtitle="
user.is_2fa_enabled
? '为您的账户禁用双因素认证'
: '为您的账户启用双因素认证以增加额外的安全层'
"
>
<template #actions>
<Button @click="show2FADialog = true">
{{ user.is_2fa_enabled ? '禁用' : '启用' }}
</Button>
</template>
</ListItem>
<ListItem
:title="teamEnabled ? '禁用账户' : '启用账户'"
:subtitle="
teamEnabled
? '禁用您的账户并停止计费'
: '启用您的账户并恢复计费'
"
>
<template #actions>
<Button
@click="
() => {
if (teamEnabled) {
showDisableAccountDialog = true;
} else {
showEnableAccountDialog = true;
}
}
"
>
<span :class="{ 'text-red-600': teamEnabled }">{{
teamEnabled ? '禁用' : '启用'
}}</span>
</Button>
</template>
</ListItem>
</div>
<Dialog
:options="{
title: '更新个人资料信息',
actions: [
{
variant: 'solid',
label: '保存更改',
onClick: () => $resources.updateProfile.submit(),
},
],
}"
v-model="showProfileEditDialog"
>
<template v-slot:body-content>
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<FormControl label="个人名称" v-model="user.first_name" />
<FormControl label="用户名" v-model="user.username" />
<FormControl label="手机" v-model="user.mobile_no" />
<FormControl label="邮箱" v-model="user.email" />
</div>
<ErrorMessage class="mt-4" :message="$resources.updateProfile.error" />
</template>
</Dialog>
<Dialog
:options="{
title: '禁用账户',
actions: [
{
label: '禁用账户',
variant: 'solid',
theme: 'red',
loading: $resources.disableAccount.loading,
onClick: () => deactivateAccount(disableAccount2FACode),
},
],
}"
v-model="showDisableAccountDialog"
>
<template v-slot:body-content>
<div class="prose text-base">
确认此操作后
<ul>
<li>您的账户将被禁用</li>
<li>
您激活的站点将立即暂停并在一周后被删除
</li>
<li>您的账户计费将停止</li>
</ul>
您可以稍后登录以重新启用您的账户您要继续吗
</div>
<FormControl
v-if="user.is_2fa_enabled"
class="mt-4"
label="输入您的2FA代码以确认"
v-model="disableAccount2FACode"
/>
<ErrorMessage class="mt-2" :message="$resources.disableAccount.error" />
</template>
</Dialog>
<Dialog
:options="{
title: '启用账户',
actions: [
{
label: '启用账户',
variant: 'solid',
loading: $resources.enableAccount.loading,
onClick: () => $resources.enableAccount.submit(),
},
],
}"
v-model="showEnableAccountDialog"
>
<template v-slot:body-content>
<div class="prose text-base">
确认此操作后
<ul>
<li>您的账户将被启用</li>
<li>您暂停的站点将重新激活</li>
<li>您的账户计费将恢复</li>
</ul>
您要继续吗
</div>
<ErrorMessage class="mt-2" :message="$resources.enableAccount.error" />
</template>
</Dialog>
<AddPrepaidCreditsDialog
:showMessage="showMessage"
v-if="showAddPrepaidCreditsDialog"
v-model="showAddPrepaidCreditsDialog"
@success="reloadAccount"
/>
</Card>
<TFADialog v-model="show2FADialog" />
</template>
<script>
import { toast } from 'vue-sonner';
import { defineAsyncComponent, h } from 'vue';
import FileUploader from '@/components/FileUploader.vue';
import { confirmDialog, renderDialog } from '../../../utils/components';
import TFADialog from './TFADialog.vue';
import router from '../../../router';
import AddPrepaidCreditsDialog from '../../billing/AddPrepaidCreditsDialog.vue';
export default {
name: 'AccountProfile',
components: {
TFADialog,
FileUploader,
AddPrepaidCreditsDialog,
},
data() {
return {
show2FADialog: false,
disableAccount2FACode: '',
showProfileEditDialog: false,
showEnableAccountDialog: false,
showDisableAccountDialog: false,
showAddPrepaidCreditsDialog: false,
showActiveServersDialog: false,
showMessage: false,
draftInvoice: {},
unpaidInvoices: [] | {},
};
},
computed: {
teamEnabled() {
return this.$team.pg.enabled;
},
user() {
return this.$team?.pg?.user_info;
},
},
resources: {
updateProfile() {
let { first_name, last_name, email, username, mobile_no } = this.user;
return {
url: 'jcloud.api.account.update_profile',
params: {
first_name,
last_name,
email,
username,
mobile_no,
},
onSuccess() {
this.showProfileEditDialog = false;
this.notifySuccess();
},
};
},
disableAccount: {
url: 'jcloud.api.account.disable_account',
onSuccess() {
this.showDisableAccountDialog = false;
const ChurnFeedbackDialog = defineAsyncComponent(
() => import('../../ChurnFeedbackDialog.vue'),
);
renderDialog(
h(ChurnFeedbackDialog, {
team: this.$team.pg.name,
onUpdated: () => {
toast.success('您的反馈已成功提交');
},
}),
);
toast.success('您的账户已成功禁用');
this.reloadAccount();
},
},
enableAccount: {
url: 'jcloud.api.account.enable_account',
onSuccess() {
toast.success('您的账户已成功启用');
this.reloadAccount();
this.showEnableAccountDialog = false;
},
},
upcomingInvoice: {
url: 'jcloud.api.billing.upcoming_invoice',
auto: true,
onSuccess(data) {
this.draftInvoice = data.upcoming_invoice;
},
},
unPaidInvoices: {
url: 'jcloud.api.billing.get_unpaid_invoices',
auto: true,
onSuccess(data) {
this.unpaidInvoices = data;
},
},
hasActiveServers() {
return {
url: 'jcloud.api.account.has_active_servers',
auto: true,
params: {
team: this.$team.pg.name,
},
onSuccess(data) {
if (data) {
this.showActiveServersDialog = true;
}
},
};
},
},
methods: {
reloadAccount() {
this.$team.reload();
},
onProfilePhotoChange() {
this.reloadAccount();
this.notifySuccess();
},
notifySuccess() {
toast.success('您的个人资料已成功更新');
},
deactivateAccount(disableAccount2FACode) {
const currency = this.$team.pg.currency;
const minAmount = currency === 'CNY' ? 0.01 : 0.01;
if (this.draftInvoice && this.draftInvoice.amount_due > minAmount) {
const finalizeInvoicesDialog = defineAsyncComponent(
() => import('../../billing/FinalizeInvoicesDialog.vue'),
);
renderDialog(h(finalizeInvoicesDialog));
} else if (this.unpaidInvoices) {
if (this.unpaidInvoices.length > 1) {
this.showDisableAccountDialog = false;
if (this.$team.pg.payment_mode === 'Prepaid Credits') {
this.showAddPrepaidCreditsDialog = true;
} else {
confirmDialog({
title: '多张未支付发票',
message:
'您有多张未支付的发票。请从发票页面支付它们',
primaryAction: {
label: '前往发票',
variant: 'solid',
onClick: ({ hide }) => {
router.push({ name: 'BillingInvoices' });
hide();
},
},
});
}
} else {
let invoice = this.unpaidInvoices;
if (invoice.amount_due > minAmount) {
this.showDisableAccountDialog = false;
confirmDialog({
title: '清除未支付发票',
message: `您有一张未支付的发票,金额为${
invoice.currency === 'CNY' ? '¥' : '$'
} ${
invoice.amount_due
}。请在停用账户前结清它。`,
primaryAction: {
label: '立即结算',
variant: 'solid',
onClick: ({ hide }) => {
if (
invoice.stripe_invoice_url &&
this.$team.pg.payment_mode === 'Card'
) {
window.open(
`/api/action/jcloud.api.client.run_pg_method?dt=Invoice&dn=${invoice.name}&method=stripe_payment_url`,
);
} else {
this.showAddPrepaidCreditsDialog = true;
}
hide();
},
},
});
}
}
}
// validate if any active servers
if (this.showActiveServersDialog) {
const activeServersDialog = defineAsyncComponent(
() => import('../../ActiveServersDialog.vue'),
);
renderDialog(h(activeServersDialog));
return;
}
this.$resources.disableAccount.submit({
totp_code: disableAccount2FACode,
});
},
confirmPublisherAccount() {
confirmDialog({
title: '成为市场应用开发者?',
message:
'确认后,您将能够将应用发布到我们的市场。',
onSuccess: ({ hide }) => {
toast.promise(
this.$team.setValue.submit(
{
is_developer: 1,
},
{
onSuccess: () => {
hide();
this.$router.push({
name: 'Marketplace App List',
});
},
onError(e) {
console.error(e);
},
},
),
{
success: '您现在可以将应用发布到我们的市场',
error: '标记您为开发者失败',
loading: '正在将您设为开发者...',
},
);
},
});
},
},
};
</script>