192 lines
5.6 KiB
TypeScript
192 lines
5.6 KiB
TypeScript
import { useState } from "react";
|
||
import {
|
||
Table, Button, Card, Typography, Modal, Form, Input, Select, InputNumber,
|
||
Switch, Popconfirm, message, Tag, Empty,
|
||
} from "antd";
|
||
import { PlusOutlined, DeleteOutlined } from "@ant-design/icons";
|
||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||
import { stockService } from "@/services/stocks";
|
||
import type { Alert } from "@/types";
|
||
|
||
const { Title } = Typography;
|
||
|
||
const ALERT_TYPES = [
|
||
{ value: "price_above", label: "价格高于" },
|
||
{ value: "price_below", label: "价格低于" },
|
||
{ value: "change_pct_above", label: "涨幅超过" },
|
||
{ value: "change_pct_below", label: "跌幅超过" },
|
||
];
|
||
|
||
export default function AlertsPage() {
|
||
const qc = useQueryClient();
|
||
const [modalOpen, setModalOpen] = useState(false);
|
||
const [form] = Form.useForm();
|
||
|
||
const { data: alerts = [], isLoading } = useQuery({
|
||
queryKey: ["alerts"],
|
||
queryFn: stockService.getAlerts,
|
||
});
|
||
|
||
const createMutation = useMutation({
|
||
mutationFn: stockService.createAlert,
|
||
onSuccess: () => {
|
||
qc.invalidateQueries({ queryKey: ["alerts"] });
|
||
message.success("预警已创建");
|
||
setModalOpen(false);
|
||
form.resetFields();
|
||
},
|
||
onError: () => message.error("创建失败"),
|
||
});
|
||
|
||
const deleteMutation = useMutation({
|
||
mutationFn: stockService.deleteAlert,
|
||
onSuccess: () => {
|
||
qc.invalidateQueries({ queryKey: ["alerts"] });
|
||
message.success("已删除");
|
||
},
|
||
});
|
||
|
||
const toggleMutation = useMutation({
|
||
mutationFn: stockService.toggleAlert,
|
||
onSuccess: () => qc.invalidateQueries({ queryKey: ["alerts"] }),
|
||
});
|
||
|
||
const columns = [
|
||
{
|
||
title: "股票",
|
||
key: "stock",
|
||
render: (_: unknown, record: Alert) => (
|
||
<span>
|
||
<span style={{ color: "#e6edf3", fontWeight: 600 }}>{record.name}</span>
|
||
<Tag style={{ marginLeft: 8, fontSize: 11 }} color="default">
|
||
{record.symbol}
|
||
</Tag>
|
||
</span>
|
||
),
|
||
},
|
||
{
|
||
title: "预警类型",
|
||
dataIndex: "alert_type",
|
||
key: "alert_type",
|
||
render: (v: string) => {
|
||
const t = ALERT_TYPES.find((t) => t.value === v);
|
||
return <Tag color="blue">{t?.label || v}</Tag>;
|
||
},
|
||
},
|
||
{
|
||
title: "阈值",
|
||
dataIndex: "threshold",
|
||
key: "threshold",
|
||
render: (v: number, record: Alert) => (
|
||
<span style={{ color: "#e6edf3", fontWeight: 600 }}>
|
||
{v}
|
||
{record.alert_type.includes("pct") ? "%" : "元"}
|
||
</span>
|
||
),
|
||
},
|
||
{
|
||
title: "状态",
|
||
key: "status",
|
||
render: (_: unknown, record: Alert) => {
|
||
if (record.triggered) return <Tag color="warning">已触发</Tag>;
|
||
return record.is_active ? <Tag color="success">监控中</Tag> : <Tag>已暂停</Tag>;
|
||
},
|
||
},
|
||
{
|
||
title: "启用",
|
||
key: "active",
|
||
render: (_: unknown, record: Alert) => (
|
||
<Switch
|
||
checked={record.is_active}
|
||
size="small"
|
||
onChange={() => toggleMutation.mutate(record.id)}
|
||
disabled={record.triggered}
|
||
/>
|
||
),
|
||
},
|
||
{
|
||
title: "操作",
|
||
key: "action",
|
||
render: (_: unknown, record: Alert) => (
|
||
<Popconfirm
|
||
title="确认删除该预警?"
|
||
onConfirm={() => deleteMutation.mutate(record.id)}
|
||
okText="删除"
|
||
cancelText="取消"
|
||
>
|
||
<Button
|
||
size="small"
|
||
type="text"
|
||
icon={<DeleteOutlined />}
|
||
danger
|
||
/>
|
||
</Popconfirm>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div>
|
||
<div style={{ marginBottom: 16, display: "flex", alignItems: "center", gap: 12 }}>
|
||
<Title level={4} style={{ margin: 0, color: "#e6edf3" }}>
|
||
价格预警
|
||
</Title>
|
||
<Button
|
||
type="primary"
|
||
icon={<PlusOutlined />}
|
||
onClick={() => setModalOpen(true)}
|
||
>
|
||
新建预警
|
||
</Button>
|
||
</div>
|
||
|
||
<Card
|
||
style={{ background: "#161b22", border: "1px solid #30363d" }}
|
||
bodyStyle={{ padding: 0 }}
|
||
>
|
||
<Table
|
||
rowKey="id"
|
||
columns={columns}
|
||
dataSource={alerts}
|
||
loading={isLoading}
|
||
pagination={{ pageSize: 20 }}
|
||
locale={{ emptyText: <Empty description="暂无预警" /> }}
|
||
/>
|
||
</Card>
|
||
|
||
<Modal
|
||
title="新建价格预警"
|
||
open={modalOpen}
|
||
onCancel={() => setModalOpen(false)}
|
||
onOk={() => form.submit()}
|
||
confirmLoading={createMutation.isPending}
|
||
okText="创建"
|
||
cancelText="取消"
|
||
styles={{ content: { background: "#161b22" }, header: { background: "#161b22" } }}
|
||
>
|
||
<Form
|
||
form={form}
|
||
layout="vertical"
|
||
onFinish={(values) => createMutation.mutate(values)}
|
||
>
|
||
<Form.Item name="symbol" label="股票代码" rules={[{ required: true }]}>
|
||
<Input placeholder="如:600519" />
|
||
</Form.Item>
|
||
<Form.Item name="name" label="股票名称" rules={[{ required: true }]}>
|
||
<Input placeholder="如:贵州茅台" />
|
||
</Form.Item>
|
||
<Form.Item name="alert_type" label="预警类型" rules={[{ required: true }]}>
|
||
<Select options={ALERT_TYPES} placeholder="选择预警类型" />
|
||
</Form.Item>
|
||
<Form.Item name="threshold" label="阈值" rules={[{ required: true }]}>
|
||
<InputNumber
|
||
style={{ width: "100%" }}
|
||
placeholder="输入阈值(价格填元,涨跌幅填%数字)"
|
||
/>
|
||
</Form.Item>
|
||
</Form>
|
||
</Modal>
|
||
</div>
|
||
);
|
||
}
|