4 Commits

Author SHA1 Message Date
hzm
a3d90a756b feat: 发布 0.1.5 2026-02-09 10:31:09 +08:00
hzm
cd89f58d14 fix: 修复同步数据变化判断问题 2026-02-09 10:30:20 +08:00
hzm
ec7938e2ac feat:增加微信用户交流群入口 2026-02-09 09:02:39 +08:00
hzm
b23befd143 feat:修改 README 2026-02-09 08:38:28 +08:00
5 changed files with 151 additions and 12 deletions

View File

@@ -91,6 +91,7 @@ docker compose up -d
## 💬 开发者交流群 ## 💬 开发者交流群
欢迎基金实时开发者加入微信群聊讨论开发与协作: 欢迎基金实时开发者加入微信群聊讨论开发与协作:
<img src="./doc/webchatGroup.jpg" width="300"> <img src="./doc/webchatGroup.jpg" width="300">
## 📝 免责声明 ## 📝 免责声明

BIN
app/assets/weChatGroup.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 563 KiB

View File

@@ -11,6 +11,7 @@ import Announcement from "./components/Announcement";
import { DatePicker, DonateTabs, NumericInput, Stat } from "./components/Common"; import { DatePicker, DonateTabs, NumericInput, Stat } from "./components/Common";
import { ChevronIcon, CloseIcon, CloudIcon, DragIcon, ExitIcon, GridIcon, ListIcon, LoginIcon, LogoutIcon, MailIcon, PlusIcon, RefreshIcon, SettingsIcon, SortIcon, StarIcon, TrashIcon, UpdateIcon, UserIcon } from "./components/Icons"; import { ChevronIcon, CloseIcon, CloudIcon, DragIcon, ExitIcon, GridIcon, ListIcon, LoginIcon, LogoutIcon, MailIcon, PlusIcon, RefreshIcon, SettingsIcon, SortIcon, StarIcon, TrashIcon, UpdateIcon, UserIcon } from "./components/Icons";
import githubImg from "./assets/github.svg"; import githubImg from "./assets/github.svg";
import weChatGroupImg from "./assets/weChatGroup.png";
import { supabase, isSupabaseConfigured } from './lib/supabase'; import { supabase, isSupabaseConfigured } from './lib/supabase';
import { fetchFundData, fetchLatestRelease, fetchShanghaiIndexDate, fetchSmartFundNetValue, searchFunds, submitFeedback } from './api/fund'; import { fetchFundData, fetchLatestRelease, fetchShanghaiIndexDate, fetchSmartFundNetValue, searchFunds, submitFeedback } from './api/fund';
import packageJson from '../package.json'; import packageJson from '../package.json';
@@ -24,7 +25,7 @@ const nowInTz = () => dayjs().tz(TZ);
const toTz = (input) => (input ? dayjs.tz(input, TZ) : nowInTz()); const toTz = (input) => (input ? dayjs.tz(input, TZ) : nowInTz());
const formatDate = (input) => toTz(input).format('YYYY-MM-DD'); const formatDate = (input) => toTz(input).format('YYYY-MM-DD');
function FeedbackModal({ onClose, user }) { function FeedbackModal({ onClose, user, onOpenWeChat }) {
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [succeeded, setSucceeded] = useState(false); const [succeeded, setSucceeded] = useState(false);
const [error, setError] = useState(""); const [error, setError] = useState("");
@@ -148,6 +149,16 @@ function FeedbackModal({ onClose, user }) {
</a> </a>
区留言互动 区留言互动
</p> </p>
<p className="muted" style={{ fontSize: '12px', lineHeight: '1.6' }}>
或加入我们的
<a
className="link-button"
style={{ color: 'var(--primary)', textDecoration: 'underline', padding: '0 4px', fontWeight: 600, cursor: 'pointer' }}
onClick={onOpenWeChat}
>
微信用户交流群
</a>
</p>
</div> </div>
</form> </form>
)} )}
@@ -156,6 +167,46 @@ function FeedbackModal({ onClose, user }) {
); );
} }
function WeChatModal({ onClose }) {
return (
<motion.div
className="modal-overlay"
role="dialog"
aria-modal="true"
aria-label="微信用户交流群"
onClick={onClose}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
style={{ zIndex: 10002 }}
>
<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"
onClick={(e) => e.stopPropagation()}
style={{ maxWidth: '360px', padding: '24px' }}
>
<div className="title" style={{ marginBottom: 20, justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span>💬 微信用户交流群</span>
</div>
<button className="icon-button" onClick={onClose} style={{ border: 'none', background: 'transparent' }}>
<CloseIcon width="20" height="20" />
</button>
</div>
<div style={{ display: 'flex', justifyContent: 'center' }}>
<img src={weChatGroupImg.src} alt="WeChat Group" style={{ maxWidth: '100%', borderRadius: '8px' }} />
</div>
<p className="muted" style={{ textAlign: 'center', marginTop: 16, fontSize: '14px' }}>
扫码加入群聊获取最新更新与交流
</p>
</motion.div>
</motion.div>
);
}
function HoldingActionModal({ fund, onClose, onAction }) { function HoldingActionModal({ fund, onClose, onAction }) {
return ( return (
<motion.div <motion.div
@@ -1838,6 +1889,7 @@ export default function HomePage() {
// 反馈弹窗状态 // 反馈弹窗状态
const [feedbackOpen, setFeedbackOpen] = useState(false); const [feedbackOpen, setFeedbackOpen] = useState(false);
const [feedbackNonce, setFeedbackNonce] = useState(0); const [feedbackNonce, setFeedbackNonce] = useState(0);
const [weChatOpen, setWeChatOpen] = useState(false);
// 搜索相关状态 // 搜索相关状态
const [searchTerm, setSearchTerm] = useState(''); const [searchTerm, setSearchTerm] = useState('');
@@ -3021,16 +3073,94 @@ export default function HomePage() {
const importFileRef = useRef(null); const importFileRef = useRef(null);
const [importMsg, setImportMsg] = useState(''); const [importMsg, setImportMsg] = useState('');
const normalizeCode = (value) => String(value || '').trim();
const normalizeNumber = (value) => {
if (value === null || value === undefined || value === '') return null;
const num = Number(value);
return Number.isFinite(num) ? num : null;
};
function getComparablePayload(payload) { function getComparablePayload(payload) {
if (!payload || typeof payload !== 'object') return ''; if (!payload || typeof payload !== 'object') return '';
const rawFunds = Array.isArray(payload.funds) ? payload.funds : [];
const fundCodes = rawFunds
.map((fund) => normalizeCode(fund?.code || fund?.CODE))
.filter(Boolean);
const uniqueFundCodes = Array.from(new Set(fundCodes)).sort();
const favorites = Array.isArray(payload.favorites)
? Array.from(new Set(payload.favorites.map(normalizeCode).filter((code) => uniqueFundCodes.includes(code)))).sort()
: [];
const collapsedCodes = Array.isArray(payload.collapsedCodes)
? Array.from(new Set(payload.collapsedCodes.map(normalizeCode).filter((code) => uniqueFundCodes.includes(code)))).sort()
: [];
const groups = Array.isArray(payload.groups)
? payload.groups
.map((group) => {
const id = normalizeCode(group?.id);
if (!id) return null;
const name = typeof group?.name === 'string' ? group.name : '';
const codes = Array.isArray(group?.codes)
? Array.from(new Set(group.codes.map(normalizeCode).filter((code) => uniqueFundCodes.includes(code)))).sort()
: [];
return { id, name, codes };
})
.filter(Boolean)
.sort((a, b) => a.id.localeCompare(b.id))
: [];
const holdingsSource = payload.holdings && typeof payload.holdings === 'object' && !Array.isArray(payload.holdings)
? payload.holdings
: {};
const holdings = {};
Object.keys(holdingsSource)
.map(normalizeCode)
.filter((code) => uniqueFundCodes.includes(code))
.sort()
.forEach((code) => {
const value = holdingsSource[code] || {};
const share = normalizeNumber(value.share);
const cost = normalizeNumber(value.cost);
if (share === null && cost === null) return;
holdings[code] = { share, cost };
});
const pendingTrades = Array.isArray(payload.pendingTrades)
? payload.pendingTrades
.map((trade) => {
const fundCode = normalizeCode(trade?.fundCode);
if (!fundCode) return null;
return {
id: trade?.id ? String(trade.id) : '',
fundCode,
type: trade?.type || '',
share: normalizeNumber(trade?.share),
amount: normalizeNumber(trade?.amount),
feeRate: normalizeNumber(trade?.feeRate),
feeMode: trade?.feeMode || '',
feeValue: normalizeNumber(trade?.feeValue),
date: trade?.date || '',
isAfter3pm: !!trade?.isAfter3pm
};
})
.filter((trade) => trade && uniqueFundCodes.includes(trade.fundCode))
.sort((a, b) => {
const keyA = a.id || `${a.fundCode}|${a.type}|${a.date}|${a.share ?? ''}|${a.amount ?? ''}|${a.feeMode}|${a.feeValue ?? ''}|${a.feeRate ?? ''}|${a.isAfter3pm ? 1 : 0}`;
const keyB = b.id || `${b.fundCode}|${b.type}|${b.date}|${b.share ?? ''}|${b.amount ?? ''}|${b.feeMode}|${b.feeValue ?? ''}|${b.feeRate ?? ''}|${b.isAfter3pm ? 1 : 0}`;
return keyA.localeCompare(keyB);
})
: [];
return JSON.stringify({ return JSON.stringify({
funds: Array.isArray(payload.funds) ? payload.funds : [], funds: uniqueFundCodes,
favorites: Array.isArray(payload.favorites) ? payload.favorites : [], favorites,
groups: Array.isArray(payload.groups) ? payload.groups : [], groups,
collapsedCodes: Array.isArray(payload.collapsedCodes) ? payload.collapsedCodes : [], collapsedCodes,
refreshMs: Number.isFinite(payload.refreshMs) ? payload.refreshMs : 30000, refreshMs: Number.isFinite(payload.refreshMs) ? payload.refreshMs : 30000,
holdings: payload.holdings && typeof payload.holdings === 'object' ? payload.holdings : {}, holdings,
pendingTrades: Array.isArray(payload.pendingTrades) ? payload.pendingTrades : [] pendingTrades
}); });
} }
@@ -3432,7 +3562,8 @@ export default function HomePage() {
!!clearConfirm || !!clearConfirm ||
donateOpen || donateOpen ||
!!fundDeleteConfirm || !!fundDeleteConfirm ||
updateModalOpen; updateModalOpen ||
weChatOpen;
if (isAnyModalOpen) { if (isAnyModalOpen) {
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
@@ -3458,7 +3589,8 @@ export default function HomePage() {
tradeModal.open, tradeModal.open,
clearConfirm, clearConfirm,
donateOpen, donateOpen,
updateModalOpen updateModalOpen,
weChatOpen
]); ]);
useEffect(() => { useEffect(() => {
@@ -4545,9 +4677,15 @@ export default function HomePage() {
key={feedbackNonce} key={feedbackNonce}
onClose={() => setFeedbackOpen(false)} onClose={() => setFeedbackOpen(false)}
user={user} user={user}
onOpenWeChat={() => setWeChatOpen(true)}
/> />
)} )}
</AnimatePresence> </AnimatePresence>
<AnimatePresence>
{weChatOpen && (
<WeChatModal onClose={() => setWeChatOpen(false)} />
)}
</AnimatePresence>
<AnimatePresence> <AnimatePresence>
{addResultOpen && ( {addResultOpen && (
<AddResultModal <AddResultModal

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "real-time-fund", "name": "real-time-fund",
"version": "0.1.4", "version": "0.1.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "real-time-fund", "name": "real-time-fund",
"version": "0.1.4", "version": "0.1.5",
"dependencies": { "dependencies": {
"@dicebear/collection": "^9.3.1", "@dicebear/collection": "^9.3.1",
"@dicebear/core": "^9.3.1", "@dicebear/core": "^9.3.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "real-time-fund", "name": "real-time-fund",
"version": "0.1.4", "version": "0.1.5",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",