Fixes bug add baseModal | baseForm 组件

This commit is contained in:
Ah jung
2021-08-05 17:24:24 +08:00
parent 98e1bf0227
commit 8a5f237630
59 changed files with 2056 additions and 106 deletions

View File

@@ -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',
{

View File

@@ -1,4 +1,23 @@
# 1.5 (2021-07-30)
# 1.5.1 (2021-08-07)
### 🐛 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 +34,7 @@
- 本次更新,有破坏性更新,涉及文件重命名,增删调整
# 1.4 (2021-07-21)
# 1.4.0 (2021-07-21)
### 🐛 Bug Fixes
- vite降至2.3.6
- 多标签页交互优化
@@ -27,7 +46,7 @@
- 持续更新更多实用组件及示例感谢Star
# 1.3 (2021-07-19)
# 1.3.0 (2021-07-19)
### 🐛 Bug Fixes
- 修复多标签页左右切换按钮自适应展示
- 修复登录页面出现多标签页
@@ -40,7 +59,7 @@
- 持续更新更多实用组件及示例感谢Star
# 1.2 (2021-07-16)
# 1.2.0 (2021-07-16)
### 🐛 Bug Fixes
- 修复面包屑显示登录页面
- 菜单支持只展开当前父级菜单
@@ -54,7 +73,7 @@
- 持续更新更多实用示例,同时也演示`Naive UI`使用方法
# 1.1 (2021-07-15)
# 1.1.0 (2021-07-15)
- ### ✨ Features
- 新增 `基础表单` 示例页面
- 新增 `分步表单` 示例页面
@@ -62,7 +81,7 @@
- 持续更新更多实用示例,同时也演示`Naive UI`使用方法
# 1.0 (2021-07-12)
# 1.0.0 (2021-07-12)
### 🐛 Bug Fixes
- 修复页面切换面包屑未及时更新

View File

@@ -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, '');
};

View File

@@ -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));

View File

@@ -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);

View File

@@ -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(),
},
},
},
]
]
: [],
},
});

View File

