Commit de2d3a59 authored by 崔婷婷's avatar 崔婷婷

对应后端多通道前端接口文档

parent 46d5f76b
# 前端唯一环境变量:后端 API 基础地址
# 默认值为空,表示使用当前页面同源地址
# 生产环境推荐前后端同源部署,无需设置此变量
VITE_API_BASE_URL=
# Mock 模式:设为 true 时使用本地模拟数据,无需启动后端
# 开发调试时使用,生产部署时不要设置
VITE_USE_MOCK=true
......@@ -10,7 +10,9 @@ lerna-debug.log*
node_modules
dist
dist-ssr
*.local
.env
.env.local
.env.*.local
# Editor directories and files
.vscode/*
......
......@@ -6,43 +6,89 @@
<div class="header-right">{{ currentTime }}</div>
</header>
<!-- 双工位面板 -->
<main class="main">
<WorkstationPanel
:ws-url="wsUrl1"
:video-url="videoUrl1"
/>
<!-- 加载中 -->
<main v-if="loading" class="main loading-main">
<span class="loading-text">正在发现检测通道…</span>
</main>
<!-- 通道错误 -->
<main v-else-if="errorMsg" class="main loading-main">
<span class="loading-text error-text">{{ errorMsg }}</span>
</main>
<!-- 动态通道面板:N 个通道 = N 个面板 -->
<main v-else class="main" :style="mainGridStyle">
<WorkstationPanel
:ws-url="wsUrl2"
:video-url="videoUrl2"
v-for="ch in channels"
:key="ch.channel_id"
:channel="ch"
/>
</main>
</div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
import WorkstationPanel from './WorkstationPanel.vue'
const wsUrl1 =
import.meta.env.VITE_WS_URL_STATION_1 ||
import.meta.env.VITE_WS_URL ||
'ws://127.0.0.1:8080/ws?station=1'
const USE_MOCK = import.meta.env.VITE_USE_MOCK === 'true'
const API_BASE = (import.meta.env.VITE_API_BASE_URL || '').replace(/\/+$/, '')
const channels = ref([])
const loading = ref(true)
const errorMsg = ref('')
const wsUrl2 =
import.meta.env.VITE_WS_URL_STATION_2 ||
import.meta.env.VITE_WS_URL ||
'ws://127.0.0.1:8080/ws?station=2'
// 获取通道列表,只展示 enabled=true 的通道,按 order 排序
async function fetchChannels() {
loading.value = true
errorMsg.value = ''
const videoUrl1 =
import.meta.env.VITE_VIDEO_URL_STATION_1 ||
import.meta.env.VITE_VIDEO_URL ||
'http://127.0.0.1:8080/stream/video?station=1'
if (USE_MOCK) {
// Mock 模式:动态导入本地数据(生产构建时整个 mock 模块会被 tree-shaking 移除)
console.log('[QcDashboard] Mock 模式,使用本地通道数据')
const { mockChannelsResponse } = await import('../mock/index.js')
channels.value = (mockChannelsResponse.channels || [])
.filter(c => c.enabled === true)
.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
loading.value = false
return
}
// 真实模式:请求后端
try {
const resp = await fetch(`${API_BASE}/api/v1/channels`)
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
const text = await resp.text()
try {
const data = JSON.parse(text)
channels.value = (data.channels || [])
.filter(c => c.enabled === true)
.sort((a, b) => (a.order ?? 999) - (b.order ?? 999))
if (channels.value.length === 0) {
errorMsg.value = '未发现可用检测通道'
}
} catch (parseErr) {
if (text.trim().startsWith('<!doctype') || text.trim().startsWith('<')) {
console.error('[QcDashboard] 收到 HTML 而非 JSON —— 请确认后端已启动且 Vite 代理已配置')
}
throw parseErr
}
} catch (e) {
console.error('[QcDashboard] 获取通道列表失败', e)
if (e.message?.includes('JSON')) {
errorMsg.value = '无法连接后端服务:收到非 JSON 响应。请确认 Vite 代理已配置(vite.config.js → server.proxy),且后端服务已启动'
} else {
errorMsg.value = `无法连接后端服务:${e.message}`
}
} finally {
loading.value = false
}
}
const videoUrl2 =
import.meta.env.VITE_VIDEO_URL_STATION_2 ||
import.meta.env.VITE_VIDEO_URL ||
'http://127.0.0.1:8080/stream/video'
// 动态网格:1 列 / 2 列 / N 列
const mainGridStyle = computed(() => ({
gridTemplateColumns: `repeat(${channels.value.length || 1}, 1fr)`,
}))
const currentTime = ref('')
let clockTimer = null
......@@ -56,6 +102,7 @@ function updateClock() {
onMounted(() => {
updateClock()
clockTimer = setInterval(updateClock, 1000)
fetchChannels()
})
onBeforeUnmount(() => {
......@@ -109,12 +156,24 @@ onBeforeUnmount(() => {
color: var(--dim);
}
/* ===== main: 双工位并排 ===== */
/* ===== main: 动态 N 工位并排 ===== */
.main {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
}
.loading-main {
display: flex;
align-items: center;
justify-content: center;
}
.loading-text {
font-size: clamp(14px, 1.1vw, 20px);
color: var(--dim);
letter-spacing: 2px;
}
.error-text {
color: #f87171;
}
</style>
This diff is collapsed.
/**
* Mock 通道列表数据,对应 GET /api/v1/channels 响应
*/
export const mockChannelsResponse = {
api_version: '1.0',
server_instance_id: 'mock-srv-001',
generated_at: new Date().toISOString(),
channels: [
{
channel_id: 'channel-a',
display_name: 'A区B线A工位',
order: 1,
enabled: true,
status: 'online',
workstation: {
station_id: 'ARBE-A',
name: 'A区B线A工位',
area: 'ARBE',
line: 'A',
},
camera: {
camera_id: 'CAM-A',
display_name: 'A工位主摄像头',
connected: true,
},
urls: {
state: '/api/v1/channels/channel-a/state',
websocket: '/api/v1/ws/channels/channel-a',
video: '/api/v1/cameras/CAM-A/stream.mjpeg',
health: '/api/v1/channels/channel-a/health',
},
},
{
channel_id: 'channel-b',
display_name: 'A区B线B工位',
order: 2,
enabled: true,
status: 'online',
workstation: {
station_id: 'ARBE-B',
name: 'A区B线B工位',
area: 'ARBE',
line: 'B',
},
camera: {
camera_id: 'CAM-B',
display_name: 'B工位主摄像头',
connected: true,
},
urls: {
state: '/api/v1/channels/channel-b/state',
websocket: '/api/v1/ws/channels/channel-b',
video: '/api/v1/cameras/CAM-B/stream.mjpeg',
health: '/api/v1/channels/channel-b/health',
},
},
],
}
export { mockChannelsResponse } from './channels.js'
export { createMockWs } from './ws-mock.js'
This diff is collapsed.
/**
* WebSocket 客户端
*
* 连接 ws://127.0.0.1:8080/ws 接收实时推送。
* 对应后端多通道前端接口文档 v1.0。
* URL 由调用方从通道列表的 urls.websocket 获取并通过 resolveWsUrl() 解析后传入。
*
* 用法:
* import { connect } from './ws/index.js'
* const ws = connect()
* ws.on('snapshot', data => { ... })
* ws.on('step_progress', data => { ... })
* ws.send({ type: 'ping' })
* ws.send({ type: 'reset_product', reason: 'operator_request' })
*
* 环境变量:
* VITE_WS_URL — WebSocket 地址,默认 ws://127.0.0.1:8080/ws
* const ws = connect('ws://127.0.0.1:8080/api/v1/ws/channels/channel-a')
* ws.on('snapshot', msg => { ... })
* ws.on('step_confirmed', msg => { ... })
* ws.send({ type: 'ping', request_id: 'front-001', data: { client_time: '...' } })
* ws.send({ type: 'request_snapshot', request_id: 'front-002', data: { reason: 'sequence_gap' } })
*/
const RECONNECT_DELAY = 2000 // 断线重连间隔(ms)
const PING_INTERVAL = 10000 // 心跳间隔(ms)
const MAX_RECONNECTS = 0 // 0 = 无限重连
let _pingSeq = 0
export function connect(url) {
const wsUrl = url || import.meta.env.VITE_WS_URL || 'ws://127.0.0.1:8080/ws'
const wsUrl = url || ''
const handlers = {}
......@@ -59,13 +59,39 @@ export function connect(url) {
}
function createSocket() {
if (!wsUrl) {
console.warn('[WS] URL 为空,跳过连接')
return
}
socket = new WebSocket(wsUrl)
socket.addEventListener('open', () => {
reconnectCount = 0
fire('open', {})
// 心跳:按文档格式发送 ping,建议间隔 10 秒
pingTimer = setInterval(() => {
api.send({ type: 'ping' })
const reqId = `ping-${++_pingSeq}-${Date.now()}`
// 本地时区 ISO 8601,如 2026-07-21T15:30:00.000+08:00
const now = new Date()
const off = -now.getTimezoneOffset()
const sign = off >= 0 ? '+' : '-'
const pad = n => String(Math.floor(Math.abs(n))).padStart(2, '0')
const localISO =
now.getFullYear() + '-' +
pad(now.getMonth() + 1) + '-' +
pad(now.getDate()) + 'T' +
pad(now.getHours()) + ':' +
pad(now.getMinutes()) + ':' +
pad(now.getSeconds()) + '.' +
String(now.getMilliseconds()).padStart(3, '0') +
sign + pad(off / 60) + ':' + pad(off % 60)
api.send({
type: 'ping',
request_id: reqId,
data: {
client_time: localISO,
},
})
}, PING_INTERVAL)
})
......
......@@ -4,4 +4,12 @@ import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://127.0.0.1:8080', // 后端地址,按实际情况修改
changeOrigin: true,
},
},
},
})
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment