feat: 全局反馈系统 + 各模块底部统一动态提示
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

- 新增 components/RotatingFooterHint:统一文案+蓝色脉冲,4 秒轮换
- 新增 components/FeedbackFab:右下角悬浮按钮(渐变 + 心形信封 + 黄色脉冲点),
  点击打开 4 步引导式弹窗
    Step 1 选类型(💡新维度 / 🐛bug / 🎨界面 / 📝其他)
    Step 2 描述需求 + 选当前板块(chip)
    Step 3 留联系方式(可选)+ 提交概览
    Step 4 ❤️ 成功页(弹簧 √ 动画)
  顶部 spring 进度条,底部上一步/下一步,下拉手柄,背景点击或 X 关闭
- 后端 routes/feedback:bi_user_feedback 表(自动建表,含 status 字段)
  POST /api/feedback/submit + GET /api/feedback/list
- Shell 全局挂载 FeedbackFab,自动从 hash 检测当前模块
- 各模块底部追加 RotatingFooterHint:
  AssetsModule / MileageModule / SchedulingModule / EleImportPage
  HydrogenOverview / HydrogenDaily / ElectricOverview / ElectricDaily
  (HydrogenOverview 旧的内嵌实现已替换为共享组件)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-30 13:50:39 +08:00
parent 08f21b7e24
commit e8f1604c11
13 changed files with 486 additions and 36 deletions

View File

@@ -4,6 +4,7 @@ import { motion, AnimatePresence } from 'motion/react';
import TrendBadge from './TrendBadge';
import { fetchElectricMonthly } from './api';
import type { CustomerType, ElectricMonthGroup } from './types';
import RotatingFooterHint from '../../components/RotatingFooterHint';
export default function ElectricDaily() {
const [customer, setCustomer] = useState<CustomerType>('lingniu');
@@ -114,6 +115,7 @@ export default function ElectricDaily() {
);
})}
</div>
<RotatingFooterHint />
</div>
);
}

View File

@@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
import { Wallet, CalendarClock } from 'lucide-react';
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, Tooltip } from 'recharts';
import { fetchElectricOverview, type ElectricOverviewResponse } from './api';
import RotatingFooterHint from '../../components/RotatingFooterHint';
function fmtYuan(yuan: number) {
return `¥${yuan.toLocaleString('zh-CN', { maximumFractionDigits: 2 })}`;
@@ -95,6 +96,7 @@ export default function ElectricOverview() {
</BarChart>
</ResponsiveContainer>
</div>
<RotatingFooterHint />
</div>
);
}

View File

@@ -5,6 +5,7 @@ import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, Tooltip } from
import TrendBadge from './TrendBadge';
import { fetchHydrogenDaily } from './api';
import type { CustomerType, DateQuickPick, HydrogenDailyRow } from './types';
import RotatingFooterHint from '../../components/RotatingFooterHint';
const QUICK_PICK_OPTIONS: Array<{ id: DateQuickPick; label: string }> = [
{ id: 'today', label: '当天' },
@@ -214,6 +215,7 @@ export default function HydrogenDaily() {
})}
</div>
)}
<RotatingFooterHint />
</div>
);
}

View File

@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, PieChart, Pie, Tooltip, LabelList } from 'recharts';
import { fetchHydrogenOverview, type HydrogenOverviewResponse } from './api';
import RotatingFooterHint from '../../components/RotatingFooterHint';
interface YAxisTickProps {
x?: number;
@@ -29,15 +30,6 @@ const REGION_COLORS = [
'#94a3b8',
];
// 幽默动态提示词,每 4 秒轮换一条
const FOOTER_HINTS = [
'更多统计维度接入中,欢迎您的建议 ~',
'下一个图表,可能就是您建议的那个',
'数据科学家正在深夜挖掘新维度…',
'想看哪个角度的数据?告诉我们一下嘛',
'维度灵感正在路上,钉一下产品同学也行',
'数字背后还有故事,等下一次上线揭晓',
];
export default function HydrogenOverview() {
const [data, setData] = useState<HydrogenOverviewResponse | null>(null);
@@ -153,32 +145,6 @@ export default function HydrogenOverview() {
);
}
function RotatingFooterHint() {
const [idx, setIdx] = useState(0);
useEffect(() => {
const t = setInterval(() => setIdx(i => (i + 1) % FOOTER_HINTS.length), 4000);
return () => clearInterval(t);
}, []);
return (
<div className="mt-1 flex items-center justify-center gap-1.5 text-[11px] text-slate-400 font-bold">
<span className="inline-block w-1.5 h-1.5 rounded-full bg-blue-400 animate-pulse" />
<span
key={idx}
className="transition-opacity duration-300"
style={{ animation: 'hintFade 0.5s ease' }}
>
{FOOTER_HINTS[idx]}
</span>
<style>{`
@keyframes hintFade {
from { opacity: 0; transform: translateY(2px); }
to { opacity: 1; transform: translateY(0); }
}
`}</style>
</div>
);
}
function HydrogenOverviewSkeleton() {
return (
<div className="flex flex-col gap-3 animate-pulse">