1347 lines
38 KiB
Vue
1347 lines
38 KiB
Vue
<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, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
},
|
||
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>
|