更新ios
This commit is contained in:
41
.env.mock
41
.env.mock
@@ -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
|
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prestart": "kill-port 3000",
|
"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",
|
"prestart:real": "kill-port 3000",
|
||||||
"start:real": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' env-cmd -f .env.local craco start",
|
"start:real": "NODE_OPTIONS='--openssl-legacy-provider --max_old_space_size=4096' env-cmd -f .env.local craco start",
|
||||||
"prestart:dev": "kill-port 3000",
|
"prestart:dev": "kill-port 3000",
|
||||||
@@ -113,7 +113,6 @@
|
|||||||
"kill-port": "^2.0.1",
|
"kill-port": "^2.0.1",
|
||||||
"less": "^4.4.2",
|
"less": "^4.4.2",
|
||||||
"less-loader": "^12.3.0",
|
"less-loader": "^12.3.0",
|
||||||
"msw": "^2.11.5",
|
|
||||||
"prettier": "2.2.1",
|
"prettier": "2.2.1",
|
||||||
"react-error-overlay": "6.0.9",
|
"react-error-overlay": "6.0.9",
|
||||||
"sharp": "^0.34.4",
|
"sharp": "^0.34.4",
|
||||||
@@ -134,11 +133,6 @@
|
|||||||
"not op_mini all"
|
"not op_mini all"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"msw": {
|
|
||||||
"workerDirectory": [
|
|
||||||
"public"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"fsevents": "^2.3.3"
|
"fsevents": "^2.3.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -103,23 +103,6 @@ const styles = {
|
|||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
fontWeight: 500,
|
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: {
|
iframeLoading: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: 0,
|
top: 0,
|
||||||
@@ -414,27 +397,6 @@ const WechatRegister = forwardRef(function WechatRegister({ subtitle }, ref) {
|
|||||||
{getStatusText(wechatStatus)}
|
{getStatusText(wechatStatus)}
|
||||||
</Text>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -661,12 +661,6 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
|
|
||||||
// ========== 连接到 Socket 服务(⚡ 异步初始化,不阻塞首屏) ==========
|
// ========== 连接到 Socket 服务(⚡ 异步初始化,不阻塞首屏) ==========
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// ⚡ Mock 模式下跳过 Socket 连接(避免连接生产服务器失败的错误)
|
|
||||||
if (process.env.REACT_APP_ENABLE_MOCK === 'true') {
|
|
||||||
logger.debug('NotificationContext', 'Mock 模式,跳过 Socket 连接');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ⚡ 防止 React Strict Mode 导致的重复初始化
|
// ⚡ 防止 React Strict Mode 导致的重复初始化
|
||||||
if (socketInitialized) {
|
if (socketInitialized) {
|
||||||
logger.debug('NotificationContext', 'Socket 已初始化,跳过重复执行(Strict Mode 保护)');
|
logger.debug('NotificationContext', 'Socket 已初始化,跳过重复执行(Strict Mode 保护)');
|
||||||
|
|||||||
24
src/index.js
24
src/index.js
@@ -52,11 +52,6 @@ if (process.env.REACT_APP_ENABLE_DEBUG === 'true') {
|
|||||||
|
|
||||||
// 注册 Service Worker(用于支持浏览器通知)
|
// 注册 Service Worker(用于支持浏览器通知)
|
||||||
function registerServiceWorker() {
|
function registerServiceWorker() {
|
||||||
// ⚠️ Mock 模式下跳过 Service Worker 注册(避免与 MSW 冲突)
|
|
||||||
if (process.env.REACT_APP_ENABLE_MOCK === 'true') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 仅在支持 Service Worker 的浏览器中注册
|
// 仅在支持 Service Worker 的浏览器中注册
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
@@ -87,26 +82,9 @@ function renderApp() {
|
|||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 注册 Service Worker(非 Mock 模式)
|
// 注册 Service Worker
|
||||||
registerServiceWorker();
|
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();
|
renderApp();
|
||||||
}
|
|
||||||
|
|
||||||
// 启动应用
|
|
||||||
startApp();
|
|
||||||
@@ -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
@@ -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亿
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -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: "建筑装饰和其他建筑业" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -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}`;
|
|
||||||
}
|
|
||||||
@@ -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'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
@@ -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] 清除当前登录用户');
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
@@ -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- **PE(TTM)**: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- RSI:58,处于中性区域\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- RSI:58,处于中性区域\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',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
@@ -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. 检查 Session(AuthContext 使用的正确端点)
|
|
||||||
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: '退出成功'
|
|
||||||
});
|
|
||||||
})
|
|
||||||
];
|
|
||||||
@@ -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;
|
|
||||||
@@ -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
@@ -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' }
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
// }),
|
|
||||||
];
|
|
||||||
@@ -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
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
@@ -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;
|
|
||||||
@@ -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,
|
|
||||||
];
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
];
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
@@ -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_data(SectorDetails 组件需要的格式)
|
|
||||||
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: '高位股数据获取成功',
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
@@ -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_data(SectorDetails 组件需要的格式)
|
|
||||||
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: '高位股数据获取成功',
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
@@ -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)
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
@@ -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,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
];
|
|
||||||
@@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
@@ -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;
|
|
||||||
@@ -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
|
|
||||||
});
|
|
||||||
})
|
|
||||||
];
|
|
||||||
@@ -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: '获取成功'
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
@@ -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: '获取成功',
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
@@ -30,11 +30,12 @@ export const getApiBase = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否处于 Mock 模式
|
* 检查是否处于 Mock 模式(已禁用)
|
||||||
* @returns {boolean}
|
* @returns {boolean} 总是返回 false
|
||||||
|
* @deprecated Mock 模式已移除,保留此函数仅为兼容性
|
||||||
*/
|
*/
|
||||||
export const isMockMode = () => {
|
export const isMockMode = () => {
|
||||||
return process.env.REACT_APP_ENABLE_MOCK === 'true';
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,8 +45,7 @@ export const isMockMode = () => {
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* const url = getApiUrl('/api/users');
|
* const url = getApiUrl('/api/users');
|
||||||
* // Mock 模式: '/api/users'
|
* // 返回: 'http://api.example.com/api/users'
|
||||||
* // 开发模式: 'http://49.232.185.254:5001/api/users'
|
|
||||||
*/
|
*/
|
||||||
export const getApiUrl = (path) => {
|
export const getApiUrl = (path) => {
|
||||||
return getApiBase() + path;
|
return getApiBase() + path;
|
||||||
|
|||||||
@@ -5,24 +5,12 @@
|
|||||||
import type { Exchange } from '../types';
|
import type { Exchange } from '../types';
|
||||||
import { getApiBase } from '@utils/apiConfig';
|
import { getApiBase } from '@utils/apiConfig';
|
||||||
|
|
||||||
/** 是否为 Mock 模式 */
|
|
||||||
export const IS_MOCK_MODE = process.env.REACT_APP_ENABLE_MOCK === 'true';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 WebSocket 配置
|
* 获取 WebSocket 配置
|
||||||
* - Mock 模式: 返回空字符串,不连接 WebSocket
|
|
||||||
* - 生产环境 (HTTPS): 通过 API 服务器 Nginx 代理使用 wss://
|
* - 生产环境 (HTTPS): 通过 API 服务器 Nginx 代理使用 wss://
|
||||||
* - 开发环境 (HTTP): 直连 ws://
|
* - 开发环境 (HTTP): 直连 ws://
|
||||||
*/
|
*/
|
||||||
const getWsConfig = (): Record<Exchange, string> => {
|
const getWsConfig = (): Record<Exchange, string> => {
|
||||||
// Mock 模式:不连接 WebSocket
|
|
||||||
if (IS_MOCK_MODE) {
|
|
||||||
return {
|
|
||||||
SSE: '',
|
|
||||||
SZSE: '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 服务端渲染或测试环境使用默认配置
|
// 服务端渲染或测试环境使用默认配置
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { logger } from '@utils/logger';
|
import { logger } from '@utils/logger';
|
||||||
import { getApiBase } from '@utils/apiConfig';
|
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 { getExchange, normalizeCode, calcChangePct } from './utils';
|
||||||
import type {
|
import type {
|
||||||
Exchange,
|
Exchange,
|
||||||
@@ -38,43 +38,6 @@ import type {
|
|||||||
/** 最大重连次数 */
|
/** 最大重连次数 */
|
||||||
const MAX_RECONNECT_ATTEMPTS = 5;
|
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 连接
|
* 创建 WebSocket 连接
|
||||||
*/
|
*/
|
||||||
const createConnection = useCallback((exchange: Exchange) => {
|
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 isHttps = typeof window !== 'undefined' && window.location.protocol === 'https:';
|
||||||
const wsUrl = WS_CONFIG[exchange];
|
const wsUrl = WS_CONFIG[exchange];
|
||||||
|
|
||||||
@@ -786,41 +743,6 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
|
|||||||
|
|
||||||
const allNewCodes = [...newSseCodes, ...newSzseCodes];
|
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));
|
const codesToLoad = allNewCodes.filter(c => !initialLoadedRef.current.has(c));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user