151 lines
6.4 KiB
Vue
151 lines
6.4 KiB
Vue
<template>
|
||
<div v-if="project">
|
||
<a-page-header class="ph" @back="router.push('/home')">
|
||
<template #title><a style="color: #b4232a">返回首页</a> / 预测项目配置</template>
|
||
</a-page-header>
|
||
|
||
<!-- 项目信息 -->
|
||
<a-card class="card">
|
||
<div class="proj-head">
|
||
<div>
|
||
<span class="proj-name">{{ project.name }}</span>
|
||
<span class="proj-id">项目ID:{{ project.id }}</span>
|
||
<a-tag :color="project.status === 'report_generated' ? 'green' : 'orange'">
|
||
{{ project.status === 'report_generated' ? '已生成预测报告' : '未生成预测报告' }}
|
||
</a-tag>
|
||
</div>
|
||
<div class="times">
|
||
创建时间:{{ fmt(project.createdAt) }} 最近更新:{{ fmt(project.updatedAt) }}
|
||
</div>
|
||
</div>
|
||
<div class="proj-meta">
|
||
<span>项目类型:{{ project.type }}</span>
|
||
<span>所在城市:{{ project.province }}/{{ project.city }}</span>
|
||
<span>建筑面积:{{ project.area }}m²</span>
|
||
<span>预测评级:<a-tag v-if="project.rating" :color="ratingColor(project.rating)">{{ project.rating }}</a-tag><span v-else>-</span></span>
|
||
<a class="edit" @click="editOpen = true">✎ 修改项目信息</a>
|
||
</div>
|
||
</a-card>
|
||
|
||
<!-- 空间 -->
|
||
<a-card class="card">
|
||
<div class="spaces-head">
|
||
<span class="title">包含空间 ({{ project.spaces.length }})</span>
|
||
<a-button type="link" @click="openAddSpace">+ 添加包含空间</a-button>
|
||
</div>
|
||
<a-table :columns="spaceColumns" :data-source="project.spaces" row-key="id" size="middle" :pagination="false">
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'ventilationRate'">{{ record.ventilationRate }}次/小时</template>
|
||
<template v-else-if="column.key === 'matCount'">{{ record.materials.length }}</template>
|
||
<template v-else-if="column.key?.startsWith('conc_')">
|
||
<span :class="{ over: isOver(record, column.key.slice(5)) }">
|
||
{{ concText(record, column.key.slice(5)) }}
|
||
</span>
|
||
</template>
|
||
<template v-else-if="column.key === 'op'">
|
||
<a @click="openEditSpace(record)">编辑</a>
|
||
<a-divider type="vertical" />
|
||
<a-popconfirm title="确认删除该空间?" @confirm="onDeleteSpace(record)">
|
||
<a style="color: #b4232a">删除</a>
|
||
</a-popconfirm>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</a-card>
|
||
|
||
<div class="actions">
|
||
<a-button type="primary" size="large" :loading="generating" @click="onGenerate">生成预测报告</a-button>
|
||
</div>
|
||
|
||
<NewProjectModal :open="editOpen" :project="project" @ok="onEdited" @cancel="editOpen = false" />
|
||
<SpaceDrawer
|
||
:open="drawerOpen"
|
||
:project-id="project.id"
|
||
:space="editingSpace"
|
||
@ok="onSpaceSaved"
|
||
@cancel="drawerOpen = false"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { onMounted, ref } from 'vue';
|
||
import { useRoute, useRouter } from 'vue-router';
|
||
import { message } from 'ant-design-vue';
|
||
import { POLLUTANTS, POLLUTANT_LABELS, type Pollutant } from '@airpredict/shared';
|
||
import { getProject, generateReport, type ProjectDetail, type SpaceRow } from '../api/projects';
|
||
import { deleteSpace } from '../api/spaces';
|
||
import NewProjectModal from '../components/NewProjectModal.vue';
|
||
import SpaceDrawer from '../components/SpaceDrawer.vue';
|
||
|
||
const route = useRoute();
|
||
const router = useRouter();
|
||
const project = ref<ProjectDetail | null>(null);
|
||
const editOpen = ref(false);
|
||
const drawerOpen = ref(false);
|
||
const editingSpace = ref<SpaceRow | null>(null);
|
||
const generating = ref(false);
|
||
|
||
const spaceColumns = [
|
||
{ title: '空间ID', dataIndex: 'id' },
|
||
{ title: '空间名称', dataIndex: 'name' },
|
||
{ title: '空间类型', dataIndex: 'type' },
|
||
{ title: '面积', dataIndex: 'area', customRender: ({ text }: any) => `${text}m²` },
|
||
{ title: '温度', dataIndex: 'temperature', customRender: ({ text }: any) => `${text}℃` },
|
||
{ title: '湿度', dataIndex: 'humidity', customRender: ({ text }: any) => `${text}%rh` },
|
||
{ title: '通风换气率', key: 'ventilationRate' },
|
||
{ title: '材料数', key: 'matCount' },
|
||
{ title: '限值标准', dataIndex: 'standard' },
|
||
...POLLUTANTS.map((p) => ({ title: `${POLLUTANT_LABELS[p].zh}预测浓度`, key: `conc_${p}` })),
|
||
{ title: '操作', key: 'op', width: 130 },
|
||
];
|
||
|
||
function fmt(s?: string) { return s ? new Date(s).toLocaleString() : '-'; }
|
||
function ratingColor(r: string) { return { A: 'green', B: 'blue', C: 'orange', D: 'red' }[r] || 'default'; }
|
||
function concText(record: SpaceRow, pol: string) {
|
||
const v = record.predictedConc?.[pol as Pollutant];
|
||
return v == null ? '-' : v.toFixed(4) + 'mg/m³';
|
||
}
|
||
function isOver(record: SpaceRow, pol: string) {
|
||
return false; // 颜色提示在报告页更完整,此处仅展示数值
|
||
}
|
||
|
||
async function load() {
|
||
project.value = await getProject(route.params.id as string);
|
||
}
|
||
|
||
function openAddSpace() { editingSpace.value = null; drawerOpen.value = true; }
|
||
function openEditSpace(s: SpaceRow) { editingSpace.value = s; drawerOpen.value = true; }
|
||
function onSpaceSaved() { drawerOpen.value = false; load(); }
|
||
function onEdited() { editOpen.value = false; load(); }
|
||
async function onDeleteSpace(s: SpaceRow) { await deleteSpace(s.id); message.success('已删除'); load(); }
|
||
|
||
async function onGenerate() {
|
||
if (!project.value?.spaces.length) return message.warning('请先添加空间');
|
||
generating.value = true;
|
||
try {
|
||
project.value = await generateReport(project.value.id);
|
||
message.success(`报告已生成,项目评级 ${project.value.rating}`);
|
||
} finally {
|
||
generating.value = false;
|
||
}
|
||
}
|
||
|
||
onMounted(load);
|
||
</script>
|
||
|
||
<style scoped>
|
||
.ph { padding: 8px 0; }
|
||
.card { margin-bottom: 16px; }
|
||
.proj-head { display: flex; justify-content: space-between; align-items: center; }
|
||
.proj-name { font-size: 18px; font-weight: 700; margin-right: 16px; }
|
||
.proj-id { color: #888; margin-right: 12px; }
|
||
.times { color: #999; font-size: 13px; }
|
||
.proj-meta { margin-top: 12px; display: flex; gap: 24px; align-items: center; color: #555; }
|
||
.proj-meta .edit { margin-left: auto; color: #b4232a; cursor: pointer; }
|
||
.spaces-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
|
||
.spaces-head .title { font-weight: 600; }
|
||
.actions { text-align: center; padding: 24px 0; }
|
||
.over { color: #b4232a; font-weight: 600; }
|
||
</style>
|