Преглед изворни кода

判断是否有历史标书和格式是否正确,如没有历史标书,请重新上传参考标书文件

mork пре 3 недеља
родитељ
комит
7f1f3b84c3
3 измењених фајлова са 364 додато и 8 уклоњено
  1. 262 0
      README.md
  2. BIN
      public/favicon.ico
  3. 102 8
      src/views/bidPreparation/RenewalExpireDetail.vue

+ 262 - 0
README.md

@@ -0,0 +1,262 @@
+# sq_manage
+
+> 申勤物业数字化平台 — 面向物业项目全生命周期的管理后台系统,涵盖项目台账、市场信息、投标准备、招标监控、投标结果等核心业务模块,并集成基于 Mapbox 的"一张图"空间可视化能力。
+
+---
+
+## 技术栈
+
+| 分类 | 技术 | 版本 |
+|------|------|------|
+| 框架 | Vue 3 | ^3.4 |
+| 语言 | TypeScript | ^5.3 |
+| 构建 | Vite | ^5.1 |
+| 路由 | Vue Router | ^4.3 |
+| 状态管理 | Pinia | ^2.1 |
+| UI 组件库 | Element Plus | ^2.6 |
+| 网络请求 | Axios | ^1.6 |
+| 地图 | Mapbox GL | ^3.27 |
+| 地图标注 | Mapbox GL Draw | ^1.5 |
+| 地理空间分析 | @turf/turf | ^7.3 |
+| 图表 | ECharts | ^5.5 |
+| 工具库 | moment.js / qs / markdown-it / vue-clipboard3 / wellknown | — |
+
+---
+
+## 环境要求
+
+- **Node.js** >= 18.0
+- **npm** >= 9.0
+
+---
+
+## 快速开始
+
+### 安装依赖
+
+```bash
+npm install
+```
+
+### 开发模式
+
+```bash
+npm run dev
+```
+
+开发服务器启动后访问 `http://localhost:5173`(Vite 默认端口)。所有 `/api` 前缀的请求会被代理到后端服务。
+
+### 生产构建
+
+```bash
+npm run build
+```
+
+执行 `vue-tsc` 类型检查后,Vite 将项目编译到 `dist/` 目录。
+
+### 预览构建产物
+
+```bash
+npm run preview
+```
+
+---
+
+## 配置说明
+
+### 运行时配置(关键)
+
+项目使用**运行时注入**的方式加载配置,避免构建时硬编码。配置文件位于 [public/static/config/config.js](public/static/config/config.js),会在 HTML 入口处被加载,并将配置挂载到 `window.CONFIG`。
+
+前端通过 [src/utils/config.ts](src/utils/config.ts) 读取 `window.CONFIG`,支持读取失败时的默认值兜底。
+
+`window.CONFIG` 包含以下字段:
+
+| 字段 | 说明 |
+|------|------|
+| `OAUTH_URL` | 认证中心服务地址(登录/改密等) |
+| `DMS_URL` | 文档管理服务地址(内容 CRUD、文件上传) |
+| `SQ_URL` | 申勤业务服务地址(统计、招标生成等) |
+| `ONEMAP_URL` | 一张图服务地址 |
+| `FILEPATH` | 文件访问基路径 |
+| `COLUMNIDARR` | DMS 栏目 ID 数组 |
+| `MODELIDARR` | DMS 模型 ID 数组 |
+| `TASKLIST` | 任务配置列表 |
+| `MAPBOX_ACCESS_TOKEN` | Mapbox 访问令牌 |
+| `MAPBOX_TILES_NORMAL` | 普通地图瓦片 URL |
+| `MAPBOX_TILES_BLUE` | 蓝色风格地图瓦片 URL |
+| `MAPBOX_TILES_YXT` | 杨浦特地图瓦片 URL |
+
+> **部署注意**:上线前必须修改 `public/static/config/config.js` 中的服务地址与 Mapbox Token 为实际生产值。
+
+### Vite 开发代理
+
+[vite.config.ts](vite.config.ts) 中配置了开发代理:
+
+```
+/api → http://121.43.55.7:10081(路径前缀 /api 会被去掉)
+```
+
+如需代理到本地或其他后端,修改 `server.proxy` 中的 `target` 即可。
+
+---
+
+## 目录结构
+
+```
+sq_manage/
+├── public/
+│   ├── favicon.ico
+│   └── static/
+│       ├── config/config.js        # ⭐ 运行时配置(后端地址 / Mapbox Token)
+│       ├── data/                   # 静态数据文件(md / html)
+│       └── plugins/                # 第三方插件(如 createAuth)
+│
+├── src/
+│   ├── api/common.ts               # ⭐ 后端 API 接口集合
+│   ├── components/
+│   │   ├── bidResult/              # 投标结果公共组件(AddContent、AddProject)
+│   │   ├── map/                    # 地图绘制组件(MapDraw)
+│   │   ├── renewal/                # 续标相关组件(BidRenewal、Renew)
+│   │   ├── tenderpool/             # 招标池组件(Intention、Monitor、Notice)
+│   │   └── view/                   # 通用视图组件(GeoJsonEditor)
+│   ├── layouts/MainLayout.vue      # 主布局(侧边栏 + 顶部 + 内容区)
+│   ├── router/index.ts             # ⭐ 路由配置(Hash 模式)
+│   ├── stores/user.ts              # Pinia 用户状态(登录/登出/token)
+│   ├── styles/                     # 全局样式 & 自定义 Element Plus 主题
+│   ├── types/                      # TypeScript 类型声明(md.d.ts / window.d.ts)
+│   ├── utils/
+│   │   ├── config.ts               # 读取 window.CONFIG
+│   │   ├── request.ts              # Axios 实例 + token 拦截器
+│   │   ├── encrypt.ts              # 加密工具
+│   │   └── util.ts                 # 通用工具函数
+│   ├── views/
+│   │   ├── home/Dashboard.vue          # 首页看板(ECharts 图表)
+│   │   ├── map/OneMap.vue              # 一张图(Mapbox 地图 + GeoJSON 展示)
+│   │   ├── ledger/Projects.vue         # 项目台账列表
+│   │   ├── ledger/ProjectDetail.vue    # 项目台账详情
+│   │   ├── market/Market.vue           # 市场信息
+│   │   ├── bidPreparation/Renewal.vue          # 续标列表
+│   │   ├── bidPreparation/RenewalDetail.vue    # 续标详情
+│   │   ├── bidPreparation/RenewalExpireDetail.vue # 续标到期详情
+│   │   ├── bidPreparation/RenewalXQDetail.vue  # 续标详情(XQ)
+│   │   ├── bidMonitor/TenderPool.vue    # 招标池 / 开标监控
+│   │   ├── bidResult/BidResult.vue      # 投标结果
+│   │   └── login/Login.vue              # 登录页
+│   ├── App.vue
+│   ├── main.ts
+│   └── env.d.ts
+│
+├── word/                           # 产品需求 & 设计文档
+├── vite.config.ts                  # ⭐ Vite 构建配置(别名 @、开发代理)
+├── tsconfig.json
+├── index.html
+└── package.json
+```
+
+---
+
+## 业务模块
+
+| 模块 | 路由 | 说明 |
+|------|------|------|
+| 登录 | `/` | 用户登录,token 存储到 localStorage |
+| 首页看板 | `/#/dashboard` | 项目统计、图表可视化 |
+| 一张图 | `/#/onemap` | Mapbox 地图展示物业项目空间分布,支持 GeoJSON 绘制 |
+| 项目台账 | `/#/projects` | 物业项目全生命周期管理,含详情页 |
+| 市场信息 | `/#/market` | 市场情报 & 竞争对手信息 |
+| 续标管理 | `/#/renewal` | 投标准备阶段的续标工作流,含多类详情页 |
+| 招标池 | `/#/tender-pool` | 招标公告、投标意向、开标监控 |
+| 投标结果 | `/#/bid-result` | 投标结果录入与展示 |
+
+---
+
+## 认证流程
+
+项目采用**基于 Token** 的认证机制,核心逻辑在 [src/utils/request.ts](src/utils/request.ts) 和 [src/stores/user.ts](src/stores/user.ts):
+
+1. 登录时调用 `api.login()`,成功后 token 存入 `localStorage`
+2. Axios 请求拦截器自动从 `localStorage` 读取 token,附加到请求头 `token` 字段
+3. 响应拦截器检测到 `无效token` 或 401/403 状态码时,自动清除本地登录态并跳转回登录页
+
+---
+
+## API 概览
+
+后端接口统一由 [src/api/common.ts](src/api/common.ts) 导出,按服务归属分为三类:
+
+| 服务 | 前缀 | 用途 |
+|------|------|------|
+| OAUTH | `CONFIG.OAUTH_URL` | 登录、修改密码 |
+| DMS | `CONFIG.DMS_URL` | 栏目查询、内容 CRUD、文件上传、审核 |
+| SQ | `CONFIG.SQ_URL` | 统计、历史合作方、招标生成 |
+
+所有请求走 `/api` 代理前缀,开发时由 Vite 转发到后端。
+
+---
+
+## 开发注意
+
+- **路径别名**:`@` → `src/`(在 `vite.config.ts` 和 `tsconfig.json` 中均已配置)
+- **构建产物路径**:Vite `base` 设为 `./`,产物可直接部署到任意子路径
+- **Token 过期**:后端返回 `无效token` 或 HTTP 401/403 时,前端会自动清理登录态并跳转登录页
+- **文件上传**:`uploadFile` 接口使用 `multipart/form-data`,需传 `FormData`
+
+---
+
+## License
+
+Internal use only.
+
+
+
+## 提交版本记录
+
+- **2023-07-10**
+1、 初始版本,包含基础功能
+2、 登录流程,添加 token 过期检测
+3、 首页看板图表,添加项目统计
+4、 项目台账模块,添加项目详情页
+5、 市场信息模块,添加竞争对手信息
+6、 续标管理模块,添加续标到期详情页
+7、 招标池模块,添加开标监控
+8、 投标结果模块,添加投标结果录入与展示
+9、 登录页,添加登录失败提示
+10、 项目台账模块,添加项目详情页的项目统计
+11、 市场信息模块,添加竞争对手详情页
+12、 续标管理模块,添加续标详情页的项目统计
+13、 招标池模块,添加开标监控页的项目统计
+14、 投标结果模块,添加投标结果详情页的项目统计
+15、 登录页,添加登录失败提示
+
+- **2023-07-17**
+1、 优化登录页,添加登录失败提示
+2、 优化项目台账模块,添加项目详情页的项目统计
+3、 优化市场信息模块,添加竞争对手详情页
+4、 优化续标管理模块,添加续标详情页的项目统计
+5、 优化招标池模块,添加开标监控页的项目统计
+6、 优化投标结果模块,添加投标结果详情页的项目统计
+
+- **2023-08-04**
+1、 增加Mapbox引擎,展示物业项目空间分布,支持 GeoJSON 绘制
+2、 增加JSONEDIT功能,用于编辑项目空间分布的 GeoJSON 数据
+3、 优化项目台账模块,添加项目详情页的项目统计
+4、 优化市场信息模块,添加竞争对手详情页
+5、 优化续标管理模块,添加续标详情页的项目统计
+6、 优化招标池模块,添加开标监控页的项目统计
+7、 优化投标结果模块,添加中标流程功能,包含中标确认、中标结果展示、中标项目详情
+
+- **2023-08-11**
+1、 优化Mapbox引擎,展示物业项目空间分布,支持 GeoJSON 绘制
+2、 优化JSONEDIT功能,用于编辑项目空间分布的 GeoJSON 数据
+3、 优化市场信息模块,添加竞争对手详情页
+4、 优化续标管理模块,添加续标详情页的项目统计
+5、 优化招标池模块,添加开标监控页的项目统计
+6、 优化投标结果模块,添加投标结果详情页的项目统计
+
+- **2023-08-28**
+1、 优化接口调用AI生成标书功能,支持文件上传
+2、 优化判断标书文件格式是否正确,仅支持 .docx 格式
+3、 优化匹配招标公告功能,根据项目信息自动匹配相关招标
+4、 优化检测标书是否中标功能,根据项目信息自动检测标书是否中标,
+5、 优化确认中标功能,手动确认中标并生成新的项目

