feat(frontend):添加热门股票、事件和话题功能

- 在 App.d.ts 和 App.js 中添加了 HotEvent、HotStock 和 HotTopic 函数
- 在 app_common.go 中实现了相关功能的后端逻辑
- 新增 HotEvents、HotStockList 和 HotTopics 组件用于前端展示
- 更新 market.vue以包含新的热门股票和话题功能
- 在 KLineChart.vue 中添加了代码和名称的显示
This commit is contained in:
ArvinLovegood 2025-06-25 09:40:04 +08:00
parent 30a3d1d9ef
commit 7dd10d443e
11 changed files with 362 additions and 3 deletions

View File

@ -31,3 +31,20 @@ func (a App) EMDictCode(code string) []any {
func (a App) AnalyzeSentiment(text string) data.SentimentResult {
return data.AnalyzeSentiment(text)
}
func (a App) HotStock(marketType string) *[]models.HotItem {
return data.NewMarketNewsApi().XUEQIUHotStock(50, marketType)
}
func (a App) HotEvent(size int) *[]models.HotEvent {
if size <= 0 {
size = 10
}
return data.NewMarketNewsApi().HotEvent(size)
}
func (a App) HotTopic(size int) []any {
if size <= 0 {
size = 10
}
return data.NewMarketNewsApi().HotTopic(size)
}

View File

@ -574,3 +574,72 @@ func (m MarketNewsApi) TradingViewNews() *[]models.TVNews {
json.Unmarshal(items, TVNews)
return TVNews
}
func (m MarketNewsApi) XUEQIUHotStock(size int, marketType string) *[]models.HotItem {
url := fmt.Sprintf("https://stock.xueqiu.com/v5/stock/hot_stock/list.json?page=1&size=%d&_type=%s&type=%s", size, marketType, marketType)
res := &models.XUEQIUHot{}
_, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "stock.xueqiu.com").
SetHeader("Origin", "https://xueqiu.com").
SetHeader("Referer", "https://xueqiu.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Cookie", "cookiesu=2617378771242871; s=c2121pp1u71; device_id=237a58584ec58d8e4d4e1040700a644f1; Hm_lvt_1db88642e346389874251b5a1eded6e3=1744100219,1744599115; xq_a_token=b7259d09435458cc3f1a963479abb270a1a016ce; xqat=b7259d09435458cc3f1a963479abb270a1a016ce; xq_r_token=28108bfa1d92ac8a46bbb57722633746218621a3; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOi0xLCJpc3MiOiJ1YyIsImV4cCI6MTc1MjU0MTk4OCwiY3RtIjoxNzUwMjMwNjA2NzI0LCJjaWQiOiJkOWQwbjRBWnVwIn0.kU_fz0luJoE7nr-K4UrNUi5-mAG-vMdXtuC4mUKIppILId4UpF70LB70yunxGiNSw6tPFR3-hyLvztKAHtekCUTm3XjUl5b3tEDP-ZUVqHnWXO_5hoeMI8h-Cfx6ZGlIr5x3icvTPkT0OV5CD5A33-ZDTKhKPf-DhJ_-m7CG5GbX4MseOBeMXuLUQUiYHPKhX1QUc0GTGrCzi8Mki0z49D0LVqCSgbsx3UGfowOOyx85_cXb4OAFvIjwbs2p0o_h-ibIT0ngVkkAyEDetVvlcZ_bkardhseCB7k9BEMgH2z8ihgkVxyy3P0degLmDUruhmqn5uZOCi1pVBDvCv9lBg; u=261737877124287; ssxmod_itna=QuG=D5AKiKDIqCqGKi7G7DgmmPlSDWFqKGHDyx4YK0CDmxjKiddDUQivnb8xpnQcGyGYoYhoqEeDBubrDSxD67DK4GTm+ogiw1o3B=xedQHDgBtN=7/i1K53N+rOjquLMU=kbqYxB3DExGkqj0tPi4DxaPD5xDTDWeDGDD3DnnsDQKDRx0kL0oDIxD1D0bmHUEvh38mDYePLmOmDYPYx94Y8KoDeEgsD7HUl/vIGGEAqjLPFegXLD0HolCqr4DCid1qDm+ECfkjDn9sD0KP8fn+CRoDv=tYr4ibx+o=W+8vstf9mjGe3cXseWdBmoFrmf4DA3bFAxnAxD7vYxADaDoerDGHPoxHF+PKGPtDKmiqQGeB5qbi4eg4KDHKDe3DeG0qeEP9xVUoHDDWMYYM0ICr4FBimBDM7D0x4QOECmhul5QCN/m5/74lGm=7x9Wp7A+i7xQ7wlMD4D; ssxmod_itna2=QuG=D5AKiKDIqCqGKi7G7DgmmPlSDWFqKGHDyx4YK0CDmxjKiddDUQivnb8xpnQcGyGYoYhoqoDirSDhPmGD24GajjDuGE3m7or4DlxOSGewHl6iaus2Q62SRX5CFjCds6ltF9xy6iaUuB262UkhRA8UXST=4/b+y3kGKzlGE8T29FA008ljy9jXXC7f7m7QsK667mlUooWrofk=qGZjxtcUrN1NtuAnne1hj+rQP5UnlFkxf+o7VjmatH7u7bCDlbTt3cz6CH9Fl4vye16W/ellc8I3Q37W7ZwiLGD/zPpZcnd2nsqqo/+zRbKAmz4plzwaDqGUe7f9E+P0IFRKqpRv+buQFHBSpcbwND7Q+9XWmnjI2UwKd98jIS3gPXwxvbx4OuiyH8gZ+OEt7DgE/AY/9W4VxDZrlFWyWnC4y4/I0IpAfaGKpbPmauKbkqawqv93vSf+9HamGe0Dt2PNgT3yiEB4vQP2/DdVpcGBOjFujWoHP32OshLPYI20LRCKddwEGkKqPzPwKPc3X5zuB=w2fUdtwKsAW5kQtsl8clNwjC5uDYrxR0h9xaj0xmD+YuI3GPT7xYTalRImPj2wL2=+91a304xa4bTWtP=dLGARhb/efRi0uktaz8i8C04G0x/ZWUzqRza8GGU=FfRfvb4GZM/q2rVsl0nLvRjGeAKgocLouyXs/uwZu3YxbAx30qCbjG1A533zAxIeIgD=0VAc3ixD").
SetResult(res).
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("XUEQIUHotStock err:%s", err.Error())
return &[]models.HotItem{}
}
logger.SugaredLogger.Infof("XUEQIUHotStock:%+v", res)
return &res.Data.Items
}
func (m MarketNewsApi) HotEvent(size int) *[]models.HotEvent {
events := &[]models.HotEvent{}
url := fmt.Sprintf("https://xueqiu.com/hot_event/list.json?count=%d", size)
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "xueqiu.com").
SetHeader("Origin", "https://xueqiu.com").
SetHeader("Referer", "https://xueqiu.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetHeader("Cookie", "cookiesu=2617378771242871; s=c2121pp1u71; device_id=237a58584ec58d8e4d4e1040700a644f1; Hm_lvt_1db88642e346389874251b5a1eded6e3=1744100219,1744599115; xq_a_token=b7259d09435458cc3f1a963479abb270a1a016ce; xqat=b7259d09435458cc3f1a963479abb270a1a016ce; xq_r_token=28108bfa1d92ac8a46bbb57722633746218621a3; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOi0xLCJpc3MiOiJ1YyIsImV4cCI6MTc1MjU0MTk4OCwiY3RtIjoxNzUwMjMwNjA2NzI0LCJjaWQiOiJkOWQwbjRBWnVwIn0.kU_fz0luJoE7nr-K4UrNUi5-mAG-vMdXtuC4mUKIppILId4UpF70LB70yunxGiNSw6tPFR3-hyLvztKAHtekCUTm3XjUl5b3tEDP-ZUVqHnWXO_5hoeMI8h-Cfx6ZGlIr5x3icvTPkT0OV5CD5A33-ZDTKhKPf-DhJ_-m7CG5GbX4MseOBeMXuLUQUiYHPKhX1QUc0GTGrCzi8Mki0z49D0LVqCSgbsx3UGfowOOyx85_cXb4OAFvIjwbs2p0o_h-ibIT0ngVkkAyEDetVvlcZ_bkardhseCB7k9BEMgH2z8ihgkVxyy3P0degLmDUruhmqn5uZOCi1pVBDvCv9lBg; u=261737877124287; ssxmod_itna=QuG=D5AKiKDIqCqGKi7G7DgmmPlSDWFqKGHDyx4YK0CDmxjKiddDUQivnb8xpnQcGyGYoYhoqEeDBubrDSxD67DK4GTm+ogiw1o3B=xedQHDgBtN=7/i1K53N+rOjquLMU=kbqYxB3DExGkqj0tPi4DxaPD5xDTDWeDGDD3DnnsDQKDRx0kL0oDIxD1D0bmHUEvh38mDYePLmOmDYPYx94Y8KoDeEgsD7HUl/vIGGEAqjLPFegXLD0HolCqr4DCid1qDm+ECfkjDn9sD0KP8fn+CRoDv=tYr4ibx+o=W+8vstf9mjGe3cXseWdBmoFrmf4DA3bFAxnAxD7vYxADaDoerDGHPoxHF+PKGPtDKmiqQGeB5qbi4eg4KDHKDe3DeG0qeEP9xVUoHDDWMYYM0ICr4FBimBDM7D0x4QOECmhul5QCN/m5/74lGm=7x9Wp7A+i7xQ7wlMD4D; ssxmod_itna2=QuG=D5AKiKDIqCqGKi7G7DgmmPlSDWFqKGHDyx4YK0CDmxjKiddDUQivnb8xpnQcGyGYoYhoqoDirSDhPmGD24GajjDuGE3m7or4DlxOSGewHl6iaus2Q62SRX5CFjCds6ltF9xy6iaUuB262UkhRA8UXST=4/b+y3kGKzlGE8T29FA008ljy9jXXC7f7m7QsK667mlUooWrofk=qGZjxtcUrN1NtuAnne1hj+rQP5UnlFkxf+o7VjmatH7u7bCDlbTt3cz6CH9Fl4vye16W/ellc8I3Q37W7ZwiLGD/zPpZcnd2nsqqo/+zRbKAmz4plzwaDqGUe7f9E+P0IFRKqpRv+buQFHBSpcbwND7Q+9XWmnjI2UwKd98jIS3gPXwxvbx4OuiyH8gZ+OEt7DgE/AY/9W4VxDZrlFWyWnC4y4/I0IpAfaGKpbPmauKbkqawqv93vSf+9HamGe0Dt2PNgT3yiEB4vQP2/DdVpcGBOjFujWoHP32OshLPYI20LRCKddwEGkKqPzPwKPc3X5zuB=w2fUdtwKsAW5kQtsl8clNwjC5uDYrxR0h9xaj0xmD+YuI3GPT7xYTalRImPj2wL2=+91a304xa4bTWtP=dLGARhb/efRi0uktaz8i8C04G0x/ZWUzqRza8GGU=FfRfvb4GZM/q2rVsl0nLvRjGeAKgocLouyXs/uwZu3YxbAx30qCbjG1A533zAxIeIgD=0VAc3ixD").
Get(url)
if err != nil {
logger.SugaredLogger.Errorf("HotEvent err:%s", err.Error())
return events
}
logger.SugaredLogger.Infof("HotEvent:%s", resp.Body())
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
items, err := json.Marshal(respMap["list"])
if err != nil {
return events
}
json.Unmarshal(items, events)
return events
}
func (m MarketNewsApi) HotTopic(size int) []any {
url := "https://gubatopic.eastmoney.com/interface/GetData.aspx?path=newtopic/api/Topic/HomePageListRead"
resp, err := resty.New().SetTimeout(time.Duration(30)*time.Second).R().
SetHeader("Host", "gubatopic.eastmoney.com").
SetHeader("Origin", "https://gubatopic.eastmoney.com").
SetHeader("Referer", "https://gubatopic.eastmoney.com/").
SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0").
SetFormData(map[string]string{
"param": fmt.Sprintf("ps=%d&p=1&type=0", size),
"path": "newtopic/api/Topic/HomePageListRead",
"env": "2",
}).
Post(url)
if err != nil {
logger.SugaredLogger.Errorf("HotTopic err:%s", err.Error())
return []any{}
}
logger.SugaredLogger.Infof("HotTopic:%s", resp.Body())
respMap := map[string]any{}
err = json.Unmarshal(resp.Body(), &respMap)
return respMap["re"].([]any)
}

