index.vue 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772
  1. <template>
  2. <div class="page-container">
  3. <!-- 搜索表单 -->
  4. <div class="search-form">
  5. <el-form :model="searchForm" inline data-id="search-form">
  6. <el-form-item :label="t('厂家代码')">
  7. <el-input
  8. v-model.trim="searchForm.code"
  9. placeholder="请输入厂家代码"
  10. clearable
  11. data-id="search-code"
  12. @keyup.enter="handleSearch"
  13. />
  14. </el-form-item>
  15. <el-form-item :label="t('厂家名称')">
  16. <el-input
  17. v-model.trim="searchForm.name"
  18. placeholder="请输入厂家名称"
  19. clearable
  20. data-id="search-name"
  21. @keyup.enter="handleSearch"
  22. />
  23. </el-form-item>
  24. <el-form-item :label="t('启用状态')">
  25. <el-select v-model="searchForm.enabled" placeholder="全部" clearable data-id="search-enabled">
  26. <el-option label="全部" value="" />
  27. <el-option label="已启用" :value="true" />
  28. <el-option label="已禁用" :value="false" />
  29. </el-select>
  30. </el-form-item>
  31. <el-form-item>
  32. <el-button type="primary" data-id="btn-search" @click="handleSearch">
  33. {{ t('查询') }}
  34. </el-button>
  35. <el-button :icon="RefreshRight" data-id="btn-reset" @click="handleReset">{{ t('重置') }}</el-button>
  36. <el-button type="primary" data-id="btn-add-vendor" @click="handleAdd">
  37. {{ t('新增') }}
  38. </el-button>
  39. <!-- <el-button type="success" :icon="Setting" data-id="btn-init" @click="handleInit">
  40. {{ t('初始化默认数据') }}
  41. </el-button> -->
  42. </el-form-item>
  43. </el-form>
  44. </div>
  45. <!-- 批量操作栏 -->
  46. <div v-if="selectedRows.length > 0" class="batch-actions">
  47. <span class="batch-info">{{ t('已选择') }} {{ selectedRows.length }} {{ t('项') }}</span>
  48. <el-button type="danger" :icon="Delete" :loading="deleteLoading" @click="handleBatchDelete">
  49. {{ t('批量删除') }}
  50. </el-button>
  51. <el-button @click="clearSelection">{{ t('取消选择') }}</el-button>
  52. </div>
  53. <!-- 数据表格 -->
  54. <div class="table-wrapper">
  55. <el-table
  56. ref="tableRef"
  57. v-loading="loading"
  58. :data="sortedList"
  59. stripe
  60. size="default"
  61. data-id="vendor-table"
  62. height="100%"
  63. @selection-change="handleSelectionChange"
  64. @sort-change="handleSortChange"
  65. >
  66. <el-table-column type="selection" width="50" align="center" />
  67. <el-table-column prop="model" :label="t('设备型号')" width="100" align="center">
  68. <template #default="{ row }">
  69. {{ row.model || '-' }}
  70. </template>
  71. </el-table-column>
  72. <el-table-column
  73. prop="vendorName"
  74. :label="t('厂家名称')"
  75. min-width="120"
  76. sortable="custom"
  77. show-overflow-tooltip
  78. >
  79. <template #default="{ row }">
  80. <el-link type="primary" :data-id="`link-edit-${row.code}`" @click="handleEdit(row)">
  81. {{ row.vendorName }}
  82. </el-link>
  83. </template>
  84. </el-table-column>
  85. <el-table-column
  86. prop="vendorCode"
  87. :label="t('厂家代码')"
  88. min-width="100"
  89. sortable="custom"
  90. show-overflow-tooltip
  91. />
  92. <el-table-column prop="description" :label="t('描述')" min-width="150" show-overflow-tooltip />
  93. <el-table-column :label="t('协议支持')" min-width="200" align="center">
  94. <template #default="{ row }">
  95. <el-tag v-if="row.supportOnvif" type="success" size="small" class="protocol-tag">ONVIF</el-tag>
  96. <el-tag v-if="row.supportPtz" type="primary" size="small" class="protocol-tag">PTZ</el-tag>
  97. <el-tag v-if="row.supportIsapi" type="warning" size="small" class="protocol-tag">ISAPI</el-tag>
  98. <el-tag v-if="row.supportGb28181" type="info" size="small" class="protocol-tag">GB28181</el-tag>
  99. <el-tag v-if="row.supportAudio" type="danger" size="small" class="protocol-tag">Audio</el-tag>
  100. </template>
  101. </el-table-column>
  102. <el-table-column prop="defaultPort" :label="t('默认端口')" width="100" align="center">
  103. <template #default="{ row }">
  104. {{ row.defaultPort || '-' }}
  105. </template>
  106. </el-table-column>
  107. <el-table-column prop="defaultRtspPort" :label="t('RTSP端口')" width="100" align="center">
  108. <template #default="{ row }">
  109. {{ row.defaultRtspPort || '-' }}
  110. </template>
  111. </el-table-column>
  112. <el-table-column prop="enabled" :label="t('启用')" sortable="custom" width="80" align="center">
  113. <template #default="{ row }">
  114. <el-tag :type="row.enabled ? 'success' : 'info'">
  115. {{ row.enabled ? t('是') : t('否') }}
  116. </el-tag>
  117. </template>
  118. </el-table-column>
  119. <el-table-column prop="sortOrder" :label="t('排序')" width="80" sortable="custom" align="center">
  120. <template #default="{ row }">
  121. {{ row.sortOrder ?? 0 }}
  122. </template>
  123. </el-table-column>
  124. <el-table-column prop="createdAt" :label="t('创建时间')" width="160" sortable="custom" align="center">
  125. <template #default="{ row }">
  126. {{ formatDateTime(row.createdAt) }}
  127. </template>
  128. </el-table-column>
  129. <el-table-column :label="t('操作')" min-width="90" align="center" fixed="right">
  130. <template #default="{ row }">
  131. <el-button type="primary" link :data-id="`btn-edit-${row.code}`" @click="handleEdit(row)">
  132. <Icon icon="mdi:note-edit-outline" width="20" height="20" />
  133. </el-button>
  134. <el-button
  135. type="danger"
  136. link
  137. :disabled="deleteLoading"
  138. :data-id="`btn-delete-${row.code}`"
  139. @click="handleDelete(row)"
  140. >
  141. <Icon icon="mdi:delete" width="20" height="20" />
  142. </el-button>
  143. </template>
  144. </el-table-column>
  145. </el-table>
  146. </div>
  147. <!-- 分页 -->
  148. <div class="pagination-container">
  149. <el-pagination
  150. v-model:current-page="currentPage"
  151. v-model:page-size="pageSize"
  152. :page-sizes="[10, 20, 50, 100]"
  153. :total="total"
  154. layout="total, sizes, prev, pager, next, jumper"
  155. background
  156. @size-change="handleSizeChange"
  157. @current-change="handleCurrentChange"
  158. />
  159. </div>
  160. <!-- 新增/编辑弹窗 -->
  161. <el-dialog v-model="dialogVisible" :title="dialogTitle" width="600px" destroy-on-close data-id="dialog-vendor">
  162. <div class="form-container">
  163. <el-scrollbar>
  164. <el-form ref="formRef" :model="form" :rules="rules" label-width="120px" data-id="form-vendor">
  165. <el-form-item :label="t('厂家代码')" prop="vendorCode">
  166. <el-input
  167. v-model="form.vendorCode"
  168. placeholder="请输入厂家代码"
  169. :disabled="isEdit"
  170. data-id="input-code"
  171. />
  172. </el-form-item>
  173. <el-form-item :label="t('厂家名称')" prop="vendorName">
  174. <el-input v-model="form.vendorName" placeholder="请输入厂家名称" data-id="input-name" />
  175. </el-form-item>
  176. <el-form-item :label="t('厂家别名')" prop="vendorAliasName">
  177. <el-input v-model="form.vendorAliasName" placeholder="请输入厂家别名" data-id="input-name" />
  178. </el-form-item>
  179. <el-form-item :label="t('设备型号')" prop="model">
  180. <el-input v-model="form.model" placeholder="请输入设备型号" data-id="input-model" />
  181. </el-form-item>
  182. <el-form-item :label="t('描述')" prop="description">
  183. <el-input
  184. v-model="form.description"
  185. type="textarea"
  186. :rows="2"
  187. placeholder="请输入描述"
  188. data-id="input-description"
  189. />
  190. </el-form-item>
  191. <el-form-item :label="t('Logo URL')" prop="logoUrl">
  192. <el-input v-model="form.logoUrl" placeholder="请输入Logo URL" data-id="input-logo-url" />
  193. </el-form-item>
  194. <el-form-item :label="t('协议支持')">
  195. <el-checkbox v-model="form.supportOnvif" data-id="check-onvif">ONVIF</el-checkbox>
  196. <el-checkbox v-model="form.supportPtz" data-id="check-ptz">PTZ</el-checkbox>
  197. <el-checkbox v-model="form.supportIsapi" data-id="check-isapi">ISAPI</el-checkbox>
  198. <el-checkbox v-model="form.supportGb28181" data-id="check-gb28181">GB28181</el-checkbox>
  199. <el-checkbox v-model="form.supportAudio" data-id="check-audio">Audio</el-checkbox>
  200. </el-form-item>
  201. <el-form-item :label="t('默认分辨率')" prop="resolution">
  202. <el-input v-model="form.resolution" placeholder="如: 1920x1080" data-id="input-resolution" />
  203. </el-form-item>
  204. <el-row>
  205. <el-col :span="12">
  206. <el-form-item :label="t('默认端口')" prop="defaultPort">
  207. <el-input-number
  208. v-model="form.defaultPort"
  209. :min="1"
  210. :max="65535"
  211. placeholder="80"
  212. data-id="input-default-port"
  213. />
  214. </el-form-item>
  215. </el-col>
  216. <el-col :span="12">
  217. <el-form-item :label="t('RTSP端口')" prop="defaultRtspPort">
  218. <el-input-number
  219. v-model="form.defaultRtspPort"
  220. :min="1"
  221. :max="65535"
  222. placeholder="554"
  223. data-id="input-rtsp-port"
  224. />
  225. </el-form-item>
  226. </el-col>
  227. </el-row>
  228. <el-form-item :label="t('RTSP URL模板')" prop="rtspUrlTemplate">
  229. <el-input
  230. v-model="form.rtspUrlTemplate"
  231. type="textarea"
  232. :rows="2"
  233. placeholder="如: rtsp://{username}:{password}@{ip}:{port}/Streaming/Channels/{channel}"
  234. data-id="input-rtsp-template"
  235. />
  236. </el-form-item>
  237. <el-row>
  238. <el-col :span="12">
  239. <el-form-item :label="t('排序号')" prop="sortOrder">
  240. <el-input-number v-model="form.sortOrder" :min="0" :max="9999" data-id="input-sort-order" />
  241. </el-form-item>
  242. </el-col>
  243. <el-col :span="12">
  244. <el-form-item :label="t('启用状态')">
  245. <el-switch v-model="form.enabled" data-id="switch-enabled" />
  246. </el-form-item>
  247. </el-col>
  248. </el-row>
  249. </el-form>
  250. </el-scrollbar>
  251. </div>
  252. <template #footer>
  253. <el-button data-id="btn-cancel" @click="dialogVisible = false">{{ t('取消') }}</el-button>
  254. <el-button type="primary" :loading="submitLoading" data-id="btn-submit" @click="handleSubmit">
  255. {{ t('确定') }}
  256. </el-button>
  257. </template>
  258. </el-dialog>
  259. </div>
  260. </template>
  261. <script setup lang="ts">
  262. import { ref, reactive, onMounted, computed } from 'vue'
  263. import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type TableInstance } from 'element-plus'
  264. import { Plus, Edit, Delete, Search, RefreshRight, Setting } from '@element-plus/icons-vue'
  265. import { Icon } from '@iconify/vue'
  266. import { useI18n } from 'vue-i18n'
  267. import {
  268. listCameraVendors,
  269. addCameraVendor,
  270. updateCameraVendor,
  271. deleteCameraVendor,
  272. initCameraVendors
  273. } from '@/api/camera-vendor'
  274. import { formatTime } from '@/utils/dayjs'
  275. import type { CameraVendorDTO, CameraVendorAddRequest, CameraVendorUpdateRequest } from '@/types'
  276. const { t } = useI18n({ useScope: 'global' })
  277. // 格式化时间(不含秒)
  278. function formatDateTime(dateStr: string | undefined): string {
  279. return formatTime(dateStr, 'YYYY-MM-DD HH:mm')
  280. }
  281. const loading = ref(false)
  282. const submitLoading = ref(false)
  283. const deleteLoading = ref(false)
  284. const vendorList = ref<CameraVendorDTO[]>([])
  285. const dialogVisible = ref(false)
  286. const formRef = ref<FormInstance>()
  287. const tableRef = ref<TableInstance>()
  288. // 选中的行
  289. const selectedRows = ref<CameraVendorDTO[]>([])
  290. // 排序状态
  291. const sortState = reactive<{
  292. prop: string
  293. order: 'ascending' | 'descending' | null
  294. }>({
  295. prop: '',
  296. order: null
  297. })
  298. // 搜索表单
  299. const searchForm = reactive<{
  300. code: string
  301. name: string
  302. enabled: boolean | ''
  303. }>({
  304. code: '',
  305. name: '',
  306. enabled: ''
  307. })
  308. // 分页相关
  309. const currentPage = ref(1)
  310. const pageSize = ref(20)
  311. const total = ref(0)
  312. // 排序后的数据
  313. const sortedList = computed(() => {
  314. const list = [...vendorList.value]
  315. if (sortState.prop && sortState.order) {
  316. list.sort((a, b) => {
  317. const aVal = a[sortState.prop as keyof CameraVendorDTO]
  318. const bVal = b[sortState.prop as keyof CameraVendorDTO]
  319. // 处理空值
  320. if (aVal == null && bVal == null) return 0
  321. if (aVal == null) return sortState.order === 'ascending' ? -1 : 1
  322. if (bVal == null) return sortState.order === 'ascending' ? 1 : -1
  323. // 比较
  324. let result = 0
  325. if (typeof aVal === 'number' && typeof bVal === 'number') {
  326. result = aVal - bVal
  327. } else if (typeof aVal === 'boolean' && typeof bVal === 'boolean') {
  328. result = aVal === bVal ? 0 : aVal ? 1 : -1
  329. } else {
  330. result = String(aVal).localeCompare(String(bVal))
  331. }
  332. return sortState.order === 'ascending' ? result : -result
  333. })
  334. }
  335. return list
  336. })
  337. const form = reactive<{
  338. id?: number
  339. vendorCode: string
  340. vendorName: string
  341. vendorAliasName: string
  342. model: string
  343. description: string
  344. logoUrl: string
  345. supportOnvif: boolean
  346. supportPtz: boolean
  347. supportIsapi: boolean
  348. supportGb28181: boolean
  349. supportAudio: boolean
  350. resolution: string
  351. defaultPort: number | undefined
  352. defaultRtspPort: number | undefined
  353. rtspUrlTemplate: string
  354. enabled: boolean
  355. sortOrder: number
  356. }>({
  357. vendorCode: '',
  358. vendorName: '',
  359. vendorAliasName: '',
  360. model: '',
  361. description: '',
  362. logoUrl: '',
  363. supportOnvif: false,
  364. supportPtz: false,
  365. supportIsapi: false,
  366. supportGb28181: false,
  367. supportAudio: false,
  368. resolution: '',
  369. defaultPort: undefined,
  370. defaultRtspPort: undefined,
  371. rtspUrlTemplate: '',
  372. enabled: true,
  373. sortOrder: 0
  374. })
  375. const isEdit = computed(() => !!form.id)
  376. const dialogTitle = computed(() => (isEdit.value ? t('编辑厂家') : t('新增厂家')))
  377. const rules: FormRules = {
  378. code: [{ required: true, message: t('请输入厂家代码'), trigger: 'blur' }],
  379. name: [{ required: true, message: t('请输入厂家名称'), trigger: 'blur' }]
  380. }
  381. async function getList() {
  382. loading.value = true
  383. try {
  384. // 构建查询参数
  385. const params: Record<string, any> = {
  386. page: currentPage.value,
  387. size: pageSize.value
  388. }
  389. // 搜索关键词
  390. if (searchForm.code || searchForm.name) {
  391. params.keyword = searchForm.code || searchForm.name
  392. }
  393. // 启用状态过滤
  394. if (searchForm.enabled !== '') {
  395. params.enabled = searchForm.enabled
  396. }
  397. // 排序
  398. if (sortState.prop && sortState.order) {
  399. params.sortBy = sortState.prop
  400. params.sortDir = sortState.order === 'ascending' ? 'ASC' : 'DESC'
  401. }
  402. const res = await listCameraVendors(params)
  403. if (res.success) {
  404. vendorList.value = res.data.list
  405. total.value = res.data.total || 0
  406. }
  407. } finally {
  408. loading.value = false
  409. }
  410. }
  411. function handleSearch() {
  412. currentPage.value = 1
  413. getList()
  414. }
  415. function handleReset() {
  416. searchForm.code = ''
  417. searchForm.name = ''
  418. searchForm.enabled = ''
  419. currentPage.value = 1
  420. sortState.prop = ''
  421. sortState.order = null
  422. getList()
  423. }
  424. // 排序变化处理
  425. function handleSortChange({ prop, order }: { prop: string; order: 'ascending' | 'descending' | null }) {
  426. sortState.prop = prop || ''
  427. sortState.order = order
  428. getList()
  429. }
  430. // 选择变化处理
  431. function handleSelectionChange(rows: CameraVendorDTO[]) {
  432. selectedRows.value = rows
  433. }
  434. // 清除选择
  435. function clearSelection() {
  436. tableRef.value?.clearSelection()
  437. }
  438. // 批量删除
  439. async function handleBatchDelete() {
  440. if (selectedRows.value.length === 0) return
  441. try {
  442. await ElMessageBox.confirm(`${t('确定要删除选中的')} ${selectedRows.value.length} ${t('个厂家吗?')}`, t('提示'), {
  443. type: 'warning'
  444. })
  445. deleteLoading.value = true
  446. const deletePromises = selectedRows.value.map((row) => deleteCameraVendor(row.id))
  447. await Promise.all(deletePromises)
  448. ElMessage.success(`${t('成功删除')} ${selectedRows.value.length} ${t('个厂家')}`)
  449. clearSelection()
  450. getList()
  451. } catch (error) {
  452. if (error !== 'cancel') {
  453. console.error(t('批量删除失败'), error)
  454. ElMessage.error(t('批量删除失败'))
  455. }
  456. } finally {
  457. deleteLoading.value = false
  458. }
  459. }
  460. function handleAdd() {
  461. Object.assign(form, {
  462. id: undefined,
  463. code: '',
  464. name: '',
  465. description: '',
  466. logoUrl: '',
  467. supportOnvif: false,
  468. supportPtz: false,
  469. supportIsapi: false,
  470. supportGb28181: false,
  471. supportAudio: false,
  472. resolution: '',
  473. defaultPort: undefined,
  474. defaultRtspPort: undefined,
  475. rtspUrlTemplate: '',
  476. enabled: true,
  477. sortOrder: 0
  478. })
  479. dialogVisible.value = true
  480. }
  481. function handleEdit(row: CameraVendorDTO) {
  482. Object.assign(form, {
  483. id: row.id,
  484. code: row.code,
  485. name: row.name,
  486. description: row.description || '',
  487. logoUrl: row.logoUrl || '',
  488. supportOnvif: row.supportOnvif,
  489. supportPtz: row.supportPtz,
  490. supportIsapi: row.supportIsapi,
  491. supportGb28181: row.supportGb28181,
  492. supportAudio: row.supportAudio,
  493. resolution: row.resolution || '',
  494. defaultPort: row.defaultPort,
  495. defaultRtspPort: row.defaultRtspPort,
  496. rtspUrlTemplate: row.rtspUrlTemplate || '',
  497. enabled: row.enabled,
  498. sortOrder: row.sortOrder ?? 0
  499. })
  500. dialogVisible.value = true
  501. }
  502. async function handleDelete(row: CameraVendorDTO) {
  503. if (deleteLoading.value) return
  504. try {
  505. await ElMessageBox.confirm(`${t('确定要删除厂家')} "${row.name}" ${t('吗?')}`, t('提示'), {
  506. type: 'warning'
  507. })
  508. deleteLoading.value = true
  509. const res = await deleteCameraVendor(row.id)
  510. if (res.success) {
  511. ElMessage.success(t('删除成功'))
  512. getList()
  513. }
  514. } catch (error) {
  515. if (error !== 'cancel') {
  516. console.error(t('删除失败'), error)
  517. }
  518. } finally {
  519. deleteLoading.value = false
  520. }
  521. }
  522. async function handleInit() {
  523. try {
  524. await ElMessageBox.confirm(t('确定要初始化默认厂家数据吗?这将添加预设的摄像头厂家信息。'), t('提示'), {
  525. type: 'warning'
  526. })
  527. loading.value = true
  528. const res = await initCameraVendors()
  529. if (res.success) {
  530. ElMessage.success(t('初始化成功'))
  531. getList()
  532. }
  533. } catch (error) {
  534. if (error !== 'cancel') {
  535. console.error(t('初始化失败'), error)
  536. ElMessage.error(t('初始化失败'))
  537. }
  538. } finally {
  539. loading.value = false
  540. }
  541. }
  542. async function handleSubmit() {
  543. if (!formRef.value) return
  544. await formRef.value.validate(async (valid) => {
  545. if (valid) {
  546. submitLoading.value = true
  547. try {
  548. if (isEdit.value) {
  549. const updateData: CameraVendorUpdateRequest = {
  550. id: form.id!,
  551. code: form.code,
  552. name: form.name,
  553. description: form.description || undefined,
  554. logoUrl: form.logoUrl || undefined,
  555. supportOnvif: form.supportOnvif,
  556. supportPtz: form.supportPtz,
  557. supportIsapi: form.supportIsapi,
  558. supportGb28181: form.supportGb28181,
  559. supportAudio: form.supportAudio,
  560. resolution: form.resolution || undefined,
  561. defaultPort: form.defaultPort,
  562. defaultRtspPort: form.defaultRtspPort,
  563. rtspUrlTemplate: form.rtspUrlTemplate || undefined,
  564. enabled: form.enabled,
  565. sortOrder: form.sortOrder
  566. }
  567. const res = await updateCameraVendor(updateData)
  568. if (res.success) {
  569. ElMessage.success(t('修改成功'))
  570. dialogVisible.value = false
  571. getList()
  572. }
  573. } else {
  574. const addData: CameraVendorAddRequest = {
  575. code: form.code,
  576. name: form.name,
  577. description: form.description || undefined,
  578. logoUrl: form.logoUrl || undefined,
  579. supportOnvif: form.supportOnvif,
  580. supportPtz: form.supportPtz,
  581. supportIsapi: form.supportIsapi,
  582. supportGb28181: form.supportGb28181,
  583. supportAudio: form.supportAudio,
  584. resolution: form.resolution || undefined,
  585. defaultPort: form.defaultPort,
  586. defaultRtspPort: form.defaultRtspPort,
  587. rtspUrlTemplate: form.rtspUrlTemplate || undefined,
  588. enabled: form.enabled,
  589. sortOrder: form.sortOrder
  590. }
  591. const res = await addCameraVendor(addData)
  592. if (res.success) {
  593. ElMessage.success(t('新增成功'))
  594. dialogVisible.value = false
  595. getList()
  596. }
  597. }
  598. } finally {
  599. submitLoading.value = false
  600. }
  601. }
  602. })
  603. }
  604. function handleSizeChange(val: number) {
  605. pageSize.value = val
  606. currentPage.value = 1
  607. getList()
  608. }
  609. function handleCurrentChange(val: number) {
  610. currentPage.value = val
  611. getList()
  612. }
  613. onMounted(() => {
  614. getList()
  615. })
  616. </script>
  617. <style lang="scss" scoped>
  618. .page-container {
  619. display: flex;
  620. flex-direction: column;
  621. box-sizing: border-box;
  622. }
  623. .form-container {
  624. padding: 18px 0;
  625. }
  626. // 批量操作栏
  627. .batch-actions {
  628. flex-shrink: 0;
  629. display: flex;
  630. align-items: center;
  631. gap: 12px;
  632. margin-bottom: 12px;
  633. padding: 12px 16px;
  634. background: #fef3c7;
  635. border: 1px solid #f59e0b;
  636. .batch-info {
  637. font-size: 14px;
  638. color: #92400e;
  639. font-weight: 500;
  640. }
  641. :deep(.el-button--danger) {
  642. background-color: #dc2626;
  643. border-color: #dc2626;
  644. &:hover {
  645. background-color: #ef4444;
  646. border-color: #ef4444;
  647. }
  648. }
  649. }
  650. .search-form {
  651. flex-shrink: 0;
  652. margin-bottom: 16px;
  653. padding: 16px 16px 4px 16px;
  654. background: #f5f7fa;
  655. :deep(.el-form-item) {
  656. margin-bottom: 12px;
  657. margin-right: 16px;
  658. }
  659. :deep(.el-input),
  660. :deep(.el-select) {
  661. width: 160px;
  662. }
  663. }
  664. .table-wrapper {
  665. flex: 1;
  666. min-height: 0;
  667. overflow: hidden;
  668. }
  669. .pagination-container {
  670. flex-shrink: 0;
  671. display: flex;
  672. justify-content: flex-end;
  673. padding-top: 16px;
  674. }
  675. // 协议标签样式
  676. .protocol-tag {
  677. margin: 2px;
  678. }
  679. // 表格样式
  680. :deep(.el-table) {
  681. --el-table-row-hover-bg-color: #f0f0ff;
  682. .el-table__row--striped td.el-table__cell {
  683. background-color: #f8f9fc;
  684. }
  685. .el-table__header th {
  686. background-color: #f5f7fa;
  687. color: #333;
  688. font-weight: 600;
  689. }
  690. }
  691. // 弹窗 Indigo 主题
  692. :deep(.el-dialog) {
  693. .el-dialog__header {
  694. border-bottom: 1px solid #e5e7eb;
  695. padding-bottom: 16px;
  696. }
  697. .el-dialog__footer {
  698. border-top: 1px solid #e5e7eb;
  699. padding-top: 16px;
  700. }
  701. .el-switch.is-checked .el-switch__core {
  702. background-color: #4f46e5;
  703. border-color: #4f46e5;
  704. }
  705. .el-checkbox__input.is-checked .el-checkbox__inner {
  706. background-color: #4f46e5;
  707. border-color: #4f46e5;
  708. }
  709. .el-checkbox__input.is-checked + .el-checkbox__label {
  710. color: #4f46e5;
  711. }
  712. }
  713. </style>