@moon791017/neo-skills 1.0.30 → 1.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "neo-skills",
3
3
  "description": "A universal capability extension for Gemini CLI",
4
- "version": "0.56.0",
4
+ "version": "0.56.2",
5
5
  "mcpServers": {
6
6
  "neo-skills": {
7
7
  "command": "node",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moon791017/neo-skills",
3
- "version": "1.0.30",
3
+ "version": "1.0.32",
4
4
  "type": "module",
5
5
  "description": "Neo Skills: A Universal Expert Agent Extension",
6
6
  "homepage": "https://neo-blog-iota.vercel.app/",
@@ -1,4 +1,3 @@
1
- # skills/neo-dotnet-tag-helper/SKILL.md
2
1
  ---
3
2
  name: neo-dotnet-tag-helper
4
3
  version: "1.0.0"
@@ -1,6 +1,6 @@
1
1
  # .NET Tag Helper Coding Style & Advanced Patterns
2
2
 
3
- This guide defines advanced coding styles and implementation patterns for ASP.NET Core Tag Helpers, specifically targeting complex UI components, extending built-in behaviors, and frontend asset management mechanisms.
3
+ This guide defines advanced coding styles and implementation patterns for ASP.NET Core Tag Helpers, specifically targeting complex UI components, extending built-in behaviors, and frontend asset management mechanisms. **When developing Tag Helpers, developers MUST refer to the implementation patterns in the Example section (Section 3) to ensure adherence to these standards.**
4
4
 
5
5
  ## 1. Inheriting and Extending Built-in Tag Helpers
6
6
 
@@ -11,7 +11,101 @@ When you need to customize standard form elements (e.g., `<select>`), you should
11
11
  - You must explicitly set `output.TagName`; otherwise, the browser will output tags with custom names (e.g., `<select-ai-agent>`).
12
12
  - Call `await base.ProcessAsync(context, output)` to allow the base class to handle native attribute binding.
13
13
 
14
- ### Example: Extending a Dropdown
14
+ ## 2. Mandatory Custom Tag Helper Properties & Asset Management
15
+
16
+ To ensure consistency, performance, and maintainability across all custom UI components, every custom Tag Helper MUST implement the following properties and logic.
17
+
18
+ ### 2.1 Mandatory Properties
19
+
20
+ 1. **`asp-for` (Model Binding)**:
21
+ Support strong typing by including a `ModelExpression` property. This is crucial for retrieving model metadata (Name, Id, Value, Validation) and generating safe HTML identifiers.
22
+ ```csharp
23
+ [HtmlAttributeName("asp-for")]
24
+ public ModelExpression For { get; set; } = null!;
25
+ ```
26
+
27
+ 2. **CSS Class Handling (UI Class)**:
28
+ Tag Helpers must provide a default CSS class. If the user provides a custom class in the HTML tag:
29
+ - If the user class is identical to the default, maintain the default.
30
+ - If the user class is different, **append** it to the default class (ensure a space separator).
31
+
32
+ ```csharp
33
+ [HtmlAttributeName("class")]
34
+ public string? CssClass { get; set; }
35
+
36
+ protected void ProcessCssClass(TagHelperOutput output, string defaultClass)
37
+ {
38
+ if (string.IsNullOrEmpty(CssClass))
39
+ {
40
+ output.Attributes.SetAttribute("class", defaultClass);
41
+ }
42
+ else if (CssClass.Trim() == defaultClass)
43
+ {
44
+ output.Attributes.SetAttribute("class", defaultClass);
45
+ }
46
+ else
47
+ {
48
+ output.Attributes.SetAttribute("class", $"{defaultClass} {CssClass.Trim()}");
49
+ }
50
+ }
51
+ ```
52
+
53
+ 3. **`AutoLoadAssets` (Asset Management)**:
54
+ Include a boolean property to control automatic resource loading, defaulting to `true`.
55
+ ```csharp
56
+ /// <summary>
57
+ /// Whether to automatically load corresponding JS and CSS. Default is true.
58
+ /// </summary>
59
+ [HtmlAttributeName("auto-load-assets")]
60
+ public bool AutoLoadAssets { get; set; } = true;
61
+ ```
62
+
63
+ ### 2.2 Inheritance and Composition Standards
64
+
65
+ 1. **Independent Tag Helpers**: Any standalone custom Tag Helper representing a single UI control (e.g., a specialized dropdown) MUST inherit from the corresponding built-in ASP.NET Core Tag Helper (e.g., `SelectTagHelper`, `InputTagHelper`, `RadioTagHelper`). This leverages framework-standard model binding and validation logic.
66
+
67
+ 2. **Composite Tag Helpers**: For components aggregating multiple elements (e.g., a search form):
68
+ - **Internal Elements**: MUST be implemented using built-in C# UI Tag Helpers to ensure they benefit from standard ASP.NET Core features.
69
+ - **Outer Container (`div-class`)**: MUST allow users to pass a custom CSS class via a `div-class` attribute.
70
+ - **CSS Consistency**: The handling of `div-class` MUST strictly follow the rules defined in **Section 2.1.2 (CSS Class Handling)**, ensuring the default container class is preserved or appended to, rather than simply overwritten.
71
+
72
+ ### 2.3 Mandatory Asset Loading Pattern
73
+
74
+ When `AutoLoadAssets` is `true`, the Tag Helper must register its resources using the following standard pattern:
75
+
76
+ - **Version Management**: Always use `IFileVersionProvider.AddFileVersionToPath` to ensure browser cache busting.
77
+ - **De-duplication & Injection**: Use `HttpContext.AddStyle` and `HttpContext.AddScript` (see Section 3 for implementation) to ensure assets are only rendered once per page.
78
+
79
+ ```csharp
80
+ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
81
+ {
82
+ // ... UI Generation Logic ...
83
+
84
+ if (AutoLoadAssets)
85
+ {
86
+ var httpContext = ViewContext.HttpContext;
87
+ var requestPathBase = httpContext.Request.PathBase;
88
+
89
+ // 1. Resolve Path with Version
90
+ string cssPath = "/css/components/my-component.css";
91
+ string versionedCss = _fileVersionProvider.AddFileVersionToPath(requestPathBase, cssPath);
92
+
93
+ string jsPath = "/js/components/my-component.js";
94
+ string versionedJs = _fileVersionProvider.AddFileVersionToPath(requestPathBase, jsPath);
95
+
96
+ // 2. Register via Context Extensions (renders in designated Layout blocks)
97
+ httpContext.AddStyle($"<link rel=\"stylesheet\" href=\"{versionedCss}\" />", "MyComponentKey");
98
+ httpContext.AddScript($"<script src=\"{versionedJs}\"></script>", "MyComponentKey");
99
+ }
100
+ }
101
+ ```
102
+
103
+
104
+
105
+ ## 3 Example
106
+
107
+ ### Extending a Dropdown
108
+
15
109
  ```csharp
16
110
  using Microsoft.AspNetCore.Mvc.Rendering;
17
111
  using Microsoft.AspNetCore.Mvc.ViewFeatures;
@@ -176,19 +270,22 @@ namespace [YourNamespace].TagHelpers
176
270
  }
177
271
  }
178
272
  }
273
+ ```
179
274
 
180
- ---
181
-
182
- ## 2. Composite Complex UI Components and Model Binding
275
+ ### Composite Complex UI Components and Model Binding
183
276
 
184
277
  For complex UIs containing multiple elements (like a complete search form), you should use `TagBuilder` to compose the DOM structure. Using `ModelExpression` allows you to accurately bind Tag Helper attributes to ViewModel properties, retrieve the exact field name (`.Name`) for the frontend, and integrate backend permission validation logic.
185
278
 
186
- ### Example: Composite Search Bar
187
- ```csharp
279
+ ``` csharp
280
+
188
281
  /// <summary>
189
282
  /// Composite AI Report Search TagHelper.
190
283
  /// A UI component integrating date range, business unit, submission unit, inspection report menu, and search button.
191
284
  /// </summary>
285
+ /// <summary>
286
+ /// 組合式 AI 報告搜尋 TagHelper
287
+ /// 整合了日期範圍、檢驗單位、送檢單位、檢驗報告選單與查詢按鈕的 UI 組件
288
+ /// </summary>
192
289
  [HtmlTargetElement("combo-ai-report-search")]
193
290
  public class ComboAIReportSearchTagHelper : TagHelper
194
291
  {
@@ -210,66 +307,72 @@ For complex UIs containing multiple elements (like a complete search form), you
210
307
  }
211
308
 
212
309
  /// <summary>
213
- /// Whether to automatically load corresponding JS and CSS, default is true.
214
- /// If set to true, TagHelper automatically inserts related resource tags after the component, ensuring they load only once per request.
310
+ /// 是否自動載入對應的 JS CSS,預設為 true
311
+ /// 若設定為 trueTagHelper 會自動在組件後方插入相關資源標籤,並確保單次請求僅載入一次
215
312
  /// </summary>
216
313
  [HtmlAttributeName("auto-load-assets")]
217
314
  public bool AutoLoadAssets { get; set; } = true;
218
315
 
219
- #region Model Binding Attributes (Supports custom ID and Name)
316
+ #region Model Binding Attributes (支援自定義 ID Name)
220
317
  /// <summary>
221
- /// Model expression bound to start date
318
+ /// 繫結開始日期的模型表達式
222
319
  /// </summary>
223
320
  [HtmlAttributeName("asp-for-sdate")]
224
321
  public ModelExpression ForSDate { get; set; }
225
322
 
226
323
  /// <summary>
227
- /// Model expression bound to end date
324
+ /// 繫結結束日期的模型表達式
228
325
  /// </summary>
229
326
  [HtmlAttributeName("asp-for-edate")]
230
327
  public ModelExpression ForEDate { get; set; }
231
328
 
232
329
  /// <summary>
233
- /// Model expression bound to business unit
330
+ /// 繫結檢驗單位的模型表達式
234
331
  /// </summary>
235
332
  [HtmlAttributeName("asp-for-bu")]
236
333
  public ModelExpression ForBusinessUnit { get; set; }
237
334
 
238
335
  /// <summary>
239
- /// Model expression bound to submission unit (HISNo)
336
+ /// 繫結送檢單位的模型表達式
240
337
  /// </summary>
241
338
  [HtmlAttributeName("asp-for-hisno")]
242
339
  public ModelExpression ForHISNo { get; set; }
243
340
 
244
341
  /// <summary>
245
- /// Model expression bound to inspection report
342
+ /// 繫結檢驗報告的模型表達式
246
343
  /// </summary>
247
344
  [HtmlAttributeName("asp-for-report")]
248
345
  public ModelExpression ForAIReport { get; set; }
249
346
  #endregion
250
347
 
251
- #region Class Attributes (Supports CSS customization)
348
+ #region Class Attributes (支援 CSS 客製化)
252
349
  [HtmlAttributeName("sdate-class")]
253
- public string SDateClass { get; set; } = "form-control";
350
+ public string SDateClass { get; set; } = "sdate-li77"; // form-control 要預設有
254
351
 
255
352
  [HtmlAttributeName("edate-class")]
256
- public string EDateClass { get; set; } = "form-control";
353
+ public string EDateClass { get; set; } = "edate-li77"; // form-control 要預設有
257
354
 
258
355
  [HtmlAttributeName("bu-class")]
259
- public string BusinessUnitClass { get; set; } = "form-control";
356
+ public string BusinessUnitClass { get; set; } = "bu-li77"; // form-control 要預設有
260
357
 
261
358
  [HtmlAttributeName("hisno-class")]
262
- public string HISNoClass { get; set; } = "form-control";
359
+ public string HISNoClass { get; set; } = "hisno-li77"; // form-control 要預設有
263
360
 
264
361
  [HtmlAttributeName("report-class")]
265
- public string AIReportClass { get; set; } = "form-control";
362
+ public string AIReportClass { get; set; } = "ai-report-li77"; // form-control 要預設有
266
363
 
267
364
  [HtmlAttributeName("btn-class")]
268
- public string BtnClass { get; set; } = "btn btn-primary";
365
+ public string BtnClass { get; set; } = "ai-btn-search-li77"; // btn btn-primary 要預設有
366
+
367
+ /// <summary>
368
+ /// 外層容器的 CSS Class。預設為 ai-report-search-container-li77
369
+ /// </summary>
370
+ [HtmlAttributeName("div-class")]
371
+ public string ContainerClass { get; set; } = "ai-report-search-container-li77";
269
372
  #endregion
270
373
 
271
374
  /// <summary>
272
- /// Gets the current ViewContext to access HttpContext and other information
375
+ /// 取得目前的 ViewContext 以存取 HttpContext 等資訊
273
376
  /// </summary>
274
377
  [HtmlAttributeNotBound]
275
378
  [ViewContext]
@@ -281,89 +384,94 @@ For complex UIs containing multiple elements (like a complete search form), you
281
384
  var currentDT = _commonService.GetCurrentDateTime();
282
385
  var isSuperAdmin = currentUser.IsSuperAdminRole;
283
386
 
284
- // Set outer container
387
+ // 設定外層容器
285
388
  output.TagName = "div";
286
389
  output.TagMode = TagMode.StartTagAndEndTag;
287
- output.Attributes.SetAttribute("class", "ai-report-search-container");
288
390
 
289
- // Generate IDs for menus and buttons (using CreateSanitizedId ensures IDs are safe for frontend selectors)
290
- string sDateId = TagBuilder.CreateSanitizedId(ForSDate.Name, "_");
291
- string eDateId = TagBuilder.CreateSanitizedId(ForEDate.Name, "_");
292
- string buId = ForBusinessUnit != null ? TagBuilder.CreateSanitizedId(ForBusinessUnit.Name, "_") : "Search_BusinessUnit";
293
- string hisNoId = TagBuilder.CreateSanitizedId(ForHISNo.Name, "_");
294
- string reportId = TagBuilder.CreateSanitizedId(ForAIReport.Name, "_");
295
- string btnId = "btnSearchAIReport";
391
+ // 處理自訂容器 Class,如果使用者自訂了,則把自訂的加在前面,並且保留預設的以便 JS 定位
392
+ string finalContainerClass = ContainerClass == "ai-report-search-container-li77"
393
+ ? "ai-report-search-container-li77"
394
+ : $"{ContainerClass} ai-report-search-container-li77";
395
+
396
+ output.Attributes.SetAttribute("class", finalContainerClass);
397
+
398
+ // 產生日選單與按鈕的 ID (一律使用 TagBuilder.CreateSanitizedId 產生)
399
+ string sDateId = TagBuilder.CreateSanitizedId(ForSDate?.Name ?? "Search_SDate", "_");
400
+ string eDateId = TagBuilder.CreateSanitizedId(ForEDate?.Name ?? "Search_EDate", "_");
401
+ string buId = TagBuilder.CreateSanitizedId(ForBusinessUnit?.Name ?? "Search_BusinessUnit", "_");
402
+ string hisNoId = TagBuilder.CreateSanitizedId(ForHISNo?.Name ?? "Search_HISNo", "_");
403
+ string reportId = TagBuilder.CreateSanitizedId(ForAIReport?.Name ?? "Search_AIReport", "_");
404
+
405
+ // 處理內層元素的 CSS Class,確保基底 class (form-control / btn btn-primary) 加上自定義或預設的 class
406
+ string finalSDateClass = SDateClass == "sdate-li77" ? "form-control sdate-li77" : $"form-control {SDateClass} sdate-li77";
407
+ string finalEDateClass = EDateClass == "edate-li77" ? "form-control edate-li77" : $"form-control {EDateClass} edate-li77";
408
+ string finalBUClass = BusinessUnitClass == "bu-li77" ? "form-control bu-li77" : $"form-control {BusinessUnitClass} bu-li77";
409
+ string finalHISNoClass = HISNoClass == "hisno-li77" ? "form-control hisno-li77" : $"form-control {HISNoClass} hisno-li77";
410
+ string finalReportClass = AIReportClass == "ai-report-li77" ? "form-control ai-report-li77" : $"form-control {AIReportClass} ai-report-li77";
411
+ string finalBtnClass = BtnClass == "ai-btn-search-li77" ? "btn btn-primary ai-btn-search-li77" : $"btn btn-primary {BtnClass} ai-btn-search-li77";
296
412
 
297
413
  var inputGroup = new TagBuilder("div");
298
414
  inputGroup.AddCssClass("input-group");
299
415
 
300
- // 1. Start date (generate label and input)
301
- inputGroup.InnerHtml.AppendHtml(CreateSpan("Date"));
302
- inputGroup.InnerHtml.AppendHtml(CreateInput(sDateId, "date", SDateClass, DateTime.Now.ToString("yyyy-MM-dd"), "Start Date"));
416
+ // 1. 開始日期
417
+ inputGroup.InnerHtml.AppendHtml(CreateSpan("日期"));
418
+ inputGroup.InnerHtml.AppendHtml(await CreateUIAsync(ForSDate, "date", finalSDateClass, DateTime.Now.ToString("yyyy-MM-dd"), "開始日期", sDateId));
303
419
 
304
- // 2. End date
305
- inputGroup.InnerHtml.AppendHtml(CreateSpan("To"));
306
- inputGroup.InnerHtml.AppendHtml(CreateInput(eDateId, "date", EDateClass, DateTime.Now.ToString("yyyy-MM-dd"), "End Date"));
420
+ // 2. 結束日期
421
+ inputGroup.InnerHtml.AppendHtml(CreateSpan(""));
422
+ inputGroup.InnerHtml.AppendHtml(await CreateUIAsync(ForEDate, "date", finalEDateClass, DateTime.Now.ToString("yyyy-MM-dd"), "結束日期", eDateId));
307
423
 
308
- // 3. Business Unit (Visible to Super Admin only)
424
+ // 3. 檢驗單位 (僅超級管理員可見)
309
425
  if (isSuperAdmin)
310
426
  {
311
- inputGroup.InnerHtml.AppendHtml(CreateSpan("Business Unit"));
312
- inputGroup.InnerHtml.AppendHtml(CreateSelect(buId, BusinessUnitClass));
427
+ inputGroup.InnerHtml.AppendHtml(CreateSpan("檢驗單位"));
428
+ inputGroup.InnerHtml.AppendHtml(await CreateUIAsync(ForBusinessUnit, null, finalBUClass, null, null, buId));
313
429
  }
314
430
 
315
- // 4. Submission Unit (Automatically filtered or initialized based on permissions)
316
- inputGroup.InnerHtml.AppendHtml(CreateSpan("Submission Unit"));
317
- var hisNoSelect = CreateSelect(hisNoId, HISNoClass);
431
+ // 4. 送檢單位 (依權限自動過濾或初始化)
432
+ inputGroup.InnerHtml.AppendHtml(CreateSpan("送檢單位"));
433
+
434
+ List<SelectListItem> hisNoOptions = new List<SelectListItem>();
318
435
  if (!isSuperAdmin)
319
436
  {
320
- // Non-Super Admin: Pre-fetch authorized organization list and populate dropdown
437
+ // 非超級管理員:預先從資料庫抓取授權的組織清單
438
+ hisNoOptions.Add(new SelectListItem { Text = "", Value = "" });
321
439
  var options = await GetAllowListOptionsAsync(currentUser.UserID, currentDT);
322
- hisNoSelect.InnerHtml.AppendHtml("<option></option>");
323
- foreach (var opt in options)
324
- {
325
- var optionTag = new TagBuilder("option");
326
- optionTag.Attributes.Add("value", opt.Value);
327
- optionTag.InnerHtml.Append(opt.Text);
328
- hisNoSelect.InnerHtml.AppendHtml(optionTag);
329
- }
440
+ hisNoOptions.AddRange(options);
330
441
  }
331
- inputGroup.InnerHtml.AppendHtml(hisNoSelect);
442
+ inputGroup.InnerHtml.AppendHtml(await CreateUIAsync(ForHISNo, null, finalHISNoClass, null, null, hisNoId, hisNoOptions));
332
443
 
333
- // 5. Inspection Report (Default empty, populated asynchronously by JS via API)
334
- inputGroup.InnerHtml.AppendHtml(CreateSpan("Inspection Report"));
335
- var reportSelect = CreateSelect(reportId, AIReportClass);
336
- reportSelect.InnerHtml.AppendHtml("<option></option>");
337
- inputGroup.InnerHtml.AppendHtml(reportSelect);
444
+ // 5. 檢驗報告 (預設為空,由 JS 透過 API 非同步填充)
445
+ inputGroup.InnerHtml.AppendHtml(CreateSpan("檢驗報告"));
446
+ var reportOptions = new List<SelectListItem> { new SelectListItem { Text = "", Value = "" } };
447
+ inputGroup.InnerHtml.AppendHtml(await CreateUIAsync(ForAIReport, null, finalReportClass, null, null, reportId, reportOptions));
338
448
 
339
- // 6. Search Button
449
+ // 6. 查詢按鈕
340
450
  var btn = new TagBuilder("button");
341
451
  btn.Attributes.Add("type", "button");
342
- btn.Attributes.Add("id", btnId);
343
- btn.AddCssClass(BtnClass);
344
- btn.InnerHtml.AppendHtml("<i class='fa fa-search'></i> Search");
452
+ btn.AddCssClass(finalBtnClass);
453
+ btn.InnerHtml.AppendHtml("<i class='fa fa-search'></i> 查詢");
345
454
  inputGroup.InnerHtml.AppendHtml(btn);
346
455
 
347
456
  output.Content.SetHtmlContent(inputGroup);
348
457
 
349
- // Auto-load assets (integrates with project's existing asset collector pattern)
458
+ // 自動載入資產
350
459
  if (AutoLoadAssets)
351
460
  {
352
461
  var httpContext = ViewContext.HttpContext;
353
462
 
354
- // 1. Load CSS (Pushed to Layout's RenderStyles for processing)
355
- string cssPath = "[your-css-path]";
463
+ string cssPath = "/css/AIAnalysis/ComboAIReportSearch.css";
356
464
  string vCssPath = _fileVersionProvider.AddFileVersionToPath(httpContext.Request.PathBase, cssPath);
357
465
  httpContext.AddStyle($"<link rel=\"stylesheet\" href=\"{vCssPath}\" />", "ComboAIReportSearch");
358
466
 
359
- // 2. Load JS (Pushed to Layout's RenderScripts for processing)
360
- string jsPath = "[your-js-path]";
467
+ string jsPath = "/js/AIAnalysis/ComboAIReportSearch.js";
361
468
  string vJsPath = _fileVersionProvider.AddFileVersionToPath(httpContext.Request.PathBase, jsPath);
362
469
  httpContext.AddScript($"<script src=\"{vJsPath}\"></script>", "ComboAIReportSearch");
363
470
  }
364
471
  }
365
472
 
366
- #region Helper Methods (Used to generate TagBuilder elements)
473
+ #region Helper Methods
474
+
367
475
  private TagBuilder CreateSpan(string text)
368
476
  {
369
477
  var span = new TagBuilder("span");
@@ -372,30 +480,105 @@ For complex UIs containing multiple elements (like a complete search form), you
372
480
  return span;
373
481
  }
374
482
 
375
- private TagBuilder CreateInput(string id, string type, string className, string value, string title)
483
+ /// <summary>
484
+ /// 通用 UI 產生方法,根據是否有 ModelExpression 選擇產生方式
485
+ /// </summary>
486
+ private async Task<IHtmlContent> CreateUIAsync(ModelExpression @for, string type, string className, string value, string title, string sanitizedId, IEnumerable<SelectListItem> items = null)
487
+ {
488
+ // 如果有 ModelExpression,使用原生 TagHelper 物件
489
+ if (@for != null)
490
+ {
491
+ if (string.IsNullOrEmpty(type)) // Select
492
+ {
493
+ return await CreateSelectTagHelperAsync(@for, className, sanitizedId, items);
494
+ }
495
+ else // Input
496
+ {
497
+ return await CreateInputTagHelperAsync(@for, type, className, value, title, sanitizedId);
498
+ }
499
+ }
500
+
501
+ // 如果沒有 ModelExpression,使用 IHtmlGenerator 直接產生 (模擬 TagHelper 行為)
502
+ if (string.IsNullOrEmpty(type)) // Select
503
+ {
504
+ var select = _generator.GenerateSelect(
505
+ ViewContext,
506
+ null,
507
+ sanitizedId,
508
+ sanitizedId,
509
+ items ?? new List<SelectListItem>(),
510
+ false,
511
+ new { @class = className, id = sanitizedId });
512
+ return select;
513
+ }
514
+ else // Input
515
+ {
516
+ var input = _generator.GenerateTextBox(
517
+ ViewContext,
518
+ null,
519
+ sanitizedId,
520
+ value,
521
+ null,
522
+ new { @class = className, id = sanitizedId, type = type, title = title });
523
+ return input;
524
+ }
525
+ }
526
+
527
+ /// <summary>
528
+ /// 使用 InputTagHelper 產生輸入框
529
+ /// </summary>
530
+ private async Task<IHtmlContent> CreateInputTagHelperAsync(ModelExpression @for, string type, string className, string value, string title, string sanitizedId)
376
531
  {
377
- var input = new TagBuilder("input");
378
- input.Attributes.Add("type", type);
379
- input.Attributes.Add("id", id);
380
- input.Attributes.Add("name", id);
381
- input.Attributes.Add("value", value);
382
- input.Attributes.Add("title", title);
383
- input.AddCssClass(className);
384
- return input;
532
+ var inputTagHelper = new InputTagHelper(_generator)
533
+ {
534
+ For = @for,
535
+ InputTypeName = type,
536
+ ViewContext = this.ViewContext
537
+ };
538
+
539
+ var output = new TagHelperOutput(
540
+ "input",
541
+ new TagHelperAttributeList(),
542
+ (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent())
543
+ );
544
+
545
+ output.Attributes.SetAttribute("id", sanitizedId);
546
+ if (!string.IsNullOrEmpty(className)) output.Attributes.SetAttribute("class", className);
547
+ if (!string.IsNullOrEmpty(value)) output.Attributes.SetAttribute("value", value);
548
+ if (!string.IsNullOrEmpty(title)) output.Attributes.SetAttribute("title", title);
549
+
550
+ await inputTagHelper.ProcessAsync(new TagHelperContext(new TagHelperAttributeList(), new Dictionary<object, object>(), Guid.NewGuid().ToString()), output);
551
+ return output;
385
552
  }
386
553
 
387
- private TagBuilder CreateSelect(string id, string className)
554
+ /// <summary>
555
+ /// 使用 SelectTagHelper 產生下拉選單
556
+ /// </summary>
557
+ private async Task<IHtmlContent> CreateSelectTagHelperAsync(ModelExpression @for, string className, string sanitizedId, IEnumerable<SelectListItem> items = null)
388
558
  {
389
- var select = new TagBuilder("select");
390
- select.Attributes.Add("id", id);
391
- select.Attributes.Add("name", id);
392
- select.AddCssClass(className);
393
- return select;
559
+ var selectTagHelper = new SelectTagHelper(_generator)
560
+ {
561
+ For = @for,
562
+ Items = items ?? new List<SelectListItem>(),
563
+ ViewContext = this.ViewContext
564
+ };
565
+
566
+ var output = new TagHelperOutput(
567
+ "select",
568
+ new TagHelperAttributeList(),
569
+ (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent())
570
+ );
571
+
572
+ output.Attributes.SetAttribute("id", sanitizedId);
573
+ if (!string.IsNullOrEmpty(className)) output.Attributes.SetAttribute("class", className);
574
+
575
+ await selectTagHelper.ProcessAsync(new TagHelperContext(new TagHelperAttributeList(), new Dictionary<object, object>(), Guid.NewGuid().ToString()), output);
576
+ return output;
394
577
  }
395
578
  #endregion
396
579
 
397
580
  /// <summary>
398
- /// Gets the list of all authorized submission organizations for the current user
581
+ /// 獲取當前使用者被授權的所有送檢機構列表
399
582
  /// </summary>
400
583
  private async Task<List<SelectListItem>> GetAllowListOptionsAsync(string userId, DateTime currentDT)
401
584
  {
@@ -403,18 +586,18 @@ For complex UIs containing multiple elements (like a complete search form), you
403
586
  return authOrgs.Select(x => new SelectListItem
404
587
  {
405
588
  Value = x.HisNo,
406
- Text = $"[{x.HisNo}] {x.OrgName}"
589
+ Text = $"{x.HisNo} {x.OrgName}"
407
590
  }).ToList();
408
591
  }
409
592
  }
