Cameras.vue 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. <script setup lang="ts">
  2. import { ref, onMounted } from 'vue'
  3. import api from '../api'
  4. import { ElMessage, ElMessageBox } from 'element-plus'
  5. import type { UploadRequestOptions } from 'element-plus'
  6. interface Camera {
  7. id: number
  8. name: string
  9. stream_url: string
  10. status: number
  11. created_at: string
  12. }
  13. const cameras = ref<Camera[]>([])
  14. const dialogVisible = ref(false)
  15. const isEdit = ref(false)
  16. const editId = ref<number | null>(null)
  17. const testing = ref(false)
  18. const previewImage = ref('')
  19. const form = ref({
  20. name: '',
  21. stream_url: ''
  22. })
  23. const fetchCameras = async () => {
  24. const res = await api.get('/cameras')
  25. cameras.value = res.data
  26. }
  27. const handleExport = async () => {
  28. try {
  29. const res = await api.get('/cameras/export_template', {
  30. responseType: 'blob'
  31. })
  32. const url = window.URL.createObjectURL(new Blob([res.data]))
  33. const link = document.createElement('a')
  34. link.href = url
  35. link.setAttribute('download', 'cameras_template.xlsx')
  36. document.body.appendChild(link)
  37. link.click()
  38. document.body.removeChild(link)
  39. window.URL.revokeObjectURL(url)
  40. ElMessage.success('导出成功')
  41. } catch (e) {
  42. ElMessage.error('导出失败')
  43. console.error(e)
  44. }
  45. }
  46. const handleImport = async (options: UploadRequestOptions) => {
  47. const formData = new FormData()
  48. formData.append('file', options.file)
  49. try {
  50. const res = await api.post('/cameras/import_template', formData, {
  51. headers: {
  52. 'Content-Type': 'multipart/form-data'
  53. }
  54. })
  55. ElMessage.success(`导入成功: 新增 ${res.data.added}, 更新 ${res.data.updated}`)
  56. fetchCameras()
  57. } catch (e: any) {
  58. const msg = e.response?.data?.detail || '导入失败'
  59. ElMessage.error(msg)
  60. }
  61. }
  62. const resetForm = () => {
  63. form.value = { name: '', stream_url: '' }
  64. isEdit.value = false
  65. editId.value = null
  66. previewImage.value = ''
  67. }
  68. const handleAdd = () => {
  69. resetForm()
  70. dialogVisible.value = true
  71. }
  72. const handleEdit = (row: Camera) => {
  73. isEdit.value = true
  74. editId.value = row.id
  75. form.value = {
  76. name: row.name,
  77. stream_url: row.stream_url
  78. }
  79. previewImage.value = ''
  80. dialogVisible.value = true
  81. }
  82. const handleSave = async () => {
  83. try {
  84. if (isEdit.value && editId.value) {
  85. await api.put(`/cameras/${editId.value}`, form.value)
  86. ElMessage.success('更新成功')
  87. } else {
  88. await api.post('/cameras', form.value)
  89. ElMessage.success('添加成功')
  90. }
  91. dialogVisible.value = false
  92. fetchCameras()
  93. } catch (e) {
  94. ElMessage.error(isEdit.value ? '更新失败' : '添加失败')
  95. }
  96. }
  97. const handleDelete = (id: number) => {
  98. ElMessageBox.confirm('确认删除?', '警告', {
  99. confirmButtonText: '确定',
  100. cancelButtonText: '取消',
  101. type: 'warning',
  102. }).then(async () => {
  103. try {
  104. await api.delete(`/cameras/${id}`)
  105. ElMessage.success('删除成功')
  106. fetchCameras()
  107. } catch (e) {
  108. ElMessage.error('删除失败')
  109. }
  110. })
  111. }
  112. const handleTest = async () => {
  113. if (!form.value.stream_url) {
  114. ElMessage.warning('请输入 RTSP 地址')
  115. return
  116. }
  117. testing.value = true
  118. previewImage.value = ''
  119. try {
  120. const res = await api.post('/cameras/test', { stream_url: form.value.stream_url })
  121. previewImage.value = res.data.image
  122. ElMessage.success('连接成功')
  123. } catch (e) {
  124. ElMessage.error('连接失败,请检查 RTSP 地址')
  125. } finally {
  126. testing.value = false
  127. }
  128. }
  129. onMounted(fetchCameras)
  130. </script>
  131. <template>
  132. <div class="cameras-view">
  133. <div class="toolbar toolbar-responsive">
  134. <el-button type="primary" @click="handleAdd">添加摄像头</el-button>
  135. <el-button type="success" @click="handleExport">导出模板</el-button>
  136. <el-upload
  137. class="upload-demo"
  138. :show-file-list="false"
  139. :http-request="handleImport"
  140. accept=".xlsx, .xls"
  141. >
  142. <el-button type="warning">导入模板</el-button>
  143. </el-upload>
  144. </div>
  145. <div class="table-scroll-wrap">
  146. <el-table :data="cameras" style="width: 100%" table-layout="fixed">
  147. <el-table-column prop="id" label="ID" width="60" />
  148. <el-table-column prop="name" label="名称" />
  149. <el-table-column prop="stream_url" label="流地址" show-overflow-tooltip />
  150. <el-table-column label="状态">
  151. <template #default="scope">
  152. <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
  153. {{ scope.row.status === 1 ? '在线' : '离线' }}
  154. </el-tag>
  155. </template>
  156. </el-table-column>
  157. <el-table-column label="操作" width="200">
  158. <template #default="scope">
  159. <el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
  160. <el-button type="danger" size="small" @click="handleDelete(scope.row.id)">删除</el-button>
  161. </template>
  162. </el-table-column>
  163. </el-table>
  164. </div>
  165. <el-dialog
  166. v-model="dialogVisible"
  167. class="responsive-dialog"
  168. :title="isEdit ? '编辑摄像头' : '添加摄像头'"
  169. >
  170. <el-form :model="form" label-width="80px">
  171. <el-form-item label="名称">
  172. <el-input v-model="form.name" />
  173. </el-form-item>
  174. <el-form-item label="RTSP 地址">
  175. <el-input v-model="form.stream_url" />
  176. </el-form-item>
  177. </el-form>
  178. <div v-if="previewImage" class="preview-container">
  179. <p>测试预览:</p>
  180. <img :src="previewImage" class="preview-img" />
  181. </div>
  182. <template #footer>
  183. <span class="dialog-footer">
  184. <el-button @click="handleTest" :loading="testing" type="success" plain>测试预览</el-button>
  185. <el-button @click="dialogVisible = false">取消</el-button>
  186. <el-button type="primary" @click="handleSave">保存</el-button>
  187. </span>
  188. </template>
  189. </el-dialog>
  190. </div>
  191. </template>
  192. <style scoped>
  193. .toolbar {
  194. display: flex;
  195. flex-wrap: wrap;
  196. align-items: center;
  197. gap: 8px 12px;
  198. margin-bottom: 16px;
  199. }
  200. .upload-demo {
  201. display: inline-block;
  202. }
  203. .preview-container {
  204. margin-top: 20px;
  205. text-align: center;
  206. }
  207. .preview-img {
  208. max-width: 100%;
  209. max-height: 200px;
  210. border: 1px solid #ccc;
  211. margin-top: 5px;
  212. }
  213. </style>