refactor: 将情景图标和设备选择器的 emoji 统一替换为 SVG 图标
- 新增 scenarioIcons.tsx 提取情景图标渲染逻辑 - 情景芯片、设备选择器、警告提示等 emoji 全部改为 Feather-style SVG - 调整 CSS 确保 SVG 图标居中对齐
This commit is contained in:
@@ -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-style,stroke-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">
|
||||
|
||||
Reference in New Issue
Block a user