更新ios

This commit is contained in:
2026-01-23 14:44:19 +08:00
parent b4710818f9
commit 60874a2cee
42 changed files with 9 additions and 20833 deletions

View File

@@ -1,41 +0,0 @@
# ========================================
# Mock 测试环境配置
# ========================================
# 使用方式: npm run start:mock
#
# 工作原理:
# 1. 通过 env-cmd 加载此配置文件
# 2. REACT_APP_ENABLE_MOCK=true 会在 src/index.js 中启动 MSW (Mock Service Worker)
# 3. MSW 在浏览器层面拦截所有 HTTP 请求
# 4. 根据 src/mocks/handlers/* 中定义的规则返回 mock 数据
# 5. 未定义 mock 的接口会继续请求真实后端
#
# 适用场景:
# - 前端独立开发,无需后端支持
# - 测试特定接口的 UI 表现
# - 后端接口未就绪时的快速原型开发
# ========================================
# React 构建优化配置
GENERATE_SOURCEMAP=false
SKIP_PREFLIGHT_CHECK=true
DISABLE_ESLINT_PLUGIN=true
TSC_COMPILE_ON_ERROR=true
IMAGE_INLINE_SIZE_LIMIT=10000
NODE_OPTIONS=--max_old_space_size=4096
# API 配置
# Mock 模式下使用空字符串,让请求使用相对路径
# MSW 会在浏览器层拦截这些请求,不需要真实的后端地址
REACT_APP_API_URL=
# Socket.IO 连接地址Mock 模式下连接生产环境)
# 注意WebSocket 不被 MSW 拦截,可以独立配置
REACT_APP_SOCKET_URL=https://valuefrontier.cn
# 启用 Mock 数据(核心配置)
# 此配置会触发 src/index.js 中的 MSW 初始化
REACT_APP_ENABLE_MOCK=true
# Mock 环境标识
REACT_APP_ENV=mock

View File

@@ -65,7 +65,7 @@
},
"scripts": {
"prestart": "kill-port 3000",
"start": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' env-cmd -f .env.mock craco start",
"start": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' env-cmd -f .env.development craco start",
"prestart:real": "kill-port 3000",
"start:real": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' env-cmd -f .env.local craco start",
"prestart:dev": "kill-port 3000",
@@ -113,7 +113,6 @@
"kill-port": "^2.0.1",
"less": "^4.4.2",
"less-loader": "^12.3.0",
"msw": "^2.11.5",
"prettier": "2.2.1",
"react-error-overlay": "6.0.9",
"sharp": "^0.34.4",
@@ -134,11 +133,6 @@
"not op_mini all"
]
},
"msw": {
"workerDirectory": [
"public"
]
},
"optionalDependencies": {
"fsevents": "^2.3.3"
}

View File

@@ -1,349 +0,0 @@
/* eslint-disable */
/* tslint:disable */
/**
* Mock Service Worker.
* @see https://github.com/mswjs/msw
* - Please do NOT modify this file.
*/
const PACKAGE_VERSION = '2.12.2'
const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()
addEventListener('install', function () {
self.skipWaiting()
})
addEventListener('activate', function (event) {
event.waitUntil(self.clients.claim())
})
addEventListener('message', async function (event) {
const clientId = Reflect.get(event.source || {}, 'id')
if (!clientId || !self.clients) {
return
}
const client = await self.clients.get(clientId)
if (!client) {
return
}
const allClients = await self.clients.matchAll({
type: 'window',
})
switch (event.data) {
case 'KEEPALIVE_REQUEST': {
sendToClient(client, {
type: 'KEEPALIVE_RESPONSE',
})
break
}
case 'INTEGRITY_CHECK_REQUEST': {
sendToClient(client, {
type: 'INTEGRITY_CHECK_RESPONSE',
payload: {
packageVersion: PACKAGE_VERSION,
checksum: INTEGRITY_CHECKSUM,
},
})
break
}
case 'MOCK_ACTIVATE': {
activeClientIds.add(clientId)
sendToClient(client, {
type: 'MOCKING_ENABLED',
payload: {
client: {
id: client.id,
frameType: client.frameType,
},
},
})
break
}
case 'CLIENT_CLOSED': {
activeClientIds.delete(clientId)
const remainingClients = allClients.filter((client) => {
return client.id !== clientId
})
// Unregister itself when there are no more clients
if (remainingClients.length === 0) {
self.registration.unregister()
}
break
}
}
})
addEventListener('fetch', function (event) {
const requestInterceptedAt = Date.now()
// Bypass navigation requests.
if (event.request.mode === 'navigate') {
return
}
// Opening the DevTools triggers the "only-if-cached" request
// that cannot be handled by the worker. Bypass such requests.
if (
event.request.cache === 'only-if-cached' &&
event.request.mode !== 'same-origin'
) {
return
}
// Bypass all requests when there are no active clients.
// Prevents the self-unregistered worked from handling requests
// after it's been terminated (still remains active until the next reload).
if (activeClientIds.size === 0) {
return
}
const requestId = crypto.randomUUID()
event.respondWith(handleRequest(event, requestId, requestInterceptedAt))
})
/**
* @param {FetchEvent} event
* @param {string} requestId
* @param {number} requestInterceptedAt
*/
async function handleRequest(event, requestId, requestInterceptedAt) {
const client = await resolveMainClient(event)
const requestCloneForEvents = event.request.clone()
const response = await getResponse(
event,
client,
requestId,
requestInterceptedAt,
)
// Send back the response clone for the "response:*" life-cycle events.
// Ensure MSW is active and ready to handle the message, otherwise
// this message will pend indefinitely.
if (client && activeClientIds.has(client.id)) {
const serializedRequest = await serializeRequest(requestCloneForEvents)
// Clone the response so both the client and the library could consume it.
const responseClone = response.clone()
sendToClient(
client,
{
type: 'RESPONSE',
payload: {
isMockedResponse: IS_MOCKED_RESPONSE in response,
request: {
id: requestId,
...serializedRequest,
},
response: {
type: responseClone.type,
status: responseClone.status,
statusText: responseClone.statusText,
headers: Object.fromEntries(responseClone.headers.entries()),
body: responseClone.body,
},
},
},
responseClone.body ? [serializedRequest.body, responseClone.body] : [],
)
}
return response
}
/**
* Resolve the main client for the given event.
* Client that issues a request doesn't necessarily equal the client
* that registered the worker. It's with the latter the worker should
* communicate with during the response resolving phase.
* @param {FetchEvent} event
* @returns {Promise<Client | undefined>}
*/
async function resolveMainClient(event) {
const client = await self.clients.get(event.clientId)
if (activeClientIds.has(event.clientId)) {
return client
}
if (client?.frameType === 'top-level') {
return client
}
const allClients = await self.clients.matchAll({
type: 'window',
})
return allClients
.filter((client) => {
// Get only those clients that are currently visible.
return client.visibilityState === 'visible'
})
.find((client) => {
// Find the client ID that's recorded in the
// set of clients that have registered the worker.
return activeClientIds.has(client.id)
})
}
/**
* @param {FetchEvent} event
* @param {Client | undefined} client
* @param {string} requestId
* @param {number} requestInterceptedAt
* @returns {Promise<Response>}
*/
async function getResponse(event, client, requestId, requestInterceptedAt) {
// Clone the request because it might've been already used
// (i.e. its body has been read and sent to the client).
const requestClone = event.request.clone()
function passthrough() {
// Cast the request headers to a new Headers instance
// so the headers can be manipulated with.
const headers = new Headers(requestClone.headers)
// Remove the "accept" header value that marked this request as passthrough.
// This prevents request alteration and also keeps it compliant with the
// user-defined CORS policies.
const acceptHeader = headers.get('accept')
if (acceptHeader) {
const values = acceptHeader.split(',').map((value) => value.trim())
const filteredValues = values.filter(
(value) => value !== 'msw/passthrough',
)
if (filteredValues.length > 0) {
headers.set('accept', filteredValues.join(', '))
} else {
headers.delete('accept')
}
}
return fetch(requestClone, { headers })
}
// Bypass mocking when the client is not active.
if (!client) {
return passthrough()
}
// Bypass initial page load requests (i.e. static assets).
// The absence of the immediate/parent client in the map of the active clients
// means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet
// and is not ready to handle requests.
if (!activeClientIds.has(client.id)) {
return passthrough()
}
// Notify the client that a request has been intercepted.
const serializedRequest = await serializeRequest(event.request)
const clientMessage = await sendToClient(
client,
{
type: 'REQUEST',
payload: {
id: requestId,
interceptedAt: requestInterceptedAt,
...serializedRequest,
},
},
[serializedRequest.body],
)
switch (clientMessage.type) {
case 'MOCK_RESPONSE': {
return respondWithMock(clientMessage.data)
}
case 'PASSTHROUGH': {
return passthrough()
}
}
return passthrough()
}
/**
* @param {Client} client
* @param {any} message
* @param {Array<Transferable>} transferrables
* @returns {Promise<any>}
*/
function sendToClient(client, message, transferrables = []) {
return new Promise((resolve, reject) => {
const channel = new MessageChannel()
channel.port1.onmessage = (event) => {
if (event.data && event.data.error) {
return reject(event.data.error)
}
resolve(event.data)
}
client.postMessage(message, [
channel.port2,
...transferrables.filter(Boolean),
])
})
}
/**
* @param {Response} response
* @returns {Response}
*/
function respondWithMock(response) {
// Setting response status code to 0 is a no-op.
// However, when responding with a "Response.error()", the produced Response
// instance will have status code set to 0. Since it's not possible to create
// a Response instance with status code 0, handle that use-case separately.
if (response.status === 0) {
return Response.error()
}
const mockedResponse = new Response(response.body, response)
Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, {
value: true,
enumerable: true,
})
return mockedResponse
}
/**
* @param {Request} request
*/
async function serializeRequest(request) {
return {
url: request.url,
mode: request.mode,
method: request.method,
headers: Object.fromEntries(request.headers.entries()),
cache: request.cache,
credentials: request.credentials,
destination: request.destination,
integrity: request.integrity,
redirect: request.redirect,
referrer: request.referrer,
referrerPolicy: request.referrerPolicy,
body: await request.arrayBuffer(),
keepalive: request.keepalive,
}
}

View File

@@ -103,23 +103,6 @@ const styles = {
fontSize: '14px',
fontWeight: 500,
},
mockSection: {
marginTop: '12px',
paddingTop: '12px',
borderTop: '1px solid rgba(212, 175, 55, 0.1)',
},
mockBtn: {
background: 'transparent',
color: '#9370DB',
border: '1px solid rgba(147, 112, 219, 0.5)',
},
mockHint: {
display: 'block',
textAlign: 'center',
color: 'rgba(255, 255, 255, 0.4)',
fontSize: '12px',
marginTop: '4px',
},
iframeLoading: {
position: 'absolute',
top: 0,
@@ -414,27 +397,6 @@ const WechatRegister = forwardRef(function WechatRegister({ subtitle }, ref) {
{getStatusText(wechatStatus)}
</Text>
)}
{process.env.REACT_APP_ENABLE_MOCK === 'true' && wechatStatus === WECHAT_STATUS.WAITING && wechatSessionId && (
<div style={styles.mockSection}>
<Button
size="small"
block
style={styles.mockBtn}
onClick={() => {
if (window.mockWechatScan) {
const success = window.mockWechatScan(wechatSessionId);
if (success) message.info("正在模拟扫码登录...");
} else {
message.warning("Mock API 未加载,请刷新页面重试");
}
}}
>
🧪 模拟扫码成功测试
</Button>
<Text style={styles.mockHint}>开发模式 | 自动登录: 5</Text>
</div>
)}
</div>
);
});

View File

@@ -661,12 +661,6 @@ export const NotificationProvider = ({ children }) => {
// ========== 连接到 Socket 服务(⚡ 异步初始化,不阻塞首屏) ==========
useEffect(() => {
// ⚡ Mock 模式下跳过 Socket 连接(避免连接生产服务器失败的错误)
if (process.env.REACT_APP_ENABLE_MOCK === 'true') {
logger.debug('NotificationContext', 'Mock 模式,跳过 Socket 连接');
return;
}
// ⚡ 防止 React Strict Mode 导致的重复初始化
if (socketInitialized) {
logger.debug('NotificationContext', 'Socket 已初始化跳过重复执行Strict Mode 保护)');

View File

@@ -52,11 +52,6 @@ if (process.env.REACT_APP_ENABLE_DEBUG === 'true') {
// 注册 Service Worker用于支持浏览器通知
function registerServiceWorker() {
// ⚠️ Mock 模式下跳过 Service Worker 注册(避免与 MSW 冲突)
if (process.env.REACT_APP_ENABLE_MOCK === 'true') {
return;
}
// 仅在支持 Service Worker 的浏览器中注册
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
@@ -87,26 +82,9 @@ function renderApp() {
</React.StrictMode>
);
// 注册 Service Worker(非 Mock 模式)
// 注册 Service Worker
registerServiceWorker();
}
// 启动应用
async function startApp() {
// ✅ 开发环境 Mock 模式:先启动 MSW再渲染应用
// 确保所有 API 请求(包括 AuthContext.checkSession都被正确拦截
if (process.env.NODE_ENV === 'development' && process.env.REACT_APP_ENABLE_MOCK === 'true') {
try {
const { startMockServiceWorker } = await import('./mocks/browser');
await startMockServiceWorker();
} catch (error) {
console.error('[MSW] 启动失败:', error);
}
}
// 渲染应用
renderApp();
}
// 启动应用
startApp();
renderApp();

View File

@@ -1,75 +0,0 @@
// src/mocks/browser.js
// 浏览器环境的 MSW Worker
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
// 创建 Service Worker 实例
export const worker = setupWorker(...handlers);
// 启动状态管理(防止重复启动)
let isStarting = false;
let isStarted = false;
// 启动 Mock Service Worker
export async function startMockServiceWorker() {
// 防止重复启动
if (isStarting || isStarted) {
console.log('[MSW] Mock Service Worker 已启动或正在启动中,跳过重复调用');
return;
}
// 只在开发环境且 REACT_APP_ENABLE_MOCK=true 时启动
const shouldEnableMock = process.env.REACT_APP_ENABLE_MOCK === 'true';
if (!shouldEnableMock) {
console.log('[MSW] Mock 已禁用 (REACT_APP_ENABLE_MOCK=false)');
return;
}
isStarting = true;
try {
await worker.start({
// 🎯 警告模式(关键配置)
// 'bypass': 未定义 Mock 的请求自动转发到真实后端
// 'warn': 未定义的请求会显示警告(调试用)✅ 当前使用(允许 passthrough
// 'error': 未定义的请求会抛出错误(严格模式,不允许 passthrough
onUnhandledRequest: 'warn',
// 自定义 Service Worker URL如果需要
serviceWorker: {
url: '/mockServiceWorker.js',
},
// 是否在控制台显示启动日志和拦截日志 静默模式(不在控制台打印启动消息)
quiet: false,
});
isStarted = true;
// 精简日志:只保留一行启动提示
console.log('%c[MSW] Mock 已启用 🎭', 'color: #4CAF50; font-weight: bold;');
} catch (error) {
console.error('[MSW] 启动失败:', error);
} finally {
isStarting = false;
}
}
// 停止 Mock Service Worker
export function stopMockServiceWorker() {
if (!isStarted) {
console.log('[MSW] Mock Service Worker 未启动,无需停止');
return;
}
worker.stop();
isStarted = false;
console.log('[MSW] Mock Service Worker 已停止');
}
// 重置所有 Handlers
export function resetMockHandlers() {
worker.resetHandlers();
console.log('[MSW] Handlers 已重置');
}

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

View File

@@ -1,429 +0,0 @@
// src/mocks/data/financial.js
// 财务数据相关的 Mock 数据
// 生成财务数据
export const generateFinancialData = (stockCode) => {
// 12 期数据 - 用于财务指标表格7个指标Tab
const metricsPeriods = [
'2024-09-30', '2024-06-30', '2024-03-31', '2023-12-31',
'2023-09-30', '2023-06-30', '2023-03-31', '2022-12-31',
'2022-09-30', '2022-06-30', '2022-03-31', '2021-12-31',
];
// 8 期数据 - 用于财务报表3个报表Tab
const statementPeriods = [
'2024-09-30', '2024-06-30', '2024-03-31', '2023-12-31',
'2023-09-30', '2023-06-30', '2023-03-31', '2022-12-31',
];
// 兼容旧代码
const periods = statementPeriods.slice(0, 4);
return {
stockCode,
// 股票基本信息
stockInfo: {
stock_code: stockCode,
stock_name: stockCode === '000001' ? '平安银行' : '示例公司',
industry: stockCode === '000001' ? '银行' : '制造业',
list_date: '1991-04-03',
market: 'SZ',
// 关键指标
key_metrics: {
eps: 2.72,
roe: 16.23,
gross_margin: 71.92,
net_margin: 32.56,
roa: 1.05
},
// 增长率
growth_rates: {
revenue_growth: 8.2,
profit_growth: 12.5,
asset_growth: 5.6,
equity_growth: 6.8
},
// 财务概要
financial_summary: {
revenue: 162350,
net_profit: 52860,
total_assets: 5024560,
total_liabilities: 4698880
},
// 最新业绩预告
latest_forecast: {
forecast_type: '预增',
content: '预计全年净利润同比增长10%-17%'
}
},
// 资产负债表 - 嵌套结构8期数据
balanceSheet: statementPeriods.map((period, i) => ({
period,
assets: {
current_assets: {
cash: 856780 - i * 10000,
trading_financial_assets: 234560 - i * 5000,
notes_receivable: 12340 - i * 200,
accounts_receivable: 45670 - i * 1000,
prepayments: 8900 - i * 100,
other_receivables: 23450 - i * 500,
inventory: 156780 - i * 3000,
contract_assets: 34560 - i * 800,
other_current_assets: 67890 - i * 1500,
total: 2512300 - i * 25000
},
non_current_assets: {
long_term_equity_investments: 234560 - i * 5000,
investment_property: 45670 - i * 1000,
fixed_assets: 678900 - i * 15000,
construction_in_progress: 123450 - i * 3000,
right_of_use_assets: 34560 - i * 800,
intangible_assets: 89012 - i * 2000,
goodwill: 45670 - i * 1000,
deferred_tax_assets: 12340 - i * 300,
other_non_current_assets: 67890 - i * 1500,
total: 2512260 - i * 25000
},
total: 5024560 - i * 50000
},
liabilities: {
current_liabilities: {
short_term_borrowings: 456780 - i * 10000,
notes_payable: 23450 - i * 500,
accounts_payable: 234560 - i * 5000,
advance_receipts: 12340 - i * 300,
contract_liabilities: 34560 - i * 800,
employee_compensation_payable: 45670 - i * 1000,
taxes_payable: 23450 - i * 500,
other_payables: 78900 - i * 1500,
non_current_liabilities_due_within_one_year: 89012 - i * 2000,
total: 3456780 - i * 35000
},
non_current_liabilities: {
long_term_borrowings: 678900 - i * 15000,
bonds_payable: 234560 - i * 5000,
lease_liabilities: 45670 - i * 1000,
deferred_tax_liabilities: 12340 - i * 300,
other_non_current_liabilities: 89012 - i * 2000,
total: 1242100 - i * 13000
},
total: 4698880 - i * 48000
},
equity: {
share_capital: 19405,
capital_reserve: 89012 - i * 2000,
surplus_reserve: 45670 - i * 1000,
undistributed_profit: 156780 - i * 3000,
treasury_stock: 0,
other_comprehensive_income: 12340 - i * 300,
parent_company_equity: 315680 - i * 1800,
minority_interests: 10000 - i * 200,
total: 325680 - i * 2000
}
})),
// 利润表 - 嵌套结构8期数据
incomeStatement: statementPeriods.map((period, i) => ({
period,
revenue: {
total_operating_revenue: 162350 - i * 4000,
operating_revenue: 158900 - i * 3900,
other_income: 3450 - i * 100
},
costs: {
total_operating_cost: 93900 - i * 2500,
operating_cost: 45620 - i * 1200,
taxes_and_surcharges: 4560 - i * 100,
selling_expenses: 12340 - i * 300,
admin_expenses: 15670 - i * 400,
rd_expenses: 8900 - i * 200,
financial_expenses: 6810 - i * 300,
interest_expense: 8900 - i * 200,
interest_income: 2090 - i * 50,
three_expenses_total: 34820 - i * 1000,
four_expenses_total: 43720 - i * 1200,
asset_impairment_loss: 1200 - i * 50,
credit_impairment_loss: 2340 - i * 100
},
other_gains: {
fair_value_change: 1230 - i * 50,
investment_income: 3450 - i * 100,
investment_income_from_associates: 890 - i * 20,
exchange_income: 560 - i * 10,
asset_disposal_income: 340 - i * 10
},
profit: {
operating_profit: 68450 - i * 1500,
total_profit: 69500 - i * 1500,
income_tax_expense: 16640 - i * 300,
net_profit: 52860 - i * 1200,
parent_net_profit: 51200 - i * 1150,
minority_profit: 1660 - i * 50,
continuing_operations_net_profit: 52860 - i * 1200,
discontinued_operations_net_profit: 0
},
non_operating: {
non_operating_income: 1050 - i * 20,
non_operating_expenses: 450 - i * 10
},
per_share: {
basic_eps: 2.72 - i * 0.06,
diluted_eps: 2.70 - i * 0.06
},
comprehensive_income: {
other_comprehensive_income: 890 - i * 20,
total_comprehensive_income: 53750 - i * 1220,
parent_comprehensive_income: 52050 - i * 1170,
minority_comprehensive_income: 1700 - i * 50
}
})),
// 现金流量表 - 嵌套结构8期数据
cashflow: statementPeriods.map((period, i) => ({
period,
operating_activities: {
inflow: {
cash_from_sales: 178500 - i * 4500
},
outflow: {
cash_for_goods: 52900 - i * 1500
},
net_flow: 125600 - i * 3000
},
investment_activities: {
net_flow: -45300 - i * 1000
},
financing_activities: {
net_flow: -38200 + i * 500
},
cash_changes: {
net_increase: 42100 - i * 1500,
ending_balance: 456780 - i * 10000
},
key_metrics: {
free_cash_flow: 80300 - i * 2000
}
})),
// 财务指标 - 嵌套结构12期数据
financialMetrics: metricsPeriods.map((period, i) => ({
period,
profitability: {
roe: 16.23 - i * 0.3,
roe_deducted: 15.89 - i * 0.3,
roe_weighted: 16.45 - i * 0.3,
roa: 1.05 - i * 0.02,
gross_margin: 71.92 - i * 0.5,
net_profit_margin: 32.56 - i * 0.3,
operating_profit_margin: 42.16 - i * 0.4,
cost_profit_ratio: 115.8 - i * 1.2,
ebit: 86140 - i * 1800
},
per_share_metrics: {
eps: 2.72 - i * 0.06,
basic_eps: 2.72 - i * 0.06,
diluted_eps: 2.70 - i * 0.06,
deducted_eps: 2.65 - i * 0.06,
bvps: 16.78 - i * 0.1,
operating_cash_flow_ps: 6.47 - i * 0.15,
capital_reserve_ps: 4.59 - i * 0.1,
undistributed_profit_ps: 8.08 - i * 0.15
},
growth: {
revenue_growth: 8.2 - i * 0.5,
net_profit_growth: 12.5 - i * 0.8,
deducted_profit_growth: 11.8 - i * 0.7,
parent_profit_growth: 12.3 - i * 0.75,
operating_cash_flow_growth: 15.6 - i * 1.0,
total_asset_growth: 5.6 - i * 0.3,
equity_growth: 6.8 - i * 0.4,
fixed_asset_growth: 4.2 - i * 0.2
},
operational_efficiency: {
total_asset_turnover: 0.41 - i * 0.01,
fixed_asset_turnover: 2.35 - i * 0.05,
current_asset_turnover: 0.82 - i * 0.02,
receivable_turnover: 12.5 - i * 0.3,
receivable_days: 29.2 + i * 0.7,
inventory_turnover: 0, // 银行无库存
inventory_days: 0,
working_capital_turnover: 1.68 - i * 0.04
},
solvency: {
current_ratio: 0.73 + i * 0.01,
quick_ratio: 0.71 + i * 0.01,
cash_ratio: 0.25 + i * 0.005,
conservative_quick_ratio: 0.68 + i * 0.01,
asset_liability_ratio: 93.52 + i * 0.05,
interest_coverage: 8.56 - i * 0.2,
cash_to_maturity_debt_ratio: 0.45 - i * 0.01,
tangible_asset_debt_ratio: 94.12 + i * 0.05
},
expense_ratios: {
selling_expense_ratio: 7.60 + i * 0.1,
admin_expense_ratio: 9.65 + i * 0.1,
financial_expense_ratio: 4.19 + i * 0.1,
rd_expense_ratio: 5.48 + i * 0.1,
three_expense_ratio: 21.44 + i * 0.3,
four_expense_ratio: 26.92 + i * 0.4,
cost_ratio: 28.10 + i * 0.2
}
})),
// 主营业务 - 按产品/业务分类
mainBusiness: {
product_classification: [
{
period: '2024-09-30',
report_type: '2024年三季报',
products: [
{ content: '零售金融业务', revenue: 81320000000, gross_margin: 68.5, profit_margin: 42.3, profit: 34398160000 },
{ content: '对公金融业务', revenue: 68540000000, gross_margin: 62.8, profit_margin: 38.6, profit: 26456440000 },
{ content: '金融市场业务', revenue: 12490000000, gross_margin: 75.2, profit_margin: 52.1, profit: 6507290000 },
{ content: '合计', revenue: 162350000000, gross_margin: 67.5, profit_margin: 41.2, profit: 66883200000 },
]
},
{
period: '2024-06-30',
report_type: '2024年中报',
products: [
{ content: '零售金融业务', revenue: 78650000000, gross_margin: 67.8, profit_margin: 41.5, profit: 32639750000 },
{ content: '对公金融业务', revenue: 66280000000, gross_margin: 61.9, profit_margin: 37.8, profit: 25053840000 },
{ content: '金融市场业务', revenue: 11870000000, gross_margin: 74.5, profit_margin: 51.2, profit: 6077440000 },
{ content: '合计', revenue: 156800000000, gross_margin: 66.8, profit_margin: 40.5, profit: 63504000000 },
]
},
{
period: '2024-03-31',
report_type: '2024年一季报',
products: [
{ content: '零售金融业务', revenue: 38920000000, gross_margin: 67.2, profit_margin: 40.8, profit: 15879360000 },
{ content: '对公金融业务', revenue: 32650000000, gross_margin: 61.2, profit_margin: 37.1, profit: 12113150000 },
{ content: '金融市场业务', revenue: 5830000000, gross_margin: 73.8, profit_margin: 50.5, profit: 2944150000 },
{ content: '合计', revenue: 77400000000, gross_margin: 66.1, profit_margin: 39.8, profit: 30805200000 },
]
},
{
period: '2023-12-31',
report_type: '2023年年报',
products: [
{ content: '零售金融业务', revenue: 152680000000, gross_margin: 66.5, profit_margin: 40.2, profit: 61377360000 },
{ content: '对公金融业务', revenue: 128450000000, gross_margin: 60.5, profit_margin: 36.5, profit: 46884250000 },
{ content: '金融市场业务', revenue: 22870000000, gross_margin: 73.2, profit_margin: 49.8, profit: 11389260000 },
{ content: '合计', revenue: 304000000000, gross_margin: 65.2, profit_margin: 39.2, profit: 119168000000 },
]
},
],
industry_classification: [
{
period: '2024-09-30',
report_type: '2024年三季报',
industries: [
{ content: '华南地区', revenue: 56817500000, gross_margin: 69.2, profit_margin: 43.5, profit: 24715612500 },
{ content: '华东地区', revenue: 48705000000, gross_margin: 67.8, profit_margin: 41.2, profit: 20066460000 },
{ content: '华北地区', revenue: 32470000000, gross_margin: 65.5, profit_margin: 38.8, profit: 12598360000 },
{ content: '西南地区', revenue: 16235000000, gross_margin: 64.2, profit_margin: 37.5, profit: 6088125000 },
{ content: '其他地区', revenue: 8122500000, gross_margin: 62.8, profit_margin: 35.2, profit: 2859120000 },
{ content: '合计', revenue: 162350000000, gross_margin: 67.5, profit_margin: 41.2, profit: 66883200000 },
]
},
{
period: '2024-06-30',
report_type: '2024年中报',
industries: [
{ content: '华南地区', revenue: 54880000000, gross_margin: 68.5, profit_margin: 42.8, profit: 23488640000 },
{ content: '华东地区', revenue: 47040000000, gross_margin: 67.1, profit_margin: 40.5, profit: 19051200000 },
{ content: '华北地区', revenue: 31360000000, gross_margin: 64.8, profit_margin: 38.1, profit: 11948160000 },
{ content: '西南地区', revenue: 15680000000, gross_margin: 63.5, profit_margin: 36.8, profit: 5770240000 },
{ content: '其他地区', revenue: 7840000000, gross_margin: 62.1, profit_margin: 34.5, profit: 2704800000 },
{ content: '合计', revenue: 156800000000, gross_margin: 66.8, profit_margin: 40.5, profit: 63504000000 },
]
},
]
},
// 业绩预告
forecast: {
period: '2024',
forecast_net_profit_min: 580000, // 百万元
forecast_net_profit_max: 620000,
yoy_growth_min: 10.0, // %
yoy_growth_max: 17.0,
forecast_type: '预增',
reason: '受益于零售业务快速增长及资产质量改善,预计全年业绩保持稳定增长',
publish_date: '2024-10-15'
},
// 行业排名(数组格式,符合 IndustryRankingView 组件要求)
industryRank: [
{
period: '2024-09-30',
report_type: '三季报',
rankings: [
{
industry_name: stockCode === '000001' ? '银行' : '制造业',
level_description: '一级行业',
metrics: {
eps: { value: 2.72, rank: 8, industry_avg: 1.85 },
bvps: { value: 15.23, rank: 12, industry_avg: 12.50 },
roe: { value: 16.23, rank: 10, industry_avg: 12.00 },
revenue_growth: { value: 8.2, rank: 15, industry_avg: 5.50 },
profit_growth: { value: 12.5, rank: 9, industry_avg: 8.00 },
operating_margin: { value: 32.56, rank: 6, industry_avg: 25.00 },
debt_ratio: { value: 92.5, rank: 35, industry_avg: 88.00 },
receivable_turnover: { value: 5.2, rank: 18, industry_avg: 4.80 }
}
}
]
}
],
// 期间对比 - 营收与利润趋势数据
periodComparison: [
{
period: '2024-09-30',
performance: {
revenue: 41500000000, // 415亿
net_profit: 13420000000 // 134.2亿
}
},
{
period: '2024-06-30',
performance: {
revenue: 40800000000, // 408亿
net_profit: 13180000000 // 131.8亿
}
},
{
period: '2024-03-31',
performance: {
revenue: 40200000000, // 402亿
net_profit: 13050000000 // 130.5亿
}
},
{
period: '2023-12-31',
performance: {
revenue: 40850000000, // 408.5亿
net_profit: 13210000000 // 132.1亿
}
},
{
period: '2023-09-30',
performance: {
revenue: 38500000000, // 385亿
net_profit: 11920000000 // 119.2亿
}
},
{
period: '2023-06-30',
performance: {
revenue: 37800000000, // 378亿
net_profit: 11850000000 // 118.5亿
}
}
]
};
};

View File

@@ -1,394 +0,0 @@
/**
* 价值论坛帖子 Mock 数据
*/
// 模拟用户
export const mockForumUsers = [
{
id: "user_1",
nickname: "价值投资者",
username: "value_investor",
avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=investor1",
},
{
id: "user_2",
nickname: "趋势猎手",
username: "trend_hunter",
avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=hunter2",
},
{
id: "user_3",
nickname: "量化先锋",
username: "quant_pioneer",
avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=quant3",
},
{
id: "user_4",
nickname: "股市老兵",
username: "stock_veteran",
avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=veteran4",
},
{
id: "user_5",
nickname: "新手小白",
username: "newbie",
avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=newbie5",
},
];
// 帖子标签
export const mockTags = [
"A股",
"美股",
"港股",
"新能源",
"半导体",
"AI",
"消费",
"医药",
"金融",
"地产",
"白酒",
"锂电池",
"光伏",
"汽车",
"军工",
];
// 帖子分类
export const mockCategories = [
"analysis", // 个股分析
"strategy", // 投资策略
"news", // 市场资讯
"discussion", // 讨论交流
"experience", // 经验分享
];
// 模拟帖子列表
export const mockPosts = [
{
id: "post_001",
author_id: "user_1",
author_name: "价值投资者",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=investor1",
title: "深度解析宁德时代2024年业绩展望",
content:
"宁德时代作为全球动力电池龙头2024年面临几个关键变量\n\n1. **产能扩张**:公司在欧洲、北美的产能布局持续推进\n2. **技术迭代**:麒麟电池、钠离子电池等新技术商业化进度\n3. **竞争格局**:比亚迪、中创新航等竞争对手的市场份额变化\n\n从估值角度看当前PE约25倍处于历史中位数附近...",
images: [
"https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?w=800",
"https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?w=800",
],
tags: ["新能源", "锂电池", "A股"],
category: "analysis",
likes_count: 156,
comments_count: 42,
views_count: 2345,
created_at: "2024-12-20T10:30:00Z",
updated_at: "2024-12-20T10:30:00Z",
is_pinned: true,
status: "active",
},
{
id: "post_002",
author_id: "user_2",
author_name: "趋势猎手",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=hunter2",
title: "技术面分析:上证指数短期走势预判",
content:
"从技术形态来看,上证指数近期呈现以下特征:\n\n- MACD金叉形成多头趋势确立\n- 5日均线上穿10日均线\n- 成交量温和放大\n\n综合来看短期内大盘有望向3200点发起冲击支撑位在3050点附近。操作上建议逢低布局优质赛道龙头。",
images: [
"https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?w=800",
],
tags: ["A股", "技术分析"],
category: "strategy",
likes_count: 89,
comments_count: 28,
views_count: 1567,
created_at: "2024-12-19T15:20:00Z",
updated_at: "2024-12-19T15:20:00Z",
is_pinned: false,
status: "active",
},
{
id: "post_003",
author_id: "user_3",
author_name: "量化先锋",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=quant3",
title: "AI芯片赛道深度研究英伟达vs AMD",
content:
"随着ChatGPT引爆AI浪潮算力需求爆发式增长。本文对比分析两大芯片巨头\n\n**英伟达 (NVDA)**\n- GPU市场份额超80%\n- CUDA生态护城河深厚\n- 数据中心业务高速增长\n\n**AMD**\n- MI300系列强势追赶\n- 性价比优势明显\n- 客户多元化策略\n\n投资建议长期看好英伟达但AMD估值更具吸引力...",
images: [
"https://images.unsplash.com/photo-1518770660439-4636190af475?w=800",
"https://images.unsplash.com/photo-1555255707-c07966088b7b?w=800",
],
tags: ["美股", "AI", "半导体"],
category: "analysis",
likes_count: 234,
comments_count: 67,
views_count: 4521,
created_at: "2024-12-18T09:00:00Z",
updated_at: "2024-12-18T09:00:00Z",
is_pinned: true,
status: "active",
},
{
id: "post_004",
author_id: "user_4",
author_name: "股市老兵",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=veteran4",
title: "白酒板块投资逻辑:消费复苏下的机会",
content:
"白酒作为A股核心资产具有以下投资逻辑\n\n1. 消费升级持续,高端白酒需求稳定\n2. 品牌壁垒高,提价能力强\n3. 现金流充沛,分红率提升空间大\n\n重点关注标的\n- 贵州茅台:行业龙头,确定性最强\n- 五粮液:次高端领军,性价比突出\n- 山西汾酒:清香型龙头,成长性好",
images: [],
tags: ["白酒", "消费", "A股"],
category: "analysis",
likes_count: 112,
comments_count: 35,
views_count: 1890,
created_at: "2024-12-17T14:30:00Z",
updated_at: "2024-12-17T14:30:00Z",
is_pinned: false,
status: "active",
},
{
id: "post_005",
author_id: "user_5",
author_name: "新手小白",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=newbie5",
title: "请教:如何判断一只股票是否值得长期持有?",
content:
"刚入市不久,想请教各位大佬几个问题:\n\n1. 判断一家公司是否值得长期投资,最重要的指标是什么?\n2. PE、PB这些估值指标应该怎么用\n3. 行业龙头和细分赛道龙头怎么选?\n\n希望大家不吝赐教感谢",
images: [],
tags: ["投资入门", "讨论"],
category: "discussion",
likes_count: 45,
comments_count: 52,
views_count: 876,
created_at: "2024-12-16T11:00:00Z",
updated_at: "2024-12-16T11:00:00Z",
is_pinned: false,
status: "active",
},
{
id: "post_006",
author_id: "user_1",
author_name: "价值投资者",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=investor1",
title: "医药板块复盘:集采影响逐步消化",
content:
"经过近两年的调整,医药板块估值已回到合理区间。从政策面看:\n\n- 集采常态化,边际影响减弱\n- 创新药审批加速\n- 医保谈判规则趋于稳定\n\n建议关注方向\n1. 创新药龙头(恒瑞医药、百济神州)\n2. CXO赛道药明康德、康龙化成\n3. 医疗器械(迈瑞医疗、联影医疗)",
images: [
"https://images.unsplash.com/photo-1584308666744-24d5c474f2ae?w=800",
],
tags: ["医药", "A股"],
category: "analysis",
likes_count: 78,
comments_count: 23,
views_count: 1234,
created_at: "2024-12-15T16:45:00Z",
updated_at: "2024-12-15T16:45:00Z",
is_pinned: false,
status: "active",
},
{
id: "post_007",
author_id: "user_2",
author_name: "趋势猎手",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=hunter2",
title: "港股科技股投资机会分析",
content:
"港股科技股经历大幅调整后,估值已具吸引力:\n\n**腾讯控股**\n- 游戏业务恢复增长\n- 视频号商业化提速\n- 回购力度加大\n\n**阿里巴巴**\n- 云业务盈利改善\n- 电商份额企稳\n- 分拆上市预期\n\n**美团**\n- 外卖业务利润率提升\n- 即时零售空间广阔",
images: [],
tags: ["港股", "科技", "互联网"],
category: "analysis",
likes_count: 145,
comments_count: 41,
views_count: 2156,
created_at: "2024-12-14T10:15:00Z",
updated_at: "2024-12-14T10:15:00Z",
is_pinned: false,
status: "active",
},
{
id: "post_008",
author_id: "user_3",
author_name: "量化先锋",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=quant3",
title: "光伏行业2024年展望产能出清进行时",
content:
"光伏行业正经历产能过剩带来的调整期:\n\n**现状分析**\n- 硅料价格跌至成本线附近\n- 组件企业盈利承压\n- 落后产能加速出清\n\n**未来展望**\n- N型技术渗透率提升\n- 海外需求持续高增\n- 龙头市占率提升\n\n投资建议等待产业链利润重新分配关注技术领先的龙头企业。",
images: [
"https://images.unsplash.com/photo-1509391366360-2e959784a276?w=800",
"https://images.unsplash.com/photo-1558449028-b53a39d100fc?w=800",
],
tags: ["光伏", "新能源", "A股"],
category: "analysis",
likes_count: 167,
comments_count: 48,
views_count: 2890,
created_at: "2024-12-13T08:30:00Z",
updated_at: "2024-12-13T08:30:00Z",
is_pinned: false,
status: "active",
},
{
id: "post_009",
author_id: "user_4",
author_name: "股市老兵",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=veteran4",
title: "分享我的投资框架:价值与成长的平衡",
content:
"投资十五年,总结出一套适合自己的投资框架:\n\n**选股标准**\n1. ROE连续5年>15%\n2. 资产负债率<50%\n3. 经营性现金流为正\n4. 行业地位前三\n\n**估值方法**\n- PE历史分位数\n- PEG估值法\n- DCF现金流折现\n\n**仓位管理**\n- 核心仓位60%(优质蓝筹)\n- 卫星仓位30%(成长股)\n- 现金储备10%",
images: [],
tags: ["投资策略", "经验分享"],
category: "experience",
likes_count: 289,
comments_count: 76,
views_count: 4567,
created_at: "2024-12-12T13:00:00Z",
updated_at: "2024-12-12T13:00:00Z",
is_pinned: true,
status: "active",
},
{
id: "post_010",
author_id: "user_1",
author_name: "价值投资者",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=investor1",
title: "军工板块:国防建设加速下的投资机会",
content:
"在复杂国际形势下,国防建设持续加码:\n\n**行业驱动力**\n- 国防预算稳定增长\n- 装备更新换代周期\n- 军民融合深化\n\n**重点方向**\n1. 航空发动机(航发动力)\n2. 导弹武器(航天电器)\n3. 信息化装备(中航电子)\n4. 新材料(光威复材)\n\n估值方面板块PE约50倍中长期看仍有配置价值。",
images: [
"https://images.unsplash.com/photo-1540575467063-178a50c2df87?w=800",
],
tags: ["军工", "A股"],
category: "analysis",
likes_count: 98,
comments_count: 31,
views_count: 1678,
created_at: "2024-12-11T09:45:00Z",
updated_at: "2024-12-11T09:45:00Z",
is_pinned: false,
status: "active",
},
];
// 帖子评论
export const mockPostComments = {
post_001: [
{
id: "comment_001_1",
post_id: "post_001",
author_id: "user_2",
author_name: "趋势猎手",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=hunter2",
content: "分析得很透彻!宁德时代确实是新能源赛道的核心资产,长期看好。",
parent_id: null,
likes_count: 23,
created_at: "2024-12-20T11:30:00Z",
status: "active",
},
{
id: "comment_001_2",
post_id: "post_001",
author_id: "user_3",
author_name: "量化先锋",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=quant3",
content:
"补充一点:钠离子电池的商业化进度值得关注,可能会打开新的增长空间。",
parent_id: null,
likes_count: 18,
created_at: "2024-12-20T12:15:00Z",
status: "active",
},
{
id: "comment_001_3",
post_id: "post_001",
author_id: "user_4",
author_name: "股市老兵",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=veteran4",
content: "目前估值还是偏贵等回调到20倍PE再考虑加仓。",
parent_id: null,
likes_count: 12,
created_at: "2024-12-20T14:00:00Z",
status: "active",
},
],
post_003: [
{
id: "comment_003_1",
post_id: "post_003",
author_id: "user_1",
author_name: "价值投资者",
author_avatar:
"https://api.dicebear.com/7.x/avataaars/svg?seed=investor1",
content: "英伟达的护城河确实深,但估值也确实贵。我选择定投,分批建仓。",
parent_id: null,
likes_count: 34,
created_at: "2024-12-18T10:30:00Z",
status: "active",
},
{
id: "comment_003_2",
post_id: "post_003",
author_id: "user_4",
author_name: "股市老兵",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=veteran4",
content: "AMD的MI300确实有竞争力但CUDA生态不是一朝一夕能追上的。",
parent_id: null,
likes_count: 28,
created_at: "2024-12-18T11:45:00Z",
status: "active",
},
],
post_005: [
{
id: "comment_005_1",
post_id: "post_005",
author_id: "user_4",
author_name: "股市老兵",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=veteran4",
content:
"最重要的是理解公司的商业模式和竞争优势。PE只是参考不同行业估值中枢不同。",
parent_id: null,
likes_count: 45,
created_at: "2024-12-16T12:00:00Z",
status: "active",
},
{
id: "comment_005_2",
post_id: "post_005",
author_id: "user_1",
author_name: "价值投资者",
author_avatar:
"https://api.dicebear.com/7.x/avataaars/svg?seed=investor1",
content:
"建议多看看公司年报特别是管理层讨论分析部分。另外ROE是个很重要的指标。",
parent_id: null,
likes_count: 38,
created_at: "2024-12-16T13:30:00Z",
status: "active",
},
{
id: "comment_005_3",
post_id: "post_005",
author_id: "user_3",
author_name: "量化先锋",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=quant3",
content:
"可以用PEG来判断成长股估值是否合理PEG=PE/盈利增速小于1通常说明被低估。",
parent_id: null,
likes_count: 32,
created_at: "2024-12-16T15:00:00Z",
status: "active",
},
],
};
export default {
mockForumUsers,
mockTags,
mockCategories,
mockPosts,
mockPostComments,
};

View File

@@ -1,554 +0,0 @@
// src/mocks/data/industries.js
// 行业分类完整树形数据 Mock
/**
* 完整的行业分类树形结构
* 包含 5 个分类体系,层级深度 2-4 层不等
*/
export const industryTreeData = [
{
value: "新财富行业分类",
label: "新财富行业分类",
children: [
{
value: "XCF001",
label: "传播与文化",
children: [
{
value: "XCF001001",
label: "互联网传媒",
children: [
{ value: "XCF001001001", label: "数字媒体" },
{ value: "XCF001001002", label: "社交平台" },
{ value: "XCF001001003", label: "短视频平台" }
]
},
{
value: "XCF001002",
label: "影视娱乐",
children: [
{ value: "XCF001002001", label: "电影制作" },
{ value: "XCF001002002", label: "网络视频" }
]
},
{
value: "XCF001003",
label: "出版发行"
}
]
},
{
value: "XCF002",
label: "交通运输仓储",
children: [
{
value: "XCF002001",
label: "航空运输",
children: [
{ value: "XCF002001001", label: "航空客运" },
{ value: "XCF002001002", label: "航空货运" }
]
},
{
value: "XCF002002",
label: "铁路运输"
},
{
value: "XCF002003",
label: "公路运输",
children: [
{ value: "XCF002003001", label: "公路客运" },
{ value: "XCF002003002", label: "公路货运" },
{ value: "XCF002003003", label: "快递物流" }
]
}
]
},
{
value: "XCF003",
label: "农林牧渔",
children: [
{ value: "XCF003001", label: "种植业" },
{ value: "XCF003002", label: "林业" },
{ value: "XCF003003", label: "畜牧业" },
{ value: "XCF003004", label: "渔业" }
]
},
{
value: "XCF004",
label: "医药生物",
children: [
{
value: "XCF004001",
label: "化学制药",
children: [
{ value: "XCF004001001", label: "化学原料药" },
{ value: "XCF004001002", label: "化学制剂" }
]
},
{
value: "XCF004002",
label: "生物制品",
children: [
{ value: "XCF004002001", label: "疫苗" },
{ value: "XCF004002002", label: "血液制品" },
{ value: "XCF004002003", label: "诊断试剂" }
]
},
{ value: "XCF004003", label: "中药" },
{ value: "XCF004004", label: "医疗器械" }
]
},
{
value: "XCF005",
label: "基础化工",
children: [
{ value: "XCF005001", label: "化学原料" },
{ value: "XCF005002", label: "化学制品" },
{ value: "XCF005003", label: "塑料" },
{ value: "XCF005004", label: "橡胶" }
]
},
{
value: "XCF006",
label: "家电",
children: [
{ value: "XCF006001", label: "白色家电" },
{ value: "XCF006002", label: "黑色家电" },
{ value: "XCF006003", label: "小家电" }
]
},
{
value: "XCF007",
label: "电子",
children: [
{
value: "XCF007001",
label: "半导体",
children: [
{ value: "XCF007001001", label: "芯片设计" },
{ value: "XCF007001002", label: "芯片制造" },
{ value: "XCF007001003", label: "封装测试" }
]
},
{ value: "XCF007002", label: "元件" },
{ value: "XCF007003", label: "光学光电子" },
{ value: "XCF007004", label: "消费电子" }
]
},
{
value: "XCF008",
label: "计算机",
children: [
{
value: "XCF008001",
label: "计算机设备",
children: [
{ value: "XCF008001001", label: "PC" },
{ value: "XCF008001002", label: "服务器" }
]
},
{
value: "XCF008002",
label: "软件开发",
children: [
{ value: "XCF008002001", label: "应用软件" },
{ value: "XCF008002002", label: "系统软件" }
]
},
{ value: "XCF008003", label: "IT服务" }
]
}
]
},
{
value: "申银万国行业分类",
label: "申银万国行业分类",
children: [
{
value: "SW001",
label: "电子",
children: [
{
value: "SW001001",
label: "半导体",
children: [
{ value: "SW001001001", label: "半导体材料" },
{ value: "SW001001002", label: "半导体设备" },
{ value: "SW001001003", label: "集成电路" }
]
},
{
value: "SW001002",
label: "电子制造",
children: [
{ value: "SW001002001", label: "PCB" },
{ value: "SW001002002", label: "被动元件" }
]
},
{ value: "SW001003", label: "光学光电子" }
]
},
{
value: "SW002",
label: "计算机",
children: [
{ value: "SW002001", label: "计算机设备" },
{ value: "SW002002", label: "计算机应用" },
{ value: "SW002003", label: "通信设备" }
]
},
{
value: "SW003",
label: "传媒",
children: [
{ value: "SW003001", label: "互联网传媒" },
{ value: "SW003002", label: "营销传播" },
{ value: "SW003003", label: "文化传媒" }
]
},
{
value: "SW004",
label: "医药生物",
children: [
{ value: "SW004001", label: "化学制药" },
{ value: "SW004002", label: "中药" },
{ value: "SW004003", label: "生物制品" },
{ value: "SW004004", label: "医疗器械" },
{ value: "SW004005", label: "医药商业" }
]
},
{
value: "SW005",
label: "汽车",
children: [
{
value: "SW005001",
label: "乘用车",
children: [
{ value: "SW005001001", label: "燃油车" },
{ value: "SW005001002", label: "新能源车" }
]
},
{ value: "SW005002", label: "商用车" },
{ value: "SW005003", label: "汽车零部件" }
]
},
{
value: "SW006",
label: "机械设备",
children: [
{ value: "SW006001", label: "通用设备" },
{ value: "SW006002", label: "专用设备" },
{ value: "SW006003", label: "仪器仪表" }
]
},
{
value: "SW007",
label: "食品饮料",
children: [
{ value: "SW007001", label: "白酒" },
{ value: "SW007002", label: "啤酒" },
{ value: "SW007003", label: "软饮料" },
{ value: "SW007004", label: "食品加工" }
]
},
{
value: "SW008",
label: "银行",
children: [
{ value: "SW008001", label: "国有银行" },
{ value: "SW008002", label: "股份制银行" },
{ value: "SW008003", label: "城商行" }
]
},
{
value: "SW009",
label: "非银金融",
children: [
{ value: "SW009001", label: "证券" },
{ value: "SW009002", label: "保险" },
{ value: "SW009003", label: "多元金融" }
]
},
{
value: "SW010",
label: "房地产",
children: [
{ value: "SW010001", label: "房地产开发" },
{ value: "SW010002", label: "房地产服务" }
]
}
]
},
{
value: "证监会行业分类2001",
label: "证监会行业分类2001",
children: [
{
value: "CSRC_A",
label: "A 农、林、牧、渔业",
children: [
{ value: "CSRC_A01", label: "A01 农业" },
{ value: "CSRC_A02", label: "A02 林业" },
{ value: "CSRC_A03", label: "A03 畜牧业" },
{ value: "CSRC_A04", label: "A04 渔业" }
]
},
{
value: "CSRC_B",
label: "B 采矿业",
children: [
{ value: "CSRC_B06", label: "B06 煤炭开采和洗选业" },
{ value: "CSRC_B07", label: "B07 石油和天然气开采业" },
{ value: "CSRC_B08", label: "B08 黑色金属矿采选业" },
{ value: "CSRC_B09", label: "B09 有色金属矿采选业" }
]
},
{
value: "CSRC_C",
label: "C 制造业",
children: [
{
value: "CSRC_C13",
label: "C13 农副食品加工业",
children: [
{ value: "CSRC_C1310", label: "C1310 肉制品加工" },
{ value: "CSRC_C1320", label: "C1320 水产品加工" }
]
},
{
value: "CSRC_C27",
label: "C27 医药制造业",
children: [
{ value: "CSRC_C2710", label: "C2710 化学药品原料药制造" },
{ value: "CSRC_C2720", label: "C2720 化学药品制剂制造" },
{ value: "CSRC_C2730", label: "C2730 中药饮片加工" }
]
},
{ value: "CSRC_C35", label: "C35 专用设备制造业" },
{ value: "CSRC_C39", label: "C39 计算机、通信和其他电子设备制造业" }
]
},
{
value: "CSRC_I",
label: "I 信息传输、软件和信息技术服务业",
children: [
{ value: "CSRC_I63", label: "I63 电信、广播电视和卫星传输服务" },
{ value: "CSRC_I64", label: "I64 互联网和相关服务" },
{ value: "CSRC_I65", label: "I65 软件和信息技术服务业" }
]
},
{
value: "CSRC_J",
label: "J 金融业",
children: [
{ value: "CSRC_J66", label: "J66 货币金融服务" },
{ value: "CSRC_J67", label: "J67 资本市场服务" },
{ value: "CSRC_J68", label: "J68 保险业" }
]
},
{
value: "CSRC_K",
label: "K 房地产业",
children: [
{ value: "CSRC_K70", label: "K70 房地产业" }
]
}
]
},
{
value: "中银国际行业分类",
label: "中银国际行业分类",
children: [
{
value: "BOC001",
label: "能源",
children: [
{ value: "BOC001001", label: "石油天然气" },
{ value: "BOC001002", label: "煤炭" },
{ value: "BOC001003", label: "新能源" }
]
},
{
value: "BOC002",
label: "原材料",
children: [
{ value: "BOC002001", label: "化工" },
{ value: "BOC002002", label: "钢铁" },
{ value: "BOC002003", label: "有色金属" },
{ value: "BOC002004", label: "建材" }
]
},
{
value: "BOC003",
label: "工业",
children: [
{ value: "BOC003001", label: "机械" },
{ value: "BOC003002", label: "电气设备" },
{ value: "BOC003003", label: "国防军工" }
]
},
{
value: "BOC004",
label: "消费",
children: [
{
value: "BOC004001",
label: "可选消费",
children: [
{ value: "BOC004001001", label: "汽车" },
{ value: "BOC004001002", label: "家电" },
{ value: "BOC004001003", label: "纺织服装" }
]
},
{
value: "BOC004002",
label: "必需消费",
children: [
{ value: "BOC004002001", label: "食品饮料" },
{ value: "BOC004002002", label: "农林牧渔" }
]
}
]
},
{
value: "BOC005",
label: "医疗保健",
children: [
{ value: "BOC005001", label: "医药" },
{ value: "BOC005002", label: "医疗器械" },
{ value: "BOC005003", label: "医疗服务" }
]
},
{
value: "BOC006",
label: "金融",
children: [
{ value: "BOC006001", label: "银行" },
{ value: "BOC006002", label: "非银金融" }
]
},
{
value: "BOC007",
label: "科技",
children: [
{
value: "BOC007001",
label: "信息技术",
children: [
{ value: "BOC007001001", label: "半导体" },
{ value: "BOC007001002", label: "电子" },
{ value: "BOC007001003", label: "计算机" },
{ value: "BOC007001004", label: "通信" }
]
},
{ value: "BOC007002", label: "传媒" }
]
}
]
},
{
value: "巨潮行业分类",
label: "巨潮行业分类",
children: [
{
value: "JC01",
label: "制造业",
children: [
{
value: "JC0101",
label: "电气机械及器材制造业",
children: [
{ value: "JC010101", label: "电机制造" },
{ value: "JC010102", label: "输配电及控制设备制造" },
{ value: "JC010103", label: "电池制造" }
]
},
{
value: "JC0102",
label: "医药制造业",
children: [
{ value: "JC010201", label: "化学药品原药制造" },
{ value: "JC010202", label: "化学药品制剂制造" },
{ value: "JC010203", label: "中成药制造" },
{ value: "JC010204", label: "生物、生化制品制造" }
]
},
{ value: "JC0103", label: "食品制造业" },
{ value: "JC0104", label: "纺织业" }
]
},
{
value: "JC02",
label: "信息传输、软件和信息技术服务业",
children: [
{ value: "JC0201", label: "互联网和相关服务" },
{ value: "JC0202", label: "软件和信息技术服务业" }
]
},
{
value: "JC03",
label: "批发和零售业",
children: [
{ value: "JC0301", label: "批发业" },
{ value: "JC0302", label: "零售业" }
]
},
{
value: "JC04",
label: "房地产业",
children: [
{ value: "JC0401", label: "房地产开发经营" },
{ value: "JC0402", label: "物业管理" }
]
},
{
value: "JC05",
label: "金融业",
children: [
{ value: "JC0501", label: "货币金融服务" },
{ value: "JC0502", label: "资本市场服务" },
{ value: "JC0503", label: "保险业" }
]
},
{
value: "JC06",
label: "交通运输、仓储和邮政业",
children: [
{ value: "JC0601", label: "道路运输业" },
{ value: "JC0602", label: "航空运输业" },
{ value: "JC0603", label: "水上运输业" }
]
},
{
value: "JC07",
label: "采矿业",
children: [
{ value: "JC0701", label: "煤炭开采和洗选业" },
{ value: "JC0702", label: "石油和天然气开采业" },
{ value: "JC0703", label: "有色金属矿采选业" }
]
},
{
value: "JC08",
label: "农、林、牧、渔业",
children: [
{ value: "JC0801", label: "农业" },
{ value: "JC0802", label: "林业" },
{ value: "JC0803", label: "畜牧业" },
{ value: "JC0804", label: "渔业" }
]
},
{
value: "JC09",
label: "建筑业",
children: [
{ value: "JC0901", label: "房屋建筑业" },
{ value: "JC0902", label: "土木工程建筑业" },
{ value: "JC0903", label: "建筑装饰和其他建筑业" }
]
}
]
}
];

View File

@@ -1,164 +0,0 @@
// src/mocks/data/kline.js
// K线数据生成函数
/**
* 生成分时数据 (timeline)
* 用于展示当日分钟级别的价格走势
*/
export const generateTimelineData = (indexCode) => {
const data = [];
const basePrice = getBasePrice(indexCode);
const today = new Date();
// 生成早盘数据 (09:30 - 11:30)
const morningStart = new Date(today.setHours(9, 30, 0, 0));
const morningEnd = new Date(today.setHours(11, 30, 0, 0));
generateTimeRange(data, morningStart, morningEnd, basePrice, 'morning');
// 生成午盘数据 (13:00 - 15:00)
const afternoonStart = new Date(today.setHours(13, 0, 0, 0));
const afternoonEnd = new Date(today.setHours(15, 0, 0, 0));
generateTimeRange(data, afternoonStart, afternoonEnd, basePrice, 'afternoon');
return data;
};
/**
* 生成日线数据 (daily)
* 用于获取历史收盘价等数据
* 默认生成 400 天的数据,覆盖足够的历史范围
*/
export const generateDailyData = (indexCode, days = 400) => {
const data = [];
const basePrice = getBasePrice(indexCode);
const today = new Date();
// 使用固定种子生成一致的随机数,确保同一天的涨跌幅一致
const seededRandom = (seed) => {
const x = Math.sin(seed) * 10000;
return x - Math.floor(x);
};
for (let i = days - 1; i >= 0; i--) {
const date = new Date(today);
date.setDate(date.getDate() - i);
// 跳过周末
const dayOfWeek = date.getDay();
if (dayOfWeek === 0 || dayOfWeek === 6) continue;
// 使用日期作为种子,确保同一天生成相同的数据
const dateSeed = date.getFullYear() * 10000 + (date.getMonth() + 1) * 100 + date.getDate();
const rand1 = seededRandom(dateSeed);
const rand2 = seededRandom(dateSeed + 1);
const rand3 = seededRandom(dateSeed + 2);
const open = basePrice * (1 + (rand1 * 0.04 - 0.02));
const close = open * (1 + (rand2 * 0.03 - 0.015));
const high = Math.max(open, close) * (1 + rand3 * 0.015);
const low = Math.min(open, close) * (1 - seededRandom(dateSeed + 3) * 0.015);
const volume = Math.floor(seededRandom(dateSeed + 4) * 50000000000 + 10000000000);
data.push({
date: formatDate(date),
time: formatDate(date),
open: parseFloat(open.toFixed(2)),
close: parseFloat(close.toFixed(2)),
high: parseFloat(high.toFixed(2)),
low: parseFloat(low.toFixed(2)),
volume: volume,
prev_close: data.length === 0 ? parseFloat((basePrice * 0.99).toFixed(2)) : data[data.length - 1]?.close
});
}
return data;
};
/**
* 计算简单移动均价(用于分时图均价线)
* @param {Array} data - 已有数据
* @param {number} currentPrice - 当前价格
* @param {number} period - 均线周期默认5
* @returns {number} 均价
*/
function calculateAvgPrice(data, currentPrice, period = 5) {
const recentPrices = data.slice(-period).map(d => d.price || d.close);
recentPrices.push(currentPrice);
const sum = recentPrices.reduce((acc, p) => acc + p, 0);
return parseFloat((sum / recentPrices.length).toFixed(2));
}
/**
* 生成时间范围内的数据
*/
function generateTimeRange(data, startTime, endTime, basePrice, session) {
const current = new Date(startTime);
let price = basePrice;
// 波动趋势(早盘和午盘可能有不同的走势)
const trend = session === 'morning' ? Math.random() * 0.02 - 0.01 : Math.random() * 0.015 - 0.005;
while (current <= endTime) {
// 添加随机波动
const volatility = (Math.random() - 0.5) * 0.005;
price = price * (1 + trend / 120 + volatility); // 每分钟微小变化
const volume = Math.floor(Math.random() * 500000000 + 100000000);
// ✅ 修复:为分时图添加完整的 OHLC 字段
const closePrice = parseFloat(price.toFixed(2));
// 计算均价和涨跌幅
const avgPrice = calculateAvgPrice(data, closePrice);
const changePercent = parseFloat(((closePrice - basePrice) / basePrice * 100).toFixed(2));
data.push({
time: formatTime(current),
timestamp: current.getTime(), // ✅ 新增:毫秒时间戳
open: parseFloat((price * 0.9999).toFixed(2)), // ✅ 新增:开盘价(略低于收盘)
high: parseFloat((price * 1.0002).toFixed(2)), // ✅ 新增:最高价(略高于收盘)
low: parseFloat((price * 0.9997).toFixed(2)), // ✅ 新增:最低价(略低于收盘)
close: closePrice, // ✅ 保留:收盘价
price: closePrice, // ✅ 保留:兼容字段(供 MiniTimelineChart 使用)
avg_price: avgPrice, // ✅ 新增:均价(供 TimelineChartModal 使用)
change_percent: changePercent, // ✅ 新增:涨跌幅(供 TimelineChartModal 使用)
volume: volume,
prev_close: basePrice
});
// 增加1分钟
current.setMinutes(current.getMinutes() + 1);
}
}
/**
* 获取不同指数的基准价格
*/
function getBasePrice(indexCode) {
const basePrices = {
'000001.SH': 3200, // 上证指数
'399001.SZ': 10500, // 深证成指
'399006.SZ': 2100 // 创业板指
};
return basePrices[indexCode] || 3000;
}
/**
* 格式化时间为 HH:mm
*/
function formatTime(date) {
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}
/**
* 格式化日期为 YYYY-MM-DD
*/
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}

View File

@@ -1,308 +0,0 @@
// src/mocks/data/market.js
// 市场行情相关的 Mock 数据
// 股票名称映射
const STOCK_NAME_MAP = {
'000001': { name: '平安银行', basePrice: 13.50 },
'600000': { name: '浦发银行', basePrice: 8.20 },
'600519': { name: '贵州茅台', basePrice: 1650.00 },
'000858': { name: '五粮液', basePrice: 165.00 },
'601318': { name: '中国平安', basePrice: 45.00 },
'600036': { name: '招商银行', basePrice: 32.00 },
'300750': { name: '宁德时代', basePrice: 180.00 },
'002594': { name: '比亚迪', basePrice: 260.00 },
};
// 生成市场数据
export const generateMarketData = (stockCode) => {
const stockInfo = STOCK_NAME_MAP[stockCode] || { name: `股票${stockCode}`, basePrice: 20.00 };
const basePrice = stockInfo.basePrice;
return {
stockCode,
// 成交数据 - 必须包含K线所需的字段
tradeData: {
success: true,
data: Array(30).fill(null).map((_, i) => {
const open = basePrice + (Math.random() - 0.5) * 0.5;
const close = basePrice + (Math.random() - 0.5) * 0.5;
const high = Math.max(open, close) + Math.random() * 0.3;
const low = Math.min(open, close) - Math.random() * 0.3;
return {
date: new Date(Date.now() - (29 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
open: parseFloat(open.toFixed(2)),
close: parseFloat(close.toFixed(2)),
high: parseFloat(high.toFixed(2)),
low: parseFloat(low.toFixed(2)),
volume: Math.floor(Math.random() * 500000000) + 100000000, // 1-6亿股
amount: Math.floor(Math.random() * 7000000000) + 1300000000, // 13-80亿元
turnover_rate: parseFloat((Math.random() * 2 + 0.5).toFixed(2)), // 0.5-2.5%
change_percent: parseFloat((Math.random() * 6 - 3).toFixed(2)), // -3% to +3%
pe_ratio: parseFloat((Math.random() * 3 + 4).toFixed(2)) // 4-7
};
})
},
// 资金流向 - 融资融券数据数组
fundingData: {
success: true,
data: Array(30).fill(null).map((_, i) => ({
date: new Date(Date.now() - (29 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
financing: {
balance: Math.floor(Math.random() * 5000000000) + 10000000000, // 融资余额
buy: Math.floor(Math.random() * 500000000) + 100000000, // 融资买入
repay: Math.floor(Math.random() * 500000000) + 80000000 // 融资偿还
},
securities: {
balance: Math.floor(Math.random() * 100000000) + 50000000, // 融券余额(股数)
balance_amount: Math.floor(Math.random() * 2000000000) + 1000000000, // 融券余额(金额)
sell: Math.floor(Math.random() * 10000000) + 5000000, // 融券卖出
repay: Math.floor(Math.random() * 10000000) + 3000000 // 融券偿还
}
}))
},
// 大宗交易 - 包含 daily_stats 数组,符合 BigDealDayStats 类型
bigDealData: {
success: true,
data: [],
daily_stats: Array(10).fill(null).map((_, i) => {
const count = Math.floor(Math.random() * 5) + 1; // 1-5 笔交易
const avgPrice = parseFloat((basePrice * (0.95 + Math.random() * 0.1)).toFixed(2)); // 折价/溢价 -5%~+5%
const deals = Array(count).fill(null).map(() => {
const volume = parseFloat((Math.random() * 500 + 100).toFixed(2)); // 100-600 万股
const price = parseFloat((avgPrice * (0.98 + Math.random() * 0.04)).toFixed(2));
return {
buyer_dept: ['中信证券北京总部', '国泰君安上海分公司', '华泰证券深圳营业部', '招商证券广州分公司'][Math.floor(Math.random() * 4)],
seller_dept: ['中金公司北京营业部', '海通证券上海分公司', '广发证券深圳营业部', '平安证券广州分公司'][Math.floor(Math.random() * 4)],
price,
volume,
amount: parseFloat((price * volume).toFixed(2))
};
});
const totalVolume = deals.reduce((sum, d) => sum + d.volume, 0);
const totalAmount = deals.reduce((sum, d) => sum + d.amount, 0);
return {
date: new Date(Date.now() - (9 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
count,
total_volume: parseFloat(totalVolume.toFixed(2)),
total_amount: parseFloat(totalAmount.toFixed(2)),
avg_price: avgPrice,
deals
};
})
},
// 龙虎榜数据 - 包含 grouped_data 数组,符合 UnusualDayData 类型
unusualData: {
success: true,
data: [],
grouped_data: Array(5).fill(null).map((_, i) => {
const buyerDepts = ['中信证券北京总部', '国泰君安上海分公司', '华泰证券深圳营业部', '招商证券广州分公司', '中金公司北京营业部'];
const sellerDepts = ['海通证券上海分公司', '广发证券深圳营业部', '平安证券广州分公司', '东方证券上海营业部', '兴业证券福州营业部'];
const infoTypes = ['日涨幅偏离值达7%', '日振幅达15%', '连续三日涨幅偏离20%', '换手率达20%'];
const buyers = buyerDepts.map(dept => ({
dept_name: dept,
buy_amount: Math.floor(Math.random() * 50000000) + 10000000 // 1000万-6000万
})).sort((a, b) => b.buy_amount - a.buy_amount);
const sellers = sellerDepts.map(dept => ({
dept_name: dept,
sell_amount: Math.floor(Math.random() * 40000000) + 8000000 // 800万-4800万
})).sort((a, b) => b.sell_amount - a.sell_amount);
const totalBuy = buyers.reduce((sum, b) => sum + b.buy_amount, 0);
const totalSell = sellers.reduce((sum, s) => sum + s.sell_amount, 0);
return {
date: new Date(Date.now() - (4 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
total_buy: totalBuy,
total_sell: totalSell,
net_amount: totalBuy - totalSell,
buyers,
sellers,
info_types: infoTypes.slice(0, Math.floor(Math.random() * 3) + 1) // 随机选1-3个类型
};
})
},
// 股权质押 - 匹配 PledgeData[] 类型
pledgeData: {
success: true,
data: Array(12).fill(null).map((_, i) => {
const date = new Date();
date.setMonth(date.getMonth() - (11 - i));
return {
end_date: date.toISOString().split('T')[0].slice(0, 7) + '-01',
unrestricted_pledge: Math.floor(Math.random() * 1000000000) + 500000000,
restricted_pledge: Math.floor(Math.random() * 200000000) + 50000000,
total_pledge: Math.floor(Math.random() * 1200000000) + 550000000,
total_shares: 19405918198,
pledge_ratio: parseFloat((Math.random() * 3 + 6).toFixed(2)), // 6-9%
pledge_count: Math.floor(Math.random() * 50) + 100 // 100-150
};
})
},
// 市场摘要 - 匹配 MarketSummary 类型
summaryData: {
success: true,
data: {
stock_code: stockCode,
stock_name: stockInfo.name,
latest_trade: {
close: basePrice,
change_percent: 1.89,
volume: 345678900,
amount: 4678900000,
turnover_rate: 1.78,
pe_ratio: 4.96
},
latest_funding: {
financing_balance: 5823000000,
securities_balance: 125600000
},
latest_pledge: {
pledge_ratio: 8.25
}
}
},
// 涨幅分析 - 匹配 RiseAnalysis 类型,每个交易日一条记录
riseAnalysisData: {
success: true,
data: Array(30).fill(null).map((_, i) => {
const tradeDate = new Date(Date.now() - (29 - i) * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
const riseRate = parseFloat(((Math.random() - 0.5) * 10).toFixed(2)); // -5% ~ +5%
const closePrice = parseFloat((basePrice * (1 + riseRate / 100)).toFixed(2));
const volume = Math.floor(Math.random() * 500000000) + 100000000;
const amount = Math.floor(volume * closePrice);
// 涨幅分析详情模板
const riseReasons = [
{
brief: '业绩超预期',
detail: `## 业绩驱动\n\n${stockInfo.name}发布业绩公告,主要经营指标超出市场预期:\n\n- **营业收入**同比增长15.3%环比增长5.2%\n- **净利润**同比增长18.7%,创历史新高\n- **毛利率**提升2.1个百分点至35.8%\n\n### 核心亮点\n\n1. 主营业务增长强劲,市场份额持续提升\n2. 成本管控效果显著,盈利能力改善\n3. 新产品放量,贡献增量收入`,
announcements: `**重要公告**\n\n1. [${stockInfo.name}关于2024年度业绩预告的公告](javascript:void(0))\n2. [${stockInfo.name}:关于获得政府补助的公告](javascript:void(0))`
},
{
brief: '政策利好',
detail: `## 政策催化\n\n近期行业政策密集出台,对${stockInfo.name}形成重大利好:\n\n### 政策要点\n\n- **行业支持政策**:国家出台支持措施,加大对行业的扶持力度\n- **税收优惠**:符合条件的企业可享受税收减免\n- **融资支持**:拓宽企业融资渠道,降低融资成本\n\n### 受益分析\n\n公司作为行业龙头,有望充分受益于政策红利,预计:\n\n1. 订单量将显著增长\n2. 毛利率有望提升\n3. 市场份额进一步扩大`,
announcements: `**相关公告**\n\n1. [${stockInfo.name}:关于行业政策影响的说明公告](javascript:void(0))`
},
{
brief: '资金流入',
detail: `## 资金面分析\n\n今日${stockInfo.name}获得主力资金大幅流入:\n\n### 资金流向\n\n| 指标 | 数值 | 变化 |\n|------|------|------|\n| 主力净流入 | 3.2亿 | +156% |\n| 超大单净流入 | 1.8亿 | +89% |\n| 大单净流入 | 1.4亿 | +67% |\n\n### 分析结论\n\n1. 机构资金持续加仓,看好公司长期价值\n2. 北向资金连续3日净买入\n3. 融资余额创近期新高`,
announcements: ''
},
{
brief: '技术突破',
detail: `## 技术面分析\n\n${stockInfo.name}今日实现技术突破:\n\n### 技术信号\n\n- **突破关键阻力位**:成功站上${(closePrice * 0.95).toFixed(2)}元重要阻力\n- **量价配合良好**成交量较昨日放大1.5倍\n- **均线多头排列**5日、10日、20日均线呈多头排列\n\n### 后市展望\n\n技术面看,股价有望继续向上挑战${(closePrice * 1.05).toFixed(2)}元目标位。建议关注:\n\n1. 能否持续放量\n2. 均线支撑情况\n3. MACD金叉确认`,
announcements: ''
}
];
const reasonIndex = i % riseReasons.length;
const reason = riseReasons[reasonIndex];
// 研报数据
const publishers = ['中信证券', '华泰证券', '国泰君安', '招商证券', '中金公司', '海通证券'];
const authors = ['张三', '李四', '王五', '赵六', '钱七', '孙八'];
const matchScores = ['好', '中', '差'];
return {
stock_code: stockCode,
stock_name: stockInfo.name,
trade_date: tradeDate,
rise_rate: riseRate,
close_price: closePrice,
volume: volume,
amount: amount,
main_business: stockInfo.business || '金融服务、零售银行、对公业务、资产管理等',
rise_reason_brief: reason.brief,
rise_reason_detail: reason.detail,
announcements: reason.announcements || '',
verification_reports: [
{
publisher: publishers[i % publishers.length],
match_score: matchScores[Math.floor(Math.random() * 3)],
match_ratio: parseFloat((Math.random() * 0.5 + 0.5).toFixed(2)),
declare_date: tradeDate,
report_title: `${stockInfo.name}深度研究:${reason.brief}带来投资机会`,
author: authors[i % authors.length],
verification_item: `${reason.brief}对公司业绩的影响分析`,
content: `我们认为${stockInfo.name}${reason.brief}的背景下,有望实现业绩的持续增长。维持"买入"评级,目标价${(closePrice * 1.2).toFixed(2)}元。`
},
{
publisher: publishers[(i + 1) % publishers.length],
match_score: matchScores[Math.floor(Math.random() * 3)],
match_ratio: parseFloat((Math.random() * 0.4 + 0.3).toFixed(2)),
declare_date: tradeDate,
report_title: `${stockInfo.name}跟踪报告:关注${reason.brief}`,
author: authors[(i + 1) % authors.length],
verification_item: '估值分析与投资建议',
content: `当前估值处于历史中低位,安全边际充足。建议投资者积极关注。`
}
],
update_time: new Date().toISOString().split('T')[0] + ' 18:30:00',
create_time: tradeDate + ' 15:30:00'
};
})
},
// 最新分时数据 - 匹配 MinuteData 类型
latestMinuteData: {
success: true,
data: (() => {
const minuteData = [];
// 上午 9:30-11:30 (120分钟)
for (let i = 0; i < 120; i++) {
const hour = 9 + Math.floor((30 + i) / 60);
const min = (30 + i) % 60;
const time = `${hour.toString().padStart(2, '0')}:${min.toString().padStart(2, '0')}`;
const randomChange = (Math.random() - 0.5) * 0.1;
const open = parseFloat((basePrice + randomChange).toFixed(2));
const close = parseFloat((basePrice + randomChange + (Math.random() - 0.5) * 0.05).toFixed(2));
const high = parseFloat(Math.max(open, close, open + Math.random() * 0.05).toFixed(2));
const low = parseFloat(Math.min(open, close, close - Math.random() * 0.05).toFixed(2));
minuteData.push({
time,
open,
close,
high,
low,
volume: Math.floor(Math.random() * 2000000) + 500000,
amount: Math.floor(Math.random() * 30000000) + 5000000
});
}
// 下午 13:00-15:00 (120分钟)
for (let i = 0; i < 120; i++) {
const hour = 13 + Math.floor(i / 60);
const min = i % 60;
const time = `${hour.toString().padStart(2, '0')}:${min.toString().padStart(2, '0')}`;
const randomChange = (Math.random() - 0.5) * 0.1;
const open = parseFloat((basePrice + randomChange).toFixed(2));
const close = parseFloat((basePrice + randomChange + (Math.random() - 0.5) * 0.05).toFixed(2));
const high = parseFloat(Math.max(open, close, open + Math.random() * 0.05).toFixed(2));
const low = parseFloat(Math.min(open, close, close - Math.random() * 0.05).toFixed(2));
minuteData.push({
time,
open,
close,
high,
low,
volume: Math.floor(Math.random() * 1500000) + 400000,
amount: Math.floor(Math.random() * 25000000) + 4000000
});
}
return minuteData;
})(),
code: stockCode,
name: stockInfo.name,
trade_date: new Date().toISOString().split('T')[0],
type: '1min'
}
};
};

View File

@@ -1,342 +0,0 @@
/**
* 预测市场 Mock 数据
*/
// 模拟用户
export const mockUsers = [
{
id: 1,
nickname: "价值投资者",
username: "value_investor",
avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=1",
},
{
id: 2,
nickname: "趋势猎手",
username: "trend_hunter",
avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
},
{
id: 3,
nickname: "量化先锋",
username: "quant_pioneer",
avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=3",
},
{
id: 4,
nickname: "股市老兵",
username: "stock_veteran",
avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=4",
},
{
id: 5,
nickname: "新手小白",
username: "newbie",
avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=5",
},
];
// 预测话题列表
export const mockTopics = [
{
id: 1,
title: "2024年A股能否突破3500点",
description:
"预测2024年内上证指数是否能够突破3500点大关。以2024年12月31日收盘价为准。",
category: "stock",
tags: ["A股", "大盘", "指数"],
author_id: 1,
author_name: "价值投资者",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=1",
created_at: "2024-01-15T10:00:00Z",
deadline: "2024-12-31T15:00:00Z",
status: "active",
total_pool: 15000,
yes_total_shares: 120,
no_total_shares: 80,
yes_price: 600,
no_price: 400,
yes_lord_id: 2,
no_lord_id: 3,
yes_lord_name: "趋势猎手",
no_lord_name: "量化先锋",
participants_count: 25,
comments_count: 18,
},
{
id: 2,
title: "英伟达股价年底能否突破800美元",
description: "预测英伟达(NVDA)股价在2024年底前是否能突破800美元。",
category: "stock",
tags: ["美股", "AI", "英伟达"],
author_id: 2,
author_name: "趋势猎手",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=2",
created_at: "2024-02-01T14:30:00Z",
deadline: "2024-12-31T23:59:00Z",
status: "active",
total_pool: 28000,
yes_total_shares: 200,
no_total_shares: 100,
yes_price: 667,
no_price: 333,
yes_lord_id: 1,
no_lord_id: 4,
yes_lord_name: "价值投资者",
no_lord_name: "股市老兵",
participants_count: 42,
comments_count: 35,
},
{
id: 3,
title: "比特币2024年能否创历史新高",
description: "预测比特币在2024年是否能够突破历史最高价69000美元。",
category: "crypto",
tags: ["比特币", "加密货币", "BTC"],
author_id: 3,
author_name: "量化先锋",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=3",
created_at: "2024-01-20T09:00:00Z",
deadline: "2024-12-31T23:59:00Z",
status: "active",
total_pool: 50000,
yes_total_shares: 300,
no_total_shares: 200,
yes_price: 600,
no_price: 400,
yes_lord_id: 2,
no_lord_id: 1,
yes_lord_name: "趋势猎手",
no_lord_name: "价值投资者",
participants_count: 68,
comments_count: 52,
},
{
id: 4,
title: "茅台股价能否重返2000元",
description: "预测贵州茅台股价在2024年内是否能够重返2000元以上。",
category: "stock",
tags: ["白酒", "茅台", "A股"],
author_id: 4,
author_name: "股市老兵",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=4",
created_at: "2024-02-10T11:00:00Z",
deadline: "2024-12-31T15:00:00Z",
status: "active",
total_pool: 12000,
yes_total_shares: 60,
no_total_shares: 140,
yes_price: 300,
no_price: 700,
yes_lord_id: 5,
no_lord_id: 3,
yes_lord_name: "新手小白",
no_lord_name: "量化先锋",
participants_count: 30,
comments_count: 22,
},
{
id: 5,
title: "美联储2024年会降息几次(3次以上)",
description: "预测美联储在2024年是否会降息3次或以上。",
category: "general",
tags: ["美联储", "降息", "宏观"],
author_id: 1,
author_name: "价值投资者",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=1",
created_at: "2024-01-25T16:00:00Z",
deadline: "2024-12-31T23:59:00Z",
status: "active",
total_pool: 20000,
yes_total_shares: 150,
no_total_shares: 150,
yes_price: 500,
no_price: 500,
yes_lord_id: 4,
no_lord_id: 2,
yes_lord_name: "股市老兵",
no_lord_name: "趋势猎手",
participants_count: 55,
comments_count: 40,
},
];
// 用户账户数据
export const mockUserAccount = {
user_id: 1,
balance: 8500,
frozen: 1500,
total: 10000,
total_earned: 12000,
total_spent: 3500,
total_profit: 2000,
last_daily_bonus: null, // 可领取
stats: {
total_topics: 3,
win_count: 5,
loss_count: 2,
win_rate: 0.714,
best_profit: 1200,
total_trades: 15,
},
};
// 用户持仓
export const mockPositions = [
{
id: 1,
topic_id: 1,
topic_title: "2024年A股能否突破3500点",
direction: "yes",
shares: 50,
avg_cost: 550,
current_price: 600,
current_value: 30000,
unrealized_pnl: 2500,
acquired_at: "2024-01-20T10:30:00Z",
},
{
id: 2,
topic_id: 2,
topic_title: "英伟达股价年底能否突破800美元",
direction: "yes",
shares: 30,
avg_cost: 600,
current_price: 667,
current_value: 20010,
unrealized_pnl: 2010,
acquired_at: "2024-02-05T14:00:00Z",
},
{
id: 3,
topic_id: 4,
topic_title: "茅台股价能否重返2000元",
direction: "no",
shares: 20,
avg_cost: 650,
current_price: 700,
current_value: 14000,
unrealized_pnl: 1000,
acquired_at: "2024-02-12T09:30:00Z",
},
];
// 评论数据
export const mockComments = {
1: [
// topic_id: 1 的评论
{
id: 101,
topic_id: 1,
user: mockUsers[1],
content:
"看好A股政策面利好不断预计下半年会有一波行情。技术面已经筑底完成可以积极布局。",
parent_id: null,
likes_count: 42,
is_liked: false,
is_lord: true,
total_investment: 2500,
investment_shares: 25,
verification_status: null,
created_at: "2024-01-16T10:30:00Z",
},
{
id: 102,
topic_id: 1,
user: mockUsers[2],
content:
"持谨慎态度,虽然政策有支持,但经济基本面还需要时间恢复。建议观望为主。",
parent_id: null,
likes_count: 28,
is_liked: true,
is_lord: true,
total_investment: 1800,
investment_shares: 18,
verification_status: null,
created_at: "2024-01-17T14:20:00Z",
},
{
id: 103,
topic_id: 1,
user: mockUsers[3],
content: "从量化指标来看,当前市场估值处于历史低位,长期来看有投资价值。",
parent_id: null,
likes_count: 35,
is_liked: false,
is_lord: false,
total_investment: 500,
investment_shares: 5,
verification_status: null,
created_at: "2024-01-18T09:15:00Z",
},
],
2: [
// topic_id: 2 的评论
{
id: 201,
topic_id: 2,
user: mockUsers[0],
content:
"AI浪潮势不可挡英伟达作为算力龙头业绩增长确定性强。800美元只是时间问题。",
parent_id: null,
likes_count: 56,
is_liked: true,
is_lord: true,
total_investment: 3500,
investment_shares: 35,
verification_status: null,
created_at: "2024-02-02T11:00:00Z",
},
{
id: 202,
topic_id: 2,
user: mockUsers[3],
content: "估值已经很高了需要警惕回调风险。但长期仍然看好AI赛道。",
parent_id: null,
likes_count: 38,
is_liked: false,
is_lord: true,
total_investment: 2000,
investment_shares: 20,
verification_status: null,
created_at: "2024-02-03T16:30:00Z",
},
],
};
// 交易记录
export const mockTrades = [
{
id: 1,
topic_id: 1,
user_id: 1,
direction: "yes",
type: "buy",
shares: 50,
price: 550,
total_cost: 27500,
tax: 550,
created_at: "2024-01-20T10:30:00Z",
},
{
id: 2,
topic_id: 2,
user_id: 1,
direction: "yes",
type: "buy",
shares: 30,
price: 600,
total_cost: 18000,
tax: 360,
created_at: "2024-02-05T14:00:00Z",
},
];
export default {
mockUsers,
mockTopics,
mockUserAccount,
mockPositions,
mockComments,
mockTrades,
};

View File

@@ -1,135 +0,0 @@
// Mock 用户数据
export const mockUsers = {
// 免费用户 - 手机号登录
'13800138000': {
id: 1,
phone: '13800138000',
nickname: '测试用户',
email: 'test@example.com',
avatar_url: 'https://i.pravatar.cc/150?img=1',
has_wechat: false,
created_at: '2024-01-01T00:00:00Z',
// 会员信息 - 免费用户
subscription_type: 'free',
subscription_status: 'active',
subscription_end_date: null,
is_subscription_active: true,
subscription_days_left: 0
},
// Pro 会员 - 手机号登录
'13900139000': {
id: 2,
phone: '13900139000',
nickname: 'Pro会员',
email: 'pro@example.com',
avatar_url: 'https://i.pravatar.cc/150?img=2',
has_wechat: true,
created_at: '2024-01-15T00:00:00Z',
// 会员信息 - Pro 会员
subscription_type: 'pro',
subscription_status: 'active',
subscription_end_date: '2025-12-31T23:59:59Z',
is_subscription_active: true,
subscription_days_left: 90
},
// Max 会员 - 手机号登录
'13700137000': {
id: 3,
phone: '13700137000',
nickname: 'Max会员',
email: 'max@example.com',
avatar_url: 'https://i.pravatar.cc/150?img=3',
has_wechat: false,
created_at: '2024-02-01T00:00:00Z',
// 会员信息 - Max 会员
subscription_type: 'max',
subscription_status: 'active',
subscription_end_date: '2026-12-31T23:59:59Z',
is_subscription_active: true,
subscription_days_left: 365
},
// 过期会员 - 测试过期状态
'13600136000': {
id: 4,
phone: '13600136000',
nickname: '过期会员',
email: 'expired@example.com',
avatar_url: 'https://i.pravatar.cc/150?img=4',
has_wechat: false,
created_at: '2023-01-01T00:00:00Z',
// 会员信息 - 已过期
subscription_type: 'pro',
subscription_status: 'expired',
subscription_end_date: '2024-01-01T00:00:00Z',
is_subscription_active: false,
subscription_days_left: -300
}
};
// Mock 验证码存储(实际项目中应该在后端验证)
export const mockVerificationCodes = new Map();
// 生成随机6位验证码
export function generateVerificationCode() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
// 微信 session 存储
export const mockWechatSessions = new Map();
// 生成微信 session ID
export function generateWechatSessionId() {
return 'wx_' + Math.random().toString(36).substring(2, 15);
}
// ==================== 当前登录用户状态管理 ====================
// Mock 模式下使用 localStorage 持久化登录状态
// 设置当前登录用户
export function setCurrentUser(user) {
if (user) {
// 数据兼容处理:确保用户数据包含订阅信息字段
const normalizedUser = {
...user,
// 如果缺少订阅信息,添加默认值
subscription_type: user.subscription_type || 'free',
subscription_status: user.subscription_status || 'active',
subscription_end_date: user.subscription_end_date || null,
is_subscription_active: user.is_subscription_active !== false,
subscription_days_left: user.subscription_days_left || 0
};
localStorage.setItem('mock_current_user', JSON.stringify(normalizedUser));
}
}
// 获取当前登录用户
export function getCurrentUser() {
try {
const stored = localStorage.getItem('mock_current_user');
if (stored) {
const user = JSON.parse(stored);
// console.log('[Mock State] 获取当前登录用户:', { // 已关闭:减少日志
// id: user.id,
// phone: user.phone,
// nickname: user.nickname,
// subscription_type: user.subscription_type,
// subscription_status: user.subscription_status,
// subscription_days_left: user.subscription_days_left
// });
return user;
}
} catch (error) {
console.error('[Mock State] 解析用户数据失败:', error);
}
// console.log('[Mock State] 未找到当前登录用户'); // 已关闭:减少日志
return null;
}
// 清除当前登录用户
export function clearCurrentUser() {
localStorage.removeItem('mock_current_user');
console.log('[Mock State] 清除当前登录用户');
}

View File

@@ -1,938 +0,0 @@
// src/mocks/handlers/account.js
import { http, HttpResponse, delay } from 'msw';
import { getCurrentUser } from '../data/users';
import {
mockWatchlist,
mockRealtimeQuotes,
mockFollowingEvents,
mockEventComments,
mockInvestmentPlans,
mockCalendarEvents,
mockSubscriptionCurrent,
getCalendarEventsByDateRange,
getFollowedEvents
} from '../data/account';
// 模拟网络延迟(毫秒)
const NETWORK_DELAY = 300;
export const accountHandlers = [
// ==================== 用户资料管理 ====================
// 1. 获取资料完整度
http.get('/api/account/profile-completeness', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '用户未登录'
}, { status: 401 });
}
console.log('[Mock] 获取资料完整度:', currentUser);
const isWechatUser = currentUser.has_wechat || !!currentUser.wechat_openid;
const completeness = {
hasPassword: !!currentUser.password_hash || !isWechatUser,
hasPhone: !!currentUser.phone,
hasEmail: !!currentUser.email && currentUser.email.includes('@') && !currentUser.email.endsWith('@valuefrontier.temp'),
isWechatUser: isWechatUser
};
const totalItems = 3;
const completedItems = [completeness.hasPassword, completeness.hasPhone, completeness.hasEmail].filter(Boolean).length;
const completenessPercentage = Math.round((completedItems / totalItems) * 100);
let needsAttention = false;
const missingItems = [];
if (isWechatUser && completenessPercentage < 100) {
needsAttention = true;
if (!completeness.hasPassword) missingItems.push('登录密码');
if (!completeness.hasPhone) missingItems.push('手机号');
if (!completeness.hasEmail) missingItems.push('邮箱');
}
const result = {
success: true,
data: {
completeness,
completenessPercentage,
needsAttention,
missingItems,
isComplete: completedItems === totalItems,
showReminder: needsAttention
}
};
console.log('[Mock] 资料完整度结果:', result.data);
return HttpResponse.json(result);
}),
// 2. 更新用户资料
http.put('/api/account/profile', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '用户未登录'
}, { status: 401 });
}
const body = await request.json();
console.log('[Mock] 更新用户资料:', body);
Object.assign(currentUser, body);
return HttpResponse.json({
success: true,
message: '资料更新成功',
data: currentUser
});
}),
// 3. 获取用户资料
http.get('/api/account/profile', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '用户未登录'
}, { status: 401 });
}
console.log('[Mock] 获取用户资料:', currentUser);
return HttpResponse.json({
success: true,
data: currentUser
});
}),
// ==================== 自选股管理 ====================
// 4. 获取自选股列表
http.get('/api/account/watchlist', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
// console.log('[Mock] 获取自选股列表'); // 已关闭:减少日志
return HttpResponse.json({
success: true,
data: mockWatchlist
});
}),
// 5. 获取自选股实时行情
http.get('/api/account/watchlist/realtime', async () => {
await delay(200);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
console.log('[Mock] 获取自选股实时行情');
return HttpResponse.json({
success: true,
data: mockRealtimeQuotes
});
}),
// 6. 添加自选股
http.post('/api/account/watchlist', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const body = await request.json();
const { stock_code, stock_name } = body;
console.log('[Mock] 添加自选股:', { stock_code, stock_name });
const newItem = {
id: mockWatchlist.length + 1,
user_id: currentUser.id,
stock_code,
stock_name,
added_at: new Date().toISOString(),
industry: '未知',
current_price: null,
change_percent: null
};
mockWatchlist.push(newItem);
// 同步添加到 mockRealtimeQuotes导航栏自选股菜单使用此数组
mockRealtimeQuotes.push({
stock_code: stock_code,
stock_name: stock_name,
current_price: null,
change_percent: 0,
change: 0,
volume: 0,
turnover: 0,
high: 0,
low: 0,
open: 0,
prev_close: 0,
update_time: new Date().toTimeString().slice(0, 8)
});
return HttpResponse.json({
success: true,
message: '添加成功',
data: newItem
});
}),
// 7. 删除自选股
http.delete('/api/account/watchlist/:id', async ({ params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const { id } = params;
console.log('[Mock] 删除自选股:', id);
// 支持按 stock_code 或 id 匹配删除
const index = mockWatchlist.findIndex(item =>
item.stock_code === id || item.id === parseInt(id)
);
if (index !== -1) {
const stockCode = mockWatchlist[index].stock_code;
mockWatchlist.splice(index, 1);
// 同步从 mockRealtimeQuotes 移除
const quotesIndex = mockRealtimeQuotes.findIndex(item => item.stock_code === stockCode);
if (quotesIndex !== -1) {
mockRealtimeQuotes.splice(quotesIndex, 1);
}
}
return HttpResponse.json({
success: true,
message: '删除成功'
});
}),
// ==================== 事件关注管理 ====================
// 8. 获取关注的事件(使用内存状态动态返回)
http.get('/api/account/events/following', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
// 从内存存储获取已关注的事件列表
const followedEvents = getFollowedEvents();
console.log('[Mock] 获取关注的事件, 数量:', followedEvents.length);
return HttpResponse.json({
success: true,
data: followedEvents
});
}),
// 9. 获取事件评论
http.get('/api/account/events/comments', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
console.log('[Mock] 获取事件评论');
return HttpResponse.json({
success: true,
data: mockEventComments
});
}),
// 10. 获取事件帖子(用户发布的评论/帖子)
http.get('/api/account/events/posts', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
console.log('[Mock] 获取事件帖子');
return HttpResponse.json({
success: true,
data: mockEventComments // 复用 mockEventComments 数据
});
}),
// ==================== 投资计划与复盘 ====================
// 10. 获取投资计划列表
http.get('/api/account/investment-plans', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
console.log('[Mock] 获取投资计划列表');
return HttpResponse.json({
success: true,
data: mockInvestmentPlans
});
}),
// 11. 创建投资计划
http.post('/api/account/investment-plans', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const body = await request.json();
console.log('[Mock] 创建投资计划:', body);
// 生成唯一 ID使用时间戳避免冲突
const newId = Date.now();
const newPlan = {
id: newId,
user_id: currentUser.id,
...body,
// 确保 target_date 字段存在(兼容前端发送的 date 字段)
target_date: body.target_date || body.date,
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
mockInvestmentPlans.push(newPlan);
console.log('[Mock] 新增计划/复盘,当前总数:', mockInvestmentPlans.length);
return HttpResponse.json({
success: true,
message: '创建成功',
data: newPlan
});
}),
// 12. 更新投资计划
http.put('/api/account/investment-plans/:id', async ({ request, params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const { id } = params;
const body = await request.json();
console.log('[Mock] 更新投资计划:', { id, body });
const index = mockInvestmentPlans.findIndex(plan => plan.id === parseInt(id));
if (index !== -1) {
mockInvestmentPlans[index] = {
...mockInvestmentPlans[index],
...body,
updated_at: new Date().toISOString()
};
return HttpResponse.json({
success: true,
message: '更新成功',
data: mockInvestmentPlans[index]
});
}
return HttpResponse.json({
success: false,
error: '计划不存在'
}, { status: 404 });
}),
// 13. 删除投资计划
http.delete('/api/account/investment-plans/:id', async ({ params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const { id } = params;
console.log('[Mock] 删除投资计划:', id);
const index = mockInvestmentPlans.findIndex(plan => plan.id === parseInt(id));
if (index !== -1) {
mockInvestmentPlans.splice(index, 1);
}
return HttpResponse.json({
success: true,
message: '删除成功'
});
}),
// ==================== 投资日历 ====================
// 14. 获取日历事件(可选日期范围)- 合并投资计划和日历事件
http.get('/api/account/calendar/events', async ({ request }) => {
await delay(NETWORK_DELAY);
// Mock 模式下允许无登录访问,使用默认用户 id: 1
const currentUser = getCurrentUser() || { id: 1 };
const url = new URL(request.url);
const startDate = url.searchParams.get('start_date');
const endDate = url.searchParams.get('end_date');
console.log('[Mock] 获取日历事件:', { startDate, endDate });
// 1. 获取日历事件
let calendarEvents = mockCalendarEvents;
if (startDate && endDate) {
calendarEvents = getCalendarEventsByDateRange(currentUser.id, startDate, endDate);
}
// 2. 获取投资计划和复盘,转换为日历事件格式
// Mock 模式:不过滤 user_id显示所有 mock 数据(方便开发测试)
const investmentPlansAsEvents = mockInvestmentPlans
.map(plan => ({
id: plan.id,
user_id: plan.user_id,
title: plan.title,
date: plan.target_date || plan.date,
event_date: plan.target_date || plan.date,
type: plan.type, // 'plan' or 'review'
category: plan.type === 'plan' ? 'investment_plan' : 'investment_review',
description: plan.content || '',
importance: 3, // 默认重要度
source: 'user', // 标记为用户创建
stocks: plan.stocks || [],
tags: plan.tags || [],
status: plan.status,
created_at: plan.created_at,
updated_at: plan.updated_at
}));
// 3. 合并两个数据源
const allEvents = [...calendarEvents, ...investmentPlansAsEvents];
// 4. 如果提供了日期范围,对合并后的数据进行过滤
let filteredEvents = allEvents;
if (startDate && endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
filteredEvents = allEvents.filter(event => {
const eventDate = new Date(event.date || event.event_date);
return eventDate >= start && eventDate <= end;
});
}
// 5. 按日期倒序排序(最新的在前面)
filteredEvents.sort((a, b) => {
const dateA = new Date(a.date || a.event_date);
const dateB = new Date(b.date || b.event_date);
return dateB - dateA; // 倒序:新日期在前
});
// 打印今天的事件(方便调试)
const today = new Date().toISOString().split('T')[0];
const todayEvents = filteredEvents.filter(e =>
(e.date === today || e.event_date === today)
);
console.log('[Mock] 日历事件详情:', {
currentUserId: currentUser.id,
calendarEvents: calendarEvents.length,
investmentPlansAsEvents: investmentPlansAsEvents.length,
total: filteredEvents.length,
plansCount: filteredEvents.filter(e => e.type === 'plan').length,
reviewsCount: filteredEvents.filter(e => e.type === 'review').length,
today,
todayEventsCount: todayEvents.length,
todayEventTitles: todayEvents.map(e => `[${e.type}] ${e.title}`)
});
return HttpResponse.json({
success: true,
data: filteredEvents
});
}),
// 15. 创建日历事件
http.post('/api/account/calendar/events', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const body = await request.json();
console.log('[Mock] 创建日历事件:', body);
const newEvent = {
id: mockCalendarEvents.length + 401,
user_id: currentUser.id,
...body,
source: 'user', // 用户创建的事件标记为 'user'
created_at: new Date().toISOString()
};
mockCalendarEvents.push(newEvent);
return HttpResponse.json({
success: true,
message: '创建成功',
data: newEvent
});
}),
// 16. 更新日历事件
http.put('/api/account/calendar/events/:id', async ({ request, params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const { id } = params;
const body = await request.json();
console.log('[Mock] 更新日历事件:', { id, body });
const index = mockCalendarEvents.findIndex(event => event.id === parseInt(id));
if (index !== -1) {
mockCalendarEvents[index] = {
...mockCalendarEvents[index],
...body
};
return HttpResponse.json({
success: true,
message: '更新成功',
data: mockCalendarEvents[index]
});
}
return HttpResponse.json({
success: false,
error: '事件不存在'
}, { status: 404 });
}),
// 17. 删除日历事件
http.delete('/api/account/calendar/events/:id', async ({ params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
const { id } = params;
console.log('[Mock] 删除日历事件:', id);
const index = mockCalendarEvents.findIndex(event => event.id === parseInt(id));
if (index !== -1) {
mockCalendarEvents.splice(index, 1);
}
return HttpResponse.json({
success: true,
message: '删除成功'
});
}),
// ==================== 订阅信息 ====================
// 18. 获取订阅信息
http.get('/api/subscription/info', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: true,
data: {
type: 'free',
status: 'active',
is_active: true,
days_left: 0,
end_date: null
}
});
}
console.log('[Mock] 获取订阅信息:', currentUser);
const subscriptionInfo = {
type: currentUser.subscription_type || 'free',
status: currentUser.subscription_status || 'active',
is_active: currentUser.is_subscription_active !== false,
days_left: currentUser.subscription_days_left || 0,
end_date: currentUser.subscription_end_date || null
};
console.log('[Mock] 订阅信息结果:', subscriptionInfo);
return HttpResponse.json({
success: true,
data: subscriptionInfo
});
}),
// 19. 获取当前订阅详情
http.get('/api/subscription/current', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
console.warn('[Mock API] 获取订阅详情失败: 用户未登录');
return HttpResponse.json(
{ success: false, error: '未登录' },
{ status: 401 }
);
}
// 基于当前用户的订阅类型返回详情
const userSubscriptionType = (currentUser.subscription_type || 'free').toLowerCase();
const subscriptionDetails = {
...mockSubscriptionCurrent,
type: userSubscriptionType,
status: currentUser.subscription_status || 'active',
is_active: currentUser.is_subscription_active !== false,
days_left: currentUser.subscription_days_left || 0,
end_date: currentUser.subscription_end_date || null
};
return HttpResponse.json({
success: true,
data: subscriptionDetails
});
}),
// 20. 获取订阅权限
http.get('/api/subscription/permissions', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: true,
data: {
permissions: {
'related_stocks': false,
'related_concepts': false,
'transmission_chain': false,
'historical_events': 'limited',
'concept_html_detail': false,
'concept_stats_panel': false,
'concept_related_stocks': false,
'concept_timeline': false,
'hot_stocks': false
}
}
});
}
const subscriptionType = (currentUser.subscription_type || 'free').toLowerCase();
let permissions = {};
if (subscriptionType === 'free') {
permissions = {
'related_stocks': false,
'related_concepts': false,
'transmission_chain': false,
'historical_events': 'limited',
'concept_html_detail': false,
'concept_stats_panel': false,
'concept_related_stocks': false,
'concept_timeline': false,
'hot_stocks': false
};
} else if (subscriptionType === 'pro') {
permissions = {
'related_stocks': true,
'related_concepts': true,
'transmission_chain': false,
'historical_events': 'full',
'concept_html_detail': true,
'concept_stats_panel': true,
'concept_related_stocks': true,
'concept_timeline': false,
'hot_stocks': true
};
} else if (subscriptionType === 'max') {
permissions = {
'related_stocks': true,
'related_concepts': true,
'transmission_chain': true,
'historical_events': 'full',
'concept_html_detail': true,
'concept_stats_panel': true,
'concept_related_stocks': true,
'concept_timeline': true,
'hot_stocks': true
};
}
console.log('[Mock] 订阅权限:', { subscriptionType, permissions });
return HttpResponse.json({
success: true,
data: {
subscription_type: subscriptionType,
permissions
}
});
}),
// ==================== 账号绑定管理 ====================
// 手机号发送验证码
http.post('/api/account/phone/send-code', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
console.log('[Mock] 发送手机验证码:', body.phone);
return HttpResponse.json({
success: true,
message: '验证码已发送'
});
}),
// 手机号绑定
http.post('/api/account/phone/bind', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
console.log('[Mock] 绑定手机号:', body.phone);
return HttpResponse.json({
success: true,
message: '手机号绑定成功',
data: { phone: body.phone, phone_confirmed: true }
});
}),
// 手机号解绑
http.post('/api/account/phone/unbind', async () => {
await delay(NETWORK_DELAY);
console.log('[Mock] 解绑手机号');
return HttpResponse.json({
success: true,
message: '手机号解绑成功'
});
}),
// 邮箱发送验证码
http.post('/api/account/email/send-bind-code', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
console.log('[Mock] 发送邮箱验证码:', body.email);
return HttpResponse.json({
success: true,
message: '验证码已发送'
});
}),
// 邮箱绑定
http.post('/api/account/email/bind', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
console.log('[Mock] 绑定邮箱:', body.email);
return HttpResponse.json({
success: true,
message: '邮箱绑定成功',
user: { email: body.email, email_confirmed: true }
});
}),
// 微信获取二维码
http.get('/api/account/wechat/qrcode', async () => {
await delay(NETWORK_DELAY);
console.log('[Mock] 获取微信绑定二维码');
return HttpResponse.json({
success: true,
auth_url: 'https://open.weixin.qq.com/connect/qrconnect?mock=true',
session_id: 'mock_session_' + Date.now()
});
}),
// 微信绑定检查
http.post('/api/account/wechat/check', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
console.log('[Mock] 检查微信绑定状态:', body.session_id);
// 模拟绑定成功
return HttpResponse.json({
success: true,
status: 'bind_ready'
});
}),
// 微信解绑
http.post('/api/account/wechat/unbind', async () => {
await delay(NETWORK_DELAY);
console.log('[Mock] 解绑微信');
return HttpResponse.json({
success: true,
message: '微信解绑成功'
});
}),
// 21. 获取订阅套餐列表
http.get('/api/subscription/plans', async () => {
await delay(NETWORK_DELAY);
const plans = [
{
id: 1,
name: 'pro',
display_name: 'Pro 专业版',
description: '事件关联股票深度分析 | 历史事件智能对比复盘 | 事件概念关联与挖掘 | 概念板块个股追踪 | 概念深度研报与解读 | 个股异动实时预警',
monthly_price: 299,
yearly_price: 2699,
pricing_options: [
{ cycle_key: 'monthly', label: '月付', months: 1, price: 299, original_price: null, discount_percent: 0 },
{ cycle_key: 'quarterly', label: '季付', months: 3, price: 799, original_price: 897, discount_percent: 11 },
{ cycle_key: 'semiannual', label: '半年付', months: 6, price: 1499, original_price: 1794, discount_percent: 16 },
{ cycle_key: 'yearly', label: '年付', months: 12, price: 2699, original_price: 3588, discount_percent: 25 }
],
features: [
'新闻信息流',
'历史事件对比',
'事件传导链分析(AI)',
'事件-相关标的分析',
'相关概念展示',
'AI复盘功能',
'企业概览',
'个股深度分析(AI) - 50家/月',
'高效数据筛选工具',
'概念中心(548大概念)',
'历史时间轴查询 - 100天',
'涨停板块数据分析',
'个股涨停分析'
],
sort_order: 1
},
{
id: 2,
name: 'max',
display_name: 'Max 旗舰版',
description: '包含Pro版全部功能 | 事件传导链路智能分析 | 概念演变时间轴追溯 | 个股全方位深度研究 | 价小前投研助手无限使用 | 新功能优先体验权 | 专属客服一对一服务',
monthly_price: 599,
yearly_price: 5399,
pricing_options: [
{ cycle_key: 'monthly', label: '月付', months: 1, price: 599, original_price: null, discount_percent: 0 },
{ cycle_key: 'quarterly', label: '季付', months: 3, price: 1599, original_price: 1797, discount_percent: 11 },
{ cycle_key: 'semiannual', label: '半年付', months: 6, price: 2999, original_price: 3594, discount_percent: 17 },
{ cycle_key: 'yearly', label: '年付', months: 12, price: 5399, original_price: 7188, discount_percent: 25 }
],
features: [
'新闻信息流',
'历史事件对比',
'事件传导链分析(AI)',
'事件-相关标的分析',
'相关概念展示',
'板块深度分析(AI)',
'AI复盘功能',
'企业概览',
'个股深度分析(AI) - 无限制',
'高效数据筛选工具',
'概念中心(548大概念)',
'历史时间轴查询 - 无限制',
'概念高频更新',
'涨停板块数据分析',
'个股涨停分析'
],
sort_order: 2
}
];
console.log('[Mock] 获取订阅套餐列表:', plans.length, '个套餐');
return HttpResponse.json({
success: true,
data: plans
});
}),
];

View File

@@ -1,606 +0,0 @@
// src/mocks/handlers/agent.js
// Agent Chat API Mock Handlers
import { http, HttpResponse, delay } from 'msw';
// 模拟会话数据
const mockSessions = [
{
session_id: 'session-001',
title: '贵州茅台投资分析',
created_at: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
message_count: 15,
},
{
session_id: 'session-002',
title: '新能源板块研究',
created_at: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
timestamp: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000).toISOString(),
message_count: 8,
},
{
session_id: 'session-003',
title: '半导体行业分析',
created_at: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
timestamp: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
message_count: 12,
},
];
// 模拟历史消息数据
const mockHistory = {
'session-001': [
{
message_type: 'user',
message: '分析一下贵州茅台的投资价值',
timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
plan: null,
steps: null,
},
{
message_type: 'assistant',
message:
'# 贵州茅台投资价值分析\n\n根据最新数据贵州茅台600519.SH具有以下投资亮点\n\n## 基本面分析\n- **营收增长**2024年Q3营收同比增长12.5%\n- **净利润率**保持在50%以上的高水平\n- **ROE**连续10年超过20%\n\n## 估值分析\n- **PETTM**35.6倍,略高于历史中位数\n- **PB**10.2倍,处于合理区间\n\n## 投资建议\n建议关注价格回调机会长期配置价值显著。',
timestamp: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000 + 5000).toISOString(),
plan: JSON.stringify({
goal: '分析贵州茅台的投资价值',
steps: [
{ step: 1, action: '获取贵州茅台最新股价和财务数据', reasoning: '需要基础数据支持分析' },
{ step: 2, action: '分析公司基本面和盈利能力', reasoning: '评估公司质量' },
{ step: 3, action: '对比行业估值水平', reasoning: '判断估值合理性' },
{ step: 4, action: '给出投资建议', reasoning: '综合判断投资价值' },
],
}),
steps: JSON.stringify([
{
tool: 'get_stock_info',
status: 'success',
result: '获取到贵州茅台最新数据股价1850元市值2.3万亿',
},
{
tool: 'analyze_financials',
status: 'success',
result: '财务分析完成:营收增长稳健,利润率行业领先',
},
]),
},
],
};
// 生成模拟的 Agent 响应
function generateAgentResponse(message, sessionId) {
const responses = {
包含: {
贵州茅台: {
plan: {
goal: '分析贵州茅台相关信息',
steps: [
{ step: 1, action: '搜索贵州茅台最新新闻', reasoning: '了解最新动态' },
{ step: 2, action: '获取股票实时行情', reasoning: '查看当前价格走势' },
{ step: 3, action: '分析财务数据', reasoning: '评估基本面' },
{ step: 4, action: '生成投资建议', reasoning: '综合判断' },
],
},
step_results: [
{
tool: 'search_news',
status: 'success',
result: '找到5条相关新闻茅台Q3业绩超预期...',
},
{
tool: 'get_stock_quote',
status: 'success',
result: '当前价格1850元涨幅+2.3%',
},
{
tool: 'analyze_financials',
status: 'success',
result: 'ROE: 25.6%, 净利润率: 52.3%',
},
],
final_summary:
'# 贵州茅台分析报告\n\n## 最新动态\n茅台Q3业绩超预期营收增长稳健。\n\n## 行情分析\n当前价格1850元今日上涨2.3%,成交量活跃。\n\n## 财务表现\n- ROE: 25.6%(行业领先)\n- 净利润率: 52.3%(极高水平)\n- 营收增长: 12.5% YoY\n\n## 投资建议\n**推荐关注**:基本面优秀,估值合理,建议逢低布局。',
},
新能源: {
plan: {
goal: '分析新能源行业',
steps: [
{ step: 1, action: '搜索新能源行业新闻', reasoning: '了解行业动态' },
{ step: 2, action: '获取新能源概念股', reasoning: '找到相关标的' },
{ step: 3, action: '分析行业趋势', reasoning: '判断投资机会' },
],
},
step_results: [
{
tool: 'search_news',
status: 'success',
result: '新能源政策利好频出,行业景气度提升',
},
{
tool: 'get_concept_stocks',
status: 'success',
result: '新能源板块共182只个股今日平均涨幅3.2%',
},
],
final_summary:
'# 新能源行业分析\n\n## 行业动态\n政策利好频出行业景气度持续提升。\n\n## 板块表现\n新能源板块今日强势上涨平均涨幅3.2%。\n\n## 投资机会\n建议关注龙头企业和细分赛道领导者。',
},
},
默认: {
plan: {
goal: '回答用户问题',
steps: [
{ step: 1, action: '理解用户意图', reasoning: '准确把握需求' },
{ step: 2, action: '搜索相关信息', reasoning: '获取数据支持' },
{ step: 3, action: '生成回复', reasoning: '提供专业建议' },
],
},
step_results: [
{
tool: 'search_related_info',
status: 'success',
result: '已找到相关信息',
},
],
final_summary: `我已经收到您的问题:"${message}"\n\n作为您的 AI 投研助手,我可以帮您:\n- 📊 分析股票基本面和技术面\n- 🔥 追踪市场热点和板块动态\n- 📈 研究行业趋势和投资机会\n- 📰 汇总最新财经新闻和研报\n\n请告诉我您想了解哪方面的信息?`,
},
};
// 根据关键词匹配响应
for (const keyword in responses.包含) {
if (message.includes(keyword)) {
return responses.包含[keyword];
}
}
return responses.默认;
}
// Agent Chat API Handlers
export const agentHandlers = [
// POST /mcp/agent/chat - 发送消息
http.post('/mcp/agent/chat', async ({ request }) => {
await delay(800); // 模拟网络延迟
const body = await request.json();
const { message, session_id, user_id, subscription_type } = body;
// 模拟权限检查(仅 max 用户可用)
if (subscription_type !== 'max') {
return HttpResponse.json(
{
success: false,
error: '很抱歉,「价小前投研」功能仅对 Max 订阅用户开放。请升级您的订阅以使用此功能。',
},
{ status: 403 }
);
}
// 生成或使用现有 session_id
const responseSessionId = session_id || `session-${Date.now()}`;
// 根据消息内容生成响应
const response = generateAgentResponse(message, responseSessionId);
return HttpResponse.json({
success: true,
message: '处理成功',
session_id: responseSessionId,
plan: response.plan,
steps: response.step_results,
final_answer: response.final_summary,
metadata: {
model: body.model || 'kimi-k2-thinking',
timestamp: new Date().toISOString(),
},
});
}),
// GET /mcp/agent/sessions - 获取会话列表
http.get('/mcp/agent/sessions', async ({ request }) => {
await delay(300);
const url = new URL(request.url);
const userId = url.searchParams.get('user_id');
const limit = parseInt(url.searchParams.get('limit') || '50');
// 返回模拟的会话列表
const sessions = mockSessions.slice(0, limit);
return HttpResponse.json({
success: true,
data: sessions,
count: sessions.length,
});
}),
// GET /mcp/agent/history/:session_id - 获取会话历史
http.get('/mcp/agent/history/:session_id', async ({ params }) => {
await delay(400);
const { session_id } = params;
// 返回模拟的历史消息
const history = mockHistory[session_id] || [];
return HttpResponse.json({
success: true,
data: history,
count: history.length,
});
}),
// ==================== 投研会议室 API Handlers ====================
// GET /mcp/agent/meeting/roles - 获取会议角色配置
http.get('/mcp/agent/meeting/roles', async () => {
await delay(200);
return HttpResponse.json({
success: true,
roles: [
{
id: 'buffett',
name: '巴菲特',
nickname: '唱多者',
role_type: 'bull',
avatar: '/avatars/buffett.png',
color: '#10B981',
description: '主观多头,善于分析事件的潜在利好和长期价值',
},
{
id: 'big_short',
name: '大空头',
nickname: '大空头',
role_type: 'bear',
avatar: '/avatars/big_short.png',
color: '#EF4444',
description: '善于分析事件和财报中的风险因素,帮助投资者避雷',
},
{
id: 'simons',
name: '量化分析员',
nickname: '西蒙斯',
role_type: 'quant',
avatar: '/avatars/simons.png',
color: '#3B82F6',
description: '中性立场,使用量化分析工具分析技术指标',
},
{
id: 'leek',
name: '韭菜',
nickname: '牢大',
role_type: 'retail',
avatar: '/avatars/leek.png',
color: '#F59E0B',
description: '贪婪又讨厌亏损,热爱追涨杀跌的典型散户',
},
{
id: 'fund_manager',
name: '基金经理',
nickname: '决策者',
role_type: 'manager',
avatar: '/avatars/fund_manager.png',
color: '#8B5CF6',
description: '总结其他人的发言做出最终决策',
},
],
});
}),
// POST /mcp/agent/meeting/start - 启动投研会议
http.post('/mcp/agent/meeting/start', async ({ request }) => {
await delay(2000); // 模拟多角色讨论耗时
const body = await request.json();
const { topic, user_id } = body;
const sessionId = `meeting-${Date.now()}`;
const timestamp = new Date().toISOString();
// 生成模拟的多角色讨论消息
const messages = [
{
role_id: 'buffett',
role_name: '巴菲特',
nickname: '唱多者',
avatar: '/avatars/buffett.png',
color: '#10B981',
content: `关于「${topic}」,我认为这里存在显著的投资机会。从价值投资的角度看,我们应该关注以下几点:\n\n1. **长期价值**:该标的具有较强的护城河\n2. **盈利能力**ROE持续保持在较高水平\n3. **管理层质量**:管理团队稳定且执行力强\n\n我的观点是**看多**,建议逢低布局。`,
timestamp,
round_number: 1,
},
{
role_id: 'big_short',
role_name: '大空头',
nickname: '大空头',
avatar: '/avatars/big_short.png',
color: '#EF4444',
content: `等等,让我泼点冷水。关于「${topic}」,市场似乎过于乐观了:\n\n⚠️ **风险提示**\n1. 当前估值处于历史高位,安全边际不足\n2. 行业竞争加剧,利润率面临压力\n3. 宏观环境不确定性增加\n\n建议投资者**保持谨慎**,不要追高。`,
timestamp: new Date(Date.now() + 1000).toISOString(),
round_number: 1,
},
{
role_id: 'simons',
role_name: '量化分析员',
nickname: '西蒙斯',
avatar: '/avatars/simons.png',
color: '#3B82F6',
content: `从量化角度分析「${topic}」:\n\n📊 **技术指标**\n- MACD金叉形态动能向上\n- RSI58处于中性区域\n- 均线5日>10日>20日多头排列\n\n📈 **资金面**\n- 主力资金近5日净流入2.3亿\n- 北向资金:持续加仓\n\n**结论**短期技术面偏多但需关注60日均线支撑。`,
timestamp: new Date(Date.now() + 2000).toISOString(),
round_number: 1,
},
{
role_id: 'leek',
role_name: '韭菜',
nickname: '牢大',
avatar: '/avatars/leek.png',
color: '#F59E0B',
content: `哇!「${topic}」看起来要涨啊!\n\n🚀 我觉得必须满仓干!隔壁老王都赚翻了!\n\n不过话说回来...万一跌了怎么办?会不会套住?\n\n算了不管了,先冲一把再说!错过这村就没这店了!\n\n内心OS希望别当接盘侠...`,
timestamp: new Date(Date.now() + 3000).toISOString(),
round_number: 1,
},
{
role_id: 'fund_manager',
role_name: '基金经理',
nickname: '决策者',
avatar: '/avatars/fund_manager.png',
color: '#8B5CF6',
content: `## 投资建议总结\n\n综合各方观点,对于「${topic}」,我的判断如下:\n\n### 综合评估\n多空双方都提出了有价值的观点。技术面短期偏多,但估值确实需要关注。\n\n### 关键观点\n- ✅ 基本面优质,长期价值明确\n- ⚠️ 短期估值偏高,需要耐心等待\n- 📊 技术面处于上升趋势\n\n### 风险提示\n注意仓位控制,避免追高\n\n### 操作建议\n**观望为主**,等待回调至支撑位再考虑建仓\n\n### 信心指数7/10`,
timestamp: new Date(Date.now() + 4000).toISOString(),
round_number: 1,
is_conclusion: true,
},
];
return HttpResponse.json({
success: true,
session_id: sessionId,
messages,
round_number: 1,
is_concluded: true,
conclusion: messages[messages.length - 1],
});
}),
// POST /mcp/agent/meeting/continue - 继续会议讨论
http.post('/mcp/agent/meeting/continue', async ({ request }) => {
await delay(1500);
const body = await request.json();
const { topic, user_message, conversation_history } = body;
const roundNumber = Math.floor(conversation_history.length / 5) + 2;
const timestamp = new Date().toISOString();
const messages = [];
// 如果用户有插话,添加用户消息
if (user_message) {
messages.push({
role_id: 'user',
role_name: '用户',
nickname: '你',
avatar: '',
color: '#6366F1',
content: user_message,
timestamp,
round_number: roundNumber,
});
}
// 生成新一轮讨论
messages.push(
{
role_id: 'buffett',
role_name: '巴菲特',
nickname: '唱多者',
avatar: '/avatars/buffett.png',
color: '#10B981',
content: `感谢用户的补充。${user_message ? `关于"${user_message}"` : ''}我依然坚持看多的观点。从更长远的角度看,短期波动不影响长期价值。`,
timestamp: new Date(Date.now() + 1000).toISOString(),
round_number: roundNumber,
},
{
role_id: 'big_short',
role_name: '大空头',
nickname: '大空头',
avatar: '/avatars/big_short.png',
color: '#EF4444',
content: `用户提出了很好的问题。我要再次强调风险控制的重要性。当前市场情绪过热,建议保持警惕。`,
timestamp: new Date(Date.now() + 2000).toISOString(),
round_number: roundNumber,
},
{
role_id: 'fund_manager',
role_name: '基金经理',
nickname: '决策者',
avatar: '/avatars/fund_manager.png',
color: '#8B5CF6',
content: `## 第${roundNumber}轮讨论总结\n\n经过进一步讨论,我维持之前的判断:\n\n- 短期观望为主\n- 中长期可以考虑分批建仓\n- 严格控制仓位,设好止损\n\n**信心指数7.5/10**\n\n会议到此结束,感谢各位的参与!`,
timestamp: new Date(Date.now() + 3000).toISOString(),
round_number: roundNumber,
is_conclusion: true,
}
);
return HttpResponse.json({
success: true,
session_id: body.session_id,
messages,
round_number: roundNumber,
is_concluded: true,
conclusion: messages[messages.length - 1],
});
}),
// POST /mcp/agent/meeting/stream - 流式会议接口V2
http.post('/mcp/agent/meeting/stream', async ({ request }) => {
const body = await request.json();
const { topic, user_id } = body;
const sessionId = `meeting-${Date.now()}`;
// 定义会议角色和他们的消息
const roleMessages = [
{
role_id: 'buffett',
role_name: '巴菲特',
content: `关于「${topic}」,我认为这里存在显著的投资机会。从价值投资的角度看,我们应该关注以下几点:\n\n1. **长期价值**:该标的具有较强的护城河\n2. **盈利能力**ROE持续保持在较高水平\n3. **管理层质量**:管理团队稳定且执行力强\n\n我的观点是**看多**,建议逢低布局。`,
tools: [
{ name: 'search_china_news', result: { articles: [{ title: '相关新闻1' }, { title: '相关新闻2' }] } },
{ name: 'get_stock_basic_info', result: { pe: 25.6, pb: 3.2, roe: 18.5 } },
],
},
{
role_id: 'big_short',
role_name: '大空头',
content: `等等,让我泼点冷水。关于「${topic}」,市场似乎过于乐观了:\n\n⚠️ **风险提示**\n1. 当前估值处于历史高位,安全边际不足\n2. 行业竞争加剧,利润率面临压力\n3. 宏观环境不确定性增加\n\n建议投资者**保持谨慎**,不要追高。`,
tools: [
{ name: 'get_stock_financial_index', result: { debt_ratio: 45.2, current_ratio: 1.8 } },
],
},
{
role_id: 'simons',
role_name: '量化分析员',
content: `从量化角度分析「${topic}」:\n\n📊 **技术指标**\n- MACD金叉形态动能向上\n- RSI58处于中性区域\n- 均线5日>10日>20日多头排列\n\n📈 **资金面**\n- 主力资金近5日净流入2.3亿\n- 北向资金:持续加仓\n\n**结论**短期技术面偏多但需关注60日均线支撑。`,
tools: [
{ name: 'get_stock_trade_data', result: { volume: 1234567, turnover: 5.2 } },
{ name: 'get_concept_statistics', result: { concepts: ['AI概念', '半导体'], avg_change: 2.3 } },
],
},
{
role_id: 'leek',
role_name: '韭菜',
content: `哇!「${topic}」看起来要涨啊!\n\n🚀 我觉得必须满仓干!隔壁老王都赚翻了!\n\n不过话说回来...万一跌了怎么办?会不会套住?\n\n算了不管了,先冲一把再说!错过这村就没这店了!\n\n内心OS希望别当接盘侠...`,
tools: [], // 韭菜不用工具
},
{
role_id: 'fund_manager',
role_name: '基金经理',
content: `## 投资建议总结\n\n综合各方观点,对于「${topic}」,我的判断如下:\n\n### 综合评估\n多空双方都提出了有价值的观点。技术面短期偏多,但估值确实需要关注。\n\n### 关键观点\n- ✅ 基本面优质,长期价值明确\n- ⚠️ 短期估值偏高,需要耐心等待\n- 📊 技术面处于上升趋势\n\n### 风险提示\n注意仓位控制,避免追高\n\n### 操作建议\n**观望为主**,等待回调至支撑位再考虑建仓\n\n### 信心指数7/10`,
tools: [
{ name: 'search_research_reports', result: { reports: [{ title: '深度研报1' }] } },
],
is_conclusion: true,
},
];
// 创建 SSE 流
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
// 发送 session_start
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
type: 'session_start',
session_id: sessionId,
})}\n\n`));
await delay(300);
// 发送 order_decided
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
type: 'order_decided',
order: roleMessages.map(r => r.role_id),
})}\n\n`));
await delay(300);
// 依次发送每个角色的消息
for (const role of roleMessages) {
// speaking_start
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
type: 'speaking_start',
role_id: role.role_id,
role_name: role.role_name,
})}\n\n`));
await delay(200);
// 发送工具调用
const toolCallResults = [];
for (const tool of role.tools) {
const toolCallId = `tc-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const execTime = 0.5 + Math.random() * 0.5;
// tool_call_start
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
type: 'tool_call_start',
role_id: role.role_id,
tool_call_id: toolCallId,
tool_name: tool.name,
arguments: {},
})}\n\n`));
await delay(500);
// tool_call_result
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
type: 'tool_call_result',
role_id: role.role_id,
tool_call_id: toolCallId,
tool_name: tool.name,
result: { success: true, data: tool.result },
status: 'success',
execution_time: execTime,
})}\n\n`));
toolCallResults.push({
tool_call_id: toolCallId,
tool_name: tool.name,
result: { success: true, data: tool.result },
status: 'success',
execution_time: execTime,
});
await delay(200);
}
// 流式发送内容
const chunks = role.content.match(/.{1,20}/g) || [];
for (const chunk of chunks) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
type: 'content_delta',
role_id: role.role_id,
content: chunk,
})}\n\n`));
await delay(30);
}
// message_complete
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
type: 'message_complete',
role_id: role.role_id,
message: {
role_id: role.role_id,
role_name: role.role_name,
content: role.content,
tool_calls: toolCallResults,
is_conclusion: role.is_conclusion || false,
},
})}\n\n`));
await delay(500);
}
// round_end
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
type: 'round_end',
round_number: 1,
is_concluded: false,
})}\n\n`));
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}),
];

View File

@@ -1,408 +0,0 @@
// src/mocks/handlers/auth.js
import { http, HttpResponse, delay } from 'msw';
import {
mockUsers,
mockVerificationCodes,
generateVerificationCode,
mockWechatSessions,
generateWechatSessionId,
setCurrentUser,
getCurrentUser,
clearCurrentUser
} from '../data/users';
// 模拟网络延迟(毫秒)
// ⚡ 开发环境使用较短延迟,加快首屏加载速度
const NETWORK_DELAY = 50;
export const authHandlers = [
// ==================== 手机验证码登录 ====================
// 1. 发送验证码
http.post('/api/auth/send-verification-code', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
const { credential, type, purpose } = body;
// 生成验证码
const code = generateVerificationCode();
mockVerificationCodes.set(credential, {
code,
expiresAt: Date.now() + 5 * 60 * 1000 // 5分钟后过期
});
return HttpResponse.json({
success: true,
message: `验证码已发送到 ${credential}Mock: ${code}`,
// 开发环境下返回验证码,方便测试
dev_code: code
});
}),
// 1.1 发送手机验证码(前端实际调用的接口)
http.post('/api/auth/send-sms-code', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
const { phone } = body;
console.log('[Mock] 发送手机验证码请求:', { phone });
// 生成验证码
const code = generateVerificationCode();
mockVerificationCodes.set(phone, {
code,
expiresAt: Date.now() + 5 * 60 * 1000 // 5分钟后过期
});
// 超醒目的验证码提示 - 方便开发调试
console.log(
`%c\n` +
`╔════════════════════════════════════════════╗\n` +
`║ 📱 手机验证码: ${code.padEnd(19)}\n` +
`║ 📞 手机号: ${phone.padEnd(23)}\n` +
`╚════════════════════════════════════════════╝\n`,
'color: #ffffff; background: #16a34a; font-weight: bold; font-size: 16px; padding: 20px; line-height: 1.8;'
);
// 额外的高亮提示
console.log(
`%c 📱 验证码: ${code} `,
'color: #ffffff; background: #dc2626; font-weight: bold; font-size: 24px; padding: 15px 30px; border-radius: 8px; margin: 10px 0;'
);
return HttpResponse.json({
success: true,
message: `验证码已发送到 ${phone}Mock: ${code}`,
// 开发环境下返回验证码,方便测试
dev_code: code
});
}),
// 1.2 发送邮箱验证码(前端实际调用的接口)
http.post('/api/auth/send-email-code', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
const { email } = body;
console.log('[Mock] 发送邮箱验证码请求:', { email });
// 生成验证码
const code = generateVerificationCode();
mockVerificationCodes.set(email, {
code,
expiresAt: Date.now() + 5 * 60 * 1000 // 5分钟后过期
});
// 超醒目的验证码提示 - 方便开发调试
console.log(
`%c\n` +
`╔════════════════════════════════════════════╗\n` +
`║ 📧 邮箱验证码: ${code.padEnd(19)}\n` +
`║ 📮 邮箱: ${email.padEnd(27)}\n` +
`╚════════════════════════════════════════════╝\n`,
'color: #ffffff; background: #2563eb; font-weight: bold; font-size: 16px; padding: 20px; line-height: 1.8;'
);
// 额外的高亮提示
console.log(
`%c 📧 验证码: ${code} `,
'color: #ffffff; background: #dc2626; font-weight: bold; font-size: 24px; padding: 15px 30px; border-radius: 8px; margin: 10px 0;'
);
return HttpResponse.json({
success: true,
message: `验证码已发送到 ${email}Mock: ${code}`,
// 开发环境下返回验证码,方便测试
dev_code: code
});
}),
// 2. 验证码登录
http.post('/api/auth/login-with-code', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
const { credential, verification_code, login_type } = body;
// 验证验证码
const storedCode = mockVerificationCodes.get(credential);
if (!storedCode) {
return HttpResponse.json({
success: false,
error: '验证码不存在或已过期'
}, { status: 400 });
}
if (storedCode.expiresAt < Date.now()) {
mockVerificationCodes.delete(credential);
return HttpResponse.json({
success: false,
error: '验证码已过期'
}, { status: 400 });
}
if (storedCode.code !== verification_code) {
return HttpResponse.json({
success: false,
error: '验证码错误'
}, { status: 400 });
}
// 验证成功,删除验证码
mockVerificationCodes.delete(credential);
// 查找或创建用户
let user = mockUsers[credential];
let isNewUser = false;
if (!user) {
// 新用户
isNewUser = true;
const id = Object.keys(mockUsers).length + 1;
user = {
id,
phone: credential,
nickname: `用户${id}`,
email: null,
avatar_url: `https://i.pravatar.cc/150?img=${id}`,
has_wechat: false,
created_at: new Date().toISOString(),
// 默认订阅信息 - 免费用户
subscription_type: 'free',
subscription_status: 'active',
subscription_end_date: null,
is_subscription_active: true,
subscription_days_left: 0
};
mockUsers[credential] = user;
}
// 设置当前登录用户
setCurrentUser(user);
return HttpResponse.json({
success: true,
message: isNewUser ? '注册成功' : '登录成功',
isNewUser,
user,
token: `mock_token_${user.id}_${Date.now()}`
});
}),
// ==================== 微信登录 ====================
// 3. 获取微信 PC 二维码
http.get('/api/auth/wechat/qrcode', async () => {
await delay(NETWORK_DELAY);
const sessionId = generateWechatSessionId();
// 创建微信 session
mockWechatSessions.set(sessionId, {
status: 'waiting', // waiting, scanned, confirmed, expired
createdAt: Date.now(),
user: null
});
// 模拟微信授权 URL实际是微信的 URL
// 使用真实的微信 AppID 和真实的授权回调地址(必须与微信开放平台配置的域名一致)
const mockRedirectUri = encodeURIComponent('http://valuefrontier.cn/api/auth/wechat/callback');
const authUrl = `https://open.weixin.qq.com/connect/qrconnect?appid=wxa8d74c47041b5f87&redirect_uri=${mockRedirectUri}&response_type=code&scope=snsapi_login&state=${sessionId}#wechat_redirect`;
console.log('[Mock] 生成微信二维码:', { sessionId, authUrl });
// 3秒后自动模拟扫码方便测试已缩短延迟
setTimeout(() => {
const session = mockWechatSessions.get(sessionId);
if (session && session.status === 'waiting') {
session.status = 'scanned';
console.log(`[Mock] 模拟用户扫码: ${sessionId}`);
// 再过5秒自动确认登录延长时间让用户看到 scanned 状态)
setTimeout(() => {
const session2 = mockWechatSessions.get(sessionId);
if (session2 && session2.status === 'scanned') {
session2.status = 'authorized'; // ✅ 使用 'authorized' 状态,与后端保持一致
session2.user = {
id: 999,
nickname: '微信用户',
wechat_openid: 'mock_openid_' + sessionId,
avatar_url: 'https://ui-avatars.com/api/?name=微信用户&size=150&background=4299e1&color=fff',
phone: null,
email: null,
has_wechat: true,
created_at: new Date().toISOString(),
// 添加默认订阅信息
subscription_type: 'free',
subscription_status: 'active',
subscription_end_date: null,
is_subscription_active: true,
subscription_days_left: 0
};
session2.user_info = { user_id: session2.user.id }; // ✅ 添加 user_info 字段
console.log(`[Mock] 模拟用户确认登录: ${sessionId}`, session2.user);
}
}, 2000);
}
}, 3000);
return HttpResponse.json({
code: 0,
message: '成功',
data: {
auth_url: authUrl,
session_id: sessionId
}
});
}),
// 4. 检查微信扫码状态
http.post('/api/auth/wechat/check', async ({ request }) => {
await delay(200); // 轮询请求,延迟短一些
const body = await request.json();
const { session_id } = body;
const session = mockWechatSessions.get(session_id);
if (!session) {
return HttpResponse.json({
code: 404,
message: 'Session 不存在',
data: { status: 'expired' }
});
}
// 检查是否过期5分钟
if (Date.now() - session.createdAt > 5 * 60 * 1000) {
session.status = 'expired';
mockWechatSessions.delete(session_id);
}
console.log('[Mock] 检查微信状态:', { session_id, status: session.status });
// ✅ 返回与后端真实 API 一致的扁平化数据结构
return HttpResponse.json({
status: session.status,
user_info: session.user_info,
expires_in: Math.floor((session.createdAt + 5 * 60 * 1000 - Date.now()) / 1000)
});
}),
// 5. 微信登录确认
http.post('/api/auth/login/wechat', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
const { session_id } = body;
const session = mockWechatSessions.get(session_id);
if (!session || session.status !== 'authorized') { // ✅ 使用 'authorized' 状态,与前端保持一致
return HttpResponse.json({
success: false,
error: '微信登录未确认或已过期'
}, { status: 400 });
}
const user = session.user;
// 清理 session
mockWechatSessions.delete(session_id);
console.log('[Mock] 微信登录成功:', user);
// 设置当前登录用户
setCurrentUser(user);
return HttpResponse.json({
success: true,
message: '微信登录成功',
user,
token: `mock_wechat_token_${user.id}_${Date.now()}`
});
}),
// 6. 获取微信 H5 授权 URL手机浏览器用
http.post('/api/auth/wechat/h5-auth', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
const { redirect_url } = body;
const state = generateWechatSessionId();
// Mock 模式下直接返回前端回调地址(模拟授权成功)
const authUrl = `${redirect_url}?wechat_login=success&state=${state}`;
console.log('[Mock] 生成微信 H5 授权 URL:', authUrl);
return HttpResponse.json({
auth_url: authUrl,
state
});
}),
// ==================== Session 管理 ====================
// 7. 检查 SessionAuthContext 使用的正确端点)
http.get('/api/auth/session', async () => {
await delay(NETWORK_DELAY); // ⚡ 使用统一延迟配置
// 获取当前登录用户
const currentUser = getCurrentUser();
if (currentUser) {
return HttpResponse.json({
success: true,
isAuthenticated: true,
user: currentUser
});
} else {
return HttpResponse.json({
success: true,
isAuthenticated: false,
user: null
});
}
}),
// 8. 检查 Session旧端点保留兼容
http.get('/api/auth/check-session', async () => {
await delay(NETWORK_DELAY); // ⚡ 使用统一延迟配置
// 获取当前登录用户
const currentUser = getCurrentUser();
if (currentUser) {
return HttpResponse.json({
success: true,
isAuthenticated: true,
user: currentUser
});
} else {
return HttpResponse.json({
success: true,
isAuthenticated: false,
user: null
});
}
}),
// 9. 退出登录
http.post('/api/auth/logout', async () => {
await delay(NETWORK_DELAY);
console.log('[Mock] 退出登录');
// 清除当前登录用户
clearCurrentUser();
return HttpResponse.json({
success: true,
message: '退出成功'
});
})
];

View File

@@ -1,34 +0,0 @@
// src/mocks/handlers/bytedesk.js
/**
* Bytedesk 客服 Widget MSW Handler
* Mock 模式下返回模拟数据
*/
import { http, HttpResponse, passthrough } from 'msw';
export const bytedeskHandlers = [
// 未读消息数量
http.get('/bytedesk/visitor/api/v1/message/unread/count', () => {
return HttpResponse.json({
code: 200,
message: 'success',
data: { count: 0 },
});
}),
// 其他 Bytedesk API - 返回通用成功响应
http.all('/bytedesk/*', () => {
return HttpResponse.json({
code: 200,
message: 'success',
data: null,
});
}),
// Bytedesk 外部 CDN/服务请求
http.all('https://www.weiyuai.cn/*', () => {
return passthrough();
}),
];
export default bytedeskHandlers;

View File

@@ -1,281 +0,0 @@
// src/mocks/handlers/company.js
// 公司相关的 Mock Handlers
import { http, HttpResponse } from 'msw';
import { PINGAN_BANK_DATA, generateCompanyData } from '../data/company';
// 模拟延迟
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// 获取公司数据的辅助函数
const getCompanyData = (stockCode) => {
return stockCode === '000001' ? PINGAN_BANK_DATA : generateCompanyData(stockCode, '示例公司');
};
export const companyHandlers = [
// 1. 综合分析
http.get('/api/company/comprehensive-analysis/:stockCode', async ({ params }) => {
await delay(300);
const { stockCode } = params;
const data = getCompanyData(stockCode);
return HttpResponse.json({
success: true,
data: data.comprehensiveAnalysis
});
}),
// 2. 价值链分析
http.get('/api/company/value-chain-analysis/:stockCode', async ({ params }) => {
await delay(250);
const { stockCode } = params;
const data = getCompanyData(stockCode);
return HttpResponse.json({
success: true,
data: data.valueChainAnalysis
});
}),
// 3. 关键因素时间线
http.get('/api/company/key-factors-timeline/:stockCode', async ({ params }) => {
await delay(200);
const { stockCode } = params;
const data = getCompanyData(stockCode);
// 直接返回 keyFactorsTimeline 对象(包含 key_factors 和 development_timeline
return HttpResponse.json({
success: true,
data: data.keyFactorsTimeline
});
}),
// 4. 基本信息
http.get('/api/stock/:stockCode/basic-info', async ({ params }) => {
await delay(150);
const { stockCode } = params;
const data = getCompanyData(stockCode);
return HttpResponse.json({
success: true,
data: data.basicInfo
});
}),
// 5. 实际控制人
http.get('/api/stock/:stockCode/actual-control', async ({ params }) => {
await delay(150);
const { stockCode } = params;
const data = getCompanyData(stockCode);
const raw = data.actualControl;
// 数据保持原始百分比格式(如 52.38 表示 52.38%
const formatted = Array.isArray(raw) ? raw : [];
return HttpResponse.json({
success: true,
data: formatted
});
}),
// 6. 股权集中度
http.get('/api/stock/:stockCode/concentration', async ({ params }) => {
await delay(150);
const { stockCode } = params;
const data = getCompanyData(stockCode);
const raw = data.concentration;
// 数据保持原始百分比格式(如 52.38 表示 52.38%
const formatted = Array.isArray(raw) ? raw : [];
return HttpResponse.json({
success: true,
data: formatted
});
}),
// 7. 高管信息
http.get('/api/stock/:stockCode/management', async ({ params, request }) => {
await delay(200);
const { stockCode } = params;
const data = getCompanyData(stockCode);
// 解析查询参数
const url = new URL(request.url);
const activeOnly = url.searchParams.get('active_only') === 'true';
let management = data.management || [];
// 如果需要只返回在职高管mock 数据中默认都是在职)
if (activeOnly) {
management = management.filter(m => m.status !== 'resigned');
}
return HttpResponse.json({
success: true,
data: management // 直接返回数组
});
}),
// 8. 十大流通股东
http.get('/api/stock/:stockCode/top-circulation-shareholders', async ({ params, request }) => {
await delay(200);
const { stockCode } = params;
const data = getCompanyData(stockCode);
// 解析查询参数
const url = new URL(request.url);
const limit = parseInt(url.searchParams.get('limit') || '10', 10);
const shareholders = (data.topCirculationShareholders || []).slice(0, limit);
return HttpResponse.json({
success: true,
data: shareholders // 直接返回数组
});
}),
// 9. 十大股东
http.get('/api/stock/:stockCode/top-shareholders', async ({ params, request }) => {
await delay(200);
const { stockCode } = params;
const data = getCompanyData(stockCode);
// 解析查询参数
const url = new URL(request.url);
const limit = parseInt(url.searchParams.get('limit') || '10', 10);
const shareholders = (data.topShareholders || []).slice(0, limit);
return HttpResponse.json({
success: true,
data: shareholders // 直接返回数组
});
}),
// 10. 分支机构
http.get('/api/stock/:stockCode/branches', async ({ params }) => {
await delay(200);
const { stockCode } = params;
const data = getCompanyData(stockCode);
return HttpResponse.json({
success: true,
data: data.branches || [] // 直接返回数组
});
}),
// 11. 公告列表
http.get('/api/stock/:stockCode/announcements', async ({ params, request }) => {
await delay(250);
const { stockCode } = params;
const data = getCompanyData(stockCode);
// 解析查询参数
const url = new URL(request.url);
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
const page = parseInt(url.searchParams.get('page') || '1', 10);
const type = url.searchParams.get('type');
let announcements = data.announcements || [];
// 类型筛选
if (type) {
announcements = announcements.filter(a => a.type === type);
}
// 分页
const start = (page - 1) * limit;
const end = start + limit;
const paginatedAnnouncements = announcements.slice(start, end);
return HttpResponse.json({
success: true,
data: paginatedAnnouncements // 直接返回数组
});
}),
// 12. 披露时间表
http.get('/api/stock/:stockCode/disclosure-schedule', async ({ params }) => {
await delay(150);
const { stockCode } = params;
const data = getCompanyData(stockCode);
return HttpResponse.json({
success: true,
data: data.disclosureSchedule || [] // 直接返回数组
});
}),
// 13. 盈利预测报告
http.get('/api/stock/:stockCode/forecast-report', async ({ params }) => {
await delay(300);
const { stockCode } = params;
const data = getCompanyData(stockCode);
return HttpResponse.json({
success: true,
data: data.forecastReport || null
});
}),
// 14. 价值链关联公司
http.get('/api/company/value-chain/related-companies', async ({ request }) => {
await delay(300);
const url = new URL(request.url);
const nodeName = url.searchParams.get('node_name') || '';
console.log('[Mock] 获取价值链关联公司:', nodeName);
// 生成模拟的关联公司数据
const relatedCompanies = [
{
stock_code: '601318',
stock_name: '中国平安',
industry: '保险',
relation: '同业竞争',
market_cap: 9200,
change_pct: 1.25
},
{
stock_code: '600036',
stock_name: '招商银行',
industry: '银行',
relation: '核心供应商',
market_cap: 8500,
change_pct: 0.85
},
{
stock_code: '601166',
stock_name: '兴业银行',
industry: '银行',
relation: '同业竞争',
market_cap: 4200,
change_pct: -0.32
},
{
stock_code: '601398',
stock_name: '工商银行',
industry: '银行',
relation: '同业竞争',
market_cap: 15000,
change_pct: 0.15
},
{
stock_code: '601288',
stock_name: '农业银行',
industry: '银行',
relation: '同业竞争',
market_cap: 12000,
change_pct: -0.08
}
];
return HttpResponse.json({
success: true,
data: relatedCompanies,
node_name: nodeName
});
}),
];

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +0,0 @@
// src/mocks/handlers/external.js
// 外部服务 Mock Handler允许通过
import { http, passthrough } from 'msw';
/**
* 外部服务处理器
* 对于外部服务如头像、CDN等使用 passthrough 让请求正常发送到真实服务器
*/
export const externalHandlers = [
// Pravatar 头像服务 - 允许通过到真实服务
http.get('https://i.pravatar.cc/*', async () => {
return passthrough();
}),
// 如果需要 mock 头像,也可以返回一个占位图片
// http.get('https://i.pravatar.cc/*', async () => {
// return HttpResponse.text(
// '<svg width="150" height="150" xmlns="http://www.w3.org/2000/svg"><rect width="150" height="150" fill="#ddd"/><text x="50%" y="50%" text-anchor="middle" dy=".3em" fill="#999">Avatar</text></svg>',
// {
// headers: { 'Content-Type': 'image/svg+xml' }
// }
// );
// }),
];

View File

@@ -1,121 +0,0 @@
// src/mocks/handlers/financial.js
// 财务数据相关的 Mock Handlers
import { http, HttpResponse } from 'msw';
import { generateFinancialData } from '../data/financial';
// 模拟延迟
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
export const financialHandlers = [
// 1. 股票基本信息
http.get('/api/financial/stock-info/:stockCode', async ({ params }) => {
await delay(150);
const { stockCode } = params;
const data = generateFinancialData(stockCode);
return HttpResponse.json({
success: true,
data: data.stockInfo
});
}),
// 2. 资产负债表
http.get('/api/financial/balance-sheet/:stockCode', async ({ params, request }) => {
await delay(250);
const { stockCode } = params;
const url = new URL(request.url);
const limit = parseInt(url.searchParams.get('limit') || '4', 10);
const data = generateFinancialData(stockCode);
return HttpResponse.json({
success: true,
data: data.balanceSheet.slice(0, limit)
});
}),
// 3. 利润表
http.get('/api/financial/income-statement/:stockCode', async ({ params, request }) => {
await delay(250);
const { stockCode } = params;
const url = new URL(request.url);
const limit = parseInt(url.searchParams.get('limit') || '4', 10);
const data = generateFinancialData(stockCode);
return HttpResponse.json({
success: true,
data: data.incomeStatement.slice(0, limit)
});
}),
// 4. 现金流量表
http.get('/api/financial/cashflow/:stockCode', async ({ params, request }) => {
await delay(250);
const { stockCode } = params;
const url = new URL(request.url);
const limit = parseInt(url.searchParams.get('limit') || '4', 10);
const data = generateFinancialData(stockCode);
return HttpResponse.json({
success: true,
data: data.cashflow.slice(0, limit)
});
}),
// 5. 财务指标
http.get('/api/financial/financial-metrics/:stockCode', async ({ params, request }) => {
await delay(250);
const { stockCode } = params;
const url = new URL(request.url);
const limit = parseInt(url.searchParams.get('limit') || '4', 10);
const data = generateFinancialData(stockCode);
return HttpResponse.json({
success: true,
data: data.financialMetrics.slice(0, limit)
});
}),
// 6. 主营业务
http.get('/api/financial/main-business/:stockCode', async ({ params }) => {
await delay(200);
const { stockCode } = params;
const data = generateFinancialData(stockCode);
return HttpResponse.json({
success: true,
data: data.mainBusiness
});
}),
// 7. 业绩预告
http.get('/api/financial/forecast/:stockCode', async ({ params }) => {
await delay(200);
const { stockCode } = params;
const data = generateFinancialData(stockCode);
return HttpResponse.json({
success: true,
data: data.forecast
});
}),
// 8. 行业排名
http.get('/api/financial/industry-rank/:stockCode', async ({ params }) => {
await delay(250);
const { stockCode } = params;
const data = generateFinancialData(stockCode);
return HttpResponse.json({
success: true,
data: data.industryRank
});
}),
// 9. 期间对比
http.get('/api/financial/comparison/:stockCode', async ({ params }) => {
await delay(250);
const { stockCode } = params;
const data = generateFinancialData(stockCode);
return HttpResponse.json({
success: true,
data: data.periodComparison
});
}),
];

View File

@@ -1,311 +0,0 @@
/**
* 价值论坛帖子 Mock Handler
* 模拟 Elasticsearch API 响应
*/
import { http, HttpResponse, delay } from "msw";
import { mockPosts, mockPostComments } from "../data/forum";
// 内存中的帖子数据(支持增删改查)
let postsData = [...mockPosts];
let commentsData = { ...mockPostComments };
// ES 基础 URL开发环境直连
const ES_BASE_URL = "http://222.128.1.157:19200";
export const forumHandlers = [
// ==================== 帖子搜索 ====================
// POST /forum_posts/_search
http.post(`${ES_BASE_URL}/forum_posts/_search`, async ({ request }) => {
await delay(200);
const body = await request.json();
const { from = 0, size = 20, query, sort } = body;
let filteredPosts = [...postsData];
// 处理查询条件
if (query?.bool?.must) {
query.bool.must.forEach((condition) => {
// 状态过滤
if (condition.match?.status) {
filteredPosts = filteredPosts.filter(
(p) => p.status === condition.match.status
);
}
// 分类过滤
if (condition.term?.category) {
filteredPosts = filteredPosts.filter(
(p) => p.category === condition.term.category
);
}
// 标签过滤
if (condition.terms?.tags) {
filteredPosts = filteredPosts.filter((p) =>
p.tags.some((tag) => condition.terms.tags.includes(tag))
);
}
// 全文搜索
if (condition.multi_match) {
const keyword = condition.multi_match.query.toLowerCase();
filteredPosts = filteredPosts.filter(
(p) =>
p.title.toLowerCase().includes(keyword) ||
p.content.toLowerCase().includes(keyword) ||
p.tags.some((tag) => tag.toLowerCase().includes(keyword))
);
}
});
}
// 处理排序
if (sort && sort.length > 0) {
filteredPosts.sort((a, b) => {
for (const sortItem of sort) {
const [field, config] = Object.entries(sortItem)[0];
const order = config.order || "desc";
if (field === "is_pinned") {
if (a.is_pinned !== b.is_pinned) {
return order === "desc"
? (b.is_pinned ? 1 : 0) - (a.is_pinned ? 1 : 0)
: (a.is_pinned ? 1 : 0) - (b.is_pinned ? 1 : 0);
}
} else if (field === "created_at") {
const dateA = new Date(a.created_at).getTime();
const dateB = new Date(b.created_at).getTime();
if (dateA !== dateB) {
return order === "desc" ? dateB - dateA : dateA - dateB;
}
} else if (field === "likes_count" || field === "views_count") {
if (a[field] !== b[field]) {
return order === "desc"
? b[field] - a[field]
: a[field] - b[field];
}
}
}
return 0;
});
}
// 分页
const total = filteredPosts.length;
const paginatedPosts = filteredPosts.slice(from, from + size);
return HttpResponse.json({
hits: {
total: { value: total },
hits: paginatedPosts.map((post) => ({
_id: post.id,
_source: post,
})),
},
});
}),
// ==================== 获取单个帖子 ====================
// GET /forum_posts/_doc/:postId
http.get(`${ES_BASE_URL}/forum_posts/_doc/:postId`, async ({ params }) => {
await delay(100);
const { postId } = params;
const post = postsData.find((p) => p.id === postId);
if (!post) {
return HttpResponse.json({ found: false }, { status: 404 });
}
return HttpResponse.json({
_id: post.id,
_source: post,
found: true,
});
}),
// ==================== 创建帖子 ====================
// POST /forum_posts/_doc/:postId
http.post(
`${ES_BASE_URL}/forum_posts/_doc/:postId`,
async ({ params, request }) => {
await delay(300);
const { postId } = params;
const postData = await request.json();
const newPost = {
...postData,
id: postId,
};
postsData.unshift(newPost);
return HttpResponse.json({
_id: postId,
result: "created",
});
}
),
// ==================== 更新帖子(增加浏览量等) ====================
// POST /forum_posts/_update/:postId
http.post(
`${ES_BASE_URL}/forum_posts/_update/:postId`,
async ({ params, request }) => {
await delay(100);
const { postId } = params;
const body = await request.json();
const postIndex = postsData.findIndex((p) => p.id === postId);
if (postIndex === -1) {
return HttpResponse.json({ error: "Post not found" }, { status: 404 });
}
// 处理脚本更新(如增加浏览量)
if (body.script?.source) {
if (body.script.source.includes("views_count")) {
postsData[postIndex].views_count += 1;
} else if (body.script.source.includes("likes_count")) {
postsData[postIndex].likes_count += 1;
} else if (body.script.source.includes("comments_count")) {
postsData[postIndex].comments_count += 1;
}
}
// 处理文档更新
if (body.doc) {
postsData[postIndex] = { ...postsData[postIndex], ...body.doc };
}
return HttpResponse.json({
_id: postId,
result: "updated",
});
}
),
// ==================== 评论搜索 ====================
// POST /forum_comments/_search
http.post(`${ES_BASE_URL}/forum_comments/_search`, async ({ request }) => {
await delay(150);
const body = await request.json();
const { from = 0, size = 50, query } = body;
let postId = null;
// 提取 post_id
if (query?.bool?.must) {
const postIdCondition = query.bool.must.find((c) => c.term?.post_id);
if (postIdCondition) {
postId = postIdCondition.term.post_id;
}
}
const comments = postId ? commentsData[postId] || [] : [];
const activeComments = comments.filter((c) => c.status === "active");
const paginatedComments = activeComments.slice(from, from + size);
return HttpResponse.json({
hits: {
total: { value: activeComments.length },
hits: paginatedComments.map((comment) => ({
_id: comment.id,
_source: comment,
})),
},
});
}),
// ==================== 创建评论 ====================
// POST /forum_comments/_doc/:commentId
http.post(
`${ES_BASE_URL}/forum_comments/_doc/:commentId`,
async ({ params, request }) => {
await delay(200);
const { commentId } = params;
const commentData = await request.json();
const newComment = {
...commentData,
id: commentId,
};
const postId = commentData.post_id;
if (!commentsData[postId]) {
commentsData[postId] = [];
}
commentsData[postId].push(newComment);
return HttpResponse.json({
_id: commentId,
result: "created",
});
}
),
// ==================== 更新评论(点赞等) ====================
// POST /forum_comments/_update/:commentId
http.post(
`${ES_BASE_URL}/forum_comments/_update/:commentId`,
async ({ params, request }) => {
await delay(100);
const { commentId } = params;
const body = await request.json();
// 在所有帖子的评论中查找
for (const postId in commentsData) {
const commentIndex = commentsData[postId].findIndex(
(c) => c.id === commentId
);
if (commentIndex !== -1) {
if (body.script?.source?.includes("likes_count")) {
commentsData[postId][commentIndex].likes_count += 1;
}
break;
}
}
return HttpResponse.json({
_id: commentId,
result: "updated",
});
}
),
// ==================== 事件时间轴搜索 ====================
// POST /forum_events/_search
http.post(`${ES_BASE_URL}/forum_events/_search`, async () => {
await delay(100);
// 返回空数组(暂无事件数据)
return HttpResponse.json({
hits: {
total: { value: 0 },
hits: [],
},
});
}),
// ==================== 索引创建(初始化时调用) ====================
// PUT /forum_posts, /forum_comments, /forum_events
http.put(`${ES_BASE_URL}/forum_posts`, async () => {
await delay(100);
return HttpResponse.json({ acknowledged: true });
}),
http.put(`${ES_BASE_URL}/forum_comments`, async () => {
await delay(100);
return HttpResponse.json({ acknowledged: true });
}),
http.put(`${ES_BASE_URL}/forum_events`, async () => {
await delay(100);
return HttpResponse.json({ acknowledged: true });
}),
];
export default forumHandlers;

View File

@@ -1,48 +0,0 @@
// src/mocks/handlers/index.js
// 汇总所有 Mock Handlers
import { authHandlers } from './auth';
import { accountHandlers } from './account';
import { simulationHandlers } from './simulation';
import { eventHandlers } from './event';
import { paymentHandlers } from './payment';
import { industryHandlers } from './industry';
import { conceptHandlers } from './concept';
import { stockHandlers } from './stock';
import { companyHandlers } from './company';
import { marketHandlers } from './market';
import { financialHandlers } from './financial';
import { limitAnalyseHandlers } from './limitAnalyse';
import { posthogHandlers } from './posthog';
import { externalHandlers } from './external';
import { agentHandlers } from './agent';
import { bytedeskHandlers } from './bytedesk';
import { predictionHandlers } from './prediction';
import { forumHandlers } from './forum';
import { invoiceHandlers } from './invoice';
// 可以在这里添加更多的 handlers
// import { userHandlers } from './user';
export const handlers = [
...authHandlers,
...accountHandlers,
...simulationHandlers,
...eventHandlers,
...paymentHandlers,
...industryHandlers,
...conceptHandlers,
...stockHandlers,
...companyHandlers,
...marketHandlers,
...financialHandlers,
...limitAnalyseHandlers,
...posthogHandlers,
...externalHandlers,
...agentHandlers,
...bytedeskHandlers, // ⚡ Bytedesk 客服 Widget passthrough
...predictionHandlers, // 预测市场
...forumHandlers, // 价值论坛帖子 (ES)
...invoiceHandlers, // 发票管理
// ...userHandlers,
];

View File

@@ -1,44 +0,0 @@
// src/mocks/handlers/industry.js
// 行业分类相关的 Mock API Handlers
import { http, HttpResponse } from 'msw';
import { industryData } from '../../data/industryData';
// 模拟网络延迟
const delay = (ms = 300) => new Promise(resolve => setTimeout(resolve, ms));
export const industryHandlers = [
// 获取行业分类完整树形结构
http.get('/api/classifications', async ({ request }) => {
await delay(500);
const url = new URL(request.url);
const classification = url.searchParams.get('classification');
console.log('[Mock] 获取行业分类树形数据(真实数据)', { classification });
try {
let data = industryData;
// 如果指定了分类体系,只返回该体系的数据
if (classification) {
data = industryData.filter(item => item.value === classification);
}
return HttpResponse.json({
success: true,
data: data
});
} catch (error) {
console.error('[Mock] 获取行业分类失败:', error);
return HttpResponse.json(
{
success: false,
error: '获取行业分类失败',
data: []
},
{ status: 500 }
);
}
})
];

View File

@@ -1,920 +0,0 @@
// src/mocks/handlers/invoice.js
import { http, HttpResponse, delay } from 'msw';
import { getCurrentUser } from '../data/users';
// 模拟网络延迟(毫秒)
const NETWORK_DELAY = 500;
// 模拟发票数据存储
const mockInvoices = new Map();
const mockTitleTemplates = new Map();
let invoiceIdCounter = 1000;
let templateIdCounter = 100;
// 模拟可开票订单数据
const mockInvoiceableOrders = [
{
id: 'ORDER_1001_1703001600000',
orderNo: 'VF20241220001',
planName: 'pro',
billingCycle: 'yearly',
amount: 2699,
paidAt: '2024-12-20T10:00:00Z',
invoiceApplied: false,
},
{
id: 'ORDER_1002_1703088000000',
orderNo: 'VF20241221001',
planName: 'max',
billingCycle: 'monthly',
amount: 599,
paidAt: '2024-12-21T10:00:00Z',
invoiceApplied: false,
},
];
// 为每个用户生成模拟发票数据
const initMockInvoices = () => {
// 为用户 ID 1-4 都生成一些发票数据
const userInvoiceData = [
// 用户1 (免费用户) - 无发票
// 用户2 (Pro会员) - 有多张发票
{
id: 'INV_001',
orderId: 'ORDER_999_1702396800000',
orderNo: 'VF20241213001',
userId: 2,
invoiceType: 'electronic',
titleType: 'company',
title: '北京价值前沿科技有限公司',
taxNumber: '91110108MA01XXXXX',
amount: 2699,
email: 'pro@example.com',
status: 'completed',
invoiceNo: 'E20241213001',
invoiceCode: '011001900111',
invoiceUrl: 'https://example.com/invoices/E20241213001.pdf',
createdAt: '2024-12-13T10:00:00Z',
updatedAt: '2024-12-14T15:30:00Z',
completedAt: '2024-12-14T15:30:00Z',
},
{
id: 'INV_002',
orderId: 'ORDER_998_1701792000000',
orderNo: 'VF20241206001',
userId: 2,
invoiceType: 'electronic',
titleType: 'personal',
title: '张三',
amount: 599,
email: 'pro@example.com',
status: 'processing',
createdAt: '2024-12-06T10:00:00Z',
updatedAt: '2024-12-06T10:00:00Z',
},
{
id: 'INV_003',
orderId: 'ORDER_997_1700000000000',
orderNo: 'VF20241115001',
userId: 2,
invoiceType: 'electronic',
titleType: 'personal',
title: '李四',
amount: 299,
email: 'pro@example.com',
status: 'pending',
createdAt: '2024-12-24T10:00:00Z',
updatedAt: '2024-12-24T10:00:00Z',
},
// 用户3 (Max会员) - 有发票
{
id: 'INV_004',
orderId: 'ORDER_996_1703000000000',
orderNo: 'VF20241220002',
userId: 3,
invoiceType: 'electronic',
titleType: 'company',
title: '上海科技发展有限公司',
taxNumber: '91310115MA01YYYYY',
amount: 5999,
email: 'max@example.com',
status: 'completed',
invoiceNo: 'E20241220001',
invoiceCode: '011001900222',
invoiceUrl: 'https://example.com/invoices/E20241220001.pdf',
createdAt: '2024-12-20T10:00:00Z',
updatedAt: '2024-12-21T09:00:00Z',
completedAt: '2024-12-21T09:00:00Z',
},
{
id: 'INV_005',
orderId: 'ORDER_995_1702500000000',
orderNo: 'VF20241214001',
userId: 3,
invoiceType: 'paper',
titleType: 'company',
title: '上海科技发展有限公司',
taxNumber: '91310115MA01YYYYY',
amount: 2699,
email: 'max@example.com',
status: 'processing',
createdAt: '2024-12-14T10:00:00Z',
updatedAt: '2024-12-15T10:00:00Z',
},
// 用户1 (测试用户) - 也添加一些发票方便测试
{
id: 'INV_006',
orderId: 'ORDER_994_1703100000000',
orderNo: 'VF20241222001',
userId: 1,
invoiceType: 'electronic',
titleType: 'personal',
title: '测试用户',
amount: 299,
email: 'test@example.com',
status: 'completed',
invoiceNo: 'E20241222001',
invoiceCode: '011001900333',
invoiceUrl: 'https://example.com/invoices/E20241222001.pdf',
createdAt: '2024-12-22T10:00:00Z',
updatedAt: '2024-12-23T10:00:00Z',
completedAt: '2024-12-23T10:00:00Z',
},
{
id: 'INV_007',
orderId: 'ORDER_993_1703200000000',
orderNo: 'VF20241223001',
userId: 1,
invoiceType: 'electronic',
titleType: 'company',
title: '测试科技有限公司',
taxNumber: '91110108MA01ZZZZZ',
amount: 599,
email: 'test@example.com',
status: 'processing',
createdAt: '2024-12-23T14:00:00Z',
updatedAt: '2024-12-23T14:00:00Z',
},
{
id: 'INV_008',
orderId: 'ORDER_992_1703250000000',
orderNo: 'VF20241225001',
userId: 1,
invoiceType: 'electronic',
titleType: 'personal',
title: '王五',
amount: 199,
email: 'test@example.com',
status: 'pending',
createdAt: '2024-12-25T10:00:00Z',
updatedAt: '2024-12-25T10:00:00Z',
},
{
id: 'INV_009',
orderId: 'ORDER_991_1702000000000',
orderNo: 'VF20241208001',
userId: 1,
invoiceType: 'electronic',
titleType: 'personal',
title: '赵六',
amount: 99,
email: 'test@example.com',
status: 'cancelled',
createdAt: '2024-12-08T10:00:00Z',
updatedAt: '2024-12-09T10:00:00Z',
},
];
userInvoiceData.forEach((invoice) => {
mockInvoices.set(invoice.id, invoice);
});
};
// 初始化模拟抬头模板 - 为每个用户生成
const initMockTemplates = () => {
const sampleTemplates = [
// 用户1 (测试用户) 的模板
{
id: 'TPL_001',
userId: 1,
isDefault: true,
titleType: 'company',
title: '测试科技有限公司',
taxNumber: '91110108MA01ZZZZZ',
companyAddress: '北京市朝阳区建国路1号',
companyPhone: '010-88888888',
bankName: '中国建设银行北京分行',
bankAccount: '1100001234567890123',
createdAt: '2024-01-01T00:00:00Z',
},
{
id: 'TPL_002',
userId: 1,
isDefault: false,
titleType: 'personal',
title: '测试用户',
createdAt: '2024-06-01T00:00:00Z',
},
// 用户2 (Pro会员) 的模板
{
id: 'TPL_003',
userId: 2,
isDefault: true,
titleType: 'company',
title: '北京价值前沿科技有限公司',
taxNumber: '91110108MA01XXXXX',
companyAddress: '北京市海淀区中关村大街1号',
companyPhone: '010-12345678',
bankName: '中国工商银行北京分行',
bankAccount: '0200001234567890123',
createdAt: '2024-01-01T00:00:00Z',
},
{
id: 'TPL_004',
userId: 2,
isDefault: false,
titleType: 'personal',
title: '张三',
createdAt: '2024-06-01T00:00:00Z',
},
// 用户3 (Max会员) 的模板
{
id: 'TPL_005',
userId: 3,
isDefault: true,
titleType: 'company',
title: '上海科技发展有限公司',
taxNumber: '91310115MA01YYYYY',
companyAddress: '上海市浦东新区陆家嘴金融中心',
companyPhone: '021-66666666',
bankName: '中国银行上海分行',
bankAccount: '4400001234567890123',
createdAt: '2024-02-01T00:00:00Z',
},
];
sampleTemplates.forEach((template) => {
mockTitleTemplates.set(template.id, template);
});
};
// 初始化数据
initMockInvoices();
initMockTemplates();
export const invoiceHandlers = [
// ==================== 发票申请管理 ====================
// 1. 获取可开票订单列表
http.get('/api/invoice/available-orders', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{
code: 401,
message: '未登录',
data: null,
},
{ status: 401 }
);
}
// 返回未申请开票的订单
const availableOrders = mockInvoiceableOrders.filter((order) => !order.invoiceApplied);
console.log('[Mock] 获取可开票订单:', { count: availableOrders.length });
return HttpResponse.json({
code: 200,
message: 'success',
data: availableOrders,
});
}),
// 2. 申请开票
http.post('/api/invoice/apply', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{
code: 401,
message: '未登录',
data: null,
},
{ status: 401 }
);
}
const body = await request.json();
const { orderId, invoiceType, titleType, title, taxNumber, email, phone, remark } = body;
console.log('[Mock] 申请开票:', { orderId, invoiceType, titleType, title });
// 验证订单
const order = mockInvoiceableOrders.find((o) => o.id === orderId);
if (!order) {
return HttpResponse.json(
{
code: 404,
message: '订单不存在',
data: null,
},
{ status: 404 }
);
}
if (order.invoiceApplied) {
return HttpResponse.json(
{
code: 400,
message: '该订单已申请开票',
data: null,
},
{ status: 400 }
);
}
// 企业开票必须有税号
if (titleType === 'company' && !taxNumber) {
return HttpResponse.json(
{
code: 400,
message: '企业开票必须填写税号',
data: null,
},
{ status: 400 }
);
}
// 创建发票申请
const invoiceId = `INV_${invoiceIdCounter++}`;
const invoice = {
id: invoiceId,
orderId: order.id,
orderNo: order.orderNo,
userId: currentUser.id,
invoiceType,
titleType,
title,
taxNumber: taxNumber || null,
companyAddress: body.companyAddress || null,
companyPhone: body.companyPhone || null,
bankName: body.bankName || null,
bankAccount: body.bankAccount || null,
amount: order.amount,
email,
phone: phone || null,
mailingAddress: body.mailingAddress || null,
recipientName: body.recipientName || null,
recipientPhone: body.recipientPhone || null,
status: 'pending',
remark: remark || null,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
mockInvoices.set(invoiceId, invoice);
order.invoiceApplied = true;
console.log('[Mock] 发票申请创建成功:', invoice);
// 模拟3秒后自动变为处理中
setTimeout(() => {
const existingInvoice = mockInvoices.get(invoiceId);
if (existingInvoice && existingInvoice.status === 'pending') {
existingInvoice.status = 'processing';
existingInvoice.updatedAt = new Date().toISOString();
console.log(`[Mock] 发票开始处理: ${invoiceId}`);
}
}, 3000);
// 模拟10秒后自动开具完成电子发票
if (invoiceType === 'electronic') {
setTimeout(() => {
const existingInvoice = mockInvoices.get(invoiceId);
if (existingInvoice && existingInvoice.status === 'processing') {
existingInvoice.status = 'completed';
existingInvoice.invoiceNo = `E${Date.now()}`;
existingInvoice.invoiceCode = '011001900111';
existingInvoice.invoiceUrl = `https://example.com/invoices/${existingInvoice.invoiceNo}.pdf`;
existingInvoice.completedAt = new Date().toISOString();
existingInvoice.updatedAt = new Date().toISOString();
console.log(`[Mock] 电子发票开具完成: ${invoiceId}`);
}
}, 10000);
}
return HttpResponse.json({
code: 200,
message: '开票申请已提交预计1-3个工作日内处理',
data: invoice,
});
}),
// 3. 获取发票列表
http.get('/api/invoice/list', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{
code: 401,
message: '未登录',
data: null,
},
{ status: 401 }
);
}
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page') || '1', 10);
const pageSize = parseInt(url.searchParams.get('pageSize') || '10', 10);
const statusFilter = url.searchParams.get('status');
// 获取用户的发票
let userInvoices = Array.from(mockInvoices.values())
.filter((invoice) => invoice.userId === currentUser.id)
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
// 状态筛选
if (statusFilter) {
userInvoices = userInvoices.filter((invoice) => invoice.status === statusFilter);
}
// 分页
const total = userInvoices.length;
const startIndex = (page - 1) * pageSize;
const paginatedInvoices = userInvoices.slice(startIndex, startIndex + pageSize);
console.log('[Mock] 获取发票列表:', { total, page, pageSize });
return HttpResponse.json({
code: 200,
message: 'success',
data: {
list: paginatedInvoices,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
},
});
}),
// 4. 获取发票详情
http.get('/api/invoice/:invoiceId', async ({ params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{
code: 401,
message: '未登录',
data: null,
},
{ status: 401 }
);
}
const { invoiceId } = params;
const invoice = mockInvoices.get(invoiceId);
if (!invoice) {
return HttpResponse.json(
{
code: 404,
message: '发票不存在',
data: null,
},
{ status: 404 }
);
}
if (invoice.userId !== currentUser.id) {
return HttpResponse.json(
{
code: 403,
message: '无权访问此发票',
data: null,
},
{ status: 403 }
);
}
console.log('[Mock] 获取发票详情:', { invoiceId });
return HttpResponse.json({
code: 200,
message: 'success',
data: invoice,
});
}),
// 5. 取消发票申请
http.post('/api/invoice/:invoiceId/cancel', async ({ params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{
code: 401,
message: '未登录',
data: null,
},
{ status: 401 }
);
}
const { invoiceId } = params;
const invoice = mockInvoices.get(invoiceId);
if (!invoice) {
return HttpResponse.json(
{
code: 404,
message: '发票不存在',
data: null,
},
{ status: 404 }
);
}
if (invoice.userId !== currentUser.id) {
return HttpResponse.json(
{
code: 403,
message: '无权操作此发票',
data: null,
},
{ status: 403 }
);
}
if (invoice.status !== 'pending') {
return HttpResponse.json(
{
code: 400,
message: '只能取消待处理的发票申请',
data: null,
},
{ status: 400 }
);
}
invoice.status = 'cancelled';
invoice.updatedAt = new Date().toISOString();
// 恢复订单的开票状态
const order = mockInvoiceableOrders.find((o) => o.id === invoice.orderId);
if (order) {
order.invoiceApplied = false;
}
console.log('[Mock] 发票申请已取消:', invoiceId);
return HttpResponse.json({
code: 200,
message: '发票申请已取消',
data: null,
});
}),
// 6. 下载电子发票
http.get('/api/invoice/:invoiceId/download', async ({ params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{
code: 401,
message: '未登录',
data: null,
},
{ status: 401 }
);
}
const { invoiceId } = params;
const invoice = mockInvoices.get(invoiceId);
if (!invoice) {
return HttpResponse.json(
{
code: 404,
message: '发票不存在',
data: null,
},
{ status: 404 }
);
}
if (invoice.userId !== currentUser.id) {
return HttpResponse.json(
{
code: 403,
message: '无权下载此发票',
data: null,
},
{ status: 403 }
);
}
if (invoice.status !== 'completed') {
return HttpResponse.json(
{
code: 400,
message: '发票尚未开具完成',
data: null,
},
{ status: 400 }
);
}
console.log('[Mock] 下载电子发票:', invoiceId);
// 返回模拟的 PDF 内容
const pdfContent = `%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
endobj
trailer
<< /Root 1 0 R >>
%%EOF`;
return new HttpResponse(pdfContent, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="invoice_${invoice.invoiceNo}.pdf"`,
},
});
}),
// 7. 获取发票统计
http.get('/api/invoice/stats', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{
code: 401,
message: '未登录',
data: null,
},
{ status: 401 }
);
}
const userInvoices = Array.from(mockInvoices.values()).filter(
(invoice) => invoice.userId === currentUser.id
);
// 计算可开票金额(未申请开票的订单)
const availableOrders = mockInvoiceableOrders.filter((order) => !order.invoiceApplied);
const availableAmount = availableOrders.reduce((sum, order) => sum + order.amount, 0);
// 计算已开票金额
const invoicedAmount = userInvoices
.filter((i) => i.status === 'completed')
.reduce((sum, invoice) => sum + invoice.amount, 0);
// 计算处理中金额
const processingAmount = userInvoices
.filter((i) => i.status === 'processing' || i.status === 'pending')
.reduce((sum, invoice) => sum + invoice.amount, 0);
const stats = {
total: userInvoices.length,
pending: userInvoices.filter((i) => i.status === 'pending').length,
processing: userInvoices.filter((i) => i.status === 'processing').length,
completed: userInvoices.filter((i) => i.status === 'completed').length,
cancelled: userInvoices.filter((i) => i.status === 'cancelled').length,
availableAmount,
invoicedAmount,
processingAmount,
};
console.log('[Mock] 获取发票统计:', stats);
return HttpResponse.json({
code: 200,
message: 'success',
data: stats,
});
}),
// ==================== 发票抬头模板管理 ====================
// 8. 获取发票抬头模板列表
http.get('/api/invoice/title-templates', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{
code: 401,
message: '未登录',
data: null,
},
{ status: 401 }
);
}
const userTemplates = Array.from(mockTitleTemplates.values())
.filter((template) => template.userId === currentUser.id)
.sort((a, b) => {
// 默认的排在前面
if (a.isDefault !== b.isDefault) {
return b.isDefault ? 1 : -1;
}
return new Date(b.createdAt) - new Date(a.createdAt);
});
console.log('[Mock] 获取抬头模板列表:', { count: userTemplates.length });
return HttpResponse.json({
code: 200,
message: 'success',
data: userTemplates,
});
}),
// 9. 保存发票抬头模板
http.post('/api/invoice/title-template', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{
code: 401,
message: '未登录',
data: null,
},
{ status: 401 }
);
}
const body = await request.json();
const templateId = `TPL_${templateIdCounter++}`;
const template = {
id: templateId,
userId: currentUser.id,
isDefault: body.isDefault || false,
titleType: body.titleType,
title: body.title,
taxNumber: body.taxNumber || null,
companyAddress: body.companyAddress || null,
companyPhone: body.companyPhone || null,
bankName: body.bankName || null,
bankAccount: body.bankAccount || null,
createdAt: new Date().toISOString(),
};
// 如果设为默认,取消其他模板的默认状态
if (template.isDefault) {
mockTitleTemplates.forEach((t) => {
if (t.userId === currentUser.id) {
t.isDefault = false;
}
});
}
mockTitleTemplates.set(templateId, template);
console.log('[Mock] 保存抬头模板:', template);
return HttpResponse.json({
code: 200,
message: '保存成功',
data: template,
});
}),
// 10. 删除发票抬头模板
http.delete('/api/invoice/title-template/:templateId', async ({ params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{
code: 401,
message: '未登录',
data: null,
},
{ status: 401 }
);
}
const { templateId } = params;
const template = mockTitleTemplates.get(templateId);
if (!template) {
return HttpResponse.json(
{
code: 404,
message: '模板不存在',
data: null,
},
{ status: 404 }
);
}
if (template.userId !== currentUser.id) {
return HttpResponse.json(
{
code: 403,
message: '无权删除此模板',
data: null,
},
{ status: 403 }
);
}
mockTitleTemplates.delete(templateId);
console.log('[Mock] 删除抬头模板:', templateId);
return HttpResponse.json({
code: 200,
message: '删除成功',
data: null,
});
}),
// 11. 设置默认发票抬头
http.post('/api/invoice/title-template/:templateId/default', async ({ params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json(
{
code: 401,
message: '未登录',
data: null,
},
{ status: 401 }
);
}
const { templateId } = params;
const template = mockTitleTemplates.get(templateId);
if (!template) {
return HttpResponse.json(
{
code: 404,
message: '模板不存在',
data: null,
},
{ status: 404 }
);
}
if (template.userId !== currentUser.id) {
return HttpResponse.json(
{
code: 403,
message: '无权操作此模板',
data: null,
},
{ status: 403 }
);
}
// 取消其他模板的默认状态
mockTitleTemplates.forEach((t) => {
if (t.userId === currentUser.id) {
t.isDefault = false;
}
});
template.isDefault = true;
console.log('[Mock] 设置默认抬头:', templateId);
return HttpResponse.json({
code: 200,
message: '设置成功',
data: null,
});
}),
];

View File

@@ -1,607 +0,0 @@
// src/mocks/handlers/limitAnalyse.js
// 涨停分析相关的 Mock Handlers
import { http, HttpResponse } from 'msw';
// 模拟延迟
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// 生成可用日期列表最近30个交易日
const generateAvailableDates = () => {
const dates = [];
const today = new Date();
let count = 0;
for (let i = 0; i < 60 && count < 30; i++) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dayOfWeek = date.getDay();
// 跳过周末
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const dateStr = `${year}${month}${day}`;
// 返回包含 date 和 count 字段的对象
dates.push({
date: dateStr,
count: Math.floor(Math.random() * 80) + 30 // 30-110 只涨停股票
});
count++;
}
}
return dates;
};
// 生成板块数据
const generateSectors = (count = 8) => {
const sectorNames = [
'人工智能', 'ChatGPT', '数字经济',
'新能源汽车', '光伏', '锂电池',
'半导体', '芯片', '5G通信',
'医疗器械', '创新药', '中药',
'白酒', '食品饮料', '消费电子',
'军工', '航空航天', '新材料'
];
const sectors = [];
for (let i = 0; i < Math.min(count, sectorNames.length); i++) {
const stockCount = Math.floor(Math.random() * 15) + 5;
const stocks = [];
for (let j = 0; j < stockCount; j++) {
stocks.push({
code: `${Math.random() > 0.5 ? '6' : '0'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`,
name: `${sectorNames[i]}股票${j + 1}`,
latest_limit_time: `${Math.floor(Math.random() * 4) + 9}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}`,
limit_up_count: Math.floor(Math.random() * 3) + 1,
price: (Math.random() * 100 + 10).toFixed(2),
change_pct: (Math.random() * 5 + 5).toFixed(2),
turnover_rate: (Math.random() * 30 + 5).toFixed(2),
volume: Math.floor(Math.random() * 100000000 + 10000000),
amount: (Math.random() * 1000000000 + 100000000).toFixed(2),
limit_type: Math.random() > 0.7 ? '一字板' : (Math.random() > 0.5 ? 'T字板' : '普通涨停'),
封单金额: (Math.random() * 500000000).toFixed(2),
});
}
sectors.push({
sector_name: sectorNames[i],
stock_count: stockCount,
avg_limit_time: `${Math.floor(Math.random() * 2) + 10}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}`,
stocks: stocks,
});
}
return sectors;
};
// 生成高位股数据(用于 HighPositionStocks 组件)
const generateHighPositionStocks = () => {
const stocks = [];
const stockNames = [
'宁德时代', '比亚迪', '隆基绿能', '东方财富', '中际旭创',
'京东方A', '海康威视', '立讯精密', '三一重工', '恒瑞医药',
'三六零', '东方通信', '贵州茅台', '五粮液', '中国平安'
];
const industries = [
'锂电池', '新能源汽车', '光伏', '金融科技', '通信设备',
'显示器件', '安防设备', '电子元件', '工程机械', '医药制造',
'网络安全', '通信服务', '白酒', '食品饮料', '保险'
];
for (let i = 0; i < stockNames.length; i++) {
const code = `${Math.random() > 0.5 ? '6' : '0'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`;
const continuousDays = Math.floor(Math.random() * 8) + 2; // 2-9连板
const price = parseFloat((Math.random() * 100 + 20).toFixed(2));
const increaseRate = parseFloat((Math.random() * 3 + 8).toFixed(2)); // 8%-11%
const turnoverRate = parseFloat((Math.random() * 20 + 5).toFixed(2)); // 5%-25%
stocks.push({
stock_code: code,
stock_name: stockNames[i],
price: price,
increase_rate: increaseRate,
continuous_limit_up: continuousDays,
industry: industries[i],
turnover_rate: turnoverRate,
});
}
// 按连板天数降序排序
stocks.sort((a, b) => b.continuous_limit_up - a.continuous_limit_up);
return stocks;
};
// 生成高位股统计数据
const generateHighPositionStatistics = (stocks) => {
if (!stocks || stocks.length === 0) {
return {
total_count: 0,
avg_continuous_days: 0,
max_continuous_days: 0,
};
}
const totalCount = stocks.length;
const sumDays = stocks.reduce((sum, stock) => sum + stock.continuous_limit_up, 0);
const maxDays = Math.max(...stocks.map(s => s.continuous_limit_up));
return {
total_count: totalCount,
avg_continuous_days: parseFloat((sumDays / totalCount).toFixed(1)),
max_continuous_days: maxDays,
};
};
// 生成词云数据
const generateWordCloudData = () => {
const keywords = [
'人工智能', 'ChatGPT', 'AI芯片', '大模型', '算力',
'新能源', '光伏', '锂电池', '储能', '充电桩',
'半导体', '芯片', 'EDA', '国产替代', '集成电路',
'医疗', '创新药', 'CXO', '医疗器械', '生物医药',
'消费', '白酒', '食品', '零售', '餐饮',
'金融', '券商', '保险', '银行', '金融科技'
];
return keywords.map(keyword => ({
text: keyword,
value: Math.floor(Math.random() * 50) + 10,
category: ['科技', '新能源', '医疗', '消费', '金融'][Math.floor(Math.random() * 5)],
}));
};
// 生成每日分析数据
const generateDailyAnalysis = (date) => {
const sectorNames = [
'公告', '人工智能', 'ChatGPT', '数字经济',
'新能源汽车', '光伏', '锂电池',
'半导体', '芯片', '5G通信',
'医疗器械', '创新药', '其他'
];
const stockNameTemplates = [
'龙头', '科技', '新能源', '智能', '数字', '云计算', '创新',
'生物', '医疗', '通信', '电子', '材料', '能源', '互联'
];
// 生成 sector_dataSectorDetails 组件需要的格式)
const sectorData = {};
let totalStocks = 0;
sectorNames.forEach((sectorName, sectorIdx) => {
const stockCount = Math.floor(Math.random() * 12) + 3; // 每个板块 3-15 只股票
const stocks = [];
for (let i = 0; i < stockCount; i++) {
const code = `${Math.random() > 0.5 ? '6' : '0'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`;
const continuousDays = Math.floor(Math.random() * 6) + 1; // 1-6连板
const ztHour = Math.floor(Math.random() * 5) + 9; // 9-13点
const ztMinute = Math.floor(Math.random() * 60);
const ztSecond = Math.floor(Math.random() * 60);
const ztTime = `2024-10-28 ${String(ztHour).padStart(2, '0')}:${String(ztMinute).padStart(2, '0')}:${String(ztSecond).padStart(2, '0')}`;
const stockName = `${stockNameTemplates[i % stockNameTemplates.length]}${sectorName === '公告' ? '公告' : ''}股份${i + 1}`;
stocks.push({
scode: code,
sname: stockName,
zt_time: ztTime,
formatted_time: `${String(ztHour).padStart(2, '0')}:${String(ztMinute).padStart(2, '0')}`,
continuous_days: continuousDays === 1 ? '首板' : `${continuousDays}连板`,
brief: `${sectorName}板块异动,${stockName}${sectorName === '公告' ? '重大公告利好' : '板块热点'}涨停。公司是${sectorName}行业龙头企业之一。`,
summary: `${sectorName}概念持续活跃`,
first_time: `2024-10-${String(28 - (continuousDays - 1)).padStart(2, '0')}`,
change_pct: parseFloat((Math.random() * 2 + 9).toFixed(2)), // 9%-11%
core_sectors: [
sectorName,
sectorNames[Math.floor(Math.random() * sectorNames.length)],
sectorNames[Math.floor(Math.random() * sectorNames.length)]
].filter((v, i, a) => a.indexOf(v) === i) // 去重
});
}
sectorData[sectorName] = {
count: stockCount,
stocks: stocks.sort((a, b) => a.zt_time.localeCompare(b.zt_time)) // 按涨停时间排序
};
totalStocks += stockCount;
});
// 统计数据
const morningCount = Math.floor(totalStocks * 0.35); // 早盘涨停
const middayCount = Math.floor(totalStocks * 0.25); // 午盘涨停
const afternoonCount = totalStocks - morningCount - middayCount; // 尾盘涨停
const announcementCount = sectorData['公告']?.count || 0;
const topSector = sectorNames.filter(s => s !== '公告' && s !== '其他')
.reduce((max, name) =>
(sectorData[name]?.count || 0) > (sectorData[max]?.count || 0) ? name : max
, '人工智能');
// 生成 chart_data板块分布饼图需要
const sortedSectors = Object.entries(sectorData)
.filter(([name]) => name !== '其他' && name !== '公告')
.sort((a, b) => b[1].count - a[1].count)
.slice(0, 10);
const chartData = {
labels: sortedSectors.map(([name]) => name),
counts: sortedSectors.map(([, info]) => info.count)
};
return {
date: date,
total_stocks: totalStocks,
total_sectors: Object.keys(sectorData).length,
sector_data: sectorData, // 👈 SectorDetails 组件需要的数据
chart_data: chartData, // 👈 板块分布饼图需要的数据
summary: {
top_sector: topSector,
top_sector_count: sectorData[topSector]?.count || 0,
announcement_stocks: announcementCount,
zt_time_distribution: {
morning: morningCount,
midday: middayCount,
afternoon: afternoonCount,
}
}
};
};
// ==================== 静态文件 Mock Handlers ====================
// 这些 handlers 用于拦截 /data/zt/* 静态文件请求
// 生成 dates.json 数据
const generateDatesJson = () => {
const dates = [];
const today = new Date();
for (let i = 0; i < 60; i++) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dayOfWeek = date.getDay();
// 跳过周末
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
dates.push({
date: `${year}${month}${day}`,
formatted_date: `${year}-${month}-${day}`,
count: Math.floor(Math.random() * 60) + 40 // 40-100 只涨停股票
});
if (dates.length >= 30) break;
}
}
return { dates };
};
// 生成每日分析 JSON 数据(用于 /data/zt/daily/${date}.json
const generateDailyJson = (date) => {
// 板块名称列表
const sectorNames = [
'公告', '人工智能', 'ChatGPT', '大模型', '算力',
'光伏', '新能源汽车', '锂电池', '储能', '充电桩',
'半导体', '芯片', '集成电路', '国产替代',
'医药', '创新药', 'CXO', '医疗器械',
'军工', '航空航天', '其他'
];
// 股票名称模板
const stockPrefixes = [
'龙头', '科技', '新能', '智能', '数字', '云计', '创新',
'生物', '医疗', '通信', '电子', '材料', '能源', '互联',
'天马', '华鑫', '中科', '东方', '西部', '南方', '北方',
'金龙', '银河', '星辰', '宏达', '盛世', '鹏程', '万里'
];
const stockSuffixes = [
'股份', '科技', '电子', '信息', '新材', '能源', '医药',
'通讯', '智造', '集团', '实业', '控股', '产业', '发展'
];
// 生成所有股票
const stocks = [];
const sectorData = {};
let stockIndex = 0;
sectorNames.forEach((sectorName, sectorIdx) => {
const stockCount = sectorName === '公告'
? Math.floor(Math.random() * 5) + 8 // 公告板块 8-12 只
: sectorName === '其他'
? Math.floor(Math.random() * 4) + 3 // 其他板块 3-6 只
: Math.floor(Math.random() * 8) + 3; // 普通板块 3-10 只
const sectorStockCodes = [];
for (let i = 0; i < stockCount; i++) {
const code = `${Math.random() > 0.6 ? '6' : Math.random() > 0.3 ? '0' : '3'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`;
const continuousDays = Math.floor(Math.random() * 6) + 1;
const ztHour = Math.floor(Math.random() * 4) + 9;
const ztMinute = Math.floor(Math.random() * 60);
const ztSecond = Math.floor(Math.random() * 60);
const prefix = stockPrefixes[Math.floor(Math.random() * stockPrefixes.length)];
const suffix = stockSuffixes[Math.floor(Math.random() * stockSuffixes.length)];
const stockName = `${prefix}${suffix}`;
// 生成关联板块
const coreSectors = [sectorName];
if (Math.random() > 0.5) {
const otherSector = sectorNames[Math.floor(Math.random() * (sectorNames.length - 1))];
if (otherSector !== sectorName && otherSector !== '其他' && otherSector !== '公告') {
coreSectors.push(otherSector);
}
}
stocks.push({
scode: code,
sname: stockName,
zt_time: `${date.slice(0,4)}-${date.slice(4,6)}-${date.slice(6,8)} ${String(ztHour).padStart(2,'0')}:${String(ztMinute).padStart(2,'0')}:${String(ztSecond).padStart(2,'0')}`,
formatted_time: `${String(ztHour).padStart(2,'0')}:${String(ztMinute).padStart(2,'0')}`,
continuous_days: continuousDays === 1 ? '首板' : `${continuousDays}连板`,
brief: sectorName === '公告'
? `${stockName}发布重大公告,公司拟收购资产/重组/增发等利好消息。`
: `${sectorName}板块异动,${stockName}因板块热点涨停。公司是${sectorName}行业核心标的。`,
summary: `${sectorName}概念活跃`,
first_time: `${date.slice(0,4)}-${date.slice(4,6)}-${String(parseInt(date.slice(6,8)) - (continuousDays - 1)).padStart(2,'0')}`,
change_pct: parseFloat((Math.random() * 1.5 + 9.5).toFixed(2)),
core_sectors: coreSectors
});
sectorStockCodes.push(code);
stockIndex++;
}
sectorData[sectorName] = {
count: stockCount,
stock_codes: sectorStockCodes
};
});
// 生成词频数据
const wordFreqData = [
{ name: '人工智能', value: Math.floor(Math.random() * 30) + 20 },
{ name: 'ChatGPT', value: Math.floor(Math.random() * 25) + 15 },
{ name: '大模型', value: Math.floor(Math.random() * 20) + 12 },
{ name: '算力', value: Math.floor(Math.random() * 18) + 10 },
{ name: '光伏', value: Math.floor(Math.random() * 15) + 10 },
{ name: '新能源', value: Math.floor(Math.random() * 15) + 8 },
{ name: '锂电池', value: Math.floor(Math.random() * 12) + 8 },
{ name: '储能', value: Math.floor(Math.random() * 12) + 6 },
{ name: '半导体', value: Math.floor(Math.random() * 15) + 10 },
{ name: '芯片', value: Math.floor(Math.random() * 15) + 8 },
{ name: '集成电路', value: Math.floor(Math.random() * 10) + 5 },
{ name: '国产替代', value: Math.floor(Math.random() * 10) + 5 },
{ name: '医药', value: Math.floor(Math.random() * 12) + 6 },
{ name: '创新药', value: Math.floor(Math.random() * 10) + 5 },
{ name: '医疗器械', value: Math.floor(Math.random() * 8) + 4 },
{ name: '军工', value: Math.floor(Math.random() * 10) + 5 },
{ name: '航空航天', value: Math.floor(Math.random() * 8) + 4 },
{ name: '数字经济', value: Math.floor(Math.random() * 12) + 6 },
{ name: '工业4.0', value: Math.floor(Math.random() * 8) + 4 },
{ name: '机器人', value: Math.floor(Math.random() * 10) + 5 },
{ name: '自动驾驶', value: Math.floor(Math.random() * 8) + 4 },
{ name: '元宇宙', value: Math.floor(Math.random() * 6) + 3 },
{ name: 'Web3.0', value: Math.floor(Math.random() * 5) + 2 },
{ name: '区块链', value: Math.floor(Math.random() * 5) + 2 },
];
// 生成 chart_data板块分布饼图需要
const sortedSectors = Object.entries(sectorData)
.filter(([name]) => name !== '其他' && name !== '公告')
.sort((a, b) => b[1].count - a[1].count)
.slice(0, 10);
const chartData = {
labels: sortedSectors.map(([name]) => name),
counts: sortedSectors.map(([, info]) => info.count)
};
// 时间分布(早盘、午盘、尾盘)
const morningCount = Math.floor(stocks.length * 0.35);
const middayCount = Math.floor(stocks.length * 0.25);
const afternoonCount = stocks.length - morningCount - middayCount;
return {
date: date,
total_stocks: stocks.length,
total_sectors: Object.keys(sectorData).length,
stocks: stocks,
sector_data: sectorData,
word_freq_data: wordFreqData,
chart_data: chartData, // 👈 板块分布饼图需要的数据
summary: {
top_sector: '人工智能',
top_sector_count: sectorData['人工智能']?.count || 0,
announcement_stocks: sectorData['公告']?.count || 0,
zt_time_distribution: {
morning: morningCount, // 早盘 9:30-11:30
midday: middayCount, // 午盘 11:30-13:00
afternoon: afternoonCount, // 尾盘 13:00-15:00
}
}
};
};
// 生成 stocks.jsonl 数据
const generateStocksJsonl = () => {
const stocks = [];
const today = new Date();
// 生成 200 只历史涨停股票记录
for (let i = 0; i < 200; i++) {
const daysAgo = Math.floor(Math.random() * 30);
const date = new Date(today);
date.setDate(date.getDate() - daysAgo);
// 跳过周末
while (date.getDay() === 0 || date.getDay() === 6) {
date.setDate(date.getDate() - 1);
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
stocks.push({
scode: `${Math.random() > 0.6 ? '6' : Math.random() > 0.3 ? '0' : '3'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`,
sname: ['龙头', '科技', '新能', '智能', '数字', '云计'][Math.floor(Math.random() * 6)] +
['股份', '科技', '电子', '信息', '新材'][Math.floor(Math.random() * 5)],
date: `${year}${month}${day}`,
formatted_date: `${year}-${month}-${day}`,
continuous_days: Math.floor(Math.random() * 5) + 1,
core_sectors: [['人工智能', 'ChatGPT', '光伏', '锂电池', '芯片'][Math.floor(Math.random() * 5)]]
});
}
return stocks;
};
// Mock Handlers
export const limitAnalyseHandlers = [
// ==================== 静态文件路径 Handlers ====================
// 1. /data/zt/dates.json - 可用日期列表
http.get('/data/zt/dates.json', async () => {
await delay(200);
console.log('[Mock LimitAnalyse] 获取 dates.json');
const data = generateDatesJson();
return HttpResponse.json(data);
}),
// 2. /data/zt/daily/:date.json - 每日分析数据
http.get('/data/zt/daily/:date', async ({ params }) => {
await delay(300);
// 移除 .json 后缀和查询参数
const dateParam = params.date.replace('.json', '').split('?')[0];
console.log('[Mock LimitAnalyse] 获取每日数据:', dateParam);
const data = generateDailyJson(dateParam);
return HttpResponse.json(data);
}),
// 3. /data/zt/stocks.jsonl - 股票列表(用于搜索)
http.get('/data/zt/stocks.jsonl', async () => {
await delay(200);
console.log('[Mock LimitAnalyse] 获取 stocks.jsonl');
const stocks = generateStocksJsonl();
// JSONL 格式:每行一个 JSON
const jsonl = stocks.map(s => JSON.stringify(s)).join('\n');
return new HttpResponse(jsonl, {
headers: { 'Content-Type': 'text/plain' }
});
}),
// ==================== API 路径 Handlers (兼容旧版本) ====================
// 1. 获取可用日期列表
http.get('http://111.198.58.126:5001/api/v1/dates/available', async () => {
await delay(300);
const availableDates = generateAvailableDates();
return HttpResponse.json({
success: true,
events: availableDates,
message: '可用日期列表获取成功',
});
}),
// 2. 获取每日分析数据
http.get('http://111.198.58.126:5001/api/v1/analysis/daily/:date', async ({ params }) => {
await delay(500);
const { date } = params;
const data = generateDailyAnalysis(date);
return HttpResponse.json({
success: true,
data: data,
message: `${date} 每日分析数据获取成功`,
});
}),
// 3. 获取词云数据
http.get('http://111.198.58.126:5001/api/v1/analysis/wordcloud/:date', async ({ params }) => {
await delay(300);
const { date } = params;
const wordCloudData = generateWordCloudData();
return HttpResponse.json({
success: true,
data: wordCloudData,
message: `${date} 词云数据获取成功`,
});
}),
// 4. 混合搜索POST
http.post('http://111.198.58.126:5001/api/v1/stocks/search/hybrid', async ({ request }) => {
await delay(400);
const body = await request.json();
const { query, type = 'all', mode = 'hybrid' } = body;
// 生成模拟搜索结果
const results = [];
const count = Math.floor(Math.random() * 10) + 5;
for (let i = 0; i < count; i++) {
results.push({
code: `${Math.random() > 0.5 ? '6' : '0'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`,
name: `${query || '搜索'}相关股票${i + 1}`,
sector: ['人工智能', 'ChatGPT', '新能源'][Math.floor(Math.random() * 3)],
limit_date: new Date().toISOString().split('T')[0].replace(/-/g, ''),
limit_time: `${Math.floor(Math.random() * 4) + 9}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}`,
price: (Math.random() * 100 + 10).toFixed(2),
change_pct: (Math.random() * 10).toFixed(2),
match_score: (Math.random() * 0.5 + 0.5).toFixed(2),
});
}
return HttpResponse.json({
success: true,
data: {
query: query,
type: type,
mode: mode,
results: results,
total: results.length,
},
message: '搜索完成',
});
}),
// 5. 获取高位股列表(涨停股票列表)
http.get('http://111.198.58.126:5001/api/limit-analyse/high-position-stocks', async ({ request }) => {
await delay(400);
const url = new URL(request.url);
const date = url.searchParams.get('date');
console.log('[Mock LimitAnalyse] 获取高位股列表:', { date });
const stocks = generateHighPositionStocks();
const statistics = generateHighPositionStatistics(stocks);
return HttpResponse.json({
success: true,
data: {
stocks: stocks,
statistics: statistics,
date: date,
},
message: '高位股数据获取成功',
});
}),
];

View File

@@ -1,965 +0,0 @@
// src/mocks/handlers/limitAnalyse.ts
// 涨停分析相关的 Mock Handlers
import { http, HttpResponse } from 'msw';
// ============ 类型定义 ============
/** 日期项 */
interface MockDateItem {
date: string;
formatted_date?: string;
count: number;
top_sector: string;
}
/** 板块股票 */
interface MockSectorStock {
code: string;
name: string;
latest_limit_time: string;
limit_up_count: number;
price: string;
change_pct: string;
turnover_rate: string;
volume: number;
amount: string;
limit_type: string;
封单金额: string;
}
/** 板块数据 */
interface MockSector {
sector_name: string;
stock_count: number;
avg_limit_time: string;
stocks: MockSectorStock[];
}
/** 高位股 */
interface MockHighPositionStock {
stock_code: string;
stock_name: string;
price: number;
increase_rate: number;
continuous_limit_up: number;
industry: string;
turnover_rate: number;
}
/** 高位股统计 */
interface MockHighPositionStatistics {
total_count: number;
avg_continuous_days: number;
max_continuous_days: number;
}
/** 词云项 */
interface MockWordCloudItem {
text: string;
value: number;
category: string;
}
/** 股票记录 */
interface MockStockRecord {
scode: string;
sname: string;
zt_time: string;
formatted_time: string;
continuous_days: string;
brief: string;
summary: string;
first_time: string;
change_pct: number;
core_sectors: string[];
}
/** 板块信息 */
interface MockSectorInfo {
count: number;
stocks?: MockStockRecord[];
stock_codes?: string[];
net_inflow?: number;
leading_stock?: string;
}
/** 板块关联节点 */
interface MockSectorNode {
id: string;
name: string;
value: number;
category: number;
symbolSize: number;
}
/** 板块关联边 */
interface MockSectorLink {
source: string;
target: string;
value: number;
}
/** 板块关联数据 */
interface MockSectorRelations {
nodes: MockSectorNode[];
links: MockSectorLink[];
}
/** 每日分析数据 */
interface MockDailyAnalysis {
date: string;
total_stocks: number;
total_sectors: number;
sector_data: Record<string, MockSectorInfo>;
chart_data: { labels: string[]; counts: number[] };
summary: {
top_sector: string;
top_sector_count: number;
announcement_stocks: number;
zt_time_distribution: {
morning: number;
midday: number;
afternoon: number;
};
};
}
/** 每日 JSON 数据(扩展版) */
interface MockDailyJson extends MockDailyAnalysis {
stocks: MockStockRecord[];
word_freq_data: { name: string; value: number }[];
sector_relations: MockSectorRelations;
}
/** 搜索结果项 */
interface MockSearchResult {
code: string;
name: string;
sector: string;
limit_date: string;
limit_time: string;
price: string;
change_pct: string;
match_score: string;
}
/** JSONL 股票记录 */
interface MockJsonlStock {
scode: string;
sname: string;
date: string;
formatted_date: string;
continuous_days: number;
core_sectors: string[];
}
// ============ 工具函数 ============
/** 模拟延迟 */
const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
/** 生成可用日期列表最近30个交易日 */
const generateAvailableDates = (): MockDateItem[] => {
const dates: MockDateItem[] = [];
const today = new Date();
let count = 0;
for (let i = 0; i < 60 && count < 30; i++) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dayOfWeek = date.getDay();
// 跳过周末
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const dateStr = `${year}${month}${day}`;
// 返回包含 date 和 count 字段的对象
// 生成 15-110 范围的涨停数,确保各阶段都有数据
// <40: 偏冷, >=40: 温和, >=60: 高涨, >=80: 超级高涨
let limitCount: number;
const rand = Math.random();
if (rand < 0.2) {
limitCount = Math.floor(Math.random() * 25) + 15; // 15-39 偏冷日
} else if (rand < 0.5) {
limitCount = Math.floor(Math.random() * 20) + 40; // 40-59 温和日
} else if (rand < 0.8) {
limitCount = Math.floor(Math.random() * 20) + 60; // 60-79 高涨日
} else {
limitCount = Math.floor(Math.random() * 30) + 80; // 80-109 超级高涨日
}
dates.push({
date: dateStr,
count: limitCount,
top_sector: ['人工智能', 'ChatGPT', '新能源', '半导体', '医药'][Math.floor(Math.random() * 5)],
});
count++;
}
}
return dates;
};
/** 生成板块数据 */
const generateSectors = (count = 8): MockSector[] => {
const sectorNames = [
'人工智能',
'ChatGPT',
'数字经济',
'新能源汽车',
'光伏',
'锂电池',
'半导体',
'芯片',
'5G通信',
'医疗器械',
'创新药',
'中药',
'白酒',
'食品饮料',
'消费电子',
'军工',
'航空航天',
'新材料',
];
const sectors: MockSector[] = [];
for (let i = 0; i < Math.min(count, sectorNames.length); i++) {
const stockCount = Math.floor(Math.random() * 15) + 5;
const stocks: MockSectorStock[] = [];
for (let j = 0; j < stockCount; j++) {
stocks.push({
code: `${Math.random() > 0.5 ? '6' : '0'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`,
name: `${sectorNames[i]}股票${j + 1}`,
latest_limit_time: `${Math.floor(Math.random() * 4) + 9}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}`,
limit_up_count: Math.floor(Math.random() * 3) + 1,
price: (Math.random() * 100 + 10).toFixed(2),
change_pct: (Math.random() * 5 + 5).toFixed(2),
turnover_rate: (Math.random() * 30 + 5).toFixed(2),
volume: Math.floor(Math.random() * 100000000 + 10000000),
amount: (Math.random() * 1000000000 + 100000000).toFixed(2),
limit_type: Math.random() > 0.7 ? '一字板' : Math.random() > 0.5 ? 'T字板' : '普通涨停',
: (Math.random() * 500000000).toFixed(2),
});
}
sectors.push({
sector_name: sectorNames[i],
stock_count: stockCount,
avg_limit_time: `${Math.floor(Math.random() * 2) + 10}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}`,
stocks: stocks,
});
}
return sectors;
};
/** 生成高位股数据(用于 HighPositionStocks 组件) */
const generateHighPositionStocks = (): MockHighPositionStock[] => {
const stocks: MockHighPositionStock[] = [];
const stockNames = [
'宁德时代',
'比亚迪',
'隆基绿能',
'东方财富',
'中际旭创',
'京东方A',
'海康威视',
'立讯精密',
'三一重工',
'恒瑞医药',
'三六零',
'东方通信',
'贵州茅台',
'五粮液',
'中国平安',
];
const industries = [
'锂电池',
'新能源汽车',
'光伏',
'金融科技',
'通信设备',
'显示器件',
'安防设备',
'电子元件',
'工程机械',
'医药制造',
'网络安全',
'通信服务',
'白酒',
'食品饮料',
'保险',
];
for (let i = 0; i < stockNames.length; i++) {
const code = `${Math.random() > 0.5 ? '6' : '0'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`;
const continuousDays = Math.floor(Math.random() * 8) + 2; // 2-9连板
const price = parseFloat((Math.random() * 100 + 20).toFixed(2));
const increaseRate = parseFloat((Math.random() * 3 + 8).toFixed(2)); // 8%-11%
const turnoverRate = parseFloat((Math.random() * 20 + 5).toFixed(2)); // 5%-25%
stocks.push({
stock_code: code,
stock_name: stockNames[i],
price: price,
increase_rate: increaseRate,
continuous_limit_up: continuousDays,
industry: industries[i],
turnover_rate: turnoverRate,
});
}
// 按连板天数降序排序
stocks.sort((a, b) => b.continuous_limit_up - a.continuous_limit_up);
return stocks;
};
/** 生成高位股统计数据 */
const generateHighPositionStatistics = (stocks: MockHighPositionStock[]): MockHighPositionStatistics => {
if (!stocks || stocks.length === 0) {
return {
total_count: 0,
avg_continuous_days: 0,
max_continuous_days: 0,
};
}
const totalCount = stocks.length;
const sumDays = stocks.reduce((sum, stock) => sum + stock.continuous_limit_up, 0);
const maxDays = Math.max(...stocks.map((s) => s.continuous_limit_up));
return {
total_count: totalCount,
avg_continuous_days: parseFloat((sumDays / totalCount).toFixed(1)),
max_continuous_days: maxDays,
};
};
/** 生成词云数据 */
const generateWordCloudData = (): MockWordCloudItem[] => {
const keywords = [
'人工智能',
'ChatGPT',
'AI芯片',
'大模型',
'算力',
'新能源',
'光伏',
'锂电池',
'储能',
'充电桩',
'半导体',
'芯片',
'EDA',
'国产替代',
'集成电路',
'医疗',
'创新药',
'CXO',
'医疗器械',
'生物医药',
'消费',
'白酒',
'食品',
'零售',
'餐饮',
'金融',
'券商',
'保险',
'银行',
'金融科技',
];
return keywords.map((keyword) => ({
text: keyword,
value: Math.floor(Math.random() * 50) + 10,
category: ['科技', '新能源', '医疗', '消费', '金融'][Math.floor(Math.random() * 5)],
}));
};
/** 生成每日分析数据 */
const generateDailyAnalysis = (date: string): MockDailyAnalysis => {
const sectorNames = [
'公告',
'人工智能',
'ChatGPT',
'数字经济',
'新能源汽车',
'光伏',
'锂电池',
'半导体',
'芯片',
'5G通信',
'医疗器械',
'创新药',
'其他',
];
const stockNameTemplates = ['龙头', '科技', '新能源', '智能', '数字', '云计算', '创新', '生物', '医疗', '通信', '电子', '材料', '能源', '互联'];
// 生成 sector_dataSectorDetails 组件需要的格式)
const sectorData: Record<string, MockSectorInfo> = {};
let totalStocks = 0;
sectorNames.forEach((sectorName) => {
const stockCount = Math.floor(Math.random() * 12) + 3; // 每个板块 3-15 只股票
const stocks: MockStockRecord[] = [];
for (let i = 0; i < stockCount; i++) {
const code = `${Math.random() > 0.5 ? '6' : '0'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`;
const continuousDays = Math.floor(Math.random() * 6) + 1; // 1-6连板
const ztHour = Math.floor(Math.random() * 5) + 9; // 9-13点
const ztMinute = Math.floor(Math.random() * 60);
const ztSecond = Math.floor(Math.random() * 60);
const ztTime = `2024-10-28 ${String(ztHour).padStart(2, '0')}:${String(ztMinute).padStart(2, '0')}:${String(ztSecond).padStart(2, '0')}`;
const stockName = `${stockNameTemplates[i % stockNameTemplates.length]}${sectorName === '公告' ? '公告' : ''}股份${i + 1}`;
stocks.push({
scode: code,
sname: stockName,
zt_time: ztTime,
formatted_time: `${String(ztHour).padStart(2, '0')}:${String(ztMinute).padStart(2, '0')}`,
continuous_days: continuousDays === 1 ? '首板' : `${continuousDays}连板`,
brief: `${sectorName}板块异动,${stockName}${sectorName === '公告' ? '重大公告利好' : '板块热点'}涨停。公司是${sectorName}行业龙头企业之一。`,
summary: `${sectorName}概念持续活跃`,
first_time: `2024-10-${String(28 - (continuousDays - 1)).padStart(2, '0')}`,
change_pct: parseFloat((Math.random() * 2 + 9).toFixed(2)), // 9%-11%
core_sectors: [sectorName, sectorNames[Math.floor(Math.random() * sectorNames.length)], sectorNames[Math.floor(Math.random() * sectorNames.length)]].filter(
(v, idx, a) => a.indexOf(v) === idx
), // 去重
});
}
sectorData[sectorName] = {
count: stockCount,
stocks: stocks.sort((a, b) => a.zt_time.localeCompare(b.zt_time)), // 按涨停时间排序
};
totalStocks += stockCount;
});
// 统计数据
const morningCount = Math.floor(totalStocks * 0.35); // 早盘涨停
const middayCount = Math.floor(totalStocks * 0.25); // 午盘涨停
const afternoonCount = totalStocks - morningCount - middayCount; // 尾盘涨停
const announcementCount = sectorData['公告']?.count || 0;
const topSector = sectorNames
.filter((s) => s !== '公告' && s !== '其他')
.reduce((max, name) => ((sectorData[name]?.count || 0) > (sectorData[max]?.count || 0) ? name : max), '人工智能');
// 生成 chart_data板块分布饼图需要
const sortedSectors = Object.entries(sectorData)
.filter(([name]) => name !== '其他' && name !== '公告')
.sort((a, b) => (b[1].count || 0) - (a[1].count || 0))
.slice(0, 10);
const chartData = {
labels: sortedSectors.map(([name]) => name),
counts: sortedSectors.map(([, info]) => info.count || 0),
};
return {
date: date,
total_stocks: totalStocks,
total_sectors: Object.keys(sectorData).length,
sector_data: sectorData, // 👈 SectorDetails 组件需要的数据
chart_data: chartData, // 👈 板块分布饼图需要的数据
summary: {
top_sector: topSector,
top_sector_count: sectorData[topSector]?.count || 0,
announcement_stocks: announcementCount,
zt_time_distribution: {
morning: morningCount,
midday: middayCount,
afternoon: afternoonCount,
},
},
};
};
// ==================== 静态文件 Mock Handlers ====================
// 这些 handlers 用于拦截 /data/zt/* 静态文件请求
/** 生成 dates.json 数据 */
const generateDatesJson = (): { dates: MockDateItem[] } => {
const dates: MockDateItem[] = [];
const today = new Date();
for (let i = 0; i < 60; i++) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dayOfWeek = date.getDay();
// 跳过周末
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
// 生成 15-110 范围的涨停数,确保各阶段都有数据
let limitCount: number;
const rand = Math.random();
if (rand < 0.2) {
limitCount = Math.floor(Math.random() * 25) + 15; // 15-39 偏冷日
} else if (rand < 0.5) {
limitCount = Math.floor(Math.random() * 20) + 40; // 40-59 温和日
} else if (rand < 0.8) {
limitCount = Math.floor(Math.random() * 20) + 60; // 60-79 高涨日
} else {
limitCount = Math.floor(Math.random() * 30) + 80; // 80-109 超级高涨日
}
dates.push({
date: `${year}${month}${day}`,
formatted_date: `${year}-${month}-${day}`,
count: limitCount,
top_sector: ['人工智能', 'ChatGPT', '新能源', '半导体', '医药'][Math.floor(Math.random() * 5)],
});
if (dates.length >= 30) break;
}
}
return { dates };
};
/** 生成板块关联数据 */
const generateSectorRelations = (sectorData: Record<string, MockSectorInfo>): MockSectorRelations => {
const sectors = Object.entries(sectorData)
.filter(([name]) => name !== '其他' && name !== '公告')
.slice(0, 12);
if (sectors.length === 0) return { nodes: [], links: [] };
// 板块分类映射
const categoryMap: Record<string, number> = {
人工智能: 0,
ChatGPT: 0,
大模型: 0,
算力: 0,
光伏: 1,
新能源汽车: 1,
锂电池: 1,
储能: 1,
充电桩: 1,
半导体: 2,
芯片: 2,
集成电路: 2,
国产替代: 2,
医药: 3,
创新药: 3,
CXO: 3,
医疗器械: 3,
军工: 4,
航空航天: 4,
};
const nodes: MockSectorNode[] = sectors.map(([name, info]) => ({
id: name,
name: name,
value: info.count || 0,
category: categoryMap[name] ?? 5,
symbolSize: Math.max(25, Math.min(60, (info.count || 0) * 3)),
}));
// 生成关联边(基于分类相似性和随机)
const links: MockSectorLink[] = [];
const sectorNames = sectors.map(([name]) => name);
for (let i = 0; i < sectorNames.length; i++) {
for (let j = i + 1; j < sectorNames.length; j++) {
const source = sectorNames[i];
const target = sectorNames[j];
const sameCategory = categoryMap[source] === categoryMap[target];
// 同类板块有更高概率关联
if (sameCategory || Math.random() > 0.7) {
links.push({
source,
target,
value: sameCategory ? 3 : 1,
});
}
}
}
return { nodes, links };
};
/** 生成每日分析 JSON 数据(用于 /data/zt/daily/${date}.json */
const generateDailyJson = (date: string): MockDailyJson => {
// 板块名称列表
const sectorNames = [
'公告',
'人工智能',
'ChatGPT',
'大模型',
'算力',
'光伏',
'新能源汽车',
'锂电池',
'储能',
'充电桩',
'半导体',
'芯片',
'集成电路',
'国产替代',
'医药',
'创新药',
'CXO',
'医疗器械',
'军工',
'航空航天',
'其他',
];
// 股票名称模板
const stockPrefixes = [
'龙头',
'科技',
'新能',
'智能',
'数字',
'云计',
'创新',
'生物',
'医疗',
'通信',
'电子',
'材料',
'能源',
'互联',
'天马',
'华鑫',
'中科',
'东方',
'西部',
'南方',
'北方',
'金龙',
'银河',
'星辰',
'宏达',
'盛世',
'鹏程',
'万里',
];
const stockSuffixes = ['股份', '科技', '电子', '信息', '新材', '能源', '医药', '通讯', '智造', '集团', '实业', '控股', '产业', '发展'];
// 生成所有股票
const stocks: MockStockRecord[] = [];
const sectorData: Record<string, MockSectorInfo> = {};
sectorNames.forEach((sectorName) => {
const stockCount =
sectorName === '公告'
? Math.floor(Math.random() * 5) + 8 // 公告板块 8-12 只
: sectorName === '其他'
? Math.floor(Math.random() * 4) + 3 // 其他板块 3-6 只
: Math.floor(Math.random() * 8) + 3; // 普通板块 3-10 只
const sectorStockCodes: string[] = [];
for (let i = 0; i < stockCount; i++) {
const code = `${Math.random() > 0.6 ? '6' : Math.random() > 0.3 ? '0' : '3'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`;
const continuousDays = Math.floor(Math.random() * 6) + 1;
const ztHour = Math.floor(Math.random() * 4) + 9;
const ztMinute = Math.floor(Math.random() * 60);
const ztSecond = Math.floor(Math.random() * 60);
const prefix = stockPrefixes[Math.floor(Math.random() * stockPrefixes.length)];
const suffix = stockSuffixes[Math.floor(Math.random() * stockSuffixes.length)];
const stockName = `${prefix}${suffix}`;
// 生成关联板块
const coreSectors = [sectorName];
if (Math.random() > 0.5) {
const otherSector = sectorNames[Math.floor(Math.random() * (sectorNames.length - 1))];
if (otherSector !== sectorName && otherSector !== '其他' && otherSector !== '公告') {
coreSectors.push(otherSector);
}
}
stocks.push({
scode: code,
sname: stockName,
zt_time: `${date.slice(0, 4)}-${date.slice(4, 6)}-${date.slice(6, 8)} ${String(ztHour).padStart(2, '0')}:${String(ztMinute).padStart(2, '0')}:${String(ztSecond).padStart(2, '0')}`,
formatted_time: `${String(ztHour).padStart(2, '0')}:${String(ztMinute).padStart(2, '0')}`,
continuous_days: continuousDays === 1 ? '首板' : `${continuousDays}连板`,
brief:
sectorName === '公告'
? `${stockName}发布重大公告,公司拟收购资产/重组/增发等利好消息。`
: `${sectorName}板块异动,${stockName}因板块热点涨停。公司是${sectorName}行业核心标的。`,
summary: `${sectorName}概念活跃`,
first_time: `${date.slice(0, 4)}-${date.slice(4, 6)}-${String(parseInt(date.slice(6, 8)) - (continuousDays - 1)).padStart(2, '0')}`,
change_pct: parseFloat((Math.random() * 1.5 + 9.5).toFixed(2)),
core_sectors: coreSectors,
});
sectorStockCodes.push(code);
}
// 添加净流入和领涨股数据
const netInflow = parseFloat((Math.random() * 30 - 10).toFixed(2));
const leadingStock = stocks.find((s) => s.core_sectors?.includes(sectorName))?.sname || stocks[0]?.sname || '-';
sectorData[sectorName] = {
count: stockCount,
stock_codes: sectorStockCodes,
net_inflow: netInflow,
leading_stock: leadingStock,
stocks: stocks.filter((s) => s.core_sectors?.includes(sectorName)).slice(0, 15),
};
});
// 生成词频数据
const wordFreqData = [
{ name: '人工智能', value: Math.floor(Math.random() * 30) + 20 },
{ name: 'ChatGPT', value: Math.floor(Math.random() * 25) + 15 },
{ name: '大模型', value: Math.floor(Math.random() * 20) + 12 },
{ name: '算力', value: Math.floor(Math.random() * 18) + 10 },
{ name: '光伏', value: Math.floor(Math.random() * 15) + 10 },
{ name: '新能源', value: Math.floor(Math.random() * 15) + 8 },
{ name: '锂电池', value: Math.floor(Math.random() * 12) + 8 },
{ name: '储能', value: Math.floor(Math.random() * 12) + 6 },
{ name: '半导体', value: Math.floor(Math.random() * 15) + 10 },
{ name: '芯片', value: Math.floor(Math.random() * 15) + 8 },
{ name: '集成电路', value: Math.floor(Math.random() * 10) + 5 },
{ name: '国产替代', value: Math.floor(Math.random() * 10) + 5 },
{ name: '医药', value: Math.floor(Math.random() * 12) + 6 },
{ name: '创新药', value: Math.floor(Math.random() * 10) + 5 },
{ name: '医疗器械', value: Math.floor(Math.random() * 8) + 4 },
{ name: '军工', value: Math.floor(Math.random() * 10) + 5 },
{ name: '航空航天', value: Math.floor(Math.random() * 8) + 4 },
{ name: '数字经济', value: Math.floor(Math.random() * 12) + 6 },
{ name: '工业4.0', value: Math.floor(Math.random() * 8) + 4 },
{ name: '机器人', value: Math.floor(Math.random() * 10) + 5 },
{ name: '自动驾驶', value: Math.floor(Math.random() * 8) + 4 },
{ name: '元宇宙', value: Math.floor(Math.random() * 6) + 3 },
{ name: 'Web3.0', value: Math.floor(Math.random() * 5) + 2 },
{ name: '区块链', value: Math.floor(Math.random() * 5) + 2 },
];
// 生成 chart_data板块分布饼图需要
const sortedSectors = Object.entries(sectorData)
.filter(([name]) => name !== '其他' && name !== '公告')
.sort((a, b) => (b[1].count || 0) - (a[1].count || 0))
.slice(0, 10);
const chartData = {
labels: sortedSectors.map(([name]) => name),
counts: sortedSectors.map(([, info]) => info.count || 0),
};
// 时间分布(早盘、午盘、尾盘)
const morningCount = Math.floor(stocks.length * 0.35);
const middayCount = Math.floor(stocks.length * 0.25);
const afternoonCount = stocks.length - morningCount - middayCount;
// 生成板块关联数据
const sectorRelations = generateSectorRelations(sectorData);
return {
date: date,
total_stocks: stocks.length,
total_sectors: Object.keys(sectorData).length,
stocks: stocks,
sector_data: sectorData,
word_freq_data: wordFreqData,
chart_data: chartData, // 👈 板块分布饼图需要的数据
sector_relations: sectorRelations, // 👈 板块关联网络图数据
summary: {
top_sector: '人工智能',
top_sector_count: sectorData['人工智能']?.count || 0,
announcement_stocks: sectorData['公告']?.count || 0,
zt_time_distribution: {
morning: morningCount, // 早盘 9:30-11:30
midday: middayCount, // 午盘 11:30-13:00
afternoon: afternoonCount, // 尾盘 13:00-15:00
},
},
};
};
/** 生成 stocks.jsonl 数据 */
const generateStocksJsonl = (): MockJsonlStock[] => {
const stocks: MockJsonlStock[] = [];
const today = new Date();
// 生成 200 只历史涨停股票记录
for (let i = 0; i < 200; i++) {
const daysAgo = Math.floor(Math.random() * 30);
const date = new Date(today);
date.setDate(date.getDate() - daysAgo);
// 跳过周末
while (date.getDay() === 0 || date.getDay() === 6) {
date.setDate(date.getDate() - 1);
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
stocks.push({
scode: `${Math.random() > 0.6 ? '6' : Math.random() > 0.3 ? '0' : '3'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`,
sname:
['龙头', '科技', '新能', '智能', '数字', '云计'][Math.floor(Math.random() * 6)] +
['股份', '科技', '电子', '信息', '新材'][Math.floor(Math.random() * 5)],
date: `${year}${month}${day}`,
formatted_date: `${year}-${month}-${day}`,
continuous_days: Math.floor(Math.random() * 5) + 1,
core_sectors: [['人工智能', 'ChatGPT', '光伏', '锂电池', '芯片'][Math.floor(Math.random() * 5)]],
});
}
return stocks;
};
// ============ Mock Handlers ============
export const limitAnalyseHandlers = [
// ==================== 静态文件路径 Handlers ====================
// 1. /data/zt/dates.json - 可用日期列表
http.get('/data/zt/dates.json', async () => {
await delay(200);
console.log('[Mock LimitAnalyse] 获取 dates.json');
const data = generateDatesJson();
return HttpResponse.json(data);
}),
// 2. /data/zt/daily/:date.json - 每日分析数据
http.get('/data/zt/daily/:date', async ({ params }) => {
await delay(300);
// 移除 .json 后缀和查询参数
const dateParam = (params.date as string).replace('.json', '').split('?')[0];
console.log('[Mock LimitAnalyse] 获取每日数据:', dateParam);
const data = generateDailyJson(dateParam);
return HttpResponse.json(data);
}),
// 3. /data/zt/stocks.jsonl - 股票列表(用于搜索)
http.get('/data/zt/stocks.jsonl', async () => {
await delay(200);
console.log('[Mock LimitAnalyse] 获取 stocks.jsonl');
const stocks = generateStocksJsonl();
// JSONL 格式:每行一个 JSON
const jsonl = stocks.map((s) => JSON.stringify(s)).join('\n');
return new HttpResponse(jsonl, {
headers: { 'Content-Type': 'text/plain' },
});
}),
// ==================== API 路径 Handlers (兼容旧版本) ====================
// 1. 获取可用日期列表
http.get('http://111.198.58.126:5001/api/v1/dates/available', async () => {
await delay(300);
const availableDates = generateAvailableDates();
return HttpResponse.json({
success: true,
events: availableDates,
message: '可用日期列表获取成功',
});
}),
// 2. 获取每日分析数据
http.get('http://111.198.58.126:5001/api/v1/analysis/daily/:date', async ({ params }) => {
await delay(500);
const { date } = params;
const data = generateDailyAnalysis(date as string);
return HttpResponse.json({
success: true,
data: data,
message: `${date} 每日分析数据获取成功`,
});
}),
// 3. 获取词云数据
http.get('http://111.198.58.126:5001/api/v1/analysis/wordcloud/:date', async ({ params }) => {
await delay(300);
const { date } = params;
const wordCloudData = generateWordCloudData();
return HttpResponse.json({
success: true,
data: wordCloudData,
message: `${date} 词云数据获取成功`,
});
}),
// 4. 混合搜索POST
http.post('http://111.198.58.126:5001/api/v1/stocks/search/hybrid', async ({ request }) => {
await delay(400);
const body = (await request.json()) as { query?: string; type?: string; mode?: string };
const { query, type = 'all', mode = 'hybrid' } = body;
// 生成模拟搜索结果
const results: MockSearchResult[] = [];
const count = Math.floor(Math.random() * 10) + 5;
for (let i = 0; i < count; i++) {
results.push({
code: `${Math.random() > 0.5 ? '6' : '0'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`,
name: `${query || '搜索'}相关股票${i + 1}`,
sector: ['人工智能', 'ChatGPT', '新能源'][Math.floor(Math.random() * 3)],
limit_date: new Date().toISOString().split('T')[0].replace(/-/g, ''),
limit_time: `${Math.floor(Math.random() * 4) + 9}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}`,
price: (Math.random() * 100 + 10).toFixed(2),
change_pct: (Math.random() * 10).toFixed(2),
match_score: (Math.random() * 0.5 + 0.5).toFixed(2),
});
}
return HttpResponse.json({
success: true,
data: {
query: query,
type: type,
mode: mode,
results: results,
total: results.length,
},
message: '搜索完成',
});
}),
// 5. 获取高位股列表(涨停股票列表)
http.get('http://111.198.58.126:5001/api/limit-analyse/high-position-stocks', async ({ request }) => {
await delay(400);
const url = new URL(request.url);
const date = url.searchParams.get('date');
console.log('[Mock LimitAnalyse] 获取高位股列表:', { date });
const stocks = generateHighPositionStocks();
const statistics = generateHighPositionStatistics(stocks);
return HttpResponse.json({
success: true,
data: {
stocks: stocks,
statistics: statistics,
date: date,
},
message: '高位股数据获取成功',
});
}),
];

View File

@@ -1,665 +0,0 @@
// src/mocks/handlers/market.js
// 市场行情相关的 Mock Handlers
import { http, HttpResponse } from 'msw';
import { generateMarketData } from '../data/market';
// 模拟延迟
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
export const marketHandlers = [
// 0. 指数实时行情数据
http.get('/api/index/:indexCode/realtime', async ({ params }) => {
await delay(100);
const { indexCode } = params;
console.log('[Mock] 获取指数实时行情, indexCode:', indexCode);
// 指数基础数据
const indexData = {
'000001': { name: '上证指数', basePrice: 3200, baseVolume: 3500 },
'399001': { name: '深证成指', basePrice: 10500, baseVolume: 4200 },
'399006': { name: '创业板指', basePrice: 2100, baseVolume: 1800 },
'000300': { name: '沪深300', basePrice: 3800, baseVolume: 2800 },
'000016': { name: '上证50', basePrice: 2600, baseVolume: 1200 },
'000905': { name: '中证500', basePrice: 5800, baseVolume: 1500 },
};
const baseData = indexData[indexCode] || { name: `指数${indexCode}`, basePrice: 3000, baseVolume: 2000 };
// 生成随机波动
const changePercent = parseFloat((Math.random() * 4 - 2).toFixed(2)); // -2% ~ +2%
const price = parseFloat((baseData.basePrice * (1 + changePercent / 100)).toFixed(2));
const change = parseFloat((price - baseData.basePrice).toFixed(2));
const volume = parseFloat((baseData.baseVolume * (0.8 + Math.random() * 0.4)).toFixed(2)); // 80%-120% of base
const amount = parseFloat((volume * price / 10000).toFixed(2)); // 亿元
return HttpResponse.json({
success: true,
data: {
index_code: indexCode,
index_name: baseData.name,
current_price: price,
change: change,
change_percent: changePercent,
open_price: parseFloat((baseData.basePrice * (1 + (Math.random() * 0.01 - 0.005))).toFixed(2)),
high_price: parseFloat((price * (1 + Math.random() * 0.01)).toFixed(2)),
low_price: parseFloat((price * (1 - Math.random() * 0.01)).toFixed(2)),
prev_close: baseData.basePrice,
volume: volume, // 亿手
amount: amount, // 亿元
update_time: new Date().toISOString(),
market_status: 'trading', // trading, closed, pre-market, after-hours
},
message: '获取成功'
});
}),
// 1. 成交数据
http.get('/api/market/trade/:stockCode', async ({ params, request }) => {
await delay(200);
const { stockCode } = params;
const data = generateMarketData(stockCode);
return HttpResponse.json(data.tradeData);
}),
// 2. 资金流向
http.get('/api/market/funding/:stockCode', async ({ params }) => {
await delay(200);
const { stockCode } = params;
const data = generateMarketData(stockCode);
return HttpResponse.json(data.fundingData);
}),
// 3. 大单统计
http.get('/api/market/bigdeal/:stockCode', async ({ params }) => {
await delay(200);
const { stockCode } = params;
const data = generateMarketData(stockCode);
return HttpResponse.json(data.bigDealData);
}),
// 4. 异动分析
http.get('/api/market/unusual/:stockCode', async ({ params }) => {
await delay(200);
const { stockCode } = params;
const data = generateMarketData(stockCode);
return HttpResponse.json(data.unusualData);
}),
// 5. 股权质押
http.get('/api/market/pledge/:stockCode', async ({ params }) => {
await delay(200);
const { stockCode } = params;
const data = generateMarketData(stockCode);
return HttpResponse.json(data.pledgeData);
}),
// 6. 市场摘要
http.get('/api/market/summary/:stockCode', async ({ params }) => {
await delay(200);
const { stockCode } = params;
const data = generateMarketData(stockCode);
return HttpResponse.json(data.summaryData);
}),
// 7. 涨停分析
http.get('/api/market/rise-analysis/:stockCode', async ({ params }) => {
await delay(200);
const { stockCode } = params;
const data = generateMarketData(stockCode);
return HttpResponse.json(data.riseAnalysisData);
}),
// 8. 最新分时数据
http.get('/api/stock/:stockCode/latest-minute', async ({ params }) => {
await delay(300);
const { stockCode } = params;
const data = generateMarketData(stockCode);
return HttpResponse.json(data.latestMinuteData);
}),
// 9. 热门概念数据(个股中心页面使用)
http.get('/api/concepts/daily-top', async ({ request }) => {
await delay(300);
const url = new URL(request.url);
const limit = parseInt(url.searchParams.get('limit') || '6');
const date = url.searchParams.get('date');
// 获取当前日期或指定日期
const tradeDate = date || new Date().toISOString().split('T')[0];
// 热门概念列表
const conceptPool = [
{ name: '人工智能', desc: '人工智能是"技术突破+政策扶持"双轮驱动的硬科技主题。随着大模型技术的突破AI应用场景不断拓展。', tags: ['AI大模型', '算力', '政策驱动'] },
{ name: '新能源汽车', desc: '新能源汽车行业景气度持续向好,渗透率不断提升。政策支持力度大,产业链上下游企业均受益。', tags: ['新能源', '汽车制造', '政策扶持'] },
{ name: '半导体', desc: '国产半导体替代加速,自主可控需求强烈。政策和资金支持力度大,行业迎来黄金发展期。', tags: ['芯片', '自主可控', '国产替代'] },
{ name: '光伏', desc: '光伏装机量快速增长,成本持续下降,行业景气度维持高位。双碳目标下前景广阔。', tags: ['清洁能源', '双碳', '装机量'] },
{ name: '锂电池', desc: '锂电池技术进步,成本优势扩大,下游应用领域持续扩张。新能源汽车和储能需求旺盛。', tags: ['储能', '新能源', '技术突破'] },
{ name: '储能', desc: '储能市场爆发式增长,政策支持力度大,应用场景不断拓展。未来市场空间巨大。', tags: ['新型储能', '电力系统', '政策驱动'] },
{ name: '算力', desc: 'AI大模型推动算力需求爆发数据中心、服务器、芯片等产业链受益明显。', tags: ['数据中心', 'AI算力', '服务器'] },
{ name: '机器人', desc: '人形机器人产业化加速,特斯拉、小米等巨头入局,产业链迎来发展机遇。', tags: ['人形机器人', '产业化', '巨头入局'] },
];
// 股票池(扩展到足够多的股票)
const stockPool = [
{ stock_code: '600519', stock_name: '贵州茅台' },
{ stock_code: '300750', stock_name: '宁德时代' },
{ stock_code: '601318', stock_name: '中国平安' },
{ stock_code: '002594', stock_name: '比亚迪' },
{ stock_code: '601012', stock_name: '隆基绿能' },
{ stock_code: '300274', stock_name: '阳光电源' },
{ stock_code: '688981', stock_name: '中芯国际' },
{ stock_code: '000725', stock_name: '京东方A' },
{ stock_code: '600036', stock_name: '招商银行' },
{ stock_code: '000858', stock_name: '五粮液' },
{ stock_code: '601166', stock_name: '兴业银行' },
{ stock_code: '600276', stock_name: '恒瑞医药' },
{ stock_code: '000333', stock_name: '美的集团' },
{ stock_code: '600887', stock_name: '伊利股份' },
{ stock_code: '002415', stock_name: '海康威视' },
{ stock_code: '601888', stock_name: '中国中免' },
{ stock_code: '300059', stock_name: '东方财富' },
{ stock_code: '002475', stock_name: '立讯精密' },
{ stock_code: '600900', stock_name: '长江电力' },
{ stock_code: '601398', stock_name: '工商银行' },
{ stock_code: '600030', stock_name: '中信证券' },
{ stock_code: '000568', stock_name: '泸州老窖' },
{ stock_code: '002352', stock_name: '顺丰控股' },
{ stock_code: '600809', stock_name: '山西汾酒' },
{ stock_code: '300015', stock_name: '爱尔眼科' },
{ stock_code: '002142', stock_name: '宁波银行' },
{ stock_code: '601899', stock_name: '紫金矿业' },
{ stock_code: '600309', stock_name: '万华化学' },
{ stock_code: '002304', stock_name: '洋河股份' },
{ stock_code: '600585', stock_name: '海螺水泥' },
{ stock_code: '601288', stock_name: '农业银行' },
{ stock_code: '600050', stock_name: '中国联通' },
{ stock_code: '000001', stock_name: '平安银行' },
{ stock_code: '601668', stock_name: '中国建筑' },
{ stock_code: '600028', stock_name: '中国石化' },
{ stock_code: '601857', stock_name: '中国石油' },
{ stock_code: '600000', stock_name: '浦发银行' },
{ stock_code: '601328', stock_name: '交通银行' },
{ stock_code: '000002', stock_name: '万科A' },
{ stock_code: '600104', stock_name: '上汽集团' },
{ stock_code: '601601', stock_name: '中国太保' },
{ stock_code: '600016', stock_name: '民生银行' },
{ stock_code: '601628', stock_name: '中国人寿' },
{ stock_code: '600031', stock_name: '三一重工' },
{ stock_code: '002230', stock_name: '科大讯飞' },
{ stock_code: '300124', stock_name: '汇川技术' },
{ stock_code: '002049', stock_name: '紫光国微' },
{ stock_code: '688012', stock_name: '中微公司' },
{ stock_code: '688008', stock_name: '澜起科技' },
{ stock_code: '603501', stock_name: '韦尔股份' },
];
// 生成历史触发时间
const generateHappenedTimes = (seed) => {
const times = [];
const count = 3 + (seed % 3); // 3-5个时间点
for (let k = 0; k < count; k++) {
const daysAgo = 30 + (seed * 7 + k * 11) % 330;
const d = new Date();
d.setDate(d.getDate() - daysAgo);
times.push(d.toISOString().split('T')[0]);
}
return times.sort().reverse();
};
const matchTypes = ['hybrid_knn', 'keyword', 'semantic'];
// 生成概念数据
const concepts = [];
for (let i = 0; i < Math.min(limit, conceptPool.length); i++) {
const concept = conceptPool[i];
const changePercent = parseFloat((Math.random() * 8 - 1).toFixed(2)); // -1% ~ 7%
const stockCount = Math.floor(Math.random() * 20) + 15; // 15-35只股票
// 生成与 stockCount 一致的股票列表(包含完整字段)
const relatedStocks = [];
for (let j = 0; j < stockCount; j++) {
const idx = (i * 7 + j) % stockPool.length;
const stock = stockPool[idx];
relatedStocks.push({
stock_code: stock.stock_code,
stock_name: stock.stock_name,
reason: `作为行业龙头企业,${stock.stock_name}在该领域具有核心竞争优势,市场份额领先。`,
change_pct: parseFloat((Math.random() * 15 - 5).toFixed(2)) // -5% ~ +10%
});
}
concepts.push({
concept_id: `CONCEPT_${1001 + i}`,
concept: concept.name, // 原始字段名
concept_name: concept.name, // 兼容字段名
description: concept.desc,
stock_count: stockCount,
score: parseFloat((Math.random() * 5 + 3).toFixed(2)), // 3-8 分数
match_type: matchTypes[i % 3],
price_info: {
avg_change_pct: changePercent,
avg_price: parseFloat((Math.random() * 100 + 10).toFixed(2)),
total_market_cap: parseFloat((Math.random() * 1000 + 100).toFixed(2))
},
change_percent: changePercent, // 兼容字段
happened_times: generateHappenedTimes(i),
outbreak_dates: generateHappenedTimes(i).slice(0, 2), // 爆发日期
tags: concept.tags || [], // 标签
stocks: relatedStocks,
hot_score: Math.floor(Math.random() * 100)
});
}
// 按涨跌幅降序排序
concepts.sort((a, b) => b.change_percent - a.change_percent);
console.log('[Mock Market] 获取热门概念:', { limit, date: tradeDate, count: concepts.length });
return HttpResponse.json({
success: true,
data: concepts,
trade_date: tradeDate
});
}),
// 10. 市值热力图数据(个股中心页面使用)
http.get('/api/market/heatmap', async ({ request }) => {
await delay(400);
const url = new URL(request.url);
const limit = parseInt(url.searchParams.get('limit') || '500');
const date = url.searchParams.get('date');
const tradeDate = date || new Date().toISOString().split('T')[0];
// 行业列表
const industries = ['食品饮料', '银行', '医药生物', '电子', '计算机', '汽车', '电力设备', '机械设备', '化工', '房地产', '有色金属', '钢铁'];
const provinces = ['北京', '上海', '广东', '浙江', '江苏', '山东', '四川', '湖北', '福建', '安徽'];
// 常见股票数据
const majorStocks = [
{ code: '600519', name: '贵州茅台', cap: 1850, industry: '食品饮料', province: '贵州' },
{ code: '601318', name: '中国平安', cap: 920, industry: '保险', province: '广东' },
{ code: '600036', name: '招商银行', cap: 850, industry: '银行', province: '广东' },
{ code: '300750', name: '宁德时代', cap: 780, industry: '电力设备', province: '福建' },
{ code: '601166', name: '兴业银行', cap: 420, industry: '银行', province: '福建' },
{ code: '000858', name: '五粮液', cap: 580, industry: '食品饮料', province: '四川' },
{ code: '002594', name: '比亚迪', cap: 650, industry: '汽车', province: '广东' },
{ code: '601012', name: '隆基绿能', cap: 320, industry: '电力设备', province: '陕西' },
{ code: '688981', name: '中芯国际', cap: 280, industry: '电子', province: '上海' },
{ code: '600900', name: '长江电力', cap: 520, industry: '公用事业', province: '湖北' },
];
// 生成热力图数据
const heatmapData = [];
let risingCount = 0;
let fallingCount = 0;
// 涨停股票名称池(模拟真实市场)
const limitUpNames = ['东方财富', '科大讯飞', '中科曙光', '寒武纪', '金山办公', '同花顺', '三六零', '紫光股份'];
const limitDownNames = ['某ST股A', '某ST股B'];
// 先添加一些涨停股票10%
for (let i = 0; i < 8; i++) {
const changePercent = parseFloat((9.9 + Math.random() * 0.15).toFixed(2)); // 9.9% ~ 10.05%(涨停)
risingCount++;
heatmapData.push({
stock_code: `00${3000 + i}`,
stock_name: limitUpNames[i] || `涨停股${i}`,
market_cap: parseFloat((Math.random() * 300 + 50).toFixed(2)),
change_percent: changePercent,
amount: parseFloat((Math.random() * 30 + 5).toFixed(2)),
industry: industries[Math.floor(Math.random() * industries.length)],
province: provinces[Math.floor(Math.random() * provinces.length)]
});
}
// 添加一些跌停股票(-10%
for (let i = 0; i < 2; i++) {
const changePercent = parseFloat((-9.9 - Math.random() * 0.15).toFixed(2)); // -10.05% ~ -9.9%(跌停)
fallingCount++;
heatmapData.push({
stock_code: `00${2000 + i}`,
stock_name: limitDownNames[i] || `跌停股${i}`,
market_cap: parseFloat((Math.random() * 50 + 10).toFixed(2)),
change_percent: changePercent,
amount: parseFloat((Math.random() * 5 + 0.5).toFixed(2)),
industry: industries[Math.floor(Math.random() * industries.length)],
province: provinces[Math.floor(Math.random() * provinces.length)]
});
}
// 添加主要股票
majorStocks.forEach(stock => {
const changePercent = parseFloat((Math.random() * 12 - 4).toFixed(2)); // -4% ~ 8%
const amount = parseFloat((Math.random() * 100 + 10).toFixed(2)); // 10-110亿
if (changePercent > 0) risingCount++;
else if (changePercent < 0) fallingCount++;
heatmapData.push({
stock_code: stock.code,
stock_name: stock.name,
market_cap: stock.cap,
change_percent: changePercent,
amount: amount,
industry: stock.industry,
province: stock.province
});
});
// 生成更多随机股票数据
for (let i = majorStocks.length; i < Math.min(limit, 200); i++) {
const changePercent = parseFloat((Math.random() * 14 - 5).toFixed(2)); // -5% ~ 9%
const marketCap = parseFloat((Math.random() * 500 + 20).toFixed(2)); // 20-520亿
const amount = parseFloat((Math.random() * 50 + 1).toFixed(2)); // 1-51亿
if (changePercent > 0) risingCount++;
else if (changePercent < 0) fallingCount++;
heatmapData.push({
stock_code: `${600000 + i}`,
stock_name: `股票${i}`,
market_cap: marketCap,
change_percent: changePercent,
amount: amount,
industry: industries[Math.floor(Math.random() * industries.length)],
province: provinces[Math.floor(Math.random() * provinces.length)]
});
}
console.log('[Mock Market] 获取热力图数据:', { limit, date: tradeDate, count: heatmapData.length });
return HttpResponse.json({
success: true,
data: heatmapData,
trade_date: tradeDate,
statistics: {
rising_count: risingCount,
falling_count: fallingCount
}
});
}),
// 11. 热点概览数据(大盘分时 + 概念异动)
http.get('/api/market/hotspot-overview', async ({ request }) => {
await delay(300);
const url = new URL(request.url);
const date = url.searchParams.get('date');
const tradeDate = date || new Date().toISOString().split('T')[0];
// 生成分时数据240个点9:30-11:30 + 13:00-15:00
const timeline = [];
const basePrice = 3900 + Math.random() * 100; // 基准价格 3900-4000
const prevClose = basePrice;
let currentPrice = basePrice;
let cumulativeVolume = 0;
// 上午时段 9:30-11:30 (120分钟)
for (let i = 0; i < 120; i++) {
const hour = 9 + Math.floor((i + 30) / 60);
const minute = (i + 30) % 60;
const time = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
// 模拟价格波动
const volatility = 0.002; // 0.2%波动
const drift = (Math.random() - 0.5) * 0.001; // 微小趋势
currentPrice = currentPrice * (1 + (Math.random() - 0.5) * volatility + drift);
const volume = Math.floor(Math.random() * 500000 + 100000); // 成交量
cumulativeVolume += volume;
timeline.push({
time,
price: parseFloat(currentPrice.toFixed(2)),
volume: cumulativeVolume,
change_pct: parseFloat(((currentPrice - prevClose) / prevClose * 100).toFixed(2))
});
}
// 下午时段 13:00-15:00 (120分钟)
for (let i = 0; i < 120; i++) {
const hour = 13 + Math.floor(i / 60);
const minute = i % 60;
const time = `${hour.toString().padStart(2, '0')}:${minute.toString().padStart(2, '0')}`;
// 下午波动略小
const volatility = 0.0015;
const drift = (Math.random() - 0.5) * 0.0008;
currentPrice = currentPrice * (1 + (Math.random() - 0.5) * volatility + drift);
const volume = Math.floor(Math.random() * 400000 + 80000);
cumulativeVolume += volume;
timeline.push({
time,
price: parseFloat(currentPrice.toFixed(2)),
volume: cumulativeVolume,
change_pct: parseFloat(((currentPrice - prevClose) / prevClose * 100).toFixed(2))
});
}
// 生成概念异动数据
const conceptNames = [
'人工智能', 'AI眼镜', '机器人', '核电', '国企', '卫星导航',
'福建自贸区', '两岸融合', 'CRO', '三季报增长', '百货零售',
'人形机器人', '央企', '数据中心', 'CPO', '新能源', '电网设备',
'氢能源', '算力租赁', '厦门国资', '乳业', '低空安防', '创新药',
'商业航天', '控制权变更', '文化传媒', '海峡两岸'
];
const alertTypes = ['surge_up', 'surge_down', 'volume_spike', 'limit_up', 'rank_jump'];
// 生成 12-18 个异动(减少数量,更符合实际)
const alertCount = Math.floor(Math.random() * 6) + 12;
const alerts = [];
const usedTimeGroups = new Set(); // 记录已使用的10分钟时间段
// 获取10分钟时间段
const getTimeGroup = (timeStr) => {
const [h, m] = timeStr.split(':').map(Number);
const groupStart = Math.floor(m / 10) * 10;
return `${h}:${groupStart < 10 ? '0' : ''}${groupStart}`;
};
for (let i = 0; i < alertCount; i++) {
// 随机选择一个时间点尽量分散到不同10分钟时间段
let timeIdx;
let attempts = 0;
let timeGroup;
do {
timeIdx = Math.floor(Math.random() * timeline.length);
timeGroup = getTimeGroup(timeline[timeIdx].time);
attempts++;
// 允许同一时间段最多2个异动
} while (usedTimeGroups.has(timeGroup) && Math.random() > 0.3 && attempts < 50);
if (attempts >= 50) continue;
usedTimeGroups.add(timeGroup);
const time = timeline[timeIdx].time;
const conceptName = conceptNames[Math.floor(Math.random() * conceptNames.length)];
const alertType = alertTypes[Math.floor(Math.random() * alertTypes.length)];
// 根据类型生成 alpha
let alpha;
if (alertType === 'surge_up') {
alpha = parseFloat((Math.random() * 3 + 2).toFixed(2)); // +2% ~ +5%
} else if (alertType === 'surge_down') {
alpha = parseFloat((-Math.random() * 3 - 1.5).toFixed(2)); // -1.5% ~ -4.5%
} else {
alpha = parseFloat((Math.random() * 4 - 1).toFixed(2)); // -1% ~ +3%
}
const finalScore = Math.floor(Math.random() * 40 + 45); // 45-85分
const ruleScore = Math.floor(Math.random() * 30 + 40);
const mlScore = Math.floor(Math.random() * 30 + 40);
alerts.push({
concept_id: `CONCEPT_${1000 + i}`,
concept_name: conceptName,
time,
alert_type: alertType,
alpha,
alpha_delta: parseFloat((Math.random() * 2 - 0.5).toFixed(2)),
amt_ratio: parseFloat((Math.random() * 5 + 1).toFixed(2)),
limit_up_count: alertType === 'limit_up' ? Math.floor(Math.random() * 5 + 1) : 0,
limit_up_ratio: parseFloat((Math.random() * 0.3).toFixed(3)),
final_score: finalScore,
rule_score: ruleScore,
ml_score: mlScore,
trigger_reason: finalScore >= 65 ? '规则强信号' : (mlScore >= 70 ? 'ML强信号' : '融合触发'),
importance_score: parseFloat((finalScore / 100).toFixed(2)),
index_price: timeline[timeIdx].price
});
}
// 按时间排序
alerts.sort((a, b) => a.time.localeCompare(b.time));
// 统计异动类型
const alertSummary = alerts.reduce((acc, alert) => {
acc[alert.alert_type] = (acc[alert.alert_type] || 0) + 1;
return acc;
}, {});
// 计算指数统计
const prices = timeline.map(t => t.price);
const latestPrice = prices[prices.length - 1];
const highPrice = Math.max(...prices);
const lowPrice = Math.min(...prices);
const changePct = ((latestPrice - prevClose) / prevClose * 100);
console.log('[Mock Market] 获取热点概览数据:', {
date: tradeDate,
timelinePoints: timeline.length,
alertCount: alerts.length
});
return HttpResponse.json({
success: true,
data: {
index: {
code: '000001.SH',
name: '上证指数',
latest_price: latestPrice,
prev_close: prevClose,
high: highPrice,
low: lowPrice,
change_pct: parseFloat(changePct.toFixed(2)),
timeline
},
alerts,
alert_summary: alertSummary
},
trade_date: tradeDate
});
}),
// 12. 市场概况数据(投资仪表盘使用)- 上证/深证/总市值/成交额
http.get('/api/market/summary', async () => {
await delay(150);
// 生成实时数据(基于当前时间产生小波动)
const now = new Date();
const seed = now.getHours() * 60 + now.getMinutes();
// 上证指数(基准 3400
const shBasePrice = 3400;
const shChange = parseFloat(((Math.sin(seed / 30) + Math.random() - 0.5) * 2).toFixed(2));
const shPrice = parseFloat((shBasePrice * (1 + shChange / 100)).toFixed(2));
const shChangeAmount = parseFloat((shPrice - shBasePrice).toFixed(2));
// 深证指数(基准 10800
const szBasePrice = 10800;
const szChange = parseFloat(((Math.sin(seed / 25) + Math.random() - 0.5) * 2.5).toFixed(2));
const szPrice = parseFloat((szBasePrice * (1 + szChange / 100)).toFixed(2));
const szChangeAmount = parseFloat((szPrice - szBasePrice).toFixed(2));
// 总市值(约 100-110 万亿波动)
const totalMarketCap = parseFloat((105 + (Math.sin(seed / 60) * 5)).toFixed(1)) * 1000000000000;
// 成交额(约 0.8-1.5 万亿波动)
const turnover = parseFloat((1.0 + (Math.random() * 0.5 - 0.2)).toFixed(2)) * 1000000000000;
console.log('[Mock Market] 获取市场概况数据');
return HttpResponse.json({
success: true,
data: {
shanghai: {
value: shPrice,
change: shChange,
changeAmount: shChangeAmount,
},
shenzhen: {
value: szPrice,
change: szChange,
changeAmount: szChangeAmount,
},
totalMarketCap,
turnover,
updateTime: now.toISOString(),
},
});
}),
// 13. 市场统计数据(个股中心页面使用)
http.get('/api/market/statistics', async ({ request }) => {
await delay(200);
const url = new URL(request.url);
const date = url.searchParams.get('date');
const tradeDate = date || new Date().toISOString().split('T')[0];
// 生成最近30个交易日
const availableDates = [];
const currentDate = new Date(tradeDate);
for (let i = 0; i < 30; i++) {
const d = new Date(currentDate);
d.setDate(d.getDate() - i);
// 跳过周末
if (d.getDay() !== 0 && d.getDay() !== 6) {
availableDates.push(d.toISOString().split('T')[0]);
}
}
console.log('[Mock Market] 获取市场统计数据:', { date: tradeDate });
// 生成今日数据
const todayMarketCap = parseFloat((Math.random() * 5000 + 80000).toFixed(2));
const todayAmount = parseFloat((Math.random() * 3000 + 8000).toFixed(2));
const todayRising = Math.floor(Math.random() * 1500 + 1500);
const todayFalling = Math.floor(Math.random() * 1500 + 1000);
// 生成昨日数据(用于对比)
const yesterdayMarketCap = parseFloat((todayMarketCap * (0.98 + Math.random() * 0.04)).toFixed(2));
const yesterdayAmount = parseFloat((todayAmount * (0.85 + Math.random() * 0.3)).toFixed(2));
const yesterdayRising = Math.floor(todayRising * (0.7 + Math.random() * 0.6));
const yesterdayFalling = Math.floor(todayFalling * (0.7 + Math.random() * 0.6));
return HttpResponse.json({
success: true,
summary: {
total_market_cap: todayMarketCap,
total_amount: todayAmount,
avg_pe: parseFloat((Math.random() * 5 + 12).toFixed(2)),
avg_pb: parseFloat((Math.random() * 0.5 + 1.3).toFixed(2)),
rising_stocks: todayRising,
falling_stocks: todayFalling,
unchanged_stocks: Math.floor(Math.random() * 200 + 100)
},
// 昨日对比数据
yesterday: {
total_market_cap: yesterdayMarketCap,
total_amount: yesterdayAmount,
rising_stocks: yesterdayRising,
falling_stocks: yesterdayFalling
},
trade_date: tradeDate,
available_dates: availableDates.slice(0, 20)
});
}),
];

View File

@@ -1,242 +0,0 @@
// src/mocks/handlers/payment.js
import { http, HttpResponse, delay } from 'msw';
import { getCurrentUser } from '../data/users';
// 模拟网络延迟(毫秒)
const NETWORK_DELAY = 500;
// 模拟订单数据存储
const mockOrders = new Map();
let orderIdCounter = 1000;
export const paymentHandlers = [
// ==================== 支付订单管理 ====================
// 1. 创建支付订单
http.post('/api/payment/create-order', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '未登录'
}, { status: 401 });
}
const body = await request.json();
const { plan_name, billing_cycle } = body;
console.log('[Mock] 创建支付订单:', { plan_name, billing_cycle, user: currentUser.id });
if (!plan_name || !billing_cycle) {
return HttpResponse.json({
success: false,
error: '参数不完整'
}, { status: 400 });
}
// 模拟价格
const prices = {
pro: { monthly: 0.01, yearly: 0.08 },
max: { monthly: 0.1, yearly: 0.8 }
};
const amount = prices[plan_name]?.[billing_cycle] || 0.01;
// 创建订单
const orderId = `ORDER_${orderIdCounter++}_${Date.now()}`;
const order = {
id: orderId,
user_id: currentUser.id,
plan_name,
billing_cycle,
amount,
status: 'pending',
qr_code_url: `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=weixin://wxpay/bizpayurl?pr=mock_${orderId}`,
created_at: new Date().toISOString(),
expires_at: new Date(Date.now() + 30 * 60 * 1000).toISOString() // 30分钟后过期
};
mockOrders.set(orderId, order);
console.log('[Mock] 订单创建成功:', order);
// 模拟5秒后自动支付成功方便测试
setTimeout(() => {
const existingOrder = mockOrders.get(orderId);
if (existingOrder && existingOrder.status === 'pending') {
existingOrder.status = 'paid';
existingOrder.paid_at = new Date().toISOString();
console.log(`[Mock] 订单自动支付成功: ${orderId}`);
}
}, 5000);
return HttpResponse.json({
success: true,
data: order
});
}),
// 2. 查询订单状态
http.get('/api/payment/order-status/:orderId', async ({ params }) => {
await delay(300);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '未登录'
}, { status: 401 });
}
const { orderId } = params;
const order = mockOrders.get(orderId);
console.log('[Mock] 查询订单状态:', { orderId, found: !!order });
if (!order) {
return HttpResponse.json({
success: false,
error: '订单不存在'
}, { status: 404 });
}
if (order.user_id !== currentUser.id) {
return HttpResponse.json({
success: false,
error: '无权访问此订单'
}, { status: 403 });
}
return HttpResponse.json({
success: true,
data: order
});
}),
// 3. 获取用户订单列表
http.get('/api/payment/orders', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '未登录'
}, { status: 401 });
}
const userOrders = Array.from(mockOrders.values())
.filter(order => order.user_id === currentUser.id)
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
console.log('[Mock] 获取用户订单列表:', { count: userOrders.length });
return HttpResponse.json({
success: true,
data: userOrders
});
}),
// 4. 取消订单
http.post('/api/payment/cancel-order', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '未登录'
}, { status: 401 });
}
const body = await request.json();
const { order_id } = body;
const order = mockOrders.get(order_id);
if (!order) {
return HttpResponse.json({
success: false,
error: '订单不存在'
}, { status: 404 });
}
if (order.user_id !== currentUser.id) {
return HttpResponse.json({
success: false,
error: '无权操作此订单'
}, { status: 403 });
}
if (order.status !== 'pending') {
return HttpResponse.json({
success: false,
error: '只能取消待支付的订单'
}, { status: 400 });
}
order.status = 'cancelled';
order.cancelled_at = new Date().toISOString();
console.log('[Mock] 订单已取消:', order_id);
return HttpResponse.json({
success: true,
message: '订单已取消'
});
}),
// ==================== 微信 JSAPI 支付 ====================
// 5. 创建 JSAPI 支付订单(小程序 H5 支付)
http.post('/api/payment/wechat/jsapi/create-order', async ({ request }) => {
await delay(NETWORK_DELAY);
const body = await request.json();
const { plan_id, user_id, openid } = body;
console.log('[Mock] 创建 JSAPI 支付订单:', { plan_id, user_id, openid });
if (!plan_id || !user_id || !openid) {
return HttpResponse.json({
success: false,
error: '参数不完整:需要 plan_id, user_id, openid'
}, { status: 400 });
}
// 模拟套餐价格
const planPrices = {
'pro_monthly': { name: 'Pro 月度会员', amount: 29.9 },
'pro_yearly': { name: 'Pro 年度会员', amount: 299 },
'max_monthly': { name: 'Max 月度会员', amount: 99 },
'max_yearly': { name: 'Max 年度会员', amount: 999 },
};
const plan = planPrices[plan_id] || { name: '测试套餐', amount: 0.01 };
// 创建订单
const orderNo = `JSAPI_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
// 模拟微信支付参数(实际由后端生成)
const paymentParams = {
appId: 'wx0edeaab76d4fa414',
timeStamp: String(Math.floor(Date.now() / 1000)),
nonceStr: Math.random().toString(36).slice(2, 18),
package: `prepay_id=mock_prepay_${orderNo}`,
signType: 'MD5',
paySign: 'MOCK_SIGN_' + Math.random().toString(36).slice(2, 10).toUpperCase(),
};
console.log('[Mock] JSAPI 订单创建成功:', { orderNo, plan: plan.name, amount: plan.amount });
return HttpResponse.json({
success: true,
order_no: orderNo,
plan_name: plan.name,
amount: plan.amount,
payment_params: paymentParams,
});
})
];

View File

@@ -1,132 +0,0 @@
// src/mocks/handlers/posthog.js
// PostHog 埋点请求 Mock Handler
import { http, HttpResponse } from 'msw';
/**
* PostHog 埋点 Mock Handler
* 拦截所有发往 PostHog 的埋点请求,避免在 Mock 模式下产生 500 错误
*/
export const posthogHandlers = [
// PostHog 事件追踪接口
http.post('https://us.i.posthog.com/e/', async ({ request }) => {
try {
// 读取埋点数据(可选,用于调试)
const body = await request.text();
// 开发环境输出埋点日志(可选,方便调试)
if (process.env.NODE_ENV === 'development' && process.env.REACT_APP_LOG_POSTHOG === 'true') {
console.log('[Mock] PostHog 埋点请求:', {
url: request.url,
bodyPreview: body.substring(0, 150) + (body.length > 150 ? '...' : ''),
});
}
// 返回成功响应(模拟 PostHog 服务器响应)
return HttpResponse.json(
{ status: 1 },
{ status: 200 }
);
} catch (error) {
console.error('[Mock] PostHog handler error:', error);
return HttpResponse.json(
{ status: 0, error: 'Mock handler error' },
{ status: 500 }
);
}
}),
// PostHog batch 批量事件追踪接口(可选)
http.post('https://us.i.posthog.com/batch/', async ({ request }) => {
try {
const body = await request.text();
if (process.env.NODE_ENV === 'development' && process.env.REACT_APP_LOG_POSTHOG === 'true') {
console.log('[Mock] PostHog 批量埋点请求:', {
url: request.url,
bodyPreview: body.substring(0, 150) + (body.length > 150 ? '...' : ''),
});
}
return HttpResponse.json(
{ status: 1 },
{ status: 200 }
);
} catch (error) {
console.error('[Mock] PostHog batch handler error:', error);
return HttpResponse.json(
{ status: 0, error: 'Mock handler error' },
{ status: 500 }
);
}
}),
// PostHog decide 接口(功能开关、特性标志)
http.post('https://us.i.posthog.com/decide/', async ({ request }) => {
try {
if (process.env.NODE_ENV === 'development' && process.env.REACT_APP_LOG_POSTHOG === 'true') {
const body = await request.json();
console.log('[Mock] PostHog decide 请求:', body);
}
// 返回空的特性标志配置
return HttpResponse.json({
featureFlags: {},
sessionRecording: false,
});
} catch (error) {
console.error('[Mock] PostHog decide handler error:', error);
return HttpResponse.json(
{ featureFlags: {}, sessionRecording: false },
{ status: 200 }
);
}
}),
// PostHog session recording 接口(会话录制)
http.post('https://us.i.posthog.com/s/', async ({ request }) => {
try {
const body = await request.text();
if (process.env.NODE_ENV === 'development' && process.env.REACT_APP_LOG_POSTHOG === 'true') {
console.log('[Mock] PostHog session recording 请求:', {
url: request.url,
bodyPreview: body.substring(0, 100) + (body.length > 100 ? '...' : ''),
});
}
// 返回成功响应
return HttpResponse.json(
{ status: 1 },
{ status: 200 }
);
} catch (error) {
console.error('[Mock] PostHog session recording handler error:', error);
return HttpResponse.json(
{ status: 0, error: 'Mock handler error' },
{ status: 500 }
);
}
}),
// PostHog feature flags 接口(特性标志查询)
http.post('https://us.i.posthog.com/flags/', async ({ request }) => {
try {
if (process.env.NODE_ENV === 'development' && process.env.REACT_APP_LOG_POSTHOG === 'true') {
console.log('[Mock] PostHog feature flags 请求:', request.url);
}
// 返回空的特性标志
return HttpResponse.json({
featureFlags: {},
featureFlagPayloads: {},
});
} catch (error) {
console.error('[Mock] PostHog flags handler error:', error);
return HttpResponse.json(
{ featureFlags: {}, featureFlagPayloads: {} },
{ status: 200 }
);
}
}),
];

View File

@@ -1,619 +0,0 @@
/**
* 预测市场 Mock Handlers
*/
import { http, HttpResponse, delay } from "msw";
import {
mockTopics,
mockUserAccount,
mockPositions,
mockComments,
mockUsers,
} from "../data/prediction";
// 内存状态(用于模拟状态变化)
let userAccount = { ...mockUserAccount };
let topics = [...mockTopics];
let positions = [...mockPositions];
let comments = JSON.parse(JSON.stringify(mockComments));
// 重置状态
const resetState = () => {
userAccount = { ...mockUserAccount };
topics = [...mockTopics];
positions = [...mockPositions];
comments = JSON.parse(JSON.stringify(mockComments));
};
export const predictionHandlers = [
// ==================== 积分系统 ====================
// 获取用户积分账户
http.get(`/api/prediction/credit/account`, async () => {
await delay(300);
return HttpResponse.json({
success: true,
code: 200,
message: "success",
data: userAccount,
});
}),
// 领取每日奖励
http.post(`/api/prediction/credit/daily-bonus`, async () => {
await delay(500);
// 检查是否已领取
const today = new Date().toDateString();
const lastBonus = userAccount.last_daily_bonus
? new Date(userAccount.last_daily_bonus).toDateString()
: null;
if (lastBonus === today) {
return HttpResponse.json(
{
success: false,
code: 400,
message: "今日已领取过奖励",
data: null,
},
{ status: 400 }
);
}
// 发放奖励
const bonusAmount = 100;
userAccount.balance += bonusAmount;
userAccount.total += bonusAmount;
userAccount.total_earned += bonusAmount;
userAccount.last_daily_bonus = new Date().toISOString();
return HttpResponse.json({
success: true,
code: 200,
message: "领取成功",
data: {
bonus_amount: bonusAmount,
new_balance: userAccount.balance,
},
});
}),
// ==================== 预测话题 ====================
// 获取话题列表
http.get(`/api/prediction/topics`, async ({ request }) => {
await delay(400);
const url = new URL(request.url);
const status = url.searchParams.get("status");
const category = url.searchParams.get("category");
const sortBy = url.searchParams.get("sort_by") || "created_at";
const page = parseInt(url.searchParams.get("page") || "1", 10);
const perPage = parseInt(url.searchParams.get("per_page") || "10", 10);
let filteredTopics = [...topics];
// 过滤状态
if (status && status !== "all") {
filteredTopics = filteredTopics.filter((t) => t.status === status);
}
// 过滤分类
if (category && category !== "all") {
filteredTopics = filteredTopics.filter((t) => t.category === category);
}
// 排序
filteredTopics.sort((a, b) => {
if (sortBy === "total_pool") return b.total_pool - a.total_pool;
if (sortBy === "participants_count")
return b.participants_count - a.participants_count;
return new Date(b.created_at) - new Date(a.created_at);
});
// 分页
const total = filteredTopics.length;
const start = (page - 1) * perPage;
const paginatedTopics = filteredTopics.slice(start, start + perPage);
return HttpResponse.json({
success: true,
code: 200,
message: "success",
data: {
topics: paginatedTopics,
pagination: {
page,
per_page: perPage,
total,
total_pages: Math.ceil(total / perPage),
},
},
});
}),
// 获取话题详情
http.get(`/api/prediction/topics/:topicId`, async ({ params }) => {
await delay(300);
const topicId = parseInt(params.topicId, 10);
const topic = topics.find((t) => t.id === topicId);
if (!topic) {
return HttpResponse.json(
{
success: false,
code: 404,
message: "话题不存在",
data: null,
},
{ status: 404 }
);
}
// 构建详细的席位信息
const topicDetail = {
...topic,
yes_seats: [
{
user_id: topic.yes_lord_id,
user_name: topic.yes_lord_name,
user_avatar: mockUsers.find((u) => u.id === topic.yes_lord_id)
?.avatar_url,
shares: Math.floor(topic.yes_total_shares * 0.4),
is_lord: true,
},
...Array(Math.min(4, Math.floor(topic.participants_count / 4)))
.fill(null)
.map((_, i) => ({
user_id: mockUsers[(i + 2) % mockUsers.length].id,
user_name: mockUsers[(i + 2) % mockUsers.length].nickname,
user_avatar: mockUsers[(i + 2) % mockUsers.length].avatar_url,
shares: Math.floor((topic.yes_total_shares * 0.15) / (i + 1)),
is_lord: false,
})),
],
no_seats: [
{
user_id: topic.no_lord_id,
user_name: topic.no_lord_name,
user_avatar: mockUsers.find((u) => u.id === topic.no_lord_id)
?.avatar_url,
shares: Math.floor(topic.no_total_shares * 0.4),
is_lord: true,
},
...Array(Math.min(4, Math.floor(topic.participants_count / 5)))
.fill(null)
.map((_, i) => ({
user_id: mockUsers[(i + 3) % mockUsers.length].id,
user_name: mockUsers[(i + 3) % mockUsers.length].nickname,
user_avatar: mockUsers[(i + 3) % mockUsers.length].avatar_url,
shares: Math.floor((topic.no_total_shares * 0.15) / (i + 1)),
is_lord: false,
})),
],
};
return HttpResponse.json({
success: true,
code: 200,
message: "success",
data: topicDetail,
});
}),
// 创建话题
http.post(`/api/prediction/topics`, async ({ request }) => {
await delay(600);
const body = await request.json();
const { title, description, category, deadline } = body;
// 扣除创建费用
const createCost = 100;
if (userAccount.balance < createCost) {
return HttpResponse.json(
{
success: false,
code: 400,
message: "积分不足创建话题需要100积分",
data: null,
},
{ status: 400 }
);
}
userAccount.balance -= createCost;
userAccount.total_spent += createCost;
const newTopic = {
id: topics.length + 1,
title,
description,
category: category || "general",
tags: [],
author_id: 1,
author_name: "当前用户",
author_avatar: "https://api.dicebear.com/7.x/avataaars/svg?seed=current",
created_at: new Date().toISOString(),
deadline:
deadline ||
new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
status: "active",
total_pool: createCost,
yes_total_shares: 0,
no_total_shares: 0,
yes_price: 500,
no_price: 500,
yes_lord_id: null,
no_lord_id: null,
yes_lord_name: null,
no_lord_name: null,
participants_count: 0,
comments_count: 0,
};
topics.unshift(newTopic);
return HttpResponse.json({
success: true,
code: 200,
message: "创建成功",
data: newTopic,
});
}),
// 结算话题
http.post(
`/api/prediction/topics/:topicId/settle`,
async ({ params, request }) => {
await delay(500);
const topicId = parseInt(params.topicId, 10);
const body = await request.json();
const { result } = body;
const topicIndex = topics.findIndex((t) => t.id === topicId);
if (topicIndex === -1) {
return HttpResponse.json(
{ success: false, code: 404, message: "话题不存在", data: null },
{ status: 404 }
);
}
topics[topicIndex] = {
...topics[topicIndex],
status: "settled",
settlement_result: result,
settled_at: new Date().toISOString(),
};
return HttpResponse.json({
success: true,
code: 200,
message: "结算成功",
data: topics[topicIndex],
});
}
),
// ==================== 交易 ====================
// 买入份额
http.post(`/api/prediction/trade/buy`, async ({ request }) => {
await delay(500);
const body = await request.json();
const { topic_id, direction, shares } = body;
const topic = topics.find((t) => t.id === topic_id);
if (!topic) {
return HttpResponse.json(
{ success: false, code: 404, message: "话题不存在", data: null },
{ status: 404 }
);
}
// 计算成本
const currentPrice = direction === "yes" ? topic.yes_price : topic.no_price;
const totalCost = Math.floor(currentPrice * shares);
const tax = Math.floor(totalCost * 0.02);
const finalCost = totalCost + tax;
if (userAccount.balance < finalCost) {
return HttpResponse.json(
{ success: false, code: 400, message: "积分不足", data: null },
{ status: 400 }
);
}
// 扣除积分
userAccount.balance -= finalCost;
userAccount.frozen += totalCost;
userAccount.total_spent += finalCost;
// 更新话题数据
if (direction === "yes") {
topic.yes_total_shares += shares;
} else {
topic.no_total_shares += shares;
}
// 重新计算价格
const totalShares = topic.yes_total_shares + topic.no_total_shares;
topic.yes_price = Math.round((topic.yes_total_shares / totalShares) * 1000);
topic.no_price = Math.round((topic.no_total_shares / totalShares) * 1000);
topic.total_pool += tax;
topic.participants_count += 1;
// 添加持仓
const existingPosition = positions.find(
(p) => p.topic_id === topic_id && p.direction === direction
);
if (existingPosition) {
existingPosition.shares += shares;
existingPosition.current_value =
existingPosition.shares *
(direction === "yes" ? topic.yes_price : topic.no_price);
} else {
positions.push({
id: positions.length + 1,
topic_id,
topic_title: topic.title,
direction,
shares,
avg_cost: currentPrice,
current_price: direction === "yes" ? topic.yes_price : topic.no_price,
current_value:
shares * (direction === "yes" ? topic.yes_price : topic.no_price),
unrealized_pnl: 0,
acquired_at: new Date().toISOString(),
});
}
return HttpResponse.json({
success: true,
code: 200,
message: "买入成功",
data: {
trade_id: Date.now(),
topic,
shares,
price: currentPrice,
total_cost: totalCost,
tax,
new_balance: userAccount.balance,
},
});
}),
// 获取用户持仓
http.get(`/api/prediction/positions`, async () => {
await delay(300);
return HttpResponse.json({
success: true,
code: 200,
message: "success",
data: positions,
});
}),
// ==================== 评论 ====================
// 获取评论列表
http.get(`/api/prediction/topics/:topicId/comments`, async ({ params }) => {
await delay(300);
const topicId = parseInt(params.topicId, 10);
const topicComments = comments[topicId] || [];
return HttpResponse.json({
success: true,
code: 200,
message: "success",
data: {
comments: topicComments,
total: topicComments.length,
},
});
}),
// 发表评论
http.post(
`/api/prediction/topics/:topicId/comments`,
async ({ params, request }) => {
await delay(400);
const topicId = parseInt(params.topicId, 10);
const body = await request.json();
const { content, parent_id } = body;
const newComment = {
id: Date.now(),
topic_id: topicId,
user: {
id: 1,
nickname: "当前用户",
username: "current_user",
avatar_url: "https://api.dicebear.com/7.x/avataaars/svg?seed=current",
},
content,
parent_id: parent_id || null,
likes_count: 0,
is_liked: false,
is_lord: false,
total_investment: 0,
investment_shares: 0,
verification_status: null,
created_at: new Date().toISOString(),
};
if (!comments[topicId]) {
comments[topicId] = [];
}
comments[topicId].unshift(newComment);
// 更新话题评论数
const topic = topics.find((t) => t.id === topicId);
if (topic) {
topic.comments_count += 1;
}
return HttpResponse.json({
success: true,
code: 200,
message: "评论成功",
data: newComment,
});
}
),
// 点赞评论
http.post(`/api/prediction/comments/:commentId/like`, async ({ params }) => {
await delay(200);
const commentId = parseInt(params.commentId, 10);
// 在所有话题的评论中查找
for (const topicId of Object.keys(comments)) {
const comment = comments[topicId].find((c) => c.id === commentId);
if (comment) {
comment.is_liked = !comment.is_liked;
comment.likes_count += comment.is_liked ? 1 : -1;
return HttpResponse.json({
success: true,
code: 200,
message: comment.is_liked ? "点赞成功" : "取消点赞",
data: {
is_liked: comment.is_liked,
likes_count: comment.likes_count,
},
});
}
}
return HttpResponse.json(
{ success: false, code: 404, message: "评论不存在", data: null },
{ status: 404 }
);
}),
// ==================== 观点IPO ====================
// 投资评论
http.post(
`/api/prediction/comments/:commentId/invest`,
async ({ params, request }) => {
await delay(400);
const commentId = parseInt(params.commentId, 10);
const body = await request.json();
const { shares } = body;
const investCost = shares * 100; // 每份100积分
if (userAccount.balance < investCost) {
return HttpResponse.json(
{ success: false, code: 400, message: "积分不足", data: null },
{ status: 400 }
);
}
// 扣除积分
userAccount.balance -= investCost;
userAccount.total_spent += investCost;
// 更新评论投资数据
for (const topicId of Object.keys(comments)) {
const comment = comments[topicId].find((c) => c.id === commentId);
if (comment) {
comment.total_investment += investCost;
comment.investment_shares += shares;
return HttpResponse.json({
success: true,
code: 200,
message: "投资成功",
data: {
comment,
invested_shares: shares,
invested_amount: investCost,
new_balance: userAccount.balance,
},
});
}
}
return HttpResponse.json(
{ success: false, code: 404, message: "评论不存在", data: null },
{ status: 404 }
);
}
),
// 获取评论投资列表
http.get(
`/api/prediction/comments/:commentId/investments`,
async ({ params }) => {
await delay(300);
// 返回模拟的投资列表
return HttpResponse.json({
success: true,
code: 200,
message: "success",
data: {
investments: [
{
user: mockUsers[0],
shares: 10,
amount: 1000,
invested_at: "2024-02-01T10:00:00Z",
},
{
user: mockUsers[1],
shares: 5,
amount: 500,
invested_at: "2024-02-02T14:30:00Z",
},
],
total: 2,
},
});
}
),
// 验证评论
http.post(
`/api/prediction/comments/:commentId/verify`,
async ({ params, request }) => {
await delay(400);
const commentId = parseInt(params.commentId, 10);
const body = await request.json();
const { result } = body;
for (const topicId of Object.keys(comments)) {
const comment = comments[topicId].find((c) => c.id === commentId);
if (comment) {
comment.verification_status = result;
return HttpResponse.json({
success: true,
code: 200,
message: "验证成功",
data: comment,
});
}
}
return HttpResponse.json(
{ success: false, code: 404, message: "评论不存在", data: null },
{ status: 404 }
);
}
),
];
export default predictionHandlers;

View File

@@ -1,374 +0,0 @@
// src/mocks/handlers/simulation.js
import { http, HttpResponse, delay } from 'msw';
import { getCurrentUser } from '../data/users';
// 模拟网络延迟(毫秒)
const NETWORK_DELAY = 300;
// 模拟交易账户数据
let mockTradingAccount = {
account_id: 'sim_001',
account_name: '模拟交易账户',
initial_capital: 1000000,
available_cash: 850000,
frozen_cash: 0,
position_value: 150000,
total_assets: 1000000,
total_profit: 0,
total_profit_rate: 0,
daily_profit: 0,
daily_profit_rate: 0,
created_at: '2024-01-01T00:00:00Z',
updated_at: new Date().toISOString()
};
// 模拟持仓数据
let mockPositions = [
{
id: 1,
stock_code: '600036',
stock_name: '招商银行',
position_qty: 1000,
available_qty: 1000,
frozen_qty: 0,
avg_cost: 42.50,
current_price: 42.80,
market_value: 42800,
profit: 300,
profit_rate: 0.71,
today_profit: 100,
today_profit_rate: 0.23,
updated_at: new Date().toISOString()
},
{
id: 2,
stock_code: '000001',
stock_name: '平安银行',
position_qty: 2000,
available_qty: 2000,
frozen_qty: 0,
avg_cost: 12.30,
current_price: 12.50,
market_value: 25000,
profit: 400,
profit_rate: 1.63,
today_profit: -50,
today_profit_rate: -0.20,
updated_at: new Date().toISOString()
}
];
// 模拟交易历史
let mockOrders = [
{
id: 1,
order_no: 'ORD20240101001',
stock_code: '600036',
stock_name: '招商银行',
order_type: 'BUY',
price_type: 'MARKET',
order_price: 42.50,
order_qty: 1000,
filled_qty: 1000,
filled_price: 42.50,
filled_amount: 42500,
commission: 12.75,
stamp_tax: 0,
transfer_fee: 0.42,
total_fee: 13.17,
status: 'FILLED',
reject_reason: null,
order_time: '2024-01-15T09:30:00Z',
filled_time: '2024-01-15T09:30:05Z'
},
{
id: 2,
order_no: 'ORD20240102001',
stock_code: '000001',
stock_name: '平安银行',
order_type: 'BUY',
price_type: 'LIMIT',
order_price: 12.30,
order_qty: 2000,
filled_qty: 2000,
filled_price: 12.30,
filled_amount: 24600,
commission: 7.38,
stamp_tax: 0,
transfer_fee: 0.25,
total_fee: 7.63,
status: 'FILLED',
reject_reason: null,
order_time: '2024-01-16T10:15:00Z',
filled_time: '2024-01-16T10:15:10Z'
}
];
export const simulationHandlers = [
// ==================== 获取模拟账户信息 ====================
http.get('/api/simulation/account', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
// 未登录时返回401
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '未登录'
}, { status: 401 });
}
console.log('[Mock] 获取模拟账户信息:', currentUser);
return HttpResponse.json({
success: true,
data: mockTradingAccount
});
}),
// ==================== 获取持仓列表 ====================
http.get('/api/simulation/positions', async () => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '未登录'
}, { status: 401 });
}
console.log('[Mock] 获取持仓列表');
return HttpResponse.json({
success: true,
data: mockPositions
});
}),
// ==================== 获取交易订单历史 ====================
http.get('/api/simulation/orders', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '未登录'
}, { status: 401 });
}
const url = new URL(request.url);
const limit = parseInt(url.searchParams.get('limit') || '100');
console.log('[Mock] 获取交易订单历史, limit:', limit);
return HttpResponse.json({
success: true,
data: mockOrders.slice(0, limit)
});
}),
// ==================== 下单(买入/卖出)====================
http.post('/api/simulation/place-order', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '未登录'
}, { status: 401 });
}
const body = await request.json();
console.log('[Mock] 下单请求:', body);
const { stock_code, order_type, order_qty, price_type } = body;
// 生成订单号
const orderNo = 'ORD' + Date.now();
// 创建新订单
const newOrder = {
id: mockOrders.length + 1,
order_no: orderNo,
stock_code: stock_code,
stock_name: '模拟股票', // 实际应该查询股票名称
order_type: order_type,
price_type: price_type,
order_price: 0,
order_qty: order_qty,
filled_qty: order_qty,
filled_price: 0,
filled_amount: 0,
commission: 0,
stamp_tax: 0,
transfer_fee: 0,
total_fee: 0,
status: 'FILLED',
reject_reason: null,
order_time: new Date().toISOString(),
filled_time: new Date().toISOString()
};
// 添加到订单列表
mockOrders.unshift(newOrder);
return HttpResponse.json({
success: true,
message: '下单成功',
data: {
order_no: orderNo,
order_id: newOrder.id
}
});
}),
// ==================== 撤销订单 ====================
http.post('/api/simulation/cancel-order/:orderId', async ({ params }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '未登录'
}, { status: 401 });
}
const { orderId } = params;
console.log('[Mock] 撤销订单:', orderId);
// 查找并更新订单状态
const order = mockOrders.find(o => o.id.toString() === orderId || o.order_no === orderId);
if (order) {
order.status = 'CANCELLED';
}
return HttpResponse.json({
success: true,
message: '撤单成功'
});
}),
// ==================== 获取资产统计数据 ====================
http.get('/api/simulation/statistics', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '未登录'
}, { status: 401 });
}
const url = new URL(request.url);
const days = parseInt(url.searchParams.get('days') || '30');
console.log('[Mock] 获取资产统计, days:', days);
// 生成模拟的资产历史数据
const dailyReturns = [];
const baseAssets = 1000000;
for (let i = 0; i < days; i++) {
const date = new Date();
date.setDate(date.getDate() - (days - 1 - i));
// 生成随机波动
const randomChange = (Math.random() - 0.5) * 0.02; // ±1%
const assets = baseAssets * (1 + randomChange * i / days);
dailyReturns.push({
date: date.toISOString().split('T')[0],
closing_assets: assets,
total_assets: assets,
daily_profit: assets - baseAssets,
daily_profit_rate: ((assets - baseAssets) / baseAssets * 100).toFixed(2)
});
}
return HttpResponse.json({
success: true,
data: {
daily_returns: dailyReturns,
summary: {
total_profit: 0,
total_profit_rate: 0,
win_rate: 50,
max_drawdown: -5.2
}
}
});
}),
// ==================== 获取交易记录 ====================
http.get('/api/simulation/transactions', async ({ request }) => {
await delay(NETWORK_DELAY);
const currentUser = getCurrentUser();
if (!currentUser) {
return HttpResponse.json({
success: false,
error: '未登录'
}, { status: 401 });
}
const url = new URL(request.url);
const limit = parseInt(url.searchParams.get('limit') || '50');
console.log('[Mock] 获取交易记录, limit:', limit);
// 返回已成交的订单作为交易记录
const transactions = mockOrders
.filter(order => order.status === 'FILLED')
.slice(0, limit);
return HttpResponse.json({
success: true,
data: transactions
});
}),
// ==================== 搜索股票 ====================
http.get('/api/stocks/search', async ({ request }) => {
await delay(200);
const url = new URL(request.url);
const keyword = url.searchParams.get('q') || '';
const limit = parseInt(url.searchParams.get('limit') || '10');
console.log('[Mock] 搜索股票:', keyword);
// 模拟股票数据
const allStocks = [
{ stock_code: '000001', stock_name: '平安银行', current_price: 12.50, pinyin_abbr: 'payh', security_type: 'A股', exchange: '深交所' },
{ stock_code: '000002', stock_name: '万科A', current_price: 8.32, pinyin_abbr: 'wka', security_type: 'A股', exchange: '深交所' },
{ stock_code: '600036', stock_name: '招商银行', current_price: 42.80, pinyin_abbr: 'zsyh', security_type: 'A股', exchange: '上交所' },
{ stock_code: '600519', stock_name: '贵州茅台', current_price: 1680.50, pinyin_abbr: 'gzmt', security_type: 'A股', exchange: '上交所' },
{ stock_code: '601318', stock_name: '中国平安', current_price: 45.20, pinyin_abbr: 'zgpa', security_type: 'A股', exchange: '上交所' },
{ stock_code: '688256', stock_name: '寒武纪', current_price: 1394.94, pinyin_abbr: 'hwj', security_type: 'A股', exchange: '上交所科创板' },
];
// 过滤股票
const results = allStocks.filter(stock =>
stock.stock_code.includes(keyword) ||
stock.stock_name.includes(keyword) ||
stock.pinyin_abbr.includes(keyword.toLowerCase())
).slice(0, limit);
return HttpResponse.json({
success: true,
data: results
});
})
];

View File

@@ -1,690 +0,0 @@
// src/mocks/handlers/stock.js
// 股票相关的 Mock Handlers
import { http, HttpResponse } from 'msw';
import { generateTimelineData, generateDailyData } from '../data/kline';
// 模拟延迟
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// 常用字拼音首字母映射(简化版)
const PINYIN_MAP = {
'平': 'p', '安': 'a', '银': 'y', '行': 'h', '浦': 'p', '发': 'f',
'招': 'z', '商': 's', '兴': 'x', '业': 'y', '北': 'b', '京': 'j',
'农': 'n', '交': 'j', '通': 't', '工': 'g', '光': 'g', '大': 'd',
'建': 'j', '设': 's', '中': 'z', '信': 'x', '证': 'z', '券': 'q',
'国': 'g', '金': 'j', '海': 'h', '华': 'h', '泰': 't', '方': 'f',
'正': 'z', '新': 'x', '保': 'b', '险': 'x', '太': 't', '人': 'r',
'寿': 's', '泸': 'l', '州': 'z', '老': 'l', '窖': 'j', '古': 'g',
'井': 'j', '贡': 'g', '酒': 'j', '五': 'w', '粮': 'l', '液': 'y',
'贵': 'g', '茅': 'm', '台': 't', '青': 'q', '岛': 'd', '啤': 'p',
'水': 's', '坊': 'f', '今': 'j', '世': 's', '缘': 'y', '云': 'y',
'南': 'n', '白': 'b', '药': 'y', '长': 'c', '春': 'c', '高': 'g',
'科': 'k', '伦': 'l', '比': 'b', '亚': 'y', '迪': 'd', '恒': 'h',
'瑞': 'r', '医': 'y', '片': 'p', '仔': 'z', '癀': 'h', '明': 'm',
'康': 'k', '德': 'd', '讯': 'x', '东': 'd', '威': 'w', '视': 's',
'立': 'l', '精': 'j', '密': 'm', '电': 'd', '航': 'h',
'动': 'd', '力': 'l', '韦': 'w', '尔': 'e', '股': 'g', '份': 'f',
'万': 'w', '赣': 'g', '锋': 'f', '锂': 'l', '宁': 'n', '时': 's',
'代': 'd', '隆': 'l', '基': 'j', '绿': 'l', '能': 'n',
'筑': 'z', '汽': 'q', '车': 'c', '宇': 'y', '客': 'k', '上': 's',
'集': 'j', '团': 't', '广': 'g', '城': 'c', '侨': 'q', '夏': 'x',
'幸': 'x', '福': 'f', '地': 'd', '控': 'k', '美': 'm', '格': 'g',
'苏': 's', '智': 'z', '家': 'j', '易': 'y', '购': 'g',
'轩': 'x', '财': 'c', '富': 'f', '石': 's', '化': 'h', '学': 'x',
'山': 's', '黄': 'h', '螺': 'l', '泥': 'n', '神': 's', '油': 'y',
'联': 'l', '移': 'y', '伊': 'y', '利': 'l', '紫': 'z', '矿': 'k',
'天': 't', '味': 'w', '港': 'g', '微': 'w',
'技': 'j', '的': 'd', '器': 'q', '泊': 'b', '铁': 't',
};
// 生成拼音缩写
const generatePinyinAbbr = (name) => {
return name.split('').map(char => PINYIN_MAP[char] || '').join('');
};
// 生成A股主要股票数据包含各大指数成分股
const generateStockList = () => {
const stocks = [
// 银行
{ code: '000001', name: '平安银行' },
{ code: '600000', name: '浦发银行' },
{ code: '600036', name: '招商银行' },
{ code: '601166', name: '兴业银行' },
{ code: '601169', name: '北京银行' },
{ code: '601288', name: '农业银行' },
{ code: '601328', name: '交通银行' },
{ code: '601398', name: '工商银行' },
{ code: '601818', name: '光大银行' },
{ code: '601939', name: '建设银行' },
{ code: '601998', name: '中信银行' },
// 证券
{ code: '600030', name: '中信证券' },
{ code: '600109', name: '国金证券' },
{ code: '600837', name: '海通证券' },
{ code: '600999', name: '招商证券' },
{ code: '601688', name: '华泰证券' },
{ code: '601901', name: '方正证券' },
// 保险
{ code: '601318', name: '中国平安' },
{ code: '601336', name: '新华保险' },
{ code: '601601', name: '中国太保' },
{ code: '601628', name: '中国人寿' },
// 白酒/食品饮料
{ code: '000568', name: '泸州老窖' },
{ code: '000596', name: '古井贡酒' },
{ code: '000858', name: '五粮液' },
{ code: '600519', name: '贵州茅台' },
{ code: '600600', name: '青岛啤酒' },
{ code: '600779', name: '水井坊' },
{ code: '603369', name: '今世缘' },
// 医药
{ code: '000538', name: '云南白药' },
{ code: '000661', name: '长春高新' },
{ code: '002422', name: '科伦药业' },
{ code: '002594', name: '比亚迪' },
{ code: '600276', name: '恒瑞医药' },
{ code: '600436', name: '片仔癀' },
{ code: '603259', name: '药明康德' },
// 科技/半导体
{ code: '000063', name: '中兴通讯' },
{ code: '000725', name: '京东方A' },
{ code: '002049', name: '紫光国微' },
{ code: '002415', name: '海康威视' },
{ code: '002475', name: '立讯精密' },
{ code: '600584', name: '长电科技' },
{ code: '600893', name: '航发动力' },
{ code: '603501', name: '韦尔股份' },
// 新能源/电力
{ code: '000002', name: '万科A' },
{ code: '002460', name: '赣锋锂业' },
{ code: '300750', name: '宁德时代' },
{ code: '600438', name: '通威股份' },
{ code: '601012', name: '隆基绿能' },
{ code: '601668', name: '中国建筑' },
// 汽车
{ code: '000625', name: '长安汽车' },
{ code: '600066', name: '宇通客车' },
{ code: '600104', name: '上汽集团' },
{ code: '601238', name: '广汽集团' },
{ code: '601633', name: '长城汽车' },
// 地产
{ code: '000002', name: '万科A' },
{ code: '000069', name: '华侨城A' },
{ code: '600340', name: '华夏幸福' },
{ code: '600606', name: '绿地控股' },
// 家电
{ code: '000333', name: '美的集团' },
{ code: '000651', name: '格力电器' },
{ code: '002032', name: '苏泊尔' },
{ code: '600690', name: '海尔智家' },
// 互联网/电商
{ code: '002024', name: '苏宁易购' },
{ code: '002074', name: '国轩高科' },
{ code: '300059', name: '东方财富' },
// 能源/化工
{ code: '600028', name: '中国石化' },
{ code: '600309', name: '万华化学' },
{ code: '600547', name: '山东黄金' },
{ code: '600585', name: '海螺水泥' },
{ code: '601088', name: '中国神华' },
{ code: '601857', name: '中国石油' },
// 电信/运营商
{ code: '600050', name: '中国联通' },
{ code: '600941', name: '中国移动' },
{ code: '601728', name: '中国电信' },
// 其他蓝筹
{ code: '600887', name: '伊利股份' },
{ code: '601111', name: '中国国航' },
{ code: '601390', name: '中国中铁' },
{ code: '601899', name: '紫金矿业' },
{ code: '603288', name: '海天味业' },
];
// 添加拼音缩写
return stocks.map(s => ({
...s,
pinyin_abbr: generatePinyinAbbr(s.name)
}));
};
// 股票相关的 Handlers
export const stockHandlers = [
// 搜索股票(个股中心页面使用)- 支持模糊搜索
http.get('/api/stocks/search', async ({ request }) => {
await delay(200);
const url = new URL(request.url);
const query = (url.searchParams.get('q') || '').toLowerCase().trim();
const limit = parseInt(url.searchParams.get('limit') || '10');
console.log('[Mock Stock] 搜索股票:', { query, limit });
const stocks = generateStockList();
// 如果没有搜索词,返回空结果
if (!query) {
return HttpResponse.json({
success: true,
data: []
});
}
// 模糊搜索:代码 + 名称 + 拼音缩写(不区分大小写)
const results = stocks.filter(s => {
const code = s.code.toLowerCase();
const name = s.name.toLowerCase();
const pinyin = (s.pinyin_abbr || '').toLowerCase();
return code.includes(query) || name.includes(query) || pinyin.includes(query);
});
// 按相关性排序:完全匹配 > 开头匹配 > 包含匹配
results.sort((a, b) => {
const aCode = a.code.toLowerCase();
const bCode = b.code.toLowerCase();
const aName = a.name.toLowerCase();
const bName = b.name.toLowerCase();
const aPinyin = (a.pinyin_abbr || '').toLowerCase();
const bPinyin = (b.pinyin_abbr || '').toLowerCase();
// 计算匹配分数(包含拼音匹配)
const getScore = (code, name, pinyin) => {
if (code === query || name === query || pinyin === query) return 100; // 完全匹配
if (code.startsWith(query)) return 80; // 代码开头
if (pinyin.startsWith(query)) return 70; // 拼音开头
if (name.startsWith(query)) return 60; // 名称开头
if (code.includes(query)) return 40; // 代码包含
if (pinyin.includes(query)) return 30; // 拼音包含
if (name.includes(query)) return 20; // 名称包含
return 0;
};
return getScore(bCode, bName, bPinyin) - getScore(aCode, aName, aPinyin);
});
// 返回格式化数据
return HttpResponse.json({
success: true,
data: results.slice(0, limit).map(s => ({
stock_code: s.code,
stock_name: s.name,
pinyin_abbr: s.pinyin_abbr,
market: s.code.startsWith('6') ? 'SH' : 'SZ',
industry: ['银行', '证券', '保险', '白酒', '医药', '科技', '新能源', '汽车', '地产', '家电'][Math.floor(Math.random() * 10)],
change_pct: parseFloat((Math.random() * 10 - 3).toFixed(2)),
price: parseFloat((Math.random() * 100 + 5).toFixed(2))
}))
});
}),
// 获取所有股票列表
http.get('/api/stocklist', async () => {
await delay(200);
try {
const stocks = generateStockList();
// console.log('[Mock Stock] 获取股票列表成功:', { count: stocks.length }); // 已关闭:减少日志
return HttpResponse.json(stocks);
} catch (error) {
console.error('[Mock Stock] 获取股票列表失败:', error);
return HttpResponse.json(
{ error: '获取股票列表失败' },
{ status: 500 }
);
}
}),
// 获取指数K线数据
http.get('/api/index/:indexCode/kline', async ({ params, request }) => {
await delay(300);
const { indexCode } = params;
const url = new URL(request.url);
const type = url.searchParams.get('type') || 'timeline';
const eventTime = url.searchParams.get('event_time');
console.log('[Mock Stock] 获取指数K线数据:', { indexCode, type, eventTime });
try {
let data;
if (type === 'timeline' || type === 'minute') {
// timeline 和 minute 都使用分时数据
data = generateTimelineData(indexCode);
} else if (type === 'daily') {
data = generateDailyData(indexCode, 30);
} else {
// 其他类型也降级使用 timeline 数据
console.log('[Mock Stock] 未知类型,降级使用 timeline:', type);
data = generateTimelineData(indexCode);
}
return HttpResponse.json({
success: true,
data: data,
index_code: indexCode,
type: type,
message: '获取成功'
});
} catch (error) {
console.error('[Mock Stock] 获取K线数据失败:', error);
return HttpResponse.json(
{ error: '获取K线数据失败' },
{ status: 500 }
);
}
}),
// 获取股票K线数据
http.get('/api/stock/:stockCode/kline', async ({ params, request }) => {
await delay(300);
const { stockCode } = params;
const url = new URL(request.url);
const type = url.searchParams.get('type') || 'timeline';
const eventTime = url.searchParams.get('event_time');
console.log('[Mock Stock] 获取股票K线数据:', { stockCode, type, eventTime });
try {
let data;
if (type === 'timeline') {
// 股票使用指数的数据生成逻辑,但价格基数不同
data = generateTimelineData('000001.SH'); // 可以根据股票代码调整
} else if (type === 'daily') {
data = generateDailyData('000001.SH', 30);
} else {
return HttpResponse.json(
{ error: '不支持的类型' },
{ status: 400 }
);
}
return HttpResponse.json({
success: true,
data: data,
stock_code: stockCode,
type: type,
message: '获取成功'
});
} catch (error) {
console.error('[Mock Stock] 获取股票K线数据失败:', error);
return HttpResponse.json(
{ error: '获取K线数据失败' },
{ status: 500 }
);
}
}),
// 批量获取股票K线数据
http.post('/api/stock/batch-kline', async ({ request }) => {
await delay(400);
try {
const body = await request.json();
const { codes, type = 'timeline', event_time } = body;
console.log('[Mock Stock] 批量获取K线数据:', {
stockCount: codes?.length,
type,
eventTime: event_time
});
if (!codes || !Array.isArray(codes) || codes.length === 0) {
return HttpResponse.json(
{ error: '股票代码列表不能为空' },
{ status: 400 }
);
}
// 为每只股票生成数据
const batchData = {};
codes.forEach(stockCode => {
let data;
if (type === 'timeline') {
data = generateTimelineData('000001.SH');
} else if (type === 'daily') {
data = generateDailyData('000001.SH', 60);
} else {
data = [];
}
batchData[stockCode] = {
success: true,
data: data,
stock_code: stockCode
};
});
return HttpResponse.json({
success: true,
data: batchData,
type: type,
message: '批量获取成功'
});
} catch (error) {
console.error('[Mock Stock] 批量获取K线数据失败:', error);
return HttpResponse.json(
{ error: '批量获取K线数据失败' },
{ status: 500 }
);
}
}),
// 获取股票业绩预告
http.get('/api/stock/:stockCode/forecast', async ({ params }) => {
await delay(200);
const { stockCode } = params;
console.log('[Mock Stock] 获取业绩预告:', { stockCode });
// 生成股票列表用于查找名称
const stockList = generateStockList();
const stockInfo = stockList.find(s => s.code === stockCode.replace(/\.(SH|SZ)$/i, ''));
const stockName = stockInfo?.name || `股票${stockCode}`;
// 业绩预告类型列表
const forecastTypes = ['预增', '预减', '略增', '略减', '扭亏', '续亏', '首亏', '续盈'];
// 生成业绩预告数据
const forecasts = [
{
forecast_type: '预增',
report_date: '2024年年报',
content: `${stockName}预计2024年度归属于上市公司股东的净利润为58亿元至62亿元同比增长10%至17%。`,
reason: '报告期内,公司主营业务收入稳步增长,产品结构持续优化,毛利率提升;同时公司加大研发投入,新产品市场表现良好。',
change_range: {
lower: 10,
upper: 17
},
publish_date: '2024-10-15'
},
{
forecast_type: '略增',
report_date: '2024年三季报',
content: `${stockName}预计2024年1-9月归属于上市公司股东的净利润为42亿元至45亿元同比增长5%至12%。`,
reason: '公司积极拓展市场渠道,销售规模持续扩大,经营效益稳步提升。',
change_range: {
lower: 5,
upper: 12
},
publish_date: '2024-07-12'
},
{
forecast_type: forecastTypes[Math.floor(Math.random() * forecastTypes.length)],
report_date: '2024年中报',
content: `${stockName}预计2024年上半年归属于上市公司股东的净利润为28亿元至30亿元。`,
reason: '受益于行业景气度回升及公司降本增效措施效果显现,经营业绩同比有所改善。',
change_range: {
lower: 3,
upper: 8
},
publish_date: '2024-04-20'
}
];
return HttpResponse.json({
success: true,
data: {
stock_code: stockCode,
stock_name: stockName,
forecasts: forecasts
}
});
}),
// 获取股票报价(批量)
http.post('/api/stock/quotes', async ({ request }) => {
await delay(200);
try {
const body = await request.json();
const { codes, event_time } = body;
console.log('[Mock Stock] 获取股票报价:', {
stockCount: codes?.length,
eventTime: event_time
});
if (!codes || !Array.isArray(codes) || codes.length === 0) {
return HttpResponse.json(
{ success: false, error: '股票代码列表不能为空' },
{ status: 400 }
);
}
// 生成股票列表用于查找名称
const stockList = generateStockList();
const stockMap = {};
stockList.forEach(s => {
stockMap[s.code] = s.name;
});
// 行业和指数映射表
const stockIndustryMap = {
'000001': { industry_l1: '金融', industry: '银行', index_tags: ['沪深300', '上证180'] },
'600519': { industry_l1: '消费', industry: '白酒', index_tags: ['沪深300', '上证50'] },
'300750': { industry_l1: '工业', industry: '电池', index_tags: ['创业板50'] },
'601318': { industry_l1: '金融', industry: '保险', index_tags: ['沪深300', '上证50'] },
'600036': { industry_l1: '金融', industry: '银行', index_tags: ['沪深300', '上证50'] },
'000858': { industry_l1: '消费', industry: '白酒', index_tags: ['沪深300'] },
'002594': { industry_l1: '汽车', industry: '乘用车', index_tags: ['沪深300', '创业板指'] },
};
const defaultIndustries = [
{ industry_l1: '科技', industry: '软件' },
{ industry_l1: '医药', industry: '化学制药' },
{ industry_l1: '消费', industry: '食品' },
{ industry_l1: '金融', industry: '证券' },
{ industry_l1: '工业', industry: '机械' },
];
// 为每只股票生成报价数据
const quotesData = {};
codes.forEach(stockCode => {
// 生成基础价格10-200之间
const basePrice = parseFloat((Math.random() * 190 + 10).toFixed(2));
// 涨跌幅(-10% 到 +10%
const changePercent = parseFloat((Math.random() * 20 - 10).toFixed(2));
// 涨跌额
const change = parseFloat((basePrice * changePercent / 100).toFixed(2));
// 昨收
const prevClose = parseFloat((basePrice - change).toFixed(2));
// 获取行业和指数信息
const codeWithoutSuffix = stockCode.replace(/\.(SH|SZ)$/i, '');
const industryInfo = stockIndustryMap[codeWithoutSuffix] ||
defaultIndustries[Math.floor(Math.random() * defaultIndustries.length)];
quotesData[stockCode] = {
code: stockCode,
name: stockMap[stockCode] || `股票${stockCode}`,
price: basePrice,
change: change,
change_percent: changePercent,
prev_close: prevClose,
open: parseFloat((prevClose * (1 + (Math.random() * 0.02 - 0.01))).toFixed(2)),
high: parseFloat((basePrice * (1 + Math.random() * 0.05)).toFixed(2)),
low: parseFloat((basePrice * (1 - Math.random() * 0.05)).toFixed(2)),
volume: Math.floor(Math.random() * 100000000),
amount: parseFloat((Math.random() * 10000000000).toFixed(2)),
market: stockCode.startsWith('6') ? 'SH' : 'SZ',
update_time: new Date().toISOString(),
// 行业和指数标签
industry_l1: industryInfo.industry_l1,
industry: industryInfo.industry,
index_tags: industryInfo.index_tags || [],
// 关键指标
pe: parseFloat((Math.random() * 50 + 5).toFixed(2)),
eps: parseFloat((Math.random() * 5 + 0.1).toFixed(3)),
pb: parseFloat((Math.random() * 8 + 0.5).toFixed(2)),
market_cap: `${(Math.random() * 5000 + 100).toFixed(0)}亿`,
week52_low: parseFloat((basePrice * 0.7).toFixed(2)),
week52_high: parseFloat((basePrice * 1.3).toFixed(2)),
// 主力动态
main_net_inflow: parseFloat((Math.random() * 10 - 5).toFixed(2)),
institution_holding: parseFloat((Math.random() * 50 + 10).toFixed(2)),
buy_ratio: parseFloat((Math.random() * 40 + 30).toFixed(2)),
sell_ratio: parseFloat((100 - (Math.random() * 40 + 30)).toFixed(2))
};
});
return HttpResponse.json({
success: true,
data: quotesData,
message: '获取成功'
});
} catch (error) {
console.error('[Mock Stock] 获取股票报价失败:', error);
return HttpResponse.json(
{ success: false, error: '获取股票报价失败' },
{ status: 500 }
);
}
}),
// 获取股票详细行情quote-detail
http.get('/api/stock/:stockCode/quote-detail', async ({ params }) => {
await delay(200);
const { stockCode } = params;
console.log('[Mock Stock] 获取股票详细行情:', { stockCode });
const stocks = generateStockList();
const codeWithoutSuffix = stockCode.replace(/\.(SH|SZ)$/i, '');
const stockInfo = stocks.find(s => s.code === codeWithoutSuffix);
const stockName = stockInfo?.name || `股票${stockCode}`;
// 生成基础价格10-200之间
const basePrice = parseFloat((Math.random() * 190 + 10).toFixed(2));
// 涨跌幅(-10% 到 +10%
const changePercent = parseFloat((Math.random() * 20 - 10).toFixed(2));
// 涨跌额
const change = parseFloat((basePrice * changePercent / 100).toFixed(2));
// 昨收
const prevClose = parseFloat((basePrice - change).toFixed(2));
return HttpResponse.json({
success: true,
data: {
stock_code: stockCode,
stock_name: stockName,
price: basePrice,
change: change,
change_percent: changePercent,
prev_close: prevClose,
open: parseFloat((prevClose * (1 + (Math.random() * 0.02 - 0.01))).toFixed(2)),
high: parseFloat((basePrice * (1 + Math.random() * 0.05)).toFixed(2)),
low: parseFloat((basePrice * (1 - Math.random() * 0.05)).toFixed(2)),
volume: Math.floor(Math.random() * 100000000),
amount: parseFloat((Math.random() * 10000000000).toFixed(2)),
turnover_rate: parseFloat((Math.random() * 10).toFixed(2)),
amplitude: parseFloat((Math.random() * 8).toFixed(2)),
market: stockCode.startsWith('6') ? 'SH' : 'SZ',
update_time: new Date().toISOString(),
// 买卖盘口
bid1: parseFloat((basePrice * 0.998).toFixed(2)),
bid1_volume: Math.floor(Math.random() * 10000),
bid2: parseFloat((basePrice * 0.996).toFixed(2)),
bid2_volume: Math.floor(Math.random() * 10000),
bid3: parseFloat((basePrice * 0.994).toFixed(2)),
bid3_volume: Math.floor(Math.random() * 10000),
bid4: parseFloat((basePrice * 0.992).toFixed(2)),
bid4_volume: Math.floor(Math.random() * 10000),
bid5: parseFloat((basePrice * 0.990).toFixed(2)),
bid5_volume: Math.floor(Math.random() * 10000),
ask1: parseFloat((basePrice * 1.002).toFixed(2)),
ask1_volume: Math.floor(Math.random() * 10000),
ask2: parseFloat((basePrice * 1.004).toFixed(2)),
ask2_volume: Math.floor(Math.random() * 10000),
ask3: parseFloat((basePrice * 1.006).toFixed(2)),
ask3_volume: Math.floor(Math.random() * 10000),
ask4: parseFloat((basePrice * 1.008).toFixed(2)),
ask4_volume: Math.floor(Math.random() * 10000),
ask5: parseFloat((basePrice * 1.010).toFixed(2)),
ask5_volume: Math.floor(Math.random() * 10000),
// 关键指标
pe: parseFloat((Math.random() * 50 + 5).toFixed(2)),
pb: parseFloat((Math.random() * 8 + 0.5).toFixed(2)),
eps: parseFloat((Math.random() * 5 + 0.1).toFixed(3)),
market_cap: `${(Math.random() * 5000 + 100).toFixed(0)}亿`,
circulating_market_cap: `${(Math.random() * 3000 + 50).toFixed(0)}亿`,
total_shares: `${(Math.random() * 100 + 10).toFixed(2)}亿`,
circulating_shares: `${(Math.random() * 80 + 5).toFixed(2)}亿`,
week52_high: parseFloat((basePrice * 1.3).toFixed(2)),
week52_low: parseFloat((basePrice * 0.7).toFixed(2))
},
message: '获取成功'
});
}),
// FlexScreen 行情数据
http.get('/api/flex-screen/quotes', async ({ request }) => {
await delay(200);
const url = new URL(request.url);
const codes = url.searchParams.get('codes')?.split(',') || [];
console.log('[Mock Stock] 获取 FlexScreen 行情:', { codes });
// 默认主要指数
const defaultIndices = ['000001', '399001', '399006'];
const targetCodes = codes.length > 0 ? codes : defaultIndices;
const indexData = {
'000001': { name: '上证指数', basePrice: 3200 },
'399001': { name: '深证成指', basePrice: 10500 },
'399006': { name: '创业板指', basePrice: 2100 },
'000300': { name: '沪深300', basePrice: 3800 },
'000016': { name: '上证50', basePrice: 2600 },
'000905': { name: '中证500', basePrice: 5800 },
};
const quotesData = {};
targetCodes.forEach(code => {
const codeWithoutSuffix = code.replace(/\.(SH|SZ)$/i, '');
const info = indexData[codeWithoutSuffix] || { name: `指数${code}`, basePrice: 3000 };
const changePercent = parseFloat((Math.random() * 4 - 2).toFixed(2));
const price = parseFloat((info.basePrice * (1 + changePercent / 100)).toFixed(2));
const change = parseFloat((price - info.basePrice).toFixed(2));
quotesData[code] = {
code: code,
name: info.name,
price: price,
change: change,
change_percent: changePercent,
prev_close: info.basePrice,
open: parseFloat((info.basePrice * (1 + (Math.random() * 0.01 - 0.005))).toFixed(2)),
high: parseFloat((price * (1 + Math.random() * 0.01)).toFixed(2)),
low: parseFloat((price * (1 - Math.random() * 0.01)).toFixed(2)),
volume: parseFloat((Math.random() * 5000 + 2000).toFixed(2)), // 亿手
amount: parseFloat((Math.random() * 8000 + 3000).toFixed(2)), // 亿元
update_time: new Date().toISOString()
};
});
return HttpResponse.json({
success: true,
data: quotesData,
message: '获取成功'
});
}),
];

View File

@@ -1,932 +0,0 @@
// src/mocks/handlers/stock.ts
// 股票相关的 Mock Handlers
import { http, HttpResponse } from 'msw';
import { generateTimelineData, generateDailyData } from '../data/kline';
// 调试日志:确认模块加载
console.log('[Mock] stockHandlers 模块已加载');
// ============ 类型定义 ============
/** 拼音映射类型 */
type PinyinMap = Record<string, string>;
/** 股票基础信息 */
interface StockInfo {
code: string;
name: string;
pinyin_abbr?: string;
}
/** 搜索结果项 */
interface SearchResultItem {
stock_code: string;
stock_name: string;
pinyin_abbr?: string;
market: 'SH' | 'SZ';
industry: string;
change_pct: number;
price: number;
}
/** 股票报价 */
interface StockQuote {
code: string;
name: string;
price: number;
change: number;
change_percent: number;
prev_close: number;
open: number;
high: number;
low: number;
volume: number;
amount: number;
market: string;
update_time: string;
industry_l1?: string;
industry?: string;
index_tags?: string[];
pe?: number;
eps?: number;
pb?: number;
market_cap?: string;
week52_low?: number;
week52_high?: number;
main_net_inflow?: number;
institution_holding?: number;
buy_ratio?: number;
sell_ratio?: number;
}
/** 股票详细行情 */
interface StockQuoteDetail extends StockQuote {
stock_code: string;
stock_name: string;
turnover_rate: number;
amplitude: number;
bid1: number;
bid1_volume: number;
bid2: number;
bid2_volume: number;
bid3: number;
bid3_volume: number;
bid4: number;
bid4_volume: number;
bid5: number;
bid5_volume: number;
ask1: number;
ask1_volume: number;
ask2: number;
ask2_volume: number;
ask3: number;
ask3_volume: number;
ask4: number;
ask4_volume: number;
ask5: number;
ask5_volume: number;
circulating_market_cap: string;
total_shares: string;
circulating_shares: string;
}
/** 业绩预告 */
interface ForecastItem {
forecast_type: string;
report_date: string;
content: string;
reason: string;
change_range: { lower: number; upper: number };
publish_date: string;
}
/** K线批量请求体 */
interface BatchKlineRequest {
codes: string[];
type?: 'timeline' | 'daily';
event_time?: string;
}
/** 股票报价批量请求体 */
interface QuotesRequest {
codes: string[];
event_time?: string;
}
/** 行业信息 */
interface IndustryInfo {
industry_l1: string;
industry: string;
index_tags?: string[];
}
/** 指数基础信息 */
interface IndexInfo {
name: string;
basePrice: number;
}
// ============ 常量配置 ============
/** 模拟延迟 */
const delay = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
/** 常用字拼音首字母映射(简化版) */
const PINYIN_MAP: Readonly<PinyinMap> = {
: 'p',
: 'a',
: 'y',
: 'h',
: 'p',
: 'f',
: 'z',
: 's',
: 'x',
: 'y',
: 'b',
: 'j',
: 'n',
: 'j',
: 't',
: 'g',
: 'g',
: 'd',
: 'j',
: 's',
: 'z',
: 'x',
: 'z',
: 'q',
: 'g',
: 'j',
: 'h',
: 'h',
: 't',
: 'f',
: 'z',
: 'x',
: 'b',
: 'x',
: 't',
: 'r',
寿: 's',
: 'l',
: 'z',
: 'l',
: 'j',
: 'g',
: 'j',
: 'g',
: 'j',
: 'w',
: 'l',
: 'y',
: 'g',
: 'm',
: 't',
: 'q',
: 'd',
: 'p',
: 's',
: 'f',
: 'j',
: 's',
: 'y',
: 'y',
: 'n',
: 'b',
: 'y',
: 'c',
: 'c',
: 'g',
: 'k',
: 'l',
: 'b',
: 'y',
: 'd',
: 'h',
: 'r',
: 'y',
: 'p',
: 'z',
: 'h',
: 'm',
: 'k',
: 'd',
: 'x',
: 'd',
: 'w',
: 's',
: 'l',
: 'j',
: 'm',
: 'd',
: 'h',
: 'd',
: 'l',
: 'w',
: 'e',
: 'g',
: 'f',
: 'w',
: 'g',
: 'f',
: 'l',
: 'n',
: 's',
: 'd',
: 'l',
: 'j',
绿: 'l',
: 'n',
: 'z',
: 'q',
: 'c',
: 'y',
: 'k',
: 's',
: 'j',
: 't',
广: 'g',
: 'c',
: 'q',
: 'x',
: 'x',
: 'f',
: 'd',
: 'k',
: 'm',
: 'g',
: 's',
: 'z',
: 'j',
: 'y',
: 'g',
: 'x',
: 'c',
: 'f',
: 's',
: 'h',
: 'x',
: 's',
: 'h',
: 'l',
: 'n',
: 's',
: 'y',
: 'l',
: 'y',
: 'y',
: 'l',
: 'z',
: 'k',
: 't',
: 'w',
: 'g',
: 'w',
: 'j',
: 'd',
: 'q',
: 'b',
: 't',
} as const;
// ============ 工具函数 ============
/** 生成拼音缩写 */
const generatePinyinAbbr = (name: string): string => {
return name
.split('')
.map((char) => PINYIN_MAP[char] || '')
.join('');
};
/** 生成A股主要股票数据包含各大指数成分股 */
const generateStockList = (): StockInfo[] => {
const stocks: Array<{ code: string; name: string }> = [
// 银行
{ code: '000001', name: '平安银行' },
{ code: '600000', name: '浦发银行' },
{ code: '600036', name: '招商银行' },
{ code: '601166', name: '兴业银行' },
{ code: '601169', name: '北京银行' },
{ code: '601288', name: '农业银行' },
{ code: '601328', name: '交通银行' },
{ code: '601398', name: '工商银行' },
{ code: '601818', name: '光大银行' },
{ code: '601939', name: '建设银行' },
{ code: '601998', name: '中信银行' },
// 证券
{ code: '600030', name: '中信证券' },
{ code: '600109', name: '国金证券' },
{ code: '600837', name: '海通证券' },
{ code: '600999', name: '招商证券' },
{ code: '601688', name: '华泰证券' },
{ code: '601901', name: '方正证券' },
// 保险
{ code: '601318', name: '中国平安' },
{ code: '601336', name: '新华保险' },
{ code: '601601', name: '中国太保' },
{ code: '601628', name: '中国人寿' },
// 白酒/食品饮料
{ code: '000568', name: '泸州老窖' },
{ code: '000596', name: '古井贡酒' },
{ code: '000858', name: '五粮液' },
{ code: '600519', name: '贵州茅台' },
{ code: '600600', name: '青岛啤酒' },
{ code: '600779', name: '水井坊' },
{ code: '603369', name: '今世缘' },
// 医药
{ code: '000538', name: '云南白药' },
{ code: '000661', name: '长春高新' },
{ code: '002422', name: '科伦药业' },
{ code: '002594', name: '比亚迪' },
{ code: '600276', name: '恒瑞医药' },
{ code: '600436', name: '片仔癀' },
{ code: '603259', name: '药明康德' },
// 科技/半导体
{ code: '000063', name: '中兴通讯' },
{ code: '000725', name: '京东方A' },
{ code: '002049', name: '紫光国微' },
{ code: '002415', name: '海康威视' },
{ code: '002475', name: '立讯精密' },
{ code: '600584', name: '长电科技' },
{ code: '600893', name: '航发动力' },
{ code: '603501', name: '韦尔股份' },
// 新能源/电力
{ code: '000002', name: '万科A' },
{ code: '002460', name: '赣锋锂业' },
{ code: '300750', name: '宁德时代' },
{ code: '600438', name: '通威股份' },
{ code: '601012', name: '隆基绿能' },
{ code: '601668', name: '中国建筑' },
// 汽车
{ code: '000625', name: '长安汽车' },
{ code: '600066', name: '宇通客车' },
{ code: '600104', name: '上汽集团' },
{ code: '601238', name: '广汽集团' },
{ code: '601633', name: '长城汽车' },
// 地产
{ code: '000069', name: '华侨城A' },
{ code: '600340', name: '华夏幸福' },
{ code: '600606', name: '绿地控股' },
// 家电
{ code: '000333', name: '美的集团' },
{ code: '000651', name: '格力电器' },
{ code: '002032', name: '苏泊尔' },
{ code: '600690', name: '海尔智家' },
// 互联网/电商
{ code: '002024', name: '苏宁易购' },
{ code: '002074', name: '国轩高科' },
{ code: '300059', name: '东方财富' },
// 能源/化工
{ code: '600028', name: '中国石化' },
{ code: '600309', name: '万华化学' },
{ code: '600547', name: '山东黄金' },
{ code: '600585', name: '海螺水泥' },
{ code: '601088', name: '中国神华' },
{ code: '601857', name: '中国石油' },
// 电信/运营商
{ code: '600050', name: '中国联通' },
{ code: '600941', name: '中国移动' },
{ code: '601728', name: '中国电信' },
// 其他蓝筹
{ code: '600887', name: '伊利股份' },
{ code: '601111', name: '中国国航' },
{ code: '601390', name: '中国中铁' },
{ code: '601899', name: '紫金矿业' },
{ code: '603288', name: '海天味业' },
];
// 添加拼音缩写
return stocks.map((s) => ({
...s,
pinyin_abbr: generatePinyinAbbr(s.name),
}));
};
// ============ Mock Handlers ============
export const stockHandlers = [
// 搜索股票(个股中心页面使用)- 支持模糊搜索
http.get('/api/stocks/search', async ({ request }) => {
console.log('[Mock Stock] ✅ 搜索请求已被 MSW 拦截:', request.url);
await delay(200);
const url = new URL(request.url);
const query = (url.searchParams.get('q') || '').toLowerCase().trim();
const limit = parseInt(url.searchParams.get('limit') || '10');
console.log('[Mock Stock] 搜索股票:', { query, limit });
const stocks = generateStockList();
// 如果没有搜索词,返回空结果
if (!query) {
return HttpResponse.json({
success: true,
data: [],
});
}
// 模糊搜索:代码 + 名称 + 拼音缩写(不区分大小写)
const results = stocks.filter((s) => {
const code = s.code.toLowerCase();
const name = s.name.toLowerCase();
const pinyin = (s.pinyin_abbr || '').toLowerCase();
return code.includes(query) || name.includes(query) || pinyin.includes(query);
});
// 按相关性排序:完全匹配 > 开头匹配 > 包含匹配
results.sort((a, b) => {
const aCode = a.code.toLowerCase();
const bCode = b.code.toLowerCase();
const aName = a.name.toLowerCase();
const bName = b.name.toLowerCase();
const aPinyin = (a.pinyin_abbr || '').toLowerCase();
const bPinyin = (b.pinyin_abbr || '').toLowerCase();
// 计算匹配分数(包含拼音匹配)
const getScore = (code: string, name: string, pinyin: string): number => {
if (code === query || name === query || pinyin === query) return 100; // 完全匹配
if (code.startsWith(query)) return 80; // 代码开头
if (pinyin.startsWith(query)) return 70; // 拼音开头
if (name.startsWith(query)) return 60; // 名称开头
if (code.includes(query)) return 40; // 代码包含
if (pinyin.includes(query)) return 30; // 拼音包含
if (name.includes(query)) return 20; // 名称包含
return 0;
};
return getScore(bCode, bName, bPinyin) - getScore(aCode, aName, aPinyin);
});
// 返回格式化数据
const searchResults: SearchResultItem[] = results.slice(0, limit).map((s) => ({
stock_code: s.code,
stock_name: s.name,
pinyin_abbr: s.pinyin_abbr,
market: (s.code.startsWith('6') ? 'SH' : 'SZ') as 'SH' | 'SZ',
industry: ['银行', '证券', '保险', '白酒', '医药', '科技', '新能源', '汽车', '地产', '家电'][Math.floor(Math.random() * 10)],
change_pct: parseFloat((Math.random() * 10 - 3).toFixed(2)),
price: parseFloat((Math.random() * 100 + 5).toFixed(2)),
}));
return HttpResponse.json({
success: true,
data: searchResults,
});
}),
// 获取所有股票列表
http.get('/api/stocklist', async () => {
await delay(200);
try {
const stocks = generateStockList();
// console.log('[Mock Stock] 获取股票列表成功:', { count: stocks.length }); // 已关闭:减少日志
return HttpResponse.json(stocks);
} catch (error) {
console.error('[Mock Stock] 获取股票列表失败:', error);
return HttpResponse.json({ error: '获取股票列表失败' }, { status: 500 });
}
}),
// 获取指数K线数据
http.get('/api/index/:indexCode/kline', async ({ params, request }) => {
await delay(300);
const { indexCode } = params;
const url = new URL(request.url);
const type = url.searchParams.get('type') || 'timeline';
const eventTime = url.searchParams.get('event_time');
console.log('[Mock Stock] 获取指数K线数据:', { indexCode, type, eventTime });
try {
let data;
if (type === 'timeline' || type === 'minute') {
// timeline 和 minute 都使用分时数据
data = generateTimelineData(indexCode as string);
} else if (type === 'daily') {
data = generateDailyData(indexCode as string, 30);
} else {
// 其他类型也降级使用 timeline 数据
console.log('[Mock Stock] 未知类型,降级使用 timeline:', type);
data = generateTimelineData(indexCode as string);
}
return HttpResponse.json({
success: true,
data: data,
index_code: indexCode,
type: type,
message: '获取成功',
});
} catch (error) {
console.error('[Mock Stock] 获取K线数据失败:', error);
return HttpResponse.json({ error: '获取K线数据失败' }, { status: 500 });
}
}),
// 获取股票K线数据
http.get('/api/stock/:stockCode/kline', async ({ params, request }) => {
await delay(300);
const { stockCode } = params;
const url = new URL(request.url);
const type = url.searchParams.get('type') || 'timeline';
const eventTime = url.searchParams.get('event_time');
console.log('[Mock Stock] 获取股票K线数据:', { stockCode, type, eventTime });
try {
let data;
if (type === 'timeline' || type === 'minute') {
// minute 和 timeline 使用相同的分时数据
data = generateTimelineData('000001.SH');
} else if (type === 'daily') {
data = generateDailyData('000001.SH', 30);
} else {
return HttpResponse.json({ error: '不支持的类型' }, { status: 400 });
}
return HttpResponse.json({
success: true,
data: data,
stock_code: stockCode,
type: type,
message: '获取成功',
});
} catch (error) {
console.error('[Mock Stock] 获取股票K线数据失败:', error);
return HttpResponse.json({ error: '获取K线数据失败' }, { status: 500 });
}
}),
// 批量获取股票K线数据
http.post('/api/stock/batch-kline', async ({ request }) => {
await delay(400);
try {
const body = (await request.json()) as BatchKlineRequest;
const { codes, type = 'timeline', event_time } = body;
console.log('[Mock Stock] 批量获取K线数据:', {
stockCount: codes?.length,
type,
eventTime: event_time,
});
if (!codes || !Array.isArray(codes) || codes.length === 0) {
return HttpResponse.json({ error: '股票代码列表不能为空' }, { status: 400 });
}
// 为每只股票生成数据
const batchData: Record<string, { success: boolean; data: unknown; stock_code: string }> = {};
codes.forEach((stockCode) => {
let data;
if (type === 'timeline') {
data = generateTimelineData('000001.SH');
} else if (type === 'daily') {
data = generateDailyData('000001.SH', 60);
} else {
data = [];
}
batchData[stockCode] = {
success: true,
data: data,
stock_code: stockCode,
};
});
return HttpResponse.json({
success: true,
data: batchData,
type: type,
message: '批量获取成功',
});
} catch (error) {
console.error('[Mock Stock] 批量获取K线数据失败:', error);
return HttpResponse.json({ error: '批量获取K线数据失败' }, { status: 500 });
}
}),
// 获取股票业绩预告
http.get('/api/stock/:stockCode/forecast', async ({ params }) => {
await delay(200);
const { stockCode } = params;
console.log('[Mock Stock] 获取业绩预告:', { stockCode });
// 生成股票列表用于查找名称
const stockList = generateStockList();
const stockInfo = stockList.find((s) => s.code === (stockCode as string).replace(/\.(SH|SZ)$/i, ''));
const stockName = stockInfo?.name || `股票${stockCode}`;
// 业绩预告类型列表
const forecastTypes = ['预增', '预减', '略增', '略减', '扭亏', '续亏', '首亏', '续盈'];
// 生成业绩预告数据
const forecasts: ForecastItem[] = [
{
forecast_type: '预增',
report_date: '2024年年报',
content: `${stockName}预计2024年度归属于上市公司股东的净利润为58亿元至62亿元同比增长10%至17%。`,
reason: '报告期内,公司主营业务收入稳步增长,产品结构持续优化,毛利率提升;同时公司加大研发投入,新产品市场表现良好。',
change_range: {
lower: 10,
upper: 17,
},
publish_date: '2024-10-15',
},
{
forecast_type: '略增',
report_date: '2024年三季报',
content: `${stockName}预计2024年1-9月归属于上市公司股东的净利润为42亿元至45亿元同比增长5%至12%。`,
reason: '公司积极拓展市场渠道,销售规模持续扩大,经营效益稳步提升。',
change_range: {
lower: 5,
upper: 12,
},
publish_date: '2024-07-12',
},
{
forecast_type: forecastTypes[Math.floor(Math.random() * forecastTypes.length)],
report_date: '2024年中报',
content: `${stockName}预计2024年上半年归属于上市公司股东的净利润为28亿元至30亿元。`,
reason: '受益于行业景气度回升及公司降本增效措施效果显现,经营业绩同比有所改善。',
change_range: {
lower: 3,
upper: 8,
},
publish_date: '2024-04-20',
},
];
return HttpResponse.json({
success: true,
data: {
stock_code: stockCode,
stock_name: stockName,
forecasts: forecasts,
},
});
}),
// 获取股票报价(批量)
http.post('/api/stock/quotes', async ({ request }) => {
await delay(200);
try {
const body = (await request.json()) as QuotesRequest;
const { codes, event_time } = body;
console.log('[Mock Stock] 获取股票报价:', {
stockCount: codes?.length,
eventTime: event_time,
});
if (!codes || !Array.isArray(codes) || codes.length === 0) {
return HttpResponse.json({ success: false, error: '股票代码列表不能为空' }, { status: 400 });
}
// 生成股票列表用于查找名称
const stockList = generateStockList();
const stockMap: Record<string, string> = {};
stockList.forEach((s) => {
stockMap[s.code] = s.name;
});
// 行业和指数映射表
const stockIndustryMap: Record<string, IndustryInfo> = {
'000001': { industry_l1: '金融', industry: '银行', index_tags: ['沪深300', '上证180'] },
'600519': { industry_l1: '消费', industry: '白酒', index_tags: ['沪深300', '上证50'] },
'300750': { industry_l1: '工业', industry: '电池', index_tags: ['创业板50'] },
'601318': { industry_l1: '金融', industry: '保险', index_tags: ['沪深300', '上证50'] },
'600036': { industry_l1: '金融', industry: '银行', index_tags: ['沪深300', '上证50'] },
'000858': { industry_l1: '消费', industry: '白酒', index_tags: ['沪深300'] },
'002594': { industry_l1: '汽车', industry: '乘用车', index_tags: ['沪深300', '创业板指'] },
};
const defaultIndustries: IndustryInfo[] = [
{ industry_l1: '科技', industry: '软件' },
{ industry_l1: '医药', industry: '化学制药' },
{ industry_l1: '消费', industry: '食品' },
{ industry_l1: '金融', industry: '证券' },
{ industry_l1: '工业', industry: '机械' },
];
// 为每只股票生成报价数据
const quotesData: Record<string, StockQuote> = {};
codes.forEach((stockCode) => {
// 生成基础价格10-200之间
const basePrice = parseFloat((Math.random() * 190 + 10).toFixed(2));
// 涨跌幅(-10% 到 +10%
const changePercent = parseFloat((Math.random() * 20 - 10).toFixed(2));
// 涨跌额
const change = parseFloat(((basePrice * changePercent) / 100).toFixed(2));
// 昨收
const prevClose = parseFloat((basePrice - change).toFixed(2));
// 获取行业和指数信息
const codeWithoutSuffix = stockCode.replace(/\.(SH|SZ)$/i, '');
const industryInfo = stockIndustryMap[codeWithoutSuffix] || defaultIndustries[Math.floor(Math.random() * defaultIndustries.length)];
quotesData[stockCode] = {
code: stockCode,
name: stockMap[stockCode] || `股票${stockCode}`,
price: basePrice,
change: change,
change_percent: changePercent,
prev_close: prevClose,
open: parseFloat((prevClose * (1 + (Math.random() * 0.02 - 0.01))).toFixed(2)),
high: parseFloat((basePrice * (1 + Math.random() * 0.05)).toFixed(2)),
low: parseFloat((basePrice * (1 - Math.random() * 0.05)).toFixed(2)),
volume: Math.floor(Math.random() * 100000000),
amount: parseFloat((Math.random() * 10000000000).toFixed(2)),
market: stockCode.startsWith('6') ? 'SH' : 'SZ',
update_time: new Date().toISOString(),
// 行业和指数标签
industry_l1: industryInfo.industry_l1,
industry: industryInfo.industry,
index_tags: industryInfo.index_tags || [],
// 关键指标
pe: parseFloat((Math.random() * 50 + 5).toFixed(2)),
eps: parseFloat((Math.random() * 5 + 0.1).toFixed(3)),
pb: parseFloat((Math.random() * 8 + 0.5).toFixed(2)),
market_cap: `${(Math.random() * 5000 + 100).toFixed(0)}亿`,
week52_low: parseFloat((basePrice * 0.7).toFixed(2)),
week52_high: parseFloat((basePrice * 1.3).toFixed(2)),
// 主力动态
main_net_inflow: parseFloat((Math.random() * 10 - 5).toFixed(2)),
institution_holding: parseFloat((Math.random() * 50 + 10).toFixed(2)),
buy_ratio: parseFloat((Math.random() * 40 + 30).toFixed(2)),
sell_ratio: parseFloat((100 - (Math.random() * 40 + 30)).toFixed(2)),
};
});
return HttpResponse.json({
success: true,
data: quotesData,
message: '获取成功',
});
} catch (error) {
console.error('[Mock Stock] 获取股票报价失败:', error);
return HttpResponse.json({ success: false, error: '获取股票报价失败' }, { status: 500 });
}
}),
// 获取股票详细行情quote-detail
http.get('/api/stock/:stockCode/quote-detail', async ({ params }) => {
await delay(200);
const { stockCode } = params;
console.log('[Mock Stock] 获取股票详细行情:', { stockCode });
const stocks = generateStockList();
const codeWithoutSuffix = (stockCode as string).replace(/\.(SH|SZ)$/i, '');
const stockInfo = stocks.find((s) => s.code === codeWithoutSuffix);
const stockName = stockInfo?.name || `股票${stockCode}`;
// 生成基础价格10-200之间
const basePrice = parseFloat((Math.random() * 190 + 10).toFixed(2));
// 涨跌幅(-10% 到 +10%
const changePercent = parseFloat((Math.random() * 20 - 10).toFixed(2));
// 涨跌额
const change = parseFloat(((basePrice * changePercent) / 100).toFixed(2));
// 昨收
const prevClose = parseFloat((basePrice - change).toFixed(2));
const quoteDetail: StockQuoteDetail = {
stock_code: stockCode as string,
stock_name: stockName,
code: stockCode as string,
name: stockName,
price: basePrice,
change: change,
change_percent: changePercent,
prev_close: prevClose,
open: parseFloat((prevClose * (1 + (Math.random() * 0.02 - 0.01))).toFixed(2)),
high: parseFloat((basePrice * (1 + Math.random() * 0.05)).toFixed(2)),
low: parseFloat((basePrice * (1 - Math.random() * 0.05)).toFixed(2)),
volume: Math.floor(Math.random() * 100000000),
amount: parseFloat((Math.random() * 10000000000).toFixed(2)),
turnover_rate: parseFloat((Math.random() * 10).toFixed(2)),
amplitude: parseFloat((Math.random() * 8).toFixed(2)),
market: (stockCode as string).startsWith('6') ? 'SH' : 'SZ',
update_time: new Date().toISOString(),
// 买卖盘口
bid1: parseFloat((basePrice * 0.998).toFixed(2)),
bid1_volume: Math.floor(Math.random() * 10000),
bid2: parseFloat((basePrice * 0.996).toFixed(2)),
bid2_volume: Math.floor(Math.random() * 10000),
bid3: parseFloat((basePrice * 0.994).toFixed(2)),
bid3_volume: Math.floor(Math.random() * 10000),
bid4: parseFloat((basePrice * 0.992).toFixed(2)),
bid4_volume: Math.floor(Math.random() * 10000),
bid5: parseFloat((basePrice * 0.99).toFixed(2)),
bid5_volume: Math.floor(Math.random() * 10000),
ask1: parseFloat((basePrice * 1.002).toFixed(2)),
ask1_volume: Math.floor(Math.random() * 10000),
ask2: parseFloat((basePrice * 1.004).toFixed(2)),
ask2_volume: Math.floor(Math.random() * 10000),
ask3: parseFloat((basePrice * 1.006).toFixed(2)),
ask3_volume: Math.floor(Math.random() * 10000),
ask4: parseFloat((basePrice * 1.008).toFixed(2)),
ask4_volume: Math.floor(Math.random() * 10000),
ask5: parseFloat((basePrice * 1.01).toFixed(2)),
ask5_volume: Math.floor(Math.random() * 10000),
// 关键指标
pe: parseFloat((Math.random() * 50 + 5).toFixed(2)),
pb: parseFloat((Math.random() * 8 + 0.5).toFixed(2)),
eps: parseFloat((Math.random() * 5 + 0.1).toFixed(3)),
market_cap: `${(Math.random() * 5000 + 100).toFixed(0)}亿`,
circulating_market_cap: `${(Math.random() * 3000 + 50).toFixed(0)}亿`,
total_shares: `${(Math.random() * 100 + 10).toFixed(2)}亿`,
circulating_shares: `${(Math.random() * 80 + 5).toFixed(2)}亿`,
week52_high: parseFloat((basePrice * 1.3).toFixed(2)),
week52_low: parseFloat((basePrice * 0.7).toFixed(2)),
};
return HttpResponse.json({
success: true,
data: quoteDetail,
message: '获取成功',
});
}),
// FlexScreen 行情数据
http.get('/api/flex-screen/quotes', async ({ request }) => {
await delay(200);
const url = new URL(request.url);
const codes = url.searchParams.get('codes')?.split(',') || [];
console.log('[Mock Stock] 获取 FlexScreen 行情:', { codes });
// 默认主要指数
const defaultIndices = ['000001', '399001', '399006'];
const targetCodes = codes.length > 0 ? codes : defaultIndices;
const indexData: Record<string, IndexInfo> = {
'000001': { name: '上证指数', basePrice: 3200 },
'399001': { name: '深证成指', basePrice: 10500 },
'399006': { name: '创业板指', basePrice: 2100 },
'000300': { name: '沪深300', basePrice: 3800 },
'000016': { name: '上证50', basePrice: 2600 },
'000905': { name: '中证500', basePrice: 5800 },
};
const quotesData: Record<string, StockQuote> = {};
targetCodes.forEach((code) => {
const codeWithoutSuffix = code.replace(/\.(SH|SZ)$/i, '');
const info = indexData[codeWithoutSuffix] || { name: `指数${code}`, basePrice: 3000 };
const changePercent = parseFloat((Math.random() * 4 - 2).toFixed(2));
const price = parseFloat((info.basePrice * (1 + changePercent / 100)).toFixed(2));
const change = parseFloat((price - info.basePrice).toFixed(2));
quotesData[code] = {
code: code,
name: info.name,
price: price,
change: change,
change_percent: changePercent,
prev_close: info.basePrice,
open: parseFloat((info.basePrice * (1 + (Math.random() * 0.01 - 0.005))).toFixed(2)),
high: parseFloat((price * (1 + Math.random() * 0.01)).toFixed(2)),
low: parseFloat((price * (1 - Math.random() * 0.01)).toFixed(2)),
volume: parseFloat((Math.random() * 5000 + 2000).toFixed(2)), // 亿手
amount: parseFloat((Math.random() * 8000 + 3000).toFixed(2)), // 亿元
market: code.startsWith('6') || code.startsWith('000') ? 'SH' : 'SZ',
update_time: new Date().toISOString(),
};
});
return HttpResponse.json({
success: true,
data: quotesData,
message: '获取成功',
});
}),
];

View File

@@ -30,11 +30,12 @@ export const getApiBase = () => {
};
/**
* 检查是否处于 Mock 模式
* @returns {boolean}
* 检查是否处于 Mock 模式(已禁用)
* @returns {boolean} 总是返回 false
* @deprecated Mock 模式已移除,保留此函数仅为兼容性
*/
export const isMockMode = () => {
return process.env.REACT_APP_ENABLE_MOCK === 'true';
return false;
};
/**
@@ -44,8 +45,7 @@ export const isMockMode = () => {
*
* @example
* const url = getApiUrl('/api/users');
* // Mock 模式: '/api/users'
* // 开发模式: 'http://49.232.185.254:5001/api/users'
* // 返回: 'http://api.example.com/api/users'
*/
export const getApiUrl = (path) => {
return getApiBase() + path;

View File

@@ -5,24 +5,12 @@
import type { Exchange } from '../types';
import { getApiBase } from '@utils/apiConfig';
/** 是否为 Mock 模式 */
export const IS_MOCK_MODE = process.env.REACT_APP_ENABLE_MOCK === 'true';
/**
* 获取 WebSocket 配置
* - Mock 模式: 返回空字符串,不连接 WebSocket
* - 生产环境 (HTTPS): 通过 API 服务器 Nginx 代理使用 wss://
* - 开发环境 (HTTP): 直连 ws://
*/
const getWsConfig = (): Record<Exchange, string> => {
// Mock 模式:不连接 WebSocket
if (IS_MOCK_MODE) {
return {
SSE: '',
SZSE: '',
};
}
// 服务端渲染或测试环境使用默认配置
if (typeof window === 'undefined') {
return {

View File

@@ -15,7 +15,7 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { logger } from '@utils/logger';
import { getApiBase } from '@utils/apiConfig';
import { WS_CONFIG, HEARTBEAT_INTERVAL, RECONNECT_INTERVAL, IS_MOCK_MODE } from './constants';
import { WS_CONFIG, HEARTBEAT_INTERVAL, RECONNECT_INTERVAL } from './constants';
import { getExchange, normalizeCode, calcChangePct } from './utils';
import type {
Exchange,
@@ -38,43 +38,6 @@ import type {
/** 最大重连次数 */
const MAX_RECONNECT_ATTEMPTS = 5;
/**
* 生成 Mock 行情数据
*/
const generateMockQuote = (code: string): QuoteData => {
const exchange: Exchange = code.endsWith('.SH') ? 'SSE' : 'SZSE';
const basePrice = 10 + Math.random() * 90; // 10-100 之间的随机价格
const prevClose = basePrice * (0.95 + Math.random() * 0.1); // 前收盘价在基准价格附近
const change = basePrice - prevClose;
const changePct = (change / prevClose) * 100;
// 生成五档买卖盘
const bidPrices = Array.from({ length: 5 }, (_, i) => +(basePrice - 0.01 * (i + 1)).toFixed(2));
const askPrices = Array.from({ length: 5 }, (_, i) => +(basePrice + 0.01 * (i + 1)).toFixed(2));
const bidVolumes = Array.from({ length: 5 }, () => Math.floor(Math.random() * 10000) * 100);
const askVolumes = Array.from({ length: 5 }, () => Math.floor(Math.random() * 10000) * 100);
return {
code,
name: `Mock股票${code.slice(0, 4)}`,
price: +basePrice.toFixed(2),
prevClose: +prevClose.toFixed(2),
open: +(prevClose * (0.99 + Math.random() * 0.02)).toFixed(2),
high: +(basePrice * (1 + Math.random() * 0.05)).toFixed(2),
low: +(basePrice * (1 - Math.random() * 0.05)).toFixed(2),
volume: Math.floor(Math.random() * 100000000),
amount: Math.floor(Math.random() * 1000000000),
change: +change.toFixed(2),
changePct: +changePct.toFixed(2),
bidPrices,
bidVolumes,
askPrices,
askVolumes,
updateTime: new Date().toISOString(),
exchange,
} as QuoteData;
};
/**
* 处理上交所消息
*/
@@ -601,12 +564,6 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
* 创建 WebSocket 连接
*/
const createConnection = useCallback((exchange: Exchange) => {
// Mock 模式:跳过 WebSocket 连接
if (IS_MOCK_MODE) {
logger.info('FlexScreen', `${exchange} Mock 模式,跳过 WebSocket 连接`);
return;
}
const isHttps = typeof window !== 'undefined' && window.location.protocol === 'https:';
const wsUrl = WS_CONFIG[exchange];
@@ -786,41 +743,6 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
const allNewCodes = [...newSseCodes, ...newSzseCodes];
// Mock 模式:生成 Mock 数据并定时更新
if (IS_MOCK_MODE) {
logger.info('FlexScreen', 'Mock 模式,使用本地 Mock 数据');
// 生成初始 Mock 数据
const mockQuotes: QuotesMap = {};
allNewCodes.forEach(code => {
mockQuotes[code] = generateMockQuote(code);
});
setQuotes(mockQuotes);
// 定时更新 Mock 数据(模拟实时行情)
const mockInterval = setInterval(() => {
setQuotes(prev => {
const updated = { ...prev };
Object.keys(updated).forEach(code => {
const quote = updated[code];
// 小幅波动价格
const priceChange = (Math.random() - 0.5) * 0.1;
const newPrice = +(quote.price + priceChange).toFixed(2);
updated[code] = {
...quote,
price: newPrice,
change: +(newPrice - quote.prevClose).toFixed(2),
changePct: +((newPrice - quote.prevClose) / quote.prevClose * 100).toFixed(2),
updateTime: new Date().toISOString(),
};
});
return updated;
});
}, 3000); // 每 3 秒更新一次
return () => clearInterval(mockInterval);
}
// 检查是否有新增的代码需要加载初始数据
const codesToLoad = allNewCodes.filter(c => !initialLoadedRef.current.has(c));