Browse Source

网页端:新增采招网招标信息

余思翰 2 weeks ago
parent
commit
3b135501f0
3 changed files with 455 additions and 5 deletions
  1. 1
    0
      oa-ui/package.json
  2. 9
    0
      oa-ui/src/api/oa/bid/bid.js
  3. 445
    5
      oa-ui/src/views/oa/bid/index.vue

+ 1
- 0
oa-ui/package.json View File

@@ -48,6 +48,7 @@
48 48
     "bpmnlint-loader": "^0.1.4",
49 49
     "clipboard": "2.0.8",
50 50
     "core-js": "3.25.3",
51
+    "crypto-js": "^4.2.0",
51 52
     "diagram-js": "^11.4.1",
52 53
     "echarts": "5.4.0",
53 54
     "echarts-gl": "^2.0.9",

+ 9
- 0
oa-ui/src/api/oa/bid/bid.js View File

@@ -0,0 +1,9 @@
1
+import request from '@/utils/request'
2
+
3
+export function listBid(query) {
4
+  return request({
5
+    url: '/oa/bid/list',
6
+    method: 'get',
7
+    params: query
8
+  })
9
+}

+ 445
- 5
oa-ui/src/views/oa/bid/index.vue View File

@@ -1,7 +1,447 @@
1 1
 <!--
2
- * @Author: wrh
3
- * @Date: 2026-04-08 16:57:49
4
- * @LastEditors: wrh
5
- * @LastEditTime: 2026-04-08 16:58:24
2
+ * @Author: ysh
3
+ * @Date: 2026-04-09 09:18:44
4
+ * @LastEditors: Please set LastEditors
5
+ * @LastEditTime: 2026-04-10 09:43:47
6 6
 -->
