This commit is contained in:
Курнат Андрей
2026-04-04 16:19:25 +03:00
parent 5a55bc5f4c
commit 84f2308e65
5 changed files with 89 additions and 47 deletions

View File

@@ -53,9 +53,6 @@
VerticalAlignment="Center" VerticalAlignment="Center"
KeyDown="SearchTextBox_KeyDown" KeyDown="SearchTextBox_KeyDown"
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" /> Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" />
<Button Margin="0,0,8,0"
Click="RefreshButton_Click"
Content="Обновить список" />
<TextBlock Margin="12,0,8,0" <TextBlock Margin="12,0,8,0"
VerticalAlignment="Center" VerticalAlignment="Center"
FontWeight="SemiBold" FontWeight="SemiBold"
@@ -94,7 +91,7 @@
<GroupBox Grid.Column="0" Header="Реестр средств измерений"> <GroupBox Grid.Column="0" Header="Реестр средств измерений">
<Grid Margin="8"> <Grid Margin="8">
<DataGrid x:Name="InstrumentGrid" <DataGrid x:Name="InstrumentGrid" AutoGenerateColumns="False"
ItemsSource="{Binding Instruments}" ItemsSource="{Binding Instruments}"
SelectedItem="{Binding SelectedSummary, Mode=TwoWay}" SelectedItem="{Binding SelectedSummary, Mode=TwoWay}"
IsReadOnly="True" IsReadOnly="True"
@@ -241,7 +238,7 @@
Content="Удалить привязку" /> Content="Удалить привязку" />
</StackPanel> </StackPanel>
<DataGrid x:Name="AttachmentGrid" <DataGrid x:Name="AttachmentGrid" AutoGenerateColumns="False"
Grid.Row="1" Grid.Row="1"
ItemsSource="{Binding SelectedInstrument.Attachments}" ItemsSource="{Binding SelectedInstrument.Attachments}"
IsReadOnly="True"> IsReadOnly="True">

View File

