BidResult.vue 30 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063
  1. <template>
  2. <div class="bid-result-page">
  3. <div class="page-tabs">
  4. <span
  5. v-for="tab in tabs"
  6. :key="tab.value"
  7. :class="['page-tab', { active: activeTab === tab.value }]"
  8. @click="handleTabClick(tab.value)"
  9. >
  10. {{ tab.label }}
  11. </span>
  12. <div class="btn-right">
  13. <el-button type="primary" class="btn enter-btn" @click="handleBidRegistration">中标登记</el-button>
  14. </div>
  15. </div>
  16. <div class="bid-container">
  17. <div v-if="activeTab === 'ongoing'" class="pool-stats">
  18. <div
  19. v-for="stat in bidStats"
  20. :key="stat.label"
  21. class="pool-stat"
  22. :class="{ highlight: stat.highlight }"
  23. @click="getTabName(stat)"
  24. >
  25. <span>{{ stat.label }}</span>
  26. <b>{{ stat.value }}</b>
  27. </div>
  28. </div>
  29. <div v-if="activeTab === 'ongoing'" class="monitor-list">
  30. <div class="panel-card">
  31. <div class="card-header">
  32. <span class="card-title">监控中的投标</span>
  33. </div>
  34. <div class="empty-state" v-if="total === 0">
  35. <span class="empty-icon">🔍</span>
  36. <p>暂无数据</p>
  37. </div>
  38. <div v-else class="card-body">
  39. <div
  40. v-for="item in allprojects"
  41. :key="item.id"
  42. :class="['monitor-card',{ active: selectedBid === item.id }, { 'won': item.c_sfzb == true }]"
  43. >
  44. <div class="monitor-head">
  45. <strong>{{ item.title }}</strong>
  46. <span :class="['tag', item.c_sfzb == false ? 'tag-danger':'']" style="float: right;">{{ item.c_sfzb == true ? '已中标' : '监控中' }}</span>
  47. </div>
  48. <div class="monitor-meta">
  49. <!-- <span>招标编号 {{ item.c_zbbh }}</span> -->
  50. <span>开标 {{ moment(item.c_kbsj).format('YYYY-MM-DD HH:mm') }}</span>
  51. <span>报价 {{ item.c_tbbj || '未报价' }}</span>
  52. <span v-if="item.days > 0">距开标 {{ item.days || '0' }} 天</span>
  53. </div>
  54. <div>
  55. <span class="monitor-status">
  56. <span class="pulse-dot"></span>
  57. 最近检查 {{ moment(item.update_time).format('YYYY-MM-DD HH:mm') }} · 已检查 {{ item.jccs || '0' }} 次
  58. </span>
  59. </div>
  60. <div v-if="item.c_sfzb == true">
  61. <strong style="color: var(--primary);">
  62. 中标
  63. </strong>
  64. <el-button type="primary" class="btn enter-btn" size="small" style="float: right;cursor: pointer;" @click="handleOpenClick(item)">查看中标详情</el-button>
  65. <!-- <span style="color: var(--primary);"> · 得分 92.5 · 排名 1</span> -->
  66. </div>
  67. </div>
  68. <!-- 分页控件 -->
  69. <div class="pagination-section">
  70. <span class="total-text">共{{ total }}个</span>
  71. <el-pagination
  72. layout="prev, pager, next"
  73. :total="total"
  74. :current-page="page"
  75. :page-size="pageSize"
  76. @current-change="handleCurrentChange"
  77. >
  78. </el-pagination>
  79. </div>
  80. </div>
  81. </div>
  82. </div>
  83. <div v-else>
  84. <div style="margin-bottom: 16px;">
  85. <el-select v-model="jkObject" placeholder="请选择项目名称" @change="handleFinishProjectChange" value-key="id">
  86. <el-option v-for="item in finishProjects" :selected="item.id == selectedBid" :key="item.id" :label="item.c_xmmc" :value="item" />
  87. </el-select>
  88. </div>
  89. <div class="empty-state" v-if="finishProjects.length < 1">
  90. <span class="empty-icon">🔍</span>
  91. <p>暂无数据</p>
  92. </div>
  93. <div v-if="finishProjects.length > 0">
  94. <div class="result-banner" :class="{ lost: !showWin }">
  95. <div>
  96. <span style="font-size: large;font-weight: bold;">{{ showWin ? '🎉 中标' : '😔 未中标' }} </span> · <span>{{jkObject.c_xmmc}}</span>
  97. <!-- {{jkObject.project?.c_winning_suppliers || '未中标'}} -->
  98. <p>{{ showWin ? '恭喜您的标书在本次招标中脱颖而出!' : '请再接再厉,下次一定成功!' }} · 结果发布:{{ jkObject.project?.c_publish_time || '2025-07-11' }} · 中标单位:上海申勤物业管理服务有限公司</p>
  99. </div>
  100. </div>
  101. <!-- <div class="card">
  102. <div class="card-head">中标结果详情</div>
  103. <div class="card-body">
  104. <dl class="info info-wide">
  105. <dt>项目名称</dt>
  106. <dd>XX商业综合体物业管理服务</dd>
  107. <dt>中标金额</dt>
  108. <dd>¥5,000,000 /年</dd>
  109. <dt>服务期限</dt>
  110. <dd>2026年7月1日 - 2029年6月30日</dd>
  111. <dt>开标时间</dt>
  112. <dd>2026年6月20日 14:00</dd>
  113. </dl>
  114. </div>
  115. </div> -->
  116. <div class="card">
  117. <div class="card-head">中标结果确认</div>
  118. <div class="card-body markdown-container">
  119. <!-- <div class="markdown-content" v-html="markdownContent"></div> -->
  120. <div class="markdown-content" v-if="jkObject.c_gljg" v-html="markdownContent"></div>
  121. <div class="markdown-content"v-else>人工处理结果:<span style="cursor: pointer;color: var(--primary);" @click="handleOpenPageClick()">点击查看中标结果详情</span></div>
  122. </div>
  123. <div style="padding: 20px;" >
  124. <el-button type="primary" class="btn enter-btn" @click="handleConfirmBidPush" :disabled="jkObject.c_sfts">确认中标 · 一键推送</el-button>
  125. <el-button type="primary" class="btn enter-btn" @click="openFileForm('1')">上传合同</el-button>
  126. <el-button type="primary" class="btn enter-btn" @click="openFileForm('2')">上传投标文件</el-button>
  127. <el-button type="primary" class="btn enter-btn" @click="openFileForm('3')">上传采购资料</el-button>
  128. </div>
  129. </div>
  130. <div class="panel-card" style="margin-bottom: 16px;">
  131. <div class="card-header">
  132. <span class="card-title">推送预览</span>
  133. <span :class="['tag', pushStartsOptions.done ? 'tag-success': 'tag-warn']" style="float: right;">{{pushStartsOptions.done ? '已完成' : '待推送'}}</span>
  134. </div>
  135. <div class="card-body">
  136. <div class="push-grid">
  137. <div
  138. v-for="item in pushStartsOptions.data"
  139. :key="item.name"
  140. :class="['push-item', { done: pushStartsOptions.done }]"
  141. >
  142. <strong :style="{'color': pushStartsOptions.done ? '' : 'var(--text3)'}">{{ item.name }}</strong>
  143. <span :style="{'color': pushStartsOptions.done ? '' : 'var(--text3)'}">{{ item.desc }}</span><br/>
  144. <span v-if="pushStartsOptions.done" style="color: var(--text3);">{{ moment(item.time).format('YYYY-MM-DD HH:mm') }}</span>
  145. </div>
  146. </div>
  147. </div>
  148. </div>
  149. <div class="card">
  150. <div class="card-head">中标流程</div>
  151. <div class="card-body">
  152. <ul class="timeline">
  153. <li class="done" v-for="item in jkObject.c_lcsjx">{{moment(item.create_time).format('YYYY-MM-DD')}} · {{item.desc}} · {{item.username}}</li>
  154. </ul>
  155. </div>
  156. </div>
  157. </div>
  158. </div>
  159. <div v-if="addFormVisible" >
  160. <addContent :flag="addFormVisible" :areaProject="areaProject" :glArea="glArea" :close="closeAddForm"/>
  161. </div>
  162. <div v-if="addProjectFormVisible" >
  163. <addProject :flag="addProjectFormVisible" :glArea="glArea" :projectData="jkObject" :close="closeAddProjectForm"/>
  164. </div>
  165. <el-dialog v-model="dialogFormFileVisible" draggable title="提示信息" width="300">
  166. <el-input v-model="FileForm.name" autocomplete="off" placeholder="请输入文件名称"/>
  167. <div style="margin-top: 10px;">
  168. <el-upload
  169. class="upload-demo"
  170. action=""
  171. style="width: 100%;"
  172. :auto-upload="false"
  173. :on-change="handleFileChange"
  174. :file-list="fileList"
  175. limit="1"
  176. >
  177. <el-button size="small" type="primary" style="width: 268px;">点击上传</el-button>
  178. <!-- <template #tip>
  179. <div class="el-upload__tip">只能上传 jpg/png 文件,且不超过 500kb</div>
  180. </template> -->
  181. </el-upload>
  182. </div>
  183. <template #footer>
  184. <div class="dialog-footer">
  185. <el-button @click="dialogFormFileVisible = false" class="btn">取消</el-button>
  186. <el-button type="primary" class="btn enter-btn" @click="submitFileForm()">
  187. 确认
  188. </el-button>
  189. </div>
  190. </template>
  191. </el-dialog>
  192. <el-dialog
  193. v-model="dialogVisible"
  194. title="提示信息"
  195. width="300"
  196. align-center
  197. draggable
  198. show-close="false"
  199. >
  200. <span>本项目已推送至市场部负责人,后续将由市场部统一分发至各部门负责人,相关后续流程敬请期待。</span>
  201. <template #footer>
  202. <div class="dialog-footer">
  203. <el-button type="primary" @click="handleConfirm">
  204. 确认
  205. </el-button>
  206. </div>
  207. </template>
  208. </el-dialog>
  209. </div>
  210. </div>
  211. </template>
  212. <script setup lang="ts">
  213. import { ref,onMounted } from 'vue'
  214. import moment from 'moment'
  215. import api from '@/api/common'
  216. import CONFIG from '@/utils/config'
  217. // import type { FormInstance } from 'element-plus'
  218. import addContent from '@/components/bidResult/addContent.vue'
  219. import addProject from '@/components/bidResult/addProject.vue'
  220. import MarkdownIt from 'markdown-it'
  221. const activeTab = ref('ongoing')
  222. const selectedBid = ref('')
  223. // const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
  224. // const userName = userInfo?.username =='user_xtw' ? '徐天炜' : userInfo?.username || ''
  225. const showWin = ref(true)
  226. const markdownContent = ref('')
  227. interface jgForm {
  228. [key: string]: any
  229. }
  230. const jkObject = ref<jgForm>({})
  231. const dialogFormFileVisible = ref(false)
  232. const FileForm = ref<jgForm>({})
  233. const fileList = ref([])
  234. const pageSize = ref(10)
  235. const page = ref(1)
  236. const total = ref(0)
  237. const md = ref<MarkdownIt>()
  238. const dialogVisible = ref(false)
  239. const finishTreeProjects = ref<any[]>([])
  240. const glArea = ref([{} as TreeItem])
  241. const areaProject = ref([{} as TreeItem])
  242. interface TreeItem {
  243. id: string | number
  244. value: string
  245. label: string
  246. children: TreeItem[],
  247. [key: string]: any
  248. }
  249. onMounted(() => {
  250. initData()
  251. getFinishProjects()
  252. md.value = new MarkdownIt({
  253. html: true,
  254. linkify: true,
  255. typographer: true
  256. })
  257. activeTab.value = sessionStorage.getItem('activeTab') || 'ongoing'
  258. selectedBid.value = sessionStorage.getItem('jkId') || ''
  259. // openMD('http://localhost:5173/public/static/data/index.html')
  260. })
  261. const openMD = async (url: string) => {
  262. try {
  263. let proxyUrl = url
  264. const response = await fetch(proxyUrl)
  265. if (!response.ok) {
  266. throw new Error(`HTTP error! status: ${response.status}`)
  267. }
  268. let specDoc = await response.text()
  269. let html = specDoc
  270. .replace(/<html[^>]*>/gi, '')
  271. .replace(/<\/html>/gi, '')
  272. .replace(/<body[^>]*>/gi, '')
  273. .replace(/<\/body>/gi, '')
  274. .replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '')
  275. .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
  276. if (md.value) {
  277. const result = md.value.render(html)
  278. if (typeof result === 'string') {
  279. markdownContent.value = result
  280. } else {
  281. try {
  282. markdownContent.value = JSON.stringify(result)
  283. } catch (e) {
  284. markdownContent.value = '<p>文档内容解析异常</p>'
  285. }
  286. }
  287. } else {
  288. markdownContent.value = html
  289. }
  290. } catch (error) {
  291. markdownContent.value = '<p>加载文档失败,请检查网络连接或文件路径</p>'
  292. }
  293. }
  294. const tabs = ref([
  295. { label: '开标监测', value: 'ongoing' },
  296. { label: '中标结果', value: 'won' },
  297. ])
  298. const bidStats = ref([
  299. { label: '全部', value: '0', highlight: true },
  300. { label: '监控中', value: '0', highlight: false },
  301. { label: '已中标', value: '0', highlight: false }
  302. ])
  303. const addFormVisible = ref(false)
  304. const addProjectFormVisible = ref(false)
  305. interface ProjectItem {
  306. id: string | number
  307. title: string
  308. [key: string]: any
  309. }
  310. const allprojects = ref<ProjectItem[]>([])
  311. const pendingProjects = ref<ProjectItem[]>([])
  312. const monitorProjects = ref<ProjectItem[]>([])
  313. const finishProjects = ref<ProjectItem[]>([])
  314. const pushStartsOptions = ref(
  315. {
  316. done: false,
  317. data:[
  318. { name: '运营部',desc:"前介看板新增卡片",time:"2026-06-08 10:40" },
  319. { name: '人力资源',desc:"招聘联动 · 缺口草稿",time:"2026-06-08 10:40" },
  320. { name: '财务部',desc:"收入科目 · 开票节点",time:"2026-06-08 10:40" },
  321. { name: '市场部',desc:"定编方案确认",time:"2026-06-08 10:40" },
  322. { name: '采购部',desc:"采购需求确认",time:"2026-06-08 10:40" },
  323. ]
  324. }
  325. )
  326. const getTabName = (stat: { label: string; highlight: boolean }) => {
  327. stat.highlight = true
  328. if(stat.label == '全部'){
  329. getProjects("","");//初始所有列表
  330. }else if(stat.label == '监控中'){
  331. getProjects("","1");//初始所有列表
  332. }else if(stat.label == '已中标'){
  333. getProjects("","2");//初始所有列表
  334. }
  335. bidStats.value.forEach(item => {
  336. if (item.label !== stat.label) {
  337. item.highlight = false
  338. }
  339. })
  340. }
  341. const handleTabClick = (tab: string) => {
  342. activeTab.value = tab
  343. sessionStorage.setItem('activeTab',tab)
  344. updateStatus()
  345. if(tab == 'won' && jkObject.value.project){
  346. openMD(jkObject.value.project.c_content_html_path || '')
  347. }
  348. }
  349. const closeAddForm = () => {
  350. addFormVisible.value = false
  351. initData()
  352. }
  353. const closeAddProjectForm = () => {
  354. addProjectFormVisible.value = false
  355. initData()
  356. }
  357. const handleBidRegistration = async () => {
  358. addFormVisible.value = true
  359. }
  360. const handleFinishProjectChange = (val: any) => {
  361. if (val && typeof val === 'object' && val.project) {
  362. jkObject.value = val
  363. updateStatus()
  364. // jkObject.value.c_lcsjx = JSON.parse(val.c_lcsjx || '[]')
  365. jkObject.value.c_lcsjx = val.c_lcsjx || '[]'
  366. openMD(jkObject.value.project.c_content_html_path || '')
  367. }
  368. }
  369. const handleOpenClick = (item: ProjectItem) => {
  370. if(item.c_sfzb == true){
  371. sessionStorage.setItem('activeTab', 'won')
  372. activeTab.value = 'won'
  373. jkObject.value = item
  374. jkObject.value.c_lcsjx = JSON.parse(item.c_lcsjx || '[]')
  375. selectedBid.value = item.id.toString()
  376. }
  377. }
  378. const handleOpenPageClick = () => {
  379. window.open(jkObject.value.c_jgjk_url)
  380. }
  381. const initData = () => {
  382. getProjects("","");//初始所有列表
  383. getProjects("0","");//初始统计所有数据
  384. getProjects("1","1");//初始统计已监控数据
  385. getProjects("2","2");//初始统计已中标数据
  386. }
  387. const handleCurrentChange = (val: number) => {
  388. page.value = val
  389. bidStats.value.forEach(item => {
  390. if(item.highlight == true){
  391. if(item.label == '全部'){
  392. getProjects("","");//初始所有列表
  393. }else if(item.label == '监控中'){
  394. getProjects("","1");//初始所有列表
  395. }else if(item.label == '已中标'){
  396. getProjects("","2");//初始所有列表
  397. }
  398. }
  399. })
  400. }
  401. const getProjects = async (label:string,type:string) => {
  402. const params: any = {
  403. columnId: CONFIG.COLUMNIDARR[6],
  404. orderBy: JSON.stringify([{ field: "update_time", orderByType: 2 }]),
  405. states: '0,1,2,3,4',
  406. pageSize: pageSize.value,
  407. page: (page.value-1)
  408. }
  409. if (type) {
  410. let flag = false
  411. if(type == '2'){
  412. flag = true
  413. }
  414. params.search = JSON.stringify([
  415. ...(params.search ? JSON.parse(params.search) : []),
  416. {
  417. field: "c_sfzb",
  418. searchType: 1,
  419. content: { value: flag },
  420. },
  421. ]);
  422. }
  423. const res = await getDmsDataList(params)
  424. if ((res as any).code === 200) {
  425. if(label == ''){
  426. allprojects.value = (res as any).content.data.map((item: any) => ({
  427. ...item,
  428. ...calculateRemainingDays(moment().format('YYYY-MM-DD HH:mm'),item.c_kbsj)
  429. }))
  430. total.value = (res as any).content.count
  431. }else if(label == '0'){
  432. const result = getCountNumber(res as any)
  433. pendingProjects.value = result.data
  434. bidStats.value.filter(item => item.label == '全部')[0].value = result.count
  435. }else if(label == '1'){
  436. const result = getCountNumber(res as any)
  437. monitorProjects.value = result.data
  438. bidStats.value.filter(item => item.label == '监控中')[0].value = result.count
  439. }else if(label == '2'){
  440. const result = getCountNumber(res as any)
  441. const promises = result.data.map(async (item: any) => {
  442. let project = {}
  443. if(item.c_gljg){
  444. project = await getJGProjects(item.c_gljg)
  445. }
  446. return {
  447. ...item,
  448. c_lcsjx:JSON.parse(item.c_lcsjx || '[]'),
  449. project:project || {}
  450. }
  451. })
  452. finishProjects.value = await Promise.all(promises)
  453. bidStats.value.filter(item => item.label == '已中标')[0].value = result.count
  454. if(selectedBid.value){
  455. jkObject.value = finishProjects.value.find(item => item.id == selectedBid.value) || {}
  456. if(jkObject.value.project){
  457. openMD(jkObject.value.project.c_content_html_path || '')
  458. }
  459. }else{
  460. jkObject.value = finishProjects.value[0] || {}
  461. }
  462. updateStatus()
  463. }
  464. }else{
  465. if(label == ''){
  466. allprojects.value = []
  467. total.value = 0
  468. }
  469. }
  470. }
  471. const updateStatus = () => {
  472. pushStartsOptions.value.done = jkObject.value.c_sfts == true ? true : false
  473. pushStartsOptions.value.data.forEach(item => {
  474. item.time = jkObject.value.c_tssj || ''
  475. })
  476. }
  477. const getJGProjects = async (id:string) => {
  478. const params: any = {
  479. columnId: CONFIG.COLUMNIDARR[7],
  480. orderBy: JSON.stringify([{ field: "update_time", orderByType: 2 }]),
  481. states: '0,1,2,3,4',
  482. pageSize: pageSize.value,
  483. page: (page.value-1)
  484. }
  485. if (id) {
  486. params.search = JSON.stringify([
  487. ...(params.search ? JSON.parse(params.search) : []),
  488. {
  489. field: "id",
  490. searchType: 1,
  491. content: { value: id },
  492. },
  493. ]);
  494. }
  495. const res = await getDmsDataList(params)
  496. return (res as any).content.data[0]
  497. }
  498. const getCountNumber = (res:any) =>{
  499. const result = {
  500. data:(res as any).content.data.map((item: any) => ({
  501. ...item,
  502. ...calculateRemainingDays(moment().format('YYYY-MM-DD HH:mm'),item.kbrq)
  503. })),
  504. count:(res as any).content.count.toString()
  505. }
  506. return result
  507. }
  508. const getDmsDataList = async (params: any) => {
  509. return await api.getDmsDataList(params)
  510. }
  511. const calculateRemainingDays = (st: string, et: string): { days: string;} => {
  512. if (!et) return { days: '' }
  513. const startDate = moment(moment(st).format('YYYY-MM-DD HH:mm'), 'YYYY-MM-DD HH:mm')
  514. const expireDate = moment(moment(et).format('YYYY-MM-DD HH:mm'), 'YYYY-MM-DD HH:mm')
  515. if (!startDate.isValid() || !expireDate.isValid()) {
  516. return { days: ''}
  517. }
  518. const today = moment().startOf('day')
  519. const diffDays = expireDate.diff(today, 'days')
  520. return { days: diffDays.toString() };
  521. }
  522. const handleConfirmBidPush = async () => {
  523. // let arr = jkObject.value.c_lcsjx ? jkObject.value.c_lcsjx : []
  524. // arr.push({
  525. // create_time: moment().format('YYYY-MM-DD HH:mm'),
  526. // desc:'市场部确认',
  527. // username:userName,
  528. // userid:userInfo?.id || ''
  529. // },{
  530. // create_time: moment().format('YYYY-MM-DD HH:mm'),
  531. // desc:'已推送运营 / 人力 / 财务 / 采购',
  532. // username:userName,
  533. // userid:userInfo?.id || ''
  534. // })
  535. // updateAudit(jkObject.value,4)
  536. // updateKBJKProject(jkObject.value,arr)
  537. // console.log(jkObject.value)
  538. if(jkObject.value.c_type == "续签"){
  539. updateProject(jkObject.value,true)
  540. dialogVisible.value = true
  541. initData()
  542. }else{//续标
  543. addProjectFormVisible.value = true
  544. }
  545. }
  546. const updateProject = async (item: any,flag:boolean) => {
  547. const params: any = {
  548. columnId: CONFIG.COLUMNIDARR[6],
  549. modelId: CONFIG.MODELIDARR[3],
  550. content: JSON.stringify({
  551. id: item.id,
  552. c_sfts: flag,
  553. c_tssj: moment().format('YYYY-MM-DD HH:mm:ss'),
  554. }),
  555. }
  556. await api.updateContent(params)
  557. }
  558. const handleConfirm = () => {
  559. dialogVisible.value = false
  560. }
  561. // const updateKBJKProject = async (item: any,arr:any[]) => {
  562. // const params: any = {
  563. // columnId: CONFIG.COLUMNIDARR[6],
  564. // modelId: CONFIG.MODELIDARR[3],
  565. // content: JSON.stringify({
  566. // id: item.id,
  567. // c_lcsjx: arr,
  568. // }),
  569. // }
  570. // const res = await api.updateContent(params)
  571. // if ((res as any).code === 200) {
  572. // // ElMessage.success('添加成功')
  573. // initData()
  574. // }else{
  575. // // ElMessage.error('添加失败')
  576. // }
  577. // }
  578. // const updateAudit = async (item: any,type:number) => {
  579. // const params: any = {
  580. // id: item.id,
  581. // columnId: CONFIG.COLUMNIDARR[6],
  582. // state: type,
  583. // }
  584. // const res = await getUpdateAudit(params)
  585. // if ((res as any).code === 200) {
  586. // // ElMessage.success('操作成功')
  587. // }else{
  588. // // ElMessage.error('操作失败')
  589. // }
  590. // }
  591. // const getUpdateAudit = async (params: any) => {
  592. // return await api.updateAudit(params)
  593. // }
  594. const handleFileChange = (file: any) => {
  595. FileForm.value = file
  596. }
  597. const submitFileForm = async () => {
  598. dialogFormFileVisible.value = false
  599. }
  600. const openFileForm = (type: string) => {
  601. FileForm.value.type = type
  602. dialogFormFileVisible.value = true
  603. }
  604. const getFinishProjects = async () => {
  605. const params: any = {
  606. columnId: CONFIG.COLUMNIDARR[0], //原项目数据
  607. orderBy: JSON.stringify([{ field: "c_zgxmhtdqrq", orderByType: 1 }]),
  608. states: '0',// 只看草稿箱
  609. pageSize: 9999,
  610. page: 0
  611. }
  612. await api.getDmsDataList(params).then(res => {
  613. if ((res as any).code === 200) {
  614. let finishProjects = (res as any).content.data
  615. finishTreeProjects.value = buildTree(finishProjects)
  616. getGLXQData()
  617. }
  618. })
  619. }
  620. const getGLXQData = async () => {
  621. const itemList = await getTypeProjects(CONFIG.COLUMNIDARR[9]) // 管理区域
  622. glArea.value = buildAreaTree(itemList.data || [])
  623. areaProject.value = itemList.data.filter((item: any) => item?.c_district_name && item?.c_district_code?.startsWith('310')) // 过滤空、null、undefined、''
  624. .map((item: any) => ({
  625. label: item.c_district_name,
  626. value: item.c_district_name,
  627. children: finishTreeProjects.value.filter((item2: any) => item.c_district_name.includes(item2.c_sq))
  628. }))
  629. const uniqueTree = Array.from(
  630. new Map(areaProject.value.map(item => [item.value, item])).values()
  631. )
  632. areaProject.value = uniqueTree
  633. }
  634. const getTypeProjects = async (columnId:number) => {
  635. const params: any = {
  636. columnId: columnId,
  637. orderBy: JSON.stringify([{ field: "update_time", orderByType: 2 }]),
  638. states: '0,1,2,3,4',
  639. pageSize: 9999,
  640. page: 0
  641. }
  642. const res = await getDmsDataList(params)
  643. return (res as any).content
  644. }
  645. const buildTree = (data: any[]): any[] => {
  646. const map = new Map()
  647. const tree: any[] = []
  648. data.forEach((item: any) => {
  649. map.set(item.c_guid, {
  650. ...item,
  651. value: item.id,
  652. label: item.c_xmmc,
  653. children: []
  654. })
  655. })
  656. data.forEach((item: any) => {
  657. const node = map.get(item.c_guid)
  658. if (item.c_father_code && map.has(item.c_father_code)) {
  659. // map.get(item.c_father_code).children.push(node)
  660. } else {
  661. tree.push(node)
  662. }
  663. })
  664. return tree
  665. }
  666. const buildAreaTree = (list: any[]) => {
  667. const districtMap = new Map<string, TreeItem>()
  668. const filteredList = list.filter(item => item?.c_district_code?.startsWith('3101'))
  669. for (const item of filteredList) {
  670. const districtCode = item.c_district_code
  671. const districtName = item.c_district_name
  672. if (!districtCode || !districtName) continue
  673. let districtNode = districtMap.get(districtCode)
  674. if (!districtNode) {
  675. districtNode = {
  676. id: districtCode,
  677. label: districtName,
  678. value: districtCode,
  679. children: []
  680. }
  681. districtMap.set(districtCode, districtNode)
  682. }
  683. const partCode = item.c_part_code
  684. const partName = item.c_part_name
  685. if (!partCode || !partName) continue
  686. const partMap = new Map<string, TreeItem>()
  687. for (const child of districtNode.children) {
  688. partMap.set(child.value, child)
  689. }
  690. let partNode = partMap.get(partCode)
  691. if (!partNode) {
  692. partNode = {
  693. id: partCode,
  694. label: partName,
  695. value: partCode,
  696. children: []
  697. }
  698. districtNode.children.push(partNode)
  699. }
  700. }
  701. return Array.from(districtMap.values())
  702. }
  703. </script>
  704. <style scoped>
  705. .bid-result-page {
  706. width: 100%;
  707. }
  708. .bid-container{
  709. height: calc(100vh - 180px);
  710. overflow-y: auto;
  711. }
  712. .markdown-container {
  713. max-height: 400px;
  714. overflow-y: auto;
  715. padding: 16px;
  716. }
  717. .markdown-content {
  718. font-size: 14px;
  719. line-height: 1.8;
  720. color: #1a2b3c;
  721. }
  722. :root {
  723. th, td {
  724. text-align: left;
  725. padding: 0px !important;
  726. border-bottom: 1px solid #e8ecef;
  727. }
  728. }
  729. .page-tabs {
  730. display: flex;
  731. gap: 0;
  732. margin-bottom: 16px;
  733. border-bottom: 1px solid #e8ecef;
  734. justify-content: space-between;
  735. align-items: center;
  736. }
  737. .btn-right {
  738. margin-left: auto;
  739. }
  740. .page-tab {
  741. padding: 10px 18px;
  742. border: none;
  743. background: none;
  744. font-family: inherit;
  745. font-size: 13px;
  746. color: #5c6f82;
  747. cursor: pointer;
  748. border-bottom: 2px solid transparent;
  749. margin-bottom: -1px;
  750. }
  751. .page-tab.active {
  752. color: #0d9488;
  753. border-bottom-color: #0d9488;
  754. font-weight: 600;
  755. }
  756. .monitor-list {
  757. display: flex;
  758. flex-direction: column;
  759. gap: 12px;
  760. }
  761. .monitor-card {
  762. border: 1px solid #e8ecef;
  763. border-radius: 10px;
  764. padding: 14px 16px;
  765. background: #fff;
  766. margin-bottom: 16px;
  767. transition: border-color 0.15s;
  768. &:hover{
  769. border-color: #0d9488;
  770. box-shadow: 0 0 0 3px #e6f6f4;
  771. }
  772. }
  773. .monitor-card.active {
  774. border-color: #0d9488;
  775. box-shadow: 0 0 0 3px #e6f6f4;
  776. }
  777. .monitor-card.won {
  778. border-color: var(--info);
  779. background: var(--info-soft);
  780. }
  781. .monitor-head {
  782. display: flex;
  783. justify-content: space-between;
  784. align-items: flex-start;
  785. gap: 12px;
  786. flex-wrap: wrap;
  787. margin-bottom: 10px;
  788. }
  789. .monitor-meta {
  790. font-size: 12px;
  791. color: var(--text3);
  792. display: flex;
  793. flex-wrap: wrap;
  794. gap: 14px;
  795. margin-bottom: 10px;
  796. }
  797. .monitor-status {
  798. display: inline-flex;
  799. align-items: center;
  800. gap: 6px;
  801. font-size: 12px;
  802. margin-bottom: 10px;
  803. }
  804. .monitor-status .pulse-dot {
  805. width: 8px;
  806. height: 8px;
  807. border-radius: 50%;
  808. background: var(--primary);
  809. animation: pulse 1.5s infinite;
  810. }
  811. @keyframes pulse {
  812. 0%, 100% { opacity: 1; }
  813. 50% { opacity: 0.4; }
  814. }
  815. .push-grid {
  816. display: grid;
  817. grid-template-columns: repeat(5, 1fr);
  818. gap: 10px;
  819. margin-top: 14px;
  820. }
  821. .push-item {
  822. border: 1px solid #e8ecef;
  823. border-radius: 8px;
  824. padding: 12px;
  825. text-align: center;
  826. font-size: 12px;
  827. background: #fafbfc;
  828. }
  829. .push-item.done {
  830. border-color: var(--info);
  831. background: var(--info-soft);
  832. }
  833. .push-item strong {
  834. display: block;
  835. font-size: 13px;
  836. margin-bottom: 4px;
  837. }
  838. .result-banner {
  839. background: linear-gradient(90deg, #ecfdf5, #fff);
  840. border: 1px solid #a7f3d0;
  841. border-left: 4px solid var(--info);
  842. padding: 14px 16px;
  843. border-radius: 10px;
  844. margin-bottom: 16px;
  845. }
  846. .result-banner.lost {
  847. background: linear-gradient(90deg, #fef2f2, #fff);
  848. border-color: var(--danger);
  849. border-left-color: var(--danger);
  850. }
  851. .result-banner h3 {
  852. font-size: 16px;
  853. margin-bottom: 4px;
  854. }
  855. .result-banner p {
  856. font-size: 13px;
  857. color: #5c6f82;
  858. }
  859. .card {
  860. background: #fff;
  861. border: 1px solid #e8ecef;
  862. border-radius: 10px;
  863. margin-bottom: 16px;
  864. }
  865. .card-head {
  866. padding: 12px 16px;
  867. border-bottom: 1px solid #e8ecef;
  868. font-weight: 600;
  869. font-size: 14px;
  870. }
  871. .card-body {
  872. padding: 16px;
  873. }
  874. .info {
  875. display: grid;
  876. grid-template-columns: 100px 1fr;
  877. gap: 8px 12px;
  878. font-size: 13px;
  879. }
  880. .info-wide {
  881. grid-template-columns: minmax(140px, 168px) 1fr;
  882. }
  883. .info dt {
  884. color: #8a9bab;
  885. }
  886. .timeline {
  887. list-style: none;
  888. margin-top: 12px;
  889. }
  890. .timeline li {
  891. padding: 8px 0 8px 16px;
  892. border-left: 2px solid #e8ecef;
  893. margin-left: 6px;
  894. font-size: 12px;
  895. color: #5c6f82;
  896. position: relative;
  897. }
  898. .timeline li::before {
  899. content: '';
  900. position: absolute;
  901. left: -5px;
  902. top: 12px;
  903. width: 8px;
  904. height: 8px;
  905. border-radius: 50%;
  906. background: var(--primary);
  907. }
  908. .timeline li.done::before {
  909. background: var(--info);
  910. }
  911. .pool-stats {
  912. display: grid;
  913. grid-template-columns: repeat(3, 1fr);
  914. gap: 10px;
  915. margin-bottom: 16px;
  916. }
  917. .pool-stat {
  918. background: #fff;
  919. border: 1px solid #e8ecef;
  920. border-radius: 8px;
  921. padding: 12px 14px;
  922. cursor: pointer;
  923. transition: border-color 0.15s;
  924. }
  925. .pool-stat:hover {
  926. border-color: var(--primary);
  927. }
  928. .pool-stat b {
  929. display: block;
  930. font-size: 20px;
  931. margin-top: 2px;
  932. }
  933. .pool-stat span {
  934. font-size: 11px;
  935. color: var(--text3);
  936. }
  937. .pool-stat.highlight {
  938. border-color: var(--danger);
  939. background: var(--danger-soft);
  940. }
  941. .tag {
  942. font-size: 11px;
  943. padding: 2px 8px;
  944. border-radius: 10px;
  945. background: var(--info-soft);
  946. color: var(--info);
  947. }
  948. .tag-warn {
  949. background: var(--warn-soft);
  950. color: var(--warn);
  951. }
  952. .tag-danger {
  953. background: var(--danger-soft);
  954. color: var(--danger);
  955. }
  956. @media (max-width: 1100px) {
  957. .push-grid {
  958. grid-template-columns: repeat(2, 1fr);
  959. }
  960. }
  961. </style>