Files
Genarrative/src/components/game-distribution/useGameReviews.ts
T
lhk229 97751e4bbd
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 2m7s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m6s
Project CI / Backend tests (pull_request) Successful in 4m25s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 9m47s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 10m51s
Project CI / Frontend tests (pull_request) Successful in 2m9s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m15s
Project CI / Native shell tests (pull_request) Successful in 5m37s
Project CI / Repository checks (pull_request) Successful in 2m48s
实现网站游戏评分与评价
新增游戏唯一评分评价私有表、事务保存与公共分页统计
接入个人评价读取保存接口并同步前后端契约及生成绑定
在网站详情页实现评分评论、编辑取消、字数统计和请求隔离
补齐定向测试、真实数据库接口验收脚本及文档证据
2026-09-30 23:28:53 +08:00

181 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useRef, useState } from 'react';
import {
type GameDistributionRatingSummary,
type GameDistributionReview,
type GameDistributionReviewsResponse,
getMyGameReview,
listGameReviews,
saveMyGameReview,
} from '../../services/gameDistributionClient';
export const GAME_REVIEW_COMMENT_LIMIT = 4000;
export function normalizeReviewNewlines(comment: string) {
return comment.replace(/\r\n?/g, '\n');
}
export function reviewCommentLength(comment: string) {
return Array.from(normalizeReviewNewlines(comment)).length;
}
function errorMessage(error: unknown, fallback: string) {
return error instanceof Error ? error.message : fallback;
}
/** 外层以游戏和账号身份为 key,切换上下文会卸载并清空整个评价草稿。 */
export function useGameReviews(gameId: string, canReview: boolean) {
const [summary, setSummary] = useState<GameDistributionRatingSummary | null>(
null,
);
const [list, setList] = useState<GameDistributionReviewsResponse | null>(
null,
);
const [page, setPage] = useState(1);
const [listLoading, setListLoading] = useState(true);
const [listError, setListError] = useState('');
const [myReview, setMyReview] = useState<GameDistributionReview | null>(null);
const [myLoading, setMyLoading] = useState(canReview);
const [myError, setMyError] = useState('');
const [editing, setEditing] = useState(false);
const [score, setScore] = useState<number | null>(null);
const [comment, setComment] = useState('');
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState('');
const listSerial = useRef(0);
const mySerial = useRef(0);
const alive = useRef(true);
const savingRef = useRef(false);
const pageRef = useRef(1);
const loadPage = useCallback(
async (nextPage: number) => {
const serial = ++listSerial.current;
pageRef.current = nextPage;
setPage(nextPage);
setListLoading(true);
setListError('');
try {
const response = await listGameReviews(gameId, nextPage);
if (!alive.current || serial !== listSerial.current) return;
setList(response);
setSummary(response.ratingSummary);
} catch (error) {
if (!alive.current || serial !== listSerial.current) return;
setListError(errorMessage(error, '读取游戏评价失败'));
} finally {
if (alive.current && serial === listSerial.current)
setListLoading(false);
}
},
[gameId],
);
const loadMine = useCallback(async () => {
if (!canReview) return;
const serial = ++mySerial.current;
setMyLoading(true);
setMyError('');
try {
const response = await getMyGameReview(gameId);
if (!alive.current || serial !== mySerial.current) return;
setMyReview(response.review);
} catch (error) {
if (!alive.current || serial !== mySerial.current) return;
setMyError(errorMessage(error, '读取我的评价失败'));
} finally {
if (alive.current && serial === mySerial.current) setMyLoading(false);
}
}, [canReview, gameId]);
useEffect(() => {
alive.current = true;
void loadPage(1);
void loadMine();
return () => {
alive.current = false;
listSerial.current += 1;
mySerial.current += 1;
};
}, [loadMine, loadPage]);
function edit() {
if (!myReview || savingRef.current) return;
setScore(myReview.score);
setComment(myReview.comment);
setSaveError('');
setEditing(true);
}
function cancel() {
if (savingRef.current) return;
setScore(null);
setComment('');
setSaveError('');
setEditing(false);
}
async function submit() {
if (savingRef.current || !canReview || myLoading || myError) return;
const normalizedComment = normalizeReviewNewlines(comment);
if (score === null || !Number.isInteger(score) || score < 1 || score > 10) {
setSaveError('请选择 1–10 分');
return;
}
if (reviewCommentLength(normalizedComment) > GAME_REVIEW_COMMENT_LIMIT) {
setSaveError('评论不能超过 4000 个字符');
return;
}
const wasNew = myReview === null;
savingRef.current = true;
setSaving(true);
setSaveError('');
try {
const response = await saveMyGameReview(gameId, {
score,
comment: normalizedComment,
});
if (!alive.current) return;
// 保存成功后,先隔离所有旧读取,再使用权威记录与摘要。
mySerial.current += 1;
listSerial.current += 1;
setMyLoading(false);
setMyError('');
setMyReview(response.review);
setSummary(response.ratingSummary);
setEditing(false);
setScore(null);
setComment('');
void loadPage(wasNew ? 1 : pageRef.current);
} catch (error) {
if (alive.current) setSaveError(errorMessage(error, '保存评价失败'));
} finally {
savingRef.current = false;
if (alive.current) setSaving(false);
}
}
return {
summary,
list,
page,
listLoading,
listError,
loadPage,
myReview,
myLoading,
myError,
loadMine,
editing,
edit,
cancel,
score,
setScore,
comment,
setComment,
saving,
saveError,
submit,
};
}