总资产 ¥0
U
自选股
总资产
¥0.00
可用资金
¥0.00
持仓市值
¥0.00
总盈亏
¥0.00
我的持仓
U
用户
***

--

--
--
-- --
今开
--
最高
--
最低
--
昨收
--
成交量
--
成交额
--

买入

-- --
可用资金 ¥0
预计金额 ¥0
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 = '
'+p.name+''+(up?'+':'')+'¥'+pft.toFixed(2)+' ('+(up?'+':'')+pftPct+'%)
持仓 '+p.qty+' 股成本 ¥'+p.cost.toFixed(2)+'现价 ¥'+p.currentPrice.toFixed(2)+'
'; card.onclick = function() { openDetail(code); }; list.appendChild(card); }); } function loadSina(codes, cb) { var s = document.createElement('script'); s.src = 'https://hq.sinajs.cn/list=' + codes + '&_=' + Date.now(); s.onload = function() { cb(); document.head.removeChild(s); }; s.onerror = function() { document.head.removeChild(s); }; document.head.appendChild(s); } function fmtVol(v) { return v >= 1e8 ? (v/1e8).toFixed(2)+'亿' : v >= 1e4 ? (v/1e4).toFixed(2)+'万' : v.toFixed(0); } function fmtAmt(a) { return a >= 1e8 ? (a/1e8).toFixed(2)+'亿' : a >= 1e4 ? (a/1e4).toFixed(2)+'万' : a.toFixed(0); } function toast(msg) { var t = document.getElementById('toast'); t.textContent = msg; t.classList.add('show'); setTimeout(function() { t.classList.remove('show'); }, 2000); }