Selaa lähdekoodia

网页端:更新招标采购登录采招网会员

余思翰 1 viikko sitten
vanhempi
commit
ed88b71cc1
2 muutettua tiedostoa jossa 698 lisäystä ja 15 poistoa
  1. 226
    0
      oa-ui/src/utils/bidcenterMemberLogin.js
  2. 472
    15
      oa-ui/src/views/oa/bid/index.vue

+ 226
- 0
oa-ui/src/utils/bidcenterMemberLogin.js Näytä tiedosto

@@ -0,0 +1,226 @@
1
+
2
+
3
+const SSO_LOGIN_JSONP =
4
+  'https://sso.bidcenter.com.cn/Inc/member_login_crs_domain.ashx'
5
+
6
+const TOKEN_SESSION_KEY = 'bidcenter_search_interface_token'
7
+/** 与采招网页面一致:JSONP 登录成功但未下发 token 时,依赖浏览器携带的采招网 cookie 调搜索接口 */
8
+const JSONP_MEMBER_SESSION_KEY = 'bidcenter_member_jsonp_session'
9
+
10
+/**
11
+ * @param {string} username
12
+ * @param {string} password
13
+ * @returns {Promise<object>}
14
+ */
15
+export function loginBidcenterMemberJsonp(username, password) {
16
+  const name = (username || '').trim()
17
+  const pwd = (password || '').trim()
18
+  if (!name || !pwd) {
19
+    return Promise.reject(new Error(''))
20
+  }
21
+
22
+  const cb = `__bidcenterLogin_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`
23
+  return new Promise((resolve, reject) => {
24
+    const timer = setTimeout(() => {
25
+      cleanup()
26
+      reject(new Error(''))
27
+    }, 25000)
28
+
29
+    let script
30
+    function cleanup() {
31
+      clearTimeout(timer)
32
+      try {
33
+        delete window[cb]
34
+      } catch (e) { /* ignore */ }
35
+      if (script && script.parentNode) {
36
+        script.parentNode.removeChild(script)
37
+      }
38
+    }
39
+
40
+    window[cb] = (data) => {
41
+      cleanup()
42
+      resolve(data)
43
+    }
44
+
45
+    script = document.createElement('script')
46
+    script.onerror = () => {
47
+      cleanup()
48
+      reject(new Error(''))
49
+    }
50
+    // 与站点 $.getJSON 拼接一致:callback 为时间戳,jsoncallback 为 JSONP 函数名(见 member_login_crs_domain 调用方式)
51
+    const srctime = Date.now()
52
+    const qs = [
53
+      `name=${encodeURIComponent(name)}`,
54
+      `d=${srctime}`,
55
+      `pwd=${encodeURIComponent(pwd)}`,
56
+      'auto=0',
57
+      'run=1',
58
+      `callback=${srctime}`,
59
+      `jsoncallback=${encodeURIComponent(cb)}`
60
+    ].join('&')
61
+    script.src = `${SSO_LOGIN_JSONP}?${qs}`
62
+    document.body.appendChild(script)
63
+  })
64
+}
65
+
66
+/**
67
+ * �ӵ�¼ JSON �н��� interface �����õ� token
68
+ * @param {object} r
69
+ * @returns {string}
70
+ */
71
+function looksLikeSearchToken(s) {
72
+  const t = String(s || '').trim()
73
+  if (t.length < 16) return false
74
+  // 采招网 token 多为较长十六进制 / Base64 风格,排除明显非 token 短句
75
+  if (t.length > 2048) return false
76
+  return /^[\w+/=\-:.]+$/i.test(t)
77
+}
78
+
79
+/**
80
+ * 深度遍历对象,按字段名或值形态猜测搜索接口 token(各版本字段名不一致)
81
+ * @param {object} root
82
+ * @param {number} depth
83
+ * @returns {string}
84
+ */
85
+export function pickTokenFromLoginResponseDeep(root, depth = 0) {
86
+  if (!root || typeof root !== 'object' || depth > 8) return ''
87
+  const preferNames =
88
+    /^(token|interface_token|search_token|user_token|u_token|usertoken|access_token|loginkey|login_token|session_token|authkey|auth_key)$/i
89
+  const maybeNames = /token|loginkey|session|authkey/i
90
+
91
+  /** @type {string[]} */
92
+  const candidates = []
93
+
94
+  const visit = (obj, d) => {
95
+    if (!obj || typeof obj !== 'object' || d > 8) return ''
96
+    for (const k of Object.keys(obj)) {
97
+      const v = obj[k]
98
+      if (v == null) continue
99
+      if (typeof v === 'string') {
100
+        const t = v.trim()
101
+        if (!t) continue
102
+        if (preferNames.test(k) && looksLikeSearchToken(t)) return t
103
+        if (maybeNames.test(k) && looksLikeSearchToken(t)) candidates.push(t)
104
+      } else if (typeof v === 'object') {
105
+        const nested = visit(v, d + 1)
106
+        if (nested) return nested
107
+      }
108
+    }
109
+    return ''
110
+  }
111
+
112
+  const direct = visit(root, depth)
113
+  if (direct) return direct
114
+  if (candidates.length) return candidates[0]
115
+
116
+  const scanValues = (obj, d) => {
117
+    if (!obj || typeof obj !== 'object' || d > 6) return ''
118
+    for (const v of Object.values(obj)) {
119
+      if (typeof v === 'string' && looksLikeSearchToken(v)) return v.trim()
120
+      if (typeof v === 'object' && v) {
121
+        const got = scanValues(v, d + 1)
122
+        if (got) return got
123
+      }
124
+    }
125
+    return ''
126
+  }
127
+  return scanValues(root, depth)
128
+}
129
+
130
+export function pickTokenFromLoginResponse(r) {
131
+  if (!r || typeof r !== 'object') return ''
132
+  const tryKeys = (obj) => {
133
+    if (!obj || typeof obj !== 'object') return ''
134
+    const keys = [
135
+      'token',
136
+      'interface_token',
137
+      'search_token',
138
+      'u_token',
139
+      'user_token',
140
+      'usertoken',
141
+      'access_token',
142
+      'loginkey',
143
+      'login_token',
144
+      'session_token',
145
+      't',
146
+      'key'
147
+    ]
148
+    for (const k of keys) {
149
+      const v = obj[k]
150
+      if (v != null && String(v).trim().length > 0) {
151
+        return String(v).trim()
152
+      }
153
+    }
154
+    return ''
155
+  }
156
+  return (
157
+    tryKeys(r) ||
158
+    tryKeys(r.data) ||
159
+    tryKeys(r.user) ||
160
+    tryKeys(r.result) ||
161
+    tryKeys(r.Result) ||
162
+    pickTokenFromLoginResponseDeep(r)
163
+  )
164
+}
165
+
166
+/**
167
+ * ��¼�ӿ������� next_token�����ڽ��������״������������һ�£�
168
+ * @param {object} r
169
+ * @returns {string}
170
+ */
171
+export function pickNextTokenFromLoginResponse(r) {
172
+  if (!r || typeof r !== 'object') return ''
173
+  const tryKeys = (obj) => {
174
+    if (!obj || typeof obj !== 'object') return ''
175
+    for (const k of ['next_token', 'nextToken', 'nexttoken']) {
176
+      const v = obj[k]
177
+      if (v != null && String(v).trim().length > 0) {
178
+        return String(v).trim()
179
+      }
180
+    }
181
+    return ''
182
+  }
183
+  return tryKeys(r) || tryKeys(r.data) || ''
184
+}
185
+
186
+export function getStoredBidcenterToken() {
187
+  try {
188
+    return sessionStorage.getItem(TOKEN_SESSION_KEY) || ''
189
+  } catch (e) {
190
+    return ''
191
+  }
192
+}
193
+
194
+export function setStoredBidcenterToken(token) {
195
+  try {
196
+    if (token && String(token).trim()) {
197
+      sessionStorage.setItem(TOKEN_SESSION_KEY, String(token).trim())
198
+    } else {
199
+      sessionStorage.removeItem(TOKEN_SESSION_KEY)
200
+    }
201
+  } catch (e) { /* ignore */ }
202
+}
203
+
204
+export function clearStoredBidcenterToken() {
205
+  setStoredBidcenterToken('')
206
+}
207
+
208
+export function setBidcenterJsonpMemberSession(active) {
209
+  try {
210
+    if (active) {
211
+      sessionStorage.setItem(JSONP_MEMBER_SESSION_KEY, '1')
212
+    } else {
213
+      sessionStorage.removeItem(JSONP_MEMBER_SESSION_KEY)
214
+    }
215
+  } catch (e) {
216
+    /* ignore */
217
+  }
218
+}
219
+
220
+export function getBidcenterJsonpMemberSession() {
221
+  try {
222
+    return sessionStorage.getItem(JSONP_MEMBER_SESSION_KEY) === '1'
223
+  } catch (e) {
224
+    return false
225
+  }
226
+}

