Skip to content

Commit b96e728

Browse files
committed
modify customer set search filter option
1 parent 2e8d0d7 commit b96e728

31 files changed

Lines changed: 2029 additions & 124 deletions

Tender.slnx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,7 @@
1111
<Project Path="tests/Tender.Core.Tests/Tender.Core.Tests.csproj" />
1212
<Project Path="tests/Tender.Crawler.Tests/Tender.Crawler.Tests.csproj" />
1313
<Project Path="tests/Tender.Storage.Tests/Tender.Storage.Tests.csproj" />
14+
<Project Path="tests/Tender.Desktop.Tests/Tender.Desktop.Tests.csproj" />
15+
<Project Path="tests/Tender.Desktop.UiTests/Tender.Desktop.UiTests.csproj" />
1416
</Folder>
1517
</Solution>

docs/internal-report.html

Lines changed: 281 additions & 11 deletions
Large diffs are not rendered by default.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace Tender.Core.Models;
2+
3+
/// <summary>
4+
/// 關鍵字群組強調色預設色盤(暖色系,依出現順序循環)。
5+
/// 同時用於:(1) keywords.json 缺 Color 時 fallback、(2) 管理 UI 的 preset palette、
6+
/// (3) 新增群組時指派下一個未用色。
7+
/// </summary>
8+
public static class KeywordGroupColors
9+
{
10+
public static readonly IReadOnlyList<string> Palette = new[]
11+
{
12+
"#8B6F47", // 暖棕
13+
"#9C5A8C", // 紫
14+
"#A0524D", // 磚紅
15+
"#7A8B5C", // 鼠尾草綠
16+
"#C4823C", // 橘
17+
"#5B7C8C", // 灰藍
18+
"#8B5A3C", // 深棕
19+
"#6B8E6B", // 葉綠
20+
"#A5664E", // 鏽紅
21+
};
22+
23+
/// <summary>依索引取色(超出範圍會循環)。</summary>
24+
public static string GetByIndex(int index) => Palette[((index % Palette.Count) + Palette.Count) % Palette.Count];
25+
}

src/Tender.Core/Models/KeywordSet.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ public sealed record KeywordGroup
1414
[JsonPropertyName("name")]
1515
public required string Name { get; init; }
1616

17+
/// <summary>群組強調色(hex string,含 #)。空字串代表沿用預設色盤(由 Repository 載入時 fallback 補上)。</summary>
18+
[JsonPropertyName("color")]
19+
public string Color { get; init; } = string.Empty;
20+
1721
[JsonPropertyName("items")]
1822
public required IReadOnlyList<KeywordItem> Items { get; init; }
1923
}

