20 Commits
v1.2 ... v1.5.0

Author SHA1 Message Date
Ah jung
f97a94e74c 版本号规范 2021-07-30 14:45:26 +08:00
Ah jung
da5231b384 Merge pull request #9 from Chika99/fix/basicForm
fix(basicForm): 修复基础表单在宽度小于640px显示问题
2021-07-30 13:39:14 +08:00
chika
97ae37efd0 fix(basicForm): 修复基础表单在宽度小于640px显示问题 2021-07-30 13:18:01 +08:00
Ah jung
b19430170f fix Bug 2021-07-30 11:37:09 +08:00
Ah jung
b642d28815 fix Bug 2021-07-30 10:33:24 +08:00
Ah jung
619669ec9e fix Bug or add docs 2021-07-30 10:26:19 +08:00
啊俊
044976b790 Perfect TS type detection 2021-07-25 22:48:32 +08:00
Ah jung
b43ab1ceb4 fix Bug 2021-07-22 13:54:36 +08:00
Ah jung
f773a3ed06 fix Bug or esLink formatting 2021-07-22 13:48:55 +08:00
Ah jung
7f81152793 fix Bug or esLink formatting 2021-07-22 13:47:44 +08:00
Ah jung
f6be8f521e fix Bug or add example 2021-07-21 18:38:56 +08:00
Ah jung
fa8b33acbe fix Bug or add example 2021-07-21 18:33:02 +08:00
啊俊
54e68db0c2 新增-系统设置-菜单权限-角色权限页面 2021-07-20 22:53:08 +08:00
Ah jung
9542345b54 版本号更新 2021-07-19 17:09:44 +08:00
Ah jung
b689fabfdd fix Bug Features CHANGELOG.ms 2021-07-19 16:42:11 +08:00
啊俊
46dc7eb69e fix Bug 2021-07-17 20:07:56 +08:00
Ah jung
c1e741dad6 fix Bug 2021-07-17 17:09:28 +08:00
Ah jung
ab4063e75e fix Bug 2021-07-17 16:34:04 +08:00
Ah jung
770d39871a fix Bug 2021-07-17 16:24:49 +08:00
Ah jung
3cb7a7f54f fix Bug or add example 2021-07-16 21:14:40 +08:00
223 changed files with 13717 additions and 7084 deletions

19
.editorconfig Normal file
View File

