# MVFrame

[English](README.md) | **简体中文**

面向 **Vue 3 + Vite** 的管理端场景封装：**布局（Frame）**、**常用业务组件**、**全局工具/指令**、**Pinia 工厂与 Tabs 路由联动**、**可组合的 composition 模块**以及 **SCSS 工具类与主题变量**。工具与部分构建配置针对 SaaS 后台类项目做了取舍，更适合与同类技术栈的项目对齐使用，而非追求完全零依赖的通用库。

---

## 技术栈与依赖

| 类型 | 说明 |
|------|------|
| 运行时 | `vue` ^3.3、`vue-router` ^4.6、`pinia` ^3 |
| 演示 / 开发 | `element-plus`（demo 中用于表单等）、`vite` ^6、`sass-embedded` |
| 构建 | Library 模式产物为 ES 模块；`vue` / `pinia` / `vue-router` 为 **external**，由上层应用提供 |

`package.json` 中声明的发布入口包括：

- `"."` → `dist/index.js`（主插件，默认导出 `install` 函数）
- `"./composition"` → `dist/composition.js`（`import { useMap, … } from 'mvframe/composition'`）
- `"./store"`、`"./directive"`、`"./util"` → 对应 `dist` 下分包
- `"./notify"` → `dist/notify.js`（浏览器端请求本地 `mvframe-notify` 服务的通知 helper）
- `"./style/mixin"` → `src/style/chip/mixin.scss`（宿主项目 SCSS 可复用的断点 / 通用 mixin）
- `"./style"` → `dist/css/style.css`（由 `src/style/index.scss` 单独入口打包的全量工具类/变量）
- `"./style/cpt"` → `dist/css/cpt.css`（随组件抽取的样式；一般用 `mvframe/style` 即可）

---

## 仓库目录概览

### `src/` — 库源码

| 路径 | 作用 |
|------|------|
| `src/index.js` | **统一安装入口**：依次注册 `config` → 全局组件 `cpt` → `util` → `directive` → `store` → `router` |
| `src/config/` | 合并默认配置与调用方传入项，挂载 `globalThis.$config`（`chip/base.js` 含 name、copyright、version 等） |
| `src/cpt/index.js` | 通过 `import.meta.glob("../component/*/index.vue")` **自动全局注册**所有「一层子目录 + `index.vue`」的组件，组件名与**文件夹名**一致（PascalCase，如 `BtnGroup`） |
| `src/component/` | 全局组件实现，根节点 class 约定为 `Mvc` + 组件名（详见 `.cursor/rules/component-hierarchy.mdc`） |
| `src/util/index.js` | 挂到 `app.config.globalProperties` 与 `globalThis` 的工具方法（如 `$getLang`、`$fa`、`$copy`、`$deepClone` 等） |
| `src/directive/index.js` | 全局指令：`v-copy`（支持 `.dblclick`）、`v-focus` |
| `src/store/` | `createPinia` + 内置 `init` / `tab` / `rmenu`；支持 `storeChips` 动态注册；`provide("store", store)` |
| `src/router/` | `createWebHistory` 创建路由；可选 `guard`；内置 `guardThis`（`meta.admin`、`afterEach` 中与 `tab` 联动、`document.title`） |
| `src/composition/` | **按需 import**，导出 `lang` / `dom` / `data` / `media` 等（别名 `@cps` 在 Vite 中指向此目录） |
| `src/style/` | 样式总入口 `index.scss`，按需 `@use` 各 `chip/*.scss`（变量、间距、字体、布局、组件覆盖等） |

**当前 `src/component/` 下的全局组件（文件夹名即注册名）：**

`BtnGroup`、`Form`、`Frame`、`Icon`、`Input`、`Page`、`Select`、`SelectV2`、`Table`、`Tabs`、`Textarea`、`VTable`

`VTable` 默认通过 `defineAsyncComponent` 异步注册，只有实际渲染 `<VTable>` 时才加载 VisActor 相关 chunk。

子目录 `chip/` 为各组件内部拆片（如 `Frame/chip/Menu.vue`、`Table/chip/ColumnConfig.vue`），不单独注册。

### `demo/` — 本地预览工程

Vite 在 **`NODE_ENV=development`** 下将 **`root` 设为 `./demo`**（见根目录 `vite.config.js`），用于跑通 Frame + 路由 + 组件演示。

