feat: 补全无依赖PF01-PF15功能闭环
This commit is contained in:
@@ -181,3 +181,51 @@ frontend/src/components/gis/OperationalGisMap.vue
|
||||
## 8. 后续实施入口
|
||||
|
||||
下一批可进入 B 类功能:PF-01、PF-02、PF-05、PF-08、PF-09、PF-10、PF-11、PF-15。开发可以使用明确标记的默认配置和 DEMO 数据,但生产发布仍需相应主数据、规则或设备参数。
|
||||
|
||||
## 9. B 类无依赖补全续实施记录
|
||||
|
||||
> 更新日期:2026-08-01
|
||||
> 本次范围:PF-01、PF-02、PF-05、PF-08、PF-09、PF-10、PF-11、PF-15 的软件闭环。生产关闭仍按设计文档要求等待权威主数据、设备矩阵、空间规则和现场参数。
|
||||
|
||||
### 9.1 新增数据库迁移
|
||||
|
||||
`V21__dependency_free_pf01_pf15.sql` 新增和扩展:
|
||||
|
||||
- PF-01:对象导入映射、导入任务、导入行、对象不可变版本。
|
||||
- PF-02:业务日历、计划执行记录、应急授权,以及计划调度时区、补跑策略、执行时间字段。
|
||||
- PF-05:作业资源、资质、不可用窗口、资源预占、冲突检查记录。
|
||||
- PF-08:GIS 数据集版本、图层、空间要素。
|
||||
- PF-09:五类候选航线生成器配置和生成运行记录。
|
||||
- PF-10:航线编辑修订、通用动作 Schema、航线编辑锁字段。
|
||||
- PF-11:航线估算配置、估算运行记录和航线版本估算结果。
|
||||
- PF-15:空间规则集/版本、空间基线要素、豁免区、地理围栏、规则命中解释。
|
||||
|
||||
迁移同时写入 DEMO/REFERENCE 种子配置,并将 PF-01、02、05、08、09、10、11、15 登记到能力追踪表,阻塞原因明确标记为生产资料待到位。
|
||||
|
||||
### 9.2 后端接口
|
||||
|
||||
新增 `DependencyFreeCompletionService`、`PlanSchedulerJob` 和对应 `/api/v1` 端点,覆盖:
|
||||
|
||||
- 对象导入:`/inspection/object-imports` 创建、预检、确认、回滚、行结果与对象版本查询。
|
||||
- 计划调度:`/business-calendars`、`/inspection/plans/{id}/preview-runs`、`/inspection/plans/{id}/executions`、`/inspection/plans/run-due`,后台作业每 30 秒扫描到期周期计划。
|
||||
- 资源冲突:`/operation-resources/sync`、`/operation-resources/calendar`、`/inspection/tasks/{id}/conflict-checks`、资源预占和取消。
|
||||
- GIS 图层:`/gis/layers`、`/gis/dataset-versions`、图层发布和图层要素查询。
|
||||
- 航线生成/编辑/估算:`/route-generation-profiles`、`/routes/generate`、`/route-action-schemas`、`/route-versions/{id}/waypoints`、`/route-versions/{id}/estimate`。
|
||||
- 空间规则:`/spatial-rule-sets`、`/spatial-exemptions`、`/geofences`、`/spatial-rule-hits/explain`。
|
||||
|
||||
### 9.3 前端落点
|
||||
|
||||
- 巡检计划页新增对象导入中心、导入确认/回滚、对象版本抽屉、计划执行预览、执行记录和到期计划触发。
|
||||
- 航线中心新增自动生成候选航线、生成器参数、航线估算、编辑修订保存、动作 Schema 和编辑审计查看。
|
||||
- GIS 态势页新增图层、规则版本、豁免区、地理围栏面板,并支持对选中点位执行空间命中解释。
|
||||
- 无人机运行页新增资源日历,支持同步作业资源、任务冲突检查和资源预占。
|
||||
|
||||
### 9.4 验证记录
|
||||
|
||||
| 验证项 | 结果 |
|
||||
|---|---|
|
||||
| Vue TypeScript 检查与 Vite 构建 | 通过 |
|
||||
| Java 局部编译 | 通过,使用仓库 `.m2-repository` 依赖缓存和 `javac -proc:none` |
|
||||
| Maven 全量测试 | 当前环境未提供 `mvn` 或 Maven Wrapper,未执行 |
|
||||
|
||||
Vite 仍保留既有大分块提示和第三方 PURE 注释提示,不影响功能正确性。
|
||||
|
||||
@@ -949,3 +949,168 @@ export async function createGisExport(payload: Record<string, unknown>) {
|
||||
export function gisExportContentUrl(exportId: string) {
|
||||
return `/api/v1/gis/exports/${exportId}/content`;
|
||||
}
|
||||
|
||||
export async function objectMappingProfiles() {
|
||||
const { data } = await client.get("/inspection/object-mapping-profiles");
|
||||
return data.data.profiles;
|
||||
}
|
||||
|
||||
export async function objectImports() {
|
||||
const { data } = await client.get("/inspection/object-imports");
|
||||
return data.data.imports;
|
||||
}
|
||||
|
||||
export async function createObjectImport(payload: Record<string, unknown>) {
|
||||
const { data } = await client.post("/inspection/object-imports", payload);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function commitObjectImport(importJobId: string) {
|
||||
const { data } = await client.post(`/inspection/object-imports/${importJobId}/commit`, {}, { params: { confirmedBy: "user-dispatcher" } });
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function rollbackObjectImport(importJobId: string) {
|
||||
const { data } = await client.post(`/inspection/object-imports/${importJobId}/rollback`, {}, { params: { requestedBy: "user-dispatcher" } });
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function objectVersions(objectId: string) {
|
||||
const { data } = await client.get(`/inspection/objects/${objectId}/versions`);
|
||||
return data.data.versions;
|
||||
}
|
||||
|
||||
export async function businessCalendars() {
|
||||
const { data } = await client.get("/business-calendars");
|
||||
return data.data.calendars;
|
||||
}
|
||||
|
||||
export async function previewPlanRuns(planId: string, count = 6) {
|
||||
const { data } = await client.post(`/inspection/plans/${planId}/preview-runs`, { count });
|
||||
return data.data.runs;
|
||||
}
|
||||
|
||||
export async function planExecutions(planId: string) {
|
||||
const { data } = await client.get(`/inspection/plans/${planId}/executions`);
|
||||
return data.data.executions;
|
||||
}
|
||||
|
||||
export async function runDueInspectionPlans(limit = 20) {
|
||||
const { data } = await client.post("/inspection/plans/run-due", { limit });
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function syncOperationResources() {
|
||||
const { data } = await client.post("/operation-resources/sync", {});
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function operationResourceCalendar(params: Record<string, unknown> = {}) {
|
||||
const { data } = await client.get("/operation-resources/calendar", { params });
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function checkTaskConflicts(taskId: string, payload: Record<string, unknown> = {}) {
|
||||
const { data } = await client.post(`/inspection/tasks/${taskId}/conflict-checks`, payload);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function reserveTaskResources(taskId: string, payload: Record<string, unknown>) {
|
||||
const { data } = await client.post(`/inspection/tasks/${taskId}/reservations`, payload);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function cancelTaskReservation(taskId: string, reservationId: string) {
|
||||
const { data } = await client.delete(`/inspection/tasks/${taskId}/reservations/${reservationId}`);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function gisLayers() {
|
||||
const { data } = await client.get("/gis/layers");
|
||||
return data.data.layers;
|
||||
}
|
||||
|
||||
export async function gisDatasetVersions() {
|
||||
const { data } = await client.get("/gis/dataset-versions");
|
||||
return data.data.dataset_versions;
|
||||
}
|
||||
|
||||
export async function gisLayerFeatures(layerCode: string) {
|
||||
const { data } = await client.get(`/gis/layers/${layerCode}/features`);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function routeGenerationProfiles() {
|
||||
const { data } = await client.get("/route-generation-profiles");
|
||||
return data.data.profiles;
|
||||
}
|
||||
|
||||
export async function generateCandidateRoute(payload: Record<string, unknown>) {
|
||||
const { data } = await client.post("/routes/generate", payload);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function routeActionSchemas() {
|
||||
const { data } = await client.get("/route-action-schemas");
|
||||
return data.data.schemas;
|
||||
}
|
||||
|
||||
export async function updateRouteWaypoints(versionId: string, payload: Record<string, unknown>) {
|
||||
const { data } = await client.put(`/route-versions/${versionId}/waypoints`, payload);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function routeEditorRevisions(versionId: string) {
|
||||
const { data } = await client.get(`/route-versions/${versionId}/editor-revisions`);
|
||||
return data.data.revisions;
|
||||
}
|
||||
|
||||
export async function routeEstimationProfiles() {
|
||||
const { data } = await client.get("/route-estimation-profiles");
|
||||
return data.data.profiles;
|
||||
}
|
||||
|
||||
export async function estimateRouteVersion(versionId: string, payload: Record<string, unknown> = {}) {
|
||||
const { data } = await client.post(`/route-versions/${versionId}/estimate`, payload);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function routeEstimations(versionId: string) {
|
||||
const { data } = await client.get(`/route-versions/${versionId}/estimations`);
|
||||
return data.data.estimations;
|
||||
}
|
||||
|
||||
export async function spatialRuleSets() {
|
||||
const { data } = await client.get("/spatial-rule-sets");
|
||||
return data.data.rule_sets;
|
||||
}
|
||||
|
||||
export async function spatialExemptions() {
|
||||
const { data } = await client.get("/spatial-exemptions");
|
||||
return data.data.exemptions;
|
||||
}
|
||||
|
||||
export async function createSpatialExemption(payload: Record<string, unknown>) {
|
||||
const { data } = await client.post("/spatial-exemptions", payload);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function approveSpatialExemption(exemptionId: string) {
|
||||
const { data } = await client.post(`/spatial-exemptions/${exemptionId}/approve`, {}, { params: { approvedBy: "user-approver" } });
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function geofences() {
|
||||
const { data } = await client.get("/geofences");
|
||||
return data.data.geofences;
|
||||
}
|
||||
|
||||
export async function createGeofence(payload: Record<string, unknown>) {
|
||||
const { data } = await client.post("/geofences", payload);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
export async function explainSpatialRuleHit(payload: Record<string, unknown>) {
|
||||
const { data } = await client.post("/spatial-rule-hits/explain", payload);
|
||||
return data.data;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<el-button :icon="Refresh" :loading="loading" @click="load">刷新图层</el-button>
|
||||
<el-button :disabled="!selectedTaskId" @click="loadPlayback">任务回放</el-button>
|
||||
<el-button :disabled="!selectedTaskId" @click="loadHeatmap">生成热力</el-button>
|
||||
<el-button :disabled="!selected" @click="explainSelected">空间解释</el-button>
|
||||
<el-button type="primary" :disabled="!selectedTaskId" @click="exportThematicMap">导出专题图</el-button>
|
||||
</PageHeader>
|
||||
|
||||
@@ -55,6 +56,17 @@
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-card>
|
||||
|
||||
<el-card class="workspace-card spatial-card" shadow="never">
|
||||
<div class="subsection-head"><div><strong>图层与空间规则</strong><span>PF-08 / PF-15 的图层、规则、豁免和围栏版本</span></div><el-tag type="warning" effect="plain">生产数据待批准</el-tag></div>
|
||||
<el-tabs v-model="spatialTab" class="secondary-tabs">
|
||||
<el-tab-pane label="GIS 图层" name="layers"><el-table :data="gisLayerRows" size="small" empty-text="暂无图层"><el-table-column prop="name" label="图层" min-width="160" /><el-table-column prop="layer_type" label="类型" width="100" /><el-table-column prop="source_type" label="来源" width="100" /><el-table-column prop="quality_status" label="质量" width="100" /><el-table-column prop="status" label="状态" width="100" /></el-table></el-tab-pane>
|
||||
<el-tab-pane label="规则版本" name="rules"><el-table :data="ruleSetRows" size="small" empty-text="暂无规则"><el-table-column prop="name" label="规则集" min-width="180" /><el-table-column prop="rule_type" label="类型" width="150" /><el-table-column label="版本" width="80"><template #default="scope">V{{ scope.row.version_no || '-' }}</template></el-table-column><el-table-column prop="version_status" label="状态" width="110" /><el-table-column label="规则数" width="95"><template #default="scope">{{ parseArray(scope.row.rules).length }}</template></el-table-column></el-table></el-tab-pane>
|
||||
<el-tab-pane label="豁免区" name="exemptions"><el-table :data="exemptionRows" size="small" empty-text="暂无豁免"><el-table-column prop="reason" label="原因" min-width="180" /><el-table-column prop="exemption_type" label="类型" width="150" /><el-table-column prop="approval_status" label="审批" width="100" /><el-table-column label="有效期" min-width="220"><template #default="scope">{{ formatDate(scope.row.effective_from) }} - {{ formatDate(scope.row.effective_to) }}</template></el-table-column></el-table></el-tab-pane>
|
||||
<el-tab-pane label="地理围栏" name="geofences"><el-table :data="geofenceRows" size="small" empty-text="暂无围栏"><el-table-column prop="name" label="围栏" min-width="180" /><el-table-column prop="fence_type" label="类型" width="120" /><el-table-column prop="severity" label="等级" width="100" /><el-table-column label="高度" width="120"><template #default="scope">{{ scope.row.altitude_min_m ?? '-' }} - {{ scope.row.altitude_max_m ?? '-' }}m</template></el-table-column><el-table-column prop="status" label="状态" width="100" /></el-table></el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-alert v-if="spatialExplanation" class="spatial-explanation" type="info" :closable="false" show-icon :title="`空间命中解释:${spatialExplanation.decision}`" :description="explanationText" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -65,15 +77,17 @@ import { ElMessage } from "element-plus";
|
||||
import { Close, Refresh } from "@element-plus/icons-vue";
|
||||
import PageHeader from "../../components/common/PageHeader.vue";
|
||||
import OperationalGisMap from "../../components/gis/OperationalGisMap.vue";
|
||||
import { alarms, createGisExport, gisExportContentUrl, gisFeatures, gisHeatmap, gisPlaybackEvents, gisPlaybackManifest, gisPlaybackTrack, tasks, workorders } from "../../services/api";
|
||||
import { alarms, createGisExport, explainSpatialRuleHit, geofences, gisExportContentUrl, gisFeatures, gisHeatmap, gisLayers, gisPlaybackEvents, gisPlaybackManifest, gisPlaybackTrack, spatialExemptions, spatialRuleSets, tasks, workorders } from "../../services/api";
|
||||
import { safeObject, severityLabel, statusLabel, type Row } from "../../types/demo-run";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const loading = ref(false);
|
||||
const alarmRows = ref<Row[]>([]); const taskRows = ref<Row[]>([]); const workorderRows = ref<Row[]>([]);
|
||||
const gisLayerRows = ref<Row[]>([]); const ruleSetRows = ref<Row[]>([]); const exemptionRows = ref<Row[]>([]); const geofenceRows = ref<Row[]>([]);
|
||||
const featureCollection = ref<Row>({ type: "FeatureCollection", features: [] });
|
||||
const selected = ref<Row | null>(null); const lineFilter = ref(""); const severityFilter = ref("");
|
||||
const spatialTab = ref("layers"); const spatialExplanation = ref<Row | null>(null);
|
||||
const selectedTaskId = ref(String(route.query.taskId || ""));
|
||||
const exportFormat = ref("PDF");
|
||||
const playbackManifest = ref<Row | null>(null); const playbackTrack = ref<Row | null>(null); const playbackEvents = ref<Row[]>([]); const heatmapFeatures = ref<Row[]>([]); const playbackIndex = ref(0);
|
||||
@@ -119,9 +133,16 @@ const playbackDeviceFeature = computed<Row | null>(() => {
|
||||
};
|
||||
});
|
||||
const visiblePlaybackEvents = computed(() => playbackEvents.value.slice(Math.max(0, playbackIndex.value - 4), playbackIndex.value + 1));
|
||||
const explanationText = computed(() => {
|
||||
if (!spatialExplanation.value) return "";
|
||||
const geofenceCount = Array.isArray(spatialExplanation.value.geofence_hits) ? spatialExplanation.value.geofence_hits.length : 0;
|
||||
const exemptionCount = Array.isArray(spatialExplanation.value.exemption_hits) ? spatialExplanation.value.exemption_hits.length : 0;
|
||||
return `围栏 ${geofenceCount} 项,豁免 ${exemptionCount} 项,规则版本 ${spatialExplanation.value.rule_version_id || "-"}`;
|
||||
});
|
||||
function taskLine(taskId: unknown) { return String(taskRows.value.find((row) => String(row.task_id) === String(taskId))?.line_id || "line-demo"); }
|
||||
function relatedWorkorder(alarmId: unknown) { return workorderRows.value.find((row) => String(row.alarm_id) === String(alarmId)); }
|
||||
function locationText(row: Row) { const location = safeObject(row.location); return [location.mileage, location.distance_to_track_m ? `距线路 ${location.distance_to_track_m}m` : ""].filter(Boolean).join(" / ") || "线路邻近"; }
|
||||
function parseArray(value: unknown): string[] { try { return Array.isArray(value) ? value.map(String) : JSON.parse(String(value || "[]")); } catch { return []; } }
|
||||
function selectFeature(properties: Row) { selected.value = alarmRows.value.find((row) => String(row.alarm_id) === String(properties.alarm_id)) || properties; }
|
||||
function formatDate(value: unknown) { return value ? new Date(String(value)).toLocaleString("zh-CN", { hour12: false }) : "-"; }
|
||||
async function loadPlayback() {
|
||||
@@ -169,7 +190,16 @@ async function exportThematicMap() {
|
||||
window.open(gisExportContentUrl(String(result.export_job_id)), "_blank", "noopener,noreferrer");
|
||||
ElMessage.success(`${exportFormat.value} 专题成果已生成,摘要 ${String(result.checksum).slice(0, 16)}`);
|
||||
}
|
||||
async function load() { loading.value = true; try { [alarmRows.value, taskRows.value, workorderRows.value, featureCollection.value] = await Promise.all([alarms(), tasks(), workorders(), gisFeatures()]); if (!selectedTaskId.value) selectedTaskId.value = String(taskRows.value[0]?.task_id || ""); selected.value = filteredAlarms.value[0] || null; } finally { loading.value = false; } }
|
||||
async function explainSelected() {
|
||||
if (!selected.value) return;
|
||||
const geometry = safeObject(selected.value.location).geometry || selected.value.geometry;
|
||||
if (!geometry) return ElMessage.warning("当前点位没有可解释空间几何");
|
||||
const result = await explainSpatialRuleHit({ task_id: selected.value.task_id, geometry, scene: selected.value.scene, object_type: selected.value.category });
|
||||
spatialExplanation.value = result;
|
||||
spatialTab.value = "rules";
|
||||
ElMessage.success(`空间解释完成:${result.decision}`);
|
||||
}
|
||||
async function load() { loading.value = true; try { [alarmRows.value, taskRows.value, workorderRows.value, featureCollection.value, gisLayerRows.value, ruleSetRows.value, exemptionRows.value, geofenceRows.value] = await Promise.all([alarms(), tasks(), workorders(), gisFeatures(), gisLayers(), spatialRuleSets(), spatialExemptions(), geofences()]); if (!selectedTaskId.value) selectedTaskId.value = String(taskRows.value[0]?.task_id || ""); selected.value = filteredAlarms.value[0] || null; } finally { loading.value = false; } }
|
||||
onMounted(load);
|
||||
onBeforeUnmount(pausePlayback);
|
||||
</script>
|
||||
@@ -185,4 +215,6 @@ onBeforeUnmount(pausePlayback);
|
||||
.playback-controls { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; color: var(--el-text-color-secondary); }
|
||||
.speed-select { width: 88px; }
|
||||
.export-format { width: 150px; }
|
||||
.spatial-card { margin-top: 18px; }
|
||||
.spatial-explanation { margin-top: 14px; }
|
||||
</style>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div v-loading="loading">
|
||||
<PageHeader title="巡检计划" description="管理周期计划、巡检对象、任务生成和执行准备">
|
||||
<el-button :icon="Refresh" @click="load">刷新</el-button>
|
||||
<el-button @click="runDuePlans">执行到期计划</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="openPlanDialog()">新建计划</el-button>
|
||||
</PageHeader>
|
||||
|
||||
@@ -32,7 +33,7 @@
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="巡检对象台账" name="objects">
|
||||
<div class="filter-bar"><el-input v-model="keyword" clearable placeholder="对象、线路或里程" :prefix-icon="Search" /><el-select v-model="objectType"><el-option label="全部类型" value="" /><el-option v-for="item in objectTypes" :key="item.value" :label="item.label" :value="item.value" /></el-select><span class="filter-spacer"></span><el-button type="primary" plain :icon="Plus" @click="objectDialog = true">新增对象</el-button></div>
|
||||
<div class="filter-bar"><el-input v-model="keyword" clearable placeholder="对象、线路或里程" :prefix-icon="Search" /><el-select v-model="objectType"><el-option label="全部类型" value="" /><el-option v-for="item in objectTypes" :key="item.value" :label="item.label" :value="item.value" /></el-select><span class="filter-spacer"></span><el-button plain @click="activeTab = 'imports'">导入中心</el-button><el-button type="primary" plain :icon="Plus" @click="objectDialog = true">新增对象</el-button></div>
|
||||
<el-card class="workspace-card" shadow="never"><el-table :data="pagedObjects" empty-text="暂无巡检对象">
|
||||
<el-table-column prop="name" label="对象" min-width="180"><template #default="scope"><strong>{{ scope.row.name }}</strong><small class="cell-subtext entity-id">{{ scope.row.object_id }}</small></template></el-table-column>
|
||||
<el-table-column label="类型" width="100"><template #default="scope">{{ objectTypeLabel(scope.row.object_type) }}</template></el-table-column>
|
||||
@@ -42,8 +43,31 @@
|
||||
<el-table-column label="风险" width="90"><template #default="scope"><el-tag :type="['CRITICAL','HIGH'].includes(scope.row.risk_level) ? 'danger' : 'info'" effect="plain">{{ scope.row.risk_level }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="周期" width="90"><template #default="scope">{{ scope.row.inspection_cycle_days ? `${scope.row.inspection_cycle_days} 天` : '-' }}</template></el-table-column>
|
||||
<el-table-column prop="source_crs" label="坐标系" width="105" />
|
||||
<el-table-column label="操作" width="90" fixed="right"><template #default="scope"><el-button link type="primary" @click="showObjectVersions(scope.row)">版本</el-button></template></el-table-column>
|
||||
</el-table><el-pagination v-model:current-page="objectPage" v-model:page-size="objectPageSize" class="table-pagination" background layout="total, sizes, prev, pager, next" :page-sizes="DEFAULT_PAGE_SIZES" :total="filteredObjects.length" /></el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="对象导入中心" name="imports">
|
||||
<section class="metric-strip">
|
||||
<div class="metric-card"><strong>{{ importJobs.length }}</strong><span>导入任务</span><small>预检、确认与回滚</small></div>
|
||||
<div class="metric-card"><strong>{{ readyImportCount }}</strong><span>可确认</span><small>无错误导入批次</small></div>
|
||||
<div class="metric-card"><strong>{{ importRowsTotal }}</strong><span>导入行</span><small>当前任务合计</small></div>
|
||||
<div class="metric-card"><strong>{{ calendars.length }}</strong><span>业务日历</span><small>周期计划引用</small></div>
|
||||
</section>
|
||||
<el-card class="workspace-card" shadow="never">
|
||||
<div class="subsection-head"><div><strong>快速导入</strong><span>JSON 行数组,生产数据导入前保持 DEMO 标记</span></div><el-button type="primary" :loading="saving" @click="createDemoImport">预检导入</el-button></div>
|
||||
<el-input v-model="objectImportText" type="textarea" :rows="8" spellcheck="false" />
|
||||
</el-card>
|
||||
<el-card class="workspace-card" shadow="never">
|
||||
<el-table :data="importJobs" empty-text="暂无导入任务">
|
||||
<el-table-column prop="file_name" label="文件/批次" min-width="180"><template #default="scope"><strong>{{ scope.row.file_name }}</strong><small class="cell-subtext entity-id">{{ scope.row.import_job_id }}</small></template></el-table-column>
|
||||
<el-table-column label="状态" width="145"><template #default="scope"><el-tag :type="importStatusTag(scope.row.status)" effect="plain">{{ importStatusLabel(scope.row.status) }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="统计" min-width="180"><template #default="scope">有效 {{ scope.row.valid_count }} / 错误 {{ scope.row.invalid_count }} / 警告 {{ scope.row.warning_count }}</template></el-table-column>
|
||||
<el-table-column label="质量报告" min-width="210"><template #default="scope">{{ importReport(scope.row.quality_report) }}</template></el-table-column>
|
||||
<el-table-column label="操作" width="170" fixed="right"><template #default="scope"><div class="list-actions"><el-button link type="primary" :disabled="scope.row.status !== 'READY'" @click="commitImport(scope.row)">确认入库</el-button><el-button link type="warning" :disabled="scope.row.status !== 'COMMITTED'" @click="rollbackImport(scope.row)">回滚</el-button></div></template></el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<el-dialog v-model="planDialog" class="plan-create-dialog" :title="planTrigger === 'MANUAL' ? '发起手动巡检' : '新建周期巡检计划'" width="min(780px, 96vw)" destroy-on-close :close-on-click-modal="false">
|
||||
@@ -60,7 +84,11 @@
|
||||
<template #footer><el-button @click="objectDialog = false">取消</el-button><el-button type="primary" :loading="saving" @click="saveObject">创建</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer v-model="detailVisible" title="计划详情" size="560px"><template v-if="selectedPlan"><el-descriptions :column="1" border><el-descriptions-item label="计划编号"><span class="entity-id">{{ selectedPlan.plan_id }}</span></el-descriptions-item><el-descriptions-item label="巡检方式">{{ triggerTypeLabel(selectedPlan.trigger_type) }}</el-descriptions-item><el-descriptions-item label="业务类型">{{ planTypeLabel(selectedPlan.plan_type) }}</el-descriptions-item><el-descriptions-item label="执行安排">{{ scheduleText(selectedPlan) }}</el-descriptions-item><el-descriptions-item label="下次/计划执行">{{ executionDateText(selectedPlan) }}</el-descriptions-item><el-descriptions-item label="优先级">{{ priorityLabel(selectedPlan.priority) }}</el-descriptions-item><el-descriptions-item label="对象">{{ objectNames(selectedPlan.object_ids) }}</el-descriptions-item><el-descriptions-item label="场景">{{ parseArray(selectedPlan.scene_set).join('、') }}</el-descriptions-item><el-descriptions-item label="已生成任务">{{ selectedPlan.generated_task_count }}</el-descriptions-item></el-descriptions></template></el-drawer>
|
||||
<el-drawer v-model="detailVisible" title="计划详情" size="720px"><template v-if="selectedPlan"><el-descriptions :column="1" border><el-descriptions-item label="计划编号"><span class="entity-id">{{ selectedPlan.plan_id }}</span></el-descriptions-item><el-descriptions-item label="巡检方式">{{ triggerTypeLabel(selectedPlan.trigger_type) }}</el-descriptions-item><el-descriptions-item label="业务类型">{{ planTypeLabel(selectedPlan.plan_type) }}</el-descriptions-item><el-descriptions-item label="执行安排">{{ scheduleText(selectedPlan) }}</el-descriptions-item><el-descriptions-item label="下次/计划执行">{{ executionDateText(selectedPlan) }}</el-descriptions-item><el-descriptions-item label="优先级">{{ priorityLabel(selectedPlan.priority) }}</el-descriptions-item><el-descriptions-item label="对象">{{ objectNames(selectedPlan.object_ids) }}</el-descriptions-item><el-descriptions-item label="场景">{{ parseArray(selectedPlan.scene_set).join('、') }}</el-descriptions-item><el-descriptions-item label="已生成任务">{{ selectedPlan.generated_task_count }}</el-descriptions-item></el-descriptions><div class="subsection-head drawer-section"><div><strong>执行预览</strong><span>使用当前调度配置计算后续窗口</span></div><el-button link @click="loadPlanRuntime(selectedPlan)">刷新</el-button></div><el-table :data="previewRunsRows" size="small" empty-text="暂无预览"><el-table-column prop="sequence" label="#" width="52" /><el-table-column label="窗口开始" min-width="160"><template #default="scope">{{ formatDate(scope.row.scheduled_window_start) }}</template></el-table-column><el-table-column label="窗口结束" min-width="160"><template #default="scope">{{ formatDate(scope.row.scheduled_window_end) }}</template></el-table-column><el-table-column label="生成键" min-width="170"><template #default="scope"><span class="entity-id">{{ String(scope.row.generation_key).slice(0, 18) }}</span></template></el-table-column></el-table><div class="subsection-head drawer-section"><div><strong>执行记录</strong><span>后台调度和手动触发均留痕</span></div></div><el-table :data="executionRows" size="small" empty-text="暂无执行记录"><el-table-column prop="status" label="状态" width="110" /><el-table-column label="窗口" min-width="185"><template #default="scope">{{ formatDate(scope.row.scheduled_window_start) }}</template></el-table-column><el-table-column label="任务" min-width="120"><template #default="scope">{{ parseArray(scope.row.generated_task_ids).length }} 个</template></el-table-column><el-table-column prop="trigger_source" label="来源" width="110" /></el-table></template></el-drawer>
|
||||
|
||||
<el-drawer v-model="versionVisible" title="对象版本" size="620px">
|
||||
<template v-if="selectedObject"><h3>{{ selectedObject.name }}</h3><p class="entity-id">{{ selectedObject.object_id }}</p><el-table :data="objectVersionRows" size="small" empty-text="暂无版本记录"><el-table-column label="版本" width="70"><template #default="scope">V{{ scope.row.version_no }}</template></el-table-column><el-table-column prop="change_type" label="变更" width="95" /><el-table-column prop="change_reason" label="原因" min-width="140" /><el-table-column label="来源" min-width="150"><template #default="scope"><span class="entity-id">{{ scope.row.source_job_id || '-' }}</span></template></el-table-column><el-table-column label="时间" min-width="155"><template #default="scope">{{ formatDate(scope.row.created_at) }}</template></el-table-column></el-table></template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -72,16 +100,21 @@ import { Plus, Refresh, Search } from "@element-plus/icons-vue";
|
||||
import PageHeader from "../../components/common/PageHeader.vue";
|
||||
import InspectionPlanForm from "../../components/planning/InspectionPlanForm.vue";
|
||||
import { DEFAULT_PAGE_SIZES, usePagination } from "../../composables/usePagination";
|
||||
import { activateInspectionPlan, createInspectionObject, createInspectionPlan, generatePlanTasks, inspectionObjects, inspectionPlans } from "../../services/api";
|
||||
import { activateInspectionPlan, businessCalendars, commitObjectImport, createInspectionObject, createInspectionPlan, createObjectImport, generatePlanTasks, inspectionObjects, inspectionPlans, objectImports, objectVersions, planExecutions, previewPlanRuns, rollbackObjectImport, runDueInspectionPlans } from "../../services/api";
|
||||
import { safeObject, type Row } from "../../types/demo-run";
|
||||
|
||||
const route = useRoute(); const router = useRouter();
|
||||
const plans = ref<Row[]>([]); const objects = ref<Row[]>([]); const loading = ref(false); const saving = ref(false); const activeTab = ref("plans"); const keyword = ref(""); const objectType = ref("");
|
||||
const planDialog = ref(false); const planFormKey = ref(0); const planTrigger = ref<"PERIODIC" | "MANUAL">("PERIODIC"); const objectDialog = ref(false); const detailVisible = ref(false); const selectedPlan = ref<Row | null>(null);
|
||||
const plans = ref<Row[]>([]); const objects = ref<Row[]>([]); const importJobs = ref<Row[]>([]); const calendars = ref<Row[]>([]); const previewRunsRows = ref<Row[]>([]); const executionRows = ref<Row[]>([]); const objectVersionRows = ref<Row[]>([]); const loading = ref(false); const saving = ref(false); const activeTab = ref("plans"); const keyword = ref(""); const objectType = ref("");
|
||||
const planDialog = ref(false); const planFormKey = ref(0); const planTrigger = ref<"PERIODIC" | "MANUAL">("PERIODIC"); const objectDialog = ref(false); const detailVisible = ref(false); const versionVisible = ref(false); const selectedPlan = ref<Row | null>(null); const selectedObject = ref<Row | null>(null);
|
||||
const objectTypes = [{ value: "RAILWAY", label: "铁路线路" }, { value: "BRIDGE", label: "桥梁" }, { value: "TUNNEL", label: "隧道" }, { value: "FLOOD", label: "防洪设施" }, { value: "POWER", label: "电力设施" }];
|
||||
const scenes = ["异物侵限", "护网破损", "桥梁裂缝", "隧道渗漏", "防洪体积变化", "接触网异物", "设备发热"];
|
||||
const objectForm = reactive({ name: "", object_type: "RAILWAY", line_id: "line-demo", mileage_start: "", mileage_end: "", risk_level: "NORMAL", longitude: 116.1, latitude: 39.1 });
|
||||
const objectImportText = ref(JSON.stringify([
|
||||
{ source_key: "demo-import-bridge-001", object_type: "BRIDGE", name: "导入桥梁样例", line_id: "line-demo", mileage_start: "K126+950", mileage_end: "K127+120", owner_org_id: "org-works", risk_level: "HIGH", longitude: 116.132, latitude: 39.116 },
|
||||
{ source_key: "demo-import-power-001", object_type: "POWER", name: "导入接触网样例", line_id: "line-demo", mileage_start: "K128+500", mileage_end: "K128+620", owner_org_id: "org-power", risk_level: "NORMAL", longitude: 116.154, latitude: 39.107 }
|
||||
], null, 2));
|
||||
const periodicPlans = computed(() => plans.value.filter((item) => item.trigger_type !== "MANUAL").length); const manualPlans = computed(() => plans.value.filter((item) => item.trigger_type === "MANUAL").length); const highRiskObjects = computed(() => objects.value.filter((item) => ["HIGH", "CRITICAL"].includes(item.risk_level)).length); const generatedTasks = computed(() => plans.value.reduce((sum, item) => sum + Number(item.generated_task_count || 0), 0));
|
||||
const readyImportCount = computed(() => importJobs.value.filter((item) => item.status === "READY").length); const importRowsTotal = computed(() => importJobs.value.reduce((sum, item) => sum + Number(item.total_count || 0), 0));
|
||||
const filteredObjects = computed(() => objects.value.filter((item) => (!objectType.value || item.object_type === objectType.value) && (!keyword.value || `${item.name} ${item.line_id} ${item.mileage_start}`.toLowerCase().includes(keyword.value.toLowerCase()))));
|
||||
const { currentPage: planPage, pageSize: planPageSize, pagedItems: pagedPlans } = usePagination(plans);
|
||||
const { currentPage: objectPage, pageSize: objectPageSize, pagedItems: pagedObjects, resetPage: resetObjectPage } = usePagination(filteredObjects);
|
||||
@@ -107,13 +140,22 @@ function parseNumberArray(value: unknown): number[] { return Array.isArray(value
|
||||
function planStatusLabel(row: Row) { if (row.trigger_type === "MANUAL") return Number(row.generated_task_count) ? "任务已生成" : "待创建任务"; return row.status === "ACTIVE" ? "已启用" : "草稿"; }
|
||||
function planStatusTag(row: Row): "success" | "warning" | "info" { if (row.trigger_type === "MANUAL") return Number(row.generated_task_count) ? "success" : "warning"; return row.status === "ACTIVE" ? "success" : "info"; }
|
||||
function objectNames(value: unknown) { const ids = parseArray(value); return ids.map((id) => objects.value.find((item) => item.object_id === id)?.name || id).join("、"); }
|
||||
async function load() { loading.value = true; try { [plans.value, objects.value] = await Promise.all([inspectionPlans(), inspectionObjects()]); } finally { loading.value = false; } }
|
||||
function importStatusLabel(value: unknown) { return ({ READY: "可确认", VALIDATED_WITH_ERRORS: "有错误", COMMITTED: "已入库", ROLLED_BACK: "已回滚", CREATED: "已创建" } as Record<string, string>)[String(value)] || String(value || "-"); }
|
||||
function importStatusTag(value: unknown): "success" | "warning" | "danger" | "info" { if (value === "READY") return "success"; if (value === "VALIDATED_WITH_ERRORS") return "danger"; if (value === "COMMITTED") return "warning"; return "info"; }
|
||||
function importReport(value: unknown) { const data = safeObject(value); return String(data.production_gate || data.software_scope || "-"); }
|
||||
async function load() { loading.value = true; try { [plans.value, objects.value, importJobs.value, calendars.value] = await Promise.all([inspectionPlans(), inspectionObjects(), objectImports(), businessCalendars()]); } finally { loading.value = false; } }
|
||||
function openPlanDialog(trigger: "PERIODIC" | "MANUAL" = "PERIODIC") { planTrigger.value = trigger; planFormKey.value += 1; planDialog.value = true; }
|
||||
async function savePlan(request: { payload: Record<string, unknown>; generateTasks: boolean }) { saving.value = true; try { const created = await createInspectionPlan(request.payload); if (request.generateTasks) { const result = await generatePlanTasks(String(created.plan_id)); ElMessage.success(`手动巡检已创建 ${result.generated} 条任务`); } else { ElMessage.success("周期巡检计划已创建,请确认后启用"); } planDialog.value = false; await load(); } finally { saving.value = false; } }
|
||||
async function saveObject() { if (!objectForm.name || !objectForm.line_id) return ElMessage.warning("请填写对象名称和线路"); saving.value = true; try { await createInspectionObject({ ...objectForm, geometry: { type: "Point", coordinates: [objectForm.longitude, objectForm.latitude] }, owner_org_id: objectForm.object_type === "FLOOD" ? "org-flood" : "org-works", inspection_cycle_days: 7, source_crs: "EPSG:4326" }); objectDialog.value = false; ElMessage.success("巡检对象已创建"); await load(); } finally { saving.value = false; } }
|
||||
async function activate(row: Row) { await activateInspectionPlan(String(row.plan_id)); ElMessage.success("计划已启用"); await load(); }
|
||||
async function generate(row: Row) { const result = await generatePlanTasks(String(row.plan_id)); ElMessage.success(`已生成 ${result.generated} 个巡检任务`); await load(); }
|
||||
function inspectPlan(row: Row) { selectedPlan.value = row; detailVisible.value = true; }
|
||||
async function runDuePlans() { const result = await runDueInspectionPlans(); ElMessage.success(`调度扫描 ${result.scanned} 个到期计划`); await load(); }
|
||||
async function createDemoImport() { let rows: Row[] = []; try { rows = JSON.parse(objectImportText.value); } catch { return ElMessage.warning("请输入合法 JSON 行数组"); } if (!Array.isArray(rows) || rows.length === 0) return ElMessage.warning("导入内容不能为空"); saving.value = true; try { const result = await createObjectImport({ file_name: `demo-object-import-${Date.now()}.json`, file_type: "JSON_ROWS", mapping_profile_id: "object-mapping-demo-line", declared_crs: "EPSG:4326", rows, created_by: "user-dispatcher" }); ElMessage[result.status === "READY" ? "success" : "warning"](`预检完成:有效 ${result.valid_count},错误 ${result.invalid_count}`); await load(); } finally { saving.value = false; } }
|
||||
async function commitImport(row: Row) { const result = await commitObjectImport(String(row.import_job_id)); ElMessage.success(`已入库 ${result.object_ids?.length || 0} 个对象`); await load(); }
|
||||
async function rollbackImport(row: Row) { const result = await rollbackObjectImport(String(row.import_job_id)); ElMessage.success(`已回滚 ${result.rolled_back_objects || 0} 个对象`); await load(); }
|
||||
async function loadPlanRuntime(row: Row) { [previewRunsRows.value, executionRows.value] = await Promise.all([previewPlanRuns(String(row.plan_id)), planExecutions(String(row.plan_id))]); }
|
||||
async function inspectPlan(row: Row) { selectedPlan.value = row; detailVisible.value = true; await loadPlanRuntime(row); }
|
||||
async function showObjectVersions(row: Row) { selectedObject.value = row; objectVersionRows.value = await objectVersions(String(row.object_id)); versionVisible.value = true; }
|
||||
watch([keyword, objectType], resetObjectPage);
|
||||
onMounted(async () => {
|
||||
await load();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div v-loading="loading">
|
||||
<PageHeader title="航线中心" description="管理厂商无关航线、航点参数、校验结果和发布版本">
|
||||
<el-button :icon="Refresh" @click="load">刷新</el-button>
|
||||
<el-button @click="generateDialog = true">自动生成</el-button>
|
||||
<el-button type="primary" :icon="Plus" @click="createDialog = true">新建航线</el-button>
|
||||
</PageHeader>
|
||||
|
||||
@@ -21,9 +22,23 @@
|
||||
<el-table-column label="航点" width="85"><template #default="scope">{{ parseWaypoints(scope.row.waypoints).length }}</template></el-table-column>
|
||||
<el-table-column label="校验" width="110"><template #default="scope"><el-tag :type="validation(scope.row).valid ? 'success' : 'danger'" effect="plain">{{ validation(scope.row).valid ? '通过' : '未通过' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="状态" width="125"><template #default="scope"><el-tag :type="scope.row.status === 'PUBLISHED' ? 'success' : scope.row.status === 'APPROVED' ? 'warning' : 'info'" effect="plain">{{ routeStatusLabel(scope.row.status) }}</el-tag><small class="cell-subtext">{{ approvalLabel(scope.row.approval_status) }}</small></template></el-table-column>
|
||||
<el-table-column label="操作" width="330" fixed="right"><template #default="scope"><div class="list-actions" @click.stop><el-button link type="primary" @click="validateRow(scope.row)">校验</el-button><el-button v-if="['DRAFT','REJECTED'].includes(String(scope.row.approval_status || 'DRAFT'))" link type="warning" @click="submitRow(scope.row)">提交审批</el-button><el-button v-if="scope.row.status === 'APPROVED'" link type="success" @click="publishRow(scope.row)">发布</el-button><el-button link @click="newVersion(scope.row)">新版本</el-button><el-button link @click="inspect(scope.row)">详情</el-button></div></template></el-table-column>
|
||||
<el-table-column label="估算" width="120"><template #default="scope">{{ estimateSummary(scope.row) }}</template></el-table-column>
|
||||
<el-table-column label="操作" width="385" fixed="right"><template #default="scope"><div class="list-actions" @click.stop><el-button link type="primary" @click="validateRow(scope.row)">校验</el-button><el-button link type="primary" @click="estimateRow(scope.row)">估算</el-button><el-button v-if="['DRAFT','REJECTED'].includes(String(scope.row.approval_status || 'DRAFT'))" link type="warning" @click="submitRow(scope.row)">提交审批</el-button><el-button v-if="scope.row.status === 'APPROVED'" link type="success" @click="publishRow(scope.row)">发布</el-button><el-button link @click="newVersion(scope.row)">新版本</el-button><el-button link @click="inspect(scope.row)">详情</el-button></div></template></el-table-column>
|
||||
</el-table><el-pagination v-model:current-page="page" v-model:page-size="pageSize" class="table-pagination" background layout="total, sizes, prev, pager, next" :page-sizes="DEFAULT_PAGE_SIZES" :total="filteredRows.length" /></el-card>
|
||||
|
||||
<el-dialog v-model="generateDialog" title="自动生成候选航线" width="min(700px, 94vw)" destroy-on-close>
|
||||
<el-alert type="warning" :closable="false" show-icon title="当前生成器使用 DEMO/REFERENCE 参数,生成结果为候选草稿,现场使用前需要复核。" />
|
||||
<el-form label-position="top" class="dialog-form"><div class="form-grid">
|
||||
<el-form-item label="巡检对象"><el-select v-model="generateForm.object_id" filterable><el-option v-for="item in objects" :key="item.object_id" :label="`${item.name} / ${objectTypeLabel(item.object_type)}`" :value="item.object_id" /></el-select></el-form-item>
|
||||
<el-form-item label="生成器"><el-select v-model="generateForm.generator_type"><el-option v-for="item in generatorOptions" :key="item.generator_type" :label="item.name" :value="item.generator_type" /></el-select></el-form-item>
|
||||
<el-form-item label="航点数"><el-input-number v-model="generateForm.waypoint_count" :min="2" :max="12" /></el-form-item>
|
||||
<el-form-item label="飞行高度"><el-input-number v-model="generateForm.altitude_m" :min="20" :max="300" /><span class="unit-label">米</span></el-form-item>
|
||||
<el-form-item label="速度"><el-input-number v-model="generateForm.speed_mps" :min="1" :max="20" /><span class="unit-label">米/秒</span></el-form-item>
|
||||
<el-form-item label="创建草稿"><el-switch v-model="generateForm.create_route" active-text="创建" inactive-text="仅预览" /></el-form-item>
|
||||
</div></el-form>
|
||||
<template #footer><el-button @click="generateDialog = false">取消</el-button><el-button type="primary" :loading="saving" @click="generateRouteFromObject">生成</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="createDialog" title="新建航线" width="min(820px, 96vw)" destroy-on-close>
|
||||
<el-form label-position="top"><div class="form-grid"><el-form-item label="航线名称"><el-input v-model="form.name" /></el-form-item><el-form-item label="巡检对象"><el-select v-model="form.object_id" filterable><el-option v-for="item in objects" :key="item.object_id" :label="item.name" :value="item.object_id" /></el-select></el-form-item><el-form-item label="飞行速度"><el-input-number v-model="form.speed_mps" :min="1" :max="20" /><span class="unit-label">米/秒</span></el-form-item><el-form-item label="返航高度"><el-input-number v-model="form.rth_altitude_m" :min="20" :max="500" /><span class="unit-label">米</span></el-form-item></div></el-form>
|
||||
<div class="subsection-head"><div><strong>航点参数</strong><span>WGS84 坐标,高德底图仅在展示层转换</span></div><el-button :icon="Plus" @click="addWaypoint">增加航点</el-button></div>
|
||||
@@ -31,7 +46,7 @@
|
||||
<template #footer><el-button @click="createDialog = false">取消</el-button><el-button type="primary" :loading="saving" @click="save">创建并校验</el-button></template>
|
||||
</el-dialog>
|
||||
|
||||
<el-drawer v-model="detailVisible" title="航线版本详情" size="720px"><template v-if="selected"><div class="route-visual"><div v-for="(point, index) in parseWaypoints(selected.waypoints)" :key="index" class="route-point" :style="pointStyle(point, index)"><span>{{ index + 1 }}</span></div><div class="route-axis"></div></div><el-descriptions :column="1" border><el-descriptions-item label="航线编号"><span class="entity-id">{{ selected.route_id }}</span></el-descriptions-item><el-descriptions-item label="工作版本">V{{ selected.version_no }} · {{ routeStatusLabel(selected.status) }} · {{ approvalLabel(selected.approval_status) }}</el-descriptions-item><el-descriptions-item label="巡检对象">{{ selected.object_name || '-' }}</el-descriptions-item><el-descriptions-item label="飞行参数">{{ jsonText(selected.flight_parameters) }}</el-descriptions-item><el-descriptions-item label="载荷动作">{{ jsonText(selected.payload_actions) }}</el-descriptions-item><el-descriptions-item label="SHA-256"><span class="entity-id">{{ selected.checksum || '-' }}</span></el-descriptions-item></el-descriptions><div class="validation-panel" :class="{ invalid: !validation(selected).valid }"><strong>{{ validation(selected).valid ? '航线校验通过' : '航线校验未通过' }}</strong><p v-for="message in [...(validation(selected).errors || []), ...(validation(selected).warnings || [])]" :key="message">{{ message }}</p><small v-if="!(validation(selected).errors?.length || validation(selected).warnings?.length)">航点数量、高度和速度均符合平台校验规则</small></div><div class="subsection-head version-head"><div><strong>不可变版本历史</strong><span>回滚将创建一个新草稿,不覆盖历史任务引用</span></div><el-button link @click="loadVersions(selected)">刷新</el-button></div><el-table :data="versionRows" size="small" empty-text="暂无版本"><el-table-column label="版本" width="70"><template #default="scope">V{{ scope.row.version_no }}</template></el-table-column><el-table-column prop="status" label="状态" width="105" /><el-table-column prop="approval_status" label="审批" width="120" /><el-table-column label="摘要" min-width="150"><template #default="scope"><span class="entity-id">{{ String(scope.row.checksum || '-').slice(0, 16) }}</span></template></el-table-column><el-table-column label="创建时间" min-width="155"><template #default="scope">{{ formatDate(scope.row.created_at) }}</template></el-table-column><el-table-column label="操作" width="90"><template #default="scope"><el-button link type="warning" :disabled="scope.row.route_version_id === activeVersionId(selected)" @click="rollbackVersion(scope.row)">回滚</el-button></template></el-table-column></el-table></template></el-drawer>
|
||||
<el-drawer v-model="detailVisible" title="航线版本详情" size="760px"><template v-if="selected"><div class="route-visual"><div v-for="(point, index) in parseWaypoints(selected.waypoints)" :key="index" class="route-point" :style="pointStyle(point, index)"><span>{{ index + 1 }}</span></div><div class="route-axis"></div></div><el-descriptions :column="1" border><el-descriptions-item label="航线编号"><span class="entity-id">{{ selected.route_id }}</span></el-descriptions-item><el-descriptions-item label="工作版本">V{{ selected.version_no }} · {{ routeStatusLabel(selected.status) }} · {{ approvalLabel(selected.approval_status) }}</el-descriptions-item><el-descriptions-item label="巡检对象">{{ selected.object_name || '-' }}</el-descriptions-item><el-descriptions-item label="飞行参数">{{ jsonText(selected.flight_parameters) }}</el-descriptions-item><el-descriptions-item label="载荷动作">{{ jsonText(selected.payload_actions) }}</el-descriptions-item><el-descriptions-item label="估算结果">{{ routeEstimateText(selected.estimation_result) }}</el-descriptions-item><el-descriptions-item label="编辑锁">V{{ selected.version_lock ?? 0 }} · {{ selected.edited_by || selected.created_by || '-' }}</el-descriptions-item><el-descriptions-item label="SHA-256"><span class="entity-id">{{ selected.checksum || '-' }}</span></el-descriptions-item></el-descriptions><div class="validation-panel" :class="{ invalid: !validation(selected).valid }"><strong>{{ validation(selected).valid ? '航线校验通过' : '航线校验未通过' }}</strong><p v-for="message in [...(validation(selected).errors || []), ...(validation(selected).warnings || [])]" :key="message">{{ message }}</p><small v-if="!(validation(selected).errors?.length || validation(selected).warnings?.length)">航点数量、高度和速度均符合平台校验规则</small></div><div class="drawer-actions"><el-button type="primary" @click="estimateRow(selected)">重新估算</el-button><el-button :disabled="!['DRAFT','REJECTED'].includes(String(selected.status))" @click="saveWaypointRevision(selected)">保存编辑修订</el-button></div><div class="subsection-head version-head"><div><strong>不可变版本历史</strong><span>回滚将创建一个新草稿,不覆盖历史任务引用</span></div><el-button link @click="loadVersions(selected)">刷新</el-button></div><el-table :data="versionRows" size="small" empty-text="暂无版本"><el-table-column label="版本" width="70"><template #default="scope">V{{ scope.row.version_no }}</template></el-table-column><el-table-column prop="status" label="状态" width="105" /><el-table-column prop="approval_status" label="审批" width="120" /><el-table-column label="估算" min-width="120"><template #default="scope">{{ routeEstimateText(scope.row.estimation_result) }}</template></el-table-column><el-table-column label="摘要" min-width="150"><template #default="scope"><span class="entity-id">{{ String(scope.row.checksum || '-').slice(0, 16) }}</span></template></el-table-column><el-table-column label="操作" width="90"><template #default="scope"><el-button link type="warning" :disabled="scope.row.route_version_id === activeVersionId(selected)" @click="rollbackVersion(scope.row)">回滚</el-button></template></el-table-column></el-table><div class="subsection-head version-head"><div><strong>编辑审计</strong><span>航点和载荷动作修改记录</span></div></div><el-table :data="revisionRows" size="small" empty-text="暂无编辑修订"><el-table-column prop="revision_no" label="修订" width="70" /><el-table-column prop="editor_id" label="编辑人" width="120" /><el-table-column prop="change_summary" label="说明" min-width="160" /><el-table-column label="摘要" min-width="130"><template #default="scope"><span class="entity-id">{{ String(scope.row.checksum || '-').slice(0, 14) }}</span></template></el-table-column></el-table><div class="subsection-head version-head"><div><strong>通用动作 Schema</strong><span>{{ actionSchemas.length }} 项厂商无关动作约束</span></div></div><el-table :data="actionSchemas" size="small"><el-table-column prop="action_type" label="动作" width="150" /><el-table-column prop="device_type" label="设备" width="130" /><el-table-column label="状态" width="90"><template #default="scope"><el-tag size="small" type="success" effect="plain">{{ scope.row.status }}</el-tag></template></el-table-column></el-table></template></el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -41,32 +56,46 @@ import { ElMessage } from "element-plus";
|
||||
import { Delete, Plus, Refresh, Search } from "@element-plus/icons-vue";
|
||||
import PageHeader from "../../components/common/PageHeader.vue";
|
||||
import { DEFAULT_PAGE_SIZES, usePagination } from "../../composables/usePagination";
|
||||
import { createInspectionRoute, createRouteVersion, executeWorkflowAction, inspectionObjects, inspectionRoutes, publishRouteVersion, rollbackRoute, routeVersions, validateRouteVersion } from "../../services/api";
|
||||
import { createInspectionRoute, createRouteVersion, estimateRouteVersion, executeWorkflowAction, generateCandidateRoute, inspectionObjects, inspectionRoutes, publishRouteVersion, rollbackRoute, routeActionSchemas, routeEditorRevisions, routeGenerationProfiles, routeVersions, updateRouteWaypoints, validateRouteVersion } from "../../services/api";
|
||||
import { safeObject, type Row } from "../../types/demo-run";
|
||||
|
||||
const rows = ref<Row[]>([]); const objects = ref<Row[]>([]); const versionRows = ref<Row[]>([]); const loading = ref(false); const saving = ref(false); const createDialog = ref(false); const detailVisible = ref(false); const selected = ref<Row | null>(null); const keyword = ref(""); const statusFilter = ref("");
|
||||
const rows = ref<Row[]>([]); const objects = ref<Row[]>([]); const versionRows = ref<Row[]>([]); const generatorProfiles = ref<Row[]>([]); const revisionRows = ref<Row[]>([]); const actionSchemas = ref<Row[]>([]); const loading = ref(false); const saving = ref(false); const createDialog = ref(false); const generateDialog = ref(false); const detailVisible = ref(false); const selected = ref<Row | null>(null); const keyword = ref(""); const statusFilter = ref("");
|
||||
const form = reactive({ name: "铁路沿线巡检航线", object_id: "object-line-demo", speed_mps: 7, rth_altitude_m: 100, waypoints: [{ longitude: 116.09, latitude: 39.095, altitude_m: 80, gimbal_pitch_deg: -45 }, { longitude: 116.125, latitude: 39.115, altitude_m: 80, gimbal_pitch_deg: -45 }, { longitude: 116.165, latitude: 39.105, altitude_m: 80, gimbal_pitch_deg: -45 }] });
|
||||
const generateForm = reactive({ object_id: "object-line-demo", generator_type: "", waypoint_count: 5, altitude_m: 80, speed_mps: 7, create_route: true });
|
||||
const filteredRows = computed(() => rows.value.filter((row) => (!statusFilter.value || row.status === statusFilter.value) && (!keyword.value || `${row.name} ${row.object_name} ${row.template_name}`.toLowerCase().includes(keyword.value.toLowerCase())))); const publishedCount = computed(() => rows.value.filter((row) => row.status === "PUBLISHED").length); const draftCount = computed(() => rows.value.filter((row) => row.status !== "PUBLISHED").length); const totalWaypoints = computed(() => rows.value.reduce((sum, row) => sum + parseWaypoints(row.waypoints).length, 0));
|
||||
const generatorOptions = computed(() => generatorProfiles.value.filter((item) => !generateForm.object_id || item.object_type === objects.value.find((object) => object.object_id === generateForm.object_id)?.object_type));
|
||||
const { currentPage: page, pageSize, pagedItems: pagedRows, resetPage } = usePagination(filteredRows);
|
||||
function parseWaypoints(value: unknown): Row[] { try { return Array.isArray(value) ? value as Row[] : JSON.parse(String(value || "[]")); } catch { return []; } }
|
||||
function validation(row: Row): Row { return safeObject(row.validation_result); }
|
||||
function activeVersionId(row: Row) { return String(row.display_version_id || row.working_version_id || row.current_version_id || ""); }
|
||||
function routeStatusLabel(value: unknown) { return ({ DRAFT: "草稿", PENDING_APPROVAL: "审批中", APPROVED: "已审批", PUBLISHED: "已发布", REJECTED: "已驳回", RETIRED: "已退役" } as Record<string, string>)[String(value)] || String(value || "-"); }
|
||||
function approvalLabel(value: unknown) { return ({ DRAFT: "待提交", PENDING_APPROVAL: "审批中", APPROVED: "审批通过", REJECTED: "已驳回" } as Record<string, string>)[String(value)] || String(value || "待提交"); }
|
||||
function objectTypeLabel(value: unknown) { return ({ RAILWAY: "铁路线路", BRIDGE: "桥梁", TUNNEL: "隧道", FLOOD: "防洪", POWER: "电力" } as Record<string, string>)[String(value)] || String(value || "-"); }
|
||||
function formatDate(value: unknown) { return value ? new Date(String(value)).toLocaleString("zh-CN", { hour12: false }) : "-"; }
|
||||
function jsonText(value: unknown) { const data = typeof value === "string" ? safeObject(value) : value; return Object.entries(data as Row || {}).map(([key, item]) => `${key}: ${Array.isArray(item) ? item.length + '项' : item}`).join(",") || "-"; }
|
||||
function routeEstimate(row: unknown) { return typeof row === "string" ? safeObject(row) : safeObject(row); }
|
||||
function routeEstimateText(value: unknown) { const data = routeEstimate(value); return data.estimated_minutes ? `${data.estimated_minutes} 分钟 · 电量 ${data.battery_percent}% · ${data.storage_mb} MB` : "待估算"; }
|
||||
function estimateSummary(row: Row) { return routeEstimateText(row.estimation_result); }
|
||||
function addWaypoint() { const previous = form.waypoints.at(-1) || { longitude: 116.1, latitude: 39.1, altitude_m: 80, gimbal_pitch_deg: -45 }; form.waypoints.push({ ...previous, longitude: previous.longitude + 0.005, latitude: previous.latitude + 0.003 }); }
|
||||
async function inspect(row: Row) { selected.value = row; detailVisible.value = true; await loadVersions(row); }
|
||||
async function inspect(row: Row) { selected.value = row; detailVisible.value = true; await Promise.all([loadVersions(row), loadRevisionRows(row)]); }
|
||||
function pointStyle(_point: Row, index: number) { const count = Math.max(2, parseWaypoints(selected.value?.waypoints).length); return { left: `${10 + index * 80 / (count - 1)}%`, top: `${60 - Math.sin(index * 1.4) * 25}%` }; }
|
||||
async function load() { loading.value = true; try { [rows.value, objects.value] = await Promise.all([inspectionRoutes(), inspectionObjects()]); } finally { loading.value = false; } }
|
||||
async function load() { loading.value = true; try { [rows.value, objects.value, generatorProfiles.value, actionSchemas.value] = await Promise.all([inspectionRoutes(), inspectionObjects(), routeGenerationProfiles(), routeActionSchemas()]); if (!generateForm.generator_type && generatorProfiles.value.length) generateForm.generator_type = String(generatorProfiles.value[0].generator_type); } finally { loading.value = false; } }
|
||||
async function save() { if (!form.name || form.waypoints.length < 2) return ElMessage.warning("请填写航线名称并配置至少两个航点"); saving.value = true; try { await createInspectionRoute({ name: form.name, object_id: form.object_id, template_id: "template-railway", waypoints: form.waypoints.map((point, index) => ({ ...point, sequence: index + 1, speed_mps: form.speed_mps, actions: ["TAKE_PHOTO"] })), flight_parameters: { speed_mps: form.speed_mps, rth_altitude_m: form.rth_altitude_m }, payload_actions: [{ type: "TAKE_PHOTO", interval_s: 2 }], created_by: "user-dispatcher" }); createDialog.value = false; ElMessage.success("航线已创建并完成校验"); await load(); } finally { saving.value = false; } }
|
||||
async function validateRow(row: Row) { const result = await validateRouteVersion(activeVersionId(row)); ElMessage[result.valid ? "success" : "error"](result.valid ? "航线校验通过" : result.errors.join(";")); await load(); }
|
||||
async function estimateRow(row: Row) { const result = await estimateRouteVersion(activeVersionId(row), { created_by: "route-estimator" }); ElMessage[result.warnings?.length ? "warning" : "success"](`估算完成:${result.estimated_minutes} 分钟,电量 ${result.battery_percent}%`); await load(); if (selected.value?.route_id === row.route_id) selected.value = rows.value.find((item) => item.route_id === row.route_id) || selected.value; }
|
||||
async function submitRow(row: Row) { const action = String(row.approval_status) === "REJECTED" ? "RESUBMIT" : "SUBMIT"; await executeWorkflowAction("ROUTE_VERSION", activeVersionId(row), action as "SUBMIT" | "RESUBMIT", "航线校验完成,提交发布审批"); ElMessage.success("航线已进入审批队列"); await load(); }
|
||||
async function publishRow(row: Row) { try { await publishRouteVersion(activeVersionId(row)); ElMessage.success("航线版本已发布,历史版本已保留"); await load(); } catch { /* global error */ } }
|
||||
async function newVersion(row: Row) { const result = await createRouteVersion(String(row.route_id), { source_version_id: activeVersionId(row), change_summary: "基于当前版本创建编辑草稿", created_by: "航线管理员" }); ElMessage.success(`已创建 V${result.version_no} 草稿`); await load(); }
|
||||
async function loadVersions(row: Row) { versionRows.value = await routeVersions(String(row.route_id)); }
|
||||
async function loadRevisionRows(row: Row) { revisionRows.value = await routeEditorRevisions(activeVersionId(row)); }
|
||||
async function rollbackVersion(version: Row) { if (!selected.value) return; const result = await rollbackRoute(String(selected.value.route_id), String(version.route_version_id), `回滚至 V${version.version_no}`); ElMessage.success(`已创建回滚草稿 V${result.version_no},请重新校验和审批`); await load(); const refreshed = rows.value.find((row) => row.route_id === selected.value?.route_id); if (refreshed) { selected.value = refreshed; await loadVersions(refreshed); } }
|
||||
async function generateRouteFromObject() { if (!generateForm.object_id) return ElMessage.warning("请选择巡检对象"); saving.value = true; try { const result = await generateCandidateRoute({ object_id: generateForm.object_id, generator_type: generateForm.generator_type || undefined, create_route: generateForm.create_route, route_name: `${objects.value.find((item) => item.object_id === generateForm.object_id)?.name || "对象"} 候选航线`, parameters: { waypoint_count: generateForm.waypoint_count, altitude_m: generateForm.altitude_m, speed_mps: generateForm.speed_mps }, created_by: "route-generator" }); ElMessage.success(generateForm.create_route ? `已创建候选航线 ${result.created_route?.route_id}` : `已生成 ${result.waypoints?.length || 0} 个候选航点`); generateDialog.value = false; await load(); } finally { saving.value = false; } }
|
||||
async function saveWaypointRevision(row: Row) { const waypoints = parseWaypoints(row.waypoints).map((point, index) => ({ ...point, sequence: index + 1 })); const result = await updateRouteWaypoints(activeVersionId(row), { waypoints, flight_parameters: safeObject(row.flight_parameters), payload_actions: parseWaypoints(row.payload_actions), change_summary: "前端地图编辑器保存航点修订", editor_id: "route-editor", expected_version_lock: Number(row.version_lock || 0) }); ElMessage.success(`已保存编辑修订 V${result.version_lock}`); await load(); const refreshed = rows.value.find((item) => item.route_id === row.route_id); if (refreshed) { selected.value = refreshed; await Promise.all([loadVersions(refreshed), loadRevisionRows(refreshed)]); } }
|
||||
watch([keyword, statusFilter], resetPage);
|
||||
watch(() => generateForm.object_id, () => {
|
||||
const first = generatorOptions.value[0];
|
||||
if (first) generateForm.generator_type = String(first.generator_type);
|
||||
});
|
||||
onMounted(load);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -30,6 +30,30 @@
|
||||
<el-pagination v-model:current-page="devicePage" v-model:page-size="devicePageSize" class="table-pagination" background layout="total, sizes, prev, pager, next" :page-sizes="DEFAULT_PAGE_SIZES" :total="devices.length" />
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="资源日历" name="resources">
|
||||
<div class="filter-bar">
|
||||
<el-select v-model="resourceTaskId" filterable placeholder="选择待检查任务"><el-option v-for="item in taskRows" :key="item.task_id" :label="`${item.task_id} / ${item.route_name || item.route_id || '待配置航线'}`" :value="String(item.task_id)" /></el-select>
|
||||
<span class="filter-spacer"></span>
|
||||
<el-button @click="syncResources">同步资源</el-button>
|
||||
<el-button type="primary" :disabled="!resourceTaskId" @click="checkResourceConflicts">冲突检查</el-button>
|
||||
<el-button :disabled="!resourceTaskId || selectedResourceIds.length === 0" @click="reserveResources">预占选中资源</el-button>
|
||||
</div>
|
||||
<el-alert v-if="conflictResult" class="resource-alert" :type="conflictResult.blocking_count ? 'error' : conflictResult.warning_count ? 'warning' : 'success'" :closable="false" show-icon :title="`检查结果:${conflictResult.status}`" :description="conflictSummary" />
|
||||
<el-card class="workspace-card" shadow="never">
|
||||
<el-table :data="resourceRows" empty-text="暂无作业资源" @selection-change="selectResources">
|
||||
<el-table-column type="selection" width="45" />
|
||||
<el-table-column prop="name" label="资源" min-width="160"><template #default="scope"><strong>{{ scope.row.name }}</strong><small class="cell-subtext">{{ resourceTypeLabel(scope.row.resource_type) }}</small></template></el-table-column>
|
||||
<el-table-column label="能力" min-width="190"><template #default="scope">{{ parseArray(scope.row.capabilities).join(' / ') || '-' }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="105"><template #default="scope"><el-tag :type="scope.row.status === 'AVAILABLE' ? 'success' : 'warning'" effect="plain">{{ scope.row.status === 'AVAILABLE' ? '可用' : scope.row.status }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="健康" min-width="145"><template #default="scope">{{ resourceHealth(scope.row.health_snapshot) }}</template></el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<el-card class="workspace-card" shadow="never">
|
||||
<div class="subsection-head"><div><strong>资源预占与不可用窗口</strong><span>下发前硬门禁使用同一资源日历</span></div></div>
|
||||
<el-table :data="[...reservationRows, ...unavailableRows]" size="small" empty-text="暂无窗口"><el-table-column label="资源" min-width="160"><template #default="scope">{{ scope.row.resource_name }}</template></el-table-column><el-table-column label="窗口" min-width="230"><template #default="scope">{{ formatDate(scope.row.start_at) }} - {{ formatDate(scope.row.end_at) }}</template></el-table-column><el-table-column label="类型" width="130"><template #default="scope">{{ scope.row.reservation_type || scope.row.reason_type }}</template></el-table-column><el-table-column label="状态/来源" width="130"><template #default="scope">{{ scope.row.status || scope.row.source }}</template></el-table-column></el-table>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="飞行任务" name="missions">
|
||||
<el-card class="workspace-card" shadow="never"><el-table :data="pagedMissions" empty-text="暂无飞行任务" @row-click="inspectMission">
|
||||
<el-table-column prop="mission_id" label="飞行任务" min-width="175"><template #default="scope"><span class="entity-id">{{ scope.row.mission_id }}</span><small class="cell-subtext">{{ scope.row.vendor_mission_id || '等待厂商回执' }}</small></template></el-table-column>
|
||||
@@ -63,11 +87,11 @@ import { ElMessage } from "element-plus";
|
||||
import { CircleCheck, Position, Promotion, Refresh, Warning } from "@element-plus/icons-vue";
|
||||
import PageHeader from "../../components/common/PageHeader.vue";
|
||||
import { DEFAULT_PAGE_SIZES, usePagination } from "../../composables/usePagination";
|
||||
import { checkUavConnection, dispatchUavMission, executeUavMissionCommand, synchronizeUavDevices, tasks, uavConnections, uavDevices, uavMissions, uavMissionTelemetry } from "../../services/api";
|
||||
import { checkTaskConflicts, checkUavConnection, dispatchUavMission, executeUavMissionCommand, operationResourceCalendar, reserveTaskResources, syncOperationResources, synchronizeUavDevices, tasks, uavConnections, uavDevices, uavMissions, uavMissionTelemetry } from "../../services/api";
|
||||
import { safeObject, type Row } from "../../types/demo-run";
|
||||
|
||||
const route = useRoute();
|
||||
const connections = ref<Row[]>([]); const devices = ref<Row[]>([]); const missions = ref<Row[]>([]); const taskRows = ref<Row[]>([]); const loading = ref(false); const saving = ref(false); const activeTab = ref(String(route.query.tab || "connections")); const dispatchDialog = ref(false); const healthVisible = ref(false); const healthResult = ref<Row | null>(null); const missionVisible = ref(false); const selectedMission = ref<Row | null>(null); const telemetry = ref<Row[]>([]); const deepLinkHandled = ref(false);
|
||||
const connections = ref<Row[]>([]); const devices = ref<Row[]>([]); const missions = ref<Row[]>([]); const taskRows = ref<Row[]>([]); const resourceRows = ref<Row[]>([]); const reservationRows = ref<Row[]>([]); const unavailableRows = ref<Row[]>([]); const selectedResourceIds = ref<string[]>([]); const conflictResult = ref<Row | null>(null); const resourceTaskId = ref(""); const loading = ref(false); const saving = ref(false); const activeTab = ref(String(route.query.tab || "connections")); const dispatchDialog = ref(false); const healthVisible = ref(false); const healthResult = ref<Row | null>(null); const missionVisible = ref(false); const selectedMission = ref<Row | null>(null); const telemetry = ref<Row[]>([]); const deepLinkHandled = ref(false);
|
||||
const dispatchForm = reactive({ task_id: "", route_id: "", connection_id: "connection-simulator", device_id: "uav-sim-01", dock_id: "dock-sim-01" });
|
||||
const availableConnections = computed(() => connections.value.filter((item) => item.status === "AVAILABLE").length); const onlineDevices = computed(() => devices.value.filter((item) => item.status === "ONLINE").length); const activeMissions = computed(() => missions.value.filter((item) => ["DISPATCHING","DISPATCHED","PREPARING","FLYING","PAUSED","RETURNING"].includes(item.status)).length); const completedMissions = computed(() => missions.value.filter((item) => item.status === "COMPLETED").length); const dispatchableTasks = computed(() => taskRows.value.filter((item) => item.can_dispatch === true || String(item.can_dispatch).toLowerCase() === "true")); const selectedTask = computed(() => taskRows.value.find((item) => String(item.task_id) === dispatchForm.task_id)); const usableConnections = computed(() => connections.value.filter((item) => item.status === "AVAILABLE")); const availableDevices = computed(() => devices.value.filter((item) => item.connection_id === dispatchForm.connection_id && item.status === "ONLINE"));
|
||||
const { currentPage: connectionPage, pageSize: connectionPageSize, pagedItems: pagedConnections } = usePagination(connections);
|
||||
@@ -82,12 +106,24 @@ function missionTag(value: unknown): "success" | "warning" | "danger" | "info" |
|
||||
function formatDate(value: unknown) { return value ? new Date(String(value)).toLocaleString("zh-CN", { hour12: false }) : "-"; } function timeOnly(value: unknown) { return value ? new Date(String(value)).toLocaleTimeString("zh-CN", { hour12: false }) : "-"; }
|
||||
function layerLabel(value: unknown) { return ({ NETWORK: "网络可达", AUTHENTICATION: "身份认证", CAPABILITIES: "能力读取", BUSINESS_HANDSHAKE: "业务握手" } as Record<string, string>)[String(value)] || String(value); }
|
||||
function telemetrySummary(value: unknown) { const data = safeObject(value); return `电量 ${data.battery_percent ?? '-'}% · 链路 ${data.link_quality ?? '-'}% · RTK ${data.rtk_status || '-'}`; }
|
||||
async function load() { loading.value = true; try { [connections.value, devices.value, missions.value, taskRows.value] = await Promise.all([uavConnections(), uavDevices(), uavMissions(), tasks()]); if (!dispatchForm.task_id && dispatchableTasks.value.length) dispatchForm.task_id = String(dispatchableTasks.value[0].task_id); selectTask(); if (!usableConnections.value.some((item) => item.connection_id === dispatchForm.connection_id)) dispatchForm.connection_id = String(usableConnections.value[0]?.connection_id || ""); selectConnection(); } finally { loading.value = false; } }
|
||||
const conflictSummary = computed(() => {
|
||||
if (!conflictResult.value) return "";
|
||||
const checks = Array.isArray(conflictResult.value.checks) ? conflictResult.value.checks as Row[] : [];
|
||||
return checks.filter((item) => item.level !== "PASSED").map((item) => `${item.code}: ${item.message}`).join(";") || "所有资源窗口可用,天气和空域若未接入会显示 NOT_CHECKED";
|
||||
});
|
||||
function resourceTypeLabel(value: unknown) { return ({ AIRCRAFT: "无人机", DOCK: "机巢", PAYLOAD: "载荷", PILOT: "飞手" } as Record<string, string>)[String(value)] || String(value || "-"); }
|
||||
function resourceHealth(value: unknown) { const data = safeObject(value); return Object.entries(data).slice(0, 2).map(([key, item]) => `${key}:${item}`).join(" · ") || "-"; }
|
||||
async function loadResourceCalendar() { const calendar = await operationResourceCalendar(); resourceRows.value = calendar.resources || []; reservationRows.value = calendar.reservations || []; unavailableRows.value = calendar.unavailable_windows || []; if (!resourceTaskId.value) resourceTaskId.value = String(dispatchableTasks.value[0]?.task_id || taskRows.value[0]?.task_id || ""); }
|
||||
async function load() { loading.value = true; try { [connections.value, devices.value, missions.value, taskRows.value] = await Promise.all([uavConnections(), uavDevices(), uavMissions(), tasks()]); if (!dispatchForm.task_id && dispatchableTasks.value.length) dispatchForm.task_id = String(dispatchableTasks.value[0].task_id); if (!resourceTaskId.value) resourceTaskId.value = String(dispatchableTasks.value[0]?.task_id || taskRows.value[0]?.task_id || ""); selectTask(); if (!usableConnections.value.some((item) => item.connection_id === dispatchForm.connection_id)) dispatchForm.connection_id = String(usableConnections.value[0]?.connection_id || ""); selectConnection(); await loadResourceCalendar(); } finally { loading.value = false; } }
|
||||
function selectTask() { dispatchForm.route_id = String(selectedTask.value?.route_id || ""); }
|
||||
function selectConnection() { const first = devices.value.find((item) => item.connection_id === dispatchForm.connection_id && item.status === "ONLINE"); dispatchForm.device_id = first ? String(first.device_id) : ""; dispatchForm.dock_id = ""; }
|
||||
function openDispatchDialog(taskId?: string) { const target = taskId ? dispatchableTasks.value.find((item) => String(item.task_id) === taskId) : dispatchableTasks.value[0]; if (!target) return ElMessage.warning(taskId ? "该任务当前不可下发,请返回巡检任务查看准备状态" : "当前没有满足审批、航线和状态条件的待下发任务"); dispatchForm.task_id = String(target.task_id); selectTask(); activeTab.value = "missions"; dispatchDialog.value = true; }
|
||||
async function checkConnection(row: Row) { healthResult.value = await checkUavConnection(String(row.connection_id)); healthVisible.value = true; await load(); }
|
||||
async function syncDevices(row: Row) { const result = await synchronizeUavDevices(String(row.connection_id)); ElMessage.success(`已同步 ${result.synchronized} 台设备`); await load(); activeTab.value = "devices"; }
|
||||
function selectResources(rows: Row[]) { selectedResourceIds.value = rows.map((row) => String(row.resource_id)); }
|
||||
async function syncResources() { const result = await syncOperationResources(); ElMessage.success(`已同步作业资源:设备 ${result.devices},机巢 ${result.docks}`); await loadResourceCalendar(); }
|
||||
async function checkResourceConflicts() { if (!resourceTaskId.value) return; const result = await checkTaskConflicts(resourceTaskId.value, { resource_ids: selectedResourceIds.value, duration_minutes: 90, requested_by: "user-dispatcher" }); conflictResult.value = result; ElMessage[result.blocking_count ? "error" : result.warning_count ? "warning" : "success"](`阻断 ${result.blocking_count || 0},警告 ${result.warning_count || 0}`); }
|
||||
async function reserveResources() { if (!resourceTaskId.value || selectedResourceIds.value.length === 0) return; const result = await reserveTaskResources(resourceTaskId.value, { resource_ids: selectedResourceIds.value, duration_minutes: 90, reservation_type: "TASK_WINDOW", idempotency_key: `reserve-${resourceTaskId.value}-${Date.now()}`, requested_by: "user-dispatcher" }); ElMessage.success(`已预占 ${result.reservation_ids?.length || 0} 项资源`); await loadResourceCalendar(); }
|
||||
async function dispatch() { if (!dispatchForm.task_id || !dispatchForm.route_id || !dispatchForm.device_id) return ElMessage.warning("请选择任务、航线和无人机"); saving.value = true; try { const result = await dispatchUavMission({ ...dispatchForm, requested_by: "user-dispatcher", idempotency_key: `dispatch-${dispatchForm.task_id}-${Date.now()}` }); ElMessage.success(`任务已下发:${result.vendor_mission_id}`); dispatchDialog.value = false; activeTab.value = "missions"; await load(); } finally { saving.value = false; } }
|
||||
async function command(row: Row, action: string) { const result = await executeUavMissionCommand(String(row.mission_id), action, Number(row.version_no)); ElMessage.success(result.accepted ? `命令已受理,等待司空 2 状态回传:${missionLabel(result.status)}` : `命令完成,任务状态:${missionLabel(result.status)}`); await load(); }
|
||||
async function inspectMission(row: Row) { selectedMission.value = row; telemetry.value = await uavMissionTelemetry(String(row.mission_id)); missionVisible.value = true; }
|
||||
|
||||
+1
@@ -334,6 +334,7 @@ public class CapabilityCompletionService {
|
||||
rv.approval_status,ST_AsGeoJSON(rv.geom) as geometry,
|
||||
rv.waypoints::text as waypoints, rv.flight_parameters::text as flight_parameters,
|
||||
rv.payload_actions::text as payload_actions, rv.validation_result::text as validation_result,
|
||||
rv.estimation_result::text as estimation_result,rv.version_lock,rv.edited_at,rv.edited_by,
|
||||
rv.checksum,rv.change_summary,rv.source_version_id,
|
||||
rv.published_at,rv.published_by,r.created_at,r.updated_at
|
||||
from routes r
|
||||
|
||||
+271
-1
@@ -2,15 +2,18 @@ package com.ai.trackwalker.foundation.api;
|
||||
|
||||
import com.ai.trackwalker.api.ApiResponse;
|
||||
import com.ai.trackwalker.capability.service.CapabilityCompletionService;
|
||||
import com.ai.trackwalker.foundation.service.DependencyFreeCompletionService;
|
||||
import com.ai.trackwalker.foundation.service.FoundationCompletionService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.ContentDisposition;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
@@ -24,13 +27,16 @@ import java.util.Map;
|
||||
public class FoundationCompletionController {
|
||||
private final FoundationCompletionService service;
|
||||
private final CapabilityCompletionService capabilityService;
|
||||
private final DependencyFreeCompletionService dependencyFreeService;
|
||||
|
||||
public FoundationCompletionController(
|
||||
FoundationCompletionService service,
|
||||
CapabilityCompletionService capabilityService
|
||||
CapabilityCompletionService capabilityService,
|
||||
DependencyFreeCompletionService dependencyFreeService
|
||||
) {
|
||||
this.service = service;
|
||||
this.capabilityService = capabilityService;
|
||||
this.dependencyFreeService = dependencyFreeService;
|
||||
}
|
||||
|
||||
@GetMapping("/inspection/task-templates")
|
||||
@@ -269,4 +275,268 @@ public class FoundationCompletionController {
|
||||
.header("X-Content-SHA256", content.checksum())
|
||||
.body(content.content());
|
||||
}
|
||||
|
||||
@GetMapping("/inspection/object-mapping-profiles")
|
||||
public ApiResponse<?> objectMappingProfiles() {
|
||||
return ApiResponse.ok(Map.of("profiles", dependencyFreeService.objectMappingProfiles()));
|
||||
}
|
||||
|
||||
@GetMapping("/inspection/object-imports")
|
||||
public ApiResponse<?> objectImports() {
|
||||
return ApiResponse.ok(Map.of("imports", dependencyFreeService.objectImports()));
|
||||
}
|
||||
|
||||
@PostMapping("/inspection/object-imports")
|
||||
public ApiResponse<?> createObjectImport(
|
||||
@Valid @RequestBody FoundationRequests.CreateObjectImportRequest request
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.createObjectImport(request));
|
||||
}
|
||||
|
||||
@GetMapping("/inspection/object-imports/{jobId}")
|
||||
public ApiResponse<?> objectImport(@PathVariable String jobId) {
|
||||
return ApiResponse.ok(dependencyFreeService.objectImport(jobId));
|
||||
}
|
||||
|
||||
@GetMapping("/inspection/object-imports/{jobId}/rows")
|
||||
public ApiResponse<?> objectImportRows(
|
||||
@PathVariable String jobId,
|
||||
@RequestParam(required = false) String status
|
||||
) {
|
||||
return ApiResponse.ok(Map.of("rows", dependencyFreeService.objectImportRows(jobId, status)));
|
||||
}
|
||||
|
||||
@PostMapping("/inspection/object-imports/{jobId}/validate")
|
||||
public ApiResponse<?> validateObjectImport(@PathVariable String jobId) {
|
||||
return ApiResponse.ok(dependencyFreeService.validateObjectImport(jobId));
|
||||
}
|
||||
|
||||
@PostMapping("/inspection/object-imports/{jobId}/commit")
|
||||
public ApiResponse<?> commitObjectImport(
|
||||
@PathVariable String jobId,
|
||||
@RequestParam(defaultValue = "user-dispatcher") String confirmedBy
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.commitObjectImport(jobId, confirmedBy));
|
||||
}
|
||||
|
||||
@PostMapping("/inspection/object-imports/{jobId}/rollback")
|
||||
public ApiResponse<?> rollbackObjectImport(
|
||||
@PathVariable String jobId,
|
||||
@RequestParam(defaultValue = "user-dispatcher") String requestedBy
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.rollbackObjectImport(jobId, requestedBy));
|
||||
}
|
||||
|
||||
@GetMapping("/inspection/objects/{objectId}/versions")
|
||||
public ApiResponse<?> objectVersions(@PathVariable String objectId) {
|
||||
return ApiResponse.ok(Map.of("versions", dependencyFreeService.objectVersions(objectId)));
|
||||
}
|
||||
|
||||
@GetMapping("/business-calendars")
|
||||
public ApiResponse<?> businessCalendars() {
|
||||
return ApiResponse.ok(Map.of("calendars", dependencyFreeService.businessCalendars()));
|
||||
}
|
||||
|
||||
@PostMapping("/inspection/plans/{planId}/preview-runs")
|
||||
public ApiResponse<?> previewPlanRuns(
|
||||
@PathVariable String planId,
|
||||
@RequestBody(required = false) FoundationRequests.PreviewPlanRunsRequest request
|
||||
) {
|
||||
return ApiResponse.ok(Map.of("runs", dependencyFreeService.previewPlanRuns(
|
||||
planId,
|
||||
request == null || request.count() == null ? 6 : request.count()
|
||||
)));
|
||||
}
|
||||
|
||||
@GetMapping("/inspection/plans/{planId}/executions")
|
||||
public ApiResponse<?> planExecutions(@PathVariable String planId) {
|
||||
return ApiResponse.ok(Map.of("executions", dependencyFreeService.planExecutions(planId)));
|
||||
}
|
||||
|
||||
@PostMapping("/inspection/plans/run-due")
|
||||
public ApiResponse<?> runDuePlans(
|
||||
@RequestBody(required = false) FoundationRequests.RunDuePlansRequest request
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.runDuePlans(
|
||||
request == null || request.limit() == null ? 20 : request.limit()
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("/operation-resources/sync")
|
||||
public ApiResponse<?> syncOperationResources() {
|
||||
return ApiResponse.ok(dependencyFreeService.syncOperationResources());
|
||||
}
|
||||
|
||||
@GetMapping("/operation-resources/calendar")
|
||||
public ApiResponse<?> operationResourceCalendar(
|
||||
@RequestParam(required = false) String from,
|
||||
@RequestParam(required = false) String to
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.operationResourceCalendar(from, to));
|
||||
}
|
||||
|
||||
@PostMapping("/inspection/tasks/{taskId}/conflict-checks")
|
||||
public ApiResponse<?> checkTaskConflicts(
|
||||
@PathVariable String taskId,
|
||||
@RequestBody(required = false) FoundationRequests.ConflictCheckRequest request
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.checkTaskConflicts(
|
||||
taskId,
|
||||
request == null ? new FoundationRequests.ConflictCheckRequest(null, null, null, null, null, null) : request
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("/inspection/tasks/{taskId}/reservations")
|
||||
public ApiResponse<?> reserveTaskResources(
|
||||
@PathVariable String taskId,
|
||||
@Valid @RequestBody FoundationRequests.ReserveResourcesRequest request
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.reserveTaskResources(taskId, request));
|
||||
}
|
||||
|
||||
@DeleteMapping("/inspection/tasks/{taskId}/reservations/{reservationId}")
|
||||
public ApiResponse<?> cancelReservation(
|
||||
@PathVariable String taskId,
|
||||
@PathVariable String reservationId
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.cancelReservation(taskId, reservationId));
|
||||
}
|
||||
|
||||
@GetMapping("/gis/layers")
|
||||
public ApiResponse<?> gisLayers() {
|
||||
return ApiResponse.ok(Map.of("layers", dependencyFreeService.gisLayers()));
|
||||
}
|
||||
|
||||
@GetMapping("/gis/dataset-versions")
|
||||
public ApiResponse<?> gisDatasetVersions() {
|
||||
return ApiResponse.ok(Map.of("dataset_versions", dependencyFreeService.gisDatasetVersions()));
|
||||
}
|
||||
|
||||
@PostMapping("/gis/layers/{layerId}/publish")
|
||||
public ApiResponse<?> publishGisLayer(@PathVariable String layerId) {
|
||||
return ApiResponse.ok(dependencyFreeService.publishGisLayer(layerId));
|
||||
}
|
||||
|
||||
@GetMapping("/gis/layers/{layerCode}/features")
|
||||
public ApiResponse<?> queryGisLayerFeatures(@PathVariable String layerCode) {
|
||||
return ApiResponse.ok(dependencyFreeService.queryGisLayerFeatures(layerCode));
|
||||
}
|
||||
|
||||
@GetMapping("/route-generation-profiles")
|
||||
public ApiResponse<?> routeGenerationProfiles() {
|
||||
return ApiResponse.ok(Map.of("profiles", dependencyFreeService.routeGenerationProfiles()));
|
||||
}
|
||||
|
||||
@PostMapping("/routes/generate")
|
||||
public ApiResponse<?> generateRoute(
|
||||
@Valid @RequestBody FoundationRequests.GenerateRouteRequest request
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.generateRoute(request));
|
||||
}
|
||||
|
||||
@GetMapping("/route-action-schemas")
|
||||
public ApiResponse<?> routeActionSchemas() {
|
||||
return ApiResponse.ok(Map.of("schemas", dependencyFreeService.routeActionSchemas()));
|
||||
}
|
||||
|
||||
@PutMapping("/route-versions/{versionId}/waypoints")
|
||||
public ApiResponse<?> updateRouteWaypoints(
|
||||
@PathVariable String versionId,
|
||||
@Valid @RequestBody FoundationRequests.UpdateRouteWaypointsRequest request
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.updateRouteWaypoints(versionId, request));
|
||||
}
|
||||
|
||||
@GetMapping("/route-versions/{versionId}/editor-revisions")
|
||||
public ApiResponse<?> routeEditorRevisions(@PathVariable String versionId) {
|
||||
return ApiResponse.ok(Map.of("revisions", dependencyFreeService.routeEditorRevisions(versionId)));
|
||||
}
|
||||
|
||||
@GetMapping("/route-estimation-profiles")
|
||||
public ApiResponse<?> routeEstimationProfiles() {
|
||||
return ApiResponse.ok(Map.of("profiles", dependencyFreeService.routeEstimationProfiles()));
|
||||
}
|
||||
|
||||
@PostMapping("/route-versions/{versionId}/estimate")
|
||||
public ApiResponse<?> estimateRoute(
|
||||
@PathVariable String versionId,
|
||||
@RequestBody(required = false) FoundationRequests.EstimateRouteRequest request
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.estimateRoute(
|
||||
versionId,
|
||||
request == null ? new FoundationRequests.EstimateRouteRequest(null, null, null, null) : request
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/route-versions/{versionId}/estimations")
|
||||
public ApiResponse<?> routeEstimations(@PathVariable String versionId) {
|
||||
return ApiResponse.ok(Map.of("estimations", dependencyFreeService.routeEstimations(versionId)));
|
||||
}
|
||||
|
||||
@GetMapping("/spatial-rule-sets")
|
||||
public ApiResponse<?> spatialRuleSets() {
|
||||
return ApiResponse.ok(Map.of("rule_sets", dependencyFreeService.spatialRuleSets()));
|
||||
}
|
||||
|
||||
@PostMapping("/spatial-rule-sets")
|
||||
public ApiResponse<?> createSpatialRuleSet(
|
||||
@Valid @RequestBody FoundationRequests.CreateSpatialRuleSetRequest request
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.createSpatialRuleSet(request));
|
||||
}
|
||||
|
||||
@PostMapping("/spatial-rule-sets/{ruleSetId}/versions")
|
||||
public ApiResponse<?> createSpatialRuleVersion(
|
||||
@PathVariable String ruleSetId,
|
||||
@Valid @RequestBody FoundationRequests.CreateSpatialRuleVersionRequest request
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.createSpatialRuleVersion(ruleSetId, request));
|
||||
}
|
||||
|
||||
@PostMapping("/spatial-rule-set-versions/{versionId}/publish")
|
||||
public ApiResponse<?> publishSpatialRuleVersion(
|
||||
@PathVariable String versionId,
|
||||
@RequestParam(defaultValue = "user-approver") String approvedBy
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.publishSpatialRuleVersion(versionId, approvedBy));
|
||||
}
|
||||
|
||||
@GetMapping("/spatial-exemptions")
|
||||
public ApiResponse<?> spatialExemptions() {
|
||||
return ApiResponse.ok(Map.of("exemptions", dependencyFreeService.spatialExemptions()));
|
||||
}
|
||||
|
||||
@PostMapping("/spatial-exemptions")
|
||||
public ApiResponse<?> createSpatialExemption(
|
||||
@Valid @RequestBody FoundationRequests.CreateSpatialExemptionRequest request
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.createSpatialExemption(request));
|
||||
}
|
||||
|
||||
@PostMapping("/spatial-exemptions/{exemptionId}/approve")
|
||||
public ApiResponse<?> approveSpatialExemption(
|
||||
@PathVariable String exemptionId,
|
||||
@RequestParam(defaultValue = "user-approver") String approvedBy
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.approveSpatialExemption(exemptionId, approvedBy));
|
||||
}
|
||||
|
||||
@GetMapping("/geofences")
|
||||
public ApiResponse<?> geofences() {
|
||||
return ApiResponse.ok(Map.of("geofences", dependencyFreeService.geofences()));
|
||||
}
|
||||
|
||||
@PostMapping("/geofences")
|
||||
public ApiResponse<?> createGeofence(
|
||||
@Valid @RequestBody FoundationRequests.CreateGeofenceRequest request
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.createGeofence(request));
|
||||
}
|
||||
|
||||
@PostMapping("/spatial-rule-hits/explain")
|
||||
public ApiResponse<?> explainSpatialHit(
|
||||
@Valid @RequestBody FoundationRequests.ExplainSpatialHitRequest request
|
||||
) {
|
||||
return ApiResponse.ok(dependencyFreeService.explainSpatialHit(request));
|
||||
}
|
||||
}
|
||||
|
||||
+121
@@ -122,4 +122,125 @@ public final class FoundationRequests {
|
||||
String createdBy
|
||||
) {
|
||||
}
|
||||
|
||||
public record CreateObjectImportRequest(
|
||||
@NotBlank String fileName,
|
||||
String fileType,
|
||||
String mappingProfileId,
|
||||
String declaredCrs,
|
||||
@NotEmpty List<Map<String, Object>> rows,
|
||||
String createdBy
|
||||
) {
|
||||
}
|
||||
|
||||
public record PreviewPlanRunsRequest(
|
||||
Integer count
|
||||
) {
|
||||
}
|
||||
|
||||
public record RunDuePlansRequest(
|
||||
Integer limit
|
||||
) {
|
||||
}
|
||||
|
||||
public record ConflictCheckRequest(
|
||||
String startAt,
|
||||
String endAt,
|
||||
Integer durationMinutes,
|
||||
List<String> resourceIds,
|
||||
String routeVersionId,
|
||||
String requestedBy
|
||||
) {
|
||||
}
|
||||
|
||||
public record ReserveResourcesRequest(
|
||||
@NotEmpty List<String> resourceIds,
|
||||
String startAt,
|
||||
String endAt,
|
||||
Integer durationMinutes,
|
||||
String reservationType,
|
||||
@NotBlank String idempotencyKey,
|
||||
String requestedBy
|
||||
) {
|
||||
}
|
||||
|
||||
public record GenerateRouteRequest(
|
||||
@NotBlank String objectId,
|
||||
String generatorType,
|
||||
Map<String, Object> parameters,
|
||||
Boolean createRoute,
|
||||
String routeName,
|
||||
String createdBy
|
||||
) {
|
||||
}
|
||||
|
||||
public record UpdateRouteWaypointsRequest(
|
||||
@NotEmpty List<Map<String, Object>> waypoints,
|
||||
Map<String, Object> flightParameters,
|
||||
List<Map<String, Object>> payloadActions,
|
||||
String changeSummary,
|
||||
String editorId,
|
||||
Long expectedVersionLock
|
||||
) {
|
||||
}
|
||||
|
||||
public record EstimateRouteRequest(
|
||||
String profileId,
|
||||
Double cruiseSpeedMps,
|
||||
Double reservePercent,
|
||||
String createdBy
|
||||
) {
|
||||
}
|
||||
|
||||
public record CreateSpatialRuleSetRequest(
|
||||
@NotBlank String code,
|
||||
@NotBlank String name,
|
||||
@NotBlank String ruleType,
|
||||
String createdBy
|
||||
) {
|
||||
}
|
||||
|
||||
public record CreateSpatialRuleVersionRequest(
|
||||
List<Map<String, Object>> rules,
|
||||
String effectiveFrom,
|
||||
String effectiveTo,
|
||||
String createdBy
|
||||
) {
|
||||
}
|
||||
|
||||
public record CreateSpatialExemptionRequest(
|
||||
@NotBlank String exemptionType,
|
||||
@NotNull Map<String, Object> geometry,
|
||||
List<String> appliesToScenes,
|
||||
List<String> appliesToObjectTypes,
|
||||
@NotBlank String reason,
|
||||
@NotBlank String effectiveFrom,
|
||||
@NotBlank String effectiveTo,
|
||||
String createdBy
|
||||
) {
|
||||
}
|
||||
|
||||
public record CreateGeofenceRequest(
|
||||
@NotBlank String code,
|
||||
@NotBlank String name,
|
||||
@NotBlank String fenceType,
|
||||
@NotNull Map<String, Object> geometry,
|
||||
Double altitudeMinM,
|
||||
Double altitudeMaxM,
|
||||
@NotBlank String effectiveFrom,
|
||||
@NotBlank String effectiveTo,
|
||||
String severity,
|
||||
String datasetVersionId,
|
||||
String createdBy
|
||||
) {
|
||||
}
|
||||
|
||||
public record ExplainSpatialHitRequest(
|
||||
String taskId,
|
||||
String analysisResultId,
|
||||
@NotNull Map<String, Object> geometry,
|
||||
String scene,
|
||||
String objectType
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
+1408
File diff suppressed because it is too large
Load Diff
+1
@@ -825,6 +825,7 @@ public class FoundationCompletionService {
|
||||
rv.waypoints::text as waypoints,rv.flight_parameters::text as flight_parameters,
|
||||
rv.payload_actions::text as payload_actions,
|
||||
rv.validation_result::text as validation_result,rv.checksum,
|
||||
rv.estimation_result::text as estimation_result,rv.edited_at,rv.edited_by,
|
||||
rv.created_by,rv.approved_by,rv.submitted_at,rv.created_at,
|
||||
rv.published_at,rv.published_by,rv.retired_at,rv.rollback_target_version_id,
|
||||
rv.version_lock,
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.ai.trackwalker.foundation.service;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class PlanSchedulerJob {
|
||||
private static final Logger log = LoggerFactory.getLogger(PlanSchedulerJob.class);
|
||||
|
||||
private final DependencyFreeCompletionService service;
|
||||
|
||||
public PlanSchedulerJob(DependencyFreeCompletionService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelayString = "${rail.planning.scheduler-delay-ms:30000}")
|
||||
public void runDuePlans() {
|
||||
try {
|
||||
service.runDuePlans(20);
|
||||
} catch (Exception exception) {
|
||||
log.warn("Dependency-free plan scheduler pass failed: {}", exception.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,577 @@
|
||||
-- Dependency-free software closure for PF-01, PF-02, PF-05, PF-08, PF-09, PF-10, PF-11 and PF-15.
|
||||
-- Production use still requires approved railway master data, device matrices and spatial rule datasets.
|
||||
|
||||
alter table inspection_plans
|
||||
add column if not exists schedule_timezone varchar(64) not null default 'Asia/Shanghai',
|
||||
add column if not exists calendar_id varchar(64),
|
||||
add column if not exists missed_execution_policy varchar(32) not null default 'RUN_ONCE',
|
||||
add column if not exists max_catch_up_count integer not null default 1,
|
||||
add column if not exists effective_from timestamptz,
|
||||
add column if not exists effective_to timestamptz,
|
||||
add column if not exists last_run_at timestamptz,
|
||||
add column if not exists version_no bigint not null default 0;
|
||||
|
||||
alter table route_versions
|
||||
add column if not exists estimation_result jsonb not null default '{}'::jsonb,
|
||||
add column if not exists edited_at timestamptz,
|
||||
add column if not exists edited_by varchar(64);
|
||||
|
||||
create table if not exists inspection_object_mapping_profiles (
|
||||
id varchar(64) primary key,
|
||||
name varchar(128) not null,
|
||||
object_type varchar(32) not null,
|
||||
file_type varchar(32) not null,
|
||||
field_mapping jsonb not null default '{}'::jsonb,
|
||||
value_mapping jsonb not null default '{}'::jsonb,
|
||||
default_values jsonb not null default '{}'::jsonb,
|
||||
validation_profile jsonb not null default '{}'::jsonb,
|
||||
status varchar(32) not null,
|
||||
version_no bigint not null default 0,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null
|
||||
);
|
||||
|
||||
create table if not exists inspection_object_import_jobs (
|
||||
id varchar(64) primary key,
|
||||
file_name varchar(512) not null,
|
||||
file_type varchar(32) not null,
|
||||
object_key text,
|
||||
mapping_profile_id varchar(64) references inspection_object_mapping_profiles(id),
|
||||
declared_crs varchar(64) not null default 'EPSG:4326',
|
||||
status varchar(32) not null,
|
||||
total_count integer not null default 0,
|
||||
valid_count integer not null default 0,
|
||||
invalid_count integer not null default 0,
|
||||
warning_count integer not null default 0,
|
||||
dataset_version_id varchar(64),
|
||||
quality_report jsonb not null default '{}'::jsonb,
|
||||
created_by varchar(64) not null,
|
||||
confirmed_by varchar(64),
|
||||
confirmed_at timestamptz,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null
|
||||
);
|
||||
|
||||
create table if not exists inspection_object_import_rows (
|
||||
id varchar(64) primary key,
|
||||
job_id varchar(64) not null references inspection_object_import_jobs(id) on delete cascade,
|
||||
row_no integer not null,
|
||||
source_key varchar(256),
|
||||
raw_attributes jsonb not null default '{}'::jsonb,
|
||||
source_geometry jsonb,
|
||||
canonical_payload jsonb not null default '{}'::jsonb,
|
||||
canonical_geometry geometry(Geometry, 4326),
|
||||
validation_status varchar(32) not null,
|
||||
error_codes jsonb not null default '[]'::jsonb,
|
||||
warning_codes jsonb not null default '[]'::jsonb,
|
||||
imported_object_id varchar(64) references inspection_objects(id),
|
||||
unique(job_id, row_no)
|
||||
);
|
||||
|
||||
create table if not exists inspection_object_versions (
|
||||
id varchar(64) primary key,
|
||||
object_id varchar(64) not null references inspection_objects(id),
|
||||
version_no integer not null,
|
||||
snapshot jsonb not null,
|
||||
geom geometry(Geometry, 4326),
|
||||
change_type varchar(32) not null,
|
||||
change_reason text,
|
||||
source_job_id varchar(64) references inspection_object_import_jobs(id),
|
||||
created_by varchar(64) not null,
|
||||
created_at timestamptz not null,
|
||||
unique(object_id, version_no)
|
||||
);
|
||||
|
||||
create index if not exists idx_object_import_rows_job_status
|
||||
on inspection_object_import_rows(job_id, validation_status, row_no);
|
||||
create index if not exists idx_object_import_rows_geom
|
||||
on inspection_object_import_rows using gist(canonical_geometry);
|
||||
create index if not exists idx_object_versions_geom
|
||||
on inspection_object_versions using gist(geom);
|
||||
|
||||
create table if not exists business_calendars (
|
||||
id varchar(64) primary key,
|
||||
name varchar(128) not null,
|
||||
timezone varchar(64) not null,
|
||||
calendar_type varchar(32) not null,
|
||||
status varchar(32) not null,
|
||||
version_no bigint not null default 0,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null
|
||||
);
|
||||
|
||||
create table if not exists business_calendar_dates (
|
||||
calendar_id varchar(64) not null references business_calendars(id) on delete cascade,
|
||||
business_date date not null,
|
||||
date_type varchar(32) not null,
|
||||
labels jsonb not null default '[]'::jsonb,
|
||||
enabled boolean not null default true,
|
||||
primary key(calendar_id, business_date)
|
||||
);
|
||||
|
||||
create table if not exists plan_executions (
|
||||
id varchar(64) primary key,
|
||||
plan_id varchar(64) not null references inspection_plans(id),
|
||||
scheduled_window_start timestamptz not null,
|
||||
scheduled_window_end timestamptz not null,
|
||||
generation_key varchar(256) not null,
|
||||
trigger_source varchar(32) not null,
|
||||
status varchar(32) not null,
|
||||
generated_task_ids jsonb not null default '[]'::jsonb,
|
||||
skip_reason text,
|
||||
error_message text,
|
||||
started_at timestamptz not null,
|
||||
completed_at timestamptz,
|
||||
unique(plan_id, scheduled_window_start, generation_key)
|
||||
);
|
||||
|
||||
create table if not exists emergency_authorizations (
|
||||
id varchar(64) primary key,
|
||||
plan_id varchar(64) references inspection_plans(id),
|
||||
task_id varchar(64) references inspection_tasks(id),
|
||||
event_source varchar(128),
|
||||
authorized_by varchar(64) not null,
|
||||
authorized_at timestamptz not null,
|
||||
risk_confirmation text not null,
|
||||
attachment_refs jsonb not null default '[]'::jsonb,
|
||||
expires_at timestamptz not null,
|
||||
status varchar(32) not null
|
||||
);
|
||||
|
||||
create index if not exists idx_plan_executions_plan_time
|
||||
on plan_executions(plan_id, scheduled_window_start desc);
|
||||
create index if not exists idx_inspection_plans_due
|
||||
on inspection_plans(status, trigger_type, next_run_at);
|
||||
|
||||
create table if not exists operation_resources (
|
||||
id varchar(64) primary key,
|
||||
resource_type varchar(32) not null,
|
||||
source_id varchar(64),
|
||||
name varchar(128) not null,
|
||||
capabilities jsonb not null default '[]'::jsonb,
|
||||
status varchar(32) not null,
|
||||
owner_org_id varchar(64) references organizations(id),
|
||||
geom geometry(Geometry, 4326),
|
||||
health_snapshot jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null,
|
||||
unique(resource_type, source_id)
|
||||
);
|
||||
|
||||
create table if not exists resource_qualifications (
|
||||
id varchar(64) primary key,
|
||||
resource_id varchar(64) not null references operation_resources(id) on delete cascade,
|
||||
qualification_type varchar(64) not null,
|
||||
qualification_no varchar(128),
|
||||
valid_from timestamptz,
|
||||
valid_to timestamptz,
|
||||
scope jsonb not null default '{}'::jsonb,
|
||||
status varchar(32) not null
|
||||
);
|
||||
|
||||
create table if not exists resource_unavailable_windows (
|
||||
id varchar(64) primary key,
|
||||
resource_id varchar(64) not null references operation_resources(id) on delete cascade,
|
||||
start_at timestamptz not null,
|
||||
end_at timestamptz not null,
|
||||
reason_type varchar(64) not null,
|
||||
reason text,
|
||||
source varchar(64) not null,
|
||||
created_at timestamptz not null
|
||||
);
|
||||
|
||||
create table if not exists resource_reservations (
|
||||
id varchar(64) primary key,
|
||||
task_id varchar(64) not null references inspection_tasks(id),
|
||||
resource_id varchar(64) not null references operation_resources(id),
|
||||
start_at timestamptz not null,
|
||||
end_at timestamptz not null,
|
||||
status varchar(32) not null,
|
||||
reservation_type varchar(32) not null,
|
||||
idempotency_key varchar(128) not null unique,
|
||||
version_no bigint not null default 0,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null
|
||||
);
|
||||
|
||||
create table if not exists conflict_check_runs (
|
||||
id varchar(64) primary key,
|
||||
task_id varchar(64) not null references inspection_tasks(id),
|
||||
route_version_id varchar(64) references route_versions(id),
|
||||
input_snapshot jsonb not null,
|
||||
status varchar(32) not null,
|
||||
blocking_count integer not null default 0,
|
||||
warning_count integer not null default 0,
|
||||
result jsonb not null default '{}'::jsonb,
|
||||
rule_version varchar(64) not null,
|
||||
checked_at timestamptz not null
|
||||
);
|
||||
|
||||
create index if not exists idx_resource_reservations_resource_time
|
||||
on resource_reservations(resource_id, start_at, end_at, status);
|
||||
create index if not exists idx_operation_resources_geom
|
||||
on operation_resources using gist(geom);
|
||||
|
||||
create table if not exists gis_dataset_versions (
|
||||
id varchar(64) primary key,
|
||||
code varchar(64) not null,
|
||||
name varchar(128) not null,
|
||||
dataset_type varchar(32) not null,
|
||||
source_type varchar(32) not null,
|
||||
crs varchar(64) not null default 'EPSG:4326',
|
||||
quality_status varchar(32) not null,
|
||||
status varchar(32) not null,
|
||||
version_no integer not null,
|
||||
metadata jsonb not null default '{}'::jsonb,
|
||||
checksum varchar(64) not null,
|
||||
created_by varchar(64) not null,
|
||||
created_at timestamptz not null,
|
||||
published_at timestamptz,
|
||||
unique(code, version_no)
|
||||
);
|
||||
|
||||
create table if not exists gis_layers (
|
||||
id varchar(64) primary key,
|
||||
code varchar(64) not null unique,
|
||||
name varchar(128) not null,
|
||||
layer_type varchar(32) not null,
|
||||
dataset_version_id varchar(64) references gis_dataset_versions(id),
|
||||
style_spec jsonb not null default '{}'::jsonb,
|
||||
visible_by_default boolean not null default true,
|
||||
min_zoom numeric(5,2),
|
||||
max_zoom numeric(5,2),
|
||||
status varchar(32) not null,
|
||||
version_no bigint not null default 0,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null
|
||||
);
|
||||
|
||||
create table if not exists gis_spatial_features (
|
||||
id varchar(64) primary key,
|
||||
dataset_version_id varchar(64) not null references gis_dataset_versions(id) on delete cascade,
|
||||
feature_type varchar(32) not null,
|
||||
business_key varchar(128),
|
||||
name varchar(128),
|
||||
geom geometry(Geometry, 4326) not null,
|
||||
properties jsonb not null default '{}'::jsonb,
|
||||
status varchar(32) not null,
|
||||
created_at timestamptz not null
|
||||
);
|
||||
|
||||
create index if not exists idx_gis_spatial_features_geom
|
||||
on gis_spatial_features using gist(geom);
|
||||
create index if not exists idx_gis_spatial_features_dataset
|
||||
on gis_spatial_features(dataset_version_id, feature_type, status);
|
||||
|
||||
create table if not exists route_generation_profiles (
|
||||
id varchar(64) primary key,
|
||||
generator_type varchar(64) not null,
|
||||
object_type varchar(32) not null,
|
||||
name varchar(128) not null,
|
||||
parameters jsonb not null default '{}'::jsonb,
|
||||
status varchar(32) not null,
|
||||
version_no bigint not null default 0,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null,
|
||||
unique(generator_type, object_type)
|
||||
);
|
||||
|
||||
create table if not exists route_generation_runs (
|
||||
id varchar(64) primary key,
|
||||
object_id varchar(64) references inspection_objects(id),
|
||||
profile_id varchar(64) references route_generation_profiles(id),
|
||||
generator_type varchar(64) not null,
|
||||
parameters jsonb not null default '{}'::jsonb,
|
||||
input_snapshot jsonb not null default '{}'::jsonb,
|
||||
status varchar(32) not null,
|
||||
candidate_route_id varchar(64) references routes(id),
|
||||
candidate_version_id varchar(64) references route_versions(id),
|
||||
result jsonb not null default '{}'::jsonb,
|
||||
created_by varchar(64) not null,
|
||||
created_at timestamptz not null,
|
||||
completed_at timestamptz
|
||||
);
|
||||
|
||||
create table if not exists route_estimation_profiles (
|
||||
id varchar(64) primary key,
|
||||
code varchar(64) not null unique,
|
||||
name varchar(128) not null,
|
||||
device_type varchar(64) not null,
|
||||
payload_type varchar(64) not null,
|
||||
parameters jsonb not null default '{}'::jsonb,
|
||||
status varchar(32) not null,
|
||||
version_no bigint not null default 0,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null
|
||||
);
|
||||
|
||||
create table if not exists route_estimation_runs (
|
||||
id varchar(64) primary key,
|
||||
route_version_id varchar(64) not null references route_versions(id),
|
||||
profile_id varchar(64) references route_estimation_profiles(id),
|
||||
input_snapshot jsonb not null,
|
||||
status varchar(32) not null,
|
||||
result jsonb not null default '{}'::jsonb,
|
||||
confidence varchar(32) not null,
|
||||
created_by varchar(64) not null,
|
||||
created_at timestamptz not null,
|
||||
completed_at timestamptz
|
||||
);
|
||||
|
||||
create index if not exists idx_route_estimation_runs_version
|
||||
on route_estimation_runs(route_version_id, created_at desc);
|
||||
|
||||
create table if not exists route_editor_revisions (
|
||||
id varchar(64) primary key,
|
||||
route_version_id varchar(64) not null references route_versions(id) on delete cascade,
|
||||
revision_no integer not null,
|
||||
editor_id varchar(64) not null,
|
||||
change_summary text,
|
||||
before_waypoints jsonb not null default '[]'::jsonb,
|
||||
after_waypoints jsonb not null default '[]'::jsonb,
|
||||
before_actions jsonb not null default '[]'::jsonb,
|
||||
after_actions jsonb not null default '[]'::jsonb,
|
||||
checksum varchar(64) not null,
|
||||
created_at timestamptz not null,
|
||||
unique(route_version_id, revision_no)
|
||||
);
|
||||
|
||||
create table if not exists route_action_schemas (
|
||||
id varchar(64) primary key,
|
||||
device_type varchar(64) not null,
|
||||
action_type varchar(64) not null,
|
||||
schema_spec jsonb not null,
|
||||
status varchar(32) not null,
|
||||
version_no bigint not null default 0,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null,
|
||||
unique(device_type, action_type)
|
||||
);
|
||||
|
||||
create table if not exists spatial_rule_sets (
|
||||
id varchar(64) primary key,
|
||||
code varchar(64) not null unique,
|
||||
name varchar(128) not null,
|
||||
rule_type varchar(32) not null,
|
||||
status varchar(32) not null,
|
||||
current_version_id varchar(64),
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null
|
||||
);
|
||||
|
||||
create table if not exists spatial_rule_set_versions (
|
||||
id varchar(64) primary key,
|
||||
rule_set_id varchar(64) not null references spatial_rule_sets(id),
|
||||
version_no integer not null,
|
||||
effective_from timestamptz,
|
||||
effective_to timestamptz,
|
||||
rules jsonb not null default '[]'::jsonb,
|
||||
checksum varchar(64) not null,
|
||||
approval_status varchar(32) not null,
|
||||
status varchar(32) not null,
|
||||
created_by varchar(64) not null,
|
||||
created_at timestamptz not null,
|
||||
published_at timestamptz,
|
||||
unique(rule_set_id, version_no)
|
||||
);
|
||||
|
||||
create table if not exists spatial_baseline_features (
|
||||
id varchar(64) primary key,
|
||||
rule_version_id varchar(64) not null references spatial_rule_set_versions(id) on delete cascade,
|
||||
object_type varchar(32) not null,
|
||||
business_key varchar(128) not null,
|
||||
geom geometry(Geometry, 4326) not null,
|
||||
feature_signature varchar(128) not null,
|
||||
source_resource_id varchar(64),
|
||||
valid_from timestamptz,
|
||||
valid_to timestamptz,
|
||||
attributes jsonb not null default '{}'::jsonb,
|
||||
status varchar(32) not null
|
||||
);
|
||||
|
||||
create table if not exists spatial_exemptions (
|
||||
id varchar(64) primary key,
|
||||
exemption_type varchar(32) not null,
|
||||
geom geometry(Geometry, 4326) not null,
|
||||
applies_to_scenes jsonb not null default '[]'::jsonb,
|
||||
applies_to_object_types jsonb not null default '[]'::jsonb,
|
||||
reason text not null,
|
||||
effective_from timestamptz not null,
|
||||
effective_to timestamptz not null,
|
||||
approval_status varchar(32) not null,
|
||||
approved_by varchar(64),
|
||||
status varchar(32) not null,
|
||||
created_by varchar(64) not null,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null
|
||||
);
|
||||
|
||||
create table if not exists geofences (
|
||||
id varchar(64) primary key,
|
||||
code varchar(64) not null unique,
|
||||
name varchar(128) not null,
|
||||
fence_type varchar(32) not null,
|
||||
geom geometry(Geometry, 4326) not null,
|
||||
altitude_min_m numeric(10,2),
|
||||
altitude_max_m numeric(10,2),
|
||||
effective_from timestamptz not null,
|
||||
effective_to timestamptz not null,
|
||||
severity varchar(32) not null,
|
||||
dataset_version_id varchar(64) references gis_dataset_versions(id),
|
||||
status varchar(32) not null,
|
||||
created_by varchar(64) not null,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null
|
||||
);
|
||||
|
||||
create table if not exists spatial_rule_hits (
|
||||
id varchar(64) primary key,
|
||||
task_id varchar(64) references inspection_tasks(id),
|
||||
analysis_result_id varchar(64) references ai_results(id),
|
||||
rule_version_id varchar(64) references spatial_rule_set_versions(id),
|
||||
matched_object_id varchar(64),
|
||||
exemption_id varchar(64) references spatial_exemptions(id),
|
||||
decision varchar(32) not null,
|
||||
explanation jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null
|
||||
);
|
||||
|
||||
create index if not exists idx_spatial_baseline_features_geom
|
||||
on spatial_baseline_features using gist(geom);
|
||||
create index if not exists idx_spatial_exemptions_geom
|
||||
on spatial_exemptions using gist(geom);
|
||||
create index if not exists idx_geofences_geom
|
||||
on geofences using gist(geom);
|
||||
|
||||
insert into inspection_object_mapping_profiles(
|
||||
id,name,object_type,file_type,field_mapping,value_mapping,default_values,validation_profile,
|
||||
status,version_no,created_at,updated_at
|
||||
) values (
|
||||
'object-mapping-demo-line', 'DEMO 巡检对象 CSV 映射', 'RAILWAY', 'JSON_ROWS',
|
||||
'{"source_key":"source_key","name":"name","object_type":"object_type","line_id":"line_id","mileage_start":"mileage_start","mileage_end":"mileage_end","owner_org_id":"owner_org_id","risk_level":"risk_level"}'::jsonb,
|
||||
'{}'::jsonb,
|
||||
'{"source_type":"DEMO","source_version":"dependency-free-v21"}'::jsonb,
|
||||
'{"allowed_object_types":["RAILWAY","BRIDGE","TUNNEL","FLOOD","POWER"],"requires_geometry":true}'::jsonb,
|
||||
'ACTIVE', 1, now(), now()
|
||||
) on conflict (id) do nothing;
|
||||
|
||||
insert into business_calendars(id,name,timezone,calendar_type,status,version_no,created_at,updated_at) values
|
||||
('calendar-demo-standard', 'DEMO 标准工作日历', 'Asia/Shanghai', 'STANDARD', 'ACTIVE', 1, now(), now()),
|
||||
('calendar-demo-flood', 'DEMO 汛期加密巡检日历', 'Asia/Shanghai', 'SEASONAL', 'ACTIVE', 1, now(), now())
|
||||
on conflict (id) do nothing;
|
||||
|
||||
insert into operation_resources(
|
||||
id,resource_type,source_id,name,capabilities,status,owner_org_id,geom,health_snapshot,created_at,updated_at
|
||||
) values
|
||||
('opres-device-sim-01','AIRCRAFT','uav-sim-01','铁路巡检模拟机','["RTK","VISIBLE","THERMAL","MISSION"]'::jsonb,'AVAILABLE','org-works',ST_GeomFromText('POINT(116.090 39.095)',4326),'{"source":"DEMO","battery_percent":92}'::jsonb,now(),now()),
|
||||
('opres-dock-sim-01','DOCK','dock-sim-01','模拟无人值守机巢','["AUTO_CHARGE","WEATHER"]'::jsonb,'AVAILABLE','org-works',ST_GeomFromText('POINT(116.089 39.094)',4326),'{"source":"DEMO"}'::jsonb,now(),now()),
|
||||
('opres-pilot-demo','PILOT','user-dispatcher','DEMO 持证飞手','["VISUAL_OBSERVER","MISSION_COMMAND"]'::jsonb,'AVAILABLE','org-works',null,'{"source":"DEMO"}'::jsonb,now(),now())
|
||||
on conflict (resource_type, source_id) do update
|
||||
set capabilities=excluded.capabilities,
|
||||
status=excluded.status,
|
||||
health_snapshot=excluded.health_snapshot,
|
||||
updated_at=excluded.updated_at;
|
||||
|
||||
insert into resource_qualifications(
|
||||
id,resource_id,qualification_type,qualification_no,valid_from,valid_to,scope,status
|
||||
) values (
|
||||
'qual-pilot-demo-uav', 'opres-pilot-demo', 'UAV_OPERATOR', 'DEMO-UAV-2026',
|
||||
now() - interval '30 days', now() + interval '365 days', '{"line_ids":["line-demo"]}'::jsonb, 'ACTIVE'
|
||||
) on conflict (id) do nothing;
|
||||
|
||||
insert into gis_dataset_versions(
|
||||
id,code,name,dataset_type,source_type,crs,quality_status,status,version_no,metadata,checksum,created_by,created_at,published_at
|
||||
) values (
|
||||
'gis-dataset-demo-line-v1','DEMO_LINE_CENTER','DEMO 线路中心线','LINE','DEMO','EPSG:4326','PASSED','PUBLISHED',1,
|
||||
'{"line_id":"line-demo","accuracy":"DEMO"}'::jsonb,
|
||||
md5('gis-dataset-demo-line-v1'),'system',now(),now()
|
||||
) on conflict (id) do nothing;
|
||||
|
||||
insert into gis_layers(
|
||||
id,code,name,layer_type,dataset_version_id,style_spec,visible_by_default,min_zoom,max_zoom,status,version_no,created_at,updated_at
|
||||
) values (
|
||||
'gis-layer-demo-line','DEMO_LINE_CENTER','DEMO 线路中心线','LINE','gis-dataset-demo-line-v1',
|
||||
'{"stroke":"#17365d","stroke_width":3}'::jsonb,true,4,20,'PUBLISHED',1,now(),now()
|
||||
) on conflict (code) do nothing;
|
||||
|
||||
insert into gis_spatial_features(
|
||||
id,dataset_version_id,feature_type,business_key,name,geom,properties,status,created_at
|
||||
) values (
|
||||
'gis-feature-demo-line','gis-dataset-demo-line-v1','LINE','line-demo','试点线路中心线',
|
||||
ST_GeomFromText('LINESTRING(116.090 39.095,116.125 39.115,116.165 39.105)',4326),
|
||||
'{"source":"DEMO","line_id":"line-demo"}'::jsonb,'ACTIVE',now()
|
||||
) on conflict (id) do nothing;
|
||||
|
||||
insert into route_generation_profiles(
|
||||
id,generator_type,object_type,name,parameters,status,version_no,created_at,updated_at
|
||||
) values
|
||||
('route-gen-railway-parallel','RAILWAY_PARALLEL','RAILWAY','沿线平行巡检生成器','{"altitude_m":80,"speed_mps":7,"offset_m":20,"waypoint_count":5}'::jsonb,'ACTIVE',1,now(),now()),
|
||||
('route-gen-bridge-orbit','BRIDGE_ORBIT','BRIDGE','桥梁环绕巡检生成器','{"altitude_m":65,"speed_mps":4,"orbit_radius_m":35,"waypoint_count":6}'::jsonb,'ACTIVE',1,now(),now()),
|
||||
('route-gen-tunnel-portal','TUNNEL_PORTAL','TUNNEL','隧道洞口巡检生成器','{"altitude_m":55,"speed_mps":4,"waypoint_count":4}'::jsonb,'ACTIVE',1,now(),now()),
|
||||
('route-gen-flood-corridor','FLOOD_CORRIDOR','FLOOD','防洪走廊巡检生成器','{"altitude_m":75,"speed_mps":6,"waypoint_count":6}'::jsonb,'ACTIVE',1,now(),now()),
|
||||
('route-gen-power-follow','POWER_FOLLOW','POWER','电力部件跟拍生成器','{"altitude_m":70,"speed_mps":5,"waypoint_count":5}'::jsonb,'ACTIVE',1,now(),now())
|
||||
on conflict (generator_type, object_type) do nothing;
|
||||
|
||||
insert into route_estimation_profiles(
|
||||
id,code,name,device_type,payload_type,parameters,status,version_no,created_at,updated_at
|
||||
) values (
|
||||
'route-estimate-demo-matrice4d','DEMO_MATRICE4D_VISIBLE','DEMO Matrice 4D 可见光估算','MATRICE_4D','VISIBLE',
|
||||
'{"cruise_speed_mps":7,"battery_capacity_wh":180,"power_w":650,"reserve_percent":25,"photo_interval_s":2,"photo_size_mb":8,"storage_reserve_percent":20}'::jsonb,
|
||||
'ACTIVE',1,now(),now()
|
||||
) on conflict (code) do nothing;
|
||||
|
||||
insert into route_action_schemas(
|
||||
id,device_type,action_type,schema_spec,status,version_no,created_at,updated_at
|
||||
) values
|
||||
('action-schema-visible-photo','GENERIC_UAV','TAKE_PHOTO','{"required":["interval_s"],"properties":{"interval_s":{"type":"number","minimum":1},"gimbal_pitch_deg":{"type":"number","minimum":-90,"maximum":30}}}'::jsonb,'ACTIVE',1,now(),now()),
|
||||
('action-schema-thermal-photo','GENERIC_UAV','THERMAL_CAPTURE','{"required":["interval_s"],"properties":{"interval_s":{"type":"number","minimum":1},"emissivity":{"type":"number","minimum":0.1,"maximum":1}}}'::jsonb,'ACTIVE',1,now(),now())
|
||||
on conflict (device_type, action_type) do nothing;
|
||||
|
||||
insert into spatial_rule_sets(id,code,name,rule_type,status,current_version_id,created_at,updated_at) values (
|
||||
'spatial-rules-demo-safety','DEMO_SPATIAL_SAFETY','DEMO 空间安全规则','BASELINE_EXEMPTION','ACTIVE','spatial-rules-demo-safety-v1',now(),now()
|
||||
) on conflict (code) do nothing;
|
||||
|
||||
insert into spatial_rule_set_versions(
|
||||
id,rule_set_id,version_no,effective_from,effective_to,rules,checksum,approval_status,status,created_by,created_at,published_at
|
||||
) values (
|
||||
'spatial-rules-demo-safety-v1','spatial-rules-demo-safety',1,now() - interval '1 day',now() + interval '365 days',
|
||||
'[{"code":"DEMO_GEOFENCE_INTERSECTION","severity":"WARNING","description":"命中演示地理围栏时提示复核"},{"code":"DEMO_BASELINE_DISTANCE","severity":"INFO","description":"与基线对象保持可解释距离"}]'::jsonb,
|
||||
md5('spatial-rules-demo-safety-v1'),'APPROVED','PUBLISHED','system',now(),now()
|
||||
) on conflict (id) do nothing;
|
||||
|
||||
insert into spatial_exemptions(
|
||||
id,exemption_type,geom,applies_to_scenes,applies_to_object_types,reason,effective_from,effective_to,
|
||||
approval_status,approved_by,status,created_by,created_at,updated_at
|
||||
) values (
|
||||
'spatial-exemption-demo-maintenance','MAINTENANCE_WINDOW',
|
||||
ST_GeomFromText('POLYGON((116.121 39.111,116.134 39.111,116.134 39.118,116.121 39.118,116.121 39.111))',4326),
|
||||
'["桥梁裂缝","异物侵限"]'::jsonb,'["BRIDGE","RAILWAY"]'::jsonb,'DEMO 施工维护窗口',
|
||||
now() - interval '1 day',now() + interval '30 days','APPROVED','user-approver','ACTIVE','system',now(),now()
|
||||
) on conflict (id) do nothing;
|
||||
|
||||
insert into geofences(
|
||||
id,code,name,fence_type,geom,altitude_min_m,altitude_max_m,effective_from,effective_to,severity,
|
||||
dataset_version_id,status,created_by,created_at,updated_at
|
||||
) values (
|
||||
'geofence-demo-construction','DEMO_CONSTRUCTION_ZONE','DEMO 施工警示区','WARNING',
|
||||
ST_GeomFromText('POLYGON((116.145 39.101,116.152 39.101,116.152 39.108,116.145 39.108,116.145 39.101))',4326),
|
||||
0,120,now() - interval '1 day',now() + interval '90 days','WARNING','gis-dataset-demo-line-v1','ACTIVE','system',now(),now()
|
||||
) on conflict (code) do nothing;
|
||||
|
||||
insert into capability_completion_status(
|
||||
capability_code, capability_name, stage, status, implementation_ref, blocker_type, blocker_reason, updated_at
|
||||
) values
|
||||
('PF-01', '巡检对象台账与空间导入', 'M1', 'PARTIAL', 'inspection_object_import_jobs, inspection_object_versions', 'PRODUCTION_MASTER_DATA', '软件导入闭环已具备,生产验收需真实字段字典、责任单位和坐标说明', now()),
|
||||
('PF-02', '日常、专项、应急计划调度', 'M1', 'PARTIAL', 'business_calendars, plan_executions, PlanSchedulerJob', 'BUSINESS_CALENDAR', '调度与补跑闭环已具备,生产验收需正式汛期、天窗和授权规则', now()),
|
||||
('PF-05', '设备、航线和作业资源冲突检查', 'M1-M2', 'PARTIAL', 'operation_resources, conflict_check_runs, resource_reservations', 'RESOURCE_MASTER_DATA', '资源日历和冲突引擎已具备,生产验收需真实设备、机巢、载荷和飞手资质', now()),
|
||||
('PF-08', 'GIS 图层与空间数据集管理', 'M1', 'PARTIAL', 'gis_dataset_versions, gis_layers, gis_spatial_features', 'AUTHORITATIVE_GIS_DATA', '图层版本和查询闭环已具备,生产验收需权威线路设施图层', now()),
|
||||
('PF-09', '五类对象候选航线自动生成', 'M1-M3', 'PARTIAL', 'route_generation_profiles, route_generation_runs', 'FIELD_ROUTE_PARAMETERS', '五类生成器可产出候选航线,生产验收需现场安全参数和复核', now()),
|
||||
('PF-10', '航线地图编辑与动作 Schema', 'M1', 'PARTIAL', 'route_editor_revisions, route_action_schemas', 'DEVICE_ACTION_MATRIX', '航点编辑、审计和通用动作 Schema 已具备,生产验收需厂商设备动作矩阵', now()),
|
||||
('PF-11', '航程、电量、时长和容量估算', 'M1-M2', 'PARTIAL', 'route_estimation_profiles, route_estimation_runs', 'DEVICE_CALIBRATION', '估算闭环已具备,生产验收需真机能耗和载荷容量校准', now()),
|
||||
('PF-15', '基线、豁免区和地理围栏', 'M2', 'PARTIAL', 'spatial_rule_sets, spatial_exemptions, geofences, spatial_rule_hits', 'SPATIAL_RULE_DATA', '规则版本、豁免和围栏管理已具备,生产验收需正式基线和审批规则', now())
|
||||
on conflict (capability_code) do update
|
||||
set status=excluded.status,
|
||||
implementation_ref=excluded.implementation_ref,
|
||||
blocker_type=excluded.blocker_type,
|
||||
blocker_reason=excluded.blocker_reason,
|
||||
updated_at=excluded.updated_at;
|
||||
Reference in New Issue
Block a user