190 lines
6.5 KiB
TypeScript
190 lines
6.5 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
|
import { listFeedbackBoard } from '../api/client';
|
|
import type { FeedbackSubmission, PortalUser } from '../types';
|
|
import {
|
|
FEEDBACK_STATUS_LABELS,
|
|
FEEDBACK_TYPE_LABELS,
|
|
formatFeedbackTime,
|
|
} from '../utils/feedbackLabels';
|
|
import { FeedbackPageHeader } from './FeedbackPageHeader';
|
|
|
|
const PAGE_SIZE = 10;
|
|
|
|
function FeedbackBoardItem({
|
|
item,
|
|
currentUserId,
|
|
onOpen,
|
|
}: {
|
|
item: FeedbackSubmission;
|
|
currentUserId?: string;
|
|
onOpen: (id: string) => void;
|
|
}) {
|
|
const isMine = item.userId === currentUserId;
|
|
return (
|
|
<li className="feedback-board-item">
|
|
<button type="button" className="feedback-board-item-trigger" onClick={() => onOpen(item.id)}>
|
|
<div className="feedback-board-item-main">
|
|
<div className="feedback-list-item-badges">
|
|
<span className={`feedback-badge feedback-badge-type feedback-badge-type-${item.type}`}>
|
|
{FEEDBACK_TYPE_LABELS[item.type]}
|
|
</span>
|
|
<span className={`feedback-badge feedback-badge-status feedback-badge-status-${item.status}`}>
|
|
{FEEDBACK_STATUS_LABELS[item.status]}
|
|
</span>
|
|
{isMine ? <span className="feedback-badge feedback-badge-mine">我的</span> : null}
|
|
</div>
|
|
<strong className="feedback-board-item-title">{item.title}</strong>
|
|
{item.description ? <p className="feedback-board-item-preview">{item.description}</p> : null}
|
|
</div>
|
|
<div className="feedback-board-item-meta">
|
|
<span>{item.submitterDisplayName ?? '用户'}</span>
|
|
<time dateTime={new Date(item.createdAt).toISOString()}>{formatFeedbackTime(item.createdAt)}</time>
|
|
{(item.imageCount ?? 0) > 0 ? <span>{item.imageCount} 图</span> : null}
|
|
<span className="feedback-list-item-chevron" aria-hidden="true">
|
|
▸
|
|
</span>
|
|
</div>
|
|
</button>
|
|
</li>
|
|
);
|
|
}
|
|
|
|
export function FeedbackBoardView({
|
|
user,
|
|
onLogout,
|
|
}: {
|
|
user?: PortalUser | null;
|
|
onLogout?: () => void;
|
|
}) {
|
|
const navigate = useNavigate();
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const page = Math.max(Number(searchParams.get('page')) || 1, 1);
|
|
|
|
const [items, setItems] = useState<FeedbackSubmission[]>([]);
|
|
const [totalPages, setTotalPages] = useState(0);
|
|
const [total, setTotal] = useState(0);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const loadPage = useCallback(async (targetPage: number) => {
|
|
setLoading(true);
|
|
setError(null);
|
|
try {
|
|
const result = await listFeedbackBoard(targetPage, PAGE_SIZE);
|
|
setItems(result.items);
|
|
setTotalPages(result.totalPages);
|
|
setTotal(result.total);
|
|
if (result.totalPages > 0 && targetPage > result.totalPages) {
|
|
setSearchParams({ page: String(result.totalPages) }, { replace: true });
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : '反馈列表加载失败');
|
|
setItems([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [setSearchParams]);
|
|
|
|
useEffect(() => {
|
|
void loadPage(page);
|
|
}, [loadPage, page]);
|
|
|
|
const goToPage = (nextPage: number) => {
|
|
if (nextPage < 1 || (totalPages > 0 && nextPage > totalPages) || nextPage === page) return;
|
|
setSearchParams({ page: String(nextPage) });
|
|
};
|
|
|
|
return (
|
|
<div className="feedback-submit-page feedback-board-page">
|
|
<FeedbackPageHeader
|
|
title="用户反馈"
|
|
onBack={() => navigate('/feedback')}
|
|
onLogout={onLogout}
|
|
/>
|
|
|
|
<main className="feedback-submit-main feedback-board-main">
|
|
<section className="feedback-submit-card feedback-board-card">
|
|
<div className="feedback-board-toolbar">
|
|
<div className="feedback-board-toolbar-copy">
|
|
<p className="feedback-submit-eyebrow">Bug List</p>
|
|
<h1>全部用户反馈</h1>
|
|
<p className="feedback-submit-desc feedback-board-desc">
|
|
共 {total} 条 · 每页 {PAGE_SIZE} 条
|
|
</p>
|
|
</div>
|
|
<div className="feedback-board-toolbar-actions">
|
|
<button
|
|
type="button"
|
|
className="feedback-panel-close"
|
|
aria-label="提交 Bug 或需求"
|
|
onClick={() => navigate('/feedback')}
|
|
>
|
|
提交 Bug 或需求
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="feedback-panel-close"
|
|
aria-label="关闭反馈"
|
|
onClick={() => navigate('/')}
|
|
>
|
|
关闭
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{!loading && total > 0 ? (
|
|
<p className="feedback-board-page-indicator">第 {page} / {totalPages || 1} 页</p>
|
|
) : null}
|
|
|
|
<div className="feedback-board-scroll">
|
|
{loading ? <p className="feedback-list-muted">加载中…</p> : null}
|
|
{error ? <p className="feedback-submit-error">{error}</p> : null}
|
|
|
|
{!loading && !error && items.length === 0 ? (
|
|
<p className="feedback-list-empty">还没有任何用户反馈。</p>
|
|
) : null}
|
|
|
|
{!loading && items.length > 0 ? (
|
|
<ul className="feedback-board-list">
|
|
{items.map((item) => (
|
|
<FeedbackBoardItem
|
|
key={item.id}
|
|
item={item}
|
|
currentUserId={user?.id}
|
|
onOpen={(id) => navigate(`/feedback/${id}`)}
|
|
/>
|
|
))}
|
|
</ul>
|
|
) : null}
|
|
</div>
|
|
|
|
{totalPages > 1 ? (
|
|
<nav className="feedback-pagination" aria-label="反馈分页">
|
|
<button
|
|
type="button"
|
|
className="ghost-btn"
|
|
disabled={loading || page <= 1}
|
|
onClick={() => goToPage(page - 1)}
|
|
>
|
|
上一页
|
|
</button>
|
|
<span className="feedback-pagination-status">
|
|
第 {page} / {totalPages} 页
|
|
</span>
|
|
<button
|
|
type="button"
|
|
className="ghost-btn"
|
|
disabled={loading || page >= totalPages}
|
|
onClick={() => goToPage(page + 1)}
|
|
>
|
|
下一页
|
|
</button>
|
|
</nav>
|
|
) : null}
|
|
</section>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|