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)); +}