BIN
public/favicon.ico


+ 102 - 8
src/views/bidPreparation/RenewalExpireDetail.vue

@@ -54,6 +54,9 @@
                   <el-button type="primary"  class="btn enter-btn" @click="handleViewClick(steps.filter(item => item.n === stage)[0] || {},'')">
                     生成标书
                   </el-button>
+                  <el-button type="primary" v-if="!bidFile.isFileFlag" class="btn enter-btn" @click="dialogFormBidFileVisible = true">
+                    上传参考标书
+                  </el-button>
                   <!-- <el-button type="primary" :loading="swb_status == '2'" class="btn enter-btn" @click="handleViewClick(steps.filter(item => item.n === stage)[0] || {},'swb')">
                     生成商务标书
                   </el-button>
@@ -638,6 +641,7 @@
         <el-upload
           class="upload-demo"
           action=""
+          accept=".pdf,.doc,.docx"
           style="width: 100%;"
           :auto-upload="false"
           :on-change="handleCQFileChange"
@@ -660,6 +664,36 @@
         </template>
        </el-dialog>
 
+
+       <el-dialog v-model="dialogFormBidFileVisible" draggable title="提示信息" width="300">
+        <!-- <el-input v-model="FileForm.name" autocomplete="off" placeholder="请输入文件名称"/> -->
+        <div style="margin-top: 10px;">
+        <el-upload
+          class="upload-demo"
+          action=""
+          accept=".docx"
+          style="width: 100%;"
+          :auto-upload="false"
+          :on-change="handleBidFileChange"
+          :file-list="bidFileList"
+          limit="1"
+        >
+        <el-button size="small" type="primary" style="width: 268px;">点击上传参考标书</el-button>
+        <!-- <template #tip>
+          <div class="el-upload__tip">只能上传 jpg/png 文件,且不超过 500kb</div>
+        </template> -->
+        </el-upload>
+        </div>
+        <template #footer>
+          <div class="dialog-footer">
+            <!-- <el-button @click="dialogFormBidFileVisible = false" class="btn">取消</el-button> -->
+            <el-button type="primary" class="btn enter-btn" @click="submitBidFileForm">
+              确认
+            </el-button>
+          </div>
+        </template>
+       </el-dialog>
+
   </div>
 </template>
 
