diff --git a/front-end/package-lock.json b/front-end/package-lock.json index 4a847d0..90e139d 100644 --- a/front-end/package-lock.json +++ b/front-end/package-lock.json @@ -67,6 +67,7 @@ "devDependencies": { "@tailwindcss/vite": "4.1.12", "@vitejs/plugin-react": "4.7.0", + "playwright": "^1.61.1", "tailwindcss": "4.1.12", "vite": "6.3.5" }, @@ -4766,6 +4767,53 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", diff --git a/front-end/package.json b/front-end/package.json index f076228..4a5b26b 100644 --- a/front-end/package.json +++ b/front-end/package.json @@ -67,6 +67,7 @@ "devDependencies": { "@tailwindcss/vite": "4.1.12", "@vitejs/plugin-react": "4.7.0", + "playwright": "^1.61.1", "tailwindcss": "4.1.12", "vite": "6.3.5" }, @@ -87,4 +88,4 @@ "vite": "6.3.5" } } -} \ No newline at end of file +} diff --git a/front-end/scripts/build_doc.py b/front-end/scripts/build_doc.py new file mode 100644 index 0000000..838b398 --- /dev/null +++ b/front-end/scripts/build_doc.py @@ -0,0 +1,148 @@ +# -*- coding: utf-8 -*- +import json +import os +from datetime import date + +from docx import Document +from docx.shared import Inches, Pt, RGBColor +from docx.enum.text import WD_ALIGN_PARAGRAPH +from docx.oxml.ns import qn +from PIL import Image + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.abspath(os.path.join(HERE, "..", "..")) +SHOTS = os.path.join(ROOT, "shots") +OUT = os.path.join(ROOT, "滨化智慧招标平台-前端界面截图汇总.docx") +BRAND = RGBColor(0x15, 0x65, 0xC0) +BRAND_HEX = "1565C0" +PAGE_W_IN = 6.3 + +with open(os.path.join(SHOTS, "manifest.json"), "r", encoding="utf-8") as f: + manifest = json.load(f) + +order = [] +for m in manifest: + if m["section"] not in order: + order.append(m["section"]) +manifest.sort(key=lambda x: order.index(x["section"])) + +doc = Document() +style = doc.styles["Normal"] +style.font.name = "Microsoft YaHei" +style.font.size = Pt(10.5) +style.element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft YaHei") + +for sec in doc.sections: + sec.top_margin = Inches(0.8) + sec.bottom_margin = Inches(0.8) + sec.left_margin = Inches(0.8) + sec.right_margin = Inches(0.8) + +def set_font(run, size=None, bold=None, color=None): + run.font.name = "Microsoft YaHei" + run._element.rPr.rFonts.set(qn("w:eastAsia"), "Microsoft YaHei") + if size: + run.font.size = Pt(size) + if bold is not None: + run.font.bold = bold + if color is not None: + run.font.color.rgb = color + +# 封面 +for _ in range(4): + doc.add_paragraph() +p = doc.add_paragraph() +p.alignment = WD_ALIGN_PARAGRAPH.CENTER +r = p.add_run("滨化集团智慧招标平台") +set_font(r, size=30, bold=True, color=BRAND) +p = doc.add_paragraph() +p.alignment = WD_ALIGN_PARAGRAPH.CENTER +r = p.add_run("Smart Tendering Platform (STP)") +set_font(r, size=14, color=RGBColor(0x88, 0x88, 0x88)) +doc.add_paragraph() +p = doc.add_paragraph() +p.alignment = WD_ALIGN_PARAGRAPH.CENTER +r = p.add_run("前端界面截图汇总") +set_font(r, size=22, bold=True) +for _ in range(6): + doc.add_paragraph() + +sections_order = [] +for m in manifest: + if m["section"] not in sections_order: + sections_order.append(m["section"]) + +meta = [ + ("文档类型", "前端界面(UI)截图汇总"), + ("覆盖范围", "门户页 / 登录页 / 新增导航页 / 工作台 / 业务页 / 弹窗"), + ("截图数量", f"{len(manifest)} 张"), + ("章节数量", f"{len(sections_order)} 章"), + ("生成日期", date.today().strftime("%Y 年 %m 月 %d 日")), +] +for k, v in meta: + p = doc.add_paragraph() + p.alignment = WD_ALIGN_PARAGRAPH.CENTER + r = p.add_run(f"{k}:") + set_font(r, size=11, bold=True) + r = p.add_run(v) + set_font(r, size=11, color=RGBColor(0x44, 0x44, 0x44)) + +# 目录 +doc.add_page_break() +p = doc.add_paragraph() +r = p.add_run("目 录") +set_font(r, size=18, bold=True, color=BRAND) +doc.add_paragraph() +for sec in sections_order: + cnt = sum(1 for m in manifest if m["section"] == sec) + p = doc.add_paragraph() + r = p.add_run(sec) + set_font(r, size=12, bold=True) + r = p.add_run(f" ({cnt} 张)") + set_font(r, size=10.5, color=RGBColor(0x88, 0x88, 0x88)) + +fig_no = 0 +current = None +for m in manifest: + img = os.path.join(SHOTS, m["file"]) + if not os.path.exists(img): + continue + if m["section"] != current: + current = m["section"] + doc.add_page_break() + p = doc.add_paragraph() + r = p.add_run(current) + set_font(r, size=17, bold=True, color=BRAND) + pPr = p._p.get_or_add_pPr() + pbdr = pPr.makeelement(qn("w:pBdr"), {}) + bottom = pbdr.makeelement(qn("w:bottom"), { + qn("w:val"): "single", qn("w:sz"): "12", + qn("w:space"): "4", qn("w:color"): BRAND_HEX, + }) + pbdr.append(bottom) + pPr.append(pbdr) + doc.add_paragraph() + + fig_no += 1 + cap = doc.add_paragraph() + r = cap.add_run(f"图 {fig_no} {m['title']}") + set_font(r, size=11, bold=True, color=RGBColor(0x22, 0x22, 0x22)) + + with Image.open(img) as im: + w, h = im.size + ratio = h / w + width_in = PAGE_W_IN + height_in = width_in * ratio + max_h = 8.6 + if height_in > max_h: + height_in = max_h + width_in = height_in / ratio + p = doc.add_paragraph() + p.alignment = WD_ALIGN_PARAGRAPH.CENTER + p.add_run().add_picture(img, width=Inches(width_in)) + doc.add_paragraph() + +doc.save(OUT) +print(f"已生成:{OUT}") +print(f"共 {fig_no} 张截图,{len(sections_order)} 个章节") + diff --git a/front-end/scripts/capture.mjs b/front-end/scripts/capture.mjs new file mode 100644 index 0000000..841b73f --- /dev/null +++ b/front-end/scripts/capture.mjs @@ -0,0 +1,145 @@ +import { chromium } from "playwright"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const OUT = path.resolve(__dirname, "../../shots"); +const BASE = process.env.BASE_URL || "http://localhost:5174"; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +fs.rmSync(OUT, { recursive: true, force: true }); +fs.mkdirSync(OUT, { recursive: true }); + +const manifest = []; +let idx = 0; + +async function expand(page) { + await page.evaluate(() => { + document.querySelectorAll(".h-screen").forEach((el) => { + el.style.height = "auto"; + el.style.minHeight = "100vh"; + el.style.overflow = "visible"; + }); + document.querySelectorAll(".overflow-hidden").forEach((el) => (el.style.overflow = "visible")); + document.querySelectorAll("main").forEach((el) => { + el.style.overflow = "visible"; + el.style.height = "auto"; + }); + }); +} + +async function shot(page, section, title, { full = true, wait = 500, expandLayout = true } = {}) { + idx += 1; + const file = `${String(idx).padStart(2, "0")}.png`; + if (expandLayout) await expand(page).catch(() => {}); + await sleep(wait); + await page.screenshot({ path: path.join(OUT, file), fullPage: full }); + manifest.push({ file, section, title }); + console.log(`[${idx}] ${section} / ${title} -> ${file}`); +} + +async function goto(page, url) { + await page.goto(BASE + url, { waitUntil: "networkidle" }).catch(async () => { + await page.goto(BASE + url, { waitUntil: "domcontentloaded" }); + }); + await sleep(450); +} + +async function clickIf(page, locator, wait = 300) { + const n = await locator.count(); + if (n > 0) { + await locator.first().click().catch(() => {}); + await sleep(wait); + } +} + +async function switchRole(page, name) { + await page.locator("header button:has(svg.lucide-refresh-cw)").first().click(); + await sleep(250); + await page.getByRole("menuitem", { name }).first().click(); + await sleep(700); +} + +async function run() { + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 1.3 }); + page.setDefaultTimeout(15000); + + const A = "一、门户与登录"; + await goto(page, "/portal"); + await shot(page, A, "首页(门户)", { wait: 700 }); + await clickIf(page, page.getByRole("link", { name: "交易信息" })); + await shot(page, A, "门户导航 · 交易信息页"); + await goto(page, "/portal/policies"); + await shot(page, A, "门户导航 · 政策法规页"); + await goto(page, "/portal/guides"); + await shot(page, A, "门户导航 · 操作指南页"); + await goto(page, "/portal/faq"); + await shot(page, A, "门户导航 · 常见问题页"); + await goto(page, "/portal/procurement-platform"); + await shot(page, A, "门户导航 · 采购平台页"); + await goto(page, "/"); + await shot(page, A, "登录页(含可拖拽小滨)", { full: false, expandLayout: false }); + + const B = "二、角色工作台"; + await goto(page, "/app"); + await shot(page, B, "招标专员工作台", { wait: 800 }); + for (const rn of ["采购需求人员", "评标专家", "评标委员会工作领导小组", "供应商", "系统管理员", "合规 / 审计"]) { + await switchRole(page, rn); + await shot(page, B, `${rn}工作台`, { wait: 700 }); + } + await switchRole(page, "招标专员"); + + const C = "三、核心业务页面"; + for (const [url, title] of [ + ["/app/delegation", "发起招标委托"], + ["/app/projects", "项目管理列表"], + ["/app/projects/P2026001", "项目详情"], + ["/app/projects/P2026001/shortlist", "供应商长短名单"], + ["/app/projects/P2026001/sourcing", "AI智慧寻源"], + ["/app/projects/P2026001/opening", "开标大厅"], + ["/app/projects/P2026001/evaluation", "在线评标室"], + ["/app/deposits", "保证金管理"], + ["/app/suppliers", "供应商管理"], + ["/app/experts", "专家管理"], + ["/app/reports", "报表中心"], + ["/app/exceptions", "异常决策"], + ["/app/expert-tasks", "评标任务"], + ["/app/bidding", "投标中心"], + ["/app/audit-logs", "审计日志"], + ["/app/collusion", "围串标分析"], + ["/app/evidence", "存证验证"], + ["/app/admin/users", "用户与权限管理"], + ["/app/admin/keys", "密钥管理"], + ["/app/settings", "系统设置"], + ]) { + await goto(page, url); + await shot(page, C, title, { wait: 600 }); + } + + const D = "四、弹窗与抽屉"; + await goto(page, "/app/suppliers"); + await clickIf(page, page.locator("tbody tr"), 700); + await shot(page, D, "供应商详情抽屉", { full: false, expandLayout: false }); + + await goto(page, "/app/expert-tasks"); + await clickIf(page, page.getByRole("button", { name: /确认邀请/ }), 500); + await clickIf(page, page.getByRole("button", { name: /签署评标承诺书/ }), 700); + await shot(page, D, "评标纪律承诺书弹窗", { full: false, expandLayout: false }); + + await goto(page, "/app/admin/keys"); + await clickIf(page, page.getByRole("button", { name: /手动触发密钥漂移/ }), 700); + await shot(page, D, "密钥漂移二次授权弹窗", { full: false, expandLayout: false }); + + fs.writeFileSync(path.join(OUT, "manifest.json"), JSON.stringify(manifest, null, 2)); + await browser.close(); + console.log(`完成:共 ${manifest.length} 张截图`); +} + +run().catch((e) => { + console.error(e); + fs.writeFileSync(path.join(OUT, "manifest.json"), JSON.stringify(manifest, null, 2)); + process.exit(1); +}); + diff --git a/front-end/src/app/App.tsx b/front-end/src/app/App.tsx index 78c90d7..992d66f 100644 --- a/front-end/src/app/App.tsx +++ b/front-end/src/app/App.tsx @@ -5,6 +5,11 @@ import { AppLayout } from "./components/layout/AppLayout"; import { LoginPage } from "./pages/LoginPage"; import { RegisterPage } from "./pages/RegisterPage"; import { PortalHome } from "./pages/PortalHome"; +import { TradingInfoPage } from "./pages/portal/TradingInfoPage"; +import { PoliciesPage } from "./pages/portal/PoliciesPage"; +import { GuidesPage } from "./pages/portal/GuidesPage"; +import { FaqPage } from "./pages/portal/FaqPage"; +import { ProcurementPlatformPage } from "./pages/portal/ProcurementPlatformPage"; import { Workbench } from "./pages/Workbench"; import { Delegation } from "./pages/Delegation"; import { ProjectList } from "./pages/ProjectList"; @@ -34,6 +39,11 @@ export default function App() { } /> } /> + } /> + } /> + } /> + } /> + } /> } /> }> } /> diff --git a/front-end/src/app/components/XiaobinAssistant.tsx b/front-end/src/app/components/XiaobinAssistant.tsx index a4f3dac..532990c 100644 --- a/front-end/src/app/components/XiaobinAssistant.tsx +++ b/front-end/src/app/components/XiaobinAssistant.tsx @@ -1,6 +1,6 @@ import { useState, useRef, useEffect } from "react"; import { useNavigate } from "react-router"; -import { X, Send, Minus, Lock, ArrowRight, Sparkles } from "lucide-react"; +import { X, Send, Minus, Lock, ArrowRight, Sparkles, Bot } from "lucide-react"; import { Button } from "./ui/button"; import { cn } from "./ui/utils"; import { useRole } from "../role-context"; @@ -131,14 +131,48 @@ const knowledge: { key: string; ans: Knowledge }[] = [ const dataKeywords = ["报价", "投标价", "多少钱", "金额", "中标", "得分", "分数", "名单", "谁"]; -export function XiaobinAssistant() { +type PageType = "app" | "portal" | "login"; + +function getPointer(e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent) { + if ("touches" in e && e.touches.length > 0) { + return { x: e.touches[0].clientX, y: e.touches[0].clientY }; + } + if ("changedTouches" in e && e.changedTouches.length > 0) { + return { x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY }; + } + if ("clientX" in e) { + return { x: e.clientX, y: e.clientY }; + } + return { x: 0, y: 0 }; +} + +const ENTRY_PROACTIVE: Proactive[] = [ + { + type: "todo", + title: "欢迎使用智慧招标平台", + content: "您可以直接从首页查看交易信息、政策法规与操作指南。", + to: "/portal/trading-info", + actionLabel: "查看交易信息", + }, + { + type: "ai", + title: "新用户建议先注册", + content: "供应商用户可先完成注册与实名认证,后续即可参与报名与投标。", + to: "/register", + actionLabel: "立即注册", + }, +]; + +export function XiaobinAssistant({ pageType = "app" }: { pageType?: PageType }) { const { sensitive, role, roleId } = useRole(); const navigate = useNavigate(); - // 默认展开:用户进入工作台即可看到小滨的主动提醒,可随时收起 - const [open, setOpen] = useState(true); + // 工作台默认展开;首页/登录页默认收起,避免遮挡主内容 + const [open, setOpen] = useState(pageType === "app"); const [showChat, setShowChat] = useState(false); const [input, setInput] = useState(""); - const [proactive, setProactive] = useState(PROACTIVE[roleId] ?? []); + const [proactive, setProactive] = useState( + pageType === "app" ? PROACTIVE[roleId] ?? [] : ENTRY_PROACTIVE, + ); const [msgs, setMsgs] = useState([ { role: "bot", @@ -146,16 +180,97 @@ export function XiaobinAssistant() { }, ]); const endRef = useRef(null); + const dragRef = useRef({ dragging: false, dx: 0, dy: 0, sx: 0, sy: 0, moved: false }); + const [pos, setPos] = useState({ x: 0, y: 0 }); + const [dragging, setDragging] = useState(false); + const EDGE_GAP = 20; + + const panelSize = open ? { w: 360, h: 520 } : { w: 68, h: 68 }; + + function clamp(x: number, y: number) { + const pad = EDGE_GAP; + const maxX = Math.max(pad, window.innerWidth - panelSize.w - pad); + const maxY = Math.max(pad, window.innerHeight - panelSize.h - pad); + return { + x: Math.min(Math.max(x, pad), maxX), + y: Math.min(Math.max(y, pad), maxY), + }; + } + + function startDrag(e: React.MouseEvent | React.TouchEvent) { + const p = getPointer(e); + dragRef.current.dragging = true; + dragRef.current.sx = p.x; + dragRef.current.sy = p.y; + dragRef.current.moved = false; + setDragging(true); + dragRef.current.dx = p.x - pos.x; + dragRef.current.dy = p.y - pos.y; + } + + function snapToEdge(x: number, y: number) { + const pad = EDGE_GAP; + const maxX = Math.max(pad, window.innerWidth - panelSize.w - pad); + const maxY = Math.max(pad, window.innerHeight - panelSize.h - pad); + const targetX = x <= maxX / 2 ? pad : maxX; + return { + x: targetX, + y: Math.min(Math.max(y, pad), maxY), + }; + } // 切换角色时刷新主动提醒 useEffect(() => { - setProactive(PROACTIVE[roleId] ?? []); - }, [roleId]); + if (pageType === "app") setProactive(PROACTIVE[roleId] ?? []); + }, [roleId, pageType]); useEffect(() => { if (showChat) endRef.current?.scrollIntoView({ behavior: "smooth" }); }, [msgs, showChat]); + // 初始位置(右下角) + useEffect(() => { + const w = pageType === "app" ? 360 : 68; + const h = pageType === "app" ? 520 : 68; + const p = clamp(window.innerWidth - w - 32, window.innerHeight - h - 32); + setPos(p); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pageType]); + + useEffect(() => { + function onMove(e: MouseEvent | TouchEvent) { + if (!dragRef.current.dragging) return; + const p = getPointer(e); + if (!dragRef.current.moved && (Math.abs(p.x - dragRef.current.sx) > 4 || Math.abs(p.y - dragRef.current.sy) > 4)) { + dragRef.current.moved = true; + } + const next = clamp(p.x - dragRef.current.dx, p.y - dragRef.current.dy); + setPos(next); + } + function onUp() { + if (!dragRef.current.dragging) return; + dragRef.current.dragging = false; + setDragging(false); + setPos((prev) => snapToEdge(prev.x, prev.y)); + } + function onResize() { + setPos((prev) => clamp(prev.x, prev.y)); + } + window.addEventListener("mousemove", onMove); + window.addEventListener("touchmove", onMove, { passive: false }); + window.addEventListener("mouseup", onUp); + window.addEventListener("touchend", onUp); + window.addEventListener("resize", onResize); + return () => { + window.removeEventListener("mousemove", onMove); + window.removeEventListener("touchmove", onMove); + window.removeEventListener("mouseup", onUp); + window.removeEventListener("touchend", onUp); + window.removeEventListener("resize", onResize); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [open]); + const gradient = `linear-gradient(135deg, ${role.bannerFrom}, ${role.bannerTo})`; const unread = proactive.length; @@ -191,166 +306,194 @@ export function XiaobinAssistant() { } return ( - <> - {/* 悬浮球 + 呼吸光环(收起状态) */} +
{ + if (!open && !dragRef.current.moved) setOpen(true); + }} + > {!open && ( - - )} - - {open && ( -
- {/* 头部 */} -
-
-
-
-
小滨 AI 助手
-
- 在线 · 已为您分析今日待办 -
-
-
- -
- - {sensitive && ( -
- 当前为流程敏感期,数据类问题暂无法回答 -
- )} - -
- {!showChat ? ( - <> -
- - 📢 为您整理的今日提醒({proactive.length}) - - {proactive.length > 0 && ( - - )} -
- {proactive.length === 0 && ( -
- 🎉 暂无待办,继续保持! -
- )} - {proactive.map((p, i) => { - const st = TYPE_STYLE[p.type]; - return ( -
-
-
- {st.badge} {p.title} -
- -
-

{p.content}

- {p.to && ( - - )} -
- ); - })} -
-

猜你想问:

-
- {quickReplies.map((q) => ( - - ))} -
-
- - ) : ( - <> - - {msgs.map((m, i) => ( -
-
-
{m.text}
- {m.actions && m.actions.length > 0 && ( -
- {m.actions.map((a) => ( - - ))} -
- )} -
-
- ))} -
- - )} -
- -
- setInput(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && send(input)} - placeholder="输入您的问题…" - className="h-9 flex-1 rounded-full border bg-input-background px-3.5 text-sm outline-none focus:border-primary" - /> - -
)} - + +
+ {/* 头部 */} +
+
+
+
+
小滨 AI 助手
+
+ 在线 · 已为您分析今日待办 +
+
+
+ +
+ + {sensitive && ( +
+ 当前为流程敏感期,数据类问题暂无法回答 +
+ )} + +
+ {!showChat ? ( + <> +
+ + 📢 为您整理的今日提醒({proactive.length}) + + {proactive.length > 0 && ( + + )} +
+ {proactive.length === 0 && ( +
+ 🎉 暂无待办,继续保持! +
+ )} + {proactive.map((p, i) => { + const st = TYPE_STYLE[p.type]; + return ( +
+
+
+ {st.badge} {p.title} +
+ +
+

{p.content}

+ {p.to && ( + + )} +
+ ); + })} +
+

猜你想问:

+
+ {quickReplies.map((q) => ( + + ))} +
+
+ + ) : ( + <> + + {msgs.map((m, i) => ( +
+
+
{m.text}
+ {m.actions && m.actions.length > 0 && ( +
+ {m.actions.map((a) => ( + + ))} +
+ )} +
+
+ ))} +
+ + )} +
+ +
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && send(input)} + placeholder="输入您的问题…" + className="h-9 flex-1 rounded-full border bg-input-background px-3.5 text-sm outline-none focus:border-primary" + /> + +
+
+
); } diff --git a/front-end/src/app/components/layout/AppLayout.tsx b/front-end/src/app/components/layout/AppLayout.tsx index 4f95de6..650739d 100644 --- a/front-end/src/app/components/layout/AppLayout.tsx +++ b/front-end/src/app/components/layout/AppLayout.tsx @@ -21,15 +21,15 @@ export function AppLayout() { const initials = role.user.slice(0, 2); return ( -
+
-
+
@@ -37,10 +37,10 @@ export function AppLayout() {
- + {initials} @@ -109,7 +109,7 @@ export function AppLayout() {
-
+
diff --git a/front-end/src/app/components/layout/Sidebar.tsx b/front-end/src/app/components/layout/Sidebar.tsx index 0a8627d..2326bcc 100644 --- a/front-end/src/app/components/layout/Sidebar.tsx +++ b/front-end/src/app/components/layout/Sidebar.tsx @@ -9,9 +9,9 @@ export function Sidebar() { const { role } = useRole(); return ( -