refactor: 将情景图标和设备选择器的 emoji 统一替换为 SVG 图标

- 新增 scenarioIcons.tsx 提取情景图标渲染逻辑
- 情景芯片、设备选择器、警告提示等 emoji 全部改为 Feather-style SVG
- 调整 CSS 确保 SVG 图标居中对齐
This commit is contained in:
2026-06-21 23:50:27 +08:00
parent 6cbabb63bb
commit e24fb2cc19
10 changed files with 232 additions and 75 deletions

View File

@@ -19,11 +19,34 @@ import { EditScenarioModal } from "./components/EditScenarioModal";
import { AuthProvider, useAuth } from "./lib/auth";
import { loadConfig, loadTheme, saveTheme } from "./lib/storage";
import { I18nContext, parseLocale, t } from "./lib/i18n";
import { renderScenarioIconById } from "./lib/scenarioIcons";
import type { Locale } from "./lib/i18n";
import type { Theme } from "./types";
import type { UserScenario } from "./lib/api/scenarios";
import "./App.css";
// ---- 统一 SVG 图标系统Feather-stylestroke-based ----
const SVG_PROPS = { width: 14, height: 14, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round" as const, strokeLinejoin: "round" as const };
/** 系统情景 SVG 图标映射 */
function scenarioSvgIcon(id: string) {
switch (id) {
case "free_chat":
return (<svg {...SVG_PROPS}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>);
case "interviewer":
return (<svg {...SVG_PROPS}><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>);
case "english_teacher":
return (<svg {...SVG_PROPS}><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>);
case "debate":
return (<svg {...SVG_PROPS}><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M8 10h8"/><path d="M8 14h4"/></svg>);
case "interpreter":
return (<svg {...SVG_PROPS}><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>);
default:
return null;
}
}
/** 内部组件,确保在 I18nContext.Provider 内部使用 hooks */
function AppContent() {
const { isAuthenticated, isLoading, user, logout, accessToken } = useAuth();
@@ -378,7 +401,7 @@ function AppContent() {
<div className="video-indicator video-indicator--audio">{tr("video.playing")}</div>
)}
{vadError && (
<div className="video-indicator video-indicator--error"> {vadError}</div>
<div className="video-indicator video-indicator--error"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> {vadError}</div>
)}
{!isConnected && !stream && (
<div className="video-placeholder">
@@ -412,7 +435,9 @@ function AppContent() {
onClick={() => handleSelectScenario(sc.id)}
title={sc.description ? sc.description : sc.descKey ? tr(sc.descKey) : sc.name}
>
<span className="scenario-chip__icon">{sc.icon}</span>
<span className="scenario-chip__icon">
{scenarioSvgIcon(sc.id) || renderScenarioIconById(sc.icon, 14) || sc.icon}
</span>
<span className="scenario-chip__name">{sc.nameKey ? tr(sc.nameKey) : sc.name}</span>
</button>
))}
@@ -422,7 +447,9 @@ function AppContent() {
onClick={() => setShowCreateScenario(true)}
title={tr("scenario.create.button")}
>
<span className="scenario-chip__icon">+</span>
<span className="scenario-chip__icon">
<svg {...SVG_PROPS}><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
</span>
<span className="scenario-chip__name">{tr("scenario.createChip")}</span>
</button>
</div>
@@ -510,7 +537,7 @@ function AppContent() {
{/* 设备选择器 */}
<div className="video-controls__devices">
<div className="device-select-wrapper">
<label className="device-select-label">📷</label>
<label className="device-select-label"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg></label>
<select
className="device-select"
value={config.cameraDeviceId || "default"}
@@ -523,7 +550,7 @@ function AppContent() {
</select>
</div>
<div className="device-select-wrapper">
<label className="device-select-label">🎤</label>
<label className="device-select-label"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg></label>
<select
className="device-select"
value={config.micDeviceId || "default"}
@@ -542,7 +569,7 @@ function AppContent() {
{/* 文字对话态:视频已结束 */}
<div className="video-controls__text-only">
<div className="video-ended-hint">
<span className="video-ended-hint__title">📹 {tr("video.ended")}</span>
<span className="video-ended-hint__title"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg> {tr("video.ended")}</span>
<span className="video-ended-hint__sub">{tr("video.ended.hint")}</span>
</div>
<div className="video-controls__toolbar">