+ 472
- 15
oa-ui/src/views/oa/bid/index.vue Näytä tiedosto

@@ -2,15 +2,44 @@
2 2
  * @Author: ysh
3 3
  * @Date: 2026-04-09 09:18:44
4 4
  * @LastEditors: Please set LastEditors
5
- * @LastEditTime: 2026-04-10 09:43:47
5
+ * @LastEditTime: 2026-04-17 15:19:25
6 6
 -->
7 7
 <template>
8 8
   <div class="app-container">
9
+    <el-alert
10
+      v-if="hasEnvBidcenterCredentials"
11
+      title="已在环境变量中配置采招网账号:密码会打进前端包,切勿提交仓库;生产环境请改为后端登录,仅向前端下发 token。"
12
+      type="warning"
13
+      show-icon
14
+      :closable="false"
15
+      class="mb8"
16
+    />
9 17
     <div class="toolbar-card mb8">
10
-      <div class="keyword-display-row">
18
+      <div class="bidcenter-auth-row">
19
+        <span class="field-label">采招网会员</span>
20
+        <el-tag :type="memberSearchActive ? 'success' : 'info'" size="small">{{ bidcenterAuthLabel }}</el-tag>
21
+        <el-button
22
+          v-if="hasEnvBidcenterCredentials"
23
+          size="small"
24
+          :loading="bidcenterLoginLoading"
25
+          @click="handleBidcenterLoginFromEnv"
26
+        >环境变量登录</el-button>
27
+        <el-button size="small" :loading="bidcenterLoginLoading" @click="openBidcenterLoginDialog">账号登录</el-button>
28
+        <el-input
29
+          v-model="manualTokenDraft"
30
+          size="small"
31
+          placeholder="手动粘贴搜索接口 token"
32
+          show-password
33
+          class="token-input"
34
+          clearable
35
+        />
36
+        <el-button size="small" @click="applyManualBidcenterToken">应用 token</el-button>
37
+        <el-button v-if="memberSearchActive" size="small" type="text" @click="clearBidcenterMemberToken">清除</el-button>
38
+      </div>
39
+      <!-- <div class="keyword-display-row">
11 40
         <span class="field-label">当前关键词</span>
