feat(ui): 增加关于弹窗(PineSound 品牌)
- AboutDialog:品牌 Logo/产品名/版本/技术栈/版权,Esc 或点击遮罩关闭 - PageHeader 右上角新增「ⓘ 关于 PineSound」入口(info 图标) - 样式:about-* 系列(毛玻璃遮罩 + 渐变品牌块),跟随 DPM 配色
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user