diff --git a/apps/api/src/projects/projects.controller.ts b/apps/api/src/projects/projects.controller.ts index 6598aef..e7205bb 100644 --- a/apps/api/src/projects/projects.controller.ts +++ b/apps/api/src/projects/projects.controller.ts @@ -50,4 +50,9 @@ export class ProjectsController { generate(@CurrentOrg() org: OrgPayload, @Param('id') id: string) { return this.projects.generate(org.id, id); } + + @Post(':id/duplicate') + duplicate(@CurrentOrg() org: OrgPayload, @Param('id') id: string) { + return this.projects.duplicate(org.id, id); + } } diff --git a/apps/api/src/projects/projects.service.ts b/apps/api/src/projects/projects.service.ts index 071b186..97cc6af 100644 --- a/apps/api/src/projects/projects.service.ts +++ b/apps/api/src/projects/projects.service.ts @@ -63,6 +63,51 @@ export class ProjectsService { }); } + /** 复用:把任意自有项目(或模板)复制成一个新草稿 */ + async duplicate(orgId: string, id: string) { + const src = await this.prisma.project.findUnique({ + where: { id }, + include: { spaces: { include: { materials: true } } }, + }); + if (!src) throw new NotFoundException('项目不存在'); + if (src.ownerOrgId !== orgId && !src.isPublic) throw new ForbiddenException('无权复用'); + + return this.prisma.project.create({ + data: { + id: this.genId('P'), + name: src.name + ' (复用)', + type: src.type, + province: src.province, + city: src.city, + area: src.area, + status: 'configuring', + ownerOrgId: orgId, + spaces: { + create: src.spaces.map((s) => ({ + id: this.genId('S'), + name: s.name, + type: s.type, + layout: s.layout, + height: s.height, + area: s.area, + volume: s.volume, + temperature: s.temperature, + humidity: s.humidity, + ventilationRate: s.ventilationRate, + standard: s.standard, + materials: { + create: s.materials.map((m) => ({ + materialId: m.materialId, + usageUnit: m.usageUnit, + usageAmount: m.usageAmount, + })), + }, + })), + }, + }, + }); + } + async list(orgId: string, q: QueryProjectsDto) { const where: Prisma.ProjectWhereInput = { ownerOrgId: orgId, isTemplate: false }; if (q.id) where.id = { contains: q.id, mode: 'insensitive' }; diff --git a/apps/web/src/api/projects.ts b/apps/web/src/api/projects.ts index 726a3a2..999e6ff 100644 --- a/apps/web/src/api/projects.ts +++ b/apps/web/src/api/projects.ts @@ -82,6 +82,9 @@ export function deleteProject(id: string) { export function generateReport(id: string) { return http.post(`/projects/${id}/generate`, {}); } -export function listProjects(params: { status?: string; unfinished?: string; page?: number; pageSize?: number }) { +export function listProjects(params: { status?: string; unfinished?: string; name?: string; type?: string; rating?: string; page?: number; pageSize?: number }) { return http.get>('/projects', { params }); } +export function duplicateProject(id: string) { + return http.post(`/projects/${id}/duplicate`, {}); +} diff --git a/apps/web/src/components/ImportProjectModal.vue b/apps/web/src/components/ImportProjectModal.vue new file mode 100644 index 0000000..b54029d --- /dev/null +++ b/apps/web/src/components/ImportProjectModal.vue @@ -0,0 +1,70 @@ + + + diff --git a/apps/web/src/pages/History.vue b/apps/web/src/pages/History.vue index 5ee11fd..cfba149 100644 --- a/apps/web/src/pages/History.vue +++ b/apps/web/src/pages/History.vue @@ -1,7 +1,102 @@ - + + + diff --git a/apps/web/src/pages/Home.vue b/apps/web/src/pages/Home.vue index 1d0444a..6393c75 100644 --- a/apps/web/src/pages/Home.vue +++ b/apps/web/src/pages/Home.vue @@ -22,6 +22,7 @@ + @@ -31,17 +32,18 @@ import { useRouter } from 'vue-router'; import { message } from 'ant-design-vue'; import { useAuthStore } from '../stores/auth'; import NewProjectModal from '../components/NewProjectModal.vue'; +import ImportProjectModal from '../components/ImportProjectModal.vue'; import type { ProjectDetail } from '../api/projects'; const router = useRouter(); const auth = useAuthStore(); -const todo = () => message.info('该功能将在后续阶段实现'); const createOpen = ref(false); +const importOpen = ref(false); const predictCards = [ { title: '新建项目预测', desc: '从头配置项目、空间、材料进行预测', action: () => (createOpen.value = true) }, - { title: '快速导入项目', desc: '根据模板或文件导入后调整配置预测', action: () => router.push({ name: 'template' }) }, + { title: '快速导入项目', desc: '根据模板导入后调整配置预测', action: () => (importOpen.value = true) }, { title: '继续配置预测', desc: '继续已保存、未提交的配置', action: () => router.push({ name: 'drafts' }) }, { title: '污染源识别 · 快速溯源', desc: '单空间快速预测,溯源主要污染材料', action: () => router.push('/source') }, ]; @@ -55,6 +57,10 @@ function onCreated(p: ProjectDetail) { createOpen.value = false; router.push({ name: 'predict', params: { id: p.id } }); } +function onImported(id: string) { + importOpen.value = false; + router.push({ name: 'predict', params: { id } }); +} diff --git a/apps/web/src/router/index.ts b/apps/web/src/router/index.ts index 00ca2bc..bd6fe1f 100644 --- a/apps/web/src/router/index.ts +++ b/apps/web/src/router/index.ts @@ -15,6 +15,7 @@ const routes = [ { path: 'history-project', name: 'history', component: () => import('../pages/History.vue') }, { path: 'drafts', name: 'drafts', component: () => import('../pages/Drafts.vue') }, { path: 'predict/:id', name: 'predict', component: () => import('../pages/ProjectConfig.vue') }, + { path: 'report/:id', name: 'report', component: () => import('../pages/Report.vue') }, ], }, ];