12 41
         <el-tag type="primary" effect="plain" class="keyword-tag">{{ displayKeyword || '—' }}</el-tag>
13
-      </div>
42
+      </div> -->
14 43
       <el-form :inline="true" size="small" class="remote-search-form" @submit.native.prevent>
15 44
         <el-form-item label="修改关键词">
16 45
           <el-input v-model="keywordText" placeholder="多个关键词用英文逗号分隔" clearable style="width: 620px"
@@ -59,6 +88,72 @@
59 88
     </el-table>
60 89
     <el-pagination v-if="pageTotal > 0" class="mt16" background :current-page="searchQuery.page" :page-size="pageSize"
61 90
       layout="total, prev, pager, next, jumper" :total="pageTotal" @current-change="handlePageChange" />
91
+
92
+    <el-dialog
93
+      :visible.sync="bidcenterLoginDialogVisible"
94
+      custom-class="bidcenter-login-dialog"
95
+      width="440px"
96
+      append-to-body
97
+      :close-on-click-modal="false"
98
+      @open="onBidcenterDialogOpen"
99
+    >
100
+      <template slot="title">
101
+        <div class="bidcenter-login-dialog__title">
102
+          <div class="bidcenter-login-dialog__brand" aria-hidden="true">
103
+            <i class="el-icon-medal" />
104
+          </div>
105
+          <div class="bidcenter-login-dialog__title-text">
106
+            <span class="bidcenter-login-dialog__title-main">采招网会员登录</span>
107
+            <span class="bidcenter-login-dialog__title-sub">登录成功后,检索请求将按会员身份发起</span>
108
+          </div>
109
+        </div>
110
+      </template>
111
+      <div class="bidcenter-login-dialog__panel">
112
+        <el-form
113
+          class="bidcenter-login-dialog__form"
114
+          label-position="top"
115
+          size="medium"
116
+          @submit.native.prevent
117
+        >
118
+          <el-form-item label="用户名 / 手机号">
119
+            <el-input
120
+              v-model="bidcenterLoginForm.username"
121
+              autocomplete="username"
122
+              clearable
123
+              placeholder="请输入采招网账号"
124
+              prefix-icon="el-icon-user"
125
+            />
126
+          </el-form-item>
127
+          <el-form-item label="密码">
128
+            <el-input
129
+              v-model="bidcenterLoginForm.password"
130
+              type="password"
131
+              show-password
132
+              autocomplete="current-password"
133
+              clearable
134
+              placeholder="请输入密码"
135
+              prefix-icon="el-icon-lock"
136
+              @keyup.enter.native="handleBidcenterLoginFromForm"
137
+            />
138
+          </el-form-item>
139
+        </el-form>
140
+        <p class="bidcenter-login-dialog__hint">
141
+          <i class="el-icon-info" />
142
+          凭账号校验后可能仅写入 Cookie;若检索权限异常,可在页面使用「应用 token」。
143
+        </p>
144
+      </div>
145
+      <div slot="footer" class="bidcenter-login-dialog__footer">
146
+        <el-button size="medium" @click="bidcenterLoginDialogVisible = false">取消</el-button>
147
+        <el-button
148
+          type="primary"
149
+          size="medium"
150
+          :loading="bidcenterLoginLoading"
151
+          @click="handleBidcenterLoginFromForm"
152
+        >
153
+          登录
154
+        </el-button>
155
+      </div>
156
+    </el-dialog>
62 157
   </div>
