feat(feedback): 新增用户反馈提交、分页列表与语音描述
支持 Bug/需求提交、截图与聊天同款语音输入,全站反馈分页浏览与详情页,并从聊天与空间页提供入口。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
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>
|
||||
<button
|
||||
type="button"
|
||||
className="feedback-panel-close"
|
||||
aria-label="关闭反馈"
|
||||
onClick={() => navigate('/')}
|
||||
>
|
||||
关闭
|
||||
</button>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user