@@ -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`;
},
},
],

View File

@@ -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()',

View File

@@ -1,6 +1,6 @@
{
"name": "naive-ui-admin",
"version": "1.5.0",
"version": "1.5.1",
"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",

View File

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

View File

@@ -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,
},
};
});

View File

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

View File

@@ -0,0 +1,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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,57 @@
import { ref, onUnmounted, unref, getCurrentInstance, watch } from 'vue';
import { isProdMode } from '@/utils/env';
import { UseModalReturnType, ModalMethods } from './type';
import { getDynamicProps } from '@/utils';
export function useModal(props?: Props): UseModalReturnType {
const modal = ref<Nullable<ModalMethods>>(null);
const loaded = ref<Nullable<boolean>>(false);
function register(modalMethod: ModalMethods) {
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,
() => {
const { setProps } = modal.value;
props && setProps(getDynamicProps(props));
},
{
immediate: true,
deep: true,
}
);
}
const getInstance = () => {
const instance = unref(modal);
if (!instance) {
error('useModal instance is undefined!');
}
return instance;
};
const methods: ReturnMethods = {
setProps: (props: Partial<ModalProps>): void => {
getInstance()?.setProps(props);
},
openModal: () => {
getInstance()?.openModal();
},
closeModal: () => {
getInstance()?.closeModal();
},
setSubLoading: () => {
getInstance()?.setSubLoading();
},
};
return [register, methods];
}

View File

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

View File

@@ -0,0 +1,12 @@
export interface ModalProps {
subBtuText?: string;
}
/**
* @description: 弹窗对外暴露的方法
*/
export interface ModalMethods {
setProps: (props: Partial<ModalProps>) => void;
openModal: () => void;
closeModal: () => void;
}

View File

@@ -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: [

View File

@@ -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);

View File

@@ -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;
}
//重置

View File

@@ -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;
}

View File

@@ -47,7 +47,6 @@ export function useDataSource(
try {
setLoading(true);
const { request, pagination }: any = unref(propsRef);
//组装分页信息
const pageField = APISETTING.pageField;
const sizeField = APISETTING.sizeField;

View File

@@ -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,

View File

@@ -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);
}

View File

@@ -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'],

View File

@@ -6,7 +6,7 @@
:collapsed="collapsed"
:collapsed-width="64"
:collapsed-icon-size="20"
:indent="28"
:indent="24"
:expanded-keys="openKeys"
v-model:value="selectedKeys"
@update:value="clickMenuItem"
@@ -18,7 +18,7 @@
import { defineComponent, 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 } from '@/utils';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
export default defineComponent({

View File

@@ -64,7 +64,7 @@
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';
export default defineComponent({
name: 'Layout',

View File

@@ -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,
],
});

View File

@@ -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);

View File

@@ -63,14 +63,49 @@ const routes: Array<RouteRecordRaw> = [
},
],
},
{
path: 'form',
name: `${routeName}_form`,
redirect: '/comp/form/basic',
component: ParentLayout,
meta: {
title: '表单',
},
children: [
{
path: 'basic',
name: `${routeName}_form_basic`,
meta: {
title: '基础使用',
},
component: () => import('@/views/comp/form/basic.vue'),
},
{
path: 'useForm',
name: `useForm`,
meta: {
title: 'useForm',
},
component: () => import('@/views/comp/form/useForm.vue'),
},
],
},
{
path: 'upload',
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'),
},
],
},
];

View File

@@ -1,7 +1,7 @@
import { RouteRecordRaw } from 'vue-router';
import { Layout } from '@/router/constant';
import { DocumentTextOutline } from '@vicons/ionicons5';
import { renderIcon } from '@/utils/index';
import { renderIcon, renderNew } from '@/utils/index';
const routes: Array<RouteRecordRaw> = [
{
@@ -12,6 +12,7 @@ const routes: Array<RouteRecordRaw> = [
title: '项目文档',
icon: renderIcon(DocumentTextOutline),
sort: 8,
extra: renderNew(),
},
},
];

View File

@@ -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'),
},

View File

@@ -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}

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

@@ -0,0 +1,98 @@
//获取相关CSS属性
const getCss = function (o, key) {
return o.currentStyle
? o.currentStyle[key]
: document.defaultView.getComputedStyle(o, false)[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 = event ? event : window.event;
if (params.flag) {
const nowX = e.clientX,
nowY = e.clientY;
const disX = nowX - params.currentX,
disY = nowY - params.currentY;
let left = parseInt(params.left) + disX;
let top = parseInt(params.top) + disY;
// 拖出屏幕边缘
if (-left > minDomLeft) {
left = -minDomLeft;
} else if (left > maxDragDomLeft) {
left = maxDragDomLeft;
}
if (-top > minDomTop) {
top = -minDomTop;
} else if (top > maxDragDomTop) {
top = maxDragDomTop;
}
target.style.left = left + 'px';
target.style.top = top + 'px';
if (typeof callback == 'function') {
callback((parseInt(params.left) || 0) + disX, (parseInt(params.top) || 0) + disY);
}
if (event.preventDefault) {
event.preventDefault();
}
return false;
}
};
};
export default startDrag;

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

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

View File

@@ -1,7 +1,8 @@
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';
/**
* render 图标
@@ -10,6 +11,24 @@ 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 }
);
}
/**
* 递归组装菜单格式
*/
@@ -17,21 +36,23 @@ export function generatorMenu(routerMap: Array<any>) {
return routerMap
.filter((item) => {
return (
item.meta.hidden != true &&
(item.meta?.hidden || false) != true &&
!['/:path(.*)*', '/', PageEnum.REDIRECT, PageEnum.BASE_LOGIN].includes(item.path)
);
})
.map((item) => {
const info =
item.meta?.alwaysShow != true && item.children?.length === 1 ? item.children[0] : item;
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) {
if (info.children && info.children.length > 0) {
// Recursion
currentMenu.children = generatorMenu(item.children);
currentMenu.children = generatorMenu(info.children);
}
return currentMenu;
});
@@ -78,3 +99,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)}`;
}

View File

@@ -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);
}

View File

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

View File

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

View File

@@ -38,7 +38,7 @@
actionColumn: {
width: 150,
title: '操作',
dataIndex: 'action',
key: 'action',
fixed: 'right',
align: 'center',
render(record) {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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: 5 },
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,
};
},
});

View File

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

View File

@@ -40,6 +40,7 @@
"src/**/*.d.ts",
"src/**/*.tsx",
"src/**/*.vue",
"types/*.ts",
"types/**/*.d.ts",
"types/**/*.ts",
"build/**/*.ts",

4
types/index.d.ts vendored
View File

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

View File

@@ -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"