├── .eslintignore ├── src ├── layouts │ ├── NotFound.vue │ ├── Empty.vue │ └── Home.vue ├── settings.json ├── components │ ├── MdEditor │ │ ├── style.css │ │ └── index.tsx │ ├── DataTable │ │ ├── components │ │ │ ├── index.ts │ │ │ └── ColumnSetting.vue │ │ └── info.ts │ ├── Layout │ │ ├── Header │ │ │ ├── Tips.vue │ │ │ ├── Network.vue │ │ │ ├── Translate.vue │ │ │ ├── Fullscreen.vue │ │ │ ├── Message.vue │ │ │ ├── Dark.vue │ │ │ ├── Tabs.vue │ │ │ └── Window.vue │ │ ├── LoadingContent.vue │ │ ├── Loading.tsx │ │ ├── MultiWindow │ │ │ ├── index.ts │ │ │ ├── index.vue │ │ │ └── setupMultiWindow.ts │ │ ├── Provider.vue │ │ └── Header.vue │ ├── Drawer │ │ ├── utils.ts │ │ └── index.vue │ ├── DataForm │ │ ├── values.ts │ │ └── utils.ts │ ├── VChart │ │ ├── example │ │ │ ├── line-data.ts │ │ │ ├── bar-data.ts │ │ │ ├── pie-data.ts │ │ │ └── pie-rose-data.ts │ │ └── index.tsx │ ├── CardCols.vue │ ├── CardRows.vue │ ├── Card.tsx │ └── DrawerForm │ │ └── index.vue ├── utils │ ├── is.ts │ ├── render.ts │ ├── lock.ts │ └── utils.ts ├── composables │ ├── useDataForm.ts │ ├── useDataTable.ts │ ├── useDarks.ts │ ├── useLanguage.ts │ ├── useWindowFullscreen.ts │ ├── usePrefixStorage.ts │ └── useHello.ts ├── stores │ ├── multiWindowStore.ts │ ├── settingStore.ts │ ├── stateStore.ts │ ├── themeStore.ts │ └── userStore.ts ├── menu.ts ├── apis │ ├── sh.ts │ ├── test.ts │ ├── system.ts │ ├── base.ts │ └── index.ts ├── modules │ ├── pinia.ts │ ├── other.ts │ ├── handler.ts │ ├── index.ts │ ├── i18n.ts │ ├── directives.ts │ └── router.ts ├── main.ts ├── pages │ ├── example │ │ ├── function │ │ │ ├── permission │ │ │ │ ├── user.vue │ │ │ │ ├── admin.vue │ │ │ │ └── index.vue │ │ │ ├── not-keep.vue │ │ │ ├── fullscreen.vue │ │ │ ├── markdown.vue │ │ │ ├── locale.vue │ │ │ ├── clipboard.vue │ │ │ ├── echart │ │ │ │ └── index.vue │ │ │ └── notice.vue │ │ ├── layout │ │ │ ├── basic.vue │ │ │ ├── table-form │ │ │ │ ├── form.ts │ │ │ │ ├── [id].vue │ │ │ │ ├── f1.vue │ │ │ │ ├── f3.vue │ │ │ │ ├── f2.vue │ │ │ │ └── table.ts │ │ │ ├── table.vue │ │ │ ├── form-drawer.vue │ │ │ ├── multi-column.vue │ │ │ ├── form-basic.vue │ │ │ └── form-advanced.vue │ │ ├── readme.vue │ │ └── menu.ts │ ├── inlay │ │ ├── loading.vue │ │ ├── detail.vue │ │ └── message.vue │ ├── system │ │ ├── menu.ts │ │ ├── site.vue │ │ ├── menu.vue │ │ └── role.vue │ └── [...notFound].vue ├── styles │ ├── base.css │ └── main.css ├── mockProdServer.ts ├── types │ ├── env.d.ts │ ├── global.d.ts │ └── components.d.ts └── App.vue ├── .devcontainer.json ├── .husky └── pre-commit ├── mock ├── utils.ts ├── user.ts └── base.ts ├── public ├── logo.png ├── favicon.ico └── browser.js ├── .npmrc ├── scripts ├── template │ ├── layout.hbs │ ├── composable.hbs │ ├── module.hbs │ ├── api.hbs │ ├── component.hbs │ ├── store.hbs │ └── page.hbs ├── shared │ └── base.js ├── create.js └── remove.js ├── .gitignore ├── presets ├── css │ ├── index.ts │ └── postcss.ts ├── plugins │ ├── removelog.ts │ ├── build.ts │ ├── eslint.ts │ ├── dev.ts │ ├── mock.ts │ ├── legacy.ts │ ├── markdown.ts │ ├── pwa.ts │ ├── html.ts │ └── h5.ts ├── server.ts ├── optimize.ts ├── shared │ ├── resolvers.ts │ ├── mock.ts │ └── env.ts └── build.ts ├── locales ├── zh.yml └── en.yml ├── .editorConfig ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── template.code-workspace ├── .eslintrc ├── .env ├── unocss.config.ts ├── tsconfig.json ├── LICENSE ├── README.md ├── vite.config.ts └── package.json /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | src/types 4 | -------------------------------------------------------------------------------- /src/layouts/NotFound.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.devcontainer.json: -------------------------------------------------------------------------------- 1 | {"image":"mcr.microsoft.com/devcontainers/javascript-node","build":{}} -------------------------------------------------------------------------------- /src/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "layout": {}, 3 | "page": {}, 4 | "menu": {} 5 | } 6 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /mock/utils.ts: -------------------------------------------------------------------------------- 1 | export const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) 2 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sohaha/naiveui-admin-template/HEAD/public/logo.png -------------------------------------------------------------------------------- /src/components/MdEditor/style.css: -------------------------------------------------------------------------------- 1 | .md-toolbar-wrapper .md-toolbar{ 2 | min-width:auto; 3 | } -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=@vue* 2 | auto-install-peers=true 3 | strict-peer-dependencies=false 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sohaha/naiveui-admin-template/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | export function isURL(path: string) { 2 | return /^(http(s)?:\/\/)/.test(path) 3 | } 4 | -------------------------------------------------------------------------------- /scripts/template/layout.hbs: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /scripts/template/composable.hbs: -------------------------------------------------------------------------------- 1 | import { ref } from "vue" 2 | 3 | export default {{name}} = () => { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /scripts/template/module.hbs: -------------------------------------------------------------------------------- 1 | import type { App } from "vue" 2 | 3 | 4 | export default (app: App) => { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/layouts/Empty.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /src/composables/useDataForm.ts: -------------------------------------------------------------------------------- 1 | import { useDataForm } from '@/components/DataForm/utils' 2 | 3 | export default useDataForm 4 | -------------------------------------------------------------------------------- /src/composables/useDataTable.ts: -------------------------------------------------------------------------------- 1 | import { useDataTable } from '@/components/DataTable/utils' 2 | 3 | export default useDataTable 4 | -------------------------------------------------------------------------------- /src/components/DataTable/components/index.ts: -------------------------------------------------------------------------------- 1 | import ColumnSetting from './ColumnSetting.vue' 2 | 3 | export default ColumnSetting 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .eslintcache 4 | .history/ 5 | .idea/ 6 | .env.*.local 7 | .DS_Store 8 | .pnpm-store/ 9 | stats.html 10 | -------------------------------------------------------------------------------- /presets/css/index.ts: -------------------------------------------------------------------------------- 1 | import { PostcssConfig } from './postcss' 2 | 3 | export default () => { 4 | return { 5 | postcss: PostcssConfig(), 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /scripts/template/api.hbs: -------------------------------------------------------------------------------- 1 | import type { InstApi } from '@/types/global' 2 | 3 | export function api{{pascalCase name}}(): Promise { 4 | return api.get('/xxx', { }) 5 | } 6 | -------------------------------------------------------------------------------- /scripts/template/component.hbs: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /locales/zh.yml: -------------------------------------------------------------------------------- 1 | button: 2 | about: 关于 3 | back: 返回 4 | loading_page: 页面加载中 5 | not_found: 未找到页面 6 | operation_tip: "操作:" 7 | page_home: 首页 8 | page_login: 登录 9 | page_table: 表格 10 | -------------------------------------------------------------------------------- /scripts/template/store.hbs: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | export default defineStore('{{name}}', { 4 | state() { 5 | return {} 6 | }, 7 | getters: {}, 8 | actions: {} 9 | }) 10 | -------------------------------------------------------------------------------- /locales/en.yml: -------------------------------------------------------------------------------- 1 | button: 2 | about: About 3 | back: Back 4 | loading_page: loading 5 | not_found: Notfound 6 | operation_tip: "Action:" 7 | page_home: Home 8 | page_login: Login 9 | page_table: Table 10 | -------------------------------------------------------------------------------- /src/stores/multiWindowStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import useStore from '@/components/Layout/MultiWindow/store' 3 | 4 | export const multiWindowStore = defineStore('multiWindowStore', useStore) 5 | -------------------------------------------------------------------------------- /src/composables/useDarks.ts: -------------------------------------------------------------------------------- 1 | const isDark = useDark() 2 | const toggle = useToggle(isDark) 3 | 4 | const toggleDark = () => { 5 | toggle() 6 | } 7 | 8 | export default () => { 9 | return { isDark, toggleDark } 10 | } 11 | -------------------------------------------------------------------------------- /src/menu.ts: -------------------------------------------------------------------------------- 1 | import type { StMenu } from './types/global' 2 | // import system from '@/pages/system/menu' 3 | 4 | export default [ 5 | { 6 | icon: 'i-bx:home', 7 | path: '/', 8 | }, 9 | // ...system, 10 | ] 11 | -------------------------------------------------------------------------------- /src/apis/sh.ts: -------------------------------------------------------------------------------- 1 | import type { InstApi } from '@/types/global' 2 | 3 | export function yclh(params: { 4 | page: number 5 | pagesize: number 6 | }): Promise { 7 | return apis.get('/0xsys/yc60lh', { 8 | params, 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /presets/plugins/removelog.ts: -------------------------------------------------------------------------------- 1 | import Removelog from 'vite-plugin-removelog' 2 | import { env } from '../shared/env' 3 | 4 | export function RemovelogPlugin() { 5 | return (env.IS_PROD && !env.VITE_APP_MOCK_IN_PRODUCTION) 6 | ? Removelog() 7 | : '' 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/pinia.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import persistedstate from 'pinia-plugin-persistedstate' 3 | 4 | export default (app: App) => { 5 | const pinia = createPinia() 6 | 7 | pinia.use(persistedstate) 8 | 9 | app.use(pinia) 10 | } 11 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import 'uno.css' 2 | import './styles/main.css' 3 | import * as Vue from 'vue' 4 | import App from './App.vue' 5 | import modules from './modules' 6 | 7 | window.Vue = Vue 8 | 9 | const app = createApp(App) 10 | modules(app) 11 | app.mount('#app') 12 | -------------------------------------------------------------------------------- /presets/plugins/build.ts: -------------------------------------------------------------------------------- 1 | import { buildPlugin } from 'vite-plugin-build' 2 | 3 | // https://github.com/samonxian/vite-plugin-build/blob/master/README.zh-CN.md 4 | export function BuildPlugin() { 5 | return buildPlugin({ 6 | // fileBuild: { emitDeclaration: true } 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /.editorConfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | insert_final_newline = false 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /presets/plugins/eslint.ts: -------------------------------------------------------------------------------- 1 | import eslintPlugin from 'vite-plugin-eslint' 2 | 3 | export function ESLintPlugin() { 4 | return eslintPlugin({ 5 | fix: true, 6 | cache: true, 7 | failOnError: false, 8 | include: ['src/**/*.js', 'src/**/*.vue', 'src/**/*.ts'], 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /src/components/DataTable/info.ts: -------------------------------------------------------------------------------- 1 | export function useInfoDrawer() { 2 | const infoID = ref(0) 3 | const show = ref(false) 4 | 5 | return { 6 | open(id: number | string) { 7 | infoID.value = id 8 | show.value = true 9 | }, 10 | id: infoID, 11 | show, 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /scripts/template/page.hbs: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 12 | 13 | 14 | { 15 | "meta": { 16 | "title": "{{pascalCase name}}" 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /src/modules/other.ts: -------------------------------------------------------------------------------- 1 | import ayyui from 'ayyui' 2 | import 'ayyui/dist/style.css' 3 | import type { App } from 'vue' 4 | import { Level } from '@sohaha/zlog' 5 | 6 | export default (app: App) => { 7 | if (import.meta.env.DEV) 8 | log.level = Level.Trace 9 | else 10 | log.level = Level.Warn 11 | 12 | app.use(ayyui as any) 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/example/function/permission/user.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 13 | 14 | 15 | { 16 | "meta": { 17 | "permission": ["user"], 18 | "title": "user权限" 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/pages/example/function/permission/admin.vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 13 | 14 | 15 | { 16 | "meta": { 17 | "permission": ["admin"], 18 | "title": "admin权限" 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/modules/handler.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | 3 | export default (_: App) => { 4 | // app.config.errorHandler = (err, vm, info) => { 5 | // console.error('errorHandler', err, vm, info) 6 | // } 7 | // app.config.warnHandler = (err, vm, info) => { 8 | // console.error('warnHandler', err, vm, info) 9 | // } 10 | // app.config.performance = true 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/inlay/loading.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 13 | 14 | 15 | { 16 | "meta": { 17 | "multiWindow": false, 18 | "title": "Loading" 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/styles/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | overflow: hidden; 3 | background-color: var(--a-bg-color); 4 | } 5 | 6 | .tip-text{ 7 | color: hsla(var(--a-base-color),var(--a-text-emphasis-light-opacity)); 8 | } 9 | 10 | i[class^='mi-'] { 11 | background-size: 100% 100% !important; 12 | } 13 | 14 | .n-icon { 15 | will-change: transform; 16 | } 17 | 18 | vite-error-overlay { 19 | z-index: 999999; 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import directives from './directives' 3 | import handler from './handler' 4 | import i18n from './i18n' 5 | import pinia from './pinia' 6 | import router from './router' 7 | import other from './other' 8 | 9 | export default (app: App) => { 10 | pinia(app) 11 | router(app) 12 | handler(app) 13 | directives(app) 14 | i18n(app) 15 | other(app) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Layout/Header/Tips.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "antfu.iconify", 4 | "mikestead.dotenv", 5 | "redhat.vscode-yaml", 6 | "lokalise.i18n-ally", 7 | "vue.volar", 8 | "steoates.autoimport", 9 | "esbenp.prettier-vscode", 10 | "dbaeumer.vscode-eslint", 11 | "editorconfig.editorconfig", 12 | "usernamehw.errorlens", 13 | "antfu.goto-alias", 14 | "Vue.vscode-typescript-vue-plugin" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /presets/plugins/dev.ts: -------------------------------------------------------------------------------- 1 | import { env } from '../shared/env' 2 | import Inspect from 'vite-plugin-inspect' 3 | import VueDevTools from "vite-plugin-vue-devtools"; 4 | 5 | export function DevPlugins() { 6 | return [ 7 | Inspect({ 8 | dev: env.VITE_DEV_INSPECT, 9 | enabled: !env.IS_PROD && env.VITE_DEV_INSPECT, 10 | }), 11 | // https://github.com/webfansplz/vite-plugin-vue-devtools 12 | !env.IS_PROD && VueDevTools(), 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /template.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".", 5 | "name": "Root" 6 | }, 7 | { 8 | "name": "Apis", 9 | "path": "src/apis" 10 | }, 11 | { 12 | "name": "Pages", 13 | "path": "src/pages" 14 | }, 15 | { 16 | "name": "Stores", 17 | "path": "src/stores" 18 | } 19 | ], 20 | "settings": { 21 | "i18n-ally.localesPaths": [ 22 | "locales" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/apis/test.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import type { InstApi } from '@/types/global' 3 | 4 | export function mockLists(params: { 5 | page: number 6 | pagesize: number 7 | }): Promise { 8 | return apis.get('/_mock/lists', { 9 | params, 10 | baseURL: '/', 11 | }) 12 | } 13 | 14 | export function getWeather() { 15 | return axios.get('https://v0.yiketianqi.com/api?unescape=1&version=v61&appid=23035354&appsecret=8YvlPNrz') 16 | } 17 | -------------------------------------------------------------------------------- /src/mockProdServer.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer' 3 | 4 | const mocks: any[] = [] 5 | const mockContext = import.meta.glob('../mock/*.ts', { eager: true }) 6 | 7 | Object.keys(mockContext).forEach((v) => { 8 | const items = mockContext[v] as any 9 | if (items?.default) 10 | mocks.push(...items.default) 11 | }) 12 | 13 | export function setupProdMockServer() { 14 | createProdMockServer(mocks) 15 | } 16 | -------------------------------------------------------------------------------- /src/components/Layout/LoadingContent.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/composables/useLanguage.ts: -------------------------------------------------------------------------------- 1 | import { locale } from '@/modules/i18n' 2 | 3 | export const language = computed(() => { 4 | return locale.value === 'zh' ? '中文' : 'English' 5 | }) 6 | 7 | export const toggleLocale = (lang?: string) => { 8 | if (lang) { 9 | locale.value = lang 10 | return 11 | } 12 | locale.value = locale.value === 'zh' ? 'en' : 'zh' 13 | } 14 | 15 | export default () => { 16 | const { t } = useI18n() 17 | 18 | return { t, locale, toggleLocale, language } 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/example/function/not-keep.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | 16 | 17 | meta: 18 | multiWindow: false 19 | fullscreen: true 20 | i18n: 21 | en: Disposable 22 | zh: 单次页面 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/Drawer/utils.ts: -------------------------------------------------------------------------------- 1 | import type { PropType } from 'vue' 2 | 3 | export const props = { 4 | show: Boolean, 5 | loading: Boolean, 6 | closable: Boolean, 7 | title: { 8 | type: String, 9 | default: '', 10 | }, 11 | placement: { 12 | type: String as PropType<'right' | 'left' | 'top' | 'bottom' | 'center' | ''>, 13 | default: '', 14 | }, 15 | width: { 16 | type: String, 17 | default: '50vw', 18 | }, 19 | height: { 20 | type: String, 21 | default: '60vh', 22 | }, 23 | } 24 | -------------------------------------------------------------------------------- /src/composables/useWindowFullscreen.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeElementRef, UseFullscreenOptions } from '@vueuse/core' 2 | import { useFullscreen } from '@vueuse/core' 3 | 4 | export default (target?: MaybeElementRef | string | boolean, options?: UseFullscreenOptions) => { 5 | if (target && (typeof target === 'boolean' || typeof target === 'string')) { 6 | target = document.querySelector('#main-window') as HTMLElement 7 | if (!target) 8 | target = document.body 9 | } 10 | 11 | return useFullscreen(target as any, options) 12 | } 13 | -------------------------------------------------------------------------------- /presets/server.ts: -------------------------------------------------------------------------------- 1 | import { env } from './shared/env' 2 | 3 | export function ServerConfig() { 4 | // 开发服务器选项 https://cn.vitejs.dev/config/#server-options 5 | return { 6 | open: false, 7 | port: env.VITE_DEV_SERVE_PORT || 4000, 8 | overlay: true, 9 | proxy: { 10 | '/proxy': { 11 | target: env.VITE_APP_API_BASEURL, 12 | changeOrigin: env.VITE_DEV_PROXY, 13 | rewrite: (path: string) => path.replace(/\/proxy/, ''), 14 | }, 15 | }, 16 | fs: { 17 | strict: true, 18 | }, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scripts/shared/base.js: -------------------------------------------------------------------------------- 1 | const showExt = (type) => { 2 | const isTs = type === 'api' || type === 'store' || type === 'module' 3 | const ext = isTs ? 'ts' : 'vue' 4 | return ext 5 | } 6 | 7 | const moduleTypes = [ 8 | 'api', 9 | 'page', 10 | 'store', 11 | 'layout', 12 | 'module', 13 | 'component', 14 | 'composable', 15 | ] 16 | 17 | const showDir = (type) => { 18 | if (type === 'api') 19 | return 'apis' 20 | 21 | return `${type}s` 22 | } 23 | 24 | module.exports = { 25 | showExt, 26 | showDir, 27 | moduleTypes, 28 | } 29 | -------------------------------------------------------------------------------- /presets/plugins/mock.ts: -------------------------------------------------------------------------------- 1 | import { viteMockServe } from 'vite-plugin-mock' 2 | import { env } from '../shared/env' 3 | 4 | // https://github.com/anncwb/vite-plugin-mock 5 | export function MockPlugin() { 6 | const prodEnabled = env.VITE_APP_MOCK_IN_PRODUCTION 7 | 8 | return viteMockServe({ 9 | ignore: /^_/, 10 | mockPath: 'mock', 11 | supportTs: true, 12 | prodEnabled, 13 | injectCode: ` 14 | import { setupProdMockServer } from './mockProdServer'; 15 | setupProdMockServer(); 16 | `, 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /src/composables/usePrefixStorage.ts: -------------------------------------------------------------------------------- 1 | import type { StorageLike, UseStorageOptions } from '@vueuse/core' 2 | import { useStorage } from '@vueuse/core' 3 | 4 | export const storageKey = (key: string) => 5 | `${ 6 | import.meta.env.VITE_APP_STORAGE_PREFIX 7 | || import.meta.env.VITE_APP_TITLE 8 | || 'zls' 9 | }:${key}` 10 | 11 | export default ( 12 | key: string, 13 | initialValue: T, 14 | storage?: StorageLike, 15 | options?: UseStorageOptions, 16 | ) => { 17 | return useStorage(storageKey(key), initialValue, storage, options) 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Layout/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { NSpin } from 'naive-ui' 2 | import { h } from 'vue' 3 | import Card from '@/components/Card' 4 | 5 | export default defineComponent({ 6 | setup(_props) { 7 | // const { t } = useI18n() 8 | return () => h(NSpin, 9 | { 10 | size: 'small', 11 | rotate: false, 12 | }, 13 | { 14 | default: () => h(Card, { 15 | height: true, 16 | }, ''), 17 | // description: () => h('span', t('loading_page')), 18 | // icon: () => h(NIcon, { class: 'i-bx:dots-horizontal' }), 19 | }) 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /src/components/Layout/Header/Network.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 21 | en: 22 | tip: Network disconnection 23 | 24 | zh: 25 | tip: 网络断开 26 | 27 | -------------------------------------------------------------------------------- /src/components/Layout/Header/Translate.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 21 | en: 22 | tip: Toggle locales 23 | 24 | zh: 25 | tip: 切换语言 26 | 27 | -------------------------------------------------------------------------------- /src/components/DataForm/values.ts: -------------------------------------------------------------------------------- 1 | export function useValues(model: any) { 2 | const rawValues = ref>({}) 3 | const isNew = ref(false) 4 | function getValues() { 5 | return { ...model.value } 6 | } 7 | 8 | function setValues(v: Record) { 9 | if (!v || !Object.keys(v).length) 10 | isNew.value = true 11 | else 12 | isNew.value = false 13 | 14 | rawValues.value = { ...v } 15 | model.value = { ...v } 16 | } 17 | 18 | onMounted(() => { 19 | rawValues.value = getValues() 20 | }) 21 | 22 | return { 23 | isNew, 24 | getValues, 25 | rawValues, 26 | setValues, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/VChart/example/line-data.ts: -------------------------------------------------------------------------------- 1 | import { LineChart } from 'echarts/charts' 2 | import { use } from 'echarts/core' 3 | import type { ECBasicOption } from 'echarts/types/dist/shared' 4 | import 'vue-echarts' 5 | use(LineChart) 6 | 7 | export const optionLine = ref({ 8 | title: { 9 | text: '折线图', 10 | }, 11 | xAxis: { 12 | type: 'category', 13 | boundaryGap: false, 14 | data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], 15 | }, 16 | yAxis: { 17 | type: 'value', 18 | }, 19 | series: [ 20 | { 21 | data: [820, 932, 901, 934, 1290, 1330, 1320], 22 | type: 'line', 23 | areaStyle: {}, 24 | }, 25 | ], 26 | }) 27 | -------------------------------------------------------------------------------- /src/utils/render.ts: -------------------------------------------------------------------------------- 1 | import { NIcon, NTooltip } from 'naive-ui' 2 | import type { VNode } from 'vue' 3 | 4 | export function renderTooltip( 5 | trigger: VNode | string, 6 | content: VNode | string, 7 | ) { 8 | return h(NTooltip, null, { 9 | trigger: () => trigger, 10 | default: () => content, 11 | }) 12 | } 13 | 14 | export function renderIcon(icon: any, size?: number) { 15 | let props: any = size ? { size } : {} 16 | const isClassIcon = typeof icon === 'string' 17 | if (isClassIcon) 18 | props = { ...props, class: icon } 19 | 20 | return () => { 21 | return h(NIcon, props, { 22 | default: () => (isClassIcon ? h(icon) : null), 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@antfu" 4 | ], 5 | "rules": { 6 | "prefer-promise-reject-errors": "off", 7 | "@typescript-eslint/ban-ts-comment": "off", 8 | "no-console": "off", 9 | "@typescript-eslint/no-use-before-define": "off", 10 | "vue/component-name-in-template-casing": [ 11 | "error", 12 | "PascalCase", 13 | { 14 | "registeredComponentsOnly": false 15 | } 16 | ], 17 | "vue/component-tags-order": [ 18 | "error", 19 | { 20 | "order": [ 21 | "script", 22 | "template", 23 | "style", 24 | "route", 25 | "i18n" 26 | ] 27 | } 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/pages/example/layout/basic.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | 20 | 21 | 22 | 23 | { "meta": { "i18n": { "en": "Basics", "zh": "基础布局" } } } 24 | 25 | 26 | 27 | { 28 | "en": { 29 | "header": "This is a simple page." 30 | }, 31 | "zh": { 32 | "header": "这是一个简单的页面。" 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/pages/system/menu.ts: -------------------------------------------------------------------------------- 1 | import type { StMenu } from '@/types/global' 2 | 3 | export default [ 4 | { 5 | type: 'divider', 6 | }, 7 | { 8 | title: '系统管理', 9 | i18n: { 10 | en: 'System', 11 | }, 12 | icon: 'i-bx:wrench', 13 | path: 'system', 14 | children: [ 15 | { 16 | icon: 'i-bx:user', 17 | path: '/system/user', 18 | }, 19 | { 20 | icon: 'i-bx-id-card', 21 | path: '/system/role', 22 | }, 23 | { 24 | icon: 'i-bx-food-menu', 25 | path: '/system/menu', 26 | }, 27 | { 28 | icon: 'i-bx-objects-horizontal-left', 29 | path: '/system/site', 30 | }, 31 | ], 32 | }, 33 | ] as StMenu[] 34 | -------------------------------------------------------------------------------- /src/components/CardCols.vue: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /presets/plugins/legacy.ts: -------------------------------------------------------------------------------- 1 | import legacy from '@vitejs/plugin-legacy' 2 | import { LegacBrowserslist, browserslist, env } from '../shared/env' 3 | 4 | // https://github.com/vitejs/vite/tree/main/packages/plugin-legacy 5 | export function LegacyPlugin() { 6 | if (!env.IS_PROD) 7 | return null 8 | 9 | return (env.IS_PROD && env.VITE_BUILD_LEGACY) 10 | ? legacy({ 11 | polyfills: true, 12 | targets: browserslist, 13 | modernPolyfills: true, 14 | additionalLegacyPolyfills: ['regenerator-runtime/runtime'], 15 | }) 16 | : legacy({ 17 | ignoreBrowserslistConfig: true, 18 | targets: LegacBrowserslist, 19 | polyfills: false, 20 | // modernPolyfills: true, 21 | renderLegacyChunks: false, 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/[...notFound].vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 23 | 24 | 25 | 26 | 27 | meta: 28 | multiWindow: false 29 | i18n: 30 | en: Not Found 31 | zh: 页面不存在 32 | 33 | -------------------------------------------------------------------------------- /src/pages/example/layout/table-form/form.ts: -------------------------------------------------------------------------------- 1 | export function useForm() { 2 | const form = useDataForm() 3 | const { setItems, setOptions } = form 4 | 5 | setOptions({ 6 | labelWidth: 80, 7 | labelPlacement: 'top', 8 | }) 9 | 10 | setItems({ 11 | name: { 12 | label: '用户名', 13 | component: 'NInput', 14 | required: true, 15 | }, 16 | passwd: { 17 | label: '密码', 18 | component: 'NInput', 19 | required: true, 20 | props: { 21 | type: 'password', 22 | }, 23 | }, 24 | }) 25 | 26 | const showDrawer = ref(false) 27 | const drawerAction = ref('') 28 | 29 | function submitForm(data: any) { 30 | console.log(data) 31 | } 32 | 33 | return { ...form, showDrawer, drawerAction, submitForm } 34 | } 35 | -------------------------------------------------------------------------------- /src/pages/example/function/fullscreen.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 28 | 29 | 30 | { 31 | "meta": { 32 | "i18n": { 33 | "zh": "切换全屏", 34 | "en": "Fullscreen" 35 | }, 36 | "layout": "Empty", 37 | "fullscreen": true 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /presets/optimize.ts: -------------------------------------------------------------------------------- 1 | import type { DepOptimizationOptions } from 'vite' 2 | import { normalizeResolvers } from './shared/resolvers' 3 | 4 | export default function (): DepOptimizationOptions { 5 | return { 6 | include: normalizeResolvers({ 7 | onlyExist: [ 8 | ['axios', 'axios'], 9 | ['vue-use-api', 'vue-use-api'], 10 | ['@vueuse/core', '@vueuse/core'], 11 | ['naive-ui', 'naive-ui'], 12 | ['vant/es', 'vant'], 13 | ['vant/es/config-provider/style/index', 'vant'], 14 | ['date-fns', 'date-fns'], 15 | ['vue-echarts', 'vue-echarts'], 16 | ['echarts/charts', 'vue-echarts'], 17 | ['echarts/components', 'vue-echarts'], 18 | ['echarts/core', 'vue-echarts'], 19 | ['echarts/renderers', 'vue-echarts'], 20 | ], 21 | include: ['vue', 'vue-router', 'ayyui'], 22 | }) as string[], 23 | exclude: [], 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # 如果需要特别配置只本地生效,请复制当前文件,并重命名为 .env.development.local ,然后在该文件中配置 2 | # 如果生产环境需要特殊配置,可以复制当前文件,并重命名为 .env.production ,然后在该文件中配置 3 | 4 | 5 | # 标题 6 | VITE_APP_TITLE = ZlsApp 7 | 8 | # 接口域名,如:https://73zls.com 9 | VITE_APP_API_BASEURL = 10 | 11 | # 接口请求超时时间,毫秒 12 | VITE_APP_API_TIMEOUT = 10000 13 | 14 | # 开发服务器端口 15 | VITE_DEV_SERVE_PORT = 4000 16 | 17 | # 是否代理转发,true 则转发上面的 VITE_APP_API_BASEURL 18 | VITE_DEV_PROXY = false 19 | 20 | # 项目根目录 21 | VITE_BUILD_BASE = / 22 | 23 | # 构建产物输出目录 24 | VITE_BUILD_OUT_DIR = dist/ 25 | 26 | # 开发时 Inspect 调试支持 27 | VITE_DEV_INSPECT = true 28 | 29 | # 路由模式 hash 或 history 30 | VITE_ROUTER_HISTORY = hash 31 | 32 | # 生产时 mock 支持 33 | VITE_APP_MOCK_IN_PRODUCTION = false 34 | 35 | # 生产时是否构建旧版浏览器兼容代码 36 | VITE_BUILD_LEGACY = false 37 | 38 | # 生产时压缩算法,可选 gzip, brotliCompress, deflate, deflateRaw 39 | VITE_APP_COMPRESSINON_ALGORITHM = 40 | 41 | # 开启 PWA 42 | VITE_BUILD_PWA = false 43 | 44 | 45 | -------------------------------------------------------------------------------- /unocss.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, presetAttributify, presetUno, 3 | } from 'unocss' 4 | import presetIcons from '@unocss/preset-icons' 5 | import presetWind from '@unocss/preset-wind' 6 | import { presetCore, presetThemeDefault } from 'ayyui' 7 | 8 | export default defineConfig({ 9 | presets: [ 10 | presetAttributify(), 11 | presetUno(), 12 | presetWind(), 13 | presetCore(), 14 | presetThemeDefault(), 15 | // https://icones.netlify.app/ 16 | presetIcons({ 17 | scale: 1.2, 18 | extraProperties: { 19 | 'height': '1em', 20 | 'width': '1em', 21 | 'flex-shrink': '0', 22 | 'display': 'inline-block', 23 | 'vertical-align': 'middle', 24 | }, 25 | }), 26 | ], 27 | include: [ 28 | /.*\/ayyui\.js(.*)?$/, 29 | './**/*.vue', 30 | './**/*.md', 31 | './**/*.ts', 32 | './**/*.tsx', 33 | './src/menu.json', 34 | ], 35 | }) 36 | -------------------------------------------------------------------------------- /src/components/Layout/Header/Fullscreen.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | 24 | 25 | en: 26 | fullscreen: Toggle Fullscreen 27 | fullscreen_exit: Exit Fullscreen 28 | 29 | zh: 30 | fullscreen: 切换全屏 31 | fullscreen_exit: 退出全屏 32 | 33 | -------------------------------------------------------------------------------- /presets/plugins/markdown.ts: -------------------------------------------------------------------------------- 1 | import LinkAttributes from 'markdown-it-link-attributes' 2 | import Prism from 'markdown-it-prism' 3 | import Markdown from 'vite-plugin-md' 4 | import { env } from './../shared/env' 5 | 6 | export const markdownWrapperClasses = env.VITE_APP_MARKDOWN 7 | ? 'prose md:prose-lg lg:prose-lg dark:prose-invert text-left p-10 prose-slate prose-img:rounded-xl prose-headings:underline prose-a:text-blue-600' 8 | : '' 9 | 10 | export default () => { 11 | return ( 12 | env.VITE_APP_MARKDOWN 13 | && Markdown({ 14 | // builders: [link()], 15 | wrapperClasses: markdownWrapperClasses, 16 | markdownItSetup(md) { 17 | md.use(Prism) 18 | md.use(LinkAttributes, { 19 | matcher: (link: string) => /^:\/\//.test(link), 20 | attrs: { 21 | target: '_blank', 22 | rel: 'noopener', 23 | }, 24 | }) 25 | }, 26 | }) 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /presets/plugins/pwa.ts: -------------------------------------------------------------------------------- 1 | 2 | import { VitePWA } from "vite-plugin-pwa" 3 | import { env } from '../shared/env' 4 | 5 | // https://github.com/antfu/vite-plugin-pwa 6 | export function PWAPlugin() { 7 | return env.VITE_BUILD_PWA ? VitePWA({ 8 | registerType: "autoUpdate", 9 | includeAssets: ["favicon.ico"], 10 | manifest: { 11 | name: "ViteBoot", 12 | short_name: "ViteBoot", 13 | theme_color: "#ffffff", 14 | icons: [ 15 | { 16 | src: "/pwa-192x192.png", 17 | sizes: "192x192", 18 | type: "image/png", 19 | }, 20 | { 21 | src: "/pwa-512x512.png", 22 | sizes: "512x512", 23 | type: "image/png", 24 | }, 25 | { 26 | src: "/pwa-512x512.png", 27 | sizes: "512x512", 28 | type: "image/png", 29 | purpose: "any maskable", 30 | }, 31 | ], 32 | }, 33 | }) : null 34 | } 35 | -------------------------------------------------------------------------------- /src/types/env.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | 9 | declare module '*.vue' { 10 | import type { DefineComponent } from 'vue' 11 | const component: DefineComponent<{}, {}, any> 12 | export default component 13 | } 14 | 15 | declare module '*.md' { 16 | import { ComponentOptions } from 'vue' 17 | const Component: ComponentOptions 18 | export default Component 19 | } 20 | 21 | 22 | interface ImportMetaEnv { 23 | [x: string]: any 24 | readonly VITE_APP_TITLE: string 25 | readonly VITE_DEV_PROXY: string 26 | } 27 | 28 | interface ImportMeta { 29 | readonly env: ImportMetaEnv 30 | } 31 | 32 | declare module 'postcss-preset-env' 33 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 41 | -------------------------------------------------------------------------------- /presets/shared/resolvers.ts: -------------------------------------------------------------------------------- 1 | import { isPackageExists } from 'local-pkg' 2 | import type { Resolver } from 'unplugin-auto-import/types' 3 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 4 | import type { ComponentResolver } from 'unplugin-vue-components/types' 5 | 6 | type Arrayable = T | Array 7 | type Resolvers = Arrayable> 8 | 9 | export const AutoImportResolvers: Resolvers = [ElementPlusResolver()] 10 | 11 | interface Options { 12 | onlyExist?: [Arrayable | string, string][] 13 | include?: ComponentResolver[] | string[] | Object[] 14 | } 15 | export const normalizeResolvers = (options: Options = {}) => { 16 | const { onlyExist = [], include = [] } = options 17 | 18 | const existedResolvers = [] 19 | for (let i = 0; i < onlyExist.length; i++) { 20 | const [resolver, packageName] = onlyExist[i] 21 | if (isPackageExists(packageName)) 22 | existedResolvers.push(resolver) 23 | } 24 | 25 | return [...existedResolvers, ...include] 26 | } 27 | -------------------------------------------------------------------------------- /presets/plugins/html.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from 'vite' 2 | import { env } from '../shared/env' 3 | 4 | export const GenerateTitle = (): Plugin => { 5 | // let title: string 6 | let base: string 7 | return { 8 | name: 'vite-plugin-env-to-generate-title', 9 | configResolved(_config) { 10 | // title = config.env.VITE_APP_TITLE 11 | base = (env.IS_PROD && env.VITE_BUILD_BASE) ? `${env.VITE_BUILD_BASE}` : '/' 12 | }, 13 | transformIndexHtml(html) { 14 | let appendBody = '' 15 | // https://polyfill.io/v3/polyfill.js?features=Object.fromEntries,es5,es6,es7&flags=gated 16 | if (env.VITE_BUILD_LEGACY) 17 | appendBody = `` 18 | else 19 | appendBody = `` 20 | 21 | html = html.replace('', ` ${appendBody}\n`) 22 | // html = html.replace(/(.*?)<\/title>/, `<title>${title}`) 23 | return html 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/VChart/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | GridComponent, 3 | LegendComponent, 4 | TitleComponent, 5 | TooltipComponent, 6 | } from 'echarts/components' 7 | 8 | import { use } from 'echarts/core' 9 | import { CanvasRenderer } from 'echarts/renderers' 10 | import type { PropType } from 'vue' 11 | import Chart, { THEME_KEY } from 'vue-echarts' 12 | 13 | use([ 14 | CanvasRenderer, 15 | TitleComponent, 16 | TooltipComponent, 17 | LegendComponent, 18 | GridComponent, 19 | ]) 20 | 21 | export default defineComponent({ 22 | name: 'VEchart', 23 | props: { 24 | option: { 25 | type: Object as PropType, 26 | required: true, 27 | }, 28 | }, 29 | setup({ option }) { 30 | const attrs = useAttrs() 31 | const { isDark } = useDarks() 32 | const theme = computed(() => { 33 | return isDark.value ? 'dark' : '' 34 | }) 35 | 36 | provide(THEME_KEY, theme) 37 | 38 | return () => h(Chart, { autoresize: true, option, ...attrs }) 39 | }, 40 | }) 41 | -------------------------------------------------------------------------------- /presets/css/postcss.ts: -------------------------------------------------------------------------------- 1 | import autoprefixer from 'autoprefixer' 2 | import preset from 'postcss-preset-env' 3 | import { LegacBrowserslist, browserslist, env } from './../shared/env' 4 | 5 | export const PostcssConfig = () => { 6 | const plugins = [] 7 | 8 | const isH5 = env.VITE_APP_TYPE === 'h5' 9 | // eslint-disable-next-line @typescript-eslint/no-var-requires 10 | const pxtorem = require('postcss-pxtorem')({ 11 | rootValue({ file }: any) { 12 | return file.includes('vant') ? 37.5 : 75 13 | }, 14 | propList: ['*', '!*px'], 15 | selectorBlackList: [/px$/, 'loading-group', 'sk-cube-grid'], 16 | replace: true, 17 | mediaQuery: false, 18 | minPixelValue: 0, 19 | }) 20 | 21 | if (isH5) 22 | plugins.push(pxtorem) 23 | 24 | const browsers = env.VITE_BUILD_LEGACY ? browserslist : LegacBrowserslist 25 | 26 | plugins.push( 27 | autoprefixer({ 28 | overrideBrowserslist: browsers, 29 | }), 30 | preset({ 31 | browsers, 32 | }), 33 | ) 34 | 35 | return { 36 | plugins, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/VChart/example/bar-data.ts: -------------------------------------------------------------------------------- 1 | import 'vue-echarts' 2 | 3 | import { BarChart } from 'echarts/charts' 4 | import { use } from 'echarts/core' 5 | import { SVGRenderer } from 'echarts/renderers' 6 | import type { ECBasicOption } from 'echarts/types/dist/shared' 7 | 8 | use(BarChart) 9 | 10 | // 使用 SVG 渲染器 11 | use(SVGRenderer) 12 | 13 | export const initOptionBar = { renderer: 'svg' } 14 | export const optionBar = ref({ 15 | title: { 16 | text: '柱状图', 17 | left: 'center', 18 | }, 19 | tooltip: { 20 | trigger: 'item', 21 | formatter: '{a}
{b} : {c}', 22 | }, 23 | xAxis: { 24 | type: 'category', 25 | data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'], 26 | }, 27 | yAxis: { 28 | type: 'value', 29 | }, 30 | series: [ 31 | { 32 | name: 'Bar Sources', 33 | data: [120, 200, 150, 80, 70, 110, 130], 34 | type: 'bar', 35 | showBackground: true, 36 | backgroundStyle: { 37 | color: 'rgba(180, 180, 180, 0.2)', 38 | }, 39 | }, 40 | ], 41 | }) 42 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "rootDir": ".", 3 | "compilerOptions": { 4 | "strict": true, 5 | "jsx": "preserve", 6 | "target": "ES5", 7 | "module": "esnext", 8 | "sourceMap": true, 9 | "skipLibCheck": true, 10 | "isolatedModules": true, 11 | "esModuleInterop": true, 12 | "lib": [ 13 | "esnext", 14 | "dom", 15 | "es5", 16 | "es2015", 17 | "es2016", 18 | "es2017", 19 | "es2018" 20 | ], 21 | "resolveJsonModule": true, 22 | "moduleResolution": "node", 23 | "useDefineForClassFields": true, 24 | "baseUrl": ".", 25 | "paths": { 26 | "@/*": [ 27 | "src/*" 28 | ], 29 | "~/*": [ 30 | "src/*" 31 | ], 32 | "#/*": [ 33 | "src/types/*" 34 | ] 35 | }, 36 | "types": [] 37 | }, 38 | "include": [ 39 | "srcipts", 40 | "presets", 41 | "src/**/*.ts", 42 | "src/**/*.d.ts", 43 | "src/**/*.tsx", 44 | "src/**/*.vue", 45 | "./vite.config.ts", 46 | ], 47 | "exclude": [ 48 | "node_modules", 49 | "dist" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /src/components/VChart/example/pie-data.ts: -------------------------------------------------------------------------------- 1 | import { PieChart } from 'echarts/charts' 2 | import { use } from 'echarts/core' 3 | import type { ECBasicOption } from 'echarts/types/dist/shared' 4 | import 'vue-echarts' 5 | use(PieChart) 6 | 7 | export const optionPie = ref({ 8 | title: { 9 | text: '饼状图', 10 | subtext: 'Fake Data', 11 | left: 'center', 12 | }, 13 | tooltip: { 14 | trigger: 'item', 15 | }, 16 | legend: { 17 | orient: 'vertical', 18 | left: 'left', 19 | }, 20 | series: [ 21 | { 22 | name: 'Access From', 23 | type: 'pie', 24 | radius: '50%', 25 | data: [ 26 | { value: 1048, name: 'Search Engine' }, 27 | { value: 735, name: 'Direct' }, 28 | { value: 580, name: 'Email' }, 29 | { value: 484, name: 'Union Ads' }, 30 | { value: 300, name: 'Video Ads' }, 31 | ], 32 | emphasis: { 33 | itemStyle: { 34 | shadowBlur: 10, 35 | shadowOffsetX: 0, 36 | shadowColor: 'rgba(0, 0, 0, 0.5)', 37 | }, 38 | }, 39 | }, 40 | ], 41 | }) 42 | -------------------------------------------------------------------------------- /presets/build.ts: -------------------------------------------------------------------------------- 1 | import type { BuildOptions } from 'vite' 2 | import { env } from './shared/env' 3 | 4 | export function BuildConfig(): BuildOptions { 5 | return { 6 | outDir: env.VITE_BUILD_OUT_DIR, 7 | chunkSizeWarningLimit: 1500, 8 | minify: 'terser', 9 | rollupOptions: { 10 | output: { 11 | chunkFileNames: 'js/[name]-[hash].js', 12 | entryFileNames: 'js/[name]-[hash].js', 13 | assetFileNames: '[ext]/[name]-[hash].[ext]', 14 | manualChunks(id: string) { 15 | if (id.includes('node_modules')) { 16 | switch (true) { 17 | case id.includes('echarts/'): 18 | return 'echarts' 19 | case id.includes('pinia'): 20 | case id.includes('vue-use-api'): 21 | case id.includes('vue/'): 22 | case id.includes('vue-router/'): 23 | return 'lib' 24 | default: 25 | // return id.toString().split("node_modules/")[1].split("/")[0].toString(); 26 | } 27 | } 28 | }, 29 | }, 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/pages/example/function/markdown.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 38 | 39 | 40 | 41 | 42 | { "meta": {"title": "Markdown" } } 43 | 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 影浅 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/VChart/example/pie-rose-data.ts: -------------------------------------------------------------------------------- 1 | import { PieChart } from 'echarts/charts' 2 | import { TimelineComponent } from 'echarts/components' 3 | import { use } from 'echarts/core' 4 | import type { ECBasicOption } from 'echarts/types/dist/shared' 5 | import 'vue-echarts' 6 | use(PieChart) 7 | use(TimelineComponent) 8 | 9 | export const optionPieRose = ref({ 10 | baseOption: { 11 | title: { 12 | text: '基础南丁格尔玫瑰图', 13 | }, 14 | legend: { 15 | top: 'bottom', 16 | }, 17 | series: [ 18 | { 19 | name: 'Nightingale Chart', 20 | type: 'pie', 21 | radius: [50, 150], 22 | center: ['50%', '50%'], 23 | roseType: 'area', 24 | data: [ 25 | { value: 40, name: 'rose 1' }, 26 | { value: 38, name: 'rose 2' }, 27 | { value: 32, name: 'rose 3' }, 28 | { value: 30, name: 'rose 4' }, 29 | { value: 28, name: 'rose 5' }, 30 | { value: 26, name: 'rose 6' }, 31 | { value: 22, name: 'rose 7' }, 32 | { value: 18, name: 'rose 8' }, 33 | ], 34 | }, 35 | ], 36 | }, 37 | }) 38 | -------------------------------------------------------------------------------- /src/components/Layout/MultiWindow/index.ts: -------------------------------------------------------------------------------- 1 | import { type InjectionKey, type Ref, inject, onBeforeUnmount } from 'vue' 2 | 3 | import { type UseStore, type Window } from './store' 4 | 5 | export const injectionKey: InjectionKey<{ 6 | window: Ref 7 | store: ReturnType 8 | }> = Symbol('multi-window') 9 | 10 | export const baseComponentKey = Symbol('base-component') 11 | 12 | export function getCurrentWindow(): Window { 13 | const _inject = inject(injectionKey) 14 | 15 | if (!_inject || !_inject.store) 16 | console.warn('不是多窗口组件页面') 17 | // return null 18 | 19 | const route = useRoute() 20 | return _inject!.store.findWindowByFullPath(route.fullPath)! 21 | } 22 | 23 | export function onRefresh(callback: any) { 24 | const window = getCurrentWindow() 25 | if (!window) 26 | return () => null 27 | 28 | const { refreshCallback } = window 29 | 30 | refreshCallback.push(callback) 31 | 32 | function removeEvent() { 33 | const index = refreshCallback.indexOf(callback) 34 | if (index > -1) 35 | refreshCallback.splice(index, 1) 36 | } 37 | 38 | onBeforeUnmount(() => removeEvent()) 39 | 40 | return removeEvent 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Layout/Header/Message.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 38 | 39 | 47 | -------------------------------------------------------------------------------- /src/modules/i18n.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { createI18n } from 'vue-i18n' 3 | 4 | import localMessages from '@intlify/unplugin-vue-i18n/messages' 5 | 6 | export const locale = usePrefixStorage( 7 | 'locale', 8 | import.meta.env.VITE_APP_LOCALE || 'zh', 9 | ) 10 | 11 | export const messages = localMessages 12 | 13 | export const i18n = createI18n({ 14 | legacy: false, 15 | silentFallbackWarn: true, 16 | silentTranslationWarn: true, 17 | locale: locale.value as string, 18 | fallbackLocale: 'en', 19 | }) 20 | watch(locale, (locale) => { 21 | i18n.global.locale.value = locale 22 | }) 23 | Object.keys(messages).forEach((k) => { 24 | i18n.global.setLocaleMessage(k, messages[k]) 25 | }) 26 | 27 | export function globalt(key: string) { 28 | return i18n.global.t(key) || '' 29 | } 30 | 31 | export function mergeLocaleMessage(locales: any) { 32 | Object.keys(locales).forEach((locale) => { 33 | const t: any = {} 34 | Object.keys(locales[locale]).forEach((k) => { 35 | t[k] = () => { 36 | return locales[locale][k] 37 | } 38 | }) 39 | i18n.global.mergeLocaleMessage(locale, t) 40 | }) 41 | } 42 | 43 | export default (app: App) => { 44 | app.use(i18n) 45 | } 46 | -------------------------------------------------------------------------------- /src/components/DataTable/components/ColumnSetting.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 37 | 38 | 44 | -------------------------------------------------------------------------------- /src/pages/example/function/locale.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | 28 | 29 | meta: 30 | i18n: 31 | en: Localization 32 | zh: 多语言包 33 | 34 | 35 | 36 | en: 37 | hello: Hello 38 | toggle: Toggle 39 | login.title: Login (overwrite global language pack) 40 | not_found: Not Found (New) 41 | header: This is a simple page. 42 | zh: 43 | hello: 你好 44 | toggle: 切换 45 | login.title: 登录 (覆盖全局语言包) 46 | not_found: 没有找到 (新) 47 | header: 这是一个简单的页面。 48 | 49 | -------------------------------------------------------------------------------- /src/components/Layout/Header/Dark.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 39 | 40 | 41 | en: 42 | toggle_light: Toggle Light mode 43 | toggle_dark: Toggle Dark mode 44 | zh: 45 | toggle_dark: 切换深色模式 46 | toggle_light: 切换浅色模式 47 | 48 | -------------------------------------------------------------------------------- /src/pages/example/function/permission/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 41 | 42 | 45 | 46 | 47 | { 48 | "meta": { 49 | "i18n": { 50 | "en": "Permission", 51 | "zh": "权限测试" 52 | } 53 | } 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/pages/example/layout/table-form/[id].vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 44 | 45 | 48 | 49 | 50 | { 51 | "meta": { 52 | "i18n": { 53 | "en": "Details", 54 | "zh": "详情页面" 55 | } 56 | } 57 | } 58 | 59 | 60 | 61 | {} 62 | 63 | -------------------------------------------------------------------------------- /src/pages/example/function/clipboard.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 49 | 50 | 51 | 52 | 53 | meta: 54 | i18n: 55 | en: Clipboard 56 | zh: 剪切板 57 | 58 | -------------------------------------------------------------------------------- /src/pages/example/function/echart/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 34 | 35 | 37 | 38 | 39 | { 40 | "meta": { 41 | "icon": "i-bx:bar-chart-square", 42 | "title": "Echarts" 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/components/Layout/MultiWindow/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 37 | 38 | 54 | -------------------------------------------------------------------------------- /src/pages/example/layout/table-form/f1.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 36 | 37 | 40 | 41 | 42 | { 43 | "meta": { 44 | "maxWidth":0, 45 | "multiWindow": false, 46 | "icon": "i-bx:table", 47 | "i18n": { 48 | "en": "Table1", 49 | "zh": "功能表格1" 50 | } 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/components/CardRows.vue: -------------------------------------------------------------------------------- 1 | 59 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | import type { FormInst } from 'naive-ui' 2 | import type { Ref } from 'vue' 3 | import type { Router } from 'vue-router' 4 | 5 | import type { Components } from 'src/components/DataForm/render.ts' 6 | import type { TableColumn as column } from 'naive-ui/lib/data-table/src/interface' 7 | import type { DialogApiInjection } from 'naive-ui/lib/dialog/src/DialogProvider' 8 | import type { MessageApiInjection } from 'naive-ui/lib/message/src/MessageProvider' 9 | import type { NotificationApiInjection } from 'naive-ui/lib/notification/src/NotificationProvider' 10 | 11 | declare global { 12 | interface Window { 13 | Vue: any 14 | $state: any 15 | $loading: any 16 | $router: Router 17 | $getLocale: any 18 | $dialog: DialogApiInjection 19 | $message: MessageApiInjection 20 | $notification: NotificationApiInjection 21 | } 22 | type mapAny = { [key: string]: any } 23 | } 24 | 25 | export { } 26 | 27 | export interface InstApi { 28 | code: number 29 | msg?: string 30 | data?: any 31 | } 32 | 33 | export interface StMenu { 34 | type?: string 35 | path: string 36 | i18n?: boolean | { [key: string]: string } 37 | show?: boolean 38 | title?: string 39 | icon?: string 40 | url?: string 41 | redirect?: string 42 | children?: StMenu[] 43 | } 44 | 45 | export type RefFormInst = Ref<(HTMLElement & FormInst) | null> 46 | 47 | 48 | export type TableColumn = column & { 49 | editableUpdate?: function(any, any, number): void; 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # naive-ui-manage 2 | 3 | ## 🐳 演示地址 4 | 5 | [https://manage.demo.73zls.com](https://manage.demo.73zls.com) 6 | 7 | ## 🔥 简介 8 | 9 | 🔥 基于 Vue3 + Unocss + Naive UI 的轻量简洁的后台管理模板,相比其他后台模板,本模板更加轻量简洁,适合快速开发后台管理模板。 10 | 11 | ## 💡 特性 12 | 13 | - 基于 Vue3 + Unocss + [Naive UI](https://www.naiveui.com) 开发 14 | - 响应式布局,适配 PC 和移动端 15 | - 支持主题色切换 16 | - 文件路由,目录结构即路由 17 | - 声明式动态表格/表单 18 | - 内置登陆、注销及权限验证 19 | - 支持多环境配置,dev、测试、生产环境 20 | - 集成 `mock` 接口服务,dev 环境和发布环境都支持 21 | - 支持多标签页 22 | - 集成 `iconify` 图标,无限图标快捷使用 23 | - 组件、Api自动导入/按需加载 24 | - 内置示例页面(打包会自动排除) 25 | - i18n 国际化支持 26 | - 更多。。。 27 | 28 | ## 演示图 29 | 30 | | 登录页面 | 功能页面 | 表格页面 | 展示页面 | 31 | |:-----:|:-----:|:-----:|:-----:| 32 | | ![image](https://sohaha.73zls.com/static/naiveui-admin-template/screenshot-1.png) | ![image](https://sohaha.73zls.com/static/naiveui-admin-template/screenshot-2.png) | ![image](https://sohaha.73zls.com/static/naiveui-admin-template/screenshot-3.png) | ![image](https://sohaha.73zls.com/static/naiveui-admin-template/screenshot-4.png) | 33 | 34 | 35 | 36 | ## 📝 使用说明 37 | 38 | 文档: [https://docs.73zls.com/manage-template/#/](https://docs.73zls.com/manage-template/#/) 39 | 40 | 41 | 42 | ### 快速开始 43 | 44 | ```bash 45 | # 拉取最新模板 46 | git clone --depth=1 https://github.com/sohaha/naiveui-admin-template.git 47 | 48 | # 安装依赖 49 | npm install 50 | 51 | # 启动 52 | npm run dev 53 | ``` 54 | 55 | ### 内置命令 56 | 57 | **快捷建立/移除** 58 | 59 | ```bash 60 | # 建立 61 | npm run auto:create 62 | 63 | # 移除 64 | npm run auto:remove 65 | ``` -------------------------------------------------------------------------------- /src/composables/useHello.ts: -------------------------------------------------------------------------------- 1 | import { format } from 'date-fns' 2 | 3 | const now = useNow() 4 | const nowTime = computed(() => format(now.value, 'yyyy-MM-dd HH:mm:ss')) 5 | 6 | const time = computed(() => format(now.value, 'HH:mm:ss')) 7 | 8 | const say = computed(() => { 9 | const hour = now.value.getHours() 10 | let text = '早点休息吧!' 11 | 12 | switch (true) { 13 | case (hour >= 0) && (hour < 7): 14 | text = '注意休息哦!' 15 | break 16 | case (hour >= 7) && (hour < 12): 17 | text = '美好的一天!' 18 | break 19 | case (hour >= 12) && (hour < 14): 20 | text = '午休咯!' 21 | break 22 | case (hour >= 14) && (hour < 18): 23 | text = '努力加油!' 24 | break 25 | case (hour >= 18) && (hour <= 22): 26 | text = '放松一下吧!' 27 | break 28 | } 29 | 30 | return `${text}` 31 | }) 32 | 33 | const ask = computed(() => { 34 | const hour = now.value.getHours() 35 | let text = '深夜好' 36 | switch (true) { 37 | case hour < 6: 38 | text = '凌晨好' 39 | break 40 | case hour < 9: 41 | text = '早上好' 42 | break 43 | case hour < 12: 44 | text = '上午好' 45 | break 46 | case hour < 14: 47 | text = '中午好' 48 | break 49 | case hour < 17: 50 | text = '下午好' 51 | break 52 | case hour < 19: 53 | text = '傍晚好' 54 | break 55 | case hour < 22: 56 | text = '晚上好' 57 | break 58 | } 59 | 60 | return `${text}` 61 | }) 62 | 63 | export default () => { 64 | return { nowTime, time, say, ask } 65 | } 66 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | @import './base.css'; 2 | 3 | .grid-row { 4 | grid-gap: 0.75rem; 5 | gap: 0.75rem; 6 | } 7 | 8 | .a-card { 9 | border-radius: var(--n-border-radius); 10 | } 11 | 12 | .a-card-typography-wrapper, 13 | .card-body, 14 | .card-padding { 15 | padding: 1rem; 16 | } 17 | 18 | html:root { 19 | --a-bg: 0, 0%, 18%; 20 | --a-bg-color: #eff2f5; 21 | /* --a-bg-color: #f6f6f6; */ 22 | } 23 | 24 | html:root.dark { 25 | --a-bg: 0, 0%, 18%; 26 | --a-bg-color: #2e2e2e; 27 | } 28 | 29 | html.dark .n-dialog, 30 | html.dark .n-notification, 31 | html.dark .n-message, 32 | html.dark .n-dropdown-menu, 33 | html.dark .n-popover { 34 | --n-color: #262626 !important; 35 | } 36 | 37 | :root { 38 | --a-text-emphasis-light-opacity: 0.8; 39 | /* --a-primary:207,76%, 38%; */ 40 | } 41 | 42 | 43 | .n-card { 44 | --un-shadow: var(--un-shadow-inset) 0 10px 15px -3px var(--un-shadow-color, rgba(0, 0, 0, 0.1)), 45 | var(--un-shadow-inset) 0 4px 6px -4px var(--un-shadow-color, rgba(0, 0, 0, 0.1)); 46 | --un-ring-shadow: 0 -1px 4px rgba(0, 0, 0, 0.08); 47 | box-shadow: var(--un-ring-offset-shadow), var(--un-ring-shadow), var(--un-shadow); 48 | --un-bg-opacity: 1; 49 | background-color: hsla(var(--a-layer), var(--un-bg-opacity)) 50 | } 51 | 52 | .n-card:not(.n-card--bordered) { 53 | border: none !important; 54 | --un-shadow: ""; 55 | } 56 | 57 | .n-card.n-card.n-card--bordered { 58 | border: none; 59 | } 60 | 61 | .n-card.n-card--bordered>.n-card__content>.z-data-table>.a-card { 62 | --un-shadow: ""; 63 | } 64 | -------------------------------------------------------------------------------- /src/pages/example/layout/table.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 67 | 68 | 69 | 70 | 71 | { 72 | "meta": { 73 | "i18n": { 74 | "en": "Table", 75 | "zh": "普通表格" 76 | } 77 | } 78 | } 79 | 80 | -------------------------------------------------------------------------------- /src/utils/lock.ts: -------------------------------------------------------------------------------- 1 | export class ReadWriteLock { 2 | private readers: number 3 | private writer: boolean 4 | private waitingWriters: ((v?: unknown) => void)[] 5 | private waitingReaders: ((v?: unknown) => void)[] 6 | 7 | constructor() { 8 | this.readers = 0 9 | this.writer = false 10 | this.waitingWriters = [] 11 | this.waitingReaders = [] 12 | } 13 | 14 | async lockRead(): Promise { 15 | if (this.writer || this.waitingWriters.length > 0) 16 | await new Promise(resolve => this.waitingReaders.push(resolve)) 17 | 18 | this.readers++ 19 | } 20 | 21 | async unlockRead(): Promise { 22 | this.readers-- 23 | if (this.readers === 0 && this.waitingWriters.length > 0) { 24 | this.writer = true 25 | const nextWriter = this.waitingWriters.shift() 26 | if (nextWriter) 27 | nextWriter() 28 | } 29 | } 30 | 31 | async lockWrite(): Promise { 32 | if (this.writer || this.readers > 0) 33 | await new Promise(resolve => this.waitingWriters.push(resolve)) 34 | 35 | this.writer = true 36 | } 37 | 38 | async unlockWrite(): Promise { 39 | this.writer = false 40 | 41 | if (this.waitingWriters.length > 0) { 42 | const nextWriter = this.waitingWriters.shift() 43 | if (nextWriter) 44 | nextWriter() 45 | } 46 | else { 47 | while (this.waitingReaders.length > 0) { 48 | const nextReader = this.waitingReaders.shift() 49 | if (nextReader) 50 | nextReader() 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /scripts/create.js: -------------------------------------------------------------------------------- 1 | const fse = require('fs-extra') 2 | const { showDir, showExt, moduleTypes } = require('./shared/base') 3 | 4 | function create(plop) { 5 | let exist = null 6 | let modulePath = null 7 | 8 | plop.setGenerator('controller', { 9 | description: '自动创建', 10 | prompts: [ 11 | { 12 | name: 'type', 13 | type: 'list', 14 | default: 'page', 15 | message: '您希望生成哪种类型的模块?', 16 | choices: moduleTypes, 17 | }, 18 | { 19 | name: 'name', 20 | type: 'input', 21 | message({ type }) { 22 | return `请输入 ${type} 的命名` 23 | }, 24 | }, 25 | { 26 | name: 'shouldReset', 27 | type: 'confirm', 28 | default: false, 29 | message({ type }) { 30 | return `目标 ${type} 已存在,是否重置?` 31 | }, 32 | // 确认模块是否已存在,是则询问是否重置 33 | when({ type, name }) { 34 | const dir = showDir(type) 35 | const ext = showExt(type) 36 | modulePath = `src/${dir}/${name}.${ext}` 37 | exist = fse.pathExistsSync(modulePath) 38 | if (exist) 39 | return true 40 | }, 41 | }, 42 | ], 43 | actions(answer) { 44 | const { type, shouldReset } = answer 45 | if (exist && !shouldReset) 46 | throw new Error(`${type} 创建失败`) 47 | 48 | return [ 49 | { 50 | type: 'add', 51 | force: true, 52 | path: `../${modulePath}`, 53 | templateFile: `./template/${type}.hbs`, 54 | }, 55 | ] 56 | }, 57 | }) 58 | } 59 | 60 | module.exports = create 61 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "pwa-msedge", 5 | "name": "Launch Microsoft Edge", 6 | "request": "launch", 7 | "runtimeArgs": ["--remote-debugging-port=9222"], 8 | "url": "C:\\Users\\sosee\\scoop\\persist\\vscode\\data\\extensions\\ms-edgedevtools.vscode-edge-devtools-2.1.1\\out\\startpage\\index.html", 9 | "presentation": { 10 | "hidden": true 11 | } 12 | }, 13 | { 14 | "type": "pwa-msedge", 15 | "name": "Launch Microsoft Edge in headless mode", 16 | "request": "launch", 17 | "runtimeArgs": ["--headless", "--remote-debugging-port=9222"], 18 | "url": "C:\\Users\\sosee\\scoop\\persist\\vscode\\data\\extensions\\ms-edgedevtools.vscode-edge-devtools-2.1.1\\out\\startpage\\index.html", 19 | "presentation": { 20 | "hidden": true 21 | } 22 | }, 23 | { 24 | "type": "vscode-edge-devtools.debug", 25 | "name": "Open Edge DevTools", 26 | "request": "attach", 27 | "url": "C:\\Users\\sosee\\scoop\\persist\\vscode\\data\\extensions\\ms-edgedevtools.vscode-edge-devtools-2.1.1\\out\\startpage\\index.html", 28 | "presentation": { 29 | "hidden": true 30 | } 31 | } 32 | ], 33 | "compounds": [ 34 | { 35 | "name": "Launch Edge Headless and attach DevTools", 36 | "configurations": [ 37 | "Launch Microsoft Edge in headless mode", 38 | "Open Edge DevTools" 39 | ] 40 | }, 41 | { 42 | "name": "Launch Edge and attach DevTools", 43 | "configurations": ["Launch Microsoft Edge", "Open Edge DevTools"] 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /src/stores/settingStore.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from '@vueuse/core' 2 | import { defineStore } from 'pinia' 3 | 4 | type themeOptions = 'dark' | 'light' 5 | 6 | export default defineStore('settingStore', { 7 | state() { 8 | return { 9 | theme: { 10 | PrimaryColor: '#1a546f', // #db3327 , #54b6fc 11 | }, 12 | layout: { 13 | maxWidth: '', 14 | drawerPlacement: 'right', 15 | }, 16 | menu: { 17 | collapsed: true, 18 | indent: 4, 19 | rootIndent: 16, 20 | theme: 'light' as themeOptions, 21 | }, 22 | toolbar: [ 23 | 'fullscreen', 24 | 'translate', 25 | 'dark', 26 | ], 27 | } 28 | }, 29 | actions: { 30 | mergeSettings(e: { [key: string]: any }) { 31 | Object.keys(e).forEach((key: any) => { 32 | // @ts-expect-error 33 | if (isObject(this[key])) { 34 | // @ts-expect-error 35 | Object.assign(this[key], e[key]) 36 | } 37 | else { 38 | // @ts-expect-error 39 | this[key] = e[key] 40 | } 41 | }) 42 | }, 43 | toggleCollapsed(s?: boolean) { 44 | if (s !== undefined) { 45 | this.menu.collapsed = s 46 | return 47 | } 48 | this.menu.collapsed = !this.menu.collapsed 49 | }, 50 | }, 51 | getters: { 52 | isCollapsed(): boolean { 53 | return this.menu.collapsed 54 | }, 55 | isMenuInverted(): boolean { 56 | return this.menu.theme === 'dark' 57 | }, 58 | getMaxWidth(): string { 59 | return this.layout.maxWidth || '' 60 | }, 61 | }, 62 | persist: true, 63 | }) 64 | -------------------------------------------------------------------------------- /src/stores/stateStore.ts: -------------------------------------------------------------------------------- 1 | import { isObject } from '@vueuse/core' 2 | import { defineStore } from 'pinia' 3 | const isSmallScreen = useMediaQuery('(max-width: 640px)') 4 | const { height } = useWindowSize({ includeScrollbar: true }) 5 | 6 | const defPageHeaderHeight = 42 + 12 7 | export default defineStore('stateStore', { 8 | state() { 9 | return { 10 | loadingMsg: '', 11 | pageHeaderHeight: defPageHeaderHeight, 12 | unreadMessage: 0, 13 | windowHeight: height, 14 | } 15 | }, 16 | getters: { 17 | getPageContentHeight(): number { 18 | return height.value - this.pageHeaderHeight - 12 * 2 19 | }, 20 | isSmallScreen() { 21 | return isSmallScreen.value 22 | }, 23 | }, 24 | actions: { 25 | toTitle(meta?: { [key: string]: any }) { 26 | const { locale, t } = useI18n() 27 | let title = '' 28 | if (!meta) { 29 | const route = useRoute() 30 | meta = route?.meta || {} 31 | } 32 | const i18n: undefined = meta?.i18n as any 33 | if (i18n) { 34 | if (isObject(i18n)) 35 | title = i18n[locale.value] || title 36 | else title = meta.i18n ? t(meta.title) : meta.title 37 | } 38 | if (!title && meta.title) 39 | title = meta.title 40 | 41 | return title 42 | }, 43 | setPageHeaderHeight(h?: number) { 44 | if (h === undefined) { 45 | this.pageHeaderHeight = defPageHeaderHeight 46 | return 47 | } 48 | this.pageHeaderHeight = h 49 | }, 50 | setLoadingMsg(msg: string) { 51 | this.loadingMsg = msg 52 | }, 53 | setUnreadMessage(i: number) { 54 | this.unreadMessage = i 55 | }, 56 | }, 57 | // persist: true, 58 | }) 59 | -------------------------------------------------------------------------------- /src/components/Card.tsx: -------------------------------------------------------------------------------- 1 | import { NCard } from 'naive-ui' 2 | import { h } from 'vue' 3 | 4 | export default defineComponent({ 5 | name: 'ZCard', 6 | props: { 7 | height: { 8 | type: [Boolean, Number, String], 9 | default: false, 10 | }, 11 | contentClass: { 12 | type: String, 13 | default: '', 14 | }, 15 | contentStyle: { 16 | type: String, 17 | default: '', 18 | }, 19 | padding: { 20 | type: [Number, String], 21 | default: '', 22 | }, 23 | }, 24 | setup(props: { height: any; padding: Number | String;contentStyle: string }, ctx: any) { 25 | const state = stateStore() 26 | const cardStyle = computed(() => { 27 | const h = props.height 28 | let height 29 | if (!h) { 30 | height = 'auto' 31 | } 32 | else if (h === true) { 33 | height = `${state.getPageContentHeight}px` 34 | } 35 | else if (typeof h === 'string') { 36 | height = h 37 | } 38 | else if (typeof h === 'number') { 39 | if (h > 0) 40 | height = `${h}px` 41 | else height = `${state.getPageContentHeight + h}px` 42 | } 43 | else { 44 | height = h 45 | } 46 | 47 | return { height } 48 | }) 49 | 50 | const contentStyle = computed(() => { 51 | return `${(props.padding !== '' && `padding:${props.padding};`) + props.contentStyle};overflow: auto;` 52 | }) 53 | 54 | return () => { 55 | const slots: { [key: string]: Function } = {} 56 | return h( 57 | NCard, 58 | { ...ctx.attrs, contentStyle: contentStyle.value, style: cardStyle.value }, 59 | { ...slots, ...ctx.slots }, 60 | ) 61 | } 62 | }, 63 | }) 64 | -------------------------------------------------------------------------------- /src/modules/directives.ts: -------------------------------------------------------------------------------- 1 | import type { App, ObjectDirective } from 'vue' 2 | import { MotionPlugin } from '@vueuse/motion' 3 | 4 | export default (app: App) => { 5 | app.use(MotionPlugin) 6 | app.directive('throttled', throttle()) 7 | app.directive('permission', permission()) 8 | } 9 | 10 | function permission(): ObjectDirective { 11 | return { 12 | created: (el: HTMLElement, binding: any) => { 13 | const user = userStore() 14 | const value = binding.value || [] 15 | const display = el.style.display 16 | watch(() => user.getPermission, (permission) => { 17 | let hasPer = false 18 | if (Array.isArray(value)) 19 | hasPer = value.every(v => permission.includes(v)) 20 | else 21 | hasPer = permission.includes(value) 22 | 23 | if (!hasPer) 24 | el.style.display = 'none' 25 | else 26 | el.style.display = display 27 | }, { immediate: true }) 28 | }, 29 | } 30 | } 31 | 32 | function throttle(): ObjectDirective { 33 | return { 34 | created: (el: HTMLElement, binding: any) => { 35 | let throttleTime = +binding.arg 36 | if (!throttleTime) 37 | throttleTime = 350 38 | 39 | ; (binding.value || 'click').split(',').forEach((name: string) => { 40 | if (!name) 41 | return 42 | let cbFun: any 43 | el.addEventListener(name, (event) => { 44 | if (!cbFun) { 45 | cbFun = setTimeout(() => { 46 | cbFun = null 47 | }, throttleTime) 48 | } 49 | else { 50 | event && event.stopImmediatePropagation() 51 | } 52 | }, true) 53 | }) 54 | }, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/pages/example/layout/form-drawer.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 67 | 68 | 71 | 72 | 73 | { 74 | "meta": { 75 | "title": "弹窗表单" 76 | } 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/components/MdEditor/index.tsx: -------------------------------------------------------------------------------- 1 | // import hljs from 'highlight.js' 2 | // import 'highlight.js/styles/tokyo-night-dark.css' 3 | 4 | import type { ToolbarNames } from 'md-editor-v3' 5 | import MdEditor from 'md-editor-v3' 6 | import 'md-editor-v3/lib/style.css' 7 | import './style.css' 8 | 9 | type MdEditorPropsType = InstanceType['$props'] 10 | 11 | MdEditor.config({ 12 | editorExtensions: { 13 | // highlight: { 14 | // instance: hljs, 15 | // }, 16 | }, 17 | }) 18 | 19 | export default defineComponent({ 20 | name: 'ZMdEditor', 21 | emits: ['update:value', 'htmlChanged', 'uploadImg'], 22 | setup(_: any, { emit }) { 23 | const state = stateStore() 24 | const { locale } = useLanguage() 25 | const { isDark } = useDarks() 26 | const attrs = useAttrs() 27 | const editorProps = computed((): MdEditorPropsType => { 28 | let toolbarsExclude: ToolbarNames[] = ['unorderedList', 'orderedList', 'github', 'mermaid', 'katex', 'prettier', 'pageFullscreen', 'fullscreen', 'codeRow', 'sub', 'sup', 'save'] 29 | if (state.isSmallScreen) 30 | toolbarsExclude = [...toolbarsExclude, 'image', 'code', 'bold', 'underline', 'italic', 'strikeThrough', 'quote', 'title', '-'] 31 | 32 | return { 33 | noPrettier: false, 34 | // preview: false, 35 | toolbarsExclude, 36 | theme: isDark.value ? 'dark' : 'light', 37 | language: locale.value === 'zh' ? 'zh-CN' : 'en-US', 38 | onHtmlChanged: (html: string) => { 39 | emit('htmlChanged', html) 40 | }, 41 | onUploadImg: (files: Array, callBack: (urls: string[]) => void) => { 42 | emit('uploadImg', files, callBack) 43 | }, 44 | ...attrs, 45 | } 46 | }) 47 | 48 | return () => h(MdEditor, editorProps.value) 49 | }, 50 | }) 51 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { UserConfig, defineConfig } from 'vite' 3 | import { BuildConfig } from './presets/build' 4 | import Css from './presets/css' 5 | import OptimizeDepsConfig from './presets/optimize' 6 | import Plugins from './presets/plugins' 7 | import { ServerConfig } from './presets/server' 8 | import { env } from './presets/shared/env' 9 | 10 | const defaultConfig = { 11 | base: (env.IS_PROD && env.VITE_BUILD_BASE) ? `${env.VITE_BUILD_BASE}` : '/', 12 | define: { 13 | __VUE_OPTIONS_API__: false, 14 | // __VUE_PROD_DEVTOOLS__: true, 15 | }, 16 | resolve: { 17 | alias: { 18 | // 'vue': 'vue/dist/vue.esm-bundler.js', 19 | '@/': `${resolve(__dirname, 'src')}/`, 20 | '~/': `${resolve(__dirname, 'src')}/`, 21 | '#/': `${resolve(__dirname, 'src/types')}/`, 22 | }, 23 | }, 24 | plugins: [Plugins()], 25 | css: Css(), 26 | optimizeDeps: OptimizeDepsConfig(), 27 | server: ServerConfig(), 28 | build: BuildConfig(), 29 | rollupOptions: { 30 | output: { 31 | globals: { 32 | vue: 'Vue', 33 | } 34 | } 35 | } 36 | } 37 | 38 | const otherConfig: UserConfig = { 39 | ...defaultConfig, build: { 40 | ...defaultConfig.build, 41 | cssCodeSplit: true, 42 | outDir: 'dist-other', 43 | lib: { 44 | formats: ['umd'], 45 | entry: ["demo"].map(v => `src/other/${v}.vue`), 46 | name: 'otherComponent', 47 | fileName: (format) => `other.${format}.js` 48 | }, 49 | rollupOptions: { 50 | external: ['vue'], 51 | output: { 52 | globals: { 53 | vue: 'Vue', 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | export default (conf: UserConfig) => { 61 | const config = conf.mode === 'other' ? otherConfig : defaultConfig 62 | return defineConfig(config); 63 | }; 64 | -------------------------------------------------------------------------------- /src/components/Layout/Provider.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/pages/example/readme.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 60 | 61 | 62 | { 63 | "meta": { 64 | "icon": "i-bx:detail", 65 | "i18n": { 66 | "en": "Readme", 67 | "zh": "示例说明" 68 | } 69 | } 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/pages/example/layout/multi-column.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 46 | 47 | 59 | 60 | 61 | meta: 62 | multiWindow: false 63 | i18n: 64 | en: Column 65 | zh: 多列布局 66 | 67 | 68 | 69 | en: 70 | hello: Hello 71 | header: This is a simple page. 72 | zh: 73 | hello: 你好 74 | header: 这是一个简单的页面。 75 | 76 | -------------------------------------------------------------------------------- /src/apis/system.ts: -------------------------------------------------------------------------------- 1 | import type { InstApi } from '@/types/global' 2 | 3 | export function apiUserList(params: { 4 | page: number 5 | pagesize: number 6 | }): Promise { 7 | return apis.get('/manage/account/user', { params }) 8 | } 9 | 10 | export function apiUserInfo(id: number | string): Promise { 11 | return apis.get(`/manage/account/user/${id}`) 12 | } 13 | 14 | export function apiUserUpdate(id: number | string, data: { [key: string]: any }): Promise { 15 | return apis.put(`/manage/account/user/${id}`, data) 16 | } 17 | 18 | export function apiUserCreare(data: { [key: string]: any }): Promise { 19 | return apis.post('/manage/account/user', data) 20 | } 21 | 22 | export function apiUserDelete(id: number | string): Promise { 23 | return apis.delete(`/manage/account/user/${id}`) 24 | } 25 | 26 | export function apiMenuReset(data: any[]): Promise { 27 | return apis.put('/manage/account/menu/reset', data) 28 | } 29 | 30 | export function apiMenuList(): Promise { 31 | return apis.get('/manage/account/menu') 32 | } 33 | 34 | export function apiRoleList(): Promise { 35 | return apis.get('/manage/account/role') 36 | } 37 | 38 | export function apiRoleCreare(data: { [key: string]: any }): Promise { 39 | return apis.post('/manage/account/role', data) 40 | } 41 | 42 | export function apiRoleUpdate(id: number | string, data: { [key: string]: any }): Promise { 43 | return apis.put(`/manage/account/role/${id}`, data) 44 | } 45 | 46 | export function apiRoleDelete(id: number | string): Promise { 47 | return apis.delete(`/manage/account/role/${id}`) 48 | } 49 | 50 | export function apiSetting(): Promise { 51 | return apis.get('/manage/setting') 52 | } 53 | 54 | export function apiSettingUpdate(data: Record[]): Promise { 55 | return apis.put('/manage/setting', data) 56 | } 57 | -------------------------------------------------------------------------------- /scripts/remove.js: -------------------------------------------------------------------------------- 1 | const { basename } = require('path') 2 | const fg = require('fast-glob') 3 | const fse = require('fs-extra') 4 | const { showDir, moduleTypes } = require('./shared/base') 5 | 6 | function remove(plop) { 7 | plop.setActionType('remove', (answers) => { 8 | const { name, type, shouldRemove } = answers 9 | const dir = showDir(type) 10 | const target = `./src/${dir}/${name}` 11 | if (shouldRemove) 12 | return fse.removeSync(target) 13 | 14 | throw new Error(`删除 ${target} 失败`) 15 | }) 16 | 17 | plop.setGenerator('controller', { 18 | description: '自动删除', 19 | prompts: [ 20 | { 21 | name: 'type', 22 | type: 'list', 23 | message: '请选择您要删除的类型', 24 | async choices() { 25 | const dirs = await fg('./src/**/*', { 26 | deep: 1, 27 | onlyDirectories: true, 28 | }) 29 | const types = moduleTypes.filter((type) => { 30 | const dir = showDir(type) 31 | return dirs.includes(`./src/${dir}`) 32 | }) 33 | return types 34 | }, 35 | }, 36 | { 37 | name: 'name', 38 | type: 'list', 39 | message({ type }) { 40 | return `请选择您要删除的 ${type} 模块` 41 | }, 42 | async choices({ type }) { 43 | const dir = showDir(type) 44 | let modules = await fg(`./src/${dir}/*.*`, { 45 | deep: 1, 46 | onlyFiles: true, 47 | }) 48 | modules = modules.map((module) => { 49 | return basename(module) 50 | }) 51 | return modules 52 | }, 53 | }, 54 | { 55 | name: 'shouldRemove', 56 | type: 'confirm', 57 | default: false, 58 | message: '再次确认是否删除', 59 | }, 60 | ], 61 | actions: [ 62 | { 63 | type: 'remove', 64 | }, 65 | ], 66 | }) 67 | } 68 | 69 | module.exports = remove 70 | -------------------------------------------------------------------------------- /presets/plugins/h5.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { env } from '../shared/env' 3 | 4 | const toRem = `(function flexible (window, document) { 5 | var docEl = document.documentElement 6 | var dpr = window.devicePixelRatio || 1 7 | 8 | // adjust body font size 9 | function setBodyFontSize () { 10 | if (document.body) { 11 | document.body.style.fontSize = (12 * dpr) + 'px' 12 | } 13 | else { 14 | document.addEventListener('DOMContentLoaded', setBodyFontSize) 15 | } 16 | } 17 | setBodyFontSize(); 18 | 19 | // set 1rem = viewWidth / 10 20 | function setRemUnit () { 21 | var rem = 56 22 | if(docEl.clientWidth<1125){ 23 | rem = docEl.clientWidth / 10 24 | } 25 | docEl.style.fontSize = rem + 'px' 26 | } 27 | 28 | setRemUnit() 29 | 30 | // reset rem unit on page resize 31 | window.addEventListener('resize', setRemUnit) 32 | window.addEventListener('pageshow', function (e) { 33 | if (e.persisted) { 34 | setRemUnit() 35 | } 36 | }) 37 | 38 | // detect 0.5px supports 39 | if (dpr >= 2) { 40 | var fakeBody = document.createElement('body') 41 | var testElement = document.createElement('div') 42 | testElement.style.border = '.5px solid transparent' 43 | fakeBody.appendChild(testElement) 44 | docEl.appendChild(fakeBody) 45 | if (testElement.offsetHeight === 1) { 46 | docEl.classList.add('hairlines') 47 | } 48 | docEl.removeChild(fakeBody) 49 | } 50 | }(window, document))` 51 | 52 | export default function H5Plugins() { 53 | const isH5 = env.VITE_APP_TYPE === 'h5' 54 | if (isH5) 55 | return [H5ToRem()] 56 | 57 | return [] 58 | } 59 | 60 | function H5ToRem() { 61 | const h5 = { 62 | name: 'h5', 63 | transform(code: string, id: string) { 64 | if (path.relative(path.resolve('src'), id) === 'main.ts') 65 | return `${code};${toRem}` 66 | 67 | return code 68 | }, 69 | } 70 | 71 | return h5 72 | } 73 | -------------------------------------------------------------------------------- /src/pages/example/layout/table-form/f3.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 48 | 49 | 50 | 51 | 52 | { 53 | "meta": { 54 | "maxWidth": 900, 55 | "icon": "i-bx:table", 56 | "i18n": { 57 | "en": "Table3", 58 | "zh": "功能表格3" 59 | } 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /src/pages/system/site.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 68 | 69 | 74 | 75 | 76 | { 77 | "meta": { 78 | "title": "站点设置" 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /presets/shared/mock.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck ignore 2 | import Mock from 'mockjs' 3 | 4 | export function createFetchSever(mockList: any[]) { 5 | if (!window.originFetch) { 6 | window.originFetch = window.fetch 7 | window.fetch = function (fetchUrl: string, init: any) { 8 | const currentMock = mockList.find(mi => fetchUrl.includes(mi.url)) 9 | if (currentMock) { 10 | const result = createFetchReturn(currentMock, init) 11 | return result 12 | } 13 | else { 14 | return window.originFetch(fetchUrl, init) 15 | } 16 | } 17 | } 18 | } 19 | 20 | function __param2Obj__(url: string) { 21 | const search = url.split('?')[1] 22 | if (!search) 23 | return {} 24 | 25 | return JSON.parse( 26 | `{"${decodeURIComponent(search) 27 | .replace(/"/g, '\\"') 28 | .replace(/&/g, '","') 29 | .replace(/=/g, '":"') 30 | .replace(/\+/g, ' ')}"}`, 31 | ) 32 | } 33 | 34 | function __Fetch2ExpressReqWrapper__(handle: () => any) { 35 | return function (options: any) { 36 | let result = null 37 | if (typeof handle === 'function') { 38 | const { body, method, url, headers } = options 39 | 40 | let b = body 41 | b = JSON.parse(body) 42 | result = handle({ 43 | method, 44 | body: b, 45 | query: __param2Obj__(url), 46 | headers, 47 | }) 48 | } 49 | else { 50 | result = handle 51 | } 52 | 53 | return Mock.mock(result) 54 | } 55 | } 56 | 57 | const sleep = (delay = 0) => { 58 | if (delay) { 59 | return new Promise((resolve) => { 60 | setTimeout(resolve, delay) 61 | }) 62 | } 63 | return null 64 | } 65 | 66 | async function createFetchReturn(mock: any, init) { 67 | const { timeout, response } = mock 68 | const mockFn = __Fetch2ExpressReqWrapper__(response) 69 | const data = mockFn(init) 70 | await sleep(timeout) 71 | const result = { 72 | ok: true, 73 | status: 200, 74 | clone() { 75 | return result 76 | }, 77 | text() { 78 | return Promise.resolve(data) 79 | }, 80 | json() { 81 | return Promise.resolve(data) 82 | }, 83 | } 84 | return result 85 | } 86 | -------------------------------------------------------------------------------- /presets/shared/env.ts: -------------------------------------------------------------------------------- 1 | import { loadEnv } from 'vite' 2 | 3 | const { NODE_ENV } = process.env 4 | 5 | // 是否是开发环境 6 | export const isDevelopment = NODE_ENV === 'development' 7 | 8 | // 是否是生产环境 9 | export const IS_PROD = NODE_ENV === 'production' 10 | 11 | const stringToBoolean = (v: string) => { 12 | return Boolean(v === 'true' || false) 13 | } 14 | 15 | // 获取环境变量 16 | const useEnv = () => { 17 | const env = IS_PROD ? loadEnv('production', '.') : loadEnv('development', '.') 18 | 19 | const { 20 | VITE_APP_TITLE, 21 | VITE_APP_TYPE, 22 | VITE_APP_DISABLED_API_AUTO_IMPORT, 23 | VITE_APP_COMPRESSINON_ALGORITHM, 24 | VITE_APP_API_BASEURL, 25 | VITE_BUILD_BASE, 26 | VITE_BUILD_OUT_DIR, 27 | } = env 28 | 29 | return { 30 | IS_PROD, 31 | ...env, 32 | VITE_APP_TITLE, 33 | VITE_APP_TYPE, 34 | VITE_APP_COMPRESSINON_ALGORITHM, 35 | VITE_APP_API_BASEURL, 36 | VITE_BUILD_OUT_DIR, 37 | VITE_BUILD_BASE: (() => (!!VITE_BUILD_BASE && VITE_BUILD_BASE.substring(VITE_BUILD_BASE.length - 1) !== '/') ? `${VITE_BUILD_BASE}/` : VITE_BUILD_BASE)(), 38 | VITE_DEV_INSPECT: stringToBoolean(env.VITE_DEV_INSPECT), 39 | VITE_APP_API_BASEURL_DYNAMIC: stringToBoolean(env.VITE_APP_API_BASEURL_DYNAMIC), 40 | VITE_DEV_PROXY: stringToBoolean(env.VITE_DEV_PROXY), 41 | VITE_DEV_SERVE_PORT: Number(env.VITE_DEV_SERVE_PORT) || 4000, 42 | VITE_BUILD_LEGACY: stringToBoolean(env.VITE_BUILD_LEGACY), 43 | VITE_BUILD_PWA: stringToBoolean(env.VITE_BUILD_PWA), 44 | VITE_APP_MARKDOWN: stringToBoolean(env.VITE_APP_MARKDOWN), 45 | VITE_APP_REMOVE_MENU: stringToBoolean(env.VITE_APP_REMOVE_MENU), 46 | VITE_APP_DISABLED_API_AUTO_IMPORT: stringToBoolean( 47 | VITE_APP_DISABLED_API_AUTO_IMPORT, 48 | ), 49 | VITE_APP_MOCK_IN_PRODUCTION: stringToBoolean( 50 | env.VITE_APP_MOCK_IN_PRODUCTION, 51 | ), 52 | } 53 | } 54 | 55 | export const env = useEnv() 56 | 57 | export const browserslist = [ 58 | '> 1%, last 1 version, ie >= 11', 59 | 'safari >= 10', 60 | 'Android > 28', 61 | 'Chrome >= 60', 62 | 'Safari >= 10.1', 63 | 'iOS >= 10.3', 64 | 'Firefox >= 54', 65 | 'Edge >= 15', 66 | ] 67 | 68 | export const LegacBrowserslist = ['Android > 28'] 69 | -------------------------------------------------------------------------------- /src/pages/example/layout/table-form/f2.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 63 | 64 | 65 | 66 | 67 | { 68 | "meta": { 69 | "icon": "i-bx:table", 70 | "i18n": { 71 | "en": "Table2", 72 | "zh": "功能表格2" 73 | } 74 | } 75 | } 76 | 77 | -------------------------------------------------------------------------------- /src/pages/inlay/detail.vue: -------------------------------------------------------------------------------- 1 | 90 | 91 | 96 | 97 | 100 | 101 | 102 | { 103 | "meta": { 104 | "title": "个人信息" 105 | } 106 | } 107 | 108 | -------------------------------------------------------------------------------- /src/pages/example/layout/form-basic.vue: -------------------------------------------------------------------------------- 1 | 86 | 87 | 98 | 99 | 100 | 101 | 102 | { 103 | "meta": { 104 | "i18n": { 105 | "en": "Form", 106 | "zh": "表单布局" 107 | } 108 | } 109 | } 110 | 111 | 112 | 113 | {} 114 | 115 | -------------------------------------------------------------------------------- /src/stores/themeStore.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | CustomThemeCommonVars, 3 | GlobalThemeOverrides, 4 | ThemeCommonVars, 5 | } from 'naive-ui' 6 | import { darkTheme as dark } from 'naive-ui' 7 | import type { BuiltInGlobalTheme } from 'naive-ui/es/themes/interface' 8 | import type { DataTableThemeVars } from 'naive-ui/lib/data-table/styles' 9 | 10 | const { isDark } = useDarks() 11 | 12 | const primaryColor = usePrefixStorage('primaryColor', '#1768AC') 13 | 14 | export function updatePrimaryColor(color: string) { 15 | primaryColor.value = color 16 | } 17 | 18 | function addLight(color: string, amount: number) { 19 | const cc = parseInt(color, 16) + amount 20 | const c = cc > 255 ? 255 : cc 21 | return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}` 22 | } 23 | 24 | function lighten(color: string, amount: number) { 25 | color = color.includes('#') ? color.substring(1, color.length) : color 26 | amount = Math.trunc((255 * amount) / 100) 27 | return `#${addLight(color.substring(0, 2), amount)}${addLight( 28 | color.substring(2, 4), 29 | amount, 30 | )}${addLight(color.substring(4, 6), amount)}` 31 | } 32 | 33 | export default defineStore('themeStore', { 34 | state() { 35 | return { 36 | style: 'block', 37 | } 38 | }, 39 | getters: { 40 | darkTheme(): BuiltInGlobalTheme { 41 | return dark 42 | }, 43 | getBaseTheme(): GlobalThemeOverrides { 44 | const setting = settingStore() 45 | let appTheme = setting.theme.PrimaryColor || '#1768AC' 46 | const common: Partial = {} 47 | common.borderRadius = '.5rem' 48 | 49 | const dataTable: Partial = {} 50 | 51 | if (this.style === 'block') 52 | common.borderRadius = '3px' 53 | 54 | if (isDark.value) { 55 | common.baseColor = '#101014' 56 | dataTable.thColor = '#18181c' 57 | appTheme = lighten(appTheme, 9) 58 | } 59 | else { 60 | dataTable.thColor = '#fff' 61 | common.baseColor = '#f5f7f9' 62 | } 63 | 64 | return { 65 | common: { 66 | ...common, 67 | primaryColor: appTheme, 68 | primaryColorHover: lighten(appTheme, 8), 69 | primaryColorPressed: lighten(appTheme, 6), 70 | primaryColorSuppl: lighten(appTheme, 9), 71 | }, 72 | DataTable: dataTable, 73 | LoadingBar: { 74 | colorLoading: appTheme, 75 | }, 76 | } 77 | }, 78 | }, 79 | actions: {}, 80 | persist: { 81 | key: storageKey('theme'), 82 | }, 83 | }) 84 | -------------------------------------------------------------------------------- /public/browser.js: -------------------------------------------------------------------------------- 1 | var _0x57bf=['112174TAdUze','length','DOMContentLoaded','/browser-tip.js','data-version','appendChild','getAttribute','body','1513087beJqZA','2zsGVeV','6418NAkhsU','967510QcayoE','currentScript','onload','2358741JssSgx','script','createElement','src','655618AHCJgX','match','367527zfoCoM','attachEvent','userAgent'];function _0x4b0b(_0x29243f,_0x26bf00){_0x29243f=_0x29243f-0x6d;var _0x57bf91=_0x57bf[_0x29243f];return _0x57bf91;}(function(_0x59c67c,_0x4649b0){var _0x1b9388=_0x4b0b;while(!![]){try{var _0x359072=parseInt(_0x1b9388(0x6d))+-parseInt(_0x1b9388(0x76))+-parseInt(_0x1b9388(0x81))+parseInt(_0x1b9388(0x83))*-parseInt(_0x1b9388(0x82))+-parseInt(_0x1b9388(0x74))+parseInt(_0x1b9388(0x79))+parseInt(_0x1b9388(0x70));if(_0x359072===_0x4649b0)break;else _0x59c67c['push'](_0x59c67c['shift']());}catch(_0x23f3c2){_0x59c67c['push'](_0x59c67c['shift']());}}}(_0x57bf,0xd920d),!function(){var _0x182c79=_0x4b0b,_0x445617=navigator[_0x182c79(0x78)],_0x48fde1={},_0x119d8e=_0x445617['match'](/Android ([\d.]+)/),_0x4a9013=_0x445617[_0x182c79(0x75)](/WebKit\/([\d.]+)/),_0x90dea2=_0x445617[_0x182c79(0x75)](/Chrome\/([\d.]+)/)||_0x445617[_0x182c79(0x75)](/CriOS\/([\d.]+)/),_0x439fe0=_0x445617[_0x182c79(0x75)](/MSIE\s([\d\.]+)/)||_0x445617[_0x182c79(0x75)](/(?:trident)(?:.*rv:([\w.]+))?/i),_0xaa1b74=_0x445617[_0x182c79(0x75)](/Firefox\/([\d.]+)/),_0x48fb5e=_0x445617[_0x182c79(0x75)](/Safari\/([\d.]+)/),_0x51427e=_0x445617['match'](/OPR\/([\d.]+)/);_0x119d8e&&(_0x48fde1['a']=parseFloat(_0x119d8e[0x1])),_0x4a9013&&(_0x48fde1['w']=parseFloat(_0x4a9013[0x1])),_0x90dea2&&!_0x119d8e&&(_0x48fde1['c']=parseFloat(_0x90dea2[0x1])),_0x439fe0&&(_0x48fde1['i']=parseFloat(_0x439fe0[0x1])),_0xaa1b74&&(_0x48fde1['f']=parseFloat(_0xaa1b74[0x1])),_0x48fb5e&&(_0x48fde1['s']=parseFloat(_0x48fb5e[0x1])),_0x51427e&&(_0x48fde1['o']=parseFloat(_0x51427e[0x1]));var _0x48f4fe=document[_0x182c79(0x6e)]||function(){var _0x59fc82=_0x182c79,_0x228b62=document['getElementsByTagName'](_0x59fc82(0x71));return _0x228b62[_0x228b62[_0x59fc82(0x7a)]-0x1];}(),_0x1f1857;try{if(_0x48f4fe){var _0x5d94e2=_0x48f4fe[_0x182c79(0x7f)](_0x182c79(0x7d));_0x5d94e2==='true'?_0x1f1857={'f':0x4f,'c':0x49,'i':0xd,'s':12.1,'a':0x9}:_0x1f1857=JSON['parse'](_0x5d94e2);}}catch(_0xb884c0){}if(!_0x1f1857)_0x1f1857={'f':0x35,'c':0x3f,'i':0xd,'a':0x7,'s':0x9};var _0x1591af=![];for(var _0x416d6d in _0x48fde1){if(_0x1f1857[_0x416d6d]&&_0x48fde1[_0x416d6d]<_0x1f1857[_0x416d6d]){_0x1591af=!![];break;}}if(!_0x1591af)return;function _0x4503d7(){var _0x43a1ee=_0x182c79,_0x5da1bd=document[_0x43a1ee(0x72)](_0x43a1ee(0x71));_0x5da1bd[_0x43a1ee(0x73)]=_0x43a1ee(0x7c),document[_0x43a1ee(0x80)][_0x43a1ee(0x7e)](_0x5da1bd);}try{document['addEventListener'](_0x182c79(0x7b),_0x4503d7,![]);}catch(_0x286069){window[_0x182c79(0x77)](_0x182c79(0x6f),_0x4503d7);}}()); -------------------------------------------------------------------------------- /src/modules/router.ts: -------------------------------------------------------------------------------- 1 | import { setupLayouts } from 'virtual:meta-layouts' 2 | import type { App } from 'vue' 3 | import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router' 4 | import { 5 | createRouter, 6 | createWebHashHistory, 7 | createWebHistory, 8 | } from 'vue-router' 9 | import { setupMultiWindow } from '@/components/Layout/MultiWindow/setupMultiWindow' 10 | import fileRoutes from '~pages' 11 | 12 | const routes = setupLayouts(fileRoutes) 13 | 14 | setupMultiWindow(routes, multiWindowStore) 15 | 16 | export const router = createRouter({ 17 | routes, 18 | history: 19 | import.meta.env.VITE_ROUTER_HISTORY === 'hash' 20 | ? createWebHashHistory(import.meta.env.VITE_PUBLIC_PATH) 21 | : createWebHistory(import.meta.env.VITE_PUBLIC_PATH), 22 | scrollBehavior: () => ({ left: 0, top: 0 }), 23 | }) 24 | 25 | export default (app: App) => { 26 | app.use(router) 27 | } 28 | 29 | router.afterEach((v) => { 30 | if (import.meta.env.SSR || v.fullPath === '/inlay/loading') 31 | return 32 | 33 | nextTick(() => { 34 | window.$loading?.finish() 35 | }) 36 | const meta = v?.meta || {} 37 | if (meta?.fullscreen) { 38 | (() => { 39 | const { toggle, isFullscreen, isSupported } = useWindowFullscreen(meta.layout === 'Home' || meta.layout === undefined) 40 | if (!isSupported.value) 41 | return 42 | 43 | const toggleFullscreen = async () => { 44 | if (!isFullscreen.value) 45 | window.$state.setPageHeaderHeight(0) 46 | 47 | try { 48 | await toggle() 49 | } 50 | catch (e) { 51 | } 52 | } 53 | toggleFullscreen() 54 | })() 55 | } 56 | }) 57 | 58 | // guards 59 | router.beforeEach( 60 | ( 61 | to: RouteLocationNormalized, 62 | from: RouteLocationNormalized, 63 | next: NavigationGuardNext, 64 | ) => { 65 | window.$loading?.start() 66 | const { getToken, validPermission, isLogged } = userStore() 67 | const isLogin = to.name === 'login' 68 | if (!getToken && !isLogin) { 69 | next({ 70 | path: '/login', 71 | query: { 72 | redirect: to.fullPath, 73 | }, 74 | }) 75 | return 76 | } 77 | 78 | const permission = to.meta?.permission || [] 79 | 80 | if (Array.isArray(permission) && permission.length > 0) { 81 | if (!isLogged) { 82 | next('/') 83 | return 84 | } 85 | 86 | if (!validPermission(permission!)) { 87 | if (import.meta.env.DEV 88 | || import.meta.env.VITE_APP_MOCK_IN_PRODUCTION === 'true') 89 | window.$message.error(`该页面需要以下权限:${JSON.stringify(permission)}`) 90 | next(false) 91 | multiWindowStore()?.closeTab(to.fullPath) 92 | return 93 | } 94 | } 95 | next() 96 | }, 97 | ) 98 | -------------------------------------------------------------------------------- /mock/user.ts: -------------------------------------------------------------------------------- 1 | import mock from 'mockjs' 2 | 3 | const infoFake = (status?: number) => { 4 | return { 5 | 'account|+1': '@name', 6 | 'nickname': '@last', 7 | 'status': status || '@pick([1, 2])', 8 | avatar(): any { 9 | return mock.Random.image('64x64', mock.Random.color(), this.nickname) 10 | }, 11 | 'remark': '@cparagraph(1, 3)', 12 | 'email': '@email', 13 | 'roles_name': ['管理员'], 14 | 'created_at': '@date("yyyy-MM-dd HH:mm:ss")', 15 | 'updated_at': '@datetime', 16 | } 17 | } 18 | 19 | export function info() { 20 | return mock.mock(infoFake()) 21 | } 22 | 23 | function createLists(v: any) { 24 | const { query } = v 25 | 26 | return { 27 | code: 200, 28 | msg: '用户列表', 29 | data: { 30 | v, 31 | page: { 32 | total: 100, 33 | curpage: query.page, 34 | }, 35 | [`items|${query.pagesize}`]: [ 36 | { 37 | 'id|+1': query.pagesize * (query.page - 1) + 1, 38 | ...infoFake(+query.status), 39 | }, 40 | ], 41 | }, 42 | } 43 | } 44 | 45 | function UseriInfo(v: any) { 46 | const { query } = v 47 | return { 48 | code: 200, 49 | msg: '用户详情', 50 | data: { 51 | id: query.id, 52 | ...info(), 53 | }, 54 | } 55 | } 56 | 57 | export default [ 58 | { 59 | url: '/manage/user/index', 60 | method: 'get', 61 | response(v: any) { 62 | const { query } = v 63 | if (query.id) 64 | return UseriInfo(v) 65 | return createLists(v) 66 | }, 67 | }, 68 | { 69 | url: '/manage/user/index', 70 | method: 'put', 71 | timeout: 1000, 72 | response() { 73 | return { 74 | code: 0, 75 | msg: '更新完成', 76 | data: {}, 77 | } 78 | }, 79 | }, 80 | { 81 | url: '/manage/user/index', 82 | method: 'post', 83 | timeout: 1000, 84 | response() { 85 | return { 86 | code: 0, 87 | msg: '添加完成', 88 | data: {}, 89 | } 90 | }, 91 | }, 92 | { 93 | url: '/manage/user/index', 94 | method: 'delete', 95 | statusCode: 200, 96 | timeout: 1000, 97 | response() { 98 | return { 99 | code: 0, 100 | msg: '删除完成', 101 | data: {}, 102 | } 103 | }, 104 | }, 105 | { 106 | url: '/manage/user/avatar.go', 107 | method: 'post', 108 | response() { 109 | return { 110 | code: 200, 111 | msg: 'ok', 112 | data: { 113 | avatar: mock.Random.image('64x64', mock.Random.color(), 'new'), 114 | }, 115 | } 116 | }, 117 | }, 118 | { 119 | url: '/_mock/lists', 120 | method: 'get', 121 | timeout: 1000, 122 | response: createLists, 123 | }, 124 | ] 125 | -------------------------------------------------------------------------------- /src/components/DataForm/utils.ts: -------------------------------------------------------------------------------- 1 | import type { NForm } from 'naive-ui' 2 | import type { PropType } from 'vue' 3 | import type { DynamicFormProps, FormItemProps } from './render' 4 | 5 | export function useDataForm(o?: DynamicFormProps) { 6 | const formRef = ref(null) 7 | const loading = ref(false) 8 | const options = ref({ 9 | requireMarkPlacement: 'left', 10 | submitBtn: '提 交', 11 | resetBtn: '重 置', 12 | ...(o || {}), 13 | }) 14 | const items = ref>({}) 15 | const config = computed(() => { 16 | return { 17 | options: options.value, 18 | items: items.value, 19 | loading: loading.value, 20 | formRef, 21 | } 22 | }) 23 | 24 | function getValues() { 25 | return { ...(formRef.value?.$parent.$parent?.getValues() || {}) } 26 | } 27 | function setValues(values: { [key: string]: any }) { 28 | const newValues: { [key: string]: any } = {} 29 | Object.keys(items.value).forEach((key) => { 30 | if (typeof values[key] !== 'undefined') 31 | newValues[key] = values[key] 32 | }) 33 | nextTick(() => { 34 | formRef.value?.$parent.$parent?.setValues(newValues) 35 | }) 36 | } 37 | return { 38 | config, 39 | setLoading(l: boolean) { 40 | loading.value = l 41 | }, 42 | setOptions(o: DynamicFormProps) { 43 | options.value = { ...options.value, ...o } 44 | }, 45 | setItems(i: Record) { 46 | items.value = i 47 | }, 48 | setItem(key: string, f: FormItemProps | Object) { 49 | if (items.value[key]) { 50 | const { props } = items.value[key] 51 | const { props: newProps } = f as any 52 | f = { ...items.value[key], ...f, props: { ...props, ...newProps } } 53 | } 54 | items.value[key] = f as FormItemProps 55 | formRef.value?.$parent?.$parent?.reRender() 56 | }, 57 | getValues, 58 | setValue: (key: string, value: any) => { 59 | setValues({ ...getValues(), [key]: value }) 60 | }, 61 | setValues, 62 | reset() { 63 | formRef.value?.$parent?.$parent?.reset() 64 | }, 65 | validate() { 66 | return new Promise((resolve, reject) => { 67 | formRef.value 68 | ?.validate() 69 | .then(() => { 70 | resolve(getValues()) 71 | }) 72 | .catch((err: any) => { 73 | reject(err) 74 | }) 75 | }) 76 | }, 77 | formRef, 78 | } 79 | } 80 | 81 | export const props = { 82 | items: { 83 | type: Object as PropType>, 84 | require: true, 85 | default() { 86 | return [] 87 | }, 88 | }, 89 | loading: Boolean, 90 | formRef: Object, 91 | options: { 92 | type: Object as PropType, 93 | default: () => ({}), 94 | }, 95 | } 96 | -------------------------------------------------------------------------------- /src/pages/example/layout/form-advanced.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 118 | 119 | 120 | { 121 | "meta": { 122 | "title": "高级表单", 123 | "i18n": { 124 | "en":"AdvancedForm" 125 | } 126 | } 127 | } 128 | 129 | -------------------------------------------------------------------------------- /src/components/Layout/Header/Tabs.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 103 | 104 | 109 | -------------------------------------------------------------------------------- /src/components/Layout/Header.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 79 | 80 | 93 | 94 | 95 | en: 96 | collapsed: Collapsed menu 97 | expand: Expand menu 98 | 99 | zh: 100 | collapsed: 收起导航菜单 101 | expand: 展开导航菜单 102 | 103 | -------------------------------------------------------------------------------- /src/apis/base.ts: -------------------------------------------------------------------------------- 1 | import settings from '@/settings.json' 2 | import type { InstApi } from '@/types/global' 3 | 4 | /** 5 | * 登录 6 | * @param account 用户名 7 | * @param password 密码 8 | */ 9 | export function apiLogin(account: string, password: string): Promise { 10 | return apis.post('/manage/base/login', { account, password }) 11 | } 12 | 13 | /** 14 | * 站点配置 15 | */ 16 | export function apiSite(): Promise { 17 | return apiNoILock.get('/manage/base/site').then((v: any) => { 18 | if (v.data?.sitename) 19 | multiWindowStore().resetAppName(v.data.sitename) 20 | return v 21 | }) 22 | } 23 | 24 | /** 25 | * 退出 26 | */ 27 | export function apiLogout(): Promise { 28 | return apis.post('/manage/base/logout') 29 | } 30 | 31 | /** 32 | * 当前用户信息 33 | */ 34 | export function apiMe(options = {}) { 35 | return useRequest(async () => { 36 | lock.lockWrite() 37 | let resp 38 | try { 39 | resp = await apiNoILock.get('/manage/base/me') 40 | lock.unlockWrite() 41 | return resp 42 | } 43 | catch (error: any) { 44 | lock.unlockWrite() 45 | if (error) 46 | throw new Error(error?.message) 47 | } 48 | }, { 49 | cacheKey: 'me', 50 | staleTime: 3000, 51 | manual: true, 52 | onBefore(params) { 53 | return params 54 | }, 55 | onAfter() { 56 | }, 57 | onSuccess(_data) { 58 | }, 59 | onError(err) { 60 | if (err) 61 | window.$message.error(err.message) 62 | }, 63 | ...options, 64 | }) 65 | } 66 | 67 | /** 68 | * 心跳 69 | */ 70 | export function apiHeartbeat(): Promise { 71 | return apis.get('/manage/base/message') 72 | } 73 | 74 | /** 75 | * 操作日志 76 | */ 77 | export function apiOperationLogs(params: { 78 | [x: string]: any 79 | page: number 80 | pagesize: number 81 | }, category?: number): Promise { 82 | if (category) 83 | params.category = category 84 | return apis.get('/manage/base/logs', { params }) 85 | } 86 | 87 | /** 88 | * 更新密码 89 | */ 90 | export function apiEditPassword( 91 | old_password: string, 92 | password: string, 93 | ): Promise { 94 | return apis.patch('/manage/base/password', { 95 | old_password, 96 | password, 97 | }) 98 | } 99 | 100 | /** 101 | * 更新用户信息 102 | */ 103 | export function apiMeUpdate(data: Record): Promise { 104 | return apis.patch('/manage/base/me', data) 105 | } 106 | 107 | /** 108 | * 上传用户头像 109 | */ 110 | export function apiUploadAvatar(file: File): Promise { 111 | const formData = new FormData() 112 | formData.append('file', file) 113 | return apis.post('/manage/base/avatar', formData, { 114 | headers: { 115 | 'Content-Type': 'multipart/form-data', 116 | }, 117 | }) 118 | } 119 | 120 | /** 121 | * 获取配置 122 | */ 123 | export function apiSettings(): Promise { 124 | // 可以远程获取配置 125 | return new Promise((resolve) => { 126 | resolve(settings) 127 | }).then((res: any) => { 128 | if (res.code === 200) 129 | return res.data 130 | return res 131 | }) 132 | } 133 | -------------------------------------------------------------------------------- /src/components/Layout/Header/Window.vue: -------------------------------------------------------------------------------- 1 | 104 | 105 | 117 | 118 | 119 | en: 120 | refresh: Refresh 121 | close: Close 122 | close_other: Close other 123 | fullscreen: Local screen 124 | zh: 125 | close: 关闭当前 126 | close_other: 关闭其他 127 | refresh: 刷新当前 128 | fullscreen: 局部全屏 129 | 130 | -------------------------------------------------------------------------------- /src/stores/userStore.ts: -------------------------------------------------------------------------------- 1 | import exampleMenu from '@/pages/example/menu' 2 | import type { StMenu } from '@/types/global' 3 | 4 | const sessionToken = usePrefixStorage('token', '', sessionStorage) 5 | 6 | const logged = ref(false) 7 | const permission = ref([]) 8 | const memu = ref([]) 9 | const user = ref<{ [key: string]: any }>({}) 10 | export default defineStore('user', { 11 | state() { 12 | return { 13 | keepLogin: true, 14 | token: '', 15 | aloneMenu: [] as StMenu[], 16 | } 17 | }, 18 | getters: { 19 | getToken(): string { 20 | return this.keepLogin ? this.token : sessionToken.value 21 | }, 22 | getNickName(): string { 23 | return user.value?.nickname || user.value?.account || '' 24 | }, 25 | getAvatar(): string { 26 | return user.value?.avatar 27 | }, 28 | getMemu(): StMenu[] { 29 | return memu.value || [] 30 | }, 31 | getAloneMenu(): StMenu[] { 32 | return this.aloneMenu || [] 33 | }, 34 | getAllMenu(): StMenu[] { 35 | return [...this.getMemu, ...this.getAloneMenu] 36 | }, 37 | getUser(): { [key: string]: any } { 38 | return user.value 39 | }, 40 | getPermission(): string[] { 41 | return permission.value 42 | }, 43 | getAllMenuPath(): string[] { 44 | let p: string[] = [] 45 | this.getAllMenu.forEach((v: StMenu) => { 46 | p = p.concat(memuPath(v)) 47 | }) 48 | if ( 49 | import.meta.env.DEV 50 | || import.meta.env.VITE_APP_MOCK_IN_PRODUCTION === 'true' || import.meta.env.VITE_BUILD_DEMONSTRATE === 'true' 51 | ) { 52 | exampleMenu.forEach((v) => { 53 | p = p.concat(memuPath(v)) 54 | }) 55 | } 56 | return p 57 | }, 58 | isLogged(): boolean { 59 | return logged.value 60 | }, 61 | }, 62 | actions: { 63 | validMenu(_path: string): boolean { 64 | return false 65 | }, 66 | validPermission(p: string[]): boolean { 67 | return p.every(v => permission.value.includes(v)) 68 | }, 69 | setLogged() { 70 | logged.value = true 71 | }, 72 | setToken(token: string) { 73 | if (!token) { 74 | this.setUset({}) 75 | this.setMenus([]) 76 | logged.value = false 77 | } 78 | 79 | if (this.keepLogin) { 80 | sessionToken.value = '' 81 | this.token = token 82 | return 83 | } 84 | sessionToken.value = token 85 | this.token = '' 86 | }, 87 | setUset(u: { [key: string]: any }) { 88 | user.value = u 89 | }, 90 | setMenus(m: any) { 91 | memu.value = m 92 | }, 93 | appendMenus(m: StMenu[]) { 94 | this.aloneMenu = m 95 | }, 96 | setPermission(p: string[]) { 97 | permission.value = p 98 | }, 99 | }, 100 | persist: { 101 | key: storageKey('user'), 102 | }, 103 | }) 104 | 105 | function memuPath(m: StMenu): string[] { 106 | let p: string[] = [] 107 | if (m.children && m.children.length > 0) { 108 | m.children.forEach((item) => { 109 | p = p.concat(memuPath(item)) 110 | }) 111 | } 112 | 113 | else if (m.path) { 114 | p.push(m.path) 115 | } 116 | 117 | return p 118 | } 119 | -------------------------------------------------------------------------------- /src/pages/system/menu.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 108 | 109 | 114 | 115 | 116 | { "meta": { "title": "菜单管理" } } 117 | 118 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import type { VNode, VNodeChild } from 'vue' 2 | import { isObject } from '@vueuse/core' 3 | import { Comment, Fragment, createTextVNode } from 'vue' 4 | import type { RouteRecord } from 'vue-router' 5 | import type { StMenu } from '@/types/global' 6 | 7 | export function openURL(url: string) { 8 | window.open(url, '', 'noopener=yes,noreferrer=yes') 9 | } 10 | 11 | export function getQuery(key: string, url?: string): string | null { 12 | if (!url) 13 | url = window.location.href 14 | 15 | const r = (new RegExp(`[?|&]${key}=` + '([^&;]+?)(&|#|;|$)').exec(url) || ['', '']) 16 | 17 | return decodeURIComponent(r[1].replace(/\+/g, '%20')) || null 18 | } 19 | 20 | export function random(min: number, max: number): number { 21 | return Math.floor(Math.random() * (max - min + 1) + min) 22 | } 23 | 24 | export function flatten( 25 | vNodes: VNodeChild[], 26 | filterCommentNode = true, 27 | result: VNode[] = [], 28 | ): VNode[] { 29 | vNodes.forEach((vNode) => { 30 | if (vNode === null) 31 | return 32 | if (typeof vNode !== 'object') { 33 | if (typeof vNode === 'string' || typeof vNode === 'number') 34 | result.push(createTextVNode(String(vNode))) 35 | 36 | return 37 | } 38 | if (Array.isArray(vNode)) { 39 | flatten(vNode, filterCommentNode, result) 40 | return 41 | } 42 | if (vNode.type === Fragment) { 43 | if (vNode.children === null) 44 | return 45 | if (Array.isArray(vNode.children)) 46 | flatten(vNode.children, filterCommentNode, result) 47 | 48 | // rawSlot 49 | } 50 | else if (vNode.type !== Comment) { 51 | result.push(vNode) 52 | } 53 | }) 54 | return result 55 | } 56 | 57 | export function findRoute(routes: RouteRecord[], path: string, locale: string, t: (key: string) => string) { 58 | const r = routes.find((route) => { 59 | return route.path === path 60 | }) 61 | 62 | let title = 'Unknown' 63 | let icon = 'i-bx:detail' 64 | if (r) { 65 | const meta = r.meta 66 | const rTitle = (meta?.title || '') as string 67 | if (meta?.icon) 68 | icon = meta.icon as string 69 | const i18n: undefined = meta?.i18n as any 70 | if (isObject(i18n) && locale) 71 | title = i18n[locale] as string || rTitle 72 | else if (rTitle) 73 | title = (i18n && t) ? t(rTitle) : rTitle 74 | } 75 | 76 | return [title, icon] 77 | } 78 | 79 | export function completionRouteTitle(menu: StMenu[], routes: RouteRecord[], locale: string, t: (key: string) => string) { 80 | return menu.map(({ children, path = '', title, icon, type = '' }) => { 81 | const item: any = { path, title, icon, type } 82 | if (children) { 83 | item.children = completionRouteTitle(children, routes, locale, t) 84 | item.path = '' 85 | } 86 | 87 | if (type === 'divider') { 88 | item.title = '分割线' 89 | item.icon = '' 90 | } 91 | else { 92 | const [rtitle, ricon] = findRoute(routes!, item.path, locale!, t!) 93 | if (!title) 94 | item.title = rtitle 95 | if (!icon) 96 | item.icon = ricon 97 | } 98 | 99 | return item 100 | }) 101 | } 102 | 103 | export function fixAvatar(url: string) { 104 | if (url[0] === '/') 105 | url = (import.meta.env.VITE_APP_API_BASEURL as string) + url 106 | 107 | return url 108 | } 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zls-template", 3 | "version": "0.0.1", 4 | "description": "vite + vue3 + ts 开箱即用现代开发模板", 5 | "scripts": { 6 | "dev": "vite --host", 7 | "dev:bun": "bun vite --host", 8 | "build": "vite build", 9 | "dev:host": "vite --host", 10 | "dev:open": "vite --open", 11 | "preview": "vite preview --host --open", 12 | "typecheck": "npx vue-tsc --noEmit", 13 | "deps:fresh": "npx taze -I -w", 14 | "preview:host": "vite preview --host", 15 | "preview:open": "vite preview --open", 16 | "auto:create": "plop --plopfile scripts/create.js", 17 | "auto:remove": "plop --plopfile scripts/remove.js", 18 | "lint": "eslint --ext .ts,.js,.jsx,.vue .", 19 | "lint:fix": "eslint --fix --ext .ts,.js,.jsx,.vue .", 20 | "release": "npx bumpp -r && npm -r publish --access public", 21 | "prepare": "husky install" 22 | }, 23 | "dependencies": { 24 | "@sohaha/zlog": "^0.0.3", 25 | "@vueuse/core": "^9.13.0", 26 | "@vueuse/motion": "2.0.0-beta.26", 27 | "ayyui": "^0.1.14", 28 | "naive-ui": "^2.35.0", 29 | "pinia": "^2.1.7", 30 | "push.js": "^1.0.12", 31 | "vue": "^3.3.10", 32 | "vue-router": "^4.2.5", 33 | "vue-use-api": "^0.0.8" 34 | }, 35 | "engines": { 36 | "node": ">=16" 37 | }, 38 | "devDependencies": { 39 | "@antfu/eslint-config": "^0.34.2", 40 | "@iconify-json/bx": "^1.1.9", 41 | "@intlify/unplugin-vue-i18n": "^0.8.2", 42 | "@sergeymakinen/vite-plugin-html-minimize": "^1.0.6", 43 | "@types/node": "18.11.13", 44 | "@unocss/preset-icons": "^0.47.6", 45 | "@unocss/preset-wind": "^0.47.6", 46 | "@vitejs/plugin-legacy": "^3.0.2", 47 | "@vitejs/plugin-vue": "^4.3.4", 48 | "@vitejs/plugin-vue-jsx": "^3.0.2", 49 | "autoprefixer": "^10.4.16", 50 | "axios": "^1.5.0", 51 | "date-fns": "^2.30.0", 52 | "echarts": "^5.4.3", 53 | "eslint": "^8.50.0", 54 | "eslint-plugin-vue": "^9.17.0", 55 | "fast-glob": "^3.3.1", 56 | "fs-extra": "^11.1.1", 57 | "husky": "^8.0.3", 58 | "lint-staged": "^13.3.0", 59 | "local-pkg": "^0.4.3", 60 | "md-editor-v3": "^2.11.3", 61 | "mockjs": "^1.1.0", 62 | "pinia-plugin-persistedstate": "^3.2.0", 63 | "plop": "^3.1.2", 64 | "postcss-preset-env": "^7.8.3", 65 | "postcss-pxtorem": "^6.0.0", 66 | "prettier": "^2.8.8", 67 | "rollup-plugin-visualizer": "^5.9.2", 68 | "terser": "^5.20.0", 69 | "typescript": "4.9.4", 70 | "unocss": "^0.47.6", 71 | "unplugin-auto-import": "^0.12.2", 72 | "unplugin-vue-components": "^0.22.12", 73 | "vite": "^4.4.9", 74 | "vite-plugin-compression": "^0.5.1", 75 | "vite-plugin-eslint": "^1.8.1", 76 | "vite-plugin-inspect": "^0.7.38", 77 | "vite-plugin-mock": "^2.9.8", 78 | "vite-plugin-pages": "^0.28.0", 79 | "vite-plugin-pwa": "^0.16.5", 80 | "vite-plugin-removelog": "^0.2.1", 81 | "vite-plugin-restart": "^0.2.0", 82 | "vite-plugin-vue-devtools": "^0.3.2", 83 | "vite-plugin-vue-meta-layouts": "^0.1.2", 84 | "vue-echarts": "^6.6.1", 85 | "vue-eslint-parser": "^9.3.1", 86 | "vue-i18n": "^9.4.1" 87 | }, 88 | "pnpm": { 89 | "peerDependencyRules": { 90 | "ignoreMissing": [ 91 | "rollup", 92 | "markdown-it", 93 | "@types/markdown-it" 94 | ] 95 | } 96 | }, 97 | "lint-staged": { 98 | "*.{js,jsx,vue,ts,tsx}": "npm run lint:fix" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/components/Drawer/index.vue: -------------------------------------------------------------------------------- 1 | 107 | 108 | 113 | -------------------------------------------------------------------------------- /src/components/Layout/MultiWindow/setupMultiWindow.ts: -------------------------------------------------------------------------------- 1 | import { type AsyncComponentLoader, KeepAlive, h, provide } from 'vue' 2 | import { type RouteRecordRaw, onBeforeRouteUpdate } from 'vue-router' 3 | import Loading from '../Loading' 4 | import { type UseStore } from './store' 5 | import { baseComponentKey, injectionKey } from '.' 6 | 7 | export function createMultiWindowComponentWrap( 8 | routeRaw: RouteRecordRaw, 9 | useStore: UseStore, 10 | ) { 11 | if (!routeRaw.component) 12 | return 13 | 14 | if (!routeRaw.meta) 15 | routeRaw.meta = {} 16 | routeRaw.meta[baseComponentKey] = defineAsyncComponent( 17 | { 18 | loader: routeRaw.component as AsyncComponentLoader, 19 | delay: 0, 20 | // timeout: 240000, 21 | loadingComponent: Loading, 22 | }, 23 | ) 24 | 25 | const name = routeRaw.name as string 26 | 27 | /* eslint-disable vue/one-component-per-file */ 28 | routeRaw.component = defineComponent({ 29 | name, 30 | setup() { 31 | const route = useRoute() 32 | const store = useStore() 33 | const { openWindow } = store 34 | const currentWindow = ref(openWindow(route)) 35 | 36 | onActivated(async () => { 37 | currentWindow.value = openWindow(route) 38 | }) 39 | 40 | onBeforeRouteUpdate((to) => { 41 | watchOnce( 42 | () => route.fullPath, 43 | (fullPath: string) => { 44 | if (fullPath === to.fullPath) 45 | currentWindow.value = openWindow(to) 46 | }, 47 | ) 48 | }) 49 | 50 | const keepAliveInclude = computed(() => { 51 | return store.windows 52 | .filter(window => window.componentName === name) 53 | .map(window => `${window.fullPath}-${window.refreshKey}`) 54 | }) 55 | 56 | provide(injectionKey, { 57 | window: currentWindow, 58 | store, 59 | }) 60 | 61 | return () => { 62 | const { component } = currentWindow.value 63 | if (!component) 64 | return null 65 | 66 | return h( 67 | KeepAlive, 68 | { 69 | include: keepAliveInclude.value, 70 | }, h(component), 71 | ) 72 | } 73 | }, 74 | }) 75 | } 76 | 77 | function createUseStore(useStore: UseStore): UseStore { 78 | let store: ReturnType 79 | return () => { 80 | if (!store) 81 | store = useStore() 82 | return store 83 | } 84 | } 85 | 86 | export function setupMultiWindow(routes: RouteRecordRaw[], useStore: any): any { 87 | const useStoreProxy = createUseStore(useStore) 88 | routes.forEach((route) => { 89 | if (route.path !== '/' && route.children) { 90 | route.children.forEach((r: RouteRecordRaw) => { 91 | const meta = r.meta || {} 92 | const { layout = 'Home', multiWindow = true } = meta 93 | meta.multiWindow = multiWindow 94 | r.meta = meta 95 | if (multiWindow && layout === 'Home') { 96 | createMultiWindowComponentWrap(r, useStoreProxy) 97 | } 98 | else { 99 | const component = r.component 100 | if (component) { 101 | r.component = defineComponent({ 102 | render() { 103 | return h('div', { class: 'not-keep' }, h(defineAsyncComponent({ 104 | loader: component as AsyncComponentLoader, 105 | }))) 106 | }, 107 | }) 108 | } 109 | } 110 | }) 111 | } 112 | }) 113 | } 114 | -------------------------------------------------------------------------------- /src/pages/example/layout/table-form/table.ts: -------------------------------------------------------------------------------- 1 | import { NTag } from 'naive-ui' 2 | import { mockLists } from '@/apis/test' 3 | 4 | export function useTable() { 5 | const table = useDataTable() 6 | 7 | // 过滤器 8 | const showFilterDrawer = ref(false) 9 | 10 | // 表格配置 11 | const { setRowKey, setColumns, setRequest, run, setToolbar, setAction, setProps } = table 12 | 13 | // 设置行主键 14 | setRowKey('id') 15 | 16 | // 请求数据 17 | setRequest( 18 | (param: any) => { 19 | return mockLists(param) 20 | }, 21 | ) 22 | 23 | const filterOptions = ref({ 24 | type: 'default', 25 | quaternary: true, 26 | }) 27 | 28 | // 工具栏 29 | setToolbar([{ 30 | title: '过滤', 31 | icon: 'i-bx-filter', 32 | options: filterOptions, 33 | action: () => { 34 | // 修改过滤按钮样式 35 | filterOptions.value.type = 'primary' 36 | filterOptions.value.quaternary = false 37 | showFilterDrawer.value = true 38 | }, 39 | }, 'new', 'refresh', 'columns', { 40 | title: '导出', 41 | icon: 'i-bx:export', 42 | action: () => { 43 | window.$message.info('点击了导出') 44 | }, 45 | }]) 46 | 47 | // 自定义渲染操作栏 48 | // setAction((row) => { 49 | // return h('div', '定义操作栏') 50 | // }) 51 | 52 | // 自定义渲染整个操作栏(包括头部) 53 | // setAction({ 54 | // title: '操作', 55 | // key: 'more', 56 | // align: 'center', 57 | // render(row) { 58 | // return h('div', '自定义操作') 59 | // }, 60 | // }) 61 | 62 | setAction([ 63 | { 64 | text: '其他', 65 | type: 'warning', 66 | key: 'other', 67 | }, 68 | ]) 69 | 70 | setColumns([ 71 | { 72 | title: '编号', 73 | key: 'id', 74 | width: 70, 75 | align: 'center', 76 | }, 77 | { 78 | title: '用户名', 79 | key: 'account', 80 | editableUpdate: (row, value, index) => { 81 | log.debug(row, `修改后的值:${value}, 索引:${index}`) 82 | row.account = value 83 | window.$message.success(`修改了用户名:${value}`) 84 | }, 85 | ellipsis: { 86 | tooltip: true, 87 | }, 88 | }, 89 | { 90 | title: '邮箱', 91 | key: 'email', 92 | ellipsis: { 93 | tooltip: true, 94 | }, 95 | }, 96 | { 97 | key: 'status', 98 | width: 80, 99 | title: '状态', 100 | filterMultiple: false, 101 | defaultFilterOptionValue: 0, 102 | filterOptions: [ 103 | { 104 | label: '全部状态', 105 | value: 0, 106 | }, 107 | { 108 | label: '正常使用中', 109 | value: 1, 110 | }, 111 | { 112 | label: '已禁止使用', 113 | value: 2, 114 | }, 115 | ], 116 | filter: true, 117 | render(rowData) { 118 | const v: { [key: string]: any } = { 119 | title: '禁用', 120 | props: { 121 | size: 'small', 122 | }, 123 | } 124 | if (rowData.status === 1) { 125 | v.title = '正常' 126 | v.props.type = 'success' 127 | } 128 | 129 | return h(NTag, v.props, { default: () => v.title }) 130 | }, 131 | }, 132 | { 133 | title: '创建时间', 134 | key: 'created_at', 135 | ellipsis: { 136 | tooltip: true, 137 | }, 138 | }, 139 | ]) 140 | 141 | setProps({ 142 | 'onUpdate:filters': (filters: { [key: string]: any }) => { 143 | window.$message.success(`点击了过滤状态:${filters.status}`) 144 | run(filters.status ? filters : {}) 145 | }, 146 | }) 147 | 148 | return { ...table, showFilterDrawer } 149 | } 150 | -------------------------------------------------------------------------------- /src/components/DrawerForm/index.vue: -------------------------------------------------------------------------------- 1 | 118 | -------------------------------------------------------------------------------- /src/pages/example/menu.ts: -------------------------------------------------------------------------------- 1 | import type { StMenu } from '@/types/global' 2 | 3 | export const locales = { 4 | en: { 5 | readme: 'Readme', 6 | }, 7 | zh: { 8 | readme: '说明文档', 9 | }, 10 | } 11 | 12 | export default [ 13 | { 14 | icon: 'i-bx:slideshow', 15 | i18n: { 16 | en: 'Example', 17 | zh: '示例页面', 18 | }, 19 | path: '/example', 20 | children: [ 21 | { 22 | show: true, 23 | path: '/example/readme', 24 | }, 25 | { 26 | type: 'group', 27 | icon: 'i-bx:layout', 28 | i18n: { 29 | en: 'Layout', 30 | zh: '布局页面', 31 | }, 32 | children: [ 33 | { 34 | icon: 'i-bx:dock-top', 35 | path: '/example/layout/basic', 36 | }, 37 | { 38 | icon: 'i-bx:columns', 39 | path: '/example/layout/multi-column', 40 | }, 41 | { icon: 'i-bx:list-ol', path: '/example/layout/table' }, 42 | { 43 | icon: 'i-bx:table', 44 | path: '/example/layout/table-form', 45 | i18n: { 46 | en: 'FunctionTable', 47 | zh: '功能表格', 48 | }, 49 | children: [ 50 | { path: '/example/layout/table-form/f1' }, 51 | { path: '/example/layout/table-form/f2' }, 52 | { path: '/example/layout/table-form/f3' }, 53 | ], 54 | }, 55 | { 56 | icon: 'i-bx:pencil', 57 | path: '/example/layout/form-basic', 58 | }, 59 | { 60 | icon: 'i-bx:paint', 61 | path: '/example/layout/form-advanced', 62 | }, 63 | { 64 | icon: 'i-bx:bxs-edit-alt', 65 | path: '/example/layout/form-dynamic', 66 | }, 67 | { 68 | icon: 'i-bx-windows', 69 | path: '/example/layout/form-drawer', 70 | }, 71 | ], 72 | }, 73 | { 74 | type: 'group', 75 | icon: 'i-bx:bxs-customize', 76 | path: '/example/function', 77 | i18n: { 78 | en: 'Function', 79 | zh: '功能示例', 80 | }, 81 | children: [ 82 | { 83 | icon: 'i-bx:log-out', 84 | path: '/example/function/not-keep', 85 | }, 86 | { 87 | icon: 'i-bx:clipboard', 88 | path: '/example/function/clipboard', 89 | }, 90 | { 91 | icon: 'i-bx-world', 92 | path: '/example/function/locale', 93 | }, 94 | { 95 | icon: 'i-bx:comment-dots', 96 | path: '/example/function/notice', 97 | }, 98 | { 99 | icon: 'i-bx:bar-chart-alt-2', 100 | path: '/example/function/echart', 101 | }, 102 | { 103 | path: '/example/function/fullscreen', 104 | icon: 'i-bx:fullscreen', 105 | }, 106 | { 107 | icon: 'i-bx:bxs-file-md', 108 | path: '/example/function/markdown', 109 | }, 110 | { 111 | icon: 'i-bx:message-alt-error', 112 | path: '/example/function/permission', 113 | }, 114 | { 115 | icon: 'i-bx:message-alt-error', 116 | path: '/example/function/permission/admin', 117 | show: false, 118 | }, 119 | ], 120 | }, 121 | { 122 | icon: 'i-bx:bxl-github', 123 | title: '外部链接', 124 | path: '', 125 | url: 'https://github.com/sohaha', 126 | }, 127 | ], 128 | }, 129 | ] 130 | -------------------------------------------------------------------------------- /src/pages/example/function/notice.vue: -------------------------------------------------------------------------------- 1 | 94 | 95 | 139 | 140 | 141 | 142 | 143 | { 144 | "meta": { 145 | "i18n": { "en": "Notice", "zh": "信息通知" } 146 | } 147 | } 148 | 149 | -------------------------------------------------------------------------------- /src/layouts/Home.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 116 | 117 | 132 | -------------------------------------------------------------------------------- /mock/base.ts: -------------------------------------------------------------------------------- 1 | import { info } from './user' 2 | 3 | let account = '' 4 | 5 | export default [ 6 | { 7 | url: '/manage/base/login', 8 | timeout: 0, 9 | method: 'post', 10 | response: ({ body }: { body: any }) => { 11 | let result = {} 12 | account = body.account || '' 13 | switch (body?.password) { 14 | case '123456': 15 | result = { 16 | code: 0, 17 | msg: '登录成功', 18 | data: { 19 | token: '@string', 20 | ...info(), 21 | }, 22 | } 23 | break 24 | default: 25 | result = { 26 | code: 400, 27 | msg: '用户名/密码错误', 28 | } 29 | } 30 | return result 31 | }, 32 | }, 33 | { 34 | url: '/manage/base/logout', 35 | method: 'post', 36 | response: () => { 37 | return { code: 0, msg: '退出成功' } 38 | }, 39 | }, 40 | { 41 | url: '/manage/base/me', 42 | method: 'get', 43 | statusCode: 200, 44 | timeout: 10, 45 | response: () => { 46 | const data: any = { 47 | info: info(), 48 | alone_menu: [ 49 | { 50 | title: '动态菜单', 51 | redirect: '/example/readme', 52 | }, 53 | ], 54 | } 55 | 56 | if (account === 'manage') 57 | data.permission = ['admin'] 58 | else 59 | data.permission = ['user'] 60 | 61 | data.info.avatar = 'data:image/svg+xml,%3Csvg viewBox=\'0 0 36 36\' fill=\'none\' role=\'img\' xmlns=\'http://www.w3.org/2000/svg\' width=\'128\' height=\'128\'%3E%3Ctitle%3EMary Roebling%3C/title%3E%3Cmask id=\'mask__beam\' maskUnits=\'userSpaceOnUse\' x=\'0\' y=\'0\' width=\'36\' height=\'36\'%3E%3Crect width=\'36\' height=\'36\' fill=\'%23FFFFFF\'%3E%3C/rect%3E%3C/mask%3E%3Cg mask=\'url(%23mask__beam)\'%3E%3Crect width=\'36\' height=\'36\' fill=\'%23f0f0d8\'%3E%3C/rect%3E%3Crect x=\'0\' y=\'0\' width=\'36\' height=\'36\' transform=\'translate(5 -1) rotate(155 18 18) scale(1.2)\' fill=\'%23000000\' rx=\'6\'%3E%3C/rect%3E%3Cg transform=\'translate(3 -4) rotate(-5 18 18)\'%3E%3Cpath d=\'M15 21c2 1 4 1 6 0\' stroke=\'%23FFFFFF\' fill=\'none\' stroke-linecap=\'round\'%3E%3C/path%3E%3Crect x=\'14\' y=\'14\' width=\'1.5\' height=\'2\' rx=\'1\' stroke=\'none\' fill=\'%23FFFFFF\'%3E%3C/rect%3E%3Crect x=\'20\' y=\'14\' width=\'1.5\' height=\'2\' rx=\'1\' stroke=\'none\' fill=\'%23FFFFFF\'%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E' 62 | return { 63 | code: 0, 64 | msg: '当前用户信息', 65 | data, 66 | } 67 | }, 68 | }, 69 | { 70 | url: '/manage/base/message', 71 | method: 'get', 72 | timeout: 0, 73 | response: () => { 74 | return { code: 0, msg: '获取未读信息', data: { unread: 100 } } 75 | }, 76 | }, 77 | { 78 | url: '/manage/base/site', 79 | method: 'get', 80 | timeout: 0, 81 | response: () => { 82 | return { code: 0, msg: '系统信息', data: {} } 83 | }, 84 | }, 85 | { 86 | url: '/manage/base/password', 87 | method: 'put', 88 | response: () => { 89 | return { code: 0, msg: '更新成功' } 90 | }, 91 | }, 92 | { 93 | url: '/manage/base/logs', 94 | method: 'get', 95 | response: (v) => { 96 | const { query } = v 97 | return { 98 | code: 0, 99 | data: { 100 | page: { 101 | total: 100, 102 | curpage: query.page, 103 | }, 104 | [`items|${query.pagesize}`]: [ 105 | { 106 | 'id|+1': query.pagesize * (query.page - 1) + 1, 107 | ...logsFake(), 108 | }, 109 | ], 110 | }, 111 | } 112 | }, 113 | }, 114 | ] 115 | 116 | const logsFake = () => { 117 | return { 118 | _id: '@id', 119 | action: '@pick([\'登录\', \'退出\', \'添加\', \'删除\', \'修改\'])', 120 | os: '@pick(["Windows", "MacOS", "android"])', 121 | path: '@url', 122 | result: '@pick([1,2])', 123 | nickname: '@cname', 124 | module: '@pick(["用户管理", "角色管理", "菜单管理", "日志管理", "系统设置"])', 125 | os_version: '', 126 | method: '@pick(["GET", "POST", "PUT", "DELETE"])', 127 | browser: 'Edge', 128 | browser_version: '108.0.1462.46', 129 | ip: '@ip', 130 | ip_region: '@county', 131 | roles_name: ['管理员'], 132 | created_at: '@date("yyyy-MM-dd HH:mm:ss")', 133 | updated_at: '@datetime', 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/apis/index.ts: -------------------------------------------------------------------------------- 1 | import type { CreateAxiosDefaults } from 'axios' 2 | import axios from 'axios' 3 | 4 | export const lock = new ReadWriteLock() 5 | export const baseURL = (import.meta.env.DEV && import.meta.env.VITE_DEV_PROXY === 'true') 6 | ? '/proxy' 7 | : (import.meta.env.VITE_APP_API_BASEURL as string) 8 | 9 | const dynamicBaseURL = usePrefixStorage('baseURL', '') 10 | const conf: CreateAxiosDefaults = { 11 | baseURL, 12 | timeout: import.meta.env.VITE_APP_API_TIMEOUT || 10000, 13 | responseType: 'json', 14 | } 15 | const api = axios.create(conf) 16 | 17 | export function getBaseURL() { 18 | return api.defaults.baseURL 19 | } 20 | 21 | if (import.meta.env.VITE_APP_API_BASEURL_DYNAMIC === 'true') { 22 | if (dynamicBaseURL.value) 23 | api.defaults.baseURL = dynamicBaseURL.value 24 | 25 | let u = getQuery('__') 26 | try { 27 | u = window.atob(u || '') 28 | } 29 | catch (error) { } 30 | if (u) 31 | dynamicBaseURL.value = api.defaults.baseURL = u 32 | } 33 | 34 | api.interceptors.request.use(async (request: any) => { 35 | await lock.lockRead() 36 | return before(request) 37 | }) 38 | 39 | api.interceptors.response.use( 40 | (response: any) => { 41 | lock.unlockRead() 42 | return after(response) 43 | }, 44 | async (err: any) => { 45 | await lock.unlockRead() 46 | return afterError(err) 47 | }, 48 | ) 49 | 50 | export default api 51 | 52 | export const apiNoILock = axios.create(conf) 53 | apiNoILock.interceptors.request.use(before) 54 | 55 | apiNoILock.interceptors.response.use(after, afterError) 56 | 57 | const throttledFn = useThrottleFn(() => { 58 | const user = userStore() 59 | window?.$message?.error(user.getToken ? '登录状态已失效,请重新登录' : '请先登录') 60 | user.setToken('') 61 | window.$router.push({ 62 | path: '/login', 63 | query: { 64 | redirect: window.$router?.currentRoute.value.fullPath, 65 | }, 66 | }) 67 | }, 5000) 68 | 69 | function errorHandler(status: number, error: Error) { 70 | if (status === 0) 71 | window?.$message?.error(error.message) 72 | else 73 | console.error(status, error) 74 | } 75 | 76 | function before(request: any) { 77 | const token = userStore().getToken 78 | if (token) { 79 | try { 80 | request.headers.Authorization = `Basic ${token}` 81 | } 82 | catch (e) { 83 | request.headers.Authorization = token 84 | } 85 | } 86 | 87 | if (!request.headers['Content-Type']) { 88 | if (typeof request.data === 'string' && request.method !== 'post') 89 | request.headers['Content-Type'] = 'text/plain' 90 | else if (typeof request.data === 'object') 91 | request.headers['Content-Type'] = 'application/json;charset=UTF-8' 92 | } 93 | 94 | return request 95 | } 96 | 97 | function after(response: any) { 98 | const { data, headers } = response 99 | let errMessage = '' 100 | if (data && typeof data !== 'object') 101 | errMessage = '接口返回数据非 JSON 格式' 102 | 103 | if (headers && headers['re-token']) 104 | userStore().setToken(headers['re-token']) 105 | 106 | if (data?.code === undefined) 107 | errMessage = '非合法接口格式' 108 | 109 | if (errMessage !== '') { 110 | const err = Error(errMessage) 111 | errorHandler(0, err) 112 | return Promise.reject() 113 | } 114 | return data 115 | } 116 | 117 | function afterError(err: any) { 118 | const response = err?.response || {} 119 | const { status = 0, data = {} } = response 120 | const cause = {} 121 | let message = err?.message 122 | const dataMsg = data?.msg || '' 123 | 124 | switch (status) { 125 | case 400: 126 | message = dataMsg || '输入不合法' 127 | // return Promise.reject(Error(message, cause)) 128 | break 129 | case 401: 130 | throttledFn() 131 | return Promise.reject() 132 | case 403: 133 | message = '权限不足' 134 | break 135 | case 404: 136 | message = dataMsg || '接口不存在' 137 | break 138 | case 500: 139 | message = '服务端错误' 140 | if (dataMsg) 141 | message += `: ${data.msg}` 142 | break 143 | default: 144 | if (message === 'Network Error') 145 | message = '网络故障' 146 | else if (message.includes('timeout')) 147 | message = '接口请求超时' 148 | else if (message.includes('Request failed with status code')) 149 | message = `接口 ${message.slice(message.length - 3)} 异常` 150 | } 151 | errorHandler(0, Error(message, cause)) 152 | 153 | return Promise.reject() 154 | } 155 | -------------------------------------------------------------------------------- /src/pages/system/role.vue: -------------------------------------------------------------------------------- 1 | 147 | 148 | 174 | 175 | 177 | 178 | 179 | { "meta": { "title": "角色管理" } } 180 | 181 | -------------------------------------------------------------------------------- /src/types/components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/core/pull/3399 4 | import '@vue/runtime-core' 5 | 6 | export {} 7 | 8 | declare module '@vue/runtime-core' { 9 | export interface GlobalComponents { 10 | ACard: typeof import('ayyui')['ACard'] 11 | Card: typeof import('./../components/Card')['default'] 12 | CardCols: typeof import('./../components/CardCols.vue')['default'] 13 | CardRows: typeof import('./../components/CardRows.vue')['default'] 14 | DataForm: typeof import('./../components/DataForm/index.vue')['default'] 15 | DataTable: typeof import('./../components/DataTable/index')['default'] 16 | DataTableComponentsColumnSetting: typeof import('./../components/DataTable/components/ColumnSetting.vue')['default'] 17 | Drawer: typeof import('./../components/Drawer/index.vue')['default'] 18 | DrawerForm: typeof import('./../components/DrawerForm/index.vue')['default'] 19 | LayoutHeader: typeof import('./../components/Layout/Header.vue')['default'] 20 | LayoutHeaderDark: typeof import('./../components/Layout/Header/Dark.vue')['default'] 21 | LayoutHeaderFullscreen: typeof import('./../components/Layout/Header/Fullscreen.vue')['default'] 22 | LayoutHeaderMessage: typeof import('./../components/Layout/Header/Message.vue')['default'] 23 | LayoutHeaderNetwork: typeof import('./../components/Layout/Header/Network.vue')['default'] 24 | LayoutHeaderTabs: typeof import('./../components/Layout/Header/Tabs.vue')['default'] 25 | LayoutHeaderTips: typeof import('./../components/Layout/Header/Tips.vue')['default'] 26 | LayoutHeaderTranslate: typeof import('./../components/Layout/Header/Translate.vue')['default'] 27 | LayoutHeaderWindow: typeof import('./../components/Layout/Header/Window.vue')['default'] 28 | LayoutLoading: typeof import('./../components/Layout/Loading')['default'] 29 | LayoutLoadingContent: typeof import('./../components/Layout/LoadingContent.vue')['default'] 30 | LayoutMenu: typeof import('./../components/Layout/Menu.vue')['default'] 31 | LayoutMultiWindow: typeof import('./../components/Layout/MultiWindow/index.vue')['default'] 32 | LayoutProvider: typeof import('./../components/Layout/Provider.vue')['default'] 33 | LayoutUser: typeof import('./../components/Layout/User.vue')['default'] 34 | MdEditor: typeof import('./../components/MdEditor/index')['default'] 35 | NAlert: typeof import('naive-ui')['NAlert'] 36 | NAvatar: typeof import('naive-ui')['NAvatar'] 37 | NBackTop: typeof import('naive-ui')['NBackTop'] 38 | NBadge: typeof import('naive-ui')['NBadge'] 39 | NButton: typeof import('naive-ui')['NButton'] 40 | NCheckbox: typeof import('naive-ui')['NCheckbox'] 41 | NCheckboxGroup: typeof import('naive-ui')['NCheckboxGroup'] 42 | NConfigProvider: typeof import('naive-ui')['NConfigProvider'] 43 | NDialogProvider: typeof import('naive-ui')['NDialogProvider'] 44 | NDivider: typeof import('naive-ui')['NDivider'] 45 | NDropdown: typeof import('naive-ui')['NDropdown'] 46 | NElement: typeof import('naive-ui')['NElement'] 47 | NEmpty: typeof import('naive-ui')['NEmpty'] 48 | NForm: typeof import('naive-ui')['NForm'] 49 | NFormItem: typeof import('naive-ui')['NFormItem'] 50 | NIcon: typeof import('naive-ui')['NIcon'] 51 | NInput: typeof import('naive-ui')['NInput'] 52 | NLayout: typeof import('naive-ui')['NLayout'] 53 | NLayoutContent: typeof import('naive-ui')['NLayoutContent'] 54 | NLayoutSider: typeof import('naive-ui')['NLayoutSider'] 55 | NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider'] 56 | NMenu: typeof import('naive-ui')['NMenu'] 57 | NMessageProvider: typeof import('naive-ui')['NMessageProvider'] 58 | NModal: typeof import('naive-ui')['NModal'] 59 | NNotificationProvider: typeof import('naive-ui')['NNotificationProvider'] 60 | NPopover: typeof import('naive-ui')['NPopover'] 61 | NRadioButton: typeof import('naive-ui')['NRadioButton'] 62 | NRadioGroup: typeof import('naive-ui')['NRadioGroup'] 63 | NResult: typeof import('naive-ui')['NResult'] 64 | NScrollbar: typeof import('naive-ui')['NScrollbar'] 65 | NSkeleton: typeof import('naive-ui')['NSkeleton'] 66 | NSpin: typeof import('naive-ui')['NSpin'] 67 | NSwitch: typeof import('naive-ui')['NSwitch'] 68 | NTab: typeof import('naive-ui')['NTab'] 69 | NTabPane: typeof import('naive-ui')['NTabPane'] 70 | NTabs: typeof import('naive-ui')['NTabs'] 71 | NThing: typeof import('naive-ui')['NThing'] 72 | NTooltip: typeof import('naive-ui')['NTooltip'] 73 | RouterLink: typeof import('vue-router')['RouterLink'] 74 | RouterView: typeof import('vue-router')['RouterView'] 75 | VChart: typeof import('./../components/VChart/index')['default'] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.autoImportFileExcludePatterns": [ 3 | "vue-router" 4 | ], 5 | "i18n-ally.localesPaths": [ 6 | "locales" 7 | ], 8 | "i18n-ally.keystyle": "nested", 9 | "i18n-ally.sourceLanguage": "zh", 10 | "css.lint.unknownAtRules": "ignore", 11 | // 开启 eslint 12 | "eslint.enable": true, 13 | // 开启 prettier 14 | "prettier.enable": true, 15 | "editor.codeActionsOnSave": { 16 | "source.fixAll.eslint": true 17 | }, 18 | "editor.linkedEditing": true, 19 | // 开启 eslint 作为格式化工具 20 | "eslint.format.enable": true, 21 | // 默认格式化选择 prettier, 22 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 23 | "[jsonc]": { 24 | "editor.defaultFormatter": "vscode.json-language-features" 25 | }, 26 | "[vue]": { 27 | "editor.defaultFormatter": "Vue.volar" 28 | }, 29 | "[css]": { 30 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" 31 | }, 32 | "[typescript]": { 33 | "editor.defaultFormatter": "vscode.typescript-language-features" 34 | }, 35 | "[javascript]": { 36 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" 37 | }, 38 | "[yaml]": { 39 | "editor.defaultFormatter": "esbenp.prettier-vscode" 40 | }, 41 | "i18n-ally.sortKeys": true, 42 | "i18n-ally.keepFulfilled": true, 43 | "explorer.fileNesting.enabled": true, 44 | "explorer.fileNesting.expand": false, 45 | "explorer.fileNesting.patterns": { 46 | "*.component.ts": "$(capture).component.html, $(capture).component.spec.ts, $(capture).component.css, $(capture).component.scss, $(capture).component.sass, $(capture).component.less", 47 | "*.css": "$(capture).css.map, $(capture).*.css", 48 | "*.js": "$(capture).js.map, $(capture).*.js", 49 | "*.jsx": "$(capture).js, $(capture).*.jsx", 50 | "*.module.ts": "$(capture).resolver.ts, $(capture).controller.ts, $(capture).service.ts", 51 | "*.ts": "$(capture).js, $(capture).*.ts", 52 | "*.tsx": "$(capture).ts, $(capture).*.tsx", 53 | "*.vue": "$(capture).*.ts, $(capture).*.js", 54 | ".env": "*.env, .env.*, .envrc, env.d.ts", 55 | "package.json": ".browserslist*, .circleci*, .codecov, .commitlint*, .cz-config.js, .czrc, .editorconfig, .eslint*, .firebase*, .flowconfig, .github*, .gitlab*, .gitpod*, .huskyrc*, .jslint*, .lighthouserc.*, .lintstagedrc*, .markdownlint*, .mocha*, .node-version, .nodemon*, .npm*, .nvmrc, .pm2*, .pnp.*, .pnpm*, .prettier*, .releaserc*, .sentry*, .stackblitz*, .styleci*, .stylelint*, .tazerc*, .textlint*, .tool-versions, .travis*, .versionrc*, .vscode*, .watchman*, .xo-config*, .yamllint*, .yarnrc*, Procfile, api-extractor.json, apollo.config.*, appveyor*, ava.config.*, azure-pipelines*, bower.json, build.config.*, commitlint*, crowdin*, cypress.json, dangerfile*, dprint.json, firebase.json, grunt*, gulp*, jasmine.*, jenkins*, jest.config.*, jsconfig.*, karma*, lerna*, lighthouserc.*, lint-staged*, nest-cli.*, netlify*, nodemon*, nx.*, package-lock.json, phpcs.xml, playwright.config.*, pm2.*, pnpm*, prettier*, pullapprove*, puppeteer.config.*, pyrightconfig.json, renovate*, rollup.config.*, stylelint*, tsconfig.*, tsdoc.*, tslint*, tsup.config.*, turbo*, typedoc*, vercel*, vetur.config.*, vitest.config.*, webpack.config.*, workspace.json, xo.config.*, yarn*", 56 | "readme*": "authors, backers.md, changelog*, citation*, code_of_conduct.md, codeowners, contributing.md, contributors, copying, credits, governance.md, history.md, license*, maintainers, readme*, security.md, sponsors.md", 57 | "vite.config.*": ".babelrc*, .codecov, .cssnanorc*,.htmlnanorc*, .lighthouserc.*, .mocha*, .postcssrc*, .terserrc*, api-extractor.json, ava.config.*, babel.config.*, cssnano.config.*, cypress.json, env.d.ts, htmlnanorc.*, jasmine.*, jest.config.*, jsconfig.*, karma*, lighthouserc.*, playwright.config.*, postcss.config.*, puppeteer.config.*, svgo.config.*, tailwind.config.*, tsconfig.*, tsdoc.*, unocss.config.*, vitest.config.*, webpack.config.*, windi.config.*" 58 | }, 59 | "peacock.remoteColor": "#59444b", 60 | "workbench.colorCustomizations": { 61 | "activityBar.activeBackground": "#765a63", 62 | "activityBar.background": "#765a63", 63 | "activityBar.foreground": "#e7e7e7", 64 | "activityBar.inactiveForeground": "#e7e7e799", 65 | "activityBarBadge.background": "#5ca738", 66 | "activityBarBadge.foreground": "#15202b", 67 | "commandCenter.border": "#e7e7e799", 68 | "sash.hoverBorder": "#765a63", 69 | "statusBar.background": "#59444b", 70 | "statusBar.foreground": "#e7e7e7", 71 | "statusBarItem.hoverBackground": "#765a63", 72 | "statusBarItem.remoteBackground": "#59444b", 73 | "statusBarItem.remoteForeground": "#e7e7e7", 74 | "titleBar.activeBackground": "#59444b", 75 | "titleBar.activeForeground": "#e7e7e7", 76 | "titleBar.inactiveBackground": "#59444b99", 77 | "titleBar.inactiveForeground": "#e7e7e799" 78 | }, 79 | "peacock.color": "#59444b", 80 | "[json]": { 81 | "editor.defaultFormatter": "rvest.vs-code-prettier-eslint" 82 | }, 83 | "[typescriptreact]": { 84 | "editor.defaultFormatter": "vscode.typescript-language-features" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/pages/inlay/message.vue: -------------------------------------------------------------------------------- 1 | 164 | 165 | 179 | 180 | 183 | 184 | 185 | { 186 | "meta": { 187 | "multiWindow": true, 188 | "activate": "/", 189 | "title": "消息中心" 190 | } 191 | } 192 | 193 | --------------------------------------------------------------------------------