410
593
 
411
- ---
594
+ ```
595
+
412
596
 
413
- ## 3. Automatic Frontend Asset Loading and Deduplication (Asset Management)
597
+ ### Automatic Frontend Asset Loading and Deduplication (Asset Management)
414
598
 
415
599
  When a Tag Helper depends on specific JavaScript or CSS, developers must ensure that these resources are not loaded multiple times if the tag is used repeatedly on the same page. It is recommended to implement an asset collector pattern by extending `HttpContext.Items`.
416
600
 
417
- ### Example: Asset Deduplication Extension Methods
418
601
 
419
602
  ```csharp
420
603
  public static partial class HtmlExtension
@@ -492,83 +675,4 @@ public static partial class HtmlExtension
492
675
 
493
676
  ---
494
677
 
495
- ## 4. Mandatory Custom Tag Helper Properties & Asset Management
496
-
497
- To ensure consistency, performance, and maintainability across all custom UI components, every custom Tag Helper MUST implement the following properties and logic.
498
-
499
- ### 4.1 Mandatory Properties
500
-
501
- 1. **`asp-for` (Model Binding)**:
502
- Support strong typing by including a `ModelExpression` property. This is crucial for retrieving model metadata (Name, Id, Value, Validation) and generating safe HTML identifiers.
503
- ```csharp
504
- [HtmlAttributeName("asp-for")]
505
- public ModelExpression For { get; set; } = null!;
506
- ```
507
-
508
- 2. **CSS Class Handling (UI Class)**:
509
- Tag Helpers must provide a default CSS class. If the user provides a custom class in the HTML tag:
510
- - If the user class is identical to the default, maintain the default.
511
- - If the user class is different, **append** it to the default class (ensure a space separator).
512
-
513
- ```csharp
514
- [HtmlAttributeName("class")]
515
- public string? CssClass { get; set; }
516
678
 
517
- protected void ProcessCssClass(TagHelperOutput output, string defaultClass)
518
- {
519
- if (string.IsNullOrEmpty(CssClass))
520
- {
521
- output.Attributes.SetAttribute("class", defaultClass);
522
- }
523
- else if (CssClass.Trim() == defaultClass)
524
- {
525
- output.Attributes.SetAttribute("class", defaultClass);
526
- }
527
- else
528
- {
529
- output.Attributes.SetAttribute("class", $"{defaultClass} {CssClass.Trim()}");
530
- }
531
- }
532
- ```
533
-
534
- 3. **`AutoLoadAssets` (Asset Management)**:
535
- Include a boolean property to control automatic resource loading, defaulting to `true`.
536
- ```csharp
537
- /// <summary>
538
- /// Whether to automatically load corresponding JS and CSS. Default is true.
539
- /// </summary>
540
- [HtmlAttributeName("auto-load-assets")]
541
- public bool AutoLoadAssets { get; set; } = true;
542
- ```
543
-
544
- ### 4.2 Mandatory Asset Loading Pattern
545
-
546
- When `AutoLoadAssets` is `true`, the Tag Helper must register its resources using the following standard pattern:
547
-
548
- - **Version Management**: Always use `IFileVersionProvider.AddFileVersionToPath` to ensure browser cache busting.
549
- - **De-duplication & Injection**: Use `HttpContext.AddStyle` and `HttpContext.AddScript` (see Section 3 for implementation) to ensure assets are only rendered once per page.
550
-
551
- ```csharp
552
- public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
553
- {
554
- // ... UI Generation Logic ...
555
-
556
- if (AutoLoadAssets)
557
- {
558
- var httpContext = ViewContext.HttpContext;
559
- var requestPathBase = httpContext.Request.PathBase;
560
-
561
- // 1. Resolve Path with Version
562
- string cssPath = "/css/components/my-component.css";
563
- string versionedCss = _fileVersionProvider.AddFileVersionToPath(requestPathBase, cssPath);
564
-
565
- string jsPath = "/js/components/my-component.js";
566
- string versionedJs = _fileVersionProvider.AddFileVersionToPath(requestPathBase, jsPath);
567
-
568
- // 2. Register via Context Extensions (renders in designated Layout blocks)
569
- httpContext.AddStyle($"<link rel=\"stylesheet\" href=\"{versionedCss}\" />", "MyComponentKey");
570
- httpContext.AddScript($"<script src=\"{versionedJs}\"></script>", "MyComponentKey");
571
- }
572
- }
573
- ```
574
- ```