You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
haoliang-net/frontend/src/views/machine/MachineListPage.vue

316 lines
14 KiB
Vue

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<template>
<div>
<div class="mb-16">
<el-button type="primary" @click="handleAdd">+ </el-button>
<el-button @click="importVisible = true">导入</el-button>
<el-button type="success" @click="handleExport"></el-button>
<el-button v-if="selectedRows.length" size="default" @click="batchToggle(0)" :disabled="!selectedRows.some(r => r.isEnabled)">批量停用({{ selectedRows.length }})</el-button>
<el-button v-if="selectedRows.length" size="default" type="primary" @click="batchToggle(1)" :disabled="!selectedRows.some(r => !r.isEnabled)">批量启用({{ selectedRows.length }})</el-button>
<el-button v-if="selectedRows.length" size="default" type="danger" @click="handleBatchDelete">批量删除({{ selectedRows.length }})</el-button>
</div>
<el-form :inline="true" class="mb-16">
<el-form-item label="车间">
<el-select v-model="query.workshopId" clearable placeholder="全部">
<el-option v-for="w in workshopList" :key="w.id" :label="w.name" :value="w.id" />
</el-select>
</el-form-item>
<el-form-item label="在线状态">
<el-select v-model="query.isOnline" clearable placeholder="全部">
<el-option label="在线" :value="1" />
<el-option label="离线" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="品牌">
<el-select v-model="query.brandId" clearable placeholder="全部">
<el-option v-for="b in brandList" :key="b.id" :label="b.brandName" :value="b.id" />
</el-select>
</el-form-item>
<el-form-item>
<el-input v-model="query.keyword" placeholder="机床名称/device_code" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="tableData" border stripe v-loading="loading" @selection-change="(rows: Machine[]) => selectedRows = rows">
<el-table-column type="selection" width="50" fixed="left" align="center" />
<el-table-column label="机床名称" min-width="120" fixed="left" show-overflow-tooltip>
<template #default="{ row }">
<el-link type="primary" @click="goDetail(row.id)">{{ row.name }}</el-link>
</template>
</el-table-column>
<el-table-column prop="deviceCode" label="device_code" show-overflow-tooltip sortable />
<el-table-column prop="workshopName" label="车间" align="center" sortable />
<el-table-column prop="brandName" label="品牌" align="center" sortable />
<el-table-column prop="ipAddress" label="IP地址" />
<el-table-column label="在线状态" align="center">
<template #default="{ row }">
<el-tag :type="row.isOnline ? 'success' : 'info'" size="small">{{ row.isOnline ? '在线' : '离线' }}</el-tag>
<span v-if="row.isOnline && row.lastPingLatency != null" style="margin-left:4px;font-size:12px;color:#909399">{{ row.lastPingLatency }}ms</span>
</template>
</el-table-column>
<el-table-column label="状态" align="center">
<template #default="{ row }">
<el-tag :type="row.isEnabled ? 'success' : 'danger'" size="small">{{ row.isEnabled ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="workerName" label="绑定工人" align="center">
<template #default="{ row }">{{ row.workerName || '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="{ row }">
<div style="white-space:nowrap">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="page.page"
v-model:page-size="page.pageSize"
:page-sizes="[20, 50, 100]"
:total="page.total"
background
layout="total, sizes, prev, pager, next, jumper"
style="margin-top:16px"
@current-change="loadData"
@size-change="handleSizeChange"
/>
<!-- 新增/编辑弹窗 -->
<el-dialog v-model="dialogVisible" :title="editingId ? '编辑机床' : '新增机床'" width="600px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
<el-form-item label="机床名称" prop="name"><el-input v-model="form.name" maxlength="100" /></el-form-item>
<el-form-item label="device_code" prop="deviceCode"><el-input v-model="form.deviceCode" maxlength="100" :disabled="!!editingId" /></el-form-item>
<el-form-item label="所属车间" prop="workshopId">
<el-select v-model="form.workshopId"><el-option v-for="w in workshopList" :key="w.id" :label="w.name" :value="w.id" /></el-select>
</el-form-item>
<el-form-item label="采集地址" prop="collectAddressId">
<el-select v-model="form.collectAddressId" @change="onAddressChange">
<el-option v-for="a in addressList" :key="a.id" :label="a.name" :value="a.id" />
</el-select>
</el-form-item>
<el-form-item label="品牌"><el-input v-model="form.brandName" disabled /></el-form-item>
<el-form-item label="IP地址" prop="ipAddress"><el-input v-model="form.ipAddress" /></el-form-item>
<el-form-item label="绑定工人">
<el-select v-model="form.workerId" clearable>
<el-option v-for="w in workerList" :key="w.id" :label="w.name" :value="w.id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
</template>
</el-dialog>
<!-- 导入弹窗 -->
<el-dialog v-model="importVisible" title="导入机床" width="500px" destroy-on-close>
<el-button type="primary" link @click="downloadTemplate">下载模板</el-button>
<el-upload ref="uploadRef" accept=".xlsx" :limit="1" :auto-upload="false" :on-change="onFileChange" style="margin:16px 0">
<el-button>选择文件</el-button>
</el-upload>
<div v-if="importResult">
<el-tag type="success">成功{{ importResult.successCount }}条</el-tag>
<el-tag v-if="importResult.failCount" type="danger" style="margin-left:8px">失败{{ importResult.failCount }}条</el-tag>
<div v-if="importResult.failDetails?.length" style="margin-top:8px;color:#f56c6c;font-size:12px">
<div v-for="(d, i) in importResult.failDetails" :key="i">{{ d }}</div>
</div>
</div>
<template #footer>
<el-button @click="importVisible = false">关闭</el-button>
<el-button type="primary" :loading="importing" @click="handleImport"></el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import request from '@/utils/request'
import { useMockMode } from '@/composables/useMockMode'
import { View } from '@element-plus/icons-vue'
import type { ApiResponse, Machine, Workshop, Brand, CollectAddress, Worker } from '@/types'
const router = useRouter()
const { isMock } = useMockMode()
const loading = ref(false)
const tableData = ref<Machine[]>([])
const selectedRows = ref<Machine[]>([])
const workshopList = ref<Workshop[]>([])
const brandList = ref<Brand[]>([])
const addressList = ref<CollectAddress[]>([])
const workerList = ref<Worker[]>([])
const query = reactive({
workshopId: undefined as number | undefined,
isOnline: undefined as number | undefined,
brandId: undefined as number | undefined,
keyword: '',
})
const page = reactive({ page: 1, pageSize: 20, total: 0 })
// 弹窗
const dialogVisible = ref(false)
const submitting = ref(false)
const editingId = ref<number | null>(null)
const formRef = ref<FormInstance>()
const form = reactive({
name: '', deviceCode: '', workshopId: undefined as number | undefined,
collectAddressId: undefined as number | undefined, brandId: undefined as number | undefined, brandName: '',
ipAddress: '', workerId: undefined as number | undefined,
})
const formRules: FormRules = {
name: [{ required: true, message: '请输入机床名称', trigger: 'blur' }],
deviceCode: [{ required: true, message: '请输入device_code', trigger: 'blur' }],
workshopId: [{ required: true, message: '请选择车间', trigger: 'change' }],
collectAddressId: [{ required: true, message: '请选择采集地址', trigger: 'change' }],
ipAddress: [{ required: true, message: '请输入IP地址', trigger: 'blur' }],
}
// 导入
const importVisible = ref(false)
const importing = ref(false)
const importFile = ref<File | null>(null)
const importResult = ref<any>(null)
function goDetail(id: number) {
router.push((isMock.value ? '/mock/machine/' : '/machine/') + id)
}
/** 查看详情:在普通路由和 Mock 路径下跳转到详情页 */
function handleViewDetail(row: Machine) {
const currentPath = window.location.pathname
const isMockPath = currentPath.startsWith('/mock')
const prefix = isMockPath ? '/mock' : ''
router.push(`${prefix}/machine/${row.id}`)
}
async function loadData() {
loading.value = true
try {
const r: ApiResponse<{ items: Machine[]; total?: number }> = await request.get('/admin/machine', { params: { ...query, page: page.page, pageSize: page.pageSize } })
tableData.value = r.data?.items || []
page.total = r.data?.total ?? 0
} finally { loading.value = false }
}
function resetQuery() {
Object.assign(query, { workshopId: undefined, isOnline: undefined, brandId: undefined, keyword: '' })
page.page = 1
loadData()
}
/** 每页条数变化时重置到第1页并重新加载 */
function handleSizeChange() {
page.page = 1
loadData()
}
function handleAdd() {
editingId.value = null
Object.assign(form, { name: '', deviceCode: '', workshopId: undefined, collectAddressId: undefined, brandId: undefined, brandName: '', ipAddress: '', workerId: undefined })
dialogVisible.value = true
}
function handleEdit(row: Machine) {
editingId.value = row.id
Object.assign(form, {
name: row.name, deviceCode: row.deviceCode, workshopId: row.workshopId,
collectAddressId: row.collectAddressId, brandId: row.brandId, brandName: row.brandName,
ipAddress: row.ipAddress, workerId: row.workerId,
})
dialogVisible.value = true
}
function onAddressChange(addressId: number) {
const addr = addressList.value.find((a: CollectAddress) => a.id === addressId)
form.brandId = addr ? addr.brandId : undefined
form.brandName = addr ? addr.brandName : ''
}
async function handleSubmit() {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
submitting.value = true
try {
await request[editingId.value ? 'put' : 'post'](editingId.value ? `/admin/machine/${editingId.value}` : '/admin/machine', { ...form })
ElMessage.success('保存成功')
dialogVisible.value = false
loadData()
} finally { submitting.value = false }
}
async function handleDelete(row: Machine) {
await ElMessageBox.confirm(`确定删除【${row.name}】?此操作不可恢复。`, '提示', { type: 'warning' })
await request.delete(`/admin/machine/${row.id}`)
ElMessage.success('已删除')
loadData()
}
async function batchToggle(isEnabled: number) {
await ElMessageBox.confirm(`确定对选中的${selectedRows.value.length}项操作?`, '提示', { type: 'warning' })
// 逐个调用toggle接口后端只提供单个toggle
for (const row of selectedRows.value) {
if ((isEnabled === 1 && !row.isEnabled) || (isEnabled === 0 && row.isEnabled)) {
await request.put(`/admin/machine/${row.id}/toggle`)
}
}
ElMessage.success('操作成功')
loadData()
}
async function handleBatchDelete() {
await ElMessageBox.confirm(`确定删除选中的${selectedRows.value.length}台机床?此操作不可恢复。`, '提示', { type: 'warning' })
await request.post('/admin/machine/batch-delete', { ids: selectedRows.value.map((r: Machine) => r.id) })
ElMessage.success('批量删除成功')
loadData()
}
async function handleExport() {
const baseURL = (request as any).defaults?.baseURL || ''
const qs = new URLSearchParams(query as any).toString()
window.open(`${baseURL}/admin/machine/export?${qs}`, '_blank')
ElMessage.success('正在导出...')
}
function onFileChange(file: any) { importFile.value = file.raw }
function downloadTemplate() { window.open('/api/admin/machine/import-template', '_blank') }
async function handleImport() {
if (!importFile.value) { ElMessage.warning('请选择文件'); return }
importing.value = true
try {
const formData = new FormData()
formData.append('file', importFile.value)
const r = await request.post('/admin/machine/import', formData) as any
importResult.value = r.data
if (r.data?.successCount) ElMessage.success(`成功导入${r.data.successCount}`)
loadData()
} finally { importing.value = false }
}
async function loadDrops() {
const w: ApiResponse<{ items: Workshop[] }> = await request.get('/admin/workshop')
const b: ApiResponse<{ items: Brand[] }> = await request.get('/admin/brand')
const a: ApiResponse<{ items: CollectAddress[] }> = await request.get('/admin/collect-address')
const wk: ApiResponse<{ items: Worker[] }> = await request.get('/admin/worker')
workshopList.value = w.data?.items ?? []
brandList.value = b.data?.items ?? []
addressList.value = a.data?.items ?? []
workerList.value = wk.data?.items ?? []
}
onMounted(() => { loadData(); loadDrops() })
</script>