Selaa lähdekoodia

完善中标建项与台账地图员工入口,并同步 README 变更记录。

接入 SQ 标书上传校验、合同额双写与按驻地查员工;投标不写父编号;列表区内滚动。

Co-authored-by: Cursor <cursoragent@cursor.com>
wdq 1 kuukausi sitten
vanhempi
sitoutus
4b6379ed60
36 muutettua tiedostoa jossa 3498 lisäystä ja 1687 poistoa
  1. 25 7
      README.md
  2. 40 1
      src/api/common.ts
  3. 230 191
      src/components/bidResult/WonBidProjectForm.vue
  4. 626 526
      src/components/bidResult/addContent.vue
  5. 261 156
      src/components/bidResult/addContent/AddContentForm.vue
  6. 421 262
      src/components/bidResult/addProject.vue
  7. 80 44
      src/components/bidResult/list/BidMonitorList.vue
  8. 7 1
      src/components/bidResult/list/BidResultHistoryList.vue
  9. 399 0
      src/components/ledger/DialogXqRenewSource.vue
  10. 16 52
      src/components/ledger/ProjectDetailInfo.vue
  11. 1 1
      src/components/ledger/ProjectDetailMap.vue
  12. 59 17
      src/components/map/ProMap.vue
  13. 1 1
      src/components/person/Table.vue
  14. 108 56
      src/components/renewal/BidRenewal.vue
  15. 26 2
      src/components/renewal/expire/RenewalHeaderBar.vue
  16. 3 1
      src/components/renewal/expire/TaskChecklistPanel.vue
  17. 63 47
      src/components/renewal/list/BidRenewalCardList.vue
  18. 138 59
      src/components/tenderpool/Intention.vue
  19. 38 5
      src/components/tenderpool/Monitor.vue
  20. 80 55
      src/components/tenderpool/monitor/MonitorCardList.vue
  21. 11 11
      src/composables/tenderpool/useMonitorAddProject.ts
  22. 4 2
      src/env.d.ts
  23. 36 36
      src/layouts/MainLayout.vue
  24. 4 3
      src/styles/element-theme-green.css
  25. 40 0
      src/utils/projectGuid.ts
  26. 53 0
      src/utils/tree.ts
  27. 63 0
      src/utils/uploadAndVerify.ts
  28. 96 0
      src/utils/util.ts
  29. 8 2
      src/views/bidMonitor/TenderPool.vue
  30. 19 12
      src/views/bidPreparation/Renewal.vue
  31. 25 10
      src/views/bidPreparation/RenewalExpireDetail.vue
  32. 3 2
      src/views/bidPreparation/RenewalXQDetail.vue
  33. 21 4
      src/views/bidResult/BidResult.vue
  34. 320 82
      src/views/ledger/ProjectDetail.vue
  35. 93 39
      src/views/ledger/Projects.vue
  36. 80 0
      word/上传并校验文件 接口手册.md

+ 25 - 7
README.md

@@ -166,7 +166,8 @@ sq_manage/
 │   │   ├── ledger/                     # 项目台账详情拆分
 │   │   │   ├── ProjectDetailHeader.vue # 详情头栏
 │   │   │   ├── ProjectDetailInfo.vue   # 合同/字段信息区
-│   │   │   └── ProjectDetailMap.vue    # 详情地图区
+│   │   │   ├── ProjectDetailMap.vue    # 详情地图区
+│   │   │   └── DialogXqRenewSource.vue # 台账续签登记弹窗(新建 SOURCE)
 │   │   ├── map/
 │   │   │   ├── MapDraw.vue             # Mapbox 绘制(建项/面要素)
 │   │   │   └── ProMap.vue              # 项目详情等场景的地图展示
@@ -229,7 +230,9 @@ sq_manage/
 │   │   ├── config.ts                   # 读取 window.CONFIG(含兜底)
 │   │   ├── request.ts                  # Axios + token 拦截 / 无效 token 踢登录
 │   │   ├── encrypt.ts                  # 登录密码加密(依赖 createAuth)
-│   │   ├── util.ts                     # 金额/UUID/下载/GeoJSON→WKT、项目标书→参考标书路径校验等
+│   │   ├── util.ts                     # 金额/UUID/下载/GeoJSON→WKT、合同额/中标金额解析、参考标书路径等
+│   │   ├── uploadAndVerify.ts          # SQ 上传并校验标书(docx/pdf)结果解析
+│   │   ├── projectGuid.ts              # 在管项目编码 / 父编号约定
 │   │   ├── displayUser.ts              # 登录名 → 展示名映射
 │   │   ├── renewalMath.ts              # 合同剩余天数 / 档位样式计算
 │   │   └── tree.ts                     # 在管项目列表 → 树节点
@@ -315,17 +318,19 @@ sq_manage/
 | 服务 | 前缀 | 用途 |
 |------|------|------|
 | OAUTH | `CONFIG.OAUTH_URL` | 登录、修改密码 |
-| DMS | `CONFIG.DMS_URL` | 栏目查询、内容 CRUD、文件上传、审核 |
-| SQ | `CONFIG.SQ_URL` | 统计、历史合作方、招标生成、标书生成进度查询 |
+| DMS | `CONFIG.DMS_URL` | 栏目查询、内容 CRUD、文件上传、审核、单选字典 |
+| SQ | `CONFIG.SQ_URL` | 统计、历史合作方、招标生成、标书进度、上传并校验 |
 
-常用 SQ 能力说明:
+常用 SQ / DMS 能力说明:
 
 | 接口方法 | 路径 | 说明 |
 |----------|------|------|
 | `getGenerateTender` | `POST /tender/generate` | 发起 AI 标书生成(投续标 ID) |
 | `getTenderProgress` | `GET /tender/api/v1/jobs/{txbId}/progress` | 查询标书生成步骤进度 |
+| `uploadAndVerify` | `POST /uploadAndVerify` | 上传标书到 DMS 并校验可读性(docx/pdf),返回 `filePath` |
+| `getSelectBySName` | `POST /select/getSelectBySName` | 按 selectName 拉取 DMS 单选/多选选项(如付款模式 `shenqin_fkms`) |
 
-对接说明见仓库根目录 `API进度查询接口对接文档.md`。
+对接说明:仓库根目录 `API进度查询接口对接文档.md`;上传校验见 `word/上传并校验文件 接口手册.md`。
 
 所有请求走 `/api` 代理前缀,开发时由 Vite 转发到后端。
 
@@ -336,8 +341,9 @@ sq_manage/
 - **路径别名**:`@` → `src/`(在 `vite.config.ts` 和 `tsconfig.json` 中均已配置)
 - **构建产物路径**:Vite `base` 设为 `./`,产物可直接部署到任意子路径
 - **Token 过期**:后端返回 `无效token` 或 HTTP 401/403 时,前端会自动清理登录态并跳转登录页
-- **文件上传**:`uploadFile` 接口使用 `multipart/form-data`,需传 `FormData`
+- **文件上传**:`uploadFile`(DMS)与 `uploadAndVerify`(SQ)均使用 `multipart/form-data`;大标书默认超时 600s
 - **AI Agent 开发**:项目已集成 Harness 工程文件,Agent 工作前请阅读根目录 `AGENTS.md`,通过 `harness/init.ps1`(PowerShell)或 `bash harness/init.sh`(Git Bash/WSL)一键完成依赖安装与基线验证
+- **业务手册**:本地 `系统操作流程.md`(已 gitignore)与 README「提交版本记录」同步维护页面/DMS 约定;改业务时须同步更新
 
 ---
 
@@ -421,3 +427,15 @@ Internal use only.
 4、标书编制:列表首屏与级联数据并行加载;修复新增弹窗区县→项目横向级联
 5、续标/投标创建投续标时,将所选在管项目的项目标书(`.docx`)写入参考标书 `c_ckbs`
 6、注册驻地/补充协议栏目 ID;本地手册 `系统操作流程.md` 纳入 `.gitignore`
+
+- **2026-09-24**
+1、**台账地图「员工信息」**:不再用滞后的 `c_xmygrs>0`;点击驻地面后按驻地内容 id(`station_id`)查员工 `c_sszd`,有人则显示入口并打开该驻地员工列表
+2、**标书上传与校验**:中标登记 / 确认中标建项改走 SQ `uploadAndVerify`(docx/pdf),路径写入 `c_history_tender`;新增 `api.uploadAndVerify`、`utils/uploadAndVerify.ts` 与接口手册
+3、**确认中标建项**:表单对齐在管 SOURCE(合同额双写 `c_hte`+`c_htzj`,单位元;默认合同数/项目数=1;付款模式等);弹窗保留底层、禁点遮罩关闭;去掉项目范围/补充协议总价等无关项
+4、**中标登记**:选标书即上传校验;续标场景按投续标 `xmbh` 自动匹配父级在管;登记完成后关闭关联招标/开标监控
+5、**投续标创建约定**:续标写父编号 `xmbh`;**投标不写 `xmbh`**,类似项目仅用于参考标书 `c_ckbs`(仅 `.docx`)
+6、**台账续签**:新增 `DialogXqRenewSource` 续签登记弹窗;付款模式改 DMS 单选(`getSelectBySName` / `shenqin_fkms`)
+7、**列表滚动**:台账 / 标书编制 / 招标监测 / 开标与中标等列表区域改为内容区内滚动,顶栏与分页固定
+8、**标书编制页**:暂时隐藏「投续标 / 续签标」顶栏 Tab,默认只展示投续标(代码保留,便于恢复)
+9、**台账详情信息区**:精简展示;合同额优先读 `c_hte`,回退 `c_htzj`,列表按万元展示
+10、**工具与金额**:`sourceContractAmountYuan` / 结果公告中标金额解析等;DMS 上传超时加长以支持大标书

+ 40 - 1
src/api/common.ts

@@ -42,6 +42,11 @@ interface ModelByIdParams {
   modelId: number
 }
 
