- event.js: 修复 /api/events 返回格式,匹配 useEventData 期望的结构 - stock.js: 添加 /api/stock/:code/quote-detail handler(完整行情数据含买卖盘) - stock.js: 添加 /api/flex-screen/quotes handler(指数行情) - stock.js: 修复 /api/index/:code/kline 支持 minute 类型 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
691 lines
29 KiB
JavaScript
691 lines
29 KiB
JavaScript
// 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: '获取成功'
|
||
});
|
||
}),
|
||
];
|