├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── VUE3-NaiveUI.drawio ├── docs ├── README.md └── screenshot │ ├── demo-403.png │ ├── demo-404.png │ ├── demo-chart.png │ ├── demo-home.png │ ├── demo-icons.png │ └── meeting.gif ├── mock ├── booking │ └── home.js └── index.js ├── package.json ├── postcss.config.js ├── public ├── imgs │ └── meeting.svg ├── index.html └── logo.svg ├── src ├── App.vue ├── basic.main.js ├── basic.router.js ├── components │ ├── Navigation.vue │ ├── README.md │ ├── chart.vue │ ├── common │ │ ├── Banner.vue │ │ └── status.vue │ ├── custom │ │ └── tag.vue │ ├── directive │ │ ├── Permission.js │ │ ├── README.md │ │ └── index.js │ ├── mixin │ │ ├── Authorize.js │ │ ├── Pagination.js │ │ └── README.md │ ├── naive-ui │ │ ├── Application.vue │ │ ├── NoticeProvider.vue │ │ └── Theme.vue │ ├── uploader.vue │ └── with-role.vue ├── main.js ├── pages │ ├── README.md │ ├── meeting │ │ ├── Dashboard.vue │ │ ├── Index.vue │ │ ├── Main.vue │ │ ├── Meeting.vue │ │ ├── README.md │ │ ├── Room.vue │ │ ├── main.js │ │ └── widget │ │ │ ├── data-edit.js │ │ │ ├── meeting-edit.vue │ │ │ ├── meeting-mine.vue │ │ │ └── room-edit.vue │ └── project │ │ ├── Main.vue │ │ ├── README.md │ │ ├── main.js │ │ └── views │ │ └── Home.vue ├── service │ └── Auth.js ├── store │ ├── index.js │ ├── uiSetting.js │ └── userStore.js ├── theme │ ├── naive.less │ ├── tailwind-simple.css │ └── tailwind.css ├── util │ ├── http.js │ ├── index.js │ ├── module │ │ ├── date.js │ │ ├── index.js │ │ └── store.js │ └── ui-tool.js └── views │ ├── @COMMON │ ├── 401.vue │ ├── 403.vue │ ├── 404.vue │ └── README.md │ ├── Main.vue │ ├── Window.vue │ └── demo │ ├── Chart.vue │ ├── Icons.vue │ ├── Index.vue │ └── Role.vue ├── tailwind.config.js └── vue.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # https://github.com/jokeyrhyme/standard-editorconfig 4 | 5 | # top-most EditorConfig file 6 | root = true 7 | 8 | # defaults 9 | [*] 10 | charset = utf-8 11 | end_of_line = lf 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | indent_size = 4 15 | indent_style = space 16 | 17 | [*.md] 18 | trim_trailing_whitespace = false 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | .vscode 6 | 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | 15 | package-lock.json 16 | yarn.lock 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | static.zip -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Anton Komarev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

🎉 VUE3 NAIVE STARTER 🎉

