You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/ui/page/user/profile.html

368 lines
10 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta name="description" content="User Profile">
<title>{{ $t('user.profile') }}</title>
<style>
body {
background-color: var(--bg-color);
display: flex;
justify-content: center;
padding-top: var(--spacing-xl);
padding-bottom: var(--spacing-xl);
box-sizing: border-box;
min-height: 100vh;
}
.profile-container {
width: 100%;
max-width: 800px;
padding: var(--spacing-xl);
background: var(--bg-color-secondary);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
border: 1px solid var(--border-color);
display: flex;
flex-direction: column;
gap: var(--spacing-xl);
height: fit-content;
}
.section-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--color-text);
margin-bottom: var(--spacing-md);
padding-bottom: var(--spacing-xs);
border-bottom: 1px solid var(--border-color);
}
.avatar-section {
display: flex;
align-items: center;
gap: var(--spacing-lg);
}
.avatar-wrapper {
position: relative;
cursor: pointer;
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
background: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: var(--color-primary-text);
font-weight: bold;
overflow: hidden;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
opacity: 0;
transition: opacity 0.2s;
}
.avatar-wrapper:hover .avatar-overlay {
opacity: 1;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-md);
}
.form-full {
grid-column: 1 / -1;
}
.oauth-list {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.oauth-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-sm);
background: var(--bg-color);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
}
.oauth-info {
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.oauth-icon {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
background: var(--bg-color-secondary);
border-radius: 50%;
}
.danger-zone {
margin-top: var(--spacing-lg);
padding-top: var(--spacing-lg);
border-top: 1px solid var(--border-color);
}
</style>
</head>
<body>
<div class="profile-container">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h2 style="margin: 0;">{{ $t('user.profile') }}</h2>
<v-btn color="danger" variant="outline" size="sm" :click="handleLogout">
{{ $t('auth.logout') }}
</v-btn>
</div>
<!-- Basic Info -->
<div>
<div class="section-title">{{ $t('user.info') || 'Basic Information' }}</div>
<div class="avatar-section">
<div class="avatar-wrapper" @click="triggerAvatarUpload">
<div class="avatar">
<img v-if="user.avatar" :src="user.avatar" alt="Avatar">
<span v-else>{{ user.nickname ? user.nickname.charAt(0).toUpperCase() : (user.username ? user.username.charAt(0).toUpperCase() : 'U') }}</span>
</div>
<div class="avatar-overlay">
<i class="fas fa-edit"></i>
</div>
</div>
<div style="flex: 1;">
<div style="font-weight: bold; font-size: 1.2rem;">{{ user.nickname || user.username }}</div>
<div style="color: var(--color-text-light); font-size: 0.9rem;">{{ $t('common.id') }}: {{ user.id }}</div>
</div>
</div>
<form style="margin-top: var(--spacing-lg);" @submit.prevent="updateProfile">
<div class="form-grid">
<v-input label="Username" :label="$t('user.username')" v:value="user.username" disabled></v-input>
<v-input label="Nickname" :label="$t('user.nickname')" v:value="user.nickname"></v-input>
<v-input label="Email" :label="$t('user.email')" type="email" v:value="user.email"></v-input>
<v-input label="Phone" :label="$t('user.phone')" type="tel" v:value="user.phone"></v-input>
<v-input class="form-full" label="Avatar URL" :label="$t('user.avatar_url')" v:value="user.avatar"></v-input>
</div>
<div style="margin-top: var(--spacing-md); display: flex; justify-content: flex-end;">
<v-btn type="submit" color="primary" :loading="loading">
{{ $t('common.save') }}
</v-btn>
</div>
</form>
</div>
<!-- Security -->
<div>
<div class="section-title">{{ $t('auth.security') || 'Security' }}</div>
<form @submit.prevent="changePassword">
<div class="form-grid">
<v-input class="form-full" :label="$t('auth.old_password')" type="password" v:value="passwordForm.old_password"></v-input>
<v-input class="form-full" :label="$t('auth.new_password')" type="password" v:value="passwordForm.new_password"></v-input>
</div>
<div style="margin-top: var(--spacing-md); display: flex; justify-content: flex-end;">
<v-btn type="submit" variant="outline" :loading="pwdLoading" :disabled="!passwordForm.new_password || !passwordForm.old_password">
{{ $t('auth.change_password') || 'Update Password' }}
</v-btn>
</div>
</form>
</div>
<!-- Linked Accounts -->
<div>
<div class="section-title">{{ $t('auth.linked_accounts') || 'Linked Accounts' }}</div>
<div class="oauth-list">
<div class="oauth-item" v-for="provider in providers">
<div class="oauth-info">
<div class="oauth-icon">
<i class="fab" :class="'fa-' + provider.code"></i>
</div>
<div>
<div style="font-weight: 500;">{{ provider.name }}</div>
<div style="font-size: 0.8rem; color: var(--color-text-light);" v-if="isBound(provider.code)">
{{ getBindInfo(provider.code) }}
</div>
<div style="font-size: 0.8rem; color: var(--color-text-light);" v-else>
{{ $t('auth.not_linked') }}
</div>
</div>
</div>
<div>
<v-btn v-if="isBound(provider.code)" size="sm" variant="outline" color="danger" :click="() => unbind(provider.code)">
{{ $t('common.unbind') }}
</v-btn>
<v-btn v-else size="sm" variant="outline" :click="() => bind(provider.code)">
{{ $t('common.bind') }}
</v-btn>
</div>
</div>
</div>
</div>
</div>
</body>
<script setup>
user = $env.$vbase.user || {};
loading = false;
pwdLoading = false;
passwordForm = { old_password: '', new_password: '' };
// Bindings
providers = [];
bindings = [];
// Fetch fresh data
loadUser = async () => {
try {
user = await $env.$vbase.fetchUser();
} catch (e) {
console.error(e);
}
};
loadBindings = async () => {
try {
// Get enabled providers
const providersRes = await $axios.get('/api/auth/login-methods');
providers = providersRes.methods.filter(p => p.type !== 'password' && p.type !== 'code' && p.type !== 'email_code' && p.type !== 'phone_code').map(p => ({
code: p.type,
name: p.name
}));
// Get current bindings
bindings = await $axios.get('/api/auth/me/bindings');
} catch (e) {
console.error(e);
}
};
updateProfile = async () => {
loading = true;
try {
await $axios.patch('/api/auth/me', {
nickname: user.nickname,
email: user.email,
phone: user.phone,
avatar: user.avatar
});
$message.success($t('common.save_success'));
await loadUser();
} catch (e) {
$message.error(e.message);
} finally {
loading = false;
}
};
changePassword = async () => {
if (!passwordForm.new_password || !passwordForm.old_password) return;
pwdLoading = true;
try {
await $axios.post('/api/auth/me/change-password', {
old_password: passwordForm.old_password,
new_password: passwordForm.new_password
});
$message.success($t('auth.password_changed'));
passwordForm.old_password = '';
passwordForm.new_password = '';
} catch (e) {
$message.error(e.message);
} finally {
pwdLoading = false;
}
};
handleLogout = async () => {
await $env.$vbase.logout('/login');
};
// OAuth Helpers
isBound = (code) => {
return bindings.some(b => b.provider === code);
};
getBindInfo = (code) => {
const b = bindings.find(b => b.provider === code);
return b ? (b.provider_name || b.email || $t('auth.linked')) : '';
};
bind = async (provider) => {
try {
// Fetch authorization URL
const res = await $axios.get('/api/auth/authorize/thirdparty', {
params: {
provider: provider,
redirect: window.location.origin + '/callback/' + provider,
bind_mode: true
}
});
// Redirect to provider auth page
if (res.auth_url) {
window.location.href = res.auth_url;
}
} catch (e) {
$message.error(e.message);
}
};
unbind = async (provider) => {
try {
await $message.confirm($t('auth.unbind_confirm'));
await $axios.delete(`/api/auth/me/bindings/${provider}`);
$message.success($t('common.success'));
loadBindings();
} catch (e) {
if (e !== 'cancel') $message.error(e.message);
}
};
triggerAvatarUpload = async () => {
try {
const url = await $message.prompt($t('user.avatar_url'), user.avatar || '');
if (url) {
user.avatar = url;
}
} catch (e) {
// Cancelled
}
};
</script>
<script>
$data.loadUser();
$data.loadBindings();
</script>
</html>