|
|
<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 ? '在线' : '离线' }}{{ row.lastPingLatency != null ? `-${row.lastPingLatency}ms` : '' }}</el-tag>
|
|
|
</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>
|