feat: 迁入 MiGu.Server、平台前端与车辆列表 reflection 回退
从 Simple-FR 拆出 Platform.Server 并重命名为 MiGu.Server;frontends 源码与构建脚本迁入本仓库。地图监控在 projection/cars 失败或为空时回退 reflection 车辆列表;Simple 仓库已移除旧 Platform.Server。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<section class="mmcg">
|
||||
<header class="mmcg-head">
|
||||
<span class="mmcg-title">{{ title }}</span>
|
||||
<el-tag size="small" effect="plain" type="info">{{ summaryText }}</el-tag>
|
||||
<div class="spacer" />
|
||||
<el-button-group v-if="available.length">
|
||||
<el-button size="small" :disabled="!canSelectAll" @click="selectAll">全选</el-button>
|
||||
<el-button size="small" :disabled="!selected.length" @click="clearAll">清空</el-button>
|
||||
</el-button-group>
|
||||
</header>
|
||||
|
||||
<p v-if="!available.length" class="mmcg-empty muted">{{ emptyTip }}</p>
|
||||
|
||||
<el-checkbox-group
|
||||
v-else
|
||||
:model-value="selected"
|
||||
class="mmcg-grid"
|
||||
@update:model-value="onChange">
|
||||
<el-checkbox v-for="key in available" :key="key" :value="key" :label="key" class="mmcg-cb" />
|
||||
</el-checkbox-group>
|
||||
|
||||
<!-- 处理"残留键":已勾选但不再出现在 available 里(后端实现变了 / 插件卸载了)-->
|
||||
<div v-if="orphanKeys.length" class="mmcg-orphans">
|
||||
<el-alert
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
:title="`检测到 ${orphanKeys.length} 个残留键(后端已经不再暴露,但仍在白名单中)`">
|
||||
<template #default>
|
||||
<div class="orphan-list">
|
||||
<el-tag
|
||||
v-for="key in orphanKeys"
|
||||
:key="key"
|
||||
type="warning"
|
||||
size="small"
|
||||
closable
|
||||
@close="removeOrphan(key)">
|
||||
{{ key }}
|
||||
</el-tag>
|
||||
<el-button link type="warning" size="small" @click="clearOrphans">一键清理全部残留</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import type { CheckboxValueType } from 'element-plus'
|
||||
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
available: string[]
|
||||
selected: string[]
|
||||
emptyTip?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:selected', value: string[]): void
|
||||
}>()
|
||||
|
||||
const summaryText = computed(() => {
|
||||
if (!props.available.length) return '空'
|
||||
if (!props.selected.length) return `白名单空 = 显示全部 (${props.available.length})`
|
||||
return `${props.selected.length} / ${props.available.length}`
|
||||
})
|
||||
|
||||
// available 是后端最新扫描结果。selected 里可能有 "残留"——之前勾选过但后端现在不再返回(插件
|
||||
// 卸载 / 类型重命名)。这些键不会出现在 checkbox-group 里(避免渲染陌生项),但要单独列出来
|
||||
// 让用户决定是否清理;不主动删,避免误清"暂时离线的插件键"。
|
||||
const orphanKeys = computed(() =>
|
||||
props.selected.filter((k) => !props.available.includes(k))
|
||||
)
|
||||
|
||||
const canSelectAll = computed(() => props.available.length > props.selected.length)
|
||||
|
||||
function onChange(v: CheckboxValueType[]) {
|
||||
// element-plus 在泛型推断下会拿到 CheckboxValueType(string | number | boolean)。我们这里键
|
||||
// 全是 string,强制断言;同时把可能残留的 orphan 键并回去,避免「在某 kind 全选」操作把
|
||||
// orphan 默默丢掉(用户应该走"清理残留"显式确认)。
|
||||
const next = (v as string[]).filter((x) => typeof x === 'string')
|
||||
const merged = [...next, ...orphanKeys.value]
|
||||
emit('update:selected', merged)
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
emit('update:selected', [...props.available, ...orphanKeys.value])
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
// 仅清空 available 内的;orphan 保留(防误删,要走"残留清理"流程)
|
||||
emit('update:selected', [...orphanKeys.value])
|
||||
}
|
||||
|
||||
function removeOrphan(key: string) {
|
||||
emit('update:selected', props.selected.filter((k) => k !== key))
|
||||
}
|
||||
|
||||
function clearOrphans() {
|
||||
emit('update:selected', props.selected.filter((k) => props.available.includes(k)))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mmcg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 8px;
|
||||
}
|
||||
.mmcg-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.mmcg-title {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
}
|
||||
.mmcg-head .spacer { flex: 1; }
|
||||
.mmcg-empty {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
font-style: italic;
|
||||
}
|
||||
.muted { color: rgba(255, 255, 255, 0.62); }
|
||||
|
||||
.mmcg-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 4px 12px;
|
||||
align-items: center;
|
||||
}
|
||||
.mmcg-grid :deep(.el-checkbox) {
|
||||
margin-right: 0;
|
||||
height: 28px;
|
||||
}
|
||||
.mmcg-grid :deep(.el-checkbox__label) {
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
font-size: 12.5px;
|
||||
font-family: ui-monospace, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.mmcg-orphans { margin-top: 4px; }
|
||||
.orphan-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user