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.
630 lines
19 KiB
Vue
630 lines
19 KiB
Vue
<template>
|
|
<ContentWrap>
|
|
<el-form class="-mb-15px" :model="queryParams" ref="queryFormRef" :inline="true" label-width="80px">
|
|
<el-form-item label="名称" prop="name">
|
|
<el-input
|
|
v-model="queryParams.name" placeholder="请输入名称" clearable @keyup.enter="handleQuery"
|
|
class="!w-240px" />
|
|
</el-form-item>
|
|
<el-form-item label="备注" prop="remark">
|
|
<el-input
|
|
v-model="queryParams.remark" placeholder="请输入备注" clearable @keyup.enter="handleQuery"
|
|
class="!w-240px" />
|
|
</el-form-item>
|
|
<el-form-item label="启用状态" prop="state">
|
|
<el-select v-model="queryParams.state" placeholder="请选择启用状态" clearable class="!w-240px">
|
|
<el-option
|
|
v-for="dict in getStrDictOptions(DICT_TYPE.COMMON_STATUS)" :key="dict.value" :label="dict.label"
|
|
:value="dict.value" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item>
|
|
<el-button type="primary" @click="openCreateDialog">
|
|
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
|
</el-button>
|
|
<el-button @click="handleQuery">
|
|
<Icon icon="ep:search" class="mr-5px" /> 搜索
|
|
</el-button>
|
|
<el-button @click="resetQuery">
|
|
<Icon icon="ep:refresh" class="mr-5px" /> 重置
|
|
</el-button>
|
|
</el-form-item>
|
|
</el-form>
|
|
</ContentWrap>
|
|
|
|
<ContentWrap>
|
|
<div class="dashboard-card-list">
|
|
<el-empty v-if="!loading && list.length === 0" description="暂无数据" />
|
|
<el-row v-else :gutter="16">
|
|
<el-col v-for="item in list" :key="item.id" :xl="6" :lg="8" :md="12" :sm="24" :xs="24" class="mb-16px">
|
|
<el-card shadow="hover" class="dashboard-card" :body-style="{ padding: '0' }">
|
|
<div class="dashboard-card-image-wrapper" @click="handlePreview(item)">
|
|
<img class="dashboard-card-image" :src="getDashboardImage(item)" alt="封面图" />
|
|
<div class="dashboard-card-state">
|
|
<el-tag v-if="item.state === 1" type="success" size="small">
|
|
启用
|
|
</el-tag>
|
|
<el-tag v-else type="info" size="small">
|
|
禁用
|
|
</el-tag>
|
|
</div>
|
|
</div>
|
|
<div class="dashboard-card-body">
|
|
<div class="dashboard-card-main">
|
|
<div class="dashboard-card-title" :title="item.name">
|
|
{{ item.name || '-' }}
|
|
</div>
|
|
<div class="dashboard-card-remark" :title="item.remark">
|
|
{{ item.remark || '暂无描述' }}
|
|
</div>
|
|
</div>
|
|
<div class="dashboard-card-actions">
|
|
<el-dropdown trigger="click" placement="bottom-end">
|
|
<el-button text circle>
|
|
<Icon icon="ep:more" />
|
|
</el-button>
|
|
<template #dropdown>
|
|
<el-dropdown-menu class="dashboard-card-menu">
|
|
<el-dropdown-item class="dashboard-card-menu-primary" @click="openEditDialog(item)">编辑</el-dropdown-item>
|
|
<el-dropdown-item divided class="dashboard-card-menu-danger" @click="handleDelete(item)">删除</el-dropdown-item>
|
|
</el-dropdown-menu>
|
|
</template>
|
|
</el-dropdown>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
</el-col>
|
|
</el-row>
|
|
<Pagination
|
|
v-if="total > 0" :total="total" v-model:page="queryParams.pageNo" v-model:limit="queryParams.pageSize"
|
|
@pagination="getList" />
|
|
</div>
|
|
</ContentWrap>
|
|
|
|
<el-dialog
|
|
v-model="createDialogVisible" :title="dialogMode === 'create' ? '新增数据大屏' : '编辑数据大屏'" width="600px"
|
|
draggable @closed="handleCreateDialogClosed">
|
|
<el-form :model="createForm" ref="createFormRef" label-width="80px" :rules="createFormRules">
|
|
<el-form-item label="名称" prop="name">
|
|
<el-input v-model="createForm.name" placeholder="请输入名称" />
|
|
</el-form-item>
|
|
<el-form-item label="大屏类型" prop="type">
|
|
<el-select v-model="createForm.type" placeholder="请选择大屏类型" class="!w-240px">
|
|
<el-option
|
|
v-for="dict in getStrDictOptions('mes_goview_type')" :key="dict.value" :label="dict.label"
|
|
:value="dict.value" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="产线" prop="orgId">
|
|
<el-tree-select
|
|
v-model="createForm.orgId" :data="organizationTree" :props="lineTreeProps" filterable clearable
|
|
class="!w-240px" placeholder="请选择产线" @change="handleOrgChange" />
|
|
</el-form-item>
|
|
<el-form-item label="设备" v-if="createForm.type === '1'">
|
|
<div class="dashboard-device-group-list">
|
|
<div v-for="(group, index) in deviceAttrSelections" :key="index" class="dashboard-device-group">
|
|
<el-select
|
|
v-model="group.deviceId" placeholder="请选择设备" clearable filterable class="!w-160px mr-8px"
|
|
@change="(val) => handleDeviceChange(val, index)">
|
|
<el-option v-for="item in deviceList" :key="item.id" :label="item.deviceName" :value="item.id" />
|
|
</el-select>
|
|
<el-select
|
|
v-model="group.attributeIds" multiple collapse-tags collapse-tags-tooltip
|
|
:disabled="!group.deviceId" placeholder="请选择点位" clearable filterable class="!w-260px">
|
|
<el-option
|
|
v-for="attr in (deviceAttributeOptionsMap[String(group.deviceId)] || [])" :key="attr.id"
|
|
:label="attr.attributeName" :value="attr.id" />
|
|
</el-select>
|
|
<el-button
|
|
v-if="deviceAttrSelections.length > 1" type="danger" text class="dashboard-device-remove-btn"
|
|
@click="removeDeviceAttrGroup(index)" circle>
|
|
<Icon icon="ep:minus" />
|
|
</el-button>
|
|
</div>
|
|
<el-button
|
|
type="primary" text @click="addDeviceAttrGroup" :disabled="deviceAttrSelections.length >= 8"
|
|
class="mt-8px">
|
|
<Icon icon="ep:plus" class="mr-5px" /> 添加设备
|
|
</el-button>
|
|
</div>
|
|
</el-form-item>
|
|
<el-form-item label="内容">
|
|
<el-input v-model="createForm.content" type="textarea" :rows="4" placeholder="请输入内容" />
|
|
</el-form-item>
|
|
<el-form-item label="备注">
|
|
<el-input v-model="createForm.remark" placeholder="请输入备注" />
|
|
</el-form-item>
|
|
<el-form-item label="启用状态">
|
|
<el-switch v-model="createForm.state" :active-value="1" :inactive-value="0" />
|
|
</el-form-item>
|
|
</el-form>
|
|
<template #footer>
|
|
<el-button @click="createDialogVisible = false">取 消</el-button>
|
|
<el-button type="primary" :loading="createLoading" @click="submitDialog">
|
|
确 定
|
|
</el-button>
|
|
</template>
|
|
</el-dialog>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { getStrDictOptions, DICT_TYPE } from '@/utils/dict'
|
|
import request from '@/config/axios'
|
|
import defaultImage from '@/assets/imgs/logo.png'
|
|
import dashboardImage1 from '@/assets/imgs/dashboard1.png'
|
|
import dashboardImage2 from '@/assets/imgs/dashboard2.png'
|
|
import { OrganizationApi } from '@/api/mes/organization'
|
|
import { handleTree } from '@/utils/tree'
|
|
import { DeviceApi } from '@/api/iot/device'
|
|
|
|
defineOptions({ name: 'DashboardList' })
|
|
|
|
const router = useRouter()
|
|
const message = useMessage()
|
|
|
|
interface DashboardItem {
|
|
id: number
|
|
name: string
|
|
remark: string
|
|
state: number
|
|
indexImage?: string
|
|
route?: string
|
|
content?: string
|
|
type?: string
|
|
orgId?: number | string
|
|
orgName?: string
|
|
deviceIds?: string | { deviceId: number; attributesIds: number[] }[]
|
|
deviceIdsList?: { deviceId: number; attributesIds: number[] }[]
|
|
}
|
|
|
|
const loading = ref(false)
|
|
const list = ref<DashboardItem[]>([])
|
|
const total = ref(0)
|
|
|
|
const getDashboardImage = (item: DashboardItem) => {
|
|
if (item.name === '智能制造产线任务总览') {
|
|
return dashboardImage1
|
|
}
|
|
if (item.name === '产线运行看板') {
|
|
return dashboardImage2
|
|
}
|
|
return item.indexImage || defaultImage
|
|
}
|
|
|
|
const queryParams = reactive({
|
|
pageNo: 1,
|
|
pageSize: 10,
|
|
name: undefined as string | undefined,
|
|
remark: undefined as string | undefined,
|
|
state: undefined as string | undefined
|
|
})
|
|
|
|
const queryFormRef = ref()
|
|
|
|
const createDialogVisible = ref(false)
|
|
const createLoading = ref(false)
|
|
const createFormRef = ref()
|
|
const dialogMode = ref<'create' | 'edit'>('create')
|
|
const editingId = ref<number | null>(null)
|
|
const createForm = reactive({
|
|
name: '',
|
|
remark: '',
|
|
state: 1,
|
|
type: '',
|
|
orgId: undefined as number | string | undefined,
|
|
orgName: '',
|
|
deviceIdsList: [] as { deviceId: number; attributesIds: number[] }[],
|
|
indexImage: '',
|
|
route: '',
|
|
content: ''
|
|
})
|
|
|
|
const createFormRules = reactive({
|
|
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
|
|
type: [{ required: true, message: '大屏类型不能为空', trigger: 'change' }],
|
|
orgId: [{ required: true, message: '产线不能为空', trigger: 'change' }]
|
|
})
|
|
|
|
const organizationTree = ref<any[]>([])
|
|
const lineTreeProps = {
|
|
label: 'name',
|
|
children: 'children',
|
|
value: 'id'
|
|
}
|
|
|
|
const deviceList = ref<any[]>([])
|
|
const deviceAttrSelections = ref<{ deviceId?: number | string; attributeIds: number[] }[]>([
|
|
{ deviceId: undefined, attributeIds: [] }
|
|
])
|
|
const deviceAttributeOptionsMap = ref<Record<string, any[]>>({})
|
|
|
|
const getList = async () => {
|
|
loading.value = true
|
|
try {
|
|
const data = await request.get({
|
|
url: '/mes/goview/page',
|
|
params: queryParams
|
|
})
|
|
list.value = (data?.list || []) as DashboardItem[]
|
|
total.value = data?.total || 0
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const handleQuery = () => {
|
|
queryParams.pageNo = 1
|
|
getList()
|
|
}
|
|
|
|
const resetQuery = () => {
|
|
queryFormRef.value?.resetFields?.()
|
|
handleQuery()
|
|
}
|
|
|
|
const handlePreview = (item: DashboardItem) => {
|
|
const typeRoute = getRouteByType(item.type)
|
|
const route = typeRoute || item.route || ''
|
|
if (!route) {
|
|
message.error('未配置预览路由')
|
|
return
|
|
}
|
|
const path = route.startsWith('/') ? route : `/${route}`
|
|
const queryParams = new URLSearchParams()
|
|
if (item.id) queryParams.append('goviewId', String(item.id))
|
|
if (item.orgId) queryParams.append('orgId', String(item.orgId))
|
|
const queryString = queryParams.toString()
|
|
const url = router.resolve(path + (queryString ? `?${queryString}` : '')).href
|
|
window.open(url, '_blank')
|
|
}
|
|
|
|
const resetCreateForm = () => {
|
|
createForm.name = ''
|
|
createForm.remark = ''
|
|
createForm.state = 1
|
|
createForm.type = ''
|
|
createForm.orgId = undefined
|
|
createForm.orgName = ''
|
|
createForm.deviceIdsList = []
|
|
createForm.indexImage = ''
|
|
createForm.route = ''
|
|
createForm.content = ''
|
|
deviceAttrSelections.value = [{ deviceId: undefined, attributeIds: [] }]
|
|
}
|
|
|
|
const handleCreateDialogClosed = () => {
|
|
resetCreateForm()
|
|
createFormRef.value?.resetFields?.()
|
|
}
|
|
|
|
const openCreateDialog = () => {
|
|
dialogMode.value = 'create'
|
|
editingId.value = null
|
|
resetCreateForm()
|
|
createDialogVisible.value = true
|
|
}
|
|
|
|
const normalizeDeviceIdsList = (val: any): { deviceId: number; attributesIds: number[] }[] => {
|
|
if (!val) return []
|
|
if (Array.isArray(val)) {
|
|
return val
|
|
.map((v: any) => ({
|
|
deviceId: Number(v?.deviceId),
|
|
attributesIds: Array.isArray(v?.attributesIds)
|
|
? v.attributesIds.map((id: any) => Number(id)).filter((id: number) => !Number.isNaN(id))
|
|
: []
|
|
}))
|
|
.filter((v) => v.deviceId && !Number.isNaN(v.deviceId))
|
|
}
|
|
if (typeof val === 'string') {
|
|
const trimmed = val.trim()
|
|
if (!trimmed) return []
|
|
if (trimmed.startsWith('[')) {
|
|
try {
|
|
const parsed = JSON.parse(trimmed)
|
|
if (Array.isArray(parsed)) {
|
|
return normalizeDeviceIdsList(parsed)
|
|
}
|
|
} catch { }
|
|
}
|
|
const ids = trimmed
|
|
.split(',')
|
|
.map((s) => Number(s.trim()))
|
|
.filter((id) => !Number.isNaN(id))
|
|
return ids.map((id) => ({ deviceId: id, attributesIds: [] }))
|
|
}
|
|
return []
|
|
}
|
|
|
|
const openEditDialog = (item: DashboardItem) => {
|
|
dialogMode.value = 'edit'
|
|
editingId.value = item.id
|
|
createForm.name = item.name || ''
|
|
createForm.remark = item.remark || ''
|
|
createForm.state = item.state
|
|
createForm.type = item.type || ''
|
|
createForm.orgId = item.orgId || undefined
|
|
createForm.orgName = item.orgName || ''
|
|
const fromNew = Array.isArray(item.deviceIdsList) && item.deviceIdsList.length
|
|
? normalizeDeviceIdsList(item.deviceIdsList)
|
|
: []
|
|
const fromLegacy = !fromNew.length ? normalizeDeviceIdsList(item.deviceIds) : []
|
|
createForm.deviceIdsList = fromNew.length ? fromNew : fromLegacy
|
|
createForm.indexImage = item.indexImage || ''
|
|
createForm.route = item.route || ''
|
|
createForm.content = item.content || ''
|
|
deviceAttrSelections.value =
|
|
createForm.deviceIdsList && createForm.deviceIdsList.length
|
|
? createForm.deviceIdsList.slice(0, 8).map((g) => ({
|
|
deviceId: g.deviceId,
|
|
attributeIds: Array.isArray(g.attributesIds) ? g.attributesIds.slice() : []
|
|
}))
|
|
: [{ deviceId: undefined, attributeIds: [] }]
|
|
deviceAttrSelections.value.forEach((g) => {
|
|
if (g.deviceId) {
|
|
loadDeviceAttributes(g.deviceId)
|
|
}
|
|
})
|
|
createDialogVisible.value = true
|
|
}
|
|
|
|
const getRouteByType = (type?: string) => {
|
|
if (type === '1') return 'iot/report/dashboardPage/Dashboard1'
|
|
if (type === '2') return 'iot/report/dashboardPage/Dashboard8'
|
|
return ''
|
|
}
|
|
|
|
const submitDialog = async () => {
|
|
try {
|
|
await createFormRef.value?.validate()
|
|
} catch {
|
|
return
|
|
}
|
|
if (createForm.type === '1') {
|
|
const groups = deviceAttrSelections.value
|
|
.map((g) => ({
|
|
deviceId: g.deviceId,
|
|
attributesIds: (g.attributeIds || []).filter((v) => v !== undefined && v !== null)
|
|
}))
|
|
.filter((g) => g.deviceId && g.attributesIds.length)
|
|
if (!groups.length) {
|
|
message.error('请至少配置一组设备和点位')
|
|
return
|
|
}
|
|
createForm.deviceIdsList = groups.map((g) => ({
|
|
deviceId: Number(g.deviceId),
|
|
attributesIds: g.attributesIds.map((id: any) => Number(id))
|
|
}))
|
|
} else {
|
|
createForm.deviceIdsList = []
|
|
}
|
|
if (createForm.orgId) {
|
|
const org = findOrgNode(organizationTree.value || [], createForm.orgId)
|
|
createForm.orgName = org?.name || ''
|
|
} else {
|
|
createForm.orgName = ''
|
|
}
|
|
const route = getRouteByType(createForm.type)
|
|
if (route) {
|
|
createForm.route = route
|
|
}
|
|
if (dialogMode.value === 'edit' && !editingId.value) {
|
|
message.error('缺少数据编号,无法编辑')
|
|
return
|
|
}
|
|
createLoading.value = true
|
|
try {
|
|
if (dialogMode.value === 'create') {
|
|
await request.post({
|
|
url: '/mes/goview/create',
|
|
data: createForm
|
|
})
|
|
message.success('新增成功')
|
|
} else {
|
|
await request.put({
|
|
url: '/mes/goview/update',
|
|
data: {
|
|
id: editingId.value,
|
|
...createForm
|
|
}
|
|
})
|
|
message.success('编辑成功')
|
|
}
|
|
createDialogVisible.value = false
|
|
handleQuery()
|
|
} finally {
|
|
createLoading.value = false
|
|
}
|
|
}
|
|
|
|
const handleDelete = async (item: DashboardItem) => {
|
|
if (!item.id) return
|
|
try {
|
|
await message.delConfirm()
|
|
await request.delete({
|
|
url: `/mes/goview/delete?id=${item.id}`
|
|
})
|
|
message.success('删除成功')
|
|
await getList()
|
|
} catch { }
|
|
}
|
|
|
|
const loadOrganizationTree = async () => {
|
|
const data = await OrganizationApi.getOrganizationList()
|
|
organizationTree.value = handleTree(data, 'id', 'parentId')
|
|
}
|
|
|
|
const findOrgNode = (nodes: any[], id: any): any | undefined => {
|
|
for (const node of nodes) {
|
|
if (String(node.id) === String(id)) return node
|
|
const children = Array.isArray(node.children) ? node.children : []
|
|
const found = findOrgNode(children, id)
|
|
if (found) return found
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
const handleOrgChange = () => {
|
|
if (!createForm.orgId) {
|
|
createForm.orgName = ''
|
|
return
|
|
}
|
|
const org = findOrgNode(organizationTree.value || [], createForm.orgId)
|
|
createForm.orgName = org?.name || ''
|
|
}
|
|
|
|
const loadDeviceList = async () => {
|
|
deviceList.value = await DeviceApi.getDeviceList()
|
|
}
|
|
|
|
const loadDeviceAttributes = async (deviceId: number | string) => {
|
|
const key = String(deviceId)
|
|
if (!key) return
|
|
if (deviceAttributeOptionsMap.value[key]) return
|
|
const data = await request.get({
|
|
url: '/iot/device/device-attribute/page',
|
|
params: {
|
|
pageNo: 1,
|
|
pageSize: 100,
|
|
deviceId
|
|
}
|
|
})
|
|
deviceAttributeOptionsMap.value[key] = data?.list || []
|
|
}
|
|
|
|
const handleDeviceChange = async (value: number | string, index: number) => {
|
|
const group = deviceAttrSelections.value[index]
|
|
if (!group) return
|
|
group.attributeIds = []
|
|
if (!value) return
|
|
await loadDeviceAttributes(value)
|
|
}
|
|
|
|
const addDeviceAttrGroup = () => {
|
|
if (deviceAttrSelections.value.length >= 8) return
|
|
deviceAttrSelections.value.push({ deviceId: undefined, attributeIds: [] })
|
|
}
|
|
|
|
const removeDeviceAttrGroup = (index: number) => {
|
|
if (deviceAttrSelections.value.length <= 1) return
|
|
deviceAttrSelections.value.splice(index, 1)
|
|
}
|
|
|
|
onMounted(() => {
|
|
getList()
|
|
loadOrganizationTree()
|
|
loadDeviceList()
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.dashboard-card-list {
|
|
min-height: 200px;
|
|
}
|
|
|
|
.dashboard-card {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.dashboard-card-image-wrapper {
|
|
position: relative;
|
|
width: 100%;
|
|
padding-top: 56.25%;
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.dashboard-card-image {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
background-color: #0f172a;
|
|
}
|
|
|
|
.dashboard-card-state {
|
|
position: absolute;
|
|
right: 8px;
|
|
bottom: 8px;
|
|
}
|
|
|
|
.dashboard-card-body {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px 16px;
|
|
}
|
|
|
|
.dashboard-card-main {
|
|
min-width: 0;
|
|
}
|
|
|
|
.dashboard-card-title {
|
|
margin-bottom: 4px;
|
|
overflow: hidden;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--el-text-color-primary);
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.dashboard-card-remark {
|
|
overflow: hidden;
|
|
font-size: 12px;
|
|
color: var(--el-text-color-secondary);
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.dashboard-card-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-left: 8px;
|
|
}
|
|
|
|
.dashboard-card-menu {
|
|
min-width: 120px;
|
|
}
|
|
|
|
:deep(.el-dropdown-menu__item.dashboard-card-menu-primary) {
|
|
color: var(--el-color-primary);
|
|
}
|
|
|
|
:deep(.el-dropdown-menu__item.dashboard-card-menu-primary:hover),
|
|
:deep(.el-dropdown-menu__item.dashboard-card-menu-primary:focus) {
|
|
color: var(--el-color-primary);
|
|
background-color: var(--el-color-primary-light-9);
|
|
}
|
|
|
|
:deep(.el-dropdown-menu__item.dashboard-card-menu-danger) {
|
|
color: var(--el-color-danger);
|
|
}
|
|
|
|
:deep(.el-dropdown-menu__item.dashboard-card-menu-danger:hover),
|
|
:deep(.el-dropdown-menu__item.dashboard-card-menu-danger:focus) {
|
|
color: var(--el-color-danger);
|
|
background-color: var(--el-color-danger-light-9);
|
|
}
|
|
|
|
.dashboard-device-group-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.dashboard-device-group {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.dashboard-device-remove-btn {
|
|
flex-shrink: 0;
|
|
}
|
|
</style>
|