unnamed-ui
气泡/容器

Feedback

Composed feedback form for reporting issues and collecting feedback

反馈组件示例

有什么问题?
"use client";

import * as React from "react";
import { FeedbackComposed } from "@/components/composed/feedback/feedback";

export function FeedbackDemo() {
  const [selectedOption, setSelectedOption] = React.useState<string>("");
  const [inputValue, setInputValue] = React.useState("");

  const feedbackOptions = [
    { id: "harmful", label: "有害/不安全" },
    { id: "false", label: "信息虚假" },
    { id: "inappropriate", label: "内容不当" },
    { id: "other", label: "其他" },
  ];

  const handleSubmit = () => {
    console.log("提交反馈:", {
      option: selectedOption,
      input: inputValue,
    });
    // 这里可以添加提交逻辑
  };

  return (
    <div className="flex flex-col gap-6 w-full max-w-md">
      <div>
        <h3 className="text-sm font-medium text-[var(--Text-text-secondary)] mb-4">
          反馈组件示例
        </h3>
        <FeedbackComposed
          title="有什么问题?"
          options={feedbackOptions}
          selectedId={selectedOption}
          onSelect={setSelectedOption}
          inputValue={inputValue}
          onInputChange={setInputValue}
          onClose={() => console.log("关闭")}
          onSubmit={handleSubmit}
        />
      </div>
    </div>
  );
}

Feedback 反馈表单组件用于收集用户反馈,支持多选项按钮、详细描述输入和表单提交,适用于内容举报、问题反馈、满意度调查等场景。

概述

  • 反馈选项:支持多个反馈类型选项,用户可快速选择问题类型(默认为多选模式)
  • 详细描述:提供输入框收集详细的反馈信息,可配置 showInputWhenSelected 实现仅选中指定选项(如「其他」)时才显示
  • 表单提交:内置提交按钮和表单处理逻辑
  • 多选/单选:支持多选和单选两种模式,默认为多选模式
  • 受控/非受控:支持受控和非受控两种使用模式
  • 可关闭:提供关闭按钮,方便用户退出反馈界面
  • 灵活定制:可自定义标题、选项、占位符和提交按钮文本
  • 按需显示输入框showInputWhenSelected 可配置仅在选中指定选项时显示输入框

快速开始

多选模式(默认):用户可以选择多个反馈选项。

import { FeedbackComposed } from "@/registry/wuhan/composed/feedback/feedback";

export function Example() {
  return (
    <FeedbackComposed
      options={[
        { id: "harmful", label: "有害/不安全" },
        { id: "false", label: "信息虚假" },
        { id: "inappropriate", label: "内容不当" },
        { id: "other", label: "其他" },
      ]}
      onSubmit={(data) => {
        console.log("反馈数据:", data);
        // data.selectedIds: 选中的选项 ID 数组
        // data.inputValue: 输入框内容
      }}
    />
  );
}

单选模式:用户只能选择一个选项。

import { FeedbackComposed } from "@/registry/wuhan/composed/feedback/feedback";

export function Example() {
  return (
    <FeedbackComposed
      multiple={false}
      title="有什么问题?"
      options={[
        { id: "harmful", label: "有害/不安全" },
        { id: "false", label: "信息虚假" },
      ]}
      onSubmit={(data) => {
        console.log("反馈数据:", data);
        // data.selectedId: 选中的选项 ID(单选模式)
        // data.inputValue: 输入框内容
      }}
    />
  );
}

