mirror of
https://github.com/ArvinLovegood/go-stock.git
synced 2025-07-19 00:00:09 +08:00
943 lines
30 KiB
Vue
943 lines
30 KiB
Vue
<script setup>
|
||
import {computed, h, onBeforeMount, onBeforeUnmount, onMounted, reactive, ref} from 'vue'
|
||
import {
|
||
Follow,
|
||
GetAIResponseResult,
|
||
GetConfig,
|
||
GetFollowList,
|
||
GetStockList,
|
||
GetVersionInfo,
|
||
Greet,
|
||
NewChatStream,
|
||
SaveAIResponseResult,
|
||
SendDingDingMessageByType,
|
||
SetAlarmChangePercent,
|
||
SetCostPriceAndVolume,
|
||
SetStockSort,
|
||
UnFollow
|
||
} from '../../wailsjs/go/main/App'
|
||
import {
|
||
NAvatar,
|
||
NButton,
|
||
NFlex,
|
||
NForm,
|
||
NFormItem,
|
||
NInputNumber,
|
||
NText,
|
||
useDialog,
|
||
useMessage,
|
||
useModal,
|
||
useNotification
|
||
} from 'naive-ui'
|
||
import {EventsEmit, EventsOn, WindowFullscreen, WindowReload, WindowUnfullscreen} from '../../wailsjs/runtime'
|
||
import {
|
||
Add,
|
||
ChatboxOutline,
|
||
} from '@vicons/ionicons5'
|
||
import {MdPreview,MdEditor } from 'md-editor-v3';
|
||
// preview.css相比style.css少了编辑器那部分样式
|
||
//import 'md-editor-v3/lib/preview.css';
|
||
import 'md-editor-v3/lib/style.css';
|
||
|
||
import { ExportPDF } from '@vavt/v3-extension';
|
||
import '@vavt/v3-extension/lib/asset/ExportPDF.css';
|
||
import html2canvas from "html2canvas";
|
||
import {asBlob} from 'html-docx-js-typescript';
|
||
|
||
import vueDanmaku from 'vue3-danmaku'
|
||
const danmus = ref([])
|
||
const ws = ref(null)
|
||
|
||
const toolbars = [0];
|
||
const handleProgress = (progress) => {
|
||
console.log(`Export progress: ${progress.ratio * 100}%`);
|
||
};
|
||
const enableEditor= ref(false)
|
||
|
||
const mdPreviewRef = ref(null)
|
||
const mdEditorRef = ref(null)
|
||
const tipsRef = ref(null)
|
||
const message = useMessage()
|
||
const modal = useModal()
|
||
const notify = useNotification()
|
||
const dialog = useDialog()
|
||
const stocks=ref([])
|
||
const results=ref({})
|
||
const ticker=ref({})
|
||
const stockList=ref([])
|
||
const followList=ref([])
|
||
const options=ref([])
|
||
const modalShow = ref(false)
|
||
const modalShow2 = ref(false)
|
||
const modalShow3 = ref(false)
|
||
const modalShow4 = ref(false)
|
||
const addBTN = ref(true)
|
||
const formModel = ref({
|
||
name: "",
|
||
code: "",
|
||
costPrice: 0.000,
|
||
volume: 0,
|
||
alarm: 0,
|
||
alarmPrice:0,
|
||
sort:999,
|
||
})
|
||
|
||
const data = reactive({
|
||
modelName:"",
|
||
chatId: "",
|
||
question:"",
|
||
name: "",
|
||
code: "",
|
||
fenshiURL:"",
|
||
kURL:"",
|
||
resultText: "Please enter your name below 👇",
|
||
fullscreen: false,
|
||
airesult: "",
|
||
openAiEnable: false,
|
||
loading: true,
|
||
enableDanmu: false,
|
||
})
|
||
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
|
||
|
||
const sortedResults = computed(() => {
|
||
//console.log("computed",sortedResults.value)
|
||
const sortedKeys =Object.keys(results.value).sort();
|
||
const sortedObject = {};
|
||
sortedKeys.forEach(key => {
|
||
sortedObject[key] = results.value[key];
|
||
});
|
||
return sortedObject
|
||
});
|
||
|
||
onBeforeMount(()=>{
|
||
GetStockList("").then(result => {
|
||
stockList.value = result
|
||
options.value=result.map(item => {
|
||
return {
|
||
label: item.name+" - "+item.ts_code,
|
||
value: item.ts_code
|
||
}
|
||
})
|
||
})
|
||
GetFollowList().then(result => {
|
||
followList.value = result
|
||
for (const followedStock of result) {
|
||
if(followedStock.StockCode.startsWith("us")){
|
||
followedStock.StockCode="gb_"+ followedStock.StockCode.replace("us", "").toLowerCase()
|
||
}
|
||
if (!stocks.value.includes(followedStock.StockCode)) {
|
||
console.log("followList",followedStock.StockCode)
|
||
stocks.value.push(followedStock.StockCode)
|
||
}
|
||
}
|
||
monitor()
|
||
message.destroyAll()
|
||
})
|
||
GetConfig().then(result => {
|
||
if (result.openAiEnable) {
|
||
data.openAiEnable = true
|
||
}
|
||
if (result.enableDanmu) {
|
||
data.enableDanmu = true
|
||
}
|
||
})
|
||
})
|
||
|
||
onMounted(() => {
|
||
message.loading("Loading...")
|
||
// console.log(`the component is now mounted.`)
|
||
|
||
ticker.value=setInterval(() => {
|
||
if(isTradingTime()){
|
||
//monitor()
|
||
data.fenshiURL='http://image.sinajs.cn/newchart/min/n/'+data.code+'.gif'+"?t="+Date.now()
|
||
}
|
||
}, 3500)
|
||
|
||
GetVersionInfo().then((res) => {
|
||
icon.value = res.icon;
|
||
});
|
||
// 创建 WebSocket 连接
|
||
ws.value = new WebSocket('ws://8.134.249.145:16688/ws'); // 替换为你的 WebSocket 服务器地址
|
||
//ws.value = new WebSocket('ws://localhost:16688/ws'); // 替换为你的 WebSocket 服务器地址
|
||
|
||
ws.value.onopen = () => {
|
||
console.log('WebSocket 连接已打开');
|
||
};
|
||
|
||
ws.value.onmessage = (event) => {
|
||
if(data.enableDanmu){
|
||
danmus.value.push(event.data);
|
||
}
|
||
};
|
||
|
||
ws.value.onerror = (error) => {
|
||
console.error('WebSocket 错误:', error);
|
||
};
|
||
|
||
ws.value.onclose = () => {
|
||
console.log('WebSocket 连接已关闭');
|
||
};
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
// console.log(`the component is now unmounted.`)
|
||
clearInterval(ticker.value)
|
||
ws.value.close()
|
||
|
||
})
|
||
|
||
EventsOn("refresh",(data)=>{
|
||
message.success(data)
|
||
})
|
||
|
||
EventsOn("showSearch",(data)=>{
|
||
addBTN.value = data === 1;
|
||
})
|
||
|
||
EventsOn("stock_price",(data)=>{
|
||
updateData(data)
|
||
})
|
||
|
||
EventsOn("refreshFollowList",(data)=>{
|
||
|
||
WindowReload()
|
||
// message.loading("refresh...")
|
||
// GetFollowList().then(result => {
|
||
// followList.value = result
|
||
// for (const followedStock of result) {
|
||
// if (!stocks.value.includes(followedStock.StockCode)) {
|
||
// stocks.value.push(followedStock.StockCode)
|
||
// }
|
||
// }
|
||
// monitor()
|
||
// message.destroyAll
|
||
// })
|
||
})
|
||
|
||
EventsOn("newChatStream",async (msg) => {
|
||
//console.log("newChatStream:->",data.airesult)
|
||
data.loading = false
|
||
//console.log(msg)
|
||
if (msg === "DONE") {
|
||
SaveAIResponseResult(data.code, data.name, data.airesult, data.chatId,data.question)
|
||
message.info("AI分析完成!")
|
||
message.destroyAll()
|
||
} else {
|
||
if(msg.chatId){
|
||
data.chatId = msg.chatId
|
||
}
|
||
if(msg.question){
|
||
data.question = msg.question
|
||
}
|
||
if(msg.content){
|
||
data.airesult = data.airesult + msg.content
|
||
}
|
||
if(msg.extraContent){
|
||
data.airesult = data.airesult + msg.extraContent
|
||
}
|
||
|
||
}
|
||
})
|
||
|
||
|
||
EventsOn("updateVersion",async (msg) => {
|
||
const githubTimeStr = msg.published_at;
|
||
// 创建一个 Date 对象
|
||
const utcDate = new Date(githubTimeStr);
|
||
// 获取本地时间
|
||
const date = new Date(utcDate.getTime());
|
||
const year = date.getFullYear();
|
||
// getMonth 返回值是 0 - 11,所以要加 1
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||
|
||
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||
|
||
console.log("GitHub UTC 时间:", utcDate);
|
||
console.log("转换后的本地时间:", formattedDate);
|
||
notify.info({
|
||
avatar: () =>
|
||
h(NAvatar, {
|
||
size: 'small',
|
||
round: false,
|
||
src: icon.value
|
||
}),
|
||
title: '发现新版本: ' + msg.tag_name,
|
||
content: () => {
|
||
//return h(MdPreview, {theme:'dark',modelValue:msg.commit?.message}, null)
|
||
return h('div', {
|
||
style: {
|
||
'text-align': 'left',
|
||
'font-size': '14px',
|
||
}
|
||
}, { default: () => msg.commit?.message })
|
||
},
|
||
duration: 5000,
|
||
meta: "发布时间:"+formattedDate,
|
||
action: () => {
|
||
return h(NButton, {
|
||
type: 'primary',
|
||
size: 'small',
|
||
onClick: () => {
|
||
window.open(msg.html_url)
|
||
}
|
||
}, { default: () => '查看' })
|
||
}
|
||
})
|
||
})
|
||
|
||
EventsOn("warnMsg",async (msg) => {
|
||
notify.error({
|
||
avatar: () =>
|
||
h(NAvatar, {
|
||
size: 'small',
|
||
round: false,
|
||
src: icon.value
|
||
}),
|
||
title: '警告',
|
||
duration: 5000,
|
||
content: () => {
|
||
return h('div', {
|
||
style: {
|
||
'text-align': 'left',
|
||
'font-size': '14px',
|
||
}
|
||
}, { default: () => msg })
|
||
},
|
||
})
|
||
})
|
||
|
||
//判断是否是A股交易时间
|
||
function isTradingTime() {
|
||
const now = new Date();
|
||
const day = now.getDay(); // 获取星期几,0表示周日,1-6表示周一至周六
|
||
if (day >= 1 && day <= 5) { // 周一至周五
|
||
const hours = now.getHours();
|
||
const minutes = now.getMinutes();
|
||
const totalMinutes = hours * 60 + minutes;
|
||
const startMorning = 9 * 60 + 15; // 上午9点15分换算成分钟数
|
||
const endMorning = 11 * 60 + 30; // 上午11点30分换算成分钟数
|
||
const startAfternoon = 13 * 60; // 下午13点换算成分钟数
|
||
const endAfternoon = 15 * 60; // 下午15点换算成分钟数
|
||
if ((totalMinutes >= startMorning && totalMinutes < endMorning) ||
|
||
(totalMinutes >= startAfternoon && totalMinutes < endAfternoon)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function AddStock(){
|
||
if (!data?.code) {
|
||
message.error("请输入有效股票代码");
|
||
return;
|
||
}
|
||
if (!stocks.value.includes(data.code)) {
|
||
Follow(data.code).then(result => {
|
||
if(result==="关注成功"){
|
||
stocks.value.push(data.code)
|
||
message.success(result)
|
||
monitor();
|
||
}else{
|
||
message.error(result)
|
||
}
|
||
})
|
||
}else{
|
||
message.error("已经关注了")
|
||
}
|
||
}
|
||
|
||
|
||
|
||
function removeMonitor(code,name,key) {
|
||
console.log("removeMonitor",name,code,key)
|
||
stocks.value.splice(stocks.value.indexOf(code),1)
|
||
console.log("removeMonitor-key",key)
|
||
console.log("removeMonitor-v",results.value[key])
|
||
|
||
delete results.value[key]
|
||
console.log("removeMonitor-v",results.value[key])
|
||
|
||
UnFollow(code).then(result => {
|
||
message.success(result)
|
||
})
|
||
}
|
||
|
||
function SendDanmu(){
|
||
//danmus.value.push(data.name)
|
||
console.log("SendDanmu",data.name)
|
||
console.log("SendDanmu-readyState", ws.value.readyState)
|
||
ws.value.send(data.name)
|
||
}
|
||
|
||
function getStockList(value){
|
||
|
||
|
||
|
||
// console.log("getStockList",value)
|
||
let result;
|
||
result=stockList.value.filter(item => item.name.includes(value)||item.ts_code.includes(value))
|
||
options.value=result.map(item => {
|
||
return {
|
||
label: item.name+" - "+item.ts_code,
|
||
value: item.ts_code
|
||
}
|
||
})
|
||
if(value&&value.indexOf("-")<=0){
|
||
data.code=value
|
||
}
|
||
}
|
||
|
||
async function updateData(result) {
|
||
console.log("stock_price",result['日期'],result['时间'],result['股票代码'],result['股票名称'],result['当前价格'],result['盘前盘后'])
|
||
|
||
if(result["当前价格"]<=0){
|
||
result["当前价格"]=result["卖一报价"]
|
||
}
|
||
|
||
if (result.changePercent>0) {
|
||
result.type="error"
|
||
result.color="#E88080"
|
||
}else if (result.changePercent<0) {
|
||
result.type="success"
|
||
result.color="#63E2B7"
|
||
}else {
|
||
result.type="default"
|
||
result.color="#FFFFFF"
|
||
}
|
||
|
||
if(result.profitAmount>0){
|
||
result.profitType="error"
|
||
}else if(result.profitAmount<0){
|
||
result.profitType="success"
|
||
}
|
||
if(result["当前价格"]){
|
||
if(result.alarmChangePercent>0&&Math.abs(result.changePercent)>=result.alarmChangePercent){
|
||
SendMessage(result,1)
|
||
}
|
||
|
||
if(result.alarmPrice>0&&result["当前价格"]>=result.alarmPrice){
|
||
SendMessage(result,2)
|
||
}
|
||
|
||
if(result.costPrice>0&&result["当前价格"]>=result.costPrice){
|
||
SendMessage(result,3)
|
||
}
|
||
}
|
||
|
||
result.key=GetSortKey(result.sort,result["股票代码"])
|
||
results.value[GetSortKey(result.sort,result["股票代码"])]=result
|
||
}
|
||
|
||
|
||
async function monitor() {
|
||
for (let code of stocks.value) {
|
||
// console.log(code)
|
||
Greet(code).then(result => {
|
||
updateData(result)
|
||
})
|
||
}
|
||
}
|
||
|
||
//数字长度不够前面补0
|
||
function padZero(num, length) {
|
||
return (Array(length).join('0') + num).slice(-length);
|
||
}
|
||
|
||
function GetSortKey(sort,code){
|
||
return padZero(sort,6)+"_"+code
|
||
}
|
||
|
||
function onSelect(item) {
|
||
//console.log("onSelect",item)
|
||
|
||
if(item.indexOf("-")>0){
|
||
item=item.split("-")[1].toLowerCase()
|
||
}
|
||
if(item.indexOf(".")>0){
|
||
data.code=item.split(".")[1].toLowerCase()+item.split(".")[0]
|
||
}
|
||
|
||
}
|
||
|
||
function search(code,name){
|
||
setTimeout(() => {
|
||
//window.open("https://xueqiu.com/S/"+code)
|
||
//window.open("https://www.cls.cn/stock?code="+code)
|
||
//window.open("https://quote.eastmoney.com/"+code+".html")
|
||
//window.open("https://finance.sina.com.cn/realstock/company/"+code+"/nc.shtml")
|
||
window.open("https://www.iwencai.com/unifiedwap/result?w="+name)
|
||
//window.open("https://www.iwencai.com/chat/?question="+code)
|
||
}, 500)
|
||
}
|
||
function setStock(code,name){
|
||
let res=followList.value.filter(item => item.StockCode===code)
|
||
//console.log("res:",res)
|
||
formModel.value.name=name
|
||
formModel.value.code=code
|
||
formModel.value.volume=res[0].Volume?res[0].Volume:0
|
||
formModel.value.costPrice=res[0].CostPrice
|
||
formModel.value.alarm=res[0].AlarmChangePercent
|
||
formModel.value.alarmPrice=res[0].AlarmPrice
|
||
formModel.value.sort=res[0].Sort
|
||
modalShow.value=true
|
||
}
|
||
|
||
function showFenshi(code,name){
|
||
data.code=code
|
||
data.name=name
|
||
data.fenshiURL='http://image.sinajs.cn/newchart/min/n/'+data.code+'.gif'+"?t="+Date.now()
|
||
|
||
if(code.startsWith('hk')){
|
||
data.fenshiURL='http://image.sinajs.cn/newchart/hk_stock/min/'+data.code.replace("hk","")+'.gif'+"?t="+Date.now()
|
||
}
|
||
if(code.startsWith('gb_')){
|
||
data.fenshiURL='http://image.sinajs.cn/newchart/usstock/min/'+data.code.replace("gb_","")+'.gif'+"?t="+Date.now()
|
||
}
|
||
|
||
modalShow2.value=true
|
||
}
|
||
function showK(code,name){
|
||
data.code=code
|
||
data.name=name
|
||
data.kURL='http://image.sinajs.cn/newchart/daily/n/'+data.code+'.gif'+"?t="+Date.now()
|
||
if(code.startsWith('hk')){
|
||
data.kURL='http://image.sinajs.cn/newchart/hk_stock/daily/'+data.code.replace("hk","")+'.gif'+"?t="+Date.now()
|
||
}
|
||
if(code.startsWith('gb_')){
|
||
data.kURL='http://image.sinajs.cn/newchart/usstock/daily/'+data.code.replace("gb_","")+'.gif'+"?t="+Date.now()
|
||
}
|
||
//https://image.sinajs.cn/newchart/usstock/daily/dji.gif
|
||
//https://image.sinajs.cn/newchart/hk_stock/daily/06030.gif?1740729404273
|
||
modalShow3.value=true
|
||
}
|
||
|
||
|
||
|
||
|
||
function updateCostPriceAndVolumeNew(code,price,volume,alarm,formModel){
|
||
|
||
if(formModel.sort){
|
||
SetStockSort(formModel.sort,code).then(result => {
|
||
//message.success(result)
|
||
})
|
||
}
|
||
|
||
if(alarm||formModel.alarmPrice){
|
||
SetAlarmChangePercent(alarm,formModel.alarmPrice,code).then(result => {
|
||
//message.success(result)
|
||
})
|
||
}
|
||
SetCostPriceAndVolume(code,price,volume).then(result => {
|
||
modalShow.value=false
|
||
message.success(result)
|
||
GetFollowList().then(result => {
|
||
followList.value = result
|
||
for (const followedStock of result) {
|
||
if (!stocks.value.includes(followedStock.StockCode)) {
|
||
stocks.value.push(followedStock.StockCode)
|
||
}
|
||
}
|
||
monitor()
|
||
message.destroyAll()
|
||
})
|
||
})
|
||
}
|
||
|
||
function fullscreen(){
|
||
if(data.fullscreen){
|
||
WindowUnfullscreen()
|
||
}else{
|
||
WindowFullscreen()
|
||
}
|
||
data.fullscreen=!data.fullscreen
|
||
}
|
||
|
||
|
||
//type 报警类型: 1 涨跌报警;2 股价报警 3 成本价报警
|
||
function SendMessage(result,type){
|
||
let typeName=getTypeName(type)
|
||
let img='http://image.sinajs.cn/newchart/min/n/'+result["股票代码"]+'.gif'+"?t="+Date.now()
|
||
let markdown="### go-stock ["+typeName+"]\n\n"+
|
||
"### "+result["股票名称"]+"("+result["股票代码"]+")\n" +
|
||
"- 当前价格: "+result["当前价格"]+" "+result.changePercent+"%\n" +
|
||
"- 最高价: "+result["今日最高价"]+" "+result.highRate+"\n" +
|
||
"- 最低价: "+result["今日最低价"]+" "+result.lowRate+"\n" +
|
||
"- 昨收价: "+result["昨日收盘价"]+"\n" +
|
||
"- 今开价: "+result["今日开盘价"]+"\n" +
|
||
"- 成本价: "+result.costPrice+" "+result.profit+"% "+result.profitAmount+" ¥\n" +
|
||
"- 成本数量: "+result.costVolume+"股\n" +
|
||
"- 日期: "+result["日期"]+" "+result["时间"]+"\n\n"+
|
||
"\n"
|
||
let title=result["股票名称"]+"("+result["股票代码"]+") "+result["当前价格"]+" "+result.changePercent
|
||
|
||
let msg='{' +
|
||
' "msgtype": "markdown",' +
|
||
' "markdown": {' +
|
||
' "title":"['+typeName+"]"+title+'",' +
|
||
' "text": "'+markdown+'"' +
|
||
' },' +
|
||
' "at": {' +
|
||
' "isAtAll": true' +
|
||
' }' +
|
||
' }'
|
||
// SendDingDingMessage(msg,result["股票代码"])
|
||
SendDingDingMessageByType(msg,result["股票代码"],type)
|
||
}
|
||
function aiReCheckStock(stock,stockCode) {
|
||
data.modelName=""
|
||
data.airesult=""
|
||
data.time=""
|
||
data.name=stock
|
||
data.code=stockCode
|
||
data.loading=true
|
||
modalShow4.value=true
|
||
message.loading("ai检测中...",{
|
||
duration: 0,
|
||
})
|
||
//
|
||
|
||
NewChatStream(stock,stockCode,data.question)
|
||
}
|
||
function aiCheckStock(stock,stockCode){
|
||
GetAIResponseResult(stockCode).then(result => {
|
||
if(result.content){
|
||
data.modelName=result.modelName
|
||
data.chatId=result.chatId
|
||
data.question=result.question
|
||
data.name=stock
|
||
data.code=stockCode
|
||
data.loading=false
|
||
modalShow4.value=true
|
||
data.airesult=result.content
|
||
const date = new Date(result.CreatedAt);
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||
data.time=`${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||
}else{
|
||
data.modelName=""
|
||
data.question=""
|
||
data.airesult=""
|
||
data.time=""
|
||
data.name=stock
|
||
data.code=stockCode
|
||
data.loading=true
|
||
modalShow4.value=true
|
||
message.loading("ai检测中...",{
|
||
duration: 0,
|
||
})
|
||
NewChatStream(stock,stockCode)
|
||
}
|
||
})
|
||
}
|
||
|
||
function getTypeName(type){
|
||
switch (type)
|
||
{
|
||
case 1:
|
||
return "涨跌报警"
|
||
case 2:
|
||
return "股价报警"
|
||
case 3:
|
||
return "成本价报警"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|
||
|
||
//获取高度
|
||
function getHeight() {
|
||
return document.documentElement.clientHeight
|
||
}
|
||
|
||
window.onerror = function (msg, source, lineno, colno, error) {
|
||
// 将错误信息发送给后端
|
||
EventsEmit("frontendError", {
|
||
page: "stock.vue",
|
||
message: msg,
|
||
source: source,
|
||
lineno: lineno,
|
||
colno: colno,
|
||
error: error ? error.stack : null,
|
||
data:data,
|
||
results:results,
|
||
followList:followList,
|
||
stockList:stockList,
|
||
stocks:stocks,
|
||
formModel:formModel,
|
||
});
|
||
message.error("发生错误:"+msg)
|
||
return true;
|
||
};
|
||
|
||
function saveAsImage(name,code) {
|
||
const element = document.querySelector('.md-editor-preview');
|
||
if (element) {
|
||
html2canvas(element,{
|
||
useCORS: true, // 解决跨域图片问题
|
||
scale: 2, // 提高截图质量
|
||
allowTaint: true, // 允许跨域图片
|
||
}).then(canvas => {
|
||
const link = document.createElement('a');
|
||
link.href = canvas.toDataURL('image/png');
|
||
link.download = name+"["+code+']-ai-analysis-result.png';
|
||
link.click();
|
||
});
|
||
} else {
|
||
message.error('无法找到分析结果元素');
|
||
}
|
||
}
|
||
|
||
async function copyToClipboard() {
|
||
try {
|
||
await navigator.clipboard.writeText(data.airesult);
|
||
message.success('分析结果已复制到剪切板');
|
||
} catch (err) {
|
||
message.error('复制失败: ' + err);
|
||
}
|
||
}
|
||
function saveAsMarkdown() {
|
||
const blob = new Blob([data.airesult], { type: 'text/markdown;charset=utf-8' });
|
||
const link = document.createElement('a');
|
||
link.href = URL.createObjectURL(blob);
|
||
link.download = `${data.name}[${data.code}]-ai-analysis-result.md`;
|
||
link.click();
|
||
URL.revokeObjectURL(link.href);
|
||
link.remove()
|
||
}
|
||
function getHtml(ref) {
|
||
if (ref.value) {
|
||
// 获取 MdPreview 组件的根元素
|
||
const rootElement = ref.value.$el;
|
||
// 获取 HTML 内容
|
||
return rootElement.innerHTML;
|
||
} else {
|
||
console.error('mdPreviewRef is not yet available');
|
||
return "";
|
||
}
|
||
}
|
||
|
||
// 导出文档
|
||
async function saveAsWord() {
|
||
// 将富文本内容拼接为一个完整的html
|
||
const html = getHtml(mdPreviewRef)
|
||
const tipsHtml = getHtml(tipsRef)
|
||
const value = `
|
||
${html}
|
||
<hr>
|
||
<div style="font-size: 12px;color: red">
|
||
${tipsHtml}
|
||
</div>
|
||
<br>
|
||
本报告由go-stock项目生成:
|
||
<p>
|
||
<a href="https://github.com/ArvinLovegood/go-stock">
|
||
AI赋能股票分析:自选股行情获取,成本盈亏展示,涨跌报警推送,市场整体/个股情绪分析,K线技术指标分析等。数据全部保留在本地。支持DeepSeek,OpenAI, Ollama,LMStudio,AnythingLLM,硅基流动,火山方舟,阿里云百炼等平台或模型。
|
||
</a></p>
|
||
`
|
||
// landscape就是横着的,portrait是竖着的,默认是竖屏portrait。
|
||
const blob = await asBlob(value, { orientation: 'portrait' })
|
||
const a = document.createElement('a')
|
||
a.href = URL.createObjectURL(blob)
|
||
a.download = `${data.name}[${data.code}]-ai-analysis-result.docx`;
|
||
a.click()
|
||
// 下载后将标签移除
|
||
URL.revokeObjectURL(a.href);
|
||
a.remove()
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<vue-danmaku v-model:danmus="danmus" style="height:100px; width:100%;z-index: 9;position:absolute; top: 400px; pointer-events: none;" ></vue-danmaku>
|
||
<n-grid :x-gap="8" :cols="3" :y-gap="8" >
|
||
<n-gi v-for="result in sortedResults" style="margin-left: 2px" onmouseover="this.style.border='1px solid #3498db' " onmouseout="this.style.border='0px'">
|
||
<n-card :data-code="result['股票代码']" :bordered="false" :title="result['股票名称']" :closable="false" @close="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
|
||
<n-grid :cols="1" :y-gap="6">
|
||
<n-gi>
|
||
<n-text :type="result.type" >
|
||
<n-number-animation :duration="1000" :precision="2" :from="result['上次当前价格']" :to="Number(result['当前价格'])" />
|
||
<n-tag size="small" :type="result.type" :bordered="false" v-if="result['盘前盘后']>0">({{result['盘前盘后']}} {{result['盘前盘后涨跌幅']}}%)</n-tag>
|
||
</n-text>
|
||
<n-text style="padding-left: 10px;" :type="result.type">
|
||
<n-number-animation :duration="1000" :precision="3" :from="0" :to="result.changePercent" />%
|
||
</n-text>
|
||
<n-text size="small" v-if="result.costVolume>0" :type="result.type">
|
||
<n-number-animation :duration="1000" :precision="2" :from="0" :to="result.profitAmountToday" />
|
||
</n-text>
|
||
</n-gi>
|
||
</n-grid>
|
||
<n-grid :cols="2" :y-gap="4" :x-gap="4" >
|
||
<n-gi>
|
||
<n-text :type="'info'">{{"最高 "+result["今日最高价"]+" "+result.highRate }}%</n-text>
|
||
</n-gi>
|
||
<n-gi>
|
||
<n-text :type="'info'">{{"最低 "+result["今日最低价"]+" "+result.lowRate }}%</n-text>
|
||
</n-gi>
|
||
<n-gi>
|
||
<n-text :type="'info'">{{"昨收 "+result["昨日收盘价"]}}</n-text>
|
||
</n-gi>
|
||
<n-gi>
|
||
<n-text :type="'info'">{{"今开 "+result["今日开盘价"]}}</n-text>
|
||
</n-gi>
|
||
</n-grid>
|
||
<template #header-extra>
|
||
|
||
<n-tag size="small" :bordered="false">{{result['股票代码']}}</n-tag>
|
||
<n-button size="tiny" secondary type="primary" @click="removeMonitor(result['股票代码'],result['股票名称'],result.key)">
|
||
取消关注
|
||
</n-button>
|
||
<n-button size="tiny" v-if="data.openAiEnable" secondary type="warning" @click="aiCheckStock(result['股票名称'],result['股票代码'])">
|
||
AI分析
|
||
</n-button>
|
||
|
||
</template>
|
||
<template #footer>
|
||
<n-flex justify="center">
|
||
<n-tag size="small" v-if="result.volume>0" :type="result.profitType">{{result.volume+"股"}}</n-tag>
|
||
<n-tag size="small" v-if="result.costPrice>0" :type="result.profitType">{{"成本:"+result.costPrice+"*"+result.costVolume+" "+result.profit+"%"+" ( "+result.profitAmount+" ¥ )"}}</n-tag>
|
||
</n-flex>
|
||
</template>
|
||
<template #action>
|
||
<n-flex justify="space-between">
|
||
<n-text :type="'info'">{{result["日期"]+" "+result["时间"]}}</n-text>
|
||
<n-button size="tiny" type="info" @click="setStock(result['股票代码'],result['股票名称'])"> 成本 </n-button>
|
||
<n-button size="tiny" type="success" @click="showFenshi(result['股票代码'],result['股票名称'])"> 分时 </n-button>
|
||
<n-button size="tiny" type="error" @click="showK(result['股票代码'],result['股票名称'])"> 日K </n-button>
|
||
<n-button size="tiny" type="warning" @click="search(result['股票代码'],result['股票名称'])"> 详情 </n-button>
|
||
</n-flex>
|
||
</template>
|
||
</n-card >
|
||
</n-gi>
|
||
</n-grid>
|
||
<div style="position: fixed;bottom: 18px;right:0;z-index: 10;width: 420px">
|
||
<!-- <n-card :bordered="false">-->
|
||
<n-input-group >
|
||
<!-- <n-button type="error" @click="addBTN=!addBTN" > <n-icon :component="Search"/> <n-text v-if="addBTN">隐藏</n-text></n-button>-->
|
||
|
||
<n-auto-complete v-model:value="data.name" v-if="addBTN"
|
||
:input-props="{
|
||
autocomplete: 'disabled',
|
||
}"
|
||
:options="options"
|
||
placeholder="股票指数名称/代码/弹幕"
|
||
clearable @update-value="getStockList" :on-select="onSelect"/>
|
||
<n-button type="primary" @click="AddStock" v-if="addBTN">
|
||
<n-icon :component="Add"/> 关注该股票
|
||
</n-button>
|
||
<n-button type="error" @click="SendDanmu" v-if="data.enableDanmu">
|
||
<n-icon :component="ChatboxOutline"/> 发送弹幕
|
||
</n-button>
|
||
</n-input-group>
|
||
<!-- </n-card>-->
|
||
</div>
|
||
<n-modal transform-origin="center" size="small" v-model:show="modalShow" :title="formModel.name" style="width: 400px" :preset="'card'">
|
||
<n-form :model="formModel" :rules="{
|
||
costPrice: { required: true, message: '请输入成本'},
|
||
volume: { required: true, message: '请输入数量'},
|
||
alarm:{required: true, message: '涨跌报警值'} ,
|
||
alarmPrice: { required: true, message: '请输入报警价格'},
|
||
sort: { required: true, message: '请输入排序值'},
|
||
}" label-placement="left" label-width="80px">
|
||
<n-form-item label="股票成本" path="costPrice">
|
||
<n-input-number v-model:value="formModel.costPrice" min="0" placeholder="请输入股票成本" >
|
||
<template #suffix>
|
||
{{formModel.code.indexOf("hk")>=0?"HK$":"¥"}}
|
||
</template>
|
||
</n-input-number>
|
||
</n-form-item>
|
||
<n-form-item label="股票数量" path="volume">
|
||
<n-input-number v-model:value="formModel.volume" min="0" step="100" placeholder="请输入股票数量" >
|
||
<template #suffix>
|
||
股
|
||
</template>
|
||
</n-input-number>
|
||
</n-form-item>
|
||
<n-form-item label="涨跌提醒" path="alarm">
|
||
<n-input-number v-model:value="formModel.alarm" min="0" placeholder="请输入涨跌报警值(%)" >
|
||
<template #suffix>
|
||
%
|
||
</template>
|
||
</n-input-number>
|
||
</n-form-item>
|
||
<n-form-item label="股价提醒" path="alarmPrice">
|
||
<n-input-number v-model:value="formModel.alarmPrice" min="0" placeholder="请输入股价报警值(¥)" >
|
||
<template #suffix>
|
||
{{formModel.code.indexOf("hk")>=0?"HK$":"¥"}}
|
||
</template>
|
||
</n-input-number>
|
||
</n-form-item>
|
||
<n-form-item label="股票排序" path="sort">
|
||
<n-input-number v-model:value="formModel.sort" min="0" placeholder="请输入股价排序值" >
|
||
</n-input-number>
|
||
</n-form-item>
|
||
</n-form>
|
||
<template #footer>
|
||
<n-button type="primary" @click="updateCostPriceAndVolumeNew(formModel.code,formModel.costPrice,formModel.volume,formModel.alarm,formModel)">保存</n-button>
|
||
</template>
|
||
</n-modal>
|
||
|
||
<n-modal v-model:show="modalShow2" :title="data.name" style="width: 600px" :preset="'card'">
|
||
<n-image :src="data.fenshiURL" />
|
||
</n-modal>
|
||
<n-modal v-model:show="modalShow3" :title="data.name" style="width: 600px" :preset="'card'">
|
||
<n-image :src="data.kURL" />
|
||
</n-modal>
|
||
|
||
<n-modal transform-origin="center" v-model:show="modalShow4" preset="card" style="width: 800px;" :title="'['+data.name+']AI分析结果'" >
|
||
<n-spin size="small" :show="data.loading">
|
||
<MdEditor v-if="enableEditor" :toolbars="toolbars" ref="mdEditorRef" style="height: 440px;text-align: left" :modelValue="data.airesult" :theme="'dark'">
|
||
<template #defToolbars>
|
||
<ExportPDF :file-name="data.name+'['+data.code+']AI分析报告'" style="text-align: left" :modelValue="data.airesult" @onProgress="handleProgress" />
|
||
</template>
|
||
</MdEditor >
|
||
<MdPreview v-if="!enableEditor" ref="mdPreviewRef" style="height: 440px;text-align: left" :modelValue="data.airesult" :theme="'dark'"/>
|
||
</n-spin>
|
||
<template #footer>
|
||
<n-flex justify="space-between" ref="tipsRef">
|
||
<n-text type="info" v-if="data.time" >
|
||
<n-tag v-if="data.modelName" type="warning" round :title="data.chatId" :bordered="false">{{data.modelName}}</n-tag>
|
||
{{data.time}}
|
||
</n-text>
|
||
<n-text type="error" >*AI分析结果仅供参考,请以实际行情为准。投资需谨慎,风险自担。</n-text>
|
||
</n-flex>
|
||
</template>
|
||
<template #action>
|
||
<n-flex justify="right">
|
||
<n-input v-model:value="data.question" style="text-align: left" clearable
|
||
type="textarea"
|
||
:show-count="true"
|
||
placeholder="请输入您的问题:例如{{stockName}}[{{stockCode}}]分析和总结"
|
||
:autosize="{
|
||
minRows: 2,
|
||
maxRows: 5
|
||
}"
|
||
/>
|
||
<!-- <n-button size="tiny" type="error" @click="enableEditor=!enableEditor">编辑/预览</n-button>-->
|
||
<n-button size="tiny" type="warning" @click="aiReCheckStock(data.name,data.code)">再次分析</n-button>
|
||
<n-button size="tiny" type="info" @click="saveAsImage(data.name,data.code)">保存为图片</n-button>
|
||
<n-button size="tiny" type="success" @click="copyToClipboard">复制到剪切板</n-button>
|
||
<n-button size="tiny" type="primary" @click="saveAsMarkdown">保存为Markdown文件</n-button>
|
||
<n-button size="tiny" type="primary" @click="saveAsWord">保存为Word文件</n-button>
|
||
</n-flex>
|
||
</template>
|
||
</n-modal>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.md-editor-preview h3{
|
||
text-align: center !important;
|
||
}
|
||
|
||
.md-editor-preview p{
|
||
text-align: left !important;
|
||
}
|
||
</style>
|