airpredict/apps/web/src/pages/ProjectConfig.vue

151 lines
6.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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) }} &nbsp; 最近更新:{{ 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}` },
{ 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>