View File

@ -108,3 +108,30 @@ func TestTradingViewNews(t *testing.T) {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestXUEQIUHotStock(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().XUEQIUHotStock(50, "10")
for _, a := range *res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestHotEvent(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().HotEvent(50)
for _, a := range *res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}
func TestHotTopic(t *testing.T) {
db.Init("../../data/stock.db")
res := NewMarketNewsApi().HotTopic(10)
for _, a := range res {
logger.SugaredLogger.Debugf("value: %+v", a)
}
}

View File

@ -317,3 +317,48 @@ type TVNews struct {
LogoId string `json:"logo_id"`
} `json:"provider"`
}
type XUEQIUHot struct {
Data struct {
Items []HotItem `json:"items"`
ItemsSize int `json:"items_size"`
} `json:"data"`
ErrorCode int `json:"error_code"`
ErrorDescription string `json:"error_description"`
}
type HotItem struct {
Type int `json:"type"`
Code string `json:"code"`
Name string `json:"name"`
Value float64 `json:"value"`
Increment int `json:"increment"`
RankChange int `json:"rank_change"`
HasExist interface{} `json:"has_exist"`
Symbol string `json:"symbol"`
Percent float64 `json:"percent"`
Current float64 `json:"current"`
Chg float64 `json:"chg"`
Exchange string `json:"exchange"`
StockType int `json:"stock_type"`
SubType string `json:"sub_type"`
Ad int `json:"ad"`
AdId interface{} `json:"ad_id"`
ContentId interface{} `json:"content_id"`
Page interface{} `json:"page"`
Model interface{} `json:"model"`
Location interface{} `json:"location"`
TradeSession interface{} `json:"trade_session"`
CurrentExt interface{} `json:"current_ext"`
PercentExt interface{} `json:"percent_ext"`
}
type HotEvent struct {
PicSize interface{} `json:"pic_size"`
Tag string `json:"tag"`
Id int `json:"id"`
Pic string `json:"pic"`
Hot int `json:"hot"`
StatusCount int `json:"status_count"`
Content string `json:"content"`
}

View File

@ -0,0 +1,37 @@
<script setup lang="ts">
import {onBeforeMount, onUnmounted, ref} from 'vue'
import {HotEvent} from "../../wailsjs/go/main/App";
const list = ref([])
const task =ref()
onBeforeMount(async () => {
list.value = await HotEvent(50)
task.value=setInterval(async ()=>{
list.value = await HotEvent(50)
}, 1000*10)
})
onUnmounted(async ()=>{
clearInterval(task.value)
})
</script>
<template>
<n-list bordered>
<template #header>
雪球热门
</template>
<n-list-item v-for="(item, index) in list" :key="index">
<n-thing :title="item.tag" :description="item.content" >
<template v-if="item.pic" #avatar>
<n-avatar :src="item.pic" :size="60">
</n-avatar>
</template>
</n-thing>
</n-list-item>
</n-list>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,76 @@
<script setup lang="ts">
import {onBeforeMount, onUnmounted, ref} from 'vue'
import {HotStock} from "../../wailsjs/go/main/App";
import KLineChart from "./KLineChart.vue";
const {marketType}=defineProps(
{
marketType: {
type: String,
default: '10'
}
}
)
const task =ref()
const list = ref([])
onBeforeMount(async () => {
list.value = await HotStock(marketType)
task.value = setInterval(async () => {
list.value = await HotStock(marketType)
}, 5000)
})
onUnmounted(()=>{
clearInterval(task.value)
})
function getMarketCode(item) {
if (item.exchange === 'SZ') {
return item.code.toLowerCase()
}
if (item.exchange === 'SH') {
return item.code.toLowerCase()
}
if (item.exchange === 'HK') {
return (item.exchange + item.code).toLowerCase()
}
return ("gb_"+item.code).toLowerCase()
}
</script>
<template>
<n-table striped size="small">
<n-thead>
<n-tr>
<n-th>股票名称</n-th>
<n-th>涨跌幅</n-th>
<n-th>当前价格</n-th>
<n-th>热度</n-th>
<n-th>热度变化</n-th>
<n-th>排名变化</n-th>
</n-tr>
</n-thead>
<n-tbody>
<n-tr v-for="item in list" :key="item.code">
<n-td><n-text type="info">
<n-popover trigger="hover" placement="right">
<template #trigger>
<n-tag type="info" :bordered="false"> {{item.name}} {{item.code}}</n-tag>
</template>
<k-line-chart style="width: 800px" :code="getMarketCode(item)" :chart-height="500" :name="item.name" :k-days="20" :dark-theme="true"></k-line-chart>
</n-popover>
</n-text></n-td>
<n-td><n-text :type="item.percent>0?'error':'success'">{{item.percent}}%</n-text></n-td>
<n-td><n-text type="info">{{item.current}}</n-text></n-td>
<n-td><n-text type="info">{{item.value}}</n-text></n-td>
<n-td><n-text type="info">{{item.increment}}</n-text></n-td>
<n-td><n-text type="info">{{item.rank_change}}</n-text></n-td>
</n-tr>
</n-tbody>
</n-table>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,43 @@
<script setup lang="ts">
import {onBeforeMount, onUnmounted, ref} from 'vue'
import {HotTopic} from "../../wailsjs/go/main/App";
const list = ref([])
const task =ref()
onBeforeMount(async () => {
list.value = await HotTopic(10)
setInterval(async ()=>{
list.value = await HotTopic(10)
}, 1000*10)
})
onUnmounted(()=>{
clearInterval(task.value)
})
</script>
<template>
<n-list bordered>
<template #header>
股吧热门
</template>
<n-list-item v-for="(item, index) in list" :key="index">
<n-thing :title="item.nickname" :description="item.desc" >
<template v-if="item.squareImg" #avatar>
<n-avatar :src="item.squareImg" :size="60">
</n-avatar>
</template>
<template v-if="item.stock_list" #footer>
<n-flex>
<n-tag type="info" v-for="(v, i) in item.stock_list" :bordered="false">
{{v.name}}
</n-tag>
</n-flex>
</template>
</n-thing>
</n-list-item>
</n-list>
</template>
<style scoped>
</style>

View File

@ -60,7 +60,7 @@ function handleKLine(code,name){
////console.log("values",values)
let option = {
title: {
text: name,
text: name+" "+code,
left: '20px',
textStyle: {
color: darkTheme?'#ccc':'#456'

View File

@ -26,6 +26,9 @@ import StockResearchReportList from "./StockResearchReportList.vue";
import StockNoticeList from "./StockNoticeList.vue";
import LongTigerRankList from "./LongTigerRankList.vue";
import IndustryResearchReportList from "./IndustryResearchReportList.vue";
import HotStockList from "./HotStockList.vue";
import HotEvents from "./HotEvents.vue";
import HotTopics from "./HotTopics.vue";
const route = useRoute()
const icon = ref('https://raw.githubusercontent.com/ArvinLovegood/go-stock/master/build/appicon.png');
@ -557,8 +560,32 @@ function ReFlesh(source) {
<n-tab-pane name="行业研究" tab="行业研究 ">
<IndustryResearchReportList/>
</n-tab-pane>
<n-tab-pane name="当前热门" tab="当前热门">
<n-tabs type="card" animated>
<n-tab-pane name="全球" tab="全球">
<HotStockList :market-type="'10'"/>
</n-tab-pane>
<n-tab-pane name="沪深" tab="沪深">
<HotStockList :market-type="'12'"/>
</n-tab-pane>
<n-tab-pane name="港股" tab="港股">
<HotStockList :market-type="'13'"/>
</n-tab-pane>
<n-tab-pane name="美股" tab="美股">
<HotStockList :market-type="'11'"/>
</n-tab-pane>
<n-tab-pane name="热门话题" tab="热门话题">
<n-grid :cols="1" :y-gap="10">
<n-grid-item>
<HotTopics/>
</n-grid-item>
<!-- <n-grid-item>-->
<!-- <HotEvents/>-->
<!-- </n-grid-item>-->
</n-grid>
</n-tab-pane>
</n-tabs>
</n-tab-pane>
</n-tabs>
</n-card>
<n-modal transform-origin="center" v-model:show="summaryModal" preset="card" style="width: 800px;"

View File

@ -65,6 +65,12 @@ export function GlobalStockIndexes():Promise<Record<string, any>>;
export function Greet(arg1:string):Promise<data.StockInfo>;
export function HotEvent(arg1:number):Promise<any>;
export function HotStock(arg1:string):Promise<any>;
export function HotTopic(arg1:number):Promise<Array<any>>;
export function IndustryResearchReport(arg1:string):Promise<Array<any>>;
export function LongTigerRank(arg1:string):Promise<any>;

View File

@ -126,6 +126,18 @@ export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
export function HotEvent(arg1) {
return window['go']['main']['App']['HotEvent'](arg1);
}
export function HotStock(arg1) {
return window['go']['main']['App']['HotStock'](arg1);
}
export function HotTopic(arg1) {
return window['go']['main']['App']['HotTopic'](arg1);
}
export function IndustryResearchReport(arg1) {
return window['go']['main']['App']['IndustryResearchReport'](arg1);
}