7
-<template></template>
7
+<template>
8
+  <div class="app-container">
9
+    <div class="toolbar-card mb8">
10
+      <div class="keyword-display-row">
11
+        <span class="field-label">当前关键词</span>
12
+        <el-tag type="primary" effect="plain" class="keyword-tag">{{ displayKeyword || '—' }}</el-tag>
13
+      </div>
14
+      <el-form :inline="true" size="small" class="remote-search-form" @submit.native.prevent>
15
+        <el-form-item label="修改关键词">
16
+          <el-input v-model="keywordText" placeholder="多个关键词用英文逗号分隔" clearable style="width: 620px"
17
+            @keyup.enter.native="handleServerSearch" />
18
+        </el-form-item>
19
+        <el-form-item>
20
+          <el-button type="primary" icon="el-icon-search" :loading="searchLoading" @click="handleServerSearch">检索</el-button>
21
+        </el-form-item>
22
+      </el-form>
23
+      <div class="table-filter-row">
24
+        <span class="field-label">表格筛选</span>
25
+        <el-input v-model="tableFilter" placeholder="在当前页结果中过滤标题、类型、地区等" clearable prefix-icon="el-icon-search"
26
+          style="width: 360px" />
27
+        <span v-if="tableFilterTrimmed" class="filter-hint">
28
+          本页匹配 {{ filteredTableData.length }} / {{ tableData.length }} 条
29
+        </span>
30
+      </div>
31
+    </div>
32
+    <el-alert v-if="searchWarning" :title="searchWarning" type="warning" show-icon :closable="false" class="mb8" />
33
+    <div v-if="realInfoCount !== null && realInfoCount !== ''" class="search-summary mb8">
34
+      约 <strong>{{ realInfoCount }}</strong> 条相关信息(分页每页 {{ pageSize }} 条)
35
+    </div>
36
+    <el-table v-loading="searchLoading" :data="filteredTableData" border stripe style="width: 100%" :empty-text="tableEmptyText">
37
+      <el-table-column type="index" width="50" label="#" align="center" />
38
+      <el-table-column prop="news_type_des" label="类型" width="100" show-overflow-tooltip />
39
+      <el-table-column label="标题" min-width="300">
40
+        <template slot-scope="scope">
41
+          <a :href="scope.row.news_url" target="_blank" rel="noopener noreferrer" class="title-link">{{
42
+            plainTitle(scope.row.news_title_show) }}</a>
43
+          <span v-if="scope.row.contain_kwd_fujian" class="fujian-tag">含附件</span>
44
+        </template>
45
+      </el-table-column>
46
+      <el-table-column label="金额/预算" width="130" show-overflow-tooltip>
47
+        <template slot-scope="scope">{{ formatAmount(scope.row) }}</template>
48
+      </el-table-column>
49
+      <el-table-column label="采购方式/阶段" width="130" show-overflow-tooltip>
50
+        <template slot-scope="scope">{{ formatCgfs(scope.row) }}</template>
51
+      </el-table-column>
52
+      <el-table-column label="截止时间/中标时间等" width="160" show-overflow-tooltip>
53
+        <template slot-scope="scope">{{ formatTimeCol(scope.row) }}</template>
54
+      </el-table-column>
55
+      <el-table-column prop="news_diqustr" label="地区" width="120" show-overflow-tooltip />
56
+      <el-table-column label="发布" width="120" show-overflow-tooltip>
57
+        <template slot-scope="scope">{{ showText(scope.row.news_star_time_show) }}</template>
58
+      </el-table-column>
59
+    </el-table>
60
+    <el-pagination v-if="pageTotal > 0" class="mt16" background :current-page="searchQuery.page" :page-size="pageSize"
61
+      layout="total, prev, pager, next, jumper" :total="pageTotal" @current-change="handlePageChange" />
62
+  </div>
63
+</template>
64
+
65
+<script>
66
+import axios from 'axios'
67
+import CryptoJS from 'crypto-js'
68
+import { Message, Loading } from 'element-ui'
69
+import { listBid } from '@/api/oa/bid/bid'
70
+
71
+const SEARCH_PRO_URL = 'https://interface.bidcenter.com.cn/search/GetSearchProHandler.ashx'
72
+const BIDCENTER_GUID = 'cad034bd-ad01-4dce-a4ca-1a1252f0dbf1'
73
+
74
+// 与 searchv16.js 中 variate.key / variate.aceIV 一致(采招网接口 AES)
75
+const bidcenterAes = {
76
+  key: CryptoJS.lib.WordArray.create(
77
+    [863652730, 2036741733, 1164342596, 1782662963],
78
+    16
79
+  ),
80
+  iv: CryptoJS.lib.WordArray.create(
81
+    [1719227713, 1314533489, 1397643880, 1749959510],
82
+    16
83
+  )
84
+}
85
+
86
+/** 与 jq_search POST 一致:keywords 已为 encodeURIComponent 结果,不再二次编码;token 允许空串也提交 */
87
+function serializeSearchParams(data) {
88
+  const parts = []
89
+  for (const key of Object.keys(data)) {
90
+    const v = data[key]
91
+    if (v === undefined || v === null) continue
92
+    if (v === '' && key !== 'token') continue
93
+    if (key === 'keywords') {
94
+      parts.push(`keywords=${v}`)
95
+    } else {
96
+      parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(v))}`)
97
+    }
98
+  }
99
+  return parts.join('&')
100
+}
101
+
102
+export default {
103
+  name: 'Bid',
104
+  data() {
105
+    return {
106
+      bidList: [],
107
+      tableData: [],
108
+      pageTotal: 0,
109
+      pageSize: 40,
110
+      realInfoCount: null,
111
+      searchWarning: '',
112
+      emptyText: '暂无数据',
113
+      searchLoading: false,
114
+      nextToken: '',
115
+      /** 检索输入框与接口关键词(明文) */
116
+      keywordText: '控制网,库容复核,测量技术,地形测量,安全监测,基准网复测,测量中心,土地测绘',
117
+      /** 接口返回或上次成功检索后的展示文案 */
118
+      displayKeyword: '控制网,库容复核,测量技术,地形测量,安全监测,基准网复测,测量中心,土地测绘',
119
+      /** 仅筛选当前页表格,不发请求 */
120
+      tableFilter: '',
121
+      searchQuery: {
122
+        time: 90,
123
+        type: 1,
124
+        mod: 0,
125
+        page: 1
126
+      }
127
+    }
128
+  },
129
+  computed: {
130
+    tableFilterTrimmed() {
131
+      return (this.tableFilter || '').trim()
132
+    },
133
+    filteredTableData() {
134
+      const q = this.tableFilterTrimmed.toLowerCase()
135
+      if (!q) return this.tableData
136
+      return this.tableData.filter((row) => {
137
+        const title = this.plainTitle(row.news_title_show).toLowerCase()
138
+        const type = String(row.news_type_des || '').toLowerCase()
139
+        const area = String(row.news_diqustr || '').toLowerCase()
140
+        const amount = this.formatAmount(row).toLowerCase()
141
+        const cgfs = this.formatCgfs(row).toLowerCase()
142
+        const timeCol = this.formatTimeCol(row).toLowerCase()
143
+        return (
144
+          title.includes(q) ||
145
+          type.includes(q) ||
146
+          area.includes(q) ||
147
+          amount.includes(q) ||
148
+          cgfs.includes(q) ||
149
+          timeCol.includes(q)
150
+        )
151
+      })
152
+    },
153
+    tableEmptyText() {
154
+      if (this.tableFilterTrimmed && this.tableData.length && this.filteredTableData.length === 0) {
155
+        return '当前页无匹配项,请调整筛选词'
156
+      }
157
+      return this.emptyText
158
+    }
159
+  },
160
+  created() {
161
+    this.getList()
162
+  },
163
+  methods: {
164
+    async getList() {
165
+      const queryParams = {
166
+        url:
167
+          'https://search.bidcenter.com.cn/search?keywords=%e6%8e%a7%e5%88%b6%e7%bd%91%2c%e5%ba%93%e5%ae%b9%e5%a4%8d%e6%a0%b8%2c%e6%b5%8b%e9%87%8f%e6%8a%80%e6%9c%af%2c%e5%9c%b0%e5%bd%a2%e6%b5%8b%e9%87%8f%2c%e5%ae%89%e5%85%a8%e7%9b%91%e6%b5%8b%2c%e5%9f%ba%e5%87%86%e7%bd%91%e5%a4%8d%e6%b5%8b%2c%e6%b5%8b%e9%87%8f%e4%b8%ad%e5%bf%83%2c%e5%9c%9f%e5%9c%b0%e6%b5%8b%e7%bb%98&time=90&type=1&mod=0&page=2'
168
+      }
169
+      await listBid(queryParams)
170
+      await this.fetchBidcenterSearch()
171
+    },
172
+    handleServerSearch() {
173
+      const t = (this.keywordText || '').trim()
174
+      if (!t) {
175
+        Message.warning('请输入关键词')
176
+        return
177
+      }
178
+      this.keywordText = t
179
+      this.searchQuery.page = 1
180
+      this.nextToken = ''
181
+      this.tableFilter = ''
182
+      this.fetchBidcenterSearch()
183
+    },
184
+    buildSearchPostBody() {
185
+      const body = {
186
+        from: 6137,
187
+        guid: BIDCENTER_GUID,
188
+        location: 6138,
189
+        token: '',
190
+        keywords: encodeURIComponent((this.keywordText || '').trim()),
191
+        time: this.searchQuery.time,
192
+        type: this.searchQuery.type,
193
+        mod: this.searchQuery.mod,
194
+        page: this.searchQuery.page
195
+      }
196
+      if (this.nextToken) {
197
+        body.next_token = this.nextToken
198
+      }
199
+      return body
200
+    },
201
+    async fetchBidcenterSearch() {
202
+      let loadingInstance = null
203
+      try {
204
+        loadingInstance = Loading.service({
205
+          lock: true,
206
+          text: '加载中',
207
+          spinner: 'el-icon-loading',
208
+          background: 'rgba(0, 0, 0, 0.1)'
209
+        })
210
+      } catch (e) { /* ignore */ }
211
+      this.searchLoading = true
212
+      const postData = this.buildSearchPostBody()
213
+      try {
214
+        const res = await axios({
215
+          url: SEARCH_PRO_URL,
216
+          method: 'post',
217
+          timeout: 20000,
218
+          responseType: 'text',
219
+          headers: {
220
+            'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
221
+          },
222
+          transformRequest: [(data) => serializeSearchParams(data)],
223
+          data: postData
224
+        })
225
+        this.bindSearchData(res.data)
226
+      } catch (e) {
227
+        Message({ message: '网络错误,请重试', type: 'error' })
228
+        this.tableData = []
229
+      } finally {
230
+        this.searchLoading = false
231
+        if (loadingInstance) {
232
+          loadingInstance.close()
233
+        }
234
+      }
235
+    },
236
+    /**
237
+     * 对齐 searchv16.js method.bindSearchData:解密后取 data.other2.listData 等
238
+     */
239
+    bindSearchData(resText) {
240
+      this.searchWarning = ''
241
+      const data = this.AESDecrypt(resText)
242
+      if (!data) {
243
+        Message.error('解密失败或响应为空')
244
+        this.tableData = []
245
+        this.emptyText = '解密失败'
246
+        return
247
+      }
248
+      if (Number(data.retbs) === 1) {
249
+        this.searchWarning =
250
+          '您查询的信息中可能含有敏感内容,因政策监管原因,部分信息会做屏蔽处理,如需查询更多详情,请联系客服专员。'
251
+      }
252
+      if (data.ret) {
253
+        if (!data.other2) {
254
+          this.tableData = []
255
+          this.pageTotal = 0
256
+          this.realInfoCount = 0
257
+          return
258
+        }
259
+        const resultData = data.other2
260
+        this.applyPageSearchJson(resultData.pageSearchJson)
261
+        this.nextToken = resultData.next_token || ''
262
+        const list = resultData.listData
263
+        this.tableData = Array.isArray(list) ? list : []
264
+        this.pageTotal = Number(resultData.showInfoCount) || 0
265
+        let total = resultData.realInfoCount
266
+        if (
267
+          resultData.optimize &&
268
+          Array.isArray(resultData.optimize) &&
269
+          resultData.optimize.indexOf('totalcount') > -1
270
+        ) {
271
+          total = `约${resultData.realInfoCount}`
272
+        }
273
+        this.realInfoCount = total != null ? total : this.pageTotal
274
+        this.emptyText = this.tableData.length ? '暂无数据' : '未查询到相关信息'
275
+      } else {
276
+        if (data.msg) {
277
+          Message.warning(data.msg)
278
+        }
279
+        if (data.other2 && data.other2.pageSearchJson) {
280
+          this.applyPageSearchJson(data.other2.pageSearchJson)
281
+        }
282
+        this.tableData = []
283
+        this.emptyText = '未查询到相关信息'
284
+      }
285
+    },
286
+    applyPageSearchJson(pageSearchJson) {
287
+      if (!pageSearchJson) {
288
+        const k = (this.keywordText || '').trim()
289
+        if (k) this.displayKeyword = k
290
+        return
291
+      }
292
+      try {
293
+        const jq = JSON.parse(pageSearchJson)
294
+        if (jq.zbje_min && jq.zbje_min > 0) {
295
+          jq.zbje_min = jq.zbje_min / 10000
296
+        }
297
+        if (jq.zbje_max && jq.zbje_max > 0 && jq.zbje_max < 999999999999) {
298
+          jq.zbje_max = jq.zbje_max / 10000
299
+        } else if (jq) {
300
+          jq.zbje_max = 0
301
+        }
302
+        if (jq.page != null) {
303
+          this.searchQuery.page = jq.page
304
+        }
305
+        if (jq.keywords != null && String(jq.keywords).trim() !== '') {
306
+          this.displayKeyword = String(jq.keywords).trim()
307
+        } else {
308
+          const k = (this.keywordText || '').trim()
309
+          if (k) this.displayKeyword = k
310
+        }
311
+      } catch (e) {
312
+        const k = (this.keywordText || '').trim()
313
+        if (k) this.displayKeyword = k
314
+      }
315
+    },
316
+    handlePageChange(page) {
317
+      this.searchQuery.page = page
318
+      this.nextToken = ''
319
+      this.fetchBidcenterSearch()
320
+    },
321
+    plainTitle(html) {
322
+      if (html == null || html === '') return '—'
323
+      return String(html).replace(/<[^>]+>/g, '')
324
+    },
325
+    showText(val) {
326
+      if (val == null || val === '' || val === '--') return '—'
327
+      return String(val)
328
+    },
329
+    formatAmount(row) {
330
+      if (row.news_type === 4) return this.showText(row.news_zhongbiaojine_show)
331
+      if (row.is_xiangmu) return this.showText(row.news_gczj_show)
332
+      return this.showText(row.news_zbje_show)
333
+    },
334
+    formatCgfs(row) {
335
+      if (row.is_xiangmu) return this.showText(row.news_jieduan_show)
336
+      return this.showText(row.news_cgfs)
337
+    },
338
+    formatTimeCol(row) {
339
+      const t = row.news_type
340
+      if (t === 4 || t === 6 || row.is_xiangmu) return this.showText(row.news_star_time_show)
341
+      if (t === 2) return this.showText(row.news_end_time_show)
342
+      return this.showText(row.news_end_time_show)
343
+    },
344
+    AESDecrypt(str) {
345
+      if (str == null || str === '') {
346
+        return null
347
+      }
348
+      var nContent = CryptoJS.AES.decrypt(str, bidcenterAes.key, {
349
+        iv: bidcenterAes.iv,
350
+        mode: CryptoJS.mode.CBC,
351
+        padding: CryptoJS.pad.ZeroPadding
352
+      })
353
+      if (nContent && nContent != null) {
354
+        try {
355
+          var constr = CryptoJS.enc.Utf8.stringify(nContent)
356
+          if (constr !== '') {
357
+            var data = JSON.parse(constr)
358
+            return data
359
+          }
360
+          return null
361
+        } catch (err) {
362
+          return null
363
+        }
364
+      }
365
+      return null
366
+    }
367
+  }
368
+}
369
+</script>
370
+
371
+<style lang="scss" scoped>
372
+.mb8 {
373
+  margin-bottom: 8px;
374
+}
375
+
376
+.toolbar-card {
377
+  padding: 16px 16px 8px;
378
+  background: #fff;
379
+  border-radius: 4px;
380
+  border: 1px solid #ebeef5;
381
+}
382
+
383
+.keyword-display-row {
384
+  display: flex;
385
+  align-items: center;
386
+  flex-wrap: wrap;
387
+  gap: 8px;
388
+  margin-bottom: 12px;
389
+}
390
+
391
+.field-label {
392
+  font-size: 14px;
393
+  color: #606266;
394
+  font-weight: 500;
395
+  flex-shrink: 0;
396
+}
397
+
398
+.keyword-tag {
399
+  max-width: 100%;
400
+  white-space: normal;
401
+  height: auto;
402
+  line-height: 1.5;
403
+  padding: 6px 12px;
404
+}
405
+
406
+.remote-search-form {
407
+  margin-bottom: 8px;
408
+}
409
+
410
+.table-filter-row {
411
+  display: flex;
412
+  align-items: center;
413
+  flex-wrap: wrap;
414
+  gap: 12px;
415
+  padding-top: 8px;
416
+  border-top: 1px dashed #ebeef5;
417
+}
418
+
419
+.filter-hint {
420
+  font-size: 13px;
421
+  color: #909399;
422
+}
423
+
424
+.mt16 {
425
+  margin-top: 16px;
426
+}
427
+
428
+.search-summary {
429
+  font-size: 14px;
430
+  color: #606266;
431
+}
432
+
433
+.title-link {
434
+  color: #409eff;
435
+  text-decoration: none;
436
+
437
+  &:hover {
438
+    text-decoration: underline;
439
+  }
440
+}
441
+
442
+.fujian-tag {
443
+  margin-left: 6px;
444
+  font-size: 12px;
445
+  color: #e6a23c;
446
+}
447
+</style>

Loading…
Cancel
Save