feat(ui): 增加关于弹窗(PineSound 品牌)

- AboutDialog:品牌 Logo/产品名/版本/技术栈/版权,Esc 或点击遮罩关闭
- PageHeader 右上角新增「ⓘ 关于 PineSound」入口(info 图标)
- 样式:about-* 系列(毛玻璃遮罩 + 渐变品牌块),跟随 DPM 配色
This commit is contained in:
Pine
2026-08-18 01:59:34 +08:00
parent 38e45aafa9
commit 1e60744961
4 changed files with 207 additions and 2 deletions
+59
View File
@@ -0,0 +1,59 @@
import { useEffect } from 'react';
/* =========================================================
AboutDialog —— 应用关于弹窗(PineSound 品牌)
页眉右上角「ⓘ」按钮打开;展示产品名/版本/技术栈/版权
========================================================= */
const APP_INFO = {
brand: 'PineSound',
product: '园区智能语音交互助手',
version: '0.1.0',
tagline: '面向大屏的数字园区智能体 —— 看得见 · 听得懂 · 说得出',
tech: [
'前端:React + Vite + Tauri',
'后端:FastAPI + 实时语音栈(s2s',
'识别:YOLO 人脸/姿态(举手开始·结束对话)',
'语音:qwen ASR / LLM / TTS(云端)',
],
copyright: '© 2026 PineSound · 昆明大学生创业园',
};
export default function AboutDialog({ open, onClose }) {
// Esc 关闭
useEffect(() => {
if (!open) return;
const onKey = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [open, onClose]);
if (!open) return null;
return (
<div className="about-overlay" onClick={onClose} role="dialog" aria-modal="true">
<div className="about-modal" onClick={(e) => e.stopPropagation()}>
<button className="about-close" type="button" aria-label="关闭" onClick={onClose}>×</button>
<div className="about-brand">
<div className="about-logo">PS</div>
<h2 className="about-name">{APP_INFO.brand}</h2>
<div className="about-product">{APP_INFO.product}</div>
</div>
<p className="about-tagline">{APP_INFO.tagline}</p>
<div className="about-section">
<h4>技术栈</h4>
<ul>
{APP_INFO.tech.map((t, i) => <li key={i}>{t}</li>)}
</ul>
</div>
<div className="about-meta">
<span>版本 v{APP_INFO.version}</span>
<span>{APP_INFO.copyright}</span>
</div>
</div>
</div>
);
}
+3 -2
View File
@@ -10,7 +10,7 @@ import {
Heart, Pill, Dna, Microscope, Atom, MapPin, Flag, Award, Medal,
History, Drone, Plane, Rocket, Satellite, Wallet, UserPlus, Activity,
Volume, VolumeOff, ChevronLeft, ChevronRight, PlayerPlay, PlayerPause,
Send, Trash, Eraser, Headset, CircleDot,
Send, Trash, Eraser, Headset, CircleDot, AlertCircle,
Microphone, MicrophoneFilled, MicrophoneOff, MessageCircle,
} from '@appica/icons-react';
@@ -80,6 +80,7 @@ const NAME_MAP = {
eraser: Eraser,
headset: Headset,
'circle-dot': CircleDot,
info: AlertCircle,
microphone: Microphone,
'microphone-filled': MicrophoneFilled,
'microphone-off': MicrophoneOff,
@@ -116,6 +117,6 @@ export {
Heart, Pill, Dna, Microscope, Atom, MapPin, Flag, Award, Medal,
History, Drone, Plane, Rocket, Satellite, Wallet, UserPlus, Activity,
Volume, VolumeOff, ChevronLeft, ChevronRight, PlayerPlay, PlayerPause,
Send, Trash, Eraser, Headset, CircleDot,
Send, Trash, Eraser, Headset, CircleDot, AlertCircle,
Microphone, MicrophoneFilled, MicrophoneOff, MessageCircle,
};
+14
View File
@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import Icon from './Icons';
import MediaHeaderControls from './MediaHeaderControls';
import AboutDialog from './AboutDialog';
import { PAGE_ORDER, usePageIndex } from '../utils/pageNav';
import { Avatar, AvatarImage, AvatarFallback } from '@appica/ui-react/avatar'
@@ -21,6 +22,7 @@ export default function PageHeader() {
const navigate = useNavigate();
const location = useLocation();
const [now, setNow] = useState(new Date());
const [aboutOpen, setAboutOpen] = useState(false);
const pageIdx = usePageIndex();
useEffect(() => {
@@ -74,11 +76,23 @@ export default function PageHeader() {
<span className="bd-live-dot" />
{status}
</div>
<button
className="bd-about-btn"
type="button"
title="关于 PineSound"
aria-label="关于 PineSound"
onClick={() => setAboutOpen(true)}
>
<Icon name="info" size={14} />
</button>
<div className="bd-clock">
<div className="bd-clock-time">{timeStr}</div>
<div className="bd-clock-date">{dateStr} {week}</div>
</div>
</div>
{/* 关于 PineSound */}
<AboutDialog open={aboutOpen} onClose={() => setAboutOpen(false)} />
</header>
);
}
+131
View File
@@ -2554,3 +2554,134 @@
@media (max-width: 1280px) {
.bd-hd-media-name { display: none; }
}
/* ==================== 关于 PineSound 弹窗 ==================== */
.bd-about-btn {
flex: none;
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid rgba(47, 107, 255, 0.25);
background: rgba(255, 255, 255, 0.7);
color: var(--bd-sub, #7d8fa9);
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.bd-about-btn:hover {
color: #2f6bff;
border-color: #2f6bff;
background: #fff;
}
.about-overlay {
position: fixed;
inset: 0;
z-index: 300;
background: rgba(13, 28, 48, 0.45);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
}
.about-modal {
position: relative;
width: min(420px, calc(100vw - 48px));
background: #fff;
border-radius: 16px;
padding: 28px 30px 22px;
box-shadow: 0 24px 60px rgba(15, 42, 96, 0.28);
animation: about-pop 0.25s ease;
}
@keyframes about-pop {
from { opacity: 0; transform: translateY(12px) scale(0.97); }
to { opacity: 1; transform: none; }
}
.about-close {
position: absolute;
top: 12px;
right: 14px;
width: 28px;
height: 28px;
border: none;
background: transparent;
color: #9fb0c6;
font-size: 20px;
line-height: 1;
cursor: pointer;
border-radius: 8px;
}
.about-close:hover { color: #1d2c44; background: #f0f4fa; }
.about-brand { text-align: center; margin-bottom: 6px; }
.about-logo {
width: 64px;
height: 64px;
margin: 0 auto 10px;
border-radius: 18px;
background: linear-gradient(135deg, #2f6bff 0%, #22d3ee 100%);
color: #fff;
font-size: 26px;
font-weight: 800;
letter-spacing: 0.02em;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10px 24px rgba(47, 107, 255, 0.35);
}
.about-name {
margin: 0;
font-size: 24px;
font-weight: 800;
color: #1d2c44;
letter-spacing: 0.04em;
}
.about-product {
margin-top: 4px;
font-size: 13px;
color: #7d8fa9;
}
.about-tagline {
text-align: center;
margin: 14px 0 18px;
font-size: 12.5px;
color: #4a5b74;
line-height: 1.6;
padding: 10px 12px;
background: #f5f8ff;
border-radius: 10px;
}
.about-section h4 {
margin: 0 0 8px;
font-size: 12px;
color: #7d8fa9;
letter-spacing: 0.08em;
}
.about-section ul {
margin: 0;
padding: 0;
list-style: none;
display: flex;
flex-direction: column;
gap: 6px;
}
.about-section li {
font-size: 12.5px;
color: #33425c;
padding: 6px 10px;
background: #f8fafd;
border-radius: 8px;
border: 1px solid #eef2f8;
}
.about-meta {
margin-top: 18px;
padding-top: 14px;
border-top: 1px dashed #e3ecf7;
display: flex;
justify-content: space-between;
font-size: 11px;
color: #9fb0c6;
}