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
新增游戏唯一评分评价私有表、事务保存与公共分页统计 接入个人评价读取保存接口并同步前后端契约及生成绑定 在网站详情页实现评分评论、编辑取消、字数统计和请求隔离 补齐定向测试、真实数据库接口验收脚本及文档证据
181 lines
5.2 KiB
TypeScript
181 lines
5.2 KiB
TypeScript
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,
|
||
};
|
||
}
|