System Design: Designing the StarWidget
System Design: Designing the StarWidgetApril 29, 2024

To create the StarComponent based on the provided plan, here's a breakdown of each section with implementation guidance:

1. General Requirements

  • Widget Purpose: The component serves as a widget for displaying and creating star-based ratings.
  • Reusable: It should be implemented in a way that can be easily reused in other parts of an application.

2. Functional Requirements

  • Customization: The component should allow customization options (e.g., color, number of stars).
  • Optional Score Label: Displaying a score label (e.g., numeric representation) should be optional.
  • Average Score Calculation: It should support calculations for average scores, if required.

3. UI

  • Display Format: Show a row of stars with an optional score label on the side.
  • Partial Stars: Support partial stars for fractional ratings.
  • Submit Button: Include a submit button if the component is being used to capture ratings.

4. Props/State Design

  • Props:
    • isReadOnly: boolean - Determines if the component is in read-only mode.
    • hideScore: boolean - Optionally hides the score display.
    • score: number - The current score to be displayed.
    • onClick: (value: number) => void - Callback for when a star is clicked.
    • onSubmit: (value: number) => void - Callback for submitting the score.
    • renderStar: (value: number) => HTMLElement - Custom rendering function for stars.
    • maxStar: number - The maximum number of stars (default is 5).
  • State:
    • score: number - Represents the selected score.
    • onChange: (value: number) => void - Handler for updating the score.

5. Implementation

  • Score Calculation: Calculate the display score based on value and max.
  • Star Rendering: Render stars based on the score, supporting both full and partial stars by controlling width dynamically.
  • Highlighting Stars: Highlight stars based on the rating score.
  • Partial Star Handling: Implement overflow with a partially transparent or white base to show partial stars.
  • Event Callbacks: Provide onChange and onSubmit callbacks to handle interactions.

6. Optimization

  • CSS Usage: Use more CSS to style the component, minimizing JavaScript where possible.
  • CSS Naming: Use clear and non-nested class names for CSS.
  • Inline Critical CSS: Embed essential styles directly if the component will be published or used externally.
  • NPM Package: Prepare the component for publishing as an npm package if needed.

7. Accessibility

  • Theme Support: Ensure the component can adapt to different themes.
  • ARIA Labels: Use aria-live attributes for screen reader support.
  • Sizing Units: Use rem and em units for scalable sizing and to support different screen resolutions.

Sample Code Implementation

Here's a basic example of how the structure of the StarComponent might look:

import React, { useState } from 'react';

const StarComponent = ({
  isReadOnly = false,
  hideScore = false,
  score = 0,
  maxStar = 5,
  onClick,
  onSubmit,
  renderStar,
}) => {
  const [currentScore, setCurrentScore] = useState(score);

  const handleClick = (value) => {
    if (!isReadOnly) {
      setCurrentScore(value);
      if (onClick) onClick(value);
    }
  };

  const handleSubmit = () => {
    if (onSubmit) onSubmit(currentScore);
  };

  return (
    <div className="star-component">
      {!hideScore && <span className="score-label">{currentScore}</span>}
      <div className="stars">
        {[...Array(maxStar)].map((_, index) => {
          const value = index + 1;
          return (
            <div
              key={value}
              className={`star ${value <= currentScore ? 'filled' : ''}`}
              onClick={() => handleClick(value)}
              style={{ width: value <= currentScore ? '100%' : `${(currentScore % 1) * 100}%` }}
            >
              {renderStar ? renderStar(value) : '★'}
            </div>
          );
        })}
      </div>
      {!isReadOnly && (
        <button onClick={handleSubmit}>Submit</button>
      )}
    </div>
  );
};

export default StarComponent;

CSS Example

Here's some CSS that aligns with the optimization guidelines:

.star-component {
  display: inline-flex;
  align-items: center;
}

.score-label {
  margin-right: 8px;
  font-size: 1rem;
}

.stars {
  display: flex;
}

.star {
  cursor: pointer;
  font-size: 2rem;
  overflow: hidden;
}

.star.filled {
  color: gold;
}

This structure covers the key requirements, including customizable props, star rendering logic, score display, and callbacks for interaction. Make sure to refine CSS and JavaScript further based on specific project requirements and accessibility standards.

