feat(ctrt): 完成 CTRT-01~03 响应契约统一 - 三端拦截器+portal_token清理+调用点适配
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# =============================================================================
|
||||
# 企微IT智能服务台 — 坐席前端开发版 Docker 镜像
|
||||
# =============================================================================
|
||||
# 说明:基于 node:20 开发模式,支持代码热更新(volume mount 源码)
|
||||
# 用途:本地开发,代码修改自动生效
|
||||
# =============================================================================
|
||||
FROM node:20-slim
|
||||
|
||||
# 安装 pnpm
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制依赖文件
|
||||
COPY package.json pnpm-lock.yaml* ./
|
||||
|
||||
# 安装依赖
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# 复制源码(后续通过 volume mount 更新)
|
||||
COPY . .
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 5173
|
||||
|
||||
# 启动开发服务器(热更新)
|
||||
CMD ["pnpm", "dev", "--host"]
|
||||
Binary file not shown.
@@ -5,9 +5,11 @@
|
||||
<!-- 移动端视口设置 -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- CSP 安全策略 -->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-eval' https://res.wx.qq.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https: http:; connect-src 'self' https://qyapi.weixin.qq.com wss://*;" />
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-eval' https://res.wx.qq.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: https: http:; connect-src 'self' https://qyapi.weixin.qq.com wss://* ws://localhost ws://127.0.0.1 ws://localhost:8000 ws://127.0.0.1:8000;" />
|
||||
<!-- 页面标题 -->
|
||||
<title>智能IT支持服务台 - 坐席工作台</title>
|
||||
<!-- 企微 JS-SDK (用于企微客户端检测和快捷登录) -->
|
||||
<script src="https://res.wx.qq.com/wwopen/js/jwxwork-1.0.0.js"></script>
|
||||
<!-- ElementPlus 图标 -->
|
||||
<link rel="icon" type="image/svg+xml" href="/itagent/vite.svg" />
|
||||
</head>
|
||||
|
||||
Generated
+1200
File diff suppressed because it is too large
Load Diff
@@ -3,10 +3,13 @@
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"description": "企微智能IT支持服务台 - 坐席工作台前端",
|
||||
"engines": { "node": ">=20.0.0 <21.0.0", "pnpm": ">=9.0.0" },
|
||||
"engines": {
|
||||
"node": ">=20.0.0 <21.0.0",
|
||||
"pnpm": ">=9.0.0"
|
||||
},
|
||||
"packageManager": "pnpm@9.15.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "node --max-old-space-size=4096 ./node_modules/vite/bin/vite.js",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"type-check": "vue-tsc --noEmit"
|
||||
@@ -14,9 +17,13 @@
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.0",
|
||||
"axios": "^1.7.0",
|
||||
"cropperjs": "^2.1.1",
|
||||
"element-plus": "^2.7.0",
|
||||
"fabric": "^7.4.0",
|
||||
"html2canvas-pro": "^2.0.4",
|
||||
"js-web-screen-shot": "^2.0.2",
|
||||
"pinia": "^2.1.0",
|
||||
"snapdom": "^0.1.2",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.3.0",
|
||||
"vue3-emoji-picker": "^1.1.8"
|
||||
|
||||
@@ -120,9 +120,8 @@ async function handleAuthExpired(source: 'http401' | 'biz1002'): Promise<void> {
|
||||
// 第二步:刷新失败,清除凭证并跳转登录
|
||||
console.warn('[API] Token 刷新失败,清除凭证并跳转登录')
|
||||
|
||||
// 清除本地 token
|
||||
// 清除本地 token(C8:移除 portal_token 遗留)
|
||||
localStorage.removeItem(TOKEN_KEY)
|
||||
localStorage.removeItem('portal_token')
|
||||
|
||||
// 跳转登录页
|
||||
ElMessage.warning('登录已过期,请重新登录')
|
||||
|
||||
@@ -82,12 +82,12 @@ export function useWebSocket() {
|
||||
let agentId = agentStore.userId
|
||||
let token = agentStore.token
|
||||
|
||||
// 如果 store 中没有,从 localStorage 读取(解决 QR 码登录时 store 未及时刷新的问题)
|
||||
// 如果 store 中没有,从 localStorage 读取(C8:移除 portal_token)
|
||||
if (!agentId) {
|
||||
agentId = localStorage.getItem('agent_user_id') || ''
|
||||
}
|
||||
if (!token) {
|
||||
token = localStorage.getItem('agent_token') || localStorage.getItem('portal_token') || ''
|
||||
token = localStorage.getItem('agent_token') || ''
|
||||
}
|
||||
|
||||
// 调试日志
|
||||
|
||||
@@ -50,6 +50,13 @@ const routes = [
|
||||
// 这里依靠守卫检查 token,但不强制 MFA 已绑定(否则永远进不去)
|
||||
meta: { title: '绑定 MFA', requiresAuth: true },
|
||||
},
|
||||
// 阶段5 自动化闭环 — 坐席端会话工作台
|
||||
{
|
||||
path: '/automation/:id',
|
||||
name: 'AutomationSession',
|
||||
component: () => import('@/views/automation/SessionWorkbench.vue'),
|
||||
meta: { title: '自动化会话', requiresAuth: true },
|
||||
},
|
||||
]
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -73,15 +80,13 @@ router.beforeEach((to, _from, next) => {
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
// Portal Token 传递:从 URL 参数 ?token=xxx 读取并保存到 localStorage
|
||||
// Token 传递:从 URL 参数 ?token=xxx 读取并保存到 localStorage(C8:移除 portal_token)
|
||||
// ========================================================================
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const urlToken = urlParams.get('token')
|
||||
if (urlToken) {
|
||||
// 保存 token 到坐席端 localStorage key
|
||||
// 保存 token 到坐席端 localStorage
|
||||
localStorage.setItem('agent_token', urlToken)
|
||||
// 同时保存到 portal_token key(方便跨端共享)
|
||||
localStorage.setItem('portal_token', urlToken)
|
||||
// 清除 URL 参数,避免刷新页面重复读取
|
||||
const cleanUrl = window.location.pathname
|
||||
window.history.replaceState({}, '', cleanUrl)
|
||||
|
||||
@@ -18,10 +18,9 @@ import router from '@/router'
|
||||
import { useWebSocket } from '@/composables/useWebSocket'
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// Token 存储 key
|
||||
// Token 存储 key(C8:移除 portal_token)
|
||||
// --------------------------------------------------------------------------
|
||||
const TOKEN_KEY = 'agent_token'
|
||||
const PORTAL_TOKEN_KEY = 'portal_token'
|
||||
const AGENT_USER_ID_KEY = 'agent_user_id'
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
@@ -35,8 +34,8 @@ export const useAgentStore = defineStore('agent', () => {
|
||||
/** 当前登录的坐席信息 */
|
||||
const agentInfo = ref<Agent | null>(null)
|
||||
|
||||
/** 认证 token — 优先从 agent_token 读取,降级读取 portal_token */
|
||||
const token = ref<string | null>(localStorage.getItem(TOKEN_KEY) || localStorage.getItem(PORTAL_TOKEN_KEY))
|
||||
/** 认证 token — 从 agent_token 读取(C8:移除 portal_token) */
|
||||
const token = ref<string | null>(localStorage.getItem(TOKEN_KEY))
|
||||
|
||||
/** 坐席用户ID */
|
||||
const agentUserId = ref<string | null>(localStorage.getItem(AGENT_USER_ID_KEY))
|
||||
|
||||
@@ -21,14 +21,10 @@ import {
|
||||
} from '@/api/automation'
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// WebSocket 辅助
|
||||
// WebSocket 辅助(C8:移除 portal_token)
|
||||
// --------------------------------------------------------------------------
|
||||
function getAgentToken(): string {
|
||||
return (
|
||||
localStorage.getItem('agent_token') ||
|
||||
localStorage.getItem('portal_token') ||
|
||||
''
|
||||
)
|
||||
return localStorage.getItem('agent_token') || ''
|
||||
}
|
||||
|
||||
function buildWsUrl(sessionId: string): string {
|
||||
|
||||
@@ -187,12 +187,11 @@ function onRightResizeEnd(): void {
|
||||
|
||||
onMounted(async () => {
|
||||
// 修复 v0.5.1: 企微点坐席直接打开 /itagent/ 时,URL 没 ?token=
|
||||
// 路由守卫虽然会跳到 /itportal/,但在这之前 axios 已经发了请求 → 弹 401
|
||||
// 这里在 onMounted 第一行主动检查 token,没 token 立刻跳 portal,避免 401 弹错
|
||||
// 路由守卫虽然会跳到 /login,但在这之前 axios 已经发了请求 → 弹 401
|
||||
// 这里在 onMounted 第一行主动检查 token,没 token 立刻跳登录页,避免 401 弹错(C8:移除 portal_token)
|
||||
const hasAgentToken = localStorage.getItem('agent_token')
|
||||
const hasPortalToken = localStorage.getItem('portal_token')
|
||||
if (!hasAgentToken && !hasPortalToken) {
|
||||
window.location.href = '/itportal/'
|
||||
if (!hasAgentToken) {
|
||||
window.location.href = '/login'
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
<!-- =============================================================================
|
||||
// 企微IT智能服务台 — 阶段5 自动化会话工作台(坐席端)
|
||||
// =============================================================================
|
||||
// 说明:坐席查看某个自动化会话的实时进展,对待审批高危/写动作进行
|
||||
// 通过/驳回,并可对会话执行「转人工接管」。
|
||||
// 复用已落地的 @/stores/automation(含专用 WebSocket 实时推送)。
|
||||
// 约定:与 ActionApprovalCard / TakeoverPanel 配套,使用 Element Plus。
|
||||
// ============================================================================= -->
|
||||
<template>
|
||||
<div class="session-workbench">
|
||||
<!-- 顶部栏:返回 / 标题 / 实时连接状态 -->
|
||||
<div class="wb-header">
|
||||
<el-button text :icon="ArrowLeft" @click="goBack">返回</el-button>
|
||||
<div class="wb-title">
|
||||
<span class="wb-title-text">{{ session?.title || '自动化会话' }}</span>
|
||||
<el-tag :type="statusTagType" size="small">{{ statusLabel }}</el-tag>
|
||||
</div>
|
||||
<div class="wb-ws" :class="{ connected: store.wsConnected }">
|
||||
<span class="dot" />
|
||||
{{ store.wsConnected ? '实时连接中' : '未连接' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 已解决横幅 -->
|
||||
<el-alert
|
||||
v-if="session?.status === 'resolved'"
|
||||
type="success"
|
||||
:closable="false"
|
||||
title="会话已自动解决并关闭"
|
||||
class="wb-alert"
|
||||
/>
|
||||
|
||||
<!-- 加载态 -->
|
||||
<div v-if="store.loading" class="wb-loading">
|
||||
<el-skeleton :rows="8" animated />
|
||||
</div>
|
||||
|
||||
<template v-else-if="session">
|
||||
<!-- 会话概览 -->
|
||||
<el-card class="wb-card" shadow="never">
|
||||
<div class="overview">
|
||||
<div class="ov-item"><span class="ov-label">场景</span><span>{{ session.scenario_key || '-' }}</span></div>
|
||||
<div class="ov-item"><span class="ov-label">模式</span><span>{{ modeLabel }}</span></div>
|
||||
<div class="ov-item"><span class="ov-label">置信度</span><span>{{ Math.round((session.confidence || 0) * 100) }}%</span></div>
|
||||
<div class="ov-item"><span class="ov-label">自动关单</span><span>{{ autoCloseText }}</span></div>
|
||||
</div>
|
||||
<div v-if="session.intent" class="intent">意图:{{ intentText }}</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 处置进展(时间线) -->
|
||||
<el-card class="wb-card" shadow="never">
|
||||
<template #header>处置进展</template>
|
||||
<el-timeline>
|
||||
<el-timeline-item
|
||||
v-for="act in session.actions"
|
||||
:key="act.id"
|
||||
:type="timelineType(act)"
|
||||
:hollow="act.status === 'pending'"
|
||||
>
|
||||
<div class="tl-title">
|
||||
<span>{{ act.title }}</span>
|
||||
<el-tag size="small" :type="riskTagType(act)">{{ riskLabel(act) }}</el-tag>
|
||||
</div>
|
||||
<div class="tl-desc">{{ act.description }}</div>
|
||||
<div class="tl-status">状态:{{ actionStatusLabel(act) }}</div>
|
||||
<pre v-if="act.result" class="tl-result">{{ prettyJson(act.result) }}</pre>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item v-if="!session.actions.length" type="info">暂无动作</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
|
||||
<!-- 待坐席审批:高危/写操作动作 -->
|
||||
<el-card v-if="pendingActions.length" class="wb-card" shadow="never">
|
||||
<template #header>待坐席审批(高危 / 写操作)</template>
|
||||
<ActionApprovalCard
|
||||
v-for="act in pendingActions"
|
||||
:key="act.id"
|
||||
:action="act"
|
||||
:ticket="session.approval"
|
||||
@approve="onApprove"
|
||||
@reject="onReject"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 转人工接管 -->
|
||||
<TakeoverPanel
|
||||
:session-id="sessionId"
|
||||
:default-agent-id="defaultAgentId"
|
||||
@takeover="onTakeover"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<el-empty v-else description="会话不存在或已结束" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAutomationStore } from '@/stores/automation'
|
||||
import { useAgentStore } from '@/stores/agent'
|
||||
import ActionApprovalCard from '@/components/automation/ActionApprovalCard.vue'
|
||||
import TakeoverPanel from '@/components/automation/TakeoverPanel.vue'
|
||||
import type { AutomationAction } from '@/api/automation'
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 路由 / Store
|
||||
// --------------------------------------------------------------------------
|
||||
const props = defineProps<{ sessionId?: string }>()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const store = useAutomationStore()
|
||||
|
||||
const sessionId = computed(() => props.sessionId || (route.params.id as string))
|
||||
const session = computed(() => store.currentSession)
|
||||
// 接管默认坐席 ID:优先用坐席 store,降级读 localStorage(避免额外耦合)
|
||||
const defaultAgentId = computed(
|
||||
() => useAgentStore().agentUserId || localStorage.getItem('agent_user_id') || '',
|
||||
)
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 待审批动作筛选:高危/写操作且尚未结案
|
||||
// --------------------------------------------------------------------------
|
||||
const pendingActions = computed(() =>
|
||||
(session.value?.actions || []).filter((a) => needsApproval(a)),
|
||||
)
|
||||
|
||||
function needsApproval(a: AutomationAction): boolean {
|
||||
if (['done', 'executed', 'approved', 'rejected', 'skipped', 'failed'].includes(a.status)) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
a.risk_level === 'high' ||
|
||||
a.risk_level === 'write' ||
|
||||
a.status === 'pending' ||
|
||||
a.status === 'awaiting_approval'
|
||||
)
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 生命周期:挂载即拉详情并连接专用 WebSocket,卸载断开
|
||||
// --------------------------------------------------------------------------
|
||||
onMounted(async () => {
|
||||
if (!sessionId.value) return
|
||||
await store.fetchSession(sessionId.value)
|
||||
store.connectWs(sessionId.value)
|
||||
})
|
||||
onUnmounted(() => store.disconnectWs())
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 事件处理:审批 / 接管(均走已落地 store 的会话级接口)
|
||||
// --------------------------------------------------------------------------
|
||||
async function onApprove(p: { actionId: string; note?: string }): Promise<void> {
|
||||
if (!sessionId.value) return
|
||||
try {
|
||||
// 坐席端审批:沿用已落地 store.approve -> POST /sessions/{id}/approve
|
||||
await store.approve(sessionId.value, 'approve', p.note)
|
||||
await store.fetchSession(sessionId.value)
|
||||
ElMessage.success('已通过并执行')
|
||||
} catch {
|
||||
ElMessage.error('审批失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
async function onReject(p: { actionId: string; note?: string }): Promise<void> {
|
||||
if (!sessionId.value) return
|
||||
try {
|
||||
await store.approve(sessionId.value, 'reject', p.note)
|
||||
await store.fetchSession(sessionId.value)
|
||||
ElMessage.success('已驳回并转人工')
|
||||
} catch {
|
||||
ElMessage.error('操作失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
async function onTakeover(p: { agentId: string; note?: string }): Promise<void> {
|
||||
if (!sessionId.value) return
|
||||
try {
|
||||
await store.takeover(sessionId.value, p.agentId, p.note)
|
||||
await store.fetchSession(sessionId.value)
|
||||
ElMessage.success('已接管,自动化终止')
|
||||
} catch {
|
||||
ElMessage.error('接管失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
function goBack(): void {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 展示辅助
|
||||
// --------------------------------------------------------------------------
|
||||
const statusLabel = computed(() => statusText(session.value?.status || ''))
|
||||
const modeLabel = computed(() => {
|
||||
switch (session.value?.mode) {
|
||||
case 'auto':
|
||||
return '全自动'
|
||||
case 'assist':
|
||||
return '辅助坐席'
|
||||
case 'supervised':
|
||||
return '监督模式'
|
||||
default:
|
||||
return session.value?.mode || '-'
|
||||
}
|
||||
})
|
||||
const intentText = computed(() => prettyJson(session.value?.intent))
|
||||
const autoCloseText = computed(() => {
|
||||
const t = session.value?.auto_close_at
|
||||
return t ? formatTime(t) : '无'
|
||||
})
|
||||
|
||||
function statusText(s: string): string {
|
||||
switch (s) {
|
||||
case 'created':
|
||||
return '已创建'
|
||||
case 'running':
|
||||
return '执行中'
|
||||
case 'paused':
|
||||
return '已暂停'
|
||||
case 'resolved':
|
||||
return '已解决'
|
||||
case 'handoff':
|
||||
return '已转人工'
|
||||
case 'error':
|
||||
return '异常'
|
||||
case 'closed':
|
||||
return '已关闭'
|
||||
default:
|
||||
return s || '未知'
|
||||
}
|
||||
}
|
||||
|
||||
const statusTagType = computed<'success' | 'warning' | 'danger' | 'info' | 'primary'>(() => {
|
||||
switch (session.value?.status) {
|
||||
case 'resolved':
|
||||
return 'success'
|
||||
case 'running':
|
||||
return 'primary'
|
||||
case 'paused':
|
||||
case 'handoff':
|
||||
return 'warning'
|
||||
case 'error':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
})
|
||||
|
||||
function riskLabel(a: AutomationAction): string {
|
||||
switch (a.risk_level) {
|
||||
case 'read':
|
||||
return '只读'
|
||||
case 'low':
|
||||
return '低风险'
|
||||
case 'write':
|
||||
return '写操作'
|
||||
case 'high':
|
||||
return '高危'
|
||||
default:
|
||||
return a.risk_level
|
||||
}
|
||||
}
|
||||
|
||||
function riskTagType(a: AutomationAction): 'info' | 'success' | 'warning' | 'danger' {
|
||||
switch (a.risk_level) {
|
||||
case 'read':
|
||||
return 'info'
|
||||
case 'low':
|
||||
return 'success'
|
||||
case 'write':
|
||||
return 'warning'
|
||||
case 'high':
|
||||
return 'danger'
|
||||
default:
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
function actionStatusLabel(a: AutomationAction): string {
|
||||
switch (a.status) {
|
||||
case 'pending':
|
||||
return '待审批'
|
||||
case 'awaiting_approval':
|
||||
return '待审批'
|
||||
case 'approved':
|
||||
return '已通过'
|
||||
case 'rejected':
|
||||
return '已驳回'
|
||||
case 'running':
|
||||
return '执行中'
|
||||
case 'done':
|
||||
case 'executed':
|
||||
return '已完成'
|
||||
case 'failed':
|
||||
return '失败'
|
||||
case 'skipped':
|
||||
return '已跳过'
|
||||
default:
|
||||
return a.status
|
||||
}
|
||||
}
|
||||
|
||||
function timelineType(a: AutomationAction): 'primary' | 'success' | 'warning' | 'danger' | 'info' {
|
||||
if (a.status === 'done' || a.status === 'executed' || a.status === 'approved') return 'success'
|
||||
if (a.status === 'failed') return 'danger'
|
||||
if (a.status === 'rejected') return 'warning'
|
||||
if (a.status === 'pending' || a.status === 'awaiting_approval') return 'warning'
|
||||
return 'primary'
|
||||
}
|
||||
|
||||
function prettyJson(v: unknown): string {
|
||||
try {
|
||||
return typeof v === 'string' ? v : JSON.stringify(v, null, 2)
|
||||
} catch {
|
||||
return String(v)
|
||||
}
|
||||
}
|
||||
|
||||
function formatTime(t: string): string {
|
||||
try {
|
||||
return new Date(t).toLocaleString('zh-CN', { hour12: false })
|
||||
} catch {
|
||||
return t
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.session-workbench {
|
||||
padding: 16px;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.wb-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.wb-title {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.wb-title-text {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
.wb-ws {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.wb-ws .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--el-color-info);
|
||||
}
|
||||
.wb-ws.connected .dot {
|
||||
background: var(--el-color-success);
|
||||
}
|
||||
.wb-alert {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.wb-card {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.overview {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
.ov-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 14px;
|
||||
}
|
||||
.ov-label {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.intent {
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.tl-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tl-desc {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.tl-status {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.tl-result {
|
||||
margin-top: 6px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
max-height: 160px;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
@@ -20,8 +20,16 @@ export default defineConfig({
|
||||
server: {
|
||||
// 开发服务器端口(避免和H5前端冲突)
|
||||
port: 5173,
|
||||
// 端口被占用时自动尝试下一个端口
|
||||
strictPort: false,
|
||||
// 自动打开浏览器
|
||||
open: true,
|
||||
open: false,
|
||||
// 禁用热更新,改用页面刷新(更稳定)
|
||||
hmr: false,
|
||||
// 增加超时时间
|
||||
timeout: 30000,
|
||||
// 最大并发请求数
|
||||
maxConcurrency: 200,
|
||||
// API 代理:将 /api 请求转发到后端,解决开发环境跨域问题
|
||||
proxy: {
|
||||
'/api': {
|
||||
|
||||
Reference in New Issue
Block a user