Răsfoiți Sursa

fix(training): show toolbar icons and render Chinese italics

彭峰 7 ore în urmă
părinte
comite
947c4bb868

BIN
docs/qa/images/special-training-icons-italic-2026-10-22.jpg


+ 12 - 0
docs/qa/special-training-editor-2026-10-22.md

@@ -47,3 +47,15 @@ git diff --check
 ![正文编辑与保存回显](images/special-training-editor-2026-10-22.jpg)
 
 ![375 像素窄屏](images/special-training-editor-narrow-2026-10-22.jpg)
+
+## 17:10 工具图标与中文斜体修复
+
+- 12 个工具按钮均增加与后台一致的 Lucide 小图标,同时保留中文名称与键盘操作;图标不重复朗读按钮名称。
+- 根因已在实际页面确认:正文包含 `i` 标签且 `font-style: italic`,但继承全局 `font-synthesis: none`,没有原生斜体字形的中文无法显示倾斜。
+- 在正文编辑区、后台预览及学生端文章正文局部设置 `font-synthesis: style`,允许模拟斜体,不修改整个应用的字体规则。
+- 回归测试先复现失败(`font-synthesis-style` 为 `none`),修复后同一测试通过,覆盖中文斜体开启、真实计算样式及再次点击取消。相关测试合计 34 项通过。
+- 浏览器读取已保存草稿与服务端预览,中文 `张不`、`同比` 的计算样式均为 `italic`,字体模拟为 `style`,截图中可见倾斜。
+- 375 像素窄屏下,工具栏 clientWidth/scrollWidth 均为 325,12 个按钮均有 SVG 图标。恢复默认视口后保留编辑页面。
+- `npm run build`(学生端、管理端)及 `git diff --check` 通过。本次没有公网部署;学生端文章样式已构建,未将测试草稿发布到学生端进行实机验收。
+
+![工具图标与中文斜体](images/special-training-icons-italic-2026-10-22.jpg)

+ 1 - 1
projects/xiaoshu-admin/src/app/pages/operations-special-training.component.scss

