暂存
This commit is contained in:
@@ -4,6 +4,11 @@
|
||||
import {loadShare} from "@module-federation/runtime";
|
||||
const importMap = {
|
||||
|
||||
"axios": async () => {
|
||||
let pkg = await import("__mf__virtual/workcase__prebuild__axios__prebuild__.js");
|
||||
return pkg;
|
||||
}
|
||||
,
|
||||
"element-plus": async () => {
|
||||
let pkg = await import("__mf__virtual/workcase__prebuild__element_mf_2_plus__prebuild__.js");
|
||||
return pkg;
|
||||
@@ -22,6 +27,36 @@
|
||||
}
|
||||
const usedShared = {
|
||||
|
||||
"axios": {
|
||||
name: "axios",
|
||||
version: "1.13.2",
|
||||
scope: ["default"],
|
||||
loaded: false,
|
||||
from: "workcase",
|
||||
async get () {
|
||||
if (false) {
|
||||
throw new Error(`Shared module '${"axios"}' must be provided by host`);
|
||||
}
|
||||
usedShared["axios"].loaded = true
|
||||
const {"axios": pkgDynamicImport} = importMap
|
||||
const res = await pkgDynamicImport()
|
||||
const exportModule = {...res}
|
||||
// All npm packages pre-built by vite will be converted to esm
|
||||
Object.defineProperty(exportModule, "__esModule", {
|
||||
value: true,
|
||||
enumerable: false
|
||||
})
|
||||
return function () {
|
||||
return exportModule
|
||||
}
|
||||
},
|
||||
shareConfig: {
|
||||
singleton: false,
|
||||
requiredVersion: "^1.13.2",
|
||||
|
||||
}
|
||||
}
|
||||
,
|
||||
"element-plus": {
|
||||
name: "element-plus",
|
||||
version: "2.12.0",
|
||||
|
||||
@@ -18,45 +18,84 @@ const router = createRouter({
|
||||
let dynamicRoutesLoaded = false
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
console.log('[Workcase Router] 路由守卫触发:', {
|
||||
to: to.path,
|
||||
from: from.path,
|
||||
meta: to.meta
|
||||
meta: to.meta,
|
||||
query: to.query
|
||||
})
|
||||
|
||||
|
||||
// 设置页面标题
|
||||
if (to.meta.title) {
|
||||
document.title = `${to.meta.title} - 工单管理系统`
|
||||
}
|
||||
|
||||
|
||||
// 检查URL参数中是否有token(用于外部链接和小程序访问)
|
||||
const tokenParam = to.query.token as string | undefined
|
||||
|
||||
// 如果URL中有token,但localStorage中没有loginDomain,使用refresh接口验证
|
||||
if (tokenParam && !localStorage.getItem('loginDomain')) {
|
||||
console.log('[Workcase Router] 检测到token参数,尝试验证登录状态...')
|
||||
try {
|
||||
const response = await fetch('/api/urban-lifeline/auth/refresh', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${tokenParam}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json()
|
||||
if (result.success && result.data) {
|
||||
const loginDomain = result.data
|
||||
const newToken = loginDomain.token
|
||||
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('token', newToken)
|
||||
localStorage.setItem('loginDomain', JSON.stringify(loginDomain))
|
||||
TokenManager.setToken(newToken)
|
||||
|
||||
console.log('[Workcase Router] Token验证成功,登录状态已保存')
|
||||
} else {
|
||||
console.warn('[Workcase Router] Token验证失败:', result.message)
|
||||
}
|
||||
} else {
|
||||
console.warn('[Workcase Router] Token验证请求失败:', response.status)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Workcase Router] Token验证异常:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要登录
|
||||
const requiresAuth = to.meta.requiresAuth !== false
|
||||
const hasToken = TokenManager.hasToken()
|
||||
|
||||
|
||||
console.log('[Workcase Router] 认证检查:', {
|
||||
requiresAuth,
|
||||
hasToken,
|
||||
tokenValue: localStorage.getItem('token')
|
||||
})
|
||||
|
||||
|
||||
// 其他页面:检查是否需要登录
|
||||
if (requiresAuth && !hasToken) {
|
||||
// 需要登录但未登录,重定向到 platform 的登录页
|
||||
// 重要:必须使用完整URL(包含origin),避免被workcase的路由拦截造成循环
|
||||
const currentUrl = window.location.href
|
||||
const origin = window.location.origin
|
||||
|
||||
|
||||
// 构建platform登录页的完整URL
|
||||
const loginUrl = `${origin}/login?redirect=${encodeURIComponent(currentUrl)}`
|
||||
|
||||
|
||||
console.log('[Workcase Router] 未登录,重定向到Platform登录页:', loginUrl)
|
||||
|
||||
|
||||
// 使用完整URL跳转,跳出workcase的路由系统
|
||||
window.location.href = loginUrl
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 如果已登录且动态路由未加载,先加载动态路由
|
||||
if (hasToken && !dynamicRoutesLoaded) {
|
||||
console.log('[Workcase Router] 开始加载动态路由...')
|
||||
@@ -64,14 +103,14 @@ router.beforeEach((to, from, next) => {
|
||||
loginDomain: localStorage.getItem('loginDomain'),
|
||||
token: localStorage.getItem('token')
|
||||
})
|
||||
|
||||
|
||||
dynamicRoutesLoaded = true
|
||||
const loaded = loadRoutesFromStorage?.()
|
||||
|
||||
|
||||
console.log('[Workcase Router] 动态路由加载结果:', loaded)
|
||||
console.log('[Workcase Router] 当前路径:', to.path)
|
||||
console.log('[Workcase Router] 所有路由:', router.getRoutes().map(r => r.path))
|
||||
|
||||
|
||||
if (loaded) {
|
||||
if (to.path === '/') {
|
||||
// 访问根路径,重定向到第一个可用路由
|
||||
@@ -103,7 +142,7 @@ router.beforeEach((to, from, next) => {
|
||||
console.warn('[Workcase Router] 动态路由加载失败')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果已登录且访问根路径,但动态路由已加载,重定向到第一个可用路由
|
||||
if (hasToken && to.path === '/' && dynamicRoutesLoaded) {
|
||||
const firstRoute = getFirstAvailableRoute()
|
||||
@@ -114,7 +153,7 @@ router.beforeEach((to, from, next) => {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果访问 /admin,重定向到第一个 admin 路由
|
||||
if (hasToken && to.path === '/admin' && dynamicRoutesLoaded) {
|
||||
const firstAdminRoute = getFirstAdminRoute()
|
||||
@@ -124,7 +163,7 @@ router.beforeEach((to, from, next) => {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log('[Workcase Router] 继续正常导航')
|
||||
next()
|
||||
})
|
||||
|
||||
@@ -240,7 +240,7 @@ export interface VideoMeetingVO extends BaseVO {
|
||||
participantCount?: number
|
||||
maxParticipants?: number
|
||||
// 预定开始时间
|
||||
startTime?: string
|
||||
startTime?: string
|
||||
// 预定结束时间
|
||||
endTime?: string
|
||||
// 提前入会时间(分钟)
|
||||
@@ -249,7 +249,10 @@ export interface VideoMeetingVO extends BaseVO {
|
||||
actualEndTime?: string
|
||||
durationSeconds?: number
|
||||
durationFormatted?: string
|
||||
/** 会议页面URL(用于路由跳转) */
|
||||
iframeUrl?: string
|
||||
/** Jitsi真正的iframe URL(用于嵌入播放) */
|
||||
jitsiIframeUrl?: string
|
||||
config?: Record<string, any>
|
||||
}
|
||||
|
||||
|
||||
@@ -92,8 +92,6 @@
|
||||
:room-id="currentRoomId"
|
||||
:workcase-id="currentWorkcaseId"
|
||||
:room-name="currentRoom?.roomName"
|
||||
:meeting-url="currentMeetingUrl"
|
||||
:show-meeting="showMeetingIframe"
|
||||
:file-download-url="FILE_DOWNLOAD_URL"
|
||||
:has-more="hasMore"
|
||||
:loading-more="loadingMore"
|
||||
@@ -145,8 +143,13 @@
|
||||
title="工单详情"
|
||||
width="800px"
|
||||
class="workcase-dialog"
|
||||
destroy-on-close
|
||||
>
|
||||
<WorkcaseDetail :workcase-id="currentWorkcaseId" />
|
||||
<WorkcaseDetail
|
||||
mode="view"
|
||||
:workcase-id="currentWorkcaseId"
|
||||
@cancel="showWorkcaseDetail = false"
|
||||
/>
|
||||
</ElDialog>
|
||||
|
||||
<!-- 工单创建对话框 -->
|
||||
@@ -155,9 +158,14 @@
|
||||
title="创建工单"
|
||||
width="800px"
|
||||
class="workcase-dialog"
|
||||
destroy-on-close
|
||||
>
|
||||
<!-- TODO: 添加工单创建组件 -->
|
||||
<div>工单创建表单</div>
|
||||
<WorkcaseDetail
|
||||
mode="create"
|
||||
:room-id="currentRoomId!"
|
||||
@cancel="showWorkcaseCreator = false"
|
||||
@created="onWorkcaseCreated"
|
||||
/>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -247,11 +255,6 @@ const showWorkcaseDetail = ref(false)
|
||||
// 工单创建对话框
|
||||
const showWorkcaseCreator = ref(false)
|
||||
|
||||
// Jitsi Meet会议相关
|
||||
const currentMeetingUrl = ref('')
|
||||
const showMeetingIframe = ref(false)
|
||||
const currentMeetingId = ref<string | null>(null)
|
||||
|
||||
// ChatRoom组件引用
|
||||
const chatRoomRef = ref<InstanceType<typeof ChatRoom> | null>(null)
|
||||
|
||||
@@ -508,13 +511,15 @@ const onWorkcaseCreated = (workcaseId: string) => {
|
||||
if (currentRoom.value) {
|
||||
currentRoom.value.workcaseId = workcaseId
|
||||
}
|
||||
// 刷新聊天室列表
|
||||
fetchChatRooms()
|
||||
ElMessage.success('工单创建成功')
|
||||
}
|
||||
|
||||
// 发起会议
|
||||
const startMeeting = async () => {
|
||||
if (!currentRoomId.value) return
|
||||
|
||||
|
||||
try {
|
||||
// 先检查是否有活跃会议
|
||||
const activeResult = await workcaseChatAPI.getActiveMeeting(currentRoomId.value)
|
||||
@@ -523,31 +528,33 @@ const startMeeting = async () => {
|
||||
currentMeetingId.value = activeResult.data.meetingId!
|
||||
const joinResult = await workcaseChatAPI.joinVideoMeeting(currentMeetingId.value!)
|
||||
if (joinResult.success && joinResult.data?.iframeUrl) {
|
||||
currentMeetingUrl.value = joinResult.data.iframeUrl
|
||||
showMeetingIframe.value = true
|
||||
// 使用router跳转到JitsiMeetingView页面,附加roomId参数用于返回
|
||||
const meetingUrl = joinResult.data.iframeUrl + `&roomId=${currentRoomId.value}`
|
||||
router.push(meetingUrl)
|
||||
} else {
|
||||
ElMessage.error(joinResult.message || '加入会议失败')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 没有活跃会议,创建新会议
|
||||
const createResult = await workcaseChatAPI.createVideoMeeting({
|
||||
roomId: currentRoomId.value,
|
||||
meetingName: currentRoom.value?.roomName || '视频会议'
|
||||
})
|
||||
|
||||
|
||||
if (createResult.success && createResult.data) {
|
||||
currentMeetingId.value = createResult.data.meetingId!
|
||||
|
||||
|
||||
// 开始会议
|
||||
await workcaseChatAPI.startVideoMeeting(currentMeetingId.value!)
|
||||
|
||||
// 加入会议获取iframe URL
|
||||
|
||||
// 加入会议获取会议页面URL
|
||||
const joinResult = await workcaseChatAPI.joinVideoMeeting(currentMeetingId.value!)
|
||||
if (joinResult.success && joinResult.data?.iframeUrl) {
|
||||
currentMeetingUrl.value = joinResult.data.iframeUrl
|
||||
showMeetingIframe.value = true
|
||||
// 使用router跳转到JitsiMeetingView页面,附加roomId参数用于返回
|
||||
const meetingUrl = joinResult.data.iframeUrl + `&roomId=${currentRoomId.value}`
|
||||
router.push(meetingUrl)
|
||||
ElMessage.success('会议已创建')
|
||||
} else {
|
||||
ElMessage.error(joinResult.message || '获取会议链接失败')
|
||||
|
||||
@@ -79,11 +79,11 @@
|
||||
<!-- 发起会议按钮 -->
|
||||
<button
|
||||
class="action-btn"
|
||||
:disabled="meetingLoading || showMeeting"
|
||||
:disabled="meetingLoading"
|
||||
@click="handleStartMeeting"
|
||||
>
|
||||
<Video :size="18" />
|
||||
{{ showMeeting ? '会议进行中' : '发起会议' }}
|
||||
发起会议
|
||||
</button>
|
||||
|
||||
<!-- 额外的操作按钮插槽 -->
|
||||
@@ -137,48 +137,20 @@
|
||||
:workcase-id="workcaseId || ''"
|
||||
@success="handleMeetingCreated"
|
||||
/>
|
||||
|
||||
<!-- 视频会议弹窗 -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showMeeting && meetingUrl" class="meeting-modal-mask">
|
||||
<div class="meeting-modal">
|
||||
<div class="meeting-modal-header">
|
||||
<span class="meeting-modal-title">
|
||||
<Video :size="18" />
|
||||
视频会议进行中
|
||||
</span>
|
||||
<div class="meeting-modal-actions">
|
||||
<button class="minimize-btn" @click="minimizeMeeting" title="最小化">
|
||||
<Minus :size="18" />
|
||||
</button>
|
||||
<button class="close-meeting-btn" @click="handleEndMeeting" title="结束会议">
|
||||
<X :size="18" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meeting-modal-body">
|
||||
<IframeView :url="meetingUrl" class="meeting-iframe" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- 最小化的会议悬浮按钮 -->
|
||||
<div v-if="meetingMinimized && meetingUrl" class="meeting-float-btn" @click="restoreMeeting">
|
||||
<Video :size="20" />
|
||||
<span>返回会议</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, onMounted } from 'vue'
|
||||
import { FileText, Video, Paperclip, Send, X, Minus } from 'lucide-vue-next'
|
||||
import IframeView from 'shared/components/iframe/IframeView.vue'
|
||||
import { ref, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { FileText, Video, Paperclip, Send } from 'lucide-vue-next'
|
||||
import MeetingCreate from '../MeetingCreate/MeetingCreate.vue'
|
||||
import MeetingCard from '../MeetingCard/MeetingCard.vue'
|
||||
import type { ChatRoomMessageVO, VideoMeetingVO } from '@/types/workcase'
|
||||
import { workcaseChatAPI } from '@/api/workcase'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface Props {
|
||||
messages: ChatRoomMessageVO[]
|
||||
@@ -204,37 +176,17 @@ const emit = defineEmits<{
|
||||
'send-message': [content: string, files: File[]]
|
||||
'download-file': [fileId: string]
|
||||
'load-more': []
|
||||
'start-meeting': []
|
||||
}>()
|
||||
|
||||
// 会议相关状态
|
||||
const showMeeting = ref(false)
|
||||
const meetingUrl = ref('')
|
||||
const currentMeetingId = ref('')
|
||||
const meetingLoading = ref(false)
|
||||
const showMeetingCreate = ref(false)
|
||||
const meetingMinimized = ref(false)
|
||||
|
||||
// 最小化会议
|
||||
const minimizeMeeting = () => {
|
||||
meetingMinimized.value = true
|
||||
showMeeting.value = false
|
||||
}
|
||||
|
||||
// 恢复会议窗口
|
||||
const restoreMeeting = () => {
|
||||
meetingMinimized.value = false
|
||||
showMeeting.value = true
|
||||
}
|
||||
|
||||
// 打开创建会议对话框
|
||||
// 打开创建会议对话框或直接emit事件给父组件处理
|
||||
const handleStartMeeting = () => {
|
||||
// 先检查是否有活跃会议
|
||||
checkActiveMeeting().then(() => {
|
||||
if (!showMeeting.value) {
|
||||
// 没有活跃会议,打开创建对话框
|
||||
showMeetingCreate.value = true
|
||||
}
|
||||
})
|
||||
// emit事件给父组件,让父组件处理会议逻辑
|
||||
emit('start-meeting')
|
||||
}
|
||||
|
||||
// 会议创建成功回调
|
||||
@@ -245,40 +197,6 @@ const handleMeetingCreated = async (meetingId: string) => {
|
||||
// 会议消息会通过后端发送到聊天室,用户可以点击消息卡片加入
|
||||
}
|
||||
|
||||
// 结束会议
|
||||
const handleEndMeeting = async () => {
|
||||
if (!currentMeetingId.value) return
|
||||
|
||||
try {
|
||||
await workcaseChatAPI.endVideoMeeting(currentMeetingId.value)
|
||||
showMeeting.value = false
|
||||
meetingUrl.value = ''
|
||||
currentMeetingId.value = ''
|
||||
meetingMinimized.value = false
|
||||
} catch (error) {
|
||||
console.error('结束会议失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有活跃会议
|
||||
const checkActiveMeeting = async () => {
|
||||
try {
|
||||
const res = await workcaseChatAPI.getActiveMeeting(props.roomId)
|
||||
if (res.code === 0 && res.data) {
|
||||
currentMeetingId.value = res.data.meetingId
|
||||
meetingUrl.value = res.data.iframeUrl
|
||||
showMeeting.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('无活跃会议')
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时检查是否有活跃会议
|
||||
onMounted(() => {
|
||||
checkActiveMeeting()
|
||||
})
|
||||
|
||||
// 滚动到顶部加载更多
|
||||
const handleScroll = (e: Event) => {
|
||||
const target = e.target as HTMLElement
|
||||
@@ -370,34 +288,36 @@ const formatTime = (time?: string) => {
|
||||
// 处理从MeetingCard发出的加入会议事件
|
||||
const handleJoinMeeting = async (meetingId: string) => {
|
||||
try {
|
||||
// 调用加入会议接口获取iframe URL
|
||||
meetingLoading.value = true
|
||||
// 调用加入会议接口获取会议页面URL
|
||||
const joinRes = await workcaseChatAPI.joinVideoMeeting(meetingId)
|
||||
if (joinRes.success && joinRes.data) {
|
||||
// 检查会议状态
|
||||
const meetingData = joinRes.data
|
||||
if (meetingData.status === 'ended') {
|
||||
// 会议已结束,提示用户
|
||||
alert('该会议已结束')
|
||||
ElMessage.warning('该会议已结束')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (!meetingData.iframeUrl) {
|
||||
console.error('加入会议失败: 未获取到会议地址')
|
||||
alert('加入会议失败:未获取到会议地址')
|
||||
ElMessage.error('加入会议失败:未获取到会议地址')
|
||||
return
|
||||
}
|
||||
|
||||
currentMeetingId.value = meetingId
|
||||
meetingUrl.value = meetingData.iframeUrl
|
||||
showMeeting.value = true
|
||||
meetingMinimized.value = false
|
||||
|
||||
// 使用router跳转到JitsiMeetingView页面,附加roomId参数用于返回
|
||||
const meetingUrl = meetingData.iframeUrl + `&roomId=${props.roomId}`
|
||||
router.push(meetingUrl)
|
||||
} else {
|
||||
console.error('加入会议失败:', joinRes.message)
|
||||
alert(joinRes.message || '加入会议失败')
|
||||
ElMessage.error(joinRes.message || '加入会议失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加入会议失败:', error)
|
||||
alert('加入会议失败,请稍后重试')
|
||||
ElMessage.error('加入会议失败,请稍后重试')
|
||||
} finally {
|
||||
meetingLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -457,8 +377,7 @@ const renderMarkdown = (text: string): string => {
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
scrollToBottom,
|
||||
handleStartMeeting,
|
||||
handleEndMeeting
|
||||
handleStartMeeting
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
.jitsi-meeting-view {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.loading-container,
|
||||
.error-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.3);
|
||||
border-top-color: #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 20px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
font-size: 16px;
|
||||
color: #ccc;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.error-btn {
|
||||
padding: 10px 30px;
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
background: #409eff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #66b1ff;
|
||||
}
|
||||
}
|
||||
|
||||
.meeting-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
// Jitsi External API 会在这个容器内创建 iframe
|
||||
#jitsi-meet-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
<template>
|
||||
<div class="jitsi-meeting-view">
|
||||
<!-- 加载中 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">正在加载会议...</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-else-if="error" class="error-container">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<div class="error-title">无法加入会议</div>
|
||||
<div class="error-message">{{ error }}</div>
|
||||
<button class="error-btn" @click="retryJoin">重新尝试</button>
|
||||
</div>
|
||||
|
||||
<!-- Jitsi 会议容器 -->
|
||||
<div v-else ref="jitsiContainer" id="jitsi-meet-container" class="meeting-container"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { workcaseChatAPI } from '@/api/workcase'
|
||||
// @ts-ignore
|
||||
import { TokenManager } from 'shared/api'
|
||||
import axios from 'axios'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
const meetingId = ref('')
|
||||
const roomId = ref('')
|
||||
const jitsiContainer = ref<HTMLDivElement | null>(null)
|
||||
let jitsiApi: any = null
|
||||
|
||||
// 从URL获取参数
|
||||
const getMeetingParams = () => {
|
||||
// 优先从query参数获取
|
||||
meetingId.value = route.query.meetingId as string || ''
|
||||
roomId.value = route.query.roomId as string || ''
|
||||
const tokenParam = route.query.token as string || ''
|
||||
|
||||
console.log('[JitsiMeetingView] URL参数:', {
|
||||
meetingId: meetingId.value,
|
||||
roomId: roomId.value,
|
||||
hasToken: !!tokenParam
|
||||
})
|
||||
|
||||
return { meetingId: meetingId.value, roomId: roomId.value, token: tokenParam }
|
||||
}
|
||||
|
||||
// 使用token刷新登录状态
|
||||
const refreshLoginWithToken = async (token: string) => {
|
||||
try {
|
||||
console.log('[JitsiMeetingView] 使用token刷新登录状态...')
|
||||
|
||||
const response = await axios.post(
|
||||
'/api/urban-lifeline/auth/refresh',
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
if (response.data?.success && response.data?.data) {
|
||||
const loginDomain = response.data.data
|
||||
const newToken = loginDomain.token
|
||||
|
||||
// 保存到localStorage
|
||||
localStorage.setItem('token', newToken)
|
||||
localStorage.setItem('loginDomain', JSON.stringify(loginDomain))
|
||||
|
||||
// 使用TokenManager设置token
|
||||
TokenManager.setToken(newToken)
|
||||
|
||||
console.log('[JitsiMeetingView] 登录状态刷新成功')
|
||||
return true
|
||||
} else {
|
||||
console.error('[JitsiMeetingView] 刷新登录失败:', response.data?.message)
|
||||
return false
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('[JitsiMeetingView] 刷新登录异常:', err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载 Jitsi External API 脚本
|
||||
const loadJitsiScript = (): Promise<void> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 检查是否已经加载
|
||||
if ((window as any).JitsiMeetExternalAPI) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
const script = document.createElement('script')
|
||||
script.src = 'http://localhost:8280/external_api.js'
|
||||
script.async = true
|
||||
script.onload = () => {
|
||||
console.log('[JitsiMeetingView] Jitsi External API 脚本加载成功')
|
||||
resolve()
|
||||
}
|
||||
script.onerror = () => {
|
||||
reject(new Error('加载 Jitsi External API 失败'))
|
||||
}
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
// 初始化 Jitsi Meet
|
||||
const initJitsiMeet = async (jitsiServerUrl: string, roomName: string, jwt: string, displayName: string) => {
|
||||
try {
|
||||
console.log('[JitsiMeetingView] 初始化 Jitsi Meet:', {
|
||||
server: jitsiServerUrl,
|
||||
room: roomName,
|
||||
name: displayName
|
||||
})
|
||||
|
||||
// 加载 External API 脚本
|
||||
await loadJitsiScript()
|
||||
|
||||
const JitsiMeetExternalAPI = (window as any).JitsiMeetExternalAPI
|
||||
|
||||
// 解析 URL 获取协议和域名
|
||||
const urlObj = new URL(jitsiServerUrl)
|
||||
const domain = urlObj.host // 获取 host (localhost:8280)
|
||||
const useHttps = urlObj.protocol === 'https:'
|
||||
|
||||
console.log('[JitsiMeetingView] 解析服务器配置:', {
|
||||
domain,
|
||||
protocol: urlObj.protocol,
|
||||
useHttps
|
||||
})
|
||||
|
||||
// 配置选项 - 关键!指定是否使用 HTTPS
|
||||
// 👇 替换你原有的 options 全部代码,保留原有逻辑,仅修改配置项
|
||||
const options: any = {
|
||||
roomName: roomName,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
parentNode: jitsiContainer.value,
|
||||
jwt: jwt,
|
||||
// ✅ 修复1:核心!正确的顶层配置项是 useHTTPS(不是 https),强制关闭HTTPS
|
||||
useHTTPS: false,
|
||||
// ✅ 修复2:禁用WebSocket的HTTPS,彻底阻止wss://请求(局域网必加)
|
||||
useWebSocket: false,
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: false,
|
||||
startWithVideoMuted: false,
|
||||
enableWelcomePage: false,
|
||||
prejoinPageEnabled: false,
|
||||
disableDeepLinking: true,
|
||||
enableChat: true,
|
||||
enableScreenSharing: true,
|
||||
// ✅ 修复3:叠加禁用,彻底阻断API内部的HTTPS强制逻辑(局域网核心)
|
||||
useHTTPS: false,
|
||||
// ✅ 修复4:禁用第三方HTTPS资源请求,避免混合内容报错
|
||||
disableThirdPartyRequests: false,
|
||||
// ✅ 修复5:关闭服务端的HTTPS重定向检测
|
||||
disableHttpsRedirect: true
|
||||
},
|
||||
interfaceConfigOverwrite: {
|
||||
SHOW_JITSI_WATERMARK: false,
|
||||
SHOW_WATERMARK_FOR_GUESTS: false,
|
||||
DISABLE_JOIN_LEAVE_NOTIFICATIONS: false
|
||||
},
|
||||
userInfo: {
|
||||
displayName: displayName
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[JitsiMeetingView] 创建 JitsiMeetExternalAPI 实例,https=' + useHttps)
|
||||
jitsiApi = new JitsiMeetExternalAPI(domain, options)
|
||||
|
||||
// 监听会议准备就绪事件
|
||||
jitsiApi.addEventListener('videoConferenceJoined', (event: any) => {
|
||||
console.log('[JitsiMeetingView] 用户已加入会议:', event)
|
||||
})
|
||||
|
||||
// 监听用户离开会议事件
|
||||
jitsiApi.addEventListener('videoConferenceLeft', (event: any) => {
|
||||
console.log('[JitsiMeetingView] 用户离开会议:', event)
|
||||
handleLeaveMeeting()
|
||||
})
|
||||
|
||||
// 监听准备关闭事件
|
||||
jitsiApi.addEventListener('readyToClose', () => {
|
||||
console.log('[JitsiMeetingView] Jitsi 准备关闭')
|
||||
handleLeaveMeeting()
|
||||
})
|
||||
|
||||
console.log('[JitsiMeetingView] Jitsi Meet 初始化完成')
|
||||
} catch (err: any) {
|
||||
console.error('[JitsiMeetingView] 初始化 Jitsi Meet 失败:', err)
|
||||
error.value = err.message || '初始化会议失败'
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加入会议
|
||||
const joinMeeting = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
const { meetingId: mid, roomId: rid, token } = getMeetingParams()
|
||||
|
||||
if (!mid) {
|
||||
error.value = '缺少会议ID参数'
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有loginDomain
|
||||
const hasLoginDomain = !!localStorage.getItem('loginDomain')
|
||||
const hasToken = !!localStorage.getItem('token') || !!token
|
||||
|
||||
console.log('[JitsiMeetingView] 登录状态检查:', {
|
||||
hasLoginDomain,
|
||||
hasToken
|
||||
})
|
||||
|
||||
// 如果没有loginDomain但有token(小程序或外部链接访问),先刷新登录
|
||||
if (!hasLoginDomain && token) {
|
||||
const refreshed = await refreshLoginWithToken(token)
|
||||
if (!refreshed) {
|
||||
error.value = '登录验证失败,请重新获取会议链接'
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
if (!TokenManager.hasToken()) {
|
||||
error.value = '未登录,请先登录'
|
||||
loading.value = false
|
||||
// 重定向到登录页
|
||||
const currentUrl = window.location.href
|
||||
window.location.href = `/login?redirect=${encodeURIComponent(currentUrl)}`
|
||||
return
|
||||
}
|
||||
|
||||
// 调用后端接口加入会议
|
||||
console.log('[JitsiMeetingView] 正在加入会议:', mid)
|
||||
const result = await workcaseChatAPI.joinVideoMeeting(mid)
|
||||
|
||||
if (result.success && result.data) {
|
||||
const meetingData = result.data
|
||||
|
||||
// 检查会议数据
|
||||
if (!meetingData.jitsiServerUrl || !meetingData.jitsiRoomName || !meetingData.jwtToken) {
|
||||
error.value = '会议数据不完整,无法加入'
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 获取用户名
|
||||
const loginDomain = JSON.parse(localStorage.getItem('loginDomain') || '{}')
|
||||
const displayName = loginDomain.user?.userName || '访客'
|
||||
|
||||
// 先设置 loading 为 false,让容器渲染出来
|
||||
loading.value = false
|
||||
|
||||
// 等待下一个 tick,确保 DOM 已渲染
|
||||
await nextTick()
|
||||
|
||||
// 检查容器是否已经渲染
|
||||
if (!jitsiContainer.value) {
|
||||
error.value = '会议容器未准备好'
|
||||
return
|
||||
}
|
||||
|
||||
// 使用 Jitsi External API 初始化会议
|
||||
await initJitsiMeet(
|
||||
meetingData.jitsiServerUrl,
|
||||
meetingData.jitsiRoomName,
|
||||
meetingData.jwtToken,
|
||||
displayName
|
||||
)
|
||||
} else {
|
||||
error.value = result.message || '加入会议失败'
|
||||
loading.value = false
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('[JitsiMeetingView] 加入会议异常:', err)
|
||||
error.value = err.message || '加入会议失败'
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理离开会议(不结束)
|
||||
const handleLeaveMeeting = () => {
|
||||
console.log('[JitsiMeetingView] 处理离开会议,返回聊天室:', roomId.value)
|
||||
|
||||
// 清理 Jitsi API
|
||||
if (jitsiApi) {
|
||||
jitsiApi.dispose()
|
||||
jitsiApi = null
|
||||
}
|
||||
|
||||
// 如果有roomId,返回到对应的聊天室
|
||||
if (roomId.value) {
|
||||
router.push(`/chatRoom?roomId=${roomId.value}`)
|
||||
} else {
|
||||
// 没有roomId,尝试返回上一页或关闭窗口
|
||||
if (window.history.length > 1) {
|
||||
router.back()
|
||||
} else {
|
||||
window.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理结束会议
|
||||
const handleEndMeeting = async () => {
|
||||
try {
|
||||
console.log('[JitsiMeetingView] 处理结束会议:', meetingId.value)
|
||||
|
||||
if (meetingId.value) {
|
||||
// 调用后端接口结束会议
|
||||
const result = await workcaseChatAPI.endVideoMeeting(meetingId.value)
|
||||
console.log('[JitsiMeetingView] 结束会议结果:', result)
|
||||
}
|
||||
|
||||
// 返回上一页或关闭窗口
|
||||
handleLeaveMeeting()
|
||||
} catch (err) {
|
||||
console.error('[JitsiMeetingView] 结束会议失败:', err)
|
||||
// 即使失败也返回上一页
|
||||
handleLeaveMeeting()
|
||||
}
|
||||
}
|
||||
|
||||
// 重新尝试加入
|
||||
const retryJoin = () => {
|
||||
joinMeeting()
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
console.log('[JitsiMeetingView] 组件挂载')
|
||||
joinMeeting()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
console.log('[JitsiMeetingView] 组件卸载')
|
||||
// 清理 Jitsi API
|
||||
if (jitsiApi) {
|
||||
jitsiApi.dispose()
|
||||
jitsiApi = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import url('./JitsiMeetingView.scss')
|
||||
</style>
|
||||
@@ -51,7 +51,9 @@
|
||||
</div>
|
||||
<div class="table-label">故障类型</div>
|
||||
<div class="table-value">
|
||||
<ElInput v-if="mode !== 'view'" v-model="formData.type" placeholder="请输入故障类型" size="small" />
|
||||
<ElSelect v-if="mode !== 'view'" v-model="formData.type" placeholder="请选择故障类型" size="small">
|
||||
<ElOption v-for="item in faultTypes" :key="item" :label="item" :value="item" />
|
||||
</ElSelect>
|
||||
<span v-else>{{ formData.type || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -141,22 +143,24 @@
|
||||
</div>
|
||||
|
||||
<!-- 处理记录 (仅查看模式) -->
|
||||
<div class="timeline-section" v-if="mode === 'view' && timeline.length">
|
||||
<div class="timeline-section" v-if="mode === 'view' && processList.length > 0">
|
||||
<div class="section-title">
|
||||
<div class="title-bar"></div>
|
||||
处理记录
|
||||
</div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-line"></div>
|
||||
<div v-for="(item, index) in timeline" :key="index" class="timeline-item">
|
||||
<div class="timeline-dot" :class="`timeline-dot-${item.status}`"></div>
|
||||
<div v-for="(item, index) in processList" :key="index" class="timeline-item">
|
||||
<div class="timeline-dot" :class="getTimelineDotClass(item.action)"></div>
|
||||
<div class="timeline-line" v-if="index < processList.length - 1"></div>
|
||||
<div class="timeline-body">
|
||||
<div class="timeline-header">
|
||||
<span class="timeline-actor">{{ item.title.split(' ')[0] }}</span>
|
||||
<span class="timeline-action">{{ item.title.split(' ').slice(1).join(' ') }}</span>
|
||||
<span class="timeline-time">{{ getTime(item.createTime) }}</span>
|
||||
<span class="timeline-date">{{ getDate(item.createTime) }}</span>
|
||||
</div>
|
||||
<div class="timeline-info">
|
||||
<span class="timeline-action">{{ getActionText(item.action) }}:</span>
|
||||
<span class="timeline-desc">{{ item.message }}</span>
|
||||
</div>
|
||||
<div class="timeline-desc">{{ item.desc }}</div>
|
||||
<div class="timeline-time">{{ item.time }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,27 +184,25 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { ChatMessage } from '@/views/public/ChatRoom/'
|
||||
import { ElButton, ElInput, ElSelect, ElOption, ElDialog, ElMessage } from 'element-plus'
|
||||
import { ElButton, ElInput, ElSelect, ElOption, ElDialog, ElMessage, ElLoading } from 'element-plus'
|
||||
import { MessageSquare, ImageIcon as ImageIcon, Plus } from 'lucide-vue-next'
|
||||
import type { TbWorkcaseDTO } from '@/types/workcase/workcase'
|
||||
import type { TbWorkcaseDTO, TbWorkcaseProcessDTO } from '@/types/workcase/workcase'
|
||||
import { workcaseAPI } from '@/api/workcase'
|
||||
import { FILE_DOWNLOAD_URL } from '@/config'
|
||||
|
||||
interface TimelineItem {
|
||||
status: 'system' | 'manager' | 'engineer'
|
||||
title: string
|
||||
desc: string
|
||||
time: string
|
||||
}
|
||||
|
||||
interface Props {
|
||||
mode?: 'view' | 'edit' | 'create'
|
||||
workcase?: TbWorkcaseDTO
|
||||
workcaseId?: string // 查看/编辑模式传入 workcaseId,组件内部加载数据
|
||||
roomId?: string // 创建模式传入 roomId
|
||||
workcase?: TbWorkcaseDTO // 兼容旧用法,直接传入数据
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
mode: 'view',
|
||||
workcaseId: '',
|
||||
roomId: '',
|
||||
workcase: () => ({} as TbWorkcaseDTO)
|
||||
})
|
||||
|
||||
@@ -209,22 +211,98 @@ const emit = defineEmits<{
|
||||
submit: [data: TbWorkcaseDTO]
|
||||
assign: [workcaseId: string]
|
||||
complete: [workcaseId: string]
|
||||
created: [workcaseId: string] // 创建成功事件
|
||||
}>()
|
||||
|
||||
const loading = ref(false)
|
||||
const formData = ref<TbWorkcaseDTO>({
|
||||
...props.workcase
|
||||
})
|
||||
|
||||
// 故障类型选项(与微信端保持一致)
|
||||
const faultTypes = ['电气系统故障', '机械故障', '控制系统故障', '油路系统故障', '其他故障']
|
||||
|
||||
const showChatMessage = ref(false)
|
||||
const currentRoomId = ref<string>('')
|
||||
const timeline = ref<TimelineItem[]>([
|
||||
{
|
||||
status: 'system',
|
||||
title: '系统 工单创建',
|
||||
desc: '客户通过小电对话提交',
|
||||
time: ''
|
||||
const processList = ref<TbWorkcaseProcessDTO[]>([])
|
||||
|
||||
// 加载工单详情
|
||||
const loadWorkcaseDetail = async (workcaseId: string) => {
|
||||
if (!workcaseId) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await workcaseAPI.getWorkcaseById(workcaseId)
|
||||
if (res.success && res.data) {
|
||||
formData.value = res.data
|
||||
currentRoomId.value = res.data.roomId || ''
|
||||
// 加载处理记录
|
||||
await loadProcessList(workcaseId)
|
||||
} else {
|
||||
ElMessage.error(res.message || '加载工单详情失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载工单详情失败:', error)
|
||||
ElMessage.error('加载工单详情失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
// 加载处理记录
|
||||
const loadProcessList = async (workcaseId: string) => {
|
||||
try {
|
||||
const res = await workcaseAPI.getWorkcaseProcessList({ workcaseId })
|
||||
if (res.success && res.dataList) {
|
||||
processList.value = res.dataList
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载处理记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取时间线圆点样式类
|
||||
const getTimelineDotClass = (action?: string): string => {
|
||||
switch (action) {
|
||||
case 'create':
|
||||
case 'info':
|
||||
return 'dot-system'
|
||||
case 'assign':
|
||||
case 'redeploy':
|
||||
return 'dot-manager'
|
||||
case 'finish':
|
||||
return 'dot-engineer'
|
||||
case 'repeal':
|
||||
return 'dot-cancel'
|
||||
default:
|
||||
return 'dot-system'
|
||||
}
|
||||
}
|
||||
|
||||
// 获取动作文本
|
||||
const getActionText = (action?: string): string => {
|
||||
const map: Record<string, string> = {
|
||||
create: '工单创建',
|
||||
info: '记录信息',
|
||||
assign: '指派工程师',
|
||||
redeploy: '转派工程师',
|
||||
repeal: '撤销工单',
|
||||
finish: '完成工单'
|
||||
}
|
||||
return map[action || ''] || '未知操作'
|
||||
}
|
||||
|
||||
// 获取时间部分
|
||||
const getTime = (datetime?: string): string => {
|
||||
if (!datetime) return ''
|
||||
return datetime.split(' ')[1] || ''
|
||||
}
|
||||
|
||||
// 获取日期部分
|
||||
const getDate = (datetime?: string): string => {
|
||||
if (!datetime) return ''
|
||||
return datetime.split(' ')[0] || ''
|
||||
}
|
||||
|
||||
function getImageUrl(fileId: string): string {
|
||||
if (!fileId) return ''
|
||||
@@ -234,26 +312,46 @@ function getImageUrl(fileId: string): string {
|
||||
return `${FILE_DOWNLOAD_URL}${fileId}`
|
||||
}
|
||||
|
||||
// 根据 workcaseId 获取聊天室ID
|
||||
const loadChatRoom = async () => {
|
||||
if (!formData.value.workcaseId) return
|
||||
|
||||
try {
|
||||
// TODO: 调用 API 根据 workcaseId 查询聊天室
|
||||
// const res = await workcaseChatAPI.getChatRoomByWorkcaseId(formData.value.workcaseId)
|
||||
// if (res.success && res.data) {
|
||||
// currentRoomId.value = res.data.roomId || ''
|
||||
// }
|
||||
|
||||
// 临时:假设 roomId 和 workcaseId 相关联
|
||||
console.log('需要根据 workcaseId 查询聊天室:', formData.value.workcaseId)
|
||||
} catch (error) {
|
||||
console.error('加载聊天室失败:', error)
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
if (props.mode === 'view' || props.mode === 'edit') {
|
||||
// 查看/编辑模式:通过 workcaseId 加载数据
|
||||
if (props.workcaseId) {
|
||||
loadWorkcaseDetail(props.workcaseId)
|
||||
} else if (props.workcase?.workcaseId) {
|
||||
// 兼容旧用法
|
||||
formData.value = { ...props.workcase }
|
||||
currentRoomId.value = props.workcase.roomId || ''
|
||||
if (props.workcase.workcaseId) {
|
||||
loadProcessList(props.workcase.workcaseId)
|
||||
}
|
||||
}
|
||||
} else if (props.mode === 'create') {
|
||||
// 创建模式:初始化空表单,设置 roomId
|
||||
const loginDomain = JSON.parse(localStorage.getItem('loginDomain') || '{}')
|
||||
formData.value = {
|
||||
roomId: props.roomId,
|
||||
username: loginDomain?.userInfo?.username || '',
|
||||
phone: loginDomain?.user?.phone || '',
|
||||
userId: loginDomain?.user?.userId || '',
|
||||
emergency: 'normal',
|
||||
imgs: []
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 workcaseId 变化
|
||||
watch(() => props.workcaseId, (newVal) => {
|
||||
if (newVal && (props.mode === 'view' || props.mode === 'edit')) {
|
||||
loadWorkcaseDetail(newVal)
|
||||
}
|
||||
})
|
||||
|
||||
// 兼容旧用法:监听 workcase prop 变化
|
||||
watch(() => props.workcase, (newVal) => {
|
||||
formData.value = { ...newVal }
|
||||
if (newVal && !props.workcaseId) {
|
||||
formData.value = { ...newVal }
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
const statusLabel = (status: string) => {
|
||||
@@ -304,13 +402,55 @@ const handleCancel = () => {
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
const handleSubmit = async () => {
|
||||
if (props.mode === 'create' || props.mode === 'edit') {
|
||||
if (!formData.value.username || !formData.value.phone) {
|
||||
ElMessage.warning('请填写必填项')
|
||||
// 校验必填项
|
||||
if (!formData.value.username) {
|
||||
ElMessage.warning('请输入客户姓名')
|
||||
return
|
||||
}
|
||||
emit('submit', formData.value)
|
||||
if (!formData.value.phone) {
|
||||
ElMessage.warning('请输入联系电话')
|
||||
return
|
||||
}
|
||||
if (!formData.value.device) {
|
||||
ElMessage.warning('请输入设备名称')
|
||||
return
|
||||
}
|
||||
if (!formData.value.type) {
|
||||
ElMessage.warning('请选择故障类型')
|
||||
return
|
||||
}
|
||||
if (!formData.value.address) {
|
||||
ElMessage.warning('请输入现场地址')
|
||||
return
|
||||
}
|
||||
if (!formData.value.description) {
|
||||
ElMessage.warning('请输入故障描述')
|
||||
return
|
||||
}
|
||||
|
||||
if (props.mode === 'create') {
|
||||
// 创建工单
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await workcaseAPI.createWorkcase(formData.value)
|
||||
if (res.success && res.data) {
|
||||
ElMessage.success('工单创建成功')
|
||||
emit('created', res.data.workcaseId!)
|
||||
} else {
|
||||
ElMessage.error(res.message || '创建失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建工单失败:', error)
|
||||
ElMessage.error('创建工单失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} else {
|
||||
// 编辑模式,触发 submit 事件
|
||||
emit('submit', formData.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user