diff --git a/src/components/FeedbackFab.tsx b/src/components/FeedbackFab.tsx
index 4b2df2d..ae70661 100644
--- a/src/components/FeedbackFab.tsx
+++ b/src/components/FeedbackFab.tsx
@@ -2,10 +2,12 @@ import { useEffect, useRef, useState } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import {
MessageCircleHeart, X, ChevronRight, ChevronLeft, Check, Sparkles,
- ImagePlus, Loader2, Inbox, Lightbulb, Bug, Palette, NotebookPen,
+ ImagePlus, Loader2, Inbox, Lightbulb, Bug, Palette, NotebookPen, Settings2,
type LucideIcon,
} from 'lucide-react';
import { fetchJson } from '../auth/api-client';
+import { useAuth } from '../auth/useAuth';
+import { canManageFeedback } from '../shared/auth/roles';
import FeedbackHistoryDrawer from './FeedbackHistoryDrawer';
const MAX_SCREENSHOTS = 6;
@@ -86,6 +88,8 @@ interface Props {
}
export default function FeedbackFab({ module: moduleProp }: Props = {}) {
+ const { user } = useAuth();
+ const isAdmin = canManageFeedback(user?.roles);
const [open, setOpen] = useState(false);
const [historyOpen, setHistoryOpen] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
@@ -229,6 +233,18 @@ export default function FeedbackFab({ module: moduleProp }: Props = {}) {
>
我的反馈
+ {isAdmin && (
+ <>
+
+ setMenuOpen(false)}
+ className="flex items-center gap-2 px-3 py-2 text-[12px] font-bold text-slate-700 rounded-lg hover:bg-violet-50 hover:text-violet-600"
+ >
+ 反馈管理
+
+ >
+ )}
)}
diff --git a/src/server/auth/types.ts b/src/server/auth/types.ts
index 46bbbe1..e0970e1 100644
--- a/src/server/auth/types.ts
+++ b/src/server/auth/types.ts
@@ -28,5 +28,7 @@ export {
FULL_ACCESS_ROLES,
DEPT_ACCESS_ROLES,
SCHEDULING_ACCESS_ROLES,
+ FEEDBACK_ADMIN_ROLES,
canAccessScheduling,
+ canManageFeedback,
} from '../../shared/auth/roles.js';
diff --git a/src/server/routes/feedback/index.ts b/src/server/routes/feedback/index.ts
index 6951f27..04507a9 100644
--- a/src/server/routes/feedback/index.ts
+++ b/src/server/routes/feedback/index.ts
@@ -2,6 +2,7 @@ import { Hono } from 'hono';
import type { ResultSetHeader, RowDataPacket } from 'mysql2';
import pool from '../../db.js';
import type { AuthUser } from '../../auth/types.js';
+import { canManageFeedback } from '../../auth/types.js';
import { uploadFeedbackImage } from './oss.js';
const app = new Hono();
@@ -127,9 +128,13 @@ app.get('/mine', async (c) => {
return c.json({ items: rows });
});
-// GET /api/feedback/list — 管理列表(含全部反馈)
+// GET /api/feedback/list — 管理列表(仅 BI-ADMIN-FEEDBACK / 全量权限)
app.get('/list', async (c) => {
await ensureTable();
+ const user = (c as { get?: (k: string) => unknown }).get?.('user') as AuthUser | undefined;
+ if (!canManageFeedback(user?.roles)) {
+ return c.json({ ok: false, message: '无权限' }, 403);
+ }
const limit = Math.min(500, Math.max(1, Number(c.req.query('limit')) || 100));
const status = c.req.query('status') || '';
const where: string[] = ['1=1'];
@@ -150,9 +155,13 @@ app.get('/list', async (c) => {
return c.json({ items: rows });
});
-// PATCH /api/feedback/:id — 管理:更新状态与回复
+// PATCH /api/feedback/:id — 管理:更新状态与回复(仅 BI-ADMIN-FEEDBACK / 全量权限)
app.patch('/:id', async (c) => {
await ensureTable();
+ const user = (c as { get?: (k: string) => unknown }).get?.('user') as AuthUser | undefined;
+ if (!canManageFeedback(user?.roles)) {
+ return c.json({ ok: false, message: '无权限' }, 403);
+ }
const id = Number(c.req.param('id'));
if (!Number.isFinite(id) || id <= 0) return c.json({ ok: false, message: 'id 不合法' }, 400);
const body = await c.req.json().catch(() => ({})) as { status?: string; reply?: string };
diff --git a/src/shared/auth/roles.ts b/src/shared/auth/roles.ts
index bed4c72..7057cd4 100644
--- a/src/shared/auth/roles.ts
+++ b/src/shared/auth/roles.ts
@@ -10,8 +10,17 @@ export const DEPT_ACCESS_ROLES = ['BI-Leader-Dep'];
/** 智能调度模块访问角色 */
export const SCHEDULING_ACCESS_ROLES = ['BI-SCHEDULE-OPT'];
+/** 反馈管理(管理员)访问角色 */
+export const FEEDBACK_ADMIN_ROLES = ['BI-ADMIN-FEEDBACK'];
+
/** 用户是否可访问智能调度模块。仅 BI-SCHEDULE-OPT 角色允许访问。 */
export function canAccessScheduling(roles: readonly string[] | null | undefined): boolean {
if (!roles || roles.length === 0) return false;
return roles.some(r => SCHEDULING_ACCESS_ROLES.includes(r));
}
+
+/** 用户是否可管理反馈。仅 BI-ADMIN-FEEDBACK 或全量权限角色可访问。 */
+export function canManageFeedback(roles: readonly string[] | null | undefined): boolean {
+ if (!roles || roles.length === 0) return false;
+ return roles.some(r => FEEDBACK_ADMIN_ROLES.includes(r) || FULL_ACCESS_ROLES.includes(r));
+}