├── .env.example ├── .eslintrc ├── .github └── workflows │ ├── ci.yml │ ├── docs-deploy.yml │ └── release.yml ├── .gitignore ├── .npmrc ├── .stackblitzrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── app.config.ts ├── app.vue ├── assets ├── css │ ├── main.css │ └── tailwind.css ├── images │ ├── avatar.png │ ├── avatar1.png │ ├── avatar2.png │ ├── avatar3.png │ └── avatar4.png ├── logo.svg └── svg │ └── coworking.svg ├── components ├── admin │ ├── dashboard │ │ ├── GrowCard.vue │ │ ├── SalesProductPie.vue │ │ ├── SiteAnalysis.vue │ │ ├── VisitRadar.vue │ │ └── VisitSource.vue │ └── workplace │ │ ├── DynamicInfo.vue │ │ ├── Header.vue │ │ ├── ProjectCard.vue │ │ └── QuickNav.vue ├── app │ ├── Footer.vue │ ├── Header.vue │ ├── Logo.vue │ ├── Nav.vue │ └── ThemeSwitcher.vue ├── basic │ ├── form │ │ ├── componentMap.ts │ │ ├── helper.ts │ │ ├── hooks │ │ │ ├── useForm.ts │ │ │ ├── useFormContext.ts │ │ │ ├── useFormEvents.ts │ │ │ └── useFormValues.ts │ │ ├── index.vue │ │ ├── props.ts │ │ └── types │ │ │ ├── form.ts │ │ │ └── index.ts │ ├── modal │ │ ├── hooks │ │ │ └── useModal.ts │ │ ├── index.vue │ │ ├── props.ts │ │ └── type │ │ │ └── index.ts │ ├── page │ │ └── index.vue │ └── table │ │ ├── components │ │ ├── TableAction.vue │ │ └── settings │ │ │ ├── ColumnSetting.vue │ │ │ ├── RedoSetting.vue │ │ │ └── SizeSetting.vue │ │ ├── const.ts │ │ ├── hooks │ │ ├── useColumns.ts │ │ ├── useDataSource.ts │ │ ├── useLoading.ts │ │ ├── usePagination.ts │ │ └── useTableContext.ts │ │ ├── index.vue │ │ ├── props.ts │ │ └── types │ │ ├── pagination.ts │ │ ├── table.ts │ │ └── tableAction.ts ├── global │ ├── LoginCard.vue │ └── UserButton.vue └── layout │ ├── Footer.vue │ ├── Header.vue │ ├── Menu.vue │ ├── Sider.vue │ ├── Tabs.vue │ └── components │ ├── Fullscreen.vue │ ├── LocalePicker.vue │ ├── Search.vue │ ├── SearchModal.vue │ ├── SettingDrawer.vue │ ├── TabContent.vue │ ├── TabRedo.vue │ └── ThemeSetting.vue ├── composables ├── setting │ ├── useAppSetting.ts │ ├── useDevice.ts │ ├── useHeaderSetting.ts │ └── userMenuSetting.ts └── web │ ├── useAuth.ts │ ├── useMenuSearch.ts │ ├── useScrollTo.ts │ └── useTabs.ts ├── config └── i18n.ts ├── constants ├── menu.ts ├── setting.ts └── site.ts ├── content └── help.md ├── docs ├── .env.example ├── .eslintignore ├── .eslintrc.cjs ├── .gitignore ├── .npmrc ├── app.config.ts ├── components │ ├── Logo.vue │ └── content │ │ └── Releases.vue ├── content │ ├── 0.index.md │ ├── 1.introduction │ │ ├── 1.introduction.md │ │ ├── 2.getting-started.md │ │ └── _dir.yml │ ├── 2.usage │ │ ├── 1.configuration.md │ │ └── _dir.yml │ └── 5.changelog.md ├── nuxt.config.ts ├── package.json ├── pnpm-lock.yaml ├── public │ ├── cover.png │ └── favicon.svg ├── renovate.json ├── tokens.config.ts └── tsconfig.json ├── ecosystem.config.js ├── error.vue ├── layouts ├── admin.vue └── default.vue ├── locales ├── en.json └── zh-CN.json ├── middleware └── admin.ts ├── netlify.toml ├── nuxt.config.ts ├── package.json ├── pages ├── 404.vue ├── account │ └── profile.vue ├── admin │ ├── about.vue │ ├── comp.vue │ ├── comp │ │ ├── form │ │ │ └── index.vue │ │ ├── page │ │ │ └── index.vue │ │ └── table │ │ │ └── index.vue │ ├── dashboard │ │ └── index.vue │ ├── external.vue │ ├── external │ │ ├── naive-ui.vue │ │ ├── nuxt.vue │ │ └── supabase.vue │ ├── level.vue │ ├── level │ │ ├── menu1.vue │ │ ├── menu1 │ │ │ └── menu1-1.vue │ │ └── menu2.vue │ ├── system.vue │ ├── system │ │ └── user │ │ │ └── index.vue │ └── workplace │ │ └── index.vue ├── auth │ ├── confirm.vue │ ├── login.vue │ ├── reset-password.vue │ └── update-password.vue ├── index.vue └── redirect │ └── [...path].vue ├── plugins └── chart.ts ├── pnpm-lock.yaml ├── public ├── img │ └── preview.png ├── logo.svg └── og.png ├── server ├── api │ ├── admin │ │ ├── dashboard │ │ │ └── console.ts │ │ └── system │ │ │ └── users │ │ │ ├── [id].delete.ts │ │ │ ├── [id].put.ts │ │ │ ├── index.get.ts │ │ │ └── index.post.ts │ └── github │ │ └── user │ │ └── [username].get.ts ├── error.ts ├── middleware │ └── 0.auth.ts ├── protocol │ └── github │ │ ├── index.ts │ │ └── types.d.ts ├── routes │ └── hello.ts └── utils │ └── index.ts ├── stores ├── settings.ts └── tab.ts ├── tailwind.config.ts ├── tsconfig.json ├── types ├── database.types.ts ├── global.d.ts └── index.d.ts ├── utils ├── date.ts ├── domUtils.ts ├── index.ts ├── is.ts ├── request.ts └── tree.ts └── vitest.config.ts /.env.example: -------------------------------------------------------------------------------- 1 | PORT=8010 2 | 3 | BASE_URL="http://localhost:8010" 4 | 5 | # Supabase 6 | SUPABASE_URL="http://db.example.supabase.co" 7 | SUPABASE_KEY="" 8 | SUPABASE_SERVICE_KE="" 9 | 10 | NUXT_PUBLIC_ADMIN_UID="" -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antfu", 3 | "rules": { 4 | "@typescript-eslint/ban-types": "off", 5 | "n/prefer-global/process": "off" 6 | } 7 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest] # macos-latest, windows-latest 19 | node: [18] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Set node version to ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - run: corepack enable 30 | 31 | - name: Setup 32 | run: npm i -g @antfu/ni 33 | 34 | - name: Install 35 | run: nci 36 | 37 | - name: Build 38 | run: nr build 39 | 40 | # - name: SSH Deploy 41 | # uses: easingthemes/ssh-deploy@v2.2.11 42 | # env: 43 | # SSH_PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 44 | # ARGS: '-avzr --delete' 45 | # SOURCE: '.output' 46 | # REMOTE_HOST: ${{ secrets.REMOTE_HOST }} 47 | # REMOTE_USER: ${{ secrets.REMOTE_USER }} 48 | # TARGET: '/www/wwwroot/NodeProject/nuxt-naive-admin' 49 | 50 | # - name: SSH Commands 51 | # uses: appleboy/ssh-action@master 52 | # with: 53 | # host: ${{ secrets.REMOTE_HOST }} 54 | # username: ${{ secrets.REMOTE_USER }} 55 | # key: ${{ secrets.PRIVATE_KEY }} 56 | # script: pm2 restart nuxt-naive-admin -------------------------------------------------------------------------------- /.github/workflows/docs-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docs Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'docs/**' 9 | 10 | jobs: 11 | deploy-docs: 12 | runs-on: ${{ matrix.os }} 13 | 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest] # macos-latest, windows-latest 17 | node: [18] 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Set node version to ${{ matrix.node-version }} 23 | uses: actions/setup-node@v3 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | 27 | - run: corepack enable 28 | 29 | - name: Setup 30 | working-directory: ./docs 31 | run: npm i -g @antfu/ni 32 | 33 | - name: Install 34 | working-directory: ./docs 35 | run: nci 36 | 37 | - name: Generate 38 | working-directory: ./docs 39 | run: nr generate 40 | 41 | # - name: Deploy 42 | # uses: JamesIves/github-pages-deploy-action@v4 43 | # with: 44 | # token: ${{ secrets.GITHUB_TOKEN }} 45 | # folder: docs/.output/public 46 | 47 | - name: Deploy to Vercel 48 | working-directory: ./docs 49 | run: npx vercel --token ${VERCEL_TOKEN} --prod 50 | env: 51 | VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} 52 | VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} 53 | VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} 54 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: actions/setup-node@v3 19 | with: 20 | node-version: lts/* 21 | 22 | - run: npx changelogithub 23 | env: 24 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | dist 4 | .output 5 | .nuxt 6 | .env 7 | .env.production 8 | .vercel 9 | data -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "installDependencies": true, 3 | "startCommand": "npm run dev" 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "antfu.iconify", 4 | "antfu.goto-alias", 5 | "csstools.postcss", 6 | "dbaeumer.vscode-eslint", 7 | "vue.volar", 8 | "bradlc.vscode-tailwindcss", 9 | "lokalise.i18n-ally" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "editor.formatOnSave": false, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true, 6 | "source.organizeImports": false 7 | }, 8 | "files.associations": { 9 | "*.css": "postcss" 10 | }, 11 | "i18n-ally.keystyle": "nested", 12 | "i18n-ally.localesPaths": [ 13 | "locales" 14 | ], 15 | "i18n-ally.preferredDelimiter": "_", 16 | "i18n-ally.sortKeys": true, 17 | "i18n-ally.sourceLanguage": "zh-CN", 18 | "vue.codeActions.enabled": false 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-PRESENT Kuizuo 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

6 | Artwork from Nuxt 7 |

8 |

Nuxt Naive Admin

9 |

一站式管理系统,融合 Nuxt、Naive UI 和 Supabase。

10 | 11 |

13 |

14 |

15 | size 16 | size 17 | License 18 |

19 | 20 |

21 | 🗺️ 项目介绍 | 22 | 📚 文档 23 |

