visual-novel-web/src/components/ui/rate.tsx

97 lines
2.5 KiB
TypeScript
Raw Normal View History

2025-10-28 07:59:26 +00:00
'use client';
import { useControllableValue } from 'ahooks';
import React, { useState } from 'react';
import { cn } from '@/lib';
import Image from 'next/image';
type RateProps = {
readonly?: boolean;
// 0 - 10
value?: number;
onChange?: (value: number) => void;
} & React.HTMLAttributes<HTMLDivElement>;
export default function Rate(props: RateProps) {
const { readonly = false, ...others } = props;
const [value = 0, onChange] = useControllableValue(props);
const [hoveredValue, setHoveredValue] = useState(0);
const readonlyRender = () => {
// value 范围 0-10每颗星代表 2 分
const fullCounts = Math.floor(value / 2); // 满星数量
const halfCount = value % 2; // 半星数量0 或 1
const emptyCount = 5 - fullCounts - halfCount; // 空星数量
const starDoms = [
// 满星
...Array.from({ length: fullCounts }, (_, i) => (
<Image
key={`full-${i}`}
src={'/component/star_full.svg'}
width={20}
height={20}
alt="full star"
/>
)),
// 半星
...Array.from({ length: halfCount }, (_, i) => (
<Image
key={`half-${i}`}
src={'/component/star_half.svg'}
width={20}
height={20}
alt="half star"
/>
)),
// 空星
];
return (
<>
{starDoms}
<span className="font-normal" style={{ color: 'rgba(255, 225, 0, 1)' }}>
{value}
</span>
</>
);
};
const editRender = () => {
const finalValue = hoveredValue || value;
// 将 0-10 的值转换为 0-5 的星级
const starValue = finalValue / 2;
return Array.from({ length: 5 }, (_, index) => {
let src = '/component/star_empty.svg';
// 判断当前星星应该显示什么状态
if (index < Math.floor(starValue)) {
// 满星
src = '/component/star_full.svg';
}
return (
<Image
key={index}
src={src}
width={20}
height={20}
alt="star"
className="cursor-pointer"
onClick={() => onChange?.((index + 1) * 2)}
onMouseEnter={() => setHoveredValue((index + 1) * 2)}
/>
);
});
};
const render = readonly ? readonlyRender : editRender;
return (
<div
{...others}
onMouseLeave={() => setHoveredValue(0)}
className={cn('flex items-center gap-1', props.className)}
>
{render()}
</div>
);
}