2513 lines
132 KiB
HTML
2513 lines
132 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>{{ project.name }} · 标伙伴</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||
<link rel="stylesheet" href="/static/style.css">
|
||
<style>
|
||
[x-cloak]{display:none!important}
|
||
body{font-family:'PingFang SC','Microsoft YaHei',sans-serif;background:#f0f4f8}
|
||
.tab-btn{@apply px-4 py-2 text-sm font-medium rounded-lg transition}
|
||
.prose-content{white-space:pre-wrap;line-height:1.9;font-size:14px}
|
||
.section-item:hover .section-actions{opacity:1}
|
||
.section-actions{opacity:0;transition:opacity .15s}
|
||
.sidebar-fixed{height:calc(100vh - 4rem);position:sticky;top:4rem;overflow-y:auto}
|
||
</style>
|
||
</head>
|
||
<body class="min-h-screen" x-data="projectApp({{ project.id }})" x-init="init()">
|
||
|
||
<!-- ── 顶栏 ── -->
|
||
<header class="bg-white border-b border-gray-200 sticky top-0 z-50 shadow-sm h-16 flex items-center px-6">
|
||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||
<a href="/" class="p-1.5 hover:bg-gray-100 rounded-lg transition text-gray-500">
|
||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||
</svg>
|
||
</a>
|
||
<div class="w-px h-5 bg-gray-200"></div>
|
||
<div class="w-7 h-7 rounded-lg bg-gradient-to-br from-blue-600 to-indigo-600 flex items-center justify-center flex-shrink-0">
|
||
<svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||
</svg>
|
||
</div>
|
||
<h1 class="text-base font-bold text-gray-900 truncate">{{ project.name }}</h1>
|
||
</div>
|
||
<!-- 步骤指示 -->
|
||
<div class="hidden md:flex items-center gap-1 mx-4">
|
||
<template x-for="(step, i) in steps" :key="i">
|
||
<div class="flex items-center">
|
||
<button @click="activeTab=step.key"
|
||
class="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition"
|
||
:class="activeTab===step.key ? 'bg-blue-600 text-white shadow-sm' : 'text-gray-500 hover:bg-gray-100'">
|
||
<span x-text="i+1" class="w-4 h-4 rounded-full flex items-center justify-center text-xs"
|
||
:class="activeTab===step.key ? 'bg-white/20' : 'bg-gray-200'"></span>
|
||
<span x-text="step.label"></span>
|
||
</button>
|
||
<div x-show="i < steps.length-1" class="w-4 h-px bg-gray-200 mx-1"></div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
<!-- 导出按钮 -->
|
||
<button @click="exportDoc()" :disabled="exporting"
|
||
class="flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-lg shadow-sm transition disabled:opacity-60 flex-shrink-0">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||
</svg>
|
||
<span x-show="!exporting">导出 Word</span>
|
||
<span x-show="exporting">导出中...</span>
|
||
</button>
|
||
</header>
|
||
|
||
<!-- ── 主体布局 ── -->
|
||
<div class="flex max-w-full">
|
||
|
||
<!-- 左侧:章节树(仅在生成大纲后显示) -->
|
||
<aside x-show="sections.length > 0" class="sidebar-fixed w-72 flex-shrink-0 bg-white border-r border-gray-200 px-3 py-4 hidden lg:block">
|
||
<div class="flex items-center justify-between mb-2 px-1">
|
||
<span class="text-xs font-semibold text-gray-500 uppercase tracking-wider">标书章节</span>
|
||
<div class="flex items-center gap-1.5">
|
||
<span x-show="progress.running > 0"
|
||
class="flex items-center gap-1 text-xs text-purple-600 font-medium">
|
||
<span class="relative flex h-2 w-2">
|
||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-purple-400 opacity-75"></span>
|
||
<span class="relative inline-flex rounded-full h-2 w-2 bg-purple-600"></span>
|
||
</span>
|
||
<span x-text="progress.running + '路'"></span>
|
||
</span>
|
||
<span class="text-xs text-gray-400" x-text="`${progress.done}/${progress.total}`"></span>
|
||
</div>
|
||
</div>
|
||
<div x-show="progress.total > 0" class="mb-3 px-1">
|
||
<div class="h-1.5 bg-gray-100 rounded-full overflow-hidden">
|
||
<div class="h-full rounded-full transition-all duration-500"
|
||
:class="progress.running > 0 ? 'bg-purple-500' : progress.percent===100 ? 'bg-green-500' : 'bg-blue-500'"
|
||
:style="'width:' + progress.percent + '%'"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="space-y-0.5">
|
||
<template x-for="s in sections" :key="s.id">
|
||
<div class="section-item flex items-start gap-2 px-2 py-1.5 rounded-lg hover:bg-gray-50 cursor-pointer group"
|
||
:class="selectedSection && selectedSection.id===s.id ? 'bg-blue-50' : ''"
|
||
@click="selectSection(s)">
|
||
<!-- 缩进 -->
|
||
<div class="flex-shrink-0 mt-0.5" :style="'width:' + (s.level-1)*12 + 'px'"></div>
|
||
<!-- 状态图标 -->
|
||
<div class="flex-shrink-0 w-4 h-4 mt-0.5">
|
||
<svg x-show="s.status==='done'" class="w-4 h-4 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/>
|
||
</svg>
|
||
<div x-show="s.status==='generating'" class="w-3.5 h-3.5 border-2 border-blue-300 border-t-blue-600 rounded-full animate-spin m-0.5"></div>
|
||
<svg x-show="s.status==='error'" class="w-4 h-4 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"/>
|
||
</svg>
|
||
<div x-show="!['done','generating','error'].includes(s.status)"
|
||
class="w-3 h-3 rounded-full border border-gray-300 m-0.5"></div>
|
||
</div>
|
||
<span class="text-xs leading-5 flex-1 min-w-0 break-words"
|
||
:class="[s.level===1 ? 'font-semibold text-gray-800' : s.level===2 ? 'font-medium text-gray-700' : 'text-gray-600',
|
||
selectedSection && selectedSection.id===s.id ? 'text-blue-700' : '']"
|
||
x-text="s.title"></span>
|
||
<!-- 生成按钮 -->
|
||
<button x-show="s.is_leaf" class="section-actions flex-shrink-0 text-blue-500 hover:text-blue-700"
|
||
@click.stop="generateSection(s.id)" :disabled="s.status==='generating'" title="生成此章节">
|
||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- 右侧:内容区 -->
|
||
<main class="flex-1 min-w-0 p-6">
|
||
|
||
<!-- ═══ TAB 1:招标分析 ═══ -->
|
||
<div x-show="activeTab==='parse'" class="space-y-5">
|
||
|
||
<!-- 上传区 -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
|
||
<h2 class="text-base font-bold text-gray-800 mb-4 flex items-center gap-2">
|
||
<span class="w-6 h-6 bg-blue-100 text-blue-600 rounded-md flex items-center justify-center text-xs font-bold">1</span>
|
||
上传招标文件
|
||
</h2>
|
||
<div class="border-2 border-dashed rounded-xl p-8 text-center transition"
|
||
:class="dragover ? 'border-blue-400 bg-blue-50' : 'border-gray-200 hover:border-gray-300'"
|
||
@dragover.prevent="dragover=true" @dragleave="dragover=false"
|
||
@drop.prevent="onDrop($event)">
|
||
<input type="file" id="fileInput" class="hidden" accept=".pdf,.doc,.docx" @change="onFileSelect($event)">
|
||
<svg class="w-10 h-10 text-gray-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"/>
|
||
</svg>
|
||
<p class="text-sm text-gray-500 mb-1">拖拽文件到此处,或</p>
|
||
<button class="text-blue-600 text-sm font-medium hover:underline" @click="$el.closest('.border-dashed').querySelector('#fileInput').click()">
|
||
点击选择文件
|
||
</button>
|
||
<p class="text-xs text-gray-400 mt-2">支持 PDF、DOC、DOCX,最大 50MB</p>
|
||
</div>
|
||
|
||
<!-- 已上传文件 -->
|
||
<div x-show="project.file_name" class="mt-4 flex items-center justify-between p-3 bg-gray-50 rounded-xl">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
|
||
<svg class="w-4 h-4 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<p class="text-sm font-medium text-gray-800" x-text="project.file_name"></p>
|
||
<p class="text-xs text-gray-400">已上传</p>
|
||
</div>
|
||
</div>
|
||
<button @click="parseTender()" :disabled="parsing || project.parse_status==='parsing'"
|
||
class="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-lg transition disabled:opacity-60">
|
||
<span x-show="!parsing">AI 解析</span>
|
||
<span x-show="parsing" class="flex items-center gap-1.5">
|
||
<span class="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin"></span>
|
||
解析中...
|
||
</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 上传进度 -->
|
||
<div x-show="uploading" class="mt-3">
|
||
<div class="flex items-center gap-2 text-sm text-blue-600">
|
||
<div class="w-4 h-4 border-2 border-blue-200 border-t-blue-600 rounded-full animate-spin"></div>
|
||
上传中...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 篇幅目标:全功能唯一入口在「解析」;位于上传与清单之间 -->
|
||
<div class="p-4 sm:p-5 bg-purple-50 border border-purple-200 rounded-2xl shadow-sm">
|
||
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||
<div>
|
||
<p class="text-sm font-semibold text-purple-900">篇幅目标(按页数粗略换算)</p>
|
||
<p class="text-xs text-purple-700 mt-1">100/150/200/250/300 页或自定义,保存后用于后续章节生成。在此「保存页数设置」会同步全局配置并写入本标书项目。</p>
|
||
</div>
|
||
<button type="button" @click="savePageSettings()" :disabled="savingPageSettings"
|
||
class="self-start lg:self-auto px-3 py-1.5 text-xs font-medium rounded-lg bg-purple-600 text-white hover:bg-purple-700 disabled:opacity-60">
|
||
<span x-show="!savingPageSettings">保存页数设置</span>
|
||
<span x-show="savingPageSettings">保存中...</span>
|
||
</button>
|
||
</div>
|
||
<div class="mt-3 flex flex-wrap gap-2">
|
||
<template x-for="p in [100,150,200,250,300]" :key="'parse-tab-preset-' + p">
|
||
<button type="button" @click="setPagePreset(p)"
|
||
:class="targetPages === p ? 'bg-purple-600 text-white border-purple-600' : 'bg-white text-purple-700 border-purple-200 hover:bg-purple-100'"
|
||
class="px-3 py-1.5 text-xs rounded-lg border transition">
|
||
<span x-text="p + '页'"></span>
|
||
</button>
|
||
</template>
|
||
<button type="button" @click="setPagePreset(0)"
|
||
:class="targetPages === 0 ? 'bg-purple-600 text-white border-purple-600' : 'bg-white text-purple-700 border-purple-200 hover:bg-purple-100'"
|
||
class="px-3 py-1.5 text-xs rounded-lg border transition">
|
||
使用原档位
|
||
</button>
|
||
</div>
|
||
<div class="mt-3 flex flex-wrap items-center gap-2">
|
||
<span class="text-xs text-purple-800">自定义页数</span>
|
||
<input type="number" min="1" max="2000" x-model.number="customTargetPages"
|
||
class="w-24 px-2 py-1.5 text-xs border border-purple-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-400 bg-white">
|
||
<button type="button" @click="applyCustomPages()"
|
||
class="px-2.5 py-1.5 text-xs bg-white border border-purple-300 text-purple-700 rounded-lg hover:bg-purple-100 transition">
|
||
应用自定义
|
||
</button>
|
||
<span class="text-xs text-purple-700">当前:<b x-text="targetPages > 0 ? (targetPages + ' 页') : '使用原篇幅档位'"></b></span>
|
||
</div>
|
||
<label class="mt-3 flex items-start gap-2 cursor-pointer select-none text-xs text-purple-900">
|
||
<input type="checkbox" x-model="noSubchapterLimit" class="mt-0.5 rounded border-purple-400 text-purple-600 focus:ring-purple-500" />
|
||
<span>不限制小章节条数(填小目时慎用,可产生极多行)。不勾选时未设页数则按约 100 页限幅。</span>
|
||
</label>
|
||
</div>
|
||
|
||
<!-- 生成偏好设置(图表 & 暗标) - 移动到解析页,实现全流程打通 -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
|
||
<div class="flex items-start justify-between mb-4">
|
||
<h3 class="text-base font-bold text-gray-800 flex items-center gap-2">
|
||
<span class="w-6 h-6 bg-teal-100 text-teal-600 rounded-md flex items-center justify-center text-xs font-bold">设置</span>
|
||
生成偏好(图表 & 暗标)
|
||
</h3>
|
||
<div class="flex items-center gap-2 text-xs text-gray-500">
|
||
解析后自动应用至大纲和生成
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图表设置按钮 (relocated) -->
|
||
<button @click="showDiagramPanel = !showDiagramPanel"
|
||
:class="(enableFigure||enableTable) ? 'bg-teal-500 hover:bg-teal-600 text-white border-teal-500' : 'bg-white hover:bg-teal-50 text-gray-600 border-gray-200'"
|
||
class="flex items-center gap-1.5 px-3 py-2 border text-sm font-medium rounded-lg transition mr-2">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||
</svg>
|
||
图表设置
|
||
<span x-show="enableFigure||enableTable" class="w-1.5 h-1.5 rounded-full bg-white"></span>
|
||
</button>
|
||
|
||
<!-- 暗标设置按钮 (relocated) -->
|
||
<button @click="showAnonPanel = !showAnonPanel"
|
||
:class="anonEnabled ? 'bg-amber-500 hover:bg-amber-600 text-white border-amber-500' : 'bg-white hover:bg-amber-50 text-gray-600 border-gray-200'"
|
||
class="flex items-center gap-1.5 px-3 py-2 border text-sm font-medium rounded-lg transition">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
||
</svg>
|
||
暗标设置
|
||
<span x-show="anonEnabled" class="w-1.5 h-1.5 rounded-full bg-white"></span>
|
||
</button>
|
||
|
||
<!-- 暗标设置面板 (relocated & adapted) -->
|
||
<div x-show="showAnonPanel" x-transition class="mt-4 p-5 bg-amber-50 border border-amber-200 rounded-xl">
|
||
<div class="flex items-center justify-between mb-3">
|
||
<div class="flex items-center gap-3">
|
||
<svg class="w-4 h-4 text-amber-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
||
</svg>
|
||
<span class="text-sm font-semibold text-amber-800">暗标模式</span>
|
||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||
<div class="relative">
|
||
<input type="checkbox" x-model="anonEnabled" class="sr-only">
|
||
<div :class="anonEnabled ? 'bg-amber-500' : 'bg-gray-300'" class="w-9 h-5 rounded-full transition"></div>
|
||
<div :class="anonEnabled ? 'translate-x-4' : 'translate-x-0'" class="absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform"></div>
|
||
</div>
|
||
<span class="text-xs text-amber-700" x-text="anonEnabled ? '已启用' : '未启用'"></span>
|
||
</label>
|
||
</div>
|
||
<button @click="saveAnon()" :disabled="savingAnon" class="flex items-center gap-1.5 px-3 py-1.5 bg-amber-500 hover:bg-amber-600 text-white text-xs font-medium rounded-lg transition disabled:opacity-60">
|
||
<span x-text="savingAnon ? '保存中...' : '保存暗标要求'"></span>
|
||
</button>
|
||
</div>
|
||
<div x-show="anonEnabled" x-transition>
|
||
<p class="text-xs text-amber-700 mb-3">暗标要求将附加到每个章节的 AI 生成规范中。解析阶段可预设,生成时自动注入。</p>
|
||
<div class="flex flex-wrap gap-2 mb-3">
|
||
<span class="text-xs text-amber-600 font-medium self-center mr-1">常用预设:</span>
|
||
<button @click="addAnonPreset('不得出现投标人公司名称、注册地址等任何可识别单位信息')" class="px-2.5 py-1 text-xs bg-white border border-amber-200 text-amber-700 rounded-md hover:bg-amber-100 transition">+ 公司名称</button>
|
||
<button @click="addAnonPreset('不得出现项目经理、技术负责人等人员的真实姓名')" class="px-2.5 py-1 text-xs bg-white border border-amber-200 text-amber-700 rounded-md hover:bg-amber-100 transition">+ 人员姓名</button>
|
||
<button @click="addAnonPreset('不得出现联系方式(电话、邮箱、网址等)')" class="px-2.5 py-1 text-xs bg-white border border-amber-200 text-amber-700 rounded-md hover:bg-amber-100 transition">+ 联系方式</button>
|
||
</div>
|
||
<textarea x-model="anonText" rows="4" placeholder="在此输入暗标要求..." class="w-full px-4 py-3 border border-amber-200 rounded-xl text-sm leading-6 focus:outline-none focus:ring-2 focus:ring-amber-400 bg-white resize-y font-mono"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图表设置面板 (relocated & adapted) -->
|
||
<div x-show="showDiagramPanel" x-transition class="mt-4 p-5 bg-teal-50 border border-teal-200 rounded-xl">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<div class="flex items-center gap-2">
|
||
<svg class="w-4 h-4 text-teal-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
|
||
</svg>
|
||
<span class="text-sm font-semibold text-teal-800">图表生成模式</span>
|
||
</div>
|
||
<button @click="saveDiagram()" :disabled="savingDiagram" class="flex items-center gap-1.5 px-3 py-1.5 bg-teal-500 hover:bg-teal-600 text-white text-xs font-medium rounded-lg transition disabled:opacity-60">
|
||
<span x-text="savingDiagram ? '保存中...' : '保存设置'"></span>
|
||
</button>
|
||
</div>
|
||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||
<label class="flex items-start gap-3 p-4 bg-white border border-teal-200 rounded-xl cursor-pointer hover:border-teal-400 transition" :class="enableFigure ? 'border-teal-400 ring-1 ring-teal-300' : ''">
|
||
<div class="relative mt-0.5 flex-shrink-0">
|
||
<input type="checkbox" x-model="enableFigure" class="sr-only">
|
||
<div :class="enableFigure ? 'bg-teal-500' : 'bg-gray-300'" class="w-9 h-5 rounded-full transition"></div>
|
||
<div :class="enableFigure ? 'translate-x-4' : 'translate-x-0'" class="absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform"></div>
|
||
</div>
|
||
<div>
|
||
<p class="text-sm font-semibold text-gray-800">启用"图"生成</p>
|
||
<p class="text-xs text-gray-500 mt-1 leading-5">解析后AI生成章节时自动插入图示,导出Word时渲染。</p>
|
||
</div>
|
||
</label>
|
||
<label class="flex items-start gap-3 p-4 bg-white border border-teal-200 rounded-xl cursor-pointer hover:border-teal-400 transition" :class="enableTable ? 'border-teal-400 ring-1 ring-teal-300' : ''">
|
||
<div class="relative mt-0.5 flex-shrink-0">
|
||
<input type="checkbox" x-model="enableTable" class="sr-only">
|
||
<div :class="enableTable ? 'bg-teal-500' : 'bg-gray-300'" class="w-9 h-5 rounded-full transition"></div>
|
||
<div :class="enableTable ? 'translate-x-4' : 'translate-x-0'" class="absolute top-0.5 left-0.5 w-4 h-4 bg-white rounded-full shadow transition-transform"></div>
|
||
</div>
|
||
<div>
|
||
<p class="text-sm font-semibold text-gray-800">启用"表"生成</p>
|
||
<p class="text-xs text-gray-500 mt-1 leading-5">解析后AI生成章节时自动插入表格,导出Word时渲染。</p>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
<p class="text-xs text-teal-600 mt-3">设置后保存,解析结果和大纲生成将参考这些偏好。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── 工程量清单导入(可选)── -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
|
||
<div class="flex items-center gap-2 mb-4">
|
||
<svg class="w-4 h-4 text-orange-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||
</svg>
|
||
<h3 class="font-semibold text-gray-800 text-sm">工程量清单导入 <span class="text-xs font-normal text-gray-400 ml-1">(可选)解析后可联动招标内容,让生成内容包含准确工程量</span></h3>
|
||
</div>
|
||
|
||
<!-- 上传区域 -->
|
||
<div x-show="!project.boq_file_name"
|
||
class="border-2 border-dashed border-orange-200 rounded-xl p-6 text-center hover:border-orange-400 transition cursor-pointer bg-orange-50"
|
||
@dragover.prevent="true"
|
||
@drop.prevent="uploadBoqFile($event.dataTransfer.files[0])"
|
||
@click="$refs.boqFileInput.click()">
|
||
<input type="file" x-ref="boqFileInput" class="hidden"
|
||
accept=".xlsx,.xls,.csv,.pdf,.docx,.doc"
|
||
@change="uploadBoqFile($event.target.files[0])">
|
||
<svg class="w-8 h-8 mx-auto mb-2 text-orange-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||
</svg>
|
||
<p class="text-sm text-orange-600">拖拽清单文件到此处,或点击选择</p>
|
||
<p class="text-xs text-gray-400 mt-1">支持 Excel(xlsx/xls)、CSV、PDF、Word,最大 50MB</p>
|
||
</div>
|
||
|
||
<!-- 已上传 -->
|
||
<div x-show="project.boq_file_name" class="flex items-center justify-between p-3 bg-orange-50 rounded-xl">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-8 h-8 bg-orange-100 rounded-lg flex items-center justify-center flex-shrink-0">
|
||
<svg class="w-4 h-4 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<p class="text-sm font-medium text-gray-800" x-text="project.boq_file_name"></p>
|
||
<p class="text-xs text-gray-400" x-text="project.boq_status==='done' ? '已解析完成' : project.boq_status==='parsing' ? '解析中...' : '已上传'"></p>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<!-- 重新上传 -->
|
||
<button @click="project.boq_file_name=''; project.boq_status='none'"
|
||
class="text-xs text-gray-400 hover:text-gray-600 transition">重新上传</button>
|
||
<!-- 解析按钮 -->
|
||
<button @click="parseBoq()"
|
||
:disabled="parsingBoq || project.boq_status==='parsing'"
|
||
class="flex items-center gap-1.5 px-4 py-2 bg-orange-500 hover:bg-orange-600 text-white text-sm rounded-lg transition disabled:opacity-60">
|
||
<span x-show="!parsingBoq && project.boq_status!=='parsing'">AI 解析清单</span>
|
||
<span x-show="parsingBoq || project.boq_status==='parsing'" class="flex items-center gap-1.5">
|
||
<span class="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin"></span>
|
||
解析中...
|
||
</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 上传进度 -->
|
||
<div x-show="uploadingBoq" class="mt-3 flex items-center gap-2 text-sm text-orange-600">
|
||
<div class="w-4 h-4 border-2 border-orange-200 border-t-orange-500 rounded-full animate-spin"></div>
|
||
上传中...
|
||
</div>
|
||
|
||
<!-- 解析错误 -->
|
||
<div x-show="project.boq_status==='error'" class="mt-3 flex items-center gap-2 text-sm text-red-600">
|
||
<svg class="w-4 h-4 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"/>
|
||
</svg>
|
||
<span x-text="'解析失败: ' + (project.boq_error || '未知错误')"></span>
|
||
</div>
|
||
|
||
<!-- 本地清单结构化分析(bill-worker 规则 Python 版) -->
|
||
<div x-show="project.boq_status==='done' && project.boq_analysis" class="mt-4 space-y-2">
|
||
<template x-if="project.boq_analysis && (project.boq_analysis.scanned || project.boq_analysis.no_bill_pages)">
|
||
<p class="text-xs text-amber-700 bg-amber-50 border border-amber-100 rounded-lg px-3 py-2">
|
||
<span x-show="project.boq_analysis.scanned">本地分析:文本过少,可能为扫描件或未提取到文字层;已仅依赖全文生成摘要。</span>
|
||
<span x-show="!project.boq_analysis.scanned && project.boq_analysis.no_bill_pages">本地分析:未识别到清单表页,摘要仍可能基于全文。</span>
|
||
</p>
|
||
</template>
|
||
<template x-if="project.boq_analysis && project.boq_analysis.categories && project.boq_analysis.categories.length && !project.boq_analysis.scanned && !project.boq_analysis.no_bill_pages">
|
||
<div class="rounded-xl border border-orange-100 bg-white p-3 text-xs">
|
||
<div class="flex items-center justify-between gap-2 mb-2">
|
||
<p class="font-semibold text-orange-800 flex items-center gap-1">
|
||
<span class="w-1.5 h-1.5 bg-orange-500 rounded-full"></span>
|
||
本地清单分析
|
||
</p>
|
||
<button type="button" @click="showBoqAnalysisDetail = !showBoqAnalysisDetail"
|
||
class="text-orange-600 hover:text-orange-800 shrink-0">
|
||
<span x-text="showBoqAnalysisDetail ? '收起明细' : '展开明细'"></span>
|
||
</button>
|
||
</div>
|
||
<p class="text-gray-600 mb-1" x-text="project.boq_analysis.project_summary && project.boq_analysis.project_summary.remark"></p>
|
||
<p class="text-gray-500">
|
||
分部 <span class="font-medium text-gray-700" x-text="boqStructuredCatCount()"></span> 个,
|
||
清单项 <span class="font-medium text-gray-700" x-text="boqStructuredItemCount()"></span> 条
|
||
<template x-if="project.boq_analysis._meta">
|
||
<span>(识别清单相关页 <span x-text="project.boq_analysis._meta.bill_pages || 0"></span> / 总 <span x-text="project.boq_analysis._meta.total_pages || 0"></span> 页)</span>
|
||
</template>
|
||
</p>
|
||
<div x-show="showBoqAnalysisDetail" class="mt-2 max-h-64 overflow-y-auto border border-gray-100 rounded-lg">
|
||
<template x-for="cat in (project.boq_analysis.categories || [])" :key="cat.name">
|
||
<div class="border-b border-gray-50 last:border-0 px-2 py-2">
|
||
<p class="font-medium text-gray-800 mb-1" x-text="cat.name"></p>
|
||
<table class="w-full text-left text-[11px] text-gray-600">
|
||
<thead>
|
||
<tr class="text-gray-400 border-b border-gray-100">
|
||
<th class="py-0.5 pr-2 font-normal">编码</th>
|
||
<th class="py-0.5 pr-2 font-normal">名称</th>
|
||
<th class="py-0.5 pr-1 font-normal w-10">单位</th>
|
||
<th class="py-0.5 font-normal w-14">工程量</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template x-for="(it, idx) in (cat.items || [])" :key="idx">
|
||
<tr class="border-b border-gray-50 align-top">
|
||
<td class="py-0.5 pr-2 font-mono text-[10px]" x-text="it.code || '—'"></td>
|
||
<td class="py-0.5 pr-2" x-text="it.name"></td>
|
||
<td class="py-0.5 pr-1" x-text="it.unit || ''"></td>
|
||
<td class="py-0.5" x-text="it.quantity || ''"></td>
|
||
</tr>
|
||
</template>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- BOQ 摘要显示/编辑(解析完成后) -->
|
||
<div x-show="project.boq_status==='done' && project.boq_summary" class="mt-4">
|
||
<div class="flex items-center justify-between mb-2">
|
||
<p class="text-xs font-semibold text-orange-700 flex items-center gap-1">
|
||
<span class="w-1.5 h-1.5 bg-orange-500 rounded-full"></span>
|
||
工程量清单摘要
|
||
</p>
|
||
<div class="flex items-center gap-2">
|
||
<template x-if="editBoq">
|
||
<div class="flex gap-2">
|
||
<button @click="saveBoqSummary()" :disabled="savingBoq"
|
||
class="flex items-center gap-1 px-3 py-1 bg-orange-500 hover:bg-orange-600 text-white text-xs rounded-lg transition disabled:opacity-60">
|
||
<span x-show="!savingBoq">保存</span>
|
||
<span x-show="savingBoq" class="flex items-center gap-1">
|
||
<span class="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin"></span>
|
||
保存中
|
||
</span>
|
||
</button>
|
||
<button @click="editBoq=false; editBoqText=project.boq_summary"
|
||
class="px-3 py-1 text-xs border border-gray-200 text-gray-500 hover:bg-gray-50 rounded-lg transition">
|
||
取消
|
||
</button>
|
||
</div>
|
||
</template>
|
||
<template x-if="!editBoq">
|
||
<button @click="editBoq=true; editBoqText=project.boq_summary"
|
||
class="flex items-center gap-1 px-3 py-1 text-xs border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-lg transition">
|
||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||
</svg>
|
||
编辑
|
||
</button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<!-- 预览 -->
|
||
<div x-show="!editBoq"
|
||
class="text-xs text-gray-700 bg-orange-50 rounded-xl p-4 max-h-48 overflow-y-auto whitespace-pre-wrap leading-6 border border-orange-100"
|
||
x-text="project.boq_summary"></div>
|
||
<!-- 编辑 -->
|
||
<div x-show="editBoq">
|
||
<textarea x-model="editBoqText" rows="10"
|
||
class="w-full px-4 py-3 border border-orange-300 rounded-xl text-xs leading-6 focus:outline-none focus:ring-2 focus:ring-orange-400 resize-y font-mono"
|
||
placeholder="工程量清单摘要内容..."></textarea>
|
||
<p class="text-xs text-gray-400 mt-1">修改后点击"保存",将在生成章节内容时作为工程量参考</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 解析状态 -->
|
||
<div x-show="project.parse_status && project.parse_status !== 'none' && project.parse_status !== 'uploaded'"
|
||
class="bg-white rounded-2xl shadow-sm border border-gray-100 p-5">
|
||
<div x-show="project.parse_status==='parsing'" class="flex items-center gap-3 text-blue-600">
|
||
<div class="w-5 h-5 border-2 border-blue-200 border-t-blue-600 rounded-full animate-spin"></div>
|
||
<span class="text-sm font-medium" x-text="project.parse_error || '正在解析,请稍候...'"></span>
|
||
</div>
|
||
<div x-show="project.parse_status==='error'" class="flex items-center gap-3 text-red-600">
|
||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"/>
|
||
</svg>
|
||
<span class="text-sm" x-text="'解析失败: ' + (project.parse_error || '未知错误')"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 解析结果 -->
|
||
<div x-show="project.parse_status==='done'" class="space-y-4">
|
||
|
||
<!-- 标书类型(解析自动识别,影响步骤 3 正文模板) -->
|
||
<div class="bg-gradient-to-r from-indigo-50 to-blue-50 rounded-2xl border border-indigo-100 p-5">
|
||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||
<div>
|
||
<h3 class="text-sm font-semibold text-indigo-900">标书类型</h3>
|
||
<p class="text-xs text-indigo-700/80 mt-0.5">解析完成后自动识别为工程类 / 服务类 / 货物类;步骤 3 生成章节将套用对应写作模板(施工组织 / 服务方案 / 供货方案)。识别有误可在此修正。</p>
|
||
</div>
|
||
<div class="flex items-center gap-2 flex-shrink-0">
|
||
<select x-model="tenderKind"
|
||
class="text-sm border border-indigo-200 rounded-lg px-3 py-2 bg-white text-gray-800 focus:ring-2 focus:ring-indigo-400 focus:outline-none min-w-[11rem]">
|
||
<option value="engineering">工程类(施工组织设计)</option>
|
||
<option value="service">服务类(服务实施方案)</option>
|
||
<option value="goods">货物类(供货技术方案)</option>
|
||
</select>
|
||
<button type="button" @click="saveTenderData('kind')"
|
||
:disabled="savingTenderData"
|
||
class="px-3 py-2 text-xs font-medium rounded-lg bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-50">
|
||
保存类型
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 招标文件摘要 -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
|
||
<div class="flex items-center justify-between mb-3">
|
||
<h3 class="font-semibold text-gray-800 flex items-center gap-2">
|
||
<span class="w-2 h-2 bg-green-500 rounded-full"></span>
|
||
招标文件摘要
|
||
</h3>
|
||
<div class="flex items-center gap-2">
|
||
<template x-if="editSummary">
|
||
<div class="flex items-center gap-2">
|
||
<button @click="saveTenderData('summary')"
|
||
:disabled="savingTenderData"
|
||
class="flex items-center gap-1 px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-xs font-medium rounded-lg transition disabled:opacity-60">
|
||
<span x-show="!savingTenderData">保存</span>
|
||
<span x-show="savingTenderData" class="flex items-center gap-1">
|
||
<span class="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin"></span>
|
||
保存中
|
||
</span>
|
||
</button>
|
||
<button @click="editSummary=false; editSummaryText=project.summary"
|
||
class="px-3 py-1.5 text-xs border border-gray-200 text-gray-500 hover:bg-gray-50 rounded-lg transition">
|
||
取消
|
||
</button>
|
||
</div>
|
||
</template>
|
||
<template x-if="!editSummary">
|
||
<div class="flex items-center gap-2">
|
||
<button @click="editSummary=true; editSummaryText=project.summary"
|
||
class="flex items-center gap-1 px-3 py-1.5 text-xs border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-lg transition">
|
||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||
</svg>
|
||
编辑
|
||
</button>
|
||
<button @click="activeTab='outline'" class="text-xs text-blue-600 hover:underline">
|
||
下一步:生成大纲 →
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<!-- 预览模式 -->
|
||
<div x-show="!editSummary"
|
||
class="prose-content text-gray-700 bg-gray-50 rounded-xl p-4 max-h-64 overflow-y-auto text-sm"
|
||
x-text="project.summary || '摘要生成中...'"></div>
|
||
<!-- 编辑模式 -->
|
||
<div x-show="editSummary">
|
||
<textarea x-model="editSummaryText" rows="12"
|
||
class="w-full px-4 py-3 border border-blue-300 rounded-xl text-sm leading-7 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y font-mono"
|
||
placeholder="招标文件摘要内容..."></textarea>
|
||
<p class="text-xs text-gray-400 mt-1">修改后点击"保存",将作为生成大纲的依据</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 技术评分要求 -->
|
||
<div x-show="project.rating_requirements" class="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
|
||
<div class="flex items-center justify-between mb-3">
|
||
<h3 class="font-semibold text-gray-800 flex items-center gap-2">
|
||
<span class="w-2 h-2 bg-orange-500 rounded-full"></span>
|
||
技术评分要求
|
||
</h3>
|
||
<div class="flex items-center gap-2">
|
||
<template x-if="editRating">
|
||
<div class="flex items-center gap-2">
|
||
<button @click="saveTenderData('rating')"
|
||
:disabled="savingTenderData"
|
||
class="flex items-center gap-1 px-3 py-1.5 bg-green-600 hover:bg-green-700 text-white text-xs font-medium rounded-lg transition disabled:opacity-60">
|
||
<span x-show="!savingTenderData">保存</span>
|
||
<span x-show="savingTenderData" class="flex items-center gap-1">
|
||
<span class="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin"></span>
|
||
保存中
|
||
</span>
|
||
</button>
|
||
<button @click="editRating=false; editRatingText=project.rating_requirements"
|
||
class="px-3 py-1.5 text-xs border border-gray-200 text-gray-500 hover:bg-gray-50 rounded-lg transition">
|
||
取消
|
||
</button>
|
||
</div>
|
||
</template>
|
||
<template x-if="!editRating">
|
||
<button @click="editRating=true; editRatingText=project.rating_requirements"
|
||
class="flex items-center gap-1 px-3 py-1.5 text-xs border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-lg transition">
|
||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||
</svg>
|
||
编辑
|
||
</button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<!-- 预览模式 -->
|
||
<div x-show="!editRating"
|
||
class="prose-content text-gray-700 bg-orange-50 rounded-xl p-4 max-h-64 overflow-y-auto text-sm"
|
||
x-text="project.rating_requirements"></div>
|
||
<!-- 编辑模式 -->
|
||
<div x-show="editRating">
|
||
<textarea x-model="editRatingText" rows="14"
|
||
class="w-full px-4 py-3 border border-orange-300 rounded-xl text-sm leading-7 focus:outline-none focus:ring-2 focus:ring-orange-400 resize-y font-mono"
|
||
placeholder="技术评分要求内容..."></textarea>
|
||
<p class="text-xs text-gray-400 mt-1">修改后点击"保存",将作为生成大纲的依据(只保留技术评分,删除商务/价格评分内容)</p>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- ═══ TAB 2:大纲生成 ═══ -->
|
||
<div x-show="activeTab==='outline'" class="space-y-5">
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
|
||
<h2 class="text-base font-bold text-gray-800 mb-4 flex items-center gap-2">
|
||
<span class="w-6 h-6 bg-indigo-100 text-indigo-600 rounded-md flex items-center justify-center text-xs font-bold">2</span>
|
||
生成标书大纲
|
||
</h2>
|
||
|
||
<div x-show="project.parse_status !== 'done'" class="py-8 text-center">
|
||
<p class="text-gray-400 text-sm">请先完成招标文件解析</p>
|
||
<button @click="activeTab='parse'" class="mt-3 text-blue-600 text-sm hover:underline">返回解析步骤</button>
|
||
</div>
|
||
|
||
<div x-show="project.parse_status === 'done'">
|
||
<p class="text-sm text-gray-500 mb-3">AI 将根据招标文件摘要和技术评分标准,生成结构化的四级标书目录。</p>
|
||
|
||
<div class="mb-4 p-3 bg-slate-50 border border-slate-200 rounded-lg text-xs text-slate-800 leading-relaxed">
|
||
<p class="font-semibold text-slate-900 mb-1.5">控长与章节数(建议按顺序执行)</p>
|
||
<ol class="list-decimal pl-4 space-y-1.5">
|
||
<li><b>定总节数 N</b>:在大纲中合并/删除不需要的编号行,点「保存并更新章节」;步骤 3「生成」中「共 N 个章节」、左侧章节目录与这里的 N 一致,由本步大纲内容决定。</li>
|
||
<li><b>用页数约束小目 AI 树</b>:在「步骤1 解析」中「上传」与「清单」之间的「篇幅目标」设页;使用「AI 自动填充小章节」时一般勿勾选「不限制小章节」;系统将按目标页对<strong>小章节条数</strong>作映射与限幅。</li>
|
||
<li><b>全稿再回调</b>:到「生成」用「一键并发生成」后看总字数/估算;若仍过长,回本页大纲继续并节/删行、保存,再回「生成」重跑。</li>
|
||
</ol>
|
||
</div>
|
||
|
||
<div class="mb-4 p-3 bg-indigo-50 border border-indigo-200 rounded-xl text-xs text-indigo-900 leading-relaxed">
|
||
<p>
|
||
<strong>目标页数、不限制小章节</strong>在
|
||
<button type="button" @click="activeTab = 'parse'" class="text-indigo-600 font-medium hover:underline">步骤1 解析</button>
|
||
(上传与清单之间)的「篇幅目标」中设置。
|
||
</p>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-2 flex-wrap">
|
||
<button @click="generateOutline()"
|
||
:disabled="project.outline_status==='outline_generating' || buildingOutlineAssistant"
|
||
class="flex items-center gap-2 px-5 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium rounded-xl shadow-sm transition disabled:opacity-60">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"/>
|
||
</svg>
|
||
<span x-show="project.outline_status !== 'outline_generating'">
|
||
<span x-text="sections.length > 0 ? '重新生成大纲' : '生成标书大纲'"></span>
|
||
</span>
|
||
<span x-show="project.outline_status === 'outline_generating'" class="flex items-center gap-1.5">
|
||
<span class="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin"></span>
|
||
正在生成...
|
||
</span>
|
||
</button>
|
||
<button @click="openOutlinePasteDialog()"
|
||
:disabled="project.outline_status==='outline_generating' || buildingOutlineAssistant || expandingOutline"
|
||
class="flex items-center gap-2 px-5 py-2.5 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-medium rounded-xl shadow-sm transition disabled:opacity-60">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||
</svg>
|
||
<span x-show="!buildingOutlineAssistant">建立(自助填写 + AI细化)</span>
|
||
<span x-show="buildingOutlineAssistant" class="flex items-center gap-1.5">
|
||
<span class="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin"></span>
|
||
执行中...
|
||
</span>
|
||
</button>
|
||
</div>
|
||
<p class="text-xs text-gray-400 mt-2" x-show="project && project.outline && project.outline.length > 0">
|
||
当前用于生成的大纲:约 <span x-text="(project && project.outline ? project.outline.length : 0)"></span> 字,<span x-text="sections.length"></span> 个章节
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 大纲进度 -->
|
||
<div x-show="project.outline_status==='outline_generating'" class="mt-4 flex items-center gap-2 text-indigo-600">
|
||
<div class="w-4 h-4 border-2 border-indigo-200 border-t-indigo-600 rounded-full animate-spin"></div>
|
||
<span class="text-sm">AI 正在生成标书大纲,通常需要 30-60 秒...</span>
|
||
</div>
|
||
<div x-show="project.outline_status==='outline_error'" class="mt-4 text-red-500 text-sm"
|
||
x-text="'生成失败: ' + (project.outline_error || '')"></div>
|
||
|
||
<!-- 大纲预览 / 编辑 -->
|
||
<div x-show="sections.length > 0" class="mt-5">
|
||
<div class="flex items-center justify-between mb-3">
|
||
<h3 class="font-medium text-gray-700">
|
||
大纲
|
||
<span x-show="!editOutline">预览</span>
|
||
<span x-show="editOutline">编辑</span>
|
||
(<span x-text="sections.length"></span> 个章节)
|
||
</h3>
|
||
<div class="flex items-center gap-2">
|
||
<!-- 编辑模式按钮组 -->
|
||
<template x-if="editOutline">
|
||
<div class="flex items-center gap-2">
|
||
<button @click="saveOutline()"
|
||
:disabled="savingOutline"
|
||
class="flex items-center gap-1 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white text-xs font-medium rounded-lg transition disabled:opacity-60">
|
||
<span x-show="!savingOutline">保存并更新章节</span>
|
||
<span x-show="savingOutline" class="flex items-center gap-1">
|
||
<span class="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin"></span>
|
||
保存中
|
||
</span>
|
||
</button>
|
||
<button @click="cancelEditOutline()"
|
||
class="px-3 py-1.5 text-xs border border-gray-200 text-gray-500 hover:bg-gray-50 rounded-lg transition">
|
||
取消
|
||
</button>
|
||
<button @click="expandOutline()"
|
||
:disabled="expandingOutline || !editOutlineText.trim()"
|
||
class="flex items-center gap-1 px-3 py-1.5 bg-emerald-600 hover:bg-emerald-700 text-white text-xs font-medium rounded-lg transition disabled:opacity-60">
|
||
<span x-show="!expandingOutline">AI自动填充小章节</span>
|
||
<span x-show="expandingOutline" class="flex items-center gap-1">
|
||
<span class="w-3 h-3 border border-white/40 border-t-white rounded-full animate-spin"></span>
|
||
填充中
|
||
</span>
|
||
</button>
|
||
</div>
|
||
</template>
|
||
<!-- 预览模式按钮组 -->
|
||
<template x-if="!editOutline">
|
||
<div class="flex items-center gap-2">
|
||
<button @click="enterEditOutline()"
|
||
class="flex items-center gap-1 px-3 py-1.5 text-xs border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-lg transition">
|
||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||
</svg>
|
||
编辑大纲
|
||
</button>
|
||
<button @click="activeTab='content'"
|
||
class="flex items-center gap-1 text-xs text-indigo-600 hover:underline font-medium">
|
||
下一步:生成内容 →
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 填小目前:页数/不限制 在「解析」顶栏篇幅区配置 -->
|
||
<div
|
||
x-show="editOutline && project.parse_status === 'done'"
|
||
class="mb-3 p-3 bg-emerald-50 border border-emerald-200 rounded-xl"
|
||
>
|
||
<p class="text-xs font-semibold text-emerald-900">「AI 自动填充小章节」与目标页</p>
|
||
<p class="text-xs text-emerald-800/95 mt-1 leading-relaxed" x-text="subchapterCountHintText()"></p>
|
||
<button type="button" @click="activeTab = 'parse'" class="mt-1.5 text-xs text-emerald-700 font-medium hover:underline">前往「步骤1 解析」修改篇幅与「不限制小章节」</button>
|
||
</div>
|
||
|
||
<!-- 预览模式:章节树 -->
|
||
<div x-show="!editOutline" class="bg-gray-50 rounded-xl p-4 max-h-96 overflow-y-auto">
|
||
<template x-for="s in sections" :key="s.id">
|
||
<div class="flex items-center gap-2 py-0.5" :style="'padding-left:' + (s.level-1)*20 + 'px'">
|
||
<span class="text-gray-400 text-xs" x-text="s.level <= 2 ? '▸' : '·'"></span>
|
||
<span class="text-sm" :class="s.level===1 ? 'font-semibold text-gray-800' : s.level===2 ? 'font-medium text-gray-700' : 'text-gray-600'"
|
||
x-text="s.title"></span>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 编辑模式:原始大纲文本 -->
|
||
<div x-show="editOutline">
|
||
<div class="mb-2 p-3 bg-amber-50 border border-amber-200 rounded-xl text-xs text-amber-700 flex items-start gap-2">
|
||
<svg class="w-4 h-4 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
|
||
</svg>
|
||
<span>保存后将按新大纲重新划分章节,<b>已生成的章节正文内容将被清除</b>,需重新生成。编辑时请保持层级编号格式(一、1.1、1.1.1…)不变。</span>
|
||
</div>
|
||
<textarea x-model="editOutlineText" @input="outlineDirty=true" rows="20"
|
||
class="w-full px-4 py-3 border border-indigo-300 rounded-xl text-sm leading-6 focus:outline-none focus:ring-2 focus:ring-indigo-500 resize-y font-mono"
|
||
placeholder="在此编辑大纲文本,每行一个章节项..."></textarea>
|
||
<p class="text-xs text-gray-400 mt-1">格式示例:第一行为标书名称,章节用"一、""1.1""1.1.1""1.1.1.1"等格式</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ TAB 3:内容生成 ═══ -->
|
||
<div x-show="activeTab==='content'" class="space-y-5">
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
|
||
<div class="flex items-start justify-between mb-4">
|
||
<h2 class="text-base font-bold text-gray-800 flex items-center gap-2">
|
||
<span class="w-6 h-6 bg-purple-100 text-purple-600 rounded-md flex items-center justify-center text-xs font-bold">3</span>
|
||
章节内容生成
|
||
</h2>
|
||
<button @click="generateAllSections()"
|
||
:disabled="progress.running > 0"
|
||
class="flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium rounded-lg transition disabled:opacity-60">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||
</svg>
|
||
<span x-show="progress.running === 0">一键并发生成</span>
|
||
<span x-show="progress.running > 0" class="flex items-center gap-1.5">
|
||
<span class="w-3 h-3 border-2 border-white/40 border-t-white rounded-full animate-spin"></span>
|
||
并发生成中...
|
||
</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="mb-5 p-3 bg-slate-50 border border-slate-200 rounded-lg text-xs text-slate-800 leading-relaxed">
|
||
<p class="font-semibold text-slate-900 mb-1">篇幅与章节数</p>
|
||
<p class="text-slate-700">
|
||
<strong>目标页数</strong>只在
|
||
<button type="button" @click="activeTab = 'parse'" class="text-indigo-600 font-medium hover:underline">步骤1 解析</button>
|
||
「上传」与「清单」之间的「篇幅目标」中配置。本页仅并发生成与查看进度。列表「共 N 节」与大纲行数相关,需并节/删行时回<strong>步骤2 大纲</strong>。
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 并发进度面板 -->
|
||
<div x-show="progress.total > 0" class="mb-5 p-4 rounded-xl border"
|
||
:class="progress.running > 0 ? 'bg-purple-50 border-purple-200' : 'bg-gray-50 border-gray-200'">
|
||
<div class="flex items-center justify-between mb-2">
|
||
<div class="flex items-center gap-3 text-sm">
|
||
<span class="font-medium text-gray-700">生成进度</span>
|
||
<span x-show="progress.running > 0"
|
||
class="flex items-center gap-1.5 text-purple-600 font-medium">
|
||
<span class="relative flex h-2.5 w-2.5">
|
||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-purple-400 opacity-75"></span>
|
||
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-purple-600"></span>
|
||
</span>
|
||
<span x-text="progress.running"></span> 路并发
|
||
</span>
|
||
</div>
|
||
<span class="text-sm font-bold" :class="progress.percent===100 ? 'text-green-600' : 'text-gray-700'"
|
||
x-text="progress.percent + '%'"></span>
|
||
</div>
|
||
<div class="h-2.5 bg-white rounded-full overflow-hidden shadow-inner">
|
||
<div class="h-full rounded-full transition-all duration-500"
|
||
:class="progress.running > 0 ? 'bg-gradient-to-r from-purple-500 to-indigo-500' : progress.percent===100 ? 'bg-green-500' : 'bg-purple-500'"
|
||
:style="'width:'+progress.percent+'%'"></div>
|
||
</div>
|
||
<div class="flex justify-between mt-2 text-xs text-gray-500">
|
||
<div class="flex gap-4">
|
||
<span class="flex items-center gap-1">
|
||
<span class="w-2 h-2 rounded-full bg-green-500"></span>
|
||
已完成 <b x-text="progress.done" class="text-gray-700"></b>
|
||
</span>
|
||
<span x-show="progress.running > 0" class="flex items-center gap-1">
|
||
<span class="w-2 h-2 rounded-full bg-purple-500 animate-pulse"></span>
|
||
生成中 <b x-text="progress.running" class="text-gray-700"></b>
|
||
</span>
|
||
<span x-show="progress.errors > 0" class="flex items-center gap-1">
|
||
<span class="w-2 h-2 rounded-full bg-red-400"></span>
|
||
失败 <b x-text="progress.errors" class="text-red-600"></b>
|
||
</span>
|
||
</div>
|
||
<span>共 <b x-text="progress.total" class="text-gray-700"></b> 个章节</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-show="sections.length === 0" class="py-8 text-center">
|
||
<p class="text-gray-400 text-sm">请先生成标书大纲</p>
|
||
<button @click="activeTab='outline'" class="mt-3 text-indigo-600 text-sm hover:underline">返回生成大纲</button>
|
||
</div>
|
||
|
||
<!-- 章节列表 -->
|
||
<div x-show="sections.length > 0" class="space-y-1">
|
||
<template x-for="s in sections" :key="s.id">
|
||
<div class="section-item flex items-center gap-3 px-3 py-2 rounded-xl hover:bg-gray-50 cursor-pointer transition"
|
||
:class="selectedSection && selectedSection.id===s.id ? 'bg-blue-50 ring-1 ring-blue-200' : ''"
|
||
@click="selectSection(s); activeTab='editor'"
|
||
:style="s.level > 1 ? 'margin-left:' + (s.level-1)*20 + 'px' : ''">
|
||
|
||
<!-- 状态 -->
|
||
<div class="w-5 h-5 flex-shrink-0 flex items-center justify-center">
|
||
<svg x-show="s.status==='done'" class="w-4.5 h-4.5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/>
|
||
</svg>
|
||
<div x-show="s.status==='generating'" class="w-4 h-4 border-2 border-purple-300 border-t-purple-600 rounded-full animate-spin"></div>
|
||
<svg x-show="s.status==='error'" class="w-4 h-4 text-red-400" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V10a1 1 0 00-1-1zm1 8a1 1 0 100-2 1 1 0 000 2z"/>
|
||
</svg>
|
||
<div x-show="!['done','generating','error'].includes(s.status)"
|
||
class="w-3.5 h-3.5 rounded-full border-2 border-gray-300"></div>
|
||
</div>
|
||
|
||
<span class="flex-1 text-sm min-w-0 truncate"
|
||
:class="s.level===1 ? 'font-semibold text-gray-800' : s.level===2 ? 'font-medium text-gray-700' : 'text-gray-600'"
|
||
x-text="s.title"></span>
|
||
|
||
<span x-show="s.has_content && s.status==='done'" class="text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full flex-shrink-0">已生成</span>
|
||
<span x-show="!s.is_leaf" class="text-xs text-gray-400 flex-shrink-0">章节</span>
|
||
|
||
<div class="section-actions flex gap-1 flex-shrink-0">
|
||
<button x-show="s.is_leaf" @click.stop="generateSection(s.id)"
|
||
:disabled="s.status==='generating'"
|
||
class="px-2.5 py-1 text-xs bg-purple-100 text-purple-700 hover:bg-purple-200 rounded-lg transition disabled:opacity-50">
|
||
生成
|
||
</button>
|
||
<button @click.stop="selectSection(s); activeTab='editor'"
|
||
class="px-2.5 py-1 text-xs bg-gray-100 text-gray-600 hover:bg-gray-200 rounded-lg transition">
|
||
查看
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ TAB 4:编辑器 ═══ -->
|
||
<div x-show="activeTab==='editor'" class="space-y-4">
|
||
<div x-show="!selectedSection" class="bg-white rounded-2xl shadow-sm border border-gray-100 p-12 text-center">
|
||
<p class="text-gray-400">从左侧章节列表或内容列表中选择一个章节</p>
|
||
<button @click="activeTab='content'" class="mt-3 text-purple-600 text-sm hover:underline">查看章节列表</button>
|
||
</div>
|
||
|
||
<div x-show="selectedSection" class="bg-white rounded-2xl shadow-sm border border-gray-100">
|
||
<!-- 编辑器头部 -->
|
||
<div class="flex items-start justify-between px-6 py-4 border-b border-gray-100 gap-4">
|
||
<div class="min-w-0">
|
||
<h3 class="font-bold text-gray-900 text-base" x-text="selectedSection && selectedSection.title"></h3>
|
||
</div>
|
||
<div class="flex flex-col items-end gap-2 flex-shrink-0">
|
||
<!-- 生成模式切换(仅叶节点) -->
|
||
<div x-show="selectedSection && selectedSection.is_leaf"
|
||
class="flex rounded-lg border border-gray-200 overflow-hidden text-xs">
|
||
<button @click="switchGenMode('auto')"
|
||
:class="genMode==='auto' ? 'bg-purple-600 text-white' : 'bg-white text-gray-500 hover:bg-gray-50'"
|
||
class="px-3 py-1.5 flex items-center gap-1 transition">
|
||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||
</svg>
|
||
自动生成
|
||
</button>
|
||
<button @click="switchGenMode('chat')"
|
||
:class="genMode==='chat' ? 'bg-indigo-600 text-white' : 'bg-white text-gray-500 hover:bg-gray-50'"
|
||
class="px-3 py-1.5 flex items-center gap-1 transition border-l border-gray-200">
|
||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-3 3v-3z"/>
|
||
</svg>
|
||
对话生成
|
||
</button>
|
||
</div>
|
||
<!-- 自动模式操作按钮 -->
|
||
<div x-show="genMode==='auto'" class="flex items-center gap-2">
|
||
<button x-show="selectedSection && selectedSection.is_leaf"
|
||
@click="generateSection(selectedSection.id)"
|
||
:disabled="selectedSection && selectedSection.status==='generating'"
|
||
class="flex items-center gap-1.5 px-3 py-1.5 text-sm bg-purple-600 hover:bg-purple-700 text-white rounded-lg transition disabled:opacity-60">
|
||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||
</svg>
|
||
<span x-show="selectedSection && selectedSection.status !== 'generating'">AI 生成</span>
|
||
<span x-show="selectedSection && selectedSection.status === 'generating'">生成中...</span>
|
||
</button>
|
||
<button @click="saveContent()" x-show="editMode"
|
||
class="px-3 py-1.5 text-sm bg-green-600 hover:bg-green-700 text-white rounded-lg transition">
|
||
保存
|
||
</button>
|
||
<button @click="editMode=!editMode"
|
||
class="px-3 py-1.5 text-sm border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-lg transition"
|
||
x-text="editMode ? '预览' : '编辑'"></button>
|
||
</div>
|
||
<!-- 对话模式操作按钮 -->
|
||
<div x-show="genMode==='chat'" class="flex items-center gap-2">
|
||
<button @click="chatMessages=[]; chatSectionId=null; switchGenMode('chat')"
|
||
class="px-3 py-1.5 text-sm border border-gray-200 text-gray-500 hover:bg-gray-50 rounded-lg transition flex items-center gap-1.5">
|
||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||
</svg>
|
||
清空对话
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 生成中提示 -->
|
||
<div x-show="selectedSection && selectedSection.status==='generating'"
|
||
class="px-6 py-3 bg-purple-50 border-b border-purple-100 flex items-center gap-2 text-purple-700 text-sm">
|
||
<div class="w-4 h-4 border-2 border-purple-300 border-t-purple-600 rounded-full animate-spin"></div>
|
||
AI 正在生成内容,请稍候...
|
||
</div>
|
||
|
||
<!-- ── 自动生成模式内容区 ── -->
|
||
<div x-show="genMode==='auto'" class="p-6">
|
||
<!-- 章节引言(非叶节点) -->
|
||
<div x-show="sectionContent && sectionContent.intro_content" class="mb-4 p-4 bg-blue-50 rounded-xl border border-blue-100">
|
||
<p class="text-xs font-medium text-blue-600 mb-2">章节引言</p>
|
||
<p class="text-sm text-gray-700 whitespace-pre-wrap leading-7" x-text="sectionContent && sectionContent.intro_content"></p>
|
||
</div>
|
||
<!-- 编辑模式 -->
|
||
<div x-show="editMode">
|
||
<textarea x-model="editContent" rows="20"
|
||
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm leading-7 focus:outline-none focus:ring-2 focus:ring-blue-500 resize-none font-mono"
|
||
placeholder="在此输入或编辑章节内容..."></textarea>
|
||
</div>
|
||
<!-- 预览模式 -->
|
||
<div x-show="!editMode" class="min-h-48">
|
||
<div x-show="!sectionContent || (!sectionContent.content && !sectionContent.intro_content)"
|
||
class="py-16 text-center text-gray-400">
|
||
<svg class="w-10 h-10 mx-auto mb-3 text-gray-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||
</svg>
|
||
<p class="text-sm">暂无内容,点击"AI 生成"或切换"对话生成"模式</p>
|
||
</div>
|
||
<div x-show="sectionContent && sectionContent.content"
|
||
class="prose-content text-gray-800 leading-8"
|
||
x-text="sectionContent && sectionContent.content"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── 对话生成模式内容区 ── -->
|
||
<div x-show="genMode==='chat'" class="flex flex-col" style="height:600px">
|
||
<!-- 消息列表 -->
|
||
<div id="chatMsgList" class="flex-1 overflow-y-auto px-6 py-4 space-y-4">
|
||
<template x-for="msg in chatMessages" :key="msg.id">
|
||
<div>
|
||
<!-- 用户消息(右侧) -->
|
||
<div x-show="msg.role==='user'" class="flex justify-end">
|
||
<div class="max-w-[80%] bg-indigo-600 text-white rounded-2xl rounded-tr-sm px-4 py-3 text-sm">
|
||
<p x-text="msg.content" class="whitespace-pre-wrap leading-6"></p>
|
||
</div>
|
||
</div>
|
||
<!-- AI 消息(左侧) -->
|
||
<div x-show="msg.role==='assistant'" class="flex justify-start gap-2.5">
|
||
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center flex-shrink-0 mt-1">
|
||
<span class="text-white text-xs font-bold">AI</span>
|
||
</div>
|
||
<div class="max-w-[85%]">
|
||
<div :class="msg.isError ? 'bg-red-50 border-red-200 text-red-700' : 'bg-gray-50 border-gray-200 text-gray-800'"
|
||
class="border rounded-2xl rounded-tl-sm px-4 py-3 text-sm">
|
||
<p x-text="msg.content" class="whitespace-pre-wrap leading-7"></p>
|
||
</div>
|
||
<!-- 采用内容按钮(非欢迎语、非错误) -->
|
||
<button x-show="!msg.isWelcome && !msg.isError"
|
||
@click="applyChatContent(msg.content)"
|
||
class="mt-2 flex items-center gap-1.5 text-xs text-indigo-600 hover:text-indigo-800 font-medium transition">
|
||
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
|
||
</svg>
|
||
采用此内容 → 填入编辑框
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<!-- 加载动画 -->
|
||
<div x-show="chatLoading" class="flex justify-start gap-2.5">
|
||
<div class="w-7 h-7 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center flex-shrink-0 mt-1">
|
||
<span class="text-white text-xs font-bold">AI</span>
|
||
</div>
|
||
<div class="bg-gray-50 border border-gray-200 rounded-2xl rounded-tl-sm px-5 py-3.5">
|
||
<div class="flex gap-1.5 items-center">
|
||
<span class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay:0ms"></span>
|
||
<span class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay:150ms"></span>
|
||
<span class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay:300ms"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 输入区 -->
|
||
<div class="border-t border-gray-100 px-6 py-4 bg-gray-50 rounded-b-2xl">
|
||
<div class="flex gap-3 items-end">
|
||
<textarea x-model="chatInput"
|
||
@keydown.enter.prevent="if(!$event.shiftKey){ sendChatMessage() }"
|
||
:disabled="chatLoading"
|
||
rows="3"
|
||
placeholder="描述您的需求,例如:请生成该章节完整内容,重点强调质量管控措施并给出具体标准编号… (Enter 发送,Shift+Enter 换行)"
|
||
class="flex-1 px-4 py-3 border border-gray-200 rounded-xl text-sm resize-none focus:outline-none focus:ring-2 focus:ring-indigo-500 bg-white leading-6 disabled:opacity-60"></textarea>
|
||
<button @click="sendChatMessage()"
|
||
:disabled="chatLoading || !chatInput.trim()"
|
||
class="flex-shrink-0 px-5 py-3 bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-medium rounded-xl transition disabled:opacity-50 flex items-center gap-2">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8"/>
|
||
</svg>
|
||
发送
|
||
</button>
|
||
</div>
|
||
<p class="text-xs text-gray-400 mt-2">点击 AI 回复下方的「采用此内容 → 填入编辑框」将内容写入编辑器,再点「保存」完成。</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ TAB 5:合规检查 ═══ -->
|
||
<div x-show="activeTab==='check'" class="space-y-4">
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
|
||
<h2 class="text-base font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||
<span class="w-6 h-6 bg-orange-100 text-orange-600 rounded-md flex items-center justify-center text-xs font-bold">4</span>
|
||
合规性检查
|
||
</h2>
|
||
<p class="text-sm text-gray-500 mb-4">AI 将对照招标要求检查标书内容的覆盖情况,给出改进建议。</p>
|
||
<button @click="runCheck()" :disabled="checking"
|
||
class="flex items-center gap-2 px-5 py-2.5 bg-orange-500 hover:bg-orange-600 text-white text-sm font-medium rounded-xl transition disabled:opacity-60">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||
</svg>
|
||
<span x-show="!checking">开始合规检查</span>
|
||
<span x-show="checking" class="flex items-center gap-1.5">
|
||
<span class="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin"></span>
|
||
检查中...
|
||
</span>
|
||
</button>
|
||
|
||
<!-- 检查结果:用 x-if 而非 x-show,null 时完全不渲染内部绑定 -->
|
||
<template x-if="checkResult">
|
||
<div class="mt-6 space-y-4">
|
||
<!-- 总分 -->
|
||
<div class="flex items-center gap-4 p-4 bg-gradient-to-r from-green-50 to-emerald-50 rounded-xl border border-green-100">
|
||
<div class="w-16 h-16 rounded-full border-4 flex items-center justify-center flex-shrink-0"
|
||
:class="(checkResult.overall_score||0) >= 80 ? 'border-green-400' : (checkResult.overall_score||0) >= 60 ? 'border-yellow-400' : 'border-red-400'">
|
||
<span class="text-xl font-bold" x-text="checkResult.overall_score || '--'"></span>
|
||
</div>
|
||
<div>
|
||
<p class="font-bold text-gray-800 text-lg" x-text="checkResult.status || '—'"></p>
|
||
<p class="text-sm text-gray-500">综合评分</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 检查项目 -->
|
||
<template x-if="checkResult.items && checkResult.items.length">
|
||
<div class="space-y-2">
|
||
<h4 class="font-medium text-gray-700 text-sm">检查项目</h4>
|
||
<template x-for="item in checkResult.items" :key="item.requirement">
|
||
<div class="flex items-start gap-3 p-3 rounded-xl border"
|
||
:class="item.covered ? 'bg-green-50 border-green-100' : 'bg-red-50 border-red-100'">
|
||
<svg x-show="item.covered" class="w-4 h-4 text-green-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/>
|
||
</svg>
|
||
<svg x-show="!item.covered" class="w-4 h-4 text-red-400 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V10a1 1 0 00-1-1zm1 8a1 1 0 100-2 1 1 0 000 2z"/>
|
||
</svg>
|
||
<div>
|
||
<p class="text-sm font-medium text-gray-800" x-text="item.requirement"></p>
|
||
<p class="text-xs text-gray-500 mt-0.5" x-text="item.note"></p>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 缺失要点 -->
|
||
<template x-if="checkResult.missing_points && checkResult.missing_points.length">
|
||
<div class="p-4 bg-yellow-50 rounded-xl border border-yellow-100">
|
||
<h4 class="font-medium text-yellow-800 text-sm mb-2">⚠️ 未覆盖要点</h4>
|
||
<ul class="space-y-1">
|
||
<template x-for="pt in checkResult.missing_points" :key="pt">
|
||
<li class="text-sm text-yellow-700 flex items-start gap-2">
|
||
<span class="mt-1 flex-shrink-0">·</span><span x-text="pt"></span>
|
||
</li>
|
||
</template>
|
||
</ul>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 改进建议 -->
|
||
<template x-if="checkResult.suggestions && checkResult.suggestions.length">
|
||
<div class="p-4 bg-blue-50 rounded-xl border border-blue-100">
|
||
<h4 class="font-medium text-blue-800 text-sm mb-2">💡 改进建议</h4>
|
||
<ul class="space-y-1">
|
||
<template x-for="sg in checkResult.suggestions" :key="sg">
|
||
<li class="text-sm text-blue-700 flex items-start gap-2">
|
||
<span class="mt-1 flex-shrink-0">·</span><span x-text="sg"></span>
|
||
</li>
|
||
</template>
|
||
</ul>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
|
||
<!-- 技术暗标 HTML 格式清标(与 AI 合规检查独立;建议 Word 另存为网页等含内联样式的 HTML) -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-6 mt-6">
|
||
<h2 class="text-base font-bold text-gray-800 mb-2 flex items-center gap-2">
|
||
<span class="w-6 h-6 bg-teal-100 text-teal-700 rounded-md flex items-center justify-center text-xs font-bold">清</span>
|
||
技术暗标格式清标
|
||
</h2>
|
||
<p class="text-sm text-gray-500 mb-3">对导出的技术暗标 HTML 做版式规则检查(身份信息、标题/正文、目录、图表位置、颜色、页边距等),不调用大模型。请粘贴完整 HTML 或选择本地 .html 文件。</p>
|
||
<p class="text-xs text-amber-700 bg-amber-50 border border-amber-100 rounded-lg px-3 py-2 mb-4">说明:检查依赖内联 <code class="bg-amber-100 px-0.5 rounded">style</code>;从 Word 另存为「网页」或含完整样式的 HTML 效果最佳;纯标签无样式时部分项易判为不通过。</p>
|
||
<label class="flex items-center gap-2 cursor-pointer mb-4">
|
||
<input type="checkbox" x-model="darkBidCheckEnabled" class="rounded border-gray-300 text-teal-600 focus:ring-teal-500" />
|
||
<span class="text-sm font-medium text-gray-800">启用清暗标格式检查</span>
|
||
</label>
|
||
<div x-show="darkBidCheckEnabled" x-transition class="space-y-4">
|
||
<textarea x-model="darkBidHtml" rows="8" placeholder="在此粘贴技术暗标完整 HTML 源码…"
|
||
class="w-full px-4 py-3 border border-teal-200 rounded-xl text-xs font-mono leading-relaxed focus:outline-none focus:ring-2 focus:ring-teal-500 resize-y"></textarea>
|
||
<div class="flex flex-wrap items-center gap-3">
|
||
<label class="text-sm text-gray-600 cursor-pointer">
|
||
<input type="file" accept=".html,.htm" class="sr-only" @change="onDarkBidFile($event)">
|
||
<span class="px-3 py-1.5 border border-gray-300 rounded-lg hover:bg-gray-50">选择 .html 文件</span>
|
||
</label>
|
||
<span x-show="darkBidFileError" class="text-xs text-red-500" x-text="darkBidFileError"></span>
|
||
</div>
|
||
<button type="button" @click="runDarkBidCheck()" :disabled="darkBidChecking"
|
||
class="flex items-center gap-2 px-5 py-2.5 bg-teal-600 hover:bg-teal-700 text-white text-sm font-medium rounded-xl transition disabled:opacity-60">
|
||
<span x-show="!darkBidChecking" class="flex items-center gap-1.5">运行清标</span>
|
||
<span x-show="darkBidChecking" class="flex items-center gap-1.5">
|
||
<span class="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin"></span>
|
||
清标中…
|
||
</span>
|
||
</button>
|
||
</div>
|
||
<template x-if="darkBidCheckResult && darkBidCheckResult.details">
|
||
<div class="mt-6 space-y-4 p-4 rounded-xl border"
|
||
:class="darkBidCheckResult.overall ? 'bg-emerald-50 border-emerald-200' : 'bg-rose-50 border-rose-200'">
|
||
<p class="font-bold text-sm" :class="darkBidCheckResult.overall ? 'text-emerald-800' : 'text-rose-800'">
|
||
<span x-text="darkBidCheckResult.overall ? '总评:通过' : '总评:未通过'"></span>
|
||
</p>
|
||
<ul class="space-y-2 text-sm">
|
||
<template x-for="row in darkBidCheckResult.details" :key="row.rule">
|
||
<li class="flex items-start gap-2" :class="row.passed ? 'text-gray-800' : 'text-rose-700'">
|
||
<span x-text="row.passed ? '✓' : '✗'"></span>
|
||
<span><b x-text="row.rule + ':'"></b><span x-text="row.message"></span></li>
|
||
</template>
|
||
</ul>
|
||
<template x-if="darkBidCheckResult.violations && darkBidCheckResult.violations.length">
|
||
<div class="mt-3 text-xs text-gray-600 space-y-2">
|
||
<p class="font-medium text-gray-700">违规模块(节选)</p>
|
||
<template x-for="v in darkBidCheckResult.violations" :key="v.rule + v.message">
|
||
<div class="pl-2 border-l-2 border-rose-300">
|
||
<p class="text-rose-800 font-medium" x-text="v.rule"></p>
|
||
<p class="text-gray-600" x-text="v.message"></p>
|
||
<ul class="list-disc pl-4 mt-1 break-all" x-show="v.elements && v.elements.length">
|
||
<template x-for="(el, idx) in v.elements" :key="idx">
|
||
<li x-text="(el && el.length > 180) ? el.slice(0, 180) + '…' : el"></li>
|
||
</template>
|
||
</ul>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ TAB 6:知识库 ═══ -->
|
||
<div x-show="activeTab==='knowledge'" class="space-y-5">
|
||
|
||
<!-- 状态栏 -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-5">
|
||
<div class="flex items-center justify-between flex-wrap gap-3">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-10 h-10 rounded-xl bg-teal-50 flex items-center justify-center flex-shrink-0">
|
||
<svg class="w-5 h-5 text-teal-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
||
</svg>
|
||
</div>
|
||
<div>
|
||
<h2 class="text-base font-bold text-gray-800">企业知识库</h2>
|
||
<p class="text-xs text-gray-400 mt-0.5">上传历史标书,AI 生成时自动检索企业优势内容</p>
|
||
</div>
|
||
</div>
|
||
<!-- 检索模式 chip -->
|
||
<div x-show="knowledgeStatus" class="flex items-center gap-2 flex-wrap">
|
||
<!-- 向量语义检索 -->
|
||
<span x-show="knowledgeStatus && knowledgeStatus.search_mode === 'vector'"
|
||
class="inline-flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-full bg-teal-50 text-teal-700 border border-teal-200">
|
||
<span class="w-2 h-2 rounded-full bg-teal-400 inline-block"></span>
|
||
语义向量检索 · <span x-text="(knowledgeStatus && knowledgeStatus.doc_count) || 0"></span> 个文本块
|
||
</span>
|
||
<!-- 关键词降级检索 -->
|
||
<span x-show="knowledgeStatus && knowledgeStatus.search_mode === 'keyword'"
|
||
class="inline-flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-full bg-amber-50 text-amber-700 border border-amber-200">
|
||
<span class="w-2 h-2 rounded-full bg-amber-400 inline-block"></span>
|
||
关键词检索模式 · <span x-text="(knowledgeStatus && knowledgeStatus.doc_count) || 0"></span> 个文本块
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 关键词模式说明(DeepSeek/Ollama 无 Embedding API 时提示) -->
|
||
<div x-show="knowledgeStatus && knowledgeStatus.search_mode === 'keyword' && knowledgeFiles.length > 0"
|
||
class="mt-4 p-4 bg-amber-50 border border-amber-200 rounded-xl text-sm text-amber-800">
|
||
<p class="font-medium mb-1">💡 当前使用关键词检索</p>
|
||
<p class="text-xs text-amber-700">DeepSeek / Ollama 暂不提供 Embedding API,知识库将以关键词匹配方式检索相关内容。
|
||
切换为 Qwen 或 OpenAI 模型(在首页 AI 配置中设置)可启用更精准的语义向量检索。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 上传区 -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-5">
|
||
<h3 class="text-sm font-semibold text-gray-700 mb-4 flex items-center gap-2">
|
||
<svg class="w-4 h-4 text-teal-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"/>
|
||
</svg>
|
||
添加知识文档
|
||
</h3>
|
||
|
||
<!-- 拖拽上传区 -->
|
||
<label
|
||
@dragover.prevent="knowledgeDragover=true"
|
||
@dragleave="knowledgeDragover=false"
|
||
@drop.prevent="onKnowledgeDrop($event)"
|
||
:class="knowledgeDragover ? 'border-teal-400 bg-teal-50' : 'border-gray-200 bg-gray-50 hover:border-teal-300 hover:bg-teal-50/50'"
|
||
class="flex flex-col items-center justify-center border-2 border-dashed rounded-xl p-8 cursor-pointer transition-all">
|
||
<input type="file" class="hidden" accept=".pdf,.doc,.docx" @change="onKnowledgeSelect($event)">
|
||
<div x-show="!knowledgeUploading && knowledgeProcessing.length === 0">
|
||
<svg class="w-10 h-10 text-teal-300 mx-auto mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||
</svg>
|
||
<p class="text-sm font-medium text-gray-600 text-center">拖拽文件到此处,或点击选择</p>
|
||
<p class="text-xs text-gray-400 mt-1 text-center">支持 PDF、DOC、DOCX 格式</p>
|
||
</div>
|
||
<div x-show="knowledgeUploading" class="flex flex-col items-center">
|
||
<div class="w-8 h-8 border-3 border-teal-200 border-t-teal-500 rounded-full animate-spin mb-3"></div>
|
||
<p class="text-sm text-teal-600">正在上传文件...</p>
|
||
</div>
|
||
<div x-show="!knowledgeUploading && knowledgeProcessing.length > 0" class="flex flex-col items-center">
|
||
<div class="w-8 h-8 border-3 border-teal-200 border-t-teal-500 rounded-full animate-spin mb-3"></div>
|
||
<p class="text-sm text-teal-600">AI 正在向量化文档,请稍候...</p>
|
||
<p class="text-xs text-gray-400 mt-1" x-text="'处理中:' + knowledgeProcessing.join(', ')"></p>
|
||
</div>
|
||
</label>
|
||
|
||
<p class="text-xs text-gray-400 mt-3">
|
||
推荐上传:历史技术方案、同类项目标书、企业资质简介、施工工法说明等。<br>
|
||
上传后 AI 在生成章节内容时将自动检索相关片段作为写作参考。
|
||
</p>
|
||
</div>
|
||
|
||
<!-- 文件列表 -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-5">
|
||
<h3 class="text-sm font-semibold text-gray-700 mb-4 flex items-center gap-2">
|
||
<svg class="w-4 h-4 text-teal-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||
</svg>
|
||
已上传文件
|
||
<span class="ml-auto text-xs font-normal text-gray-400" x-text="knowledgeFiles.length + ' 个文件'"></span>
|
||
</h3>
|
||
|
||
<!-- 处理中的文件(还未出现在列表里) -->
|
||
<template x-for="fn in knowledgeProcessing.filter(fn => !knowledgeFiles.some(f => f.name === fn))" :key="fn">
|
||
<div class="flex items-center gap-3 px-4 py-3 mb-2 rounded-xl border border-teal-100 bg-teal-50">
|
||
<div class="w-4 h-4 border-2 border-teal-300 border-t-teal-600 rounded-full animate-spin flex-shrink-0"></div>
|
||
<div class="flex-1 min-w-0">
|
||
<p class="text-sm font-medium text-teal-700 truncate" x-text="fn"></p>
|
||
<p class="text-xs text-teal-500">正在向量化入库...</p>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 已完成的文件 -->
|
||
<template x-if="knowledgeFiles.length > 0">
|
||
<div class="space-y-2">
|
||
<template x-for="file in knowledgeFiles" :key="file.name">
|
||
<div class="flex items-center gap-3 px-4 py-3 rounded-xl border border-gray-100 bg-gray-50 hover:bg-gray-100 transition group">
|
||
<div class="w-8 h-8 rounded-lg bg-teal-100 flex items-center justify-center flex-shrink-0">
|
||
<svg class="w-4 h-4 text-teal-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||
</svg>
|
||
</div>
|
||
<div class="flex-1 min-w-0">
|
||
<p class="text-sm font-medium text-gray-800 truncate" x-text="file.name"></p>
|
||
<p class="text-xs text-gray-400" x-text="file.chunks + ' 个文本块 · ' + (file.added_at || '').slice(0, 10)"></p>
|
||
</div>
|
||
<button @click.stop="deleteKnowledgeFile(file.name)"
|
||
class="opacity-0 group-hover:opacity-100 transition flex-shrink-0 p-1.5 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded-lg">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- 空状态 -->
|
||
<div x-show="knowledgeFiles.length === 0 && knowledgeProcessing.length === 0"
|
||
class="py-10 flex flex-col items-center text-gray-300">
|
||
<svg class="w-12 h-12 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
||
</svg>
|
||
<p class="text-sm">知识库暂无文件</p>
|
||
<p class="text-xs mt-1">上传历史标书后,AI 生成内容时将自动引用</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 使用说明 -->
|
||
<div class="bg-gradient-to-br from-teal-50 to-cyan-50 rounded-2xl border border-teal-100 p-5">
|
||
<h3 class="text-sm font-semibold text-teal-800 mb-3 flex items-center gap-2">
|
||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||
</svg>
|
||
使用说明
|
||
</h3>
|
||
<ul class="space-y-2 text-xs text-teal-700">
|
||
<li class="flex items-start gap-2">
|
||
<span class="flex-shrink-0 w-4 h-4 rounded-full bg-teal-200 text-teal-800 flex items-center justify-center font-bold text-[10px] mt-0.5">1</span>
|
||
<span>上传企业历史技术标书、施工方案、资质简介等文档(支持 PDF/DOC/DOCX)</span>
|
||
</li>
|
||
<li class="flex items-start gap-2">
|
||
<span class="flex-shrink-0 w-4 h-4 rounded-full bg-teal-200 text-teal-800 flex items-center justify-center font-bold text-[10px] mt-0.5">2</span>
|
||
<span>系统自动将文档切分并向量化入库(首次入库需等待 AI 处理完成)</span>
|
||
</li>
|
||
<li class="flex items-start gap-2">
|
||
<span class="flex-shrink-0 w-4 h-4 rounded-full bg-teal-200 text-teal-800 flex items-center justify-center font-bold text-[10px] mt-0.5">3</span>
|
||
<span>生成章节内容(步骤 3)时,系统将自动检索知识库中最相关的段落供 AI 参考写作</span>
|
||
</li>
|
||
<li class="flex items-start gap-2">
|
||
<span class="flex-shrink-0 w-4 h-4 rounded-full bg-teal-200 text-teal-800 flex items-center justify-center font-bold text-[10px] mt-0.5">4</span>
|
||
<span>知识库为全局共享,对所有项目均有效;可随时添加或删除文档</span>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</main>
|
||
</div>
|
||
|
||
<!-- 一级大纲输入对话框 -->
|
||
<div x-show="showOutlinePasteDialog" x-cloak class="fixed inset-0 z-[80] bg-black/40 flex items-center justify-center p-4"
|
||
@click.self="closeOutlinePasteDialog()">
|
||
<div class="w-full max-w-3xl bg-white rounded-2xl shadow-2xl border border-gray-200">
|
||
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
|
||
<h3 class="text-base font-semibold text-gray-800">粘贴或编辑一级大纲</h3>
|
||
<button @click="closeOutlinePasteDialog()" class="text-gray-400 hover:text-gray-600 text-lg leading-none">×</button>
|
||
</div>
|
||
<div class="px-6 py-4 space-y-3">
|
||
<p class="text-xs text-gray-500">
|
||
请输入一级大纲(每行一个一级章节,支持“附件四、xxx / 附图、xxx / 附表、xxx”)。
|
||
系统将自动标准化编号并执行 AI 自动细化小章节(包含附件部分)。
|
||
</p>
|
||
<textarea x-model="outlinePasteText" rows="14"
|
||
class="w-full px-4 py-3 border border-emerald-300 rounded-xl text-sm leading-6 focus:outline-none focus:ring-2 focus:ring-emerald-500 resize-y font-mono"
|
||
placeholder="示例: 一、施工部署 二、施工进度计划 附件一、施工平面布置图 附件二、临时用地表"></textarea>
|
||
</div>
|
||
<div class="px-6 py-4 border-t border-gray-100 flex items-center justify-end gap-2">
|
||
<button @click="closeOutlinePasteDialog()"
|
||
class="px-4 py-2 text-sm border border-gray-200 text-gray-600 hover:bg-gray-50 rounded-lg transition">取消</button>
|
||
<button @click="confirmOutlinePasteAndBuild()"
|
||
:disabled="buildingOutlineAssistant"
|
||
class="px-4 py-2 text-sm bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg transition disabled:opacity-60">
|
||
<span x-show="!buildingOutlineAssistant">确认并执行 AI 细化</span>
|
||
<span x-show="buildingOutlineAssistant" class="flex items-center gap-1.5">
|
||
<span class="w-3.5 h-3.5 border-2 border-white/40 border-t-white rounded-full animate-spin"></span>
|
||
执行中...
|
||
</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 底部导航(移动端) -->
|
||
<nav class="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 flex md:hidden z-40">
|
||
<template x-for="(step, i) in steps" :key="i">
|
||
<button @click="activeTab=step.key" class="flex-1 py-3 flex flex-col items-center gap-0.5"
|
||
:class="activeTab===step.key ? 'text-blue-600' : 'text-gray-400'">
|
||
<span class="text-lg" x-text="step.icon"></span>
|
||
<span class="text-xs" x-text="step.label"></span>
|
||
</button>
|
||
</template>
|
||
</nav>
|
||
|
||
<script>
|
||
const PROJECT_ID = {{ project.id }}
|
||
|
||
function projectApp(projectId) {
|
||
return {
|
||
projectId,
|
||
activeTab: 'parse',
|
||
project: {},
|
||
sections: [],
|
||
selectedSection: null,
|
||
sectionContent: null,
|
||
editMode: false,
|
||
editContent: '',
|
||
progress: { total: 0, done: 0, running: 0, errors: 0, percent: 0 },
|
||
checkResult: null,
|
||
checking: false,
|
||
darkBidCheckEnabled: false,
|
||
darkBidHtml: '',
|
||
darkBidChecking: false,
|
||
darkBidCheckResult: null,
|
||
darkBidFileError: '',
|
||
exporting: false,
|
||
parsing: false,
|
||
uploading: false,
|
||
dragover: false,
|
||
knowledgeFiles: [],
|
||
knowledgeStatus: null,
|
||
knowledgeUploading: false,
|
||
knowledgeDragover: false,
|
||
knowledgeProcessing: [],
|
||
knowledgePollTimer: null,
|
||
pollTimer: null,
|
||
editSummary: false,
|
||
editSummaryText: '',
|
||
editRating: false,
|
||
editRatingText: '',
|
||
savingTenderData: false,
|
||
uploadingBoq: false,
|
||
parsingBoq: false,
|
||
editBoq: false,
|
||
editBoqText: '',
|
||
tenderKind: 'engineering',
|
||
savingBoq: false,
|
||
showBoqAnalysisDetail: false,
|
||
editOutline: false,
|
||
editOutlineText: '',
|
||
savingOutline: false,
|
||
expandingOutline: false,
|
||
buildingOutlineAssistant: false,
|
||
showOutlinePasteDialog: false,
|
||
outlinePasteText: '',
|
||
outlineDirty: false,
|
||
genMode: 'auto',
|
||
chatMessages: [],
|
||
chatInput: '',
|
||
chatLoading: false,
|
||
chatSectionId: null,
|
||
showAnonPanel: false,
|
||
anonEnabled: false,
|
||
anonText: '',
|
||
savingAnon: false,
|
||
showDiagramPanel: false,
|
||
enableFigure: false,
|
||
enableTable: false,
|
||
savingDiagram: false,
|
||
targetPages: 0,
|
||
customTargetPages: 150,
|
||
/** 为 true 时 expand-outline 不限制小章节;否则在页数为 0 时由服务端回退 100 页/约 70–86 条 */
|
||
noSubchapterLimit: false,
|
||
savingPageSettings: false,
|
||
steps: [
|
||
{ key: 'parse', label: '解析', icon: '📄' },
|
||
{ key: 'outline', label: '大纲', icon: '📋' },
|
||
{ key: 'content', label: '生成', icon: '✍️' },
|
||
{ key: 'editor', label: '编辑', icon: '📝' },
|
||
{ key: 'check', label: '检查', icon: '✅' },
|
||
{ key: 'knowledge', label: '知识库', icon: '🗂️' },
|
||
],
|
||
|
||
async init() {
|
||
await this.loadProject(true)
|
||
await this.loadPageSettings()
|
||
await this.loadSections()
|
||
await this.loadKnowledge()
|
||
await this.loadKnowledgeStatus()
|
||
this.startPolling()
|
||
},
|
||
|
||
boqStructuredItemCount() {
|
||
const a = this.project.boq_analysis
|
||
if (!a || !a.categories) return 0
|
||
return a.categories.reduce((s, c) => s + ((c.items && c.items.length) || 0), 0)
|
||
},
|
||
boqStructuredCatCount() {
|
||
const a = this.project.boq_analysis
|
||
if (!a || !a.categories) return 0
|
||
return a.categories.length
|
||
},
|
||
|
||
async loadProject(isInitial = false) {
|
||
const localOutlineDraft = this.editOutline && this.outlineDirty ? this.editOutlineText : ''
|
||
const res = await fetch(`/api/projects/${this.projectId}`)
|
||
this.project = await res.json()
|
||
if (localOutlineDraft) {
|
||
// 编辑未保存时,保护草稿不被轮询数据覆盖
|
||
this.editOutlineText = localOutlineDraft
|
||
}
|
||
this.tenderKind = this.project.tender_kind || 'engineering'
|
||
this.parsing = this.project.parse_status === 'parsing'
|
||
// 只在首次加载时同步用户可编辑的设置状态,轮询更新时不覆盖,
|
||
// 避免用户改动开关后被下一次 poll 自动还原
|
||
if (isInitial) {
|
||
const anon = this.project.anon_requirements || ''
|
||
this.anonText = anon
|
||
this.anonEnabled = anon.trim().length > 0
|
||
this.enableFigure = !!this.project.enable_figure
|
||
this.enableTable = !!this.project.enable_table
|
||
}
|
||
// BOQ 解析状态:轮询中同步(完成/出错时停止 loading)
|
||
if (this.parsingBoq && this.project.boq_status !== 'parsing') {
|
||
this.parsingBoq = false
|
||
}
|
||
},
|
||
|
||
async loadSections() {
|
||
const res = await fetch(`/api/projects/${this.projectId}/sections`)
|
||
const data = await res.json()
|
||
this.sections = data.sections || []
|
||
await this.updateProgress()
|
||
},
|
||
|
||
async loadKnowledge() {
|
||
const res = await fetch('/api/knowledge/files')
|
||
const data = await res.json()
|
||
this.knowledgeFiles = data.files || []
|
||
},
|
||
|
||
async loadKnowledgeStatus() {
|
||
try {
|
||
const res = await fetch('/api/knowledge/status')
|
||
this.knowledgeStatus = await res.json()
|
||
this.knowledgeProcessing = (this.knowledgeStatus && this.knowledgeStatus.processing) || []
|
||
} catch (e) {
|
||
this.knowledgeStatus = { available: false, reason: '无法连接服务器' }
|
||
}
|
||
},
|
||
|
||
// 知识库:拖拽上传
|
||
onKnowledgeDrop(event) {
|
||
this.knowledgeDragover = false
|
||
const file = event.dataTransfer.files[0]
|
||
if (file) this.uploadKnowledgeFile(file)
|
||
},
|
||
onKnowledgeSelect(event) {
|
||
const file = event.target.files[0]
|
||
if (file) this.uploadKnowledgeFile(file)
|
||
event.target.value = ''
|
||
},
|
||
|
||
async uploadKnowledgeFile(file) {
|
||
this.knowledgeUploading = true
|
||
const fd = new FormData()
|
||
fd.append('file', file)
|
||
try {
|
||
const res = await fetch('/api/knowledge/upload', { method: 'POST', body: fd })
|
||
const data = await res.json()
|
||
if (data.success || data.queued) {
|
||
// 上传后台处理中,轮询直到文件出现在列表
|
||
this._startKnowledgePoll(data.filename)
|
||
} else {
|
||
alert('上传失败: ' + (data.error || '未知错误'))
|
||
}
|
||
} catch (e) {
|
||
alert('上传失败: ' + e.message)
|
||
} finally {
|
||
this.knowledgeUploading = false
|
||
}
|
||
},
|
||
|
||
_startKnowledgePoll(filename) {
|
||
if (this.knowledgePollTimer) clearInterval(this.knowledgePollTimer)
|
||
this.knowledgeProcessing = [...new Set([...this.knowledgeProcessing, filename])]
|
||
this.knowledgePollTimer = setInterval(async () => {
|
||
await this.loadKnowledge()
|
||
await this.loadKnowledgeStatus()
|
||
// 当处理中的文件出现在列表里,视为完成
|
||
const done = this.knowledgeProcessing.filter(
|
||
fn => this.knowledgeFiles.some(f => f.name === fn)
|
||
)
|
||
if (done.length > 0) {
|
||
this.knowledgeProcessing = this.knowledgeProcessing.filter(fn => !done.includes(fn))
|
||
}
|
||
if (this.knowledgeProcessing.length === 0) {
|
||
clearInterval(this.knowledgePollTimer)
|
||
this.knowledgePollTimer = null
|
||
}
|
||
}, 2000)
|
||
},
|
||
|
||
async deleteKnowledgeFile(fileName) {
|
||
if (!confirm(`确定删除知识库文件「${fileName}」?此操作不可恢复。`)) return
|
||
const res = await fetch('/api/knowledge/delete', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ file_name: fileName })
|
||
})
|
||
const data = await res.json()
|
||
if (data.success) {
|
||
await this.loadKnowledge()
|
||
await this.loadKnowledgeStatus()
|
||
} else {
|
||
alert('删除失败: ' + (data.error || ''))
|
||
}
|
||
},
|
||
|
||
async updateProgress() {
|
||
if (this.sections.length === 0) return
|
||
const res = await fetch(`/api/projects/${this.projectId}/section-progress`)
|
||
this.progress = await res.json()
|
||
},
|
||
|
||
async loadPageSettings() {
|
||
try {
|
||
const ptp = parseInt(this.project && this.project.target_pages, 10)
|
||
if (!Number.isNaN(ptp) && ptp > 0) {
|
||
this.targetPages = ptp
|
||
this.customTargetPages = ptp
|
||
return
|
||
}
|
||
const res = await fetch('/api/config')
|
||
const cfg = await res.json()
|
||
this.targetPages = parseInt(cfg.target_pages || 0, 10) || 0
|
||
this.customTargetPages = this.targetPages > 0 ? this.targetPages : 150
|
||
} catch (e) {
|
||
// 忽略读取失败,沿用默认值
|
||
}
|
||
},
|
||
|
||
setPagePreset(pages) {
|
||
this.targetPages = pages
|
||
if (pages > 0) this.customTargetPages = pages
|
||
},
|
||
|
||
applyCustomPages() {
|
||
const n = parseInt(this.customTargetPages || 0, 10)
|
||
if (!n || n < 1) {
|
||
alert('请输入有效页数')
|
||
return
|
||
}
|
||
this.targetPages = n
|
||
},
|
||
|
||
/** 与 utils.volume_chapters 中全标小章节行合计公式一致,仅作界面说明(实际随机在后端) */
|
||
subchapterCountHintText() {
|
||
if (this.noSubchapterLimit) {
|
||
return '已勾选「不限制小章节」:本次将不按页数限幅,章节数可能达到数百。'
|
||
}
|
||
const p = parseInt(this.targetPages || 0, 10) || 0
|
||
if (p <= 0) {
|
||
return '未选目标页时:填充仍按「默认 100 页」估算小章节(全标小章节行约 70~86),避免一次生成数百节。请勾选「不限制」以关闭。'
|
||
}
|
||
const m = 0.67
|
||
const b = 11
|
||
const nBase = m * p + b
|
||
const mid = Math.round(nBase)
|
||
const lo = Math.round(nBase * 0.9)
|
||
const hi = Math.round(nBase * 1.1)
|
||
return `全标子章节行合计约 ${lo}–${hi} 条(中值约 ${mid},由 0.67×页+11 再经 ±10% 随机后取整,并分配到各主章)。`
|
||
},
|
||
|
||
/**
|
||
* @param { { silent?: boolean } } [options] 注:JSDoc 中双花括号需拆开写,避免被 Jinja 当作模板
|
||
* @returns {Promise<boolean>} 成功为 true
|
||
*/
|
||
async savePageSettings(options) {
|
||
const silent = options && options.silent
|
||
if (!silent) this.savingPageSettings = true
|
||
try {
|
||
const pages = parseInt(this.targetPages || 0, 10) || 0
|
||
const res = await fetch('/api/config', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ target_pages: pages })
|
||
})
|
||
const data = await res.json()
|
||
if (!data.success) {
|
||
if (silent) console.error('savePageSettings', data.error || '')
|
||
else alert('保存失败: ' + (data.error || ''))
|
||
return false
|
||
}
|
||
if (!silent) {
|
||
alert('页数已保存(同步全局并写入本标书)。在「步骤1 解析」→「篇幅目标」可随时修改。')
|
||
}
|
||
const r2 = await fetch(`/api/projects/${this.projectId}/tender-data`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ target_pages: pages })
|
||
})
|
||
const d2 = await r2.json()
|
||
if (!r2.ok || d2.error) {
|
||
const err = (d2 && d2.error) || r2.statusText || '请求失败'
|
||
if (silent) console.error('target_pages 写入项目失败', err)
|
||
else alert('项目级页数保存失败: ' + err)
|
||
return false
|
||
}
|
||
if (this.project) this.project.target_pages = pages
|
||
return true
|
||
} catch (e) {
|
||
if (silent) console.error('savePageSettings', e)
|
||
else alert('保存失败: ' + e.message)
|
||
return false
|
||
} finally {
|
||
if (!silent) this.savingPageSettings = false
|
||
}
|
||
},
|
||
|
||
startPolling() {
|
||
const poll = async () => {
|
||
const prevParse = this.project.parse_status
|
||
const prevOutline = this.project.outline_status
|
||
await this.loadProject()
|
||
await this.loadSections()
|
||
|
||
if (prevParse === 'parsing' && this.project.parse_status === 'done') {
|
||
this.parsing = false
|
||
}
|
||
// 按需求:步骤切换仅允许用户手动点击,不做自动跳转
|
||
// if (prevOutline === 'outline_generating' && this.project.outline_status === 'outline_done') {
|
||
// this.activeTab = 'content'
|
||
// }
|
||
|
||
if (this.selectedSection) {
|
||
const updated = this.sections.find(s => s.id === this.selectedSection.id)
|
||
if (updated) {
|
||
const wasGenerating = this.selectedSection.status === 'generating'
|
||
this.selectedSection = updated
|
||
if (wasGenerating && updated.status === 'done') {
|
||
await this.loadSectionContent(updated.id)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Adaptive interval: fast when busy, slow when idle
|
||
const busy = this.progress.running > 0 || this.parsing || this.project.outline_status === 'outline_generating'
|
||
const interval = busy ? 1500 : 5000
|
||
this.pollTimer = setTimeout(poll, interval)
|
||
}
|
||
this.pollTimer = setTimeout(poll, 2000)
|
||
},
|
||
|
||
// ── 文件操作 ──
|
||
onDrop(event) {
|
||
this.dragover = false
|
||
const file = event.dataTransfer.files[0]
|
||
if (file) this.uploadFile(file)
|
||
},
|
||
|
||
onFileSelect(event) {
|
||
const file = event.target.files[0]
|
||
if (file) this.uploadFile(file)
|
||
},
|
||
|
||
async uploadFile(file) {
|
||
this.uploading = true
|
||
const fd = new FormData()
|
||
fd.append('file', file)
|
||
const res = await fetch(`/api/projects/${this.projectId}/upload`, { method: 'POST', body: fd })
|
||
const data = await res.json()
|
||
this.uploading = false
|
||
if (data.success) {
|
||
this.project.file_name = data.file_name
|
||
this.project.parse_status = 'uploaded'
|
||
} else {
|
||
alert('上传失败: ' + (data.error || ''))
|
||
}
|
||
},
|
||
|
||
async parseTender() {
|
||
this.parsing = true
|
||
this.project.parse_status = 'parsing'
|
||
const res = await fetch(`/api/projects/${this.projectId}/parse`, { method: 'POST' })
|
||
const data = await res.json()
|
||
if (!data.success) {
|
||
this.parsing = false
|
||
alert('启动解析失败: ' + (data.error || ''))
|
||
}
|
||
},
|
||
|
||
// ── 保存手动编辑的解析结果 ──
|
||
async saveTenderData(field) {
|
||
this.savingTenderData = true
|
||
const payload = {}
|
||
if (field === 'summary') {
|
||
payload.summary = this.editSummaryText
|
||
} else if (field === 'rating') {
|
||
payload.rating_requirements = this.editRatingText
|
||
} else if (field === 'kind') {
|
||
payload.tender_kind = this.tenderKind
|
||
}
|
||
try {
|
||
const res = await fetch(`/api/projects/${this.projectId}/tender-data`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload)
|
||
})
|
||
const data = await res.json()
|
||
if (data.success) {
|
||
if (field === 'summary') {
|
||
this.project.summary = this.editSummaryText
|
||
this.editSummary = false
|
||
} else if (field === 'rating') {
|
||
this.project.rating_requirements = this.editRatingText
|
||
this.editRating = false
|
||
} else if (field === 'kind') {
|
||
this.project.tender_kind = this.tenderKind
|
||
}
|
||
} else {
|
||
alert('保存失败: ' + (data.error || ''))
|
||
}
|
||
} catch (e) {
|
||
alert('保存失败: ' + e.message)
|
||
} finally {
|
||
this.savingTenderData = false
|
||
}
|
||
},
|
||
|
||
// ── 工程量清单 ──
|
||
async uploadBoqFile(file) {
|
||
this.uploadingBoq = true
|
||
const fd = new FormData()
|
||
fd.append('file', file)
|
||
try {
|
||
const res = await fetch(`/api/projects/${this.projectId}/upload-boq`, { method: 'POST', body: fd })
|
||
const data = await res.json()
|
||
if (data.success) {
|
||
this.project.boq_file_name = data.file_name
|
||
this.project.boq_status = 'uploaded'
|
||
} else {
|
||
alert('上传失败: ' + (data.error || ''))
|
||
}
|
||
} catch (e) {
|
||
alert('上传失败: ' + e.message)
|
||
} finally {
|
||
this.uploadingBoq = false
|
||
}
|
||
},
|
||
|
||
async parseBoq() {
|
||
this.parsingBoq = true
|
||
this.project.boq_status = 'parsing'
|
||
try {
|
||
const res = await fetch(`/api/projects/${this.projectId}/parse-boq`, { method: 'POST' })
|
||
const data = await res.json()
|
||
if (!data.success) alert('解析启动失败: ' + (data.error || ''))
|
||
} catch (e) {
|
||
alert('解析失败: ' + e.message)
|
||
this.parsingBoq = false
|
||
}
|
||
},
|
||
|
||
async saveBoqSummary() {
|
||
this.savingBoq = true
|
||
try {
|
||
await fetch(`/api/projects/${this.projectId}/boq`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ boq_summary: this.editBoqText })
|
||
})
|
||
this.project.boq_summary = this.editBoqText
|
||
this.editBoq = false
|
||
} catch (e) {
|
||
alert('保存失败: ' + e.message)
|
||
} finally {
|
||
this.savingBoq = false
|
||
}
|
||
},
|
||
|
||
// ── 保存手动编辑的大纲 ──
|
||
async saveOutline() {
|
||
if (!this.editOutlineText.trim()) return
|
||
const hasDone = this.sections.some(s => s.status === 'done')
|
||
if (hasDone && !confirm('已有章节内容生成完成,保存新大纲后这些内容将被清除,确定继续?')) return
|
||
this.savingOutline = true
|
||
try {
|
||
const res = await fetch(`/api/projects/${this.projectId}/outline`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ outline: this.editOutlineText })
|
||
})
|
||
const data = await res.json()
|
||
if (data.success) {
|
||
// 用后端重排序号后的规范文本更新本地,下次打开编辑器显示正确编号
|
||
this.project.outline = data.normalized_outline || this.editOutlineText
|
||
this.editOutlineText = this.project.outline
|
||
this.outlineDirty = false
|
||
this.editOutline = false
|
||
await this.loadProject()
|
||
await this.loadSections()
|
||
alert(`大纲已保存并用于生成(章节数:${data.section_count || this.sections.length},持久化长度:${data.persisted_outline_len || ((this.project && this.project.outline) ? this.project.outline.length : 0)})`)
|
||
} else {
|
||
alert('保存失败: ' + (data.error || ''))
|
||
}
|
||
} catch (e) {
|
||
alert('保存失败: ' + e.message)
|
||
} finally {
|
||
this.savingOutline = false
|
||
}
|
||
},
|
||
|
||
// ── 大纲生成 ──
|
||
async generateOutline() {
|
||
const hasExistingOutline = !!(this.project.outline && this.project.outline.trim())
|
||
let force = false
|
||
if (hasExistingOutline) {
|
||
if (!confirm('当前项目已有大纲,重新生成会覆盖现有大纲。是否继续覆盖?')) return
|
||
force = true
|
||
}
|
||
this.project.outline_status = 'outline_generating'
|
||
try {
|
||
const res = await fetch(`/api/projects/${this.projectId}/generate-outline`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ force })
|
||
})
|
||
let data = {}
|
||
try {
|
||
data = await res.json()
|
||
} catch (e) {
|
||
await this.loadProject()
|
||
alert('服务器响应异常,请重试。')
|
||
return
|
||
}
|
||
if (!res.ok) {
|
||
await this.loadProject()
|
||
alert('启动失败: ' + (data.error || res.statusText || 'HTTP ' + res.status))
|
||
return
|
||
}
|
||
if (!data.success) {
|
||
await this.loadProject()
|
||
alert('启动失败: ' + (data.error || ''))
|
||
return
|
||
}
|
||
await this.loadProject()
|
||
await this.loadSections()
|
||
} catch (e) {
|
||
await this.loadProject()
|
||
alert('启动失败: ' + (e.message || String(e)))
|
||
}
|
||
},
|
||
|
||
enterEditOutline() {
|
||
// 若已有未保存编辑稿,进入编辑模式时保持草稿,不回灌旧目录
|
||
if (this.outlineDirty && this.editOutlineText && this.editOutlineText.trim()) {
|
||
this.editOutline = true
|
||
return
|
||
}
|
||
this.editOutline = true
|
||
this.editOutlineText = this.project.outline || ''
|
||
this.outlineDirty = false
|
||
},
|
||
|
||
cancelEditOutline() {
|
||
this.editOutline = false
|
||
this.editOutlineText = this.project.outline || ''
|
||
this.outlineDirty = false
|
||
},
|
||
|
||
async expandOutline() {
|
||
if (!this.editOutlineText.trim()) return
|
||
this.expandingOutline = true
|
||
try {
|
||
const saved = await this.savePageSettings({ silent: true })
|
||
if (saved === false) {
|
||
alert('页数设置未能保存到服务器,请检查网络后重试。')
|
||
return
|
||
}
|
||
const res = await fetch(`/api/projects/${this.projectId}/expand-outline`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
outline: this.editOutlineText,
|
||
target_pages: parseInt(this.targetPages, 10) || 0,
|
||
no_subchapter_limit: !!this.noSubchapterLimit
|
||
})
|
||
})
|
||
const data = await res.json()
|
||
if (data.success) {
|
||
const normalized = data.normalized_outline || data.expanded_outline || this.editOutlineText
|
||
this.project.outline = normalized
|
||
this.editOutlineText = normalized
|
||
this.outlineDirty = false
|
||
await this.loadProject()
|
||
await this.loadSections()
|
||
const u = (data.used_target_pages !== undefined && data.used_target_pages !== null)
|
||
? (data.no_subchapter_limit ? '不限制' : (data.used_target_pages + ' 页限幅'))
|
||
: ''
|
||
const umsg = u ? `,小章节策略:${u}` : ''
|
||
alert(`AI填充已保存并用于生成(章节数:${data.section_count || this.sections.length},持久化长度:${data.persisted_outline_len || ((this.project && this.project.outline) ? this.project.outline.length : 0)}${umsg})`)
|
||
} else {
|
||
alert('AI填充失败: ' + (data.error || ''))
|
||
}
|
||
} catch (e) {
|
||
alert('AI填充失败: ' + e.message)
|
||
} finally {
|
||
this.expandingOutline = false
|
||
}
|
||
},
|
||
|
||
async waitOutlineReady(maxWaitMs = 120000) {
|
||
const start = Date.now()
|
||
while (Date.now() - start < maxWaitMs) {
|
||
const res = await fetch(`/api/projects/${this.projectId}/outline-status`)
|
||
const data = await res.json()
|
||
const status = data.status || 'none'
|
||
if (status === 'outline_done') return true
|
||
if (status === 'outline_error') {
|
||
alert('大纲生成失败: ' + (data.error || '未知错误'))
|
||
return false
|
||
}
|
||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||
}
|
||
alert('等待大纲生成超时,请稍后重试。')
|
||
return false
|
||
},
|
||
|
||
normalizeFirstLevelOutline(rawText) {
|
||
const lines = (rawText || '')
|
||
.split('\n')
|
||
.map(x => x.trim())
|
||
.filter(Boolean)
|
||
const cn = ['一','二','三','四','五','六','七','八','九','十','十一','十二','十三','十四','十五','十六','十七','十八','十九','二十']
|
||
const firstLevel = []
|
||
for (const line of lines) {
|
||
let t = line
|
||
.replace(/^[\-\*\•\·]\s*/, '')
|
||
.replace(/^\d+\s*[\.、.]\s*/, '')
|
||
.replace(/^第?[一二三四五六七八九十百千]+\s*[章节篇卷]?\s*[、。..]?\s*/, '')
|
||
.trim()
|
||
if (!t) continue
|
||
// 附件/附图/附表保持用户语义,不强制重写编号前缀
|
||
if (/^(附件|附图|附表)/.test(t)) {
|
||
firstLevel.push(t)
|
||
} else {
|
||
firstLevel.push(t)
|
||
}
|
||
}
|
||
if (!firstLevel.length) return ''
|
||
const bidTitle = (this.project && this.project.name ? `${this.project.name}技术标书` : '技术标书')
|
||
const normalized = [bidTitle]
|
||
firstLevel.forEach((t, i) => {
|
||
if (/^(附件|附图|附表)/.test(t)) {
|
||
normalized.push(t)
|
||
} else {
|
||
const n = cn[i] || String(i + 1)
|
||
normalized.push(`${n}、${t}`)
|
||
}
|
||
})
|
||
return normalized.join('\n')
|
||
},
|
||
|
||
openOutlinePasteDialog() {
|
||
if (this.project.parse_status !== 'done') {
|
||
alert('请先完成步骤1解析后再执行。')
|
||
return
|
||
}
|
||
const src = (this.editOutline && this.outlineDirty && this.editOutlineText && this.editOutlineText.trim())
|
||
? this.editOutlineText
|
||
: (this.project.outline || '')
|
||
this.outlinePasteText = src
|
||
this.showOutlinePasteDialog = true
|
||
},
|
||
|
||
closeOutlinePasteDialog() {
|
||
if (this.buildingOutlineAssistant) return
|
||
this.showOutlinePasteDialog = false
|
||
},
|
||
|
||
async saveOutlineForAssistant(outlineText) {
|
||
this.savingOutline = true
|
||
try {
|
||
const res = await fetch(`/api/projects/${this.projectId}/outline`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ outline: outlineText })
|
||
})
|
||
const data = await res.json()
|
||
if (!res.ok || !data.success) {
|
||
alert('一级大纲保存失败: ' + (data.error || res.statusText || '未知错误'))
|
||
return false
|
||
}
|
||
this.project.outline = data.normalized_outline || outlineText
|
||
this.editOutline = true
|
||
this.editOutlineText = this.project.outline
|
||
this.outlineDirty = false
|
||
await this.loadProject()
|
||
await this.loadSections()
|
||
return true
|
||
} catch (e) {
|
||
alert('一级大纲保存失败: ' + (e.message || String(e)))
|
||
return false
|
||
} finally {
|
||
this.savingOutline = false
|
||
}
|
||
},
|
||
|
||
async confirmOutlinePasteAndBuild() {
|
||
if (this.buildingOutlineAssistant) return
|
||
this.buildingOutlineAssistant = true
|
||
try {
|
||
if (this.project.outline_status === 'outline_generating') {
|
||
alert('大纲正在生成中,请稍候。')
|
||
return
|
||
}
|
||
const normalized = this.normalizeFirstLevelOutline(this.outlinePasteText)
|
||
if (!normalized.trim()) {
|
||
alert('请先粘贴或输入一级大纲内容。')
|
||
return
|
||
}
|
||
const ok = await this.saveOutlineForAssistant(normalized)
|
||
if (!ok) return
|
||
this.showOutlinePasteDialog = false
|
||
if (!this.editOutlineText || !this.editOutlineText.trim()) {
|
||
this.editOutlineText = normalized
|
||
}
|
||
if (!this.editOutline) this.editOutline = true
|
||
if (!this.editOutlineText.trim()) {
|
||
alert('大纲内容为空,无法执行 AI 自动细化。')
|
||
return
|
||
}
|
||
await this.expandOutline()
|
||
} catch (e) {
|
||
alert('建立失败: ' + (e.message || String(e)))
|
||
} finally {
|
||
this.buildingOutlineAssistant = false
|
||
}
|
||
},
|
||
|
||
// 兼容旧入口:直接打开一级大纲对话框
|
||
async buildOutlineAssistant() {
|
||
this.openOutlinePasteDialog()
|
||
},
|
||
|
||
// ── 章节生成 ──
|
||
async generateSection(sectionId) {
|
||
const s = this.sections.find(x => x.id === sectionId)
|
||
if (s) s.status = 'generating'
|
||
if (this.selectedSection && this.selectedSection.id === sectionId) {
|
||
this.selectedSection.status = 'generating'
|
||
}
|
||
const res = await fetch(`/api/projects/${this.projectId}/generate-section`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ section_id: sectionId })
|
||
})
|
||
await this.updateProgress()
|
||
},
|
||
|
||
async generateAllSections() {
|
||
const pending = this.progress.total - this.progress.done - this.progress.errors
|
||
if (!confirm(`将并发生成 ${pending > 0 ? pending : this.progress.total} 个章节内容,确定继续?`)) return
|
||
const res = await fetch(`/api/projects/${this.projectId}/generate-all-sections`, { method: 'POST' })
|
||
const data = await res.json()
|
||
if (!data.success) alert('启动失败: ' + (data.error || ''))
|
||
},
|
||
|
||
// ── 章节选择与编辑 ──
|
||
async selectSection(s) {
|
||
if (this.chatSectionId !== s.id) {
|
||
this.chatMessages = []
|
||
this.chatSectionId = s.id
|
||
}
|
||
this.selectedSection = s
|
||
this.editMode = false
|
||
await this.loadSectionContent(s.id)
|
||
},
|
||
|
||
switchGenMode(mode) {
|
||
this.genMode = mode
|
||
if (mode === 'chat' && this.chatMessages.length === 0 && this.selectedSection) {
|
||
this.chatMessages = [{
|
||
role: 'assistant',
|
||
content: `您好!我可以协助您以对话方式生成「${this.selectedSection.title}」的章节内容。\n\n您可以这样提问:\n· 请生成该章节完整内容\n· 重点强调××方面的技术措施\n· 帮我补充关于××的具体数据和标准编号\n· 将刚才的内容改得更具体一些\n\n每条 AI 回复下方有「采用此内容」按钮,点击后内容将填入左侧编辑框。`,
|
||
id: Date.now(),
|
||
isWelcome: true
|
||
}]
|
||
}
|
||
},
|
||
|
||
async sendChatMessage() {
|
||
const text = this.chatInput.trim()
|
||
if (!text || this.chatLoading || !this.selectedSection) return
|
||
const userMsg = { role: 'user', content: text, id: Date.now() }
|
||
this.chatMessages.push(userMsg)
|
||
this.chatInput = ''
|
||
this.chatLoading = true
|
||
this.$nextTick(() => {
|
||
const el = document.getElementById('chatMsgList')
|
||
if (el) el.scrollTop = el.scrollHeight
|
||
})
|
||
const sendMsgs = this.chatMessages.filter(m => !m.isWelcome)
|
||
try {
|
||
const res = await fetch(
|
||
`/api/projects/${this.projectId}/sections/${this.selectedSection.id}/chat`,
|
||
{
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ messages: sendMsgs })
|
||
}
|
||
)
|
||
const data = await res.json()
|
||
if (data.success) {
|
||
this.chatMessages.push({ role: 'assistant', content: data.content, id: Date.now() })
|
||
} else {
|
||
this.chatMessages.push({ role: 'assistant', content: '生成失败:' + (data.error || '未知错误'), id: Date.now(), isError: true })
|
||
}
|
||
} catch (e) {
|
||
this.chatMessages.push({ role: 'assistant', content: '请求失败:' + e.message, id: Date.now(), isError: true })
|
||
} finally {
|
||
this.chatLoading = false
|
||
this.$nextTick(() => {
|
||
const el = document.getElementById('chatMsgList')
|
||
if (el) el.scrollTop = el.scrollHeight
|
||
})
|
||
}
|
||
},
|
||
|
||
applyChatContent(content) {
|
||
this.editContent = content
|
||
if (this.sectionContent) this.sectionContent.content = content
|
||
this.genMode = 'auto'
|
||
this.editMode = true
|
||
},
|
||
|
||
async loadSectionContent(sectionId) {
|
||
const res = await fetch(`/api/projects/${this.projectId}/sections/${sectionId}`)
|
||
const data = await res.json()
|
||
this.sectionContent = data
|
||
this.editContent = data.content || ''
|
||
},
|
||
|
||
async saveContent() {
|
||
if (!this.selectedSection) return
|
||
await fetch(`/api/projects/${this.projectId}/sections/${this.selectedSection.id}`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ content: this.editContent })
|
||
})
|
||
if (this.sectionContent) this.sectionContent.content = this.editContent
|
||
this.editMode = false
|
||
const s = this.sections.find(x => x.id === this.selectedSection.id)
|
||
if (s) { s.status = 'done'; s.has_content = true }
|
||
},
|
||
|
||
// ── 合规检查 ──
|
||
async runCheck() {
|
||
this.checking = true
|
||
this.checkResult = null
|
||
const res = await fetch(`/api/projects/${this.projectId}/check`, { method: 'POST' })
|
||
const data = await res.json()
|
||
this.checking = false
|
||
if (data.error) {
|
||
alert('检查失败: ' + data.error)
|
||
} else {
|
||
this.checkResult = data
|
||
}
|
||
},
|
||
|
||
onDarkBidFile(event) {
|
||
this.darkBidFileError = ''
|
||
const f = event.target.files && event.target.files[0]
|
||
if (!f) return
|
||
if (!/\.html?$/i.test(f.name)) {
|
||
this.darkBidFileError = '请选择 .html / .htm 文件'
|
||
return
|
||
}
|
||
const reader = new FileReader()
|
||
reader.onload = () => { this.darkBidHtml = reader.result || '' }
|
||
reader.onerror = () => { this.darkBidFileError = '读取文件失败' }
|
||
reader.readAsText(f, 'UTF-8')
|
||
event.target.value = ''
|
||
},
|
||
|
||
async runDarkBidCheck() {
|
||
if (!this.darkBidCheckEnabled) {
|
||
alert('请先勾选「启用清暗标格式检查」')
|
||
return
|
||
}
|
||
const html = (this.darkBidHtml || '').trim()
|
||
if (!html) {
|
||
alert('请粘贴 HTML 或选择 .html 文件')
|
||
return
|
||
}
|
||
this.darkBidChecking = true
|
||
this.darkBidCheckResult = null
|
||
try {
|
||
const res = await fetch(`/api/projects/${this.projectId}/check-dark-bid-format`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ html })
|
||
})
|
||
const data = await res.json()
|
||
if (!res.ok || data.error) {
|
||
alert('清标失败: ' + (data.error || res.statusText || res.status))
|
||
} else {
|
||
this.darkBidCheckResult = data
|
||
}
|
||
} catch (e) {
|
||
alert('清标失败: ' + (e.message || e))
|
||
} finally {
|
||
this.darkBidChecking = false
|
||
}
|
||
},
|
||
|
||
// ── 导出 ──
|
||
async exportDoc() {
|
||
if (this.sections.length === 0) { alert('请先生成大纲'); return }
|
||
this.exporting = true
|
||
try {
|
||
const res = await fetch(`/api/projects/${this.projectId}/export`, { method: 'POST' })
|
||
let data = null
|
||
try {
|
||
data = await res.json()
|
||
} catch (e) {
|
||
const raw = await res.text()
|
||
alert('导出失败:服务返回非 JSON 响应,请检查后端日志。')
|
||
console.error('export non-json response:', raw?.slice?.(0, 500) || raw)
|
||
return
|
||
}
|
||
if (data && data.url) {
|
||
const a = document.createElement('a')
|
||
a.href = data.url
|
||
a.download = data.filename
|
||
a.click()
|
||
} else {
|
||
alert('导出失败: ' + ((data && data.error) || `HTTP ${res.status}`))
|
||
}
|
||
} catch (e) {
|
||
alert('导出失败:网络或服务异常')
|
||
console.error(e)
|
||
} finally {
|
||
this.exporting = false
|
||
}
|
||
},
|
||
|
||
// ── 知识库 ──
|
||
async uploadKnowledge(event) {
|
||
const file = event.target.files[0]
|
||
if (!file) return
|
||
const fd = new FormData()
|
||
fd.append('file', file)
|
||
const res = await fetch('/api/knowledge/upload', { method: 'POST', body: fd })
|
||
const data = await res.json()
|
||
if (data.success) {
|
||
await this.loadKnowledge()
|
||
alert(`上传成功,共处理 ${data.chunks} 个文本块`)
|
||
} else {
|
||
alert('上传失败: ' + (data.error || ''))
|
||
}
|
||
},
|
||
|
||
async deleteKnowledge(fileName) {
|
||
if (!confirm(`删除知识文件"${fileName}"?`)) return
|
||
const res = await fetch('/api/knowledge/delete', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ file_name: fileName })
|
||
})
|
||
const data = await res.json()
|
||
if (data.success) await this.loadKnowledge()
|
||
else alert('删除失败: ' + (data.error || ''))
|
||
},
|
||
|
||
// ── 暗标设置 ──
|
||
addAnonPreset(text) {
|
||
const sep = this.anonText.trim() ? '\n' : ''
|
||
this.anonText = (this.anonText.trim() + sep + '- ' + text).trimStart()
|
||
},
|
||
|
||
async saveAnon() {
|
||
this.savingAnon = true
|
||
const anon = this.anonEnabled ? this.anonText.trim() : ''
|
||
await fetch(`/api/projects/${this.projectId}/anon`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ anon_requirements: anon })
|
||
})
|
||
this.project.anon_requirements = anon
|
||
this.savingAnon = false
|
||
},
|
||
|
||
async saveDiagram() {
|
||
this.savingDiagram = true
|
||
await fetch(`/api/projects/${this.projectId}/diagram`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ enable_figure: this.enableFigure, enable_table: this.enableTable })
|
||
})
|
||
this.project.enable_figure = this.enableFigure
|
||
this.project.enable_table = this.enableTable
|
||
this.savingDiagram = false
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<!-- 页脚版权声明 -->
|
||
<footer class="py-4 px-6 border-t border-gray-200 bg-white text-center text-xs text-gray-500 space-y-1">
|
||
<p class="font-medium text-gray-600">© 标书老崔</p>
|
||
<p>本工具仅限学习交流免费使用,生成的技术方案请人工核对。本工具不会在任何平台售卖,请注意甄别。</p>
|
||
</footer>
|
||
</body>
|
||
</html>
|