const API_BASE = '/api/stock';
let user = null;
let balance = 1000000;
let positions = {};
let watchCodes = ['sh600519','sz000858','sh601318','sz000009','sz300750','sz002594'];
let currentStock = null;
let tradeType = 'buy';
let editMode = false;
let chart = null;
const defaultStocks = [
{code:'sh600519',name:'贵州茅台'},{code:'sz000858',name:'五粮液'},
{code:'sh601318',name:'中国平安'},{code:'sz000009',name:'中国宝安'},
{code:'sz300750',name:'宁德时代'},{code:'sz002594',name:'比亚迪'},
{code:'sz000333',name:'美的集团'},{code:'sh600036',name:'招商银行'},
{code:'sh601398',name:'工商银行'},{code:'sz000651',name:'格力电器'}
];
async function api(endpoint, params) {
const url = API_BASE + endpoint + '?' + new URLSearchParams(params).toString();
try {
const res = await fetch(url);
const data = await res.json();
return data;
} catch (e) {
console.error('API Error:', e);
return { code: -1, msg: '网络错误' };
}
}
document.addEventListener('DOMContentLoaded', function() {
const saved = localStorage.getItem('kenUser');
if (saved) {
user = JSON.parse(saved);
if (user.userId) {
loadUserData().then(() => showMain());
} else {
localStorage.removeItem('kenUser');
}
}
bindEvents();
});
function bindEvents() {
document.getElementById('loginBtn').addEventListener('click', login);
document.getElementById('loginPwd').addEventListener('keypress', function(e) { if(e.key==='Enter') login(); });
document.getElementById('logoutBtn').addEventListener('click', logout);
document.querySelectorAll('.tab-item').forEach(function(tab) {
tab.addEventListener('click', function() { switchTab(this.dataset.page); });
});
document.getElementById('searchBarBtn').addEventListener('click', showAddModal);
document.getElementById('closeAddBtn').addEventListener('click', closeAddModal);
document.getElementById('addModal').addEventListener('click', function(e) { if(e.target.id==='addModal') closeAddModal(); });
document.getElementById('doSearchBtn').addEventListener('click', searchStock);
document.getElementById('searchInput').addEventListener('keypress', function(e) { if(e.key==='Enter') searchStock(); });
document.getElementById('editWatchBtn').addEventListener('click', toggleEdit);
document.getElementById('detailBackBtn').addEventListener('click', closeDetail);
document.getElementById('detailFav').addEventListener('click', toggleFav);
document.getElementById('chartTabs').addEventListener('click', function(e) {
if (e.target.classList.contains('chart-tab')) {
document.querySelectorAll('.chart-tab').forEach(function(t){ t.classList.remove('active'); });
e.target.classList.add('active');
loadKline(e.target.dataset.scale);
}
});
document.getElementById('buyBtn').addEventListener('click', function() { openTrade('buy'); });
document.getElementById('sellBtn').addEventListener('click', function() { openTrade('sell'); });
document.getElementById('closeTradeBtn').addEventListener('click', closeTrade);
document.getElementById('tradeModal').addEventListener('click', function(e) { if(e.target.id==='tradeModal') closeTrade(); });
document.getElementById('setMarketPriceBtn').addEventListener('click', setMarketPrice);
document.getElementById('priceMinusBtn').addEventListener('click', function() { adjustPrice(-0.01); });
document.getElementById('pricePlusBtn').addEventListener('click', function() { adjustPrice(0.01); });
document.getElementById('qtyMinusBtn').addEventListener('click', function() { adjustQty(-100); });
document.getElementById('qtyPlusBtn').addEventListener('click', function() { adjustQty(100); });
document.getElementById('tradeMaxLink').addEventListener('click', setMaxQty);
document.getElementById('tradePrice').addEventListener('input', updateTradeTotal);
document.getElementById('tradeQty').addEventListener('input', updateTradeTotal);
document.getElementById('tradeConfirmBtn').addEventListener('click', confirmTrade);
}
async function login() {
var phone = document.getElementById('loginPhone').value.trim();
var pwd = document.getElementById('loginPwd').value;
if (!phone || phone.length < 6) { toast('请输入正确的手机号'); return; }
if (!pwd) { toast('请输入密码'); return; }
toast('登录中...');
// 先尝试登录
var res = await api('/user.jsp', { action: 'login', phone: phone, password: pwd });
if (res.code !== 0) {
// 登录失败,尝试注册
res = await api('/user.jsp', { action: 'register', phone: phone, password: pwd });
if (res.code !== 0) {
toast(res.msg || '登录失败');
return;
}
// 注册成功后再登录
res = await api('/user.jsp', { action: 'login', phone: phone, password: pwd });
if (res.code !== 0) {
toast(res.msg || '登录失败');
return;
}
}
user = {
userId: res.data.userId,
phone: res.data.phone,
name: res.data.nickname || (phone.slice(0,3)+'****'+phone.slice(-4))
};
balance = res.data.balance || 1000000;
localStorage.setItem('kenUser', JSON.stringify(user));
await loadUserData();
showMain();
toast('登录成功');
}
function logout() {
localStorage.removeItem('kenUser');
user = null;
balance = 1000000;
positions = {};
watchCodes = [];
document.getElementById('loginPage').classList.remove('hide');
document.getElementById('mainApp').classList.remove('show');
}
async function loadUserData() {
if (!user || !user.userId) return;
// 加载余额
var balRes = await api('/trade.jsp', { action: 'balance', userId: user.userId });
if (balRes.code === 0) balance = balRes.data.balance;
// 加载自选股
var watchRes = await api('/watchlist.jsp', { action: 'list', userId: user.userId });
if (watchRes.code === 0) {
watchCodes = watchRes.data.map(function(w) { return w.stockCode; });
}
// 加载持仓
var posRes = await api('/trade.jsp', { action: 'list', userId: user.userId });
if (posRes.code === 0) {
positions = {};
posRes.data.forEach(function(p) {
positions[p.stockCode] = {
code: p.stockCode,
name: p.stockName,
qty: p.quantity,
cost: p.costPrice,
currentPrice: p.costPrice
};
});
}
}
function showMain() {
document.getElementById('loginPage').classList.add('hide');
document.getElementById('mainApp').classList.add('show');
document.getElementById('headerAvatar').textContent = user.name.charAt(0);
document.getElementById('profileAvatar').textContent = user.name.charAt(0);
document.getElementById('profileName').textContent = user.name;
document.getElementById('profilePhone').textContent = user.phone;
loadMarketBar();
loadWatchList();
updatePortfolio();
setInterval(function() { loadMarketBar(); loadWatchList(); }, 30000);
}
function switchTab(page) {
document.querySelectorAll('.tab-item').forEach(function(t){ t.classList.remove('active'); });
document.querySelector('[data-page="'+page+'"]').classList.add('active');
document.querySelectorAll('.page').forEach(function(p){ p.classList.remove('active'); });
document.getElementById(page).classList.add('active');
if (page==='pagePortfolio') updatePortfolio();
}
function loadMarketBar() {
loadSina('sh000001,sz399001,sz399006', function() {
var bar = document.getElementById('marketBar');
bar.innerHTML = '';
[['sh000001','上证指数'],['sz399001','深证成指'],['sz399006','创业板指']].forEach(function(item) {
var code = item[0], name = item[1];
var d = window['hq_str_'+code];
if (!d) return;
var p = d.split(',');
var price = parseFloat(p[3]);
var prev = parseFloat(p[2]);
var chg = ((price-prev)/prev*100).toFixed(2);
var up = chg >= 0;
var div = document.createElement('div');
div.className = 'market-item';
div.innerHTML = '
'+name+'
'+price.toFixed(2)+'
'+(up?'↑':'↓')+(up?'+':'')+chg+'%
';
div.onclick = function() { openDetail(code); };
bar.appendChild(div);
});
});
}
function loadWatchList() {
var list = document.getElementById('watchList');
if (!watchCodes.length) {
list.innerHTML = '
暂无自选股
';
document.getElementById('emptyAddBtn').addEventListener('click', showAddModal);
return;
}
loadSina(watchCodes.join(','), function() {
list.innerHTML = '';
watchCodes.forEach(function(code) {
var d = window['hq_str_'+code];
if (!d) return;
var p = d.split(',');
var name = p[0];
var price = parseFloat(p[3]);
var prev = parseFloat(p[2]);
var chg = ((price-prev)/prev*100).toFixed(2);
var up = chg >= 0;
var card = document.createElement('div');
card.className = 'stock-card' + (editMode ? ' editing' : '');
card.innerHTML = '
'+name+'
'+code.toUpperCase()+'
'+price.toFixed(2)+'
'+(up?'+':'')+chg+'%
';
if (!editMode) {
card.onclick = function() { openDetail(code); };
}
card.querySelector('.del-btn').onclick = function(e) {
e.stopPropagation();
removeWatch(code);
};
list.appendChild(card);
});
});
}
function toggleEdit() {
editMode = !editMode;
document.getElementById('editWatchBtn').textContent = editMode ? '完成' : '编辑';
loadWatchList();
}
async function removeWatch(code) {
if (!user || !user.userId) return;
var res = await api('/watchlist.jsp', { action: 'remove', userId: user.userId, stockCode: code });
if (res.code === 0) {
watchCodes = watchCodes.filter(function(c) { return c !== code; });
loadWatchList();
toast('已移除');
} else {
toast(res.msg || '操作失败');
}
}
function showAddModal() {
document.getElementById('addModal').classList.add('show');
document.getElementById('searchInput').value = '';
showDefaultStocks();
}
function closeAddModal() {
document.getElementById('addModal').classList.remove('show');
}
function showDefaultStocks() {
var html = '';
defaultStocks.forEach(function(s) {
var added = watchCodes.indexOf(s.code) >= 0;
html += '
'+s.name+'
'+s.code.toUpperCase()+'
';
});
document.getElementById('searchResults').innerHTML = html;
bindAddButtons();
}
function bindAddButtons() {
document.querySelectorAll('.search-results .add-btn').forEach(function(btn) {
btn.onclick = function() {
addWatch(this.dataset.code, this.dataset.name, this);
};
});
}
function searchStock() {
var code = document.getElementById('searchInput').value.trim();
if (!code) { showDefaultStocks(); return; }
if (/^\d{6}$/.test(code)) {
code = (code[0]==='6'||code[0]==='5'||code[0]==='9') ? 'sh'+code : 'sz'+code;
} else if (!/^(sh|sz)\d{6}$/i.test(code)) {
toast('请输入6位股票代码'); return;
}
code = code.toLowerCase();
loadSina(code, function() {
var d = window['hq_str_'+code];
if (!d || !d.length) {
document.getElementById('searchResults').innerHTML = '
未找到该股票
';
return;
}
var name = d.split(',')[0];
var added = watchCodes.indexOf(code) >= 0;
document.getElementById('searchResults').innerHTML = '
'+name+'
'+code.toUpperCase()+'
';
bindAddButtons();
});
}
async function addWatch(code, name, btn) {
if (watchCodes.indexOf(code) >= 0) { toast('已在自选中'); return; }
if (!user || !user.userId) { toast('请先登录'); return; }
var res = await api('/watchlist.jsp', { action: 'add', userId: user.userId, stockCode: code, stockName: name });
if (res.code === 0) {
watchCodes.push(code);
btn.classList.add('added');
btn.textContent = '已添加';
loadWatchList();
toast('添加成功');
} else {
toast(res.msg || '添加失败');
}
}
function openDetail(code) {
currentStock = { code: code };
document.getElementById('detailPage').classList.add('show');
var isIndex = code.indexOf('000001') >= 0 || code.indexOf('399') >= 0;
document.getElementById('detailTrade').style.display = isIndex ? 'none' : 'flex';
loadSina(code, function() {
var d = window['hq_str_'+code];
if (!d) return;
var p = d.split(',');
currentStock.name = p[0];
currentStock.price = parseFloat(p[3]);
currentStock.prev = parseFloat(p[2]);
currentStock.open = parseFloat(p[1]);
currentStock.high = parseFloat(p[4]);
currentStock.low = parseFloat(p[5]);
currentStock.volume = parseFloat(p[8]);
currentStock.amount = parseFloat(p[9]);
var chg = currentStock.price - currentStock.prev;
var chgPct = (chg/currentStock.prev*100).toFixed(2);
var up = chg >= 0;
document.getElementById('detailName').textContent = currentStock.name;
document.getElementById('detailCode').textContent = code.toUpperCase();
document.getElementById('detailPrice').textContent = currentStock.price.toFixed(2);
document.getElementById('detailPrice').className = 'detail-price ' + (up?'up':'down');
document.getElementById('detailChange').innerHTML = ''+(up?'+':'')+chg.toFixed(2)+' ('+(up?'+':'')+chgPct+'%)';
document.getElementById('dOpen').textContent = currentStock.open.toFixed(2);
document.getElementById('dHigh').textContent = currentStock.high.toFixed(2);
document.getElementById('dLow').textContent = currentStock.low.toFixed(2);
document.getElementById('dPrev').textContent = currentStock.prev.toFixed(2);
document.getElementById('dVol').textContent = fmtVol(currentStock.volume);
document.getElementById('dAmt').textContent = fmtAmt(currentStock.amount);
var isFav = watchCodes.indexOf(code) >= 0;
document.getElementById('detailFav').textContent = isFav ? '★' : '☆';
document.getElementById('detailFav').className = 'detail-fav' + (isFav ? ' active' : '');
});
loadKline(240);
}
function closeDetail() {
document.getElementById('detailPage').classList.remove('show');
}
async function toggleFav() {
if (!currentStock) return;
if (!user || !user.userId) { toast('请先登录'); return; }
var idx = watchCodes.indexOf(currentStock.code);
if (idx >= 0) {
var res = await api('/watchlist.jsp', { action: 'remove', userId: user.userId, stockCode: currentStock.code });
if (res.code === 0) {
watchCodes.splice(idx, 1);
document.getElementById('detailFav').textContent = '☆';
document.getElementById('detailFav').classList.remove('active');
loadWatchList();
toast('已取消自选');
}
} else {
var res = await api('/watchlist.jsp', { action: 'add', userId: user.userId, stockCode: currentStock.code, stockName: currentStock.name });
if (res.code === 0) {
watchCodes.push(currentStock.code);
document.getElementById('detailFav').textContent = '★';
document.getElementById('detailFav').classList.add('active');
loadWatchList();
toast('已添加自选');
}
}
}
function loadKline(scale) {
if (!currentStock) return;
var script = document.createElement('script');
script.src = 'https://quotes.sina.cn/cn/api/jsonp.php/var%20kline=/CN_MarketDataService.getKLineData?symbol='+currentStock.code+'&scale='+scale+'&datalen=60&_='+Date.now();
script.onload = function() {
try {
if (window.kline && window.kline.length) renderChart(window.kline);
else renderChartFallback();
} catch(e) { renderChartFallback(); }
document.head.removeChild(script);
};
script.onerror = function() { renderChartFallback(); document.head.removeChild(script); };
document.head.appendChild(script);
}
function renderChart(data) {
if (!chart) chart = echarts.init(document.getElementById('detailChart'));
var dates = data.map(function(d){ return d.day; });
var ohlc = data.map(function(d){ return [+d.open, +d.close, +d.low, +d.high]; });
var vols = data.map(function(d){ return +d.volume; });
chart.setOption({
backgroundColor: 'transparent',
tooltip: { trigger: 'axis', backgroundColor: '#fff', borderColor: '#e8e8e8', textStyle: { color: '#333', fontSize: 12 } },
grid: [{ left: 50, right: 10, top: 10, height: '55%' }, { left: 50, right: 10, top: '70%', height: '20%' }],
xAxis: [{ type: 'category', data: dates, axisLine: { lineStyle: { color: '#e8e8e8' } }, axisLabel: { color: '#888', fontSize: 10 } },
{ type: 'category', gridIndex: 1, data: dates, axisLabel: { show: false } }],
yAxis: [{ scale: true, axisLine: { show: false }, axisLabel: { color: '#888', fontSize: 10 }, splitLine: { lineStyle: { color: '#f0f0f0' } } },
{ scale: true, gridIndex: 1, axisLabel: { show: false }, splitLine: { show: false } }],
dataZoom: [{ type: 'inside', xAxisIndex: [0,1], start: 60, end: 100 }],
series: [
{ type: 'candlestick', data: ohlc, itemStyle: { color: '#e53935', color0: '#4caf50', borderColor: '#e53935', borderColor0: '#4caf50' } },
{ type: 'bar', xAxisIndex: 1, yAxisIndex: 1, data: vols, itemStyle: { color: function(p) { return ohlc[p.dataIndex][1] >= ohlc[p.dataIndex][0] ? '#e53935' : '#4caf50'; } } }
]
});
}
function renderChartFallback() {
var data = [];
var base = currentStock ? currentStock.price : 100;
for (var i = 60; i >= 0; i--) {
var d = new Date(); d.setDate(d.getDate()-i);
var v = base * 0.02;
var o = base + (Math.random()-0.5)*v;
var c = o + (Math.random()-0.5)*v;
data.push({ day: d.toISOString().slice(0,10), open: o.toFixed(2), close: c.toFixed(2), high: (Math.max(o,c)+Math.random()*v*0.5).toFixed(2), low: (Math.min(o,c)-Math.random()*v*0.5).toFixed(2), volume: Math.floor(Math.random()*5e7+1e7) });
base = c;
}
renderChart(data);
}
function openTrade(type) {
if (!currentStock) return;
tradeType = type;
document.getElementById('tradeModal').classList.add('show');
var title = document.getElementById('tradeTitle');
title.textContent = type==='buy' ? '买入' : '卖出';
title.className = type==='buy' ? 'buy-title' : 'sell-title';
document.getElementById('tradeName').textContent = currentStock.name;
var priceEl = document.getElementById('tradeCurrentPrice');
priceEl.textContent = currentStock.price.toFixed(2);
priceEl.className = 'price ' + (type==='buy' ? 'up' : 'down');
document.getElementById('tradePrice').value = currentStock.price.toFixed(2);
document.getElementById('tradeQty').value = 100;
var btn = document.getElementById('tradeConfirmBtn');
btn.className = 'trade-confirm ' + type;
btn.textContent = type==='buy' ? '确认买入' : '确认卖出';
if (type==='buy') {
document.getElementById('tradeLimitLabel').textContent = '可用资金';
document.getElementById('tradeLimitValue').textContent = '¥' + balance.toLocaleString();
document.getElementById('tradeMaxLink').textContent = '最大可买';
} else {
var pos = positions[currentStock.code];
document.getElementById('tradeLimitLabel').textContent = '可卖数量';
document.getElementById('tradeLimitValue').textContent = (pos ? pos.qty : 0) + ' 股';
document.getElementById('tradeMaxLink').textContent = '全部卖出';
}
updateTradeTotal();
}
function closeTrade() {
document.getElementById('tradeModal').classList.remove('show');
}
function setMarketPrice() {
if (currentStock) document.getElementById('tradePrice').value = currentStock.price.toFixed(2);
updateTradeTotal();
}
function adjustPrice(d) {
var inp = document.getElementById('tradePrice');
inp.value = Math.max(0.01, (parseFloat(inp.value)||0) + d).toFixed(2);
updateTradeTotal();
}
function adjustQty(d) {
var inp = document.getElementById('tradeQty');
inp.value = Math.max(100, (parseInt(inp.value)||0) + d);
updateTradeTotal();
}
function setMaxQty() {
var price = parseFloat(document.getElementById('tradePrice').value) || currentStock.price;
if (tradeType==='buy') {
document.getElementById('tradeQty').value = Math.floor(balance/price/100)*100;
} else {
var pos = positions[currentStock.code];
document.getElementById('tradeQty').value = pos ? pos.qty : 0;
}
updateTradeTotal();
}
function updateTradeTotal() {
var price = parseFloat(document.getElementById('tradePrice').value) || 0;
var qty = parseInt(document.getElementById('tradeQty').value) || 0;
document.getElementById('tradeTotal').textContent = '¥' + (price*qty).toLocaleString();
}
async function confirmTrade() {
var price = parseFloat(document.getElementById('tradePrice').value);
var qty = parseInt(document.getElementById('tradeQty').value);
if (!price || price <= 0) { toast('请输入有效价格'); return; }
if (!qty || qty < 100 || qty % 100 !== 0) { toast('数量需为100整数倍'); return; }
if (!user || !user.userId) { toast('请先登录'); return; }
toast('交易处理中...');
if (tradeType==='buy') {
var total = price * qty;
if (total > balance) { toast('资金不足'); return; }
var res = await api('/trade.jsp', {
action: 'buy',
userId: user.userId,
stockCode: currentStock.code,
stockName: currentStock.name,
quantity: qty,
price: price
});
if (res.code === 0) {
balance = res.data.newBalance;
// 更新本地持仓
if (positions[currentStock.code]) {
var p = positions[currentStock.code];
p.cost = (p.cost*p.qty + price*qty) / (p.qty+qty);
p.qty += qty;
} else {
positions[currentStock.code] = { code: currentStock.code, name: currentStock.name, qty: qty, cost: price, currentPrice: price };
}
toast('买入成功');
closeTrade();
updatePortfolio();
} else {
toast(res.msg || '买入失败');
}
} else {
var pos = positions[currentStock.code];
if (!pos || pos.qty < qty) { toast('持仓不足'); return; }
var res = await api('/trade.jsp', {
action: 'sell',
userId: user.userId,
stockCode: currentStock.code,
quantity: qty,
price: price
});
if (res.code === 0) {
balance = res.data.newBalance;
pos.qty -= qty;
if (pos.qty === 0) delete positions[currentStock.code];
toast('卖出成功');
closeTrade();
updatePortfolio();
} else {
toast(res.msg || '卖出失败');
}
}
}
function updatePortfolio() {
var posValue = 0, profit = 0;
var codes = Object.keys(positions);
if (codes.length) {
loadSina(codes.join(','), function() {
codes.forEach(function(code) {
var d = window['hq_str_'+code];
if (d) {
var price = parseFloat(d.split(',')[3]);
positions[code].currentPrice = price;
posValue += price * positions[code].qty;
profit += (price - positions[code].cost) * positions[code].qty;
}
});
updatePortfolioUI(posValue, profit);
});
} else {
updatePortfolioUI(0, 0);
}
}
function updatePortfolioUI(posValue, profit) {
var total = balance + posValue;
document.getElementById('headerAssets').textContent = '¥' + Math.floor(total).toLocaleString();
document.getElementById('totalAssets').textContent = '¥' + total.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2});
document.getElementById('availableCash').textContent = '¥' + balance.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2});
document.getElementById('positionValue').textContent = '¥' + posValue.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2});
var profitEl = document.getElementById('totalProfit');
profitEl.textContent = (profit >= 0 ? '+' : '') + '¥' + profit.toFixed(2);
profitEl.className = 'value ' + (profit >= 0 ? 'up' : 'down');
var list = document.getElementById('positionList');
var codes = Object.keys(positions);
if (!codes.length) {
list.innerHTML = '
暂无持仓
';
return;
}
list.innerHTML = '';
codes.forEach(function(code) {
var p = positions[code];
var pft = (p.currentPrice - p.cost) * p.qty;
var pftPct = ((p.currentPrice - p.cost) / p.cost * 100).toFixed(2);
var up = pft >= 0;
var card = document.createElement('div');
card.className = 'position-card';
card.innerHTML = '