Update Profile page to use ViewModel and display full user info including skills

This commit is contained in:
Данил Ярош 2026-01-13 11:08:15 +03:00
parent 4eecf3792d
commit 39a840fd2d
4 changed files with 303 additions and 100 deletions

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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);
}
}

View 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));
}