src/Tender.Desktop/App.xaml.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ public partial class App : Application
1818
{
1919
private IHost? _host;
2020

21+
/// <summary>UI 測試會設這個 env var,跳過 scheduled task install / missed run / update check 等副作用。</summary>
22+
internal static bool IsTestMode =>
23+
Environment.GetEnvironmentVariable("TENDER_TEST_MODE") == "1";
24+
2125
protected override async void OnStartup(StartupEventArgs e)
2226
{
2327
base.OnStartup(e);
@@ -31,6 +35,8 @@ protected override async void OnStartup(StartupEventArgs e)
3135
var shell = _host.Services.GetRequiredService<ShellWindow>();
3236
shell.Show();
3337

38+
if (IsTestMode) return;
39+
3440
// 確保每日排程任務以使用者層級存在(idempotent,schtasks /F)。
3541
// 失敗(例:找不到 crawler exe、或公司 group policy 禁用 schtasks)不影響主程式。
3642
_ = Task.Run(async () =>

src/Tender.Desktop/GlobalUsings.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,12 @@
88
global using ColorConverter = System.Windows.Media.ColorConverter;
99
global using UserControl = System.Windows.Controls.UserControl;
1010
global using SaveFileDialog = Microsoft.Win32.SaveFileDialog;
11+
global using ListBox = System.Windows.Controls.ListBox;
12+
global using ListBoxItem = System.Windows.Controls.ListBoxItem;
13+
global using Point = System.Windows.Point;
14+
global using MouseEventArgs = System.Windows.Input.MouseEventArgs;
15+
global using MouseButtonEventArgs = System.Windows.Input.MouseButtonEventArgs;
16+
global using DragEventArgs = System.Windows.DragEventArgs;
17+
global using DragDropEffects = System.Windows.DragDropEffects;
18+
global using DragDrop = System.Windows.DragDrop;
19+
global using Button = System.Windows.Controls.Button;
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System.Windows;
2+
using System.Windows.Controls;
3+
using System.Windows.Input;
4+
using System.Windows.Media;
5+
6+
namespace Tender.Desktop.Helpers;
7+
8+
/// <summary>
9+
/// 為 ListBox 加上 drag-and-drop 重新排序能力。
10+
/// 用法:<c>ListBoxReorderHelper.Attach(myListBox, (from, to) =&gt; vm.Move(from, to));</c>
11+
/// 真正的集合操作交給 callback;helper 不假設 ItemsSource 是哪種集合,只算 index。
12+
/// </summary>
13+
public static class ListBoxReorderHelper
14+
{
15+
private const double DragThreshold = 6.0;
16+
17+
private sealed class State
18+
{
19+
public ListBox ListBox = null!;
20+
public Action<int, int> OnReorder = null!;
21+
public Point StartPoint;
22+
public int DragStartIndex = -1;
23+
}
24+
25+
public static void Attach(ListBox listBox, Action<int, int> onReorder)
26+
{
27+
var state = new State { ListBox = listBox, OnReorder = onReorder };
28+
listBox.AllowDrop = true;
29+
listBox.PreviewMouseLeftButtonDown += (s, e) => OnMouseDown(state, e);
30+
listBox.PreviewMouseMove += (s, e) => OnMouseMove(state, e);
31+
listBox.Drop += (s, e) => OnDrop(state, e);
32+
listBox.DragOver += (s, e) =>
33+
{
34+
e.Effects = e.Data.GetDataPresent(typeof(int)) ? DragDropEffects.Move : DragDropEffects.None;
35+
e.Handled = true;
36+
};
37+
}
38+
39+
private static void OnMouseDown(State state, MouseButtonEventArgs e)
40+
{
41+
// 點擊時記錄起始 index;交給 ListBox 本身先處理 selection,等 MouseMove 才啟動 drag
42+
var item = FindAncestor<ListBoxItem>(e.OriginalSource as DependencyObject);
43+
if (item is null) return;
44+
state.StartPoint = e.GetPosition(state.ListBox);
45+
state.DragStartIndex = state.ListBox.ItemContainerGenerator.IndexFromContainer(item);
46+
}
47+
48+
private static void OnMouseMove(State state, MouseEventArgs e)
49+
{
50+
if (e.LeftButton != MouseButtonState.Pressed || state.DragStartIndex < 0) return;
51+
var pos = e.GetPosition(state.ListBox);
52+
if (Math.Abs(pos.X - state.StartPoint.X) < DragThreshold &&
53+
Math.Abs(pos.Y - state.StartPoint.Y) < DragThreshold) return;
54+
55+
// 啟動 drag-drop;payload 放 source index
56+
try
57+
{
58+
DragDrop.DoDragDrop(state.ListBox, state.DragStartIndex, DragDropEffects.Move);
59+
}
60+
finally
61+
{
62+
state.DragStartIndex = -1;
63+
}
64+
}
65+
66+
private static void OnDrop(State state, DragEventArgs e)
67+
{
68+
if (!e.Data.GetDataPresent(typeof(int))) return;
69+
var from = (int)e.Data.GetData(typeof(int));
70+
71+
// 找到 drop 目標的 index:游標下方的 ListBoxItem;若 drop 到空白處則放最末
72+
var targetItem = FindAncestor<ListBoxItem>(e.OriginalSource as DependencyObject);
73+
int to;
74+
if (targetItem is not null)
75+
{
76+
to = state.ListBox.ItemContainerGenerator.IndexFromContainer(targetItem);
77+
}
78+
else
79+
{
80+
to = state.ListBox.Items.Count - 1;
81+
}
82+
if (to < 0 || from == to) return;
83+
state.OnReorder(from, to);
84+
e.Handled = true;
85+
}
86+
87+
private static T? FindAncestor<T>(DependencyObject? d) where T : DependencyObject
88+
{
89+
while (d != null)
90+
{
91+
if (d is T t) return t;
92+
d = VisualTreeHelper.GetParent(d);
93+
}
94+
return null;
95+
}
96+
}

src/Tender.Desktop/ViewModels/DailyQueryViewModel.cs

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,6 @@ public partial class DailyQueryViewModel : ObservableObject
2424
private readonly IUserMarksRepository _userMarksRepo;
2525
private readonly ISavedSearchesRepository _savedSearchesRepo;
2626

27-
/// <summary>群組顯示色(暖色系,依出現順序循環)。</summary>
28-
private static readonly string[] GroupAccentColors =
29-
{
30-
"#8B6F47", "#9C5A8C", "#A0524D", "#7A8B5C", "#C4823C",
31-
"#5B7C8C", "#8B5A3C", "#6B8E6B", "#A5664E",
32-
};
33-
3427
/// <summary>記憶體中的 user-marks 表(sourcePk → UserMark),由 LoadAsync 填入。</summary>
3528
private Dictionary<string, UserMark> _userMarks = new();
3629

@@ -253,17 +246,19 @@ private async Task LoadAsync(CancellationToken ct)
253246
if (KeywordGroups.Count == 0)
254247
{
255248
var keywordSet = await _keywordsRepo.LoadAsync(ct);
256-
int colorIdx = 0;
249+
int fallbackIdx = 0;
257250
foreach (var group in keywordSet.Groups)
258251
{
259252
var keywords = group.Items
260253
.Where(k => k.Enabled)
261254
.Select(k => k.Keyword);
262-
var color = GroupAccentColors[colorIdx % GroupAccentColors.Length];
255+
var color = string.IsNullOrWhiteSpace(group.Color)
256+
? KeywordGroupColors.GetByIndex(fallbackIdx)
257+
: group.Color;
263258
var groupVm = new KeywordGroupViewModel(group.Name, color, keywords);
264259
groupVm.AnyButtonToggled += ApplyFilter;
265260
KeywordGroups.Add(groupVm);
266-
colorIdx++;
261+
fallbackIdx++;
267262
}
268263
}
269264

0 commit comments

Comments
 (0)