index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  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('机器ID')">
  7. <el-input
  8. v-model="searchForm.machineId"
  9. placeholder="请输入机器ID"
  10. clearable
  11. data-id="search-machine-id"
  12. @keyup.enter="handleSearch"
  13. />
  14. </el-form-item>
  15. <el-form-item :label="t('名称')">
  16. <el-input
  17. v-model="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 :label="t('创建时间')">
  32. <el-date-picker
  33. v-model="searchForm.dateRange"
  34. type="daterange"
  35. range-separator="至"
  36. start-placeholder="开始日期"
  37. end-placeholder="结束日期"
  38. value-format="YYYY-MM-DD"
  39. data-id="search-date-range"
  40. />
  41. </el-form-item>
  42. <el-form-item>
  43. <el-button type="primary" :icon="Search" data-id="btn-search" @click="handleSearch">
  44. {{ t('查询') }}
  45. </el-button>
  46. <el-button :icon="RefreshRight" data-id="btn-reset" @click="handleReset">{{ t('重置') }}</el-button>
  47. <el-button type="primary" :icon="Plus" data-id="btn-add-machine" @click="handleAdd">
  48. {{ t('新增') }}
  49. </el-button>
  50. </el-form-item>
  51. </el-form>
  52. </div>
  53. <!-- 批量操作栏 -->
  54. <div v-if="selectedRows.length > 0" class="batch-actions">
  55. <span class="batch-info">{{ t('已选择') }} {{ selectedRows.length }} {{ t('项') }}</span>
  56. <el-button type="danger" :icon="Delete" :loading="deleteLoading" @click="handleBatchDelete">
  57. {{ t('批量删除') }}
  58. </el-button>
  59. <el-button @click="clearSelection">{{ t('取消选择') }}</el-button>
  60. </div>
  61. <!-- 数据表格 -->
  62. <div class="table-wrapper">
  63. <el-table
  64. ref="tableRef"
  65. v-loading="loading"
  66. :data="sortedList"
  67. stripe
  68. size="default"
  69. data-id="machine-table"
  70. height="100%"
  71. @selection-change="handleSelectionChange"
  72. @sort-change="handleSortChange"
  73. >
  74. <el-table-column type="selection" width="50" align="center" />
  75. <el-table-column type="index" label="序号" width="60" align="center" />
  76. <el-table-column
  77. prop="machineId"
  78. :label="t('机器ID')"
  79. min-width="120"
  80. sortable="custom"
  81. show-overflow-tooltip
  82. />
  83. <el-table-column prop="name" :label="t('名称')" min-width="120" sortable="custom" show-overflow-tooltip>
  84. <template #default="{ row }">
  85. <el-link type="primary" :data-id="`link-edit-${row.machineId}`" @click="handleEdit(row)">
  86. {{ row.name }}
  87. </el-link>
  88. </template>
  89. </el-table-column>
  90. <el-table-column prop="location" :label="t('位置')" min-width="120" show-overflow-tooltip />
  91. <el-table-column prop="description" :label="t('描述')" min-width="150" show-overflow-tooltip />
  92. <el-table-column prop="cameraCount" :label="t('摄像头数')" sortable="custom" align="center">
  93. <template #default="{ row }">
  94. <el-tag type="info">{{ row.cameraCount || 0 }}</el-tag>
  95. </template>
  96. </el-table-column>
  97. <el-table-column prop="enabled" :label="t('启用')" width="80" sortable="custom" align="center">
  98. <template #default="{ row }">
  99. <el-tag :type="row.enabled ? 'success' : 'info'">
  100. {{ row.enabled ? t('是') : t('否') }}
  101. </el-tag>
  102. </template>
  103. </el-table-column>
  104. <el-table-column prop="createdAt" :label="t('创建时间')" width="160" sortable="custom" align="center">
  105. <template #default="{ row }">
  106. {{ formatDateTime(row.createdAt) }}
  107. </template>
  108. </el-table-column>
  109. <el-table-column :label="t('操作')" width="150" align="center" fixed="right">
  110. <template #default="{ row }">
  111. <el-button type="primary" link :icon="Edit" :data-id="`btn-edit-${row.machineId}`" @click="handleEdit(row)">
  112. {{ t('编辑') }}
  113. </el-button>
  114. <el-button
  115. type="danger"
  116. link
  117. :icon="Delete"
  118. :disabled="deleteLoading"
  119. :data-id="`btn-delete-${row.machineId}`"
  120. @click="handleDelete(row)"
  121. >
  122. {{ t('删除') }}
  123. </el-button>
  124. </template>
  125. </el-table-column>
  126. </el-table>
  127. </div>
  128. <!-- 分页 -->
  129. <div class="pagination-container">
  130. <el-pagination
  131. v-model:current-page="currentPage"
  132. v-model:page-size="pageSize"
  133. :page-sizes="[10, 20, 50, 100]"
  134. :total="total"
  135. layout="total, sizes, prev, pager, next, jumper"
  136. background
  137. @size-change="handleSizeChange"
  138. @current-change="handleCurrentChange"
  139. />
  140. </div>
  141. <!-- 新增/编辑弹窗 -->
  142. <el-dialog v-model="dialogVisible" :title="dialogTitle" width="500px" destroy-on-close data-id="dialog-machine">
  143. <el-form ref="formRef" :model="form" :rules="rules" label-width="80px" data-id="form-machine">
  144. <el-form-item :label="t('机器ID')" prop="machineId">
  145. <el-input v-model="form.machineId" placeholder="请输入机器ID" :disabled="isEdit" data-id="input-machine-id" />
  146. </el-form-item>
  147. <el-form-item :label="t('名称')" prop="name">
  148. <el-input v-model="form.name" placeholder="请输入名称" data-id="input-name" />
  149. </el-form-item>
  150. <el-form-item :label="t('位置')" prop="location">
  151. <el-input v-model="form.location" placeholder="请输入位置" data-id="input-location" />
  152. </el-form-item>
  153. <el-form-item :label="t('描述')" prop="description">
  154. <el-input
  155. v-model="form.description"
  156. type="textarea"
  157. :rows="3"
  158. placeholder="请输入描述"
  159. data-id="input-description"
  160. />
  161. </el-form-item>
  162. <el-form-item v-if="isEdit" :label="t('启用状态')">
  163. <el-switch v-model="form.enabled" data-id="switch-enabled" />
  164. </el-form-item>
  165. </el-form>
  166. <template #footer>
  167. <el-button data-id="btn-cancel" @click="dialogVisible = false">{{ t('取消') }}</el-button>
  168. <el-button type="primary" :loading="submitLoading" data-id="btn-submit" @click="handleSubmit">
  169. {{ t('确定') }}
  170. </el-button>
  171. </template>
  172. </el-dialog>
  173. </div>
  174. </template>
  175. <script setup lang="ts">
  176. import { ref, reactive, onMounted, computed } from 'vue'
  177. import { ElMessage, ElMessageBox, type FormInstance, type FormRules, type TableInstance } from 'element-plus'
  178. import { Plus, Edit, Delete, Search, RefreshRight } from '@element-plus/icons-vue'
  179. import { listMachines, addMachine, updateMachine, deleteMachine } from '@/api/machine'
  180. import type { MachineDTO, MachineAddRequest, MachineUpdateRequest } from '@/types'
  181. import dayjs from 'dayjs'
  182. import { useI18n } from 'vue-i18n'
  183. const { t } = useI18n()
  184. // 格式化时间
  185. function formatDateTime(dateStr: string | undefined): string {
  186. if (!dateStr) return '-'
  187. return dayjs(dateStr).format('YYYY-MM-DD HH:mm')
  188. }
  189. const loading = ref(false)
  190. const submitLoading = ref(false)
  191. const deleteLoading = ref(false)
  192. const machineList = ref<MachineDTO[]>([])
  193. const dialogVisible = ref(false)
  194. const formRef = ref<FormInstance>()
  195. const tableRef = ref<TableInstance>()
  196. // 选中的行
  197. const selectedRows = ref<MachineDTO[]>([])
  198. // 排序状态
  199. const sortState = reactive<{
  200. prop: string
  201. order: 'ascending' | 'descending' | null
  202. }>({
  203. prop: '',
  204. order: null
  205. })
  206. // 搜索表单
  207. const searchForm = reactive<{
  208. machineId: string
  209. name: string
  210. enabled: boolean | ''
  211. dateRange: [string, string] | null
  212. }>({
  213. machineId: '',
  214. name: '',
  215. enabled: '',
  216. dateRange: null
  217. })
  218. // 分页相关
  219. const currentPage = ref(1)
  220. const pageSize = ref(30)
  221. // 过滤后的数据
  222. const filteredList = computed(() => {
  223. return machineList.value.filter((item) => {
  224. // 机器ID过滤
  225. if (searchForm.machineId && !item.machineId.toLowerCase().includes(searchForm.machineId.toLowerCase())) {
  226. return false
  227. }
  228. // 名称过滤
  229. if (searchForm.name && !item.name.toLowerCase().includes(searchForm.name.toLowerCase())) {
  230. return false
  231. }
  232. // 启用状态过滤
  233. if (searchForm.enabled !== '' && item.enabled !== searchForm.enabled) {
  234. return false
  235. }
  236. // 时间范围过滤
  237. if (searchForm.dateRange && searchForm.dateRange.length === 2) {
  238. const itemDate = item.createdAt ? item.createdAt.split('T')[0] : ''
  239. const [startDate, endDate] = searchForm.dateRange
  240. if (itemDate < startDate || itemDate > endDate) {
  241. return false
  242. }
  243. }
  244. return true
  245. })
  246. })
  247. const total = computed(() => filteredList.value.length)
  248. // 排序后的数据
  249. const sortedList = computed(() => {
  250. const list = [...filteredList.value]
  251. if (sortState.prop && sortState.order) {
  252. list.sort((a, b) => {
  253. const aVal = a[sortState.prop as keyof MachineDTO]
  254. const bVal = b[sortState.prop as keyof MachineDTO]
  255. // 处理空值
  256. if (aVal == null && bVal == null) return 0
  257. if (aVal == null) return sortState.order === 'ascending' ? -1 : 1
  258. if (bVal == null) return sortState.order === 'ascending' ? 1 : -1
  259. // 比较
  260. let result = 0
  261. if (typeof aVal === 'number' && typeof bVal === 'number') {
  262. result = aVal - bVal
  263. } else if (typeof aVal === 'boolean' && typeof bVal === 'boolean') {
  264. result = aVal === bVal ? 0 : aVal ? 1 : -1
  265. } else {
  266. result = String(aVal).localeCompare(String(bVal))
  267. }
  268. return sortState.order === 'ascending' ? result : -result
  269. })
  270. }
  271. // 分页
  272. const start = (currentPage.value - 1) * pageSize.value
  273. const end = start + pageSize.value
  274. return list.slice(start, end)
  275. })
  276. const form = reactive<{
  277. id?: number
  278. machineId: string
  279. name: string
  280. location: string
  281. description: string
  282. enabled: boolean
  283. }>({
  284. machineId: '',
  285. name: '',
  286. location: '',
  287. description: '',
  288. enabled: true
  289. })
  290. const isEdit = computed(() => !!form.id)
  291. const dialogTitle = computed(() => (isEdit.value ? t('编辑机器') : t('新增机器')))
  292. const rules: FormRules = {
  293. machineId: [{ required: true, message: t('请输入机器ID'), trigger: 'blur' }],
  294. name: [{ required: true, message: t('请输入名称'), trigger: 'blur' }]
  295. }
  296. async function getList() {
  297. loading.value = true
  298. try {
  299. const res = await listMachines()
  300. if (res.code === 200) {
  301. machineList.value = res.data
  302. }
  303. } finally {
  304. loading.value = false
  305. }
  306. }
  307. function handleSearch() {
  308. currentPage.value = 1 // 搜索时重置到第一页
  309. }
  310. function handleReset() {
  311. searchForm.machineId = ''
  312. searchForm.name = ''
  313. searchForm.enabled = ''
  314. searchForm.dateRange = null
  315. currentPage.value = 1
  316. // 重置排序
  317. sortState.prop = ''
  318. sortState.order = null
  319. }
  320. // 排序变化处理
  321. function handleSortChange({ prop, order }: { prop: string; order: 'ascending' | 'descending' | null }) {
  322. sortState.prop = prop || ''
  323. sortState.order = order
  324. }
  325. // 选择变化处理
  326. function handleSelectionChange(rows: MachineDTO[]) {
  327. selectedRows.value = rows
  328. }
  329. // 清除选择
  330. function clearSelection() {
  331. tableRef.value?.clearSelection()
  332. }
  333. // 批量删除
  334. async function handleBatchDelete() {
  335. if (selectedRows.value.length === 0) return
  336. try {
  337. await ElMessageBox.confirm(`${t('确定要删除选中的')} ${selectedRows.value.length} ${t('台机器吗?')}`, t('提示'), {
  338. type: 'warning'
  339. })
  340. deleteLoading.value = true
  341. // 逐个删除
  342. const deletePromises = selectedRows.value.map((row) => deleteMachine(row.id))
  343. await Promise.all(deletePromises)
  344. ElMessage.success(`${t('成功删除')} ${selectedRows.value.length} ${t('台机器')}`)
  345. clearSelection()
  346. getList()
  347. } catch (error) {
  348. if (error !== 'cancel') {
  349. console.error(t('批量删除失败'), error)
  350. ElMessage.error(t('批量删除失败'))
  351. }
  352. } finally {
  353. deleteLoading.value = false
  354. }
  355. }
  356. function handleAdd() {
  357. Object.assign(form, {
  358. id: undefined,
  359. machineId: '',
  360. name: '',
  361. location: '',
  362. description: '',
  363. enabled: true
  364. })
  365. dialogVisible.value = true
  366. }
  367. function handleEdit(row: MachineDTO) {
  368. Object.assign(form, {
  369. id: row.id,
  370. machineId: row.machineId,
  371. name: row.name,
  372. location: row.location || '',
  373. description: row.description || '',
  374. enabled: row.enabled
  375. })
  376. dialogVisible.value = true
  377. }
  378. async function handleDelete(row: MachineDTO) {
  379. if (deleteLoading.value) return
  380. try {
  381. await ElMessageBox.confirm(`${t('确定要删除机器')} "${row.name}" ${t('吗?')}`, t('提示'), {
  382. type: 'warning'
  383. })
  384. deleteLoading.value = true
  385. const res = await deleteMachine(row.id)
  386. if (res.code === 200) {
  387. ElMessage.success(t('删除成功'))
  388. getList()
  389. }
  390. } catch (error) {
  391. if (error !== 'cancel') {
  392. console.error(t('删除失败'), error)
  393. }
  394. } finally {
  395. deleteLoading.value = false
  396. }
  397. }
  398. async function handleSubmit() {
  399. if (!formRef.value) return
  400. await formRef.value.validate(async (valid) => {
  401. if (valid) {
  402. submitLoading.value = true
  403. try {
  404. if (isEdit.value) {
  405. const updateData: MachineUpdateRequest = {
  406. id: form.id!,
  407. name: form.name,
  408. location: form.location || undefined,
  409. description: form.description || undefined,
  410. enabled: form.enabled
  411. }
  412. const res = await updateMachine(updateData)
  413. if (res.code === 200) {
  414. ElMessage.success(t('修改成功'))
  415. dialogVisible.value = false
  416. getList()
  417. }
  418. } else {
  419. const addData: MachineAddRequest = {
  420. machineId: form.machineId,
  421. name: form.name,
  422. location: form.location || undefined,
  423. description: form.description || undefined
  424. }
  425. const res = await addMachine(addData)
  426. if (res.code === 200) {
  427. ElMessage.success(t('新增成功'))
  428. dialogVisible.value = false
  429. getList()
  430. }
  431. }
  432. } finally {
  433. submitLoading.value = false
  434. }
  435. }
  436. })
  437. }
  438. function handleSizeChange(val: number) {
  439. pageSize.value = val
  440. currentPage.value = 1
  441. }
  442. function handleCurrentChange(val: number) {
  443. currentPage.value = val
  444. }
  445. onMounted(() => {
  446. getList()
  447. })
  448. </script>
  449. <style lang="scss" scoped>
  450. .page-container {
  451. display: flex;
  452. flex-direction: column;
  453. height: 100%;
  454. padding: 1rem;
  455. box-sizing: border-box;
  456. overflow: hidden;
  457. }
  458. // 批量操作栏
  459. .batch-actions {
  460. flex-shrink: 0;
  461. display: flex;
  462. align-items: center;
  463. gap: 12px;
  464. margin-bottom: 12px;
  465. padding: 12px 16px;
  466. background: #fef3c7;
  467. border: 1px solid #f59e0b;
  468. .batch-info {
  469. font-size: 14px;
  470. color: #92400e;
  471. font-weight: 500;
  472. }
  473. :deep(.el-button--danger) {
  474. background-color: #dc2626;
  475. border-color: #dc2626;
  476. &:hover {
  477. background-color: #ef4444;
  478. border-color: #ef4444;
  479. }
  480. }
  481. }
  482. .search-form {
  483. flex-shrink: 0;
  484. margin-bottom: 16px;
  485. padding: 16px;
  486. background: #f5f7fa;
  487. :deep(.el-form-item) {
  488. margin-bottom: 0;
  489. margin-right: 16px;
  490. }
  491. :deep(.el-input),
  492. :deep(.el-select) {
  493. width: 160px;
  494. }
  495. :deep(.el-date-editor--daterange) {
  496. width: 280px;
  497. .el-range-input {
  498. width: 90px;
  499. }
  500. .el-range-separator {
  501. width: 30px;
  502. }
  503. }
  504. // Indigo 主题按钮
  505. :deep(.el-button--primary) {
  506. background-color: #4f46e5;
  507. border-color: #4f46e5;
  508. &:hover,
  509. &:focus {
  510. background-color: #6366f1;
  511. border-color: #6366f1;
  512. }
  513. }
  514. }
  515. .table-wrapper {
  516. flex: 1;
  517. min-height: 0;
  518. overflow: hidden;
  519. }
  520. .pagination-container {
  521. flex-shrink: 0;
  522. display: flex;
  523. justify-content: flex-end;
  524. padding-top: 16px;
  525. // Indigo 主题分页
  526. :deep(.el-pagination) {
  527. .el-pager li.is-active {
  528. background-color: #4f46e5;
  529. color: #fff;
  530. }
  531. .el-pager li:not(.is-active):hover {
  532. color: #4f46e5;
  533. }
  534. .btn-prev:hover,
  535. .btn-next:hover {
  536. color: #4f46e5;
  537. }
  538. }
  539. }
  540. // 表格样式
  541. :deep(.el-table) {
  542. // 斑马纹对比度
  543. --el-table-row-hover-bg-color: #f0f0ff;
  544. .el-table__row--striped td.el-table__cell {
  545. background-color: #f8f9fc;
  546. }
  547. // 表头样式
  548. .el-table__header th {
  549. background-color: #f5f7fa;
  550. color: #333;
  551. font-weight: 600;
  552. }
  553. // 链接颜色
  554. .el-link--primary {
  555. color: #4f46e5;
  556. &:hover {
  557. color: #6366f1;
  558. }
  559. }
  560. // 主要按钮颜色
  561. .el-button--primary.is-link {
  562. color: #4f46e5;
  563. &:hover {
  564. color: #6366f1;
  565. }
  566. }
  567. // 排序图标颜色
  568. .el-table__column-filter-trigger,
  569. .caret-wrapper {
  570. .sort-caret.ascending {
  571. border-bottom-color: #4f46e5;
  572. }
  573. .sort-caret.descending {
  574. border-top-color: #4f46e5;
  575. }
  576. }
  577. // Checkbox Indigo 主题
  578. .el-checkbox__input.is-checked .el-checkbox__inner {
  579. background-color: #4f46e5;
  580. border-color: #4f46e5;
  581. }
  582. .el-checkbox__input.is-indeterminate .el-checkbox__inner {
  583. background-color: #4f46e5;
  584. border-color: #4f46e5;
  585. }
  586. }
  587. // 弹窗 Indigo 主题
  588. :deep(.el-dialog) {
  589. .el-dialog__header {
  590. border-bottom: 1px solid #e5e7eb;
  591. padding-bottom: 16px;
  592. }
  593. .el-dialog__footer {
  594. border-top: 1px solid #e5e7eb;
  595. padding-top: 16px;
  596. }
  597. .el-button--primary {
  598. background-color: #4f46e5;
  599. border-color: #4f46e5;
  600. &:hover,
  601. &:focus {
  602. background-color: #6366f1;
  603. border-color: #6366f1;
  604. }
  605. }
  606. // Switch Indigo 主题
  607. .el-switch.is-checked .el-switch__core {
  608. background-color: #4f46e5;
  609. border-color: #4f46e5;
  610. }
  611. }
  612. </style>