From 72ba1a9f588951942bf933fea692dc82596af266 Mon Sep 17 00:00:00 2001 From: CJK_mkp <113243675+CJKmkp@users.noreply.github.com> Date: Sun, 2 Nov 2025 09:29:06 +0800 Subject: [PATCH 01/25] =?UTF-8?q?add:Dlass=E8=81=94=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Helpers/DlassApiClient.cs | 374 +++++++++++++ Ink Canvas/MainWindow.xaml | 11 + Ink Canvas/MainWindow_cs/MW_Settings.cs | 56 ++ Ink Canvas/Resources/Settings.cs | 14 + Ink Canvas/Windows/DlassSettingsWindow.xaml | 388 ++++++++++++++ .../Windows/DlassSettingsWindow.xaml.cs | 490 ++++++++++++++++++ 6 files changed, 1333 insertions(+) create mode 100644 Ink Canvas/Helpers/DlassApiClient.cs create mode 100644 Ink Canvas/Windows/DlassSettingsWindow.xaml create mode 100644 Ink Canvas/Windows/DlassSettingsWindow.xaml.cs diff --git a/Ink Canvas/Helpers/DlassApiClient.cs b/Ink Canvas/Helpers/DlassApiClient.cs new file mode 100644 index 00000000..438c4947 --- /dev/null +++ b/Ink Canvas/Helpers/DlassApiClient.cs @@ -0,0 +1,374 @@ +using Ink_Canvas.Helpers; +using Newtonsoft.Json; +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace Ink_Canvas.Helpers +{ + /// + /// Dlass API 客户端,用于与服务端通信 + /// + public class DlassApiClient + { + private const string DEFAULT_BASE_URL = "https://dlass.tech"; + private readonly string _appId; + private readonly string _appSecret; + private readonly string _baseUrl; + private HttpClient _httpClient; + private string _accessToken; + private DateTime _tokenExpiresAt; + + private string _userToken; + + /// + /// 初始化 Dlass API 客户端 + /// + /// 应用ID + /// 应用密钥 + /// API基础URL,如果为空则使用默认URL + /// 用户Token,如果提供则优先使用用户token而不是App Secret + public DlassApiClient(string appId, string appSecret, string baseUrl = null, string userToken = null) + { + _appId = appId ?? throw new ArgumentNullException(nameof(appId)); + _appSecret = appSecret ?? throw new ArgumentNullException(nameof(appSecret)); + _userToken = userToken; + _baseUrl = baseUrl ?? DEFAULT_BASE_URL; + + _baseUrl = _baseUrl.TrimEnd('/'); + if (!_baseUrl.StartsWith("http://") && !_baseUrl.StartsWith("https://")) + { + _baseUrl = "https://" + _baseUrl; + } + + _httpClient = new HttpClient + { + BaseAddress = new Uri(_baseUrl), + Timeout = TimeSpan.FromSeconds(30) + }; + _httpClient.DefaultRequestHeaders.Add("User-Agent", "InkCanvas/1.0"); + } + + /// + /// 获取访问令牌(Access Token) + /// + public async Task GetAccessTokenAsync() + { + if (!string.IsNullOrEmpty(_userToken)) + { + return _userToken; + } + + if (!string.IsNullOrEmpty(_accessToken) && DateTime.Now < _tokenExpiresAt.AddMinutes(-5)) + { + return _accessToken; + } + + try + { + var requestData = new + { + app_id = _appId, + app_secret = _appSecret, + grant_type = "client_credentials" + }; + + var json = JsonConvert.SerializeObject(requestData); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync("/oauth/token", content); + var responseContent = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + var tokenResponse = JsonConvert.DeserializeObject(responseContent); + _accessToken = tokenResponse.AccessToken; + _tokenExpiresAt = DateTime.Now.AddSeconds(tokenResponse.ExpiresIn ?? 3600); + return _accessToken; + } + else + { + throw new Exception($"获取Access Token失败: {response.StatusCode}"); + } + } + catch (HttpRequestException httpEx) + { + throw new Exception($"获取Access Token时网络错误: {httpEx.Message}", httpEx); + } + catch (TaskCanceledException timeoutEx) + { + throw new Exception("获取Access Token时请求超时", timeoutEx); + } + catch (Exception ex) + { + throw new Exception($"获取Access Token时出错: {ex.Message}", ex); + } + } + + /// + /// 发送GET请求 + /// + public async Task GetAsync(string endpoint, bool requireAuth = true) + { + try + { + string token = null; + if (requireAuth) + { + token = await GetAccessTokenAsync(); + } + + var request = new HttpRequestMessage(HttpMethod.Get, endpoint); + + if (requireAuth && !string.IsNullOrEmpty(token)) + { + if (!string.IsNullOrEmpty(_userToken)) + { + request.Headers.Add("X-User-Token", token); + } + else + { + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + } + } + + var response = await _httpClient.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + if (string.IsNullOrEmpty(content)) + { + return default(T); + } + return JsonConvert.DeserializeObject(content); + } + else + { + throw new Exception($"API请求失败: {response.StatusCode} - {content}"); + } + } + catch (HttpRequestException httpEx) + { + throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx); + } + catch (TaskCanceledException timeoutEx) + { + throw new Exception($"请求超时: {endpoint}", timeoutEx); + } + catch (Exception ex) + { + throw new Exception($"发送请求时出错: {ex.Message}", ex); + } + } + + /// + /// 发送POST请求 + /// + public async Task PostAsync(string endpoint, object data = null, bool requireAuth = true) + { + try + { + string token = null; + if (requireAuth) + { + token = await GetAccessTokenAsync(); + } + + var request = new HttpRequestMessage(HttpMethod.Post, endpoint); + + if (requireAuth && !string.IsNullOrEmpty(token)) + { + if (!string.IsNullOrEmpty(_userToken)) + { + request.Headers.Add("X-User-Token", token); + } + else + { + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + } + } + + if (data != null) + { + var json = JsonConvert.SerializeObject(data); + request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + } + + var response = await _httpClient.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + if (string.IsNullOrEmpty(content)) + { + return default(T); + } + return JsonConvert.DeserializeObject(content); + } + else + { + throw new Exception($"API请求失败: {response.StatusCode} - {content}"); + } + } + catch (HttpRequestException httpEx) + { + throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx); + } + catch (TaskCanceledException timeoutEx) + { + throw new Exception($"请求超时: {endpoint}", timeoutEx); + } + catch (Exception ex) + { + throw new Exception($"发送请求时出错: {ex.Message}", ex); + } + } + + /// + /// 发送PUT请求 + /// + public async Task PutAsync(string endpoint, object data = null, bool requireAuth = true) + { + try + { + string token = null; + if (requireAuth) + { + token = await GetAccessTokenAsync(); + } + + var request = new HttpRequestMessage(HttpMethod.Put, endpoint); + + if (requireAuth && !string.IsNullOrEmpty(token)) + { + // 如果是用户token,使用X-User-Token header + if (!string.IsNullOrEmpty(_userToken)) + { + request.Headers.Add("X-User-Token", token); + } + else + { + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + } + } + + if (data != null) + { + var json = JsonConvert.SerializeObject(data); + request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + } + + var response = await _httpClient.SendAsync(request); + var content = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + if (string.IsNullOrEmpty(content)) + { + return default(T); + } + return JsonConvert.DeserializeObject(content); + } + else + { + throw new Exception($"API请求失败: {response.StatusCode} - {content}"); + } + } + catch (HttpRequestException httpEx) + { + throw new Exception($"发送请求时出错: {httpEx.Message}", httpEx); + } + catch (TaskCanceledException timeoutEx) + { + throw new Exception($"请求超时: {endpoint}", timeoutEx); + } + catch (Exception ex) + { + throw new Exception($"发送请求时出错: {ex.Message}", ex); + } + } + + /// + /// 发送DELETE请求 + /// + public async Task DeleteAsync(string endpoint, bool requireAuth = true) + { + try + { + string token = null; + if (requireAuth) + { + token = await GetAccessTokenAsync(); + } + + var request = new HttpRequestMessage(HttpMethod.Delete, endpoint); + + if (requireAuth && !string.IsNullOrEmpty(token)) + { + // 如果是用户token,使用X-User-Token header + if (!string.IsNullOrEmpty(_userToken)) + { + request.Headers.Add("X-User-Token", token); + } + else + { + request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token); + } + } + + var response = await _httpClient.SendAsync(request); + + if (response.IsSuccessStatusCode) + { + return true; + } + else + { + return false; + } + } + catch (HttpRequestException httpEx) + { + return false; + } + catch (TaskCanceledException timeoutEx) + { + return false; + } + catch (Exception ex) + { + return false; + } + } + + /// + /// 释放资源 + /// + public void Dispose() + { + _httpClient?.Dispose(); + } + + #region 内部类 + + /// + /// Token响应模型 + /// + private class TokenResponse + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + + [JsonProperty("expires_in")] + public int? ExpiresIn { get; set; } + + [JsonProperty("token_type")] + public string TokenType { get; set; } + } + + #endregion + } +} + diff --git a/Ink Canvas/MainWindow.xaml b/Ink Canvas/MainWindow.xaml index 32a65bc6..beaa21df 100644 --- a/Ink Canvas/MainWindow.xaml +++ b/Ink Canvas/MainWindow.xaml @@ -3185,6 +3185,17 @@ FontSize="14" Margin="8,0,0,0" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs b/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs new file mode 100644 index 00000000..3f80c958 --- /dev/null +++ b/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs @@ -0,0 +1,490 @@ +using Ink_Canvas.Helpers; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Input; +using MessageBox = iNKORE.UI.WPF.Modern.Controls.MessageBox; + +namespace Ink_Canvas.Windows +{ + /// + /// DlassSettingsWindow.xaml 的交互逻辑 + /// + public partial class DlassSettingsWindow : Window + { + private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA"; + private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA"; + + private DlassApiClient _apiClient; + + public DlassSettingsWindow(MainWindow mainWindow = null) + { + InitializeComponent(); + + // 加载保存的token + LoadUserToken(); + + // 初始化API客户端(优先使用用户token) + InitializeApiClient(); + + // 窗口关闭时释放资源 + Closed += (s, e) => _apiClient?.Dispose(); + + // 测试连接 + _ = TestConnectionAsync(); + } + + /// + /// 初始化API客户端 + /// + private void InitializeApiClient() + { + var userToken = GetUserToken(); + var apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl; + + if (string.IsNullOrEmpty(apiBaseUrl) || apiBaseUrl.Contains("api.dlass.tech")) + { + apiBaseUrl = "https://dlass.tech"; + if (MainWindow.Settings?.Dlass != null) + { + MainWindow.Settings.Dlass.ApiBaseUrl = apiBaseUrl; + MainWindow.SaveSettingsToFile(); + } + } + + if (!string.IsNullOrEmpty(userToken)) + { + _apiClient = new DlassApiClient(APP_ID, APP_SECRET, baseUrl: apiBaseUrl, userToken: userToken); + } + else + { + _apiClient = new DlassApiClient(APP_ID, APP_SECRET, baseUrl: apiBaseUrl); + } + } + + /// + /// 获取用户token + /// + private string GetUserToken() + { + if (MainWindow.Settings?.Dlass != null) + { + return MainWindow.Settings.Dlass.UserToken ?? string.Empty; + } + return string.Empty; + } + + /// + /// 获取保存的Token列表 + /// + private List GetSavedTokens() + { + if (MainWindow.Settings?.Dlass != null) + { + return MainWindow.Settings.Dlass.SavedTokens ?? new List(); + } + return new List(); + } + + /// + /// 加载用户token到UI + /// + private void LoadUserToken() + { + var savedTokens = GetSavedTokens(); + var currentToken = GetUserToken(); + + CmbSavedTokens.Items.Clear(); + if (savedTokens.Count > 0) + { + foreach (var token in savedTokens) + { + CmbSavedTokens.Items.Add(token); + } + if (!string.IsNullOrEmpty(currentToken)) + { + var index = savedTokens.IndexOf(currentToken); + if (index >= 0) + { + CmbSavedTokens.SelectedIndex = index; + } + else + { + CmbSavedTokens.SelectedIndex = 0; + } + } + else if (CmbSavedTokens.Items.Count > 0) + { + CmbSavedTokens.SelectedIndex = 0; + } + } + else + { + CmbSavedTokens.Items.Add("(无保存的Token)"); + CmbSavedTokens.SelectedIndex = 0; + CmbSavedTokens.IsEnabled = false; + } + + TxtNewToken.Text = string.Empty; + + if (!string.IsNullOrEmpty(currentToken)) + { + TxtTokenStatus.Text = "已选择Token"; + TxtTokenStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(34, 197, 94)); + } + else + { + TxtTokenStatus.Text = "未设置Token"; + TxtTokenStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(161, 161, 170)); + } + } + + /// + /// 保存用户token + /// + private void SaveUserToken(string token) + { + if (MainWindow.Settings?.Dlass != null) + { + MainWindow.Settings.Dlass.UserToken = token ?? string.Empty; + MainWindow.SaveSettingsToFile(); + } + } + + /// + /// 添加Token到保存列表 + /// + private void AddTokenToList(string token) + { + if (MainWindow.Settings?.Dlass != null) + { + if (MainWindow.Settings.Dlass.SavedTokens == null) + { + MainWindow.Settings.Dlass.SavedTokens = new List(); + } + + if (!string.IsNullOrEmpty(token) && !MainWindow.Settings.Dlass.SavedTokens.Contains(token)) + { + MainWindow.Settings.Dlass.SavedTokens.Add(token); + MainWindow.SaveSettingsToFile(); + } + } + } + + /// + /// 从列表删除Token + /// + private void RemoveTokenFromList(string token) + { + if (MainWindow.Settings?.Dlass != null && MainWindow.Settings.Dlass.SavedTokens != null) + { + MainWindow.Settings.Dlass.SavedTokens.Remove(token); + MainWindow.SaveSettingsToFile(); + } + } + + /// + /// 标题栏拖动事件 + /// + private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (e.ButtonState == MouseButtonState.Pressed) + { + DragMove(); + } + } + + /// + /// 关闭按钮点击事件 + /// + private void BtnClose_Click(object sender, RoutedEventArgs e) + { + Close(); + } + + /// + /// 下拉框选择改变事件 + /// + private void CmbSavedTokens_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) + { + try + { + if (CmbSavedTokens.SelectedItem != null && CmbSavedTokens.SelectedItem.ToString() != "(无保存的Token)") + { + var selectedToken = CmbSavedTokens.SelectedItem.ToString(); + SaveUserToken(selectedToken); + + _apiClient?.Dispose(); + InitializeApiClient(); + + TxtTokenStatus.Text = "已选择Token"; + TxtTokenStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(34, 197, 94)); + + _ = TestConnectionAsync(); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"选择Token时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 保存Token按钮点击事件 + /// + private void BtnSaveToken_Click(object sender, RoutedEventArgs e) + { + try + { + var token = TxtNewToken.Text?.Trim() ?? string.Empty; + if (string.IsNullOrEmpty(token)) + { + MessageBox.Show("请输入新的用户Token", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); + return; + } + + AddTokenToList(token); + SaveUserToken(token); + + _apiClient?.Dispose(); + InitializeApiClient(); + + LoadUserToken(); + + MessageBox.Show("Token已成功保存并已选择", "成功", MessageBoxButton.OK, MessageBoxImage.Information); + + _ = TestConnectionAsync(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存Token时出错: {ex.Message}", LogHelper.LogType.Error); + MessageBox.Show($"保存Token时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + /// + /// 清除Token按钮点击事件 + /// + private void BtnClearToken_Click(object sender, RoutedEventArgs e) + { + try + { + if (CmbSavedTokens.SelectedItem == null || CmbSavedTokens.SelectedItem.ToString() == "(无保存的Token)") + { + MessageBox.Show("请先选择一个Token", "提示", MessageBoxButton.OK, MessageBoxImage.Warning); + return; + } + + var selectedToken = CmbSavedTokens.SelectedItem.ToString(); + var result = MessageBox.Show($"确定要删除已选中的Token吗?", "确认", MessageBoxButton.YesNo, MessageBoxImage.Question); + if (result == MessageBoxResult.Yes) + { + RemoveTokenFromList(selectedToken); + + if (GetUserToken() == selectedToken) + { + SaveUserToken(string.Empty); + } + + _apiClient?.Dispose(); + InitializeApiClient(); + + LoadUserToken(); + + TxtConnectionStatus.Text = "未连接"; + TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(161, 161, 170)); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"删除Token时出错: {ex.Message}", LogHelper.LogType.Error); + MessageBox.Show($"删除Token时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + /// + /// 测试Token连接按钮点击事件 + /// + private async void BtnTestToken_Click(object sender, RoutedEventArgs e) + { + await TestConnectionAsync(); + } + + /// + /// 保存按钮点击事件 + /// + private async void BtnSave_Click(object sender, RoutedEventArgs e) + { + try + { + // TODO: 根据实际API文档实现保存逻辑 + // 示例:保存设置到服务器 + // var settings = new { ... }; + // await _apiClient.PostAsync("/api/settings", settings); + + MessageBox.Show("设置已保存", "成功", MessageBoxButton.OK, MessageBoxImage.Information); + Close(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存设置时出错: {ex.Message}", LogHelper.LogType.Error); + MessageBox.Show($"保存设置时发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + /// + /// 取消按钮点击事件 + /// + private void BtnCancel_Click(object sender, RoutedEventArgs e) + { + Close(); + } + + /// + /// 测试API连接 + /// + private async Task TestConnectionAsync() + { + Dispatcher.Invoke(() => + { + TxtConnectionStatus.Text = "测试中..."; + TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(161, 161, 170)); // 灰色 + }); + + try + { + var userToken = GetUserToken(); + if (string.IsNullOrEmpty(userToken)) + { + Dispatcher.Invoke(() => + { + TxtConnectionStatus.Text = "未设置Token"; + TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(239, 68, 68)); // 红色 + }); + return; + } + + // 根据文档,使用 auth-with-token 接口验证token + // 此接口需要POST请求,包含app_id, app_secret和user_token + try + { + var authData = new + { + app_id = APP_ID, + app_secret = APP_SECRET, + user_token = userToken + }; + + var result = await _apiClient.PostAsync("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false); + + if (result != null && result.Success) + { + var whiteboardCount = result.Whiteboards?.Count ?? 0; + + Dispatcher.Invoke(() => + { + TxtConnectionStatus.Text = $"已连接 (找到 {whiteboardCount} 个白板)"; + TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(34, 197, 94)); + }); + } + else + { + throw new Exception("认证响应失败"); + } + } + catch (Exception ex) + { + if (userToken.Length < 10) + { + throw new Exception("Token格式可能不正确(长度过短,至少需要10个字符)"); + } + + LogHelper.WriteLogToFile($"Token验证失败: {ex.Message}", LogHelper.LogType.Error); + throw; + } + + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"Dlass API连接测试失败: {ex.Message}", LogHelper.LogType.Error); + Dispatcher.Invoke(() => + { + TxtConnectionStatus.Text = "连接失败"; + TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(239, 68, 68)); // 红色 + }); + } + } + } + + #region API响应模型 + + /// + /// auth-with-token接口响应模型 + /// + public class AuthWithTokenResponse + { + [Newtonsoft.Json.JsonProperty("success")] + public bool Success { get; set; } + + [Newtonsoft.Json.JsonProperty("whiteboards")] + public List Whiteboards { get; set; } + + [Newtonsoft.Json.JsonProperty("count")] + public int Count { get; set; } + + [Newtonsoft.Json.JsonProperty("user")] + public UserInfo User { get; set; } + } + + /// + /// 白板信息模型 + /// + public class WhiteboardInfo + { + [Newtonsoft.Json.JsonProperty("id")] + public int Id { get; set; } + + [Newtonsoft.Json.JsonProperty("name")] + public string Name { get; set; } + + [Newtonsoft.Json.JsonProperty("board_id")] + public string BoardId { get; set; } + + [Newtonsoft.Json.JsonProperty("secret_key")] + public string SecretKey { get; set; } + + [Newtonsoft.Json.JsonProperty("class_name")] + public string ClassName { get; set; } + + [Newtonsoft.Json.JsonProperty("class_id")] + public int ClassId { get; set; } + + [Newtonsoft.Json.JsonProperty("is_online")] + public bool IsOnline { get; set; } + + [Newtonsoft.Json.JsonProperty("last_heartbeat")] + public string LastHeartbeat { get; set; } + + [Newtonsoft.Json.JsonProperty("created_at")] + public string CreatedAt { get; set; } + } + + /// + /// 用户信息模型 + /// + public class UserInfo + { + [Newtonsoft.Json.JsonProperty("id")] + public int Id { get; set; } + + [Newtonsoft.Json.JsonProperty("username")] + public string Username { get; set; } + + [Newtonsoft.Json.JsonProperty("email")] + public string Email { get; set; } + } + + #endregion +} + From 4ef77c2e72f0150cbf53a36ac63ad0141b190f23 Mon Sep 17 00:00:00 2001 From: CJK_mkp <113243675+CJKmkp@users.noreply.github.com> Date: Sun, 2 Nov 2025 09:41:53 +0800 Subject: [PATCH 02/25] =?UTF-8?q?add:Dlass=E8=81=94=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Resources/Settings.cs | 3 + Ink Canvas/Windows/DlassSettingsWindow.xaml | 32 +++++ .../Windows/DlassSettingsWindow.xaml.cs | 127 +++++++++++++++++- 3 files changed, 160 insertions(+), 2 deletions(-) diff --git a/Ink Canvas/Resources/Settings.cs b/Ink Canvas/Resources/Settings.cs index ccb2615f..4ec614da 100644 --- a/Ink Canvas/Resources/Settings.cs +++ b/Ink Canvas/Resources/Settings.cs @@ -735,6 +735,9 @@ namespace Ink_Canvas [JsonProperty("savedTokens")] public List SavedTokens { get; set; } = new List(); + [JsonProperty("selectedClassName")] + public string SelectedClassName { get; set; } = string.Empty; + [JsonProperty("apiBaseUrl")] public string ApiBaseUrl { get; set; } = "https://dlass.tech"; } diff --git a/Ink Canvas/Windows/DlassSettingsWindow.xaml b/Ink Canvas/Windows/DlassSettingsWindow.xaml index 0ec103df..a8e998ee 100644 --- a/Ink Canvas/Windows/DlassSettingsWindow.xaml +++ b/Ink Canvas/Windows/DlassSettingsWindow.xaml @@ -280,6 +280,38 @@ Margin="0,4,0,0"/> + + + + + + + + + + _currentWhiteboards = new List(); + private UserInfo _currentUser; public DlassSettingsWindow(MainWindow mainWindow = null) { InitializeComponent(); + // 初始化班级下拉框 + CmbClassSelection.Items.Clear(); + CmbClassSelection.Items.Add("(等待连接)"); + CmbClassSelection.SelectedIndex = 0; + CmbClassSelection.IsEnabled = false; + // 加载保存的token LoadUserToken(); @@ -184,6 +193,85 @@ namespace Ink_Canvas.Windows } } + /// + /// 加载班级列表到下拉框 + /// + private void LoadClasses(List whiteboards, UserInfo user = null) + { + CmbClassSelection.Items.Clear(); + + if (whiteboards != null && whiteboards.Count > 0) + { + var teacherName = user?.Username ?? "未知教师"; + var classGroups = whiteboards + .Where(w => !string.IsNullOrEmpty(w.ClassName)) + .GroupBy(w => w.ClassName) + .OrderBy(g => g.Key) + .ToList(); + + foreach (var group in classGroups) + { + var className = group.Key; + var displayText = $"{teacherName} - {className}"; + CmbClassSelection.Items.Add(new ClassSelectionItem + { + DisplayText = displayText, + ClassName = className, + TeacherName = teacherName + }); + } + + var savedClassName = MainWindow.Settings?.Dlass?.SelectedClassName ?? string.Empty; + if (!string.IsNullOrEmpty(savedClassName)) + { + var savedItem = CmbClassSelection.Items.Cast() + .FirstOrDefault(item => item.ClassName == savedClassName); + if (savedItem != null) + { + CmbClassSelection.SelectedItem = savedItem; + } + else if (CmbClassSelection.Items.Count > 0) + { + CmbClassSelection.SelectedIndex = 0; + } + } + else if (CmbClassSelection.Items.Count > 0) + { + CmbClassSelection.SelectedIndex = 0; + } + + CmbClassSelection.IsEnabled = true; + } + else + { + CmbClassSelection.Items.Add("(无可用班级)"); + CmbClassSelection.SelectedIndex = 0; + CmbClassSelection.IsEnabled = false; + } + } + + /// + /// 班级选择改变事件 + /// + private void CmbClassSelection_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e) + { + try + { + if (CmbClassSelection.SelectedItem is ClassSelectionItem selectedItem) + { + if (MainWindow.Settings?.Dlass != null) + { + MainWindow.Settings.Dlass.SelectedClassName = selectedItem.ClassName; + MainWindow.SaveSettingsToFile(); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"选择班级时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + /// /// 标题栏拖动事件 /// @@ -292,6 +380,13 @@ namespace Ink_Canvas.Windows LoadUserToken(); + CmbClassSelection.Items.Clear(); + CmbClassSelection.Items.Add("(等待连接)"); + CmbClassSelection.SelectedIndex = 0; + CmbClassSelection.IsEnabled = false; + _currentWhiteboards.Clear(); + _currentUser = null; + TxtConnectionStatus.Text = "未连接"; TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(161, 161, 170)); } @@ -380,12 +475,18 @@ namespace Ink_Canvas.Windows if (result != null && result.Success) { - var whiteboardCount = result.Whiteboards?.Count ?? 0; + var whiteboards = result.Whiteboards ?? new List(); + _currentWhiteboards = whiteboards; + _currentUser = result.User; + var whiteboardCount = whiteboards.Count; Dispatcher.Invoke(() => { TxtConnectionStatus.Text = $"已连接 (找到 {whiteboardCount} 个白板)"; TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(34, 197, 94)); + + // 加载班级列表 + LoadClasses(whiteboards, result.User); }); } else @@ -411,7 +512,14 @@ namespace Ink_Canvas.Windows Dispatcher.Invoke(() => { TxtConnectionStatus.Text = "连接失败"; - TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(239, 68, 68)); // 红色 + TxtConnectionStatus.Foreground = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(239, 68, 68)); + + // 清空班级列表 + CmbClassSelection.Items.Clear(); + CmbClassSelection.Items.Add("(无可用班级)"); + CmbClassSelection.SelectedIndex = 0; + CmbClassSelection.IsEnabled = false; + _currentWhiteboards.Clear(); }); } } @@ -485,6 +593,21 @@ namespace Ink_Canvas.Windows public string Email { get; set; } } + /// + /// 班级选择项 + /// + public class ClassSelectionItem + { + public string DisplayText { get; set; } + public string ClassName { get; set; } + public string TeacherName { get; set; } + + public override string ToString() + { + return DisplayText; + } + } + #endregion } From 4fb70310609364e270f2bea27cd48a588279632a Mon Sep 17 00:00:00 2001 From: CJK_mkp <113243675+CJKmkp@users.noreply.github.com> Date: Sun, 2 Nov 2025 10:11:15 +0800 Subject: [PATCH 03/25] =?UTF-8?q?add:Dlass=E8=81=94=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Helpers/DlassApiClient.cs | 82 +++++++ Ink Canvas/Helpers/DlassNoteUploader.cs | 200 ++++++++++++++++++ .../MainWindow_cs/MW_Save&OpenStrokes.cs | 18 ++ Ink Canvas/MainWindow_cs/MW_Screenshot.cs | 24 +++ Ink Canvas/Resources/Settings.cs | 6 + Ink Canvas/Windows/DlassSettingsWindow.xaml | 116 ++++++++-- .../Windows/DlassSettingsWindow.xaml.cs | 96 +++++++++ 7 files changed, 521 insertions(+), 21 deletions(-) create mode 100644 Ink Canvas/Helpers/DlassNoteUploader.cs diff --git a/Ink Canvas/Helpers/DlassApiClient.cs b/Ink Canvas/Helpers/DlassApiClient.cs index 438c4947..61a04f42 100644 --- a/Ink Canvas/Helpers/DlassApiClient.cs +++ b/Ink Canvas/Helpers/DlassApiClient.cs @@ -1,7 +1,9 @@ using Ink_Canvas.Helpers; using Newtonsoft.Json; using System; +using System.IO; using System.Net.Http; +using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; @@ -343,6 +345,86 @@ namespace Ink_Canvas.Helpers } } + /// + /// 上传笔记文件 + /// + /// 上传端点 + /// 文件路径 + /// 白板ID + /// 白板密钥 + /// 笔记标题(可选) + /// 笔记描述(可选) + /// 笔记标签(可选) + public async Task UploadNoteAsync(string endpoint, string filePath, string boardId, string secretKey, string title = null, string description = null, string tags = null) + { + try + { + if (!File.Exists(filePath)) + { + throw new FileNotFoundException($"文件不存在: {filePath}"); + } + + var request = new HttpRequestMessage(HttpMethod.Post, endpoint); + + // 设置白板认证头 + request.Headers.Add("X-Board-ID", boardId); + request.Headers.Add("X-Secret-Key", secretKey); + + // 创建multipart/form-data内容 + var content = new MultipartFormDataContent(); + + // 添加文件 + var fileContent = new ByteArrayContent(File.ReadAllBytes(filePath)); + var fileName = Path.GetFileName(filePath); + fileContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + content.Add(fileContent, "file", fileName); + + // 添加可选参数 + if (!string.IsNullOrEmpty(title)) + { + content.Add(new StringContent(title), "title"); + } + if (!string.IsNullOrEmpty(description)) + { + content.Add(new StringContent(description), "description"); + } + if (!string.IsNullOrEmpty(tags)) + { + content.Add(new StringContent(tags), "tags"); + } + + request.Content = content; + + var response = await _httpClient.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + if (response.IsSuccessStatusCode) + { + if (string.IsNullOrEmpty(responseContent)) + { + return default(T); + } + return JsonConvert.DeserializeObject(responseContent); + } + else + { + throw new Exception($"上传文件失败: {response.StatusCode} - {responseContent}"); + } + } + catch (HttpRequestException httpEx) + { + throw new Exception($"上传文件时网络错误: {httpEx.Message}", httpEx); + } + catch (TaskCanceledException timeoutEx) + { + throw new Exception($"上传文件超时: {endpoint}", timeoutEx); + } + catch (Exception ex) + { + throw new Exception($"上传文件时出错: {ex.Message}", ex); + } + } + /// /// 释放资源 /// diff --git a/Ink Canvas/Helpers/DlassNoteUploader.cs b/Ink Canvas/Helpers/DlassNoteUploader.cs new file mode 100644 index 00000000..ed08fece --- /dev/null +++ b/Ink Canvas/Helpers/DlassNoteUploader.cs @@ -0,0 +1,200 @@ +using Ink_Canvas.Helpers; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Ink_Canvas.Helpers +{ + /// + /// Dlass笔记自动上传辅助类 + /// + public class DlassNoteUploader + { + private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA"; + private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA"; + + /// + /// 上传笔记响应模型 + /// + public class UploadNoteResponse + { + [JsonProperty("success")] + public bool Success { get; set; } + + [JsonProperty("message")] + public string Message { get; set; } + + [JsonProperty("note_id")] + public int? NoteId { get; set; } + + [JsonProperty("filename")] + public string Filename { get; set; } + + [JsonProperty("file_path")] + public string FilePath { get; set; } + + [JsonProperty("file_url")] + public string FileUrl { get; set; } + } + + /// + /// 白板信息模型(用于查找白板) + /// + private class WhiteboardInfo + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("board_id")] + public string BoardId { get; set; } + + [JsonProperty("secret_key")] + public string SecretKey { get; set; } + + [JsonProperty("class_name")] + public string ClassName { get; set; } + + [JsonProperty("class_id")] + public int ClassId { get; set; } + } + + /// + /// 认证响应模型 + /// + private class AuthWithTokenResponse + { + [JsonProperty("success")] + public bool Success { get; set; } + + [JsonProperty("whiteboards")] + public List Whiteboards { get; set; } + } + + /// + /// 异步上传PNG文件到Dlass + /// + /// PNG文件路径 + /// 是否上传成功 + public static async Task UploadPngNoteAsync(string pngFilePath) + { + try + { + // 检查是否启用自动上传 + if (MainWindow.Settings?.Dlass?.IsAutoUploadNotes != true) + { + return false; + } + + // 检查文件是否存在 + if (!File.Exists(pngFilePath)) + { + LogHelper.WriteLogToFile($"上传失败:文件不存在 - {pngFilePath}", LogHelper.LogType.Error); + return false; + } + + // 检查文件大小(最大10MB) + var fileInfo = new FileInfo(pngFilePath); + if (fileInfo.Length > 10 * 1024 * 1024) + { + LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过10MB限制", LogHelper.LogType.Error); + return false; + } + + // 获取设置的班级名称 + var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName; + if (string.IsNullOrEmpty(selectedClassName)) + { + LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error); + return false; + } + + // 获取用户Token + var userToken = MainWindow.Settings?.Dlass?.UserToken; + if (string.IsNullOrEmpty(userToken)) + { + LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error); + return false; + } + + // 获取API基础URL + var apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech"; + + // 创建API客户端并获取白板信息 + DlassApiClient apiClient = null; + try + { + apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken); + + // 调用认证接口获取白板列表 + var authData = new + { + app_id = APP_ID, + app_secret = APP_SECRET, + user_token = userToken + }; + + var authResult = await apiClient.PostAsync("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false); + + if (authResult == null || !authResult.Success || authResult.Whiteboards == null) + { + LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error); + return false; + } + + // 查找匹配班级的白板 + var whiteboard = authResult.Whiteboards + .FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName); + + if (whiteboard == null || string.IsNullOrEmpty(whiteboard.BoardId) || string.IsNullOrEmpty(whiteboard.SecretKey)) + { + LogHelper.WriteLogToFile($"上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error); + return false; + } + + // 准备上传参数 + var fileName = Path.GetFileNameWithoutExtension(pngFilePath); + var title = fileName; + var description = $"自动上传的笔记 - {DateTime.Now:yyyy-MM-dd HH:mm:ss}"; + var tags = "自动上传,笔记"; + + // 上传文件 + var uploadResult = await apiClient.UploadNoteAsync( + "/api/whiteboard/upload_note", + pngFilePath, + whiteboard.BoardId, + whiteboard.SecretKey, + title, + description, + tags); + + if (uploadResult != null && uploadResult.Success) + { + LogHelper.WriteLogToFile($"笔记上传成功:{fileName} -> {uploadResult.FileUrl}", LogHelper.LogType.Event); + return true; + } + else + { + LogHelper.WriteLogToFile($"上传失败:服务器响应失败 - {uploadResult?.Message ?? "未知错误"}", LogHelper.LogType.Error); + return false; + } + } + finally + { + apiClient?.Dispose(); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"上传笔记时出错: {ex.Message}", LogHelper.LogType.Error); + return false; + } + } + } +} + diff --git a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs b/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs index b0aae681..bcbc487c 100644 --- a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs +++ b/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs @@ -7,6 +7,7 @@ using System.Drawing.Imaging; using System.IO; using System.IO.Compression; using System.Text; +using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Forms; @@ -310,6 +311,23 @@ namespace Ink_Canvas var fs = new FileStream(savePathWithName, FileMode.Create); inkCanvas.Strokes.Save(fs); fs.Close(); + + _ = Task.Run(async () => + { + try + { + var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0; + if (delayMinutes > 0) + { + await Task.Delay(TimeSpan.FromMinutes(delayMinutes)); + } + + await Helpers.DlassNoteUploader.UploadPngNoteAsync(imagePathWithName); + } + catch (Exception) + { + } + }); } } diff --git a/Ink Canvas/MainWindow_cs/MW_Screenshot.cs b/Ink Canvas/MainWindow_cs/MW_Screenshot.cs index f398a4e9..844c0982 100644 --- a/Ink Canvas/MainWindow_cs/MW_Screenshot.cs +++ b/Ink Canvas/MainWindow_cs/MW_Screenshot.cs @@ -3,6 +3,7 @@ using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; +using System.Threading.Tasks; using System.Windows; using System.Windows.Forms; @@ -65,6 +66,29 @@ namespace Ink_Canvas { ShowNotification($"截图成功保存至 {savePath}"); } + _ = Task.Run(async () => + { + try + { + var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0; + if (delayMinutes > 0) + { + await Task.Delay(TimeSpan.FromMinutes(delayMinutes)); + } + + var uploaded = await Helpers.DlassNoteUploader.UploadPngNoteAsync(savePath); + if (uploaded && !isHideNotification) + { + Dispatcher.Invoke(() => + { + ShowNotification($"笔记已自动上传到Dlass"); + }); + } + } + catch (Exception) + { + } + }); } // 获取日期文件夹路径 diff --git a/Ink Canvas/Resources/Settings.cs b/Ink Canvas/Resources/Settings.cs index 4ec614da..3941e2ac 100644 --- a/Ink Canvas/Resources/Settings.cs +++ b/Ink Canvas/Resources/Settings.cs @@ -740,5 +740,11 @@ namespace Ink_Canvas [JsonProperty("apiBaseUrl")] public string ApiBaseUrl { get; set; } = "https://dlass.tech"; + + [JsonProperty("isAutoUploadNotes")] + public bool IsAutoUploadNotes { get; set; } = false; + + [JsonProperty("autoUploadDelayMinutes")] + public int AutoUploadDelayMinutes { get; set; } = 0; } } diff --git a/Ink Canvas/Windows/DlassSettingsWindow.xaml b/Ink Canvas/Windows/DlassSettingsWindow.xaml index a8e998ee..fe993dd6 100644 --- a/Ink Canvas/Windows/DlassSettingsWindow.xaml +++ b/Ink Canvas/Windows/DlassSettingsWindow.xaml @@ -106,22 +106,22 @@ X1="0" Y1="0" X2="1" Y2="0" Stroke="{StaticResource BorderBrush}" StrokeThickness="1" - Margin="0,8,0,12"/> + Margin="0,2,0,2"/> + Margin="0,0,0,1"/> + Margin="0,0,0,2"/> - + + + + + + + + Margin="0,2,0,1"/> + Margin="0,0,0,1"/> + Margin="0,0,0,2"/> + Margin="0,2,0,1"/> - - - - + + + + + + + + TextWrapping="Wrap" + Margin="0,0,0,2"/> + + + + + + + + + + + + + Margin="0,2,0,1"/> diff --git a/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs b/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs index 494e17d9..a68d7640 100644 --- a/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs +++ b/Ink Canvas/Windows/DlassSettingsWindow.xaml.cs @@ -2,6 +2,7 @@ using Ink_Canvas.Helpers; using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows; using System.Windows.Input; @@ -34,6 +35,9 @@ namespace Ink_Canvas.Windows // 加载保存的token LoadUserToken(); + // 加载自动上传设置 + LoadAutoUploadSettings(); + // 初始化API客户端(优先使用用户token) InitializeApiClient(); @@ -272,6 +276,98 @@ namespace Ink_Canvas.Windows } } + /// + /// 加载自动上传设置 + /// + private void LoadAutoUploadSettings() + { + try + { + if (MainWindow.Settings?.Dlass != null) + { + ToggleSwitchAutoUploadNotes.IsOn = MainWindow.Settings.Dlass.IsAutoUploadNotes; + var delayMinutes = MainWindow.Settings.Dlass.AutoUploadDelayMinutes; + if (delayMinutes < 0 || delayMinutes > 60) + { + delayMinutes = 0; + } + TxtUploadDelayMinutes.Text = delayMinutes.ToString(); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"加载自动上传设置时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 自动上传开关切换事件 + /// + private void ToggleSwitchAutoUploadNotes_Toggled(object sender, RoutedEventArgs e) + { + try + { + if (MainWindow.Settings?.Dlass != null) + { + MainWindow.Settings.Dlass.IsAutoUploadNotes = ToggleSwitchAutoUploadNotes.IsOn; + MainWindow.SaveSettingsToFile(); + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存自动上传设置时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 上传延迟时间输入框文本改变事件 + /// + private void TxtUploadDelayMinutes_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) + { + try + { + if (MainWindow.Settings?.Dlass != null && int.TryParse(TxtUploadDelayMinutes.Text, out int delayMinutes)) + { + // 限制范围在0-60分钟 + if (delayMinutes < 0) + { + delayMinutes = 0; + TxtUploadDelayMinutes.Text = "0"; + } + else if (delayMinutes > 60) + { + delayMinutes = 60; + TxtUploadDelayMinutes.Text = "60"; + } + + MainWindow.Settings.Dlass.AutoUploadDelayMinutes = delayMinutes; + MainWindow.SaveSettingsToFile(); + } + else if (string.IsNullOrWhiteSpace(TxtUploadDelayMinutes.Text)) + { + // 空文本时设置为0 + if (MainWindow.Settings?.Dlass != null) + { + MainWindow.Settings.Dlass.AutoUploadDelayMinutes = 0; + MainWindow.SaveSettingsToFile(); + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"保存上传延迟时间时出错: {ex.Message}", LogHelper.LogType.Error); + } + } + + /// + /// 上传延迟时间输入框预览文本输入事件(只允许数字) + /// + private void TxtUploadDelayMinutes_PreviewTextInput(object sender, TextCompositionEventArgs e) + { + Regex regex = new Regex("[^0-9]+"); + e.Handled = regex.IsMatch(e.Text); + } + /// /// 标题栏拖动事件 /// From b6020481866bc91b43fd2db1c0e377311505978f Mon Sep 17 00:00:00 2001 From: CJK_mkp <113243675+CJKmkp@users.noreply.github.com> Date: Sun, 2 Nov 2025 10:16:31 +0800 Subject: [PATCH 04/25] =?UTF-8?q?add:Dlass=E8=81=94=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Helpers/DlassNoteUploader.cs | 23 +++++++++++++++++-- .../MainWindow_cs/MW_Save&OpenStrokes.cs | 19 ++++++++++++++- Ink Canvas/MainWindow_cs/MW_Screenshot.cs | 9 +------- Ink Canvas/Windows/DlassSettingsWindow.xaml | 4 ++-- 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/Ink Canvas/Helpers/DlassNoteUploader.cs b/Ink Canvas/Helpers/DlassNoteUploader.cs index ed08fece..30da0f51 100644 --- a/Ink Canvas/Helpers/DlassNoteUploader.cs +++ b/Ink Canvas/Helpers/DlassNoteUploader.cs @@ -76,6 +76,16 @@ namespace Ink_Canvas.Helpers public List Whiteboards { get; set; } } + /// + /// 异步上传笔记文件到Dlass(支持PNG和ICSTK格式) + /// + /// 文件路径(支持PNG和ICSTK) + /// 是否上传成功 + public static async Task UploadNoteFileAsync(string filePath) + { + return await UploadPngNoteAsync(filePath); + } + /// /// 异步上传PNG文件到Dlass /// @@ -98,6 +108,14 @@ namespace Ink_Canvas.Helpers return false; } + // 检查文件扩展名 + var fileExtension = Path.GetExtension(pngFilePath).ToLower(); + if (fileExtension != ".png" && fileExtension != ".icstk") + { + LogHelper.WriteLogToFile($"上传失败:不支持的文件格式 - {fileExtension},仅支持PNG和ICSTK", LogHelper.LogType.Error); + return false; + } + // 检查文件大小(最大10MB) var fileInfo = new FileInfo(pngFilePath); if (fileInfo.Length > 10 * 1024 * 1024) @@ -160,8 +178,9 @@ namespace Ink_Canvas.Helpers // 准备上传参数 var fileName = Path.GetFileNameWithoutExtension(pngFilePath); var title = fileName; - var description = $"自动上传的笔记 - {DateTime.Now:yyyy-MM-dd HH:mm:ss}"; - var tags = "自动上传,笔记"; + var fileType = fileExtension == ".icstk" ? "墨迹文件" : "笔记"; + var description = $"自动上传的{fileType} - {DateTime.Now:yyyy-MM-dd HH:mm:ss}"; + var tags = fileExtension == ".icstk" ? "自动上传,墨迹,icstk" : "自动上传,笔记,png"; // 上传文件 var uploadResult = await apiClient.UploadNoteAsync( diff --git a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs b/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs index bcbc487c..0e3b6f12 100644 --- a/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs +++ b/Ink Canvas/MainWindow_cs/MW_Save&OpenStrokes.cs @@ -133,6 +133,23 @@ namespace Ink_Canvas var fs = new FileStream(savePathWithName, FileMode.Create); inkCanvas.Strokes.Save(fs); fs.Close(); + _ = Task.Run(async () => + { + try + { + var delayMinutes = Settings?.Dlass?.AutoUploadDelayMinutes ?? 0; + if (delayMinutes > 0) + { + await Task.Delay(TimeSpan.FromMinutes(delayMinutes)); + } + + await Helpers.DlassNoteUploader.UploadNoteFileAsync(savePathWithName); + } + catch (Exception) + { + } + }); + // 保存元素信息 var elementInfos = new List(); foreach (var child in inkCanvas.Children) @@ -322,7 +339,7 @@ namespace Ink_Canvas await Task.Delay(TimeSpan.FromMinutes(delayMinutes)); } - await Helpers.DlassNoteUploader.UploadPngNoteAsync(imagePathWithName); + await Helpers.DlassNoteUploader.UploadNoteFileAsync(imagePathWithName); } catch (Exception) { diff --git a/Ink Canvas/MainWindow_cs/MW_Screenshot.cs b/Ink Canvas/MainWindow_cs/MW_Screenshot.cs index 844c0982..18ef9fec 100644 --- a/Ink Canvas/MainWindow_cs/MW_Screenshot.cs +++ b/Ink Canvas/MainWindow_cs/MW_Screenshot.cs @@ -76,14 +76,7 @@ namespace Ink_Canvas await Task.Delay(TimeSpan.FromMinutes(delayMinutes)); } - var uploaded = await Helpers.DlassNoteUploader.UploadPngNoteAsync(savePath); - if (uploaded && !isHideNotification) - { - Dispatcher.Invoke(() => - { - ShowNotification($"笔记已自动上传到Dlass"); - }); - } + await Helpers.DlassNoteUploader.UploadNoteFileAsync(savePath); } catch (Exception) { diff --git a/Ink Canvas/Windows/DlassSettingsWindow.xaml b/Ink Canvas/Windows/DlassSettingsWindow.xaml index fe993dd6..9591776b 100644 --- a/Ink Canvas/Windows/DlassSettingsWindow.xaml +++ b/Ink Canvas/Windows/DlassSettingsWindow.xaml @@ -341,13 +341,13 @@ - Date: Sun, 2 Nov 2025 10:30:36 +0800 Subject: [PATCH 05/25] =?UTF-8?q?add:Dlass=E8=81=94=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Helpers/DlassApiClient.cs | 2 +- Ink Canvas/Helpers/DlassNoteUploader.cs | 301 +++++++++++++++++++----- 2 files changed, 239 insertions(+), 64 deletions(-) diff --git a/Ink Canvas/Helpers/DlassApiClient.cs b/Ink Canvas/Helpers/DlassApiClient.cs index 61a04f42..d77bed80 100644 --- a/Ink Canvas/Helpers/DlassApiClient.cs +++ b/Ink Canvas/Helpers/DlassApiClient.cs @@ -12,7 +12,7 @@ namespace Ink_Canvas.Helpers /// /// Dlass API 客户端,用于与服务端通信 /// - public class DlassApiClient + public class DlassApiClient : IDisposable { private const string DEFAULT_BASE_URL = "https://dlass.tech"; private readonly string _appId; diff --git a/Ink Canvas/Helpers/DlassNoteUploader.cs b/Ink Canvas/Helpers/DlassNoteUploader.cs index 30da0f51..0d67e3a2 100644 --- a/Ink Canvas/Helpers/DlassNoteUploader.cs +++ b/Ink Canvas/Helpers/DlassNoteUploader.cs @@ -1,9 +1,11 @@ using Ink_Canvas.Helpers; using Newtonsoft.Json; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace Ink_Canvas.Helpers @@ -15,6 +17,17 @@ namespace Ink_Canvas.Helpers { private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA"; private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA"; + private const int BATCH_SIZE = 10; // 批量上传大小 + + /// + /// 上传队列(线程安全) + /// + private static readonly ConcurrentQueue _uploadQueue = new ConcurrentQueue(); + + /// + /// 队列处理锁,防止并发处理 + /// + private static readonly SemaphoreSlim _queueProcessingLock = new SemaphoreSlim(1, 1); /// /// 上传笔记响应模型 @@ -80,18 +93,8 @@ namespace Ink_Canvas.Helpers /// 异步上传笔记文件到Dlass(支持PNG和ICSTK格式) /// /// 文件路径(支持PNG和ICSTK) - /// 是否上传成功 + /// 是否成功加入队列(不等待实际上传完成) public static async Task UploadNoteFileAsync(string filePath) - { - return await UploadPngNoteAsync(filePath); - } - - /// - /// 异步上传PNG文件到Dlass - /// - /// PNG文件路径 - /// 是否上传成功 - public static async Task UploadPngNoteAsync(string pngFilePath) { try { @@ -101,91 +104,267 @@ namespace Ink_Canvas.Helpers return false; } - // 检查文件是否存在 - if (!File.Exists(pngFilePath)) + // 基本验证 + if (!File.Exists(filePath)) { - LogHelper.WriteLogToFile($"上传失败:文件不存在 - {pngFilePath}", LogHelper.LogType.Error); + LogHelper.WriteLogToFile($"上传失败:文件不存在 - {filePath}", LogHelper.LogType.Error); return false; } - // 检查文件扩展名 - var fileExtension = Path.GetExtension(pngFilePath).ToLower(); + var fileExtension = Path.GetExtension(filePath).ToLower(); if (fileExtension != ".png" && fileExtension != ".icstk") { - LogHelper.WriteLogToFile($"上传失败:不支持的文件格式 - {fileExtension},仅支持PNG和ICSTK", LogHelper.LogType.Error); return false; } - // 检查文件大小(最大10MB) - var fileInfo = new FileInfo(pngFilePath); + var fileInfo = new FileInfo(filePath); if (fileInfo.Length > 10 * 1024 * 1024) { LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过10MB限制", LogHelper.LogType.Error); return false; } - // 获取设置的班级名称 - var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName; - if (string.IsNullOrEmpty(selectedClassName)) + // 获取上传延迟时间(分钟) + var delayMinutes = MainWindow.Settings?.Dlass?.AutoUploadDelayMinutes ?? 0; + + // 如果设置了延迟时间,在后台任务中等待后再加入队列 + if (delayMinutes > 0) { - LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error); - return false; + _ = Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMinutes(delayMinutes)); + EnqueueFile(filePath); + }); + } + else + { + EnqueueFile(filePath); } - // 获取用户Token - var userToken = MainWindow.Settings?.Dlass?.UserToken; - if (string.IsNullOrEmpty(userToken)) + return true; + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"加入上传队列时出错: {ex.Message}", LogHelper.LogType.Error); + return false; + } + } + + /// + /// 将文件加入上传队列 + /// + private static void EnqueueFile(string filePath) + { + _uploadQueue.Enqueue(filePath); + + // 如果队列达到批量大小,触发批量上传 + if (_uploadQueue.Count >= BATCH_SIZE) + { + _ = ProcessUploadQueueAsync(); + } + } + + /// + /// 处理上传队列,批量上传文件 + /// + private static async Task ProcessUploadQueueAsync() + { + // 使用信号量防止并发处理 + if (!await _queueProcessingLock.WaitAsync(0)) + { + return; // 已有处理任务在运行 + } + + try + { + var filesToUpload = new List(); + + // 从队列中取出最多BATCH_SIZE个文件 + while (filesToUpload.Count < BATCH_SIZE && _uploadQueue.TryDequeue(out string filePath)) { - LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error); - return false; + // 再次检查文件是否存在(可能在队列中时被删除) + if (File.Exists(filePath)) + { + filesToUpload.Add(filePath); + } } - // 获取API基础URL - var apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech"; + if (filesToUpload.Count == 0) + { + return; + } - // 创建API客户端并获取白板信息 - DlassApiClient apiClient = null; + // 获取共享的白板信息(同一批次的所有文件共享认证信息) + WhiteboardInfo sharedWhiteboard = null; + string apiBaseUrl = null; + string userToken = null; + try { - apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken); - - // 调用认证接口获取白板列表 - var authData = new + var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName; + if (string.IsNullOrEmpty(selectedClassName)) { - app_id = APP_ID, - app_secret = APP_SECRET, - user_token = userToken - }; + LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error); + return; + } - var authResult = await apiClient.PostAsync("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false); - - if (authResult == null || !authResult.Success || authResult.Whiteboards == null) + userToken = MainWindow.Settings?.Dlass?.UserToken; + if (string.IsNullOrEmpty(userToken)) { - LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error); + LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error); + return; + } + + apiBaseUrl = MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech"; + + // 获取白板信息(只获取一次,所有文件共享) + using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken)) + { + var authData = new + { + app_id = APP_ID, + app_secret = APP_SECRET, + user_token = userToken + }; + + var authResult = await apiClient.PostAsync("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false); + + if (authResult == null || !authResult.Success || authResult.Whiteboards == null) + { + LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error); + return; + } + + sharedWhiteboard = authResult.Whiteboards + .FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName); + + if (sharedWhiteboard == null || string.IsNullOrEmpty(sharedWhiteboard.BoardId) || string.IsNullOrEmpty(sharedWhiteboard.SecretKey)) + { + LogHelper.WriteLogToFile($"上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error); + return; + } + } + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"批量上传获取白板信息时出错: {ex.Message}", LogHelper.LogType.Error); + return; + } + + // 并发上传所有文件(共享白板信息) + var uploadTasks = filesToUpload.Select(filePath => UploadFileInternalAsync(filePath, sharedWhiteboard, apiBaseUrl, userToken)); + await Task.WhenAll(uploadTasks); + + // 如果队列中还有文件,继续处理 + if (_uploadQueue.Count >= BATCH_SIZE) + { + _ = ProcessUploadQueueAsync(); + } + } + finally + { + _queueProcessingLock.Release(); + } + } + + /// + /// 内部上传方法,执行实际上传操作 + /// + /// 文件路径 + /// 白板信息(如果为null则重新获取) + /// API基础URL(如果为null则从设置获取) + /// 用户Token(如果为null则从设置获取) + private static async Task UploadFileInternalAsync(string filePath, WhiteboardInfo whiteboard = null, string apiBaseUrl = null, string userToken = null) + { + try + { + // 再次检查文件是否存在(可能在队列等待时被删除) + if (!File.Exists(filePath)) + { + return false; + } + + // 检查文件扩展名 + var fileExtension = Path.GetExtension(filePath).ToLower(); + if (fileExtension != ".png" && fileExtension != ".icstk") + { + return false; + } + + // 检查文件大小(最大10MB) + var fileInfo = new FileInfo(filePath); + if (fileInfo.Length > 10 * 1024 * 1024) + { + LogHelper.WriteLogToFile($"上传失败:文件过大({fileInfo.Length / 1024 / 1024}MB),超过10MB限制", LogHelper.LogType.Error); + return false; + } + + // 如果白板信息未提供,则重新获取 + if (whiteboard == null) + { + var selectedClassName = MainWindow.Settings?.Dlass?.SelectedClassName; + if (string.IsNullOrEmpty(selectedClassName)) + { + LogHelper.WriteLogToFile("上传失败:未选择班级", LogHelper.LogType.Error); return false; } - // 查找匹配班级的白板 - var whiteboard = authResult.Whiteboards - .FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName); - - if (whiteboard == null || string.IsNullOrEmpty(whiteboard.BoardId) || string.IsNullOrEmpty(whiteboard.SecretKey)) + userToken = userToken ?? MainWindow.Settings?.Dlass?.UserToken; + if (string.IsNullOrEmpty(userToken)) { - LogHelper.WriteLogToFile($"上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error); + LogHelper.WriteLogToFile("上传失败:未设置用户Token", LogHelper.LogType.Error); return false; } - // 准备上传参数 - var fileName = Path.GetFileNameWithoutExtension(pngFilePath); - var title = fileName; - var fileType = fileExtension == ".icstk" ? "墨迹文件" : "笔记"; - var description = $"自动上传的{fileType} - {DateTime.Now:yyyy-MM-dd HH:mm:ss}"; - var tags = fileExtension == ".icstk" ? "自动上传,墨迹,icstk" : "自动上传,笔记,png"; + apiBaseUrl = apiBaseUrl ?? MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech"; - // 上传文件 + // 创建API客户端并获取白板信息 + using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken)) + { + var authData = new + { + app_id = APP_ID, + app_secret = APP_SECRET, + user_token = userToken + }; + + var authResult = await apiClient.PostAsync("/api/whiteboard/framework/auth-with-token", authData, requireAuth: false); + + if (authResult == null || !authResult.Success || authResult.Whiteboards == null) + { + LogHelper.WriteLogToFile("上传失败:无法获取白板信息", LogHelper.LogType.Error); + return false; + } + + // 查找匹配班级的白板 + whiteboard = authResult.Whiteboards + .FirstOrDefault(w => !string.IsNullOrEmpty(w.ClassName) && w.ClassName == selectedClassName); + + if (whiteboard == null || string.IsNullOrEmpty(whiteboard.BoardId) || string.IsNullOrEmpty(whiteboard.SecretKey)) + { + LogHelper.WriteLogToFile($"上传失败:未找到班级'{selectedClassName}'对应的白板", LogHelper.LogType.Error); + return false; + } + } + } + + // 获取API基础URL和用户Token(如果未提供) + apiBaseUrl = apiBaseUrl ?? MainWindow.Settings?.Dlass?.ApiBaseUrl ?? "https://dlass.tech"; + userToken = userToken ?? MainWindow.Settings?.Dlass?.UserToken; + + // 准备上传参数 + var fileName = Path.GetFileNameWithoutExtension(filePath); + var title = fileName; + var fileType = fileExtension == ".icstk" ? "墨迹文件" : "笔记"; + var description = $"自动上传的{fileType} - {DateTime.Now:yyyy-MM-dd HH:mm:ss}"; + var tags = fileExtension == ".icstk" ? "自动上传,墨迹,icstk" : "自动上传,笔记,png"; + + // 创建API客户端并上传文件 + using (var apiClient = new DlassApiClient(APP_ID, APP_SECRET, apiBaseUrl, userToken)) + { var uploadResult = await apiClient.UploadNoteAsync( "/api/whiteboard/upload_note", - pngFilePath, + filePath, whiteboard.BoardId, whiteboard.SecretKey, title, @@ -203,10 +382,6 @@ namespace Ink_Canvas.Helpers return false; } } - finally - { - apiClient?.Dispose(); - } } catch (Exception ex) { From d2906476c87ed69c90a3e69496c2faecf940da40 Mon Sep 17 00:00:00 2001 From: CJK_mkp <113243675+CJKmkp@users.noreply.github.com> Date: Sun, 2 Nov 2025 10:46:16 +0800 Subject: [PATCH 06/25] =?UTF-8?q?add:Dlass=E8=81=94=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Helpers/DlassNoteUploader.cs | 125 +++++++++++++++++++++--- 1 file changed, 112 insertions(+), 13 deletions(-) diff --git a/Ink Canvas/Helpers/DlassNoteUploader.cs b/Ink Canvas/Helpers/DlassNoteUploader.cs index 0d67e3a2..4265c2bc 100644 --- a/Ink Canvas/Helpers/DlassNoteUploader.cs +++ b/Ink Canvas/Helpers/DlassNoteUploader.cs @@ -18,11 +18,21 @@ namespace Ink_Canvas.Helpers private const string APP_ID = "app_WkjocWqsrVY7T6zQV2CfiA"; private const string APP_SECRET = "o7dx5b5ASGUMcM72PCpmRQYAhSijqaOVHoGyBK0IxbA"; private const int BATCH_SIZE = 10; // 批量上传大小 + private const int MAX_RETRY_COUNT = 3; // 最大重试次数 /// - /// 上传队列(线程安全) + /// 上传队列项 /// - private static readonly ConcurrentQueue _uploadQueue = new ConcurrentQueue(); + private class UploadQueueItem + { + public string FilePath { get; set; } + public int RetryCount { get; set; } + } + + /// + /// 上传队列 + /// + private static readonly ConcurrentQueue _uploadQueue = new ConcurrentQueue(); /// /// 队列处理锁,防止并发处理 @@ -153,9 +163,13 @@ namespace Ink_Canvas.Helpers /// /// 将文件加入上传队列 /// - private static void EnqueueFile(string filePath) + private static void EnqueueFile(string filePath, int retryCount = 0) { - _uploadQueue.Enqueue(filePath); + _uploadQueue.Enqueue(new UploadQueueItem + { + FilePath = filePath, + RetryCount = retryCount + }); // 如果队列达到批量大小,触发批量上传 if (_uploadQueue.Count >= BATCH_SIZE) @@ -177,15 +191,15 @@ namespace Ink_Canvas.Helpers try { - var filesToUpload = new List(); + var filesToUpload = new List(); // 从队列中取出最多BATCH_SIZE个文件 - while (filesToUpload.Count < BATCH_SIZE && _uploadQueue.TryDequeue(out string filePath)) + while (filesToUpload.Count < BATCH_SIZE && _uploadQueue.TryDequeue(out UploadQueueItem item)) { - // 再次检查文件是否存在(可能在队列中时被删除) - if (File.Exists(filePath)) + // 再次检查文件是否存在 + if (File.Exists(item.FilePath)) { - filesToUpload.Add(filePath); + filesToUpload.Add(item); } } @@ -251,11 +265,59 @@ namespace Ink_Canvas.Helpers return; } - // 并发上传所有文件(共享白板信息) - var uploadTasks = filesToUpload.Select(filePath => UploadFileInternalAsync(filePath, sharedWhiteboard, apiBaseUrl, userToken)); + // 并发上传所有文件(共享白板信息),并处理失败重试 + var uploadTasks = filesToUpload.Select(async item => + { + try + { + var success = await UploadFileInternalAsync(item.FilePath, sharedWhiteboard, apiBaseUrl, userToken); + if (!success) + { + // 检查是否是可重试的错误 + if (IsRetryableError(item.FilePath)) + { + // 检查重试次数 + if (item.RetryCount < MAX_RETRY_COUNT) + { + LogHelper.WriteLogToFile($"上传失败,将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event); + EnqueueFile(item.FilePath, item.RetryCount + 1); + } + else + { + LogHelper.WriteLogToFile($"上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error); + } + } + } + return success; + } + catch (Exception ex) + { + // 检查是否是可重试的错误(超时、网络错误等) + var errorMessage = ex.Message.ToLower(); + bool isRetryable = errorMessage.Contains("超时") || + errorMessage.Contains("timeout") || + errorMessage.Contains("网络错误") || + errorMessage.Contains("network"); + + if (isRetryable && IsRetryableError(item.FilePath)) + { + // 检查重试次数 + if (item.RetryCount < MAX_RETRY_COUNT) + { + LogHelper.WriteLogToFile($"上传失败({ex.Message}),将重试 ({item.RetryCount + 1}/{MAX_RETRY_COUNT}): {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Event); + EnqueueFile(item.FilePath, item.RetryCount + 1); + } + else + { + LogHelper.WriteLogToFile($"上传失败,已达到最大重试次数: {Path.GetFileName(item.FilePath)}", LogHelper.LogType.Error); + } + } + return false; + } + }); await Task.WhenAll(uploadTasks); - // 如果队列中还有文件,继续处理 + // 如果队列达到批量大小,继续处理 if (_uploadQueue.Count >= BATCH_SIZE) { _ = ProcessUploadQueueAsync(); @@ -385,10 +447,47 @@ namespace Ink_Canvas.Helpers } catch (Exception ex) { + // 记录错误信息,抛出异常以便调用方判断是否可重试 LogHelper.WriteLogToFile($"上传笔记时出错: {ex.Message}", LogHelper.LogType.Error); - return false; + throw; } } + + /// + /// 判断错误是否可重试(超时、网络错误等) + /// + private static bool IsRetryableError(string filePath) + { + // 检查文件是否存在 + if (!File.Exists(filePath)) + { + return false; // 文件不存在,不可重试 + } + + // 检查文件扩展名 + var fileExtension = Path.GetExtension(filePath).ToLower(); + if (fileExtension != ".png" && fileExtension != ".icstk") + { + return false; // 文件格式错误,不可重试 + } + + // 检查文件大小 + try + { + var fileInfo = new FileInfo(filePath); + if (fileInfo.Length > 10 * 1024 * 1024) + { + return false; // 文件过大,不可重试 + } + } + catch + { + return false; // 无法读取文件信息,不可重试 + } + + // 其他错误(超时、网络错误等)可以重试 + return true; + } } } From 24b2bffe8e42b459f5889b42d64889e6e8306259 Mon Sep 17 00:00:00 2001 From: CJK_mkp <113243675+CJKmkp@users.noreply.github.com> Date: Sun, 2 Nov 2025 11:12:13 +0800 Subject: [PATCH 07/25] =?UTF-8?q?improve:=E8=AE=A1=E6=97=B6=E5=99=A8?= =?UTF-8?q?=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Windows/MinimizedTimerWindow.xaml.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Ink Canvas/Windows/MinimizedTimerWindow.xaml.cs b/Ink Canvas/Windows/MinimizedTimerWindow.xaml.cs index b8004d2b..50731801 100644 --- a/Ink Canvas/Windows/MinimizedTimerWindow.xaml.cs +++ b/Ink Canvas/Windows/MinimizedTimerWindow.xaml.cs @@ -28,6 +28,9 @@ namespace Ink_Canvas this.Left = parent.Left; this.Top = parent.Top; + // 根据分辨率和DPI缩放窗口 + ScaleWindowForResolution(); + // 启动更新定时器 updateTimer = new System.Timers.Timer(100); // 100ms更新一次 updateTimer.Elapsed += UpdateTimer_Elapsed; @@ -39,6 +42,43 @@ namespace Ink_Canvas ApplyTheme(); } + /// + /// 根据屏幕分辨率和 DPI 缩放窗口大小(保持原始尺寸,使用Transform缩放) + /// + private void ScaleWindowForResolution() + { + try + { + // 获取屏幕尺寸(考虑 DPI 缩放) + double screenWidth = SystemParameters.PrimaryScreenWidth; + double screenHeight = SystemParameters.PrimaryScreenHeight; + + // 基准分辨率(1920x1080) + const double baseWidth = 1920.0; + const double baseHeight = 1080.0; + + // 计算缩放比例(使用较小的比例以保持比例) + double scaleX = screenWidth / baseWidth; + double scaleY = screenHeight / baseHeight; + double scale = Math.Min(scaleX, scaleY); + + // 限制最小和最大缩放,避免过小或过大 + scale = Math.Max(0.5, Math.Min(2.0, scale)); + + // 应用缩放变换到整个窗口内容 + var scaleTransform = this.FindName("WindowScaleTransform") as ScaleTransform; + if (scaleTransform != null) + { + scaleTransform.ScaleX = scale; + scaleTransform.ScaleY = scale; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"缩放窗口大小时出错: {ex.Message}"); + } + } + private void UpdateTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { if (parentWindow != null) From e7d89e65b22da0ab1a0871673cf377eba4fd151e Mon Sep 17 00:00:00 2001 From: CJK_mkp <113243675+CJKmkp@users.noreply.github.com> Date: Sun, 2 Nov 2025 11:27:46 +0800 Subject: [PATCH 08/25] improve:AutoUpdate --- Ink Canvas/App.xaml.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/Ink Canvas/App.xaml.cs b/Ink Canvas/App.xaml.cs index 56385a85..51ea0265 100644 --- a/Ink Canvas/App.xaml.cs +++ b/Ink Canvas/App.xaml.cs @@ -706,6 +706,20 @@ namespace Ink_Canvas { LogHelper.WriteLogToFile($"App | 清理更新标记文件失败: {ex.Message}", LogHelper.LogType.Warning); } + + Task.Run(async () => + { + try + { + await Task.Delay(3000); + LogHelper.WriteLogToFile("App | 最终应用启动,删除AutoUpdate文件夹"); + AutoUpdateHelper.DeleteUpdatesFolder(); + } + catch (Exception ex) + { + LogHelper.WriteLogToFile($"App | 删除AutoUpdate文件夹失败: {ex.Message}", LogHelper.LogType.Warning); + } + }); } // 如果不是最终应用启动,才检查更新标记文件 From 74eca093da11eb7b9b07ccf36bc9862ded019f04 Mon Sep 17 00:00:00 2001 From: CJK_mkp <113243675+CJKmkp@users.noreply.github.com> Date: Sun, 2 Nov 2025 11:40:27 +0800 Subject: [PATCH 09/25] =?UTF-8?q?improve:=E6=82=AC=E6=B5=AE=E5=BF=AB?= =?UTF-8?q?=E6=8A=BD=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Windows/QuickDrawFloatingButton.cs | 69 ++++++++++++++++++ .../Windows/QuickDrawFloatingButton.xaml | 71 +++++++++++++++---- 2 files changed, 127 insertions(+), 13 deletions(-) diff --git a/Ink Canvas/Windows/QuickDrawFloatingButton.cs b/Ink Canvas/Windows/QuickDrawFloatingButton.cs index 6c5360bd..61aebc2f 100644 --- a/Ink Canvas/Windows/QuickDrawFloatingButton.cs +++ b/Ink Canvas/Windows/QuickDrawFloatingButton.cs @@ -14,6 +14,10 @@ namespace Ink_Canvas /// public partial class QuickDrawFloatingButton : Window { + private bool isDragging = false; + private Point dragStartPoint; + private Point windowStartPoint; + public QuickDrawFloatingButton() { InitializeComponent(); @@ -62,6 +66,9 @@ namespace Ink_Canvas { try { + // 如果正在拖动,不触发点击事件 + if (isDragging) return; + // 打开快抽窗口 var quickDrawWindow = new QuickDrawWindow(); quickDrawWindow.ShowDialog(); @@ -72,6 +79,68 @@ namespace Ink_Canvas } } + private void DragArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + isDragging = false; + // 记录鼠标在屏幕上的初始位置 + dragStartPoint = this.PointToScreen(e.GetPosition(this)); + // 记录窗口的初始位置 + windowStartPoint = new Point(this.Left, this.Top); + ((UIElement)sender).CaptureMouse(); + e.Handled = true; + } + + private void DragArea_MouseMove(object sender, MouseEventArgs e) + { + if (e.LeftButton == MouseButtonState.Pressed && ((UIElement)sender).IsMouseCaptured) + { + // 获取鼠标在屏幕上的当前位置 + Point currentScreenPoint = this.PointToScreen(e.GetPosition(this)); + Vector diff = currentScreenPoint - dragStartPoint; + + if (!isDragging && (Math.Abs(diff.X) > 3 || Math.Abs(diff.Y) > 3)) + { + isDragging = true; + } + + if (isDragging) + { + // 使用窗口初始位置加上鼠标移动的距离 + double newLeft = windowStartPoint.X + diff.X; + double newTop = windowStartPoint.Y + diff.Y; + + // 限制在屏幕范围内 + var workingArea = SystemParameters.WorkArea; + newLeft = Math.Max(workingArea.Left, Math.Min(newLeft, workingArea.Right - this.Width)); + newTop = Math.Max(workingArea.Top, Math.Min(newTop, workingArea.Bottom - this.Height)); + + this.Left = newLeft; + this.Top = newTop; + } + } + } + + private void DragArea_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (((UIElement)sender).IsMouseCaptured) + { + ((UIElement)sender).ReleaseMouseCapture(); + } + + // 延迟重置拖动状态,避免触发点击事件 + if (isDragging) + { + Dispatcher.BeginInvoke(new Action(() => { isDragging = false; }), + DispatcherPriority.Background); + } + else + { + isDragging = false; + } + + e.Handled = true; + } + diff --git a/Ink Canvas/Windows/QuickDrawFloatingButton.xaml b/Ink Canvas/Windows/QuickDrawFloatingButton.xaml index d743c383..07786911 100644 --- a/Ink Canvas/Windows/QuickDrawFloatingButton.xaml +++ b/Ink Canvas/Windows/QuickDrawFloatingButton.xaml @@ -7,7 +7,7 @@ mc:Ignorable="d" WindowStyle="None" AllowsTransparency="True" Loaded="FloatingButton_Loaded" WindowStartupLocation="Manual" ShowInTaskbar="False" Focusable="False" - Title="快抽悬浮按钮" Height="45" Width="45"> + Title="快抽悬浮按钮" Height="45" Width="65"> @@ -21,23 +21,68 @@ + BorderBrush="{DynamicResource QuickDrawFloatingButtonBorderBrush}"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 01009f9e355194b12a274ff2cd372fe2d9054e65 Mon Sep 17 00:00:00 2001 From: CJK_mkp <113243675+CJKmkp@users.noreply.github.com> Date: Sun, 2 Nov 2025 11:47:52 +0800 Subject: [PATCH 10/25] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/AssemblyInfo.cs | 4 ++-- Ink Canvas/Properties/AssemblyInfo.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Ink Canvas/AssemblyInfo.cs b/Ink Canvas/AssemblyInfo.cs index 93561d41..604af0dd 100644 --- a/Ink Canvas/AssemblyInfo.cs +++ b/Ink Canvas/AssemblyInfo.cs @@ -49,5 +49,5 @@ using System.Windows; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.7.17.0")] -[assembly: AssemblyFileVersion("1.7.17.0")] +[assembly: AssemblyVersion("1.7.17.1")] +[assembly: AssemblyFileVersion("1.7.17.1")] diff --git a/Ink Canvas/Properties/AssemblyInfo.cs b/Ink Canvas/Properties/AssemblyInfo.cs index 93561d41..604af0dd 100644 --- a/Ink Canvas/Properties/AssemblyInfo.cs +++ b/Ink Canvas/Properties/AssemblyInfo.cs @@ -49,5 +49,5 @@ using System.Windows; // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.7.17.0")] -[assembly: AssemblyFileVersion("1.7.17.0")] +[assembly: AssemblyVersion("1.7.17.1")] +[assembly: AssemblyFileVersion("1.7.17.1")] From 92c631d6ce266f5a9ff9a3de5e0d5d82d4ba2e3b Mon Sep 17 00:00:00 2001 From: CJK_mkp <113243675+CJKmkp@users.noreply.github.com> Date: Sun, 2 Nov 2025 12:34:50 +0800 Subject: [PATCH 11/25] =?UTF-8?q?improve:=E6=93=8D=E4=BD=9C=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/MainWindow_cs/MW_Settings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Ink Canvas/MainWindow_cs/MW_Settings.cs b/Ink Canvas/MainWindow_cs/MW_Settings.cs index 97c55672..0c29fcfe 100644 --- a/Ink Canvas/MainWindow_cs/MW_Settings.cs +++ b/Ink Canvas/MainWindow_cs/MW_Settings.cs @@ -2728,8 +2728,8 @@ namespace Ink_Canvas if (ToggleSwitchEnableOvertimeRedText.IsOn && !ToggleSwitchEnableOvertimeCountUp.IsOn) { - ToggleSwitchEnableOvertimeRedText.IsOn = false; - return; + ToggleSwitchEnableOvertimeCountUp.IsOn = true; + Settings.RandSettings.EnableOvertimeCountUp = true; } Settings.RandSettings.EnableOvertimeRedText = ToggleSwitchEnableOvertimeRedText.IsOn; From ce1998b70121d1f4f8a87f8fd8280f8f33cfe865 Mon Sep 17 00:00:00 2001 From: CJK_mkp <113243675+CJKmkp@users.noreply.github.com> Date: Fri, 7 Nov 2025 09:46:21 +0800 Subject: [PATCH 12/25] =?UTF-8?q?improve:Dlass=20=E7=95=8C=E9=9D=A2UI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Windows/DlassSettingsWindow.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Ink Canvas/Windows/DlassSettingsWindow.xaml b/Ink Canvas/Windows/DlassSettingsWindow.xaml index 9591776b..30be8f9a 100644 --- a/Ink Canvas/Windows/DlassSettingsWindow.xaml +++ b/Ink Canvas/Windows/DlassSettingsWindow.xaml @@ -343,7 +343,7 @@ From dfab0d7ddfa462eec50114ac978f92c2a58530d4 Mon Sep 17 00:00:00 2001 From: CJK_mkp <113243675+CJKmkp@users.noreply.github.com> Date: Fri, 7 Nov 2025 10:35:27 +0800 Subject: [PATCH 13/25] =?UTF-8?q?improvve:=E7=82=B9=E5=90=8D=E5=BF=AB?= =?UTF-8?q?=E6=8A=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Windows/QuickDrawWindow.cs | 45 ++++++++++++++++----------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/Ink Canvas/Windows/QuickDrawWindow.cs b/Ink Canvas/Windows/QuickDrawWindow.cs index eb92a816..e8bfec04 100644 --- a/Ink Canvas/Windows/QuickDrawWindow.cs +++ b/Ink Canvas/Windows/QuickDrawWindow.cs @@ -99,7 +99,7 @@ namespace Ink_Canvas { const int animationTimes = 100; // 动画次数 const int sleepTime = 5; // 每次动画间隔(毫秒) - + new System.Threading.Thread(() => { if (nameList.Count > 0) @@ -121,7 +121,7 @@ namespace Ink_Canvas private void StartNameDrawAnimation(int animationTimes, int sleepTime) { List usedNames = new List(); - + for (int i = 0; i < animationTimes; i++) { // 随机选择一个名字进行动画显示,避免立即重复 @@ -130,25 +130,29 @@ namespace Ink_Canvas { randomName = nameList[random.Next(0, nameList.Count)]; } while (usedNames.Count > 0 && usedNames[usedNames.Count - 1] == randomName); - + usedNames.Add(randomName); - + Application.Current.Dispatcher.Invoke(() => { MainResultDisplay.Text = randomName; }); - + System.Threading.Thread.Sleep(sleepTime); } - + // 动画结束,显示最终结果 Application.Current.Dispatcher.Invoke(() => { - // 随机选择一个最终名字 - string finalName = nameList[random.Next(0, nameList.Count)]; + // 使用降重抽选方法选择最终名字 + var selectedNames = NewStyleRollCallWindow.SelectNamesWithML(nameList, 1, random); + string finalName = selectedNames.Count > 0 ? selectedNames[0] : nameList[random.Next(0, nameList.Count)]; MainResultDisplay.Text = finalName; + + // 更新历史记录 + NewStyleRollCallWindow.UpdateRollCallHistory(new List { finalName }); }); - + // 显示结果后,等待一段时间让用户看到结果,然后关闭窗口 new System.Threading.Thread(() => { @@ -166,7 +170,7 @@ namespace Ink_Canvas private void StartNumberDrawAnimation(int animationTimes, int sleepTime) { List usedNumbers = new List(); - + for (int i = 0; i < animationTimes; i++) { // 随机选择一个数字进行动画显示,避免立即重复 @@ -175,25 +179,30 @@ namespace Ink_Canvas { randomNumber = random.Next(1, 61); // 1-60 } while (usedNumbers.Count > 0 && usedNumbers[usedNumbers.Count - 1] == randomNumber); - + usedNumbers.Add(randomNumber); - + Application.Current.Dispatcher.Invoke(() => { MainResultDisplay.Text = randomNumber.ToString(); }); - + System.Threading.Thread.Sleep(sleepTime); } - + // 动画结束,显示最终结果 Application.Current.Dispatcher.Invoke(() => { - // 随机选择一个最终数字 - int finalNumber = random.Next(1, 61); - MainResultDisplay.Text = finalNumber.ToString(); + // 使用降重抽选方法选择最终数字 + var numberList = Enumerable.Range(1, 60).Select(n => n.ToString()).ToList(); + var selectedNumbers = NewStyleRollCallWindow.SelectNamesWithML(numberList, 1, random); + string finalNumber = selectedNumbers.Count > 0 ? selectedNumbers[0] : random.Next(1, 61).ToString(); + MainResultDisplay.Text = finalNumber; + + // 更新历史记录 + NewStyleRollCallWindow.UpdateRollCallHistory(new List { finalNumber }); }); - + // 显示结果后,等待一段时间让用户看到结果,然后关闭窗口 new System.Threading.Thread(() => { From 4b2f29442a2cd3fa9f03c4808300dd3e1066e107 Mon Sep 17 00:00:00 2001 From: CJK_mkp <113243675+CJKmkp@users.noreply.github.com> Date: Fri, 7 Nov 2025 10:36:24 +0800 Subject: [PATCH 14/25] =?UTF-8?q?improve:=E7=82=B9=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Windows/NewStyleRollCallWindow.cs | 89 ++++++++++++-------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/Ink Canvas/Windows/NewStyleRollCallWindow.cs b/Ink Canvas/Windows/NewStyleRollCallWindow.cs index bfe85d88..84dec3a3 100644 --- a/Ink Canvas/Windows/NewStyleRollCallWindow.cs +++ b/Ink Canvas/Windows/NewStyleRollCallWindow.cs @@ -126,9 +126,10 @@ namespace Ink_Canvas private DateTime lastActivityTime = DateTime.Now; // 机器学习相关 - private RollCallHistoryData historyData = new RollCallHistoryData(); - private int maxRecentHistory = 20; - private double avoidanceWeight = 0.8; // 避免重复的权重 + private static RollCallHistoryData historyData = null; + private static readonly object historyLock = new object(); + private static int maxRecentHistory = 20; + private static double avoidanceWeight = 0.8; // 避免重复的权重 private const double FREQUENCY_WEIGHT = 0.2; // 频率平衡的权重 // 单次抽相关 @@ -394,13 +395,13 @@ namespace Ink_Canvas switch (selectedRollCallMode) { case "Random": - return SelectNamesWithML(availableNames, count); + return SelectNamesWithML(availableNames, count, random); case "Sequential": return SelectNamesSequentially(availableNames, count); case "Group": return SelectNamesInGroups(availableNames, count); default: - return SelectNamesWithML(availableNames, count); + return SelectNamesWithML(availableNames, count, random); } } @@ -471,18 +472,25 @@ namespace Ink_Canvas /// /// 可用名单 /// 需要选择的人数 + /// 随机数生成器 /// 选择的人员名单 - private List SelectNamesWithML(List availableNames, int count) + public static List SelectNamesWithML(List availableNames, int count, Random random) { if (availableNames == null || availableNames.Count == 0) return new List(); + // 确保历史数据已初始化 + if (historyData == null) + { + LoadRollCallHistory(); + } + // 检查是否启用机器学习避免重复 bool enableML = MainWindow.Settings?.RandSettings?.EnableMLAvoidance ?? true; if (!enableML) { // 如果禁用机器学习,使用简单随机选择 - return SelectNamesRandomly(availableNames, count); + return SelectNamesRandomly(availableNames, count, random); } var selectedNames = new List(); @@ -490,7 +498,7 @@ namespace Ink_Canvas for (int i = 0; i < count && remainingNames.Count > 0; i++) { - string selectedName = SelectSingleNameWithML(remainingNames, selectedNames); + string selectedName = SelectSingleNameWithML(remainingNames, selectedNames, random); if (!string.IsNullOrEmpty(selectedName)) { selectedNames.Add(selectedName); @@ -504,7 +512,7 @@ namespace Ink_Canvas /// /// 简单随机选择点名人员 /// - private List SelectNamesRandomly(List availableNames, int count) + private static List SelectNamesRandomly(List availableNames, int count, Random random) { if (availableNames == null || availableNames.Count == 0) return new List(); @@ -525,7 +533,7 @@ namespace Ink_Canvas /// /// 使用机器学习算法选择单个人员 /// - private string SelectSingleNameWithML(List availableNames, List alreadySelected) + private static string SelectSingleNameWithML(List availableNames, List alreadySelected, Random random) { if (availableNames.Count == 0) return null; if (availableNames.Count == 1) return availableNames[0]; @@ -554,15 +562,15 @@ namespace Ink_Canvas } // 使用加权随机选择 - return WeightedRandomSelection(nameWeights); + return WeightedRandomSelection(nameWeights, random); } /// /// 计算避免最近重复的权重 /// - private double CalculateRecentAvoidanceWeight(string name) + private static double CalculateRecentAvoidanceWeight(string name) { - if (historyData.History == null || historyData.History.Count == 0) + if (historyData == null || historyData.History == null || historyData.History.Count == 0) return 0.0; // 获取最近记录 @@ -576,9 +584,9 @@ namespace Ink_Canvas /// /// 计算频率平衡权重 /// - private double CalculateFrequencyWeight(string name) + private static double CalculateFrequencyWeight(string name) { - if (historyData.NameFrequency == null || !historyData.NameFrequency.ContainsKey(name)) + if (historyData == null || historyData.NameFrequency == null || !historyData.NameFrequency.ContainsKey(name)) return 0.5; // 如果从未被选中,给予中等权重 int totalSelections = historyData.NameFrequency.Values.Sum(); @@ -594,7 +602,7 @@ namespace Ink_Canvas /// /// 加权随机选择 /// - private string WeightedRandomSelection(Dictionary nameWeights) + private static string WeightedRandomSelection(Dictionary nameWeights, Random random) { if (nameWeights.Count == 0) return null; @@ -619,15 +627,23 @@ namespace Ink_Canvas /// /// 更新点名历史记录 /// - private void UpdateRollCallHistory(List selectedNames) + public static void UpdateRollCallHistory(List selectedNames) { if (selectedNames == null || selectedNames.Count == 0) return; - // 更新历史记录 - if (historyData.History == null) - historyData.History = new List(); + // 确保历史数据已初始化 + if (historyData == null) + { + LoadRollCallHistory(); + } - historyData.History.AddRange(selectedNames); + lock (historyLock) + { + // 更新历史记录 + if (historyData.History == null) + historyData.History = new List(); + + historyData.History.AddRange(selectedNames); // 保持历史记录不超过100条 if (historyData.History.Count > 100) @@ -647,18 +663,20 @@ namespace Ink_Canvas historyData.NameFrequency[name] = 1; } - historyData.LastUpdate = DateTime.Now; + historyData.LastUpdate = DateTime.Now; - // 保存到文件 - SaveRollCallHistory(); + // 保存到文件 + SaveRollCallHistory(); + } } + #endregion #region 数据持久化 /// /// 加载点名历史记录 /// - private void LoadRollCallHistory() + private static void LoadRollCallHistory() { try { @@ -695,7 +713,7 @@ namespace Ink_Canvas /// /// 保存点名历史记录 /// - private void SaveRollCallHistory() + private static void SaveRollCallHistory() { try { @@ -1292,8 +1310,9 @@ namespace Ink_Canvas // 动画结束,显示最终结果 Application.Current.Dispatcher.Invoke(() => { - // 使用60个数字进行抽选 - var selectedNumbers = SelectMultipleNumbers(currentCount); + // 使用降重抽选方法选择数字 + var numberList = Enumerable.Range(1, 60).Select(n => n.ToString()).ToList(); + var selectedNumbers = SelectNamesWithML(numberList, currentCount, random); // 更新历史记录 UpdateRollCallHistory(selectedNumbers); @@ -1384,8 +1403,8 @@ namespace Ink_Canvas // 动画结束,显示最终结果 Application.Current.Dispatcher.Invoke(() => { - // 根据选择的模式进行不同的点名逻辑 - var selectedNames = SelectNamesByMode(nameList, currentCount); + // 使用降重抽选方法 + var selectedNames = SelectNamesWithML(nameList, currentCount, random); // 更新历史记录 UpdateRollCallHistory(selectedNames); @@ -1442,8 +1461,12 @@ namespace Ink_Canvas // 动画结束,显示最终结果 Application.Current.Dispatcher.Invoke(() => { - // 根据选择的数量进行抽选 - var selectedNumbers = SelectMultipleNumbers(currentCount); + // 使用降重抽选方法选择数字 + var numberList = Enumerable.Range(1, 60).Select(n => n.ToString()).ToList(); + var selectedNumbers = SelectNamesWithML(numberList, currentCount, random); + + // 更新历史记录 + UpdateRollCallHistory(selectedNumbers); if (selectedNumbers.Count == 1) { @@ -1482,7 +1505,7 @@ namespace Ink_Canvas } /// - /// 选择多个数字(不重复) + /// 选择多个数字 /// private List SelectMultipleNumbers(int count) { From a8dcbd4af030da2ceb31a86a7d08972887ade3af Mon Sep 17 00:00:00 2001 From: CJK_mkp <113243675+CJKmkp@users.noreply.github.com> Date: Sat, 8 Nov 2025 18:01:56 +0800 Subject: [PATCH 15/25] =?UTF-8?q?add:=E7=82=B9=E5=90=8D=E5=8E=86=E5=8F=B2?= =?UTF-8?q?=E6=9F=A5=E7=9C=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ink Canvas/Windows/NewStyleRollCallWindow.cs | 15 ++ .../Windows/NewStyleRollCallWindow.xaml | 14 +- Ink Canvas/Windows/RollCallHistoryWindow.xaml | 46 +++++ .../Windows/RollCallHistoryWindow.xaml.cs | 184 ++++++++++++++++++ 4 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 Ink Canvas/Windows/RollCallHistoryWindow.xaml create mode 100644 Ink Canvas/Windows/RollCallHistoryWindow.xaml.cs diff --git a/Ink Canvas/Windows/NewStyleRollCallWindow.cs b/Ink Canvas/Windows/NewStyleRollCallWindow.cs index 84dec3a3..b80090d6 100644 --- a/Ink Canvas/Windows/NewStyleRollCallWindow.cs +++ b/Ink Canvas/Windows/NewStyleRollCallWindow.cs @@ -856,6 +856,21 @@ namespace Ink_Canvas } } + private void ViewHistory_Click(object sender, RoutedEventArgs e) + { + try + { + // 打开历史记录查看窗口 + var historyWindow = new RollCallHistoryWindow(); + historyWindow.ShowDialog(); + } + catch (Exception ex) + { + MessageBox.Show($"打开历史记录失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error); + LogHelper.WriteLogToFile($"打开历史记录失败: {ex.Message}", LogHelper.LogType.Error); + } + } + private void LoadNamesFromFile() { try diff --git a/Ink Canvas/Windows/NewStyleRollCallWindow.xaml b/Ink Canvas/Windows/NewStyleRollCallWindow.xaml index 2fd0d5e9..473ac152 100644 --- a/Ink Canvas/Windows/NewStyleRollCallWindow.xaml +++ b/Ink Canvas/Windows/NewStyleRollCallWindow.xaml @@ -418,7 +418,7 @@ Foreground="{DynamicResource NewRollCallWindowButtonForeground}"/> + diff --git a/Ink Canvas/Windows/RollCallHistoryWindow.xaml b/Ink Canvas/Windows/RollCallHistoryWindow.xaml new file mode 100644 index 00000000..b2b92e71 --- /dev/null +++ b/Ink Canvas/Windows/RollCallHistoryWindow.xaml @@ -0,0 +1,46 @@ + + + + + + + + + + +