@@ -1040,15 +1074,32 @@ const handleViewClick = (item: any,type: string) => {
       }else if(stepsType.value == '2'){
         let flag =  projects.value.rwlcqd.find((p: any) => p.id == '5').done
         if(flag){
-          updateTypeProject(3)
-          // 生成招标文件
-          api.getGenerateTender({txbId: projects.value.id}).then((res: any) => {
-            if ((res as any).code === 200) {
-              ElMessage.success("标书文件生成成功")
+          if(soureProjects.value.c_history_tender){// 有历史标书
+            if(soureProjects.value.c_history_tender.indexOf(".docx") != -1){// 历史标书是正常的docx文件
+              // 生成招标文件
+              api.getGenerateTender({txbId: projects.value.id}).then((res: any) => {
+                if ((res as any).code === 200) {
+                  updateTypeProject(3)
+                  ElMessage.success("标书文件生成成功")
+                  bidFile.value.isFileFlag = false
+                  sessionStorage.setItem('isFileFlag', 'false')
+                }else{
+                  ElMessage.error((res as any).content || "标书文件生成失败")
+                }
+              })
+
             }else{
-              ElMessage.error((res as any).msg || "标书文件生成失败")
+              ElMessage.error("请先确认上传参考标书")
+              bidFile.value.isFileFlag = false
+              sessionStorage.setItem('isFileFlag', 'false')
             }
-          })
+          }else{
+            // 无历史标书
+            ElMessage.error("请先确认上传参考标书")
+            bidFile.value.isFileFlag = false
+            sessionStorage.setItem('isFileFlag', 'false')
+          }
+          
           
         }else{
           ElMessage.error("请先确认标书需求拆解与生成")
@@ -1609,12 +1660,54 @@ const handleBidFileSubmit = () => {
   updaterwlcqdProject(tempTask.value)
 }
 const dialogFormFileVisible = ref(false)
+const dialogFormBidFileVisible = ref(false)
 const FileForm = ref<RuleForm>({})
 const fileList = ref([])
 const handleCQFileChange = (file: any) => {
   FileForm.value.name = file.name
   FileForm.value.file = file
 }
+const BidFileForm = ref<RuleForm>({})
+const bidFileList = ref([])
+const handleBidFileChange = (file: any) => {
+  BidFileForm.value.name = file.name
+  BidFileForm.value.file = file
+}
+
+const submitBidFileForm = () => {
+    console.log(BidFileForm.value)
+    const columnId = CONFIG.COLUMNIDARR[0]
+    const params: any = {
+      columnId: columnId,
+      contentId: soureProjects.value.id,
+      paraName: 'c_history_tender',
+      type: 3,
+      file: BidFileForm.value.file.raw
+    }
+    api.uploadFile(params).then((res: any) => {
+      if((res as any).code === 200){
+        const content = res.content
+        const params: any = {
+            columnId: CONFIG.COLUMNIDARR[0],
+            modelId: CONFIG.MODELIDARR[7],
+            content: JSON.stringify({
+              id: soureProjects.value.id,
+              c_history_tender: content,
+            }),
+          }
+         api.updateContent(params).then((res: any) => {
+          if((res as any).code === 200){
+            dialogFormBidFileVisible.value = false
+            bidFile.value.isFileFlag = true
+            sessionStorage.setItem('isFileFlag', 'true')
+          }
+         })
+      }else{
+        ElMessage.error('上传失败')
+      }
+    })
+}
+
 const submitFileForm = (file:any) => {
   const columnId = CONFIG.COLUMNIDARR[10]
   const modelId = CONFIG.MODELIDARR[5]
@@ -1785,9 +1878,10 @@ const getFIlePath = async (params: any) => {
 
 
 const bidFile = ref({
-  flag: sessionStorage.getItem('bidFileFlag') == 'true' ? true : false,
+  flag: sessionStorage.getItem('bidFileFlag') == 'true' ? true : false, //是否获取到标书
   swb:false,
   jsb:false,
+  isFileFlag: sessionStorage.getItem('isFileFlag') == 'true' ? true : false, //是否历史标书
 })
 const getBidFile = async () => {
   const loading = ElLoading.service({