@@ -49,7 +49,7 @@
 .preview-cover { width: 100%; aspect-ratio: 4 / 3; border-radius: 14px; object-fit: cover; object-position: center; }
 .preview-synopsis { padding: 14px; border-left: 4px solid #2b8369; color: #566963; background: #f1f7f4; }
 .preview-video { width: 100%; max-height: 420px; border-radius: 14px; background: #111; }
-.preview-content { overflow-wrap: anywhere; line-height: 1.75; }
+.preview-content { overflow-wrap: anywhere; line-height: 1.75; font-synthesis: style; }
 :host ::ng-deep .preview-content :is(img, video) { max-width: 100%; }
 
 @media (max-width: 1100px) {

+ 2 - 2
projects/xiaoshu-admin/src/app/shared/training-body-editor.component.html

@@ -1,9 +1,9 @@
 <div class="editor" [attr.aria-busy]="disabled">
   <div class="tools" role="group" aria-label="正文编辑工具">
-    <button type="button" class="add-images" [disabled]="disabled" (mousedown)="$event.preventDefault()" (click)="chooseImages(images)">添加多张图片</button>
+    <button type="button" class="add-images" [disabled]="disabled" (mousedown)="$event.preventDefault()" (click)="chooseImages(images)"><lucide-icon [img]="imagesIcon" [size]="16" aria-hidden="true"/>添加多张图片</button>
     <input #images type="file" hidden multiple accept="image/jpeg,image/png,image/webp,image/gif" [disabled]="disabled" (change)="filesChanged($event)"/>
     @for (tool of tools; track tool.label) {
-      <button type="button" [disabled]="disabled" (mousedown)="$event.preventDefault()" (click)="format(tool.command, tool.value)">{{tool.label}}</button>
+      <button type="button" [disabled]="disabled" (mousedown)="$event.preventDefault()" (click)="format(tool.command, tool.value)"><lucide-icon [img]="tool.icon" [size]="16" aria-hidden="true"/>{{tool.label}}</button>
     }
   </div>
   <div #body class="body" role="textbox" aria-label="图文正文" aria-multiline="true" aria-describedby="training-body-help" tabindex="0"

+ 4 - 1
projects/xiaoshu-admin/src/app/shared/training-body-editor.component.scss

@@ -2,7 +2,8 @@
 .editor { overflow: hidden; border: 1px solid var(--line, #d8e4df); border-radius: 12px; background: #fff; }
 .editor:focus-within { border-color: #176c55; box-shadow: 0 0 0 2px #176c5520; }
 .tools { display: flex; flex-wrap: wrap; gap: 4px; padding: 8px; border-bottom: 1px solid #d8e4df; background: #f1f7f4; }
-button { min-height: 40px; padding: 8px 10px; border: 1px solid transparent; border-radius: 6px; color: #314a41; background: transparent; font: inherit; font-size: 13px; cursor: pointer; }
+button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 40px; padding: 8px 10px; border: 1px solid transparent; border-radius: 6px; color: #314a41; background: transparent; font: inherit; font-size: 13px; white-space: nowrap; cursor: pointer; }
+lucide-icon { display: inline-flex; flex-shrink: 0; }
 button:hover:not(:disabled) { background: #e0ece6; }
 button:focus-visible { outline: 2px solid #176c55; outline-offset: -2px; }
 button:disabled { opacity: .5; cursor: wait; }
@@ -10,6 +11,8 @@ button:disabled { opacity: .5; cursor: wait; }
 .add-images:hover:not(:disabled) { background: #125640; }
 .body { min-height: 320px; max-height: 65vh; overflow-y: auto; padding: 16px; outline: none; color: #263a32; font-size: 16px; font-weight: 400; line-height: 1.8; overflow-wrap: anywhere; }
 .body:empty::before { content: attr(data-placeholder); color: #687b72; pointer-events: none; }
+// Chinese system fonts commonly have no italic face. Override the global synthesis reset only in rich text.
+.body { font-synthesis: style; }
 :host ::ng-deep .body img { display: inline-block; max-width: 100%; height: auto; }
 :host ::ng-deep .body blockquote { margin: 12px 0; padding-left: 14px; border-left: 3px solid #176c55; color: #52685d; }
 small { display: block; margin-top: 8px; color: #60716b; font-size: 12px; line-height: 1.6; font-weight: 400; }

+ 14 - 0
projects/xiaoshu-admin/src/app/shared/training-body-editor.component.spec.ts

@@ -57,6 +57,20 @@ describe('TrainingBodyEditorComponent', () => {
     expect(body.querySelector('img')!.hasAttribute('onerror')).toBeFalse();
   });
 
+  it('renders Chinese italics even when the application disables font synthesis, and can toggle them off', () => {
+    fixture.nativeElement.style.fontSynthesis = 'none';
+    fixture.componentRef.setInput('value', '<p>中文斜体验证 English</p>');
+    fixture.detectChanges();
+    select(0, 6);
+    editor.format('italic');
+    const italic = body.querySelector('i,em')!;
+    expect(italic.textContent).toBe('中文斜体验证');
+    expect(getComputedStyle(italic).fontStyle).toBe('italic');
+    expect(getComputedStyle(italic).getPropertyValue('font-synthesis-style')).toBe('auto');
+    editor.format('italic');
+    expect(body.querySelector('i,em')).toBeNull();
+  });
+
   it('pastes external content as plain text', () => {
     select(0, 6);
     const data = new DataTransfer();

+ 14 - 11
projects/xiaoshu-admin/src/app/shared/training-body-editor.component.ts

@@ -1,9 +1,11 @@
 import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnChanges, SecurityContext, ViewChild, inject, output } from '@angular/core';
 import { DomSanitizer } from '@angular/platform-browser';
+import { Bold, Heading2, Images, Italic, List, ListOrdered, LucideAngularModule, Quote, Redo2, RemoveFormatting, Type, Underline, Undo2 } from 'lucide-angular';
 
 @Component({
   selector: 'app-training-body-editor',
   standalone: true,
+  imports: [LucideAngularModule],
   templateUrl: './training-body-editor.component.html',
   styleUrl: './training-body-editor.component.scss',
   changeDetection: ChangeDetectionStrategy.OnPush,
@@ -17,18 +19,19 @@ export class TrainingBodyEditorComponent implements AfterViewInit, OnChanges {
   private readonly sanitizer = inject(DomSanitizer);
   private selection: Range | null = null;
   private emittedValue: string | null = null;
+  readonly imagesIcon = Images;
   readonly tools = [
-    { label: '加粗', command: 'bold', value: '' },
-    { label: '斜体', command: 'italic', value: '' },
-    { label: '下划线', command: 'underline', value: '' },
-    { label: '小标题', command: 'formatBlock', value: 'h2' },
-    { label: '正文', command: 'formatBlock', value: 'p' },
-    { label: '无序列表', command: 'insertUnorderedList', value: '' },
-    { label: '有序列表', command: 'insertOrderedList', value: '' },
-    { label: '引用', command: 'formatBlock', value: 'blockquote' },
-    { label: '清除格式', command: 'removeFormat', value: '' },
-    { label: '撤销', command: 'undo', value: '' },
-    { label: '重做', command: 'redo', value: '' },
+    { label: '加粗', icon: Bold, command: 'bold', value: '' },
+    { label: '斜体', icon: Italic, command: 'italic', value: '' },
+    { label: '下划线', icon: Underline, command: 'underline', value: '' },
+    { label: '小标题', icon: Heading2, command: 'formatBlock', value: 'h2' },
+    { label: '正文', icon: Type, command: 'formatBlock', value: 'p' },
+    { label: '无序列表', icon: List, command: 'insertUnorderedList', value: '' },
+    { label: '有序列表', icon: ListOrdered, command: 'insertOrderedList', value: '' },
+    { label: '引用', icon: Quote, command: 'formatBlock', value: 'blockquote' },
+    { label: '清除格式', icon: RemoveFormatting, command: 'removeFormat', value: '' },
+    { label: '撤销', icon: Undo2, command: 'undo', value: '' },
+    { label: '重做', icon: Redo2, command: 'redo', value: '' },
   ];
 
   ngAfterViewInit(): void { this.renderValue(); }

+ 1 - 1
projects/xiaoshu-mobile/src/app/features/article/article-page.component.scss

@@ -2,7 +2,7 @@
 .article-page header, .article-content { margin-bottom: 10px; padding: 18px 16px; background: white; }
 .article-page h1 { margin: 0 0 12px; font-size: 23px; line-height: 1.35; text-align: center; letter-spacing: 0; }
 .article-page header p { display: flex; align-items: center; justify-content: center; margin: 0; color: var(--ink-soft); font-size: 12px; gap: 6px; }
-.article-content { min-height: 180px; color: var(--ink); font-size: 15px; line-height: 1.8; overflow-wrap: anywhere; }
+.article-content { min-height: 180px; color: var(--ink); font-size: 15px; line-height: 1.8; overflow-wrap: anywhere; font-synthesis: style; }
 .article-content :is(p, ul, ol, blockquote) { margin: 0 0 12px; }
 .article-content :is(img, table) { max-width: 100%; }
 .article-content img { height: auto; border-radius: var(--radius-sm); }