x-control-panel/web-ui/vue-app/src/views/ProfileView.vue

348 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>
<div class="profile-container">
<v-container fluid>
<v-row justify="center">
<v-col cols="12" md="8" lg="6">
<v-card elevation="2" class="profile-card">
<v-card-text>
<!-- Форма профиля -->
<v-form ref="profileForm" v-model="isFormValid">
<!-- Аватар и имя -->
<div class="text-center mb-6">
<v-avatar size="120" color="primary" class="mb-3">
<v-icon size="60" color="white">mdi-account</v-icon>
</v-avatar>
<div class="text-h6">{{ getUserDisplayName() }}</div>
<div class="text-body-2 text-medium-emphasis">{{ userRole }}</div>
</div>
<!-- Поля для редактирования -->
<v-row dense>
<v-col cols="12">
<v-text-field
v-model="profileData.login"
:rules="loginRules"
:disabled="!isEditing"
label="Логин"
prepend-inner-icon="mdi-account"
variant="outlined"
hint="Только буквы, цифры, точка и дефис"
persistent-hint
/>
</v-col>
</v-row>
<v-row dense>
<v-col cols="12">
<v-text-field
v-model="profileData.name"
:rules="nameRules"
:disabled="!isEditing"
label="Отображаемое имя"
prepend-inner-icon="mdi-account-circle"
variant="outlined"
hint="Имя для отображения в интерфейсе"
persistent-hint
/>
</v-col>
</v-row>
<!-- Поля для смены пароля (только в режиме редактирования) -->
<template v-if="isEditing">
<v-divider class="my-4" />
<div class="text-subtitle-2 mb-3">Смена пароля</div>
<v-row dense>
<v-col cols="12">
<v-text-field
v-model="profileData.currentPassword"
:disabled="!isEditing"
label="Текущий пароль"
type="password"
prepend-inner-icon="mdi-lock"
variant="outlined"
hint="Введите текущий пароль для смены"
persistent-hint
/>
</v-col>
</v-row>
<v-row dense>
<v-col cols="12" md="6">
<v-text-field
v-model="profileData.newPassword"
:rules="passwordRules"
:disabled="!isEditing"
label="Новый пароль"
type="password"
prepend-inner-icon="mdi-lock-reset"
variant="outlined"
hint="Минимум 6 символов"
persistent-hint
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="profileData.confirmPassword"
:rules="confirmPasswordRules"
:disabled="!isEditing"
label="Подтверждение пароля"
type="password"
prepend-inner-icon="mdi-lock-check"
variant="outlined"
hint="Повторите новый пароль"
persistent-hint
/>
</v-col>
</v-row>
</template>
<!-- Кнопки действий -->
<div class="d-flex justify-end mt-4">
<v-btn
v-if="!isEditing"
@click="startEditing"
color="primary"
prepend-icon="mdi-pencil"
>
Редактировать
</v-btn>
<template v-else>
<v-btn @click="cancelEditing" variant="outlined" class="me-2"> Отмена </v-btn>
<v-btn
@click="saveProfile"
color="primary"
:loading="isSaving"
:disabled="!isFormValid"
prepend-icon="mdi-content-save"
>
Сохранить
</v-btn>
</template>
</div>
</v-form>
</v-card-text>
</v-card>
<!-- Карта с дополнительной информацией -->
<v-card elevation="2" class="mt-4">
<v-card-text>
<v-list density="compact">
<v-list-item>
<template #prepend>
<v-icon>mdi-fingerprint</v-icon>
</template>
<v-list-item-title>ID пользователя</v-list-item-title>
<v-list-item-subtitle>{{ currentUser?.id }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template #prepend>
<v-icon>mdi-identifier</v-icon>
</template>
<v-list-item-title>UUID</v-list-item-title>
<v-list-item-subtitle>{{ currentUser?.uuid }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template #prepend>
<v-icon>mdi-shield-account</v-icon>
</template>
<v-list-item-title>Роль</v-list-item-title>
<v-list-item-subtitle>{{ userRole }}</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import { useAppStore } from '@/stores/app'
import { useNotificationStore } from '@/stores/notification'
const appStore = useAppStore()
const { showSuccess, showError } = useNotificationStore()
// Данные формы
const profileForm = ref()
const isFormValid = ref(false)
const isEditing = ref(false)
const isSaving = ref(false)
const currentUser = computed(() => appStore.currentUser)
const userRole = computed(() => {
const role = currentUser.value?.role
const roleMap: Record<string, string> = {
Admin: 'Администратор',
Operator: 'Оператор',
User: 'Пользователь',
Guest: 'Гость'
}
return roleMap[role || ''] || role || 'Неизвестно'
})
// Данные профиля для редактирования
const profileData = ref({
login: '',
name: '',
currentPassword: '',
newPassword: '',
confirmPassword: ''
})
const originalData = ref({
login: '',
name: '',
currentPassword: '',
newPassword: '',
confirmPassword: ''
})
// Валидация логина: только точка, дефис, A-z, 0-9
const loginRules = [
(v: string) => !!v || 'Логин обязателен',
(v: string) =>
/^[a-zA-Z0-9.-]+$/.test(v) || 'Логин может содержать только буквы, цифры, точку и дефис',
(v: string) => v.length >= 3 || 'Минимальная длина логина - 3 символа',
(v: string) => v.length <= 50 || 'Максимальная длина логина - 50 символов'
]
const nameRules = [
(v: string) => !!v || 'Имя обязательно',
(v: string) => v.length >= 2 || 'Минимальная длина имени - 2 символа',
(v: string) => v.length <= 100 || 'Максимальная длина имени - 100 символов'
]
const passwordRules = [
(v: string) => !v || v.length >= 6 || 'Минимальная длина пароля - 6 символов'
]
const confirmPasswordRules = [
(v: string) => !profileData.newPassword || v === profileData.newPassword || 'Пароли не совпадают'
]
const getUserDisplayName = () => {
const user = currentUser.value
if (user?.name && user.name.trim()) {
return user.name
}
return user?.login || 'Профиль'
}
const startEditing = () => {
originalData.value = {
login: profileData.value.login,
name: profileData.value.name
}
isEditing.value = true
}
const cancelEditing = () => {
profileData.value = { ...originalData.value }
isEditing.value = false
profileForm.value?.resetValidation()
}
const saveProfile = async () => {
if (!profileForm.value?.validate() || !appStore.api) {
return
}
// Проверяем, если хочет сменить пароль - нужен текущий пароль
if (profileData.value.newPassword && !profileData.value.currentPassword) {
showError('Для смены пароля введите текущий пароль')
return
}
isSaving.value = true
try {
// Определяем параметр password только если меняем пароль
const password = profileData.value.newPassword ? profileData.value.newPassword : undefined
const success = await appStore.api.Profile.save(
profileData.value.login,
profileData.value.name,
currentUser.value?.values || {},
password,
profileData.value.currentPassword
)
if (success) {
// Обновляем данные пользователя в store
const updatedUser = await appStore.api.Profile.get()
appStore.setCurrentUser(updatedUser)
showSuccess('Профиль успешно обновлен!')
isEditing.value = false
// Очищаем поля пароля после успешного сохранения
profileData.value.currentPassword = ''
profileData.value.newPassword = ''
profileData.value.confirmPassword = ''
originalData.value = { ...profileData.value }
} else {
showError('Не удалось обновить профиль')
}
} catch (error) {
console.error('Error saving profile:', error)
showError('Ошибка при сохранении профиля')
} finally {
isSaving.value = false
}
}
const loadProfileData = () => {
const user = currentUser.value
if (user) {
profileData.value = {
login: user.login || '',
name: user.name || '',
currentPassword: '',
newPassword: '',
confirmPassword: ''
}
originalData.value = { ...profileData.value }
}
}
// Загружаем данные при монтировании
onMounted(() => {
loadProfileData()
})
// Следим за изменениями currentUser
watch(
currentUser,
() => {
loadProfileData()
},
{ immediate: true }
)
</script>
<style scoped>
.profile-container {
padding: 16px;
}
.profile-card {
border-radius: 12px;
}
@media (max-width: 768px) {
.profile-container {
padding: 8px;
}
}
</style>