+/** DMS 单选/多选选项:按 selectName 拉取 */
+interface SelectBySNameParams {
+  sName: string
+}
+
 interface UpdateAuditParams {
   id: string
   columnId: number
@@ -54,6 +59,16 @@ interface UploadFileParams {
   paraName: string
   type: number
   file: File
+  /** 大文件上传可单独加长,默认 10 分钟;覆盖全局 100s */
+  timeout?: number
+}
+
+/** SQ 上传并校验(multipart:file + columnId + fileType) */
+interface UploadAndVerifyParams {
+  file: File
+  columnId: string
+  fileType: 'docx' | 'pdf'
+  timeout?: number
 }
 
 const login = (params: LoginParams) => {
@@ -76,6 +91,11 @@ const getDmsTypes = (params: DmsTypesParams) => {
   return axios.post(CONFIG.DMS_URL + '/category/selectByCNameAType', qs.stringify(params))
 }
 
+/** 拉取 DMS 单选/多选选项(frontType=check 等),如付款模式 shenqin_fkms */
+const getSelectBySName = (params: SelectBySNameParams) => {
+  return axios.post(CONFIG.DMS_URL + '/select/getSelectBySName', qs.stringify(params))
+}
+
 const updateContent = (params: UpdateContentParams) => {
   return axios.post(CONFIG.DMS_URL + '/content/updateContent', qs.stringify(params))
 }
@@ -108,7 +128,24 @@ const uploadFile = (params: UploadFileParams) => {
   formData.append('type', String(params.type))
   formData.append('file', params.file)
   return axios.post(CONFIG.DMS_URL + '/file/uploadFile', formData, {
-    headers: { 'Content-Type': 'multipart/form-data' }
+    headers: { 'Content-Type': 'multipart/form-data' },
+    // 标书约 80–100MB,单独加长超时,避免命中全局 100000ms
+    timeout: params.timeout ?? 600000,
+  })
+}
+
+/**
+ * SQ:上传到 DMS 并校验可读性,返回 filePath / valid 等。
+ * 见 word/上传并校验文件 接口手册.md
+ */
+const uploadAndVerify = (params: UploadAndVerifyParams) => {
+  const formData = new FormData()
+  formData.append('file', params.file)
+  formData.append('columnId', params.columnId)
+  formData.append('fileType', params.fileType)
+  return axios.post(CONFIG.SQ_URL + '/uploadAndVerify', formData, {
+    headers: { 'Content-Type': 'multipart/form-data' },
+    timeout: params.timeout ?? 600000,
   })
 }
 
@@ -134,6 +171,7 @@ export default {
   login,
   updatePassword,
   getDmsTypes,
+  getSelectBySName,
   updateContent,
   addContent,
   getDmsDataList,
@@ -141,6 +179,7 @@ export default {
   getModelById,
   updateAudit,
   uploadFile,
+  uploadAndVerify,
   getBasicCount,
   getHistoryPartner,
   getGenerateTender,

+ 230 - 191
src/components/bidResult/WonBidProjectForm.vue

@@ -1,172 +1,199 @@
 <template>
-  <!-- 中标建项表单字段区(不含提交逻辑) -->
-  <el-form
-    ref="formRef"
-    class="won-bid-form"
-    :model="addForm"
-    :rules="rules"
-    label-width="auto"
-  >
-    <el-form-item label="项目名称" prop="c_xmmc">
-      <el-input v-model="addForm.c_xmmc" placeholder="请输入项目名称" />
-    </el-form-item>
-    <el-form-item label="项目地址" prop="c_xmdz">
-      <el-input v-model="addForm.c_xmdz" placeholder="请输入项目地址" />
-    </el-form-item>
-    <el-form-item label="管理区域" prop="c_glqynbkj">
-      <el-cascader
-        v-model="areaModel"
-        :options="glArea"
-        :show-all-levels="true"
-        style="width: 100%"
-        @change="(v: any) => emit('area-change', v)"
-      />
-    </el-form-item>
-    <el-form-item label="合同面积(平方米)" prop="c_htmj">
-      <el-input-number
-        style="width: 100%"
-        v-model="addForm.c_htmj"
-        controls-position="right"
-        placeholder="请输入合同面积(平方米)"
-      />
-    </el-form-item>
-    <el-form-item label="合同签约甲方" prop="c_htqyjf">
-      <el-input v-model="addForm.c_htqyjf" placeholder="请输入合同签约甲方" />
-    </el-form-item>
-    <el-form-item label="首次服务日期" prop="c_scfwrq">
-      <el-date-picker
-        style="width: 100%"
-        v-model="addForm.c_scfwrq"
-        type="date"
-        placeholder="请输入首次服务日期"
-      />
-    </el-form-item>
-    <el-form-item label="意向公示日期" prop="c_xmyxgsrq">
-      <el-date-picker
-        style="width: 100%"
-        v-model="addForm.c_xmyxgsrq"
-        type="date"
-        placeholder="请输入意向公示日期"
-      />
-    </el-form-item>
-    <el-form-item label="招标公告日期" prop="c_xmzbggrq">
-      <el-date-picker
-        style="width: 100%"
-        v-model="addForm.c_xmzbggrq"
-        type="date"
-        placeholder="请输入招标公告日期"
-      />
-    </el-form-item>
-    <el-form-item label="投标日期" prop="c_xmtbrq">
-      <el-date-picker
-        style="width: 100%"
-        v-model="addForm.c_xmtbrq"
-        type="date"
-        placeholder="请输入投标日期"
-      />
-    </el-form-item>
-    <el-form-item label="中标结果" prop="c_zbjgrzbdrhxsj">
-      <el-radio
-        v-for="item in zbjgOptions"
-        :key="item.value"
-        v-model="zbjgModel"
-        :label="item.value"
-        >{{ item.label }}</el-radio
-      >
-    </el-form-item>
-    <el-form-item label="中标日期(中标函)" prop="c_xmzbrq">
-      <el-date-picker
-        style="width: 100%"
-        v-model="addForm.c_xmzbrq"
-        type="date"
-        placeholder="请输入中标日期(中标函)"
-      />
-    </el-form-item>
-    <el-form-item label="服务合同签订日期" prop="c_xmfwhtqdrq">
-      <el-date-picker
-        style="width: 100%"
-        v-model="addForm.c_xmfwhtqdrq"
-        type="date"
-        placeholder="请输入服务合同签订日期"
-      />
-    </el-form-item>
-    <el-form-item label="合同编号" prop="c_htbh">
-      <el-input v-model="addForm.c_htbh" placeholder="请输入合同编号" />
-    </el-form-item>
-    <el-form-item label="合同开始日期" prop="c_zgxmhtksrq">
-      <el-date-picker
-        style="width: 100%"
-        v-model="addForm.c_zgxmhtksrq"
-        type="date"
-        placeholder="请输入合同开始日期"
-      />
-    </el-form-item>
-    <el-form-item label="合同到期日期" prop="c_zgxmhtdqrq">
-      <el-date-picker
-        style="width: 100%"
-        v-model="addForm.c_zgxmhtdqrq"
-        type="date"
-        placeholder="请输入合同到期日期"
-      />
-    </el-form-item>
-    <el-form-item label="本年度合同月数" prop="c_bndhtys">
-      <el-input-number
-        style="width: 100%"
-        v-model="addForm.c_bndhtys"
-        controls-position="right"
-        placeholder="请输入本年度合同月数"
-      />
-    </el-form-item>
-    <el-form-item label="合同总价" prop="c_htzj">
-      <el-input-number
-        style="width: 100%"
-        v-model="addForm.c_htzj"
-        controls-position="right"
-        placeholder="请输入合同总价"
-      />
-    </el-form-item>
-    <el-form-item label="中标得分" prop="c_zbdfqk">
-      <el-input-number
-        style="width: 100%"
-        v-model="addForm.c_zbdfqk"
-        controls-position="right"
-        placeholder="请输入中标得分"
-      />
-    </el-form-item>
+  <!-- 中标建项表单:对齐线上 SOURCE;参考中标登记,并保留投续标建项常用日期/得分 -->
+  <div class="won-bid-scroll">
+    <el-form
+      ref="formRef"
+      class="won-bid-form"
+      :model="addForm"
+      :rules="rules"
+      label-width="150px"
+    >
+      <el-form-item label="项目名称" prop="c_xmmc">
+        <el-input v-model="addForm.c_xmmc" placeholder="请输入项目名称" />
+      </el-form-item>
+      <el-form-item label="项目地址" prop="c_xmdz">
+        <el-input v-model="addForm.c_xmdz" placeholder="请输入项目地址" />
+      </el-form-item>
+      <el-form-item label="合同签约甲方" prop="c_htqyjf">
+        <el-input v-model="addForm.c_htqyjf" placeholder="请输入合同签约甲方" />
+      </el-form-item>
+      <el-form-item label="管理区域" prop="c_glqynbkj">
+        <el-cascader
+          v-model="areaModel"
+          :options="glArea"
+          :show-all-levels="true"
+          style="width: 100%"
+          @change="(v: any) => emit('area-change', v)"
+        />
+      </el-form-item>
+      <el-form-item label="合同面积(平方米)" prop="c_htmj">
+        <el-input-number
+          v-model="addForm.c_htmj"
+          style="width: 100%"
+          controls-position="right"
+          placeholder="请输入合同面积"
+        />
+      </el-form-item>
+      <el-form-item label="合同编号" prop="c_htbh">
+        <el-input v-model="addForm.c_htbh" placeholder="请输入合同编号" />
+      </el-form-item>
+      <el-form-item label="合同开始日期" prop="c_zgxmhtksrq">
+        <el-date-picker
+          v-model="addForm.c_zgxmhtksrq"
+          style="width: 100%"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="请选择合同开始日期"
+        />
+      </el-form-item>
+      <el-form-item label="合同到期日期" prop="c_zgxmhtdqrq">
+        <el-date-picker
+          v-model="addForm.c_zgxmhtdqrq"
+          style="width: 100%"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="请选择合同到期日期"
+        />
+      </el-form-item>
+      <el-form-item label="合同额" prop="c_htzj">
+        <el-input-number
+          v-model="addForm.c_htzj"
+          style="width: 100%"
+          controls-position="right"
+          :min="0"
+          :precision="2"
+          placeholder="请输入合同额(元)"
+        />
+      </el-form-item>
+      <el-form-item label="合同总月数" prop="c_bndhtys">
+        <el-input-number
+          v-model="addForm.c_bndhtys"
+          style="width: 100%"
+          controls-position="right"
+          placeholder="请输入合同总月数"
+        />
+      </el-form-item>
+      <el-form-item label="业态" prop="c_hzyt">
+        <el-tree-select
+          v-model="ytModel"
+          placeholder="请选择业态"
+          :data="ytList"
+          collapse-tags
+          filterable
+          style="width: 100%"
+        />
+      </el-form-item>
+      <el-form-item label="委托类型" prop="c_wtfs">
+        <el-radio-group v-model="zgfwModel">
+          <el-radio v-for="item in zgfwOptions" :key="item.value" :value="item.value">{{
+            item.label
+          }}</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="计费方式" prop="c_jffs">
+        <el-radio-group v-model="jffsModel">
+          <el-radio v-for="item in jffsOptions" :key="item.value" :value="item.value">{{
+            item.label
+          }}</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="付款模式" prop="c_fkms">
+        <el-radio-group v-model="fkmsModel">
+          <el-radio v-for="item in fkmsOptions" :key="item.value" :value="item.value">{{
+            item.label
+          }}</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="项目开始时间" prop="c_xmkssj">
+        <el-date-picker
+          v-model="addForm.c_xmkssj"
+          style="width: 100%"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="请选择项目开始时间"
+        />
+      </el-form-item>
+      <el-form-item label="项目到期时间" prop="c_xmdqsj">
+        <el-date-picker
+          v-model="addForm.c_xmdqsj"
+          style="width: 100%"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="请选择项目到期时间"
+        />
+      </el-form-item>
 
-    <el-form-item label="业态" prop="c_hzyt">
-      <el-tree-select
-        placeholder="请选择业态"
-        v-model="ytModel"
-        :data="ytList"
-        collapse-tags
-        filterable
-        style="width: 100%"
-      />
-    </el-form-item>
-    <el-form-item label="委托类型" prop="c_wtfs">
-      <el-radio
-        v-for="item in zgfwOptions"
-        :key="item.value"
-        v-model="zgfwModel"
-        :label="item.value"
-        >{{ item.label }}</el-radio
-      >
-    </el-form-item>
+      <el-divider content-position="left">投续标相关(可预填)</el-divider>
 
-    <el-form-item label="服务类型" prop="c_fwlx">
-      <el-checkbox-group v-model="fwlxModel">
-        <el-checkbox label="会务"></el-checkbox>
-        <el-checkbox label="工程"></el-checkbox>
-        <el-checkbox label="绿化"></el-checkbox>
-        <el-checkbox label="保安"></el-checkbox>
-        <el-checkbox label="保洁"></el-checkbox>
-      </el-checkbox-group>
-    </el-form-item>
-    <el-form-item label="geometry" prop="geometry">
-      <el-button class="btn" style="width: 100%" @click="emit('open-map')">绘制geometry</el-button>
-    </el-form-item>
-  </el-form>
+      <el-form-item label="招标公告日期" prop="c_xmzbggrq">
+        <el-date-picker
+          v-model="addForm.c_xmzbggrq"
+          style="width: 100%"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="请选择招标公告日期"
+        />
+      </el-form-item>
+      <el-form-item label="招标公示日期" prop="c_xmzbgsrq">
+        <el-date-picker
+          v-model="addForm.c_xmzbgsrq"
+          style="width: 100%"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="请选择招标公示日期"
+        />
+      </el-form-item>
+      <el-form-item label="投标日期" prop="c_xmtbrq">
+        <el-date-picker
+          v-model="addForm.c_xmtbrq"
+          style="width: 100%"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="请选择投标日期"
+        />
+      </el-form-item>
+      <el-form-item label="中标日期(中标函)" prop="c_xmzbrq">
+        <el-date-picker
+          v-model="addForm.c_xmzbrq"
+          style="width: 100%"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="请选择中标日期"
+        />
+      </el-form-item>
+      <el-form-item label="中标得分" prop="c_zbdfqk">
+        <el-input-number
+          v-model="addForm.c_zbdfqk"
+          style="width: 100%"
+          controls-position="right"
+          placeholder="请输入中标得分"
+        />
+      </el-form-item>
+
+      <el-form-item label="项目标书" prop="tenderFile" required>
+        <div class="tender-upload">
+          <el-upload
+            :auto-upload="false"
+            :limit="1"
+            :disabled="tenderUploading"
+            :on-change="(f: any) => emit('tender-file-change', f)"
+            :on-remove="() => emit('tender-file-change', null)"
+            accept=".docx,.pdf"
+          >
+            <el-button class="btn" :loading="tenderUploading">
+              {{ tenderUploading ? '上传校验中…' : '选择标书文件' }}
+            </el-button>
+            <template #tip>
+              <div class="el-upload__tip">
+                仅支持 .docx / .pdf;选文件后自动上传并校验,文件较大请耐心等待
+              </div>
+            </template>
+          </el-upload>
+          <div v-if="tenderFilePath" class="upload-ok">已上传并通过校验</div>
+        </div>
+      </el-form-item>
+    </el-form>
+  </div>
 </template>
 
 <script setup lang="ts">
@@ -179,22 +206,25 @@ const props = defineProps<{
   glArea: any[]
   ytList: any[]
   zgfwOptions: any[]
-  zbjgOptions: any[]
+  jffsOptions: any[]
+  fkmsOptions: any[]
   addFormArea: any[]
-  addFormZgfw: string
-  addFormZbjg: number
-  addFormFwlx: any[]
+  addFormZgfw: string | number
+  addFormJffs: string | number
+  addFormFkms: string | number
   addFormYt: string
+  tenderUploading?: boolean
+  tenderFilePath?: string
 }>()
 
 const emit = defineEmits<{
   'update:addFormArea': [value: any[]]
-  'update:addFormZgfw': [value: string]
-  'update:addFormZbjg': [value: number]
-  'update:addFormFwlx': [value: any[]]
+  'update:addFormZgfw': [value: string | number]
+  'update:addFormJffs': [value: string | number]
+  'update:addFormFkms': [value: string | number]
   'update:addFormYt': [value: string]
   'area-change': [value: any]
-  'open-map': []
+  'tender-file-change': [file: any]
 }>()
 
 const formRef = ref<FormInstance>()
@@ -206,15 +236,15 @@ const areaModel = computed({
 })
 const zgfwModel = computed({
   get: () => props.addFormZgfw,
-  set: (v: string) => emit('update:addFormZgfw', v),
+  set: (v: string | number) => emit('update:addFormZgfw', v),
 })
-const zbjgModel = computed({
-  get: () => props.addFormZbjg,
-  set: (v: number) => emit('update:addFormZbjg', v),
+const jffsModel = computed({
+  get: () => props.addFormJffs,
+  set: (v: string | number) => emit('update:addFormJffs', v),
 })
-const fwlxModel = computed({
-  get: () => props.addFormFwlx,
-  set: (v: any[]) => emit('update:addFormFwlx', v),
+const fkmsModel = computed({
+  get: () => props.addFormFkms,
+  set: (v: string | number) => emit('update:addFormFkms', v),
 })
 const ytModel = computed({
   get: () => props.addFormYt,
@@ -223,24 +253,33 @@ const ytModel = computed({
 </script>
 
 <style scoped>
-.won-bid-form {
-  max-height: 500px;
-  overflow-y: scroll;
-  /* 表单内容与右侧滚动条留出间距 */
-  padding-right: 12px;
+.won-bid-scroll {
+  max-height: 520px;
+  overflow-y: auto;
+  padding-right: 8px;
 }
-
-/* 覆盖全局「默认隐藏、悬停才显示」的滚动条,保证弹窗内始终可见 */
-.won-bid-form::-webkit-scrollbar {
+.won-bid-scroll::-webkit-scrollbar {
   width: 6px;
 }
-.won-bid-form::-webkit-scrollbar-thumb {
+.won-bid-scroll::-webkit-scrollbar-thumb {
   display: block;
   background-color: rgba(163, 166, 173, 0.6);
   border-radius: 6px;
 }
-.won-bid-form::-webkit-scrollbar-track {
+.won-bid-scroll::-webkit-scrollbar-track {
   background-color: rgba(0, 0, 0, 0.04);
   border-radius: 6px;
 }
+.tender-upload {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 4px;
+}
+.upload-ok {
+  font-size: 12px;
+  line-height: 1.4;
+  color: var(--el-color-success, #16a34a);
+}
 </style>

+ 626 - 526
src/components/bidResult/addContent.vue

@@ -1,153 +1,236 @@
 <template>
-  <!-- 中标登记弹窗编排层:Dialog 壳 + 提交逻辑;字段区见 AddContentForm -->
-  <div class="bid-content-page">
-    <div class="bid-container">
-      <el-dialog v-model="dialogFormVisible" draggable title="提示信息" width="500" @close="close">
-        <AddContentForm
-          ref="formCompRef"
-          :add-form="addForm"
-          :rules="rules"
-          :type-options="typeOptions"
-          :jk-list="jkList"
-          :txb-list="txbList"
-          :yt-list="ytList"
-          :zgfw-options="zgfwOptions"
-          :gl-area="props.glArea"
-          :area-project="props.areaProject"
-          :finish-bid-object-temp="finishBidObjectTemp"
-          v-model:admin-cascader="adminCascader"
-          v-model:add-form-area="addFormArea"
-          v-model:add-form-zgfw="addFormZgfw"
-          v-model:add-form-fwlx="addFormFwlx"
-          v-model:add-form-yt="addFormYT"
-          @type-change="handleTypeChange"
-          @project-change="handleProjectChange"
-          @admin-cascader-change="handleAdminCascaderChange"
-          @xb-switch-change="handleXBSwitchChange"
-          @area-change="handleNodeAreaClick"
-        />
-        <template #footer>
-          <div class="dialog-footer">
-            <el-button @click="close" class="btn">取消</el-button>
-            <el-button type="primary" class="btn enter-btn" @click="submitForm">
-              确认
-            </el-button>
-          </div>
-        </template>
-      </el-dialog>
-    </div>
-  </div>
+  <!-- 中标登记:必建 SOURCE + 必传标书;进行中投续标写 c_terminated -->
+  <el-dialog
+    :model-value="flag"
+    draggable
+    title="中标登记"
+    width="640"
+    :close-on-click-modal="false"
+    @close="close"
+  >
+    <AddContentForm
+      ref="formCompRef"
+      :add-form="addForm"
+      :rules="rules"
+      :type-options="typeOptions"
+      :txb-list="txbList"
+      :yt-list="ytList"
+      :zgfw-options="zgfwOptions"
+      :jffs-options="jffsOptions"
+      :fkms-options="fkmsOptions"
+      :gl-area="props.glArea"
+      :area-project="props.areaProject"
+      :tender-uploading="tenderUploading"
+      :tender-file-path="tenderFilePath"
+      v-model:parent-cascader="parentCascader"
+      v-model:add-form-area="addFormArea"
+      v-model:add-form-zgfw="addFormZgfw"
+      v-model:add-form-jffs="addFormJffs"
+      v-model:add-form-fkms="addFormFkms"
+      v-model:add-form-yt="addFormYT"
+      @type-change="handleTypeChange"
+      @txb-change="handleTxbChange"
+      @parent-cascader-change="handleParentCascaderChange"
+      @xb-switch-change="handleXBSwitchChange"
+      @area-change="handleNodeAreaClick"
+      @tender-file-change="handleTenderFileChange"
+    />
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button class="btn" @click="close">取消</el-button>
+        <el-button
+          type="primary"
+          class="btn enter-btn"
+          :loading="submitting"
+          @click="submitForm"
+        >
+          确认登记
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
 </template>
 
 <script setup lang="ts">
-import { ref, onMounted } from 'vue'
+import { ref, onMounted, reactive, nextTick } from 'vue'
 import moment from 'moment'
+import { ElMessage, ElLoading } from 'element-plus'
 import api from '@/api/common'
-import CONFIG from '@/utils/config'
 import { COLUMN, MODEL } from '@/constants/dmsColumns'
 import { buildProjectTree } from '@/utils/tree'
-import { getDisplayUserName } from '@/utils/displayUser'
+import { nextSourceGuidFromLatest } from '@/utils/projectGuid'
+import { uploadAndVerifyFile } from '@/utils/uploadAndVerify'
 import type { FormRules } from 'element-plus'
 import AddContentForm from '@/components/bidResult/addContent/AddContentForm.vue'
 
 const formCompRef = ref<InstanceType<typeof AddContentForm>>()
 
-const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
-const userName = getDisplayUserName(userInfo)
-
 const props = defineProps({
-  flag: {
-    type: Boolean,
-    default: false,
-  },
-  close: {
-    type: Function,
-    default: () => {},
-  },
-  glArea: {
-    type: Array,
-    default: () => [],
-  },
-  areaProject: {
-    type: Array,
-    default: () => [],
-  },
+  flag: { type: Boolean, default: false },
+  close: { type: Function, default: () => {} },
+  glArea: { type: Array, default: () => [] },
+  areaProject: { type: Array, default: () => [] },
 })
 
-interface resultForm {
-  [key: string]: any
-}
-const finishObject = ref<resultForm>({})
 const finishProjects = ref<any[]>([])
 const finishTreeProjects = ref<any[]>([])
+const selectedTxb = ref<any>(null)
+const parentProject = ref<any>(null)
+const tenderFileRaw = ref<File | null>(null)
+/** 选文件后上传+校验通过得到的路径;确认登记时写入 c_history_tender */
+const tenderFilePath = ref('')
+const tenderUploading = ref(false)
+const submitting = ref(false)
 
 const txbList = ref<any[]>([])
-const jkList = ref<any[]>([])
+const ytList = ref<any[]>([])
+const zgfwOptions = ref<any[]>([])
+const jffsOptions = ref<any[]>([])
+const fkmsOptions = ref<any[]>([])
+
+const addFormZgfw = ref<string | number>('')
+const addFormJffs = ref<string | number>('')
+const addFormFkms = ref<string | number>('')
+const addFormYT = ref('')
+const addFormArea = ref<any[]>([])
+const parentCascader = ref<any[]>([])
+const areaCodes = ref<string[]>([])
+
+const typeOptions = ref([
+  { value: '新项目登记', label: '新项目登记' },
+  { value: '进行中投续标', label: '进行中投续标' },
+])
 
-const ytList = ref<resultForm[]>([])
+const emptySourceForm = () => ({
+  type: '新项目登记',
+  txbId: '' as string | number,
+  c_is_xb: false,
+  c_xmmc: '',
+  c_xmdz: '',
+  c_htqyjf: '',
+  c_glqynbkj: '',
+  c_sq: '',
+  c_sfzxs: '上海市',
+  c_htmj: null as number | null,
+  c_htbh: '',
+  c_zgxmhtksrq: '',
+  c_zgxmhtdqrq: '',
+  c_htzj: null as number | null,
+  c_bndhtys: null as number | null,
+  c_hzyt: '',
+  c_wtfs: '' as string | number,
+  c_jffs: '' as string | number,
+  c_fkms: '' as string | number,
+  c_hts: '1',
+  c_xms: '1',
+  c_xmkssj: '',
+  c_xmdqsj: '',
+  c_bcxyzj: '',
+  c_zbdfqk: '',
+  c_xmzbggrq: '',
+  c_xmzbgsrq: '',
+  c_xmtbrq: '',
+  c_xmzbrq: '',
+  tenderFile: null as any,
+})
 
-const addFormZgfw = ref('')
-const addFormFwlx = ref([])
-const addFormYT = ref('')
-const addFormArea = ref([])
-const zgfwOptions = ref<any>([])
-
-const areaObject = ref({})
-const adminCascader = ref([])
-const jkBidObject = ref<any>(null)
-const txbBidObject = ref<any>(null)
-const finishBidObject = ref<any>(null)
-const finishBidObjectTemp = ref({
-  flag: false,
-  url: '',
+const addForm = ref(emptySourceForm())
+
+// 规则对象保持稳定,避免切换「登记类型」时整表重算 rules 触发误提示
+const rules = reactive<FormRules>({
+  type: [{ required: true, message: '请选择登记类型', trigger: 'change' }],
+  txbId: [
+    {
+      validator: (_r, v, cb) => {
+        if (addForm.value.type === '进行中投续标' && (v === '' || v == null)) {
+          cb(new Error('请选择进行中投续标'))
+        } else cb()
+      },
+      trigger: 'change',
+    },
+  ],
+  c_xmmc: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
+  c_xmdz: [{ required: true, message: '请输入项目地址', trigger: 'blur' }],
+  c_htqyjf: [{ required: true, message: '请输入合同签约甲方', trigger: 'blur' }],
+  c_glqynbkj: [{ required: true, message: '请选择管理区域', trigger: 'change' }],
+  c_zgxmhtksrq: [{ required: true, message: '请选择合同开始日期', trigger: 'change' }],
+  c_zgxmhtdqrq: [{ required: true, message: '请选择合同到期日期', trigger: 'change' }],
+  c_hzyt: [{ required: true, message: '请选择业态', trigger: 'change' }],
+  tenderFile: [
+    {
+      validator: (_r, _v, cb) => {
+        if (!tenderFilePath.value) cb(new Error('请上传并通过校验的项目标书(.docx / .pdf)'))
+        else cb()
+      },
+      trigger: 'change',
+    },
+  ],
 })
 
 onMounted(() => {
   initData()
-  getDmsTypes()
+  loadDicts()
 })
 
-const getDmsTypes = () => {
-  let params = {
-    type: 0,
-    cName: 'sq_wtfs',
+const close = () => {
+  props.close()
+}
+
+const loadDicts = async () => {
+  const [wt, jf, fk] = await Promise.all([
+    getDmsTypes('sq_wtfs'),
+    getDmsTypes('sq_jffs'),
+    getSelectOptions('shenqin_fkms'),
+  ])
+  zgfwOptions.value = wt
+  jffsOptions.value = jf
+  fkmsOptions.value = fk
+}
+
+const getDmsTypes = async (cName: string) => {
+  const res = await api.getDmsTypes({ type: 0, cName } as any)
+  if ((res as any).code === 200) {
+    // value 统一成字符串,避免首项 index=0 与空串 '' 发生 == 误判导致无法点选
+    return ((res as any).content || []).map((item: any) => ({
+      ...item,
+      label: item.name,
+      value: String(item.index),
+    }))
   }
-  api.getDmsTypes(params).then((res) => {
-    if ((res as any).code === 200) {
-      let ret = (res as any).content
-      zgfwOptions.value = ret.map((item: any) => ({
-        ...item,
-        label: item.name,
-        value: item.index,
-      }))
-    } else {
-      zgfwOptions.value = []
-    }
-  })
+  return []
+}
+
+const getSelectOptions = async (sName: string) => {
+  const res = await api.getSelectBySName({ sName })
+  if ((res as any).code === 200 && Array.isArray((res as any).content)) {
+    return (res as any).content.map((item: any) => ({
+      ...item,
+      label: item.name,
+      value: String(item.index),
+    }))
+  }
+  return []
 }
 
 const initData = async () => {
   await getFinishProjects()
-  const itemTxbList = await getTypeProjects(COLUMN.TXB) // 投续标项目
-  const itemJkList = await getTypeProjects(COLUMN.KB_MONITOR) // 中标监控项目
-  const itemYtList = await getTypeProjects(COLUMN.SERVICE_TYPE) // 业态
+  const [itemTxbList, itemYtList] = await Promise.all([
+    getTypeProjects(COLUMN.TXB),
+    getTypeProjects(COLUMN.SERVICE_TYPE),
+  ])
   txbList.value = []
-  jkList.value = []
-  ytList.value = []
-  itemTxbList.data.filter((item: any) => {
-    const json = JSON.parse(item.c_xmjd_json)
-    if (json.length > 0 && json[json.length - 1].xmjd < 6) {
-      txbList.value.push(item)
-    }
-  })
-  itemJkList.data.filter((item: any) => {
-    const flag = item.c_sfzb
-    if (flag == false) {
-      // 已中标
-      jkList.value.push(item)
+  ;(itemTxbList?.data || []).forEach((item: any) => {
+    if (item.c_terminated === true || item.c_terminated === 'true') return
+    try {
+      const json = JSON.parse(item.c_xmjd_json || '[]')
+      if (json.length > 0 && json[json.length - 1].xmjd < 6) {
+        txbList.value.push(item)
+      }
+    } catch {
+      /* ignore */
     }
   })
-  ytList.value = itemYtList.data.map((item: any) => ({
+  ytList.value = (itemYtList?.data || []).map((item: any) => ({
     ...item,
     label: item.c_yt_name,
     value: item.c_yt_name,
@@ -155,458 +238,475 @@ const initData = async () => {
   }))
 }
 
-const dialogFormVisible = props.flag
-const close = () => {
-  props.close()
-}
-
 const getFinishProjects = async () => {
   const params: any = {
-    columnId: COLUMN.SOURCE, // 原项目数据
+    columnId: COLUMN.SOURCE,
     orderBy: JSON.stringify([{ field: 'c_zgxmhtdqrq', orderByType: 1 }]),
-    states: '0', // 只看草稿箱
+    states: '0,1,2,3',
     pageSize: 9999,
     page: 0,
   }
-  await api.getDmsDataList(params).then((res) => {
-    if ((res as any).code === 200) {
-      finishProjects.value = (res as any).content.data
-      finishTreeProjects.value = buildProjectTree(finishProjects.value)
-    } else {
-      finishProjects.value = []
-    }
-  })
+  const res = await api.getDmsDataList(params)
+  if ((res as any).code === 200) {
+    finishProjects.value = (res as any).content.data || []
+    finishTreeProjects.value = buildProjectTree(finishProjects.value)
+  } else {
+    finishProjects.value = []
+    finishTreeProjects.value = []
+  }
 }
 
-const typeOptions = ref([
-  {
-    value: '新登记',
-    label: '新登记',
-  },
-  {
-    value: '监控中未中标',
-    label: '监控中未中标',
-  },
-  {
-    value: '投续标中登记',
-    label: '投续标中登记',
-  },
-])
-
-const addForm = ref({
-  id: '',
-  type: '新登记',
-  title: '',
-  tb_date: '',
-  tb_money: '',
-  zb_code: '',
-  zb_name: '',
-  zb_org: '',
-  zb_source: '',
-  zb_money: '',
-  zb_startdate: '',
-  zb_enddate: '',
-  zb_url: '',
-  c_is_xb: false,
-})
-
-const rules = ref<FormRules>({})
+const getTypeProjects = async (columnId: number) => {
+  const params: any = {
+    columnId,
+    orderBy: JSON.stringify([{ field: 'update_time', orderByType: 2 }]),
+    states: '0,1,2,3',
+    pageSize: 9999,
+    page: 0,
+  }
+  const res = await api.getDmsDataList(params)
+  return (res as any).content
+}
 
-const submitForm = () => {
-  const formEl = formCompRef.value?.formRef
-  if (!formEl) return
-  formEl.validate(async (valid) => {
-    if (valid) {
-      if (addForm.value.type == '监控中未中标') {
-        // 流程
-        const id = 7 // 固定进度数值
-        const rwlcqd = JSON.parse(txbBidObject.value.rwlcqd || '[]')
-        rwlcqd.find((p: any) => p.id == id).done = true
-        rwlcqd.find((p: any) => p.id == id).create_time = moment().format('YYYY-MM-DD HH:mm')
-        rwlcqd.find((p: any) => p.id == id).username = userName
-        rwlcqd.find((p: any) => p.id == id).userid = userInfo?.id || ''
-        rwlcqd.find((p: any) => p.id == id).type = 0
-
-        // 进度
-        let xmjd_json = JSON.parse(txbBidObject.value.c_xmjd_json || '[]')
-        xmjd_json[5].create_time = moment().format('YYYY-MM-DD HH:mm')
-        xmjd_json[5].username = userName
-        xmjd_json[5].userid = userInfo?.id || ''
-        xmjd_json[5].type = 0
-        if (!xmjd_json[6]) {
-          xmjd_json.push({ xmjd: id, type: 1, create_time: '', username: '', userid: '' })
-        }
-        const content = JSON.stringify({
-          id: txbBidObject.value.id,
-          rwlcqd: rwlcqd,
-          c_xmjd_json: xmjd_json,
-        })
-        updateProject(COLUMN.TXB, MODEL.TXB, content)
-        // 修改中标监控
-        const steps = [
-          { n: 1, label: '预警触发' },
-          { n: 2, label: '投/续标准备' },
-          { n: 3, label: '招标监控' },
-          { n: 4, label: '标书制作' },
-          { n: 5, label: '投标提交' },
-          { n: 6, label: '开标监控' },
-          { n: 7, label: '确认中标' },
-        ]
-        let json = xmjd_json.map((item: any) => {
-          return {
-            create_time: item.create_time ? item.create_time : moment().format('YYYY-MM-DD HH:mm'),
-            desc: steps.find((s: any) => s.n == item.xmjd)?.label || '',
-            username: item.username ? item.username : userName,
-            userid: item.userid ? item.userid : userInfo?.id || '',
-          }
-        })
-        const content2 = JSON.stringify({
-          id: jkBidObject.value.id || '',
-          c_gljg: finishBidObjectTemp.value?.flag ? finishBidObject.value?.id : '',
-          c_jgjk_url: finishBidObjectTemp.value?.flag
-            ? finishBidObject.value?.c_detail_url
-            : finishBidObjectTemp.value?.url || '',
-          c_lcsjx: json || '',
-          c_sfzb: true,
-          c_type: '中标登记',
-          c_enable: false,
-        })
-        updateProject(COLUMN.KB_MONITOR, MODEL.KB_MONITOR, content2)
-      } else if (addForm.value.type == '投续标中登记') {
-        // 流程
-        const rwlcqd = JSON.parse(selectedObject.value.rwlcqd || '[]')
-        rwlcqd.forEach((p: any) => {
-          if (!p.danger && p.id != 8) {
-            // 未处理的流程全部重新处理
-            ;(p.done = true),
-              (p.create_time = moment().format('YYYY-MM-DD HH:mm')),
-              (p.username = userName),
-              (p.userid = userInfo?.id || ''),
-              (p.type = 0)
-          }
-        })
-        let xmjd_json = JSON.parse(selectedObject.value.c_xmjd_json || '[]')
-        const curNumber = xmjd_json[xmjd_json.length - 1].xmjd
-        for (let i = curNumber; i < 8; i++) {
-          if (i == curNumber) {
-            xmjd_json[i - 1].create_time = moment().format('YYYY-MM-DD HH:mm')
-            xmjd_json[i - 1].username = userName
-            xmjd_json[i - 1].userid = userInfo?.id || ''
-            xmjd_json[i - 1].type = 0
-          } else {
-            if (i == 7) {
-              xmjd_json.push({ xmjd: i, type: 1, create_time: '', username: '', userid: '' })
-            } else {
-              xmjd_json.push({
-                xmjd: i,
-                type: 0,
-                create_time: moment().format('YYYY-MM-DD HH:mm'),
-                username: userName,
-                userid: userInfo?.id || '',
-              })
-            }
-          }
-        }
-        const steps = [
-          { n: 1, label: '预警触发' },
-          { n: 2, label: '投/续标准备' },
-          { n: 3, label: '招标监控' },
-          { n: 4, label: '标书制作' },
-          { n: 5, label: '投标提交' },
-          { n: 6, label: '开标监控' },
-          { n: 7, label: '确认中标' },
-        ]
-        let json = xmjd_json.map((item: any) => {
-          return {
-            create_time: item.create_time ? item.create_time : moment().format('YYYY-MM-DD HH:mm'),
-            desc: steps.find((s: any) => s.n == item.xmjd)?.label || '',
-            username: item.username ? item.username : userName,
-            userid: item.userid ? item.userid : userInfo?.id || '',
-          }
-        })
+const handleTypeChange = (val: any) => {
+  addForm.value.type = val || '新项目登记'
+  addForm.value.txbId = ''
+  selectedTxb.value = null
+  parentProject.value = null
+  parentCascader.value = []
+  if (val === '新项目登记') {
+    addForm.value.c_is_xb = false
+  }
+  // 切换类型只改显隐,不应带出整表必填红字
+  nextTick(() => {
+    formCompRef.value?.formRef?.clearValidate()
+  })
+}
 
-        // 开标监控
-        let kbFormData = {
-          title: selectedObject.value.title,
-          content: selectedObject.value.content,
-          c_zbbh: selectedObject.value.c_xgzb ? selectedObject.value.c_xgzb : addForm.value.zb_code,
-          c_sfzb: true,
-          c_jgjk_url: finishBidObjectTemp.value?.url || '',
-          c_kbsj: selectedObject.value.zbjzsj
-            ? selectedObject.value.zbjzsj
-            : addForm.value.zb_enddate,
-          c_xmmc: selectedObject.value.c_xmbt,
-          c_zbtjr: selectedObject.value.tbrq ? selectedObject.value.tbrq : addForm.value.tb_date,
-          c_tbbj: selectedObject.value.tbbj ? selectedObject.value.tbbj : addForm.value.tb_money,
-          c_glxm: selectedObject.value.xmbh,
-          c_lcsjx: json || '',
-          c_type: '中标登记',
-          c_enable: false,
-        }
+/** 用投续标 xmbh(原项目父编号)匹配父级在管,并回填级联 [区名, 项目id] */
+const matchParentByXmbh = (xmbh: string) => {
+  const guid = String(xmbh || '').trim()
+  if (!guid) {
+    parentProject.value = null
+    parentCascader.value = []
+    return false
+  }
+  const parent =
+    finishProjects.value.find((p) => String(p.c_guid) === guid) ||
+    (props.areaProject as any[])
+      .flatMap((d: any) => d.children || [])
+      .find((c: any) => String(c.c_guid) === guid)
+  if (!parent) {
+    parentProject.value = null
+    parentCascader.value = []
+    ElMessage.warning('未找到投续标关联的父级在管项目,请手动选择')
+    return false
+  }
+  parentProject.value = parent
+  // 在区→项目级联中定位路径,驱动「父级在管项目」展示
+  let path: any[] | null = null
+  for (const district of props.areaProject as any[]) {
+    const child = (district.children || []).find(
+      (c: any) =>
+        String(c.c_guid) === guid ||
+        String(c.value) === String(parent.id) ||
+        String(c.id) === String(parent.id)
+    )
+    if (child) {
+      path = [district.value, child.value]
+      break
+    }
+  }
+  if (!path && parent.c_sq) {
+    const district = (props.areaProject as any[]).find(
+      (d: any) => d.label && String(d.label).includes(String(parent.c_sq))
+    )
+    if (district) {
+      const child = (district.children || []).find((c: any) => String(c.c_guid) === guid)
+      if (child) path = [district.value, child.value]
+      else if (parent.id != null) path = [district.value, parent.id]
+    }
+  }
+  parentCascader.value = path || []
+  fillFromParent(parent)
+  return true
+}
 
-        const res = await addContentParams(kbFormData, COLUMN.KB_MONITOR, MODEL.KB_MONITOR)
-        let desc = {
-          zb_project_code: selectedObject.value.c_xgzb
-            ? selectedObject.value.c_xgzb
-            : addForm.value.zb_code,
-          zb_source_name: addForm.value.zb_source,
-          zb_project_name: addForm.value.zb_name,
-          zb_budget_amount: selectedObject.value.tbbj
-            ? selectedObject.value.tbbj
-            : addForm.value.tb_money,
-          zb_detail_url: finishBidObjectTemp.value?.url || '',
-          zb_publish_time: selectedObject.value.zbkssj
-            ? selectedObject.value.zbkssj
-            : addForm.value.zb_startdate,
-          zb_bid_deadline: selectedObject.value.zbjzsj
-            ? selectedObject.value.zbjzsj
-            : addForm.value.zb_enddate,
-          zb_author: userName,
-        }
-        let areaArr = (areaObject.value as any)?.pathLabels || []
-        let tbFormData = {
-          id: selectedObject.value.id || '',
-          c_zgfw: selectedObject.value.c_zgfw ? selectedObject.value.c_zgfw : addFormZgfw.value,
-          c_xgzb: selectedObject.value.c_xgzb ? selectedObject.value.c_xgzb : addForm.value.zb_code,
-          c_xgjk: (res as any).content || '',
-          c_xmbtqc: selectedObject.value.c_xmbt,
-          c_qx: selectedObject.value.c_qx ? selectedObject.value.c_qx : areaArr[0] || '',
-          c_xmbt: selectedObject.value.c_xmbt,
-          c_xmjd_json: xmjd_json,
-          zbxmbh: selectedObject.value.c_xgzb ? selectedObject.value.c_xgzb : addForm.value.zb_code,
-          tbrq: selectedObject.value.tbrq ? selectedObject.value.tbrq : addForm.value.tb_date,
-          tbbj: selectedObject.value.tbbj ? selectedObject.value.tbbj : addForm.value.tb_money,
-          zbkssj: selectedObject.value.zbkssj
-            ? selectedObject.value.zbkssj
-            : addForm.value.zb_startdate,
-          zbjzsj: selectedObject.value.zbjzsj
-            ? selectedObject.value.zbjzsj
-            : addForm.value.zb_enddate,
-          rwlcqd: rwlcqd,
-          zbmsxx: selectedObject.value.zbmsx ? selectedObject.value.zbmsx : desc,
-          c_ytfw: selectedObject.value.ytfxw ? selectedObject.value.ytfxw : addFormYT.value,
-          c_fwlx: selectedObject.value.fwlx
-            ? selectedObject.value.fwlx
-            : addFormFwlx.value.toString(),
-          c_glqy: selectedObject.value.glqy ? selectedObject.value.glqy : areaArr[1] || '',
-        }
-        updateProject(COLUMN.TXB, MODEL.TXB, JSON.stringify(tbFormData))
-      } else {
-        // 流程
-        const rwlcqd = CONFIG.TASKLIST
-        rwlcqd.forEach((p: any) => {
-          if (!p.danger && p.id != 8) {
-            // 未处理的流程全部重新处理
-            ;(p.done = true),
-              (p.create_time = moment().format('YYYY-MM-DD HH:mm')),
-              (p.username = userName),
-              (p.userid = userInfo?.id || ''),
-              (p.type = 0)
-          }
-        })
-        let xmjd_json = []
-        for (let i = 1; i < 8; i++) {
-          if (i == 7) {
-            xmjd_json.push({ xmjd: i, type: 1, create_time: '', username: '', userid: '' })
-          } else {
-            xmjd_json.push({
-              xmjd: i,
-              type: 0,
-              create_time: moment().format('YYYY-MM-DD HH:mm'),
-              username: userName,
-              userid: userInfo?.id || '',
-            })
-          }
-        }
-        const steps = [
-          { n: 1, label: '预警触发' },
-          { n: 2, label: '投/续标准备' },
-          { n: 3, label: '招标监控' },
-          { n: 4, label: '标书制作' },
-          { n: 5, label: '投标提交' },
-          { n: 6, label: '开标监控' },
-          { n: 7, label: '确认中标' },
-        ]
-        let json = xmjd_json.map((item: any) => {
-          return {
-            create_time: item.create_time ? item.create_time : moment().format('YYYY-MM-DD HH:mm'),
-            desc: steps.find((s: any) => s.n == item.xmjd)?.label || '',
-            username: item.username ? item.username : userName,
-            userid: item.userid ? item.userid : userInfo?.id || '',
-          }
-        })
-        // 开标监控
-        let kbFormData = {
-          title: finishObject.value.title,
-          content: finishObject.value.content,
-          c_zbbh: addForm.value.zb_code,
-          c_sfzb: true,
-          c_jgjk_url: finishBidObjectTemp.value?.url || '',
-          c_kbsj: addForm.value.zb_enddate,
-          c_xmmc: finishObject.value.c_xmmc,
-          c_zbtjr: addForm.value.tb_date,
-          c_tbbj: addForm.value.tb_money,
-          c_glxm: finishObject.value.c_guid,
-          c_lcsjx: json || '',
-          c_type: '中标登记',
-          c_enable: false,
-        }
-        const res = await addContentParams(kbFormData, COLUMN.KB_MONITOR, MODEL.KB_MONITOR)
-        let desc = {
-          zb_project_code: addForm.value.zb_code,
-          zb_source_name: addForm.value.zb_source,
-          zb_project_name: addForm.value.zb_name,
-          zb_budget_amount: addForm.value.tb_money,
-          zb_detail_url: finishBidObjectTemp.value?.url || '',
-          zb_publish_time: addForm.value.zb_startdate,
-          zb_bid_deadline: addForm.value.zb_enddate,
-          zb_author: userName,
-        }
-        let areaArr = (areaObject.value as any)?.pathLabels || []
-        let tbFormData = {
-          title: finishObject.value.title,
-          content: finishObject.value.content,
-          c_sfzb: finishObject.value.c_htqyjf,
-          c_zgfw: addFormZgfw.value ? addFormZgfw.value : finishObject.value.c_wtfs || '',
-          c_kssj: moment(finishObject.value.c_zgxmhtksrq).format('YYYY-MM-DD HH:mm') || '',
-          c_dqsj: moment(finishObject.value.c_zgxmhtdqrq).format('YYYY-MM-DD HH:mm') || '',
-          xmbh: finishObject.value.c_guid,
-          c_nh: finishObject.value.c_nhhtsr || '',
-          c_is_xb: addForm.value.c_is_xb,
-          c_xgzb: addForm.value.zb_code,
-          c_xgjk: (res as any).content || '',
-          c_xmbtqc: finishObject.value.c_xmmc,
-          c_qx: areaArr[0] ? areaArr[0] : finishObject.value.c_sq || '',
-          c_xmbt: finishObject.value.c_xmmc,
-          c_xmjd_json: xmjd_json,
-          zbxmbh: addForm.value.zb_code,
-          tbrq: addForm.value.tb_date,
-          tbbj: addForm.value.tb_money,
-          zbkssj: addForm.value.zb_startdate,
-          zbjzsj: addForm.value.zb_enddate,
-          rwlcqd: rwlcqd,
-          zbmsxx: desc,
-          c_ytfw: addFormYT.value,
-          c_fwlx: addFormFwlx.value.toString(),
-          c_glqy: areaArr[1] ? areaArr[1] : finishObject.value.c_glqynbkj || '',
-        }
-        await addContentParams(tbFormData, COLUMN.TXB, MODEL.TXB)
-      }
+const handleTxbChange = (id: any) => {
+  const row = txbList.value.find((t) => t.id === id)
+  selectedTxb.value = row || null
+  parentProject.value = null
+  parentCascader.value = []
+  if (!row) return
+  addForm.value.c_is_xb = !!row.c_is_xb
+  addForm.value.c_xmmc = row.c_xmbtqc || row.c_xmbt || row.title || ''
+  addForm.value.c_htqyjf = row.c_fbz || ''
+  // 续标:用投续标「原项目父编号」xmbh 自动匹配父级在管项目
+  if (row.c_is_xb) {
+    if (row.xmbh) {
+      matchParentByXmbh(row.xmbh)
     } else {
+      ElMessage.warning('该投续标为续标但未关联原项目编号,请手动选择父级在管项目')
     }
-
-    close()
-  })
+  }
 }
 
-const addContentParams = async (formData: any, columnId: number, modelId: number) => {
-  const params = {
-    content: JSON.stringify(formData),
-    columnId: columnId,
-    modelId: modelId,
+const handleXBSwitchChange = (val: any) => {
+  if (addForm.value.type === '进行中投续标') return
+  addForm.value.c_is_xb = !!val
+  if (!val) {
+    parentCascader.value = []
+    parentProject.value = null
   }
-  const res = await api.addContent(params)
-  return res
 }
 
-const handleTypeChange = (val: any) => {
-  addForm.value.title = ''
-  addForm.value.type = val || ''
+const handleParentCascaderChange = (val: any) => {
+  parentCascader.value = val || []
+  const tempObj = finishTreeProjects.value.find((item: any) =>
+    String(item.id).includes(String(val?.[1]))
+  )
+  parentProject.value = tempObj || null
+  if (tempObj) fillFromParent(tempObj)
 }
 
-const selectedObject = ref<any>({})
-const handleProjectChange = async (val: any) => {
-  addForm.value.title = val.title || ''
-  jkBidObject.value = null
-  txbBidObject.value = null
-  finishBidObject.value = null
-  if (addForm.value.type == '监控中未中标') {
-    jkBidObject.value = val
-    const res = await getTxbBidContent(COLUMN.TXB, val.id, 'c_xgjk')
-    txbBidObject.value = res || {}
-  } else {
-    selectedObject.value = val
+const fillFromParent = (p: any) => {
+  if (!addForm.value.c_xmmc) addForm.value.c_xmmc = p.c_xmmc || ''
+  if (!addForm.value.c_xmdz) addForm.value.c_xmdz = p.c_xmdz || ''
+  if (!addForm.value.c_htqyjf) addForm.value.c_htqyjf = p.c_htqyjf || ''
+  addForm.value.c_htmj =
+    p.c_htmj != null && p.c_htmj !== '' ? Number(p.c_htmj) : addForm.value.c_htmj
+  addForm.value.c_htzj =
+    p.c_hte != null && p.c_hte !== ''
+      ? Number(p.c_hte)
+      : p.c_htzj != null && p.c_htzj !== ''
+        ? Number(p.c_htzj)
+        : addForm.value.c_htzj
+  if (p.c_hzyt) {
+    addForm.value.c_hzyt = p.c_hzyt
+    addFormYT.value = p.c_hzyt
+  }
+  if (p.c_wtfs != null && p.c_wtfs !== '') {
+    addFormZgfw.value = String(p.c_wtfs)
+    addForm.value.c_wtfs = String(p.c_wtfs)
+  }
+  if (p.c_jffs != null && p.c_jffs !== '') {
+    addFormJffs.value = String(p.c_jffs)
+    addForm.value.c_jffs = String(p.c_jffs)
+  }
+  if (p.c_fkms != null && p.c_fkms !== '') {
+    addFormFkms.value = String(p.c_fkms)
+    addForm.value.c_fkms = String(p.c_fkms)
   }
 }
 
-const getTypeProjects = async (columnId: number) => {
-  const params: any = {
-    columnId: columnId,
-    orderBy: JSON.stringify([{ field: 'update_time', orderByType: 2 }]),
-    states: '0,1,2,3,4',
-    pageSize: 9999,
-    page: 0,
-  }
-  const res = await getDmsDataList(params)
-  return (res as any).content
+const handleNodeAreaClick = () => {
+  const nodes = (formCompRef.value?.areaCascader as any)?.getCheckedNodes?.() || []
+  if (!nodes.length) return
+  const leaf = nodes[nodes.length - 1]
+  const path = leaf?.pathValues || addFormArea.value
+  areaCodes.value = Array.isArray(path) ? [...path] : []
+  const district = (props.glArea as any[]).find((d: any) => d.value === areaCodes.value[0])
+  const part = district?.children?.find((c: any) => c.value === areaCodes.value[1])
+  addForm.value.c_glqynbkj = part?.label || leaf?.label || ''
+  addForm.value.c_sfzxs = district?.provinceName || addForm.value.c_sfzxs || '上海市'
+  addForm.value.c_sq = district?.cityName || addForm.value.c_sq || ''
 }
 
-const getDmsDataList = async (params: any) => {
-  return await api.getDmsDataList(params)
+const clearTenderUpload = () => {
+  tenderFileRaw.value = null
+  tenderFilePath.value = ''
+  addForm.value.tenderFile = null
 }
 
-const updateProject = async (columnId: number, modelId: number, content: string) => {
-  const params: any = {
-    columnId: columnId,
-    modelId: modelId,
-    content: content,
+const handleTenderFileChange = async (file: any) => {
+  if (!file) {
+    clearTenderUpload()
+    formCompRef.value?.formRef?.validateField?.('tenderFile')
+    return
   }
-  const res = await api.updateContent(params)
-  if ((res as any).code === 200) {
-    // ElMessage.success('添加成功')
-    initData()
-  } else {
-    // ElMessage.error('添加失败')
+  const raw: File | undefined = file?.raw
+  if (!raw) return
+
+  tenderFileRaw.value = raw
+  tenderFilePath.value = ''
+  addForm.value.tenderFile = file
+  tenderUploading.value = true
+  const loading = ElLoading.service({
+    lock: true,
+    text: '标书上传并校验中,文件较大请耐心等待…',
+    background: 'rgba(0, 0, 0, 0.7)',
+  })
+  try {
+    const result = await uploadAndVerifyFile(raw, COLUMN.SOURCE)
+    if (!result.ok) {
+      clearTenderUpload()
+      ElMessage.error(result.message)
+      return
+    }
+    tenderFilePath.value = result.filePath
+    ElMessage.success(result.message || '标书上传并校验通过')
+  } catch (e: any) {
+    clearTenderUpload()
+    ElMessage.error(e?.message || '标书上传失败')
+  } finally {
+    loading.close()
+    tenderUploading.value = false
+    formCompRef.value?.formRef?.validateField?.('tenderFile')
   }
 }
 
-const handleXBSwitchChange = (val: any) => {
-  addForm.value.c_is_xb = val
+const putDateTime = (content: Record<string, any>, key: string, value: string) => {
+  if (!value) return
+  const m = moment(value)
+  if (!m.isValid()) return
+  content[key] = m.valueOf()
 }
 
-/** 获取投续标内容 */
-const getTxbBidContent = async (columnId: number, search: string, type: string) => {
-  const params: any = {
-    columnId: columnId,
-    orderBy: JSON.stringify([{ field: 'update_time', orderByType: 2 }]),
-    states: '0,1,2,3,4',
+const findLatestSourceByGuidPrefix = async (prefix: string) => {
+  const res = await api.getDmsDataList({
+    columnId: COLUMN.SOURCE,
+    states: '0,1,2,3',
     pageSize: 1,
     page: 0,
-  }
-  if (search) {
-    params.search = JSON.stringify([
-      ...(params.search ? JSON.parse(params.search) : []),
-      {
-        field: type,
-        searchType: 1,
-        content: { value: search },
-      },
-    ])
-  }
-  const res = await getDmsDataList(params)
-  return (res as any).content.data[0]
+    orderBy: JSON.stringify([{ field: 'update_time', orderByType: 2 }]),
+    search: JSON.stringify([
+      { field: 'c_guid', searchType: 2, content: { value: `%${prefix}%` } },
+    ]),
+  })
+  if ((res as any).code === 200) return (res as any).content.data?.[0] || null
+  return null
 }
 
-const handleNodeAreaClick = () => {
-  const nodes = (formCompRef.value?.areaCascader as any)?.getCheckedNodes?.() || []
-  if (nodes.length) {
-    const targetNode = nodes[nodes.length - 1]
-    areaObject.value = targetNode
-  }
+/** 按字段模糊查栏目内容 */
+const findContentsByField = async (columnId: number, field: string, value: string) => {
+  const res = await api.getDmsDataList({
+    columnId,
+    states: '0,1,2,3',
+    pageSize: 50,
+    page: 0,
+    search: JSON.stringify([
+      { field, searchType: 2, content: { value: `%${value}%` } },
+    ]),
+  })
+  if ((res as any)?.code === 200) return (res as any).content?.data || []
+  return []
 }
 
-const handleAdminCascaderChange = (val: any) => {
-  const tempObj = finishTreeProjects.value.filter((item: any) => item.id.includes(val[1]))[0]
-  finishObject.value = tempObj
+/** 中标登记收口:关联招标监控 c_gl_xm=投续标 id → c_complete/c_enable */
+const closeRelatedZbMonitors = async (txbId: string | number) => {
+  const rows = await findContentsByField(COLUMN.ZB_MONITOR, 'c_gl_xm', String(txbId))
+  for (const row of rows) {
+    if (!row?.id) continue
+    const res = await api.updateContent({
+      columnId: String(COLUMN.ZB_MONITOR),
+      modelId: String(MODEL.ZB_MONITOR),
+      content: JSON.stringify({
+        id: row.id,
+        c_complete: true,
+        c_enable: true,
+      }),
+    } as any)
+    if ((res as any)?.code !== 200) {
+      ElMessage.warning('项目已创建,但关联招标监控收口失败')
+    }
+  }
 }
-</script>
 
-<style scoped>
-.bid-content-page {
-  width: 100%;
+/** 中标登记收口:改关联开标监控(不新增) */
+const updateRelatedKbMonitor = async (opts: {
+  kbId: string | number
+  xmmc: string
+  glxm: string
+}) => {
+  const res = await api.updateContent({
+    columnId: String(COLUMN.KB_MONITOR),
+    modelId: String(MODEL.KB_MONITOR),
+    content: JSON.stringify({
+      id: opts.kbId,
+      c_sfzb: true,
+      c_xmmc: opts.xmmc,
+      c_glxm: opts.glxm,
+      c_type: '中标登记',
+      c_enable: false,
+      c_sfts: true,
+      c_tssj: moment().format('YYYY-MM-DD HH:mm:ss'),
+    }),
+  } as any)
+  if ((res as any)?.code !== 200) {
+    ElMessage.warning('项目已创建,但关联开标监控更新失败')
+  }
 }
-.bid-container {
-  height: calc(100vh - 180px);
-  overflow-y: auto;
+
+const submitForm = () => {
+  const formEl = formCompRef.value?.formRef
+  if (!formEl) return
+  // 同步业态/字典到表单
+  addForm.value.c_hzyt = addFormYT.value || addForm.value.c_hzyt
+  addForm.value.c_wtfs = addFormZgfw.value
+  addForm.value.c_jffs = addFormJffs.value
+  addForm.value.c_fkms = addFormFkms.value
+
+  formEl.validate(async (valid) => {
+    if (!valid) return
+    if (!tenderFilePath.value) {
+      ElMessage.error('请先上传并通过校验的项目标书')
+      return
+    }
+    if (!areaCodes.value || areaCodes.value.length < 2) {
+      // 尝试从 addFormArea 取
+      if (addFormArea.value?.length >= 2) {
+        areaCodes.value = [...addFormArea.value]
+        handleNodeAreaClick()
+      }
+    }
+    if (!areaCodes.value || areaCodes.value.length < 2) {
+      ElMessage.error('请选择管理区域(用于生成项目编码)')
+      return
+    }
+    if (addForm.value.c_is_xb && !parentProject.value?.c_guid) {
+      ElMessage.error('续标必须选择父级在管项目')
+      return
+    }
+    if (addForm.value.type === '进行中投续标' && !selectedTxb.value) {
+      ElMessage.error('请选择进行中投续标')
+      return
+    }
+
+    const ytcode =
+      ytList.value.find((item: any) => item.c_yt_name === addForm.value.c_hzyt)?.c_yt_code || ''
+    if (!ytcode) {
+      ElMessage.error('所选业态缺少业态编码,无法生成项目编码')
+      return
+    }
+
+    submitting.value = true
+    try {
+      const hasFather = !!addForm.value.c_is_xb
+      const prefix = `${areaCodes.value[0]}${areaCodes.value[1]}${moment().format('YYYYMMDD')}${ytcode}`
+      const latest = await findLatestSourceByGuidPrefix(prefix)
+      const c_guid = nextSourceGuidFromLatest(latest?.c_guid, {
+        districtCode: areaCodes.value[0],
+        partCode: areaCodes.value[1],
+        ytCode: ytcode,
+        hasFather,
+      })
+
+      const content: any = {
+        title: c_guid,
+        content: c_guid,
+        c_guid,
+        c_father_code: hasFather ? parentProject.value.c_guid : '',
+        c_xmmc: addForm.value.c_xmmc,
+        c_xmdz: addForm.value.c_xmdz,
+        c_htqyjf: addForm.value.c_htqyjf,
+        c_glqynbkj: addForm.value.c_glqynbkj,
+        c_sq: addForm.value.c_sq,
+        c_sfzxs: addForm.value.c_sfzxs || '上海市',
+        c_htmj: addForm.value.c_htmj ?? '',
+        c_htbh: addForm.value.c_htbh,
+        // 表单「合同额」双写 c_hte + c_htzj(元)
+        c_hte:
+          addForm.value.c_htzj != null && addForm.value.c_htzj !== ('' as any)
+            ? String(addForm.value.c_htzj)
+            : '',
+        c_htzj:
+          addForm.value.c_htzj != null && addForm.value.c_htzj !== ('' as any)
+            ? String(addForm.value.c_htzj)
+            : '',
+        c_bndhtys: addForm.value.c_bndhtys ?? '',
+        c_hzyt: addForm.value.c_hzyt,
+        c_wtfs: addForm.value.c_wtfs,
+        c_jffs: addForm.value.c_jffs,
+        c_fkms: addForm.value.c_fkms,
+        c_hts: addForm.value.c_hts,
+        c_xms: addForm.value.c_xms,
+        c_bcxyzj: addForm.value.c_bcxyzj,
+        c_zbdfqk: addForm.value.c_zbdfqk,
+        // 选文件时已上传并通过校验的路径
+        c_history_tender: tenderFilePath.value,
+      }
+      putDateTime(content, 'c_zgxmhtksrq', addForm.value.c_zgxmhtksrq)
+      putDateTime(content, 'c_zgxmhtdqrq', addForm.value.c_zgxmhtdqrq)
+      putDateTime(content, 'c_xmkssj', addForm.value.c_xmkssj)
+      putDateTime(content, 'c_xmdqsj', addForm.value.c_xmdqsj)
+
+      const addRes = await api.addContent({
+        content: JSON.stringify(content),
+        columnId: COLUMN.SOURCE,
+        modelId: MODEL.SOURCE,
+      })
+      if ((addRes as any)?.code !== 200) {
+        ElMessage.error((addRes as any)?.message || '新建项目失败')
+        return
+      }
+      const newId = (addRes as any).content
+      if (!newId) {
+        ElMessage.error('新建项目未返回内容 ID')
+        return
+      }
+
+      // 进行中投续标:终止原流程,并收口关联招标/开标监控
+      if (addForm.value.type === '进行中投续标' && selectedTxb.value?.id) {
+        const term = await api.updateContent({
+          columnId: String(COLUMN.TXB),
+          modelId: String(MODEL.TXB),
+          content: JSON.stringify({
+            id: selectedTxb.value.id,
+            c_terminated: true,
+            c_sfwc: true,
+            c_scxxmglid: String(newId),
+          }),
+        } as any)
+        if ((term as any)?.code !== 200) {
+          ElMessage.warning('项目已创建,但投续标终止状态写入失败')
+        }
+
+        // 已在招标监控:关联 ZB(c_gl_xm=投续标 id)→ 完成并启用
+        await closeRelatedZbMonitors(selectedTxb.value.id)
+
+        // 已在开标监控:改关联 KB(TXB.c_xgjk),不新增
+        if (selectedTxb.value.c_xgjk) {
+          await updateRelatedKbMonitor({
+            kbId: selectedTxb.value.c_xgjk,
+            xmmc: addForm.value.c_xmmc,
+            glxm: hasFather ? parentProject.value?.c_guid || c_guid : c_guid,
+          })
+        }
+      } else {
+        // 新项目登记:无关联开标监控,写一条归档痕迹
+        await api.addContent({
+          content: JSON.stringify({
+            title: addForm.value.c_xmmc,
+            content: addForm.value.c_xmmc,
+            c_sfzb: true,
+            c_xmmc: addForm.value.c_xmmc,
+            c_glxm: hasFather ? parentProject.value?.c_guid || c_guid : c_guid,
+            c_type: '中标登记',
+            c_enable: false,
+            c_sfts: true,
+            c_tssj: moment().format('YYYY-MM-DD HH:mm:ss'),
+          }),
+          columnId: COLUMN.KB_MONITOR,
+          modelId: MODEL.KB_MONITOR,
+        })
+      }
+
+      ElMessage.success('中标登记成功,已生成新在管项目')
+      addForm.value = emptySourceForm()
+      clearTenderUpload()
+      selectedTxb.value = null
+      parentProject.value = null
+      parentCascader.value = []
+      addFormArea.value = []
+      areaCodes.value = []
+      await initData()
+      close()
+    } catch (e) {
+      console.error(e)
+      ElMessage.error('中标登记失败')
+    } finally {
+      submitting.value = false
+    }
+  })
 }
-</style>
+</script>

+ 261 - 156
src/components/bidResult/addContent/AddContentForm.vue

@@ -1,17 +1,19 @@
 <template>
-  <!-- 中标登记表单字段区(不含提交逻辑) -->
+  <!-- 中标登记表单:新项目 / 进行中投续标 → 必建 SOURCE + 必传标书 -->
+  <!-- 滚动放在外层容器,避免 el-form 自带 overflow 导致内容贴滚动条 -->
+  <div class="won-reg-scroll">
   <el-form
     ref="formRef"
-    label-width="auto"
+    label-width="150px"
+    class="won-reg-form"
     :model="addForm"
     :rules="rules"
-    style="max-height: 500px; overflow: auto"
   >
-    <el-form-item label="登记类型">
+    <el-form-item label="登记类型" prop="type">
       <el-select
         v-model="addForm.type"
         placeholder="请选择登记类型"
-        value-key="value"
+        style="width: 100%"
         @change="(v: any) => emit('type-change', v)"
       >
         <el-option
@@ -23,165 +25,208 @@
       </el-select>
     </el-form-item>
 
-    <el-form-item v-if="addForm.type == '监控中未中标'" label="项目名称">
+    <el-form-item
+      v-if="addForm.type === '进行中投续标'"
+      label="进行中投续标"
+      prop="txbId"
+    >
       <el-select
-        v-model="addForm.title"
-        placeholder="请选择项目名称"
-        value-key="id"
-        @change="(v: any) => emit('project-change', v)"
-      >
-        <el-option
-          v-for="item in jkList"
-          :key="item.id"
-          :label="item.title"
-          :value="item"
-        />
-      </el-select>
-    </el-form-item>
-    <el-form-item v-else-if="addForm.type == '投续标中登记'" label="项目名称">
-      <el-select
-        v-model="addForm.title"
-        placeholder="请选择项目名称"
-        value-key="id"
-        @change="(v: any) => emit('project-change', v)"
+        v-model="addForm.txbId"
+        placeholder="请选择投续标项目"
+        filterable
+        style="width: 100%"
+        @change="(v: any) => emit('txb-change', v)"
       >
         <el-option
           v-for="item in txbList"
           :key="item.id"
-          :label="item.title"
-          :value="item"
+          :label="itemTitle(item)"
+          :value="item.id"
         />
       </el-select>
     </el-form-item>
-    <el-form-item v-else label="类似项目名称" prop="title">
-      <el-cascader
-        v-model="adminCascaderModel"
-        :options="areaProject"
-        :show-all-levels="true"
-        style="width: 100%"
-        @change="(v: any) => emit('admin-cascader-change', v)"
-      />
-    </el-form-item>
 
-    <div v-if="addForm.type == '新登记'">
-      <el-form-item label="是否续标">
+    <el-form-item label="是否续标" prop="c_is_xb">
+      <div class="xb-row">
         <el-switch
           v-model="addForm.c_is_xb"
           inline-prompt
           active-text="是"
           inactive-text="否"
           style="background-color: unset"
+          :disabled="addForm.type === '进行中投续标'"
           @change="(v: any) => emit('xb-switch-change', v)"
         />
-      </el-form-item>
-    </div>
-    <el-form-item label="中标结果URL">
-      <el-input v-model="finishBidObjectTemp.url" placeholder="请输入中标结果URL" />
+        <span v-if="addForm.type === '进行中投续标'" class="hint">(取自投续标记录)</span>
+      </div>
     </el-form-item>
 
-    <div v-if="addForm.type != '监控中未中标'">
-      <el-form-item label="管理区域" prop="c_glqy">
-        <el-cascader
-          ref="areaCascader"
-          v-model="areaModel"
-          :options="glArea"
-          :show-all-levels="true"
-          style="width: 100%"
-          @change="() => emit('area-change')"
-        />
-      </el-form-item>
-      <el-form-item label="投标日期" prop="tb_date">
-        <el-date-picker
-          v-model="addForm.tb_date"
-          style="width: 100%"
-          type="date"
-          placeholder="请选择投标日期"
-        />
-      </el-form-item>
-      <el-form-item label="投标报价" prop="tb_money">
-        <el-input-number
-          v-model="addForm.tb_money"
-          style="width: 100%"
-          controls-position="right"
-          placeholder="请输入投标报价"
-        />
-      </el-form-item>
-      <el-form-item label="招标编号" prop="zb_code">
-        <el-input v-model="addForm.zb_code" autocomplete="off" placeholder="请输入招标编号" />
-      </el-form-item>
-      <el-form-item label="招标项目名称" prop="zb_name">
-        <el-input v-model="addForm.zb_name" autocomplete="off" placeholder="请输入招标项目名称" />
-      </el-form-item>
-      <el-form-item label="招标发布机构" prop="zb_org">
-        <el-input v-model="addForm.zb_org" autocomplete="off" placeholder="请输入招标发布机构" />
-      </el-form-item>
-      <el-form-item label="招标数据来源" prop="zb_source">
-        <el-input
-          v-model="addForm.zb_source"
-          autocomplete="off"
-          placeholder="请输入招标数据来源"
-        />
-      </el-form-item>
-      <el-form-item label="招标金额" prop="zb_money">
-        <el-input-number
-          v-model="addForm.zb_money"
-          style="width: 100%"
-          controls-position="right"
-          placeholder="请输入招标金额"
-        />
-      </el-form-item>
-      <el-form-item label="开标发布日期" prop="zb_startdate">
-        <el-date-picker
-          v-model="addForm.zb_startdate"
-          style="width: 100%"
-          type="datetime"
-          placeholder="请选择开标发布日期"
-        />
-      </el-form-item>
-      <el-form-item label="开标截止日期" prop="zb_enddate">
-        <el-date-picker
-          v-model="addForm.zb_enddate"
-          style="width: 100%"
-          type="datetime"
-          placeholder="请选择开标截止日期"
-        />
-      </el-form-item>
-      <el-form-item label="招标详情URL" prop="zb_url">
-        <el-input v-model="addForm.zb_url" autocomplete="off" placeholder="请输入招标详情URL" />
-      </el-form-item>
-    </div>
+    <el-form-item
+      v-if="addForm.c_is_xb"
+      label="父级在管项目"
+      prop="parentCascader"
+    >
+      <el-cascader
+        v-model="parentCascaderModel"
+        :options="areaProject"
+        :show-all-levels="true"
+        filterable
+        style="width: 100%"
+        @change="(v: any) => emit('parent-cascader-change', v)"
+      />
+    </el-form-item>
 
-    <div v-if="addForm.type != '监控中未中标'">
-      <el-form-item label="业态" prop="c_ytfw">
-        <el-tree-select
-          v-model="ytModel"
-          placeholder="请选择业态"
-          :data="ytList"
-          collapse-tags
-          filterable
-          style="width: 100%"
-        />
-      </el-form-item>
-      <el-form-item label="委托类型" prop="c_zgfw">
+    <el-divider content-position="left">新项目信息</el-divider>
+
+    <el-form-item label="项目名称" prop="c_xmmc">
+      <el-input v-model="addForm.c_xmmc" placeholder="请输入项目名称" />
+    </el-form-item>
+    <el-form-item label="项目地址" prop="c_xmdz">
+      <el-input v-model="addForm.c_xmdz" placeholder="请输入项目地址" />
+    </el-form-item>
+    <el-form-item label="合同签约甲方" prop="c_htqyjf">
+      <el-input v-model="addForm.c_htqyjf" placeholder="请输入合同签约甲方" />
+    </el-form-item>
+    <el-form-item label="管理区域" prop="c_glqynbkj">
+      <el-cascader
+        ref="areaCascader"
+        v-model="areaModel"
+        :options="glArea"
+        :show-all-levels="true"
+        style="width: 100%"
+        @change="() => emit('area-change')"
+      />
+    </el-form-item>
+    <el-form-item label="合同面积(平方米)" prop="c_htmj">
+      <el-input-number
+        v-model="addForm.c_htmj"
+        style="width: 100%"
+        controls-position="right"
+        placeholder="请输入合同面积"
+      />
+    </el-form-item>
+    <el-form-item label="合同编号" prop="c_htbh">
+      <el-input v-model="addForm.c_htbh" placeholder="请输入合同编号" />
+    </el-form-item>
+    <el-form-item label="合同开始日期" prop="c_zgxmhtksrq">
+      <el-date-picker
+        v-model="addForm.c_zgxmhtksrq"
+        style="width: 100%"
+        type="date"
+        value-format="YYYY-MM-DD"
+        placeholder="请选择合同开始日期"
+      />
+    </el-form-item>
+    <el-form-item label="合同到期日期" prop="c_zgxmhtdqrq">
+      <el-date-picker
+        v-model="addForm.c_zgxmhtdqrq"
+        style="width: 100%"
+        type="date"
+        value-format="YYYY-MM-DD"
+        placeholder="请选择合同到期日期"
+      />
+    </el-form-item>
+    <el-form-item label="合同额" prop="c_htzj">
+      <el-input-number
+        v-model="addForm.c_htzj"
+        style="width: 100%"
+        controls-position="right"
+        :min="0"
+        :precision="2"
+        placeholder="请输入合同额(元)"
+      />
+    </el-form-item>
+    <el-form-item label="合同总月数" prop="c_bndhtys">
+      <el-input-number
+        v-model="addForm.c_bndhtys"
+        style="width: 100%"
+        controls-position="right"
+        placeholder="请输入合同总月数"
+      />
+    </el-form-item>
+    <el-form-item label="业态" prop="c_hzyt">
+      <el-tree-select
+        v-model="ytModel"
+        placeholder="请选择业态"
+        :data="ytList"
+        collapse-tags
+        filterable
+        style="width: 100%"
+      />
+    </el-form-item>
+    <el-form-item label="委托类型" prop="c_wtfs">
+      <el-radio-group v-model="zgfwModel">
         <el-radio
           v-for="item in zgfwOptions"
           :key="item.value"
-          v-model="zgfwModel"
-          :label="item.value"
+          :value="item.value"
+          >{{ item.label }}</el-radio
+        >
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item label="计费方式" prop="c_jffs">
+      <el-radio-group v-model="jffsModel">
+        <el-radio
+          v-for="item in jffsOptions"
+          :key="item.value"
+          :value="item.value"
+          >{{ item.label }}</el-radio
+        >
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item label="付款模式" prop="c_fkms">
+      <el-radio-group v-model="fkmsModel">
+        <el-radio
+          v-for="item in fkmsOptions"
+          :key="item.value"
+          :value="item.value"
           >{{ item.label }}</el-radio
         >
-      </el-form-item>
-      <el-form-item label="服务类型" prop="c_fwlx">
-        <el-checkbox-group v-model="fwlxModel">
-          <el-checkbox label="会务"></el-checkbox>
-          <el-checkbox label="工程"></el-checkbox>
-          <el-checkbox label="绿化"></el-checkbox>
-          <el-checkbox label="保安"></el-checkbox>
-          <el-checkbox label="保洁"></el-checkbox>
-        </el-checkbox-group>
-      </el-form-item>
-    </div>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item label="项目开始时间" prop="c_xmkssj">
+      <el-date-picker
+        v-model="addForm.c_xmkssj"
+        style="width: 100%"
+        type="date"
+        value-format="YYYY-MM-DD"
+        placeholder="请选择项目开始时间"
+      />
+    </el-form-item>
+    <el-form-item label="项目到期时间" prop="c_xmdqsj">
+      <el-date-picker
+        v-model="addForm.c_xmdqsj"
+        style="width: 100%"
+        type="date"
+        value-format="YYYY-MM-DD"
+        placeholder="请选择项目到期时间"
+      />
+    </el-form-item>
+
+    <el-form-item label="项目标书" prop="tenderFile" required>
+      <div class="tender-upload">
+        <el-upload
+          :auto-upload="false"
+          :limit="1"
+          :disabled="tenderUploading"
+          :on-change="(f: any) => emit('tender-file-change', f)"
+          :on-remove="() => emit('tender-file-change', null)"
+          accept=".docx,.pdf"
+        >
+          <el-button class="btn" :loading="tenderUploading">
+            {{ tenderUploading ? '上传校验中…' : '选择标书文件' }}
+          </el-button>
+          <template #tip>
+            <div class="el-upload__tip">
+              仅支持 .docx / .pdf;选文件后自动上传并校验,文件较大请耐心等待
+            </div>
+          </template>
+        </el-upload>
+        <div v-if="tenderFilePath" class="upload-ok">已上传并通过校验</div>
+      </div>
+    </el-form-item>
   </el-form>
+  </div>
 </template>
 
 <script setup lang="ts">
@@ -192,41 +237,48 @@ const props = defineProps<{
   addForm: Record<string, any>
   rules: FormRules
   typeOptions: { value: string; label: string }[]
-  jkList: any[]
   txbList: any[]
   ytList: any[]
   zgfwOptions: any[]
+  jffsOptions: any[]
+  fkmsOptions: any[]
   glArea: any[]
   areaProject: any[]
-  finishBidObjectTemp: { flag: boolean; url: string }
-  adminCascader: any[]
+  parentCascader: any[]
   addFormArea: any[]
-  addFormZgfw: string
-  addFormFwlx: any[]
+  addFormZgfw: string | number
+  addFormJffs: string | number
+  addFormFkms: string | number
   addFormYt: string
+  tenderUploading?: boolean
+  tenderFilePath?: string
 }>()
 
 const emit = defineEmits<{
-  'update:adminCascader': [value: any[]]
+  'update:parentCascader': [value: any[]]
   'update:addFormArea': [value: any[]]
-  'update:addFormZgfw': [value: string]
-  'update:addFormFwlx': [value: any[]]
+  'update:addFormZgfw': [value: string | number]
+  'update:addFormJffs': [value: string | number]
+  'update:addFormFkms': [value: string | number]
   'update:addFormYt': [value: string]
   'type-change': [value: any]
-  'project-change': [value: any]
-  'admin-cascader-change': [value: any]
+  'txb-change': [value: any]
+  'parent-cascader-change': [value: any]
   'xb-switch-change': [value: any]
   'area-change': []
+  'tender-file-change': [file: any]
 }>()
 
 const formRef = ref<FormInstance>()
-/** 管理区域级联,供编排层读取 checkedNodes */
 const areaCascader = ref(null)
 defineExpose({ formRef, areaCascader })
 
-const adminCascaderModel = computed({
-  get: () => props.adminCascader,
-  set: (v: any[]) => emit('update:adminCascader', v),
+const itemTitle = (item: any) =>
+  item.c_xmbtqc || item.c_xmbt || item.title || `投续标#${item.id}`
+
+const parentCascaderModel = computed({
+  get: () => props.parentCascader,
+  set: (v: any[]) => emit('update:parentCascader', v),
 })
 const areaModel = computed({
   get: () => props.addFormArea,
@@ -234,14 +286,67 @@ const areaModel = computed({
 })
 const zgfwModel = computed({
   get: () => props.addFormZgfw,
-  set: (v: string) => emit('update:addFormZgfw', v),
+  set: (v: string | number) => emit('update:addFormZgfw', v),
+})
+const jffsModel = computed({
+  get: () => props.addFormJffs,
+  set: (v: string | number) => emit('update:addFormJffs', v),
 })
-const fwlxModel = computed({
-  get: () => props.addFormFwlx,
-  set: (v: any[]) => emit('update:addFormFwlx', v),
+const fkmsModel = computed({
+  get: () => props.addFormFkms,
+  set: (v: string | number) => emit('update:addFormFkms', v),
 })
 const ytModel = computed({
   get: () => props.addFormYt,
   set: (v: string) => emit('update:addFormYt', v),
 })
 </script>
+
+<style scoped>
+.won-reg-scroll {
+  max-height: 520px;
+  overflow-y: scroll;
+  /* 内容与滚动条之间留白(滚动在外层,避免贴住输入框) */
+  padding: 4px 16px 8px 0;
+  scrollbar-width: thin;
+  scrollbar-color: rgba(163, 166, 173, 0.65) rgba(0, 0, 0, 0.04);
+}
+
+/* 覆盖全局「默认隐藏、悬停才显示」,弹窗内始终显示细滚动条 */
+.won-reg-scroll::-webkit-scrollbar {
+  width: 6px;
+}
+.won-reg-scroll::-webkit-scrollbar-thumb {
+  display: block !important;
+  background-color: rgba(163, 166, 173, 0.65);
+  border-radius: 6px;
+}
+.won-reg-scroll::-webkit-scrollbar-track {
+  background-color: rgba(0, 0, 0, 0.04);
+  border-radius: 6px;
+}
+
+.xb-row {
+  display: flex;
+  align-items: center;
+  min-height: 32px;
+  gap: 8px;
+}
+.hint {
+  font-size: 12px;
+  color: var(--text2, #6b7280);
+  white-space: nowrap;
+}
+.tender-upload {
+  width: 100%;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 4px;
+}
+.upload-ok {
+  font-size: 12px;
+  line-height: 1.4;
+  color: var(--el-color-success, #16a34a);
+}
+</style>

+ 421 - 262
src/components/bidResult/addProject.vue

@@ -1,251 +1,383 @@
 <template>
-  <div class="bid-content-page">
-    <div class="bid-container">
-      <el-dialog v-model="dialogFormVisible" draggable title="新项目信息登记" width="600" @close="close">
-        <WonBidProjectForm
-          ref="formCompRef"
-          :add-form="addForm"
-          :rules="rules"
-          :gl-area="props.glArea"
-          :yt-list="ytList"
-          :zgfw-options="zgfwOptions"
-          :zbjg-options="zbjgOptions"
-          v-model:add-form-area="addFormArea"
-          v-model:add-form-zgfw="addFormZgfw"
-          v-model:add-form-zbjg="addFormZbjg"
-          v-model:add-form-fwlx="addFormFwlx"
-          v-model:add-form-yt="addFormYT"
-          @area-change="handleNodeAreaClick"
-          @open-map="openMapForm"
-        />
-        <template #footer>
-          <div class="dialog-footer">
-            <el-button @click="close" class="btn">取消</el-button>
-            <el-button type="primary" class="btn enter-btn" @click="submitForm"> 确认 </el-button>
-          </div>
-        </template>
-      </el-dialog>
-
-      <ProjectMapSection
-        v-model:visible="mapFormVisible"
-        v-model="geoData"
-        @confirm="handleMapConfirm"
-      />
-
-      <el-dialog
-        v-model="dialogVisible"
-        title="提示信息"
-        width="300"
-        align-center
-        draggable
-        show-close="false"
-      >
-        <span
-          >本项目已推送至市场部负责人,后续将由市场部统一分发至各部门负责人,相关后续流程敬请期待。</span
-        >
-        <template #footer>
-          <div class="dialog-footer">
-            <el-button type="primary" @click="handleConfirm"> 确认 </el-button>
-          </div>
-        </template>
-      </el-dialog>
-    </div>
-  </div>
+  <!-- 确认中标·一键推送:新项目信息登记(字段对齐线上 SOURCE;参考中标登记并保留投续标建项常用项) -->
+  <el-dialog
+    :model-value="flag"
+    draggable
+    title="新项目信息登记"
+    width="640"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    append-to-body
+    @close="close"
+  >
+    <WonBidProjectForm
+      ref="formCompRef"
+      :add-form="addForm"
+      :rules="rules"
+      :gl-area="props.glArea"
+      :yt-list="ytList"
+      :zgfw-options="zgfwOptions"
+      :jffs-options="jffsOptions"
+      :fkms-options="fkmsOptions"
+      :tender-uploading="tenderUploading"
+      :tender-file-path="tenderFilePath"
+      v-model:add-form-area="addFormArea"
+      v-model:add-form-zgfw="addFormZgfw"
+      v-model:add-form-jffs="addFormJffs"
+      v-model:add-form-fkms="addFormFkms"
+      v-model:add-form-yt="addFormYT"
+      @area-change="handleNodeAreaClick"
+      @tender-file-change="handleTenderFileChange"
+    />
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button class="btn" @click="close">取消</el-button>
+        <el-button type="primary" class="btn enter-btn" :loading="submitting" @click="submitForm">
+          确认
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+
+  <el-dialog
+    v-model="dialogVisible"
+    title="提示信息"
+    width="300"
+    align-center
+    draggable
+    :show-close="false"
+    :close-on-click-modal="false"
+    append-to-body
+  >
+    <span
+      >本项目已推送至市场部负责人,后续将由市场部统一分发至各部门负责人,相关后续流程敬请期待。</span
+    >
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button type="primary" @click="handleConfirm">确认</el-button>
+      </div>
+    </template>
+  </el-dialog>
 </template>
 
 <script setup lang="ts">
 import { ref, onMounted } from 'vue'
 import moment from 'moment'
+import { ElMessage, ElLoading } from 'element-plus'
 import api from '@/api/common'
 import { COLUMN, MODEL } from '@/constants/dmsColumns'
 import { getDisplayUserName } from '@/utils/displayUser'
+import { sourceProjectAddress, extractWinningAmountYuan, extractWinningScore } from '@/utils/util'
+import { uploadAndVerifyFile } from '@/utils/uploadAndVerify'
 import type { FormRules } from 'element-plus'
 import WonBidProjectForm from '@/components/bidResult/WonBidProjectForm.vue'
-import ProjectMapSection from '@/components/bidResult/ProjectMapSection.vue'
 
 const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
 const userName = getDisplayUserName(userInfo)
-const geoData = ref<GeoJSON.Feature<GeoJSON.Polygon> | null>(null)
 const dialogVisible = ref(false)
-const mapFormVisible = ref(false)
 const formCompRef = ref<InstanceType<typeof WonBidProjectForm>>()
+const tenderFilePath = ref('')
+const tenderUploading = ref(false)
+const submitting = ref(false)
 
 const soureProjects = ref<any>({})
 const txbProjects = ref<any>({})
 
 const props = defineProps({
-  flag: {
-    type: Boolean,
-    default: false,
-  },
-  close: {
-    type: Function,
-    default: () => {},
-  },
-  projectData: {
-    type: Object,
-    default: () => {},
-  },
-  glArea: {
-    type: Array,
-    default: () => [],
-  },
+  flag: { type: Boolean, default: false },
+  close: { type: Function, default: () => {} },
+  projectData: { type: Object, default: () => ({}) },
+  glArea: { type: Array, default: () => [] },
 })
 
-interface resultForm {
-  [key: string]: any
-}
-
-const ytList = ref<resultForm[]>([])
-const addFormZgfw = ref('')
-const addFormZbjg = ref(0)
-const addFormFwlx = ref([])
+const ytList = ref<any[]>([])
+const addFormZgfw = ref<string | number>('')
+const addFormJffs = ref<string | number>('')
+const addFormFkms = ref<string | number>('')
 const addFormYT = ref('')
-const addFormArea = ref([])
-const zgfwOptions = ref<any>([])
-const zbjgOptions = ref<any>([])
+const addFormArea = ref<any[]>([])
+const zgfwOptions = ref<any[]>([])
+const jffsOptions = ref<any[]>([])
+const fkmsOptions = ref<any[]>([])
+
+const emptyForm = () => ({
+  c_guid: '',
+  c_father_code: '',
+  c_xmmc: props.projectData?.c_xmmc || '',
+  c_sfzxs: '上海市',
+  c_sq: '',
+  c_glqynbkj: '',
+  c_xmdz: '',
+  c_hzyt: '',
+  c_wtfs: '' as string | number,
+  c_jffs: '' as string | number,
+  c_fkms: '' as string | number,
+  c_htmj: null as number | null,
+  c_htqyjf: '',
+  c_htbh: '',
+  c_zgxmhtksrq: '',
+  c_zgxmhtdqrq: '',
+  c_bndhtys: null as number | null,
+  c_htzj: null as number | null,
+  c_hts: '1',
+  c_xms: '1',
+  c_xmkssj: '',
+  c_xmdqsj: '',
+  c_xmzbggrq: '',
+  c_xmzbgsrq: '',
+  c_xmtbrq: '',
+  c_xmzbrq: '',
+  c_zbdfqk: null as number | null,
+  c_history_tender: '',
+  tenderFile: null as any,
+})
+
+const addForm = ref(emptyForm())
+
+const rules: FormRules = {
+  c_glqynbkj: [{ required: true, message: '请选择管理区域', trigger: 'change' }],
+  c_xmmc: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
+  c_xmdz: [{ required: true, message: '请输入项目地址', trigger: 'blur' }],
+  c_htqyjf: [{ required: true, message: '请输入合同签约甲方', trigger: 'blur' }],
+  c_zgxmhtksrq: [{ required: true, message: '请选择合同开始日期', trigger: 'change' }],
+  c_zgxmhtdqrq: [{ required: true, message: '请选择合同到期日期', trigger: 'change' }],
+  c_hzyt: [{ required: true, message: '请选择业态', trigger: 'change' }],
+  tenderFile: [
+    {
+      validator: (_r, _v, cb) => {
+        if (!tenderFilePath.value) cb(new Error('请上传并通过校验的项目标书(.docx / .pdf)'))
+        else cb()
+      },
+      trigger: 'change',
+    },
+  ],
+}
 
 onMounted(() => {
   initData()
-  initCName()
+  loadDicts()
 })
 
-const initCName = async () => {
-  const [wtOptions, jfOptions] = await Promise.all([
+const close = () => {
+  props.close()
+}
+
+const loadDicts = async () => {
+  const [wt, jf, fk] = await Promise.all([
     getDmsTypes('sq_wtfs'),
-    getDmsTypes('sq_zbjg'),
+    getDmsTypes('sq_jffs'),
+    getSelectOptions('shenqin_fkms'),
   ])
-  zgfwOptions.value = wtOptions
-  zbjgOptions.value = jfOptions
+  zgfwOptions.value = wt
+  jffsOptions.value = jf
+  fkmsOptions.value = fk
 }
 
 const getDmsTypes = async (cName: string) => {
-  const params = {
-    type: 0,
-    cName: cName,
-  }
-  const res = await api.getDmsTypes(params)
+  const res = await api.getDmsTypes({ type: 0, cName } as any)
   if ((res as any).code === 200) {
-    const ret = (res as any).content
-    return ret.map((item: any) => ({
+    return ((res as any).content || []).map((item: any) => ({
       ...item,
       label: item.name,
-      value: item.index,
+      value: String(item.index),
+    }))
+  }
+  return []
+}
+
+const getSelectOptions = async (sName: string) => {
+  const res = await api.getSelectBySName({ sName })
+  if ((res as any).code === 200 && Array.isArray((res as any).content)) {
+    return (res as any).content.map((item: any) => ({
+      ...item,
+      label: item.name,
+      value: String(item.index),
     }))
-  } else {
-    return []
   }
+  return []
+}
+
+const fmtDate = (v: any) => {
+  if (v == null || v === '') return ''
+  const m = moment(v)
+  return m.isValid() ? m.format('YYYY-MM-DD') : ''
+}
+
+const putDateTime = (content: Record<string, any>, key: string, value: string) => {
+  if (!value) return
+  const m = moment(value)
+  if (!m.isValid()) return
+  content[key] = m.valueOf()
 }
 
 const initData = async () => {
   const itemYtList = await getTypeProjects(COLUMN.SERVICE_TYPE)
-  ytList.value = []
-  ytList.value = itemYtList.data.map((item: any) => ({
+  ytList.value = (itemYtList?.data || []).map((item: any) => ({
     ...item,
     label: item.c_yt_name,
     value: item.c_yt_name,
     children: item.children || [],
   }))
-  // 源项目数据
-  soureProjects.value = await getSoureProjects(COLUMN.SOURCE, props.projectData.c_glxm, 'guid')
 
-  addForm.value.c_xmdz = soureProjects.value.c_query_address || ''
-  addForm.value.c_htqyjf = soureProjects.value.c_htqyjf || ''
-  addForm.value.c_hzyt = soureProjects.value.c_hzyt || ''
-  addFormYT.value = soureProjects.value.c_hzyt || ''
-  addForm.value.c_father_code = soureProjects.value.c_father_code || ''
-  if (soureProjects.value.c_scfwrq) {
-    addForm.value.c_scfwrq = moment(soureProjects.value.c_scfwrq).format('YYYY-MM-DD') || ''
+  soureProjects.value = await getSoureProjects(COLUMN.SOURCE, props.projectData.c_glxm, 'guid')
+  const p = soureProjects.value || {}
+
+  addForm.value.c_xmdz = sourceProjectAddress(p) || addForm.value.c_xmdz
+  addForm.value.c_htqyjf = p.c_htqyjf || ''
+  addForm.value.c_hzyt = p.c_hzyt || ''
+  addFormYT.value = p.c_hzyt || ''
+  addForm.value.c_father_code = p.c_father_code || ''
+  addForm.value.c_htmj = p.c_htmj != null && p.c_htmj !== '' ? Number(p.c_htmj) : null
+  addForm.value.c_htbh = p.c_htbh || ''
+  // 合同额(c_hte) / 总合同额(c_htzj) 均存元;表单只展示「合同额」,提交双写
+  const winAmt = extractWinningAmountYuan(props.projectData?.project)
+  const winScore = extractWinningScore(props.projectData?.project)
+  if (winAmt != null) {
+    addForm.value.c_htzj = winAmt
+  } else if (p.c_hte != null && p.c_hte !== '') {
+    addForm.value.c_htzj = Number(p.c_hte)
+  } else if (p.c_htzj != null && p.c_htzj !== '') {
+    addForm.value.c_htzj = Number(p.c_htzj)
+  }
+  addForm.value.c_bndhtys = p.c_bndhtys != null && p.c_bndhtys !== '' ? Number(p.c_bndhtys) : null
+  addForm.value.c_zgxmhtksrq = fmtDate(p.c_zgxmhtksrq)
+  addForm.value.c_zgxmhtdqrq = fmtDate(p.c_zgxmhtdqrq)
+  addForm.value.c_xmkssj = fmtDate(p.c_xmkssj)
+  addForm.value.c_xmdqsj = fmtDate(p.c_xmdqsj)
+  if (p.c_wtfs != null && p.c_wtfs !== '') {
+    addFormZgfw.value = String(p.c_wtfs)
+    addForm.value.c_wtfs = String(p.c_wtfs)
+  }
+  if (p.c_jffs != null && p.c_jffs !== '') {
+    addFormJffs.value = String(p.c_jffs)
+    addForm.value.c_jffs = String(p.c_jffs)
+  }
+  if (p.c_fkms != null && p.c_fkms !== '') {
+    addFormFkms.value = String(p.c_fkms)
+    addForm.value.c_fkms = String(p.c_fkms)
   }
 
-  // 投续标项目数据
   txbProjects.value = await getSoureProjects(COLUMN.TXB, props.projectData.id, 'txb')
-
-  const zbInfo = txbProjects.value.zbmsxx ? JSON.parse(txbProjects.value.zbmsxx || '{}') : {}
+  const txb = txbProjects.value || {}
+  if (
+    (addForm.value.c_htzj == null || addForm.value.c_htzj === ('' as any)) &&
+    txb.c_nh != null &&
+    txb.c_nh !== ''
+  ) {
+    addForm.value.c_htzj = Number(txb.c_nh)
+  }
+  let zbInfo: any = {}
+  try {
+    zbInfo = txb.zbmsxx ? JSON.parse(txb.zbmsxx || '{}') : {}
+  } catch {
+    zbInfo = {}
+  }
   if (zbInfo.zb_publish_time) {
-    addForm.value.c_xmzbggrq = moment(zbInfo.zb_publish_time).format('YYYY-MM-DD') || ''
+    addForm.value.c_xmzbggrq = fmtDate(zbInfo.zb_publish_time)
+  }
+  if (txb.tbrq) {
+    addForm.value.c_xmtbrq = fmtDate(txb.tbrq)
   }
-  addForm.value.c_xmyxgsrq = ''
-  addForm.value.c_xmtbrq = moment(txbProjects.value.tbrq).format('YYYY-MM-DD') || ''
 
-  const params: any = {
-    columnId: COLUMN.ADMIN_AREA,
-    states: '0,1,2,3',
-    pageSize: 1,
-    page: 0,
+  if (winScore != null) {
+    addForm.value.c_zbdfqk = winScore
+  } else {
+    const score = props.projectData?.c_zbdfqk ?? props.projectData?.project?.c_zbdfqk
+    if (score != null && score !== '') {
+      addForm.value.c_zbdfqk = Number(score)
+    }
   }
-  params.search = JSON.stringify([
-    ...(params.search ? JSON.parse(params.search) : []),
-    {
-      field: 'c_part_name',
-      searchType: 2,
-      content: { value: `%${soureProjects.value.c_glqynbkj}%` },
-    },
-  ])
-  if (soureProjects.value.c_glqynbkj) {
+
+  if (p.c_glqynbkj) {
+    const params: any = {
+      columnId: COLUMN.ADMIN_AREA,
+      states: '0,1,2,3',
+      pageSize: 1,
+      page: 0,
+      search: JSON.stringify([
+        {
+          field: 'c_part_name',
+          searchType: 2,
+          content: { value: `%${p.c_glqynbkj}%` },
+        },
+      ]),
+    }
     const res = await getDmsDataList(params)
-    const data = (res as any).content.data[0]
-    addFormArea.value = [data?.c_district_code || '', data?.c_part_code || ''] as unknown as never[]
-    addForm.value.c_sq = data.c_district_name || ''
-    addForm.value.c_glqynbkj = data.c_part_name || ''
+    const data = (res as any)?.content?.data?.[0]
+    if (data) {
+      addFormArea.value = [data.c_district_code || '', data.c_part_code || '']
+      addForm.value.c_sq = data.c_district_name || p.c_sq || ''
+      addForm.value.c_glqynbkj = data.c_part_name || p.c_glqynbkj || ''
+      addForm.value.c_sfzxs = data.c_province_name || p.c_sfzxs || '上海市'
+    } else {
+      addForm.value.c_glqynbkj = p.c_glqynbkj || ''
+      addForm.value.c_sq = p.c_sq || ''
+    }
   }
 }
 
-const dialogFormVisible = ref(props.flag)
-const close = () => {
-  props.close()
+const clearTenderUpload = () => {
+  tenderFilePath.value = ''
+  addForm.value.tenderFile = null
+  addForm.value.c_history_tender = ''
 }
 
-const addForm = ref({
-  id: '',
-  title: '',
-  content: '',
-  c_guid: '',
-  c_father_code: '',
-  c_xmmc: props.projectData.c_xmmc,
-  c_sfzxs: '上海市',
-  c_sq: '',
-  c_glqynbkj: '',
-  c_xmdz: '',
-  c_xfyt: '',
-  c_hzyt: '',
-  c_wtfs: '',
-  c_htmj: null,
-  c_htqyjf: '',
-  c_scfwrq: '',
-  c_xmyxgsrq: '',
-  c_xmzbggrq: '',
-  c_xmtbrq: '',
-  c_zbjgrzbdrhxsj: 0,
-  c_xmzbrq: '',
-  c_xmfwhtqdrq: '',
-  c_htbh: '',
-  c_zgxmhtksrq: '',
-  c_zgxmhtdqrq: '',
-  c_bndhtys: null,
-  c_htzj: null,
-  c_zbdfqk: null,
-  c_polygon: '',
-  c_bcdzb: '',
-  c_history_tender: '',
-})
-
-const rules: FormRules = {
-  c_glqynbkj: [{ required: true, message: '请选择管理区域', trigger: 'change' }],
-  c_xmmc: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
-  c_xmdz: [{ required: true, message: '请输入项目地址', trigger: 'blur' }],
-  c_htqyjf: [{ required: true, message: '请输入合同签约甲方', trigger: 'blur' }],
+const handleTenderFileChange = async (file: any) => {
+  if (!file) {
+    clearTenderUpload()
+    formCompRef.value?.formRef?.validateField?.('tenderFile')
+    return
+  }
+  const raw: File | undefined = file?.raw
+  if (!raw) return
+
+  addForm.value.tenderFile = file
+  tenderFilePath.value = ''
+  tenderUploading.value = true
+  const loading = ElLoading.service({
+    lock: true,
+    text: '标书上传并校验中,文件较大请耐心等待…',
+    background: 'rgba(0, 0, 0, 0.7)',
+  })
+  try {
+    const result = await uploadAndVerifyFile(raw, COLUMN.SOURCE)
+    if (!result.ok) {
+      clearTenderUpload()
+      ElMessage.error(result.message)
+      return
+    }
+    tenderFilePath.value = result.filePath
+    addForm.value.c_history_tender = result.filePath
+    ElMessage.success(result.message || '标书上传并校验通过')
+  } catch (e: any) {
+    clearTenderUpload()
+    ElMessage.error(e?.message || '标书上传失败')
+  } finally {
+    loading.close()
+    tenderUploading.value = false
+    formCompRef.value?.formRef?.validateField?.('tenderFile')
+  }
 }
 
 const submitForm = () => {
   const formEl = formCompRef.value?.formRef
   if (!formEl) return
-  formEl.validate((valid: boolean) => {
-    if (valid) {
-      addProject()
-    } else {
-      dialogVisible.value = false
+  addForm.value.c_hzyt = addFormYT.value || addForm.value.c_hzyt
+  addForm.value.c_wtfs = addFormZgfw.value
+  addForm.value.c_jffs = addFormJffs.value
+  addForm.value.c_fkms = addFormFkms.value
+
+  formEl.validate(async (valid: boolean) => {
+    if (!valid) return
+    if (!tenderFilePath.value) {
+      ElMessage.error('请先上传并通过校验的项目标书')
+      return
     }
+    if (!addFormArea.value || addFormArea.value.length < 2) {
+      ElMessage.error('请选择管理区域(用于生成项目编码)')
+      return
+    }
+    await addProject()
   })
 }
 
@@ -256,11 +388,12 @@ const handleConfirm = () => {
 
 const handleNodeAreaClick = (item: any) => {
   addFormArea.value = item
-  props.glArea.forEach((areaItem: any) => {
+  ;(props.glArea as any[]).forEach((areaItem: any) => {
     if (areaItem.value === addFormArea.value[0]) {
       addForm.value.c_sq = areaItem.label
+      addForm.value.c_sfzxs = areaItem.provinceName || areaItem.c_province_name || addForm.value.c_sfzxs || '上海市'
     }
-    areaItem.children.forEach((child: any) => {
+    ;(areaItem.children || []).forEach((child: any) => {
       if (child.value === addFormArea.value[1]) {
         addForm.value.c_glqynbkj = child.label
       }
@@ -270,14 +403,13 @@ const handleNodeAreaClick = (item: any) => {
 
 const getSoureProjects = async (columnId: number, ids: string, type: string) => {
   const params: any = {
-    columnId: columnId,
+    columnId,
     states: '0,1,2,3',
     pageSize: 1,
     page: 0,
   }
   if (ids !== '') {
     params.search = JSON.stringify([
-      ...(params.search ? JSON.parse(params.search) : []),
       {
         field: type === 'guid' ? 'c_guid' : 'c_xgjk',
         searchType: 2,
@@ -288,14 +420,13 @@ const getSoureProjects = async (columnId: number, ids: string, type: string) =>
   const res = await getDmsDataList(params)
   if ((res as any).code == 200) {
     return (res as any).content.data[0]
-  } else {
-    return ''
   }
+  return ''
 }
 
 const getTypeProjects = async (columnId: number) => {
   const params: any = {
-    columnId: columnId,
+    columnId,
     orderBy: JSON.stringify([{ field: 'update_time', orderByType: 2 }]),
     states: '0,1,2,3,4',
     pageSize: 9999,
@@ -309,84 +440,123 @@ const getDmsDataList = async (params: any) => {
   return await api.getDmsDataList(params)
 }
 
-const openMapForm = () => {
-  mapFormVisible.value = true
-}
-
-const handleMapConfirm = (payload: { polygon: string; center: string }) => {
-  addForm.value.c_bcdzb = payload.center
-  addForm.value.c_polygon = payload.polygon
-}
-
 const addProject = async () => {
-  const curDate = moment().format('YYYYMMDD')
-  const ytcode = ytList.value.find((item: any) => item.c_yt_name === addFormYT.value)?.c_yt_code || ''
-  let guid = `${addFormArea.value[0]}${addFormArea.value[1]}${curDate}${ytcode}`
-  const curNewGUID = await getNewSoureProject(COLUMN.SOURCE, guid, 'guid')
-  let count = 0
-  if (curNewGUID) {
-    count =
-      Number(curNewGUID.c_guid.substring(curNewGUID.c_guid.length - 4, curNewGUID.c_guid.length - 2)) ||
-      0
-    count++
-  }
-  addForm.value.c_guid = `${addFormArea.value[0]}${addFormArea.value[1]}${curDate}${ytcode}${
-    count.toString().length > 1 ? count.toString() : '0' + count.toString()
-  }${addForm.value.c_father_code ? '01' : '00'}`
-  addForm.value.title = addForm.value.c_guid
-  addForm.value.content = addForm.value.c_guid
-  addForm.value.c_hzyt = addFormYT.value
-  addForm.value.c_xfyt = addFormYT.value
-  addForm.value.c_wtfs = addFormZgfw.value
-  addForm.value.c_zbjgrzbdrhxsj = addFormZbjg.value
-  addForm.value.c_history_tender = txbProjects.value.c_ai_scbs || ''
-  const params: any = {
-    content: JSON.stringify(addForm.value),
-    columnId: COLUMN.SOURCE,
-    modelId: MODEL.SOURCE,
-  }
-
-  const res = await api.addContent(params)
-  if ((res as any).code === 200) {
-    console.log(res)
-    const ids = (res as any).content
-    dialogVisible.value = true
-    updateProject(props.projectData, true)
-    updaterwlcqdProject({ id: 8 }, ids)
-  } else {
-    dialogVisible.value = false
+  submitting.value = true
+  try {
+    const curDate = moment().format('YYYYMMDD')
+    const ytcode =
+      ytList.value.find((item: any) => item.c_yt_name === addFormYT.value)?.c_yt_code || ''
+    if (!ytcode) {
+      ElMessage.error('所选业态缺少业态编码,无法生成项目编码')
+      return
+    }
+    const guidPrefix = `${addFormArea.value[0]}${addFormArea.value[1]}${curDate}${ytcode}`
+    const curNewGUID = await getSoureProjects(COLUMN.SOURCE, guidPrefix, 'guid')
+    let count = 0
+    if (curNewGUID?.c_guid) {
+      count =
+        Number(
+          curNewGUID.c_guid.substring(curNewGUID.c_guid.length - 4, curNewGUID.c_guid.length - 2)
+        ) || 0
+      count++
+    }
+    const hasFather = !!addForm.value.c_father_code
+    const c_guid = `${addFormArea.value[0]}${addFormArea.value[1]}${curDate}${ytcode}${
+      count.toString().length > 1 ? count.toString() : '0' + count.toString()
+    }${hasFather ? '01' : '00'}`
+
+    const content: any = {
+      title: c_guid,
+      content: c_guid,
+      c_guid,
+      c_father_code: addForm.value.c_father_code || '',
+      c_xmmc: addForm.value.c_xmmc,
+      c_xmdz: addForm.value.c_xmdz,
+      c_htqyjf: addForm.value.c_htqyjf,
+      c_glqynbkj: addForm.value.c_glqynbkj,
+      c_sq: addForm.value.c_sq,
+      c_sfzxs: addForm.value.c_sfzxs || '上海市',
+      c_htmj: addForm.value.c_htmj ?? '',
+      c_htbh: addForm.value.c_htbh,
+      // 表单「合同额」双写:c_hte(合同额)+ c_htzj(总合同额),单位元
+      c_hte:
+        addForm.value.c_htzj != null && addForm.value.c_htzj !== ('' as any)
+          ? String(addForm.value.c_htzj)
+          : '',
+      c_htzj:
+        addForm.value.c_htzj != null && addForm.value.c_htzj !== ('' as any)
+          ? String(addForm.value.c_htzj)
+          : '',
+      c_bndhtys: addForm.value.c_bndhtys ?? '',
+      c_hzyt: addFormYT.value || addForm.value.c_hzyt,
+      c_wtfs: addFormZgfw.value,
+      c_jffs: addFormJffs.value,
+      c_fkms: addFormFkms.value,
+      c_hts: addForm.value.c_hts || '1',
+      c_xms: addForm.value.c_xms || '1',
+      c_zbdfqk: addForm.value.c_zbdfqk ?? '',
+      c_history_tender: tenderFilePath.value,
+    }
+    putDateTime(content, 'c_zgxmhtksrq', addForm.value.c_zgxmhtksrq)
+    putDateTime(content, 'c_zgxmhtdqrq', addForm.value.c_zgxmhtdqrq)
+    putDateTime(content, 'c_xmkssj', addForm.value.c_xmkssj)
+    putDateTime(content, 'c_xmdqsj', addForm.value.c_xmdqsj)
+    putDateTime(content, 'c_xmzbggrq', addForm.value.c_xmzbggrq)
+    putDateTime(content, 'c_xmzbgsrq', addForm.value.c_xmzbgsrq)
+    putDateTime(content, 'c_xmtbrq', addForm.value.c_xmtbrq)
+    putDateTime(content, 'c_xmzbrq', addForm.value.c_xmzbrq)
+
+    const res = await api.addContent({
+      content: JSON.stringify(content),
+      columnId: COLUMN.SOURCE,
+      modelId: MODEL.SOURCE,
+    })
+    if ((res as any).code === 200) {
+      const ids = (res as any).content
+      dialogVisible.value = true
+      await updateProject(props.projectData, true)
+      await updaterwlcqdProject({ id: 8 }, ids)
+    } else {
+      ElMessage.error((res as any)?.message || '新建项目失败')
+    }
+  } catch (e) {
+    console.error(e)
+    ElMessage.error('新建项目失败')
+  } finally {
+    submitting.value = false
   }
 }
 
-// 更新投续标项目流程状态
 const updaterwlcqdProject = async (item: any, ids: string) => {
-  txbProjects.value.rwlcqd = JSON.parse(txbProjects.value.rwlcqd)
-  txbProjects.value.rwlcqd.find((p: any) => p.id == item.id).done = true
-  txbProjects.value.rwlcqd.find((p: any) => p.id == item.id).create_time = moment().format(
-    'YYYY-MM-DD HH:mm'
-  )
-  txbProjects.value.rwlcqd.find((p: any) => p.id == item.id).username = userName
-  txbProjects.value.rwlcqd.find((p: any) => p.id == item.id).userid = userInfo?.id || ''
-  txbProjects.value.rwlcqd.find((p: any) => p.id == item.id).type = 0
-  const params: any = {
+  if (!txbProjects.value?.id) return
+  let rwlcqd = txbProjects.value.rwlcqd
+  try {
+    if (typeof rwlcqd === 'string') rwlcqd = JSON.parse(rwlcqd || '[]')
+  } catch {
+    rwlcqd = []
+  }
+  const task = (rwlcqd || []).find((p: any) => p.id == item.id)
+  if (task) {
+    task.done = true
+    task.create_time = moment().format('YYYY-MM-DD HH:mm')
+    task.username = userName
+    task.userid = userInfo?.id || ''
+    task.type = 0
+  }
+  await api.updateContent({
     columnId: COLUMN.TXB,
     modelId: MODEL.TXB,
     content: JSON.stringify({
       id: txbProjects.value.id,
-      rwlcqd: txbProjects.value.rwlcqd,
+      rwlcqd,
       c_sfwc: true,
       c_scxxmglid: ids,
     }),
-  }
-  await api.updateContent(params)
-}
-
-const getNewSoureProject = async (columnId: number, ids: string, type: string) => {
-  return await getSoureProjects(columnId, ids, type)
+  } as any)
 }
 
 const updateProject = async (item: any, flag: boolean) => {
-  const params: any = {
+  await api.updateContent({
     columnId: COLUMN.KB_MONITOR,
     modelId: MODEL.KB_MONITOR,
     content: JSON.stringify({
@@ -394,17 +564,6 @@ const updateProject = async (item: any, flag: boolean) => {
       c_sfts: flag,
       c_tssj: moment().format('YYYY-MM-DD HH:mm:ss'),
     }),
-  }
-  await api.updateContent(params)
+  } as any)
 }
 </script>
-
-<style scoped>
-.bid-content-page {
-  width: 100%;
-}
-.bid-container {
-  height: calc(100vh - 180px);
-  overflow-y: auto;
-}
-</style>

+ 80 - 44
src/components/bidResult/list/BidMonitorList.vue

@@ -1,6 +1,6 @@
-<!-- 开标监测:统计条 + 监控中的投标卡片列表 + 分页 -->
+<!-- 开标监测:统计条 + 监控中的投标卡片列表 + 分页;仅列表区内滚 -->
 <template>
-  <div>
+  <div class="bid-monitor-page">
     <div class="pool-stats">
       <div
         v-for="stat in bidStats"
@@ -15,7 +15,7 @@
     </div>
 
     <div class="monitor-list">
-      <div class="panel-card">
+      <div class="panel-card monitor-panel">
         <div class="card-header">
           <span class="card-title">监控中的投标</span>
         </div>
@@ -23,47 +23,48 @@
           <span class="empty-icon">🔍</span>
           <p>暂无数据</p>
         </div>
-        <div v-else class="card-body">
-          <div
-            v-for="item in allprojects"
-            :key="item.id"
-            :class="['monitor-card', { active: selectedBid === item.id }, { won: item.c_sfzb == true }]"
-          >
-            <div class="monitor-head">
-              <strong>{{ item.title }}</strong>
-              <span
-                :class="['tag', item.c_sfzb == false ? 'tag-danger' : '']"
-                style="float: right"
-              >
-                {{ item.c_sfzb == true ? '已中标' : '监控中' }}
-              </span>
-            </div>
-            <div class="monitor-meta">
-              <span>开标 {{ moment(item.c_kbsj).format('YYYY-MM-DD HH:mm') }}</span>
-              <span>报价 {{ item.c_tbbj || '未报价' }}</span>
-              <span v-if="item.days > 0">距开标 {{ item.days || '0' }} 天</span>
-            </div>
-            <div>
-              <span class="monitor-status">
-                <span class="pulse-dot"></span>
-                最近检查 {{ moment(item.update_time).format('YYYY-MM-DD HH:mm') }} · 已检查
-                {{ item.jccs || '0' }} 次
-              </span>
-            </div>
-            <div v-if="item.c_sfzb == true">
-              <strong style="color: var(--primary)">中标</strong>
-              <el-button
-                type="primary"
-                class="btn enter-btn"
-                size="small"
-                style="float: right; cursor: pointer"
-                @click="emit('open-detail', item)"
-              >
-                查看中标详情
-              </el-button>
+        <template v-else>
+          <div class="monitor-cards-scroll">
+            <div
+              v-for="item in allprojects"
+              :key="item.id"
+              :class="['monitor-card', { active: selectedBid === item.id }, { won: item.c_sfzb == true }]"
+            >
+              <div class="monitor-head">
+                <strong>{{ item.title }}</strong>
+                <span
+                  :class="['tag', item.c_sfzb == false ? 'tag-danger' : '']"
+                  style="float: right"
+                >
+                  {{ item.c_sfzb == true ? '已中标' : '监控中' }}
+                </span>
+              </div>
+              <div class="monitor-meta">
+                <span>开标 {{ moment(item.c_kbsj).format('YYYY-MM-DD HH:mm') }}</span>
+                <span>报价 {{ item.c_tbbj || '未报价' }}</span>
+                <span v-if="item.days > 0">距开标 {{ item.days || '0' }} 天</span>
+              </div>
+              <div>
+                <span class="monitor-status">
+                  <span class="pulse-dot"></span>
+                  最近检查 {{ moment(item.update_time).format('YYYY-MM-DD HH:mm') }} · 已检查
+                  {{ item.jccs || '0' }} 次
+                </span>
+              </div>
+              <div v-if="item.c_sfzb == true">
+                <strong style="color: var(--primary)">中标</strong>
+                <el-button
+                  type="primary"
+                  class="btn enter-btn"
+                  size="small"
+                  style="float: right; cursor: pointer"
+                  @click="emit('open-detail', item)"
+                >
+                  查看中标详情
+                </el-button>
+              </div>
             </div>
           </div>
-
           <div class="pagination-section">
             <span class="total-text">共{{ total }}个</span>
             <el-pagination
@@ -74,7 +75,7 @@
               @current-change="(p: number) => emit('page-change', p)"
             />
           </div>
-        </div>
+        </template>
       </div>
     </div>
   </div>
@@ -100,10 +101,44 @@ const emit = defineEmits<{
 </script>
 
 <style scoped>
+.bid-monitor-page {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  min-height: 0;
+}
+
 .monitor-list {
   display: flex;
   flex-direction: column;
-  gap: 12px;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.monitor-panel {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.monitor-cards-scroll {
+  flex: 1;
+  min-height: 0;
+  overflow-x: hidden;
+  overflow-y: auto;
+  padding: 16px;
+}
+
+.pagination-section {
+  flex-shrink: 0;
+  border-top: 1px solid #e8ecef;
+  margin: 0 !important;
+  padding: 8px 16px;
+  background: #fff;
 }
 
 .monitor-card {
@@ -179,6 +214,7 @@ const emit = defineEmits<{
   grid-template-columns: repeat(3, 1fr);
   gap: 10px;
   margin-bottom: 16px;
+  flex-shrink: 0;
 }
 
 .pool-stat {

+ 7 - 1
src/components/bidResult/list/BidResultHistoryList.vue

@@ -1,6 +1,6 @@
 <!-- 中标结果 Tab:项目选择、结果确认、推送预览、流程时间线 -->
 <template>
-  <div>
+  <div class="history-result-page">
     <div style="margin-bottom: 16px">
       <el-select
         :model-value="jkObject"
@@ -143,6 +143,12 @@ const emit = defineEmits<{
 </script>
 
 <style scoped>
+.history-result-page {
+  height: 100%;
+  min-height: 0;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
 .markdown-container {
   max-height: 400px;
   overflow-y: auto;

+ 399 - 0
src/components/ledger/DialogXqRenewSource.vue

@@ -0,0 +1,399 @@
+<!-- 台账续签:确认后续签弹窗,展示父项目业务字段,提交后新建 SOURCE -->
+<template>
+  <el-dialog
+    :model-value="visible"
+    draggable
+    title="项目续签登记"
+    width="600"
+    :close-on-click-modal="false"
+    @update:model-value="(v: boolean) => emit('update:visible', v)"
+  >
+    <el-form
+      ref="formRef"
+      class="xq-renew-form"
+      :model="form"
+      :rules="rules"
+      label-width="auto"
+    >
+      <el-form-item label="项目名称" prop="c_xmmc">
+        <el-input v-model="form.c_xmmc" placeholder="请输入项目名称" />
+      </el-form-item>
+      <el-form-item label="项目地址" prop="c_xmdz">
+        <el-input v-model="form.c_xmdz" placeholder="请输入项目地址" />
+      </el-form-item>
+      <el-form-item label="合同签约甲方" prop="c_htqyjf">
+        <el-input v-model="form.c_htqyjf" placeholder="请输入合同签约甲方" />
+      </el-form-item>
+      <el-form-item label="管理区域" prop="c_glqynbkj">
+        <el-cascader
+          v-model="areaCodes"
+          :options="glArea"
+          :show-all-levels="true"
+          style="width: 100%"
+          @change="onAreaChange"
+        />
+      </el-form-item>
+      <el-form-item label="合同面积(平方米)" prop="c_htmj">
+        <el-input-number
+          v-model="form.c_htmj"
+          style="width: 100%"
+          controls-position="right"
+          placeholder="请输入合同面积"
+        />
+      </el-form-item>
+      <el-form-item label="合同编号" prop="c_htbh">
+        <el-input v-model="form.c_htbh" placeholder="请输入合同编号" />
+      </el-form-item>
+      <el-form-item label="合同开始日期" prop="c_zgxmhtksrq">
+        <el-date-picker
+          v-model="form.c_zgxmhtksrq"
+          style="width: 100%"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="请选择合同开始日期"
+        />
+      </el-form-item>
+      <el-form-item label="合同到期日期" prop="c_zgxmhtdqrq">
+        <el-date-picker
+          v-model="form.c_zgxmhtdqrq"
+          style="width: 100%"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="请选择合同到期日期"
+        />
+      </el-form-item>
+      <el-form-item label="合同额" prop="c_htzj">
+        <el-input-number
+          v-model="form.c_htzj"
+          style="width: 100%"
+          controls-position="right"
+          :min="0"
+          :precision="2"
+          placeholder="请输入合同额(元)"
+        />
+      </el-form-item>
+      <el-form-item label="合同总月数" prop="c_bndhtys">
+        <el-input-number
+          v-model="form.c_bndhtys"
+          style="width: 100%"
+          controls-position="right"
+          placeholder="请输入合同总月数"
+        />
+      </el-form-item>
+      <el-form-item label="业态" prop="c_hzyt">
+        <el-tree-select
+          v-model="form.c_hzyt"
+          placeholder="请选择业态"
+          :data="ytList"
+          collapse-tags
+          filterable
+          style="width: 100%"
+        />
+      </el-form-item>
+      <el-form-item label="细分业态" prop="c_xfyt">
+        <el-input v-model="form.c_xfyt" placeholder="请输入细分业态" />
+      </el-form-item>
+      <el-form-item label="委托类型" prop="c_wtfs">
+        <el-radio
+          v-for="item in zgfwOptions"
+          :key="item.value"
+          v-model="form.c_wtfs"
+          :label="item.value"
+          >{{ item.label }}</el-radio
+        >
+      </el-form-item>
+      <el-form-item label="计费方式" prop="c_jffs">
+        <el-radio
+          v-for="item in jffsOptions"
+          :key="item.value"
+          v-model="form.c_jffs"
+          :label="item.value"
+          >{{ item.label }}</el-radio
+        >
+      </el-form-item>
+      <el-form-item label="付款模式" prop="c_fkms">
+        <el-radio
+          v-for="item in fkmsOptions"
+          :key="item.value"
+          v-model="form.c_fkms"
+          :label="item.value"
+          >{{ item.label }}</el-radio
+        >
+      </el-form-item>
+      <el-form-item label="片区负责人" prop="c_pjfzr">
+        <el-input v-model="form.c_pjfzr" placeholder="请输入片区负责人" />
+      </el-form-item>
+      <el-form-item label="项目员工人数" prop="c_xmygrs">
+        <el-input v-model="form.c_xmygrs" placeholder="请输入项目员工人数" />
+      </el-form-item>
+      <el-form-item label="合同数" prop="c_hts">
+        <el-input v-model="form.c_hts" placeholder="请输入合同数" />
+      </el-form-item>
+      <el-form-item label="项目数" prop="c_xms">
+        <el-input v-model="form.c_xms" placeholder="请输入项目数" />
+      </el-form-item>
+      <el-form-item label="项目开始时间" prop="c_xmkssj">
+        <el-date-picker
+          v-model="form.c_xmkssj"
+          style="width: 100%"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="请选择项目开始时间"
+        />
+      </el-form-item>
+      <el-form-item label="项目到期时间" prop="c_xmdqsj">
+        <el-date-picker
+          v-model="form.c_xmdqsj"
+          style="width: 100%"
+          type="date"
+          value-format="YYYY-MM-DD"
+          placeholder="请选择项目到期时间"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button class="btn" @click="emit('update:visible', false)">取消</el-button>
+        <el-button type="primary" class="btn enter-btn" :loading="submitting" @click="onSubmit">
+          确认续签
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+import type { FormInstance, FormRules } from 'element-plus'
+import moment from 'moment'
+
+export type XqRenewForm = {
+  c_xmmc: string
+  c_xmdz: string
+  c_htqyjf: string
+  c_glqynbkj: string
+  /** 不展示:选管理区域时用行政编码的省份名称 / 市级名称填充 */
+  c_sq: string
+  c_htmj: number | null
+  c_htbh: string
+  c_zgxmhtksrq: string
+  c_zgxmhtdqrq: string
+  c_htzj: number | null
+  c_bndhtys: number | null
+  c_hzyt: string
+  c_xfyt: string
+  c_wtfs: string | number
+  c_jffs: string | number
+  c_fkms: string | number
+  c_pjfzr: string
+  c_xmygrs: string
+  c_hts: string
+  c_xms: string
+  c_xmkssj: string
+  c_xmdqsj: string
+  /** 以下不在弹窗展示,预填父项目,确认续签时写入新 SOURCE */
+  c_bcxyzj: string
+  c_zbdfqk: string
+  c_xmzbggrq: string
+  c_xmzbgsrq: string
+  c_xmtbrq: string
+  c_xmzbrq: string
+  c_polygon: any
+  c_history_tender: string
+  /** 不展示:选管理区域时用行政编码的省份名称填充 */
+  c_sfzxs: string
+}
+
+const props = defineProps<{
+  visible: boolean
+  /** 父级在管项目(预填) */
+  parent: Record<string, any>
+  glArea: any[]
+  ytList: any[]
+  zgfwOptions: any[]
+  jffsOptions: any[]
+  /** 付款模式选项(DMS select: shenqin_fkms) */
+  fkmsOptions: any[]
+  submitting?: boolean
+  /** 打开时已反查到的区码/片区码 */
+  initialAreaCodes?: string[]
+}>()
+
+const emit = defineEmits<{
+  'update:visible': [value: boolean]
+  submit: [payload: { form: XqRenewForm; areaCodes: string[] }]
+}>()
+
+const formRef = ref<FormInstance>()
+const areaCodes = ref<string[]>([])
+const form = ref<XqRenewForm>(emptyForm())
+
+const rules: FormRules = {
+  c_xmmc: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
+  c_xmdz: [{ required: true, message: '请输入项目地址', trigger: 'blur' }],
+  c_htqyjf: [{ required: true, message: '请输入合同签约甲方', trigger: 'blur' }],
+  c_glqynbkj: [{ required: true, message: '请选择管理区域', trigger: 'change' }],
+  c_zgxmhtksrq: [{ required: true, message: '请选择合同开始日期', trigger: 'change' }],
+  c_zgxmhtdqrq: [{ required: true, message: '请选择合同到期日期', trigger: 'change' }],
+  c_hzyt: [{ required: true, message: '请选择业态', trigger: 'change' }],
+}
+
+function emptyForm(): XqRenewForm {
+  return {
+    c_xmmc: '',
+    c_xmdz: '',
+    c_htqyjf: '',
+    c_glqynbkj: '',
+    c_sq: '',
+    c_htmj: null,
+    c_htbh: '',
+    c_zgxmhtksrq: '',
+    c_zgxmhtdqrq: '',
+    c_htzj: null,
+    c_bndhtys: null,
+    c_hzyt: '',
+    c_xfyt: '',
+    c_wtfs: '',
+    c_jffs: '',
+    c_fkms: '',
+    c_pjfzr: '',
+    c_xmygrs: '',
+    c_hts: '',
+    c_xms: '',
+    c_bcxyzj: '',
+    c_zbdfqk: '',
+    c_xmkssj: '',
+    c_xmdqsj: '',
+    c_xmzbggrq: '',
+    c_xmzbgsrq: '',
+    c_xmtbrq: '',
+    c_xmzbrq: '',
+    c_polygon: '',
+    c_history_tender: '',
+    c_sfzxs: '上海市',
+  }
+}
+
+/** 字典 index 常为数字,DMS 存的是字符串;对齐到选项 value,单选才能选中 */
+function alignSelect(raw: any, options: any[]) {
+  if (raw == null || raw === '') return ''
+  const key = typeof raw === 'object' ? (raw.index ?? raw.value ?? raw.id ?? '') : raw
+  const hit = options.find((o) => o.value == key || o.index == key)
+  return hit ? hit.value : key
+}
+
+function str(v: any) {
+  if (v == null || v === '') return ''
+  return String(v)
+}
+
+function fmtDate(v: any) {
+  if (!v) return ''
+  return moment(v).format('YYYY-MM-DD')
+}
+
+function fillFromParent(p: Record<string, any>) {
+  form.value = {
+    c_xmmc: p.c_xmmc || '',
+    c_xmdz: p.c_xmdz || p.c_query_address || '',
+    c_htqyjf: p.c_htqyjf || '',
+    c_glqynbkj: p.c_glqynbkj || '',
+    c_sq: p.c_sq || '',
+    c_htmj: p.c_htmj != null && p.c_htmj !== '' ? Number(p.c_htmj) : null,
+    c_htbh: p.c_htbh || '',
+    c_zgxmhtksrq: fmtDate(p.c_zgxmhtksrq),
+    c_zgxmhtdqrq: fmtDate(p.c_zgxmhtdqrq),
+    c_htzj: p.c_hte != null && p.c_hte !== '' ? Number(p.c_hte) : p.c_htzj != null && p.c_htzj !== '' ? Number(p.c_htzj) : null,
+    c_bndhtys: p.c_bndhtys != null && p.c_bndhtys !== '' ? Number(p.c_bndhtys) : null,
+    c_hzyt: p.c_hzyt || '',
+    c_xfyt: p.c_xfyt || '',
+    c_wtfs: alignSelect(p.c_wtfs, props.zgfwOptions),
+    c_jffs: alignSelect(p.c_jffs, props.jffsOptions),
+    c_fkms: alignSelect(p.c_fkms, props.fkmsOptions),
+    c_pjfzr: str(p.c_pjfzr),
+    c_xmygrs: str(p.c_xmygrs),
+    c_hts: str(p.c_hts),
+    c_xms: str(p.c_xms),
+    c_bcxyzj: str(p.c_bcxyzj),
+    c_zbdfqk: str(p.c_zbdfqk),
+    c_xmkssj: fmtDate(p.c_xmkssj),
+    c_xmdqsj: fmtDate(p.c_xmdqsj),
+    c_xmzbggrq: fmtDate(p.c_xmzbggrq),
+    c_xmzbgsrq: fmtDate(p.c_xmzbgsrq),
+    c_xmtbrq: fmtDate(p.c_xmtbrq),
+    c_xmzbrq: fmtDate(p.c_xmzbrq),
+    c_polygon: p.c_polygon || '',
+    c_history_tender: str(p.c_history_tender),
+    c_sfzxs: p.c_sfzxs || '上海市',
+  }
+  areaCodes.value = []
+}
+
+function openFromParent() {
+  if (!props.parent) return
+  fillFromParent(props.parent)
+  if (props.initialAreaCodes?.length === 2) {
+    areaCodes.value = [...props.initialAreaCodes]
+    onAreaChange(areaCodes.value)
+  }
+}
+
+watch(
+  () => props.visible,
+  (v) => {
+    if (v) openFromParent()
+  }
+)
+
+watch(
+  () => props.glArea,
+  () => {
+    if (!props.visible || areaCodes.value.length < 2) return
+    onAreaChange(areaCodes.value)
+  }
+)
+
+/** 字典可能晚于弹窗到达,到达后按父项目再对齐一次 */
+watch(
+  () => [props.zgfwOptions, props.jffsOptions, props.fkmsOptions],
+  () => {
+    if (!props.visible || !props.parent) return
+    form.value.c_wtfs = alignSelect(props.parent.c_wtfs, props.zgfwOptions)
+    form.value.c_jffs = alignSelect(props.parent.c_jffs, props.jffsOptions)
+    form.value.c_fkms = alignSelect(props.parent.c_fkms, props.fkmsOptions)
+  }
+)
+
+const onAreaChange = (val: any) => {
+  if (!Array.isArray(val) || val.length < 2) return
+  const district = props.glArea.find((d: any) => d.value === val[0])
+  if (!district) return
+  const part = district.children?.find((c: any) => c.value === val[1])
+  form.value.c_glqynbkj = part?.label || form.value.c_glqynbkj
+  form.value.c_sfzxs = district.provinceName || form.value.c_sfzxs
+  form.value.c_sq = district.cityName || form.value.c_sq
+}
+
+const onSubmit = () => {
+  formRef.value?.validate((valid) => {
+    if (!valid) return
+    emit('submit', { form: { ...form.value }, areaCodes: [...areaCodes.value] })
+  })
+}
+</script>
+
+<style scoped>
+.xq-renew-form {
+  max-height: 520px;
+  overflow-y: auto;
+  padding-right: 12px;
+}
+.xq-renew-form::-webkit-scrollbar {
+  width: 6px;
+}
+.xq-renew-form::-webkit-scrollbar-thumb {
+  display: block;
+  background-color: rgba(163, 166, 173, 0.6);
+  border-radius: 6px;
+}
+</style>

+ 16 - 52
src/components/ledger/ProjectDetailInfo.vue

@@ -1,5 +1,5 @@
 <template>
-  <!-- 台账详情:项目/合同/金额/历史投标字段区 -->
+  <!-- 台账详情:项目与甲方 / 合同与服务 -->
   <div class="content-grid">
     <div class="card">
       <div class="card-header">
@@ -10,14 +10,6 @@
           <span class="label">区域</span>
           <span class="value">{{ projects.c_glqynbkj }}</span>
         </div>
-        <div class="info-row">
-          <span class="label">金蝶编码</span>
-          <span class="value">XM600161</span>
-        </div>
-        <div class="info-row">
-          <span class="label">序号</span>
-          <span class="value">{{ projects.c_xh }}</span>
-        </div>
         <div class="info-row">
           <span class="label">甲方名称</span>
           <span class="value">{{ projects.c_htqyjf }}</span>
@@ -61,49 +53,22 @@
           <span class="label">计费方式</span>
           <span class="value">{{ jfLabel }}</span>
         </div>
-      </div>
-    </div>
-
-    <div class="card">
-      <div class="card-header">
-        <span class="card-title">合同金额</span>
-      </div>
-      <div class="card-body">
-        <div class="info-row">
-          <span class="label">合同金额</span>
-          <span class="value">{{ UTIL.formatCurrency(projects.c_htzj / 10000) }} 万元</span>
-        </div>
-        <div class="info-row">
-          <span class="label">年化合同额</span>
-          <span class="value">{{ UTIL.formatCurrency(projects.c_nhhtsr / 10000) }} 万元</span>
-        </div>
         <div class="info-row">
-          <span class="label">当年饱和收入</span>
-          <span class="value">{{ UTIL.formatCurrency(projects.c_dnbhsr / 10000) }} 万元</span>
-        </div>
-        <div class="info-row">
-          <span class="label">投标核算年度总成本</span>
-          <span class="value">{{ UTIL.formatCurrency(projects.c_tbhsndzcb / 10000) }} 万元</span>
-        </div>
-        <div class="info-row">
-          <span class="label">自用人力总成本</span>
-          <span class="value">{{ UTIL.formatCurrency(projects.c_zyrlzcbjss / 10000) }} 万元</span>
-        </div>
-        <div class="info-row">
-          <span class="label">综合成本</span>
-          <span class="value">{{ UTIL.formatCurrency(projects.c_zhcbjss / 10000) }} 万元</span>
-        </div>
-      </div>
-    </div>
-
-    <div class="card">
-      <div class="card-header">
-        <span class="card-title">历史投标</span>
-      </div>
-      <div class="card-body">
-        <div class="info-row" v-for="item in historyBidList" :key="item.year">
-          <span class="label">{{ item.year }}年</span>
-          <span class="value">中标 {{ UTIL.formatCurrency(item.money / 10000) }} 万元</span>
+          <span class="label">合同额</span>
+          <span class="value"
+            >{{
+              (() => {
+                const raw =
+                  projects.c_hte != null && projects.c_hte !== ''
+                    ? projects.c_hte
+                    : projects.c_htzj
+                return raw != null && raw !== ''
+                  ? UTIL.formatCurrency(Number(raw) / 10000)
+                  : '-'
+              })()
+            }}
+            万元</span
+          >
         </div>
       </div>
     </div>
@@ -116,7 +81,6 @@ import UTIL from '@/utils/util'
 
 defineProps<{
   projects: any
-  historyBidList: any[]
   wtfwLabel: string
   jfLabel: string
 }>()

+ 1 - 1
src/components/ledger/ProjectDetailMap.vue

@@ -1,5 +1,5 @@
 <template>
-  <!-- 台账详情:地图中心(ProMap 回显面要素) -->
+  <!-- 台账详情:地图中心(按项目编码查申勤驻地表面要素) -->
   <div class="renewal-section">
     <div class="section-header">
       <span class="section-title">地图中心</span>

+ 59 - 17
src/components/map/ProMap.vue

@@ -38,7 +38,8 @@ let map: mapboxgl.Map | null = null
 let draw: MapboxDraw | null = null
 const isLoaded = ref(false)
 const popup = ref<mapboxgl.Popup | null>(null)
-const guid = ref<string>("")
+/** 当前点击驻地内容 id(员工表 c_sszd) */
+const stationId = ref<string>("")
 
 const COLUMNMODELARR = ref({"id":1742,"mid":1793,"name":"申勤员工"})
 //员工列表
@@ -126,11 +127,21 @@ const initMap = () => {
   })
 }
 
-// 通用渲染函数:支持 Geometry / Feature / FeatureCollection
+// 通用渲染函数:支持 Geometry / Feature / FeatureCollection,或含 c_polygon.value 的 DMS 记录
 function renderGeoJson(fet:any, styleConfig = {}) {
-  const geoData = wellknown.parse(fet.c_polygon.value) as any
-  geoData.properties = { ...fet }
-  delete geoData.properties.c_polygon
+  let geoData: any
+  if (fet?.type === 'FeatureCollection' || fet?.type === 'Feature') {
+    geoData = fet
+  } else if (fet?.c_polygon?.value != null) {
+    geoData = wellknown.parse(fet.c_polygon.value) as any
+    if (geoData) {
+      geoData.properties = { ...fet }
+      delete geoData.properties.c_polygon
+    }
+  } else {
+    return
+  }
+  if (!geoData) return
   // 默认样式
   const opt = Object.assign({
     pointColor: '#ff4400',
@@ -277,12 +288,16 @@ const mapClickValue = (e:any)  => {
 
     }
 
+    // 功能键默认隐藏;按驻地 id 查员工表后再决定是否显示(不再依赖滞后的 c_xmygrs)
+    const clickedStationId = String(fet.properties.station_id ?? '')
+    stationId.value = clickedStationId
+
     const popupContent = `
     <div class="popup-header">
         <span class="popup-close-btn" id="popup-close-btn">×</span>
     </div>
     <div style="font-size:14px;line-height:1.6;padding:10px;">
-        <h4 style="margin:0 0 6px 0;color:#222;"><span>内容信息</span><span style="display: ${fet.properties.c_xmygrs>0 ? 'block' : 'none'};" class="more" id="popup-more-btn">员工信息>></span></h4>
+        <h4 style="margin:0 0 6px 0;color:#222;"><span>内容信息</span><span style="display: none;" class="more" id="popup-more-btn">员工信息>></span></h4>
         ${str}
     </div>
     `;
@@ -304,8 +319,7 @@ const mapClickValue = (e:any)  => {
         if (moreBtn) {
             moreBtn.addEventListener('click', () => {
                 tableSearchText.value = "";
-                guid.value = fet.properties.c_guid;
-                handleMoreClick(guid.value,"",0);
+                handleMoreClick(stationId.value, "", 0);
             });
         }
         if (closeBtn) {
@@ -313,9 +327,38 @@ const mapClickValue = (e:any)  => {
                 if (popup.value) popup.value.remove();
             });
         }
+        if (clickedStationId) {
+            probeStationEmployees(clickedStationId)
+        }
     }, 0);
       
 }
+
+/** 按驻地内容 id(员工 c_sszd)探测是否有人,有则显示「员工信息」 */
+const probeStationEmployees = (sid: string) => {
+  const requestParams = {
+    columnId: COLUMNMODELARR.value.id,
+    states: "0,1,2,3",
+    orderBy: JSON.stringify([{ field: "c_xm", orderByType: 2 }]),
+    pageSize: 1,
+    page: 0,
+    search: JSON.stringify([
+      {
+        field: "c_sszd",
+        searchType: 2,
+        content: { value: sid },
+      },
+    ]),
+  }
+  api.getDmsDataList(requestParams).then((res: any) => {
+    // 用户已点到其他驻地时忽略过期响应
+    if (stationId.value !== sid) return
+    if (res.code == 200 && Number(res.content?.count) > 0) {
+      const moreBtn = document.getElementById('popup-more-btn')
+      if (moreBtn) moreBtn.style.display = 'block'
+    }
+  })
+}
     
 // 初始化绘制控件
 const initDraw = () => {
@@ -469,13 +512,12 @@ const handlePageChange = (parPage:number, parPageSize:number, text:string) => {
     page.value = parPage;
     pageSize.value = parPageSize;
     tableSearchText.value = text;
-    handleMoreClick(guid.value,text,page.value);
+    handleMoreClick(stationId.value, text, page.value);
 }
-// 处理弹窗中"更多"按钮点击
-const handleMoreClick = (guid:any,text:any,parPage:any) => {
+// 处理弹窗中「员工信息」:按当前驻地 id 查员工表 c_sszd
+const handleMoreClick = (sid: any, text: any, parPage: any) => {
   page.value = parPage;
-  //1743 项目数据  1742 申勤员工
-  // 排序条件orderByType  1 升序 2 降序
+  // 1742 申勤员工;orderByType 1 升序 2 降序
   let requestParams = {
     columnId: COLUMNMODELARR.value.id,
     states: "0,1,2,3",
@@ -485,14 +527,14 @@ const handleMoreClick = (guid:any,text:any,parPage:any) => {
     search: "",
   };
   let arr = [...(requestParams.search ? JSON.parse(requestParams.search) : [])];
-  if(guid){
+  if (sid) {
     arr.push({
-      field: "c_xmguid",  // 申勤员工 c_xmguid   项目数据 c_guid
+      field: "c_sszd", // 所属驻地 = 驻地表内容 id
       searchType: 2,
-      content: { value: guid },
+      content: { value: sid },
     });
   }
-  if(text){
+  if (text) {
     arr.push({
       field: "c_xm",
       searchType: 2,

+ 1 - 1
src/components/person/Table.vue

@@ -95,7 +95,7 @@ export default {
     },
     created() {
         let that = this;
-        console.log(that.columnModel)
+        // console.log(that.columnModel)
         that.searchText2 = that.searchText;
         that.dialogTableVisible = that.isview;
         let modelFields = JSON.parse(this.columnModel.fieldList);

+ 108 - 56
src/components/renewal/BidRenewal.vue

@@ -16,7 +16,7 @@
 
     <div class="card">
       <div class="card-head">项目列表</div>
-      <div class="card-body" style="padding: 12px 16px 0">
+      <div class="card-body card-filters">
         <BidRenewalFilters
           v-model:search-text="searchText"
           v-model:selected-region="selectedRegion"
@@ -26,22 +26,24 @@
         />
       </div>
 
-      <div class="empty-state" v-if="total === 0">
-        <span class="empty-icon">🔍</span>
-        <p>暂无数据</p>
+      <div class="card-list-area">
+        <div class="empty-state" v-if="total === 0">
+          <span class="empty-icon">🔍</span>
+          <p>暂无数据</p>
+        </div>
+
+        <BidRenewalCardList
+          v-else
+          :renewal-list="renewalList"
+          :column-id="columnId"
+          :total="total"
+          :page="page"
+          :page-size="pageSize"
+          @enter="navigateToRenewalExpireDetail"
+          @page-change="handleCurrentChange"
+        />
       </div>
 
-      <BidRenewalCardList
-        v-else
-        :renewal-list="renewalList"
-        :column-id="columnId"
-        :total="total"
-        :page="page"
-        :page-size="pageSize"
-        @enter="navigateToRenewalExpireDetail"
-        @page-change="handleCurrentChange"
-      />
-
       <BidRenewalAddDialog
         v-model="dialogFormVisible"
         v-model:cascader-arr="cascaderArr"
@@ -73,7 +75,7 @@ import CONFIG from '@/utils/config'
 import { COLUMN, MODEL } from '@/constants/dmsColumns'
 import { calcRemainingBase } from '@/utils/renewalMath'
 import { getDisplayUserName } from '@/utils/displayUser'
-import { resolveDocxHistoryTender } from '@/utils/util'
+import { resolveDocxHistoryTender, sourceContractAmountYuan } from '@/utils/util'
 import { useAdminArea } from '@/composables/useAdminArea'
 import { useNavigateToRenewalDetail } from '@/composables/useNavigateToRenewalDetail'
 import BidRenewalFilters from '@/components/renewal/list/BidRenewalFilters.vue'
@@ -232,49 +234,62 @@ const getTabName = (stat: { label: string; highlight: boolean }) => {
 const submitForm = (formEl: FormInstance | undefined) => {
   if (!formEl) return
   formEl.validate((valid: boolean) => {
-    if (valid) {
+    if (!valid) return
+
+    const doAdd = () => {
+      tempArr.value = []
+      const expireDate = moment(moment(addForm.value.c_dqsj).format('YYYY-MM-DD'), 'YYYY-MM-DD')
+      const today = moment().startOf('day')
+      const days = expireDate.diff(today, 'days')
+
+      if (days >= 100) {
+        // 大于百天,不需要预警
+        for (let i = 1; i < 3; i++) {
+          const str = {
+            xmjd: i,
+            type: i == 2 ? 1 : 0,
+            create_time: moment().format('YYYY-MM-DD HH:mm'),
+            username: userName,
+            userid: userInfo?.id || '',
+          }
+          tempArr.value.push(str)
+        }
+      } else {
+        // 需要预警的项目
+        const str = {
+          xmjd: 1,
+          type: 1,
+          create_time: '',
+          username: userName,
+          userid: userInfo?.id || '',
+        }
+        tempArr.value.push(str)
+      }
+      addForm.value.c_zgfw = addFormZgfw.value
+      addForm.value.c_ytfw = addFormYT.value
+      addForm.value.c_fwlx = addFormFwlx.value.join(',')
+      addForm.value.c_xmjd_json = JSON.stringify(tempArr.value)
+      // 续标/投标:选项目时已写入 c_ckbs;投标不写 xmbh
+
+      dialogFormVisible.value = false
+      addProject()
+    }
+
+    // 仅续标按原项目编号去重;投标为新项目,不去重 xmbh
+    if (addForm.value.c_is_xb) {
+      if (!addForm.value.xmbh) {
+        ElMessage.error('请选择原项目')
+        return
+      }
       getXMProjects(addForm.value.xmbh, 'xmbh').then(async (res) => {
         if (res.length > 0) {
           ElMessage.error('此续标项目已存在,请重新选择')
         } else {
-          tempArr.value = []
-          const expireDate = moment(moment(addForm.value.c_dqsj).format('YYYY-MM-DD'), 'YYYY-MM-DD')
-          const today = moment().startOf('day')
-          const days = expireDate.diff(today, 'days')
-
-          if (days >= 100) {
-            // 大于百天,不需要预警
-            for (let i = 1; i < 3; i++) {
-              const str = {
-                xmjd: i,
-                type: i == 2 ? 1 : 0,
-                create_time: moment().format('YYYY-MM-DD HH:mm'),
-                username: userName,
-                userid: userInfo?.id || ''
-              }
-              tempArr.value.push(str)
-            }
-          } else {
-            // 需要预警的项目
-            const str = {
-              xmjd: 1,
-              type: 1,
-              create_time: '',
-              username: userName,
-              userid: userInfo?.id || ''
-            }
-            tempArr.value.push(str)
-          }
-          addForm.value.c_zgfw = addFormZgfw.value
-          addForm.value.c_ytfw = addFormYT.value
-          addForm.value.c_fwlx = addFormFwlx.value.join(',')
-          addForm.value.c_xmjd_json = JSON.stringify(tempArr.value)
-          // 续标/投标:选项目时已把父级或参考项目的项目标书写入 c_ckbs,提交时保留
-
-          dialogFormVisible.value = false
-          addProject()
+          doAdd()
         }
       })
+    } else {
+      doAdd()
     }
   })
 }
@@ -525,7 +540,7 @@ const handleCascaderChange = (val: any) => {
   addForm.value.c_fbz = tempObj.c_htqyjf
   addForm.value.c_jdbm = ''
   addForm.value.c_zgfw = ''
-  addForm.value.c_nh = tempObj.c_nhhtsr
+  addForm.value.c_nh = String(sourceContractAmountYuan(tempObj) || '')
   addForm.value.c_xmbtqc = tempObj.c_xmmc
   addForm.value.c_xmjd_json = ''
   addForm.value.c_ytfw = ''
@@ -533,8 +548,13 @@ const handleCascaderChange = (val: any) => {
   addForm.value.c_kssj = moment(tempObj.c_zgxmhtksrq).format('YYYY-MM-DD HH:mm')
   addForm.value.c_dqsj = moment(tempObj.c_zgxmhtdqrq as any).format('YYYY-MM-DD HH:mm').toString()
   addForm.value.c_xmbt = tempObj.c_xmmc
-  addForm.value.xmbh = tempObj.c_father_code ? tempObj.c_father_code : tempObj.c_guid
-  // 续标:父级项目标书 → c_ckbs;投标:参考项目标书 → c_ckbs(仅 .docx)
+  // 续标:原项目编码 → xmbh(父级关联);投标:类似项目只借标书,不写 xmbh
+  if (addForm.value.c_is_xb) {
+    addForm.value.xmbh = tempObj.c_father_code ? tempObj.c_father_code : tempObj.c_guid
+  } else {
+    addForm.value.xmbh = ''
+  }
+  // 续标/投标:所选在管「项目标书」→ 参考标书 c_ckbs(仅 .docx)
   addForm.value.c_ckbs = resolveDocxHistoryTender(tempObj.c_history_tender)
   addForm.value.rwlcqd = JSON.stringify(CONFIG.TASKLIST)
 }
@@ -578,6 +598,11 @@ const getDmsDataList = async (params: any) => {
 <style scoped>
 .renewal-page {
   width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  min-height: 0;
 }
 
 .pool-stats {
@@ -585,6 +610,7 @@ const getDmsDataList = async (params: any) => {
   grid-template-columns: repeat(3, 1fr);
   gap: 10px;
   margin-bottom: 16px;
+  flex-shrink: 0;
 }
 
 .pool-stat {
@@ -620,6 +646,11 @@ const getDmsDataList = async (params: any) => {
   background: #fff;
   border: 1px solid #e8ecef;
   border-radius: 10px;
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
 }
 
 .card-head {
@@ -627,12 +658,33 @@ const getDmsDataList = async (params: any) => {
   border-bottom: 1px solid #e8ecef;
   font-weight: 600;
   font-size: 14px;
+  flex-shrink: 0;
 }
 
 .card-body {
   padding: 16px;
 }
 
+.card-filters {
+  padding: 12px 16px 0;
+  flex-shrink: 0;
+}
+
+/* 仅列表+分页区域可滚;筛选固定在卡片上方 */
+.card-list-area {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.card-list-area > :deep(.renewal-list),
+.card-list-area > .empty-state {
+  flex: 1;
+  min-height: 0;
+}
+
 @media (max-width: 1100px) {
   .pool-stats {
     grid-template-columns: repeat(2, 1fr);

+ 26 - 2
src/components/renewal/expire/RenewalHeaderBar.vue

@@ -1,13 +1,17 @@
 <template>
-  <div class="header-bar">
+  <div class="header-bar" :class="{ terminated: !!terminated }">
     <div class="header-info">
-      <div class="project-title">{{ projects.title }}</div>
+      <div class="project-title">
+        {{ projects.title }}
+        <span v-if="terminated" class="term-badge">流程已终止</span>
+      </div>
       <div class="project-meta">
         {{ projects.c_fbz || projects.zbmsxx?.zb_source_name || '' }} ·
         {{ projects.zbmsxx?.zb_project_name || '' }} · 金额{{
           UTIL.formatCurrency(projects.c_nh / 10000 || '0')
         }}万元
       </div>
+      <div v-if="terminated" class="term-tip">该投续标流程已终止,仅可查看,不可继续操作</div>
     </div>
     <div class="countdown-badge">
       <span class="countdown-number">{{ projects.days }}</span>
@@ -21,6 +25,8 @@ import UTIL from '@/utils/util'
 
 defineProps<{
   projects: any
+  /** 投续标 c_terminated:中标登记终止后的只读态 */
+  terminated?: boolean
 }>()
 </script>
 
@@ -34,6 +40,9 @@ defineProps<{
   color: #fff;
   border-radius: 8px;
 }
+.header-bar.terminated {
+  background: linear-gradient(90deg, #6b7280 0%, #4b5563 100%);
+}
 
 .header-info {
   display: flex;
@@ -49,6 +58,21 @@ defineProps<{
   font-weight: 600;
 }
 
+.term-badge {
+  font-size: 12px;
+  font-weight: 600;
+  padding: 2px 8px;
+  border-radius: 4px;
+  background: rgba(255, 255, 255, 0.2);
+  border: 1px solid rgba(255, 255, 255, 0.35);
+}
+
+.term-tip {
+  font-size: 12px;
+  opacity: 0.9;
+  margin-top: 2px;
+}
+
 .project-meta {
   font-size: 12px;
   opacity: 0.8;

+ 3 - 1
src/components/renewal/expire/TaskChecklistPanel.vue

@@ -15,7 +15,7 @@
             @click="$emit('open-view', '1')"
             >查看招标公告</strong
           >
-          <div class="checklist-actions" v-if="task.seq <= stage">
+          <div class="checklist-actions" v-if="task.seq <= stage && !terminated">
             <!-- 任务3 关注招标公告 -->
             <template v-if="task.id == 3 || task.id == '3'">
               <el-button v-if="task.done" type="primary" size="small" disabled plain
@@ -123,6 +123,8 @@ defineProps<{
   zbDetailUrl?: string
   isTaskDone: (id: number | string) => boolean
   getTaskDisplayText: (task: any) => string
+  /** 流程已终止时隐藏操作按钮 */
+  terminated?: boolean
 }>()
 
 defineEmits<{

+ 63 - 47
src/components/renewal/list/BidRenewalCardList.vue

@@ -1,55 +1,48 @@
-<!-- 投续标列表:卡片行 + 分页;「进入」向上抛出 -->
+<!-- 投续标列表:卡片行 + 分页;「进入」向上抛出;仅行列表区域内部滚动 -->
 <template>
-  <div id="renewal-list" style="padding: 0">
-    <div v-for="item in renewalList" :key="item.title" class="renewal-row">
-      <div>
-        <div class="renewal-row-title">
-          {{ item[columnId + '_title'] }}
-          <span :class="['days-badge', item[columnId + '_c_is_xb'] === true ? 'warn' : 'ok']">
-            {{ item[columnId + '_c_is_xb'] === true ? '续标' : '投标' }}
-          </span>
-        </div>
-        <div class="renewal-row-meta">
-          <div>
-            <span>{{ item[columnId + '_c_xmbtqc'] }} </span>
-            <span>
-              · 合同时间 {{ moment(item[columnId + '_c_kssj']).format('YYYY-MM-DD') }} 至
-              {{ moment(item[columnId + '_c_dqsj']).format('YYYY-MM-DD') }}
+  <div class="renewal-list">
+    <div class="renewal-list-scroll">
+      <div v-for="item in renewalList" :key="item.title" class="renewal-row">
+        <div>
+          <div class="renewal-row-title">
+            {{ item[columnId + '_title'] }}
+            <span :class="['days-badge', item[columnId + '_c_is_xb'] === true ? 'warn' : 'ok']">
+              {{ item[columnId + '_c_is_xb'] === true ? '续标' : '投标' }}
             </span>
-            <span> · 年化 {{ UTIL.formatCurrency(item[columnId + '_c_nh'] / 10000) }} 万元</span>
           </div>
-          <div v-if="item.zb_source_name">
-            <span>{{ item.zb_source_name }} · {{ item.zb_project_name }}</span>
-            <span>
-              · 招标截止时间 {{ moment(item[columnId + '_zbbjzsj']).format('YYYY-MM-DD') }}
-            </span>
-            <span> · 招标金额 {{ UTIL.formatCurrency(item.zb_budget_amount / 10000) }}万元</span>
+          <div class="renewal-row-meta">
+            <div>
+              <span>{{ item[columnId + '_c_xmbtqc'] }} </span>
+              <span>
+                · 合同时间 {{ moment(item[columnId + '_c_kssj']).format('YYYY-MM-DD') }} 至
+                {{ moment(item[columnId + '_c_dqsj']).format('YYYY-MM-DD') }}
+              </span>
+              <span> · 年化 {{ UTIL.formatCurrency(item[columnId + '_c_nh'] / 10000) }} 万元</span>
+            </div>
+            <div v-if="item.zb_source_name">
+              <span>{{ item.zb_source_name }} · {{ item.zb_project_name }}</span>
+              <span>
+                · 招标截止时间 {{ moment(item[columnId + '_zbbjzsj']).format('YYYY-MM-DD') }}
+              </span>
+              <span> · 招标金额 {{ UTIL.formatCurrency(item.zb_budget_amount / 10000) }}万元</span>
+            </div>
           </div>
         </div>
-      </div>
-      <div class="renewal-row-actions">
-        <span
-          v-if="!['已完成', '已中标'].includes(item.type)"
-          :class="['badge', item.daysClass]"
-          >{{ item.days }}</span
-        >
-        <span :class="['badge', item.jdClass]">{{ item.type }}</span>
-        <el-button
-          type="primary"
-          size="small"
-          class="btn enter-btn"
-          v-if="item.jdClass === 'ok'"
-          @click="emit('enter', item)"
-          >进入</el-button
-        >
-        <el-button
-          type="primary"
-          size="small"
-          class="btn enter-btn"
-          v-else
-          @click="emit('enter', item)"
-          >进入</el-button
-        >
+        <div class="renewal-row-actions">
+          <span
+            v-if="!['已完成', '已中标'].includes(item.type)"
+            :class="['badge', item.daysClass]"
+            >{{ item.days }}</span
+          >
+          <span :class="['badge', item.jdClass]">{{ item.type }}</span>
+          <el-button
+            type="primary"
+            size="small"
+            class="btn enter-btn"
+            @click="emit('enter', item)"
+            >进入</el-button
+          >
+        </div>
       </div>
     </div>
 
@@ -85,6 +78,29 @@ const emit = defineEmits<{
 </script>
 
 <style scoped>
+.renewal-list {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.renewal-list-scroll {
+  flex: 1;
+  min-height: 0;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.pagination-section {
+  flex-shrink: 0;
+  border-top: 1px solid #e8ecef;
+  margin: 0 !important;
+  padding: 8px 16px;
+  background: #fff;
+}
+
 .renewal-row {
   display: grid;
   grid-template-columns: 1fr auto;

+ 138 - 59
src/components/tenderpool/Intention.vue

@@ -2,70 +2,107 @@
   <div class="intention-page">
     <div class="card">
       <div class="card-head">招标意向列表</div>
-      <div class="card-body">
+      <div class="card-body card-filters">
         <div class="notice-toolbar">
-            <el-input 
-                v-model="searchText" 
-                placeholder="搜索项目名称" 
-                clearable
-                @keyup.enter="queryProjects"
-                class="search-input"
-            />
-            <el-select v-model="selectedSource" style="width: 180px" placeholder="全部" clearable @change="queryProjects">
-                <el-option label="全部来源" value="" />
-                <el-option label="上海政府采购网" value="上海政府采购网" />
-                <el-option label="中国政府采购网" value="中国政府采购网" />
-            </el-select>
-            
-            <el-button type="primary" class="btn enter-btn" @click="queryProjects">查询</el-button>
+          <el-input
+            v-model="searchText"
+            placeholder="搜索项目名称"
+            clearable
+            @keyup.enter="queryProjects"
+            class="search-input"
+          />
+          <el-select
+            v-model="selectedSource"
+            style="width: 180px"
+            placeholder="全部"
+            clearable
+            @change="queryProjects"
+          >
+            <el-option label="全部来源" value="" />
+            <el-option label="上海政府采购网" value="上海政府采购网" />
+            <el-option label="中国政府采购网" value="中国政府采购网" />
+          </el-select>
+          <el-button type="primary" class="btn enter-btn" @click="queryProjects">查询</el-button>
         </div>
+      </div>
+
+      <div class="card-list-area">
         <div class="empty-state" v-if="total === 0">
           <span class="empty-icon">🔍</span>
           <p>暂无数据</p>
         </div>
-        <div v-else>
-            <div id="notice-list" style="padding: 0">
-                <div 
-                v-for="item in dataList" 
-                :key="item.id"
-                class="notice-row"
-                >
-                <div>
-                    <div class="notice-row-title">
-                      <span @click="handleViewClick(item)" style="cursor: pointer;">{{ item.title }}</span>
-                      <span style="margin-left: 5px;"  :class="['badge', Number(item.days) < 0 ? 'grey‌' : 'danger']" v-if="Number(item.days) < 0">
-                          {{ Number(item.days) > 0 ? 'new' : '已过期' }}
-                      </span> 
-                      <span style="margin-left: 5px;" :class="['badge', 'ok']" v-if="props.historyProjectData && props.historyProjectData.filter(item2 => item2 == item.c_author)[0]">
-                        {{props.historyProjectData && props.historyProjectData.filter(item2 => item2 == item.c_author)[0]? '历史客户' : '新客户'}}
-                      </span>
-                    </div>
-                    <div class="notice-row-meta">
-                    <span>{{ item.c_source_name }}</span>
-                    <span>{{item.keyword}}</span>
-                    <span>发布 {{ moment(item.c_publish_time).format('YYYY-MM-DD HH:mm') }}</span>
-                    <span>预算 {{item.contentDocument.sections[0].blocks[0].rows[0]['预算金额(元)']}}</span>
-                    <span>预计采购时间 {{item.contentDocument.sections[0].blocks[0].rows[0]['预计采购时间(填写到月)']}}</span>
-                    <span>发布机构 {{ item.c_author }}</span>
-                    </div>
+        <div v-else class="intention-list">
+          <div class="intention-list-scroll">
+            <div v-for="item in dataList" :key="item.id" class="notice-row">
+              <div>
+                <div class="notice-row-title">
+                  <span @click="handleViewClick(item)" style="cursor: pointer">{{ item.title }}</span>
+                  <span
+                    style="margin-left: 5px"
+                    :class="['badge', Number(item.days) < 0 ? 'grey‌' : 'danger']"
+                    v-if="Number(item.days) < 0"
+                  >
+                    {{ Number(item.days) > 0 ? 'new' : '已过期' }}
+                  </span>
+                  <span
+                    style="margin-left: 5px"
+                    :class="['badge', 'ok']"
+                    v-if="
+                      props.historyProjectData &&
+                      props.historyProjectData.filter((item2) => item2 == item.c_author)[0]
+                    "
+                  >
+                    {{
+                      props.historyProjectData &&
+                      props.historyProjectData.filter((item2) => item2 == item.c_author)[0]
+                        ? '历史客户'
+                        : '新客户'
+                    }}
+                  </span>
                 </div>
-                <div class="notice-row-actions">
-                    <el-button type="primary" size="small" :class="[item.c_isfollow == '是' ? 'btn enter-btn' : 'btn']"  style="margin-left: 6px !important;"  @click="handleGZViewClick(item)">{{item.c_isfollow == '是' ? '已关注' : '关注'}}</el-button>
+                <div class="notice-row-meta">
+                  <span>{{ item.c_source_name }}</span>
+                  <span>{{ item.keyword }}</span>
+                  <span>发布 {{ moment(item.c_publish_time).format('YYYY-MM-DD HH:mm') }}</span>
+                  <span
+                    >预算
+                    {{
+                      item.contentDocument.sections[0].blocks[0].rows[0]['预算金额(元)']
+                    }}</span
+                  >
+                  <span
+                    >预计采购时间
+                    {{
+                      item.contentDocument.sections[0].blocks[0].rows[0][
+                        '预计采购时间(填写到月)'
+                      ]
+                    }}</span
+                  >
+                  <span>发布机构 {{ item.c_author }}</span>
                 </div>
-                </div>
-            </div>
-            <!-- 分页控件 -->
-            <div class="pagination-section">
-                <span class="total-text">共{{ total }}个意向</span>
-                <el-pagination
-                layout="prev, pager, next"
-                :total="total"
-                :current-page="page"
-                :page-size="pageSize"
-                @current-change="handleCurrentChange"
+              </div>
+              <div class="notice-row-actions">
+                <el-button
+                  type="primary"
+                  size="small"
+                  :class="[item.c_isfollow == '是' ? 'btn enter-btn' : 'btn']"
+                  style="margin-left: 6px !important"
+                  @click="handleGZViewClick(item)"
+                  >{{ item.c_isfollow == '是' ? '已关注' : '关注' }}</el-button
                 >
-                </el-pagination>
+              </div>
             </div>
+          </div>
+          <div class="pagination-section">
+            <span class="total-text">共{{ total }}个意向</span>
+            <el-pagination
+              layout="prev, pager, next"
+              :total="total"
+              :current-page="page"
+              :page-size="pageSize"
+              @current-change="handleCurrentChange"
+            />
+          </div>
         </div>
       </div>
     </div>
@@ -221,12 +258,22 @@ const calculateRemaining = (et: string): { days: string; daysClass: string; } =>
 <style scoped>
 .intention-page {
   width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  min-height: 0;
 }
 
 .card {
   background: #fff;
   border: 1px solid #e8ecef;
   border-radius: 10px;
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
 }
 
 .card-head {
@@ -234,12 +281,48 @@ const calculateRemaining = (et: string): { days: string; daysClass: string; } =>
   border-bottom: 1px solid #e8ecef;
   font-weight: 600;
   font-size: 14px;
+  flex-shrink: 0;
 }
 
 .card-body {
   padding: 16px;
 }
 
+.card-filters {
+  padding: 12px 16px 0;
+  flex-shrink: 0;
+}
+
+.card-list-area {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.intention-list {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.intention-list-scroll {
+  flex: 1;
+  min-height: 0;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.pagination-section {
+  flex-shrink: 0;
+  border-top: 1px solid #e8ecef;
+  margin: 0 !important;
+  padding: 8px 16px;
+  background: #fff;
+}
 
 .notice-toolbar {
   display: flex;
@@ -258,7 +341,6 @@ const calculateRemaining = (et: string): { days: string; daysClass: string; } =>
   gap: 12px;
   padding: 14px 16px;
   border-bottom: 1px solid #e8ecef;
-  /* cursor: pointer; */
   transition: background 0.12s;
 }
 
@@ -286,11 +368,8 @@ const calculateRemaining = (et: string): { days: string; daysClass: string; } =>
 
 .notice-row-actions {
   display: flex;
-  /* gap: 6px; */
   align-items: center;
   flex-wrap: wrap;
   justify-content: flex-end;
 }
-
-
-</style>
+</style>

+ 38 - 5
src/components/tenderpool/Monitor.vue

@@ -15,7 +15,7 @@
 
     <div class="card">
       <div class="card-head">招标公告列表</div>
-      <div class="card-body" style="padding: 12px 16px 0">
+      <div class="card-body card-filters">
         <MonitorFilters
           v-model:search-text="searchText"
           v-model:selected-source="selectedSource"
@@ -23,7 +23,9 @@
           :area-list="areaList"
           @query="queryProjects"
         />
+      </div>
 
+      <div class="card-list-area">
         <MonitorCardList
           :pool-list="poolList"
           :total="total"
@@ -361,8 +363,8 @@ const handleConfirmClick = (formEl: FormInstance | undefined) => {
   formEl.validate((valid: boolean) => {
     if (valid) {
       if (dialogName.value == '投标') {
-        getXMProjects(finishObject.value.id, 'zbxmbh').then((res) => {
-          if (res > 0) {
+        getXMProjects(project.value.id, 'zbxmbh').then((res) => {
+          if (res?.length > 0) {
             ElMessage.error('此标已投标,不能重复投标')
           } else {
             addProject(false)
@@ -373,8 +375,9 @@ const handleConfirmClick = (formEl: FormInstance | undefined) => {
           ElMessage.error('请选择项目名称')
           return
         }
-        getXMProjects(finishObject.value.id, 'xmbh').then((res) => {
-          if (res > 0) {
+        const xmbh = finishObject.value.c_father_code || finishObject.value.c_guid
+        getXMProjects(xmbh, 'xmbh').then((res) => {
+          if (res?.length > 0) {
             ElMessage.error('此项目已续标,不能重复续标')
           } else {
             addProject(true)
@@ -581,6 +584,11 @@ const getDmsDataList = async (params: any) => {
 <style scoped>
 .monitorStatspage {
   width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  min-height: 0;
 }
 
 .el-drawer__header {
@@ -593,6 +601,7 @@ const getDmsDataList = async (params: any) => {
   grid-template-columns: repeat(4, 1fr);
   gap: 10px;
   margin-bottom: 16px;
+  flex-shrink: 0;
 }
 
 .monitorStatsstat {
@@ -628,6 +637,11 @@ const getDmsDataList = async (params: any) => {
   background: #fff;
   border: 1px solid #e8ecef;
   border-radius: 10px;
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
 }
 
 .card-head {
@@ -635,12 +649,31 @@ const getDmsDataList = async (params: any) => {
   border-bottom: 1px solid #e8ecef;
   font-weight: 600;
   font-size: 14px;
+  flex-shrink: 0;
 }
 
 .card-body {
   padding: 16px;
 }
 
+.card-filters {
+  padding: 12px 16px 0;
+  flex-shrink: 0;
+}
+
+.card-list-area {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.card-list-area > :deep(.monitor-list) {
+  flex: 1;
+  min-height: 0;
+}
+
 @media (max-width: 1100px) {
   .monitorStatsstats {
     grid-template-columns: repeat(2, 1fr);

+ 80 - 55
src/components/tenderpool/monitor/MonitorCardList.vue

@@ -1,63 +1,65 @@
 <template>
-  <div class="empty-state" v-if="total === 0">
-    <span class="empty-icon">🔍</span>
-    <p>暂无数据</p>
-  </div>
-  <div v-else>
-    <div id="monitorStatslist" style="padding: 0">
-      <div
-        v-for="item in poolList"
-        :key="item.id"
-        class="monitorStatsrow"
-      >
-        <div>
-          <div class="monitorStatsrow-title">
-            <span @click="emit('action', item, '详情')" style="cursor: pointer;">{{ item.title }}</span>
-            <span
-              style="margin-left: 5px;"
-              :class="['badge', item.c_bid_type == '1' ? 'ok' : item.c_bid_type == '2' ? 'warn' : (item.c_bid_type == '3' || Number(item.days) <= 0 ) ? 'grey‌' : 'danger']"
-            >
-              {{ item.c_bid_type == '1' ? '投标' : item.c_bid_type == '2' ? '续标' : item.c_bid_type == '3' ? '储备' : Number(item.days) > 0 ? 'new' : '已过期' }}
-            </span>
-            <span
-              style="margin-left: 5px;"
-              :class="['badge', 'ok']"
-              v-if="historyProjectData && historyProjectData.filter((item2: any) => item2 == item.c_author)[0]"
-            >
-              {{ historyProjectData && historyProjectData.filter((item2: any) => item2 == item.c_author)[0] ? '历史客户' : '新客户' }}
-            </span>
+  <div class="monitor-list">
+    <div class="empty-state" v-if="total === 0">
+      <span class="empty-icon">🔍</span>
+      <p>暂无数据</p>
+    </div>
+    <template v-else>
+      <div class="monitor-list-scroll">
+        <div
+          v-for="item in poolList"
+          :key="item.id"
+          class="monitorStatsrow"
+        >
+          <div>
+            <div class="monitorStatsrow-title">
+              <span @click="emit('action', item, '详情')" style="cursor: pointer;">{{ item.title }}</span>
+              <span
+                style="margin-left: 5px;"
+                :class="['badge', item.c_bid_type == '1' ? 'ok' : item.c_bid_type == '2' ? 'warn' : (item.c_bid_type == '3' || Number(item.days) <= 0 ) ? 'grey‌' : 'danger']"
+              >
+                {{ item.c_bid_type == '1' ? '投标' : item.c_bid_type == '2' ? '续标' : item.c_bid_type == '3' ? '储备' : Number(item.days) > 0 ? 'new' : '已过期' }}
+              </span>
+              <span
+                style="margin-left: 5px;"
+                :class="['badge', 'ok']"
+                v-if="historyProjectData && historyProjectData.filter((item2: any) => item2 == item.c_author)[0]"
+              >
+                {{ historyProjectData && historyProjectData.filter((item2: any) => item2 == item.c_author)[0] ? '历史客户' : '新客户' }}
+              </span>
+            </div>
+            <div class="monitorStatsrow-meta">
+              <span>{{ item.c_source_name }}</span>
+              <span>{{ item.c_district }}</span>
+              <span>发布 {{ moment(item.c_publish_time).format('YYYY-MM-DD HH:mm') }}</span>
+              <span>截止 {{ item.c_bid_deadline ? moment(item.c_bid_deadline.replace(/\s*\(.*\)$/, '') , 'YYYY年MM月DD日 HH:mm').format('YYYY-MM-DD HH:mm') : '-'  }}</span>
+              <span>{{ Number(item.days) > 0 ? '剩余' : '已过期' }} {{ Math.abs(Number(item.days)) }} 天</span>
+              <span>预算 {{ item.c_budget_amount }}</span>
+            </div>
           </div>
-          <div class="monitorStatsrow-meta">
-            <span>{{ item.c_source_name }}</span>
-            <span>{{ item.c_district }}</span>
-            <span>发布 {{ moment(item.c_publish_time).format('YYYY-MM-DD HH:mm') }}</span>
-            <span>截止 {{ item.c_bid_deadline ? moment(item.c_bid_deadline.replace(/\s*\(.*\)$/, '') , 'YYYY年MM月DD日 HH:mm').format('YYYY-MM-DD HH:mm') : '-'  }}</span>
-            <span>{{ Number(item.days) > 0 ? '剩余' : '已过期' }} {{ Math.abs(Number(item.days)) }} 天</span>
-            <span>预算 {{ item.c_budget_amount }}</span>
+          <div class="monitorStatsrow-actions">
+            <el-button type="primary" size="small" class="btn" @click="emit('action', item, '详情')">详情</el-button>
+            <el-button type="primary" size="small" class="btn enter-btn" style="margin-left: 6px !important;" v-if="item.c_bid_type == '1' || item.c_bid_type == '2'" @click="emit('action', item, '标书编制')">标书编制</el-button>
+            <el-button type="primary" size="small" class="btn enter-btn" style="margin-left: 6px !important;" v-if="item.c_bid_type != '1' && item.c_bid_type != '2' && Number(item.days) > 0" @click="emit('action', item, '投标')">投标</el-button>
+            <el-button type="primary" size="small" class="btn enter-btn" style="margin-left: 6px !important;" v-if="item.c_bid_type != '1' && item.c_bid_type != '2' && Number(item.days) > 0" @click="emit('action', item, '续标')">续标</el-button>
+            <el-button type="primary" size="small" class="btn enter-btn" style="margin-left: 6px !important;" v-if="item.c_bid_type != '1' && item.c_bid_type != '2' && item.c_bid_type != '3'" @click="emit('action', item, '储备')">储备</el-button>
+            <el-button type="primary" size="small" class="btn" style="margin-left: 6px !important;" v-if="item.c_bid_type == '0' || item.c_bid_type == null" @click="emit('action', item, '忽略')">忽略</el-button>
+            <el-button type="primary" size="small" class="btn" style="margin-left: 6px !important;" v-if="item.c_bid_type == '1' || item.c_bid_type == '2' || item.c_bid_type == '3'" @click="emit('action', item, '原公告链接')">原公告</el-button>
+            <el-button type="primary" size="small" class="btn" style="margin-left: 6px !important;" v-if="item.c_bid_type == '3'" @click="emit('action', item, '移出')">移出</el-button>
           </div>
         </div>
-        <div class="monitorStatsrow-actions">
-          <el-button type="primary" size="small" class="btn" @click="emit('action', item, '详情')">详情</el-button>
-          <el-button type="primary" size="small" class="btn enter-btn" style="margin-left: 6px !important;" v-if="item.c_bid_type == '1' || item.c_bid_type == '2'" @click="emit('action', item, '标书编制')">标书编制</el-button>
-          <el-button type="primary" size="small" class="btn enter-btn" style="margin-left: 6px !important;" v-if="item.c_bid_type != '1' && item.c_bid_type != '2' && Number(item.days) > 0" @click="emit('action', item, '投标')">投标</el-button>
-          <el-button type="primary" size="small" class="btn enter-btn" style="margin-left: 6px !important;" v-if="item.c_bid_type != '1' && item.c_bid_type != '2' && Number(item.days) > 0" @click="emit('action', item, '续标')">续标</el-button>
-          <el-button type="primary" size="small" class="btn enter-btn" style="margin-left: 6px !important;" v-if="item.c_bid_type != '1' && item.c_bid_type != '2' && item.c_bid_type != '3'" @click="emit('action', item, '储备')">储备</el-button>
-          <el-button type="primary" size="small" class="btn" style="margin-left: 6px !important;" v-if="item.c_bid_type == '0' || item.c_bid_type == null" @click="emit('action', item, '忽略')">忽略</el-button>
-          <el-button type="primary" size="small" class="btn" style="margin-left: 6px !important;" v-if="item.c_bid_type == '1' || item.c_bid_type == '2' || item.c_bid_type == '3'" @click="emit('action', item, '原公告链接')">原公告</el-button>
-          <el-button type="primary" size="small" class="btn" style="margin-left: 6px !important;" v-if="item.c_bid_type == '3'" @click="emit('action', item, '移出')">移出</el-button>
-        </div>
       </div>
-    </div>
-    <div class="pagination-section">
-      <span class="total-text">共{{ total }}个公告</span>
-      <el-pagination
-        layout="prev, pager, next"
-        :total="total"
-        :current-page="page"
-        :page-size="pageSize"
-        @current-change="(p: number) => emit('page-change', p)"
-      />
-    </div>
+      <div class="pagination-section">
+        <span class="total-text">共{{ total }}个公告</span>
+        <el-pagination
+          layout="prev, pager, next"
+          :total="total"
+          :current-page="page"
+          :page-size="pageSize"
+          @current-change="(p: number) => emit('page-change', p)"
+        />
+      </div>
+    </template>
   </div>
 </template>
 
@@ -79,6 +81,29 @@ const emit = defineEmits<{
 </script>
 
 <style scoped>
+.monitor-list {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+}
+
+.monitor-list-scroll {
+  flex: 1;
+  min-height: 0;
+  overflow-x: hidden;
+  overflow-y: auto;
+}
+
+.pagination-section {
+  flex-shrink: 0;
+  border-top: 1px solid #e8ecef;
+  margin: 0 !important;
+  padding: 8px 16px;
+  background: #fff;
+}
+
 .monitorStatsrow {
   display: grid;
   grid-template-columns: 1fr auto;

+ 11 - 11
src/composables/tenderpool/useMonitorAddProject.ts

@@ -8,7 +8,7 @@ import { ElMessage } from 'element-plus'
 import api from '@/api/common'
 import CONFIG from '@/utils/config'
 import { COLUMN, MODEL } from '@/constants/dmsColumns'
-import { resolveDocxHistoryTender } from '@/utils/util'
+import { resolveDocxHistoryTender, sourceContractAmountYuan } from '@/utils/util'
 
 export type UseMonitorAddProjectOptions = {
   /** 当前操作的招标公告 */
@@ -103,13 +103,15 @@ export function useMonitorAddProject(opts: UseMonitorAddProjectOptions) {
     addForm.value.c_fbz = tempObj.c_htqyjf
     addForm.value.c_jdbm = ''
     addForm.value.c_zgfw = addFormZgfw.value
-    addForm.value.c_nh = tempObj.c_nhhtsr
+    addForm.value.c_nh = String(sourceContractAmountYuan(tempObj) || '')
     addForm.value.c_xmbtqc = tempObj.c_xmmc
     addForm.value.c_xmjd_json = JSON.stringify(tempArr)
-    addForm.value.c_kssj = moment(tempObj.c_kssj).format('YYYY-MM-DD HH:mm')
-    addForm.value.c_dqsj = moment(tempObj.c_dqsj).format('YYYY-MM-DD HH:mm')
+    addForm.value.c_kssj = moment(tempObj.c_zgxmhtksrq).format('YYYY-MM-DD HH:mm')
+    addForm.value.c_dqsj = moment(tempObj.c_zgxmhtdqrq).format('YYYY-MM-DD HH:mm')
     addForm.value.c_xmbt = tempObj.c_xmmc
-    addForm.value.xmbh = tempObj.c_guid
+    // 类似项目/原项目编码不在此处写入 xmbh:
+    // 续标在 addProject(true) 写父级;投标绝不写 xmbh(只借标书到 c_ckbs)
+    addForm.value.xmbh = ''
     addForm.value.c_is_xb = false
     addForm.value.rwlcqd = JSON.stringify(CONFIG.TASKLIST)
   }
@@ -156,7 +158,7 @@ export function useMonitorAddProject(opts: UseMonitorAddProjectOptions) {
         content: finishObject.value.content,
         c_fbz: finishObject.value.c_htqyjf,
         c_zgfw: addFormZgfw.value,
-        c_nh: finishObject.value.c_nh || '0',
+        c_nh: sourceContractAmountYuan(finishObject.value),
         c_xmbtqc: finishObject.value.c_xmmc,
         c_qx: finishObject.value.c_sq,
         c_xmjd_json: JSON.stringify(tempArr),
@@ -194,22 +196,20 @@ export function useMonitorAddProject(opts: UseMonitorAddProjectOptions) {
       const ckbs = resolveDocxHistoryTender(finishObject.value.c_history_tender)
       if (ckbs) formData.c_ckbs = ckbs
     } else {
-      // 投标:参考项目「项目标书」→ 投续标「参考标书」c_ckbs(仅 .docx)
+      // 投标:新项目,无父级;类似项目仅用于参考标书 c_ckbs(仅 .docx),不写 xmbh
       const ckbs = resolveDocxHistoryTender(finishObject.value.c_history_tender)
       formData = {
         title: addForm.value.title,
         content: addForm.value.content,
         c_fbz: addForm.value.c_fbz,
         c_zgfw: addFormZgfw.value,
-        c_nh: addForm.value.c_nh || '0',
+        c_nh: sourceContractAmountYuan(finishObject.value),
         c_xmbtqc: addForm.value.c_xmbtqc,
         c_qx: addForm.value.c_qx,
         c_kssj: moment(finishObject.value.c_zgxmhtksrq).format('YYYY-MM-DD HH:mm'),
         c_dqsj: moment(finishObject.value.c_zgxmhtdqrq).format('YYYY-MM-DD HH:mm'),
         c_xmbt: addForm.value.c_xmbt,
-        xmbh: finishObject.value.c_father_code
-          ? finishObject.value.c_father_code
-          : finishObject.value.c_guid,
+        xmbh: '',
         c_is_xb: isType,
         c_sfwc: false,
         c_ytfw: addFormYT.value,

+ 4 - 2
src/env.d.ts

@@ -1,3 +1,5 @@
+declare module '*.css'
+
 declare module 'wellknown' {
-  export function parse(wkt: string): GeoJSON.Geometry;
-}
+  export function parse(wkt: string): GeoJSON.Geometry
+}

+ 36 - 36
src/layouts/MainLayout.vue

@@ -148,42 +148,42 @@ const navTree: NavNode[] = [
       // { name: '投标 / 续标跟进' },
     ],
   },
-  {
-    id: 'ops',
-    type: 'group',
-    name: '运营部',
-    children: [
-      { name: '部门工作台' },
-      { name: '交接与定编' },
-      { name: '履约与项目报告' },
-    ],
-  },
-  {
-    id: 'hr',
-    type: 'group',
-    name: '人事部',
-    children: [
-      { name: '部门工作台' },
-      { name: '人员清单' },
-      { name: '证书管理' },
-      { name: '差额调配' },
-    ],
-  },
-  {
-    id: 'purchase',
-    type: 'group',
-    name: '采购部',
-    children: [{ name: '采购与物资' }],
-  },
-  {
-    id: 'finance',
-    type: 'group',
-    name: '财务部',
-    children: [
-      { name: '财务工作台' },
-      { name: '合同与回款' },
-    ],
-  },
+  // {
+  //   id: 'ops',
+  //   type: 'group',
+  //   name: '运营部',
+  //   children: [
+  //     { name: '部门工作台' },
+  //     { name: '交接与定编' },
+  //     { name: '履约与项目报告' },
+  //   ],
+  // },
+  // {
+  //   id: 'hr',
+  //   type: 'group',
+  //   name: '人事部',
+  //   children: [
+  //     { name: '部门工作台' },
+  //     { name: '人员清单' },
+  //     { name: '证书管理' },
+  //     { name: '差额调配' },
+  //   ],
+  // },
+  // {
+  //   id: 'purchase',
+  //   type: 'group',
+  //   name: '采购部',
+  //   children: [{ name: '采购与物资' }],
+  // },
+  // {
+  //   id: 'finance',
+  //   type: 'group',
+  //   name: '财务部',
+  //   children: [
+  //     { name: '财务工作台' },
+  //     { name: '合同与回款' },
+  //   ],
+  // },
   // ,
   // {
   //   id: 'field',

+ 4 - 3
src/styles/element-theme-green.css

@@ -137,9 +137,10 @@
   background: var(--el-color-primary);
 }
 
-/* Switch 开关样式覆盖 */
-.el-switch.is-checked {
-  background-color: var(--el-color-primary);
+/* Switch:勿给根节点设背景(会多出一层矩形底,盖住 inline-prompt)
+   主题色走 EP 变量,作用在 .el-switch__core */
+.el-switch {
+  --el-switch-on-color: var(--el-color-primary);
 }
 
 /* Slider 滑块样式覆盖 */

+ 40 - 0
src/utils/projectGuid.ts

@@ -0,0 +1,40 @@
+/**
+ * 在管项目 c_guid 生成(与中标建项 addProject 规则一致)
+ * 结构:区码 + 片区码 + YYYYMMDD + 业态码 + 两位序号 + 父子标记(01/00)
+ */
+import moment from 'moment'
+
+export function parseGuidSeq(guid: string): number {
+  if (!guid || guid.length < 4) return 0
+  return Number(guid.substring(guid.length - 4, guid.length - 2)) || 0
+}
+
+export function buildSourceGuid(parts: {
+  districtCode: string
+  partCode: string
+  dateYmd?: string
+  ytCode: string
+  seq: number
+  hasFather: boolean
+}): string {
+  const date = parts.dateYmd ?? moment().format('YYYYMMDD')
+  const seqStr = parts.seq.toString().padStart(2, '0')
+  return `${parts.districtCode}${parts.partCode}${date}${parts.ytCode}${seqStr}${
+    parts.hasFather ? '01' : '00'
+  }`
+}
+
+/** 按前缀查到已有 guid 后,计算下一个完整 c_guid */
+export function nextSourceGuidFromLatest(
+  latestGuid: string | undefined | null,
+  parts: {
+    districtCode: string
+    partCode: string
+    dateYmd?: string
+    ytCode: string
+    hasFather: boolean
+  }
+): string {
+  const seq = latestGuid ? parseGuidSeq(latestGuid) + 1 : 0
+  return buildSourceGuid({ ...parts, seq })
+}

+ 53 - 0
src/utils/tree.ts

@@ -24,3 +24,56 @@ export function buildProjectTree(data: any[]): any[] {
   })
   return tree
 }
+
+export type AreaTreeItem = {
+  id: string
+  label: string
+  value: string
+  children: AreaTreeItem[]
+  /** 行政编码及片区:省份名称,选区时写入 SOURCE.c_sfzxs */
+  provinceName?: string
+  /** 行政编码及片区:市级名称,选区时写入 SOURCE.c_sq */
+  cityName?: string
+}
+
+/** 管理区域:区县 → 片区 两级树(value 为区码/片区码,供 c_guid 生成) */
+export function buildAreaTree(list: any[], districtPrefix = '3101'): AreaTreeItem[] {
+  const districtMap = new Map<string, AreaTreeItem>()
+  const filteredList = list.filter((item) => item?.c_district_code?.startsWith(districtPrefix))
+  for (const item of filteredList) {
+    const districtCode = item.c_district_code
+    const districtName = item.c_district_name
+    if (!districtCode || !districtName) continue
+    let districtNode = districtMap.get(districtCode)
+    if (!districtNode) {
+      districtNode = {
+        id: districtCode,
+        label: districtName,
+        value: districtCode,
+        provinceName: item.c_province_name || '',
+        cityName: item.c_city_name || '',
+        children: [],
+      }
+      districtMap.set(districtCode, districtNode)
+    } else {
+      if (!districtNode.provinceName && item.c_province_name) {
+        districtNode.provinceName = item.c_province_name
+      }
+      if (!districtNode.cityName && item.c_city_name) {
+        districtNode.cityName = item.c_city_name
+      }
+    }
+    const partCode = item.c_part_code
+    const partName = item.c_part_name
+    if (!partCode || !partName) continue
+    if (!districtNode.children.some((c) => c.value === partCode)) {
+      districtNode.children.push({
+        id: partCode,
+        label: partName,
+        value: partCode,
+        children: [],
+      })
+    }
+  }
+  return Array.from(districtMap.values())
+}

+ 63 - 0
src/utils/uploadAndVerify.ts

@@ -0,0 +1,63 @@
+/**
+ * SQ /uploadAndVerify 结果解析(中标登记、确认中标建项共用)
+ * 契约见 word/上传并校验文件 接口手册.md
+ */
+import api from '@/api/common'
+
+export type UploadAndVerifyOk = { ok: true; filePath: string; message: string }
+export type UploadAndVerifyFail = { ok: false; message: string }
+export type UploadAndVerifyResult = UploadAndVerifyOk | UploadAndVerifyFail
+
+const CODE_MSG: Record<number, string> = {
+  400: '参数异常',
+  401: '文件不可用,请检查后重试',
+  402: '文件和实际类型不符合,请检查是否为 .docx / .pdf',
+  403: '文件不是含有文字的 PDF',
+}
+
+export function detectUploadFileType(fileName: string): 'docx' | 'pdf' | null {
+  const name = String(fileName || '').toLowerCase()
+  if (name.endsWith('.docx')) return 'docx'
+  if (name.endsWith('.pdf')) return 'pdf'
+  return null
+}
+
+/** 上传并校验;成功返回 content.filePath */
+export async function uploadAndVerifyFile(
+  file: File,
+  columnId: string | number
+): Promise<UploadAndVerifyResult> {
+  const fileType = detectUploadFileType(file.name)
+  if (!fileType) {
+    return { ok: false, message: '当前上传的标书格式无法读取,请检查是否为.docx或.pdf文件' }
+  }
+  try {
+    const res: any = await api.uploadAndVerify({
+      file,
+      columnId: String(columnId),
+      fileType,
+      timeout: 600000,
+    })
+    const code = Number(res?.code)
+    const content = res?.content
+    if (code === 200 && content?.valid && content?.filePath) {
+      return {
+        ok: true,
+        filePath: String(content.filePath),
+        message: String(content.message || '文件检测通过'),
+      }
+    }
+    const tip =
+      (content && content.message) ||
+      CODE_MSG[code] ||
+      res?.message ||
+      '当前上传的标书格式无法读取,请检查是否为.docx或.pdf文件'
+    return { ok: false, message: String(tip) }
+  } catch (e: any) {
+    const timedOut = e?.code === 'ECONNABORTED' || /timeout/i.test(String(e?.message || ''))
+    return {
+      ok: false,
+      message: timedOut ? '标书上传超时,请检查网络后重试' : e?.message || '标书上传失败',
+    }
+  }
+}

+ 96 - 0
src/utils/util.ts

@@ -102,6 +102,100 @@ function geojsonToWkt(geojson: any) {
   throw new Error('不支持的几何类型: ' + geom.type);
 }
 
+/**
+ * 投续标金额,单位:元。
+ * 优先 `c_hte`(合同额),其次 `c_htzj`(总合同额);线上均存**元**。
+ * 列表和指挥页展示时再除以 10000 显示为万元。
+ */
+export function sourceContractAmountYuan(project: any): number {
+  if (!project) return 0
+  const legacy = project.c_nhhtsr
+  if (legacy != null && legacy !== '' && !Number.isNaN(Number(legacy))) return Number(legacy)
+  for (const key of ['c_hte', 'c_htzj'] as const) {
+    const yuan = Number(project[key])
+    if (project[key] != null && project[key] !== '' && !Number.isNaN(yuan)) return yuan
+  }
+  return 0
+}
+
+/**
+ * 从结果公告结构化文档解析中标(成交)金额(元)。
+ * 表头行含「中标(成交金额)」,取下一行同列数值。
+ */
+export function extractWinningAmountYuan(resultNotice: any): number | null {
+  if (!resultNotice) return null
+  let doc: any = resultNotice.c_content_document
+  if (!doc) return null
+  if (typeof doc === 'string') {
+    try {
+      doc = JSON.parse(doc)
+    } catch {
+      return null
+    }
+  }
+  const sections = doc?.sections || []
+  for (const sec of sections) {
+    for (const block of sec?.blocks || []) {
+      const rows = block?.rows
+      if (!Array.isArray(rows) || rows.length < 2) continue
+      const header = rows[0] || {}
+      let amountCol = ''
+      for (const [k, v] of Object.entries(header)) {
+        if (typeof v === 'string' && v.includes('成交金额')) {
+          amountCol = k
+          break
+        }
+      }
+      if (!amountCol) continue
+      const raw = String((rows[1] || {})[amountCol] ?? '').replace(/[,,\s]/g, '')
+      const m = raw.match(/([\d.]+)/)
+      if (!m) continue
+      const n = Number(m[1])
+      if (!Number.isNaN(n) && n > 0) return n
+    }
+  }
+  return null
+}
+
+/** 从结果公告解析评审总得分(若有) */
+export function extractWinningScore(resultNotice: any): number | null {
+  if (!resultNotice) return null
+  let doc: any = resultNotice.c_content_document
+  if (!doc) return null
+  if (typeof doc === 'string') {
+    try {
+      doc = JSON.parse(doc)
+    } catch {
+      return null
+    }
+  }
+  const sections = doc?.sections || []
+  for (const sec of sections) {
+    for (const block of sec?.blocks || []) {
+      const rows = block?.rows
+      if (!Array.isArray(rows) || rows.length < 2) continue
+      const header = rows[0] || {}
+      let scoreCol = ''
+      for (const [k, v] of Object.entries(header)) {
+        if (typeof v === 'string' && v.includes('总得分')) {
+          scoreCol = k
+          break
+        }
+      }
+      if (!scoreCol) continue
+      const n = Number(String((rows[1] || {})[scoreCol] ?? '').replace(/[,,\s]/g, ''))
+      if (!Number.isNaN(n) && n > 0) return n
+    }
+  }
+  return null
+}
+
+/** 在管项目地址。c_query_address 已从模型移除,改用 c_xmdz。 */
+export function sourceProjectAddress(project: any): string {
+  if (!project) return ''
+  return project.c_xmdz || project.c_query_address || ''
+}
+
 /**
  * 在管「项目标书」c_history_tender → 投续标「参考标书」c_ckbs。
  * 仅当路径为 .docx(可带 query)时返回原串,否则返回空串。
@@ -119,4 +213,6 @@ export default {
   copyText,
   geojsonToWkt,
   resolveDocxHistoryTender,
+  sourceContractAmountYuan,
+  sourceProjectAddress,
 }

+ 8 - 2
src/views/bidMonitor/TenderPool.vue

@@ -57,11 +57,17 @@ const handleTabClick = (tab: string) => {
 
 .pool-page {
   width: 100%;
+  height: calc(100vh - 70px - 52px);
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
 }
 
 .pool-content {
   width: 100%;
-  height: 100%;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
 }
 
 .page-tabs {
@@ -69,8 +75,8 @@ const handleTabClick = (tab: string) => {
   gap: 0;
   margin-bottom: 16px;
   border-bottom: 1px solid #e8ecef;
-  /* justify-content: space-between; */
   align-items: center;
+  flex-shrink: 0;
 }
 
 .btn-right {

+ 19 - 12
src/views/bidPreparation/Renewal.vue

@@ -1,8 +1,9 @@
 <template>
   <div class="renewal-page">
-    <div class="page-tabs">
-      <span 
-        v-for="tab in tabs" 
+    <!-- 暂时隐藏顶栏 Tab(投续标 / 续签标);后续可能恢复,仅展示投续标内容 -->
+    <div v-if="false" class="page-tabs">
+      <span
+        v-for="tab in tabs"
         :key="tab.value"
         :class="['page-tab', { active: activeTab === tab.value }]"
         @click="handleTabClick(tab.value)"
@@ -11,20 +12,21 @@
       </span>
     </div>
     <div class="renewal-content">
-      <BidRenewal v-if="activeTab === 'bidrenewal'"></BidRenewal>
-      <Renew v-if="activeTab === 'renew'" ></Renew>
+      <BidRenewal />
+      <!-- <Renew v-if="activeTab === 'renew'" /> -->
     </div>
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref,onMounted } from 'vue'
+import { ref, onMounted } from 'vue'
 import BidRenewal from '@/components/renewal/BidRenewal.vue'
-import Renew from '@/components/renewal/Renew.vue'
+// import Renew from '@/components/renewal/Renew.vue'
 import api from '@/api/common'
 
 const historyProjects = ref<any[]>([])
 
+/** 固定投续标;恢复 Tab 时改回可切换 */
 const activeTab = ref('bidrenewal')
 const tabs = ref([
   { label: '投续标', value: 'bidrenewal' },
@@ -39,7 +41,7 @@ const initData = () => {
 }
 
 const getHistoryProjects = () => {
-  api.getHistoryPartner().then(res => {
+  api.getHistoryPartner().then((res) => {
     historyProjects.value = (res as any).content || []
   })
 }
@@ -47,18 +49,23 @@ const getHistoryProjects = () => {
 const handleTabClick = (tab: string) => {
   activeTab.value = tab
 }
-
 </script>
 
 <style scoped>
-
+/* 铺满主内容区(.content = 100vh-70px,上下 padding 20+32),禁止整页滚 */
 .renewal-page {
   width: 100%;
+  height: calc(100vh - 70px - 52px);
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
 }
 
 .renewal-content {
   width: 100%;
-  height: 100%;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
 }
 
 .page-tabs {
@@ -66,8 +73,8 @@ const handleTabClick = (tab: string) => {
   gap: 0;
   margin-bottom: 16px;
   border-bottom: 1px solid #e8ecef;
-  /* justify-content: space-between; */
   align-items: center;
+  flex-shrink: 0;
 }
 
 .btn-right {

+ 25 - 10
src/views/bidPreparation/RenewalExpireDetail.vue

@@ -1,11 +1,11 @@
 <template>
   <div class="expire-detail-page">
-    <RenewalHeaderBar :projects="projects" />
+    <RenewalHeaderBar :projects="projects" :terminated="isTerminated" />
 
     <RenewalStepper :steps="steps" :stage="stage" />
 
     <!-- 主内容区域 -->
-    <div class="main-content">
+    <div class="main-content" :class="{ 'is-terminated': isTerminated }">
       <!-- 左侧内容 -->
       <div class="left-panel">
         <!-- 监控招标公告 -->
@@ -17,7 +17,7 @@
             <p class="monitoring-desc">
               {{currentStep.text || ''}}
             </p>
-            <div class="monitoring-actions" v-if="currentStep.left_btn_text">
+            <div class="monitoring-actions" v-if="currentStep.left_btn_text && !isTerminated">
               <div v-if="currentStep.label == '标书制作'" style="width: 100%;">
                 <!-- 招标公告与需求拆解:报名确认后可上传;待确认后可生成 -->
                 <div v-if="stepsType == '1' || stepsType == '2'" class="bid-make-actions">
@@ -116,6 +116,7 @@
           :zb-detail-url="projects.zbmsxx?.zb_detail_url"
           :is-task-done="isTaskDone"
           :get-task-display-text="getTaskDisplayText"
+          :terminated="isTerminated"
           @confirm-task="confirmTask"
           @open-view="handleOpenViewClick"
         />
@@ -335,6 +336,12 @@ const bidRules = reactive<FormRules<RuleForm>>({
 
 const finishBidLoading = ref(false)
 
+/** 中标登记后 c_terminated:流程终止,只读 */
+const isTerminated = computed(() => {
+  const v = projects.value?.c_terminated
+  return v === true || v === 'true' || v === 1 || v === '1'
+})
+
 // ---- 编排层:阶段 / 任务 / 标书生成 / 上传 ----
 const {
   steps,
@@ -610,6 +617,10 @@ const initData = () => {
   getStage()
 }
 const confirmTask = async (task: any) => {
+  if (isTerminated.value) {
+    ElMessage.warning('流程已终止,无法继续操作')
+    return
+  }
   tempTask.value = {}
   if(task.id == "1" || task.id == "2"){
     const msg = getMessage(2)
@@ -692,6 +703,10 @@ const confirmTask = async (task: any) => {
 }
 // 点击进入下一步
 const handleViewClick = (item: any,type: string) => {
+  if (isTerminated.value) {
+    ElMessage.warning('流程已终止,无法继续操作')
+    return
+  }
     if(item.n == 1){
       updateProject()
     }else if(item.n == 2){
@@ -831,8 +846,8 @@ const getProjects = async () => {
       year: soureProjects.value.c_guid.substring(10,14),
       name: soureProjects.value.c_xmmc,
       type: "中标",
-      money: soureProjects.value.c_nhhtsr,
-      address: soureProjects.value.c_query_address || ''
+      money: UTIL.sourceContractAmountYuan(soureProjects.value),
+      address: UTIL.sourceProjectAddress(soureProjects.value)
     }]
     // 续标:投续标参考标书为空时,尝试用在管项目历史标书(仅 .docx)回填
     await syncRefBidFromSourceProject()
@@ -893,13 +908,13 @@ const updaterwlcqdProject2 = async (item:any,lcid:any) => {
 
 // 项目类型改变时,进行更新项目类型
 const handleJKTypeChange = async (val:any) => {
-  console.log(val)
+  // console.log(val)
   let opt = zbjkProject.value.find((item:any) => item.id == val)
-  console.log(opt)
+  // console.log(opt)
   if(opt){
     if(opt.type == 'notice'){//公告
         let ret = await getSoureProjects(COLUMN.ZB_NOTICE,opt.id,"id")
-        console.log(ret)
+        // console.log(ret)
         ruleZBForm.c_project_code = ret.c_project_code || ""
         ruleZBForm.c_project_name = ret.c_project_name || ""
         ruleZBForm.c_budget_amount = ret.c_budget_amount || ""
@@ -911,7 +926,7 @@ const handleJKTypeChange = async (val:any) => {
         ruleZBForm.c_detail_url = ret.c_detail_url || ""
     }else{//意向
         let ret = await getSoureProjects(COLUMN.ZB_INTENTION,opt.id,"id")
-        console.log(ret)
+        // console.log(ret)
         ruleZBForm.c_project_code = ret.id || ""
         ruleZBForm.c_project_name = ret.title || ""
         ruleZBForm.c_budget_amount = 0
@@ -1244,7 +1259,7 @@ const handleBidFileChange = (file: any) => {
 }
 
 const submitBidFileForm = () => {
-    console.log(BidFileForm.value)
+    // console.log(BidFileForm.value)
     // 参考标书写入投续标表字段 c_ckbs(modelId 1852)
     const params: any = {
       columnId: COLUMN.TXB,

+ 3 - 2
src/views/bidPreparation/RenewalXQDetail.vue

@@ -48,6 +48,7 @@ import XqHeaderBar from '@/components/renewal/xq/XqHeaderBar.vue'
 import XqMainPanel from '@/components/renewal/xq/XqMainPanel.vue'
 import XqSidePanel from '@/components/renewal/xq/XqSidePanel.vue'
 import DialogXqRegister from '@/components/renewal/xq/dialogs/DialogXqRegister.vue'
+import { sourceContractAmountYuan, sourceProjectAddress } from '@/utils/util'
 
 const router = useRouter()
 const projects = ref<any>({})
@@ -338,8 +339,8 @@ const getProjects = async () => {
         year: soureProjects.value.c_guid.substring(10, 14),
         name: soureProjects.value.c_xmmc,
         type: '中标',
-        money: soureProjects.value.c_nhhtsr,
-        address: soureProjects.value.c_query_address || '',
+        money: sourceContractAmountYuan(soureProjects.value),
+        address: sourceProjectAddress(soureProjects.value),
       },
     ]
   }

+ 21 - 4
src/views/bidResult/BidResult.vue

@@ -442,12 +442,12 @@ const openFileForm = (type: string) => {
   dialogFormFileVisible.value = true
 }
 
-/** 在管项目树(中标登记级联用);与 useAdminArea 的 glArea 结构不同,保留本页 buildAreaTree */
+/** 在管项目树(中标登记「父级在管」级联用);需含已发布在管,否则续标 xmbh 无法匹配展示 */
 const getFinishProjects = async () => {
   const params: any = {
     columnId: COLUMN.SOURCE,
     orderBy: JSON.stringify([{ field: 'c_zgxmhtdqrq', orderByType: 1 }]),
-    states: '0',
+    states: '0,1,2,3',
     pageSize: 9999,
     page: 0,
   }
@@ -535,11 +535,28 @@ const buildAreaTree = (list: any[]) => {
 <style scoped>
 .bid-result-page {
   width: 100%;
+  height: calc(100vh - 70px - 52px);
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
 }
 
 .bid-container {
-  height: calc(100vh - 180px);
-  overflow-y: auto;
+  flex: 1;
+  min-height: 0;
+  overflow: hidden;
+  display: flex;
+  flex-direction: column;
+}
+
+.bid-container > :deep(.bid-monitor-page),
+.bid-container > :deep(.history-result-page) {
+  flex: 1;
+  min-height: 0;
+}
+
+.bid-result-page > :deep(.page-tabs) {
+  flex-shrink: 0;
 }
 
 :root {

+ 320 - 82
src/views/ledger/ProjectDetail.vue

@@ -11,11 +11,23 @@
     />
     <ProjectDetailInfo
       :projects="projects"
-      :history-bid-list="historyBidList"
       :wtfw-label="getWTFWLabel(projects.c_wtfs)"
       :jf-label="getJFLabel(projects.c_jffs)"
     />
     <ProjectDetailMap ref="mapSectionRef" />
+
+    <DialogXqRenewSource
+      v-model:visible="xqDialogVisible"
+      :parent="projects"
+      :gl-area="glAreaTree"
+      :yt-list="ytList"
+      :zgfw-options="wtfwOptions"
+      :jffs-options="jffsOptions"
+      :fkms-options="fkmsOptions"
+      :initial-area-codes="xqAreaCodes"
+      :submitting="xqSubmitting"
+      @submit="submitXqRenewSource"
+    />
   </div>
 </template>
 
@@ -31,9 +43,15 @@ import { COLUMN, MODEL } from '@/constants/dmsColumns'
 import { getDisplayUserName } from '@/utils/displayUser'
 import { calcRemainingBase } from '@/utils/renewalMath'
 import { resolveProgresFromXmjdJson } from '@/composables/useNavigateToRenewalDetail'
+import { nextSourceGuidFromLatest } from '@/utils/projectGuid'
+import { buildAreaTree } from '@/utils/tree'
+import { sourceContractAmountYuan } from '@/utils/util'
 import ProjectDetailHeader from '@/components/ledger/ProjectDetailHeader.vue'
 import ProjectDetailInfo from '@/components/ledger/ProjectDetailInfo.vue'
 import ProjectDetailMap from '@/components/ledger/ProjectDetailMap.vue'
+import DialogXqRenewSource, {
+  type XqRenewForm,
+} from '@/components/ledger/DialogXqRenewSource.vue'
 
 const router = useRouter()
 const mapSectionRef = ref<InstanceType<typeof ProjectDetailMap>>()
@@ -45,15 +63,23 @@ const projects = ref<any>({})
 const tempArr = ref<any>([])
 const wtfwOptions = ref<any>([])
 const jffsOptions = ref<any>([])
-const historyBidList = ref<any>([])
+const fkmsOptions = ref<any>([])
+const ytList = ref<any[]>([])
+const glAreaTree = ref<any[]>([])
 
 /** 当前在管项目已关联的进行中续标 / 续签记录(有则顶栏显示直达按钮) */
 const ongoingTxb = ref<any | null>(null)
 const ongoingXq = ref<any | null>(null)
 
+const xqDialogVisible = ref(false)
+const xqSubmitting = ref(false)
+/** 打开弹窗时解析出的区码/片区码,提交时可作兜底 */
+const xqAreaCodes = ref<string[]>([])
+
 onMounted(() => {
   initCName()
   initData()
+  loadXqDialogMeta()
 })
 
 const initData = () => {
@@ -85,14 +111,57 @@ const getDmsTypes = async (cName: string) => {
   }
 }
 
+/** DMS 单选选项(frontType=check),如付款模式 */
+const getSelectOptions = async (sName: string) => {
+  const res = await api.getSelectBySName({ sName })
+  if ((res as any).code === 200 && Array.isArray((res as any).content)) {
+    return (res as any).content.map((item: any) => ({
+      ...item,
+      label: item.name,
+      value: item.index,
+    }))
+  }
+  return []
+}
+
 const initCName = async () => {
-  // 并行请求两个字典,比 await 串行更快
-  const [wtOptions, jfOptions] = await Promise.all([
+  const [wtOptions, jfOptions, fkOptions] = await Promise.all([
     getDmsTypes('sq_wtfs'),
     getDmsTypes('sq_jffs'),
+    getSelectOptions('shenqin_fkms'),
   ])
   wtfwOptions.value = wtOptions
   jffsOptions.value = jfOptions
+  fkmsOptions.value = fkOptions
+}
+
+/** 续签弹窗依赖:业态 + 管理区域(区码/片区码树) */
+const loadXqDialogMeta = async () => {
+  const [ytRes, areaRes] = await Promise.all([
+    api.getDmsDataList({
+      columnId: COLUMN.SERVICE_TYPE,
+      states: '0,1,2,3',
+      pageSize: 999,
+      page: 0,
+    }),
+    api.getDmsDataList({
+      columnId: COLUMN.ADMIN_AREA,
+      states: '0,1,2,3',
+      pageSize: 9999,
+      page: 0,
+    }),
+  ])
+  if ((ytRes as any).code === 200) {
+    ytList.value = ((ytRes as any).content.data || []).map((item: any) => ({
+      ...item,
+      label: item.c_yt_name,
+      value: item.c_yt_name,
+      children: item.children || [],
+    }))
+  }
+  if ((areaRes as any).code === 200) {
+    glAreaTree.value = buildAreaTree((areaRes as any).content.data || [])
+  }
 }
 
 const getProjects = async () => {
@@ -119,26 +188,93 @@ const getProjects = async () => {
       ...item,
       ...calculateRemaining(item.c_zgxmhtksrq, item.c_zgxmhtdqrq),
     }))[0]
-    historyBidList.value = []
-    historyBidList.value = [
+
+    await Promise.all([loadStationMap(), loadOngoingFlows()])
+  }
+}
+
+/** 地图中心:几何来自驻地表;弹窗展示在管项目信息;员工入口按驻地 id 查员工表 */
+const loadStationMap = async () => {
+  const guid = projects.value?.c_guid
+  if (!guid) return
+  const res = await getDmsDataList({
+    columnId: COLUMN.STATION,
+    states: '0,1,2,3',
+    pageSize: 999,
+    page: 0,
+    search: JSON.stringify([
       {
-        year: projects.value.c_guid.substring(10, 14),
-        name: projects.value.c_xmmc,
-        type: '中标',
-        money: projects.value.c_nhhtsr,
-        address: projects.value.c_query_address || '',
+        field: 'c_father_project',
+        searchType: 2,
+        content: { value: `%${guid}%` },
       },
-    ]
-
-    if (projects.value.c_polygon) {
-      const geojson = wellknown.parse(projects.value.c_polygon.value)
-      mapSectionRef.value?.getBounds(geojson)
-      mapSectionRef.value?.renderGeoJson(projects.value)
+    ]),
+  })
+  if ((res as any).code !== 200) return
+  const list = (res as any).content?.data || []
+  const projectProps = projectPopupProps(projects.value)
+  const features: any[] = []
+  for (const row of list) {
+    const raw = row?.c_polygon?.value ?? row?.c_polygon
+    if (!raw || typeof raw !== 'string') continue
+    try {
+      const parsed = wellknown.parse(raw) as any
+      if (!parsed) continue
+      // station_id = 驻地表内容 id,对应员工 c_sszd;不覆盖项目 id / 弹窗字段
+      const props = {
+        ...projectProps,
+        station_id: String(row.id ?? ''),
+      }
+      if (parsed.type === 'FeatureCollection') {
+        for (const f of parsed.features || []) {
+          features.push({
+            ...f,
+            properties: { ...(f.properties || {}), ...props },
+          })
+        }
+      } else if (parsed.type === 'Feature') {
+        features.push({
+          ...parsed,
+          properties: { ...(parsed.properties || {}), ...props },
+        })
+      } else {
+        features.push({
+          type: 'Feature',
+          geometry: parsed,
+          properties: props,
+        })
+      }
+    } catch {
+      /* 单条几何解析失败则跳过 */
     }
+  }
+  if (!features.length) return
+  const fc = { type: 'FeatureCollection', features }
+  mapSectionRef.value?.getBounds(fc)
+  mapSectionRef.value?.renderGeoJson(fc)
+}
 
-    // 查是否已有续签 / 续标流程,决定顶栏直达按钮
-    await loadOngoingFlows()
+/** 弹窗沿用项目表字段;Mapbox properties 只接受原始类型 */
+function projectPopupProps(project: any): Record<string, string | number | boolean> {
+  const keys = [
+    'c_xmmc',
+    'c_xmdz',
+    'c_sfzxs',
+    'c_sq',
+    'c_xmygrs',
+    'c_glqynbkj',
+    'c_hzyt',
+    'id',
+    'c_guid',
+  ]
+  const props: Record<string, string | number | boolean> = {}
+  for (const k of keys) {
+    const v = project?.[k]
+    if (v == null || v === '') continue
+    if (typeof v === 'object') continue
+    props[k] = k === 'c_xmygrs' ? Number(v) || 0 : (v as string | number | boolean)
   }
+  return props
 }
 
 /** 按项目 guid 查投续标、续签栏目是否已有记录 */
@@ -205,7 +341,6 @@ const getServiceYear = (start: string | Date, end?: string | Date) => {
   let months = endM.month() - startM.month()
   let days = endM.date() - startM.date()
 
-  // 日期不够,借位
   if (days < 0) {
     months--
     days += startM.clone().add(years, 'y').add(months + 1, 'M').daysInMonth()
@@ -214,7 +349,6 @@ const getServiceYear = (start: string | Date, end?: string | Date) => {
     years--
     months += 12
   }
-  // 小数总年(用于展示 2.5 年这种)
   const totalYear = Number((years + months / 12 + days / 365).toFixed(2))
   return {
     years,
@@ -227,11 +361,10 @@ const getServiceYear = (start: string | Date, end?: string | Date) => {
 const handleRenewalClick = () => {
   getXMProjects(projects.value.c_guid, 'xmbh', COLUMN.TXB).then(async (res) => {
     if (res.length > 0) {
-      // 直接进入投续标详情页面
       writeFlowSession(res[0])
       router.push(`/renewal-expire-detail/${res[0].id}`)
     } else {
-      const ids = await addProject()
+      const ids = await addTxbProject()
       if (ids) {
         sessionStorage.setItem('projectName', projects.value.c_xmmc)
         sessionStorage.setItem('name', projects.value.c_xmmc)
@@ -245,28 +378,173 @@ const handleRenewalClick = () => {
   })
 }
 
-const handleRenewaXQlClick = () => {
-  getXMProjects(projects.value.c_guid, 'c_xmbh', COLUMN.RENEW_XQ).then(async (res) => {
-    if (res.length > 0) {
-      // 直接进入续签详情页面
-      writeFlowSession(res[0])
-      router.push(`/renewal-xq-detail/${res[0].id}`)
-    } else {
-      const ids = await addXQProject()
-      if (ids) {
-        sessionStorage.setItem('projectName', projects.value.c_xmmc)
-        sessionStorage.setItem('name', projects.value.c_xmmc)
-        sessionStorage.setItem('dmsId', `${ids}`)
-        sessionStorage.setItem('progres', tempArr.value[tempArr.value.length - 1].xmjd)
-        router.push(`/renewal-xq-detail/${ids}`)
-      } else {
-        ElMessage.error('续签失败')
+/** 确认续签:打开弹窗(不再建 RENEW_XQ、不进续签详情页) */
+const handleRenewaXQlClick = async () => {
+  await resolveParentAreaCodes()
+  xqDialogVisible.value = true
+}
+
+/** 用父项目片区名反查区码/片区码,便于生成 c_guid */
+const resolveParentAreaCodes = async () => {
+  xqAreaCodes.value = []
+  const partName = projects.value?.c_glqynbkj
+  if (!partName) return
+  const res = await getDmsDataList({
+    columnId: COLUMN.ADMIN_AREA,
+    states: '0,1,2,3',
+    pageSize: 1,
+    page: 0,
+    search: JSON.stringify([
+      {
+        field: 'c_part_name',
+        searchType: 2,
+        content: { value: `%${partName}%` },
+      },
+    ]),
+  })
+  if ((res as any).code === 200) {
+    const row = (res as any).content.data?.[0]
+    if (row?.c_district_code && row?.c_part_code) {
+      xqAreaCodes.value = [row.c_district_code, row.c_part_code]
+    }
+  }
+}
+
+/** DMS date_time 按时间戳写入;空值不传,避免空串导致整单失败 */
+function putDateTime(content: Record<string, any>, key: string, value: string) {
+  if (!value) return
+  const m = moment(value)
+  if (!m.isValid()) return
+  content[key] = m.valueOf()
+}
+
+/** 读出常为 { value: WKT },写入须为字符串 */
+function polygonForWrite(raw: any): string {
+  if (raw == null || raw === '') return ''
+  if (typeof raw === 'string') return raw
+  if (typeof raw === 'object' && raw.value != null) {
+    return typeof raw.value === 'string' ? raw.value : JSON.stringify(raw.value)
+  }
+  return ''
+}
+
+const submitXqRenewSource = async (payload: { form: XqRenewForm; areaCodes: string[] }) => {
+  const codes =
+    payload.areaCodes?.length >= 2 ? payload.areaCodes : xqAreaCodes.value
+  if (!codes || codes.length < 2) {
+    ElMessage.error('请选择管理区域(用于生成项目编码)')
+    return
+  }
+  const ytcode =
+    ytList.value.find((item: any) => item.c_yt_name === payload.form.c_hzyt)?.c_yt_code || ''
+  if (!ytcode) {
+    ElMessage.error('所选业态缺少业态编码,无法生成项目编码')
+    return
+  }
+
+  xqSubmitting.value = true
+  try {
+    const prefix = `${codes[0]}${codes[1]}${moment().format('YYYYMMDD')}${ytcode}`
+    const latest = await findLatestSourceByGuidPrefix(prefix)
+    const c_guid = nextSourceGuidFromLatest(latest?.c_guid, {
+      districtCode: codes[0],
+      partCode: codes[1],
+      ytCode: ytcode,
+      hasFather: true,
+    })
+
+    const content: any = {
+      // DMS 必填壳字段:用新编码;不在弹窗展示
+      title: c_guid,
+      content: c_guid,
+      c_guid,
+      c_father_code: projects.value.c_guid,
+      c_xmmc: payload.form.c_xmmc,
+      c_xmdz: payload.form.c_xmdz,
+      c_htqyjf: payload.form.c_htqyjf,
+      c_glqynbkj: payload.form.c_glqynbkj,
+      c_sq: payload.form.c_sq,
+      c_sfzxs: payload.form.c_sfzxs || '上海市',
+      c_htmj: payload.form.c_htmj ?? '',
+      c_htbh: payload.form.c_htbh,
+      // 合同额双写:c_hte(合同额)+ c_htzj(总合同额),单位元
+      c_hte: payload.form.c_htzj ?? '',
+      c_htzj: payload.form.c_htzj ?? '',
+      c_bndhtys: payload.form.c_bndhtys ?? '',
+      c_hzyt: payload.form.c_hzyt,
+      c_xfyt: payload.form.c_xfyt || payload.form.c_hzyt,
+      c_wtfs: payload.form.c_wtfs,
+      c_jffs: payload.form.c_jffs,
+      c_fkms: payload.form.c_fkms,
+      c_pjfzr: payload.form.c_pjfzr,
+      c_xmygrs: payload.form.c_xmygrs,
+      c_hts: payload.form.c_hts,
+      c_xms: payload.form.c_xms,
+      c_bcxyzj: payload.form.c_bcxyzj,
+      c_zbdfqk: payload.form.c_zbdfqk,
+      c_xmzbggrq: payload.form.c_xmzbggrq || '',
+      c_xmzbgsrq: payload.form.c_xmzbgsrq || '',
+      c_xmtbrq: payload.form.c_xmtbrq || '',
+      c_xmzbrq: payload.form.c_xmzbrq || '',
+      c_history_tender: payload.form.c_history_tender || projects.value.c_history_tender || '',
+    }
+    // date_time 须为时间戳;空串会被 DMS 拒掉
+    putDateTime(content, 'c_zgxmhtksrq', payload.form.c_zgxmhtksrq)
+    putDateTime(content, 'c_zgxmhtdqrq', payload.form.c_zgxmhtdqrq)
+    putDateTime(content, 'c_xmkssj', payload.form.c_xmkssj)
+    putDateTime(content, 'c_xmdqsj', payload.form.c_xmdqsj)
+    // 读出是 { value: WKT },写入须为 WKT/geojson 字符串
+    const polygon = polygonForWrite(
+      projects.value.c_polygon?.value != null ? projects.value.c_polygon : payload.form.c_polygon
+    )
+    if (polygon) content.c_polygon = polygon
+
+    const res = await api.addContent({
+      content: JSON.stringify(content),
+      columnId: COLUMN.SOURCE,
+      modelId: MODEL.SOURCE,
+    })
+    if ((res as any)?.code === 200) {
+      ElMessage.success('续签成功,已生成新在管项目')
+      xqDialogVisible.value = false
+      const newId = (res as any).content
+      if (newId) {
+        sessionStorage.setItem('dmsId', String(newId))
+        sessionStorage.setItem('name', payload.form.c_xmmc)
+        sessionStorage.setItem('projectName', payload.form.c_xmmc)
+        router.push(`/project-detail/${newId}`)
       }
+    } else {
+      ElMessage.error((res as any)?.message || '续签失败,未能写入项目表')
     }
+  } catch {
+    ElMessage.error('续签失败,未能写入项目表')
+  } finally {
+    xqSubmitting.value = false
+  }
+}
+
+const findLatestSourceByGuidPrefix = async (prefix: string) => {
+  const res = await getDmsDataList({
+    columnId: COLUMN.SOURCE,
+    states: '0,1,2,3',
+    pageSize: 1,
+    page: 0,
+    orderBy: JSON.stringify([{ field: 'update_time', orderByType: 2 }]),
+    search: JSON.stringify([
+      {
+        field: 'c_guid',
+        searchType: 2,
+        content: { value: `%${prefix}%` },
+      },
+    ]),
   })
+  if ((res as any).code === 200) {
+    return (res as any).content.data?.[0] || null
+  }
+  return null
 }
 
-// 获取投续标/续签数据
 const getXMProjects = async (id: string, type: string, columnId: number) => {
   const params: any = {
     columnId: columnId,
@@ -292,10 +570,9 @@ const getXMProjects = async (id: string, type: string, columnId: number) => {
   }
 }
 
-const addProject = async () => {
+const addTxbProject = async () => {
   tempArr.value = []
   if (projects.value.days >= 100) {
-    // 大于百天,不需要预警
     for (let i = 1; i < 3; i++) {
       let str = {
         xmjd: i,
@@ -307,7 +584,6 @@ const addProject = async () => {
       tempArr.value.push(str)
     }
   } else {
-    // 需要预警的项目
     let str = {
       xmjd: 1,
       type: 1,
@@ -318,12 +594,11 @@ const addProject = async () => {
     tempArr.value.push(str)
   }
   let formData: any = {
-    // 项目
     title: projects.value.c_xmmc,
     content: projects.value.content,
     c_fbz: projects.value.c_htqyjf,
     c_zgfw: projects.value.c_wtfs,
-    c_nh: projects.value.c_nhhtsr,
+    c_nh: sourceContractAmountYuan(projects.value),
     c_xmbtqc: projects.value.c_xmmc,
     c_qx: projects.value.c_sq,
     c_xmjd_json: JSON.stringify(tempArr.value),
@@ -336,7 +611,6 @@ const addProject = async () => {
     rwlcqd: JSON.stringify(CONFIG.TASKLIST),
     c_sfwc: false,
   }
-  // 历史标书须以 .docx 结尾才写入投续标参考标书 c_ckbs;否则不补,生成前必须上传
   const historyTender = projects.value.c_history_tender || ''
   if (typeof historyTender === 'string') {
     const clean = historyTender.toLowerCase().replace(/\?.*$/, '').trim()
@@ -360,42 +634,6 @@ const addProject = async () => {
     return null
   }
 }
-
-const addXQProject = async () => {
-  tempArr.value = []
-  let str = {
-    xmjd: 1,
-    type: 1,
-    create_time: '',
-    username: userName,
-    userid: userInfo?.id || '',
-  }
-  tempArr.value.push(str)
-  let formData = {
-    title: projects.value.c_xmmc,
-    content: projects.value.content,
-    c_wtfs: projects.value.c_wtfs,
-    c_htje: projects.value.c_nhhtsr,
-    c_qx: projects.value.c_sq,
-    c_xmjd_json: JSON.stringify(tempArr.value),
-    c_kssj: moment(projects.value.c_zgxmhtksrq).format('YYYY-MM-DD HH:mm'),
-    c_jssj: moment(projects.value.c_zgxmhtdqrq).format('YYYY-MM-DD HH:mm'),
-    c_xmbt: projects.value.c_xmmc,
-    c_xmbh: projects.value.c_guid,
-  }
-
-  const params = {
-    content: JSON.stringify(formData),
-    columnId: COLUMN.RENEW_XQ,
-    modelId: MODEL.RENEW_XQ,
-  }
-  const res = await api.addContent(params)
-  if ((res as any).code === 200) {
-    return (res as any)?.content
-  } else {
-    return null
-  }
-}
 </script>
 
 <style scoped>

+ 93 - 39
src/views/ledger/Projects.vue

@@ -1,14 +1,14 @@
 <template>
   <div class="projects-page">
-    <div class="panel-card">
+    <div class="panel-card projects-panel">
       <div class="card-header">
         <h3 class="card-title">项目列表</h3>
       </div>
-      <div class="card-body">
+      <div class="card-filters">
         <div class="pool-toolbar">
-          <el-input 
-            v-model="searchText" 
-            placeholder="搜索项目名称" 
+          <el-input
+            v-model="searchText"
+            placeholder="搜索项目名称"
             clearable
             @keyup.enter="queryProjects"
             @clear="queryProjects"
@@ -33,11 +33,14 @@
           </el-select>
           <el-button type="primary" class="btn enter-btn" @click="queryProjects">查询</el-button>
         </div>
+      </div>
+
+      <div class="card-list-area">
         <div class="empty-state" v-if="total === 0">
-            <span class="empty-icon">🔍</span>
-            <p>暂无数据</p>
+          <span class="empty-icon">🔍</span>
+          <p>暂无数据</p>
         </div>
-        <div v-else>
+        <div v-else class="table-scroll">
           <table>
             <thead>
               <tr>
@@ -50,44 +53,53 @@
               </tr>
             </thead>
             <tbody>
-               <!-- MultipleFormsOfJointInvestigation 接口获取的数据展示 -->
-              <!-- <tr v-for="item in projects" :key="(item as any)[columnId+'_c_xmmc']">
-                <td><span class="link-text" @click="handleViewClick(item)">{{ (item as any)[columnId+'_c_xmmc'] }}</span></td>
-                <td>{{ (item as any)[columnId+'_c_hzyt']}}</td>
-                <td>{{ (item as any)[columnId+'_c_glqynbkj'] }}</td>
-                <td>{{ moment((item as any)[columnId+'_c_zgxmhtksrq']).format('YYYY-MM-DD') }} - {{ moment((item as any)[columnId+'_c_zgxmhtdqrq']).format('YYYY-MM-DD') }}</td>
-                <td><span :class="['badge', (item as any)['daysClass']]">{{ (item as any)['days'] }}</span></td>
+              <tr v-for="item in projects" :key="(item as any)['id']">
                 <td>
-                  <span class="link-text" @click="handleViewClick(item)">{{ (item as any)[columnId+'_name'] || '详情' }}</span>
+                  <span class="link-text" @click="handleViewClick(item)">{{
+                    (item as any)['c_xmmc']
+                  }}</span>
                 </td>
-              </tr> -->
-              <!-- selectContentList 接口获取的数据展示 -->
-              <tr v-for="item in projects" :key="(item as any)['id']">
-                <td><span class="link-text" @click="handleViewClick(item)">{{ (item as any)['c_xmmc'] }}</span></td>
-                <td>{{ (item as any)['c_hzyt']}}</td>
+                <td>{{ (item as any)['c_hzyt'] }}</td>
                 <td>{{ (item as any)['c_glqynbkj'] }}</td>
-                <td>{{ (item as any)['c_zgxmhtksrq'] == undefined ? '' : moment((item as any)['c_zgxmhtksrq']).format('YYYY-MM-DD') }} - {{ (item as any)['c_zgxmhtdqrq'] == undefined ? '' : moment((item as any)['c_zgxmhtdqrq']).format('YYYY-MM-DD') }}</td>
-                <td><span :class="['badge', (item as any)['daysClass']]">{{ (item as any)['days'] }}</span></td>
                 <td>
-                  <span class="link-text" @click="handleViewClick(item)">{{ (item as any)['name'] || '详情' }}</span>
+                  {{
+                    (item as any)['c_zgxmhtksrq'] == undefined
+                      ? ''
+                      : moment((item as any)['c_zgxmhtksrq']).format('YYYY-MM-DD')
+                  }}
+                  -
+                  {{
+                    (item as any)['c_zgxmhtdqrq'] == undefined
+                      ? ''
+                      : moment((item as any)['c_zgxmhtdqrq']).format('YYYY-MM-DD')
+                  }}
+                </td>
+                <td>
+                  <span :class="['badge', (item as any)['daysClass']]">{{
+                    (item as any)['days']
+                  }}</span>
+                </td>
+                <td>
+                  <span class="link-text" @click="handleViewClick(item)">{{
+                    (item as any)['name'] || '详情'
+                  }}</span>
                 </td>
               </tr>
             </tbody>
           </table>
-          <!-- 分页控件 -->
-          <div class="pagination-section">
-            <span class="total-text">共{{ total }}个项目</span>
-            <el-pagination
-              layout="prev, pager, next"
-              :total="total"
-              :current-page="page"
-              :page-size="pageSize"
-              @current-change="handleCurrentChange"
-            >
-            </el-pagination>
-          </div>
         </div>
       </div>
+
+      <div class="pagination-section" v-if="total > 0">
+        <span class="total-text">共{{ total }}个项目</span>
+        <el-pagination
+          layout="prev, pager, next"
+          :total="total"
+          :current-page="page"
+          :page-size="pageSize"
+          @current-change="handleCurrentChange"
+        />
+      </div>
     </div>
   </div>
 </template>
@@ -235,7 +247,7 @@ const getAllProjects = async () => {
   const res = await getDmsDataList(params)
   loading.close()
   if ((res as any).code === 200) {
-    console.log(res)
+    // console.log(res)
     const allData = (res as any).content.data.map((item: any) => ({
       ...item,
       ...calculateRemainingDays((item as any)['c_zgxmhtdqrq'])
@@ -326,8 +338,51 @@ const getDmsDataList = async (params: any) => {
 <style scoped>
 .projects-page {
   width: 100%;
+  height: calc(100vh - 70px - 52px);
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.projects-panel {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
 }
-th, td {
+
+.card-filters {
+  padding: 12px 16px 0;
+  flex-shrink: 0;
+}
+
+.card-list-area {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.table-scroll {
+  flex: 1;
+  min-height: 0;
+  overflow-x: hidden;
+  overflow-y: auto;
+  padding: 0 16px;
+}
+
+.pagination-section {
+  flex-shrink: 0;
+  border-top: 1px solid #e8ecef;
+  margin: 0 !important;
+  padding: 8px 16px;
+  background: #fff;
+}
+
+th,
+td {
   text-align: left;
   padding: 10px 12px;
   border-bottom: 1px solid #e8ecef;
@@ -357,5 +412,4 @@ th, td {
   width: 260px;
   flex-shrink: 0;
 }
-
 </style>

+ 80 - 0
word/上传并校验文件 接口手册.md

@@ -0,0 +1,80 @@
+# 上传并校验文件 接口手册
+
+上传文件到dms-》验证文件可用性-》返回验证结果及文件访问路径
+
+**接口地址**:`POST http://121.43.55.7:10026/shenqin/uploadAndVerify`  
+**认证方式**:Header `token`  
+**请求类型**:`multipart/form-data`
+
+## 请求头
+
+| Header | 必填 | 说明 |
+|---|---|---|
+| token | 是 |  |
+| Content-Type | 是 | `multipart/form-data` |
+
+## 请求参数(form-data)
+
+| 参数 | 类型 | 必填 | 说明 | 示例 |
+|---|---|---|---|---|
+| file | File | 是 | 上传文件 |  |
+| columnId | string | 是 |  | 1743 |
+| fileType | string | 是 | 可选:`docx`、`pdf` | pdf |
+
+## 请求示例
+
+```bash
+curl --location --request POST 'http://121.43.55.7:10026/shenqin/uploadAndVerify' \
+--header 'token: ' \
+--header 'Content-Type: multipart/form-data' \
+--form 'file=@"<文件路径>"' \
+--form 'columnId="1743"' \
+--form 'fileType="pdf"'
+```
+
+## 成功响应示例
+
+```json
+{
+    "code": 200,
+    "message": "成功",
+    "content": {
+        "valid": true,
+        "file_role": "REFERENCE_BID",
+        "filename": "1743-file-89090312-3956-42c4-8155-4f10833c4d61.pdf",
+        "size": 1576446,
+        "sha256": "aa0e543ed653629419cfd2474b88b9f7b888ef9ebef8cb90d189318347223f8a",
+        "file_type": "pdf",
+        "is_text_pdf": true,
+        "filePath": "/static/file/1743-file-89090312-3956-42c4-8155-4f10833c4d61.pdf",
+        "resolved_path": "/root/dms_upload/file/1743-file-89090312-3956-42c4-8155-4f10833c4d61.pdf",
+        "message": "文件检测通过"
+    }
+}
+```
+
+## 业务错误码
+
+| code | 说明 |
+|---|---|
+| 400 | 参数异常 |
+| 401 | 文件不可用 |
+| 402 | 文件和实际类型不符合 |
+| 403 | 文件不是含有文字的PDF |
+
+## 响应字段说明
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| code | int | 业务状态码 |
+| message | string | 业务消息 |
+| content.filePath | string | 文件网络访问路径,**uploadFile的返回** |
+| content.valid | boolean | 文件是否校验通过 |
+| content.file_role | string | 文件角色 |
+| content.filename | string | 服务端文件名 |
+| content.size | int | 文件大小(字节) |
+| content.sha256 | string | 文件 SHA-256 |
+| content.file_type | string | 文件类型 |
+| content.is_text_pdf | boolean | 是否文本型 PDF |
+| content.resolved_path | string | 服务器存储路径 |
+| content.message | string | 检测消息 |