feat(frontend): 对齐万华门户风格并更新全量截图文档
- 新增门户导航子页面(交易信息、政策法规、操作指南、常见问题、采购平台)并统一门户壳层样式 - 按万华站点风格重构首页与登录页(导航、轮播、公告、登录区) - 优化小滨助手:首页/登录页接入、可拖拽吸边、圆形图标、圆形外框与切换动画优化 - 重新生成全量页面截图并更新《滨化智慧招标平台-前端界面截图汇总.docx》 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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)} 个章节")
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user