@@ -29,11 +29,6 @@ public partial class MainWindow : Window
await ExecuteUiAsync(_viewModel.InitializeAsync); await ExecuteUiAsync(_viewModel.InitializeAsync);
} }
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
{
await ExecuteUiAsync(() => _viewModel.RefreshAsync());
}
private async void SyncButton_Click(object sender, RoutedEventArgs e) private async void SyncButton_Click(object sender, RoutedEventArgs e)
{ {
await ExecuteUiAsync(async () => await ExecuteUiAsync(async () =>

View File

@@ -15,7 +15,15 @@ internal sealed class PdfStorageService
_client = client; _client = client;
var options = configuration.GetSection("Crawler").Get<CrawlerOptions>() var options = configuration.GetSection("Crawler").Get<CrawlerOptions>()
?? throw new InvalidOperationException("Раздел Crawler не найден в appsettings.json."); ?? throw new InvalidOperationException("Раздел Crawler не найден в appsettings.json.");
_rootPath = Environment.ExpandEnvironmentVariables(options.PdfStoragePath); var configuredPath = Environment.ExpandEnvironmentVariables(options.PdfStoragePath ?? string.Empty).Trim();
if (string.IsNullOrWhiteSpace(configuredPath))
{
configuredPath = "PdfStore";
}
_rootPath = Path.IsPathRooted(configuredPath)
? configuredPath
: Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, configuredPath));
Directory.CreateDirectory(_rootPath); Directory.CreateDirectory(_rootPath);
} }

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
@@ -11,6 +12,8 @@ namespace CRAWLER.ViewModels;
internal sealed class MainWindowViewModel : ObservableObject internal sealed class MainWindowViewModel : ObservableObject
{ {
private const int SearchRefreshDelayMilliseconds = 350;
private readonly InstrumentCatalogService _catalogService; private readonly InstrumentCatalogService _catalogService;
private readonly IPdfOpener _pdfOpener; private readonly IPdfOpener _pdfOpener;
private InstrumentSummary _selectedSummary; private InstrumentSummary _selectedSummary;
@@ -19,6 +22,8 @@ internal sealed class MainWindowViewModel : ObservableObject
private int _pagesToScan; private int _pagesToScan;
private string _statusText; private string _statusText;
private bool _isBusy; private bool _isBusy;
private bool _isInitialized;
private CancellationTokenSource _searchRefreshCancellationTokenSource;
private CancellationTokenSource _selectionCancellationTokenSource; private CancellationTokenSource _selectionCancellationTokenSource;
public MainWindowViewModel(InstrumentCatalogService catalogService, IPdfOpener pdfOpener) public MainWindowViewModel(InstrumentCatalogService catalogService, IPdfOpener pdfOpener)
@@ -53,7 +58,13 @@ internal sealed class MainWindowViewModel : ObservableObject
public string SearchText public string SearchText
{ {
get { return _searchText; } get { return _searchText; }
set { SetProperty(ref _searchText, value); } set
{
if (SetProperty(ref _searchText, value) && _isInitialized)
{
ScheduleSearchRefresh();
}
}
} }
public int PagesToScan public int PagesToScan
@@ -80,40 +91,14 @@ internal sealed class MainWindowViewModel : ObservableObject
{ {
StatusText = "Подготовка базы данных..."; StatusText = "Подготовка базы данных...";
await _catalogService.InitializeAsync(CancellationToken.None); await _catalogService.InitializeAsync(CancellationToken.None);
await RefreshAsync(); await RefreshCoreAsync();
_isInitialized = true;
}); });
} }
public async Task RefreshAsync(long? selectId = null) public async Task RefreshAsync(long? selectId = null)
{ {
await RunBusyAsync(async () => await RunBusyAsync(() => RefreshCoreAsync(selectId));
{
StatusText = "Загрузка списка записей...";
var items = await _catalogService.SearchAsync(SearchText, CancellationToken.None);
Instruments.Clear();
foreach (var item in items)
{
Instruments.Add(item);
}
if (Instruments.Count == 0)
{
SelectedInstrument = null;
SelectedSummary = null;
StatusText = "Записи не найдены.";
return;
}
var summary = selectId.HasValue
? Instruments.FirstOrDefault(item => item.Id == selectId.Value)
: SelectedSummary == null
? Instruments.FirstOrDefault()
: Instruments.FirstOrDefault(item => item.Id == SelectedSummary.Id) ?? Instruments.FirstOrDefault();
SelectedSummary = summary;
StatusText = $"Загружено записей: {Instruments.Count}.";
});
} }
public async Task<SyncResult> SyncAsync() public async Task<SyncResult> SyncAsync()
@@ -124,7 +109,7 @@ internal sealed class MainWindowViewModel : ObservableObject
{ {
var progress = new Progress<string>(message => StatusText = message); var progress = new Progress<string>(message => StatusText = message);
result = await _catalogService.SyncFromSiteAsync(PagesToScan, progress, CancellationToken.None); result = await _catalogService.SyncFromSiteAsync(PagesToScan, progress, CancellationToken.None);
await RefreshAsync(SelectedSummary?.Id); await RefreshCoreAsync(SelectedSummary?.Id);
}); });
return result; return result;
@@ -143,7 +128,7 @@ internal sealed class MainWindowViewModel : ObservableObject
return SelectedInstrument?.Clone(); return SelectedInstrument?.Clone();
} }
public async Task<long> SaveAsync(InstrumentRecord draft, System.Collections.Generic.IEnumerable<string> pendingPdfPaths) public async Task<long> SaveAsync(InstrumentRecord draft, IEnumerable<string> pendingPdfPaths)
{ {
long id = 0; long id = 0;
@@ -151,7 +136,7 @@ internal sealed class MainWindowViewModel : ObservableObject
{ {
StatusText = "Сохранение записи..."; StatusText = "Сохранение записи...";
id = await _catalogService.SaveInstrumentAsync(draft, pendingPdfPaths, CancellationToken.None); id = await _catalogService.SaveInstrumentAsync(draft, pendingPdfPaths, CancellationToken.None);
await RefreshAsync(id); await RefreshCoreAsync(id);
StatusText = "Изменения сохранены."; StatusText = "Изменения сохранены.";
}); });
@@ -171,12 +156,12 @@ internal sealed class MainWindowViewModel : ObservableObject
{ {
StatusText = "Удаление записи..."; StatusText = "Удаление записи...";
await _catalogService.DeleteInstrumentAsync(SelectedInstrument, CancellationToken.None); await _catalogService.DeleteInstrumentAsync(SelectedInstrument, CancellationToken.None);
await RefreshAsync(); await RefreshCoreAsync();
StatusText = $"Запись {deletedId} удалена."; StatusText = $"Запись {deletedId} удалена.";
}); });
} }
public async Task AddAttachmentsToSelectedAsync(System.Collections.Generic.IEnumerable<string> paths) public async Task AddAttachmentsToSelectedAsync(IEnumerable<string> paths)
{ {
if (SelectedInstrument == null) if (SelectedInstrument == null)
{ {
@@ -221,9 +206,66 @@ internal sealed class MainWindowViewModel : ObservableObject
} }
} }
private async Task RefreshCoreAsync(long? selectId = null)
{
StatusText = "Загрузка списка записей...";
var items = await _catalogService.SearchAsync(SearchText, CancellationToken.None);
Instruments.Clear();
foreach (var item in items)
{
Instruments.Add(item);
}
if (Instruments.Count == 0)
{
SelectedInstrument = null;
SelectedSummary = null;
StatusText = "Записи не найдены.";
return;
}
var summary = selectId.HasValue
? Instruments.FirstOrDefault(item => item.Id == selectId.Value)
: SelectedSummary == null
? Instruments.FirstOrDefault()
: Instruments.FirstOrDefault(item => item.Id == SelectedSummary.Id) ?? Instruments.FirstOrDefault();
SelectedSummary = summary;
StatusText = $"Загружено записей: {Instruments.Count}.";
}
private void ScheduleSearchRefresh()
{
_searchRefreshCancellationTokenSource?.Cancel();
_searchRefreshCancellationTokenSource?.Dispose();
_searchRefreshCancellationTokenSource = new CancellationTokenSource();
_ = RunSearchRefreshAsync(_searchRefreshCancellationTokenSource.Token);
}
private async Task RunSearchRefreshAsync(CancellationToken cancellationToken)
{
try
{
await Task.Delay(SearchRefreshDelayMilliseconds, cancellationToken);
while (IsBusy)
{
await Task.Delay(100, cancellationToken);
}
cancellationToken.ThrowIfCancellationRequested();
await RefreshAsync();
}
catch (OperationCanceledException)
{
}
}
private async Task LoadSelectedInstrumentAsync(long? id) private async Task LoadSelectedInstrumentAsync(long? id)
{ {
_selectionCancellationTokenSource?.Cancel(); _selectionCancellationTokenSource?.Cancel();
_selectionCancellationTokenSource?.Dispose();
_selectionCancellationTokenSource = new CancellationTokenSource(); _selectionCancellationTokenSource = new CancellationTokenSource();
var token = _selectionCancellationTokenSource.Token; var token = _selectionCancellationTokenSource.Token;

View File

@@ -20,7 +20,7 @@
"CatalogPathFormat": "/poverka/gosreestr_sredstv_izmereniy?page={0}", "CatalogPathFormat": "/poverka/gosreestr_sredstv_izmereniy?page={0}",
"RequestDelayMilliseconds": 350, "RequestDelayMilliseconds": 350,
"DefaultPagesToScan": 1, "DefaultPagesToScan": 1,
"PdfStoragePath": "%LOCALAPPDATA%\\CRAWLER\\PdfStore", "PdfStoragePath": "PdfStore",
"TimeoutSeconds": 30, "TimeoutSeconds": 30,
"UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) CRAWLER/1.0" "UserAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) CRAWLER/1.0"
} }