index.vue 22 KB

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