755866e28b
配合后端把支付宝默认渠道改回电脑网站支付(当面付需单独签约并提交 资料审核,实测被网关以 ACQ.ACCESS_FORBIDDEN 拒绝)。 改动(tender/pay.vue 收银台 + template-07/BuyDocument.vue 购标流程): - 恢复 alipayForm 这套处理:后端返回自提交表单时用 document.write 写进新窗口跳转支付宝收银台,原页面留在原标签页继续轮询查单; 弹窗被拦截时给出可行动提示,按钮可重试。 - 模板恢复「表单」与「二维码」两支:支付宝走表单、微信走 codeUrl。 - 切换支付方式时把两种产物都清掉,避免上一支的残留留在页面上。 - 文件头注释同步:微信出码、支付宝跳转。 前端不传 payType 之外的值即可——裸 alipay 现在由后端解析为网页支付, 所以「微信支付 / 支付宝」选择器保持不变。 验证:eslint 0 error;npm run build:dev 通过。
129 lines
5.0 KiB
Vue
129 lines
5.0 KiB
Vue
<template>
|
||
<div class="container mx-auto px-4 py-10">
|
||
<div class="mb-6 flex items-center justify-between">
|
||
<div>
|
||
<h1 class="text-2xl font-bold text-gray-900">标书中心</h1>
|
||
<p class="text-sm text-gray-500">汇聚各类采购项目,选择您需要的标书进行购买</p>
|
||
</div>
|
||
<HjcUserBar />
|
||
</div>
|
||
|
||
<!-- 搜索 + 分类 -->
|
||
<div class="mb-6 flex flex-col sm:flex-row gap-3">
|
||
<input
|
||
v-model="keywords"
|
||
class="flex-1 rounded-md border border-gray-300 px-4 py-2 text-sm"
|
||
placeholder="搜索项目名称 / 采购编号"
|
||
@keyup.enter="load()"
|
||
/>
|
||
<select v-model="category" class="rounded-md border border-gray-300 px-4 py-2 text-sm">
|
||
<option value="">全部项目</option>
|
||
<option value="服务类">服务类</option>
|
||
<option value="工程类">工程类</option>
|
||
<option value="货物类">货物类</option>
|
||
</select>
|
||
<button
|
||
class="rounded-md bg-blue-600 px-5 py-2 text-sm text-white hover:bg-blue-700"
|
||
@click="load()"
|
||
>
|
||
搜索
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 列表 -->
|
||
<div v-if="items.length" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
<NuxtLink
|
||
v-for="item in items"
|
||
:key="item.id"
|
||
:to="`/tender/${item.id}`"
|
||
class="group rounded-lg border border-gray-200 p-5 hover:shadow-md transition"
|
||
>
|
||
<div class="text-xs text-gray-400 mb-1">项目编号:{{ item.projectNo }}</div>
|
||
<h2 class="text-base font-semibold text-gray-900 group-hover:text-blue-600 line-clamp-2">
|
||
{{ item.projectName }}
|
||
</h2>
|
||
<div class="mt-3 flex items-center gap-2 text-xs text-gray-500">
|
||
<span class="rounded bg-gray-100 px-2 py-0.5">{{ item.category || '未分类' }}</span>
|
||
<span>截止:{{ item.deadlineTime?.slice(0, 19)?.replace('T', ' ') || '-' }}</span>
|
||
</div>
|
||
<div class="mt-4 flex items-end justify-between">
|
||
<div class="text-blue-600">
|
||
<span class="text-xl font-bold">¥{{ formatMoney(item.tenderPrice) }}</span>
|
||
<span class="text-xs text-gray-400 ml-1">信息服务费</span>
|
||
</div>
|
||
<span class="text-sm text-blue-600 group-hover:underline">查看详情</span>
|
||
</div>
|
||
</NuxtLink>
|
||
</div>
|
||
|
||
<div v-else-if="loaded" class="py-16 text-center text-gray-500">暂无标书项目</div>
|
||
<SiteLoading v-show="!loaded" />
|
||
|
||
<!-- 加载更多 -->
|
||
<div v-if="items.length < count" class="mt-8 text-center">
|
||
<button class="rounded-md border border-gray-300 px-6 py-2 text-sm" @click="loadMore()">
|
||
加载更多
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { Tender } from '~/types/tender'
|
||
|
||
const keywords = ref('')
|
||
const category = ref('')
|
||
const page = ref(1)
|
||
const limit = 12
|
||
const loaded = ref(false)
|
||
const listState = shallowRef<Tender[]>([])
|
||
|
||
const items = computed(() => listState.value)
|
||
const count = computed(() => (listState as any).__count || 0)
|
||
|
||
/**
|
||
* 必须用 useRequestFetch 取数,不能用裸 `$fetch`。
|
||
*
|
||
* 本组件在 setup 顶层就 `await load()`(SSR 期间执行):裸 `$fetch` 走 Nuxt 全局 ofetch,
|
||
* 服务端渲染时**不会**把浏览器原始请求头(Cookie、X-Forwarded-* 等)转发给站内 `/api` 代理,
|
||
* 代理收到的是 host=localhost、无 Cookie 的「新请求」——租户中间件按 localhost 走本地分支,
|
||
* 解析不到租户 → 上游按空租户查询 → SSR 渲染出「暂无标书项目」空列表。
|
||
* 客户端水合时浏览器重新请求拿到真实数据,两棵树分支错位(空状态 vs 网格列表),
|
||
* 卡片被插进残留的空状态 div 里 → 标书列表区域样式错乱(inline 排布、边框断裂)。
|
||
*
|
||
* `useRequestFetch` 会转发原始请求头:SSR 期间代理能拿到真实 Host 与 Cookie,
|
||
* 与客户端刷新后的行为一致。(同一个坑的另外两种表现见 TenderDetail.vue 与
|
||
* app/pages/tender/orders/index.vue 的注释。)
|
||
*
|
||
* 客户端侧 `useRequestFetch()` 等价于 `globalThis.$fetch`,行为不变。
|
||
*/
|
||
const requestFetch = useRequestFetch()
|
||
|
||
async function load() {
|
||
page.value = 1
|
||
const res: any = await requestFetch('/api/tender/list', {
|
||
query: { page: page.value, limit, keywords: keywords.value || undefined, category: category.value || undefined }
|
||
})
|
||
listState.value = res?.list || []
|
||
;(listState as any).__count = res?.count || 0
|
||
loaded.value = true
|
||
}
|
||
|
||
async function loadMore() {
|
||
page.value += 1
|
||
const res: any = await requestFetch('/api/tender/list', {
|
||
query: { page: page.value, limit, keywords: keywords.value || undefined, category: category.value || undefined }
|
||
})
|
||
const next = res?.list || []
|
||
listState.value = [...listState.value, ...next]
|
||
;(listState as any).__count = res?.count || 0
|
||
}
|
||
|
||
function formatMoney(n?: number | string) {
|
||
if (n === undefined || n === null) return '0.00'
|
||
return Number(n).toFixed(2)
|
||
}
|
||
|
||
await load()
|
||
</script>
|