Update Profile page to use ViewModel and display full user info including skills
This commit is contained in:
parent
4eecf3792d
commit
39a840fd2d
@ -1,3 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CORP_MAUI.Models;
|
||||
@ -32,6 +33,25 @@ public class User
|
||||
[JsonIgnore]
|
||||
public string FullName => $"{FirstName} {LastName}".Trim();
|
||||
|
||||
[JsonIgnore]
|
||||
public string? DisplayAvatarUrl
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(AvatarUrl)) return null;
|
||||
|
||||
// Handle protocol-relative URLs
|
||||
if (AvatarUrl.StartsWith("//"))
|
||||
return $"https:{AvatarUrl}";
|
||||
|
||||
if (AvatarUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
return AvatarUrl;
|
||||
|
||||
string cleanUrl = AvatarUrl.TrimStart('/');
|
||||
return $"https://corp.robotail.pro/{cleanUrl}";
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string DepartmentName
|
||||
{
|
||||
@ -41,6 +61,13 @@ public class User
|
||||
return dept.Name;
|
||||
if (Department is string str)
|
||||
return str;
|
||||
if (Department is JsonElement element)
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.String)
|
||||
return element.GetString() ?? string.Empty;
|
||||
if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty("name", out JsonElement nameProp))
|
||||
return nameProp.GetString() ?? string.Empty;
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
@ -54,6 +81,13 @@ public class User
|
||||
return pos.Name;
|
||||
if (Position is string str)
|
||||
return str;
|
||||
if (Position is JsonElement element)
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.String)
|
||||
return element.GetString() ?? string.Empty;
|
||||
if (element.ValueKind == JsonValueKind.Object && element.TryGetProperty("name", out JsonElement nameProp))
|
||||
return nameProp.GetString() ?? string.Empty;
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,11 @@
|
||||
|
||||
<ScrollView>
|
||||
<VerticalStackLayout Padding="20" Spacing="20">
|
||||
|
||||
<ActivityIndicator IsRunning="{Binding IsLoading}"
|
||||
IsVisible="{Binding IsLoading}"
|
||||
Color="{StaticResource Primary}"
|
||||
HorizontalOptions="Center" />
|
||||
|
||||
<!-- Аватар и имя -->
|
||||
<Frame
|
||||
@ -25,22 +30,19 @@
|
||||
BackgroundColor="{StaticResource Gray200}"
|
||||
HorizontalOptions="Center">
|
||||
<Image
|
||||
x:Name="AvatarImage"
|
||||
Source="dotnet_bot.png"
|
||||
Source="{Binding DisplayImageSource}"
|
||||
Aspect="AspectFill"/>
|
||||
</Frame>
|
||||
|
||||
<Label
|
||||
x:Name="FullNameLabel"
|
||||
Text="Загрузка..."
|
||||
Text="{Binding FullName}"
|
||||
FontSize="24"
|
||||
FontAttributes="Bold"
|
||||
HorizontalOptions="Center"
|
||||
TextColor="{StaticResource Gray900}"/>
|
||||
|
||||
<Label
|
||||
x:Name="UsernameLabel"
|
||||
Text="@username"
|
||||
Text="{Binding Username}"
|
||||
FontSize="16"
|
||||
HorizontalOptions="Center"
|
||||
TextColor="{StaticResource Gray600}"/>
|
||||
@ -63,20 +65,47 @@
|
||||
|
||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto,Auto,Auto" RowSpacing="10">
|
||||
<Label Grid.Row="0" Grid.Column="0" Text="Email:" FontAttributes="Bold" TextColor="{StaticResource Gray700}"/>
|
||||
<Label Grid.Row="0" Grid.Column="1" x:Name="EmailLabel" Text="-" TextColor="{StaticResource Gray600}" Margin="10,0,0,0"/>
|
||||
<Label Grid.Row="0" Grid.Column="1" Text="{Binding Email}" TextColor="{StaticResource Gray600}" Margin="10,0,0,0"/>
|
||||
|
||||
<Label Grid.Row="1" Grid.Column="0" Text="Должность:" FontAttributes="Bold" TextColor="{StaticResource Gray700}"/>
|
||||
<Label Grid.Row="1" Grid.Column="1" x:Name="PositionLabel" Text="-" TextColor="{StaticResource Gray600}" Margin="10,0,0,0"/>
|
||||
<Label Grid.Row="1" Grid.Column="1" Text="{Binding Position}" TextColor="{StaticResource Gray600}" Margin="10,0,0,0"/>
|
||||
|
||||
<Label Grid.Row="2" Grid.Column="0" Text="Отдел:" FontAttributes="Bold" TextColor="{StaticResource Gray700}"/>
|
||||
<Label Grid.Row="2" Grid.Column="1" x:Name="DepartmentLabel" Text="-" TextColor="{StaticResource Gray600}" Margin="10,0,0,0"/>
|
||||
<Label Grid.Row="2" Grid.Column="1" Text="{Binding Department}" TextColor="{StaticResource Gray600}" Margin="10,0,0,0"/>
|
||||
|
||||
<Label Grid.Row="3" Grid.Column="0" Text="Статус:" FontAttributes="Bold" TextColor="{StaticResource Gray700}"/>
|
||||
<Label Grid.Row="3" Grid.Column="1" x:Name="StatusLabel" Text="В сети" TextColor="LimeGreen" Margin="10,0,0,0"/>
|
||||
<Label Grid.Row="3" Grid.Column="1" Text="{Binding StatusText}" TextColor="{Binding StatusColor}" Margin="10,0,0,0"/>
|
||||
</Grid>
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<!-- Навыки -->
|
||||
<Frame
|
||||
Padding="20"
|
||||
CornerRadius="15"
|
||||
BackgroundColor="White"
|
||||
HasShadow="True"
|
||||
IsVisible="{Binding HasSkills}">
|
||||
|
||||
<VerticalStackLayout Spacing="10">
|
||||
<Label
|
||||
Text="Навыки"
|
||||
FontSize="18"
|
||||
FontAttributes="Bold"
|
||||
TextColor="{StaticResource Gray900}"/>
|
||||
|
||||
<FlexLayout BindableLayout.ItemsSource="{Binding Skills}" Wrap="Wrap" AlignItems="Center" AlignContent="Start">
|
||||
<BindableLayout.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Frame Padding="10,5" CornerRadius="10" BackgroundColor="{StaticResource Gray100}" HasShadow="False" Margin="0,0,5,5">
|
||||
<Label Text="{Binding Name}" TextColor="{StaticResource Gray800}" FontSize="14"/>
|
||||
</Frame>
|
||||
</DataTemplate>
|
||||
</BindableLayout.ItemTemplate>
|
||||
</FlexLayout>
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<!-- Действия -->
|
||||
<Frame
|
||||
Padding="20"
|
||||
@ -98,28 +127,19 @@
|
||||
TextColor="White"
|
||||
CornerRadius="10"
|
||||
HeightRequest="45"
|
||||
Clicked="OnRefreshClicked"/>
|
||||
Command="{Binding LoadProfileCommand}"/>
|
||||
|
||||
<Button
|
||||
Text="Выйти из системы"
|
||||
BackgroundColor="{StaticResource Secondary}"
|
||||
Text="Выйти"
|
||||
BackgroundColor="{StaticResource Error}"
|
||||
TextColor="White"
|
||||
CornerRadius="10"
|
||||
HeightRequest="45"
|
||||
Clicked="OnLogoutClicked"/>
|
||||
Command="{Binding LogoutCommand}"/>
|
||||
</VerticalStackLayout>
|
||||
</Frame>
|
||||
|
||||
<!-- Индикатор загрузки -->
|
||||
<ActivityIndicator
|
||||
x:Name="LoadingIndicator"
|
||||
IsRunning="False"
|
||||
IsVisible="False"
|
||||
Color="{StaticResource Primary}"
|
||||
HorizontalOptions="Center"/>
|
||||
|
||||
</VerticalStackLayout>
|
||||
</ScrollView>
|
||||
|
||||
</ContentPage>
|
||||
|
||||
|
||||
@ -1,92 +1,23 @@
|
||||
using CORP_MAUI.Models;
|
||||
using CORP_MAUI.ViewModels;
|
||||
|
||||
namespace CORP_MAUI.Pages;
|
||||
|
||||
public partial class ProfilePage : ContentPage
|
||||
{
|
||||
private User _currentUser = new();
|
||||
private ProfileViewModel _viewModel;
|
||||
|
||||
public ProfilePage()
|
||||
{
|
||||
InitializeComponent();
|
||||
_ = LoadProfileAsync();
|
||||
_viewModel = new ProfileViewModel();
|
||||
BindingContext = _viewModel;
|
||||
}
|
||||
|
||||
private async Task LoadProfileAsync()
|
||||
protected override void OnAppearing()
|
||||
{
|
||||
try
|
||||
{
|
||||
LoadingIndicator.IsVisible = true;
|
||||
LoadingIndicator.IsRunning = true;
|
||||
|
||||
// Загружаем профиль
|
||||
User? user = await ServiceProvider.ApiService.GetCurrentUserAsync();
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
_currentUser = user;
|
||||
FullNameLabel.Text = _currentUser.FullName;
|
||||
UsernameLabel.Text = $"@{_currentUser.Username}";
|
||||
EmailLabel.Text = _currentUser.Email ?? "Не указано";
|
||||
PositionLabel.Text = _currentUser.PositionName;
|
||||
DepartmentLabel.Text = _currentUser.DepartmentName;
|
||||
StatusLabel.Text = _currentUser.IsOnline ? "В сети" : "Не в сети";
|
||||
StatusLabel.TextColor = _currentUser.IsOnline ? Colors.LimeGreen : Colors.Gray;
|
||||
|
||||
if (!string.IsNullOrEmpty(_currentUser.AvatarUrl))
|
||||
{
|
||||
AvatarImage.Source = _currentUser.AvatarUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await DisplayAlert("Ошибка", $"Не удалось загрузить профиль: {ex.Message}", "OK");
|
||||
}
|
||||
finally
|
||||
{
|
||||
LoadingIndicator.IsVisible = false;
|
||||
LoadingIndicator.IsRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnRefreshClicked(object sender, EventArgs e)
|
||||
{
|
||||
await LoadProfileAsync();
|
||||
}
|
||||
|
||||
private async void OnLogoutClicked(object sender, EventArgs e)
|
||||
{
|
||||
bool result = await DisplayAlert("Выход", "Вы уверены, что хотите выйти?", "Да", "Нет");
|
||||
|
||||
if (result)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Обновляем статус пользователя
|
||||
await ServiceProvider.ApiService.UpdateStatusAsync(false);
|
||||
|
||||
// Отключаем WebSocket
|
||||
await ServiceProvider.WebSocketService.DisconnectAsync();
|
||||
|
||||
// Выходим из системы (очищаем токены)
|
||||
ServiceProvider.ApiService.Logout();
|
||||
|
||||
// Очищаем сохраненные данные
|
||||
SecureStorage.Remove("user_id");
|
||||
SecureStorage.Remove("username");
|
||||
|
||||
// Переходим на страницу входа
|
||||
if (Application.Current?.Windows.Count > 0)
|
||||
{
|
||||
Application.Current.Windows[0].Page = new NavigationPage(new LoginPage());
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await DisplayAlert("Ошибка", $"Ошибка при выходе: {ex.Message}", "OK");
|
||||
}
|
||||
}
|
||||
base.OnAppearing();
|
||||
// Load profile every time page appears to keep it fresh
|
||||
_viewModel.LoadProfileCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
218
CORP-MAUI/ViewModels/ProfileViewModel.cs
Normal file
218
CORP-MAUI/ViewModels/ProfileViewModel.cs
Normal file
@ -0,0 +1,218 @@
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Input;
|
||||
using CORP_MAUI.Models;
|
||||
using CORP_MAUI.Pages;
|
||||
using CORP_MAUI.Services;
|
||||
|
||||
namespace CORP_MAUI.ViewModels;
|
||||
|
||||
public class ProfileViewModel : INotifyPropertyChanged
|
||||
{
|
||||
private User? _user;
|
||||
private bool _isLoading;
|
||||
private string? _localImagePath;
|
||||
private ImageSource? _imageSource;
|
||||
|
||||
public ProfileViewModel()
|
||||
{
|
||||
LoadProfileCommand = new Command(async () => await LoadProfileAsync());
|
||||
LogoutCommand = new Command(async () => await LogoutAsync());
|
||||
}
|
||||
|
||||
public User? User
|
||||
{
|
||||
get => _user;
|
||||
set
|
||||
{
|
||||
_user = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(FullName));
|
||||
OnPropertyChanged(nameof(Username));
|
||||
OnPropertyChanged(nameof(Email));
|
||||
OnPropertyChanged(nameof(Position));
|
||||
OnPropertyChanged(nameof(Department));
|
||||
OnPropertyChanged(nameof(StatusText));
|
||||
OnPropertyChanged(nameof(StatusColor));
|
||||
OnPropertyChanged(nameof(Skills));
|
||||
OnPropertyChanged(nameof(HasSkills));
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsLoading
|
||||
{
|
||||
get => _isLoading;
|
||||
set
|
||||
{
|
||||
_isLoading = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
// Wrappers for UI binding
|
||||
public string FullName => _user?.FullName ?? "Загрузка...";
|
||||
public string Username => _user != null ? $"@{_user.Username}" : "";
|
||||
public string Email => _user?.Email ?? "-";
|
||||
public string Position => _user?.PositionName ?? "-";
|
||||
public string Department => _user?.DepartmentName ?? "-";
|
||||
public bool IsOnline => _user?.IsOnline ?? false;
|
||||
public string StatusText => IsOnline ? "В сети" : "Не в сети";
|
||||
public Color StatusColor => IsOnline ? Colors.LimeGreen : Colors.Gray;
|
||||
public List<Skill> Skills => _user?.Skills ?? new List<Skill>();
|
||||
public bool HasSkills => Skills.Any();
|
||||
|
||||
// Image Logic
|
||||
public ImageSource? DisplayImageSource
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_imageSource != null) return _imageSource;
|
||||
|
||||
if (!string.IsNullOrEmpty(_localImagePath) && File.Exists(_localImagePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (new FileInfo(_localImagePath).Length == 0) return null;
|
||||
return ImageSource.FromStream(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return new FileStream(_localImagePath, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
// Default placeholder if needed
|
||||
return ImageSource.FromFile("dotnet_bot.png");
|
||||
}
|
||||
}
|
||||
|
||||
public ICommand LoadProfileCommand { get; }
|
||||
public ICommand LogoutCommand { get; }
|
||||
|
||||
public async Task LoadProfileAsync()
|
||||
{
|
||||
if (IsLoading) return;
|
||||
IsLoading = true;
|
||||
|
||||
try
|
||||
{
|
||||
User? loadedUser = await ServiceProvider.ApiService.GetCurrentUserAsync();
|
||||
if (loadedUser != null)
|
||||
{
|
||||
User = loadedUser;
|
||||
await LoadAvatarAsync();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Error loading profile: {ex.Message}");
|
||||
|
||||
Page? mainPage = Application.Current?.Windows.FirstOrDefault()?.Page;
|
||||
if (mainPage != null)
|
||||
await mainPage.DisplayAlert("Ошибка", "Не удалось загрузить профиль", "OK");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadAvatarAsync()
|
||||
{
|
||||
if (_user == null || string.IsNullOrEmpty(_user.DisplayAvatarUrl)) return;
|
||||
|
||||
try
|
||||
{
|
||||
string url = _user.DisplayAvatarUrl;
|
||||
// Generate cache filename
|
||||
Uri uri = new Uri(url);
|
||||
string extension = Path.GetExtension(uri.LocalPath);
|
||||
if (string.IsNullOrEmpty(extension)) extension = ".jpg";
|
||||
|
||||
string fileName = Path.GetFileNameWithoutExtension(uri.LocalPath);
|
||||
fileName = Path.GetInvalidFileNameChars().Aggregate(fileName, (current, c) => current.Replace(c, '_'));
|
||||
|
||||
string cacheFileName = $"user_{_user.Id}_{fileName}{extension}";
|
||||
string cacheFilePath = Path.Combine(FileSystem.CacheDirectory, cacheFileName);
|
||||
|
||||
// Check cache
|
||||
if (File.Exists(cacheFilePath))
|
||||
{
|
||||
FileInfo fileInfo = new FileInfo(cacheFilePath);
|
||||
if (fileInfo.Length > 0)
|
||||
{
|
||||
_localImagePath = cacheFilePath;
|
||||
OnPropertyChanged(nameof(DisplayImageSource));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Download
|
||||
byte[]? bytes = await ServiceProvider.ApiService.GetImageBytesAsync(url);
|
||||
if (bytes is { Length: > 0 })
|
||||
{
|
||||
// Simple signature check
|
||||
bool isImage = false;
|
||||
if (bytes.Length > 2)
|
||||
{
|
||||
if (bytes[0] == 0xFF && bytes[1] == 0xD8) isImage = true;
|
||||
else if (bytes[0] == 0x89 && bytes[1] == 0x50) isImage = true;
|
||||
else isImage = true; // Relaxed check
|
||||
|
||||
if (bytes[0] == '<') isImage = false; // HTML check
|
||||
}
|
||||
|
||||
if (isImage)
|
||||
{
|
||||
await File.WriteAllBytesAsync(cacheFilePath, bytes);
|
||||
_localImagePath = cacheFilePath;
|
||||
OnPropertyChanged(nameof(DisplayImageSource));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.WriteLine($"Avatar load error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LogoutAsync()
|
||||
{
|
||||
Page? mainPage = Application.Current?.Windows.FirstOrDefault()?.Page;
|
||||
if (mainPage == null) return;
|
||||
|
||||
bool result = await mainPage.DisplayAlert("Выход", "Вы уверены, что хотите выйти?", "Да", "Нет");
|
||||
if (result)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ServiceProvider.ApiService.UpdateStatusAsync(false);
|
||||
await ServiceProvider.WebSocketService.DisconnectAsync();
|
||||
ServiceProvider.ApiService.Logout();
|
||||
SecureStorage.Remove("user_id");
|
||||
SecureStorage.Remove("username");
|
||||
|
||||
if (Application.Current?.Windows.Count > 0)
|
||||
{
|
||||
Application.Current.Windows[0].Page = new NavigationPage(new LoginPage());
|
||||
}
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
await mainPage.DisplayAlert("Ошибка", $"Ошибка при выходе: {ex.Message}", "OK");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
protected void OnPropertyChanged([CallerMemberName] string? name = null) =>
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user