dev #3
@ -1,188 +1,295 @@
|
||||
<template>
|
||||
<Card :title="$t('Profile')" 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 }"
|
||||
<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"
|
||||
>
|
||||
<span v-if="uploading">{{ progress }}%</span>
|
||||
<span v-else>{{ $t('Edit') }}</span>
|
||||
</button>
|
||||
{{ 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>
|
||||
</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">{{ $t('Username') }}: {{ user.username }}</p>
|
||||
<p class="mt-1 text-base text-gray-600">{{ $t('Phone') }}: {{ user.mobile_no }}</p>
|
||||
<p class="mt-1 text-base text-gray-600">{{ $t('Email') }}: {{ user.email }}</p>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<Button icon-left="edit" @click="showProfileEditDialog = true">
|
||||
{{ $t('Edit') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ListItem
|
||||
:title="$t('Marketplace Developer')"
|
||||
:subtitle="$t('Developers can publish their apps on the marketplace for users to subscribe to, either paid or free.')"
|
||||
v-if="!$team.pg.is_developer"
|
||||
>
|
||||
<template #actions>
|
||||
<Button @click="confirmPublisherAccount">
|
||||
<span>{{ $t('Become a Developer') }}</span>
|
||||
</Button>
|
||||
</template>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
:title="twoFactorAuthTitle"
|
||||
:subtitle="twoFactorAuthSubtitle"
|
||||
>
|
||||
<template #actions>
|
||||
<Button @click="show2FADialog = true">
|
||||
{{ twoFactorAuthButtonLabel }}
|
||||
</Button>
|
||||
</template>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
:title="$t('Reset Password')"
|
||||
:subtitle="$t('Change your account login password')"
|
||||
>
|
||||
<template #actions>
|
||||
<Button @click="showResetPasswordDialog = true">
|
||||
{{ $t('Reset Password') }}
|
||||
</Button>
|
||||
</template>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
:title="accountStatusTitle"
|
||||
:subtitle="accountStatusSubtitle"
|
||||
>
|
||||
<template #actions>
|
||||
<Button
|
||||
@click="
|
||||
() => {
|
||||
if (teamEnabled) {
|
||||
showDisableAccountDialog = true;
|
||||
} else {
|
||||
showEnableAccountDialog = true;
|
||||
}
|
||||
}
|
||||
"
|
||||
<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>
|
||||
</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"
|
||||
>
|
||||
<span :class="{ 'text-red-600': teamEnabled }">{{
|
||||
accountStatusButtonLabel
|
||||
}}</span>
|
||||
</Button>
|
||||
</template>
|
||||
</ListItem>
|
||||
</div>
|
||||
<Dialog
|
||||
:options="{
|
||||
title: $t('Update Profile Information'),
|
||||
actions: [
|
||||
{
|
||||
variant: 'solid',
|
||||
label: $t('Save Changes'),
|
||||
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="$t('First Name')" v-model="user.first_name" />
|
||||
<FormControl :label="$t('Username')" v-model="user.username" />
|
||||
<FormControl :label="$t('Phone')" v-model="user.mobile_no" />
|
||||
<FormControl :label="$t('Email')" v-model="user.email" />
|
||||
</div>
|
||||
<ErrorMessage class="mt-4" :message="$resources.updateProfile.error" />
|
||||
{{ $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>
|
||||
</Dialog>
|
||||
</n-modal>
|
||||
|
||||
<Dialog
|
||||
:options="{
|
||||
title: $t('Disable Account'),
|
||||
actions: [
|
||||
{
|
||||
label: $t('Disable Account'),
|
||||
variant: 'solid',
|
||||
theme: 'red',
|
||||
loading: $resources.disableAccount.loading,
|
||||
onClick: () => deactivateAccount(disableAccount2FACode),
|
||||
},
|
||||
],
|
||||
}"
|
||||
v-model="showDisableAccountDialog"
|
||||
<!-- 禁用账户对话框 -->
|
||||
<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)"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<div class="prose text-base">
|
||||
{{ $t('After confirming this action:') }}
|
||||
<ul>
|
||||
<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>
|
||||
{{ $t('You can log in later to re-enable your account. Do you want to continue?') }}
|
||||
</div>
|
||||
<FormControl
|
||||
<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"
|
||||
class="mt-4"
|
||||
:label="$t('Enter your 2FA code to confirm')"
|
||||
v-model="disableAccount2FACode"
|
||||
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"
|
||||
/>
|
||||
<ErrorMessage class="mt-2" :message="$resources.disableAccount.error" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</n-modal>
|
||||
|
||||
<Dialog
|
||||
:options="{
|
||||
title: $t('Enable Account'),
|
||||
actions: [
|
||||
{
|
||||
label: $t('Enable Account'),
|
||||
variant: 'solid',
|
||||
loading: $resources.enableAccount.loading,
|
||||
onClick: () => $resources.enableAccount.submit(),
|
||||
},
|
||||
],
|
||||
}"
|
||||
v-model="showEnableAccountDialog"
|
||||
<!-- 启用账户对话框 -->
|
||||
<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()"
|
||||
>
|
||||
<template v-slot:body-content>
|
||||
<div class="prose text-base">
|
||||
{{ $t('After confirming this action:') }}
|
||||
<ul>
|
||||
<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>
|
||||
{{ $t('Do you want to continue?') }}
|
||||
</div>
|
||||
<ErrorMessage class="mt-2" :message="$resources.enableAccount.error" />
|
||||
</template>
|
||||
</Dialog>
|
||||
<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"
|
||||
@ -194,27 +301,61 @@
|
||||
v-if="showResetPasswordDialog"
|
||||
v-model="showResetPasswordDialog"
|
||||
/>
|
||||
</Card>
|
||||
<TFADialog v-model="show2FADialog" />
|
||||
<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 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,
|
||||
EditIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -229,6 +370,7 @@ export default {
|
||||
showMessage: false,
|
||||
draftInvoice: {},
|
||||
unpaidInvoices: [] | {},
|
||||
windowWidth: window.innerWidth,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@ -238,6 +380,24 @@ export default {
|
||||
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')
|
||||
@ -342,7 +502,21 @@ url: 'jcloud.api.billing.get_unpaid_invoices',
|
||||
};
|
||||
},
|
||||
},
|
||||
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();
|
||||
},
|
||||
@ -460,4 +634,293 @@ url: 'jcloud.api.billing.get_unpaid_invoices',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
</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>
|
||||
Loading…
x
Reference in New Issue
Block a user