feat:基金详情支持编辑持仓金额
This commit is contained in:
@@ -1,10 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { CloseIcon } from './Icons';
|
import { CloseIcon } from './Icons';
|
||||||
import { fetchSmartFundNetValue } from '../api/fund';
|
import { fetchSmartFundNetValue } from '../api/fund';
|
||||||
import { DatePicker } from './Common';
|
import { DatePicker } from './Common';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
export default function AddHistoryModal({ fund, onClose, onConfirm }) {
|
export default function AddHistoryModal({ fund, onClose, onConfirm }) {
|
||||||
const [type, setType] = useState('');
|
const [type, setType] = useState('');
|
||||||
@@ -77,30 +82,36 @@ export default function AddHistoryModal({ fund, onClose, onConfirm }) {
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenChange = (open) => {
|
||||||
|
if (!open) {
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseClick = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onClose?.();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<Dialog open onOpenChange={handleOpenChange}>
|
||||||
className="modal-overlay"
|
<DialogContent
|
||||||
role="dialog"
|
showCloseButton={false}
|
||||||
aria-modal="true"
|
|
||||||
aria-label="添加历史记录"
|
|
||||||
onClick={onClose}
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
style={{ zIndex: 1200 }}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
className="glass card modal"
|
className="glass card modal"
|
||||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
overlayClassName="modal-overlay"
|
||||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
overlayStyle={{ zIndex: 9998 }}
|
||||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
style={{ maxWidth: '420px', zIndex: 9999, width: '90vw' }}
|
||||||
style={{ maxWidth: '420px' }}
|
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
|
<DialogTitle className="sr-only">添加历史记录</DialogTitle>
|
||||||
|
|
||||||
<div className="title" style={{ marginBottom: 20, justifyContent: 'space-between' }}>
|
<div className="title" style={{ marginBottom: 20, justifyContent: 'space-between' }}>
|
||||||
<span>添加历史记录</span>
|
<span>添加历史记录</span>
|
||||||
<button className="icon-button" onClick={onClose} style={{ border: 'none', background: 'transparent' }}>
|
<button
|
||||||
<CloseIcon />
|
className="icon-button"
|
||||||
|
onClick={handleCloseClick}
|
||||||
|
style={{ border: 'none', background: 'transparent' }}
|
||||||
|
>
|
||||||
|
<CloseIcon width="20" height="20" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -200,15 +211,18 @@ export default function AddHistoryModal({ fund, onClose, onConfirm }) {
|
|||||||
<div className="muted" style={{ fontSize: '11px', lineHeight: 1.5, marginBottom: 16, paddingTop: 12, borderTop: '1px solid rgba(255,255,255,0.08)' }}>
|
<div className="muted" style={{ fontSize: '11px', lineHeight: 1.5, marginBottom: 16, paddingTop: 12, borderTop: '1px solid rgba(255,255,255,0.08)' }}>
|
||||||
*此处补录的买入/卖出仅作记录展示,不会改变当前持仓金额与份额;实际持仓请在持仓设置中维护。
|
*此处补录的买入/卖出仅作记录展示,不会改变当前持仓金额与份额;实际持仓请在持仓设置中维护。
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
<button
|
<Button
|
||||||
className="button primary full-width"
|
type="button"
|
||||||
onClick={handleSubmit}
|
variant="default"
|
||||||
disabled={!type || !date || !netValue || !amount || !share || loading}
|
size="lg"
|
||||||
>
|
onClick={handleSubmit}
|
||||||
确认添加
|
disabled={!type || !date || !netValue || !amount || !share || loading}
|
||||||
</button>
|
>
|
||||||
</motion.div>
|
确认添加
|
||||||
</motion.div>
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, useRef } from 'react';
|
import { useEffect, useState, useRef } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
import { DatePicker, NumericInput } from './Common';
|
import { DatePicker, NumericInput } from './Common';
|
||||||
import { isNumber } from 'lodash';
|
import { isNumber } from 'lodash';
|
||||||
import { CloseIcon } from './Icons';
|
import { CloseIcon } from './Icons';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
@@ -170,30 +174,28 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenChange = (open) => {
|
||||||
|
if (!open) {
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<Dialog open onOpenChange={handleOpenChange}>
|
||||||
className="modal-overlay"
|
<DialogContent
|
||||||
role="dialog"
|
showCloseButton={false}
|
||||||
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 dca-modal"
|
className="glass card modal dca-modal"
|
||||||
onClick={(e) => e.stopPropagation()}
|
overlayClassName="modal-overlay"
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '420px',
|
maxWidth: '420px',
|
||||||
maxHeight: '90vh',
|
maxHeight: '90vh',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
|
zIndex: 999,
|
||||||
|
width: '90vw',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
<DialogTitle className="sr-only">定投设置</DialogTitle>
|
||||||
<div
|
<div
|
||||||
className="scrollbar-y-styled"
|
className="scrollbar-y-styled"
|
||||||
style={{
|
style={{
|
||||||
@@ -376,8 +378,8 @@ export default function DcaModal({ fund, plan, onClose, onConfirm }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</DialogContent>
|
||||||
</motion.div>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -227,25 +227,25 @@ export default function FundCard({
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 4,
|
gap: 4,
|
||||||
cursor: layoutMode === 'drawer' ? 'default' : 'pointer',
|
cursor: 'pointer',
|
||||||
}}
|
}}
|
||||||
onClick={() => layoutMode !== 'drawer' && onHoldingClick?.(f)}
|
onClick={() => onHoldingClick?.(f)}
|
||||||
>
|
>
|
||||||
未设置 {layoutMode !== 'drawer' && <SettingsIcon width="12" height="12" />}
|
未设置 <SettingsIcon width="12" height="12" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="stat"
|
className="stat"
|
||||||
style={{ cursor: layoutMode === 'drawer' ? 'default' : 'pointer', flexDirection: 'column', gap: 4 }}
|
style={{ cursor: 'pointer', flexDirection: 'column', gap: 4 }}
|
||||||
onClick={() => layoutMode !== 'drawer' && onActionClick?.(f)}
|
onClick={() => onActionClick?.(f)}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="label"
|
className="label"
|
||||||
style={{ display: 'flex', alignItems: 'center', gap: 4 }}
|
style={{ display: 'flex', alignItems: 'center', gap: 4 }}
|
||||||
>
|
>
|
||||||
持仓金额 {layoutMode !== 'drawer' && <SettingsIcon width="12" height="12" style={{ opacity: 0.7 }} />}
|
持仓金额 <SettingsIcon width="12" height="12" style={{ opacity: 0.7 }} />
|
||||||
</span>
|
</span>
|
||||||
<span className="value">
|
<span className="value">
|
||||||
{masked ? '******' : `¥${profit.amount.toFixed(2)}`}
|
{masked ? '******' : `¥${profit.amount.toFixed(2)}`}
|
||||||
|
|||||||
@@ -1,53 +1,50 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { CloseIcon, SettingsIcon } from './Icons';
|
import { CloseIcon, SettingsIcon } from './Icons';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
|
||||||
export default function HoldingActionModal({ fund, onClose, onAction, hasHistory }) {
|
export default function HoldingActionModal({ fund, onClose, onAction, hasHistory }) {
|
||||||
|
const handleOpenChange = (open) => {
|
||||||
|
if (!open) {
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<Dialog open onOpenChange={handleOpenChange}>
|
||||||
className="modal-overlay"
|
<DialogContent
|
||||||
role="dialog"
|
showCloseButton={false}
|
||||||
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"
|
className="glass card modal"
|
||||||
onClick={(e) => e.stopPropagation()}
|
overlayClassName="modal-overlay"
|
||||||
style={{ maxWidth: '320px' }}
|
style={{ maxWidth: '320px', zIndex: 99 }}
|
||||||
>
|
>
|
||||||
|
<DialogTitle className="sr-only">持仓操作</DialogTitle>
|
||||||
<div className="title" style={{ marginBottom: 20, justifyContent: 'space-between' }}>
|
<div className="title" style={{ marginBottom: 20, justifyContent: 'space-between' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
<SettingsIcon width="20" height="20" />
|
<SettingsIcon width="20" height="20" />
|
||||||
<span>持仓操作</span>
|
<span>持仓操作</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onAction('history')}
|
className="button secondary"
|
||||||
style={{
|
onClick={() => onAction('history')}
|
||||||
marginLeft: 8,
|
style={{
|
||||||
padding: '4px 8px',
|
marginLeft: 8,
|
||||||
fontSize: '12px',
|
padding: '4px 10px',
|
||||||
background: 'rgba(255,255,255,0.1)',
|
fontSize: '12px',
|
||||||
border: 'none',
|
height: '28px',
|
||||||
borderRadius: '4px',
|
display: 'flex',
|
||||||
color: 'var(--text)',
|
alignItems: 'center',
|
||||||
cursor: 'pointer',
|
gap: 4,
|
||||||
display: 'flex',
|
}}
|
||||||
alignItems: 'center',
|
title="查看交易记录"
|
||||||
gap: 4
|
>
|
||||||
}}
|
<span>📜</span>
|
||||||
title="查看交易记录"
|
<span>交易记录</span>
|
||||||
>
|
</button>
|
||||||
<span>📜</span>
|
|
||||||
<span>交易记录</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<button className="icon-button" onClick={onClose} style={{ border: 'none', background: 'transparent' }}>
|
<button className="icon-button" onClick={onClose} style={{ border: 'none', background: 'transparent' }}>
|
||||||
<CloseIcon width="20" height="20" />
|
<CloseIcon width="20" height="20" />
|
||||||
@@ -92,13 +89,13 @@ export default function HoldingActionModal({ fund, onClose, onAction, hasHistory
|
|||||||
background: 'linear-gradient(180deg, #ef4444, #f87171)',
|
background: 'linear-gradient(180deg, #ef4444, #f87171)',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
color: '#2b0b0b',
|
color: '#2b0b0b',
|
||||||
fontWeight: 600
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
清空持仓
|
清空持仓
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</DialogContent>
|
||||||
</motion.div>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { CloseIcon, SettingsIcon } from './Icons';
|
import { CloseIcon, SettingsIcon } from './Icons';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
|
||||||
export default function HoldingEditModal({ fund, holding, onClose, onSave }) {
|
export default function HoldingEditModal({ fund, holding, onClose, onSave }) {
|
||||||
const [mode, setMode] = useState('amount'); // 'amount' | 'share'
|
const [mode, setMode] = useState('amount'); // 'amount' | 'share'
|
||||||
@@ -89,25 +93,21 @@ export default function HoldingEditModal({ fund, holding, onClose, onSave }) {
|
|||||||
? (share && cost && !isNaN(share) && !isNaN(cost))
|
? (share && cost && !isNaN(share) && !isNaN(cost))
|
||||||
: (amount && !isNaN(amount) && (!profit || !isNaN(profit)) && dwjz > 0);
|
: (amount && !isNaN(amount) && (!profit || !isNaN(profit)) && dwjz > 0);
|
||||||
|
|
||||||
|
const handleOpenChange = (open) => {
|
||||||
|
if (!open) {
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<Dialog open onOpenChange={handleOpenChange}>
|
||||||
className="modal-overlay"
|
<DialogContent
|
||||||
role="dialog"
|
showCloseButton={false}
|
||||||
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"
|
className="glass card modal"
|
||||||
onClick={(e) => e.stopPropagation()}
|
overlayClassName="modal-overlay"
|
||||||
style={{ maxWidth: '400px' }}
|
style={{ maxWidth: '400px', zIndex: 999, width: '90vw' }}
|
||||||
>
|
>
|
||||||
|
<DialogTitle className="sr-only">编辑持仓</DialogTitle>
|
||||||
<div className="title" style={{ marginBottom: 20, justifyContent: 'space-between' }}>
|
<div className="title" style={{ marginBottom: 20, justifyContent: 'space-between' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
<SettingsIcon width="20" height="20" />
|
<SettingsIcon width="20" height="20" />
|
||||||
@@ -238,7 +238,7 @@ export default function HoldingEditModal({ fund, holding, onClose, onSave }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</motion.div>
|
</DialogContent>
|
||||||
</motion.div>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
94
app/components/PendingTradesModal.jsx
Normal file
94
app/components/PendingTradesModal.jsx
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { CloseIcon } from './Icons';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
|
||||||
|
export default function PendingTradesModal({
|
||||||
|
open,
|
||||||
|
trades = [],
|
||||||
|
onClose,
|
||||||
|
onRevoke,
|
||||||
|
}) {
|
||||||
|
const handleOpenChange = (nextOpen) => {
|
||||||
|
if (!nextOpen) {
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogContent
|
||||||
|
showCloseButton={false}
|
||||||
|
className="glass card modal trade-modal"
|
||||||
|
overlayClassName="modal-overlay"
|
||||||
|
overlayStyle={{ zIndex: 998 }}
|
||||||
|
style={{ maxWidth: '420px', zIndex: 999, width: '90vw' }}
|
||||||
|
>
|
||||||
|
<DialogTitle className="sr-only">待交易队列</DialogTitle>
|
||||||
|
|
||||||
|
<div className="title" style={{ marginBottom: 20, justifyContent: 'space-between' }}>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
|
<span style={{ fontSize: '20px' }}>📥</span>
|
||||||
|
<span>待交易队列</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="icon-button"
|
||||||
|
onClick={onClose}
|
||||||
|
style={{ border: 'none', background: 'transparent' }}
|
||||||
|
>
|
||||||
|
<CloseIcon width="20" height="20" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pending-list" style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
||||||
|
<div className="pending-list-items" style={{ paddingTop: 0 }}>
|
||||||
|
{trades.map((trade, idx) => (
|
||||||
|
<div key={trade.id || idx} className="trade-pending-item">
|
||||||
|
<div className="row" style={{ justifyContent: 'space-between', marginBottom: 4 }}>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: '14px',
|
||||||
|
color: trade.type === 'buy' ? 'var(--danger)' : 'var(--success)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{trade.type === 'buy' ? '买入' : '卖出'}
|
||||||
|
</span>
|
||||||
|
<span className="muted" style={{ fontSize: '12px' }}>
|
||||||
|
{trade.date} {trade.isAfter3pm ? '(15:00后)' : ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="row" style={{ justifyContent: 'space-between', fontSize: '12px' }}>
|
||||||
|
<span className="muted">份额/金额</span>
|
||||||
|
<span>{trade.share ? `${trade.share} 份` : `¥${trade.amount}`}</span>
|
||||||
|
</div>
|
||||||
|
<div className="row" style={{ justifyContent: 'space-between', fontSize: '12px', marginTop: 4 }}>
|
||||||
|
<span className="muted">状态</span>
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
|
<span className="trade-pending-status">等待净值更新...</span>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="xs"
|
||||||
|
variant="destructive"
|
||||||
|
className="bg-destructive text-white hover:bg-destructive/90"
|
||||||
|
onClick={() => onRevoke?.(trade)}
|
||||||
|
style={{ paddingInline: 10 }}
|
||||||
|
>
|
||||||
|
撤销
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import utc from 'dayjs/plugin/utc';
|
import utc from 'dayjs/plugin/utc';
|
||||||
import timezone from 'dayjs/plugin/timezone';
|
import timezone from 'dayjs/plugin/timezone';
|
||||||
@@ -10,6 +10,12 @@ import { fetchSmartFundNetValue } from '../api/fund';
|
|||||||
import { DatePicker, NumericInput } from './Common';
|
import { DatePicker, NumericInput } from './Common';
|
||||||
import ConfirmModal from './ConfirmModal';
|
import ConfirmModal from './ConfirmModal';
|
||||||
import { CloseIcon } from './Icons';
|
import { CloseIcon } from './Icons';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import PendingTradesModal from './PendingTradesModal';
|
||||||
|
|
||||||
dayjs.extend(utc);
|
dayjs.extend(utc);
|
||||||
dayjs.extend(timezone);
|
dayjs.extend(timezone);
|
||||||
@@ -153,36 +159,33 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
|
|||||||
|
|
||||||
const [revokeTrade, setRevokeTrade] = useState(null);
|
const [revokeTrade, setRevokeTrade] = useState(null);
|
||||||
|
|
||||||
|
const handleOpenChange = (open) => {
|
||||||
|
if (!open) {
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<Dialog open onOpenChange={handleOpenChange}>
|
||||||
className="modal-overlay"
|
<DialogContent
|
||||||
role="dialog"
|
showCloseButton={false}
|
||||||
aria-modal="true"
|
|
||||||
aria-label={isBuy ? "加仓" : "减仓"}
|
|
||||||
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 trade-modal"
|
className="glass card modal trade-modal"
|
||||||
onClick={(e) => e.stopPropagation()}
|
overlayClassName="modal-overlay"
|
||||||
style={{ maxWidth: '420px' }}
|
overlayStyle={{ zIndex: 99 }}
|
||||||
|
style={{ maxWidth: '420px', width: '90vw', zIndex: 99 }}
|
||||||
>
|
>
|
||||||
|
<DialogTitle className="sr-only">{isBuy ? '加仓' : '减仓'}</DialogTitle>
|
||||||
<div className="title" style={{ marginBottom: 20, justifyContent: 'space-between' }}>
|
<div className="title" style={{ marginBottom: 20, justifyContent: 'space-between' }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
<span style={{ fontSize: '20px' }}>{isBuy ? '📥' : '📤'}</span>
|
<span style={{ fontSize: '20px' }}>{isBuy ? '📥' : '📤'}</span>
|
||||||
<span>{showPendingList ? '待交易队列' : (showConfirm ? (isBuy ? '买入确认' : '卖出确认') : (isBuy ? '加仓' : '减仓'))}</span>
|
<span>{showConfirm ? (isBuy ? '买入确认' : '卖出确认') : (isBuy ? '加仓' : '减仓')}</span>
|
||||||
</div>
|
</div>
|
||||||
<button className="icon-button" onClick={onClose} style={{ border: 'none', background: 'transparent' }}>
|
<button className="icon-button" onClick={onClose} style={{ border: 'none', background: 'transparent' }}>
|
||||||
<CloseIcon width="20" height="20" />
|
<CloseIcon width="20" height="20" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!showPendingList && !showConfirm && currentPendingTrades.length > 0 && (
|
{!showConfirm && currentPendingTrades.length > 0 && (
|
||||||
<div
|
<div
|
||||||
className="trade-pending-alert"
|
className="trade-pending-alert"
|
||||||
onClick={() => setShowPendingList(true)}
|
onClick={() => setShowPendingList(true)}
|
||||||
@@ -192,49 +195,6 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showPendingList ? (
|
|
||||||
<div className="pending-list" style={{ maxHeight: '300px', overflowY: 'auto' }}>
|
|
||||||
<div className="pending-list-header trade-pending-header">
|
|
||||||
<button
|
|
||||||
className="button secondary"
|
|
||||||
onClick={() => setShowPendingList(false)}
|
|
||||||
style={{ padding: '4px 8px', fontSize: '12px' }}
|
|
||||||
>
|
|
||||||
< 返回
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="pending-list-items" style={{ paddingTop: 0 }}>
|
|
||||||
{currentPendingTrades.map((trade, idx) => (
|
|
||||||
<div key={trade.id || idx} className="trade-pending-item">
|
|
||||||
<div className="row" style={{ justifyContent: 'space-between', marginBottom: 4 }}>
|
|
||||||
<span style={{ fontWeight: 600, fontSize: '14px', color: trade.type === 'buy' ? 'var(--danger)' : 'var(--success)' }}>
|
|
||||||
{trade.type === 'buy' ? '买入' : '卖出'}
|
|
||||||
</span>
|
|
||||||
<span className="muted" style={{ fontSize: '12px' }}>{trade.date} {trade.isAfter3pm ? '(15:00后)' : ''}</span>
|
|
||||||
</div>
|
|
||||||
<div className="row" style={{ justifyContent: 'space-between', fontSize: '12px' }}>
|
|
||||||
<span className="muted">份额/金额</span>
|
|
||||||
<span>{trade.share ? `${trade.share} 份` : `¥${trade.amount}`}</span>
|
|
||||||
</div>
|
|
||||||
<div className="row" style={{ justifyContent: 'space-between', fontSize: '12px', marginTop: 4 }}>
|
|
||||||
<span className="muted">状态</span>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
||||||
<span className="trade-pending-status">等待净值更新...</span>
|
|
||||||
<button
|
|
||||||
className="button secondary trade-revoke-btn"
|
|
||||||
onClick={() => setRevokeTrade(trade)}
|
|
||||||
style={{ padding: '2px 8px', fontSize: '10px', height: 'auto' }}
|
|
||||||
>
|
|
||||||
撤销
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{!showConfirm && (
|
{!showConfirm && (
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<div className="fund-name" style={{ fontWeight: 600, fontSize: '16px', marginBottom: 4 }}>{fund?.name}</div>
|
<div className="fund-name" style={{ fontWeight: 600, fontSize: '16px', marginBottom: 4 }}>{fund?.name}</div>
|
||||||
@@ -316,10 +276,10 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="button"
|
className="button queue-button"
|
||||||
onClick={handleFinalConfirm}
|
onClick={handleFinalConfirm}
|
||||||
disabled={loadingPrice}
|
disabled={loadingPrice}
|
||||||
style={{ flex: 1, background: 'var(--primary)', opacity: loadingPrice ? 0.6 : 1, color: '#05263b' }}
|
style={{ flex: 1, background: 'var(--primary)', opacity: loadingPrice ? 0.6 : 1 }}
|
||||||
>
|
>
|
||||||
{loadingPrice ? '请稍候' : (price ? '确认买入' : '加入待处理队列')}
|
{loadingPrice ? '请稍候' : (price ? '确认买入' : '加入待处理队列')}
|
||||||
</button>
|
</button>
|
||||||
@@ -398,7 +358,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="button"
|
className="button queue-button"
|
||||||
onClick={handleFinalConfirm}
|
onClick={handleFinalConfirm}
|
||||||
disabled={loadingPrice}
|
disabled={loadingPrice}
|
||||||
style={{ flex: 1, background: 'var(--danger)', opacity: loadingPrice ? 0.6 : 1 }}
|
style={{ flex: 1, background: 'var(--danger)', opacity: loadingPrice ? 0.6 : 1 }}
|
||||||
@@ -612,9 +572,7 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
</>
|
</DialogContent>
|
||||||
)}
|
|
||||||
</motion.div>
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{revokeTrade && (
|
{revokeTrade && (
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
@@ -630,6 +588,12 @@ export default function TradeModal({ type, fund, holding, onClose, onConfirm, pe
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</motion.div>
|
<PendingTradesModal
|
||||||
|
open={showPendingList}
|
||||||
|
trades={currentPendingTrades}
|
||||||
|
onClose={() => setShowPendingList(false)}
|
||||||
|
onRevoke={(trade) => setRevokeTrade(trade)}
|
||||||
|
/>
|
||||||
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
import { CloseIcon } from './Icons';
|
import { CloseIcon } from './Icons';
|
||||||
import ConfirmModal from './ConfirmModal';
|
import ConfirmModal from './ConfirmModal';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
|
||||||
export default function TransactionHistoryModal({
|
export default function TransactionHistoryModal({
|
||||||
fund,
|
fund,
|
||||||
@@ -12,7 +18,7 @@ export default function TransactionHistoryModal({
|
|||||||
onClose,
|
onClose,
|
||||||
onDeleteTransaction,
|
onDeleteTransaction,
|
||||||
onDeletePending,
|
onDeletePending,
|
||||||
onAddHistory
|
onAddHistory,
|
||||||
}) {
|
}) {
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState(null); // { type: 'pending' | 'history', item }
|
const [deleteConfirm, setDeleteConfirm] = useState(null); // { type: 'pending' | 'history', item }
|
||||||
|
|
||||||
@@ -39,31 +45,46 @@ export default function TransactionHistoryModal({
|
|||||||
setDeleteConfirm(null);
|
setDeleteConfirm(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCloseClick = (event) => {
|
||||||
|
// 只关闭交易记录弹框,避免事件冒泡影响到其他弹框(例如 HoldingActionModal)
|
||||||
|
event.stopPropagation();
|
||||||
|
onClose?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenChange = (open) => {
|
||||||
|
if (!open) {
|
||||||
|
onClose?.();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<Dialog open onOpenChange={handleOpenChange}>
|
||||||
className="modal-overlay"
|
<DialogContent
|
||||||
role="dialog"
|
showCloseButton={false}
|
||||||
aria-modal="true"
|
|
||||||
onClick={onClose}
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
style={{ zIndex: 1100 }} // Higher than TradeModal if stacked, but usually TradeModal closes or this opens on top
|
|
||||||
>
|
|
||||||
<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 tx-history-modal"
|
className="glass card modal tx-history-modal"
|
||||||
onClick={(e) => e.stopPropagation()}
|
overlayClassName="modal-overlay"
|
||||||
style={{ maxWidth: '480px', maxHeight: '80vh', display: 'flex', flexDirection: 'column' }}
|
overlayStyle={{ zIndex: 998 }}
|
||||||
|
style={{
|
||||||
|
maxWidth: '480px',
|
||||||
|
width: '90vw',
|
||||||
|
maxHeight: '80vh',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
zIndex: 999, // 保持原有层级,确保在其他弹框之上
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<DialogTitle className="sr-only">交易记录</DialogTitle>
|
||||||
|
|
||||||
<div className="title" style={{ marginBottom: 20, justifyContent: 'space-between', flexShrink: 0 }}>
|
<div className="title" style={{ marginBottom: 20, justifyContent: 'space-between', flexShrink: 0 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
||||||
<span style={{ fontSize: '20px' }}>📜</span>
|
<span style={{ fontSize: '20px' }}>📜</span>
|
||||||
<span>交易记录</span>
|
<span>交易记录</span>
|
||||||
</div>
|
</div>
|
||||||
<button className="icon-button" onClick={onClose} style={{ border: 'none', background: 'transparent' }}>
|
<button
|
||||||
|
className="icon-button"
|
||||||
|
onClick={handleCloseClick}
|
||||||
|
style={{ border: 'none', background: 'transparent' }}
|
||||||
|
>
|
||||||
<CloseIcon width="20" height="20" />
|
<CloseIcon width="20" height="20" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,7 +97,7 @@ export default function TransactionHistoryModal({
|
|||||||
<button
|
<button
|
||||||
className="button primary"
|
className="button primary"
|
||||||
onClick={onAddHistory}
|
onClick={onAddHistory}
|
||||||
style={{ fontSize: '12px', padding: '4px 12px', height: 'auto' }}
|
style={{ fontSize: '12px', padding: '4px 12px', height: 'auto', width: '80px' }}
|
||||||
>
|
>
|
||||||
添加记录
|
添加记录
|
||||||
</button>
|
</button>
|
||||||
@@ -108,13 +129,16 @@ export default function TransactionHistoryModal({
|
|||||||
</div>
|
</div>
|
||||||
<div className="row" style={{ justifyContent: 'space-between', fontSize: '12px', marginTop: 8 }}>
|
<div className="row" style={{ justifyContent: 'space-between', fontSize: '12px', marginTop: 8 }}>
|
||||||
<span className="tx-history-pending-status">等待净值更新...</span>
|
<span className="tx-history-pending-status">等待净值更新...</span>
|
||||||
<button
|
<Button
|
||||||
className="button secondary tx-history-action-btn"
|
type="button"
|
||||||
|
size="xs"
|
||||||
|
variant="destructive"
|
||||||
|
className="bg-destructive text-white hover:bg-destructive/90"
|
||||||
onClick={() => handleDeleteClick(item, 'pending')}
|
onClick={() => handleDeleteClick(item, 'pending')}
|
||||||
style={{ padding: '2px 8px', fontSize: '10px', height: 'auto' }}
|
style={{ paddingInline: 10 }}
|
||||||
>
|
>
|
||||||
撤销
|
撤销
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -158,13 +182,16 @@ export default function TransactionHistoryModal({
|
|||||||
)}
|
)}
|
||||||
<div className="row" style={{ justifyContent: 'space-between', fontSize: '12px', marginTop: 8 }}>
|
<div className="row" style={{ justifyContent: 'space-between', fontSize: '12px', marginTop: 8 }}>
|
||||||
<span className="muted"></span>
|
<span className="muted"></span>
|
||||||
<button
|
<Button
|
||||||
className="button secondary tx-history-action-btn"
|
type="button"
|
||||||
|
size="xs"
|
||||||
|
variant="destructive"
|
||||||
|
className="bg-destructive text-white hover:bg-destructive/90"
|
||||||
onClick={() => handleDeleteClick(item, 'history')}
|
onClick={() => handleDeleteClick(item, 'history')}
|
||||||
style={{ padding: '2px 8px', fontSize: '10px', height: 'auto' }}
|
style={{ paddingInline: 10 }}
|
||||||
>
|
>
|
||||||
删除记录
|
删除记录
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
@@ -172,22 +199,21 @@ export default function TransactionHistoryModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</motion.div>
|
<AnimatePresence>
|
||||||
|
{deleteConfirm && (
|
||||||
<AnimatePresence>
|
<ConfirmModal
|
||||||
{deleteConfirm && (
|
key="delete-confirm"
|
||||||
<ConfirmModal
|
title={deleteConfirm.type === 'pending' ? '撤销交易' : '删除记录'}
|
||||||
key="delete-confirm"
|
message={deleteConfirm.type === 'pending'
|
||||||
title={deleteConfirm.type === 'pending' ? "撤销交易" : "删除记录"}
|
? '确定要撤销这笔待处理交易吗?'
|
||||||
message={deleteConfirm.type === 'pending'
|
: '确定要删除这条交易记录吗?\n注意:删除记录不会恢复已变更的持仓数据。'}
|
||||||
? "确定要撤销这笔待处理交易吗?"
|
onConfirm={handleConfirmDelete}
|
||||||
: "确定要删除这条交易记录吗?\n注意:删除记录不会恢复已变更的持仓数据。"}
|
onCancel={() => setDeleteConfirm(null)}
|
||||||
onConfirm={handleConfirmDelete}
|
confirmText="确认删除"
|
||||||
onCancel={() => setDeleteConfirm(null)}
|
/>
|
||||||
confirmText="确认删除"
|
)}
|
||||||
/>
|
</AnimatePresence>
|
||||||
)}
|
</DialogContent>
|
||||||
</AnimatePresence>
|
</Dialog>
|
||||||
</motion.div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2270,6 +2270,10 @@ input[type="number"] {
|
|||||||
color: var(--text) !important;
|
color: var(--text) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] .trade-modal .queue-button {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
.trade-time-slot {
|
.trade-time-slot {
|
||||||
background: rgba(0, 0, 0, 0.2);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const buttonVariants = cva(
|
|||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
default: "bg-[linear-gradient(#0ea5e9,#0891b2)] text-white hover:bg-[linear-gradient(#0284c7,#0e7490)]",
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
|
||||||
outline:
|
outline:
|
||||||
|
|||||||
@@ -50,11 +50,12 @@ function DialogContent({
|
|||||||
children,
|
children,
|
||||||
showCloseButton = true,
|
showCloseButton = true,
|
||||||
overlayClassName,
|
overlayClassName,
|
||||||
|
overlayStyle,
|
||||||
...props
|
...props
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<DialogPortal data-slot="dialog-portal">
|
<DialogPortal data-slot="dialog-portal">
|
||||||
<DialogOverlay className={overlayClassName} />
|
<DialogOverlay className={overlayClassName} style={overlayStyle} />
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
Reference in New Issue
Block a user