如果需要在 StarComponent 中发出请求并获取数据,可以在组件挂载时使用 useEffect 钩子来请求数据(例如获取初始评分),然后在提交评分时再发出提交请求。以下是一个完整的示例,展示如何在组件挂载时获取数据,并在用户提交评分时发送请求。

示例:在 StarComponent 中发出请求和获取数据

假设我们要获取和提交评分数据到 /api/getRating/api/submitRating 端点。

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const StarComponent = ({
  isReadOnly = false,
  hideScore = false,
  initialScore = 0,
  maxStar = 5,
  onClick,
  onSubmit,
  renderStar,
}) => {
  const [currentScore, setCurrentScore] = useState(initialScore);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [fetchLoading, setFetchLoading] = useState(true);

  // 获取初始评分数据
  useEffect(() => {
    const fetchInitialScore = async () => {
      try {
        setFetchLoading(true);
        const response = await axios.get('/api/getRating');
        setCurrentScore(response.data.score); // 假设服务器返回的数据包含 `score` 字段
      } catch (error) {
        console.error('获取评分数据失败:', error);
        setError('无法加载评分数据');
      } finally {
        setFetchLoading(false);
      }
    };

    fetchInitialScore();
  }, []);

  const handleClick = (value) => {
    if (!isReadOnly) {
      setCurrentScore(value);
      if (onClick) onClick(value);
    }
  };

  const handleSubmit = async () => {
    if (onSubmit) onSubmit(currentScore);

    setLoading(true);
    setError(null);
    
    try {
      // 提交评分数据
      const response = await axios.post('/api/submitRating', { score: currentScore });
      console.log('评分提交成功:', response.data);
      // 可以在这里添加成功处理逻辑,例如显示成功消息
    } catch (error) {
      console.error('评分提交失败:', error);
      setError('提交评分失败,请重试');
    } finally {
      setLoading(false);
    }
  };

  if (fetchLoading) {
    return <p>加载中...</p>; // 显示加载状态
  }

  return (
    <div className="star-component">
      {!hideScore && <span className="score-label">{currentScore}</span>}
      <div className="stars">
        {[...Array(maxStar)].map((_, index) => {
          const value = index + 1;
          return (
            <div
              key={value}
              className={`star ${value <= currentScore ? 'filled' : ''}`}
              onClick={() => handleClick(value)}
              style={{ width: value <= currentScore ? '100%' : `${(currentScore % 1) * 100)%` }}
            >
              {renderStar ? renderStar(value) : '★'}
            </div>
          );
        })}
      </div>
      {!isReadOnly && (
        <button onClick={handleSubmit} disabled={loading}>
          {loading ? '提交中...' : '提交'}
        </button>
      )}
      {error && <p className="error-message">{error}</p>}
    </div>
  );
};

export default StarComponent;

解释

  1. 获取初始评分数据

    • 在组件挂载时(即 useEffect 中),我们调用 fetchInitialScore 函数发送一个 GET 请求到 /api/getRating
    • 如果请求成功,设置 currentScore 为服务器返回的评分数据。如果请求失败,则设置 error 状态以显示错误消息。
    • fetchLoading 用来控制加载状态,确保在数据加载完成之前显示"加载中..."消息。
  2. 评分点击处理

    • handleClick 函数用于在用户点击星星时更新评分,如果组件不是只读模式,则更新评分并触发 onClick 回调(如果提供)。
  3. 提交评分数据

    • handleSubmit 函数用于在用户点击"提交"按钮时发送 POST 请求到 /api/submitRating,提交当前评分数据。
    • 请求期间,禁用提交按钮,并在请求完成或失败时恢复。
    • 如果请求成功,可以添加额外的逻辑,例如显示成功消息;如果失败,显示错误消息。
  4. 加载状态

    • fetchLoadingtrue 时,显示"加载中..."文本,直到初始数据加载完成。
  5. 错误处理

    • 如果获取初始评分或提交评分时出错,显示错误消息。

备注

  • 需要确保 /api/getRating/api/submitRating 端点在后端已配置并能处理相应的请求。
  • 可以根据实际需求调整组件的样式和逻辑,比如处理评分变化的视觉效果、不同分数显示等。