特性

  • 多种反馈类型:通过选项按钮快速分类问题
  • 多选支持:默认为多选模式,用户可选择多个选项(单选模式需设置 multiple={false}
  • 详细信息收集:输入框支持用户补充详细描述
  • 双模式支持:受控模式用于复杂表单,非受控模式用于简单场景
  • 表单验证:内置表单提交处理,可扩展验证逻辑
  • 无障碍支持:包含适当的 ARIA 标签
  • 响应式设计:自适应不同屏幕尺寸

安装

pnpm dlx shadcn@latest add http://localhost:3000/r/wuhan/feedback.json

代码演示

基本

基础的反馈组件示例,包含完整的反馈流程。

反馈组件示例

有什么问题?
"use client";

import * as React from "react";
import { FeedbackComposed } from "@/components/composed/feedback/feedback";

export function FeedbackDemo() {
  const [selectedOption, setSelectedOption] = React.useState<string>("");
  const [inputValue, setInputValue] = React.useState("");

  const feedbackOptions = [
    { id: "harmful", label: "有害/不安全" },
    { id: "false", label: "信息虚假" },
    { id: "inappropriate", label: "内容不当" },
    { id: "other", label: "其他" },
  ];

  const handleSubmit = () => {
    console.log("提交反馈:", {
      option: selectedOption,
      input: inputValue,
    });
    // 这里可以添加提交逻辑
  };

  return (
    <div className="flex flex-col gap-6 w-full max-w-md">
      <div>
        <h3 className="text-sm font-medium text-[var(--Text-text-secondary)] mb-4">
          反馈组件示例
        </h3>
        <FeedbackComposed
          title="有什么问题?"
          options={feedbackOptions}
          selectedId={selectedOption}
          onSelect={setSelectedOption}
          inputValue={inputValue}
          onInputChange={setInputValue}
          onClose={() => console.log("关闭")}
          onSubmit={handleSubmit}
        />
      </div>
    </div>
  );
}

受控模式

使用受控模式管理反馈状态,适合需要外部状态管理的场景。

受控模式反馈表单

反馈问题
当前选择:
输入内容:
"use client";

import * as React from "react";
import { FeedbackComposed } from "@/components/composed/feedback/feedback";

export function FeedbackControlled() {
  const [selectedOption, setSelectedOption] = React.useState<string>("");
  const [inputValue, setInputValue] = React.useState("");
  const [submitted, setSubmitted] = React.useState(false);

  const feedbackOptions = [
    { id: "bug", label: "Bug 报告" },
    { id: "feature", label: "功能建议" },
    { id: "question", label: "使用问题" },
    { id: "other", label: "其他" },
  ];

  const handleSubmit = (data: { selectedId: string; inputValue: string }) => {
    console.log("提交反馈:", data);
    setSubmitted(true);

    // 模拟提交成功后重置
    setTimeout(() => {
      setSelectedOption("");
      setInputValue("");
      setSubmitted(false);
    }, 2000);
  };

  return (
    <div className="flex flex-col gap-6 w-full max-w-md">
      <div>
        <h3 className="text-sm font-medium text-[var(--Text-text-secondary)] mb-4">
          受控模式反馈表单
        </h3>
        {submitted ? (
          <div className="p-4 bg-green-50 text-green-700 rounded-lg">
            提交成功!感谢您的反馈。
          </div>
        ) : (
          <FeedbackComposed
            title="反馈问题"
            options={feedbackOptions}
            selectedId={selectedOption}
            onSelect={setSelectedOption}
            inputValue={inputValue}
            onInputChange={setInputValue}
            placeholder="请详细描述您遇到的问题..."
            submitLabel="提交反馈"
            onSubmit={handleSubmit}
            onClose={() => console.log("关闭反馈")}
          />
        )}
      </div>

      {/* 显示当前状态 */}
      <div className="text-xs text-[var(--Text-text-tertiary)] space-y-1">
        <div>当前选择: {selectedOption || "无"}</div>
        <div>输入内容: {inputValue || "空"}</div>
      </div>
    </div>
  );
}

自定义选项

自定义反馈选项,支持不同的业务场景。

自定义选项(带图标)

对我们的服务满意吗?
"use client";

import * as React from "react";
import { FeedbackComposed } from "@/components/composed/feedback/feedback";
import { ThumbsUp, ThumbsDown, Meh, Star } from "lucide-react";

export function FeedbackCustomOptions() {
  const handleSubmit = (data: { selectedId: string; inputValue: string }) => {
    console.log("满意度调查:", data);
  };

  // 自定义选项,包含图标和文本
  const satisfactionOptions = [
    {
      id: "very-satisfied",
      label: (
        <span className="flex items-center gap-1.5">
          <Star className="w-4 h-4" />
          非常满意
        </span>
      ),
    },
    {
      id: "satisfied",
      label: (
        <span className="flex items-center gap-1.5">
          <ThumbsUp className="w-4 h-4" />
          满意
        </span>
      ),
    },
    {
      id: "neutral",
      label: (
        <span className="flex items-center gap-1.5">
          <Meh className="w-4 h-4" />
          一般
        </span>
      ),
    },
    {
      id: "dissatisfied",
      label: (
        <span className="flex items-center gap-1.5">
          <ThumbsDown className="w-4 h-4" />
          不满意
        </span>
      ),
    },
  ];

  return (
    <div className="flex flex-col gap-6 w-full max-w-md">
      <div>
        <h3 className="text-sm font-medium text-[var(--Text-text-secondary)] mb-4">
          自定义选项(带图标)
        </h3>
        <FeedbackComposed
          title="对我们的服务满意吗?"
          options={satisfactionOptions}
          defaultSelectedId=""
          placeholder="请告诉我们您的想法和建议..."
          submitLabel="提交评价"
          onSubmit={handleSubmit}
        />
      </div>
    </div>
  );
}

定义内容

内容举报场景,包含常见的举报理由选项。

内容举报场景

举报此内容

💡 提示:举报信息将严格保密,我们会在24小时内处理。

"use client";

import * as React from "react";
import { FeedbackComposed } from "@/components/composed/feedback/feedback";
import { AlertTriangle, Flag, MessageSquareWarning, Ban } from "lucide-react";

export function FeedbackReport() {
  const [isReported, setIsReported] = React.useState(false);

  const reportOptions = [
    {
      id: "harmful",
      label: (
        <span className="flex items-center gap-1.5">
          <AlertTriangle className="w-4 h-4" />
          有害/不安全
        </span>
      ),
    },
    {
      id: "false-info",
      label: (
        <span className="flex items-center gap-1.5">
          <Flag className="w-4 h-4" />
          虚假信息
        </span>
      ),
    },
    {
      id: "spam",
      label: (
        <span className="flex items-center gap-1.5">
          <Ban className="w-4 h-4" />
          垃圾内容
        </span>
      ),
    },
    {
      id: "inappropriate",
      label: (
        <span className="flex items-center gap-1.5">
          <MessageSquareWarning className="w-4 h-4" />
          内容不当
        </span>
      ),
    },
  ];

  const handleSubmit = (data: { selectedId: string; inputValue: string }) => {
    console.log("举报内容:", data);
    setIsReported(true);
  };

  const handleClose = () => {
    console.log("取消举报");
    setIsReported(false);
  };

  if (isReported) {
    return (
      <div className="w-full max-w-md p-6 bg-green-50 border border-green-200 rounded-lg">
        <div className="flex flex-col items-center gap-3 text-center">
          <div className="w-12 h-12 bg-green-100 rounded-full flex items-center justify-center">
            <Flag className="w-6 h-6 text-green-600" />
          </div>
          <h3 className="text-base font-medium text-green-900">举报已提交</h3>
          <p className="text-sm text-green-700">
            感谢您的举报,我们会尽快审核处理。
          </p>
          <button
            onClick={() => setIsReported(false)}
            className="mt-2 text-sm text-green-600 hover:text-green-700 underline"
          >
            返回
          </button>
        </div>
      </div>
    );
  }

  return (
    <div className="flex flex-col gap-6 w-full max-w-md">
      <div>
        <h3 className="text-sm font-medium text-[var(--Text-text-secondary)] mb-4">
          内容举报场景
        </h3>
        <FeedbackComposed
          title="举报此内容"
          options={reportOptions}
          placeholder="请详细说明举报理由(必填)..."
          submitLabel="提交举报"
          onSubmit={handleSubmit}
          onClose={handleClose}
        />
      </div>

      <div className="text-xs text-[var(--Text-text-tertiary)] p-3 bg-[var(--Container-bg-container-secondary)] rounded">
        <p>💡 提示:举报信息将严格保密,我们会在24小时内处理。</p>
      </div>
    </div>
  );
}

带验证的反馈表单

多选模式下的表单验证示例。

有什么问题?
"use client";

import { FeedbackComposed } from "@/components/composed/feedback/feedback";
import { useState } from "react";

export function FeedbackValidated() {
  const [selectedIds, setSelectedIds] = useState<string[]>([]);
  const [inputValue, setInputValue] = useState("");
  const [error, setError] = useState("");

  const handleSubmit = (data: {
    selectedIds: string[];
    inputValue: string;
  }) => {
    // 验证
    if (data.selectedIds.length === 0) {
      setError("请至少选择一个反馈类型");
      return;
    }
    if (!data.inputValue.trim()) {
      setError("请输入详细描述");
      return;
    }

    // 提交
    console.log("提交反馈:", data);
    setError("");
  };

  return (
    <div>
      <FeedbackComposed
        options={[
          { id: "bug", label: "Bug" },
          { id: "feature", label: "功能" },
          { id: "other", label: "其他" },
        ]}
        selectedIds={selectedIds}
        onSelectChange={setSelectedIds}
        inputValue={inputValue}
        onInputChange={setInputValue}
        onSubmit={handleSubmit}
      />
      {error && <p className="text-red-500 text-sm mt-2">{error}</p>}
    </div>
  );
}

异步提交

异步提交反馈数据的示例。

有什么问题?
"use client";

import { FeedbackComposed } from "@/components/composed/feedback/feedback";
import { useState } from "react";

export function FeedbackAsync() {
  const [loading, setLoading] = useState(false);
  const [success, setSuccess] = useState(false);

  const handleSubmit = async (data: {
    selectedIds: string[];
    inputValue: string;
  }) => {
    setLoading(true);
    try {
      // 模拟 API 调用
      await new Promise((resolve) => setTimeout(resolve, 1500));
      console.log("提交反馈:", data);
      setSuccess(true);
    } catch (error) {
      console.error("提交失败:", error);
    } finally {
      setLoading(false);
    }
  };

  if (success) {
    return <div className="text-green-600 p-4 text-center">感谢您的反馈!</div>;
  }

  return (
    <FeedbackComposed
      options={[
        { id: "good", label: "👍 很好" },
        { id: "helpful", label: "有帮助" },
        { id: "bad", label: "👎 不好" },
      ]}
      submitLabel={loading ? "提交中..." : "提交"}
      onSubmit={handleSubmit}
    />
  );
}

按需显示输入框

仅当选中「其他」选项时才显示输入框,适用于希望用户在选定预设选项后补充说明的场景。

import { FeedbackComposed } from "@/registry/wuhan/composed/feedback/feedback";
import { useRef } from "react";

export function Example() {
  const feedbackRef = useRef<HTMLFormElement>(null);
  return (
    <FeedbackComposed
      ref={feedbackRef}
      title="有什么问题?"
      options={[
        { id: "harmful", label: "有害/不安全" },
        { id: "false", label: "信息虚假" },
        { id: "other", label: "其他" },
      ]}
      submitLabel="确认"
      showInputWhenSelected={["other"]}
      onInputShown={() => {
        feedbackRef.current?.scrollIntoView?.({ behavior: "smooth", block: "end" });
      }}
      onSubmit={(data) => console.log("反馈数据:", data)}
    />
  );
}

带验证的反馈表单

多选模式下的表单验证示例。

有什么问题?
"use client";

import { FeedbackComposed } from "@/components/composed/feedback/feedback";
import { useState } from "react";

export function FeedbackValidated() {
  const [selectedIds, setSelectedIds] = useState<string[]>([]);
  const [inputValue, setInputValue] = useState("");
  const [error, setError] = useState("");

  const handleSubmit = (data: {
    selectedIds: string[];
    inputValue: string;
  }) => {
    // 验证
    if (data.selectedIds.length === 0) {
      setError("请至少选择一个反馈类型");
      return;
    }
    if (!data.inputValue.trim()) {
      setError("请输入详细描述");
      return;
    }

    // 提交
    console.log("提交反馈:", data);
    setError("");
  };

  return (
    <div>
      <FeedbackComposed
        options={[
          { id: "bug", label: "Bug" },
          { id: "feature", label: "功能" },
          { id: "other", label: "其他" },
        ]}
        selectedIds={selectedIds}
        onSelectChange={setSelectedIds}
        inputValue={inputValue}
        onInputChange={setInputValue}
        onSubmit={handleSubmit}
      />
      {error && <p className="text-red-500 text-sm mt-2">{error}</p>}
    </div>
  );
}

异步提交

支持异步提交的反馈表单。

有什么问题?
"use client";

import { FeedbackComposed } from "@/components/composed/feedback/feedback";
import { useState } from "react";

export function FeedbackAsync() {
  const [loading, setLoading] = useState(false);
  const [success, setSuccess] = useState(false);

  const handleSubmit = async (data: {
    selectedIds: string[];
    inputValue: string;
  }) => {
    setLoading(true);
    try {
      // 模拟 API 调用
      await new Promise((resolve) => setTimeout(resolve, 1500));
      console.log("提交反馈:", data);
      setSuccess(true);
    } catch (error) {
      console.error("提交失败:", error);
    } finally {
      setLoading(false);
    }
  };

  if (success) {
    return <div className="text-green-600 p-4 text-center">感谢您的反馈!</div>;
  }

  return (
    <FeedbackComposed
      options={[
        { id: "good", label: "👍 很好" },
        { id: "helpful", label: "有帮助" },
        { id: "bad", label: "👎 不好" },
      ]}
      submitLabel={loading ? "提交中..." : "提交"}
      onSubmit={handleSubmit}
    />
  );
}

API

FeedbackComposed

反馈表单主组件,提供完整的反馈收集界面。

Props

PropTypeDefaultDescription
titleReactNode"有什么问题?"反馈表单标题
optionsFeedbackOption[]-反馈选项列表(必填)
multiplebooleantrue是否支持多选模式
selectedIdstring-单选模式:受控模式当前选中的选项 ID
selectedIdsstring[]-多选模式:受控模式当前选中的选项 ID 列表
defaultSelectedIdstring""单选模式:非受控模式默认选中的选项 ID
defaultSelectedIdsstring[][]多选模式:非受控模式默认选中的选项 ID 列表
onSelect(id: string) => void-单选模式:选项选择变化时的回调
onSelectChange(ids: string[]) => void-多选模式:选项选择变化时的回调
inputValuestring-受控模式:输入框的当前值
defaultInputValuestring""非受控模式:输入框的默认值
onInputChange(value: string) => void-输入框内容变化时的回调
placeholderstring"请输入详细描述..."输入框占位符
submitLabelReactNode"提交"提交按钮文本
showInputWhenSelectedstring | string[]-仅当选中指定选项时显示输入框,如 ["other"] 表示选中「其他」时才显示,未配置时始终显示
onInputShown() => void-输入框由隐藏变为显示时调用,可用于滚动到可视区域等
onSubmit(payload: { selectedId: string; selectedIds: string[]; inputValue: string }) => void-表单提交时的回调
onClose() => void-关闭按钮点击时的回调

Example

多选模式(默认):

import { FeedbackComposed } from "@/registry/wuhan/composed/feedback/feedback";

function FeedbackExample() {
  return (
    <FeedbackComposed
      options={[
        { id: "bug", label: "Bug 报告" },
        { id: "feature", label: "功能建议" },
        { id: "question", label: "使用问题" },
      ]}
      onSubmit={(data) => {
        console.log("提交:", data);
        // data.selectedIds: 选中的选项 ID 数组
        // data.selectedId: 空字符串(多选模式)
        // data.inputValue: 输入框内容
      }}
    />
  );
}

单选模式:

import { FeedbackComposed } from "@/registry/wuhan/composed/feedback/feedback";
import { useState } from "react";

function FeedbackExample() {
  const [selectedId, setSelectedId] = useState("");
  const [inputValue, setInputValue] = useState("");

  return (
    <FeedbackComposed
      multiple={false}
      title="反馈问题"
      options={[
        { id: "bug", label: "Bug 报告" },
        { id: "feature", label: "功能建议" },
        { id: "question", label: "使用问题" },
      ]}
      // 单选模式受控
      selectedId={selectedId}
      onSelect={setSelectedId}
      inputValue={inputValue}
      onInputChange={setInputValue}
      // 提交处理
      onSubmit={(data) => {
        console.log("提交:", data);
        // data.selectedId: 选中的选项 ID
        // data.selectedIds: 空数组(单选模式)
        // data.inputValue: 输入框内容
      }}
      onClose={() => {
        console.log("关闭反馈");
      }}
    />
  );
}

FeedbackOption

反馈选项类型定义。

interface FeedbackOption {
  id: string;          // 选项的唯一标识符
  label: ReactNode;    // 选项显示的标签
}

使用场景

  • 内容举报:用户举报不当内容、虚假信息、有害内容等
  • 问题反馈:收集用户遇到的问题和 Bug 报告
  • 功能建议:收集用户的产品改进建议和新功能需求
  • 满意度调查:收集用户对服务、产品的满意度反馈
  • 客服咨询:用户提交咨询问题的表单
  • 质量评价:对内容、服务质量的评价和意见收集

最佳实践

  1. 选项设计:反馈选项应涵盖常见问题类型,并提供"其他"选项
  2. 输入引导:使用清晰的占位符文本引导用户输入详细信息
  3. 提交反馈:提交成功后给予明确的反馈提示
  4. 数据验证:提交前验证必填项,确保收集到有效信息
  5. 隐私保护:明确告知用户数据用途,保护用户隐私
  6. 响应及时:收到反馈后及时响应,提升用户体验

注意事项

  • options 是必填属性,至少需要提供一个选项
  • 多选模式(默认):使用 selectedIds / defaultSelectedIdsonSelectChange
  • 单选模式:使用 selectedId / defaultSelectedIdonSelect
  • 受控模式下需要同时提供 selectedId / selectedIds 和对应的回调函数、inputValueonInputChange
  • 非受控模式下可以使用 defaultSelectedId / defaultSelectedIdsdefaultInputValue 设置初始值
  • onSubmit 回调会接收包含 selectedIdselectedIdsinputValue 的对象
    • 多选模式下:selectedIds 为选中的 ID 数组,selectedId 为空字符串
    • 单选模式下:selectedId 为选中的 ID,selectedIds 为空数组
  • 表单提交会自动调用 preventDefault(),需要在 onSubmit 中处理实际的提交逻辑
  • showInputWhenSelected 未配置时输入框始终显示;配置后仅当选中指定选项时显示,如 ["other"] 表示选中「其他」时才显示
  • onInputShown 在输入框由隐藏变为显示时触发,可用于滚动到可视区域等场景

样式定制

组件使用 Tailwind CSS,可以通过原语组件进行完全定制:

import {
  FeedbackContainerPrimitive,
  FeedbackHeaderPrimitive,
  FeedbackInputPrimitive,
  FeedbackSubmitButtonPrimitive,
} from "@/registry/wuhan/blocks/feedback/feedback-01";

function CustomFeedback() {
  return (
    <FeedbackContainerPrimitive className="bg-gray-50 rounded-lg">
      <FeedbackHeaderPrimitive 
        title="自定义反馈" 
        className="border-b border-gray-200"
      />
      <FeedbackInputPrimitive 
        placeholder="输入您的反馈..." 
        className="border-2 border-blue-200 focus:border-blue-500"
      />
      <FeedbackSubmitButtonPrimitive className="bg-purple-500 hover:bg-purple-600">
        发送反馈
      </FeedbackSubmitButtonPrimitive>
    </FeedbackContainerPrimitive>
  );
}

原语组件

Feedback 组件基于以下原语组件构建,可用于需要完全自定义的场景。

FeedbackContainerPrimitive

反馈容器组件,包含所有反馈内容。

参数类型说明
childrenReactNode子元素
onClose() => void关闭回调
classNamestring自定义样式类名

FeedbackHeaderPrimitive

反馈头部组件,包含标题和关闭按钮。

参数类型说明
titleReactNode标题内容
onClose() => void关闭回调
classNamestring自定义样式类名

FeedbackInputContainerPrimitive

输入框容器组件。

参数类型说明
childrenReactNode子元素
classNamestring自定义样式类名

FeedbackInputPrimitive

反馈输入框组件,复用 sidebar 搜索框样式。

参数类型说明
placeholderstring占位符文本
valuestring输入框值
onChange(e: ChangeEvent) => void变化回调
classNamestring自定义样式类名

FeedbackSubmitButtonPrimitive

提交按钮组件,品牌色样式。

参数类型说明
childrenReactNode按钮文本
type"submit" | "button"按钮类型
classNamestring自定义样式类名

已废弃的原语

注意:以下组件已废弃,建议使用 toggle-button block 中的组件。

  • FeedbackButtonPrimitive - 已废弃,请使用 ToggleButtonPrimitive
  • FeedbackButtonGroupPrimitive - 已废弃,请使用 ToggleButtonGroupPrimitive

原语组件可以在 registry/wuhan/blocks/feedback/feedback-01.tsx 中找到。