937 lines
23 KiB
Vue
937 lines
23 KiB
Vue
<template>
|
||
<div v-if="user" class="profile-container">
|
||
<n-space vertical :size="24">
|
||
<!-- 个人资料卡片 -->
|
||
<n-card :title="$t('Profile')" class="profile-card">
|
||
<n-space vertical :size="20">
|
||
<div class="profile-header">
|
||
<div class="profile-avatar-wrapper">
|
||
<n-avatar
|
||
:size="avatarSize"
|
||
:src="user.user_image"
|
||
round
|
||
class="profile-avatar"
|
||
>
|
||
{{ user.first_name?.[0]?.toUpperCase() || 'A' }}
|
||
</n-avatar>
|
||
<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 }">
|
||
<n-button
|
||
tertiary
|
||
circle
|
||
size="small"
|
||
class="avatar-edit-btn"
|
||
:loading="uploading"
|
||
@click="openFileSelector()"
|
||
>
|
||
<template #icon>
|
||
<n-icon>
|
||
<EditIcon />
|
||
</n-icon>
|
||
</template>
|
||
</n-button>
|
||
</template>
|
||
</FileUploader>
|
||
</div>
|
||
<div class="profile-info">
|
||
<h2 class="profile-name">
|
||
{{ user.first_name }} {{ user.last_name }}
|
||
</h2>
|
||
<div class="profile-details">
|
||
<div class="profile-detail-item">
|
||
<span class="profile-detail-label">{{ $t('Username') }}:</span>
|
||
<span class="profile-detail-value">{{ user.username }}</span>
|
||
</div>
|
||
<div class="profile-detail-item">
|
||
<span class="profile-detail-label">{{ $t('Phone') }}:</span>
|
||
<span class="profile-detail-value">{{ user.mobile_no || '-' }}</span>
|
||
</div>
|
||
<div class="profile-detail-item">
|
||
<span class="profile-detail-label">{{ $t('Email') }}:</span>
|
||
<span class="profile-detail-value">{{ user.email }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="profile-actions">
|
||
<n-button type="primary" @click="showProfileEditDialog = true" :block="isMobile">
|
||
<template #icon>
|
||
<n-icon><EditIcon /></n-icon>
|
||
</template>
|
||
{{ $t('Edit') }}
|
||
</n-button>
|
||
</div>
|
||
</div>
|
||
</n-space>
|
||
</n-card>
|
||
|
||
<!-- 功能设置卡片 -->
|
||
<n-card class="settings-card">
|
||
<n-list>
|
||
<n-list-item v-if="!$team.pg.is_developer">
|
||
<n-thing>
|
||
<template #header>
|
||
<span class="text-base font-medium">{{ $t('Marketplace Developer') }}</span>
|
||
</template>
|
||
<template #description>
|
||
<span class="text-sm text-gray-600">
|
||
{{ $t('Developers can publish their apps on the marketplace for users to subscribe to, either paid or free.') }}
|
||
</span>
|
||
</template>
|
||
<template #action>
|
||
<n-button type="primary" @click="confirmPublisherAccount">
|
||
{{ $t('Become a Developer') }}
|
||
</n-button>
|
||
</template>
|
||
</n-thing>
|
||
</n-list-item>
|
||
|
||
<n-list-item>
|
||
<n-thing>
|
||
<template #header>
|
||
<span class="text-base font-medium">{{ twoFactorAuthTitle }}</span>
|
||
</template>
|
||
<template #description>
|
||
<span class="text-sm text-gray-600">{{ twoFactorAuthSubtitle }}</span>
|
||
</template>
|
||
<template #action>
|
||
<n-button @click="show2FADialog = true">
|
||
{{ twoFactorAuthButtonLabel }}
|
||
</n-button>
|
||
</template>
|
||
</n-thing>
|
||
</n-list-item>
|
||
|
||
<n-list-item>
|
||
<n-thing>
|
||
<template #header>
|
||
<span class="text-base font-medium">{{ $t('Reset Password') }}</span>
|
||
</template>
|
||
<template #description>
|
||
<span class="text-sm text-gray-600">{{ $t('Change your account login password') }}</span>
|
||
</template>
|
||
<template #action>
|
||
<n-button @click="showResetPasswordDialog = true">
|
||
{{ $t('Reset Password') }}
|
||
</n-button>
|
||
</template>
|
||
</n-thing>
|
||
</n-list-item>
|
||
|
||
<n-list-item>
|
||
<n-thing>
|
||
<template #header>
|
||
<span class="text-base font-medium">{{ accountStatusTitle }}</span>
|
||
</template>
|
||
<template #description>
|
||
<span class="text-sm text-gray-600">{{ accountStatusSubtitle }}</span>
|
||
</template>
|
||
<template #action>
|
||
<n-button
|
||
:type="teamEnabled ? 'error' : 'primary'"
|
||
@click="
|
||
() => {
|
||
if (teamEnabled) {
|
||
showDisableAccountDialog = true;
|
||
} else {
|
||
showEnableAccountDialog = true;
|
||
}
|
||
}
|
||
"
|
||
>
|
||
{{ accountStatusButtonLabel }}
|
||
</n-button>
|
||
</template>
|
||
</n-thing>
|
||
</n-list-item>
|
||
</n-list>
|
||
</n-card>
|
||
|
||
<!-- 推荐有礼组件 -->
|
||
<AccountReferral />
|
||
|
||
<!-- Jingrow 合作伙伴组件 -->
|
||
<AccountPartner />
|
||
</n-space>
|
||
|
||
<!-- 编辑资料对话框 -->
|
||
<n-modal
|
||
v-model:show="showProfileEditDialog"
|
||
preset="card"
|
||
:title="$t('Update Profile Information')"
|
||
:style="modalStyle"
|
||
:mask-closable="true"
|
||
:close-on-esc="true"
|
||
class="profile-edit-modal"
|
||
>
|
||
<template #header>
|
||
<span class="text-lg font-semibold">{{ $t('Update Profile Information') }}</span>
|
||
</template>
|
||
<n-form :model="user" label-placement="top" class="mt-4">
|
||
<n-space vertical :size="20">
|
||
<n-form-item :label="$t('First Name')">
|
||
<n-input
|
||
v-model:value="user.first_name"
|
||
:size="inputSize"
|
||
class="w-full"
|
||
/>
|
||
</n-form-item>
|
||
<n-form-item :label="$t('Username')">
|
||
<n-input
|
||
v-model:value="user.username"
|
||
:size="inputSize"
|
||
class="w-full"
|
||
/>
|
||
</n-form-item>
|
||
<n-form-item :label="$t('Phone')">
|
||
<n-input
|
||
v-model:value="user.mobile_no"
|
||
:size="inputSize"
|
||
class="w-full"
|
||
/>
|
||
</n-form-item>
|
||
<n-form-item :label="$t('Email')">
|
||
<n-input
|
||
v-model:value="user.email"
|
||
type="email"
|
||
:size="inputSize"
|
||
class="w-full"
|
||
/>
|
||
</n-form-item>
|
||
<n-alert
|
||
v-if="$resources.updateProfile.error"
|
||
type="error"
|
||
:title="$resources.updateProfile.error"
|
||
/>
|
||
</n-space>
|
||
</n-form>
|
||
<template #action>
|
||
<n-space :vertical="isMobile" :size="isMobile ? 12 : 16" class="w-full">
|
||
<n-button
|
||
@click="showProfileEditDialog = false"
|
||
:block="isMobile"
|
||
:size="buttonSize"
|
||
>
|
||
{{ $t('Cancel') }}
|
||
</n-button>
|
||
<n-button
|
||
type="primary"
|
||
@click="handleSaveProfile"
|
||
:loading="$resources.updateProfile.loading"
|
||
:block="isMobile"
|
||
:size="buttonSize"
|
||
>
|
||
{{ $t('Save Changes') }}
|
||
</n-button>
|
||
</n-space>
|
||
</template>
|
||
</n-modal>
|
||
|
||
<!-- 禁用账户对话框 -->
|
||
<n-modal
|
||
v-model:show="showDisableAccountDialog"
|
||
preset="dialog"
|
||
:title="$t('Disable Account')"
|
||
:positive-text="$t('Disable Account')"
|
||
:positive-button-props="{ type: 'error' }"
|
||
:loading="$resources.disableAccount.loading"
|
||
:mask-closable="true"
|
||
:close-on-esc="true"
|
||
@positive-click="() => deactivateAccount(disableAccount2FACode)"
|
||
>
|
||
<div class="py-4">
|
||
<p class="text-base mb-4">{{ $t('After confirming this action:') }}</p>
|
||
<ul class="list-disc list-inside space-y-2 text-sm text-gray-700 mb-4">
|
||
<li>{{ $t('Your account will be disabled') }}</li>
|
||
<li>{{ $t('Your activated sites will be suspended immediately and deleted after one week.') }}</li>
|
||
<li>{{ $t('Your account billing will stop') }}</li>
|
||
</ul>
|
||
<p class="text-base mb-4">{{ $t('You can log in later to re-enable your account. Do you want to continue?') }}</p>
|
||
<n-form-item
|
||
v-if="user.is_2fa_enabled"
|
||
:label="$t('Enter your 2FA code to confirm')"
|
||
class="mt-4"
|
||
>
|
||
<n-input v-model:value="disableAccount2FACode" />
|
||
</n-form-item>
|
||
<n-alert
|
||
v-if="$resources.disableAccount.error"
|
||
type="error"
|
||
class="mt-4"
|
||
:title="$resources.disableAccount.error"
|
||
/>
|
||
</div>
|
||
</n-modal>
|
||
|
||
<!-- 启用账户对话框 -->
|
||
<n-modal
|
||
v-model:show="showEnableAccountDialog"
|
||
preset="dialog"
|
||
:title="$t('Enable Account')"
|
||
:positive-text="$t('Enable Account')"
|
||
:loading="$resources.enableAccount.loading"
|
||
:mask-closable="true"
|
||
:close-on-esc="true"
|
||
@positive-click="() => $resources.enableAccount.submit()"
|
||
>
|
||
<div class="py-4">
|
||
<p class="text-base mb-4">{{ $t('After confirming this action:') }}</p>
|
||
<ul class="list-disc list-inside space-y-2 text-sm text-gray-700 mb-4">
|
||
<li>{{ $t('Your account will be enabled') }}</li>
|
||
<li>{{ $t('Your suspended sites will be reactivated') }}</li>
|
||
<li>{{ $t('Your account billing will resume') }}</li>
|
||
</ul>
|
||
<p class="text-base">{{ $t('Do you want to continue?') }}</p>
|
||
<n-alert
|
||
v-if="$resources.enableAccount.error"
|
||
type="error"
|
||
class="mt-4"
|
||
:title="$resources.enableAccount.error"
|
||
/>
|
||
</div>
|
||
</n-modal>
|
||
|
||
<AddPrepaidCreditsDialog
|
||
:showMessage="showMessage"
|
||
v-if="showAddPrepaidCreditsDialog"
|
||
v-model="showAddPrepaidCreditsDialog"
|
||
@success="reloadAccount"
|
||
/>
|
||
<ResetPasswordDialog
|
||
v-if="showResetPasswordDialog"
|
||
v-model="showResetPasswordDialog"
|
||
/>
|
||
<TFADialog v-model="show2FADialog" />
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { toast } from 'vue-sonner';
|
||
import { defineAsyncComponent, h } from 'vue';
|
||
import {
|
||
NCard,
|
||
NSpace,
|
||
NButton,
|
||
NAvatar,
|
||
NList,
|
||
NListItem,
|
||
NThing,
|
||
NModal,
|
||
NForm,
|
||
NFormItem,
|
||
NFormItemGi,
|
||
NInput,
|
||
NGrid,
|
||
NAlert,
|
||
NIcon,
|
||
} from 'naive-ui';
|
||
import FileUploader from '@/components/FileUploader.vue';
|
||
import { confirmDialog, renderDialog } from '../../../utils/components';
|
||
import TFADialog from './TFADialog.vue';
|
||
import ResetPasswordDialog from './ResetPasswordDialog.vue';
|
||
import router from '../../../router';
|
||
import AddPrepaidCreditsDialog from '../../billing/AddPrepaidCreditsDialog.vue';
|
||
import AccountReferral from './AccountReferral.vue';
|
||
import AccountPartner from './AccountPartner.vue';
|
||
import EditIcon from '~icons/lucide/edit';
|
||
|
||
export default {
|
||
name: 'AccountProfile',
|
||
components: {
|
||
NCard,
|
||
NSpace,
|
||
NButton,
|
||
NAvatar,
|
||
NList,
|
||
NListItem,
|
||
NThing,
|
||
NModal,
|
||
NForm,
|
||
NFormItem,
|
||
NFormItemGi,
|
||
NInput,
|
||
NGrid,
|
||
NAlert,
|
||
NIcon,
|
||
TFADialog,
|
||
ResetPasswordDialog,
|
||
FileUploader,
|
||
AddPrepaidCreditsDialog,
|
||
AccountReferral,
|
||
AccountPartner,
|
||
EditIcon,
|
||
},
|
||
data() {
|
||
return {
|
||
show2FADialog: false,
|
||
showResetPasswordDialog: false,
|
||
disableAccount2FACode: '',
|
||
showProfileEditDialog: false,
|
||
showEnableAccountDialog: false,
|
||
showDisableAccountDialog: false,
|
||
showAddPrepaidCreditsDialog: false,
|
||
showActiveServersDialog: false,
|
||
showMessage: false,
|
||
draftInvoice: {},
|
||
unpaidInvoices: [] | {},
|
||
windowWidth: window.innerWidth,
|
||
};
|
||
},
|
||
computed: {
|
||
teamEnabled() {
|
||
return this.$team.pg.enabled;
|
||
},
|
||
user() {
|
||
return this.$team?.pg?.user_info;
|
||
},
|
||
isMobile() {
|
||
return this.windowWidth <= 768;
|
||
},
|
||
avatarSize() {
|
||
return this.isMobile ? 80 : 96;
|
||
},
|
||
modalStyle() {
|
||
return {
|
||
width: this.isMobile ? '95vw' : '800px',
|
||
maxWidth: this.isMobile ? '95vw' : '90vw',
|
||
};
|
||
},
|
||
inputSize() {
|
||
return this.isMobile ? 'medium' : 'large';
|
||
},
|
||
buttonSize() {
|
||
return this.isMobile ? 'medium' : 'medium';
|
||
},
|
||
twoFactorAuthTitle() {
|
||
return this.user.is_2fa_enabled
|
||
? this.$t('Disable Two-Factor Authentication')
|
||
: this.$t('Enable Two-Factor Authentication');
|
||
},
|
||
twoFactorAuthSubtitle() {
|
||
return this.user.is_2fa_enabled
|
||
? this.$t('Disable two-factor authentication for your account')
|
||
: this.$t('Enable two-factor authentication for your account to add an extra layer of security');
|
||
},
|
||
twoFactorAuthButtonLabel() {
|
||
return this.user.is_2fa_enabled ? this.$t('Disable') : this.$t('Enable');
|
||
},
|
||
accountStatusTitle() {
|
||
return this.teamEnabled ? this.$t('Disable Account') : this.$t('Enable Account');
|
||
},
|
||
accountStatusSubtitle() {
|
||
return this.teamEnabled
|
||
? this.$t('Disable your account and stop billing')
|
||
: this.$t('Enable your account and resume billing');
|
||
},
|
||
accountStatusButtonLabel() {
|
||
return this.teamEnabled ? this.$t('Disable') : this.$t('Enable');
|
||
},
|
||
},
|
||
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();
|
||
},
|
||
onError() {
|
||
// Error handling
|
||
},
|
||
};
|
||
},
|
||
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(this.$t('Your feedback has been submitted successfully'));
|
||
},
|
||
}),
|
||
);
|
||
toast.success(this.$t('Your account has been disabled successfully'));
|
||
this.reloadAccount();
|
||
},
|
||
},
|
||
enableAccount: {
|
||
url: 'jcloud.api.account.enable_account',
|
||
onSuccess() {
|
||
toast.success(this.$t('Your account has been enabled successfully'));
|
||
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;
|
||
}
|
||
},
|
||
};
|
||
},
|
||
},
|
||
mounted() {
|
||
this.handleResize();
|
||
window.addEventListener('resize', this.handleResize);
|
||
},
|
||
beforeUnmount() {
|
||
window.removeEventListener('resize', this.handleResize);
|
||
},
|
||
methods: {
|
||
handleResize() {
|
||
this.windowWidth = window.innerWidth;
|
||
},
|
||
handleSaveProfile() {
|
||
// 提交资源,onSuccess 中会关闭对话框
|
||
this.$resources.updateProfile.submit();
|
||
},
|
||
reloadAccount() {
|
||
this.$team.reload();
|
||
},
|
||
onProfilePhotoChange() {
|
||
this.reloadAccount();
|
||
this.notifySuccess();
|
||
},
|
||
notifySuccess() {
|
||
toast.success(this.$t('Your profile has been updated successfully'));
|
||
},
|
||
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: this.$t('Multiple Unpaid Invoices'),
|
||
message: this.$t('You have multiple unpaid invoices. Please pay them from the invoices page.'),
|
||
primaryAction: {
|
||
label: this.$t('Go to Invoices'),
|
||
variant: 'solid',
|
||
onClick: ({ hide }) => {
|
||
router.push({ name: 'BillingInvoices' });
|
||
hide();
|
||
},
|
||
},
|
||
});
|
||
}
|
||
} else {
|
||
let invoice = this.unpaidInvoices;
|
||
if (invoice.amount_due > minAmount) {
|
||
this.showDisableAccountDialog = false;
|
||
confirmDialog({
|
||
title: this.$t('Clear Unpaid Invoice'),
|
||
message: this.$t('You have an unpaid invoice of {amount}. Please settle it before deactivating your account.', {
|
||
amount: `${invoice.currency === 'CNY' ? '¥' : '$'} ${invoice.amount_due}`
|
||
}),
|
||
primaryAction: {
|
||
label: this.$t('Settle Now'),
|
||
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: this.$t('Become a Marketplace Developer?'),
|
||
message: this.$t('After confirmation, you will be able to publish apps to our marketplace.'),
|
||
primaryAction: {
|
||
label: this.$t('Confirm'),
|
||
variant: 'solid'
|
||
},
|
||
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: this.$t('You can now publish apps to our marketplace'),
|
||
error: this.$t('Failed to mark you as a developer'),
|
||
loading: this.$t('Setting you as a developer...'),
|
||
},
|
||
);
|
||
},
|
||
});
|
||
},
|
||
},
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.profile-container {
|
||
width: 100%;
|
||
}
|
||
|
||
.profile-card {
|
||
border-radius: 12px;
|
||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||
transition: box-shadow 0.2s ease;
|
||
}
|
||
|
||
.profile-card:hover {
|
||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.settings-card {
|
||
border-radius: 12px;
|
||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
||
transition: box-shadow 0.2s ease;
|
||
}
|
||
|
||
.settings-card:hover {
|
||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
/* 个人资料头部布局 */
|
||
.profile-header {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 24px;
|
||
width: 100%;
|
||
}
|
||
|
||
.profile-avatar-wrapper {
|
||
position: relative;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.profile-avatar {
|
||
border: 3px solid #f3f4f6;
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.profile-avatar:hover {
|
||
transform: scale(1.05);
|
||
}
|
||
|
||
.avatar-edit-btn {
|
||
position: absolute;
|
||
bottom: -4px;
|
||
right: -4px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.profile-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.profile-name {
|
||
font-size: 24px;
|
||
font-weight: 600;
|
||
color: #111827;
|
||
margin: 0 0 16px 0;
|
||
line-height: 1.2;
|
||
word-break: break-word;
|
||
}
|
||
|
||
.profile-details {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.profile-detail-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.profile-detail-label {
|
||
font-size: 14px;
|
||
color: #6b7280;
|
||
flex-shrink: 0;
|
||
min-width: 80px;
|
||
}
|
||
|
||
.profile-detail-value {
|
||
font-size: 14px;
|
||
color: #111827;
|
||
word-break: break-word;
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.profile-actions {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 弹窗样式 */
|
||
:deep(.profile-edit-modal .n-card) {
|
||
width: 800px;
|
||
max-width: 90vw;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
:deep(.profile-edit-modal .n-card-body) {
|
||
padding: 24px;
|
||
}
|
||
|
||
:deep(.profile-edit-modal .n-form-item) {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
:deep(.profile-edit-modal .n-input) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.profile-edit-modal .n-form-item-label) {
|
||
font-weight: 500;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
:deep(.profile-edit-modal .n-card__action) {
|
||
padding: 16px 24px;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.profile-card,
|
||
.settings-card {
|
||
border-radius: 8px;
|
||
}
|
||
|
||
/* 移动端个人资料头部布局 */
|
||
.profile-header {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
text-align: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.profile-avatar-wrapper {
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.profile-info {
|
||
width: 100%;
|
||
text-align: center;
|
||
}
|
||
|
||
.profile-name {
|
||
font-size: 20px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.profile-details {
|
||
align-items: flex-start;
|
||
text-align: left;
|
||
gap: 10px;
|
||
}
|
||
|
||
.profile-detail-item {
|
||
width: 100%;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 4px;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid #f3f4f6;
|
||
}
|
||
|
||
.profile-detail-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.profile-detail-label {
|
||
font-size: 12px;
|
||
color: #9ca3af;
|
||
min-width: auto;
|
||
width: 100%;
|
||
}
|
||
|
||
.profile-detail-value {
|
||
font-size: 14px;
|
||
width: 100%;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.profile-actions {
|
||
width: 100%;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.profile-actions .n-button {
|
||
width: 100%;
|
||
}
|
||
|
||
/* 移动端弹窗优化 */
|
||
:deep(.profile-edit-modal .n-card) {
|
||
width: 95vw !important;
|
||
max-width: 95vw !important;
|
||
margin: 20px auto;
|
||
border-radius: 12px;
|
||
}
|
||
|
||
:deep(.profile-edit-modal .n-card-body) {
|
||
padding: 20px 16px;
|
||
}
|
||
|
||
:deep(.profile-edit-modal .n-card__header) {
|
||
padding: 16px;
|
||
font-size: 18px;
|
||
}
|
||
|
||
:deep(.profile-edit-modal .n-card__action) {
|
||
padding: 12px 16px;
|
||
}
|
||
|
||
:deep(.profile-edit-modal .n-space) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.profile-edit-modal .n-button) {
|
||
width: 100%;
|
||
font-size: 16px;
|
||
height: 44px;
|
||
}
|
||
|
||
:deep(.profile-edit-modal .n-form-item-label) {
|
||
font-size: 14px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
:deep(.profile-edit-modal .n-input) {
|
||
font-size: 16px;
|
||
height: 44px;
|
||
}
|
||
|
||
/* 设置列表移动端优化 */
|
||
:deep(.settings-card .n-list-item) {
|
||
padding: 12px 0;
|
||
}
|
||
|
||
:deep(.settings-card .n-thing) {
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
:deep(.settings-card .n-thing-main) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.settings-card .n-thing-main__action) {
|
||
width: 100%;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
:deep(.settings-card .n-button) {
|
||
width: 100%;
|
||
}
|
||
}
|
||
|
||
/* 超小屏幕优化 */
|
||
@media (max-width: 480px) {
|
||
:deep(.profile-edit-modal .n-card) {
|
||
width: 100vw !important;
|
||
max-width: 100vw !important;
|
||
margin: 0;
|
||
border-radius: 0;
|
||
height: 100vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
:deep(.profile-edit-modal .n-card-body) {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 16px;
|
||
}
|
||
|
||
:deep(.profile-edit-modal .n-card__action) {
|
||
padding: 12px 16px;
|
||
border-top: 1px solid var(--n-border-color);
|
||
}
|
||
}
|
||
</style> |