mirror of
https://github.com/jekip/naive-ui-admin.git
synced 2026-02-04 21:52:27 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c5c52d9fa | ||
|
|
3e0b8efe7e | ||
|
|
450234e7ea | ||
|
|
5116c387d5 | ||
|
|
8a5f237630 | ||
|
|
1e3ccaa6dc | ||
|
|
98e1bf0227 | ||
|
|
6a290b314a | ||
|
|
58f0997fb6 | ||
|
|
e602fc50c0 | ||
|
|
81a3e6d970 | ||
|
|
0c709871f3 |
@@ -61,6 +61,7 @@ module.exports = defineConfig({
|
||||
'vue/singleline-html-element-content-newline': 'off',
|
||||
'vue/attribute-hyphenation': 'off',
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/script-setup-uses-vars': 'off',
|
||||
'vue/html-self-closing': [
|
||||
'error',
|
||||
{
|
||||
|
||||
41
CHANGELOG.md
41
CHANGELOG.md
@@ -1,4 +1,33 @@
|
||||
# 1.5 (2021-07-30)
|
||||
# 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
|
||||
- 修复表格列配置,拖拽时最后的操作列重复增加
|
||||
- 多标签页交互优化
|
||||
@@ -15,7 +44,7 @@
|
||||
- 本次更新,有破坏性更新,涉及文件重命名,增删调整
|
||||
|
||||
|
||||
# 1.4 (2021-07-21)
|
||||
# 1.4.0 (2021-07-21)
|
||||
### 🐛 Bug Fixes
|
||||
- vite降至2.3.6
|
||||
- 多标签页交互优化
|
||||
@@ -27,7 +56,7 @@
|
||||
- 持续更新更多实用组件及示例,感谢Star
|
||||
|
||||
|
||||
# 1.3 (2021-07-19)
|
||||
# 1.3.0 (2021-07-19)
|
||||
### 🐛 Bug Fixes
|
||||
- 修复多标签页左右切换按钮自适应展示
|
||||
- 修复登录页面出现多标签页
|
||||
@@ -40,7 +69,7 @@
|
||||
- 持续更新更多实用组件及示例,感谢Star
|
||||
|
||||
|
||||
# 1.2 (2021-07-16)
|
||||
# 1.2.0 (2021-07-16)
|
||||
### 🐛 Bug Fixes
|
||||
- 修复面包屑显示登录页面
|
||||
- 菜单支持只展开当前父级菜单
|
||||
@@ -54,7 +83,7 @@
|
||||
- 持续更新更多实用示例,同时也演示`Naive UI`使用方法
|
||||
|
||||
|
||||
# 1.1 (2021-07-15)
|
||||
# 1.1.0 (2021-07-15)
|
||||
- ### ✨ Features
|
||||
- 新增 `基础表单` 示例页面
|
||||
- 新增 `分步表单` 示例页面
|
||||
@@ -62,7 +91,7 @@
|
||||
- 持续更新更多实用示例,同时也演示`Naive UI`使用方法
|
||||
|
||||
|
||||
# 1.0 (2021-07-12)
|
||||
# 1.0.0 (2021-07-12)
|
||||
### 🐛 Bug Fixes
|
||||
- 修复页面切换面包屑未及时更新
|
||||
|
||||
|
||||
@@ -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 等前端前沿技术开发
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @param env
|
||||
*/
|
||||
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()
|
||||
.replace(/\s/g, '');
|
||||
};
|
||||
|
||||
@@ -18,19 +18,19 @@ function createConfig(
|
||||
}: { configName: string; config: any; configFileName?: string } = { configName: '', config: {} }
|
||||
) {
|
||||
try {
|
||||
const windowConf = `window.${ configName }`;
|
||||
const windowConf = `window.${configName}`;
|
||||
// Ensure that the variable will not be modified
|
||||
const configStr = `${ windowConf }=${ JSON.stringify(config) };
|
||||
Object.freeze(${ windowConf });
|
||||
Object.defineProperty(window, "${ configName }", {
|
||||
const configStr = `${windowConf}=${JSON.stringify(config)};
|
||||
Object.freeze(${windowConf});
|
||||
Object.defineProperty(window, "${configName}", {
|
||||
configurable: false,
|
||||
writable: false,
|
||||
});
|
||||
`.replace(/\s/g, '');
|
||||
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');
|
||||
} catch (error) {
|
||||
console.log(chalk.red('configuration file configuration file failed to package:\n' + error));
|
||||
|
||||
@@ -14,7 +14,7 @@ export const runBuild = async () => {
|
||||
await runBuildConfig();
|
||||
}
|
||||
|
||||
console.log(`✨ ${ chalk.cyan(`[${ pkg.name }]`) }` + ' - build successfully!');
|
||||
console.log(`✨ ${chalk.cyan(`[${pkg.name}]`)}` + ' - build successfully!');
|
||||
} catch (error) {
|
||||
console.log(chalk.red('vite build error:\n' + error));
|
||||
process.exit(1);
|
||||
|
||||
@@ -12,10 +12,10 @@ import { GLOB_CONFIG_FILE_NAME } from '../../constant';
|
||||
export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) {
|
||||
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 = () => {
|
||||
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({
|
||||
@@ -28,13 +28,13 @@ export function configHtmlPlugin(env: ViteEnv, isBuild: boolean) {
|
||||
// Embed the generated app.config.js file
|
||||
tags: isBuild
|
||||
? [
|
||||
{
|
||||
tag: 'script',
|
||||
attrs: {
|
||||
src: getAppConfigSrc(),
|
||||
{
|
||||
tag: 'script',
|
||||
attrs: {
|
||||
src: getAppConfigSrc(),
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
]
|
||||
: [],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ export function configStyleImportPlugin(isBuild: boolean) {
|
||||
libraryName: 'ant-design-vue',
|
||||
esModule: true,
|
||||
resolveStyle: (name) => {
|
||||
return `ant-design-vue/es/${ name }/style/index`;
|
||||
return `ant-design-vue/es/${name}/style/index`;
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -5,7 +5,7 @@ const tableList = (pageSize) => {
|
||||
const result: any[] = [];
|
||||
doCustomTimes(pageSize, () => {
|
||||
result.push({
|
||||
id: '@integer(10,100)',
|
||||
id: '@integer(10,999999)',
|
||||
beginTime: '@datetime',
|
||||
endTime: '@datetime',
|
||||
address: '@city()',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "naive-ui-admin",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.2",
|
||||
"author": {
|
||||
"name": "Ahjung",
|
||||
"email": "735878602@qq.com",
|
||||
@@ -30,7 +30,7 @@
|
||||
"@vueuse/core": "^5.0.3",
|
||||
"axios": "^0.21.1",
|
||||
"blueimp-md5": "^2.18.0",
|
||||
"dayjs": "^1.10.5",
|
||||
"date-fns": "^2.23.0",
|
||||
"echarts": "^5.1.2",
|
||||
"element-resize-detector": "^1.2.3",
|
||||
"lodash": "^4.17.21",
|
||||
@@ -38,7 +38,7 @@
|
||||
"makeit-captcha": "^1.2.5",
|
||||
"mitt": "^2.1.0",
|
||||
"mockjs": "^1.1.0",
|
||||
"naive-ui": "^2.15.11",
|
||||
"naive-ui": "^2.16.0",
|
||||
"pinia": "^2.0.0-beta.3",
|
||||
"qs": "^6.10.1",
|
||||
"vfonts": "^0.1.0",
|
||||
|
||||
@@ -15,6 +15,6 @@ module.exports = {
|
||||
requirePragma: false,
|
||||
proseWrap: 'never',
|
||||
htmlWhitespaceSensitivity: 'strict',
|
||||
endOfLine: 'lf',
|
||||
endOfLine: 'auto',
|
||||
rangeStart: 0,
|
||||
};
|
||||
|
||||
13
src/App.vue
13
src/App.vue
@@ -24,6 +24,7 @@
|
||||
import { useLockscreenStore } from '@/store/modules/lockscreen';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useDesignSettingStore } from '@/store/modules/designSetting';
|
||||
import { lighten } from '@/utils/index';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'App',
|
||||
@@ -35,14 +36,20 @@
|
||||
const isLock = computed(() => useLockscreen.isLock);
|
||||
const lockTime = computed(() => useLockscreen.lockTime);
|
||||
|
||||
/**
|
||||
* @type import('naive-ui').GlobalThemeOverrides
|
||||
*/
|
||||
const getThemeOverrides = computed(() => {
|
||||
const appTheme = designStore.appTheme;
|
||||
const lightenStr = lighten(designStore.appTheme, 6);
|
||||
return {
|
||||
common: {
|
||||
primaryColor: designStore.appTheme,
|
||||
primaryColorHover: '#57a3f3',
|
||||
primaryColor: appTheme,
|
||||
primaryColorHover: lightenStr,
|
||||
primaryColorPressed: lightenStr,
|
||||
},
|
||||
LoadingBar: {
|
||||
colorLoading: designStore.appTheme,
|
||||
colorLoading: appTheme,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
15
src/assets/images/nav-horizontal-mix.svg
Normal file
15
src/assets/images/nav-horizontal-mix.svg
Normal 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 |
4
src/components/Form/index.ts
Normal file
4
src/components/Form/index.ts
Normal 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';
|
||||
322
src/components/Form/src/BasicForm.vue
Normal file
322
src/components/Form/src/BasicForm.vue
Normal file
@@ -0,0 +1,322 @@
|
||||
<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="
|
||||
isInline &&
|
||||
getSchema.length > (getProps.gridProps?.cols || 0) &&
|
||||
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,
|
||||
};
|
||||
});
|
||||
|
||||
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>
|
||||
42
src/components/Form/src/helper.ts
Normal file
42
src/components/Form/src/helper.ts
Normal 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;
|
||||
}
|
||||
87
src/components/Form/src/hooks/useForm.ts
Normal file
87
src/components/Form/src/hooks/useForm.ts
Normal 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];
|
||||
}
|
||||
11
src/components/Form/src/hooks/useFormContext.ts
Normal file
11
src/components/Form/src/hooks/useFormContext.ts
Normal 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);
|
||||
}
|
||||
107
src/components/Form/src/hooks/useFormEvents.ts
Normal file
107
src/components/Form/src/hooks/useFormEvents.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
54
src/components/Form/src/hooks/useFormValues.ts
Normal file
54
src/components/Form/src/hooks/useFormValues.ts
Normal 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 };
|
||||
}
|
||||
82
src/components/Form/src/props.ts
Normal file
82
src/components/Form/src/props.ts
Normal 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,
|
||||
},
|
||||
};
|
||||
58
src/components/Form/src/types/form.ts
Normal file
58
src/components/Form/src/types/form.ts
Normal 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];
|
||||
28
src/components/Form/src/types/index.ts
Normal file
28
src/components/Form/src/types/index.ts
Normal 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';
|
||||
@@ -21,6 +21,7 @@
|
||||
:battery="battery"
|
||||
:battery-status="batteryStatus"
|
||||
:calc-discharging-time="calcDischargingTime"
|
||||
:calc-charging-time="calcChargingTime"
|
||||
/>
|
||||
|
||||
<div class="local-time">
|
||||
@@ -114,7 +115,7 @@
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const { battery, batteryStatus, calcDischargingTime } = useBattery();
|
||||
const { battery, batteryStatus, calcDischargingTime, calcChargingTime } = useBattery();
|
||||
const userInfo: object = userStore.getUserInfo || {};
|
||||
const username = userInfo['username'] || '';
|
||||
const state = reactive({
|
||||
@@ -176,6 +177,7 @@
|
||||
battery,
|
||||
batteryStatus,
|
||||
calcDischargingTime,
|
||||
calcChargingTime,
|
||||
onLockLogin,
|
||||
onLogin,
|
||||
goLogin,
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
剩余可使用时间:{{ calcDischargingTime }}
|
||||
</div>
|
||||
<span v-show="Number.isFinite(battery.chargingTime) && battery.chargingTime != 0">
|
||||
距离电池充满需要:{{ calcDischargingTime }}
|
||||
距离电池充满需要:{{ calcChargingTime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,6 +36,10 @@
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
calcChargingTime: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
batteryStatus: {
|
||||
// 电池状态
|
||||
type: String,
|
||||
@@ -51,12 +55,12 @@
|
||||
bottom: 20vh;
|
||||
left: 50vw;
|
||||
width: 300px;
|
||||
height: 400px;
|
||||
height: 500px;
|
||||
transform: translateX(-50%);
|
||||
|
||||
.number {
|
||||
position: absolute;
|
||||
top: 27%;
|
||||
top: 20%;
|
||||
z-index: 10;
|
||||
width: 300px;
|
||||
font-size: 32px;
|
||||
|
||||
3
src/components/Modal/index.ts
Normal file
3
src/components/Modal/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as basicModal } from './src/basicModal.vue';
|
||||
export { useModal } from './src/hooks/useModal';
|
||||
export * from './src/type';
|
||||
127
src/components/Modal/src/basicModal.vue
Normal file
127
src/components/Modal/src/basicModal.vue
Normal 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>
|
||||
58
src/components/Modal/src/hooks/useModal.ts
Normal file
58
src/components/Modal/src/hooks/useModal.ts
Normal 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];
|
||||
}
|
||||
30
src/components/Modal/src/props.ts
Normal file
30
src/components/Modal/src/props.ts
Normal 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',
|
||||
},
|
||||
};
|
||||
9
src/components/Modal/src/type/index.ts
Normal file
9
src/components/Modal/src/type/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @description: 弹窗对外暴露的方法
|
||||
*/
|
||||
export interface ReturnMethods {
|
||||
setProps: (props) => void;
|
||||
openModal: () => void;
|
||||
closeModal: () => void;
|
||||
setSubLoading: (status) => void;
|
||||
}
|
||||
@@ -73,7 +73,6 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { NDataTable } from 'naive-ui';
|
||||
import {
|
||||
ref,
|
||||
defineComponent,
|
||||
@@ -129,7 +128,6 @@
|
||||
QuestionCircleOutlined,
|
||||
},
|
||||
props: {
|
||||
...NDataTable.props, // 这里继承原 UI 组件的 props
|
||||
...basicProps,
|
||||
},
|
||||
emits: [
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
import { set, omit } from 'lodash-es';
|
||||
import { EventEnum } from '@/components/Table/src/componentMap';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { milliseconds } from 'date-fns';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EditableCell',
|
||||
@@ -108,10 +108,11 @@
|
||||
|
||||
let value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
|
||||
|
||||
if (component === 'NDatePicker') {
|
||||
value = dayjs(value).valueOf();
|
||||
if (isString(value) && component === 'NDatePicker') {
|
||||
value = milliseconds(value as Duration);
|
||||
} else if (isArray(value) && component === 'NDatePicker') {
|
||||
value = value.map((item) => milliseconds(item));
|
||||
}
|
||||
|
||||
const onEvent: any = editComponent ? EventEnum[editComponent] : undefined;
|
||||
|
||||
return {
|
||||
@@ -196,12 +197,12 @@
|
||||
}
|
||||
|
||||
//TODO 这里组件参数格式,和dayjs格式不一致
|
||||
if (component === 'NDatePicker') {
|
||||
let format = (props.column.editComponentProps?.format)
|
||||
.replace(/yyyy/g, 'YYYY')
|
||||
.replace(/dd/g, 'DD');
|
||||
currentValueRef.value = dayjs(currentValueRef.value).format(format);
|
||||
}
|
||||
// if (component === 'NDatePicker') {
|
||||
// let format = (props.column.editComponentProps?.format)
|
||||
// .replace(/yyyy/g, 'YYYY')
|
||||
// .replace(/dd/g, 'DD');
|
||||
// currentValueRef.value = dayjs(currentValueRef.value).format(format);
|
||||
// }
|
||||
|
||||
const onChange = props.column?.editComponentProps?.onChange;
|
||||
if (onChange && isFunction(onChange)) onChange(...arguments);
|
||||
|
||||
@@ -81,12 +81,14 @@
|
||||
<script lang="ts">
|
||||
import { ref, defineComponent, reactive, unref, toRaw, computed, toRefs, watchEffect } from 'vue';
|
||||
import { useTableContext } from '../../hooks/useTableContext';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import {
|
||||
SettingOutlined,
|
||||
DragOutlined,
|
||||
VerticalRightOutlined,
|
||||
VerticalLeftOutlined,
|
||||
} from '@vicons/antd';
|
||||
// @ts-ignore
|
||||
import Draggable from 'vuedraggable/src/vuedraggable';
|
||||
import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
|
||||
|
||||
@@ -107,7 +109,7 @@
|
||||
},
|
||||
setup() {
|
||||
const { getDarkTheme } = useDesignSetting();
|
||||
const table = useTableContext();
|
||||
const table: any = useTableContext();
|
||||
const columnsList = ref<Options[]>([]);
|
||||
const cacheColumnsList = ref<Options[]>([]);
|
||||
|
||||
@@ -135,8 +137,11 @@
|
||||
const checkList: any = columns.map((item) => item.key);
|
||||
state.checkList = checkList;
|
||||
state.defaultCheckList = checkList;
|
||||
columnsList.value = columns;
|
||||
cacheColumnsList.value = columns;
|
||||
const newColumns = columns.filter((item) => item.key != 'action' && item.title != '操作');
|
||||
if (!columnsList.value.length) {
|
||||
columnsList.value = cloneDeep(newColumns);
|
||||
cacheColumnsList.value = cloneDeep(newColumns);
|
||||
}
|
||||
}
|
||||
|
||||
//切换
|
||||
@@ -154,11 +159,11 @@
|
||||
|
||||
//获取
|
||||
function getColumns() {
|
||||
let newRet = [];
|
||||
let newRet: any[] = [];
|
||||
table.getColumns().forEach((item) => {
|
||||
newRet.push({ ...item });
|
||||
});
|
||||
return newRet.filter((item) => item.key != 'action' && item.title != '操作');
|
||||
return newRet;
|
||||
}
|
||||
|
||||
//重置
|
||||
|
||||
@@ -52,7 +52,7 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
|
||||
return hasPermission(column.auth) && isIfShow(column);
|
||||
})
|
||||
.map((column) => {
|
||||
const { edit, editRow } = column;
|
||||
const { edit } = column;
|
||||
if (edit) {
|
||||
column.render = renderEditCell(column);
|
||||
if (edit) {
|
||||
@@ -140,12 +140,12 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
|
||||
}
|
||||
|
||||
//更新原始数据单个字段
|
||||
function setCacheColumnsField(dataIndex: string | undefined, value: Partial<BasicColumn>) {
|
||||
if (!dataIndex || !value) {
|
||||
function setCacheColumnsField(key: string | undefined, value: Partial<BasicColumn>) {
|
||||
if (!key || !value) {
|
||||
return;
|
||||
}
|
||||
cacheColumns.forEach((item) => {
|
||||
if (item.key === dataIndex) {
|
||||
if (item.key === key) {
|
||||
Object.assign(item, value);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ export function useDataSource(
|
||||
try {
|
||||
setLoading(true);
|
||||
const { request, pagination }: any = unref(propsRef);
|
||||
|
||||
//组装分页信息
|
||||
const pageField = APISETTING.pageField;
|
||||
const sizeField = APISETTING.sizeField;
|
||||
@@ -74,10 +73,9 @@ export function useDataSource(
|
||||
|
||||
// 如果数据异常,需获取正确的页码再次执行
|
||||
if (resultTotal) {
|
||||
const currentTotalPage = Math.ceil(resultTotal / pageSize);
|
||||
if (page > currentTotalPage) {
|
||||
if (page > resultTotal) {
|
||||
setPagination({
|
||||
[pageField]: currentTotalPage,
|
||||
[pageField]: resultTotal,
|
||||
});
|
||||
fetch(opt);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { PropType } from 'vue';
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
import { BasicColumn } from './types/table';
|
||||
|
||||
import { NDataTable } from 'naive-ui';
|
||||
export const basicProps = {
|
||||
...NDataTable.props, // 这里继承原 UI 组件的 props
|
||||
title: {
|
||||
type: String,
|
||||
default: null,
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface BasicColumn extends TableBaseColumn {
|
||||
editValueMap?: (value: any) => string;
|
||||
onEditRow?: () => void;
|
||||
// 权限编码控制是否显示
|
||||
auth?: RoleEnum | RoleEnum[] | string | string[];
|
||||
auth?: string[];
|
||||
// 业务控制是否显示
|
||||
ifShow?: boolean | ((column: BasicColumn) => boolean);
|
||||
}
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, toRefs, reactive, computed } from 'vue';
|
||||
import { EyeOutlined, DeleteOutlined, PlusOutlined } from '@vicons/antd';
|
||||
import { NUpload } from 'naive-ui';
|
||||
import { basicProps } from './props';
|
||||
import { useMessage, useDialog } from 'naive-ui';
|
||||
import { ResultEnum } from '@/enums/httpEnum';
|
||||
@@ -85,7 +84,6 @@
|
||||
|
||||
components: { EyeOutlined, DeleteOutlined, PlusOutlined },
|
||||
props: {
|
||||
...NUpload.props, // 这里继承原 UI 组件的 props
|
||||
...basicProps,
|
||||
},
|
||||
emits: ['uploadChange', 'delete'],
|
||||
|
||||
@@ -33,6 +33,14 @@ export const useBattery = () => {
|
||||
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(() => {
|
||||
if (state.battery.charging && state.battery.level >= 100) {
|
||||
@@ -80,5 +88,6 @@ export const useBattery = () => {
|
||||
...toRefs(state),
|
||||
batteryStatus,
|
||||
calcDischargingTime,
|
||||
calcChargingTime,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -46,7 +46,11 @@
|
||||
<div class="drawer-setting-item-style align-items-top">
|
||||
<n-tooltip placement="top">
|
||||
<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>
|
||||
<span>左侧菜单模式</span>
|
||||
</n-tooltip>
|
||||
@@ -56,12 +60,30 @@
|
||||
<div class="drawer-setting-item-style">
|
||||
<n-tooltip placement="top">
|
||||
<template #trigger>
|
||||
<img src="~@/assets/images/nav-horizontal.svg" @click="togNavMode('horizontal')" />
|
||||
<img
|
||||
src="~@/assets/images/nav-horizontal.svg"
|
||||
alt="顶部菜单模式"
|
||||
@click="togNavMode('horizontal')"
|
||||
/>
|
||||
</template>
|
||||
<span>顶部菜单模式</span>
|
||||
</n-tooltip>
|
||||
<n-badge dot color="#19be6b" v-show="settingStore.navMode === 'horizontal'" />
|
||||
</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>
|
||||
|
||||
<n-divider title-placement="center">导航栏风格</n-divider>
|
||||
@@ -70,7 +92,11 @@
|
||||
<div class="drawer-setting-item-style align-items-top">
|
||||
<n-tooltip placement="top">
|
||||
<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>
|
||||
<span>暗色侧边栏</span>
|
||||
</n-tooltip>
|
||||
@@ -80,7 +106,11 @@
|
||||
<div class="drawer-setting-item-style">
|
||||
<n-tooltip placement="top">
|
||||
<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>
|
||||
<span>白色侧边栏</span>
|
||||
</n-tooltip>
|
||||
@@ -95,6 +125,7 @@
|
||||
<img
|
||||
src="~@/assets/images/header-theme-dark.svg"
|
||||
@click="togNavTheme('header-dark')"
|
||||
alt="暗色顶栏"
|
||||
/>
|
||||
</template>
|
||||
<span>暗色顶栏</span>
|
||||
@@ -105,6 +136,16 @@
|
||||
|
||||
<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-title"> 固定顶栏 </div>
|
||||
<div class="drawer-setting-item-action">
|
||||
@@ -312,6 +353,7 @@
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dark-switch .n-switch {
|
||||
::v-deep(.n-switch__rail) {
|
||||
background-color: #000e1c;
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
<template>
|
||||
<div class="layout-header">
|
||||
<!--顶部菜单-->
|
||||
<div class="layout-header-left" v-if="navMode === 'horizontal'">
|
||||
<AsideMenu v-model:collapsed="collapsed" :inverted="getInverted" mode="horizontal" />
|
||||
<div
|
||||
class="layout-header-left"
|
||||
v-if="navMode === 'horizontal' || (navMode === 'horizontal-mix' && mixMenu)"
|
||||
>
|
||||
<AsideMenu
|
||||
v-model:collapsed="collapsed"
|
||||
v-model:location="getMenuLocation"
|
||||
:inverted="getInverted"
|
||||
mode="horizontal"
|
||||
/>
|
||||
</div>
|
||||
<!--左侧菜单-->
|
||||
<div class="layout-header-left" v-else>
|
||||
@@ -161,6 +169,10 @@
|
||||
return ['light', 'header-dark'].includes(navTheme) ? props.inverted : !props.inverted;
|
||||
});
|
||||
|
||||
const mixMenu = computed(() => {
|
||||
return unref(getMenuSetting).mixMenu;
|
||||
});
|
||||
|
||||
const getChangeStyle = computed(() => {
|
||||
const { collapsed } = props;
|
||||
const { minMenuWidth, menuWidth }: any = unref(getMenuSetting);
|
||||
@@ -170,6 +182,10 @@
|
||||
};
|
||||
});
|
||||
|
||||
const getMenuLocation = computed(() => {
|
||||
return 'header';
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
@@ -314,6 +330,8 @@
|
||||
drawerSetting,
|
||||
openSetting,
|
||||
getInverted,
|
||||
getMenuLocation,
|
||||
mixMenu,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -330,11 +348,6 @@
|
||||
transition: all 0.2s ease-in-out;
|
||||
width: 100%;
|
||||
z-index: 11;
|
||||
//color: #fff;
|
||||
|
||||
//.n-icon {
|
||||
// color: #fff
|
||||
//}
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
@@ -344,10 +357,6 @@
|
||||
color: #515a6e;
|
||||
}
|
||||
|
||||
::v-deep(.n-breadcrumb .n-breadcrumb-item:last-child .n-breadcrumb-item__link) {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.n-breadcrumb {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
@@ -6,19 +6,19 @@
|
||||
:collapsed="collapsed"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="20"
|
||||
:indent="28"
|
||||
:indent="24"
|
||||
:expanded-keys="openKeys"
|
||||
v-model:value="selectedKeys"
|
||||
:value="getSelectedKeys"
|
||||
@update:value="clickMenuItem"
|
||||
@update:expanded-keys="menuExpanded"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<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 { useAsyncRouteStore } from '@/store/modules/asyncRoute';
|
||||
import { generatorMenu } from '@/utils/index';
|
||||
import { generatorMenu, generatorMenuMix } from '@/utils';
|
||||
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||
|
||||
export default defineComponent({
|
||||
@@ -34,6 +34,11 @@
|
||||
// 侧边栏菜单是否收起
|
||||
type: Boolean,
|
||||
},
|
||||
//位置
|
||||
location: {
|
||||
type: String,
|
||||
default: 'left',
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
// 当前路由
|
||||
@@ -41,6 +46,9 @@
|
||||
const router = useRouter();
|
||||
const asyncRouteStore = useAsyncRouteStore();
|
||||
const settingStore = useProjectSettingStore();
|
||||
const menus = ref<any[]>([]);
|
||||
const selectedKeys = ref<string>(currentRoute.name as string);
|
||||
const headerMenuSelectKey = ref<string>('');
|
||||
|
||||
// 获取当前打开的子菜单
|
||||
const matched = currentRoute.matched;
|
||||
@@ -49,23 +57,30 @@
|
||||
|
||||
const state = reactive({
|
||||
openKeys: getOpenKeys,
|
||||
selectedKeys: currentRoute.name,
|
||||
});
|
||||
|
||||
const inverted = computed(() => {
|
||||
return ['dark', 'header-dark'].includes(settingStore.navTheme);
|
||||
});
|
||||
|
||||
const menus = computed(() => {
|
||||
return generatorMenu(asyncRouteStore.getMenus);
|
||||
const getSelectedKeys = computed(() => {
|
||||
return props.location === 'left' ? unref(selectedKeys) : unref(headerMenuSelectKey);
|
||||
});
|
||||
|
||||
// 监听分割菜单
|
||||
watch(
|
||||
() => settingStore.menuSetting.mixMenu,
|
||||
() => {
|
||||
updateMenu();
|
||||
}
|
||||
);
|
||||
|
||||
// 监听菜单收缩状态
|
||||
watch(
|
||||
() => props.collapsed,
|
||||
(newVal) => {
|
||||
state.openKeys = newVal ? [] : getOpenKeys;
|
||||
state.selectedKeys = currentRoute.name;
|
||||
selectedKeys.value = currentRoute.name as string;
|
||||
}
|
||||
);
|
||||
|
||||
@@ -73,12 +88,26 @@
|
||||
watch(
|
||||
() => currentRoute.fullPath,
|
||||
() => {
|
||||
updateMenu();
|
||||
const matched = currentRoute.matched;
|
||||
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) {
|
||||
if (/http(s)?:/.test(key)) {
|
||||
@@ -101,17 +130,24 @@
|
||||
if (!key) return false;
|
||||
const subRouteChildren: string[] = [];
|
||||
for (const { children, key } of unref(menus)) {
|
||||
if (children && children.length > 0) {
|
||||
if (children && children.length) {
|
||||
subRouteChildren.push(key as string);
|
||||
}
|
||||
}
|
||||
return subRouteChildren.includes(key);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
updateMenu();
|
||||
});
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
inverted,
|
||||
menus,
|
||||
selectedKeys,
|
||||
headerMenuSelectKey,
|
||||
getSelectedKeys,
|
||||
clickMenuItem,
|
||||
menuExpanded,
|
||||
};
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
import { renderIcon } from '@/utils/index';
|
||||
import elementResizeDetectorMaker from 'element-resize-detector';
|
||||
import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
|
||||
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TabsView',
|
||||
@@ -135,6 +136,7 @@
|
||||
const { getDarkTheme } = useDesignSetting();
|
||||
const { getNavMode, getHeaderSetting, getMenuSetting, getMultiTabsSetting } =
|
||||
useProjectSetting();
|
||||
const settingStore = useProjectSettingStore();
|
||||
|
||||
const message = useMessage();
|
||||
const route = useRoute();
|
||||
@@ -165,6 +167,17 @@
|
||||
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 { collapsed } = props;
|
||||
@@ -172,7 +185,11 @@
|
||||
const { minMenuWidth, menuWidth }: any = unref(getMenuSetting);
|
||||
const { fixed }: any = unref(getMultiTabsSetting);
|
||||
let lenNum =
|
||||
navMode === 'horizontal' ? '0px' : collapsed ? `${minMenuWidth}px` : `${menuWidth}px`;
|
||||
navMode === 'horizontal' || !isMixMenuNoneSub.value
|
||||
? '0px'
|
||||
: collapsed
|
||||
? `${minMenuWidth}px`
|
||||
: `${menuWidth}px`;
|
||||
return {
|
||||
left: lenNum,
|
||||
width: `calc(100% - ${!fixed ? '0px' : lenNum})`,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<NLayout class="layout" :position="fixedMenu" has-sider>
|
||||
<NLayoutSider
|
||||
v-if="navMode === 'vertical'"
|
||||
v-if="isMixMenuNoneSub && (navMode === 'vertical' || navMode === 'horizontal-mix')"
|
||||
show-trigger
|
||||
@collapse="collapsed = true"
|
||||
:position="fixedMenu"
|
||||
@@ -15,7 +15,7 @@
|
||||
class="layout-sider"
|
||||
>
|
||||
<Logo :collapsed="collapsed" />
|
||||
<AsideMenu v-model:collapsed="collapsed" />
|
||||
<AsideMenu v-model:collapsed="collapsed" v-model:location="getMenuLocation" />
|
||||
</NLayoutSider>
|
||||
|
||||
<NLayout :inverted="inverted">
|
||||
@@ -64,7 +64,9 @@
|
||||
import { PageHeader } from './components/Header';
|
||||
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
|
||||
import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
|
||||
import { useLoadingBar } from "naive-ui";
|
||||
import { useLoadingBar } from 'naive-ui';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useProjectSettingStore } from '@/store/modules/projectSetting';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Layout',
|
||||
@@ -77,7 +79,6 @@
|
||||
},
|
||||
setup() {
|
||||
const { getDarkTheme } = useDesignSetting();
|
||||
|
||||
const {
|
||||
getShowFooter,
|
||||
getNavMode,
|
||||
@@ -87,6 +88,8 @@
|
||||
getMultiTabsSetting,
|
||||
} = useProjectSetting();
|
||||
|
||||
const settingStore = useProjectSettingStore();
|
||||
|
||||
const navMode = getNavMode;
|
||||
|
||||
const collapsed = ref<boolean>(false);
|
||||
@@ -96,6 +99,16 @@
|
||||
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 { fixed } = unref(getHeaderSetting);
|
||||
return fixed ? 'absolute' : 'static';
|
||||
@@ -130,6 +143,10 @@
|
||||
};
|
||||
});
|
||||
|
||||
const getMenuLocation = computed(() => {
|
||||
return 'left';
|
||||
});
|
||||
|
||||
function watchWidth() {
|
||||
const Width = document.body.clientWidth;
|
||||
if (Width <= 950) {
|
||||
@@ -157,6 +174,8 @@
|
||||
getShowFooter,
|
||||
getDarkTheme,
|
||||
getHeaderInverted,
|
||||
getMenuLocation,
|
||||
isMixMenuNoneSub,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -63,6 +63,8 @@ import {
|
||||
NUpload,
|
||||
NTree,
|
||||
NSpin,
|
||||
NTimePicker,
|
||||
NBackTop,
|
||||
} from 'naive-ui';
|
||||
|
||||
const naive = create({
|
||||
@@ -129,6 +131,8 @@ const naive = create({
|
||||
NUpload,
|
||||
NTree,
|
||||
NSpin,
|
||||
NTimePicker,
|
||||
NBackTop,
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Object.keys(modules).forEach((key) => {
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
32
src/router/modules/about.ts
Normal file
32
src/router/modules/about.ts
Normal 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: 9,
|
||||
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;
|
||||
@@ -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',
|
||||
name: `${routeName}_upload`,
|
||||
meta: {
|
||||
title: '上传',
|
||||
title: '上传图片',
|
||||
},
|
||||
component: () => import('@/views/comp/upload/index.vue'),
|
||||
},
|
||||
{
|
||||
path: 'modal',
|
||||
name: `${routeName}_modal`,
|
||||
meta: {
|
||||
title: '弹窗扩展',
|
||||
},
|
||||
component: () => import('@/views/comp/modal/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
import { Layout } from '@/router/constant';
|
||||
import { TableOutlined } from '@vicons/antd';
|
||||
import { renderIcon } from '@/utils/index';
|
||||
import { renderIcon, renderNew } from '@/utils/index';
|
||||
|
||||
/**
|
||||
* @param name 路由名称, 必须设置,且不能重名
|
||||
@@ -31,6 +31,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
name: 'basic-list',
|
||||
meta: {
|
||||
title: '基础列表',
|
||||
extra: renderNew(),
|
||||
},
|
||||
component: () => import('@/views/list/basicList/index.vue'),
|
||||
},
|
||||
@@ -40,6 +41,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
meta: {
|
||||
title: '基础详情',
|
||||
hidden: true,
|
||||
activeMenu: 'basic-list',
|
||||
},
|
||||
component: () => import('@/views/list/basicList/info.vue'),
|
||||
},
|
||||
|
||||
@@ -31,6 +31,8 @@ const setting = {
|
||||
menuWidth: 200,
|
||||
//固定菜单
|
||||
fixed: true,
|
||||
//分割菜单
|
||||
mixMenu: false,
|
||||
},
|
||||
//面包屑
|
||||
crumbsSetting: {
|
||||
|
||||
@@ -110,6 +110,10 @@ body .proCard {
|
||||
}
|
||||
}
|
||||
|
||||
body .n-modal{
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
//body .proCardTabs{
|
||||
// .n-card__content{ padding-top: 3px}
|
||||
// .n-card__content:first-child{ padding-top: 3px}
|
||||
|
||||
99
src/utils/Drag.ts
Normal file
99
src/utils/Drag.ts
Normal 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
12
src/utils/dateUtil.ts
Normal 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);
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { h } from 'vue';
|
||||
import { h, unref } 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 { isObject } from './is/index';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
/**
|
||||
* render 图标
|
||||
* */
|
||||
@@ -10,31 +11,111 @@ export function renderIcon(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>) {
|
||||
return routerMap
|
||||
.filter((item) => {
|
||||
return (
|
||||
item.meta.hidden != true &&
|
||||
!['/:path(.*)*', '/', PageEnum.REDIRECT, PageEnum.BASE_LOGIN].includes(item.path)
|
||||
);
|
||||
})
|
||||
.map((item) => {
|
||||
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 = 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 = {
|
||||
...item,
|
||||
...item.meta,
|
||||
label: item.meta.title,
|
||||
key: item.name,
|
||||
...info,
|
||||
...info.meta,
|
||||
label: info.meta?.title,
|
||||
key: info.name,
|
||||
};
|
||||
// 是否有子菜单,并递归处理
|
||||
if (item.children && item.children.length > 0) {
|
||||
// Recursion
|
||||
currentMenu.children = generatorMenu(item.children);
|
||||
}
|
||||
return currentMenu;
|
||||
firstRouter.push(currentMenu);
|
||||
});
|
||||
return firstRouter;
|
||||
} else {
|
||||
return getChildrenRouter(newRouter.filter((item) => item.name === routerName));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归组装子菜单
|
||||
* */
|
||||
export function getChildrenRouter(routerMap: Array<any>) {
|
||||
return 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) => {
|
||||
@@ -78,3 +159,49 @@ export function getTreeAll(data: any[]): any[] {
|
||||
});
|
||||
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)}`;
|
||||
}
|
||||
|
||||
@@ -112,3 +112,7 @@ export function isNull(val: unknown): val is null {
|
||||
export function isNullAndUnDef(val: unknown): val is null | undefined {
|
||||
return isUnDef(val) && isNull(val);
|
||||
}
|
||||
|
||||
export function isNullOrUnDef(val: unknown): val is null | undefined {
|
||||
return isUnDef(val) || isNull(val);
|
||||
}
|
||||
|
||||
117
src/views/about/index.vue
Normal file
117
src/views/about/index.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="n-layout-page-header">
|
||||
<n-card :bordered="false" title="关于">
|
||||
{{ name }} 是一个基于 vue3,vite2,TypeScript
|
||||
的中后台解决方案,它可以帮助你快速搭建企业级中后台项目,相信不管是从新技术使用还是其他方面,都能帮助到你,持续更新中。
|
||||
</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://jekip.github.io/docs/" class="py-2" target="_blank">查看文档地址</a>
|
||||
</div>
|
||||
</n-descriptions-item>
|
||||
<n-descriptions-item label="预览地址">
|
||||
<div class="flex items-center">
|
||||
<a href="https://jekip.github.io/" 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>
|
||||
184
src/views/comp/form/basic.vue
Normal file
184
src/views/comp/form/basic.vue
Normal 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>
|
||||
214
src/views/comp/form/useForm.vue
Normal file
214
src/views/comp/form/useForm.vue
Normal 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>
|
||||
306
src/views/comp/modal/index.vue
Normal file
306
src/views/comp/modal/index.vue
Normal 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>
|
||||
@@ -38,7 +38,7 @@
|
||||
actionColumn: {
|
||||
width: 150,
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
render(record) {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
actionColumn: {
|
||||
width: 150,
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
align: 'center',
|
||||
render(record) {
|
||||
@@ -61,7 +61,7 @@
|
||||
}
|
||||
|
||||
function onEditChange({ column, value, record }) {
|
||||
if (column.dataIndex === 'id') {
|
||||
if (column.key === 'id') {
|
||||
record.editValueRefs.name4.value = `${value}`;
|
||||
}
|
||||
console.log(column, value, record);
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
}
|
||||
|
||||
function onEditChange({ column, value, record }) {
|
||||
if (column.dataIndex === 'id') {
|
||||
if (column.key === 'id') {
|
||||
record.editValueRefs.name4.value = `${value}`;
|
||||
}
|
||||
console.log(column, value, record);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:rules="rules"
|
||||
label-placement="left"
|
||||
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-select
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
:rules="rules"
|
||||
label-placement="left"
|
||||
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">
|
||||
<span>NaiveUiAdmin@163.com</span>
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
</n-card>
|
||||
</div>
|
||||
<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-step title="填写转账信息" description="确保填写正确" />
|
||||
<n-step title="确认转账信息" description="确认转账信息" />
|
||||
<n-step title="完成" description="恭喜您,转账成功" />
|
||||
<n-step title="完成转账" description="恭喜您,转账成功" />
|
||||
</n-steps>
|
||||
<step1 v-if="currentTab === 1" @nextStep="nextStep" />
|
||||
<step2 v-if="currentTab === 2" @nextStep="nextStep" @prevStep="prevStep" />
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<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
|
||||
:columns="columns"
|
||||
:request="loadDataTable"
|
||||
@@ -22,10 +28,6 @@
|
||||
<template #toolbar>
|
||||
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
|
||||
</template>
|
||||
|
||||
<template #action>
|
||||
<TableAction />
|
||||
</template>
|
||||
</BasicTable>
|
||||
|
||||
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="新建">
|
||||
@@ -59,9 +61,10 @@
|
||||
</template>
|
||||
|
||||
<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 { BasicTable, TableAction } from '@/components/Table';
|
||||
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
|
||||
import { getTableList } from '@/api/table/list';
|
||||
import { columns } from './columns';
|
||||
import { PlusOutlined } from '@vicons/antd';
|
||||
@@ -86,8 +89,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({
|
||||
components: { BasicTable, PlusOutlined, TableAction },
|
||||
// eslint-disable-next-line vue/no-unused-components
|
||||
components: { BasicTable, PlusOutlined, TableAction, BasicForm },
|
||||
setup() {
|
||||
const router = useRouter();
|
||||
const formRef: any = ref(null);
|
||||
@@ -96,11 +224,7 @@
|
||||
const state = reactive({
|
||||
showModal: false,
|
||||
formBtnLoading: false,
|
||||
formParams: {
|
||||
name: '',
|
||||
address: '',
|
||||
date: [],
|
||||
},
|
||||
formParams: {},
|
||||
params: {
|
||||
pageSize: 5,
|
||||
name: 'xiaoMa',
|
||||
@@ -108,10 +232,10 @@
|
||||
actionColumn: {
|
||||
width: 250,
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
render(record) {
|
||||
return h(TableAction, {
|
||||
return h(TableAction as any, {
|
||||
style: 'button',
|
||||
actions: [
|
||||
{
|
||||
@@ -159,13 +283,22 @@
|
||||
},
|
||||
});
|
||||
|
||||
const [register, {}] = useForm({
|
||||
gridProps: { cols: '4' },
|
||||
labelWidth: 80,
|
||||
schemas,
|
||||
});
|
||||
|
||||
function addTable() {
|
||||
state.showModal = true;
|
||||
}
|
||||
|
||||
const loadDataTable = async (params) => {
|
||||
const data = await getTableList(params);
|
||||
return data;
|
||||
const loadDataTable = async (res) => {
|
||||
let params = {
|
||||
...res,
|
||||
...state.formParams,
|
||||
};
|
||||
return await getTableList(params);
|
||||
};
|
||||
|
||||
function onCheckedRow(rowKeys) {
|
||||
@@ -208,12 +341,23 @@
|
||||
message.info('点击了删除');
|
||||
}
|
||||
|
||||
function handleSubmit(values: Recordable) {
|
||||
console.log(values);
|
||||
state.formParams = values;
|
||||
reloadTable();
|
||||
}
|
||||
|
||||
function handleReset(values: Recordable) {
|
||||
console.log(values);
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
formRef,
|
||||
columns,
|
||||
rules,
|
||||
actionRef,
|
||||
register,
|
||||
confirmForm,
|
||||
loadDataTable,
|
||||
onCheckedRow,
|
||||
@@ -222,6 +366,8 @@
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
handleOpen,
|
||||
handleSubmit,
|
||||
handleReset,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
actionColumn: {
|
||||
width: 250,
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
render(record) {
|
||||
return h(TableAction, {
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue",
|
||||
"types/*.ts",
|
||||
"types/**/*.d.ts",
|
||||
"types/**/*.ts",
|
||||
"build/**/*.ts",
|
||||
|
||||
1
types/config.d.ts
vendored
1
types/config.d.ts
vendored
@@ -27,6 +27,7 @@ export interface ImenuSetting {
|
||||
minMenuWidth: number;
|
||||
menuWidth: number;
|
||||
fixed: boolean;
|
||||
mixMenu: boolean;
|
||||
}
|
||||
|
||||
export interface IcrumbsSetting {
|
||||
|
||||
4
types/index.d.ts
vendored
4
types/index.d.ts
vendored
@@ -26,3 +26,7 @@ declare interface ComponentElRef<T extends HTMLElement = HTMLDivElement> {
|
||||
declare type ComponentRef<T extends HTMLElement = HTMLDivElement> = ComponentElRef<T> | null;
|
||||
|
||||
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]>;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,14 @@ import { wrapperEnv } from './build/utils';
|
||||
import { createVitePlugins } from './build/vite/plugin';
|
||||
import { OUTPUT_DIR } from './build/constant';
|
||||
import { createProxy } from './build/vite/proxy';
|
||||
import pkg from './package.json';
|
||||
import { format } from 'date-fns';
|
||||
const { dependencies, devDependencies, name, version } = pkg;
|
||||
|
||||
const __APP_INFO__ = {
|
||||
pkg: { dependencies, devDependencies, name, version },
|
||||
lastBuildTime: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
|
||||
};
|
||||
|
||||
function pathResolve(dir: string) {
|
||||
return resolve(process.cwd(), '.', dir);
|
||||
@@ -35,6 +43,9 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
|
||||
dedupe: ['vue'],
|
||||
},
|
||||
plugins: createVitePlugins(viteEnv, isBuild, prodMock),
|
||||
define: {
|
||||
__APP_INFO__: JSON.stringify(__APP_INFO__),
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
less: {
|
||||
|
||||
16
yarn.lock
16
yarn.lock
@@ -2382,10 +2382,10 @@ date-fns@^2.19.0:
|
||||
resolved "https://registry.nlark.com/date-fns/download/date-fns-2.22.1.tgz#1e5af959831ebb1d82992bf67b765052d8f0efc4"
|
||||
integrity sha1-Hlr5WYMeux2CmSv2e3ZQUtjw78Q=
|
||||
|
||||
dayjs@^1.10.5:
|
||||
version "1.10.5"
|
||||
resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.10.5.tgz#5600df4548fc2453b3f163ebb2abbe965ccfb986"
|
||||
integrity sha512-BUFis41ikLz+65iH6LHQCDm4YPMj5r1YFLdupPIyM4SGcXMmtiLQ7U37i+hGS8urIuqe7I/ou3IS1jVc4nbN4g==
|
||||
date-fns@^2.23.0:
|
||||
version "2.23.0"
|
||||
resolved "https://registry.nlark.com/date-fns/download/date-fns-2.23.0.tgz?cache=0&sync_timestamp=1627020299263&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fdate-fns%2Fdownload%2Fdate-fns-2.23.0.tgz#4e886c941659af0cf7b30fafdd1eaa37e88788a9"
|
||||
integrity sha1-TohslBZZrwz3sw+v3R6qN+iHiKk=
|
||||
|
||||
debug@2.6.9:
|
||||
version "2.6.9"
|
||||
@@ -4999,10 +4999,10 @@ mute-stream@0.0.7:
|
||||
resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
||||
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
|
||||
|
||||
naive-ui@^2.15.11:
|
||||
version "2.15.11"
|
||||
resolved "https://registry.nlark.com/naive-ui/download/naive-ui-2.15.11.tgz#1a7fa5ca6d42ed2c4d50cde827b994f42edc5743"
|
||||
integrity sha1-Gn+lym1C7SxNUM3oJ7mU9C7cV0M=
|
||||
naive-ui@^2.16.0:
|
||||
version "2.16.0"
|
||||
resolved "https://registry.nlark.com/naive-ui/download/naive-ui-2.16.0.tgz#42d8b6120ab061e46a316ac074c5b788139cd744"
|
||||
integrity sha1-Qti2EgqwYeRqMWrAdMW3iBOc10Q=
|
||||
dependencies:
|
||||
"@css-render/plugin-bem" "^0.15.4"
|
||||
"@css-render/vue3-ssr" "^0.15.4"
|
||||
|
||||
Reference in New Issue
Block a user