24 | 25 |
26 | 27 | > **Warning**: 本项目还处于开发阶段, 暂不建议用于生产环境。 28 | 29 |
30 | 31 | ## ✨ 特性 32 | 33 | - 一个完整的 Nuxt 全栈项目 34 | - 使用 naive-ui 组件 35 | - 集成 supabase 服务 36 | 37 | ## 🖥️ 演示 38 | 39 | - [nuxt-naive-admin.vercel.app](https://nuxt-naive-admin.vercel.app) 40 | 41 | 管理员账号:admin@kuizuo.cn 密码:Aa123456 42 | 43 | ## 🚀 快速开始 44 | 45 | ``` 46 | git clone https://github.com/kuizuo/nuxt-naive-admin 47 | cd nuxt-naive-admin 48 | pnpm i 49 | pnpm run dev 50 | ``` 51 | 52 | ## 🎉 部署 53 | 54 | ### 服务器 55 | 56 | 这里使用 pm2 进行部署,创建 `ecosystem.config.js` 文件: 57 | 58 | ```js 59 | module.exports = { 60 | apps: [ 61 | { 62 | name: 'Nuxt-Naive-Admin', 63 | exec_mode: 'cluster', 64 | instances: '1', 65 | env: { 66 | NITRO_PORT: 8010, 67 | NITRO_HOST: '127.0.0.1', 68 | NODE_ENV: 'production', 69 | }, 70 | script: './.output/server/index.mjs', 71 | }, 72 | ], 73 | } 74 | ``` 75 | 76 | 执行 `pm2 start ecosystem.config.js` 即可。 77 | 78 | ## 📝 License 79 | 80 | [MIT](./LICENSE) License © 2023-PRESENT [Kuizuo](https://github.com/kuizuo) 81 | -------------------------------------------------------------------------------- /app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | title: 'nuxt-naive-admin', 3 | description: '一站式管理系统,融合 Nuxt、Naive UI 和 Supabase。', 4 | author: { 5 | name: 'kuizuo', 6 | link: 'https://github.com/kuizuo', 7 | }, 8 | nuxtIcon: { 9 | size: '20px', 10 | class: 'icon', 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /app.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 72 | -------------------------------------------------------------------------------- /assets/css/main.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --app-header-height: 48px; 3 | --app-footer-height: 48px; 4 | --app-tabs-height: 40px; 5 | } 6 | 7 | .page-enter-active, 8 | .page-leave-active { 9 | transition: all 0.4s; 10 | } 11 | .page-enter-from, 12 | .page-leave-to { 13 | transform: translateY(20px); 14 | opacity: 0; 15 | } 16 | 17 | .layout-enter-active , 18 | .layout-leave-active { 19 | transition: all 0.1s ease-out; 20 | transition: all 0.4s; 21 | } 22 | .layout-enter-from, 23 | .layout-leave-to { 24 | transform: translateY(-20px); 25 | opacity: 0; 26 | } 27 | -------------------------------------------------------------------------------- /assets/css/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | @tailwind variants; 5 | 6 | @layer base { 7 | button, [type='button'], [type='reset'], [type='submit'] { 8 | background-color: var(--n-color) 9 | } 10 | } 11 | 12 | @layer components { 13 | .icon-btn { 14 | @apply inline-block w-5 h-5 cursor-pointer select-none transition duration-200 ease-in-out 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /assets/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuizuo/nuxt-naive-admin/47611c3a84db8c7fe5c6d11f6cc29c9a821b3ca7/assets/images/avatar.png -------------------------------------------------------------------------------- /assets/images/avatar1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuizuo/nuxt-naive-admin/47611c3a84db8c7fe5c6d11f6cc29c9a821b3ca7/assets/images/avatar1.png -------------------------------------------------------------------------------- /assets/images/avatar2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuizuo/nuxt-naive-admin/47611c3a84db8c7fe5c6d11f6cc29c9a821b3ca7/assets/images/avatar2.png -------------------------------------------------------------------------------- /assets/images/avatar3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuizuo/nuxt-naive-admin/47611c3a84db8c7fe5c6d11f6cc29c9a821b3ca7/assets/images/avatar3.png -------------------------------------------------------------------------------- /assets/images/avatar4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuizuo/nuxt-naive-admin/47611c3a84db8c7fe5c6d11f6cc29c9a821b3ca7/assets/images/avatar4.png -------------------------------------------------------------------------------- /assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/admin/dashboard/GrowCard.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 79 | -------------------------------------------------------------------------------- /components/admin/dashboard/SalesProductPie.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 46 | -------------------------------------------------------------------------------- /components/admin/dashboard/SiteAnalysis.vue: -------------------------------------------------------------------------------- 1 | 108 | 109 | 133 | -------------------------------------------------------------------------------- /components/admin/dashboard/VisitRadar.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 78 | -------------------------------------------------------------------------------- /components/admin/dashboard/VisitSource.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 66 | -------------------------------------------------------------------------------- /components/admin/workplace/DynamicInfo.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 40 | -------------------------------------------------------------------------------- /components/admin/workplace/Header.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 39 | -------------------------------------------------------------------------------- /components/admin/workplace/ProjectCard.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 42 | 43 | 48 | -------------------------------------------------------------------------------- /components/admin/workplace/QuickNav.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 26 | 27 | 32 | -------------------------------------------------------------------------------- /components/app/Footer.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 44 | -------------------------------------------------------------------------------- /components/app/Header.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 43 | -------------------------------------------------------------------------------- /components/app/Logo.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | -------------------------------------------------------------------------------- /components/app/Nav.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 23 | -------------------------------------------------------------------------------- /components/app/ThemeSwitcher.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | -------------------------------------------------------------------------------- /components/basic/form/componentMap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | NAutoComplete, 3 | NCascader, 4 | NCheckbox, 5 | NCheckboxGroup, 6 | NColorPicker, 7 | NDatePicker, 8 | NDivider, 9 | NDynamicInput, 10 | NDynamicTags, 11 | NInput, 12 | NInputGroup, 13 | NInputNumber, 14 | NMention, 15 | NRadio, 16 | NRadioButton, 17 | NRadioGroup, 18 | NRate, 19 | NSelect, 20 | NSlider, 21 | NSwitch, 22 | NTimePicker, 23 | NTreeSelect, 24 | NUpload, 25 | } from 'naive-ui' 26 | import type { ComponentType } from './types/index' 27 | 28 | const componentMap = new Map() 29 | 30 | componentMap.set('NInput', NInput) 31 | componentMap.set('NInputGroup', NInputGroup) 32 | componentMap.set('NInputNumber', NInputNumber) 33 | componentMap.set('NAutoComplete', NAutoComplete) 34 | componentMap.set('NMention', NMention) 35 | componentMap.set('NColorPicker', NColorPicker) 36 | 37 | componentMap.set('NSelect', NSelect) 38 | componentMap.set('NTreeSelect', NTreeSelect) 39 | componentMap.set('NSwitch', NSwitch) 40 | componentMap.set('NRadio', NRadio) 41 | componentMap.set('NRadioButton', NRadioButton) 42 | componentMap.set('NRadioGroup', NRadioGroup) 43 | componentMap.set('NCheckbox', NCheckbox) 44 | componentMap.set('NCheckboxGroup', NCheckboxGroup) 45 | componentMap.set('NCascader', NCascader) 46 | componentMap.set('NSlider', NSlider) 47 | componentMap.set('NRate', NRate) 48 | 49 | componentMap.set('NDatePicker', NDatePicker) 50 | componentMap.set('NTimePicker', NTimePicker) 51 | 52 | componentMap.set('NUpload', NUpload) 53 | componentMap.set('NDynamicInput', NDynamicInput) 54 | componentMap.set('NDynamicTags', NDynamicTags) 55 | componentMap.set('NDivider', NDivider) 56 | 57 | export { componentMap } 58 | -------------------------------------------------------------------------------- /components/basic/form/helper.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentType } from './types/index' 2 | 3 | export function createPlaceholderMessage(component: ComponentType) { 4 | if (component === 'NInput') 5 | return '请输入' 6 | if ( 7 | ['NPicker', 'NSelect', 'NCheckbox', 'NRadio', 'NSwitch', 'NDatePicker', 'NTimePicker'].includes( 8 | component, 9 | ) 10 | ) 11 | return '请选择' 12 | return '' 13 | } 14 | 15 | export function defaultType(component: string) { 16 | if (component === 'NInput') 17 | return '' 18 | if (component === 'NInputNumber') 19 | return null 20 | return [ 21 | 'NPicker', 22 | 'NSelect', 23 | 'NCheckbox', 24 | 'NRadio', 25 | 'NSwitch', 26 | 'NDatePicker', 27 | 'NTimePicker', 28 | ].includes(component) 29 | ? '' 30 | : undefined 31 | } 32 | 33 | export const dateItemType = ['NDatePicker', 'NTimePicker'] 34 | -------------------------------------------------------------------------------- /components/basic/form/hooks/useForm.ts: -------------------------------------------------------------------------------- 1 | import type { FormActionType, FormProps, FormSchema, UseFormReturnType } from '../types/form' 2 | 3 | type Props = Partial 4 | 5 | export function useForm(props?: Props): UseFormReturnType { 6 | const formRef = ref(null) 7 | const loadedRef = ref(false) 8 | 9 | async function getForm() { 10 | const form = unref(formRef) 11 | if (!form) { 12 | console.error( 13 | 'The form instance has not been obtained, please make sure that the form has been rendered when performing the form operation!', 14 | ) 15 | } 16 | await nextTick() 17 | return form as FormActionType 18 | } 19 | 20 | function register(instance: FormActionType) { 21 | if (unref(loadedRef) && instance === unref(formRef)) 22 | return 23 | 24 | formRef.value = instance 25 | loadedRef.value = true 26 | 27 | watch( 28 | () => props, 29 | () => { 30 | props && instance.setProps(props) 31 | }, 32 | { 33 | immediate: true, 34 | deep: true, 35 | }, 36 | ) 37 | } 38 | 39 | const methods: FormActionType = { 40 | setProps: async (formProps: Partial) => { 41 | const form = await getForm() 42 | await form.setProps(formProps) 43 | }, 44 | 45 | resetFields: async () => { 46 | getForm().then(async (form) => { 47 | await form.resetFields() 48 | }) 49 | }, 50 | 51 | restoreValidation: async (name?: string | string[]) => { 52 | const form = await getForm() 53 | await form.restoreValidation(name) 54 | }, 55 | 56 | getFieldsValue: () => { 57 | return unref(formRef)?.getFieldsValue() as T 58 | }, 59 | 60 | setFieldsValue: async (values: T) => { 61 | const form = await getForm() 62 | await form.setFieldsValue(values) 63 | }, 64 | 65 | submit: async (): Promise => { 66 | const form = await getForm() 67 | return form.submit() 68 | }, 69 | 70 | validate: async (): Promise> => { 71 | const form = await getForm() 72 | return form.validate() 73 | }, 74 | 75 | setLoading: (value: boolean) => { 76 | loadedRef.value = value 77 | }, 78 | 79 | updateSchema: async (values) => { 80 | const form = await getForm() 81 | form.updateSchema(values) 82 | }, 83 | 84 | resetSchema: async (data: Partial | Partial[]) => { 85 | const form = await getForm() 86 | form.resetSchema(data) 87 | }, 88 | } 89 | 90 | return [register, methods] 91 | } 92 | -------------------------------------------------------------------------------- /components/basic/form/hooks/useFormContext.ts: -------------------------------------------------------------------------------- 1 | import { inject, provide } from 'vue' 2 | 3 | export interface FormContextProps { 4 | resetAction: () => Promise 5 | submitAction: () => Promise 6 | } 7 | 8 | const key: InjectionKey = Symbol('formElRef') 9 | 10 | export function createFormContext(instance: FormContextProps) { 11 | provide(key, instance) 12 | } 13 | 14 | export function useFormContext() { 15 | return inject(key) 16 | } 17 | -------------------------------------------------------------------------------- /components/basic/form/hooks/useFormValues.ts: -------------------------------------------------------------------------------- 1 | import { unref } from 'vue' 2 | import type { ComputedRef, Ref } from 'vue' 3 | import { set } from 'lodash-es' 4 | import type { FormSchema } from '../types/form' 5 | import { isArray, isFunction, isNullOrUnDef, isObject, isString } from '~/utils/is' 6 | 7 | interface UseFormValuesContext { 8 | defaultValueRef: Ref 9 | getSchema: ComputedRef 10 | formModel: Record 11 | } 12 | export function useFormValues({ defaultValueRef, getSchema, formModel }: UseFormValuesContext) { 13 | function handleFormValues(values: Record) { 14 | if (!isObject(values)) 15 | return {} 16 | 17 | const res: Record = {} 18 | for (const item of Object.entries(values)) { 19 | let [, value] = item 20 | const [key] = item 21 | if ( 22 | !key 23 | || (isArray(value) && value.length === 0) 24 | || isFunction(value) 25 | || isNullOrUnDef(value) 26 | ) 27 | continue 28 | 29 | // 删除空格 30 | if (isString(value)) 31 | value = value.trim() 32 | 33 | set(res, key, value) 34 | } 35 | return res 36 | } 37 | 38 | // 初始化默认值 39 | function initDefault() { 40 | const schemas = unref(getSchema) 41 | const obj: Record = {} 42 | schemas.forEach((item) => { 43 | const { defaultValue } = item 44 | if (!isNullOrUnDef(defaultValue)) { 45 | obj[item.field] = defaultValue 46 | formModel[item.field] = defaultValue 47 | } 48 | }) 49 | defaultValueRef.value = obj 50 | } 51 | 52 | return { handleFormValues, initDefault } 53 | } 54 | -------------------------------------------------------------------------------- /components/basic/form/props.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties, PropType } from 'vue' 2 | import type { GridItemProps, GridProps } from 'naive-ui/lib/grid' 3 | import type { ButtonProps } from 'naive-ui/lib/button' 4 | import type { FormSchema } from './types/form' 5 | 6 | export const basicProps = { 7 | labelWidth: { 8 | type: [Number, String] as PropType, 9 | }, 10 | schemas: { 11 | type: [Array] as PropType, 12 | default: () => [], 13 | }, 14 | layout: { 15 | type: String, 16 | default: 'horizontal', 17 | }, 18 | inline: { 19 | type: Boolean, 20 | default: false, 21 | }, 22 | size: { 23 | type: String, 24 | default: 'medium', 25 | }, 26 | labelPlacement: { 27 | type: String, 28 | default: 'left', 29 | }, 30 | isFull: { 31 | type: Boolean, 32 | default: true, 33 | }, 34 | showActionButtonGroup: { 35 | type: Boolean, 36 | default: true, 37 | }, 38 | showResetButton: { 39 | type: Boolean, 40 | default: true, 41 | }, 42 | resetButtonOptions: Object as PropType>, 43 | showSubmitButton: { 44 | type: Boolean, 45 | default: true, 46 | }, 47 | submitButtonOptions: Object as PropType>, 48 | showAdvancedButton: { 49 | type: Boolean, 50 | default: true, 51 | }, 52 | submitButtonText: { 53 | type: String, 54 | default: '提交', 55 | }, 56 | resetButtonText: { 57 | type: String, 58 | default: '重置', 59 | }, 60 | gridProps: { 61 | type: Object as PropType, 62 | default(rawProps: GridProps) { 63 | return { cols: 1, xGap: 8 } 64 | }, 65 | }, 66 | giProps: Object as PropType, 67 | baseGridStyle: { 68 | type: Object as PropType, 69 | }, 70 | collapsed: { 71 | type: Boolean, 72 | default: false, 73 | }, 74 | collapsedRows: { 75 | type: Number, 76 | default: 1, 77 | }, 78 | } 79 | -------------------------------------------------------------------------------- /components/basic/form/types/form.ts: -------------------------------------------------------------------------------- 1 | import type { CSSProperties } from 'vue' 2 | import type { GridItemProps, GridProps } from 'naive-ui/lib/grid' 3 | import type { FormItemRule } from 'naive-ui' 4 | 5 | import type { ButtonProps } from 'naive-ui/lib/button' 6 | import type { ComponentType } from './index' 7 | 8 | export interface FormSchema { 9 | field: string 10 | label: string 11 | labelMessage?: string 12 | labelMessageStyle?: object | string 13 | defaultValue?: any 14 | component?: ComponentType 15 | componentProps?: Record 16 | slot?: string 17 | rules?: FormItemRule | FormItemRule[] 18 | giProps?: GridItemProps 19 | isFull?: boolean 20 | suffix?: string 21 | } 22 | 23 | export interface FormProps { 24 | model?: Record 25 | labelWidth?: number | string 26 | schemas?: FormSchema[] 27 | inline: boolean 28 | layout?: string 29 | size: string 30 | labelPlacement: string 31 | isFull: boolean 32 | showActionButtonGroup?: boolean 33 | showResetButton?: boolean 34 | resetButtonOptions?: Partial 35 | showSubmitButton?: boolean 36 | showAdvancedButton?: boolean 37 | submitButtonOptions?: Partial 38 | submitButtonText?: string 39 | resetButtonText?: string 40 | gridProps?: GridProps 41 | giProps?: GridItemProps 42 | resetFunc?: () => Promise 43 | submitFunc?: () => Promise 44 | submitOnReset?: boolean 45 | baseGridStyle?: CSSProperties 46 | collapsedRows?: number 47 | } 48 | 49 | export interface FormActionType { 50 | submit: () => Promise 51 | setProps: (formProps: Partial) => Promise 52 | updateSchema: (data: Partial | Partial[]) => Promise 53 | resetSchema: (data: Partial | Partial[]) => Promise 54 | setFieldsValue: (values: Record | any) => void 55 | restoreValidation: (name?: string | string[]) => Promise 56 | getFieldsValue: () => Record 57 | resetFields: () => Promise 58 | validate: () => Promise 59 | setLoading: (status: boolean) => void 60 | } 61 | 62 | export type RegisterFn = (formInstance: FormActionType) => void 63 | 64 | export type UseFormReturnType = [RegisterFn, FormActionType] 65 | -------------------------------------------------------------------------------- /components/basic/form/types/index.ts: -------------------------------------------------------------------------------- 1 | export type ComponentType = 2 | | 'NInput' 3 | | 'NInputGroup' 4 | | 'NInputNumber' 5 | | 'NSelect' 6 | | 'NTreeSelect' 7 | | 'NMention' 8 | | 'NColorPicker' 9 | | 'NRadio' 10 | | 'NRadioButton' 11 | | 'NRadioGroup' 12 | | 'NRadioButtonGroup' 13 | | 'NCheckbox' 14 | | 'NCheckboxGroup' 15 | | 'NAutoComplete' 16 | | 'NCascader' 17 | | 'NDatePicker' 18 | | 'NMonthPicker' 19 | | 'NRangePicker' 20 | | 'NWeekPicker' 21 | | 'NTimePicker' 22 | | 'NSwitch' 23 | | 'NStrengthMeter' 24 | | 'NUpload' 25 | | 'NIconPicker' 26 | | 'NRender' 27 | | 'NSlider' 28 | | 'NRate' 29 | | 'NDivider' 30 | | 'NUpload' 31 | | 'NDynamicInput' 32 | | 'NDynamicTags' 33 | -------------------------------------------------------------------------------- /components/basic/modal/hooks/useModal.ts: -------------------------------------------------------------------------------- 1 | import type { ModalProps } from 'naive-ui' 2 | import type { ModalMethods, UseModalReturnType } from '../type' 3 | 4 | export function useModal(): UseModalReturnType { 5 | const modal = ref() 6 | const currentInstance = getCurrentInstance() 7 | 8 | function register(modalInstance: ModalMethods) { 9 | if (!getCurrentInstance()) 10 | throw new Error('useModal() can only be used inside setup() or functional components!') 11 | 12 | modal.value = modalInstance 13 | currentInstance?.emit('register', modalInstance) 14 | } 15 | 16 | const getInstance = () => { 17 | const instance = unref(modal.value) 18 | if (!instance) 19 | console.error('useModal instance is undefined!') 20 | 21 | return instance 22 | } 23 | 24 | const methods: ModalMethods = { 25 | setProps: (props: Partial): void => { 26 | getInstance()?.setProps(props) 27 | }, 28 | openModal: (show = true, data?: T): void => { 29 | getInstance()?.openModal() 30 | }, 31 | closeModal: () => { 32 | getInstance()?.closeModal() 33 | }, 34 | setConfirmLoading: (status: boolean) => { 35 | getInstance()?.setConfirmLoading(status) 36 | }, 37 | } 38 | 39 | return [register, methods] 40 | } 41 | -------------------------------------------------------------------------------- /components/basic/modal/index.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 96 | -------------------------------------------------------------------------------- /components/basic/modal/props.ts: -------------------------------------------------------------------------------- 1 | import { NModal } from 'naive-ui' 2 | 3 | export const basicProps = { 4 | ...NModal.props, 5 | okText: { 6 | type: String, 7 | default: '确认', 8 | }, 9 | showIcon: { 10 | type: Boolean, 11 | default: false, 12 | }, 13 | width: { 14 | type: Number, 15 | default: 446, 16 | }, 17 | title: { 18 | type: String, 19 | default: '', 20 | }, 21 | maskClosable: { 22 | type: Boolean, 23 | default: false, 24 | }, 25 | preset: { 26 | type: String, 27 | default: 'dialog', 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /components/basic/modal/type/index.ts: -------------------------------------------------------------------------------- 1 | import type { ModalProps } from 'naive-ui' 2 | 3 | export interface ModalMethods { 4 | setProps: (props: Partial) => void 5 | openModal: (props?: boolean) => void 6 | closeModal: () => void 7 | setConfirmLoading: (status: boolean) => void 8 | } 9 | 10 | export type RegisterFn = (ModalInstance: ModalMethods) => void 11 | 12 | export type UseModalReturnType = [RegisterFn, ModalMethods] 13 | -------------------------------------------------------------------------------- /components/basic/page/index.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 104 | 105 | 127 | -------------------------------------------------------------------------------- /components/basic/table/components/TableAction.vue: -------------------------------------------------------------------------------- 1 | 82 | 83 | 121 | -------------------------------------------------------------------------------- /components/basic/table/components/settings/RedoSetting.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 19 | -------------------------------------------------------------------------------- /components/basic/table/components/settings/SizeSetting.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 47 | -------------------------------------------------------------------------------- /components/basic/table/const.ts: -------------------------------------------------------------------------------- 1 | const { 2 | pageSizeOptions, 3 | defaultPageSize, 4 | fetchSetting, 5 | defaultSize, 6 | defaultFilterFn, 7 | } = { 8 | // Form interface request general configuration 9 | // support xxx.xxx.xxx 10 | fetchSetting: { 11 | // The field name of the current page passed to the background 12 | pageField: 'page', 13 | // The number field name of each page displayed in the background 14 | sizeField: 'pageSize', 15 | // Field name of the form data returned by the interface 16 | listField: 'items', 17 | // Total number of tables returned by the interface field name 18 | totalField: 'total', 19 | }, 20 | // Number of pages that can be selected 21 | pageSizeOptions: [10, 50, 80, 100], 22 | // Default display quantity on one page 23 | defaultPageSize: 10, 24 | // Default Size 25 | defaultSize: 'medium', 26 | // Custom general filter function 27 | defaultFilterFn: (data: Partial>) => { 28 | return data 29 | }, 30 | 31 | } 32 | 33 | export const ROW_KEY = 'key' 34 | 35 | export const PAGE_SIZE_OPTIONS = pageSizeOptions 36 | 37 | export const PAGE_SIZE = defaultPageSize 38 | 39 | export const FETCH_SETTING = fetchSetting 40 | 41 | export const DEFAULT_SIZE = defaultSize 42 | 43 | export const DEFAULT_FILTER_FN = defaultFilterFn 44 | 45 | export const DEFAULT_ALIGN = 'center' 46 | 47 | export const INDEX_COLUMN_FLAG = 'INDEX' 48 | 49 | export const ACTION_COLUMN_FLAG = 'ACTION' 50 | -------------------------------------------------------------------------------- /components/basic/table/hooks/useLoading.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef } from 'vue' 2 | import type { BasicTableProps } from '../types/table' 3 | 4 | export function useLoading(props: ComputedRef) { 5 | const loadingRef = ref(unref(props).loading) 6 | 7 | watch( 8 | () => unref(props).loading, 9 | (loading) => { 10 | loadingRef.value = loading 11 | }, 12 | ) 13 | 14 | const getLoading = computed(() => unref(loadingRef)) 15 | 16 | function setLoading(loading: boolean) { 17 | loadingRef.value = loading 18 | } 19 | 20 | return { getLoading, setLoading } 21 | } 22 | -------------------------------------------------------------------------------- /components/basic/table/hooks/usePagination.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef } from 'vue' 2 | import { computed, ref, unref, watch } from 'vue' 3 | import { isBoolean } from 'lodash-es' 4 | import type { PaginationProps } from '../types/pagination' 5 | import type { BasicTableProps } from '../types/table' 6 | import { PAGE_SIZE, PAGE_SIZE_OPTIONS } from '../const' 7 | 8 | export function usePagination(refProps: ComputedRef) { 9 | const configRef = ref({}) 10 | const show = ref(true) 11 | 12 | watch( 13 | () => unref(refProps).pagination, 14 | (pagination) => { 15 | if (!isBoolean(pagination) && pagination) { 16 | configRef.value = { 17 | ...unref(configRef), 18 | ...(pagination ?? {}), 19 | } 20 | } 21 | }, 22 | ) 23 | 24 | const getPaginationInfo = computed((): PaginationProps | boolean => { 25 | const { pagination } = unref(refProps) 26 | 27 | if (!unref(show) || (isBoolean(pagination) && !pagination)) 28 | return false 29 | 30 | return { 31 | // size: 'small', 32 | page: 1, 33 | pageSize: PAGE_SIZE, 34 | pageSizes: PAGE_SIZE_OPTIONS, 35 | showQuickJumper: true, 36 | showSizePicker: true, 37 | prefix: ({ itemCount }) => { 38 | return `共 ${itemCount} 条数据` 39 | }, 40 | ...(isBoolean(pagination) ? {} : pagination), 41 | ...unref(configRef), 42 | } 43 | }) 44 | 45 | function setPagination(info: Partial) { 46 | const paginationInfo = unref(getPaginationInfo) 47 | 48 | configRef.value = { 49 | ...(!isBoolean(paginationInfo) ? paginationInfo : {}), 50 | ...info, 51 | } 52 | } 53 | 54 | function getPagination() { 55 | return unref(getPaginationInfo) 56 | } 57 | 58 | function getShowPagination() { 59 | return unref(show) 60 | } 61 | 62 | async function setShowPagination(flag: boolean) { 63 | show.value = flag 64 | } 65 | 66 | return { getPagination, getPaginationInfo, setShowPagination, getShowPagination, setPagination } 67 | } 68 | -------------------------------------------------------------------------------- /components/basic/table/hooks/useTableContext.ts: -------------------------------------------------------------------------------- 1 | import type { ComputedRef, Ref } from 'vue' 2 | import { inject, provide } from 'vue' 3 | import type { BasicTableProps, TableActionType } from '../types/table' 4 | 5 | const key = Symbol('basic-table') 6 | 7 | type Instance = TableActionType & { 8 | wrapRef: Ref 9 | getBindValues: ComputedRef> 10 | } 11 | 12 | type RetInstance = Omit & { 13 | getBindValues: ComputedRef 14 | } 15 | 16 | export function createTableContext(instance: Instance) { 17 | provide(key, instance) 18 | } 19 | 20 | export function useTableContext(): RetInstance { 21 | return inject(key) as RetInstance 22 | } 23 | -------------------------------------------------------------------------------- /components/basic/table/index.vue: -------------------------------------------------------------------------------- 1 | 158 | 159 | 192 | -------------------------------------------------------------------------------- /components/basic/table/props.ts: -------------------------------------------------------------------------------- 1 | import { NDataTable } from 'naive-ui' 2 | import type { TableBaseColumn } from 'naive-ui/es/data-table/src/interface' 3 | import type { BasicTableProps } from './types/table' 4 | 5 | export const basicProps: BasicTableProps | any = { 6 | ...NDataTable.props, 7 | title: { 8 | type: String, 9 | default: null, 10 | }, 11 | titleTooltip: { 12 | type: String, 13 | default: null, 14 | }, 15 | size: { 16 | type: String, 17 | default: 'medium', 18 | }, 19 | dataSource: { 20 | type: [Object], 21 | default: () => [], 22 | }, 23 | columns: { 24 | type: [Array] as PropType, 25 | default: () => [], 26 | required: true, 27 | }, 28 | beforeRequest: { 29 | type: Function as PropType<(...arg: any[]) => void | Promise>, 30 | default: null, 31 | }, 32 | request: { 33 | type: Function as PropType<(...arg: any[]) => Promise>, 34 | default: null, 35 | }, 36 | afterRequest: { 37 | type: Function as PropType<(...arg: any[]) => void | Promise>, 38 | default: null, 39 | }, 40 | rowKey: { 41 | type: [String, Function] as PropType string)>, 42 | default: undefined, 43 | }, 44 | pagination: { 45 | type: [Object, Boolean], 46 | default: () => {}, 47 | }, 48 | actionColumn: { 49 | type: Object as PropType, 50 | default: null, 51 | }, 52 | } 53 | -------------------------------------------------------------------------------- /components/basic/table/types/pagination.ts: -------------------------------------------------------------------------------- 1 | export interface PaginationProps { 2 | size?: 'small' | 'medium' | 'large' 3 | page?: number 4 | pageCount?: number 5 | itemCount?: number | undefined 6 | pageSize?: number 7 | pageSizes?: number[] 8 | showSizePicker?: boolean 9 | showQuickJumper?: boolean 10 | prefix?: (obj: { itemCount: number }) => string 11 | } 12 | -------------------------------------------------------------------------------- /components/basic/table/types/tableAction.ts: -------------------------------------------------------------------------------- 1 | import type { NButton } from 'naive-ui' 2 | import type { Component } from 'vue' 3 | 4 | export interface ActionItem extends Partial> { 5 | onClick?: Fn 6 | label?: string 7 | 8 | popConfirm?: PopConfirm 9 | disabled?: boolean 10 | divider?: boolean 11 | // 权限编码控制是否显示 12 | // auth?: PermissionsEnum | PermissionsEnum[] | string | string[] 13 | // 业务控制是否显示 14 | ifShow?: boolean | ((action: ActionItem) => boolean) 15 | } 16 | 17 | export interface PopConfirm { 18 | title: string 19 | okText?: string 20 | cancelText?: string 21 | confirm: Fn 22 | cancel?: Fn 23 | icon?: Component 24 | } 25 | -------------------------------------------------------------------------------- /components/global/UserButton.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 84 | -------------------------------------------------------------------------------- /components/layout/Footer.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | -------------------------------------------------------------------------------- /components/layout/Header.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 94 | 95 | 102 | -------------------------------------------------------------------------------- /components/layout/Menu.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 109 | 110 | 138 | -------------------------------------------------------------------------------- /components/layout/Sider.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 61 | -------------------------------------------------------------------------------- /components/layout/Tabs.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 53 | 54 | 90 | -------------------------------------------------------------------------------- /components/layout/components/Fullscreen.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | -------------------------------------------------------------------------------- /components/layout/components/LocalePicker.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /components/layout/components/Search.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | -------------------------------------------------------------------------------- /components/layout/components/TabContent.vue: -------------------------------------------------------------------------------- 1 | 135 | 136 | 178 | -------------------------------------------------------------------------------- /components/layout/components/TabRedo.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 26 | -------------------------------------------------------------------------------- /components/layout/components/ThemeSetting.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 24 | -------------------------------------------------------------------------------- /composables/setting/useAppSetting.ts: -------------------------------------------------------------------------------- 1 | export function useAppSetting() { 2 | const settings = useSettingsStore() 3 | 4 | const showFooter = computed(() => settings.appSetting.showFooter) 5 | 6 | const showLogo = computed(() => settings.appSetting.showLogo) 7 | 8 | const themeColor = computed(() => settings.appSetting.themeColor) 9 | 10 | function setThemeColor(color: string): void { 11 | settings.setSetting({ 12 | appSetting: { 13 | themeColor: color, 14 | }, 15 | }) 16 | } 17 | 18 | return { 19 | showFooter, 20 | showLogo, 21 | themeColor, 22 | setThemeColor, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /composables/setting/useDevice.ts: -------------------------------------------------------------------------------- 1 | import { breakpointsTailwind } from '@vueuse/core' 2 | 3 | export function useDevice() { 4 | const breakpoints = useBreakpoints(breakpointsTailwind) 5 | const { setMenuSetting } = useMenuSetting() 6 | 7 | const sm = breakpoints.smaller('sm') 8 | const isMobile = ref(sm.value) 9 | 10 | watch(sm, (val: boolean) => { 11 | isMobile.value = val 12 | 13 | setMenuSetting({ 14 | collapsed: val, 15 | }) 16 | }) 17 | 18 | return { 19 | isMobile, 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /composables/setting/useHeaderSetting.ts: -------------------------------------------------------------------------------- 1 | export function useHeaderSetting() { 2 | const settings = useSettingsStore() 3 | 4 | const headerSetting = computed(() => settings.headerSetting) 5 | 6 | const showTabs = computed(() => settings.headerSetting.showTabs) 7 | 8 | return { 9 | headerSetting, 10 | showTabs, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /composables/setting/userMenuSetting.ts: -------------------------------------------------------------------------------- 1 | import { useSettingsStore } from '~~/stores/settings' 2 | 3 | import type { MenuSetting } from '~~/types/config' 4 | 5 | export function useMenuSetting() { 6 | const settingsStore = useSettingsStore() 7 | 8 | const collapsed = computed(() => settingsStore.menuSetting.collapsed) 9 | 10 | const menuType = computed(() => settingsStore.menuSetting.type) 11 | 12 | const menuMode = computed(() => settingsStore.menuSetting.mode) 13 | 14 | const menuWidth = computed(() => settingsStore.menuSetting.menuWidth) 15 | 16 | const menuSetting = computed(() => settingsStore.menuSetting) 17 | 18 | // Set menu configuration 19 | function setMenuSetting(menuSetting: DeepPartial): void { 20 | settingsStore.setSetting({ menuSetting }) 21 | } 22 | 23 | function toggleCollapsed() { 24 | setMenuSetting({ 25 | collapsed: !unref(collapsed), 26 | }) 27 | } 28 | 29 | function setMenuType(type: MenuSetting['type']) { 30 | setMenuSetting({ type }) 31 | } 32 | 33 | return { 34 | setMenuSetting, 35 | toggleCollapsed, 36 | setMenuType, 37 | menuType, 38 | menuMode, 39 | collapsed, 40 | menuWidth, 41 | menuSetting, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /composables/web/useAuth.ts: -------------------------------------------------------------------------------- 1 | import type { MenuOption } from 'naive-ui' 2 | import type { RouteRecordName, RouteRecordRaw } from 'vue-router' 3 | 4 | interface MenuRoute { 5 | key: string 6 | label?: string 7 | name?: RouteRecordName 8 | icon?: string 9 | children?: MenuRoute[] 10 | } 11 | 12 | function getKey(path: string, parentPath: string) { 13 | return path.startsWith('http') ? path : `${parentPath}${parentPath ? '/' : ''}${path}` 14 | } 15 | 16 | export function buildMenuList(routes: Readonly, parentPath = ''): MenuOption[] { 17 | const i18n = useNuxtApp().$i18n 18 | 19 | const menuList: MenuOption[] = [] 20 | 21 | routes 22 | .filter(route => route.meta?.layout === 'admin') 23 | .filter(route => !route.meta?.hideMenu) 24 | .sort((a, b) => a.meta!.order as number - (b.meta!.order as number)) 25 | .forEach((route) => { 26 | const { meta, path, children, name } = route 27 | const { title, icon } = meta! 28 | 29 | const menu: MenuOption = { 30 | label: i18n.t(title), 31 | key: getKey(path, parentPath), 32 | name, 33 | path, 34 | icon: renderIcon(icon as string), 35 | } 36 | 37 | if (children && children.length > 0) 38 | menu.children = buildMenuList(children, menu.key as string) 39 | 40 | menuList.push(menu) 41 | }) 42 | 43 | return menuList 44 | } 45 | 46 | export function buildRouteList(routes: Readonly, parentPath = '') { 47 | const i18n = useNuxtApp().$i18n 48 | 49 | const routeList: MenuRoute[] = [] 50 | 51 | routes 52 | .filter(route => route.meta?.layout === 'admin') 53 | .filter(route => !route.meta?.hideMenu) 54 | .sort((a, b) => a.meta!.order as number - (b.meta!.order as number)) 55 | .forEach((route) => { 56 | const { meta, path, children, name } = route 57 | const { title, icon } = meta! 58 | 59 | const menu: MenuRoute = { 60 | label: i18n.t(title), 61 | key: getKey(path, parentPath), 62 | name, 63 | path, 64 | icon, 65 | } 66 | 67 | if (children && children.length > 0) 68 | menu.children = buildRouteList(children, menu.key as string) 69 | 70 | routeList.push(menu) 71 | }) 72 | 73 | return routeList 74 | } 75 | 76 | export function useAuth() { 77 | const router = useRouter() 78 | 79 | // useState 所存放值不能是不可序列化的,如 renderIcon 80 | const routeList = useState('routeList', () => buildRouteList(router.options.routes)) 81 | 82 | return { 83 | routeList, 84 | // TODO: premissionList 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /composables/web/useMenuSearch.ts: -------------------------------------------------------------------------------- 1 | import { cloneDeep } from 'lodash-es' 2 | 3 | export interface SearchResult { 4 | name: string 5 | path: string 6 | icon?: string 7 | } 8 | 9 | // interface ChangeEvent extends Event { 10 | // target: HTMLInputElement 11 | // } 12 | 13 | // Translate special characters 14 | function transform(c: string) { 15 | const code: string[] = [ 16 | '$', 17 | '(', 18 | ')', 19 | '*', 20 | '+', 21 | '.', 22 | '[', 23 | ']', 24 | '?', 25 | '\\', 26 | '^', 27 | '{', 28 | '}', 29 | '|', 30 | ] 31 | return code.includes(c) ? `\\${c}` : c 32 | } 33 | 34 | function createSearchReg(key: string) { 35 | const keys = [...key].map(item => transform(item)) 36 | const str = ['', ...keys, ''].join('.*') 37 | return new RegExp(str) 38 | } 39 | 40 | export function usemenu_search(emit: any) { 41 | const router = useRouter() 42 | const { routeList: menus } = useAuth() 43 | const searchResult = ref([]) 44 | const keyword = ref('') 45 | const activeIndex = ref(-1) 46 | 47 | let menuList: Array = [] 48 | 49 | const handleSearch = useDebounceFn(search, 200) 50 | 51 | onMounted(async () => { 52 | const list = menus.value 53 | 54 | menuList = cloneDeep(list) 55 | 56 | // forEachTree(menuList, (item) => { 57 | // item.label = t(item.label) 58 | // }) 59 | }) 60 | 61 | function search(e: string) { 62 | const key = e 63 | keyword.value = key.trim() 64 | if (!key) { 65 | searchResult.value = [] 66 | return 67 | } 68 | const reg = createSearchReg(unref(keyword)) 69 | const filterMenu = filterTree(menuList, (item) => { 70 | return reg.test(item.label) && !item.hideMenu 71 | }) 72 | 73 | searchResult.value = handlerSearchResult(filterMenu, reg) 74 | activeIndex.value = 0 75 | } 76 | 77 | function handlerSearchResult(filterMenu: any[], reg: RegExp, parent?: any) { 78 | const ret: SearchResult[] = [] 79 | filterMenu.forEach((item) => { 80 | const { name, label, key, path, icon, children, hideMenu, meta } = item 81 | if ( 82 | !hideMenu 83 | && reg.test(label) 84 | && (!children?.length) 85 | ) { 86 | ret.push({ 87 | name: parent?.label ? `${parent.label} > ${label}` : label, 88 | path: key, 89 | icon, 90 | }) 91 | } 92 | if ( 93 | Array.isArray(children) 94 | && children.length 95 | ) 96 | ret.push(...handlerSearchResult(children, reg, item)) 97 | }) 98 | return ret 99 | } 100 | 101 | // Activate when the mouse moves to a certain line 102 | function handleMouseenter(e: any) { 103 | const index = e.target.dataset.index 104 | activeIndex.value = Number(index) 105 | } 106 | 107 | // Arrow key up 108 | function handleUp() { 109 | if (!searchResult.value.length) 110 | return 111 | activeIndex.value-- 112 | if (activeIndex.value < 0) 113 | activeIndex.value = searchResult.value.length - 1 114 | 115 | handleScroll() 116 | } 117 | 118 | // Arrow key down 119 | function handleDown() { 120 | if (!searchResult.value.length) 121 | return 122 | activeIndex.value++ 123 | if (activeIndex.value > searchResult.value.length - 1) 124 | activeIndex.value = 0 125 | } 126 | 127 | // enter keyboard event 128 | async function handleEnter() { 129 | if (!searchResult.value.length) 130 | return 131 | 132 | const result = unref(searchResult) 133 | const index = unref(activeIndex) 134 | if (result.length === 0 || index < 0) 135 | return 136 | 137 | const to = result[index] 138 | handleClose() 139 | await nextTick() 140 | router.push(to.path) 141 | } 142 | 143 | // close search modal 144 | function handleClose() { 145 | searchResult.value = [] 146 | emit('close') 147 | } 148 | 149 | // enter search 150 | onKeyStroke('Enter', handleEnter) 151 | // Monitor keyboard arrow keys 152 | onKeyStroke('ArrowUp', handleUp) 153 | onKeyStroke('ArrowDown', handleDown) 154 | // esc close 155 | onKeyStroke('Escape', handleClose) 156 | 157 | return { 158 | handleSearch, 159 | searchResult, 160 | keyword, 161 | activeIndex, 162 | handleMouseenter, 163 | handleEnter, 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /composables/web/useScrollTo.ts: -------------------------------------------------------------------------------- 1 | import { isUndefined } from 'lodash-es' 2 | 3 | export interface ScrollToParams { 4 | el: any 5 | to: number 6 | duration?: number 7 | callback?: () => any 8 | } 9 | 10 | function easeInOutQuad(t: number, b: number, c: number, d: number) { 11 | t /= d / 2 12 | if (t < 1) 13 | return (c / 2) * t * t + b 14 | 15 | t-- 16 | return (-c / 2) * (t * (t - 2) - 1) + b 17 | } 18 | function move(el: HTMLElement, amount: number) { 19 | el.scrollTop = amount 20 | } 21 | 22 | function position(el: HTMLElement) { 23 | return el.scrollTop 24 | } 25 | 26 | export function useScrollTo({ 27 | el, 28 | to, 29 | duration = 500, 30 | callback, 31 | }: ScrollToParams) { 32 | const isActiveRef = ref(false) 33 | const start = position(el) 34 | const change = to - start 35 | const increment = 20 36 | let currentTime = 0 37 | duration = isUndefined(duration) ? 500 : duration 38 | 39 | const animateScroll = function () { 40 | if (!unref(isActiveRef)) 41 | return 42 | 43 | currentTime += increment 44 | const val = easeInOutQuad(currentTime, start, change, duration) 45 | move(el, val) 46 | if (currentTime < duration && unref(isActiveRef)) { 47 | requestAnimationFrame(animateScroll) 48 | } 49 | else { 50 | if (callback && isFunction(callback)) 51 | callback() 52 | } 53 | } 54 | const run = () => { 55 | isActiveRef.value = true 56 | animateScroll() 57 | } 58 | 59 | const stop = () => { 60 | isActiveRef.value = false 61 | } 62 | 63 | return { start: run, stop } 64 | } 65 | -------------------------------------------------------------------------------- /composables/web/useTabs.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalized, Router } from 'vue-router' 2 | 3 | export function useTabs(_router?: Router) { 4 | const { headerSetting } = useHeaderSetting() 5 | 6 | function canIUseTabs(): boolean { 7 | const { showTabs } = headerSetting.value 8 | if (!showTabs) 9 | throw new Error('The multi-tab page is currently not open, please open it in the settings!') 10 | 11 | return !!showTabs 12 | } 13 | 14 | const tabStore = useTabStore() 15 | const router = _router || useRouter() 16 | 17 | const { currentRoute } = router 18 | 19 | function getCurrentTab() { 20 | const route = unref(currentRoute) 21 | return tabStore.tabList.find(item => item.fullPath === route.fullPath)! 22 | } 23 | 24 | async function updateTabTitle(title: string, tab?: RouteLocationNormalized) { 25 | const canIUse = canIUseTabs 26 | if (!canIUse) 27 | return 28 | 29 | const targetTab = tab || getCurrentTab() 30 | await tabStore.setTabTitle(title, targetTab) 31 | } 32 | 33 | async function updateTabPath(path: string, tab?: RouteLocationNormalized) { 34 | const canIUse = canIUseTabs 35 | if (!canIUse) 36 | return 37 | 38 | const targetTab = tab || getCurrentTab() 39 | await tabStore.updateTabPath(path, targetTab) 40 | } 41 | 42 | return { 43 | setTitle: (title: string, tab?: RouteLocationNormalized) => updateTabTitle(title, tab), 44 | updatePath: (fullPath: string, tab?: RouteLocationNormalized) => updateTabPath(fullPath, tab), 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /config/i18n.ts: -------------------------------------------------------------------------------- 1 | import type { NuxtI18nOptions } from '@nuxtjs/i18n' 2 | import type { LocaleObject } from '#i18n' 3 | 4 | const locales: LocaleObject [] = [ 5 | { 6 | code: 'zh-CN', 7 | file: 'zh-CN.json', 8 | name: '简体中文', 9 | }, 10 | { 11 | code: 'en', 12 | file: 'en.json', 13 | name: 'English', 14 | }, 15 | ] 16 | 17 | export const i18n: NuxtI18nOptions = { 18 | locales, 19 | lazy: true, 20 | langDir: 'locales', 21 | strategy: 'no_prefix', 22 | defaultLocale: 'zh-CN', 23 | detectBrowserLanguage: { 24 | useCookie: true, 25 | // fallbackLocale: 'zh-CN', 26 | redirectOn: 'root', 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /constants/menu.ts: -------------------------------------------------------------------------------- 1 | // menu mode 2 | export enum MenuModeEnum { 3 | VERTICAL = 'vertical', 4 | HORIZONTAL = 'horizontal', 5 | } 6 | -------------------------------------------------------------------------------- /constants/setting.ts: -------------------------------------------------------------------------------- 1 | import { MenuModeEnum } from './menu' 2 | import { theme } from '#tailwind-config' 3 | 4 | export interface AppSetting { 5 | themeColor: string 6 | showLogo: boolean 7 | showFooter: boolean 8 | } 9 | 10 | export interface HeaderSetting { 11 | showBreadCrumb: boolean 12 | showBreadCrumbIcon: boolean 13 | showTabs: boolean 14 | } 15 | 16 | export interface MenuSetting { 17 | collapsed: boolean 18 | siderHidden: boolean 19 | menuWidth: number 20 | mode: MenuModeEnum 21 | type: 'light' | 'dark' 22 | } 23 | 24 | export interface ProjectSetting { 25 | appSetting: AppSetting 26 | headerSetting: HeaderSetting 27 | menuSetting: MenuSetting 28 | } 29 | 30 | export const defaultSettings: ProjectSetting = { 31 | appSetting: { 32 | themeColor: theme.colors.emerald[500], 33 | showLogo: true, 34 | showFooter: false, 35 | }, 36 | headerSetting: { 37 | showBreadCrumb: true, 38 | showBreadCrumbIcon: false, 39 | showTabs: true, 40 | }, 41 | menuSetting: { 42 | collapsed: false, 43 | mode: MenuModeEnum.VERTICAL, 44 | menuWidth: 200, 45 | siderHidden: false, 46 | type: 'dark', 47 | }, 48 | } 49 | -------------------------------------------------------------------------------- /constants/site.ts: -------------------------------------------------------------------------------- 1 | export const GITHUB_URL = 'https://github.com/kuizuo/nuxt-naive-admin' 2 | 3 | export const DOC_URL = 'https://nuxt-naive-admin-docs.vercel.app' 4 | 5 | export const SITE_URL = 'https://nuxt-naive-admin.vercel.app' 6 | -------------------------------------------------------------------------------- /content/help.md: -------------------------------------------------------------------------------- 1 | # 帮助说明 2 | 3 | > 使用 github api 获取用户信息 4 | 5 | ### 用法 6 | 7 | 1. 输入你要查询的 Github 账号 8 | 9 | 2. 点击查询按钮 10 | 11 | 3. 等待响应结果,效果如下 12 | 13 | ![preview.png](/img/preview.png) 14 | -------------------------------------------------------------------------------- /docs/.env.example: -------------------------------------------------------------------------------- 1 | GITHUB_TOKEN='' -------------------------------------------------------------------------------- /docs/.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .output 4 | .nuxt -------------------------------------------------------------------------------- /docs/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@nuxt/eslint-config', 4 | rules: { 5 | 'vue/max-attributes-per-line': 'off', 6 | 'vue/multi-word-component-names': 'off' 7 | } 8 | } -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.iml 3 | .idea 4 | *.log* 5 | .nuxt 6 | .vscode 7 | .DS_Store 8 | coverage 9 | dist 10 | sw.* 11 | .env 12 | .output 13 | .vercel 14 | -------------------------------------------------------------------------------- /docs/.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /docs/app.config.ts: -------------------------------------------------------------------------------- 1 | export default defineAppConfig({ 2 | docus: { 3 | title: 'Nuxt Naive Admin', 4 | description: '一站式管理系统,融合 Nuxt、Naive UI 和 Supabase', 5 | image: 'https://nuxt-naive-admin.vercel.app/cover.png', 6 | socials: { 7 | github: 'kuizuo/nuxt-naive-admin', 8 | nuxt: { 9 | label: 'Nuxt', 10 | icon: 'simple-icons:nuxtdotjs', 11 | href: 'https://nuxt.com' 12 | }, 13 | supabase: { 14 | label: 'Supabase', 15 | icon: 'simple-icons:supabase', 16 | href: 'https://supabase.com' 17 | } 18 | }, 19 | aside: { 20 | level: 0, 21 | exclude: [] 22 | }, 23 | header: { 24 | logo: true, 25 | exclude: [], 26 | }, 27 | github: { 28 | owner: "kuizuo", 29 | repo: "nuxt-naive-admin", 30 | branch: "main", 31 | dir: "docs/content", 32 | edit: true, 33 | }, 34 | } 35 | }) 36 | -------------------------------------------------------------------------------- /docs/components/Logo.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/components/content/Releases.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /docs/content/0.index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | navigation: false 4 | layout: page 5 | main: 6 | fluid: false 7 | --- 8 | 9 | :ellipsis{right=0px width=75% blur=150px} 10 | 11 | ::block-hero 12 | --- 13 | cta: 14 | - 快速开始 15 | - /introduction/getting-started 16 | secondary: 17 | - 在线预览 → 18 | - https://nuxt-naive-admin.vercel.app 19 | --- 20 | 21 | #title 22 | 一站式管理系统,融合 Nuxt、Naive UI 和 Supabase。 23 | 24 | #description 25 | 使用 Nuxt 框架, 基于 Naive UI 组件库和 Supabase 服务所开发的管理系统 26 | 27 | #support 28 | ::terminal 29 | --- 30 | content: 31 | - git clone https://github.com/kuizuo/nuxt-naive-admin 32 | - cd nuxt-naive-admin 33 | - pnpm install 34 | - pnpm run dev 35 | --- 36 | :: 37 | :: 38 | 39 | ::card-grid 40 | #title 41 | 特性 42 | 43 | #root 44 | :ellipsis{left=0px width=40rem top=10rem blur=140px} 45 | 46 | #default 47 | ::card{icon=logos:nuxt-icon} 48 | #title 49 | Nuxt 50 | #description 51 | 充分利用 [Nuxt 3](https://v3.nuxtjs.org) 及其[模块](https://modules.nuxtjs.org)生态系统的全部功能。 52 | :: 53 | 54 | ::card{icon=logos:naiveui} 55 | #title 56 | Naive UI 57 | #description 58 | 集成 [Naive UI](https://www.naiveui.com/) 组件库 59 | :: 60 | 61 | ::card{icon="logos:tailwindcss-icon"} 62 | #title 63 | Tailwindcss 64 | #description 65 | 实用为先的 CSS 框架,可轻松构建现代且响应式的用户界面。 66 | :: 67 | 68 | ::card{icon="logos:supabase-icon"} 69 | #title 70 | Supabase 71 | #description 72 | 一个开源的后端服务,使用 Postgres 数据库、身份验证、即时 API、边缘函数、实时订阅、存储和向量嵌入。 73 | :: 74 | 75 | ::card{icon="⚗"} 76 | #title 77 | Nitro 78 | #description 79 | 使用 [Nitro](https://nitro.unjs.io/) 能够快速在 Nuxt 项目中编写后端服务。 80 | :: 81 | 82 | ::card{icon=noto:rocket} 83 | #title 84 | 部署在任意地方 85 | #description 86 | 只需要填写 Supabase 项目 API keys, 即可部署在 [Vercel](https://vercel.com) 或 [Netlify](https://netlify.com)。 87 | :: 88 | :: 89 | -------------------------------------------------------------------------------- /docs/content/1.introduction/1.introduction.md: -------------------------------------------------------------------------------- 1 | # 介绍 2 | 3 | 一站式管理系统,融合 Nuxt、Naive UI 和 Supabase。 4 | 5 | 这是一套使用 Nuxt 框架, 基于 Naive UI 组件库, 以及 Supabase 服务所开发的管理系统(模版),帮助你快速开发全栈 Nuxt 应用 6 | 7 | 演示地址: [nuxt-naive-admin.vercel.app](https://nuxt-naive-admin.vercel.app) 8 | 9 | 管理员账号:admin@kuizuo.cn 密码:Aa123456 10 | 11 | ## 文档 12 | 13 | 本文档采用 [Docus](https://docus.dev/) 开发。 14 | 15 | ### 基础知识 16 | 17 | 本项目需要一定基础知识,,以便能处理一些常见的问题。建议在开发前先学一下以下内容,提前了解和学习这些知识,会对项目理解非常有帮助: 18 | 19 | - [Vue3](https://cn.vuejs.org/) 20 | - [Nuxt](https://nuxt.com) 21 | - [TypeScript](https:/typescriptlang.org/) 22 | - [Naive UI](https://naiveui.com/) 23 | - [Supabase](https://supabase.io/) 24 | 25 | ### 项目所用到的模块 26 | 27 | - [@nuxt/content](https://nuxt.com/modules/content) 28 | - [@nuxt/devtools](https://nuxt.com/modules/devtools) 29 | - [@vueuse/nuxt](https://nuxt.com/modules/vueuse) 30 | - [@nuxtjs/color-mode](https://nuxt.com/modules/color-mode) 31 | - [@nuxtjs/i18n](https://nuxt.com/modules/i18n) 32 | - [@nuxtjs/tailwindcss](https://nuxt.com/modules/tailwindcss) 33 | - [@nuxtjs/supabase](https://nuxt.com/modules/supabase) 34 | - [@pinia/nuxt](https://nuxt.com/modules/pinia) 35 | - [@pinia-plugin-persistedstate/nuxt](https://nuxt.com/modules/pinia-plugin-persistedstate) 36 | - [@huntersofbook/naive-ui-nuxt](https://github.com/huntersofbook/naive-ui) 37 | - [nuxt-icon](https://nuxt.com/modules/icon) 38 | 39 | ## 项目的演进和技术选择 40 | 41 | 起初这个项目是我用于做 Node.js 来请求 api 接口或者爬虫,并且部署在 Web 上,所编写的一个模版,名 Protocol(即用于协议复现的)。 42 | 关于该模版,我曾写过篇 [blog](https://kuizuo.cn/blog/protocol-template) 介绍我为何开发它。 43 | 44 | 有次使用该模块的时候需要管理用户数据,与其搭建一个 [vben + nest 的管理网站](https://github.com/kuizuo/nest-vben-admin),不如直接在 nuxt 的 page 下创建 admin 目录,将管理端的页面都写在该目录下,故这个项目便改名为 [nuxt-naive-admin](https://github.com/kuizuo/nuxt-naive-admin)。 45 | 46 | 于是我开始使用 nitro 来实现后端服务,如身份效验,数据库查询等等。期间用到了 [nuxt-auth](https://nuxt.com/modules/nuxt-auth) 与 [prisma](https://www.prisma.io/),相信使用过全栈框架肯定不陌生。 47 | 48 | 但后来我思考到,既然要自己实现后端服务,为何不考虑使用更全面的 [nest.js](https://nestjs.com/) 框架。可这样违背了我一开始所想要编写这个项目的初心,即我只希望**所有的代码都在全栈框架体系下**。这不同于 monorepo,就仅仅只是一个 nuxt 或 next 仓库。 49 | 50 | 加之在开发期间受 serverless 影响较重,与其自己在 server 目录下实现后端服务接口,不如直接使用一些第三方平台,综合考量下选用 supabase。 51 | 52 | 虽然本项目使用 supabase cloud,但 supabase 支持[私有化部署](https://supabase.com/docs/guides/self-hosting)(会阉割许多功能)。后续可能会考虑私有化部署,这样你就能通过 [supabase.kuizuo.cn](http://supabase.kuizuo.cn/) 来访问 supabase 后台。 53 | 54 | 后续或许会根据实际业务将 supabase 脱离出来单独为其编写后端服务,但其核心宗旨都不同于前后端分离那一套,即**所有的代码都在全栈框架体系下**。 -------------------------------------------------------------------------------- /docs/content/1.introduction/2.getting-started.md: -------------------------------------------------------------------------------- 1 | # 开始 2 | 3 | 由于该项目并不是一个纯前端项目, 未为其编写 mock 数据,因此需要你到 Supabase 注册一个账号,并创建一个项目,然后将项目的配置信息填写到项目中。 4 | 5 | 下文便会告诉你该如何进行配置 6 | 7 | 1. 下载项目: 8 | 9 | ```bash 10 | git clone https://github.com/kuizuo/nuxt-naive-admin 11 | cd nuxt-naive-admin 12 | ``` 13 | 14 | 2. 安装相关依赖: 15 | 16 | ::code-group 17 | 18 | ```bash [npm] 19 | npm install 20 | ``` 21 | 22 | ```bash [yarn] 23 | yarn install 24 | ``` 25 | 26 | ```bash [pnpm] 27 | pnpm install --shamefully-hoist 28 | ``` 29 | 30 | :: 31 | 32 | 3. 配置 Supabase: 33 | 34 | 首先你需要到 [Supabase](https://supabase.com/) 注册一个账号,并创建一个项目,然后将项目的配置信息填写到项目中 `supabase/config.toml` 文件。 35 | 36 | 执行以下命令来启动本地 Supabase docker 实例 37 | 38 | 39 | ```bash 40 | npx supabase start 41 | ``` 42 | 43 | 将 `.env.example` 文件复制并重命名为 `.env`。现在将你在运行 `supabase start` 时得到的凭据复制到此文件中。 44 | 45 | 4. 运行 `dev` 命令来启动项目: 46 | 47 | ::code-group 48 | 49 | ```bash [npm] 50 | npm run dev 51 | ``` 52 | 53 | ```bash [yarn] 54 | yarn dev 55 | ``` 56 | 57 | ```bash [pnpm] 58 | pnpm run dev 59 | ``` 60 | 61 | :: 62 | 63 | ::alert{type="success"} 64 | ✨ 很好! 你已经成功启动了项目,现在你可以在浏览器中访问 来查看效果 65 | :: 66 | 67 | 你可以在 Stackblitz 中在线体验: 68 | 69 | :button-link[Play on StackBlitz]{size="small" icon="IconStackBlitz" href="https://stackblitz.com/github/kuizuo/nuxt-naive-admin" blank} 70 | 71 | ### 目录结构 72 | 73 | ```bash 74 | nuxt-naive-admin 75 | ├── assets # 前端静态资源文件 76 | ├── components # 组件 77 | ├── composables # 组合式API 78 | ├── docs # docus 文档 79 | ├── layouts # 布局 80 | │ ├── admin.vue # 管理端布局 81 | │ └── default.vue # 默认布局 82 | ├── middleware # 中间件 83 | ├── pages # 页面 84 | │ ├── admin # 管理端页面 85 | │ ├── auth # 身份验证页面 86 | │ ├── index.vue # 客户端页面 87 | ├── public # 服务端静态资源文件 88 | ├── server # 服务端文件 89 | │ ├── api # 接口服务 90 | │ └── middleware # 接口中间件 91 | ├── stores # pinia 状态管理 92 | ├── types # 类型定义 93 | ├── utils # 工具函数 94 | ├── ecosystem.config.js # pm2 配置文件 95 | ├── app.vue # 入口文件 96 | ├── app.config.ts # 应用配置文件 97 | ├── nuxt.config.ts # nuxt 配置文件 98 | |-- .env # 环境变量 99 | └── package.json # 依赖包 100 | ``` 101 | 102 | 从这个项目的目录结构中其实就可以看出,本项目是集成了全栈能力,并且使用 Vue 与 Node 来编写前端与后端,并不会产生前后端分离的分割感,只需要打开一个项目即可开始工作。这得益于Nuxt3 与 Nitro。 103 | 104 | ### 项目开发说明 105 | 106 | 本项目相当于客户端页面(page/index)与管理端页面(page/admin)相结合。 107 | -------------------------------------------------------------------------------- /docs/content/1.introduction/_dir.yml: -------------------------------------------------------------------------------- 1 | icon: ph:star-duotone 2 | title: 指南 3 | navigation.redirect: /introduction/getting-started 4 | -------------------------------------------------------------------------------- /docs/content/2.usage/1.configuration.md: -------------------------------------------------------------------------------- 1 | # 配置 2 | 3 | TODO... 4 | 5 | ### 环境变量 6 | 7 | 8 | 9 | ### 应用配置 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/content/2.usage/_dir.yml: -------------------------------------------------------------------------------- 1 | icon: heroicons:book-open 2 | title: 使用 3 | navigation.redirect: /usage/configuration 4 | -------------------------------------------------------------------------------- /docs/content/5.changelog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 更新日志 3 | icon: heroicons:bookmark 4 | --- 5 | 6 | # 更新日志 7 | 8 | :releases -------------------------------------------------------------------------------- /docs/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtConfig({ 2 | // https://github.com/nuxt-themes/docus 3 | extends: '@nuxt-themes/docus', 4 | 5 | modules: [ 6 | // https://github.com/nuxt-modules/plausible 7 | '@nuxtjs/plausible', 8 | // https://github.com/nuxt/devtools 9 | '@nuxt/devtools', 10 | '@nuxtlabs/github-module' 11 | ], 12 | github: { 13 | repo: 'kuizuo/nuxt-naive-admin' 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docus", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxi dev", 7 | "build": "nuxi build", 8 | "generate": "nuxi generate", 9 | "preview": "nuxi preview", 10 | "lint": "eslint ." 11 | }, 12 | "devDependencies": { 13 | "@nuxt-themes/docus": "^1.13.1", 14 | "@nuxt/devtools": "^0.6.7", 15 | "@nuxt/eslint-config": "^0.1.1", 16 | "@nuxtjs/plausible": "^0.2.1", 17 | "@nuxtlabs/github-module": "^1.6.3", 18 | "@types/node": "^20.4.0", 19 | "eslint": "^8.44.0", 20 | "nuxt": "^3.6.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/public/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuizuo/nuxt-naive-admin/47611c3a84db8c7fe5c6d11f6cc29c9a821b3ca7/docs/public/cover.png -------------------------------------------------------------------------------- /docs/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@nuxtjs" 4 | ], 5 | "lockFileMaintenance": { 6 | "enabled": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docs/tokens.config.ts: -------------------------------------------------------------------------------- 1 | import { defineTheme } from 'pinceau' 2 | 3 | export default defineTheme({ 4 | }) 5 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /ecosystem.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | apps: [ 3 | { 4 | name: 'nuxt-naive-admin', 5 | script: './.output/server/index.mjs', 6 | exec_mode: 'cluster', 7 | instances: '1', 8 | instance_var: 'INSTANCE_ID', 9 | env: { 10 | PORT: 8010, 11 | HOST: 'localhost', 12 | NODE_ENV: 'production', 13 | }, 14 | }, 15 | ], 16 | } 17 | -------------------------------------------------------------------------------- /error.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | -------------------------------------------------------------------------------- /layouts/admin.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 64 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": { 3 | "sign_in": "Sign In" 4 | }, 5 | "common": { 6 | "not_found": "404 Not Found" 7 | }, 8 | "layout": { 9 | "header": { 10 | "document": "Document", 11 | "home": "Home", 12 | "logout": "Log out", 13 | "profile": "\bProfile", 14 | "search": "Search", 15 | "tooltip_entry_full": "Full screen", 16 | "tooltip_exit_full": "exit full screen", 17 | "tooltip_notify": "Message notification" 18 | }, 19 | "setting": { 20 | "breadcrumb": "Breadcrumb", 21 | "breadcrumb_icon": "Breadcrumb Icon", 22 | "dark_mode": "DarkMode", 23 | "drawer_title": "Project Setting", 24 | "interface_display": "interface display", 25 | "interface_function": "interface function", 26 | "menu_accordion": "Accordion", 27 | "menu_collapse": "Menu collapse", 28 | "menu_search": "Menu Search", 29 | "menu_type": "Menu type", 30 | "menu_type_dark": "Menu dark type", 31 | "menu_type_light": "Menu light type", 32 | "menu_width": "Menu width", 33 | "sys_theme": "System theme", 34 | "tabs": "Tabs" 35 | }, 36 | "tabs": { 37 | "close": "Close current", 38 | "close_all": "Close All", 39 | "close_other": "Close Other", 40 | "close_right": "Close Right", 41 | "reload": "Reload" 42 | } 43 | }, 44 | "pages": { 45 | "about": { 46 | "title": "About" 47 | }, 48 | "comp": { 49 | "form": "Form", 50 | "page": "Page", 51 | "table": "Table", 52 | "title": "Comp" 53 | }, 54 | "dashboard": { 55 | "title": "Dashboard" 56 | }, 57 | "external": { 58 | "naive_ui": "Naive UI", 59 | "nuxt": "Nuxt", 60 | "supabase": "Supabase", 61 | "title": "External" 62 | }, 63 | "level_menu": { 64 | "level1": "level-1", 65 | "level2": "level-2", 66 | "title": "level-menu" 67 | }, 68 | "system": { 69 | "title": "System", 70 | "user": { 71 | "title": "User" 72 | } 73 | }, 74 | "workplace": { 75 | "title": "WorkPlace" 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /locales/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "action": { 3 | "sign_in": "登录" 4 | }, 5 | "common": { 6 | "not_found": "无法找到相关内容" 7 | }, 8 | "layout": { 9 | "header": { 10 | "document": "文档", 11 | "home": "首页", 12 | "logout": "退出登录", 13 | "profile": "用户资料", 14 | "search": "搜索", 15 | "tooltip_entry_full": "全屏", 16 | "tooltip_exit_full": "退出全屏", 17 | "tooltip_notify": "消息通知" 18 | }, 19 | "setting": { 20 | "breadcrumb": "面包屑", 21 | "breadcrumb_icon": "面包屑图标", 22 | "dark_mode": "主题", 23 | "drawer_title": "项目配置", 24 | "interface_display": "界面显示", 25 | "interface_function": "界面功能", 26 | "menu_accordion": "侧边菜单手风琴模式", 27 | "menu_collapse": "折叠菜单", 28 | "menu_search": "菜单搜索", 29 | "menu_type": "导航栏风格", 30 | "menu_type_dark": "暗色侧边栏", 31 | "menu_type_light": "亮色侧边栏", 32 | "menu_width": "菜单展开宽度", 33 | "sys_theme": "系统主题", 34 | "tabs": "标签页" 35 | }, 36 | "tabs": { 37 | "close": "关闭标签页", 38 | "close_all": "关闭全部标签页", 39 | "close_other": "关闭其它标签页", 40 | "close_right": "关闭右侧标签页", 41 | "reload": "重新加载" 42 | } 43 | }, 44 | "pages": { 45 | "about": { 46 | "title": "关于" 47 | }, 48 | "comp": { 49 | "form": "表单", 50 | "page": "页面", 51 | "table": "表格", 52 | "title": "组件" 53 | }, 54 | "dashboard": { 55 | "title": "仪表盘" 56 | }, 57 | "external": { 58 | "naive_ui": "Naive UI", 59 | "nuxt": "Nuxt", 60 | "supabase": "Supabase", 61 | "title": "外链" 62 | }, 63 | "level_menu": { 64 | "level1": "一级菜单1", 65 | "level2": "二级菜单", 66 | "title": "多级菜单" 67 | }, 68 | "system": { 69 | "title": "系统管理", 70 | "user": { 71 | "title": "用户管理" 72 | } 73 | }, 74 | "workplace": { 75 | "title": "工作台" 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /middleware/admin.ts: -------------------------------------------------------------------------------- 1 | export default defineNuxtRouteMiddleware((to, from) => { 2 | const user = useSupabaseUser() 3 | const { public: { adminUid } } = useRuntimeConfig() 4 | 5 | if (user.value.id !== adminUid) 6 | return navigateTo('/') 7 | }) 8 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | NPM_FLAGS = "--version" 3 | NODE_VERSION = "18" 4 | 5 | [build] 6 | publish = "dist" 7 | command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run build" 8 | 9 | [[redirects]] 10 | from = "/*" 11 | to = "/index.html" 12 | status = 200 13 | -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from './config/i18n' 2 | 3 | export default defineNuxtConfig({ 4 | app: { 5 | keepalive: true, 6 | pageTransition: { name: 'page', mode: 'out-in' }, 7 | layoutTransition: { name: 'layout', mode: 'out-in' }, 8 | }, 9 | runtimeConfig: { 10 | public: { 11 | baseUrl: process.env.BASE_URL || 'http://localhost:8010', 12 | adminUid: process.env.NUXT_PUBLIC_ADMIN_UID, 13 | }, 14 | }, 15 | modules: [ 16 | '@vueuse/nuxt', 17 | '@nuxtjs/color-mode', 18 | '@nuxtjs/tailwindcss', 19 | '@nuxtjs/i18n', 20 | ['@pinia/nuxt', { 21 | autoImports: ['defineStore', 'definePiniaStore'], 22 | }], 23 | '@pinia-plugin-persistedstate/nuxt', 24 | ['@nuxtjs/supabase', { 25 | autoImports: ['serverSupabaseClient'], 26 | }], 27 | '@huntersofbook/naive-ui-nuxt', 28 | 'nuxt-icon', 29 | '@nuxt/devtools', 30 | ], 31 | imports: { 32 | dirs: [ 33 | 'composables/**/*', 34 | 'stores/**/*', 35 | ], 36 | }, 37 | components: [ 38 | { 39 | path: '~/components/global', 40 | global: true, 41 | }, 42 | { 43 | path: '~/components', 44 | extensions: ['vue'], 45 | }, 46 | ], 47 | colorMode: { 48 | classSuffix: '', 49 | }, 50 | tailwindcss: { 51 | viewer: false, 52 | exposeConfig: true, 53 | config: { 54 | content: [ 55 | 'content/**/**.md', 56 | ], 57 | }, 58 | }, 59 | supabase: { 60 | redirectOptions: { 61 | login: '/auth/login', 62 | callback: '/auth/confirm', 63 | exclude: ['/', '/auth/reset-password', '/auth/update-password'], 64 | }, 65 | }, 66 | i18n, 67 | css: ['~/assets/css/main.css'], 68 | routeRules: { 69 | '/': { prerender: true }, 70 | '/admin/**': { ssr: false }, 71 | '/api/**': { cors: true }, 72 | '/admin': { redirect: '/admin/dashboard' }, 73 | }, 74 | experimental: { 75 | componentIslands: true, 76 | }, 77 | build: { 78 | transpile: [/echarts/, /resize-detector/], 79 | }, 80 | devtools: { 81 | enabled: true, 82 | }, 83 | }) 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-naive-admin", 3 | "version": "0.4.7", 4 | "private": true, 5 | "packageManager": "pnpm@8.7.0", 6 | "description": "一站式管理系统,融合 Nuxt、Naive UI 和 Supabase", 7 | "author": { 8 | "url": "https://kuizuo.cn", 9 | "email": "hi@kuizuo.cn", 10 | "name": "Kuizuo" 11 | }, 12 | "license": "MIT", 13 | "repository": { 14 | "url": "https://github.com/kuizuo/nuxt-naive-admin", 15 | "type": "git" 16 | }, 17 | "keywords": [ 18 | "admin", 19 | "template", 20 | "web", 21 | "nuxt", 22 | "naive-ui", 23 | "supabase" 24 | ], 25 | "scripts": { 26 | "build": "nuxi build", 27 | "dev": "nuxi dev -o", 28 | "start": "node -r dotenv/config .output/server/index.mjs dotenv_config_path=.env.production", 29 | "start:pm2": "pm2 start ecosystem.config.js --env production", 30 | "typecheck": "nuxi typecheck --noEmit", 31 | "lint": "eslint .", 32 | "postinstall": "nuxi prepare", 33 | "generate": "nuxi generate", 34 | "release": "bumpp --commit --tag --push", 35 | "genTypes": "supabase gen types typescript --project-id '$PROJECT_REF' --schema public > types/database.types.ts" 36 | }, 37 | "dependencies": { 38 | "echarts": "^5.4.3", 39 | "vue-echarts": "^6.6.1" 40 | }, 41 | "devDependencies": { 42 | "@antfu/eslint-config": "^0.40.0", 43 | "@egoist/tailwindcss-icons": "^1.1.0", 44 | "@huntersofbook/naive-ui-nuxt": "^0.7.1", 45 | "@iconify/json": "^2.2.23", 46 | "@nuxt/devtools": "^0.4.6", 47 | "@nuxtjs/color-mode": "^3.3.0", 48 | "@nuxtjs/i18n": "8.0.0-rc.4", 49 | "@nuxtjs/supabase": "^1.0.2", 50 | "@nuxtjs/tailwindcss": "^6.8.0", 51 | "@pinia-plugin-persistedstate/nuxt": "^1.1.1", 52 | "@pinia/nuxt": "^0.4.11", 53 | "@sidebase/nuxt-auth": "^0.4.4", 54 | "@tailwindcss/typography": "^0.5.9", 55 | "@types/sortablejs": "^1.15.1", 56 | "@vueuse/components": "^10.4.1", 57 | "@vueuse/nuxt": "^10.3.0", 58 | "bumpp": "^8.2.1", 59 | "dayjs": "^1.11.9", 60 | "eslint": "^8.46.0", 61 | "got": "^12.6.0", 62 | "lodash-es": "^4.17.21", 63 | "naive-ui": "^2.34.4", 64 | "nuxt": "^3.6.0", 65 | "nuxt-icon": "^0.3.3", 66 | "sass": "^1.64.2", 67 | "sortablejs": "^1.15.0", 68 | "typescript": "^4.9.5", 69 | "vitest": "^0.24.5" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /pages/404.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | -------------------------------------------------------------------------------- /pages/account/profile.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | -------------------------------------------------------------------------------- /pages/admin/about.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 92 | 93 | 98 | -------------------------------------------------------------------------------- /pages/admin/comp.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /pages/admin/comp/form/index.vue: -------------------------------------------------------------------------------- 1 | 202 | 203 | 212 | -------------------------------------------------------------------------------- /pages/admin/comp/page/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 25 | -------------------------------------------------------------------------------- /pages/admin/comp/table/index.vue: -------------------------------------------------------------------------------- 1 | 182 | 183 | 200 | -------------------------------------------------------------------------------- /pages/admin/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 40 | -------------------------------------------------------------------------------- /pages/admin/external.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /pages/admin/external/naive-ui.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /pages/admin/external/nuxt.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /pages/admin/external/supabase.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /pages/admin/level.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | -------------------------------------------------------------------------------- /pages/admin/level/menu1.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 15 | 2 16 | -------------------------------------------------------------------------------- /pages/admin/level/menu1/menu1-1.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 25 | -------------------------------------------------------------------------------- /pages/admin/level/menu2.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /pages/admin/system.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | -------------------------------------------------------------------------------- /pages/admin/workplace/index.vue: -------------------------------------------------------------------------------- 1 | 161 | 162 | 182 | -------------------------------------------------------------------------------- /pages/auth/confirm.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /pages/auth/login.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /pages/auth/reset-password.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 79 | -------------------------------------------------------------------------------- /pages/auth/update-password.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 58 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 131 | -------------------------------------------------------------------------------- /pages/redirect/[...path].vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 24 | -------------------------------------------------------------------------------- /plugins/chart.ts: -------------------------------------------------------------------------------- 1 | import { use } from 'echarts/core' 2 | 3 | // import ECharts modules manually to reduce bundle size 4 | import { CanvasRenderer } from 'echarts/renderers' 5 | import { BarChart, LineChart, PieChart, RadarChart } from 'echarts/charts' 6 | import { GridComponent, LegendComponent, RadarComponent, TooltipComponent } from 'echarts/components' 7 | 8 | export default defineNuxtPlugin(() => { 9 | use([ 10 | CanvasRenderer, 11 | LineChart, PieChart, BarChart, RadarChart, 12 | LegendComponent, GridComponent, TooltipComponent, RadarComponent, 13 | ]) 14 | }) 15 | -------------------------------------------------------------------------------- /public/img/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuizuo/nuxt-naive-admin/47611c3a84db8c7fe5c6d11f6cc29c9a821b3ca7/public/img/preview.png -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuizuo/nuxt-naive-admin/47611c3a84db8c7fe5c6d11f6cc29c9a821b3ca7/public/og.png -------------------------------------------------------------------------------- /server/api/admin/dashboard/console.ts: -------------------------------------------------------------------------------- 1 | import type { TagProps } from 'naive-ui' 2 | 3 | const { public: { adminUid } } = useRuntimeConfig() 4 | 5 | export interface GrowCardItem { 6 | title: string 7 | value: number 8 | total: number 9 | action: string 10 | type?: TagProps['type'] 11 | } 12 | 13 | export default defineEventHandler(async (event) => { 14 | if (event.context._user?.id !== adminUid) 15 | throw createError({ statusMessage: '无权限' }) 16 | 17 | const data: GrowCardItem[] = [{ 18 | title: '访问数', 19 | value: Math.floor(Math.random() * 10000), 20 | total: 10000, 21 | type: 'info', 22 | action: '日', 23 | }, 24 | { 25 | title: '用户量', 26 | value: 1234, 27 | total: 10000, 28 | type: 'info', 29 | action: '日', 30 | }, 31 | { 32 | title: '下载数', 33 | value: Math.floor(Math.random() * 1000), 34 | total: 1000, 35 | type: 'warning', 36 | action: '周', 37 | }, 38 | { 39 | title: '成交数', 40 | value: Math.floor(Math.random() * 5000), 41 | total: 5000, 42 | type: 'success', 43 | action: '月', 44 | }] 45 | 46 | return data 47 | }) 48 | -------------------------------------------------------------------------------- /server/api/admin/system/users/[id].delete.ts: -------------------------------------------------------------------------------- 1 | import { serverSupabaseServiceRole } from '#supabase/server' 2 | import type { Database } from '~~/types/database.types' 3 | 4 | const { public: { adminUid } } = useRuntimeConfig() 5 | 6 | export default defineEventHandler(async (event) => { 7 | if (event.context._user?.id !== adminUid) 8 | throw createError({ statusMessage: '无权限' }) 9 | 10 | const id = getRouterParam(event, 'id') 11 | 12 | if (!id) 13 | throw createError({ statusMessage: '无效的用户 ID' }) 14 | 15 | const { auth } = await serverSupabaseServiceRole(event) 16 | 17 | const { data, error } = await auth.admin.deleteUser(id) 18 | 19 | if (error) 20 | throw createError({ statusMessage: error.message }) 21 | }) 22 | -------------------------------------------------------------------------------- /server/api/admin/system/users/[id].put.ts: -------------------------------------------------------------------------------- 1 | import { serverSupabaseServiceRole } from '#supabase/server' 2 | import type { Database } from '~~/types/database.types' 3 | 4 | interface Body { 5 | email: string 6 | password?: string 7 | user_metadata: Record 8 | } 9 | 10 | const { public: { adminUid } } = useRuntimeConfig() 11 | 12 | export default defineEventHandler(async (event) => { 13 | if (event.context._user?.id !== adminUid) 14 | throw createError({ statusMessage: '无权限' }) 15 | 16 | const id = getRouterParam(event, 'id') 17 | if (!id) 18 | throw createError({ statusMessage: '无效的用户 ID' }) 19 | 20 | const { email, password, user_metadata } = await readBody(event) 21 | 22 | const { auth } = await serverSupabaseServiceRole(event) 23 | 24 | const { data, error } = await auth.admin.updateUserById(id, 25 | { 26 | email, 27 | ...(password && { password }), 28 | ...(user_metadata && { user_metadata }), 29 | }) 30 | 31 | if (error) 32 | throw createError({ statusMessage: error.message }) 33 | 34 | return data 35 | }) 36 | -------------------------------------------------------------------------------- /server/api/admin/system/users/index.get.ts: -------------------------------------------------------------------------------- 1 | import { serverSupabaseServiceRole } from '#supabase/server' 2 | import type { Database } from '~~/types/database.types' 3 | 4 | const { public: { adminUid } } = useRuntimeConfig() 5 | 6 | export default defineEventHandler(async (event) => { 7 | if (event.context._user?.id !== adminUid) 8 | throw createError({ statusMessage: '无权限' }) 9 | 10 | const { page, pageSize } = getQuery(event) as { page: number; pageSize: number } 11 | 12 | const { auth } = await serverSupabaseServiceRole(event) 13 | 14 | const { data, error } = await auth.admin.listUsers({ page, perPage: pageSize }) 15 | 16 | if (error) 17 | throw createError({ statusMessage: error.message }) 18 | 19 | return data 20 | }) 21 | -------------------------------------------------------------------------------- /server/api/admin/system/users/index.post.ts: -------------------------------------------------------------------------------- 1 | import { serverSupabaseServiceRole } from '#supabase/server' 2 | import type { Database } from '~~/types/database.types' 3 | 4 | interface Body { 5 | email: string 6 | password: string 7 | user_metadata: Record 8 | email_confirm: boolean 9 | } 10 | 11 | const { public: { adminUid } } = useRuntimeConfig() 12 | 13 | export default defineEventHandler(async (event) => { 14 | if (event.context._user?.id !== adminUid) 15 | throw createError({ statusMessage: '无权限' }) 16 | 17 | const { email, password, user_metadata, email_confirm } = await readBody(event) 18 | 19 | const { auth } = await serverSupabaseServiceRole(event) 20 | 21 | const { data, error } = await auth.admin.createUser({ 22 | email, 23 | password, 24 | user_metadata, 25 | email_confirm, 26 | }) 27 | 28 | if (error) 29 | throw createError({ statusMessage: error.message }) 30 | 31 | return data 32 | }) 33 | -------------------------------------------------------------------------------- /server/api/github/user/[username].get.ts: -------------------------------------------------------------------------------- 1 | import { serverSupabaseServiceRole } from '#supabase/server' 2 | import type { Database } from '~~/types/database.types' 3 | import { Github } from '~~/server/protocol/github' 4 | 5 | export default defineEventHandler(async (event) => { 6 | const client = await serverSupabaseServiceRole(event) 7 | 8 | const { username } = event.context.params as { username: string } 9 | 10 | const user = await Github.getUser(username) 11 | 12 | if (!user.login) 13 | throw createError({ statusCode: 404, message: user.message ?? 'User not found' }) 14 | 15 | const repos = await Github.getRepos(username) 16 | 17 | const data = { ...user, repos } 18 | 19 | await client.from('github_user').upsert({ 20 | id: user.id, 21 | name: username, 22 | avatar_url: user.avatar_url, 23 | bio: user.bio, 24 | followers: user.followers, 25 | following: user.following, 26 | }) 27 | 28 | return ResOp.success(data) 29 | }) 30 | -------------------------------------------------------------------------------- /server/error.ts: -------------------------------------------------------------------------------- 1 | import type { NitroErrorHandler } from 'nitropack' 2 | 3 | export default function (error, event) { 4 | event.res.end(`[custom error handler] ${error.stack}`) 5 | } 6 | -------------------------------------------------------------------------------- /server/middleware/0.auth.ts: -------------------------------------------------------------------------------- 1 | import type { User } from '@supabase/supabase-js' 2 | import { serverSupabaseUser } from '#supabase/server' 3 | 4 | declare module 'h3' { 5 | interface H3EventContext { 6 | _user: User 7 | } 8 | } 9 | 10 | const whitelist: string[] = [] 11 | 12 | export default defineEventHandler(async (event) => { 13 | const { context, node: { req } } = event 14 | 15 | if (!req.url.startsWith('/api/admin')) 16 | return 17 | 18 | try { 19 | const user = await serverSupabaseUser(event) 20 | 21 | if (whitelist.includes(req.url!)) 22 | return 23 | 24 | if (user?.aud !== 'authenticated') 25 | throw createError({ statusMessage: '请登录后再操作', statusCode: 401 }) 26 | 27 | // context._user = user 28 | } 29 | catch (err) { 30 | throw createError({ statusMessage: err.message }) 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /server/protocol/github/index.ts: -------------------------------------------------------------------------------- 1 | import got from 'got' 2 | 3 | const api = got.extend({ 4 | prefixUrl: 'https://api.github.com/', 5 | responseType: 'json', 6 | }) 7 | 8 | export class Github { 9 | static async getUser(username: string) { 10 | return await api.get(`users/${username}`).json() 11 | } 12 | 13 | static async getRepos(username: string) { 14 | return await api.get(`users/${username}/repos`).json() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server/protocol/github/types.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Github { 2 | export interface User { 3 | login: string 4 | id: number 5 | node_id: string 6 | avatar_url: string 7 | gravatar_id: string 8 | url: string 9 | html_url: string 10 | followers_url: string 11 | following_url: string 12 | gists_url: string 13 | starred_url: string 14 | subscriptions_url: string 15 | organizations_url: string 16 | repos_url: string 17 | events_url: string 18 | received_events_url: string 19 | type: string 20 | site_admin: boolean 21 | name: string 22 | company: any 23 | blog: string 24 | location: string 25 | email: any 26 | hireable: any 27 | bio: string 28 | twitter_username: string 29 | public_repos: number 30 | public_gists: number 31 | followers: number 32 | following: number 33 | created_at: string 34 | updated_at: string 35 | repos: Repo[] 36 | 37 | message: string 38 | } 39 | 40 | export interface Repo { 41 | id: number 42 | node_id: string 43 | name: string 44 | full_name: string 45 | private: boolean 46 | owner: Owner 47 | html_url: string 48 | description: string 49 | fork: boolean 50 | url: string 51 | forks_url: string 52 | keys_url: string 53 | collaborators_url: string 54 | teams_url: string 55 | hooks_url: string 56 | issue_events_url: string 57 | events_url: string 58 | assignees_url: string 59 | branches_url: string 60 | tags_url: string 61 | blobs_url: string 62 | git_tags_url: string 63 | git_refs_url: string 64 | trees_url: string 65 | statuses_url: string 66 | languages_url: string 67 | stargazers_url: string 68 | contributors_url: string 69 | subscribers_url: string 70 | subscription_url: string 71 | commits_url: string 72 | git_commits_url: string 73 | comments_url: string 74 | issue_comment_url: string 75 | contents_url: string 76 | compare_url: string 77 | merges_url: string 78 | archive_url: string 79 | downloads_url: string 80 | issues_url: string 81 | pulls_url: string 82 | milestones_url: string 83 | notifications_url: string 84 | labels_url: string 85 | releases_url: string 86 | deployments_url: string 87 | created_at: string 88 | updated_at: string 89 | pushed_at: string 90 | git_url: string 91 | ssh_url: string 92 | clone_url: string 93 | svn_url: string 94 | homepage: string 95 | size: number 96 | stargazers_count: number 97 | watchers_count: number 98 | language: string 99 | has_issues: boolean 100 | has_projects: boolean 101 | has_downloads: boolean 102 | has_wiki: boolean 103 | has_pages: boolean 104 | forks_count: number 105 | mirror_url: any 106 | archived: boolean 107 | disabled: boolean 108 | open_issues_count: number 109 | license: License 110 | allow_forking: boolean 111 | is_template: boolean 112 | web_commit_signoff_required: boolean 113 | topics: string[] 114 | visibility: string 115 | forks: number 116 | open_issues: number 117 | watchers: number 118 | default_branch: string 119 | } 120 | 121 | export interface Owner { 122 | login: string 123 | id: number 124 | node_id: string 125 | avatar_url: string 126 | gravatar_id: string 127 | url: string 128 | html_url: string 129 | followers_url: string 130 | following_url: string 131 | gists_url: string 132 | starred_url: string 133 | subscriptions_url: string 134 | organizations_url: string 135 | repos_url: string 136 | events_url: string 137 | received_events_url: string 138 | type: string 139 | site_admin: boolean 140 | } 141 | 142 | export interface License { 143 | key: string 144 | name: string 145 | spdx_id: string 146 | url: string 147 | node_id: string 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /server/routes/hello.ts: -------------------------------------------------------------------------------- 1 | export default eventHandler(() => { 2 | return { hello: 'world' } 3 | }) 4 | -------------------------------------------------------------------------------- /server/utils/index.ts: -------------------------------------------------------------------------------- 1 | export class ResOp { 2 | readonly data: T 3 | readonly code: number 4 | readonly message: string 5 | readonly error: string | null | undefined 6 | 7 | constructor(code: number, data: T, message = 'success') { 8 | this.code = code 9 | this.data = data 10 | this.message = message 11 | } 12 | 13 | static success(data: T) { 14 | return new ResOp(200, data) 15 | } 16 | 17 | static error(code: number, message: string) { 18 | return new ResOp(code, null, message) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /stores/settings.ts: -------------------------------------------------------------------------------- 1 | import type { ProjectSetting } from '~~/constants/setting' 2 | import { defaultSettings } from '~~/constants/setting' 3 | 4 | export const useSettingsStore = defineStore('app-settings', { 5 | state: (): ProjectSetting => defaultSettings, 6 | actions: { 7 | setSetting(setting: DeepPartial): void { 8 | this.$state = deepMerge(this.$state || {}, setting) 9 | }, 10 | }, 11 | persist: { 12 | key: 'app-settings', 13 | }, 14 | }) 15 | -------------------------------------------------------------------------------- /stores/tab.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalized, RouteRecordNormalized, Router } from 'vue-router' 2 | 3 | // 不需要出现在标签页中的路由 4 | const whiteList = ['Redirect'] 5 | 6 | export interface ITabsState { 7 | tabList: RouteLocationNormalized[] 8 | refreshing: boolean 9 | } 10 | 11 | export function getRawRoute(route: RouteLocationNormalized): RouteLocationNormalized { 12 | if (!route) 13 | return route 14 | const { matched, ...opt } = route 15 | return { 16 | ...opt, 17 | matched: (matched 18 | ? matched.map(item => ({ 19 | meta: item.meta, 20 | name: item.name, 21 | path: item.path, 22 | })) 23 | : undefined) as RouteRecordNormalized[], 24 | } 25 | } 26 | 27 | export const useTabStore = defineStore({ 28 | id: 'app-tab', 29 | state: (): ITabsState => ({ 30 | tabList: [], 31 | refreshing: false, 32 | }), 33 | actions: { 34 | async refreshPage() { 35 | // const router = useRouter() 36 | // router.replace({ 37 | // path: `/redirect${router.currentRoute.value.fullPath}`, 38 | // }) 39 | this.refreshing = true 40 | setTimeout(() => { 41 | this.refreshing = false 42 | }, 500) 43 | }, 44 | setTabList(tabList: RouteLocationNormalized[]) { 45 | this.tabList = tabList 46 | }, 47 | addTab(route: RouteLocationNormalized) { 48 | const rawRoute = getRawRoute(route) 49 | const { name, fullPath, path, meta } = rawRoute 50 | if (whiteList.includes(name as string)) 51 | return 52 | 53 | if (meta?.layout !== 'admin') 54 | return 55 | 56 | const tabHasExits = this.tabList.some((tab, index) => { 57 | return (tab.fullPath || tab.path) === (fullPath || path) 58 | }) 59 | 60 | if (!tabHasExits && rawRoute) 61 | this.tabList.push(rawRoute) 62 | }, 63 | closeLeftTab(route: RouteLocationNormalized) { 64 | const index = this.tabList.findIndex(item => item.fullPath === route.fullPath) 65 | this.tabList = this.tabList.filter((item, i) => i >= index || (item?.meta?.affix ?? false)) 66 | }, 67 | close_rightTab(route: RouteLocationNormalized) { 68 | const index = this.tabList.findIndex(item => item.fullPath === route.fullPath) 69 | this.tabList = this.tabList.filter((item, i) => i <= index || (item?.meta?.affix ?? false)) 70 | }, 71 | close_otherTab(route: RouteLocationNormalized) { 72 | this.tabList = this.tabList.filter(item => item.fullPath === route.fullPath || (item?.meta?.affix ?? false)) 73 | }, 74 | async closeTab(route: RouteLocationNormalized, router: Router) { 75 | const close = (route: RouteLocationNormalized) => { 76 | const { fullPath, meta: { affix } = {} } = route 77 | if (affix) 78 | return 79 | 80 | const index = this.tabList.findIndex(item => item.fullPath === fullPath) 81 | index !== -1 && this.tabList.splice(index, 1) 82 | } 83 | 84 | const { currentRoute, replace } = router 85 | 86 | const { path } = unref(currentRoute) 87 | 88 | if (path !== route.path) { 89 | close(unref(route)) 90 | return 91 | } 92 | 93 | close(currentRoute.value) 94 | const toTarget = this.tabList[Math.max(0, this.tabList.length - 1)] 95 | await replace(toTarget) 96 | }, 97 | close_allTab() { 98 | this.tabList = this.tabList.filter(item => item?.meta?.affix ?? false) 99 | }, 100 | 101 | async sortTabs(oldIndex: number, newIndex: number) { 102 | const currentTab = this.tabList[oldIndex] 103 | this.tabList.splice(oldIndex, 1) 104 | this.tabList.splice(newIndex, 0, currentTab) 105 | }, 106 | 107 | async setTabTitle(title: string, route: RouteLocationNormalized) { 108 | const findTab = this.tabList.find(item => item === route) 109 | if (findTab) 110 | findTab.meta.title = title 111 | }, 112 | 113 | async updateTabPath(fullPath: string, route: RouteLocationNormalized) { 114 | const findTab = this.tabList.find(item => item === route) 115 | if (findTab) { 116 | findTab.fullPath = fullPath 117 | findTab.path = fullPath 118 | } 119 | }, 120 | }, 121 | persist: { 122 | key: 'app-tabs', 123 | }, 124 | }) 125 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import { getIconCollections, iconsPlugin } from '@egoist/tailwindcss-icons' 2 | import Typography from '@tailwindcss/typography' 3 | import type { Config } from 'tailwindcss' 4 | 5 | export default > { 6 | darkMode: 'class', 7 | theme: {}, 8 | plugins: [ 9 | iconsPlugin({ 10 | collections: getIconCollections(['ri', 'mdi', 'uil', 'ant-design']), 11 | }), 12 | Typography, 13 | ], 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /types/database.types.ts: -------------------------------------------------------------------------------- 1 | export type Json = 2 | | string 3 | | number 4 | | boolean 5 | | null 6 | | { [key: string]: Json } 7 | | Json[] 8 | 9 | export interface Database { 10 | public: { 11 | Tables: { 12 | tasks: { 13 | Row: { 14 | completed: boolean | null 15 | created_at: string | null 16 | id: number 17 | title: string | null 18 | user: string 19 | } 20 | Insert: { 21 | completed?: boolean | null 22 | created_at?: string | null 23 | id?: number 24 | title?: string | null 25 | user: string 26 | } 27 | Update: { 28 | completed?: boolean | null 29 | created_at?: string | null 30 | id?: number 31 | title?: string | null 32 | user?: string 33 | } 34 | } 35 | } 36 | Views: { 37 | [_ in never]: never 38 | } 39 | Functions: { 40 | [_ in never]: never 41 | } 42 | Enums: { 43 | [_ in never]: never 44 | } 45 | CompositeTypes: { 46 | [_ in never]: never 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | declare interface Fn { 3 | (...arg: T[]): R; 4 | } 5 | 6 | declare interface PromiseFn { 7 | (...arg: T[]): Promise; 8 | } 9 | 10 | declare type RefType = T | null; 11 | 12 | declare type LabelValueOptions = { 13 | label: string; 14 | value: any; 15 | [key: string]: string | number | boolean; 16 | }[]; 17 | 18 | declare type EmitType = (event: string | any, ...args: any[]) => void; 19 | 20 | // vue 21 | declare interface ComponentElRef { 22 | $el: T; 23 | } 24 | 25 | declare type ComponentRef = ComponentElRef | null; 26 | 27 | declare type ElRef = Nullable; 28 | 29 | declare type VueNode = VNodeChild | JSX.Element; 30 | 31 | export type Writable = { 32 | -readonly [P in keyof T]: T[P]; 33 | }; 34 | 35 | export type AnyFunction = (...args: any[]) => T 36 | 37 | declare type Nullable = T | null; 38 | declare type NonNullable = T extends null | undefined ? never : T; 39 | declare type Recordable = Record; 40 | declare type ReadonlyRecordable = { 41 | readonly [key: string]: T; 42 | }; 43 | declare type Indexable = { 44 | [key: string]: T; 45 | }; 46 | declare type DeepPartial = { 47 | [P in keyof T]?: DeepPartial; 48 | }; 49 | declare type TimeoutHandle = ReturnType; 50 | declare type IntervalHandle = ReturnType; 51 | } 52 | 53 | export { } 54 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '#app' { 2 | interface PageMeta { 3 | title?: string 4 | order?: number 5 | icon?: string 6 | affix?: boolean 7 | hideMenu?: boolean 8 | } 9 | } 10 | 11 | declare namespace API { 12 | interface Result { 13 | code: number 14 | message: string 15 | data: T 16 | } 17 | } -------------------------------------------------------------------------------- /utils/date.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Independent time operation tool to facilitate subsequent switch to dayjs 3 | */ 4 | import dayjs from 'dayjs' 5 | 6 | const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' 7 | const DATE_FORMAT = 'YYYY-MM-DD' 8 | 9 | export function formatToDateTime(date?: dayjs.ConfigType, format = DATE_TIME_FORMAT): string { 10 | return dayjs(date).format(format) 11 | } 12 | 13 | export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): string { 14 | return dayjs(date).format(format) 15 | } 16 | 17 | export const dateUtil = dayjs 18 | -------------------------------------------------------------------------------- /utils/domUtils.ts: -------------------------------------------------------------------------------- 1 | import { upperFirst } from 'lodash-es' 2 | 3 | export interface ViewportOffsetResult { 4 | left: number 5 | top: number 6 | right: number 7 | bottom: number 8 | rightIncludeBody: number 9 | bottomIncludeBody: number 10 | } 11 | 12 | export function getBoundingClientRect(element: Element): DOMRect | number { 13 | if (!element || !element.getBoundingClientRect) 14 | return 0 15 | 16 | return element.getBoundingClientRect() 17 | } 18 | 19 | function trim(string: string) { 20 | return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '') 21 | } 22 | 23 | /* istanbul ignore next */ 24 | export function hasClass(el: Element, cls: string) { 25 | if (!el || !cls) 26 | return false 27 | if (cls.includes(' ')) 28 | throw new Error('className should not contain space.') 29 | if (el.classList) 30 | return el.classList.contains(cls) 31 | 32 | else 33 | return (` ${el.className} `).includes(` ${cls} `) 34 | } 35 | 36 | /* istanbul ignore next */ 37 | export function addClass(el: Element, cls: string) { 38 | if (!el) 39 | return 40 | let curClass = el.className 41 | const classes = (cls || '').split(' ') 42 | 43 | for (let i = 0, j = classes.length; i < j; i++) { 44 | const clsName = classes[i] 45 | if (!clsName) 46 | continue 47 | 48 | if (el.classList) 49 | el.classList.add(clsName) 50 | 51 | else if (!hasClass(el, clsName)) 52 | curClass += ` ${clsName}` 53 | } 54 | if (!el.classList) 55 | el.className = curClass 56 | } 57 | 58 | /* istanbul ignore next */ 59 | export function removeClass(el: Element, cls: string) { 60 | if (!el || !cls) 61 | return 62 | const classes = cls.split(' ') 63 | let curClass = ` ${el.className} ` 64 | 65 | for (let i = 0, j = classes.length; i < j; i++) { 66 | const clsName = classes[i] 67 | if (!clsName) 68 | continue 69 | 70 | if (el.classList) 71 | el.classList.remove(clsName) 72 | 73 | else if (hasClass(el, clsName)) 74 | curClass = curClass.replace(` ${clsName} `, ' ') 75 | } 76 | if (!el.classList) 77 | el.className = trim(curClass) 78 | } 79 | /** 80 | * Get the left and top offset of the current element 81 | * left: the distance between the leftmost element and the left side of the document 82 | * top: the distance from the top of the element to the top of the document 83 | * right: the distance from the far right of the element to the right of the document 84 | * bottom: the distance from the bottom of the element to the bottom of the document 85 | * rightIncludeBody: the distance between the leftmost element and the right side of the document 86 | * bottomIncludeBody: the distance from the bottom of the element to the bottom of the document 87 | * 88 | * @description: 89 | */ 90 | export function getViewportOffset(element: Element): ViewportOffsetResult { 91 | const doc = document.documentElement 92 | 93 | const docScrollLeft = doc.scrollLeft 94 | const docScrollTop = doc.scrollTop 95 | const docClientLeft = doc.clientLeft 96 | const docClientTop = doc.clientTop 97 | 98 | const pageXOffset = window.pageXOffset 99 | const pageYOffset = window.pageYOffset 100 | 101 | const box = getBoundingClientRect(element) 102 | 103 | const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect 104 | 105 | const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0) 106 | const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0) 107 | const offsetLeft = retLeft + pageXOffset 108 | const offsetTop = rectTop + pageYOffset 109 | 110 | const left = offsetLeft - scrollLeft 111 | const top = offsetTop - scrollTop 112 | 113 | const clientWidth = window.document.documentElement.clientWidth 114 | const clientHeight = window.document.documentElement.clientHeight 115 | return { 116 | left, 117 | top, 118 | right: clientWidth - rectWidth - left, 119 | bottom: clientHeight - rectHeight - top, 120 | rightIncludeBody: clientWidth - left, 121 | bottomIncludeBody: clientHeight - top, 122 | } 123 | } 124 | 125 | export function hackCss(attr: string, value: string) { 126 | const prefix: string[] = ['webkit', 'Moz', 'ms', 'OT'] 127 | 128 | const styleObj: any = {} 129 | prefix.forEach((item) => { 130 | styleObj[`${item}${upperFirst(attr)}`] = value 131 | }) 132 | return { 133 | ...styleObj, 134 | [attr]: value, 135 | } 136 | } 137 | 138 | /* istanbul ignore next */ 139 | export function on( 140 | element: Element | HTMLElement | Document | Window, 141 | event: string, 142 | handler: EventListenerOrEventListenerObject, 143 | ): void { 144 | if (element && event && handler) 145 | element.addEventListener(event, handler, false) 146 | } 147 | 148 | /* istanbul ignore next */ 149 | export function off( 150 | element: Element | HTMLElement | Document | Window, 151 | event: string, 152 | handler: Fn, 153 | ): void { 154 | if (element && event && handler) 155 | element.removeEventListener(event, handler, false) 156 | } 157 | 158 | /* istanbul ignore next */ 159 | export function once(el: HTMLElement, event: string, fn: EventListener): void { 160 | const listener = function (this: any, ...args: unknown[]) { 161 | if (fn) 162 | fn.apply(this, args) 163 | 164 | off(el, event, listener) 165 | } 166 | on(el, event, listener) 167 | } 168 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | import { cloneDeep, isEqual, mergeWith, unionWith } from 'lodash-es' 2 | import { Icon } from '#components' 3 | 4 | export function deepMerge( 5 | target: T, 6 | source: U, 7 | ): T & U { 8 | return mergeWith(cloneDeep(target), source, (objValue, srcValue) => { 9 | if (isObject(objValue) && isObject(srcValue)) { 10 | return mergeWith(cloneDeep(objValue), srcValue, (prevValue, nextValue) => { 11 | // 如果是数组,合并数组(去重) If it is an array, merge the array (remove duplicates) 12 | return isArray(prevValue) ? unionWith(prevValue, nextValue, isEqual) : undefined 13 | }) 14 | } 15 | }) 16 | } 17 | 18 | export function renderIcon(icon: string) { 19 | return () => h(Icon, { name: icon }) 20 | } 21 | 22 | /** 23 | * Sums the passed percentage to the R, G or B of a HEX color 24 | * @param {string} color The color to change 25 | * @param {number} amount The amount to change the color by 26 | * @returns {string} The processed part of the color 27 | */ 28 | function addLight(color: string, amount: number) { 29 | const cc = Number.parseInt(color, 16) + amount 30 | const c = cc > 255 ? 255 : cc 31 | return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}` 32 | } 33 | 34 | /** 35 | * Lightens a 6 char HEX color according to the passed percentage 36 | * @param {string} color The color to change 37 | * @param {number} amount The amount to change the color by 38 | * @returns {string} The processed color represented as HEX 39 | */ 40 | export function lighten(color: string, amount: number) { 41 | color = color.includes('#') ? color.substring(1, color.length) : color 42 | amount = Math.trunc((255 * amount) / 100) 43 | return `#${addLight(color.substring(0, 2), amount)}${addLight( 44 | color.substring(2, 4), 45 | amount, 46 | )}${addLight(color.substring(4, 6), amount)}` 47 | } 48 | -------------------------------------------------------------------------------- /utils/is.ts: -------------------------------------------------------------------------------- 1 | const toString = Object.prototype.toString 2 | 3 | /** 4 | * @description: 判断值是否未某个类型 5 | */ 6 | export function is(val: unknown, type: string) { 7 | return toString.call(val) === `[object ${type}]` 8 | } 9 | 10 | /** 11 | * @description: 是否为函数 12 | */ 13 | 14 | export function isFunction(val: unknown): val is T { 15 | return is(val, 'Function') || is(val, 'AsyncFunction') 16 | } 17 | 18 | /** 19 | * @description: 是否已定义 20 | */ 21 | export function isDef(val?: T): val is T { 22 | return typeof val !== 'undefined' 23 | } 24 | 25 | export function isUnDef(val?: T): val is T { 26 | return !isDef(val) 27 | } 28 | /** 29 | * @description: 是否为对象 30 | */ 31 | export function isObject(val: any): val is Record { 32 | return val !== null && is(val, 'Object') 33 | } 34 | 35 | /** 36 | * @description: 是否为时间 37 | */ 38 | export function isDate(val: unknown): val is Date { 39 | return is(val, 'Date') 40 | } 41 | 42 | /** 43 | * @description: 是否为数值 44 | */ 45 | export function isNumber(val: unknown): val is number { 46 | return is(val, 'Number') 47 | } 48 | 49 | /** 50 | * @description: 是否为AsyncFunction 51 | */ 52 | export function isAsyncFunction(val: unknown): val is () => Promise { 53 | return is(val, 'AsyncFunction') 54 | } 55 | 56 | /** 57 | * @description: 是否为promise 58 | */ 59 | export function isPromise(val: unknown): val is Promise { 60 | return is(val, 'Promise') && isObject(val) && isFunction(val.then) && isFunction(val.catch) 61 | } 62 | 63 | /** 64 | * @description: 是否为字符串 65 | */ 66 | export function isString(val: unknown): val is string { 67 | return is(val, 'String') 68 | } 69 | 70 | /** 71 | * @description: 是否为boolean类型 72 | */ 73 | export function isBoolean(val: unknown): val is boolean { 74 | return is(val, 'Boolean') 75 | } 76 | 77 | /** 78 | * @description: 是否为数组 79 | */ 80 | export function isArray(val: any): val is Array { 81 | return val && Array.isArray(val) 82 | } 83 | 84 | /** 85 | * @description: 是否客户端 86 | */ 87 | export function isClient() { 88 | return typeof window !== 'undefined' 89 | } 90 | 91 | /** 92 | * @description: 是否为浏览器 93 | */ 94 | export function isWindow(val: any): val is Window { 95 | return typeof window !== 'undefined' && is(val, 'Window') 96 | } 97 | 98 | export function isElement(val: unknown): val is Element { 99 | return isObject(val) && !!val.tagName 100 | } 101 | 102 | export const isServer = typeof window === 'undefined' 103 | 104 | // 是否为图片节点 105 | export function isImageDom(o: Element) { 106 | return o && ['IMAGE', 'IMG'].includes(o.tagName) 107 | } 108 | 109 | export function isNull(val: unknown): val is null { 110 | return val === null 111 | } 112 | 113 | export function isNullAndUnDef(val: unknown): val is null | undefined { 114 | return isUnDef(val) && isNull(val) 115 | } 116 | 117 | export function isNullOrUnDef(val: unknown): val is null | undefined { 118 | return isUnDef(val) || isNull(val) 119 | } 120 | 121 | export function isEmail(str: string) { 122 | const reg = /^([a-zA-Z]|[0-9])(\w|-)+@[a-zA-Z0-9]+\.([a-zA-Z]{2,4})$/ 123 | return reg.test(str) 124 | } 125 | -------------------------------------------------------------------------------- /utils/request.ts: -------------------------------------------------------------------------------- 1 | import { createDiscreteApi } from 'naive-ui' 2 | 3 | const { message } = createDiscreteApi( 4 | ['message'], 5 | ) 6 | 7 | export function getHeaders(defaultHeaders = {}) { 8 | return { 9 | ...defaultHeaders, 10 | } 11 | } 12 | 13 | const _fetch = $fetch.create({ 14 | async onRequest({ options }) { 15 | options.headers = getHeaders(options.headers) 16 | }, 17 | async onResponse({ response }) { 18 | 19 | }, 20 | async onResponseError({ response, options }) { 21 | options?.params?.noMessage || message.error(response._data?.message || '服务器错误') 22 | }, 23 | }) 24 | 25 | const request = _fetch 26 | 27 | export default request 28 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | test: { 6 | testTimeout: 60 * 1000, 7 | }, 8 | }) 9 | --------------------------------------------------------------------------------