add:增加反馈功能

This commit is contained in:
hzm
2026-02-02 12:42:21 +08:00
parent b97c63b9d3
commit f08113e990
4 changed files with 222 additions and 1 deletions

View File

@@ -298,11 +298,27 @@ body {
color: var(--muted); color: var(--muted);
} }
.error-text {
color: var(--danger);
font-size: 12px;
margin-top: 4px;
}
.feedback-modal {
max-width: 420px !important;
}
.link-button:hover {
color: var(--accent) !important;
opacity: 0.8;
}
.footer { .footer {
margin-top: 24px; margin-top: 24px;
font-size: 12px; font-size: 12px;
color: var(--muted); color: var(--muted);
text-align: center; text-align: center;
padding-bottom: 60px;
} }
.actions { .actions {

View File

@@ -2,6 +2,7 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { useForm, ValidationError } from '@formspree/react';
import Announcement from "./components/Announcement"; import Announcement from "./components/Announcement";
function PlusIcon(props) { function PlusIcon(props) {
@@ -70,6 +71,14 @@ function GridIcon(props) {
); );
} }
function CloseIcon(props) {
return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function ListIcon(props) { function ListIcon(props) {
return ( return (
<svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"> <svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
@@ -96,6 +105,98 @@ function Stat({ label, value, delta }) {
); );
} }
function FeedbackModal({ onClose }) {
const [state, handleSubmit] = useForm("xdadgvjd");
const onSubmit = (e) => {
const form = e?.target;
const nicknameInput = form?.elements?.namedItem?.('nickname');
if (nicknameInput && typeof nicknameInput.value === 'string') {
const v = nicknameInput.value.trim();
if (!v) nicknameInput.value = '匿名';
}
return handleSubmit(e);
};
return (
<motion.div
className="modal-overlay"
role="dialog"
aria-modal="true"
aria-label="意见反馈"
onClick={onClose}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 20 }}
className="glass card modal feedback-modal"
onClick={(e) => e.stopPropagation()}
>
<div className="title" style={{ marginBottom: 20, justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<SettingsIcon width="20" height="20" />
<span>意见反馈</span>
</div>
<button className="icon-button" onClick={onClose} style={{ border: 'none', background: 'transparent' }}>
<CloseIcon width="20" height="20" />
</button>
</div>
{state.succeeded ? (
<div className="success-message" style={{ textAlign: 'center', padding: '20px 0' }}>
<div style={{ fontSize: '48px', marginBottom: 16 }}>🎉</div>
<h3 style={{ marginBottom: 8 }}>感谢您的反馈</h3>
<p className="muted">我们已收到您的建议会尽快查看</p>
<button className="button" onClick={onClose} style={{ marginTop: 24, width: '100%' }}>
关闭
</button>
</div>
) : (
<form onSubmit={onSubmit} className="feedback-form">
<div className="form-group" style={{ marginBottom: 16 }}>
<label htmlFor="nickname" className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
您的昵称可选
</label>
<input
id="nickname"
type="text"
name="nickname"
className="input"
placeholder="匿名"
style={{ width: '100%' }}
/>
<ValidationError prefix="Nickname" field="nickname" errors={state.errors} className="error-text" />
</div>
<div className="form-group" style={{ marginBottom: 20 }}>
<label htmlFor="message" className="muted" style={{ display: 'block', marginBottom: 8, fontSize: '14px' }}>
反馈内容
</label>
<textarea
id="message"
name="message"
className="input"
required
placeholder="请描述您遇到的问题或建议..."
style={{ width: '100%', minHeight: '120px', padding: '12px', resize: 'vertical' }}
/>
<ValidationError prefix="Message" field="message" errors={state.errors} className="error-text" />
</div>
<button className="button" type="submit" disabled={state.submitting} style={{ width: '100%' }}>
{state.submitting ? '发送中...' : '提交反馈'}
</button>
</form>
)}
</motion.div>
</motion.div>
);
}
export default function HomePage() { export default function HomePage() {
const [funds, setFunds] = useState([]); const [funds, setFunds] = useState([]);
const [code, setCode] = useState(''); const [code, setCode] = useState('');
@@ -125,6 +226,10 @@ export default function HomePage() {
// 视图模式 // 视图模式
const [viewMode, setViewMode] = useState('card'); // card, list const [viewMode, setViewMode] = useState('card'); // card, list
// 反馈弹窗状态
const [feedbackOpen, setFeedbackOpen] = useState(false);
const [feedbackNonce, setFeedbackNonce] = useState(0);
const toggleFavorite = (code) => { const toggleFavorite = (code) => {
setFavorites(prev => { setFavorites(prev => {
const next = new Set(prev); const next = new Set(prev);
@@ -768,10 +873,35 @@ export default function HomePage() {
</div> </div>
<div className="footer"> <div className="footer">
<p>数据源实时估值与重仓直连东方财富无需后端部署即用</p> <p>数据源实时估值与重仓直连东方财富仅供个人学习及参考使用数据可能存在延迟不作为任何投资建议
</p>
<p>估算数据与真实结算数据会有1%左右误差</p> <p>估算数据与真实结算数据会有1%左右误差</p>
<div style={{ marginTop: 12, opacity: 0.8 }}>
<p>
遇到任何问题或需求建议可
<button
className="link-button"
onClick={() => {
setFeedbackNonce((n) => n + 1);
setFeedbackOpen(true);
}}
style={{ background: 'none', border: 'none', color: 'var(--primary)', cursor: 'pointer', padding: '0 4px', textDecoration: 'underline', fontSize: 'inherit', fontWeight: 600 }}
>
点此提交反馈
</button>
</p>
</div>
</div> </div>
<AnimatePresence>
{feedbackOpen && (
<FeedbackModal
key={feedbackNonce}
onClose={() => setFeedbackOpen(false)}
/>
)}
</AnimatePresence>
{settingsOpen && ( {settingsOpen && (
<div className="modal-overlay" role="dialog" aria-modal="true" aria-label="设置" onClick={() => setSettingsOpen(false)}> <div className="modal-overlay" role="dialog" aria-modal="true" aria-label="设置" onClick={() => setSettingsOpen(false)}>
<div className="glass card modal" onClick={(e) => e.stopPropagation()}> <div className="glass card modal" onClick={(e) => e.stopPropagation()}>

74
package-lock.json generated
View File

@@ -8,12 +8,37 @@
"name": "real-time-fund", "name": "real-time-fund",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@formspree/react": "^3.0.0",
"framer-motion": "^12.29.2", "framer-motion": "^12.29.2",
"next": "14.2.5", "next": "14.2.5",
"react": "18.3.1", "react": "18.3.1",
"react-dom": "18.3.1" "react-dom": "18.3.1"
} }
}, },
"node_modules/@formspree/core": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@formspree/core/-/core-4.0.0.tgz",
"integrity": "sha512-geNlUut5nME1Ztej5Pzx1BrlQ1fFIcJYIqmF+Vm0jaUbpZxjXvt7SDOGeQVkuxn80QJiIHlwBGGjSBFjPX/KDw==",
"license": "MIT",
"dependencies": {
"@stripe/stripe-js": "^5.7.0"
}
},
"node_modules/@formspree/react": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@formspree/react/-/react-3.0.0.tgz",
"integrity": "sha512-8PufBZ4l13VmCp9xTGQXwyF6mZtYSecqgTlFuMo5Fbe4Q6zUk7PMU2uKOwIaytZyTyJgFVCvdbpQElPPBKYNLw==",
"license": "MIT",
"dependencies": {
"@formspree/core": "^4.0.0",
"@stripe/react-stripe-js": "^3.1.1",
"@stripe/stripe-js": "^5.7.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0"
}
},
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "14.2.5", "version": "14.2.5",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.5.tgz",
@@ -164,6 +189,29 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/@stripe/react-stripe-js": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.10.0.tgz",
"integrity": "sha512-UPqHZwMwDzGSax0ZI7XlxR3tZSpgIiZdk3CiwjbTK978phwR/fFXeAXQcN/h8wTAjR4ZIAzdlI9DbOqJhuJdeg==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.7.2"
},
"peerDependencies": {
"@stripe/stripe-js": ">=1.44.1 <8.0.0",
"react": ">=16.8.0 <20.0.0",
"react-dom": ">=16.8.0 <20.0.0"
}
},
"node_modules/@stripe/stripe-js": {
"version": "5.10.0",
"resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-5.10.0.tgz",
"integrity": "sha512-PTigkxMdMUP6B5ISS7jMqJAKhgrhZwjprDqR1eATtFfh0OpKVNp110xiH+goeVdrJ29/4LeZJR4FaHHWstsu0A==",
"license": "MIT",
"engines": {
"node": ">=12.16"
}
},
"node_modules/@swc/counter": { "node_modules/@swc/counter": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz",
@@ -352,6 +400,15 @@
} }
} }
}, },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -386,6 +443,17 @@
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
} }
}, },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/react": { "node_modules/react": {
"version": "18.3.1", "version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
@@ -411,6 +479,12 @@
"react": "^18.3.1" "react": "^18.3.1"
} }
}, },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.23.2", "version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",

View File

@@ -8,6 +8,7 @@
"start": "next start" "start": "next start"
}, },
"dependencies": { "dependencies": {
"@formspree/react": "^3.0.0",
"framer-motion": "^12.29.2", "framer-motion": "^12.29.2",
"next": "14.2.5", "next": "14.2.5",
"react": "18.3.1", "react": "18.3.1",