3 | 4 | ![Language](https://img.shields.io/github/languages/top/0604hx/vue3-naive-starter?logo=javascript&color=blue) 5 | ![License](https://img.shields.io/badge/License-MIT-green) 6 | ![LastCommit](https://img.shields.io/github/last-commit/0604hx/vue3-naive-starter?color=blue&logo=github) 7 | 8 |
9 | 10 | > [VUE3](https://cn.vuejs.org/) + [Naive UI](https://www.naiveui.com) 快速开发框架 11 | 12 | 13 |
14 | 15 | ## 功能 / FEATURE 🎉 16 | 17 |
18 | 19 | - ✅ 基于 Vue3、Naive-UI、Tailwind CSS、Echarts 5 20 | - ✅ 纯 `JavaScript` 21 | - ✅ 集成 `Mock`、多页面等配置 22 | - ⭕ 敬请期待…… 23 | 24 |
25 | 26 | ## 使用方法 / HOW-TO-USE 📖 27 | 28 |
29 | 30 | ```shell 31 | # 先安装依赖 `npm i`(建议使用) OR `yarn install` 32 | 33 | npm run serve 34 | # 以 MOCK 模式启动(无需后台服务) 35 | npm run serve:mock 36 | # 打包(保存到 dist 目录) 37 | npm run build 38 | ``` 39 | 40 |
41 | 42 | ## 运行截图 / SCREEN-SHOT 🖼️ 43 | 44 |
45 | 46 | ![](docs/screenshot/demo-home.png) 47 | 48 | ![](docs/screenshot/demo-icons.png) 49 | 50 | ![](docs/screenshot/demo-chart.png) 51 | 52 | ![](docs/screenshot/demo-403.png) 53 | 54 | ## 多页面演示 55 | 56 | ### 会议室预约系统 57 | 58 | ![](docs/screenshot/meeting.gif) 59 | 60 |
61 | 62 | ## 更新记录 / CHANGELOG 🕒 63 | 64 |
65 | 66 | ## 2022-11-02 67 | 68 | - [x] 增加自定义指令`权限控制` 69 | -------------------------------------------------------------------------------- /VUE3-NaiveUI.drawio: -------------------------------------------------------------------------------- 1 | dZHBDoIwDIafZndGlYQzol48cfC8sMqWDErGDOjTC9kQF/W07vv/rmvLoGinkxW9upBEw9JETgwOLE055DAfC3l4knPuQWO1DKYNVPqJASaB3rXEITI6IuN0H8Oaug5rFzFhLY2x7UYmrtqLBr9AVQvzTa9aOhUoz/JNOKNu1Fo62++80orVHVoZlJA0fiAoGRSWyPmonQo0y/TWwfi84x/1/TOLnfuRMAfb2/MlWhGULw== -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## 自定义指令 3 | > 指令保存到 `src/components/directive` 目录 4 | -------------------------------------------------------------------------------- /docs/screenshot/demo-403.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0604hx/vue3-naive-starter/be69f91be25cc030c9d48101324c9f460c6c0781/docs/screenshot/demo-403.png -------------------------------------------------------------------------------- /docs/screenshot/demo-404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0604hx/vue3-naive-starter/be69f91be25cc030c9d48101324c9f460c6c0781/docs/screenshot/demo-404.png -------------------------------------------------------------------------------- /docs/screenshot/demo-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0604hx/vue3-naive-starter/be69f91be25cc030c9d48101324c9f460c6c0781/docs/screenshot/demo-chart.png -------------------------------------------------------------------------------- /docs/screenshot/demo-home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0604hx/vue3-naive-starter/be69f91be25cc030c9d48101324c9f460c6c0781/docs/screenshot/demo-home.png -------------------------------------------------------------------------------- /docs/screenshot/demo-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0604hx/vue3-naive-starter/be69f91be25cc030c9d48101324c9f460c6c0781/docs/screenshot/demo-icons.png -------------------------------------------------------------------------------- /docs/screenshot/meeting.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0604hx/vue3-naive-starter/be69f91be25cc030c9d48101324c9f460c6c0781/docs/screenshot/meeting.gif -------------------------------------------------------------------------------- /mock/booking/home.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 集成显卡 3 | * @Date: 2021-09-16 16:17:29 4 | * @Last Modified by: 集成显卡 5 | * @Last Modified time: 2022-10-08 13:27:21 6 | * 7 | * mockjs语法 https://www.jianshu.com/p/533a869c808c 8 | * 使用 mockjs 随机生成大批量常用字段的值 https://blog.csdn.net/wuyujin1997/article/details/111656446 9 | */ 10 | import Mock from "mockjs" 11 | const R = Mock.Random 12 | 13 | const ROOMS = "meeting.room" 14 | const MEETINGS = "meeting.list" 15 | 16 | let getRooms = ()=> Store.getList(ROOMS) 17 | let meetings = ()=> Store.getList(MEETINGS) 18 | let toHour = i=>i<10?("0"+i):i 19 | let getId = opts=> JSON.parse(opts.body).id 20 | 21 | /** 22 | * 修改或者删除指定预约 23 | * @param {*} opts mock 传递过来的参数对象 24 | * @param {*} modifyFunc 修改函数或者 true(删除元素) 25 | */ 26 | let modifyMeeting = (opts, modifyFunc)=>{ 27 | let id = getId(opts) 28 | let list = meetings() 29 | 30 | if(typeof(modifyFunc) == 'function'){ 31 | let m = list.filter(m=> m.id==id)[0] 32 | if(!m) throw Error(`ID=${id} 的会议预约不存在`) 33 | 34 | modifyFunc(m) 35 | } 36 | else if(modifyFunc == true){ 37 | let index = list.map(m=>m.id).indexOf(id) 38 | if(index > -1) 39 | list.splice(index, 1) 40 | } 41 | else 42 | throw Error("参数 2 必须为 函数 或者 TRUE") 43 | 44 | Store.setList(MEETINGS, list) 45 | } 46 | 47 | export default { 48 | 'overview': opts=>{ 49 | return { 50 | roles : ['ADMIN'], 51 | name : "集成显卡" 52 | } 53 | }, 54 | 'room/list' : opts=> getRooms(), 55 | 'room/add' : opts=>{ 56 | let r = JSON.parse(opts.body) 57 | let rooms = getRooms() 58 | if(r.id){ 59 | rooms[rooms.findIndex(v=>v.id==r.id)] = r 60 | } 61 | else{ 62 | r.id = Date.now() 63 | rooms.push(r) 64 | } 65 | Store.setList(ROOMS, rooms) 66 | }, 67 | 'room/delete': opts=>{ 68 | let id = getId(opts) 69 | let rooms = getRooms() 70 | let index = rooms.map(r=>r.id).indexOf(id) 71 | if(index > -1) 72 | rooms.splice(index, 1) 73 | 74 | Store.setList(ROOMS, rooms) 75 | }, 76 | /* 77 | 会议预约相关 78 | */ 79 | 'meeting/overview': opts=>{ 80 | let day = JSON.parse(opts.body).day 81 | let mList = meetings().filter(m=>m.day==day) 82 | return { 83 | hourBegin: 9, 84 | hourEnd: 20, 85 | rooms: getRooms().map(v=>{ 86 | let label = v.name 87 | let begin = `${toHour(9+Math.floor(Math.random()*10))}:00` 88 | return { 89 | label, 90 | value: v.id, 91 | color: "#18a058",//colors[v], 92 | items: mList.filter(m=>m.roomId==v.id) 93 | } 94 | }) 95 | } 96 | }, 97 | 'meeting/list': opts=>{ 98 | return meetings() 99 | }, 100 | 'meeting/mine': opts=>{ 101 | return meetings() 102 | }, 103 | //提交会议预约,不做冲突处理 104 | 'meeting/add': opts=>{ 105 | let m = JSON.parse(opts.body) 106 | let room = getRooms().filter(v=>v.id==m.roomId)[0] 107 | if(!room) throw Error(`会议室 ${m.roomId} 不存在`) 108 | 109 | m.uid = "admin" 110 | m.uname = "集成显卡(admin)" 111 | m.room = room.name 112 | m.status = room.special? 0 : 1 113 | m.createOn = D.date() 114 | let list = meetings() 115 | list.push(m) 116 | 117 | Store.setList(MEETINGS, list) 118 | }, 119 | 'meeting/delete': opts=>{ 120 | modifyMeeting(opts, true) 121 | }, 122 | 'meeting/confirm': opts=>{ 123 | modifyMeeting(opts, m=> m.status = 1) 124 | }, 125 | 'meeting/dashboard': opts=>{ 126 | let rooms = getRooms().map(r=>r.name) 127 | let res = Mock.mock({ 128 | "data|40-100": [{ 129 | id: "@increment", 130 | title: "@cword(5,20)", 131 | 'status|0-1': 1, 132 | 'uid|000101-000999': 1, 133 | uname: "@cname", 134 | day:"@date(2022-MM-dd)" 135 | }] 136 | }) 137 | let meetings = res.data 138 | meetings.forEach(m=> m.room = rooms[R.integer(0, rooms.length-1)]) 139 | return { data: {rooms, meetings}, message:"" } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /mock/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Mock配置文件 3 | **/ 4 | // 首先引入Mock 5 | import Mock from 'mockjs' 6 | 7 | const COLOR = `color:white;background:green;padding:0 10px 0 10px` 8 | 9 | // 设置拦截ajax请求的相应时间 10 | Mock.setup({ 11 | timeout: '100-300' 12 | }) 13 | 14 | let configArray = [] 15 | 16 | let _GET = "get" 17 | let _POST = "post" 18 | let _RESULT = "result" 19 | 20 | // 使用webpack的require.context()遍历所有Mock文件 21 | const files = require.context('.', true, /\.js$/); 22 | files.keys().forEach((key) => { 23 | if (key === './index.js') return 24 | //排查非指定模块 25 | if (!key.startsWith(`./${_MODULE_}`)) return 26 | 27 | configArray = configArray.concat({key, routes:files(key).default}); 28 | }) 29 | //https://vitejs.cn/guide/features.html#glob-import 30 | // const modules = import.meta.globEager('./**/*.js') 31 | // Object.keys(modules).forEach(key=>{ 32 | // if (key === './index.js') return 33 | // configArray = configArray.concat(modules[key].default || {}); 34 | // }) 35 | 36 | let buildResult = data=>{ 37 | if(typeof(data)=='object' && 'data' in data && ('message' in data || 'total' in data)) return Object.assign({success:true}, data) 38 | return {success:true, data } 39 | } 40 | 41 | /** 42 | * 特殊情况下,需要对 path 进行转换(本项目就是需要全部转换成 /app/api?A=xxxx) 43 | * 如无特殊直接返回 path 44 | * @param {*} path 45 | */ 46 | let buildMockPath = path=>{ 47 | return `/app/api?A=${path}` 48 | } 49 | 50 | console.group("[MOCK 数据注册]") 51 | // 注册所有的Mock服务 52 | configArray.forEach(({key, routes}) => { 53 | let prefix = key.split("/")[1] 54 | for (let [path, target] of Object.entries(routes)) { 55 | path = `/${prefix}/${path}` 56 | Mock.mock( 57 | new RegExp('^' + path), 58 | option=>{ 59 | console.debug(`%c[请求MOCK] ${option.type}|${option.url}\t参数=${option.body}`, COLOR) 60 | let data = target(option) 61 | return buildResult(data) 62 | } 63 | ) 64 | console.log(`\t%c${path}`, COLOR) 65 | } 66 | }) 67 | console.groupEnd() 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-naive-starter", 3 | "version": "1.0.0", 4 | "appName": "VUE3 + Naive UI快速开发框架", 5 | "description": "VUE3 + Naive UI 快速开发框架", 6 | "main": "index.js", 7 | "scripts": { 8 | "serve": "vue-cli-service serve", 9 | "serve:mock": "vue-cli-service serve --mode test", 10 | "build": "vue-cli-service build" 11 | }, 12 | "keywords": [ 13 | "VUE3", 14 | "Naive", 15 | "UI" 16 | ], 17 | "author": "0604hx", 18 | "license": "MIT", 19 | "dependencies": { 20 | "@vicons/fa": "^0.12.0", 21 | "axios": "^1.4.0", 22 | "core-js": "^3.31.0", 23 | "countup": "^1.8.2", 24 | "dayjs": "^1.11.9", 25 | "echarts": "^5.4.2", 26 | "highlight.js": "^11.8.0", 27 | "mitt": "^3.0.0", 28 | "naive-ui": "^2.34.4", 29 | "pinia": "^2.1.4", 30 | "postcss": "^8.4.24", 31 | "tailwindcss": "^3.3.2", 32 | "vue": "^3.3.4", 33 | "vue-router": "^4.2.2" 34 | }, 35 | "devDependencies": { 36 | "@vue/cli-plugin-router": "^5.0.8", 37 | "@vue/cli-service": "^5.0.8", 38 | "archiver": "^5.3.1", 39 | "autoprefixer": "^10.4.14", 40 | "blueimp-md5": "^2.19.0", 41 | "crypto-js": "^4.1.1", 42 | "less": "^4.1.3", 43 | "less-loader": "^11.1.3", 44 | "mockjs": "^1.1.0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/imgs/meeting.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 44 | 45 | 52 | -------------------------------------------------------------------------------- /src/basic.main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import naive from 'naive-ui' 3 | 4 | import App from './App.vue' 5 | 6 | /** 7 | * 如果不想引入 tailwind 相关的插件而是直接引入 tailwind-simple.css 8 | * 9 | * import '@T/tailwind.css' 10 | * 11 | * 删除 tailwind 相关的依赖:tailwindcss、autoprefixer、postcss 12 | * 还有文件 postcss.config.js、tailwind.config.js 13 | */ 14 | import '@T/tailwind.css' 15 | import '@T/naive.less' 16 | 17 | // 全局工具配置 18 | import "@U" 19 | 20 | import { setupDirectives } from '@C/directive' 21 | 22 | const isProduction = process.env.NODE_ENV == 'production' 23 | 24 | /** 25 | * 向 window 注入全局对象 Config 26 | * @param {*} ps 27 | */ 28 | const customConfig = (ps = {}) => { 29 | window.Config = Object.assign( 30 | { 31 | JSON: false, //是否以 JSON 格式发送请求,默认 false 32 | 33 | WATERMARK: true, //是否显示水印 34 | AUTH_URL: "", //远程授权信息获取地址 35 | UI_NOTICE_PLACEMENT: "top-right", //通知框弹出位置 36 | appName: window.document.title || _APPNAME_, //应用名称 37 | }, 38 | ps 39 | ) 40 | } 41 | 42 | const loadRole = url => new Promise((onOk, onFail) => { 43 | RESULT( 44 | url, {}, 45 | d => { 46 | let account = Object.assign({ roles: [] }, d.data) 47 | //锁定用户对象,不支持修改 48 | Object.keys(account).forEach(k => { 49 | Object.defineProperty(account, k, { value: account[k], writable: false, enumerable: true, configurable: true }) 50 | }) 51 | window.User = account 52 | onOk() 53 | }, 54 | { 55 | fail(e) { 56 | // throw Error(`无法从 ${url} 获取用户信息`, e) 57 | console.error(`无法从 ${url} 获取用户信息:`, e) 58 | onFail(`无法从 ${url} 获取用户信息`) 59 | return true 60 | } 61 | } 62 | ) 63 | }) 64 | 65 | /** 66 | * 初始化应用 67 | * @param {*} routerPath 路由对象 68 | * @param {*} config 自定义配置,详见 customConfig 方法 69 | * @param {*} enables 开关项 70 | */ 71 | export async function initApp(routerPath, config={}, enables={}) { 72 | customConfig(config) 73 | enables = Object.assign({banner:true, store: true}, enables) 74 | if(process.env.NODE_ENV == "test"){ 75 | window.isMock = true 76 | // 导入mock 77 | require('../mock') 78 | } 79 | 80 | const app = createApp(App) 81 | 82 | if(enables.store){ 83 | //============================================================ 84 | //初始化 store 85 | //按需开启 86 | //============================================================ 87 | const { setupStore } = require("@/store") 88 | setupStore(app) 89 | } 90 | 91 | if(window.Config.AUTH_URL){ 92 | await loadRole(window.Config.AUTH_URL).catch(e=> setTimeout(()=> M.notice.error(e), 1000)) 93 | } 94 | 95 | if(typeof(routerPath) == 'object' && typeof(routerPath.beforeEach)=='function'){ 96 | app.use(routerPath) 97 | } 98 | else { 99 | const router = require(routerPath) 100 | if(!isProduction) console.debug(`从 ${routerPath} 加载路由信息...`) 101 | app.use(router) 102 | } 103 | 104 | app.use(naive) 105 | setupDirectives(app) 106 | 107 | app.mount('#app') 108 | 109 | if(isProduction){ 110 | // VUE3 全局异常处理 111 | app.config.errorHandler = (err, vm, info)=>{ 112 | console.error(err) 113 | let nodeInfo = "" 114 | if(vm.rawNode){ 115 | nodeInfo = ` 116 |
节点信息
117 |
${JSON.stringify(vm.rawNode)}
118 | ` 119 | } 120 | window.M.dialog({ 121 | type: 'error', 122 | style:{width:"640px"}, 123 | title:"应用执行出错", 124 | content: UI.html(` 125 |
126 |
很遗憾,当前应用执行过程出现无法自处理的异常 🐛
127 |
请尝试刷新页面重试,如果问题依旧请联系技术人员进行处理
128 |
129 | 130 |
131 |
异常信息
132 |
${err}
133 | ${nodeInfo} 134 |
135 | `) 136 | }) 137 | } 138 | } 139 | 140 | // 显示横幅 141 | if(enables.banner) { 142 | console.debug( 143 | `%c欢迎使用 · ${_APPNAME_}(UI) · ${process.env.NODE_ENV != 'production'? ("环境变量="+JSON.stringify(process.env)) : ""}`, 144 | "background:green;color:white;padding:20px 40px 20px 40px; font-size:14px;font-family:微软雅黑" 145 | ) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/basic.router.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | 3 | import WindowView from "@V/Window.vue" 4 | import { checkRole } from "@S/Auth" 5 | 6 | let isProduction = process.env.NODE_ENV == 'production' 7 | 8 | /** 9 | * 10 | * @param {*} mainComponent 11 | * @param {*} ps 12 | * homePage 默认跳转页面 13 | * mainRoutes 主路由 14 | * blankRoutes 全页面路由(适合做诸如 登陆页面 等没有导航的页面) 15 | * windowRoutes 新开窗口路由(具备 footer) 16 | * 17 | * @returns router 对象,可以在此基础上添加钩子(如 beforeEach) 18 | */ 19 | export default function ( mainComponent, ps) { 20 | ps = Object.assign({mainRoutes:[], blankRoutes:[], windowRoutes:[], homePage: "/home"}, ps||{}) 21 | 22 | let appRouter = { 23 | path: "/", 24 | name: "main", 25 | redirect: ps.homePage, 26 | component: mainComponent, 27 | children: [ 28 | ...ps.mainRoutes, 29 | { path: '/401', name: "401", component: () => import('@V/@COMMON/401.vue') }, 30 | { path: '/403', name: "403", component: () => import('@V/@COMMON/403.vue') }, 31 | { path: '/404', name: "404", component: () => import('@V/@COMMON/404.vue') }, 32 | ] 33 | } 34 | 35 | /** 36 | * 通过新窗口打开的路由(仅包含页面 footer ) 37 | */ 38 | let windowRouter = { 39 | path: '/', 40 | component: WindowView, 41 | children: ps.windowRoutes 42 | } 43 | 44 | let routes = [ 45 | ...ps.blankRoutes, 46 | appRouter, 47 | windowRouter, 48 | { 49 | path: "/*", 50 | redirect: "/404" 51 | } 52 | ] 53 | 54 | const router = createRouter({ 55 | history: createWebHashHistory(), 56 | routes 57 | }) 58 | 59 | router.beforeEach((to, from, next) => { 60 | //判断权限 61 | if (to.meta.role && !checkRole(to.meta.role)) { 62 | console.debug(`☹ ${to.name} (${to.fullPath}) 需要权限 ${to.meta.role},请联系管理员授权 ☹`) 63 | return next({ name: P403 }) 64 | } 65 | window.document.title = (to.meta.title?`${to.meta.title} · `:"") + window.Config.appName 66 | next() 67 | // if (to.name != P401 && !window.TOKEN) { 68 | // console.debug(`检测到用户未登录,即将跳转到 401 页面`) 69 | // next({ name: P401 }) 70 | // } 71 | // else { 72 | // //判断权限 73 | // if (to.meta.role && !checkRole(to.meta.role)) { 74 | // console.debug(`☹ ${to.name} (${to.fullPath}) 需要权限 ${to.meta.role},请联系管理员授权 ☹`) 75 | // next({ name: P403 }) 76 | // } 77 | // next() 78 | // } 79 | }) 80 | 81 | return router 82 | } 83 | -------------------------------------------------------------------------------- /src/components/Navigation.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 62 | 63 | 93 | 94 | 101 | -------------------------------------------------------------------------------- /src/components/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0604hx/vue3-naive-starter/be69f91be25cc030c9d48101324c9f460c6c0781/src/components/README.md -------------------------------------------------------------------------------- /src/components/chart.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 142 | -------------------------------------------------------------------------------- /src/components/common/Banner.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /src/components/common/status.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | -------------------------------------------------------------------------------- /src/components/custom/tag.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /src/components/directive/Permission.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 权限判断指令,如组件标记了 v-role="ADMIN" 需要 ADMIN 权限方可显示 3 | */ 4 | export const Role = { 5 | mounted(el, binding) { 6 | const { value } = binding 7 | /** 8 | * 由于自定义指令无法作用于自定义组件上(即智能用于 div、span 等标准元素) 9 | * 所以移除 dom 元素时,直接将父元素移除 10 | */ 11 | if (!checkRole(value)) el.parentNode && el.parentNode.remove() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/directive/README.md: -------------------------------------------------------------------------------- 1 | # 自定义指令 2 | -------------------------------------------------------------------------------- /src/components/directive/index.js: -------------------------------------------------------------------------------- 1 | import * as PermissionDirectives from "./Permission" 2 | 3 | export function setupDirectives(app) { 4 | console.groupCollapsed("[注册自定义指令]") 5 | let directives = [PermissionDirectives] 6 | directives.forEach(ds=>{ 7 | Object.keys(ds).forEach(key => { 8 | app.directive(key, ds[key]) 9 | console.debug(`自定义指令\t${key}`) 10 | }) 11 | }) 12 | console.groupEnd() 13 | } 14 | -------------------------------------------------------------------------------- /src/components/mixin/Authorize.js: -------------------------------------------------------------------------------- 1 | import { ref, onMounted,reactive, onBeforeMount } from 'vue' 2 | import { useRouter } from 'vue-router' 3 | 4 | import { useUser } from "@Store/userStore" 5 | 6 | const user = useUser() 7 | 8 | /** 9 | * 判断是否具备某个角色,否则跳转到指定的页面 10 | * @param {*} role 11 | * @param {*} jumpTo 12 | */ 13 | export function needRole(role, jumpTo="403"){ 14 | let roles = ref([]) 15 | let authed = ref(false) 16 | 17 | onBeforeMount(async ()=>{ 18 | const router = useRouter() 19 | 20 | roles.value = await user.loadRole() 21 | authed.value= roles.value.includes(role) 22 | if(!authed.value) { 23 | console.debug(`%c当前页面(${router.currentRoute.value.path})需要角色 ${role} !!`, "color:white;background:red;padding:10px") 24 | router.replace({name:"403"}) 25 | } 26 | }) 27 | 28 | return { roles, authed } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/mixin/Pagination.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 集成显卡 3 | * @Date: 2022-03-31 17:50:26 4 | * @Last Modified by: 集成显卡 5 | * @Last Modified time: 2022-08-26 13:03:54 6 | * 7 | * 分页复用模块 8 | */ 9 | 10 | import { ref, onMounted,reactive, createVNode } from 'vue' 11 | 12 | export default (api, autoLoad=true, loader=undefined)=>{ 13 | let ps = typeof(api) == 'object'? api: {url: api, form:{}} 14 | let beans = ref([]) 15 | let pagination = reactive({ 16 | loading: false, 17 | page:1, 18 | pageSize: 20, 19 | showSizePicker:true, 20 | pageSizes: [20, 50, 100], 21 | itemCount:0, 22 | prefix: info=> createVNode('div', {}, `加载 ${beans.value.length} 条数据(数据总数 ${info.itemCount})`), 23 | onChange: page=> { 24 | pagination.page = page 25 | refresh() 26 | }, 27 | onUpdatePageSize : pageSize => { 28 | pagination.pageSize = pageSize 29 | refresh() 30 | } 31 | }) 32 | let form = ref(ps.form||{}) 33 | 34 | let refresh = loader || function(){ 35 | let p = {page: pagination.page, pageSize: pagination.pageSize} 36 | pagination.loading = true 37 | RESULT(ps.url, {form: form.value, pagination: p}, d=>{ 38 | beans.value = d.data 39 | pagination.itemCount = d.total 40 | pagination.loading = false 41 | console.debug(`分页信息加载完成`, ps.url, d) 42 | }) 43 | } 44 | 45 | onMounted(() => { 46 | if(autoLoad) refresh() 47 | }) 48 | 49 | return { beans, pagination, form, refresh } 50 | } 51 | -------------------------------------------------------------------------------- /src/components/mixin/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0604hx/vue3-naive-starter/be69f91be25cc030c9d48101324c9f460c6c0781/src/components/mixin/README.md -------------------------------------------------------------------------------- /src/components/naive-ui/Application.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 35 | -------------------------------------------------------------------------------- /src/components/naive-ui/NoticeProvider.vue: -------------------------------------------------------------------------------- 1 | 2 | 122 | -------------------------------------------------------------------------------- /src/components/naive-ui/Theme.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 25 | -------------------------------------------------------------------------------- /src/components/uploader.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/with-role.vue: -------------------------------------------------------------------------------- 1 | 4 | 7 | 8 | 17 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { initApp } from "./basic.main" 2 | import BuildRouter from "./basic.router" 3 | 4 | import Main from "@V/Main.vue" 5 | 6 | let router = BuildRouter( 7 | Main, 8 | { 9 | homePage: "/demo-home", 10 | mainRoutes: [ 11 | { path: '/demo-home', name: 'demo-home', component: () => import('@V/demo/Index.vue') }, 12 | { path: '/demo-icon', name: 'demo-icon', component: () => import('@V/demo/Icons.vue') }, 13 | { path: '/demo-chart', name: 'demo-chart', component: () => import('@V/demo/Chart.vue') }, 14 | { path: '/demo-role', name: 'demo-role', component: () => import('@V/demo/Role.vue') } 15 | ] 16 | } 17 | ) 18 | 19 | initApp(router) 20 | -------------------------------------------------------------------------------- /src/pages/README.md: -------------------------------------------------------------------------------- 1 | # 多页面 2 | 3 | 页面均可用 `public/index.html`、 `App.vue` 作为入口 4 | -------------------------------------------------------------------------------- /src/pages/meeting/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 94 | -------------------------------------------------------------------------------- /src/pages/meeting/Index.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 124 | 125 | 126 | 142 | -------------------------------------------------------------------------------- /src/pages/meeting/Main.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 31 | -------------------------------------------------------------------------------- /src/pages/meeting/Meeting.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 48 | -------------------------------------------------------------------------------- /src/pages/meeting/README.md: -------------------------------------------------------------------------------- 1 | # 会议室使用预览 2 | 3 | 显示各会议室(共10个)在指定天内的预约情况 4 | 5 | 6 | ![](/docs/screenshot/meeting-room.png) 7 | 8 | # 功能 9 | 10 | ## 会议室管理 11 | > 🔒`ADMIN` 12 | 13 | 字段名|中文名|类型|非空|默认值|说明 14 | -|-|-|-|-|- 15 | id|编号(主键)|String|是||房间编号,不可重复 16 | name|名称|String|是||房间名称 17 | location|位置|String|否||位置信息 18 | tag|标签|String|否||属性标签(以英文逗号隔开),如:视频,投影仪 19 | scale|规模|int|是|10|会议室人数规模 20 | special|特殊|boolean|是|false|是否为特殊房间,需要管理员审核才能预约成功 21 | summary|说明|String|否||详细说明信息 22 | 23 | ## 预约 24 | > 任意用户均可操作 25 | 26 | 字段名|类型|非空|默认值|说明 27 | -|-|-|-|-|- 28 | title|String|是||会议主题 29 | roomId|String|是||会议室编号 30 | room|String|是||会议室名称 31 | uid|String|是||预约人ID 32 | uname|String|是||预约人名称 33 | begin|String|是||开始时间,格式为 HH:mm 34 | end|String|是||结束时间,格式为 HH:mm 35 | cell|int|是|1|持续时段(按半小时一个时段计算),系统自动计算 36 | status|int|是|0|状态,0=待确认,1=已确认,2=已取消 37 | summary|String|否||备注信息 38 | 39 | ## 预约总览 40 | -------------------------------------------------------------------------------- /src/pages/meeting/Room.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 72 | -------------------------------------------------------------------------------- /src/pages/meeting/main.js: -------------------------------------------------------------------------------- 1 | import { initApp } from "@/basic.main" 2 | import BuildRouter from "@/basic.router" 3 | 4 | import Main from "@/pages/meeting/Main.vue" 5 | 6 | let router = BuildRouter( 7 | Main, 8 | { 9 | homePage: "/home", 10 | mainRoutes: [ 11 | { path: '/home', name: 'home', component: () => import('@/pages/meeting/Index.vue') }, 12 | { path: '/room', name: 'room', component: () => import('@/pages/meeting/Room.vue') }, 13 | { path: '/meeting', name: 'meeting', component: () => import('@/pages/meeting/Meeting.vue') }, 14 | { path: '/dashboard', name: 'dashboard', component: () => import('@/pages/meeting/Dashboard.vue') }, 15 | ] 16 | } 17 | ) 18 | 19 | initApp(router) 20 | -------------------------------------------------------------------------------- /src/pages/meeting/widget/data-edit.js: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue' 2 | 3 | /** 4 | * 5 | * @param {*} beforeEdit 6 | * @param {*} afterEdit 7 | */ 8 | export function enableEdit(beforeEdit=()=>{}, afterEdit()=>{}) { 9 | let bean = ref({}) 10 | let isNew= ref(false) 11 | 12 | let toEdit = row=>{ 13 | isNew.value= !row || !row.id 14 | bean.value = row 15 | 16 | beforeEdit() 17 | } 18 | 19 | defineExpose({ toEdit }) 20 | 21 | return { bean, isNew } 22 | } 23 | -------------------------------------------------------------------------------- /src/pages/meeting/widget/meeting-edit.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 66 | -------------------------------------------------------------------------------- /src/pages/meeting/widget/meeting-mine.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 56 | -------------------------------------------------------------------------------- /src/pages/meeting/widget/room-edit.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 73 | -------------------------------------------------------------------------------- /src/pages/project/Main.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 29 | -------------------------------------------------------------------------------- /src/pages/project/README.md: -------------------------------------------------------------------------------- 1 | # IT 项目管理系统 2 | -------------------------------------------------------------------------------- /src/pages/project/main.js: -------------------------------------------------------------------------------- 1 | import { initApp } from "@/basic.main" 2 | import BuildRouter from "@/basic.router" 3 | 4 | import Main from "@/pages/project/Main.vue" 5 | 6 | let router = BuildRouter( 7 | Main, 8 | { 9 | homePage: "/home", 10 | mainRoutes: [ 11 | { path: '/home', name: 'home', component: () => import('@/pages/project/views/Home.vue') }, 12 | ] 13 | } 14 | ) 15 | 16 | initApp(router) 17 | -------------------------------------------------------------------------------- /src/pages/project/views/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/service/Auth.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 集成显卡 3 | * @Date: 2022-11-02 13:34:30 4 | * @Last Modified by: 集成显卡 5 | * @Last Modified time: 2022-11-02 13:34:30 6 | */ 7 | 8 | export function checkRole(requireRole) { 9 | let roles = window.User ? (User.roles || []) : [] 10 | return typeof (requireRole) === 'string' ? roles.includes(requireRole) : requireRole(roles) 11 | } 12 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | 3 | const store = createPinia() 4 | 5 | export function setupStore(app) { 6 | app.use(store) 7 | } 8 | 9 | export { store } 10 | -------------------------------------------------------------------------------- /src/store/uiSetting.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 0604hx/集成显卡 3 | * @Date: 2022-03-26 21:58:12 4 | * @Last Modified by: 集成显卡 5 | * @Last Modified time: 2023-07-04 15:19:17 6 | * 7 | * UI 相关配置 8 | */ 9 | import { defineStore } from 'pinia' 10 | 11 | let detectTheme = v =>{ 12 | let theme = v || H.store.get("ui.theme", "auto") 13 | let _dark = theme==='dark' 14 | if(!_dark && theme==='auto'){ 15 | let hour = new Date().getHours() 16 | _dark = hour >= 18 || hour<=8 17 | } 18 | return _dark?"dark":"light" 19 | } 20 | 21 | export const useUISetting = defineStore('ui', { 22 | state: () => ({ 23 | theme: detectTheme(), // light,dark,auto(自动) 24 | }), 25 | getters: { 26 | getTheme() { 27 | return this.theme 28 | } 29 | }, 30 | actions: { 31 | updateTheme(theme) { 32 | this.theme = detectTheme(theme) 33 | H.store.set("ui.theme", theme) 34 | } 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /src/store/userStore.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 集成显卡 3 | * @Date: 2022-08-25 08:36:34 4 | * @Last Modified by: 集成显卡 5 | * @Last Modified time: 2022-09-01 16:19:41 6 | * 7 | * 用户相关操作 8 | * 1、获取当前用户角色 9 | * 2、判断是否具备某个角色 10 | */ 11 | 12 | import { defineStore } from 'pinia' 13 | 14 | let inited = false 15 | 16 | export const useUser = defineStore("user", { 17 | state: () => ({ 18 | roles : [], 19 | name : "" 20 | }), 21 | // getters: { 22 | // getRoles (){ 23 | // return this.roles 24 | // }, 25 | // getName (){ 26 | // return this.name 27 | // } 28 | // }, 29 | actions: { 30 | /** 31 | * 加载用户角色列表,如果 inited 为 true 则直接返回 32 | * @returns 33 | */ 34 | loadRole (){ 35 | return new Promise((ok, fail)=>{ 36 | if(inited) return ok(this.roles) 37 | 38 | RESULT("/kaoping/overview", {}, d=> { 39 | this.roles = d.data.roles 40 | this.name = d.data.name 41 | console.debug("获取用户总览信息", this.roles, this.name) 42 | ok(this.roles) 43 | inited = true 44 | }) 45 | }) 46 | } 47 | } 48 | }) 49 | -------------------------------------------------------------------------------- /src/theme/naive.less: -------------------------------------------------------------------------------- 1 | // tailwind 会覆盖背景色,导致按钮背景色为纯白,此处强制使用主题色 2 | .n-button { 3 | background-color: var(--n-color); 4 | } 5 | 6 | svg { 7 | &.icon { 8 | height: 1em !important; 9 | display: inline-block; 10 | margin-top: -2px; 11 | } 12 | } 13 | 14 | .n-menu-item-content__icon { 15 | svg { 16 | display: block; 17 | margin-top: 0px !important; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/theme/tailwind-simple.css: -------------------------------------------------------------------------------- 1 | /* 2 | 截取 tailwind 部分样式 3 | 主要是 内外边距 4 | */ 5 | *,::before,::after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb;}::before,::after{--tw-content:'';}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-feature-settings:normal;}body{margin:0;line-height:inherit;}hr{height:0;color:inherit;border-top-width:1px;}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted;}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit;}a{color:inherit;text-decoration:inherit;}b,strong{font-weight:bolder;}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;}small{font-size:80%;}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}sub{bottom:-0.25em;}sup{top:-0.5em;}table{text-indent:0;border-color:inherit;border-collapse:collapse;}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0;}button,select{text-transform:none;}button,[type='button'],[type='reset'],[type='submit']{-webkit-appearance:button;background-color:transparent;background-image:none;}:-moz-focusring{outline:auto;}:-moz-ui-invalid{box-shadow:none;}progress{vertical-align:baseline;}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto;}[type='search']{-webkit-appearance:textfield;outline-offset:-2px;}::-webkit-search-decoration{-webkit-appearance:none;}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit;}summary{display:list-item;}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0;}fieldset{margin:0;padding:0;}legend{padding:0;}ol,ul,menu{list-style:none;margin:0;padding:0;}textarea{resize:vertical;}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af;}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af;}button,[role="button"]{cursor:pointer;}:disabled{cursor:default;}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle;}img,video{max-width:100%;height:auto;}[hidden]{display:none;}*,::before,::after{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x:;--tw-pan-y:;--tw-pinch-zoom:;--tw-scroll-snap-strictness:proximity;--tw-ordinal:;--tw-slashed-zero:;--tw-numeric-figure:;--tw-numeric-spacing:;--tw-numeric-fraction:;--tw-ring-inset:;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur:;--tw-brightness:;--tw-contrast:;--tw-grayscale:;--tw-hue-rotate:;--tw-invert:;--tw-saturate:;--tw-sepia:;--tw-drop-shadow:;--tw-backdrop-blur:;--tw-backdrop-brightness:;--tw-backdrop-contrast:;--tw-backdrop-grayscale:;--tw-backdrop-hue-rotate:;--tw-backdrop-invert:;--tw-backdrop-opacity:;--tw-backdrop-saturate:;--tw-backdrop-sepia:;}::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x:;--tw-pan-y:;--tw-pinch-zoom:;--tw-scroll-snap-strictness:proximity;--tw-ordinal:;--tw-slashed-zero:;--tw-numeric-figure:;--tw-numeric-spacing:;--tw-numeric-fraction:;--tw-ring-inset:;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgb(59 130 246 / 0.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur:;--tw-brightness:;--tw-contrast:;--tw-grayscale:;--tw-hue-rotate:;--tw-invert:;--tw-saturate:;--tw-sepia:;--tw-drop-shadow:;--tw-backdrop-blur:;--tw-backdrop-brightness:;--tw-backdrop-contrast:;--tw-backdrop-grayscale:;--tw-backdrop-hue-rotate:;--tw-backdrop-invert:;--tw-backdrop-opacity:;--tw-backdrop-saturate:;--tw-backdrop-sepia:;}.container{width:100%;}@media (min-width:640px){.container{max-width:640px;}}@media (min-width:768px){.container{max-width:768px;}}@media (min-width:1024px){.container{max-width:1024px;}}@media (min-width:1280px){.container{max-width:1280px;}}@media (min-width:1536px){.container{max-width:1536px;}}.h-full{height:100%;}.w-full{width:100%;}.cursor-pointer{cursor:pointer;}.text-center{text-align:center !important;}.text-right{text-align:right;}.text-lg{font-size:1.125rem;line-height:1.75rem;}.text-2xl{font-size:1.5rem;line-height:2rem;}.text-5xl{font-size:3rem;line-height:1;}.text-3xl{font-size:1.875rem;line-height:2.25rem;}.text-xs{font-size:0.75rem;line-height:1rem;}.text-sm{font-size:0.9rem;line-height:1rem;}.text-4xl{font-size:2.25rem;line-height:2.5rem;}.text-6xl{font-size:3.75rem;line-height:1;}.font-bold{font-weight:700;}.blur{--tw-blur:blur(8px);filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);}.p-0{padding:0px}.p-1{padding:0.25rem}.p-2{padding:0.5rem}.p-3{padding:0.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-7{padding:1.75rem}.p-8{padding:2rem}.p-9{padding:2.25rem}.p-10{padding:2.5rem}.px-0{padding-left:0px;padding-right:0px}.px-1{padding-left:0.25rem;padding-right:0.25rem}.px-2{padding-left:0.5rem;padding-right:0.5rem}.px-3{padding-left:0.75rem;padding-right:0.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.px-7{padding-left:1.75rem;padding-right:1.75rem}.px-8{padding-left:2rem;padding-right:2rem}.px-9{padding-left:2.25rem;padding-right:2.25rem}.px-10{padding-left:2.5rem;padding-right:2.5rem}.py-0{padding-top:0px;padding-bottom:0px}.py-1{padding-top:0.25rem;padding-bottom:0.25rem}.py-2{padding-top:0.5rem;padding-bottom:0.5rem}.py-3{padding-top:0.75rem;padding-bottom:0.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-7{padding-top:1.75rem;padding-bottom:1.75rem}.py-8{padding-top:2rem;padding-bottom:2rem}.py-9{padding-top:2.25rem;padding-bottom:2.25rem}.py-10{padding-top:2.5rem;padding-bottom:2.5rem}.pt-0{padding-top:0px}.pt-1{padding-top:0.25rem}.pt-2{padding-top:0.5rem}.pt-3{padding-top:0.75rem}.pt-4{padding-top:1rem}.pt-5{padding-top:1.25rem}.pt-6{padding-top:1.5rem}.pt-7{padding-top:1.75rem}.pt-8{padding-top:2rem}.pt-9{padding-top:2.25rem}.pt-10{padding-top:2.5rem}.pl-0{padding-left:0px}.pl-1{padding-left:0.25rem}.pl-2{padding-left:0.5rem}.pl-3{padding-left:0.75rem}.pl-4{padding-left:1rem}.pl-5{padding-left:1.25rem}.pl-6{padding-left:1.5rem}.pl-7{padding-left:1.75rem}.pl-8{padding-left:2rem}.pl-9{padding-left:2.25rem}.pl-10{padding-left:2.5rem}.pr-0{padding-right:0px}.pr-1{padding-right:0.25rem}.pr-2{padding-right:0.5rem}.pr-3{padding-right:0.75rem}.pr-4{padding-right:1rem}.pr-5{padding-right:1.25rem}.pr-6{padding-right:1.5rem}.pr-7{padding-right:1.75rem}.pr-8{padding-right:2rem}.pr-9{padding-right:2.25rem}.pr-10{padding-right:2.5rem}.mt-0{margin-top:0px}.mt-1{margin-top:0.25rem}.mt-2{margin-top:0.5rem}.mt-3{margin-top:0.75rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.mt-6{margin-top:1.5rem}.mt-7{margin-top:1.75rem}.mt-8{margin-top:2rem}.mt-9{margin-top:2.25rem}.mt-10{margin-top:2.5rem}.mr-0{margin-right:0px}.mr-1{margin-right:0.25rem}.mr-2{margin-right:0.5rem}.mr-3{margin-right:0.75rem}.mr-4{margin-right:1rem}.mr-5{margin-right:1.25rem}.mr-6{margin-right:1.5rem}.mr-7{margin-right:1.75rem}.mr-8{margin-right:2rem}.mr-9{margin-right:2.25rem}.mr-10{margin-right:2.5rem}.mb-1{margin-bottom:0.25rem}.mb-2{margin-bottom:0.5rem}.mb-3{margin-bottom:0.75rem}.mb-4{margin-bottom:1rem}.mb-5{margin-bottom:1.25rem}.mb-6{margin-bottom:1.5rem}.mb-7{margin-bottom:1.75rem}.mb-8{margin-bottom:2rem}.mb-9{margin-bottom:2.25rem}.mb-10{margin-bottom:2.5rem}.ml-0{margin-left:0px}.ml-1{margin-left:0.25rem}.ml-2{margin-left:0.5rem}.ml-3{margin-left:0.75rem}.ml-4{margin-left:1rem}.ml-5{margin-left:1.25rem}.ml-6{margin-left:1.5rem}.ml-7{margin-left:1.75rem}.ml-8{margin-left:2rem}.ml-9{margin-left:2.25rem}.ml-10{margin-left:2.5rem}.mx-0{margin-left:0px;margin-right:0px}.mx-1{margin-left:0.25rem;margin-right:0.25rem}.mx-2{margin-left:0.5rem;margin-right:0.5rem}.mx-3{margin-left:0.75rem;margin-right:0.75rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-5{margin-left:1.25rem;margin-right:1.25rem}.mx-6{margin-left:1.5rem;margin-right:1.5rem}.mx-7{margin-left:1.75rem;margin-right:1.75rem}.mx-8{margin-left:2rem;margin-right:2rem}.mx-9{margin-left:2.25rem;margin-right:2.25rem}.mx-10{margin-left:2.5rem;margin-right:2.5rem}.my-0{margin-top:0px;margin-bottom:0px}.my-1{margin-top:0.25rem;margin-bottom:0.25rem}.my-2{margin-top:0.5rem;margin-bottom:0.5rem}.my-3{margin-top:0.75rem;margin-bottom:0.75rem}.my-4{margin-top:1rem;margin-bottom:1rem}.my-5{margin-top:1.25rem;margin-bottom:1.25rem}.my-6{margin-top:1.5rem;margin-bottom:1.5rem}.my-7{margin-top:1.75rem;margin-bottom:1.75rem}.my-8{margin-top:2rem;margin-bottom:2rem}.my-9{margin-top:2.25rem;margin-bottom:2.25rem}.my-10{margin-top:2.5rem;margin-bottom:2.5rem}.m-0{margin:0px}.m-1{margin:0.25rem}.m-2{margin:0.5rem}.m-3{margin:0.75rem}.m-4{margin:1rem}.m-5{margin:1.25rem}.m-6{margin:1.5rem}.m-7{margin:1.75rem}.m-8{margin:2rem}.m-9{margin:2.25rem}.m-10{margin:2.5rem} 6 | 7 | /* 2023-01-09 增加 flex 相关 */ 8 | .flex-1{flex:1 1 0%;}.flex-auto{flex:1 1 auto;}.flex-initial{flex:0 1 auto;}.flex-none{flex:none;}.flex-shrink-0{flex-shrink:0;}.flex-shrink{flex-shrink:1;}.flex-grow-0{flex-grow:0;}.flex-grow{flex-grow:1;}.flex{display:flex;}.inline-flex{display:inline-flex;} 9 | 10 | .cursor-help { cursor: help;} 11 | .text-left {text-align: left;} 12 | -------------------------------------------------------------------------------- /src/theme/tailwind.css: -------------------------------------------------------------------------------- 1 | /*! @import */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | -------------------------------------------------------------------------------- /src/util/http.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 集成显卡 3 | * @Date: 2022-08-23 13:04:59 4 | * @Last Modified by: 集成显卡 5 | * @Last Modified time: 2023-07-04 15:20:51 6 | * 7 | * 8 | * 注意: 9 | * ① 统一使用 json 提交数据 10 | */ 11 | 12 | import axios from 'axios' 13 | import qs from 'qs' 14 | 15 | //默认的 server 前缀为空 16 | window.SERVER = "" 17 | const UA = "UA" 18 | 19 | 20 | /* 21 | * 对于生产环境,部署在网关下,可以直接读取本地 localStorage 的令牌信息 22 | */ 23 | if(process.env.NODE_ENV == 'production'){ 24 | const token = localStorage.getItem(UA) || "" 25 | if(!!token) axios.defaults.headers.common[UA] = window.TOKEN = token 26 | } 27 | else { 28 | window.changeUser = value=>{ 29 | let user = typeof(value)=='object'? value : {id: value, name:"集成显卡", ip:"127.0.0.1"} 30 | 31 | const token = `${user.id}-${user.name}-${user.ip}` 32 | axios.defaults.headers.common['UA'] = window.TOKEN = encodeURI(token) 33 | console.debug(`[测试环境] 切换用户 TOKEN 为:${token} `) 34 | } 35 | 36 | changeUser("admin") 37 | } 38 | 39 | let _dealWithErrorRequest = (url, error, onFail)=>{ 40 | M.loadingBar.error() 41 | 42 | if(!!onFail && typeof(onFail) === 'function'){ 43 | if(onFail(error? error.response: null) === true) return 44 | } 45 | console.debug(error) 46 | let content = "" 47 | let meta = "" 48 | 49 | if(error.response && error.response.status) { 50 | meta = `CODE=${error.response.status}` 51 | content = error.response.status == 404 ? "请求的接口不存在": error.response.data.message 52 | } 53 | else{ 54 | meta = "程序逻辑错误" 55 | content = error.message 56 | } 57 | 58 | window.M.notice.create({ type:"error", content, title:"数据接口异常", description: url, meta}) 59 | } 60 | 61 | /** 62 | * 发送POST请求到服务器 63 | * @param url 64 | * @param data 65 | * @param onOk 66 | * @param onFail 67 | * @constructor 68 | */ 69 | window.POST=(url,data,onOk,onFail, useJson=true, headers={})=>{ 70 | M.loadingBar.start() 71 | 72 | //提交数据到服务器 73 | axios.post(window.SERVER + url, useJson ? data : qs.stringify(data||{}), {headers}).then(function (response) { 74 | if(response.status===200){ 75 | M.loadingBar.finish() 76 | if(onOk) onOk(response.data) 77 | } 78 | else{ 79 | M.notice.error(`POST ${url} 失败\n响应码:${response.status}`) 80 | } 81 | }).catch(function (error) { 82 | _dealWithErrorRequest(url,error, onFail) 83 | }) 84 | } 85 | window.GET=(url,data,onOk,onFail)=>{ 86 | axios.get(window.SERVER + url, {params: data}).then(function (response) { 87 | if(response.status===200){ 88 | if(onOk) onOk(response.data) 89 | }else{ 90 | M.notice.error(`GET ${url} 失败\n响应码:${response.status}`) 91 | } 92 | }).catch(function (error) { 93 | _dealWithErrorRequest(url, error, onFail) 94 | }) 95 | } 96 | /** 97 | * 对于返回Result对象的请求封装 98 | * @param url 99 | * @param data 100 | * @param onOk 101 | * @param onFail 102 | * @constructor 103 | */ 104 | // window.RESULT=(url,data,onOk,onFail, useJson=true, headers={})=>{ 105 | // POST(url,data,function (res) { 106 | // if(res.success === true && onOk) onOk(res) 107 | // else{ 108 | // //当自定义了异常处理函数,就优先调用,当 onFail 返回 true 时不显示系统级别的错误提示 109 | // let notShowError = onFail && onFail(res)===true 110 | // if(!notShowError){ 111 | // M.notice.create({ 112 | // type:"error", 113 | // content: res.message, 114 | // title:"数据接口异常", 115 | // description: url 116 | // }) 117 | // } 118 | // } 119 | // },onFail, useJson, headers) 120 | // } 121 | 122 | /** 123 | * 对于返回 Result 对象的请求封装 124 | * @param {*} url 请求地址 125 | * @param {*} data 参数 126 | * @param {*} onOk 请求成功后回调函数 127 | * @param {*} ps 额外设置 128 | * fail 请求失败后的回调函数 129 | * json 是否以 JSON 格式提交参数 130 | * headers 自定义请求头 131 | * loading 加载中的开关(RefImpl 类型),在开始请求时设置为 true,请求结束(无论成功与否)都设置为 false 132 | */ 133 | window.RESULT=(url,data,onOk, ps={})=>{ 134 | ps = Object.assign( 135 | { 136 | fail (){}, //失败时的回调函数 137 | json: true, //是否以 JSON Body 形式提交参数 138 | headers:{}, //自定义请求头 139 | loading:undefined //加载中开关 140 | }, 141 | ps 142 | ) 143 | if(ps.loading) ps.loading.value = true 144 | 145 | POST( 146 | url,data, 147 | function (res) { 148 | if(ps.loading) ps.loading.value = false 149 | 150 | if(res.success === true && onOk) onOk(res) 151 | else{ 152 | //当自定义了异常处理函数,就优先调用,当 onFail 返回 true 时不显示系统级别的错误提示 153 | let notShowError = onFail && onFail(res)===true 154 | if(!notShowError){ 155 | M.notice.create({ 156 | type:"error", 157 | content: res.message, 158 | title:"数据接口异常", 159 | description: url 160 | }) 161 | } 162 | } 163 | }, 164 | function (e){ 165 | if(ps.loading) ps.loading.value = false 166 | 167 | if(ps.fail) ps.fail(e) 168 | }, 169 | ps.json, 170 | ps.headers 171 | ) 172 | } 173 | 174 | /** 175 | * 使用 axios 上传文件 176 | * 需要设置头部 177 | */ 178 | window.UPLOAD = (url, data, onOk, onFail)=>{ 179 | let form = new FormData() 180 | Object.keys(data).forEach(k=> form.append(k, data[k])) 181 | RESULT(url, form, onOk, {fail: onFail, json: true, headers: {'Content-Type': "multipart/form-data"}} ) 182 | } 183 | 184 | /** 185 | * 下载文件到本地(使用 axios) 186 | * 程序如何判断是否为异常(后端异常返回的是 JSON 格式的异常信息) 187 | * 1. 后端没有返回文件名 188 | * 2. 返回的格式为 application/json 189 | * 190 | * ---------------------------------------------------------------------------- 191 | * 另外一种下载方式: 192 | * window.open("/attach/zipDownload") 193 | * 194 | * @param url 195 | * @param data 表单参数 196 | * @param onOk 默认成功后:M.notice({文件名}, "文件下载成功") 197 | * @param onFail 默认失败后通过 alert 打印错误信息 198 | * @param json 199 | * @param fName 下载后文件名,若不为空则强制修改为该文件名 200 | * @param useGet 是否使用 GET 方式下载 201 | * @constructor 202 | */ 203 | window.DOWNLOAD=(url, data, onOk, onFail,json=false, fName=null, useGet=false)=>{ 204 | let form = new FormData() 205 | let headers = {} 206 | if(json){ 207 | Object.keys(data).forEach(k=> form.append(k, data[k])) 208 | 209 | headers['Content-Type'] = "multipart/form-data" 210 | } 211 | let method = useGet? axios.get:axios.post 212 | //提交数据到服务器 213 | method(window.SERVER + url, json?form:qs.stringify(data||{}), {responseType: 'blob', headers}).then(function (response) { 214 | let headers = response.headers 215 | let contentType = headers['content-type'] 216 | 217 | console.debug("下载响应头部:", headers) 218 | console.debug("下载响应内容:", response) 219 | if(!response.data){ 220 | console.error("服务器响应异常", response) 221 | return onFail && onFail(response) 222 | } 223 | 224 | const blob = new Blob([response.data], {type: contentType}) 225 | const contentDisposition = headers['content-disposition'] 226 | const length = headers['content-length'] 227 | let fileName = fName 228 | if (!fileName && contentDisposition) { 229 | fileName = window.decodeURI(contentDisposition.split('=')[1]) 230 | } 231 | 232 | console.debug("下载文件:", fileName, contentDisposition) 233 | 234 | //判断是否为后端出错 235 | if((!fileName || !contentDisposition) && response.data.type=="application/json"){ 236 | let fileReader = new FileReader() 237 | fileReader.onload = e=>{ 238 | let jsonText = fileReader.result 239 | let result = JSON.parse(jsonText) 240 | 241 | console.debug("来自后端的下载响应:", result) 242 | 243 | //如果 onFail 返回 false 则不显示错误窗口 244 | let showErrorMsg = !onFail || (onFail && onFail(result)!=false) 245 | if(showErrorMsg){ 246 | let content = UI.html(`
${result.message}

247 | 1. 请确认您提交的参数是否正确后再重试
2. 若错误依旧请联系信息科技部
` 248 | ) 249 | M.dialog({content, title:"文件下载失败(服务器响应内容如下)", type:"error"}) 250 | } 251 | } 252 | fileReader.readAsText(response.data) 253 | } 254 | else { 255 | fileName = fileName || ("文件下载-"+H.date.datetime(Date.now(), "YYYYMMDDHHmmss")) 256 | 257 | let link = document.createElement('a') 258 | // 非IE下载 259 | if ('download' in link) { 260 | link.href = window.URL.createObjectURL(blob) // 创建下载的链接 261 | link.download = fileName // 下载后文件名 262 | link.style.display = 'none' 263 | document.body.appendChild(link) 264 | link.click() // 点击下载 265 | window.URL.revokeObjectURL(link.href) // 释放掉blob对象 266 | document.body.removeChild(link) // 下载完成移除元素 267 | } else { 268 | // IE10+下载 269 | window.navigator.msSaveBlob(blob, fileName) 270 | } 271 | 272 | if(onOk) 273 | onOk({fileName, contentType, headers, length}) 274 | else 275 | M.notice.ok(fileName, "文件下载成功") 276 | } 277 | }).catch(function (error) { 278 | _dealWithErrorRequest(url,error, onFail) 279 | }) 280 | } 281 | export default {} 282 | -------------------------------------------------------------------------------- /src/util/index.js: -------------------------------------------------------------------------------- 1 | import * as H from "./module" 2 | import './http' 3 | 4 | import UI from "./ui-tool" 5 | 6 | window.H = H 7 | window.UI = UI 8 | -------------------------------------------------------------------------------- /src/util/module/date.js: -------------------------------------------------------------------------------- 1 | import dayjs from "dayjs" 2 | 3 | const YMD = "YYYY-MM-DD" 4 | const HMS = "HH:mm:ss" 5 | 6 | let date = (d = new Date(), f = YMD) => dayjs(d).format(f) 7 | let time = (d = new Date()) => dayjs(d).format(HMS) 8 | let datetime = (d = new Date()) => dayjs(d).format(`${YMD} ${HMS}`) 9 | let addDay = (step = 1, d, key = "day") => dayjs(d).add(step, key).format(YMD) 10 | 11 | export { 12 | date, 13 | time, 14 | datetime, 15 | addDay 16 | } 17 | 18 | export const compact = (d = new Date())=> date(d, "YYYYMMDD") 19 | export const compactTime = (d=new Date())=> date(d, "HHmmss") 20 | export const hour = ()=> new Date().getHours() 21 | /** 22 | * 获取时间点开始日期 23 | * @param {*} key 24 | * @returns 25 | */ 26 | export const beginOf = (key="month", f = YMD)=> dayjs().startOf(key).format(f) 27 | export const endOf = (key="month",f = YMD) => dayjs().endOf(key).format(f) 28 | /** 29 | * 获取两个日期相差的值,默认为天数 30 | * @param {*} from 31 | * @param {*} to 32 | * @param {*} type 33 | */ 34 | export const diff = (form, to, type="day")=> dayjs(from).diff(dayjs(to), type) 35 | /** 36 | * 日期字符串转化为时间戳 37 | * @param {*} v 38 | * @returns 39 | */ 40 | export const toLong = v=> new Date(v).getTime() 41 | 42 | /** 43 | * 转换为 Y,M,D 数组 44 | * @returns 45 | */ 46 | export const array = (d=new Date()) => date(d).split("-") 47 | -------------------------------------------------------------------------------- /src/util/module/index.js: -------------------------------------------------------------------------------- 1 | import * as date from './date' 2 | import * as store from './store' 3 | 4 | 5 | export { date, store } 6 | 7 | /** 8 | * 获取指定对象(转换为 JSON 字符串后)的 哈希 值 9 | * @param {*} obj 10 | * @param {*} caseSensitive 11 | * @returns 12 | */ 13 | export const hashCode = (obj = null, caseSensitive = false)=>{ 14 | obj = JSON.stringify(obj) 15 | if (!caseSensitive) obj = obj.toLowerCase() 16 | 17 | var hash = 100000000, i, ch 18 | for (i = obj.length - 1; i >= 0; i--) { 19 | ch = obj.charCodeAt(i) 20 | hash ^= ((hash << 5) + ch + (hash >> 2)) 21 | } 22 | 23 | return (hash & 0x7FFFFFFF) 24 | } 25 | 26 | /** 27 | * 格式化文件大小 28 | * @param {*} mem 29 | */ 30 | export const filesize = (mem, fixed=1, split=" ")=>{ 31 | var G = 0 32 | var M = 0 33 | var KB = 0 34 | mem >= (1 << 30) && (G = (mem / (1 << 30)).toFixed(fixed)) 35 | mem >= (1 << 20) && (mem < (1 << 30)) && (M = (mem / (1 << 20)).toFixed(fixed)) 36 | mem >= (1 << 10) && (mem < (1 << 20)) && (KB = (mem / (1 << 10)).toFixed(fixed)) 37 | return G > 0 38 | ? G + split + 'GB' 39 | : M > 0 40 | ? M + split + 'MB' 41 | : KB > 0 42 | ? KB + split + 'KB' 43 | : mem + split + 'B' 44 | } 45 | 46 | /** 47 | * 48 | * @param {*} name 49 | */ 50 | export const setTitle = (name)=> document.title = name 51 | 52 | /** 53 | * 转换为千位符 54 | * @param {*} num 55 | * @returns 56 | */ 57 | export const toThousands = (num)=>{ 58 | let ps = (num || 0).toString().split(".") 59 | ps[0] = ps[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",") 60 | return ps.join(".") 61 | } 62 | 63 | export const split = (text, splitor = ",") => typeof (text) == 'string' && text ? text.split(splitor) : [] 64 | 65 | /** 66 | * 将指定数据写入到系统粘贴板 67 | * @param {*} obj 如果为非字符串,事先进行 JSON 转换 68 | * @returns 69 | */ 70 | export const copyTo = (obj, pretty=false)=> { 71 | let text = typeof(obj)=='string'? obj: JSON.stringify(obj, null, pretty?4:0) 72 | // 仅在 localhost 或者 https 环境下才能使用该 API 73 | if(navigator.clipboard) 74 | navigator.clipboard.writeText(text) 75 | else { 76 | const textarea = document.createElement('textarea') 77 | textarea.addEventListener('focusin', (event) => event.stopPropagation()) 78 | textarea.value = text 79 | textarea.setAttribute('readonly', '') 80 | textarea.style.cssText = 'position:fixed; pointer-events:none; z-index:-9999; opacity:0;' 81 | 82 | document.body.appendChild(textarea) 83 | textarea.select() 84 | document.execCommand('copy') 85 | document.body.removeChild(textarea) 86 | } 87 | } 88 | 89 | /** 90 | * 判断参数是否为一个有内容的字符串 91 | * @param {*} v 92 | * @returns 93 | */ 94 | export const hasText = v=>{ 95 | if(v == null) return false 96 | if(typeof(v)==='string') return /[^\s]/.test(v) 97 | return false 98 | } 99 | 100 | /** 101 | * 打开新页面(同源) 102 | * @param {*} target 103 | */ 104 | export const openUrl = (target, ps={})=>{ 105 | ps = Object.assign( 106 | { 107 | title:"", 108 | width:1320, 109 | height:720, 110 | type:"_blank", 111 | center: false //是否居中 112 | }, 113 | ps 114 | ) 115 | let options = undefined 116 | if(ps.type == '_blank'){ 117 | // 一旦设置了宽度,则打开新窗口 118 | options = `width=${ps.width},height=${ps.height}` 119 | if(ps.center) { 120 | let top = (window.screen.availHeight - ps.height)/2 121 | let left = (window.screen.availWidth - ps.width)/2 122 | options+=`,top=${top},left=${left}` 123 | } 124 | } 125 | 126 | let newWin = window.open(target, ps.type, options) 127 | if(!!ps.title && !!newWin){ 128 | newWin.onload = function(){ 129 | newWin.document.title = ps.title 130 | } 131 | } 132 | return newWin 133 | } 134 | 135 | /** 136 | * 保存内容到文件 137 | * @param {*} blob  138 | * @param {*} fileName  139 | */ 140 | export const saveToFile = (blob, fileName = "下载文件.txt")=>{ 141 | if (!(!!blob && blob.toString() == '[object Blob]')) { 142 | blob = new Blob([blob]) 143 | } 144 | let link = document.createElement('a') 145 | link.href = window.URL.createObjectURL(blob) // 创建下载的链接 146 | link.download = fileName // 下载后文件名 147 | link.style.display = 'none' 148 | document.body.appendChild(link) 149 | link.click() // 点击下载 150 | window.URL.revokeObjectURL(link.href) // 释放掉blob对象 151 | document.body.removeChild(link) // 下载完成移除元素 152 | } 153 | 154 | /** 155 | * 保存到 CSV 默认编码为 UTF-8 156 | * @param {*} text  157 | * @param {*} fileName  158 | */ 159 | export const saveToCSV = (text, fileName = "下载文件.csv")=> saveToFile( 160 | new Blob([Array.isArray(text) ? text.join("\n") : text], { type: "application/csv;charset=utf-8" }), 161 | fileName 162 | ) 163 | -------------------------------------------------------------------------------- /src/util/module/store.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: 集成显卡 3 | * @Date: 2021-11-30 08:30:23 4 | * @Last Modified by: 集成显卡 5 | * @Last Modified time: 2022-09-20 17:50:40 6 | * 7 | * 基于 localStorage 的简单封装 8 | */ 9 | const get = (k, dv)=> localStorage.getItem(k) || dv 10 | const set = (k, dv)=> localStorage.setItem(k, dv) 11 | 12 | export const boolean = (k, dv=false)=> { 13 | let v = get(k) 14 | return v == undefined?dv: (v ==='true'|| v==='1') 15 | } 16 | 17 | export const getObj = (k, dv={})=>{ 18 | let item = get(k) 19 | return /^{.*}$/.test(item)? JSON.parse(item) : dv 20 | } 21 | 22 | export const setObj = (k, v, v2)=>{ 23 | if(typeof(v) === 'object') 24 | return set(k, JSON.stringify(v)) 25 | else if(typeof(v)==='string'){ 26 | let obj = getObj(k) 27 | obj[v] = v2 28 | return set(k, JSON.stringify(obj)) 29 | } 30 | } 31 | 32 | export const getList = k=> JSON.parse(get(k,"[]")) 33 | 34 | export const setList = (k, v, maxLen=10)=>{ 35 | let l = [] 36 | if(Array.isArray(v)) l = v 37 | else { 38 | l = getList(k) 39 | let oldIndex = l.indexOf(v) 40 | if(oldIndex>=0) l.splice(oldIndex, 1) 41 | l.unshift(v) 42 | } 43 | set(k, JSON.stringify(l.slice(0, maxLen))) 44 | } 45 | 46 | export { set, get } 47 | -------------------------------------------------------------------------------- /src/util/ui-tool.js: -------------------------------------------------------------------------------- 1 | import { createVNode, h, render } from "vue" 2 | import { RouterLink } from "vue-router" 3 | import { NButton, NIcon } from "naive-ui" 4 | 5 | const themes = ["success", "info", "warning", "error"] 6 | 7 | let buildIcon= (icon, ps={})=>{ 8 | return createVNode(NIcon, Object.assign({component: icon}, ps)) 9 | } 10 | let buildIcon2 = (icon, ps)=> ()=> buildIcon(icon, ps) 11 | 12 | /** 13 | * 创建按钮 14 | * @param {*} icon 图标,如果传递 string 则使用 fontawesome 图标 15 | * @param {*} text 文字 16 | * @param {*} onClick 回调事件 17 | * @param {*} ps 其他自定属性 18 | * @returns 19 | */ 20 | let Button = (icon, text, onClick, ps={})=>{ 21 | return createVNode( 22 | NButton, 23 | Object.assign({onClick}, ps), 24 | ()=>{ 25 | let tags = [] 26 | if(!!icon) tags.push(buildIcon(icon)) // 27 | if(!!text) tags.push(text) 28 | return tags 29 | } 30 | ) 31 | } 32 | 33 | const _buildTree = (items, field, pid, suffix)=> items.filter(v=>v[field]==pid).map(v=>{ 34 | let childs = _buildTree(items, field, v.id, suffix) 35 | let node = { 36 | label : v.name, 37 | key : v.id, 38 | suffix : suffix && suffix(v), 39 | bean : v 40 | } 41 | if(childs.length) node.children=childs 42 | return node 43 | }) 44 | 45 | export default { 46 | /** 47 | * 构建图标 48 | * @param {*} icons 49 | * @param {*} spin 50 | * @returns 51 | */ 52 | buildIcon, 53 | buildIcon2, 54 | Button, 55 | /** 56 | * 创建圆形仅含图标的按钮,默认值:size=small circle=true quaternary=true 57 | * @param {*} icon 58 | * @param {*} onClick 59 | * @param {*} ps 60 | * @returns 61 | */ 62 | iconBtn (icon, onClick, ps={}){ 63 | return Button(icon, null, onClick, Object.assign({ quaternary:true, circle: true, size: 'small'}, ps)) 64 | }, 65 | /** 66 | * 生成菜单项 67 | * @param {*} routeName 路由名称或者路由对象 68 | * @param {*} text 菜单文本 69 | * @param {*} icon 图标 70 | * @returns 71 | */ 72 | menuItem (routeName, text, icon){ 73 | let to = typeof(routeName)==='string'? { name: routeName }: routeName 74 | return { 75 | label: () => createVNode(RouterLink, { to }, ()=>text), 76 | key: to.name, 77 | icon: buildIcon2(icon) 78 | } 79 | }, 80 | /** 81 | * 转换为 naive-ui 兼容的 SelectOption 82 | * @param {*} list 83 | */ 84 | toOptions (list, disableFun=()=>false){ 85 | return (Array.isArray(list)? list:[list]).map(v=>( 86 | { 87 | label:v, 88 | value:v, 89 | disabled: disableFun(v) 90 | } 91 | )) 92 | }, 93 | /** 94 | * 构建适配于 naive-ui 的下拉框选择内容,示例:[{label:"选项一",value:"01"}] 95 | * 96 | * 参数 text 类型可以是 Array、String、Object 97 | * Array ["01|选项一","02|选项二"] 98 | * String 01|选项一,02|选项二 99 | * Object {"01":"选项一", "02":"选项二"} 100 | * 101 | * 处理逻辑: 102 | * 1、将参数 text 转换为 Array(key与value 用英文 | 隔开) 103 | * 2、遍历上述数组元素转换为 { label, value } 104 | * 3、若参数没有区分 key 跟 value 则二者相同 105 | * 106 | * @param {*} text 107 | * @param {*} valueField 默认为 value 108 | * @param {*} labelField 默认为 label 109 | */ 110 | buildOptions (text, disableFun=()=>false) { 111 | let options = [] 112 | if(!text) return options 113 | 114 | if(Array.isArray(text)) 115 | options = text 116 | else if(typeof(text) === 'string'){ 117 | options = text.replace(" ", "").split(",") 118 | } 119 | else if(typeof(text) === 'object'){ 120 | options = Object.keys(text).map(k=> `${k}|${text[k]}`) 121 | } 122 | else 123 | throw Error(`${text} 不是有效的 options 数据内容,请参考文档进行配置`) 124 | 125 | return options.map(o=> { 126 | let i = o.indexOf("|") 127 | let obj = {} 128 | if(i==-1) 129 | obj.value = obj.label = o 130 | else{ 131 | obj.label = o.substring(i+1) 132 | obj.value = o.substring(0, i) 133 | } 134 | if(typeof(disableFun) === 'function') 135 | obj.disabled = disableFun(obj) 136 | 137 | return obj 138 | }) 139 | }, 140 | 141 | /** 142 | * 构建适配于 naive-ui 的树形结构 143 | * @param {Array} items 144 | * @param {String} field 父节点标记字段 145 | * @param {String} notSetValue 146 | */ 147 | buildTree (items, ps={}){ 148 | ps = Object.assign({field:"pid", notSetValue:null}, ps) 149 | return _buildTree(items, ps.field, ps.notSetValue, ps.suffix) 150 | }, 151 | 152 | html (html){ 153 | return ()=>h('div', {innerHTML: html }) 154 | }, 155 | 156 | /** 157 | * 给定 Map 数据,返回适配 echarts 饼状图的 series 158 | * 159 | * 160 | * roseType 不为空,则是否展示成南丁格尔图,通过半径区分数据大小。可选择两种模式: 161 | 'radius' 扇区圆心角展现数据的百分比,半径展现数据的大小。 162 | 'area' 所有扇区圆心角相同,仅通过半径展现数据大小。 163 | * @param {*} d 164 | * @param {*} ps 165 | * @param {*} ignoreZero 是否忽略数据小于等于 0 的值 166 | * @param {*} selected 选择名称,若传递函数,则接受 name、index 两个参数 167 | */ 168 | buildPieChart (d, ps={}, ignoreZero=true, selected){ 169 | return Object.assign({ 170 | type: 'pie', 171 | radius: [20, 200], 172 | roseType: '', 173 | itemStyle: { 174 | borderRadius: ps.borderRadius || 10 175 | }, 176 | selectedMode:"single", 177 | data: Array.isArray(d)? d : Object.keys(d) 178 | .filter(name=> !ignoreZero || d[name] >0) 179 | .map((name, index)=>({ 180 | value: d[name], 181 | name, 182 | selected: typeof(selected)==='function'? selected(name, index): name==selected 183 | })) 184 | }, ps) 185 | }, 186 | /** 187 | * 仅适用于单个 series 188 | * @param {*} d 189 | * @param {*} ps 190 | * @returns 191 | */ 192 | buildChart (d, ps={}){ 193 | return { 194 | xItems: Array.isArray(d)? d.map(v=>v.name) : Object.keys(d), 195 | series: Object.assign({ type:"line", data: Array.isArray(d)? d : Object.keys(d).map(k=> d[k])}, ps) 196 | } 197 | }, 198 | 199 | themes, 200 | getTheme (key){ 201 | return themes[H.hashCode(key) % themes.length] 202 | }, 203 | 204 | /** 205 | * 以对话框的形式显示异常 206 | * @param {*} e 207 | * @param {*} title 208 | */ 209 | showError (e, title="执行脚本代码出错"){ 210 | M.dialog({ 211 | type:"error", 212 | title, 213 | content: this.html(` 214 |
错误类型:${e.name}
215 |
错误信息${e.message}
216 | `) 217 | }) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/views/@COMMON/401.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 18 | -------------------------------------------------------------------------------- /src/views/@COMMON/403.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/views/@COMMON/404.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 14 | -------------------------------------------------------------------------------- /src/views/@COMMON/README.md: -------------------------------------------------------------------------------- 1 | # 通用页面 2 | > 如 404、403、500 等页面 3 | -------------------------------------------------------------------------------- /src/views/Main.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 50 | -------------------------------------------------------------------------------- /src/views/Window.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | -------------------------------------------------------------------------------- /src/views/demo/Chart.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 77 | -------------------------------------------------------------------------------- /src/views/demo/Icons.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 30 | 31 | 45 | -------------------------------------------------------------------------------- /src/views/demo/Index.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 82 | -------------------------------------------------------------------------------- /src/views/demo/Role.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | darkMode: "media", // or 'media' or 'class' 3 | minify: true, 4 | content: ['./public/index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 5 | theme: { 6 | extend: {}, 7 | }, 8 | plugins: [] 9 | } 10 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs') 3 | const webpack = require('webpack') 4 | 5 | const isProduction = process.env.NODE_ENV == 'production' 6 | 7 | const pkg = require("./package.json") 8 | const moduleName = process.env.npm_config_module||"" 9 | if(moduleName) console.debug(`指定模块为 ${moduleName} (将影响到具体的路由、MOCK 等)`) 10 | 11 | let resolve = dir=>path.join(__dirname,dir) 12 | 13 | let VERSION = (()=>{ 14 | let now = new Date 15 | return `${now.getUTCFullYear() - 2000}.${now.getUTCMonth() + 1}.${now.getUTCDate()}` 16 | })() 17 | 18 | let devServer = { 19 | host: "0.0.0.0", 20 | port: 10000, 21 | hot: true, // 热更新 22 | client: { 23 | overlay: { 24 | warnings: false, 25 | errors: true 26 | } 27 | }, 28 | proxy: (() => { 29 | let targets = {} //url 前缀 与 映射地址,如:"/booking" : "http://localhost:8080" 30 | 31 | let proxy = {} 32 | Object.keys(targets).forEach(k => { 33 | proxy[k] = { 34 | target: targets[k], 35 | changeOrigin: true, 36 | secure: false 37 | //ws: true,//websocket支持 38 | } 39 | }) 40 | return proxy 41 | })() 42 | } 43 | 44 | let pages = { 45 | index : 'src/main.js', 46 | meeting : { entry: 'src/pages/meeting/main.js', title:"会议室预约管理系统" }, 47 | project : { entry: 'src/pages/project/main.js', title:"IT 项目管理系统" } 48 | } 49 | 50 | /** 51 | * @type {import('@vue/cli-service').ProjectOptions} 52 | */ 53 | module.exports = { 54 | pages, 55 | productionSourceMap: false, 56 | configureWebpack: config => { 57 | /** 58 | * 构建提速设置 59 | * 配置如下: 60 | Windows 11 家庭中文版 61 | 版本 21H2 62 | 操作系统版本 22000.1098 63 | 处理器 11th Gen Intel(R) Core(TM) i5-11300H @ 3.10GHz 3.11 GHz 64 | 机带 RAM 16.0 GB (15.8 GB 可用) 65 | 66 | 构建速度约为 2100ms(未优化前为 14000 ms 左右) 67 | */ 68 | //Webpack5 简单的配置 cache 属性可大大提高构建速度(约 600% 的加速) 69 | config.cache = { 70 | type: 'filesystem' 71 | }, 72 | config.optimization.concatenateModules = false 73 | config.optimization.usedExports = false 74 | 75 | config.resolve = { 76 | extensions: ['.js', '.vue', '.json', ".css"], 77 | alias: { 78 | '@P' : resolve('public'), 79 | '@' : resolve('src'), //代码目录 80 | '@M' : resolve("src/components/macro"), //宏组件 81 | '@C' : resolve("src/components"), //常用组件 82 | '@CN' : resolve("src/components/naive-ui"), //常用组件(适配 NaiveUI) 83 | '@CC' : resolve("src/components/common"), //常用组件(通用) 84 | '@CM' : resolve("src/components/mixin"), //mixin 组件 85 | '@Pagination' : resolve("src/components/mixin/Pagination"), 86 | '@V' : resolve("src/views"), //视图目录 87 | '@Store' : resolve("src/store"), 88 | '@S' : resolve("src/service"), //接口相关 89 | '@T' : resolve("src/theme"), //主题相关 90 | '@U' : resolve("src/util") //通用工具 91 | } 92 | } 93 | 94 | config.plugins.push( 95 | /** 96 | * 增加全局变量(名称以 _ 开始及结尾) 97 | * 代码中如何使用: 98 | * 1、打印 console.debug(_MODULE_) 99 | * 2、逻辑 if(_MODULE_=='kaoping') {} 100 | */ 101 | new webpack.DefinePlugin({ 102 | "_APPNAME_": JSON.stringify(pkg.appName), 103 | "_VERSION_": JSON.stringify(VERSION), 104 | "_MODULE_": JSON.stringify(moduleName), 105 | "_APP_INFO_": JSON.stringify({ dependencies: pkg.dependencies, devDependencies: pkg.devDependencies }) 106 | }) 107 | ) 108 | 109 | node = { 110 | __filename: true, 111 | __dirname: true 112 | } 113 | // console.debug(config) 114 | }, 115 | devServer, 116 | // chainWebpack: (config) => { 117 | // if(isProduction){ 118 | // // 在 html 中注入参数变量 119 | // config.plugin('html').use(require("html-webpack-plugin")).tap((args) => { 120 | // console.debug(args) 121 | // // 在这里 122 | // args.title = `${pkg.appName}` 123 | // args.version = VERSION 124 | // return args 125 | // }) 126 | // } 127 | // return config 128 | // } 129 | } 130 | --------------------------------------------------------------------------------