index.vue 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705
  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.trim="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.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 :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="t('序号')" 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('启用')" 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('操作')" min-width="90" 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({ useScope: 'global' })
  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(20)
  221. const total = ref(0)
  222. // 过滤后的数据
  223. const filteredList = computed(() => {
  224. return machineList.value.filter((item) => {
  225. // 机器ID过滤
  226. if (searchForm.machineId && !item.machineId.toLowerCase().includes(searchForm.machineId.toLowerCase())) {
  227. return false
  228. }
  229. // 名称过滤
  230. if (searchForm.name && !item.name.toLowerCase().includes(searchForm.name.toLowerCase())) {
  231. return false
  232. }
  233. // 启用状态过滤
  234. if (searchForm.enabled !== '' && item.enabled !== searchForm.enabled) {
  235. return false
  236. }
  237. // 时间范围过滤
  238. if (searchForm.dateRange && searchForm.dateRange.length === 2) {
  239. const itemDate = item.createdAt ? item.createdAt.split('T')[0] : ''
  240. const [startDate, endDate] = searchForm.dateRange
  241. if (itemDate < startDate || itemDate > endDate) {
  242. return false
  243. }
  244. }
  245. return true
  246. })
  247. })
  248. // 排序后的数据(后端已分页,前端只做排序)
  249. const sortedList = computed(() => {
  250. const list = [...machineList.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. return list
  272. })
  273. const form = reactive<{
  274. id?: number
  275. machineId: string
  276. name: string
  277. location: string
  278. description: string
  279. enabled: boolean
  280. }>({
  281. machineId: '',
  282. name: '',
  283. location: '',
  284. description: '',
  285. enabled: true
  286. })
  287. const isEdit = computed(() => !!form.id)
  288. const dialogTitle = computed(() => (isEdit.value ? t('编辑机器') : t('新增机器')))
  289. const rules: FormRules = {
  290. machineId: [{ required: true, message: t('请输入机器ID'), trigger: 'blur' }],
  291. name: [{ required: true, message: t('请输入名称'), trigger: 'blur' }]
  292. }
  293. async function getList() {
  294. loading.value = true
  295. try {
  296. // 构建查询参数
  297. const params: Record<string, any> = {
  298. page: currentPage.value,
  299. size: pageSize.value
  300. }
  301. // 搜索关键词(机器ID或名称)
  302. if (searchForm.machineId || searchForm.name) {
  303. params.keyword = searchForm.machineId || searchForm.name
  304. }
  305. // 启用状态过滤
  306. if (searchForm.enabled !== '') {
  307. params.enabled = searchForm.enabled
  308. }
  309. // 排序
  310. if (sortState.prop && sortState.order) {
  311. params.sortBy = sortState.prop
  312. params.sortDir = sortState.order === 'ascending' ? 'ASC' : 'DESC'
  313. }
  314. const res = await listMachines(params)
  315. if (res.success) {
  316. machineList.value = res.data.list
  317. total.value = res.data.total || 0
  318. }
  319. } finally {
  320. loading.value = false
  321. }
  322. }
  323. function handleSearch() {
  324. currentPage.value = 1 // 搜索时重置到第一页
  325. getList()
  326. }
  327. function handleReset() {
  328. searchForm.machineId = ''
  329. searchForm.name = ''
  330. searchForm.enabled = ''
  331. searchForm.dateRange = null
  332. currentPage.value = 1
  333. // 重置排序
  334. sortState.prop = ''
  335. sortState.order = null
  336. getList()
  337. }
  338. // 排序变化处理
  339. function handleSortChange({ prop, order }: { prop: string; order: 'ascending' | 'descending' | null }) {
  340. sortState.prop = prop || ''
  341. sortState.order = order
  342. getList()
  343. }
  344. // 选择变化处理
  345. function handleSelectionChange(rows: MachineDTO[]) {
  346. selectedRows.value = rows
  347. }
  348. // 清除选择
  349. function clearSelection() {
  350. tableRef.value?.clearSelection()
  351. }
  352. // 批量删除
  353. async function handleBatchDelete() {
  354. if (selectedRows.value.length === 0) return
  355. try {
  356. await ElMessageBox.confirm(`${t('确定要删除选中的')} ${selectedRows.value.length} ${t('台机器吗?')}`, t('提示'), {
  357. type: 'warning'
  358. })
  359. deleteLoading.value = true
  360. // 逐个删除
  361. const deletePromises = selectedRows.value.map((row) => deleteMachine(row.id))
  362. await Promise.all(deletePromises)
  363. ElMessage.success(`${t('成功删除')} ${selectedRows.value.length} ${t('台机器')}`)
  364. clearSelection()
  365. getList()
  366. } catch (error) {
  367. if (error !== 'cancel') {
  368. console.error(t('批量删除失败'), error)
  369. ElMessage.error(t('批量删除失败'))
  370. }
  371. } finally {
  372. deleteLoading.value = false
  373. }
  374. }
  375. function handleAdd() {
  376. Object.assign(form, {
  377. id: undefined,
  378. machineId: '',
  379. name: '',
  380. location: '',
  381. description: '',
  382. enabled: true
  383. })
  384. dialogVisible.value = true
  385. }
  386. function handleEdit(row: MachineDTO) {
  387. Object.assign(form, {
  388. id: row.id,
  389. machineId: row.machineId,
  390. name: row.name,
  391. location: row.location || '',
  392. description: row.description || '',
  393. enabled: row.enabled
  394. })
  395. dialogVisible.value = true
  396. }
  397. async function handleDelete(row: MachineDTO) {
  398. if (deleteLoading.value) return
  399. try {
  400. await ElMessageBox.confirm(`${t('确定要删除机器')} "${row.name}" ${t('吗?')}`, t('提示'), {
  401. type: 'warning'
  402. })
  403. deleteLoading.value = true
  404. const res = await deleteMachine(row.id)
  405. if (res.success) {
  406. ElMessage.success(t('删除成功'))
  407. getList()
  408. }
  409. } catch (error) {
  410. if (error !== 'cancel') {
  411. console.error(t('删除失败'), error)
  412. }
  413. } finally {
  414. deleteLoading.value = false
  415. }
  416. }
  417. async function handleSubmit() {
  418. if (!formRef.value) return
  419. await formRef.value.validate(async (valid) => {
  420. if (valid) {
  421. submitLoading.value = true
  422. try {
  423. if (isEdit.value) {
  424. const updateData: MachineUpdateRequest = {
  425. id: form.id!,
  426. name: form.name,
  427. location: form.location || undefined,
  428. description: form.description || undefined,
  429. enabled: form.enabled
  430. }
  431. const res = await updateMachine(updateData)
  432. if (res.success) {
  433. ElMessage.success(t('修改成功'))
  434. dialogVisible.value = false
  435. getList()
  436. }
  437. } else {
  438. const addData: MachineAddRequest = {
  439. machineId: form.machineId,
  440. name: form.name,
  441. location: form.location || undefined,
  442. description: form.description || undefined
  443. }
  444. const res = await addMachine(addData)
  445. if (res.success) {
  446. ElMessage.success(t('新增成功'))
  447. dialogVisible.value = false
  448. getList()
  449. }
  450. }
  451. } finally {
  452. submitLoading.value = false
  453. }
  454. }
  455. })
  456. }
  457. function handleSizeChange(val: number) {
  458. pageSize.value = val
  459. currentPage.value = 1
  460. getList()
  461. }
  462. function handleCurrentChange(val: number) {
  463. currentPage.value = val
  464. getList()
  465. }
  466. onMounted(() => {
  467. getList()
  468. })
  469. </script>
  470. <style lang="scss" scoped>
  471. .page-container {
  472. display: flex;
  473. flex-direction: column;
  474. height: 100%;
  475. padding: 1rem;
  476. box-sizing: border-box;
  477. overflow: hidden;
  478. }
  479. // 批量操作栏
  480. .batch-actions {
  481. flex-shrink: 0;
  482. display: flex;
  483. align-items: center;
  484. gap: 12px;
  485. margin-bottom: 12px;
  486. padding: 12px 16px;
  487. background: #fef3c7;
  488. border: 1px solid #f59e0b;
  489. .batch-info {
  490. font-size: 14px;
  491. color: #92400e;
  492. font-weight: 500;
  493. }
  494. :deep(.el-button--danger) {
  495. background-color: #dc2626;
  496. border-color: #dc2626;
  497. &:hover {
  498. background-color: #ef4444;
  499. border-color: #ef4444;
  500. }
  501. }
  502. }
  503. .search-form {
  504. flex-shrink: 0;
  505. margin-bottom: 16px;
  506. padding: 16px 16px 4px 16px;
  507. background: #f5f7fa;
  508. :deep(.el-form-item) {
  509. margin-bottom: 12px;
  510. margin-right: 16px;
  511. }
  512. :deep(.el-input),
  513. :deep(.el-select) {
  514. width: 160px;
  515. }
  516. :deep(.el-date-editor--daterange) {
  517. width: 280px;
  518. .el-range-input {
  519. width: 90px;
  520. }
  521. .el-range-separator {
  522. width: 30px;
  523. }
  524. }
  525. // Indigo 主题按钮
  526. :deep(.el-button--primary) {
  527. background-color: #4f46e5;
  528. border-color: #4f46e5;
  529. &:hover,
  530. &:focus {
  531. background-color: #6366f1;
  532. border-color: #6366f1;
  533. }
  534. }
  535. }
  536. .table-wrapper {
  537. flex: 1;
  538. min-height: 0;
  539. overflow: hidden;
  540. }
  541. .pagination-container {
  542. flex-shrink: 0;
  543. display: flex;
  544. justify-content: flex-end;
  545. padding-top: 16px;
  546. // Indigo 主题分页
  547. :deep(.el-pagination) {
  548. .el-pager li.is-active {
  549. background-color: #4f46e5;
  550. color: #fff;
  551. }
  552. .el-pager li:not(.is-active):hover {
  553. color: #4f46e5;
  554. }
  555. .btn-prev:hover,
  556. .btn-next:hover {
  557. color: #4f46e5;
  558. }
  559. }
  560. }
  561. // 表格样式
  562. :deep(.el-table) {
  563. // 斑马纹对比度
  564. --el-table-row-hover-bg-color: #f0f0ff;
  565. .el-table__row--striped td.el-table__cell {
  566. background-color: #f8f9fc;
  567. }
  568. // 表头样式
  569. .el-table__header th {
  570. background-color: #f5f7fa;
  571. color: #333;
  572. font-weight: 600;
  573. }
  574. // 链接颜色
  575. .el-link--primary {
  576. color: #4f46e5;
  577. &:hover {
  578. color: #6366f1;
  579. }
  580. }
  581. // 主要按钮颜色
  582. .el-button--primary.is-link {
  583. color: #4f46e5;
  584. &:hover {
  585. color: #6366f1;
  586. }
  587. }
  588. // 排序图标颜色
  589. .el-table__column-filter-trigger,
  590. .caret-wrapper {
  591. .sort-caret.ascending {
  592. border-bottom-color: #4f46e5;
  593. }
  594. .sort-caret.descending {
  595. border-top-color: #4f46e5;
  596. }
  597. }
  598. // Checkbox Indigo 主题
  599. .el-checkbox__input.is-checked .el-checkbox__inner {
  600. background-color: #4f46e5;
  601. border-color: #4f46e5;
  602. }
  603. .el-checkbox__input.is-indeterminate .el-checkbox__inner {
  604. background-color: #4f46e5;
  605. border-color: #4f46e5;
  606. }
  607. }
  608. // 弹窗 Indigo 主题
  609. :deep(.el-dialog) {
  610. .el-dialog__header {
  611. border-bottom: 1px solid #e5e7eb;
  612. padding-bottom: 16px;
  613. }
  614. .el-dialog__footer {
  615. border-top: 1px solid #e5e7eb;
  616. padding-top: 16px;
  617. }
  618. .el-button--primary {
  619. background-color: #4f46e5;
  620. border-color: #4f46e5;
  621. &:hover,
  622. &:focus {
  623. background-color: #6366f1;
  624. border-color: #6366f1;
  625. }
  626. }
  627. // Switch Indigo 主题
  628. .el-switch.is-checked .el-switch__core {
  629. background-color: #4f46e5;
  630. border-color: #4f46e5;
  631. }
  632. }
  633. </style>