Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eddddc56ae | ||
|
|
4d02439ecb | ||
|
|
547181d333 | ||
|
|
ec15f40323 | ||
|
|
b10b1c44db | ||
|
|
4da9489662 | ||
|
|
7fcd0c9569 | ||
|
|
7f72bf809b |
@@ -31,3 +31,7 @@ lerna-debug.log*
|
|||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
dist/*
|
dist/*
|
||||||
|
|
||||||
|
# local env (contains API keys)
|
||||||
|
/.env
|
||||||
|
.reasonix/*
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react'
|
|
||||||
|
|
||||||
const TimeRangePicker = ({
|
|
||||||
value = '1m',
|
|
||||||
onChange,
|
|
||||||
className = ''
|
|
||||||
}) => {
|
|
||||||
const [timeRange, setTimeRange] = useState(value)
|
|
||||||
const [startDate, setStartDate] = useState('')
|
|
||||||
const [endDate, setEndDate] = useState('')
|
|
||||||
const [showDropdown, setShowDropdown] = useState(false)
|
|
||||||
|
|
||||||
// 计算日期范围
|
|
||||||
const calculateDateRange = (range) => {
|
|
||||||
const end = new Date()
|
|
||||||
const start = new Date()
|
|
||||||
|
|
||||||
if (range === '1m') {
|
|
||||||
start.setMonth(start.getMonth() - 1)
|
|
||||||
} else if (range === '3m') {
|
|
||||||
start.setMonth(start.getMonth() - 3)
|
|
||||||
} else if (range === '1y') {
|
|
||||||
start.setFullYear(start.getFullYear() - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = (date) => {
|
|
||||||
return date.toISOString().split('T')[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
start: formatDate(start),
|
|
||||||
end: formatDate(end)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化时设置日期范围
|
|
||||||
useEffect(() => {
|
|
||||||
const range = calculateDateRange(value)
|
|
||||||
setStartDate(range.start)
|
|
||||||
setEndDate(range.end)
|
|
||||||
}, [value])
|
|
||||||
|
|
||||||
// 处理时间范围变化
|
|
||||||
const handleTimeRangeChange = (range) => {
|
|
||||||
const dateRange = calculateDateRange(range)
|
|
||||||
setStartDate(dateRange.start)
|
|
||||||
setEndDate(dateRange.end)
|
|
||||||
setTimeRange(range)
|
|
||||||
if (onChange) {
|
|
||||||
onChange(range, dateRange.start, dateRange.end)
|
|
||||||
}
|
|
||||||
setShowDropdown(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理自定义日期范围
|
|
||||||
const handleCustomRangeApply = () => {
|
|
||||||
if (startDate && endDate) {
|
|
||||||
setTimeRange('custom')
|
|
||||||
if (onChange) {
|
|
||||||
onChange('custom', startDate, endDate)
|
|
||||||
}
|
|
||||||
setShowDropdown(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理开始日期变化
|
|
||||||
const handleStartDateChange = (e) => {
|
|
||||||
setStartDate(e.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理结束日期变化
|
|
||||||
const handleEndDateChange = (e) => {
|
|
||||||
setEndDate(e.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`time-range-picker ${className}`}>
|
|
||||||
<button
|
|
||||||
className="time-range-btn"
|
|
||||||
onClick={() => setShowDropdown(!showDropdown)}
|
|
||||||
>
|
|
||||||
{timeRange === '1m' && '近一个月'}
|
|
||||||
{timeRange === '3m' && '近三个月'}
|
|
||||||
{timeRange === '1y' && '近一年'}
|
|
||||||
{timeRange === 'custom' && startDate && endDate && `${startDate} 至 ${endDate}`}
|
|
||||||
{timeRange === 'custom' && (!startDate || !endDate) && '自定义'}
|
|
||||||
<span className="dropdown-arrow">▼</span>
|
|
||||||
</button>
|
|
||||||
{showDropdown && (
|
|
||||||
<div className="time-range-dropdown">
|
|
||||||
<div className="dropdown-section">
|
|
||||||
<h4 className="dropdown-title">自定义范围</h4>
|
|
||||||
<div className="custom-date-range">
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={startDate}
|
|
||||||
onChange={handleStartDateChange}
|
|
||||||
max={endDate || new Date().toISOString().split('T')[0]}
|
|
||||||
/>
|
|
||||||
<span>至</span>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={endDate}
|
|
||||||
onChange={handleEndDateChange}
|
|
||||||
min={startDate}
|
|
||||||
max={new Date().toISOString().split('T')[0]}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className="apply-btn"
|
|
||||||
onClick={handleCustomRangeApply}
|
|
||||||
>
|
|
||||||
确定
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="dropdown-section">
|
|
||||||
<h4 className="dropdown-title">快捷选项</h4>
|
|
||||||
<ul className="dropdown-options">
|
|
||||||
<li
|
|
||||||
className={`dropdown-option ${timeRange === '1m' ? 'active' : ''}`}
|
|
||||||
onClick={() => handleTimeRangeChange('1m')}
|
|
||||||
>
|
|
||||||
近一个月
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
className={`dropdown-option ${timeRange === '3m' ? 'active' : ''}`}
|
|
||||||
onClick={() => handleTimeRangeChange('3m')}
|
|
||||||
>
|
|
||||||
近三个月
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
className={`dropdown-option ${timeRange === '1y' ? 'active' : ''}`}
|
|
||||||
onClick={() => handleTimeRangeChange('1y')}
|
|
||||||
>
|
|
||||||
近一年
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TimeRangePicker
|
|
||||||
@@ -37,6 +37,36 @@ export type Strategy = Prisma.StrategyModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Indicator = Prisma.IndicatorModel
|
export type Indicator = Prisma.IndicatorModel
|
||||||
|
/**
|
||||||
|
* Model IndicatorPoint
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type IndicatorPoint = Prisma.IndicatorPointModel
|
||||||
|
/**
|
||||||
|
* Model News
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type News = Prisma.NewsModel
|
||||||
|
/**
|
||||||
|
* Model Alert
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Alert = Prisma.AlertModel
|
||||||
|
/**
|
||||||
|
* Model ServerNode
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type ServerNode = Prisma.ServerNodeModel
|
||||||
|
/**
|
||||||
|
* Model ServerNodeHistory
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type ServerNodeHistory = Prisma.ServerNodeHistoryModel
|
||||||
|
/**
|
||||||
|
* Model ServiceCheck
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type ServiceCheck = Prisma.ServiceCheckModel
|
||||||
/**
|
/**
|
||||||
* Model Econ_SowInventory
|
* Model Econ_SowInventory
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -61,6 +61,36 @@ export type Strategy = Prisma.StrategyModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Indicator = Prisma.IndicatorModel
|
export type Indicator = Prisma.IndicatorModel
|
||||||
|
/**
|
||||||
|
* Model IndicatorPoint
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type IndicatorPoint = Prisma.IndicatorPointModel
|
||||||
|
/**
|
||||||
|
* Model News
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type News = Prisma.NewsModel
|
||||||
|
/**
|
||||||
|
* Model Alert
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type Alert = Prisma.AlertModel
|
||||||
|
/**
|
||||||
|
* Model ServerNode
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type ServerNode = Prisma.ServerNodeModel
|
||||||
|
/**
|
||||||
|
* Model ServerNodeHistory
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type ServerNodeHistory = Prisma.ServerNodeHistoryModel
|
||||||
|
/**
|
||||||
|
* Model ServiceCheck
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type ServiceCheck = Prisma.ServiceCheckModel
|
||||||
/**
|
/**
|
||||||
* Model Econ_SowInventory
|
* Model Econ_SowInventory
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -150,6 +150,46 @@ export type JsonWithAggregatesFilterBase<$PrismaModel = never> = {
|
|||||||
_max?: Prisma.NestedJsonFilter<$PrismaModel>
|
_max?: Prisma.NestedJsonFilter<$PrismaModel>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type BoolFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FloatFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
in?: number[]
|
||||||
|
notIn?: number[]
|
||||||
|
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FloatWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
in?: number[]
|
||||||
|
notIn?: number[]
|
||||||
|
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedFloatWithAggregatesFilter<$PrismaModel> | number
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
|
_sum?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type StringNullableFilter<$PrismaModel = never> = {
|
export type StringNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
in?: string[] | null
|
in?: string[] | null
|
||||||
@@ -165,6 +205,56 @@ export type StringNullableFilter<$PrismaModel = never> = {
|
|||||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SortOrderInput = {
|
||||||
|
sort: Prisma.SortOrder
|
||||||
|
nulls?: Prisma.NullsOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
|
in?: string[] | null
|
||||||
|
notIn?: string[] | null
|
||||||
|
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||||
|
search?: string
|
||||||
|
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||||
|
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BigIntFilter<$PrismaModel = never> = {
|
||||||
|
equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
in?: bigint[] | number[]
|
||||||
|
notIn?: bigint[] | number[]
|
||||||
|
lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBigIntFilter<$PrismaModel> | bigint | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BigIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
in?: bigint[] | number[]
|
||||||
|
notIn?: bigint[] | number[]
|
||||||
|
lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBigIntWithAggregatesFilter<$PrismaModel> | bigint | number
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
|
_sum?: Prisma.NestedBigIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBigIntFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBigIntFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type JsonNullableFilter<$PrismaModel = never> =
|
export type JsonNullableFilter<$PrismaModel = never> =
|
||||||
| Prisma.PatchUndefined<
|
| Prisma.PatchUndefined<
|
||||||
Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>,
|
Prisma.Either<Required<JsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableFilterBase<$PrismaModel>>, 'path'>>,
|
||||||
@@ -189,29 +279,6 @@ export type JsonNullableFilterBase<$PrismaModel = never> = {
|
|||||||
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SortOrderInput = {
|
|
||||||
sort: Prisma.SortOrder
|
|
||||||
nulls?: Prisma.NullsOrder
|
|
||||||
}
|
|
||||||
|
|
||||||
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
|
||||||
in?: string[] | null
|
|
||||||
notIn?: string[] | null
|
|
||||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
|
||||||
search?: string
|
|
||||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
|
||||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
|
||||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
|
||||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
|
||||||
}
|
|
||||||
|
|
||||||
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
|
export type JsonNullableWithAggregatesFilter<$PrismaModel = never> =
|
||||||
| Prisma.PatchUndefined<
|
| Prisma.PatchUndefined<
|
||||||
Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
|
Prisma.Either<Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, Exclude<keyof Required<JsonNullableWithAggregatesFilterBase<$PrismaModel>>, 'path'>>,
|
||||||
@@ -359,6 +426,35 @@ export type NestedJsonFilterBase<$PrismaModel = never> = {
|
|||||||
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
not?: runtime.InputJsonValue | Prisma.JsonFieldRefInput<$PrismaModel> | Prisma.JsonNullValueFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NestedBoolFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBoolFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedFloatWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
in?: number[]
|
||||||
|
notIn?: number[]
|
||||||
|
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedFloatWithAggregatesFilter<$PrismaModel> | number
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
|
_sum?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
||||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||||
in?: string[] | null
|
in?: string[] | null
|
||||||
@@ -403,6 +499,33 @@ export type NestedIntNullableFilter<$PrismaModel = never> = {
|
|||||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NestedBigIntFilter<$PrismaModel = never> = {
|
||||||
|
equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
in?: bigint[] | number[]
|
||||||
|
notIn?: bigint[] | number[]
|
||||||
|
lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBigIntFilter<$PrismaModel> | bigint | number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NestedBigIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||||
|
equals?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
in?: bigint[] | number[]
|
||||||
|
notIn?: bigint[] | number[]
|
||||||
|
lt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
lte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
gt?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
gte?: bigint | number | Prisma.BigIntFieldRefInput<$PrismaModel>
|
||||||
|
not?: Prisma.NestedBigIntWithAggregatesFilter<$PrismaModel> | bigint | number
|
||||||
|
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||||
|
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||||
|
_sum?: Prisma.NestedBigIntFilter<$PrismaModel>
|
||||||
|
_min?: Prisma.NestedBigIntFilter<$PrismaModel>
|
||||||
|
_max?: Prisma.NestedBigIntFilter<$PrismaModel>
|
||||||
|
}
|
||||||
|
|
||||||
export type NestedJsonNullableFilter<$PrismaModel = never> =
|
export type NestedJsonNullableFilter<$PrismaModel = never> =
|
||||||
| Prisma.PatchUndefined<
|
| Prisma.PatchUndefined<
|
||||||
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,
|
Prisma.Either<Required<NestedJsonNullableFilterBase<$PrismaModel>>, Exclude<keyof Required<NestedJsonNullableFilterBase<$PrismaModel>>, 'path'>>,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -388,6 +388,12 @@ export const ModelName = {
|
|||||||
Article: 'Article',
|
Article: 'Article',
|
||||||
Strategy: 'Strategy',
|
Strategy: 'Strategy',
|
||||||
Indicator: 'Indicator',
|
Indicator: 'Indicator',
|
||||||
|
IndicatorPoint: 'IndicatorPoint',
|
||||||
|
News: 'News',
|
||||||
|
Alert: 'Alert',
|
||||||
|
ServerNode: 'ServerNode',
|
||||||
|
ServerNodeHistory: 'ServerNodeHistory',
|
||||||
|
ServiceCheck: 'ServiceCheck',
|
||||||
Econ_SowInventory: 'Econ_SowInventory',
|
Econ_SowInventory: 'Econ_SowInventory',
|
||||||
ScraperCache: 'ScraperCache'
|
ScraperCache: 'ScraperCache'
|
||||||
} as const
|
} as const
|
||||||
@@ -405,7 +411,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
omit: GlobalOmitOptions
|
omit: GlobalOmitOptions
|
||||||
}
|
}
|
||||||
meta: {
|
meta: {
|
||||||
modelProps: "user" | "article" | "strategy" | "indicator" | "econ_SowInventory" | "scraperCache"
|
modelProps: "user" | "article" | "strategy" | "indicator" | "indicatorPoint" | "news" | "alert" | "serverNode" | "serverNodeHistory" | "serviceCheck" | "econ_SowInventory" | "scraperCache"
|
||||||
txIsolationLevel: TransactionIsolationLevel
|
txIsolationLevel: TransactionIsolationLevel
|
||||||
}
|
}
|
||||||
model: {
|
model: {
|
||||||
@@ -673,6 +679,402 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
IndicatorPoint: {
|
||||||
|
payload: Prisma.$IndicatorPointPayload<ExtArgs>
|
||||||
|
fields: Prisma.IndicatorPointFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.IndicatorPointFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$IndicatorPointPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.IndicatorPointFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$IndicatorPointPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.IndicatorPointFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$IndicatorPointPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.IndicatorPointFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$IndicatorPointPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.IndicatorPointFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$IndicatorPointPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.IndicatorPointCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$IndicatorPointPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.IndicatorPointCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.IndicatorPointDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$IndicatorPointPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.IndicatorPointUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$IndicatorPointPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.IndicatorPointDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.IndicatorPointUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.IndicatorPointUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$IndicatorPointPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.IndicatorPointAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateIndicatorPoint>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.IndicatorPointGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.IndicatorPointGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.IndicatorPointCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.IndicatorPointCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
News: {
|
||||||
|
payload: Prisma.$NewsPayload<ExtArgs>
|
||||||
|
fields: Prisma.NewsFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.NewsFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$NewsPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.NewsFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$NewsPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.NewsFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$NewsPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.NewsFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$NewsPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.NewsFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$NewsPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.NewsCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$NewsPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.NewsCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.NewsDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$NewsPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.NewsUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$NewsPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.NewsDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.NewsUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.NewsUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$NewsPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.NewsAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateNews>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.NewsGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.NewsGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.NewsCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.NewsCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Alert: {
|
||||||
|
payload: Prisma.$AlertPayload<ExtArgs>
|
||||||
|
fields: Prisma.AlertFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.AlertFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$AlertPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.AlertFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$AlertPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.AlertFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$AlertPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.AlertFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$AlertPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.AlertFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$AlertPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.AlertCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$AlertPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.AlertCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.AlertDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$AlertPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.AlertUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$AlertPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.AlertDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.AlertUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.AlertUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$AlertPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.AlertAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateAlert>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.AlertGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AlertGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.AlertCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AlertCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServerNode: {
|
||||||
|
payload: Prisma.$ServerNodePayload<ExtArgs>
|
||||||
|
fields: Prisma.ServerNodeFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.ServerNodeFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.ServerNodeFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.ServerNodeFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.ServerNodeFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.ServerNodeFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.ServerNodeCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.ServerNodeCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.ServerNodeDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.ServerNodeUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.ServerNodeDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.ServerNodeUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.ServerNodeUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodePayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.ServerNodeAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateServerNode>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.ServerNodeGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.ServerNodeGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.ServerNodeCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.ServerNodeCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServerNodeHistory: {
|
||||||
|
payload: Prisma.$ServerNodeHistoryPayload<ExtArgs>
|
||||||
|
fields: Prisma.ServerNodeHistoryFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.ServerNodeHistoryFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.ServerNodeHistoryFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.ServerNodeHistoryFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.ServerNodeHistoryFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.ServerNodeHistoryFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.ServerNodeHistoryCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.ServerNodeHistoryCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.ServerNodeHistoryDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.ServerNodeHistoryUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.ServerNodeHistoryDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.ServerNodeHistoryUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.ServerNodeHistoryUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServerNodeHistoryPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.ServerNodeHistoryAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateServerNodeHistory>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.ServerNodeHistoryGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.ServerNodeHistoryGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.ServerNodeHistoryCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.ServerNodeHistoryCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ServiceCheck: {
|
||||||
|
payload: Prisma.$ServiceCheckPayload<ExtArgs>
|
||||||
|
fields: Prisma.ServiceCheckFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.ServiceCheckFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.ServiceCheckFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.ServiceCheckFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.ServiceCheckFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.ServiceCheckFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.ServiceCheckCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.ServiceCheckCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.ServiceCheckDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.ServiceCheckUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.ServiceCheckDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.ServiceCheckUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.ServiceCheckUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$ServiceCheckPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.ServiceCheckAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateServiceCheck>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.ServiceCheckGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.ServiceCheckGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.ServiceCheckCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.ServiceCheckCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Econ_SowInventory: {
|
Econ_SowInventory: {
|
||||||
payload: Prisma.$Econ_SowInventoryPayload<ExtArgs>
|
payload: Prisma.$Econ_SowInventoryPayload<ExtArgs>
|
||||||
fields: Prisma.Econ_SowInventoryFieldRefs
|
fields: Prisma.Econ_SowInventoryFieldRefs
|
||||||
@@ -885,7 +1287,11 @@ export const IndicatorScalarFieldEnum = {
|
|||||||
id: 'id',
|
id: 'id',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
groupName: 'groupName',
|
groupName: 'groupName',
|
||||||
data: 'data',
|
title: 'title',
|
||||||
|
unit: 'unit',
|
||||||
|
source: 'source',
|
||||||
|
frequency: 'frequency',
|
||||||
|
hidden: 'hidden',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
} as const
|
} as const
|
||||||
@@ -893,6 +1299,113 @@ export const IndicatorScalarFieldEnum = {
|
|||||||
export type IndicatorScalarFieldEnum = (typeof IndicatorScalarFieldEnum)[keyof typeof IndicatorScalarFieldEnum]
|
export type IndicatorScalarFieldEnum = (typeof IndicatorScalarFieldEnum)[keyof typeof IndicatorScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const IndicatorPointScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
indicatorId: 'indicatorId',
|
||||||
|
period: 'period',
|
||||||
|
value: 'value',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type IndicatorPointScalarFieldEnum = (typeof IndicatorPointScalarFieldEnum)[keyof typeof IndicatorPointScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const NewsScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
title: 'title',
|
||||||
|
summary: 'summary',
|
||||||
|
content: 'content',
|
||||||
|
source: 'source',
|
||||||
|
url: 'url',
|
||||||
|
publishedAt: 'publishedAt',
|
||||||
|
impactRank: 'impactRank',
|
||||||
|
createdAt: 'createdAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type NewsScalarFieldEnum = (typeof NewsScalarFieldEnum)[keyof typeof NewsScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const AlertScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
indicatorName: 'indicatorName',
|
||||||
|
period: 'period',
|
||||||
|
valueA: 'valueA',
|
||||||
|
valueB: 'valueB',
|
||||||
|
sourceA: 'sourceA',
|
||||||
|
sourceB: 'sourceB',
|
||||||
|
diff: 'diff',
|
||||||
|
diffRatio: 'diffRatio',
|
||||||
|
createdAt: 'createdAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type AlertScalarFieldEnum = (typeof AlertScalarFieldEnum)[keyof typeof AlertScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ServerNodeScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
uuid: 'uuid',
|
||||||
|
name: 'name',
|
||||||
|
cpuName: 'cpuName',
|
||||||
|
os: 'os',
|
||||||
|
region: 'region',
|
||||||
|
memTotal: 'memTotal',
|
||||||
|
diskTotal: 'diskTotal',
|
||||||
|
cpuCores: 'cpuCores',
|
||||||
|
cpuUsage: 'cpuUsage',
|
||||||
|
ramUsed: 'ramUsed',
|
||||||
|
swapUsed: 'swapUsed',
|
||||||
|
load1: 'load1',
|
||||||
|
load5: 'load5',
|
||||||
|
load15: 'load15',
|
||||||
|
diskUsed: 'diskUsed',
|
||||||
|
netIn: 'netIn',
|
||||||
|
netOut: 'netOut',
|
||||||
|
uptime: 'uptime',
|
||||||
|
process: 'process',
|
||||||
|
connections: 'connections',
|
||||||
|
status: 'status',
|
||||||
|
recordedAt: 'recordedAt',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ServerNodeScalarFieldEnum = (typeof ServerNodeScalarFieldEnum)[keyof typeof ServerNodeScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ServerNodeHistoryScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
uuid: 'uuid',
|
||||||
|
period: 'period',
|
||||||
|
cpu: 'cpu',
|
||||||
|
ram: 'ram',
|
||||||
|
load: 'load',
|
||||||
|
disk: 'disk',
|
||||||
|
netIn: 'netIn',
|
||||||
|
netOut: 'netOut',
|
||||||
|
process: 'process',
|
||||||
|
connections: 'connections',
|
||||||
|
createdAt: 'createdAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ServerNodeHistoryScalarFieldEnum = (typeof ServerNodeHistoryScalarFieldEnum)[keyof typeof ServerNodeHistoryScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ServiceCheckScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
name: 'name',
|
||||||
|
url: 'url',
|
||||||
|
status: 'status',
|
||||||
|
latencyMs: 'latencyMs',
|
||||||
|
message: 'message',
|
||||||
|
lastCheckAt: 'lastCheckAt',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ServiceCheckScalarFieldEnum = (typeof ServiceCheckScalarFieldEnum)[keyof typeof ServiceCheckScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const Econ_SowInventoryScalarFieldEnum = {
|
export const Econ_SowInventoryScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
month: 'month',
|
month: 'month',
|
||||||
@@ -986,17 +1499,21 @@ export type StrategyOrderByRelevanceFieldEnum = (typeof StrategyOrderByRelevance
|
|||||||
|
|
||||||
export const IndicatorOrderByRelevanceFieldEnum = {
|
export const IndicatorOrderByRelevanceFieldEnum = {
|
||||||
name: 'name',
|
name: 'name',
|
||||||
groupName: 'groupName'
|
groupName: 'groupName',
|
||||||
|
title: 'title',
|
||||||
|
unit: 'unit',
|
||||||
|
source: 'source',
|
||||||
|
frequency: 'frequency'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type IndicatorOrderByRelevanceFieldEnum = (typeof IndicatorOrderByRelevanceFieldEnum)[keyof typeof IndicatorOrderByRelevanceFieldEnum]
|
export type IndicatorOrderByRelevanceFieldEnum = (typeof IndicatorOrderByRelevanceFieldEnum)[keyof typeof IndicatorOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const Econ_SowInventoryOrderByRelevanceFieldEnum = {
|
export const IndicatorPointOrderByRelevanceFieldEnum = {
|
||||||
month: 'month'
|
period: 'period'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type Econ_SowInventoryOrderByRelevanceFieldEnum = (typeof Econ_SowInventoryOrderByRelevanceFieldEnum)[keyof typeof Econ_SowInventoryOrderByRelevanceFieldEnum]
|
export type IndicatorPointOrderByRelevanceFieldEnum = (typeof IndicatorPointOrderByRelevanceFieldEnum)[keyof typeof IndicatorPointOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const NullsOrder = {
|
export const NullsOrder = {
|
||||||
@@ -1007,6 +1524,63 @@ export const NullsOrder = {
|
|||||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||||
|
|
||||||
|
|
||||||
|
export const NewsOrderByRelevanceFieldEnum = {
|
||||||
|
title: 'title',
|
||||||
|
summary: 'summary',
|
||||||
|
content: 'content',
|
||||||
|
source: 'source',
|
||||||
|
url: 'url'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type NewsOrderByRelevanceFieldEnum = (typeof NewsOrderByRelevanceFieldEnum)[keyof typeof NewsOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const AlertOrderByRelevanceFieldEnum = {
|
||||||
|
indicatorName: 'indicatorName',
|
||||||
|
period: 'period',
|
||||||
|
sourceA: 'sourceA',
|
||||||
|
sourceB: 'sourceB'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type AlertOrderByRelevanceFieldEnum = (typeof AlertOrderByRelevanceFieldEnum)[keyof typeof AlertOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ServerNodeOrderByRelevanceFieldEnum = {
|
||||||
|
uuid: 'uuid',
|
||||||
|
name: 'name',
|
||||||
|
cpuName: 'cpuName',
|
||||||
|
os: 'os',
|
||||||
|
region: 'region',
|
||||||
|
status: 'status'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ServerNodeOrderByRelevanceFieldEnum = (typeof ServerNodeOrderByRelevanceFieldEnum)[keyof typeof ServerNodeOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ServerNodeHistoryOrderByRelevanceFieldEnum = {
|
||||||
|
uuid: 'uuid'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ServerNodeHistoryOrderByRelevanceFieldEnum = (typeof ServerNodeHistoryOrderByRelevanceFieldEnum)[keyof typeof ServerNodeHistoryOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ServiceCheckOrderByRelevanceFieldEnum = {
|
||||||
|
name: 'name',
|
||||||
|
url: 'url',
|
||||||
|
status: 'status',
|
||||||
|
message: 'message'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ServiceCheckOrderByRelevanceFieldEnum = (typeof ServiceCheckOrderByRelevanceFieldEnum)[keyof typeof ServiceCheckOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const Econ_SowInventoryOrderByRelevanceFieldEnum = {
|
||||||
|
month: 'month'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type Econ_SowInventoryOrderByRelevanceFieldEnum = (typeof Econ_SowInventoryOrderByRelevanceFieldEnum)[keyof typeof Econ_SowInventoryOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const ScraperCacheOrderByRelevanceFieldEnum = {
|
export const ScraperCacheOrderByRelevanceFieldEnum = {
|
||||||
scraperName: 'scraperName',
|
scraperName: 'scraperName',
|
||||||
lastDataHash: 'lastDataHash'
|
lastDataHash: 'lastDataHash'
|
||||||
@@ -1056,12 +1630,26 @@ export type EnumQueryModeFieldRefInput<$PrismaModel> = FieldRefInputType<$Prisma
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'Boolean'
|
||||||
|
*/
|
||||||
|
export type BooleanFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Boolean'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reference to a field of type 'Float'
|
* Reference to a field of type 'Float'
|
||||||
*/
|
*/
|
||||||
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
|
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reference to a field of type 'BigInt'
|
||||||
|
*/
|
||||||
|
export type BigIntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'BigInt'>
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Batch Payload for updateMany & deleteMany & createMany
|
* Batch Payload for updateMany & deleteMany & createMany
|
||||||
*/
|
*/
|
||||||
@@ -1161,6 +1749,12 @@ export type GlobalOmitConfig = {
|
|||||||
article?: Prisma.ArticleOmit
|
article?: Prisma.ArticleOmit
|
||||||
strategy?: Prisma.StrategyOmit
|
strategy?: Prisma.StrategyOmit
|
||||||
indicator?: Prisma.IndicatorOmit
|
indicator?: Prisma.IndicatorOmit
|
||||||
|
indicatorPoint?: Prisma.IndicatorPointOmit
|
||||||
|
news?: Prisma.NewsOmit
|
||||||
|
alert?: Prisma.AlertOmit
|
||||||
|
serverNode?: Prisma.ServerNodeOmit
|
||||||
|
serverNodeHistory?: Prisma.ServerNodeHistoryOmit
|
||||||
|
serviceCheck?: Prisma.ServiceCheckOmit
|
||||||
econ_SowInventory?: Prisma.Econ_SowInventoryOmit
|
econ_SowInventory?: Prisma.Econ_SowInventoryOmit
|
||||||
scraperCache?: Prisma.ScraperCacheOmit
|
scraperCache?: Prisma.ScraperCacheOmit
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ export const ModelName = {
|
|||||||
Article: 'Article',
|
Article: 'Article',
|
||||||
Strategy: 'Strategy',
|
Strategy: 'Strategy',
|
||||||
Indicator: 'Indicator',
|
Indicator: 'Indicator',
|
||||||
|
IndicatorPoint: 'IndicatorPoint',
|
||||||
|
News: 'News',
|
||||||
|
Alert: 'Alert',
|
||||||
|
ServerNode: 'ServerNode',
|
||||||
|
ServerNodeHistory: 'ServerNodeHistory',
|
||||||
|
ServiceCheck: 'ServiceCheck',
|
||||||
Econ_SowInventory: 'Econ_SowInventory',
|
Econ_SowInventory: 'Econ_SowInventory',
|
||||||
ScraperCache: 'ScraperCache'
|
ScraperCache: 'ScraperCache'
|
||||||
} as const
|
} as const
|
||||||
@@ -116,7 +122,11 @@ export const IndicatorScalarFieldEnum = {
|
|||||||
id: 'id',
|
id: 'id',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
groupName: 'groupName',
|
groupName: 'groupName',
|
||||||
data: 'data',
|
title: 'title',
|
||||||
|
unit: 'unit',
|
||||||
|
source: 'source',
|
||||||
|
frequency: 'frequency',
|
||||||
|
hidden: 'hidden',
|
||||||
createdAt: 'createdAt',
|
createdAt: 'createdAt',
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
} as const
|
} as const
|
||||||
@@ -124,6 +134,113 @@ export const IndicatorScalarFieldEnum = {
|
|||||||
export type IndicatorScalarFieldEnum = (typeof IndicatorScalarFieldEnum)[keyof typeof IndicatorScalarFieldEnum]
|
export type IndicatorScalarFieldEnum = (typeof IndicatorScalarFieldEnum)[keyof typeof IndicatorScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const IndicatorPointScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
indicatorId: 'indicatorId',
|
||||||
|
period: 'period',
|
||||||
|
value: 'value',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type IndicatorPointScalarFieldEnum = (typeof IndicatorPointScalarFieldEnum)[keyof typeof IndicatorPointScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const NewsScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
title: 'title',
|
||||||
|
summary: 'summary',
|
||||||
|
content: 'content',
|
||||||
|
source: 'source',
|
||||||
|
url: 'url',
|
||||||
|
publishedAt: 'publishedAt',
|
||||||
|
impactRank: 'impactRank',
|
||||||
|
createdAt: 'createdAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type NewsScalarFieldEnum = (typeof NewsScalarFieldEnum)[keyof typeof NewsScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const AlertScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
indicatorName: 'indicatorName',
|
||||||
|
period: 'period',
|
||||||
|
valueA: 'valueA',
|
||||||
|
valueB: 'valueB',
|
||||||
|
sourceA: 'sourceA',
|
||||||
|
sourceB: 'sourceB',
|
||||||
|
diff: 'diff',
|
||||||
|
diffRatio: 'diffRatio',
|
||||||
|
createdAt: 'createdAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type AlertScalarFieldEnum = (typeof AlertScalarFieldEnum)[keyof typeof AlertScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ServerNodeScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
uuid: 'uuid',
|
||||||
|
name: 'name',
|
||||||
|
cpuName: 'cpuName',
|
||||||
|
os: 'os',
|
||||||
|
region: 'region',
|
||||||
|
memTotal: 'memTotal',
|
||||||
|
diskTotal: 'diskTotal',
|
||||||
|
cpuCores: 'cpuCores',
|
||||||
|
cpuUsage: 'cpuUsage',
|
||||||
|
ramUsed: 'ramUsed',
|
||||||
|
swapUsed: 'swapUsed',
|
||||||
|
load1: 'load1',
|
||||||
|
load5: 'load5',
|
||||||
|
load15: 'load15',
|
||||||
|
diskUsed: 'diskUsed',
|
||||||
|
netIn: 'netIn',
|
||||||
|
netOut: 'netOut',
|
||||||
|
uptime: 'uptime',
|
||||||
|
process: 'process',
|
||||||
|
connections: 'connections',
|
||||||
|
status: 'status',
|
||||||
|
recordedAt: 'recordedAt',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ServerNodeScalarFieldEnum = (typeof ServerNodeScalarFieldEnum)[keyof typeof ServerNodeScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ServerNodeHistoryScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
uuid: 'uuid',
|
||||||
|
period: 'period',
|
||||||
|
cpu: 'cpu',
|
||||||
|
ram: 'ram',
|
||||||
|
load: 'load',
|
||||||
|
disk: 'disk',
|
||||||
|
netIn: 'netIn',
|
||||||
|
netOut: 'netOut',
|
||||||
|
process: 'process',
|
||||||
|
connections: 'connections',
|
||||||
|
createdAt: 'createdAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ServerNodeHistoryScalarFieldEnum = (typeof ServerNodeHistoryScalarFieldEnum)[keyof typeof ServerNodeHistoryScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ServiceCheckScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
name: 'name',
|
||||||
|
url: 'url',
|
||||||
|
status: 'status',
|
||||||
|
latencyMs: 'latencyMs',
|
||||||
|
message: 'message',
|
||||||
|
lastCheckAt: 'lastCheckAt',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ServiceCheckScalarFieldEnum = (typeof ServiceCheckScalarFieldEnum)[keyof typeof ServiceCheckScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const Econ_SowInventoryScalarFieldEnum = {
|
export const Econ_SowInventoryScalarFieldEnum = {
|
||||||
id: 'id',
|
id: 'id',
|
||||||
month: 'month',
|
month: 'month',
|
||||||
@@ -217,17 +334,21 @@ export type StrategyOrderByRelevanceFieldEnum = (typeof StrategyOrderByRelevance
|
|||||||
|
|
||||||
export const IndicatorOrderByRelevanceFieldEnum = {
|
export const IndicatorOrderByRelevanceFieldEnum = {
|
||||||
name: 'name',
|
name: 'name',
|
||||||
groupName: 'groupName'
|
groupName: 'groupName',
|
||||||
|
title: 'title',
|
||||||
|
unit: 'unit',
|
||||||
|
source: 'source',
|
||||||
|
frequency: 'frequency'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type IndicatorOrderByRelevanceFieldEnum = (typeof IndicatorOrderByRelevanceFieldEnum)[keyof typeof IndicatorOrderByRelevanceFieldEnum]
|
export type IndicatorOrderByRelevanceFieldEnum = (typeof IndicatorOrderByRelevanceFieldEnum)[keyof typeof IndicatorOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const Econ_SowInventoryOrderByRelevanceFieldEnum = {
|
export const IndicatorPointOrderByRelevanceFieldEnum = {
|
||||||
month: 'month'
|
period: 'period'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type Econ_SowInventoryOrderByRelevanceFieldEnum = (typeof Econ_SowInventoryOrderByRelevanceFieldEnum)[keyof typeof Econ_SowInventoryOrderByRelevanceFieldEnum]
|
export type IndicatorPointOrderByRelevanceFieldEnum = (typeof IndicatorPointOrderByRelevanceFieldEnum)[keyof typeof IndicatorPointOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const NullsOrder = {
|
export const NullsOrder = {
|
||||||
@@ -238,6 +359,63 @@ export const NullsOrder = {
|
|||||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||||
|
|
||||||
|
|
||||||
|
export const NewsOrderByRelevanceFieldEnum = {
|
||||||
|
title: 'title',
|
||||||
|
summary: 'summary',
|
||||||
|
content: 'content',
|
||||||
|
source: 'source',
|
||||||
|
url: 'url'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type NewsOrderByRelevanceFieldEnum = (typeof NewsOrderByRelevanceFieldEnum)[keyof typeof NewsOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const AlertOrderByRelevanceFieldEnum = {
|
||||||
|
indicatorName: 'indicatorName',
|
||||||
|
period: 'period',
|
||||||
|
sourceA: 'sourceA',
|
||||||
|
sourceB: 'sourceB'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type AlertOrderByRelevanceFieldEnum = (typeof AlertOrderByRelevanceFieldEnum)[keyof typeof AlertOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ServerNodeOrderByRelevanceFieldEnum = {
|
||||||
|
uuid: 'uuid',
|
||||||
|
name: 'name',
|
||||||
|
cpuName: 'cpuName',
|
||||||
|
os: 'os',
|
||||||
|
region: 'region',
|
||||||
|
status: 'status'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ServerNodeOrderByRelevanceFieldEnum = (typeof ServerNodeOrderByRelevanceFieldEnum)[keyof typeof ServerNodeOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ServerNodeHistoryOrderByRelevanceFieldEnum = {
|
||||||
|
uuid: 'uuid'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ServerNodeHistoryOrderByRelevanceFieldEnum = (typeof ServerNodeHistoryOrderByRelevanceFieldEnum)[keyof typeof ServerNodeHistoryOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const ServiceCheckOrderByRelevanceFieldEnum = {
|
||||||
|
name: 'name',
|
||||||
|
url: 'url',
|
||||||
|
status: 'status',
|
||||||
|
message: 'message'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type ServiceCheckOrderByRelevanceFieldEnum = (typeof ServiceCheckOrderByRelevanceFieldEnum)[keyof typeof ServiceCheckOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const Econ_SowInventoryOrderByRelevanceFieldEnum = {
|
||||||
|
month: 'month'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type Econ_SowInventoryOrderByRelevanceFieldEnum = (typeof Econ_SowInventoryOrderByRelevanceFieldEnum)[keyof typeof Econ_SowInventoryOrderByRelevanceFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const ScraperCacheOrderByRelevanceFieldEnum = {
|
export const ScraperCacheOrderByRelevanceFieldEnum = {
|
||||||
scraperName: 'scraperName',
|
scraperName: 'scraperName',
|
||||||
lastDataHash: 'lastDataHash'
|
lastDataHash: 'lastDataHash'
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ export type * from './models/User.ts'
|
|||||||
export type * from './models/Article.ts'
|
export type * from './models/Article.ts'
|
||||||
export type * from './models/Strategy.ts'
|
export type * from './models/Strategy.ts'
|
||||||
export type * from './models/Indicator.ts'
|
export type * from './models/Indicator.ts'
|
||||||
|
export type * from './models/IndicatorPoint.ts'
|
||||||
|
export type * from './models/News.ts'
|
||||||
|
export type * from './models/Alert.ts'
|
||||||
|
export type * from './models/ServerNode.ts'
|
||||||
|
export type * from './models/ServerNodeHistory.ts'
|
||||||
|
export type * from './models/ServiceCheck.ts'
|
||||||
export type * from './models/Econ_SowInventory.ts'
|
export type * from './models/Econ_SowInventory.ts'
|
||||||
export type * from './models/ScraperCache.ts'
|
export type * from './models/ScraperCache.ts'
|
||||||
export type * from './commonInputTypes.ts'
|
export type * from './commonInputTypes.ts'
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,11 @@ export type IndicatorMinAggregateOutputType = {
|
|||||||
id: number | null
|
id: number | null
|
||||||
name: string | null
|
name: string | null
|
||||||
groupName: string | null
|
groupName: string | null
|
||||||
|
title: string | null
|
||||||
|
unit: string | null
|
||||||
|
source: string | null
|
||||||
|
frequency: string | null
|
||||||
|
hidden: boolean | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
}
|
}
|
||||||
@@ -46,6 +51,11 @@ export type IndicatorMaxAggregateOutputType = {
|
|||||||
id: number | null
|
id: number | null
|
||||||
name: string | null
|
name: string | null
|
||||||
groupName: string | null
|
groupName: string | null
|
||||||
|
title: string | null
|
||||||
|
unit: string | null
|
||||||
|
source: string | null
|
||||||
|
frequency: string | null
|
||||||
|
hidden: boolean | null
|
||||||
createdAt: Date | null
|
createdAt: Date | null
|
||||||
updatedAt: Date | null
|
updatedAt: Date | null
|
||||||
}
|
}
|
||||||
@@ -54,7 +64,11 @@ export type IndicatorCountAggregateOutputType = {
|
|||||||
id: number
|
id: number
|
||||||
name: number
|
name: number
|
||||||
groupName: number
|
groupName: number
|
||||||
data: number
|
title: number
|
||||||
|
unit: number
|
||||||
|
source: number
|
||||||
|
frequency: number
|
||||||
|
hidden: number
|
||||||
createdAt: number
|
createdAt: number
|
||||||
updatedAt: number
|
updatedAt: number
|
||||||
_all: number
|
_all: number
|
||||||
@@ -73,6 +87,11 @@ export type IndicatorMinAggregateInputType = {
|
|||||||
id?: true
|
id?: true
|
||||||
name?: true
|
name?: true
|
||||||
groupName?: true
|
groupName?: true
|
||||||
|
title?: true
|
||||||
|
unit?: true
|
||||||
|
source?: true
|
||||||
|
frequency?: true
|
||||||
|
hidden?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
}
|
}
|
||||||
@@ -81,6 +100,11 @@ export type IndicatorMaxAggregateInputType = {
|
|||||||
id?: true
|
id?: true
|
||||||
name?: true
|
name?: true
|
||||||
groupName?: true
|
groupName?: true
|
||||||
|
title?: true
|
||||||
|
unit?: true
|
||||||
|
source?: true
|
||||||
|
frequency?: true
|
||||||
|
hidden?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
}
|
}
|
||||||
@@ -89,7 +113,11 @@ export type IndicatorCountAggregateInputType = {
|
|||||||
id?: true
|
id?: true
|
||||||
name?: true
|
name?: true
|
||||||
groupName?: true
|
groupName?: true
|
||||||
data?: true
|
title?: true
|
||||||
|
unit?: true
|
||||||
|
source?: true
|
||||||
|
frequency?: true
|
||||||
|
hidden?: true
|
||||||
createdAt?: true
|
createdAt?: true
|
||||||
updatedAt?: true
|
updatedAt?: true
|
||||||
_all?: true
|
_all?: true
|
||||||
@@ -185,7 +213,11 @@ export type IndicatorGroupByOutputType = {
|
|||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
groupName: string
|
groupName: string
|
||||||
data: runtime.JsonValue
|
title: string
|
||||||
|
unit: string
|
||||||
|
source: string
|
||||||
|
frequency: string
|
||||||
|
hidden: boolean
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
_count: IndicatorCountAggregateOutputType | null
|
_count: IndicatorCountAggregateOutputType | null
|
||||||
@@ -217,18 +249,28 @@ export type IndicatorWhereInput = {
|
|||||||
id?: Prisma.IntFilter<"Indicator"> | number
|
id?: Prisma.IntFilter<"Indicator"> | number
|
||||||
name?: Prisma.StringFilter<"Indicator"> | string
|
name?: Prisma.StringFilter<"Indicator"> | string
|
||||||
groupName?: Prisma.StringFilter<"Indicator"> | string
|
groupName?: Prisma.StringFilter<"Indicator"> | string
|
||||||
data?: Prisma.JsonFilter<"Indicator">
|
title?: Prisma.StringFilter<"Indicator"> | string
|
||||||
|
unit?: Prisma.StringFilter<"Indicator"> | string
|
||||||
|
source?: Prisma.StringFilter<"Indicator"> | string
|
||||||
|
frequency?: Prisma.StringFilter<"Indicator"> | string
|
||||||
|
hidden?: Prisma.BoolFilter<"Indicator"> | boolean
|
||||||
createdAt?: Prisma.DateTimeFilter<"Indicator"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Indicator"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"Indicator"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Indicator"> | Date | string
|
||||||
|
points?: Prisma.IndicatorPointListRelationFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IndicatorOrderByWithRelationInput = {
|
export type IndicatorOrderByWithRelationInput = {
|
||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
groupName?: Prisma.SortOrder
|
groupName?: Prisma.SortOrder
|
||||||
data?: Prisma.SortOrder
|
title?: Prisma.SortOrder
|
||||||
|
unit?: Prisma.SortOrder
|
||||||
|
source?: Prisma.SortOrder
|
||||||
|
frequency?: Prisma.SortOrder
|
||||||
|
hidden?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
|
points?: Prisma.IndicatorPointOrderByRelationAggregateInput
|
||||||
_relevance?: Prisma.IndicatorOrderByRelevanceInput
|
_relevance?: Prisma.IndicatorOrderByRelevanceInput
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,16 +281,25 @@ export type IndicatorWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
OR?: Prisma.IndicatorWhereInput[]
|
OR?: Prisma.IndicatorWhereInput[]
|
||||||
NOT?: Prisma.IndicatorWhereInput | Prisma.IndicatorWhereInput[]
|
NOT?: Prisma.IndicatorWhereInput | Prisma.IndicatorWhereInput[]
|
||||||
groupName?: Prisma.StringFilter<"Indicator"> | string
|
groupName?: Prisma.StringFilter<"Indicator"> | string
|
||||||
data?: Prisma.JsonFilter<"Indicator">
|
title?: Prisma.StringFilter<"Indicator"> | string
|
||||||
|
unit?: Prisma.StringFilter<"Indicator"> | string
|
||||||
|
source?: Prisma.StringFilter<"Indicator"> | string
|
||||||
|
frequency?: Prisma.StringFilter<"Indicator"> | string
|
||||||
|
hidden?: Prisma.BoolFilter<"Indicator"> | boolean
|
||||||
createdAt?: Prisma.DateTimeFilter<"Indicator"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Indicator"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"Indicator"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Indicator"> | Date | string
|
||||||
|
points?: Prisma.IndicatorPointListRelationFilter
|
||||||
}, "id" | "name">
|
}, "id" | "name">
|
||||||
|
|
||||||
export type IndicatorOrderByWithAggregationInput = {
|
export type IndicatorOrderByWithAggregationInput = {
|
||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
groupName?: Prisma.SortOrder
|
groupName?: Prisma.SortOrder
|
||||||
data?: Prisma.SortOrder
|
title?: Prisma.SortOrder
|
||||||
|
unit?: Prisma.SortOrder
|
||||||
|
source?: Prisma.SortOrder
|
||||||
|
frequency?: Prisma.SortOrder
|
||||||
|
hidden?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
_count?: Prisma.IndicatorCountOrderByAggregateInput
|
_count?: Prisma.IndicatorCountOrderByAggregateInput
|
||||||
@@ -265,7 +316,11 @@ export type IndicatorScalarWhereWithAggregatesInput = {
|
|||||||
id?: Prisma.IntWithAggregatesFilter<"Indicator"> | number
|
id?: Prisma.IntWithAggregatesFilter<"Indicator"> | number
|
||||||
name?: Prisma.StringWithAggregatesFilter<"Indicator"> | string
|
name?: Prisma.StringWithAggregatesFilter<"Indicator"> | string
|
||||||
groupName?: Prisma.StringWithAggregatesFilter<"Indicator"> | string
|
groupName?: Prisma.StringWithAggregatesFilter<"Indicator"> | string
|
||||||
data?: Prisma.JsonWithAggregatesFilter<"Indicator">
|
title?: Prisma.StringWithAggregatesFilter<"Indicator"> | string
|
||||||
|
unit?: Prisma.StringWithAggregatesFilter<"Indicator"> | string
|
||||||
|
source?: Prisma.StringWithAggregatesFilter<"Indicator"> | string
|
||||||
|
frequency?: Prisma.StringWithAggregatesFilter<"Indicator"> | string
|
||||||
|
hidden?: Prisma.BoolWithAggregatesFilter<"Indicator"> | boolean
|
||||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Indicator"> | Date | string
|
createdAt?: Prisma.DateTimeWithAggregatesFilter<"Indicator"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Indicator"> | Date | string
|
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"Indicator"> | Date | string
|
||||||
}
|
}
|
||||||
@@ -273,42 +328,66 @@ export type IndicatorScalarWhereWithAggregatesInput = {
|
|||||||
export type IndicatorCreateInput = {
|
export type IndicatorCreateInput = {
|
||||||
name: string
|
name: string
|
||||||
groupName: string
|
groupName: string
|
||||||
data: Prisma.JsonNullValueInput | runtime.InputJsonValue
|
title?: string
|
||||||
|
unit?: string
|
||||||
|
source?: string
|
||||||
|
frequency?: string
|
||||||
|
hidden?: boolean
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
points?: Prisma.IndicatorPointCreateNestedManyWithoutIndicatorInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IndicatorUncheckedCreateInput = {
|
export type IndicatorUncheckedCreateInput = {
|
||||||
id?: number
|
id?: number
|
||||||
name: string
|
name: string
|
||||||
groupName: string
|
groupName: string
|
||||||
data: Prisma.JsonNullValueInput | runtime.InputJsonValue
|
title?: string
|
||||||
|
unit?: string
|
||||||
|
source?: string
|
||||||
|
frequency?: string
|
||||||
|
hidden?: boolean
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
|
points?: Prisma.IndicatorPointUncheckedCreateNestedManyWithoutIndicatorInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IndicatorUpdateInput = {
|
export type IndicatorUpdateInput = {
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
groupName?: Prisma.StringFieldUpdateOperationsInput | string
|
groupName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
data?: Prisma.JsonNullValueInput | runtime.InputJsonValue
|
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
unit?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
source?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
frequency?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
hidden?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
points?: Prisma.IndicatorPointUpdateManyWithoutIndicatorNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IndicatorUncheckedUpdateInput = {
|
export type IndicatorUncheckedUpdateInput = {
|
||||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
groupName?: Prisma.StringFieldUpdateOperationsInput | string
|
groupName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
data?: Prisma.JsonNullValueInput | runtime.InputJsonValue
|
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
unit?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
source?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
frequency?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
hidden?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
points?: Prisma.IndicatorPointUncheckedUpdateManyWithoutIndicatorNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IndicatorCreateManyInput = {
|
export type IndicatorCreateManyInput = {
|
||||||
id?: number
|
id?: number
|
||||||
name: string
|
name: string
|
||||||
groupName: string
|
groupName: string
|
||||||
data: Prisma.JsonNullValueInput | runtime.InputJsonValue
|
title?: string
|
||||||
|
unit?: string
|
||||||
|
source?: string
|
||||||
|
frequency?: string
|
||||||
|
hidden?: boolean
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
updatedAt?: Date | string
|
updatedAt?: Date | string
|
||||||
}
|
}
|
||||||
@@ -316,7 +395,11 @@ export type IndicatorCreateManyInput = {
|
|||||||
export type IndicatorUpdateManyMutationInput = {
|
export type IndicatorUpdateManyMutationInput = {
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
groupName?: Prisma.StringFieldUpdateOperationsInput | string
|
groupName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
data?: Prisma.JsonNullValueInput | runtime.InputJsonValue
|
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
unit?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
source?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
frequency?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
hidden?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -325,7 +408,11 @@ export type IndicatorUncheckedUpdateManyInput = {
|
|||||||
id?: Prisma.IntFieldUpdateOperationsInput | number
|
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
name?: Prisma.StringFieldUpdateOperationsInput | string
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
groupName?: Prisma.StringFieldUpdateOperationsInput | string
|
groupName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
data?: Prisma.JsonNullValueInput | runtime.InputJsonValue
|
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
unit?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
source?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
frequency?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
hidden?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
}
|
}
|
||||||
@@ -340,7 +427,11 @@ export type IndicatorCountOrderByAggregateInput = {
|
|||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
groupName?: Prisma.SortOrder
|
groupName?: Prisma.SortOrder
|
||||||
data?: Prisma.SortOrder
|
title?: Prisma.SortOrder
|
||||||
|
unit?: Prisma.SortOrder
|
||||||
|
source?: Prisma.SortOrder
|
||||||
|
frequency?: Prisma.SortOrder
|
||||||
|
hidden?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
@@ -353,6 +444,11 @@ export type IndicatorMaxOrderByAggregateInput = {
|
|||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
groupName?: Prisma.SortOrder
|
groupName?: Prisma.SortOrder
|
||||||
|
title?: Prisma.SortOrder
|
||||||
|
unit?: Prisma.SortOrder
|
||||||
|
source?: Prisma.SortOrder
|
||||||
|
frequency?: Prisma.SortOrder
|
||||||
|
hidden?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
@@ -361,6 +457,11 @@ export type IndicatorMinOrderByAggregateInput = {
|
|||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
name?: Prisma.SortOrder
|
name?: Prisma.SortOrder
|
||||||
groupName?: Prisma.SortOrder
|
groupName?: Prisma.SortOrder
|
||||||
|
title?: Prisma.SortOrder
|
||||||
|
unit?: Prisma.SortOrder
|
||||||
|
source?: Prisma.SortOrder
|
||||||
|
frequency?: Prisma.SortOrder
|
||||||
|
hidden?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
updatedAt?: Prisma.SortOrder
|
updatedAt?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
@@ -369,15 +470,139 @@ export type IndicatorSumOrderByAggregateInput = {
|
|||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IndicatorScalarRelationFilter = {
|
||||||
|
is?: Prisma.IndicatorWhereInput
|
||||||
|
isNot?: Prisma.IndicatorWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BoolFieldUpdateOperationsInput = {
|
||||||
|
set?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IndicatorCreateNestedOneWithoutPointsInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.IndicatorCreateWithoutPointsInput, Prisma.IndicatorUncheckedCreateWithoutPointsInput>
|
||||||
|
connectOrCreate?: Prisma.IndicatorCreateOrConnectWithoutPointsInput
|
||||||
|
connect?: Prisma.IndicatorWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IndicatorUpdateOneRequiredWithoutPointsNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.IndicatorCreateWithoutPointsInput, Prisma.IndicatorUncheckedCreateWithoutPointsInput>
|
||||||
|
connectOrCreate?: Prisma.IndicatorCreateOrConnectWithoutPointsInput
|
||||||
|
upsert?: Prisma.IndicatorUpsertWithoutPointsInput
|
||||||
|
connect?: Prisma.IndicatorWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.IndicatorUpdateToOneWithWhereWithoutPointsInput, Prisma.IndicatorUpdateWithoutPointsInput>, Prisma.IndicatorUncheckedUpdateWithoutPointsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IndicatorCreateWithoutPointsInput = {
|
||||||
|
name: string
|
||||||
|
groupName: string
|
||||||
|
title?: string
|
||||||
|
unit?: string
|
||||||
|
source?: string
|
||||||
|
frequency?: string
|
||||||
|
hidden?: boolean
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IndicatorUncheckedCreateWithoutPointsInput = {
|
||||||
|
id?: number
|
||||||
|
name: string
|
||||||
|
groupName: string
|
||||||
|
title?: string
|
||||||
|
unit?: string
|
||||||
|
source?: string
|
||||||
|
frequency?: string
|
||||||
|
hidden?: boolean
|
||||||
|
createdAt?: Date | string
|
||||||
|
updatedAt?: Date | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IndicatorCreateOrConnectWithoutPointsInput = {
|
||||||
|
where: Prisma.IndicatorWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.IndicatorCreateWithoutPointsInput, Prisma.IndicatorUncheckedCreateWithoutPointsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IndicatorUpsertWithoutPointsInput = {
|
||||||
|
update: Prisma.XOR<Prisma.IndicatorUpdateWithoutPointsInput, Prisma.IndicatorUncheckedUpdateWithoutPointsInput>
|
||||||
|
create: Prisma.XOR<Prisma.IndicatorCreateWithoutPointsInput, Prisma.IndicatorUncheckedCreateWithoutPointsInput>
|
||||||
|
where?: Prisma.IndicatorWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IndicatorUpdateToOneWithWhereWithoutPointsInput = {
|
||||||
|
where?: Prisma.IndicatorWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.IndicatorUpdateWithoutPointsInput, Prisma.IndicatorUncheckedUpdateWithoutPointsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IndicatorUpdateWithoutPointsInput = {
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
groupName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
unit?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
source?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
frequency?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
hidden?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IndicatorUncheckedUpdateWithoutPointsInput = {
|
||||||
|
id?: Prisma.IntFieldUpdateOperationsInput | number
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
groupName?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
title?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
unit?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
source?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
frequency?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
hidden?: Prisma.BoolFieldUpdateOperationsInput | boolean
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count Type IndicatorCountOutputType
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type IndicatorCountOutputType = {
|
||||||
|
points: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IndicatorCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
points?: boolean | IndicatorCountOutputTypeCountPointsArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IndicatorCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type IndicatorCountOutputTypeDefaultArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the IndicatorCountOutputType
|
||||||
|
*/
|
||||||
|
select?: Prisma.IndicatorCountOutputTypeSelect<ExtArgs> | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IndicatorCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type IndicatorCountOutputTypeCountPointsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.IndicatorPointWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type IndicatorSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type IndicatorSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
name?: boolean
|
name?: boolean
|
||||||
groupName?: boolean
|
groupName?: boolean
|
||||||
data?: boolean
|
title?: boolean
|
||||||
|
unit?: boolean
|
||||||
|
source?: boolean
|
||||||
|
frequency?: boolean
|
||||||
|
hidden?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
|
points?: boolean | Prisma.Indicator$pointsArgs<ExtArgs>
|
||||||
|
_count?: boolean | Prisma.IndicatorCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["indicator"]>
|
}, ExtArgs["result"]["indicator"]>
|
||||||
|
|
||||||
|
|
||||||
@@ -386,21 +611,35 @@ export type IndicatorSelectScalar = {
|
|||||||
id?: boolean
|
id?: boolean
|
||||||
name?: boolean
|
name?: boolean
|
||||||
groupName?: boolean
|
groupName?: boolean
|
||||||
data?: boolean
|
title?: boolean
|
||||||
|
unit?: boolean
|
||||||
|
source?: boolean
|
||||||
|
frequency?: boolean
|
||||||
|
hidden?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
updatedAt?: boolean
|
updatedAt?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IndicatorOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "groupName" | "data" | "createdAt" | "updatedAt", ExtArgs["result"]["indicator"]>
|
export type IndicatorOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "groupName" | "title" | "unit" | "source" | "frequency" | "hidden" | "createdAt" | "updatedAt", ExtArgs["result"]["indicator"]>
|
||||||
|
export type IndicatorInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
points?: boolean | Prisma.Indicator$pointsArgs<ExtArgs>
|
||||||
|
_count?: boolean | Prisma.IndicatorCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
|
}
|
||||||
|
|
||||||
export type $IndicatorPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type $IndicatorPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
name: "Indicator"
|
name: "Indicator"
|
||||||
objects: {}
|
objects: {
|
||||||
|
points: Prisma.$IndicatorPointPayload<ExtArgs>[]
|
||||||
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
groupName: string
|
groupName: string
|
||||||
data: runtime.JsonValue
|
title: string
|
||||||
|
unit: string
|
||||||
|
source: string
|
||||||
|
frequency: string
|
||||||
|
hidden: boolean
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
updatedAt: Date
|
updatedAt: Date
|
||||||
}, ExtArgs["result"]["indicator"]>
|
}, ExtArgs["result"]["indicator"]>
|
||||||
@@ -743,6 +982,7 @@ readonly fields: IndicatorFieldRefs;
|
|||||||
*/
|
*/
|
||||||
export interface Prisma__IndicatorClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
export interface Prisma__IndicatorClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
||||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||||
|
points<T extends Prisma.Indicator$pointsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.Indicator$pointsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$IndicatorPointPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
/**
|
/**
|
||||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||||
@@ -775,7 +1015,11 @@ export interface IndicatorFieldRefs {
|
|||||||
readonly id: Prisma.FieldRef<"Indicator", 'Int'>
|
readonly id: Prisma.FieldRef<"Indicator", 'Int'>
|
||||||
readonly name: Prisma.FieldRef<"Indicator", 'String'>
|
readonly name: Prisma.FieldRef<"Indicator", 'String'>
|
||||||
readonly groupName: Prisma.FieldRef<"Indicator", 'String'>
|
readonly groupName: Prisma.FieldRef<"Indicator", 'String'>
|
||||||
readonly data: Prisma.FieldRef<"Indicator", 'Json'>
|
readonly title: Prisma.FieldRef<"Indicator", 'String'>
|
||||||
|
readonly unit: Prisma.FieldRef<"Indicator", 'String'>
|
||||||
|
readonly source: Prisma.FieldRef<"Indicator", 'String'>
|
||||||
|
readonly frequency: Prisma.FieldRef<"Indicator", 'String'>
|
||||||
|
readonly hidden: Prisma.FieldRef<"Indicator", 'Boolean'>
|
||||||
readonly createdAt: Prisma.FieldRef<"Indicator", 'DateTime'>
|
readonly createdAt: Prisma.FieldRef<"Indicator", 'DateTime'>
|
||||||
readonly updatedAt: Prisma.FieldRef<"Indicator", 'DateTime'>
|
readonly updatedAt: Prisma.FieldRef<"Indicator", 'DateTime'>
|
||||||
}
|
}
|
||||||
@@ -794,6 +1038,10 @@ export type IndicatorFindUniqueArgs<ExtArgs extends runtime.Types.Extensions.Int
|
|||||||
* Omit specific fields from the Indicator
|
* Omit specific fields from the Indicator
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.IndicatorInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* Filter, which Indicator to fetch.
|
* Filter, which Indicator to fetch.
|
||||||
*/
|
*/
|
||||||
@@ -812,6 +1060,10 @@ export type IndicatorFindUniqueOrThrowArgs<ExtArgs extends runtime.Types.Extensi
|
|||||||
* Omit specific fields from the Indicator
|
* Omit specific fields from the Indicator
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.IndicatorInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* Filter, which Indicator to fetch.
|
* Filter, which Indicator to fetch.
|
||||||
*/
|
*/
|
||||||
@@ -830,6 +1082,10 @@ export type IndicatorFindFirstArgs<ExtArgs extends runtime.Types.Extensions.Inte
|
|||||||
* Omit specific fields from the Indicator
|
* Omit specific fields from the Indicator
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.IndicatorInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* Filter, which Indicator to fetch.
|
* Filter, which Indicator to fetch.
|
||||||
*/
|
*/
|
||||||
@@ -878,6 +1134,10 @@ export type IndicatorFindFirstOrThrowArgs<ExtArgs extends runtime.Types.Extensio
|
|||||||
* Omit specific fields from the Indicator
|
* Omit specific fields from the Indicator
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.IndicatorInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* Filter, which Indicator to fetch.
|
* Filter, which Indicator to fetch.
|
||||||
*/
|
*/
|
||||||
@@ -926,6 +1186,10 @@ export type IndicatorFindManyArgs<ExtArgs extends runtime.Types.Extensions.Inter
|
|||||||
* Omit specific fields from the Indicator
|
* Omit specific fields from the Indicator
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.IndicatorInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* Filter, which Indicators to fetch.
|
* Filter, which Indicators to fetch.
|
||||||
*/
|
*/
|
||||||
@@ -974,6 +1238,10 @@ export type IndicatorCreateArgs<ExtArgs extends runtime.Types.Extensions.Interna
|
|||||||
* Omit specific fields from the Indicator
|
* Omit specific fields from the Indicator
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.IndicatorInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* The data needed to create a Indicator.
|
* The data needed to create a Indicator.
|
||||||
*/
|
*/
|
||||||
@@ -1003,6 +1271,10 @@ export type IndicatorUpdateArgs<ExtArgs extends runtime.Types.Extensions.Interna
|
|||||||
* Omit specific fields from the Indicator
|
* Omit specific fields from the Indicator
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.IndicatorInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* The data needed to update a Indicator.
|
* The data needed to update a Indicator.
|
||||||
*/
|
*/
|
||||||
@@ -1043,6 +1315,10 @@ export type IndicatorUpsertArgs<ExtArgs extends runtime.Types.Extensions.Interna
|
|||||||
* Omit specific fields from the Indicator
|
* Omit specific fields from the Indicator
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.IndicatorInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* The filter to search for the Indicator to update in case it exists.
|
* The filter to search for the Indicator to update in case it exists.
|
||||||
*/
|
*/
|
||||||
@@ -1069,6 +1345,10 @@ export type IndicatorDeleteArgs<ExtArgs extends runtime.Types.Extensions.Interna
|
|||||||
* Omit specific fields from the Indicator
|
* Omit specific fields from the Indicator
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.IndicatorInclude<ExtArgs> | null
|
||||||
/**
|
/**
|
||||||
* Filter which Indicator to delete.
|
* Filter which Indicator to delete.
|
||||||
*/
|
*/
|
||||||
@@ -1089,6 +1369,30 @@ export type IndicatorDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Int
|
|||||||
limit?: number
|
limit?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicator.points
|
||||||
|
*/
|
||||||
|
export type Indicator$pointsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the IndicatorPoint
|
||||||
|
*/
|
||||||
|
select?: Prisma.IndicatorPointSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the IndicatorPoint
|
||||||
|
*/
|
||||||
|
omit?: Prisma.IndicatorPointOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.IndicatorPointInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.IndicatorPointWhereInput
|
||||||
|
orderBy?: Prisma.IndicatorPointOrderByWithRelationInput | Prisma.IndicatorPointOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.IndicatorPointWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.IndicatorPointScalarFieldEnum | Prisma.IndicatorPointScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicator without action
|
* Indicator without action
|
||||||
*/
|
*/
|
||||||
@@ -1101,4 +1405,8 @@ export type IndicatorDefaultArgs<ExtArgs extends runtime.Types.Extensions.Intern
|
|||||||
* Omit specific fields from the Indicator
|
* Omit specific fields from the Indicator
|
||||||
*/
|
*/
|
||||||
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
omit?: Prisma.IndicatorOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.IndicatorInclude<ExtArgs> | null
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -391,10 +391,6 @@ export type ScraperCacheSumOrderByAggregateInput = {
|
|||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NullableStringFieldUpdateOperationsInput = {
|
|
||||||
set?: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export type ScraperCacheSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type ScraperCacheSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -234,15 +234,15 @@ export type StrategyOrderByWithRelationInput = {
|
|||||||
|
|
||||||
export type StrategyWhereUniqueInput = Prisma.AtLeast<{
|
export type StrategyWhereUniqueInput = Prisma.AtLeast<{
|
||||||
id?: number
|
id?: number
|
||||||
name?: string
|
|
||||||
AND?: Prisma.StrategyWhereInput | Prisma.StrategyWhereInput[]
|
AND?: Prisma.StrategyWhereInput | Prisma.StrategyWhereInput[]
|
||||||
OR?: Prisma.StrategyWhereInput[]
|
OR?: Prisma.StrategyWhereInput[]
|
||||||
NOT?: Prisma.StrategyWhereInput | Prisma.StrategyWhereInput[]
|
NOT?: Prisma.StrategyWhereInput | Prisma.StrategyWhereInput[]
|
||||||
|
name?: Prisma.StringFilter<"Strategy"> | string
|
||||||
description?: Prisma.StringFilter<"Strategy"> | string
|
description?: Prisma.StringFilter<"Strategy"> | string
|
||||||
data?: Prisma.JsonFilter<"Strategy">
|
data?: Prisma.JsonFilter<"Strategy">
|
||||||
createdAt?: Prisma.DateTimeFilter<"Strategy"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"Strategy"> | Date | string
|
||||||
updatedAt?: Prisma.DateTimeFilter<"Strategy"> | Date | string
|
updatedAt?: Prisma.DateTimeFilter<"Strategy"> | Date | string
|
||||||
}, "id" | "name">
|
}, "id">
|
||||||
|
|
||||||
export type StrategyOrderByWithAggregationInput = {
|
export type StrategyOrderByWithAggregationInput = {
|
||||||
id?: Prisma.SortOrder
|
id?: Prisma.SortOrder
|
||||||
|
|||||||
Generated
+412
-2
@@ -11,6 +11,7 @@
|
|||||||
"@huolala-tech/page-spy": "^1.5.7",
|
"@huolala-tech/page-spy": "^1.5.7",
|
||||||
"@prisma/adapter-mariadb": "^7.9.1",
|
"@prisma/adapter-mariadb": "^7.9.1",
|
||||||
"@prisma/client": "^7.7.0",
|
"@prisma/client": "^7.7.0",
|
||||||
|
"antd-mobile": "^5.42.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
@@ -18,11 +19,13 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mariadb": "^3.5.3",
|
"mariadb": "^3.5.3",
|
||||||
"marked": "^17.0.6",
|
"marked": "^17.0.6",
|
||||||
|
"node-cron": "^3.0.3",
|
||||||
"prisma": "^7.7.0",
|
"prisma": "^7.7.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.0",
|
||||||
"recharts": "^3.8.1"
|
"recharts": "^3.8.1",
|
||||||
|
"ws": "^8.21.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
@@ -766,6 +769,31 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.8.0",
|
||||||
|
"@floating-ui/utils": "^0.2.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.12",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.12.tgz",
|
||||||
|
"integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@hono/node-server": {
|
"node_modules/@hono/node-server": {
|
||||||
"version": "1.19.11",
|
"version": "1.19.11",
|
||||||
"resolved": "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.11.tgz",
|
"resolved": "https://registry.npmmirror.com/@hono/node-server/-/node-server-1.19.11.tgz",
|
||||||
@@ -1635,6 +1663,91 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@rc-component/mini-decimal": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@rc-component/mini-decimal/-/mini-decimal-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-xiuXcaCwyOWpD8a8scdExFl+bntNphAW8XeenL1ig2en0AAZY0Pcp4pC0dI22qJ+NvxKn9RoNIoRdqYU3BLH4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/animated": {
|
||||||
|
"version": "9.6.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@react-spring/animated/-/animated-9.6.1.tgz",
|
||||||
|
"integrity": "sha512-ls/rJBrAqiAYozjLo5EPPLLOb1LM0lNVQcXODTC1SMtS6DbuBCPaKco5svFUQFMP2dso3O+qcC4k9FsKc0KxMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-spring/shared": "~9.6.1",
|
||||||
|
"@react-spring/types": "~9.6.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/core": {
|
||||||
|
"version": "9.6.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@react-spring/core/-/core-9.6.1.tgz",
|
||||||
|
"integrity": "sha512-3HAAinAyCPessyQNNXe5W0OHzRfa8Yo5P748paPcmMowZ/4sMfaZ2ZB6e5x5khQI8NusOHj8nquoutd6FRY5WQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-spring/animated": "~9.6.1",
|
||||||
|
"@react-spring/rafz": "~9.6.1",
|
||||||
|
"@react-spring/shared": "~9.6.1",
|
||||||
|
"@react-spring/types": "~9.6.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/react-spring/donate"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/rafz": {
|
||||||
|
"version": "9.6.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@react-spring/rafz/-/rafz-9.6.1.tgz",
|
||||||
|
"integrity": "sha512-v6qbgNRpztJFFfSE3e2W1Uz+g8KnIBs6SmzCzcVVF61GdGfGOuBrbjIcp+nUz301awVmREKi4eMQb2Ab2gGgyQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/shared": {
|
||||||
|
"version": "9.6.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@react-spring/shared/-/shared-9.6.1.tgz",
|
||||||
|
"integrity": "sha512-PBFBXabxFEuF8enNLkVqMC9h5uLRBo6GQhRMQT/nRTnemVENimgRd+0ZT4yFnAQ0AxWNiJfX3qux+bW2LbG6Bw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-spring/rafz": "~9.6.1",
|
||||||
|
"@react-spring/types": "~9.6.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/types": {
|
||||||
|
"version": "9.6.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@react-spring/types/-/types-9.6.1.tgz",
|
||||||
|
"integrity": "sha512-POu8Mk0hIU3lRXB3bGIGe4VHIwwDsQyoD1F394OK7STTiX9w4dG3cTLljjYswkQN+hDSHRrj4O36kuVa7KPU8Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@react-spring/web": {
|
||||||
|
"version": "9.6.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@react-spring/web/-/web-9.6.1.tgz",
|
||||||
|
"integrity": "sha512-X2zR6q2Z+FjsWfGAmAXlQaoUHbPmfuCaXpuM6TcwXPpLE1ZD4A1eys/wpXboFQmDkjnrlTmKvpVna1MjWpZ5Hw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-spring/animated": "~9.6.1",
|
||||||
|
"@react-spring/core": "~9.6.1",
|
||||||
|
"@react-spring/shared": "~9.6.1",
|
||||||
|
"@react-spring/types": "~9.6.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@reduxjs/toolkit": {
|
"node_modules/@reduxjs/toolkit": {
|
||||||
"version": "2.11.2",
|
"version": "2.11.2",
|
||||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||||
@@ -2398,6 +2511,12 @@
|
|||||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/js-cookie": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/js-cookie/-/js-cookie-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "26.1.2",
|
"version": "26.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-26.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/node/-/node-26.1.2.tgz",
|
||||||
@@ -2423,6 +2542,24 @@
|
|||||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@use-gesture/core": {
|
||||||
|
"version": "10.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@use-gesture/core/-/core-10.3.0.tgz",
|
||||||
|
"integrity": "sha512-rh+6MND31zfHcy9VU3dOZCqGY511lvGcfyJenN4cWZe0u1BH6brBpBddLVXhF2r4BMqWbvxfsbL7D287thJU2A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@use-gesture/react": {
|
||||||
|
"version": "10.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@use-gesture/react/-/react-10.3.0.tgz",
|
||||||
|
"integrity": "sha512-3zc+Ve99z4usVP6l9knYVbVnZgfqhKah7sIG+PS2w+vpig2v2OLct05vs+ZXMzwxdNCMka8B+8WlOo0z6Pn6DA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@use-gesture/core": "10.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||||
@@ -2457,6 +2594,28 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ahooks": {
|
||||||
|
"version": "3.9.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ahooks/-/ahooks-3.9.7.tgz",
|
||||||
|
"integrity": "sha512-S0lvzhbdlhK36RFBkGv+RbOM/dbbweym+BIHM/bwwuWVSVN5TuVErHPMWo4w0t1NDYg5KPp2iEf7Y7E5LASYiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.21.0",
|
||||||
|
"@types/js-cookie": "^3.0.6",
|
||||||
|
"dayjs": "^1.9.1",
|
||||||
|
"intersection-observer": "^0.12.0",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"react-fast-compare": "^3.2.2",
|
||||||
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
|
"screenfull": "^5.0.0",
|
||||||
|
"tslib": "^2.4.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "8.18.0",
|
"version": "8.18.0",
|
||||||
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz",
|
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz",
|
||||||
@@ -2499,6 +2658,56 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/antd-mobile": {
|
||||||
|
"version": "5.42.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/antd-mobile/-/antd-mobile-5.42.3.tgz",
|
||||||
|
"integrity": "sha512-HLykA0Cr2am7dgEch/TRUnr4GHgTOM59rA9CVjgBJVVJ8dk2w8uUBz//Bji19sRH2leiUObWY7KQZpsWF+WQeg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.4.2",
|
||||||
|
"@rc-component/mini-decimal": "^1.1.0",
|
||||||
|
"@react-spring/web": "~9.6.1",
|
||||||
|
"@use-gesture/react": "10.3.0",
|
||||||
|
"ahooks": "^3.7.6",
|
||||||
|
"antd-mobile-icons": "^0.3.0",
|
||||||
|
"antd-mobile-v5-count": "^1.0.1",
|
||||||
|
"classnames": "^2.3.2",
|
||||||
|
"dayjs": "^1.11.7",
|
||||||
|
"deepmerge": "^4.3.1",
|
||||||
|
"nano-memoize": "^3.0.16",
|
||||||
|
"rc-field-form": "^1.34.2",
|
||||||
|
"rc-segmented": "~2.4.1",
|
||||||
|
"rc-util": "^5.44.4",
|
||||||
|
"react-fast-compare": "^3.2.2",
|
||||||
|
"react-is": "^18.2.0",
|
||||||
|
"runes2": "^1.1.2",
|
||||||
|
"staged-components": "^1.1.3",
|
||||||
|
"tslib": "^2.5.0",
|
||||||
|
"use-sync-external-store": "^1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/antd-mobile-icons": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/antd-mobile-icons/-/antd-mobile-icons-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-rqINQpJWZWrva9moCd1Ye695MZYWmqLPE+bY8d2xLRy7iSQwPsinCdZYjpUPp2zL/LnKYSyXxP2ut2A+DC+whQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/antd-mobile-v5-count": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/antd-mobile-v5-count/-/antd-mobile-v5-count-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-YGsiEDCPUDz3SzfXi6gLZn/HpeSMW+jgPc4qiYUr1fSopg3hkUie2TnooJdExgfiETHefH3Ggs58He0OVfegLA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/antd-mobile/node_modules/react-is": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||||
@@ -2514,6 +2723,12 @@
|
|||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/async-validator": {
|
||||||
|
"version": "4.2.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
|
||||||
|
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
@@ -2856,6 +3071,12 @@
|
|||||||
"consola": "^3.2.3"
|
"consola": "^3.2.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/classnames": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cliui": {
|
"node_modules/cliui": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz",
|
||||||
@@ -3232,6 +3453,12 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.21",
|
||||||
|
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz",
|
||||||
|
"integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -3256,6 +3483,15 @@
|
|||||||
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/deepmerge": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/deepmerge-ts": {
|
"node_modules/deepmerge-ts": {
|
||||||
"version": "7.1.5",
|
"version": "7.1.5",
|
||||||
"resolved": "https://registry.npmmirror.com/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
"resolved": "https://registry.npmmirror.com/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz",
|
||||||
@@ -4203,6 +4439,13 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/intersection-observer": {
|
||||||
|
"version": "0.12.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.12.2.tgz",
|
||||||
|
"integrity": "sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==",
|
||||||
|
"deprecated": "The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019.",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -4259,6 +4502,12 @@
|
|||||||
"jiti": "lib/jiti-cli.mjs"
|
"jiti": "lib/jiti-cli.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-cookie": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmmirror.com/js-cookie/-/js-cookie-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
@@ -4374,6 +4623,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.18.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz",
|
||||||
|
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.includes": {
|
"node_modules/lodash.includes": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
"resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||||
@@ -4593,6 +4848,12 @@
|
|||||||
"node": ">=8.0.0"
|
"node": ">=8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nano-memoize": {
|
||||||
|
"version": "3.0.16",
|
||||||
|
"resolved": "https://registry.npmmirror.com/nano-memoize/-/nano-memoize-3.0.16.tgz",
|
||||||
|
"integrity": "sha512-JyK96AKVGAwVeMj3MoMhaSXaUNqgMbCRSQB3trUV8tYZfWEzqUBKdK1qJpfuNXgKeHOx1jv/IEYTM659ly7zUA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.16",
|
"version": "3.3.16",
|
||||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.16.tgz",
|
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.16.tgz",
|
||||||
@@ -4621,6 +4882,18 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-cron": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/node-cron/-/node-cron-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": "8.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/node-fetch-native": {
|
"node_modules/node-fetch-native": {
|
||||||
"version": "1.6.7",
|
"version": "1.6.7",
|
||||||
"resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
"resolved": "https://registry.npmmirror.com/node-fetch-native/-/node-fetch-native-1.6.7.tgz",
|
||||||
@@ -4973,6 +5246,75 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rc-field-form": {
|
||||||
|
"version": "1.44.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/rc-field-form/-/rc-field-form-1.44.0.tgz",
|
||||||
|
"integrity": "sha512-el7w87fyDUsca63Y/s8qJcq9kNkf/J5h+iTdqG5WsSHLH0e6Usl7QuYSmSVzJMgtp40mOVZIY/W/QP9zwrp1FA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.0",
|
||||||
|
"async-validator": "^4.1.0",
|
||||||
|
"rc-util": "^5.32.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.x"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.9.0",
|
||||||
|
"react-dom": ">=16.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rc-motion": {
|
||||||
|
"version": "2.9.5",
|
||||||
|
"resolved": "https://registry.npmmirror.com/rc-motion/-/rc-motion-2.9.5.tgz",
|
||||||
|
"integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.11.1",
|
||||||
|
"classnames": "^2.2.1",
|
||||||
|
"rc-util": "^5.44.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.9.0",
|
||||||
|
"react-dom": ">=16.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rc-segmented": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/rc-segmented/-/rc-segmented-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-KUi+JJFdKnumV9iXlm+BJ00O4NdVBp2TEexLCk6bK1x/RH83TvYKQMzIz/7m3UTRPD08RM/8VG/JNjWgWbd4cw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.11.1",
|
||||||
|
"classnames": "^2.2.1",
|
||||||
|
"rc-motion": "^2.4.4",
|
||||||
|
"rc-util": "^5.17.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.0.0",
|
||||||
|
"react-dom": ">=16.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rc-util": {
|
||||||
|
"version": "5.44.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/rc-util/-/rc-util-5.44.4.tgz",
|
||||||
|
"integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.3",
|
||||||
|
"react-is": "^18.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.9.0",
|
||||||
|
"react-dom": ">=16.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rc-util/node_modules/react-is": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/rc9": {
|
"node_modules/rc9": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmmirror.com/rc9/-/rc9-2.1.2.tgz",
|
"resolved": "https://registry.npmmirror.com/rc9/-/rc9-2.1.2.tgz",
|
||||||
@@ -5008,6 +5350,12 @@
|
|||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-fast-compare": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "19.2.4",
|
"version": "19.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz",
|
||||||
@@ -5172,6 +5520,12 @@
|
|||||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/resize-observer-polyfill": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/retry": {
|
"node_modules/retry": {
|
||||||
"version": "0.12.0",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz",
|
"resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz",
|
||||||
@@ -5227,6 +5581,12 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/runes2": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/runes2/-/runes2-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-LNPnEDPOOU4ehF71m5JoQyzT2yxwD6ZreFJ7MxZUAoMKNMY1XrAo60H1CUoX5ncSm0rIuKlqn9JZNRrRkNou2g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/rxjs": {
|
"node_modules/rxjs": {
|
||||||
"version": "7.8.2",
|
"version": "7.8.2",
|
||||||
"resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz",
|
"resolved": "https://registry.npmmirror.com/rxjs/-/rxjs-7.8.2.tgz",
|
||||||
@@ -5272,6 +5632,18 @@
|
|||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/screenfull": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/screenfull/-/screenfull-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/section-matter": {
|
"node_modules/section-matter": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
||||||
@@ -5566,6 +5938,15 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/staged-components": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/staged-components/-/staged-components-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-9EIswzDqjwlEu+ymkV09TTlJfzSbKgEnNteUnZSTxkpMgr5Wx2CzzA9WcMFWBNCldqVPsHVnRGGrApduq2Se5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/statuses": {
|
"node_modules/statuses": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz",
|
||||||
@@ -5678,7 +6059,6 @@
|
|||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
"dev": true,
|
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/type-is": {
|
"node_modules/type-is": {
|
||||||
@@ -5768,6 +6148,15 @@
|
|||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/valibot": {
|
"node_modules/valibot": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/valibot/-/valibot-1.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/valibot/-/valibot-1.2.0.tgz",
|
||||||
@@ -5972,6 +6361,27 @@
|
|||||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.21.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ws/-/ws-8.21.2.tgz",
|
||||||
|
"integrity": "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|||||||
+8
-2
@@ -12,6 +12,8 @@
|
|||||||
"start": "node --experimental-strip-types --no-warnings server/index.js",
|
"start": "node --experimental-strip-types --no-warnings server/index.js",
|
||||||
"db:migrate": "prisma migrate deploy",
|
"db:migrate": "prisma migrate deploy",
|
||||||
"db:seed": "node scripts/seed.cjs",
|
"db:seed": "node scripts/seed.cjs",
|
||||||
|
"data:refresh": "node --no-warnings scripts/refresh-indicators.cjs",
|
||||||
|
"data:discover": "node --no-warnings scripts/discover-mcp-tools.cjs",
|
||||||
"icons": "node scripts/generate-icons.mjs",
|
"icons": "node scripts/generate-icons.mjs",
|
||||||
"tauri:dev": "tauri dev",
|
"tauri:dev": "tauri dev",
|
||||||
"tauri:build": "tauri build",
|
"tauri:build": "tauri build",
|
||||||
@@ -19,12 +21,14 @@
|
|||||||
"tauri:android:dev": "tauri android dev",
|
"tauri:android:dev": "tauri android dev",
|
||||||
"tauri:android:build": "tauri android build",
|
"tauri:android:build": "tauri android build",
|
||||||
"tauri:android:apk": "tauri android build --apk",
|
"tauri:android:apk": "tauri android build --apk",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"data:news": "node --no-warnings scripts/refresh-news.cjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@huolala-tech/page-spy": "^1.5.7",
|
"@huolala-tech/page-spy": "^1.5.7",
|
||||||
"@prisma/adapter-mariadb": "^7.9.1",
|
"@prisma/adapter-mariadb": "^7.9.1",
|
||||||
"@prisma/client": "^7.7.0",
|
"@prisma/client": "^7.7.0",
|
||||||
|
"antd-mobile": "^5.42.3",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
@@ -32,11 +36,13 @@
|
|||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"mariadb": "^3.5.3",
|
"mariadb": "^3.5.3",
|
||||||
"marked": "^17.0.6",
|
"marked": "^17.0.6",
|
||||||
|
"node-cron": "^3.0.3",
|
||||||
"prisma": "^7.7.0",
|
"prisma": "^7.7.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^6.26.0",
|
||||||
"recharts": "^3.8.1"
|
"recharts": "^3.8.1",
|
||||||
|
"ws": "^8.21.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
import { createPool } from 'mariadb';
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
function loadDatabaseConfig() {
|
|
||||||
const envPath = path.join(process.cwd(), '.env');
|
|
||||||
if (fs.existsSync(envPath)) {
|
|
||||||
const envContent = fs.readFileSync(envPath, 'utf-8');
|
|
||||||
const lines = envContent.split('\n');
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const trimmedLine = line.trim();
|
|
||||||
if (trimmedLine.startsWith('DATABASE_URL=')) {
|
|
||||||
const url = trimmedLine.substring('DATABASE_URL='.length).replace(/^"|"$/g, '');
|
|
||||||
|
|
||||||
// 解析URL
|
|
||||||
const parts = url.split('://')[1].split('@');
|
|
||||||
const auth = parts[0].split(':');
|
|
||||||
const hostAndDb = parts[1].split('/');
|
|
||||||
const hostAndPort = hostAndDb[0].split(':');
|
|
||||||
|
|
||||||
return {
|
|
||||||
user: auth[0],
|
|
||||||
password: auth[1],
|
|
||||||
host: hostAndPort[0],
|
|
||||||
port: parseInt(hostAndPort[1]),
|
|
||||||
database: hostAndDb[1]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
host: '192.168.111.111',
|
|
||||||
port: 3306,
|
|
||||||
user: 'root',
|
|
||||||
password: 'fullstack',
|
|
||||||
database: 'iboard'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let pool;
|
|
||||||
|
|
||||||
async function initPool() {
|
|
||||||
if (!pool) {
|
|
||||||
const config = loadDatabaseConfig();
|
|
||||||
pool = createPool({
|
|
||||||
host: config.host,
|
|
||||||
port: config.port,
|
|
||||||
user: config.user,
|
|
||||||
password: config.password,
|
|
||||||
database: config.database,
|
|
||||||
connectionLimit: 5
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return pool;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function handler(req, res) {
|
|
||||||
try {
|
|
||||||
await initPool();
|
|
||||||
|
|
||||||
// 获取查询参数
|
|
||||||
const { startDate, endDate, limit = 30 } = req.query;
|
|
||||||
|
|
||||||
let query = 'SELECT date, centerPrice FROM econ_ExchangeRate WHERE currency = ? ORDER BY date DESC';
|
|
||||||
const params = ['USD/CNY'];
|
|
||||||
|
|
||||||
if (startDate && endDate) {
|
|
||||||
query = 'SELECT date, centerPrice FROM econ_ExchangeRate WHERE currency = ? AND date BETWEEN ? AND ? ORDER BY date DESC';
|
|
||||||
params.push(startDate, endDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await pool.query(query, params);
|
|
||||||
|
|
||||||
// 限制返回数量
|
|
||||||
const limitedResult = result.slice(0, parseInt(limit));
|
|
||||||
|
|
||||||
// 反转顺序,使日期从早到晚
|
|
||||||
const sortedResult = limitedResult.reverse();
|
|
||||||
|
|
||||||
// 格式化数据
|
|
||||||
const formattedData = sortedResult.map(row => ({
|
|
||||||
date: row.date instanceof Date ? row.date.toISOString().split('T')[0] : row.date,
|
|
||||||
rate: parseFloat(row.centerPrice)
|
|
||||||
}));
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: formattedData,
|
|
||||||
total: result.length
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取汇率数据失败:', error.message);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: '获取汇率数据失败',
|
|
||||||
error: error.message
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
// 不关闭连接池,保持连接池打开以提高性能
|
|
||||||
// 如果需要关闭连接池,应该在关闭后将 pool 变量设置为 null
|
|
||||||
// if (pool) {
|
|
||||||
// await pool.end();
|
|
||||||
// pool = null;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import { createPool } from 'mariadb';
|
|
||||||
|
|
||||||
async function handler(req, res) {
|
|
||||||
if (req.method !== 'GET') {
|
|
||||||
return res.status(405).json({ message: '只支持GET请求' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { startDate, endDate, limit } = req.query;
|
|
||||||
|
|
||||||
let pool;
|
|
||||||
try {
|
|
||||||
pool = createPool({
|
|
||||||
host: process.env.DB_HOST || '192.168.111.111',
|
|
||||||
port: parseInt(process.env.DB_PORT || '3306'),
|
|
||||||
user: process.env.DB_USER || 'root',
|
|
||||||
password: process.env.DB_PASSWORD || 'fullstack',
|
|
||||||
database: process.env.DB_NAME || 'iboard',
|
|
||||||
connectionLimit: 5
|
|
||||||
});
|
|
||||||
|
|
||||||
let query = 'SELECT date, close FROM econ_SH300Index';
|
|
||||||
const params = [];
|
|
||||||
const conditions = [];
|
|
||||||
|
|
||||||
if (startDate && endDate) {
|
|
||||||
conditions.push('date BETWEEN ? AND ?');
|
|
||||||
params.push(startDate, endDate);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (conditions.length > 0) {
|
|
||||||
query += ' WHERE ' + conditions.join(' AND ');
|
|
||||||
}
|
|
||||||
|
|
||||||
query += ' ORDER BY date DESC';
|
|
||||||
|
|
||||||
const result = await pool.query(query, params);
|
|
||||||
|
|
||||||
// 限制返回数量,默认为30
|
|
||||||
const limitValue = limit ? parseInt(limit) : 30;
|
|
||||||
const limitedResult = result.slice(0, limitValue);
|
|
||||||
|
|
||||||
// 反转顺序,使日期从早到晚
|
|
||||||
const sortedResult = limitedResult.reverse();
|
|
||||||
|
|
||||||
const data = sortedResult.map(row => {
|
|
||||||
let dateStr;
|
|
||||||
if (row.date instanceof Date) {
|
|
||||||
dateStr = row.date.toISOString().split('T')[0];
|
|
||||||
} else if (typeof row.date === 'string') {
|
|
||||||
dateStr = row.date;
|
|
||||||
} else {
|
|
||||||
dateStr = String(row.date);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
date: dateStr,
|
|
||||||
close: parseFloat(row.close)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
data: data
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取沪深300数据失败:', error);
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
message: '获取沪深300数据失败: ' + error.message
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
if (pool) {
|
|
||||||
await pool.end();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default handler;
|
|
||||||
-721
@@ -1,721 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react'
|
|
||||||
import Head from 'next/head'
|
|
||||||
import Navbar from '../components/Navbar'
|
|
||||||
import Footer from '../components/Footer'
|
|
||||||
import TimeRangePicker from '../components/TimeRangePicker'
|
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
|
||||||
|
|
||||||
const chinaBondData = [
|
|
||||||
{ month: '2024-01', oneYear: 1.85, fiveYear: 2.45, tenYear: 2.65 },
|
|
||||||
{ month: '2024-02', oneYear: 1.82, fiveYear: 2.42, tenYear: 2.62 },
|
|
||||||
{ month: '2024-03', oneYear: 1.78, fiveYear: 2.38, tenYear: 2.58 },
|
|
||||||
{ month: '2024-04', oneYear: 1.75, fiveYear: 2.35, tenYear: 2.55 },
|
|
||||||
{ month: '2024-05', oneYear: 1.72, fiveYear: 2.32, tenYear: 2.52 },
|
|
||||||
{ month: '2024-06', oneYear: 1.68, fiveYear: 2.28, tenYear: 2.48 },
|
|
||||||
{ month: '2024-07', oneYear: 1.65, fiveYear: 2.25, tenYear: 2.45 },
|
|
||||||
{ month: '2024-08', oneYear: 1.62, fiveYear: 2.22, tenYear: 2.42 },
|
|
||||||
{ month: '2024-09', oneYear: 1.58, fiveYear: 2.18, tenYear: 2.38 },
|
|
||||||
{ month: '2024-10', oneYear: 1.55, fiveYear: 2.15, tenYear: 2.35 },
|
|
||||||
{ month: '2024-11', oneYear: 1.52, fiveYear: 2.12, tenYear: 2.32 },
|
|
||||||
{ month: '2024-12', oneYear: 1.48, fiveYear: 2.08, tenYear: 2.28 }
|
|
||||||
]
|
|
||||||
|
|
||||||
const usBondData = [
|
|
||||||
{ month: '2024-01', oneYear: 4.85, fiveYear: 4.25, tenYear: 3.95 },
|
|
||||||
{ month: '2024-02', oneYear: 4.82, fiveYear: 4.22, tenYear: 3.92 },
|
|
||||||
{ month: '2024-03', oneYear: 4.78, fiveYear: 4.18, tenYear: 3.88 },
|
|
||||||
{ month: '2024-04', oneYear: 4.65, fiveYear: 4.05, tenYear: 3.75 },
|
|
||||||
{ month: '2024-05', oneYear: 4.55, fiveYear: 3.95, tenYear: 3.65 },
|
|
||||||
{ month: '2024-06', oneYear: 4.45, fiveYear: 3.85, tenYear: 3.55 },
|
|
||||||
{ month: '2024-07', oneYear: 4.35, fiveYear: 3.75, tenYear: 3.45 },
|
|
||||||
{ month: '2024-08', oneYear: 4.25, fiveYear: 3.65, tenYear: 3.35 },
|
|
||||||
{ month: '2024-09', oneYear: 4.15, fiveYear: 3.55, tenYear: 3.25 },
|
|
||||||
{ month: '2024-10', oneYear: 4.05, fiveYear: 3.45, tenYear: 3.15 },
|
|
||||||
{ month: '2024-11', oneYear: 3.95, fiveYear: 3.35, tenYear: 3.05 },
|
|
||||||
{ month: '2024-12', oneYear: 3.85, fiveYear: 3.25, tenYear: 2.95 }
|
|
||||||
]
|
|
||||||
|
|
||||||
const chinaLPRData = [
|
|
||||||
{ month: '2024-01', oneYear: 3.45, fiveYear: 3.95 },
|
|
||||||
{ month: '2024-02', oneYear: 3.45, fiveYear: 3.95 },
|
|
||||||
{ month: '2024-03', oneYear: 3.45, fiveYear: 3.95 },
|
|
||||||
{ month: '2024-04', oneYear: 3.40, fiveYear: 3.90 },
|
|
||||||
{ month: '2024-05', oneYear: 3.40, fiveYear: 3.90 },
|
|
||||||
{ month: '2024-06', oneYear: 3.40, fiveYear: 3.90 },
|
|
||||||
{ month: '2024-07', oneYear: 3.35, fiveYear: 3.85 },
|
|
||||||
{ month: '2024-08', oneYear: 3.35, fiveYear: 3.85 },
|
|
||||||
{ month: '2024-09', oneYear: 3.35, fiveYear: 3.85 },
|
|
||||||
{ month: '2024-10', oneYear: 3.30, fiveYear: 3.80 },
|
|
||||||
{ month: '2024-11', oneYear: 3.30, fiveYear: 3.80 },
|
|
||||||
{ month: '2024-12', oneYear: 3.30, fiveYear: 3.80 }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 中国黄金储备月度数据(单位:吨)
|
|
||||||
const chinaGoldReserveMonthly = [
|
|
||||||
{ month: '2024-01', reserves: 2245.4 },
|
|
||||||
{ month: '2024-02', reserves: 2255.2 },
|
|
||||||
{ month: '2024-03', reserves: 2267.5 },
|
|
||||||
{ month: '2024-04', reserves: 2276.9 },
|
|
||||||
{ month: '2024-05', reserves: 2285.3 },
|
|
||||||
{ month: '2024-06', reserves: 2296.1 },
|
|
||||||
{ month: '2024-07', reserves: 2305.8 },
|
|
||||||
{ month: '2024-08', reserves: 2316.2 },
|
|
||||||
{ month: '2024-09', reserves: 2325.7 },
|
|
||||||
{ month: '2024-10', reserves: 2336.5 },
|
|
||||||
{ month: '2024-11', reserves: 2345.9 },
|
|
||||||
{ month: '2024-12', reserves: 2356.3 }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 美国黄金储备月度数据(单位:吨)
|
|
||||||
const usGoldReserveMonthly = [
|
|
||||||
{ month: '2024-01', reserves: 8133.5 },
|
|
||||||
{ month: '2024-02', reserves: 8133.5 },
|
|
||||||
{ month: '2024-03', reserves: 8133.5 },
|
|
||||||
{ month: '2024-04', reserves: 8133.5 },
|
|
||||||
{ month: '2024-05', reserves: 8133.5 },
|
|
||||||
{ month: '2024-06', reserves: 8133.5 },
|
|
||||||
{ month: '2024-07', reserves: 8133.5 },
|
|
||||||
{ month: '2024-08', reserves: 8133.5 },
|
|
||||||
{ month: '2024-09', reserves: 8133.5 },
|
|
||||||
{ month: '2024-10', reserves: 8133.5 },
|
|
||||||
{ month: '2024-11', reserves: 8133.5 },
|
|
||||||
{ month: '2024-12', reserves: 8133.5 }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 中国黄金储备年度数据(单位:吨)
|
|
||||||
const chinaGoldReserveYearly = [
|
|
||||||
{ year: '2019', reserves: 1948.3 },
|
|
||||||
{ year: '2020', reserves: 1948.3 },
|
|
||||||
{ year: '2021', reserves: 1948.3 },
|
|
||||||
{ year: '2022', reserves: 1948.3 },
|
|
||||||
{ year: '2023', reserves: 2190.2 },
|
|
||||||
{ year: '2024', reserves: 2356.3 }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 美国黄金储备年度数据(单位:吨)
|
|
||||||
const usGoldReserveYearly = [
|
|
||||||
{ year: '2019', reserves: 8133.5 },
|
|
||||||
{ year: '2020', reserves: 8133.5 },
|
|
||||||
{ year: '2021', reserves: 8133.5 },
|
|
||||||
{ year: '2022', reserves: 8133.5 },
|
|
||||||
{ year: '2023', reserves: 8133.5 },
|
|
||||||
{ year: '2024', reserves: 8133.5 }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 猪肉价格数据(单位:元/公斤)
|
|
||||||
const porkPriceData = [
|
|
||||||
{ month: '2023-01', price: 24.5 },
|
|
||||||
{ month: '2023-02', price: 25.2 },
|
|
||||||
{ month: '2023-03', price: 26.8 },
|
|
||||||
{ month: '2023-04', price: 27.5 },
|
|
||||||
{ month: '2023-05', price: 28.3 },
|
|
||||||
{ month: '2023-06', price: 29.1 },
|
|
||||||
{ month: '2023-07', price: 28.5 },
|
|
||||||
{ month: '2023-08', price: 27.8 },
|
|
||||||
{ month: '2023-09', price: 26.5 },
|
|
||||||
{ month: '2023-10', price: 25.8 },
|
|
||||||
{ month: '2023-11', price: 25.2 },
|
|
||||||
{ month: '2023-12', price: 24.8 },
|
|
||||||
{ month: '2024-01', price: 24.5 },
|
|
||||||
{ month: '2024-02', price: 25.1 },
|
|
||||||
{ month: '2024-03', price: 25.8 },
|
|
||||||
{ month: '2024-04', price: 26.5 },
|
|
||||||
{ month: '2024-05', price: 27.2 },
|
|
||||||
{ month: '2024-06', price: 27.8 },
|
|
||||||
{ month: '2024-07', price: 28.5 },
|
|
||||||
{ month: '2024-08', price: 29.2 },
|
|
||||||
{ month: '2024-09', price: 28.8 },
|
|
||||||
{ month: '2024-10', price: 28.2 },
|
|
||||||
{ month: '2024-11', price: 27.5 },
|
|
||||||
{ month: '2024-12', price: 26.8 }
|
|
||||||
]
|
|
||||||
|
|
||||||
// 能繁母猪数量数据(单位:万头)
|
|
||||||
const sowInventoryData = [
|
|
||||||
{ month: '2023-01', inventory: 4321 },
|
|
||||||
{ month: '2023-02', inventory: 4356 },
|
|
||||||
{ month: '2023-03', inventory: 4389 },
|
|
||||||
{ month: '2023-04', inventory: 4423 },
|
|
||||||
{ month: '2023-05', inventory: 4456 },
|
|
||||||
{ month: '2023-06', inventory: 4489 },
|
|
||||||
{ month: '2023-07', inventory: 4523 },
|
|
||||||
{ month: '2023-08', inventory: 4556 },
|
|
||||||
{ month: '2023-09', inventory: 4589 },
|
|
||||||
{ month: '2023-10', inventory: 4623 },
|
|
||||||
{ month: '2023-11', inventory: 4656 },
|
|
||||||
{ month: '2023-12', inventory: 4689 },
|
|
||||||
{ month: '2024-01', inventory: 4723 },
|
|
||||||
{ month: '2024-02', inventory: 4756 },
|
|
||||||
{ month: '2024-03', inventory: 4789 },
|
|
||||||
{ month: '2024-04', inventory: 4823 },
|
|
||||||
{ month: '2024-05', inventory: 4856 },
|
|
||||||
{ month: '2024-06', inventory: 4889 },
|
|
||||||
{ month: '2024-07', inventory: 4923 },
|
|
||||||
{ month: '2024-08', inventory: 4956 },
|
|
||||||
{ month: '2024-09', inventory: 4989 },
|
|
||||||
{ month: '2024-10', inventory: 5023 },
|
|
||||||
{ month: '2024-11', inventory: 5056 },
|
|
||||||
{ month: '2024-12', inventory: 5089 }
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function Data() {
|
|
||||||
const [selectedIndicator, setSelectedIndicator] = useState('bond-china-treasury')
|
|
||||||
const [exchangeRateData, setExchangeRateData] = useState([])
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [error, setError] = useState(null)
|
|
||||||
const [timeRange, setTimeRange] = useState('1m') // 1m: 近一个月, 3m: 近三个月, 1y: 近一年, custom: 自定义
|
|
||||||
const [startDate, setStartDate] = useState('')
|
|
||||||
const [endDate, setEndDate] = useState('')
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedIndicator === 'central-bank-usd-cny-rate') {
|
|
||||||
fetchExchangeRateData()
|
|
||||||
}
|
|
||||||
}, [selectedIndicator, timeRange, startDate, endDate])
|
|
||||||
|
|
||||||
const handleTimeRangeChange = (range, start, end) => {
|
|
||||||
setTimeRange(range)
|
|
||||||
setStartDate(start)
|
|
||||||
setEndDate(end)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchExchangeRateData = async () => {
|
|
||||||
setLoading(true)
|
|
||||||
setError(null)
|
|
||||||
try {
|
|
||||||
let url = '/api/exchange-rate'
|
|
||||||
|
|
||||||
if (timeRange === 'custom' && startDate && endDate) {
|
|
||||||
url += `?startDate=${startDate}&endDate=${endDate}`
|
|
||||||
} else if (timeRange === '1m') {
|
|
||||||
url += '?limit=30'
|
|
||||||
} else if (timeRange === '3m') {
|
|
||||||
url += '?limit=90'
|
|
||||||
} else if (timeRange === '1y') {
|
|
||||||
url += '?limit=365'
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url)
|
|
||||||
const data = await response.json()
|
|
||||||
if (data.success) {
|
|
||||||
setExchangeRateData(data.data)
|
|
||||||
} else {
|
|
||||||
setError(data.message)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
setError('获取汇率数据失败')
|
|
||||||
console.error('获取汇率数据失败:', err)
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const indicators = [
|
|
||||||
{
|
|
||||||
group: '债券',
|
|
||||||
items: [
|
|
||||||
{ id: 'bond-china-treasury', name: '中国国债' },
|
|
||||||
{ id: 'bond-us-treasury', name: '美国国债' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: '贷款',
|
|
||||||
items: [
|
|
||||||
{ id: 'loan-china-lpr', name: '中国LPR' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: '央行数据',
|
|
||||||
items: [
|
|
||||||
{ id: 'central-bank-china-gold-monthly', name: '中国黄金储备(月)' },
|
|
||||||
{ id: 'central-bank-us-gold-monthly', name: '美国黄金储备(月)' },
|
|
||||||
{ id: 'central-bank-china-gold-yearly', name: '中国黄金储备(年)' },
|
|
||||||
{ id: 'central-bank-us-gold-yearly', name: '美国黄金储备(年)' },
|
|
||||||
{ id: 'central-bank-usd-cny-rate', name: '人民币兑美元汇率' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: '商品',
|
|
||||||
items: [
|
|
||||||
{ id: 'commodity-pork-price', name: '猪肉价格' },
|
|
||||||
{ id: 'commodity-sow-inventory', name: '能繁母猪数量' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container">
|
|
||||||
<Head>
|
|
||||||
<title>数据 - iBoard</title>
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
<Navbar />
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<h1 className="title">
|
|
||||||
经济数据
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="description">
|
|
||||||
各种经济数据和图表
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="data-layout">
|
|
||||||
<div className="data-sidebar">
|
|
||||||
{indicators.map((group, groupIndex) => (
|
|
||||||
<div key={groupIndex} className="indicator-group">
|
|
||||||
<h3 className="group-title">{group.group}</h3>
|
|
||||||
<ul className="indicator-list">
|
|
||||||
{group.items.map((item) => (
|
|
||||||
<li
|
|
||||||
key={item.id}
|
|
||||||
className={`indicator-item ${selectedIndicator === item.id ? 'active' : ''}`}
|
|
||||||
onClick={() => setSelectedIndicator(item.id)}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-content">
|
|
||||||
{selectedIndicator === 'bond-china-treasury' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">中国国债收益率走势</h2>
|
|
||||||
<p className="indicator-description">展示一年期、五年期、十年期国债收益率的变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={chinaBondData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="month" />
|
|
||||||
<YAxis domain={['dataMin - 0.2', 'dataMax + 0.2']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="oneYear" name="一年期" stroke="#8884d8" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
<Line type="monotone" dataKey="fiveYear" name="五年期" stroke="#82ca9d" strokeWidth={2} />
|
|
||||||
<Line type="monotone" dataKey="tenYear" name="十年期" stroke="#ffc658" strokeWidth={2} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>月份</th>
|
|
||||||
<th>一年期(%)</th>
|
|
||||||
<th>五年期(%)</th>
|
|
||||||
<th>十年期(%)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{chinaBondData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.month}</td>
|
|
||||||
<td>{row.oneYear.toFixed(2)}</td>
|
|
||||||
<td>{row.fiveYear.toFixed(2)}</td>
|
|
||||||
<td>{row.tenYear.toFixed(2)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'bond-us-treasury' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">美国国债收益率走势</h2>
|
|
||||||
<p className="indicator-description">展示一年期、五年期、十年期国债收益率的变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={usBondData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="month" />
|
|
||||||
<YAxis domain={['dataMin - 0.2', 'dataMax + 0.2']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="oneYear" name="一年期" stroke="#ff7300" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
<Line type="monotone" dataKey="fiveYear" name="五年期" stroke="#0088fe" strokeWidth={2} />
|
|
||||||
<Line type="monotone" dataKey="tenYear" name="十年期" stroke="#00C49F" strokeWidth={2} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>月份</th>
|
|
||||||
<th>一年期(%)</th>
|
|
||||||
<th>五年期(%)</th>
|
|
||||||
<th>十年期(%)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{usBondData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.month}</td>
|
|
||||||
<td>{row.oneYear.toFixed(2)}</td>
|
|
||||||
<td>{row.fiveYear.toFixed(2)}</td>
|
|
||||||
<td>{row.tenYear.toFixed(2)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'loan-china-lpr' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">中国LPR走势</h2>
|
|
||||||
<p className="indicator-description">展示一年期和五年期贷款市场报价利率(LPR)的变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={chinaLPRData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="month" />
|
|
||||||
<YAxis domain={['dataMin - 0.2', 'dataMax + 0.2']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="oneYear" name="一年期LPR" stroke="#e6194b" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
<Line type="monotone" dataKey="fiveYear" name="五年期LPR" stroke="#3cb44b" strokeWidth={2} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>月份</th>
|
|
||||||
<th>一年期LPR(%)</th>
|
|
||||||
<th>五年期LPR(%)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{chinaLPRData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.month}</td>
|
|
||||||
<td>{row.oneYear.toFixed(2)}</td>
|
|
||||||
<td>{row.fiveYear.toFixed(2)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'central-bank-china-gold-monthly' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">中国黄金储备(月度)</h2>
|
|
||||||
<p className="indicator-description">展示中国央行黄金储备的月度变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={chinaGoldReserveMonthly} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="month" />
|
|
||||||
<YAxis domain={['dataMin - 20', 'dataMax + 20']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="reserves" name="黄金储备(吨)" stroke="#ffd700" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>月份</th>
|
|
||||||
<th>黄金储备(吨)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{chinaGoldReserveMonthly.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.month}</td>
|
|
||||||
<td>{row.reserves.toFixed(1)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'central-bank-us-gold-monthly' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">美国黄金储备(月度)</h2>
|
|
||||||
<p className="indicator-description">展示美国央行黄金储备的月度变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={usGoldReserveMonthly} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="month" />
|
|
||||||
<YAxis domain={[8130, 8140]} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="reserves" name="黄金储备(吨)" stroke="#333333" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>月份</th>
|
|
||||||
<th>黄金储备(吨)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{usGoldReserveMonthly.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.month}</td>
|
|
||||||
<td>{row.reserves.toFixed(1)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'central-bank-china-gold-yearly' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">中国黄金储备(年度)</h2>
|
|
||||||
<p className="indicator-description">展示中国央行黄金储备的年度变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={chinaGoldReserveYearly} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 50', 'dataMax + 50']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="reserves" name="黄金储备(吨)" stroke="#ffd700" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>年份</th>
|
|
||||||
<th>黄金储备(吨)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{chinaGoldReserveYearly.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.year}</td>
|
|
||||||
<td>{row.reserves.toFixed(1)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'central-bank-us-gold-yearly' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">美国黄金储备(年度)</h2>
|
|
||||||
<p className="indicator-description">展示美国央行黄金储备的年度变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={usGoldReserveYearly} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={[8130, 8140]} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="reserves" name="黄金储备(吨)" stroke="#333333" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>年份</th>
|
|
||||||
<th>黄金储备(吨)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{usGoldReserveYearly.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.year}</td>
|
|
||||||
<td>{row.reserves.toFixed(1)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'central-bank-usd-cny-rate' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">人民币兑美元汇率</h2>
|
|
||||||
<p className="indicator-description">展示人民币兑美元汇率的变化趋势</p>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className="loading">加载中...</div>
|
|
||||||
) : error ? (
|
|
||||||
<div className="error">{error}</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="chart-header">
|
|
||||||
<TimeRangePicker
|
|
||||||
value={timeRange}
|
|
||||||
onChange={handleTimeRangeChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={exchangeRateData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="date" />
|
|
||||||
<YAxis domain={['dataMin - 0.1', 'dataMax + 0.1']} tickFormatter={(value) => value.toFixed(4)} />
|
|
||||||
<Tooltip formatter={(value) => [value.toFixed(4), '汇率']} />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="rate" name="汇率" stroke="#1e88e5" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>日期</th>
|
|
||||||
<th>汇率</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{exchangeRateData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.date}</td>
|
|
||||||
<td>{row.rate.toFixed(4)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'commodity-pork-price' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">猪肉价格</h2>
|
|
||||||
<p className="indicator-description">展示中国猪肉价格的月度变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={porkPriceData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="month" />
|
|
||||||
<YAxis domain={['dataMin - 1', 'dataMax + 1']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="price" name="猪肉价格(元/公斤)" stroke="#ff6b6b" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>月份</th>
|
|
||||||
<th>猪肉价格(元/公斤)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{porkPriceData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.month}</td>
|
|
||||||
<td>{row.price.toFixed(1)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedIndicator === 'commodity-sow-inventory' && (
|
|
||||||
<div className="indicator-detail">
|
|
||||||
<h2 className="indicator-title">能繁母猪数量</h2>
|
|
||||||
<p className="indicator-description">展示中国能繁母猪数量的月度变化趋势</p>
|
|
||||||
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={500}>
|
|
||||||
<LineChart data={sowInventoryData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="month" />
|
|
||||||
<YAxis domain={['dataMin - 50', 'dataMax + 50']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="inventory" name="能繁母猪数量(万头)" stroke="#4ecdc4" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>月份</th>
|
|
||||||
<th>能繁母猪数量(万头)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{sowInventoryData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.month}</td>
|
|
||||||
<td>{row.inventory}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,403 +0,0 @@
|
|||||||
import { useState, useEffect } from 'react'
|
|
||||||
import Head from 'next/head'
|
|
||||||
import Navbar from '../components/Navbar'
|
|
||||||
import Footer from '../components/Footer'
|
|
||||||
import TimeRangePicker from '../components/TimeRangePicker'
|
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
|
||||||
|
|
||||||
const usDebtData = [
|
|
||||||
{ year: '2019', debt: 22.7, gdp: 21.4, debtToGdp: 106.1, interestToGdp: 1.8 },
|
|
||||||
{ year: '2020', debt: 26.9, gdp: 20.9, debtToGdp: 128.7, interestToGdp: 1.6 },
|
|
||||||
{ year: '2021', debt: 28.4, gdp: 23.0, debtToGdp: 123.5, interestToGdp: 1.5 },
|
|
||||||
{ year: '2022', debt: 30.9, gdp: 25.4, debtToGdp: 121.6, interestToGdp: 1.6 },
|
|
||||||
{ year: '2023', debt: 33.1, gdp: 26.9, debtToGdp: 123.0, interestToGdp: 1.9 },
|
|
||||||
{ year: '2024', debt: 35.3, gdp: 28.5, debtToGdp: 123.9, interestToGdp: 2.1 },
|
|
||||||
{ year: '2025', debt: 37.5, gdp: 30.2, debtToGdp: 124.2, interestToGdp: 2.3 },
|
|
||||||
{ year: '2026', debt: 39.8, gdp: 31.9, debtToGdp: 124.8, interestToGdp: 2.5 }
|
|
||||||
]
|
|
||||||
|
|
||||||
const chinaDebtData = [
|
|
||||||
{ year: '2019', debt: 8.8, gdp: 14.3, debtToGdp: 61.5, interestToGdp: 1.9 },
|
|
||||||
{ year: '2020', debt: 10.3, gdp: 14.7, debtToGdp: 70.1, interestToGdp: 1.8 },
|
|
||||||
{ year: '2021', debt: 11.7, gdp: 16.1, debtToGdp: 72.7, interestToGdp: 1.7 },
|
|
||||||
{ year: '2022', debt: 13.1, gdp: 17.9, debtToGdp: 73.2, interestToGdp: 1.6 },
|
|
||||||
{ year: '2023', debt: 14.5, gdp: 19.4, debtToGdp: 74.7, interestToGdp: 1.6 },
|
|
||||||
{ year: '2024', debt: 15.9, gdp: 21.0, debtToGdp: 75.7, interestToGdp: 1.5 },
|
|
||||||
{ year: '2025', debt: 17.3, gdp: 22.8, debtToGdp: 75.9, interestToGdp: 1.5 },
|
|
||||||
{ year: '2026', debt: 18.8, gdp: 24.7, debtToGdp: 76.1, interestToGdp: 1.4 }
|
|
||||||
]
|
|
||||||
|
|
||||||
export default function Strategy() {
|
|
||||||
const [selectedStrategy, setSelectedStrategy] = useState('us-debt-gdp')
|
|
||||||
const [sh300ExchangeData, setSh300ExchangeData] = useState([])
|
|
||||||
const [sh300Loading, setSh300Loading] = useState(false)
|
|
||||||
const [timeRange, setTimeRange] = useState('1m')
|
|
||||||
const [startDate, setStartDate] = useState('')
|
|
||||||
const [endDate, setEndDate] = useState('')
|
|
||||||
|
|
||||||
const strategies = [
|
|
||||||
{
|
|
||||||
id: 'us-debt-gdp',
|
|
||||||
name: '美国政府债务/GDP'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'china-debt-gdp',
|
|
||||||
name: '中国政府债务/GDP'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sh300-exchange-rate',
|
|
||||||
name: '沪深300与汇率对比'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const handleTimeRangeChange = (range, start, end) => {
|
|
||||||
setTimeRange(range)
|
|
||||||
setStartDate(start)
|
|
||||||
setEndDate(end)
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchSh300ExchangeData = async () => {
|
|
||||||
console.log('fetchSh300ExchangeData called')
|
|
||||||
setSh300Loading(true)
|
|
||||||
try {
|
|
||||||
let sh300Url = '/api/sh300-index'
|
|
||||||
let exchangeUrl = '/api/exchange-rate'
|
|
||||||
|
|
||||||
const params = []
|
|
||||||
if (timeRange === 'custom' && startDate && endDate) {
|
|
||||||
params.push(`startDate=${startDate}`)
|
|
||||||
params.push(`endDate=${endDate}`)
|
|
||||||
} else if (timeRange === '1m') {
|
|
||||||
params.push('limit=30')
|
|
||||||
} else if (timeRange === '3m') {
|
|
||||||
params.push('limit=90')
|
|
||||||
} else if (timeRange === '1y') {
|
|
||||||
params.push('limit=365')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.length > 0) {
|
|
||||||
sh300Url += '?' + params.join('&')
|
|
||||||
exchangeUrl += '?' + params.join('&')
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Fetching from:', sh300Url, exchangeUrl)
|
|
||||||
|
|
||||||
const [sh300Res, exchangeRes] = await Promise.all([
|
|
||||||
fetch(sh300Url),
|
|
||||||
fetch(exchangeUrl)
|
|
||||||
])
|
|
||||||
console.log('Response status:', sh300Res.status, exchangeRes.status)
|
|
||||||
|
|
||||||
const sh300Json = await sh300Res.json()
|
|
||||||
const exchangeJson = await exchangeRes.json()
|
|
||||||
|
|
||||||
console.log('Response data:', sh300Json, exchangeJson)
|
|
||||||
|
|
||||||
if (sh300Json.success && exchangeJson.success) {
|
|
||||||
const sh300Map = new Map(sh300Json.data.map(item => [item.date, item.close]))
|
|
||||||
const exchangeMap = new Map(exchangeJson.data.map(item => [item.date, item.rate]))
|
|
||||||
|
|
||||||
const allDates = [...new Set([...sh300Map.keys(), ...exchangeMap.keys()])].sort()
|
|
||||||
|
|
||||||
const mergedData = allDates.map(date => ({
|
|
||||||
date,
|
|
||||||
sh300: sh300Map.get(date) || null,
|
|
||||||
exchangeRate: exchangeMap.get(date) || null
|
|
||||||
})).filter(item => item.sh300 !== null && item.exchangeRate !== null)
|
|
||||||
|
|
||||||
console.log('Merged data:', mergedData)
|
|
||||||
setSh300ExchangeData(mergedData)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取数据失败:', error)
|
|
||||||
} finally {
|
|
||||||
setSh300Loading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectedStrategy === 'sh300-exchange-rate') {
|
|
||||||
fetchSh300ExchangeData()
|
|
||||||
}
|
|
||||||
}, [selectedStrategy, timeRange, startDate, endDate])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container">
|
|
||||||
<Head>
|
|
||||||
<title>策略 - iBoard</title>
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
<Navbar />
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<h1 className="title">
|
|
||||||
经济策略
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="description">
|
|
||||||
按照经济理论组织的相关数据和图表的对比
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="strategy-layout">
|
|
||||||
<div className="strategy-sidebar">
|
|
||||||
<h3 className="sidebar-title">策略列表</h3>
|
|
||||||
<ul className="strategy-list">
|
|
||||||
{strategies.map((strategy) => (
|
|
||||||
<li
|
|
||||||
key={strategy.id}
|
|
||||||
className={`strategy-item ${selectedStrategy === strategy.id ? 'active' : ''}`}
|
|
||||||
onClick={() => setSelectedStrategy(strategy.id)}
|
|
||||||
>
|
|
||||||
{strategy.name}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="strategy-content">
|
|
||||||
{selectedStrategy === 'us-debt-gdp' && (
|
|
||||||
<div className="strategy-detail">
|
|
||||||
<h2 className="strategy-title">美国政府债务/GDP分析</h2>
|
|
||||||
<p className="strategy-description">
|
|
||||||
分析美国政府债务、GDP、债务/GDP比率以及债务利息/GDP比率的走势
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">美国政府债务走势</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={usDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 2', 'dataMax + 2']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="debt" name="美国政府债务(万亿美元)" stroke="#8884d8" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">美国GDP走势</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={usDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 1', 'dataMax + 1']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="gdp" name="美国GDP(万亿美元)" stroke="#82ca9d" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">美国政府债务/GDP走势</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={usDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 5', 'dataMax + 5']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="debtToGdp" name="债务/GDP比率(%)" stroke="#ffc658" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">美国政府债务利息/GDP</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={usDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 0.2', 'dataMax + 0.2']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="interestToGdp" name="债务利息/GDP比率(%)" stroke="#ff7300" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>年份</th>
|
|
||||||
<th>政府债务(万亿美元)</th>
|
|
||||||
<th>GDP(万亿美元)</th>
|
|
||||||
<th>债务/GDP(%)</th>
|
|
||||||
<th>债务利息/GDP(%)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{usDebtData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.year}</td>
|
|
||||||
<td>{row.debt.toFixed(1)}</td>
|
|
||||||
<td>{row.gdp.toFixed(1)}</td>
|
|
||||||
<td>{row.debtToGdp.toFixed(1)}</td>
|
|
||||||
<td>{row.interestToGdp.toFixed(1)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedStrategy === 'china-debt-gdp' && (
|
|
||||||
<div className="strategy-detail">
|
|
||||||
<h2 className="strategy-title">中国政府债务/GDP分析</h2>
|
|
||||||
<p className="strategy-description">
|
|
||||||
分析中国政府债务、GDP、债务/GDP比率以及债务利息/GDP比率的走势
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">中国政府债务走势</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={chinaDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 1', 'dataMax + 1']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="debt" name="中国政府债务(万亿美元)" stroke="#e6194b" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">中国GDP走势</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={chinaDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 1', 'dataMax + 1']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="gdp" name="中国GDP(万亿美元)" stroke="#3cb44b" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">中国政府债务/GDP走势</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={chinaDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 5', 'dataMax + 5']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="debtToGdp" name="债务/GDP比率(%)" stroke="#4363d8" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">中国政府债务利息/GDP</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={chinaDebtData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" />
|
|
||||||
<YAxis domain={['dataMin - 0.2', 'dataMax + 0.2']} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="interestToGdp" name="债务利息/GDP比率(%)" stroke="#f58231" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="data-table-container">
|
|
||||||
<h3 className="table-title">详细数据</h3>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>年份</th>
|
|
||||||
<th>政府债务(万亿美元)</th>
|
|
||||||
<th>GDP(万亿美元)</th>
|
|
||||||
<th>债务/GDP(%)</th>
|
|
||||||
<th>债务利息/GDP(%)</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{chinaDebtData.map((row, index) => (
|
|
||||||
<tr key={index}>
|
|
||||||
<td>{row.year}</td>
|
|
||||||
<td>{row.debt.toFixed(1)}</td>
|
|
||||||
<td>{row.gdp.toFixed(1)}</td>
|
|
||||||
<td>{row.debtToGdp.toFixed(1)}</td>
|
|
||||||
<td>{row.interestToGdp.toFixed(1)}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedStrategy === 'sh300-exchange-rate' && (
|
|
||||||
<div className="strategy-detail">
|
|
||||||
<h2 className="strategy-title">沪深300与汇率对比分析</h2>
|
|
||||||
<p className="strategy-description">
|
|
||||||
对比沪深300指数收盘价与人民币兑美元汇率的走势关系
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{sh300Loading ? (
|
|
||||||
<div className="loading">加载中...</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="chart-header">
|
|
||||||
<TimeRangePicker
|
|
||||||
value={timeRange}
|
|
||||||
onChange={handleTimeRangeChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="chart-section">
|
|
||||||
<h3 className="chart-title">沪深300与汇率对比(双Y轴)</h3>
|
|
||||||
<div className="chart-container">
|
|
||||||
<ResponsiveContainer width="100%" height={400}>
|
|
||||||
<LineChart data={sh300ExchangeData} margin={{ top: 20, right: 60, left: 20, bottom: 5 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="date" />
|
|
||||||
<YAxis yAxisId="left" domain={['auto', 'auto']} allowDataOverflow={true} />
|
|
||||||
<YAxis yAxisId="right" orientation="right" domain={['auto', 'auto']} allowDataOverflow={true} tickFormatter={(value) => value.toFixed(4)} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line yAxisId="left" type="monotone" dataKey="sh300" name="沪深300收盘价" stroke="#8884d8" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
<Line yAxisId="right" type="monotone" dataKey="exchangeRate" name="汇率" stroke="#82ca9d" strokeWidth={2} activeDot={{ r: 8 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<Footer />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
-- 经济指标: 明细数据拆到独立表, 兼容年/季/月/日频率
|
||||||
|
-- 第一步: 建 IndicatorPoint 表 + Indicator 加元数据列 (data 列暂保留, 由迁移脚本搬运后删除)
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `IndicatorPoint` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`indicatorId` INTEGER NOT NULL,
|
||||||
|
`period` VARCHAR(191) NOT NULL,
|
||||||
|
`value` DOUBLE NOT NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE INDEX `IndicatorPoint_indicatorId_period_key`(`indicatorId`, `period`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- AlterTable: 元数据列 (data JSON 拆出)
|
||||||
|
ALTER TABLE `Indicator` ADD COLUMN `title` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN `unit` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN `source` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
ADD COLUMN `frequency` VARCHAR(191) NOT NULL DEFAULT 'month';
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `IndicatorPoint` ADD CONSTRAINT `IndicatorPoint_indicatorId_fkey` FOREIGN KEY (`indicatorId`) REFERENCES `Indicator`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- 第二步: 明细数据已迁移到 IndicatorPoint, 删除 Indicator.data JSON 列
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Indicator` DROP COLUMN `data`;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- 指标隐藏标记: 数据与获取脚本保留, 前端展示过滤 hidden=true
|
||||||
|
ALTER TABLE `Indicator` ADD COLUMN `hidden` BOOLEAN NOT NULL DEFAULT false;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `News` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`title` VARCHAR(191) NOT NULL,
|
||||||
|
`summary` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
`source` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
`url` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
`publishedAt` DATETIME(3) NOT NULL,
|
||||||
|
`impactRank` INTEGER NOT NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
|
||||||
|
UNIQUE INDEX `News_title_publishedAt_key`(`title`, `publishedAt`),
|
||||||
|
INDEX `News_publishedAt_idx`(`publishedAt`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `Alert` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`indicatorName` VARCHAR(191) NOT NULL,
|
||||||
|
`period` VARCHAR(191) NOT NULL,
|
||||||
|
`valueA` DOUBLE NOT NULL,
|
||||||
|
`valueB` DOUBLE NOT NULL,
|
||||||
|
`sourceA` VARCHAR(191) NOT NULL,
|
||||||
|
`sourceB` VARCHAR(191) NOT NULL,
|
||||||
|
`diff` DOUBLE NOT NULL,
|
||||||
|
`diffRatio` DOUBLE NOT NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
|
||||||
|
INDEX `Alert_createdAt_idx`(`createdAt`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
-- 服务器监控 + 服务健康检查
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `ServerNode` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`uuid` VARCHAR(191) NOT NULL,
|
||||||
|
`name` VARCHAR(191) NOT NULL,
|
||||||
|
`cpuName` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
`os` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
`region` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
`memTotal` BIGINT NOT NULL DEFAULT 0,
|
||||||
|
`diskTotal` BIGINT NOT NULL DEFAULT 0,
|
||||||
|
`cpuCores` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`cpuUsage` DOUBLE NOT NULL DEFAULT 0,
|
||||||
|
`ramUsed` BIGINT NOT NULL DEFAULT 0,
|
||||||
|
`swapUsed` BIGINT NOT NULL DEFAULT 0,
|
||||||
|
`load1` DOUBLE NOT NULL DEFAULT 0,
|
||||||
|
`load5` DOUBLE NOT NULL DEFAULT 0,
|
||||||
|
`load15` DOUBLE NOT NULL DEFAULT 0,
|
||||||
|
`diskUsed` BIGINT NOT NULL DEFAULT 0,
|
||||||
|
`netIn` BIGINT NOT NULL DEFAULT 0,
|
||||||
|
`netOut` BIGINT NOT NULL DEFAULT 0,
|
||||||
|
`uptime` BIGINT NOT NULL DEFAULT 0,
|
||||||
|
`process` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`connections` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`status` VARCHAR(191) NOT NULL DEFAULT 'unknown',
|
||||||
|
`recordedAt` DATETIME(3) NOT NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE INDEX `ServerNode_uuid_key`(`uuid`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `ServerNodeHistory` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`uuid` VARCHAR(191) NOT NULL,
|
||||||
|
`period` DATETIME(3) NOT NULL,
|
||||||
|
`cpu` DOUBLE NOT NULL DEFAULT 0,
|
||||||
|
`ram` BIGINT NOT NULL DEFAULT 0,
|
||||||
|
`load` DOUBLE NOT NULL DEFAULT 0,
|
||||||
|
`disk` BIGINT NOT NULL DEFAULT 0,
|
||||||
|
`netIn` BIGINT NOT NULL DEFAULT 0,
|
||||||
|
`netOut` BIGINT NOT NULL DEFAULT 0,
|
||||||
|
`process` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`connections` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
|
||||||
|
UNIQUE INDEX `ServerNodeHistory_uuid_period_key`(`uuid`, `period`),
|
||||||
|
INDEX `ServerNodeHistory_uuid_period_idx`(`uuid`, `period`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE `ServiceCheck` (
|
||||||
|
`id` INTEGER NOT NULL AUTO_INCREMENT,
|
||||||
|
`name` VARCHAR(191) NOT NULL,
|
||||||
|
`url` VARCHAR(191) NOT NULL,
|
||||||
|
`status` VARCHAR(191) NOT NULL DEFAULT 'unknown',
|
||||||
|
`latencyMs` INTEGER NOT NULL DEFAULT 0,
|
||||||
|
`message` VARCHAR(191) NOT NULL DEFAULT '',
|
||||||
|
`lastCheckAt` DATETIME(3) NOT NULL,
|
||||||
|
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
|
`updatedAt` DATETIME(3) NOT NULL,
|
||||||
|
|
||||||
|
UNIQUE INDEX `ServiceCheck_name_key`(`name`),
|
||||||
|
PRIMARY KEY (`id`)
|
||||||
|
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable (news.content 全文, 须在 news_and_alerts 建表之后执行)
|
||||||
|
ALTER TABLE `News` ADD COLUMN `content` TEXT NULL;
|
||||||
+119
-4
@@ -41,12 +41,125 @@ model Strategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model Indicator {
|
model Indicator {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String @unique
|
||||||
groupName String
|
groupName String
|
||||||
data Json
|
title String @default("")
|
||||||
|
unit String @default("")
|
||||||
|
source String @default("")
|
||||||
|
frequency String @default("month") // year | quarter | month | day
|
||||||
|
hidden Boolean @default(false) // true = 数据/脚本保留, 但不在前端展示
|
||||||
|
points IndicatorPoint[]
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
model IndicatorPoint {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
indicatorId Int
|
||||||
|
indicator Indicator @relation(fields: [indicatorId], references: [id], onDelete: Cascade)
|
||||||
|
period String // "2024" | "2024-Q1" | "2024-01" | "2024-01-15"
|
||||||
|
value Float
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
@@unique([indicatorId, period])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 新闻: 每天 3 点由 mx_finance_search_news 拉取, 按影响大小排序, 累加多日供首页无限滚动
|
||||||
|
model News {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
title String
|
||||||
|
summary String @default("")
|
||||||
|
content String? @db.Text
|
||||||
|
source String @default("")
|
||||||
|
url String @default("")
|
||||||
|
publishedAt DateTime
|
||||||
|
impactRank Int
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
|
||||||
|
@@unique([title, publishedAt])
|
||||||
|
@@index([publishedAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 报警: refresh 写库时比对同 (indicatorName, period) 的跨源数据, 差异超阈值记录
|
||||||
|
model Alert {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
indicatorName String
|
||||||
|
period String
|
||||||
|
valueA Float
|
||||||
|
valueB Float
|
||||||
|
sourceA String
|
||||||
|
sourceB String
|
||||||
|
diff Float
|
||||||
|
diffRatio Float
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@index([createdAt])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 服务器监控: 对接 komari 公开 API
|
||||||
|
model ServerNode {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
uuid String @unique
|
||||||
|
name String
|
||||||
|
cpuName String @default("")
|
||||||
|
os String @default("")
|
||||||
|
region String @default("")
|
||||||
|
memTotal BigInt @default(0)
|
||||||
|
diskTotal BigInt @default(0)
|
||||||
|
cpuCores Int @default(0)
|
||||||
|
// 最新实时快照
|
||||||
|
cpuUsage Float @default(0)
|
||||||
|
ramUsed BigInt @default(0)
|
||||||
|
swapUsed BigInt @default(0)
|
||||||
|
load1 Float @default(0)
|
||||||
|
load5 Float @default(0)
|
||||||
|
load15 Float @default(0)
|
||||||
|
diskUsed BigInt @default(0)
|
||||||
|
netIn BigInt @default(0)
|
||||||
|
netOut BigInt @default(0)
|
||||||
|
uptime BigInt @default(0)
|
||||||
|
process Int @default(0)
|
||||||
|
connections Int @default(0)
|
||||||
|
status String @default("unknown") // online | offline | unknown
|
||||||
|
recordedAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时序历史: 每节点每分钟一条 (komari records 1 分钟粒度)
|
||||||
|
model ServerNodeHistory {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
uuid String
|
||||||
|
period DateTime
|
||||||
|
cpu Float @default(0)
|
||||||
|
ram BigInt @default(0)
|
||||||
|
load Float @default(0)
|
||||||
|
disk BigInt @default(0)
|
||||||
|
netIn BigInt @default(0)
|
||||||
|
netOut BigInt @default(0)
|
||||||
|
process Int @default(0)
|
||||||
|
connections Int @default(0)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([uuid, period])
|
||||||
|
@@index([uuid, period])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务健康检查: 本应用自检 (健康端点 / DB / 第三方)
|
||||||
|
model ServiceCheck {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
name String @unique
|
||||||
|
url String
|
||||||
|
status String @default("unknown") // up | down | unknown
|
||||||
|
latencyMs Int @default(0)
|
||||||
|
message String @default("")
|
||||||
|
lastCheckAt DateTime
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model Econ_SowInventory {
|
model Econ_SowInventory {
|
||||||
@@ -66,3 +179,5 @@ model ScraperCache {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,3 +4,21 @@
|
|||||||
- 数据主要是放各种经济数据和图表。
|
- 数据主要是放各种经济数据和图表。
|
||||||
- 策略是放按照经济理论组织的相关数据和图表的对比。
|
- 策略是放按照经济理论组织的相关数据和图表的对比。
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 首页
|
||||||
|
首页显示新闻模块和报警模块的数据,按照时间最新优先,无限滚动,直到数据库中没有数据为止。
|
||||||
|
|
||||||
|
|
||||||
|
### 新闻模块
|
||||||
|
增加一个新闻模块,数据来源是每日夜间 3点,通过妙想 MCP 获取国内外最新经济政治新闻,按影响大小排序10条。
|
||||||
|
存入数据库。
|
||||||
|
|
||||||
|
|
||||||
|
### 报警模块
|
||||||
|
World Bank 中的数据只用来对比妙想 MCP 拉取的数据正确性。当同一时间的同一项数据不同时,记录一个报警。
|
||||||
|
|
||||||
|
## 监控
|
||||||
|
监控分为服务器监控和服务监控两部分。
|
||||||
|
服务器监控对接 komari,地址:https://komari.honor3.com/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
const fs = require('fs')
|
||||||
|
const content = `import cron from 'node-cron'
|
||||||
|
import { prisma } from './db.js'
|
||||||
|
import mysql from 'mysql2/promise'
|
||||||
|
import { fetchNewsList, persistNews } from '../scripts/fetchers/news.fetcher.js'
|
||||||
|
import { checkAfterRefresh } from './alert-engine.js'
|
||||||
|
|
||||||
|
function parseUrl(url) {
|
||||||
|
const u = new URL(url)
|
||||||
|
return { host: u.hostname, port: Number(u.port) || 3306, user: decodeURIComponent(u.username), password: decodeURIComponent(u.password), database: u.pathname.replace(/^\\//, '') }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openConn() {
|
||||||
|
return mysql.createConnection(parseUrl(process.env.DATABASE_URL))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每天凌晨 3:00 (本地时间) 拉取新闻 + 跑跨源对比
|
||||||
|
function scheduleNews() {
|
||||||
|
cron.schedule('0 3 * * *', async () => {
|
||||||
|
console.log('[cron] 3:00 触发')
|
||||||
|
try {
|
||||||
|
const list = await fetchNewsList()
|
||||||
|
const r = await persistNews(list)
|
||||||
|
console.log('[cron] 新闻入库: 新增 ' + r.inserted + ' / 跳过 ' + r.skipped)
|
||||||
|
const conn = await openConn()
|
||||||
|
try {
|
||||||
|
const a = await checkAfterRefresh(conn)
|
||||||
|
console.log('[cron] 报警检测: 新增 ' + a.inserted + ' 条 (扫描 ' + a.scanned + ')')
|
||||||
|
} finally { await conn.end() }
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[cron] 任务失败:', e.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('[cron] 已注册: 每天 03:00 拉取新闻 + 跨源报警检测')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每 2 分钟: 拉取 komari 节点 + 服务自检
|
||||||
|
function scheduleMonitors() {
|
||||||
|
cron.schedule('*/2 * * * *', async () => {
|
||||||
|
try {
|
||||||
|
const k = require('./komari-monitor.cjs')
|
||||||
|
const r = await k.runOnce()
|
||||||
|
console.log('[cron] komari: nodes=' + r.nodes + ' snapshots=' + r.snapshots + ' history=' + r.history + ' failed=' + r.failed)
|
||||||
|
} catch (e) { console.error('[cron] komari 任务失败:', e.message) }
|
||||||
|
try {
|
||||||
|
const h = require('./health-check.cjs')
|
||||||
|
const arr = await h.runOnce()
|
||||||
|
const down = arr.filter(x => x.status !== 'up').map(x => x.name)
|
||||||
|
console.log('[cron] health: ' + arr.length + ' 项, down=' + (down.length ? down.join(',') : '无'))
|
||||||
|
} catch (e) { console.error('[cron] health 任务失败:', e.message) }
|
||||||
|
})
|
||||||
|
console.log('[cron] 已注册: 每 2 分钟 komari + 服务自检')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每小时: 清理 24h 前的 komari 历史
|
||||||
|
function scheduleMonitorCleanup() {
|
||||||
|
cron.schedule('0 * * * *', async () => {
|
||||||
|
try {
|
||||||
|
const k = require('./komari-monitor.cjs')
|
||||||
|
const n = await k.cleanupOld()
|
||||||
|
console.log('[cron] 监控历史清理: 删除 ' + n + ' 条')
|
||||||
|
} catch (e) { console.error('[cron] 监控清理失败:', e.message) }
|
||||||
|
})
|
||||||
|
console.log('[cron] 已注册: 每小时清理 24h 前监控历史')
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleNews()
|
||||||
|
scheduleMonitors()
|
||||||
|
scheduleMonitorCleanup()
|
||||||
|
`
|
||||||
|
fs.writeFileSync('server/cron.js', content, 'utf8')
|
||||||
|
console.log('OK len=' + content.length)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
require('dotenv').config()
|
||||||
|
const axios = require('axios')
|
||||||
|
|
||||||
|
async function call(tool, query) {
|
||||||
|
const base = process.env.EASTMONEY_MCP_BASE || 'https://mxapi.eastmoney.com/mxds/mcp'
|
||||||
|
const payload = {
|
||||||
|
jsonrpc: '2.0', id: 1, method: 'tools/call',
|
||||||
|
params: { name: tool, arguments: { query } },
|
||||||
|
}
|
||||||
|
const resp = await axios.post(base, payload, {
|
||||||
|
timeout: 60000,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json, text/event-stream',
|
||||||
|
'em_api_key': process.env.EM_API_KEY,
|
||||||
|
},
|
||||||
|
validateStatus: () => true,
|
||||||
|
responseType: 'text',
|
||||||
|
transformResponse: [(d) => d],
|
||||||
|
})
|
||||||
|
const body = String(resp.data)
|
||||||
|
try {
|
||||||
|
const j = JSON.parse(body)
|
||||||
|
const text = j.result.content[0].text
|
||||||
|
const parsed = JSON.parse(text)
|
||||||
|
console.log(`\n=== ${tool}: "${query}" ===`)
|
||||||
|
console.log('sheets 数量:', parsed.data.length)
|
||||||
|
parsed.data.forEach((s, i) => {
|
||||||
|
console.log(`\nsheet[${i}]:`)
|
||||||
|
console.log(' sheetName:', s.sheetName)
|
||||||
|
console.log(' columns[0..3]:', s.columns.slice(0, 4))
|
||||||
|
console.log(' items[0][0..3]:', s.items[0] ? s.items[0].slice(0, 4) : '(无)')
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
console.log('解析失败:', e.message)
|
||||||
|
console.log(body.slice(0, 1000))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await call('mx_macro_data', '查询中国 2020-2025 年 CPI 月度同比数据')
|
||||||
|
await call('mx_macro_data', '查询中国 2015-2024 年 GDP 同比增速年度数据')
|
||||||
|
}
|
||||||
|
main().catch(console.error)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 列出东方财富妙想 MCP 暴露的所有工具
|
||||||
|
* 用法: npm run data:discover
|
||||||
|
* 输出: tool 名称 + 描述 + 输入 schema
|
||||||
|
*/
|
||||||
|
require('dotenv').config()
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (!process.env.EM_API_KEY) {
|
||||||
|
console.error('缺少 EM_API_KEY (在 .env 配)')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
const { listAvailableTools } = await import('./fetchers/eastmoney-mcp.fetcher.js')
|
||||||
|
console.log('[Discover] 调妙想 MCP tools/list...')
|
||||||
|
const tools = await listAvailableTools()
|
||||||
|
console.log(`[Discover] 找到 ${tools.length} 个工具\n`)
|
||||||
|
for (const t of tools) {
|
||||||
|
console.log(`── ${t.name} ${'─'.repeat(Math.max(0, 60 - t.name.length))}`)
|
||||||
|
if (t.description) console.log(` ${t.description}`)
|
||||||
|
if (t.inputSchema) {
|
||||||
|
console.log(' inputSchema:')
|
||||||
|
const s = JSON.stringify(t.inputSchema, null, 2)
|
||||||
|
s.split('\n').forEach((line) => console.log(' ' + line))
|
||||||
|
}
|
||||||
|
console.log('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error('[Discover] 失败:', e.message)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
// 东方财富妙想 MCP 数据源
|
||||||
|
// 协议: StreamableHttp (MCP 2025-03-26 spec), HTTP POST + JSON-RPC 2.0
|
||||||
|
// - 认证: header `em_api_key: <EM_API_KEY>`
|
||||||
|
// - 妙想 MCP 是"自然语言驱动": 传 query 字符串, 内部 LLM 解析 + 调底层数据接口
|
||||||
|
// - 重要: 每次调用 5-20s + LLM token 成本, 必须配合"DB 优先" (refresh 时已存在的指标跳过)
|
||||||
|
//
|
||||||
|
// 需要的 .env:
|
||||||
|
// EM_API_KEY=xxx 必填
|
||||||
|
// EASTMONEY_MCP_BASE=... 可选, 默认 https://mxapi.eastmoney.com/mxds/mcp
|
||||||
|
//
|
||||||
|
// INDICATORS 数组: 配置要拉的指标
|
||||||
|
// 每项:
|
||||||
|
// name → Indicator.name (主键)
|
||||||
|
// groupName → Indicator.groupName
|
||||||
|
// title → data.title
|
||||||
|
// unit → data.unit
|
||||||
|
// tool → MCP tool 名
|
||||||
|
// query → 自然语言查询
|
||||||
|
// pickSheet(sheets) → 选一张 sheet (默认取第一张)
|
||||||
|
// parseSheet(sheet) → [{period, value}, ...] 默认按宽表转长表 (列=日期, 行=值)
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
const DEFAULT_BASE = 'https://mxapi.eastmoney.com/mxds/mcp'
|
||||||
|
const TIMEOUT = 60000
|
||||||
|
|
||||||
|
// ====== INDICATORS 配置 ======
|
||||||
|
// 跑 npm run data:discover 看可用 tool, 再加新指标
|
||||||
|
const INDICATORS = [
|
||||||
|
{
|
||||||
|
name: 'china-cpi-yoy',
|
||||||
|
groupName: 'china-macro',
|
||||||
|
title: '中国 CPI 同比',
|
||||||
|
unit: '%',
|
||||||
|
tool: 'mx_macro_data',
|
||||||
|
query: '查询中国 2015 年至今 CPI 月度同比数据,每一个月的数值',
|
||||||
|
pickSheet: (sheets) =>
|
||||||
|
sheets.find((s) => s.items?.[0]?.[0]?.includes('CPI') && s.items[0][0].includes('同比'))
|
||||||
|
|| sheets.find((s) => s.sheetName?.includes('宏观数据'))
|
||||||
|
|| sheets[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'china-gdp-yoy',
|
||||||
|
groupName: 'china-macro',
|
||||||
|
title: '中国 GDP 同比',
|
||||||
|
unit: '%',
|
||||||
|
tool: 'mx_macro_data',
|
||||||
|
query: '查询中国 2015-2025 年 GDP 不变价同比增速,季度数据,每一季度的数值',
|
||||||
|
pickSheet: (sheets) =>
|
||||||
|
sheets.find((s) => s.items?.[0]?.[0]?.includes('GDP') && s.items[0][0].includes('同比'))
|
||||||
|
|| sheets[0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// 国债收益率: MCP 返回日频, 用月末降采样成月频点
|
||||||
|
name: 'china-bond-10y',
|
||||||
|
groupName: 'china-macro',
|
||||||
|
title: '中国国债 10 年期收益率',
|
||||||
|
unit: '%',
|
||||||
|
frequency: 'month',
|
||||||
|
tool: 'mx_macro_data',
|
||||||
|
query: '查询中国10年期国债到期收益率 2024年至今 每日数据',
|
||||||
|
pickSheet: (sheets) =>
|
||||||
|
sheets.find((s) => s.items?.some?.((row) => String(row[0] || '').includes('国债到期收益率:10年')))
|
||||||
|
|| sheets[0],
|
||||||
|
parseSheet: (sheet) => monthEndSample(sheet, '国债到期收益率:10年'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'china-bond-1y',
|
||||||
|
groupName: 'china-macro',
|
||||||
|
title: '中国国债 1 年期收益率',
|
||||||
|
unit: '%',
|
||||||
|
frequency: 'month',
|
||||||
|
tool: 'mx_macro_data',
|
||||||
|
query: '查询中国1年期国债到期收益率 2024年至今 每日数据',
|
||||||
|
pickSheet: (sheets) =>
|
||||||
|
sheets.find((s) => s.items?.some?.((row) => String(row[0] || '').includes('国债到期收益率:1年')))
|
||||||
|
|| sheets[0],
|
||||||
|
parseSheet: (sheet) => monthEndSample(sheet, '国债到期收益率:1年'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'china-lpr-1y',
|
||||||
|
groupName: 'china-macro',
|
||||||
|
title: '中国 LPR 1 年期',
|
||||||
|
unit: '%',
|
||||||
|
frequency: 'month',
|
||||||
|
tool: 'mx_macro_data',
|
||||||
|
query: '查询中国LPR 1年期贷款市场报价利率 2024年至今 月度数据,每一个月的数值',
|
||||||
|
pickSheet: (sheets) =>
|
||||||
|
sheets.find((s) => s.sheetName?.includes('宏观数据(月)'))
|
||||||
|
|| sheets[0],
|
||||||
|
parseSheet: (sheet) => pickRowByKeyword(sheet, '(LPR):1年'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'china-lpr-5y',
|
||||||
|
groupName: 'china-macro',
|
||||||
|
title: '中国 LPR 5 年期',
|
||||||
|
unit: '%',
|
||||||
|
frequency: 'month',
|
||||||
|
tool: 'mx_macro_data',
|
||||||
|
query: '查询中国LPR 5年期以上贷款市场报价利率 2024年至今 月度数据,每一个月的数值',
|
||||||
|
pickSheet: (sheets) =>
|
||||||
|
sheets.find((s) => s.sheetName?.includes('宏观数据(月)'))
|
||||||
|
|| sheets[0],
|
||||||
|
parseSheet: (sheet) => pickRowByKeyword(sheet, '(LPR):5年'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const META = {
|
||||||
|
name: 'eastmoney-mcp',
|
||||||
|
description: '东方财富妙想 MCP (StreamableHttp, NL 查询)',
|
||||||
|
docs: 'https://mxapi.eastmoney.com/mxds/doc/mcp-install.md',
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== StreamableHttp 客户端 ======
|
||||||
|
|
||||||
|
class McpClient {
|
||||||
|
constructor(baseUrl, apiKey) {
|
||||||
|
this.baseUrl = baseUrl
|
||||||
|
this.apiKey = apiKey
|
||||||
|
this.id = 0
|
||||||
|
}
|
||||||
|
async _request(method, params) {
|
||||||
|
const payload = { jsonrpc: '2.0', id: ++this.id, method, ...(params ? { params } : {}) }
|
||||||
|
const resp = await axios.post(this.baseUrl, payload, {
|
||||||
|
timeout: TIMEOUT,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json, text/event-stream',
|
||||||
|
'em_api_key': this.apiKey,
|
||||||
|
},
|
||||||
|
responseType: 'text',
|
||||||
|
transformResponse: [(d) => d],
|
||||||
|
validateStatus: () => true,
|
||||||
|
})
|
||||||
|
if (resp.status >= 400) {
|
||||||
|
throw new Error(`HTTP ${resp.status}: ${String(resp.data).slice(0, 300)}`)
|
||||||
|
}
|
||||||
|
const parsed = parseMcpResponse(String(resp.data || ''))
|
||||||
|
// JSON-RPC 信封 {jsonrpc,id,result|error}, 统一解包 result
|
||||||
|
if (parsed.error) {
|
||||||
|
throw new Error(`MCP error ${parsed.error.code}: ${parsed.error.message}`)
|
||||||
|
}
|
||||||
|
return parsed.result || parsed
|
||||||
|
}
|
||||||
|
async listTools() {
|
||||||
|
const r = await this._request('tools/list')
|
||||||
|
return r.tools || []
|
||||||
|
}
|
||||||
|
async callTool(name, args = {}) {
|
||||||
|
const r = await this._request('tools/call', { name, arguments: args })
|
||||||
|
if (r.isError) {
|
||||||
|
const msg = (r.content || []).map((c) => c.text || '').join('; ')
|
||||||
|
throw new Error(`tool ${name} error: ${msg || JSON.stringify(r).slice(0, 200)}`)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应: JSON 或 SSE
|
||||||
|
function parseMcpResponse(text) {
|
||||||
|
const t = text.trim()
|
||||||
|
if (t.startsWith('{')) return JSON.parse(t)
|
||||||
|
if (t.startsWith('data:')) {
|
||||||
|
const lines = t.split('\n').filter((l) => l.startsWith('data:'))
|
||||||
|
let last = null
|
||||||
|
for (const line of lines) {
|
||||||
|
const json = line.slice(5).trim()
|
||||||
|
if (!json || json === '[DONE]') continue
|
||||||
|
try { last = JSON.parse(json) } catch { /* skip */ }
|
||||||
|
}
|
||||||
|
if (!last) throw new Error('SSE 无有效 data 块: ' + text.slice(0, 200))
|
||||||
|
return last
|
||||||
|
}
|
||||||
|
throw new Error('未知响应格式: ' + t.slice(0, 200))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认 sheet 解析: 宽表 (列=日期, 行=值) → 长表
|
||||||
|
// 例: { columns:["指标","来源","2024-12","2024-11"], items:[["CPI","统计局","0.1","0.2"]] }
|
||||||
|
// → [{period:"2024-12", value:0.1}, {period:"2024-11", value:0.2}]
|
||||||
|
// 返回时反转成从老到新
|
||||||
|
function defaultParseSheet(sheet) {
|
||||||
|
if (!sheet || !sheet.columns || !sheet.items || !sheet.items[0]) return []
|
||||||
|
const cols = sheet.columns
|
||||||
|
const row = sheet.items[0]
|
||||||
|
const dates = cols.slice(2) // 跳过 [指标名, 来源]
|
||||||
|
const values = row.slice(2)
|
||||||
|
const out = []
|
||||||
|
for (let i = 0; i < dates.length; i++) {
|
||||||
|
const v = parseFloat(values[i])
|
||||||
|
if (!isNaN(v)) out.push({ period: dates[i], value: v })
|
||||||
|
}
|
||||||
|
return out.reverse() // 从老到新
|
||||||
|
}
|
||||||
|
|
||||||
|
// 宽表 → 长表, 指定 item 行 (按指标全名前缀匹配), 日频列降采样为月末值
|
||||||
|
// 例: columns=["指标","来源","2026-08-05","2026-08-04",...] → [{period:"2024-01",value},...]
|
||||||
|
function monthEndSample(sheet, keyword) {
|
||||||
|
if (!sheet || !sheet.columns || !sheet.items) return []
|
||||||
|
const row = sheet.items.find((r) => String(r[0] || '').includes(keyword))
|
||||||
|
if (!row) return []
|
||||||
|
const cols = sheet.columns
|
||||||
|
const dates = cols.slice(2) // 新 → 老
|
||||||
|
const values = row.slice(2)
|
||||||
|
const out = []
|
||||||
|
const seen = new Set()
|
||||||
|
for (let i = 0; i < dates.length; i++) {
|
||||||
|
const v = parseFloat(values[i])
|
||||||
|
const m = String(dates[i] || '').slice(0, 7) // "2024-01-02" → "2024-01"
|
||||||
|
if (!/^\d{4}-\d{2}$/.test(m) || Number.isNaN(v)) continue
|
||||||
|
if (seen.has(m)) continue // 列是降序, 第一次遇到的即当月最后一个交易日
|
||||||
|
seen.add(m)
|
||||||
|
out.push({ period: m, value: v })
|
||||||
|
}
|
||||||
|
return out.reverse() // 从老到新
|
||||||
|
}
|
||||||
|
|
||||||
|
// 宽表 → 长表, 指定 item 行 (按指标全名前缀匹配), 保留原频率 (月/季/年)
|
||||||
|
function pickRowByKeyword(sheet, keyword) {
|
||||||
|
if (!sheet || !sheet.columns || !sheet.items) return []
|
||||||
|
const row = sheet.items.find((r) => String(r[0] || '').includes(keyword))
|
||||||
|
if (!row) return []
|
||||||
|
const cols = sheet.columns
|
||||||
|
const dates = cols.slice(2)
|
||||||
|
const values = row.slice(2)
|
||||||
|
const out = []
|
||||||
|
for (let i = 0; i < dates.length; i++) {
|
||||||
|
const v = parseFloat(values[i])
|
||||||
|
if (!Number.isNaN(v)) out.push({ period: String(dates[i]), value: v })
|
||||||
|
}
|
||||||
|
return out.reverse() // 从老到新
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====== fetcher 入口 ======
|
||||||
|
|
||||||
|
export async function listAvailableTools() {
|
||||||
|
const key = process.env.EM_API_KEY
|
||||||
|
if (!key) throw new Error('缺少 EM_API_KEY')
|
||||||
|
const base = process.env.EASTMONEY_MCP_BASE || DEFAULT_BASE
|
||||||
|
const client = new McpClient(base, key)
|
||||||
|
return client.listTools()
|
||||||
|
}
|
||||||
|
|
||||||
|
// opts.skipNames: Set<string> — DB 已存在的指标名, 跳过不调 MCP (省 token)
|
||||||
|
// opts.onlyNames: Set<string> — 白名单, 只跑这些 (用于补跑单个)
|
||||||
|
export async function fetchAll(opts = {}) {
|
||||||
|
if (INDICATORS.length === 0) {
|
||||||
|
throw new Error('eastmoney-mcp: INDICATORS 数组为空, 加新指标见文件顶部注释')
|
||||||
|
}
|
||||||
|
const skip = opts.skipNames || new Set()
|
||||||
|
const force = opts.forceNames || new Set()
|
||||||
|
const only = opts.onlyNames || null
|
||||||
|
|
||||||
|
const key = process.env.EM_API_KEY
|
||||||
|
if (!key) throw new Error('缺少 EM_API_KEY')
|
||||||
|
const base = process.env.EASTMONEY_MCP_BASE || DEFAULT_BASE
|
||||||
|
const client = new McpClient(base, key)
|
||||||
|
|
||||||
|
const results = []
|
||||||
|
for (const cfg of INDICATORS) {
|
||||||
|
if (skip.has(cfg.name) && !force.has(cfg.name)) {
|
||||||
|
console.log(`[MCP] ${cfg.name} 跳过 (DB 已存在)`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (only && !only.has(cfg.name)) continue
|
||||||
|
try {
|
||||||
|
console.log(`[MCP] ${cfg.name} 调用 ${cfg.tool}: ${cfg.query}`)
|
||||||
|
const r = await client.callTool(cfg.tool, { query: cfg.query })
|
||||||
|
const text = (r.content || []).map((c) => c.text || '').join('')
|
||||||
|
let payload
|
||||||
|
try { payload = JSON.parse(text) } catch { payload = { raw: text } }
|
||||||
|
const sheets = payload.data || (Array.isArray(payload) ? payload : [payload])
|
||||||
|
const sheet = cfg.pickSheet ? cfg.pickSheet(sheets) : sheets[0]
|
||||||
|
const parsed = cfg.parseSheet ? cfg.parseSheet(sheet) : defaultParseSheet(sheet)
|
||||||
|
if (parsed.length === 0) {
|
||||||
|
console.log(`[MCP] ${cfg.name} 返回空 rows, 跳过`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
results.push({
|
||||||
|
name: cfg.name,
|
||||||
|
groupName: cfg.groupName,
|
||||||
|
data: {
|
||||||
|
unit: cfg.unit,
|
||||||
|
title: cfg.title,
|
||||||
|
source: 'eastmoney-mcp',
|
||||||
|
rows: parsed,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
console.log(`[MCP] ${cfg.name} OK, ${parsed.length} rows (${parsed[0]?.period}..${parsed.at(-1)?.period})`)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[MCP] ${cfg.name} FAIL: ${e.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 全部被 DB 优先跳过时, 返回显式标记而不是空数组 (避免被误判为失败)
|
||||||
|
if (results.length === 0 && INDICATORS.every((cfg) => skip.has(cfg.name) && !force.has(cfg.name))) {
|
||||||
|
return { skipped: true, reason: 'DB 已存在全部指标' }
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
// Fetcher 注册表: 列出所有启用的数据源
|
||||||
|
// 加新数据源只要:
|
||||||
|
// 1. 写一个 scripts/fetchers/<source>.fetcher.js, 导出 META + fetchAll
|
||||||
|
// 2. 在本文件 FETCHERS 数组里加一行
|
||||||
|
// 3. 完事, refresh 脚本会自动跑
|
||||||
|
|
||||||
|
// 每项的 schema:
|
||||||
|
// name: 数据源标识 (用于日志)
|
||||||
|
// module: fetcher 模块路径 (相对 scripts/fetchers/)
|
||||||
|
// enabled: true/false, 可由 env 关闭 (如 ENABLE_EASTMONEY_MCP=0)
|
||||||
|
// env: 需要的环境变量名列表, 缺一即跳过
|
||||||
|
export const FETCHERS = [
|
||||||
|
{
|
||||||
|
name: 'wb-indicators',
|
||||||
|
module: './wb-indicators.fetcher.js',
|
||||||
|
enabled: true,
|
||||||
|
env: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'eastmoney-mcp',
|
||||||
|
module: './eastmoney-mcp.fetcher.js',
|
||||||
|
// 默认开启, 有 APIKey 才真正能跑 (fetcher 内部会自检)
|
||||||
|
enabled: process.env.ENABLE_EASTMONEY_MCP !== '0',
|
||||||
|
env: ['EM_API_KEY'],
|
||||||
|
},
|
||||||
|
// 后续在这里追加:
|
||||||
|
// { name: 'fred', module: './fred.fetcher.js', enabled: true, env: ['FRED_API_KEY'] },
|
||||||
|
// { name: 'tushare', module: './tushare.fetcher.js', enabled: true, env: ['TUSHARE_TOKEN'] },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 校验环境变量, 返回 { ok, missing[] }
|
||||||
|
export function checkEnv(required) {
|
||||||
|
const missing = required.filter((k) => !process.env[k] || !process.env[k].trim())
|
||||||
|
return { ok: missing.length === 0, missing }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载并运行所有启用的 fetcher, 返回 [{ name, ok, items, error, skipped }]
|
||||||
|
// 错误隔离: 一个 fetcher 挂了不影响其他
|
||||||
|
// opts.skipNames: Set<string> — DB 优先, 已存在的指标名, 透传给 fetcher
|
||||||
|
// opts.forceNames: Set<string> — 强制刷新名单, 即使 skipNames 里有也照拉
|
||||||
|
export async function runFetchers(opts = {}) {
|
||||||
|
const results = []
|
||||||
|
for (const meta of FETCHERS) {
|
||||||
|
if (!meta.enabled) {
|
||||||
|
results.push({ name: meta.name, skipped: true, reason: 'disabled' })
|
||||||
|
console.log(`[Fetcher] ${meta.name} 跳过 (disabled)`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const envCheck = checkEnv(meta.env)
|
||||||
|
if (!envCheck.ok) {
|
||||||
|
results.push({ name: meta.name, skipped: true, reason: `缺少 env: ${envCheck.missing.join(', ')}` })
|
||||||
|
console.log(`[Fetcher] ${meta.name} 跳过 (${envCheck.missing.join(', ')})`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const mod = await import(meta.module)
|
||||||
|
if (!mod.META || !mod.fetchAll) {
|
||||||
|
throw new Error('fetcher 必须导出 META 和 fetchAll')
|
||||||
|
}
|
||||||
|
console.log(`[Fetcher] ${meta.name} 开始...`)
|
||||||
|
const out = await mod.fetchAll({ skipNames: opts.skipNames, onlyNames: opts.onlyNames, forceNames: opts.forceNames })
|
||||||
|
// fetcher 可返回 { skipped: true, reason } 表示"全部被 DB 优先跳过", 不算失败
|
||||||
|
if (out && out.skipped) {
|
||||||
|
results.push({ name: meta.name, skipped: true, reason: out.reason })
|
||||||
|
console.log(`[Fetcher] ${meta.name} 跳过 (${out.reason})`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const items = Array.isArray(out) ? out : []
|
||||||
|
// 校验返回结构
|
||||||
|
const valid = items.filter((it) => it && it.name && it.data && Array.isArray(it.data.rows))
|
||||||
|
if (valid.length === 0) {
|
||||||
|
throw new Error('fetcher 未返回任何有效指标 (需包含 name/data.rows)')
|
||||||
|
}
|
||||||
|
console.log(`[Fetcher] ${meta.name} 拉取 ${valid.length} 个指标`)
|
||||||
|
results.push({ name: meta.name, ok: true, items: valid })
|
||||||
|
} catch (e) {
|
||||||
|
results.push({ name: meta.name, ok: false, error: e.message })
|
||||||
|
console.error(`[Fetcher] ${meta.name} 失败: ${e.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
// 新闻 fetcher: 每天 3 点调用 mx_finance_search_news 拉国内外经济政治新闻
|
||||||
|
// 按影响大小排序, 取前 10 条 (LLM 可能多/少几条, 全部入 DB, 按 impactRank 标记)
|
||||||
|
// 数据直接写 News 表, 与 Indicator 解耦
|
||||||
|
import axios from 'axios'
|
||||||
|
import mysql from 'mysql2/promise'
|
||||||
|
|
||||||
|
const MCP_BASE_DEFAULT = 'https://mxapi.eastmoney.com/mxds/mcp'
|
||||||
|
const TIMEOUT = 90000
|
||||||
|
const TOP_N = 10
|
||||||
|
|
||||||
|
const QUERIES = [
|
||||||
|
'今天国内外最新重大经济政治新闻, 按影响大小排序, 给出前 ' + TOP_N + ' 条, 每条含标题、摘要、发布时间、来源、链接',
|
||||||
|
]
|
||||||
|
|
||||||
|
class McpClient {
|
||||||
|
constructor(baseUrl, apiKey) { this.baseUrl = baseUrl; this.apiKey = apiKey; this.id = 0 }
|
||||||
|
async _request(method, params) {
|
||||||
|
const payload = { jsonrpc: '2.0', id: ++this.id, method, ...(params ? { params } : {}) }
|
||||||
|
const resp = await axios.post(this.baseUrl, payload, {
|
||||||
|
timeout: TIMEOUT,
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream', 'em_api_key': this.apiKey },
|
||||||
|
responseType: 'text', transformResponse: [(d) => d], validateStatus: () => true,
|
||||||
|
})
|
||||||
|
if (resp.status >= 400) throw new Error('HTTP ' + resp.status + ': ' + String(resp.data).slice(0, 300))
|
||||||
|
const parsed = JSON.parse(String(resp.data || ''))
|
||||||
|
if (parsed.error) throw new Error('MCP error ' + parsed.error.code + ': ' + parsed.error.message)
|
||||||
|
return parsed.result || parsed
|
||||||
|
}
|
||||||
|
async callTool(name, args = {}) {
|
||||||
|
const r = await this._request('tools/call', { name, arguments: args })
|
||||||
|
if (r.isError) throw new Error('tool error: ' + (r.content || []).map((c) => c.text || '').join('; '))
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePublishedAt(s) {
|
||||||
|
if (!s) return null
|
||||||
|
const d = new Date(String(s).replace(/-/g, '/'))
|
||||||
|
return isNaN(d.getTime()) ? null : d
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickNewsSheet(sheets) {
|
||||||
|
if (!Array.isArray(sheets) || sheets.length === 0) return null
|
||||||
|
return sheets.find((s) => s.sheetName && (s.sheetName.includes('资讯') || s.sheetName.includes('新闻'))) || sheets[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// items[i] = [title, summary, publishedAt, source, url]
|
||||||
|
function parseNewsSheet(sheet, impactBase = 0) {
|
||||||
|
if (!sheet || !sheet.items) return []
|
||||||
|
const out = []
|
||||||
|
for (let i = 0; i < sheet.items.length; i++) {
|
||||||
|
const r = sheet.items[i]
|
||||||
|
const title = String(r[0] || '').trim().slice(0, 190)
|
||||||
|
if (!title) continue
|
||||||
|
const publishedAt = parsePublishedAt(r[2])
|
||||||
|
if (!publishedAt) continue
|
||||||
|
const body = String(r[1] || '').trim()
|
||||||
|
out.push({
|
||||||
|
title,
|
||||||
|
summary: body.slice(0, 180),
|
||||||
|
content: body.slice(0, 8000), // 正文全文 (MCP "摘要"列实际是正文), 上限防异常超长
|
||||||
|
source: String(r[3] || '').trim().slice(0, 80),
|
||||||
|
url: String(r[4] || '').trim().slice(0, 190), // 列宽 varchar(191), 超长会被 MySQL 拒绝
|
||||||
|
publishedAt,
|
||||||
|
impactRank: impactBase + i + 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
export const META = { name: 'news-mcp', description: '妙想 MCP 新闻资讯 (mx_finance_search_news)' }
|
||||||
|
|
||||||
|
export async function fetchNewsList() {
|
||||||
|
const key = process.env.EM_API_KEY
|
||||||
|
if (!key) throw new Error('缺少 EM_API_KEY')
|
||||||
|
const base = process.env.EASTMONEY_MCP_BASE || MCP_BASE_DEFAULT
|
||||||
|
const client = new McpClient(base, key)
|
||||||
|
const all = []
|
||||||
|
let impactBase = 0
|
||||||
|
for (const q of QUERIES) {
|
||||||
|
try {
|
||||||
|
console.log('[News] 调用: ' + q)
|
||||||
|
const r = await client.callTool('mx_finance_search_news', { query: q })
|
||||||
|
const text = (r.content || []).map((c) => c.text || '').join('')
|
||||||
|
const payload = JSON.parse(text)
|
||||||
|
const sheets = payload.data || (Array.isArray(payload) ? payload : [payload])
|
||||||
|
const sheet = pickNewsSheet(sheets)
|
||||||
|
const news = parseNewsSheet(sheet, impactBase)
|
||||||
|
console.log('[News] 解析 ' + news.length + ' 条')
|
||||||
|
all.push(...news)
|
||||||
|
impactBase = all.length
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[News] 拉取失败:', e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return all
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUrl(url) {
|
||||||
|
const u = new URL(url)
|
||||||
|
return { host: u.hostname, port: Number(u.port) || 3306, user: decodeURIComponent(u.username), password: decodeURIComponent(u.password), database: u.pathname.replace(/^\//, '') }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function persistNews(newsList) {
|
||||||
|
if (!process.env.DATABASE_URL) throw new Error('缺少 DATABASE_URL')
|
||||||
|
if (!newsList || newsList.length === 0) return { inserted: 0, skipped: 0 }
|
||||||
|
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
|
||||||
|
let inserted = 0, skipped = 0, failed = 0
|
||||||
|
try {
|
||||||
|
for (const n of newsList) {
|
||||||
|
try {
|
||||||
|
// 注意: content 列依赖迁移 add_news_content 已应用, 发布顺序须先迁移后跑本脚本
|
||||||
|
// 重复命中 (title, publishedAt) 时只回填仍为 NULL 的 content, 避免覆盖已有全文
|
||||||
|
const [r] = await conn.execute(
|
||||||
|
'INSERT INTO `News` (`title`, `summary`, `content`, `source`, `url`, `publishedAt`, `impactRank`, `createdAt`) ' +
|
||||||
|
'VALUES (?, ?, ?, ?, ?, ?, ?, NOW(3)) ' +
|
||||||
|
'AS new ON DUPLICATE KEY UPDATE `impactRank` = new.`impactRank`, `content` = IFNULL(News.`content`, new.`content`)',
|
||||||
|
[n.title, n.summary, n.content || null, n.source, n.url, n.publishedAt, n.impactRank],
|
||||||
|
)
|
||||||
|
if (r.affectedRows === 1) inserted++
|
||||||
|
else skipped++
|
||||||
|
} catch (e) {
|
||||||
|
failed++
|
||||||
|
console.error('[News] 单条入库失败:', n.title, e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await conn.end()
|
||||||
|
}
|
||||||
|
return { inserted, skipped, failed }
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
// World Bank API 数据源
|
||||||
|
// 文档: https://datahelpdesk.worldbank.org/knowledgebase/articles/898581
|
||||||
|
// 优点: 官方、JSON、免 key、稳定, 中国数据滞后约 1 年 (这是全球宏观数据物理上限)
|
||||||
|
// 接口: GET /v2/country/{code}/indicator/{id}?format=json&date={startYear}:{endYear}&per_page=100
|
||||||
|
|
||||||
|
const WB_BASE = 'https://api.worldbank.org/v2'
|
||||||
|
const TIMEOUT = 15000
|
||||||
|
|
||||||
|
// 6 个核心宏观指标
|
||||||
|
// - 中国 GDP (现价美元) - 国民经济总规模
|
||||||
|
// - 中国 GDP 增速 - 增长动能
|
||||||
|
// - 中国 CPI 同比 - 通胀
|
||||||
|
// - 中国失业率 - 就业
|
||||||
|
// - 美国 GDP (现价美元) - 对标
|
||||||
|
// - 美国 CPI 同比 - 对标
|
||||||
|
export const WB_INDICATORS = [
|
||||||
|
{ id: 'NY.GDP.MKTP.CD', name: 'china-gdp-usd', groupName: 'china-macro', title: '中国 GDP', unit: '万亿美元', scale: 1e-8, digits: 2 },
|
||||||
|
{ id: 'NY.GDP.MKTP.KD.ZG',name: 'china-gdp-growth', groupName: 'china-macro', title: '中国 GDP 增速', unit: '%', scale: 1, digits: 2 },
|
||||||
|
{ id: 'FP.CPI.TOTL.ZG', name: 'china-cpi', groupName: 'china-macro', title: '中国 CPI', unit: '%', scale: 1, digits: 2 },
|
||||||
|
{ id: 'SL.UEM.TOTL.ZS', name: 'china-unemploy', groupName: 'china-macro', title: '中国失业率', unit: '%', scale: 1, digits: 2 },
|
||||||
|
{ id: 'NY.GDP.MKTP.CD', name: 'usa-gdp-usd', groupName: 'usa-macro', title: '美国 GDP', unit: '万亿美元', scale: 1e-8, digits: 2 },
|
||||||
|
{ id: 'FP.CPI.TOTL.ZG', name: 'usa-cpi', groupName: 'usa-macro', title: '美国 CPI', unit: '%', scale: 1, digits: 2 },
|
||||||
|
]
|
||||||
|
|
||||||
|
// World Bank 中国是 CHN, 美国是 USA
|
||||||
|
function countryCodeOf(name) {
|
||||||
|
if (name.startsWith('china-')) return 'CHN'
|
||||||
|
if (name.startsWith('usa-')) return 'USA'
|
||||||
|
throw new Error('Unknown country for ' + name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拉单个指标从 startYear 到当前年
|
||||||
|
// 返回 { name, groupName, title, unit, rows: [{ year, value }] }
|
||||||
|
// 注意: WB 对连续请求会限流返回 400 Invalid value, 加 3 次退避重试
|
||||||
|
export async function fetchIndicator(meta, startYear = 2015) {
|
||||||
|
const country = countryCodeOf(meta.name)
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
const url = `${WB_BASE}/country/${country}/indicator/${meta.id}?format=json&date=${startYear}:${currentYear}&per_page=200`
|
||||||
|
|
||||||
|
let resp
|
||||||
|
const delays = [0, 1500, 4000]
|
||||||
|
for (let attempt = 0; attempt < delays.length; attempt++) {
|
||||||
|
if (delays[attempt]) await new Promise((r) => setTimeout(r, delays[attempt]))
|
||||||
|
try {
|
||||||
|
resp = await axios.get(url, { timeout: TIMEOUT, headers: { 'User-Agent': 'iboard-refresh/1.0' } })
|
||||||
|
break
|
||||||
|
} catch (e) {
|
||||||
|
if (attempt === delays.length - 1) throw e
|
||||||
|
console.log(`[WB] ${meta.name} 请求失败, ${delays[attempt + 1] / 1000}s 后重试 (${e.message})`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// WB 返回: [{page,pages,per_page,total,...}, [{indicator, country, value, date}, ...]]
|
||||||
|
const data = resp.data
|
||||||
|
if (!Array.isArray(data) || data.length < 2 || !Array.isArray(data[1])) {
|
||||||
|
throw new Error('World Bank 返回结构异常: ' + JSON.stringify(data).slice(0, 200))
|
||||||
|
}
|
||||||
|
const points = data[1]
|
||||||
|
.filter((p) => p.value !== null && p.value !== undefined)
|
||||||
|
.map((p) => ({
|
||||||
|
year: String(p.date),
|
||||||
|
value: Number((Number(p.value) * meta.scale).toFixed(meta.digits)),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => Number(a.year) - Number(b.year))
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: meta.name,
|
||||||
|
groupName: meta.groupName,
|
||||||
|
data: {
|
||||||
|
unit: meta.unit,
|
||||||
|
title: meta.title,
|
||||||
|
source: 'World Bank API',
|
||||||
|
rows: points,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const META = {
|
||||||
|
name: 'wb-indicators',
|
||||||
|
description: 'World Bank 官方 API 宏观数据',
|
||||||
|
docs: 'https://datahelpdesk.worldbank.org/knowledgebase/articles/898581',
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拉全部 (返回符合 fetcher 契约的 [{name, groupName, data}, ...])
|
||||||
|
// 错误隔离: 单个指标失败 -> 跳过 + 记日志, 不影响其他
|
||||||
|
// opts.skipNames: Set<string> — DB 已存在的指标名, 跳过不拉 (WB 免费但也要省请求)
|
||||||
|
export async function fetchAll(opts = {}) {
|
||||||
|
const skip = opts.skipNames || new Set()
|
||||||
|
const startYear = 2015
|
||||||
|
const results = []
|
||||||
|
for (const m of WB_INDICATORS) {
|
||||||
|
if (skip.has(m.name)) {
|
||||||
|
console.log(`[WB] ${m.name} 跳过 (DB 已存在)`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const r = await fetchIndicator(m, startYear)
|
||||||
|
console.log(`[WB] ${r.name} OK, ${r.data.rows.length} rows (${r.data.rows[0]?.year}..${r.data.rows.at(-1)?.year})`)
|
||||||
|
results.push(r)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[WB] ${m.name} FAIL: ${e.message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 全部被 DB 优先跳过时, 返回显式标记而不是空数组 (避免被误判为失败)
|
||||||
|
if (results.length === 0 && WB_INDICATORS.every((m) => skip.has(m.name))) {
|
||||||
|
return { skipped: true, reason: 'DB 已存在全部指标' }
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* 一次性数据迁移: 把 Indicator.data (JSON) 拆分到 IndicatorPoint 明细表
|
||||||
|
* 用法: 在迁移 A (建表+加列) 之后、迁移 B (删 data 列) 之前运行
|
||||||
|
* 幂等: 同 (indicatorId, period) 重复插入会更新 value, 可安全重跑
|
||||||
|
*/
|
||||||
|
require('dotenv').config()
|
||||||
|
const mysql = require('mysql2/promise')
|
||||||
|
|
||||||
|
function parseUrl(url) {
|
||||||
|
const u = new URL(url)
|
||||||
|
return {
|
||||||
|
host: u.hostname,
|
||||||
|
port: Number(u.port) || 3306,
|
||||||
|
user: decodeURIComponent(u.username),
|
||||||
|
password: decodeURIComponent(u.password),
|
||||||
|
database: u.pathname.replace(/^\//, ''),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据 row 的时间字段推断频率: year | quarter | month | day
|
||||||
|
function inferFrequency(row) {
|
||||||
|
if (row && row.month !== undefined) return 'month'
|
||||||
|
if (row && row.year !== undefined) return 'year'
|
||||||
|
const p = String((row && row.period) || '')
|
||||||
|
if (/^\d{4}$/.test(p)) return 'year'
|
||||||
|
if (/^\d{4}-Q[1-4]$/.test(p)) return 'quarter'
|
||||||
|
if (/^\d{4}-\d{2}$/.test(p)) return 'month'
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}$/.test(p)) return 'day'
|
||||||
|
return 'month'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一 row 的时间字段为 period
|
||||||
|
function periodOf(row) {
|
||||||
|
if (!row) return ''
|
||||||
|
return String(row.period !== undefined ? row.period : (row.month !== undefined ? row.month : row.year))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
|
||||||
|
try {
|
||||||
|
const [rows] = await conn.execute('SELECT id, name, groupName, data FROM Indicator')
|
||||||
|
console.log('[Migrate] 读取', rows.length, '个 Indicator')
|
||||||
|
|
||||||
|
let points = 0, updated = 0
|
||||||
|
for (const ind of rows) {
|
||||||
|
// mysql2 对 JSON 列已自动解析为对象, 兼容字符串兜底
|
||||||
|
let data = ind.data
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
try { data = JSON.parse(data) } catch { data = null }
|
||||||
|
}
|
||||||
|
const title = (data && data.title) || ind.name
|
||||||
|
const unit = (data && data.unit) || ''
|
||||||
|
const source = (data && data.source) || ''
|
||||||
|
const rowsArr = (data && Array.isArray(data.rows)) ? data.rows : []
|
||||||
|
const freq = rowsArr.length ? inferFrequency(rowsArr[0]) : 'month'
|
||||||
|
|
||||||
|
// 更新 Indicator 元数据列
|
||||||
|
await conn.execute(
|
||||||
|
'UPDATE Indicator SET title=?, unit=?, source=?, frequency=?, updatedAt=NOW(3) WHERE id=?',
|
||||||
|
[title, unit, source, freq, ind.id],
|
||||||
|
)
|
||||||
|
updated++
|
||||||
|
|
||||||
|
// 写入明细
|
||||||
|
for (const r of rowsArr) {
|
||||||
|
const period = periodOf(r)
|
||||||
|
const value = Number(r.value)
|
||||||
|
if (!period || Number.isNaN(value)) continue
|
||||||
|
const [res] = await conn.execute(
|
||||||
|
'INSERT INTO IndicatorPoint (indicatorId, period, value, createdAt, updatedAt) VALUES (?, ?, ?, NOW(3), NOW(3)) ' +
|
||||||
|
'ON DUPLICATE KEY UPDATE value=VALUES(value), updatedAt=NOW(3)',
|
||||||
|
[ind.id, period, value],
|
||||||
|
)
|
||||||
|
points += (res.affectedRows > 0 ? 1 : 0)
|
||||||
|
}
|
||||||
|
console.log('[Migrate]', ind.name, '->', rowsArr.length, '个点, 频率', freq)
|
||||||
|
}
|
||||||
|
console.log('[Migrate] 完成: 更新', updated, '个 Indicator, 写入/更新', points, '个明细点')
|
||||||
|
} finally {
|
||||||
|
await conn.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => { console.error('[Migrate] 失败:', e); process.exit(1) })
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
/**
|
||||||
|
* 拉所有启用的数据源, upsert 到 Indicator + IndicatorPoint 明细表
|
||||||
|
* 用法:
|
||||||
|
* npm run data:refresh # 只拉 DB 里没有的指标
|
||||||
|
* npm run data:refresh -- --force a,b,c # 强制刷新指定指标 (忽略 DB 已有)
|
||||||
|
*
|
||||||
|
* 数据结构 (2026-08 重构):
|
||||||
|
* Indicator 元数据: name/groupName/title/unit/source/frequency/hidden
|
||||||
|
* IndicatorPoint 明细: (indicatorId, period, value), period 兼容年/季/月/日
|
||||||
|
*
|
||||||
|
* DB 优先策略:
|
||||||
|
* 1. 先从 DB 查已存在的 name 集合
|
||||||
|
* 2. 把集合传给每个 fetcher, fetcher 跳过已存在的
|
||||||
|
* 3. 只对 DB 没有的指标调远程拉取, 节省 token (尤其妙想 MCP 这种 LLM 接口)
|
||||||
|
*
|
||||||
|
* 加新数据源: 写 scripts/fetchers/<name>.fetcher.js 导出 META + fetchAll(opts),
|
||||||
|
* 然后在 scripts/fetchers/index.js 的 FETCHERS 注册
|
||||||
|
*/
|
||||||
|
require('dotenv').config()
|
||||||
|
const mysql = require('mysql2/promise')
|
||||||
|
|
||||||
|
// 隐藏指标: 数据与获取脚本保留, 但不参与前端展示 (API 过滤)
|
||||||
|
const HIDDEN_INDICATORS = new Set([
|
||||||
|
'china-gdp-usd', // 中国 GDP (现价美元)
|
||||||
|
'china-gdp-growth', // 中国 GDP 同比 (World Bank)
|
||||||
|
'china-gdp-yoy', // 中国 GDP 同比 (妙想季度)
|
||||||
|
'china-unemploy', // 中国失业率
|
||||||
|
'usa-gdp-usd', // 美国 GDP
|
||||||
|
])
|
||||||
|
|
||||||
|
function parseUrl(url) {
|
||||||
|
const u = new URL(url)
|
||||||
|
return {
|
||||||
|
host: u.hostname,
|
||||||
|
port: Number(u.port) || 3306,
|
||||||
|
user: decodeURIComponent(u.username),
|
||||||
|
password: decodeURIComponent(u.password),
|
||||||
|
database: u.pathname.replace(/^\//, ''),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseForceArg(argv) {
|
||||||
|
const i = argv.indexOf('--force')
|
||||||
|
if (i === -1) return new Set()
|
||||||
|
return new Set(String(argv[i + 1] || '').split(',').map((s) => s.trim()).filter(Boolean))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 推断频率: year | quarter | month | day (优先用 fetcher 显式声明)
|
||||||
|
function inferFrequency(data) {
|
||||||
|
if (data && data.frequency) return data.frequency
|
||||||
|
const p = String(((data && data.rows) || [])[0]?.period || '')
|
||||||
|
if (/^\d{4}$/.test(p)) return 'year'
|
||||||
|
if (/^\d{4}-Q[1-4]$/.test(p)) return 'quarter'
|
||||||
|
if (/^\d{4}-\d{2}$/.test(p)) return 'month'
|
||||||
|
if (/^\d{4}-\d{2}-\d{2}$/.test(p)) return 'day'
|
||||||
|
return 'month'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写 Indicator 元数据 + 全量替换明细点 (每指标一个事务)
|
||||||
|
async function upsertIndicatorWithPoints(conn, payload) {
|
||||||
|
const { name, groupName, data, hidden } = payload
|
||||||
|
const title = data.title || name
|
||||||
|
const unit = data.unit || ''
|
||||||
|
const source = data.source || ''
|
||||||
|
const frequency = inferFrequency(data)
|
||||||
|
const rows = Array.isArray(data.rows) ? data.rows : []
|
||||||
|
|
||||||
|
await conn.beginTransaction()
|
||||||
|
try {
|
||||||
|
const [res] = await conn.execute(
|
||||||
|
'INSERT INTO `Indicator` (`name`, `groupName`, `title`, `unit`, `source`, `frequency`, `hidden`, `createdAt`, `updatedAt`) ' +
|
||||||
|
'VALUES (?, ?, ?, ?, ?, ?, ?, NOW(3), NOW(3)) ' +
|
||||||
|
'ON DUPLICATE KEY UPDATE `groupName` = VALUES(`groupName`), `title` = VALUES(`title`), ' +
|
||||||
|
'`unit` = VALUES(`unit`), `source` = VALUES(`source`), `frequency` = VALUES(`frequency`), ' +
|
||||||
|
'`hidden` = VALUES(`hidden`), `updatedAt` = NOW(3)',
|
||||||
|
[name, groupName, title, unit, source, frequency, hidden ? 1 : 0],
|
||||||
|
)
|
||||||
|
const indicatorId = res.insertId
|
||||||
|
|
||||||
|
// 明细: 先删旧点再插入新点 (全量替换, 保证与数据源一致)
|
||||||
|
await conn.execute('DELETE FROM `IndicatorPoint` WHERE `indicatorId` = ?', [indicatorId])
|
||||||
|
let points = 0
|
||||||
|
for (const r of rows) {
|
||||||
|
const period = String(r.period !== undefined ? r.period : (r.month !== undefined ? r.month : r.year))
|
||||||
|
const value = Number(r.value)
|
||||||
|
if (!period || Number.isNaN(value)) continue
|
||||||
|
await conn.execute(
|
||||||
|
'INSERT INTO `IndicatorPoint` (`indicatorId`, `period`, `value`, `createdAt`, `updatedAt`) VALUES (?, ?, ?, NOW(3), NOW(3))',
|
||||||
|
[indicatorId, period, value],
|
||||||
|
)
|
||||||
|
points++
|
||||||
|
}
|
||||||
|
await conn.commit()
|
||||||
|
return { indicatorId, points }
|
||||||
|
} catch (e) {
|
||||||
|
await conn.rollback()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchExistingNames(conn) {
|
||||||
|
const [rows] = await conn.execute('SELECT `name` FROM `Indicator`')
|
||||||
|
return new Set(rows.map((r) => r.name))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (!process.env.DATABASE_URL) {
|
||||||
|
console.error('缺少 DATABASE_URL 环境变量')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
const force = parseForceArg(process.argv.slice(2))
|
||||||
|
if (force.size) console.log('[Refresh] 强制刷新: ' + [...force].join(', '))
|
||||||
|
|
||||||
|
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
|
||||||
|
try {
|
||||||
|
// 1) 查 DB 已存在的指标名
|
||||||
|
const existing = await fetchExistingNames(conn)
|
||||||
|
console.log('[Refresh] DB 已有 ' + existing.size + ' 个指标: ' + ([...existing].join(', ') || '(空)'))
|
||||||
|
|
||||||
|
// 2) 跑所有 fetcher, 传入 existing + force (DB 优先, force 覆盖)
|
||||||
|
const { runFetchers } = await import('./fetchers/index.js')
|
||||||
|
const fetcherResults = await runFetchers({ skipNames: existing, forceNames: force })
|
||||||
|
|
||||||
|
// 3) 汇总: force 名单里的指标即使 DB 已有也写入
|
||||||
|
const allItems = []
|
||||||
|
let skipped = 0, failed = 0
|
||||||
|
for (const r of fetcherResults) {
|
||||||
|
if (r.skipped) { skipped++; continue }
|
||||||
|
if (!r.ok) { failed++; continue }
|
||||||
|
for (const it of r.items) {
|
||||||
|
if (existing.has(it.name) && !force.has(it.name)) continue
|
||||||
|
allItems.push(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('\n[Refresh] 汇总: ' + allItems.length + ' 个指标待写 (跳过 ' + skipped + ' 个数据源, 失败 ' + failed + ' 个, DB 已存在 ' + existing.size + ' 个)')
|
||||||
|
|
||||||
|
if (failed > 0) {
|
||||||
|
console.error('[Refresh] 警告: 有数据源失败, 仅写入成功的指标')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) 同步隐藏标记: 即使指标被 DB 优先跳过, 名单内的也确保 hidden=true
|
||||||
|
if (HIDDEN_INDICATORS.size > 0) {
|
||||||
|
const names = [...HIDDEN_INDICATORS]
|
||||||
|
const ph = names.map(() => '?').join(',')
|
||||||
|
const sql = "UPDATE `Indicator` SET `hidden` = 1, `updatedAt` = NOW(3) WHERE `name` IN (" + ph + ")"
|
||||||
|
await conn.execute(sql, names)
|
||||||
|
console.log('[Refresh] 同步隐藏标记: ' + names.length + ' 个指标 hidden=true (数据保留, 不展示)')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) 跨源对比报警: 全量跑一次 (即便无新指标也跑, 防止上次漏检)
|
||||||
|
try {
|
||||||
|
const m = await import('../server/alert-engine.js')
|
||||||
|
const a = await m.checkAfterRefresh(conn)
|
||||||
|
console.log('[Refresh] 报警检测: 新增 ' + a.inserted + ' 条 (扫描 ' + a.scanned + ')')
|
||||||
|
} catch (e) { console.error('[Refresh] 报警检测异常:', e.message) }
|
||||||
|
|
||||||
|
if (allItems.length === 0) {
|
||||||
|
console.log('[Refresh] 无指标待写, 不写库 (DB 优先策略生效)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) 写库
|
||||||
|
console.log('[Refresh] 写入数据库...')
|
||||||
|
let ins = 0, upd = 0, totalPoints = 0
|
||||||
|
for (const it of allItems) {
|
||||||
|
const hidden = HIDDEN_INDICATORS.has(it.name)
|
||||||
|
const { points } = await upsertIndicatorWithPoints(conn, { name: it.name, groupName: it.groupName, data: it.data, hidden })
|
||||||
|
if (points > 0) ins++
|
||||||
|
else upd++
|
||||||
|
totalPoints += points
|
||||||
|
console.log('[Refresh] ' + it.name + (hidden ? ' (hidden)' : '') + ': ' + points + ' 个明细点')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
await conn.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error('[Refresh] 异常:', e)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
require('dotenv').config()
|
||||||
|
async function main() {
|
||||||
|
console.log('[News] 开始拉取...')
|
||||||
|
const t0 = Date.now()
|
||||||
|
const mod = await import('./fetchers/news.fetcher.js')
|
||||||
|
const list = await mod.fetchNewsList()
|
||||||
|
console.log('[News] 拉取 ' + list.length + ' 条, 耗时 ' + ((Date.now() - t0) / 1000).toFixed(1) + 's')
|
||||||
|
if (list.length === 0) { console.log('[News] 无数据, 跳过'); return }
|
||||||
|
const r = await mod.persistNews(list)
|
||||||
|
console.log('[News] 入库: 新增 ' + r.inserted + ' 条, 重复跳过 ' + r.skipped + ' 条' + (r.failed ? ', 失败 ' + r.failed + ' 条' : ''))
|
||||||
|
}
|
||||||
|
main().catch((e) => { console.error('[News] 失败:', e); process.exit(1) })
|
||||||
+2
-33
@@ -92,39 +92,8 @@ async function main() {
|
|||||||
}
|
}
|
||||||
console.log('[Strategy] upsert 策略: ' + n2 + ' 行 (1=新增, 2=更新)');
|
console.log('[Strategy] upsert 策略: ' + n2 + ' 行 (1=新增, 2=更新)');
|
||||||
|
|
||||||
const indicators = [
|
// Indicator 表: 改由 scripts/refresh-indicators.cjs 管理 (2026-08 重构后数据存 IndicatorPoint 明细表)
|
||||||
{ name: 'china-bond-1y', groupName: 'china-bond',
|
// 本脚本不再写入 Indicator, 如需初始化指标请运行 npm run data:refresh
|
||||||
data: { unit: '%', title: '中国国债1年', rows: [
|
|
||||||
{ month: '2024-01', value: 1.85 }, { month: '2024-02', value: 1.82 },
|
|
||||||
{ month: '2024-03', value: 1.78 }, { month: '2024-04', value: 1.75 },
|
|
||||||
{ month: '2024-05', value: 1.72 }, { month: '2024-06', value: 1.68 },
|
|
||||||
]}},
|
|
||||||
{ name: 'china-bond-10y', groupName: 'china-bond',
|
|
||||||
data: { unit: '%', title: '中国国债10年', rows: [
|
|
||||||
{ month: '2024-01', value: 2.65 }, { month: '2024-02', value: 2.62 },
|
|
||||||
{ month: '2024-03', value: 2.58 }, { month: '2024-04', value: 2.55 },
|
|
||||||
{ month: '2024-05', value: 2.52 }, { month: '2024-06', value: 2.48 },
|
|
||||||
]}},
|
|
||||||
{ name: 'us-bond-1y', groupName: 'us-bond',
|
|
||||||
data: { unit: '%', title: '美国国债1年', rows: [
|
|
||||||
{ month: '2024-01', value: 4.85 }, { month: '2024-02', value: 4.82 },
|
|
||||||
{ month: '2024-03', value: 4.78 }, { month: '2024-04', value: 4.65 },
|
|
||||||
{ month: '2024-05', value: 4.55 }, { month: '2024-06', value: 4.45 },
|
|
||||||
]}},
|
|
||||||
{ name: 'china-lpr-1y', groupName: 'china-lpr',
|
|
||||||
data: { unit: '%', title: '中国LPR 1年期', rows: [
|
|
||||||
{ month: '2024-01', value: 3.45 }, { month: '2024-02', value: 3.45 },
|
|
||||||
{ month: '2024-03', value: 3.45 }, { month: '2024-04', value: 3.40 },
|
|
||||||
{ month: '2024-05', value: 3.40 }, { month: '2024-06', value: 3.40 },
|
|
||||||
]}},
|
|
||||||
];
|
|
||||||
let n3 = 0;
|
|
||||||
for (const i of indicators) {
|
|
||||||
n3 += await upsertByName(conn, 'Indicator',
|
|
||||||
{ name: i.name, groupName: i.groupName, data: JSON.stringify(i.data), updatedAt: new Date() },
|
|
||||||
'name');
|
|
||||||
}
|
|
||||||
console.log('[Indicator] upsert 指标: ' + n3 + ' 行 (1=新增, 2=更新)');
|
|
||||||
|
|
||||||
const article = {
|
const article = {
|
||||||
title: '欢迎使用 iBoard',
|
title: '欢迎使用 iBoard',
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { prisma } from './db.js'
|
||||||
|
|
||||||
|
// 跨源对比: 同 (indicatorName, period) 多源数据差异超阈值则插 Alert
|
||||||
|
// conn: mysql2 连接
|
||||||
|
// 阈值: 绝对差 >= 0.01 且 相对差 >= 1%
|
||||||
|
// 返回 { inserted, scanned }
|
||||||
|
const ABS_TOL = 0.01
|
||||||
|
const REL_TOL = 0.01
|
||||||
|
|
||||||
|
export async function checkAfterRefresh(conn) {
|
||||||
|
// conn 是 mysql2 连接, 走原始 SQL
|
||||||
|
const [rows] = await conn.execute(`
|
||||||
|
SELECT ia.name AS indicatorName, a.period, ia.source, a.value
|
||||||
|
FROM IndicatorPoint a
|
||||||
|
JOIN Indicator ia ON a.indicatorId = ia.id
|
||||||
|
ORDER BY ia.name, a.period
|
||||||
|
`)
|
||||||
|
const groups = new Map()
|
||||||
|
for (const r of rows) {
|
||||||
|
const k = r.indicatorName + '|' + r.period
|
||||||
|
if (!groups.has(k)) groups.set(k, [])
|
||||||
|
groups.get(k).push({ source: r.source, value: Number(r.value) })
|
||||||
|
}
|
||||||
|
let inserted = 0
|
||||||
|
for (const [k, arr] of groups) {
|
||||||
|
if (arr.length < 2) continue
|
||||||
|
const sources = new Set(arr.map((x) => x.source))
|
||||||
|
if (sources.size < 2) continue
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
for (let j = i + 1; j < arr.length; j++) {
|
||||||
|
const A = arr[i], B = arr[j]
|
||||||
|
const diff = Math.abs(A.value - B.value)
|
||||||
|
const base = Math.max(Math.abs(A.value), Math.abs(B.value), 1e-9)
|
||||||
|
const ratio = diff / base
|
||||||
|
if (diff < ABS_TOL || ratio < REL_TOL) continue
|
||||||
|
const [name, period] = k.split('|')
|
||||||
|
const [r] = await conn.execute(
|
||||||
|
'INSERT INTO `Alert` (`indicatorName`, `period`, `valueA`, `valueB`, `sourceA`, `sourceB`, `diff`, `diffRatio`, `createdAt`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NOW(3))',
|
||||||
|
[name, period, A.value, B.value, A.source, B.source, diff, ratio],
|
||||||
|
)
|
||||||
|
if (r.affectedRows === 1) inserted++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { inserted, scanned: rows.length }
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import cron from 'node-cron'
|
||||||
|
import { prisma } from './db.js'
|
||||||
|
import mysql from 'mysql2/promise'
|
||||||
|
import { fetchNewsList, persistNews } from '../scripts/fetchers/news.fetcher.js'
|
||||||
|
import { checkAfterRefresh } from './alert-engine.js'
|
||||||
|
|
||||||
|
function parseUrl(url) {
|
||||||
|
const u = new URL(url)
|
||||||
|
return { host: u.hostname, port: Number(u.port) || 3306, user: decodeURIComponent(u.username), password: decodeURIComponent(u.password), database: u.pathname.replace(/^\//, '') }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openConn() {
|
||||||
|
return mysql.createConnection(parseUrl(process.env.DATABASE_URL))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每天凌晨 3:00 (本地时间) 拉取新闻 + 跑跨源对比
|
||||||
|
function scheduleNews() {
|
||||||
|
cron.schedule('0 3 * * *', async () => {
|
||||||
|
console.log('[cron] 3:00 触发')
|
||||||
|
try {
|
||||||
|
const list = await fetchNewsList()
|
||||||
|
const r = await persistNews(list)
|
||||||
|
console.log('[cron] 新闻入库: 新增 ' + r.inserted + ' / 跳过 ' + r.skipped)
|
||||||
|
const conn = await openConn()
|
||||||
|
try {
|
||||||
|
const a = await checkAfterRefresh(conn)
|
||||||
|
console.log('[cron] 报警检测: 新增 ' + a.inserted + ' 条 (扫描 ' + a.scanned + ')')
|
||||||
|
} finally { await conn.end() }
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[cron] 任务失败:', e.message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('[cron] 已注册: 每天 03:00 拉取新闻 + 跨源报警检测')
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleNews()
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// ===== 已停用 =====
|
||||||
|
// 服务自检的定时任务已从 server/cron.js 移除 (服务监控暂留空)。
|
||||||
|
// 如恢复服务监控, 重新在 cron.js 注册 runOnce 并复用本文件。
|
||||||
|
// 服务健康检查: 探测本应用各关键端点, 写入 ServiceCheck 表
|
||||||
|
const mysql = require('mysql2/promise')
|
||||||
|
|
||||||
|
function parseUrl(url) { const u = new URL(url); return { host: u.hostname, port: Number(u.port) || 3306, user: decodeURIComponent(u.username), password: decodeURIComponent(u.password), database: u.pathname.replace(/^\//, '') } }
|
||||||
|
|
||||||
|
const TARGETS = [
|
||||||
|
{ name: 'api-health', url: 'http://127.0.0.1:' + (process.env.PORT || 3001) + '/api/health' },
|
||||||
|
{ name: 'auth-login', url: 'http://127.0.0.1:' + (process.env.PORT || 3001) + '/api/auth/login' },
|
||||||
|
]
|
||||||
|
|
||||||
|
async function probeOne(target) {
|
||||||
|
const t0 = Date.now()
|
||||||
|
try {
|
||||||
|
const ctrl = new AbortController()
|
||||||
|
setTimeout(() => ctrl.abort(), 8000)
|
||||||
|
const r = await fetch(target.url, { method: 'GET', signal: ctrl.signal })
|
||||||
|
const latency = Date.now() - t0
|
||||||
|
const status = (r.ok || r.status === 400 || r.status === 401) ? 'up' : 'down'
|
||||||
|
const msg = 'HTTP ' + r.status
|
||||||
|
return { name: target.name, url: target.url, status, latencyMs: latency, message: msg }
|
||||||
|
} catch (e) {
|
||||||
|
return { name: target.name, url: target.url, status: 'down', latencyMs: Date.now() - t0, message: e.message || String(e) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkDb() {
|
||||||
|
const t0 = Date.now()
|
||||||
|
try {
|
||||||
|
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
|
||||||
|
await conn.execute('SELECT 1')
|
||||||
|
await conn.end()
|
||||||
|
return { name: 'database', url: 'mysql://' + (new URL(process.env.DATABASE_URL)).host, status: 'up', latencyMs: Date.now() - t0, message: 'ok' }
|
||||||
|
} catch (e) {
|
||||||
|
return { name: 'database', url: 'mysql', status: 'down', latencyMs: Date.now() - t0, message: e.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runOnce() {
|
||||||
|
const results = await Promise.all([...TARGETS.map(probeOne), checkDb()])
|
||||||
|
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
|
||||||
|
try {
|
||||||
|
for (const r of results) {
|
||||||
|
await conn.execute(
|
||||||
|
'INSERT INTO `ServiceCheck` (`name`,`url`,`status`,`latencyMs`,`message`,`lastCheckAt`,`createdAt`,`updatedAt`) VALUES (?,?,?,?,?,NOW(3),NOW(3),NOW(3)) ' +
|
||||||
|
'ON DUPLICATE KEY UPDATE `url`=VALUES(`url`),`status`=VALUES(`status`),`latencyMs`=VALUES(`latencyMs`),`message`=VALUES(`message`),`lastCheckAt`=NOW(3),`updatedAt`=NOW(3)',
|
||||||
|
[r.name, r.url, r.status, r.latencyMs, r.message.slice(0, 180)],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await conn.end()
|
||||||
|
}
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { runOnce, TARGETS }
|
||||||
@@ -5,7 +5,10 @@ import express from 'express'
|
|||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
import authRouter from './routes/auth.js'
|
import authRouter from './routes/auth.js'
|
||||||
import dataRouter from './routes/data.js'
|
import dataRouter from './routes/data.js'
|
||||||
|
import newsRouter from './routes/news.js'
|
||||||
|
import alertsRouter from './routes/alerts.js'
|
||||||
import articlesRouter from './routes/articles.js'
|
import articlesRouter from './routes/articles.js'
|
||||||
|
import monitorsRouter from './routes/monitors.js'
|
||||||
|
|
||||||
const PORT = Number(process.env.PORT) || 3001
|
const PORT = Number(process.env.PORT) || 3001
|
||||||
const DIST_DIR = path.join(process.cwd(), 'dist')
|
const DIST_DIR = path.join(process.cwd(), 'dist')
|
||||||
@@ -21,7 +24,10 @@ app.get('/api/health', (_req, res) => res.json({ ok: true, ts: Date.now() }))
|
|||||||
// 业务路由
|
// 业务路由
|
||||||
app.use('/api/auth', authRouter)
|
app.use('/api/auth', authRouter)
|
||||||
app.use('/api', dataRouter)
|
app.use('/api', dataRouter)
|
||||||
|
app.use('/api', newsRouter)
|
||||||
|
app.use('/api', alertsRouter)
|
||||||
app.use('/api', articlesRouter)
|
app.use('/api', articlesRouter)
|
||||||
|
app.use('/api', monitorsRouter)
|
||||||
|
|
||||||
// 静态资源 (Vite build 产物)
|
// 静态资源 (Vite build 产物)
|
||||||
if (HAS_DIST) {
|
if (HAS_DIST) {
|
||||||
@@ -41,6 +47,9 @@ app.use((err, _req, res, _next) => {
|
|||||||
res.status(500).json({ error: err.message || '服务器内部错误' })
|
res.status(500).json({ error: err.message || '服务器内部错误' })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 启动定时任务: 每天凌晨 3 点拉取新闻
|
||||||
|
import('./cron.js').catch((e) => console.error('[cron] 加载失败:', e.message))
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`[iboard-api] listening on http://127.0.0.1:${PORT}`)
|
console.log(`[iboard-api] listening on http://127.0.0.1:${PORT}`)
|
||||||
console.log(`[iboard-api] static dist: ${HAS_DIST ? DIST_DIR : '(not built, API only)'}`)
|
console.log(`[iboard-api] static dist: ${HAS_DIST ? DIST_DIR : '(not built, API only)'}`)
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// komari 实时监控客户端: 公开 API, 无鉴权, 请求时实时获取, 不落库
|
||||||
|
// GET /api/nodes 节点列表
|
||||||
|
// GET /api/recent/{uuid} 最近实时状态 (数组, 多个时间点)
|
||||||
|
// GET /api/records/load?uuid=... 1 分钟粒度历史 (komari 保留 24h)
|
||||||
|
|
||||||
|
const BASE = process.env.KOMARI_BASE || 'https://komari.honor3.com'
|
||||||
|
const TIMEOUT = 15000
|
||||||
|
|
||||||
|
async function jget(path) {
|
||||||
|
const ctrl = new AbortController()
|
||||||
|
const timer = setTimeout(() => ctrl.abort(), TIMEOUT)
|
||||||
|
try {
|
||||||
|
const r = await fetch(BASE + path, { headers: { 'User-Agent': 'iboard-monitor/1.0' }, signal: ctrl.signal })
|
||||||
|
if (!r.ok) throw new Error('komari ' + path + ' HTTP ' + r.status)
|
||||||
|
const j = await r.json()
|
||||||
|
if (j.status && j.status !== 'success') throw new Error('komari ' + path + ' status: ' + j.status + ' ' + (j.message || ''))
|
||||||
|
return j.data
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 节点列表 + 每节点最新实时快照 (并行拉取, 单节点失败不拖垮整体)
|
||||||
|
export async function fetchServers() {
|
||||||
|
const nodes = await jget('/api/nodes')
|
||||||
|
if (!Array.isArray(nodes)) return []
|
||||||
|
const snaps = await Promise.all(
|
||||||
|
nodes
|
||||||
|
.filter((n) => n.uuid)
|
||||||
|
.map(async (n) => {
|
||||||
|
try {
|
||||||
|
const recent = await jget('/api/recent/' + n.uuid)
|
||||||
|
return { node: n, snap: Array.isArray(recent) && recent.length ? recent[recent.length - 1] : null }
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[komari] 节点 ' + n.uuid + ' 快照失败:', e.message)
|
||||||
|
return { node: n, snap: null }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
return snaps.map(({ node: n, snap }) => {
|
||||||
|
const uuid = n.uuid
|
||||||
|
return {
|
||||||
|
uuid,
|
||||||
|
name: n.name || uuid,
|
||||||
|
cpuName: n.cpu_name || '',
|
||||||
|
os: n.os || '',
|
||||||
|
region: n.region || '',
|
||||||
|
memTotal: snap ? Number(snap.ram?.total || 0) : Number(n.mem_total || 0),
|
||||||
|
diskTotal: snap ? Number(snap.disk?.total || 0) : Number(n.disk_total || 0),
|
||||||
|
cpuCores: Number(n.cpu_cores || 0),
|
||||||
|
cpuUsage: snap ? Number(snap.cpu?.usage || 0) : 0,
|
||||||
|
ramUsed: snap ? Number(snap.ram?.used || 0) : 0,
|
||||||
|
swapUsed: snap ? Number(snap.swap?.used || 0) : 0,
|
||||||
|
load1: snap ? Number(snap.load?.load1 || 0) : 0,
|
||||||
|
load5: snap ? Number(snap.load?.load5 || 0) : 0,
|
||||||
|
load15: snap ? Number(snap.load?.load15 || 0) : 0,
|
||||||
|
diskUsed: snap ? Number(snap.disk?.used || 0) : 0,
|
||||||
|
netIn: snap ? Number(snap.network?.down || 0) : 0,
|
||||||
|
netOut: snap ? Number(snap.network?.up || 0) : 0,
|
||||||
|
uptime: snap ? Number(snap.uptime || 0) : 0,
|
||||||
|
process: snap ? Number(snap.process || 0) : 0,
|
||||||
|
connections: snap ? Number(snap.connections?.tcp || 0) : 0,
|
||||||
|
status: snap ? 'online' : 'unknown',
|
||||||
|
recordedAt: snap && snap.updated_at ? new Date(snap.updated_at).toISOString() : new Date().toISOString(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 节点时序: komari 保留最近 24h, 1 分钟粒度
|
||||||
|
// 节点未开启 record 或已下线 (komari 404) 时返回空列表, 不向上抛 500
|
||||||
|
export async function fetchHistory(uuid) {
|
||||||
|
let data
|
||||||
|
try {
|
||||||
|
data = await jget('/api/records/load?uuid=' + encodeURIComponent(uuid))
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[komari] ' + uuid + ' history 获取失败:', e.message)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const records = data && Array.isArray(data.records) ? data.records : []
|
||||||
|
return records
|
||||||
|
.map((r) => ({
|
||||||
|
period: new Date(r.time).toISOString(),
|
||||||
|
cpu: Number(r.cpu || 0),
|
||||||
|
ram: Number(r.ram || 0),
|
||||||
|
load: Number(r.load || 0),
|
||||||
|
disk: Number(r.disk || 0),
|
||||||
|
// net_in/net_out 为 komari 原始值 (字节口径), 前端展示时再按需换算
|
||||||
|
netIn: Number(r.net_in || 0),
|
||||||
|
netOut: Number(r.net_out || 0),
|
||||||
|
process: Number(r.process || 0),
|
||||||
|
connections: Number(r.connections || 0),
|
||||||
|
}))
|
||||||
|
.filter((r) => !isNaN(Date.parse(r.period)))
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
// ===== 已停用 =====
|
||||||
|
// 监控数据不再落库 (用户要求实时从 komari 获取), 本脚本的定时入库调用已从 server/cron.js 移除。
|
||||||
|
// 如需恢复"数据库记录监控数据"模式, 重新在 cron.js 注册 runOnce/cleanupOld 即可。
|
||||||
|
// komari 服务器监控拉取: 公开 API (无需 key)
|
||||||
|
// GET /api/nodes 节点列表
|
||||||
|
// GET /api/recent/{uuid} 最近实时状态 (数组, 多个时间点)
|
||||||
|
// GET /api/records/load?uuid=... 1 分钟粒度历史 (保留 24h)
|
||||||
|
const mysql = require('mysql2/promise')
|
||||||
|
|
||||||
|
const BASE = process.env.KOMARI_BASE || 'https://komari.honor3.com'
|
||||||
|
const TIMEOUT = 20000
|
||||||
|
|
||||||
|
function parseUrl(url) {
|
||||||
|
const u = new URL(url)
|
||||||
|
return { host: u.hostname, port: Number(u.port) || 3306, user: decodeURIComponent(u.username), password: decodeURIComponent(u.password), database: u.pathname.replace(/^\//, '') }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function jget(path) {
|
||||||
|
const ctrl = new AbortController()
|
||||||
|
setTimeout(() => ctrl.abort(), TIMEOUT)
|
||||||
|
const r = await fetch(BASE + path, { headers: { 'User-Agent': 'iboard-monitor/1.0' }, signal: ctrl.signal })
|
||||||
|
if (!r.ok) throw new Error('komari ' + path + ' HTTP ' + r.status)
|
||||||
|
const j = await r.json()
|
||||||
|
if (j.status && j.status !== 'success') throw new Error('komari ' + path + ' status: ' + j.status + ' ' + (j.message || ''))
|
||||||
|
return j.data
|
||||||
|
}
|
||||||
|
|
||||||
|
// 拉一次所有节点 + 最近状态 + 1 分钟历史, 入库
|
||||||
|
async function runOnce() {
|
||||||
|
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
|
||||||
|
const stats = { nodes: 0, snapshots: 0, history: 0, failed: 0 }
|
||||||
|
try {
|
||||||
|
const nodes = await jget('/api/nodes')
|
||||||
|
if (!Array.isArray(nodes)) throw new Error('nodes 格式异常')
|
||||||
|
stats.nodes = nodes.length
|
||||||
|
for (const n of nodes) {
|
||||||
|
const uuid = n.uuid
|
||||||
|
if (!uuid) continue
|
||||||
|
try {
|
||||||
|
const recent = await jget('/api/recent/' + uuid)
|
||||||
|
let snap = null
|
||||||
|
if (Array.isArray(recent) && recent.length) snap = recent[recent.length - 1]
|
||||||
|
const recordedAt = snap ? new Date(snap.updated_at) : new Date()
|
||||||
|
const ramTotal = snap && snap.ram ? Number(snap.ram.total) : Number(n.mem_total || 0)
|
||||||
|
const diskTotal = snap && snap.disk ? Number(snap.disk.total) : Number(n.disk_total || 0)
|
||||||
|
await conn.execute(
|
||||||
|
'INSERT INTO `ServerNode` (`uuid`,`name`,`cpuName`,`os`,`region`,`memTotal`,`diskTotal`,`cpuCores`,' +
|
||||||
|
'`cpuUsage`,`ramUsed`,`swapUsed`,`load1`,`load5`,`load15`,`diskUsed`,`netIn`,`netOut`,`uptime`,`process`,`connections`,`status`,`recordedAt`,`createdAt`,`updatedAt`) ' +
|
||||||
|
'VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW(3),NOW(3)) ' +
|
||||||
|
'ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`cpuName`=VALUES(`cpuName`),`os`=VALUES(`os`),`region`=VALUES(`region`),' +
|
||||||
|
'`memTotal`=VALUES(`memTotal`),`diskTotal`=VALUES(`diskTotal`),`cpuCores`=VALUES(`cpuCores`),' +
|
||||||
|
'`cpuUsage`=VALUES(`cpuUsage`),`ramUsed`=VALUES(`ramUsed`),`swapUsed`=VALUES(`swapUsed`),' +
|
||||||
|
'`load1`=VALUES(`load1`),`load5`=VALUES(`load5`),`load15`=VALUES(`load15`),`diskUsed`=VALUES(`diskUsed`),' +
|
||||||
|
'`netIn`=VALUES(`netIn`),`netOut`=VALUES(`netOut`),`uptime`=VALUES(`uptime`),' +
|
||||||
|
'`process`=VALUES(`process`),`connections`=VALUES(`connections`),`status`=VALUES(`status`),' +
|
||||||
|
'`recordedAt`=VALUES(`recordedAt`),`updatedAt`=NOW(3)',
|
||||||
|
[
|
||||||
|
uuid, n.name || '', n.cpu_name || '', n.os || '', n.region || '',
|
||||||
|
ramTotal, diskTotal, Number(n.cpu_cores || 0),
|
||||||
|
snap ? Number(snap.cpu?.usage || 0) : 0,
|
||||||
|
snap ? Number(snap.ram?.used || 0) : 0,
|
||||||
|
snap ? Number(snap.swap?.used || 0) : 0,
|
||||||
|
snap ? Number(snap.load?.load1 || 0) : 0,
|
||||||
|
snap ? Number(snap.load?.load5 || 0) : 0,
|
||||||
|
snap ? Number(snap.load?.load15 || 0) : 0,
|
||||||
|
snap ? Number(snap.disk?.used || 0) : 0,
|
||||||
|
snap ? Number(snap.network?.down || 0) : 0,
|
||||||
|
snap ? Number(snap.network?.up || 0) : 0,
|
||||||
|
snap ? Number(snap.uptime || 0) : 0,
|
||||||
|
snap ? Number(snap.process || 0) : 0,
|
||||||
|
snap ? Number(snap.connections?.tcp || 0) : 0,
|
||||||
|
snap ? 'online' : 'unknown',
|
||||||
|
recordedAt,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
stats.snapshots++
|
||||||
|
|
||||||
|
let rec = null
|
||||||
|
try { rec = await jget('/api/records/load?uuid=' + uuid) } catch (e) { /* 节点未开启 record */ }
|
||||||
|
if (rec && Array.isArray(rec.records)) {
|
||||||
|
for (const r of rec.records) {
|
||||||
|
const period = new Date(r.time)
|
||||||
|
if (isNaN(period.getTime())) continue
|
||||||
|
const [res] = await conn.execute(
|
||||||
|
'INSERT INTO `ServerNodeHistory` (`uuid`,`period`,`cpu`,`ram`,`load`,`disk`,`netIn`,`netOut`,`process`,`connections`,`createdAt`) ' +
|
||||||
|
'VALUES (?,?,?,?,?,?,?,?,?,?,NOW(3)) ' +
|
||||||
|
'ON DUPLICATE KEY UPDATE `cpu`=VALUES(`cpu`),`ram`=VALUES(`ram`),`load`=VALUES(`load`),`disk`=VALUES(`disk`),' +
|
||||||
|
'`netIn`=VALUES(`netIn`),`netOut`=VALUES(`netOut`),`process`=VALUES(`process`),`connections`=VALUES(`connections`)',
|
||||||
|
[
|
||||||
|
uuid, period,
|
||||||
|
Number(r.cpu || 0), Number(r.ram || 0), Number(r.load || 0),
|
||||||
|
Number(r.disk || 0), Number(r.net_in || 0), Number(r.net_out || 0),
|
||||||
|
Number(r.process || 0), Number(r.connections || 0),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if (res.affectedRows === 1) stats.history++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
stats.failed++
|
||||||
|
console.error('[komari] 节点 ' + uuid + ' 失败:', e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await conn.end()
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理 24h 之前的历史
|
||||||
|
async function cleanupOld() {
|
||||||
|
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
|
||||||
|
try {
|
||||||
|
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
||||||
|
const [r] = await conn.execute('DELETE FROM `ServerNodeHistory` WHERE `period` < ?', [cutoff])
|
||||||
|
return r.affectedRows
|
||||||
|
} finally {
|
||||||
|
await conn.end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { runOnce, cleanupOld, BASE }
|
||||||
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
import { prisma } from '../db.js'
|
||||||
|
import { authRequired } from '../middleware.js'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
router.use(authRequired)
|
||||||
|
|
||||||
|
// 报警列表: 按时间最新优先, cursor 分页 (cursor = id)
|
||||||
|
router.get('/alerts', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const rawLimit = Number(req.query.limit)
|
||||||
|
const limit = Number.isFinite(rawLimit) && rawLimit >= 1 ? Math.min(Math.floor(rawLimit), 50) : 10
|
||||||
|
const rawCursor = Number(req.query.cursor)
|
||||||
|
const cursor = Number.isInteger(rawCursor) && rawCursor > 0 ? rawCursor : null
|
||||||
|
const where = cursor ? { id: { lt: cursor } } : {}
|
||||||
|
const [items, total] = await Promise.all([
|
||||||
|
prisma.alert.findMany({
|
||||||
|
where,
|
||||||
|
orderBy: { id: 'desc' },
|
||||||
|
take: limit,
|
||||||
|
}),
|
||||||
|
prisma.alert.count(),
|
||||||
|
])
|
||||||
|
const nextCursor = items.length === limit ? String(items[items.length - 1].id) : null
|
||||||
|
res.json({ items, nextCursor, total })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
+18
-2
@@ -6,10 +6,26 @@ const router = Router()
|
|||||||
|
|
||||||
router.use(authRequired)
|
router.use(authRequired)
|
||||||
|
|
||||||
|
// 指标列表: 元数据 + 明细点, 过滤 hidden (数据与脚本保留, 仅不展示)
|
||||||
router.get('/indicators', async (_req, res, next) => {
|
router.get('/indicators', async (_req, res, next) => {
|
||||||
try {
|
try {
|
||||||
const rows = await prisma.indicator.findMany({ orderBy: [{ groupName: 'asc' }, { name: 'asc' }] })
|
const rows = await prisma.indicator.findMany({
|
||||||
res.json({ indicators: rows })
|
where: { hidden: false },
|
||||||
|
include: { points: { orderBy: { period: 'asc' } } },
|
||||||
|
orderBy: [{ groupName: 'asc' }, { name: 'asc' }],
|
||||||
|
})
|
||||||
|
// 组装前端友好结构: points 保持从老到新
|
||||||
|
const indicators = rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
name: r.name,
|
||||||
|
groupName: r.groupName,
|
||||||
|
title: r.title,
|
||||||
|
unit: r.unit,
|
||||||
|
source: r.source,
|
||||||
|
frequency: r.frequency,
|
||||||
|
points: r.points.map((p) => ({ period: p.period, value: p.value })),
|
||||||
|
}))
|
||||||
|
res.json({ indicators })
|
||||||
} catch (e) { next(e) }
|
} catch (e) { next(e) }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
import { prisma } from '../db.js'
|
||||||
|
import { authRequired } from '../middleware.js'
|
||||||
|
import { fetchServers, fetchHistory } from '../komari-client.js'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
router.use(authRequired)
|
||||||
|
|
||||||
|
// 服务器节点列表: 实时从 komari 拉取 (不落库)
|
||||||
|
router.get('/servers', async (_req, res, next) => {
|
||||||
|
try {
|
||||||
|
const items = await fetchServers()
|
||||||
|
res.json({ items })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 节点时序: komari 保留最近 24h, 1 分钟粒度 (不落库)
|
||||||
|
router.get('/servers/:uuid/history', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const items = await fetchHistory(req.params.uuid)
|
||||||
|
res.json({ items })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 服务健康列表 (服务监控暂留空, 接口保留)
|
||||||
|
router.get('/services', async (_req, res, next) => {
|
||||||
|
try {
|
||||||
|
const rows = await prisma.serviceCheck.findMany({
|
||||||
|
orderBy: { name: 'asc' },
|
||||||
|
})
|
||||||
|
res.json({ items: rows })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Router } from 'express'
|
||||||
|
import { prisma } from '../db.js'
|
||||||
|
import { authRequired } from '../middleware.js'
|
||||||
|
|
||||||
|
const router = Router()
|
||||||
|
router.use(authRequired)
|
||||||
|
|
||||||
|
// 新闻列表: 按时间最新优先, cursor 分页
|
||||||
|
// cursor 格式: "<publishedAt ISO>|<lastId>", 兼容旧版纯时间戳 (不带 |)
|
||||||
|
// 同秒多条时用 id 兜底, 避免翻页时整批漏掉
|
||||||
|
router.get('/news', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const rawLimit = Number(req.query.limit)
|
||||||
|
const limit = Number.isFinite(rawLimit) && rawLimit >= 1 ? Math.min(Math.floor(rawLimit), 50) : 10
|
||||||
|
let where = {}
|
||||||
|
const cursor = req.query.cursor || ''
|
||||||
|
if (cursor) {
|
||||||
|
const [ts, idRaw] = cursor.split('|')
|
||||||
|
const c = new Date(ts)
|
||||||
|
if (!isNaN(c.getTime())) {
|
||||||
|
const id = Number(idRaw)
|
||||||
|
where = id > 0
|
||||||
|
? { OR: [{ publishedAt: { lt: c } }, { publishedAt: c, id: { lt: id } }] }
|
||||||
|
: { publishedAt: { lt: c } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const items = await prisma.news.findMany({
|
||||||
|
select: { id: true, title: true, summary: true, source: true, url: true, publishedAt: true, impactRank: true },
|
||||||
|
where,
|
||||||
|
orderBy: [{ publishedAt: 'desc' }, { id: 'desc' }],
|
||||||
|
take: limit,
|
||||||
|
})
|
||||||
|
const nextCursor = items.length === limit && items.length > 0
|
||||||
|
? items[items.length - 1].publishedAt.toISOString() + '|' + items[items.length - 1].id
|
||||||
|
: null
|
||||||
|
res.json({ items, nextCursor })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 新闻详情: 含正文 content
|
||||||
|
router.get('/news/:id', async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const id = Number(req.params.id)
|
||||||
|
if (!Number.isInteger(id) || id <= 0) return res.status(400).json({ error: '无效的新闻 id' })
|
||||||
|
const item = await prisma.news.findUnique({ where: { id } })
|
||||||
|
if (!item) return res.status(404).json({ error: '新闻不存在' })
|
||||||
|
res.json({ item })
|
||||||
|
} catch (e) { next(e) }
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
@@ -2,8 +2,10 @@ import { Routes, Route, Navigate, useLocation } from 'react-router-dom'
|
|||||||
import MobileShell from './layouts/MobileShell.jsx'
|
import MobileShell from './layouts/MobileShell.jsx'
|
||||||
import LoginPage from './pages/LoginPage.jsx'
|
import LoginPage from './pages/LoginPage.jsx'
|
||||||
import HomePage from './pages/HomePage.jsx'
|
import HomePage from './pages/HomePage.jsx'
|
||||||
|
import NewsDetail from './pages/news/NewsDetail.jsx'
|
||||||
import MacroPage from './pages/MacroPage.jsx'
|
import MacroPage from './pages/MacroPage.jsx'
|
||||||
import MonitorPage from './pages/MonitorPage.jsx'
|
import MonitorPage from './pages/MonitorPage.jsx'
|
||||||
|
import ServerDetail from './pages/monitor/ServerDetail.jsx'
|
||||||
import MePage from './pages/MePage.jsx'
|
import MePage from './pages/MePage.jsx'
|
||||||
import { useAuth } from './auth/AuthContext.jsx'
|
import { useAuth } from './auth/AuthContext.jsx'
|
||||||
|
|
||||||
@@ -30,8 +32,10 @@ export default function App() {
|
|||||||
<Route path="/login" element={<RedirectIfAuthed><LoginPage /></RedirectIfAuthed>} />
|
<Route path="/login" element={<RedirectIfAuthed><LoginPage /></RedirectIfAuthed>} />
|
||||||
<Route element={<RequireAuth><MobileShell /></RequireAuth>}>
|
<Route element={<RequireAuth><MobileShell /></RequireAuth>}>
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/news/:id" element={<NewsDetail />} />
|
||||||
<Route path="/macro/*" element={<MacroPage />} />
|
<Route path="/macro/*" element={<MacroPage />} />
|
||||||
<Route path="/monitor" element={<MonitorPage />} />
|
<Route path="/monitor" element={<MonitorPage />} />
|
||||||
|
<Route path="/monitor/server/:uuid" element={<ServerDetail />} />
|
||||||
<Route path="/me" element={<MePage />} />
|
<Route path="/me" element={<MePage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
|||||||
@@ -1,26 +1,49 @@
|
|||||||
import { NavLink } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
import { TabBar } from 'antd-mobile'
|
||||||
|
import TabIcon from './TabIcon.jsx'
|
||||||
|
|
||||||
|
// 底部 Tab 配置: key 既是路由前缀, 又是 icon 名
|
||||||
|
// 使用 antd-mobile TabBar: 自带 safe-area 处理, 仿原生 iOS/Android 风格
|
||||||
const TABS = [
|
const TABS = [
|
||||||
{ to: '/', label: '首页', icon: '⌂' },
|
{ key: '/', name: 'home', label: '首页' },
|
||||||
{ to: '/macro', label: '宏观', icon: '◫' },
|
{ key: '/macro', name: 'macro', label: '宏观' },
|
||||||
{ to: '/monitor', label: '监控', icon: '◉' },
|
{ key: '/monitor', name: 'monitor', label: '监控' },
|
||||||
{ to: '/me', label: '我', icon: '☺' },
|
{ key: '/me', name: 'me', label: '我' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 根据当前路径计算激活的 tab key (精确匹配 '/' , 其他前缀匹配)
|
||||||
|
function activeKeyFromPath(pathname) {
|
||||||
|
for (const t of TABS) {
|
||||||
|
if (t.key === '/') {
|
||||||
|
if (pathname === '/') return t.key
|
||||||
|
} else if (pathname === t.key || pathname.startsWith(t.key + '/')) {
|
||||||
|
return t.key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端底部 Tab 导航: antd-mobile TabBar, 内置安全区
|
||||||
|
// 作用: 渲染底部 4 个 tab, 切换路由; 激活态根据当前 URL 计算
|
||||||
export default function BottomNav() {
|
export default function BottomNav() {
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const activeKey = activeKeyFromPath(pathname)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="bottom-nav" aria-label="主导航">
|
<TabBar
|
||||||
|
activeKey={activeKey}
|
||||||
|
safeArea
|
||||||
|
className="app-tabbar"
|
||||||
|
onChange={navigate}
|
||||||
|
>
|
||||||
{TABS.map((t) => (
|
{TABS.map((t) => (
|
||||||
<NavLink
|
<TabBar.Item
|
||||||
key={t.to}
|
key={t.key}
|
||||||
to={t.to}
|
title={t.label}
|
||||||
end={t.to === '/'}
|
icon={(active) => <TabIcon name={t.name} active={active} />}
|
||||||
className={({ isActive }) => 'bottom-nav-item' + (isActive ? ' active' : '')}
|
/>
|
||||||
>
|
|
||||||
<span className="icon" aria-hidden="true">{t.icon}</span>
|
|
||||||
<span>{t.label}</span>
|
|
||||||
</NavLink>
|
|
||||||
))}
|
))}
|
||||||
</nav>
|
</TabBar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,86 +1,27 @@
|
|||||||
import { useRef, useState, useEffect } from 'react'
|
import { Tabs } from 'antd-mobile'
|
||||||
|
|
||||||
// 滑屏容器 + 顶栏 tabs
|
// 顶部分类 + 下方仅渲染当前激活 tab 的内容
|
||||||
|
// - 顶部: antd-mobile Tabs (点击切换)
|
||||||
|
// - 下方: 同时挂载所有 tab, 通过 [hidden] 仅显示激活的那个 (保留各 tab 内部状态)
|
||||||
// props:
|
// props:
|
||||||
// tabs: [{ key, label, render }]
|
// tabs: [{ key, label, render }]
|
||||||
// active: 当前激活 key
|
// active: 当前激活 key
|
||||||
// onChange(key): 切换回调 (用户滑动或点击 tab)
|
// onChange(key): 切换回调
|
||||||
export default function SwipeTabs({ tabs, active, onChange }) {
|
export default function SwipeTabs({ tabs, active, onChange }) {
|
||||||
const idx = Math.max(0, tabs.findIndex((t) => t.key === active))
|
|
||||||
const [dragging, setDragging] = useState(false)
|
|
||||||
const [offsetPx, setOffsetPx] = useState(0)
|
|
||||||
const [animOff, setAnimOff] = useState(false)
|
|
||||||
const startX = useRef(0)
|
|
||||||
const containerRef = useRef(null)
|
|
||||||
|
|
||||||
// idx 变化时关闭拖动动画 (避免快速切换时滑动残影)
|
|
||||||
useEffect(() => { setOffsetPx(0); setAnimOff(false) }, [idx])
|
|
||||||
|
|
||||||
function onTouchStart(e) {
|
|
||||||
startX.current = e.touches[0].clientX
|
|
||||||
setDragging(true)
|
|
||||||
setAnimOff(true)
|
|
||||||
}
|
|
||||||
function onTouchMove(e) {
|
|
||||||
if (!dragging) return
|
|
||||||
const dx = e.touches[0].clientX - startX.current
|
|
||||||
const w = containerRef.current?.offsetWidth || window.innerWidth
|
|
||||||
// 限制左右溢出 (超过边界阻力增大)
|
|
||||||
const limited = Math.max(Math.min(dx, w * 0.4), -w * 0.4)
|
|
||||||
setOffsetPx(limited)
|
|
||||||
}
|
|
||||||
function onTouchEnd() {
|
|
||||||
if (!dragging) return
|
|
||||||
setDragging(false)
|
|
||||||
setAnimOff(false)
|
|
||||||
const w = containerRef.current?.offsetWidth || window.innerWidth
|
|
||||||
const dx = offsetPx
|
|
||||||
let next = idx
|
|
||||||
if (dx < -w * 0.18 && idx < tabs.length - 1) next = idx + 1
|
|
||||||
else if (dx > w * 0.18 && idx > 0) next = idx - 1
|
|
||||||
setOffsetPx(0)
|
|
||||||
if (next !== idx) onChange(tabs[next].key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 用 -33.33% × idx 作为基础位移, 再叠加拖动偏移
|
|
||||||
const basePct = idx * 33.3333
|
|
||||||
const dragPct = containerRef.current
|
|
||||||
? (offsetPx / containerRef.current.offsetWidth) * 100
|
|
||||||
: 0
|
|
||||||
const translate = `calc(-${basePct}% + ${dragPct}%)`
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="swipe-tabs-wrap">
|
||||||
<div className="top-tabs" role="tablist">
|
<Tabs activeKey={active} onChange={onChange}>
|
||||||
{tabs.map((t) => (
|
{tabs.map((t) => (
|
||||||
<button
|
<Tabs.Tab key={t.key} title={t.label} />
|
||||||
key={t.key}
|
))}
|
||||||
role="tab"
|
</Tabs>
|
||||||
aria-selected={t.key === active}
|
<div className="tab-content">
|
||||||
className={'top-tabs-item' + (t.key === active ? ' active' : '')}
|
{tabs.map((t) => (
|
||||||
onClick={() => onChange(t.key)}
|
<div key={t.key} className="tab-pane" hidden={t.key !== active}>
|
||||||
>
|
{t.render()}
|
||||||
{t.label}
|
</div>
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div
|
</div>
|
||||||
ref={containerRef}
|
|
||||||
className="swipe-container"
|
|
||||||
onTouchStart={onTouchStart}
|
|
||||||
onTouchMove={onTouchMove}
|
|
||||||
onTouchEnd={onTouchEnd}
|
|
||||||
onTouchCancel={onTouchEnd}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={'swipe-track' + (animOff || dragging ? ' no-anim' : '')}
|
|
||||||
style={{ '--swipe-offset': translate, transform: `translateX(${translate})` }}
|
|
||||||
>
|
|
||||||
{tabs.map((t) => (
|
|
||||||
<div key={t.key} className="swipe-pane">{t.render()}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// 底部 TabBar 图标: 简洁线性 SVG, 仿原生 App 风格
|
||||||
|
// props: name 字符串, active 布尔
|
||||||
|
// 返回对应的 24x24 SVG 节点
|
||||||
|
const ICONS = {
|
||||||
|
home: (active) => (
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M3 11l9-7 9 7" />
|
||||||
|
<path d="M5 10v10h14V10" />
|
||||||
|
{active ? <path d="M10 20v-5h4v5" fill="currentColor" stroke="none" /> : null}
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
macro: (active) => (
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<rect x="3" y="3" width="7" height="9" rx="1" fill={active ? 'currentColor' : 'none'} fillOpacity={active ? 0.15 : 0} />
|
||||||
|
<rect x="14" y="3" width="7" height="5" rx="1" />
|
||||||
|
<rect x="14" y="12" width="7" height="9" rx="1" fill={active ? 'currentColor' : 'none'} fillOpacity={active ? 0.15 : 0} />
|
||||||
|
<rect x="3" y="16" width="7" height="5" rx="1" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
monitor: (active) => (
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M3 12h4l2-5 4 10 2-5h6" fill={active ? 'currentColor' : 'none'} fillOpacity={active ? 0.12 : 0} />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
me: (active) => (
|
||||||
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={active ? 2.2 : 1.8} strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<circle cx="12" cy="8" r="4" fill={active ? 'currentColor' : 'none'} fillOpacity={active ? 0.15 : 0} />
|
||||||
|
<path d="M4 20c1.5-4 4.5-6 8-6s6.5 2 8 6" />
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TabIcon({ name, active }) {
|
||||||
|
const render = ICONS[name]
|
||||||
|
if (!render) return null
|
||||||
|
return <span className="tab-icon">{render(!!active)}</span>
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
// 趋势徽标: 上升 / 下降 / 持平 / 震荡
|
||||||
|
// props:
|
||||||
|
// trend: 'up' | 'down' | 'flat' | 'volatile'
|
||||||
|
// showText: 是否显示文字 (默认 true)
|
||||||
|
const META = {
|
||||||
|
up: { icon: '\u2191', text: '上升' },
|
||||||
|
down: { icon: '\u2193', text: '下降' },
|
||||||
|
flat: { icon: '=', text: '持平' },
|
||||||
|
volatile: { icon: '\u223F', text: '震荡' },
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据最近 N 个数据点计算趋势:
|
||||||
|
// - 数据不足 2 个 → flat
|
||||||
|
// - 取最近 6 个点的变化, 整体涨幅 < 1% → flat
|
||||||
|
// - 单调递增 → up, 单调递减 → down
|
||||||
|
// - 其余 (有升有降) → volatile
|
||||||
|
// 返回: { trend, change } (change 为百分比变化, 保留 1 位小数)
|
||||||
|
export function calcTrend(rows) {
|
||||||
|
if (!rows || rows.length < 2) return { trend: 'flat', change: 0 }
|
||||||
|
const N = Math.min(6, rows.length)
|
||||||
|
const recent = rows.slice(-N).map((r) => r.value).filter((v) => typeof v === 'number')
|
||||||
|
if (recent.length < 2) return { trend: 'flat', change: 0 }
|
||||||
|
const first = recent[0]
|
||||||
|
const last = recent[recent.length - 1]
|
||||||
|
const change = first === 0 ? 0 : ((last - first) / Math.abs(first)) * 100
|
||||||
|
if (Math.abs(change) < 1) return { trend: 'flat', change }
|
||||||
|
let up = 0, down = 0
|
||||||
|
for (let i = 1; i < recent.length; i++) {
|
||||||
|
if (recent[i] > recent[i - 1]) up++
|
||||||
|
else if (recent[i] < recent[i - 1]) down++
|
||||||
|
}
|
||||||
|
if (up === recent.length - 1) return { trend: 'up', change }
|
||||||
|
if (down === recent.length - 1) return { trend: 'down', change }
|
||||||
|
return { trend: 'volatile', change }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取数据序列最后一个有效点: { month, value } 或 null
|
||||||
|
export function lastPoint(data) {
|
||||||
|
const rows = (data && data.rows) || []
|
||||||
|
for (let i = rows.length - 1; i >= 0; i--) {
|
||||||
|
if (rows[i] && typeof rows[i].value === 'number') return rows[i]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TrendBadge({ trend, showText = true }) {
|
||||||
|
const m = META[trend] || META.flat
|
||||||
|
return (
|
||||||
|
<span className={'trend-badge trend-' + (trend || 'flat')}>
|
||||||
|
<span className="trend-icon">{m.icon}</span>
|
||||||
|
{showText && <span>{m.text}</span>}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
import { Outlet } from 'react-router-dom'
|
import { Outlet } from 'react-router-dom'
|
||||||
|
import { SafeArea } from 'antd-mobile'
|
||||||
import BottomNav from '../components/BottomNav.jsx'
|
import BottomNav from '../components/BottomNav.jsx'
|
||||||
|
|
||||||
// 移动端应用外壳: 底栏 + 滚动主区
|
// 移动端应用外壳
|
||||||
|
// - 顶部 SafeArea: 让出手机状态栏高度
|
||||||
|
// - 中间 <main>: flex:1 可滚动主区
|
||||||
|
// - 底部 TabBar: antd-mobile TabBar 自带底部安全区
|
||||||
|
// 解决: 状态栏遮挡/底部 TabBar 遮挡/左右贴边
|
||||||
export default function MobileShell() {
|
export default function MobileShell() {
|
||||||
return (
|
return (
|
||||||
<div className="app-root">
|
<div className="app-root">
|
||||||
|
<SafeArea position="top" />
|
||||||
<main className="app-main">
|
<main className="app-main">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
// 远程调试: 仅在 Tauri WebView 内启用, web 模式自动跳过
|
// 远程调试: 仅在 Tauri WebView 内启用, web 模式自动跳过
|
||||||
import './debug/pagespy.js'
|
import './debug/pagespy.js'
|
||||||
|
|
||||||
|
// 移动端 UI 框架: 全局样式 + 主题 (antd-mobile 5)
|
||||||
|
import 'antd-mobile/es/global'
|
||||||
|
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
|
|||||||
+186
-5
@@ -1,10 +1,191 @@
|
|||||||
// 首页: 暂留占位, 后续接入监控告警 / 个性化推荐
|
import { useEffect, useState, useRef, useCallback } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { NavBar, Card, Empty, DotLoading, Badge } from 'antd-mobile'
|
||||||
|
import { api } from '../api/client.js'
|
||||||
|
import SwipeTabs from '../components/SwipeTabs.jsx'
|
||||||
|
|
||||||
|
// 首页: 报警模块 + 新闻模块
|
||||||
|
// - 报警: World Bank 与妙想 MCP 跨源同 (指标, 周期) 数据差异超阈值 (按时间倒序)
|
||||||
|
// - 新闻: mx_finance_search_news 每日 3 点拉取国内外经济政治新闻 (按时间倒序)
|
||||||
|
// - 两个模块均无限滚动, 触底自动加载下一页
|
||||||
|
function fmtDateTime(s) {
|
||||||
|
if (!s) return ''
|
||||||
|
const d = new Date(s)
|
||||||
|
if (isNaN(d.getTime())) return s
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes())
|
||||||
|
}
|
||||||
|
function fmtDate(s) {
|
||||||
|
if (!s) return ''
|
||||||
|
const d = new Date(s)
|
||||||
|
if (isNaN(d.getTime())) return s
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
|
||||||
|
}
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
|
const [active, setActive] = useState('news') // 默认选中新闻
|
||||||
|
const [alertCount, setAlertCount] = useState(0)
|
||||||
|
const handleAlertCount = useCallback((n) => setAlertCount(n), [])
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
key: 'alerts',
|
||||||
|
label: (
|
||||||
|
<span className="tab-label">
|
||||||
|
报警
|
||||||
|
{alertCount > 0 && <Badge content={alertCount} />}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
render: () => <AlertSection onCount={handleAlertCount} />,
|
||||||
|
},
|
||||||
|
{ key: 'news', label: '新闻', render: () => <NewsSection /> },
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="placeholder-page">
|
<div className="page-with-navbar">
|
||||||
<div className="icon">⌂</div>
|
<NavBar back={null}>首页</NavBar>
|
||||||
<div>首页</div>
|
<div className="home-content">
|
||||||
<div style={{ fontSize: 12, opacity: .7 }}>后续接入: 监控告警 / 个性化推荐 / 收藏的指标</div>
|
<SwipeTabs tabs={tabs} active={active} onChange={setActive} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 报警模块
|
||||||
|
function AlertSection({ onCount }) {
|
||||||
|
const [items, setItems] = useState([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [done, setDone] = useState(false)
|
||||||
|
const cursorRef = useRef(null)
|
||||||
|
const sentinelRef = useRef(null)
|
||||||
|
const inFlightRef = useRef(false)
|
||||||
|
|
||||||
|
const fetchPage = useCallback(async () => {
|
||||||
|
if (inFlightRef.current || done) return
|
||||||
|
inFlightRef.current = true
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const url = '/alerts?limit=10' + (cursorRef.current ? '&cursor=' + cursorRef.current : '')
|
||||||
|
const data = await api(url)
|
||||||
|
setItems((prev) => prev.concat(data.items || []))
|
||||||
|
onCount?.(data.total ?? (data.items?.length || 0))
|
||||||
|
cursorRef.current = data.nextCursor
|
||||||
|
if (!data.nextCursor) setDone(true)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('alerts load error', e)
|
||||||
|
setDone(true)
|
||||||
|
} finally {
|
||||||
|
inFlightRef.current = false
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [done, onCount])
|
||||||
|
|
||||||
|
useEffect(() => { fetchPage() }, [])
|
||||||
|
useEffect(() => {
|
||||||
|
if (done || items.length === 0) return
|
||||||
|
const el = sentinelRef.current
|
||||||
|
if (!el) return
|
||||||
|
const io = new IntersectionObserver((entries) => {
|
||||||
|
if (entries[0].isIntersecting) fetchPage()
|
||||||
|
}, { rootMargin: '200px' })
|
||||||
|
io.observe(el)
|
||||||
|
return () => io.disconnect()
|
||||||
|
}, [items.length, done, fetchPage])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="home-section">
|
||||||
|
<div className="section-title">报警</div>
|
||||||
|
{items.length === 0 && !loading ? (
|
||||||
|
<Empty description="暂无报警" />
|
||||||
|
) : (
|
||||||
|
<div className="section-list">
|
||||||
|
{items.map((a) => (
|
||||||
|
<Card key={a.id} className="alert-card">
|
||||||
|
<div className="alert-title">
|
||||||
|
<span className="alert-name">{a.indicatorName}</span>
|
||||||
|
<span className="alert-period">{a.period}</span>
|
||||||
|
</div>
|
||||||
|
<div className="alert-diff">
|
||||||
|
{a.sourceA} = {a.valueA} vs {a.sourceB} = {a.valueB}
|
||||||
|
<span className="alert-ratio"> 差异 {(a.diffRatio * 100).toFixed(1)}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="alert-time">{fmtDateTime(a.createdAt)}</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
<div ref={sentinelRef} className="scroll-sentinel">
|
||||||
|
{loading ? <DotLoading /> : (done ? '— 已加载全部 —' : '')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新闻模块
|
||||||
|
function NewsSection() {
|
||||||
|
const [items, setItems] = useState([])
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
const [done, setDone] = useState(false)
|
||||||
|
const cursorRef = useRef(null)
|
||||||
|
const sentinelRef = useRef(null)
|
||||||
|
const inFlightRef = useRef(false)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const fetchPage = useCallback(async () => {
|
||||||
|
if (inFlightRef.current || done) return
|
||||||
|
inFlightRef.current = true
|
||||||
|
setLoading(true)
|
||||||
|
try {
|
||||||
|
const url = '/news?limit=10' + (cursorRef.current ? '&cursor=' + encodeURIComponent(cursorRef.current) : '')
|
||||||
|
const data = await api(url)
|
||||||
|
setItems((prev) => prev.concat(data.items || []))
|
||||||
|
cursorRef.current = data.nextCursor
|
||||||
|
if (!data.nextCursor) setDone(true)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('news load error', e)
|
||||||
|
setDone(true)
|
||||||
|
} finally {
|
||||||
|
inFlightRef.current = false
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}, [done])
|
||||||
|
|
||||||
|
useEffect(() => { fetchPage() }, [])
|
||||||
|
useEffect(() => {
|
||||||
|
if (done || items.length === 0) return
|
||||||
|
const el = sentinelRef.current
|
||||||
|
if (!el) return
|
||||||
|
const io = new IntersectionObserver((entries) => {
|
||||||
|
if (entries[0].isIntersecting) fetchPage()
|
||||||
|
}, { rootMargin: '200px' })
|
||||||
|
io.observe(el)
|
||||||
|
return () => io.disconnect()
|
||||||
|
}, [items.length, done, fetchPage])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="home-section">
|
||||||
|
<div className="section-title">新闻</div>
|
||||||
|
{items.length === 0 && !loading ? (
|
||||||
|
<Empty description="暂无新闻" />
|
||||||
|
) : (
|
||||||
|
<div className="section-list">
|
||||||
|
{items.map((n) => (
|
||||||
|
<Card key={n.id} className="news-card" onClick={() => navigate('/news/' + n.id)}>
|
||||||
|
<div className="news-title">{n.title}</div>
|
||||||
|
{n.summary && <div className="news-summary">{n.summary}</div>}
|
||||||
|
<div className="news-meta">
|
||||||
|
<span className="news-source">{n.source || '未知来源'}</span>
|
||||||
|
<span className="news-time">{fmtDate(n.publishedAt)}</span>
|
||||||
|
<span className="news-rank">#{n.impactRank}</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
<div ref={sentinelRef} className="scroll-sentinel">
|
||||||
|
{loading ? <DotLoading /> : (done ? '— 已加载全部 —' : '')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
+52
-22
@@ -1,7 +1,10 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useNavigate, useLocation, Navigate } from 'react-router-dom'
|
import { useNavigate, useLocation, Navigate } from 'react-router-dom'
|
||||||
|
import { Button, Card, Checkbox, Input, Toast } from 'antd-mobile'
|
||||||
import { useAuth } from '../auth/AuthContext.jsx'
|
import { useAuth } from '../auth/AuthContext.jsx'
|
||||||
|
|
||||||
|
// 登录页 (未走 MobileShell, 独立全屏布局)
|
||||||
|
// 使用 antd-mobile: Input / Checkbox / Button / Card / Toast
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const { user, login } = useAuth()
|
const { user, login } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -22,6 +25,7 @@ export default function LoginPage() {
|
|||||||
setBusy(true)
|
setBusy(true)
|
||||||
try {
|
try {
|
||||||
await login(email.trim(), password, remember)
|
await login(email.trim(), password, remember)
|
||||||
|
Toast.show({ icon: 'success', content: '登录成功' })
|
||||||
navigate(from, { replace: true })
|
navigate(from, { replace: true })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setErr(e.message)
|
setErr(e.message)
|
||||||
@@ -32,29 +36,55 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-page">
|
<div className="login-page">
|
||||||
<form className="login-card" onSubmit={onSubmit}>
|
<form className="login-form" onSubmit={onSubmit}>
|
||||||
<div className="login-title">iBoard</div>
|
<div className="login-brand">iBoard</div>
|
||||||
<div className="login-subtitle">登录后继续</div>
|
<div className="login-subtitle">登录后继续</div>
|
||||||
{err && <div className="login-error">{err}</div>}
|
<Card className="login-card">
|
||||||
<div className="login-field">
|
{err && <div className="login-error">{err}</div>}
|
||||||
<label htmlFor="email">邮箱</label>
|
<div className="login-field">
|
||||||
<input id="email" type="email" autoComplete="username" required value={email} onChange={(e) => setEmail(e.target.value)} />
|
<label htmlFor="email">邮箱</label>
|
||||||
</div>
|
<Input
|
||||||
<div className="login-field">
|
id="email"
|
||||||
<label htmlFor="password">密码</label>
|
type="email"
|
||||||
<input id="password" type="password" autoComplete="current-password" required value={password} onChange={(e) => setPassword(e.target.value)} />
|
autoComplete="username"
|
||||||
</div>
|
required
|
||||||
<label className="login-options">
|
value={email}
|
||||||
<input type="checkbox" checked={remember} onChange={(e) => setRemember(e.target.checked)} />
|
onChange={setEmail}
|
||||||
<span>记住登录 (7 天免登录)</span>
|
placeholder="请输入邮箱"
|
||||||
</label>
|
/>
|
||||||
<button className="login-btn" type="submit" disabled={busy}>
|
</div>
|
||||||
{busy ? '登录中...' : '登录'}
|
<div className="login-field">
|
||||||
</button>
|
<label htmlFor="password">密码</label>
|
||||||
<div className="login-hint">
|
<Input
|
||||||
默认账号: admin@iboard.local<br />
|
id="password"
|
||||||
首次登录后请尽快修改密码
|
type="password"
|
||||||
</div>
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={setPassword}
|
||||||
|
placeholder="请输入密码"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="login-options">
|
||||||
|
<Checkbox checked={remember} onChange={setRemember} block>
|
||||||
|
记住登录 (7 天免登录)
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
block
|
||||||
|
color="primary"
|
||||||
|
size="large"
|
||||||
|
type="submit"
|
||||||
|
loading={busy}
|
||||||
|
loadingText="登录中"
|
||||||
|
>
|
||||||
|
登录
|
||||||
|
</Button>
|
||||||
|
<div className="login-hint">
|
||||||
|
默认账号: admin@iboard.local<br />
|
||||||
|
首次登录后请尽快修改密码
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,10 +45,13 @@ export default function MacroPage() {
|
|||||||
if (next) navigate(next.path)
|
if (next) navigate(next.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 详情页 /macro/articles/:slug 单独走 React Router 渲染, 不参与滑屏
|
// 详情页 /macro/articles/:slug 和 /macro/data/:name 单独走 React Router 渲染, 不参与滑屏
|
||||||
if (location.pathname.match(/^\/macro\/articles\/[^/]+$/)) {
|
// 这样浏览器/手机物理返回键也能正常回到列表
|
||||||
|
if (location.pathname.match(/^\/macro\/(articles|data|strategy)\/[^/]+$/)) {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route path="data/:name" element={<DataView />} />
|
||||||
|
<Route path="strategy/:name" element={<StrategyView />} />
|
||||||
<Route path="articles/:slug" element={<ArticleDetail />} />
|
<Route path="articles/:slug" element={<ArticleDetail />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
|
|||||||
+30
-17
@@ -1,36 +1,49 @@
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { Avatar, List, NavBar, Toast } from 'antd-mobile'
|
||||||
import { useAuth } from '../auth/AuthContext.jsx'
|
import { useAuth } from '../auth/AuthContext.jsx'
|
||||||
|
|
||||||
|
// 我的页: 顶部 NavBar + 用户头像卡片 + 信息列表
|
||||||
|
// 使用 antd-mobile: NavBar / List / Avatar / Toast
|
||||||
export default function MePage() {
|
export default function MePage() {
|
||||||
const { user, logout } = useAuth()
|
const { user, logout } = useAuth()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
async function onLogout() {
|
async function onLogout() {
|
||||||
if (!confirm('确定退出登录?')) return
|
const confirmed = window.confirm('确定退出登录?')
|
||||||
|
if (!confirmed) return
|
||||||
await logout()
|
await logout()
|
||||||
|
Toast.show({ icon: 'success', content: '已退出' })
|
||||||
navigate('/login', { replace: true })
|
navigate('/login', { replace: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="me-page">
|
||||||
<div className="me-header">
|
<NavBar back={null} className="me-navbar">我的</NavBar>
|
||||||
|
<div className="me-profile">
|
||||||
|
<Avatar
|
||||||
|
className="me-avatar"
|
||||||
|
src={user?.avatar}
|
||||||
|
style={{ '--size': '64px', '--border-radius': '50%' }}
|
||||||
|
>
|
||||||
|
{(user?.name || '?').slice(0, 1).toUpperCase()}
|
||||||
|
</Avatar>
|
||||||
<div className="me-name">{user?.name || '用户'}</div>
|
<div className="me-name">{user?.name || '用户'}</div>
|
||||||
<div className="me-email">{user?.email}</div>
|
<div className="me-email">{user?.email}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="me-list">
|
<List className="me-list" mode="card">
|
||||||
<div className="list-item">
|
<List.Item extra={'#' + (user?.id || '-')}>用户 ID</List.Item>
|
||||||
<span className="label">用户 ID</span>
|
<List.Item extra={user?.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}>
|
||||||
<span className="value">#{user?.id}</span>
|
注册时间
|
||||||
</div>
|
</List.Item>
|
||||||
<div className="list-item">
|
<List.Item
|
||||||
<span className="label">注册时间</span>
|
onClick={onLogout}
|
||||||
<span className="value">{user?.createdAt ? new Date(user.createdAt).toLocaleDateString() : '-'}</span>
|
clickable
|
||||||
</div>
|
arrowIcon={false}
|
||||||
<div className="list-item" onClick={onLogout} role="button" style={{ cursor: 'pointer' }}>
|
className="me-logout"
|
||||||
<span className="label danger">退出登录</span>
|
>
|
||||||
<span className="value">›</span>
|
退出登录
|
||||||
</div>
|
</List.Item>
|
||||||
</div>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+141
-5
@@ -1,10 +1,146 @@
|
|||||||
// 监控: 暂留占位
|
import { useEffect, useState, useCallback } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { NavBar, Card, Empty, DotLoading } from 'antd-mobile'
|
||||||
|
import { api } from '../api/client.js'
|
||||||
|
import SwipeTabs from '../components/SwipeTabs.jsx'
|
||||||
|
|
||||||
|
// 监控页: 两个横向分类
|
||||||
|
// - 主机监控: 实时数据来自 komari (服务端实时代理, 不落库), 点击节点看 24h 时序
|
||||||
|
// - 服务监控: 暂留空占位
|
||||||
|
function fmtTime(s) {
|
||||||
|
if (!s) return ''
|
||||||
|
const d = new Date(s)
|
||||||
|
if (isNaN(d.getTime())) return s
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
return pad(d.getHours()) + ':' + pad(d.getMinutes())
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtBytes(b) {
|
||||||
|
if (!b) return '0 B'
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
let i = 0
|
||||||
|
let v = Number(b)
|
||||||
|
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++ }
|
||||||
|
return v.toFixed(v >= 100 ? 0 : 1) + ' ' + units[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtUptime(sec) {
|
||||||
|
const s = Number(sec) || 0
|
||||||
|
const d = Math.floor(s / 86400)
|
||||||
|
const h = Math.floor((s % 86400) / 3600)
|
||||||
|
return d > 0 ? d + '天' + h + '小时' : h + '小时'
|
||||||
|
}
|
||||||
|
|
||||||
|
function pct(used, total) {
|
||||||
|
return total > 0 ? Math.min(100, (used / total) * 100) : 0
|
||||||
|
}
|
||||||
|
|
||||||
export default function MonitorPage() {
|
export default function MonitorPage() {
|
||||||
|
const [active, setActive] = useState('servers')
|
||||||
|
const tabs = [
|
||||||
|
{ key: 'servers', label: '主机监控', render: () => <ServerView /> },
|
||||||
|
{ key: 'services', label: '服务监控', render: () => <ServiceView /> },
|
||||||
|
]
|
||||||
return (
|
return (
|
||||||
<div className="placeholder-page">
|
<div className="page-with-navbar">
|
||||||
<div className="icon">◉</div>
|
<NavBar back={null}>监控</NavBar>
|
||||||
<div>监控</div>
|
<div className="monitor-content">
|
||||||
<div style={{ fontSize: 12, opacity: .7 }}>后续接入: 指标阈值告警 / 时序趋势</div>
|
<SwipeTabs tabs={tabs} active={active} onChange={setActive} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主机监控: komari 实时节点列表 (30s 轮询, 静默刷新)
|
||||||
|
function ServerView() {
|
||||||
|
const [items, setItems] = useState(null) // null = 加载中
|
||||||
|
const [err, setErr] = useState('')
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const load = useCallback((silent) => {
|
||||||
|
if (!silent) { setErr(''); setItems(null) }
|
||||||
|
api('/servers')
|
||||||
|
.then((data) => { setErr(''); setItems(data.items || []) })
|
||||||
|
.catch((e) => { if (!silent) { setErr(e.message); setItems([]) } })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load(false)
|
||||||
|
const timer = setInterval(() => load(true), 30000)
|
||||||
|
return () => clearInterval(timer)
|
||||||
|
}, [load])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="server-list">
|
||||||
|
{items === null && <div className="monitor-loading"><DotLoading /> 加载中...</div>}
|
||||||
|
{err && (
|
||||||
|
<div className="error-banner">
|
||||||
|
{err}
|
||||||
|
<span className="retry-link" onClick={load}>重试</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{items !== null && !err && items.length === 0 && <Empty description="暂无主机" />}
|
||||||
|
{items !== null && items.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="monitor-tip">实时数据 · 每 30 秒自动刷新</div>
|
||||||
|
<div className="section-list">
|
||||||
|
{items.map((n) => (
|
||||||
|
<Card key={n.uuid} className="server-card" onClick={() => navigate('/monitor/server/' + n.uuid)}>
|
||||||
|
<div className="server-head">
|
||||||
|
<span className="server-name">
|
||||||
|
<span className={'status-dot ' + (n.status === 'online' ? 'on' : 'off')} />
|
||||||
|
{n.name}
|
||||||
|
</span>
|
||||||
|
<span className="server-update">更新于 {fmtTime(n.recordedAt)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="server-metrics">
|
||||||
|
<div className="metric">
|
||||||
|
<div className="metric-label">CPU</div>
|
||||||
|
<Meter percent={n.cpuUsage} />
|
||||||
|
<div className="metric-value">{n.cpuUsage.toFixed ? n.cpuUsage.toFixed(1) : n.cpuUsage}%</div>
|
||||||
|
</div>
|
||||||
|
<div className="metric">
|
||||||
|
<div className="metric-label">内存</div>
|
||||||
|
<Meter percent={pct(n.ramUsed, n.memTotal)} />
|
||||||
|
<div className="metric-value">{fmtBytes(n.ramUsed)} / {fmtBytes(n.memTotal)}</div>
|
||||||
|
</div>
|
||||||
|
<div className="metric">
|
||||||
|
<div className="metric-label">磁盘</div>
|
||||||
|
<Meter percent={pct(n.diskUsed, n.diskTotal)} />
|
||||||
|
<div className="metric-value">{fmtBytes(n.diskUsed)} / {fmtBytes(n.diskTotal)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="server-foot">
|
||||||
|
<span>负载 {n.load1} / {n.load5} / {n.load15}</span>
|
||||||
|
<span>{n.cpuCores} 核</span>
|
||||||
|
<span>进程 {n.process}</span>
|
||||||
|
<span>连接 {n.connections}</span>
|
||||||
|
<span>运行 {fmtUptime(n.uptime)}</span>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 进度条 (超 80% 变红)
|
||||||
|
function Meter({ percent }) {
|
||||||
|
const p = Math.max(0, Math.min(100, Number(percent) || 0))
|
||||||
|
return (
|
||||||
|
<div className="meter">
|
||||||
|
<div className={'meter-fill' + (p >= 80 ? ' warn' : '')} style={{ width: p + '%' }} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 服务监控: 暂留空
|
||||||
|
function ServiceView() {
|
||||||
|
return (
|
||||||
|
<div className="monitor-empty">
|
||||||
|
<Empty description="服务监控待接入" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useParams, useNavigate, Link } from 'react-router-dom'
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
|
import { NavBar } from 'antd-mobile'
|
||||||
import { api } from '../../api/client.js'
|
import { api } from '../../api/client.js'
|
||||||
|
|
||||||
// 文章详情: /macro/articles/:slug
|
// 文章详情: /macro/articles/:slug
|
||||||
|
// 使用 antd-mobile NavBar 提供原生风格的返回按钮
|
||||||
export default function ArticleDetail() {
|
export default function ArticleDetail() {
|
||||||
const { slug } = useParams()
|
const { slug } = useParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -20,16 +22,19 @@ export default function ArticleDetail() {
|
|||||||
return () => { cancelled = true }
|
return () => { cancelled = true }
|
||||||
}, [slug])
|
}, [slug])
|
||||||
|
|
||||||
if (loading) return <div className="loading">加载中...</div>
|
|
||||||
if (err) return <div className="error-banner">{err}</div>
|
|
||||||
if (!article) return <div className="empty">文章不存在</div>
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="article-detail">
|
<div className="article-page">
|
||||||
<div className="article-back" onClick={() => navigate(-1)} role="button">‹ 返回</div>
|
<NavBar onBack={() => navigate(-1)}>文章详情</NavBar>
|
||||||
<h1>{article.title}</h1>
|
{loading && <div className="loading">加载中...</div>}
|
||||||
<div className="meta">{article.date}</div>
|
{err && <div className="error-banner">{err}</div>}
|
||||||
<div className="article-content" dangerouslySetInnerHTML={{ __html: article.contentHtml }} />
|
{!loading && !err && !article && <div className="empty">文章不存在</div>}
|
||||||
|
{!loading && !err && article && (
|
||||||
|
<div className="article-detail">
|
||||||
|
<h1>{article.title}</h1>
|
||||||
|
<div className="meta">{article.date}</div>
|
||||||
|
<div className="article-content" dangerouslySetInnerHTML={{ __html: article.contentHtml }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { List } from 'antd-mobile'
|
||||||
import { api } from '../../api/client.js'
|
import { api } from '../../api/client.js'
|
||||||
|
|
||||||
// 经济文章: 列表, 点进 /macro/articles/:slug
|
// 经济文章: 列表, 点进 /macro/articles/:slug
|
||||||
|
// 使用 antd-mobile List 仿原生设置/通讯录样式
|
||||||
export default function ArticlesView() {
|
export default function ArticlesView() {
|
||||||
const [articles, setArticles] = useState([])
|
const [articles, setArticles] = useState([])
|
||||||
const [err, setErr] = useState('')
|
const [err, setErr] = useState('')
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
@@ -25,15 +28,23 @@ export default function ArticlesView() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="page-title">经济文章</div>
|
<div className="page-title">经济文章</div>
|
||||||
<div className="list">
|
<List className="articles-list" mode="card">
|
||||||
{articles.map((a) => (
|
{articles.map((a) => (
|
||||||
<Link key={a.id} to={`/macro/articles/${encodeURIComponent(a.slug)}`} className="list-item">
|
<List.Item
|
||||||
<div className="list-item-title">{a.title}</div>
|
key={a.id}
|
||||||
<div className="list-item-meta">{a.date}</div>
|
title={a.title}
|
||||||
<div className="list-item-excerpt">{a.excerpt}</div>
|
description={
|
||||||
</Link>
|
<>
|
||||||
|
<div className="list-item-meta">{a.date}</div>
|
||||||
|
<div className="list-item-excerpt">{a.excerpt}</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
arrowIcon
|
||||||
|
clickable
|
||||||
|
onClick={() => navigate('/macro/articles/' + encodeURIComponent(a.slug))}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,28 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
||||||
|
import { Card, List, NavBar } from 'antd-mobile'
|
||||||
import { api } from '../../api/client.js'
|
import { api } from '../../api/client.js'
|
||||||
|
import TrendBadge, { calcTrend, lastPoint } from '../../components/TrendBadge.jsx'
|
||||||
|
|
||||||
|
// 经济数据
|
||||||
|
// 列表态 (默认): 列出所有指标, 每行显示最新值 + 趋势徽标, 点击进入详情
|
||||||
|
// 详情态: 顶部 NavBar (返回) + 折线图 + 数据表
|
||||||
|
// 频率中文标签 (对应 Indicator.frequency)
|
||||||
|
const FREQ_LABEL = { year: '年', quarter: '季', month: '月', day: '日' }
|
||||||
|
|
||||||
|
// 统一数据行的时间字段: 兼容 {month}/{year}/{period} 三种存储形状
|
||||||
|
function periodOf(r) {
|
||||||
|
return (r && (r.period || r.month || r.year)) || ''
|
||||||
|
}
|
||||||
|
|
||||||
// 经济数据: 从 /api/indicators 拉所有指标, 顶上一个 chip 行切换, 下面渲染图表+表格
|
|
||||||
export default function DataView() {
|
export default function DataView() {
|
||||||
const [indicators, setIndicators] = useState([])
|
const [indicators, setIndicators] = useState([])
|
||||||
const [selected, setSelected] = useState(null)
|
|
||||||
const [err, setErr] = useState('')
|
const [err, setErr] = useState('')
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
// 路由驱动: /macro/data 列表, /macro/data/:name 详情, 浏览器/手机返回键天然可用
|
||||||
|
const { name } = useParams()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
@@ -16,7 +31,6 @@ export default function DataView() {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
setIndicators(data.indicators || [])
|
setIndicators(data.indicators || [])
|
||||||
if (data.indicators && data.indicators[0]) setSelected(data.indicators[0].name)
|
|
||||||
})
|
})
|
||||||
.catch((e) => !cancelled && setErr(e.message))
|
.catch((e) => !cancelled && setErr(e.message))
|
||||||
.finally(() => !cancelled && setLoading(false))
|
.finally(() => !cancelled && setLoading(false))
|
||||||
@@ -27,83 +41,84 @@ export default function DataView() {
|
|||||||
if (err) return <div className="error-banner">{err}</div>
|
if (err) return <div className="error-banner">{err}</div>
|
||||||
if (!indicators.length) return <div className="empty">暂无指标数据, 请先执行 npm run db:seed</div>
|
if (!indicators.length) return <div className="empty">暂无指标数据, 请先执行 npm run db:seed</div>
|
||||||
|
|
||||||
// 按 groupName 分组, chip 行按组排列
|
// 详情态 (路径 /macro/data/:name)
|
||||||
const groups = groupBy(indicators, 'groupName')
|
if (name) {
|
||||||
const cur = indicators.find((i) => i.name === selected)
|
const cur = indicators.find((i) => i.name === name)
|
||||||
const chartData = cur ? buildMonthlySeries(cur.data) : []
|
if (cur) return <DataDetail indicator={cur} onBack={() => navigate('/macro/data')} />
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表态
|
||||||
|
const labelOf = (i) => i.title || i.name
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="list-page">
|
||||||
<div className="page-title">经济数据</div>
|
<div className="page-title">经济数据</div>
|
||||||
{Object.entries(groups).map(([groupName, list]) => (
|
<List>
|
||||||
<div key={groupName}>
|
{indicators.map((i) => {
|
||||||
<div className="section-title">{groupName}</div>
|
const lp = lastPoint({ rows: i.points || [] })
|
||||||
<div className="chip-row">
|
const { trend, change } = calcTrend(i.points || [])
|
||||||
{list.map((i) => (
|
const unit = i.unit || ''
|
||||||
<button
|
return (
|
||||||
key={i.name}
|
<List.Item
|
||||||
className={'chip' + (selected === i.name ? ' active' : '')}
|
key={i.name}
|
||||||
onClick={() => setSelected(i.name)}
|
title={labelOf(i)}
|
||||||
>
|
description={lp ? <><b>{lp.value}{unit}</b> · {periodOf(lp)}</> : '暂无数据'}
|
||||||
{shortTitle(i)}
|
extra={<TrendBadge trend={trend} />}
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{cur && (
|
arrowIcon
|
||||||
<div style={{ padding: '0 12px 12px' }}>
|
clickable
|
||||||
<div className="card">
|
onClick={() => navigate('/macro/data/' + i.name)}
|
||||||
<div style={{ fontWeight: 600, marginBottom: 4 }}>{cur.data.title || cur.name}</div>
|
/>
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-muted)', marginBottom: 8 }}>
|
)
|
||||||
单位 {cur.data.unit || '-'} · 来源 {cur.data.source || 'seed'}
|
})}
|
||||||
</div>
|
</List>
|
||||||
<div className="chart-wrap">
|
|
||||||
<ResponsiveContainer width="100%" height={260}>
|
|
||||||
<LineChart data={chartData} margin={{ top: 10, right: 16, left: 0, bottom: 0 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="month" tick={{ fontSize: 11 }} />
|
|
||||||
<YAxis tick={{ fontSize: 11 }} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey="value" name={cur.data.unit || '值'} stroke="#2563eb" strokeWidth={2} dot={{ r: 3 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
<div style={{ maxHeight: 240, overflow: 'auto' }}>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr><th>月份</th><th style={{ textAlign: 'right' }}>数值</th></tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{chartData.map((r) => (
|
|
||||||
<tr key={r.month}><td>{r.month}</td><td style={{ textAlign: 'right' }}>{r.value}</td></tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupBy(arr, key) {
|
// 详情: 折线图 + 数据表
|
||||||
return arr.reduce((acc, x) => {
|
function DataDetail({ indicator, onBack }) {
|
||||||
const k = x[key] || '其它'
|
const i = indicator
|
||||||
;(acc[k] = acc[k] || []).push(x)
|
const rows = (i.points || [])
|
||||||
return acc
|
// 图表 X 轴统一用 period 字段
|
||||||
}, {})
|
const chartRows = rows.map((r) => ({ ...r, period: periodOf(r) }))
|
||||||
}
|
const lp = lastPoint({ rows })
|
||||||
|
const { trend } = calcTrend(rows)
|
||||||
function shortTitle(i) {
|
const title = i.title || i.name
|
||||||
if (i.data && i.data.title) return i.data.title
|
return (
|
||||||
return i.name
|
<div className="detail-page">
|
||||||
}
|
<NavBar onBack={onBack}>{title}</NavBar>
|
||||||
|
<Card className="detail-card">
|
||||||
// 把 data.rows: [{month, value}, ...] 直接返回, 服务端已经按月排好
|
<div className="detail-meta">
|
||||||
function buildMonthlySeries(data) {
|
单位 {i.unit || '-'} · 频率 {FREQ_LABEL[i.frequency] || i.frequency || '-'} · 来源 {i.source || 'seed'} ·
|
||||||
return (data && data.rows) || []
|
{lp && <> 最新 <b style={{ color: 'var(--color-text)' }}>{lp.value}{i.unit || ''}</b> ({periodOf(lp)}) </>}
|
||||||
|
<TrendBadge trend={trend} />
|
||||||
|
</div>
|
||||||
|
<div className="chart-wrap">
|
||||||
|
<ResponsiveContainer width="100%" height={260}>
|
||||||
|
<LineChart data={chartRows} margin={{ top: 10, right: 16, left: 0, bottom: 0 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="period" tick={{ fontSize: 11 }} />
|
||||||
|
<YAxis tick={{ fontSize: 11 }} />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Line type="monotone" dataKey="value" name={i.unit || '值'} stroke="#1677ff" strokeWidth={2} dot={{ r: 3 }} />
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
<div style={{ maxHeight: 240, overflow: 'auto' }}>
|
||||||
|
<table className="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr><th>月份</th><th style={{ textAlign: 'right' }}>数值</th></tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.map((r) => (
|
||||||
|
<tr key={periodOf(r)}><td>{periodOf(r)}</td><td style={{ textAlign: 'right' }}>{r.value}</td></tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -1,30 +1,41 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts'
|
||||||
|
import { Card, List, NavBar } from 'antd-mobile'
|
||||||
import { api } from '../../api/client.js'
|
import { api } from '../../api/client.js'
|
||||||
|
import TrendBadge, { calcTrend } from '../../components/TrendBadge.jsx'
|
||||||
|
|
||||||
// 策略研究: 顶部 chip 行选策略, 下面显示 4 张图 (debt / gdp / debtToGdp / interestToGdp) + 表格
|
// 策略研究
|
||||||
|
// 列表态 (默认): 列出所有策略, 每行显示主指标 (债务/GDP) 最新值 + 趋势
|
||||||
|
// 详情态: 顶部 NavBar (返回) + 4 张图 (debt/gdp/debtToGdp/interestToGdp) + 表格
|
||||||
const SERIES = [
|
const SERIES = [
|
||||||
{ key: 'debt', label: '政府债务 (万亿美元)', color: '#8884d8' },
|
{ key: 'debt', label: '政府债务 (万亿美元)', color: '#8884d8' },
|
||||||
{ key: 'gdp', label: 'GDP (万亿美元)', color: '#3cb44b' },
|
{ key: 'gdp', label: 'GDP (万亿美元)', color: '#3cb44b' },
|
||||||
{ key: 'debtToGdp', label: '债务/GDP (%)', color: '#4363d8' },
|
{ key: 'debtToGdp', label: '债务/GDP (%)', color: '#4363d8' },
|
||||||
{ key: 'interestToGdp', label: '债务利息/GDP (%)', color: '#f58231' },
|
{ key: 'interestToGdp', label: '债务利息/GDP (%)', color: '#f58231' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 取最后一个有效数据点
|
||||||
|
function lastRow(rows) {
|
||||||
|
for (let i = rows.length - 1; i >= 0; i--) {
|
||||||
|
if (rows[i] && typeof rows[i].debtToGdp === 'number') return rows[i]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
export default function StrategyView() {
|
export default function StrategyView() {
|
||||||
const [strategies, setStrategies] = useState([])
|
const [strategies, setStrategies] = useState([])
|
||||||
const [selected, setSelected] = useState(null)
|
|
||||||
const [err, setErr] = useState('')
|
const [err, setErr] = useState('')
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
// 路由驱动: /macro/strategy 列表, /macro/strategy/:name 详情, 浏览器/手机返回键天然可用
|
||||||
|
const { name } = useParams()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
api('/strategies')
|
api('/strategies')
|
||||||
.then((data) => {
|
.then((data) => { if (!cancelled) setStrategies(data.strategies || []) })
|
||||||
if (cancelled) return
|
|
||||||
setStrategies(data.strategies || [])
|
|
||||||
if (data.strategies && data.strategies[0]) setSelected(data.strategies[0].name)
|
|
||||||
})
|
|
||||||
.catch((e) => !cancelled && setErr(e.message))
|
.catch((e) => !cancelled && setErr(e.message))
|
||||||
.finally(() => !cancelled && setLoading(false))
|
.finally(() => !cancelled && setLoading(false))
|
||||||
return () => { cancelled = true }
|
return () => { cancelled = true }
|
||||||
@@ -34,75 +45,97 @@ export default function StrategyView() {
|
|||||||
if (err) return <div className="error-banner">{err}</div>
|
if (err) return <div className="error-banner">{err}</div>
|
||||||
if (!strategies.length) return <div className="empty">暂无策略数据</div>
|
if (!strategies.length) return <div className="empty">暂无策略数据</div>
|
||||||
|
|
||||||
const cur = strategies.find((s) => s.name === selected)
|
// 详情态 (路径 /macro/strategy/:name)
|
||||||
const rows = cur ? cur.data.rows : []
|
if (name) {
|
||||||
|
const cur = strategies.find((s) => s.name === name)
|
||||||
|
if (cur) return <StrategyDetail strategy={cur} onBack={() => navigate('/macro/strategy')} />
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列表态: 主指标 = 债务/GDP, 趋势基于其最近变化
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="list-page">
|
||||||
<div className="page-title">策略研究</div>
|
<div className="page-title">策略研究</div>
|
||||||
<div className="chip-row" style={{ paddingLeft: 12 }}>
|
<List>
|
||||||
{strategies.map((s) => (
|
{strategies.map((s) => {
|
||||||
<button
|
const rows = (s.data && s.data.rows) || []
|
||||||
key={s.name}
|
const last = lastRow(rows)
|
||||||
className={'chip' + (selected === s.name ? ' active' : '')}
|
const { trend, change } = calcTrend(rows)
|
||||||
onClick={() => setSelected(s.name)}
|
return (
|
||||||
>
|
<List.Item
|
||||||
{s.description || s.name}
|
key={s.name}
|
||||||
</button>
|
title={s.description || s.name}
|
||||||
))}
|
description={last
|
||||||
</div>
|
? <><b>{last.debtToGdp}%</b> · 债务利息/GDP {last.interestToGdp}% · {last.year}</>
|
||||||
|
: '暂无数据'}
|
||||||
|
extra={<TrendBadge trend={trend} />}
|
||||||
|
arrowIcon
|
||||||
|
clickable
|
||||||
|
onClick={() => navigate('/macro/strategy/' + s.name)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{cur && (
|
// 详情: 4 张图 + 表格
|
||||||
<div style={{ padding: '0 12px 12px' }}>
|
function StrategyDetail({ strategy, onBack }) {
|
||||||
<div className="card">
|
const s = strategy
|
||||||
<div style={{ fontWeight: 600, marginBottom: 4 }}>{cur.description || cur.name}</div>
|
const rows = (s.data && s.data.rows) || []
|
||||||
<div style={{ fontSize: 12, color: 'var(--color-muted)', marginBottom: 8 }}>
|
const last = lastRow(rows)
|
||||||
数据 {rows.length} 条 · 来源 {cur.data.source || 'seed'}
|
const { trend } = calcTrend(rows)
|
||||||
|
return (
|
||||||
|
<div className="detail-page">
|
||||||
|
<NavBar onBack={onBack}>{s.description || s.name}</NavBar>
|
||||||
|
<Card className="detail-card">
|
||||||
|
<div className="detail-meta">
|
||||||
|
数据 {rows.length} 条 · 来源 {s.data.source || 'seed'} ·
|
||||||
|
{last && <> 最新 <b style={{ color: 'var(--color-text)' }}>{last.year}</b> 债务/GDP <b style={{ color: 'var(--color-text)' }}>{last.debtToGdp}%</b> </>}
|
||||||
|
<TrendBadge trend={trend} />
|
||||||
|
</div>
|
||||||
|
{SERIES.map((s2) => (
|
||||||
|
<div key={s2.key} style={{ marginBottom: 12 }}>
|
||||||
|
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 4 }}>{s2.label}</div>
|
||||||
|
<div className="chart-wrap">
|
||||||
|
<ResponsiveContainer width="100%" height={220}>
|
||||||
|
<LineChart data={rows} margin={{ top: 8, right: 12, left: 0, bottom: 0 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="year" tick={{ fontSize: 11 }} />
|
||||||
|
<YAxis tick={{ fontSize: 11 }} />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Line type="monotone" dataKey={s2.key} name={s2.label} stroke={s2.color} strokeWidth={2} dot={{ r: 3 }} />
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
{SERIES.map((s) => (
|
|
||||||
<div key={s.key} style={{ marginBottom: 12 }}>
|
|
||||||
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 4 }}>{s.label}</div>
|
|
||||||
<div className="chart-wrap">
|
|
||||||
<ResponsiveContainer width="100%" height={220}>
|
|
||||||
<LineChart data={rows} margin={{ top: 8, right: 12, left: 0, bottom: 0 }}>
|
|
||||||
<CartesianGrid strokeDasharray="3 3" />
|
|
||||||
<XAxis dataKey="year" tick={{ fontSize: 11 }} />
|
|
||||||
<YAxis tick={{ fontSize: 11 }} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line type="monotone" dataKey={s.key} name={s.label} stroke={s.color} strokeWidth={2} dot={{ r: 3 }} />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div style={{ maxHeight: 220, overflow: 'auto', marginTop: 8 }}>
|
|
||||||
<table className="data-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>年份</th>
|
|
||||||
<th style={{ textAlign: 'right' }}>债务</th>
|
|
||||||
<th style={{ textAlign: 'right' }}>GDP</th>
|
|
||||||
<th style={{ textAlign: 'right' }}>债务/GDP</th>
|
|
||||||
<th style={{ textAlign: 'right' }}>利息/GDP</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{rows.map((r) => (
|
|
||||||
<tr key={r.year}>
|
|
||||||
<td>{r.year}</td>
|
|
||||||
<td style={{ textAlign: 'right' }}>{r.debt}</td>
|
|
||||||
<td style={{ textAlign: 'right' }}>{r.gdp}</td>
|
|
||||||
<td style={{ textAlign: 'right' }}>{r.debtToGdp}</td>
|
|
||||||
<td style={{ textAlign: 'right' }}>{r.interestToGdp}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
<div style={{ maxHeight: 220, overflow: 'auto', marginTop: 8 }}>
|
||||||
|
<table className="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>年份</th>
|
||||||
|
<th style={{ textAlign: 'right' }}>债务</th>
|
||||||
|
<th style={{ textAlign: 'right' }}>GDP</th>
|
||||||
|
<th style={{ textAlign: 'right' }}>债务/GDP</th>
|
||||||
|
<th style={{ textAlign: 'right' }}>利息/GDP</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.map((r) => (
|
||||||
|
<tr key={r.year}>
|
||||||
|
<td>{r.year}</td>
|
||||||
|
<td style={{ textAlign: 'right' }}>{r.debt}</td>
|
||||||
|
<td style={{ textAlign: 'right' }}>{r.gdp}</td>
|
||||||
|
<td style={{ textAlign: 'right' }}>{r.debtToGdp}</td>
|
||||||
|
<td style={{ textAlign: 'right' }}>{r.interestToGdp}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts'
|
||||||
|
import { NavBar, Card } from 'antd-mobile'
|
||||||
|
import { api } from '../../api/client.js'
|
||||||
|
|
||||||
|
// 节点详情: /monitor/server/:uuid
|
||||||
|
// 时序实时来自 komari (服务端代理, 24h 分钟粒度)
|
||||||
|
function fmtHM(s) {
|
||||||
|
const d = new Date(s)
|
||||||
|
if (isNaN(d.getTime())) return s
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
return pad(d.getHours()) + ':' + pad(d.getMinutes())
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtBytes(b) {
|
||||||
|
if (!b) return '0 B'
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
let i = 0
|
||||||
|
let v = Number(b)
|
||||||
|
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++ }
|
||||||
|
return v.toFixed(v >= 100 ? 0 : 1) + ' ' + units[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
const GB = 1024 * 1024 * 1024
|
||||||
|
|
||||||
|
export default function ServerDetail() {
|
||||||
|
const { uuid } = useParams()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [rows, setRows] = useState(null)
|
||||||
|
const [err, setErr] = useState('')
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
setRows(null)
|
||||||
|
setErr('')
|
||||||
|
api('/servers/' + encodeURIComponent(uuid) + '/history')
|
||||||
|
.then((data) => { if (!cancelled) setRows(data.items || []) })
|
||||||
|
.catch((e) => !cancelled && setErr(e.message))
|
||||||
|
.finally(() => !cancelled && setRows((r) => r ?? []))
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [uuid])
|
||||||
|
|
||||||
|
// 网络吞吐: komari 原始字节口径, 按 KB/s 展示
|
||||||
|
const chart = (rows || []).map((p) => ({
|
||||||
|
time: fmtHM(p.period),
|
||||||
|
cpu: Number(p.cpu) || 0,
|
||||||
|
memGB: (Number(p.ram) || 0) / GB,
|
||||||
|
load: Number(p.load) || 0,
|
||||||
|
netIn: (Number(p.netIn) || 0) / 1024,
|
||||||
|
netOut: (Number(p.netOut) || 0) / 1024,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="article-page">
|
||||||
|
<NavBar onBack={() => navigate(-1)}>主机监控</NavBar>
|
||||||
|
{err && <div className="error-banner">{err}</div>}
|
||||||
|
{!err && rows === null && <div className="loading">加载中...</div>}
|
||||||
|
{!err && rows !== null && chart.length === 0 && <div className="empty">该节点暂无时序数据</div>}
|
||||||
|
{!err && chart.length > 0 && (
|
||||||
|
<div className="server-detail">
|
||||||
|
<ChartCard title="CPU 使用率 (%)" data={chart} lines={[{ key: 'cpu', name: 'CPU', color: '#1677ff' }]} yMax={100} />
|
||||||
|
<ChartCard title="内存使用 (GB)" data={chart} lines={[{ key: 'memGB', name: '内存', color: '#00b578' }]} />
|
||||||
|
<ChartCard title="系统负载 (1分钟)" data={chart} lines={[{ key: 'load', name: 'Load', color: '#ff8f1f' }]} />
|
||||||
|
<ChartCard
|
||||||
|
title="网络吞吐 (KB/s)"
|
||||||
|
data={chart}
|
||||||
|
lines={[
|
||||||
|
{ key: 'netIn', name: '下行', color: '#1677ff' },
|
||||||
|
{ key: 'netOut', name: '上行', color: '#00b578' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<div className="monitor-tip">近 24 小时 · 1 分钟粒度 · 数据实时来自 komari</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChartCard({ title, data, lines, yMax }) {
|
||||||
|
return (
|
||||||
|
<Card className="chart-card">
|
||||||
|
<div className="chart-title">{title}</div>
|
||||||
|
<div className="chart-wrap">
|
||||||
|
<ResponsiveContainer width="100%" height={180}>
|
||||||
|
<LineChart data={data} margin={{ top: 6, right: 12, left: -8, bottom: 0 }}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" />
|
||||||
|
<XAxis dataKey="time" tick={{ fontSize: 10 }} minTickGap={40} />
|
||||||
|
<YAxis tick={{ fontSize: 10 }} domain={yMax ? [0, yMax] : [0, 'auto']} width={44} />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend wrapperStyle={{ fontSize: 12 }} />
|
||||||
|
{lines.map((l) => (
|
||||||
|
<Line key={l.key} type="monotone" dataKey={l.key} name={l.name} stroke={l.color} strokeWidth={1.5} dot={false} isAnimationActive={false} />
|
||||||
|
))}
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
|
import { NavBar } from 'antd-mobile'
|
||||||
|
import { api } from '../../api/client.js'
|
||||||
|
|
||||||
|
// 新闻详情: /news/:id
|
||||||
|
// 正文来自 News.content (MCP 返回的完整正文), 旧数据可能无 content, 兜底显示摘要
|
||||||
|
function fmtDate(s) {
|
||||||
|
if (!s) return ''
|
||||||
|
const d = new Date(s)
|
||||||
|
if (isNaN(d.getTime())) return s
|
||||||
|
const pad = (n) => String(n).padStart(2, '0')
|
||||||
|
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NewsDetail() {
|
||||||
|
const { id } = useParams()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const [item, setItem] = useState(null)
|
||||||
|
const [err, setErr] = useState('')
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
setLoading(true)
|
||||||
|
api('/news/' + encodeURIComponent(id))
|
||||||
|
.then((data) => { if (!cancelled) setItem(data.item) })
|
||||||
|
.catch((e) => !cancelled && setErr(e.message))
|
||||||
|
.finally(() => !cancelled && setLoading(false))
|
||||||
|
return () => { cancelled = true }
|
||||||
|
}, [id])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="article-page">
|
||||||
|
<NavBar onBack={() => navigate(-1)}>新闻详情</NavBar>
|
||||||
|
{loading && <div className="loading">加载中...</div>}
|
||||||
|
{err && <div className="error-banner">{err}</div>}
|
||||||
|
{!loading && !err && !item && <div className="empty">新闻不存在</div>}
|
||||||
|
{!loading && !err && item && (
|
||||||
|
<div className="news-detail">
|
||||||
|
<h1>{item.title}</h1>
|
||||||
|
<div className="meta">
|
||||||
|
{item.source || '未知来源'} · {fmtDate(item.publishedAt)} · #{item.impactRank}
|
||||||
|
{/^https?:\/\//i.test(item.url || '') && (
|
||||||
|
<a className="news-origin" href={item.url} target="_blank" rel="noreferrer">原文链接</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="news-content">
|
||||||
|
{item.content ? item.content : item.summary}
|
||||||
|
{!item.content && <div className="news-content-empty">(本条暂无全文,仅摘要)</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+192
-652
@@ -1,513 +1,20 @@
|
|||||||
* {
|
/* ================== 移动端优先布局 (mobile-first, antd-mobile) ================== */
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navbar */
|
|
||||||
.navbar {
|
|
||||||
background-color: #333;
|
|
||||||
color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo a {
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links a {
|
|
||||||
color: white;
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links a:hover {
|
|
||||||
background-color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Global link styles */
|
|
||||||
a {
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main content */
|
|
||||||
main {
|
|
||||||
flex: 1;
|
|
||||||
padding: 2rem;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
transform: translateY(-5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card h2 {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card p {
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
.footer {
|
|
||||||
background-color: #333;
|
|
||||||
color: white;
|
|
||||||
padding: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
margin-top: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-content {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Data page layout */
|
|
||||||
.data-layout {
|
|
||||||
display: flex;
|
|
||||||
gap: 2rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-sidebar {
|
|
||||||
width: 250px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.indicator-group {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group-title {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
border-bottom: 2px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.indicator-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.indicator-item {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.indicator-item:hover {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
transform: translateX(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.indicator-item.active {
|
|
||||||
background-color: #333;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.indicator-detail {
|
|
||||||
background-color: white;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.indicator-title {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.indicator-description {
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-container {
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: #fafafa;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table-container {
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-title {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table th,
|
|
||||||
.data-table td {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
text-align: left;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table th {
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table tbody tr:hover {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Articles section */
|
|
||||||
.articles-section {
|
|
||||||
margin-top: 4rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 2rem;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
border-bottom: 2px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.articles-grid {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-card {
|
|
||||||
background-color: white;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-card:hover {
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
transform: translateX(5px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-date {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-title {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-excerpt {
|
|
||||||
color: #666;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pagination */
|
|
||||||
.pagination {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 0.5rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-link {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background-color: white;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: #333;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-link:hover {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-link.active {
|
|
||||||
background-color: #333;
|
|
||||||
color: white;
|
|
||||||
border-color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Article detail page */
|
|
||||||
.article-detail {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background-color: white;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-detail-title {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-detail-date {
|
|
||||||
font-size: 1rem;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
padding-bottom: 1rem;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-detail-content {
|
|
||||||
color: #333;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-detail-content h1,
|
|
||||||
.article-detail-content h2,
|
|
||||||
.article-detail-content h3,
|
|
||||||
.article-detail-content h4,
|
|
||||||
.article-detail-content h5,
|
|
||||||
.article-detail-content h6 {
|
|
||||||
margin-top: 2rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-detail-content p {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-detail-content ul,
|
|
||||||
.article-detail-content ol {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
padding-left: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-detail-content li {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-detail-content img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
margin: 2rem 0;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
.data-layout {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-sidebar {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-detail {
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.article-detail-title {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strategy-layout {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strategy-sidebar {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Strategy page layout */
|
|
||||||
.strategy-layout {
|
|
||||||
display: flex;
|
|
||||||
gap: 2rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strategy-sidebar {
|
|
||||||
width: 250px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strategy-content {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-title {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
border-bottom: 2px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strategy-list {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strategy-item {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.strategy-item:hover {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
transform: translateX(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.strategy-item.active {
|
|
||||||
background-color: #333;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strategy-detail {
|
|
||||||
background-color: white;
|
|
||||||
padding: 2rem;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.strategy-title {
|
|
||||||
font-size: 1.8rem;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.strategy-description {
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-section {
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-title {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* ================== 移动端优先布局 (mobile-first) ================== */
|
|
||||||
|
|
||||||
|
/* 主题变量: 与 antd-mobile 的 --adm-* 保持一致, 便于组件统一 */
|
||||||
:root {
|
:root {
|
||||||
--color-bg: #f5f5f5;
|
--color-bg: #f5f5f5;
|
||||||
--color-card: #ffffff;
|
--color-card: #ffffff;
|
||||||
--color-text: #333333;
|
--color-text: #333333;
|
||||||
--color-muted: #666666;
|
--color-muted: #666666;
|
||||||
--color-border: #e5e7eb;
|
--color-border: #eeeeee;
|
||||||
--color-primary: #1f2937;
|
--color-primary: #1677ff;
|
||||||
--color-accent: #2563eb;
|
--color-accent: #1677ff;
|
||||||
--color-danger: #dc2626;
|
--color-danger: #ff3141;
|
||||||
--bottom-nav-h: 56px;
|
--safe-top: env(safe-area-inset-top, 0px);
|
||||||
--top-tabs-h: 44px;
|
|
||||||
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
--safe-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
--safe-left: env(safe-area-inset-left, 0px);
|
||||||
|
--safe-right: env(safe-area-inset-right, 0px);
|
||||||
|
--tabbar-h: 72px;
|
||||||
}
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
@@ -528,178 +35,72 @@ a { color: inherit; text-decoration: none; cursor: pointer; }
|
|||||||
button { font: inherit; cursor: pointer; border: none; background: none; color: inherit; }
|
button { font: inherit; cursor: pointer; border: none; background: none; color: inherit; }
|
||||||
input, textarea, select { font: inherit; }
|
input, textarea, select { font: inherit; }
|
||||||
|
|
||||||
/* 应用主框架 */
|
/* 应用主框架: 全屏 flex 列布局, 不再 max-width 居中 (解决"左右不贴边") */
|
||||||
.app-root {
|
.app-root {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
min-height: 100dvh;
|
height: 100dvh;
|
||||||
max-width: 768px;
|
width: 100%;
|
||||||
margin: 0 auto;
|
max-width: 100%;
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding-left: var(--safe-left);
|
||||||
|
padding-right: var(--safe-right);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main {
|
.app-main {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
padding-bottom: calc(var(--bottom-nav-h) + var(--safe-bottom) + 8px);
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-bottom: calc(var(--tabbar-h) + var(--safe-bottom));
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-main.has-top-tabs {
|
/* 带有 antd-mobile NavBar 的页面 (Home/Monitor/Me/Article) */
|
||||||
padding-top: var(--top-tabs-h);
|
.page-with-navbar { display: flex; flex-direction: column; min-height: 100%; }
|
||||||
}
|
.page-with-navbar .adm-error-block { flex: 1; }
|
||||||
|
|
||||||
/* 顶栏 tabs (宏观页用) */
|
/* 底部 antd-mobile TabBar 定位 */
|
||||||
.top-tabs {
|
.app-tabbar.adm-tab-bar {
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 10;
|
|
||||||
height: var(--top-tabs-h);
|
|
||||||
display: flex;
|
|
||||||
background: #fff;
|
|
||||||
border-bottom: 1px solid var(--color-border);
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-tabs-item {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--color-muted);
|
|
||||||
position: relative;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-tabs-item.active {
|
|
||||||
color: var(--color-primary);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-tabs-item.active::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 30%;
|
|
||||||
right: 30%;
|
|
||||||
bottom: 6px;
|
|
||||||
height: 2px;
|
|
||||||
background: var(--color-primary);
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 滑屏容器 */
|
|
||||||
.swipe-container {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
touch-action: pan-y;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swipe-track {
|
|
||||||
display: flex;
|
|
||||||
width: 300%;
|
|
||||||
transform: translateX(var(--swipe-offset, 0%));
|
|
||||||
transition: transform 0.25s ease;
|
|
||||||
will-change: transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
.swipe-track.no-anim { transition: none; }
|
|
||||||
|
|
||||||
.swipe-pane {
|
|
||||||
width: 33.3333%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 底部 tab */
|
|
||||||
.bottom-nav {
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
height: calc(var(--bottom-nav-h) + var(--safe-bottom));
|
|
||||||
padding-bottom: var(--safe-bottom);
|
|
||||||
display: flex;
|
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
max-width: 768px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
.app-tabbar.adm-tab-bar .adm-tab-bar-wrap { min-height: var(--tabbar-h); }
|
||||||
|
.app-tabbar.adm-tab-bar .adm-tab-bar-item {
|
||||||
|
/* 调高 padding, 把文字在 TabBar 中向上推, 避免下半部被裁切 */
|
||||||
|
padding: 8px 8px 4px;
|
||||||
|
}
|
||||||
|
.tab-icon { display: inline-flex; }
|
||||||
|
|
||||||
.bottom-nav-item {
|
/* 宏观页 SwipeTabs */
|
||||||
flex: 1;
|
.swipe-tabs-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
height: 100%;
|
||||||
justify-content: center;
|
min-height: 0;
|
||||||
gap: 2px;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--color-muted);
|
|
||||||
user-select: none;
|
|
||||||
}
|
}
|
||||||
|
.tab-content {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
.tab-pane[hidden] { display: none; }
|
||||||
|
|
||||||
.bottom-nav-item .icon { font-size: 20px; line-height: 1; }
|
/* ================== 通用组件辅助类 ================== */
|
||||||
.bottom-nav-item.active { color: var(--color-primary); font-weight: 600; }
|
|
||||||
|
|
||||||
/* 通用卡片 */
|
|
||||||
.card { background: #fff; border-radius: 10px; padding: 14px 16px; box-shadow: 0 1px 2px rgba(0,0,0,.04); }
|
|
||||||
.card + .card { margin-top: 10px; }
|
|
||||||
|
|
||||||
.page-title { font-size: 20px; font-weight: 700; padding: 14px 16px 4px; }
|
|
||||||
.page-desc { font-size: 13px; color: var(--color-muted); padding: 0 16px 12px; }
|
|
||||||
.section-title { font-size: 16px; font-weight: 600; padding: 14px 16px 8px; }
|
|
||||||
|
|
||||||
/* 列表 */
|
|
||||||
.list { padding: 0 12px; }
|
|
||||||
.list-item { background: #fff; padding: 14px; border-radius: 10px; margin-bottom: 10px; box-shadow: 0 1px 2px rgba(0,0,0,.04); display: block; }
|
|
||||||
.list-item-title { font-size: 15px; font-weight: 600; margin-bottom: 4px; line-height: 1.4; }
|
|
||||||
.list-item-meta { font-size: 12px; color: var(--color-muted); margin-bottom: 6px; }
|
|
||||||
.list-item-excerpt { font-size: 13px; color: var(--color-muted); line-height: 1.5; }
|
|
||||||
|
|
||||||
/* 图表 */
|
|
||||||
.chart-wrap { background: #fafafa; border-radius: 8px; padding: 8px; margin: 8px 0 16px; }
|
|
||||||
|
|
||||||
/* 表格 */
|
|
||||||
.data-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
||||||
.data-table th, .data-table td { padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--color-border); }
|
|
||||||
.data-table th { background: #f8f8f8; font-weight: 600; }
|
|
||||||
|
|
||||||
/* 登录 */
|
|
||||||
.login-page { min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 24px; background: linear-gradient(180deg, #1f2937 0%, #1f2937 35%, #f5f5f5 35%); }
|
|
||||||
.login-card { background: #fff; width: 100%; max-width: 380px; border-radius: 14px; padding: 24px; box-shadow: 0 4px 24px rgba(0,0,0,.1); }
|
|
||||||
.login-title { font-size: 20px; font-weight: 700; text-align: center; margin-bottom: 6px; }
|
|
||||||
.login-subtitle { text-align: center; color: var(--color-muted); margin-bottom: 20px; font-size: 13px; }
|
|
||||||
.login-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
|
|
||||||
.login-field label { font-size: 12px; color: var(--color-muted); }
|
|
||||||
.login-field input { padding: 12px; border: 1px solid var(--color-border); border-radius: 8px; outline: none; }
|
|
||||||
.login-field input:focus { border-color: var(--color-primary); }
|
|
||||||
.login-options { display: flex; align-items: center; gap: 6px; margin-bottom: 18px; font-size: 13px; color: var(--color-muted); }
|
|
||||||
.login-btn { width: 100%; padding: 13px; background: var(--color-primary); color: #fff; border-radius: 8px; font-weight: 600; }
|
|
||||||
.login-btn:disabled { opacity: 0.6; }
|
|
||||||
.login-hint { text-align: center; font-size: 12px; color: var(--color-muted); margin-top: 14px; line-height: 1.6; }
|
|
||||||
.login-error { background: #fef2f2; color: var(--color-danger); padding: 10px; border-radius: 6px; font-size: 13px; margin-bottom: 12px; }
|
|
||||||
|
|
||||||
/* 我 */
|
|
||||||
.me-header { background: var(--color-primary); color: #fff; padding: 30px 16px 24px; }
|
|
||||||
.me-name { font-size: 18px; font-weight: 600; }
|
|
||||||
.me-email { font-size: 13px; opacity: .8; margin-top: 2px; }
|
|
||||||
.me-list { padding: 12px; }
|
|
||||||
.me-list .list-item { display: flex; align-items: center; justify-content: space-between; }
|
|
||||||
.me-list .list-item .label { font-size: 14px; }
|
|
||||||
.me-list .list-item .value { font-size: 13px; color: var(--color-muted); }
|
|
||||||
.me-list .danger { color: var(--color-danger); }
|
|
||||||
|
|
||||||
.loading, .page-loading { padding: 40px; text-align: center; color: var(--color-muted); }
|
|
||||||
.empty { padding: 40px; text-align: center; color: var(--color-muted); font-size: 13px; }
|
|
||||||
.error-banner { background: #fef2f2; color: var(--color-danger); padding: 10px 14px; font-size: 13px; border-radius: 6px; margin: 12px; }
|
|
||||||
|
|
||||||
/* 占位页 */
|
|
||||||
.placeholder-page { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; padding: 24px; text-align: center; color: var(--color-muted); gap: 12px; }
|
|
||||||
.placeholder-page .icon { font-size: 48px; opacity: .6; }
|
|
||||||
|
|
||||||
/* 文章详情 */
|
/* 文章详情 */
|
||||||
|
.article-page { display: flex; flex-direction: column; min-height: 100%; }
|
||||||
|
.article-page .adm-nav-bar { background: #fff; border-bottom: 1px solid var(--color-border); }
|
||||||
.article-detail { background: #fff; padding: 18px 16px 24px; }
|
.article-detail { background: #fff; padding: 18px 16px 24px; }
|
||||||
.article-detail h1 { font-size: 20px; line-height: 1.35; margin-bottom: 8px; }
|
.article-detail h1 { font-size: 20px; line-height: 1.35; margin-bottom: 8px; }
|
||||||
.article-detail .meta { font-size: 12px; color: var(--color-muted); margin-bottom: 16px; padding-bottom: 10px; border-bottom: 1px solid var(--color-border); }
|
.article-detail .meta { font-size: 12px; color: var(--color-muted); margin-bottom: 16px; padding-bottom: 10px; border-bottom: 1px solid var(--color-border); }
|
||||||
@@ -708,10 +109,149 @@ input, textarea, select { font: inherit; }
|
|||||||
.article-content p { margin-bottom: 0.9em; }
|
.article-content p { margin-bottom: 0.9em; }
|
||||||
.article-content ul, .article-content ol { margin: 0 0 1em 1.4em; }
|
.article-content ul, .article-content ol { margin: 0 0 1em 1.4em; }
|
||||||
.article-content img { max-width: 100%; border-radius: 6px; margin: 0.6em 0; }
|
.article-content img { max-width: 100%; border-radius: 6px; margin: 0.6em 0; }
|
||||||
.article-back { display: inline-flex; align-items: center; gap: 4px; padding: 8px 0; color: var(--color-accent); font-size: 13px; }
|
|
||||||
|
|
||||||
/* 侧边栏式选择器 (移动端改为顶部 chip 行) */
|
/* 文章列表项 (用于 ArticlesView) */
|
||||||
.chip-row { display: flex; gap: 8px; overflow-x: auto; padding: 8px 12px; }
|
.articles-list .adm-list-item-description { color: var(--color-muted); }
|
||||||
.chip-row::-webkit-scrollbar { display: none; }
|
.list-item-meta { font-size: 12px; color: var(--color-muted); margin-bottom: 4px; }
|
||||||
.chip { padding: 7px 12px; background: #fff; border: 1px solid var(--color-border); border-radius: 999px; font-size: 13px; white-space: nowrap; }
|
.list-item-excerpt { font-size: 13px; color: var(--color-muted); line-height: 1.5; }
|
||||||
.chip.active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
|
|
||||||
|
/* ================== 列表视图 (DataView/StrategyView 列表态) ================== */
|
||||||
|
.list-page { padding: 0 0 16px; }
|
||||||
|
.list-page .page-title { padding: 14px 16px 4px; }
|
||||||
|
.list-page .adm-list { margin: 0 12px; background: transparent; }
|
||||||
|
.list-page .adm-list .adm-list-item {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,.04);
|
||||||
|
}
|
||||||
|
.list-page .adm-list .adm-list-item:last-child { margin-bottom: 0; }
|
||||||
|
.list-page .adm-list-item-title { font-size: 15px; font-weight: 600; color: var(--color-text); }
|
||||||
|
.list-page .adm-list-item-description { color: var(--color-muted); margin-top: 4px; }
|
||||||
|
.list-page .row-meta { display: flex; align-items: center; gap: 6px; font-variant-numeric: tabular-nums; }
|
||||||
|
.list-page .row-change { font-size: 12px; color: var(--color-muted); }
|
||||||
|
/* 趋势徽标: 上升(绿) / 下降(红) / 持平(灰) / 震荡(橙) */
|
||||||
|
.trend-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(0,0,0,.04);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.trend-badge .trend-icon { font-weight: 700; }
|
||||||
|
.trend-up { color: #00b578; background: rgba(0,181,120,.1); }
|
||||||
|
.trend-down { color: #ff3141; background: rgba(255,49,65,.1); }
|
||||||
|
.trend-flat { color: #666; background: rgba(0,0,0,.05); }
|
||||||
|
.trend-volatile { color: #ff8f1f; background: rgba(255,143,31,.12); }
|
||||||
|
|
||||||
|
/* 详情页 (DataView/StrategyView 详情态) */
|
||||||
|
.detail-page { display: flex; flex-direction: column; min-height: 100%; }
|
||||||
|
.detail-page .adm-nav-bar { background: #fff; border-bottom: 1px solid var(--color-border); }
|
||||||
|
.detail-card.adm-card { margin: 12px; border-radius: 10px; }
|
||||||
|
.detail-card.adm-card .adm-card-body { padding: 12px; }
|
||||||
|
.detail-meta { font-size: 12px; color: var(--color-muted); margin-bottom: 8px; }
|
||||||
|
|
||||||
|
/* 图表 / 表格 */
|
||||||
|
.chart-wrap { background: #fafafa; border-radius: 8px; padding: 8px; margin: 8px 0 16px; }
|
||||||
|
.data-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||||
|
.data-table th, .data-table td { padding: 8px 10px; text-align: left; border-bottom: 1px solid var(--color-border); }
|
||||||
|
.data-table th { background: #f8f8f8; font-weight: 600; }
|
||||||
|
|
||||||
|
/* 页面标题 */
|
||||||
|
.page-title { font-size: 20px; font-weight: 700; padding: 14px 16px 4px; }
|
||||||
|
|
||||||
|
/* 通用状态 */
|
||||||
|
.loading, .page-loading { padding: 40px; text-align: center; color: var(--color-muted); }
|
||||||
|
.empty { padding: 40px; text-align: center; color: var(--color-muted); font-size: 13px; }
|
||||||
|
.error-banner { background: #fef2f2; color: var(--color-danger); padding: 10px 14px; font-size: 13px; border-radius: 6px; margin: 12px; }
|
||||||
|
|
||||||
|
/* ================== 登录页 (独立全屏, 未走 MobileShell) ================== */
|
||||||
|
.login-page { min-height: 100vh; min-height: 100dvh; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 24px; background: linear-gradient(180deg, #1f2937 0%, #1f2937 38%, #f5f5f5 38%); }
|
||||||
|
.login-form { width: 100%; max-width: 380px; }
|
||||||
|
.login-brand { color: #fff; font-size: 28px; font-weight: 700; text-align: center; margin-bottom: 4px; }
|
||||||
|
.login-subtitle { color: rgba(255,255,255,.7); text-align: center; margin-bottom: 16px; font-size: 13px; }
|
||||||
|
.login-card.adm-card { border-radius: 14px; }
|
||||||
|
.login-card.adm-card .adm-card-body { padding: 22px 18px; }
|
||||||
|
.login-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
|
||||||
|
.login-field label { font-size: 12px; color: var(--color-muted); }
|
||||||
|
.login-field .adm-input { padding: 8px 10px; border: 1px solid var(--color-border); border-radius: 8px; }
|
||||||
|
.login-field .adm-input:focus-within { border-color: var(--color-primary); }
|
||||||
|
.login-options { margin-bottom: 18px; }
|
||||||
|
.login-hint { text-align: center; font-size: 12px; color: var(--color-muted); margin-top: 14px; line-height: 1.6; }
|
||||||
|
.login-error { background: #fef2f2; color: var(--color-danger); padding: 10px; border-radius: 6px; font-size: 13px; margin-bottom: 12px; }
|
||||||
|
|
||||||
|
/* ================== 我的页 ================== */
|
||||||
|
.me-page { display: flex; flex-direction: column; min-height: 100%; background: var(--color-bg); }
|
||||||
|
.me-navbar.adm-nav-bar { background: #fff; border-bottom: 1px solid var(--color-border); }
|
||||||
|
.me-profile { background: linear-gradient(135deg, #1677ff, #4096ff); color: #fff; padding: 24px 16px 28px; display: flex; flex-direction: column; align-items: center; gap: 6px; }
|
||||||
|
.me-avatar { background: rgba(255,255,255,.2); color: #fff; font-size: 28px; }
|
||||||
|
.me-name { font-size: 18px; font-weight: 600; }
|
||||||
|
.me-email { font-size: 13px; opacity: .85; }
|
||||||
|
.me-list { margin-top: 12px; }
|
||||||
|
.me-list .adm-list-item { padding-left: 16px; padding-right: 16px; }
|
||||||
|
.me-logout { color: var(--color-danger); }
|
||||||
|
/* 首页模块: 报警 + 新闻 */
|
||||||
|
.home-content { padding: 0 0 16px; }
|
||||||
|
.home-content .adm-tabs-content { --content-padding: 0; }
|
||||||
|
.home-content .tab-label { display: inline-flex; align-items: center; gap: 4px; }
|
||||||
|
.home-section { padding: 0 12px 16px; }
|
||||||
|
.section-title { font-size: 16px; font-weight: 700; padding: 14px 4px 8px; color: var(--color-text); }
|
||||||
|
.section-list { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
.alert-card.adm-card { border-radius: 10px; margin: 0; }
|
||||||
|
.alert-card .adm-card-body { padding: 10px 12px; }
|
||||||
|
.alert-title { display: flex; justify-content: space-between; font-weight: 600; font-size: 14px; }
|
||||||
|
.alert-name { color: var(--color-danger); }
|
||||||
|
.alert-period { color: var(--color-muted); font-weight: 400; }
|
||||||
|
.alert-diff { font-size: 12px; color: var(--color-text); margin-top: 4px; }
|
||||||
|
.alert-ratio { color: var(--color-danger); font-weight: 600; margin-left: 6px; }
|
||||||
|
.alert-time { font-size: 11px; color: var(--color-muted); margin-top: 4px; }
|
||||||
|
.news-card.adm-card { border-radius: 10px; margin: 0; }
|
||||||
|
.news-card .adm-card-body { padding: 10px 12px; }
|
||||||
|
.news-title { font-size: 14px; font-weight: 600; line-height: 1.4; color: var(--color-text); }
|
||||||
|
.news-summary { font-size: 12px; color: var(--color-muted); margin-top: 4px; line-height: 1.5; max-height: 3.6em; overflow: hidden; }
|
||||||
|
.news-meta { display: flex; gap: 10px; font-size: 11px; color: var(--color-muted); margin-top: 6px; }
|
||||||
|
.news-rank { color: var(--color-primary); font-weight: 600; }
|
||||||
|
.news-card:active .adm-card-body { background: #f5f5f5; }
|
||||||
|
/* 新闻详情页 */
|
||||||
|
.news-detail { background: #fff; padding: 18px 16px 24px; }
|
||||||
|
.news-detail h1 { font-size: 20px; line-height: 1.35; margin-bottom: 8px; }
|
||||||
|
.news-detail .meta { font-size: 12px; color: var(--color-muted); margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid var(--color-border); display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
|
||||||
|
.news-origin { color: var(--color-primary); margin-left: auto; }
|
||||||
|
.news-content { font-size: 15px; line-height: 1.75; color: #333; white-space: pre-wrap; word-break: break-word; }
|
||||||
|
.news-content-empty { margin-top: 12px; font-size: 12px; color: var(--color-muted); }
|
||||||
|
/* ================== 监控页 ================== */
|
||||||
|
.monitor-content { padding: 0 0 16px; }
|
||||||
|
.monitor-content .adm-tabs-content { --content-padding: 0; }
|
||||||
|
.monitor-tip { font-size: 11px; color: var(--color-muted); padding: 8px 14px 2px; }
|
||||||
|
.monitor-loading { padding: 40px 0; text-align: center; color: var(--color-muted); font-size: 13px; }
|
||||||
|
.monitor-empty { padding: 48px 0; }
|
||||||
|
.retry-link { color: var(--color-primary); margin-left: 8px; }
|
||||||
|
.server-list { padding: 0 12px; }
|
||||||
|
.server-card.adm-card { border-radius: 10px; margin: 0; }
|
||||||
|
.server-card:active .adm-card-body { background: #f5f5f5; }
|
||||||
|
.server-card .adm-card-body { padding: 12px; }
|
||||||
|
.server-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||||||
|
.server-name { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; font-size: 15px; color: var(--color-text); }
|
||||||
|
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||||
|
.status-dot.on { background: #00b578; box-shadow: 0 0 0 3px rgba(0, 181, 120, 0.15); }
|
||||||
|
.status-dot.off { background: #ff3141; box-shadow: 0 0 0 3px rgba(255, 49, 65, 0.15); }
|
||||||
|
.server-update { font-size: 11px; color: var(--color-muted); }
|
||||||
|
.server-metrics { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.metric { display: grid; grid-template-columns: 34px 1fr 96px; align-items: center; gap: 8px; }
|
||||||
|
.metric-label { font-size: 12px; color: var(--color-muted); }
|
||||||
|
.metric-value { font-size: 11px; color: var(--color-text); text-align: right; font-variant-numeric: tabular-nums; }
|
||||||
|
.meter { height: 6px; border-radius: 3px; background: #f0f0f0; overflow: hidden; }
|
||||||
|
.meter-fill { height: 100%; border-radius: 3px; background: var(--color-primary); transition: width 0.4s ease; }
|
||||||
|
.meter-fill.warn { background: #ff3141; }
|
||||||
|
.server-foot { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--color-border); font-size: 11px; color: var(--color-muted); }
|
||||||
|
/* 节点详情图表 */
|
||||||
|
.server-detail { padding: 10px 12px 20px; display: flex; flex-direction: column; gap: 10px; }
|
||||||
|
.chart-card.adm-card { border-radius: 10px; margin: 0; }
|
||||||
|
.chart-card .adm-card-body { padding: 12px 8px 8px; }
|
||||||
|
.chart-title { font-size: 13px; font-weight: 600; color: var(--color-text); padding: 0 4px 6px; }
|
||||||
|
.scroll-sentinel { text-align: center; padding: 14px 0 4px; color: var(--color-muted); font-size: 12px; }
|
||||||
|
|||||||
@@ -2,11 +2,18 @@ import { defineConfig } from 'vite'
|
|||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
// 移动端优先: 默认按移动视口开发, 同时兼容桌面浏览器
|
// 移动端优先: 默认按移动视口开发, 同时兼容桌面浏览器
|
||||||
|
// HMR 需要 WebSocket; 某些受限 webview (VS Code Simple Browser / 老版 Android WebView) 不支持.
|
||||||
|
// 设置 hmr: false 后, Vite 会改用文件监听 + 页面全量刷新兜底, 在任何环境都能用.
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
port: 5173,
|
port: 5173,
|
||||||
|
hmr: false, // 关闭 WebSocket HMR, 改用 full-reload
|
||||||
|
watch: {
|
||||||
|
usePolling: true,
|
||||||
|
interval: 300,
|
||||||
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://127.0.0.1:3001',
|
target: 'http://127.0.0.1:3001',
|
||||||
|
|||||||
Reference in New Issue
Block a user