feat(listings): 足迹与收藏改为账号同步并新增后端表与接口
This commit is contained in:
@@ -1,28 +1,88 @@
|
||||
import { useState } from 'react'
|
||||
import { App, Button, Card, Empty, Segmented, Space, Tag, Typography } from 'antd'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { App, Button, Card, Empty, Segmented, Skeleton, Space, Tag, Typography } from 'antd'
|
||||
import { DeleteOutlined, StarFilled, StarOutlined } from '@ant-design/icons'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import {
|
||||
useListingCollections,
|
||||
type CollectionItem,
|
||||
} from '@/features/listings/collections'
|
||||
addRemoteFavorite,
|
||||
clearRemoteHistory,
|
||||
fetchRemoteCollections,
|
||||
removeRemoteFavorite,
|
||||
removeRemoteHistory,
|
||||
} from '@/features/listings/collectionsApi'
|
||||
import { useListingCollections } from '@/features/listings/collections'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
import { formatDateTime, formatMoney } from '@/shared/utils/format'
|
||||
|
||||
type TabKey = 'history' | 'favorites'
|
||||
type TabKey = 'history' | 'favorite'
|
||||
|
||||
function timeText(item: CollectionItem, tab: TabKey) {
|
||||
return formatDateTime(tab === 'favorites' ? item.favorited_at : item.viewed_at)
|
||||
interface RowListing {
|
||||
id: number
|
||||
listing_no: string
|
||||
title: string
|
||||
cover_url: string
|
||||
price_cent: number
|
||||
deposit_amount_cent: number
|
||||
haf_coin_amount: number
|
||||
server_region: string
|
||||
login_platform: string
|
||||
rank_level: string
|
||||
in_transaction: boolean
|
||||
status: string
|
||||
}
|
||||
|
||||
interface RowItem {
|
||||
listing: RowListing
|
||||
occurredAt?: string
|
||||
}
|
||||
|
||||
export function ListingCollectionsPage() {
|
||||
const navigate = useNavigate()
|
||||
const { message, modal } = App.useApp()
|
||||
const { history, favorites, isFavorite, toggleFavorite, removeHistory, removeFavorite, clearHistory } =
|
||||
useListingCollections()
|
||||
const queryClient = useQueryClient()
|
||||
const token = useSessionStore(state => state.token)
|
||||
const local = useListingCollections()
|
||||
const [tab, setTab] = useState<TabKey>('history')
|
||||
|
||||
const items = tab === 'favorites' ? favorites : history
|
||||
const remote = useQuery({
|
||||
queryKey: ['listing-collections'],
|
||||
queryFn: async () => {
|
||||
const [history, favorites] = await Promise.all([
|
||||
fetchRemoteCollections('history'),
|
||||
fetchRemoteCollections('favorite'),
|
||||
])
|
||||
return { history: history.items, favorites: favorites.items, counts: history.counts }
|
||||
},
|
||||
enabled: Boolean(token),
|
||||
})
|
||||
|
||||
const remoteHistory: RowItem[] = (remote.data?.history ?? []).map(item => ({
|
||||
listing: item.listing,
|
||||
occurredAt: item.occurred_at,
|
||||
}))
|
||||
const remoteFavorites: RowItem[] = (remote.data?.favorites ?? []).map(item => ({
|
||||
listing: item.listing,
|
||||
occurredAt: item.occurred_at,
|
||||
}))
|
||||
const localHistory: RowItem[] = local.history.map(item => ({
|
||||
listing: item.listing,
|
||||
occurredAt: item.viewed_at,
|
||||
}))
|
||||
const localFavorites: RowItem[] = local.favorites.map(item => ({
|
||||
listing: item.listing,
|
||||
occurredAt: item.favorited_at,
|
||||
}))
|
||||
|
||||
const historyItems = token ? remoteHistory : localHistory
|
||||
const favoriteItems = token ? remoteFavorites : localFavorites
|
||||
const historyCount = token ? remote.data?.counts.history ?? 0 : local.history.length
|
||||
const favoriteCount = token ? remote.data?.counts.favorite ?? 0 : local.favorites.length
|
||||
const favoriteIDs = new Set(favoriteItems.map(item => item.listing.id))
|
||||
const items = tab === 'favorite' ? favoriteItems : historyItems
|
||||
const loading = Boolean(token) && remote.isPending
|
||||
|
||||
const refresh = () => queryClient.invalidateQueries({ queryKey: ['listing-collections'] })
|
||||
|
||||
const onClear = () => {
|
||||
modal.confirm({
|
||||
@@ -31,26 +91,47 @@ export function ListingCollectionsPage() {
|
||||
okText: '清空',
|
||||
okButtonProps: { danger: true },
|
||||
cancelText: '取消',
|
||||
onOk: () => {
|
||||
clearHistory()
|
||||
onOk: async () => {
|
||||
if (token) {
|
||||
await clearRemoteHistory()
|
||||
await refresh()
|
||||
} else {
|
||||
local.clearHistory()
|
||||
}
|
||||
message.success('浏览记录已清空')
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const onToggle = (item: CollectionItem) => {
|
||||
const added = toggleFavorite(item.listing)
|
||||
message.success(added ? '已收藏' : '已取消收藏')
|
||||
const onToggle = async (item: RowItem) => {
|
||||
const favorited = favoriteIDs.has(item.listing.id)
|
||||
if (token) {
|
||||
if (favorited) {
|
||||
await removeRemoteFavorite(item.listing.id)
|
||||
} else {
|
||||
await addRemoteFavorite(item.listing.id)
|
||||
}
|
||||
await refresh()
|
||||
} else {
|
||||
local.toggleFavorite(item.listing)
|
||||
}
|
||||
message.success(favorited ? '已取消收藏' : '已收藏')
|
||||
}
|
||||
|
||||
const onRemove = (item: CollectionItem) => {
|
||||
if (tab === 'favorites') {
|
||||
removeFavorite(item.listing.id)
|
||||
message.success('已取消收藏')
|
||||
return
|
||||
const onRemove = async (item: RowItem) => {
|
||||
if (token) {
|
||||
if (tab === 'favorite') {
|
||||
await removeRemoteFavorite(item.listing.id)
|
||||
} else {
|
||||
await removeRemoteHistory(item.listing.id)
|
||||
}
|
||||
await refresh()
|
||||
} else if (tab === 'favorite') {
|
||||
local.removeFavorite(item.listing.id)
|
||||
} else {
|
||||
local.removeHistory(item.listing.id)
|
||||
}
|
||||
removeHistory(item.listing.id)
|
||||
message.success('已移除记录')
|
||||
message.success(tab === 'favorite' ? '已取消收藏' : '已移除记录')
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -62,10 +143,12 @@ export function ListingCollectionsPage() {
|
||||
我的足迹
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph type="secondary">
|
||||
找回看过和收藏过的账号,数据保存在当前浏览器。
|
||||
{token
|
||||
? '浏览记录与收藏已同步到账号,换设备登录也能找回。'
|
||||
: '当前未登录,记录仅保存在本浏览器;登录后会同步到账号。'}
|
||||
</Typography.Paragraph>
|
||||
</div>
|
||||
{tab === 'history' && history.length > 0 ? (
|
||||
{tab === 'history' && historyCount > 0 ? (
|
||||
<Button icon={<DeleteOutlined />} onClick={onClear}>
|
||||
清空记录
|
||||
</Button>
|
||||
@@ -77,16 +160,18 @@ export function ListingCollectionsPage() {
|
||||
value={tab}
|
||||
onChange={value => setTab(value as TabKey)}
|
||||
options={[
|
||||
{ label: `浏览记录 ${history.length}`, value: 'history' },
|
||||
{ label: `收藏账号 ${favorites.length}`, value: 'favorites' },
|
||||
{ label: `浏览记录 ${historyCount}`, value: 'history' },
|
||||
{ label: `收藏账号 ${favoriteCount}`, value: 'favorite' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{items.length === 0 ? (
|
||||
{loading ? (
|
||||
<Card className="collections-page__empty">
|
||||
<Empty
|
||||
description={tab === 'favorites' ? '暂无收藏账号' : '暂无浏览记录'}
|
||||
>
|
||||
<Skeleton active paragraph={{ rows: 4 }} />
|
||||
</Card>
|
||||
) : items.length === 0 ? (
|
||||
<Card className="collections-page__empty">
|
||||
<Empty description={tab === 'favorite' ? '暂无收藏账号' : '暂无浏览记录'}>
|
||||
<Button type="primary" onClick={() => navigate('/')}>
|
||||
去逛柜选
|
||||
</Button>
|
||||
@@ -95,8 +180,8 @@ export function ListingCollectionsPage() {
|
||||
) : (
|
||||
<div className="collections-list">
|
||||
{items.map(item => (
|
||||
<Card key={item.listing.id} className="collections-item" hoverable={false}>
|
||||
<div className="collections-item__time">{timeText(item, tab)}</div>
|
||||
<Card key={item.listing.id} className="collections-item">
|
||||
<div className="collections-item__time">{formatDateTime(item.occurredAt)}</div>
|
||||
<div className="collections-item__body">
|
||||
<button
|
||||
type="button"
|
||||
@@ -145,12 +230,12 @@ export function ListingCollectionsPage() {
|
||||
<Space size={8} wrap>
|
||||
<Button
|
||||
size="small"
|
||||
icon={isFavorite(item.listing.id) ? <StarFilled /> : <StarOutlined />}
|
||||
onClick={() => onToggle(item)}
|
||||
icon={favoriteIDs.has(item.listing.id) ? <StarFilled /> : <StarOutlined />}
|
||||
onClick={() => void onToggle(item)}
|
||||
>
|
||||
{isFavorite(item.listing.id) ? '已收藏' : '收藏'}
|
||||
{favoriteIDs.has(item.listing.id) ? '已收藏' : '收藏'}
|
||||
</Button>
|
||||
<Button size="small" type="text" onClick={() => onRemove(item)}>
|
||||
<Button size="small" type="text" onClick={() => void onRemove(item)}>
|
||||
移除
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
@@ -29,6 +30,12 @@ import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import { BrandLogo } from '@/components/BrandLogo'
|
||||
import { useListingDetail } from '@/features/listings/api'
|
||||
import { recordListingView, useListingCollections } from '@/features/listings/collections'
|
||||
import {
|
||||
addRemoteFavorite,
|
||||
fetchFavoriteIDs,
|
||||
recordRemoteView,
|
||||
removeRemoteFavorite,
|
||||
} from '@/features/listings/collectionsApi'
|
||||
import {
|
||||
createOrder,
|
||||
useOrderAgreements,
|
||||
@@ -65,10 +72,16 @@ export function ListingDetailPage() {
|
||||
const navigate = useNavigate()
|
||||
const { message } = App.useApp()
|
||||
const { modal } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const { data, isPending, isError, refetch } = useListingDetail(id)
|
||||
const { isFavorite, toggleFavorite } = useListingCollections()
|
||||
const user = useSessionStore(state => state.user)
|
||||
const token = useSessionStore(state => state.token)
|
||||
const favoriteIDsQuery = useQuery({
|
||||
queryKey: ['listing-favorite-ids'],
|
||||
queryFn: fetchFavoriteIDs,
|
||||
enabled: Boolean(token),
|
||||
})
|
||||
const agreements = useOrderAgreements()
|
||||
const postRentalNotice = usePostRentalNotice()
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
@@ -79,8 +92,15 @@ export function ListingDetailPage() {
|
||||
const [agreementContent, setAgreementContent] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (data) recordListingView(data)
|
||||
}, [data])
|
||||
if (!data) return
|
||||
if (token) {
|
||||
void recordRemoteView(data.id)
|
||||
.then(() => queryClient.invalidateQueries({ queryKey: ['listing-collections'] }))
|
||||
.catch(() => undefined)
|
||||
return
|
||||
}
|
||||
recordListingView(data)
|
||||
}, [data, token, queryClient])
|
||||
|
||||
const showAgreement = (title: string, content?: string) => {
|
||||
setAgreementTitle(title)
|
||||
@@ -170,6 +190,23 @@ export function ListingDetailPage() {
|
||||
)
|
||||
const onlineTime = data.asset_summary?.online_time
|
||||
const isOwner = user?.id === data.owner_id
|
||||
const favorited = token ? Boolean(favoriteIDsQuery.data?.has(data.id)) : isFavorite(data.id)
|
||||
|
||||
const toggleFavoriteState = async () => {
|
||||
if (token) {
|
||||
if (favorited) {
|
||||
await removeRemoteFavorite(data.id)
|
||||
} else {
|
||||
await addRemoteFavorite(data.id)
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: ['listing-favorite-ids'] })
|
||||
await queryClient.invalidateQueries({ queryKey: ['listing-collections'] })
|
||||
message.success(favorited ? '已取消收藏' : '已收藏账号')
|
||||
return
|
||||
}
|
||||
const added = toggleFavorite(data)
|
||||
message.success(added ? '已收藏账号' : '已取消收藏')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="listing-detail-page">
|
||||
@@ -221,13 +258,10 @@ export function ListingDetailPage() {
|
||||
</Tag>
|
||||
<Button
|
||||
size="small"
|
||||
icon={isFavorite(data.id) ? <StarFilled /> : <StarOutlined />}
|
||||
onClick={() => {
|
||||
const added = toggleFavorite(data)
|
||||
message.success(added ? '已收藏账号' : '已取消收藏')
|
||||
}}
|
||||
icon={favorited ? <StarFilled /> : <StarOutlined />}
|
||||
onClick={() => void toggleFavoriteState()}
|
||||
>
|
||||
{isFavorite(data.id) ? '已收藏' : '收藏'}
|
||||
{favorited ? '已收藏' : '收藏'}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { apiClient, unwrapData } from '@/shared/api/client'
|
||||
import type { ApiResponse, Listing } from '@/shared/api/types'
|
||||
|
||||
export type CollectionKind = 'history' | 'favorite'
|
||||
|
||||
export interface RemoteCollectionItem {
|
||||
listing: Listing
|
||||
occurred_at: string
|
||||
}
|
||||
|
||||
export interface RemoteCollectionCounts {
|
||||
history: number
|
||||
favorite: number
|
||||
}
|
||||
|
||||
export interface RemoteCollectionList {
|
||||
items: RemoteCollectionItem[]
|
||||
counts: RemoteCollectionCounts
|
||||
}
|
||||
|
||||
export function fetchRemoteCollections(kind: CollectionKind) {
|
||||
return unwrapData<RemoteCollectionList>(
|
||||
apiClient.get<ApiResponse<RemoteCollectionList>>('/me/listing-collections', {
|
||||
params: { kind },
|
||||
})
|
||||
).then(data => ({
|
||||
items: data.items ?? [],
|
||||
counts: data.counts ?? { history: 0, favorite: 0 },
|
||||
}))
|
||||
}
|
||||
|
||||
export async function recordRemoteView(listingID: number) {
|
||||
await apiClient.post('/me/listing-collections/view', { listing_id: listingID })
|
||||
}
|
||||
|
||||
export async function addRemoteFavorite(listingID: number) {
|
||||
await apiClient.post('/me/listing-collections/favorites', { listing_id: listingID })
|
||||
}
|
||||
|
||||
export async function removeRemoteFavorite(listingID: number) {
|
||||
await apiClient.delete(`/me/listing-collections/favorites/${listingID}`)
|
||||
}
|
||||
|
||||
export async function removeRemoteHistory(listingID: number) {
|
||||
await apiClient.delete(`/me/listing-collections/history/${listingID}`)
|
||||
}
|
||||
|
||||
export async function clearRemoteHistory() {
|
||||
await apiClient.delete('/me/listing-collections/history')
|
||||
}
|
||||
|
||||
/** 收藏状态下拉用:只取收藏列表的 id 集合。 */
|
||||
export async function fetchFavoriteIDs() {
|
||||
const data = await fetchRemoteCollections('favorite')
|
||||
return new Set(data.items.map(item => item.listing.id))
|
||||
}
|
||||
Reference in New Issue
Block a user