@@ -0,0 +1,19 @@
< template >
< section class = "page-container" >
< div class = "page-heading" > < div > < h1 > 区域与警戒线 < / h1 > < p > 在已验证画面上配置检测区域和通行方向 , 每次发布都会保留版本 。 < / p > < / div > < / div >
< div class = "search-toolbar" > < el-form :inline = "true" > < el-form-item label = "设备" > < el-select v-model = "deviceFilter" clearable filterable placeholder="全部设备" style="width:260px" @change="load"><el-option v-for="item in deviceOptions" :key="item.device_id" :label="item.device_id" :value="item.device_id" /></el-select></el-form-item><el-form-item><el-button :icon="Refresh" @click="load" > 刷新 < / el -button > < / el-form-item > < / el-form > < / div >
< div class = "table-container" > < div class = "table-actions" > < strong > 配置列表 < / strong > < el-button v-if = "canWrite" type="primary" :icon="Plus" @click="openCreate">新增配置</el-button></div><el-table v-loading="loading" :data="areas" border >
< el-table-column prop = "name" label = "名称" min -width = " 150 " / > < el-table-column prop = "device_id" label = "设备" min -width = " 180 " / > < el-table-column prop = "profile_token" label = "Profile" min -width = " 110 " / > < el-table-column label = "类型" width = "120" > < template # default = "scope" > { { scope . row . kind === 'polygon' ? '多边形区域' : '方向警戒线' } } < / template > < / el-table-column > < el-table-column label = "分辨率" width = "130" > < template # default = "scope" > { { scope . row . width } } × { { scope . row . height } } < / template > < / el-table-column > < el-table-column label = "版本" width = "90" > < template # default = "scope" > v { { scope . row . version } } < / template > < / el-table-column > < el-table-column label = "状态" width = "130" > < template # default = "scope" > < el-tag v-if = "scope.row.recalibration_required" type="warning">需要重新校准</el-tag><el-tag v-else :type="scope.row.enabled ? 'success' : 'info'">{{ scope.row.enabled ? '已启用' : '已停用' }}</el-tag></template></el-table-column><el-table-column v-if="canWrite" label="操作" width="180" fixed="right"><template #default="scope"><el-button link type="primary" @click="openEdit(scope.row)">{{ scope.row.recalibration_required ? '重新校准' : '编辑' }}</el-button><el-button v-if="scope.row.enabled" link type="danger" @click="disable(scope.row)" > 停用 < / el -button > < / template > < / el-table-column >
< / el-table > < / div >
< el-dialog v-model = "dialog" :title="editing ? (editing.recalibration_required ? '重新校准配置' : '编辑配置') : '新增配置'" width="900px" destroy-on-close><el-form ref="formRef" :model="form" :rules="rules" label-width="92px"><el-row :gutter="16"><el-col :span="12"><el-form-item label="设备/Profile" prop="route_id"><el-select v-model="form.route_id" :disabled="Boolean(editing)" style="width:100%"><el-option v-for="item in routes" :key="item.id" :label="`${item.device_id} · ${item.profile_token}`" :value="item.id" /></el-select></el-form-item></el-col><el-col :span="12"><el-form-item label="名称" prop="name"><el-input v-model="form.name" /></el-form-item></el-col><el-col :span="12"><el-form-item label="类型" prop="kind"><el-radio-group v-model="form.kind" :disabled="Boolean(editing)" @change="form.points=[]"><el-radio-button value="polygon">多边形区域</el-radio-button><el-radio-button value="line">方向警戒线</el-radio-button></el-radio-group></el-form-item></el-col><el-col v-if="form.kind==='line'" :span="12"><el-form-item label="方向" prop="direction"><el-select v-model="form.direction" style="width:100%"><el-option label="A → B" value="a_to_b" /><el-option label="B → A" value="b_to_a" /><el-option label="双向" value="both" /></el-select></el-form-item></el-col></el-row><el-form-item label="画面坐标" prop="points"><AreaCanvas v-model="form.points" :kind="form.kind" /></el-form-item><el-alert v-if="editing?.recalibration_required" title="Profile 分辨率已变化,请按新画面重新绘制后发布。" type="warning" show-icon :closable="false" /></el-form><template #footer><el-button @click="dialog=false">取消</el-button><el-button type="primary" :loading="saving" @click="save" > 发布新版本 < / el -button > < / template > < / el-dialog >
< / section >
< / template >
< script setup >
import { computed , onMounted , reactive , ref } from 'vue' ; import { ElMessage , ElMessageBox } from 'element-plus' ; import { Plus , Refresh } from '@element-plus/icons-vue' ; import { useStore } from 'vuex' ; import { createArea , disableArea , listAreas , recalibrateArea , updateArea } from '../../../api/sense/area' ; import { playbackRoutes } from '../../../api/sense/liveview' ; import AreaCanvas from '../../../components/sense/area/AreaCanvas.vue'
const store = useStore ( ) , loading = ref ( false ) , saving = ref ( false ) , dialog = ref ( false ) , editing = ref ( null ) , formRef = ref ( ) , areas = ref ( [ ] ) , routes = ref ( [ ] ) , deviceFilter = ref ( '' ) ; const form = reactive ( { route _id : '' , name : '' , kind : 'polygon' , direction : '' , points : [ ] } ) ; const rules = { route _id : [ { required : true , message : '请选择设备和 Profile' , trigger : 'change' } ] , name : [ { required : true , message : '请输入名称' , trigger : 'blur' } ] , kind : [ { required : true , message : '请选择类型' , trigger : 'change' } ] , points : [ { validator : ( _r , value , done ) => value . length < ( form . kind === 'polygon' ? 3 : 2 ) ? done ( new Error ( form . kind === 'polygon' ? '至少绘制 3 个点' : '请绘制 2 个端点' ) ) : done ( ) , trigger : 'change' } ] } ; const canWrite = computed ( ( ) => store . getters [ 'sense-identity/hasPermission' ] ? . ( 'area.write' ) ) ; const deviceOptions = computed ( ( ) => { const map = new Map ( ) ; routes . value . forEach ( item => map . set ( item . device _id , item ) ) ; return [ ... map . values ( ) ] } )
async function load ( ) { loading . value = true ; try { const [ result , routeResult ] = await Promise . all ( [ listAreas ( deviceFilter . value ) , playbackRoutes ( ) ] ) ; areas . value = result . items || [ ] ; routes . value = routeResult . items || [ ] } finally { loading . value = false } }
function resetForm ( ) { Object . assign ( form , { route _id : '' , name : '' , kind : 'polygon' , direction : '' , points : [ ] } ) } function openCreate ( ) { editing . value = null ; resetForm ( ) ; dialog . value = true } function openEdit ( item ) { editing . value = item ; Object . assign ( form , { route _id : ` ${ item . device _id } : ${ item . profile _token } ` , name : item . name , kind : item . kind , direction : item . direction , points : item . points . map ( point => ( { ... point } ) ) } ) ; dialog . value = true }
async function save ( ) { const valid = await formRef . value ? . validate ( ) . catch ( ( ) => false ) ; if ( ! valid ) return ; const route = routes . value . find ( item => item . id === form . route _id ) || { device _id : editing . value ? . device _id , profile _token : editing . value ? . profile _token } ; const payload = { device _id : route . device _id , profile _token : route . profile _token , name : form . name , kind : form . kind , direction : form . kind === 'line' ? form . direction : '' , points : form . points , version : editing . value ? . version || 0 } ; saving . value = true ; try { if ( ! editing . value ) await createArea ( payload ) ; else if ( editing . value . recalibration _required ) await recalibrateArea ( editing . value . id , payload ) ; else await updateArea ( editing . value . id , payload ) ; ElMessage . success ( '新版本已发布' ) ; dialog . value = false ; await load ( ) } catch ( error ) { ElMessage . error ( error . message || '发布失败' ) } finally { saving . value = false } }
async function disable ( item ) { await ElMessageBox . confirm ( ` 停用“ ${ item . name } ”将发布一个停用版本,历史版本仍保留。 ` , '确认停用' , { type : 'warning' } ) ; await disableArea ( item . id , item . version ) ; ElMessage . success ( '已停用' ) ; await load ( ) }
onMounted ( load )
< / script >