Files
wecom_it_smart_desk/frontend-terminal/src/views/RepairView.vue
T
Simon 449c6d4875 feat: 2026-07-12~13 全量更新 - AI对话链路改造+H5 v4/v5+坐席端v5+上下文感知诊断+知识库迭代3
## H5 员工端 v4 (2026-07-13 00:48 已部署)
- 人工按钮三态文案统一为"人工坐席"
- 按钮位置移至发送键和语音按钮上方(垂直堆叠)
- 点按钮直接调 store.shakeAgent(),删除 CallAgentModal 弹窗动画
- 截图快捷键提示改为"截图->粘贴:Alt+Shift+A-Ctrl+V ---> Ctrl+V"
- 移动端隐藏截图提示(CSS 媒体查询)
- AI转人工提示改为"已为您呼叫人工坐席,请稍等!"
- 坐席接入提示改为"坐席正在查看您的信息,请等待处理回复!"
- 删除"摇铃呼叫坐席"入口和文案
- 删除孤儿组件 MessageList.vue + shake 动画 CSS

## H5 员工端 v5 (2026-07-13 02:08 已部署)
- RightPanel v2.1:删除"软件安装"和"资源权限"标签页
- 移除标签栏,智能推荐(DynamicRecommend)直接展示
- 删除 SoftwareDownloads/ApprovalLinks 引用和相关 CSS

## AI 对话链路全栈改造 Phase 1-6 (已部署)
- Phase 1: Dify JSON输出 + 后端blocking解析 + 双WS推送 + 错误降级
- Phase 2: 关键词收窄(~25强意图词) + 两级分类Prompt + 删除前端checkApprovalIntent
- Phase 3: WS扩展(ai_thinking+dynamic_recommend) + ai_structured气泡 + RightPanel v2 + 选项回传
- Phase 4: VisionService接入 + 图片消息融合(5秒窗口) + 降级策略
- Phase 5: 坐席端ai_thinking指示器 + ai_structured/byod_card渲染 + handleNewMessage修复
- Phase 6: diagnosis_stage(6值) + response_time_ms计时 + 慢响应告警(>10s)

## 坐席端 v5 (2026-07-13 01:38 已部署)
- ai_structured/byod_card 只读渲染
- AI思考指示器 UI
- handleNewMessage 透传 msg_type/extra_data 修复
- 布局优化v2.0: QuickReplyBar L1+L2悬浮 + ReplyBox左右分区 + 右栏260/560px切换
- 键盘快捷键v2.3: 纯数字路由 + ESC分层撤销 + Shift+Space用event.code

## 上下文感知智能诊断闭环 (2026-07-12 已部署)
- 三层诊断(API→Script→AI) + 三段排队(VIP→info_locked→not locked)
- 答题插队 + 五场景关闭
- 迁移052(6表+6列) + queue_service + quiz_service + closing_service
- H5前端: QueueWaiting + RightPanel双Tab + InputBar三态 + ResolveConfirmCard
- 坐席前端: pending_close结单流程 + 信息锁定(Dify步骤完成+有效回答率≥70%)

## 知识库迭代3 (2026-07-12 已部署)
- 分诊交互(H5+坐席+Dify独立应用)
- 拓扑预览(ECharts只读)
- 代答排除(4种匹配器: keyword/regex/intent/category)
- 迁移051 + 44文件43测试通过

## 后端变更
- 6个Python文件改造(h5_ai_task.py/h5.py/ai_service.py/closing_service.py等)
- funny_phrase_service.py: shake/connected/keyword 默认文案更新
- session_service.py: 企微消息文案同步
- 新增: queue.py/quiz.py/triage.py/exclusion_rules.py 等API端点
- 新增: diagnostic.py/quiz.py/triage_session.py 等模型
- 新增: closing_service/queue_service/quiz_service/triage_service 等服务

## 文档更新
- CHANGELOG.md: 新增 [未发布] 区全部变更记录
- 项目管理主文档 v2.5: 新增v0.7.3版本 + 已完成看板 + 最近搞定
- 版本记录: 新增v0.7.3条目
- AI对话链路实施计划: Phase 1-6 全部标记已实施
- 新增架构图/时序图/类图(mermaid)

## 部署路径修正
- 服务器项目根路径: /opt/wecom-it-desk/
- 所有前端dist均为ro bind mount,只能在宿主机源路径操作
- 服务器nginx /h5/ 是静态文件服务(非proxy_pass)
- elFinder上传二进制不可靠(MD5不匹配),改用base64分块上传
2026-07-13 02:17:03 +08:00

346 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
=============================================================================
企微IT智能服务台 终端设备报修页面
=============================================================================
说明小鱼终端大屏设备报修页面
- 设备类型选择投影仪/视频会议设备/空调/桌椅/网络/其他
- 故障描述文本输入支持快捷选项
- 匿名报修未登录时也可提交
- 提交后创建 IT 工单 + 通知管理员
- 成功/失败反馈自动返回状态页
- 1920×1080 横屏深色主题适配
=============================================================================
-->
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTerminalStore } from '@/stores/terminal'
import { submitRepair } from '@/api/meetingroom'
import { useTerminal } from '@/composables/useTerminal'
import type { DeviceTypeOption } from '@/types/meetingroom'
// ==========================================================================
// 路由 & Store & 终端
// ==========================================================================
const route = useRoute()
const router = useRouter()
const store = useTerminalStore()
const { close: closePage } = useTerminal()
// ==========================================================================
// 设备类型选项
// ==========================================================================
/** 设备类型列表(与后端 repair_service.py 的 DEVICE_TYPE_LABELS 对应) */
const deviceTypeOptions: DeviceTypeOption[] = [
{ value: 'projector', label: '投影仪', icon: '📽️' },
{ value: 'video_conf', label: '视频会议设备', icon: '📹' },
{ value: 'aircon', label: '空调', icon: '❄️' },
{ value: 'desk_chair', label: '桌椅', icon: '🪑' },
{ value: 'network', label: '网络', icon: '🌐' },
{ value: 'other', label: '其他设备', icon: '🔧' },
]
// ==========================================================================
// 表单状态
// ==========================================================================
/** 选中的设备类型 */
const selectedDeviceType = ref<string>('')
/** 故障描述 */
const faultDescription = ref('')
/** 提交中 */
const submitting = ref(false)
/** 提交结果 */
const submitResult = ref<{ success: boolean; repairId?: number; message: string } | null>(null)
/** 快捷故障描述选项 */
const quickFaultOptions = [
'无法开机',
'画面模糊',
'声音异常',
'连接失败',
'设备老化',
'其他问题',
]
// ==========================================================================
// 计算属性
// ==========================================================================
/** 终端SN */
const terminalSn = computed(() => route.params.sn as string || store.terminalSn)
/** 会议室信息 */
const meetingroomId = computed(() => store.meetingroomInfo?.meetingroom_id ?? null)
const meetingroomName = computed(() => store.meetingroomName)
/** 是否可提交 */
const canSubmit = computed(() => {
return selectedDeviceType.value !== '' && faultDescription.value.trim().length > 0 && !submitting.value
})
/** 选中设备类型的显示信息 */
const selectedDeviceInfo = computed(() => {
return deviceTypeOptions.find(d => d.value === selectedDeviceType.value)
})
// ==========================================================================
// 方法
// ==========================================================================
/** 选择设备类型 */
function selectDeviceType(value: string) {
selectedDeviceType.value = value
}
/** 快速填充故障描述 */
function quickFillFault(text: string) {
// 如果已有内容,追加而不是覆盖
if (faultDescription.value && !faultDescription.value.includes(text)) {
faultDescription.value = `${faultDescription.value}${text}`
} else {
faultDescription.value = text
}
}
/** 提交报修 */
async function handleSubmit() {
if (!canSubmit.value) return
if (!meetingroomId.value) {
submitResult.value = { success: false, message: '会议室信息缺失,无法提交报修' }
return
}
submitting.value = true
submitResult.value = null
try {
const result = await submitRepair({
terminal_sn: terminalSn.value,
meetingroom_id: meetingroomId.value,
meetingroom_name: meetingroomName.value,
device_type: selectedDeviceType.value,
fault_description: faultDescription.value.trim(),
// 如果已登录,附带用户信息;未登录则匿名提交
reporter_name: store.isLoggedIn ? store.loginUser?.name : undefined,
reporter_userid: store.isLoggedIn ? store.loginUser?.userid : undefined,
})
submitResult.value = {
success: true,
repairId: result.repair_id,
message: '报修已提交,IT人员将尽快处理',
}
// 3秒后自动返回状态页
setTimeout(() => {
goBack()
}, 3000)
} catch (e: any) {
submitResult.value = {
success: false,
message: e.message || '提交失败,请稍后重试或联系IT管理员',
}
} finally {
submitting.value = false
}
}
/** 返回状态页 */
function goBack() {
const sn = route.params.sn as string
router.replace(`/${sn}`)
}
/** 关闭页面(小鱼终端返回主页) */
function handleClose() {
closePage()
}
// ==========================================================================
// 生命周期
// ==========================================================================
onMounted(() => {
// 如果终端绑定信息未加载,尝试加载
if (!store.meetingroomInfo && terminalSn.value) {
store.loadBinding(terminalSn.value)
}
})
</script>
<template>
<div class="w-full h-screen flex items-center justify-center bg-bg-primary p-4 md:p-8">
<div class="bg-bg-card rounded-2xl border border-border-subtle shadow-2xl w-full max-w-[900px] max-h-[92vh] overflow-y-auto animate-slide-up">
<!-- ================================================================ -->
<!-- 标题栏 -->
<!-- ================================================================ -->
<div class="flex items-center justify-between p-6 border-b border-border-subtle">
<div>
<h2 class="text-2xl font-bold text-text-primary">🔧 设备报修</h2>
<p class="text-sm text-text-secondary mt-1">
{{ meetingroomName }}
<span v-if="store.location"> · {{ store.location }}</span>
</p>
</div>
<div class="flex items-center gap-3">
<button
class="px-4 py-2 rounded-lg text-base text-text-secondary border border-border-subtle hover:text-text-primary transition-colors"
@click="handleClose"
>
关闭
</button>
<button
class="text-text-secondary hover:text-text-primary text-2xl"
@click="goBack"
>
</button>
</div>
</div>
<!-- ================================================================ -->
<!-- 提交成功状态 -->
<!-- ================================================================ -->
<div v-if="submitResult?.success" class="p-12 text-center">
<div class="text-6xl mb-4"></div>
<h3 class="text-3xl font-bold text-status-free mb-3">报修已提交</h3>
<p class="text-lg text-text-secondary mb-2">
{{ submitResult.message }}
</p>
<p v-if="submitResult.repairId" class="text-base text-text-muted">
报修编号: #{{ submitResult.repairId }}
</p>
<p class="text-sm text-text-muted mt-4">
3 秒后自动返回...
</p>
</div>
<!-- ================================================================ -->
<!-- 报修表单 -->
<!-- ================================================================ -->
<div v-else class="p-6 space-y-6">
<!-- 报修人信息 -->
<div class="flex items-center gap-4">
<span class="text-lg text-text-secondary min-w-[100px]">报修人</span>
<div class="px-4 py-2 bg-bg-input rounded-lg text-lg text-text-primary">
<span v-if="store.isLoggedIn">
{{ store.loginUser?.name || store.loginUser?.userid }}
</span>
<span v-else class="text-text-muted">匿名报修终端提交</span>
</div>
</div>
<!-- 设备类型选择 -->
<div>
<div class="flex items-center gap-4 mb-3">
<span class="text-lg text-text-secondary min-w-[100px]">设备类型</span>
<span class="text-sm text-text-muted">请选择故障设备</span>
</div>
<div class="grid grid-cols-2 md:grid-cols-3 gap-3 md:gap-4 ml-0 md:ml-[116px]">
<button
v-for="opt in deviceTypeOptions"
:key="opt.value"
class="flex items-center gap-3 px-5 py-4 rounded-xl border-2 transition-all"
:class="selectedDeviceType === opt.value
? 'border-status-free bg-status-free/10 text-text-primary'
: 'border-border-subtle bg-bg-input text-text-secondary hover:text-text-primary hover:border-border-subtle'"
@click="selectDeviceType(opt.value)"
>
<span class="text-3xl">{{ opt.icon }}</span>
<span class="text-lg font-medium">{{ opt.label }}</span>
</button>
</div>
</div>
<!-- 故障描述 -->
<div>
<div class="flex items-center gap-4 mb-3">
<span class="text-lg text-text-secondary min-w-[100px]">故障描述</span>
<span class="text-sm text-text-muted">请描述故障现象</span>
</div>
<div class="ml-0 md:ml-[116px]">
<textarea
v-model="faultDescription"
placeholder="请输入故障描述,例如:投影仪无法开机,指示灯不亮..."
rows="4"
maxlength="500"
class="w-full px-4 py-3 bg-bg-input rounded-xl text-lg text-text-primary border border-border-subtle focus:border-status-free focus:outline-none resize-none"
/>
<div class="flex items-center justify-between mt-2">
<!-- 快捷选项 -->
<div class="flex gap-2 flex-wrap">
<button
v-for="quick in quickFaultOptions"
:key="quick"
class="px-3 py-1 text-sm bg-bg-input text-text-secondary rounded border border-border-subtle hover:text-text-primary hover:border-status-free transition-colors"
@click="quickFillFault(quick)"
>
{{ quick }}
</button>
</div>
<!-- 字数统计 -->
<span class="text-sm text-text-muted">
{{ faultDescription.length }} / 500
</span>
</div>
</div>
</div>
<!-- 选中设备预览 -->
<div
v-if="selectedDeviceInfo"
class="flex items-center gap-3 p-4 bg-bg-input rounded-xl"
>
<span class="text-3xl">{{ selectedDeviceInfo.icon }}</span>
<div>
<div class="text-lg text-text-primary font-medium">
{{ selectedDeviceInfo.label }}
</div>
<div class="text-sm text-text-secondary">
将创建IT工单并通知管理员
</div>
</div>
</div>
<!-- 提交失败提示 -->
<div
v-if="submitResult && !submitResult.success"
class="p-4 bg-status-busy/10 border border-status-busy/30 rounded-xl text-status-busy text-lg"
>
{{ submitResult.message }}
</div>
</div>
<!-- ================================================================ -->
<!-- 底部操作栏 -->
<!-- ================================================================ -->
<div
v-if="!submitResult?.success"
class="flex items-center justify-end gap-4 p-6 border-t border-border-subtle"
>
<button
class="px-8 py-3 rounded-xl text-button font-medium bg-bg-input text-text-secondary border border-border-subtle hover:text-text-primary transition-colors"
@click="goBack"
>
取消
</button>
<button
class="px-10 py-3 rounded-xl text-button font-bold transition-all"
:class="[
canSubmit
? 'bg-status-free text-white hover:scale-105'
: 'bg-text-muted text-text-secondary cursor-not-allowed',
]"
:disabled="!canSubmit"
@click="handleSubmit"
>
{{ submitting ? '提交中...' : '提交报修' }}
</button>
</div>
</div>
</div>
</template>