├── .vercelignore ├── src ├── hooks │ ├── index.ts │ └── fullscreen.ts ├── store │ ├── types │ │ └── index.d.ts │ ├── index.ts │ └── modules │ │ ├── setting.ts │ │ └── tagsView.ts ├── views │ ├── list │ │ ├── card │ │ │ └── card.tsx │ │ └── search-table │ │ │ └── search-table.tsx │ ├── profile │ │ └── basic │ │ │ └── basic.tsx │ ├── document │ │ └── document.tsx │ ├── dashboard │ │ ├── monitor │ │ │ └── monitor.tsx │ │ └── workplace │ │ │ └── workplace.tsx │ ├── home │ │ └── home.tsx │ ├── login.tsx │ └── base-component │ │ └── baseComponent.tsx ├── components │ ├── layouts │ │ ├── style │ │ │ ├── header.module.less │ │ │ └── logo.module.less │ │ ├── BaseLogo.tsx │ │ ├── AppMain.tsx │ │ ├── BaseSider.tsx │ │ ├── Menu.tsx │ │ └── BaseHeader.tsx │ ├── TriggerCollapse │ │ ├── style │ │ │ └── index.module.less │ │ └── index.tsx │ ├── DButton.tsx │ ├── TipIcon.tsx │ ├── Breadcrumb │ │ └── index.tsx │ ├── RightTool │ │ ├── DragItem.tsx │ │ └── index.tsx │ ├── GlobalProvider.tsx │ ├── TagsView │ │ ├── useTagsView.ts │ │ ├── index.tsx │ │ └── useDropdown.ts │ └── GlobalDraw.tsx ├── layout │ ├── BlankLayout.tsx │ └── PageLayout.tsx ├── router │ ├── modules │ │ ├── login.ts │ │ ├── index.ts │ │ ├── base.ts │ │ ├── document.ts │ │ ├── home.ts │ │ ├── profile.ts │ │ ├── dashboard.ts │ │ └── list.ts │ ├── routes.ts │ ├── typings.d.ts │ └── index.ts ├── utils │ └── index.tsx ├── assets │ ├── logo.svg │ └── base.css ├── style │ └── base.less ├── main.ts ├── App.tsx ├── settings.ts └── auto-imports.d.ts ├── vercel.json ├── public └── favicon.ico ├── .eslintignore ├── .prettierignore ├── .vscode └── extensions.json ├── .prettierrc.js ├── tsconfig.config.json ├── .editorconfig ├── index.html ├── env.d.ts ├── .gitignore ├── md ├── hmr.md ├── 笔记.md └── note.md ├── tsconfig.json ├── vite.config.ts ├── package.json ├── .eslintrc.js └── README.md /.vercelignore: -------------------------------------------------------------------------------- 1 | README.md 2 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fullscreen' 2 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "rewrites": [{ "source": "/:path*", "destination": "/index.html" }] 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WalkAlone0325/tsx-naive-admin/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | pnpm-lock.yaml 4 | src/components.d.ts 5 | src/auto-import.d.ts 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | pnpm-lock.yaml 4 | src/components.d.ts 5 | src/auto-import.d.ts 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | trailingComma: 'none', 5 | endOfLine: 'lf' 6 | } 7 | -------------------------------------------------------------------------------- /src/store/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { RouteMeta } from 'vue-router' 2 | 3 | export interface TagView extends RouteMeta { 4 | fullPath: string 5 | [key: string]: any 6 | } 7 | -------------------------------------------------------------------------------- /src/views/list/card/card.tsx: -------------------------------------------------------------------------------- 1 | const CardView = defineComponent({ 2 | name: 'CardView', 3 | setup() { 4 | return () =>
CardView
5 | } 6 | }) 7 | 8 | export default CardView 9 | -------------------------------------------------------------------------------- /tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /src/views/profile/basic/basic.tsx: -------------------------------------------------------------------------------- 1 | const BasicView = defineComponent({ 2 | name: 'BasicView', 3 | setup() { 4 | return () =>
BasicView
5 | } 6 | }) 7 | 8 | export default BasicView 9 | -------------------------------------------------------------------------------- /src/views/document/document.tsx: -------------------------------------------------------------------------------- 1 | const DocumentView = defineComponent({ 2 | name: 'DocumentView', 3 | setup() { 4 | return () =>
DocumentView
5 | } 6 | }) 7 | 8 | export default DocumentView 9 | -------------------------------------------------------------------------------- /src/views/dashboard/monitor/monitor.tsx: -------------------------------------------------------------------------------- 1 | const MonitorView = defineComponent({ 2 | name: 'MonitorView', 3 | setup() { 4 | return () =>
MonitorView
5 | } 6 | }) 7 | 8 | export default MonitorView 9 | -------------------------------------------------------------------------------- /src/components/layouts/style/header.module.less: -------------------------------------------------------------------------------- 1 | .header-container { 2 | width: 100%; 3 | height: 50px; 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | padding: 0 10px 0 0; 8 | } 9 | -------------------------------------------------------------------------------- /src/layout/BlankLayout.tsx: -------------------------------------------------------------------------------- 1 | import { RouterView } from 'vue-router' 2 | 3 | const BlankLayout = defineComponent({ 4 | name: 'BlankLayout', 5 | setup() { 6 | return () => 7 | } 8 | }) 9 | 10 | export default BlankLayout 11 | -------------------------------------------------------------------------------- /src/views/home/home.tsx: -------------------------------------------------------------------------------- 1 | const HomeView = defineComponent({ 2 | name: 'HomeView', 3 | setup() { 4 | return () => ( 5 |
6 |

首页不知道写什么嗨,先就这样空着吧

7 |
8 | ) 9 | } 10 | }) 11 | 12 | export default HomeView 13 | -------------------------------------------------------------------------------- /src/router/modules/login.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | export default { 4 | path: '/login', 5 | name: 'login', 6 | component: () => import('@/views/login'), 7 | meta: { requiresAuth: false, icon: '', title: '登录' } 8 | } as RouteRecordRaw 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /src/router/modules/index.ts: -------------------------------------------------------------------------------- 1 | import dashboard from './dashboard' 2 | import list from './list' 3 | import profile from './profile' 4 | import document from './document' 5 | import home from './home' 6 | import base from './base' 7 | 8 | export default [home, dashboard, base, list, document, profile] 9 | -------------------------------------------------------------------------------- /src/utils/index.tsx: -------------------------------------------------------------------------------- 1 | import { NIcon } from 'naive-ui' 2 | import { h } from 'vue' 3 | import type { Component } from 'vue' 4 | 5 | export const renderIcon = (icon: Component) => { 6 | const slots = { 7 | default: () => h(icon) 8 | } 9 | return () => 10 | } 11 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/views/login.tsx: -------------------------------------------------------------------------------- 1 | const loginView = defineComponent({ 2 | name: 'LoginView', 3 | setup() { 4 | return () => ( 5 |
6 |

Login

7 |
8 | 9 |
10 |
11 | ) 12 | } 13 | }) 14 | 15 | export default loginView 16 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | export type { TagView } from './types' 3 | export { useSettingStore } from './modules/setting' 4 | export { useTagsViewStore } from './modules/tagsView' 5 | 6 | const store = createPinia() 7 | 8 | export function setupStore(app: App) { 9 | app.use(store) 10 | } 11 | -------------------------------------------------------------------------------- /src/components/TriggerCollapse/style/index.module.less: -------------------------------------------------------------------------------- 1 | .trigger { 2 | cursor: pointer; 3 | width: 50px; 4 | height: 50px; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | 9 | // &:hover { 10 | // background-color: var(--hover-color); 11 | // // background-color: var(--inverted-color); 12 | // } 13 | } 14 | -------------------------------------------------------------------------------- /src/router/modules/base.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | export default { 4 | path: '/base', 5 | name: 'base', 6 | component: () => import('@/views/base-component/baseComponent'), 7 | meta: { 8 | locale: 'basic', 9 | requiresAuth: false, 10 | icon: '', 11 | title: '基础组件' 12 | } 13 | } as RouteRecordRaw 14 | -------------------------------------------------------------------------------- /src/router/routes.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | import PageLayout from '@/layout/PageLayout' 3 | import appRoutes from './modules' 4 | 5 | export const constantRoutes: RouteRecordRaw[] = [ 6 | { 7 | path: '/', 8 | name: 'root', 9 | component: PageLayout, 10 | redirect: '/home', 11 | children: appRoutes 12 | } 13 | ] 14 | -------------------------------------------------------------------------------- /src/hooks/fullscreen.ts: -------------------------------------------------------------------------------- 1 | import { useFullscreen } from '@vueuse/core' 2 | 3 | const { isFullscreen, isSupported, toggle } = useFullscreen() 4 | 5 | const toggleScreen = () => { 6 | if (!isSupported) { 7 | window.$message.error('Your browser does not support fullscreen mode') 8 | } else { 9 | toggle() 10 | } 11 | } 12 | 13 | export { isFullscreen, toggleScreen } 14 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/style/base.less: -------------------------------------------------------------------------------- 1 | *, 2 | *::before, 3 | *::after { 4 | box-sizing: border-box; 5 | } 6 | 7 | .logoFixedBorder { 8 | position: fixed; 9 | top: 0; 10 | left: 0; 11 | border-right: solid 1px var(--n-border-color); 12 | } 13 | 14 | .mb8 { 15 | margin-bottom: 8px; 16 | } 17 | 18 | .item-hover { 19 | line-height: 20px; 20 | &:hover { 21 | background-color: skyblue; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/router/typings.d.ts: -------------------------------------------------------------------------------- 1 | import type { Component } from 'vue' 2 | import 'vue-router' 3 | 4 | declare module 'vue-router' { 5 | interface RouteMeta { 6 | icon: string | Component 7 | title: string 8 | role?: string[] 9 | requiresAuth?: boolean 10 | locale?: string 11 | isAffix?: boolean 12 | noCache?: boolean 13 | // menu select key 14 | menuSelectKey?: string 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import type { 5 | DialogApi, 6 | LoadingBarApi, 7 | MessageApi, 8 | NotificationApi 9 | } from 'naive-ui' 10 | 11 | declare global { 12 | interface Window { 13 | $message: MessageApi 14 | $dialog: DialogApi 15 | $loadingBar: LoadingBarApi 16 | $notification: NotificationApi 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/router/modules/document.ts: -------------------------------------------------------------------------------- 1 | import { AppstoreFilled } from '@vicons/antd' 2 | import type { RouteRecordRaw } from 'vue-router' 3 | 4 | export default { 5 | path: '/document', 6 | name: 'document', 7 | component: () => import('@/views/document/document'), 8 | meta: { 9 | locale: 'document', 10 | requiresAuth: false, 11 | icon: AppstoreFilled, 12 | title: '文档管理' 13 | } 14 | } as RouteRecordRaw 15 | -------------------------------------------------------------------------------- /src/router/modules/home.ts: -------------------------------------------------------------------------------- 1 | import { HomeOutlined } from '@vicons/antd' 2 | import type { RouteRecordRaw } from 'vue-router' 3 | import HomeView from '@/views/home/home' 4 | 5 | export default { 6 | path: '/home', 7 | name: 'home', 8 | component: HomeView, 9 | meta: { 10 | locale: 'document', 11 | requiresAuth: false, 12 | icon: HomeOutlined, 13 | title: '首页', 14 | isAffix: true 15 | } 16 | } as RouteRecordRaw 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | .vercel 30 | -------------------------------------------------------------------------------- /src/store/modules/setting.ts: -------------------------------------------------------------------------------- 1 | import type { SettingConfig } from '@/settings' 2 | import settings from '@/settings' 3 | import type { TriggerStyle } from '@/settings' 4 | 5 | export const useSettingStore = defineStore('setting', () => { 6 | const setting = reactive(settings) 7 | 8 | const changeSetting = ( 9 | key: keyof SettingConfig, 10 | value: boolean | TriggerStyle 11 | ) => { 12 | setting[key] = value 13 | } 14 | 15 | return { 16 | ...toRefs(setting), 17 | changeSetting 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /src/components/layouts/BaseLogo.tsx: -------------------------------------------------------------------------------- 1 | import { NAvatar, NEl, NH3 } from 'naive-ui' 2 | import styles from './style/logo.module.less' 3 | import logoUrl from '@/assets/logo.svg' 4 | 5 | const BaseLogo = defineComponent({ 6 | name: 'BaseLogo', 7 | props: { 8 | isCollapse: Boolean 9 | }, 10 | setup(props) { 11 | return () => ( 12 | 13 |
14 | 15 | {!props.isCollapse && Admin Plus} 16 |
17 |
18 | ) 19 | } 20 | }) 21 | 22 | export default BaseLogo 23 | -------------------------------------------------------------------------------- /src/views/dashboard/workplace/workplace.tsx: -------------------------------------------------------------------------------- 1 | import { useSettingStore } from '@/store' 2 | import { NButton } from 'naive-ui' 3 | 4 | const WorkPlaceView = defineComponent({ 5 | name: 'WorkPlaceView', 6 | setup() { 7 | const settingStore = useSettingStore() 8 | const { isShowLogo } = storeToRefs(settingStore) 9 | 10 | const click = () => { 11 | settingStore.changeSetting('isShowLogo', !isShowLogo.value) 12 | } 13 | 14 | return () => ( 15 |
16 | WorkPlaceView 17 | 按钮 18 |
19 | ) 20 | } 21 | }) 22 | 23 | export default WorkPlaceView 24 | -------------------------------------------------------------------------------- /md/hmr.md: -------------------------------------------------------------------------------- 1 | ```tsx 2 | import { defineComponent } from 'vue' 3 | 4 | // named exports w/ variable declaration: ok 5 | export const Foo = defineComponent({}) 6 | 7 | // named exports referencing variable declaration: ok 8 | const Bar = defineComponent({ 9 | render() { 10 | return
Test
11 | } 12 | }) 13 | export { Bar } 14 | 15 | // default export call: ok 16 | export default defineComponent({ 17 | render() { 18 | return
Test
19 | } 20 | }) 21 | 22 | // default export referencing variable declaration: ok 23 | const Baz = defineComponent({ 24 | render() { 25 | return
Test
26 | } 27 | }) 28 | export default Baz 29 | ``` 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "env.d.ts", 4 | "src/**/*.ts", 5 | "src/**/*.d.ts", 6 | "src/**/*.tsx", 7 | "src/**/*.vue" 8 | ], 9 | "compilerOptions": { 10 | "target": "esnext", 11 | "useDefineForClassFields": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "strict": true, 15 | "jsx": "preserve", 16 | "sourceMap": true, 17 | "resolveJsonModule": true, 18 | "esModuleInterop": true, 19 | "lib": ["esnext", "dom"], 20 | "baseUrl": ".", 21 | "paths": { 22 | "@/*": ["src/*"] 23 | } 24 | }, 25 | 26 | "references": [ 27 | { 28 | "path": "./tsconfig.config.json" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | 3 | // global less 4 | import './style/base.less' 5 | 6 | import App from './App' 7 | import { setupStore } from './store' 8 | import { router, setupRouter, setupRouterGuard } from './router' 9 | import { DashboardFilled } from '@vicons/antd' 10 | 11 | function bootstrap() { 12 | const app = createApp(App) 13 | 14 | // 全局组件 15 | app.component('BlankLayout', () => import('@/layout/BlankLayout')) 16 | app.component('DashboardFilled', DashboardFilled) 17 | 18 | // store 19 | setupStore(app) 20 | 21 | // router 22 | setupRouter(app) 23 | 24 | // router guard 25 | setupRouterGuard(router) 26 | 27 | app.mount('#app') 28 | } 29 | 30 | bootstrap() 31 | -------------------------------------------------------------------------------- /src/components/TriggerCollapse/index.tsx: -------------------------------------------------------------------------------- 1 | import { MenuFoldOutlined, MenuUnfoldOutlined } from '@vicons/antd' 2 | import { NEl, NIcon } from 'naive-ui' 3 | import styles from './style/index.module.less' 4 | 5 | const TriggerCollapse = defineComponent({ 6 | name: 'TriggerCollapse', 7 | props: { 8 | isCollapse: Boolean 9 | }, 10 | emits: ['changeSetting'], 11 | setup(props, { emit }) { 12 | return () => ( 13 | emit('changeSetting', 'isCollapse', !props.isCollapse)} 16 | > 17 | 18 | {!props.isCollapse ? : } 19 | 20 | 21 | ) 22 | } 23 | }) 24 | 25 | export default TriggerCollapse 26 | -------------------------------------------------------------------------------- /src/components/DButton.tsx: -------------------------------------------------------------------------------- 1 | import { LikeOutlined } from '@vicons/antd' 2 | import { NButton, NIcon } from 'naive-ui' 3 | import type { Component, PropType } from 'vue' 4 | 5 | const DButton = defineComponent({ 6 | name: 'DButton', 7 | extends: NButton, 8 | props: { 9 | icon: { 10 | type: Object as PropType, 11 | default: LikeOutlined 12 | }, 13 | content: { 14 | type: String as PropType, 15 | default: undefined 16 | } 17 | }, 18 | setup(props) { 19 | return () => ( 20 | {h(props.icon)}, 24 | default: () => props.content 25 | }} 26 | /> 27 | ) 28 | } 29 | }) 30 | 31 | export default DButton 32 | -------------------------------------------------------------------------------- /src/router/modules/profile.ts: -------------------------------------------------------------------------------- 1 | import BlankLayout from '@/layout/BlankLayout' 2 | import { UserOutlined } from '@vicons/antd' 3 | import type { RouteRecordRaw } from 'vue-router' 4 | 5 | export default { 6 | path: '/profile', 7 | name: 'profile', 8 | component: BlankLayout, 9 | meta: { 10 | locale: 'menu.profile', 11 | requiresAuth: true, 12 | icon: UserOutlined, 13 | title: '个人中心' 14 | }, 15 | children: [ 16 | { 17 | path: '/basic', 18 | name: 'basic', 19 | component: () => import('@/views/profile/basic/basic'), 20 | meta: { 21 | locale: 'menu.profile.basic', 22 | requiresAuth: true, 23 | role: ['admin'], 24 | title: '基本资料', 25 | icon: UserOutlined 26 | } 27 | } 28 | ] 29 | } as RouteRecordRaw 30 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueJsx from '@vitejs/plugin-vue-jsx' 6 | import AutoImport from 'unplugin-auto-import/vite' 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig({ 10 | // test build vercel 11 | optimizeDeps: { 12 | include: ['vue', 'vue-router', 'pinia'] 13 | }, 14 | plugins: [ 15 | vue({ 16 | // reactivityTransform: true 17 | }), 18 | vueJsx(), 19 | AutoImport({ 20 | imports: ['vue', 'vue-router', 'pinia'], 21 | dts: './src/auto-imports.d.ts' 22 | }) 23 | ], 24 | resolve: { 25 | alias: { 26 | '@': fileURLToPath(new URL('./src', import.meta.url)) 27 | } 28 | }, 29 | css: { 30 | modules: { 31 | localsConvention: 'camelCase' 32 | } 33 | } 34 | }) 35 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { darkTheme, dateZhCN, NConfigProvider, zhCN } from 'naive-ui' 2 | import { defineComponent } from 'vue' 3 | import { RouterView } from 'vue-router' 4 | import GlobalProvider from '@/components/GlobalProvider' 5 | import { useSettingStore } from './store' 6 | 7 | export default defineComponent({ 8 | name: 'App', 9 | setup() { 10 | const settingStore = useSettingStore() 11 | const { globalTheme } = storeToRefs(settingStore) 12 | 13 | const theme = computed(() => { 14 | if (globalTheme.value === 'darkTheme') { 15 | return darkTheme 16 | } else { 17 | return null 18 | } 19 | }) 20 | 21 | return () => ( 22 | 23 | 24 | 25 | 26 | 27 | ) 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import type { Router } from 'vue-router' 3 | import { createRouter, createWebHistory } from 'vue-router' 4 | import { constantRoutes } from './routes' 5 | 6 | export const router = createRouter({ 7 | history: createWebHistory(import.meta.env.BASE_URL), 8 | routes: constantRoutes, 9 | scrollBehavior: () => ({ left: 0, top: 0 }) 10 | }) 11 | 12 | // router 13 | export function setupRouter(app: App) { 14 | app.use(router) 15 | } 16 | 17 | // guard 18 | export function setupRouterGuard(router: Router) { 19 | router.beforeEach((to, from, next) => { 20 | window.$loadingBar.start() 21 | next() 22 | }) 23 | 24 | router.afterEach((to, from) => { 25 | window.$loadingBar.finish() 26 | }) 27 | 28 | router.onError((error) => { 29 | window.$message.error(error.message) 30 | window.$loadingBar.error() 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | export type TriggerStyle = 'bar' | 'arrow-circle' | 'custom' 2 | export type MenuMode = 'vertical' | 'horizontal' 3 | 4 | export interface SettingConfig { 5 | isShowDraw: boolean 6 | isShowLogo: boolean 7 | isFixedHeader: boolean 8 | isCollapse: boolean 9 | isShowTagViews: boolean 10 | isShowBreadcrumb: boolean 11 | isShowBreadcrumbIcon: boolean 12 | isInverted: boolean 13 | collapsedIconSize: number 14 | collapsedWidth: number 15 | globalTheme: 'darkTheme' | 'lightTheme' 16 | menuMode: MenuMode 17 | triggerStyle: TriggerStyle 18 | } 19 | 20 | export default { 21 | isShowDraw: false, 22 | isShowLogo: true, 23 | isFixedHeader: true, 24 | isCollapse: false, 25 | isShowTagViews: true, 26 | isShowBreadcrumb: true, 27 | isShowBreadcrumbIcon: true, 28 | isInverted: false, 29 | collapsedIconSize: 24, 30 | collapsedWidth: 50, 31 | globalTheme: 'lightTheme', 32 | menuMode: 'vertical', 33 | triggerStyle: 'custom' 34 | } as SettingConfig 35 | -------------------------------------------------------------------------------- /src/components/TipIcon.tsx: -------------------------------------------------------------------------------- 1 | import { NIcon, NTooltip } from 'naive-ui' 2 | import { defineComponent } from 'vue' 3 | import type { Component, PropType } from 'vue' 4 | 5 | const TipIcon = defineComponent({ 6 | name: 'TipIcon', 7 | props: { 8 | iconName: Object as PropType, 9 | tipContent: String, 10 | size: { 11 | type: Number as PropType, 12 | default: 26 13 | } 14 | }, 15 | emits: ['clickIcon'], 16 | setup(props, { emit }) { 17 | const slots = { 18 | trigger: () => ( 19 |
emit('clickIcon')}> 20 | 24 | {h(props.iconName as Component)} 25 | 26 |
27 | ), 28 | default: () => {props.tipContent} 29 | } 30 | 31 | return () => 32 | } 33 | }) 34 | 35 | export default TipIcon 36 | -------------------------------------------------------------------------------- /src/components/layouts/style/logo.module.less: -------------------------------------------------------------------------------- 1 | // 用固定不是很完美,滚动条还是算在里面 2 | .logo-container { 3 | width: 200px; 4 | height: 50px; 5 | max-width: 200px; 6 | border-bottom: solid 1px var(--n-border-color); 7 | background-color: var(--n-color); 8 | z-index: 2; 9 | transition: color 0.3s var(--n-bezier), border-color 0.3s var(--n-bezier), 10 | min-width 0.3s var(--n-bezier), max-width 0.3s var(--n-bezier), 11 | transform 0.3s var(--n-bezier), background-color 0.3s var(--n-bezier); 12 | 13 | .logo-content { 14 | width: 100%; 15 | height: 100%; 16 | display: flex; 17 | justify-content: center; 18 | align-items: center; 19 | 20 | img { 21 | width: 36px; 22 | height: 36px; 23 | } 24 | 25 | span { 26 | font-weight: 700; 27 | font-size: 18px; 28 | opacity: 1; 29 | overflow: hidden; 30 | white-space: nowrap; 31 | margin-left: 10px; 32 | transition: margin-left 0.3s var(--n-bezier), color 0.3s var(--n-bezier), 33 | opacity 0.3s var(--n-bezier); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/router/modules/dashboard.ts: -------------------------------------------------------------------------------- 1 | import BlankLayout from '@/layout/BlankLayout' 2 | import { AppstoreFilled, DashboardFilled, PieChartFilled } from '@vicons/antd' 3 | import type { RouteRecordRaw } from 'vue-router' 4 | 5 | export default { 6 | path: 'dashboard', 7 | name: 'dashboard', 8 | redirect: '/dashboard/workplace', 9 | component: BlankLayout, 10 | meta: { 11 | locale: 'dashboard', 12 | requiresAuth: true, 13 | icon: AppstoreFilled, 14 | title: '仪表管理' 15 | }, 16 | children: [ 17 | { 18 | path: 'workplace', 19 | name: 'workplace', 20 | component: () => import('@/views/dashboard/workplace/workplace'), 21 | meta: { 22 | locale: 'workplace', 23 | requiresAuth: true, 24 | role: ['*'], 25 | title: '工作站', 26 | icon: DashboardFilled, 27 | noCache: true 28 | } 29 | }, 30 | { 31 | path: 'monitor', 32 | name: 'monitor', 33 | component: () => import('@/views/dashboard/monitor/monitor'), 34 | meta: { 35 | locale: 'monitor', 36 | requiresAuth: true, 37 | role: ['*'], 38 | title: '仪表盘', 39 | icon: PieChartFilled 40 | } 41 | } 42 | ] 43 | } as RouteRecordRaw 44 | -------------------------------------------------------------------------------- /src/components/layouts/AppMain.tsx: -------------------------------------------------------------------------------- 1 | import type { PropType } from 'vue' 2 | import { KeepAlive, Suspense, Transition } from 'vue' 3 | import { RouterView } from 'vue-router' 4 | import type { VNode } from 'vue' 5 | import type { RouteLocationNormalizedLoaded } from 'vue-router' 6 | import { NSpin } from 'naive-ui' 7 | 8 | const AppMain = defineComponent({ 9 | name: 'AppMain', 10 | props: { 11 | cachedViews: Array as PropType 12 | }, 13 | setup(props) { 14 | const routerSlots = { 15 | default: ({ 16 | Component, 17 | route 18 | }: { 19 | Component: VNode 20 | route: RouteLocationNormalizedLoaded 21 | }) => 22 | Component && ( 23 | 24 | 25 | {/* handle multi root component warning */} 26 |
{h(Component, { key: route.path })}
27 |
28 |
29 | ) 30 | } 31 | 32 | const slots = { 33 | default: () => , 34 | fallback: () => 35 | } 36 | 37 | return () => 38 | } 39 | }) 40 | 41 | export default AppMain 42 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.tsx: -------------------------------------------------------------------------------- 1 | import { NBreadcrumb, NBreadcrumbItem, NIcon } from 'naive-ui' 2 | 3 | const Breadcrumb = defineComponent({ 4 | name: 'Breadcrumb', 5 | props: { 6 | isShowBreadcrumbIcon: Boolean 7 | }, 8 | setup(props) { 9 | const route = useRoute() 10 | 11 | const breadList = computed(() => { 12 | return route.matched 13 | .map((i) => { 14 | return { 15 | title: i.meta.title, 16 | icon: i.meta.icon, 17 | href: i.path, 18 | clickable: !!i.redirect && !!i.children.length 19 | } 20 | }) 21 | .filter((i) => i.href !== '/') 22 | }) 23 | 24 | return () => ( 25 | 26 | {breadList.value.map((i) => ( 27 | 32 | {props.isShowBreadcrumbIcon && ( 33 | 36 | )} 37 | {i.title} 38 | 39 | ))} 40 | 41 | ) 42 | } 43 | }) 44 | 45 | export default Breadcrumb 46 | -------------------------------------------------------------------------------- /md/笔记.md: -------------------------------------------------------------------------------- 1 | # 整体结构 2 | 3 | 1. 主题变化 4 | 2. 侧边栏配置 5 | 3. 头部配置 6 | 4. 内容区域配置 7 | 8 | ## 侧边栏 9 | 10 | 1. 可收缩, 11 | 1. 是否显示边框 bordered 12 | 2. 是否显示 trigger show-trigger 13 | 3. 切换侧边栏的位置,左右 sider-placement 14 | 4. 背景色反转 inverted 15 | 16 | ## 头部 17 | 18 | 1. 面包屑 19 | 2. 可显示顶部菜单 20 | 3. 背景色反转 21 | 4. 收回右侧菜单项 22 | 5. 可固定头部 23 | 24 | ```js 25 | 26 | // Generated by 'unplugin-auto-import' 27 | // We suggest you to commit this file into source control 28 | declare global { 29 | const ref: typeof import('vue')['ref'] 30 | const reactive: typeof import('vue')['reactive'] 31 | const computed: typeof import('vue')['computed'] 32 | const createApp: typeof import('vue')['createApp'] 33 | const watch: typeof import('vue')['watch'] 34 | const customRef: typeof import('vue')['customRef'] 35 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 36 | 37 | interface Window { 38 | $message: import('naive-ui').MessageApi 39 | $dialog: import('naive-ui').DialogApi 40 | $$notification: import('naive-ui').NotificationApi 41 | $loadingBar: import('naive-ui').LoadingBarApi 42 | } 43 | 44 | interface GlobalComponents { 45 | NConfigProvider: typeof import('naive-ui')['NConfigProvider'] 46 | NLayout: typeof import('naive-ui')['NLayout'] 47 | NMenu: typeof import('naive-ui')['NMenu'] 48 | } 49 | } 50 | export {} 51 | 52 | ``` 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsx-naive-admin", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "dev": "vite", 6 | "build-check": "run-p type-check build-only", 7 | "preview": "vite preview --port 4173", 8 | "build": "vite build", 9 | "type-check": "vue-tsc --noEmit", 10 | "format": "prettier --write .", 11 | "lint": "eslint src --fix --ext .ts,.tsx,.vue,.js,.jsx" 12 | }, 13 | "dependencies": { 14 | "@vueuse/core": "9.2.0", 15 | "pinia": "2.0.22", 16 | "vue": "3.2.38", 17 | "vue-router": "4.1.5" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "18.7.16", 21 | "@typescript-eslint/eslint-plugin": "5.36.2", 22 | "@typescript-eslint/parser": "5.36.2", 23 | "@vicons/antd": "^0.12.0", 24 | "@vitejs/plugin-vue": "3.1.0", 25 | "@vitejs/plugin-vue-jsx": "2.0.1", 26 | "@vue/tsconfig": "^0.1.3", 27 | "eslint": "8.23.0", 28 | "eslint-config-prettier": "^8.5.0", 29 | "eslint-define-config": "1.7.0", 30 | "eslint-plugin-import": "^2.26.0", 31 | "eslint-plugin-prettier": "4.2.1", 32 | "eslint-plugin-vue": "9.4.0", 33 | "less": "^4.1.3", 34 | "naive-ui": "2.33.2", 35 | "npm-run-all": "^4.1.5", 36 | "prettier": "^2.7.1", 37 | "typescript": "4.8.2", 38 | "unplugin-auto-import": "0.11.2", 39 | "vite": "3.1.0", 40 | "vue-eslint-parser": "9.0.3", 41 | "vue-tsc": "0.40.12" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/RightTool/DragItem.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DragOutlined, 3 | VerticalRightOutlined, 4 | VerticalLeftOutlined 5 | } from '@vicons/antd' 6 | import { NSpace, NIcon, NCheckbox } from 'naive-ui' 7 | import TipIcon from '../TipIcon' 8 | 9 | const DragItem = defineComponent({ 10 | name: 'DragItem', 11 | setup() { 12 | return () => ( 13 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | { 34 | console.log('first') 35 | }} 36 | /> 37 | { 42 | console.log('first') 43 | }} 44 | /> 45 | 46 |
47 | ) 48 | } 49 | }) 50 | 51 | export default DragItem 52 | -------------------------------------------------------------------------------- /src/views/base-component/baseComponent.tsx: -------------------------------------------------------------------------------- 1 | import DButton from '@/components/DButton' 2 | import { PlusOutlined, EditOutlined, DeleteOutlined } from '@vicons/antd' 3 | import { NCard, NSpace } from 'naive-ui' 4 | 5 | const BaseComponent = defineComponent({ 6 | name: 'BaseComponent', 7 | setup() { 8 | return () => ( 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 |
37 | ) 38 | } 39 | }) 40 | 41 | export default BaseComponent 42 | -------------------------------------------------------------------------------- /src/components/GlobalProvider.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createDiscreteApi, 3 | NDialogProvider, 4 | NLoadingBarProvider, 5 | NMessageProvider, 6 | NNotificationProvider 7 | } from 'naive-ui' 8 | // import { useMessage, useDialog, useLoadingBar, useNotification } from 'naive-ui' 9 | 10 | const GlobalProvider = defineComponent({ 11 | name: 'GlobalProvider', 12 | setup(_, { slots }) { 13 | return () => ( 14 | 15 | 16 | 17 | 18 | {slots.default!()} 19 | 20 | 21 | 22 | 23 | ) 24 | } 25 | }) 26 | 27 | // 注入到 window 上 28 | // const GlobalInject = defineComponent({ 29 | // name: 'GlovalInject', 30 | // setup(_, { slots }) { 31 | // window.$dialog = useDialog() 32 | // window.$message = useMessage() 33 | // window.$loadingBar = useLoadingBar() 34 | // window.$notification = useNotification() 35 | 36 | // return () => <>{slots.default!()} 37 | // } 38 | // }) 39 | 40 | const GlobalInject = defineComponent({ 41 | name: 'GlovalInject', 42 | setup(_, { slots }) { 43 | const { message, notification, dialog, loadingBar } = createDiscreteApi( 44 | ['message', 'dialog', 'notification', 'loadingBar'], 45 | { 46 | // configProviderProps: {} 47 | } 48 | ) 49 | 50 | window.$dialog = dialog 51 | window.$message = message 52 | window.$loadingBar = loadingBar 53 | window.$notification = notification 54 | 55 | return () => <>{slots.default!()} 56 | } 57 | }) 58 | 59 | export default GlobalProvider 60 | -------------------------------------------------------------------------------- /src/components/TagsView/useTagsView.ts: -------------------------------------------------------------------------------- 1 | import { useTagsViewStore } from '@/store' 2 | import type { TagView } from '@/store' 3 | 4 | const useTagsView = () => { 5 | const route = useRoute() 6 | const router = useRouter() 7 | const tagsViewStore = useTagsViewStore() 8 | const { visitedViews } = storeToRefs(tagsViewStore) 9 | 10 | // 固定页 11 | const affixViews = computed(() => 12 | router.getRoutes().filter((item) => item.meta?.isAffix) 13 | ) 14 | 15 | // 固定页 16 | const initTags = () => { 17 | affixViews.value.forEach((route) => { 18 | tagsViewStore.addView({ 19 | ...route.meta, 20 | fullPath: route?.path, 21 | name: route.name 22 | } as TagView) 23 | }) 24 | } 25 | // 添加 26 | const addTags = () => { 27 | const { name } = route 28 | name && 29 | tagsViewStore.addView({ 30 | ...route.meta, 31 | fullPath: route.fullPath, 32 | name: route.name as string 33 | }) 34 | } 35 | // 上一个 tag 36 | const toLastView = () => { 37 | const lastView = visitedViews.value.slice(-1)[0] 38 | lastView && router.push(lastView.fullPath) 39 | } 40 | 41 | // 点击 Tag 42 | const handleClickTag = (tag: TagView) => { 43 | router.push(tag.fullPath) 44 | } 45 | // 点击关闭 46 | const handleClose = async (tag: TagView) => { 47 | await tagsViewStore.delView(tag) 48 | toLastView() 49 | } 50 | 51 | // 路由变化添加 52 | watch( 53 | () => route.fullPath, 54 | () => addTags() 55 | ) 56 | 57 | onMounted(() => { 58 | initTags() 59 | addTags() 60 | }) 61 | 62 | return { 63 | visitedList: visitedViews, 64 | initTags, 65 | addTags, 66 | handleClickTag, 67 | handleClose 68 | } 69 | } 70 | 71 | export default useTagsView 72 | -------------------------------------------------------------------------------- /src/components/layouts/BaseSider.tsx: -------------------------------------------------------------------------------- 1 | import { NLayoutSider } from 'naive-ui' 2 | import Menu from './Menu' 3 | import BaseLogo from './BaseLogo' 4 | import type { MenuMode, TriggerStyle } from '@/settings' 5 | import type { PropType } from 'vue' 6 | 7 | type Trigger = boolean | 'bar' | 'arrow-circle' 8 | 9 | const BaseSider = defineComponent({ 10 | name: 'BaseSider', 11 | props: { 12 | isShowLogo: Boolean, 13 | triggerStyle: String as PropType, 14 | isCollapse: Boolean, 15 | isInverted: Boolean, 16 | collapsedWidth: Number, 17 | collapsedIconSize: Number, 18 | menuMode: { 19 | type: String as PropType, 20 | default: 'vertical' 21 | } 22 | }, 23 | emits: ['changeCollapsed'], 24 | setup(props, { emit }) { 25 | const triggerStyle = computed(() => 26 | props.triggerStyle === 'custom' ? false : props.triggerStyle 27 | ) 28 | 29 | return () => ( 30 | 39 | emit('changeCollapsed', 'isCollapse', !props.isCollapse) 40 | } 41 | inverted={props.isInverted} 42 | > 43 | {props.isShowLogo && ( 44 | 51 | )} 52 | 53 | 60 | 61 | ) 62 | } 63 | }) 64 | 65 | export default BaseSider 66 | -------------------------------------------------------------------------------- /src/store/modules/tagsView.ts: -------------------------------------------------------------------------------- 1 | import type { TagView } from '../types' 2 | 3 | export const useTagsViewStore = defineStore('tagsView', () => { 4 | let visitedViews = ref([]) 5 | let cachedViews = ref([]) 6 | 7 | // 添加 8 | const addView = (view: TagView) => { 9 | const hasView = visitedViews.value.some((i) => i.fullPath === view.fullPath) 10 | if (hasView) return 11 | visitedViews.value.push(view) 12 | if (!view.noCache) { 13 | cachedViews.value.push(view.name) 14 | } 15 | } 16 | 17 | // 删除 18 | const delView = (view: TagView) => { 19 | return new Promise((resolve) => { 20 | visitedViews.value = visitedViews.value.filter( 21 | (i) => i.fullPath !== view.fullPath 22 | ) 23 | cachedViews.value = cachedViews.value.filter((i) => i !== view.name) 24 | resolve() 25 | }) 26 | } 27 | 28 | // 关闭右侧 29 | const closeRightView = (view: TagView) => { 30 | return new Promise((resolve) => { 31 | const index = visitedViews.value.findIndex( 32 | (i) => i.fullPath === view.fullPath 33 | ) 34 | visitedViews.value = visitedViews.value.slice(0, index + 1) 35 | cachedViews.value = cachedViews.value.slice(0, index + 1) 36 | resolve() 37 | }) 38 | } 39 | 40 | // 关闭其它 41 | const closeOtherView = (view: TagView) => { 42 | return new Promise((resolve) => { 43 | visitedViews.value = visitedViews.value.filter( 44 | (i) => i.fullPath === view.fullPath || i.isAffix 45 | ) 46 | // cachedViews = cachedViews.filter((i) => i === view.name || i === 'home') 47 | cachedViews.value = visitedViews.value.map((i) => i.name) 48 | resolve() 49 | }) 50 | } 51 | 52 | // 关闭所有 53 | const closeAllView = () => { 54 | return new Promise((resolve) => { 55 | visitedViews.value = visitedViews.value.filter((i) => i.isAffix) 56 | cachedViews.value = visitedViews.value.map((i) => i.name) 57 | resolve() 58 | }) 59 | } 60 | 61 | return { 62 | visitedViews, 63 | cachedViews, 64 | addView, 65 | delView, 66 | closeRightView, 67 | closeOtherView, 68 | closeAllView 69 | } 70 | }) 71 | -------------------------------------------------------------------------------- /src/components/TagsView/index.tsx: -------------------------------------------------------------------------------- 1 | import { useTagsViewStore } from '@/store' 2 | import { NDropdown, NSpace, NTag } from 'naive-ui' 3 | import useDropdown from './useDropdown' 4 | import useTagsView from './useTagsView' 5 | 6 | const TagsView = defineComponent({ 7 | name: 'TagsView', 8 | setup() { 9 | const route = useRoute() 10 | // const tagsViewStore = useTagsViewStore() 11 | // const { visitedViews, cachedViews } = storeToRefs(tagsViewStore) 12 | const { handleClickTag, handleClose, visitedList } = useTagsView() 13 | 14 | const { 15 | showDropRef, 16 | x, 17 | y, 18 | handleSelect, 19 | handleContextMenu, 20 | clickoutSide, 21 | dropOptions 22 | } = useDropdown(toRaw(visitedList.value)) 23 | 24 | return () => ( 25 | 30 | {visitedList.value.map((i) => ( 31 | handleClose(i)} 36 | color={ 37 | i.fullPath === route.fullPath 38 | ? { 39 | color: 'var(--n-color-checked)', 40 | textColor: 'var(--n-text-color-checked)' 41 | } 42 | : {} 43 | } 44 | style={{ cursor: 'pointer' }} 45 | > 46 | handleClickTag(i)} 48 | onContextmenu={(...args) => handleContextMenu(...args, i)} 49 | > 50 | {i.title} 51 | 52 | 53 | ))} 54 | 55 | 66 | 67 | ) 68 | } 69 | }) 70 | 71 | export default TagsView 72 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('eslint-define-config') 2 | 3 | module.exports = defineConfig({ 4 | root: true, 5 | parser: 'vue-eslint-parser', 6 | parserOptions: { 7 | parser: '@typescript-eslint/parser', 8 | sourceType: 'module', 9 | ecmaFeatures: { 10 | jsx: true, 11 | tsx: true 12 | } 13 | }, 14 | env: { 15 | browser: true, 16 | node: true 17 | }, 18 | plugins: ['@typescript-eslint', 'prettier', 'import'], 19 | extends: [ 20 | 'eslint:recommended', 21 | 'plugin:@typescript-eslint/recommended', 22 | 'plugin:vue/vue3-recommended', 23 | 'prettier' 24 | ], 25 | overrides: [ 26 | { 27 | files: ['*.ts', '*.vue', '*.tsx'], 28 | rules: { 29 | 'no-undef': 'off' 30 | } 31 | } 32 | ], 33 | rules: { 34 | // js/ts 35 | 'no-console': ['warn', { allow: ['error'] }], 36 | 'no-restricted-syntax': ['error', 'LabeledStatement', 'WithStatement'], 37 | camelcase: ['error', { properties: 'never' }], 38 | 39 | 'no-var': 'error', 40 | 'no-empty': ['error', { allowEmptyCatch: true }], 41 | 'no-void': 'error', 42 | 'prefer-const': [ 43 | 'warn', 44 | { destructuring: 'all', ignoreReadBeforeAssign: true } 45 | ], 46 | 'prefer-template': 'error', 47 | 'object-shorthand': [ 48 | 'error', 49 | 'always', 50 | { ignoreConstructors: false, avoidQuotes: true } 51 | ], 52 | 'block-scoped-var': 'error', 53 | 'no-constant-condition': ['error', { checkLoops: false }], 54 | 55 | '@typescript-eslint/explicit-module-boundary-types': 'off', 56 | '@typescript-eslint/no-var-requires': 'off', 57 | '@typescript-eslint/no-explicit-any': 'off', 58 | '@typescript-eslint/no-non-null-assertion': 'off', 59 | '@typescript-eslint/no-non-null-asserted-optional-chain': 'off', 60 | '@typescript-eslint/consistent-type-imports': [ 61 | 'error', 62 | { disallowTypeAnnotations: false } 63 | ], 64 | 65 | // vue 66 | 'vue/no-v-html': 'off', 67 | 'vue/require-default-prop': 'off', 68 | 'vue/require-explicit-emits': 'off', 69 | 'vue/multi-word-component-names': 'off', 70 | 71 | // prettier 72 | 'prettier/prettier': 'error' 73 | } 74 | }) 75 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tsx-naive-admin 2 | 3 | 白嫖部署 `vercel`,具体预览:[https://tsx-naive-admin.vercel.app](https://tsx-naive-admin.vercel.app) 4 | 5 | `vercel` 现在需要翻,国内网络可以访问: [https://tsx-naive-admin.netlify.app/](https://tsx-naive-admin.netlify.app/) 6 | 7 | ## 用了 纯粹的 TSX + Pinia + Reactivity Transform,感觉更完美了 8 | 9 | 临时起意,恰好有点时间,就瞎搞了,也遇到不少问题 10 | 11 | 1. `vite + tsx` 的写法 热更新会有一些问题,所以必须要包裹在 `defineComponent()` 选项当中 12 | 13 | 2. [naive-ui](http://www.naiveui.com) tsx 的写法,提示真的很爽,也因此发现了几个小问题,也蹭了几个 `PR` 14 | 15 | 3. **这一项已经不再推荐使用,新的提交 commit close reactivityTransform 已经关闭** 对于 `Reactivity Ttransform`,说说看法,这个玩意不熟属实有点坑,尤其在你做封装导出的再用的时候,不能准确把握数据的类型,就特别容易出 现不在响应式的 `bug`,在 `tsx` 的写法中尤其需要注意 16 | 17 | 4. 关于 [Suspense](https://vuejs.org/guide/built-ins/suspense.html#suspense),代码中写到的应该正确的方式。不知道在 `vue` 官网还是在 `vue-router` 官网看的的两一种写法,在 `tsx` 中是会出现 `KeepAlive` 重复性渲染视图的问题 18 | 19 | 5. 对于 [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components),在 `tsx` 中就不要用了吧。我记得好像是能够识别,但是是没有 `ts` 类型提示的,按需导入也挺好用的 20 | 21 | 6. 还有在 `views` 页面这一块,细心的都可能发现了我文件夹下主文件还是用的和文件名一样的 `.tsx` 文件,如 `/home/home.tsx`,这是为了 `vite` 打包之后生成 `chunk` 的文件名,用 `index.tsx` 命名主文件,打包后都是 `index.[hash].js` 22 | 23 | 7. **组件二次封装**,这里涉及到 `props` 的类型提示问题,目前想到的办法是直接使用配置项 `extends: NXxx`,具体可查看 [DButton](./src/components/DButton.tsx),感觉应该是一种比较完美的解决方式 24 | 25 | 8. `css modules`,使用了 `less`,开启了 `localsConvention: 'camelCase'`,查看 [config](./vite.config.ts)。 26 | 27 | 本来准备上 [unocss](https://github.com/unocss/unocss) 的,但是吧,`Naive-UI` 组件比较多,大部分布局组件都能够实现,对大量的自定义样式需求不大,而且想要适配主题色,也可能需要一些相关的预设(说实话,我只会写类名,预设不太整明白) 28 | 29 | 9. 关于 `TS`,我是菜鸡中的菜鸡,代码里面应该写了很多让大佬一看就摇头的 类型标注。没办法,慢慢搞慢慢搞 30 | 31 | 32 | 10. 还有等等,想起来再说吧 33 | 34 | ## 临时查看: 35 | 36 | 1. `git clone git@github.com:WalkAlone0325/tsx-naive-admin.git` 37 | 2. `cd tsx-naive-admin` 38 | 3. `pnpm build` => `pnpm preview` 39 | 4. 或者使用 `serve` 等静态服务,如 `serve dist/` 40 | 41 | ## 如何使用: 42 | 43 | 1. `git clone git@github.com:WalkAlone0325/tsx-naive-admin.git` 44 | 2. `cd tsx-naive-admin` 45 | 3. `pnpm i` 46 | 4. `pnpm dev` 47 | 48 | ## TSX + naive-ui + Vue3 + Vite 49 | 50 | 后台管理系统基本架子 51 | 52 | ## 包含内容 53 | 54 | 1. Pinia + Vue-Router + Naive-UI 55 | 2. 主题色 56 | 3. 全局配置组件 57 | 4. 多标签 58 | 5. 多级菜单 59 | 6. 面包屑 60 | 7. 侧边折叠 61 | 8. 等吧 62 | 63 | ### test mac commit 64 | -------------------------------------------------------------------------------- /src/assets/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | body { 63 | min-height: 100vh; 64 | color: var(--color-text); 65 | background: var(--color-background); 66 | transition: color 0.5s, background-color 0.5s; 67 | line-height: 1.6; 68 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 69 | Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 70 | sans-serif; 71 | font-size: 15px; 72 | text-rendering: optimizeLegibility; 73 | -webkit-font-smoothing: antialiased; 74 | -moz-osx-font-smoothing: grayscale; 75 | } 76 | -------------------------------------------------------------------------------- /src/components/layouts/Menu.tsx: -------------------------------------------------------------------------------- 1 | import { renderIcon } from '@/utils' 2 | import { NMenu } from 'naive-ui' 3 | import type { MenuOption } from 'naive-ui' 4 | import type { Component, PropType } from 'vue' 5 | import type { RouteRecordRaw } from 'vue-router' 6 | import type { MenuMode } from '@/settings' 7 | 8 | const Menu = defineComponent({ 9 | name: 'Menu', 10 | props: { 11 | isInverted: Boolean, 12 | collapsedWidth: Number, 13 | collapsedIconSize: Number, 14 | menuMode: { 15 | type: String as PropType, 16 | default: 'vertical' 17 | } 18 | }, 19 | setup(props) { 20 | const route = useRoute() 21 | const router = useRouter() 22 | 23 | // 高亮菜单 24 | let activeKey = ref(route.name) 25 | watch( 26 | () => route.fullPath, 27 | () => { 28 | activeKey.value = route.name as string 29 | } 30 | ) 31 | 32 | const handleClickItem = (key: string) => { 33 | return router.push({ name: key }) 34 | } 35 | 36 | // root menu 37 | const appRoute = computed(() => 38 | router.getRoutes().find((route) => route.name === 'root') 39 | ) 40 | 41 | const menuTree = computed(() => { 42 | // const copyRoutes = JSON.parse(JSON.stringify(appRoute?.children)) 43 | const copyRoutes = [...appRoute.value!.children] 44 | 45 | function travel(routes: RouteRecordRaw[], layer: number) { 46 | return routes.map((route) => { 47 | if (route.children) { 48 | route.children = travel(route.children, layer + 1) 49 | } 50 | return { 51 | ...route, 52 | label: route.meta?.title, 53 | icon: 54 | typeof route.meta?.icon === 'string' 55 | ? renderIcon(resolveComponent('DashboardFilled') as Component) 56 | : renderIcon(route.meta?.icon as unknown as Component), 57 | key: route.name 58 | } 59 | }) 60 | } 61 | return travel(copyRoutes, 0) 62 | }) 63 | 64 | return () => ( 65 | 76 | ) 77 | } 78 | }) 79 | 80 | export default Menu 81 | -------------------------------------------------------------------------------- /src/router/modules/list.ts: -------------------------------------------------------------------------------- 1 | import BlankLayout from '@/layout/BlankLayout' 2 | import { ClusterOutlined } from '@vicons/antd' 3 | import type { RouteRecordRaw } from 'vue-router' 4 | 5 | export default { 6 | path: 'list', 7 | name: 'list', 8 | component: BlankLayout, 9 | meta: { 10 | locale: 'menu.list', 11 | requiresAuth: true, 12 | icon: ClusterOutlined, 13 | title: '表格管理' 14 | }, 15 | children: [ 16 | { 17 | path: 'search-table', // The midline path complies with SEO specifications 18 | name: 'searchTable', 19 | component: () => import('@/views/list/search-table/search-table'), 20 | meta: { 21 | locale: 'menu.list.searchTable', 22 | requiresAuth: true, 23 | role: ['*'], 24 | title: '查询表格', 25 | icon: ClusterOutlined 26 | } 27 | }, 28 | { 29 | path: 'card', 30 | name: 'card', 31 | component: () => import('@/views/list/card/card'), 32 | meta: { 33 | locale: 'menu.list.cardList', 34 | requiresAuth: true, 35 | role: ['*'], 36 | title: '卡片管理', 37 | icon: ClusterOutlined 38 | }, 39 | children: [ 40 | { 41 | path: 'card1', 42 | name: 'card1', 43 | component: () => import('@/views/list/card/card'), 44 | meta: { 45 | locale: 'menu.list.cardList', 46 | requiresAuth: true, 47 | role: ['*'], 48 | title: '卡片第一级1', 49 | icon: ClusterOutlined 50 | } 51 | }, 52 | { 53 | path: 'card2', 54 | name: 'card2', 55 | component: () => import('@/views/list/card/card'), 56 | meta: { 57 | locale: 'menu.list.cardList', 58 | requiresAuth: true, 59 | role: ['*'], 60 | title: '卡片第一级2', 61 | icon: ClusterOutlined 62 | }, 63 | children: [ 64 | { 65 | path: 'card2-1', 66 | name: 'card2-1', 67 | component: () => import('@/views/list/card/card'), 68 | meta: { 69 | locale: 'menu.list.cardList', 70 | requiresAuth: true, 71 | role: ['*'], 72 | title: '卡片第二级2-1', 73 | icon: ClusterOutlined 74 | } 75 | }, 76 | { 77 | path: 'card2-2', 78 | name: 'card2-2', 79 | component: () => import('@/views/list/card/card'), 80 | meta: { 81 | locale: 'menu.list.cardList', 82 | requiresAuth: true, 83 | role: ['*'], 84 | title: '卡片第二级2-2', 85 | icon: ClusterOutlined 86 | } 87 | } 88 | ] 89 | } 90 | ] 91 | } 92 | ] 93 | } as RouteRecordRaw 94 | -------------------------------------------------------------------------------- /src/layout/PageLayout.tsx: -------------------------------------------------------------------------------- 1 | import { NEl, NLayout, NLayoutContent } from 'naive-ui' 2 | import BaseSider from '@/components/layouts/BaseSider' 3 | import BaseHeader from '@/components/layouts/BaseHeader' 4 | import GlobalDraw from '@/components/GlobalDraw' 5 | import { useSettingStore, useTagsViewStore } from '@/store' 6 | import AppMain from '@/components/layouts/AppMain' 7 | import TagsView from '@/components/TagsView' 8 | 9 | const PageLayout = defineComponent({ 10 | name: 'PageLayout', 11 | setup() { 12 | const settingStore = useSettingStore() 13 | const { 14 | isShowLogo, 15 | isFixedHeader, 16 | isCollapse, 17 | isShowTagViews, 18 | isInverted, 19 | triggerStyle, 20 | collapsedIconSize, 21 | collapsedWidth, 22 | menuMode 23 | } = storeToRefs(settingStore) 24 | const tagsViewStore = useTagsViewStore() 25 | const { visitedViews, cachedViews } = storeToRefs(tagsViewStore) 26 | 27 | watchEffect(() => { 28 | console.log(cachedViews.value) 29 | }) 30 | 31 | const contentStyle = computed(() => { 32 | if (isFixedHeader) { 33 | if (isShowTagViews) { 34 | return { marginTop: '84px' } 35 | } else { 36 | return { marginTop: '50px' } 37 | } 38 | } else { 39 | return {} 40 | } 41 | }) 42 | 43 | return () => ( 44 | 45 | {menuMode.value === 'vertical' && ( 46 | 56 | )} 57 | 58 | 59 | 67 | 68 | {isShowTagViews && ( 69 | 77 | 78 | 79 | )} 80 | 81 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | ) 94 | } 95 | }) 96 | 97 | export default PageLayout 98 | -------------------------------------------------------------------------------- /src/components/TagsView/useDropdown.ts: -------------------------------------------------------------------------------- 1 | import type { TagView } from '@/store' 2 | import { useTagsViewStore } from '@/store' 3 | import type { DropdownOption } from 'naive-ui' 4 | import useTagsView from './useTagsView' 5 | 6 | const options = [ 7 | { 8 | label: '刷新页面', 9 | key: 'refresh' 10 | }, 11 | { 12 | label: '关闭当前', 13 | key: 'current' 14 | }, 15 | { 16 | label: '关闭右侧', 17 | key: 'right' 18 | }, 19 | { 20 | label: '关闭其它', 21 | key: 'other' 22 | }, 23 | { 24 | label: '关闭所有', 25 | key: 'all' 26 | } 27 | ] 28 | 29 | const useDropdown = (visitedList: TagView[]) => { 30 | const route = useRoute() 31 | const router = useRouter() 32 | const { handleClose } = useTagsView() 33 | const { closeRightView, closeOtherView, closeAllView } = useTagsViewStore() 34 | 35 | const showDropRef = ref(false) 36 | const x = ref(0) 37 | const y = ref(0) 38 | // 点击的 tag 39 | let currentTag = ref() 40 | const dropOptions = ref([]) 41 | 42 | // 选择 43 | const handleSelect = async (key: string) => { 44 | showDropRef.value = false 45 | switch (key) { 46 | case 'refresh': 47 | if (currentTag.value?.fullPath === route.fullPath) { 48 | router.go(0) 49 | } else { 50 | router.push(currentTag.value!.fullPath) 51 | } 52 | break 53 | case 'current': 54 | handleClose(currentTag.value!) 55 | break 56 | case 'right': 57 | await closeRightView(currentTag.value!) 58 | router.push(currentTag.value!.fullPath) 59 | break 60 | case 'other': 61 | await closeOtherView(currentTag.value!) 62 | router.push(currentTag.value!.fullPath) 63 | break 64 | case 'all': 65 | await closeAllView() 66 | router.push('/') 67 | break 68 | default: 69 | } 70 | } 71 | 72 | // 右击 tag 73 | const isLastTag = (tag: TagView) => { 74 | const index = visitedList.findIndex( 75 | (item) => tag.fullPath === item.fullPath 76 | ) 77 | if (index !== -1 && index + 1 !== visitedList.length) return false 78 | else return true 79 | } 80 | 81 | // 判断返回的 options 82 | const checkDropOptions = (tag: TagView) => { 83 | if (tag.fullPath === '/home') { 84 | // 点击的是首页,不显示关闭当前 85 | dropOptions.value = options.filter((item) => item.key !== 'current') 86 | } else if (isLastTag(tag)) { 87 | dropOptions.value = options.filter((item) => item.key !== 'right') 88 | } else { 89 | dropOptions.value = options 90 | } 91 | } 92 | 93 | const handleContextMenu = async (e: MouseEvent, tag: TagView) => { 94 | currentTag.value = tag 95 | e.preventDefault() 96 | showDropRef.value = false 97 | await nextTick() 98 | checkDropOptions(tag) 99 | showDropRef.value = true 100 | x.value = e.clientX 101 | y.value = e.clientY 102 | } 103 | 104 | // 点击外面 105 | const clickoutSide = () => { 106 | showDropRef.value = false 107 | } 108 | 109 | return { 110 | showDropRef, 111 | x, 112 | y, 113 | handleSelect, 114 | handleContextMenu, 115 | clickoutSide, 116 | dropOptions 117 | } 118 | } 119 | 120 | export default useDropdown 121 | -------------------------------------------------------------------------------- /md/note.md: -------------------------------------------------------------------------------- 1 | # 构建项目 2 | 3 | ## 创建项目 4 | 5 | `pnpm create vite` 6 | 7 | ### 配置包管理器 8 | 9 | `"preinstall": "npx only-allow pnpm"` 10 | 11 | ## 约束代码风格 12 | 13 | ### eslint prettier 14 | 15 | #### `pnpm add eslint -D` 16 | 17 | #### `pnpx eslint --init` 18 | 19 | ```sh 20 | √ How would you like to use ESLint? · problems 21 | √ What type of modules does your project use? · esm 22 | √ Which framework does your project use? · vue 23 | √ Does your project use TypeScript? · No / Yes 24 | √ Where does your code run? · browser, node 25 | √ What format do you want your config file to be in? · JavaScript 26 | ``` 27 | 28 | #### `pnpm add eslint-plugin-vue@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest -D` 29 | 30 | #### `pnpm add eslint-config-typescript -D` 31 | 32 | eslint-config-prettier 解决冲突,以 prettier 为准,使 eslint 规范失效 33 | eslint-plugin-prettier prettier 34 | 35 | #### 配置 .eslintrc.js 文件 36 | 37 | ```js 38 | module.exports = { 39 | root: true, 40 | env: { 41 | browser: true, 42 | es2021: true, 43 | node: true 44 | }, 45 | parser: 'vue-eslint-parser', 46 | extends: [ 47 | 'plugin:vue/vue3-essential', 48 | 'plugin:@typescript-eslint/recommended', 49 | 'prettier', 50 | 'plugin:prettier/recommended' 51 | ], 52 | parserOptions: { 53 | ecmaVersion: 13, 54 | parser: '@typescript-eslint/parser', 55 | sourceType: 'module', 56 | jsxPragma: 'React', 57 | ecmaFeatures: { 58 | jsx: true 59 | } 60 | }, 61 | plugins: ['vue', '@typescript-eslint', 'prettier'], 62 | rules: {}, 63 | globals: { 64 | defineProps: 'readonly', 65 | defineEmits: 'readonly', 66 | defineExpose: 'readonly', 67 | withDefaults: 'readonly' 68 | } 69 | } 70 | ``` 71 | 72 | #### 配置 .prettierrc 文件 73 | 74 | ```json 75 | { 76 | "semi": false, 77 | "tabWidth": 2, 78 | "singleQuote": true, 79 | "printWidth": 100, 80 | "bracketSpacing": true, 81 | "trailingComma": "none", 82 | "arrowParens": "always" 83 | } 84 | ``` 85 | 86 | #### 配置启动命令 package.json 87 | 88 | ```json 89 | "format": "prettier --write .", 90 | "lint": "eslint src --fix --ext .ts,.tsx,.vue,.js,.jsx", 91 | ``` 92 | 93 | ### 配置 husky + lint-staged 94 | 95 | 1. 安装 mrm (pnpm add mrm -D) 96 | 2. 使用 mrm 安装 lint-staged (pnpx mrm lint-staged) 97 | 3. 生成 .husky 文件,并生成 `"prepare": "husky install"` 命令 98 | 4. 修改 commit 提交规范,结合 prettier 代码格式化,修改配置 99 | 100 | ```json 101 | "husky": { 102 | "hooks": { 103 | "pre-commit": "lint-staged" 104 | } 105 | }, 106 | "lint-staged": { 107 | "*.{js,jsx,vue,ts,tsx}": [ 108 | "pnpm lint", 109 | "prettier --write", 110 | "git add" 111 | ] 112 | } 113 | ``` 114 | 115 | ## 打包规范相关 116 | 117 | `pnpm add vite-plugin-compression -D` 118 | 119 | ```js 120 | base: './', 121 | plugins: [ 122 | vue(), 123 | vueJsx(), 124 | // prod generator .gz files 125 | viteCompression({ 126 | verbose: true, 127 | disable: false, 128 | threshold: 10240, 129 | algorithm: 'gzip', 130 | ext: '.gz' 131 | }) 132 | ], 133 | resolve: { 134 | alias: { 135 | '@': fileURLToPath(new URL('./src', import.meta.url)) 136 | } 137 | }, 138 | server: { 139 | host: '0.0.0.0', 140 | port: 3000, 141 | open: true, 142 | https: false, 143 | proxy: {} 144 | }, 145 | build: { 146 | terserOptions: { 147 | // prod clear console debugger 148 | compress: { 149 | drop_console: true, 150 | drop_debugger: true 151 | } 152 | } 153 | } 154 | ``` 155 | -------------------------------------------------------------------------------- /src/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | export {} 3 | declare global { 4 | const EffectScope: typeof import('vue')['EffectScope'] 5 | const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] 6 | const computed: typeof import('vue')['computed'] 7 | const createApp: typeof import('vue')['createApp'] 8 | const createPinia: typeof import('pinia')['createPinia'] 9 | const customRef: typeof import('vue')['customRef'] 10 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 11 | const defineComponent: typeof import('vue')['defineComponent'] 12 | const defineStore: typeof import('pinia')['defineStore'] 13 | const effectScope: typeof import('vue')['effectScope'] 14 | const getActivePinia: typeof import('pinia')['getActivePinia'] 15 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 16 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 17 | const h: typeof import('vue')['h'] 18 | const inject: typeof import('vue')['inject'] 19 | const isProxy: typeof import('vue')['isProxy'] 20 | const isReactive: typeof import('vue')['isReactive'] 21 | const isReadonly: typeof import('vue')['isReadonly'] 22 | const isRef: typeof import('vue')['isRef'] 23 | const mapActions: typeof import('pinia')['mapActions'] 24 | const mapGetters: typeof import('pinia')['mapGetters'] 25 | const mapState: typeof import('pinia')['mapState'] 26 | const mapStores: typeof import('pinia')['mapStores'] 27 | const mapWritableState: typeof import('pinia')['mapWritableState'] 28 | const markRaw: typeof import('vue')['markRaw'] 29 | const nextTick: typeof import('vue')['nextTick'] 30 | const onActivated: typeof import('vue')['onActivated'] 31 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 32 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 33 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 34 | const onDeactivated: typeof import('vue')['onDeactivated'] 35 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 36 | const onMounted: typeof import('vue')['onMounted'] 37 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 38 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 39 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 40 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 41 | const onUnmounted: typeof import('vue')['onUnmounted'] 42 | const onUpdated: typeof import('vue')['onUpdated'] 43 | const provide: typeof import('vue')['provide'] 44 | const reactive: typeof import('vue')['reactive'] 45 | const readonly: typeof import('vue')['readonly'] 46 | const ref: typeof import('vue')['ref'] 47 | const resolveComponent: typeof import('vue')['resolveComponent'] 48 | const setActivePinia: typeof import('pinia')['setActivePinia'] 49 | const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] 50 | const shallowReactive: typeof import('vue')['shallowReactive'] 51 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 52 | const shallowRef: typeof import('vue')['shallowRef'] 53 | const storeToRefs: typeof import('pinia')['storeToRefs'] 54 | const toRaw: typeof import('vue')['toRaw'] 55 | const toRef: typeof import('vue')['toRef'] 56 | const toRefs: typeof import('vue')['toRefs'] 57 | const triggerRef: typeof import('vue')['triggerRef'] 58 | const unref: typeof import('vue')['unref'] 59 | const useAttrs: typeof import('vue')['useAttrs'] 60 | const useCssModule: typeof import('vue')['useCssModule'] 61 | const useCssVars: typeof import('vue')['useCssVars'] 62 | const useRoute: typeof import('vue-router')['useRoute'] 63 | const useRouter: typeof import('vue-router')['useRouter'] 64 | const useSlots: typeof import('vue')['useSlots'] 65 | const watch: typeof import('vue')['watch'] 66 | const watchEffect: typeof import('vue')['watchEffect'] 67 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 68 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 69 | } 70 | -------------------------------------------------------------------------------- /src/components/GlobalDraw.tsx: -------------------------------------------------------------------------------- 1 | import { defineComponent } from 'vue' 2 | import { 3 | NDivider, 4 | NDrawer, 5 | NDrawerContent, 6 | NInputNumber, 7 | NSelect, 8 | NSpace, 9 | NSwitch 10 | } from 'naive-ui' 11 | import type { SelectOption } from 'naive-ui' 12 | import { useSettingStore } from '@/store' 13 | 14 | const DescSetting = defineComponent({ 15 | name: 'DescSetting', 16 | props: { 17 | title: String 18 | }, 19 | setup(props, { slots }) { 20 | return () => ( 21 |
29 | {props.title} 30 | {slots.default!()} 31 |
32 | ) 33 | } 34 | }) 35 | 36 | const GlobalDraw = defineComponent({ 37 | name: 'GlobalDraw', 38 | setup(props, { emit }) { 39 | const settingStore = useSettingStore() 40 | const { 41 | isShowDraw, 42 | isFixedHeader, 43 | isShowLogo, 44 | isShowTagViews, 45 | isShowBreadcrumb, 46 | isShowBreadcrumbIcon, 47 | isInverted, 48 | triggerStyle, 49 | collapsedWidth, 50 | collapsedIconSize, 51 | globalTheme, 52 | menuMode 53 | } = storeToRefs(settingStore) 54 | 55 | // 折叠菜单风格 56 | const triggerOptions = ref([ 57 | { label: '竖线', value: 'bar' }, 58 | { label: '圆角', value: 'arrow-circle' }, 59 | { label: '自定义', value: 'custom' } 60 | ]) 61 | 62 | // 主题色 63 | const themeOptions = ref([ 64 | // { label: '默认', value: 'default' }, 65 | // { label: '蓝色', value: 'blue' }, 66 | // { label: '红色', value: 'red' } 67 | { label: '亮色主题', value: 'lightTheme' }, 68 | { label: '暗色主题', value: 'darkTheme' } 69 | ]) 70 | 71 | // 菜单风格 72 | const menuStyleOptions = ref([ 73 | { label: '侧栏菜单', value: 'vertical' }, 74 | { label: '顶栏菜单', value: 'horizontal' } 75 | ]) 76 | 77 | return () => ( 78 | 79 | 80 | 主题 81 | 82 | 83 | 88 | 89 | 90 | 91 | 配置项 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | {isShowBreadcrumb && ( 106 | 107 | 108 | 109 | )} 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 125 | 126 | 127 | 128 | 133 | 134 | 135 | 136 | 142 | 143 | 144 | 145 | 151 | 152 | 153 | 154 | 155 | ) 156 | } 157 | }) 158 | 159 | export default GlobalDraw 160 | -------------------------------------------------------------------------------- /src/components/RightTool/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ColumnHeightOutlined, 3 | SettingOutlined, 4 | SearchOutlined, 5 | SyncOutlined, 6 | DragOutlined, 7 | VerticalLeftOutlined, 8 | VerticalRightOutlined 9 | } from '@vicons/antd' 10 | import { 11 | NButton, 12 | NCheckbox, 13 | NCheckboxGroup, 14 | NDropdown, 15 | NIcon, 16 | NPopover, 17 | NSpace, 18 | NTooltip 19 | } from 'naive-ui' 20 | import type { MaybeArray } from 'naive-ui/es/_utils' 21 | import type { Component, PropType } from 'vue' 22 | import TipIcon from '../TipIcon' 23 | import DragItem from './DragItem' 24 | 25 | // 密度 26 | const options = [ 27 | { label: '紧凑', key: 'small' }, 28 | { label: '默认', key: 'medium' }, 29 | { label: '宽松', key: 'large' } 30 | ] 31 | 32 | const RightTool = defineComponent({ 33 | name: 'RightTool', 34 | props: { 35 | showSearch: { 36 | type: Boolean as PropType, 37 | default: true 38 | } 39 | }, 40 | emits: ['queryTable', 'update:showSearch', 'changeSize'], 41 | setup(props, { emit }) { 42 | const toggleSearch = () => { 43 | emit('update:showSearch', !props.showSearch) 44 | } 45 | 46 | const handleRefresh = () => { 47 | emit('queryTable') 48 | } 49 | // 列 50 | const showPop = ref(false) 51 | const handleColumns = () => { 52 | // 53 | } 54 | 55 | // 选择 56 | const handleSelect = (key: string) => { 57 | emit('changeSize', key) 58 | } 59 | 60 | const allShowColumn = ref(true) 61 | const canSelect = ref(false) 62 | const popHeader = () => ( 63 | 64 | 列展示 65 | 勾选列 66 | 67 | 重置 68 | 69 | 70 | ) 71 | const popDefault = () => ( 72 |
73 | 74 | 75 | {[1, 2, 3, 4].map((i) => ( 76 | 77 | ))} 78 | 79 | 80 |
81 | ) 82 | 83 | // 弹出 84 | const popSlots = (click: (e: MouseEvent) => void, icon: Component) => ({ 85 | trigger: () => ( 86 | {h(icon)} 91 | }} 92 | /> 93 | ), 94 | header: popHeader, 95 | default: popDefault 96 | }) 97 | 98 | const tipSlots = ( 99 | icon: Component, 100 | tipTitle: string, 101 | click?: (e: MouseEvent) => void 102 | ) => { 103 | return { 104 | trigger: () => { 105 | switch (tipTitle) { 106 | case '密度': 107 | return ( 108 |
109 | {/* 没有外面这一层 div 不显示 tip */} 110 | 115 | {h(icon)} 119 | }} 120 | /> 121 | 122 |
123 | ) 124 | 125 | case '列设置': 126 | return ( 127 |
128 | 134 |
135 | ) 136 | 137 | default: 138 | return ( 139 | {h(icon)} 144 | }} 145 | /> 146 | ) 147 | } 148 | }, 149 | default: () => {tipTitle} 150 | } 151 | } 152 | 153 | return () => ( 154 | 155 | 159 | 163 | 167 | 171 | 172 | ) 173 | } 174 | }) 175 | 176 | export default RightTool 177 | -------------------------------------------------------------------------------- /src/components/layouts/BaseHeader.tsx: -------------------------------------------------------------------------------- 1 | import type { PropType } from 'vue' 2 | import { NAvatar, NDivider, NDropdown, NLayoutHeader, NSpace } from 'naive-ui' 3 | import styles from './style/header.module.less' 4 | import TipIcon from '@/components/TipIcon' 5 | import { 6 | GithubOutlined, 7 | UserOutlined, 8 | SettingOutlined, 9 | LogoutOutlined, 10 | FullscreenExitOutlined, 11 | FullscreenOutlined, 12 | ProfileOutlined 13 | } from '@vicons/antd' 14 | import { renderIcon } from '@/utils' 15 | import { useSettingStore } from '@/store' 16 | import TriggerCollapse from '../TriggerCollapse' 17 | import type { MenuMode, TriggerStyle } from '@/settings' 18 | import Breadcrumb from '../Breadcrumb' 19 | import { isFullscreen, toggleScreen } from '@/hooks' 20 | import Menu from './Menu' 21 | import BaseLogo from './BaseLogo' 22 | 23 | const BaseHeader = defineComponent({ 24 | name: 'BaseHeader', 25 | props: { 26 | isCollapse: Boolean, 27 | isInverted: Boolean, 28 | triggerStyle: String as PropType, 29 | collapsedWidth: Number, 30 | collapsedIconSize: Number, 31 | menuMode: { 32 | type: String as PropType, 33 | default: 'vertical' 34 | } 35 | }, 36 | setup(props) { 37 | const settingStore = useSettingStore() 38 | const { isShowBreadcrumb, isShowBreadcrumbIcon } = storeToRefs(settingStore) 39 | 40 | const userUrl = 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg' 41 | 42 | const dropOptions = [ 43 | { 44 | label: '个人中心', 45 | key: 'personCenter', 46 | icon: renderIcon(UserOutlined) 47 | }, 48 | { 49 | label: '全局设置', 50 | key: 'setting', 51 | icon: renderIcon(SettingOutlined) 52 | }, 53 | { 54 | type: 'render', 55 | key: 'divider', 56 | render: () => 57 | }, 58 | { 59 | label: '退出登录', 60 | key: 'logout', 61 | icon: renderIcon(LogoutOutlined) 62 | } 63 | ] 64 | 65 | const handleSelect = (key: string) => { 66 | console.log(key) 67 | switch (key) { 68 | case 'setting': 69 | settingStore.changeSetting('isShowDraw', true) 70 | break 71 | 72 | default: 73 | break 74 | } 75 | } 76 | 77 | // 跳转 78 | const goToGithub = () => { 79 | window.open('https://github.com/WalkAlone0325', '_blank') 80 | } 81 | 82 | return () => ( 83 | 88 |
96 | {props.menuMode === 'vertical' ? ( 97 | 98 | {props.triggerStyle === 'custom' && ( 99 | 103 | )} 104 | 105 | {isShowBreadcrumb.value && ( 106 | 114 | )} 115 | 116 | ) : ( 117 | 118 | 119 | 125 | 126 | )} 127 | 128 | 129 | {isFullscreen.value ? ( 130 | 135 | ) : ( 136 | 141 | )} 142 | 143 | 148 | 149 | 150 | 156 | 157 | 158 |
159 |
160 | ) 161 | } 162 | }) 163 | 164 | export default BaseHeader 165 | -------------------------------------------------------------------------------- /src/views/list/search-table/search-table.tsx: -------------------------------------------------------------------------------- 1 | import DButton from '@/components/DButton' 2 | import RightTool from '@/components/RightTool' 3 | import { 4 | DeleteOutlined, 5 | EditOutlined, 6 | PlusOutlined, 7 | SettingOutlined 8 | } from '@vicons/antd' 9 | import type { DataTableColumns } from 'naive-ui' 10 | import { NIcon } from 'naive-ui' 11 | import { NSpace } from 'naive-ui' 12 | import { useMessage } from 'naive-ui' 13 | import { NButton, NDataTable, NTag } from 'naive-ui' 14 | // import useDataTable from '@/hooks/dataTable' 15 | 16 | type RowData = { 17 | key: number 18 | name: string 19 | age: number 20 | address: string 21 | tags: string[] 22 | } 23 | 24 | const createColumns = ({ 25 | sendMail, 26 | handleDel 27 | }: { 28 | sendMail: (rowData: RowData) => void 29 | handleDel: (rowData: RowData) => void 30 | }): DataTableColumns => { 31 | return [ 32 | { 33 | title: 'Name', 34 | key: 'name' 35 | }, 36 | { 37 | title: 'Age', 38 | key: 'age' 39 | }, 40 | { 41 | title: 'Address', 42 | key: 'address' 43 | }, 44 | { 45 | title: 'Tags', 46 | key: 'tags', 47 | render(row) { 48 | const tags = row.tags.map((tagKey) => { 49 | return h( 50 | NTag, 51 | { 52 | style: { 53 | marginRight: '6px' 54 | }, 55 | type: 'info', 56 | bordered: false 57 | }, 58 | { 59 | default: () => tagKey 60 | } 61 | ) 62 | }) 63 | return tags 64 | } 65 | }, 66 | { 67 | title: 'Action', 68 | key: 'actions', 69 | render(row) { 70 | return ( 71 | 72 | sendMail(row)} 77 | /> 78 | handleDel(row)} 84 | /> 85 | 86 | ) 87 | } 88 | } 89 | ] 90 | } 91 | 92 | const createData = (): RowData[] => { 93 | const resList: RowData[] = [] 94 | for (let i = 0; i < 40; i++) { 95 | if (Math.random() * 10 > 5) { 96 | resList.push({ 97 | key: i++, 98 | name: 'John Brown', 99 | age: 32, 100 | address: 'New York No. 1 Lake Park', 101 | tags: ['nice', 'developer'] 102 | }) 103 | } else { 104 | resList.push({ 105 | key: i++, 106 | name: 'Jim Green', 107 | age: 42, 108 | address: 'London No. 1 Lake Park', 109 | tags: ['wow'] 110 | }) 111 | } 112 | } 113 | return resList 114 | } 115 | 116 | const SearchTable = defineComponent({ 117 | name: 'SearchTable', 118 | setup() { 119 | const message = useMessage() 120 | let size = ref('medium') 121 | 122 | // const { columns, data, pagination } = useDataTable() 123 | const data = createData() 124 | const columns = createColumns({ 125 | sendMail(rowData) { 126 | message.info(`send mail to ${rowData.name}`) 127 | }, 128 | handleDel(rowData) { 129 | message.error(`send mail to ${rowData.name}`) 130 | } 131 | }) 132 | const pagination = { 133 | pageNum: 1, 134 | pageSize: 10 135 | } 136 | 137 | // EyeOutlined 明细 ExportOutlined 导出 EditOutlined 编辑 DownloadOutlined 下载 DeleteOutlined 删除 CloseOutlined 关闭 CheckOutlined 检测 138 | const handleAdd = () => { 139 | message.success('成功') 140 | } 141 | 142 | return () => ( 143 |
144 | {/* 操作栏 */} 145 | 146 | 147 | 148 | 154 | 160 | 166 | 167 | 168 | { 170 | size.value = payload 171 | }} 172 | /> 173 | {/* 174 | 180 | 186 | 192 | */} 193 | 194 | {/* 数据表格 */} 195 | 201 |
202 | ) 203 | } 204 | }) 205 | 206 | export default SearchTable 207 | --------------------------------------------------------------------------------