更新ai广场bug
This commit is contained in:
@@ -42,13 +42,37 @@
|
||||
:value="m"
|
||||
/>
|
||||
</el-select>
|
||||
<el-tooltip :content="mcpTip" placement="bottom">
|
||||
<span class="mcp-indicator" :class="{ 'has-mcp': activeMcpTools.length > 0 }">
|
||||
<el-icon><Cpu /></el-icon>
|
||||
<span class="mcp-ind-text">MCP</span>
|
||||
<span v-if="activeMcpTools.length > 0" class="mcp-ind-count">{{ activeMcpTools.length }}</span>
|
||||
</span>
|
||||
</el-tooltip>
|
||||
<el-popover placement="bottom-end" :width="300" trigger="click" @show="fetchMcpStatus">
|
||||
<template #reference>
|
||||
<span class="mcp-indicator" :class="{ 'has-mcp': mcpOn }">
|
||||
<el-icon><Cpu /></el-icon>
|
||||
<span class="mcp-ind-text">MCP</span>
|
||||
<span v-if="mcpOn" class="mcp-ind-count">{{ selectedMcpServers.length }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<div class="mcp-panel">
|
||||
<div class="mcp-panel-head">
|
||||
<span class="mcp-panel-title">MCP 工具</span>
|
||||
<el-switch v-model="mcpEnabled" size="small" @change="persistMcpSetting" />
|
||||
</div>
|
||||
<div class="mcp-panel-tip">{{ mcpTip }}</div>
|
||||
<template v-if="mcpEnabled">
|
||||
<div v-if="mcpServerList.length === 0" class="mcp-panel-empty">
|
||||
暂无已启用的 MCP 服务,可在侧边栏 CPU 按钮中配置
|
||||
</div>
|
||||
<el-checkbox-group
|
||||
v-else
|
||||
v-model="selectedMcpServers"
|
||||
class="mcp-check-list"
|
||||
@change="persistMcpSetting"
|
||||
>
|
||||
<el-checkbox v-for="s in mcpServerList" :key="s.id" :value="s.id">
|
||||
{{ s.name }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</template>
|
||||
</div>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -92,13 +116,16 @@
|
||||
class="message-bubble"
|
||||
:class="{
|
||||
typing: msg.role === 'assistant' && !msg.content && sending,
|
||||
'has-tools': msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length
|
||||
'has-tools': msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length,
|
||||
generating: msg.role === 'assistant' && sending,
|
||||
error: msg.role === 'assistant' && msg.error
|
||||
}"
|
||||
>
|
||||
<div v-if="msg.role === 'assistant' && sending" class="generating-indicator">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
</div>
|
||||
<template v-if="msg.role === 'assistant' && !msg.content && sending">
|
||||
<span class="typing-dot"></span>
|
||||
<span class="typing-dot"></span>
|
||||
<span class="typing-dot"></span>
|
||||
<span class="thinking-text">思考中…</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length" class="tool-call-list">
|
||||
@@ -129,6 +156,16 @@
|
||||
<pre v-if="msg.content" class="message-text">{{ msg.content }}</pre>
|
||||
</template>
|
||||
</div>
|
||||
<div
|
||||
v-if="msg.role === 'assistant' && (msg.tokens || msg.durationMs)"
|
||||
class="message-meta"
|
||||
>
|
||||
<span class="meta-item">{{ formatDuration(msg.durationMs) }}</span>
|
||||
<span class="meta-dot">·</span>
|
||||
<span class="meta-item">{{ msg.tokens }} tokens</span>
|
||||
<span v-if="msg.model" class="meta-dot">·</span>
|
||||
<span v-if="msg.model" class="meta-item">{{ msg.model }}</span>
|
||||
</div>
|
||||
<div class="message-actions">
|
||||
<el-tooltip content="复制" placement="top">
|
||||
<el-button link type="primary" :icon="CopyDocument" class="action-btn" @click="copyMessage(msg)" />
|
||||
@@ -196,6 +233,10 @@ const sending = ref(false)
|
||||
const inputText = ref('')
|
||||
const messages = ref([])
|
||||
const messagesRef = ref()
|
||||
// 流式回复进行中时 sessionId 变化的暂存值(等待本轮结束后再加载历史)
|
||||
const pendingSessionId = ref(null)
|
||||
// 标记「本次 sessionId 变化是本轮发送自己创建的会话」,避免拉取历史覆盖正在流式追加的消息
|
||||
const selfSessionSwitch = ref(false)
|
||||
|
||||
// 消息编辑
|
||||
const editingId = ref(-1)
|
||||
@@ -213,14 +254,56 @@ const defaultPreset = ref(null)
|
||||
|
||||
// MCP 接入状态
|
||||
const activeMcpTools = ref([])
|
||||
// 本轮对话是否启用 MCP 工具(本地记忆上次选择)
|
||||
const savedMcp = loadMcpSetting()
|
||||
const mcpEnabled = ref(savedMcp.enabled)
|
||||
// 参与本轮对话的 MCP 服务器(默认全部已启用的服务)
|
||||
const selectedMcpServers = ref(savedMcp.serverIds)
|
||||
const mcpServerList = ref([])
|
||||
// 开关打开且至少勾选了一个服务才算真正启用
|
||||
const mcpOn = computed(() => mcpEnabled.value && selectedMcpServers.value.length > 0)
|
||||
const mcpTip = computed(() => {
|
||||
if (activeMcpTools.value.length === 0) {
|
||||
return '当前未启用 MCP 工具,可在侧边栏 CPU 按钮中配置'
|
||||
if (!mcpEnabled.value) {
|
||||
return 'MCP 工具已关闭,本轮对话不会调用任何 MCP 工具'
|
||||
}
|
||||
const servers = [...new Set(activeMcpTools.value.map((t) => t.server_name).filter(Boolean))]
|
||||
return `已接入 ${activeMcpTools.value.length} 个 MCP 工具(${servers.join('、')}),会话中 AI 会自动调用`
|
||||
if (selectedMcpServers.value.length === 0) {
|
||||
return '未勾选任何 MCP 服务,本轮对话不会调用工具'
|
||||
}
|
||||
const names = mcpServerList.value
|
||||
.filter((s) => selectedMcpServers.value.includes(s.id))
|
||||
.map((s) => s.name)
|
||||
return `本轮将调用:${names.join('、')}`
|
||||
})
|
||||
|
||||
function loadMcpSetting() {
|
||||
const setting = { enabled: true, serverIds: [] }
|
||||
try {
|
||||
const e = localStorage.getItem('ai_mcp_enabled')
|
||||
if (e !== null) setting.enabled = e === '1'
|
||||
const s = localStorage.getItem('ai_mcp_servers')
|
||||
if (s) setting.serverIds = JSON.parse(s) || []
|
||||
} catch (e) {
|
||||
// 忽略
|
||||
}
|
||||
return setting
|
||||
}
|
||||
|
||||
function persistMcpSetting() {
|
||||
try {
|
||||
localStorage.setItem('ai_mcp_enabled', mcpEnabled.value ? '1' : '0')
|
||||
localStorage.setItem('ai_mcp_servers', JSON.stringify(selectedMcpServers.value))
|
||||
} catch (e) {
|
||||
// 忽略
|
||||
}
|
||||
}
|
||||
|
||||
// 将毫秒格式化为可读耗时
|
||||
function formatDuration(ms) {
|
||||
if (!ms || ms <= 0) return '0ms'
|
||||
if (ms < 1000) return `${ms}ms`
|
||||
return `${(ms / 1000).toFixed(1)}s`
|
||||
}
|
||||
|
||||
const currentModels = computed(() => {
|
||||
const p = providerList.value.find(item => item.id === selectedProviderId.value)
|
||||
return p && p.models ? p.models : []
|
||||
@@ -237,11 +320,10 @@ async function fetchMcpStatus() {
|
||||
const res = await getMcpServerList()
|
||||
if (res.code === 200 && res.data && res.data.list) {
|
||||
const enabled = res.data.list.filter((s) => s.enabled === 1)
|
||||
const tools = []
|
||||
enabled.forEach((s) => {
|
||||
tools.push({ key: '', server_id: s.id, server_name: s.name, tool_name: '' })
|
||||
})
|
||||
activeMcpTools.value = tools
|
||||
mcpServerList.value = enabled.map((s) => ({ id: s.id, name: s.name }))
|
||||
// 过滤掉已删除/已停用的服务;没有历史选择时默认全选
|
||||
const valid = selectedMcpServers.value.filter((id) => mcpServerList.value.some((s) => s.id === id))
|
||||
selectedMcpServers.value = valid.length > 0 ? valid : mcpServerList.value.map((s) => s.id)
|
||||
}
|
||||
} catch (e) {
|
||||
// 静默失败
|
||||
@@ -295,6 +377,15 @@ const quickTips = [
|
||||
watch(
|
||||
() => props.sessionId,
|
||||
(val) => {
|
||||
if (sending.value) {
|
||||
// 流式回复期间不能重新拉取历史:会把本地正在追加的 assistant 消息整体替换掉,导致回复不显示
|
||||
if (selfSessionSwitch.value) {
|
||||
selfSessionSwitch.value = false
|
||||
return
|
||||
}
|
||||
pendingSessionId.value = val
|
||||
return
|
||||
}
|
||||
if (val) {
|
||||
fetchMessages(val)
|
||||
} else {
|
||||
@@ -452,9 +543,12 @@ async function doSend(content) {
|
||||
|
||||
// 添加用户消息
|
||||
messages.value.push({ role: 'user', content })
|
||||
// 注意:必须取数组内的响应式代理对象来修改,直接持有 push 进去的原始对象不会触发视图更新
|
||||
const userMsg = messages.value[messages.value.length - 1]
|
||||
// 添加空的AI消息,用于流式更新
|
||||
const aiMsg = { id: null, role: 'assistant', content: '', toolCalls: [] }
|
||||
messages.value.push(aiMsg)
|
||||
messages.value.push({ id: null, role: 'assistant', content: '', toolCalls: [], tokens: 0, durationMs: 0, model: '', error: false })
|
||||
const aiIndex = messages.value.length - 1
|
||||
const aiMsg = messages.value[aiIndex]
|
||||
sending.value = true
|
||||
scrollToBottom()
|
||||
|
||||
@@ -471,7 +565,9 @@ async function doSend(content) {
|
||||
session_id: props.sessionId || 0,
|
||||
content,
|
||||
provider_id: selectedProviderId.value,
|
||||
model: selectedModel.value
|
||||
model: selectedModel.value,
|
||||
mcp_enabled: mcpOn.value,
|
||||
mcp_server_ids: mcpOn.value ? selectedMcpServers.value : []
|
||||
})
|
||||
})
|
||||
|
||||
@@ -485,14 +581,19 @@ async function doSend(content) {
|
||||
let sessionCreated = false
|
||||
let currentEvent = ''
|
||||
let currentData = ''
|
||||
let streamError = null
|
||||
|
||||
const handleSSEEvent = (eventType, data) => {
|
||||
if (!data) return
|
||||
try {
|
||||
const parsed = JSON.parse(data)
|
||||
if (eventType === 'session') {
|
||||
if (parsed.user_message_id) {
|
||||
userMsg.id = parsed.user_message_id
|
||||
}
|
||||
if (!props.sessionId && parsed.session_id && !sessionCreated) {
|
||||
sessionCreated = true
|
||||
selfSessionSwitch.value = true
|
||||
emit('session-created', {
|
||||
id: parsed.session_id,
|
||||
title: content.length > 20 ? content.slice(0, 20) + '...' : content
|
||||
@@ -532,11 +633,13 @@ async function doSend(content) {
|
||||
scrollToBottom()
|
||||
} else if (eventType === 'done') {
|
||||
aiMsg.id = parsed.message_id
|
||||
if (parsed.tokens) aiMsg.tokens = parsed.tokens
|
||||
if (parsed.duration_ms) aiMsg.durationMs = parsed.duration_ms
|
||||
if (parsed.model) aiMsg.model = parsed.model
|
||||
} else if (eventType === 'error') {
|
||||
throw new Error(parsed.error || 'AI调用失败')
|
||||
streamError = new Error(parsed.error || 'AI调用失败')
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.message === 'AI调用失败') throw e
|
||||
// JSON解析错误忽略
|
||||
}
|
||||
}
|
||||
@@ -569,15 +672,28 @@ async function doSend(content) {
|
||||
if (currentData) {
|
||||
handleSSEEvent(currentEvent, currentData)
|
||||
}
|
||||
if (streamError) {
|
||||
throw streamError
|
||||
}
|
||||
} catch (e) {
|
||||
ElMessage.error(e.message || '发送失败')
|
||||
// 移除空的AI消息
|
||||
if (aiMsg.content === '') {
|
||||
const idx = messages.value.indexOf(aiMsg)
|
||||
if (idx >= 0) messages.value.splice(idx, 1)
|
||||
// 将错误以对话形式展示,不弹出顶部提示
|
||||
const errText = e.message || '发送失败,请稍后重试'
|
||||
if (messages.value[aiIndex] === aiMsg) {
|
||||
if (!aiMsg.content) {
|
||||
aiMsg.content = errText
|
||||
} else {
|
||||
aiMsg.content += '\n\n⚠️ ' + errText
|
||||
}
|
||||
aiMsg.error = true
|
||||
}
|
||||
} finally {
|
||||
sending.value = false
|
||||
// 流式期间被挂起的会话切换,此时再加载历史
|
||||
if (pendingSessionId.value) {
|
||||
const id = pendingSessionId.value
|
||||
pendingSessionId.value = null
|
||||
await fetchMessages(id)
|
||||
}
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
@@ -733,6 +849,46 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
// 助手回复下方的响应信息(耗时 / tokens / 模型)
|
||||
.message-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.meta-dot {
|
||||
color: var(--el-border-color);
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
// 模型运行中的加载指示(流式输出期间显示)
|
||||
.generating-indicator {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
color: var(--el-color-primary);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.thinking-text {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
// 助手消息报错(对话内错误展示)
|
||||
.message-bubble.error {
|
||||
background: var(--el-color-danger-light-9);
|
||||
border: 1px solid var(--el-color-danger-light-5);
|
||||
color: var(--el-color-danger);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.edit-area {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
@@ -850,8 +1006,14 @@ defineExpose({
|
||||
border: 1px solid var(--el-border-color);
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
cursor: default;
|
||||
cursor: pointer;
|
||||
height: 32px;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.has-mcp {
|
||||
background: var(--el-color-success-light-9);
|
||||
@@ -871,6 +1033,43 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
// MCP 开关面板
|
||||
.mcp-panel {
|
||||
.mcp-panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.mcp-panel-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mcp-panel-tip {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 10px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.mcp-panel-empty {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-placeholder);
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
||||
.mcp-check-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
// 工具调用卡片
|
||||
.tool-call-list {
|
||||
display: flex;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div>
|
||||
<h2>文档图谱</h2>
|
||||
<p>
|
||||
默认展示顶级往下 3 级分类脉络,点击带 ▸ 的分类节点可继续延展 3 级;滚轮缩放,按住空格 + 左键拖拽平移
|
||||
默认展示顶级往下 3 级分类脉络,点击带 ▸ 的分类节点可继续延展 3 级;滚轮缩放,按住右键拖拽平移画布
|
||||
</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
@@ -66,7 +66,12 @@
|
||||
</div>
|
||||
|
||||
<div v-loading="loading" class="chart-wrap">
|
||||
<div ref="chartRef" class="chart" />
|
||||
<div
|
||||
ref="chartRef"
|
||||
class="chart"
|
||||
@contextmenu.prevent
|
||||
@mousedown="onChartMouseDown"
|
||||
/>
|
||||
<el-empty
|
||||
v-if="!loading && !displayNodes.length"
|
||||
description="暂无可展示的分类或文档,请先到文档库新建分类并上传文档"
|
||||
@@ -97,7 +102,8 @@
|
||||
<i class="line dotted" />分类层级
|
||||
</span>
|
||||
<span class="legend-tip"
|
||||
>滚轮缩放 · 空格+左键拖拽平移 · 点击 ▸ 分类展开下级(更深层级循环使用这 3 色)</span
|
||||
>滚轮缩放 · 右键拖拽平移 · 左键拖拽节点 · 点击 ▸ 分类展开下级(更深层级循环使用这 3
|
||||
色)</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,8 +216,8 @@ const detailId = ref(0);
|
||||
// 分层展开:默认展示顶级往下 3 级,点击分支节点后在该分支继续延展 3 级
|
||||
const INITIAL_DEPTH = 3;
|
||||
const expandedIds = ref(new Set());
|
||||
// 按住空格 + 鼠标左键拖拽平移
|
||||
const spaceHeld = ref(false);
|
||||
// 按住右键拖拽平移画布(ECharts 原生 roam 只响应左键,右键需自行接管)
|
||||
const panning = ref(false);
|
||||
|
||||
const filters = reactive({
|
||||
keyword: "",
|
||||
@@ -455,10 +461,10 @@ function buildOption() {
|
||||
{
|
||||
type: "graph",
|
||||
layout: filters.layout,
|
||||
// roam: 滚轮缩放 + 拖拽平移;按住空格时禁用节点拖拽,让左键拖拽整体平移
|
||||
// roam: 滚轮缩放 + 左键在空白处拖拽平移;节点左键可拖拽,右键拖拽整体平移
|
||||
roam: true,
|
||||
scaleLimit: { min: 0.15, max: 6 },
|
||||
draggable: !spaceHeld.value,
|
||||
draggable: true,
|
||||
data,
|
||||
links: edgeList,
|
||||
categories: [
|
||||
@@ -540,39 +546,50 @@ function openNode(node) {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- 空格 + 鼠标左键拖拽平移 ----------------
|
||||
function isTypingTarget(el) {
|
||||
return (
|
||||
!!el &&
|
||||
(el.tagName === "INPUT" ||
|
||||
el.tagName === "TEXTAREA" ||
|
||||
el.isContentEditable === true)
|
||||
);
|
||||
}
|
||||
// ---------------- 按住右键拖拽平移 ----------------
|
||||
// ECharts 的 roam 会忽略右键(isMiddleOrRightButtonOnMouseUpDown),
|
||||
// 这里自行接管右键拖拽,通过 graphRoam 动作平移,不改动节点数据与布局。
|
||||
let panLastX = 0;
|
||||
let panLastY = 0;
|
||||
|
||||
function applyPanMode(on) {
|
||||
if (chartRef.value) chartRef.value.style.cursor = on ? "grab" : "";
|
||||
try {
|
||||
// 空格按下时禁用节点拖拽,使左键拖拽变为整体平移
|
||||
chart?.setOption({ series: [{ draggable: !on }] });
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
function onSpaceDown(e) {
|
||||
if (e.code !== "Space") return;
|
||||
if (isTypingTarget(e.target)) return;
|
||||
function onChartMouseDown(e) {
|
||||
if (e.button !== 2) return; // 仅响应右键
|
||||
e.preventDefault();
|
||||
if (spaceHeld.value) return;
|
||||
spaceHeld.value = true;
|
||||
applyPanMode(true);
|
||||
panning.value = true;
|
||||
panLastX = e.clientX;
|
||||
panLastY = e.clientY;
|
||||
chartRef.value?.classList.add("is-panning");
|
||||
window.addEventListener("mousemove", onPanMouseMove);
|
||||
window.addEventListener("mouseup", onPanMouseUp);
|
||||
}
|
||||
|
||||
function onSpaceUp(e) {
|
||||
if (e.code !== "Space") return;
|
||||
spaceHeld.value = false;
|
||||
applyPanMode(false);
|
||||
function onPanMouseMove(e) {
|
||||
if (!panning.value) return;
|
||||
// 右键在画布外松开时 mouseup 不会触发,用 buttons 兜底结束拖拽
|
||||
if ((e.buttons & 2) === 0) {
|
||||
stopPan();
|
||||
return;
|
||||
}
|
||||
const dx = e.clientX - panLastX;
|
||||
const dy = e.clientY - panLastY;
|
||||
panLastX = e.clientX;
|
||||
panLastY = e.clientY;
|
||||
if (!dx && !dy) return;
|
||||
e.preventDefault();
|
||||
chart?.dispatchAction({ type: "graphRoam", seriesIndex: 0, dx, dy });
|
||||
}
|
||||
|
||||
function onPanMouseUp(e) {
|
||||
if (e.button !== 2) return;
|
||||
stopPan();
|
||||
}
|
||||
|
||||
function stopPan() {
|
||||
if (!panning.value) return;
|
||||
panning.value = false;
|
||||
chartRef.value?.classList.remove("is-panning");
|
||||
window.removeEventListener("mousemove", onPanMouseMove);
|
||||
window.removeEventListener("mouseup", onPanMouseUp);
|
||||
}
|
||||
|
||||
async function loadCategories() {
|
||||
@@ -677,14 +694,10 @@ onMounted(async () => {
|
||||
attributes: true,
|
||||
attributeFilter: ["class"]
|
||||
});
|
||||
// 空格 + 左键拖拽平移
|
||||
window.addEventListener("keydown", onSpaceDown);
|
||||
window.addEventListener("keyup", onSpaceUp);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener("keydown", onSpaceDown);
|
||||
window.removeEventListener("keyup", onSpaceUp);
|
||||
stopPan();
|
||||
resizeObserver?.disconnect();
|
||||
themeObserver?.disconnect();
|
||||
chart?.dispose();
|
||||
@@ -752,6 +765,12 @@ onBeforeUnmount(() => {
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// 右键拖拽平移时强制抓手光标(zrender 会改写 canvas 内联 cursor)
|
||||
&.is-panning,
|
||||
&.is-panning * {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-empty {
|
||||
|
||||
Reference in New Issue
Block a user