add:增加反馈功能
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
132
app/page.jsx
132
app/page.jsx
@@ -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
74
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user