This commit is contained in:
qsc
2026-07-12 19:18:26 +08:00
parent dd588b5daa
commit 34ec65c8b5
52 changed files with 10859 additions and 0 deletions
+330
View File
@@ -0,0 +1,330 @@
<script setup lang="ts">
import { onMounted, reactive, ref } from "vue";
import {
Download,
Plus,
Search,
Upload,
FolderPlus,
Pencil,
Trash2,
} from "lucide-vue-next";
import {
collectionApi,
deviceApi,
exportConfig,
importConfig,
} from "@/api/platform";
const items = ref<any[]>([]),
devices = ref<any[]>([]),
keyword = ref(""),
show = ref(false),
editing = ref<string | null>(null);
const groups = ref<any[]>([]),
selectedGroup = ref(""),
groupEditor = ref<string | null>(null),
groupName = ref("");
const form = reactive({
name: "",
group_name: "default",
device_id: "",
enabled: true,
address: "",
data_type: "REAL",
unit: "",
collect_interval: 1,
store_history: true,
history_interval: 1,
});
async function load() {
try {
const r: any = await collectionApi.list({
keyword: keyword.value,
page_size: 100,
group_name: selectedGroup.value,
});
items.value = r.data.items;
} catch {
items.value = [];
}
}
async function loadGroups() {
const r: any = await collectionApi.groups();
groups.value = r.data.groups;
}
async function addGroup() {
const name = prompt("请输入新分组名称")?.trim();
if (!name) return;
await collectionApi.createGroup(name);
selectedGroup.value = name;
await loadGroups();
await load();
}
async function editGroup(g: any) {
const name = prompt("修改分组名称", g.name)?.trim();
if (!name || name === g.name) return;
await collectionApi.updateGroup(g.name, name);
if (selectedGroup.value === g.name) selectedGroup.value = name;
await loadGroups();
await load();
}
async function deleteGroup(g: any) {
if (g.name === "default") {
alert("default 分组不可删除");
return;
}
if (!confirm(`删除分组“${g.name}”后,里面的采集点也会跟着删除,是否继续?`))
return;
await collectionApi.removeGroup(g.name);
if (selectedGroup.value === g.name) selectedGroup.value = "";
await loadGroups();
await load();
}
function open(p?: any) {
editing.value = p?.id ?? null;
Object.assign(
form,
p ?? {
name: "",
group_name: "default",
device_id: devices.value[0]?.id ?? "",
enabled: true,
address: "",
data_type: "REAL",
unit: "",
collect_interval: 1,
store_history: true,
history_interval: 1,
},
);
if (!form.group_name && groups.value.length)
form.group_name = groups.value[0].name;
show.value = true;
}
async function save() {
editing.value
? await collectionApi.update(editing.value, form)
: await collectionApi.create(form);
show.value = false;
await load();
}
async function remove(id: string) {
if (confirm("确认删除该采集点?")) {
await collectionApi.remove(id);
await load();
}
}
async function upload(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return;
const r: any = await importConfig("collection-points", file);
alert(
`导入完成:新增${r.data.created},更新${r.data.updated},失败${r.data.failed}`,
);
await load();
}
onMounted(async () => {
const r: any = await deviceApi.list({ page_size: 100 });
devices.value = r.data.items;
await loadGroups();
await load();
});
</script>
<template>
<section>
<div class="collection-layout">
<aside class="group-sidebar panel">
<div class="group-header">
<span>采集点分组</span
><button class="icon-btn" title="添加分组" @click="addGroup">
<FolderPlus />
</button>
</div>
<button
:class="['group-item', !selectedGroup && 'active']"
@click="
selectedGroup = '';
load();
"
>
全部分组
<span>{{ groups.reduce((sum, g) => sum + g.count, 0) }}</span>
</button>
<div
v-for="g in groups"
:key="g.name"
:class="['group-item-wrap', selectedGroup === g.name && 'active']"
>
<button
:class="['group-item', selectedGroup === g.name && 'active']"
@click="
selectedGroup = g.name;
load();
"
>
<span>{{ g.name }}</span
><span>{{ g.count }}</span>
</button>
<span class="group-actions"
><button class="icon-btn" title="修改分组" @click="editGroup(g)">
<Pencil /></button
><button
class="icon-btn danger"
title="删除分组"
@click="deleteGroup(g)"
>
<Trash2 /></button
></span>
</div>
</aside>
<div class="collection-main">
<div class="page-head">
<div>
<h2>数据采集</h2>
<p>点位地址采集周期历史策略与实时质量</p>
</div>
<div class="toolbar">
<label class="btn"
><Upload />导入CSV<input
hidden
type="file"
accept=".csv"
@change="upload"
/></label>
<button class="btn" @click="exportConfig('collection-points')">
<Download />导出CSV
</button>
<button class="btn btn-primary" @click="open()">
<Plus />新增采集点
</button>
</div>
</div>
<div class="toolbar" style="margin-bottom: 12px">
<input
v-model="keyword"
class="input"
placeholder="搜索名称或分组"
/><button class="btn" @click="load"><Search />查询</button>
</div>
<div class="panel">
<table class="data-table">
<thead>
<tr>
<th>名称</th>
<th>分组</th>
<th>所属设备</th>
<th>地址</th>
<th>类型</th>
<th>采集周期</th>
<th>历史</th>
<th>最新值</th>
<th>质量</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="p in items" :key="p.id">
<td>{{ p.name }}</td>
<td>{{ p.group_name }}</td>
<td>{{ p.device_name }}</td>
<td>{{ p.address }}</td>
<td>
<span class="tag">{{ p.data_type }}</span>
</td>
<td>{{ p.collect_interval }} </td>
<td>
{{ p.store_history ? p.history_interval + " 分钟" : "关闭" }}
</td>
<td>{{ p.latest_value?.value ?? "—" }} {{ p.unit }}</td>
<td
:class="
p.latest_value?.quality === 'bad'
? 'quality-bad'
: 'quality-good'
"
>
{{ p.latest_value?.quality ?? "none" }}
</td>
<td>
<button class="btn" @click="open(p)">编辑</button>
<button class="btn" @click="remove(p.id)">删除</button>
</td>
</tr>
</tbody>
</table>
<div v-if="!items.length" class="empty">暂无采集点</div>
</div>
</div>
</div>
<div v-if="show" class="modal-mask">
<form class="modal" @submit.prevent="save">
<h3>{{ editing ? "编辑采集点" : "新增采集点" }}</h3>
<div class="form-grid">
<label class="field"
>名称<input
v-model="form.name"
class="input"
required
maxlength="128" /></label
><label class="field"
>分组<select v-model="form.group_name" class="select" required>
<option v-for="g in groups" :key="g.name" :value="g.name">
{{ g.name }}
</option>
</select></label
>
<label class="field"
>设备<select v-model="form.device_id" class="select" required>
<option v-for="d in devices" :key="d.id" :value="d.id">
{{ d.name }}
</option>
</select></label
><label class="field"
>数据类型<select v-model="form.data_type" class="select">
<option>BOOL</option>
<option>INT</option>
<option>REAL</option>
</select></label
>
<label class="field"
>地址<input
v-model="form.address"
class="input"
required
placeholder="DB2.1186.0" /></label
><label class="field"
>单位<input v-model="form.unit" class="input" maxlength="32"
/></label>
<label class="field"
>采集周期<input
v-model.number="form.collect_interval"
class="input"
type="number"
min="1" /></label
><label class="field"
>历史间隔分钟<input
v-model.number="form.history_interval"
class="input"
type="number"
min="1"
max="1440"
/></label>
<label class="field"
><span
><input v-model="form.enabled" type="checkbox" /> 启用</span
></label
><label class="field"
><span
><input v-model="form.store_history" type="checkbox" />
存储历史</span
></label
>
</div>
<div class="modal-actions">
<button type="button" class="btn" @click="show = false">取消</button
><button class="btn btn-primary">保存</button>
</div>
</form>
</div>
</section>
</template>