style(feedback): 选类型步骤改用 Lucide 图标 + 克制白卡
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

之前用 emoji + 整卡渐变背景,颜色饱和度高、和系统其他模块的视觉
语言不一致,看起来有点像玩具。

新视觉:
  - 替换 emoji 为 Lucide 图标:Lightbulb / Bug / Palette / NotebookPen
    与项目其他模块(Truck/Route/Zap)保持一致
  - 卡片白底 + 1px 浅边框,hover 阴影;选中态用 ring 替代填色
  - 图标放在彩色圆角小容器里(amber/rose/violet/blue),强度更克制
  - 标题升级到 13px,副标题统一 11px slate-400 medium
  - 入场级联动画 + 微交互(hover y=-1,→ 按钮位移)

文案微调:「想反馈点什么?」+ 副标题「选一个最贴近的类型」

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-30 14:16:26 +08:00
parent 90b34b681e
commit c5541fbbf5

View File

@@ -1,6 +1,10 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { motion, AnimatePresence } from 'motion/react'; import { motion, AnimatePresence } from 'motion/react';
import { MessageCircleHeart, X, ChevronRight, ChevronLeft, Check, Sparkles, ImagePlus, Loader2, Inbox } from 'lucide-react'; import {
MessageCircleHeart, X, ChevronRight, ChevronLeft, Check, Sparkles,
ImagePlus, Loader2, Inbox, Lightbulb, Bug, Palette, NotebookPen,
type LucideIcon,
} from 'lucide-react';
import { fetchJson } from '../auth/api-client'; import { fetchJson } from '../auth/api-client';
import FeedbackHistoryDrawer from './FeedbackHistoryDrawer'; import FeedbackHistoryDrawer from './FeedbackHistoryDrawer';
@@ -37,11 +41,25 @@ function readAsDataUrl(file: File): Promise<string> {
type FeedbackType = 'dimension' | 'bug' | 'ux' | 'other'; type FeedbackType = 'dimension' | 'bug' | 'ux' | 'other';
const TYPE_OPTIONS: { key: FeedbackType; emoji: string; label: string; sub: string; color: string }[] = [ interface TypeOption {
{ key: 'dimension', emoji: '💡', label: '想看新的统计维度', sub: '比如按 XX 维度分组', color: 'from-amber-50 to-orange-50 border-amber-100' }, key: FeedbackType;
{ key: 'bug', emoji: '🐛', label: '报告一个 bug', sub: '哪里看着不对劲', color: 'from-rose-50 to-red-50 border-rose-100' }, icon: LucideIcon;
{ key: 'ux', emoji: '🎨', label: '界面 / 体验建议', sub: '哪里能更顺手', color: 'from-violet-50 to-fuchsia-50 border-violet-100' }, label: string;
{ key: 'other', emoji: '📝', label: '其他想法', sub: '欢迎随便聊聊', color: 'from-slate-50 to-blue-50 border-slate-100' }, sub: string;
iconBg: string;
iconFg: string;
ring: string;
}
const TYPE_OPTIONS: TypeOption[] = [
{ key: 'dimension', icon: Lightbulb, label: '想看新的统计维度', sub: '比如按 XX 维度切片',
iconBg: 'bg-amber-50', iconFg: 'text-amber-500', ring: 'ring-amber-200' },
{ key: 'bug', icon: Bug, label: '报告一个 Bug', sub: '哪里看着不对劲',
iconBg: 'bg-rose-50', iconFg: 'text-rose-500', ring: 'ring-rose-200' },
{ key: 'ux', icon: Palette, label: '界面 / 体验建议', sub: '哪里能更顺手',
iconBg: 'bg-violet-50', iconFg: 'text-violet-500', ring: 'ring-violet-200' },
{ key: 'other', icon: NotebookPen, label: '其他想法', sub: '欢迎随便聊聊',
iconBg: 'bg-blue-50', iconFg: 'text-blue-500', ring: 'ring-blue-200' },
]; ];
const MODULE_LABELS: Record<string, string> = { const MODULE_LABELS: Record<string, string> = {
@@ -286,22 +304,34 @@ export default function FeedbackFab({ module: moduleProp }: Props = {}) {
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{step === 1 && ( {step === 1 && (
<motion.div key="s1" initial={{ opacity: 0, x: 12 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -12 }} transition={{ duration: 0.2 }}> <motion.div key="s1" initial={{ opacity: 0, x: 12 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -12 }} transition={{ duration: 0.2 }}>
<p className="text-[12px] font-bold text-slate-600 mb-3"></p> <p className="text-[13px] font-bold text-slate-700 mb-1"></p>
<p className="text-[11px] text-slate-400 font-bold mb-4"></p>
<div className="grid grid-cols-1 gap-2"> <div className="grid grid-cols-1 gap-2">
{TYPE_OPTIONS.map(opt => ( {TYPE_OPTIONS.map((opt, i) => {
<button const Icon = opt.icon;
key={opt.key} const selected = type === opt.key;
onClick={() => { setType(opt.key); setStep(2); }} return (
className={`text-left p-3 rounded-2xl border bg-gradient-to-br ${opt.color} hover:shadow-md transition-all flex items-center gap-3 ${type === opt.key ? 'ring-2 ring-blue-400 shadow' : ''}`} <motion.button
> key={opt.key}
<span className="text-2xl">{opt.emoji}</span> initial={{ opacity: 0, y: 6 }}
<div className="flex-1 min-w-0"> animate={{ opacity: 1, y: 0 }}
<div className="text-[12px] font-black text-slate-800">{opt.label}</div> transition={{ delay: i * 0.04, duration: 0.2 }}
<div className="text-[10px] text-slate-500 font-bold mt-0.5">{opt.sub}</div> whileHover={{ y: -1 }}
</div> whileTap={{ scale: 0.99 }}
<ChevronRight size={14} className="text-slate-400 flex-shrink-0" /> onClick={() => { setType(opt.key); setStep(2); }}
</button> className={`text-left p-3.5 rounded-2xl border bg-white transition-all flex items-center gap-3 group ${selected ? `ring-2 ${opt.ring} border-transparent shadow-sm` : 'border-slate-100 hover:border-slate-200 hover:shadow-sm'}`}
))} >
<div className={`w-10 h-10 rounded-xl ${opt.iconBg} flex items-center justify-center flex-shrink-0`}>
<Icon size={18} className={opt.iconFg} strokeWidth={2.2} />
</div>
<div className="flex-1 min-w-0">
<div className="text-[13px] font-bold text-slate-800 leading-tight">{opt.label}</div>
<div className="text-[11px] text-slate-400 font-medium mt-0.5">{opt.sub}</div>
</div>
<ChevronRight size={15} className="text-slate-300 flex-shrink-0 group-hover:text-slate-500 group-hover:translate-x-0.5 transition-all" />
</motion.button>
);
})}
</div> </div>
</motion.div> </motion.div>
)} )}