├── .env.dev
├── .env.prod
├── .env.test
├── public
├── img.png
├── img1.png
├── img10.png
├── img11.png
├── img12.png
├── img13.png
├── img14.png
├── img15.png
├── img16.png
├── img2.png
├── img3.png
├── img4.png
├── img5.png
├── img6.png
├── img7.png
├── img8.png
├── img9.png
├── img_1.png
├── vite.svg
└── print-lock.css
├── src
├── service
│ ├── list
│ │ └── index.js
│ ├── user
│ │ └── index.js
│ └── posts
│ │ └── index.js
├── assets
│ ├── login-banner.png
│ └── vue.svg
├── pages
│ ├── test
│ │ ├── list.vue
│ │ ├── test1.vue
│ │ └── test2.vue
│ ├── user
│ │ ├── list
│ │ │ └── index.vue
│ │ └── role
│ │ │ ├── components
│ │ │ └── RoleFormModal.vue
│ │ │ └── index.vue
│ ├── com
│ │ ├── lazy
│ │ │ └── index.vue
│ │ ├── count-down
│ │ │ └── index.vue
│ │ ├── modal
│ │ │ └── index.vue
│ │ ├── radius-tabs
│ │ │ └── index.vue
│ │ ├── auth
│ │ │ └── index.vue
│ │ ├── mark-keyword
│ │ │ └── index.vue
│ │ └── text-ellipsis
│ │ │ └── index.vue
│ ├── plugin
│ │ ├── icons
│ │ │ └── index.vue
│ │ ├── watermark
│ │ │ └── index.vue
│ │ ├── check-card
│ │ │ └── index.vue
│ │ ├── browser-key
│ │ │ └── index.vue
│ │ ├── org_tree
│ │ │ └── index.vue
│ │ ├── lodash
│ │ │ └── index.vue
│ │ ├── sign
│ │ │ └── index.vue
│ │ ├── zip
│ │ │ └── index.vue
│ │ ├── excel
│ │ │ └── index.vue
│ │ ├── dayjs
│ │ │ └── index.vue
│ │ ├── calendar
│ │ │ └── index.vue
│ │ └── idcard
│ │ │ └── index.vue
│ ├── setting
│ │ ├── menu-enter
│ │ │ ├── components
│ │ │ │ ├── HeaderSearch.vue
│ │ │ │ ├── MenuFormModal.vue
│ │ │ │ └── List.vue
│ │ │ └── index.vue
│ │ └── profile
│ │ │ └── index.vue
│ ├── login
│ │ ├── index.vue
│ │ └── components
│ │ │ ├── login-form.vue
│ │ │ └── banner.vue
│ ├── dashboard
│ │ ├── components
│ │ │ ├── slot_prop.vue
│ │ │ ├── child.vue
│ │ │ └── sun.vue
│ │ └── index.vue
│ ├── exception
│ │ ├── 500-page.vue
│ │ ├── 401-page.vue
│ │ └── 404-page.vue
│ ├── case
│ │ └── tab
│ │ │ └── index.vue
│ ├── list
│ │ ├── card-list
│ │ │ └── index.vue
│ │ └── search-list
│ │ │ └── index.vue
│ ├── print
│ │ └── index.vue
│ └── charts
│ │ ├── g2-column
│ │ └── index.vue
│ │ ├── line
│ │ └── index.vue
│ │ └── pie
│ │ └── chart
│ │ └── p.vue
├── layout
│ ├── sider
│ │ ├── index.js
│ │ ├── components
│ │ │ ├── Logo.vue
│ │ │ ├── util.js
│ │ │ ├── Menu.vue
│ │ │ └── ColumnTabs.vue
│ │ └── index.vue
│ ├── header
│ │ ├── components
│ │ │ ├── Lock.vue
│ │ │ └── Avatar.vue
│ │ └── index.vue
│ └── index.vue
├── config
│ ├── index.js
│ └── icons.js
├── router
│ ├── index.js
│ ├── group
│ │ ├── home.js
│ │ ├── print.js
│ │ ├── case.js
│ │ ├── user.js
│ │ ├── list.js
│ │ ├── setting.js
│ │ ├── exception.js
│ │ ├── chart.js
│ │ ├── com.js
│ │ └── plugin.js
│ └── routes.js
├── components
│ ├── Footer
│ │ └── index.vue
│ ├── ConfigProvider
│ │ └── index.vue
│ ├── DynamicIcons
│ │ └── index.vue
│ ├── global.component.js
│ ├── TextEllipsis
│ │ └── index.vue
│ ├── FullScreen
│ │ └── index.vue
│ ├── ModalFooter
│ │ └── index.vue
│ ├── TextMark
│ │ └── index.vue
│ ├── RadiusTabs
│ │ └── index.vue
│ ├── Lock
│ │ └── index.vue
│ ├── ProTable
│ │ └── index.vue
│ ├── SearchBox
│ │ └── index.vue
│ └── CheckCard
│ │ └── index.vue
├── store
│ ├── counter.js
│ ├── setting.js
│ ├── user-info.js
│ ├── side-menu.js
│ └── dynamic-router.js
├── directive
│ ├── index.js
│ ├── lazyImg.js
│ ├── scrollbar.js
│ ├── loading.js
│ └── hasAuth.js
├── utils
│ ├── get.js
│ ├── router-listener.js
│ ├── index.js
│ ├── print
│ │ └── index.js
│ ├── table.js
│ ├── storage.js
│ └── date.js
├── App.vue
├── main.js
├── hooks
│ ├── util.js
│ ├── useModal
│ │ ├── index.js
│ │ └── useModal.md
│ ├── useFetch
│ │ ├── demo.md
│ │ └── index.js
│ └── useCountDown
│ │ └── index.js
├── vendor
│ ├── zip.js
│ └── excel.js
├── styles
│ ├── variables.less
│ └── global.less
├── lib
│ ├── echarts.js
│ └── theme
│ │ └── walden.project.json
├── mock
│ └── data.js
└── bootstrap.js
├── env.d.ts
├── vercel.json
├── .editorconfig
├── .gitignore
├── uno.config.js
├── prettier.config.js
├── index.html
├── vite.config.js
├── package.json
└── README.md
/.env.dev:
--------------------------------------------------------------------------------
1 | VITE_BASE_API = https://mock.apifox.com/m1/3887514-0-default
2 |
--------------------------------------------------------------------------------
/.env.prod:
--------------------------------------------------------------------------------
1 | VITE_BASE_API = https://mock.apifox.com/m1/3887514-0-default
2 |
--------------------------------------------------------------------------------
/.env.test:
--------------------------------------------------------------------------------
1 | VITE_BASE_API = https://mock.apifox.com/m1/3887514-0-default
2 |
--------------------------------------------------------------------------------
/public/img.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img.png
--------------------------------------------------------------------------------
/public/img1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img1.png
--------------------------------------------------------------------------------
/public/img10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img10.png
--------------------------------------------------------------------------------
/public/img11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img11.png
--------------------------------------------------------------------------------
/public/img12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img12.png
--------------------------------------------------------------------------------
/public/img13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img13.png
--------------------------------------------------------------------------------
/public/img14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img14.png
--------------------------------------------------------------------------------
/public/img15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img15.png
--------------------------------------------------------------------------------
/public/img16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img16.png
--------------------------------------------------------------------------------
/public/img2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img2.png
--------------------------------------------------------------------------------
/public/img3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img3.png
--------------------------------------------------------------------------------
/public/img4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img4.png
--------------------------------------------------------------------------------
/public/img5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img5.png
--------------------------------------------------------------------------------
/public/img6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img6.png
--------------------------------------------------------------------------------
/public/img7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img7.png
--------------------------------------------------------------------------------
/public/img8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img8.png
--------------------------------------------------------------------------------
/public/img9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img9.png
--------------------------------------------------------------------------------
/public/img_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/public/img_1.png
--------------------------------------------------------------------------------
/src/service/list/index.js:
--------------------------------------------------------------------------------
1 | export const API_LIST = {
2 | // 列表
3 | list: '/list/card',
4 | }
5 |
--------------------------------------------------------------------------------
/src/assets/login-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/TopAlien/vue-admin/HEAD/src/assets/login-banner.png
--------------------------------------------------------------------------------
/env.d.ts:
--------------------------------------------------------------------------------
1 | interface ImportMetaEnv {
2 | // Auto generate by env-parse
3 | readonly VITE_BASE_API: string
4 | }
--------------------------------------------------------------------------------
/src/pages/test/list.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | list - page
5 |
6 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {
4 | "source": "/(.*)",
5 | "destination": "/"
6 | }
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/src/pages/user/list/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 用户列表(编辑角色-就会带出权限, 其他个人信息,管理用户还是,普通用户等)
5 |
6 |
--------------------------------------------------------------------------------
/src/pages/test/test1.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | test1 - page
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/service/user/index.js:
--------------------------------------------------------------------------------
1 | export const API_USER = {
2 | // 角色动态权限
3 | roleRoutes: '/user/routes',
4 |
5 | // 用户角色列表
6 | roleList: '/role/list'
7 | }
8 |
--------------------------------------------------------------------------------
/src/pages/test/test2.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | test2 - page
6 | 跳转一下
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/service/posts/index.js:
--------------------------------------------------------------------------------
1 | export const API_POSTS = {
2 | // 列表
3 | photoList: 'https://jsonplaceholder.typicode.com/photos',
4 |
5 | // 详情
6 | photoDetail: ''
7 | }
8 |
--------------------------------------------------------------------------------
/src/layout/sider/index.js:
--------------------------------------------------------------------------------
1 | import ColumnTabs from '@/layout/sider/components/ColumnTabs.vue'
2 | import Menu from '@/layout/sider/components/Menu.vue'
3 |
4 | export { ColumnTabs, Menu }
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,ts,tsx,vue}]
2 | indent_style = space
3 | indent_size = 2
4 | end_of_line = lf
5 | trim_trailing_whitespace = true
6 | insert_final_newline = true
7 | max_line_length = 100
8 |
--------------------------------------------------------------------------------
/src/layout/sider/components/Logo.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/config/index.js:
--------------------------------------------------------------------------------
1 | export default {
2 | title: 'Vue3 Vite',
3 |
4 | onlyMenu: true,
5 |
6 | /** 自定义滚动条 */
7 | useCustomScrollBar: true,
8 |
9 | /** 后台动态路由权限 */
10 | useDynamicRoute: false
11 | }
12 |
--------------------------------------------------------------------------------
/src/layout/sider/index.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/pages/com/lazy/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/pages/plugin/icons/index.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/pages/setting/menu-enter/components/HeaderSearch.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/pages/login/index.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/router/index.js:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHistory } from 'vue-router'
2 | import { BASE_ROUTE } from './routes.js'
3 |
4 | const router = createRouter({
5 | history: createWebHistory(),
6 | routes: BASE_ROUTE,
7 | scrollBehavior: () => ({ left: 0, top: 0 })
8 | })
9 |
10 | export default router
11 |
--------------------------------------------------------------------------------
/src/components/Footer/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
16 |
--------------------------------------------------------------------------------
/src/pages/dashboard/components/slot_prop.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/pages/dashboard/components/child.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
child-sex {{ sex }}
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/components/ConfigProvider/index.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/store/counter.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 |
3 | const useCounterStore = defineStore('counter', {
4 | state: () => ({ count: 0 }),
5 | getters: {
6 | double: (state) => state.count * 2
7 | },
8 | actions: {
9 | increment() {
10 | this.count++
11 | }
12 | }
13 | })
14 |
15 | export default useCounterStore
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/src/pages/plugin/watermark/index.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/pages/exception/500-page.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | 返回首页
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/pages/exception/401-page.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 | 返回首页
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/directive/index.js:
--------------------------------------------------------------------------------
1 | import scrollbar from './scrollbar.js'
2 | import auth from './hasAuth.js'
3 | import loading from './loading.js'
4 | import lazyImg from './lazyImg.js'
5 |
6 | const directives = {
7 | scrollbar,
8 | auth,
9 | loading,
10 | lazyImg
11 | }
12 |
13 | export default {
14 | install(app) {
15 | Object.keys(directives).forEach((key) => {
16 | app.directive(key, directives[key])
17 | })
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/pages/setting/menu-enter/index.vue:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/pages/exception/404-page.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 | 返回首页
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/layout/header/components/Lock.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 | 锁屏
10 |
11 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/pages/dashboard/components/sun.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 |
child-sex {{ sex }}
15 |
孙子节点修改
16 |
17 |
18 |
--------------------------------------------------------------------------------
/uno.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig, presetUno } from 'unocss'
2 | import presetIcons from '@unocss/preset-icons/browser'
3 | import safelist from './src/config/icons.js'
4 |
5 | export default defineConfig({
6 | presets: [
7 | presetUno(),
8 | presetIcons({
9 | scale: 1.2,
10 | collections: {
11 | carbon: () => import('@iconify-json/carbon/icons.json').then((i) => i.default)
12 | }
13 | })
14 | ],
15 | safelist
16 | })
17 |
--------------------------------------------------------------------------------
/src/pages/plugin/check-card/index.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
18 |
19 |
--------------------------------------------------------------------------------
/src/assets/vue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/directive/lazyImg.js:
--------------------------------------------------------------------------------
1 | import { useIntersectionObserver } from '@vueuse/core'
2 |
3 | /**
4 | v-lazyImg="'https://via.placeholder.com/250'"
5 | */
6 | export default {
7 | mounted(el, binding) {
8 | const { stop } = useIntersectionObserver(
9 | el,
10 | ([{ isIntersecting }], observerElement) => {
11 | if (isIntersecting) {
12 | stop()
13 |
14 | el.src = binding.value
15 | }
16 | },
17 | { threshold: 0 }
18 | )
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/DynamicIcons/index.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
21 |
22 |
--------------------------------------------------------------------------------
/src/utils/get.js:
--------------------------------------------------------------------------------
1 | /**
2 | 性能对比: at > slice > length
3 | */
4 | export const getValByIndex = (arr, index) => {
5 | if (Object.prototype.toString.call(arr) !== '[object Array]') {
6 | console.error('arr 不是一个数组!')
7 | return
8 | }
9 | if (arr.at) {
10 | return arr.at(index)
11 | }
12 |
13 | return arr.slice(index)[0]
14 | }
15 |
16 | export const getLast = (arr) => {
17 | return getValByIndex(arr, -1)
18 | }
19 |
20 | export const getFirst = (arr) => {
21 | return getValByIndex(arr, 0)
22 | }
23 |
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import { createPinia } from 'pinia'
3 | import directive from './directive'
4 | import globalComponent from '@/components/global.component.js'
5 | import router from '@/router/index.js'
6 | import 'virtual:uno.css'
7 | import './styles/global.less'
8 | import App from './App.vue'
9 |
10 | import './bootstrap.js'
11 |
12 | const app = createApp(App)
13 | const pinia = createPinia()
14 | app.use(pinia)
15 | app.use(directive)
16 | app.use(globalComponent)
17 | app.use(router)
18 | app.mount('#app')
19 |
--------------------------------------------------------------------------------
/src/pages/com/count-down/index.vue:
--------------------------------------------------------------------------------
1 |
16 |
17 | count-down {{ current }}
18 |
19 |
20 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | arrowParens: 'always',
3 | bracketSameLine: false,
4 | bracketSpacing: true,
5 | embeddedLanguageFormatting: 'auto',
6 | htmlWhitespaceSensitivity: 'ignore',
7 | insertPragma: false,
8 | jsxSingleQuote: false,
9 | printWidth: 120,
10 | proseWrap: 'preserve',
11 | quoteProps: 'as-needed',
12 | requirePragma: false,
13 | trailingComma: 'none',
14 | semi: false,
15 | singleQuote: true,
16 | tabWidth: 2,
17 | singleAttributePerLine: true,
18 | useTabs: false,
19 | vueIndentScriptAndStyle: true,
20 | }
21 |
--------------------------------------------------------------------------------
/src/router/group/home.js:
--------------------------------------------------------------------------------
1 | import Layout from '@/layout/index.vue'
2 |
3 | export default [
4 | {
5 | path: '/',
6 | component: Layout,
7 | name: 'Base',
8 | redirect: '/home',
9 | meta: {
10 | title: '首页',
11 | isGroup: true,
12 | icon: 'i-carbon-cloud-monitoring'
13 | },
14 | children: [
15 | {
16 | path: '/home',
17 | name: 'Home',
18 | component: () => import('@/pages/dashboard/index.vue'),
19 | meta: {
20 | title: '首页'
21 | }
22 | }
23 | ]
24 | }
25 | ]
26 |
--------------------------------------------------------------------------------
/src/pages/plugin/browser-key/index.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/utils/router-listener.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 单独监听路由会浪费渲染性能。使用发布订阅模式去进行分发管理。
3 | */
4 | import mitt from 'mitt'
5 |
6 | const emitter = mitt()
7 |
8 | const EMIT_KEY = 'ROUTE_CHANGE'
9 |
10 | let latestRoute
11 |
12 | export function setRouteEmitter(to) {
13 | emitter.emit(EMIT_KEY, to)
14 | latestRoute = to
15 | }
16 |
17 | export function listenerRouteChange(handler, immediate = true) {
18 | emitter.on(EMIT_KEY, handler)
19 | if (immediate && latestRoute) {
20 | handler(latestRoute)
21 | }
22 | }
23 |
24 | export function removeRouteListener() {
25 | emitter.off(EMIT_KEY)
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/global.component.js:
--------------------------------------------------------------------------------
1 | import ProTable from './ProTable/index.vue'
2 | import SearchBox from './SearchBox/index.vue'
3 | import ModalFooter from './ModalFooter/index.vue'
4 |
5 | import { setupCalendar } from 'v-calendar'
6 | import vue3TreeOrg from 'vue3-tree-org'
7 | import 'vue3-tree-org/lib/vue3-tree-org.css'
8 |
9 | export default {
10 | install(app) {
11 | app.component('ProTable', ProTable)
12 | app.component('SearchBox', SearchBox)
13 | app.component('ModalFooter', ModalFooter)
14 |
15 | app.use(vue3TreeOrg)
16 | app.use(setupCalendar, {})
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/router/group/print.js:
--------------------------------------------------------------------------------
1 | import Layout from '@/layout/index.vue'
2 |
3 | const BASE_URL = '/print'
4 |
5 | export default [
6 | {
7 | path: BASE_URL,
8 | component: Layout,
9 | name: 'Print',
10 | meta: {
11 | title: '打印',
12 | isGroup: true,
13 | icon: 'i-carbon-printer'
14 | },
15 | children: [
16 | {
17 | path: `${BASE_URL}/list`,
18 | name: 'Print-List',
19 | component: () => import('@/pages/print/index.vue'),
20 | meta: {
21 | title: '打印-1',
22 | icon: 'i-carbon-printer'
23 | }
24 | }
25 | ]
26 | }
27 | ]
28 |
--------------------------------------------------------------------------------
/src/pages/com/modal/index.vue:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
21 | use-modal
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/router/group/case.js:
--------------------------------------------------------------------------------
1 | import Layout from '@/layout/index.vue'
2 |
3 | const BASE_URL = '/case'
4 |
5 | export default [
6 | {
7 | path: BASE_URL,
8 | component: Layout,
9 | name: 'Case',
10 | meta: {
11 | title: '案例',
12 | isGroup: true,
13 | icon: 'i-carbon-location-heart'
14 | },
15 | children: [
16 | {
17 | path: `${BASE_URL}/iso-no`,
18 | name: 'IsoNo',
19 | component: () => import('@/pages/case/tab/index.vue'),
20 | meta: {
21 | title: '非同源Tab',
22 | icon: 'i-carbon-data-check'
23 | }
24 | }
25 | ]
26 | }
27 | ]
28 |
--------------------------------------------------------------------------------
/src/components/TextEllipsis/index.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
18 | {{ text }}
19 |
20 |
21 |
22 |
31 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import Scrollbar from 'smooth-scrollbar'
2 | import config from '@/config/index.js'
3 | import { isFunction } from 'lodash-es'
4 |
5 | export const scrollToByEl = (options) => {
6 | const { x = 0, y = 0, el } = options
7 |
8 | if (!el) {
9 | throw new Error('scroll is by el, el is not defined!')
10 | }
11 | const scrollDom = document.querySelector(el)
12 |
13 | if (config.useCustomScrollBar) {
14 | Scrollbar.get(scrollDom)?.scrollTo(x, y)
15 | } else {
16 | scrollDom?.scrollTo(x, y)
17 | }
18 | }
19 |
20 | export const isVueComponent = (obj) => {
21 | return isFunction(obj.render) || isFunction(obj.setup)
22 | }
23 |
--------------------------------------------------------------------------------
/src/layout/sider/components/util.js:
--------------------------------------------------------------------------------
1 | export const getTabMenu = (routes) => {
2 | const tabMenu = []
3 | routes.forEach((it) => {
4 | const { isGroup, hideInMenu } = it.meta || {}
5 | if (isGroup && !hideInMenu) {
6 | tabMenu.push(it)
7 | }
8 | })
9 |
10 | return tabMenu
11 | }
12 |
13 | export const getRedirectPath = (route) => {
14 | if (!route) return ''
15 |
16 | let path = route.path
17 | const getPath = (child) => {
18 | if (child[0].children && child[0].children.length) {
19 | getPath(child[0].children)
20 | } else {
21 | path = child[0].path
22 | }
23 | }
24 | getPath([route])
25 |
26 | return path
27 | }
28 |
--------------------------------------------------------------------------------
/src/hooks/util.js:
--------------------------------------------------------------------------------
1 | export const inBrowser = typeof window !== 'undefined'
2 |
3 | // Keep forward compatible
4 | // should be removed in next major version
5 | export const supportsPassive = true
6 |
7 | /**
8 | * @param fn FrameRequestCallback
9 | * @returns { number }
10 | */
11 | export function raf(fn) {
12 | return inBrowser ? requestAnimationFrame(fn) : -1
13 | }
14 |
15 | /**
16 | * @param id { number }
17 | */
18 | export function cancelRaf(id) {
19 | if (inBrowser) {
20 | cancelAnimationFrame(id)
21 | }
22 | }
23 |
24 | /**
25 | * double raf for animation
26 | * @param fn { FrameRequestCallback }
27 | */
28 | export function doubleRaf(fn) {
29 | raf(() => raf(fn))
30 | }
31 |
--------------------------------------------------------------------------------
/src/pages/com/radius-tabs/index.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
11 |
15 | Content of Tab Pane 1
16 |
17 |
21 | Content of Tab Pane 2
22 |
23 |
27 | Content of Tab Pane 3
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/components/FullScreen/index.vue:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 | {{ isFullscreen ? '退出全屏' : '全屏' }}
12 |
13 |
14 |
19 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/store/setting.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 | import { getLock, setLock } from '@/utils/storage.js'
3 |
4 | const useSettingStore = defineStore('setting', {
5 | state: () => ({
6 | collapsed: false,
7 | openKeys: [],
8 | selectedKeys: [],
9 |
10 | lockScreen: getLock()
11 | }),
12 | actions: {
13 | toggleCollapsed() {
14 | this.collapsed = !this.collapsed
15 | },
16 |
17 | changeMenuHighlight(openKeys, selectedKeys) {
18 | this.openKeys = openKeys
19 | this.selectedKeys = selectedKeys
20 | },
21 |
22 | toggleLock() {
23 | this.lockScreen = !this.lockScreen
24 | setLock(this.lockScreen)
25 | }
26 | }
27 | })
28 |
29 | export default useSettingStore
30 |
--------------------------------------------------------------------------------
/src/vendor/zip.js:
--------------------------------------------------------------------------------
1 | import { saveAs } from 'file-saver'
2 | import JSZip from 'jszip'
3 |
4 | export function export_txt_to_zip(th, jsonData, txtName, zipName) {
5 | const zip = new JSZip()
6 | const txt_name = txtName || 'file'
7 | const zip_name = zipName || 'file'
8 | const data = jsonData
9 | let txtData = `${th}\r\n`
10 | data.forEach((row) => {
11 | let tempStr = ''
12 | tempStr = row.toString()
13 | txtData += `${tempStr}\r\n`
14 | })
15 | zip.file(`${txt_name}.txt`, txtData)
16 | zip
17 | .generateAsync({
18 | type: 'blob'
19 | })
20 | .then(
21 | (blob) => {
22 | saveAs(blob, `${zip_name}.zip`)
23 | },
24 | (err) => {
25 | alert('导出失败')
26 | }
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/src/styles/variables.less:
--------------------------------------------------------------------------------
1 | @primary-color: #2546f0;
2 |
3 | @header-height: 60px;
4 |
5 | @menu-width: 184px;
6 |
7 | @box-shadow:
8 | 0 1px 2px 0 rgba(0, 0, 0, 0.03),
9 | 0 1px 6px -1px rgba(0, 0, 0, 0.02),
10 | 0 2px 4px 0 rgba(0, 0, 0, 0.02);
11 |
12 | @scroll-content-min-width: 600px;
13 |
14 | @main-bg: rgb(242, 243, 245);
15 |
16 | // ==============breakpoint============
17 |
18 | // Extra small screen / phone
19 | @screen-xs: 480px;
20 |
21 | // Small screen / tablet
22 | @screen-sm: 576px;
23 |
24 | // Medium screen / desktop
25 | @screen-md: 768px;
26 |
27 | // Large screen / wide desktop
28 | @screen-lg: 992px;
29 |
30 | // Extra large screen / full hd
31 | @screen-xl: 1200px;
32 |
33 | // Extra extra large screen / large desktop
34 | @screen-xxl: 1600px;
35 |
--------------------------------------------------------------------------------
/src/pages/case/tab/index.vue:
--------------------------------------------------------------------------------
1 |
19 |
20 |
21 |
22 |
23 | 1、场景:公司不同项目下a项目的商品详情,同样适用于b项目,不想二次开发。所以b项目引入了a商品详情。
24 | a项目下的商品详情有改动后希望通知b。b项目接到通知可刷新列表。b项目下新建路由iframe套入a项目的商品详情地址。a,b在iframe中可以使用postMessage通信。然后b在做bus通知其他具体的页面刷新
25 |
26 |
27 |
31 |
32 |
--------------------------------------------------------------------------------
/src/components/ModalFooter/index.vue:
--------------------------------------------------------------------------------
1 |
21 |
22 |
23 |
24 |
28 | {{ cancelText }}
29 |
30 |
35 | {{ okText }}
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/utils/print/index.js:
--------------------------------------------------------------------------------
1 | import { hiprint, disAutoConnect } from 'vue-plugin-hiprint'
2 | import tem1 from '@/utils/print/template/tem1.js'
3 |
4 | disAutoConnect()
5 |
6 | const templates = {
7 | tem1
8 | }
9 |
10 | class Print {
11 | constructor(template) {
12 | this.hiprintTemplate = new hiprint.PrintTemplate({
13 | template: templates[template]
14 | })
15 | }
16 |
17 | toPdf(printData, name = 'pdf') {
18 | this.hiprintTemplate.toPdf(printData, name)
19 | }
20 |
21 | print(printData) {
22 | this.hiprintTemplate.print(
23 | printData,
24 | {},
25 | {
26 | callback: () => {
27 | console.log('浏览器打印窗口已打开')
28 | }
29 | }
30 | )
31 | }
32 |
33 | getPreviewHtml(printData) {
34 | return this.hiprintTemplate.getHtml(printData)[0].outerHTML
35 | }
36 | }
37 |
38 | export default Print
39 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
9 |
13 |
17 |
22 |
28 | Vite + Vue
29 |
30 |
31 |
32 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/router/group/user.js:
--------------------------------------------------------------------------------
1 | import Layout from '@/layout/index.vue'
2 |
3 | const BASE_URL = '/user'
4 |
5 | export default [
6 | {
7 | path: BASE_URL,
8 | component: Layout,
9 | name: 'User',
10 | meta: {
11 | title: '用户',
12 | isGroup: true,
13 | icon: 'i-carbon-user-multiple'
14 | },
15 | children: [
16 | {
17 | path: `${BASE_URL}/list`,
18 | name: 'User_List',
19 | component: () => import('@/pages/user/list/index.vue'),
20 | meta: {
21 | title: '用户列表',
22 | icon: 'i-carbon-user-profile'
23 | }
24 | },
25 | {
26 | path: `${BASE_URL}/role`,
27 | name: 'User_Role',
28 | component: () => import('@/pages/user/role/index.vue'),
29 | meta: {
30 | title: '角色列表',
31 | icon: 'i-carbon-user-role'
32 | }
33 | }
34 | ]
35 | }
36 | ]
37 |
--------------------------------------------------------------------------------
/src/router/group/list.js:
--------------------------------------------------------------------------------
1 | import Layout from '@/layout/index.vue'
2 |
3 | const BASE_URL = '/list'
4 |
5 | export default [
6 | {
7 | path: BASE_URL,
8 | component: Layout,
9 | name: 'List',
10 | meta: {
11 | title: '列表',
12 | isGroup: true,
13 | icon: 'i-carbon-ibm-cloud-transit-gateway'
14 | },
15 | children: [
16 | {
17 | path: `${BASE_URL}/search`,
18 | name: 'List-Search',
19 | component: () => import('@/pages/list/search-list/index.vue'),
20 | meta: {
21 | title: '查询表格',
22 | icon: 'i-carbon-list'
23 | }
24 | },
25 | {
26 | path: `${BASE_URL}/card`,
27 | name: 'List-Card',
28 | component: () => import('@/pages/list/card-list/index.vue'),
29 | meta: {
30 | title: '卡片表格',
31 | icon: 'i-carbon-list-boxes'
32 | }
33 | }
34 | ]
35 | }
36 | ]
37 |
--------------------------------------------------------------------------------
/src/router/group/setting.js:
--------------------------------------------------------------------------------
1 | import Layout from '@/layout/index.vue'
2 |
3 | const BASE_URL = '/setting'
4 |
5 | export default [
6 | {
7 | path: BASE_URL,
8 | component: Layout,
9 | name: 'Setting',
10 | meta: {
11 | title: '设置',
12 | isGroup: true,
13 | icon: 'i-carbon-settings'
14 | },
15 | children: [
16 | {
17 | path: `${BASE_URL}/menu`,
18 | name: 'Setting_Menu',
19 | component: () => import('@/pages/setting/menu-enter/index.vue'),
20 | meta: {
21 | title: '菜单列表',
22 | icon: 'i-carbon-list'
23 | }
24 | },
25 | {
26 | path: `${BASE_URL}/profile`,
27 | name: 'Setting_Profile',
28 | component: () => import('@/pages/setting/profile/index.vue'),
29 | meta: {
30 | title: '个人中心',
31 | icon: 'i-carbon-user-data'
32 | }
33 | }
34 | ]
35 | }
36 | ]
37 |
--------------------------------------------------------------------------------
/src/store/user-info.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 | import useDynamicRouterStore from '@/store/dynamic-router.js'
3 | import { removeRouteListener } from '@/utils/router-listener.js'
4 | import { getUserInfo, removeToken, removeUserInfo, setToken, setUserInfo } from '@/utils/storage.js'
5 |
6 | const useUserInfoStore = defineStore('userInfo', {
7 | state: () => ({
8 | userInfo: getUserInfo()
9 | }),
10 | actions: {
11 | login() {
12 | const us = { name: 'Ealien', age: 18 }
13 |
14 | this.userInfo = us
15 | setUserInfo(us)
16 | setToken('asdasd')
17 | },
18 | logout() {
19 | const dynamicRouter = useDynamicRouterStore()
20 |
21 | removeToken()
22 | this.userInfo = {}
23 | removeUserInfo()
24 | removeRouteListener()
25 | dynamicRouter.syncRoutes = false
26 | dynamicRouter.roleRoutes = []
27 | }
28 | }
29 | })
30 |
31 | export default useUserInfoStore
32 |
--------------------------------------------------------------------------------
/src/hooks/useModal/index.js:
--------------------------------------------------------------------------------
1 | import { h } from 'vue'
2 | import { Modal, message } from 'ant-design-vue'
3 | import { isVueComponent } from '@/utils/index.js'
4 |
5 | export const useModal = (modalProps) => {
6 | const open = (component, props) => {
7 | if (!isVueComponent(component)) {
8 | message.warn('这里强制是VueComponent,否则你不应该使用它!')
9 | }
10 |
11 | const modal = Modal.confirm({
12 | title: 'useModal 请设置title覆盖',
13 | content: h(
14 | component,
15 | {
16 | ...props,
17 | ok: (val) => {
18 | props.ok && props.ok(val)
19 | modal.destroy()
20 | },
21 | cancel: () => {
22 | modal.destroy()
23 | }
24 | },
25 | () => null
26 | ),
27 | icon: null,
28 | closable: true,
29 | ...modalProps,
30 | footer: null,
31 | wrapClassName: 'use-modal'
32 | })
33 | }
34 |
35 | return {
36 | open
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/layout/index.vue:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
21 |
22 |
23 |
24 |
44 |
--------------------------------------------------------------------------------
/src/lib/echarts.js:
--------------------------------------------------------------------------------
1 | import * as echarts from 'echarts/core'
2 | import {
3 | DatasetComponent,
4 | TooltipComponent,
5 | GridComponent,
6 | LegendComponent,
7 | TitleComponent,
8 | GraphicComponent,
9 | ToolboxComponent
10 | } from 'echarts/components'
11 | import {
12 | BarChart,
13 | LineChart,
14 | PieChart,
15 | GaugeChart,
16 | CustomChart,
17 | GraphChart,
18 | LinesChart,
19 | PictorialBarChart
20 | } from 'echarts/charts'
21 | import { CanvasRenderer } from 'echarts/renderers'
22 | import walden from './theme/walden.project.json'
23 |
24 | const components = [
25 | ToolboxComponent,
26 | DatasetComponent,
27 | TooltipComponent,
28 | GridComponent,
29 | LegendComponent,
30 | TitleComponent,
31 | GraphicComponent
32 | ]
33 |
34 | const charts = [BarChart, LineChart, PieChart, GaugeChart, CustomChart, GraphChart, LinesChart, PictorialBarChart]
35 |
36 | echarts.use([...components, ...charts, CanvasRenderer])
37 |
38 | echarts.registerTheme('walden', walden.theme)
39 |
40 | export default echarts
41 |
--------------------------------------------------------------------------------
/src/hooks/useModal/useModal.md:
--------------------------------------------------------------------------------
1 | ### 示例
2 |
3 | ```js
4 | const modal = useModal({ title: '你好', width: '800px' })
5 | const handleFormModal = () => {
6 | modal.open(FormMenu, {
7 | id: 1,
8 | limit: 10,
9 | /// 对应FormMenu组件的ok,函数名称固定
10 | ok: (val) => {
11 | console.log('=>(List.vue:61) val', val)
12 | console.log('操作了ok')
13 | }
14 | })
15 | }
16 | ```
17 |
18 | ### MenuFormModal 组件props(弹窗组件 + 弹窗组件按钮)
19 |
20 | ```js
21 | const props = defineProps({
22 | id: {
23 | type: Number
24 | },
25 | limit: {
26 | type: Number
27 | },
28 | ok: {
29 | type: Function
30 | },
31 | cancel: {
32 | type: Function
33 | }
34 | })
35 |
36 | const ok = (val) => {
37 | // 校验等。。。
38 |
39 | // 这里固定格式
40 | props.ok && props.ok(val)
41 | }
42 |
43 | const handleCancel = () => {
44 | // 取消函数内部固定格式,暂时无法优化到不用写取消函数
45 | props.cancel && props.cancel()
46 | }
47 | ```
48 |
49 | ### 弹窗组件底部按钮
50 |
51 | ```vue
52 | props.cancel && props.cancel()" />
53 | ```
54 |
--------------------------------------------------------------------------------
/src/config/icons.js:
--------------------------------------------------------------------------------
1 | export default [
2 | 'i-carbon-home',
3 | 'i-carbon-list-boxes',
4 | 'i-carbon-ibm-cloud-transit-gateway',
5 | 'i-carbon-list',
6 | 'i-carbon-location-heart',
7 | 'i-carbon-cloud-monitoring',
8 | 'i-carbon-data-check',
9 | 'i-carbon-ibm-content-services',
10 | 'i-carbon-encryption',
11 | 'i-carbon-chart-bar',
12 | 'i-carbon-chart-line',
13 | 'i-carbon-chart-combo',
14 | 'i-carbon-chart-pie',
15 | 'i-carbon-printer',
16 | 'i-carbon-decision-tree',
17 | 'i-carbon-calendar',
18 | 'i-carbon-id-management',
19 | 'i-carbon-contour-draw',
20 | 'i-carbon-event-schedule',
21 | 'i-carbon-time',
22 | 'i-carbon-model-alt',
23 | 'i-carbon-settings',
24 | 'i-carbon-user-multiple',
25 | 'i-carbon-user-role',
26 | 'i-carbon-user-profile',
27 | 'i-carbon-list',
28 | 'i-carbon-ibm-cloud-key-protect',
29 | 'i-carbon-user-data',
30 | 'i-carbon-ibm-toolchain',
31 | 'i-carbon-dot-mark',
32 | 'i-carbon-text-footnote',
33 | 'i-carbon-image',
34 | 'i-carbon-table-alias',
35 | 'i-carbon-zip',
36 | 'i-carbon-document'
37 | ]
38 |
--------------------------------------------------------------------------------
/src/pages/list/card-list/index.vue:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
25 |
30 |
31 |
32 |
33 |
34 | {{ photo.title }}
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/utils/table.js:
--------------------------------------------------------------------------------
1 | /**
2 | * const columns = setTableColumn([
3 | * { no: '集合编号' },
4 | * { name: '集合名称' },
5 | * { type: '内容体裁' },
6 | * { fs: '筛选方式' },
7 | * { action: { title: '操作', width: '130px', ... } }
8 | * ])
9 | * @param simpleArr
10 | */
11 | export const setTableColumn = (simpleArr = []) => {
12 | if (!simpleArr || !Array.isArray(simpleArr) || !simpleArr?.length) {
13 | return []
14 | }
15 |
16 | return simpleArr.map((it) => {
17 | const key = Object.keys(it)[0]
18 | const val = it[key]
19 |
20 | return typeof val === 'string'
21 | ? {
22 | title: val,
23 | dataIndex: key,
24 | key
25 | }
26 | : {
27 | dataIndex: key,
28 | key,
29 | ...val
30 | }
31 | })
32 | }
33 |
34 | export const getScrollHeight = (className, extraBottom) => {
35 | try {
36 | const rect = document.querySelector(className).getBoundingClientRect()
37 | return window.innerHeight - rect.top - extraBottom
38 | } catch {
39 | console.log('滚动区域高度计算失败')
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/pages/com/auth/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 常显
5 |
9 | 可见1
10 |
11 |
12 |
16 | 不可见
17 |
18 |
19 |
23 | 可见2
24 |
25 |
26 |
30 | 可见3
31 |
32 |
33 |
37 | 不可见
38 |
39 |
40 |
44 | 可见4
45 |
46 |
47 | 错误示例
48 |
49 |
--------------------------------------------------------------------------------
/src/pages/plugin/org_tree/index.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
33 |
37 |
38 |
42 | https://sangtian152.github.io/vue3-tree-org/guide/
43 |
44 |
45 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import path from 'path'
3 | import vue from '@vitejs/plugin-vue'
4 | import { envParse } from 'vite-plugin-env-parse'
5 | import UnoCSS from 'unocss/vite'
6 | import Components from 'unplugin-vue-components/vite'
7 | import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
8 |
9 | // https://vitejs.dev/config/
10 | export default defineConfig({
11 | resolve: {
12 | alias: {
13 | '@': path.resolve(__dirname, 'src')
14 | }
15 | },
16 | plugins: [
17 | vue(),
18 | envParse(),
19 | UnoCSS(),
20 | Components({
21 | resolvers: [
22 | AntDesignVueResolver({
23 | importStyle: false // css in js
24 | })
25 | ]
26 | })
27 | ],
28 | server: {
29 | warmup: {
30 | clientFiles: ['./*.html', './src/global.component.js']
31 | }
32 | },
33 | css: {
34 | preprocessorOptions: {
35 | less: {
36 | modifyVars: {
37 | hack: `true; @import (reference) "${path.resolve('src/styles/variables.less')}";`
38 | },
39 | javascriptEnabled: true
40 | }
41 | }
42 | }
43 | })
44 |
--------------------------------------------------------------------------------
/src/pages/setting/profile/index.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
30 |
34 |
39 |
43 | 确认设置
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/router/group/exception.js:
--------------------------------------------------------------------------------
1 | import Layout from '@/layout/index.vue'
2 |
3 | const BASE_URL = '/exception'
4 |
5 | export default [
6 | {
7 | path: BASE_URL,
8 | component: Layout,
9 | name: 'Exception',
10 | redirect: `${BASE_URL}/404-page`,
11 | meta: {
12 | title: '异常页',
13 | isGroup: true,
14 | icon: 'i-carbon-ibm-cloud-transit-gateway'
15 | },
16 | children: [
17 | {
18 | path: `${BASE_URL}/404-page`,
19 | name: '404-PAGE',
20 | component: () => import('@/pages/exception/404-page.vue'),
21 | meta: {
22 | title: '404',
23 | icon: 'i-carbon-list-boxes'
24 | }
25 | },
26 | {
27 | path: `${BASE_URL}/401-page`,
28 | name: '401-PAGE',
29 | component: () => import('@/pages/exception/401-page.vue'),
30 | meta: {
31 | title: '401',
32 | icon: 'i-carbon-list-boxes'
33 | }
34 | },
35 | {
36 | path: `${BASE_URL}/500-page`,
37 | name: '500-PAGE',
38 | component: () => import('@/pages/exception/500-page.vue'),
39 | meta: {
40 | title: '50x',
41 | icon: 'i-carbon-list-boxes'
42 | }
43 | }
44 | ]
45 | }
46 | ]
47 |
--------------------------------------------------------------------------------
/src/utils/storage.js:
--------------------------------------------------------------------------------
1 | // 屏幕锁
2 | const LOCK_KEY = 'lock_screen'
3 |
4 | export const setLock = (isLock) => {
5 | localStorage.setItem(LOCK_KEY, isLock)
6 | }
7 |
8 | export const getLock = () => {
9 | return JSON.parse(localStorage.getItem(LOCK_KEY) || 'false')
10 | }
11 |
12 | // 锁屏密码
13 | const LOCK_PAS_KEY = 'lock_pas'
14 |
15 | export const setLockPas = (pas = '123456') => {
16 | return localStorage.setItem(LOCK_PAS_KEY, pas)
17 | }
18 |
19 | export const getLockPas = () => {
20 | return localStorage.getItem(LOCK_PAS_KEY)
21 | }
22 |
23 | // 用户TOKEN
24 | const TOKEN_KEY = 'tk'
25 |
26 | export const setToken = (token) => {
27 | localStorage.setItem(TOKEN_KEY, token)
28 | }
29 |
30 | export const getToken = () => {
31 | return localStorage.getItem(TOKEN_KEY)
32 | }
33 |
34 | export const removeToken = () => {
35 | localStorage.removeItem(TOKEN_KEY)
36 | }
37 |
38 | // 用户信息
39 | const USER_INFO_KEY = 'usi'
40 |
41 | export const setUserInfo = (userInfo) => {
42 | localStorage.setItem(USER_INFO_KEY, JSON.stringify(userInfo || {}))
43 | }
44 |
45 | export const getUserInfo = () => {
46 | return JSON.parse(localStorage.getItem(USER_INFO_KEY))
47 | }
48 |
49 | export const removeUserInfo = () => {
50 | localStorage.removeItem(USER_INFO_KEY)
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/TextMark/index.vue:
--------------------------------------------------------------------------------
1 |
40 |
41 |
42 |
43 |
44 |
45 |
50 |
--------------------------------------------------------------------------------
/src/hooks/useFetch/demo.md:
--------------------------------------------------------------------------------
1 | ## 可以不使用await, 这样不会阻塞页面
2 |
3 | ## useFetch 没有对get请求参数做很好的处理(query参数只能手动拼接到url中),所以二次封装了
4 |
5 | ### 原来get请求url拼接示例
6 | ```js
7 | useFetch('https://www.baidu.com/asd?name=1&age=18').json()
8 | ```
9 |
10 | ### 封装后的get请求参数处理
11 | ### 示例1 响应式查询参数
12 | ```js
13 | const inputVal = ref({ id: '1', name: null, age: undefined, as: '' })
14 | const { data, isFetching, execute } = await useFetch(API_POSTS.photos, {
15 | params: inputVal
16 | }).json()
17 | ```
18 |
19 |
20 | ### 示例2 响应式查询参数
21 | ```js
22 | const state = reactive({
23 | page_index: 1,
24 | page_size: 2,
25 | id: 1
26 | })
27 |
28 | const { data, isFetching, execute } = await useFetch(API_POSTS.photos, {
29 | params: state
30 | }).json()
31 | ```
32 |
33 | ### 示例3 响应式payload, 使用reactive定义参数;继续执行execute数据会是最新的
34 | ```js
35 | const state = reactive({
36 | page_index: 1,
37 | page_size: 2,
38 | id: 1
39 | })
40 |
41 | const { data, isFetching, execute } = await useFetch(API_POSTS.photos).post(state).json()
42 | ```
43 |
44 | ### 示例4 payload只初始化一次
45 | ```js
46 | const state = ref({
47 | page_index: 1,
48 | page_size: 2,
49 | id: 1
50 | })
51 |
52 | const { data, isFetching, execute } = await useFetch(API_POSTS.photos).post({ ...state.value }).json()
53 | ```
54 |
--------------------------------------------------------------------------------
/src/pages/plugin/lodash/index.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/pages/print/index.vue:
--------------------------------------------------------------------------------
1 |
22 |
23 |
24 |
25 |
29 | 打印
30 |
31 |
32 |
36 | 下载pdf
37 |
38 |
39 |
43 | 预览
44 |
45 |
46 |
47 |
52 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/src/components/RadiusTabs/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
56 |
--------------------------------------------------------------------------------
/src/pages/charts/g2-column/index.vue:
--------------------------------------------------------------------------------
1 |
45 |
46 |
47 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/directive/scrollbar.js:
--------------------------------------------------------------------------------
1 | import Scrollbar from 'smooth-scrollbar'
2 | import config from '@/config/index.js'
3 |
4 | const extractProp = (prop) => (obj) => (typeof obj === 'undefined' ? undefined : obj[prop])
5 | const extractOptions = extractProp('options')
6 | const extractEl = extractProp('el')
7 |
8 | const bestMatch = (extractor) => (possibilities) =>
9 | extractor(possibilities.find((p) => typeof extractor(p) !== 'undefined'))
10 | const bestEl = bestMatch(extractEl)
11 | const bestOptions = bestMatch(extractOptions)
12 |
13 | /**
14 | v-scrollbar
15 | v-scrollbar="{ el: "" }"
16 | */
17 | export default {
18 | mounted(el, binding) {
19 | if (config.useCustomScrollBar) {
20 | const possibilities = [binding.value]
21 | const targetEl = bestEl(possibilities)
22 | const config = bestOptions(possibilities)
23 |
24 | const scrollY = binding.modifiers.y
25 | const scrollX = binding.modifiers.x
26 | Scrollbar.init(targetEl ? document.querySelector(targetEl) : el)
27 | }
28 | },
29 |
30 | updated(el, binding, vnode, prevVnode) {},
31 |
32 | unmounted(el, binding) {
33 | if (config.useCustomScrollBar) {
34 | const possibilities = [binding.value]
35 | const targetEl = bestEl(possibilities)
36 | Scrollbar.destroy(targetEl ? document.querySelector(targetEl) : el, {})
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/pages/plugin/sign/index.vue:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
36 |
37 |
41 | 清空画布
42 |
43 |
47 | 生成图片
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/src/layout/sider/components/Menu.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
25 |
35 |
36 |
37 |
38 |
55 |
--------------------------------------------------------------------------------
/src/router/group/chart.js:
--------------------------------------------------------------------------------
1 | import Layout from '@/layout/index.vue'
2 |
3 | const BASE_URL = '/chart'
4 |
5 | export default [
6 | {
7 | path: BASE_URL,
8 | component: Layout,
9 | name: 'Chart',
10 | meta: {
11 | title: '图表',
12 | isGroup: true,
13 | icon: 'i-carbon-chart-combo'
14 | },
15 | children: [
16 | {
17 | path: `${BASE_URL}/bar`,
18 | name: 'Chart_Bar',
19 | component: () => import('@/pages/charts/bar/index.vue'),
20 | meta: {
21 | title: '柱状图',
22 | icon: 'i-carbon-chart-bar'
23 | }
24 | },
25 | {
26 | path: `${BASE_URL}/line`,
27 | name: 'Chart_Line',
28 | component: () => import('@/pages/charts/line/index.vue'),
29 | meta: {
30 | title: '折线图',
31 | icon: 'i-carbon-chart-line'
32 | }
33 | },
34 | {
35 | path: `${BASE_URL}/pie`,
36 | name: 'Chart_Pie',
37 | component: () => import('@/pages/charts/pie/index.vue'),
38 | meta: {
39 | title: '饼图',
40 | icon: 'i-carbon-chart-pie'
41 | }
42 | },
43 | {
44 | path: `${BASE_URL}/column`,
45 | name: 'Chart2_Pie',
46 | component: () => import('@/pages/charts/g2-column/index.vue'),
47 | meta: {
48 | title: 'G2-堆叠',
49 | icon: 'i-carbon-chart-pie'
50 | }
51 | }
52 | ]
53 | }
54 | ]
55 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/pages/com/mark-keyword/index.vue:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
10 |
16 |
17 |
18 |
29 |
--------------------------------------------------------------------------------
/src/layout/header/components/Avatar.vue:
--------------------------------------------------------------------------------
1 |
20 |
21 |
43 |
44 |
45 |
51 |
--------------------------------------------------------------------------------
/src/directive/loading.js:
--------------------------------------------------------------------------------
1 | const loading = ``
2 |
3 | /**
4 | * 通过自定义样式(global.less),对 primary 类型按钮,和官方示例一样。事件只执行一次
5 | *
6 | * 默认值1500毫秒
7 | * v-loading="2000"
8 | * v-loading === v-loading="1500"
9 | */
10 | export default {
11 | mounted(el, binding) {
12 | const originInnerHtml = el.innerHTML
13 |
14 | if (binding.value && typeof binding.value !== 'number') {
15 | console.error('自定义时间应为数字 例: v-loading="2000"')
16 | return
17 | }
18 |
19 | el.addEventListener(
20 | 'click',
21 | () => {
22 | if (!el.disabled) {
23 | el.disabled = true
24 | el.innerHTML = `${loading}${originInnerHtml}`
25 |
26 | setTimeout(() => {
27 | el.innerHTML = originInnerHtml
28 | el.disabled = false
29 | }, binding.value || 1500)
30 | }
31 | },
32 | false
33 | )
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/mock/data.js:
--------------------------------------------------------------------------------
1 | export const typeEnum = {
2 | '图文': 'https://p3-armor.byteimg.com/tos-cn-i-49unhts6dw/581b17753093199839f2e327e726b157.svg~tplv-49unhts6dw-image.image',
3 | '横版短视频':
4 | 'https://p3-armor.byteimg.com/tos-cn-i-49unhts6dw/77721e365eb2ab786c889682cbc721c1.svg~tplv-49unhts6dw-image.image',
5 | '竖版短视频':
6 | 'https://p3-armor.byteimg.com/tos-cn-i-49unhts6dw/ea8b09190046da0ea7e070d83c5d1731.svg~tplv-49unhts6dw-image.image'
7 | }
8 |
9 | /// 通过name匹配的, 其他信息可传入后覆盖, 更改为Apifox云端mock获取
10 | export const adminRoutes = [
11 | {
12 | id: 1,
13 | name: 'Chart',
14 | path: '/chart',
15 | title: '图标',
16 | status: 1,
17 | children: [
18 | {
19 | id: 2,
20 | title: '柱状图',
21 | path: '/chart/bar',
22 | name: 'Chart_Bar',
23 | status: 1
24 | },
25 | {
26 | id: 3,
27 | title: '折线图',
28 | path: '/chart/line',
29 | name: 'Chart_Line',
30 | status: 0
31 | },
32 | {
33 | id: 4,
34 | title: '饼图',
35 | path: '/chart/pie',
36 | name: 'Chart2_Pie',
37 | status: 1
38 | }
39 | ]
40 | },
41 | {
42 | id: 5,
43 | name: 'Print',
44 | path: '/print',
45 | title: '打印',
46 | status: 1,
47 | children: [
48 | {
49 | id: 6,
50 | status: 1,
51 | title: '打印列表',
52 | name: 'Print-List',
53 | path: '/print/list'
54 | }
55 | ]
56 | }
57 | ]
58 |
--------------------------------------------------------------------------------
/src/router/routes.js:
--------------------------------------------------------------------------------
1 | import Layout from '@/layout/index.vue'
2 | import HomeGroup from './group/home.js'
3 | import ComGroup from './group/com.js'
4 | import PluginGroup from './group/plugin.js'
5 | import CaseGroup from './group/case.js'
6 | import ListGroup from './group/list.js'
7 | import ExceptionGroup from './group/exception.js'
8 | import ChartGroup from './group/chart.js'
9 | import PrintGroup from './group/print.js'
10 | import UserGroup from './group/user.js'
11 | import SettingGroup from './group/setting.js'
12 |
13 | /**
14 | * meta: { icon, hideInMenu, title }
15 | *
16 | * TIP path必须写完整的路径,要做跳转匹配, path必填项-要匹配路由
17 | */
18 | export const BASE_ROUTE = [
19 | {
20 | path: '/login',
21 | name: 'Login',
22 | component: () => import('@/pages/login/index.vue'),
23 | meta: {
24 | title: '登录'
25 | }
26 | },
27 | ...HomeGroup,
28 | ...ExceptionGroup,
29 | {
30 | path: '/:path(.*)*',
31 | name: '404-page',
32 | component: Layout,
33 | meta: {
34 | title: '404页面找不到'
35 | },
36 | children: [
37 | {
38 | path: '/:path(.*)*',
39 | name: '404-child-page',
40 | component: () => import('@/pages/exception/404-page.vue'),
41 | meta: {
42 | title: '404页面找不到'
43 | }
44 | }
45 | ]
46 | }
47 | ]
48 |
49 | export const DYNAMIC_ROUTE = [
50 | ...ComGroup,
51 | ...PluginGroup,
52 | ...CaseGroup,
53 | ...ChartGroup,
54 | ...UserGroup,
55 | ...ListGroup,
56 | ...PrintGroup,
57 | ...SettingGroup
58 | ]
59 |
--------------------------------------------------------------------------------
/src/store/side-menu.js:
--------------------------------------------------------------------------------
1 | import { h } from 'vue'
2 | import { defineStore } from 'pinia'
3 | import config from '@/config/index.js'
4 |
5 | const generator = (routerMap) => {
6 | return routerMap.map((item) => {
7 | const { title, hideInMenu, icon } = item.meta || {}
8 | const currentRouter = {
9 | label: title,
10 | key: item.path,
11 | icon: icon ? h('i', { class: icon }) : null
12 | // router警告组件是响应式时可使用 shallowRef 包裹
13 | // https://cn.vuejs.org/api/reactivity-advanced.html#shallowref
14 | }
15 |
16 | item.redirect && (currentRouter.redirect = item.redirect)
17 |
18 | if (item.children && item.children.length > 0) {
19 | currentRouter.children = generator(item.children)
20 | }
21 | return hideInMenu ? null : currentRouter
22 | })
23 | }
24 |
25 | const emptyMenu = [{ label: '', key: '' }]
26 |
27 | const useSideMenuStore = defineStore('sideMenu', {
28 | state: () => ({ menus: emptyMenu, menuMap: new Map() }),
29 | getters: {
30 | onlyMenu() {
31 | return config.onlyMenu
32 | ? this.menus.length > 1 || (this.menus[0] && this.menus[0].children && this.menus[0].children.length)
33 | : true
34 | }
35 | },
36 | actions: {
37 | changeSide(side) {
38 | if(!side) return
39 |
40 | if (!this.menuMap.has(side.path)) {
41 | this.menuMap.set(side.path, generator(side.children) || [])
42 | }
43 |
44 | this.menus = this.menuMap.get(side.path)
45 | }
46 | }
47 | })
48 |
49 | export default useSideMenuStore
50 |
--------------------------------------------------------------------------------
/src/bootstrap.js:
--------------------------------------------------------------------------------
1 | import router from '@/router/index.js'
2 | import config from '@/config/index.js'
3 | import NProgress from 'nprogress'
4 | import 'nprogress/nprogress.css'
5 | import useDynamicRouterStore from '@/store/dynamic-router.js'
6 | import { scrollToByEl } from '@/utils/index.js'
7 | import { getToken, setLockPas, getLockPas } from '@/utils/storage.js'
8 | import { setRouteEmitter } from '@/utils/router-listener.js'
9 |
10 | const LOGIN_PATH = '/login'
11 |
12 | /// set default lock screen pas
13 | if (!getLockPas()) {
14 | setLockPas()
15 | }
16 |
17 | NProgress.configure({ showSpinner: false })
18 |
19 | router.beforeEach(async (to) => {
20 | setRouteEmitter(to)
21 |
22 | NProgress.start()
23 |
24 | document.title = to.meta.title || config.title
25 |
26 | const token = getToken()
27 | if (token) {
28 | if (to.path === LOGIN_PATH) {
29 | router.replace('/')
30 | } else {
31 | const dynamicRouter = useDynamicRouterStore()
32 | if (!dynamicRouter.syncRoutes) {
33 | await dynamicRouter.getUserRoutes()
34 |
35 | const resultRoute = await dynamicRouter.generator()
36 |
37 | resultRoute.forEach((route) => router.addRoute(route))
38 |
39 | dynamicRouter.syncRoutes = true
40 |
41 | // TIP: 如果想做登录后的redirect,需要检测路径是否存在(权限变更,路径不存在等问题)
42 | router.replace(to.fullPath)
43 | }
44 | }
45 | } else {
46 | to.path !== LOGIN_PATH && router.replace(`${LOGIN_PATH}?redirect=${to.fullPath}`)
47 | }
48 |
49 | scrollToByEl({ el: '.content_wrap' })
50 | NProgress.done()
51 | })
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-admin",
3 | "private": true,
4 | "version": "0.0.1",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite --mode dev",
8 | "build:dev": "vite build --mode dev",
9 | "build:test": "vite build --mode test",
10 | "build:prod": "vite build --mode prod",
11 | "preview": "vite preview"
12 | },
13 | "dependencies": {
14 | "@antv/g2": "^5.1.10",
15 | "@fingerprintjs/fingerprintjs": "^4.1.0",
16 | "@popperjs/core": "^2.11.8",
17 | "@vueuse/core": "^12.2.0",
18 | "ant-design-vue": "^4.2.6",
19 | "codemirror": "^5.65.16",
20 | "codemirror-editor-vue3": "^2.4.1",
21 | "dayjs": "^1.11.13",
22 | "echarts": "^5.4.3",
23 | "file-saver": "^2.0.5",
24 | "idcard": "^4.2.0",
25 | "jszip": "^3.10.1",
26 | "lodash-es": "^4.17.21",
27 | "mitt": "^3.0.1",
28 | "nprogress": "^0.2.0",
29 | "pinia": "^2.3.0",
30 | "smooth-scrollbar": "^8.8.4",
31 | "v-calendar": "^3.1.2",
32 | "vue": "^3.5.13",
33 | "vue-esign": "^1.1.4",
34 | "vue-json-pretty": "^2.4.0",
35 | "vue-plugin-hiprint": "^0.0.56",
36 | "vue-router": "^4.5.0",
37 | "vue3-tree-org": "^4.2.2",
38 | "xlsx": "^0.18.5"
39 | },
40 | "devDependencies": {
41 | "@iconify-json/carbon": "^1.2.5",
42 | "@unocss/preset-icons": "^0.65.2",
43 | "@vitejs/plugin-vue": "^5.2.1",
44 | "less": "^4.2.1",
45 | "prettier": "^3.4.2",
46 | "unocss": "^0.65.2",
47 | "unplugin-vue-components": "^0.28.0",
48 | "vite": "^6.0.5",
49 | "vite-plugin-env-parse": "^1.0.15"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/pages/plugin/zip/index.vue:
--------------------------------------------------------------------------------
1 |
43 |
44 |
45 |
46 |
52 | 导出ZIP
53 |
54 |
58 |
59 |
--------------------------------------------------------------------------------
/src/pages/com/text-ellipsis/index.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 | 一行溢出
14 |
15 |
19 |
20 |
21 | 二行溢出
22 |
23 |
27 |
28 |
29 | 三行溢出
30 |
31 |
35 |
36 |
37 | 英文测试
38 | 一行溢出
39 |
40 |
44 |
45 |
46 | 二行溢出
47 |
48 |
52 |
53 |
54 | 三行溢出
55 |
56 |
60 |
61 |
62 |
--------------------------------------------------------------------------------
/src/layout/header/index.vue:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
29 |
30 |
31 |
69 |
--------------------------------------------------------------------------------
/src/pages/setting/menu-enter/components/MenuFormModal.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
45 |
50 |
51 |
52 |
57 |
58 |
59 |
64 |
65 |
66 |
67 | props.cancel && props.cancel()"
70 | />
71 |
72 |
--------------------------------------------------------------------------------
/src/styles/global.less:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | height: 100%;
4 | width: 100%;
5 | margin: 0;
6 |
7 | // 防止左右滑动返回
8 | overscroll-behavior: none;
9 | }
10 |
11 | #app {
12 | width: 100%;
13 | height: 100%;
14 | overflow: hidden;
15 | }
16 |
17 | .header_title {
18 | font-size: 16px;
19 | font-weight: 500;
20 | color: rgb(29, 33, 41);
21 | margin-bottom: 16px;
22 | }
23 |
24 | // reset smooth-scrollbar
25 | .scrollbar-track {
26 | background: transparent !important;
27 | }
28 |
29 | .scrollbar-track-y,
30 | .scrollbar-thumb.scrollbar-thumb-y {
31 | width: 6px !important;
32 | }
33 |
34 | .scrollbar-track-x,
35 | .scrollbar-thumb.scrollbar-thumb-x {
36 | height: 6px !important;
37 | }
38 |
39 | //textarea:-webkit-autofill
40 | //select:-webkit-autofill
41 | input:-webkit-autofill {
42 | -webkit-box-shadow: 0 0 0 1000px transparent inset !important;
43 | background-color: transparent !important;
44 | background-image: none;
45 | transition: background-color 50000s ease-in-out 0s;
46 | -webkit-text-fill-color: black !important;
47 | }
48 |
49 | // 覆盖useModal
50 | .ant-modal-wrap.use-modal .ant-modal-confirm-content {
51 | max-width: 100% !important;
52 | }
53 |
54 | .ant-modal-wrap.use-modal .ant-modal-confirm-title {
55 | margin-bottom: 10px;
56 | }
57 |
58 | // 覆盖自定义 v-loading
59 | button:disabled.ant-btn-primary {
60 | color: #ffffff;
61 | background-color: #2546f0;
62 | box-shadow: 0 2px 0 rgba(5, 145, 255, 0.1);
63 | opacity: 0.65;
64 | }
65 |
66 | button:disabled.ant-btn-default {
67 | color: rgba(0, 0, 0, 0.88);
68 | background-color: #ffffff;
69 | box-shadow: 0 2px 0 rgba(0, 0, 0, 0.02);
70 | opacity: 0.65;
71 | }
72 |
73 | .center_spin {
74 | position: absolute;
75 | top: 40%;
76 | left: 48%;
77 | z-index: 1;
78 | }
79 |
--------------------------------------------------------------------------------
/src/pages/user/role/components/RoleFormModal.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
37 |
42 |
46 |
47 |
52 |
53 | 启用
54 | 禁用
55 |
56 |
57 |
62 |
70 |
71 |
72 |
76 |
77 |
--------------------------------------------------------------------------------
/src/store/dynamic-router.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from 'pinia'
2 | import useFetch from '@/hooks/useFetch/index.js'
3 | import config from '@/config/index.js'
4 | import { DYNAMIC_ROUTE } from '@/router/routes.js'
5 | import { API_USER } from '@/service/user/index.js'
6 |
7 | const HAS_KEY = 'path'
8 | const hasPermission = (flattenApiRoutes, route) => {
9 | if (route[HAS_KEY]) {
10 | return flattenApiRoutes.findIndex((systemRoute) => systemRoute[HAS_KEY] === route[HAS_KEY]) >= 0
11 | } else {
12 | return false
13 | }
14 | }
15 |
16 | const filterAsyncRoutes = (flattenApiRoutes, systemRoutes) => {
17 | const res = []
18 |
19 | systemRoutes.forEach((route) => {
20 | const tmp = { ...route }
21 | if (hasPermission(flattenApiRoutes, tmp)) {
22 | if (tmp.children) {
23 | tmp.children = filterAsyncRoutes(flattenApiRoutes, tmp.children)
24 | }
25 | res.push(tmp)
26 | }
27 | })
28 |
29 | return res
30 | }
31 |
32 | const flattenSystemRoutes = (routes) => {
33 | const res = []
34 |
35 | const filterRoutes = (routes) => {
36 | routes.forEach((item) => {
37 | if (item.children && item.children.length) {
38 | filterRoutes(item.children)
39 | item.children = []
40 | }
41 | res.push(item)
42 | })
43 | }
44 | filterRoutes(routes)
45 |
46 | return res
47 | }
48 |
49 | const useDynamicRouterStore = defineStore('dynamicRouter', {
50 | state: () => ({
51 | syncRoutes: false,
52 | roleRoutes: []
53 | }),
54 | actions: {
55 | async generator() {
56 | if (config.useDynamicRoute) {
57 | return filterAsyncRoutes(flattenSystemRoutes(this.roleRoutes), DYNAMIC_ROUTE)
58 | }
59 |
60 | return DYNAMIC_ROUTE
61 | },
62 |
63 | async getUserRoutes() {
64 | if (config.useDynamicRoute) {
65 | const { data } = await useFetch(API_USER.roleRoutes).json()
66 | this.roleRoutes = data.value.routes
67 | }
68 | }
69 | }
70 | })
71 |
72 | export default useDynamicRouterStore
73 |
--------------------------------------------------------------------------------
/src/hooks/useFetch/index.js:
--------------------------------------------------------------------------------
1 | import { unref } from 'vue'
2 | import { createFetch } from '@vueuse/core'
3 | import { message } from 'ant-design-vue'
4 |
5 | // useFetch 没有很方便的query传参
6 | export const montageUrl = (originUrl, query) => {
7 | const params = new URLSearchParams()
8 | for (let q in query) {
9 | const val = query[q]
10 |
11 | if ((val ?? '') !== '') {
12 | params.append(q, val)
13 | }
14 | }
15 |
16 | const connector = originUrl.includes('?') ? '&' : '?'
17 |
18 | return `${originUrl}${connector}${params.toString()}`
19 | }
20 |
21 | const codeMessage = {
22 | 200: '服务器成功返回请求的数据。',
23 | 201: '新建或修改数据成功。',
24 | 202: '一个请求已经进入后台排队(异步任务)。',
25 | 204: '删除数据成功。',
26 | 400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
27 | 401: '用户没有权限(令牌、用户名、密码错误)。',
28 | 403: '用户得到授权,但是访问是被禁止的。',
29 | 404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
30 | 406: '请求的格式不可得。',
31 | 410: '请求的资源被永久删除,且不会再得到的。',
32 | 422: '当创建一个对象时,发生一个验证错误。',
33 | 500: '服务器发生错误,请检查服务器。',
34 | 502: '网关错误。',
35 | 503: '服务不可用,服务器暂时过载或维护。',
36 | 504: '网关超时。'
37 | }
38 |
39 | const useFetch = createFetch({
40 | baseUrl: import.meta.env.VITE_BASE_API,
41 | options: {
42 | async beforeFetch(ctx) {
43 | const { options } = ctx
44 |
45 | options.headers.Authorization = `Bearer custom`
46 | options.headers.apifoxToken = 'IbvbVFA8uGdREezk4bfv9'
47 |
48 | const { params } = options
49 | // 自定义 Query String Parameters 携带请求参数。 一般是get请求
50 | if (params && typeof params !== 'string') {
51 | ctx.url = montageUrl(ctx.url, unref(params))
52 | }
53 |
54 | return ctx
55 | },
56 |
57 | async afterFetch(ctx) {
58 | const { code, msg } = ctx.data
59 |
60 | if (code === 200) {
61 | return ctx.data
62 | }
63 |
64 | // 兼容多个api
65 | if (code !== 200 && msg) {
66 | message.error(msg || '服务器错误!')
67 | }
68 |
69 | return ctx
70 | },
71 |
72 | async onFetchError({ response }) {
73 | message.error(codeMessage[response?.status] || '服务器错误,请联系管理员!')
74 | }
75 | },
76 | fetchOptions: {
77 | mode: 'cors'
78 | }
79 | })
80 |
81 | export default useFetch
82 |
--------------------------------------------------------------------------------
/src/pages/setting/menu-enter/components/List.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
54 |
55 |
59 | 新增菜单
60 |
61 |
62 |
63 |
64 |
65 |
69 |
70 |
71 |
72 |
73 | {{ record.status === 1 ? '停用' : '启用' }}
74 |
75 | 编辑
76 |
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/src/components/Lock/index.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |

32 |
屏幕已锁定
33 |
37 |
41 |
46 |
47 |
48 |
49 | 解锁
50 |
51 |
52 |
53 |
54 |
55 |
56 |
90 |
--------------------------------------------------------------------------------
/src/pages/user/role/index.vue:
--------------------------------------------------------------------------------
1 |
49 |
50 |
51 |
56 |
57 |
61 | 新增角色
62 |
63 |
64 |
65 |
66 |
67 |
71 |
72 |
73 |
74 |
75 | {{ record.status === 1 ? '禁用' : '启用' }}
76 |
77 | 编辑
78 |
79 |
80 |
81 |
82 |
83 |
--------------------------------------------------------------------------------
/src/components/ProTable/index.vue:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
79 |
84 |
88 |
89 |
90 |
91 |
--------------------------------------------------------------------------------
/src/pages/plugin/excel/index.vue:
--------------------------------------------------------------------------------
1 |
64 |
65 |
66 |
67 |
73 | 导出excel
74 |
75 |
79 |
80 |
81 |
87 | 导出excel
88 |
89 |
93 |
94 |
--------------------------------------------------------------------------------
/src/components/SearchBox/index.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
47 |
51 |
52 |
53 |
54 | 查询
55 |
56 |
57 |
58 |
59 |
60 | 重置
61 |
62 |
63 |
64 |
65 |
66 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
102 |
--------------------------------------------------------------------------------
/src/pages/plugin/dayjs/index.vue:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
19 | 开始结束日期00:00:00 - 23:59:59 化格式
20 | {{ dayStartEndTime(dateRange) }}
21 | dayStartEndTime
22 |
23 |
28 |
29 |
30 |
31 |
36 | 某个日期的 - 几天前后
37 | {{ dayAgoDate }}
38 | dayAgoOrSo
39 |
40 |
41 |
45 | 当天
46 |
47 |
51 | 一天后
52 |
53 |
57 | 七天后
58 |
59 |
63 | 30天后
64 |
65 |
69 | 10天前
70 |
71 |
72 |
73 |
74 |
75 |
80 | 2023-09-20 15:00:00
81 | fromCurrentTime
82 |
83 | {{ fromCurrentTime('2023-09-19 16:00:00') }}
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/src/pages/plugin/calendar/index.vue:
--------------------------------------------------------------------------------
1 |
81 |
82 |
83 |
88 |
89 |
90 |
96 |
97 |
98 | {{ format(dragValue ? dragValue.start : range.start, 'MMM D') }}
99 | -
100 | {{ format(dragValue ? dragValue.end : range.end, 'MMM D') }}
101 |
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/src/pages/plugin/idcard/index.vue:
--------------------------------------------------------------------------------
1 |
40 |
41 |
42 |
46 | 随机一个身份证号码🆔
47 |
48 |
49 |
56 |
60 |
64 |
65 |
66 |
70 | 校验
71 |
72 |
73 |
74 |
75 |
79 |
80 | {{ userInfo.valid ? '合法的身份证号' : '不合法的身份证号' }}
81 |
82 | {{ userInfo.address }}
83 | {{ { M: '男', F: '女' }[userInfo.gender] }}
84 | {{ userInfo.province }}
85 | {{ userInfo.city }}
86 |
87 | {{ userInfo.area }}
88 |
89 |
90 | {{ userInfo.cardText }}
91 |
92 |
93 | {{ userInfo.age }}
94 |
95 |
96 |
97 |
101 | idcard api
102 |
103 |
104 |
--------------------------------------------------------------------------------
/src/utils/date.js:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 | import { message } from 'ant-design-vue'
3 |
4 | const _getDayOrigin = (date) => {
5 | const curDay = dayjs(date)
6 | if (!curDay.isValid()) {
7 | const err = `存在不合法的日期格式:${date}`
8 | message.error(err)
9 | console.error(err)
10 | }
11 |
12 | return curDay
13 | }
14 |
15 | /**
16 | *
17 | * 某个日期的 - 几天前后
18 | * A few days ago or so
19 | * @param dayNum
20 | * @param date
21 | * @returns {string}
22 | * @example
23 | *
24 | * afterDay(1)
25 | * afterDay(-7)
26 | */
27 | export const dayAgoOrSo = (dayNum = 0, date) => {
28 | return _getDayOrigin(date).add(dayNum, 'day').format('YYYY-MM-DD')
29 | }
30 |
31 | /**
32 | * 日期补充00:00:00
33 | * @param date
34 | * @returns {string}
35 | *
36 | * @example
37 | * dayStartTime('2023-01-01') - 2023-01-01:00:00:00
38 | * dayStartTime('2023-01-01 12:00:11') - 2023-01-01:00:00:00
39 | */
40 | export const dayStartTime = (date) => {
41 | return _getDayOrigin(date).startOf('day').format('YYYY-MM-DD HH:mm:ss')
42 | }
43 |
44 | /**
45 | * 日期补充23:59:59
46 | * @param date
47 | * @returns {string}
48 | *
49 | * @example
50 | * dayEndTime('2023-01-01') - 2023-01-01:00:00:00
51 | * dayEndTime('2023-01-01 12:00:11') - 2023-01-01:00:00:00
52 | */
53 | export const dayEndTime = (date) => {
54 | return _getDayOrigin(date).endOf('day').format('YYYY-MM-DD HH:mm:ss')
55 | }
56 |
57 | /**
58 | * 获取日期区间的开始结束日期补充 00:00:00 - 23:59:59
59 | * @param dateArr
60 | * @returns {[string,string]|*[]}
61 | */
62 | export const dayStartEndTime = (dateArr) => {
63 | if (!Array.isArray(dateArr)) {
64 | console.error("dayStartEndTime 入参是一个日期数组 例:['2023-01-01', '2023-01-22']")
65 | return []
66 | }
67 |
68 | if (dateArr.length === 0) {
69 | return []
70 | }
71 |
72 | return [dayStartTime(dateArr[0]), dayEndTime(dateArr[1])]
73 | }
74 |
75 | /**
76 | * 获取 UNIX 时间戳,10位数字,秒
77 | * @param date
78 | * @returns {number}
79 | */
80 | export const getUnixTime = (date) => {
81 | return _getDayOrigin(date).unix()
82 | }
83 |
84 | /**
85 | * 获取 UNIX 时间戳,13位数字,毫秒
86 | * @param date
87 | * @returns {number}
88 | */
89 | export const getTimeMill = (date) => {
90 | return _getDayOrigin(date).valueOf()
91 | }
92 |
93 | /**
94 | *
95 | * @param date date小于当前时间
96 | */
97 | const ONE_HOUR = 60 * 60
98 | export const fromCurrentTime = (date) => {
99 | const dateUnixTime = getUnixTime(date)
100 | const currentUnixTime = getUnixTime()
101 |
102 | const spaceTime = Math.abs(currentUnixTime - dateUnixTime)
103 |
104 | if (spaceTime < ONE_HOUR) {
105 | return `${parseInt(String(spaceTime / 60))}分钟前`
106 | } else if (spaceTime < ONE_HOUR * 24) {
107 | return `${parseInt(String(spaceTime / ONE_HOUR))}小时前`
108 | } else if (spaceTime < ONE_HOUR * 24 * 2) {
109 | return '昨天'
110 | } else {
111 | return `${parseInt(String(spaceTime / (ONE_HOUR * 24)))}天前`
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/pages/login/components/login-form.vue:
--------------------------------------------------------------------------------
1 |
32 |
33 |
34 |
76 |
77 |
78 |
104 |
--------------------------------------------------------------------------------
/src/router/group/com.js:
--------------------------------------------------------------------------------
1 | import Layout from '@/layout/index.vue'
2 |
3 | const BASE_URL = '/com'
4 |
5 | export default [
6 | {
7 | path: BASE_URL,
8 | component: Layout,
9 | name: 'Com',
10 | meta: {
11 | title: '组件',
12 | isGroup: true,
13 | icon: 'i-carbon-ibm-cloud-transit-gateway'
14 | },
15 | children: [
16 | {
17 | path: `${BASE_URL}/list-1`,
18 | name: 'List-1',
19 | meta: {
20 | title: '列表-1',
21 | icon: 'i-carbon-list-boxes'
22 | },
23 | children: [
24 | {
25 | path: `${BASE_URL}/list-1/list2-1`,
26 | name: 'List2-1',
27 | component: () => import('@/pages/test/test2.vue'),
28 | meta: {
29 | title: '列表2-1'
30 | }
31 | },
32 | {
33 | path: `${BASE_URL}/list-1/list3-1`,
34 | name: 'List3-1',
35 | component: () => import('@/pages/exception/404-page.vue'),
36 | meta: {
37 | title: '列表3-1'
38 | }
39 | }
40 | ]
41 | },
42 | {
43 | path: `${BASE_URL}/count-down`,
44 | name: 'CountDown',
45 | component: () => import('@/pages/com/count-down/index.vue'),
46 | meta: {
47 | title: '倒计时',
48 | icon: 'i-carbon-time'
49 | }
50 | },
51 | {
52 | path: `${BASE_URL}/modal`,
53 | name: 'Modal',
54 | component: () => import('@/pages/com/modal/index.vue'),
55 | meta: {
56 | title: 'Modal',
57 | icon: 'i-carbon-model-alt'
58 | }
59 | },
60 | {
61 | path: `${BASE_URL}/auto`,
62 | name: 'BtnAuth',
63 | component: () => import('@/pages/com/auth/index.vue'),
64 | meta: {
65 | title: '按钮权限',
66 | icon: 'i-carbon-model-alt'
67 | }
68 | },
69 | {
70 | path: `${BASE_URL}/mark`,
71 | name: 'TextMark',
72 | component: () => import('@/pages/com/mark-keyword/index.vue'),
73 | meta: {
74 | title: '关键词高亮',
75 | icon: 'i-carbon-dot-mark'
76 | }
77 | },
78 | {
79 | path: `${BASE_URL}/ellipsis`,
80 | name: 'TextEllipsis',
81 | component: () => import('@/pages/com/text-ellipsis/index.vue'),
82 | meta: {
83 | title: '文本溢出',
84 | icon: 'i-carbon-text-footnote'
85 | }
86 | },
87 | {
88 | path: `${BASE_URL}/tabs`,
89 | name: 'RadiusTabs',
90 | component: () => import('@/pages/com/radius-tabs/index.vue'),
91 | meta: {
92 | title: '反圆角Tabs',
93 | icon: 'i-carbon-table-alias'
94 | }
95 | },
96 | {
97 | path: `${BASE_URL}/lazy`,
98 | name: 'LazyImage',
99 | component: () => import('@/pages/com/lazy/index.vue'),
100 | meta: {
101 | title: '图片懒加载',
102 | icon: 'i-carbon-image'
103 | }
104 | }
105 | ]
106 | }
107 | ]
108 |
--------------------------------------------------------------------------------
/src/pages/dashboard/index.vue:
--------------------------------------------------------------------------------
1 |
45 |
46 |
47 |
48 |
49 |
53 |
54 |
55 | {{ age }} - {{ user }} - {{ userInfo }}
56 |
57 |
58 |
59 |
60 |
61 |
62 |
原loading
63 |
67 | 原loading
68 |
69 |
70 |
75 | v-loading 点击1
76 |
77 |
81 | v-loading 点击2
82 |
83 |
88 | v-loading 自定义时间
89 |
90 |
91 |
95 | watch
96 |
97 |
{{ counterStore.count }}
98 |
103 | overlayscrollbars
104 |
105 |
106 |
107 | (open = true)"
109 | type="primary"
110 | >
111 | modal
112 |
113 |
119 |
120 |
124 | asdasd
125 |
126 |
127 |
128 |
provide, inject
129 |
provide初始数据 {{ sex }}
130 |
134 | 顶级修改原数据
135 |
136 |
137 |
138 |
139 |
140 |
--------------------------------------------------------------------------------
/src/router/group/plugin.js:
--------------------------------------------------------------------------------
1 | import Layout from '@/layout/index.vue'
2 |
3 | const BASE_URL = '/plugin'
4 |
5 | export default [
6 | {
7 | path: BASE_URL,
8 | component: Layout,
9 | name: 'Plugin',
10 | meta: {
11 | title: '插件',
12 | isGroup: true,
13 | icon: 'i-carbon-location-heart'
14 | },
15 | children: [
16 | {
17 | path: `${BASE_URL}/check`,
18 | name: 'PluginList',
19 | component: () => import('@/pages/plugin/check-card/index.vue'),
20 | meta: {
21 | title: 'CheckCard',
22 | icon: 'i-carbon-data-check'
23 | }
24 | },
25 | {
26 | path: `${BASE_URL}/watermark`,
27 | name: 'Watermark',
28 | component: () => import('@/pages/plugin/watermark/index.vue'),
29 | meta: {
30 | title: '水印',
31 | icon: 'i-carbon-encryption'
32 | }
33 | },
34 | {
35 | path: `${BASE_URL}/idcard`,
36 | name: 'IdCard',
37 | component: () => import('@/pages/plugin/idcard/index.vue'),
38 | meta: {
39 | title: 'idcard',
40 | icon: 'i-carbon-id-management'
41 | }
42 | },
43 | {
44 | path: `${BASE_URL}/icons`,
45 | name: 'Icons',
46 | component: () => import('@/pages/plugin/icons/index.vue'),
47 | meta: {
48 | title: '图标',
49 | icon: 'i-carbon-ibm-content-services'
50 | }
51 | },
52 | {
53 | path: `${BASE_URL}/org`,
54 | name: 'OrgTree',
55 | component: () => import('@/pages/plugin/org_tree/index.vue'),
56 | meta: {
57 | title: 'OrgTree',
58 | icon: 'i-carbon-decision-tree'
59 | }
60 | },
61 | {
62 | path: `${BASE_URL}/calendar`,
63 | name: 'Calendar',
64 | component: () => import('@/pages/plugin/calendar/index.vue'),
65 | meta: {
66 | title: '日历',
67 | icon: 'i-carbon-calendar'
68 | }
69 | },
70 | {
71 | path: `${BASE_URL}/sign`,
72 | name: 'Sign',
73 | component: () => import('@/pages/plugin/sign/index.vue'),
74 | meta: {
75 | title: '签字板',
76 | icon: 'i-carbon-contour-draw'
77 | }
78 | },
79 | {
80 | path: `${BASE_URL}/day`,
81 | name: 'DayJS',
82 | component: () => import('@/pages/plugin/dayjs/index.vue'),
83 | meta: {
84 | title: 'dayjs',
85 | icon: 'i-carbon-event-schedule'
86 | }
87 | },
88 | {
89 | path: `${BASE_URL}/key`,
90 | name: 'BRKey',
91 | component: () => import('@/pages/plugin/browser-key/index.vue'),
92 | meta: {
93 | title: '浏览器唯一标识',
94 | icon: 'i-carbon-ibm-cloud-key-protect'
95 | }
96 | },
97 | {
98 | path: `${BASE_URL}/lodash`,
99 | name: 'Lodash',
100 | component: () => import('@/pages/plugin/lodash/index.vue'),
101 | meta: {
102 | title: 'LODASH常用',
103 | icon: 'i-carbon-ibm-toolchain'
104 | }
105 | },
106 | {
107 | path: `${BASE_URL}/zip`,
108 | name: 'ExportZip',
109 | component: () => import('@/pages/plugin/zip/index.vue'),
110 | meta: {
111 | title: 'Zip',
112 | icon: 'i-carbon-zip'
113 | }
114 | },
115 | {
116 | path: `${BASE_URL}/excel`,
117 | name: 'ExportExcel',
118 | component: () => import('@/pages/plugin/excel/index.vue'),
119 | meta: {
120 | title: 'Excel',
121 | icon: 'i-carbon-document'
122 | }
123 | }
124 | ]
125 | }
126 | ]
127 |
--------------------------------------------------------------------------------
/src/layout/sider/components/ColumnTabs.vue:
--------------------------------------------------------------------------------
1 |
47 |
48 |
49 |
50 |
54 |
60 |
61 | {{ it.meta.title }}
62 |
63 |
64 |
69 |
75 |
76 |
80 |
81 |
82 |
83 |
128 |
129 |
139 |
--------------------------------------------------------------------------------
/src/pages/login/components/banner.vue:
--------------------------------------------------------------------------------
1 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
36 |
37 |
41 |
42 |
43 |
44 |
45 |
49 |
50 |
51 |
52 |
53 |
58 |
62 |
{{ item.slogan }}
63 |
{{ item.subSlogan }}
64 |
![]()
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
158 |
--------------------------------------------------------------------------------
/src/components/CheckCard/index.vue:
--------------------------------------------------------------------------------
1 |
66 |
67 |
68 |
69 |
80 |
81 |
82 |
{{ it.title }}
83 |
84 |
85 |
86 |
87 |
88 |
157 |
--------------------------------------------------------------------------------
/src/hooks/useCountDown/index.js:
--------------------------------------------------------------------------------
1 | import { ref, computed, onActivated, onDeactivated, onBeforeUnmount } from 'vue'
2 | import { raf, cancelRaf, inBrowser } from '../util.js'
3 |
4 | // export type CurrentTime = {
5 | // days: number;
6 | // hours: number;
7 | // total: number;
8 | // minutes: number;
9 | // seconds: number;
10 | // milliseconds: number;
11 | // };
12 |
13 | const SECOND = 1000
14 | const MINUTE = 60 * SECOND
15 | const HOUR = 60 * MINUTE
16 | const DAY = 24 * HOUR
17 |
18 | /**
19 | *
20 | * @param time { number }
21 | * @returns {{ milliseconds: number, total: number, hours: number, seconds: number, minutes: number, days: number }}
22 | */
23 | function parseTime(time) {
24 | const days = Math.floor(time / DAY)
25 | const hours = Math.floor((time % DAY) / HOUR)
26 | const minutes = Math.floor((time % HOUR) / MINUTE)
27 | const seconds = Math.floor((time % MINUTE) / SECOND)
28 | const milliseconds = Math.floor(time % SECOND)
29 |
30 | return {
31 | total: time,
32 | days,
33 | hours,
34 | minutes,
35 | seconds,
36 | milliseconds
37 | }
38 | }
39 |
40 | /**
41 | *
42 | * @param time1 { number }
43 | * @param time2 { number }
44 | * @returns { boolean }
45 | */
46 | function isSameSecond(time1, time2) {
47 | return Math.floor(time1 / 1000) === Math.floor(time2 / 1000)
48 | }
49 |
50 | /**
51 | * @param options UseCountDownOptions = {
52 | * time: number;
53 | * millisecond?: boolean;
54 | * onChange?: (current: CurrentTime) => void;
55 | * onFinish?: () => void;
56 | * }
57 | * @returns {{current: ComputedRef<{CurrentTime}>, start: (function(): void), reset: (function(number=): void), pause: (function(): void)}}
58 | */
59 | // 文档详情: https://vant-ui.github.io/vant/#/zh-CN/count-down
60 | export function useCountDown(options) {
61 | let rafId
62 | let endTime
63 | let counting
64 | let deactivated
65 |
66 | const remain = ref(options.time)
67 | const current = computed(() => parseTime(remain.value))
68 |
69 | const pause = () => {
70 | counting = false
71 | cancelRaf(rafId)
72 | }
73 |
74 | const getCurrentRemain = () => Math.max(endTime - Date.now(), 0)
75 |
76 | /**
77 | * @param value { number }
78 | */
79 | const setRemain = (value) => {
80 | remain.value = value
81 | options.onChange?.(current.value)
82 |
83 | if (value === 0) {
84 | pause()
85 | options.onFinish?.()
86 | }
87 | }
88 |
89 | const microTick = () => {
90 | rafId = raf(() => {
91 | // in case of call reset immediately after finish
92 | if (counting) {
93 | setRemain(getCurrentRemain())
94 |
95 | if (remain.value > 0) {
96 | microTick()
97 | }
98 | }
99 | })
100 | }
101 |
102 | const macroTick = () => {
103 | rafId = raf(() => {
104 | // in case of call reset immediately after finish
105 | if (counting) {
106 | const remainRemain = getCurrentRemain()
107 |
108 | if (!isSameSecond(remainRemain, remain.value) || remainRemain === 0) {
109 | setRemain(remainRemain)
110 | }
111 |
112 | if (remain.value > 0) {
113 | macroTick()
114 | }
115 | }
116 | })
117 | }
118 |
119 | const tick = () => {
120 | // should not start counting in server
121 | // see: https://github.com/vant-ui/vant/issues/7807
122 | if (!inBrowser) {
123 | return
124 | }
125 |
126 | if (options.millisecond) {
127 | microTick()
128 | } else {
129 | macroTick()
130 | }
131 | }
132 |
133 | const start = () => {
134 | if (!counting) {
135 | endTime = Date.now() + remain.value
136 | counting = true
137 | tick()
138 | }
139 | }
140 |
141 | /**
142 | *
143 | * @param totalTime { number }
144 | */
145 | const reset = (totalTime = options.time) => {
146 | pause()
147 | remain.value = totalTime
148 | }
149 |
150 | onBeforeUnmount(pause)
151 |
152 | onActivated(() => {
153 | if (deactivated) {
154 | counting = true
155 | deactivated = false
156 | tick()
157 | }
158 | })
159 |
160 | onDeactivated(() => {
161 | if (counting) {
162 | pause()
163 | deactivated = true
164 | }
165 | })
166 |
167 | return {
168 | start,
169 | pause,
170 | reset,
171 | current
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/src/pages/list/search-list/index.vue:
--------------------------------------------------------------------------------
1 |
39 |
40 |
41 |
45 |
49 |
50 |
54 |
55 |
56 |
60 |
61 |
62 |
66 | 人工筛选
67 | 规则筛选
68 |
69 |
70 |
71 |
75 | 已上线
76 | 已下线
77 |
78 |
79 |
80 |
84 |
85 |
86 |
87 |
88 | 新建
89 | 批量导入
90 |
91 |
92 |
93 | 导出
94 |
95 |
96 |
97 |
104 |
105 |
106 |
107 | {{ title }}
108 |
109 |
110 |
111 |
112 |
113 |
114 |
![]()
118 |
{{ record.type }}
119 |
120 |
121 |
122 |
123 |
127 |
128 |
129 |
130 |
131 | 失效
132 |
133 | 删除
134 |
135 |
136 |
140 |
141 |
142 |
143 |
144 | 操作一
145 | 操作二
146 | 操作三
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
--------------------------------------------------------------------------------
/src/directive/hasAuth.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 设计场景
3 | *
4 | * 1、后台新增权限时选择类型是否是按钮,选择按钮类型。登录后调取接口查出所有按钮类型权限:(我使用接口作为唯一标识)
5 | * response = ['/user/list', '/user/add', '/user/detail/add', '/user/detail/edit']
6 | *
7 | * 指令使用格式
8 | * v-auth="'/user/list'"
9 | * v-auth="['/user/list', '/user/detail/edit']"
10 | *
11 | *
12 | * 2、按照菜单权限层级返回。类似 mock 中的 adminRoutes,再增加类型区分是否是按钮权限即可。
13 | * response = [{ key: 'user', children: [{ key: 'user/list', children: [{ type: 'btn', key: 'api/user/list' }] }] }]
14 | *
15 | * 使用(.[user]修饰符用来快速定位查找,也可以起到命名空间的作用)
16 | *
17 | * 找到命名空间内的
18 | * v-auth.user="'api/user/list'"
19 | * v-auth.user="['api/user/list', 'api/user/list']"
20 | * v-auth="{ user: ["api/user/list", "api/user/add"], setting: [""] }"
21 | * v-auth="{ user: "", setting: "" }"
22 | *
23 | * user和setting模块中任意找到
24 | * v-auth.user.setting="api/user/list"
25 | * v-auth.user.setting="['api/user/list', 'api/user/add']"
26 | *
27 | * tip:要是有 user下面,或者setting下面有某个权限都可以显示按钮这种场景该怎么办
28 | *
29 | *
30 | *
31 | */
32 |
33 | import { isArray, isString, isPlainObject } from 'lodash-es'
34 |
35 | const _mockResRouteData = [
36 | {
37 | key: 'user',
38 | name: '用户管理',
39 | children: [
40 | {
41 | key: 'user/list',
42 | name: '用户列表',
43 | children: [
44 | { type: 'btn', key: 'api/user/list', name: '用户列表查看' },
45 | { type: 'btn', key: 'api/user/detail', name: '用户详情' },
46 | { type: 'btn', key: 'api/user/auth-edit', name: '用户权限编辑' }
47 | ]
48 | },
49 | {
50 | key: 'user/list1',
51 | name: '用户列表1',
52 | children: [
53 | { type: 'btn', key: 'api/user/list1', name: '用户列表查看1' },
54 | { type: 'btn', key: 'api/user/detail1', name: '用户详情1' },
55 | { type: 'btn', key: 'api/user/auth-edit1', name: '用户权限编辑1' }
56 | ]
57 | }
58 | ]
59 | },
60 | {
61 | key: 'setting',
62 | name: '设置',
63 | children: [
64 | {
65 | key: 'setting/auth',
66 | name: '权限设置',
67 | children: [
68 | { type: 'btn', key: 'api/auth/add', name: '新增权限' },
69 | { type: 'btn', key: 'api/auth/edit', name: '编辑权限' },
70 | { type: 'btn', key: 'api/auth/list', name: '权限列表' }
71 | ]
72 | }
73 | ]
74 | },
75 | {
76 | key: '404',
77 | name: '异常页面',
78 | children: [
79 | {
80 | key: 'exception/404',
81 | name: '404页面',
82 | children: [
83 | { type: 'btn', key: 'api/exception/add', name: '新增' },
84 | { type: 'btn', key: 'api/exception/edit', name: '编辑' }
85 | ]
86 | },
87 | {
88 | key: 'exception/503',
89 | name: '503页面'
90 | }
91 | ]
92 | }
93 | ]
94 |
95 | // 模块唯一标识key
96 | const KEY_NAME = 'key'
97 | const findNamesRoutes = (moduleName) => {
98 | return (_mockResRouteData.find((route) => route[KEY_NAME] === moduleName) || {}).children || []
99 | }
100 |
101 | const btnKeys = (routes) => {
102 | const keys = []
103 |
104 | function find(arr) {
105 | arr.forEach((it) => {
106 | // 按钮类型的唯一key
107 | if (it.type === 'btn') {
108 | keys.push(it[KEY_NAME])
109 | }
110 |
111 | if (it.children && it.children.length) {
112 | find(it.children)
113 | }
114 | })
115 | }
116 | find(routes)
117 |
118 | return keys
119 | }
120 |
121 | /**
122 | * 比对是否有相同项,只要找到一个有相同的,就立即返回(或的关系,所以可以提前返回)
123 | *
124 | * arrModuleValue 必然存在
125 | */
126 | const hasDuplicates = (arr1, arrModuleValue) => {
127 | for (let i = 0, len = arrModuleValue.length; i < len; i++) {
128 | if (arr1.includes(arrModuleValue[i])) {
129 | return true
130 | }
131 | }
132 |
133 | return false
134 | }
135 |
136 | const hasPer = (moduleName, moduleValue) => {
137 | const keys = btnKeys(findNamesRoutes(moduleName))
138 |
139 | if (isString(moduleValue)) {
140 | return keys.includes(moduleValue)
141 | }
142 |
143 | if (isArray(moduleValue) && moduleValue.length > 0) {
144 | return hasDuplicates(keys, moduleValue)
145 | }
146 |
147 | return false
148 | }
149 |
150 | const DOM_MARK = 'data-auth'
151 | const hasMark = (el) => {
152 | return el.getAttribute(DOM_MARK) === 'true'
153 | }
154 |
155 | const setMark = (el) => {
156 | el.setAttribute(DOM_MARK, true)
157 | }
158 |
159 | const removeEl = (el) => {
160 | el && el.parentNode && el.parentNode.removeChild(el)
161 | }
162 |
163 | /**
164 | * 场景2方式实现
165 | */
166 | export default {
167 | mounted(el, binding) {
168 | const { modifiers, value } = binding
169 |
170 | const valueIsPlainObj = isPlainObject(value)
171 | const routeModules = Object.keys(valueIsPlainObj ? value : modifiers)
172 |
173 | if (routeModules.length) {
174 | try {
175 | routeModules.forEach((module) => {
176 | const curModuleValue = valueIsPlainObj ? value[module] : value
177 | if (hasPer(module, curModuleValue)) {
178 | setMark(el)
179 | throw new Error('当前el已打标可立即跳出')
180 | }
181 | })
182 | } catch {}
183 | } else {
184 | // 没有命名空间直接删除,例:v-auth='"api/list"'
185 | removeEl(el)
186 | return
187 | }
188 |
189 | if (!hasMark(el)) {
190 | removeEl(el)
191 | }
192 | },
193 |
194 | updated() {},
195 |
196 | unmounted() {}
197 | }
198 |
--------------------------------------------------------------------------------
/public/print-lock.css:
--------------------------------------------------------------------------------
1 | @media print{body{margin:0px;padding:0px;}}@page{margin:0;}.hiprint-printPaper *{box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;}.hiprint-printPaper *:focus{outline:-webkit-focus-ring-color auto 0px;}.hiprint-printPaper{position:relative;padding:0 0 0 0;page-break-after:always;-webkit-user-select:none;-moz-user-select:none;user-select:none;overflow-x:hidden;overflow:hidden;}.hiprint-printPaper .hiprint-printPaper-content{position:relative;}@-moz-document url-prefix(){.hiprint-printPaper .hiprint-printPaper-content{position:relative;margin-top:20px;top:-20px;}}.hiprint-printPaper.design{overflow:visible;}.hiprint-printTemplate .hiprint-printPanel{page-break-after:always;}.hiprint-printPaper,hiprint-printPanel{box-sizing:border-box;border:0px;}.hiprint-printPanel .hiprint-printPaper:last-child{page-break-after:avoid;}.hiprint-printTemplate .hiprint-printPanel:last-child{page-break-after:avoid;}.hiprint-printPaper .hideheaderLinetarget{border-top:0px dashed rgb(201,190,190) !important;}.hiprint-printPaper .hidefooterLinetarget{border-top:0px dashed rgb(201,190,190) !important;}.hiprint-printPaper.design{border:1px dashed rgba(170,170,170,0.7);}.design .hiprint-printElement-table-content,.design .hiprint-printElement-longText-content{overflow:hidden;box-sizing:border-box;}.design .resize-panel{box-sizing:border-box;border:1px dotted;}.hiprint-printElement-text{background-color:transparent;background-repeat:repeat;padding:0 0 0 0;border:0.75pt none rgb(0,0,0);direction:ltr;font-family:'SimSun';font-size:9pt;font-style:normal;font-weight:normal;padding-bottom:0pt;padding-left:0pt;padding-right:0pt;padding-top:0pt;text-align:left;text-decoration:none;line-height:9.75pt;box-sizing:border-box;word-wrap:break-word;word-break:break-all;}.design .hiprint-printElement-text-content{border:1px dashed rgb(206,188,188);box-sizing:border-box;}.hiprint-printElement-longText{background-color:transparent;background-repeat:repeat;border:0.75pt none rgb(0,0,0);direction:ltr;font-family:'SimSun';font-size:9pt;font-style:normal;font-weight:normal;padding-bottom:0pt;padding-left:0pt;padding-right:0pt;padding-top:0pt;text-align:left;text-decoration:none;line-height:9.75pt;box-sizing:border-box;word-wrap:break-word;word-break:break-all;}.hiprint-printElement-table{background-color:transparent;background-repeat:repeat;color:rgb(0,0,0);border-color:rgb(0,0,0);border-style:none;direction:ltr;font-family:'SimSun';font-size:9pt;font-style:normal;font-weight:normal;padding-bottom:0pt;padding-left:0pt;padding-right:0pt;padding-top:0pt;text-align:left;text-decoration:none;padding:0 0 0 0;box-sizing:border-box;line-height:9.75pt;}.hiprint-printElement-table thead{background:#e8e8e8;font-weight:700;}table.hiprint-printElement-tableTarget{width:100%;}.hiprint-printElement-tableTarget,.hiprint-printElement-tableTarget tr,.hiprint-printElement-tableTarget td{border-color:rgb(0,0,0);font-weight:normal;direction:ltr;padding-bottom:0pt;padding-left:4pt;padding-right:4pt;padding-top:0pt;text-decoration:none;vertical-align:middle;box-sizing:border-box;word-wrap:break-word;word-break:break-all;}.hiprint-printElement-tableTarget-border-all{border:1px solid;}.hiprint-printElement-tableTarget-border-none{border:0px solid;}.hiprint-printElement-tableTarget-border-lr{border-left:1px solid;border-right:1px solid;}.hiprint-printElement-tableTarget-border-left{border-left:1px solid;}.hiprint-printElement-tableTarget-border-right{border-right:1px solid;}.hiprint-printElement-tableTarget-border-tb{border-top:1px solid;border-bottom:1px solid;}.hiprint-printElement-tableTarget-border-top{border-top:1px solid;}.hiprint-printElement-tableTarget-border-bottom{border-bottom:1px solid;}.hiprint-printElement-tableTarget-border-td-none td{border:0px solid;}.hiprint-printElement-tableTarget-border-td-all td:not(:nth-last-child(-n + 2)){border-right:1px solid;}.hiprint-printElement-tableTarget-border-td-all td:last-child{border-left:1px solid;}.hiprint-printElement-tableTarget-border-td-all td:last-child:first-child{border-left:none;}.hiprint-printElement-tableTarget td{height:18pt;}.hiprint-printPaper .hiprint-paperNumber{font-size:9pt;}.design .hiprint-printElement-table-handle{position:absolute;height:21pt;width:21pt;background:red;z-index:1;}.hiprint-printPaper .hiprint-paperNumber-disabled{float:right !important;right:0 !important;color:gainsboro !important;}.hiprint-printElement-vline,.hiprint-printElement-hline{border:0px none rgb(0,0,0);}.hiprint-printElement-vline{border-left:0.75pt solid #000;border-right:0px none rgb(0,0,0) !important;border-bottom:0px none rgb(0,0,0) !important;border-top:0px none rgb(0,0,0) !important;}.hiprint-printElement-hline{border-top:0.75pt solid #000;border-right:0px none rgb(0,0,0) !important;border-bottom:0px none rgb(0,0,0) !important;border-left:0px none rgb(0,0,0) !important;}.hiprint-printElement-oval,.hiprint-printElement-rect{border:0.75pt solid #000;}.hiprint-text-content-middle{}.hiprint-text-content-middle > div{display:grid;align-items:center;}.hiprint-text-content-bottom{}.hiprint-text-content-bottom > div{display:grid;align-items:flex-end;}.hiprint-text-content-wrap{}.hiprint-text-content-wrap .hiprint-text-content-wrap-nowrap{white-space:nowrap;}.hiprint-text-content-wrap .hiprint-text-content-wrap-clip{white-space:nowrap;overflow:hidden;text-overflow:clip;}.hiprint-text-content-wrap .hiprint-text-content-wrap-ellipsis{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.hi-grid-row{position:relative;height:auto;margin-right:0;margin-left:0;zoom:1;display:block;box-sizing:border-box;}.hi-grid-row::after,.hi-grid-row::before{display:table;content:'';box-sizing:border-box;}.hi-grid-col{display:block;box-sizing:border-box;position:relative;float:left;flex:0 0 auto;}.table-grid-row{margin-left:-0pt;margin-right:-0pt;}.tableGridColumnsGutterRow{padding-left:0pt;padding-right:0pt;}.hiprint-gridColumnsFooter{text-align:left;clear:both;}
--------------------------------------------------------------------------------
/src/lib/theme/walden.project.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 1,
3 | "themeName": "walden",
4 | "theme": {
5 | "seriesCnt": "12",
6 | "backgroundColor": "rgba(252,252,252,0)",
7 | "titleColor": "#666666",
8 | "subtitleColor": "#999999",
9 | "textColorShow": false,
10 | "textColor": "#333",
11 | "markTextColor": "#ffffff",
12 | "color": [
13 | "#3fb1e3",
14 | "#6be6c1",
15 | "#626c91",
16 | "#a0a7e6",
17 | "#c4ebad",
18 | "#96dee8"
19 | ],
20 | "borderColor": "#ccc",
21 | "borderWidth": 0,
22 | "visualMapColor": [
23 | "#2a99c9",
24 | "#afe8ff"
25 | ],
26 | "legendTextColor": "#999999",
27 | "kColor": "#e6a0d2",
28 | "kColor0": "transparent",
29 | "kBorderColor": "#e6a0d2",
30 | "kBorderColor0": "#3fb1e3",
31 | "kBorderWidth": "2",
32 | "lineWidth": "3",
33 | "symbolSize": "8",
34 | "symbol": "emptyCircle",
35 | "symbolBorderWidth": "2",
36 | "lineSmooth": false,
37 | "graphLineWidth": "1",
38 | "graphLineColor": "#cccccc",
39 | "mapLabelColor": "#ffffff",
40 | "mapLabelColorE": "#3fb1e3",
41 | "mapBorderColor": "#aaaaaa",
42 | "mapBorderColorE": "#3fb1e3",
43 | "mapBorderWidth": 0.5,
44 | "mapBorderWidthE": 1,
45 | "mapAreaColor": "#eeeeee",
46 | "mapAreaColorE": "rgba(63,177,227,0.25)",
47 | "axes": [
48 | {
49 | "type": "all",
50 | "name": "通用坐标轴",
51 | "axisLineShow": true,
52 | "axisLineColor": "#cccccc",
53 | "axisTickShow": false,
54 | "axisTickColor": "#333",
55 | "axisLabelShow": true,
56 | "axisLabelColor": "#999999",
57 | "splitLineShow": true,
58 | "splitLineColor": [
59 | "#eeeeee"
60 | ],
61 | "splitAreaShow": false,
62 | "splitAreaColor": [
63 | "rgba(250,250,250,0.05)",
64 | "rgba(200,200,200,0.02)"
65 | ]
66 | },
67 | {
68 | "type": "category",
69 | "name": "类目坐标轴",
70 | "axisLineShow": true,
71 | "axisLineColor": "#333",
72 | "axisTickShow": true,
73 | "axisTickColor": "#333",
74 | "axisLabelShow": true,
75 | "axisLabelColor": "#333",
76 | "splitLineShow": false,
77 | "splitLineColor": [
78 | "#ccc"
79 | ],
80 | "splitAreaShow": false,
81 | "splitAreaColor": [
82 | "rgba(250,250,250,0.3)",
83 | "rgba(200,200,200,0.3)"
84 | ]
85 | },
86 | {
87 | "type": "value",
88 | "name": "数值坐标轴",
89 | "axisLineShow": true,
90 | "axisLineColor": "#333",
91 | "axisTickShow": true,
92 | "axisTickColor": "#333",
93 | "axisLabelShow": true,
94 | "axisLabelColor": "#333",
95 | "splitLineShow": true,
96 | "splitLineColor": [
97 | "#ccc"
98 | ],
99 | "splitAreaShow": false,
100 | "splitAreaColor": [
101 | "rgba(250,250,250,0.3)",
102 | "rgba(200,200,200,0.3)"
103 | ]
104 | },
105 | {
106 | "type": "log",
107 | "name": "对数坐标轴",
108 | "axisLineShow": true,
109 | "axisLineColor": "#333",
110 | "axisTickShow": true,
111 | "axisTickColor": "#333",
112 | "axisLabelShow": true,
113 | "axisLabelColor": "#333",
114 | "splitLineShow": true,
115 | "splitLineColor": [
116 | "#ccc"
117 | ],
118 | "splitAreaShow": false,
119 | "splitAreaColor": [
120 | "rgba(250,250,250,0.3)",
121 | "rgba(200,200,200,0.3)"
122 | ]
123 | },
124 | {
125 | "type": "time",
126 | "name": "时间坐标轴",
127 | "axisLineShow": true,
128 | "axisLineColor": "#333",
129 | "axisTickShow": true,
130 | "axisTickColor": "#333",
131 | "axisLabelShow": true,
132 | "axisLabelColor": "#333",
133 | "splitLineShow": true,
134 | "splitLineColor": [
135 | "#ccc"
136 | ],
137 | "splitAreaShow": false,
138 | "splitAreaColor": [
139 | "rgba(250,250,250,0.3)",
140 | "rgba(200,200,200,0.3)"
141 | ]
142 | }
143 | ],
144 | "axisSeperateSetting": false,
145 | "toolboxColor": "#999999",
146 | "toolboxEmphasisColor": "#666666",
147 | "tooltipAxisColor": "#cccccc",
148 | "tooltipAxisWidth": 1,
149 | "timelineLineColor": "#626c91",
150 | "timelineLineWidth": 1,
151 | "timelineItemColor": "#626c91",
152 | "timelineItemColorE": "#626c91",
153 | "timelineCheckColor": "#3fb1e3",
154 | "timelineCheckBorderColor": "#3fb1e3",
155 | "timelineItemBorderWidth": 1,
156 | "timelineControlColor": "#626c91",
157 | "timelineControlBorderColor": "#626c91",
158 | "timelineControlBorderWidth": 0.5,
159 | "timelineLabelColor": "#626c91",
160 | "datazoomBackgroundColor": "rgba(255,255,255,0)",
161 | "datazoomDataColor": "rgba(222,222,222,1)",
162 | "datazoomFillColor": "rgba(114,230,212,0.25)",
163 | "datazoomHandleColor": "#cccccc",
164 | "datazoomHandleWidth": "100",
165 | "datazoomLabelColor": "#999999"
166 | }
167 | }
--------------------------------------------------------------------------------
/src/pages/charts/line/index.vue:
--------------------------------------------------------------------------------
1 |
232 |
233 |
234 |
238 |
239 |
--------------------------------------------------------------------------------
/src/vendor/excel.js:
--------------------------------------------------------------------------------
1 | import { saveAs } from 'file-saver'
2 | import { writeXLSX, utils, SSF } from 'xlsx'
3 | import { isObject } from 'lodash-es'
4 |
5 | function generateArray(table) {
6 | let out = []
7 | let rows = table.querySelectorAll('tr')
8 | let ranges = []
9 | for (let R = 0; R < rows.length; ++R) {
10 | let outRow = []
11 | let row = rows[R]
12 | let columns = row.querySelectorAll('td')
13 | for (let C = 0; C < columns.length; ++C) {
14 | let cell = columns[C]
15 | let colspan = cell.getAttribute('colspan')
16 | let rowspan = cell.getAttribute('rowspan')
17 | let cellValue = cell.innerText
18 | if (cellValue !== '' && cellValue == +cellValue) cellValue = +cellValue
19 |
20 | //Skip ranges
21 | ranges.forEach(function (range) {
22 | if (R >= range.s.r && R <= range.e.r && outRow.length >= range.s.c && outRow.length <= range.e.c) {
23 | for (let i = 0; i <= range.e.c - range.s.c; ++i) outRow.push(null)
24 | }
25 | })
26 |
27 | //Handle Row Span
28 | if (rowspan || colspan) {
29 | rowspan = rowspan || 1
30 | colspan = colspan || 1
31 | ranges.push({
32 | s: {
33 | r: R,
34 | c: outRow.length
35 | },
36 | e: {
37 | r: R + rowspan - 1,
38 | c: outRow.length + colspan - 1
39 | }
40 | })
41 | }
42 |
43 | //Handle Value
44 | outRow.push(cellValue !== '' ? cellValue : null)
45 |
46 | //Handle Colspan
47 | if (colspan) for (let k = 0; k < colspan - 1; ++k) outRow.push(null)
48 | }
49 | out.push(outRow)
50 | }
51 | return [out, ranges]
52 | }
53 |
54 | function datenum(v, date1904) {
55 | if (date1904) v += 1462
56 | let epoch = Date.parse(v)
57 | return (epoch - new Date(Date.UTC(1899, 11, 30))) / (24 * 60 * 60 * 1000)
58 | }
59 |
60 | function sheet_from_array_of_arrays(data, opts) {
61 | let ws = {}
62 | let range = {
63 | s: {
64 | c: 10000000,
65 | r: 10000000
66 | },
67 | e: {
68 | c: 0,
69 | r: 0
70 | }
71 | }
72 | for (let R = 0; R != data.length; ++R) {
73 | for (let C = 0; C != data[R].length; ++C) {
74 | if (range.s.r > R) range.s.r = R
75 | if (range.s.c > C) range.s.c = C
76 | if (range.e.r < R) range.e.r = R
77 | if (range.e.c < C) range.e.c = C
78 | let cell = {
79 | v: data[R][C]
80 | }
81 | if (cell.v == null) continue
82 | let cell_ref = utils.encode_cell({
83 | c: C,
84 | r: R
85 | })
86 |
87 | if (typeof cell.v === 'number') cell.t = 'n'
88 | else if (typeof cell.v === 'boolean') cell.t = 'b'
89 | else if (cell.v instanceof Date) {
90 | cell.t = 'n'
91 | cell.z = SSF._table[14]
92 | cell.v = datenum(cell.v)
93 | } else cell.t = 's'
94 |
95 | ws[cell_ref] = cell
96 | }
97 | }
98 | if (range.s.c < 10000000) ws['!ref'] = utils.encode_range(range)
99 | return ws
100 | }
101 |
102 | function Workbook() {
103 | if (!(this instanceof Workbook)) return new Workbook()
104 | this.SheetNames = []
105 | this.Sheets = {}
106 | }
107 |
108 | function s2ab(s) {
109 | let buf = new ArrayBuffer(s.length)
110 | let view = new Uint8Array(buf)
111 | for (let i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
112 | return buf
113 | }
114 |
115 | export function export_table_to_excel(id) {
116 | let theTable = document.getElementById(id)
117 | let oo = generateArray(theTable)
118 | let ranges = oo[1]
119 |
120 | /* original data */
121 | let data = oo[0]
122 | let ws_name = 'SheetJS'
123 |
124 | let wb = new Workbook(),
125 | ws = sheet_from_array_of_arrays(data)
126 |
127 | /* add ranges to worksheet */
128 | // ws['!cols'] = ['apple', 'banan'];
129 | ws['!merges'] = ranges
130 |
131 | /* add worksheet to workbook */
132 | wb.SheetNames.push(ws_name)
133 | wb.Sheets[ws_name] = ws
134 |
135 | let wbout = writeXLSX(wb, {
136 | bookType: 'xlsx',
137 | bookSST: false,
138 | type: 'binary'
139 | })
140 |
141 | saveAs(
142 | new Blob([s2ab(wbout)], {
143 | type: 'application/octet-stream'
144 | }),
145 | 'test.xlsx'
146 | )
147 | }
148 |
149 | export function export_json_to_excel({
150 | multiHeader = [],
151 | header,
152 | data,
153 | filename,
154 | merges = [],
155 | autoWidth = true,
156 | bookType = 'xlsx'
157 | } = {}) {
158 | /* original data */
159 | filename = filename || 'excel-list'
160 | data = [...data]
161 | data.unshift(header)
162 |
163 | for (let i = multiHeader.length - 1; i > -1; i--) {
164 | data.unshift(multiHeader[i])
165 | }
166 |
167 | let ws_name = 'SheetJS'
168 | let wb = new Workbook(),
169 | ws = sheet_from_array_of_arrays(data)
170 |
171 | if (merges.length > 0) {
172 | if (!ws['!merges']) ws['!merges'] = []
173 | merges.forEach((item) => {
174 | ws['!merges'].push(utils.decode_range(item))
175 | })
176 | }
177 |
178 | if (autoWidth) {
179 | /*设置worksheet每列的最大宽度*/
180 | const colWidth = data.map((row) =>
181 | row.map((val) => {
182 | /*先判断是否为null/undefined*/
183 | if (val == null) {
184 | return {
185 | wch: 10
186 | }
187 | } else if (val.toString().charCodeAt(0) > 255) {
188 | /*再判断是否为中文*/
189 | return {
190 | wch: val.toString().length * 2
191 | }
192 | } else {
193 | return {
194 | wch: val.toString().length
195 | }
196 | }
197 | })
198 | )
199 | /*以第一行为初始值*/
200 | let result = colWidth[0]
201 | for (let i = 1; i < colWidth.length; i++) {
202 | for (let j = 0; j < colWidth[i].length; j++) {
203 | if (result[j]['wch'] < colWidth[i][j]['wch']) {
204 | result[j]['wch'] = colWidth[i][j]['wch']
205 | }
206 | }
207 | }
208 | ws['!cols'] = result
209 | }
210 |
211 | /* add worksheet to workbook */
212 | wb.SheetNames.push(ws_name)
213 | wb.Sheets[ws_name] = ws
214 |
215 | let wbout = writeXLSX(wb, {
216 | bookType: bookType,
217 | bookSST: false,
218 | type: 'binary'
219 | })
220 | saveAs(
221 | new Blob([s2ab(wbout)], {
222 | type: 'application/octet-stream'
223 | }),
224 | `${filename}.${bookType}`
225 | )
226 | }
227 |
228 | /**
229 |
230 | @params filterVal: ['id', 'name', 'age']
231 |
232 | @params jsonData: [
233 | { id: 1, name: 'ealien', age: 18 },
234 | { id: 2, name: '哇咔咔', age: 99 },
235 | { id: 3, name: '124', age: 18 },
236 | { id: 4, name: '1', age: 99 },
237 | { id: 5, name: 'nihao', age: 18 },
238 | { id: 6, name: '1838', age: 99 }
239 | ]
240 |
241 |
242 | const tHeader = ['ID', '姓名', '年龄']
243 | @return [
244 | [1, 'ealien', 18],
245 | [2, '哇咔咔', 99],
246 | [3, '124', 18],
247 | [4, '1', 99],
248 | [5, 'nihao', 18],
249 | [6, '1838', 99]
250 | ]
251 | */
252 | export const formatJson = (filterVal, jsonData) => {
253 | return jsonData.map((v) => filterVal.map((j) => v[j]))
254 | }
255 |
256 | /**
257 | const columns = [{ id: 'ID' }, { name: '姓名' }, { age: '年龄' }]
258 |
259 | OR
260 |
261 | const columns = [{ id: 'ID' }, { name: '姓名' }, { action: { title: '操作’, width: '120px' } }]
262 |
263 | return [['ID', '姓名', '年龄'], ['id', 'name', 'age']]
264 |
265 | getFormatDataByColumns(columns)
266 |
267 | getFormatDataByColumns(columns, ['name'])
268 |
269 | */
270 | export const getFormatDataByColumns = (columns, excludeKeys = []) => {
271 | let header = []
272 | let keys = []
273 | columns.forEach((it) => {
274 | const key = Object.keys(it)[0]
275 | const value = Object.values(it)[0]
276 |
277 | if (!excludeKeys.includes(key)) {
278 | header.push(isObject(value) ? value.title : value)
279 | keys.push(key)
280 | }
281 | })
282 |
283 | console.log(header, keys)
284 | return [header, keys]
285 | }
286 |
--------------------------------------------------------------------------------
/src/pages/charts/pie/chart/p.vue:
--------------------------------------------------------------------------------
1 |
410 |
411 |
412 |
416 |
417 |
418 |
419 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 运行
2 |
3 | ### ProTable 封装可以参考
4 | ### https://github.com/TopAlien/base-admin-naive
5 | ### https://github.com/TopAlien/vue3-admin-cli
6 | ### https://procomponents.ant.design/components/table
7 |
8 | ### 1、yarn install
9 |
10 | ### 2、yarn dev
11 |
12 | ## 构建
13 |
14 | ### 1、build:dev 打包开发环境
15 |
16 | ### 2、build:test 打包测试环境
17 |
18 | ### 3、build:prod 打包正式环境
19 |
20 | ... 自定义可以增加 .env.[mode]
21 | ... package.json scripts 对应 --mode [mode]
22 |
23 | ### 功能/技术栈
24 |
25 | - vue@3
26 | - pinia
27 | - vite@6
28 | - less css预处理器, 变量
29 | - 请求使用原生支持的fetch(vueuse/useFetch hook)useFetch二次封装, 不再使用axios
30 | - vue-router@4
31 | - unocss - safelist加载动态icon class
32 | - vueuse
33 | - ant-design-vue@4.x - zh-cn 组件二次封装slot, attrs透传
34 | - smooth-scrollbar自定义滚动条(自定义指令)
35 | - 可配置右侧content接口请加载动画(如图7),见service/index.js showLoading配置
36 | - 图表库 Echarts-v5
37 | - 图表库 G2-5.0
38 | - 简单易用的打印,局部打印 hiprint,直接预览,导出pdf
39 | - lodash-es版 方便vite tree shake, 减少包体积;所以我们在选择第三方库时,要尽可能使用 ESM 版本,可以提升不少性能!
40 | - 切换页面回到顶部,区域滚动router无效
41 | - Org Tree
42 | - Calendar 日历
43 | - idcard 校验身份证[第二代]合法性,获取身份证详细信息(可15位转18位)
44 | - NProgress 页面切换进度条
45 | - 后端动态路由匹配 config 开启后查看示例, 权限更新后刷新即可,无需重新登录
46 | - 基于Modal封装useModal函数式调用
47 | - 浏览器唯一标识,用于游客记录等
48 | - json美化预览/编辑
49 | - 按钮 v-loading(loading动画)自定义指令 - 节流
50 | - 按钮级权限指令 v-auth.[moduleName]
51 | - 关键词高亮组件TextMark组件
52 | - 文本溢出显示...组件
53 | - 数据使用[Apifox云端Mock](https://apifox.com/help/api-mock/cloud-mock)
54 | - Tabs反圆角
55 | - Table区域滚动(自动计算)
56 | - 图片懒加载指令
57 |
58 | ### import.meta.env 访问环境变量,自定义 VITE\_ 开头
59 |
60 | ### 项目部分截图
61 |
62 | 
63 | 
64 | 
65 | 
66 | 
67 | 
68 | 
69 | 
70 | 
71 | 
72 | 
73 | 
74 | 
75 | 
76 | 
77 | 
78 | 
79 |
80 | ### 路由配置
81 |
82 | ```js
83 | const BASE_URL = '/other'[
84 | {
85 | // path必须写完整的路径,要做跳转匹配
86 | path: BASE_URL,
87 | component: Layout,
88 | name: 'Com',
89 | redirect: `${BASE_URL}/list-1/list2-1`, // 不再需要,自动重定向第一个
90 | // icon 为carbon时在,safelist中添加class
91 | // meta: { icon, hideInMenu, title }
92 | meta: {
93 | title: '组件',
94 | // 需要显示到column tab中的分组
95 | isGroup: true,
96 | icon: 'i-carbon-ibm-cloud-transit-gateway'
97 | },
98 | children: [
99 | {
100 | path: `${BASE_URL}/list-1`,
101 | redirect: `${BASE_URL}/list-1/list2-1`,
102 | name: 'List-1',
103 | meta: {
104 | title: '列表-1',
105 | icon: 'i-carbon-list-boxes'
106 | }
107 | }
108 | ]
109 | }
110 | ]
111 | ```
112 |
113 | ## 真香👍
114 |
115 | ### [图表来自](http://ppchart.com/#/)
116 |
117 | ### [插画](https://undraw.co/illustrations)
118 |
119 | ### [404插画](https://error404.fun/)
120 |
121 | ### [打印](https://www.npmjs.com/package/vue-plugin-hiprint)
122 |
123 | ### [数据mock-Apifox](https://apifox.com/)
124 |
125 | ## 开发经验/优化
126 |
127 | 1. 避免整体监听对象,会隐式触发deep。分清楚watch({}), 和 watch(() => {}) 使用场景
128 |
129 | ```js
130 | const watState = reactive({ arr: [], count: 1, str: '123', bo: true })
131 |
132 | // watch(watState.str, () => {})
133 | // 原始值不能直接监听,需要用getter函数
134 | // 引用可以直接监听,会隐式创建deep,用到getter函数,需显示deep监听,否则需要整体替换才触发watch 例: watState.arr = []
135 | watch(
136 | () => watState.arr,
137 | (newVal, oldVal) => {
138 | message.success('触发!')
139 | console.log('-> newVal, oldVal', newVal, oldVal)
140 | }
141 | )
142 |
143 | const onWatch = () => {
144 | watState.arr = [{ name: 'ealien', age: '123', sex: '1' }]
145 | }
146 |
147 | const counter = ref(0)
148 | // 不是原始值不能直接监听吗?啊这...。 别忘了ref访问需要 .value呀。souga
149 | watch(counter, (newVal, oldVal) => {
150 | console.log('-> newVal, oldVal', newVal, oldVal)
151 | })
152 | ```
153 |
154 | 2. 子组件想知道emit父级到底传没传?
155 |
156 | ```vue
157 |
158 | {}" />
159 |
160 |
161 | const emit = defineEmits(['confirm', 'cancel'])
162 |
163 |
164 | const props = defineProps({ onConfirm: { type: Function }, onCancel: { type: Function }, })
165 |
166 |
167 | props.onConfirm props.onCancel
168 | ```
169 |
170 | ## 指令
171 |
172 | 1. v-loading
173 |
174 | ```js
175 | const loading = ``
176 |
177 | /**
178 | * 通过自定义样式(global.less),对 primary 类型按钮,和官方示例一样。事件只执行一次
179 | *
180 | * 默认值1500毫秒
181 | * v-loading="2000"
182 | * v-loading == v-loading="1500"
183 | */
184 | export default {
185 | mounted(el, binding) {
186 | const originInnerHtml = el.innerHTML
187 |
188 | if (binding.value && typeof binding.value !== 'number') {
189 | console.error('自定义时间应为数字 例: v-loading="2000"')
190 | return
191 | }
192 |
193 | el.addEventListener(
194 | 'click',
195 | () => {
196 | if (!el.disabled) {
197 | el.disabled = true
198 | el.innerHTML = `${loading}${originInnerHtml}`
199 |
200 | setTimeout(() => {
201 | el.innerHTML = originInnerHtml
202 | el.disabled = false
203 | }, binding.value || 1500)
204 | }
205 | },
206 | false
207 | )
208 | }
209 | }
210 | ```
211 |
212 | 2. v-auth 按钮权限指令
213 |
214 | ```js
215 | /**
216 | * 设计场景
217 | *
218 | * 1、后台新增权限时选择类型是否是按钮,选择按钮类型。登录后调取接口查出所有按钮类型权限:(我使用接口作为唯一标识)
219 | * response = ['/user/list', '/user/add', '/user/detail/add', '/user/detail/edit']
220 | *
221 | * 指令使用格式
222 | * v-auth="'/user/list'"
223 | * v-auth="['/user/list', '/user/detail/edit']"
224 | *
225 | *
226 | * 2、按照菜单权限层级返回。类似 mock 中的 adminRoutes,再增加类型区分是否是按钮权限即可。
227 | * response = [{ key: 'user', children: [{ key: 'user/list', children: [{ type: 'btn', key: 'api/user/list' }] }] }]
228 | *
229 | * 使用(.[user]修饰符用来快速定位查找,也可以起到命名空间的作用)
230 | *
231 | * 找到命名空间内的
232 | * v-auth.user="'api/user/list'"
233 | * v-auth.user="['api/user/list', 'api/user/list']"
234 | * v-auth="{ user: ["api/user/list", "api/user/add"], setting: [""] }"
235 | * v-auth="{ user: "", setting: "" }"
236 | *
237 | * user和setting模块中任意找到
238 | * v-auth.user.setting="api/user/list"
239 | * v-auth.user.setting="['api/user/list', 'api/user/add']"
240 | *
241 | * tip:要是有 user下面,或者setting下面有某个权限都可以显示按钮这种场景该怎么办
242 | *
243 | *
244 | *
245 | */
246 |
247 | import { isArray, isString, isPlainObject } from 'lodash-es'
248 |
249 | const _mockResRouteData = [
250 | {
251 | key: 'user',
252 | name: '用户管理',
253 | children: [
254 | {
255 | key: 'user/list',
256 | name: '用户列表',
257 | children: [
258 | { type: 'btn', key: 'api/user/list', name: '用户列表查看' },
259 | { type: 'btn', key: 'api/user/detail', name: '用户详情' },
260 | { type: 'btn', key: 'api/user/auth-edit', name: '用户权限编辑' }
261 | ]
262 | },
263 | {
264 | key: 'user/list1',
265 | name: '用户列表1',
266 | children: [
267 | { type: 'btn', key: 'api/user/list1', name: '用户列表查看1' },
268 | { type: 'btn', key: 'api/user/detail1', name: '用户详情1' },
269 | { type: 'btn', key: 'api/user/auth-edit1', name: '用户权限编辑1' }
270 | ]
271 | }
272 | ]
273 | },
274 | {
275 | key: 'setting',
276 | name: '设置',
277 | children: [
278 | {
279 | key: 'setting/auth',
280 | name: '权限设置',
281 | children: [
282 | { type: 'btn', key: 'api/auth/add', name: '新增权限' },
283 | { type: 'btn', key: 'api/auth/edit', name: '编辑权限' },
284 | { type: 'btn', key: 'api/auth/list', name: '权限列表' }
285 | ]
286 | }
287 | ]
288 | },
289 | {
290 | key: '404',
291 | name: '异常页面',
292 | children: [
293 | {
294 | key: 'exception/404',
295 | name: '404页面',
296 | children: [
297 | { type: 'btn', key: 'api/exception/add', name: '新增' },
298 | { type: 'btn', key: 'api/exception/edit', name: '编辑' }
299 | ]
300 | },
301 | {
302 | key: 'exception/503',
303 | name: '503页面'
304 | }
305 | ]
306 | }
307 | ]
308 |
309 | // 模块唯一标识key
310 | const KEY_NAME = 'key'
311 | const findNamesRoutes = (moduleName) => {
312 | return (_mockResRouteData.find((route) => route[KEY_NAME] === moduleName) || {}).children || []
313 | }
314 |
315 | const btnKeys = (routes) => {
316 | const keys = []
317 |
318 | function find(arr) {
319 | arr.forEach((it) => {
320 | // 按钮类型的唯一key
321 | if (it.type === 'btn') {
322 | keys.push(it[KEY_NAME])
323 | }
324 |
325 | if (it.children && it.children.length) {
326 | find(it.children)
327 | }
328 | })
329 | }
330 |
331 | find(routes)
332 |
333 | return keys
334 | }
335 |
336 | /**
337 | * 比对是否有相同项,只要找到一个有相同的,就立即返回(或的关系,所以可以提前返回)
338 | *
339 | * arrModuleValue 必然存在
340 | */
341 | const hasDuplicates = (arr1, arrModuleValue) => {
342 | for (let i = 0, len = arrModuleValue.length; i < len; i++) {
343 | if (arr1.includes(arrModuleValue[i])) {
344 | return true
345 | }
346 | }
347 |
348 | return false
349 | }
350 |
351 | const hasPer = (moduleName, moduleValue) => {
352 | const keys = btnKeys(findNamesRoutes(moduleName))
353 |
354 | if (isString(moduleValue)) {
355 | return keys.includes(moduleValue)
356 | }
357 |
358 | if (isArray(moduleValue) && moduleValue.length > 0) {
359 | return hasDuplicates(keys, moduleValue)
360 | }
361 |
362 | return false
363 | }
364 |
365 | const DOM_MARK = 'data-auth'
366 | const hasMark = (el) => {
367 | return el.getAttribute(DOM_MARK) === 'true'
368 | }
369 |
370 | const setMark = (el) => {
371 | el.setAttribute(DOM_MARK, true)
372 | }
373 |
374 | const removeEl = (el) => {
375 | el && el.parentNode && el.parentNode.removeChild(el)
376 | }
377 |
378 | /**
379 | * 场景2方式实现
380 | */
381 | export default {
382 | mounted(el, binding) {
383 | const { modifiers, value } = binding
384 |
385 | const valueIsPlainObj = isPlainObject(value)
386 | const routeModules = Object.keys(valueIsPlainObj ? value : modifiers)
387 |
388 | if (routeModules.length) {
389 | try {
390 | routeModules.forEach((module) => {
391 | const curModuleValue = valueIsPlainObj ? value[module] : value
392 | if (hasPer(module, curModuleValue)) {
393 | setMark(el)
394 | throw new Error('当前el已打标可立即跳出')
395 | }
396 | })
397 | } catch {}
398 | } else {
399 | // 没有命名空间直接删除,例:v-auth='"api/list"'
400 | removeEl(el)
401 | return
402 | }
403 |
404 | if (!hasMark(el)) {
405 | removeEl(el)
406 | }
407 | },
408 |
409 | updated() {},
410 |
411 | unmounted() {}
412 | }
413 | ```
414 |
415 | 3. v-scrollbar 自定义scrollbar样式,类似mac滚动条
416 |
417 | ```js
418 | import Scrollbar from 'smooth-scrollbar'
419 | import config from '@/config/index.js'
420 |
421 | const extractProp = (prop) => (obj) => (typeof obj === 'undefined' ? undefined : obj[prop])
422 | const extractOptions = extractProp('options')
423 | const extractEl = extractProp('el')
424 |
425 | const bestMatch = (extractor) => (possibilities) =>
426 | extractor(possibilities.find((p) => typeof extractor(p) !== 'undefined'))
427 | const bestEl = bestMatch(extractEl)
428 | const bestOptions = bestMatch(extractOptions)
429 |
430 | /**
431 | v-scrollbar
432 | v-scrollbar="{ el: "" }"
433 | */
434 | export default {
435 | mounted(el, binding) {
436 | if (config.useCustomScrollBar) {
437 | const possibilities = [binding.value]
438 | const targetEl = bestEl(possibilities)
439 | const config = bestOptions(possibilities)
440 |
441 | const scrollY = binding.modifiers.y
442 | const scrollX = binding.modifiers.x
443 | Scrollbar.init(targetEl ? document.querySelector(targetEl) : el)
444 | }
445 | },
446 |
447 | updated(el, binding, vnode, prevVnode) {},
448 |
449 | unmounted(el, binding) {
450 | if (config.useCustomScrollBar) {
451 | const possibilities = [binding.value]
452 | const targetEl = bestEl(possibilities)
453 | Scrollbar.destroy(targetEl ? document.querySelector(targetEl) : el, {})
454 | }
455 | }
456 | }
457 | ```
458 |
459 | 4. v-lazyImg 图片懒加载
460 |
461 | ```
462 | import { useIntersectionObserver } from '@vueuse/core'
463 |
464 | /**
465 | v-lazyImg="'https://via.placeholder.com/250'"
466 | */
467 | export default {
468 | mounted(el, binding) {
469 | const { stop } = useIntersectionObserver(
470 | el,
471 | ([{ isIntersecting }], observerElement) => {
472 | if (isIntersecting) {
473 | stop()
474 |
475 | el.src = binding.value
476 | }
477 | },
478 | { threshold: 0 }
479 | )
480 | }
481 | }
482 | ```
483 |
--------------------------------------------------------------------------------