| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- <script setup lang="ts">
- import { ref, onMounted } from 'vue'
- import api from '../api'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import type { UploadRequestOptions } from 'element-plus'
- interface Camera {
- id: number
- name: string
- stream_url: string
- status: number
- created_at: string
- }
- const cameras = ref<Camera[]>([])
- const dialogVisible = ref(false)
- const isEdit = ref(false)
- const editId = ref<number | null>(null)
- const testing = ref(false)
- const previewImage = ref('')
- const form = ref({
- name: '',
- stream_url: ''
- })
- const fetchCameras = async () => {
- const res = await api.get('/cameras')
- cameras.value = res.data
- }
- const handleExport = async () => {
- try {
- const res = await api.get('/cameras/export_template', {
- responseType: 'blob'
- })
- const url = window.URL.createObjectURL(new Blob([res.data]))
- const link = document.createElement('a')
- link.href = url
- link.setAttribute('download', 'cameras_template.xlsx')
- document.body.appendChild(link)
- link.click()
- document.body.removeChild(link)
- window.URL.revokeObjectURL(url)
- ElMessage.success('导出成功')
- } catch (e) {
- ElMessage.error('导出失败')
- console.error(e)
- }
- }
- const handleImport = async (options: UploadRequestOptions) => {
- const formData = new FormData()
- formData.append('file', options.file)
- try {
- const res = await api.post('/cameras/import_template', formData, {
- headers: {
- 'Content-Type': 'multipart/form-data'
- }
- })
- ElMessage.success(`导入成功: 新增 ${res.data.added}, 更新 ${res.data.updated}`)
- fetchCameras()
- } catch (e: any) {
- const msg = e.response?.data?.detail || '导入失败'
- ElMessage.error(msg)
- }
- }
- const resetForm = () => {
- form.value = { name: '', stream_url: '' }
- isEdit.value = false
- editId.value = null
- previewImage.value = ''
- }
- const handleAdd = () => {
- resetForm()
- dialogVisible.value = true
- }
- const handleEdit = (row: Camera) => {
- isEdit.value = true
- editId.value = row.id
- form.value = {
- name: row.name,
- stream_url: row.stream_url
- }
- previewImage.value = ''
- dialogVisible.value = true
- }
- const handleSave = async () => {
- try {
- if (isEdit.value && editId.value) {
- await api.put(`/cameras/${editId.value}`, form.value)
- ElMessage.success('更新成功')
- } else {
- await api.post('/cameras', form.value)
- ElMessage.success('添加成功')
- }
- dialogVisible.value = false
- fetchCameras()
- } catch (e) {
- ElMessage.error(isEdit.value ? '更新失败' : '添加失败')
- }
- }
- const handleDelete = (id: number) => {
- ElMessageBox.confirm('确认删除?', '警告', {
- confirmButtonText: '确定',
- cancelButtonText: '取消',
- type: 'warning',
- }).then(async () => {
- try {
- await api.delete(`/cameras/${id}`)
- ElMessage.success('删除成功')
- fetchCameras()
- } catch (e) {
- ElMessage.error('删除失败')
- }
- })
- }
- const handleTest = async () => {
- if (!form.value.stream_url) {
- ElMessage.warning('请输入 RTSP 地址')
- return
- }
-
- testing.value = true
- previewImage.value = ''
- try {
- const res = await api.post('/cameras/test', { stream_url: form.value.stream_url })
- previewImage.value = res.data.image
- ElMessage.success('连接成功')
- } catch (e) {
- ElMessage.error('连接失败,请检查 RTSP 地址')
- } finally {
- testing.value = false
- }
- }
- onMounted(fetchCameras)
- </script>
- <template>
- <div class="cameras-view">
- <div class="toolbar toolbar-responsive">
- <el-button type="primary" @click="handleAdd">添加摄像头</el-button>
- <el-button type="success" @click="handleExport">导出模板</el-button>
- <el-upload
- class="upload-demo"
- :show-file-list="false"
- :http-request="handleImport"
- accept=".xlsx, .xls"
- >
- <el-button type="warning">导入模板</el-button>
- </el-upload>
- </div>
- <div class="table-scroll-wrap">
- <el-table :data="cameras" style="width: 100%" table-layout="fixed">
- <el-table-column prop="id" label="ID" width="60" />
- <el-table-column prop="name" label="名称" />
- <el-table-column prop="stream_url" label="流地址" show-overflow-tooltip />
- <el-table-column label="状态">
- <template #default="scope">
- <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
- {{ scope.row.status === 1 ? '在线' : '离线' }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="操作" width="200">
- <template #default="scope">
- <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
- <el-button type="danger" size="small" @click="handleDelete(scope.row.id)">删除</el-button>
- </template>
- </el-table-column>
- </el-table>
- </div>
- <el-dialog
- v-model="dialogVisible"
- class="responsive-dialog"
- :title="isEdit ? '编辑摄像头' : '添加摄像头'"
- >
- <el-form :model="form" label-width="80px">
- <el-form-item label="名称">
- <el-input v-model="form.name" />
- </el-form-item>
- <el-form-item label="RTSP 地址">
- <el-input v-model="form.stream_url" />
- </el-form-item>
- </el-form>
-
- <div v-if="previewImage" class="preview-container">
- <p>测试预览:</p>
- <img :src="previewImage" class="preview-img" />
- </div>
- <template #footer>
- <span class="dialog-footer">
- <el-button @click="handleTest" :loading="testing" type="success" plain>测试预览</el-button>
- <el-button @click="dialogVisible = false">取消</el-button>
- <el-button type="primary" @click="handleSave">保存</el-button>
- </span>
- </template>
- </el-dialog>
- </div>
- </template>
- <style scoped>
- .toolbar {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 8px 12px;
- margin-bottom: 16px;
- }
- .upload-demo {
- display: inline-block;
- }
- .preview-container {
- margin-top: 20px;
- text-align: center;
- }
- .preview-img {
- max-width: 100%;
- max-height: 200px;
- border: 1px solid #ccc;
- margin-top: 5px;
- }
- </style>
|