22 changed files with 2338 additions and 404 deletions
-
2.env.development
-
2.env.production
-
7README.md
-
1138package-lock.json
-
9package.json
-
76src/api/AIxiaocaishen.js
-
BINsrc/assets/img/homePage/tail/voice-no-active.png
-
50src/assets/js/useAppBridge.js
-
159src/assets/js/useProjectTracking.js
-
6src/main.js
-
2src/router/index.js
-
33src/store/audio.js
-
16src/store/chat.js
-
186src/store/dataList.js
-
2src/store/userPessionCode.js
-
2src/utils/languageService.js
-
2src/utils/request.js
-
416src/views/AIchat.vue
-
51src/views/AIfind.vue
-
105src/views/Announcement.vue
-
219src/views/Echarts/KLine.vue
-
243src/views/homePage.vue
1138
package-lock.json
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
After Width: 61 | Height: 61 | Size: 2.1 KiB |
@ -0,0 +1,50 @@ |
|||
//跳转app方法
|
|||
export function useAppBridge() { |
|||
const fullClose = (n, m) => { |
|||
let result = Math.random() * (m + 1 - n) + n |
|||
while (result > m) { |
|||
result = Math.random() * (m + 1 - n) + n |
|||
} |
|||
return Math.floor(result) |
|||
} |
|||
|
|||
const packageFun = (funName, fun = () => {}, platform, data = {}) => { |
|||
const JWrandom = fullClose(10000, 99999) |
|||
data.JWrandom = JWrandom |
|||
|
|||
window[funName + JWrandom] = fun |
|||
|
|||
try { |
|||
const params = { |
|||
name: funName, |
|||
extra: { data } |
|||
} |
|||
|
|||
switch (platform) { |
|||
case 2: // app apicloud
|
|||
window.api.sendEvent(params) |
|||
break |
|||
case 3: // app ios
|
|||
window.webkit.messageHandlers.getTouJiaoData.postMessage(JSON.stringify(params)) |
|||
break |
|||
case 4: // app android
|
|||
window.android.getTouJiaoData(JSON.stringify(params)) |
|||
break |
|||
case 5: // app uniapp
|
|||
window.uni.postMessage({ |
|||
data: { |
|||
val: JSON.stringify(params) |
|||
} |
|||
}) |
|||
break |
|||
} |
|||
} catch (e) { |
|||
console.error('Error in packageFun:', e) |
|||
} |
|||
} |
|||
|
|||
return { |
|||
packageFun, |
|||
fullClose |
|||
} |
|||
} |
@ -0,0 +1,159 @@ |
|||
import { ref, onMounted, onBeforeUnmount, watch } from 'vue' |
|||
import { useRouter } from 'vue-router' |
|||
import { computedUsersAPI } from '@/api/sword' |
|||
|
|||
export function useProjectTracking(projectRoutes) { |
|||
const router = useRouter() |
|||
const entryTime = ref(Date.now()) |
|||
const isInProject = ref(true) |
|||
const hasRecordedEntry = ref(sessionStorage.getItem('hasRecordedEntry') === 'true') |
|||
// const parentUrl = window.parent.location.href
|
|||
// console.log('Link平台地址:', parentUrl)
|
|||
|
|||
let isPageRefreshing = false // 标志位:是否刷新页面
|
|||
|
|||
// 记录用户进入项目的时间
|
|||
const recordEntryTime = () => { |
|||
if (hasRecordedEntry.value) { |
|||
return |
|||
} |
|||
|
|||
entryTime.value = Date.now() |
|||
const date = new Date(entryTime.value) |
|||
const formattedDate = `${date.getFullYear()}-${(date.getMonth() + 1) |
|||
.toString() |
|||
.padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date |
|||
.getHours() |
|||
.toString() |
|||
.padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date |
|||
.getSeconds() |
|||
.toString() |
|||
.padStart(2, '0')}`
|
|||
sessionStorage.setItem('projectEntryTime', formattedDate) |
|||
sessionStorage.setItem('hasRecordedEntry', 'true') |
|||
isInProject.value = true |
|||
hasRecordedEntry.value = true |
|||
console.log('记录首次进入时间:', formattedDate) |
|||
} |
|||
|
|||
// 发送追踪数据到后端
|
|||
const sendTrackingData = async () => { |
|||
if (!isInProject.value) return |
|||
|
|||
const storedEntryTime = sessionStorage.getItem('projectEntryTime') |
|||
if (!storedEntryTime) { |
|||
console.warn('未找到存储的进入时间,取消发送跟踪数据') |
|||
return |
|||
} |
|||
|
|||
let timestamp |
|||
try { |
|||
timestamp = new Date(storedEntryTime.replace(' ', 'T')).getTime() |
|||
if (isNaN(timestamp)) throw new Error('无效日期') |
|||
} catch (error) { |
|||
console.error('解析存储的进入时间时出错:', error) |
|||
return |
|||
} |
|||
|
|||
const exitTime = Date.now() |
|||
const duration = Math.floor((exitTime - timestamp) / 1000) |
|||
const localToken = localStorage.getItem('localToken') |
|||
console.log('进入项目的时间', storedEntryTime) |
|||
console.log('停留时间', duration) |
|||
|
|||
const params = { |
|||
stayTime: duration, |
|||
loginTime: storedEntryTime, |
|||
token: localToken |
|||
} |
|||
|
|||
if (localToken) { |
|||
try { |
|||
const res = await computedUsersAPI(params) |
|||
console.log('跟踪数据已发送:', res) |
|||
sessionStorage.removeItem('projectEntryTime') |
|||
sessionStorage.removeItem('hasRecordedEntry') |
|||
isInProject.value = false |
|||
hasRecordedEntry.value = false |
|||
} catch (error) { |
|||
console.error('发送跟踪数据失败:', error) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 页面可见性变化时触发
|
|||
const handleVisibilityChange = () => { |
|||
// console.log(window.location.pathname.includes('duobaoqibing'), '路径是否包含了页面不可见触发')
|
|||
// if (window.location.pathname.includes('duobaoqibing')) {
|
|||
// console.log('在 searchCode.html 页面,不发送数据')
|
|||
// return
|
|||
// }
|
|||
if (document.visibilityState === 'hidden') { |
|||
console.log('页面不可见,用户可能离开或切换标签页') |
|||
sendTrackingData() |
|||
} |
|||
} |
|||
|
|||
// 页面关闭或刷新时触发
|
|||
const handleBeforeUnload = (event) => { |
|||
// console.log(window.location.pathname)
|
|||
// console.log(
|
|||
// window.location.pathname.includes('duobaoqibing'),
|
|||
// '路径是否包含了页面关闭了啦啦啦啦啦啦触发'
|
|||
// )
|
|||
// if (window.location.pathname.includes('duobaoqibing')) {
|
|||
// console.log('在 searchCode.html 页面,不发送数据')
|
|||
// return
|
|||
// }
|
|||
if (isPageRefreshing) { |
|||
console.log('页面刷新,不触发数据发送') |
|||
return |
|||
} |
|||
|
|||
console.log('页面即将关闭或跳转') |
|||
sendTrackingData() |
|||
} |
|||
|
|||
const handleRefreshDetection = () => { |
|||
isPageRefreshing = true |
|||
} |
|||
|
|||
// 监听路由变化
|
|||
watch( |
|||
() => router.currentRoute.value.path, |
|||
(newPath) => { |
|||
const isProjectRoute = projectRoutes.some((route) => newPath.startsWith(route)) |
|||
let isProjectRouteName = projectRoutes[0] |
|||
console.log(isProjectRouteName) |
|||
// 判断是否是 searchCode.html 的访问
|
|||
const isSearchCodePage = window.location.pathname.includes('duobaoqibing') |
|||
if (!isProjectRoute && !isSearchCodePage) { |
|||
console.log('离开项目路由:', newPath) |
|||
sendTrackingData() |
|||
} else if (isProjectRouteName && !hasRecordedEntry.value) { |
|||
console.log('首次进入项目路由:', newPath) |
|||
recordEntryTime() |
|||
} |
|||
} |
|||
) |
|||
|
|||
// 添加事件监听
|
|||
onMounted(() => { |
|||
document.addEventListener('visibilitychange', handleVisibilityChange) |
|||
window.addEventListener('beforeunload', handleBeforeUnload) |
|||
window.addEventListener('unload', handleRefreshDetection) |
|||
}) |
|||
|
|||
// 移除事件监听
|
|||
onBeforeUnmount(() => { |
|||
document.removeEventListener('visibilitychange', handleVisibilityChange) |
|||
window.removeEventListener('beforeunload', handleBeforeUnload) |
|||
window.removeEventListener('unload', handleRefreshDetection) |
|||
}) |
|||
|
|||
return { |
|||
entryTime, |
|||
isInProject, |
|||
sendTrackingData |
|||
} |
|||
} |
@ -0,0 +1,33 @@ |
|||
import { defineStore } from 'pinia' |
|||
|
|||
export const useAudioStore = defineStore('audio', { |
|||
state: () => ({ |
|||
soundInstance: null, // Howl 实例
|
|||
isPlaying: false, // 播放状态
|
|||
isVoiceEnabled: true // 新增声音开关状态
|
|||
}), |
|||
actions: { |
|||
// 设置音频实例
|
|||
setAudioInstance(instance) { |
|||
this.soundInstance = instance |
|||
}, |
|||
// 播放控制
|
|||
play() { |
|||
if (this.soundInstance) { |
|||
this.soundInstance.play() |
|||
this.isPlaying = true |
|||
} |
|||
}, |
|||
// 暂停控制
|
|||
pause() { |
|||
if (this.soundInstance) { |
|||
this.soundInstance.pause() |
|||
this.isPlaying = false |
|||
} |
|||
}, |
|||
toggleVoice() { |
|||
this.isVoiceEnabled = !this.isVoiceEnabled |
|||
} |
|||
} |
|||
}) |
|||
|
@ -0,0 +1,16 @@ |
|||
import { defineStore } from 'pinia'; |
|||
|
|||
export const useChatStore = defineStore('chat', { |
|||
state: () => ({ |
|||
messages: [] |
|||
}), |
|||
persist: { |
|||
enabled: true, |
|||
strategies: [ |
|||
{ |
|||
key: 'chat_messages', |
|||
storage: localStorage |
|||
} |
|||
] |
|||
} |
|||
}); |
@ -1,40 +1,95 @@ |
|||
<script setup></script> |
|||
<script setup> |
|||
import { ref, onMounted } from "vue"; |
|||
import { getAnnouncementAPI } from "../api/AIxiaocaishen"; |
|||
|
|||
const announcementVideo = ref({}); |
|||
const getAnnouncement = async () => { |
|||
const result = await getAnnouncementAPI() |
|||
console.log(result.data, "result.data"); |
|||
announcementVideo.value.url = result.data[0].url; |
|||
announcementVideo.value.img = result.data[0].img; |
|||
|
|||
console.log(announcementVideo.value, "announcementVideo"); |
|||
} |
|||
|
|||
const handleVideoPlay = () => { |
|||
console.log('视频开始播放'); |
|||
// 可以在这里添加播放统计逻辑 |
|||
} |
|||
|
|||
onMounted(() => { |
|||
getAnnouncement() |
|||
}) |
|||
|
|||
</script> |
|||
<template> |
|||
<el-main class="homepage-body"> |
|||
<div class="main-wrapper"> |
|||
<img |
|||
src="src\assets\img\AIchat\AIgif1.gif" |
|||
alt="图片加载失败" |
|||
class="logo1" |
|||
/> |
|||
<div class="video-container"> |
|||
<video ref="videoPlayer" :poster="announcementVideo.img" :src="announcementVideo.url" controls |
|||
class="video-player" @play="handleVideoPlay"> |
|||
Your browser does not support the video tag. |
|||
</video> |
|||
</div> |
|||
<!-- 一段文字,水平居中,宽度为500px --> |
|||
|
|||
<div style="width: 500px; margin: 0 auto; text-align: center"> |
|||
<p> |
|||
欢迎使用AI智能问答系统,本系统基于OpenAI的GPT-3.5模型,为您提供智能问答服务。 |
|||
</p> |
|||
<p>这个是公告部分</p> |
|||
<div class="announcement"> |
|||
<p class="announcementItem">各位AI小财神的用户,大家好!</p> |
|||
<p class="announcementItem">试运行期间,用户可在AI小财神中查看全</p> |
|||
<p class="announcementItem">市场数据,每个市场可查看20只股票.</p> |
|||
<p class="announcementItem">试运行结束后,会员用户可查看市场与弘</p> |
|||
<p class="announcementItem">历软件云版静态市场一致!</p> |
|||
<p class="announcementItem">特此公告!</p> |
|||
</div> |
|||
</div> |
|||
</el-main> |
|||
</template> |
|||
<style scoped> |
|||
.homepage-body { |
|||
padding: 0px; |
|||
height: calc(100% - 70px); |
|||
/* 根据底部高度调整 */ |
|||
} |
|||
|
|||
.main-wrapper { |
|||
height: 100%; |
|||
display: flex; |
|||
flex-direction: column; |
|||
overflow-y: auto; |
|||
} |
|||
.logo1 { |
|||
/* 居中显示 30%大小 不要拉伸*/ |
|||
display: block; |
|||
width: 30%; |
|||
height: auto; |
|||
/* 水平居中 */ |
|||
margin: 0 auto; |
|||
|
|||
.video-container { |
|||
max-width: 90%; /* 从 800px 改为百分比 */ |
|||
width: 100%; |
|||
margin: 20px auto; |
|||
border-radius: 8px; |
|||
overflow: hidden; |
|||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); |
|||
flex-shrink: 0; |
|||
} |
|||
|
|||
.video-player { |
|||
width: 100%; |
|||
aspect-ratio: 16/9; |
|||
background-color: #000; |
|||
object-fit: contain; /* 从 cover 改为 contain */ |
|||
} |
|||
|
|||
/* 添加移动端适配 */ |
|||
@media (max-width: 768px) { |
|||
.video-container { |
|||
margin: 10px auto; |
|||
border-radius: 4px; |
|||
} |
|||
|
|||
.announcement { |
|||
max-width: 90%; |
|||
font-size: 14px; |
|||
} |
|||
} |
|||
|
|||
.announcement { |
|||
max-width: 500px; |
|||
margin: 20px 10%; |
|||
font-size: 20px; |
|||
font-weight: bold; |
|||
} |
|||
|
|||
.announcementItem { |
|||
margin-bottom: 10px; |
|||
} |
|||
|
|||
</style> |
@ -0,0 +1,219 @@ |
|||
<template> |
|||
<!-- 趋势研判K线图 --> |
|||
<div ref="KlineCanvs" class="KlineClass"></div> |
|||
</template> |
|||
|
|||
<script setup> |
|||
import * as echarts from 'echarts' |
|||
import { useDataStore } from '@/store/dataList' |
|||
// import { useLanguage } from '@/utils/languageService' |
|||
// const { translate, t } = useLanguage() |
|||
const KlineCanvs = ref() // Echarts实例 |
|||
const dataStore = useDataStore() |
|||
const klineData = computed(() => dataStore.klineData) |
|||
watch( |
|||
klineData, |
|||
(newValue) => { |
|||
const currentData = newValue // 深拷贝防止切换回来数据更改 |
|||
if (currentData) { |
|||
nextTick(() => { |
|||
KlineCanvsEcharts(currentData) |
|||
}) |
|||
} |
|||
}, |
|||
{ immediate: true, deep: true } |
|||
) |
|||
// 监听数据变化或语言变化 |
|||
// watch( |
|||
// () => t.value, |
|||
// (newLang) => { |
|||
// // 更新 textEcharts 内容并重新渲染图表 |
|||
// if (klineData.value) { |
|||
// const currentData = klineData |
|||
// nextTick(() => { |
|||
// KlineCanvsEcharts(currentData) |
|||
// }) |
|||
// } |
|||
// }, |
|||
// { immediate: true, deep: true } |
|||
// ) |
|||
function KlineCanvsEcharts(datatok) { |
|||
// const textEcahrts = t.value // 创建多语言实例 |
|||
const data = datatok.Kline |
|||
// 切割数据方法 |
|||
const spliteDate = (a) => { |
|||
const categoryData = [] |
|||
let value = [] |
|||
for (let i = 0; i < a.length; i++) { |
|||
categoryData.push(a[i][0]) |
|||
value.push([a[i][1],a[i][2],a[i][3],a[i][4]]) |
|||
} |
|||
return { categoryData, value } |
|||
} |
|||
const dealData = spliteDate(data) |
|||
// 给配置项 |
|||
const KlineOption = { |
|||
title: { |
|||
// text: k_name, |
|||
// Canvs的间隔大小 |
|||
text: datatok.name, |
|||
top: 20, |
|||
left: 20 |
|||
}, |
|||
tooltip: { |
|||
trigger: 'axis', // 触发类型 坐标轴触发 |
|||
// 调用接口之后方法 |
|||
formatter: function (a, b, d) { |
|||
let def = |
|||
a[0].name + |
|||
'<br/>' + |
|||
'开盘价' + |
|||
a[0].data[1] + |
|||
'<br/>' + |
|||
'收盘价' + |
|||
a[0].data[2] + |
|||
'<br/>' + |
|||
'最低价' + |
|||
a[0].data[3] + |
|||
'<br/>' + |
|||
'最高价' + |
|||
a[0].data[4] |
|||
// 判断a[1]是否存在 |
|||
if (a[1] && a[1].seriesName) { |
|||
def += '<br/>' + a[1].seriesName + ':' + a[1].value |
|||
} |
|||
return def |
|||
}, |
|||
// 鼠标放上去出现的数据展示 |
|||
axisPointer: { |
|||
animation: false, |
|||
type: 'line', |
|||
linestyle: { |
|||
color: '#376df4', |
|||
width: 2, |
|||
opacity: 1 |
|||
} |
|||
} |
|||
}, |
|||
// 横坐标内容 |
|||
xAxis: { |
|||
type: 'category', |
|||
data: dealData.categoryData, |
|||
axisLine: { lineStyle: { color: '#8392A5' } } |
|||
}, |
|||
//控制坐标轴 |
|||
grid: { |
|||
left: '12%', |
|||
right: '10%', |
|||
bottom: '10%', |
|||
top: '18%' |
|||
}, |
|||
yAxis: { |
|||
scale: !0, //true |
|||
// 自定义纵坐标现实的数据 |
|||
axisLabel: { |
|||
formatter: function (value) { |
|||
return value // 返回原始值 |
|||
} |
|||
}, |
|||
axisLine: { lineStyle: { color: '#8392A5' } }, |
|||
splitLine: { |
|||
show: !1 |
|||
} |
|||
}, |
|||
// 下拉条 |
|||
dataZoom: [ |
|||
{ |
|||
textStyle: { |
|||
color: '#8392A5' |
|||
}, |
|||
handleIcon: |
|||
'M10.7,11.9v-1.3H9.3v1.3c-4.9,0.3-8.8,4.4-8.8,9.4c0,5,3.9,9.1,8.8,9.4v1.3h1.3v-1.3c4.9-0.3,8.8-4.4,8.8-9.4C19.5,16.3,15.6,12.2,10.7,11.9z M13.3,24.4H6.7V23h6.6V24.4z M13.3,19.6H6.7v-1.4h6.6V19.6z', |
|||
handleSize: '80%', |
|||
dataBackground: { |
|||
areaStyle: { |
|||
color: '#8392A5' |
|||
}, |
|||
lineStyle: { |
|||
opacity: 0.8, |
|||
color: '#8392A5' |
|||
} |
|||
}, |
|||
handleStyle: { |
|||
color: '#fff', |
|||
shadowBlur: 3, |
|||
shadowColor: 'rgba(0, 0, 0, 0.6)', |
|||
shadowOffsetX: 2, |
|||
shadowOffsetY: 2 |
|||
} |
|||
}, |
|||
{ |
|||
show: !1, |
|||
type: 'slider' |
|||
}, |
|||
{ |
|||
type: 'inside' |
|||
} |
|||
], |
|||
animation: !1, //false |
|||
// 线条的数据 |
|||
series: [ |
|||
{ |
|||
type: 'candlestick', |
|||
name: '\u65e5K', |
|||
// 数据 |
|||
data: dealData.value, |
|||
itemStyle: { |
|||
normal: { |
|||
color0: '#FD1050', |
|||
color: '#0CF49B', |
|||
borderColor0: '#FD1050', |
|||
borderColor: '#0CF49B' |
|||
} |
|||
} |
|||
}, |
|||
{ |
|||
name: 'MA5', |
|||
type: 'line', |
|||
// 此处需要接口调用同类型数据 |
|||
// 计算出MA5的数据 |
|||
data: (function (a) { |
|||
for (var MA5 = [], d = 0, g = dealData.value.length; d < g; d++) { |
|||
if (d < a) { |
|||
MA5.push('-') |
|||
} else { |
|||
for (var f = 0, e = 0; e < a; e++) { |
|||
f += dealData.value[d - e][1] |
|||
} |
|||
MA5.push((f / a).toFixed(2)) |
|||
} |
|||
} |
|||
return MA5 |
|||
})(5), |
|||
smooth: !0 |
|||
} |
|||
] |
|||
} |
|||
// 创造echarts图 |
|||
const KlineCanvsChart = echarts.init(KlineCanvs.value) |
|||
KlineCanvsChart.setOption(KlineOption) |
|||
// 窗口大小变化时重置图表大小 |
|||
window.addEventListener('resize', () => { |
|||
KlineCanvsChart.resize() |
|||
}) |
|||
} |
|||
|
|||
onMounted(() => { |
|||
// fnGetData() |
|||
}) |
|||
</script> |
|||
|
|||
<style scoped> |
|||
.KlineClass { |
|||
position: relative; |
|||
width: 100%; |
|||
height: 100%; |
|||
left: 5%; |
|||
bottom: 8%; |
|||
} |
|||
</style> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue