mvframe-nuxt
v0.0.39
Published
Nuxt full-stack framework layer inspired by mvframe.
Readme
mvframe-nuxt
Nuxt full-stack framework package inspired by mvframe.
This package is intentionally separate from the existing mvframe Vue/Vite library. It keeps mvframe stable while providing a Nuxt module path for full-stack app work: auto-registered UI components, composables, runtime config, and Nitro server handlers.
Install
yarn add mvframe-nuxtexport default defineNuxtConfig({
modules: ["mvframe-nuxt"],
mvframe: {
config: {
name: "My App",
lang: "en_us",
mode: "dark",
theme: "glass",
baseURL: "https://api.example.com",
elementPlus: {
size: "default",
zIndex: 3000,
},
},
server: {
enabled: true,
prefix: "/api/mvframe",
proxy: {
enabled: false,
target: "",
headers: {},
},
},
},
});Host AI Rules
Initialize the package's Cursor and Codex rules in the host project:
yarn exec mvframe-nuxt-init-rules --forceThe command copies the current installed package README to .mvframe-nuxt/docs/README.md and adds a relative link to it in the generated AGENTS.md section. Codex can then read the same component usage that is maintained in this README. Run the command again after upgrading mvframe-nuxt so the host documentation and component list match the installed version.
DingTalk Completion Notifications
Initialize the host-only notification environment file from the project root:
yarn exec mvframe-nuxt-init-notifyThe command uses the package's test DingTalk robot by default. A host project can override it with --webhook and --secret, or with the DINGTALK_WEBHOOK and DINGTALK_SECRET process environment variables.
For automation where secrets should not appear in process arguments, pass a JSON object through standard input:
printf '%s' '{"webhook":"https://oapi.dingtalk.com/robot/send?access_token=...","secret":"SEC..."}' \
| yarn exec mvframe-nuxt-init-notify --stdinThe command updates DINGTALK_WEBHOOK and DINGTALK_SECRET in .env.mvframe-notify, preserves unrelated settings, applies file mode 0600, and never prints the configured values. Override the target with --env <path> when needed. Keep .env.mvframe-notify out of version control.
Declare the notification source through the module's private server interface:
export default defineNuxtConfig({
mvframe: {
server: {
notify: {
enabled: true,
envFile: ".env.mvframe-notify",
},
},
},
});mvframe.server.notify also accepts webhook, secret, atMobiles, atUserIds, atAll, and timeout. The module resolves explicit values first, then process environment variables, then envFile. The resolved object is stored only in private runtimeConfig.mvframe.server.notify; it is never returned by /api/mvframe/config or exposed through runtimeConfig.public.
Send one completion message without starting the local notification server:
yarn exec mvframe-nuxt-notify --once --message "Title
Summary"Runtime Surface
- Components:
MvFrame,MvPage,MvCard,MvTable,MvVTable,MvEChart,MvDatePicker,MvSingleDatePicker,MvSelect,MvSelectV2,MvDropdown,MvBtnGroup,MvMode,MvTheme,MvPrimaryColor,MvList,MvNote,MvLoading,MvIcon - Composables:
useMvConfig,useMvBootstrap,useMvRq,useMvGet,useMvPost,useMvNotify - Server routes:
/api/mvframe/health,/api/mvframe/config,/api/mvframe/proxy/** - Theme defaults:
mode: "dark"andtheme: "glass"; the client plugin applieshtml.dark.mv-theme-glass. MvBtnGrouphas no built-in group background; passbg="white"or another globalbg-*suffix when a filled surface is needed.MvTabsforwardsbgto its internalMvBtnGroup.
Request Language
useMvRq appends the current plugin language as a lang query field on every request. It reads the value through the same runtime language getter used by the rest of the plugin, and preserves an explicit query.lang when the caller provides one.
Remote Language Maps
mvframe.config.langApi can load host-provided language maps before $l is mounted. The endpoint receives the current language through queryKey and may return {data: {maps}}, {maps}, or a raw key-value map. Remote maps are merged after static langMaps, so database values can override local defaults.
export default defineNuxtConfig({
mvframe: {
config: {
lang: "en_us",
langApi: {
enabled: true,
endpoint: "/api/mvframe/language",
queryKey: "lang",
timeout: 5000,
},
},
},
});Bootstrap Before Route Render
Use useMvBootstrap().setHandler() for reusable post-login authorization or dictionary loading that must finish before normal pages render. The module installs a global route middleware; if no handler or endpoint is configured, it immediately lets the route continue.
Register the async handler in a host plugin. Do not put the function in runtimeConfig.public, because Nuxt serializes runtime config.
export default defineNuxtPlugin(() => {
const bootstrap = useMvBootstrap();
bootstrap.setHandler(async ({ $fetch }) => {
if (!$fetch) return;
const [auth, dictionaries] = await Promise.all([
$fetch("/api/auth/bootstrap"),
$fetch("/api/mvframe/dictionaries"),
]);
return [auth, dictionaries];
});
});Use mvframe.config.bootstrap only for route matching and error behavior:
export default defineNuxtConfig({
mvframe: {
config: {
bootstrap: {
excludePaths: ["/login"],
redirectOnError: "/login",
timeout: 10000,
},
},
},
});The handler may return nothing when it only needs to wait. If it returns an object, or an array of objects, config, langMaps, and maps are merged into mvframe runtime state. Values may be returned at the response root or under data:
export default defineEventHandler(() => ({
data: {
config: { accountId: "123" },
langMaps: { Search: "搜索" },
maps: { asa: { matchType: { obj: {} } } },
},
}));For a single simple request, bootstrap.endpoint is still supported and is used when no handler is registered. After a login action, call useMvBootstrap().refresh() to rerun the check with the new auth state.
Map Language Cache
useMap.lang(path) translates map labels lazily and caches the translated result per path. The cache is cleared when bootstrap patches langMaps / maps, or when runtime maps are applied again. Timezone maps (common.timezone, common.timezoneAll) are excluded from label translation by default.
Data Helpers
Data normalization helpers are exported from mvframe-nuxt/composition. Prefer the direct helper when the target type is known; use typedValue only for dynamic type dispatch.
import {stringValue, numberValue, typedValue} from "mvframe-nuxt/composition";
const keyword = stringValue(route.query.keyword, {trim: true});
const pageSize = numberValue(route.query.pageSize, {integer: true, min: 1, fallback: 20});
const value = typedValue(input, {type: "string", trim: true});MvInput
MvInput forwards Element Plus input props, including type="textarea". It supports fixed height through height, with number values and numeric strings treated as pixels and CSS size strings used as-is.
Field component labels are translated inside MvInput, MvInputNumber, MvTextarea, MvMention, MvSelect, and MvSelectV2. Pass the original language key or text to label; callers should not wrap it with $l() first.
The same rule applies to title and label props handled by other mvframe-nuxt components. MvForm also translates descendant el-form-item labels through its form-item middleware. Plain DOM copy, business options, buttons, tooltips, validation, and message text remain the caller's responsibility unless their component explicitly handles them.
<MvInput v-model="name" type="textarea" label="Campaign Name" material-label height="120" />MvInput type="textarea" blocks Enter from inserting a hard line break. IME composition is guarded, so pressing Enter to confirm a pinyin candidate does not trigger the normal enter flow.
Use nobreak when textarea content must not soft-wrap. In textarea mode, MvInput will set wrap="off" unless the caller already passed a wrap value.
<MvInput v-model="name" type="textarea" label="Campaign Name" material-label height="120" nobreak />MvTextarea keeps normal multiline behavior by default. Add nobreak only when it should block Enter line breaks and soft wrapping:
<MvTextarea v-model="keywords" height="120" nobreak resize="none" />MvLogin
MvLogin includes a persistent Light/Dark mode switch and built-in Login, Register, and Forget Password panels. The default Register and Forget Password forms emit register and forget; use v-model:panel when the host also needs the active panel.
<MvLogin
v-model="loginForm"
v-model:panel="panel"
:submit="login"
@register="register"
@forget="sendReset"
/>Use the register or forget slot to replace that panel's entire form. Slot submissions are owned by the host and do not invoke the corresponding MvLogin emit. Both slots expose form, errors, loading, showLogin(), and switchPanel(panel).
<MvLogin>
<template #register="{showLogin}">
<form @submit.prevent="registerDirectly">
<!-- Host-owned fields and submit behavior. -->
<button type="button" @click="showLogin">Back to Login</button>
</form>
</template>
</MvLogin>MvDatePicker
MvDatePicker wraps Element Plus el-date-picker as a daterange field with mvframe material-label styling and built-in translation for label, placeholders, and shortcuts.
<MvDatePicker
v-model="dateRange"
label="Date Range"
material-label
shortcuts
clearable
/>By default it uses the last 7 days excluding today through default-date="[-7, -1]". Pass :default-date="false" when a date filter should start empty. It emits update:startDate and update:endDate in addition to v-model.
MvSingleDatePicker uses the same field style for single-day values:
<MvSingleDatePicker v-model="date" label="Current Date" material-label />It emits a YYYY-MM-DD string. Pass :default-date="-1" to initialize yesterday, or keep the default false to start empty.
MvSelect / MvSelectV2
MvSelect and MvSelectV2 wrap Element Plus select controls with mvframe field styling. Use loading while options are being fetched; the field shows imicon im-loading ani-rotate on the left and blocks dropdown opening until loading is cleared.
MvSelect forwards its header slot to Element Plus. MvBtnGroup supports icon-only for icon filters; in that mode each translated option label is exposed through aria-label and the global tipbtn tooltip. Use tip-placement to set the tooltip direction.
In multiple mode, option changes remain pending until the built-in Confirm action is used. The clear button is immediate: it commits the cleared value and emits update:modelValue, change, and clear.
<MvSelect v-model="accountId" label="Account" material-label :options="accountOptions">
<template #header>
<MvBtnGroup
v-model="accountType"
:options="accountTypeOptions"
icon-only
tip-placement="bottom"
size="small"
/>
</template>
</MvSelect>MvDropdown
MvDropdown wraps Element Plus dropdown with a button trigger and option mapping. The trigger shows its label and the selected count in a primary tag, while selected menu items use is-active. It supports single or multiple values, optional menu filtering, and FIFO selection limits. When a multiple selection exceeds multiple-limit, the earliest selected value is removed automatically.
<MvDropdown
v-model="metrics"
:options="metricOptions"
label="Metrics"
multiple
filterable
:multiple-limit="5"
/>Element Plus Config
Element Plus is installed by the module. Put global Element Plus config under mvframe.config.elementPlus; it is passed to app.use(ElementPlus, config).
export default defineNuxtConfig({
mvframe: {
config: {
elementPlus: {
size: "large",
zIndex: 3000,
namespace: "el",
},
},
},
});idPrefix is also supported as an mvframe-nuxt SSR id provider option and is not passed to Element Plus.
Frame Menu
For a menu group with child routes, set default: true on the child that should open when the parent is clicked. The default route may also be hidden; it remains available as the parent's navigation target while staying out of the rendered submenu. If no enabled default route exists, MvFrame keeps the existing behavior of opening the first enabled visible child.
{
label: "Campaign",
children: [
{path: "/campaigns", default: true, hidden: true},
{path: "/createcampaigns", hidden: true},
],
}Frame Tabs
Frame tabs are recorded by the built-in router guard when mvframe.config.pinia.useTab is enabled. A page can opt out with route meta:
definePageMeta({
title: "No Account",
noTab: true,
});Equivalent supported flags are tab: false and saveTab: false. To remove already-saved tabs or exclude generated routes by rule, configure names or paths:
export default defineNuxtConfig({
mvframe: {
config: {
tab: {
excludeNames: ["Common_NoAccount"],
excludePaths: ["/common/no-account"],
},
},
},
});Frame Tools
MvFrame renders its built-in tools, including the Language switcher and Setting tool, on the right side of the tools area. The language switcher reuses MvLang, so changes follow the same localStorage / cookie / reload flow. Use #tools to insert host-specific entries before those built-in tools; the built-in tools remain rendered after the slot. By default, the Setting tool opens a built-in Appearance drawer with MvMode, MvTheme, and a persistent primary color control.
<MvFrame>
<template #tools>
<ModuleSwitcher />
<UserPopover />
</template>
</MvFrame>Use the setting slot when the host only needs to replace the contents while retaining the built-in drawer:
<MvFrame>
<template #setting="{close}">
<MySettingPanel @close="close" />
</template>
</MvFrame>tool-action receives the tool and a synchronous action context. Call preventDefault() when the host owns the entire Setting workflow, such as opening an application-level drawer. Listeners that only observe or report tool actions do not disable the built-in fallback.
<MvFrame @tool-action="onToolAction" />
<script setup lang="ts">
import type {
MvFrameToolActionContext,
MvFrameToolItem,
} from "mvframe-nuxt/types";
function onToolAction(tool: MvFrameToolItem, context: MvFrameToolActionContext) {
if (!tool.__mvframeSetting) return;
context.preventDefault();
openApplicationSettingDrawer();
}
</script>Set :show-lang-tool="false" to hide the built-in language switcher. Set :show-setting-tool="false" to hide the Setting tool.
MvMode, MvTheme, and MvPrimaryColor can also be used independently. They read the applied frame theme when model-value is omitted, emit update:modelValue and change, and persist changes by default.
<MvMode v-model="mode" />
<MvTheme v-model="theme" />
<MvPrimaryColor v-model="primary" />MvPrimaryColor includes a color picker, HEX input, preset colors, validation, and a restore-default action. Pass :predefine="brandColors" to replace its presets, or :persist="false" when the host owns persistence. Restoring the default emits null through v-model; with persistence enabled, it also removes the persisted primary override.
MvDialog
MvDialog locks page body scrolling by default while it is open. Long dialog content should scroll inside MvDialogArea / .MvcDialogBody; do not let the page body become the scroll container behind the modal.
<MvDialog v-model:current="current" :dialog="dialogMap" /><MvDialogArea height="560">
<LargeDialogContent />
</MvDialogArea>MvDialogArea wraps its default slot in an internal el-scrollbar. Use height or max-height to define the body scroll area. For large or frequently changing dialog bodies, pass native to use the browser scrollbar and avoid custom-thumb rendering lag. Only use noscroll for fixed, already-contained content. A dialog item can opt out of body locking with lockScroll: false, but the default should stay locked for modal workflows.
Host App Structure
mvframe-nuxt-init-app generates Nuxt-native host paths instead of the old Vue/Vite-style src/* aggregate tree:
components/AdminEntry.vuefor the application shell aroundMvFrameconfig/frame-routes.tsfor Frame menu/tab metadata; Nuxt routing still comes frompages/config/mvframe.tsfor build-timemvframe.configdefaults consumed bynuxt.config.tscomponents/<module>for page-level business componentscomposables/for host request/composition helpersserver/api/for host API routesmaps/index.tsonly when the host has real business maps to pass throughmvframe.config.maps
Do not scaffold placeholder src/api, src/pinia/chip, src/router, src/component, src/composition, or src/assets/style directories in Nuxt host apps.
MvVTable Tools
MvVTable supports the same built-in table tools as MvTable: refresh, column customization, and CSV download.
<MvVTable
table-name="campaigns"
:columns="columns"
:records="rows"
:tool="{ column: true, download: true }"
/>Use tool=true to enable all tools. Use tool="{ refresh: false, column: true, download: true }" to hide refresh. Column settings are stored by tableName. Columns with visible: false are hidden only by default and can still be enabled from Columns. Download opens a column picker, defaults to the current visible columns, and can export only selected rows when a selection column is active.
MvVTable Border and Radius
Use border to configure the component frame and its header/footer separators. It accepts true for the default border, false to remove those borders, or a complete CSS border value. Use border-radius with a number (treated as pixels by the shared size converter) or a CSS size. The internal VTable cell grid is not affected by the frame border prop.
<MvVTable
:columns="columns"
:records="rows"
border="2px solid var(--color-primary)"
:border-radius="12"
/>
<MvVTable
:columns="columns"
:records="rows"
:border="false"
:border-radius="0"
/>MvVTable Cell Tooltip
Set tooltip: true on a column to show the full cell text when the rendered text overflows. Pass a string to always show fixed tooltip content, or pass an object for placement, style, delay, and row-aware content. String content and formatter results are translated inside MvVTable through globalThis.$l; consumers should pass the original language key or text.
const columns = [
{ field: "campaignName", title: "Campaign Name", minWidth: 240, tooltip: true },
{ field: "url", title: "URL", tooltip: "Open Product Page" },
{
field: "status",
title: "Status",
tooltip: {
content: (record) => record.enabled ? "Enabled" : "Disabled",
placement: "top",
},
},
];MvVTable Custom Layout Interaction
Elements returned by a customRenderLayout column can expose page-owned actions through customLayoutAction. An action with type: "copy" uses the component clipboard fallback and feedback message; @custom-layout-click is also emitted for page-owned actions.
const meta = {
mvCustomLayout: true,
customLayoutKey: "app:123",
customLayoutGroup: "app:123",
customLayoutRole: "copy",
customLayoutAction: {type: "copy", text: "Example App (123)"},
};
const elements = [
{
type: "image",
src: icon,
x: 0,
y: 0,
width: 28,
height: 28,
cursor: "pointer",
pickable: true,
...meta,
},
];MvVTable Double-Click Copy
Use copy-on-dblclick to copy normal cell text on double click. Selection, index, operation, expand, and columns without a field are excluded by default.
<MvVTable
:columns="columns"
:records="rows"
copy-on-dblclick
@cell-copy="handleCellCopy"
/>Column-level copyable has the highest priority. Use it to enable a single column without a table default, disable a special column, or copy a formatted field.
const columns = [
{ field: "campaignName", title: "Campaign Name", copyable: true },
{ field: "budget", title: "Budget", copyable: { field: "budgetFormat" } },
{
field: "__actions",
title: "Actions",
type: "operation",
copyable: false,
},
];copy-on-dblclick also accepts an object: { included, excluded, message, successText, errorText }. Use message: false when the page wants to own feedback. @cell-copy-error is emitted when clipboard writing fails.
To avoid conflicts with copy and row/cell actions, MvVTable does not allow direct click or doubleclick cell-edit triggers. Cell editing should be opened through cellActions with trigger: "edit", startEditCell, or optional edit-cell-trigger="keydown".
MvVTable Mono Columns
Set mono: true on ID, code, token, or metric columns that should use a free system monospace font stack. Keep mixed-language text columns on the default UI font.
const columns = [
{ field: "spend", title: "Spend", align: "right", mono: true },
{ field: "campaignName", title: "Campaign Name", minWidth: 240 },
];MvVTable Metric Columns
MvVTable can consume metric-like column definitions directly. A column with value and unit / precision is treated as a metric column; formatter remains the formatted field key such as spendFormat, and missing formatted values are calculated at display/download time with $fa / $fu semantics.
import {useMap} from "mvframe-nuxt/maps";
const metricMap = useMap("asa.metricAll") as { arr: [] };
const columns = [
{field: "campaignName", title: "Campaign Name"},
...metricMap.arr,
];Use createMetricVTableColumn / createMetricVTableColumns from mvframe-nuxt/composition when code outside MvVTable needs to manually build VTable column objects. Use normalizeMetricFormatRecord(s) only when the caller needs to persist formatted fields onto rows before passing them elsewhere.
import {
createMetricVTableColumns,
normalizeMetricFormatRecord,
type MetricMapItem,
} from "mvframe-nuxt/composition";
import {useMap} from "mvframe-nuxt/maps";
const metricMap = useMap("asa.metricAll") as { arr: MetricMapItem[] };
const columns = createMetricVTableColumns(metricMap, {currencyField: "currency"});
const records = rows.map((row) =>
normalizeMetricFormatRecord(row, metricMap, {currencyField: "currency"}),
);Keep metric.formatter as the formatted field key. Do not change dictionary formatter to a function in host code unless a special row-level formatter is truly needed.
MvVTable Flag Columns
Use flag: true when a VTable text column needs a flag prefix. The flag renderer uses the flag-icons SVG assets exposed by the module at /assets/flags, while sorting, copying, searching, and downloading continue to use the text value or formatter value.
const columns = [
{
field: "country",
title: "Country",
flag: true,
formatter: "countryFormat",
},
];By default, flag: true reads the current column value as an ISO 3166-1 alpha-2 country code and renders /assets/flags/1x1/{code}.svg. Use flagField when the display text and country code come from different fields.
const columns = [
{
field: "countryName",
title: "Country",
flag: true,
flagField: "countryCode",
},
];Locale values such as en-US are not split into country codes; pass a real alpha-2 code field when a flag is required.
Country cells can contain multiple alpha-2 codes as an array, a JSON array string, or a comma-separated string. VTable displays up to flagLimit flags, shows a tooltip for each hovered flag, and renders im-more-circle when more countries are available. Clicking the more icon opens the built-in country popover.
const columns = [
{
field: "countries",
title: "Countries",
flag: true,
flagLimit: 2,
formatter: "countriesFormat",
},
];MvVTable Operation Actions
Use type: "operation" with actions when an operation column needs multiple clickable actions, icons, row-level permission visibility, or per-action hover colors. @cell-click and @cell-action-click receive payload.action.
const actionField = "__actions";
const columns = [
{
field: actionField,
title: "Actions",
width: 120,
fixed: "right",
type: "operation",
actions: [
{
key: "edit",
iconClass: "imicon im-edit",
tooltip: "Edit",
visible: "canEdit",
hoverColor: "var(--color-primary)",
},
{
key: "del",
iconClass: "imicon im-delete",
tooltip: "Delete",
visible: (record) => record.canDelete === true,
hoverColor: "var(--color-red)",
},
],
},
];
function handleCellClick(payload: Record<string, unknown>) {
if (payload.field !== actionField) return;
if (payload.action === "edit") {
// open edit
}
if (payload.action === "del") {
// confirm delete
}
}Icon-only actions can set tooltip to a string or tooltip options object. The
tooltip is rendered by MvVTable and localized internally through $l.
Normal cells can also define right-aligned cellActions. The table renders the cell text plus action icons, applies visible / disabled per row, and emits cell-action-click with action, record, field, actionRect, and anchor for popovers or dropdowns. A trigger: "edit" action automatically enables the current column editor. editType defaults to "input" and can be set to "select", "list", "textarea", "date", or a registered VTable editor name.
const columns = [
{
field: "name",
title: "Name",
cellActions: [
{
key: "editName",
iconClass: "imicon im-edit",
visible: "canEdit",
trigger: "edit",
editField: "name",
editType: "input",
loading: "_nameEditorLoading",
editConfig: {
placeholder: "Name",
},
},
{
key: "openProfile",
iconClass: "imicon im-more",
visible: "canManage",
trigger: "click",
},
],
},
{
field: "status",
title: "Status",
cellActions: [
{
key: "editStatus",
iconClass: "imicon im-edit",
trigger: "edit",
editType: "select",
editOptions: ["Draft", "Active", "Paused"],
},
],
},
];
function handleCellActionClick(payload: Record<string, unknown>) {
if (payload.action === "openProfile") {
currentRecord.value = payload.record;
profileDrawerVisible.value = true;
}
}For built-in dropdown actions, listen to @cell-action-command; the payload includes command, commandItem, record, field, and the original action metadata.
MvEChart
MvEChart wraps ECharts with the mvframe-nuxt resize guard, theme-aware tooltip defaults, and default options for normal cartesian charts.
<MvEChart
height="320"
:options="{
xAxis: { data: ['Mon', 'Tue', 'Wed'] },
yAxis: {},
series: [{ name: 'Spend', type: 'line', data: [12, 18, 9] }],
}"
/>Default options are merged as defaults first and caller options second. Plain objects merge recursively, arrays are replaced by caller arrays, and xAxis / yAxis arrays merge the default axis object into each caller axis item. Use :merge="false" when the page needs to pass pure ECharts options without mvframe defaults.
Useful props:
type: chart type hint for non-cartesian charts such aspie,radar,scatter,gauge, ormap.width/height: number and numeric strings are px; CSS strings are used as-is.renderer:canvasby default,svgwhen vector rendering is required.tool: enables the ECharts toolbox default.notMerge,lazyUpdate,replaceMerge,setOptionOpts: forwarded tosetOption.
Local Development
yarn install
yarn devThe built-in demo lives in demo/ and mounts the local module from src/module.ts.
yarn dev starts it at http://127.0.0.1:3600/.
The smaller playground from the initial scaffold remains available:
yarn dev:playgroundRoadmap
- Port more mvframe components into Nuxt runtime components.
- Add schema-backed API handlers for auth, menu, dictionary, and mock data.
- Keep request helpers aligned with Nuxt full-stack REST usage.
- Decide whether to depend on
mvframedirectly or keep this package as a standalone Nuxt rewrite.
