feat(hjc-web): 标书详情加「收藏」按钮,「我的收藏」页与顶栏入口
后端接口是端无关的,本轮 **mp-java 零改动**,纯前端 + Nitro 代理。 - 新增 favorites.get/post/delete 三个站内代理,原样透传 ApiResult (HTTP 恒 200,按 body.code 判登录态与业务失败)。delete 用查询串传 projectId。 - detail.get.ts 补发 Authorization:详情接口内联了 favorited,而它此前是匿名调用, 已收藏的项目会显示成「未收藏」。未登录时不带这个头,行为与改动前完全一致 (返回体仍整体返回 data,favorited 正是靠这一点透传的)。 - 「我的收藏」做成**模板无关的公共路由** app/pages/tender/favorites.vue(10 套模板共用), 顶栏 HjcUserBar 在「我的订单」后加入口。列表按收藏时间倒序 + 加载更多; 只渲染后端算好的 saleState/purchased(前端不再判一遍);失效项目**保留条目**、 点击只提示不进详情、仍可取消;取消收藏弹二次确认(复用 HjcConfirmDialog)。 取数用 useRequestFetch —— 裸 $fetch 在 SSR 不转发 cookie,会被代理当成匿名调用, 拿到 401 后 handleAuthCode 会清凭据跳登录,等于刷新页面把自己登出。 - template-07 详情页加收藏按钮(heart SVG 两态,不引图标库),未登录点它跳登录、 回跳带 favorite=1 后自动补一次收藏(只在客户端 onMounted 做,不改写 URL—— 详情壳是 :key="route.fullPath",改 query 会重挂载并与收藏请求赛跑)。 取数同样改用 useRequestFetch,否则首屏 favorited 恒为 false。 - 只改 template-07:10 套模板里只有它有 TenderDetail.vue,其余按需再补。
This commit is contained in:
@@ -60,8 +60,33 @@
|
||||
<p>售卖方式:{{ sellingMethodText }}</p>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
收藏:**不需要企业资质**(资质只卡购买),所以这里不看 authStatus / canBuy。
|
||||
用内联 SVG 而不是图标库:这个模板是纯 Tailwind 手写的,没有引入 a-* 组件。
|
||||
-->
|
||||
<button
|
||||
class="mt-6 w-full rounded-md bg-blue-600 py-3 text-white font-medium hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
type="button"
|
||||
class="mt-6 flex w-full items-center justify-center gap-2 rounded-md border border-gray-300 py-2.5 text-sm text-gray-700 hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="favBusy"
|
||||
@click="toggleFavorite"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
class="h-4 w-4"
|
||||
:class="favorited ? 'text-red-500' : 'text-gray-400'"
|
||||
:fill="favorited ? 'currentColor' : 'none'"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M12 20.5 4.6 13.4a4.6 4.6 0 0 1 0-6.6 4.9 4.9 0 0 1 6.9 0l.5.5.5-.5a4.9 4.9 0 0 1 6.9 0 4.6 4.6 0 0 1 0 6.6Z" />
|
||||
</svg>
|
||||
<span>{{ favorited ? '已收藏' : '收藏' }}</span>
|
||||
</button>
|
||||
<p v-if="favError" class="mt-2 text-center text-xs text-red-500">{{ favError }}</p>
|
||||
|
||||
<button
|
||||
class="mt-3 w-full rounded-md bg-blue-600 py-3 text-white font-medium hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:disabled="!buyable"
|
||||
@click="goBuy"
|
||||
>
|
||||
@@ -84,6 +109,26 @@ const id = route.params.id as string
|
||||
const tender = shallowRef<Tender | null>(null)
|
||||
const notFound = ref(false)
|
||||
|
||||
/** 是否已收藏:由详情接口**内联**返回(未登录恒 false),不额外发请求 */
|
||||
const favorited = ref(false)
|
||||
const favBusy = ref(false)
|
||||
const favError = ref('')
|
||||
/** 本次挂载是否已处理过「登录回跳补收藏」——保证只补一次 */
|
||||
const favPendingHandled = ref(false)
|
||||
|
||||
const { isLoggedIn, handleAuthCode } = useHjcAuth()
|
||||
/**
|
||||
* 必须用 useRequestFetch 取数,不能用裸 `$fetch`。
|
||||
*
|
||||
* 详情响应里内联了 `favorited`,而站内代理只有在 **Cookie 被转发** 时才认得出登录态:
|
||||
* Nuxt 只在 `useRequestFetch` / `useFetch` 里做转发,裸 `$fetch` 在 SSR 期间不转发 Cookie,
|
||||
* 代理就是匿名调用,后端按契约返回 `favorited = false` —— 已收藏的项目首屏会显示成「未收藏」。
|
||||
* (同一个坑的另一种表现见 app/pages/tender/orders.vue:77-88:那次是刷新把自己登出。)
|
||||
*
|
||||
* 客户端侧 `useRequestFetch()` 等价于 `globalThis.$fetch`,行为不变。
|
||||
*/
|
||||
const requestFetch = useRequestFetch()
|
||||
|
||||
const buyable = computed(() => {
|
||||
if (!tender.value) return false
|
||||
if (tender.value.status !== undefined && tender.value.status !== 1) return false
|
||||
@@ -116,11 +161,82 @@ function goBuy() {
|
||||
return navigateTo(`/buy?id=${tender.value.id}`)
|
||||
}
|
||||
|
||||
/** 补收藏:登录回跳后静默补一次,失败不打扰(用户自己再点一次即可) */
|
||||
async function addFavoriteQuietly() {
|
||||
try {
|
||||
const res: any = await requestFetch('/api/tender/favorites', {
|
||||
method: 'POST',
|
||||
body: { projectId: Number(id) }
|
||||
})
|
||||
if (res?.code === 0) {
|
||||
favorited.value = !!res.data?.favorited
|
||||
}
|
||||
} catch {
|
||||
// 静默:补收藏失败就退化成「未收藏」
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 收藏 / 取消收藏。
|
||||
*
|
||||
* **不判资质**:未认证 / 审核中 / 已驳回的企业都能收藏(资质只卡购买,
|
||||
* 右侧「立即购买」仍然照旧拦)。**也不弹二次确认**:详情页点「已收藏」是明确意图,
|
||||
* 弹窗只是摩擦——列表页的取消才弹确认(见 app/pages/tender/favorites.vue)。
|
||||
*/
|
||||
async function toggleFavorite() {
|
||||
if (!isLoggedIn()) {
|
||||
// 与 h5 一致:跳登录并带回跳地址,登录成功后自动补上这次收藏
|
||||
return navigateTo(`/login?redirect=${encodeURIComponent(`/tender/${id}?favorite=1`)}`)
|
||||
}
|
||||
if (favBusy.value) return
|
||||
favBusy.value = true
|
||||
favError.value = ''
|
||||
try {
|
||||
const res: any = favorited.value
|
||||
? await requestFetch(`/api/tender/favorites?projectId=${id}`, { method: 'DELETE' })
|
||||
: await requestFetch('/api/tender/favorites', {
|
||||
method: 'POST',
|
||||
body: { projectId: Number(id) }
|
||||
})
|
||||
const auth = await handleAuthCode(res, `/tender/${id}`)
|
||||
if (auth.handled) return
|
||||
if (res?.code === 0) {
|
||||
// 以后端返回的状态为准:不做乐观更新,失败时状态不乱
|
||||
favorited.value = !!res.data?.favorited
|
||||
} else {
|
||||
favError.value = res?.message || '操作失败,请稍后重试'
|
||||
}
|
||||
} catch (e: any) {
|
||||
favError.value = e?.data?.message || e?.message || '操作失败,请稍后重试'
|
||||
} finally {
|
||||
favBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const res: any = await $fetch(`/api/tender/detail?id=${id}`)
|
||||
const res: any = await requestFetch(`/api/tender/detail?id=${id}`)
|
||||
tender.value = res && res.id ? res : null
|
||||
favorited.value = !!res?.favorited
|
||||
if (!res) notFound.value = true
|
||||
} catch {
|
||||
notFound.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 未登录点收藏 → 登录 → 回跳带上 `favorite=1`,这里补一次。
|
||||
*
|
||||
* 三条约束:
|
||||
* 1. **只在客户端做**(onMounted):SSR 期间发写请求是服务端副作用,且可能被执行两次;
|
||||
* 2. **不改写 URL 去掉 `favorite=1`**:详情页壳 `app/pages/tender/[id].vue` 用
|
||||
* `:key="route.fullPath"`,改 query 会让模板组件整体重挂载并重新取数,
|
||||
* 那次重取会与刚发出的收藏请求赛跑;组件内的标记做「每次挂载只补一次」就够了;
|
||||
* 3. 失败**静默**退化成「未收藏」,让用户自己再点一次,不弹二次打扰(与 h5 一致)。
|
||||
*/
|
||||
onMounted(async () => {
|
||||
if (favPendingHandled.value) return
|
||||
if (route.query.favorite !== '1') return
|
||||
favPendingHandled.value = true
|
||||
if (!isLoggedIn()) return
|
||||
await addFavoriteQuietly()
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user