跳转至

写作口味:Wiki 文档 & 报纸版 HTML

这篇收两套写作偏好:

  1. Wiki 文档 —— 用 MkDocs Material 写技术 wiki 时的语法约定,本 wiki 就在用
  2. 报纸版 HTML —— 给"审核大纲、项目复盘、内部简报"做的中文报纸观感模板

两个都来自我自己的 Claude Code skill。文章主体讲核心规范,详细资产(项目配置、CSS、JS、SVG)放最后折叠,需要的人展开复制。


Part 1:MkDocs Wiki 写作口味

本项目使用 MkDocs Material 主题。以下是项目已启用的插件特有语法。

项目配置、目录结构、导航配置、新增页面流程等参见 reference/项目配置.md


选项卡 ===(pymdownx.tabbed)

同一功能有多种语言/方案时使用,避免页面过长。本项目用得最多的特殊语法。

=== "Python"

    ```python
    import pandas as pd
    df = pd.read_csv("data.csv")
    ```

=== "R"

    ```R
    library(readr)
    df <- read_csv("data.csv")
    ```

要点: Tab 内容缩进 4 空格,=== 之间留空行。

提示框 !!!(admonition)

始终展开的彩色提示块,用于强调重要信息。

!!! note "补充说明"
    内容缩进4个空格。

!!! warning "注意"
    警告性内容。

!!! info "说明"
    信息介绍。

!!! tip "提示"
    操作建议。

!!! success "完成"
    成功标记。

可折叠块 ???(pymdownx.details)

默认折叠,点击展开。适合补充说明、长代码、详细参数。

??? note "命令的详细参数"
    - `-h`: 人类可读格式
    - `-T`: 显示文件系统类型

??? "点击查看完整 SQL"
    ```sql
    SELECT * FROM table
    ```

!!! 写法一致,仅 !!! 换成 ???。可以不写类型。

嵌套选项卡: 折叠块内可嵌套 Tab,注意每层缩进 4 空格:

??? note "多语言代码"

    === "Python"

        ```python
        print("hello")
        ```

    === "R"

        ```R
        cat("hello")
        ```

代码注解 # (1)(content.code.annotate)

在代码行尾标注编号,紧跟编号列表写解释,渲染后点击弹出说明。

```python
with open('file.sql') as f:  # (1)
    sql = f.read()
params = {"game_cd": 1041}  # (2)
  1. 说明第一处标注
  2. 说明第二处标注
    Python/bash 用 `# (1)`,SQL 用 `-- (1)`。编号列表紧跟代码块,不多空行。
    
    非代码场景需要加 `{ .annotate }`:
    
    ```markdown
    工作时间 9:30-18:30 (1)
    { .annotate }
    
    1. 最多可提前30分钟下班
    

代码行高亮 hl_lines(pymdownx.highlight)

高亮代码块中的指定行。

``` python hl_lines="3 5"
import pandas as pd
from jinja2 import Template
import os, time
os.chdir(os.path.dirname(os.path.abspath(__file__)))
os.environ['TZ'] = 'Asia/Shanghai'
多行用空格分隔,连续行用 `-` 连接(如 `hl_lines="2-4"`)。

### Mermaid 图表(pymdownx.superfences)

用文本生成流程图、时序图等。

```markdown
```mermaid
graph TD
    A[数据采集] --> B{数据类型}
    B -->|结构化| C[写入数据库]
    B -->|非结构化| D[文本处理]

sequenceDiagram
    participant 用户
    participant 服务端
    用户->>服务端: 发起请求
    服务端-->>用户: 响应数据
方向:`TD` 上到下,`LR` 左到右。节点:`[]` 矩形,`{}` 菱形,`(())` 圆形。

### 脚注 `[^1]`(footnotes)

正文标记,页面底部自动生成注释。