@@ -0,0 +1,19 @@
root = true
[*]
charset=utf-8
end_of_line=LF
insert_final_newline=true
indent_style=space
indent_size=2
max_line_length = 100
[*.{yml,yaml,json}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab

View File

@@ -13,11 +13,18 @@ VITE_BASE_URL = /
# 是否删除console # 是否删除console
VITE_DROP_CONSOLE = true VITE_DROP_CONSOLE = true
# 跨域代理,可以配置多个,请注意不要换行
#VITE_PROXY = [["/appApi","http://localhost:8001"],["/upload","http://localhost:8001/upload"]]
# VITE_PROXY=[["/api","https://naive-ui-admin"]]
# API 接口地址 # API 接口地址
VITE_APP_API_URL = / VITE_GLOB_API_URL =
# 图片上传地址 # 图片上传地址
VITE_GLOB_UPLOAD_URL= / VITE_GLOB_UPLOAD_URL=
# 图片前缀地址
VITE_GLOB_IMG_URL=
# 接口前缀 # 接口前缀
VITE_GLOB_API_URL_PREFIX = /api VITE_GLOB_API_URL_PREFIX = /api

View File

@@ -11,10 +11,21 @@ VITE_BASE_URL = /
VITE_DROP_CONSOLE = true VITE_DROP_CONSOLE = true
# API # API
VITE_APP_API_URL = / VITE_GLOB_API_URL =
# 图片上传地址 # 图片上传地址
VITE_GLOB_UPLOAD_URL= / VITE_GLOB_UPLOAD_URL=
# 图片前缀地址
VITE_GLOB_IMG_URL=
# 接口前缀 # 接口前缀
VITE_GLOB_API_URL_PREFIX = /api VITE_GLOB_API_URL_PREFIX = /api
# 是否启用gzip压缩或brotli压缩
# 可选: gzip | brotli | none
# 如果你需要多种形式,你可以用','来分隔
VITE_BUILD_COMPRESS = 'none'
# 使用压缩时是否删除原始文件默认为false
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false

View File

@@ -1,9 +1,11 @@
module.exports = { // @ts-check
const { defineConfig } = require('eslint-define-config');
module.exports = defineConfig({
root: true, root: true,
env: { env: {
browser: true, browser: true,
node: true, node: true,
es6: true es6: true,
}, },
parser: 'vue-eslint-parser', parser: 'vue-eslint-parser',
parserOptions: { parserOptions: {
@@ -12,21 +14,46 @@ module.exports = {
sourceType: 'module', sourceType: 'module',
jsxPragma: 'React', jsxPragma: 'React',
ecmaFeatures: { ecmaFeatures: {
jsx: true jsx: true,
} },
}, },
extends: [ extends: [
'plugin:vue/vue3-recommended', 'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'prettier', 'prettier',
'plugin:prettier/recommended' 'plugin:prettier/recommended',
'plugin:jest/recommended',
], ],
rules: { rules: {
'vue/no-unused-components': 'off', '@typescript-eslint/ban-ts-ignore': 'off',
'vue/no-unused-vars': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'vue/no-v-for-template-key-on-child': 'off', '@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'vue/custom-event-name-casing': 'off', 'vue/custom-event-name-casing': 'off',
// 'vue/attributes-order': 'off', 'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
},
],
'space-before-function-paren': 'off',
'vue/attributes-order': 'off',
'vue/one-component-per-file': 'off', 'vue/one-component-per-file': 'off',
'vue/html-closing-bracket-newline': 'off', 'vue/html-closing-bracket-newline': 'off',
'vue/max-attributes-per-line': 'off', 'vue/max-attributes-per-line': 'off',
@@ -34,34 +61,17 @@ module.exports = {
'vue/singleline-html-element-content-newline': 'off', 'vue/singleline-html-element-content-newline': 'off',
'vue/attribute-hyphenation': 'off', 'vue/attribute-hyphenation': 'off',
'vue/require-default-prop': 'off', 'vue/require-default-prop': 'off',
'space-before-function-paren': 'off',
'@typescript-eslint/camelcase': 'off',
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'vue/html-self-closing': [ 'vue/html-self-closing': [
'error', 'error',
{ {
html: { html: {
void: 'always', void: 'always',
normal: 'never', normal: 'never',
component: 'always' component: 'always',
}, },
svg: 'always', svg: 'always',
math: 'always' math: 'always',
} },
] ],
} },
} });

View File

@@ -1,3 +1,45 @@
# 1.5 (2021-07-30)
### 🐛 Bug Fixes
- 修复表格列配置,拖拽时最后的操作列重复增加
- 多标签页交互优化
- ### ✨ Features
- `项目文档`已上线
- `Application`组件加载机制优化解决路由守卫Axios中可使用DialogMessage 等之类组件
- `BasicTable` 组件新增`高度自适应``单元格编辑``整行编辑` 特性
- `nprogress` 移除,用 `Loading Bar`代替
- 打包支持`gzip``brotli` 压缩
- 新增代理`VITE_PROXY`配置
- 路由菜单,支持多级菜单
- 依赖升级
- 本次更新,有破坏性更新,涉及文件重命名,增删调整
# 1.4 (2021-07-21)
### 🐛 Bug Fixes
- vite降至2.3.6
- 多标签页交互优化
- ### ✨ Features
- 新增 `TableAction` 组件
- 新增 `菜单权限管理` 示例
- 新增 `角色权限管理` 示例
- 持续更新更多实用组件及示例感谢Star
# 1.3 (2021-07-19)
### 🐛 Bug Fixes
- 修复多标签页左右切换按钮自适应展示
- 修复登录页面出现多标签页
- ### ✨ Features
- 新增 `Upload` 组件及配置
- 新增 `VITE_GLOB_IMG_URL` 图片前缀地址配合Upload
- 新增 `滑块验证码` 组件
- 新增 `登录页面-滑块验证码` 示例
- 持续更新更多实用组件及示例感谢Star
# 1.2 (2021-07-16) # 1.2 (2021-07-16)
### 🐛 Bug Fixes ### 🐛 Bug Fixes
- 修复面包屑显示登录页面 - 修复面包屑显示登录页面

View File

@@ -1,6 +1,6 @@
## 简介 ## 简介
Naive Ui Admin 是一个免费开源的中后台模版,使用了最新的`vue3`,`vite2`,`TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考 [Naive Ui Admin](https://github.com/jekip/naive-ui-admin) 是一个基于 [Vue3.0](https://github.com/vuejs/vue-next)、[Vite](https://github.com/vitejs/vite)、 [Naive UI](https://www.naiveui.com/)、[TypeScript](https://www.typescriptlang.org/) 的中后台解决方案,它使用了最新的前端技术栈,并提炼了典型的业务模型,页面,包括二次封装组件、动态菜单、权限校验、粒子化权限控制等功能,它可以帮助你快速搭建企业级中后台项目,该项目使用最新的前端技术栈,相信不管是从新技术使用还是其他方面,都能帮助到你
## 特性 ## 特性
- **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发 - **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发
@@ -10,30 +10,15 @@ Naive Ui Admin 是一个免费开源的中后台模版,使用了最新的`vue3
- **权限** 内置完善的动态路由权限生成方案 - **权限** 内置完善的动态路由权限生成方案
- **组件** 二次封装了多个常用的组件 - **组件** 二次封装了多个常用的组件
### 页面功能
#### 系统看板
- [x] 主控台
- [ ] 监控页
- [x] 工作台
- [x] 表单页面
- [x] 列表页面
- [x] 异常页面
- [x] 结果页面
- [x] 设置页面
### 页面组件
#### ProTable
- [x] 表格
- 持续开发中...
## 在线预览 ## 在线预览
- [naive-ui-admin](https://jekip.github.io) - [naive-ui-admin](https://jekip.github.io)
账号admin密码123456 账号admin密码123456(随意)
## 文档 ## 文档
[文档地址](https://github.com/jekip/naive-ui-admin) - 待完善 [文档地址](https://jekip.github.io/docs/)
## 准备 ## 准备
@@ -80,7 +65,7 @@ yarn build
[CHANGELOG](./CHANGELOG.md) [CHANGELOG](./CHANGELOG.md)
## 感谢 ## 感谢
[@Vben](https://github.com/anncwb/vue-vben-admin) 借鉴 vue-vben-admin 实现的骨架,同时也使用作者开发的 vite 插件,非常感谢作者。 [@Vben](https://github.com/anncwb/vue-vben-admin) 借鉴 vue-vben-admin 实现的骨架,同时也使用作者开发的 vite 插件,再次感谢作者。
## 如何贡献 ## 如何贡献

View File

@@ -31,8 +31,7 @@ export function wrapperEnv(envConf: Recordable): ViteEnv {
if (envName === 'VITE_PROXY') { if (envName === 'VITE_PROXY') {
try { try {
realName = JSON.parse(realName); realName = JSON.parse(realName);
} catch (error) { } catch (error) {}
}
} }
ret[envName] = realName; ret[envName] = realName;
process.env[envName] = realName; process.env[envName] = realName;
@@ -51,8 +50,7 @@ export function getEnvConfig(match = 'VITE_GLOB_', confFiles = ['.env', '.env.pr
try { try {
const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item))); const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)));
envConfig = { ...envConfig, ...env }; envConfig = { ...envConfig, ...env };
} catch (error) { } catch (error) {}
}
}); });
Object.keys(envConfig).forEach((key) => { Object.keys(envConfig).forEach((key) => {

View File

@@ -0,0 +1,35 @@
/**
* Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
* https://github.com/anncwb/vite-plugin-compression
*/
import type { Plugin } from 'vite';
import compressPlugin from 'vite-plugin-compression';
export function configCompressPlugin(
compress: 'gzip' | 'brotli' | 'none',
deleteOriginFile = false
): Plugin | Plugin[] {
const compressList = compress.split(',');
const plugins: Plugin[] = [];
if (compressList.includes('gzip')) {
plugins.push(
compressPlugin({
ext: '.gz',
deleteOriginFile,
})
);
}
if (compressList.includes('brotli')) {
plugins.push(
compressPlugin({
ext: '.br',
algorithm: 'brotliCompress',
deleteOriginFile,
})
);
}
return plugins;
}

View File

@@ -5,9 +5,10 @@ import vueJsx from '@vitejs/plugin-vue-jsx';
import { configHtmlPlugin } from './html'; import { configHtmlPlugin } from './html';
import { configMockPlugin } from './mock'; import { configMockPlugin } from './mock';
import { configCompressPlugin } from './compress';
export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, prodMock) { export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, prodMock) {
const { VITE_USE_MOCK } = viteEnv; const { VITE_USE_MOCK, VITE_BUILD_COMPRESS, VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE } = viteEnv;
const vitePlugins: (Plugin | Plugin[])[] = [ const vitePlugins: (Plugin | Plugin[])[] = [
// have to // have to
@@ -22,5 +23,12 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, prodMock)
// vite-plugin-mock // vite-plugin-mock
VITE_USE_MOCK && vitePlugins.push(configMockPlugin(isBuild, prodMock)); VITE_USE_MOCK && vitePlugins.push(configMockPlugin(isBuild, prodMock));
if (isBuild) {
// rollup-plugin-gzip
vitePlugins.push(
configCompressPlugin(VITE_BUILD_COMPRESS, VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE)
);
}
return vitePlugins; return vitePlugins;
} }

34
build/vite/proxy.ts Normal file
View File

@@ -0,0 +1,34 @@
/**
* Used to parse the .env.development proxy configuration
*/
import type { ProxyOptions } from 'vite';
type ProxyItem = [string, string];
type ProxyList = ProxyItem[];
type ProxyTargetList = Record<string, ProxyOptions & { rewrite: (path: string) => string }>;
const httpsRE = /^https:\/\//;
/**
* Generate proxy
* @param list
*/
export function createProxy(list: ProxyList = []) {
const ret: ProxyTargetList = {};
for (const [prefix, target] of list) {
const isHttps = httpsRE.test(target);
// https://github.com/http-party/node-http-proxy#options
ret[prefix] = {
target: target,
changeOrigin: true,
ws: true,
rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''),
// https is require secure=false
...(isHttps ? { secure: false } : {}),
};
}
return ret;
}

View File

@@ -13,6 +13,7 @@
<style>.first-loading-wrp{display:flex;justify-content:center;align-items:center;flex-direction:column;min-height:420px;height:100%}.first-loading-wrp>h1{font-size:128px}.first-loading-wrp .loading-wrp{padding:98px;display:flex;justify-content:center;align-items:center}.dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:32px;width:32px;height:32px;box-sizing:border-box}.dot i{width:14px;height:14px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.dot i:nth-child(1){top:0;left:0}.dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style> <style>.first-loading-wrp{display:flex;justify-content:center;align-items:center;flex-direction:column;min-height:420px;height:100%}.first-loading-wrp>h1{font-size:128px}.first-loading-wrp .loading-wrp{padding:98px;display:flex;justify-content:center;align-items:center}.dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:32px;width:32px;height:32px;box-sizing:border-box}.dot i{width:14px;height:14px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.dot i:nth-child(1){top:0;left:0}.dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style>
</head> </head>
<body> <body>
<div id="appProvider" style="display: none"></div>
<div id="app"> <div id="app">
<div class="first-loading-wrp"> <div class="first-loading-wrp">
<div class="loading-wrp"> <div class="loading-wrp">

View File

@@ -1,4 +1,4 @@
import Mock from 'mockjs' import Mock from 'mockjs';
export function resultSuccess(result, { message = 'ok' } = {}) { export function resultSuccess(result, { message = 'ok' } = {}) {
return Mock.mock({ return Mock.mock({
@@ -51,9 +51,9 @@ export function pagination<T = any>(pageNo: number, pageSize: number, array: T[]
* @param {Function} callback 回调函数 * @param {Function} callback 回调函数
*/ */
export function doCustomTimes(times: number, callback: any) { export function doCustomTimes(times: number, callback: any) {
let i = -1 let i = -1;
while (++i < times) { while (++i < times) {
callback(i) callback(i);
} }
} }

View File

@@ -1,5 +1,5 @@
import { Random } from 'mockjs' import { Random } from 'mockjs';
import { resultSuccess } from '../_util' import { resultSuccess } from '../_util';
const consoleInfo = { const consoleInfo = {
//访问量 //访问量
@@ -13,7 +13,7 @@ const consoleInfo = {
saleroom: { saleroom: {
weekSaleroom: Random.float(10000, 99999, 2, 2), weekSaleroom: Random.float(10000, 99999, 2, 2),
amount: Random.float(99999, 999999, 2, 2), amount: Random.float(99999, 999999, 2, 2),
degree:Random.float(10,99) degree: Random.float(10, 99),
}, },
//订单量 //订单量
orderLarge: { orderLarge: {
@@ -27,10 +27,9 @@ const consoleInfo = {
weekLarge: Random.float(10000, 99999, 2, 2), weekLarge: Random.float(10000, 99999, 2, 2),
rise: Random.float(10, 99), rise: Random.float(10, 99),
decline: Random.float(10, 99), decline: Random.float(10, 99),
amount:Random.float(99999,999999,2,2) amount: Random.float(99999, 999999, 2, 2),
}, },
} };
export default [ export default [
//主控台 卡片数据 //主控台 卡片数据
@@ -41,7 +40,5 @@ export default [
response: () => { response: () => {
return resultSuccess(consoleInfo); return resultSuccess(consoleInfo);
}, },
} },
] ];

89
mock/system/menu.ts Normal file
View File

@@ -0,0 +1,89 @@
import { resultSuccess } from '../_util';
const menuList = () => {
const result: any[] = [
{
label: 'Dashboard',
key: 'dashboard',
type: 1,
subtitle: 'dashboard',
openType: 1,
auth: 'dashboard',
path: '/dashboard',
children: [
{
label: '主控台',
key: 'console',
type: 1,
subtitle: 'console',
openType: 1,
auth: 'console',
path: '/dashboard/console',
},
{
label: '工作台',
key: 'workplace',
type: 1,
subtitle: 'workplace',
openType: 1,
auth: 'workplace',
path: '/dashboard/workplace',
},
],
},
{
label: '表单管理',
key: 'form',
type: 1,
subtitle: 'form',
openType: 1,
auth: 'form',
path: '/form',
children: [
{
label: '基础表单',
key: 'basic-form',
type: 1,
subtitle: 'basic-form',
openType: 1,
auth: 'basic-form',
path: '/form/basic-form',
},
{
label: '分步表单',
key: 'step-form',
type: 1,
subtitle: 'step-form',
openType: 1,
auth: 'step-form',
path: '/form/step-form',
},
{
label: '表单详情',
key: 'detail',
type: 1,
subtitle: 'detail',
openType: 1,
auth: 'detail',
path: '/form/detail',
},
],
},
];
return result;
};
export default [
{
url: '/api/menu/list',
timeout: 1000,
method: 'get',
response: () => {
const list = menuList();
return resultSuccess({
list,
});
},
},
];

45
mock/system/role.ts Normal file
View File

@@ -0,0 +1,45 @@
import { resultSuccess, doCustomTimes } from '../_util';
function getMenuKeys() {
const keys = ['dashboard', 'console', 'workplace', 'basic-form', 'step-form', 'detail'];
const newKeys = [];
doCustomTimes(parseInt(Math.random() * 6), () => {
const key = keys[Math.floor(Math.random() * keys.length)];
newKeys.push(key);
});
return Array.from(new Set(newKeys));
}
const roleList = (pageSize) => {
const result: any[] = [];
doCustomTimes(pageSize, () => {
result.push({
id: '@integer(10,100)',
name: '@cname()',
explain: '@cname()',
isDefault: '@boolean()',
menu_keys: getMenuKeys(),
create_date: `@date('yyyy-MM-dd hh:mm:ss')`,
'status|1': ['normal', 'enable', 'disable'],
});
});
return result;
};
export default [
{
url: '/api/role/list',
timeout: 1000,
method: 'get',
response: ({ query }) => {
const { page = 1, pageSize = 10 } = query;
const list = roleList(Number(pageSize));
return resultSuccess({
page: Number(page),
pageSize: Number(pageSize),
pageCount: 60,
list,
});
},
},
];

View File

@@ -1,8 +1,8 @@
import { Random } from 'mockjs' import { Random } from 'mockjs';
import { resultSuccess, doCustomTimes, resultPageSuccess } from '../_util' import { resultSuccess, doCustomTimes } from '../_util';
const tableList = ((pageSize) => { const tableList = (pageSize) => {
const result:any[] = [] const result: any[] = [];
doCustomTimes(pageSize, () => { doCustomTimes(pageSize, () => {
result.push({ result.push({
id: '@integer(10,100)', id: '@integer(10,100)',
@@ -14,12 +14,11 @@ const tableList = ((pageSize) => {
date: `@date('yyyy-MM-dd')`, date: `@date('yyyy-MM-dd')`,
time: `@time('HH:mm')`, time: `@time('HH:mm')`,
'no|100000-10000000': 100000, 'no|100000-10000000': 100000,
'status|1': ['normal', 'enable', 'disable'], 'status|1': [true, false],
}); });
})
return result
}); });
return result;
};
export default [ export default [
//表格数据列表 //表格数据列表
@@ -29,16 +28,13 @@ export default [
method: 'get', method: 'get',
response: ({ query }) => { response: ({ query }) => {
const { page = 1, pageSize = 10 } = query; const { page = 1, pageSize = 10 } = query;
const list = tableList(Number(pageSize)) const list = tableList(Number(pageSize));
return resultSuccess({ return resultSuccess({
page: Number(page), page: Number(page),
pageSize: Number(pageSize), pageSize: Number(pageSize),
pageCount: 60, pageCount: 60,
list list,
} });
);
}, },
} },
] ];

View File

@@ -1,5 +1,4 @@
import { MockMethod } from 'vite-plugin-mock' import { resultSuccess } from '../_util';
import { resultSuccess, getRequestToken } from '../_util'
const menusList = [ const menusList = [
{ {
@@ -18,7 +17,7 @@ const menusList = [
component: 'DashboardConsole', component: 'DashboardConsole',
meta: { meta: {
title: '主控台', title: '主控台',
} },
}, },
{ {
path: 'monitor', path: 'monitor',
@@ -26,7 +25,7 @@ const menusList = [
component: 'DashboardMonitor', component: 'DashboardMonitor',
meta: { meta: {
title: '监控页', title: '监控页',
} },
}, },
{ {
path: 'workplace', path: 'workplace',
@@ -35,11 +34,11 @@ const menusList = [
meta: { meta: {
hidden: true, hidden: true,
title: '工作台', title: '工作台',
} },
}, },
], ],
} },
] ];
export default [ export default [
{ {
@@ -49,5 +48,5 @@ export default [
response: () => { response: () => {
return resultSuccess(menusList); return resultSuccess(menusList);
}, },
} },
] ];

View File

@@ -1,9 +1,9 @@
import Mock from 'mockjs' import Mock from 'mockjs';
import { resultSuccess, getRequestToken } from '../_util' import { resultSuccess } from '../_util';
const Random = Mock.Random const Random = Mock.Random;
const token = Random.string('upper', 32, 32) const token = Random.string('upper', 32, 32);
const adminInfo = { const adminInfo = {
userId: '1', userId: '1',
@@ -25,9 +25,17 @@ const adminInfo = {
{ {
roleName: '工作台', roleName: '工作台',
value: 'dashboard_workplace', value: 'dashboard_workplace',
} },
{
roleName: '基础列表',
value: 'basic_list',
},
{
roleName: '基础列表删除',
value: 'basic_list_delete',
},
], ],
} };
export default [ export default [
{ {
@@ -48,4 +56,4 @@ export default [
return resultSuccess(adminInfo); return resultSuccess(adminInfo);
}, },
}, },
] ];

View File

@@ -1,6 +1,6 @@
{ {
"name": "naive-ui-admin", "name": "naive-ui-admin",
"version": "1.2", "version": "1.5.0",
"author": { "author": {
"name": "Ahjung", "name": "Ahjung",
"email": "735878602@qq.com", "email": "735878602@qq.com",
@@ -8,8 +8,12 @@
}, },
"private": true, "private": true,
"scripts": { "scripts": {
"bootstrap": "yarn install",
"serve": "npm run dev",
"dev": "vite", "dev": "vite",
"build": "vite build && esno ./build/script/postBuild.ts", "build": "vite build && esno ./build/script/postBuild.ts",
"build:no-cache": "yarn clean:cache && npm run build",
"report": "cross-env REPORT=true npm run build",
"preview": "vite preview", "preview": "vite preview",
"build typecheck": "vuedx-typecheck . && vite build", "build typecheck": "vuedx-typecheck . && vite build",
"deploy": "gh-pages -d dist", "deploy": "gh-pages -d dist",
@@ -31,10 +35,10 @@
"element-resize-detector": "^1.2.3", "element-resize-detector": "^1.2.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"makeit-captcha": "^1.2.5",
"mitt": "^2.1.0", "mitt": "^2.1.0",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"naive-ui": "^2.15.5", "naive-ui": "^2.15.11",
"nprogress": "^1.0.0-1",
"pinia": "^2.0.0-beta.3", "pinia": "^2.0.0-beta.3",
"qs": "^6.10.1", "qs": "^6.10.1",
"vfonts": "^0.1.0", "vfonts": "^0.1.0",
@@ -61,27 +65,32 @@
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"eslint": "^7.28.0", "eslint": "^7.28.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-define-config": "^1.0.9",
"eslint-plugin-jest": "^24.4.0",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^7.11.1", "eslint-plugin-vue": "^7.11.1",
"esno": "^0.7.3", "esno": "^0.7.3",
"gh-pages": "^3.2.0", "gh-pages": "^3.2.0",
"husky": "^6.0.0", "husky": "^6.0.0",
"jest": "^27.0.6",
"less": "^4.1.1", "less": "^4.1.1",
"less-loader": "^9.0.0", "less-loader": "^9.0.0",
"lint-staged": "^11.0.0", "lint-staged": "^11.0.0",
"postcss": "^8.3.5", "postcss": "^8.3.5",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"pretty-quick": "^3.1.0", "pretty-quick": "^3.1.0",
"rimraf": "^3.0.2",
"stylelint": "^13.13.1", "stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2", "stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0", "stylelint-config-standard": "^22.0.0",
"stylelint-order": "^4.1.0", "stylelint-order": "^4.1.0",
"stylelint-scss": "^3.19.0", "stylelint-scss": "^3.19.0",
"tailwindcss": "^2.2.4", "tailwindcss": "^2.2.7",
"typescript": "^4.3.2", "typescript": "^4.3.5",
"vite": "^2.4.2", "vite": "2.3.6",
"vite-plugin-compression": "^0.3.1",
"vite-plugin-html": "^2.0.7", "vite-plugin-html": "^2.0.7",
"vite-plugin-mock": "^2.9.1", "vite-plugin-mock": "^2.9.3",
"vite-plugin-style-import": "^1.0.1", "vite-plugin-style-import": "^1.0.1",
"vue-eslint-parser": "^7.8.0" "vue-eslint-parser": "^7.8.0"
}, },

View File

@@ -11,70 +11,68 @@
</AppProvider> </AppProvider>
</NConfigProvider> </NConfigProvider>
<transition v-if="isLock && $route.name != 'login'" name="slide-up"> <transition v-if="isLock && $route.name !== 'login'" name="slide-up">
<LockScreen /> <LockScreen />
</transition> </transition>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, computed, onMounted, onUnmounted } from 'vue' import { defineComponent, computed, onMounted, onUnmounted } from 'vue';
import { zhCN, dateZhCN, createTheme, inputDark, datePickerDark, darkTheme } from 'naive-ui' import { zhCN, dateZhCN, createTheme, inputDark, datePickerDark, darkTheme } from 'naive-ui';
import { LockScreen } from '@/components/Lockscreen' import { LockScreen } from '@/components/Lockscreen';
import { AppProvider } from '@/components/Application' import { AppProvider } from '@/components/Application';
import { useLockscreenStore } from '@/store/modules/lockscreen' import { useLockscreenStore } from '@/store/modules/lockscreen';
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router';
import { useDesignSettingStore } from '@/store/modules/designSetting' import { useDesignSettingStore } from '@/store/modules/designSetting';
export default defineComponent({ export default defineComponent({
name: 'App', name: 'App',
components: { LockScreen, AppProvider }, components: { LockScreen, AppProvider },
setup() { setup() {
const route = useRoute() const route = useRoute();
const useLockscreen = useLockscreenStore() const useLockscreen = useLockscreenStore();
const designStore = useDesignSettingStore() const designStore = useDesignSettingStore();
const isLock = computed(() => useLockscreen.isLock) const isLock = computed(() => useLockscreen.isLock);
const lockTime = computed(() => useLockscreen.lockTime) const lockTime = computed(() => useLockscreen.lockTime);
/**
* @type import('naive-ui').GlobalThemeOverrides
*/
const getThemeOverrides = computed(() => { const getThemeOverrides = computed(() => {
return { return {
common: { common: {
primaryColor: designStore.appTheme, primaryColor: designStore.appTheme,
primaryColorHover: '#57a3f3' primaryColorHover: '#57a3f3',
} },
} };
}) });
const getDarkTheme = computed(() => (designStore.darkTheme ? darkTheme : undefined)) const getDarkTheme = computed(() => (designStore.darkTheme ? darkTheme : undefined));
let timer let timer;
const timekeeping = () => { const timekeeping = () => {
clearInterval(timer) clearInterval(timer);
if (route.name == 'login' || isLock.value) return if (route.name == 'login' || isLock.value) return;
// 设置不锁屏 // 设置不锁屏
useLockscreen.setLock(false) useLockscreen.setLock(false);
// 重置锁屏时间 // 重置锁屏时间
useLockscreen.setLockTime() useLockscreen.setLockTime();
timer = setInterval(() => { timer = setInterval(() => {
// 锁屏倒计时递减 // 锁屏倒计时递减
useLockscreen.setLockTime(lockTime.value - 1) useLockscreen.setLockTime(lockTime.value - 1);
if (lockTime.value <= 0) { if (lockTime.value <= 0) {
// 设置锁屏 // 设置锁屏
useLockscreen.setLock(true) useLockscreen.setLock(true);
return clearInterval(timer) return clearInterval(timer);
}
}, 1000)
} }
}, 1000);
};
onMounted(() => { onMounted(() => {
document.addEventListener('mousedown', timekeeping) document.addEventListener('mousedown', timekeeping);
}) });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('mousedown', timekeeping) document.removeEventListener('mousedown', timekeeping);
}) });
return { return {
darkTheme: createTheme([inputDark, datePickerDark]), darkTheme: createTheme([inputDark, datePickerDark]),
@@ -82,16 +80,14 @@ export default defineComponent({
zhCN, zhCN,
dateZhCN, dateZhCN,
isLock, isLock,
getThemeOverrides getThemeOverrides,
} };
} },
}) });
</script> </script>
<style lang="less"> <style lang="less">
@import 'styles/global.less';
@import 'styles/common.less'; @import 'styles/common.less';
@import 'styles/override.less';
.slide-up-enter-active, .slide-up-enter-active,
.slide-up-leave-active { .slide-up-leave-active {

View File

@@ -1,11 +1,9 @@
import http from '@/utils/http/axios' import http from '@/utils/http/axios';
//获取主控台信息 //获取主控台信息
export function getConsoleInfo() { export function getConsoleInfo() {
return http.request( return http.request({
{
url: '/dashboard/console', url: '/dashboard/console',
method: 'get' method: 'get',
} });
)
} }

View File

@@ -1,33 +1,23 @@
import http from '@/utils/http/axios' import http from '@/utils/http/axios';
import {
GetByUserIdParams,
GetMenuListByUserIdResult,
GetAuthCodeByUserIdResult
} from './model/menuModel'
enum Api {
adminMenus = '/menus',
GetBtnCodeListByUserId = '/getBtnCodeListByUserId'
}
/** /**
* @description: 根据用户id获取用户菜单 * @description: 根据用户id获取用户菜单
*/ */
export function adminMenus() { export function adminMenus() {
return http.request<GetMenuListByUserIdResult>({ return http.request({
url: Api.adminMenus, url: '/menus',
method: 'GET' method: 'GET',
}) });
} }
/** /**
* 根据用户Id获取权限编码 * 获取tree菜单列表
* @param params * @param params
*/ */
export function getBtnCodeListByUserId(params: GetByUserIdParams) { export function getMenuList(params?) {
return http.request<GetAuthCodeByUserIdResult>({ return http.request({
url: Api.GetBtnCodeListByUserId, url: '/menu/list',
method: 'GET', method: 'GET',
params params,
}) });
} }

11
src/api/system/role.ts Normal file
View File

@@ -0,0 +1,11 @@
import http from '@/utils/http/axios';
/**
* @description: 角色列表
*/
export function getRoleList() {
return http.request({
url: '/role/list',
method: 'GET',
});
}

View File

@@ -1,44 +1,41 @@
import http from '@/utils/http/axios' import http from '@/utils/http/axios';
import { LoginParams, LoginResultModel } from './model/userModel'
export interface BasicResponseModel<T = any> { export interface BasicResponseModel<T = any> {
code: number code: number;
message: string message: string;
result: T result: T;
} }
export interface BasicPageParams { export interface BasicPageParams {
pageNumber: number pageNumber: number;
pageSize: number pageSize: number;
total: number total: number;
} }
/** /**
* @description: 获取用户信息 * @description: 获取用户信息
*/ */
export function getUserInfo() { export function getUserInfo() {
return http.request( return http.request({
{
url: '/admin_info', url: '/admin_info',
method: 'get' method: 'get',
} });
)
} }
/** /**
* @description: 用户登录 * @description: 用户登录
*/ */
export function login(params: LoginParams) { export function login(params) {
return http.request<BasicResponseModel<LoginResultModel>>( return http.request<BasicResponseModel>(
{ {
url: '/login', url: '/login',
method: 'POST', method: 'POST',
params params,
}, },
{ {
isTransformRequestResult: false isTransformResponse: false,
} }
) );
} }
/** /**
@@ -49,12 +46,12 @@ export function changePassword(params, uid) {
{ {
url: `/user/u${uid}/changepw`, url: `/user/u${uid}/changepw`,
method: 'POST', method: 'POST',
params params,
}, },
{ {
isTransformRequestResult: false isTransformResponse: false,
} }
) );
} }
/** /**
@@ -64,6 +61,6 @@ export function logout(params) {
return http.request({ return http.request({
url: '/login/logout', url: '/login/logout',
method: 'POST', method: 'POST',
params params,
}) });
} }

View File

@@ -1,12 +1,10 @@
import http from '@/utils/http/axios' import http from '@/utils/http/axios';
//获取table //获取table
export function getTableList(params) { export function getTableList(params) {
return http.request( return http.request({
{
url: '/table/list', url: '/table/list',
method: 'get', method: 'get',
params params,
} });
)
} }

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 34 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 26 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 33 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,4 +1,6 @@
<template> <template>
<n-loading-bar-provider>
<LoadingContent />
<n-dialog-provider> <n-dialog-provider>
<DialogContent /> <DialogContent />
<n-notification-provider> <n-notification-provider>
@@ -8,18 +10,34 @@
</n-message-provider> </n-message-provider>
</n-notification-provider> </n-notification-provider>
</n-dialog-provider> </n-dialog-provider>
</n-loading-bar-provider>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent } from 'vue';
import { MessageContent } from '@/components/MessageContent' import {
import { DialogContent } from '@/components/DialogContent' NDialogProvider,
NNotificationProvider,
NMessageProvider,
NLoadingBarProvider,
} from 'naive-ui';
import { LoadingContent } from '@/components/LoadingContent';
import { MessageContent } from '@/components/MessageContent';
import { DialogContent } from '@/components/DialogContent';
export default defineComponent({ export default defineComponent({
name: 'Application', name: 'Application',
components: { MessageContent, DialogContent }, components: {
NDialogProvider,
NNotificationProvider,
NMessageProvider,
NLoadingBarProvider,
LoadingContent,
MessageContent,
DialogContent,
},
setup() { setup() {
return {} return {};
} },
}) });
</script> </script>

View File

@@ -1,3 +1,3 @@
import AppProvider from './Application.vue' import AppProvider from './Application.vue';
export { AppProvider } export { AppProvider };

View File

@@ -1,3 +1,3 @@
import DialogContent from './index.vue' import DialogContent from './index.vue';
export { DialogContent } export { DialogContent };

View File

@@ -1,12 +1,12 @@
<template></template> <template></template>
<script lang="ts"> <script lang="ts">
import { useDialog } from 'naive-ui' import { useDialog } from 'naive-ui';
export default { export default {
name: 'DialogContent', name: 'DialogContent',
setup() { setup() {
//挂载在 window 方便与在js中使用 //挂载在 window 方便与在js中使用
window.$dialog = useDialog() window['$dialog'] = useDialog();
} },
} };
</script> </script>

View File

@@ -0,0 +1,3 @@
import LoadingContent from './index.vue';
export { LoadingContent };

View File

@@ -0,0 +1,12 @@
<template></template>
<script lang="ts">
import { useLoadingBar } from 'naive-ui';
export default {
name: 'LoadingContent',
setup() {
//挂载在 window 方便与在js中使用
window['$loading'] = useLoadingBar();
},
};
</script>

View File

@@ -7,7 +7,6 @@
@contextmenu.prevent @contextmenu.prevent
> >
<template v-if="!showLogin"> <template v-if="!showLogin">
<div class="lock-box"> <div class="lock-box">
<div class="lock"> <div class="lock">
<span class="lock-icon" title="解锁屏幕" @click="onLockLogin(true)"> <span class="lock-icon" title="解锁屏幕" @click="onLockLogin(true)">
@@ -16,14 +15,13 @@
</n-icon> </n-icon>
</span> </span>
</div> </div>
</div> </div>
<!--充电--> <!--充电-->
<recharge <recharge
:battery="battery" :battery="battery"
:battery-status="batteryStatus" :battery-status="batteryStatus"
:calc-discharging-time="calcDischargingTime" :calc-discharging-time="calcDischargingTime"
></recharge> />
<div class="local-time"> <div class="local-time">
<div class="time">{{ hour }}:{{ minute }}</div> <div class="time">{{ hour }}:{{ minute }}</div>
@@ -50,9 +48,10 @@
type="password" type="password"
autofocus autofocus
v-model:value="loginParams.password" v-model:value="loginParams.password"
placeholder="请输入登录密码"> placeholder="请输入登录密码"
>
<template #suffix> <template #suffix>
<n-icon @click="onLogin" style="cursor: pointer;"> <n-icon @click="onLogin" style="cursor: pointer">
<LoadingOutlined v-if="loginLoading" /> <LoadingOutlined v-if="loginLoading" />
<arrow-right-outlined v-else /> <arrow-right-outlined v-else />
</n-icon> </n-icon>
@@ -68,58 +67,56 @@
<div><a @click="goLogin">重新登录</a></div> <div><a @click="goLogin">重新登录</a></div>
<div><a @click="onLogin">进入系统</a></div> <div><a @click="onLogin">进入系统</a></div>
</div> </div>
</div> </div>
</template> </template>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, onMounted, reactive, toRefs, computed } from 'vue' import { defineComponent, reactive, toRefs } from 'vue';
import { ResultEnum } from '@/enums/httpEnum' import { ResultEnum } from '@/enums/httpEnum';
import recharge from './Recharge.vue' import recharge from './Recharge.vue';
import { import {
LockOutlined, LockOutlined,
LoadingOutlined, LoadingOutlined,
UnlockOutlined,
UserOutlined, UserOutlined,
ApiOutlined, ApiOutlined,
ArrowRightOutlined, ArrowRightOutlined,
WifiOutlined, WifiOutlined,
} from '@vicons/antd' } from '@vicons/antd';
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router';
import { useOnline } from '@/hooks/useOnline' import { useOnline } from '@/hooks/useOnline';
import { useTime } from '@/hooks/useTime' import { useTime } from '@/hooks/useTime';
import { useBattery } from '@/hooks/useBattery' import { useBattery } from '@/hooks/useBattery';
import { useLockscreenStore } from '@/store/modules/lockscreen' import { useLockscreenStore } from '@/store/modules/lockscreen';
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user';
export default defineComponent({ export default defineComponent({
name: 'Lockscreen', name: 'Lockscreen',
components: { components: {
LockOutlined, LockOutlined,
LoadingOutlined, LoadingOutlined,
UnlockOutlined,
UserOutlined, UserOutlined,
ArrowRightOutlined, ArrowRightOutlined,
ApiOutlined, ApiOutlined,
WifiOutlined, WifiOutlined,
recharge, recharge,
}, },
setup(props, { emit }) { setup() {
const useLockscreen = useLockscreenStore() const useLockscreen = useLockscreenStore();
const userStore = useUserStore(); const userStore = useUserStore();
// 获取时间 // 获取时间
const { month, day, hour, minute, second, week } = useTime() const { month, day, hour, minute, second, week } = useTime();
const { online } = useOnline() const { online } = useOnline();
const router = useRouter() const router = useRouter();
const route = useRoute() const route = useRoute();
const { battery, batteryStatus, calcDischargingTime } = useBattery() const { battery, batteryStatus, calcDischargingTime } = useBattery();
const { username } = userStore.getUserInfo || {} const userInfo: object = userStore.getUserInfo || {};
const username = userInfo['username'] || '';
const state = reactive({ const state = reactive({
showLogin: false, showLogin: false,
loginLoading: false, // 正在登录 loginLoading: false, // 正在登录
@@ -127,45 +124,45 @@ export default defineComponent({
errorMsg: '密码错误', errorMsg: '密码错误',
loginParams: { loginParams: {
username: username || '', username: username || '',
password: '' password: '',
} },
}) });
// 解锁登录 // 解锁登录
const onLockLogin = (value: boolean) => (state.showLogin = value) const onLockLogin = (value: boolean) => (state.showLogin = value);
// 登录 // 登录
const onLogin = async () => { const onLogin = async () => {
if (!state.loginParams.password.trim()) { if (!state.loginParams.password.trim()) {
return return;
} }
const params = { const params = {
isLock: true, isLock: true,
...state.loginParams ...state.loginParams,
} };
state.loginLoading = true state.loginLoading = true;
const { code, result, message } = await userStore.login(params) const { code, message } = await userStore.login(params);
if (code === ResultEnum.SUCCESS) { if (code === ResultEnum.SUCCESS) {
onLockLogin(false) onLockLogin(false);
useLockscreen.setLock(false) useLockscreen.setLock(false);
} else { } else {
state.errorMsg = message state.errorMsg = message;
state.isLoginError = true state.isLoginError = true;
}
state.loginLoading = false
} }
state.loginLoading = false;
};
//重新登录 //重新登录
const goLogin = () => { const goLogin = () => {
onLockLogin(false) onLockLogin(false);
useLockscreen.setLock(false) useLockscreen.setLock(false);
router.replace({ router.replace({
path: '/login', path: '/login',
query: { query: {
redirect: route.fullPath redirect: route.fullPath,
} },
}) });
} };
return { return {
...toRefs(state), ...toRefs(state),
@@ -181,10 +178,10 @@ export default defineComponent({
calcDischargingTime, calcDischargingTime,
onLockLogin, onLockLogin,
onLogin, onLogin,
goLogin goLogin,
} };
} },
}) });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@@ -20,7 +20,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue' import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
name: 'HuaweiCharge', name: 'HuaweiCharge',
@@ -29,20 +29,20 @@ export default defineComponent({
battery: { battery: {
// 电池对象 // 电池对象
type: Object, type: Object,
default: () => ({}) default: () => ({}),
}, },
calcDischargingTime: { calcDischargingTime: {
// 电池剩余时间可用时间 // 电池剩余时间可用时间
type: String, type: String,
default: '' default: '',
}, },
batteryStatus: { batteryStatus: {
// 电池状态 // 电池状态
type: String, type: String,
validator: (val: string) => ['充电中', '已充满', '已断开电源'].includes(val) validator: (val: string) => ['充电中', '已充满', '已断开电源'].includes(val),
} },
} },
}) });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@@ -147,7 +147,6 @@ each(range(15), {
} }
}); });
@keyframes rotate { @keyframes rotate {
50% { 50% {
border-radius: 45% / 42% 38% 58% 49%; border-radius: 45% / 42% 38% 58% 49%;

View File

@@ -1,3 +1,3 @@
import LockScreen from './Lockscreen.vue' import LockScreen from './Lockscreen.vue';
export { LockScreen } export { LockScreen };

View File

@@ -1,3 +1,3 @@
import MessageContent from './index.vue' import MessageContent from './index.vue';
export { MessageContent } export { MessageContent };

View File

@@ -1,12 +1,12 @@
<template></template> <template></template>
<script lang="ts"> <script lang="ts">
import { useMessage } from 'naive-ui' import { useMessage } from 'naive-ui';
export default { export default {
name: 'MessageContent', name: 'MessageContent',
setup() { setup() {
//挂载在 window 方便与在js中使用 //挂载在 window 方便与在js中使用
window.$message = useMessage() window['$message'] = useMessage();
} },
} };
</script> </script>

View File

@@ -1,91 +0,0 @@
ProTable 重封装组件说明
====
封装说明
----
> 基础的使用方式与 API 与 [官方版(data-table)](https://www.naiveui.com/zh-CN/os-theme/components/data-table#tree) 本一致,在其基础上,封装了加载数据的方法。
>
> 你无需在你是用表格的页面进行分页逻辑处理,仅需向 ProTable 组件传递绑定 `:api="Promise"` 对象即可
>
> 例子1
----
(基础使用)
```vue
<template>
<ProTable
title="表格列表"
:columns="columns"
:api="loadDataTable"
:row-key="row => row.id"
@update:checked-row-keys="onCheckedRow"
>
<template #toolbar>
<n-button type="primary">添加会员</n-button>
</template>
</ProTable>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { ProTable } from '@/components/ProTable'
import { getTableList } from '@/api/table/list'
const columns = [
{
title: 'id',
key: 'id'
},
{
title: '名称',
key: 'name'
},
{
title: '地址',
key: 'address'
},
{
title: '日期',
key: 'date'
},
]
export default defineComponent({
components: { ProTable },
setup() {
const loadDataTable = async (params) => {
const data = await getTableList(params);
return data
}
return {
columns,
loadDataTable
}
}
})
</script>
```
API
----
ProTable 在 NaiveUi 的 data-table 上进行了一层封装,支持了一些预设,并且封装了一些行为。这里只列出与 data-table 不同的 api。
> requestPromise 参考上面例子写法
> ref可绑定ref 调用组件内部方法data-table本身的方法和参数
Methods
----
> reloadactionRef.value.reload()
> 其余方法,请打印查看
Slots
----
> 名称tableTitle | 表格顶部左侧区域
> 名称toolbar | 表格顶部右侧区域
更新时间
----
该文档最后更新于: 2021-07-12 PM 10:13

View File

@@ -1 +0,0 @@
export { default as ProTable } from './src/ProTable.vue';

View File

@@ -1,295 +0,0 @@
<template>
<div class="table-toolbar">
<!--顶部左侧区域-->
<div class="flex items-center table-toolbar-left ">
<template v-if="title">
<div class="table-toolbar-left-title">
{{ title }}
<n-tooltip trigger="hover" v-if="titleTooltip">
<template #trigger>
<n-icon size="18" class="ml-1 cursor-pointer text-gray-400">
<QuestionCircleOutlined/>
</n-icon>
</template>
{{ titleTooltip }}
</n-tooltip>
</div>
</template>
<slot name="tableTitle"></slot>
</div>
<div class="flex items-center table-toolbar-right">
<!--顶部右侧区域-->
<slot name="toolbar"></slot>
<!--刷新-->
<n-tooltip trigger="hover">
<template #trigger>
<div class="table-toolbar-right-icon" @click="reload">
<n-icon size="18">
<ReloadOutlined/>
</n-icon>
</div>
</template>
<span>刷新</span>
</n-tooltip>
<!--密度-->
<n-tooltip trigger="hover">
<template #trigger>
<div class="table-toolbar-right-icon">
<n-dropdown @select="densitySelect" trigger="click" :options="densityOptions" v-model:value="tableSize">
<n-icon size="18">
<ColumnHeightOutlined/>
</n-icon>
</n-dropdown>
</div>
</template>
<span>密度</span>
</n-tooltip>
<!--表格设置单独抽离成组件-->
<ColumnSetting></ColumnSetting>
</div>
</div>
<div class="s-table">
<n-data-table
v-bind="getBindValues"
:pagination="pagination"
@update:page="updatePage"
@update:page-size="updatePageSize"
>
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
<slot :name="item" v-bind="data"></slot>
</template>
</n-data-table>
</div>
</template>
<script lang="ts">
import { NDataTable } from 'naive-ui'
import { ref, defineComponent, reactive, unref, onMounted, toRaw, onBeforeMount, computed, toRefs, watch } from "vue"
import { ReloadOutlined, ColumnHeightOutlined, SettingOutlined, DragOutlined, QuestionCircleOutlined } from '@vicons/antd'
import { createTableContext } from './hooks/useTableContext';
import ColumnSetting from './components/settings/ColumnSetting.vue'
import { useLoading } from './hooks/useLoading';
import { useColumns } from './hooks/useColumns';
import { useDataSource } from './hooks/useDataSource';
import { usePagination } from './hooks/usePagination';
import { basicProps } from './props'
import { BasicTableProps } from './types/table'
const densityOptions = [
{
type: "menu",
label: '紧凑',
key: 'small',
},
{
type: "menu",
label: '默认',
key: "medium"
},
{
type: "menu",
label: '宽松',
key: 'large'
}
]
export default defineComponent({
components: {
ReloadOutlined, ColumnHeightOutlined, SettingOutlined, DragOutlined, ColumnSetting, QuestionCircleOutlined
},
props: {
...NDataTable.props, // 这里继承原 UI 组件的 props
...basicProps
},
emits: [
'fetch-success',
'fetch-error',
'update:checked-row-keys'
],
setup(props, { emit }) {
const wrapRef = ref<Nullable<HTMLDivElement>>(null);
const tableData = ref<Recordable[]>([]);
const innerPropsRef = ref<Partial<BasicTableProps>>();
const getProps = computed(() => {
return { ...props, ...unref(innerPropsRef) } as BasicTableProps;
});
const { getLoading, setLoading } = useLoading(getProps);
const {
getPaginationInfo,
getPagination,
setPagination,
setShowPagination,
getShowPagination,
} = usePagination(getProps)
const { getDataSourceRef, getRowKey, getDataSource, setDataSource, reload } = useDataSource(
getProps, {
getPaginationInfo,
setPagination,
tableData,
setLoading
}, emit
)
const {
getPageColumns,
setColumns,
getColumns,
getCacheColumns,
setCacheColumnsField,
getColumnsRef
} = useColumns(getProps)
const state = reactive({
tableSize: 'medium',
isColumnSetting: false
})
//页码切换
function updatePage(page) {
setPagination({ page: page, });
reload()
}
//分页数量切换
function updatePageSize(size) {
setPagination({ page: 1, pageSize: size, });
reload()
}
//密度切换
function densitySelect(e) {
state.tableSize = e
}
//选中行
function updateCheckedRowKeys(rowKeys) {
emit('update:checked-row-keys', rowKeys)
}
//重置 Columns
const resetColumns = () => {
columns.map(item => {
item.isShow = true
})
}
//获取表格大小
const getTableSize = computed(() => state.tableSize)
//组装表格信息
const getBindValues = computed(() => {
const tableData = unref(getDataSourceRef);
let propsData = {
...unref(getProps),
loading: unref(getLoading),
columns: toRaw(unref(getPageColumns)),
rowKey: unref(getRowKey),
data: tableData,
size: unref(getTableSize),
remote: true
}
return propsData
})
//获取分页信息
const pagination = computed(() => toRaw(unref(getPaginationInfo)))
function setProps(props: Partial<BasicTableProps>) {
innerPropsRef.value = { ...unref(innerPropsRef), ...props };
}
const tableAction: TableActionType = {
reload,
setColumns,
setLoading,
setProps,
getColumns,
getPageColumns,
getCacheColumns,
setCacheColumnsField,
emit,
getSize: () => {
return unref(getBindValues).size as SizeType;
},
};
createTableContext({ ...tableAction, wrapRef, getBindValues });
return {
...toRefs(state),
getBindValues,
densityOptions,
reload,
densitySelect,
updatePage,
updatePageSize,
updateCheckedRowKeys,
pagination,
resetColumns,
tableAction
}
}
})
</script>
<style lang='less' scoped>
.table-toolbar {
display: flex;
justify-content: space-between;
padding: 0 0 16px 0;
&-left {
display: flex;
align-items: center;
justify-content: flex-start;
flex: 1;
&-title {
display: flex;
align-items: center;
justify-content: flex-start;
font-size: 16px;
font-weight: 600;
}
}
&-right {
display: flex;
justify-content: flex-end;
flex: 1;
&-icon {
margin-left: 12px;
font-size: 16px;
cursor: pointer;
color:var(--text-color);
:hover {
color: #1890ff;
}
}
}
}
.table-toolbar-inner-popover-title {
padding: 2px 0;
}
</style>

View File

@@ -1,275 +0,0 @@
<template>
<n-tooltip trigger="hover">
<template #trigger>
<div class="cursor-pointer table-toolbar-right-icon">
<n-popover trigger="click" :width="230" class="toolbar-popover" placement="bottom-end">
<template #trigger>
<n-icon size="18">
<SettingOutlined/>
</n-icon>
</template>
<template #header>
<div class="table-toolbar-inner-popover-title">
<n-space>
<n-checkbox v-model:checked="checkAll" @update:checked="onCheckAll">列展示</n-checkbox>
<n-checkbox v-model:checked="selection" @update:checked="onSelection">勾选列</n-checkbox>
<n-button text type="info" size="small" class="mt-1" @click="resetColumns">重置</n-button>
</n-space>
</div>
</template>
<div class="table-toolbar-inner">
<n-checkbox-group v-model:value="checkList" @update:value="onChange">
<Draggable v-model="columnsList" animation="300" item-key="key" @end="draggableEnd">
<template #item="{element, index}">
<div class="table-toolbar-inner-checkbox" :class="{'table-toolbar-inner-checkbox-dark':getDarkTheme === true}">
<span class="drag-icon">
<n-icon size="18">
<DragOutlined/>
</n-icon>
</span>
<n-checkbox :value="element.key" :label="element.title"/>
<div class="fixed-item">
<n-tooltip trigger="hover" placement="bottom">
<template #trigger>
<n-icon size="18" :color="element.fixed === 'left' ? '#2080f0':undefined"
class="cursor-pointer" @click="fixedColumn(element,'left')">
<VerticalRightOutlined />
</n-icon>
</template>
<span>固定到左侧</span>
</n-tooltip>
<n-divider vertical/>
<n-tooltip trigger="hover" placement="bottom">
<template #trigger>
<n-icon size="18" :color="element.fixed === 'right' ? '#2080f0':undefined"
class="cursor-pointer" @click="fixedColumn(element,'right')">
<VerticalLeftOutlined />
</n-icon>
</template>
<span>固定到右侧</span>
</n-tooltip>
</div>
</div>
</template>
</Draggable>
</n-checkbox-group>
</div>
</n-popover>
</div>
</template>
<span>列设置</span>
</n-tooltip>
</template>
<script lang="ts">
import { ref, defineComponent, reactive, unref, toRaw, computed, toRefs, watchEffect } from "vue"
import { useTableContext } from '../../hooks/useTableContext';
import { ReloadOutlined, ColumnHeightOutlined, SettingOutlined, DragOutlined, VerticalRightOutlined, VerticalLeftOutlined } from '@vicons/antd'
import Draggable from 'vuedraggable/src/vuedraggable'
import { useDesignSetting } from "@/hooks/setting/useDesignSetting";
interface Options {
title: string;
key: string;
fixed?: boolean | 'left' | 'right';
}
export default defineComponent({
name: 'ColumnSetting',
components: {
ReloadOutlined, ColumnHeightOutlined, SettingOutlined, DragOutlined, Draggable,
VerticalRightOutlined, VerticalLeftOutlined
},
setup(props, { emit }) {
const { getDarkTheme } = useDesignSetting()
const table = useTableContext();
const columnsList = ref<Options[]>([]);
const cacheColumnsList = ref<Options[]>([]);
const state = reactive({
selection: false,
checkAll: true,
checkList: [],
defaultCheckList: []
})
const getSelection = computed(() => {
return state.selection
})
watchEffect(() => {
const columns = table.getColumns();
if (columns.length) {
init();
}
});
//初始化
function init() {
const columns = getColumns();
const checkList = columns.map(item => item.key)
state.checkList = checkList
state.defaultCheckList = checkList
columnsList.value = columns
cacheColumnsList.value = columns
}
//切换
function onChange(checkList) {
if (state.selection) {
checkList.unshift('selection')
}
setColumns(checkList)
}
//设置
function setColumns(columns) {
table.setColumns(columns)
}
//获取
function getColumns() {
let newRet = []
table.getColumns().forEach(item => {
newRet.push({...item })
})
return newRet
}
//重置
function resetColumns() {
state.checkList = [...state.defaultCheckList]
state.checkAll = true;
let cacheColumnsKeys:any[] = table.getCacheColumns()
let newColumns = cacheColumnsKeys.map(item => {
return {
...item,
fixed:undefined
}
})
setColumns(newColumns);
columnsList.value = newColumns
}
//全选
function onCheckAll(e) {
let checkList = table.getCacheColumns(true)
if (e) {
setColumns(checkList);
state.checkList = checkList
} else {
setColumns([]);
state.checkList = []
}
}
//拖拽排序
function draggableEnd() {
const newColumns = toRaw(unref(columnsList))
columnsList.value = newColumns
setColumns(newColumns);
}
//勾选列
function onSelection(e) {
let checkList = table.getCacheColumns()
if (e) {
checkList.unshift({ type: 'selection', key: 'selection' })
setColumns(checkList);
} else {
checkList.splice(0, 1)
setColumns(checkList);
}
}
//固定
function fixedColumn(item, fixed) {
console.log('item',item)
if (!state.checkList.includes(item.key)) return;
let columns = getColumns();
const isFixed = item.fixed === fixed ? undefined : fixed
let index = columns.findIndex(res => res.key === item.key)
console.log('index',index)
if(index !== -1){
columns[index].fixed = isFixed;
}
table.setCacheColumnsField(item.key, { fixed: isFixed })
columnsList.value[index].fixed = isFixed
console.log('columnsList',columnsList.value)
setColumns(columns);
}
return {
...toRefs(state),
columnsList,
getDarkTheme,
onChange,
onCheckAll,
onSelection,
resetColumns,
fixedColumn,
draggableEnd,
getSelection
}
}
})
</script>
<style lang="less">
.table-toolbar {
&-inner-popover-title{
padding: 3px 0;
}
&-right {
&-icon {
margin-left: 12px;
font-size: 16px;
color:var(--text-color);
cursor: pointer;
:hover {
color: #1890ff;
}
}
}
}
.table-toolbar-inner {
&-checkbox {
display: flex;
align-items: center;
padding: 10px 14px;
&:hover {
background: #e6f7ff;
}
.drag-icon {
display: inline-flex;
margin-right: 8px;
cursor: move;
}
.fixed-item {
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: auto;
}
.ant-checkbox-wrapper {
flex: 1;
&:hover {
color: #1890ff !important;
}
}
}
&-checkbox-dark{
&:hover {
background: hsla(0, 0%, 100%, .08);
}
}
}
.toolbar-popover{
.n-popover__content{
padding: 0;
}
}
</style>

View File

@@ -1,95 +0,0 @@
import { ref, Ref, ComputedRef, unref, computed, watch, toRaw } from 'vue';
import type { BasicColumn, BasicTableProps } from '../types/table';
import { isEqual, cloneDeep } from 'lodash-es';
import { isArray, isString } from '@/utils/is';
export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>;
let cacheColumns = unref(propsRef).columns;
const getColumnsRef = computed(() => {
const columns = cloneDeep(unref(columnsRef));
return columns;
})
const getPageColumns = computed(() => {
const pageColumns = unref(getColumnsRef);
const columns = cloneDeep(pageColumns);
return columns
})
watch(
() => unref(propsRef).columns,
(columns) => {
columnsRef.value = columns;
cacheColumns = columns;
}
);
//设置
function setColumns(columnList: string[]) {
const columns: any[] = cloneDeep(columnList);
if (!isArray(columns)) return;
if (!columns.length) {
columnsRef.value = [];
return;
}
const cacheKeys = cacheColumns.map((item) => item.key);
//针对拖拽排序
if (!isString(columns[0])) {
columnsRef.value = columns;
} else {
const newColumns: any[] = []
cacheColumns.forEach(item => {
if (columnList.includes(item.key)) {
newColumns.push({ ...item })
}
})
if (!isEqual(cacheKeys, columns)) {
newColumns.sort((prev, next) => {
return (
cacheKeys.indexOf(prev.key) - cacheKeys.indexOf(next.key)
);
});
}
columnsRef.value = newColumns
}
}
//获取
function getColumns() {
let columns = toRaw(unref(getColumnsRef));
return columns.map(item => {
return { ...item, title: item.title, key: item.key, fixed: item.fixed || undefined }
})
}
//获取原始
function getCacheColumns(isKey?: boolean): any[] {
return isKey ? cacheColumns.map(item => item.key) : cacheColumns;
}
//更新原始数据单个字段
function setCacheColumnsField(dataIndex: string | undefined, value: Partial<BasicColumn>) {
if (!dataIndex || !value) {
return;
}
cacheColumns.forEach((item) => {
if (item.key === dataIndex) {
Object.assign(item, value);
return;
}
});
}
return {
getColumnsRef,
getCacheColumns,
setCacheColumnsField,
setColumns,
getColumns,
getPageColumns
};
}

View File

@@ -1,11 +0,0 @@
import Pagination from 'naive-ui/lib/pagination';
import { VNodeChild } from 'vue';
export interface PaginationProps {
page?: number;
pageCount?: number,
pageSize?: number,
pageSizes?: number[],
showSizePicker?: boolean,
showQuickJumper?: boolean,
}

View File

@@ -1,22 +0,0 @@
import type {
TableBaseColumn,
} from 'naive-ui/lib/data-table/src/interface';
export interface BasicColumn extends TableBaseColumn {
}
export interface TableActionType {
reload: (opt) => Promise<void>;
emit?: any;
getColumns: (opt) => BasicColumn[];
setColumns: (columns: BasicColumn[] | string[]) => void;
}
export interface BasicTableProps<T = any> {
title?: string,
dataSource: Function,
columns: any[],
pagination: object,
showPagination: boolean
}

View File

@@ -0,0 +1,4 @@
export { default as BasicTable } from './src/Table.vue';
export { default as TableAction } from './src/components/TableAction.vue';
export * from './src/types/table';
export * from './src/types/tableAction';

View File

@@ -0,0 +1,342 @@
<template>
<div class="table-toolbar">
<!--顶部左侧区域-->
<div class="flex items-center table-toolbar-left">
<template v-if="title">
<div class="table-toolbar-left-title">
{{ title }}
<n-tooltip trigger="hover" v-if="titleTooltip">
<template #trigger>
<n-icon size="18" class="ml-1 cursor-pointer text-gray-400">
<QuestionCircleOutlined />
</n-icon>
</template>
{{ titleTooltip }}
</n-tooltip>
</div>
</template>
<slot name="tableTitle"></slot>
</div>
<div class="flex items-center table-toolbar-right">
<!--顶部右侧区域-->
<slot name="toolbar"></slot>
<!--刷新-->
<n-tooltip trigger="hover">
<template #trigger>
<div class="table-toolbar-right-icon" @click="reload">
<n-icon size="18">
<ReloadOutlined />
</n-icon>
</div>
</template>
<span>刷新</span>
</n-tooltip>
<!--密度-->
<n-tooltip trigger="hover">
<template #trigger>
<div class="table-toolbar-right-icon">
<n-dropdown
@select="densitySelect"
trigger="click"
:options="densityOptions"
v-model:value="tableSize"
>
<n-icon size="18">
<ColumnHeightOutlined />
</n-icon>
</n-dropdown>
</div>
</template>
<span>密度</span>
</n-tooltip>
<!--表格设置单独抽离成组件-->
<ColumnSetting />
</div>
</div>
<div class="s-table">
<n-data-table
ref="tableElRef"
v-bind="getBindValues"
:pagination="pagination"
@update:page="updatePage"
@update:page-size="updatePageSize"
>
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
<slot :name="item" v-bind="data"></slot>
</template>
</n-data-table>
</div>
</template>
<script lang="ts">
import { NDataTable } from 'naive-ui';
import {
ref,
defineComponent,
reactive,
unref,
toRaw,
computed,
toRefs,
onMounted,
nextTick,
} from 'vue';
import { ReloadOutlined, ColumnHeightOutlined, QuestionCircleOutlined } from '@vicons/antd';
import { createTableContext } from './hooks/useTableContext';
import ColumnSetting from './components/settings/ColumnSetting.vue';
import { useLoading } from './hooks/useLoading';
import { useColumns } from './hooks/useColumns';
import { useDataSource } from './hooks/useDataSource';
import { usePagination } from './hooks/usePagination';
import { basicProps } from './props';
import { BasicTableProps } from './types/table';
import { getViewportOffset } from '@/utils/domUtils';
import { useWindowSizeFn } from '@/hooks/event/useWindowSizeFn';
import { isBoolean } from '@/utils/is';
const densityOptions = [
{
type: 'menu',
label: '紧凑',
key: 'small',
},
{
type: 'menu',
label: '默认',
key: 'medium',
},
{
type: 'menu',
label: '宽松',
key: 'large',
},
];
export default defineComponent({
components: {
ReloadOutlined,
ColumnHeightOutlined,
ColumnSetting,
QuestionCircleOutlined,
},
props: {
...NDataTable.props, // 这里继承原 UI 组件的 props
...basicProps,
},
emits: [
'fetch-success',
'fetch-error',
'update:checked-row-keys',
'edit-end',
'edit-cancel',
'edit-row-end',
'edit-change',
],
setup(props, { emit }) {
const deviceHeight = ref(150);
const tableElRef = ref<ComponentRef>(null);
const wrapRef = ref<Nullable<HTMLDivElement>>(null);
let paginationEl: HTMLElement | null;
const tableData = ref<Recordable[]>([]);
const innerPropsRef = ref<Partial<BasicTableProps>>();
const getProps = computed(() => {
return { ...props, ...unref(innerPropsRef) } as BasicTableProps;
});
const { getLoading, setLoading } = useLoading(getProps);
const { getPaginationInfo, setPagination } = usePagination(getProps);
const { getDataSourceRef, getRowKey, reload } = useDataSource(
getProps,
{
getPaginationInfo,
setPagination,
tableData,
setLoading,
},
emit
);
const { getPageColumns, setColumns, getColumns, getCacheColumns, setCacheColumnsField } =
useColumns(getProps);
const state = reactive({
tableSize: 'medium',
isColumnSetting: false,
});
//页码切换
function updatePage(page) {
setPagination({ page: page });
reload();
}
//分页数量切换
function updatePageSize(size) {
setPagination({ page: 1, pageSize: size });
reload();
}
//密度切换
function densitySelect(e) {
state.tableSize = e;
}
//选中行
function updateCheckedRowKeys(rowKeys) {
emit('update:checked-row-keys', rowKeys);
}
//获取表格大小
const getTableSize = computed(() => state.tableSize);
//组装表格信息
const getBindValues = computed(() => {
const tableData = unref(getDataSourceRef);
const maxHeight = tableData.length ? `${unref(deviceHeight)}px` : 'auto';
return {
...unref(getProps),
loading: unref(getLoading),
columns: toRaw(unref(getPageColumns)),
rowKey: unref(getRowKey),
data: tableData,
size: unref(getTableSize),
remote: true,
'max-height': maxHeight,
};
});
//获取分页信息
const pagination = computed(() => toRaw(unref(getPaginationInfo)));
function setProps(props: Partial<BasicTableProps>) {
innerPropsRef.value = { ...unref(innerPropsRef), ...props };
}
const tableAction = {
reload,
setColumns,
setLoading,
setProps,
getColumns,
getPageColumns,
getCacheColumns,
setCacheColumnsField,
emit,
getSize: () => {
return unref(getBindValues).size;
},
};
const getCanResize = computed(() => {
const { canResize } = unref(getProps);
return canResize;
});
async function computeTableHeight() {
const table = unref(tableElRef);
if (!table) return;
if (!unref(getCanResize)) return;
const tableEl: any = table?.$el;
const headEl = tableEl.querySelector('.n-data-table-thead ');
const { bottomIncludeBody } = getViewportOffset(headEl);
const headerH = 64;
let paginationH = 2;
let marginH = 24;
if (!isBoolean(pagination)) {
paginationEl = tableEl.querySelector('.n-data-table__pagination') as HTMLElement;
if (paginationEl) {
const offsetHeight = paginationEl.offsetHeight;
paginationH += offsetHeight || 0;
} else {
paginationH += 28;
}
}
let height =
bottomIncludeBody - (headerH + paginationH + marginH + (props.resizeHeightOffset || 0));
const maxHeight = props.maxHeight;
height = maxHeight && maxHeight < height ? maxHeight : height;
deviceHeight.value = height;
}
useWindowSizeFn(computeTableHeight, 280);
onMounted(() => {
nextTick(() => {
computeTableHeight();
});
});
createTableContext({ ...tableAction, wrapRef, getBindValues });
return {
...toRefs(state),
tableElRef,
getBindValues,
densityOptions,
reload,
densitySelect,
updatePage,
updatePageSize,
updateCheckedRowKeys,
pagination,
tableAction,
};
},
});
</script>
<style lang="less" scoped>
.table-toolbar {
display: flex;
justify-content: space-between;
padding: 0 0 16px 0;
&-left {
display: flex;
align-items: center;
justify-content: flex-start;
flex: 1;
&-title {
display: flex;
align-items: center;
justify-content: flex-start;
font-size: 16px;
font-weight: 600;
}
}
&-right {
display: flex;
justify-content: flex-end;
flex: 1;
&-icon {
margin-left: 12px;
font-size: 16px;
cursor: pointer;
color: var(--text-color);
:hover {
color: #1890ff;
}
}
}
}
.table-toolbar-inner-popover-title {
padding: 2px 0;
}
</style>

View File

@@ -0,0 +1,41 @@
import type { Component } from 'vue';
import {
NInput,
NSelect,
NCheckbox,
NInputNumber,
NSwitch,
NDatePicker,
NTimePicker,
} from 'naive-ui';
import type { ComponentType } from './types/componentType';
export enum EventEnum {
NInput = 'on-input',
NInputNumber = 'on-input',
NSelect = 'on-update:value',
NSwitch = 'on-update:value',
NCheckbox = 'on-update:value',
NDatePicker = 'on-update:value',
NTimePicker = 'on-update:value',
}
const componentMap = new Map<ComponentType, Component>();
componentMap.set('NInput', NInput);
componentMap.set('NInputNumber', NInputNumber);
componentMap.set('NSelect', NSelect);
componentMap.set('NSwitch', NSwitch);
componentMap.set('NCheckbox', NCheckbox);
componentMap.set('NDatePicker', NDatePicker);
componentMap.set('NTimePicker', NTimePicker);
export function add(compName: ComponentType, component: Component) {
componentMap.set(compName, component);
}
export function del(compName: ComponentType) {
componentMap.delete(compName);
}
export { componentMap };

View File

@@ -0,0 +1,136 @@
<template>
<div class="tableAction">
<div class="flex items-center justify-center">
<template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
<n-button v-bind="action" class="mx-2">{{ action.label }}</n-button>
</template>
<n-dropdown
v-if="dropDownActions && getDropdownList.length"
trigger="hover"
:options="getDropdownList"
@select="select"
>
<slot name="more"></slot>
<n-button v-bind="getMoreProps" class="mx-2" v-if="!$slots.more" icon-placement="right">
<div class="flex items-center">
<span>更多</span>
<n-icon size="14" class="ml-1">
<DownOutlined />
</n-icon>
</div>
<!-- <template #icon>-->
<!-- -->
<!-- </template>-->
</n-button>
</n-dropdown>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, PropType, computed, toRaw } from 'vue';
import { ActionItem } from '@/components/Table';
import { usePermission } from '@/hooks/web/usePermission';
import { isBoolean, isFunction } from '@/utils/is';
import { DownOutlined } from '@vicons/antd';
export default defineComponent({
name: 'TableAction',
components: { DownOutlined },
props: {
actions: {
type: Array as PropType<ActionItem[]>,
default: null,
required: true,
},
dropDownActions: {
type: Array as PropType<ActionItem[]>,
default: null,
},
style: {
type: String as PropType<String>,
default: 'button',
},
select: {
type: Function as PropType<Function>,
default: () => {},
},
},
setup(props) {
const { hasPermission } = usePermission();
const actionType =
props.style === 'button' ? 'default' : props.style === 'text' ? 'primary' : 'default';
const actionText =
props.style === 'button' ? undefined : props.style === 'text' ? true : undefined;
const getMoreProps = computed(() => {
return {
text: actionText,
type: actionType,
size: 'small',
};
});
const getDropdownList = computed(() => {
return (toRaw(props.dropDownActions) || [])
.filter((action) => {
return hasPermission(action.auth) && isIfShow(action);
})
.map((action) => {
const { popConfirm } = action;
return {
size: 'small',
text: actionText,
type: actionType,
...action,
...popConfirm,
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
};
});
});
function isIfShow(action: ActionItem): boolean {
const ifShow = action.ifShow;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow(action);
}
return isIfShow;
}
const getActions = computed(() => {
return (toRaw(props.actions) || [])
.filter((action) => {
return hasPermission(action.auth) && isIfShow(action);
})
.map((action) => {
const { popConfirm } = action;
//需要展示什么风格,自己修改一下参数
return {
size: 'small',
text: actionText,
type: actionType,
...action,
...(popConfirm || {}),
onConfirm: popConfirm?.confirm,
onCancel: popConfirm?.cancel,
enable: !!popConfirm,
};
});
});
return {
getActions,
getDropdownList,
getMoreProps,
};
},
});
</script>

View File

@@ -0,0 +1,47 @@
import type { FunctionalComponent, defineComponent } from 'vue';
import type { ComponentType } from '../../types/componentType';
import { componentMap } from '@/components/Table/src/componentMap';
import { h } from 'vue';
import { NPopover } from 'naive-ui';
export interface ComponentProps {
component: ComponentType;
rule: boolean;
popoverVisible: boolean;
ruleMessage: string;
}
export const CellComponent: FunctionalComponent = (
{ component = 'NInput', rule = true, ruleMessage, popoverVisible }: ComponentProps,
{ attrs }
) => {
const Comp = componentMap.get(component) as typeof defineComponent;
const DefaultComp = h(Comp, attrs);
if (!rule) {
return DefaultComp;
}
return h(
NPopover,
{ 'display-directive': 'show', show: !!popoverVisible, manual: 'manual' },
{
trigger: () => DefaultComp,
default: () =>
h(
'span',
{
style: {
color: 'red',
width: '90px',
display: 'inline-block',
},
},
{
default: () => ruleMessage,
}
),
}
);
};

View File

@@ -0,0 +1,402 @@
<template>
<div class="editable-cell">
<div v-show="!isEdit" class="editable-cell-content" @click="handleEdit">
{{ getValues }}
<n-icon class="edit-icon" v-if="!column.editRow">
<FormOutlined />
</n-icon>
</div>
<div class="flex editable-cell-content" v-show="isEdit" v-click-outside="onClickOutside">
<CellComponent
v-bind="getComponentProps"
:component="getComponent"
:style="getWrapperStyle"
:popoverVisible="getRuleVisible"
:ruleMessage="ruleMessage"
:rule="getRule"
:class="getWrapperClass"
ref="elRef"
@options-change="handleOptionsChange"
@pressEnter="handleEnter"
/>
<div class="editable-cell-action" v-if="!getRowEditable">
<n-icon class="cursor-pointer mx-2">
<CheckOutlined @click="handleSubmit" />
</n-icon>
<n-icon class="cursor-pointer mx-2">
<CloseOutlined @click="handleCancel" />
</n-icon>
</div>
</div>
</div>
</template>
<script lang="ts">
import type { CSSProperties, PropType } from 'vue';
import type { BasicColumn } from '../../types/table';
import type { EditRecordRow } from './index';
import { defineComponent, ref, unref, nextTick, computed, watchEffect, toRaw } from 'vue';
import { FormOutlined, CloseOutlined, CheckOutlined } from '@vicons/antd';
import { CellComponent } from './CellComponent';
import { useTableContext } from '../../hooks/useTableContext';
import clickOutside from '@/directives/clickOutside';
import { propTypes } from '@/utils/propTypes';
import { isString, isBoolean, isFunction, isNumber, isArray } from '@/utils/is';
import { createPlaceholderMessage } from './helper';
import { set, omit } from 'lodash-es';
import { EventEnum } from '@/components/Table/src/componentMap';
import dayjs from 'dayjs';
export default defineComponent({
name: 'EditableCell',
components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent },
directives: {
clickOutside,
},
props: {
value: {
type: [String, Number, Boolean, Object] as PropType<string | number | boolean | Recordable>,
default: '',
},
record: {
type: Object as PropType<EditRecordRow>,
},
column: {
type: Object as PropType<BasicColumn>,
default: () => ({}),
},
index: propTypes.number,
},
setup(props) {
const table = useTableContext();
const isEdit = ref(false);
const elRef = ref();
const ruleVisible = ref(false);
const ruleMessage = ref('');
const optionsRef = ref<LabelValueOptions>([]);
const currentValueRef = ref<any>(props.value);
const defaultValueRef = ref<any>(props.value);
// const { prefixCls } = useDesign('editable-cell');
const getComponent = computed(() => props.column?.editComponent || 'NInput');
const getRule = computed(() => props.column?.editRule);
const getRuleVisible = computed(() => {
return unref(ruleMessage) && unref(ruleVisible);
});
const getIsCheckComp = computed(() => {
const component = unref(getComponent);
return ['NCheckbox', 'NSwitch'].includes(component);
});
const getComponentProps = computed(() => {
const compProps = props.column?.editComponentProps ?? {};
const editComponent = props.column?.editComponent ?? null;
const component = unref(getComponent);
const apiSelectProps: Recordable = {};
const isCheckValue = unref(getIsCheckComp);
const valueField = isCheckValue ? 'checked' : 'value';
const val = unref(currentValueRef);
let value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
if (component === 'NDatePicker') {
value = dayjs(value).valueOf();
}
const onEvent: any = editComponent ? EventEnum[editComponent] : undefined;
return {
placeholder: createPlaceholderMessage(unref(getComponent)),
...apiSelectProps,
...omit(compProps, 'onChange'),
[onEvent]: handleChange,
[valueField]: value,
};
});
const getValues = computed(() => {
const { editComponentProps, editValueMap } = props.column;
const value = unref(currentValueRef);
if (editValueMap && isFunction(editValueMap)) {
return editValueMap(value);
}
const component = unref(getComponent);
if (!component.includes('NSelect')) {
return value;
}
const options: LabelValueOptions = editComponentProps?.options ?? (unref(optionsRef) || []);
const option = options.find((item) => `${item.value}` === `${value}`);
return option?.label ?? value;
});
const getWrapperStyle = computed((): CSSProperties => {
// if (unref(getIsCheckComp) || unref(getRowEditable)) {
// return {};
// }
return {
width: 'calc(100% - 48px)',
};
});
const getWrapperClass = computed(() => {
const { align = 'center' } = props.column;
return `edit-cell-align-${align}`;
});
const getRowEditable = computed(() => {
const { editable } = props.record || {};
return !!editable;
});
watchEffect(() => {
defaultValueRef.value = props.value;
});
watchEffect(() => {
const { editable } = props.column;
if (isBoolean(editable) || isBoolean(unref(getRowEditable))) {
isEdit.value = !!editable || unref(getRowEditable);
}
});
function handleEdit() {
if (unref(getRowEditable) || unref(props.column?.editRow)) return;
ruleMessage.value = '';
isEdit.value = true;
nextTick(() => {
const el = unref(elRef);
el?.focus?.();
});
}
async function handleChange(e: any) {
const component = unref(getComponent);
if (!e) {
currentValueRef.value = e;
} else if (e?.target && Reflect.has(e.target, 'value')) {
currentValueRef.value = (e as ChangeEvent).target.value;
} else if (component === 'NCheckbox') {
currentValueRef.value = (e as ChangeEvent).target.checked;
} else if (isString(e) || isBoolean(e) || isNumber(e)) {
currentValueRef.value = e;
}
//TODO 这里组件参数格式和dayjs格式不一致
if (component === 'NDatePicker') {
let format = (props.column.editComponentProps?.format)
.replace(/yyyy/g, 'YYYY')
.replace(/dd/g, 'DD');
currentValueRef.value = dayjs(currentValueRef.value).format(format);
}
const onChange = props.column?.editComponentProps?.onChange;
if (onChange && isFunction(onChange)) onChange(...arguments);
table.emit?.('edit-change', {
column: props.column,
value: unref(currentValueRef),
record: toRaw(props.record),
});
await handleSubmiRule();
}
async function handleSubmiRule() {
const { column, record } = props;
const { editRule } = column;
const currentValue = unref(currentValueRef);
if (editRule) {
if (isBoolean(editRule) && !currentValue && !isNumber(currentValue)) {
ruleVisible.value = true;
const component = unref(getComponent);
ruleMessage.value = createPlaceholderMessage(component);
return false;
}
if (isFunction(editRule)) {
const res = await editRule(currentValue, record as Recordable);
if (!!res) {
ruleMessage.value = res;
ruleVisible.value = true;
return false;
} else {
ruleMessage.value = '';
return true;
}
}
}
ruleMessage.value = '';
return true;
}
async function handleSubmit(needEmit = true, valid = true) {
if (valid) {
const isPass = await handleSubmiRule();
if (!isPass) return false;
}
const { column, index, record } = props;
if (!record) return false;
const { key } = column;
const value = unref(currentValueRef);
if (!key) return;
const dataKey = key as string;
set(record, dataKey, value);
//const record = await table.updateTableData(index, dataKey, value);
needEmit && table.emit?.('edit-end', { record, index, key, value });
isEdit.value = false;
}
async function handleEnter() {
if (props.column?.editRow) {
return;
}
await handleSubmit();
}
function handleCancel() {
isEdit.value = false;
currentValueRef.value = defaultValueRef.value;
const { column, index, record } = props;
const { key } = column;
ruleVisible.value = true;
ruleMessage.value = '';
table.emit?.('edit-cancel', {
record,
index,
key: key,
value: unref(currentValueRef),
});
}
function onClickOutside() {
if (props.column?.editable || unref(getRowEditable)) {
return;
}
const component = unref(getComponent);
if (component.includes('NInput')) {
handleCancel();
}
}
// only ApiSelect
function handleOptionsChange(options: LabelValueOptions) {
optionsRef.value = options;
}
function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) {
if (props.record) {
/* eslint-disable */
isArray(props.record[cbs])
? props.record[cbs]?.push(handle)
: (props.record[cbs] = [handle]);
}
}
if (props.record) {
initCbs('submitCbs', handleSubmit);
initCbs('validCbs', handleSubmiRule);
initCbs('cancelCbs', handleCancel);
if (props.column.key) {
if (!props.record.editValueRefs) props.record.editValueRefs = {};
props.record.editValueRefs[props.column.key] = currentValueRef;
}
/* eslint-disable */
props.record.onCancelEdit = () => {
isArray(props.record?.cancelCbs) && props.record?.cancelCbs.forEach((fn) => fn());
};
/* eslint-disable */
props.record.onSubmitEdit = async() => {
if (isArray(props.record?.submitCbs)) {
const validFns = (props.record?.validCbs || []).map((fn) => fn());
const res = await Promise.all(validFns);
const pass = res.every((item) => !!item);
if (!pass) return;
const submitFns = props.record?.submitCbs || [];
submitFns.forEach((fn) => fn(false, false));
table.emit?.('edit-row-end');
return true;
}
};
}
return {
isEdit,
handleEdit,
currentValueRef,
handleSubmit,
handleChange,
handleCancel,
elRef,
getComponent,
getRule,
onClickOutside,
ruleMessage,
getRuleVisible,
getComponentProps,
handleOptionsChange,
getWrapperStyle,
getWrapperClass,
getRowEditable,
getValues,
handleEnter,
// getSize,
};
},
});
</script>
<style lang="less">
.editable-cell {
&-content {
position: relative;
overflow-wrap: break-word;
word-break: break-word;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.edit-icon {
font-size: 14px;
//position: absolute;
//top: 4px;
//right: 0;
display: none;
width: 20px;
cursor: pointer;
}
&:hover {
.edit-icon {
display: inline-block;
}
}
}
&-action {
display: flex;
align-items: center;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,15 @@
import { ComponentType } from '../../types/componentType';
/**
* @description: 生成placeholder
*/
export function createPlaceholderMessage(component: ComponentType) {
if (component === 'NInput') return '请输入';
if (
['NPicker', 'NSelect', 'NCheckbox', 'NRadio', 'NSwitch', 'NDatePicker', 'NTimePicker'].includes(
component
)
)
return '请选择';
return '';
}

View File

@@ -0,0 +1,49 @@
import type { BasicColumn } from '@/components/Table/src/types/table';
import { h, Ref } from 'vue';
import EditableCell from './EditableCell.vue';
export function renderEditCell(column: BasicColumn) {
return (record, index) => {
const _key = column.key;
const value = record[_key];
record.onEdit = async (edit: boolean, submit = false) => {
if (!submit) {
record.editable = edit;
}
if (!edit && submit) {
const res = await record.onSubmitEdit?.();
if (res) {
record.editable = false;
return true;
}
return false;
}
// cancel
if (!edit && !submit) {
record.onCancelEdit?.();
}
return true;
};
return h(EditableCell, {
value,
record,
column,
index,
});
};
}
export type EditRecordRow<T = Recordable> = Partial<
{
onEdit: (editable: boolean, submit?: boolean) => Promise<boolean>;
editable: boolean;
onCancel: Fn;
onSubmit: Fn;
submitCbs: Fn[];
cancelCbs: Fn[];
validCbs: Fn[];
editValueRefs: Recordable<Ref>;
} & T
>;

View File

@@ -0,0 +1,304 @@
<template>
<n-tooltip trigger="hover">
<template #trigger>
<div class="cursor-pointer table-toolbar-right-icon">
<n-popover trigger="click" :width="230" class="toolbar-popover" placement="bottom-end">
<template #trigger>
<n-icon size="18">
<SettingOutlined />
</n-icon>
</template>
<template #header>
<div class="table-toolbar-inner-popover-title">
<n-space>
<n-checkbox v-model:checked="checkAll" @update:checked="onCheckAll"
>列展示</n-checkbox
>
<n-checkbox v-model:checked="selection" @update:checked="onSelection"
>勾选列</n-checkbox
>
<n-button text type="info" size="small" class="mt-1" @click="resetColumns"
>重置</n-button
>
</n-space>
</div>
</template>
<div class="table-toolbar-inner">
<n-checkbox-group v-model:value="checkList" @update:value="onChange">
<Draggable v-model="columnsList" animation="300" item-key="key" @end="draggableEnd">
<template #item="{ element }">
<div
class="table-toolbar-inner-checkbox"
:class="{ 'table-toolbar-inner-checkbox-dark': getDarkTheme === true }"
>
<span class="drag-icon">
<n-icon size="18">
<DragOutlined />
</n-icon>
</span>
<n-checkbox :value="element.key" :label="element.title" />
<div class="fixed-item">
<n-tooltip trigger="hover" placement="bottom">
<template #trigger>
<n-icon
size="18"
:color="element.fixed === 'left' ? '#2080f0' : undefined"
class="cursor-pointer"
@click="fixedColumn(element, 'left')"
>
<VerticalRightOutlined />
</n-icon>
</template>
<span>固定到左侧</span>
</n-tooltip>
<n-divider vertical />
<n-tooltip trigger="hover" placement="bottom">
<template #trigger>
<n-icon
size="18"
:color="element.fixed === 'right' ? '#2080f0' : undefined"
class="cursor-pointer"
@click="fixedColumn(element, 'right')"
>
<VerticalLeftOutlined />
</n-icon>
</template>
<span>固定到右侧</span>
</n-tooltip>
</div>
</div>
</template>
</Draggable>
</n-checkbox-group>
</div>
</n-popover>
</div>
</template>
<span>列设置</span>
</n-tooltip>
</template>
<script lang="ts">
import { ref, defineComponent, reactive, unref, toRaw, computed, toRefs, watchEffect } from 'vue';
import { useTableContext } from '../../hooks/useTableContext';
import {
SettingOutlined,
DragOutlined,
VerticalRightOutlined,
VerticalLeftOutlined,
} from '@vicons/antd';
import Draggable from 'vuedraggable/src/vuedraggable';
import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
interface Options {
title: string;
key: string;
fixed?: boolean | 'left' | 'right';
}
export default defineComponent({
name: 'ColumnSetting',
components: {
SettingOutlined,
DragOutlined,
Draggable,
VerticalRightOutlined,
VerticalLeftOutlined,
},
setup() {
const { getDarkTheme } = useDesignSetting();
const table = useTableContext();
const columnsList = ref<Options[]>([]);
const cacheColumnsList = ref<Options[]>([]);
const state = reactive({
selection: false,
checkAll: true,
checkList: [],
defaultCheckList: [],
});
const getSelection = computed(() => {
return state.selection;
});
watchEffect(() => {
const columns = table.getColumns();
if (columns.length) {
init();
}
});
//初始化
function init() {
const columns: any[] = getColumns();
const checkList: any = columns.map((item) => item.key);
state.checkList = checkList;
state.defaultCheckList = checkList;
columnsList.value = columns;
cacheColumnsList.value = columns;
}
//切换
function onChange(checkList) {
if (state.selection) {
checkList.unshift('selection');
}
setColumns(checkList);
}
//设置
function setColumns(columns) {
table.setColumns(columns);
}
//获取
function getColumns() {
let newRet = [];
table.getColumns().forEach((item) => {
newRet.push({ ...item });
});
return newRet.filter((item) => item.key != 'action' && item.title != '操作');
}
//重置
function resetColumns() {
state.checkList = [...state.defaultCheckList];
state.checkAll = true;
let cacheColumnsKeys: any[] = table.getCacheColumns();
let newColumns = cacheColumnsKeys.map((item) => {
return {
...item,
fixed: undefined,
};
});
setColumns(newColumns);
columnsList.value = newColumns;
}
//全选
function onCheckAll(e) {
let checkList = table.getCacheColumns(true);
if (e) {
setColumns(checkList);
state.checkList = checkList;
} else {
setColumns([]);
state.checkList = [];
}
}
//拖拽排序
function draggableEnd() {
const newColumns = toRaw(unref(columnsList));
columnsList.value = newColumns;
setColumns(newColumns);
}
//勾选列
function onSelection(e) {
let checkList = table.getCacheColumns();
if (e) {
checkList.unshift({ type: 'selection', key: 'selection' });
setColumns(checkList);
} else {
checkList.splice(0, 1);
setColumns(checkList);
}
}
//固定
function fixedColumn(item, fixed) {
if (!state.checkList.includes(item.key)) return;
let columns = getColumns();
const isFixed = item.fixed === fixed ? undefined : fixed;
let index = columns.findIndex((res) => res.key === item.key);
if (index !== -1) {
columns[index].fixed = isFixed;
}
table.setCacheColumnsField(item.key, { fixed: isFixed });
columnsList.value[index].fixed = isFixed;
setColumns(columns);
}
return {
...toRefs(state),
columnsList,
getDarkTheme,
onChange,
onCheckAll,
onSelection,
resetColumns,
fixedColumn,
draggableEnd,
getSelection,
};
},
});
</script>
<style lang="less">
.table-toolbar {
&-inner-popover-title {
padding: 3px 0;
}
&-right {
&-icon {
margin-left: 12px;
font-size: 16px;
color: var(--text-color);
cursor: pointer;
:hover {
color: #1890ff;
}
}
}
}
.table-toolbar-inner {
&-checkbox {
display: flex;
align-items: center;
padding: 10px 14px;
&:hover {
background: #e6f7ff;
}
.drag-icon {
display: inline-flex;
margin-right: 8px;
cursor: move;
}
.fixed-item {
display: flex;
align-items: center;
justify-content: flex-end;
margin-left: auto;
}
.ant-checkbox-wrapper {
flex: 1;
&:hover {
color: #1890ff !important;
}
}
}
&-checkbox-dark {
&:hover {
background: hsla(0, 0%, 100%, 0.08);
}
}
}
.toolbar-popover {
.n-popover__content {
padding: 0;
}
}
</style>

View File

@@ -1,6 +1,6 @@
import componentSetting from '@/settings/componentSetting' import componentSetting from '@/settings/componentSetting';
const { table } = componentSetting const { table } = componentSetting;
const { apiSetting, defaultPageSize, pageSizes } = table; const { apiSetting, defaultPageSize, pageSizes } = table;
@@ -9,7 +9,3 @@ export const DEFAULTPAGESIZE = defaultPageSize;
export const APISETTING = apiSetting; export const APISETTING = apiSetting;
export const PAGESIZES = pageSizes; export const PAGESIZES = pageSizes;

View File

@@ -0,0 +1,163 @@
import { ref, Ref, ComputedRef, unref, computed, watch, toRaw, h } from 'vue';
import type { BasicColumn, BasicTableProps } from '../types/table';
import { isEqual, cloneDeep } from 'lodash-es';
import { isArray, isString, isBoolean, isFunction } from '@/utils/is';
import { usePermission } from '@/hooks/web/usePermission';
import { ActionItem } from '@/components/Table';
import { renderEditCell } from '../components/editable';
import { NTooltip, NIcon } from 'naive-ui';
import { FormOutlined } from '@vicons/antd';
export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>;
let cacheColumns = unref(propsRef).columns;
const getColumnsRef = computed(() => {
const columns = cloneDeep(unref(columnsRef));
handleActionColumn(propsRef, columns);
if (!columns) return [];
return columns;
});
const { hasPermission } = usePermission();
function isIfShow(action: ActionItem): boolean {
const ifShow = action.ifShow;
let isIfShow = true;
if (isBoolean(ifShow)) {
isIfShow = ifShow;
}
if (isFunction(ifShow)) {
isIfShow = ifShow(action);
}
return isIfShow;
}
const renderTooltip = (trigger, content) => {
return h(NTooltip, null, {
trigger: () => trigger,
default: () => content,
});
};
const getPageColumns = computed(() => {
const pageColumns = unref(getColumnsRef);
const columns = cloneDeep(pageColumns);
return columns
.filter((column) => {
// @ts-ignore
return hasPermission(column.auth) && isIfShow(column);
})
.map((column) => {
const { edit, editRow } = column;
if (edit) {
column.render = renderEditCell(column);
if (edit) {
const title: any = column.title;
column.title = () => {
return renderTooltip(
h('span', {}, [
h('span', { style: { 'margin-right': '5px' } }, title),
h(
NIcon,
{
size: 14,
},
{
default: () => h(FormOutlined),
}
),
]),
'该列可编辑'
);
};
}
}
return column;
});
});
watch(
() => unref(propsRef).columns,
(columns) => {
columnsRef.value = columns;
cacheColumns = columns;
}
);
function handleActionColumn(propsRef: ComputedRef<BasicTableProps>, columns: BasicColumn[]) {
const { actionColumn } = unref(propsRef);
if (!actionColumn) return;
// @ts-ignore
columns.push({
...actionColumn,
});
}
//设置
function setColumns(columnList: string[]) {
const columns: any[] = cloneDeep(columnList);
if (!isArray(columns)) return;
if (!columns.length) {
columnsRef.value = [];
return;
}
const cacheKeys = cacheColumns.map((item) => item.key);
//针对拖拽排序
if (!isString(columns[0])) {
columnsRef.value = columns;
} else {
const newColumns: any[] = [];
cacheColumns.forEach((item) => {
if (columnList.includes(item.key)) {
newColumns.push({ ...item });
}
});
if (!isEqual(cacheKeys, columns)) {
newColumns.sort((prev, next) => {
return cacheKeys.indexOf(prev.key) - cacheKeys.indexOf(next.key);
});
}
columnsRef.value = newColumns;
}
}
//获取
function getColumns() {
const columns = toRaw(unref(getColumnsRef));
return columns.map((item) => {
return { ...item, title: item.title, key: item.key, fixed: item.fixed || undefined };
});
}
//获取原始
function getCacheColumns(isKey?: boolean): any[] {
return isKey ? cacheColumns.map((item) => item.key) : cacheColumns;
}
//更新原始数据单个字段
function setCacheColumnsField(dataIndex: string | undefined, value: Partial<BasicColumn>) {
if (!dataIndex || !value) {
return;
}
cacheColumns.forEach((item) => {
if (item.key === dataIndex) {
Object.assign(item, value);
return;
}
});
}
return {
getColumnsRef,
getCacheColumns,
setCacheColumnsField,
setColumns,
getColumns,
getPageColumns,
};
}

View File

@@ -6,12 +6,7 @@ import { APISETTING } from '../const';
export function useDataSource( export function useDataSource(
propsRef: ComputedRef<BasicTableProps>, propsRef: ComputedRef<BasicTableProps>,
{ { getPaginationInfo, setPagination, setLoading, tableData },
getPaginationInfo,
setPagination,
setLoading,
tableData
},
emit emit
) { ) {
const dataSourceRef = ref([]); const dataSourceRef = ref([]);
@@ -33,8 +28,10 @@ export function useDataSource(
const getRowKey = computed(() => { const getRowKey = computed(() => {
const { rowKey }: any = unref(propsRef); const { rowKey }: any = unref(propsRef);
return rowKey ? rowKey : () => { return rowKey
return 'key' ? rowKey
: () => {
return 'key';
}; };
}); });
@@ -52,10 +49,10 @@ export function useDataSource(
const { request, pagination }: any = unref(propsRef); const { request, pagination }: any = unref(propsRef);
//组装分页信息 //组装分页信息
const pageField = APISETTING.pageField const pageField = APISETTING.pageField;
const sizeField = APISETTING.sizeField const sizeField = APISETTING.sizeField;
const totalField = APISETTING.totalField const totalField = APISETTING.totalField;
const listField = APISETTING.listField const listField = APISETTING.listField;
let pageParams = {}; let pageParams = {};
const { page = 1, pageSize = 10 } = unref(getPaginationInfo) as PaginationProps; const { page = 1, pageSize = 10 } = unref(getPaginationInfo) as PaginationProps;
@@ -67,13 +64,13 @@ export function useDataSource(
pageParams[sizeField] = pageSize; pageParams[sizeField] = pageSize;
} }
let params = { const params = {
...pageParams, ...pageParams,
} };
const res = await request(params); const res = await request(params);
const resultTotal = res[totalField] || 0 const resultTotal = res[totalField] || 0;
const currentPage = res[pageField] const currentPage = res[pageField];
// 如果数据异常,需获取正确的页码再次执行 // 如果数据异常,需获取正确的页码再次执行
if (resultTotal) { if (resultTotal) {
@@ -85,7 +82,7 @@ export function useDataSource(
fetch(opt); fetch(opt);
} }
} }
let resultInfo = res[listField] ? res[listField] : [] const resultInfo = res[listField] ? res[listField] : [];
dataSourceRef.value = resultInfo; dataSourceRef.value = resultInfo;
setPagination({ setPagination({
[pageField]: currentPage, [pageField]: currentPage,
@@ -98,10 +95,10 @@ export function useDataSource(
} }
emit('fetch-success', { emit('fetch-success', {
items: unref(resultInfo), items: unref(resultInfo),
resultTotal resultTotal,
}); });
} catch (error) { } catch (error) {
console.error(error) console.error(error);
emit('fetch-error', error); emit('fetch-error', error);
dataSourceRef.value = []; dataSourceRef.value = [];
// setPagination({ // setPagination({
@@ -115,7 +112,7 @@ export function useDataSource(
onMounted(() => { onMounted(() => {
setTimeout(() => { setTimeout(() => {
fetch(); fetch();
}, 16) }, 16);
}); });
function setTableData(values) { function setTableData(values) {
@@ -136,6 +133,6 @@ export function useDataSource(
getDataSourceRef, getDataSourceRef,
getDataSource, getDataSource,
setTableData, setTableData,
reload reload,
} };
} }

View File

@@ -1,5 +1,6 @@
import type { PropType } from 'vue' import type { PropType } from 'vue';
import { BasicColumn } from './types/table' import { propTypes } from '@/utils/propTypes';
import { BasicColumn } from './types/table';
export const basicProps = { export const basicProps = {
title: { title: {
@@ -16,8 +17,7 @@ export const basicProps = {
}, },
tableData: { tableData: {
type: [Object], type: [Object],
default: () => { default: () => [],
},
}, },
columns: { columns: {
type: [Array] as PropType<BasicColumn[]>, type: [Array] as PropType<BasicColumn[]>,
@@ -27,7 +27,7 @@ export const basicProps = {
request: { request: {
type: Function as PropType<(...arg: any[]) => Promise<any>>, type: Function as PropType<(...arg: any[]) => Promise<any>>,
default: null, default: null,
required: true required: true,
}, },
rowKey: { rowKey: {
type: [String, Function] as PropType<string | ((record) => string)>, type: [String, Function] as PropType<string | ((record) => string)>,
@@ -35,11 +35,17 @@ export const basicProps = {
}, },
pagination: { pagination: {
type: [Object, Boolean], type: [Object, Boolean],
default: () => { default: () => {},
}
}, },
//废弃
showPagination: { showPagination: {
type: [String, Boolean], type: [String, Boolean],
default: 'auto' default: 'auto',
} },
} actionColumn: {
type: Object as PropType<BasicColumn>,
default: null,
},
canResize: propTypes.bool.def(true),
resizeHeightOffset: propTypes.number.def(0),
};

View File

@@ -0,0 +1,8 @@
export type ComponentType =
| 'NInput'
| 'NInputNumber'
| 'NSelect'
| 'NCheckbox'
| 'NSwitch'
| 'NDatePicker'
| 'NTimePicker';

View File

@@ -0,0 +1,8 @@
export interface PaginationProps {
page?: number;
pageCount?: number;
pageSize?: number;
pageSizes?: number[];
showSizePicker?: boolean;
showQuickJumper?: boolean;
}

View File

@@ -0,0 +1,35 @@
import type { TableBaseColumn } from 'naive-ui/lib/data-table/src/interface';
import { ComponentType } from './componentType';
export interface BasicColumn extends TableBaseColumn {
//编辑表格
edit?: boolean;
editRow?: boolean;
editable?: boolean;
editComponent?: ComponentType;
editComponentProps?: Recordable;
editRule?: boolean | ((text: string, record: Recordable) => Promise<string>);
editValueMap?: (value: any) => string;
onEditRow?: () => void;
// 权限编码控制是否显示
auth?: RoleEnum | RoleEnum[] | string | string[];
// 业务控制是否显示
ifShow?: boolean | ((column: BasicColumn) => boolean);
}
export interface TableActionType {
reload: (opt) => Promise<void>;
emit?: any;
getColumns: (opt?) => BasicColumn[];
setColumns: (columns: BasicColumn[] | string[]) => void;
}
export interface BasicTableProps {
title?: string;
dataSource: Function;
columns: any[];
pagination: object;
showPagination: boolean;
actionColumn: any[];
canResize: boolean;
resizeHeightOffset: number;
}

View File

@@ -0,0 +1,26 @@
// @ts-ignore
import { NButton } from 'naive-ui';
import { RoleEnum } from '@/enums/roleEnum';
// @ts-ignore
export interface ActionItem extends NButton.props {
onClick?: Fn;
label?: string;
color?: 'success' | 'error' | 'warning';
icon?: string;
popConfirm?: PopConfirm;
disabled?: boolean;
divider?: boolean;
// 权限编码控制是否显示
auth?: RoleEnum | RoleEnum[] | string | string[];
// 业务控制是否显示
ifShow?: boolean | ((action: ActionItem) => boolean);
}
export interface PopConfirm {
title: string;
okText?: string;
cancelText?: string;
confirm: Fn;
cancel?: Fn;
icon?: string;
}

View File

@@ -0,0 +1 @@
export { default as BasicUpload } from './src/BasicUpload.vue';

View File

@@ -0,0 +1,306 @@
<template>
<div class="w-full">
<div class="upload">
<div class="upload-card">
<!--图片列表-->
<div
class="upload-card-item"
:style="getCSSProperties"
v-for="(item, index) in imgList"
:key="`img_${index}`"
>
<div class="upload-card-item-info">
<div class="img-box">
<img :src="item" />
</div>
<div class="img-box-actions">
<n-icon size="18" class="action-icon mx-2" @click="preview(item)">
<EyeOutlined />
</n-icon>
<n-icon size="18" class="action-icon mx-2" @click="remove(index)">
<DeleteOutlined />
</n-icon>
</div>
</div>
</div>
<!--上传图片-->
<div
class="upload-card-item upload-card-item-select-picture"
:style="getCSSProperties"
v-if="imgList.length < maxNumber"
>
<n-upload
v-bind="$props"
:file-list-style="{ display: 'none' }"
@before-upload="beforeUpload"
@finish="finish"
>
<div class="flex justify-center flex-col">
<n-icon size="18" class="m-auto">
<PlusOutlined />
</n-icon>
<span class="upload-title">上传图片</span>
</div>
</n-upload>
</div>
</div>
</div>
<!--上传图片-->
<n-space>
<n-alert title="提示" type="info" v-if="helpText" class="flex w-full">
{{ helpText }}
</n-alert>
</n-space>
</div>
<!--预览图片-->
<n-modal
v-model:show="showModal"
preset="card"
title="预览"
:bordered="false"
:style="{ width: '520px' }"
>
<img :src="previewUrl" />
</n-modal>
</template>
<script lang="ts">
import { defineComponent, toRefs, reactive, computed } from 'vue';
import { EyeOutlined, DeleteOutlined, PlusOutlined } from '@vicons/antd';
import { NUpload } from 'naive-ui';
import { basicProps } from './props';
import { useMessage, useDialog } from 'naive-ui';
import { ResultEnum } from '@/enums/httpEnum';
import componentSetting from '@/settings/componentSetting';
import { useGlobSetting } from '@/hooks/setting';
import { isString } from '@/utils/is';
const globSetting = useGlobSetting();
export default defineComponent({
name: 'BasicUpload',
components: { EyeOutlined, DeleteOutlined, PlusOutlined },
props: {
...NUpload.props, // 这里继承原 UI 组件的 props
...basicProps,
},
emits: ['uploadChange', 'delete'],
setup(props, { emit }) {
const getCSSProperties = computed(() => {
return {
width: `${props.width}px`,
height: `${props.height}px`,
};
});
const message = useMessage();
const dialog = useDialog();
const state = reactive({
showModal: false,
previewUrl: '',
originalImgList: [],
imgList: [],
});
//赋值默认图片显示
if (props.value.length) {
state.imgList = props.value.map((item) => {
return getImgUrl(item);
});
}
//预览
function preview(url: string) {
state.showModal = true;
state.previewUrl = url;
}
//删除
function remove(index: number) {
dialog.info({
title: '提示',
content: '你确定要删除吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
state.imgList.splice(index, 1);
state.originalImgList.splice(index, 1);
emit('uploadChange', state.originalImgList);
emit('delete', state.originalImgList);
},
onNegativeClick: () => {},
});
}
//组装完整图片地址
function getImgUrl(url: string): string {
const { imgUrl } = globSetting;
return /(^http|https:\/\/)/g.test(url) ? url : `${imgUrl}${url}`;
}
function checkFileType(fileType: string) {
return componentSetting.upload.fileType.includes(fileType);
}
//上传之前
function beforeUpload({ file }) {
const fileInfo = file.file;
const { maxSize, accept } = props;
const acceptRef = (isString(accept) && accept.split(',')) || [];
// 设置最大值,则判断
if (maxSize && fileInfo.size / 1024 / 1024 >= maxSize) {
message.error(`上传文件最大值不能超过${maxSize}M`);
return false;
}
// 设置类型,则判断
const fileType = componentSetting.upload.fileType;
if (acceptRef.length > 0 && !checkFileType(fileInfo.type)) {
message.error(`只能上传文件类型为${fileType.join(',')}`);
return false;
}
return true;
}
//上传结束
function finish({ event: Event }) {
const res = eval('(' + Event.target.response + ')');
const infoField = componentSetting.upload.apiSetting.infoField;
const { code } = res;
const message = res.msg || res.message || '上传失败';
const result = res[infoField];
//成功
if (code === ResultEnum.SUCCESS) {
let imgUrl = getImgUrl(result.photo);
state.imgList.push(imgUrl);
state.originalImgList.push(result.photo);
emit('uploadChange', state.originalImgList);
} else message.error(message);
}
return {
...toRefs(state),
finish,
preview,
remove,
beforeUpload,
getCSSProperties,
};
},
});
</script>
<style lang="less">
.upload {
width: 100%;
overflow: hidden;
&-card {
width: auto;
height: auto;
display: flex;
flex-wrap: wrap;
align-items: center;
&-item {
margin: 0 8px 8px 0;
position: relative;
padding: 8px;
border: 1px solid #d9d9d9;
border-radius: 2px;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
&:hover {
background: 0 0;
.upload-card-item-info::before {
opacity: 1;
}
&-info::before {
opacity: 1;
}
}
&-info {
position: relative;
height: 100%;
padding: 0;
overflow: hidden;
&:hover {
.img-box-actions {
opacity: 1;
}
}
&::before {
position: absolute;
z-index: 1;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: all 0.3s;
content: ' ';
}
.img-box {
position: relative;
//padding: 8px;
//border: 1px solid #d9d9d9;
border-radius: 2px;
}
.img-box-actions {
position: absolute;
top: 50%;
left: 50%;
z-index: 10;
white-space: nowrap;
transform: translate(-50%, -50%);
opacity: 0;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: space-between;
&:hover {
background: 0 0;
}
.action-icon {
color: rgba(255, 255, 255, 0.85);
&:hover {
cursor: pointer;
color: #fff;
}
}
}
}
}
&-item-select-picture {
border: 1px dashed #d9d9d9;
border-radius: 2px;
cursor: pointer;
background: #fafafa;
color: #666;
.upload-title {
color: #666;
}
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
import type { PropType } from 'vue';
import { NUpload } from 'naive-ui';
export const basicProps = {
...NUpload.props,
accept: {
type: String,
default: '.jpg,.png,.jpeg,.svg,.gif',
},
helpText: {
type: String as PropType<string>,
default: '',
},
maxSize: {
type: Number as PropType<number>,
default: 2,
},
maxNumber: {
type: Number as PropType<number>,
default: Infinity,
},
value: {
type: Array as PropType<string[]>,
default: () => [],
},
width: {
type: Number as PropType<number>,
default: 104,
},
height: {
type: Number as PropType<number>,
default: 104, //建议不小于这个尺寸 太小页面可能显示有异常
},
};

View File

@@ -0,0 +1,7 @@
export interface BasicProps {
title?: string;
dataSource: Function;
columns: any[];
pagination: object;
showPagination: boolean;
}

View File

@@ -0,0 +1,86 @@
import { on } from '@/utils/domUtils';
import { isServer } from '@/utils/is';
import type { ComponentPublicInstance, DirectiveBinding, ObjectDirective } from 'vue';
type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void;
type FlushList = Map<
HTMLElement,
{
documentHandler: DocumentHandler;
bindingFn: (...args: unknown[]) => unknown;
}
>;
const nodeList: FlushList = new Map();
let startClick: MouseEvent;
if (!isServer) {
on(document, 'mousedown', (e: MouseEvent) => (startClick = e));
on(document, 'mouseup', (e: MouseEvent) => {
for (const { documentHandler } of nodeList.values()) {
documentHandler(e, startClick);
}
});
}
function createDocumentHandler(el: HTMLElement, binding: DirectiveBinding): DocumentHandler {
let excludes: HTMLElement[] = [];
if (Array.isArray(binding.arg)) {
excludes = binding.arg;
} else {
// due to current implementation on binding type is wrong the type casting is necessary here
excludes.push(binding.arg as unknown as HTMLElement);
}
return function (mouseup, mousedown) {
const popperRef = (
binding.instance as ComponentPublicInstance<{
popperRef: Nullable<HTMLElement>;
}>
).popperRef;
const mouseUpTarget = mouseup.target as Node;
const mouseDownTarget = mousedown.target as Node;
const isBound = !binding || !binding.instance;
const isTargetExists = !mouseUpTarget || !mouseDownTarget;
const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget);
const isSelf = el === mouseUpTarget;
const isTargetExcluded =
(excludes.length && excludes.some((item) => item?.contains(mouseUpTarget))) ||
(excludes.length && excludes.includes(mouseDownTarget as HTMLElement));
const isContainedByPopper =
popperRef && (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget));
if (
isBound ||
isTargetExists ||
isContainedByEl ||
isSelf ||
isTargetExcluded ||
isContainedByPopper
) {
return;
}
binding.value();
};
}
const ClickOutside: ObjectDirective = {
beforeMount(el, binding) {
nodeList.set(el, {
documentHandler: createDocumentHandler(el, binding),
bindingFn: binding.value,
});
},
updated(el, binding) {
nodeList.set(el, {
documentHandler: createDocumentHandler(el, binding),
bindingFn: binding.value,
});
},
unmounted(el) {
nodeList.delete(el);
},
};
export default ClickOutside;

View File

@@ -1,19 +1,19 @@
import { ObjectDirective } from 'vue' import { ObjectDirective } from 'vue';
import { usePermission } from "@/hooks/web/usePermission"; import { usePermission } from '@/hooks/web/usePermission';
export const permission: ObjectDirective = { export const permission: ObjectDirective = {
mounted(el: HTMLButtonElement, binding, vnode) { mounted(el: HTMLButtonElement, binding) {
if (binding.value == undefined) return if (binding.value == undefined) return;
const { action, effect } = binding.value const { action, effect } = binding.value;
const { hasPermission } = usePermission() const { hasPermission } = usePermission();
if (!hasPermission(action)) { if (!hasPermission(action)) {
if (effect == 'disabled') { if (effect == 'disabled') {
el.disabled = true el.disabled = true;
el.style["disabled"] = 'disabled' el.style['disabled'] = 'disabled';
el.classList.add("n-button--disabled") el.classList.add('n-button--disabled');
} else { } else {
el.remove() el.remove();
}
}
} }
} }
},
};

View File

@@ -1,20 +1,20 @@
// token key // token key
export const TOKEN_KEY = 'TOKEN' export const TOKEN_KEY = 'TOKEN';
// user info key // user info key
export const USER_INFO_KEY = 'USER__INFO__' export const USER_INFO_KEY = 'USER__INFO__';
// role info key // role info key
export const ROLES_KEY = 'ROLES__KEY__' export const ROLES_KEY = 'ROLES__KEY__';
// project config key // project config key
export const PROJ_CFG_KEY = 'PROJ__CFG__KEY__' export const PROJ_CFG_KEY = 'PROJ__CFG__KEY__';
// lock info // lock info
export const LOCK_INFO_KEY = 'LOCK__INFO__KEY__' export const LOCK_INFO_KEY = 'LOCK__INFO__KEY__';
// base global local key // base global local key
export const BASE_LOCAL_CACHE_KEY = 'LOCAL__CACHE__KEY__' export const BASE_LOCAL_CACHE_KEY = 'LOCAL__CACHE__KEY__';
// base global session key // base global session key
export const BASE_SESSION_CACHE_KEY = 'SESSION__CACHE__KEY__' export const BASE_SESSION_CACHE_KEY = 'SESSION__CACHE__KEY__';

View File

@@ -5,7 +5,7 @@ export enum ResultEnum {
SUCCESS = 200, SUCCESS = 200,
ERROR = -1, ERROR = -1,
TIMEOUT = 10042, TIMEOUT = 10042,
TYPE = 'success' TYPE = 'success',
} }
/** /**
@@ -16,7 +16,7 @@ export enum RequestEnum {
POST = 'POST', POST = 'POST',
PATCH = 'PATCH', PATCH = 'PATCH',
PUT = 'PUT', PUT = 'PUT',
DELETE = 'DELETE' DELETE = 'DELETE',
} }
/** /**
@@ -30,5 +30,5 @@ export enum ContentTypeEnum {
// form-data 一般配合qs // form-data 一般配合qs
FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8', FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8',
// form-data 上传 // form-data 上传
FORM_DATA = 'multipart/form-data;charset=UTF-8' FORM_DATA = 'multipart/form-data;charset=UTF-8',
} }

View File

@@ -1,10 +1,14 @@
export enum PageEnum { export enum PageEnum {
// 登录 // 登录
BASE_LOGIN = '/login', BASE_LOGIN = '/login',
BASE_LOGIN_NAME = 'Login',
//重定向 //重定向
REDIRECT = '/Redirect', REDIRECT = '/redirect',
REDIRECT_NAME = 'Redirect',
// 首页 // 首页
BASE_HOME = '/dashboard', BASE_HOME = '/dashboard',
//首页跳转默认路由
BASE_HOME_REDIRECT = '/dashboard/console',
// 错误 // 错误
ERROR_PAGE = '/exception', ERROR_PAGE_NAME = 'ErrorPage',
} }

View File

@@ -3,5 +3,5 @@ export enum RoleEnum {
ADMIN = 'admin', ADMIN = 'admin',
// 普通用户 // 普通用户
NORMAL = 'normal' NORMAL = 'normal',
} }

View File

@@ -0,0 +1,36 @@
import { tryOnMounted, tryOnUnmounted } from '@vueuse/core';
import { useDebounceFn } from '@vueuse/core';
interface WindowSizeOptions {
once?: boolean;
immediate?: boolean;
listenerOptions?: AddEventListenerOptions | boolean;
}
export function useWindowSizeFn<T>(fn: Fn<T>, wait = 150, options?: WindowSizeOptions) {
let handler = () => {
fn();
};
const handleSize = useDebounceFn(handler, wait);
handler = handleSize;
const start = () => {
if (options && options.immediate) {
handler();
}
window.addEventListener('resize', handler);
};
const stop = () => {
window.removeEventListener('resize', handler);
};
tryOnMounted(() => {
start();
});
tryOnUnmounted(() => {
stop();
});
return [start, stop];
}

View File

@@ -1,3 +1,3 @@
import { useAsync } from './use-async' import { useAsync } from './use-async';
export { useAsync } export { useAsync };

View File

@@ -10,8 +10,8 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
VITE_GLOB_APP_SHORT_NAME, VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX, VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL, VITE_GLOB_UPLOAD_URL,
VITE_GLOB_PERMISSION_MODE, VITE_GLOB_PROD_MOCK,
VITE_GLOB_PROD_MOCK VITE_GLOB_IMG_URL,
} = getAppEnvConfig(); } = getAppEnvConfig();
if (!/[a-zA-Z\_]*/.test(VITE_GLOB_APP_SHORT_NAME)) { if (!/[a-zA-Z\_]*/.test(VITE_GLOB_APP_SHORT_NAME)) {
@@ -27,8 +27,8 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
shortName: VITE_GLOB_APP_SHORT_NAME, shortName: VITE_GLOB_APP_SHORT_NAME,
urlPrefix: VITE_GLOB_API_URL_PREFIX, urlPrefix: VITE_GLOB_API_URL_PREFIX,
uploadUrl: VITE_GLOB_UPLOAD_URL, uploadUrl: VITE_GLOB_UPLOAD_URL,
permissionMode: VITE_GLOB_PERMISSION_MODE, prodMock: VITE_GLOB_PROD_MOCK,
prodMock: VITE_GLOB_PROD_MOCK imgUrl: VITE_GLOB_IMG_URL,
}; };
return glob as Readonly<GlobConfig>; return glob as Readonly<GlobConfig>;
}; };

View File

@@ -13,7 +13,6 @@ export function useDesignSetting() {
return { return {
getDarkTheme, getDarkTheme,
getAppTheme, getAppTheme,
getAppThemeList getAppThemeList,
} };
} }

View File

@@ -2,7 +2,6 @@ import { computed } from 'vue';
import { useProjectSettingStore } from '@/store/modules/projectSetting'; import { useProjectSettingStore } from '@/store/modules/projectSetting';
export function useProjectSetting() { export function useProjectSetting() {
const projectStore = useProjectSettingStore(); const projectStore = useProjectSettingStore();
const getNavMode = computed(() => projectStore.navMode); const getNavMode = computed(() => projectStore.navMode);
@@ -29,6 +28,6 @@ export function useProjectSetting() {
getMenuSetting, getMenuSetting,
getCrumbsSetting, getCrumbsSetting,
getPermissionMode, getPermissionMode,
getShowFooter getShowFooter,
} };
} }

View File

@@ -1,15 +1,15 @@
import { Ref, isReactive, isRef } from 'vue' import { isReactive, isRef } from 'vue';
function setLoading(loading, val) { function setLoading(loading, val) {
if (loading != undefined && isRef(loading)) { if (loading != undefined && isRef(loading)) {
loading.value = val loading.value = val;
} else if (loading != undefined && isReactive(loading)) { } else if (loading != undefined && isReactive(loading)) {
loading.loading = val loading.loading = val;
} }
} }
export const useAsync = async (func: Promise<any>, loading: any): Promise<any> => { export const useAsync = async (func: Promise<any>, loading: any): Promise<any> => {
setLoading(loading, true) setLoading(loading, true);
return await func.finally(() => setLoading(loading, false)) return await func.finally(() => setLoading(loading, false));
} };

View File

@@ -1,11 +1,11 @@
import { computed, onMounted, reactive, toRefs } from 'vue' import { computed, onMounted, reactive, toRefs } from 'vue';
interface Battery { interface Battery {
charging: boolean // 当前电池是否正在充电 charging: boolean; // 当前电池是否正在充电
chargingTime: number // 距离充电完毕还需多少秒如果为0则充电完毕 chargingTime: number; // 距离充电完毕还需多少秒如果为0则充电完毕
dischargingTime: number // 代表距离电池耗电至空且挂起需要多少秒 dischargingTime: number; // 代表距离电池耗电至空且挂起需要多少秒
level: number // 代表电量的放大等级,这个值在 0.0 至 1.0 之间 level: number; // 代表电量的放大等级,这个值在 0.0 至 1.0 之间
[key: string]: any [key: string]: any;
} }
export const useBattery = () => { export const useBattery = () => {
@@ -14,56 +14,56 @@ export const useBattery = () => {
charging: false, charging: false,
chargingTime: 0, chargingTime: 0,
dischargingTime: 0, dischargingTime: 0,
level: 100 level: 100,
} },
}) });
// 更新电池使用状态 // 更新电池使用状态
const updateBattery = (target) => { const updateBattery = (target) => {
for (const key in state.battery) { for (const key in state.battery) {
state.battery[key] = target[key] state.battery[key] = target[key];
}
state.battery.level = state.battery.level * 100
} }
state.battery.level = state.battery.level * 100;
};
// 计算电池剩余可用时间 // 计算电池剩余可用时间
const calcDischargingTime = computed(() => { const calcDischargingTime = computed(() => {
const hour = state.battery.dischargingTime / 3600 const hour = state.battery.dischargingTime / 3600;
const minute = (state.battery.dischargingTime / 60) % 60 const minute = (state.battery.dischargingTime / 60) % 60;
return `${ ~~hour }小时${ ~~minute }分钟` return `${~~hour}小时${~~minute}分钟`;
}) });
// 电池状态 // 电池状态
const batteryStatus = computed(() => { const batteryStatus = computed(() => {
if (state.battery.charging && state.battery.level >= 100) { if (state.battery.charging && state.battery.level >= 100) {
return '已充满' return '已充满';
} else if (state.battery.charging) { } else if (state.battery.charging) {
return '充电中' return '充电中';
} else { } else {
return '已断开电源' return '已断开电源';
} }
}) });
onMounted(async () => { onMounted(async () => {
const BatteryManager: Battery = await (window.navigator as any).getBattery() const BatteryManager: Battery = await (window.navigator as any).getBattery();
updateBattery(BatteryManager) updateBattery(BatteryManager);
// 电池充电状态更新时被调用 // 电池充电状态更新时被调用
BatteryManager.onchargingchange = ({ target }) => { BatteryManager.onchargingchange = ({ target }) => {
updateBattery(target) updateBattery(target);
} };
// 电池充电时间更新时被调用 // 电池充电时间更新时被调用
BatteryManager.onchargingtimechange = ({ target }) => { BatteryManager.onchargingtimechange = ({ target }) => {
updateBattery(target) updateBattery(target);
} };
// 电池断开充电时间更新时被调用 // 电池断开充电时间更新时被调用
BatteryManager.ondischargingtimechange = ({ target }) => { BatteryManager.ondischargingtimechange = ({ target }) => {
updateBattery(target) updateBattery(target);
} };
// 电池电量更新时被调用 // 电池电量更新时被调用
BatteryManager.onlevelchange = ({ target }) => { BatteryManager.onlevelchange = ({ target }) => {
updateBattery(target) updateBattery(target);
} };
// new Intl.DateTimeFormat('zh', { // new Intl.DateTimeFormat('zh', {
// year: 'numeric', // year: 'numeric',
@@ -74,11 +74,11 @@ export const useBattery = () => {
// second: '2-digit', // second: '2-digit',
// hour12: false // hour12: false
// }).format(new Date()) // }).format(new Date())
}) });
return { return {
...toRefs(state), ...toRefs(state),
batteryStatus, batteryStatus,
calcDischargingTime calcDischargingTime,
} };
} };

View File

@@ -1,23 +1,23 @@
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue';
import { debounce } from 'lodash' import { debounce } from 'lodash';
/** /**
* description: 获取页面宽度 * description: 获取页面宽度
*/ */
export function useDomWidth() { export function useDomWidth() {
const domWidth = ref(window.innerWidth) const domWidth = ref(window.innerWidth);
function resize() { function resize() {
domWidth.value = document.body.clientWidth domWidth.value = document.body.clientWidth;
} }
onMounted(() => { onMounted(() => {
window.addEventListener('resize', debounce(resize, 80)) window.addEventListener('resize', debounce(resize, 80));
}) });
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener('resize', resize) window.removeEventListener('resize', resize);
}) });
return domWidth return domWidth;
} }

View File

@@ -1,30 +1,30 @@
import { ref, onMounted, onUnmounted, watch } from 'vue' import { ref, onMounted, onUnmounted } from 'vue';
/** /**
* @description 用户网络是否可用 * @description 用户网络是否可用
* */ * */
export function useOnline() { export function useOnline() {
const online = ref(true) const online = ref(true);
const showStatus = (val) => { const showStatus = (val) => {
online.value = typeof val == 'boolean' ? val : val.target.online online.value = typeof val == 'boolean' ? val : val.target.online;
} };
// 在页面加载后,设置正确的网络状态 // 在页面加载后,设置正确的网络状态
navigator.onLine ? showStatus(true) : showStatus(false) navigator.onLine ? showStatus(true) : showStatus(false);
onMounted(() => { onMounted(() => {
// 开始监听网络状态的变化 // 开始监听网络状态的变化
window.addEventListener('online', showStatus) window.addEventListener('online', showStatus);
window.addEventListener('offline', showStatus) window.addEventListener('offline', showStatus);
}) });
onUnmounted(() => { onUnmounted(() => {
// 移除监听网络状态的变化 // 移除监听网络状态的变化
window.removeEventListener('online', showStatus) window.removeEventListener('online', showStatus);
window.removeEventListener('offline', showStatus) window.removeEventListener('offline', showStatus);
}) });
return { online } return { online };
} }

View File

@@ -1,33 +1,33 @@
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue';
/** /**
* @description 获取本地时间 * @description 获取本地时间
*/ */
export function useTime() { export function useTime() {
let timer // 定时器 let timer; // 定时器
const year = ref(0) // 年份 const year = ref(0); // 年份
const month = ref(0) // 月份 const month = ref(0); // 月份
const week = ref('') // 星期几 const week = ref(''); // 星期几
const day = ref(0) // 天数 const day = ref(0); // 天数
const hour = ref<number | string>(0) // 小时 const hour = ref<number | string>(0); // 小时
const minute = ref<number | string>(0) // 分钟 const minute = ref<number | string>(0); // 分钟
const second = ref(0) // 秒 const second = ref(0); // 秒
// 更新时间 // 更新时间
const updateTime = () => { const updateTime = () => {
const date = new Date() const date = new Date();
year.value = date.getFullYear() year.value = date.getFullYear();
month.value = date.getMonth() + 1 month.value = date.getMonth() + 1;
week.value = '日一二三四五六'.charAt(date.getDay()) week.value = '日一二三四五六'.charAt(date.getDay());
day.value = date.getDate() day.value = date.getDate();
hour.value = hour.value =
(date.getHours() + '')?.padStart(2, '0') || (date.getHours() + '')?.padStart(2, '0') ||
new Intl.NumberFormat(undefined, { minimumIntegerDigits: 2 }).format(date.getHours()) new Intl.NumberFormat(undefined, { minimumIntegerDigits: 2 }).format(date.getHours());
minute.value = minute.value =
(date.getMinutes() + '')?.padStart(2, '0') || (date.getMinutes() + '')?.padStart(2, '0') ||
new Intl.NumberFormat(undefined, { minimumIntegerDigits: 2 }).format(date.getMinutes()) new Intl.NumberFormat(undefined, { minimumIntegerDigits: 2 }).format(date.getMinutes());
second.value = date.getSeconds() second.value = date.getSeconds();
} };
// 原生时间格式化 // 原生时间格式化
// new Intl.DateTimeFormat('zh', { // new Intl.DateTimeFormat('zh', {
@@ -40,16 +40,16 @@ export function useTime() {
// hour12: false // hour12: false
// }).format(new Date()) // }).format(new Date())
updateTime() updateTime();
onMounted(() => { onMounted(() => {
clearInterval(timer) clearInterval(timer);
timer = setInterval(() => updateTime(), 1000) timer = setInterval(() => updateTime(), 1000);
}) });
onUnmounted(() => { onUnmounted(() => {
clearInterval(timer) clearInterval(timer);
}) });
return { month, day, hour, minute, second, week } return { month, day, hour, minute, second, week };
} }

View File

@@ -12,18 +12,16 @@ import echarts from '@/utils/lib/echarts';
// import { useRootSetting } from '@/hooks/setting/useRootSetting'; // import { useRootSetting } from '@/hooks/setting/useRootSetting';
export function useECharts( export function useECharts(
elRef: Ref<HTMLDivElement>, elRef: Ref<HTMLDivElement>,
theme: 'light' | 'dark' | 'default' = 'light' theme: 'light' | 'dark' | 'default' = 'light'
) { ) {
// const { getDarkMode } = useRootSetting(); // const { getDarkMode } = useRootSetting();
const getDarkMode = 'light' const getDarkMode = 'light';
let chartInstance: echarts.ECharts | null = null; let chartInstance: echarts.ECharts | null = null;
let resizeFn: Fn = resize; let resizeFn: Fn = resize;
const cacheOptions = ref<EChartsOption>({}); const cacheOptions = ref<EChartsOption>({});
let removeResizeFn: Fn = () => { let removeResizeFn: Fn = () => {};
};
resizeFn = useDebounceFn(resize, 200); resizeFn = useDebounceFn(resize, 200);

View File

@@ -1,4 +1,4 @@
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user';
export function usePermission() { export function usePermission() {
const userStore = useUserStore(); const userStore = useUserStore();
@@ -8,10 +8,10 @@ export function usePermission() {
* @param accesses * @param accesses
*/ */
function _someRoles(accesses: string[]) { function _someRoles(accesses: string[]) {
return userStore.getRoles.some(item => { return userStore.getRoles.some((item) => {
const { value }: any = item const { value }: any = item;
return accesses.includes(value) return accesses.includes(value);
}) });
} }
/** /**
@@ -19,8 +19,8 @@ export function usePermission() {
* 可用于 v-if 显示逻辑 * 可用于 v-if 显示逻辑
* */ * */
function hasPermission(accesses: string[]): boolean { function hasPermission(accesses: string[]): boolean {
if (!accesses.length) return true if (!accesses || !accesses.length) return true;
return _someRoles(accesses) return _someRoles(accesses);
} }
/** /**
@@ -28,11 +28,11 @@ export function usePermission() {
* @param accesses * @param accesses
*/ */
function hasEveryPermission(accesses: string[]): boolean { function hasEveryPermission(accesses: string[]): boolean {
const rolesList = userStore.getRoles const rolesList = userStore.getRoles;
if (Array.isArray(accesses)) { if (Array.isArray(accesses)) {
return accesses.every((access) => !!rolesList[access]) return accesses.every((access) => !!rolesList[access]);
} }
throw new Error(`[hasEveryPermission]: ${ accesses } should be a array !`) throw new Error(`[hasEveryPermission]: ${accesses} should be a array !`);
} }
/** /**
@@ -41,11 +41,11 @@ export function usePermission() {
* @param accessMap * @param accessMap
*/ */
function hasSomePermission(accesses: string[]): boolean { function hasSomePermission(accesses: string[]): boolean {
const rolesList = userStore.getRoles const rolesList = userStore.getRoles;
if (Array.isArray(accesses)) { if (Array.isArray(accesses)) {
return accesses.some((access) => !!rolesList[access]) return accesses.some((access) => !!rolesList[access]);
} }
throw new Error(`[hasSomePermission]: ${ accesses } should be a array !`) throw new Error(`[hasSomePermission]: ${accesses} should be a array !`);
} }
return { hasPermission, hasEveryPermission, hasSomePermission }; return { hasPermission, hasEveryPermission, hasSomePermission };

View File

@@ -1,3 +1,3 @@
import PageFooter from './index.vue' import PageFooter from './index.vue';
export { PageFooter } export { PageFooter };

View File

@@ -1,48 +1,37 @@
<template> <template>
<div class="page-footer"> <div class="page-footer">
<div class="page-footer-link"> <div class="page-footer-link">
<a href="https://github.com/jekip/naive-ui-admin" target="_blank"> <a href="https://github.com/jekip/naive-ui-admin" target="_blank"> 官网 </a>
官网 <a href="https://github.com/jekip/naive-ui-admin" target="_blank"> 社区 </a>
</a> <a href="https://github.com/jekip/naive-ui-admin/issues" target="_blank"> 交流 </a>
<a href="https://github.com/jekip/naive-ui-admin" target="_blank">
社区
</a>
<a href="https://github.com/jekip/naive-ui-admin/issues" target="_blank">
交流
</a>
</div> </div>
<div class="copyright"> <div class="copyright"> naive-ui-admin 1.4 · Made by Ah jung </div>
naive-ui-admin 1.2 · Made by Ah jung
</div> </div>
</div>
</template> </template>
<script> <script>
import { GithubOutlined, CopyrightOutlined } from '@vicons/antd'
export default { export default {
name: 'PageFooter', name: 'PageFooter',
components: { GithubOutlined, CopyrightOutlined }, components: {},
props: { props: {
collapsed: { collapsed: {
type: Boolean type: Boolean,
} },
} },
} };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.page-footer { .page-footer {
margin: 48px 0 24px 0; //margin: 28px 0 24px 0;
padding: 0 16px; padding: 0 16px;
text-align: center; text-align: center;
a { a {
font-size: 14px; font-size: 14px;
color: #808695; color: #808695;
-webkit-transition: all .2s ease-in-out; -webkit-transition: all 0.2s ease-in-out;
transition: all .2s ease-in-out; transition: all 0.2s ease-in-out;
&:hover { &:hover {
color: #515a6e; color: #515a6e;

View File

@@ -7,17 +7,28 @@
<div class="drawer-setting-item justify-center dark-switch"> <div class="drawer-setting-item justify-center dark-switch">
<n-tooltip placement="bottom"> <n-tooltip placement="bottom">
<template #trigger> <template #trigger>
<n-switch v-model:value="designStore.darkTheme"/> <n-switch v-model:value="designStore.darkTheme" class="dark-theme-switch">
<template #checked>
<n-icon size="14" color="#ffd93b">
<SunnySharp />
</n-icon>
</template>
<template #unchecked>
<n-icon size="14" color="#ffd93b">
<Moon />
</n-icon>
</template>
</n-switch>
</template> </template>
<span>深色主题</span> <span>深色主题</span>
</n-tooltip> </n-tooltip>
</div> </div>
<n-divider title-placement="center">系统主题</n-divider> <n-divider title-placement="center">系统主题</n-divider>
<div class="drawer-setting-item align-items-top"> <div class="drawer-setting-item align-items-top">
<span class="theme-item" <span
class="theme-item"
v-for="(item, index) in appThemeList" v-for="(item, index) in appThemeList"
:key="index" :key="index"
:style="{ 'background-color': item }" :style="{ 'background-color': item }"
@@ -53,7 +64,6 @@
</div> </div>
</div> </div>
<n-divider title-placement="center">导航栏风格</n-divider> <n-divider title-placement="center">导航栏风格</n-divider>
<div class="drawer-setting-item align-items-top"> <div class="drawer-setting-item align-items-top">
@@ -82,7 +92,10 @@
<div class="drawer-setting-item-style"> <div class="drawer-setting-item-style">
<n-tooltip placement="top"> <n-tooltip placement="top">
<template #trigger> <template #trigger>
<img src="~@/assets/images/header-theme-dark.svg" @click="togNavTheme('header-dark')"/> <img
src="~@/assets/images/header-theme-dark.svg"
@click="togNavTheme('header-dark')"
/>
</template> </template>
<span>暗色顶栏</span> <span>暗色顶栏</span>
</n-tooltip> </n-tooltip>
@@ -93,9 +106,7 @@
<n-divider title-placement="center">界面功能</n-divider> <n-divider title-placement="center">界面功能</n-divider>
<div class="drawer-setting-item"> <div class="drawer-setting-item">
<div class="drawer-setting-item-title"> <div class="drawer-setting-item-title"> 固定顶栏 </div>
固定顶栏
</div>
<div class="drawer-setting-item-action"> <div class="drawer-setting-item-action">
<n-switch v-model:value="settingStore.headerSetting.fixed" /> <n-switch v-model:value="settingStore.headerSetting.fixed" />
</div> </div>
@@ -111,9 +122,7 @@
<!-- </div>--> <!-- </div>-->
<div class="drawer-setting-item"> <div class="drawer-setting-item">
<div class="drawer-setting-item-title"> <div class="drawer-setting-item-title"> 固定多页签 </div>
固定多页签
</div>
<div class="drawer-setting-item-action"> <div class="drawer-setting-item-action">
<n-switch v-model:value="settingStore.multiTabsSetting.fixed" /> <n-switch v-model:value="settingStore.multiTabsSetting.fixed" />
</div> </div>
@@ -122,127 +131,116 @@
<n-divider title-placement="center">界面显示</n-divider> <n-divider title-placement="center">界面显示</n-divider>
<div class="drawer-setting-item"> <div class="drawer-setting-item">
<div class="drawer-setting-item-title"> <div class="drawer-setting-item-title"> 显示重载页面按钮 </div>
显示重载页面按钮
</div>
<div class="drawer-setting-item-action"> <div class="drawer-setting-item-action">
<n-switch v-model:value="settingStore.headerSetting.isReload" /> <n-switch v-model:value="settingStore.headerSetting.isReload" />
</div> </div>
</div> </div>
<div class="drawer-setting-item"> <div class="drawer-setting-item">
<div class="drawer-setting-item-title"> <div class="drawer-setting-item-title"> 显示面包屑导航 </div>
显示面包屑导航
</div>
<div class="drawer-setting-item-action"> <div class="drawer-setting-item-action">
<n-switch v-model:value="settingStore.crumbsSetting.show" /> <n-switch v-model:value="settingStore.crumbsSetting.show" />
</div> </div>
</div> </div>
<div class="drawer-setting-item"> <div class="drawer-setting-item">
<div class="drawer-setting-item-title"> <div class="drawer-setting-item-title"> 显示面包屑显示图标 </div>
显示面包屑显示图标
</div>
<div class="drawer-setting-item-action"> <div class="drawer-setting-item-action">
<n-switch v-model:value="settingStore.crumbsSetting.showIcon" /> <n-switch v-model:value="settingStore.crumbsSetting.showIcon" />
</div> </div>
</div> </div>
<div class="drawer-setting-item"> <div class="drawer-setting-item">
<div class="drawer-setting-item-title"> <div class="drawer-setting-item-title"> 显示多页签 </div>
显示多页签
</div>
<div class="drawer-setting-item-action"> <div class="drawer-setting-item-action">
<n-switch v-model:value="settingStore.multiTabsSetting.show" /> <n-switch v-model:value="settingStore.multiTabsSetting.show" />
</div> </div>
</div> </div>
<!--1.15废弃没啥用占用操作空间-->
<div class="drawer-setting-item"> <!-- <div class="drawer-setting-item">-->
<div class="drawer-setting-item-title"> <!-- <div class="drawer-setting-item-title"> 显示页脚 </div>-->
显示页脚 <!-- <div class="drawer-setting-item-action">-->
</div> <!-- <n-switch v-model:value="settingStore.showFooter" />-->
<div class="drawer-setting-item-action"> <!-- </div>-->
<n-switch v-model:value="settingStore.showFooter"/> <!-- </div>-->
</div>
</div>
<div class="drawer-setting-item"> <div class="drawer-setting-item">
<n-alert type="warning" :showIcon="false"> <n-alert type="warning" :showIcon="false">
<p>{{ alertText }}</p> <p>{{ alertText }}</p>
</n-alert> </n-alert>
</div> </div>
</div> </div>
</n-drawer-content> </n-drawer-content>
</n-drawer> </n-drawer>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, toRefs, watch, createVNode, computed, unref } from 'vue' import { defineComponent, reactive, toRefs, watch } from 'vue';
import { useProjectSettingStore } from "@/store/modules/projectSetting"; import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useDesignSettingStore } from "@/store/modules/designSetting"; import { useDesignSettingStore } from '@/store/modules/designSetting';
import { CheckOutlined } from '@vicons/antd' import { CheckOutlined } from '@vicons/antd';
import { darkTheme } from 'naive-ui' import { Moon, SunnySharp } from '@vicons/ionicons5';
import { darkTheme } from 'naive-ui';
export default defineComponent({ export default defineComponent({
name: 'ProjectSetting', name: 'ProjectSetting',
components: { CheckOutlined, Moon, SunnySharp },
props: { props: {
title: { title: {
type: String, type: String,
default: '项目配置' default: '项目配置',
}, },
width: { width: {
type: Number, type: Number,
default: 280 default: 280,
}, },
}, },
components: { CheckOutlined }, setup(props) {
setup(props, { emit }) { const settingStore = useProjectSettingStore();
const settingStore = useProjectSettingStore() const designStore = useDesignSettingStore();
const designStore = useDesignSettingStore()
const { width, title } = props
const state = reactive({ const state = reactive({
width, width: props.width,
title, title: props.title,
isDrawer: false, isDrawer: false,
placement: "right", placement: 'right',
alertText: '该功能主要实时预览各种布局效果,更多完整配置在 projectSetting.ts 中设置,建议在生产环境关闭该布局预览功能。', alertText:
appThemeList: designStore.appThemeList '该功能主要实时预览各种布局效果,更多完整配置在 projectSetting.ts 中设置,建议在生产环境关闭该布局预览功能。',
}) appThemeList: designStore.appThemeList,
});
watch( watch(
() => designStore.darkTheme, () => designStore.darkTheme,
(to) => { (to) => {
settingStore.navTheme = to ? 'header-dark' : 'dark' settingStore.navTheme = to ? 'header-dark' : 'dark';
} }
) );
function openDrawer(isDrawer) { function openDrawer() {
state.isDrawer = true state.isDrawer = true;
} }
function closeDrawer() { function closeDrawer() {
state.isDrawer = false state.isDrawer = false;
} }
function togNavTheme(theme) { function togNavTheme(theme) {
settingStore.navTheme = theme settingStore.navTheme = theme;
if (settingStore.navMode === 'horizontal' && theme === 'light') { if (settingStore.navMode === 'horizontal' && theme === 'light') {
designStore.navTheme = 'dark' settingStore.navTheme = 'dark';
} }
} }
function togTheme(color) { function togTheme(color) {
designStore.appTheme = color designStore.appTheme = color;
} }
function togNavMode(mode) { function togNavMode(mode) {
settingStore.navMode = mode settingStore.navMode = mode;
if (mode === 'horizontal') { if (mode === 'horizontal') {
settingStore.setNavTheme('light') settingStore.setNavTheme('light');
} else { } else {
settingStore.setNavTheme('dark') settingStore.setNavTheme('dark');
} }
} }
@@ -256,9 +254,9 @@ export default defineComponent({
darkTheme, darkTheme,
openDrawer, openDrawer,
closeDrawer, closeDrawer,
} };
} },
}) });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@@ -301,7 +299,7 @@ export default defineComponent({
text-align: center; text-align: center;
.n-icon { .n-icon {
color: #fff color: #fff;
} }
} }
} }
@@ -314,8 +312,7 @@ export default defineComponent({
.justify-center { .justify-center {
justify-content: center; justify-content: center;
} }
.dark-switch .n-switch {
.dark-switch .n-switch--active {
::v-deep(.n-switch__rail) { ::v-deep(.n-switch__rail) {
background-color: #000e1c; background-color: #000e1c;
} }

View File

@@ -11,8 +11,8 @@ import {
ReloadOutlined, ReloadOutlined,
LogoutOutlined, LogoutOutlined,
UserOutlined, UserOutlined,
CheckOutlined CheckOutlined,
} from '@vicons/antd' } from '@vicons/antd';
export default { export default {
SettingOutlined, SettingOutlined,
@@ -27,5 +27,5 @@ export default {
ReloadOutlined, ReloadOutlined,
LogoutOutlined, LogoutOutlined,
UserOutlined, UserOutlined,
CheckOutlined CheckOutlined,
} };

View File

@@ -1,3 +1,3 @@
import PageHeader from './index.vue' import PageHeader from './index.vue';
export { PageHeader } export { PageHeader };

View File

@@ -7,8 +7,10 @@
<!--左侧菜单--> <!--左侧菜单-->
<div class="layout-header-left" v-else> <div class="layout-header-left" v-else>
<!-- 菜单收起 --> <!-- 菜单收起 -->
<div class="ml-1 layout-header-trigger layout-header-trigger-min" <div
@click="() => $emit('update:collapsed', !collapsed)"> class="ml-1 layout-header-trigger layout-header-trigger-min"
@click="() => $emit('update:collapsed', !collapsed)"
>
<n-icon size="18" v-if="collapsed"> <n-icon size="18" v-if="collapsed">
<MenuUnfoldOutlined /> <MenuUnfoldOutlined />
</n-icon> </n-icon>
@@ -17,8 +19,11 @@
</n-icon> </n-icon>
</div> </div>
<!-- 刷新 --> <!-- 刷新 -->
<div class="mr-1 layout-header-trigger layout-header-trigger-min" v-if="headerSetting.isReload" <div
@click="reloadPage"> class="mr-1 layout-header-trigger layout-header-trigger-min"
v-if="headerSetting.isReload"
@click="reloadPage"
>
<n-icon size="18"> <n-icon size="18">
<ReloadOutlined /> <ReloadOutlined />
</n-icon> </n-icon>
@@ -33,12 +38,18 @@
@select="dropdownSelect" @select="dropdownSelect"
> >
<span class="link-text"> <span class="link-text">
<component v-if="crumbsSetting.showIcon && routeItem.meta.icon" :is="routeItem.meta.icon"></component> <component
v-if="crumbsSetting.showIcon && routeItem.meta.icon"
:is="routeItem.meta.icon"
/>
{{ routeItem.meta.title }} {{ routeItem.meta.title }}
</span> </span>
</n-dropdown> </n-dropdown>
<span class="link-text" v-else> <span class="link-text" v-else>
<component v-if="crumbsSetting.showIcon && routeItem.meta.icon" :is="routeItem.meta.icon"></component> <component
v-if="crumbsSetting.showIcon && routeItem.meta.icon"
:is="routeItem.meta.icon"
/>
{{ routeItem.meta.title }} {{ routeItem.meta.title }}
</span> </span>
</n-breadcrumb-item> </n-breadcrumb-item>
@@ -46,7 +57,11 @@
</n-breadcrumb> </n-breadcrumb>
</div> </div>
<div class="layout-header-right"> <div class="layout-header-right">
<div class="layout-header-trigger layout-header-trigger-min" v-for="item in iconList" :key="item.icon.name"> <div
class="layout-header-trigger layout-header-trigger-min"
v-for="item in iconList"
:key="item.icon.name"
>
<n-tooltip placement="bottom"> <n-tooltip placement="bottom">
<template #trigger> <template #trigger>
<n-icon size="18"> <n-icon size="18">
@@ -71,9 +86,11 @@
<div class="layout-header-trigger layout-header-trigger-min"> <div class="layout-header-trigger layout-header-trigger-min">
<n-dropdown trigger="hover" @select="avatarSelect" :options="avatarOptions"> <n-dropdown trigger="hover" @select="avatarSelect" :options="avatarOptions">
<div class="avatar"> <div class="avatar">
<n-avatar> <n-avatar round>
{{ username }} {{ username }}
<template #icon><UserOutlined/></template> <template #icon>
<UserOutlined />
</template>
</n-avatar> </n-avatar>
</div> </div>
</n-dropdown> </n-dropdown>
@@ -96,42 +113,36 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, toRefs, ref, computed, unref } from 'vue' import { defineComponent, reactive, toRefs, ref, computed, unref } from 'vue';
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router';
import components from './components' import components from './components';
import { NDialogProvider, useDialog, useMessage, useNotification } from 'naive-ui' import { NDialogProvider, useDialog, useMessage } from 'naive-ui';
import { TABS_ROUTES } from '@/store/mutation-types' import { TABS_ROUTES } from '@/store/mutation-types';
import { useUserStore } from '@/store/modules/user' import { useUserStore } from '@/store/modules/user';
import { useLockscreenStore } from '@/store/modules/lockscreen' import { useLockscreenStore } from '@/store/modules/lockscreen';
import ProjectSetting from './ProjectSetting.vue' import ProjectSetting from './ProjectSetting.vue';
import { AsideMenu } from '@/layout/components/Menu' import { AsideMenu } from '@/layout/components/Menu';
import { useProjectSetting } from "@/hooks/setting/useProjectSetting"; import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
export default defineComponent({ export default defineComponent({
name: 'PageHeader', name: 'PageHeader',
components: { ...components, NDialogProvider, ProjectSetting, AsideMenu }, components: { ...components, NDialogProvider, ProjectSetting, AsideMenu },
props: { props: {
collapsed: { collapsed: {
type: Boolean type: Boolean,
} },
}, },
setup(props) { setup(props) {
const userStore = useUserStore() const userStore = useUserStore();
const useLockscreen = useLockscreenStore() const useLockscreen = useLockscreenStore();
const message = useMessage() const message = useMessage();
const notification = useNotification() const dialog = useDialog();
const dialog = useDialog() const { getNavMode, getNavTheme, getHeaderSetting, getMenuSetting, getCrumbsSetting } =
const { useProjectSetting();
getNavMode,
getNavTheme,
getHeaderSetting,
getMenuSetting,
getCrumbsSetting
} = useProjectSetting()
const { username } = userStore?.info || {} const { username } = userStore?.info || {};
const drawerSetting = ref() const drawerSetting = ref();
const state = reactive({ const state = reactive({
username: username || '', username: username || '',
@@ -140,146 +151,144 @@ export default defineComponent({
navTheme: getNavTheme, navTheme: getNavTheme,
headerSetting: getHeaderSetting, headerSetting: getHeaderSetting,
crumbsSetting: getCrumbsSetting, crumbsSetting: getCrumbsSetting,
}) });
const getChangeStyle = computed(() => { const getChangeStyle = computed(() => {
const { collapsed } = props const { collapsed } = props;
const { minMenuWidth, menuWidth }: any = unref(getMenuSetting) const { minMenuWidth, menuWidth }: any = unref(getMenuSetting);
return { return {
'left': collapsed ? `${ minMenuWidth }px` : `${ menuWidth }px`, left: collapsed ? `${minMenuWidth}px` : `${menuWidth}px`,
'width': `calc(100% - ${ collapsed ? `${ minMenuWidth }px` : `${ menuWidth }px` })` width: `calc(100% - ${collapsed ? `${minMenuWidth}px` : `${menuWidth}px`})`,
} };
}) });
const router = useRouter() const router = useRouter();
const route = useRoute() const route = useRoute();
const generator: any = (routerMap, parent) => { const generator: any = (routerMap) => {
return routerMap.map((item, key) => { return routerMap.map((item) => {
const currentMenu = { const currentMenu = {
...item, ...item,
label: item.meta.title, label: item.meta.title,
key: item.name, key: item.name,
disabled: item.path === '/', disabled: item.path === '/',
} };
// 是否有子菜单,并递归处理 // 是否有子菜单,并递归处理
if (item.children && item.children.length > 0) { if (item.children && item.children.length > 0) {
// Recursion // Recursion
currentMenu.children = generator(item.children, currentMenu) currentMenu.children = generator(item.children, currentMenu);
}
return currentMenu
})
} }
return currentMenu;
});
};
const breadcrumbList = computed(() => { const breadcrumbList = computed(() => {
return generator(route.matched) return generator(route.matched);
}) });
const dropdownSelect = (key) => { const dropdownSelect = (key) => {
router.push({ name: key }) router.push({ name: key });
} };
// 刷新页面 // 刷新页面
const reloadPage = () => { const reloadPage = () => {
router.push({ router.push({
path: '/redirect' + unref(route).fullPath path: '/redirect' + unref(route).fullPath,
}) });
} };
// 退出登录 // 退出登录
const doLogout = () => { const doLogout = () => {
dialog.warning({ dialog.info({
title: '提示', title: '提示',
content: '您确定要退出登录吗', content: '您确定要退出登录吗',
positiveText: '确定', positiveText: '确定',
negativeText: '取消', negativeText: '取消',
onPositiveClick: () => { onPositiveClick: () => {
userStore.logout().then((res) => { userStore.logout().then(() => {
message.success('成功退出登录') message.success('成功退出登录');
// 移除标签页 // 移除标签页
localStorage.removeItem(TABS_ROUTES) localStorage.removeItem(TABS_ROUTES);
router router
.replace({ .replace({
name: 'Login', name: 'Login',
query: { query: {
redirect: route.fullPath redirect: route.fullPath,
}
})
.finally(() => location.reload())
})
}, },
onNegativeClick: () => {
}
}) })
} .finally(() => location.reload());
});
},
onNegativeClick: () => {},
});
};
// 切换全屏图标 // 切换全屏图标
const toggleFullscreenIcon = () => const toggleFullscreenIcon = () =>
(state.fullscreenIcon = (state.fullscreenIcon =
document.fullscreenElement !== null ? 'FullscreenExitOutlined' : 'FullscreenOutlined') document.fullscreenElement !== null ? 'FullscreenExitOutlined' : 'FullscreenOutlined');
// 监听全屏切换事件 // 监听全屏切换事件
document.addEventListener('fullscreenchange', toggleFullscreenIcon) document.addEventListener('fullscreenchange', toggleFullscreenIcon);
// 全屏切换 // 全屏切换
const toggleFullScreen = () => { const toggleFullScreen = () => {
if (!document.fullscreenElement) { if (!document.fullscreenElement) {
document.documentElement.requestFullscreen() document.documentElement.requestFullscreen();
} else { } else {
if (document.exitFullscreen) { if (document.exitFullscreen) {
document.exitFullscreen() document.exitFullscreen();
}
} }
} }
};
// 图标列表 // 图标列表
const iconList = [ const iconList = [
{ {
icon: 'SearchOutlined', icon: 'SearchOutlined',
tips: '搜索' tips: '搜索',
}, },
{ {
icon: 'GithubOutlined', icon: 'GithubOutlined',
tips: 'github', tips: 'github',
eventObject: { eventObject: {
click: () => window.open('https://github.com/jekip/naive-ui-admin') click: () => window.open('https://github.com/jekip/naive-ui-admin'),
} },
}, },
{ {
icon: 'LockOutlined', icon: 'LockOutlined',
tips: '锁屏', tips: '锁屏',
eventObject: { eventObject: {
click: () => useLockscreen.setLock(true) click: () => useLockscreen.setLock(true),
} },
} },
] ];
const avatarOptions = [ const avatarOptions = [
{ {
label: '个人设置', label: '个人设置',
key: 1 key: 1,
}, },
{ {
label: '退出登录', label: '退出登录',
key: 2 key: 2,
}, },
] ];
//头像下拉菜单 //头像下拉菜单
const avatarSelect = (key) => { const avatarSelect = (key) => {
switch (key) { switch (key) {
case 1: case 1:
router.push({name:'Setting'}) router.push({ name: 'Setting' });
break; break;
case 2: case 2:
doLogout() doLogout();
break; break;
} }
} };
function openSetting() { function openSetting() {
const { openDrawer } = drawerSetting.value const { openDrawer } = drawerSetting.value;
openDrawer() openDrawer();
} }
return { return {
@@ -296,9 +305,9 @@ export default defineComponent({
reloadPage, reloadPage,
drawerSetting, drawerSetting,
openSetting, openSetting,
} };
} },
}) });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@@ -309,7 +318,7 @@ export default defineComponent({
padding: 0; padding: 0;
height: @header-height; height: @header-height;
box-shadow: 0 1px 4px rgb(0 21 41 / 8%); box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
transition: all .2s ease-in-out; transition: all 0.2s ease-in-out;
width: 100%; width: 100%;
z-index: 11; z-index: 11;
//color: #fff; //color: #fff;
@@ -367,7 +376,7 @@ export default defineComponent({
} }
&:hover { &:hover {
background: hsla(0, 0%, 100%, .08); background: hsla(0, 0%, 100%, 0.08);
} }
.anticon { .anticon {
@@ -387,7 +396,7 @@ export default defineComponent({
color: #515a6e; color: #515a6e;
.n-icon { .n-icon {
color: #515a6e color: #515a6e;
} }
.layout-header-left { .layout-header-left {

View File

@@ -1,3 +1,3 @@
import Logo from './index.vue' import Logo from './index.vue';
export { Logo } export { Logo };

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="logo"> <div class="logo">
<img src="~@/assets/images/logo.png" alt="" /> <img src="~@/assets/images/logo.png" alt="" />
<h2 v-show="!collapsed" class="title">NaiveUiAdmin</h2> <h2 v-show="!collapsed" class="title">&nbsp;NaiveUiAdmin</h2>
</div> </div>
</template> </template>
@@ -10,10 +10,10 @@ export default {
name: 'Index', name: 'Index',
props: { props: {
collapsed: { collapsed: {
type: Boolean type: Boolean,
} },
} },
} };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@@ -27,6 +27,7 @@ export default {
white-space: nowrap; white-space: nowrap;
img { img {
width: auto;
height: 32px; height: 32px;
} }

View File

@@ -1,3 +1,3 @@
import MainView from './index.vue' import MainView from './index.vue';
export { MainView } export { MainView };

Some files were not shown because too many files have changed in this diff Show More