chore: 整理项目结构,清理归档文件,更新部署配置

This commit is contained in:
Simon
2026-07-04 21:01:39 +08:00
parent 8bd4ab0366
commit 64ff1bf7d5
508 changed files with 43575 additions and 14129 deletions
Binary file not shown.
Binary file not shown.
+7 -8
View File
@@ -280,16 +280,15 @@ export async function sendMessage(data: SendMessageRequest): Promise<SendMessage
const response: any = await apiClient.post('/h5/conversations/current/messages', data, {
timeout: 30000,
})
// response = {code:0, data: {user_message:..., ai_reply:...}, message:"success"}
// response.data = 业务数据 {user_message:..., ai_reply:..., ...}
const raw = response.data
// 注意:apiClient 拦截器返回的是 {code: 0, data: {...}, message: "success"} 包装对象,
// 需要通过 response.data 获取实际业务数据
// 修复字段映射:后端返回 id/sender_typeH5前端期望 message_id/message_type
return {
user_message: mapMessage(raw.user_message),
ai_reply: raw.ai_reply ? mapMessage(raw.ai_reply) : raw.ai_reply,
is_guidance: raw.is_guidance,
ai_reply_count: raw.ai_reply_count,
can_call_agent: raw.can_call_agent,
user_message: mapMessage(response.data.user_message),
ai_reply: response.data.ai_reply ? mapMessage(response.data.ai_reply) : response.data.ai_reply,
is_guidance: response.data.is_guidance,
ai_reply_count: response.data.ai_reply_count,
can_call_agent: response.data.can_call_agent,
}
}
+4 -2
View File
@@ -115,8 +115,9 @@ export async function uploadImage(file: File): Promise<{
'/messages/image',
formData,
{
// ISS-B4 修复:不显式设置 Content-Type,让浏览器自动生成带 boundary 的 multipart/form-data
headers: {
'Content-Type': 'multipart/form-data',
'Content-Type': undefined,
},
}
)
@@ -141,8 +142,9 @@ export async function uploadMessageFile(file: File): Promise<{
'/messages/file',
formData,
{
// ISS-B4 修复:不显式设置 Content-Type,让浏览器自动生成带 boundary 的 multipart/form-data
headers: {
'Content-Type': 'multipart/form-data',
'Content-Type': undefined,
},
}
)
+41 -14
View File
@@ -47,19 +47,46 @@ export async function uploadFile(file: File | Blob, blobNamePrefix: string = 'pa
formData.append('file', file as File)
}
// 发送上传请求(60 秒超时,大文件上传可能较慢
// 注意:必须显式删除 Content-Type,让浏览器自动生成带 boundary 的 multipart/form-data
// 原因:apiClient 实例默认设置了 'Content-Type': 'application/json'
// 如果不覆盖,Axios 会保留 application/json,后端无法解析 FormData 中的 file 字段
const response: any = await apiClient.post('/upload', formData, {
headers: {
'Content-Type': undefined,
},
timeout: 60000,
})
// ISS-B3 修复:上传带自动重试(3次,指数退避 1s→2s→4s
return await uploadWithRetry(formData)
}
// 响应拦截器已确保 code === 0
// response = {code:0, data: {url:"...",...}, message:"success"}(拦截器返回值)
// response.data = 业务数据 {url:"...", filename:"...", ...}
return response.data as UploadResponse
/**
* 上传重试包装函数
*
* 做什么:对文件上传请求进行自动重试,最多 3 次
* 为什么:网络波动时避免用户手动重试,提升上传成功率
* 重试策略:指数退避(1秒 → 2秒 → 4秒),最多 3 次重试(共 4 次尝试)
*
* @param formData - 已构建好的 FormData 对象
* @param maxRetries - 最大重试次数(默认 3)
* @returns 上传响应数据
*/
async function uploadWithRetry(formData: FormData, maxRetries: number = 3): Promise<UploadResponse> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
// 发送上传请求(60 秒超时,大文件上传可能较慢)
// 注意:必须显式删除 Content-Type,让浏览器自动生成带 boundary 的 multipart/form-data
// 原因:apiClient 实例默认设置了 'Content-Type': 'application/json'
// 如果不覆盖,Axios 会保留 application/json,后端无法解析 FormData 中的 file 字段
const response: any = await apiClient.post('/upload', formData, {
headers: {
'Content-Type': undefined,
},
timeout: 60000,
})
// 响应拦截器已确保 code === 0
// response = {code:0, data: {url:"...",...}, message:"success"}(拦截器返回值)
// response.data = 业务数据 {url:"...", filename:"...", ...}
return response.data as UploadResponse
} catch (err) {
if (attempt === maxRetries) throw err
// 指数退避:1s, 2s, 4s
const delay = 1000 * Math.pow(2, attempt)
console.warn(`[Upload H5] 上传失败,${delay / 1000}秒后重试(第 ${attempt + 1}/${maxRetries} 次)`)
await new Promise(r => setTimeout(r, delay))
}
}
// 不应到达此处,但 TypeScript 需要返回值
throw new Error('上传失败:已达最大重试次数')
}
@@ -118,6 +118,7 @@
:trigger-text="store.approvalCardTriggerText"
@select="handleApprovalSelect"
/>
</div>
</template>
+6 -197
View File
@@ -2,7 +2,7 @@
// 企微IT智能服务台 — H5用户端输入栏组件
// =============================================================================
// 说明:底部输入栏,固定在消息列表下方,包含:
// [工具栏:表情/图片/文件/拍照] [文本输入框] [发送按钮]
// [工具栏:表情/文件] [文本输入框] [发送按钮]
// - 输入框默认3行可见,高度随内容动态适应
// - 输入框顶部拖拽手柄可手动调节高度
// - Enter 发送,Shift+Enter 换行
@@ -23,23 +23,14 @@
<!-- 输入区域工具栏 + 输入框 + 发送按钮 -->
<div class="input-bar__row">
<!-- 工具栏表情/图片/文件/拍照/截图 -->
<!-- 工具栏表情/文件 (已移除图片/拍照/截图) -->
<div class="input-bar__toolbar">
<button class="input-bar__tool-btn" title="表情" @click="handleEmoji">
<span>😊</span>
</button>
<button class="input-bar__tool-btn" title="图片" @click="handleImage">
<span>🖼</span>
</button>
<button class="input-bar__tool-btn" title="文件" @click="handleFile">
<span>📎</span>
</button>
<button class="input-bar__tool-btn" title="拍照" @click="handleCamera">
<span>📷</span>
</button>
<button class="input-bar__tool-btn" title="截图" @click="handleScreenshot">
<span></span>
</button>
</div>
<!-- 表情选择面板简易版常用 Emoji 网格 -->
@@ -95,31 +86,13 @@
请描述你遇到的问题AI 助手会帮你分析 💡
</div>
<!-- 隐藏的文件输入框图片/文件上传用由工具栏按钮触发 -->
<!-- 隐藏的文件输入框文件上传用由工具栏按钮触发 -->
<input
ref="fileInputRef"
type="file"
style="display: none"
@change="handleFileSelect"
/>
<!-- 隐藏的拍照输入框移动端直接调用摄像头 -->
<input
ref="cameraInputRef"
type="file"
accept="image/*"
capture="environment"
style="display: none"
@change="handleCameraCapture"
/>
<!-- 截图区域选择编辑器对标微信/企微截图体验 -->
<ScreenshotEditor
v-if="showScreenshotEditor"
:screenshot-canvas="screenshotCanvas"
@confirm="onScreenshotConfirm"
@cancel="onScreenshotCancel"
/>
</div>
</template>
@@ -129,15 +102,13 @@
* 布局:[工具栏] [输入框] [发送按钮]
* 输入框固定底部,默认3行可见,高度动态适应
* 顶部拖拽手柄可手动调节输入栏整体高度
* 工具栏:表情/图片/文件/拍照
* 支持粘贴图片上传(Ctrl+V)和文件选择上传
* 工具栏:表情/文件 (已移除图片/拍照/截图)
* 支持文件选择上传(图片上传已禁用)
*/
import { ref, computed, nextTick, onMounted, onUnmounted } from 'vue'
import { showToast } from 'vant'
import html2canvas from 'html2canvas-pro'
import { useConversationStore } from '@/stores/conversation'
import { uploadFile } from '@/api/upload'
import ScreenshotEditor from './ScreenshotEditor.vue'
// ============================================================================
// 工具函数:安全提取错误详情(防止 [object Object]
@@ -176,9 +147,6 @@ const inputFieldRef = ref<any>(null)
/** 隐藏文件输入框 DOM 引用(用于触发系统文件选择器) */
const fileInputRef = ref<HTMLInputElement | null>(null)
/** 隐藏拍照输入框 DOM 引用(用于调用移动端摄像头) */
const cameraInputRef = ref<HTMLInputElement | null>(null)
/** 是否可以发送消息(输入框有内容且未在加载中) */
const canSend = computed(() => {
return inputText.value.trim().length > 0 && !store.loading && store.isLoggedIn
@@ -190,12 +158,6 @@ const isInputResizing = ref<boolean>(false)
/** 表情面板是否可见 */
const showEmojiPanel = ref<boolean>(false)
/** 截图编辑器是否可见 */
const showScreenshotEditor = ref<boolean>(false)
/** html2canvas 生成的完整页面截图 Canvas 对象(传给 ScreenshotEditor */
let screenshotCanvas: HTMLCanvasElement | null = null
// ============================================================================
// 生命周期:Document 级别粘贴监听(修复 van-field @paste 不触发文件粘贴的问题)
// ============================================================================
@@ -450,18 +412,6 @@ function handleEmoji(): void {
showEmojiPanel.value = !showEmojiPanel.value
}
/**
* 处理图片按钮点击
* 触发文件选择器(限定图片类型)
*/
function handleImage(): void {
if (fileInputRef.value) {
fileInputRef.value.accept = 'image/*'
fileInputRef.value.multiple = true
fileInputRef.value.click()
}
}
/**
* 处理文件按钮点击
* 触发文件选择器(不限类型)
@@ -474,147 +424,6 @@ function handleFile(): void {
}
}
/**
* 处理拍照按钮点击
* 触发移动端摄像头拍照(capture="environment" 调用后置摄像头)
* 桌面端降级为普通图片选择器
*/
function handleCamera(): void {
if (cameraInputRef.value) {
// 重置 input,允许重复拍照
cameraInputRef.value.value = ''
cameraInputRef.value.click()
}
}
/**
* 处理拍照/选择图片后的回调
* 将拍摄的图片上传并发送图片消息
*/
async function handleCameraCapture(event: Event): Promise<void> {
const input = event.target as HTMLInputElement
const files = input.files
if (!files || files.length === 0) return
const file = files[0]
if (file.type.startsWith('image/')) {
await handleImageUpload(file)
}
// 重置 input,允许重复拍照
input.value = ''
}
/**
* 处理截图按钮点击(对标微信/企微)
*
* 做什么:截取当前页面,然后进入区域选择模式
* 为什么:用户反馈截图不好用,需要对标微信/企微的截图体验
*
* 交互流程:
* 1. 用 html2canvas 截取整个页面
* 2. 显示 ScreenshotEditor 组件(全屏遮罩+区域选择)
* 3. 用户在编辑器中选择区域并确认
* 4. 接收裁剪后的图片 Blob,上传并发送
* 5. 如果 html2canvas 失败(企微内置浏览器兼容问题),fallback 到手动选择图片
*/
async function handleScreenshot(): Promise<void> {
try {
showToast('正在截取页面...')
// 1. 截取整个页面(增加兼容性配置:useCORS + allowTaint 提升企微浏览器兼容性)
const canvas = await html2canvas(document.body, {
useCORS: true,
allowTaint: true,
scale: window.devicePixelRatio || 1,
logging: false,
backgroundColor: '#ffffff',
// 企微内置浏览器兼容性优化
foreignObjectRendering: false,
removeContainer: true,
})
// 2. 保存 canvas 并显示截图编辑器
screenshotCanvas = canvas
showScreenshotEditor.value = true
} catch (error) {
console.error('截图失败,尝试 fallback 方案:', error)
// Fallbackhtml2canvas 在企微内置浏览器中可能失败
// 降级为手动选择图片(从相册选取或重新拍照)
try {
if (cameraInputRef.value) {
showToast('截图功能不可用,请选择图片替代')
cameraInputRef.value.value = ''
// 不使用 capture 属性,允许从相册选择
cameraInputRef.value.removeAttribute('capture')
cameraInputRef.value.click()
// 恢复 capture 属性
nextTick(() => {
if (cameraInputRef.value) {
cameraInputRef.value.setAttribute('capture', 'environment')
}
})
} else {
showToast('截图失败,请重试')
}
} catch (fallbackError) {
console.error('截图 fallback 也失败:', fallbackError)
showToast('截图失败,请重试')
}
}
}
/**
* 截图编辑器确认回调(对标微信/企微)
* 接收裁剪后的图片 Blob,上传并发送
*
* 注意:H5 端当前后端 sendMessage API 只支持文本消息,
* 所以截图上传后以文本形式发送截图链接,后续后端支持图片消息后可升级
*/
async function onScreenshotConfirm(blob: Blob): Promise<void> {
try {
console.log('[InputBar] 截图确认,开始上传,blob size:', blob.size)
showToast('截图上传中...')
const result = await uploadFile(blob, 'screenshot')
console.log('[InputBar] 上传成功,result:', result)
// 以图片消息类型发送截图(携带 media_urlMessageBubble 会渲染缩略图)
console.log('[InputBar] 开始调用 store.sendNewMessagemedia_url:', result.url)
await store.sendNewMessage('[截图]', {
msg_type: 'image',
media_url: result.url,
file_name: result.filename,
file_size: result.file_size,
})
console.log('[InputBar] store.sendNewMessage 完成,当前消息数:', store.messages.length)
showToast('截图发送成功')
console.log('[InputBar] 截图发送成功 toast 已显示')
} catch (error: any) {
console.error('[InputBar] 截图发送失败:', error)
showToast(
`截图发送失败:${
formatErrorDetail(error?.response?.data?.detail) ||
error?.message ||
'未知错误'
}`
)
} finally {
showScreenshotEditor.value = false
screenshotCanvas = null
console.log('[InputBar] 截图流程 finally 块执行完成')
}
}
/**
* 截图编辑器取消回调
*/
function onScreenshotCancel(): void {
showScreenshotEditor.value = false
screenshotCanvas = null
}
// ============================================================================
// 拖拽调节输入栏高度
// ============================================================================
@@ -710,7 +519,7 @@ function handleInputResizeStart(event: MouseEvent): void {
gap: 8px;
}
/* 工具栏:表情/图片/文件/截图 */
/* 工具栏:表情/文件 (已移除图片/拍照/截图) */
.input-bar__toolbar {
display: flex;
gap: 4px;
@@ -105,10 +105,12 @@ export function useH5WebSocket() {
const isDev = import.meta.env.DEV
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
const wsHost = isDev ? 'localhost:8000' : window.location.host
const wsUrl = `${wsProtocol}//${wsHost}/ws/h5/${employeeId}?token=${token}`
const wsUrl = `${wsProtocol}//${wsHost}/ws/h5/${employeeId}`
console.log(`[H5 WS] 正在连接: ${wsUrl.replace(/token=[^&]+/, 'token=***')}`)
ws = new WebSocket(wsUrl)
console.log(`[H5 WS] 正在连接: ${wsUrl}`)
// ISS-B2 修复: 使用 WebSocket subprotocol 传递 token(与坐席端一致)
// 浏览器原生 WebSocket API 第2参数是 protocols,服务端从 sec-websocket-protocol 头读取 bearer.{token}
ws = new WebSocket(wsUrl, [`bearer.${token}`])
// ----------------------------------------------------------------------
// 连接成功
+13 -5
View File
@@ -17,7 +17,6 @@ import ChatView from '@/views/ChatView.vue'
// v0.5.4 BC/DR 应急页(身份检测 + H5 右栏)
import EmergencyDispatcher from '@/views/EmergencyDispatcher.vue'
import H5PreviewView from '@/views/H5PreviewView.vue'
// --------------------------------------------------------------------------
// 企微环境检测工具函数
// --------------------------------------------------------------------------
@@ -129,20 +128,29 @@ router.beforeEach(async (to, _from, next) => {
if (urlToken) {
// token 已存入 localStorage,重新初始化 store 中的 token 状态
employeeStore.$patch({ token: urlToken })
// #90 修复:Portal传递token后,需要调用后端API获取用户信息
// 否则 isAuthenticated 只检查 token 存在,不检查用户信息有效性
try {
await employeeStore.fetchEmployeeInfo()
} catch (e) {
console.warn('[Router] Portal token 验证失败:', e)
}
}
// ========================================================================
// 第一道防线:企微环境检测(非企微环境 → 拦截
// 第一道防线:企微环境检测(非企微环境 → 降级登录
// ========================================================================
// 生产环境强制企微内访问;开发环境(localhost)跳过检测
// 非企微环境:允许有 token 的用户直接进入,没有 token 则跳转降级登录页
// 开发环境(localhost)跳过检测
const isLocalhost = /^localhost(:\d+)?$/.test(window.location.hostname) || window.location.hostname === '127.0.0.1'
if (!isLocalhost && !isWeworkEnv()) {
// 如果有 token(从 Portal 传入),允许直接进入
// 如果有 token(从 Portal 传入或本地存储),允许直接进入
if (employeeStore.isAuthenticated) {
next()
return
}
next({ name: 'WeworkOnly' })
// 非企微环境:跳转到降级登录页,而非企微专属拦截页
next({ name: 'Login' })
return
}
+143 -9
View File
@@ -34,6 +34,79 @@ import {
} from '@/api/conversation'
import { useEmployeeStore } from '@/stores/employee'
// --------------------------------------------------------------------------
// 本地缓存配置
// --------------------------------------------------------------------------
const MESSAGES_CACHE_KEY = 'h5_messages_cache' // 消息缓存 key 前缀
const MESSAGES_CACHE_EXPIRE_DAYS = 7 // 缓存过期天数
const MESSAGES_CACHE_MAX = 100 // 单会话最多缓存消息数
/** 获取消息缓存 key */
function getCacheKey(conversationId: string): string {
return `${MESSAGES_CACHE_KEY}_${conversationId}`
}
/** 从 localStorage 读取消息缓存 */
function loadMessagesFromCache(conversationId: string): Message[] | null {
try {
const key = getCacheKey(conversationId)
const cached = localStorage.getItem(key)
if (!cached) return null
const data = JSON.parse(cached)
// 检查是否过期
const cacheTime = data.timestamp || 0
const now = Date.now()
const expireMs = MESSAGES_CACHE_EXPIRE_DAYS * 24 * 60 * 60 * 1000
if (now - cacheTime > expireMs) {
localStorage.removeItem(key)
return null
}
return data.messages || null
} catch (e) {
console.warn('[Store] 读取消息缓存失败:', e)
return null
}
}
/** 保存消息到 localStorage */
function saveMessagesToCache(conversationId: string, messages: Message[]): void {
try {
const key = getCacheKey(conversationId)
// 只保留最近 N 条消息
const trimmed = messages.slice(-MESSAGES_CACHE_MAX)
localStorage.setItem(key, JSON.stringify({
messages: trimmed,
timestamp: Date.now(),
}))
} catch (e) {
console.warn('[Store] 保存消息缓存失败:', e)
}
}
/** 清除指定会话的缓存 */
function clearMessagesCache(conversationId: string): void {
try {
const key = getCacheKey(conversationId)
localStorage.removeItem(key)
} catch (e) {
console.warn('[Store] 清除消息缓存失败:', e)
}
}
/** 合并缓存和新消息(去重) */
function mergeMessages(cached: Message[], fresh: Message[]): Message[] {
const map = new Map<string, Message>()
// 先添加缓存
cached.forEach(m => map.set(m.message_id, m))
// 再添加新消息(覆盖缓存)
fresh.forEach(m => map.set(m.message_id, m))
// 按时间排序
return Array.from(map.values()).sort((a, b) =>
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
)
}
// --------------------------------------------------------------------------
// Store 定义
// --------------------------------------------------------------------------
@@ -432,6 +505,12 @@ export const useConversationStore = defineStore('conversation', () => {
status: 'sent',
}
console.log('[Store] 乐观更新成功:临时消息已替换为真实消息')
// 更新本地缓存
const convId = currentConversation.value?.conversation_id
if (convId) {
saveMessagesToCache(convId, messages.value)
}
} else {
// 防御性:找不到临时消息时直接添加
messages.value.push({ ...resp.user_message, status: 'sent' })
@@ -520,6 +599,12 @@ export const useConversationStore = defineStore('conversation', () => {
// 更新最后消息 ID
lastMessageId.value = uniqueNewMessages[uniqueNewMessages.length - 1].message_id
console.log('[Store] 轮询到新消息:', uniqueNewMessages.length, '条')
// 更新本地缓存
const convId = currentConversation.value?.conversation_id
if (convId) {
saveMessagesToCache(convId, messages.value)
}
}
}
} catch (error) {
@@ -819,19 +904,64 @@ export const useConversationStore = defineStore('conversation', () => {
try {
console.log('[Store] 开始初始化应用...')
// 获取用户信息
await fetchUserInfo()
// 获取当前会话
await fetchCurrentConversation()
// 加载右侧面板数据
// ===== 步骤1:并行加载用户信息和会话(不等待消息) =====
await Promise.all([
fetchApprovalLinks(),
fetchSoftwareDownloads(),
fetchUserInfo(),
fetchCurrentConversation(),
])
// 启动消息轮询
// ===== 步骤2:加载本地缓存(如果有) =====
const convId = currentConversation.value?.conversation_id
if (convId) {
const cached = loadMessagesFromCache(convId)
if (cached && cached.length > 0) {
console.log(`[Store] 加载缓存消息 ${cached.length}`)
messages.value = cached
}
}
// ===== 步骤3:后台加载后端消息(不阻塞UI) =====
// 使用 Promise.resolve().then() 让它在下一次事件循环执行,不阻塞主线程
Promise.resolve().then(async () => {
try {
const freshMessages = await pollMessages()
if (freshMessages.length > 0) {
// 合并缓存和最新消息
if (convId) {
messages.value = mergeMessages(messages.value, freshMessages)
// 保存到缓存
saveMessagesToCache(convId, messages.value)
} else {
messages.value = freshMessages
}
// 更新最后消息ID
const lastMsg = freshMessages[freshMessages.length - 1]
if (lastMsg) {
lastMessageId.value = lastMsg.message_id
}
console.log(`[Store] 后端消息已合并,共 ${messages.value.length}`)
}
} catch (e) {
console.warn('[Store] 加载后端消息失败:', e)
}
})
// ===== 步骤4:延迟加载右侧面板数据(不阻塞首屏) =====
// 使用 setTimeout 让它不阻塞消息加载
setTimeout(() => {
Promise.all([
fetchApprovalLinks(),
fetchSoftwareDownloads(),
]).catch(e => console.warn('[Store] 加载面板数据失败:', e))
}, 500)
// ===== 步骤5:启动轮询 =====
startPolling()
// ===== 步骤6:标记初始化完成(消息已显示) =====
initialized.value = true
console.log('[Store] 应用初始化完成')
console.log('[Store] 应用初始化完成(缓存优先显示)')
} catch (error) {
console.error('[Store] 应用初始化失败:', error)
}
@@ -842,6 +972,10 @@ export const useConversationStore = defineStore('conversation', () => {
* 在组件卸载时调用,停止轮询,清理定时器
*/
function cleanup(): void {
// 清除当前会话的缓存
if (currentConversation.value?.conversation_id) {
clearMessagesCache(currentConversation.value.conversation_id)
}
stopPolling()
}
+5
View File
@@ -45,6 +45,11 @@ export const useEmployeeStore = defineStore('employee', () => {
/** 访问令牌(Bearer Token)— 优先从 h5_token 读取,降级读取 portal_token */
const token = ref<string>(localStorage.getItem(TOKEN_KEY) || localStorage.getItem(PORTAL_TOKEN_KEY) || '')
// 页面刷新时:如果 token 存在且有效,重置重定向计数,避免残留计数导致误报"登录状态异常"
if (token.value && !isTokenExpired(token.value)) {
localStorage.removeItem(OAUTH_REDIRECT_COUNT_KEY)
}
/** 当前员工信息 */
const employeeInfo = ref<EmployeeInfo | null>(null)
@@ -25,9 +25,9 @@
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showLoadingToast, showFailToast } from 'vant'
import { showFailToast } from 'vant'
import axios from 'axios'
const router = useRouter()
@@ -36,8 +36,6 @@ const loading = ref(true)
const errorMsg = ref('')
const statusText = ref('正在检测身份...')
const isMobile = computed(() => window.innerWidth < 500)
/**
* 加载企微 JS-SDK
* 注意:企微 JS-SDK 文件名是 jweixin-1.2.0.js(历史遗留,虽然叫 jweixin)
@@ -108,7 +106,7 @@ function wxAgentConfig(config: any): Promise<{ userId: string }> {
nonceStr: config.nonce_str,
signature: config.signature,
jsApiList: ['selectExternalContact'],
success: (res: any) => {
success: (_res: any) => {
// 拿当前 userid(实际场景可能要从 selectExternalContact 等接口拿)
// 这里我们直接通过 URL 参数或后端回查
// 简化版:从后端 cookie / 之前登录态拿
@@ -368,8 +368,6 @@ function handleSelect(option: 'yes' | 'no'): void {
// 标记当前节点为已完成
currentNode.value.status = 'done'
const currentId = currentNode.value.id
// 获取下一个节点
const nextNode: FlowchartNode | undefined =
option === 'yes'