| 文件 | 作用 |
|------|------|
| `demo/index.html` | 入口 HTML |
| `demo/main.js` | 创建应用：`ElementPlus` + 引入 `../src/index.js` + `../src/style/index.scss` + `routes` |
| `demo/App.vue` | 使用 **`Frame`** 布局，传入 `menu: { iconClass: 'imicon', routes }`，插槽 `logo` / `logomini` |
| `demo/routes.js` | 演示路由表：`/` → `Overview/Home`（组件与 Table 演示），`/a` → `A/Home`；含多级 `children` 用于侧栏结构 |
| `demo/views/Overview/Home.vue` | 使用 **`Page`** + **`Tabs`**，`Tabs` 切换 **`FormPanel`** / **`IconPanel`** |
| `demo/views/Overview/Home/FormPanel.vue` | Form 等表单控件演示 |
| `demo/views/Overview/Home/IconPanel.vue`、`iconfontAntNames.js` | 图标展示与 Ant 图标名列表 |
| `demo/auto-imports.d.ts` | `unplugin-auto-import` 生成的类型声明（Vue / vue-router） |

本地开发：

```bash
yarn install
yarn dev
```

默认开发服务器端口为 **8088**（见 `vite.config.js`）。

### 路径别名（开发与库内）

| 别名 | 指向 |
|------|------|
| `@` | `src/` |
| `@cps` | `src/composition/` |
| `@scss` | `assets/scss`（若存在） |

演示里路由组件使用形如 `import("/views/Overview/Home.vue")` 的路径，解析相对于 **`demo`** 根目录。

---

## 在上层应用中安装

### 1. 安装依赖

业务项目需自行安装并对齐版本：**`vue`、`vue-router`、`pinia`**。若使用 `Form` / `Table` 等与 Element Plus 绑定的能力，还需安装 **`element-plus`** 并在应用中 `app.use(ElementPlus)`。

### 2. 注册 MVFrame

```js
import { createApp } from "vue";
import mvframe from "mvframe";
// 样式：源码依赖 / monorepo 可用包内路径；仅发布 dist 时见下文「样式」小节
import "mvframe/src/style/index.scss";

const app = createApp(App);

app.use(mvframe, {
  components: {
    async: ["VTable"],
  },
  vueRouter: {
    routes: yourRoutes,
    // 可选：自定义 beforeEach 等
    // guard: (router) => { /* ... */ },
    // useAdmin: true,
    // adminPermission: () => true,
    // noaccess: (next) => next(false),
  },
  pinia: {
    useTab: true, // 为 true 时启用多 Tab 与 localStorage 恢复（tabs / ctab）
    // storeChips: import.meta.glob("./pinia/chip/*.js", { eager: true }),
  },
  config: {
    // 会与 src/config/chip/base.js 合并后赋给 globalThis.$config
    iconfont: {
      url: "//at.alicdn.com/t/c/your_font.js",
      prefix: "ant",
    },
  },
});
```

`components.async` 用于控制全局组件异步注册。`VTable` 默认异步，以避免 `@visactor/vtable` 进入首屏同步组件链；在业务配置中显式写出可以保留这个体积策略的意图。

**Frame** 组件在挂载时根据 `globalThis.$config.iconfont.url` 动态插入 `script` 加载图标字体（加载后源码中会删除 `url` 字段以避免重复插入）。

### 3. 全局能力摘要

- **`globalThis.$config`**：应用配置  
- **`globalThis.$router`**：安装后由本库赋值的 router 实例  
- **`inject("store")`**：得到 **store 工厂对象**，如 `store.tab()`、`store.rmenu()` 及自定义 chip  
- **全局组件**：直接使用 `Frame`、`Page`、`Table` 等，无需再注册  
- **指令**：`v-copy`、`v-focus`  
- **composition**：`import { ... } from "mvframe/composition"` 或项目内 `@cps` 指向的等价路径  

工具方法完整列表与约定见仓库内 **`.cursor/rules/util.mdc`** 与 **`src/util/index.js`**。

### 4. 样式

- 工具类与变量说明见 `.cursor/rules/style-system.mdc`（`--color-*`、间距、字体、布局类等）。  
- SCSS 会通过 `vite.config.js` 的 **`additionalData`** 注入 **`src/style/chip/mixin.scss`**，与库同构构建的业务项目可保持一致配置。  
- 响应式断点统一在 `mixin.scss`：`xs / sm / md / lg / xl / xxl`；业务 SCSS 可直接 `@use "mvframe/style/mixin" as *;`，然后使用 `@include media-up(md)`、`media-down(md)`、`media-only(md)`、`media-between(sm, lg)`。  
- 发布包的 `mvframe/style` 里已内置一组响应式工具类：`{断点}-{up|down|only}-{工具类}`，例如 `md-down-hide`、`lg-up-flexMode`、`xs-only-block`。  
- Grid 容器还支持断点列数类：`class="grid col2 md2 lg3 sm1"`，表示默认 2 列，`sm` 1 列，`md` 2 列，`lg` 3 列。  
- 发布包可 **`import 'mvframe/style'`**（即 `dist/css/style.css`）；需与 Element Plus 等并存时仍可在业务工程中链入源码 **`src/style/index.scss`** 以便覆写变量。

业务侧示例：

