34 Commits

Author SHA1 Message Date
xiaoma
9c512002d2 Fixes bug 动态路由配置重构 2021-08-10 17:16:58 +08:00
Ah jung
737f967aab Merge pull request #24 from CasbaL/fix/table-setting-bug
fix(Table): 基本表格设置列固定时,重复添加action列导致的样式错乱问题
2021-08-10 17:11:51 +08:00
casbal
1cdb02c9d7 fix: 基本表格设置列固定时,重复添加action列导致的样式错乱问题 2021-08-10 17:05:52 +08:00
Ah jung
bc8dd21405 Merge pull request #23 from Dishone/main
暗色模式下多页签背景问题
2021-08-10 17:00:38 +08:00
Dishone
2dba60405e 暗色模式下多页签背景问题 2021-08-10 03:21:40 +08:00
xiaoma
eba3047be2 iframe 开启滚动条 2021-08-09 16:32:33 +08:00
xiaoma
0979b5af5d 新增:Form组件支持响应式配置,路由支持外部地址(内联) 2021-08-09 16:16:16 +08:00
xiaoma
ade138997d logo美化,顶部菜单新增logo展示 2021-08-09 10:50:16 +08:00
xiaoma
d388ae5656 fix bug 2021-08-09 10:17:37 +08:00
啊俊
8f05b20ffa fix bug #22 表格列默认开启 ellipsis 属性 2021-08-08 15:17:02 +08:00
啊俊
d973b2a543 fix bug #20 2021-08-07 16:38:54 +08:00
Ah jung
1d5113a663 Merge pull request #21 from zhouyuf/master
添加锁屏时enter键解除锁屏
2021-08-07 16:14:48 +08:00
zhouyuf
f331d9c4c7 添加锁屏时enter键解除锁屏 2021-08-07 15:20:41 +08:00
Ah jung
c647e19d06 文档和预览地址变更 2021-08-07 10:00:03 +08:00
Ah jung
5c5c52d9fa 日常版本更新 2021-08-06 17:10:54 +08:00
Ah jung
3e0b8efe7e Fixes bug 新增 顶部混合菜单 2021-08-06 17:06:33 +08:00
Ah jung
450234e7ea Fixes bug 2021-08-05 21:38:32 +08:00
Ah jung
5116c387d5 Merge remote-tracking branch 'origin/main' 2021-08-05 17:24:55 +08:00
Ah jung
8a5f237630 Fixes bug add baseModal | baseForm 组件 2021-08-05 17:24:24 +08:00
Ah jung
1e3ccaa6dc Update README.md 2021-08-03 08:30:08 +08:00
Ah jung
98e1bf0227 Merge pull request #17 from Chika99/fix/table
fix(table): 修复表格分页计算问题
2021-08-02 09:06:43 +08:00
chika
6a290b314a fix(table): 修复表格分页计算问题 2021-08-01 23:07:37 +08:00
Ah jung
58f0997fb6 Merge pull request #16 from Chika99/fix/lockscreen
fix(lockscreen): 修复锁屏几个问题
2021-07-31 14:30:17 +08:00
chika
e602fc50c0 fix(lockscreen): 调整电量显示位置 2021-07-31 14:25:52 +08:00
chika
81a3e6d970 fix(lockscreen): 修复电池组件在宽度较小的情况下会与时间重叠 2021-07-31 14:20:47 +08:00
chika
0c709871f3 fix(lockscreen): 修复距离电池充满时间显示错误 2021-07-31 14:11:24 +08:00
Ah jung
9d9cac8064 Fixes bug add Project Docs 2021-07-30 20:00:38 +08:00
Ah jung
b8f8334539 Merge remote-tracking branch 'origin/main' 2021-07-30 19:55:54 +08:00
Ah jung
361a2a14c7 Merge pull request #15 from Chika99/feat/loadingBar
Feat/loading bar
2021-07-30 19:55:09 +08:00
chika
dd4e6c1670 feat(loadingBar): 增加加载条与主题颜色适配 2021-07-30 19:32:59 +08:00
chika
85d39add87 feat(loadingBar): 增加加载条与主题颜色适配 2021-07-30 19:26:41 +08:00
Ah jung
20da92aeab Fixes bug 2021-07-30 17:27:58 +08:00
Ah jung
57245d21ee Merge pull request #12 from it-fork/fix/naming-conv
fix(命名规范):修复store中重构之后遗留的命名错误
2021-07-30 14:57:02 +08:00
liujunzheng
fd6fd723d7 fix(命名规范):修复store中重构之后遗留的命名错误 2021-07-30 14:39:52 +08:00
102 changed files with 3144 additions and 376 deletions

View File

