├── index.js ├── example ├── views │ ├── theming.vue │ ├── components │ │ ├── code.vue │ │ ├── menu.vue │ │ ├── anchor.vue │ │ ├── clock.vue │ │ ├── layout.vue │ │ ├── title.vue │ │ ├── backtop.vue │ │ ├── dropdown.vue │ │ ├── socialite.vue │ │ ├── index.vue │ │ ├── captcha.vue │ │ ├── password.vue │ │ ├── search.vue │ │ ├── modal.vue │ │ ├── notice.vue │ │ └── quote.vue │ ├── tools │ │ ├── cache.vue │ │ ├── global.vue │ │ ├── function.vue │ │ ├── request.vue │ │ └── index.vue │ ├── advanced │ │ ├── index.vue │ │ ├── menu.vue │ │ └── language.vue │ ├── items │ │ ├── index.vue │ │ ├── image.vue │ │ ├── list.vue │ │ └── text.vue │ ├── pages │ │ ├── index.vue │ │ ├── login.vue │ │ ├── register.vue │ │ └── forget.vue │ ├── socialite │ │ └── login.vue │ ├── about.vue │ ├── passport │ │ ├── register.vue │ │ ├── forget.vue │ │ └── login.vue │ ├── start.vue │ ├── home.vue │ └── dashboard.vue ├── vite-env.d.ts ├── app.vue ├── shims-vue.d.ts ├── assets │ └── images │ │ ├── news │ │ ├── 1.jpg │ │ ├── 2.webp │ │ └── 3.jpg │ │ ├── poet │ │ ├── dufu.jpg │ │ ├── baijuyi.jpg │ │ ├── libai.jpg │ │ └── lishangyin.jpeg │ │ ├── background.jpg │ │ ├── demo-dark.webp │ │ ├── demo-light.webp │ │ ├── demo-mobile.webp │ │ ├── demo-dark-theme.webp │ │ ├── logo-pure-no-rgb.png │ │ ├── logo-combine-no-rgb.png │ │ └── demo-dark-collapsed.webp └── main.ts ├── public └── favicon.ico ├── .vscode └── extensions.json ├── src ├── locales │ ├── zh-cn │ │ ├── code.ts │ │ ├── anchor.ts │ │ ├── pagination.ts │ │ ├── notice.ts │ │ ├── search.ts │ │ ├── password.ts │ │ ├── login.ts │ │ ├── forget.ts │ │ ├── captcha.ts │ │ ├── index.ts │ │ ├── register.ts │ │ ├── menu.ts │ │ └── global.ts │ ├── en-us │ │ ├── code.ts │ │ ├── pagination.ts │ │ ├── anchor.ts │ │ ├── notice.ts │ │ ├── search.ts │ │ ├── login.ts │ │ ├── password.ts │ │ ├── index.ts │ │ ├── captcha.ts │ │ ├── forget.ts │ │ ├── register.ts │ │ └── menu.ts │ └── index.ts ├── typings.d.ts ├── components │ ├── link │ │ ├── index.ts │ │ ├── style │ │ │ └── link.module.less │ │ ├── props.ts │ │ └── README.md │ ├── clock │ │ ├── index.ts │ │ ├── props.ts │ │ ├── README.md │ │ └── __tests__ │ │ │ └── clock.test.ts │ ├── image │ │ ├── index.ts │ │ ├── README.md │ │ ├── props.ts │ │ ├── Image.tsx │ │ └── __tests__ │ │ │ └── image.test.ts │ ├── login │ │ ├── index.ts │ │ └── props.ts │ ├── quote │ │ ├── index.ts │ │ ├── props.ts │ │ ├── Quote.tsx │ │ ├── README.md │ │ └── __tests__ │ │ │ └── quote.test.ts │ ├── title │ │ ├── index.ts │ │ ├── props.ts │ │ ├── Title.tsx │ │ ├── README.md │ │ ├── style │ │ │ └── title.module.less │ │ └── __tests__ │ │ │ └── title.test.ts │ ├── backtop │ │ ├── index.ts │ │ ├── style │ │ │ └── backtop.module.less │ │ ├── props.ts │ │ └── README.md │ ├── button │ │ ├── index.ts │ │ ├── README.md │ │ └── props.ts │ ├── captcha │ │ └── index.ts │ ├── forget │ │ └── index.ts │ ├── palette │ │ ├── index.ts │ │ ├── props.ts │ │ └── README.md │ ├── code │ │ ├── index.ts │ │ ├── README.md │ │ ├── props.ts │ │ └── style │ │ │ └── demo.module.less │ ├── modal │ │ ├── index.ts │ │ └── Teleport.tsx │ ├── password │ │ ├── index.ts │ │ └── props.ts │ ├── register │ │ └── index.ts │ ├── apps │ │ ├── menu │ │ │ └── index.ts │ │ └── language │ │ │ └── index.ts │ ├── items │ │ ├── list │ │ │ └── index.ts │ │ ├── text │ │ │ ├── index.ts │ │ │ ├── style │ │ │ │ ├── marker.module.less │ │ │ │ └── text.module.less │ │ │ └── __tests__ │ │ │ │ └── text.test.ts │ │ ├── detail │ │ │ ├── index.ts │ │ │ ├── props.ts │ │ │ └── __tests__ │ │ │ │ └── detail.test.ts │ │ └── image │ │ │ └── index.ts │ ├── anchor │ │ ├── index.ts │ │ ├── style │ │ │ ├── link.module.less │ │ │ └── anchor.module.less │ │ ├── Link.tsx │ │ ├── __tests__ │ │ │ └── link.test.ts │ │ └── props.ts │ ├── breadcrumb │ │ ├── index.ts │ │ ├── props.ts │ │ ├── README.md │ │ ├── style │ │ │ └── breadcrumb.module.less │ │ └── Breadcrumb.tsx │ ├── search │ │ ├── index.ts │ │ └── Key.tsx │ ├── dropdown │ │ ├── index.ts │ │ ├── style │ │ │ ├── dropdown.module.less │ │ │ └── item.module.less │ │ ├── props.ts │ │ ├── Item.tsx │ │ └── README.md │ ├── notice │ │ ├── index.ts │ │ └── Tab.tsx │ ├── historical │ │ ├── index.ts │ │ ├── props.ts │ │ └── README.md │ ├── menu │ │ ├── index.ts │ │ ├── style │ │ │ ├── drawer.module.less │ │ │ ├── item.module.less │ │ │ ├── submenu.module.less │ │ │ └── title.module.less │ │ ├── Item.tsx │ │ ├── Submenu.tsx │ │ └── Drawer.tsx │ ├── socialite │ │ ├── index.ts │ │ ├── props.ts │ │ ├── style │ │ │ ├── callback.module.less │ │ │ └── socialite.module.less │ │ └── README.md │ ├── layout │ │ ├── index.ts │ │ ├── style │ │ │ ├── sider.module.less │ │ │ ├── footer.module.less │ │ │ ├── layout.module.less │ │ │ ├── content.module.less │ │ │ └── header.module.less │ │ ├── Footer.tsx │ │ └── Sider.tsx │ ├── theme │ │ ├── style │ │ │ └── theme.module.less │ │ ├── index.ts │ │ ├── props.ts │ │ ├── Provider.tsx │ │ └── README.md │ └── _utils │ │ └── theme.ts ├── hooks │ ├── useGlobal.ts │ ├── useTools.ts │ ├── useCookie.ts │ ├── useRequest.ts │ ├── useStorage.ts │ ├── useApi.ts │ ├── index.ts │ ├── useHooks.ts │ └── useWindowResize.ts ├── stores │ ├── breadcrumbs.ts │ ├── index.ts │ ├── theme.ts │ ├── search.ts │ ├── layout.ts │ ├── historical.ts │ └── menu.ts ├── utils │ ├── mixins.ts │ ├── install.ts │ ├── basic.ts │ ├── cookie.ts │ └── storage.ts └── directives │ ├── prism.ts │ └── limit.ts ├── .eslintignore ├── .browserslistrc ├── tsconfig.node.json ├── .prettierrc ├── .gitignore ├── index.html ├── tsconfig.umd.json ├── tsconfig.json ├── vitest.setup.ts ├── ESBUILD_QUICKSTART.md └── .eslintrc /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src') 2 | -------------------------------------------------------------------------------- /example/views/theming.vue: -------------------------------------------------------------------------------- 1 | Theming -------------------------------------------------------------------------------- /example/views/components/code.vue: -------------------------------------------------------------------------------- 1 | Code -------------------------------------------------------------------------------- /example/views/components/menu.vue: -------------------------------------------------------------------------------- 1 | Menu -------------------------------------------------------------------------------- /example/views/tools/cache.vue: -------------------------------------------------------------------------------- 1 | Cache -------------------------------------------------------------------------------- /example/views/tools/global.vue: -------------------------------------------------------------------------------- 1 | Global -------------------------------------------------------------------------------- /example/views/components/anchor.vue: -------------------------------------------------------------------------------- 1 | Anchor -------------------------------------------------------------------------------- /example/views/components/clock.vue: -------------------------------------------------------------------------------- 1 | Clock -------------------------------------------------------------------------------- /example/views/components/layout.vue: -------------------------------------------------------------------------------- 1 | Layout -------------------------------------------------------------------------------- /example/views/components/title.vue: -------------------------------------------------------------------------------- 1 | Title -------------------------------------------------------------------------------- /example/views/tools/function.vue: -------------------------------------------------------------------------------- 1 | Function -------------------------------------------------------------------------------- /example/views/tools/request.vue: -------------------------------------------------------------------------------- 1 | Request -------------------------------------------------------------------------------- /example/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /example/app.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /example/views/components/backtop.vue: -------------------------------------------------------------------------------- 1 | Backtop -------------------------------------------------------------------------------- /example/views/components/dropdown.vue: -------------------------------------------------------------------------------- 1 | Dropdown -------------------------------------------------------------------------------- /example/views/components/socialite.vue: -------------------------------------------------------------------------------- 1 | Socialite -------------------------------------------------------------------------------- /example/views/components/index.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/views/advanced/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /example/views/items/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /example/views/pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /example/views/tools/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /example/views/socialite/login.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /example/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.png' 2 | declare module '*.jpg' 3 | declare module '*.svg' -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /src/locales/zh-cn/code.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | show: `显示代码`, 3 | copy: `复制代码`, 4 | copied: `复制成功` 5 | } 6 | -------------------------------------------------------------------------------- /src/locales/en-us/code.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | show: `Show Code`, 3 | copy: `Copy Code`, 4 | copied: `Copied` 5 | } 6 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | es/ 4 | build/ 5 | node_modules/ 6 | **/styles/ 7 | *.html 8 | example/ 9 | .history 10 | .vscode -------------------------------------------------------------------------------- /example/assets/images/news/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/example/assets/images/news/1.jpg -------------------------------------------------------------------------------- /example/assets/images/news/2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/example/assets/images/news/2.webp -------------------------------------------------------------------------------- /example/assets/images/news/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/example/assets/images/news/3.jpg -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.less' { 2 | const classes: { [key: string]: any } 3 | export default classes 4 | } 5 | -------------------------------------------------------------------------------- /example/assets/images/poet/dufu.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/example/assets/images/poet/dufu.jpg -------------------------------------------------------------------------------- /example/assets/images/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/example/assets/images/background.jpg -------------------------------------------------------------------------------- /example/assets/images/demo-dark.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/example/assets/images/demo-dark.webp -------------------------------------------------------------------------------- /example/assets/images/demo-light.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/example/assets/images/demo-light.webp -------------------------------------------------------------------------------- /example/assets/images/demo-mobile.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/example/assets/images/demo-mobile.webp -------------------------------------------------------------------------------- /example/assets/images/poet/baijuyi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/example/assets/images/poet/baijuyi.jpg -------------------------------------------------------------------------------- /example/assets/images/poet/libai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/example/assets/images/poet/libai.jpg -------------------------------------------------------------------------------- /src/components/link/index.ts: -------------------------------------------------------------------------------- 1 | import Link from './Link' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(Link) 5 | -------------------------------------------------------------------------------- /example/views/about.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/components/clock/index.ts: -------------------------------------------------------------------------------- 1 | import Clock from './Clock' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(Clock) 5 | -------------------------------------------------------------------------------- /src/components/image/index.ts: -------------------------------------------------------------------------------- 1 | import Image from './Image' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(Image) 5 | -------------------------------------------------------------------------------- /src/components/login/index.ts: -------------------------------------------------------------------------------- 1 | import Login from './Login' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(Login) 5 | -------------------------------------------------------------------------------- /src/components/quote/index.ts: -------------------------------------------------------------------------------- 1 | import Quote from './Quote' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(Quote) 5 | -------------------------------------------------------------------------------- /src/components/title/index.ts: -------------------------------------------------------------------------------- 1 | import Title from './Title' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(Title) 5 | -------------------------------------------------------------------------------- /example/assets/images/demo-dark-theme.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/example/assets/images/demo-dark-theme.webp -------------------------------------------------------------------------------- /example/assets/images/logo-pure-no-rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/example/assets/images/logo-pure-no-rgb.png -------------------------------------------------------------------------------- /example/assets/images/poet/lishangyin.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/example/assets/images/poet/lishangyin.jpeg -------------------------------------------------------------------------------- /src/components/backtop/index.ts: -------------------------------------------------------------------------------- 1 | import BackTop from './Backtop' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(BackTop) 5 | -------------------------------------------------------------------------------- /src/components/button/index.ts: -------------------------------------------------------------------------------- 1 | import Button from './Button' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(Button) 5 | -------------------------------------------------------------------------------- /src/components/captcha/index.ts: -------------------------------------------------------------------------------- 1 | import Captcha from './Captcha' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(Captcha) 5 | -------------------------------------------------------------------------------- /src/components/forget/index.ts: -------------------------------------------------------------------------------- 1 | import Forget from './Forget' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(Forget) 5 | -------------------------------------------------------------------------------- /src/components/palette/index.ts: -------------------------------------------------------------------------------- 1 | import Palette from './Palette' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(Palette) 5 | -------------------------------------------------------------------------------- /example/assets/images/logo-combine-no-rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/example/assets/images/logo-combine-no-rgb.png -------------------------------------------------------------------------------- /src/components/code/index.ts: -------------------------------------------------------------------------------- 1 | import { installs } from '../../utils/install' 2 | import Code from './Code' 3 | 4 | export default installs(Code, [Code.Demo]) 5 | -------------------------------------------------------------------------------- /src/components/modal/index.ts: -------------------------------------------------------------------------------- 1 | import Modal from './Modal' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(Modal, '$modal') 5 | -------------------------------------------------------------------------------- /src/components/password/index.ts: -------------------------------------------------------------------------------- 1 | import Password from './Password' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(Password) 5 | -------------------------------------------------------------------------------- /src/components/register/index.ts: -------------------------------------------------------------------------------- 1 | import Register from './Register' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(Register) 5 | -------------------------------------------------------------------------------- /example/assets/images/demo-dark-collapsed.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lirongtong/miitvip-vue-admin-manager/HEAD/example/assets/images/demo-dark-collapsed.webp -------------------------------------------------------------------------------- /src/components/apps/menu/index.ts: -------------------------------------------------------------------------------- 1 | import MiAppsMenu from './Menu' 2 | import { install } from '../../../utils/install' 3 | 4 | export default install(MiAppsMenu) 5 | -------------------------------------------------------------------------------- /src/components/items/list/index.ts: -------------------------------------------------------------------------------- 1 | import ItemsList from './List' 2 | import { install } from '../../../utils/install' 3 | 4 | export default install(ItemsList) 5 | -------------------------------------------------------------------------------- /src/components/items/text/index.ts: -------------------------------------------------------------------------------- 1 | import ItemsText from './Text' 2 | import { install } from '../../../utils/install' 3 | 4 | export default install(ItemsText) 5 | -------------------------------------------------------------------------------- /src/hooks/useGlobal.ts: -------------------------------------------------------------------------------- 1 | import { $g } from '../utils/global' 2 | 3 | export function useGlobal() { 4 | return { $g } 5 | } 6 | 7 | export default useGlobal 8 | -------------------------------------------------------------------------------- /src/hooks/useTools.ts: -------------------------------------------------------------------------------- 1 | import { $tools } from '../utils/tools' 2 | 3 | export function useTools() { 4 | return { $tools } 5 | } 6 | 7 | export default useTools 8 | -------------------------------------------------------------------------------- /src/components/anchor/index.ts: -------------------------------------------------------------------------------- 1 | import { installs } from '../../utils/install' 2 | import Anchor from './Anchor' 3 | 4 | export default installs(Anchor, [Anchor.Link]) 5 | -------------------------------------------------------------------------------- /src/components/breadcrumb/index.ts: -------------------------------------------------------------------------------- 1 | import Breadcrumb from './Breadcrumb' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(Breadcrumb) 5 | -------------------------------------------------------------------------------- /src/components/items/detail/index.ts: -------------------------------------------------------------------------------- 1 | import ItemsDetail from './Detail' 2 | import { install } from '../../../utils/install' 3 | 4 | export default install(ItemsDetail) 5 | -------------------------------------------------------------------------------- /src/components/items/image/index.ts: -------------------------------------------------------------------------------- 1 | import ItemsImage from './Image' 2 | import { install } from '../../../utils/install' 3 | 4 | export default install(ItemsImage) 5 | -------------------------------------------------------------------------------- /src/components/search/index.ts: -------------------------------------------------------------------------------- 1 | import { installs } from '../../utils/install' 2 | import Search from './Search' 3 | 4 | export default installs(Search, [Search.Key]) 5 | -------------------------------------------------------------------------------- /src/components/apps/language/index.ts: -------------------------------------------------------------------------------- 1 | import AppsLanguage from './Language' 2 | import { install } from '../../../utils/install' 3 | 4 | export default install(AppsLanguage) 5 | -------------------------------------------------------------------------------- /src/hooks/useCookie.ts: -------------------------------------------------------------------------------- 1 | import { $cookie } from '../utils/cookie' 2 | 3 | export function useCookie() { 4 | return { $cookie } 5 | } 6 | 7 | export default useCookie 8 | -------------------------------------------------------------------------------- /src/components/dropdown/index.ts: -------------------------------------------------------------------------------- 1 | import { installs } from '../../utils/install' 2 | import Dropdown from './Dropdown' 3 | 4 | export default installs(Dropdown, [Dropdown.Item]) 5 | -------------------------------------------------------------------------------- /src/components/notice/index.ts: -------------------------------------------------------------------------------- 1 | import { installs } from '../../utils/install' 2 | import Notice from './Notice' 3 | 4 | export default installs(Notice, [Notice.Tab, Notice.Item]) 5 | -------------------------------------------------------------------------------- /src/hooks/useRequest.ts: -------------------------------------------------------------------------------- 1 | import { $request } from '../utils/request' 2 | 3 | export function useRequest() { 4 | return { $request } 5 | } 6 | 7 | export default useRequest 8 | -------------------------------------------------------------------------------- /src/hooks/useStorage.ts: -------------------------------------------------------------------------------- 1 | import { $storage } from '../utils/storage' 2 | 3 | export function useStorage() { 4 | return { $storage } 5 | } 6 | 7 | export default useStorage 8 | -------------------------------------------------------------------------------- /src/locales/zh-cn/anchor.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | text: `目录`, 3 | close: `关闭锚点链接`, 4 | sticky: { 5 | yes: `开启固定悬浮`, 6 | no: `取消固定悬浮` 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/locales/zh-cn/pagination.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | prefix: '第', 3 | prev: '上一页', 4 | next: '下一页', 5 | unit: '页', 6 | strip: '条', 7 | total: '共' 8 | } 9 | -------------------------------------------------------------------------------- /.browserslistrc: -------------------------------------------------------------------------------- 1 | >= 0.5% 2 | last 2 major versions 3 | not dead 4 | Chrome >= 61 5 | Firefox >= 60 6 | Firefox ESR 7 | iOS >= 12 8 | Safari >= 12 9 | Edge >= 16 10 | Opera >= 48 -------------------------------------------------------------------------------- /src/components/historical/index.ts: -------------------------------------------------------------------------------- 1 | import MiHistoricalRouting from './Historical' 2 | import { install } from '../../utils/install' 3 | 4 | export default install(MiHistoricalRouting) 5 | -------------------------------------------------------------------------------- /src/components/menu/index.ts: -------------------------------------------------------------------------------- 1 | import { installs } from '../../utils/install' 2 | import Menu from './Menu' 3 | 4 | export default installs(Menu, [Menu.SubMenu, Menu.Item, Menu.Item.Title]) 5 | -------------------------------------------------------------------------------- /src/components/socialite/index.ts: -------------------------------------------------------------------------------- 1 | import Socialite from './Socialite' 2 | import { installs } from '../../utils/install' 3 | 4 | export default installs(Socialite, [Socialite.Callback]) 5 | -------------------------------------------------------------------------------- /src/hooks/useApi.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue' 2 | import { api } from '../utils/api' 3 | 4 | export function useApi() { 5 | return reactive(api) 6 | } 7 | 8 | export default useApi 9 | -------------------------------------------------------------------------------- /example/views/components/captcha.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/locales/en-us/pagination.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | prefix: 'Page ', 3 | prev: 'Previous', 4 | next: 'Next', 5 | unit: 'Page', 6 | strip: 'Entries', 7 | total: 'Total' 8 | } 9 | -------------------------------------------------------------------------------- /src/locales/en-us/anchor.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | text: 'Directory', 3 | close: 'Close anchor links', 4 | sticky: { 5 | yes: 'Enable sticky positioning', 6 | no: 'Disable sticky positioning' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /example/views/passport/register.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /src/stores/breadcrumbs.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export const useBreadcrumbsStore = defineStore('breadcrumbs', { 4 | state: () => ({ 5 | breadcrumbs: [] as any[] 6 | }) 7 | }) 8 | 9 | export default useBreadcrumbsStore 10 | -------------------------------------------------------------------------------- /example/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import router from './router' 3 | import App from './app.vue' 4 | import MakeitAdminPro from '../src/index' 5 | 6 | const app = createApp(App) 7 | app.use(router) 8 | app.use(MakeitAdminPro) 9 | app.mount('#app') 10 | -------------------------------------------------------------------------------- /src/components/layout/index.ts: -------------------------------------------------------------------------------- 1 | import { installs } from '../../utils/install' 2 | import Layout from './Layout' 3 | 4 | export default installs(Layout, [ 5 | Layout.Header, 6 | Layout.Sider, 7 | Layout.Sider.Logo, 8 | Layout.Content, 9 | Layout.Footer 10 | ]) 11 | -------------------------------------------------------------------------------- /src/locales/zh-cn/notice.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: `消息中心`, 3 | wonderful: `美好的一天`, 4 | fine: `诸事皆宜`, 5 | empty: { 6 | bugs: `无缺陷`, 7 | metting: `无会议`, 8 | business: `无需求` 9 | }, 10 | view: `查看详情`, 11 | official: `官方` 12 | } 13 | -------------------------------------------------------------------------------- /example/views/pages/login.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/utils/mixins.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import Theme from '../components/theme' 3 | import Basic, { __tree_shaking_basic__ } from './basic' 4 | 5 | export default (app: App) => { 6 | if (!__tree_shaking_basic__) { 7 | app.use(Basic) 8 | app.use(Theme) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/views/pages/register.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/locales/zh-cn/search.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: `搜索`, 3 | searching: `搜索中 ···`, 4 | placeholder: `请输入待搜索的关键词`, 5 | components: `搜索组件`, 6 | failed: { 7 | message: `源数据获取失败,无法完成搜索`, 8 | code: `错误代码:`, 9 | reason: `错误原因:`, 10 | error: `接口发生了不可预知的错误` 11 | }, 12 | data: [] 13 | } 14 | -------------------------------------------------------------------------------- /example/views/start.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useStoreAuth } from './auth' 2 | export { default as useStoreLayout } from './layout' 3 | export { default as useStoreMenu } from './menu' 4 | export { default as useStoreTheme } from './theme' 5 | export { default as useStoreBreadcrumbs } from './breadcrumbs' 6 | export { default as useStoreSearch } from './search' 7 | -------------------------------------------------------------------------------- /src/locales/en-us/notice.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Message Center', 3 | wonderful: 'Have a nice day', 4 | fine: 'All systems go', 5 | empty: { 6 | bugs: 'No defects', 7 | metting: 'No meetings', 8 | business: 'No requirements' 9 | }, 10 | view: 'View Details', 11 | official: 'Official' 12 | } 13 | -------------------------------------------------------------------------------- /example/views/advanced/menu.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/link/style/link.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | > a { 5 | width: 100%; 6 | height: 100%; 7 | cursor: pointer; 8 | position: relative; 9 | .flex(flex-start, flex-start); 10 | } 11 | } 12 | 13 | .vertical { 14 | > a { 15 | .flex(flex-start, flex-start, column); 16 | } 17 | } -------------------------------------------------------------------------------- /src/stores/theme.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { $tools } from '../utils/tools' 3 | 4 | export const useThemeStore = defineStore('themes', { 5 | state: () => ({ properties: {} }) as Record, 6 | actions: { 7 | updateProperties(theme: string) { 8 | $tools.createThemeProperties(theme) 9 | } 10 | } 11 | }) 12 | 13 | export default useThemeStore 14 | -------------------------------------------------------------------------------- /src/locales/en-us/search.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | name: 'Search', 3 | searching: 'Searching...', 4 | placeholder: 'Enter keywords', 5 | components: 'Search Components', 6 | failed: { 7 | message: 'Failed to fetch source data, search unavailable', 8 | code: 'Error Code:', 9 | reason: 'Cause:', 10 | error: 'Unexpected interface error occurred' 11 | }, 12 | data: [] 13 | } 14 | -------------------------------------------------------------------------------- /src/locales/zh-cn/password.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | lv1: '弱不禁风', 3 | lv2: '平淡无奇', 4 | lv3: '出神入化', 5 | lv4: '登峰造极', 6 | tip: '需包含字母、数字及特殊字符两种或以上组合', 7 | placeholder: '请输入密码', 8 | confirm: '请再次输入密码', 9 | strong: '密码强度', 10 | size: '{min}-{max}个字符,区分大小写,前后无空格', 11 | format: '字母、数字、英文、下划线等其他特殊字符', 12 | different: '两次密码输入不一致', 13 | setting: '请设置密码', 14 | least: '密码长度至少为{min}个字符' 15 | } 16 | -------------------------------------------------------------------------------- /src/stores/search.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { SearchData } from '../utils/types' 3 | 4 | export const useSearchStore = defineStore('search', { 5 | state: () => ({ data: [] as Partial[] }) as Record, 6 | actions: { 7 | updateData(data?: Partial[]) { 8 | this.data = data || [] 9 | } 10 | } 11 | }) 12 | export default useSearchStore 13 | -------------------------------------------------------------------------------- /src/directives/prism.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import Prism from 'prismjs' 3 | 4 | export default { 5 | install(app: App) { 6 | app.directive('prism', { 7 | beforeMount(el: any) { 8 | Prism.highlightAllUnder(el) 9 | }, 10 | updated(el: any) { 11 | Prism.highlightAllUnder(el) 12 | } 13 | }) 14 | return app 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { default as useApi } from './useApi' 2 | export { default as useGlobal } from './useGlobal' 3 | export { default as useTools } from './useTools' 4 | export { default as useRequest } from './useRequest' 5 | export { default as useCookie } from './useCookie' 6 | export { default as useStorage } from './useStorage' 7 | export { default as useWindowResize } from './useWindowResize' 8 | export { default as useHooks } from './useHooks' 9 | -------------------------------------------------------------------------------- /src/components/quote/props.ts: -------------------------------------------------------------------------------- 1 | import { PropTypes } from '../../utils/types' 2 | 3 | /** 4 | * +====================+ 5 | * | Quote | 6 | * +====================+ 7 | * @param background 背景色 8 | * @param color 文案颜色 9 | */ 10 | export interface QuoteProperties { 11 | background: string 12 | color: string 13 | } 14 | export const QuoteProps = () => ({ 15 | background: PropTypes.string, 16 | color: PropTypes.string 17 | }) 18 | -------------------------------------------------------------------------------- /src/locales/zh-cn/login.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: `登录`, 3 | signup: `立即注册`, 4 | 'no-account': `没有账号?`, 5 | 'has-account': `已有帐号?`, 6 | unknown: `不可预知错误`, 7 | username: `请输入用户名 / 邮箱地址 / 手机号码`, 8 | password: `请输入登录密码`, 9 | verify: `请点击按钮进行验证码校验`, 10 | forget: `忘记密码`, 11 | remember: `保持登录`, 12 | register: `注册`, 13 | socialite: `快捷登录方式`, 14 | auth: `授权登录中,请稍后 ...`, 15 | 'auth-failed': `授权失败,请联系管理员!` 16 | } 17 | -------------------------------------------------------------------------------- /example/views/home.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/clock/props.ts: -------------------------------------------------------------------------------- 1 | import { object } from 'vue-types' 2 | import { DeviceSize, PropTypes } from '../../utils/types' 3 | 4 | /** 5 | * +====================+ 6 | * | Clock | 7 | * +====================+ 8 | * @param size 大小 9 | */ 10 | export interface ClockProperties { 11 | size: string | number | DeviceSize 12 | } 13 | export const ClockProps = () => ({ 14 | size: PropTypes.oneOfType([PropTypes.number, PropTypes.string, object()]).def(240) 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/items/text/style/marker.module.less: -------------------------------------------------------------------------------- 1 | @import '../../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | .flex(); 5 | } 6 | 7 | .marker { 8 | background-color: var(--mi-on-background); 9 | 10 | &-text { 11 | color: var(--mi-on-background); 12 | } 13 | } 14 | 15 | .circle { 16 | .properties(width, 4); 17 | .properties(height, 4); 18 | .circle(); 19 | } 20 | 21 | .square { 22 | .properties(width, 4); 23 | .properties(height, 4); 24 | } -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./tsconfig.schema.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "types": [ 6 | "vitest/globals", 7 | "@testing-library/jest-dom", 8 | "@types/node" 9 | ], 10 | "skipLibCheck": true, 11 | "module": "ESNext", 12 | "moduleResolution": "bundler", 13 | "allowSyntheticDefaultImports": true 14 | }, 15 | "include": ["vite.config.ts", "vitest.setup.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /src/components/notice/Tab.tsx: -------------------------------------------------------------------------------- 1 | import { SlotsType, defineComponent } from 'vue' 2 | import { NoticeTabProps } from './props' 3 | 4 | const MiNoticeTab = defineComponent({ 5 | name: 'MiNoticeTab', 6 | inheritAttrs: false, 7 | slots: Object as SlotsType<{ 8 | default: any 9 | name: any 10 | icon: any 11 | }>, 12 | props: NoticeTabProps(), 13 | setup(_props, { slots }) { 14 | return () => slots?.default() 15 | } 16 | }) 17 | 18 | export default MiNoticeTab 19 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false, 4 | "printWidth": 100, 5 | "proseWrap": "never", 6 | "tabWidth": 4, 7 | "endOfLine": "auto", 8 | "ignorePath": ".prettierrcignore", 9 | "trailingComma": "none", 10 | "bracketSameLine": true, 11 | "htmlWhitespaceSensitivity": "ignore", 12 | "overrides": [ 13 | { 14 | "files": ".prettierrc", 15 | "options": { 16 | "parser": "json" 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /src/hooks/useHooks.ts: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue' 2 | import { api } from '../utils/api' 3 | import { $g } from '../utils/global' 4 | import { $cookie } from '../utils/cookie' 5 | import { $request } from '../utils/request' 6 | import { $storage } from '../utils/storage' 7 | import { $tools } from '../utils/tools' 8 | import { useWindowResize } from './useWindowResize' 9 | 10 | export function useHooks() { 11 | return { api: reactive(api), $g, $cookie, $request, $storage, $tools, useWindowResize } 12 | } 13 | 14 | export default useHooks 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | es 12 | lib 13 | /typings 14 | /fonts 15 | dist 16 | dist-ssr 17 | *.local 18 | stats.html 19 | .env.production 20 | 21 | # Build caches 22 | node_modules/.rpt2_cache_* 23 | node_modules/.cache 24 | .tsbuildinfo 25 | 26 | # Editor directories and files 27 | .vscode/* 28 | !.vscode/extensions.json 29 | .idea 30 | .DS_Store 31 | *.suo 32 | *.ntvs* 33 | *.njsproj 34 | *.sln 35 | *.sw? 36 | coverage -------------------------------------------------------------------------------- /src/stores/layout.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { $g } from '../utils/global' 3 | import { $storage } from '../utils/storage' 4 | 5 | export const useLayoutStore = defineStore('layout', { 6 | state: () => ({ 7 | collapsed: ($storage.get($g.caches.storages?.collapsed) || false) as boolean 8 | }), 9 | actions: { 10 | updateCollapsed() { 11 | this.collapsed = !this.collapsed 12 | $storage.set($g.caches.storages?.collapsed, this.collapsed) 13 | } 14 | } 15 | }) 16 | 17 | export default useLayoutStore 18 | -------------------------------------------------------------------------------- /src/components/breadcrumb/props.ts: -------------------------------------------------------------------------------- 1 | import { tuple, animations } from './../_utils/props' 2 | import { PropTypes } from '../../utils/types' 3 | 4 | /** 5 | * +=====================+ 6 | * | 面包屑 | 7 | * +=====================+ 8 | * @param separator 分隔符 9 | * @param animation 动画效果 10 | */ 11 | export interface BreadcrumbProperties { 12 | separator?: string 13 | animation?: string 14 | } 15 | export const BreadcrumbProps = () => ({ 16 | separator: PropTypes.string.def('/'), 17 | animation: PropTypes.oneOf(tuple(...animations)).def('breadcrumb') 18 | }) 19 | -------------------------------------------------------------------------------- /src/locales/zh-cn/forget.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | username: `请输入邮箱地址`, 3 | sent: `验证码已发送至密保邮箱 {email}`, 4 | code: `请输入验证码`, 5 | resend: { 6 | normal: `重新发送`, 7 | downtime: `{sec}s后可重新发送` 8 | }, 9 | update: `更新密码`, 10 | success: '密码重置成功', 11 | illegal: '当前操作不被允许', 12 | login: `返回登录`, 13 | register: `前往注册`, 14 | ok: `我知道了`, 15 | expired: `页面已失效,请刷新后再试`, 16 | emailExpired: ` 10 分钟`, 17 | successText: `重置密码的验证码已经发送至 {email},有效时间为{expired}。倘若您尚未收到验证码邮件,有可能被归类为垃圾邮件,请至「 垃圾邮件 」或「 广告邮件 」中查看。` 18 | } 19 | -------------------------------------------------------------------------------- /src/components/palette/props.ts: -------------------------------------------------------------------------------- 1 | import { tuple, placement, actions } from './../_utils/props' 2 | import { PropTypes } from '../../utils/types' 3 | 4 | /** 5 | * +======================+ 6 | * | Palette | 7 | * +======================+ 8 | * @param trigger 触发方式 9 | * @param placement 弹出位置 10 | */ 11 | export interface PaletteProperties { 12 | trigger: string 13 | placement: string 14 | } 15 | export const PaletteProps = () => ({ 16 | trigger: PropTypes.oneOf(tuple(...actions)).def('click'), 17 | placement: PropTypes.oneOf(tuple(...placement)).def('bottom') 18 | }) 19 | -------------------------------------------------------------------------------- /src/stores/historical.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { $g } from '../utils/global' 3 | import { $storage } from '../utils/storage' 4 | 5 | export const useHistoricalStore = defineStore('historical', { 6 | state: () => ({ 7 | routes: ($storage.get($g.caches.storages?.routes) || {}) as Record 8 | }), 9 | actions: { 10 | setRoutes(routes?: Record) { 11 | this.routes = routes 12 | $storage.set($g.caches.storages?.routes, routes) 13 | } 14 | } 15 | }) 16 | 17 | export default useHistoricalStore 18 | -------------------------------------------------------------------------------- /src/locales/en-us/login.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Login', 3 | signup: 'Sign Up Now', 4 | 'no-account': 'No account?', 5 | 'has-account': 'Already have an account?', 6 | unknown: 'Unexpected Error', 7 | username: 'Please enter username/email/phone', 8 | password: 'Enter password', 9 | verify: 'Click to verify CAPTCHA', 10 | forget: 'Forgot Password', 11 | remember: 'Stay Signed In', 12 | register: 'Register', 13 | socialite: 'Quick Login Methods', 14 | auth: 'Authorizing... Please wait', 15 | 'auth-failed': 'Authorization failed. Contact administrator.' 16 | } 17 | -------------------------------------------------------------------------------- /src/locales/zh-cn/captcha.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | loading: `正在加载验证码 ···`, 3 | init: `正在初始化验证码 ···`, 4 | click: `点击按钮进行验证`, 5 | checking: `智能检测中 ···`, 6 | pass: `通过验证`, 7 | tip: `错误提示`, 8 | error: { 9 | try: `已连续错误达 {num} 次,请稍候再试`, 10 | init: `初始化接口有误,请稍候再试` 11 | }, 12 | move: `请移动滑块,完成验证`, 13 | drag: `拖动左边滑块完成上方拼图`, 14 | dragging: `拖动滑块将悬浮图像正确拼合`, 15 | close: `关闭验证`, 16 | refresh: `刷新验证`, 17 | feedback: `帮助反馈`, 18 | provide: `「 makeit.vip 」提供技术支持`, 19 | success: `{take}s速度完成图片拼合验证`, 20 | flatten: `拖动滑块拼合图片`, 21 | verify: `就能验证成功哦` 22 | } 23 | -------------------------------------------------------------------------------- /src/locales/en-us/password.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | lv1: 'Weak', 3 | lv2: 'Fair', 4 | lv3: 'Strong', 5 | lv4: 'Excellent', 6 | tip: 'Must include two or more character types: letters, numbers & symbols', 7 | placeholder: 'Password', 8 | confirm: 'Confirm Password', 9 | strong: 'Password Strength', 10 | size: '{min}-{max} characters • Case-sensitive • No leading/trailing spaces', 11 | format: 'Allowed: letters (a-z/A-Z), numbers, underscores (_) & special symbols', 12 | different: "Passwords don't match", 13 | setting: 'Set Password', 14 | least: 'Minimum {min} characters required' 15 | } 16 | -------------------------------------------------------------------------------- /src/components/dropdown/style/dropdown.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | z-index: 202411131625; 5 | } 6 | 7 | .avatar { 8 | .properties(width, 32); 9 | .properties(height, 32); 10 | overflow: hidden; 11 | .flex(); 12 | .circle(); 13 | border: .0625rem solid var(--mi-primary); 14 | .transition(); 15 | 16 | &:hover { 17 | transform: rotate(360deg); 18 | } 19 | 20 | :global(.ant-avatar) { 21 | width: 100%; 22 | height: 100%; 23 | transform: scale(1.1); 24 | .circle(); 25 | cursor: pointer; 26 | } 27 | } -------------------------------------------------------------------------------- /src/components/theme/style/theme.module.less: -------------------------------------------------------------------------------- 1 | @import './basic.module.less'; 2 | @import './animation.module.less'; 3 | 4 | :global { 5 | #nprogress { 6 | & .bar { 7 | .gradient-hint(); 8 | z-index: @mi-zindex; 9 | .radius(8); 10 | } 11 | 12 | & .peg { 13 | box-shadow: none; 14 | } 15 | 16 | & .spinner-icon { 17 | border-top-color: var(--mi-primary); 18 | border-left-color: var(--mi-primary); 19 | } 20 | } 21 | } 22 | 23 | :export { 24 | --primary: #63ACFF; 25 | --radius: 6; 26 | --theme: dark; 27 | } -------------------------------------------------------------------------------- /src/components/_utils/theme.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUnmounted } from 'vue' 2 | import { $tools } from '../../utils/tools' 3 | import { useThemeStore } from '../../stores/theme' 4 | 5 | export default function (moduleStyled: any, destroy = false) { 6 | onMounted(() => { 7 | const properties = useThemeStore()?.$state?.properties || {} 8 | $tools.applyThemeModuleProperties( 9 | moduleStyled, 10 | $tools.assignThemeModuleProperties(moduleStyled, properties) 11 | ) 12 | }) 13 | 14 | onUnmounted(() => { 15 | if (destroy) $tools.destroyThemeModuleProperties(moduleStyled) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /example/views/components/password.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/components/anchor/style/link.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | .flex(center, flex-start); 5 | .properties(padding-top); 6 | .properties(padding-bottom); 7 | cursor: pointer; 8 | .transition(); 9 | color: var(--mi-anchor-link-text-default); 10 | 11 | .link { 12 | .ellipsis(); 13 | .properties(max-width, 180); 14 | .properties(margin-left, 16); 15 | } 16 | 17 | &.active, 18 | &:hover { 19 | color: var(--mi-anchor-link-text-active); 20 | } 21 | } 22 | 23 | :export { 24 | --anchor-link-text-default: var(--mi-on-surface); 25 | --anchor-link-text-active: var(--mi-primary); 26 | } -------------------------------------------------------------------------------- /src/components/link/props.ts: -------------------------------------------------------------------------------- 1 | import { PropTypes } from '../../utils/types' 2 | 3 | /** 4 | * +===================+ 5 | * | Link | 6 | * +===================+ 7 | * @param type 可额外识别 `email` 8 | * @param path 链接地址 9 | * @param query 参数配置 10 | * @param target 链接弹窗类型 11 | * @param vertical 是否垂直 12 | */ 13 | export interface LinkProperties { 14 | type: string 15 | path: string 16 | query: object 17 | target: string 18 | vertical: boolean 19 | } 20 | export const LinkProps = () => ({ 21 | type: PropTypes.string, 22 | path: PropTypes.string, 23 | query: PropTypes.object.def({}), 24 | target: PropTypes.string, 25 | vertical: PropTypes.bool.def(false) 26 | }) 27 | -------------------------------------------------------------------------------- /src/components/menu/style/drawer.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | z-index: @mi-zindex + 2000; 5 | 6 | :global { 7 | .ant-layout { 8 | background: transparent; 9 | 10 | .ant-layout-sider { 11 | background: transparent; 12 | } 13 | } 14 | 15 | .ant-drawer-body { 16 | padding: 0; 17 | } 18 | 19 | .ant-menu { 20 | background: transparent; 21 | 22 | .ant-menu-submenu .ant-menu-sub.ant-menu-inline { 23 | background: transparent; 24 | } 25 | } 26 | } 27 | } 28 | 29 | .icon { 30 | .font-size(20); 31 | } -------------------------------------------------------------------------------- /src/locales/en-us/index.ts: -------------------------------------------------------------------------------- 1 | import global from './global' 2 | import notice from './notice' 3 | import search from './search' 4 | import pagination from './pagination' 5 | import captcha from './captcha' 6 | import password from './password' 7 | import login from './login' 8 | import register from './register' 9 | import anchor from './anchor' 10 | import forget from './forget' 11 | import code from './code' 12 | import language from './language' 13 | import menu from './menu' 14 | 15 | export default { 16 | global, 17 | notice, 18 | search, 19 | pagination, 20 | captcha, 21 | password, 22 | login, 23 | register, 24 | anchor, 25 | forget, 26 | code, 27 | language, 28 | menu 29 | } 30 | -------------------------------------------------------------------------------- /src/locales/zh-cn/index.ts: -------------------------------------------------------------------------------- 1 | import global from './global' 2 | import notice from './notice' 3 | import search from './search' 4 | import pagination from './pagination' 5 | import captcha from './captcha' 6 | import password from './password' 7 | import login from './login' 8 | import register from './register' 9 | import anchor from './anchor' 10 | import forget from './forget' 11 | import code from './code' 12 | import language from './language' 13 | import menu from './menu' 14 | 15 | export default { 16 | global, 17 | notice, 18 | search, 19 | pagination, 20 | captcha, 21 | password, 22 | login, 23 | register, 24 | anchor, 25 | forget, 26 | code, 27 | language, 28 | menu 29 | } 30 | -------------------------------------------------------------------------------- /src/directives/limit.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | 3 | export default { 4 | install(app: App) { 5 | app.directive('limit', { 6 | mounted(el: any, params: any) { 7 | if (el && (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)) { 8 | const reg = params?.value?.reg 9 | if (reg) { 10 | el.addEventListener('keyup', (evt: any) => { 11 | evt.preventDefault() 12 | el.value = evt.target.value.replace(new RegExp(reg, 'ig'), '') 13 | }) 14 | } 15 | } 16 | } 17 | }) 18 | return app 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/layout/style/sider.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | .transition(); 5 | .properties(width, @mi-sider-width); 6 | .background(); 7 | position: absolute; 8 | left: 0; 9 | top: 0; 10 | overflow: hidden; 11 | z-index: @mi-zindex + 100; 12 | .properties(margin-top, @mi-margin); 13 | .properties(margin-bottom, @mi-margin); 14 | 15 | &.collapsed { 16 | .properties(width, @mi-sider-width-mini); 17 | } 18 | 19 | &:global(>.ant-menu) { 20 | max-height: calc(100vh - @mi-sider-logo-container - @mi-margin); 21 | } 22 | 23 | :global { 24 | .ant-skeleton { 25 | .properties(padding, 16); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /example/views/pages/forget.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 27 | -------------------------------------------------------------------------------- /src/components/theme/index.ts: -------------------------------------------------------------------------------- 1 | import type { App, Plugin } from 'vue' 2 | import Theme from './Theme' 3 | import Provider from './Provider' 4 | 5 | // 避免与 utils/install -> utils/mixins -> components/theme 产生循环依赖: 6 | // Theme 作为 Mixins 的一部分被安装时,不应再依赖 installs() 去反向引用 utils/install。 7 | ;(Theme as any).Provider = Provider 8 | ;(Theme as any).install = (app: App) => { 9 | if (typeof app.component((Theme as any).name) === 'undefined') { 10 | app.component((Theme as any).name, Theme as any) 11 | } 12 | if (typeof app.component((Provider as any).name) === 'undefined') { 13 | app.component((Provider as any).name, Provider as any) 14 | } 15 | } 16 | 17 | export default Theme as typeof Theme & 18 | Plugin & { 19 | readonly Provider: typeof Provider 20 | } 21 | -------------------------------------------------------------------------------- /src/components/quote/Quote.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import { getPropSlot } from '../_utils/props' 3 | import { QuoteProps } from './props' 4 | import applyTheme from '../_utils/theme' 5 | import styled from './style/quote.module.less' 6 | 7 | const MiQuote = defineComponent({ 8 | name: 'MiQuote', 9 | inheritAttrs: false, 10 | props: QuoteProps(), 11 | setup(props, { slots }) { 12 | applyTheme(styled) 13 | return () => ( 14 | 17 | {getPropSlot(slots, props, 'default')} 18 | 19 | ) 20 | } 21 | }) 22 | 23 | export default MiQuote 24 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Makeit Admin Pro - Powered by makeit.vip 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/link/README.md: -------------------------------------------------------------------------------- 1 | # 链接 2 | 3 | > 「 链接 」 组件用于快速生成 `a` 标签链接,根据传入的 `path` 属性,区分是内部跳转(不携带协议的地址)或是外部跳转地址,同时可用于邮箱类型的快速生成(`type="email"`) 4 | 5 | ## 使用示例 6 | 7 | ### 默认 8 | 9 | ```html 10 | 首页 11 | ``` 12 | 13 | ### 参数 14 | 15 | ```html 16 | 首页 17 | ``` 18 | 19 | ### 邮箱 20 | 21 | ```html 22 | 23 | ``` 24 | 25 | ## API 26 | 27 | ### MiLink `` 28 | 29 | #### `MiLink` 属性 ( `Properties` ) 30 | 31 | | 参数 | 类型 | 默认值 | 说明 32 | | :---- | :---- | :---- | :---- 33 | | `type` | `string` | `''` | 类型 `['email']` 34 | | `path` | `string` | `''` | 链接地址 35 | | `query` | `object` | `{}` | 参数配置 36 | | `target` | `string` | `_blank` | 链接打开方式 37 | | `vertical` | `boolean` | `false` | 是否垂直排列 38 | -------------------------------------------------------------------------------- /src/components/title/props.ts: -------------------------------------------------------------------------------- 1 | import { PropTypes, type DeviceSize, Position } from '../../utils/types' 2 | import { object } from 'vue-types' 3 | 4 | /** 5 | * +====================+ 6 | * | Title | 7 | * +====================+ 8 | * @param title 标题 9 | * @param center 居中 10 | * @param size 大小 11 | * @param color 颜色 12 | * @param margin 间距 13 | */ 14 | export interface TitleProperties { 15 | title: string 16 | center: boolean 17 | size: string | number | DeviceSize 18 | color: string 19 | margin: Position 20 | } 21 | export const TitleProps = () => ({ 22 | title: PropTypes.string.isRequired, 23 | center: PropTypes.bool.def(false), 24 | size: PropTypes.oneOfType([PropTypes.number, PropTypes.string, object()]).def(24), 25 | color: PropTypes.string, 26 | margin: PropTypes.object.def({}) 27 | }) 28 | -------------------------------------------------------------------------------- /src/components/layout/style/footer.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | .flex(); 5 | .properties(padding, 24); 6 | .transition(); 7 | position: sticky; 8 | bottom: 0; 9 | 10 | &.collapsed { 11 | .content { 12 | .properties(max-width, 810); 13 | 14 | @media only screen and (max-width: @mi-lg) and (min-width: @mi-sm) { 15 | .properties(max-width, 550); 16 | } 17 | } 18 | } 19 | } 20 | 21 | .content { 22 | color: var(--mi-layout-footer-text); 23 | text-align: center; 24 | .letter-spacing(); 25 | 26 | @media only screen and (max-width: @mi-xl) { 27 | .properties(max-width, 550); 28 | } 29 | } 30 | 31 | :export { 32 | --layout-footer-background: var(--mi-background); 33 | --layout-footer-text: var(--mi-on-background) 34 | } -------------------------------------------------------------------------------- /src/components/historical/props.ts: -------------------------------------------------------------------------------- 1 | import { PropTypes } from '../../utils/types' 2 | 3 | /** 4 | * +======================+ 5 | * | Routing | 6 | * +======================+ 7 | * @param name 名称 8 | * @param title 标题 9 | * @param path 路由 10 | */ 11 | export interface Routing { 12 | name: string 13 | title: string 14 | path: string 15 | } 16 | 17 | /** 18 | * +==========================================+ 19 | * | HistoricalRoutingProperties | 20 | * +==========================================+ 21 | * @param animationName 动画名称 22 | * @param animationDuration 动画时长 23 | */ 24 | export interface HistoricalRoutingProperties { 25 | animationName: string 26 | animationDuration: number 27 | } 28 | 29 | export const HistoricalRoutingProps = () => ({ 30 | animationName: PropTypes.string.def('false'), 31 | animationDuration: PropTypes.number.def(400) 32 | }) 33 | -------------------------------------------------------------------------------- /example/views/components/search.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/views/dashboard.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/components/items/text/style/text.module.less: -------------------------------------------------------------------------------- 1 | @import '../../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | width: 100%; 5 | overflow: hidden; 6 | position: relative; 7 | background-position: center; 8 | background-repeat: no-repeat; 9 | background-size: cover; 10 | 11 | &-center { 12 | .flex(); 13 | } 14 | } 15 | 16 | .inner { 17 | position: relative; 18 | } 19 | 20 | .items { 21 | .transition(); 22 | } 23 | 24 | .item { 25 | &-object { 26 | position: relative; 27 | } 28 | 29 | &-title { 30 | .flex(flex-start, flex-start); 31 | color: var(--mi-on-background); 32 | 33 | &-center { 34 | .flex(center, flex-start); 35 | } 36 | } 37 | 38 | &-info { 39 | .flex(flex-start, flex-start); 40 | 41 | &-center { 42 | .flex(center, flex-start); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/hooks/useWindowResize.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, onUnmounted } from 'vue' 2 | import { $tools } from '../utils/tools' 3 | import { $g } from '../utils/global' 4 | 5 | export function useWindowResize() { 6 | const defaultWidth = typeof window !== 'undefined' ? window.innerWidth : 1920 7 | const defaultHeight = typeof window !== 'undefined' ? window.innerHeight : 945 8 | const width = ref(defaultWidth) 9 | const height = ref(defaultHeight) 10 | 11 | const handleWindowResize = () => { 12 | width.value = window.innerWidth 13 | height.value = window.innerHeight 14 | $g.winSize.width = width.value 15 | $g.winSize.height = height.value 16 | } 17 | 18 | onMounted(() => $tools.on(window, 'resize', handleWindowResize)) 19 | 20 | onUnmounted(() => $tools.off(window, 'resize', handleWindowResize)) 21 | 22 | return { width, height } 23 | } 24 | 25 | export default useWindowResize 26 | -------------------------------------------------------------------------------- /src/locales/en-us/captcha.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | loading: 'Loading CAPTCHA...', 3 | init: 'Initializing CAPTCHA...', 4 | click: 'Click the button to verify', 5 | checking: 'Detecting...', 6 | pass: 'Verification passed', 7 | tip: 'Error message', 8 | error: { 9 | try: 'Failed {num} times in a row. Please try again later.', 10 | init: 'Initialization error. Please try again later.' 11 | }, 12 | move: 'Move the slider to complete verification', 13 | drag: 'Drag the left slider to complete the puzzle above', 14 | dragging: 'Drag the slider to align the floating image', 15 | close: 'Close verification', 16 | refresh: 'Refresh verification', 17 | feedback: 'Feedback & Support', 18 | provide: 'Powered by 「 makeit.vip 」', 19 | success: 'Completed image puzzle verification in {take}s', 20 | flatten: 'Drag the slider to align the image', 21 | verify: 'Verification successful' 22 | } 23 | -------------------------------------------------------------------------------- /src/locales/en-us/forget.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | username: 'Please enter your email address', 3 | sent: 'A verification code has been sent to your secure email {email}', 4 | code: 'Please enter the verification code', 5 | resend: { 6 | normal: 'Resend', 7 | downtime: 'Resend in {sec}s' 8 | }, 9 | update: 'Update Password', 10 | success: 'Password reset successful', 11 | illegal: 'This operation is not allowed', 12 | login: 'Return to Login', 13 | register: 'Go to Registration', 14 | ok: 'I understand', 15 | expired: 'The page has expired, please refresh and try again', 16 | emailExpired: '10 minutes', 17 | successText: 18 | 'The password reset verification code has been sent to {email} and is valid for {expired}. If you have not received the verification code email, it may have been classified as spam. Please check your "Spam" or "Promotions" folder.' 19 | } 20 | -------------------------------------------------------------------------------- /src/components/layout/style/layout.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | display: flex; 5 | flex: auto; 6 | min-height: auto; 7 | flex-direction: column; 8 | position: relative; 9 | } 10 | 11 | .content { 12 | width: 100%; 13 | height: 100%; 14 | .transition(); 15 | .hide-scrollbar(); 16 | .properties(padding, @mi-margin); 17 | overflow: hidden; 18 | position: relative; 19 | } 20 | 21 | .inner { 22 | width: 100%; 23 | height: calc(100svh - 2rem); 24 | .radius(16); 25 | .hide-scrollbar(); 26 | background: var(--mi-layout-background); 27 | overflow: hidden; 28 | position: relative; 29 | } 30 | 31 | .hasSider { 32 | .properties(padding-left, @mi-sider-width); 33 | 34 | &.collapsed { 35 | .properties(padding-left, @mi-sider-width-mini); 36 | } 37 | } 38 | 39 | :export { 40 | --layout-background: rgba(var(--mi-rgb-surface-variant), .6); 41 | } -------------------------------------------------------------------------------- /src/components/backtop/style/backtop.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | position: fixed; 5 | .flex(); 6 | .gradient-hint(var(--mi-backtop-background-start), var(--mi-backtop-background-hint), var(--mi-backtop-background-stop)); 7 | .transition(); 8 | cursor: pointer; 9 | 10 | .inner { 11 | position: absolute; 12 | width: 100%; 13 | height: 100%; 14 | left: 0; 15 | right: 0; 16 | .flex(); 17 | } 18 | 19 | .icon { 20 | .font-size(22, bold); 21 | 22 | :global { 23 | .anticon { 24 | .flex(); 25 | color: var(--mi-backtop-icon); 26 | } 27 | } 28 | } 29 | } 30 | 31 | :export { 32 | --backtop-background-start: var(--mi-primary); 33 | --backtop-background-hint: var(--mi-secondary); 34 | --backtop-background-stop: var(--mi-tertiary); 35 | --backtop-icon: var(--mi-on-secondary); 36 | } -------------------------------------------------------------------------------- /src/components/image/README.md: -------------------------------------------------------------------------------- 1 | # 图片加载 2 | 3 | > 「 图片加载 」 重新封装 `img` 标签,新增 load 事件处理。 4 | 5 | ## 使用示例 6 | 7 | ### 基础 8 | 9 | ```html 10 | 11 | ``` 12 | 13 | ## 主题配置 14 | 15 | ### 配置示例 16 | 17 | > 请查看 「 [`主题配置`](../theme/README.md) 」组件 18 | 19 | ## API 20 | 21 | ### MiImage `` 22 | 23 | #### `MiImage` 属性 ( `Properties` ) 24 | 25 | | 参数 | 类型 | 默认值 | 说明 26 | | :---- | :---- | :---- | :---- 27 | | `src` | `string` | `''` | 图片地址 28 | | `alt` | `string` | `''` | 图片描述 29 | | `width` | `number \| string \|` [`DeviceSize`](../../utils/README.md#interface-devicesize) | `''` | 宽度 30 | | `height` | `number \| string \|` [`DeviceSize`](../../utils/README.md#interface-devicesize) | `''` | 高度 31 | | `radius` | `number \| string \|` [`DeviceSize`](../../utils/README.md#interface-devicesize) | `''` | 圆角 32 | 33 | #### `MiImage` 事件 ( `Events` ) 34 | 35 | | 方法 | 返回值 | 说明 36 | | :---- | :---- | :---- 37 | | `load` | element | 加载完成后返回当前元素 38 | -------------------------------------------------------------------------------- /src/components/image/props.ts: -------------------------------------------------------------------------------- 1 | import { object } from 'vue-types' 2 | import { PropTypes, type DeviceSize } from '../../utils/types' 3 | 4 | /** 5 | * +==============================+ 6 | * | ImageProperties | 7 | * +==============================+ 8 | * @param src 图片地址 9 | * @param alt 图片描述 10 | * @param width 宽度 11 | * @param height 高度 12 | * @param radius 圆角 13 | */ 14 | export interface ImageProperties { 15 | src: string 16 | alt?: string 17 | width?: string | number | DeviceSize 18 | height?: string | number | DeviceSize 19 | radius?: string | number | DeviceSize 20 | } 21 | 22 | export const ImageProps = () => ({ 23 | src: PropTypes.string.isRequired, 24 | alt: PropTypes.string, 25 | width: PropTypes.oneOfType([PropTypes.string, PropTypes.number, object()]), 26 | height: PropTypes.oneOfType([PropTypes.string, PropTypes.number, object()]), 27 | radius: PropTypes.oneOfType([PropTypes.string, PropTypes.number, object()]) 28 | }) 29 | -------------------------------------------------------------------------------- /src/components/menu/style/item.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | .transition(); 5 | background-color: var(--mi-menu-item-background-default); 6 | 7 | &.active { 8 | .gradient-hint(var(--mi-menu-item-background-active-start), var(--mi-menu-item-background-active-hint), var(--mi-menu-item-background-active-stop)); 9 | } 10 | 11 | &.collapsed { 12 | .flex(); 13 | } 14 | 15 | &:hover, 16 | &:visited, 17 | &:active, 18 | &:link { 19 | background-color: transparent !important; 20 | } 21 | } 22 | 23 | .link { 24 | .properties(margin-top); 25 | color: var(--mi-menu-item-text); 26 | 27 | > a { 28 | .flex(); 29 | } 30 | } 31 | 32 | :export { 33 | --menu-item-text: var(--mi-on-background); 34 | --menu-item-background-default: transparent; 35 | --menu-item-background-active-start: var(--mi-primary); 36 | --menu-item-background-active-hint: var(--mi-secondary); 37 | --menu-item-background-active-stop: var(--mi-tertiary); 38 | } -------------------------------------------------------------------------------- /src/components/socialite/props.ts: -------------------------------------------------------------------------------- 1 | import { PropTypes, type DropdownItem } from '../../utils/types' 2 | import { object } from 'vue-types' 3 | 4 | /** 5 | * +========================+ 6 | * | Socialite | 7 | * +========================+ 8 | * @param tip 显示文案 9 | * @param domain 域名 10 | * @param showMore 更多下拉显示方式 11 | * @param items 下拉数据 12 | */ 13 | export interface SocialiteProperties { 14 | tip: string 15 | domain: string 16 | showMore: boolean 17 | items: Partial[] 18 | } 19 | 20 | export const SocialiteProps = () => ({ 21 | tip: PropTypes.string, 22 | domain: PropTypes.string.def(undefined), 23 | showMore: PropTypes.bool.def(true), 24 | items: object[]>() 25 | }) 26 | 27 | /** 28 | * +========================+ 29 | * | Socialite | 30 | * +========================+ 31 | * @param link 授权地址 32 | */ 33 | export interface SocialiteCallbakProperties { 34 | link?: string 35 | } 36 | 37 | export const SocialiteCallbakProps = () => ({ 38 | link: PropTypes.string 39 | }) 40 | -------------------------------------------------------------------------------- /src/locales/zh-cn/register.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: `注册`, 3 | account: `请设置用户账号`, 4 | 'no-account': `没有账号?`, 5 | 'has-account': `已有帐号?`, 6 | placeholder: { 7 | username: `请输入用户名`, 8 | email: `请输入邮箱地址` 9 | }, 10 | email: { 11 | text: `请输入邮箱地址`, 12 | invalid: `请输入有效的邮箱地址` 13 | }, 14 | format: '仅允许字母+数字,4-16 个字符,且以字母开头', 15 | tips: { 16 | special: `「 特别提醒 」 登录用户名,一旦设置,则无法更改。`, 17 | structure: `- 由 字母、数字 或 下划线 组成。`, 18 | start: `- 只能以 字母开头,例如:makeit。`, 19 | length: `- 用户名长度为 4-16 个字符。` 20 | }, 21 | login: `前往登录`, 22 | validate: `前往验证`, 23 | socialite: `快捷登录方式`, 24 | unknown: `未知错误`, 25 | verify: `请点击按钮进行验证码校验`, 26 | success: `注册成功`, 27 | emailExpired: ` 30 分钟`, 28 | successText: `您的邮箱激活验证链接已经发送至 {email},有效时间为{expired}。倘若您尚未收到激活邮件,有可能被归类为垃圾邮件,请至「 垃圾邮件 」或「 广告邮件 」中查看。` 29 | } 30 | -------------------------------------------------------------------------------- /src/components/breadcrumb/README.md: -------------------------------------------------------------------------------- 1 | # 面包屑导航(MiBreadcrumb) 2 | 3 | > 「面包屑导航」组件会根据路由配置自动生成当前位置的路径导航,支持自定义分隔符和切换动画效果。 4 | 5 | ## 使用示例 6 | 7 | ### 默认 8 | 9 | ```html 10 | 11 | 12 | ``` 13 | 14 | ### 自定义分隔符 15 | 16 | ```html 17 | 18 | ``` 19 | 20 | ### 启用动画效果 21 | 22 | ```html 23 | 24 | 25 | ``` 26 | 27 | ## 主题配置 28 | 29 | ### 配置示例 30 | 31 | > 请查看 「 [`主题配置`](../theme/README.md) 」组件 32 | 33 | ### Tokens 34 | 35 | #### Breadcrumbs Tokens 36 | 37 | | Token | 默认值 38 | | :---- | :---- 39 | | `--mi-breadcrumb-text-default` | `--mi-on-surface-variant` 40 | | `--mi-breadcrumb-text-active` | `--mi-primary` 41 | | `--mi-breadcrumb-separator` | `--mi-on-surface-variant` 42 | 43 | ## API 44 | 45 | ### MiBreadcrumb `` 46 | 47 | #### `MiBreadcrumb` 属性 ( `Properties` ) 48 | 49 | | 参数 | 类型 | 默认值 | 说明 50 | | :---- | :---- | :---- | :---- 51 | | `separator` | `string` | `/` | 分隔符 52 | | `animation` | `string` | `breadcrumb` | 动画效果 53 | -------------------------------------------------------------------------------- /example/views/components/modal.vue: -------------------------------------------------------------------------------- 1 | 2 | 点击打开弹窗 3 | Success 4 | 5 | 自定义弹窗内容(Content) 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/palette/README.md: -------------------------------------------------------------------------------- 1 | # 调色板 2 | 3 | > 「 调色板 」 组件为「 主题定制 」功能而开发。 4 | 5 | ## 使用示例 6 | 7 | ### 默认 8 | 9 | ```html 10 | 11 | ``` 12 | 13 | ## 主题配置 14 | 15 | ### 配置示例 16 | 17 | > 请查看 「 [`主题配置`](../theme/README.md) 」组件 18 | 19 | ### Tokens 20 | 21 | #### Palette Tokens 22 | 23 | | Token | 默认值 24 | | :---- | :---- 25 | | `--mi-palette-text` | `--mi-on-surface-variant` 26 | | `--mi-palette-background` | `--mi-surface-variant` 27 | | `--mi-palette-border` | `--mi-surface-variant` 28 | | `--mi-palette-btn-border` | `rgba(--mi-rgb-on-surface-variant, 0.5);` 29 | | `--mi-palette-btn-text` | `--mi-on-surface-variant` 30 | | `--mi-palette-btn-save-color` | `--mi-on-primary` 31 | | `--mi-palette-btn-save-start` | `--mi-primary` 32 | | `--mi-palette-btn-save-hint` | `--mi-secondary` 33 | | `--mi-palette-btn-save-stop` | `--mi-tertiary` 34 | 35 | ## API 36 | 37 | ### MiPalette `` 38 | 39 | #### `MiPalette` 属性 ( `Properties` ) 40 | 41 | | 参数 | 类型 | 默认值 | 说明 42 | | :---- | :---- | :---- | :---- 43 | | `trigger` | `string` | `click` | 触发方式 44 | | `placement` | `string` | `bottom` | 弹出位置 45 | -------------------------------------------------------------------------------- /src/components/quote/README.md: -------------------------------------------------------------------------------- 1 | # 引用说明 2 | 3 | > 「 引用说明 」 组件将重要内容或引用内容,采用额外的显眼的样式进行说明,突出重要性。 4 | 5 | ## 使用示例 6 | 7 | ### 默认 8 | 9 | ```html 10 | 11 | 当前内容所处区域即为「 引用说明 」组件效果 12 | 13 | ``` 14 | 15 | ## 主题配置 16 | 17 | ### 配置示例 18 | 19 | > 请查看 「 [`主题配置`](../theme/README.md) 」组件 20 | 21 | ### Tokens 22 | 23 | #### Quote Tokens 24 | 25 | | Token | 默认值 26 | | :---- | :---- 27 | | `--mi-quote-text` | `--mi-on-secondary` 28 | | `--mi-quote-background-start` | `--mi-primary` 29 | | `--mi-quote-background-hint` | `--mi-secondary` 30 | | `--mi-quote-background-stop` | `--mi-tertiary` 31 | | `--mi-quote-btn-gradient-start` | `--mi-surface` 32 | | `--mi-quote-btn-gradient-stop` | `--mi-surface-variant` 33 | | `--mi-quote-btn-text` | `--mi-on-surface-variant` 34 | | `--mi-quote-btn-border` | `transparent` 35 | | `--mi-quote-btn-shadow` | `--mi-shadow` 36 | 37 | ## API 38 | 39 | ### MiQuote `` 40 | 41 | #### `MiQuote` 属性 ( `Properties` ) 42 | 43 | | 参数 | 类型 | 默认值 | 说明 44 | | :---- | :---- | :---- | :---- 45 | | `background` | `string` | `''` | 背景色 46 | | `color` | `string` | `''` | 文案颜色 47 | -------------------------------------------------------------------------------- /src/components/theme/props.ts: -------------------------------------------------------------------------------- 1 | import { object } from 'vue-types' 2 | import { ComponentTokens, ThemeTokens } from './tokens' 3 | 4 | /** 5 | * +====================+ 6 | * | Theme | 7 | * +====================+ 8 | * @param theme 主题配置 9 | * - `Record` 10 | * - 全局一次性载入配置 11 | * 12 | * e.g. 13 | * ``` 14 | * 15 | * 16 | * // ... 17 | * 18 | * 19 | * ``` 20 | * @see ThemeTokens 21 | */ 22 | export interface ThemeProperties { 23 | theme: Partial 24 | } 25 | export const ThemeProps = () => ({ 26 | theme: object>() 27 | }) 28 | 29 | /** 30 | * +============================+ 31 | * | Theme Provider | 32 | * +============================+ 33 | * @param tokens 独立组件 Token 配置 34 | */ 35 | export interface ThemeProviderProperties { 36 | tokens: Partial 37 | } 38 | 39 | export const ThemeProviderProps = () => ({ 40 | tokens: object>().def({}) 41 | }) 42 | -------------------------------------------------------------------------------- /example/views/passport/forget.vue: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | 39 | -------------------------------------------------------------------------------- /src/stores/menu.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import type { MenuItem, DropdownItem } from '../utils/types' 3 | 4 | /** 5 | * Menu Store States. 6 | * @param menus 所有选单的 key 值 7 | * @param dropdowns 下拉菜单 ( Layout Header ) 8 | * @param accordion 手风琴模式 9 | * @param openKeys 打开的子菜单 key 值数组 10 | * @param activeKeys 当前选中的菜单项 key 值数组 11 | * @param relationshipChain 选中菜单的关系链 12 | * @param drawer 抽屉式菜单的状态 13 | * @param loading 数据加载中 14 | */ 15 | export const useMenuStore = defineStore('menus', { 16 | state: () => ({ 17 | menus: [] as MenuItem[], 18 | dropdowns: [] as Partial[], 19 | accordion: true, 20 | openKeys: [] as (string | number)[], 21 | activeKeys: [] as (string | number)[], 22 | relationshipChain: [] as string[], 23 | drawer: false, 24 | loading: false 25 | }), 26 | actions: { 27 | updateMenus(menus: MenuItem[]) { 28 | this.menus = menus 29 | }, 30 | updateDropdownMenus(menus: Partial[]) { 31 | this.dropdowns = menus 32 | } 33 | } 34 | }) 35 | 36 | export default useMenuStore 37 | -------------------------------------------------------------------------------- /example/views/components/notice.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/dropdown/style/item.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | .flex(center, space-between); 5 | color: var(--mi-dropdown-item-text); 6 | .properties(padding-top, 2); 7 | .properties(padding-bottom, 2); 8 | 9 | .item { 10 | .flex(); 11 | } 12 | 13 | .title { 14 | .properties(margin-left); 15 | white-space: nowrap; 16 | } 17 | 18 | .tag { 19 | color: var(--mi-dropdown-item-tag-text); 20 | .gradient-hint(var(--mi-dropdown-item-tag-start), var(--mi-dropdown-item-tag-hint), var(--mi-dropdown-item-tag-stop)); 21 | .properties(margin-left, 24); 22 | .ellipsis(); 23 | .radius(); 24 | .properties(padding-left); 25 | .properties(padding-right); 26 | width: auto; 27 | } 28 | 29 | .icon { 30 | .properties(margin-left, 24); 31 | } 32 | } 33 | 34 | :export { 35 | --dropdown-item-text: var(--mi-on-surface); 36 | --dropdown-item-tag-text: var(--mi-surface); 37 | --dropdown-item-tag-start: var(--mi-primary); 38 | --dropdown-item-tag-hint: var(--mi-secondary); 39 | --dropdown-item-tag-stop: var(--mi-tertiary); 40 | } -------------------------------------------------------------------------------- /src/components/items/text/__tests__/text.test.ts: -------------------------------------------------------------------------------- 1 | import { mount, type VueWrapper } from '@vue/test-utils' 2 | import { afterEach, describe, expect, test } from 'vitest' 3 | import { h, nextTick } from 'vue' 4 | import MiItemsText from '../Text' 5 | 6 | describe('MiItemsText', () => { 7 | const wrappers: VueWrapper[] = [] 8 | 9 | afterEach(() => { 10 | wrappers.splice(0).forEach((w) => w.unmount()) 11 | document.body.innerHTML = '' 12 | }) 13 | 14 | test('string items 会渲染 marker 与文本,并支持 item slot', async () => { 15 | const wrapper = mount(MiItemsText, { 16 | props: { 17 | items: ['a', 'b'], 18 | marker: { type: 'number' }, 19 | indent: 10 20 | }, 21 | slots: { 22 | item: ({ index }: any) => h('div', { 'data-testid': 'slot', 'data-index': index }) 23 | }, 24 | attachTo: document.body 25 | }) 26 | wrappers.push(wrapper) 27 | 28 | await nextTick() 29 | expect(wrapper.text()).toContain('a') 30 | expect(wrapper.text()).toContain('b') 31 | expect(wrapper.findAll('[data-testid="slot"]').length).toBe(2) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/components/layout/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { computed, defineComponent } from 'vue' 2 | import { getPropSlot } from '../_utils/props' 3 | import { $g } from '../../utils/global' 4 | import { useWindowResize } from '../../hooks/useWindowResize' 5 | import { useLayoutStore } from '../../stores/layout' 6 | import applyTheme from '../_utils/theme' 7 | import styled from './style/footer.module.less' 8 | 9 | const MiLayoutFooter = defineComponent({ 10 | name: 'MiLayoutFooter', 11 | inheritAttrs: false, 12 | setup(props, { slots }) { 13 | const { width } = useWindowResize() 14 | const store = useLayoutStore() 15 | const collapsed = computed(() => store.collapsed) 16 | const copyright = computed(() => { 17 | return width.value < $g.breakpoints.md ? $g.copyright.mobile : $g.copyright.laptop 18 | }) 19 | 20 | applyTheme(styled) 21 | 22 | return () => ( 23 | 28 | ) 29 | } 30 | }) 31 | 32 | export default MiLayoutFooter 33 | -------------------------------------------------------------------------------- /src/components/anchor/Link.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import { AnchorLinkProps } from './props' 3 | import { StarOutlined, TagOutlined } from '@ant-design/icons-vue' 4 | import type { AnchorLinkItem } from '../../utils/types' 5 | import applyTheme from '../_utils/theme' 6 | import styled from './style/link.module.less' 7 | 8 | const MiAnchorLink = defineComponent({ 9 | name: 'MiAnchorLink', 10 | inheritAttrs: false, 11 | props: AnchorLinkProps(), 12 | emits: ['click'], 13 | setup(props, { emit }) { 14 | applyTheme(styled) 15 | 16 | const handleClick = (evt?: Event) => { 17 | emit( 18 | 'click', 19 | { 20 | id: props.id, 21 | title: props.title 22 | } as AnchorLinkItem, 23 | evt 24 | ) 25 | } 26 | 27 | return () => ( 28 | 31 | {props.active ? : } 32 | 33 | 34 | ) 35 | } 36 | }) 37 | 38 | export default MiAnchorLink 39 | -------------------------------------------------------------------------------- /src/components/dropdown/props.ts: -------------------------------------------------------------------------------- 1 | import { PropTypes, type DropdownItem } from '../../utils/types' 2 | import { tuple, placement, actions } from './../_utils/props' 3 | import { array, object } from 'vue-types' 4 | 5 | /** 6 | * +=======================+ 7 | * | Dropdown | 8 | * +=======================+ 9 | * @param title 显示名称 10 | * @param placement popup 弹出位置 11 | * @param trigger 触发方式 12 | * @param items 数据 13 | * @param overlay 首选的下拉菜单 14 | */ 15 | export interface DropdownProperties { 16 | title: any 17 | placement: string 18 | trigger: string 19 | items: Partial[] 20 | overlay: any 21 | } 22 | export const DropdownProps = () => ({ 23 | title: PropTypes.any, 24 | placement: PropTypes.oneOf(tuple(...placement)).def('bottom'), 25 | trigger: PropTypes.oneOf(tuple(...actions)).def('click'), 26 | items: array>().def([]), 27 | overlay: PropTypes.any 28 | }) 29 | 30 | /** 31 | * +===========================+ 32 | * | Dropdown Item | 33 | * +===========================+ 34 | * @param item 数据 35 | * 36 | * @see DropdownItem 37 | */ 38 | export interface DropdownItemProperties { 39 | item: Partial 40 | } 41 | export const DropdownItemProps = () => ({ 42 | item: object>().isRequired 43 | }) 44 | -------------------------------------------------------------------------------- /example/views/passport/login.vue: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 37 | -------------------------------------------------------------------------------- /src/components/socialite/style/callback.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | position: fixed; 5 | width: 100%; 6 | height: 100svh; 7 | overflow: hidden; 8 | .hide-scrollbar(); 9 | left: 0; 10 | top: 0; 11 | .flex(center, center, column); 12 | } 13 | 14 | .inner { 15 | .properties(height, 64); 16 | .flex(); 17 | } 18 | 19 | .lines { 20 | display: block; 21 | position: relative; 22 | width: 0.375rem; 23 | height: 0.625rem; 24 | animation: rectangle infinite 1s ease-in-out -0.2s; 25 | background-color: var(--mi-primary); 26 | 27 | &:before, 28 | &:after { 29 | position: absolute; 30 | width: 0.375rem; 31 | height: 0.625rem; 32 | content: ''; 33 | background-color: var(--mi-primary); 34 | } 35 | 36 | &:before { 37 | left: -0.875rem; 38 | animation: rectangle infinite 1s ease-in-out -0.4s; 39 | } 40 | 41 | &:after { 42 | right: -0.875rem; 43 | animation: rectangle infinite 1s ease-in-out; 44 | } 45 | } 46 | 47 | @keyframes rectangle { 48 | 0%, 49 | 80%, 50 | 100% { 51 | height: 1.25rem; 52 | box-shadow: 0 0 var(--mi-primary); 53 | } 54 | 55 | 40% { 56 | height: 30px; 57 | box-shadow: 0 -1.25rme var(--mi-primary); 58 | } 59 | } -------------------------------------------------------------------------------- /src/utils/install.ts: -------------------------------------------------------------------------------- 1 | import type { App, Plugin } from 'vue' 2 | import Mixins from './mixins' 3 | 4 | /** 5 | * 通用组件安装方法 6 | * @desc common installation function 7 | * @param component 组件 8 | * @param alias 别名 9 | * @returns 10 | */ 11 | export const install = (component: T, alias?: string) => { 12 | const comp = component as any 13 | comp.install = (app: App) => { 14 | Mixins(app) 15 | if (typeof app.component(comp.name) === 'undefined') { 16 | app.component(comp.name, component) 17 | } 18 | if (alias) app.config.globalProperties[alias] = comp 19 | } 20 | return comp as T & Plugin 21 | } 22 | 23 | export const installs = (component: T, subs?: any[], alias?: string) => { 24 | const comp = component as any 25 | comp.install = (app: App) => { 26 | Mixins(app) 27 | if (typeof app.component(comp.name) === 'undefined') { 28 | app.component(comp.name, component) 29 | if (subs && subs.length > 0) { 30 | subs.forEach((sub: any) => { 31 | if (typeof app.component(sub?.name) === 'undefined') { 32 | app.component(sub?.name, sub) 33 | } 34 | }) 35 | } 36 | } 37 | if (alias) app.config.globalProperties[alias] = comp 38 | } 39 | return comp as T & Plugin 40 | } 41 | -------------------------------------------------------------------------------- /src/locales/en-us/register.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'Register', 3 | account: 'Set up user account', 4 | 'no-account': 'No account?', 5 | 'has-account': 'Already have an account?', 6 | placeholder: { 7 | username: 'Username', 8 | email: 'Email address' 9 | }, 10 | email: { 11 | text: 'Email address', 12 | invalid: 'Please enter a valid email address' 13 | }, 14 | format: 'Allowed: letters + numbers, 4-16 characters, must start with a letter', 15 | tips: { 16 | special: '[Important Notice] Username cannot be modified once set', 17 | structure: 18 | '- Contains letters, numbers or underscores', 19 | start: '- Must start with a letter (e.g. makeit)', 20 | length: '- Length: 4-16 characters' 21 | }, 22 | login: 'Login now', 23 | validate: 'Verify now', 24 | socialite: 'Quick login methods', 25 | unknown: 'Unknown error', 26 | verify: 'Click to verify CAPTCHA', 27 | success: 'Registration successful', 28 | emailExpired: '30 minutes', 29 | successText: 30 | 'Activation link sent to {email} (valid for {expired}). Check spam/ad folders if not received.' 31 | } 32 | -------------------------------------------------------------------------------- /src/components/clock/README.md: -------------------------------------------------------------------------------- 1 | # 在线钟表 2 | 3 | > 「 在线钟表 」 组件纯属娱乐, 观赏用 4 | 5 | ## 使用示例 6 | 7 | ### 默认 8 | 9 | ```html 10 | 11 | ``` 12 | 13 | ### 宽度 14 | 15 | ```html 16 | 17 | 18 | ``` 19 | 20 | ## 主题配置 21 | 22 | ### 配置示例 23 | 24 | > 请查看 「 [`主题配置`](../theme/README.md) 」组件 25 | 26 | ### Tokens 27 | 28 | #### Clock Tokens 29 | 30 | | Token | 默认值 31 | | :---- | :---- 32 | | `--mi-clock-shadow` | `--mi-shadow` 33 | | `--mi-clock-background-color` | `--mi-surface` 34 | | `--mi-clock-background-gradient-start` | `--mi-surface` 35 | | `--mi-clock-background-gradient-stop` | `--mi-surface-variant` 36 | | `--mi-clock-hour-text` | `--mi-on-surface` 37 | | `--mi-clock-minute-text` | `--mi-on-surface` 38 | | `--mi-clock-minute-line` | `--mi-on-surface` 39 | | `--mi-clock-pointer-background` | `--mi-on-surface` 40 | | `--mi-clock-pointer-mid` | `--mi-primary` 41 | | `--mi-clock-pointer-top` | `--mi-shadow` 42 | | `--mi-clock-point-background` | `--mi-on-surface` 43 | | `--mi-clock-point-hour` | `--mi-on-surface` 44 | | `--mi-clock-point-minute` | `--mi-on-surface` 45 | | `--mi-clock-point-second` | `--mi-primary` 46 | 47 | ## API 48 | 49 | ### MiClock `` 50 | 51 | #### `MiClock` 属性 ( `Properties` ) 52 | 53 | | 参数 | 类型 | 默认值 | 说明 54 | | :---- | :---- | :---- | :---- 55 | | `size` | `string \| number \|` [`DeviceSize`](../../utils/README.md) | `240` | 大小 56 | -------------------------------------------------------------------------------- /src/components/quote/__tests__/quote.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable vue/one-component-per-file */ 2 | import { mount, type VueWrapper } from '@vue/test-utils' 3 | import { afterEach, describe, expect, test, vi } from 'vitest' 4 | 5 | import MiQuote from '../Quote' 6 | 7 | vi.mock('../../_utils/theme', () => ({ default: () => null })) 8 | 9 | describe('MiQuote', () => { 10 | const wrappers: VueWrapper[] = [] 11 | 12 | afterEach(() => { 13 | wrappers.splice(0).forEach((w) => w.unmount()) 14 | vi.restoreAllMocks() 15 | document.body.innerHTML = '' 16 | }) 17 | 18 | test('渲染默认插槽内容', () => { 19 | const wrapper = mount(MiQuote, { 20 | slots: { 21 | default: () => 'hello' 22 | } 23 | }) 24 | wrappers.push(wrapper) 25 | 26 | expect(wrapper.text()).toContain('hello') 27 | }) 28 | 29 | test('background / color 会体现在根节点 style 上', () => { 30 | const wrapper = mount(MiQuote, { 31 | props: { 32 | background: 'rgb(1, 2, 3)', 33 | color: '#fff' 34 | }, 35 | slots: { 36 | default: () => 'x' 37 | } 38 | }) 39 | wrappers.push(wrapper) 40 | 41 | const style = wrapper.attributes('style') 42 | expect(style).toContain('background: rgb(1, 2, 3)') 43 | expect(style).toContain('color: #fff') 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /src/components/historical/README.md: -------------------------------------------------------------------------------- 1 | # 历史路由 2 | 3 | > 「 历史路由 」 组件根据页面路由切换,自动生成历史路由选单,方便历史路由导航。 4 | 5 | ## 使用示例 6 | 7 | ### 默认 8 | 9 | ```html 10 | 11 | 12 | 13 | ``` 14 | 15 | ### 动画 16 | 17 | ```html 18 | 19 | 20 | 21 | ``` 22 | 23 | ## 主题配置 24 | 25 | ### 配置示例 26 | 27 | > 请查看 「 [`主题配置`](../theme/README.md) 」组件 28 | 29 | ### Tokens 30 | 31 | #### Historical Tokens 32 | 33 | | Token | 默认值 34 | | :---- | :---- 35 | | `--mi-historical-routing-bg` | `--mi-background` 36 | | `--mi-historical-routing-text` | `--mi-on-background` 37 | | `--mi-historical-routing-item-active-text` | `--mi-on-primary` 38 | | `--mi-historical-routing-item-bg-active-start` | `--mi-primary` 39 | | `--mi-historical-routing-item-bg-active-hint` | `--mi-secondary` 40 | | `--mi-historical-routing-item-bg-active-stop` | `--mi-tertiary` 41 | 42 | ## API 43 | 44 | ### MiHistoricalRouting `` 45 | 46 | #### `MiHistoricalRouting` 属性 ( `Properties` ) 47 | 48 | | 参数 | 类型 | 默认值 | 说明 49 | | :---- | :---- | :---- | :---- 50 | | `animationName` | `string` | `'false'` | 动画名称 51 | | `animationDuration` | `number` | `400` | 动画时长 52 | 53 | ### Interface `HistoricalRouting` 54 | 55 | | 参数 | 类型 | 默认值 | 说明 56 | | :---- | :---- | :---- | :---- 57 | | `name` | string | `''` | 名称 58 | | `title` | string | `''` | 标题 59 | | `path` | string | `''` | 路由 60 | -------------------------------------------------------------------------------- /src/components/breadcrumb/style/breadcrumb.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | .properties(padding, 16); 5 | .flex(center, flex-start); 6 | flex-wrap: nowrap; 7 | .transition(); 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | z-index: @mi-zindex + 1000; 12 | } 13 | 14 | .separator { 15 | .properties(margin-right); 16 | .properties(margin-left); 17 | color: var(--mi-breadcrumbs-separator); 18 | } 19 | 20 | .item { 21 | color: var(--mi-breadcrumbs-text-active); 22 | .flex(center, flex-start); 23 | .ellipsis(); 24 | width: auto; 25 | 26 | span { 27 | a { 28 | .flex(); 29 | 30 | :global(.anticon) { 31 | .properties(margin-right, 6); 32 | 33 | &-home { 34 | .properties(margin-top, -2); 35 | } 36 | } 37 | } 38 | } 39 | 40 | &:last-child { 41 | span { 42 | a { 43 | cursor: default; 44 | color: var(--mi-breadcrumbs-text-default); 45 | } 46 | 47 | &:last-child { 48 | display: none; 49 | } 50 | } 51 | } 52 | } 53 | 54 | :export { 55 | --breadcrumb-text-default: var(--mi-on-surface-variant); 56 | --breadcrumb-text-active: var(--mi-primary); 57 | --breadcrumb-separator: var(--mi-on-surface-variant); 58 | } -------------------------------------------------------------------------------- /example/views/advanced/language.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/components/socialite/README.md: -------------------------------------------------------------------------------- 1 | # 社会化登录 / 注册 2 | 3 | > 「 社会化登录 / 注册 」 组件作用于登录 / 注册表单内,快速定义三方的社会化登录 / 注册的相关内容及 `UI` 效果 4 | 5 | ## 使用示例 6 | 7 | ### 默认 8 | 9 | ```html 10 | 11 | ``` 12 | 13 | ## 主题配置 14 | 15 | ### 配置示例 16 | 17 | > 请查看 「 [`主题配置`](../theme/README.md) 」组件 18 | 19 | ### Tokens 20 | 21 | #### Socialite Tokens 22 | 23 | | Token | 默认值 24 | | :---- | :---- 25 | | `--mi-socialite-icon` | `--mi-on-background` 26 | | `--mi-socialite-title-text` | `--mi-on-background` 27 | | `--mi-socialite-mobile-line` | `--mi-primary` 28 | | `--mi-socialite-mobile-icon` | `--mi-primary` 29 | | `--mi-socialite-mobile-title-text` | `--mi-surface-variant` 30 | | `--mi-socialite-mobile-title-background-start` | `--mi-primary` 31 | | `--mi-socialite-mobile-title-background-hint` | `--mi-secondary` 32 | | `--mi-socialite-mobile-title-background-stop` | `--mi-tertiary` 33 | 34 | ## API 35 | 36 | ### MiSocialite `` 37 | 38 | #### `MiSocialite` 属性 ( `Properties` ) 39 | 40 | | 参数 | 类型 | 默认值 | 说明 41 | | :---- | :---- | :---- | :---- 42 | | `tip` | `string` | `''` | 显示文案 43 | | `showMore` | `boolean` | `true` | 更多下拉选单的显示方式 ( 移动端会自动切换 ) 44 | | `domain` | `string` | `''` | 社会化登录/注册跳转链接 45 | | `items` | [`DropdownItem`](../dropdown/README.md) | `[]` | 下拉选项 46 | 47 | ### MiSocialite `` 48 | 49 | #### `MiSocialiteCallback` 属性 ( `Properties` ) 50 | 51 | | 参数 | 类型 | 默认值 | 说明 52 | | :---- | :---- | :---- | :---- 53 | | `link` | `string` | `''` | 授权地址 ( 发放令牌接口地址 ) 54 | -------------------------------------------------------------------------------- /example/views/items/image.vue: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/title/Title.tsx: -------------------------------------------------------------------------------- 1 | import { computed, defineComponent, type SlotsType } from 'vue' 2 | import { TitleProps } from './props' 3 | import { $tools } from '../../utils/tools' 4 | import { getPropSlot } from '../_utils/props' 5 | import { useWindowResize } from '../../hooks/useWindowResize' 6 | import applyTheme from '../_utils/theme' 7 | import styled from './style/title.module.less' 8 | 9 | const MiTitle = defineComponent({ 10 | name: 'MiTitle', 11 | inheritAttrs: false, 12 | props: TitleProps(), 13 | slots: Object as SlotsType<{ extra: any }>, 14 | setup(props, { slots }) { 15 | applyTheme(styled) 16 | 17 | const { width } = useWindowResize() 18 | const fontSize = computed(() => { 19 | return $tools.convert2rem($tools.distinguishSize(props.size, width.value)) 20 | }) 21 | const style = computed(() => { 22 | return { 23 | fontSize: fontSize.value, 24 | color: props?.color ?? null, 25 | ...$tools.wrapPositionOrSpacing(props.margin, 'margin') 26 | } 27 | }) 28 | const extra = getPropSlot(slots, props, 'default') 29 | return () => ( 30 | 31 | 32 | 33 | {extra ? {extra} : null} 34 | 35 | 36 | ) 37 | } 38 | }) 39 | 40 | export default MiTitle 41 | -------------------------------------------------------------------------------- /src/components/theme/Provider.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, onMounted, ref } from 'vue' 2 | import { ThemeProviderProps } from './props' 3 | import { $g } from '../../utils/global' 4 | import { $tools } from '../../utils/tools' 5 | 6 | const MiThemeProvider = defineComponent({ 7 | name: 'MiThemeProvider', 8 | inheritAttrs: false, 9 | props: ThemeProviderProps(), 10 | setup(props, { slots }) { 11 | const key = ref($tools.uid(false, `${$g.prefix}theme-provider-scope-`)) 12 | const id = `${$g.prefix}component-custom-provider-css-variables-${$tools.uid(false, '')}` 13 | const tokens = ref([]) 14 | const getCustomTokens = (data: Record, name?: string) => { 15 | for (const key in data) { 16 | const index = `${name}-${key}` 17 | if (typeof data[key] === 'string') { 18 | tokens.value.push(`--${index}: ${data[key]};`) 19 | } else if (typeof data[key] === 'object') { 20 | getCustomTokens(data[key], index) 21 | } 22 | } 23 | } 24 | const prefix = $g.prefix 25 | getCustomTokens( 26 | props.tokens, 27 | prefix.charAt(prefix.length - 1) === '-' 28 | ? prefix.substring(0, prefix.length - 1) 29 | : prefix 30 | ) 31 | 32 | onMounted(() => 33 | $tools.createCssVariablesElement(tokens.value, id, true, true, `#${key.value}`) 34 | ) 35 | 36 | return () => {slots?.default()} 37 | } 38 | }) 39 | 40 | export default MiThemeProvider 41 | -------------------------------------------------------------------------------- /src/components/code/README.md: -------------------------------------------------------------------------------- 1 | # 代码高亮 2 | 3 | > 集成轻量且优雅的语法高亮库「 `prismjs` 」 4 | > 5 | > 「 `HTML` 」及「 `JS` 」无需额外引入,直接使用即可 6 | > 7 | > 其他语言的高亮显示,需要引入对应的语言组件,所有组件文件在「 `node_modules/prismjs/components/**` 」 8 | 9 | ## 使用示例 10 | 11 | ### 默认 12 | 13 | ```html 14 | 15 | 16 | 17 | 18 | 19 | 32 | ``` 33 | 34 | ## 主题配置 35 | 36 | ### 配置示例 37 | 38 | > 请查看 「 [`主题配置`](../theme/README.md) 」组件 39 | 40 | ### Tokens 41 | 42 | #### Clock Tokens 43 | 44 | | Token | 默认值 45 | | :---- | :---- 46 | | `--mi-code-text` | `--mi-on-background` 47 | | `--mi-code-scrollbar` | `--mi-primary` 48 | | `--mi-code-background` | `--mi-background` 49 | | `--mi-code-border` | `--mi-background` 50 | | `--mi-code-dot-red` | `--mi-primary` 51 | | `--mi-code-dot-orange` | `--mi-secondary` 52 | | `--mi-code-dot-green` | `--mi-tertiary` 53 | | `--mi-code-copy-icon` | `--mi-surface` 54 | | `--mi-code-copy-background-start` | `--mi-primary` 55 | | `--mi-code-copy-background-hint` | `--mi-secondary` 56 | | `--mi-code-copy-background-stop` | `--mi-tertiary` 57 | 58 | ## API 59 | 60 | ### MiCode `` 61 | 62 | #### `MiCode` 属性 ( `Properties` ) 63 | 64 | | 参数 | 类型 | 默认值 | 说明 65 | | :---- | :---- | :---- | :---- 66 | | `language` | `string` | `html` | 语言 67 | | `content` | `string \| vSlot` | `''` | 内容 68 | | `canCopy` | `boolean` | `true` | 是否开启复制功能 69 | -------------------------------------------------------------------------------- /src/components/code/props.ts: -------------------------------------------------------------------------------- 1 | import type { VNodeTypes } from 'vue' 2 | import { object } from 'vue-types' 3 | import { PropTypes } from '../../utils/types' 4 | import type { DividerProps } from 'ant-design-vue' 5 | 6 | /** 7 | * +===================+ 8 | * | Code | 9 | * +===================+ 10 | * @param language 语言类型 11 | * @param content 代码内容 12 | * @param canCopy 开启复制代码的按钮 13 | */ 14 | export interface CodeProperties { 15 | language: string 16 | content: any 17 | canCopy: boolean 18 | } 19 | export const CodeProps = () => ({ 20 | language: PropTypes.string.def('html'), 21 | content: PropTypes.any, 22 | canCopy: PropTypes.bool.def(true) 23 | }) 24 | 25 | /** 26 | * +=======================+ 27 | * | Code Demo | 28 | * +=======================+ 29 | * @param title 标题内容 30 | * @param titleSetting 标题设置 ( Antdv Divider ) 31 | * @param summary 摘要 32 | * @param effect 效果 33 | * @param animation Code 展示动画 34 | * @param language 代码语言 35 | * @param code 示例代码 36 | * 37 | * @see DividerProps 38 | */ 39 | export interface CodeDemoProperties { 40 | title: string 41 | titleSetting: Partial 42 | summary: VNodeTypes 43 | effect: VNodeTypes 44 | animation: string 45 | language: string 46 | code: string 47 | } 48 | 49 | export const CodeDemoProps = () => ({ 50 | title: PropTypes.string, 51 | titleSetting: object>().def({ orientation: 'left' }), 52 | summary: PropTypes.any, 53 | effect: PropTypes.any, 54 | animation: PropTypes.string.def('fade'), 55 | language: PropTypes.string.def('html'), 56 | code: PropTypes.string 57 | }) 58 | -------------------------------------------------------------------------------- /example/views/items/list.vue: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /tsconfig.umd.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./tsconfig.schema.json", 3 | "compilerOptions": { 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "lib": ["ESNext", "ES2020", "DOM", "DOM.Iterable"], 8 | "skipLibCheck": true, 9 | "strictNullChecks": false, 10 | "importHelpers": false, 11 | "esModuleInterop": true, 12 | "moduleResolution": "node", 13 | "experimentalDecorators": true, 14 | "allowSyntheticDefaultImports": true, 15 | "resolveJsonModule": true, 16 | "removeComments": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "declaration": false, 20 | "allowJs": true, 21 | "jsx": "preserve", 22 | "strict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "noImplicitAny": false, 27 | "noImplicitThis": false, 28 | "baseUrl": "./", 29 | "plugins": [{ "name": "typescript-plugin-css-modules", "options": { "customMatcher": "\\.module\\.(c|le|sa|sc)ss$" } }], 30 | "paths": { 31 | "@/*": ["example/*"], 32 | "@miitvip/admin-pro": ["src/index.ts"] 33 | }, 34 | "types": [ 35 | "vitest/globals", 36 | "@testing-library/jest-dom", 37 | "@types/node" 38 | ] 39 | }, 40 | "include": [ 41 | "src/**/*.ts", 42 | "src/**/*.tsx", 43 | "src/**/*.vue" 44 | ], 45 | "exclude": [ 46 | "node_modules", 47 | "dist", 48 | "lib", 49 | "es", 50 | "build", 51 | "example" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /src/components/backtop/props.ts: -------------------------------------------------------------------------------- 1 | import { VNodeTypes } from 'vue' 2 | import { PropTypes } from '../../utils/types' 3 | import type { DeviceSize, Position } from '../../utils/types' 4 | import { object } from 'vue-types' 5 | 6 | /** 7 | * +======================+ 8 | * | 回到顶部 | 9 | * +======================+ 10 | * @param width 宽度 11 | * @param height 高度 12 | * @param radius 圆角弧度 13 | * @param offset 触发偏移量 14 | * @param duration 滚动时长 15 | * @param zIndex 层级 16 | * @param tip 提示语 17 | * @param position 定位 18 | * @param background 背景色 19 | * @param icon 图标 20 | * @param listenerContainer scroll 监听容器 21 | */ 22 | export interface BacktopProperties { 23 | width: number | string | DeviceSize 24 | height: number | string | DeviceSize 25 | radius: number | string | DeviceSize 26 | offset: number 27 | duration: number 28 | zIndex: number 29 | tip: string 30 | position: Position 31 | background: string 32 | icon: VNodeTypes 33 | listenerContainer: HTMLElement 34 | } 35 | 36 | export const BacktopProps = () => ({ 37 | width: PropTypes.oneOfType([PropTypes.string, PropTypes.number, object()]).def(48), 38 | height: PropTypes.oneOfType([PropTypes.string, PropTypes.number, object()]).def(48), 39 | radius: PropTypes.oneOfType([PropTypes.string, PropTypes.number, object()]).def(48), 40 | offset: PropTypes.number.def(200), 41 | duration: PropTypes.number.def(1000), 42 | zIndex: PropTypes.number.def(Date.now()), 43 | tip: PropTypes.string, 44 | position: object().def({ bottom: 40, right: 40 }), 45 | background: PropTypes.string, 46 | icon: PropTypes.any, 47 | listenerContainer: PropTypes.oneOfType([HTMLElement]) 48 | }) 49 | -------------------------------------------------------------------------------- /src/utils/basic.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | import { type App } from 'vue' 3 | import { createPinia } from 'pinia' 4 | import i18n from '../locales' 5 | import Global, { $g } from './global' 6 | import { default as Api, api } from './api' 7 | import Cookie, { $cookie } from './cookie' 8 | import Storage, { $storage } from './storage' 9 | import Request, { $request } from './request' 10 | import Tools, { $tools } from './tools' 11 | import Prism from '../directives/prism' 12 | import Limit from '../directives/limit' 13 | import { register, SwiperContainer, SwiperSlide } from 'swiper/element/bundle' 14 | 15 | const pinia = createPinia() 16 | const components = [pinia, i18n, Global, Api, Cookie, Storage, Request, Prism, Limit, Tools] 17 | 18 | declare module '@vue/runtime-core' { 19 | export interface ComponentCustomProperties { 20 | $g: typeof $g 21 | api: typeof api 22 | $tools: typeof $tools 23 | $cookie: typeof $cookie 24 | $storage: typeof $storage 25 | $request: typeof $request 26 | } 27 | export interface GlobalComponents { 28 | SwiperContainer: SwiperContainer 29 | SwiperSlide: SwiperSlide 30 | } 31 | } 32 | 33 | let __tree_shaking_basic__ = false 34 | export default { 35 | install(app: App) { 36 | __tree_shaking_basic__ = true 37 | // 默认主题 38 | $tools.createThemeProperties($g.theme.primary || '#63ACFF') 39 | // 窗口大小 40 | $tools.setWinSize() 41 | // 轮播 42 | register() 43 | // 注册组件 44 | components.forEach((component) => [ 45 | app.use(component as typeof component & { install: () => void }) 46 | ]) 47 | return app 48 | } 49 | } 50 | 51 | export { __tree_shaking_basic__ } 52 | -------------------------------------------------------------------------------- /src/components/image/Image.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, ref, onMounted, nextTick, onBeforeUnmount } from 'vue' 2 | import { ImageProps } from './props' 3 | import { $g } from '../../utils/global' 4 | import { $tools } from '../../utils/tools' 5 | 6 | const MiImage = defineComponent({ 7 | name: 'MiImage', 8 | inheritAttrs: false, 9 | props: ImageProps(), 10 | emits: ['load'], 11 | setup(props, { emit, attrs }) { 12 | const imgRef = ref() 13 | let temp: any = null 14 | onMounted(() => { 15 | nextTick().then(() => { 16 | temp = new Image() 17 | temp.src = props.src 18 | temp.onload = () => { 19 | emit('load', imgRef.value) 20 | temp.src = '' 21 | } 22 | }) 23 | }) 24 | 25 | onBeforeUnmount(() => { 26 | if (temp) { 27 | temp.onload = null 28 | temp.onerror = null 29 | temp = null 30 | } 31 | }) 32 | 33 | const style = { 34 | width: $tools.convert2rem($tools.distinguishSize(props?.width)), 35 | height: $tools.convert2rem($tools.distinguishSize(props?.height)), 36 | borderRadius: $tools.convert2rem($tools.distinguishSize(props?.radius)) 37 | } as any 38 | 39 | return () => ( 40 | 50 | ) 51 | } 52 | }) 53 | 54 | export default MiImage 55 | -------------------------------------------------------------------------------- /src/components/menu/Item.tsx: -------------------------------------------------------------------------------- 1 | import { computed, defineComponent, type Plugin } from 'vue' 2 | import { MenuItemProps } from './props' 3 | import { Menu } from 'ant-design-vue' 4 | import { $g } from '../../utils/global' 5 | import { useMenuStore } from '../../stores/menu' 6 | import { useLayoutStore } from '../../stores/layout' 7 | import MiLink from '../link/Link' 8 | import MiMenuItemTitle from './Title' 9 | import applyTheme from '../_utils/theme' 10 | import styled from './style/item.module.less' 11 | 12 | const MiMenuItem = defineComponent({ 13 | name: 'MiMenuItem', 14 | inheritAttrs: false, 15 | props: MenuItemProps(), 16 | setup(props) { 17 | const useLayout = useLayoutStore() 18 | const useMenu = useMenuStore() 19 | const collapsed = computed(() => useLayout.collapsed) 20 | const activeKeys = computed(() => useMenu.activeKeys) 21 | applyTheme(styled) 22 | 23 | const key = $g.prefix + props?.item?.name 24 | const classes = computed(() => { 25 | return [ 26 | styled.container, 27 | { [styled.collapsed]: collapsed.value }, 28 | { [styled.active]: activeKeys.value.includes(key) } 29 | ] 30 | }) 31 | const linkProps = { 32 | path: props?.item?.path, 33 | query: props?.item?.query || {} 34 | } 35 | 36 | return () => ( 37 | 38 | 39 | 40 | 41 | 42 | ) 43 | } 44 | }) 45 | 46 | MiMenuItem.Title = MiMenuItemTitle 47 | 48 | export default MiMenuItem as typeof MiMenuItem & 49 | Plugin & { 50 | readonly Title: typeof MiMenuItemTitle 51 | } 52 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./tsconfig.schema.json", 3 | "compilerOptions": { 4 | "target": "ES2020", 5 | "useDefineForClassFields": true, 6 | "module": "ESNext", 7 | "sourceMap": true, 8 | "lib": ["ESNext", "ES2020", "DOM", "DOM.Iterable"], 9 | "skipLibCheck": true, 10 | "strictNullChecks": false, 11 | "importHelpers": true, 12 | "esModuleInterop": true, 13 | "moduleResolution": "node", 14 | "experimentalDecorators": true, 15 | "allowSyntheticDefaultImports": true, 16 | "resolveJsonModule": true, 17 | "removeComments": false, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "declaration": true, 21 | "declarationDir": "es", 22 | "allowJs": true, 23 | "jsx": "preserve", 24 | "jsxFactory": "h", 25 | "jsxFragmentFactory": "Fragment", 26 | "strict": true, 27 | "noUnusedLocals": true, 28 | "noUnusedParameters": true, 29 | "noFallthroughCasesInSwitch": true, 30 | "noImplicitAny": false, 31 | "noImplicitThis": false, 32 | "baseUrl": "./", 33 | "plugins": [{ "name": "typescript-plugin-css-modules", "options": { "customMatcher": "\\.module\\.(c|le|sa|sc)ss$" } }], 34 | "paths": { 35 | "@/*": ["example/*"], 36 | "@miitvip/admin-pro": ["src/index.ts"] 37 | }, 38 | "types": [ 39 | "vitest/globals", 40 | "@testing-library/jest-dom", 41 | "@types/node" 42 | ] 43 | }, 44 | "files": ["src/typings.d.ts"], 45 | "include": [ 46 | "src/**/*.ts", 47 | "src/**/*.tsx", 48 | "src/**/*.vue" 49 | ], 50 | "exclude": [ 51 | "node_modules", 52 | "dist", 53 | "lib", 54 | "es", 55 | "build", 56 | "example" 57 | ] 58 | } 59 | -------------------------------------------------------------------------------- /src/components/title/README.md: -------------------------------------------------------------------------------- 1 | # 标题设置(MiTitle) 2 | 3 | > 「标题设置」组件用于统一页面区块标题的展示样式,支持字号、颜色、间距、对齐方式以及额外操作区域。 4 | 5 | ## 使用示例 6 | 7 | ### 基础用法 8 | 9 | ```html 10 | 11 | ``` 12 | 13 | ### 标题居中 14 | 15 | ```html 16 | 17 | ``` 18 | 19 | ### 带操作区(默认插槽) 20 | 21 | 默认插槽会渲染在标题右侧,适合放按钮、筛选条件等操作区域: 22 | 23 | ```html 24 | 25 | Extra 内容 26 | 27 | ``` 28 | 29 | ### 自定义字号与颜色 30 | 31 | ```html 32 | 38 | ``` 39 | 40 | ### 在 TSX / JSX 中使用 41 | 42 | ```text 43 | import MiTitle from '@/components/title' 44 | 45 | export default () => ( 46 | 47 | 操作 48 | 49 | ) 50 | ``` 51 | 52 | ## 主题配置 53 | 54 | ### 配置示例 55 | 56 | > 请查看 「 [`主题配置`](../theme/README.md) 」组件 57 | 58 | ### Tokens 59 | 60 | #### Title Tokens 61 | 62 | | Token | 默认值 | 63 | | :---- | :---- | 64 | | `--mi-title-undeline-start` | `--mi-primary` | 65 | | `--mi-title-undeline-hint` | `--mi-secondary` | 66 | | `--mi-title-undeline-stop` | `--mi-tertiary` | 67 | 68 | ## API 69 | 70 | ### 组件:`` / `MiTitle` 71 | 72 | #### Props 73 | 74 | | 参数 | 类型 | 默认值 | 说明 | 75 | | :------- | :--- | :----- | :--- | 76 | | `title` | `string` | `''` | 标题文本(支持 HTML 字符串渲染) | 77 | | `center` | `boolean` | `false` | 是否居中显示标题内容 | 78 | | `size` | `string \| number \|` [`DeviceSize`](../../utils/README.md) | `24` | 标题字号,支持固定像素、响应式设备尺寸配置 | 79 | | `color` | `string` | `''` | 标题文字颜色(CSS 颜色值) | 80 | | `margin` | [`Position`](../../utils/README.md) | `{}` | 外边距配置,支持 top / right / bottom / left 属性 | 81 | 82 | #### Slots 83 | 84 | | 插槽名 | 说明 | 85 | | :-------- | :--- | 86 | | `default` | 标题右侧额外内容区域(如按钮、操作入口等),不传则不显示 | 87 | -------------------------------------------------------------------------------- /example/views/components/quote.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 「 5 | Makeit Admin Pro 6 | 」致力于提供给程序员愉悦的开发体验。 7 | 8 | 9 | 10 | 11 | 在开始之前,推荐先学习「 12 | 13 | Vue 14 | 15 | 」和「 16 | 17 | 18 | ES2015 19 | 20 | 21 | 」,并正确安装和配置了「 22 | 23 | NodeJS 24 | 25 | 」v18.18.2 或以上版本。 26 | 27 | 28 | 29 | 30 | 官方指南假设你已了解关于「 31 | HTML / CSS / Javascript 32 | 」的中级知识,并且已经完全掌握了「 33 | Vue 34 | 」的正确开发方式。 35 | 36 | 37 | 38 | 39 | 如果你刚开始学习前端或者「 40 | Vue 41 | 」,将 42 | UI 框架 作为你的第一步可能不是最好的主意。 43 | 44 | 45 | 46 | 47 | 48 | 49 | 局部主题配置生效 50 | 51 | -------------------------------------------------------------------------------- /src/components/theme/README.md: -------------------------------------------------------------------------------- 1 | # 主题配置 2 | 3 | > 「 主题配置 」 组件为系统内置组件,无需额外引入,用于配置整站的主题色及各个组件的 `tokens` 配置。 4 | 5 | ## 使用示例 6 | 7 | ### 全局主题配置 8 | 9 | ```html 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 43 | ``` 44 | 45 | ### 单独主题配置 46 | 47 | ```html 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 69 | ``` 70 | 71 | ## API 72 | 73 | ### MiTheme `` 74 | 75 | #### `MiTheme` 属性 ( `Properties` ) 76 | 77 | | 参数 | 类型 | 默认值 | 说明 78 | | :---- | :---- | :---- | :---- 79 | | `theme` | [`ThemeTokens`](./tokens.ts) | `{}` | 主题配置 80 | 81 | ### MiThemeProvider `` 82 | 83 | #### `MiThemeProvider` 属性 ( `Properties` ) 84 | 85 | | 参数 | 类型 | 默认值 | 说明 86 | | :---- | :---- | :---- | :---- 87 | | `tokens` | [`ComponentTokens`](./tokens.ts) | `{}` | 组件主题配置 88 | -------------------------------------------------------------------------------- /src/components/layout/style/content.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | position: relative; 5 | color: var(--mi-layout-content-text); 6 | display: flex; 7 | flex: auto; 8 | flex-direction: column; 9 | min-height: 0; 10 | top: 0; 11 | height: calc(100svh - 2rem); 12 | padding-top: @mi-header-height; 13 | .properties(padding-left, 16); 14 | .properties(padding-right, 16); 15 | .properties(padding-bottom, 72); 16 | overflow: hidden; 17 | .radius(8); 18 | 19 | @media only screen and (max-width: @mi-xl) and (min-width: @mi-sm) { 20 | .properties(padding-bottom, 104); 21 | } 22 | 23 | &.collapsed { 24 | .properties(padding-bottom, 72); 25 | 26 | @media only screen and (max-width: @mi-lg) and (min-width: @mi-sm) { 27 | .properties(padding-bottom, 104); 28 | } 29 | } 30 | } 31 | 32 | .has { 33 | &-historical { 34 | &-routes { 35 | .properties(padding-top, @mi-header-height + @mi-historical-height + 1.5rem); 36 | } 37 | } 38 | } 39 | 40 | .inner { 41 | width: 100%; 42 | height: 100%; 43 | overflow-x: hidden; 44 | overflow-y: auto; 45 | .radius(8); 46 | position: relative; 47 | .hide-scrollbar(); 48 | 49 | .box { 50 | position: absolute; 51 | top: 0; 52 | left: 0; 53 | width: 100%; 54 | height: 100%; 55 | .transition(); 56 | } 57 | } 58 | 59 | .mask { 60 | position: fixed; 61 | left: 0; 62 | top: 0; 63 | background: var(--mi-layout-content-mask); 64 | width: 100%; 65 | .frosted(16); 66 | box-shadow: 0 0 .625rem var(--mi-layout-content-shadow); 67 | } 68 | 69 | :export { 70 | --layout-content-text: var(--mi-on-background); 71 | --layout-content-mask: var(--mi-shadow); 72 | --layout-content-shadow: var(--mi-shadow); 73 | --layout-content-background: var(--mi-background); 74 | } -------------------------------------------------------------------------------- /src/components/menu/style/submenu.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | @ant-menu-submenu: ant-menu-submenu; 4 | 5 | .container { 6 | position: relative; 7 | 8 | &.collapsed { 9 | :global { 10 | .@{ant-menu-submenu}-title { 11 | .flex(center, flex-start); 12 | flex-wrap: nowrap; 13 | white-space: nowrap; 14 | 15 | .@{ant-menu-submenu}-arrow { 16 | display: none; 17 | } 18 | 19 | .ant-menu-title-content { 20 | width: 100%; 21 | } 22 | } 23 | } 24 | } 25 | } 26 | 27 | .popup { 28 | max-height: calc(100vh - @mi-margin * 2); 29 | border: .0625rem solid var(--mi-menu-submenu-popup-border); 30 | z-index: @mi-zindex + 1100; 31 | 32 | :global { 33 | color: var(--mi-menu-submenu-popup-text); 34 | background: var(--mi-menu-submenu-popup-background); 35 | 36 | ul.ant-menu.ant-menu-sub { 37 | .properties(padding); 38 | background-color: var(--mi-menu-submenu-popup-background); 39 | 40 | .ant-menu-item { 41 | .properties(height, @mi-menu-item-height); 42 | .properties(line-height, @mi-menu-item-height); 43 | margin-left: 0; 44 | margin-right: 0; 45 | margin-bottom: 0; 46 | .flex(center, flex-start); 47 | width: 100%; 48 | 49 | .mi-menu-item-title-name { 50 | display: flex !important; 51 | .properties(margin-left, 16); 52 | } 53 | } 54 | } 55 | } 56 | } 57 | 58 | :export { 59 | --menu-submenu-popup-text: var(--mi-on-surface-variant); 60 | --menu-submenu-popup-border: rgba(var(--mi-rgb-on-surface-variant), .5); 61 | --menu-submenu-popup-background: var(--mi-surface-variant); 62 | } -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { type I18n, createI18n, type VueI18n } from 'vue-i18n' 3 | import { $g } from '../utils/global' 4 | import { $storage } from '../utils/storage' 5 | import zhCN from './zh-cn' 6 | import enUS from './en-us' 7 | 8 | const DEFAULT_LANG = $g?.locale || 'zh-cn' 9 | const LOCALE_KEY = $g.caches.storages.locale || 'language-locale' 10 | 11 | const locales = { 12 | 'zh-cn': zhCN, 13 | 'en-us': enUS 14 | } 15 | 16 | const i18nIns = createI18n({ 17 | legacy: false, 18 | locale: DEFAULT_LANG, 19 | fallbackLocale: DEFAULT_LANG, 20 | silentTranslationWarn: true, 21 | messages: locales, 22 | globalInjection: true, 23 | warnHtmlMessage: false 24 | }) as I18n & VueI18n & Record 25 | 26 | const setLocale = async (locale?: string, message?: {}) => { 27 | if (!locale) 28 | locale = 29 | typeof window !== 'undefined' ? $storage.get(LOCALE_KEY) || DEFAULT_LANG : DEFAULT_LANG 30 | if (locales[locale]) { 31 | i18nIns.global.mergeLocaleMessage(locale, message || {}) 32 | } else if (Object.keys(message || {}).length > 0) { 33 | i18nIns.global.setLocaleMessage(locale, message) 34 | } else locale = DEFAULT_LANG 35 | ;(i18nIns.global.locale as any).value = locale 36 | $storage.set(LOCALE_KEY, locale) 37 | } 38 | i18nIns.setLocale = setLocale 39 | 40 | let _i18n: any = null 41 | export const setupI18n = (options: { i18n?: any } = {}) => { 42 | _i18n = options.i18n || null 43 | } 44 | 45 | export default { 46 | install(app: App) { 47 | if (_i18n) { 48 | Object.entries(locales).forEach(([locale, msg]) => { 49 | _i18n.global.mergeLocaleMessage(locale, msg) 50 | }) 51 | } else { 52 | app.use(i18nIns) 53 | app.config.globalProperties.$i18n = i18nIns 54 | app.provide('setLocale', (lang: string, message?: {}) => setLocale(lang, message)) 55 | return app 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/anchor/__tests__/link.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { describe, expect, test } from 'vitest' 3 | import MiAnchorLink from '../Link' 4 | import styled from '../style/link.module.less' 5 | 6 | describe('MiAnchorLink', () => { 7 | test('渲染 title & 默认 icon', () => { 8 | const wrapper = mount(MiAnchorLink, { 9 | props: { 10 | id: 'test-id', 11 | title: 'Test Title', 12 | active: false 13 | } 14 | }) 15 | 16 | expect(wrapper.text()).toContain('Test Title') 17 | expect(wrapper.classes()).toContain(styled.container) 18 | expect(wrapper.classes()).not.toContain(styled.active) 19 | expect(wrapper.findComponent({ name: 'TagOutlined' }).exists()).toBe(true) 20 | expect(wrapper.findComponent({ name: 'StarOutlined' }).exists()).toBe(false) 21 | }) 22 | 23 | test('active=true 时显示 active class 与 StarOutlined', () => { 24 | const wrapper = mount(MiAnchorLink, { 25 | props: { 26 | id: 'test-id', 27 | title: 'Test Title', 28 | active: true 29 | } 30 | }) 31 | 32 | expect(wrapper.classes()).toContain(styled.active) 33 | expect(wrapper.findComponent({ name: 'StarOutlined' }).exists()).toBe(true) 34 | expect(wrapper.findComponent({ name: 'TagOutlined' }).exists()).toBe(false) 35 | }) 36 | 37 | test('点击会 emit click 并携带 payload', async () => { 38 | const wrapper = mount(MiAnchorLink, { 39 | props: { 40 | id: 'test-id', 41 | title: 'Test Title', 42 | active: false 43 | } 44 | }) 45 | 46 | await wrapper.trigger('click') 47 | 48 | const emitted = wrapper.emitted().click 49 | expect(emitted).toBeTruthy() 50 | expect(emitted?.[0]?.[0]).toEqual({ id: 'test-id', title: 'Test Title' }) 51 | expect(emitted?.[0]?.[1]).toBeInstanceOf(Event) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /src/components/title/style/title.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | .flex(); 5 | width: 100%; 6 | 7 | .inner { 8 | .flex(center, space-between); 9 | width: 100%; 10 | min-width: 100%; 11 | } 12 | 13 | .center { 14 | align-items: center; 15 | justify-content: center; 16 | 17 | .content { 18 | .flex(); 19 | 20 | &:after { 21 | left: calc(50% - 1rem); 22 | } 23 | } 24 | } 25 | 26 | .content { 27 | position: relative; 28 | .font-size(32, bold); 29 | .transition(); 30 | 31 | @media only screen and (max-width: @mi-md) { 32 | width: 100%; 33 | } 34 | 35 | &:after { 36 | position: absolute; 37 | left: 0; 38 | .properties(bottom, -8); 39 | .properties(width, 32); 40 | .properties(height, 2); 41 | content: ''; 42 | .gradient-hint(var(--mi-title-undeline-start), var(--mi-title-undeline-hint), var(--mi-title-undeline-stop)); 43 | .flex(); 44 | } 45 | } 46 | 47 | .extra { 48 | .flex(); 49 | position: relative; 50 | 51 | @media only screen and (max-width: @mi-md) { 52 | .properties(margin-top, 32); 53 | flex-direction: column; 54 | align-items: flex-start; 55 | width: 100%; 56 | } 57 | 58 | > div { 59 | .flex(flex-start, flex-start); 60 | 61 | @media only screen and (max-width: @mi-md) { 62 | width: 100%; 63 | } 64 | } 65 | } 66 | 67 | & + h1, 68 | & + h2, 69 | & + h3, 70 | & + h4, 71 | & + h5 { 72 | .properties(margin-top); 73 | } 74 | } 75 | 76 | :export { 77 | --title-undeline-start: var(--mi-primary); 78 | --title-undeline-hint: var(--mi-secondary); 79 | --title-undeline-stop: var(--mi-tertiary); 80 | } -------------------------------------------------------------------------------- /src/components/button/README.md: -------------------------------------------------------------------------------- 1 | # 按钮组件 2 | 3 | > 「 按钮组件 」基础作用用于触发一个操作,如提交表单或执行命令。此处封装的按钮组件,主要针对按钮增加箭头动画效果,增强按钮的互动性。 4 | 5 | ## 使用示例 6 | 7 | ### 默认效果 8 | 9 | ```vue 10 | 11 | 12 | 13 | 14 | ``` 15 | 16 | ### 按钮文案 17 | 18 | ```vue 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ``` 27 | 28 | ### 方形按钮 29 | 30 | ```vue 31 | 32 | 33 | 34 | ``` 35 | 36 | ## 主题配置 37 | 38 | ### 配置示例 39 | 40 | > 请查看 「 [`主题配置`](../theme/README.md) 」组件 41 | 42 | ## API 43 | 44 | ### MiButton `` 45 | 46 | #### `MiButton` 属性 ( `Properties` ) 47 | 48 | | 参数 | 类型 | 默认值 | 说明 49 | | :---- | :---- | :---- | :---- 50 | | `width` | `number \| string \|` [`DeviceSize`](../../utils/README.md#interface-devicesize) | `''` | 宽度 51 | | `height` | `number \| string \|` [`DeviceSize`](../../utils/README.md#interface-devicesize) | `''` | 高度 52 | | `text` | `string \|` [`TextSetting`](../../utils/README.md#interface-textsetting) | `''` | 文案配置 53 | | `link` | `string` | `''` | 链接地址 54 | | `target` | `_blank \| _self` | `_self` | 链接打开方式 55 | | `query` | `object` | `{}` | 链接参数配置 56 | | `circle` | `boolean` | `true` | 圆形按钮 57 | | `background` | `string` | `''` | 背景颜色 58 | | `background` | `string` | `blur(1rem)` | 背景过滤 59 | | `arrow` | [`ButtonArrow`](./README.md#interface-buttonarrow) | `{}` | 箭头配置 60 | | `radius` | `number \| string \|` [`DeviceSize`](../../utils/README.md#interface-devicesize) | `''` | 圆角 61 | | `borderColor` | `string` | `rgba(var(--mi-rgb-primary), 0.5)` | 边框颜色 62 | 63 | ### Interface `ButtonArrow` 64 | 65 | | 参数 | 类型 | 默认值 | 说明 66 | | :---- | :---- | :---- | :---- 67 | | `direction` | `'up' \| 'down' \| 'right' \| 'left'` | `right` | 箭头方向 ( & 动画方向 ) 68 | | `delay` | `number` | `0` | 动画执行延迟时长 ( 秒 ) 69 | | `immediate` | `boolean` | `false` | 初始化组件后是否立即执行箭头动画 70 | | `color` | `string` | `''` | 箭头颜色 71 | -------------------------------------------------------------------------------- /src/components/clock/__tests__/clock.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' 3 | import MiClock from '../Clock' 4 | import styled from '../style/clock.module.less' 5 | import { $tools } from '../../../utils/tools' 6 | 7 | describe('MiClock', () => { 8 | let rafSpy: any 9 | let cafSpy: any 10 | 11 | beforeEach(() => { 12 | vi.useFakeTimers() 13 | rafSpy = vi.spyOn($tools, 'raf').mockImplementation(() => 123) 14 | cafSpy = vi.spyOn($tools, 'caf').mockImplementation(() => {}) 15 | }) 16 | 17 | afterEach(() => { 18 | vi.useRealTimers() 19 | rafSpy.mockRestore() 20 | cafSpy.mockRestore() 21 | }) 22 | 23 | test('渲染刻度与指针结构(60 分刻度 + 12 小时刻度)', () => { 24 | const wrapper = mount(MiClock) 25 | 26 | expect(wrapper.find(`.${styled.container}`).exists()).toBe(true) 27 | expect(wrapper.findAll(`.${styled.anchor}`).length).toBe(48) // 60 - 12 个文字刻度 28 | expect(wrapper.findAll(`.${styled.minsText}`).length).toBe(12) 29 | expect(wrapper.findAll(`.${styled.hourText}`).length).toBe(12) 30 | 31 | // 三个旋转点(时/分/秒) 32 | expect(wrapper.findAll(`.${styled.point}`).length).toBe(3) 33 | }) 34 | 35 | test('会调用 raf 启动动画,并在卸载时 caf 取消', () => { 36 | const wrapper = mount(MiClock) 37 | expect(rafSpy).toHaveBeenCalled() 38 | 39 | wrapper.unmount() 40 | expect(cafSpy).toHaveBeenCalledWith(123) 41 | }) 42 | 43 | test('size 为字符串(如 240px)时不应产生 NaN 样式', () => { 44 | const wrapper = mount(MiClock, { props: { size: '240px' } }) 45 | 46 | const containerStyle = wrapper.find(`.${styled.container}`).attributes('style') || '' 47 | expect(containerStyle).not.toContain('NaN') 48 | 49 | // 指针高度也不应是 NaN 50 | const hands = wrapper.findAll(`.${styled.hand}`) 51 | hands.forEach((h) => { 52 | const s = h.attributes('style') || '' 53 | expect(s).not.toContain('NaN') 54 | }) 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /src/components/layout/Sider.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, type SlotsType, type Plugin, computed } from 'vue' 2 | import { LayoutSiderProps } from './props' 3 | import { getPropSlot } from '../_utils/props' 4 | import { useLayoutStore } from '../../stores/layout' 5 | import { useMenuStore } from '../../stores/menu' 6 | import { Skeleton } from 'ant-design-vue' 7 | import MiMenu from '../menu/Menu' 8 | import MiLayoutSiderLogo from './Logo' 9 | import applyTheme from '../_utils/theme' 10 | import styled from './style/sider.module.less' 11 | 12 | const MiLayoutSider = defineComponent({ 13 | name: 'MiLayoutSider', 14 | inheritAttrs: false, 15 | slots: Object as SlotsType<{ 16 | logo: any 17 | menu: any 18 | }>, 19 | props: LayoutSiderProps(), 20 | setup(props, { slots }) { 21 | const useLayout = useLayoutStore() 22 | const useMenu = useMenuStore() 23 | const collapsed = computed(() => useLayout.collapsed) 24 | const menus = computed(() => useMenu.menus) 25 | const loading = computed(() => useMenu.loading) 26 | applyTheme(styled) 27 | 28 | const renderLogo = () => { 29 | return getPropSlot(slots, props, 'logo') ?? 30 | } 31 | 32 | const renderMenu = () => { 33 | return ( 34 | getPropSlot(slots, props, 'menu') ?? ( 35 | 36 | 37 | 38 | ) 39 | ) 40 | } 41 | 42 | return () => ( 43 | 47 | ) 48 | } 49 | }) 50 | 51 | MiLayoutSider.Logo = MiLayoutSiderLogo 52 | 53 | export default MiLayoutSider as typeof MiLayoutSider & 54 | Plugin & { 55 | readonly Logo: typeof MiLayoutSiderLogo 56 | } 57 | -------------------------------------------------------------------------------- /src/locales/zh-cn/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | add: `新增菜单`, 3 | update: `更新菜单`, 4 | name: `名称`, 5 | title: `标题`, 6 | type: `类型`, 7 | icon: `图标`, 8 | page: `前端组件`, 9 | path: `访问路径`, 10 | weight: `排序值`, 11 | top: `一级菜单`, 12 | sub: { 13 | name: `子菜单`, 14 | add: `添加子菜单` 15 | }, 16 | up: `上级菜单`, 17 | detail: `详情`, 18 | subtitle: `子标题`, 19 | unknown: `未知类型`, 20 | auth: `授权标识`, 21 | policy: `授权策略`, 22 | btn: { 23 | name: `按钮名称`, 24 | permission: `按钮/权限` 25 | }, 26 | lang: `多语言关键词`, 27 | open: `打开方式`, 28 | router: { hide: `隐藏菜单` }, 29 | redirect: `默认跳转地址`, 30 | login: `是否需要登录`, 31 | policies: { 32 | invisible: '不可见', 33 | visible: '仅可见', 34 | accessible: '可见/可访问' 35 | }, 36 | icons: { 37 | wireframe: `线框风格`, 38 | solid: `实底风格`, 39 | directional: '方向类图标', 40 | tips: '提示类图标', 41 | edit: '编辑类图标', 42 | data: '数据类图标', 43 | brands: '品牌类图标', 44 | generic: '通用类图标' 45 | }, 46 | badge: { 47 | label: `徽章设置`, 48 | content: `文案内容`, 49 | color: `字体颜色`, 50 | bg: `背景颜色`, 51 | radius: `圆角弧度`, 52 | setting: `点击配置`, 53 | preview: `效果预览`, 54 | none: `暂无效果`, 55 | size: `字体大小` 56 | }, 57 | tips: { 58 | title: `非必填,首选多语言配置标题,次选手动配置标题,默认都为空则与 '名称' 属性内容一致`, 59 | weight: `值越大,越靠前`, 60 | badge: `首选文案,次选图标` 61 | }, 62 | placeholder: { 63 | name: '请输入唯一的菜单名称,如:mi-dashboard', 64 | title: '请输入菜单标题,如:控制台', 65 | subtitle: '请输入菜单子标题,如:Dashboard', 66 | path: '请输入菜单访问路径,如:/dashboard', 67 | page: '请输入菜单对应的前端组件页面,如:views/dashboard', 68 | redirect: '请输入默认跳转地址', 69 | lang: '请输入显示标题的多语言 Key 值', 70 | search: '请输入待搜索的菜单名称', 71 | btn: '请输入按钮/权限名称,如:新增', 72 | icon: '请选择图标', 73 | up: '请选择上级菜单', 74 | auth: '请输入唯一的授权标识, 如: personal:data', 75 | content: `请输入文案内容`, 76 | color: `点击选择颜色` 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /example/views/items/text.vue: -------------------------------------------------------------------------------- 1 | 2 | 39 | 40 | 50 | 51 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/components/socialite/style/socialite.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | .flex(); 5 | color: var(--mi-socialite-title-text); 6 | 7 | :global { 8 | .anticon { 9 | .font-size(16); 10 | .properties(margin-left); 11 | cursor: pointer; 12 | color: var(--mi-socialite-icon); 13 | } 14 | } 15 | } 16 | 17 | .mobile { 18 | width: 100%; 19 | .flex(center, center, column); 20 | position: relative; 21 | 22 | &-line { 23 | border-top: .0625rem solid var(--mi-socialite-mobile-line); 24 | position: absolute; 25 | .properties(top, 16); 26 | left: 0; 27 | z-index: 1; 28 | width: 100%; 29 | .flex(); 30 | } 31 | 32 | &-title { 33 | .gradient-hint(var(--mi-socialite-mobile-title-background-start), var(--mi-socialite-mobile-title-background-hint), var(--mi-socialite-mobile-title-background-stop)); 34 | .properties(height, 32); 35 | .flex(); 36 | .radius(32); 37 | .properties(padding-left, 16); 38 | .properties(padding-right, 16); 39 | color: var(--mi-socialite-mobile-title-text); 40 | z-index: 2; 41 | } 42 | 43 | &-cates { 44 | .flex(center, space-between); 45 | .properties(margin-top, 24); 46 | width: 85%; 47 | 48 | :global { 49 | .anticon { 50 | .properties(font-size, 28); 51 | color: var(--mi-socialite-mobile-icon); 52 | } 53 | } 54 | } 55 | } 56 | 57 | :export { 58 | --socialite-icon: var(--mi-on-background); 59 | --socialite-title-text: var(--mi-on-background); 60 | --socialite-mobile-line: var(--mi-primary); 61 | --socialite-mobile-icon: var(--mi-primary); 62 | --socialite-mobile-title-text: var(--mi-surface-variant); 63 | --socialite-mobile-title-background-start: var(--mi-primary); 64 | --socialite-mobile-title-background-hint: var(--mi-secondary); 65 | --socialite-mobile-title-background-stop: var(--mi-tertiary); 66 | } -------------------------------------------------------------------------------- /vitest.setup.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | import { config } from '@vue/test-utils' 3 | import '@testing-library/jest-dom/vitest' 4 | import { createI18n } from 'vue-i18n' 5 | import zh from './src/locales/zh-cn/index' 6 | import en from './src/locales/en-us/index' 7 | 8 | class MemoryStorage implements Storage { 9 | private store = new Map() 10 | 11 | get length() { 12 | return this.store.size 13 | } 14 | 15 | clear(): void { 16 | this.store.clear() 17 | } 18 | 19 | getItem(key: string): string | null { 20 | return this.store.has(key) ? (this.store.get(key) as string) : null 21 | } 22 | 23 | key(index: number): string | null { 24 | return Array.from(this.store.keys())[index] ?? null 25 | } 26 | 27 | removeItem(key: string): void { 28 | this.store.delete(key) 29 | } 30 | 31 | setItem(key: string, value: string): void { 32 | this.store.set(key, String(value)) 33 | } 34 | } 35 | 36 | const ensureStorage = (name: 'localStorage' | 'sessionStorage') => { 37 | // Node(v20+) 可能在 globalThis 上提供带 getter 的 WebStorage(读取会触发 `--localstorage-file` warning)。 38 | // 这里用 descriptor 读取,避免访问 getter。 39 | const desc = Object.getOwnPropertyDescriptor(globalThis, name) 40 | const s: any = desc && 'value' in desc ? (desc as any).value : undefined 41 | if (!s || typeof s.getItem !== 'function' || typeof s.setItem !== 'function') { 42 | ;(globalThis as any)[name] = new MemoryStorage() 43 | } 44 | } 45 | 46 | ensureStorage('localStorage') 47 | ensureStorage('sessionStorage') 48 | 49 | const pinia = createPinia() 50 | const i18n = createI18n({ 51 | legacy: false, 52 | locale: 'zh-cn', 53 | fallbackLocale: 'zh-cn', 54 | silentTranslationWarn: true, 55 | messages: { zh, en }, 56 | globalInjection: true, 57 | warnHtmlMessage: false 58 | }) 59 | 60 | config.global.stubs = { Transition: false } 61 | config.global.plugins = [pinia, i18n] 62 | 63 | export const setTestLocale = (locale: 'zh' | 'en') => { 64 | i18n.global.locale.value = locale 65 | } 66 | -------------------------------------------------------------------------------- /src/components/code/style/demo.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | .properties(margin-top); 5 | .properties(margin-bottom); 6 | position: relative; 7 | 8 | .inner { 9 | width: 100%; 10 | height: 100%; 11 | border: .0625rem solid var(--mi-code-demo-border); 12 | .radius(8); 13 | .properties(padding, 24); 14 | position: relative; 15 | .transition(); 16 | 17 | @media only screen and (max-width: @mi-md) { 18 | .properties(padding, 16); 19 | } 20 | 21 | @media only screen and (max-width: @mi-sm) { 22 | .properties(padding); 23 | } 24 | 25 | .result { 26 | .radius(8); 27 | overflow: hidden; 28 | } 29 | 30 | .icons { 31 | .flex(); 32 | 33 | @media only screen and (max-width: @mi-sm) { 34 | .properties(margin-bottom, 16); 35 | } 36 | 37 | :global { 38 | .anticon { 39 | .font-size(18); 40 | cursor: pointer; 41 | } 42 | 43 | .anticon + .anticon { 44 | .properties(margin-left, 16); 45 | } 46 | } 47 | } 48 | 49 | .code { 50 | position: relative; 51 | .transition(); 52 | .properties(margin-top, 24); 53 | 54 | @media only screen and (max-width: @mi-sm) { 55 | margin-top: 0; 56 | } 57 | } 58 | 59 | .info { 60 | :global { 61 | .ant-divider-inner-text { 62 | h1, 63 | h2, 64 | h3, 65 | h4, 66 | h5, 67 | h6 { 68 | margin: 0; 69 | .font-size(16, bold); 70 | } 71 | } 72 | } 73 | } 74 | 75 | .margin-top { 76 | .properties(margin-top, 24); 77 | } 78 | } 79 | } 80 | 81 | :export { 82 | --code-demo-border: var(--mi-primary); 83 | } -------------------------------------------------------------------------------- /src/components/search/Key.tsx: -------------------------------------------------------------------------------- 1 | import { SlotsType, VNode, VNodeTypes, createVNode, defineComponent, h, isVNode } from 'vue' 2 | import { SearchKeyProps } from './props' 3 | import MiLink from '../link/Link' 4 | import { getPropSlot } from '../_utils/props' 5 | 6 | const MiSearchKey = defineComponent({ 7 | name: 'MiSearchKey', 8 | inheritAttrs: false, 9 | props: SearchKeyProps(), 10 | slots: Object as SlotsType<{ 11 | default: VNodeTypes 12 | content: VNodeTypes 13 | }>, 14 | setup(props, { slots }) { 15 | let elem: VNode | null = null 16 | const contentSlot = getPropSlot(slots, props, 'content') 17 | const handleContentElem = () => { 18 | return contentSlot 19 | ? isVNode(contentSlot) 20 | ? contentSlot 21 | : typeof contentSlot === 'function' 22 | ? createVNode(contentSlot) 23 | : 'other' 24 | : null 25 | } 26 | const content = handleContentElem() 27 | switch (props.type) { 28 | case 'text': 29 | default: 30 | if (content) { 31 | if (content === 'other') { 32 | if (Array.isArray(contentSlot)) { 33 | elem = h(')} />) 34 | } else elem = h() 35 | } else elem = content 36 | } 37 | break 38 | case 'image': 39 | if (content) { 40 | if (content === 'other') { 41 | elem = h() 42 | } else elem = content 43 | } 44 | break 45 | case 'link': 46 | if (content) { 47 | if (content === 'other') { 48 | elem = {props.content} 49 | } else elem = content 50 | } 51 | break 52 | } 53 | return () => elem 54 | } 55 | }) 56 | 57 | export default MiSearchKey 58 | -------------------------------------------------------------------------------- /src/components/login/props.ts: -------------------------------------------------------------------------------- 1 | import { VNodeTypes } from 'vue' 2 | import { PropTypes } from '../../utils/types' 3 | import { object } from 'vue-types' 4 | import type { CaptchaProperties } from '../captcha/props' 5 | import { SocialiteProperties } from '../socialite/props' 6 | 7 | /** 8 | * +====================+ 9 | * | Login | 10 | * +====================+ 11 | * @param title 标题 12 | * @param video 背景视频 ( 优先级高于 `background` ) 13 | * @param background 背景图 14 | * @param rules Form Rules 校验 15 | * @param content 内容配置 16 | * @param footer 页脚配置 17 | * @param captcha 是否开启验证码 18 | * @param captchaSetting 验证码组件配置 19 | * @param action 登录动作 20 | * @param registerLink 注册链接 21 | * @param forgetPasswordLink 忘记密码链接 22 | * @param socialiteLogin 是否为社会化登录回调 23 | * @param socialiteSetting 社会化登录组件配置 24 | * 25 | * @see CaptchaProperties 26 | */ 27 | export interface LoginProperties { 28 | title: string 29 | video: string 30 | background: string 31 | rules: object 32 | content: VNodeTypes 33 | footer: VNodeTypes 34 | captcha: boolean 35 | captchaSetting: Partial 36 | action: string | Function 37 | registerLink: string 38 | forgetPasswordLink: string 39 | socialiteLogin: boolean 40 | socialiteSetting: Partial 41 | } 42 | 43 | export const LoginProps = () => ({ 44 | title: PropTypes.string, 45 | video: PropTypes.string, 46 | background: PropTypes.string.def(undefined), 47 | rules: PropTypes.object.def({}), 48 | content: PropTypes.any, 49 | footer: PropTypes.any, 50 | captcha: PropTypes.bool.def(true), 51 | captchaSetting: object>().def({}), 52 | action: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, 53 | registerLink: PropTypes.string.def('/register'), 54 | forgetPasswordLink: PropTypes.string.def('/forget'), 55 | socialiteLogin: PropTypes.bool.def(false), 56 | socialiteSetting: object>().def({}) 57 | }) 58 | 59 | export interface LoginFormParams { 60 | url?: string 61 | username?: string 62 | password?: string 63 | remember?: boolean 64 | captcha?: boolean 65 | cuid?: number | string 66 | } 67 | -------------------------------------------------------------------------------- /src/components/anchor/props.ts: -------------------------------------------------------------------------------- 1 | import { Position, PropTypes } from './../../utils/types' 2 | import { object } from 'vue-types' 3 | 4 | /** 5 | * +======================+ 6 | * | 锚点链接 | 7 | * +======================+ 8 | * @param collectContainer 选择器 - 指定待收集的区域 9 | * @param selector 选择器 - 指定待收集的标签 10 | * @param requireAttr 收集的元素所要包含的指定属性 ( 比如必须包含 `id` 的元素 ) 11 | * @param affix 固定悬浮 12 | * @param position 定位 13 | * @param scrollOffset 滚动定位偏移量 14 | * @param reserveOffset 预留偏移量 15 | * @param delayInit 延迟初始化 ( 避免渲染未完成, 节点获取失败 ) 16 | * @param affixText 悬浮状态显示的文案 17 | * @param duration 滚动动画时长 18 | * @param listenerContainer scroll 监听容器 19 | */ 20 | export interface AnchorProperties { 21 | collectContainer: string 22 | selector: string | string[] 23 | requireAttr: string 24 | affix: boolean 25 | position: Position 26 | scrollOffset: number 27 | reserveOffset: number 28 | delayInit: number 29 | affixText: string 30 | duration: number 31 | listenerContainer: HTMLElement 32 | } 33 | 34 | export const AnchorProps = () => ({ 35 | collectContainer: PropTypes.string, 36 | selector: PropTypes.oneOfType([PropTypes.string, PropTypes.array]).def([ 37 | 'h1', 38 | 'h2', 39 | 'h3', 40 | 'h4', 41 | 'h5', 42 | 'h6' 43 | ]), 44 | requireAttr: PropTypes.string, 45 | affix: PropTypes.bool.def(false), 46 | position: object().def({ top: 200 }), 47 | scrollOffset: PropTypes.number.def(80), 48 | reserveOffset: PropTypes.number, 49 | delayInit: PropTypes.number.def(800), 50 | affixText: PropTypes.string, 51 | duration: PropTypes.number.def(1000), 52 | listenerContainer: PropTypes.oneOfType([HTMLElement]) 53 | }) 54 | 55 | /** 56 | * +==========================+ 57 | * | Anchor Link | 58 | * +==========================+ 59 | * @param id 唯一值 60 | * @param title 标题 61 | * @param active 是否选中 62 | */ 63 | export interface AnchorLinkProperties { 64 | id: string 65 | title: string 66 | active: boolean 67 | } 68 | 69 | export const AnchorLinkProps = () => ({ 70 | id: PropTypes.string.isRequired, 71 | title: PropTypes.string.isRequired, 72 | active: PropTypes.bool.def(false) 73 | }) 74 | -------------------------------------------------------------------------------- /src/components/menu/Submenu.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, computed } from 'vue' 2 | import { Menu } from 'ant-design-vue' 3 | import { MenuSubProps } from './props' 4 | import { MenuItem } from '../../utils/types' 5 | import { $g } from '../../utils/global' 6 | import { useLayoutStore } from '../../stores/layout' 7 | import MiMenuItemTitle from './Title' 8 | import MiMenuItem from './Item' 9 | import applyTheme from '../_utils/theme' 10 | import styled from './style/submenu.module.less' 11 | 12 | const MiSubMenu = defineComponent({ 13 | name: 'MiSubMenu', 14 | inheritAttrs: false, 15 | props: MenuSubProps(), 16 | setup(props, { attrs }) { 17 | const useLayout = useLayoutStore() 18 | const children = props?.item?.children 19 | const hasChildren = children && children.length > 0 20 | const pk = $g.prefix + props?.item?.name 21 | const title = ( 22 | 23 | ) 24 | const key = $g.prefix + props?.item?.name 25 | const collapsed = computed(() => useLayout.collapsed) 26 | applyTheme(styled) 27 | 28 | const getSubmenuItem = (): any[] => { 29 | const items: any[] = [] 30 | if (hasChildren) { 31 | children.forEach((child: MenuItem) => { 32 | const grandChild = child?.children 33 | const k = $g.prefix + child?.name 34 | if (grandChild && grandChild.length > 0) { 35 | items.push() 36 | } else items.push() 37 | }) 38 | } 39 | return items 40 | } 41 | 42 | return () => ( 43 | 50 | {...getSubmenuItem()} 51 | 52 | ) 53 | } 54 | }) 55 | 56 | export default MiSubMenu 57 | -------------------------------------------------------------------------------- /ESBUILD_QUICKSTART.md: -------------------------------------------------------------------------------- 1 | # 🚀 esbuild 优化 - 快速开始 2 | 3 | ## 📦 安装依赖 4 | 5 | 首先,安装 esbuild 相关依赖: 6 | 7 | ```bash 8 | npm install --save-dev rollup-plugin-esbuild esbuild 9 | ``` 10 | 11 | ## ✅ 优化完成清单 12 | 13 | - ✅ [rollup.esm.mjs](build/rollup.esm.mjs) - 已配置 esbuild 14 | - ✅ [rollup.umd.mjs](build/rollup.umd.mjs) - 已配置 esbuild 15 | - ✅ Babel 配置精简 - 仅处理 Vue JSX 16 | - ✅ 文档完善 - [ESBUILD_OPTIMIZATION.md](ESBUILD_OPTIMIZATION.md) 17 | 18 | ## 🎯 性能提升 19 | 20 | | 场景 | 提升幅度 | 21 | |------|---------| 22 | | 首次构建 | **减少 40%** | 23 | | 增量构建 | **减少 60%** | 24 | | TS/JS 转译 | **快 10-100 倍** | 25 | 26 | ## 💻 立即使用 27 | 28 | ```bash 29 | # 正常构建(自动使用 esbuild) 30 | npm run build 31 | 32 | # 快速构建 33 | npm run build:fast 34 | 35 | # 完整重建 36 | npm run build:fresh 37 | ``` 38 | 39 | ## 📚 详细文档 40 | 41 | 查看 [ESBUILD_OPTIMIZATION.md](ESBUILD_OPTIMIZATION.md) 了解: 42 | 43 | - 完整技术方案 44 | - 性能对比测试 45 | - 配置说明 46 | - 故障排查 47 | 48 | ## ⚡️ 核心改进 49 | 50 | ### 混合方案 51 | 52 | - **esbuild** → 处理 TypeScript/JavaScript 转译(极速) 53 | - **Babel** → 仅处理 Vue JSX 语法(必需) 54 | 55 | ### 插件执行顺序 56 | 57 | ```text 58 | TypeScript → esbuild ⚡️ → Babel 🎨 → 完成 59 | ``` 60 | 61 | ## 🔧 配置文件变更 62 | 63 | ### build/rollup.esm.mjs 64 | 65 | ```javascript 66 | import esbuild from 'rollup-plugin-esbuild' 67 | 68 | // esbuild 处理 TS/JS 转译 69 | esbuild({ 70 | include: /\.[jt]sx?$/, 71 | exclude: /node_modules/, 72 | sourceMap: true, 73 | target: 'es2015' 74 | }) 75 | 76 | // Babel 仅处理 Vue JSX 77 | babel({ 78 | extensions: ['.jsx', '.tsx'], 79 | plugins: [ 80 | ['@vue/babel-plugin-jsx', { isCustomElement: (tag) => tag.startsWith('swiper-') }] 81 | ], 82 | babelHelpers: 'bundled' 83 | }) 84 | ``` 85 | 86 | ## 💡 注意事项 87 | 88 | 1. **依赖安装**:必须先安装 `rollup-plugin-esbuild` 和 `esbuild` 89 | 2. **向后兼容**:构建输出格式完全不变 90 | 3. **类型检查**:esbuild 不做类型检查,由 TypeScript 插件完成 91 | 4. **Vue JSX**:完全支持,由 Babel 处理 92 | 93 | ## 🐛 遇到问题? 94 | 95 | 如果构建失败,请执行: 96 | 97 | ```bash 98 | # 1. 确认依赖已安装 99 | npm install 100 | 101 | # 2. 清理缓存重试 102 | npm run build:fresh 103 | 104 | # 3. 查看详细日志 105 | npm run build 2>&1 | tee build.log 106 | ``` 107 | 108 | 更多排查步骤请查看 [ESBUILD_OPTIMIZATION.md - 故障排查](ESBUILD_OPTIMIZATION.md#-故障排查) 109 | -------------------------------------------------------------------------------- /src/components/backtop/README.md: -------------------------------------------------------------------------------- 1 | # 回到顶部(MiBacktop) 2 | 3 | > 「回到顶部」组件用于在长页面中快速返回顶部,支持监听指定滚动容器、定制尺寸/位置/样式等。 4 | 5 | ## 使用示例 6 | 7 | ### 默认 8 | 9 | ```html 10 | 11 | 12 | ``` 13 | 14 | ### 指定监听容器 15 | 16 | ```html 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 29 | ``` 30 | 31 | ### 自定义尺寸、位置与提示文案 32 | 33 | ```html 34 | 42 | ``` 43 | 44 | ## 主题配置 45 | 46 | ### 配置示例 47 | 48 | > 请查看 「 [`主题配置`](../theme/README.md) 」组件 49 | 50 | ### Tokens 51 | 52 | #### Backtop Tokens 53 | 54 | | Token | 默认值 55 | | :---- | :---- 56 | | `--mi-backtop-background-start` | `--mi-primary` 57 | | `--mi-backtop-background-hint` | `--mi-secondary` 58 | | `--mi-backtop-background-stop` | `--mi-tertiary` 59 | | `--mi-backtop-icon` | `--mi-on-secondary` 60 | 61 | ## API 62 | 63 | ### MiBacktop `` 64 | 65 | #### `MiBacktop` 属性 ( `Properties` ) 66 | 67 | | 参数 | 类型 | 默认值 | 说明 68 | | :---- | :---- | :---- | :---- 69 | | `width` | `number \| string \|` [`DeviceSize`](../../utils/README.md) | `48` | 宽度 70 | | `height` | `number \| string \|` [`DeviceSize`](../../utils/README.md) | `48` | 高度 71 | | `radius` | `number \| string \|` [`DeviceSize`](../../utils/README.md) | `48` | 圆角弧度 72 | | `offset` | `number` | `200` | 触发偏移量 73 | | `duration` | `number` | `1000` | 滚动时长 74 | | `tip` | `string` | `回到顶部` | 提示语 ( 移动端无效 ) 75 | | `zIndex` | `number` | `Date.now()` | 容器层级 76 | | `position` | [`Position`](../../utils/README.md) | `{ bottom: 40, right: 40 }` | 容器定位 77 | | `background` | `string` | `''` | 背景色 78 | | `icon` | `vSlot` | `` | 图标 79 | | `listenerContainer` | `Window \| HTMLElement` | `document.body` | `scroll` 事件的监听容器 80 | 81 | #### `MiBacktop` 事件 ( `Events` ) 82 | 83 | | 方法 | 返回值 | 说明 84 | | :---- | :---- | :---- 85 | | `end` | *None* | 回到顶部后的事件回调 86 | -------------------------------------------------------------------------------- /src/components/button/props.ts: -------------------------------------------------------------------------------- 1 | import { object } from 'vue-types' 2 | import { tuple } from './../_utils/props' 3 | import { type DeviceSize, PropTypes, type TextSetting } from '../../utils/types' 4 | 5 | /** 6 | * +==========================+ 7 | * | ButtonArrow | 8 | * +==========================+ 9 | * @param direction 箭头方向 ( & 动画方向 ) 10 | * @param delay 动画延迟时长 11 | * @param immediate 初始化组件后是否立即执行箭头动画 12 | * @param color 箭头颜色 13 | */ 14 | export interface ButtonArrow { 15 | direction?: 'up' | 'down' | 'right' | 'left' 16 | delay?: number 17 | immediate?: boolean 18 | color?: string 19 | } 20 | 21 | /** 22 | * +===============================+ 23 | * | ButtonProperties | 24 | * +===============================+ 25 | * @param text 文案内容 26 | * @param link 链接地址 27 | * @param query 链接参数 28 | * @param target 链接打开方式 29 | * @param width 宽度 30 | * @param height 高度 31 | * @param circle 是否为圆形 32 | * @param background 背景色 33 | * @param backdrop 背景过滤 34 | * @param arrow 图标设置 35 | * @param radius 圆角 36 | * @param borderColor 边框颜色 37 | */ 38 | export interface ButtonProperties { 39 | text?: string | TextSetting 40 | link?: string 41 | target?: '_blank' | '_self' 42 | query?: Record 43 | width?: number | string | DeviceSize 44 | height?: number | string | DeviceSize 45 | circle?: boolean 46 | background?: string 47 | backdrop?: string 48 | arrow?: ButtonArrow 49 | radius?: string | number | DeviceSize 50 | borderColor?: string 51 | } 52 | 53 | export const ButtonProps = () => ({ 54 | text: PropTypes.oneOfType([PropTypes.string, object()]), 55 | link: PropTypes.string, 56 | query: PropTypes.object.def({}), 57 | target: PropTypes.oneOf(tuple('_blank', '_self')).def('_self'), 58 | width: PropTypes.oneOfType([PropTypes.number, PropTypes.string, object()]), 59 | height: PropTypes.oneOfType([PropTypes.number, PropTypes.string, object()]), 60 | circle: PropTypes.bool.def(true), 61 | background: PropTypes.string, 62 | backdrop: PropTypes.string, 63 | arrow: object(), 64 | radius: PropTypes.oneOfType([PropTypes.string, PropTypes.number, object()]), 65 | borderColor: PropTypes.string 66 | }) 67 | -------------------------------------------------------------------------------- /src/components/password/props.ts: -------------------------------------------------------------------------------- 1 | import { PropTypes, type DeviceSize } from '../../utils/types' 2 | import { object } from 'vue-types' 3 | import { placement, tuple } from '../_utils/props' 4 | 5 | /** 6 | * +=======================+ 7 | * | Password | 8 | * +=======================+ 9 | * @param width 输入框宽度 10 | * @param height 输入框高度 11 | * @param radius 输入框圆角弧度 12 | * @param value v-model 13 | * @param skipCheck 跳过密码检查 14 | * @param min 密码最低长度 15 | * @param max 密码最大长度 16 | * @param complexity 是否为复杂密码 ( 字母 + 数字 + 符号 ) 17 | * @param complexityTip 复杂密码提示语 18 | * @param confirm 是否显示确认密码输入框 19 | * @param confirmValue v-model 再次确认密码 20 | * @param level 密码等级提示语 21 | * @param rules 校验规则 ( Form Rules ) 22 | * @param placement 弹出位置 23 | * @param isRequired 是否必填 ( 结合 skipCheck 单独生成密码输入框时 ) 24 | */ 25 | export interface PasswordProperties { 26 | width: string | number | DeviceSize 27 | height: string | number | DeviceSize 28 | radius: string | number | DeviceSize 29 | value: string | number 30 | skipCheck: boolean 31 | min: number 32 | max: number 33 | complexity: boolean 34 | complexityTip: string 35 | confirm: boolean 36 | confirmValue: string | number 37 | level: object 38 | rules: object 39 | placement: string 40 | isRequired: boolean 41 | } 42 | 43 | export const PasswordProps = () => ({ 44 | width: PropTypes.oneOfType([PropTypes.string, PropTypes.number, object()]), 45 | height: PropTypes.oneOfType([PropTypes.string, PropTypes.number, object()]).def(42), 46 | radius: PropTypes.oneOfType([PropTypes.string, PropTypes.number, object()]).def(42), 47 | value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 48 | skipCheck: PropTypes.bool.def(false), 49 | min: PropTypes.number.def(6), 50 | max: PropTypes.number.def(32), 51 | complexity: PropTypes.bool.def(true), 52 | complexityTip: PropTypes.string.def(undefined), 53 | confirm: PropTypes.bool.def(false), 54 | confirmValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 55 | level: object<{ [index: number]: string }>().def(), 56 | rules: PropTypes.object.def({}), 57 | placement: PropTypes.oneOf(tuple(...placement)).def('top'), 58 | isRequired: PropTypes.bool.def(false) 59 | }) 60 | -------------------------------------------------------------------------------- /src/components/dropdown/Item.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, isVNode, h } from 'vue' 2 | import { DropdownItemProps } from './props' 3 | import { $tools } from '../../utils/tools' 4 | import applyTheme from '../_utils/theme' 5 | import styled from './style/item.module.less' 6 | 7 | const MiDropdownItem = defineComponent({ 8 | name: 'MiDropdownItem', 9 | inheritAttrs: false, 10 | props: DropdownItemProps(), 11 | setup(props) { 12 | applyTheme(styled) 13 | 14 | const icon = props.item?.icon 15 | const fontSize = $tools.convert2rem($tools.distinguishSize(props.item?.fontSize)) 16 | const iconSize = $tools.convert2rem($tools.distinguishSize(props.item?.iconSize)) 17 | let tag: any = null 18 | if (props.item?.tag) { 19 | if (props.item.tag?.content) { 20 | tag = 21 | } else if (props.item.tag?.icon) { 22 | tag = ( 23 | 31 | {isVNode(props.item.tag.icon) 32 | ? props.item.tag.icon 33 | : h(props.item.tag.icon)} 34 | 35 | ) 36 | } 37 | } 38 | 39 | return () => ( 40 | 41 | 42 | 43 | {icon ? (isVNode(icon) ? icon : h(icon)) : null} 44 | 45 | {props.item?.title ? ( 46 | 51 | ) : null} 52 | 53 | {tag} 54 | 55 | ) 56 | } 57 | }) 58 | 59 | export default MiDropdownItem 60 | -------------------------------------------------------------------------------- /src/components/image/__tests__/image.test.ts: -------------------------------------------------------------------------------- 1 | import { mount } from '@vue/test-utils' 2 | import { afterEach, describe, expect, test, vi } from 'vitest' 3 | import { nextTick } from 'vue' 4 | import MiImage from '../Image' 5 | 6 | const flushPromises = async () => { 7 | await Promise.resolve() 8 | await Promise.resolve() 9 | } 10 | 11 | describe('MiImage', () => { 12 | const originals = { 13 | Image: globalThis.Image 14 | } 15 | 16 | afterEach(() => { 17 | globalThis.Image = originals.Image 18 | vi.restoreAllMocks() 19 | document.body.innerHTML = '' 20 | }) 21 | 22 | test('加载完成会 emit load,并传出 img element,同时支持透传 style/class', async () => { 23 | let onload: null | (() => void) = null 24 | 25 | // mock window.Image(用于预加载) 26 | // eslint-disable-next-line @typescript-eslint/no-extraneous-class 27 | class FakeImage { 28 | src = '' 29 | onload: any = null 30 | onerror: any = null 31 | constructor() { 32 | onload = () => this.onload?.() 33 | } 34 | } 35 | globalThis.Image = FakeImage as any 36 | 37 | const wrapper = mount(MiImage, { 38 | props: { 39 | src: 'x.png', 40 | width: 10, 41 | height: 12, 42 | radius: 8 43 | }, 44 | attrs: { 45 | class: 'c1', 46 | style: { border: '1px solid red' } 47 | }, 48 | attachTo: document.body 49 | }) 50 | 51 | await nextTick() 52 | await flushPromises() 53 | 54 | expect(onload).toBeTruthy() 55 | onload?.() 56 | await nextTick() 57 | 58 | expect(wrapper.emitted().load).toBeTruthy() 59 | const payload = wrapper.emitted().load?.[0]?.[0] as HTMLImageElement 60 | expect(payload).toBeInstanceOf(HTMLImageElement) 61 | 62 | const img = wrapper.find('img') 63 | expect(img.classes()).toContain('c1') 64 | expect(img.attributes('style') || '').toContain('border: 1px solid red') 65 | expect(img.attributes('style') || '').toContain('width:') 66 | expect(img.attributes('style') || '').toContain('height:') 67 | expect(img.attributes('style') || '').toContain('border-radius:') 68 | 69 | wrapper.unmount() 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /src/components/menu/style/title.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | .flex(center, flex-start); 5 | flex-wrap: nowrap; 6 | white-space: nowrap; 7 | .transition(); 8 | width: 100%; 9 | .properties(height, @mi-menu-item-height); 10 | position: relative; 11 | min-width: unset; 12 | 13 | &.collapsed { 14 | .flex(); 15 | } 16 | 17 | .icon { 18 | .properties(margin-right, 12); 19 | .transition(); 20 | 21 | :global(.anticon) { 22 | color: var(--mi-menu-item-title-icon); 23 | .transition(); 24 | } 25 | 26 | &.collapsed { 27 | margin-right: 0; 28 | .properties(width, 20); 29 | 30 | :global(.anticon) { 31 | .font-size(20); 32 | .properties(width, 20); 33 | .flex(); 34 | } 35 | } 36 | } 37 | 38 | &.fold, 39 | &.active, 40 | &.collapsed-active { 41 | .title { 42 | color: var(--mi-menu-item-title-active-text); 43 | font-weight: bold; 44 | 45 | &-sub { 46 | color: var(--mi-menu-item-title-active-sub); 47 | } 48 | } 49 | 50 | .icon { 51 | :global(.anticon) { 52 | color: var(--mi-menu-item-title-active-icon); 53 | } 54 | } 55 | } 56 | } 57 | 58 | .title { 59 | .flex(flex-start, center, column); 60 | .properties(height, @mi-menu-item-height); 61 | .font-size(14); 62 | .properties(line-height, 20); 63 | color: var(--mi-menu-item-title-text); 64 | .transition(); 65 | 66 | &-sub { 67 | .font-size(12); 68 | color: var(--mi-menu-item-title-sub); 69 | } 70 | } 71 | 72 | .tag { 73 | position: absolute; 74 | right: 0; 75 | top: 50%; 76 | transform: translateY(-50%); 77 | .ellipsis(); 78 | width: auto; 79 | .flex(); 80 | .properties(max-width, @mi-gap * 8); 81 | .transition(); 82 | margin-inline-end: 0; 83 | } 84 | 85 | :export { 86 | --menu-item-title-text: var(--mi-on-background); 87 | --menu-item-title-sub: rgba(var(--mi-rgb-on-background), .5); 88 | --menu-item-title-icon: var(--mi-on-background); 89 | --menu-item-title-active-text: var(--mi-on-primary); 90 | --menu-item-title-active-sub: rgba(var(--mi-rgb-on-primary), .6); 91 | --menu-item-title-active-icon: var(--mi-on-primary); 92 | } -------------------------------------------------------------------------------- /src/locales/zh-cn/global.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | site: `Admin Pro`, 3 | ok: `确定`, 4 | cancel: `取消`, 5 | save: `保存`, 6 | all: `全部`, 7 | add: `新增`, 8 | create: `立刻添加`, 9 | edit: `编辑`, 10 | editable: `进入编辑`, 11 | update: `更新`, 12 | submit: `提交`, 13 | close: `关闭`, 14 | closes: { 15 | all: '关闭全部', 16 | left: '关闭左侧', 17 | right: '关闭右侧', 18 | other: '关闭其它' 19 | }, 20 | logout: `退出登录`, 21 | sent: `发送成功`, 22 | delete: { 23 | normal: `删除`, 24 | batch: `批量删除`, 25 | confirm: `确定删除当前所选项?`, 26 | select: `请选择需要删除的选项` 27 | }, 28 | type: `类型`, 29 | state: `状态`, 30 | valid: `有效`, 31 | invalid: `无效`, 32 | reset: `重置`, 33 | more: `更多`, 34 | details: `详细信息`, 35 | code: `编码`, 36 | action: `管理操作`, 37 | key: `关键词`, 38 | system: `系统`, 39 | builtin: `内置`, 40 | customize: `自定义`, 41 | tips: `温馨提示`, 42 | knew: `知道了`, 43 | activate: `前往激活`, 44 | success: `操作成功`, 45 | error: { 46 | normal: `操作失败`, 47 | id: `ID 有误, 请刷新后再试`, 48 | auth: `请求的资源需要认证 ( Unauthorized )`, 49 | unknown: `未知错误`, 50 | status: `状态值不符合要求,请重新设置`, 51 | form: `找不到 Form 表单,校验失败,退出执行流程` 52 | }, 53 | yes: `是`, 54 | no: `否`, 55 | seek: `查询`, 56 | app: `应用`, 57 | menu: `菜单`, 58 | internal: `内部`, 59 | external: `外部`, 60 | view: `点击查看`, 61 | back: `返回`, 62 | back2top: `回到顶部`, 63 | week: { 64 | sun: `周日`, 65 | mon: `周一`, 66 | tues: `周二`, 67 | wed: `周三`, 68 | thur: `周四`, 69 | fri: `周五`, 70 | sat: `周六` 71 | }, 72 | dark: `深色`, 73 | light: `浅色`, 74 | 'no-data': `暂无符合条件的数据`, 75 | backtop: `回到顶部`, 76 | meta: { 77 | title: ``, 78 | keywords: ``, 79 | description: `` 80 | }, 81 | step: { 82 | prev: `上一步`, 83 | next: `下一步` 84 | }, 85 | donate: `您的支持,是我最大的动力`, 86 | colon: `:`, 87 | disable: `禁用`, 88 | enable: `启用`, 89 | available: `上架`, 90 | removed: `下架`, 91 | select: `请选择`, 92 | name: `名称`, 93 | copy: `复制`, 94 | copied: `复制成功`, 95 | setting: `设置`, 96 | page: { 97 | prefix: `第`, 98 | prev: `上一页`, 99 | next: `下一页`, 100 | unit: `页`, 101 | strip: `条`, 102 | total: `共` 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/components/items/detail/props.ts: -------------------------------------------------------------------------------- 1 | import { array, object } from 'vue-types' 2 | import { 3 | type DeviceSize, 4 | PropTypes, 5 | type TextSetting, 6 | type ThumbSetting, 7 | type Gap 8 | } from '../../../utils/types' 9 | import { tuple, animations } from '../../_utils/props' 10 | 11 | export interface DetailItem { 12 | [key: string]: any 13 | title?: string | TextSetting 14 | subtitle?: string | TextSetting 15 | thumb?: string 16 | } 17 | 18 | /** 19 | * +====================================+ 20 | * | ItemsDetailProperties | 21 | * +====================================+ 22 | * @param active 当前展开项 ( v-model ) 23 | * @param data 数据 24 | * @param number 一行显示个数 25 | * @param maxWidth 容器最大宽度 ( 默认 100% ) 26 | * @param gap 间距 27 | * @param animation 详情容器弹出动画 28 | * @param arrowColor 箭头颜色 29 | * @param titleSetting 标题设置 30 | * @param subtitleSetting 副标题设置 31 | * @param thumbSetting 缩略图设置 32 | * @param scrollToPosition 点击后滚动到指定位置 33 | * @param scrollOffset 滚动指定位置时的偏移量 34 | */ 35 | export interface ItemsDetailProperties { 36 | active?: number 37 | data?: DetailItem[] 38 | number?: number | string | DeviceSize 39 | maxWidth?: string | number | DeviceSize 40 | gap?: number | string | DeviceSize | Gap 41 | animation?: string 42 | arrowColor?: string 43 | titleSetting?: TextSetting 44 | subtitleSetting?: TextSetting 45 | thumbSetting?: ThumbSetting 46 | scrollToPosition?: boolean 47 | scrollOffset?: number | string | DeviceSize 48 | } 49 | 50 | export const ItemsDetailProps = () => ({ 51 | active: PropTypes.number.def(-1), 52 | data: array().def([]), 53 | number: PropTypes.oneOfType([PropTypes.number, PropTypes.string, object()]).def({ 54 | mobile: 1, 55 | tablet: 2, 56 | laptop: 3 57 | }), 58 | maxWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number, object()]).def( 59 | '100%' 60 | ), 61 | gap: PropTypes.oneOfType([ 62 | PropTypes.string, 63 | PropTypes.number, 64 | object(), 65 | object() 66 | ]).def(16), 67 | animation: PropTypes.oneOf(tuple(...animations)).def('shake'), 68 | arrowColor: PropTypes.string, 69 | titleSetting: object(), 70 | subtitleSetting: object(), 71 | thumbSetting: object(), 72 | scrollToPosition: PropTypes.bool.def(true), 73 | scrollOffset: PropTypes.oneOfType([PropTypes.number, PropTypes.string, object()]) 74 | }) 75 | -------------------------------------------------------------------------------- /src/components/dropdown/README.md: -------------------------------------------------------------------------------- 1 | # 下拉菜单 2 | 3 | > 「 下拉菜单 」 组件多用于页面操作选项过多时,用此组件收纳操作元素,点击或移入触发点,显示下拉选单,选择对应的菜单项即可执行相应的指令。 4 | 5 | ## 使用示例 6 | 7 | ### 默认 8 | 9 | ```html 10 | 11 | 12 | 13 | 14 | 36 | ``` 37 | 38 | ### 自定义菜单 39 | 40 | ```html 41 | 42 | 43 | 44 | 自定义菜单一 45 | 自定义菜单二 46 | 47 | 48 | 49 | ``` 50 | 51 | ## 主题配置 52 | 53 | ### 配置示例 54 | 55 | > 请查看 「 [`主题配置`](../theme/README.md) 」组件 56 | 57 | ### Tokens 58 | 59 | #### Dropdown Tokens 60 | 61 | | Token | 默认值 62 | | :---- | :---- 63 | | `--mi-dropdown-item-text` | `--mi-on-surface` 64 | | `--mi-dropdown-item-tag-text` | `--mi-surface` 65 | | `--mi-dropdown-item-tag-start` | `--mi-primary` 66 | | `--mi-dropdown-item-tag-hint` | `--mi-secondary` 67 | | `--mi-dropdown-item-tag-stop` | `--mi-tertiary` 68 | 69 | ## API 70 | 71 | ### MiDropdown `` 72 | 73 | #### `MiDropdown` 属性 ( `Properties` ) 74 | 75 | | 参数 | 类型 | 默认值 | 说明 76 | | :---- | :---- | :---- | :---- 77 | | `title` | `string \| vSlot` | `Avatar` | 下拉菜单的触发点 78 | | `placement` | `string` | `bottom` | 弹窗打开位置 `['top', 'left', 'right', 'bottom', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight', 'leftTop', 'leftBottom', 'rightTop', 'rightBottom']` 79 | | `trigger` | `string` | `click` | 触发方式 `['click', 'hover', 'focus', 'contextmenu']` 80 | | `items` | [`DropdownItem`](../../utils/README.md) | `[]` | 菜单选项 81 | | `overlay` | `vSlot` | `''` | 自定义菜单 82 | 83 | ### MiDropdownItem `` 84 | 85 | #### `MiDropdownItem` 属性 ( `Properties` ) 86 | 87 | | 参数 | 类型 | 默认值 | 说明 88 | | :---- | :---- | :---- | :---- 89 | | `item` | [`DropdownItem`](../../utils/README.md) | `{}` | 下拉菜单的触发点 90 | -------------------------------------------------------------------------------- /src/locales/en-us/menu.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | add: 'Add Menu', 3 | update: 'Update Menu', 4 | name: 'Name', 5 | title: 'Title', 6 | type: 'Type', 7 | icon: 'Icon', 8 | page: 'Component', 9 | path: 'Access Path', 10 | weight: 'Sort Order', 11 | top: 'Primary Menu', 12 | sub: { 13 | name: 'Submenu', 14 | add: 'Add Submenu' 15 | }, 16 | up: 'Parent Menu', 17 | detail: 'Details', 18 | subtitle: 'Subtitle', 19 | unknown: 'Unknown Type', 20 | auth: 'Authorization ID', 21 | policy: 'Authorization Policy', 22 | btn: { 23 | name: 'Button Name', 24 | permission: 'Button/Permission' 25 | }, 26 | lang: 'Language Key', 27 | open: 'Opening Method', 28 | router: { hide: 'Hide Menu' }, 29 | redirect: 'Default Redirect', 30 | login: 'Requires Login', 31 | policies: { 32 | invisible: 'Invisible', 33 | visible: 'Visible', 34 | accessible: 'Accessible' 35 | }, 36 | icons: { 37 | wireframe: 'Wireframe', 38 | solid: 'Solid', 39 | directional: 'Directional', 40 | tips: 'Tooltip', 41 | edit: 'Editable', 42 | data: 'Data', 43 | brands: 'Brand', 44 | generic: 'Generic' 45 | }, 46 | badge: { 47 | label: 'Badge Settings', 48 | content: 'Text Content', 49 | color: 'Text Color', 50 | bg: 'Background', 51 | radius: 'Border Radius', 52 | setting: 'Configure', 53 | preview: 'Preview', 54 | none: 'No Preview', 55 | size: 'Font Size' 56 | }, 57 | tips: { 58 | title: 'Optional. Priority: 1) Multilingual title 2) Manual title 3) Defaults to Name', 59 | weight: 'Higher values appear first', 60 | badge: 'Text priority, icon fallback' 61 | }, 62 | placeholder: { 63 | name: 'Enter unique menu name (e.g. mi-dashboard)', 64 | title: 'Enter menu title (e.g. Dashboard)', 65 | subtitle: 'Enter menu subtitle (e.g. Overview)', 66 | path: 'Enter access path (e.g. /dashboard)', 67 | page: 'Enter component path (e.g. views/dashboard)', 68 | redirect: 'Enter default redirect URL', 69 | lang: 'Enter i18n key for title', 70 | search: 'Search menu by name', 71 | btn: 'Enter button/permission (e.g. create)', 72 | icon: 'Select icon', 73 | up: 'Select parent menu', 74 | auth: 'Enter unique auth ID (e.g. personal:data)', 75 | content: 'Enter display text', 76 | color: 'Click to select color' 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/components/modal/Teleport.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, ref, Teleport, watch, onMounted, onBeforeMount } from 'vue' 2 | import { TeleportProps } from './props' 3 | 4 | const windowIsUndefined = !( 5 | typeof window !== 'undefined' && 6 | window.document && 7 | window.document.createElement 8 | ) 9 | 10 | const MiModalTeleport = defineComponent({ 11 | name: 'MiModalTeleport', 12 | inheritAttrs: false, 13 | props: TeleportProps(), 14 | setup(props) { 15 | const teleportRef = ref(null) 16 | const openCount = ref(props.open ? 1 : 0) 17 | const _container = ref(null) 18 | 19 | const createContainer = () => { 20 | if (windowIsUndefined) return null 21 | const type = typeof props.container 22 | if (type === 'function') return props.container() 23 | if (type === 'string') { 24 | let temp = props.container 25 | const firstLetter = temp.charAt(0) 26 | if (!(firstLetter === '#' || firstLetter === '.')) temp = `#${temp}` 27 | return document.querySelector(temp) 28 | } 29 | if (type === 'object' && props.container instanceof window.HTMLElement) { 30 | return props.container 31 | } 32 | return document.body 33 | } 34 | 35 | const removeContainer = () => { 36 | _container.value = null 37 | } 38 | 39 | watch( 40 | () => props.open, 41 | (n) => (openCount.value = n ? openCount.value + 1 : openCount.value - 1) 42 | ) 43 | 44 | watch( 45 | () => props.container, 46 | (curr, prev) => { 47 | const containerIsFunc = typeof curr === 'function' && typeof prev === 'function' 48 | if (containerIsFunc ? curr.toString() !== prev.toString() : curr !== prev) 49 | removeContainer() 50 | } 51 | ) 52 | 53 | onBeforeMount(() => removeContainer()) 54 | onMounted(() => (_container.value = createContainer() as HTMLElement | null)) 55 | 56 | return () => { 57 | const childProps = { container: createContainer() } 58 | return (props.open || props.forceRender || teleportRef.value) && _container.value ? ( 59 | 60 | {props.children && props.children(childProps)} 61 | 62 | ) : null 63 | } 64 | } 65 | }) 66 | 67 | export default MiModalTeleport 68 | -------------------------------------------------------------------------------- /src/components/menu/Drawer.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent, computed, Fragment } from 'vue' 2 | import { Drawer } from 'ant-design-vue' 3 | import { DrawerMenuProps } from './props' 4 | import { $tools } from '../../utils/tools' 5 | import { useMenuStore } from '../../stores/menu' 6 | import { useLayoutStore } from '../../stores/layout' 7 | import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons-vue' 8 | import { useWindowResize } from '../../hooks/useWindowResize' 9 | import MiMenu from '../menu/Menu' 10 | import MiLayoutSiderLogo from '../layout/Logo' 11 | import applyTheme from '../_utils/theme' 12 | import styled from './style/drawer.module.less' 13 | 14 | const MiDrawerMenu = defineComponent({ 15 | name: 'MiDrawerMenu', 16 | inheritAttrs: false, 17 | props: DrawerMenuProps(), 18 | emits: ['update:open'], 19 | setup(props, { emit }) { 20 | const useMenu = useMenuStore() 21 | const useLayout = useLayoutStore() 22 | const open = computed(() => props.open) 23 | const menus = computed(() => useMenu.menus) 24 | const { width } = useWindowResize() 25 | const size = computed(() => $tools.distinguishSize(props.width, width.value)) 26 | 27 | applyTheme(styled) 28 | 29 | const handleOpen = () => { 30 | useMenu.$patch({ drawer: true }) 31 | useLayout.$patch({ collapsed: false }) 32 | emit('update:open', true) 33 | } 34 | 35 | const handleClose = () => { 36 | useMenu.$patch({ drawer: false }) 37 | emit('update:open', false) 38 | } 39 | 40 | return () => ( 41 | 42 | 43 | {open.value ? : } 44 | 45 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ) 62 | } 63 | }) 64 | 65 | export default MiDrawerMenu 66 | -------------------------------------------------------------------------------- /src/components/anchor/style/anchor.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | position: relative; 5 | 6 | .anchor { 7 | position: fixed; 8 | .properties(min-height, 120); 9 | max-height: calc(100vh - 24rem); 10 | border: .0625rem solid var(--mi-anchor-border); 11 | .transition(); 12 | .gradient(var(--mi-anchor-background-start), var(--mi-anchor-background-stop)); 13 | overflow-y: auto; 14 | overflow-x: hidden; 15 | .properties(top, 200); 16 | .properties(right, -4); 17 | .properties(padding, 16); 18 | .properties(max-width, 360); 19 | .radius(4); 20 | .scrollbar(); 21 | 22 | &-title { 23 | .flex(); 24 | .properties(margin-bottom); 25 | } 26 | 27 | &-icon { 28 | .properties(margin-right, 24); 29 | .font-size(18); 30 | cursor: pointer; 31 | .transition(); 32 | 33 | &:hover { 34 | color: var(--mi-primary); 35 | } 36 | 37 | &:last-child { 38 | margin-right: 0; 39 | } 40 | } 41 | 42 | &-inner { 43 | .properties(padding-right, 16); 44 | .properties(padding-left, 16); 45 | list-style: none; 46 | position: relative; 47 | } 48 | } 49 | 50 | .sticky { 51 | position: fixed; 52 | .properties(right, -4); 53 | .properties(top, 200); 54 | border: .0625rem solid var(--mi-anchor-border); 55 | .transition(); 56 | .gradient(var(--mi-anchor-background-start), var(--mi-anchor-background-stop)); 57 | cursor: pointer; 58 | .flex(center, center, column); 59 | .properties(padding-left, 12); 60 | .properties(padding-right, 14); 61 | .properties(padding-top, 12); 62 | .properties(padding-bottom, 12); 63 | .radius(4); 64 | 65 | &-text { 66 | .flex(center, center, column-reverse); 67 | .properties(margin-top, 4); 68 | 69 | > span { 70 | transform: rotate(-90deg); 71 | .properties(line-height, 18); 72 | } 73 | 74 | &-empty { 75 | .properties(height, 4); 76 | } 77 | } 78 | } 79 | } 80 | 81 | :export { 82 | --anchor-border: var(--mi-primary); 83 | --anchor-background-start: var(--mi-surface); 84 | --anchor-background-stop: var(--mi-surface-variant); 85 | --anchor-text: var(--mi-surface-variant); 86 | } -------------------------------------------------------------------------------- /src/utils/cookie.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { $g } from './global' 3 | 4 | class MiCookie { 5 | prefix: string = $g?.prefix || 'mi-' 6 | 7 | /** 8 | * 获取 cookie 名称 9 | * @param key 10 | * @param prefix 11 | * @returns 12 | */ 13 | getName(key: string, prefix?: string) { 14 | return `${prefix ?? this.prefix}${key}=` 15 | } 16 | 17 | /** 18 | * 获取 cookie 19 | * @param key 20 | * @param prefix 21 | * @returns 22 | */ 23 | get(key: string, prefix?: string): any { 24 | const name = this.getName(key, prefix) 25 | const values = document.cookie.split(';') 26 | const len = values.length 27 | for (let i = 0; i < len; i++) { 28 | let value = decodeURIComponent(values[i]) 29 | while (value.charAt(0) === ' ') value = value.substring(1) 30 | if (value.indexOf(name) !== -1) { 31 | return value.substring(name.length, value.length) 32 | } 33 | } 34 | return null 35 | } 36 | 37 | /** 38 | * 设置 cookie 39 | * @param name 40 | * @param value 41 | * @param expire 42 | * @param prefix 43 | */ 44 | set(name: string, value: any, expire?: number | null, prefix?: string): void { 45 | let expires: string | null = null 46 | if (expire) { 47 | const date = new Date() 48 | date.setTime(date.getTime() + expire * 24 * 60 * 60 * 1000) 49 | expires = `expires=${date.toUTCString()}` 50 | } 51 | const params = [ 52 | `${this.getName(name, prefix)}${encodeURIComponent(value)}`, 53 | expires, 54 | 'path=/' 55 | ] 56 | document.cookie = params.join(';') 57 | } 58 | 59 | /** 60 | * 删除 cookie 61 | * @param names 62 | * @param prefix 63 | */ 64 | del(names: string | any[], prefix?: string): void { 65 | if (Array.isArray(names)) { 66 | const len = names.length 67 | for (let i = 0; i < len; i++) { 68 | const name = names[i] 69 | if (name) this.set(name, '', -1, prefix) 70 | } 71 | } else { 72 | this.set(names, '', -1, prefix) 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * Cookie 缓存 ( `document.cookie` ) 79 | * 80 | * e.g. 81 | * ``` 82 | * this.$cookie.get('key') 83 | * this.$cookie.set('key', 'value') 84 | * ``` 85 | */ 86 | export const $cookie: MiCookie = new MiCookie() 87 | export default { 88 | install(app: App) { 89 | app.config.globalProperties.$cookie = $cookie 90 | return app 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/components/layout/style/header.module.less: -------------------------------------------------------------------------------- 1 | @import '../../theme/style/variables.module.less'; 2 | 3 | .container { 4 | .transition(); 5 | .flex(flex-start); 6 | .properties(height, @mi-header-height); 7 | position: absolute; 8 | top: 0; 9 | left: 0; 10 | width: 100%; 11 | z-index: @mi-zindex + 1000; 12 | } 13 | 14 | .has { 15 | &-historical { 16 | &-routes { 17 | .properties(height, @mi-header-height + @mi-historical-height); 18 | } 19 | } 20 | } 21 | 22 | .inner { 23 | width: 100%; 24 | .properties(height, @mi-header-height); 25 | position: relative; 26 | .flex(); 27 | } 28 | 29 | .trigger { 30 | .flex(); 31 | } 32 | 33 | .left, 34 | .right { 35 | position: absolute; 36 | top: 0; 37 | width: auto; 38 | z-index: 10; 39 | } 40 | 41 | .left { 42 | .flex(); 43 | left: 0; 44 | 45 | @media only screen and (max-width: @mi-sm) { 46 | .properties(left, 16); 47 | top: 50%; 48 | transform: translateY(-50%); 49 | } 50 | } 51 | 52 | .right { 53 | .properties(right, 16); 54 | top: 50%; 55 | transform: translateY(-50%); 56 | .flex(); 57 | } 58 | 59 | .trigger { 60 | .transition(); 61 | position: relative; 62 | cursor: pointer; 63 | 64 | .anticon { 65 | .properties(width, 32); 66 | .properties(height, 32); 67 | .flex(); 68 | .circle(); 69 | } 70 | } 71 | 72 | .palette, 73 | .screenfull, 74 | .drawer { 75 | .properties(width, 32); 76 | .properties(height, 32); 77 | .flex(); 78 | .properties(margin-left, 16); 79 | 80 | :global(.anticon) { 81 | .font-size(20); 82 | cursor: pointer; 83 | } 84 | } 85 | 86 | .user { 87 | .flex(); 88 | .properties(margin-left, 16); 89 | cursor: pointer; 90 | } 91 | 92 | .drawer { 93 | margin-left: 0; 94 | .flex(); 95 | } 96 | 97 | .coffee { 98 | .qrcode { 99 | .flex(); 100 | 101 | @media only screen and (max-width: @mi-lg) { 102 | flex-direction: column; 103 | } 104 | 105 | img { 106 | .properties(width, 372); 107 | .properties(height, 505); 108 | max-width: 100%; 109 | .properties(margin-top, 16); 110 | object-fit: fill; 111 | .radius(); 112 | overflow: hidden; 113 | 114 | &:last-child { 115 | .properties(margin-left, 16); 116 | 117 | @media only screen and (max-width: @mi-lg) { 118 | margin-left: 0; 119 | } 120 | } 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /src/components/title/__tests__/title.test.ts: -------------------------------------------------------------------------------- 1 | import { mount, type VueWrapper } from '@vue/test-utils' 2 | import { afterEach, describe, expect, test, vi } from 'vitest' 3 | import { h } from 'vue' 4 | 5 | import MiTitle from '../Title' 6 | import styled from '../style/title.module.less' 7 | 8 | describe('MiTitle', () => { 9 | const wrappers: VueWrapper[] = [] 10 | 11 | afterEach(() => { 12 | wrappers.splice(0).forEach((w) => w.unmount()) 13 | vi.restoreAllMocks() 14 | document.body.innerHTML = '' 15 | }) 16 | 17 | test('渲染必需的标题文本', () => { 18 | const wrapper = mount(MiTitle, { 19 | props: { 20 | title: '主标题' 21 | }, 22 | attachTo: document.body 23 | }) 24 | wrappers.push(wrapper) 25 | 26 | const h2 = wrapper.find('h2') 27 | expect(h2.exists()).toBe(true) 28 | expect(h2.attributes('class')).toContain(styled.content) 29 | expect(h2.html()).toContain('主标题') 30 | }) 31 | 32 | test('center=true 时容器添加居中 class', () => { 33 | const wrapper = mount(MiTitle, { 34 | props: { 35 | title: '居中标题', 36 | center: true 37 | }, 38 | attachTo: document.body 39 | }) 40 | wrappers.push(wrapper) 41 | 42 | const inner = wrapper.find(`.${styled.inner}`) 43 | expect(inner.exists()).toBe(true) 44 | expect(inner.classes()).toContain(styled.center) 45 | }) 46 | 47 | test('color 和 margin 配置会透传到 style', () => { 48 | const wrapper = mount(MiTitle, { 49 | props: { 50 | title: '带样式标题', 51 | color: '#ff0000', 52 | margin: { top: 10, bottom: 20 } 53 | }, 54 | attachTo: document.body 55 | }) 56 | wrappers.push(wrapper) 57 | 58 | const h2 = wrapper.find('h2') 59 | expect(h2.attributes('style')).toContain('color: rgb(255, 0, 0)') 60 | expect(h2.attributes('style')).toContain('margin-top') 61 | expect(h2.attributes('style')).toContain('margin-bottom') 62 | }) 63 | 64 | test('额外内容通过 extra 插槽渲染在 extra 区域', () => { 65 | const wrapper = mount(MiTitle, { 66 | props: { 67 | title: '有 extra' 68 | }, 69 | slots: { 70 | extra: () => h('button', { class: 'extra-btn' }, '操作') 71 | }, 72 | attachTo: document.body 73 | }) 74 | wrappers.push(wrapper) 75 | 76 | const extra = wrapper.find(`.${styled.extra}`) 77 | expect(extra.exists()).toBe(true) 78 | const btn = extra.find('button.extra-btn') 79 | expect(btn.exists()).toBe(true) 80 | expect(btn.text()).toBe('操作') 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /src/components/breadcrumb/Breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import { Transition, defineComponent, createVNode, watch } from 'vue' 2 | import { BreadcrumbProps } from './props' 3 | import { getPrefixCls } from '../_utils/props' 4 | import { useRoute, type RouteRecordNormalized } from 'vue-router' 5 | import { useBreadcrumbsStore } from '../../stores/breadcrumbs' 6 | import { HomeOutlined } from '@ant-design/icons-vue' 7 | import MiLink from '../link/Link' 8 | import applyTheme from '../_utils/theme' 9 | import styled from './style/breadcrumb.module.less' 10 | 11 | const MiBreadcrumb = defineComponent({ 12 | name: 'MiBreadcrumb', 13 | inheritAttrs: false, 14 | props: BreadcrumbProps(), 15 | setup(props) { 16 | const route = useRoute() 17 | const breadcrumbsStore = useBreadcrumbsStore() 18 | applyTheme(styled) 19 | 20 | const getBreadcrumbs = () => { 21 | const breadcrumbs: any[] = route.matched.map((match, idx) => 22 | resolveMatchedRoute(match, idx) 23 | ) 24 | breadcrumbsStore.$patch({ breadcrumbs }) 25 | } 26 | 27 | const resolveMatchedRoute = (match: RouteRecordNormalized, idx: number): any => { 28 | const title = match.meta?.title ?? match.name 29 | let path = match.redirect ?? match.path ?? '/' 30 | if (typeof path === 'string' && !path.startsWith('/')) path = '/' + path 31 | return { 32 | title, 33 | icon: idx === 0 ? createVNode(HomeOutlined) : undefined, 34 | path: idx === route.matched.length - 1 ? undefined : path 35 | } 36 | } 37 | 38 | const renderBreadcrumbItems = (): any[] => 39 | breadcrumbsStore.breadcrumbs.map((breadcrumb: any) => ( 40 | 41 | 42 | 43 | {breadcrumb.icon ?? null} 44 | {breadcrumb.title} 45 | 46 | 47 | {props.separator} 48 | 49 | )) 50 | 51 | watch( 52 | () => route.fullPath, 53 | () => getBreadcrumbs(), 54 | { immediate: true, deep: true } 55 | ) 56 | 57 | return () => ( 58 | 59 | 60 | {...renderBreadcrumbItems()} 61 | 62 | 63 | ) 64 | } 65 | }) 66 | 67 | export default MiBreadcrumb 68 | -------------------------------------------------------------------------------- /src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { $g } from './global' 3 | 4 | class MiStorage { 5 | prefix: string = $g?.prefix || 'mi-' 6 | instance!: Storage 7 | 8 | constructor(type?: string) { 9 | this.change(type) 10 | } 11 | 12 | getKey(key: string, prefix?: string): string { 13 | return `${prefix ?? this.prefix}${key}` 14 | } 15 | 16 | /** 17 | * get 18 | * @param keys 19 | * @param prefix 20 | * @returns 21 | */ 22 | get(keys: string | string[], prefix?: string): any { 23 | let data: { [index: string]: any } | null = {} 24 | if (Array.isArray(keys)) { 25 | for (let i = 0, len = keys.length; i < len; i++) { 26 | const key = keys[i] 27 | data[key] = JSON.parse( 28 | this.instance.getItem(this.getKey(key, prefix)) as string 29 | ) as any 30 | } 31 | } else { 32 | data = null 33 | if (keys) { 34 | data = JSON.parse(this.instance.getItem(this.getKey(keys, prefix)) as string) as any 35 | } 36 | } 37 | return data 38 | } 39 | 40 | /** 41 | * set 42 | * @param key 43 | * @param value 44 | * @param prefix 45 | */ 46 | set(key: string, value: any, prefix?: string): void { 47 | this.instance.setItem(this.getKey(key, prefix), JSON.stringify(value)) 48 | } 49 | 50 | /** 51 | * delete 52 | * @param keys 53 | * @param prefix 54 | */ 55 | del(keys: string | string[], prefix?: string) { 56 | if (Array.isArray(keys)) { 57 | for (let i = 0, len = keys.length; i < len; i++) { 58 | const key = keys[i] 59 | this.instance.removeItem(this.getKey(key, prefix)) 60 | } 61 | } else this.instance.removeItem(this.getKey(keys, prefix)) 62 | } 63 | 64 | /** 65 | * change ( default: localStorage ) 66 | * @param type 67 | * @returns 68 | */ 69 | change(type?: string): MiStorage { 70 | if (typeof window !== 'undefined') { 71 | this.instance = 72 | type === 'local' ? localStorage : type === 'session' ? sessionStorage : localStorage 73 | } 74 | return this 75 | } 76 | } 77 | 78 | /** 79 | * 本地存储 ( `localStorage & sessionStorage` ) 80 | * - default: `localStorage` 81 | * 82 | * e.g. 83 | * ``` 84 | * this.$storage.get('name') 85 | * this.$storage.set('name', 'value') 86 | * this.$storage.change('session').set('name', 'value') 87 | * ``` 88 | */ 89 | export const $storage: MiStorage = new MiStorage() 90 | export default { 91 | install(app: App) { 92 | app.config.globalProperties.$storage = $storage 93 | return app 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/components/items/detail/__tests__/detail.test.ts: -------------------------------------------------------------------------------- 1 | import { mount, type VueWrapper } from '@vue/test-utils' 2 | import { afterEach, describe, expect, test, vi } from 'vitest' 3 | import { h, nextTick } from 'vue' 4 | import MiItemsDetail from '../Detail' 5 | import styled from '../style/detail.module.less' 6 | import { $tools } from '../../../../utils/tools' 7 | 8 | describe('MiItemsDetail', () => { 9 | const wrappers: VueWrapper[] = [] 10 | 11 | afterEach(() => { 12 | wrappers.splice(0).forEach((w) => w.unmount()) 13 | vi.restoreAllMocks() 14 | document.body.innerHTML = '' 15 | }) 16 | 17 | test('subtitle slot 会正确渲染(修复 slot 名称错误)', async () => { 18 | vi.spyOn($tools, 'getParents').mockReturnValue(document.createElement('div') as any) 19 | const wrapper = mount(MiItemsDetail, { 20 | props: { 21 | data: [ 22 | { title: 't1', subtitle: 's1', thumb: 'x.png' }, 23 | { title: 't2', subtitle: 's2', thumb: 'y.png' } 24 | ], 25 | scrollToPosition: false 26 | }, 27 | slots: { 28 | subtitle: ({ subtitle }: any) => 29 | h('div', { 'data-testid': 'subtitle-slot' }, subtitle?.text || '') 30 | }, 31 | global: { 32 | stubs: { 33 | MiImage: { name: 'MiImage', props: ['src'], template: '' } 34 | } 35 | }, 36 | attachTo: document.body 37 | }) 38 | wrappers.push(wrapper) 39 | 40 | await nextTick() 41 | expect(wrapper.findAll('[data-testid="subtitle-slot"]').length).toBe(2) 42 | expect(wrapper.text()).toContain('s1') 43 | expect(wrapper.text()).toContain('s2') 44 | }) 45 | 46 | test('scrollToPosition=false 时点击不会调用 back2pos', async () => { 47 | const getParentsSpy = vi 48 | .spyOn($tools, 'getParents') 49 | .mockReturnValue(document.createElement('div') as any) 50 | const back2posSpy = vi.spyOn($tools, 'back2pos') 51 | 52 | const wrapper = mount(MiItemsDetail, { 53 | props: { 54 | data: [{ title: 't1', subtitle: 's1', thumb: 'x.png' }], 55 | scrollToPosition: false 56 | }, 57 | global: { 58 | stubs: { 59 | MiImage: { name: 'MiImage', props: ['src'], template: '' } 60 | } 61 | }, 62 | attachTo: document.body 63 | }) 64 | wrappers.push(wrapper) 65 | 66 | await nextTick() 67 | await wrapper.find(`.${styled.item}`).trigger('click') 68 | await nextTick() 69 | 70 | expect(getParentsSpy).toHaveBeenCalled() 71 | expect(back2posSpy).not.toHaveBeenCalled() 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "extends": [ 9 | "plugin:vue/vue3-recommended", 10 | "plugin:import/recommended", 11 | "plugin:import/typescript", 12 | "@vue/prettier" 13 | ], 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": "latest", 17 | "parser": "@babel/eslint-parser" 18 | }, 19 | "plugins": ["markdown", "jest", "@typescript-eslint", "import"], 20 | "overrides": [ 21 | { 22 | "files": ["*.md", "*.md/*.vue", "*.md/*.js", "*.md/*.ts", "*.md/*.base"], 23 | "processor": "markdown/markdown", 24 | "rules": { 25 | "no-console": "off", 26 | "import/no-unresolved": "off" 27 | } 28 | }, 29 | { 30 | "files": ["*.ts", "*.tsx"], 31 | "parserOptions": { "project": ["./tsconfig.json", "./tsconfig.node.json", "./rollup.config.ts"] }, 32 | "rules": { 33 | "semi": "off", 34 | "quotes": ["error", "single", { 35 | "allowTemplateLiterals": true, 36 | "avoidEscape": true 37 | }], 38 | "@typescript-eslint/ban-types": 0, 39 | "@typescript-eslint/no-var-requires": "off", 40 | "@typescript-eslint/no-explicit-any": "off", 41 | "@typescript-eslint/explicit-module-boundary-types": "off", 42 | "@typescript-eslint/no-empty-function": "off", 43 | "@typescript-eslint/class-name-casing": "off", 44 | "vue/html-closing-bracket-newline": ["error", { 45 | "singleline": "never", 46 | "multiline": "never" 47 | }], 48 | "@typescript-eslint/ban-ts-comment": 0 49 | } 50 | }, 51 | { 52 | "files": ["*.vue"], 53 | "parser": "vue-eslint-parser", 54 | "parserOptions": { 55 | "parser": "@typescript-eslint/parser", 56 | "ecmaVersion": 2021 57 | }, 58 | "rules": { 59 | "no-console": "off", 60 | "semi": "off", 61 | "vue/no-reserved-component-names": "off" 62 | } 63 | } 64 | ], 65 | "rules": { 66 | "no-console": "off", 67 | "no-debugger": "off", 68 | "no-var": "error", 69 | "semi": "off", 70 | "camelcase": "off", 71 | "import/namespace": [2, { "allowComputed": true }], 72 | "vue/no-v-html": "off", 73 | "no-extra-boolean-cast": "off", 74 | "import/no-named-as-default": "off", 75 | "prettier/prettier": ["error", { 76 | "endOfLine": "auto" 77 | }] 78 | } 79 | } --------------------------------------------------------------------------------
4 | 「 5 | Makeit Admin Pro 6 | 」致力于提供给程序员愉悦的开发体验。 7 |