```scss
@use "mvframe/style/mixin" as *;

.BillingPanel {
  padding: 1.5rem;

  @include media-down(sm) {
    padding: 1rem;
  }
}
```

---

## 构建库

```bash
yarn build
```

生产构建使用 **library 模式**，入口为 `src/index.js`，并对产物启用混淆等插件（见 `vite.config.js`）。**开发时** `NODE_ENV` 为 `development` 时走的是 **demo** 工程，若需本地验证构建结果，请在执行 `vite build` 时使用生产环境变量。

---

## 钉钉通知服务

MVFrame 提供只支持钉钉机器人的本地 Node 服务，命令由框架包提供：

```bash
# 仅启动钉钉通知服务
yarn notify
# 或发布包命令
yarn exec mvframe-notify

# 启动 MVFrame 封装的 Vite dev
yarn d
# 或发布包命令
yarn exec mvframe-d

# 同时启动 Vite dev 与通知服务
yarn exec mvframe-d -n
```

`mvframe-d` 默认只启动封装后的 Vite dev server；追加 `-n` / `--notify` 时才启动通知服务。通知服务启动失败只打印异常，不会停止 Vite。

服务读取宿主项目根目录的 `.env.mvframe-notify`（可用 `--env` 指定）：

```env
MVFRAME_NOTIFY_PORT=3300
MVFRAME_NOTIFY_HOST=127.0.0.1
MVFRAME_NOTIFY_TOKEN=
DINGTALK_WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxx"
DINGTALK_SECRET="SECxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

浏览器端只配置本地服务地址和访问 token：

```js
app.use(mvframe, {
  config: {
    notify: {
      enabled: true,
      endpoint: import.meta.env.VITE_MVFRAME_NOTIFY_ENDPOINT || "http://127.0.0.1:3300",
      token: import.meta.env.VITE_MVFRAME_NOTIFY_TOKEN || "",
    },
  },
});
```

发送消息：

```js
await globalThis.$notify.send("需要发送到钉钉的消息");

import { notify } from "mvframe/notify";
await notify("需要发送到钉钉的消息");
```

钉钉 webhook / secret 必须留在 Node 服务环境变量里，不要写进 `main.js`、`src/config/index.js` 或其它会被 Vite 打包到浏览器的文件。

---

## 设计取向与注意

- 布局、路由守卫、Tab、菜单等 **强绑定一套交互模型**；深度定制时可局部 fork `src/router`、`src/store/tab.js` 或外层包一层封装。  
- **组件 class 与单位**：根节点 `Mvc*` + 子命名 camelCase；组件内尺寸除 1px/2px 外建议 **rem**（根号 16px），与 `style-system.mdc` 一致。  
- 工具函数、Store、路由在非 setup 场景（如守卫）中通过 **`globalThis`** 或 **`store` / `pinia` 导出**访问，与 `src/router/chip/guard.js` 用法一致。

## 表单 Label

`Input`、`Textarea`、`Select`、`SelectV2` 现已支持统一的 label 接口：

```vue
<Input v-model="form.name" label="Name" />
<Input v-model="form.name" label="Material Name" material-label />

<Textarea v-model="form.remark" label="Remark" />
<Textarea v-model="form.remark" label="Material Remark" material-label />