@@ -61,6 +61,7 @@ module.exports = defineConfig({
'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',
'vue/script-setup-uses-vars': 'off',
'vue/html-self-closing': [ 'vue/html-self-closing': [
'error', 'error',
{ {

View File

@@ -1,4 +1,61 @@
# 1.5 (2021-07-30) # CHANGELOG
## 1.5.4 (2021-08-10)
### 🐛 Bug Fixes
- `暗色模式下多页签背景问题 ` 合并 [#23](https://github.com/jekip/naive-ui-admin/pull/23) 感谢 [@Dishone](https://github.com/Dishone)
- `表格设置列重复添加action列样式错乱问题` 合并 [#24](https://github.com/jekip/naive-ui-admin/pull/24) 感谢 [@CasbaL](https://github.com/CasbaL)
- ### ✨ Features
-(破坏性更新)
- 优化 `动态路由配置` 取消`constantRouterComponents.ts`,中组件映射配置,更名为 `router-icons.ts`
- 优化 `admin_info接口结构`roles 更名为permissionsroles.roleName更名为label
- 优化 多级路由,当没有配置时,`redirect` `redirect` 默认为第一个子路由,配置则优先按配置
- 依赖升级
# 1.5.3 (2021-08-09)
### 🐛 Bug Fixes
- 修复顶部菜单,选中联动
- 修复混合菜单模式,切换其他模式菜单未重置
- 实例基础列表,和表格组件实例,开启横向滚动特性
- `naiveui` 升级成最新版
- ### ✨ Features
- table组件默认开启 `ellipsis` 特性
# 1.5.2 (2021-08-06)
### 🐛 Bug Fixes
- 修复已知bug
- ### ✨ Features
- 新增 `混合菜单模式`
- 新增 `根路由`
- 新增 `关于` 根路由示例页面
- 文档同步更新,组件和示例
# 1.5.1 (2021-08-05)
### 🐛 Bug Fixes
- 修复windows系统获取项目换行符问题
- 修复表格分页计算问题 [@Chika99](https://github.com/Chika99)
- 修复锁屏样式自适应问题 [@Chika99](https://github.com/Chika99)
- 依赖 dayjs 移除用date-fns和UI框架底层保持一致
- 修复已知bug
- ### ✨ Features
- 新增 `baseForm` 组件,和`基础``useForm`使用方式
- 新增 `baseModal`,组件,和 `useForm`使用方式
- 新增`子菜单` new Tag标签
- 菜单支持 `根路由`配置
# 1.5.0 (2021-07-30)
### 🐛 Bug Fixes ### 🐛 Bug Fixes
- 修复表格列配置,拖拽时最后的操作列重复增加 - 修复表格列配置,拖拽时最后的操作列重复增加
- 多标签页交互优化 - 多标签页交互优化
@@ -15,7 +72,7 @@
- 本次更新,有破坏性更新,涉及文件重命名,增删调整 - 本次更新,有破坏性更新,涉及文件重命名,增删调整
# 1.4 (2021-07-21) # 1.4.0 (2021-07-21)
### 🐛 Bug Fixes ### 🐛 Bug Fixes
- vite降至2.3.6 - vite降至2.3.6
- 多标签页交互优化 - 多标签页交互优化
@@ -27,7 +84,7 @@
- 持续更新更多实用组件及示例感谢Star - 持续更新更多实用组件及示例感谢Star
# 1.3 (2021-07-19) # 1.3.0 (2021-07-19)
### 🐛 Bug Fixes ### 🐛 Bug Fixes
- 修复多标签页左右切换按钮自适应展示 - 修复多标签页左右切换按钮自适应展示
- 修复登录页面出现多标签页 - 修复登录页面出现多标签页
@@ -40,7 +97,7 @@
- 持续更新更多实用组件及示例感谢Star - 持续更新更多实用组件及示例感谢Star
# 1.2 (2021-07-16) # 1.2.0 (2021-07-16)
### 🐛 Bug Fixes ### 🐛 Bug Fixes
- 修复面包屑显示登录页面 - 修复面包屑显示登录页面
- 菜单支持只展开当前父级菜单 - 菜单支持只展开当前父级菜单
@@ -54,7 +111,7 @@
- 持续更新更多实用示例,同时也演示`Naive UI`使用方法 - 持续更新更多实用示例,同时也演示`Naive UI`使用方法
# 1.1 (2021-07-15) # 1.1.0 (2021-07-15)
- ### ✨ Features - ### ✨ Features
- 新增 `基础表单` 示例页面 - 新增 `基础表单` 示例页面
- 新增 `分步表单` 示例页面 - 新增 `分步表单` 示例页面
@@ -62,7 +119,7 @@
- 持续更新更多实用示例,同时也演示`Naive UI`使用方法 - 持续更新更多实用示例,同时也演示`Naive UI`使用方法
# 1.0 (2021-07-12) # 1.0.0 (2021-07-12)
### 🐛 Bug Fixes ### 🐛 Bug Fixes
- 修复页面切换面包屑未及时更新 - 修复页面切换面包屑未及时更新

View File

@@ -1,6 +1,6 @@
## 简介 ## 简介
[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/) 的中后台解决方案,它使用了最新的前端技术栈,并提炼了典型的业务模型,页面,包括二次封装组件、动态菜单、权限校验、粒子化权限控制等功能,它可以帮助你快速搭建企业级中后台项目,该项目使用最新的前端技术栈,相信不管是从新技术使用还是其他方面,都能帮助到你。 [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 等前端前沿技术开发
@@ -12,13 +12,13 @@
## 在线预览 ## 在线预览
- [naive-ui-admin](https://jekip.github.io) - [naive-ui-admin](https://naive-ui-admin.vercel.app)
账号admin密码123456随意 账号admin密码123456随意
## 文档 ## 文档
[文档地址](https://jekip.github.io/docs/) [文档地址](https://naive-ui-admin-docs.vercel.app)
## 准备 ## 准备

View File

@@ -3,7 +3,7 @@
* @param env * @param env
*/ */
export const getConfigFileName = (env: Record<string, any>) => { export const getConfigFileName = (env: Record<string, any>) => {
return `__PRODUCTION__${ env.VITE_GLOB_APP_SHORT_NAME || '__APP' }__CONF__` return `__PRODUCTION__${env.VITE_GLOB_APP_SHORT_NAME || '__APP'}__CONF__`
.toUpperCase() .toUpperCase()
.replace(/\s/g, ''); .replace(/\s/g, '');
}; };

View File

@@ -18,19 +18,19 @@ function createConfig(
}: { configName: string; config: any; configFileName?: string } = { configName: '', config: {} } }: { configName: string; config: any; configFileName?: string } = { configName: '', config: {} }
) { ) {
try { try {
const windowConf = `window.${ configName }`; const windowConf = `window.${configName}`;
// Ensure that the variable will not be modified // Ensure that the variable will not be modified
const configStr = `${ windowConf }=${ JSON.stringify(config) }; const configStr = `${windowConf}=${JSON.stringify(config)};
Object.freeze(${ windowConf }); Object.freeze(${windowConf});
Object.defineProperty(window, "${ configName }", { Object.defineProperty(window, "${configName}", {
configurable: false, configurable: false,
writable: false, writable: false,
}); });
`.replace(/\s/g, ''); `.replace(/\s/g, '');
fs.mkdirp(getRootPath(OUTPUT_DIR)); fs.mkdirp(getRootPath(OUTPUT_DIR));
writeFileSync(getRootPath(`${ OUTPUT_DIR }/${ configFileName }`), configStr); writeFileSync(getRootPath(`${OUTPUT_DIR}/${configFileName}`), configStr);
console.log(chalk.cyan(`✨ [${ pkg.name }]`) + ` - configuration file is build successfully:`); console.log(chalk.cyan(`✨ [${pkg.name}]`) + ` - configuration file is build successfully:`);
console.log(chalk.gray(OUTPUT_DIR + '/' + chalk.green(configFileName)) + '\n'); console.log(chalk.gray(OUTPUT_DIR + '/' + chalk.green(configFileName)) + '\n');
} catch (error) { } catch (error) {
console.log(chalk.red('configuration file configuration file failed to package:\n' + error)); console.log(chalk.red('configuration file configuration file failed to package:\n' + error));

View File

@@ -14,7 +14,7 @@ export const runBuild = async () => {
await runBuildConfig(); await runBuildConfig();
} }
console.log(`${ chalk.cyan(`[${ pkg.name }]`) }` + ' - build successfully!'); console.log(`${chalk.cyan(`[${pkg.name}]`)}` + ' - build successfully!');
} catch (error) { } catch (error) {
console.log(chalk.red('vite build error:\n' + error)); console.log(chalk.red('vite build error:\n' + error));
process.exit(1); process.exit(1);

View File

@@ -12,10 +12,10 @@ import { GLOB_CONFIG_FILE_NAME } from '../../constant';
export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) { export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) {
const { VITE_GLOB_APP_TITLE, VITE_PUBLIC_PATH } = env; const { VITE_GLOB_APP_TITLE, VITE_PUBLIC_PATH } = env;
const path = VITE_PUBLIC_PATH.endsWith('/') ? VITE_PUBLIC_PATH : `${ VITE_PUBLIC_PATH }/`; const path = VITE_PUBLIC_PATH.endsWith('/') ? VITE_PUBLIC_PATH : `${VITE_PUBLIC_PATH}/`;
const getAppConfigSrc = () => { const getAppConfigSrc = () => {
return `${ path || '/' }${ GLOB_CONFIG_FILE_NAME }?v=${ pkg.version }-${ new Date().getTime() }`; return `${path || '/'}${GLOB_CONFIG_FILE_NAME}?v=${pkg.version}-${new Date().getTime()}`;
}; };
const htmlPlugin: Plugin[] = html({ const htmlPlugin: Plugin[] = html({
@@ -28,13 +28,13 @@ export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) {
// Embed the generated app.config.js file // Embed the generated app.config.js file
tags: isBuild tags: isBuild
? [ ? [
{ {
tag: 'script', tag: 'script',
attrs: { attrs: {
src: getAppConfigSrc(), src: getAppConfigSrc(),
},
}, },
}, ]
]
: [], : [],
}, },
}); });

View File

@@ -13,7 +13,7 @@ export function configStyleImportPlugin(isBuild: boolean) {
libraryName: 'ant-design-vue', libraryName: 'ant-design-vue',
esModule: true, esModule: true,
resolveStyle: (name) => { resolveStyle: (name) => {
return `ant-design-vue/es/${ name }/style/index`; return `ant-design-vue/es/${name}/style/index`;
}, },
}, },
], ],

View File

@@ -5,7 +5,7 @@ const tableList = (pageSize) => {
const result: any[] = []; const result: any[] = [];
doCustomTimes(pageSize, () => { doCustomTimes(pageSize, () => {
result.push({ result.push({
id: '@integer(10,100)', id: '@integer(10,999999)',
beginTime: '@datetime', beginTime: '@datetime',
endTime: '@datetime', endTime: '@datetime',
address: '@city()', address: '@city()',

View File

@@ -4,7 +4,7 @@ const menusList = [
{ {
path: '/dashboard', path: '/dashboard',
name: 'Dashboard', name: 'Dashboard',
component: 'Layout', component: 'LAYOUT',
redirect: '/dashboard/console', redirect: '/dashboard/console',
meta: { meta: {
icon: 'DashboardOutlined', icon: 'DashboardOutlined',
@@ -14,7 +14,7 @@ const menusList = [
{ {
path: 'console', path: 'console',
name: 'dashboard_console', name: 'dashboard_console',
component: 'DashboardConsole', component: '/dashboard/console/console',
meta: { meta: {
title: '主控台', title: '主控台',
}, },
@@ -22,7 +22,7 @@ const menusList = [
{ {
path: 'monitor', path: 'monitor',
name: 'dashboard_monitor', name: 'dashboard_monitor',
component: 'DashboardMonitor', component: '/dashboard/monitor/monitor',
meta: { meta: {
title: '监控页', title: '监控页',
}, },
@@ -30,7 +30,7 @@ const menusList = [
{ {
path: 'workplace', path: 'workplace',
name: 'dashboard_workplace', name: 'dashboard_workplace',
component: 'DashboardWorkplace', component: '/dashboard/workplace/workplace',
meta: { meta: {
hidden: true, hidden: true,
title: '工作台', title: '工作台',

View File

@@ -13,25 +13,25 @@ const adminInfo = {
desc: 'manager', desc: 'manager',
password: Random.string('upper', 4, 16), password: Random.string('upper', 4, 16),
token, token,
roles: [ permissions: [
{ {
roleName: '主控台', label: '主控台',
value: 'dashboard_console', value: 'dashboard_console',
}, },
{ {
roleName: '监控页', label: '监控页',
value: 'dashboard_monitor', value: 'dashboard_monitor',
}, },
{ {
roleName: '工作台', label: '工作台',
value: 'dashboard_workplace', value: 'dashboard_workplace',
}, },
{ {
roleName: '基础列表', label: '基础列表',
value: 'basic_list', value: 'basic_list',
}, },
{ {
roleName: '基础列表删除', label: '基础列表删除',
value: 'basic_list_delete', value: 'basic_list_delete',
}, },
], ],

View File

@@ -1,6 +1,6 @@
{ {
"name": "naive-ui-admin", "name": "naive-ui-admin",
"version": "1.5.0", "version": "1.5.4",
"author": { "author": {
"name": "Ahjung", "name": "Ahjung",
"email": "735878602@qq.com", "email": "735878602@qq.com",
@@ -30,7 +30,7 @@
"@vueuse/core": "^5.0.3", "@vueuse/core": "^5.0.3",
"axios": "^0.21.1", "axios": "^0.21.1",
"blueimp-md5": "^2.18.0", "blueimp-md5": "^2.18.0",
"dayjs": "^1.10.5", "date-fns": "^2.23.0",
"echarts": "^5.1.2", "echarts": "^5.1.2",
"element-resize-detector": "^1.2.3", "element-resize-detector": "^1.2.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@@ -38,7 +38,7 @@
"makeit-captcha": "^1.2.5", "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.11", "naive-ui": "^2.16.2",
"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",
@@ -87,7 +87,7 @@
"stylelint-scss": "^3.19.0", "stylelint-scss": "^3.19.0",
"tailwindcss": "^2.2.7", "tailwindcss": "^2.2.7",
"typescript": "^4.3.5", "typescript": "^4.3.5",
"vite": "2.3.6", "vite": "2.4.4",
"vite-plugin-compression": "^0.3.1", "vite-plugin-compression": "^0.3.1",
"vite-plugin-html": "^2.0.7", "vite-plugin-html": "^2.0.7",
"vite-plugin-mock": "^2.9.3", "vite-plugin-mock": "^2.9.3",

View File

@@ -15,6 +15,6 @@ module.exports = {
requirePragma: false, requirePragma: false,
proseWrap: 'never', proseWrap: 'never',
htmlWhitespaceSensitivity: 'strict', htmlWhitespaceSensitivity: 'strict',
endOfLine: 'lf', endOfLine: 'auto',
rangeStart: 0, rangeStart: 0,
}; };

View File

@@ -24,6 +24,7 @@
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';
import { lighten } from '@/utils/index';
export default defineComponent({ export default defineComponent({
name: 'App', name: 'App',
@@ -35,11 +36,20 @@
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(() => {
const appTheme = designStore.appTheme;
const lightenStr = lighten(designStore.appTheme, 6);
return { return {
common: { common: {
primaryColor: designStore.appTheme, primaryColor: appTheme,
primaryColorHover: '#57a3f3', primaryColorHover: lightenStr,
primaryColorPressed: lightenStr,
},
LoadingBar: {
colorLoading: appTheme,
}, },
}; };
}); });

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="52px" height="45px" viewBox="0 0 52 45" enable-background="new 0 0 52 45" xml:space="preserve"> <image id="image0" width="52" height="45" x="0" y="0"
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAAtCAMAAADWf7iKAAAABGdBTUEAALGPC/xhBQAAACBjSFJN
AAB6JQAAgIMAAPn/AACA6QAAdTAAAOpgAAA6mAAAF2+SX8VGAAAAdVBMVEX///8AAABkbnc9RVY7
QE9fY3GIiJZjbHk9QFOCi5QAAAA9QlZfZHMAAAA4QFEAAADt7/Lf5OTt7/KChoYAAADu8PTf3+Nw
c3MAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyNUkyOEn////w8vWhURXFAAAAI3RS
TlMAAE/2/uZJUP45AvfkBP4F9LrwPwP0vUkOAREsNjk0JwYHCLrjEiIAAAABYktHRACIBR1IAAAA
CXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5QgGAhE5kB5L+gAAAIZJREFUSMft1rsSgjAQhWEW
FTUYIopKlPui7/+IZqFhbMxmhm7//qvPiaKgAOIN+rbdJQCE9qO3cR2OhFTKMYgn5ZDmGcy0Q4aJ
0AhaoPdPnz8JEiRIkKCV0DkE5YQ0D12uBQ01B93uj9LSJdDPV1V71rRlMf0Iq7p+8Kw3aj4fAJYR
TCigL0lMJ5P4y7LRAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDIxLTA4LTA2VDAyOjE3OjU2KzAwOjAw
Kbo8/wAAACV0RVh0ZGF0ZTptb2RpZnkAMjAyMS0wOC0wNlQwMjoxNzo1NiswMDowMFjnhEMAAAAA
SUVORK5CYII=" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,6 +1,5 @@
<template> <template>
<n-loading-bar-provider> <n-loading-bar-provider>
<LoadingContent />
<n-dialog-provider> <n-dialog-provider>
<DialogContent /> <DialogContent />
<n-notification-provider> <n-notification-provider>
@@ -21,7 +20,6 @@
NMessageProvider, NMessageProvider,
NLoadingBarProvider, NLoadingBarProvider,
} from 'naive-ui'; } from 'naive-ui';
import { LoadingContent } from '@/components/LoadingContent';
import { MessageContent } from '@/components/MessageContent'; import { MessageContent } from '@/components/MessageContent';
import { DialogContent } from '@/components/DialogContent'; import { DialogContent } from '@/components/DialogContent';
@@ -32,7 +30,6 @@
NNotificationProvider, NNotificationProvider,
NMessageProvider, NMessageProvider,
NLoadingBarProvider, NLoadingBarProvider,
LoadingContent,
MessageContent, MessageContent,
DialogContent, DialogContent,
}, },

View File

@@ -0,0 +1,4 @@
export { default as BasicForm } from './src/BasicForm.vue';
export { useForm } from './src/hooks/useForm';
export * from './src/types/form';
export * from './src/types/index';

View File

@@ -0,0 +1,319 @@
<template>
<n-form v-bind="getBindValue" :model="formModel" ref="formElRef">
<n-grid v-bind="getGrid">
<n-gi v-bind="schema.giProps" v-for="schema in getSchema" :key="schema.field">
<n-form-item :label="schema.label" :path="schema.field">
<!--标签名右侧温馨提示-->
<template #label v-if="schema.labelMessage">
{{ schema.label }}
<n-tooltip trigger="hover" :style="schema.labelMessageStyle">
<template #trigger>
<n-icon size="18" class="cursor-pointer text-gray-400">
<QuestionCircleOutlined />
</n-icon>
</template>
{{ schema.labelMessage }}
</n-tooltip>
</template>
<!--判断插槽-->
<template v-if="schema.slot">
<slot
:name="schema.slot"
:model="formModel"
:field="schema.field"
:value="formModel[schema.field]"
></slot>
</template>
<!--NCheckbox-->
<template v-else-if="schema.component === 'NCheckbox'">
<n-checkbox-group v-model:value="formModel[schema.field]">
<n-space>
<n-checkbox
v-for="item in schema.componentProps.options"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</n-space>
</n-checkbox-group>
</template>
<!--NRadioGroup-->
<template v-else-if="schema.component === 'NRadioGroup'">
<n-radio-group v-model:value="formModel[schema.field]">
<n-space>
<n-radio
v-for="item in schema.componentProps.options"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</n-radio>
</n-space>
</n-radio-group>
</template>
<!--动态渲染表单组件-->
<component
v-else
v-bind="getComponentProps(schema)"
:is="schema.component"
v-model:value="formModel[schema.field]"
:class="{ isFull: schema.isFull != false && getProps.isFull }"
/>
<!--组件后面的内容-->
<template v-if="schema.suffix">
<slot
:name="schema.suffix"
:model="formModel"
:field="schema.field"
:value="formModel[schema.field]"
></slot>
</template>
</n-form-item>
</n-gi>
<!--提交 重置 展开 收起 按钮-->
<n-gi
:span="isInline ? '' : 24"
:suffix="isInline ? true : false"
#="{ overflow }"
v-if="getProps.showActionButtonGroup"
>
<n-space
align="center"
:justify="isInline ? 'end' : 'start'"
:style="{ 'margin-left': `${isInline ? 12 : getProps.labelWidth}px` }"
>
<n-button
v-if="getProps.showSubmitButton"
v-bind="getSubmitBtnOptions"
@click="handleSubmit"
:loading="loadingSub"
>{{ getProps.submitButtonText }}</n-button
>
<n-button
v-if="getProps.showResetButton"
v-bind="getResetBtnOptions"
@click="resetFields"
>{{ getProps.resetButtonText }}</n-button
>
<n-button
type="primary"
text
icon-placement="right"
v-if="overflow && isInline && getProps.showAdvancedButton"
@click="unfoldToggle"
>
<template #icon>
<n-icon size="14" class="unfold-icon" v-if="overflow">
<DownOutlined />
</n-icon>
<n-icon size="14" class="unfold-icon" v-else>
<UpOutlined />
</n-icon>
</template>
{{ overflow ? '展开' : '收起' }}
</n-button>
</n-space>
</n-gi>
</n-grid>
</n-form>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, computed, unref, onMounted, watch } from 'vue';
import { createPlaceholderMessage } from './helper';
import { useFormEvents } from './hooks/useFormEvents';
import { useFormValues } from './hooks/useFormValues';
import { basicProps } from './props';
import { DownOutlined, UpOutlined, QuestionCircleOutlined } from '@vicons/antd';
import type { Ref } from 'vue';
import type { GridProps } from 'naive-ui/lib/grid';
import type { FormSchema, FormProps, FormActionType } from './types/form';
import { isArray } from '@/utils/is/index';
import { deepMerge } from '@/utils';
export default defineComponent({
name: 'BasicUpload',
components: { DownOutlined, UpOutlined, QuestionCircleOutlined },
props: {
...basicProps,
},
emits: ['reset', 'submit', 'register'],
setup(props, { emit, attrs }) {
const defaultFormModel = ref<Recordable>({});
const formModel = reactive<Recordable>({});
const propsRef = ref<Partial<FormProps>>({});
const schemaRef = ref<Nullable<FormSchema[]>>(null);
const formElRef = ref<Nullable<FormActionType>>(null);
const gridCollapsed = ref(true);
const loadingSub = ref(false);
const isUpdateDefaultRef = ref(false);
const getSubmitBtnOptions = computed(() => {
return Object.assign(
{
size: props.size,
type: 'primary',
},
props.submitButtonOptions
);
});
const getResetBtnOptions = computed(() => {
return Object.assign(
{
size: props.size,
type: 'default',
},
props.resetButtonOptions
);
});
function getComponentProps(schema) {
const compProps = schema.componentProps ?? {};
const component = schema.component;
return {
clearable: true,
placeholder: createPlaceholderMessage(unref(component)),
...compProps,
};
}
const getProps = computed((): FormProps => {
const formProps = { ...props, ...unref(propsRef) } as FormProps;
const rulesObj: any = {
rules: {},
};
const schemas: any = formProps.schemas || [];
schemas.forEach((item) => {
if (item.rules && isArray(item.rules)) {
rulesObj.rules[item.field] = item.rules;
}
});
return { ...formProps, ...unref(rulesObj) };
});
const isInline = computed(() => {
const { layout } = unref(getProps);
return layout === 'inline';
});
const getGrid = computed((): GridProps => {
const { gridProps } = unref(getProps);
return {
...gridProps,
collapsed: isInline.value ? gridCollapsed.value : false,
responsive: 'screen',
};
});
const getBindValue = computed(
() => ({ ...attrs, ...props, ...unref(getProps) } as Recordable)
);
const getSchema = computed((): FormSchema[] => {
const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any);
for (const schema of schemas) {
const { defaultValue } = schema;
// handle date type
// dateItemType.includes(component as string)
if (defaultValue) {
schema.defaultValue = defaultValue;
}
}
return schemas as FormSchema[];
});
const { handleFormValues, initDefault } = useFormValues({
getProps,
defaultFormModel,
getSchema,
formModel,
});
const { handleSubmit, validate, resetFields, getFieldsValue, clearValidate, setFieldsValue } =
useFormEvents({
emit,
getProps,
formModel,
getSchema,
formElRef: formElRef as Ref<FormActionType>,
defaultFormModel,
loadingSub,
handleFormValues,
});
function unfoldToggle() {
gridCollapsed.value = !gridCollapsed.value;
}
async function setProps(formProps: Partial<FormProps>): Promise<void> {
propsRef.value = deepMerge(unref(propsRef) || {}, formProps);
}
const formActionType: Partial<FormActionType> = {
getFieldsValue,
setFieldsValue,
resetFields,
validate,
clearValidate,
setProps,
submit: handleSubmit,
};
watch(
() => getSchema.value,
(schema) => {
if (unref(isUpdateDefaultRef)) {
return;
}
if (schema?.length) {
initDefault();
isUpdateDefaultRef.value = true;
}
}
);
onMounted(() => {
initDefault();
emit('register', formActionType);
});
return {
formElRef,
formModel,
getGrid,
getProps,
getBindValue,
getSchema,
getSubmitBtnOptions,
getResetBtnOptions,
handleSubmit,
resetFields,
loadingSub,
isInline,
getComponentProps,
unfoldToggle,
};
},
});
</script>
<style lang="less" scoped>
.isFull {
width: 100%;
justify-content: flex-start;
}
.unfold-icon {
display: flex;
align-items: center;
height: 100%;
margin-left: -3px;
}
</style>

View File

@@ -0,0 +1,42 @@
import { ComponentType } from '/types/index';
/**
* @description: 生成placeholder
*/
export function createPlaceholderMessage(component: ComponentType) {
if (component === 'NInput') return '请输入';
if (
['NPicker', 'NSelect', 'NCheckbox', 'NRadio', 'NSwitch', 'NDatePicker', 'NTimePicker'].includes(
component
)
)
return '请选择';
return '';
}
const DATE_TYPE = ['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'];
function genType() {
return [...DATE_TYPE, 'RangePicker'];
}
/**
* 时间字段
*/
export const dateItemType = genType();
export function defaultType(component) {
if (component === 'NInput') return '';
if (component === 'NInputNumber') return null;
return [
'NPicker',
'NSelect',
'NCheckbox',
'NRadio',
'NSwitch',
'NDatePicker',
'NTimePicker',
].includes(component)
? ''
: undefined;
}

View File

@@ -0,0 +1,87 @@
import type { FormProps, FormActionType, UseFormReturnType } from '../types/form';
// @ts-ignore
import type { DynamicProps } from '/#/utils';
import { ref, onUnmounted, unref, nextTick, watch } from 'vue';
import { isProdMode } from '@/utils/env';
import { getDynamicProps } from '@/utils';
type Props = Partial<DynamicProps<FormProps>>;
export function useForm(props?: Props): UseFormReturnType {
const formRef = ref<Nullable<FormActionType>>(null);
const loadedRef = ref<Nullable<boolean>>(false);
async function getForm() {
const form = unref(formRef);
if (!form) {
console.error(
'The form instance has not been obtained, please make sure that the form has been rendered when performing the form operation!'
);
}
await nextTick();
return form as FormActionType;
}
function register(instance: FormActionType) {
isProdMode() &&
onUnmounted(() => {
formRef.value = null;
loadedRef.value = null;
});
if (unref(loadedRef) && isProdMode() && instance === unref(formRef)) return;
formRef.value = instance;
loadedRef.value = true;
watch(
() => props,
() => {
props && instance.setProps(getDynamicProps(props));
},
{
immediate: true,
deep: true,
}
);
}
const methods: FormActionType = {
setProps: async (formProps: Partial<FormProps>) => {
const form = await getForm();
await form.setProps(formProps);
},
resetFields: async () => {
getForm().then(async (form) => {
await form.resetFields();
});
},
clearValidate: async (name?: string | string[]) => {
const form = await getForm();
await form.clearValidate(name);
},
getFieldsValue: <T>() => {
return unref(formRef)?.getFieldsValue() as T;
},
setFieldsValue: async <T>(values: T) => {
const form = await getForm();
await form.setFieldsValue<T>(values);
},
submit: async (): Promise<any> => {
const form = await getForm();
return form.submit();
},
validate: async (nameList?: any[]): Promise<Recordable> => {
const form = await getForm();
return form.validate(nameList);
},
};
return [register, methods];
}

View File

@@ -0,0 +1,11 @@
import { provide, inject } from 'vue';
const key = Symbol('formElRef');
export function createFormContext(instance) {
provide(key, instance);
}
export function useFormContext() {
return inject(key);
}

View File

@@ -0,0 +1,107 @@
import type { ComputedRef, Ref } from 'vue';
import type { FormProps, FormSchema, FormActionType } from '../types/form';
import { unref, toRaw } from 'vue';
import { isFunction } from '@/utils/is';
declare type EmitType = (event: string, ...args: any[]) => void;
interface UseFormActionContext {
emit: EmitType;
getProps: ComputedRef<FormProps>;
getSchema: ComputedRef<FormSchema[]>;
formModel: Recordable;
formElRef: Ref<FormActionType>;
defaultFormModel: Recordable;
loadingSub: Ref<boolean>;
handleFormValues: Function;
}
export function useFormEvents({
emit,
getProps,
formModel,
getSchema,
formElRef,
defaultFormModel,
loadingSub,
handleFormValues,
}: UseFormActionContext) {
// 验证
async function validate() {
return unref(formElRef)?.validate();
}
// 提交
async function handleSubmit(e?: Event): Promise<void> {
e && e.preventDefault();
loadingSub.value = true;
const { submitFunc } = unref(getProps);
if (submitFunc && isFunction(submitFunc)) {
await submitFunc();
return;
}
const formEl = unref(formElRef);
if (!formEl) return;
try {
await validate();
loadingSub.value = false;
emit('submit', formModel);
return true;
} catch (error) {
loadingSub.value = false;
return false;
}
}
//清空校验
async function clearValidate() {
// @ts-ignore
await unref(formElRef)?.restoreValidation();
}
//重置
async function resetFields(): Promise<void> {
const { resetFunc, submitOnReset } = unref(getProps);
resetFunc && isFunction(resetFunc) && (await resetFunc());
const formEl = unref(formElRef);
if (!formEl) return;
Object.keys(formModel).forEach((key) => {
formModel[key] = unref(defaultFormModel)[key] || null;
});
await clearValidate();
const fromValues = handleFormValues(toRaw(unref(formModel)));
emit('reset', fromValues);
submitOnReset && (await handleSubmit());
}
//获取表单值
function getFieldsValue(): Recordable {
const formEl = unref(formElRef);
if (!formEl) return {};
return handleFormValues(toRaw(unref(formModel)));
}
//设置表单字段值
async function setFieldsValue(values: Recordable): Promise<void> {
const fields = unref(getSchema)
.map((item) => item.field)
.filter(Boolean);
Object.keys(values).forEach((key) => {
const value = values[key];
if (fields.includes(key)) {
formModel[key] = value;
}
});
}
return {
handleSubmit,
validate,
resetFields,
getFieldsValue,
clearValidate,
setFieldsValue,
};
}

View File

@@ -0,0 +1,54 @@
import { isArray, isFunction, isObject, isString, isNullOrUnDef } from '@/utils/is';
import { unref } from 'vue';
import type { Ref, ComputedRef } from 'vue';
import type { FormSchema } from '../types/form';
import { set } from 'lodash-es';
interface UseFormValuesContext {
defaultFormModel: Ref<any>;
getSchema: ComputedRef<FormSchema[]>;
formModel: Recordable;
}
export function useFormValues({ defaultFormModel, getSchema, formModel }: UseFormValuesContext) {
// 加工 form values
function handleFormValues(values: Recordable) {
if (!isObject(values)) {
return {};
}
const res: Recordable = {};
for (const item of Object.entries(values)) {
let [, value] = item;
const [key] = item;
if (
!key ||
(isArray(value) && value.length === 0) ||
isFunction(value) ||
isNullOrUnDef(value)
) {
continue;
}
// 删除空格
if (isString(value)) {
value = value.trim();
}
set(res, key, value);
}
return res;
}
//初始化默认值
function initDefault() {
const schemas = unref(getSchema);
const obj: Recordable = {};
schemas.forEach((item) => {
const { defaultValue } = item;
if (!isNullOrUnDef(defaultValue)) {
obj[item.field] = defaultValue;
formModel[item.field] = defaultValue;
}
});
defaultFormModel.value = obj;
}
return { handleFormValues, initDefault };
}

View File

@@ -0,0 +1,82 @@
import type { CSSProperties, PropType } from 'vue';
import { FormSchema } from './types/form';
import type { GridProps, GridItemProps } from 'naive-ui/lib/grid';
import type { ButtonProps } from 'naive-ui/lib/button';
import { propTypes } from '@/utils/propTypes';
export const basicProps = {
// 标签宽度 固定宽度
labelWidth: {
type: [Number, String] as PropType<number | string>,
default: 80,
},
// 表单配置规则
schemas: {
type: [Array] as PropType<FormSchema[]>,
default: () => [],
},
//布局方式
layout: {
type: String,
default: 'inline',
},
//是否展示为行内表单
inline: {
type: Boolean,
default: false,
},
//大小
size: {
type: String,
default: 'medium',
},
//标签位置
labelPlacement: {
type: String,
default: 'left',
},
//组件是否width 100%
isFull: {
type: Boolean,
default: true,
},
//是否显示操作按钮(查询/重置)
showActionButtonGroup: propTypes.bool.def(true),
// 显示重置按钮
showResetButton: propTypes.bool.def(true),
//重置按钮配置
resetButtonOptions: Object as PropType<Partial<ButtonProps>>,
// 显示确认按钮
showSubmitButton: propTypes.bool.def(true),
// 确认按钮配置
submitButtonOptions: Object as PropType<Partial<ButtonProps>>,
//展开收起按钮
showAdvancedButton: propTypes.bool.def(true),
// 确认按钮文字
submitButtonText: {
type: String,
default: '查询',
},
//重置按钮文字
resetButtonText: {
type: String,
default: '重置',
},
//grid 配置
gridProps: Object as PropType<GridProps>,
//gi配置
giProps: Object as PropType<GridItemProps>,
//grid 样式
baseGridStyle: {
type: Object as PropType<CSSProperties>,
},
//是否折叠
collapsed: {
type: Boolean,
default: false,
},
//默认展示的行数
collapsedRows: {
type: Number,
default: 1,
},
};

View File

@@ -0,0 +1,58 @@
import { ComponentType } from './index';
import type { CSSProperties } from 'vue';
import type { GridProps, GridItemProps } from 'naive-ui/lib/grid';
import type { ButtonProps } from 'naive-ui/lib/button';
export interface FormSchema {
field: string;
label: string;
labelMessage?: string;
labelMessageStyle?: object | string;
defaultValue?: any;
component?: ComponentType;
componentProps?: object;
slot?: string;
rules?: object | object[];
giProps?: GridItemProps;
isFull?: boolean;
suffix?: string;
}
export interface FormProps {
model?: Recordable;
labelWidth?: number | string;
schemas?: FormSchema[];
inline: boolean;
layout?: string;
size: string;
labelPlacement: string;
isFull: boolean;
showActionButtonGroup?: boolean;
showResetButton?: boolean;
resetButtonOptions?: Partial<ButtonProps>;
showSubmitButton?: boolean;
showAdvancedButton?: boolean;
submitButtonOptions?: Partial<ButtonProps>;
submitButtonText?: string;
resetButtonText?: string;
gridProps?: GridProps;
giProps?: GridItemProps;
resetFunc?: () => Promise<void>;
submitFunc?: () => Promise<void>;
submitOnReset?: boolean;
baseGridStyle?: CSSProperties;
}
export interface FormActionType {
submit: () => Promise<any>;
setProps: (formProps: Partial<FormProps>) => Promise<void>;
setFieldsValue: <T>(values: T) => Promise<void>;
clearValidate: (name?: string | string[]) => Promise<void>;
getFieldsValue: () => Recordable;
resetFields: () => Promise<void>;
validate: (nameList?: any[]) => Promise<any>;
}
export type RegisterFn = (formInstance: FormActionType) => void;
export type UseFormReturnType = [RegisterFn, FormActionType];

View File

@@ -0,0 +1,28 @@
export type ComponentType =
| 'NInput'
| 'NInputGroup'
| 'NInputPassword'
| 'NInputSearch'
| 'NInputTextArea'
| 'NInputNumber'
| 'NInputCountDown'
| 'NSelect'
| 'NTreeSelect'
| 'NRadioButtonGroup'
| 'NRadioGroup'
| 'NCheckbox'
| 'NCheckboxGroup'
| 'NAutoComplete'
| 'NCascader'
| 'NDatePicker'
| 'NMonthPicker'
| 'NRangePicker'
| 'NWeekPicker'
| 'NTimePicker'
| 'NSwitch'
| 'NStrengthMeter'
| 'NUpload'
| 'NIconPicker'
| 'NRender'
| 'NSlider'
| 'NRate';

View File

@@ -21,6 +21,7 @@
:battery="battery" :battery="battery"
:battery-status="batteryStatus" :battery-status="batteryStatus"
:calc-discharging-time="calcDischargingTime" :calc-discharging-time="calcDischargingTime"
:calc-charging-time="calcChargingTime"
/> />
<div class="local-time"> <div class="local-time">
@@ -48,6 +49,7 @@
type="password" type="password"
autofocus autofocus
v-model:value="loginParams.password" v-model:value="loginParams.password"
@keyup.enter="onLogin"
placeholder="请输入登录密码" placeholder="请输入登录密码"
> >
<template #suffix> <template #suffix>
@@ -114,7 +116,7 @@
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
const { battery, batteryStatus, calcDischargingTime } = useBattery(); const { battery, batteryStatus, calcDischargingTime, calcChargingTime } = useBattery();
const userInfo: object = userStore.getUserInfo || {}; const userInfo: object = userStore.getUserInfo || {};
const username = userInfo['username'] || ''; const username = userInfo['username'] || '';
const state = reactive({ const state = reactive({
@@ -176,6 +178,7 @@
battery, battery,
batteryStatus, batteryStatus,
calcDischargingTime, calcDischargingTime,
calcChargingTime,
onLockLogin, onLockLogin,
onLogin, onLogin,
goLogin, goLogin,

View File

@@ -13,7 +13,7 @@
剩余可使用时间{{ calcDischargingTime }} 剩余可使用时间{{ calcDischargingTime }}
</div> </div>
<span v-show="Number.isFinite(battery.chargingTime) && battery.chargingTime != 0"> <span v-show="Number.isFinite(battery.chargingTime) && battery.chargingTime != 0">
距离电池充满需要{{ calcDischargingTime }} 距离电池充满需要{{ calcChargingTime }}
</span> </span>
</div> </div>
</div> </div>
@@ -36,6 +36,10 @@
type: String, type: String,
default: '', default: '',
}, },
calcChargingTime: {
type: String,
default: '',
},
batteryStatus: { batteryStatus: {
// 电池状态 // 电池状态
type: String, type: String,
@@ -51,12 +55,12 @@
bottom: 20vh; bottom: 20vh;
left: 50vw; left: 50vw;
width: 300px; width: 300px;
height: 400px; height: 500px;
transform: translateX(-50%); transform: translateX(-50%);
.number { .number {
position: absolute; position: absolute;
top: 27%; top: 20%;
z-index: 10; z-index: 10;
width: 300px; width: 300px;
font-size: 32px; font-size: 32px;

View File

@@ -0,0 +1,3 @@
export { default as basicModal } from './src/basicModal.vue';
export { useModal } from './src/hooks/useModal';
export * from './src/type';

View File

@@ -0,0 +1,127 @@
<template>
<n-modal id="basic-modal" v-bind="getBindValue" v-model:show="isModal" @close="onCloseModal">
<template #header>
<div class="w-full cursor-move" id="basic-modal-bar">{{ getBindValue.title }}</div>
</template>
<template #default>
<slot name="default"></slot>
</template>
<template #action v-if="!$slots.action">
<n-space>
<n-button @click="closeModal">取消</n-button>
<n-button type="primary" :loading="subLoading" @click="handleSubmit">{{
subBtuText
}}</n-button>
</n-space>
</template>
<template v-else #action>
<slot name="action"></slot>
</template>
</n-modal>
</template>
<script lang="ts">
import {
defineComponent,
getCurrentInstance,
ref,
nextTick,
unref,
toRefs,
reactive,
computed,
} from 'vue';
import { basicProps } from './props';
import startDrag from '@/utils/Drag';
import { deepMerge } from '@/utils';
import { FormProps } from '@/components/Form';
export default defineComponent({
name: 'BasicModal',
components: {},
props: {
...basicProps,
},
emits: ['on-close', 'on-ok', 'register'],
setup(props, { emit, attrs }) {
const propsRef = ref<Partial>({});
const state = reactive({
isModal: false,
subLoading: false,
});
const getProps = computed((): FormProps => {
const modalProps = { ...props, ...unref(propsRef) };
return { ...modalProps };
});
async function setProps(modalProps: Partial): Promise<void> {
propsRef.value = deepMerge(unref(propsRef) || {}, modalProps);
}
const getBindValue = computed(() => {
return {
...attrs,
...unref(getProps),
};
});
function setSubLoading(status: boolean) {
state.subLoading = status;
}
function openModal() {
state.isModal = true;
nextTick(() => {
const oBox = document.getElementById('basic-modal');
const oBar = document.getElementById('basic-modal-bar');
startDrag(oBar, oBox);
});
}
function closeModal() {
state.isModal = false;
state.subLoading = false;
emit('on-close');
}
function onCloseModal() {
state.isModal = false;
emit('on-close');
}
function handleSubmit() {
state.subLoading = true;
emit('on-ok');
}
const modalMethods: ModalMethods = {
setProps,
openModal,
closeModal,
setSubLoading,
};
const instance = getCurrentInstance();
if (instance) {
emit('register', modalMethods);
}
return {
...toRefs(state),
getBindValue,
openModal,
closeModal,
onCloseModal,
handleSubmit,
setProps,
};
},
});
</script>
<style lang="less">
.cursor-move {
cursor: move;
}
</style>

View File

@@ -0,0 +1,58 @@
import { ref, onUnmounted, unref, getCurrentInstance, watch } from 'vue';
import { isProdMode } from '@/utils/env';
import { ReturnMethods } from '../type';
import { getDynamicProps } from '@/utils';
export function useModal(props): (((modalMethod: ReturnMethods) => any) | ReturnMethods)[] {
const modal = ref<Nullable<ReturnMethods>>(null);
const loaded = ref<Nullable<boolean>>(false);
function register(modalMethod: ReturnMethods) {
if (!getCurrentInstance()) {
throw new Error('useModal() can only be used inside setup() or functional components!');
}
isProdMode() &&
onUnmounted(() => {
modal.value = null;
loaded.value = false;
});
if (unref(loaded) && isProdMode() && modalMethod === unref(modal)) return;
modal.value = modalMethod;
watch(
() => props,
() => {
// @ts-ignore
const { setProps } = modal.value;
props && setProps(getDynamicProps(props));
},
{
immediate: true,
deep: true,
}
);
}
const getInstance = () => {
const instance = unref(modal);
if (!instance) {
console.error('useModal instance is undefined!');
}
return instance;
};
const methods: ReturnMethods = {
setProps: (props): void => {
getInstance()?.setProps(props);
},
openModal: () => {
getInstance()?.openModal();
},
closeModal: () => {
getInstance()?.closeModal();
},
setSubLoading: (status) => {
getInstance()?.setSubLoading(status);
},
};
return [register, methods];
}

View File

@@ -0,0 +1,30 @@
import { NModal } from 'naive-ui';
export const basicProps = {
...NModal.props,
// 确认按钮文字
subBtuText: {
type: String,
default: '确认',
},
showIcon: {
type: Boolean,
default: false,
},
width: {
type: Number,
default: 446,
},
title: {
type: String,
default: '',
},
maskClosable: {
type: Boolean,
default: false,
},
preset: {
type: String,
default: 'dialog',
},
};

View File

@@ -0,0 +1,9 @@
/**
* @description: 弹窗对外暴露的方法
*/
export interface ReturnMethods {
setProps: (props) => void;
openModal: () => void;
closeModal: () => void;
setSubLoading: (status) => void;
}

View File

@@ -73,7 +73,6 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { NDataTable } from 'naive-ui';
import { import {
ref, ref,
defineComponent, defineComponent,
@@ -129,7 +128,6 @@
QuestionCircleOutlined, QuestionCircleOutlined,
}, },
props: { props: {
...NDataTable.props, // 这里继承原 UI 组件的 props
...basicProps, ...basicProps,
}, },
emits: [ emits: [
@@ -235,9 +233,6 @@
getCacheColumns, getCacheColumns,
setCacheColumnsField, setCacheColumnsField,
emit, emit,
getSize: () => {
return unref(getBindValues).size;
},
}; };
const getCanResize = computed(() => { const getCanResize = computed(() => {
@@ -290,7 +285,6 @@
densitySelect, densitySelect,
updatePage, updatePage,
updatePageSize, updatePageSize,
updateCheckedRowKeys,
pagination, pagination,
tableAction, tableAction,
}; };

View File

@@ -49,7 +49,7 @@
import { set, omit } from 'lodash-es'; import { set, omit } from 'lodash-es';
import { EventEnum } from '@/components/Table/src/componentMap'; import { EventEnum } from '@/components/Table/src/componentMap';
import dayjs from 'dayjs'; import { milliseconds } from 'date-fns';
export default defineComponent({ export default defineComponent({
name: 'EditableCell', name: 'EditableCell',
@@ -108,10 +108,11 @@
let value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val; let value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
if (component === 'NDatePicker') { if (isString(value) && component === 'NDatePicker') {
value = dayjs(value).valueOf(); value = milliseconds(value as Duration);
} else if (isArray(value) && component === 'NDatePicker') {
value = value.map((item) => milliseconds(item));
} }
const onEvent: any = editComponent ? EventEnum[editComponent] : undefined; const onEvent: any = editComponent ? EventEnum[editComponent] : undefined;
return { return {
@@ -196,12 +197,12 @@
} }
//TODO 这里组件参数格式和dayjs格式不一致 //TODO 这里组件参数格式和dayjs格式不一致
if (component === 'NDatePicker') { // if (component === 'NDatePicker') {
let format = (props.column.editComponentProps?.format) // let format = (props.column.editComponentProps?.format)
.replace(/yyyy/g, 'YYYY') // .replace(/yyyy/g, 'YYYY')
.replace(/dd/g, 'DD'); // .replace(/dd/g, 'DD');
currentValueRef.value = dayjs(currentValueRef.value).format(format); // currentValueRef.value = dayjs(currentValueRef.value).format(format);
} // }
const onChange = props.column?.editComponentProps?.onChange; const onChange = props.column?.editComponentProps?.onChange;
if (onChange && isFunction(onChange)) onChange(...arguments); if (onChange && isFunction(onChange)) onChange(...arguments);

View File

@@ -81,12 +81,14 @@
<script lang="ts"> <script lang="ts">
import { ref, defineComponent, reactive, unref, toRaw, computed, toRefs, watchEffect } from 'vue'; import { ref, defineComponent, reactive, unref, toRaw, computed, toRefs, watchEffect } from 'vue';
import { useTableContext } from '../../hooks/useTableContext'; import { useTableContext } from '../../hooks/useTableContext';
import { cloneDeep } from 'lodash-es';
import { import {
SettingOutlined, SettingOutlined,
DragOutlined, DragOutlined,
VerticalRightOutlined, VerticalRightOutlined,
VerticalLeftOutlined, VerticalLeftOutlined,
} from '@vicons/antd'; } from '@vicons/antd';
// @ts-ignore
import Draggable from 'vuedraggable/src/vuedraggable'; import Draggable from 'vuedraggable/src/vuedraggable';
import { useDesignSetting } from '@/hooks/setting/useDesignSetting'; import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
@@ -107,7 +109,7 @@
}, },
setup() { setup() {
const { getDarkTheme } = useDesignSetting(); const { getDarkTheme } = useDesignSetting();
const table = useTableContext(); const table: any = useTableContext();
const columnsList = ref<Options[]>([]); const columnsList = ref<Options[]>([]);
const cacheColumnsList = ref<Options[]>([]); const cacheColumnsList = ref<Options[]>([]);
@@ -135,8 +137,11 @@
const checkList: any = columns.map((item) => item.key); const checkList: any = columns.map((item) => item.key);
state.checkList = checkList; state.checkList = checkList;
state.defaultCheckList = checkList; state.defaultCheckList = checkList;
columnsList.value = columns; const newColumns = columns.filter((item) => item.key != 'action' && item.title != '操作');
cacheColumnsList.value = columns; if (!columnsList.value.length) {
columnsList.value = cloneDeep(newColumns);
cacheColumnsList.value = cloneDeep(newColumns);
}
} }
//切换 //切换
@@ -154,11 +159,11 @@
//获取 //获取
function getColumns() { function getColumns() {
let newRet = []; let newRet: any[] = [];
table.getColumns().forEach((item) => { table.getColumns().forEach((item) => {
newRet.push({ ...item }); newRet.push({ ...item });
}); });
return newRet.filter((item) => item.key != 'action' && item.title != '操作'); return newRet;
} }
//重置 //重置

View File

@@ -52,7 +52,9 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
return hasPermission(column.auth) && isIfShow(column); return hasPermission(column.auth) && isIfShow(column);
}) })
.map((column) => { .map((column) => {
const { edit, editRow } = column; //默认 ellipsis 为true
column.ellipsis = typeof column.ellipsis === 'undefined' ? { tooltip: true } : false;
const { edit } = column;
if (edit) { if (edit) {
column.render = renderEditCell(column); column.render = renderEditCell(column);
if (edit) { if (edit) {
@@ -92,7 +94,7 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
const { actionColumn } = unref(propsRef); const { actionColumn } = unref(propsRef);
if (!actionColumn) return; if (!actionColumn) return;
// @ts-ignore // @ts-ignore
columns.push({ !columns.find((col) => col.key === 'action') && columns.push({
...actionColumn, ...actionColumn,
}); });
} }
@@ -127,7 +129,7 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
} }
//获取 //获取
function getColumns() { function getColumns(): BasicColumn[] {
const columns = toRaw(unref(getColumnsRef)); const columns = toRaw(unref(getColumnsRef));
return columns.map((item) => { return columns.map((item) => {
return { ...item, title: item.title, key: item.key, fixed: item.fixed || undefined }; return { ...item, title: item.title, key: item.key, fixed: item.fixed || undefined };
@@ -140,12 +142,12 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
} }
//更新原始数据单个字段 //更新原始数据单个字段
function setCacheColumnsField(dataIndex: string | undefined, value: Partial<BasicColumn>) { function setCacheColumnsField(key: string | undefined, value: Partial<BasicColumn>) {
if (!dataIndex || !value) { if (!key || !value) {
return; return;
} }
cacheColumns.forEach((item) => { cacheColumns.forEach((item) => {
if (item.key === dataIndex) { if (item.key === key) {
Object.assign(item, value); Object.assign(item, value);
return; return;
} }

View File

@@ -47,7 +47,6 @@ export function useDataSource(
try { try {
setLoading(true); setLoading(true);
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;
@@ -74,10 +73,9 @@ export function useDataSource(
// 如果数据异常,需获取正确的页码再次执行 // 如果数据异常,需获取正确的页码再次执行
if (resultTotal) { if (resultTotal) {
const currentTotalPage = Math.ceil(resultTotal / pageSize); if (page > resultTotal) {
if (page > currentTotalPage) {
setPagination({ setPagination({
[pageField]: currentTotalPage, [pageField]: resultTotal,
}); });
fetch(opt); fetch(opt);
} }

View File

@@ -1,8 +1,9 @@
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { propTypes } from '@/utils/propTypes'; import { propTypes } from '@/utils/propTypes';
import { BasicColumn } from './types/table'; import { BasicColumn } from './types/table';
import { NDataTable } from 'naive-ui';
export const basicProps = { export const basicProps = {
...NDataTable.props, // 这里继承原 UI 组件的 props
title: { title: {
type: String, type: String,
default: null, default: null,

View File

@@ -11,7 +11,7 @@ export interface BasicColumn extends TableBaseColumn {
editValueMap?: (value: any) => string; editValueMap?: (value: any) => string;
onEditRow?: () => void; onEditRow?: () => void;
// 权限编码控制是否显示 // 权限编码控制是否显示
auth?: RoleEnum | RoleEnum[] | string | string[]; auth?: string[];
// 业务控制是否显示 // 业务控制是否显示
ifShow?: boolean | ((column: BasicColumn) => boolean); ifShow?: boolean | ((column: BasicColumn) => boolean);
} }

View File

@@ -1,6 +1,6 @@
// @ts-ignore // @ts-ignore
import { NButton } from 'naive-ui'; import { NButton } from 'naive-ui';
import { RoleEnum } from '@/enums/roleEnum'; import { PermissionsEnum } from '@/enums/permissionsEnum';
// @ts-ignore // @ts-ignore
export interface ActionItem extends NButton.props { export interface ActionItem extends NButton.props {
onClick?: Fn; onClick?: Fn;
@@ -11,7 +11,7 @@ export interface ActionItem extends NButton.props {
disabled?: boolean; disabled?: boolean;
divider?: boolean; divider?: boolean;
// 权限编码控制是否显示 // 权限编码控制是否显示
auth?: RoleEnum | RoleEnum[] | string | string[]; auth?: PermissionsEnum | PermissionsEnum[] | string | string[];
// 业务控制是否显示 // 业务控制是否显示
ifShow?: boolean | ((action: ActionItem) => boolean); ifShow?: boolean | ((action: ActionItem) => boolean);
} }

View File

@@ -70,7 +70,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, toRefs, reactive, computed } from 'vue'; import { defineComponent, toRefs, reactive, computed } from 'vue';
import { EyeOutlined, DeleteOutlined, PlusOutlined } from '@vicons/antd'; import { EyeOutlined, DeleteOutlined, PlusOutlined } from '@vicons/antd';
import { NUpload } from 'naive-ui';
import { basicProps } from './props'; import { basicProps } from './props';
import { useMessage, useDialog } from 'naive-ui'; import { useMessage, useDialog } from 'naive-ui';
import { ResultEnum } from '@/enums/httpEnum'; import { ResultEnum } from '@/enums/httpEnum';
@@ -85,7 +84,6 @@
components: { EyeOutlined, DeleteOutlined, PlusOutlined }, components: { EyeOutlined, DeleteOutlined, PlusOutlined },
props: { props: {
...NUpload.props, // 这里继承原 UI 组件的 props
...basicProps, ...basicProps,
}, },
emits: ['uploadChange', 'delete'], emits: ['uploadChange', 'delete'],
@@ -103,8 +101,8 @@
const state = reactive({ const state = reactive({
showModal: false, showModal: false,
previewUrl: '', previewUrl: '',
originalImgList: [], originalImgList: [] as string[],
imgList: [], imgList: [] as string[],
}); });
//赋值默认图片显示 //赋值默认图片显示
@@ -178,7 +176,7 @@
const result = res[infoField]; const result = res[infoField];
//成功 //成功
if (code === ResultEnum.SUCCESS) { if (code === ResultEnum.SUCCESS) {
let imgUrl = getImgUrl(result.photo); let imgUrl: string = getImgUrl(result.photo);
state.imgList.push(imgUrl); state.imgList.push(imgUrl);
state.originalImgList.push(result.photo); state.originalImgList.push(result.photo);
emit('uploadChange', state.originalImgList); emit('uploadChange', state.originalImgList);
@@ -222,6 +220,7 @@
&:hover { &:hover {
background: 0 0; background: 0 0;
.upload-card-item-info::before { .upload-card-item-info::before {
opacity: 1; opacity: 1;
} }

View File

@@ -33,6 +33,14 @@ export const useBattery = () => {
return `${~~hour}小时${~~minute}分钟`; return `${~~hour}小时${~~minute}分钟`;
}); });
// 计算电池充满剩余时间
const calcChargingTime = computed(() => {
console.log(state.battery);
const hour = state.battery.chargingTime / 3600;
const minute = (state.battery.chargingTime / 60) % 60;
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) {
@@ -80,5 +88,6 @@ export const useBattery = () => {
...toRefs(state), ...toRefs(state),
batteryStatus, batteryStatus,
calcDischargingTime, calcDischargingTime,
calcChargingTime,
}; };
}; };

View File

@@ -7,8 +7,8 @@ export function usePermission() {
* 检查权限 * 检查权限
* @param accesses * @param accesses
*/ */
function _someRoles(accesses: string[]) { function _somePermissions(accesses: string[]) {
return userStore.getRoles.some((item) => { return userStore.getPermissions.some((item) => {
const { value }: any = item; const { value }: any = item;
return accesses.includes(value); return accesses.includes(value);
}); });
@@ -20,7 +20,7 @@ export function usePermission() {
* */ * */
function hasPermission(accesses: string[]): boolean { function hasPermission(accesses: string[]): boolean {
if (!accesses || !accesses.length) return true; if (!accesses || !accesses.length) return true;
return _someRoles(accesses); return _somePermissions(accesses);
} }
/** /**
@@ -28,9 +28,9 @@ export function usePermission() {
* @param accesses * @param accesses
*/ */
function hasEveryPermission(accesses: string[]): boolean { function hasEveryPermission(accesses: string[]): boolean {
const rolesList = userStore.getRoles; const permissionsList = userStore.getPermissions;
if (Array.isArray(accesses)) { if (Array.isArray(accesses)) {
return accesses.every((access) => !!rolesList[access]); return accesses.every((access) => !!permissionsList[access]);
} }
throw new Error(`[hasEveryPermission]: ${accesses} should be a array !`); throw new Error(`[hasEveryPermission]: ${accesses} should be a array !`);
} }
@@ -41,9 +41,9 @@ export function usePermission() {
* @param accessMap * @param accessMap
*/ */
function hasSomePermission(accesses: string[]): boolean { function hasSomePermission(accesses: string[]): boolean {
const rolesList = userStore.getRoles; const permissionsList = userStore.getPermissions;
if (Array.isArray(accesses)) { if (Array.isArray(accesses)) {
return accesses.some((access) => !!rolesList[access]); return accesses.some((access) => !!permissionsList[access]);
} }
throw new Error(`[hasSomePermission]: ${accesses} should be a array !`); throw new Error(`[hasSomePermission]: ${accesses} should be a array !`);
} }

View File

@@ -46,7 +46,11 @@
<div class="drawer-setting-item-style align-items-top"> <div class="drawer-setting-item-style align-items-top">
<n-tooltip placement="top"> <n-tooltip placement="top">
<template #trigger> <template #trigger>
<img src="~@/assets/images/nav-theme-dark.svg" @click="togNavMode('vertical')" /> <img
src="~@/assets/images/nav-theme-dark.svg"
@click="togNavMode('vertical')"
alt="左侧菜单模式"
/>
</template> </template>
<span>左侧菜单模式</span> <span>左侧菜单模式</span>
</n-tooltip> </n-tooltip>
@@ -56,12 +60,30 @@
<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/nav-horizontal.svg" @click="togNavMode('horizontal')" /> <img
src="~@/assets/images/nav-horizontal.svg"
alt="顶部菜单模式"
@click="togNavMode('horizontal')"
/>
</template> </template>
<span>顶部菜单模式</span> <span>顶部菜单模式</span>
</n-tooltip> </n-tooltip>
<n-badge dot color="#19be6b" v-show="settingStore.navMode === 'horizontal'" /> <n-badge dot color="#19be6b" v-show="settingStore.navMode === 'horizontal'" />
</div> </div>
<div class="drawer-setting-item-style">
<n-tooltip placement="top">
<template #trigger>
<img
src="~@/assets/images/nav-horizontal-mix.svg"
@click="togNavMode('horizontal-mix')"
alt="顶部菜单混合模式"
/>
</template>
<span>顶部菜单混合模式</span>
</n-tooltip>
<n-badge dot color="#19be6b" v-show="settingStore.navMode === 'horizontal-mix'" />
</div>
</div> </div>
<n-divider title-placement="center">导航栏风格</n-divider> <n-divider title-placement="center">导航栏风格</n-divider>
@@ -70,7 +92,11 @@
<div class="drawer-setting-item-style align-items-top"> <div class="drawer-setting-item-style align-items-top">
<n-tooltip placement="top"> <n-tooltip placement="top">
<template #trigger> <template #trigger>
<img src="~@/assets/images/nav-theme-dark.svg" @click="togNavTheme('dark')" /> <img
src="~@/assets/images/nav-theme-dark.svg"
alt="暗色侧边栏"
@click="togNavTheme('dark')"
/>
</template> </template>
<span>暗色侧边栏</span> <span>暗色侧边栏</span>
</n-tooltip> </n-tooltip>
@@ -80,7 +106,11 @@
<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/nav-theme-light.svg" @click="togNavTheme('light')" /> <img
src="~@/assets/images/nav-theme-light.svg"
alt="白色侧边栏"
@click="togNavTheme('light')"
/>
</template> </template>
<span>白色侧边栏</span> <span>白色侧边栏</span>
</n-tooltip> </n-tooltip>
@@ -95,6 +125,7 @@
<img <img
src="~@/assets/images/header-theme-dark.svg" src="~@/assets/images/header-theme-dark.svg"
@click="togNavTheme('header-dark')" @click="togNavTheme('header-dark')"
alt="暗色顶栏"
/> />
</template> </template>
<span>暗色顶栏</span> <span>暗色顶栏</span>
@@ -105,6 +136,16 @@
<n-divider title-placement="center">界面功能</n-divider> <n-divider title-placement="center">界面功能</n-divider>
<div class="drawer-setting-item">
<div class="drawer-setting-item-title"> 分割菜单 </div>
<div class="drawer-setting-item-action">
<n-switch
:disabled="settingStore.navMode === 'horizontal-mix' ? false : true"
v-model:value="settingStore.menuSetting.mixMenu"
/>
</div>
</div>
<div class="drawer-setting-item"> <div class="drawer-setting-item">
<div class="drawer-setting-item-title"> 固定顶栏 </div> <div class="drawer-setting-item-title"> 固定顶栏 </div>
<div class="drawer-setting-item-action"> <div class="drawer-setting-item-action">
@@ -226,7 +267,7 @@
function togNavTheme(theme) { function togNavTheme(theme) {
settingStore.navTheme = theme; settingStore.navTheme = theme;
if (settingStore.navMode === 'horizontal' && theme === 'light') { if (settingStore.navMode === 'horizontal' && ['light'].includes(theme)) {
settingStore.navTheme = 'dark'; settingStore.navTheme = 'dark';
} }
} }
@@ -237,11 +278,7 @@
function togNavMode(mode) { function togNavMode(mode) {
settingStore.navMode = mode; settingStore.navMode = mode;
if (mode === 'horizontal') { settingStore.menuSetting.mixMenu = false;
settingStore.setNavTheme('light');
} else {
settingStore.setNavTheme('dark');
}
} }
return { return {
@@ -312,6 +349,7 @@
.justify-center { .justify-center {
justify-content: center; justify-content: center;
} }
.dark-switch .n-switch { .dark-switch .n-switch {
::v-deep(.n-switch__rail) { ::v-deep(.n-switch__rail) {
background-color: #000e1c; background-color: #000e1c;

View File

@@ -1,8 +1,20 @@
<template> <template>
<div class="layout-header" :class="{ 'layout-header-light': !(navTheme == 'header-dark') }"> <div class="layout-header">
<!--顶部菜单--> <!--顶部菜单-->
<div class="layout-header-left" v-if="navMode === 'horizontal'"> <div
<AsideMenu v-model:collapsed="collapsed" mode="horizontal" class="n-menu-horizontal-light" /> class="layout-header-left"
v-if="navMode === 'horizontal' || (navMode === 'horizontal-mix' && mixMenu)"
>
<div class="logo">
<img src="~@/assets/images/logo.png" alt="" />
<h2 v-show="!collapsed" class="title">NaiveUiAdmin</h2>
</div>
<AsideMenu
v-model:collapsed="collapsed"
v-model:location="getMenuLocation"
:inverted="getInverted"
mode="horizontal"
/>
</div> </div>
<!--左侧菜单--> <!--左侧菜单-->
<div class="layout-header-left" v-else> <div class="layout-header-left" v-else>
@@ -131,6 +143,9 @@
collapsed: { collapsed: {
type: Boolean, type: Boolean,
}, },
inverted: {
type: Boolean,
},
}, },
setup(props) { setup(props) {
const userStore = useUserStore(); const userStore = useUserStore();
@@ -153,6 +168,15 @@
crumbsSetting: getCrumbsSetting, crumbsSetting: getCrumbsSetting,
}); });
const getInverted = computed(() => {
const navTheme = unref(getNavTheme);
return ['light', 'header-dark'].includes(navTheme) ? props.inverted : !props.inverted;
});
const mixMenu = computed(() => {
return unref(getMenuSetting).mixMenu;
});
const getChangeStyle = computed(() => { const getChangeStyle = computed(() => {
const { collapsed } = props; const { collapsed } = props;
const { minMenuWidth, menuWidth }: any = unref(getMenuSetting); const { minMenuWidth, menuWidth }: any = unref(getMenuSetting);
@@ -162,6 +186,10 @@
}; };
}); });
const getMenuLocation = computed(() => {
return 'header';
});
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
@@ -305,6 +333,9 @@
reloadPage, reloadPage,
drawerSetting, drawerSetting,
openSetting, openSetting,
getInverted,
getMenuLocation,
mixMenu,
}; };
}, },
}); });
@@ -321,27 +352,43 @@
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
width: 100%; width: 100%;
z-index: 11; z-index: 11;
//color: #fff;
//.n-icon {
// color: #fff
//}
&-left { &-left {
display: flex; display: flex;
align-items: center; align-items: center;
.logo {
display: flex;
align-items: center;
justify-content: center;
height: 64px;
line-height: 64px;
overflow: hidden;
white-space: nowrap;
padding-left: 10px;
img {
width: auto;
height: 32px;
margin-right: 10px;
}
.title {
margin-bottom: 0;
}
}
::v-deep(.ant-breadcrumb span:last-child .link-text) { ::v-deep(.ant-breadcrumb span:last-child .link-text) {
color: #515a6e; color: #515a6e;
} }
::v-deep(.n-breadcrumb .n-breadcrumb-item:last-child .n-breadcrumb-item__link) {
color: #fff;
}
.n-breadcrumb { .n-breadcrumb {
display: inline-block; display: inline-block;
} }
&-menu {
color: var(--text-color);
}
} }
&-right { &-right {

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">&nbsp;NaiveUiAdmin</h2> <h2 v-show="!collapsed" class="title">NaiveUiAdmin</h2>
</div> </div>
</template> </template>
@@ -29,10 +29,10 @@
img { img {
width: auto; width: auto;
height: 32px; height: 32px;
margin-right: 10px;
} }
.title { .title {
color: white;
margin-bottom: 0; margin-bottom: 0;
} }
} }

View File

@@ -6,20 +6,21 @@
:collapsed="collapsed" :collapsed="collapsed"
:collapsed-width="64" :collapsed-width="64"
:collapsed-icon-size="20" :collapsed-icon-size="20"
:indent="28" :indent="24"
:expanded-keys="openKeys" :expanded-keys="openKeys"
v-model:value="selectedKeys" :value="getSelectedKeys"
@update:value="clickMenuItem" @update:value="clickMenuItem"
@update:expanded-keys="menuExpanded" @update:expanded-keys="menuExpanded"
/> />
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, computed, watch, toRefs, unref } from 'vue'; import { defineComponent, ref, onMounted, reactive, computed, watch, toRefs, unref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useAsyncRouteStore } from '@/store/modules/asyncRoute'; import { useAsyncRouteStore } from '@/store/modules/asyncRoute';
import { generatorMenu } from '@/utils/index'; import { generatorMenu, generatorMenuMix } from '@/utils';
import { useProjectSettingStore } from '@/store/modules/projectSetting'; import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
export default defineComponent({ export default defineComponent({
name: 'Menu', name: 'Menu',
@@ -34,13 +35,26 @@
// 侧边栏菜单是否收起 // 侧边栏菜单是否收起
type: Boolean, type: Boolean,
}, },
//位置
location: {
type: String,
default: 'left',
},
}, },
setup(props) { emits: ['update:collapsed'],
setup(props, { emit }) {
// 当前路由 // 当前路由
const currentRoute = useRoute(); const currentRoute = useRoute();
const router = useRouter(); const router = useRouter();
const asyncRouteStore = useAsyncRouteStore(); const asyncRouteStore = useAsyncRouteStore();
const settingStore = useProjectSettingStore(); const settingStore = useProjectSettingStore();
const menus = ref<any[]>([]);
const selectedKeys = ref<string>(currentRoute.name as string);
const headerMenuSelectKey = ref<string>('');
const { getNavMode } = useProjectSetting();
const navMode = getNavMode;
// 获取当前打开的子菜单 // 获取当前打开的子菜单
const matched = currentRoute.matched; const matched = currentRoute.matched;
@@ -49,23 +63,36 @@
const state = reactive({ const state = reactive({
openKeys: getOpenKeys, openKeys: getOpenKeys,
selectedKeys: currentRoute.name,
}); });
const inverted = computed(() => { const inverted = computed(() => {
return ['dark', 'header-dark'].includes(settingStore.navTheme); return ['dark', 'header-dark'].includes(settingStore.navTheme);
}); });
const menus = computed(() => { const getSelectedKeys = computed(() => {
return generatorMenu(asyncRouteStore.getMenus); let location = props.location;
return location === 'left' || (location === 'header' && unref(navMode) === 'horizontal')
? unref(selectedKeys)
: unref(headerMenuSelectKey);
}); });
// 监听分割菜单
watch(
() => settingStore.menuSetting.mixMenu,
() => {
updateMenu();
if (props.collapsed) {
emit('update:collapsed', !props.collapsed);
}
}
);
// 监听菜单收缩状态 // 监听菜单收缩状态
watch( watch(
() => props.collapsed, () => props.collapsed,
(newVal) => { (newVal) => {
state.openKeys = newVal ? [] : getOpenKeys; state.openKeys = newVal ? [] : getOpenKeys;
state.selectedKeys = currentRoute.name; selectedKeys.value = currentRoute.name as string;
} }
); );
@@ -73,12 +100,26 @@
watch( watch(
() => currentRoute.fullPath, () => currentRoute.fullPath,
() => { () => {
updateMenu();
const matched = currentRoute.matched; const matched = currentRoute.matched;
state.openKeys = matched.map((item) => item.name); state.openKeys = matched.map((item) => item.name);
state.selectedKeys = currentRoute.name; const activeMenu: string = (currentRoute.meta?.activeMenu as string) || '';
selectedKeys.value = activeMenu ? (activeMenu as string) : (currentRoute.name as string);
} }
); );
function updateMenu() {
if (!settingStore.menuSetting.mixMenu) {
menus.value = generatorMenu(asyncRouteStore.getMenus);
} else {
//混合菜单
const firstRouteName: string = (currentRoute.matched[0].name as string) || '';
menus.value = generatorMenuMix(asyncRouteStore.getMenus, firstRouteName, props.location);
const activeMenu: string = currentRoute?.matched[0].meta?.activeMenu as string;
headerMenuSelectKey.value = (activeMenu ? activeMenu : firstRouteName) || '';
}
}
// 点击菜单 // 点击菜单
function clickMenuItem(key: string) { function clickMenuItem(key: string) {
if (/http(s)?:/.test(key)) { if (/http(s)?:/.test(key)) {
@@ -101,17 +142,24 @@
if (!key) return false; if (!key) return false;
const subRouteChildren: string[] = []; const subRouteChildren: string[] = [];
for (const { children, key } of unref(menus)) { for (const { children, key } of unref(menus)) {
if (children && children.length > 0) { if (children && children.length) {
subRouteChildren.push(key as string); subRouteChildren.push(key as string);
} }
} }
return subRouteChildren.includes(key); return subRouteChildren.includes(key);
} }
onMounted(() => {
updateMenu();
});
return { return {
...toRefs(state), ...toRefs(state),
inverted, inverted,
menus, menus,
selectedKeys,
headerMenuSelectKey,
getSelectedKeys,
clickMenuItem, clickMenuItem,
menuExpanded, menuExpanded,
}; };

View File

@@ -5,6 +5,7 @@
'tabs-view-fix': multiTabsSetting.fixed, 'tabs-view-fix': multiTabsSetting.fixed,
'tabs-view-fixed-header': isMultiHeaderFixed, 'tabs-view-fixed-header': isMultiHeaderFixed,
'tabs-view-default-background': getDarkTheme === false, 'tabs-view-default-background': getDarkTheme === false,
'tabs-view-dark-background': getDarkTheme === true,
}" }"
:style="getChangeStyle" :style="getChangeStyle"
> >
@@ -59,7 +60,7 @@
placement="bottom-end" placement="bottom-end"
:options="TabsMenuOptions" :options="TabsMenuOptions"
> >
<div class="tabs-close-btn" @click.prevent> <div class="tabs-close-btn">
<n-icon size="16" color="#515a6e"> <n-icon size="16" color="#515a6e">
<DownOutlined /> <DownOutlined />
</n-icon> </n-icon>
@@ -116,6 +117,7 @@
import { renderIcon } from '@/utils/index'; import { renderIcon } from '@/utils/index';
import elementResizeDetectorMaker from 'element-resize-detector'; import elementResizeDetectorMaker from 'element-resize-detector';
import { useDesignSetting } from '@/hooks/setting/useDesignSetting'; import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
export default defineComponent({ export default defineComponent({
name: 'TabsView', name: 'TabsView',
@@ -135,6 +137,7 @@
const { getDarkTheme } = useDesignSetting(); const { getDarkTheme } = useDesignSetting();
const { getNavMode, getHeaderSetting, getMenuSetting, getMultiTabsSetting } = const { getNavMode, getHeaderSetting, getMenuSetting, getMultiTabsSetting } =
useProjectSetting(); useProjectSetting();
const settingStore = useProjectSettingStore();
const message = useMessage(); const message = useMessage();
const route = useRoute(); const route = useRoute();
@@ -165,6 +168,17 @@
return { fullPath, hash, meta, name, params, path, query }; return { fullPath, hash, meta, name, params, path, query };
}; };
const isMixMenuNoneSub = computed(() => {
const mixMenu = settingStore.menuSetting.mixMenu;
const currentRoute = useRoute();
const navMode = unref(getNavMode);
if (unref(navMode) != 'horizontal-mix') return true;
if (unref(navMode) === 'horizontal-mix' && mixMenu && currentRoute.meta.isRoot) {
return false;
}
return true;
});
//动态组装样式 菜单缩进 //动态组装样式 菜单缩进
const getChangeStyle = computed(() => { const getChangeStyle = computed(() => {
const { collapsed } = props; const { collapsed } = props;
@@ -172,7 +186,11 @@
const { minMenuWidth, menuWidth }: any = unref(getMenuSetting); const { minMenuWidth, menuWidth }: any = unref(getMenuSetting);
const { fixed }: any = unref(getMultiTabsSetting); const { fixed }: any = unref(getMultiTabsSetting);
let lenNum = let lenNum =
navMode === 'horizontal' ? '0px' : collapsed ? `${minMenuWidth}px` : `${menuWidth}px`; navMode === 'horizontal' || !isMixMenuNoneSub.value
? '0px'
: collapsed
? `${minMenuWidth}px`
: `${menuWidth}px`;
return { return {
left: lenNum, left: lenNum,
width: `calc(100% - ${!fixed ? '0px' : lenNum})`, width: `calc(100% - ${!fixed ? '0px' : lenNum})`,
@@ -354,6 +372,7 @@
break; break;
} }
updateNavScroll(); updateNavScroll();
state.showDropdown = false;
}; };
function getCurrentScrollOffset() { function getCurrentScrollOffset() {
@@ -557,6 +576,7 @@
cursor: pointer; cursor: pointer;
display: inline-block; display: inline-block;
position: relative; position: relative;
flex: 0 0 auto;
span { span {
float: left; float: left;
@@ -623,7 +643,11 @@
.tabs-view-default-background { .tabs-view-default-background {
background: #f5f7f9; background: #f5f7f9;
} }
.tabs-view-dark-background {
background: #101014;
}
.tabs-view-fix { .tabs-view-fix {
position: fixed; position: fixed;
z-index: 5; z-index: 5;

View File

@@ -1,7 +1,7 @@
<template> <template>
<NLayout class="layout" :position="fixedMenu" has-sider> <NLayout class="layout" :position="fixedMenu" has-sider>
<NLayoutSider <NLayoutSider
v-if="navMode === 'vertical'" v-if="isMixMenuNoneSub && (navMode === 'vertical' || navMode === 'horizontal-mix')"
show-trigger show-trigger
@collapse="collapsed = true" @collapse="collapsed = true"
:position="fixedMenu" :position="fixedMenu"
@@ -15,12 +15,12 @@
class="layout-sider" class="layout-sider"
> >
<Logo :collapsed="collapsed" /> <Logo :collapsed="collapsed" />
<AsideMenu v-model:collapsed="collapsed" /> <AsideMenu v-model:collapsed="collapsed" v-model:location="getMenuLocation" />
</NLayoutSider> </NLayoutSider>
<NLayout :inverted="inverted"> <NLayout :inverted="inverted">
<NLayoutHeader :inverted="inverted" :position="fixedHeader"> <NLayoutHeader :inverted="getHeaderInverted" :position="fixedHeader">
<PageHeader v-model:collapsed="collapsed" /> <PageHeader v-model:collapsed="collapsed" :inverted="inverted" />
</NLayoutHeader> </NLayoutHeader>
<NLayoutContent <NLayoutContent
@@ -62,9 +62,11 @@
import { MainView } from './components/Main'; import { MainView } from './components/Main';
import { AsideMenu } from './components/Menu'; import { AsideMenu } from './components/Menu';
import { PageHeader } from './components/Header'; import { PageHeader } from './components/Header';
import { PageFooter } from './components/Footer';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting'; import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
import { useDesignSetting } from '@/hooks/setting/useDesignSetting'; import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
import { useLoadingBar } from 'naive-ui';
import { useRoute } from 'vue-router';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
export default defineComponent({ export default defineComponent({
name: 'Layout', name: 'Layout',
@@ -74,11 +76,9 @@
PageHeader, PageHeader,
AsideMenu, AsideMenu,
Logo, Logo,
PageFooter,
}, },
setup() { setup() {
const { getDarkTheme } = useDesignSetting(); const { getDarkTheme } = useDesignSetting();
const { const {
getShowFooter, getShowFooter,
getNavMode, getNavMode,
@@ -88,6 +88,8 @@
getMultiTabsSetting, getMultiTabsSetting,
} = useProjectSetting(); } = useProjectSetting();
const settingStore = useProjectSettingStore();
const navMode = getNavMode; const navMode = getNavMode;
const collapsed = ref<boolean>(false); const collapsed = ref<boolean>(false);
@@ -97,6 +99,16 @@
return fixed ? 'absolute' : 'static'; return fixed ? 'absolute' : 'static';
}); });
const isMixMenuNoneSub = computed(() => {
const mixMenu = settingStore.menuSetting.mixMenu;
const currentRoute = useRoute();
if (unref(navMode) != 'horizontal-mix') return true;
if (unref(navMode) === 'horizontal-mix' && mixMenu && currentRoute.meta.isRoot) {
return false;
}
return true;
});
const fixedMenu = computed(() => { const fixedMenu = computed(() => {
const { fixed } = unref(getHeaderSetting); const { fixed } = unref(getHeaderSetting);
return fixed ? 'absolute' : 'static'; return fixed ? 'absolute' : 'static';
@@ -114,6 +126,11 @@
return ['dark', 'header-dark'].includes(unref(getNavTheme)); return ['dark', 'header-dark'].includes(unref(getNavTheme));
}); });
const getHeaderInverted = computed(() => {
const navTheme = unref(getNavTheme);
return ['light', 'header-dark'].includes(navTheme) ? unref(inverted) : !unref(inverted);
});
const leftMenuWidth = computed(() => { const leftMenuWidth = computed(() => {
const { minMenuWidth, menuWidth } = unref(getMenuSetting); const { minMenuWidth, menuWidth } = unref(getMenuSetting);
return collapsed.value ? minMenuWidth : menuWidth; return collapsed.value ? minMenuWidth : menuWidth;
@@ -126,6 +143,10 @@
}; };
}); });
const getMenuLocation = computed(() => {
return 'left';
});
function watchWidth() { function watchWidth() {
const Width = document.body.clientWidth; const Width = document.body.clientWidth;
if (Width <= 950) { if (Width <= 950) {
@@ -135,6 +156,9 @@
onMounted(() => { onMounted(() => {
window.addEventListener('resize', watchWidth); window.addEventListener('resize', watchWidth);
//挂载在 window 方便与在js中使用
window['$loading'] = useLoadingBar();
window['$loading'].finish();
}); });
return { return {
@@ -149,6 +173,9 @@
navMode, navMode,
getShowFooter, getShowFooter,
getDarkTheme, getDarkTheme,
getHeaderInverted,
getMenuLocation,
isMixMenuNoneSub,
}; };
}, },
}); });

View File

@@ -63,6 +63,8 @@ import {
NUpload, NUpload,
NTree, NTree,
NSpin, NSpin,
NTimePicker,
NBackTop,
} from 'naive-ui'; } from 'naive-ui';
const naive = create({ const naive = create({
@@ -129,6 +131,8 @@ const naive = create({
NUpload, NUpload,
NTree, NTree,
NSpin, NSpin,
NTimePicker,
NBackTop,
], ],
}); });

View File

@@ -13,7 +13,7 @@ export const ErrorPageRoute: AppRouteRecordRaw = {
children: [ children: [
{ {
path: '/:path(.*)*', path: '/:path(.*)*',
name: 'ErrorPage', name: 'ErrorPageSon',
component: ErrorPage, component: ErrorPage,
meta: { meta: {
title: 'ErrorPage', title: 'ErrorPage',

View File

@@ -1,16 +0,0 @@
import { renderIcon } from '@/utils/index';
import { DashboardOutlined } from '@vicons/antd';
// import { RouterTransition } from '@/components/transition'
//前端路由映射表
export const constantRouterComponents = {
Layout: () => import('@/layout/index.vue'), //布局
DashboardConsole: () => import('@/views/dashboard/console/console.vue'), // 主控台
DashboardMonitor: () => import('@/views/dashboard/monitor/monitor.vue'), // 监控页
DashboardWorkplace: () => import('@/views/dashboard/workplace/workplace.vue'), // 工作台
};
//前端路由图标映射表
export const constantRouterIcon = {
DashboardOutlined: renderIcon(DashboardOutlined),
};

View File

@@ -1,12 +1,19 @@
import { adminMenus } from '@/api/system/menu'; import { adminMenus } from '@/api/system/menu';
import { constantRouterComponents, constantRouterIcon } from './constantRouterComponents'; import { constantRouterIcon } from './router-icons';
import router from '@/router/index'; import router from '@/router/index';
import { constantRouter } from '@/router/index'; import { constantRouter } from '@/router/index';
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import { Layout, ParentLayout } from '@/router/constant';
import type { AppRouteRecordRaw } from '@/router/types';
const Iframe = () => import('@/views/iframe/index.vue');
const LayoutMap = new Map<string, () => Promise<typeof import('*.vue')>>();
LayoutMap.set('LAYOUT', Layout);
LayoutMap.set('IFRAME', Iframe);
/** /**
* 格式化 后端 结构信息并递归生成层级路由表 * 格式化 后端 结构信息并递归生成层级路由表
*
* @param routerMap * @param routerMap
* @param parent * @param parent
* @returns {*} * @returns {*}
@@ -19,21 +26,24 @@ export const routerGenerator = (routerMap, parent?): any[] => {
// 路由名称,建议唯一 // 路由名称,建议唯一
name: item.name || '', name: item.name || '',
// 该路由对应页面的 组件 // 该路由对应页面的 组件
component: constantRouterComponents[item.component], component: item.component,
// meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉) // meta: 页面标题, 菜单图标, 页面权限(供指令权限用,可去掉)
meta: { meta: {
...item.meta, ...item.meta,
label: item.meta.title, label: item.meta.title,
icon: constantRouterIcon[item.meta.icon] || null, icon: constantRouterIcon[item.meta.icon] || null,
permission: item.meta.permission || null, permissions: item.meta.permissions || null,
}, },
}; };
// 为了防止出现后端返回结果不规范,处理有可能出现拼接出两个 反斜杠 // 为了防止出现后端返回结果不规范,处理有可能出现拼接出两个 反斜杠
currentRouter.path = currentRouter.path.replace('//', '/'); currentRouter.path = currentRouter.path.replace('//', '/');
// 重定向 // 重定向
item.redirect && (currentRouter.redirect = item.redirect); item.redirect && (currentRouter.redirect = item.redirect);
// 是否有子菜单,并递归处理 // 是否有子菜单,并递归处理
if (item.children && item.children.length > 0) { if (item.children && item.children.length > 0) {
//如果未定义 redirect 默认第一个子路由为 redirect
!item.redirect && (currentRouter.redirect = `${item.path}/${item.children[0].path}`);
// Recursion // Recursion
currentRouter.children = routerGenerator(item.children, currentRouter); currentRouter.children = routerGenerator(item.children, currentRouter);
} }
@@ -43,7 +53,6 @@ export const routerGenerator = (routerMap, parent?): any[] => {
/** /**
* 动态生成菜单 * 动态生成菜单
* @param token
* @returns {Promise<Router>} * @returns {Promise<Router>}
*/ */
export const generatorDynamicRouter = (): Promise<RouteRecordRaw[]> => { export const generatorDynamicRouter = (): Promise<RouteRecordRaw[]> => {
@@ -51,7 +60,8 @@ export const generatorDynamicRouter = (): Promise<RouteRecordRaw[]> => {
adminMenus() adminMenus()
.then((result) => { .then((result) => {
const routeList = routerGenerator(result); const routeList = routerGenerator(result);
const asyncRoutesList = [...constantRouter, ...routeList]; asyncImportRoute(routeList);
const asyncRoutesList = [...routeList, ...constantRouter];
asyncRoutesList.forEach((item) => { asyncRoutesList.forEach((item) => {
router.addRoute(item); router.addRoute(item);
}); });
@@ -62,3 +72,56 @@ export const generatorDynamicRouter = (): Promise<RouteRecordRaw[]> => {
}); });
}); });
}; };
/**
* 查找views中对应的组件文件
* */
let viewsModules: Record<string, () => Promise<Recordable>>;
export const asyncImportRoute = (routes: AppRouteRecordRaw[] | undefined): void => {
viewsModules = viewsModules || import.meta.glob('../views/**/*.{vue,tsx}');
if (!routes) return;
routes.forEach((item) => {
if (!item.component && item.meta?.frameSrc) {
item.component = 'IFRAME';
}
const { component, name } = item;
const { children } = item;
if (component) {
const layoutFound = LayoutMap.get(component as string);
if (layoutFound) {
item.component = layoutFound;
} else {
item.component = dynamicImport(viewsModules, component as string);
}
} else if (name) {
item.component = ParentLayout;
}
children && asyncImportRoute(children);
});
};
/**
* 动态导入
* */
export const dynamicImport = (
viewsModules: Record<string, () => Promise<Recordable>>,
component: string
) => {
const keys = Object.keys(viewsModules);
const matchKeys = keys.filter((key) => {
let k = key.replace('../views', '');
const lastIndex = k.lastIndexOf('.');
k = k.substring(0, lastIndex);
return k === component;
});
if (matchKeys?.length === 1) {
const matchKey = matchKeys[0];
return viewsModules[matchKey];
}
if (matchKeys?.length > 1) {
console.warn(
'Please do not create `.vue` and `.TSX` files with the same file name in the same hierarchical directory under the views folder. This will cause dynamic introduction failure'
);
return;
}
};

View File

@@ -1,6 +1,6 @@
import { App } from 'vue'; import { App } from 'vue';
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router'; import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import { ErrorPageRoute, RedirectRoute } from '@/router/base'; import { RedirectRoute } from '@/router/base';
import { PageEnum } from '@/enums/pageEnum'; import { PageEnum } from '@/enums/pageEnum';
import { createRouterGuards } from './router-guards'; import { createRouterGuards } from './router-guards';
@@ -16,7 +16,7 @@ Object.keys(modules).forEach((key) => {
}); });
function sortRoute(a, b) { function sortRoute(a, b) {
return (a.meta.sort || 0) - (b.meta.sort || 0); return (a.meta?.sort || 0) - (b.meta?.sort || 0);
} }
routeModuleList.sort(sortRoute); routeModuleList.sort(sortRoute);
@@ -40,7 +40,7 @@ export const LoginRoute: RouteRecordRaw = {
}; };
//需要验证权限 //需要验证权限
export const asyncRoutes = [ErrorPageRoute, ...routeModuleList]; export const asyncRoutes = [...routeModuleList];
//普通路由 无需验证权限 //普通路由 无需验证权限
export const constantRouter: any[] = [LoginRoute, RootRoute, RedirectRoute]; export const constantRouter: any[] = [LoginRoute, RootRoute, RedirectRoute];

View File

@@ -0,0 +1,32 @@
import { RouteRecordRaw } from 'vue-router';
import { Layout } from '@/router/constant';
import { ProjectOutlined } from '@vicons/antd';
import { renderIcon, renderNew } from '@/utils/index';
const routes: Array<RouteRecordRaw> = [
{
path: '/about',
name: 'about',
component: Layout,
meta: {
sort: 10,
isRoot: true,
activeMenu: 'about_index',
},
children: [
{
path: 'index',
name: `about_index`,
meta: {
title: '关于',
icon: renderIcon(ProjectOutlined),
extra: renderNew(),
activeMenu: 'about_index',
},
component: () => import('@/views/about/index.vue'),
},
],
},
];
export default routes;

View File

@@ -63,14 +63,49 @@ const routes: Array<RouteRecordRaw> = [
}, },
], ],
}, },
{
path: 'form',
name: `${routeName}_form`,
redirect: '/comp/form/basic',
component: ParentLayout,
meta: {
title: '表单',
},
children: [
{
path: 'basic',
name: `${routeName}_form_basic`,
meta: {
title: '基础使用',
},
component: () => import('@/views/comp/form/basic.vue'),
},
{
path: 'useForm',
name: `useForm`,
meta: {
title: 'useForm',
},
component: () => import('@/views/comp/form/useForm.vue'),
},
],
},
{ {
path: 'upload', path: 'upload',
name: `${routeName}_upload`, name: `${routeName}_upload`,
meta: { meta: {
title: '上传', title: '上传图片',
}, },
component: () => import('@/views/comp/upload/index.vue'), component: () => import('@/views/comp/upload/index.vue'),
}, },
{
path: 'modal',
name: `${routeName}_modal`,
meta: {
title: '弹窗扩展',
},
component: () => import('@/views/comp/modal/index.vue'),
},
], ],
}, },
]; ];

View File

@@ -24,7 +24,7 @@ const routes: Array<RouteRecordRaw> = [
meta: { meta: {
title: 'Dashboard', title: 'Dashboard',
icon: renderIcon(DashboardOutlined), icon: renderIcon(DashboardOutlined),
permission: ['dashboard_console', 'dashboard_console', 'dashboard_workplace'], permissions: ['dashboard_console', 'dashboard_console', 'dashboard_workplace'],
sort: 0, sort: 0,
}, },
children: [ children: [
@@ -33,7 +33,7 @@ const routes: Array<RouteRecordRaw> = [
name: `${routeName}_console`, name: `${routeName}_console`,
meta: { meta: {
title: '主控台', title: '主控台',
permission: ['dashboard_console'], permissions: ['dashboard_console'],
}, },
component: () => import('@/views/dashboard/console/console.vue'), component: () => import('@/views/dashboard/console/console.vue'),
}, },
@@ -42,7 +42,7 @@ const routes: Array<RouteRecordRaw> = [
// name: `${ routeName }_monitor`, // name: `${ routeName }_monitor`,
// meta: { // meta: {
// title: '监控页', // title: '监控页',
// permission: ['dashboard_monitor'] // permissions: ['dashboard_monitor']
// }, // },
// component: () => import('@/views/dashboard/monitor/monitor.vue') // component: () => import('@/views/dashboard/monitor/monitor.vue')
// }, // },
@@ -52,7 +52,7 @@ const routes: Array<RouteRecordRaw> = [
meta: { meta: {
title: '工作台', title: '工作台',
keepAlive: true, keepAlive: true,
permission: ['dashboard_workplace'], permissions: ['dashboard_workplace'],
}, },
component: () => import('@/views/dashboard/workplace/workplace.vue'), component: () => import('@/views/dashboard/workplace/workplace.vue'),
}, },

View File

@@ -0,0 +1,19 @@
import { RouteRecordRaw } from 'vue-router';
import { Layout } from '@/router/constant';
import { DocumentTextOutline } from '@vicons/ionicons5';
import { renderIcon } from '@/utils/index';
const routes: Array<RouteRecordRaw> = [
{
path: '/external',
name: 'https://naive-ui-admin-docs.vercel.app',
component: Layout,
meta: {
title: '项目文档',
icon: renderIcon(DocumentTextOutline),
sort: 9,
},
},
];
export default routes;

View File

@@ -0,0 +1,42 @@
import { RouteRecordRaw } from 'vue-router';
import { Layout } from '@/router/constant';
import { DesktopOutline } from '@vicons/ionicons5';
import { renderIcon } from '@/utils/index';
const IFrame = () => import('@/views/iframe/index.vue');
const routes: Array<RouteRecordRaw> = [
{
path: '/frame',
name: 'Frame',
redirect: '/frame/docs',
component: Layout,
meta: {
title: '外部页面',
sort: 8,
icon: renderIcon(DesktopOutline),
},
children: [
{
path: 'docs',
name: 'frame-docs',
meta: {
title: '项目文档(内嵌)',
frameSrc: 'https://naive-ui-admin-docs.vercel.app',
},
component: IFrame,
},
{
path: 'naive',
name: 'frame-naive',
meta: {
title: 'NaiveUi(内嵌)',
frameSrc: 'https://www.naiveui.com',
},
component: IFrame,
},
],
},
];
export default routes;

View File

@@ -1,7 +1,7 @@
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import { Layout } from '@/router/constant'; import { Layout } from '@/router/constant';
import { TableOutlined } from '@vicons/antd'; import { TableOutlined } from '@vicons/antd';
import { renderIcon } from '@/utils/index'; import { renderIcon, renderNew } from '@/utils/index';
/** /**
* @param name 路由名称, 必须设置,且不能重名 * @param name 路由名称, 必须设置,且不能重名
@@ -31,6 +31,7 @@ const routes: Array<RouteRecordRaw> = [
name: 'basic-list', name: 'basic-list',
meta: { meta: {
title: '基础列表', title: '基础列表',
extra: renderNew(),
}, },
component: () => import('@/views/list/basicList/index.vue'), component: () => import('@/views/list/basicList/index.vue'),
}, },
@@ -40,6 +41,7 @@ const routes: Array<RouteRecordRaw> = [
meta: { meta: {
title: '基础详情', title: '基础详情',
hidden: true, hidden: true,
activeMenu: 'basic-list',
}, },
component: () => import('@/views/list/basicList/info.vue'), component: () => import('@/views/list/basicList/info.vue'),
}, },

View File

@@ -1,9 +1,11 @@
import type { RouteRecordRaw } from 'vue-router';
import { isNavigationFailure, Router } from 'vue-router'; import { isNavigationFailure, Router } from 'vue-router';
import { useUserStoreWidthOut } from '@/store/modules/user'; import { useUserStoreWidthOut } from '@/store/modules/user';
import { useAsyncRouteStoreWidthOut } from '@/store/modules/asyncRoute'; import { useAsyncRouteStoreWidthOut } from '@/store/modules/asyncRoute';
import { ACCESS_TOKEN } from '@/store/mutation-types'; import { ACCESS_TOKEN } from '@/store/mutation-types';
import { storage } from '@/utils/Storage'; import { storage } from '@/utils/Storage';
import { PageEnum } from '@/enums/pageEnum'; import { PageEnum } from '@/enums/pageEnum';
import { ErrorPageRoute } from '@/router/base';
const LOGIN_PATH = PageEnum.BASE_LOGIN; const LOGIN_PATH = PageEnum.BASE_LOGIN;
@@ -12,8 +14,8 @@ const whitePathList = [LOGIN_PATH]; // no redirect whitelist
export function createRouterGuards(router: Router) { export function createRouterGuards(router: Router) {
const userStore = useUserStoreWidthOut(); const userStore = useUserStoreWidthOut();
const asyncRouteStore = useAsyncRouteStoreWidthOut(); const asyncRouteStore = useAsyncRouteStoreWidthOut();
const Loading = window['$loading'] || null;
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
const Loading = window['$loading'] || null;
Loading && Loading.start(); Loading && Loading.start();
if (from.path === LOGIN_PATH && to.name === 'errorPage') { if (from.path === LOGIN_PATH && to.name === 'errorPage') {
next(PageEnum.BASE_HOME); next(PageEnum.BASE_HOME);
@@ -29,7 +31,7 @@ export function createRouterGuards(router: Router) {
const token = storage.get(ACCESS_TOKEN); const token = storage.get(ACCESS_TOKEN);
if (!token) { if (!token) {
// You can access without permission. You need to set the routing meta.ignoreAuth to true // You can access without permissions. You need to set the routing meta.ignoreAuth to true
if (to.meta.ignoreAuth) { if (to.meta.ignoreAuth) {
next(); next();
return; return;
@@ -60,9 +62,15 @@ export function createRouterGuards(router: Router) {
// 动态添加可访问路由表 // 动态添加可访问路由表
routes.forEach((item) => { routes.forEach((item) => {
router.addRoute(item); router.addRoute(item as unknown as RouteRecordRaw);
}); });
//添加404
const isErrorPage = router.getRoutes().findIndex((item) => item.name === ErrorPageRoute.name);
if (isErrorPage === -1) {
router.addRoute(ErrorPageRoute as unknown as RouteRecordRaw);
}
const redirectPath = (from.query.redirect || to.path) as string; const redirectPath = (from.query.redirect || to.path) as string;
const redirect = decodeURIComponent(redirectPath); const redirect = decodeURIComponent(redirectPath);
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }; const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect };
@@ -91,6 +99,7 @@ export function createRouterGuards(router: Router) {
} }
} }
asyncRouteStore.setKeepAliveComponents(keepAliveComponents); asyncRouteStore.setKeepAliveComponents(keepAliveComponents);
const Loading = window['$loading'] || null;
Loading && Loading.finish(); Loading && Loading.finish();
}); });

View File

@@ -0,0 +1,7 @@
import { renderIcon } from '@/utils/index';
import { DashboardOutlined } from '@vicons/antd';
//前端路由图标映射表
export const constantRouterIcon = {
DashboardOutlined: renderIcon(DashboardOutlined),
};

View File

@@ -1,6 +1,5 @@
import type { RouteRecordRaw, RouteMeta } from 'vue-router'; import type { RouteRecordRaw, RouteMeta } from 'vue-router';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { RoleEnum } from '@/enums/roleEnum';
export type Component<T extends any = any> = export type Component<T extends any = any> =
| ReturnType<typeof defineComponent> | ReturnType<typeof defineComponent>
@@ -23,7 +22,7 @@ export interface Meta {
title: string; title: string;
// 是否忽略权限 // 是否忽略权限
ignoreAuth?: boolean; ignoreAuth?: boolean;
roles?: RoleEnum[]; permissions?: string[];
// 是否不缓存 // 是否不缓存
noKeepAlive?: boolean; noKeepAlive?: boolean;
// 是否固定在tab上 // 是否固定在tab上

View File

@@ -31,6 +31,8 @@ const setting = {
menuWidth: 200, menuWidth: 200,
//固定菜单 //固定菜单
fixed: true, fixed: true,
//分割菜单
mixMenu: false,
}, },
//面包屑 //面包屑
crumbsSetting: { crumbsSetting: {
@@ -39,7 +41,7 @@ const setting = {
//显示图标 //显示图标
showIcon: false, showIcon: false,
}, },
//菜单权限模式 ROLE 前端固定角色 BACK 动态获取 //菜单权限模式 FIXED 前端固定路由 BACK 动态获取
permissionMode: 'ROLE', permissionMode: 'FIXED',
}; };
export default setting; export default setting;

View File

@@ -20,7 +20,7 @@ const DEFAULT_CONFIG: TreeHelperConfig = {
const getConfig = (config: Partial<TreeHelperConfig>) => Object.assign({}, DEFAULT_CONFIG, config); const getConfig = (config: Partial<TreeHelperConfig>) => Object.assign({}, DEFAULT_CONFIG, config);
interface AsyncRouteState { export interface IAsyncRouteState {
menus: RouteRecordRaw[]; menus: RouteRecordRaw[];
routers: any[]; routers: any[];
addRouters: any[]; addRouters: any[];
@@ -50,7 +50,7 @@ function filter<T = any>(
export const useAsyncRouteStore = defineStore({ export const useAsyncRouteStore = defineStore({
id: 'app-async-route', id: 'app-async-route',
state: (): AsyncRouteState => ({ state: (): IAsyncRouteState => ({
menus: [], menus: [],
routers: constantRouter, routers: constantRouter,
addRouters: [], addRouters: [],
@@ -88,12 +88,12 @@ export const useAsyncRouteStore = defineStore({
}, },
async generateRoutes(data) { async generateRoutes(data) {
let accessedRouters; let accessedRouters;
const roleList = data.roles || []; const permissionsList = data.permissions || [];
const routeFilter = (route) => { const routeFilter = (route) => {
const { meta } = route; const { meta } = route;
const { permission } = meta || {}; const { permissions } = meta || {};
if (!permission) return true; if (!permissions) return true;
return roleList.some((role) => permission.includes(role.value)); return permissionsList.some((item) => permissions.includes(item.value));
}; };
const { getPermissionMode } = useProjectSetting(); const { getPermissionMode } = useProjectSetting();
const permissionMode = unref(getPermissionMode); const permissionMode = unref(getPermissionMode);

View File

@@ -8,23 +8,23 @@ const Storage = createStorage({ storage: localStorage });
import { getUserInfo, login } from '@/api/system/user'; import { getUserInfo, login } from '@/api/system/user';
import { storage } from '@/utils/Storage'; import { storage } from '@/utils/Storage';
interface UserState { export interface IUserState {
token: string; token: string;
username: string; username: string;
welcome: string; welcome: string;
avatar: string; avatar: string;
roles: any[]; permissions: any[];
info: any; info: any;
} }
export const useUserStore = defineStore({ export const useUserStore = defineStore({
id: 'app-user', id: 'app-user',
state: (): UserState => ({ state: (): IUserState => ({
token: Storage.get(ACCESS_TOKEN, ''), token: Storage.get(ACCESS_TOKEN, ''),
username: '', username: '',
welcome: '', welcome: '',
avatar: '', avatar: '',
roles: [], permissions: [],
info: Storage.get(CURRENT_USER, {}), info: Storage.get(CURRENT_USER, {}),
}), }),
getters: { getters: {
@@ -37,8 +37,8 @@ export const useUserStore = defineStore({
getNickname(): string { getNickname(): string {
return this.username; return this.username;
}, },
getRoles(): [any][] { getPermissions(): [any][] {
return this.roles; return this.permissions;
}, },
getUserInfo(): object { getUserInfo(): object {
return this.info; return this.info;
@@ -51,8 +51,8 @@ export const useUserStore = defineStore({
setAvatar(avatar: string) { setAvatar(avatar: string) {
this.avatar = avatar; this.avatar = avatar;
}, },
setRoles(roles) { setPermissions(permissions) {
this.roles = roles; this.permissions = permissions;
}, },
setUserInfo(info) { setUserInfo(info) {
this.info = info; this.info = info;
@@ -83,12 +83,12 @@ export const useUserStore = defineStore({
getUserInfo() getUserInfo()
.then((res) => { .then((res) => {
const result = res; const result = res;
if (result.roles && result.roles.length) { if (result.permissions && result.permissions.length) {
const roles = result.roles; const permissionsList = result.permissions;
that.setRoles(roles); that.setPermissions(permissionsList);
that.setUserInfo(result); that.setUserInfo(result);
} else { } else {
reject(new Error('getInfo: roles must be a non-null array !')); reject(new Error('getInfo: permissionsList must be a non-null array !'));
} }
that.setAvatar(result.avatar); that.setAvatar(result.avatar);
resolve(res); resolve(res);
@@ -101,7 +101,7 @@ export const useUserStore = defineStore({
// 登出 // 登出
async logout() { async logout() {
this.setRoles([]); this.setPermissions([]);
this.setUserInfo(''); this.setUserInfo('');
storage.remove(ACCESS_TOKEN); storage.remove(ACCESS_TOKEN);
storage.remove(CURRENT_USER); storage.remove(CURRENT_USER);

View File

@@ -1,7 +1,7 @@
import { IAsyncRouteState } from '@/store/modules/async-route'; import { IAsyncRouteState } from '@/store/modules/asyncRoute';
import { IUserState } from '@/store/modules/user/state'; import { IUserState } from '@/store/modules/user';
import { ILockscreenState } from '@/store/modules/lockscreen'; import { ILockscreenState } from '@/store/modules/lockscreen';
import { ITabsViewState } from '@/store/modules/tabs-view'; import { ITabsViewState } from '@/store/modules/tabsView';
export interface IStore { export interface IStore {
asyncRoute: IAsyncRouteState; asyncRoute: IAsyncRouteState;

View File

@@ -110,6 +110,10 @@ body .proCard {
} }
} }
body .n-modal{
border-radius: 6px;
}
//body .proCardTabs{ //body .proCardTabs{
// .n-card__content{ padding-top: 3px} // .n-card__content{ padding-top: 3px}
// .n-card__content:first-child{ padding-top: 3px} // .n-card__content:first-child{ padding-top: 3px}

99
src/utils/Drag.ts Normal file
View File

@@ -0,0 +1,99 @@
//获取相关CSS属性
const getCss = function (o, key) {
// @ts-ignore
return o.currentStyle
? o.currentStyle[key]
: document.defaultView?.getComputedStyle(o, null)[key];
};
const params = {
left: 0,
top: 0,
currentX: 0,
currentY: 0,
flag: false,
};
const startDrag = function (bar, target, callback) {
const screenWidth = document.body.clientWidth; // body当前宽度
const screenHeight = document.documentElement.clientHeight; // 可见区域高度
const dragDomW = target.offsetWidth; // 对话框宽度
const dragDomH = target.offsetHeight; // 对话框高度
const minDomLeft = target.offsetLeft;
const minDomTop = target.offsetTop;
const maxDragDomLeft = screenWidth - minDomLeft - dragDomW;
const maxDragDomTop = screenHeight - minDomTop - dragDomH;
if (getCss(target, 'left') !== 'auto') {
params.left = getCss(target, 'left');
}
if (getCss(target, 'top') !== 'auto') {
params.top = getCss(target, 'top');
}
//o是移动对象
bar.onmousedown = function (event) {
params.flag = true;
if (!event) {
event = window.event;
//防止IE文字选中
bar.onselectstart = function () {
return false;
};
}
const e = event;
params.currentX = e.clientX;
params.currentY = e.clientY;
};
document.onmouseup = function () {
params.flag = false;
if (getCss(target, 'left') !== 'auto') {
params.left = getCss(target, 'left');
}
if (getCss(target, 'top') !== 'auto') {
params.top = getCss(target, 'top');
}
};
document.onmousemove = function (event) {
const e: any = event ? event : window.event;
if (params.flag) {
const nowX = e.clientX,
nowY = e.clientY;
const disX = nowX - params.currentX,
disY = nowY - params.currentY;
let left = parseInt(params.left) + disX;
let top = parseInt(params.top) + disY;
// 拖出屏幕边缘
if (-left > minDomLeft) {
left = -minDomLeft;
} else if (left > maxDragDomLeft) {
left = maxDragDomLeft;
}
if (-top > minDomTop) {
top = -minDomTop;
} else if (top > maxDragDomTop) {
top = maxDragDomTop;
}
target.style.left = left + 'px';
target.style.top = top + 'px';
if (typeof callback == 'function') {
callback((parseInt(params.left) || 0) + disX, (parseInt(params.top) || 0) + disY);
}
if (event.preventDefault) {
event.preventDefault();
}
return false;
}
};
};
export default startDrag;

12
src/utils/dateUtil.ts Normal file
View File

@@ -0,0 +1,12 @@
import { format } from 'date-fns';
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm';
const DATE_FORMAT = 'YYYY-MM-DD ';
export function formatToDateTime(date: null, formatStr = DATE_TIME_FORMAT): string {
return format(date, formatStr);
}
export function formatToDate(date: null, formatStr = DATE_FORMAT): string {
return format(date, formatStr);
}

View File

@@ -1,8 +1,9 @@
import { h } from 'vue'; import { h, unref } from 'vue';
import type { App, Plugin } from 'vue'; import type { App, Plugin } from 'vue';
import { NIcon } from 'naive-ui'; import { NIcon, NTag } from 'naive-ui';
import { PageEnum } from '@/enums/pageEnum'; import { PageEnum } from '@/enums/pageEnum';
import { isObject } from './is/index';
import { cloneDeep } from 'lodash-es';
/** /**
* render 图标 * render 图标
* */ * */
@@ -10,31 +11,111 @@ export function renderIcon(icon) {
return () => h(NIcon, null, { default: () => h(icon) }); return () => h(NIcon, null, { default: () => h(icon) });
} }
/**
* render new Tag
* */
const newTagColors = { color: '#f90', textColor: '#fff', borderColor: '#f90' };
export function renderNew(type = 'warning', text = 'New', color: object = newTagColors) {
return () =>
h(
NTag as any,
{
type,
round: true,
size: 'small',
color,
},
{ default: () => text }
);
}
/** /**
* 递归组装菜单格式 * 递归组装菜单格式
*/ */
export function generatorMenu(routerMap: Array<any>) { export function generatorMenu(routerMap: Array<any>) {
return routerMap return filterRouter(routerMap).map((item) => {
.filter((item) => { const isRoot = isRootRouter(item);
return ( const info = isRoot ? item.children[0] : item;
item.meta.hidden != true && const currentMenu = {
!['/:path(.*)*', '/', PageEnum.REDIRECT, PageEnum.BASE_LOGIN].includes(item.path) ...info,
); ...info.meta,
}) label: info.meta?.title,
.map((item) => { key: info.name,
};
// 是否有子菜单,并递归处理
if (info.children && info.children.length > 0) {
// Recursion
currentMenu.children = generatorMenu(info.children);
}
return currentMenu;
});
}
/**
* 混合菜单
* */
export function generatorMenuMix(routerMap: Array<any>, routerName: string, location: string) {
const cloneRouterMap = cloneDeep(routerMap);
const newRouter = filterRouter(cloneRouterMap);
if (location === 'header') {
const firstRouter: any[] = [];
newRouter.forEach((item) => {
const isRoot = isRootRouter(item);
const info = isRoot ? item.children[0] : item;
info.children = undefined;
const currentMenu = { const currentMenu = {
...item, ...info,
...item.meta, ...info.meta,
label: item.meta.title, label: info.meta?.title,
key: item.name, key: info.name,
}; };
// 是否有子菜单,并递归处理 firstRouter.push(currentMenu);
if (item.children && item.children.length > 0) {
// Recursion
currentMenu.children = generatorMenu(item.children);
}
return currentMenu;
}); });
return firstRouter;
} else {
return getChildrenRouter(newRouter.filter((item) => item.name === routerName));
}
}
/**
* 递归组装子菜单
* */
export function getChildrenRouter(routerMap: Array<any>) {
return filterRouter(routerMap).map((item) => {
const isRoot = isRootRouter(item);
const info = isRoot ? item.children[0] : item;
const currentMenu = {
...info,
...info.meta,
label: info.meta?.title,
key: info.name,
};
// 是否有子菜单,并递归处理
if (info.children && info.children.length > 0) {
// Recursion
currentMenu.children = getChildrenRouter(info.children);
}
return currentMenu;
});
}
/**
* 判断根路由 Router
* */
export function isRootRouter(item) {
return item.meta?.alwaysShow != true && item.children?.length === 1;
}
/**
* 排除Router
* */
export function filterRouter(routerMap: Array<any>) {
return routerMap.filter((item) => {
return (
(item.meta?.hidden || false) != true &&
!['/:path(.*)*', '/', PageEnum.REDIRECT, PageEnum.BASE_LOGIN].includes(item.path)
);
});
} }
export const withInstall = <T>(component: T, alias?: string) => { export const withInstall = <T>(component: T, alias?: string) => {
@@ -78,3 +159,49 @@ export function getTreeAll(data: any[]): any[] {
}); });
return treeAll; return treeAll;
} }
// dynamic use hook props
export function getDynamicProps<T, U>(props: T): Partial<U> {
const ret: Recordable = {};
Object.keys(props).map((key) => {
ret[key] = unref((props as Recordable)[key]);
});
return ret as Partial<U>;
}
export function deepMerge<T = any>(src: any = {}, target: any = {}): T {
let key: string;
for (key in target) {
src[key] = isObject(src[key]) ? deepMerge(src[key], target[key]) : (src[key] = target[key]);
}
return src;
}
/**
* Sums the passed percentage to the R, G or B of a HEX color
* @param {string} color The color to change
* @param {number} amount The amount to change the color by
* @returns {string} The processed part of the color
*/
function addLight(color: string, amount: number) {
const cc = parseInt(color, 16) + amount;
const c = cc > 255 ? 255 : cc;
return c.toString(16).length > 1 ? c.toString(16) : `0${c.toString(16)}`;
}
/**
* Lightens a 6 char HEX color according to the passed percentage
* @param {string} color The color to change
* @param {number} amount The amount to change the color by
* @returns {string} The processed color represented as HEX
*/
export function lighten(color: string, amount: number) {
color = color.indexOf('#') >= 0 ? color.substring(1, color.length) : color;
amount = Math.trunc((255 * amount) / 100);
return `#${addLight(color.substring(0, 2), amount)}${addLight(
color.substring(2, 4),
amount
)}${addLight(color.substring(4, 6), amount)}`;
}

View File

@@ -112,3 +112,7 @@ export function isNull(val: unknown): val is null {
export function isNullAndUnDef(val: unknown): val is null | undefined { export function isNullAndUnDef(val: unknown): val is null | undefined {
return isUnDef(val) && isNull(val); return isUnDef(val) && isNull(val);
} }
export function isNullOrUnDef(val: unknown): val is null | undefined {
return isUnDef(val) || isNull(val);
}

121
src/views/about/index.vue Normal file
View File

@@ -0,0 +1,121 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="关于">
{{ name }} 是一个基于 vue3vite2TypeScript
的中后台解决方案它可以帮助你快速搭建企业级中后台项目相信不管是从新技术使用还是其他方面都能帮助到你持续更新中
</n-card>
</div>
<n-card
:bordered="false"
title="项目信息"
class="proCard mt-4"
size="small"
:segmented="{ content: 'hard' }"
>
<n-descriptions bordered label-placement="left" class="py-2">
<n-descriptions-item label="版本">
<n-tag type="info"> {{ version }} </n-tag>
</n-descriptions-item>
<n-descriptions-item label="最后编译时间">
<n-tag type="info"> {{ lastBuildTime }} </n-tag>
</n-descriptions-item>
<n-descriptions-item label="文档地址">
<div class="flex items-center">
<a href="https://naive-ui-admin-docs.vercel.app" class="py-2" target="_blank"
>查看文档地址</a
>
</div>
</n-descriptions-item>
<n-descriptions-item label="预览地址">
<div class="flex items-center">
<a href="https://naive-ui-admin.vercel.app" class="py-2" target="_blank"
>查看预览地址</a
>
</div>
</n-descriptions-item>
<n-descriptions-item label="Github">
<div class="flex items-center">
<a href="https://github.com/jekip/naive-ui-admin" class="py-2" target="_blank"
>查看Github地址</a
>
</div>
</n-descriptions-item>
<n-descriptions-item label="QQ交流群">
<div class="flex items-center">
<a href="https://jq.qq.com/?_wv=1027&k=xib9dU4C" class="py-2" target="_blank"
>点击链接加入群聊Naive Admin</a
>
</div>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
title="开发环境依赖"
class="proCard mt-4"
size="small"
:segmented="{ content: 'hard' }"
>
<n-descriptions bordered label-placement="left" class="py-2">
<n-descriptions-item v-for="item in devSchema" :key="item.field" :label="item.field">
{{ item.label }}
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
title="生产环境依赖"
class="proCard mt-4"
size="small"
:segmented="{ content: 'hard' }"
>
<n-descriptions bordered label-placement="left" class="py-2">
<n-descriptions-item v-for="item in schema" :key="item.field" :label="item.field">
{{ item.label }}
</n-descriptions-item>
</n-descriptions>
</n-card>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export interface schemaItem {
field: string;
label: string;
}
export default defineComponent({
setup() {
const { pkg, lastBuildTime } = __APP_INFO__;
const { dependencies, devDependencies, name, version } = pkg;
const schema: schemaItem[] = [];
const devSchema: schemaItem[] = [];
Object.keys(dependencies).forEach((key) => {
schema.push({ field: key, label: dependencies[key] });
});
Object.keys(devDependencies).forEach((key) => {
devSchema.push({ field: key, label: devDependencies[key] });
});
return {
lastBuildTime,
dependencies,
devDependencies,
name,
version,
schema,
devSchema,
};
},
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,184 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="基础表单"> 基础表单用于向用户收集表单信息 </n-card>
</div>
<n-card :bordered="false" class="proCard mt-4">
<div class="BasicForm">
<BasicForm
submitButtonText="提交预约"
layout="horizontal"
:gridProps="{ cols: 1 }"
:schemas="schemas"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
</div>
</n-card>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { BasicForm, FormSchema } from '@/components/Form/index';
import { useMessage } from 'naive-ui';
const schemas: FormSchema[] = [
{
field: 'name',
component: 'NInput',
label: '姓名',
labelMessage: '这是一个提示',
componentProps: {
placeholder: '请输入姓名',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ required: true, message: '请输入姓名', trigger: ['blur'] }],
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'type',
component: 'NSelect',
label: '类型',
componentProps: {
placeholder: '请选择类型',
options: [
{
label: '舒适性',
value: 1,
},
{
label: '经济性',
value: 2,
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeDate',
component: 'NDatePicker',
label: '预约时间',
componentProps: {
type: 'date',
clearable: true,
defaultValue: 1183135260000,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeTime',
component: 'NTimePicker',
label: '停留时间',
componentProps: {
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeProject',
component: 'NCheckbox',
label: '预约项目',
componentProps: {
placeholder: '请选择预约项目',
options: [
{
label: '种牙',
value: 1,
},
{
label: '补牙',
value: 2,
},
{
label: '根管',
value: 3,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeSource',
component: 'NRadioGroup',
label: '来源',
componentProps: {
options: [
{
label: '网上',
value: 1,
},
{
label: '门店',
value: 2,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
label: '状态',
//插槽
slot: 'statusSlot',
},
];
export default defineComponent({
components: { BasicForm },
setup() {
const formRef: any = ref(null);
const message = useMessage();
function handleSubmit(values: Recordable) {
console.log(values);
message.success(JSON.stringify(values));
}
function handleReset(values: Recordable) {
console.log(values);
}
return {
schemas,
formRef,
handleSubmit,
handleReset,
};
},
});
</script>
<style lang="less" scoped>
.BasicForm {
width: 550px;
margin: 0 auto;
overflow: hidden;
padding-top: 20px;
}
</style>

View File

@@ -0,0 +1,214 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="基础表单"> useForm 表单用于向用户收集表单信息 </n-card>
</div>
<n-card :bordered="false" class="proCard mt-4">
<div class="BasicForm">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
</div>
</n-card>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { useMessage } from 'naive-ui';
const schemas: FormSchema[] = [
{
field: 'name',
component: 'NInput',
label: '姓名',
labelMessage: '这是一个提示',
giProps: {
span: 1,
},
componentProps: {
placeholder: '请输入姓名',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ required: true, message: '请输入姓名', trigger: ['blur'] }],
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'type',
component: 'NSelect',
label: '类型',
giProps: {
//span: 24,
},
componentProps: {
placeholder: '请选择类型',
options: [
{
label: '舒适性',
value: 1,
},
{
label: '经济性',
value: 2,
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeDate',
component: 'NDatePicker',
label: '预约时间',
giProps: {
//span: 24,
},
componentProps: {
type: 'date',
clearable: true,
defaultValue: 1183135260000,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeTime',
component: 'NTimePicker',
label: '停留时间',
giProps: {
//span: 24,
},
componentProps: {
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeProject',
component: 'NCheckbox',
label: '预约项目',
giProps: {
//span: 24,
},
componentProps: {
placeholder: '请选择预约项目',
options: [
{
label: '种牙',
value: 1,
},
{
label: '补牙',
value: 2,
},
{
label: '根管',
value: 3,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeSource',
component: 'NRadioGroup',
label: '来源',
giProps: {
//span: 24,
},
componentProps: {
options: [
{
label: '网上',
value: 1,
},
{
label: '门店',
value: 2,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
label: '状态',
giProps: {
//span: 24,
},
//插槽
slot: 'statusSlot',
},
];
export default defineComponent({
components: { BasicForm },
setup() {
const formRef: any = ref(null);
const message = useMessage();
const [register, { setFieldsValue }] = useForm({
gridProps: { cols: 1 },
collapsedRows: 3,
labelWidth: 120,
layout: 'horizontal',
submitButtonText: '提交预约',
schemas,
});
function setName() {
setFieldsValue({ name: '小马哥' });
}
function handleSubmit(values: Recordable) {
console.log(values);
message.success(JSON.stringify(values));
}
function handleReset(values: Recordable) {
console.log(values);
}
return {
register,
formRef,
handleSubmit,
handleReset,
setName,
};
},
});
</script>
<style lang="less" scoped>
.BasicForm {
width: 550px;
margin: 0 auto;
overflow: hidden;
padding-top: 20px;
}
</style>

View File

@@ -0,0 +1,306 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="模态框">
模态框用于向用户收集或展示信息Modal 采用 Dialog 预设扩展拖拽效果
<br />
以下是 useModal
方式ref方式也支持使用方式和其他组件一致modalRef.value.closeModal()
</n-card>
</div>
<n-card :bordered="false" class="proCard mt-4">
<n-alert title="Modal嵌套Form" type="info">
使用 useModal 进行弹窗展示和操作并演示了在Modal内和Form组件组合使用方法
</n-alert>
<n-divider />
<n-space>
<n-button type="primary" @click="showModal">打开Modal嵌套Form例子</n-button>
</n-space>
<n-divider />
<n-alert title="个性化轻量级" type="info">
使用 useModal 进行弹窗展示和操作自定义配置实现轻量级效果更多配置请参考文档
</n-alert>
<n-divider />
<n-space>
<n-button type="primary" @click="showLightModal">轻量级确认</n-button>
</n-space>
<n-divider />
<n-alert title="提示" type="info">
组件暴露了setProps 方法用于修改组件内部
Props比如标题具体参考UI框架文档DialogReactive Properties
</n-alert>
</n-card>
<basicModal @register="modalRegister" ref="modalRef" class="basicModal" @on-ok="okModal">
<template #default>
<BasicForm @register="register" @reset="handleReset" class="basicForm">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
</template>
</basicModal>
<basicModal
@register="lightModalRegister"
class="basicModalLight"
ref="modalRef"
@on-ok="lightOkModal"
>
<template #default>
<p class="text-gray-500" style="padding-left: 35px">一些对话框内容</p>
</template>
</basicModal>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, toRefs } from 'vue';
import { useMessage } from 'naive-ui';
import { basicModal, useModal } from '@/components/Modal';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
const schemas: FormSchema[] = [
{
field: 'name',
component: 'NInput',
label: '姓名',
labelMessage: '这是一个提示',
giProps: {
span: 1,
},
componentProps: {
placeholder: '请输入姓名',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ required: true, message: '请输入姓名', trigger: ['blur'] }],
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'type',
component: 'NSelect',
label: '类型',
giProps: {
//span: 24,
},
componentProps: {
placeholder: '请选择类型',
options: [
{
label: '舒适性',
value: 1,
},
{
label: '经济性',
value: 2,
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeDate',
component: 'NDatePicker',
label: '预约时间',
giProps: {
//span: 24,
},
componentProps: {
type: 'date',
clearable: true,
defaultValue: 1183135260000,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeTime',
component: 'NTimePicker',
label: '停留时间',
giProps: {
//span: 24,
},
componentProps: {
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeProject',
component: 'NCheckbox',
label: '预约项目',
giProps: {
//span: 24,
},
componentProps: {
placeholder: '请选择预约项目',
options: [
{
label: '种牙',
value: 1,
},
{
label: '补牙',
value: 2,
},
{
label: '根管',
value: 3,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeSource',
component: 'NRadioGroup',
label: '来源',
giProps: {
//span: 24,
},
componentProps: {
options: [
{
label: '网上',
value: 1,
},
{
label: '门店',
value: 2,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
label: '状态',
giProps: {
//span: 24,
},
//插槽
slot: 'statusSlot',
},
];
export default defineComponent({
components: { basicModal, BasicForm },
setup() {
const modalRef: any = ref(null);
const message = useMessage();
const [modalRegister, { openModal, closeModal, setSubLoading }] = useModal({
title: '新增预约',
});
const [
lightModalRegister,
{
openModal: lightOpenModal,
closeModal: lightCloseModal,
setSubLoading: lightSetSubLoading,
},
] = useModal({
title: '确认对话框',
showIcon: true,
type: 'warning',
closable: false,
maskClosable: true,
});
const [register, { submit }] = useForm({
gridProps: { cols: 1 },
collapsedRows: 3,
labelWidth: 120,
layout: 'horizontal',
submitButtonText: '提交预约',
showActionButtonGroup: false,
schemas,
});
const state = reactive({
formValue: {
name: '小马哥',
},
});
async function okModal() {
const formRes = await submit();
if (formRes) {
closeModal();
message.success('提交成功');
} else {
message.error('验证失败,请填写完整信息');
setSubLoading(false);
}
}
function lightOkModal() {
lightCloseModal();
lightSetSubLoading();
}
function showLightModal() {
lightOpenModal();
}
function showModal() {
openModal();
}
function handleReset(values: Recordable) {
console.log(values);
}
return {
...toRefs(state),
modalRef,
register,
modalRegister,
lightModalRegister,
handleReset,
showModal,
okModal,
lightOkModal,
showLightModal,
};
},
});
</script>
<style lang="less">
.basicForm {
padding-top: 20px;
}
.n-dialog.basicModal {
width: 640px;
}
.n-dialog.basicModalLight {
width: 416px;
padding-top: 26px;
}
</style>

View File

@@ -5,10 +5,12 @@ export const columns = [
{ {
title: 'id', title: 'id',
key: 'id', key: 'id',
width: 100,
}, },
{ {
title: '编码', title: '编码',
key: 'no', key: 'no',
width: 100,
}, },
{ {
title: '名称', title: '名称',
@@ -22,6 +24,7 @@ export const columns = [
{ {
title: '头像', title: '头像',
key: 'avatar', key: 'avatar',
width: 100,
render(row) { render(row) {
return h(NAvatar, { return h(NAvatar, {
size: 48, size: 48,
@@ -47,29 +50,33 @@ export const columns = [
}, },
edit: true, edit: true,
width: 200, width: 200,
ellipsis: false,
}, },
{ {
title: '开始日期', title: '开始日期',
key: 'beginTime', key: 'beginTime',
edit: true, edit: true,
width: 250, width: 160,
editComponent: 'NDatePicker', editComponent: 'NDatePicker',
editComponentProps: { editComponentProps: {
type: 'datetime', type: 'datetime',
format: 'yyyy-MM-dd HH:mm:ss', format: 'yyyy-MM-dd HH:mm:ss',
}, },
ellipsis: false,
}, },
{ {
title: '结束日期', title: '结束日期',
key: 'endTime', key: 'endTime',
width: 200, width: 160,
}, },
{ {
title: '创建时间', title: '创建时间',
key: 'date', key: 'date',
width: 160,
}, },
{ {
title: '停留时间', title: '停留时间',
key: 'time', key: 'time',
width: 80,
}, },
]; ];

View File

@@ -8,6 +8,7 @@
:row-key="(row) => row.id" :row-key="(row) => row.id"
ref="actionRef" ref="actionRef"
:actionColumn="actionColumn" :actionColumn="actionColumn"
:scroll-x="1360"
@update:checked-row-keys="onCheckedRow" @update:checked-row-keys="onCheckedRow"
> >
<template #toolbar> <template #toolbar>
@@ -38,7 +39,7 @@
actionColumn: { actionColumn: {
width: 150, width: 150,
title: '操作', title: '操作',
dataIndex: 'action', key: 'action',
fixed: 'right', fixed: 'right',
align: 'center', align: 'center',
render(record) { render(record) {

View File

@@ -5,19 +5,22 @@ export const columns = [
{ {
title: 'id', title: 'id',
key: 'id', key: 'id',
width: 100,
}, },
{ {
title: '编码', title: '编码',
key: 'no', key: 'no',
width: 100,
}, },
{ {
title: '名称', title: '名称',
key: 'name', key: 'name',
width: 200, width: 100,
}, },
{ {
title: '头像', title: '头像',
key: 'avatar', key: 'avatar',
width: 100,
render(row) { render(row) {
return h(NAvatar, { return h(NAvatar, {
size: 48, size: 48,
@@ -28,21 +31,22 @@ export const columns = [
{ {
title: '地址', title: '地址',
key: 'address', key: 'address',
width: 200, width: 150,
}, },
{ {
title: '开始日期', title: '开始日期',
key: 'beginTime', key: 'beginTime',
width: 200, width: 160,
}, },
{ {
title: '结束日期', title: '结束日期',
key: 'endTime', key: 'endTime',
width: 200, width: 160,
}, },
{ {
title: '状态', title: '状态',
key: 'status', key: 'status',
width: 100,
render(row) { render(row) {
return h( return h(
NTag, NTag,
@@ -58,9 +62,11 @@ export const columns = [
{ {
title: '创建时间', title: '创建时间',
key: 'date', key: 'date',
width: 160,
}, },
{ {
title: '停留时间', title: '停留时间',
key: 'time', key: 'time',
width: 80,
}, },
]; ];

View File

@@ -7,10 +7,10 @@
:request="loadDataTable" :request="loadDataTable"
:row-key="(row) => row.id" :row-key="(row) => row.id"
ref="actionRef" ref="actionRef"
:actionColumn="actionColumn"
@edit-end="editEnd" @edit-end="editEnd"
@edit-change="onEditChange" @edit-change="onEditChange"
@update:checked-row-keys="onCheckedRow" @update:checked-row-keys="onCheckedRow"
:scroll-x="1360"
> >
<template #toolbar> <template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button> <n-button type="primary" @click="reloadTable">刷新数据</n-button>
@@ -35,19 +35,6 @@
pageSize: 5, pageSize: 5,
name: 'xiaoMa', name: 'xiaoMa',
}, },
actionColumn: {
width: 150,
title: '操作',
dataIndex: 'action',
fixed: 'right',
align: 'center',
render(record) {
return h(TableAction, {
style: 'button',
actions: createActions(record),
});
},
},
}); });
function handleEdit(record) { function handleEdit(record) {
@@ -61,7 +48,7 @@
} }
function onEditChange({ column, value, record }) { function onEditChange({ column, value, record }) {
if (column.dataIndex === 'id') { if (column.key === 'id') {
record.editValueRefs.name4.value = `${value}`; record.editValueRefs.name4.value = `${value}`;
} }
console.log(column, value, record); console.log(column, value, record);

View File

@@ -11,6 +11,7 @@
@edit-end="editEnd" @edit-end="editEnd"
@edit-change="onEditChange" @edit-change="onEditChange"
@update:checked-row-keys="onCheckedRow" @update:checked-row-keys="onCheckedRow"
:scroll-x="1510"
> >
<template #toolbar> <template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button> <n-button type="primary" @click="reloadTable">刷新数据</n-button>
@@ -61,7 +62,7 @@
} }
function onEditChange({ column, value, record }) { function onEditChange({ column, value, record }) {
if (column.dataIndex === 'id') { if (column.key === 'id') {
record.editValueRefs.name4.value = `${value}`; record.editValueRefs.name4.value = `${value}`;
} }
console.log(column, value, record); console.log(column, value, record);

View File

@@ -5,10 +5,12 @@ export const columns = [
{ {
title: 'id', title: 'id',
key: 'id', key: 'id',
width: 100,
}, },
{ {
title: '编码', title: '编码',
key: 'no', key: 'no',
width: 100,
}, },
{ {
title: '名称', title: '名称',
@@ -23,6 +25,7 @@ export const columns = [
{ {
title: '头像', title: '头像',
key: 'avatar', key: 'avatar',
width: 100,
render(row) { render(row) {
return h(NAvatar, { return h(NAvatar, {
size: 48, size: 48,
@@ -49,23 +52,25 @@ export const columns = [
}, },
edit: true, edit: true,
width: 200, width: 200,
ellipsis: false,
}, },
{ {
title: '开始日期', title: '开始日期',
key: 'beginTime', key: 'beginTime',
editRow: true, editRow: true,
edit: true, edit: true,
width: 250, width: 160,
editComponent: 'NDatePicker', editComponent: 'NDatePicker',
editComponentProps: { editComponentProps: {
type: 'datetime', type: 'datetime',
format: 'yyyy-MM-dd HH:mm:ss', format: 'yyyy-MM-dd HH:mm:ss',
}, },
ellipsis: false,
}, },
{ {
title: '结束日期', title: '结束日期',
key: 'endTime', key: 'endTime',
width: 200, width: 160,
}, },
{ {
title: '状态', title: '状态',
@@ -81,9 +86,11 @@ export const columns = [
{ {
title: '创建时间', title: '创建时间',
key: 'date', key: 'date',
width: 160,
}, },
{ {
title: '停留时间', title: '停留时间',
key: 'time', key: 'time',
width: 80,
}, },
]; ];

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="console"> <div class="console">
<!--数据卡片--> <!--数据卡片-->
<n-grid cols="1 s:2 m:3 l:4 xl:4 2xl:4" responsive="screen" :x-gap="12" :y-gap="8" :cols="4"> <n-grid cols="1 s:2 m:3 l:4 xl:4 2xl:4" responsive="screen" :x-gap="12" :y-gap="8">
<n-grid-item> <n-grid-item>
<NCard <NCard
title="访问量" title="访问量"
@@ -19,14 +19,14 @@
<div class="text-sn"> <div class="text-sn">
日同比 日同比
<CountTo :startVal="1" suffix="%" :endVal="visits.rise" /> <CountTo :startVal="1" suffix="%" :endVal="visits.rise" />
<n-icon size="12" style="color: #00ff6f"> <n-icon size="12" color="#00ff6f">
<component is="CaretUpOutlined" /> <component is="CaretUpOutlined" />
</n-icon> </n-icon>
</div> </div>
<div class="text-sn"> <div class="text-sn">
周同比 周同比
<CountTo :startVal="1" suffix="%" :endVal="visits.decline" /> <CountTo :startVal="1" suffix="%" :endVal="visits.decline" />
<n-icon size="12" style="color: #ffde66"> <n-icon size="12" color="#ffde66">
<component is="CaretDownOutlined" /> <component is="CaretDownOutlined" />
</n-icon> </n-icon>
</div> </div>
@@ -91,14 +91,14 @@
<div class="text-sn"> <div class="text-sn">
日同比 日同比
<CountTo :startVal="1" suffix="%" :endVal="orderLarge.rise" /> <CountTo :startVal="1" suffix="%" :endVal="orderLarge.rise" />
<n-icon size="12" style="color: #00ff6f"> <n-icon size="12" color="#00ff6f">
<component is="CaretUpOutlined" /> <component is="CaretUpOutlined" />
</n-icon> </n-icon>
</div> </div>
<div class="text-sn"> <div class="text-sn">
周同比 周同比
<CountTo :startVal="1" suffix="%" :endVal="orderLarge.rise" /> <CountTo :startVal="1" suffix="%" :endVal="orderLarge.rise" />
<n-icon size="12" style="color: #ffde66"> <n-icon size="12" color="#ffde66">
<component is="CaretDownOutlined" /> <component is="CaretDownOutlined" />
</n-icon> </n-icon>
</div> </div>
@@ -130,14 +130,14 @@
<div class="text-sn"> <div class="text-sn">
月同比 月同比
<CountTo :startVal="1" suffix="%" :endVal="volume.rise" /> <CountTo :startVal="1" suffix="%" :endVal="volume.rise" />
<n-icon size="12" style="color: #00ff6f"> <n-icon size="12" color="#00ff6f">
<component is="CaretUpOutlined" /> <component is="CaretUpOutlined" />
</n-icon> </n-icon>
</div> </div>
<div class="text-sn"> <div class="text-sn">
月同比 月同比
<CountTo :startVal="1" suffix="%" :endVal="volume.decline" /> <CountTo :startVal="1" suffix="%" :endVal="volume.decline" />
<n-icon size="12" style="color: #ffde66"> <n-icon size="12" color="#ffde66">
<component is="CaretDownOutlined" /> <component is="CaretDownOutlined" />
</n-icon> </n-icon>
</div> </div>
@@ -156,14 +156,14 @@
<!--导航卡片--> <!--导航卡片-->
<div class="mt-4"> <div class="mt-4">
<n-grid cols="1 s:2 m:3 l:8 xl:8 2xl:8" responsive="screen" :x-gap="16" :y-gap="8" :cols="8"> <n-grid cols="1 s:2 m:3 l:8 xl:8 2xl:8" responsive="screen" :x-gap="16" :y-gap="8">
<n-grid-item v-for="(item, index) in iconList" :key="index"> <n-grid-item v-for="(item, index) in iconList" :key="index">
<NCard content-style="padding-top: 0;" size="small" :bordered="false"> <NCard content-style="padding-top: 0;" size="small" :bordered="false">
<template #footer> <template #footer>
<div class="cursor-pointer"> <div class="cursor-pointer">
<p class="flex justify-center"> <p class="flex justify-center">
<span> <span>
<n-icon :size="item.size" class="flex-1" :style="{ color: `${item.color}` }"> <n-icon :size="item.size" class="flex-1" :color="item.color">
<component :is="item.icon" v-on="item.eventObject || {}" /> <component :is="item.icon" v-on="item.eventObject || {}" />
</n-icon> </n-icon>
</span> </span>

View File

@@ -33,14 +33,7 @@
</n-grid> </n-grid>
</n-card> </n-card>
</div> </div>
<n-grid <n-grid class="mt-4" cols="2 s:1 m:1 l:2 xl:2 2xl:2" responsive="screen" :x-gap="12" :y-gap="9">
class="mt-4"
cols="2 s:1 m:1 l:2 xl:2 2xl:2"
responsive="screen"
:x-gap="12"
:y-gap="9"
:cols="2"
>
<n-gi> <n-gi>
<n-card <n-card
:segmented="{ content: 'hard' }" :segmented="{ content: 'hard' }"
@@ -75,7 +68,7 @@
> >
<div class="flex"> <div class="flex">
<span> <span>
<n-icon size="30" style="color: #42b983"> <n-icon size="30" color="#42b983">
<LogoVue /> <LogoVue />
</n-icon> </n-icon>
</span> </span>
@@ -91,7 +84,7 @@
> >
<div class="flex"> <div class="flex">
<span> <span>
<n-icon size="30" style="color: #e44c27"> <n-icon size="30" color="#e44c27">
<Html5Outlined /> <Html5Outlined />
</n-icon> </n-icon>
</span> </span>
@@ -107,7 +100,7 @@
> >
<div class="flex"> <div class="flex">
<span> <span>
<n-icon size="30" style="color: #dd0031"> <n-icon size="30" color="#dd0031">
<LogoAngular /> <LogoAngular />
</n-icon> </n-icon>
</span> </span>
@@ -123,7 +116,7 @@
> >
<div class="flex"> <div class="flex">
<span> <span>
<n-icon size="30" style="color: #61dafb"> <n-icon size="30" color="#61dafb">
<LogoReact /> <LogoReact />
</n-icon> </n-icon>
</span> </span>
@@ -238,7 +231,7 @@
<n-card size="small" class="cursor-pointer project-card-item" hoverable> <n-card size="small" class="cursor-pointer project-card-item" hoverable>
<div class="flex flex-col justify-center text-gray-500"> <div class="flex flex-col justify-center text-gray-500">
<span class="text-center"> <span class="text-center">
<n-icon size="30" style="color: #68c755"> <n-icon size="30" color="#68c755">
<DashboardOutlined /> <DashboardOutlined />
</n-icon> </n-icon>
</span> </span>
@@ -248,7 +241,7 @@
<n-card size="small" class="cursor-pointer project-card-item" hoverable> <n-card size="small" class="cursor-pointer project-card-item" hoverable>
<div class="flex flex-col justify-center text-gray-500"> <div class="flex flex-col justify-center text-gray-500">
<span class="text-center"> <span class="text-center">
<n-icon size="30" style="color: #fab251"> <n-icon size="30" color="#fab251">
<ProfileOutlined /> <ProfileOutlined />
</n-icon> </n-icon>
</span> </span>
@@ -258,7 +251,7 @@
<n-card size="small" class="cursor-pointer project-card-item" hoverable> <n-card size="small" class="cursor-pointer project-card-item" hoverable>
<div class="flex flex-col justify-center text-gray-500"> <div class="flex flex-col justify-center text-gray-500">
<span class="text-center"> <span class="text-center">
<n-icon size="30" style="color: #1890ff"> <n-icon size="30" color="#1890ff">
<FileProtectOutlined /> <FileProtectOutlined />
</n-icon> </n-icon>
</span> </span>
@@ -268,7 +261,7 @@
<n-card size="small" class="cursor-pointer project-card-item" hoverable> <n-card size="small" class="cursor-pointer project-card-item" hoverable>
<div class="flex flex-col justify-center text-gray-500"> <div class="flex flex-col justify-center text-gray-500">
<span class="text-center"> <span class="text-center">
<n-icon size="30" style="color: #f06b96"> <n-icon size="30" color="#f06b96">
<ApartmentOutlined /> <ApartmentOutlined />
</n-icon> </n-icon>
</span> </span>
@@ -278,7 +271,7 @@
<n-card size="small" class="cursor-pointer project-card-item" hoverable> <n-card size="small" class="cursor-pointer project-card-item" hoverable>
<div class="flex flex-col justify-center text-gray-500"> <div class="flex flex-col justify-center text-gray-500">
<span class="text-center"> <span class="text-center">
<n-icon size="30" style="color: #7238d1"> <n-icon size="30" color="#7238d1">
<SettingOutlined /> <SettingOutlined />
</n-icon> </n-icon>
</span> </span>

View File

@@ -29,9 +29,9 @@
<style lang="less" scoped> <style lang="less" scoped>
.page-container { .page-container {
width: 100%; width: 100%;
background-color: white;
border-radius: 4px; border-radius: 4px;
padding: 50px 0; padding: 50px 0;
height: 100vh;
.text-center { .text-center {
h1 { h1 {

View File

@@ -29,9 +29,9 @@
<style lang="less" scoped> <style lang="less" scoped>
.page-container { .page-container {
width: 100%; width: 100%;
background-color: white;
border-radius: 4px; border-radius: 4px;
padding: 50px 0; padding: 50px 0;
height: 100vh;
.text-center { .text-center {
h1 { h1 {

View File

@@ -29,9 +29,9 @@
<style lang="less" scoped> <style lang="less" scoped>
.page-container { .page-container {
width: 100%; width: 100%;
background-color: white;
border-radius: 4px; border-radius: 4px;
padding: 50px 0; padding: 50px 0;
height: 100vh;
.text-center { .text-center {
h1 { h1 {

View File

@@ -5,7 +5,7 @@
:rules="rules" :rules="rules"
label-placement="left" label-placement="left"
ref="form1Ref" ref="form1Ref"
style="max-width: 500px; margin: 40px auto 0" style="max-width: 500px; margin: 40px auto 0 80px"
> >
<n-form-item label="付款账户" path="myAccount"> <n-form-item label="付款账户" path="myAccount">
<n-select <n-select

View File

@@ -5,7 +5,7 @@
:rules="rules" :rules="rules"
label-placement="left" label-placement="left"
ref="form2Ref" ref="form2Ref"
style="max-width: 500px; margin: 40px auto 0" style="max-width: 500px; margin: 40px auto 0 80px"
> >
<n-form-item label="付款账户" path="myAccount"> <n-form-item label="付款账户" path="myAccount">
<span>NaiveUiAdmin@163.com</span> <span>NaiveUiAdmin@163.com</span>

View File

@@ -6,11 +6,11 @@
</n-card> </n-card>
</div> </div>
<n-card :bordered="false" class="proCard mt-4"> <n-card :bordered="false" class="proCard mt-4">
<n-space vertical class="steps"> <n-space vertical class="steps" justify="center">
<n-steps :current="currentTab" :status="currentStatus"> <n-steps :current="currentTab" :status="currentStatus">
<n-step title="填写转账信息" description="确保填写正确" /> <n-step title="填写转账信息" description="确保填写正确" />
<n-step title="确认转账信息" description="确认转账信息" /> <n-step title="确认转账信息" description="确认转账信息" />
<n-step title="完成" description="恭喜您,转账成功" /> <n-step title="完成转账" description="恭喜您,转账成功" />
</n-steps> </n-steps>
<step1 v-if="currentTab === 1" @nextStep="nextStep" /> <step1 v-if="currentTab === 1" @nextStep="nextStep" />
<step2 v-if="currentTab === 2" @nextStep="nextStep" @prevStep="prevStep" /> <step2 v-if="currentTab === 2" @nextStep="nextStep" @prevStep="prevStep" />

1
src/views/frame/docs.vue Normal file
View File

@@ -0,0 +1 @@
<template> 项目文档 </template>

View File

@@ -0,0 +1,72 @@
<template>
<n-spin :show="loading">
<div class="frame">
<iframe :src="frameSrc" class="frame-iframe" ref="frameRef"></iframe>
</div>
</n-spin>
</template>
<script lang="ts">
import { defineComponent, ref, unref, onMounted, nextTick } from 'vue';
import { useRoute } from 'vue-router';
export default defineComponent({
name: 'IFrame',
setup() {
const currentRoute = useRoute();
const loading = ref(false);
const frameRef = ref<HTMLFrameElement | null>(null);
const frameSrc = ref<string>('');
if (unref(currentRoute.meta)?.frameSrc) {
frameSrc.value = unref(currentRoute.meta)?.frameSrc as string;
}
function hideLoading() {
loading.value = false;
}
function init() {
nextTick(() => {
const iframe = unref(frameRef);
if (!iframe) return;
const _frame = iframe as any;
if (_frame.attachEvent) {
_frame.attachEvent('onload', () => {
hideLoading();
});
} else {
iframe.onload = () => {
hideLoading();
};
}
});
}
onMounted(() => {
loading.value = true;
init();
});
return {
loading,
frameRef,
frameSrc,
};
},
});
</script>
<style lang="less" scoped>
.frame {
width: 100%;
height: 100vh;
&-iframe {
width: 100%;
height: 100%;
overflow: hidden;
border: 0;
box-sizing: border-box;
}
}
</style>

View File

@@ -5,14 +5,17 @@ export const columns = [
{ {
title: 'id', title: 'id',
key: 'id', key: 'id',
width: 100,
}, },
{ {
title: '名称', title: '名称',
key: 'name', key: 'name',
width: 100,
}, },
{ {
title: '头像', title: '头像',
key: 'avatar', key: 'avatar',
width: 100,
render(row) { render(row) {
return h(NAvatar, { return h(NAvatar, {
size: 48, size: 48,
@@ -27,48 +30,21 @@ export const columns = [
ifShow: (_column) => { ifShow: (_column) => {
return true; // 根据业务控制是否显示 return true; // 根据业务控制是否显示
}, },
width: 150,
}, },
{ {
title: '开始日期', title: '开始日期',
key: 'beginTime', key: 'beginTime',
width: 160,
}, },
{ {
title: '结束日期', title: '结束日期',
key: 'endTime', key: 'endTime',
width: 160,
}, },
{ {
title: '创建时间', title: '创建时间',
key: 'date', key: 'date',
width: 100,
}, },
// {
// title: '操作',
// key: 'actions',
// width: 150,
// //简单写一下例子,不建议这么写,过段时间,这里封二次封装
// render() {
// return [
// h(
// NButton,
// {
// size: 'small',
// type: 'error',
// style: 'margin-right:10px',
// onClick: () => {
// }
// },
// { default: () => '删除' }
// ),
// h(
// NButton,
// {
// size: 'small',
// onClick: () => {
//
// }
// },
// { default: () => '编辑' }
// )
// ]
// }
// }
]; ];

View File

@@ -1,5 +1,11 @@
<template> <template>
<n-card :bordered="false" class="proCard"> <n-card :bordered="false" class="proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable <BasicTable
:columns="columns" :columns="columns"
:request="loadDataTable" :request="loadDataTable"
@@ -7,6 +13,7 @@
ref="actionRef" ref="actionRef"
:actionColumn="actionColumn" :actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow" @update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
> >
<template #tableTitle> <template #tableTitle>
<n-button type="primary" @click="addTable"> <n-button type="primary" @click="addTable">
@@ -22,10 +29,6 @@
<template #toolbar> <template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button> <n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template> </template>
<template #action>
<TableAction />
</template>
</BasicTable> </BasicTable>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="新建"> <n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="新建">
@@ -59,9 +62,10 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, toRefs, ref, h } from 'vue'; import { defineComponent, h, reactive, ref, toRefs } from 'vue';
import { useMessage } from 'naive-ui'; import { useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table'; import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getTableList } from '@/api/table/list'; import { getTableList } from '@/api/table/list';
import { columns } from './columns'; import { columns } from './columns';
import { PlusOutlined } from '@vicons/antd'; import { PlusOutlined } from '@vicons/antd';
@@ -86,8 +90,133 @@
}, },
}; };
const schemas: FormSchema[] = [
{
field: 'name',
labelMessage: '这是一个提示',
component: 'NInput',
label: '姓名',
componentProps: {
placeholder: '请输入姓名',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ required: true, message: '请输入姓名', trigger: ['blur'] }],
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'type',
component: 'NSelect',
label: '类型',
componentProps: {
placeholder: '请选择类型',
options: [
{
label: '舒适性',
value: 1,
},
{
label: '经济性',
value: 2,
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeDate',
component: 'NDatePicker',
label: '预约时间',
componentProps: {
type: 'date',
clearable: true,
defaultValue: 1183135260000,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeTime',
component: 'NTimePicker',
label: '停留时间',
componentProps: {
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
label: '状态',
//插槽
slot: 'statusSlot',
},
{
field: 'makeProject',
component: 'NCheckbox',
label: '预约项目',
componentProps: {
placeholder: '请选择预约项目',
options: [
{
label: '种牙',
value: 1,
},
{
label: '补牙',
value: 2,
},
{
label: '根管',
value: 3,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeSource',
component: 'NRadioGroup',
label: '来源',
componentProps: {
options: [
{
label: '网上',
value: 1,
},
{
label: '门店',
value: 2,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
];
export default defineComponent({ export default defineComponent({
components: { BasicTable, PlusOutlined, TableAction }, // eslint-disable-next-line vue/no-unused-components
components: { BasicTable, PlusOutlined, TableAction, BasicForm },
setup() { setup() {
const router = useRouter(); const router = useRouter();
const formRef: any = ref(null); const formRef: any = ref(null);
@@ -96,22 +225,18 @@
const state = reactive({ const state = reactive({
showModal: false, showModal: false,
formBtnLoading: false, formBtnLoading: false,
formParams: { formParams: {},
name: '',
address: '',
date: [],
},
params: { params: {
pageSize: 5, pageSize: 5,
name: 'xiaoMa', name: 'xiaoMa',
}, },
actionColumn: { actionColumn: {
width: 250, width: 220,
title: '操作', title: '操作',
dataIndex: 'action', key: 'action',
fixed: 'right', fixed: 'right',
render(record) { render(record) {
return h(TableAction, { return h(TableAction as any, {
style: 'button', style: 'button',
actions: [ actions: [
{ {
@@ -159,13 +284,22 @@
}, },
}); });
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function addTable() { function addTable() {
state.showModal = true; state.showModal = true;
} }
const loadDataTable = async (params) => { const loadDataTable = async (res) => {
const data = await getTableList(params); let params = {
return data; ...res,
...state.formParams,
};
return await getTableList(params);
}; };
function onCheckedRow(rowKeys) { function onCheckedRow(rowKeys) {
@@ -208,12 +342,23 @@
message.info('点击了删除'); message.info('点击了删除');
} }
function handleSubmit(values: Recordable) {
console.log(values);
state.formParams = values;
reloadTable();
}
function handleReset(values: Recordable) {
console.log(values);
}
return { return {
...toRefs(state), ...toRefs(state),
formRef, formRef,
columns, columns,
rules, rules,
actionRef, actionRef,
register,
confirmForm, confirmForm,
loadDataTable, loadDataTable,
onCheckedRow, onCheckedRow,
@@ -222,6 +367,8 @@
handleEdit, handleEdit,
handleDelete, handleDelete,
handleOpen, handleOpen,
handleSubmit,
handleReset,
}; };
}, },
}); });

View File

@@ -116,7 +116,7 @@
actionColumn: { actionColumn: {
width: 250, width: 250,
title: '操作', title: '操作',
dataIndex: 'action', key: 'action',
fixed: 'right', fixed: 'right',
render(record) { render(record) {
return h(TableAction, { return h(TableAction, {

View File

@@ -16,18 +16,9 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"lib": [ "lib": ["dom", "esnext"],
"dom", "types": ["vite/client", "jest"],
"esnext" "typeRoots": ["./node_modules/@types/", "./types"],
],
"types": [
"vite/client",
"jest"
],
"typeRoots": [
"./node_modules/@types/",
"./types"
],
"noImplicitAny": false, "noImplicitAny": false,
"skipLibCheck": true, "skipLibCheck": true,
"paths": { "paths": {
@@ -47,9 +38,5 @@
"mock/**/*.ts", "mock/**/*.ts",
"vite.config.ts" "vite.config.ts"
], ],
"exclude": [ "exclude": ["node_modules", "dist", "**/*.js"]
"node_modules",
"dist",
"**/*.js"
]
} }

1
types/config.d.ts vendored
View File

@@ -27,6 +27,7 @@ export interface ImenuSetting {
minMenuWidth: number; minMenuWidth: number;
menuWidth: number; menuWidth: number;
fixed: boolean; fixed: boolean;
mixMenu: boolean;
} }
export interface IcrumbsSetting { export interface IcrumbsSetting {

4
types/index.d.ts vendored
View File

@@ -26,3 +26,7 @@ declare interface ComponentElRef<T extends HTMLElement = HTMLDivElement> {
declare type ComponentRef<T extends HTMLElement = HTMLDivElement> = ComponentElRef<T> | null; declare type ComponentRef<T extends HTMLElement = HTMLDivElement> = ComponentElRef<T> | null;
declare type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T>; declare type ElRef<T extends HTMLElement = HTMLDivElement> = Nullable<T>;
export type DynamicProps<T> = {
[P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>;
};

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