2026-08-04 17:35:35 +08:00

1347 lines
38 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="top-nav">
<div class="top-nav-placeholder"></div>
<header id="topContainer" class="nav-container">
<div class="brand" @click="goHome">
<img
v-if="JSON.stringify(logoInfoNew) !== '{}'"
class="brand-logo"
:src="(logoInfoNew.home && logoInfoNew.home.logoImg) || ''"
alt="brand"
>
</div>
<nav class="site-nav" aria-label="home nav">
<button type="button" class="nav-item" :class="{ active: isActiveHome }" @click.stop="goHome">
{{ $t('topbar.home') }}
</button>
<div
ref="productWrap"
class="nav-product-wrap"
@mouseenter="handleProductPointerEnter"
@mouseleave="closeProductPanelImmediate"
>
<button
type="button"
class="nav-item nav-item--product"
:class="{ active: isProductPanelVisible }"
@click.prevent.stop="openProductPanelByClick"
>
{{ $t('topbar.product') }}
<i class="iconfont" :class="isProductPanelVisible ? 'icon-shang' : 'icon-xia'"></i>
</button>
<div
v-if="isProductPanelVisible"
class="product-panel product-panel--side-only"
>
<aside class="product-panel__side">
<button
v-for="item in localizedProductMenuItems"
:key="item.action"
type="button"
class="product-side-item"
@click="handleProductMenuClick(item.action)"
>
{{ item.label }}
</button>
</aside>
</div>
</div>
<button v-if="!isNcmatchDomain" type="button" class="nav-item" @click.stop="goHomeAnchor('cases-section')">
{{ $t('topbar.cases') }}
</button>
<button v-if="!isNcmatchDomain" type="button" class="nav-item" @click.stop="goHomeAnchor('news')">
{{ $t('topbar.news') }}
</button>
<button
type="button"
class="nav-item"
:class="{ active: $route.path.includes('/homePage/about') }"
@click.stop="navigateTo('/homePage/about')"
>
{{ aboutUsText }}
</button>
</nav>
<div class="user-actions">
<button
v-if="!isNcmatchDomain"
type="button"
class="nav-lang-text"
:class="{ 'is-en': activeLocale === 'en-US' }"
:title="langToggleTitle"
@click.stop="toggleLanguage"
>
{{ langToggleText }}
</button>
<i v-if="loginState" class="iconfont icon-xiaoxi functions" @click="handleMessageClick"></i>
<button v-if="loginState" type="button" class="console-link" @click="goB">
{{ $t('topbar.console') }}
</button>
<button v-if="!loginState" type="button" class="login-btn" @click.stop="openLoginDialog">
{{ $t('topbar.login') }}
</button>
<el-dropdown
v-if="loginState"
class="avatar-container right-menu-item hover-effect nick-name-style"
trigger="click"
@visible-change="handleShow"
>
<button type="button" class="login-pill login-pill--user">
{{ nick_name || $t('topbar.console') }}
<i id="resverIcon" class="el-icon-arrow-up el-icon--right resverIcon"></i>
</button>
<el-dropdown-menu slot="dropdown" style="width: 230px; font-size: 16px;" divided>
<div class="user-dropdown-head">
<span class="user-dropdown-name">{{ nick_name }}</span>
<span class="user-dropdown-id">
ID:
<span ref="contentToCopy">{{ userId }}</span>
<svg
class="copy-btn"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
style="fill: #1019ab;"
@click="copyBtn"
>
<path d="M394.666667 106.666667h448a74.666667 74.666667 0 0 1 74.666666 74.666666v448a74.666667 74.666667 0 0 1-74.666666 74.666667H394.666667a74.666667 74.666667 0 0 1-74.666667-74.666667V181.333333a74.666667 74.666667 0 0 1 74.666667-74.666666z m0 64a10.666667 10.666666 0 0 0-10.666667 10.666666v448a10.666667 10.666667 0 0 0 10.666667 10.666667h448a10.666667 10.666667 0 0 0 10.666666-10.666667V181.333333a10.666667 10.666667 0 0 0-10.666666-10.666666H394.666667z m245.333333 597.333333a32 32 0 0 1 64 0v74.666667a74.666667 74.666667 0 0 1-74.666667 74.666666H181.333333a74.666667 74.666667 0 0 1-74.666666-74.666666V394.666667a74.666667 74.666667 0 0 1 74.666666-74.666667h74.666667a32 32 0 0 1 0 64h-74.666667a10.666667 10.666667 0 0 0-10.666666 10.666667v448a10.666667 10.666667 0 0 0 10.666666 10.666666h448a10.666667 10.666667 0 0 0 10.666667-10.666666v-74.666667z"></path>
</svg>
</span>
</div>
<el-divider style="margin: 0 !important;"></el-divider>
<div
v-if="role.includes('客户')"
class="user-dropdown-balance"
>
<span class="moneyNow">
<span class="moneyNow-label">余额</span>
{{ mybalance ? mybalance : initMybalance() }}
</span>
<el-button
v-if="!isShowKbossCharge"
type="primary"
plain
style="padding: 8px;"
@click="$router.push('/kbossCharge')"
>
充值
</el-button>
</div>
<el-dropdown-item
v-if="role.includes('客户')"
@click.native="$router.push('/customer/customerInformation')"
>
<span style="display: block">个人中心</span>
</el-dropdown-item>
<el-dropdown-item @click.native="logout">
<span style="display: block">退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</header>
<div
v-show="aiDialogVisible"
ref="aiChatPanel"
class="ai-chat-panel"
:style="aiPanelStyle"
>
<div class="ai-chat-panel__header" @mousedown="startAIPanelDrag">
<div class="ai-chat-panel__title">
<span class="ai-chat-panel__logo">AI</span>
<span>有问题找开元</span>
</div>
<i class="el-icon-close ai-chat-panel__close" @click="closeAIPanel"></i>
</div>
<div ref="aiChatMessages" class="ai-chat-panel__body">
<div v-if="!aiMessages.length" class="ai-chat-panel__welcome">
<h3>Hi!</h3>
<p>我是开元智能助手可以为您解答算力服务器选型采购部署和资源配置等问题</p>
<div class="ai-chat-panel__suggestions">
<span
v-for="item in aiQuickQuestions"
:key="item"
class="ai-chat-panel__tag"
@click="useAISuggestion(item)"
>
{{ item }}
</span>
</div>
</div>
<div
v-for="(item, index) in aiMessages"
:key="index"
:class="['ai-chat__message', `is-${item.role}`]"
>
<div class="ai-chat__avatar">
{{ item.role === 'assistant' ? 'AI' : '我' }}
</div>
<div class="ai-chat__bubble">
<div
v-if="item.role === 'assistant'"
class="ai-chat__rich-text"
v-html="formatAIContent(item.content)"
></div>
<span v-else>{{ item.content }}</span>
</div>
</div>
</div>
<div class="ai-chat-panel__input-wrap">
<div class="ai-chat-panel__actions">
<span class="ai-chat-panel__new" @click="resetAIChat">新对话</span>
<span class="ai-chat__tip">Enter 发送</span>
</div>
<div class="ai-chat-panel__input">
<el-input
v-model="aiInput"
type="textarea"
:rows="2"
resize="none"
maxlength="500"
placeholder="请输入你的问题"
@keydown.native="handleAIKeydown"
/>
<el-button class="ai-chat-panel__send" type="text" :loading="aiLoading" @click="sendAIMessage">
<i class="iconfont icon-fasong"></i>
</el-button>
</div>
</div>
</div>
<message-center
ref="messageCenter"
:visible.sync="messageCenterVisible"
:user-id="userId"
@unread-count-update="handleUnreadCountUpdate"
/>
<login-dialog
:visible.sync="loginDialogVisible"
@success="handleLoginDialogSuccess"
@forgot-password="goLogin"
/>
</div>
</template>
<script>
import Vue from 'vue'
import { mapGetters, mapState } from 'vuex'
import store from '@/store'
import { getLogoAPI, getUserInfoAPI } from '@/api/login'
import { getHomePath } from '@/views/setting/tools'
import { reqAIChat } from '@/api/AI/ai'
import { gotoYuanJingAPI } from '@/api/gotoYuanJing'
import { setLocale } from '@/i18n'
import MessageCenter from '@/components/MessageCenter/MessageCenter.vue'
import LoginDialog from '@/views/LoginDialog/LoginDialog.vue'
const LOCALE_KEY = 'kboss-locale'
const SUPPORTED_LOCALES = ['zh-CN', 'en-US']
export default Vue.extend({
name: 'TopBox',
components: { MessageCenter, LoginDialog },
data() {
return {
// AI 聊天面板状态
aiDialogVisible: false,
aiInput: '',
aiLoading: false,
aiMessages: [],
aiPanelPosition: { left: null, top: null },
aiPanelDragging: false,
aiPanelDragOffset: { x: 0, y: 0 },
aiQuickQuestions: [
'推荐适合训练大模型的 GPU 服务器',
'4090 和 A100 怎么选',
'有没有高性价比 8 卡方案',
'国产化算力服务器有哪些'
],
// 顶栏交互状态
messageCenterVisible: false,
messageCount: 0,
activeLocale: 'zh-CN',
isProductPanelVisible: false,
loginDialogVisible: false,
consoleRoutesEnsured: false,
// 登录信息
isShowKbossCharge: false,
role: sessionStorage.getItem('jueseNew') === 'admin' ? '' : (sessionStorage.getItem('jueseNew') || ''),
userId: sessionStorage.getItem('userId'),
nick_name: '',
// 产品菜单action 为稳定路由标识label 由计算属性按语言生成)
productMenuItems: [
{ action: 'opc' },
{ action: 'computeMarket' },
{ action: 'tokenMarket' },
{ action: 'yuanjing' },
{ action: 'trainPlatform' },
{ action: 'agentStore' },
{ action: 'supplySquare' },
// { action: 'about' }
]
}
},
computed: {
...mapGetters(['sidebar', 'avatar', 'device', 'permission_routes']),
...mapState({
mybalance: state => state.user.mybalance,
logoutUrl: state => state.login.logoutUrl,
loginStateVuex: state => state.login.loginState,
logoInfoNew: state => state.product.logoInfoNew
}),
loginState() {
const userId = sessionStorage.getItem('userId')
return this.loginStateVuex || (userId !== null && userId !== 'null' && userId !== '')
},
showRegisterButton() {
const orgType = sessionStorage.getItem('org_type')
const userId = sessionStorage.getItem('userId')
return orgType !== '2' && orgType !== '3' && userId === null
},
isNcmatchDomain() {
return window.location.hostname.includes('ncmatch.cn') || window.location.hostname.includes('zgcopc.opencomputing.cn')
},
isActiveHome() {
return this.isNcmatchDomain
? this.$route.path.includes('/ncmatchHome/index')
: this.$route.path.includes('/homePage/index')
},
langToggleText() {
return this.activeLocale === 'en-US' ? 'EN/中' : '中/EN'
},
langToggleTitle() {
return this.activeLocale === 'en-US' ? 'Switch to Chinese' : '切换到英文'
},
aboutUsText() {
return this.activeLocale === 'en-US' ? 'About Us' : '关于我们'
},
localizedProductMenuItems() {
const isEn = this.activeLocale === 'en-US'
const labels = {
opc: 'OPC',
computeMarket: isEn ? 'Compute Market' : '算力市场',
tokenMarket: isEn ? 'Token Market' : 'Token市集',
yuanjing: isEn ? 'Yuanjing' : '元境',
trainPlatform: isEn ? 'Train & Infer' : '训推平台',
agentStore: isEn ? 'Agent Store' : '智能体商店',
supplySquare: isEn ? 'Supply Square' : '供需广场',
// about: isEn ? 'About Us' : '关于我们'
}
const hiddenNcmatchActions = ['trainPlatform', 'agentStore']
return this.productMenuItems
.filter(item => !this.isNcmatchDomain || !hiddenNcmatchActions.includes(item.action))
.map(item => ({ ...item, label: labels[item.action] || item.action }))
},
aiPanelStyle() {
if (this.aiPanelPosition.left === null || this.aiPanelPosition.top === null) return {}
return {
left: `${this.aiPanelPosition.left}px`,
top: `${this.aiPanelPosition.top}px`,
right: 'auto',
bottom: 'auto'
}
}
},
watch: {
'$route.fullPath'() {
this.closeProductPanelImmediate()
}
},
created() {
this.initLocale()
if (sessionStorage.getItem('userId')) this.$store.commit('setLoginState', true)
if (!this.showRegisterButton) this.getUserInfo()
this.fetchLogoInfo()
},
mounted() {
this.nick_name = sessionStorage.getItem('username') || ''
window.addEventListener('kboss-open-ai-chat', this.handleExternalAIOpen)
window.addEventListener('kboss-open-login-dialog', this.handleExternalLoginOpen)
},
beforeDestroy() {
window.removeEventListener('kboss-open-ai-chat', this.handleExternalAIOpen)
window.removeEventListener('kboss-open-login-dialog', this.handleExternalLoginOpen)
this.stopAIPanelDrag()
},
methods: {
// =========================
// 多语言
// =========================
normalizeLocale(locale) {
return SUPPORTED_LOCALES.includes(locale) ? locale : 'zh-CN'
},
initLocale() {
const locale = this.normalizeLocale(localStorage.getItem(LOCALE_KEY) || (this.$i18n && this.$i18n.locale))
this.applyLocale(locale)
},
applyLocale(locale) {
const safeLocale = this.normalizeLocale(locale)
setLocale(safeLocale)
if (this.$i18n) this.$i18n.locale = safeLocale
if (this.$root && this.$root.$i18n) this.$root.$i18n.locale = safeLocale
this.activeLocale = safeLocale
return safeLocale
},
toggleLanguage() {
this.closeProductPanelImmediate()
const next = this.activeLocale === 'en-US' ? 'zh-CN' : 'en-US'
const appliedLocale = this.applyLocale(next)
this.$message.success(
appliedLocale === 'en-US' ? this.$t('topbar.switchEnSuccess') : this.$t('topbar.switchZhSuccess')
)
},
// =========================
// 顶部产品菜单:稳定的开关逻辑
// =========================
openProductPanel() {
this.isProductPanelVisible = true
this.$store.commit('setShowHomeNav', true)
},
closeProductPanelImmediate() {
this.isProductPanelVisible = false
this.$store.commit('setShowHomeNav', false)
},
handleProductPointerEnter() {
this.openProductPanel()
},
openProductPanelByClick() {
this.openProductPanel()
},
handleProductMenuClick(action) {
const actionMap = {
opc: () => this.navigateTo('/homePage/opc'),
computeMarket: () => this.navigateTo('/homePage/computeMarket'),
tokenMarket: this.handleModelSquareClick,
yuanjing: this.goYuanjing,
trainPlatform: () => this.navigateTo('/homePage/trainPlatform'),
agentStore: () => this.navigateTo('/homePage/agentStore'),
supplySquare: () => this.navigateTo('/ncmatchHome/supplyAndDemandSquare'),
about: () => this.navigateTo('/homePage/about')
}
const handler = actionMap[action]
if (typeof handler === 'function') handler.call(this)
this.closeProductPanelImmediate()
},
// =========================
// 页面导航
// =========================
navigateTo(path) {
this.closeProductPanelImmediate()
if (this.$route.path === path) return
this.$router.push(path).catch(() => {})
},
goHome() {
this.closeProductPanelImmediate()
const target = this.isNcmatchDomain ? '/ncmatchHome/index' : '/homePage/index'
this.$router.push(target).catch(() => {})
},
goHomeAnchor(id) {
this.closeProductPanelImmediate()
const scrollToTarget = () => {
const target = document.getElementById(id)
if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
if (this.$route.path.includes('/homePage/index')) {
this.$nextTick(scrollToTarget)
return
}
this.$router.push('/homePage/index').then(() => this.$nextTick(scrollToTarget)).catch(() => {})
},
handleModelSquareClick() {
this.closeProductPanelImmediate()
this.$router.push({
path: '/homePage/tokenMarket',
query: { category: 'TOKEN市集', single: '1' }
}).catch(() => {})
},
async goYuanjing() {
this.closeProductPanelImmediate()
const yuanJingBaseUrl = window.location.origin === 'https://dev.opencomputing.cn'
? 'https://aidev.opencomputing.cn'
: 'https://ai.opencomputing.cn'
if (!this.loginState) {
window.open(`${yuanJingBaseUrl}/`, '_blank')
return
}
const yuanJingWindow = window.open('', '_blank')
try {
const res = await gotoYuanJingAPI({ user_id: sessionStorage.getItem('userId') })
const deerer = this.getYuanJingAuthorization(res)
if (!deerer) {
if (yuanJingWindow) yuanJingWindow.close()
this.$message.error((res && res.msg) || '获取元境授权参数失败')
return
}
const loginUrl = `${yuanJingBaseUrl}/#/getCookie?deerer=${encodeURIComponent(deerer)}`
if (yuanJingWindow) yuanJingWindow.location.href = loginUrl
else window.location.href = loginUrl
} catch (error) {
if (yuanJingWindow) yuanJingWindow.close()
this.$message.error('跳转元境失败,请稍后重试')
}
},
getYuanJingAuthorization(res) {
if (!res) return ''
if (typeof res === 'string') return res
const data = res.data || res
if (typeof data === 'string') return data
return data.Authorization || data.authorization || data.token || data.header || data.value || ''
},
// =========================
// 登录、用户与消息
// =========================
openLoginDialog() {
this.closeProductPanelImmediate()
if (this.$route && this.$route.fullPath && !this.$route.fullPath.includes('/login')) {
sessionStorage.setItem('loginRedirectPath', this.$route.fullPath)
}
this.loginDialogVisible = true
},
handleLoginDialogSuccess() {
this.loginDialogVisible = false
this.nick_name = sessionStorage.getItem('username') || ''
this.userId = sessionStorage.getItem('userId')
this.consoleRoutesEnsured = false
},
goLogin() {
this.openLoginDialog()
},
handleExternalLoginOpen() {
this.openLoginDialog()
},
handleMessageClick() {
if (this.loginState) {
this.messageCenterVisible = true
if (this.$refs.messageCenter) this.$refs.messageCenter.open()
return
}
this.goLogin()
},
handleUnreadCountUpdate(count) {
this.messageCount = count
},
async ensureConsoleRoutes() {
if (this.consoleRoutesEnsured) return
const authsText = sessionStorage.getItem('auths') || '[]'
let auths = []
try {
auths = JSON.parse(authsText)
} catch (error) {
auths = []
}
const rolesText = sessionStorage.getItem('roles') || '[]'
let roles = []
try {
roles = JSON.parse(rolesText)
} catch (error) {
roles = []
}
const accessRoutes = await this.$store.dispatch('permission/generateRoutes', {
user: sessionStorage.getItem('username') || '',
auths,
orgType: sessionStorage.getItem('org_type'),
roles
})
this.$router.addRoutes(accessRoutes)
this.consoleRoutesEnsured = true
},
async goB() {
await this.ensureConsoleRoutes()
const role = sessionStorage.getItem('jueseNew') || ''
if (role.includes('客户')) this.$router.push('/product/productHome').catch(() => this.$router.replace('/product/productHome'))
else if (role.includes('运营')) this.$router.push('/operation/supplierManagement')
else if (role.includes('运维')) this.$router.push('/operationAndMaintenance/workOrderProcessing')
else if (role.includes('销售')) this.$router.push('/sales/distributorManagement')
else if (role.includes('财务')) this.$router.push('/finance/supplierSettlementStatistics')
else if (role.includes('管理员')) this.$router.push('/superAdministrator/roleManagement')
else if (role.includes('admin')) this.$router.push('/superAdministrator/addAdmin')
},
async logout() {
this.$store.commit('setLoginState', false)
store.commit('tagsView/resetBreadcrumbState')
store.commit('permission/RESET_ROUTES')
sessionStorage.removeItem('auths')
sessionStorage.removeItem('routes')
sessionStorage.removeItem('user')
sessionStorage.removeItem('userId')
sessionStorage.removeItem('org_type')
sessionStorage.removeItem('userType')
sessionStorage.removeItem('orgType')
sessionStorage.removeItem('roles')
sessionStorage.removeItem('juese')
sessionStorage.removeItem('jueseNew')
sessionStorage.removeItem('loginRedirectPath')
localStorage.removeItem('auths')
localStorage.removeItem('routes')
localStorage.removeItem('user')
localStorage.removeItem('userId')
localStorage.removeItem('org_type')
this.userId = ''
this.nick_name = ''
await this.$router.replace(getHomePath()).catch(() => {})
},
initMybalance() {
return sessionStorage.getItem('mybalance')
},
copyBtn() {
const textToCopy = this.$refs.contentToCopy ? this.$refs.contentToCopy.innerText : ''
navigator.clipboard.writeText(textToCopy).then(() => {
this.$message.success('复制成功!')
}).catch(() => {
this.$message.error('复制失败,请重试')
})
},
getUserInfo() {
if (!this.userId) return
getUserInfoAPI({ id: this.userId }).then((res) => {
if (res.status && res.data && res.data[0]) {
sessionStorage.setItem('orgid', res.data[0].orgid)
}
})
},
handleShow(isShow) {
const icon = document.getElementById('resverIcon')
if (!icon) return
icon.style.transform = isShow ? 'rotate(180deg)' : 'rotate(0)'
if (isShow && this.role.includes('客户')) {
this.$store.dispatch('user/getCustmoersMoney')
}
},
fetchLogoInfo() {
const params = { url_link: window.location.href.split('#')[0] }
getLogoAPI(params).then((res) => {
if (res.status === true && res.data && res.data.length) {
const info = res.data[0]
const additionalMsg = info.additional_msg || {}
const home = { ...(additionalMsg.home || {}) }
home.mobile = home.mobile || info.mobile || ''
home.email = home.email || info.email || ''
home.address_zh = home.address_zh || home.address || info.address || home.adress || ''
home.address_en = home.address_en || home.address_zh || home.address || info.address || home.adress || ''
home.adress = home.address_zh
this.$store.commit('setLogoInfoNew', {
...additionalMsg,
home
})
if (info.orgname !== '业主机构') {
this.$store.commit('setLogo', info.logo)
} else {
this.$store.commit('setLogo', '')
}
}
})
},
// =========================
// AI 面板
// =========================
handleAIClick() {
this.aiDialogVisible = true
this.$nextTick(() => {
this.initAIPanelPosition()
this.scrollAIChatToBottom()
})
},
handleExternalAIOpen() {
this.handleAIClick()
},
closeAIPanel() {
this.aiDialogVisible = false
this.stopAIPanelDrag()
},
resetAIChat() {
this.aiMessages = []
this.aiInput = ''
},
useAISuggestion(question) {
this.sendAIMessage(question)
},
initAIPanelPosition() {
if (this.aiPanelPosition.left !== null && this.aiPanelPosition.top !== null) return
const panel = this.$refs.aiChatPanel
if (!panel) return
const panelWidth = panel.offsetWidth || 380
const panelHeight = panel.offsetHeight || 620
this.aiPanelPosition = {
left: Math.max(window.innerWidth - panelWidth - 24, 16),
top: Math.max(window.innerHeight - panelHeight - 24, 16)
}
},
startAIPanelDrag(event) {
if (event.button !== 0) return
if (event.target && event.target.closest('.ai-chat-panel__close')) return
const panel = this.$refs.aiChatPanel
if (!panel) return
const rect = panel.getBoundingClientRect()
this.aiPanelDragging = true
this.aiPanelDragOffset = {
x: event.clientX - rect.left,
y: event.clientY - rect.top
}
window.addEventListener('mousemove', this.handleAIPanelDrag)
window.addEventListener('mouseup', this.stopAIPanelDrag)
document.body.style.userSelect = 'none'
},
handleAIPanelDrag(event) {
if (!this.aiPanelDragging) return
const panel = this.$refs.aiChatPanel
if (!panel) return
const panelWidth = panel.offsetWidth || 380
const panelHeight = panel.offsetHeight || 620
const maxLeft = Math.max(window.innerWidth - panelWidth, 0)
const maxTop = Math.max(window.innerHeight - panelHeight, 0)
const nextLeft = event.clientX - this.aiPanelDragOffset.x
const nextTop = event.clientY - this.aiPanelDragOffset.y
this.aiPanelPosition = {
left: Math.min(Math.max(nextLeft, 0), maxLeft),
top: Math.min(Math.max(nextTop, 0), maxTop)
}
},
stopAIPanelDrag() {
this.aiPanelDragging = false
window.removeEventListener('mousemove', this.handleAIPanelDrag)
window.removeEventListener('mouseup', this.stopAIPanelDrag)
document.body.style.userSelect = ''
},
handleAIKeydown(event) {
if (event.key === 'Enter' && !event.shiftKey) {
event.preventDefault()
this.sendAIMessage()
}
},
async sendAIMessage(quickQuestion) {
const message = (quickQuestion || this.aiInput).trim()
if (!message || this.aiLoading) return
this.aiMessages.push({ role: 'user', content: message })
this.aiInput = ''
const loadingIndex = this.aiMessages.push({
role: 'assistant',
content: '[正在检索并生成回答...]'
}) - 1
this.aiLoading = true
this.$nextTick(() => this.scrollAIChatToBottom())
try {
const response = await reqAIChat({ message }, (content) => {
if (!content) return
this.$set(this.aiMessages, loadingIndex, { role: 'assistant', content })
this.$nextTick(() => this.scrollAIChatToBottom())
})
if (response && response.content) {
this.$set(this.aiMessages, loadingIndex, { role: 'assistant', content: response.content })
} else {
this.$set(this.aiMessages, loadingIndex, {
role: 'assistant',
content: '暂时没有获取到回复,请稍后再试。'
})
}
} catch (error) {
this.$set(this.aiMessages, loadingIndex, {
role: 'assistant',
content: '抱歉,当前服务繁忙,请稍后再试。'
})
} finally {
this.aiLoading = false
this.$nextTick(() => this.scrollAIChatToBottom())
}
},
escapeAIHtml(content) {
return String(content || '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
},
formatAIInline(content) {
return this.escapeAIHtml(content).replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
},
formatAIContent(content) {
if (!content) return ''
const normalized = String(content)
.replace(/\r\n/g, '\n')
.replace(/ {2,}/g, '\n')
.replace(/\n{3,}/g, '\n\n')
return normalized.split('\n').map((line) => {
const trimmed = line.trim()
if (!trimmed) return '<div class="ai-chat__paragraph ai-chat__paragraph--space"></div>'
if (/^-\s+/.test(trimmed)) {
return `<div class="ai-chat__item"><span class="ai-chat__item-dot"></span><span>${this.formatAIInline(trimmed.replace(/^-\s+/, ''))}</span></div>`
}
if (/^\*\*(.+?)\*\*$/.test(trimmed)) {
return `<div class="ai-chat__section-title">${this.formatAIInline(trimmed)}</div>`
}
return `<div class="ai-chat__paragraph">${this.formatAIInline(trimmed)}</div>`
}).join('')
},
scrollAIChatToBottom() {
const messageBox = this.$refs.aiChatMessages
if (messageBox) messageBox.scrollTop = messageBox.scrollHeight
}
}
})
</script>
<style scoped lang="less">
.top-nav,
.top-nav-placeholder,
.nav-container {
background: transparent !important;
background-color: transparent !important;
background-image: none !important;
box-shadow: none !important;
}
.top-nav {
position: relative;
height: 0;
width: 100%;
min-width: 1200px;
color: #111827;
font-family: PingFang SC, Microsoft YaHei, Arial, sans-serif;
box-sizing: border-box;
}
.top-nav-placeholder {
display: block;
height: 0;
width: 100%;
flex-shrink: 0;
}
.nav-container {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
width: 100%;
height: 82px;
padding: 0 42px;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
}
.brand {
display: inline-flex;
align-items: center;
min-width: 220px;
cursor: pointer;
}
.brand-logo {
width: 184px;
max-height: 52px;
object-fit: contain;
object-position: left center;
display: block;
}
.site-nav {
display: flex;
align-items: center;
gap: 38px;
margin-left: auto;
margin-right: 32px;
position: relative;
z-index: 2;
}
.nav-item {
padding: 0;
border: 0;
background: transparent;
color: #111827;
cursor: pointer;
font-size: 17px !important;
line-height: 82px;
font-weight: 600;
white-space: nowrap;
transition: color 0.2s ease;
&:hover,
&.active {
color: #1e6fff;
}
}
.nav-item--product {
display: inline-flex;
align-items: center;
gap: 6px;
.iconfont {
font-size: 13px !important;
line-height: 1;
}
}
.nav-product-wrap {
position: relative;
height: 82px;
display: inline-flex;
align-items: center;
}
.nav-lang-text {
color: #111827;
display: inline-flex;
align-items: center;
justify-content: center;
border: 0;
background: transparent;
font-size: 16px !important;
font-weight: 600;
padding: 6px 12px;
cursor: pointer;
user-select: none;
transition: color 0.2s ease;
border-radius: 4px;
&:hover {
color: #1e6fff;
background: rgba(30, 111, 255, 0.08);
}
}
.nav-lang-text.is-en {
color: #1e6fff;
}
.user-actions {
display: flex;
align-items: center;
gap: 16px;
position: relative;
z-index: 2;
}
.functions {
color: #000;
cursor: pointer;
font-size: 20px !important;
line-height: 1;
transition: color 0.2s ease;
&:hover {
color: #1e6fff;
}
}
.console-link {
height: 36px;
padding: 0;
border: 0;
background: transparent;
color: #333;
cursor: pointer;
font-size: 15px !important;
font-weight: 600;
line-height: 36px;
transition: color 0.2s ease;
&:hover {
color: #1e6fff;
}
}
.login-btn {
height: 48px;
width: 86px;
padding: 0;
border: 0;
border-radius: 99px;
color: #fff;
background-color: #000;
cursor: pointer;
font-size: 16px !important;
font-weight: 700;
}
.login-pill {
min-width: 58px;
height: 36px;
padding: 0 20px;
border: 0;
border-radius: 999px;
background: #050505;
color: #fff;
cursor: pointer;
font-size: 15px !important;
font-weight: 600;
line-height: 36px;
&:hover {
opacity: 0.84;
}
}
.login-pill--user {
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-panel {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
z-index: 1001;
display: flex;
}
.product-panel--side-only {
width: 210px;
}
.product-panel__side {
width: 210px;
padding: 20px 16px;
background: #fff;
border-bottom-left-radius: 12px;
border-bottom-right-radius: 12px;
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: 6px;
}
.product-side-item {
width: 100%;
height: 42px;
padding: 0 16px;
display: flex;
align-items: center;
border: 0;
border-radius: 12px;
background: transparent;
color: #374151;
cursor: pointer;
font-size: 14px !important;
text-align: left;
transition: all 0.2s ease;
&:hover {
background: #e8f1ff;
color: #1e6fff;
font-weight: 600;
}
}
.user-dropdown-head {
display: flex;
flex-direction: column;
padding: 15px;
background-color: #f0f2f5;
}
.user-dropdown-name {
font-weight: bold;
font-size: 20px;
margin-bottom: 5px;
color: #333333;
}
.user-dropdown-id {
font-size: 12px;
color: #666;
display: flex;
justify-content: flex-start;
align-items: center;
}
.copy-btn {
margin-left: 6px;
cursor: pointer;
}
.user-dropdown-balance {
padding: 0 18px;
width: 100%;
height: 60px;
display: flex;
justify-content: space-between;
align-items: center;
}
.moneyNow-label {
display: block;
margin-bottom: 5px;
}
.ai-chat-panel {
position: fixed;
right: 24px;
bottom: 24px;
width: 380px;
height: 620px;
background: #fff;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
z-index: 3000;
display: flex;
flex-direction: column;
overflow: hidden;
&__header {
height: 64px;
padding: 0 18px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f0f0f0;
background: #fff;
cursor: move;
}
&__title {
display: flex;
align-items: center;
gap: 10px;
font-size: 16px !important;
font-weight: 600;
color: #222;
}
&__logo {
width: 28px;
height: 28px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(90deg, #d66bff 0%, #7d7cff 100%);
color: #fff;
font-size: 12px !important;
}
&__close {
cursor: pointer;
color: #666;
font-size: 18px !important;
}
&__body {
flex: 1;
overflow-y: auto;
padding: 18px 16px;
background: #fff;
}
&__welcome h3 {
margin: 0 0 12px;
color: #5f59ff;
font-size: 22px !important;
font-weight: 700;
}
&__welcome p {
margin: 0 0 20px;
font-size: 14px !important;
line-height: 1.8;
color: #333;
}
&__suggestions {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
&__tag {
display: inline-flex;
align-items: center;
padding: 10px 14px;
border-radius: 10px;
background: #f7f7f8;
color: #333;
font-size: 14px !important;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
color: #1e6fff;
background: #eef5ff;
}
}
&__input-wrap {
border-top: 1px solid #f0f0f0;
padding: 12px 14px 14px;
background: #fff;
}
&__actions {
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
}
&__new {
font-size: 13px !important;
color: #666;
cursor: pointer;
}
&__new:hover {
color: #1e6fff;
}
&__input {
position: relative;
}
&__send {
position: absolute;
right: 8px;
bottom: 8px;
padding: 0;
color: #7d7cff;
}
}
::v-deep .ai-chat-panel__input .el-textarea__inner {
min-height: 84px !important;
padding: 12px 48px 12px 12px;
border-radius: 14px;
border: 1px solid #e5eaf3;
font-size: 14px;
line-height: 1.6;
}
.ai-chat__message {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.ai-chat__message.is-user {
flex-direction: row-reverse;
}
.ai-chat__message.is-user .ai-chat__bubble {
background: linear-gradient(90deg, #275aff 0%, #2ebdfa 100%);
color: #fff;
}
.ai-chat__avatar {
flex-shrink: 0;
width: 34px;
height: 34px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px !important;
color: #1e6fff;
background: rgba(30, 111, 255, 0.12);
}
.ai-chat__bubble {
max-width: calc(100% - 46px);
padding: 12px 14px;
border-radius: 12px;
background: #f7f9fc;
color: #333;
font-size: 14px !important;
line-height: 1.7;
white-space: pre-wrap;
word-break: break-word;
}
.ai-chat__rich-text {
font-size: 14px !important;
line-height: 1.7;
}
.ai-chat__paragraph {
margin-bottom: 8px;
}
.ai-chat__paragraph--space {
height: 8px;
margin-bottom: 0;
}
.ai-chat__section-title {
margin: 12px 0 8px;
font-weight: 700;
color: #222f60;
}
.ai-chat__item {
display: flex;
align-items: flex-start;
gap: 8px;
margin-bottom: 8px;
}
.ai-chat__item-dot {
width: 6px;
height: 6px;
margin-top: 9px;
border-radius: 50%;
background: #7d7cff;
flex-shrink: 0;
}
.ai-chat__tip {
color: #999;
font-size: 12px !important;
}
::v-deep .ai-chat__rich-text strong {
font-weight: 700;
color: #222f60;
}
</style>