63 158
 </template>
64 159
 
@@ -67,6 +162,16 @@ import axios from 'axios'
67 162
 import CryptoJS from 'crypto-js'
68 163
 import { Message, Loading } from 'element-ui'
69 164
 import { listBid } from '@/api/oa/bid/bid'
165
+import {
166
+  loginBidcenterMemberJsonp,
167
+  pickTokenFromLoginResponse,
168
+  pickNextTokenFromLoginResponse,
169
+  getStoredBidcenterToken,
170
+  setStoredBidcenterToken,
171
+  clearStoredBidcenterToken,
172
+  setBidcenterJsonpMemberSession,
173
+  getBidcenterJsonpMemberSession
174
+} from '@/utils/bidcenterMemberLogin'
70 175
 
71 176
 const SEARCH_PRO_URL = 'https://interface.bidcenter.com.cn/search/GetSearchProHandler.ashx'
72 177
 const BIDCENTER_GUID = 'cad034bd-ad01-4dce-a4ca-1a1252f0dbf1'
@@ -89,7 +194,7 @@ function serializeSearchParams(data) {
89 194
   for (const key of Object.keys(data)) {
90 195
     const v = data[key]
91 196
     if (v === undefined || v === null) continue
92
-    if (v === '' && key !== 'token') continue
197
+    if (v === '' && key !== 'token' && key !== 'next_token') continue
93 198
     if (key === 'keywords') {
94 199
       parts.push(`keywords=${v}`)
95 200
     } else {
@@ -111,7 +216,10 @@ export default {
111 216
       searchWarning: '',
112 217
       emptyText: '暂无数据',
113 218
       searchLoading: false,
114
-      nextToken: '',
219
+      /** 接口每次返回的 next_token(searchv16:20 页后靠此续页) */
220
+      storedNextTokenFromApi: '',
221
+      /** 仅用于下一次 POST 的 next_token,发出后清空 */
222
+      pendingNextToken: '',
115 223
       /** 检索输入框与接口关键词(明文) */
116 224
       keywordText: '控制网,库容复核,测量技术,地形测量,安全监测,基准网复测,测量中心,土地测绘',
117 225
       /** 接口返回或上次成功检索后的展示文案 */
@@ -123,10 +231,34 @@ export default {
123 231
         type: 1,
124 232
         mod: 0,
125 233
         page: 1
234
+      },
235
+      /** 采招网搜索接口 token(与 searchv16 中 token 一致) */
236
+      bidcenterMemberToken: '',
237
+      /** JSONP 登录成功但未返回 token 时,与 sessionStorage 同步:用采招网 cookie + withCredentials 调搜索 */
238
+      bidcenterCookieMember: false,
239
+      bidcenterLoginLoading: false,
240
+      manualTokenDraft: '',
241
+      bidcenterLoginDialogVisible: false,
242
+      bidcenterLoginForm: {
243
+        username: '',
244
+        password: ''
126 245
       }
127 246
     }
128 247
   },
129 248
   computed: {
249
+    bidcenterAuthLabel() {
250
+      if ((this.bidcenterMemberToken || '').trim()) return '已登录(请求已携带 token)'
251
+      if (this.bidcenterCookieMember) return '已登录(采招网 Cookie,无 token 字段)'
252
+      return '未登录(匿名)'
253
+    },
254
+    memberSearchActive() {
255
+      return !!(this.bidcenterMemberToken || '').trim() || this.bidcenterCookieMember
256
+    },
257
+    hasEnvBidcenterCredentials() {
258
+      const u = process.env.VUE_APP_BIDCENTER_USERNAME
259
+      const p = process.env.VUE_APP_BIDCENTER_PASSWORD
260
+      return !!(u && String(u).trim() && p && String(p).trim())
261
+    },
130 262
     tableFilterTrimmed() {
131 263
       return (this.tableFilter || '').trim()
132 264
     },
@@ -157,10 +289,135 @@ export default {
157 289
       return this.emptyText
158 290
     }
159 291
   },
160
-  created() {
161
-    this.getList()
292
+  async created() {
293
+    this.restoreBidcenterTokenFromStorage()
294
+    await this.tryAutoLoginBidcenterFromEnv()
295
+    await this.getList()
162 296
   },
163 297
   methods: {
298
+    restoreBidcenterTokenFromStorage() {
299
+      this.bidcenterMemberToken = getStoredBidcenterToken()
300
+      this.bidcenterCookieMember = getBidcenterJsonpMemberSession()
301
+      if (this.memberSearchActive && Number(this.searchQuery.mod) === 0) {
302
+        this.searchQuery.mod = 1
303
+      }
304
+    },
305
+    async tryAutoLoginBidcenterFromEnv() {
306
+      if (!this.hasEnvBidcenterCredentials) return
307
+      if (this.memberSearchActive) return
308
+      const u = String(process.env.VUE_APP_BIDCENTER_USERNAME || '').trim()
309
+      const p = String(process.env.VUE_APP_BIDCENTER_PASSWORD || '').trim()
310
+      await this.runBidcenterMemberLogin(u, p, { silent: true })
311
+    },
312
+    onBidcenterDialogOpen() {
313
+      const u = String(process.env.VUE_APP_BIDCENTER_USERNAME || '').trim()
314
+      const p = String(process.env.VUE_APP_BIDCENTER_PASSWORD || '').trim()
315
+      if (!this.bidcenterLoginForm.username && u) this.bidcenterLoginForm.username = u
316
+      if (!this.bidcenterLoginForm.password && p) this.bidcenterLoginForm.password = p
317
+    },
318
+    openBidcenterLoginDialog() {
319
+      this.bidcenterLoginDialogVisible = true
320
+    },
321
+    async handleBidcenterLoginFromEnv() {
322
+      const u = String(process.env.VUE_APP_BIDCENTER_USERNAME || '').trim()
323
+      const p = String(process.env.VUE_APP_BIDCENTER_PASSWORD || '').trim()
324
+      await this.runBidcenterMemberLogin(u, p, { silent: false })
325
+    },
326
+    async handleBidcenterLoginFromForm() {
327
+      const u = (this.bidcenterLoginForm.username || '').trim()
328
+      const p = (this.bidcenterLoginForm.password || '').trim()
329
+      const ok = await this.runBidcenterMemberLogin(u, p, { silent: false })
330
+      if (ok) this.bidcenterLoginDialogVisible = false
331
+    },
332
+    /**
333
+     * @returns {Promise<boolean>} 是否拿到 token
334
+     */
335
+    async runBidcenterMemberLogin(username, password, { silent }) {
336
+      this.bidcenterLoginLoading = true
337
+      try {
338
+        const r = await loginBidcenterMemberJsonp(username, password)
339
+        const ok = r.status === 'true' || r.status === true
340
+        if (!ok) {
341
+          let brief = '登录失败'
342
+          if (r.brief != null) {
343
+            try {
344
+              brief = decodeURIComponent(String(r.brief))
345
+            } catch (e) {
346
+              brief = String(r.brief)
347
+            }
348
+          }
349
+          Message.error(brief)
350
+          return false
351
+        }
352
+        const tok = pickTokenFromLoginResponse(r)
353
+        if (tok) {
354
+          setBidcenterJsonpMemberSession(false)
355
+          this.bidcenterCookieMember = false
356
+          this.bidcenterMemberToken = tok
357
+          setStoredBidcenterToken(tok)
358
+          this.searchQuery.mod = 1
359
+          const nt = pickNextTokenFromLoginResponse(r)
360
+          this.storedNextTokenFromApi = nt || ''
361
+          if (!silent) {
362
+            Message.success('采招网会员登录成功')
363
+            this.fetchBidcenterSearch()
364
+          }
365
+          return true
366
+        }
367
+        // 与采招网原页一致:登录接口常只写 cookie、不返回搜索 token;凭 cookie 请求搜索接口
368
+        setBidcenterJsonpMemberSession(true)
369
+        this.bidcenterCookieMember = true
370
+        this.bidcenterMemberToken = ''
371
+        this.searchQuery.mod = 1
372
+        this.storedNextTokenFromApi = ''
373
+        this.pendingNextToken = ''
374
+        if (!silent) {
375
+          Message.success('采招网账号已校验;将携带采招网 Cookie 请求检索(若仍非会员数据,请从采招网页复制 token 后「应用 token」)')
376
+          this.fetchBidcenterSearch()
377
+        } else {
378
+          Message({
379
+            message:
380
+              '采招网环境账号已登录(Cookie 会话)。若检索权限异常,请在页面使用「应用 token」或检查浏览器是否拦截第三方 Cookie。',
381
+            type: 'warning',
382
+            duration: 8000
383
+          })
384
+        }
385
+        return true
386
+      } catch (e) {
387
+        Message.error(e.message || '采招网登录失败')
388
+        return false
389
+      } finally {
390
+        this.bidcenterLoginLoading = false
391
+      }
392
+    },
393
+    applyManualBidcenterToken() {
394
+      const t = (this.manualTokenDraft || '').trim()
395
+      if (!t) {
396
+        Message.warning('请先粘贴 token')
397
+        return
398
+      }
399
+      setBidcenterJsonpMemberSession(false)
400
+      this.bidcenterCookieMember = false
401
+      this.bidcenterMemberToken = t
402
+      setStoredBidcenterToken(t)
403
+      this.searchQuery.mod = 1
404
+      this.storedNextTokenFromApi = ''
405
+      this.pendingNextToken = ''
406
+      this.manualTokenDraft = ''
407
+      Message.success('已保存 token,正在按会员身份重新检索')
408
+      this.fetchBidcenterSearch()
409
+    },
410
+    clearBidcenterMemberToken() {
411
+      clearStoredBidcenterToken()
412
+      setBidcenterJsonpMemberSession(false)
413
+      this.bidcenterCookieMember = false
414
+      this.bidcenterMemberToken = ''
415
+      this.searchQuery.mod = 0
416
+      this.storedNextTokenFromApi = ''
417
+      this.pendingNextToken = ''
418
+      Message.success('已清除采招网 token')
419
+      this.fetchBidcenterSearch()
420
+    },
164 421
     async getList() {
165 422
       const queryParams = {
166 423
         url:
@@ -177,24 +434,37 @@ export default {
177 434
       }
178 435
       this.keywordText = t
179 436
       this.searchQuery.page = 1
180
-      this.nextToken = ''
437
+      this.storedNextTokenFromApi = ''
438
+      this.pendingNextToken = ''
181 439
       this.tableFilter = ''
440
+      if (this.memberSearchActive) {
441
+        this.searchQuery.mod = 1
442
+      }
182 443
       this.fetchBidcenterSearch()
183 444
     },
184 445
     buildSearchPostBody() {
446
+      const token = (this.bidcenterMemberToken || '').trim()
447
+      const isMember = this.memberSearchActive
448
+      const modNum = Number(this.searchQuery.mod)
185 449
       const body = {
186 450
         from: 6137,
187 451
         guid: BIDCENTER_GUID,
188 452
         location: 6138,
189
-        token: '',
453
+        token,
190 454
         keywords: encodeURIComponent((this.keywordText || '').trim()),
191 455
         time: this.searchQuery.time,
192 456
         type: this.searchQuery.type,
193
-        mod: this.searchQuery.mod,
457
+        mod: isMember ? (modNum >= 1 ? modNum : 1) : this.searchQuery.mod,
194 458
         page: this.searchQuery.page
195 459
       }
196
-      if (this.nextToken) {
197
-        body.next_token = this.nextToken
460
+      let nextTok = ''
461
+      if (this.pendingNextToken) {
462
+        nextTok = this.pendingNextToken
463
+      } else if (isMember && this.storedNextTokenFromApi) {
464
+        nextTok = this.storedNextTokenFromApi
465
+      }
466
+      if (nextTok) {
467
+        body.next_token = nextTok
198 468
       }
199 469
       return body
200 470
     },
@@ -210,12 +480,16 @@ export default {
210 480
       } catch (e) { /* ignore */ }
211 481
       this.searchLoading = true
212 482
       const postData = this.buildSearchPostBody()
483
+      this.pendingNextToken = ''
484
+      const useCookieMember =
485
+        this.bidcenterCookieMember && !(this.bidcenterMemberToken || '').trim()
213 486
       try {
214 487
         const res = await axios({
215 488
           url: SEARCH_PRO_URL,
216 489
           method: 'post',
217 490
           timeout: 20000,
218 491
           responseType: 'text',
492
+          withCredentials: useCookieMember,
219 493
           headers: {
220 494
             'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'
221 495
           },
@@ -224,7 +498,16 @@ export default {
224 498
         })
225 499
         this.bindSearchData(res.data)
226 500
       } catch (e) {
227
-        Message({ message: '网络错误,请重试', type: 'error' })
501
+        if (useCookieMember && e && e.message === 'Network Error') {
502
+          Message({
503
+            message:
504
+              '携带采招网 Cookie 的请求被浏览器或 CORS 拦截。请关闭第三方 Cookie 限制,或从采招网搜索页复制 token 后使用「应用 token」。',
505
+            type: 'error',
506
+            duration: 10000
507
+          })
508
+        } else {
509
+          Message({ message: '网络错误,请重试', type: 'error' })
510
+        }
228 511
         this.tableData = []
229 512
       } finally {
230 513
         this.searchLoading = false
@@ -258,7 +541,10 @@ export default {
258 541
         }
259 542
         const resultData = data.other2
260 543
         this.applyPageSearchJson(resultData.pageSearchJson)
261
-        this.nextToken = resultData.next_token || ''
544
+        this.storedNextTokenFromApi =
545
+          resultData.next_token != null && resultData.next_token !== ''
546
+            ? String(resultData.next_token)
547
+            : ''
262 548
         const list = resultData.listData
263 549
         this.tableData = Array.isArray(list) ? list : []
264 550
         this.pageTotal = Number(resultData.showInfoCount) || 0
@@ -273,6 +559,7 @@ export default {
273 559
         this.realInfoCount = total != null ? total : this.pageTotal
274 560
         this.emptyText = this.tableData.length ? '暂无数据' : '未查询到相关信息'
275 561
       } else {
562
+        this.storedNextTokenFromApi = ''
276 563
         if (data.msg) {
277 564
           Message.warning(data.msg)
278 565
         }
@@ -302,6 +589,12 @@ export default {
302 589
         if (jq.page != null) {
303 590
           this.searchQuery.page = jq.page
304 591
         }
592
+        if (jq.mod != null && jq.mod !== '') {
593
+          const m = parseInt(jq.mod, 10)
594
+          if (!Number.isNaN(m)) {
595
+            this.searchQuery.mod = m
596
+          }
597
+        }
305 598
         if (jq.keywords != null && String(jq.keywords).trim() !== '') {
306 599
           this.displayKeyword = String(jq.keywords).trim()
307 600
         } else {
@@ -313,9 +606,21 @@ export default {
313 606
         if (k) this.displayKeyword = k
314 607
       }
315 608
     },
609
+    /**
610
+     * 匿名:仅第 20→21 页用 pending 带 next_token(与 searchv16 layui-last 一致)。
611
+     * 会员:翻页时在 buildSearchPostBody 中自动带上一响应的 storedNextTokenFromApi;跳页/回退则清空游标。
612
+     */
316 613
     handlePageChange(page) {
614
+      const oldPage = this.searchQuery.page
615
+      this.pendingNextToken = ''
616
+      const hasMember = this.memberSearchActive
617
+      if (!hasMember && page === oldPage + 1 && oldPage >= 20 && this.storedNextTokenFromApi) {
618
+        this.pendingNextToken = this.storedNextTokenFromApi
619
+      }
620
+      if (page < oldPage || page > oldPage + 1) {
621
+        this.storedNextTokenFromApi = ''
622
+      }
317 623
       this.searchQuery.page = page
318
-      this.nextToken = ''
319 624
       this.fetchBidcenterSearch()
320 625
     },
321 626
     plainTitle(html) {
@@ -407,6 +712,20 @@ export default {
407 712
   margin-bottom: 8px;
408 713
 }
409 714
 
715
+.bidcenter-auth-row {
716
+  display: flex;
717
+  align-items: center;
718
+  flex-wrap: wrap;
719
+  gap: 10px;
720
+  margin-bottom: 14px;
721
+  padding-bottom: 12px;
722
+  border-bottom: 1px dashed #ebeef5;
723
+}
724
+
725
+.token-input {
726
+  width: 200px;
727
+}
728
+
410 729
 .table-filter-row {
411 730
   display: flex;
412 731
   align-items: center;
@@ -445,3 +764,141 @@ export default {
445 764
   color: #e6a23c;
446 765
 }
447 766
 </style>
767
+
768
+<!-- append-to-body 时对话框在 body 下,需非 scoped 样式 + custom-class -->
769
+<style lang="scss">
770
+.bidcenter-login-dialog {
771
+  border-radius: 14px;
772
+  overflow: hidden;
773
+  box-shadow: 0 22px 55px rgba(15, 52, 96, 0.14), 0 0 1px rgba(15, 52, 96, 0.06);
774
+
775
+  .el-dialog__header {
776
+    padding: 0;
777
+    margin: 0;
778
+    border: none;
779
+  }
780
+
781
+  .el-dialog__headerbtn {
782
+    top: 18px;
783
+    right: 18px;
784
+    font-size: 16px;
785
+
786
+    .el-dialog__close {
787
+      color: rgba(255, 255, 255, 0.88);
788
+    }
789
+
790
+    &:hover .el-dialog__close,
791
+    &:focus .el-dialog__close {
792
+      color: #fff;
793
+    }
794
+  }
795
+
796
+  .el-dialog__body {
797
+    padding: 0 24px 8px;
798
+    background: linear-gradient(180deg, #f7fafc 0%, #fff 48%);
799
+  }
800
+
801
+  .el-dialog__footer {
802
+    padding: 12px 24px 20px;
803
+    border-top: 1px solid #eef2f6;
804
+    background: #fff;
805
+  }
806
+}
807
+
808
+.bidcenter-login-dialog__title {
809
+  display: flex;
810
+  align-items: center;
811
+  gap: 14px;
812
+  padding: 22px 48px 18px 24px;
813
+  background: linear-gradient(135deg, #1a5fb4 0%, #409eff 55%, #66b1ff 100%);
814
+  color: #fff;
815
+}
816
+
817
+.bidcenter-login-dialog__brand {
818
+  flex-shrink: 0;
819
+  width: 48px;
820
+  height: 48px;
821
+  border-radius: 12px;
822
+  display: flex;
823
+  align-items: center;
824
+  justify-content: center;
825
+  background: rgba(255, 255, 255, 0.18);
826
+  backdrop-filter: blur(6px);
827
+  font-size: 24px;
828
+}
829
+
830
+.bidcenter-login-dialog__title-text {
831
+  display: flex;
832
+  flex-direction: column;
833
+  gap: 6px;
834
+  min-width: 0;
835
+}
836
+
837
+.bidcenter-login-dialog__title-main {
838
+  font-size: 18px;
839
+  font-weight: 600;
840
+  letter-spacing: 0.02em;
841
+  line-height: 1.3;
842
+}
843
+
844
+.bidcenter-login-dialog__title-sub {
845
+  font-size: 12px;
846
+  opacity: 0.92;
847
+  line-height: 1.45;
848
+}
849
+
850
+.bidcenter-login-dialog__panel {
851
+  padding-top: 20px;
852
+}
853
+
854
+.bidcenter-login-dialog__form {
855
+  .el-form-item {
856
+    margin-bottom: 18px;
857
+  }
858
+
859
+  .el-form-item__label {
860
+    padding: 0 0 6px;
861
+    line-height: 1.4;
862
+    color: #606266;
863
+    font-weight: 500;
864
+  }
865
+
866
+  .el-input__inner {
867
+    border-radius: 8px;
868
+    height: 40px;
869
+    line-height: 40px;
870
+  }
871
+}
872
+
873
+.bidcenter-login-dialog__hint {
874
+  margin: 4px 0 0;
875
+  padding: 10px 12px;
876
+  font-size: 12px;
877
+  line-height: 1.55;
878
+  color: #909399;
879
+  background: #f4f7fb;
880
+  border-radius: 8px;
881
+  border: 1px solid #e8eef5;
882
+
883
+  .el-icon-info {
884
+    margin-right: 6px;
885
+    color: #409eff;
886
+  }
887
+}
888
+
889
+.bidcenter-login-dialog__footer {
890
+  display: flex;
891
+  justify-content: flex-end;
892
+  align-items: center;
893
+  gap: 12px;
894
+
895
+  .el-button {
896
+    min-width: 96px;
897
+    border-radius: 8px;
898
+  }
899
+
900
+  .el-button--primary {
901
+    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.35);
902
+  }
903
+}
904
+</style>

Loading…
Peruuta
Tallenna