11 |
12 | {{ config.title }}
13 |
14 |
3 | Admin 3
4 |
3 | Admin 3
4 | ${t}
`:t};e.add("pagebreak",(e=>{(e=>{const a=e.options.register;a("pagebreak_separator",{processor:"string",default:"\x3c!-- pagebreak --\x3e"}),a("pagebreak_split_block",{processor:"boolean",default:!1})})(e),(e=>{e.addCommand("mcePageBreak",(()=>{e.insertContent(s(n(e)))}))})(e),(e=>{const a=()=>e.execCommand("mcePageBreak");e.ui.registry.addButton("pagebreak",{icon:"page-break",tooltip:"Page break",onAction:a}),e.ui.registry.addMenuItem("pagebreak",{text:"Page break",icon:"page-break",onAction:a})})(e),(e=>{const a=r(e),t=()=>n(e),c=new RegExp(a.replace(/[\?\.\*\[\]\(\)\{\}\+\^\$\:]/g,(e=>"\\"+e)),"gi");e.on("BeforeSetContent",(e=>{e.content=e.content.replace(c,s(t()))})),e.on("PreInit",(()=>{e.serializer.addNodeFilter("img",(r=>{let n,s,c=r.length;for(;c--;)if(n=r[c],s=n.attr("class"),s&&-1!==s.indexOf(o)){const r=n.parent;if(r&&e.schema.getBlockElements()[r.name]&&t()){r.type=3,r.value=a,r.raw=!0,n.remove();continue}n.type=3,n.value=a,n.raw=!0}}))}))})(e),(e=>{e.on("ResolveName",(a=>{"IMG"===a.target.nodeName&&e.dom.hasClass(a.target,o)&&(a.name="pagebreak")}))})(e)}))}(); -------------------------------------------------------------------------------- /src/middleware/auth.global.ts: -------------------------------------------------------------------------------- 1 | import { storeToRefs } from 'pinia' 2 | 3 | const whiteList = ['/', '/Home*'] 4 | 5 | function pathToRegExp(path: string) { 6 | const pattern = path.replace(/\//g, '\/').replace(/\*/g, '.*') 7 | return new RegExp(`^${pattern}$`) 8 | } 9 | 10 | export default defineNuxtRouteMiddleware(async (to: any) => { 11 | 12 | const userStore = useUserStore() 13 | const { authToken, userInfo } = storeToRefs(userStore) 14 | if (authToken.value && userInfo.value.username) { 15 | const permissionStore = usePermissionStore() 16 | const { menus, taggedMenus, currentMenu } = storeToRefs(permissionStore) 17 | // if (menus.value.length === 0) 18 | await permissionStore.generateMenus() 19 | 20 | const tagged = taggedMenus.value.find(menu => menu.path === to.path) 21 | if (tagged) 22 | currentMenu.value = tagged 23 | if (to.matched.length > 0 && to.matched[0].path) { 24 | /* Hack: 去掉浏览器自动给 href 末尾添加的 '/' */ 25 | if (to.path.endsWith('/') && to.fullPath.endsWith('/') && to.href.endsWith('/')) { 26 | to.path = to.path.slice(0, -1) 27 | to.fullPath = to.fullPath.slice(0, -1) 28 | to.href = to.href.slice(0, -1) 29 | } 30 | 31 | const menuItem = useTreeFind(menus.value, (menu: any) => to.matched[0].path.replace(/[()]/g, '') === menu.path) 32 | if (!tagged && menuItem) 33 | permissionStore.addMenuTag({ ...menuItem }) 34 | } 35 | } 36 | 37 | /* 路径不在白名单内,重定向至登陆页面 */ 38 | else if (!whiteList.some(path => pathToRegExp(path).test(to.path))) { 39 | return navigateTo('/') 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /src/public/js/tinymce/plugins/save/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.3.1 (2022-12-06) 3 | */ 4 | !function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const n=("function",e=>"function"==typeof e);var o=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),t=tinymce.util.Tools.resolve("tinymce.util.Tools");const a=e=>n=>n.options.get(e),c=a("save_enablewhendirty"),i=a("save_onsavecallback"),s=a("save_oncancelcallback"),r=(e,n)=>{e.notificationManager.open({text:n,type:"error"})},l=e=>n=>{const o=()=>{n.setEnabled(!c(e)||e.isDirty())};return o(),e.on("NodeChange dirty",o),()=>e.off("NodeChange dirty",o)};e.add("save",(e=>{(e=>{const n=e.options.register;n("save_enablewhendirty",{processor:"boolean",default:!0}),n("save_onsavecallback",{processor:"function"}),n("save_oncancelcallback",{processor:"function"})})(e),(e=>{e.ui.registry.addButton("save",{icon:"save",tooltip:"Save",enabled:!1,onAction:()=>e.execCommand("mceSave"),onSetup:l(e)}),e.ui.registry.addButton("cancel",{icon:"cancel",tooltip:"Cancel",enabled:!1,onAction:()=>e.execCommand("mceCancel"),onSetup:l(e)}),e.addShortcut("Meta+S","","mceSave")})(e),(e=>{e.addCommand("mceSave",(()=>{(e=>{const t=o.DOM.getParent(e.id,"form");if(c(e)&&!e.isDirty())return;e.save();const a=i(e);if(n(a))return a.call(e,e),void e.nodeChanged();t?(e.setDirty(!1),t.onsubmit&&!t.onsubmit()||("function"==typeof t.submit?t.submit():r(e,"Error: Form submit field collision.")),e.nodeChanged()):r(e,"Error: No form element found.")})(e)})),e.addCommand("mceCancel",(()=>{(e=>{const o=t.trim(e.startContent),a=s(e);n(a)?a.call(e,e):e.resetContent(o)})(e)}))})(e)}))}(); -------------------------------------------------------------------------------- /nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import Aura from '@primevue/themes/aura' 2 | import { primeLocaleCN } from './src/assets/primeLocaleCN' 3 | 4 | const baseUrl = '/' 5 | 6 | export default defineNuxtConfig({ 7 | ssr: false, 8 | devtools: { enabled: false }, 9 | srcDir: 'src/', 10 | 11 | /* 模拟后端 */ 12 | serverDir: 'server-mock/', 13 | 14 | nitro: { 15 | devStorage: { 16 | db: { 17 | driver: 'fs', 18 | base: 'server-mock/db', 19 | }, 20 | }, 21 | }, 22 | 23 | runtimeConfig: { 24 | public: { 25 | iconifyProvider: 'https://api.iconify.design', 26 | apiBase: '/mock-api', // 'http://localhost:8001/mock-api' 27 | }, 28 | }, 29 | 30 | app: { 31 | baseURL: baseUrl, 32 | }, 33 | 34 | /* 禁用载入的 nuxt loading 动画 */ 35 | spaLoadingTemplate: false, 36 | 37 | experimental: { 38 | /* 加快首次启动速度 */ 39 | watcher: 'chokidar', 40 | /* 生成静态文件 */ 41 | payloadExtraction: false, 42 | }, 43 | 44 | css: [ 45 | '@/assets/css/main.css', 46 | ], 47 | 48 | modules: [ 49 | '@unocss/nuxt', 50 | '@element-plus/nuxt', 51 | '@primevue/nuxt-module', 52 | '@vueuse/nuxt', 53 | ['@pinia/nuxt', { autoImports: ['defineStore'] }], 54 | ], 55 | 56 | elementPlus: { 57 | defaultLocale: 'zh-cn', 58 | globalConfig: { 59 | size: 'default', 60 | }, 61 | }, 62 | 63 | primevue: { 64 | options: { 65 | locale: primeLocaleCN, 66 | theme: { 67 | preset: Aura, 68 | }, 69 | }, 70 | composables: { 71 | include: '*', 72 | }, 73 | }, 74 | 75 | compatibilityDate: '2024-09-30', 76 | }) 77 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // ========== FileNesting Config ========== 3 | "explorer.fileNesting.enabled": true, 4 | "explorer.fileNesting.expand": false, 5 | "explorer.fileNesting.patterns": { 6 | "README.md": "LICENSE, *.md, *.sh, cliff.toml", 7 | "nuxt.config.ts": "*config.ts, *config.json, .env.*", 8 | "package.json": ".gitignore, yarn.lock, *config.js" 9 | }, 10 | // ========== ESlint Config ========== 11 | "editor.formatOnSave": false, 12 | "editor.codeActionsOnSave": { 13 | "source.fixAll.eslint": "explicit", 14 | "source.organizeImports": "never" 15 | }, 16 | "eslint.validate": [ 17 | "javascript", 18 | "typescript", 19 | "vue", 20 | "html", 21 | "markdown", 22 | "json", 23 | "jsonc", 24 | "yaml" 25 | ], 26 | // ========== cSpell Config ========== 27 | "cSpell.words": [ 28 | "antfu", 29 | "Attributify", 30 | "bumpp", 31 | "cascader", 32 | "Commitizen", 33 | "Cpath", 34 | "Csvg", 35 | "datav", 36 | "dimm", 37 | "docus", 38 | "Gitee", 39 | "iconify", 40 | "Jian", 41 | "keyid", 42 | "nuxt", 43 | "nuxtjs", 44 | "octocat", 45 | "ofetch", 46 | "pinceau", 47 | "Pinia", 48 | "Prefixs", 49 | "primevue", 50 | "shiki", 51 | "shimo", 52 | "sortablejs", 53 | "ssjson", 54 | "tada", 55 | "taze", 56 | "tiktok", 57 | "tinymce", 58 | "todos", 59 | "topo", 60 | "twemoji", 61 | "uncategorized", 62 | "unocss", 63 | "unstorage", 64 | "vitepress", 65 | "vitesse", 66 | "vuepress", 67 | "vueuse", 68 | "Vuex", 69 | "Zhongsong" 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /src/utils/docs.ts: -------------------------------------------------------------------------------- 1 | export const sourceUrl = 'https://github.com/vampirefan/admin3' 2 | export const guideUrl = 'https://admin3-docs.netlify.app' 3 | 4 | export const docList = [ 5 | { label: 'Nuxt3', icon: 'i-logos-nuxt-icon', url: 'https://nuxt.com/' }, 6 | { label: 'Vue3', icon: 'i-logos-vue', url: 'https://cn.vuejs.org/' }, 7 | { label: 'ElementPlus', icon: 'i-logos-element', url: 'https://element-plus.org/' }, 8 | { label: 'PrimeVue', image: '/image/primevue.png', url: 'https://primevue.org/' }, 9 | { label: 'VueUse', icon: 'i-logos-vueuse', url: 'https://vueuse.org/' }, 10 | { label: 'Typescript', icon: 'i-logos-typescript-icon', url: 'https://www.typescriptlang.org/' }, 11 | { label: 'Eslint', icon: 'i-logos-eslint', url: 'https://github.com/antfu/eslint-config' }, 12 | { label: 'Unocss', icon: 'i-logos-unocss', url: 'https://unocss.dev/' }, 13 | { label: 'TailwindCSS', icon: 'i-logos-tailwindcss-icon', url: 'https://tailwindcss.com/' }, 14 | { label: 'Iconify', icon: 'i-simple-icons-iconify', url: 'https://iconify.design/docs/' }, 15 | ] 16 | 17 | export const showcase = { 18 | label: '快速链接', 19 | list: [ 20 | { label: 'Admin3 文档', image: '/image/logo/admin3.png', url: 'https://admin3-docs.netlify.app' }, 21 | { label: 'Admin3 源码', icon: 'i-logos-github-octocat', url: 'https://github.com/vampirefan/admin3' }, 22 | ], 23 | } 24 | 25 | export const pdfFileUrlOptions = [{ 26 | label: '本地 /static/Nuxtjs-Cheat-Sheet.pdf', 27 | value: '/static/Nuxtjs-Cheat-Sheet.pdf', 28 | }, { 29 | label: '在线 https://education.github.com/git-cheat-sheet-education.pdf', 30 | value: 'https://education.github.com/git-cheat-sheet-education.pdf', 31 | }] 32 | -------------------------------------------------------------------------------- /src/public/js/tinymce/plugins/preview/plugin.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.3.1 (2022-12-06) 3 | */ 4 | !function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.Env"),o=tinymce.util.Tools.resolve("tinymce.util.Tools");const n=e=>t=>t.options.get(e),i=n("content_style"),s=n("content_css_cors"),c=n("body_class"),r=n("body_id");e.add("preview",(e=>{(e=>{e.addCommand("mcePreview",(()=>{(e=>{const n=(e=>{var n;let l="";const a=e.dom.encode,d=null!==(n=i(e))&&void 0!==n?n:"";l+='
45 | {{ tableData }}
46 |
47 |
62 | {{ tableData }}
63 |
64 | {{ valueBasic }}
37 |{{ valueFromLabel }}
42 |{{ valueClearable }}
47 |{{ valueOnlyShowLast }}
52 |{{ valueCheckStrictly }}
57 |{{ valueFilterable }}
62 |{{ valueFilterable }}
67 |
79 | {{ tableData }}
80 |
81 |
95 | {{ columnData }}
96 |
97 | | 姓名 | 85 |86 | {{ day }} 87 | | 88 |
|---|---|
| {{ employee.name }} | 93 |98 | {{ isAbsent(employee.id, day) ? '缺' : '√' }} 99 | | 100 |
{{ "" }} 。另外,也提供了几种实现思路。
22 |
25 | 1. {{ "
65 | 2. 使用 vueuse 中的 useIntervalFn() 实现动态更新文字元素的样式实现其滚动效果。 66 |
67 |82 | 3. 使用 el-carousel(走马灯)实现的垂直滚动效果 83 |
84 |93 | 4. 使用 unocss 的动画样式效果 94 |
95 |76 | {{ item.label }} 77 |
78 |
47 | Admin 3
48 | 113 | {{ articleNumber }} 114 |
115 | 116 |117 | {{ articleTitle }} 118 |
119 |