<Select v-model="form.type" :options="options" label="Type" />
<Select v-model="form.type" :options="options" label="Material Type" material-label />
```

规则：

- 只传 `label`：渲染普通上方标题
- 传 `label` + `material-label`：渲染 Material 风格浮动标签
- 不传 `label`：保持组件默认外观

浮动 label 的公共壳样式统一实现在 `src/style/chip/element.scss`。如果你希望表单控件呈现 Material 风格，优先直接使用 `label` + `material-label`，不要在业务侧再重复造一套浮动标题样式。

---

## 命令行：项目目录雏形（推荐）

在**空目录或已有 Vite 工程根目录**执行，生成 `src/views`、`src/component`、`src/api`、`src/assets/img`、`src/assets/style`、`src/router`、`src/pinia/chip`、`src/config` 及示例 `main.js` / `App.vue`（已 `app.use(mvframe, …)` 挂好路由、Pinia `storeChips`、config，并写入 `components.async: ["VTable"]`），同时写入/更新宿主项目 **`AGENTS.md`** 的 MVFrame Codex 规则，其中包含 **每次 AI 完成开发后执行 `yarn exec mvframe-notify --once --message "..."` 发送完成通知** 的要求，并复制 **`/.cursor/rules/*.mdc`**（与 mvframe 仓库内 Cursor 规则一致）。若不存在则附带 `index.html` 与 `vite.config.js`。会**合并** `package.json` 的 `scripts` / `dependencies` / `devDependencies`（与 Vite 模板一致；**同名脚本和包保留你原有值**），初始化 `.env.mvframe-notify.example` / `.env.local.example` 与 `config.notify`，并自动执行 **`yarn install`**，最后请执行 **`yarn dev`**；需要同时启动通知服务时显式执行 **`yarn exec mvframe-d -n`**。加 `--no-package-json` 可不改动 `package.json` 且跳过安装。

```bash
cd /path/to/your-app
node /path/to/mvframe/scripts/scaffold-app.js
# 覆盖已有同名文件：
node /path/to/mvframe/scripts/scaffold-app.js --force
node /path/to/mvframe/scripts/scaffold-app.js -f

# 不修改 package.json（仅生成源码与配置）：
node /path/to/mvframe/scripts/scaffold-app.js --no-package-json
node /path/to/mvframe/scripts/scaffold-app.js -n

# 安装 mvframe 后：
yarn exec mvframe-init-app
# 或：npx mvframe-init-app
```

生成说明见项目根 **`MVFRAME-SCAFFOLD.md`**。环境变量 **`MVFRAME_SCAFFOLD_TARGET`** 可指定目标路径。

---

## Codex 规则（`AGENTS.md`）

Codex 不会自动读取 Cursor 的 `.cursor/rules/*.mdc`。宿主项目需要安装 MVFrame 的 `AGENTS.md` 区块，明确要求 Codex 在写业务代码前优先使用 MVFrame 全局组件、全局方法与全局样式工具类：

```bash
cd /path/to/your-app
yarn exec mvframe-init-rules
# 或：npx mvframe-init-rules

# 仅初始化 Codex 规则
yarn exec mvframe-install-codex-rules
# 或：npx mvframe-install-codex-rules

# 克隆 / monorepo 中指向 mvframe 源码时
node /path/to/mvframe/scripts/init-rules.js
node /path/to/mvframe/scripts/init-rules.js /path/to/your-app

# 仅初始化 Codex 规则
node /path/to/mvframe/scripts/install-codex-agents.js
node /path/to/mvframe/scripts/install-codex-agents.js /path/to/your-app
```

脚手架命令已自动执行这一步。生成区块带有 `MVFRAME-CODEX-RULES` 标记，重复执行只更新这段内容，不覆盖你在 `AGENTS.md` 中的其它规则。

## Cursor 规则（`.cursor/rules`）

统一命令 `mvframe-init-rules` 会同时把 **mvframe 包内**与仓库一致的 **`*.mdc`** 补充到目标项目 **`/.cursor/rules/`**（`component-hierarchy`、`script-setup`、`style-system`、`views`、`router`、`global-components`、`data`、`util`）。
若宿主项目已存在同名规则文件，则保留宿主文件，仅补缺失项。

仅安装 Cursor 规则时：

```bash
cd /path/to/your-app
yarn exec mvframe-install-cursor-rules
# 或：npx mvframe-install-cursor-rules

# 克隆 / monorepo 中指向 mvframe 源码时
node /path/to/mvframe/scripts/install-cursor-rules.js
node /path/to/mvframe/scripts/install-cursor-rules.js /path/to/your-app
```

## Cursor Skill（应用初始化）

本仓库自带 **`.cursor/skills/mvframe-app-init`**，消费方项目可复制到自身 `.cursor/skills` 以便 Cursor 识别 MVFrame 接入规范。

在**你的业务项目根目录**执行任一种方式：

```bash
# 克隆 / monorepo 中指向 mvframe 源码时
node /path/to/mvframe/scripts/install-cursor-skill.js
node /path/to/mvframe/scripts/install-cursor-skill.js /path/to/your-app

# 安装 npm 包后（发布包需包含 scripts 与 .cursor/skills，见 package.json files）
cd /path/to/your-app
yarn exec mvframe-install-cursor-skill
# 或：npx mvframe-install-cursor-skill
```

默认安装到**当前工作目录**下的 `.cursor/skills/mvframe-app-init`。也可设置环境变量 `MVFRAME_CURSOR_SKILL_OUT=/path/to/your-app`。

---

## 迭代记录

- 新增 `mvframe-notify`：只支持钉钉机器人的本地通知服务，钉钉 webhook / secret 留在 Node 环境变量。
- 新增 `mvframe/notify` 与全局 `$notify`：前端通过 `config.notify` 请求本地通知服务。
- 新增框架命令 `yarn d` / `mvframe-d`：启动 MVFrame 封装的 Vite dev，追加 `-n` 时同时启动钉钉通知服务。
- `mvframe-init-app` 初始化 `config.notify`、带测试钉钉机器人的 `.env.mvframe-notify` / `.env.mvframe-notify.example` 与 `.env.local.example`，不自动覆盖宿主 `d` 脚本。
- `VTable` 默认使用异步全局注册；`mvframe-init-app` 会在 `main.js` 写入 `components.async: ["VTable"]`。

---

## 协议

ISC（见 `package.json`）。