```markdown
文件最大只能20MB[^1]。

[^1]: 钉钉开放平台接口限制。

[^1]: 定义可放文件任意位置,MkDocs 自动收集到页底。


写作规范

  • 同一功能有 Python / R 两种写法 → 用选项卡 ===
  • 内容很长但非必读 → 用折叠 ???
  • 重要注意事项 → 用提示框 !!!(warning 易犯错误、tip 建议、note 补充)
  • 系统架构或交互流程 → 用 Mermaid 图
  • 缩进统一用 4个空格,不用 Tab 键
  • 代码块语言标识统一用 python(不用 py
  • 新增页面必须在 mkdocs.ymlnav: 中注册
项目配置(mkdocs.yml + 目录结构 + 新增页面流程)

项目配置

项目结构

项目根目录/
├── mkdocs.yml              # 配置文件
└── docs/                   # 所有文档
    ├── index.md            # 首页
    ├── <分类目录>/
    │   ├── xxx.md
    │   ├── imgs/           # 该目录下文档的图片
    │   └── <子目录>/
    │       └── xxx.md
    └── ...

图片放在对应文档目录下的 imgs/ 子目录,用相对路径引用:![描述](imgs/xxx.jpg)

新增页面流程

  1. docs/ 的合适目录下创建 .md 文件
  2. 编写内容(语法见 SKILL.md)
  3. mkdocs.ymlnav: 中注册路径,否则不会出现在导航栏
  4. mkdocs serve 本地预览(自动刷新,不用重启)
  5. git commit & push
nav:
  # 一级导航(显示为顶部标签页)
  - 欢迎新同学:
    - index.md

  # 带子页面的分组
  - 常用技能:
    - 如何使用git: linux/git.md         # 自定义显示名
    - 如何管理虚拟环境:                  # 二级分组
      - 常见场景/Python.md              # 自动取一级标题作为名称
      - 常见场景/R.md

  # 简单列表
  - 常用代码:
    - 常用代码/钉钉消息.md
    - 常用代码/数据库.md
  • - 显示名: 路径 → 自定义导航名
  • - 路径.md → 自动取文件一级标题
  • 路径相对于 docs/ 目录
  • 注释掉的行(#)不会显示在导航中

基础配置

site_name: BI新人手册
theme:
  name: material
  features:
    - content.code.annotate   # 代码注解 # (1)
    - navigation.tabs         # 顶部标签页导航
    - navigation.indexes      # 目录索引页

已启用的 Markdown 扩展

markdown_extensions:
  # 代码高亮
  - pymdownx.highlight:        # 代码块语法高亮 + hl_lines
      anchor_linenums: true
      line_spans: __span
      pygments_lang_class: true
  - pymdownx.inlinehilite      # 行内代码高亮
  - pymdownx.snippets          # 插入外部代码片段

  # 代码块增强
  - pymdownx.superfences:      # 增强围栏代码块 + Mermaid
      custom_fences:
        - name: mermaid
          class: mermaid
          format: !!python/name:pymdownx.superfences.fence_code_format

  # 内容组织
  - pymdownx.tabbed:           # 选项卡 ===
      alternate_style: true
  - pymdownx.details           # 可折叠块 ???
  - admonition                 # 提示框 !!!
  - footnotes                  # 脚注 [^1]

  # HTML 增强
  - attr_list                  # 属性列表 { .class }
  - md_in_html                 # HTML 标签内写 Markdown

添加新插件:在此列表中添加扩展名,部分插件需先 pip install 安装对应 Python 包。


Part 2:报纸版 HTML 设计口味

一套用于「审核大纲、分享提纲、项目复盘、研究摘要、内部简报、报告门户」的视觉风格:接近一张严肃但有冲击力的中文报纸。靠排版建立层级,不用装饰图片、渐变、阴影、圆角。

配套资源都在本 skill 的 assets/: - newspaper.css —— 设计系统 + 三个组件的样式(可直接 <link>) - newspaper.js —— 三个组件的交互(自初始化,class 钩子驱动) - favicon.svg —— 模板图标(可替换中间标记)

何时用 / 何时不用

  • :网页文档、审核稿、长页、报告列表/门户、需要打印或导出 PDF 的严肃页面。
  • 不用:演示页 / 滚动动画 / 投屏页面(那是另一套 16:9 舞台式 v0.3 风格,偏 Inter + 容器单位)。

接入方式

  1. assets/newspaper.cssassets/newspaper.jsassets/favicon.svg 到目标项目的静态目录(如 static/cssstatic/jsstatic/)。
  2. 页面 <head>
    <script>document.documentElement.className += ' js';</script>  <!-- 尽早执行,防原生下拉闪现 -->
    <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
    <link rel="stylesheet" href="/static/css/newspaper.css">
    
  3. </body> 前(仅当用到 JS 组件时):
    <script src="/static/js/newspaper.js"></script>
    
  4. 多页项目建议做一个 Jinja base.html 放上面这些公共头/尾,各页 {% extends %} + {% block content %},样式只维护一处。

设计令牌(必须照用)

--paper: #f3efe4;  /* 纸面背景,必须铺满 html,body,别只铺容器 */
--ink:   #151515;  /* 主文字、粗线、重点块 */
--muted: #595247;  /* 小号元信息、栏目名 */
--body:  #2e2a24;  /* 正文长段落,降低阅读疲劳 */
--accent:#8a2f1b;  /* 仅用于章节编号或少量重点,不做大面积背景 */

字体:"Songti SC", "Noto Serif CJK SC", "STSong", Georgia, serif。 字重:H1/H2/H3/H4 一律 900;次级强调(目标句、结论句)800;正文 16px / line-height 1.8不要负字距;标题 letter-spacing: 0,元信息可用较大正字距。

分割线系统(结构全靠线)

  • 大区块:4px solid --ink.section-rule / .part / 报头上下线)
  • 组件框 / 徽标 / kicker 上下线:2px solid --ink
  • 社论摘句左线:8px solid --ink.core-left
  • 避免:细灰线过多、阴影、渐变线、圆角。

现成类(在 newspaper.css 里)

  • 版心 .page;报头 .masthead + .meta + h1 + .subtitle;栏目条 .kicker
  • 区块 .section-rule / .part / .part-number;摘句 .core-left / .core-invert
  • 按钮 .btn / .btn-solid / .btn.disabled;徽标 .badge;信息框 .card;空状态 .no-data(+.hint)

骨架示例:

<div class="page">
  <header class="masthead">
    <div class="meta"><span>栏目 · KICKER</span><span>右侧元信息</span></div>
    <h1>大标题</h1>
    <p class="subtitle">副标题</p>
  </header>
  <div class="kicker">栏目条</div>
  <!-- 内容 -->
</div>

组件(需引 newspaper.js,全部 class 钩子驱动,无硬编码 id)

1. 自定义下拉(连展开列表也走报纸版风格)

给原生 <select>class="np-select":JS 会隐藏它、生成 .custom-select 列表(原生 select 仍作数据源 + 无 JS 兜底)。 - 想「改值即整页提交」:把 select 放进 <form data-np-autosubmit>,并加无 JS 兜底按钮 <noscript><button type="submit" class="btn">应用</button></noscript>

<form method="get" data-np-autosubmit>
  <select name="dept" class="np-select"><option value="">全部</option></select>
  <noscript><button type="submit" class="btn">应用</button></noscript>
</form>

2. 复制链接 + toast

<button class="np-copy" data-link="/view/foo">复制链接</button>
data-link 为相对路径时自动补 window.location.origin;绝对 URL 原样复制。页面有 .toast 则复用,否则自动创建。

3. 加载遮罩(整页跳转 / 慢加载时的局部反馈)

把要被遮罩的内容包进 .np-loading-region,内置一个遮罩节点;任意表单 submit 时自动 .showpageshow(后退)时自动清除。

<div class="np-loading-region">
  <div class="loading-overlay">
    <div class="loading-box"><div class="label">正在加载…</div><div class="loading-bar"></div></div>
  </div>
  <!-- 结果内容 -->
</div>

响应式

断点 860px:多列网格全部变单列,.page 收窄边距,保留粗线、不退化成卡片。各页自己的多列布局(grid/flex)在该断点改单列即可。

favicon

assets/favicon.svg 是极简圆框 + 两字母大字(默认 BI)+ 蓝图辅助网格的模板标记。要换成团队标识,改 <text> 里的字母即可——文件头注释里有示例。

配套资产文件

以下三个文件是这套风格的实际实现,复制到你的项目 static/ 下即可使用。

newspaper.css(设计令牌 + 组件样式,约 300 行)
/* ============================================================
   报纸版设计系统 (newspaper-style)
   暖米纸面 · 粗黑分割线 · 宋体大标题 · 强对比 · 可打印
   用法:<link rel="stylesheet" href=".../newspaper.css">
   配合 newspaper.js 可启用自定义下拉 / 复制链接 / 加载遮罩组件。
   ============================================================ */

/* ---- 设计令牌 ---- */
:root {
    --paper: #f3efe4;  /* 暖米色纸张背景 */
    --ink: #151515;    /* 主文字与粗线 */
    --muted: #595247;  /* 报头元信息、栏目 kicker */
    --body: #2e2a24;   /* 正文色,比纯黑柔和 */
    --accent: #8a2f1b; /* 强调色(章节编号 / 少量重点) */
}

/* ---- 基础 ---- */
* { box-sizing: border-box; }

html,
body {
    min-height: 100vh;
    margin: 0;
    background: var(--paper);
    color: var(--ink);
    font-family: "Songti SC", "Noto Serif CJK SC", "STSong", Georgia, serif;
}

/* 列向 flex,配合 .page 的四向 auto margin 实现版心上下居中 */
body {
    display: flex;
    flex-direction: column;
}

/* ---- 版心 ---- */
.page {
    width: min(1120px, calc(100% - 40px));
    /* 四向 auto:内容比视口短时上下左右居中,超高时也不裁切顶部 */
    margin: auto;
    padding: 24px 0 36px;
}

/* ---- 报头 ---- */
.masthead {
    border-top: 4px solid var(--ink);
    border-bottom: 4px solid var(--ink);
    padding: 22px 0 24px;
}

.masthead .meta {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    color: var(--muted);
    font-size: 14px;
    font-weight: 800;
    letter-spacing: 0.18em;
}

.masthead h1 {
    margin: 24px 0 0;
    max-width: 900px;
    font-size: clamp(40px, 8vw, 88px);
    line-height: 0.95;
    font-weight: 900;
    letter-spacing: 0;
}

.masthead .subtitle {
    margin: 16px 0 0;
    font-size: 18px;
    font-weight: 800;
    color: var(--body);
}

/* ---- 栏目条 ---- */
.kicker {
    margin: 28px 0 20px;
    border-top: 2px solid var(--ink);
    border-bottom: 2px solid var(--ink);
    padding: 8px 0;
    color: var(--muted);
    text-align: center;
    font-size: 14px;
    font-weight: 800;
    letter-spacing: 0.26em;
}

/* ---- 区块分割线 ---- */
.section-rule { border-bottom: 4px solid var(--ink); padding: 26px 0; }
.part { border-bottom: 4px solid var(--ink); padding-bottom: 24px; }
.part-number { color: var(--accent); font-size: 14px; font-weight: 900; letter-spacing: 0.24em; }

/* ---- 社论摘句 ---- */
.core-left {
    margin-top: 22px;
    border-left: 8px solid var(--ink);
    padding-left: 16px;
    font-size: 22px;
    line-height: 1.55;
    font-weight: 900;
}
.core-invert {
    margin-top: 22px;
    border: 2px solid var(--ink);
    background: var(--ink);
    color: var(--paper);
    padding: 16px;
    font-size: 22px;
    line-height: 1.55;
    font-weight: 900;
}

/* ---- 按钮(padding/font-size 可在使用处覆盖)---- */
.btn {
    border: 2px solid var(--ink);
    background: var(--paper);
    color: var(--ink);
    text-decoration: none;
    padding: 9px 18px;
    font-size: 15px;
    font-weight: 900;
    font-family: inherit;
    letter-spacing: 0.06em;
    white-space: nowrap;
    cursor: pointer;
}
.btn:hover { background: var(--ink); color: var(--paper); }
.btn-solid { background: var(--ink); color: var(--paper); }
.btn-solid:hover { background: var(--paper); color: var(--ink); }
.btn.disabled {
    background: var(--paper);
    color: var(--muted);
    border-color: var(--muted);
    cursor: not-allowed;
}

/* ---- 徽标 ---- */
.badge {
    display: inline-block;
    border: 2px solid var(--ink);
    background: var(--ink);
    color: var(--paper);
    padding: 2px 10px;
    font-size: 13px;
    font-weight: 800;
    letter-spacing: 0.06em;
    margin-left: 8px;
}

/* ---- 信息框(不是现代卡片:方角、无阴影)---- */
.card { border: 2px solid var(--ink); padding: 16px 18px; }

/* ---- 空状态 ---- */
.no-data {
    border: 2px solid var(--ink);
    text-align: center;
    padding: 40px 20px;
    font-size: 18px;
    font-weight: 800;
}
.no-data .hint { font-size: 15px; font-weight: 800; color: var(--muted); margin-top: 10px; }

/* ============================================================
   组件 1:复制提示 toast(配合 newspaper.js)
   ============================================================ */
.toast {
    position: fixed;
    left: 50%;
    bottom: 32px;
    transform: translateX(-50%);
    border: 2px solid var(--ink);
    background: var(--ink);
    color: var(--paper);
    padding: 12px 24px;
    font-size: 15px;
    font-weight: 900;
    letter-spacing: 0.1em;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease;
    z-index: 1000;
}
.toast.show { opacity: 1; }

/* ============================================================
   组件 2:自定义下拉(配合 newspaper.js)
   原生 <select class="np-select"> 作为数据源与无 JS 兜底;
   JS 启用后隐藏它、生成 .custom-select 列表。
   需在 <head> 尽早执行 document.documentElement.className += ' js'。
   ============================================================ */
.js select.np-select { display: none; }

.custom-select { position: relative; width: 100%; }

.custom-select .cs-trigger {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    width: 100%;
    padding: 10px 12px;
    border: 2px solid var(--ink);
    border-radius: 0;
    background: var(--paper);
    color: var(--ink);
    font-family: inherit;
    font-size: 16px;
    font-weight: 800;
    text-align: left;
    cursor: pointer;
}
.custom-select.open .cs-trigger { border-color: var(--accent); }

.custom-select .cs-arrow {
    flex: none;
    width: 0;
    height: 0;
    border-left: 6px solid transparent;
    border-right: 6px solid transparent;
    border-top: 7px solid var(--ink);
}
.custom-select.open .cs-arrow { transform: rotate(180deg); }

.custom-select .cs-menu {
    position: absolute;
    left: 0;
    right: 0;
    top: calc(100% - 2px);
    z-index: 30;
    margin: 0;
    padding: 0;
    list-style: none;
    border: 2px solid var(--ink);
    background: var(--paper);
    max-height: 280px;
    overflow-y: auto;
    display: none;
    scrollbar-width: thin;
    scrollbar-color: var(--ink) var(--paper);
}
.custom-select.open .cs-menu { display: block; }

.custom-select .cs-menu::-webkit-scrollbar { width: 12px; }
.custom-select .cs-menu::-webkit-scrollbar-track { background: var(--paper); border-left: 2px solid var(--ink); }
.custom-select .cs-menu::-webkit-scrollbar-thumb { background: var(--ink); border: 2px solid var(--paper); }
.custom-select .cs-menu::-webkit-scrollbar-thumb:hover { background: var(--accent); }

.custom-select .cs-option {
    padding: 10px 12px;
    font-size: 15px;
    font-weight: 800;
    color: var(--body);
    cursor: pointer;
    border-bottom: 1px solid rgba(21, 21, 21, 0.15);
}
.custom-select .cs-option:last-child { border-bottom: none; }
.custom-select .cs-option.selected { color: var(--accent); }
.custom-select .cs-option:hover,
.custom-select .cs-option.active { background: var(--ink); color: var(--paper); }

/* ============================================================
   组件 3:加载遮罩(配合 newspaper.js)
   把内容放进 .np-loading-region(相对定位容器),
   内置一个 .loading-overlay,切换/提交时加 .show。
   ============================================================ */
.np-loading-region { position: relative; min-height: 160px; }

.loading-overlay {
    position: absolute;
    inset: 0;
    background: rgba(243, 239, 228, 0.88);
    display: none;
    align-items: center;
    justify-content: center;
    z-index: 10;
}
.loading-overlay.show { display: flex; }

.loading-box { border: 2px solid var(--ink); background: var(--paper); padding: 24px 32px; text-align: center; }
.loading-box .label { font-size: 18px; font-weight: 900; letter-spacing: 0.2em; color: var(--ink); margin-bottom: 14px; }
.loading-bar { width: 220px; height: 10px; border: 2px solid var(--ink); overflow: hidden; }
.loading-bar::before {
    content: "";
    display: block;
    width: 40%;
    height: 100%;
    background: var(--ink);
    animation: loading-slide 1s linear infinite;
}
@keyframes loading-slide {
    0% { transform: translateX(-110%); }
    100% { transform: translateX(275%); }
}

/* ---- 响应式:860px 断点,多列变单列、保留粗线 ---- */
@media (max-width: 860px) {
    .page { width: calc(100% - 28px); }
}
newspaper.js(三个组件的交互逻辑,约 180 行)
/* ============================================================
   报纸版交互组件 (newspaper-style)
   全部基于 class/属性钩子,自初始化,无硬编码 id。
   放在 </body> 前: <script src=".../newspaper.js"></script>
   (另需在 <head> 尽早放:
      <script>document.documentElement.className += ' js';</script>
    以避免原生下拉闪现。本文件也会兜底补上 .js 类。)

   钩子约定:
   - 自定义下拉:    <select class="np-select"> … </select>
   - 改值即提交:    把 select 放进 <form data-np-autosubmit> (可选)
   - 加载遮罩:      表单提交时,自动给页面里的 .loading-overlay 加 .show
   - 复制链接:      <button class="np-copy" data-link="/foo">复制链接</button>
   - 提示 toast:    页面有 <div class="toast"></div> 则复用,否则自动创建
   ============================================================ */
(function () {
    document.documentElement.className += ' js';

    /* ---- toast ---- */
    var toastEl = document.querySelector('.toast');
    var toastTimer = null;
    function ensureToast() {
        if (!toastEl) {
            toastEl = document.createElement('div');
            toastEl.className = 'toast';
            document.body.appendChild(toastEl);
        }
        return toastEl;
    }
    function showToast(msg) {
        var t = ensureToast();
        t.textContent = msg;
        t.classList.add('show');
        if (toastTimer) clearTimeout(toastTimer);
        toastTimer = setTimeout(function () { t.classList.remove('show'); }, 1600);
    }

    /* ---- 加载遮罩 ---- */
    function showLoading() {
        document.querySelectorAll('.loading-overlay').forEach(function (o) { o.classList.add('show'); });
    }
    function hideLoading() {
        document.querySelectorAll('.loading-overlay').forEach(function (o) { o.classList.remove('show'); });
    }
    // 后退/bfcache 恢复时清除残留遮罩
    window.addEventListener('pageshow', hideLoading);
    // 任意表单提交(含原生按钮)都显示遮罩
    document.querySelectorAll('form').forEach(function (f) {
        f.addEventListener('submit', showLoading);
    });

    /* ---- 复制链接 ---- */
    function fallbackCopy(text) {
        var ta = document.createElement('textarea');
        ta.value = text;
        ta.style.position = 'fixed';
        ta.style.opacity = '0';
        document.body.appendChild(ta);
        ta.select();
        try { document.execCommand('copy'); showToast('链接已复制'); }
        catch (e) { showToast('复制失败,请手动复制'); }
        document.body.removeChild(ta);
    }
    document.querySelectorAll('.np-copy').forEach(function (btn) {
        btn.addEventListener('click', function () {
            var link = btn.getAttribute('data-link') || '';
            var full = /^https?:/i.test(link) ? link : (window.location.origin + link);
            if (navigator.clipboard && navigator.clipboard.writeText) {
                navigator.clipboard.writeText(full).then(
                    function () { showToast('链接已复制'); },
                    function () { fallbackCopy(full); }
                );
            } else {
                fallbackCopy(full);
            }
        });
    });

    /* ---- 自定义下拉 ---- */
    function closeAll(except) {
        document.querySelectorAll('.custom-select.open').forEach(function (w) {
            if (w !== except && w._close) w._close();
        });
    }

    function buildSelect(sel) {
        var autosubmit = !!sel.closest('form[data-np-autosubmit]');

        var wrap = document.createElement('div');
        wrap.className = 'custom-select';

        var trigger = document.createElement('button');
        trigger.type = 'button';
        trigger.className = 'cs-trigger';
        trigger.setAttribute('aria-haspopup', 'listbox');
        trigger.setAttribute('aria-expanded', 'false');

        var label = document.createElement('span');
        label.className = 'cs-label';
        var arrow = document.createElement('span');
        arrow.className = 'cs-arrow';
        trigger.appendChild(label);
        trigger.appendChild(arrow);

        var menu = document.createElement('ul');
        menu.className = 'cs-menu';
        menu.setAttribute('role', 'listbox');

        var activeIdx = -1;
        function setActive(i) {
            var items = menu.children;
            for (var k = 0; k < items.length; k++) items[k].classList.toggle('active', k === i);
            activeIdx = i;
            if (i >= 0 && items[i]) items[i].scrollIntoView({ block: 'nearest' });
        }
        function syncLabel() { label.textContent = sel.options[sel.selectedIndex].textContent.trim(); }
        function choose(i) {
            sel.selectedIndex = i;
            syncLabel();
            close();
            sel.dispatchEvent(new Event('change', { bubbles: true }));
        }
        function open() {
            closeAll(wrap);
            wrap.classList.add('open');
            trigger.setAttribute('aria-expanded', 'true');
            setActive(sel.selectedIndex);
        }
        function close() {
            wrap.classList.remove('open');
            trigger.setAttribute('aria-expanded', 'false');
            setActive(-1);
        }
        wrap._close = close;

        Array.prototype.forEach.call(sel.options, function (opt, i) {
            var li = document.createElement('li');
            li.className = 'cs-option' + (opt.selected ? ' selected' : '');
            li.setAttribute('role', 'option');
            li.textContent = opt.textContent.trim();
            li.addEventListener('click', function (e) { e.stopPropagation(); choose(i); });
            menu.appendChild(li);
        });
        syncLabel();

        trigger.addEventListener('click', function (e) {
            e.stopPropagation();
            if (wrap.classList.contains('open')) close(); else open();
        });
        trigger.addEventListener('keydown', function (e) {
            var n = menu.children.length;
            if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
                e.preventDefault();
                if (!wrap.classList.contains('open')) { open(); return; }
                var ni = activeIdx + (e.key === 'ArrowDown' ? 1 : -1);
                if (ni < 0) ni = 0;
                if (ni >= n) ni = n - 1;
                setActive(ni);
            } else if (e.key === 'Enter' || e.key === ' ') {
                if (wrap.classList.contains('open')) {
                    e.preventDefault();
                    if (activeIdx >= 0) choose(activeIdx);
                }
            } else if (e.key === 'Escape') {
                close();
            }
        });

        // 改值即提交(可选):先显示遮罩再提交
        if (autosubmit) {
            sel.addEventListener('change', function () {
                showLoading();
                sel.form.submit();
            });
        }

        wrap.appendChild(trigger);
        wrap.appendChild(menu);
        sel.parentNode.appendChild(wrap);
    }

    document.querySelectorAll('select.np-select').forEach(buildSelect);
    document.addEventListener('click', function () { closeAll(null); });
})();
favicon.svg(模板图标,改中间文字即可换标识)
<!-- 模板 favicon:极简圆框 + 两字母大字 + 蓝图辅助网格。
     换成团队标识时,改 <text> 里的字母即可(如 BI / NS / XX)。 -->
<svg id="bi-svg-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%" style="background:#FFFFFF;">
<!-- 极简画布背景 -->
<rect width="512" height="512" fill="#FFFFFF"/>
<!-- 基础圆框容器 -->
<circle cx="256" cy="256" r="232" fill="none" stroke="#000000" stroke-width="8"/>
<!-- 绝对中心对齐核心大字 -->
<text x="254" y="257" text-anchor="middle" dominant-baseline="central" style="font-family: 'Inter', sans-serif; font-weight: 900; letter-spacing: -15px;" font-size="330" fill="#000000">BI</text>
<!-- 辅助设计线 -->
<g opacity="0.2">
<pattern id="icon-grid" width="32" height="32" patternUnits="userSpaceOnUse">
<path d="M 32 0 L 0 0 0 32" fill="none" stroke="#000000" stroke-width="0.5"/>
</pattern>
<rect width="512" height="512" fill="url(#icon-grid)"/>
<line x1="256" y1="0" x2="256" y2="512" stroke="#FF0000" stroke-width="1" stroke-dasharray="2 2"/>
<line x1="0" y1="256" x2="512" y2="256" stroke="#FF0000" stroke-width="1" stroke-dasharray="2 2"/>
<circle cx="256" cy="256" r="236" fill="none" stroke="#000000" stroke-width="0.5" stroke-dasharray="4 4"/>
<circle cx="256" cy="256" r="128" fill="none" stroke="#000000" stroke-width="0.5" stroke-dasharray="4 4"/>
</g>
</svg>