mirror of
https://github.com/bufanyun/hotgo.git
synced 2025-08-29 23:26:33 +08:00
v2.0
This commit is contained in:
40
web/src/components/Application/Application.vue
Normal file
40
web/src/components/Application/Application.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<n-loading-bar-provider>
|
||||
<n-dialog-provider>
|
||||
<DialogContent />
|
||||
<n-notification-provider>
|
||||
<n-message-provider>
|
||||
<MessageContent />
|
||||
<slot name="default"></slot>
|
||||
</n-message-provider>
|
||||
</n-notification-provider>
|
||||
</n-dialog-provider>
|
||||
</n-loading-bar-provider>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import {
|
||||
NDialogProvider,
|
||||
NNotificationProvider,
|
||||
NMessageProvider,
|
||||
NLoadingBarProvider,
|
||||
} from 'naive-ui';
|
||||
import { MessageContent } from '@/components/MessageContent';
|
||||
import { DialogContent } from '@/components/DialogContent';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Application',
|
||||
components: {
|
||||
NDialogProvider,
|
||||
NNotificationProvider,
|
||||
NMessageProvider,
|
||||
NLoadingBarProvider,
|
||||
MessageContent,
|
||||
DialogContent,
|
||||
},
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
</script>
|
3
web/src/components/Application/index.ts
Normal file
3
web/src/components/Application/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import AppProvider from './Application.vue';
|
||||
|
||||
export { AppProvider };
|
110
web/src/components/CountTo/CountTo.vue
Normal file
110
web/src/components/CountTo/CountTo.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<span :style="{ color }">
|
||||
{{ value }}
|
||||
</span>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref, computed, watchEffect, unref, onMounted, watch } from 'vue';
|
||||
import { useTransition, TransitionPresets } from '@vueuse/core';
|
||||
import { isNumber } from '@/utils/is';
|
||||
|
||||
const props = {
|
||||
startVal: { type: Number, default: 0 },
|
||||
endVal: { type: Number, default: 2021 },
|
||||
duration: { type: Number, default: 1500 },
|
||||
autoplay: { type: Boolean, default: true },
|
||||
decimals: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
validator(value: number) {
|
||||
return value >= 0;
|
||||
},
|
||||
},
|
||||
prefix: { type: String, default: '' },
|
||||
suffix: { type: String, default: '' },
|
||||
separator: { type: String, default: ',' },
|
||||
decimal: { type: String, default: '.' },
|
||||
/**
|
||||
* font color
|
||||
*/
|
||||
color: { type: String },
|
||||
/**
|
||||
* Turn on digital animation
|
||||
*/
|
||||
useEasing: { type: Boolean, default: true },
|
||||
/**
|
||||
* Digital animation
|
||||
*/
|
||||
transition: { type: String, default: 'linear' },
|
||||
};
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CountTo',
|
||||
props,
|
||||
emits: ['onStarted', 'onFinished'],
|
||||
setup(props, { emit }) {
|
||||
const source = ref(props.startVal);
|
||||
const disabled = ref(false);
|
||||
let outputValue = useTransition(source);
|
||||
|
||||
const value = computed(() => formatNumber(unref(outputValue)));
|
||||
|
||||
watchEffect(() => {
|
||||
source.value = props.startVal;
|
||||
});
|
||||
|
||||
watch([() => props.startVal, () => props.endVal], () => {
|
||||
if (props.autoplay) {
|
||||
start();
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
props.autoplay && start();
|
||||
});
|
||||
|
||||
function start() {
|
||||
run();
|
||||
source.value = props.endVal;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
source.value = props.startVal;
|
||||
run();
|
||||
}
|
||||
|
||||
function run() {
|
||||
outputValue = useTransition(source, {
|
||||
disabled,
|
||||
duration: props.duration,
|
||||
onFinished: () => emit('onFinished'),
|
||||
onStarted: () => emit('onStarted'),
|
||||
...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
function formatNumber(num: number | string) {
|
||||
if (!num) {
|
||||
return '';
|
||||
}
|
||||
const { decimals, decimal, separator, suffix, prefix } = props;
|
||||
num = Number(num).toFixed(decimals);
|
||||
num += '';
|
||||
|
||||
const x = num.split('.');
|
||||
let x1 = x[0];
|
||||
const x2 = x.length > 1 ? decimal + x[1] : '';
|
||||
|
||||
const rgx = /(\d+)(\d{3})/;
|
||||
if (separator && !isNumber(separator)) {
|
||||
while (rgx.test(x1)) {
|
||||
x1 = x1.replace(rgx, '$1' + separator + '$2');
|
||||
}
|
||||
}
|
||||
return prefix + x1 + x2 + suffix;
|
||||
}
|
||||
|
||||
return { value, start, reset };
|
||||
},
|
||||
});
|
||||
</script>
|
4
web/src/components/CountTo/index.ts
Normal file
4
web/src/components/CountTo/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { withInstall } from '@/utils';
|
||||
import countTo from './CountTo.vue';
|
||||
|
||||
export const CountTo = withInstall(countTo);
|
3
web/src/components/DialogContent/index.ts
Normal file
3
web/src/components/DialogContent/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import DialogContent from './index.vue';
|
||||
|
||||
export { DialogContent };
|
12
web/src/components/DialogContent/index.vue
Normal file
12
web/src/components/DialogContent/index.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template></template>
|
||||
<script lang="ts">
|
||||
import { useDialog } from 'naive-ui';
|
||||
|
||||
export default {
|
||||
name: 'DialogContent',
|
||||
setup() {
|
||||
//挂载在 window 方便与在js中使用
|
||||
window['$dialog'] = useDialog();
|
||||
},
|
||||
};
|
||||
</script>
|
4
web/src/components/Form/index.ts
Normal file
4
web/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';
|
318
web/src/components/Form/src/BasicForm.vue
Normal file
318
web/src/components/Form/src/BasicForm.vue
Normal file
@@ -0,0 +1,318 @@
|
||||
<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 && getProps.showAdvancedButton"
|
||||
@click="unfoldToggle"
|
||||
>
|
||||
<template #icon>
|
||||
<n-icon size="14" class="unfold-icon" v-if="overflow">
|
||||
<DownOutlined />
|
||||
</n-icon>
|
||||
<n-icon size="14" class="unfold-icon" v-else>
|
||||
<UpOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ overflow ? '展开' : '收起' }}
|
||||
</n-button>
|
||||
</n-space>
|
||||
</n-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, ref, computed, unref, onMounted, watch } from 'vue';
|
||||
import { createPlaceholderMessage } from './helper';
|
||||
import { useFormEvents } from './hooks/useFormEvents';
|
||||
import { useFormValues } from './hooks/useFormValues';
|
||||
|
||||
import { basicProps } from './props';
|
||||
import { DownOutlined, UpOutlined, QuestionCircleOutlined } from '@vicons/antd';
|
||||
|
||||
import type { Ref } from 'vue';
|
||||
import type { GridProps } from 'naive-ui/lib/grid';
|
||||
import type { FormSchema, FormProps, FormActionType } from './types/form';
|
||||
|
||||
import { isArray } from '@/utils/is/index';
|
||||
import { deepMerge } from '@/utils';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasicUpload',
|
||||
components: { DownOutlined, UpOutlined, QuestionCircleOutlined },
|
||||
props: {
|
||||
...basicProps,
|
||||
},
|
||||
emits: ['reset', 'submit', 'register'],
|
||||
setup(props, { emit, attrs }) {
|
||||
const defaultFormModel = ref<Recordable>({});
|
||||
const formModel = reactive<Recordable>({});
|
||||
const propsRef = ref<Partial<FormProps>>({});
|
||||
const schemaRef = ref<Nullable<FormSchema[]>>(null);
|
||||
const formElRef = ref<Nullable<FormActionType>>(null);
|
||||
const gridCollapsed = ref(true);
|
||||
const loadingSub = ref(false);
|
||||
const isUpdateDefaultRef = ref(false);
|
||||
|
||||
const getSubmitBtnOptions = computed(() => {
|
||||
return Object.assign(
|
||||
{
|
||||
size: props.size,
|
||||
type: 'primary',
|
||||
},
|
||||
props.submitButtonOptions
|
||||
);
|
||||
});
|
||||
|
||||
const getResetBtnOptions = computed(() => {
|
||||
return Object.assign(
|
||||
{
|
||||
size: props.size,
|
||||
type: 'default',
|
||||
},
|
||||
props.resetButtonOptions
|
||||
);
|
||||
});
|
||||
|
||||
function getComponentProps(schema) {
|
||||
const compProps = schema.componentProps ?? {};
|
||||
const component = schema.component;
|
||||
return {
|
||||
clearable: true,
|
||||
placeholder: createPlaceholderMessage(unref(component)),
|
||||
...compProps,
|
||||
};
|
||||
}
|
||||
|
||||
const getProps = computed((): FormProps => {
|
||||
const formProps = { ...props, ...unref(propsRef) } as FormProps;
|
||||
const rulesObj: any = {
|
||||
rules: {},
|
||||
};
|
||||
const schemas: any = formProps.schemas || [];
|
||||
schemas.forEach((item) => {
|
||||
if (item.rules && isArray(item.rules)) {
|
||||
rulesObj.rules[item.field] = item.rules;
|
||||
}
|
||||
});
|
||||
return { ...formProps, ...unref(rulesObj) };
|
||||
});
|
||||
|
||||
const isInline = computed(() => {
|
||||
const { layout } = unref(getProps);
|
||||
return layout === 'inline';
|
||||
});
|
||||
|
||||
const getGrid = computed((): GridProps => {
|
||||
const { gridProps } = unref(getProps);
|
||||
return {
|
||||
...gridProps,
|
||||
collapsed: isInline.value ? gridCollapsed.value : false,
|
||||
responsive: 'screen',
|
||||
};
|
||||
});
|
||||
|
||||
const getBindValue = computed(
|
||||
() => ({ ...attrs, ...props, ...unref(getProps) } as Recordable)
|
||||
);
|
||||
|
||||
const getSchema = computed((): FormSchema[] => {
|
||||
const schemas: FormSchema[] = unref(schemaRef) || (unref(getProps).schemas as any);
|
||||
for (const schema of schemas) {
|
||||
const { defaultValue } = schema;
|
||||
// handle date type
|
||||
// dateItemType.includes(component as string)
|
||||
if (defaultValue) {
|
||||
schema.defaultValue = defaultValue;
|
||||
}
|
||||
}
|
||||
return schemas as FormSchema[];
|
||||
});
|
||||
|
||||
const { handleFormValues, initDefault } = useFormValues({
|
||||
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
web/src/components/Form/src/helper.ts
Normal file
42
web/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;
|
||||
}
|
86
web/src/components/Form/src/hooks/useForm.ts
Normal file
86
web/src/components/Form/src/hooks/useForm.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { FormProps, FormActionType, UseFormReturnType } from '../types/form';
|
||||
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
web/src/components/Form/src/hooks/useFormContext.ts
Normal file
11
web/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
web/src/components/Form/src/hooks/useFormEvents.ts
Normal file
107
web/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;
|
||||
} catch (error) {
|
||||
loadingSub.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//清空校验
|
||||
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
web/src/components/Form/src/hooks/useFormValues.ts
Normal file
54
web/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
web/src/components/Form/src/props.ts
Normal file
82
web/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,
|
||||
},
|
||||
};
|
59
web/src/components/Form/src/types/form.ts
Normal file
59
web/src/components/Form/src/types/form.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
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;
|
||||
collapsedRows?: number;
|
||||
}
|
||||
|
||||
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
web/src/components/Form/src/types/index.ts
Normal file
28
web/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';
|
3
web/src/components/LoadingContent/index.ts
Normal file
3
web/src/components/LoadingContent/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import LoadingContent from './index.vue';
|
||||
|
||||
export { LoadingContent };
|
12
web/src/components/LoadingContent/index.vue
Normal file
12
web/src/components/LoadingContent/index.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template></template>
|
||||
<script lang="ts">
|
||||
import { useLoadingBar } from 'naive-ui';
|
||||
|
||||
export default {
|
||||
name: 'LoadingContent',
|
||||
setup() {
|
||||
//挂载在 window 方便与在js中使用
|
||||
window['$loading'] = useLoadingBar();
|
||||
},
|
||||
};
|
||||
</script>
|
304
web/src/components/Lockscreen/Lockscreen.vue
Normal file
304
web/src/components/Lockscreen/Lockscreen.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<div
|
||||
:class="{ onLockLogin: showLogin }"
|
||||
class="lockscreen"
|
||||
@keyup="onLockLogin(true)"
|
||||
@mousedown.stop
|
||||
@contextmenu.prevent
|
||||
>
|
||||
<template v-if="!showLogin">
|
||||
<div class="lock-box">
|
||||
<div class="lock">
|
||||
<span class="lock-icon" title="解锁屏幕" @click="onLockLogin(true)">
|
||||
<n-icon>
|
||||
<lock-outlined />
|
||||
</n-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!--充电-->
|
||||
<recharge
|
||||
:battery="battery"
|
||||
:battery-status="batteryStatus"
|
||||
:calc-discharging-time="calcDischargingTime"
|
||||
:calc-charging-time="calcChargingTime"
|
||||
/>
|
||||
|
||||
<div class="local-time">
|
||||
<div class="time">{{ hour }}:{{ minute }}</div>
|
||||
<div class="date">{{ month }}月{{ day }}号,星期{{ week }}</div>
|
||||
</div>
|
||||
<div class="computer-status">
|
||||
<span :class="{ offline: !online }" class="network">
|
||||
<wifi-outlined class="network" />
|
||||
</span>
|
||||
<api-outlined />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!--登录-->
|
||||
<template v-if="showLogin">
|
||||
<div class="login-box">
|
||||
<n-avatar :size="128">
|
||||
<n-icon>
|
||||
<user-outlined />
|
||||
</n-icon>
|
||||
</n-avatar>
|
||||
<div class="username">{{ loginParams.username }}</div>
|
||||
<n-input
|
||||
type="password"
|
||||
autofocus
|
||||
v-model:value="loginParams.password"
|
||||
@keyup.enter="onLogin"
|
||||
placeholder="请输入登录密码"
|
||||
>
|
||||
<template #suffix>
|
||||
<n-icon @click="onLogin" style="cursor: pointer">
|
||||
<LoadingOutlined v-if="loginLoading" />
|
||||
<arrow-right-outlined v-else />
|
||||
</n-icon>
|
||||
</template>
|
||||
</n-input>
|
||||
|
||||
<div class="w-full flex" v-if="isLoginError">
|
||||
<span class="text-red-500">{{ errorMsg }}</span>
|
||||
</div>
|
||||
|
||||
<div class="w-full mt-1 flex justify-around">
|
||||
<div><a @click="showLogin = false">返回</a></div>
|
||||
<div><a @click="goLogin">重新登录</a></div>
|
||||
<div><a @click="onLogin">进入系统</a></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, reactive, toRefs } from 'vue';
|
||||
import { ResultEnum } from '@/enums/httpEnum';
|
||||
import recharge from './Recharge.vue';
|
||||
import {
|
||||
LockOutlined,
|
||||
LoadingOutlined,
|
||||
UserOutlined,
|
||||
ApiOutlined,
|
||||
ArrowRightOutlined,
|
||||
WifiOutlined,
|
||||
} from '@vicons/antd';
|
||||
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { useOnline } from '@/hooks/useOnline';
|
||||
import { useTime } from '@/hooks/useTime';
|
||||
import { useBattery } from '@/hooks/useBattery';
|
||||
import { useLockscreenStore } from '@/store/modules/lockscreen';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Lockscreen',
|
||||
components: {
|
||||
LockOutlined,
|
||||
LoadingOutlined,
|
||||
UserOutlined,
|
||||
ArrowRightOutlined,
|
||||
ApiOutlined,
|
||||
WifiOutlined,
|
||||
recharge,
|
||||
},
|
||||
setup() {
|
||||
const useLockscreen = useLockscreenStore();
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 获取时间
|
||||
const { month, day, hour, minute, second, week } = useTime();
|
||||
const { online } = useOnline();
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const { battery, batteryStatus, calcDischargingTime, calcChargingTime } = useBattery();
|
||||
const userInfo: object = userStore.getUserInfo || {};
|
||||
const username = userInfo['username'] || '';
|
||||
const state = reactive({
|
||||
showLogin: false,
|
||||
loginLoading: false, // 正在登录
|
||||
isLoginError: false, //密码错误
|
||||
errorMsg: '密码错误',
|
||||
loginParams: {
|
||||
username: username || '',
|
||||
password: '',
|
||||
},
|
||||
});
|
||||
|
||||
// 解锁登录
|
||||
const onLockLogin = (value: boolean) => (state.showLogin = value);
|
||||
|
||||
// 登录
|
||||
const onLogin = async () => {
|
||||
if (!state.loginParams.password.trim()) {
|
||||
return;
|
||||
}
|
||||
const params = {
|
||||
isLock: true,
|
||||
...state.loginParams,
|
||||
};
|
||||
state.loginLoading = true;
|
||||
const { code, message } = await userStore.login(params);
|
||||
if (code === ResultEnum.SUCCESS) {
|
||||
onLockLogin(false);
|
||||
useLockscreen.setLock(false);
|
||||
} else {
|
||||
state.errorMsg = message;
|
||||
state.isLoginError = true;
|
||||
}
|
||||
state.loginLoading = false;
|
||||
};
|
||||
|
||||
//重新登录
|
||||
const goLogin = () => {
|
||||
onLockLogin(false);
|
||||
useLockscreen.setLock(false);
|
||||
router.replace({
|
||||
path: '/login',
|
||||
query: {
|
||||
redirect: route.fullPath,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
online,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
week,
|
||||
battery,
|
||||
batteryStatus,
|
||||
calcDischargingTime,
|
||||
calcChargingTime,
|
||||
onLockLogin,
|
||||
onLogin,
|
||||
goLogin,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.lockscreen {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
background: #000;
|
||||
color: white;
|
||||
overflow: hidden;
|
||||
z-index: 9999;
|
||||
|
||||
&.onLockLogin {
|
||||
background-color: rgba(25, 28, 34, 0.88);
|
||||
backdrop-filter: blur(7px);
|
||||
}
|
||||
|
||||
.login-box {
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
> * {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.lock-box {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 34px;
|
||||
z-index: 100;
|
||||
|
||||
.tips {
|
||||
color: white;
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.lock {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
.lock-icon {
|
||||
cursor: pointer;
|
||||
|
||||
.anticon-unlock {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover .anticon-unlock {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
&:hover .anticon-lock {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.local-time {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
left: 60px;
|
||||
font-family: helvetica;
|
||||
|
||||
.time {
|
||||
font-size: 70px;
|
||||
}
|
||||
|
||||
.date {
|
||||
font-size: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.computer-status {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
right: 60px;
|
||||
font-size: 24px;
|
||||
|
||||
> * {
|
||||
margin-left: 14px;
|
||||
}
|
||||
|
||||
.network {
|
||||
position: relative;
|
||||
|
||||
&.offline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 2px;
|
||||
height: 28px;
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
background-color: red;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
164
web/src/components/Lockscreen/Recharge.vue
Normal file
164
web/src/components/Lockscreen/Recharge.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<div class="number">{{ battery.level }}%</div>
|
||||
<div class="contrast">
|
||||
<div class="circle"></div>
|
||||
<ul class="bubbles">
|
||||
<li v-for="i in 15" :key="i"></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="charging">
|
||||
<div>{{ batteryStatus }}</div>
|
||||
<div v-show="Number.isFinite(battery.dischargingTime) && battery.dischargingTime != 0">
|
||||
剩余可使用时间:{{ calcDischargingTime }}
|
||||
</div>
|
||||
<span v-show="Number.isFinite(battery.chargingTime) && battery.chargingTime != 0">
|
||||
距离电池充满需要:{{ calcChargingTime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'HuaweiCharge',
|
||||
// props: ['batteryStatus', 'battery', 'calcDischargingTime'],
|
||||
props: {
|
||||
battery: {
|
||||
// 电池对象
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
calcDischargingTime: {
|
||||
// 电池剩余时间可用时间
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
calcChargingTime: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
batteryStatus: {
|
||||
// 电池状态
|
||||
type: String,
|
||||
validator: (val: string) => ['充电中', '已充满', '已断开电源'].includes(val),
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
position: absolute;
|
||||
bottom: 20vh;
|
||||
left: 50vw;
|
||||
width: 300px;
|
||||
height: 500px;
|
||||
transform: translateX(-50%);
|
||||
|
||||
.number {
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
z-index: 10;
|
||||
width: 300px;
|
||||
font-size: 32px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.contrast {
|
||||
width: 300px;
|
||||
height: 400px;
|
||||
overflow: hidden;
|
||||
background-color: #000;
|
||||
filter: contrast(15) hue-rotate(0);
|
||||
animation: hueRotate 10s infinite linear;
|
||||
|
||||
.circle {
|
||||
position: relative;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
filter: blur(8px);
|
||||
box-sizing: border-box;
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background-color: #00ff6f;
|
||||
border-radius: 42% 38% 62% 49% / 45%;
|
||||
content: '';
|
||||
transform: translate(-50%, -50%) rotate(0);
|
||||
animation: rotate 10s infinite linear;
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
z-index: 10;
|
||||
width: 176px;
|
||||
height: 176px;
|
||||
background-color: #000;
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
}
|
||||
|
||||
.bubbles {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
width: 100px;
|
||||
height: 40px;
|
||||
background-color: #00ff6f;
|
||||
border-radius: 100px 100px 0 0;
|
||||
filter: blur(5px);
|
||||
transform: translate(-50%, 0);
|
||||
|
||||
li {
|
||||
position: absolute;
|
||||
background: #00ff6f;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.charging {
|
||||
font-size: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
50% {
|
||||
border-radius: 45% / 42% 38% 58% 49%;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(-50%, -50%) rotate(720deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveToTop {
|
||||
90% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.1;
|
||||
transform: translate(-50%, -180px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes hueRotate {
|
||||
100% {
|
||||
filter: contrast(15) hue-rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
3
web/src/components/Lockscreen/index.ts
Normal file
3
web/src/components/Lockscreen/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import LockScreen from './Lockscreen.vue';
|
||||
|
||||
export { LockScreen };
|
3
web/src/components/MessageContent/index.ts
Normal file
3
web/src/components/MessageContent/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import MessageContent from './index.vue';
|
||||
|
||||
export { MessageContent };
|
12
web/src/components/MessageContent/index.vue
Normal file
12
web/src/components/MessageContent/index.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<template></template>
|
||||
<script lang="ts">
|
||||
import { useMessage } from 'naive-ui';
|
||||
|
||||
export default {
|
||||
name: 'MessageContent',
|
||||
setup() {
|
||||
//挂载在 window 方便与在js中使用
|
||||
window['$message'] = useMessage();
|
||||
},
|
||||
};
|
||||
</script>
|
3
web/src/components/Modal/index.ts
Normal file
3
web/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';
|
117
web/src/components/Modal/src/basicModal.vue
Normal file
117
web/src/components/Modal/src/basicModal.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<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" setup>
|
||||
import {
|
||||
getCurrentInstance,
|
||||
ref,
|
||||
nextTick,
|
||||
unref,
|
||||
computed,
|
||||
useAttrs,
|
||||
defineEmits,
|
||||
defineProps,
|
||||
} from 'vue';
|
||||
import { basicProps } from './props';
|
||||
import startDrag from '@/utils/Drag';
|
||||
import { deepMerge } from '@/utils';
|
||||
import { FormProps } from '@/components/Form';
|
||||
import { ModalProps, ModalMethods } from './type';
|
||||
|
||||
const attrs = useAttrs();
|
||||
const props = defineProps({ ...basicProps });
|
||||
const emit = defineEmits(['on-close', 'on-ok', 'register']);
|
||||
|
||||
const propsRef = ref<Partial<ModalProps> | null>(null);
|
||||
|
||||
const isModal = ref(false);
|
||||
const subLoading = ref(false);
|
||||
|
||||
const getProps = computed((): FormProps => {
|
||||
return { ...props, ...(unref(propsRef) as any) };
|
||||
});
|
||||
|
||||
const subBtuText = computed(() => {
|
||||
const { subBtuText } = propsRef.value as any;
|
||||
return subBtuText || props.subBtuText;
|
||||
});
|
||||
|
||||
async function setProps(modalProps: Partial<ModalProps>): Promise<void> {
|
||||
propsRef.value = deepMerge(unref(propsRef) || ({} as any), modalProps);
|
||||
}
|
||||
|
||||
const getBindValue = computed(() => {
|
||||
return {
|
||||
...attrs,
|
||||
...unref(getProps),
|
||||
...unref(propsRef),
|
||||
};
|
||||
});
|
||||
|
||||
function setSubLoading(status: boolean) {
|
||||
subLoading.value = status;
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
isModal.value = true;
|
||||
nextTick(() => {
|
||||
const oBox = document.getElementById('basic-modal');
|
||||
const oBar = document.getElementById('basic-modal-bar');
|
||||
startDrag(oBar, oBox);
|
||||
});
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
isModal.value = false;
|
||||
subLoading.value = false;
|
||||
emit('on-close');
|
||||
}
|
||||
|
||||
function onCloseModal() {
|
||||
isModal.value = false;
|
||||
emit('on-close');
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
subLoading.value = true;
|
||||
console.log(subLoading.value);
|
||||
emit('on-ok');
|
||||
}
|
||||
|
||||
const modalMethods: ModalMethods = {
|
||||
setProps,
|
||||
openModal,
|
||||
closeModal,
|
||||
setSubLoading,
|
||||
};
|
||||
|
||||
const instance = getCurrentInstance();
|
||||
if (instance) {
|
||||
emit('register', modalMethods);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.cursor-move {
|
||||
cursor: move;
|
||||
}
|
||||
</style>
|
54
web/src/components/Modal/src/hooks/useModal.ts
Normal file
54
web/src/components/Modal/src/hooks/useModal.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { ref, unref, getCurrentInstance, watch } from 'vue';
|
||||
import { isProdMode } from '@/utils/env';
|
||||
import { ModalMethods, UseModalReturnType } from '../type';
|
||||
import { getDynamicProps } from '@/utils';
|
||||
import { tryOnUnmounted } from '@vueuse/core';
|
||||
export function useModal(props): UseModalReturnType {
|
||||
const modalRef = ref<Nullable<ModalMethods>>(null);
|
||||
const currentInstance = getCurrentInstance();
|
||||
|
||||
const getInstance = () => {
|
||||
const instance = unref(modalRef.value);
|
||||
if (!instance) {
|
||||
console.error('useModal instance is undefined!');
|
||||
}
|
||||
return instance;
|
||||
};
|
||||
|
||||
const register = (modalInstance: ModalMethods) => {
|
||||
isProdMode() &&
|
||||
tryOnUnmounted(() => {
|
||||
modalRef.value = null;
|
||||
});
|
||||
modalRef.value = modalInstance;
|
||||
currentInstance?.emit('register', modalInstance);
|
||||
|
||||
watch(
|
||||
() => props,
|
||||
() => {
|
||||
props && modalInstance.setProps(getDynamicProps(props));
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const methods: ModalMethods = {
|
||||
setProps: (props): void => {
|
||||
getInstance()?.setProps(props);
|
||||
},
|
||||
openModal: () => {
|
||||
getInstance()?.openModal();
|
||||
},
|
||||
closeModal: () => {
|
||||
getInstance()?.closeModal();
|
||||
},
|
||||
setSubLoading: (status) => {
|
||||
getInstance()?.setSubLoading(status);
|
||||
},
|
||||
};
|
||||
|
||||
return [register, methods];
|
||||
}
|
30
web/src/components/Modal/src/props.ts
Normal file
30
web/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',
|
||||
},
|
||||
};
|
19
web/src/components/Modal/src/type/index.ts
Normal file
19
web/src/components/Modal/src/type/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { DialogOptions } from 'naive-ui/lib/dialog';
|
||||
/**
|
||||
* @description: 弹窗对外暴露的方法
|
||||
*/
|
||||
export interface ModalMethods {
|
||||
setProps: (props) => void;
|
||||
openModal: () => void;
|
||||
closeModal: () => void;
|
||||
setSubLoading: (status) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持修改,DialogOptions 參數
|
||||
*/
|
||||
export type ModalProps = DialogOptions;
|
||||
|
||||
export type RegisterFn = (ModalInstance: ModalMethods) => void;
|
||||
|
||||
export type UseModalReturnType = [RegisterFn, ModalMethods];
|
59
web/src/components/SvgIcon/index.vue
Normal file
59
web/src/components/SvgIcon/index.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<component :is="component" :class="className" aria-hidden="true">
|
||||
<use :href="iconName" />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SvgIcon',
|
||||
props: {
|
||||
prefix: {
|
||||
type: String,
|
||||
default: 'icon',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
component(): string {
|
||||
return this.prefix === 'icon' ? 'svg' : 'i'
|
||||
},
|
||||
iconName(): string {
|
||||
return `#${this.prefix}-${this.name}`
|
||||
},
|
||||
className(): string {
|
||||
if (this.prefix === 'icon') {
|
||||
return 'svg-icon'
|
||||
} else if (this.prefix === 'iconfont') {
|
||||
return 'iconfont icon-' + this.name
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
.svg-icon:hover {
|
||||
fill: var(--primary-color-hover);
|
||||
}
|
||||
|
||||
.svg-external-icon {
|
||||
background-color: currentColor;
|
||||
mask-size: cover !important;
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
4
web/src/components/Table/index.ts
Normal file
4
web/src/components/Table/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as BasicTable } from './src/Table.vue';
|
||||
export { default as TableAction } from './src/components/TableAction.vue';
|
||||
export * from './src/types/table';
|
||||
export * from './src/types/tableAction';
|
353
web/src/components/Table/src/Table.vue
Normal file
353
web/src/components/Table/src/Table.vue
Normal file
@@ -0,0 +1,353 @@
|
||||
<template>
|
||||
<div class="table-toolbar">
|
||||
<!--顶部左侧区域-->
|
||||
<div class="flex items-center table-toolbar-left">
|
||||
<template v-if="title">
|
||||
<div class="table-toolbar-left-title">
|
||||
{{ title }}
|
||||
<n-tooltip trigger="hover" v-if="titleTooltip">
|
||||
<template #trigger>
|
||||
<n-icon size="18" class="ml-1 text-gray-400 cursor-pointer">
|
||||
<QuestionCircleOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
{{ titleTooltip }}
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<slot name="tableTitle"></slot>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center table-toolbar-right">
|
||||
<!--顶部右侧区域-->
|
||||
<slot name="toolbar"></slot>
|
||||
|
||||
<!--斑马纹-->
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="mr-2 table-toolbar-right-icon">
|
||||
<n-switch v-model:value="isStriped" @update:value="setStriped" />
|
||||
</div>
|
||||
</template>
|
||||
<span>表格斑马纹</span>
|
||||
</n-tooltip>
|
||||
<n-divider vertical />
|
||||
|
||||
<!--刷新-->
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="table-toolbar-right-icon" @click="reload">
|
||||
<n-icon size="18">
|
||||
<ReloadOutlined />
|
||||
</n-icon>
|
||||
</div>
|
||||
</template>
|
||||
<span>刷新</span>
|
||||
</n-tooltip>
|
||||
|
||||
<!--密度-->
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="table-toolbar-right-icon">
|
||||
<n-dropdown
|
||||
@select="densitySelect"
|
||||
trigger="click"
|
||||
:options="densityOptions"
|
||||
v-model:value="tableSize"
|
||||
>
|
||||
<n-icon size="18">
|
||||
<ColumnHeightOutlined />
|
||||
</n-icon>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
<span>密度</span>
|
||||
</n-tooltip>
|
||||
|
||||
<!--表格设置单独抽离成组件-->
|
||||
<ColumnSetting />
|
||||
</div>
|
||||
</div>
|
||||
<div class="s-table">
|
||||
<n-data-table
|
||||
ref="tableElRef"
|
||||
v-bind="getBindValues"
|
||||
:striped="isStriped"
|
||||
:pagination="pagination"
|
||||
@update:page="updatePage"
|
||||
@update:page-size="updatePageSize"
|
||||
>
|
||||
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
|
||||
<slot :name="item" v-bind="data"></slot>
|
||||
</template>
|
||||
</n-data-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
ref,
|
||||
defineComponent,
|
||||
reactive,
|
||||
unref,
|
||||
toRaw,
|
||||
computed,
|
||||
toRefs,
|
||||
onMounted,
|
||||
nextTick,
|
||||
} from 'vue';
|
||||
import { ReloadOutlined, ColumnHeightOutlined, QuestionCircleOutlined } from '@vicons/antd';
|
||||
import { createTableContext } from './hooks/useTableContext';
|
||||
|
||||
import ColumnSetting from './components/settings/ColumnSetting.vue';
|
||||
|
||||
import { useLoading } from './hooks/useLoading';
|
||||
import { useColumns } from './hooks/useColumns';
|
||||
import { useDataSource } from './hooks/useDataSource';
|
||||
import { usePagination } from './hooks/usePagination';
|
||||
|
||||
import { basicProps } from './props';
|
||||
|
||||
import { BasicTableProps } from './types/table';
|
||||
|
||||
import { getViewportOffset } from '@/utils/domUtils';
|
||||
import { useWindowSizeFn } from '@/hooks/event/useWindowSizeFn';
|
||||
import { isBoolean } from '@/utils/is';
|
||||
|
||||
const densityOptions = [
|
||||
{
|
||||
type: 'menu',
|
||||
label: '紧凑',
|
||||
key: 'small',
|
||||
},
|
||||
{
|
||||
type: 'menu',
|
||||
label: '默认',
|
||||
key: 'medium',
|
||||
},
|
||||
{
|
||||
type: 'menu',
|
||||
label: '宽松',
|
||||
key: 'large',
|
||||
},
|
||||
];
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
ReloadOutlined,
|
||||
ColumnHeightOutlined,
|
||||
ColumnSetting,
|
||||
QuestionCircleOutlined,
|
||||
},
|
||||
props: {
|
||||
...basicProps,
|
||||
},
|
||||
emits: [
|
||||
'fetch-success',
|
||||
'fetch-error',
|
||||
'update:checked-row-keys',
|
||||
'edit-end',
|
||||
'edit-cancel',
|
||||
'edit-row-end',
|
||||
'edit-change',
|
||||
],
|
||||
setup(props, { emit }) {
|
||||
const deviceHeight = ref(150);
|
||||
const tableElRef = ref<ComponentRef>(null);
|
||||
const wrapRef = ref<Nullable<HTMLDivElement>>(null);
|
||||
let paginationEl: HTMLElement | null;
|
||||
const isStriped = ref(false);
|
||||
const tableData = ref<Recordable[]>([]);
|
||||
const innerPropsRef = ref<Partial<BasicTableProps>>();
|
||||
|
||||
const getProps = computed(() => {
|
||||
return { ...props, ...unref(innerPropsRef) } as BasicTableProps;
|
||||
});
|
||||
|
||||
const { getLoading, setLoading } = useLoading(getProps);
|
||||
|
||||
const { getPaginationInfo, setPagination } = usePagination(getProps);
|
||||
|
||||
const { getDataSourceRef, getDataSource, getRowKey, reload } = useDataSource(
|
||||
getProps,
|
||||
{
|
||||
getPaginationInfo,
|
||||
setPagination,
|
||||
tableData,
|
||||
setLoading,
|
||||
},
|
||||
emit
|
||||
);
|
||||
|
||||
const { getPageColumns, setColumns, getColumns, getCacheColumns, setCacheColumnsField } =
|
||||
useColumns(getProps);
|
||||
|
||||
const state = reactive({
|
||||
tableSize: unref(getProps as any).size || 'medium',
|
||||
isColumnSetting: false,
|
||||
});
|
||||
|
||||
//页码切换
|
||||
function updatePage(page) {
|
||||
setPagination({ page: page });
|
||||
reload();
|
||||
}
|
||||
|
||||
//分页数量切换
|
||||
function updatePageSize(size) {
|
||||
setPagination({ page: 1, pageSize: size });
|
||||
reload();
|
||||
}
|
||||
|
||||
//密度切换
|
||||
function densitySelect(e) {
|
||||
state.tableSize = e;
|
||||
}
|
||||
|
||||
//选中行
|
||||
function updateCheckedRowKeys(rowKeys) {
|
||||
emit('update:checked-row-keys', rowKeys);
|
||||
}
|
||||
|
||||
//获取表格大小
|
||||
const getTableSize = computed(() => state.tableSize);
|
||||
|
||||
//组装表格信息
|
||||
const getBindValues = computed(() => {
|
||||
const tableData = unref(getDataSourceRef);
|
||||
const maxHeight = tableData.length ? `${unref(deviceHeight)}px` : 'auto';
|
||||
return {
|
||||
...unref(getProps),
|
||||
loading: unref(getLoading),
|
||||
columns: toRaw(unref(getPageColumns)),
|
||||
rowKey: unref(getRowKey),
|
||||
data: tableData,
|
||||
size: unref(getTableSize),
|
||||
remote: true,
|
||||
'max-height': maxHeight,
|
||||
};
|
||||
});
|
||||
|
||||
//获取分页信息
|
||||
const pagination = computed(() => toRaw(unref(getPaginationInfo)));
|
||||
|
||||
function setProps(props: Partial<BasicTableProps>) {
|
||||
innerPropsRef.value = { ...unref(innerPropsRef), ...props };
|
||||
}
|
||||
|
||||
const setStriped = (value: boolean) => (isStriped.value = value);
|
||||
|
||||
const tableAction = {
|
||||
reload,
|
||||
setColumns,
|
||||
setLoading,
|
||||
setProps,
|
||||
getColumns,
|
||||
getPageColumns,
|
||||
getCacheColumns,
|
||||
setCacheColumnsField,
|
||||
emit,
|
||||
};
|
||||
|
||||
const getCanResize = computed(() => {
|
||||
const { canResize } = unref(getProps);
|
||||
return canResize;
|
||||
});
|
||||
|
||||
async function computeTableHeight() {
|
||||
const table = unref(tableElRef);
|
||||
if (!table) return;
|
||||
if (!unref(getCanResize)) return;
|
||||
const tableEl: any = table?.$el;
|
||||
const headEl = tableEl.querySelector('.n-data-table-thead ');
|
||||
const { bottomIncludeBody } = getViewportOffset(headEl);
|
||||
const headerH = 64;
|
||||
let paginationH = 2;
|
||||
let marginH = 24;
|
||||
if (!isBoolean(pagination)) {
|
||||
paginationEl = tableEl.querySelector('.n-data-table__pagination') as HTMLElement;
|
||||
if (paginationEl) {
|
||||
const offsetHeight = paginationEl.offsetHeight;
|
||||
paginationH += offsetHeight || 0;
|
||||
} else {
|
||||
paginationH += 28;
|
||||
}
|
||||
}
|
||||
let height =
|
||||
bottomIncludeBody - (headerH + paginationH + marginH + (props.resizeHeightOffset || 0));
|
||||
const maxHeight = props.maxHeight;
|
||||
height = maxHeight && maxHeight < height ? maxHeight : height;
|
||||
deviceHeight.value = height;
|
||||
}
|
||||
|
||||
useWindowSizeFn(computeTableHeight, 280);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
computeTableHeight();
|
||||
});
|
||||
});
|
||||
|
||||
createTableContext({ ...tableAction, wrapRef, getBindValues });
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
tableElRef,
|
||||
getBindValues,
|
||||
getDataSource,
|
||||
densityOptions,
|
||||
reload,
|
||||
densitySelect,
|
||||
updatePage,
|
||||
updatePageSize,
|
||||
pagination,
|
||||
tableAction,
|
||||
setStriped,
|
||||
isStriped,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.table-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 0 16px 0;
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex: 1;
|
||||
|
||||
&-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
&-right {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex: 1;
|
||||
|
||||
&-icon {
|
||||
margin-left: 12px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
color: var(--text-color);
|
||||
|
||||
:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-toolbar-inner-popover-title {
|
||||
padding: 2px 0;
|
||||
}
|
||||
</style>
|
41
web/src/components/Table/src/componentMap.ts
Normal file
41
web/src/components/Table/src/componentMap.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { Component } from 'vue';
|
||||
import {
|
||||
NInput,
|
||||
NSelect,
|
||||
NCheckbox,
|
||||
NInputNumber,
|
||||
NSwitch,
|
||||
NDatePicker,
|
||||
NTimePicker,
|
||||
} from 'naive-ui';
|
||||
import type { ComponentType } from './types/componentType';
|
||||
|
||||
export enum EventEnum {
|
||||
NInput = 'on-input',
|
||||
NInputNumber = 'on-input',
|
||||
NSelect = 'on-update:value',
|
||||
NSwitch = 'on-update:value',
|
||||
NCheckbox = 'on-update:value',
|
||||
NDatePicker = 'on-update:value',
|
||||
NTimePicker = 'on-update:value',
|
||||
}
|
||||
|
||||
const componentMap = new Map<ComponentType, Component>();
|
||||
|
||||
componentMap.set('NInput', NInput);
|
||||
componentMap.set('NInputNumber', NInputNumber);
|
||||
componentMap.set('NSelect', NSelect);
|
||||
componentMap.set('NSwitch', NSwitch);
|
||||
componentMap.set('NCheckbox', NCheckbox);
|
||||
componentMap.set('NDatePicker', NDatePicker);
|
||||
componentMap.set('NTimePicker', NTimePicker);
|
||||
|
||||
export function add(compName: ComponentType, component: Component) {
|
||||
componentMap.set(compName, component);
|
||||
}
|
||||
|
||||
export function del(compName: ComponentType) {
|
||||
componentMap.delete(compName);
|
||||
}
|
||||
|
||||
export { componentMap };
|
141
web/src/components/Table/src/components/TableAction.vue
Normal file
141
web/src/components/Table/src/components/TableAction.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="tableAction">
|
||||
<div class="flex items-center justify-center">
|
||||
<template v-for="(action, index) in getActions" :key="`${index}-${action.label}`">
|
||||
<n-button v-bind="action" class="mx-2">
|
||||
{{ action.label }}
|
||||
<template #icon v-if="action.hasOwnProperty('icon')">
|
||||
<n-icon :component="action.icon" />
|
||||
</template>
|
||||
</n-button>
|
||||
</template>
|
||||
<n-dropdown
|
||||
v-if="dropDownActions && getDropdownList.length"
|
||||
trigger="hover"
|
||||
:options="getDropdownList"
|
||||
@select="select"
|
||||
>
|
||||
<slot name="more"></slot>
|
||||
<n-button v-bind="getMoreProps" class="mx-2" v-if="!$slots.more" icon-placement="right">
|
||||
<div class="flex items-center">
|
||||
<span>更多</span>
|
||||
<n-icon size="14" class="ml-1">
|
||||
<DownOutlined />
|
||||
</n-icon>
|
||||
</div>
|
||||
<!-- <template #icon>-->
|
||||
<!-- -->
|
||||
<!-- </template>-->
|
||||
</n-button>
|
||||
</n-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, PropType, computed, toRaw } from 'vue';
|
||||
import { ActionItem } from '@/components/Table';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { isBoolean, isFunction } from '@/utils/is';
|
||||
import { DownOutlined } from '@vicons/antd';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'TableAction',
|
||||
components: { DownOutlined },
|
||||
props: {
|
||||
actions: {
|
||||
type: Array as PropType<ActionItem[]>,
|
||||
default: null,
|
||||
required: true,
|
||||
},
|
||||
dropDownActions: {
|
||||
type: Array as PropType<ActionItem[]>,
|
||||
default: null,
|
||||
},
|
||||
style: {
|
||||
type: String as PropType<String>,
|
||||
default: 'button',
|
||||
},
|
||||
select: {
|
||||
type: Function as PropType<Function>,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const { hasPermission } = usePermission();
|
||||
|
||||
const actionType =
|
||||
props.style === 'button' ? 'default' : props.style === 'text' ? 'primary' : 'default';
|
||||
const actionText =
|
||||
props.style === 'button' ? undefined : props.style === 'text' ? true : undefined;
|
||||
|
||||
const getMoreProps = computed(() => {
|
||||
return {
|
||||
text: actionText,
|
||||
type: actionType,
|
||||
size: 'small',
|
||||
};
|
||||
});
|
||||
|
||||
const getDropdownList = computed(() => {
|
||||
return (toRaw(props.dropDownActions) || [])
|
||||
.filter((action) => {
|
||||
return hasPermission(action.auth as string[]) && isIfShow(action);
|
||||
})
|
||||
.map((action) => {
|
||||
const { popConfirm } = action;
|
||||
return {
|
||||
size: 'small',
|
||||
text: actionText,
|
||||
type: actionType,
|
||||
...action,
|
||||
...popConfirm,
|
||||
onConfirm: popConfirm?.confirm,
|
||||
onCancel: popConfirm?.cancel,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
function isIfShow(action: ActionItem): boolean {
|
||||
const ifShow = action.ifShow;
|
||||
|
||||
let isIfShow = true;
|
||||
|
||||
if (isBoolean(ifShow)) {
|
||||
isIfShow = ifShow;
|
||||
}
|
||||
if (isFunction(ifShow)) {
|
||||
isIfShow = ifShow(action);
|
||||
}
|
||||
return isIfShow;
|
||||
}
|
||||
|
||||
const getActions = computed(() => {
|
||||
return (toRaw(props.actions) || [])
|
||||
.filter((action) => {
|
||||
return hasPermission(action.auth as string[]) && isIfShow(action);
|
||||
})
|
||||
.map((action) => {
|
||||
const { popConfirm } = action;
|
||||
//需要展示什么风格,自己修改一下参数
|
||||
return {
|
||||
size: 'small',
|
||||
text: actionText,
|
||||
type: actionType,
|
||||
...action,
|
||||
...(popConfirm || {}),
|
||||
onConfirm: popConfirm?.confirm,
|
||||
onCancel: popConfirm?.cancel,
|
||||
enable: !!popConfirm,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
getActions,
|
||||
getDropdownList,
|
||||
getMoreProps,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
@@ -0,0 +1,47 @@
|
||||
import type { FunctionalComponent, defineComponent } from 'vue';
|
||||
import type { ComponentType } from '../../types/componentType';
|
||||
import { componentMap } from '@/components/Table/src/componentMap';
|
||||
|
||||
import { h } from 'vue';
|
||||
|
||||
import { NPopover } from 'naive-ui';
|
||||
|
||||
export interface ComponentProps {
|
||||
component: ComponentType;
|
||||
rule: boolean;
|
||||
popoverVisible: boolean;
|
||||
ruleMessage: string;
|
||||
}
|
||||
|
||||
export const CellComponent: FunctionalComponent = (
|
||||
{ component = 'NInput', rule = true, ruleMessage, popoverVisible }: ComponentProps,
|
||||
{ attrs }
|
||||
) => {
|
||||
const Comp = componentMap.get(component) as typeof defineComponent;
|
||||
|
||||
const DefaultComp = h(Comp, attrs);
|
||||
if (!rule) {
|
||||
return DefaultComp;
|
||||
}
|
||||
return h(
|
||||
NPopover,
|
||||
{ 'display-directive': 'show', show: !!popoverVisible, manual: 'manual' },
|
||||
{
|
||||
trigger: () => DefaultComp,
|
||||
default: () =>
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
style: {
|
||||
color: 'red',
|
||||
width: '90px',
|
||||
display: 'inline-block',
|
||||
},
|
||||
},
|
||||
{
|
||||
default: () => ruleMessage,
|
||||
}
|
||||
),
|
||||
}
|
||||
);
|
||||
};
|
@@ -0,0 +1,418 @@
|
||||
<template>
|
||||
<div class="editable-cell">
|
||||
<div v-show="!isEdit" class="editable-cell-content" @click="handleEdit">
|
||||
{{ getValues }}
|
||||
<n-icon class="edit-icon" v-if="!column.editRow">
|
||||
<FormOutlined />
|
||||
</n-icon>
|
||||
</div>
|
||||
<div class="flex editable-cell-content" v-show="isEdit" v-click-outside="onClickOutside">
|
||||
<div class="editable-cell-content-comp">
|
||||
<CellComponent
|
||||
v-bind="getComponentProps"
|
||||
:component="getComponent"
|
||||
:popoverVisible="getRuleVisible"
|
||||
:ruleMessage="ruleMessage"
|
||||
:rule="getRule"
|
||||
:class="getWrapperClass"
|
||||
ref="elRef"
|
||||
@options-change="handleOptionsChange"
|
||||
@pressEnter="handleEnter"
|
||||
/>
|
||||
</div>
|
||||
<div class="editable-cell-action" v-if="!getRowEditable">
|
||||
<n-icon class="mx-2 cursor-pointer">
|
||||
<CheckOutlined @click="handleSubmit" />
|
||||
</n-icon>
|
||||
<n-icon class="mx-2 cursor-pointer">
|
||||
<CloseOutlined @click="handleCancel" />
|
||||
</n-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import type { BasicColumn } from '../../types/table';
|
||||
import type { EditRecordRow } from './index';
|
||||
|
||||
import { defineComponent, ref, unref, nextTick, computed, watchEffect, toRaw } from 'vue';
|
||||
import { FormOutlined, CloseOutlined, CheckOutlined } from '@vicons/antd';
|
||||
import { CellComponent } from './CellComponent';
|
||||
|
||||
import { useTableContext } from '../../hooks/useTableContext';
|
||||
|
||||
import clickOutside from '@/directives/clickOutside';
|
||||
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
import { isString, isBoolean, isFunction, isNumber, isArray } from '@/utils/is';
|
||||
import { createPlaceholderMessage } from './helper';
|
||||
import { set, omit } from 'lodash-es';
|
||||
import { EventEnum } from '@/components/Table/src/componentMap';
|
||||
|
||||
import { parseISO, format } from 'date-fns';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'EditableCell',
|
||||
components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent },
|
||||
directives: {
|
||||
clickOutside,
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
type: [String, Number, Boolean, Object] as PropType<string | number | boolean | Recordable>,
|
||||
default: '',
|
||||
},
|
||||
record: {
|
||||
type: Object as PropType<EditRecordRow>,
|
||||
},
|
||||
column: {
|
||||
type: Object as PropType<BasicColumn>,
|
||||
default: () => ({}),
|
||||
},
|
||||
index: propTypes.number,
|
||||
},
|
||||
setup(props) {
|
||||
const table = useTableContext();
|
||||
const isEdit = ref(false);
|
||||
const elRef = ref();
|
||||
const ruleVisible = ref(false);
|
||||
const ruleMessage = ref('');
|
||||
const optionsRef = ref<LabelValueOptions>([]);
|
||||
const currentValueRef = ref<any>(props.value);
|
||||
const defaultValueRef = ref<any>(props.value);
|
||||
|
||||
// const { prefixCls } = useDesign('editable-cell');
|
||||
|
||||
const getComponent = computed(() => props.column?.editComponent || 'NInput');
|
||||
const getRule = computed(() => props.column?.editRule);
|
||||
|
||||
const getRuleVisible = computed(() => {
|
||||
return unref(ruleMessage) && unref(ruleVisible);
|
||||
});
|
||||
|
||||
const getIsCheckComp = computed(() => {
|
||||
const component = unref(getComponent);
|
||||
return ['NCheckbox', 'NRadio'].includes(component);
|
||||
});
|
||||
|
||||
const getComponentProps = computed(() => {
|
||||
const compProps = props.column?.editComponentProps ?? {};
|
||||
const editComponent = props.column?.editComponent ?? null;
|
||||
const component = unref(getComponent);
|
||||
const apiSelectProps: Recordable = {};
|
||||
|
||||
const isCheckValue = unref(getIsCheckComp);
|
||||
|
||||
let valueField = isCheckValue ? 'checked' : 'value';
|
||||
const val = unref(currentValueRef);
|
||||
|
||||
let value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
|
||||
|
||||
//TODO 特殊处理 NDatePicker 可能要根据项目 规范自行调整代码
|
||||
if (component === 'NDatePicker') {
|
||||
if (isString(value)) {
|
||||
if (compProps.valueFormat) {
|
||||
valueField = 'formatted-value';
|
||||
} else {
|
||||
value = parseISO(value as any).getTime();
|
||||
}
|
||||
} else if (isArray(value)) {
|
||||
if (compProps.valueFormat) {
|
||||
valueField = 'formatted-value';
|
||||
} else {
|
||||
value = value.map((item) => parseISO(item).getTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onEvent: any = editComponent ? EventEnum[editComponent] : undefined;
|
||||
|
||||
return {
|
||||
placeholder: createPlaceholderMessage(unref(getComponent)),
|
||||
...apiSelectProps,
|
||||
...omit(compProps, 'onChange'),
|
||||
[onEvent]: handleChange,
|
||||
[valueField]: value,
|
||||
};
|
||||
});
|
||||
|
||||
const getValues = computed(() => {
|
||||
const { editComponentProps, editValueMap } = props.column;
|
||||
|
||||
const value = unref(currentValueRef);
|
||||
|
||||
if (editValueMap && isFunction(editValueMap)) {
|
||||
return editValueMap(value);
|
||||
}
|
||||
|
||||
const component = unref(getComponent);
|
||||
if (!component.includes('NSelect')) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const options: LabelValueOptions = editComponentProps?.options ?? (unref(optionsRef) || []);
|
||||
const option = options.find((item) => `${item.value}` === `${value}`);
|
||||
|
||||
return option?.label ?? value;
|
||||
});
|
||||
|
||||
const getWrapperClass = computed(() => {
|
||||
const { align = 'center' } = props.column;
|
||||
return `edit-cell-align-${align}`;
|
||||
});
|
||||
|
||||
const getRowEditable = computed(() => {
|
||||
const { editable } = props.record || {};
|
||||
return !!editable;
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
defaultValueRef.value = props.value;
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
const { editable } = props.column;
|
||||
if (isBoolean(editable) || isBoolean(unref(getRowEditable))) {
|
||||
isEdit.value = !!editable || unref(getRowEditable);
|
||||
}
|
||||
});
|
||||
|
||||
function handleEdit() {
|
||||
if (unref(getRowEditable) || unref(props.column?.editRow)) return;
|
||||
ruleMessage.value = '';
|
||||
isEdit.value = true;
|
||||
nextTick(() => {
|
||||
const el = unref(elRef);
|
||||
el?.focus?.();
|
||||
});
|
||||
}
|
||||
|
||||
async function handleChange(e: any) {
|
||||
const component = unref(getComponent);
|
||||
const compProps = props.column?.editComponentProps ?? {};
|
||||
if (!e) {
|
||||
currentValueRef.value = e;
|
||||
} else if (e?.target && Reflect.has(e.target, 'value')) {
|
||||
currentValueRef.value = (e as ChangeEvent).target.value;
|
||||
} else if (component === 'NCheckbox') {
|
||||
currentValueRef.value = (e as ChangeEvent).target.checked;
|
||||
} else if (isString(e) || isBoolean(e) || isNumber(e)) {
|
||||
currentValueRef.value = e;
|
||||
}
|
||||
|
||||
//TODO 特殊处理 NDatePicker 可能要根据项目 规范自行调整代码
|
||||
if (component === 'NDatePicker') {
|
||||
if (isNumber(currentValueRef.value)) {
|
||||
if (compProps.valueFormat) {
|
||||
currentValueRef.value = format(currentValueRef.value, compProps.valueFormat);
|
||||
}
|
||||
} else if (isArray(currentValueRef.value)) {
|
||||
if (compProps.valueFormat) {
|
||||
currentValueRef.value = currentValueRef.value.map((item) => {
|
||||
format(item, compProps.valueFormat);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const onChange = props.column?.editComponentProps?.onChange;
|
||||
if (onChange && isFunction(onChange)) onChange(...arguments);
|
||||
|
||||
table.emit?.('edit-change', {
|
||||
column: props.column,
|
||||
value: unref(currentValueRef),
|
||||
record: toRaw(props.record),
|
||||
});
|
||||
await handleSubmiRule();
|
||||
}
|
||||
|
||||
async function handleSubmiRule() {
|
||||
const { column, record } = props;
|
||||
const { editRule } = column;
|
||||
const currentValue = unref(currentValueRef);
|
||||
|
||||
if (editRule) {
|
||||
if (isBoolean(editRule) && !currentValue && !isNumber(currentValue)) {
|
||||
ruleVisible.value = true;
|
||||
const component = unref(getComponent);
|
||||
ruleMessage.value = createPlaceholderMessage(component);
|
||||
return false;
|
||||
}
|
||||
if (isFunction(editRule)) {
|
||||
const res = await editRule(currentValue, record as Recordable);
|
||||
if (!!res) {
|
||||
ruleMessage.value = res;
|
||||
ruleVisible.value = true;
|
||||
return false;
|
||||
} else {
|
||||
ruleMessage.value = '';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ruleMessage.value = '';
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleSubmit(needEmit = true, valid = true) {
|
||||
if (valid) {
|
||||
const isPass = await handleSubmiRule();
|
||||
if (!isPass) return false;
|
||||
}
|
||||
|
||||
const { column, index, record } = props;
|
||||
if (!record) return false;
|
||||
const { key } = column;
|
||||
const value = unref(currentValueRef);
|
||||
if (!key) return;
|
||||
|
||||
const dataKey = key as string;
|
||||
|
||||
set(record, dataKey, value);
|
||||
//const record = await table.updateTableData(index, dataKey, value);
|
||||
needEmit && table.emit?.('edit-end', { record, index, key, value });
|
||||
isEdit.value = false;
|
||||
}
|
||||
|
||||
async function handleEnter() {
|
||||
if (props.column?.editRow) {
|
||||
return;
|
||||
}
|
||||
await handleSubmit();
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
isEdit.value = false;
|
||||
currentValueRef.value = defaultValueRef.value;
|
||||
const { column, index, record } = props;
|
||||
const { key } = column;
|
||||
ruleVisible.value = true;
|
||||
ruleMessage.value = '';
|
||||
table.emit?.('edit-cancel', {
|
||||
record,
|
||||
index,
|
||||
key: key,
|
||||
value: unref(currentValueRef),
|
||||
});
|
||||
}
|
||||
|
||||
function onClickOutside() {
|
||||
if (props.column?.editable || unref(getRowEditable)) {
|
||||
return;
|
||||
}
|
||||
const component = unref(getComponent);
|
||||
|
||||
if (component.includes('NInput')) {
|
||||
handleCancel();
|
||||
}
|
||||
}
|
||||
|
||||
// only ApiSelect
|
||||
function handleOptionsChange(options: LabelValueOptions) {
|
||||
optionsRef.value = options;
|
||||
}
|
||||
|
||||
function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) {
|
||||
if (props.record) {
|
||||
/* eslint-disable */
|
||||
isArray(props.record[cbs])
|
||||
? props.record[cbs]?.push(handle)
|
||||
: (props.record[cbs] = [handle]);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.record) {
|
||||
initCbs('submitCbs', handleSubmit);
|
||||
initCbs('validCbs', handleSubmiRule);
|
||||
initCbs('cancelCbs', handleCancel);
|
||||
|
||||
if (props.column.key) {
|
||||
if (!props.record.editValueRefs) props.record.editValueRefs = {};
|
||||
props.record.editValueRefs[props.column.key] = currentValueRef;
|
||||
}
|
||||
/* eslint-disable */
|
||||
props.record.onCancelEdit = () => {
|
||||
isArray(props.record?.cancelCbs) && props.record?.cancelCbs.forEach((fn) => fn());
|
||||
};
|
||||
/* eslint-disable */
|
||||
props.record.onSubmitEdit = async () => {
|
||||
if (isArray(props.record?.submitCbs)) {
|
||||
const validFns = (props.record?.validCbs || []).map((fn) => fn());
|
||||
|
||||
const res = await Promise.all(validFns);
|
||||
|
||||
const pass = res.every((item) => !!item);
|
||||
|
||||
if (!pass) return;
|
||||
const submitFns = props.record?.submitCbs || [];
|
||||
submitFns.forEach((fn) => fn(false, false));
|
||||
table.emit?.('edit-row-end');
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isEdit,
|
||||
handleEdit,
|
||||
currentValueRef,
|
||||
handleSubmit,
|
||||
handleChange,
|
||||
handleCancel,
|
||||
elRef,
|
||||
getComponent,
|
||||
getRule,
|
||||
onClickOutside,
|
||||
ruleMessage,
|
||||
getRuleVisible,
|
||||
getComponentProps,
|
||||
handleOptionsChange,
|
||||
getWrapperClass,
|
||||
getRowEditable,
|
||||
getValues,
|
||||
handleEnter,
|
||||
// getSize,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.editable-cell {
|
||||
&-content {
|
||||
position: relative;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&-comp {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.edit-icon {
|
||||
font-size: 14px;
|
||||
//position: absolute;
|
||||
//top: 4px;
|
||||
//right: 0;
|
||||
display: none;
|
||||
width: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.edit-icon {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
15
web/src/components/Table/src/components/editable/helper.ts
Normal file
15
web/src/components/Table/src/components/editable/helper.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ComponentType } from '../../types/componentType';
|
||||
|
||||
/**
|
||||
* @description: 生成placeholder
|
||||
*/
|
||||
export function createPlaceholderMessage(component: ComponentType) {
|
||||
if (component === 'NInput') return '请输入';
|
||||
if (
|
||||
['NPicker', 'NSelect', 'NCheckbox', 'NRadio', 'NSwitch', 'NDatePicker', 'NTimePicker'].includes(
|
||||
component
|
||||
)
|
||||
)
|
||||
return '请选择';
|
||||
return '';
|
||||
}
|
49
web/src/components/Table/src/components/editable/index.ts
Normal file
49
web/src/components/Table/src/components/editable/index.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { BasicColumn } from '@/components/Table/src/types/table';
|
||||
import { h, Ref } from 'vue';
|
||||
|
||||
import EditableCell from './EditableCell.vue';
|
||||
|
||||
export function renderEditCell(column: BasicColumn) {
|
||||
return (record, index) => {
|
||||
const _key = column.key;
|
||||
const value = record[_key];
|
||||
record.onEdit = async (edit: boolean, submit = false) => {
|
||||
if (!submit) {
|
||||
record.editable = edit;
|
||||
}
|
||||
|
||||
if (!edit && submit) {
|
||||
const res = await record.onSubmitEdit?.();
|
||||
if (res) {
|
||||
record.editable = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// cancel
|
||||
if (!edit && !submit) {
|
||||
record.onCancelEdit?.();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return h(EditableCell, {
|
||||
value,
|
||||
record,
|
||||
column,
|
||||
index,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export type EditRecordRow<T = Recordable> = Partial<
|
||||
{
|
||||
onEdit: (editable: boolean, submit?: boolean) => Promise<boolean>;
|
||||
editable: boolean;
|
||||
onCancel: Fn;
|
||||
onSubmit: Fn;
|
||||
submitCbs: Fn[];
|
||||
cancelCbs: Fn[];
|
||||
validCbs: Fn[];
|
||||
editValueRefs: Recordable<Ref>;
|
||||
} & T
|
||||
>;
|
@@ -0,0 +1,335 @@
|
||||
<template>
|
||||
<n-tooltip trigger="hover">
|
||||
<template #trigger>
|
||||
<div class="cursor-pointer table-toolbar-right-icon">
|
||||
<n-popover trigger="click" :width="230" class="toolbar-popover" placement="bottom-end">
|
||||
<template #trigger>
|
||||
<n-icon size="18">
|
||||
<SettingOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
<template #header>
|
||||
<div class="table-toolbar-inner-popover-title">
|
||||
<n-space>
|
||||
<n-checkbox v-model:checked="checkAll" @update:checked="onCheckAll"
|
||||
>列展示
|
||||
</n-checkbox>
|
||||
<n-checkbox v-model:checked="selection" @update:checked="onSelection"
|
||||
>勾选列
|
||||
</n-checkbox>
|
||||
<n-button text type="info" size="small" class="mt-1" @click="resetColumns"
|
||||
>重置
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
</template>
|
||||
<div class="table-toolbar-inner">
|
||||
<n-checkbox-group v-model:value="checkList" @update:value="onChange">
|
||||
<Draggable
|
||||
v-model="columnsList"
|
||||
animation="300"
|
||||
item-key="key"
|
||||
filter=".no-draggable"
|
||||
:move="onMove"
|
||||
@end="draggableEnd"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div
|
||||
class="table-toolbar-inner-checkbox"
|
||||
:class="{
|
||||
'table-toolbar-inner-checkbox-dark': getDarkTheme === true,
|
||||
'no-draggable': element.draggable === false,
|
||||
}"
|
||||
>
|
||||
<span
|
||||
class="drag-icon"
|
||||
:class="{ 'drag-icon-hidden': element.draggable === false }"
|
||||
>
|
||||
<n-icon size="18">
|
||||
<DragOutlined />
|
||||
</n-icon>
|
||||
</span>
|
||||
<n-checkbox :value="element.key" :label="element.title" />
|
||||
<div class="fixed-item">
|
||||
<n-tooltip trigger="hover" placement="bottom">
|
||||
<template #trigger>
|
||||
<n-icon
|
||||
size="18"
|
||||
:color="element.fixed === 'left' ? '#2080f0' : undefined"
|
||||
class="cursor-pointer"
|
||||
@click="fixedColumn(element, 'left')"
|
||||
>
|
||||
<VerticalRightOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
<span>固定到左侧</span>
|
||||
</n-tooltip>
|
||||
<n-divider vertical />
|
||||
<n-tooltip trigger="hover" placement="bottom">
|
||||
<template #trigger>
|
||||
<n-icon
|
||||
size="18"
|
||||
:color="element.fixed === 'right' ? '#2080f0' : undefined"
|
||||
class="cursor-pointer"
|
||||
@click="fixedColumn(element, 'right')"
|
||||
>
|
||||
<VerticalLeftOutlined />
|
||||
</n-icon>
|
||||
</template>
|
||||
<span>固定到右侧</span>
|
||||
</n-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Draggable>
|
||||
</n-checkbox-group>
|
||||
</div>
|
||||
</n-popover>
|
||||
</div>
|
||||
</template>
|
||||
<span>列设置</span>
|
||||
</n-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { computed, defineComponent, reactive, ref, toRaw, toRefs, unref, watchEffect } from 'vue';
|
||||
import { useTableContext } from '../../hooks/useTableContext';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import {
|
||||
DragOutlined,
|
||||
SettingOutlined,
|
||||
VerticalLeftOutlined,
|
||||
VerticalRightOutlined,
|
||||
} from '@vicons/antd';
|
||||
import Draggable from 'vuedraggable/src/vuedraggable';
|
||||
import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
|
||||
|
||||
interface Options {
|
||||
title: string;
|
||||
key: string;
|
||||
fixed?: boolean | 'left' | 'right';
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ColumnSetting',
|
||||
components: {
|
||||
SettingOutlined,
|
||||
DragOutlined,
|
||||
Draggable,
|
||||
VerticalRightOutlined,
|
||||
VerticalLeftOutlined,
|
||||
},
|
||||
setup() {
|
||||
const { getDarkTheme } = useDesignSetting();
|
||||
const table: any = useTableContext();
|
||||
const columnsList = ref<Options[]>([]);
|
||||
const cacheColumnsList = ref<Options[]>([]);
|
||||
|
||||
const state = reactive({
|
||||
selection: true,
|
||||
checkAll: true,
|
||||
checkList: [],
|
||||
defaultCheckList: [],
|
||||
});
|
||||
|
||||
const getSelection = computed(() => {
|
||||
return state.selection;
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
const columns = table.getColumns();
|
||||
if (columns.length) {
|
||||
init();
|
||||
}
|
||||
});
|
||||
|
||||
//初始化
|
||||
function init() {
|
||||
const columns: any[] = getColumns();
|
||||
const checkList: any = columns.map((item) => item.key);
|
||||
state.checkList = checkList;
|
||||
state.defaultCheckList = checkList;
|
||||
const newColumns = columns.filter((item) => item.key != 'action' && item.title != '操作');
|
||||
if (!columnsList.value.length) {
|
||||
columnsList.value = cloneDeep(newColumns);
|
||||
cacheColumnsList.value = cloneDeep(newColumns);
|
||||
}
|
||||
onSelection(true);
|
||||
}
|
||||
|
||||
//切换
|
||||
function onChange(checkList) {
|
||||
if (state.selection) {
|
||||
checkList.unshift('selection');
|
||||
}
|
||||
setColumns(checkList);
|
||||
}
|
||||
|
||||
//设置
|
||||
function setColumns(columns) {
|
||||
table.setColumns(columns);
|
||||
}
|
||||
|
||||
//获取
|
||||
function getColumns() {
|
||||
let newRet: any[] = [];
|
||||
table.getColumns().forEach((item) => {
|
||||
newRet.push({ ...item });
|
||||
});
|
||||
return newRet;
|
||||
}
|
||||
|
||||
//重置
|
||||
function resetColumns() {
|
||||
state.checkList = [...state.defaultCheckList];
|
||||
state.checkAll = true;
|
||||
let cacheColumnsKeys: any[] = table.getCacheColumns();
|
||||
let newColumns = cacheColumnsKeys.map((item) => {
|
||||
return {
|
||||
...item,
|
||||
fixed: undefined,
|
||||
};
|
||||
});
|
||||
setColumns(newColumns);
|
||||
columnsList.value = newColumns;
|
||||
}
|
||||
|
||||
//全选
|
||||
function onCheckAll(e) {
|
||||
let checkList = table.getCacheColumns(true);
|
||||
if (e) {
|
||||
setColumns(checkList);
|
||||
state.checkList = checkList;
|
||||
} else {
|
||||
setColumns([]);
|
||||
state.checkList = [];
|
||||
}
|
||||
}
|
||||
|
||||
//拖拽排序
|
||||
function draggableEnd() {
|
||||
const newColumns = toRaw(unref(columnsList));
|
||||
columnsList.value = newColumns;
|
||||
setColumns(newColumns);
|
||||
}
|
||||
|
||||
//勾选列
|
||||
function onSelection(e) {
|
||||
let checkList = table.getCacheColumns();
|
||||
if (e) {
|
||||
if (checkList[0].type === undefined || checkList[0].type !== 'selection') {
|
||||
checkList.unshift({ type: 'selection', key: 'selection' });
|
||||
setColumns(checkList);
|
||||
}
|
||||
} else {
|
||||
checkList.splice(0, 1);
|
||||
setColumns(checkList);
|
||||
}
|
||||
}
|
||||
|
||||
function onMove(e) {
|
||||
if (e.draggedContext.element.draggable === false) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
//固定
|
||||
function fixedColumn(item, fixed) {
|
||||
if (!state.checkList.includes(item.key)) return;
|
||||
let columns = getColumns();
|
||||
const isFixed = item.fixed === fixed ? undefined : fixed;
|
||||
let index = columns.findIndex((res) => res.key === item.key);
|
||||
if (index !== -1) {
|
||||
columns[index].fixed = isFixed;
|
||||
}
|
||||
table.setCacheColumnsField(item.key, { fixed: isFixed });
|
||||
columnsList.value[index].fixed = isFixed;
|
||||
setColumns(columns);
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
columnsList,
|
||||
getDarkTheme,
|
||||
onChange,
|
||||
onCheckAll,
|
||||
onSelection,
|
||||
onMove,
|
||||
resetColumns,
|
||||
fixedColumn,
|
||||
draggableEnd,
|
||||
getSelection,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.table-toolbar {
|
||||
&-inner-popover-title {
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
&-right {
|
||||
&-icon {
|
||||
margin-left: 12px;
|
||||
font-size: 16px;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
|
||||
:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table-toolbar-inner {
|
||||
&-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
|
||||
&:hover {
|
||||
background: #e6f7ff;
|
||||
}
|
||||
|
||||
.drag-icon {
|
||||
display: inline-flex;
|
||||
margin-right: 8px;
|
||||
cursor: move;
|
||||
|
||||
&-hidden {
|
||||
visibility: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.fixed-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.ant-checkbox-wrapper {
|
||||
flex: 1;
|
||||
|
||||
&:hover {
|
||||
color: #1890ff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-checkbox-dark {
|
||||
&:hover {
|
||||
background: hsla(0, 0%, 100%, 0.08);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar-popover {
|
||||
.n-popover__content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
11
web/src/components/Table/src/const.ts
Normal file
11
web/src/components/Table/src/const.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import componentSetting from '@/settings/componentSetting';
|
||||
|
||||
const { table } = componentSetting;
|
||||
|
||||
const { apiSetting, defaultPageSize, pageSizes } = table;
|
||||
|
||||
export const DEFAULTPAGESIZE = defaultPageSize;
|
||||
|
||||
export const APISETTING = apiSetting;
|
||||
|
||||
export const PAGESIZES = pageSizes;
|
164
web/src/components/Table/src/hooks/useColumns.ts
Normal file
164
web/src/components/Table/src/hooks/useColumns.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { ref, Ref, ComputedRef, unref, computed, watch, toRaw, h } from 'vue';
|
||||
import type { BasicColumn, BasicTableProps } from '../types/table';
|
||||
import { isEqual, cloneDeep } from 'lodash-es';
|
||||
import { isArray, isString, isBoolean, isFunction } from '@/utils/is';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { ActionItem } from '@/components/Table';
|
||||
import { renderEditCell } from '../components/editable';
|
||||
import { NTooltip, NIcon } from 'naive-ui';
|
||||
import { FormOutlined } from '@vicons/antd';
|
||||
|
||||
export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
|
||||
const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>;
|
||||
let cacheColumns = unref(propsRef).columns;
|
||||
|
||||
const getColumnsRef = computed(() => {
|
||||
const columns = cloneDeep(unref(columnsRef));
|
||||
|
||||
handleActionColumn(propsRef, columns);
|
||||
if (!columns) return [];
|
||||
return columns;
|
||||
});
|
||||
|
||||
const { hasPermission } = usePermission();
|
||||
|
||||
function isIfShow(action: ActionItem): boolean {
|
||||
const ifShow = action.ifShow;
|
||||
|
||||
let isIfShow = true;
|
||||
|
||||
if (isBoolean(ifShow)) {
|
||||
isIfShow = ifShow;
|
||||
}
|
||||
if (isFunction(ifShow)) {
|
||||
isIfShow = ifShow(action);
|
||||
}
|
||||
return isIfShow;
|
||||
}
|
||||
|
||||
const renderTooltip = (trigger, content) => {
|
||||
return h(NTooltip, null, {
|
||||
trigger: () => trigger,
|
||||
default: () => content,
|
||||
});
|
||||
};
|
||||
|
||||
const getPageColumns = computed(() => {
|
||||
const pageColumns = unref(getColumnsRef);
|
||||
const columns = cloneDeep(pageColumns);
|
||||
return columns
|
||||
.filter((column) => {
|
||||
return hasPermission(column.auth as string[]) && isIfShow(column);
|
||||
})
|
||||
.map((column) => {
|
||||
//默认 ellipsis 为true
|
||||
column.ellipsis = typeof column.ellipsis === 'undefined' ? { tooltip: true } : false;
|
||||
const { edit } = column;
|
||||
if (edit) {
|
||||
column.render = renderEditCell(column);
|
||||
if (edit) {
|
||||
const title: any = column.title;
|
||||
column.title = () => {
|
||||
return renderTooltip(
|
||||
h('span', {}, [
|
||||
h('span', { style: { 'margin-right': '5px' } }, title),
|
||||
h(
|
||||
NIcon,
|
||||
{
|
||||
size: 14,
|
||||
},
|
||||
{
|
||||
default: () => h(FormOutlined),
|
||||
}
|
||||
),
|
||||
]),
|
||||
'该列可编辑'
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
return column;
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
() => unref(propsRef).columns,
|
||||
(columns) => {
|
||||
columnsRef.value = columns;
|
||||
cacheColumns = columns;
|
||||
}
|
||||
);
|
||||
|
||||
function handleActionColumn(propsRef: ComputedRef<BasicTableProps>, columns: BasicColumn[]) {
|
||||
const { actionColumn } = unref(propsRef);
|
||||
if (!actionColumn) return;
|
||||
!columns.find((col) => col.key === 'action') &&
|
||||
columns.push({
|
||||
...(actionColumn as any),
|
||||
});
|
||||
}
|
||||
|
||||
//设置
|
||||
function setColumns(columnList: string[]) {
|
||||
const columns: any[] = cloneDeep(columnList);
|
||||
if (!isArray(columns)) return;
|
||||
|
||||
if (!columns.length) {
|
||||
columnsRef.value = [];
|
||||
return;
|
||||
}
|
||||
const cacheKeys = cacheColumns.map((item) => item.key);
|
||||
//针对拖拽排序
|
||||
if (!isString(columns[0])) {
|
||||
columnsRef.value = columns;
|
||||
} else {
|
||||
const newColumns: any[] = [];
|
||||
cacheColumns.forEach((item) => {
|
||||
if (columnList.includes(item.key)) {
|
||||
newColumns.push({ ...item });
|
||||
}
|
||||
});
|
||||
if (!isEqual(cacheKeys, columns)) {
|
||||
newColumns.sort((prev, next) => {
|
||||
return cacheKeys.indexOf(prev.key) - cacheKeys.indexOf(next.key);
|
||||
});
|
||||
}
|
||||
columnsRef.value = newColumns;
|
||||
}
|
||||
}
|
||||
|
||||
//获取
|
||||
function getColumns(): BasicColumn[] {
|
||||
const columns = toRaw(unref(getColumnsRef));
|
||||
return columns.map((item) => {
|
||||
return { ...item, title: item.title, key: item.key, fixed: item.fixed || undefined };
|
||||
});
|
||||
}
|
||||
|
||||
//获取原始
|
||||
function getCacheColumns(isKey?: boolean): any[] {
|
||||
return isKey ? cacheColumns.map((item) => item.key) : cacheColumns;
|
||||
}
|
||||
|
||||
//更新原始数据单个字段
|
||||
function setCacheColumnsField(key: string | undefined, value: Partial<BasicColumn>) {
|
||||
if (!key || !value) {
|
||||
return;
|
||||
}
|
||||
cacheColumns.forEach((item) => {
|
||||
if (item.key === key) {
|
||||
Object.assign(item, value);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
getColumnsRef,
|
||||
getCacheColumns,
|
||||
setCacheColumnsField,
|
||||
setColumns,
|
||||
getColumns,
|
||||
getPageColumns,
|
||||
};
|
||||
}
|
145
web/src/components/Table/src/hooks/useDataSource.ts
Normal file
145
web/src/components/Table/src/hooks/useDataSource.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import { ref, ComputedRef, unref, computed, onMounted, watchEffect, watch } from 'vue';
|
||||
import type { BasicTableProps } from '../types/table';
|
||||
import type { PaginationProps } from '../types/pagination';
|
||||
import { isBoolean, isFunction, isArray } from '@/utils/is';
|
||||
import { APISETTING } from '../const';
|
||||
|
||||
export function useDataSource(
|
||||
propsRef: ComputedRef<BasicTableProps>,
|
||||
{ getPaginationInfo, setPagination, setLoading, tableData },
|
||||
emit
|
||||
) {
|
||||
const dataSourceRef = ref<Recordable[]>([]);
|
||||
|
||||
watchEffect(() => {
|
||||
tableData.value = unref(dataSourceRef);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => unref(propsRef).dataSource,
|
||||
() => {
|
||||
const { dataSource }: any = unref(propsRef);
|
||||
dataSource && (dataSourceRef.value = dataSource);
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const getRowKey = computed(() => {
|
||||
const { rowKey }: any = unref(propsRef);
|
||||
return rowKey
|
||||
? rowKey
|
||||
: () => {
|
||||
return 'key';
|
||||
};
|
||||
});
|
||||
|
||||
const getDataSourceRef = computed(() => {
|
||||
const dataSource = unref(dataSourceRef);
|
||||
if (!dataSource || dataSource.length === 0) {
|
||||
return unref(dataSourceRef);
|
||||
}
|
||||
return unref(dataSourceRef);
|
||||
});
|
||||
|
||||
async function fetch(opt?) {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { request, pagination, beforeRequest, afterRequest }: any = unref(propsRef);
|
||||
if (!request) return;
|
||||
//组装分页信息
|
||||
const pageField = APISETTING.pageField;
|
||||
const sizeField = APISETTING.sizeField;
|
||||
const totalField = APISETTING.totalField;
|
||||
const listField = APISETTING.listField;
|
||||
|
||||
let pageParams = {};
|
||||
const { page = 1, pageSize = 10 } = unref(getPaginationInfo) as PaginationProps;
|
||||
|
||||
if ((isBoolean(pagination) && !pagination) || isBoolean(getPaginationInfo)) {
|
||||
pageParams = {};
|
||||
} else {
|
||||
pageParams[pageField] = (opt && opt[pageField]) || page;
|
||||
pageParams[sizeField] = pageSize;
|
||||
}
|
||||
|
||||
let params = {
|
||||
...pageParams,
|
||||
};
|
||||
if (beforeRequest && isFunction(beforeRequest)) {
|
||||
// The params parameter can be modified by outsiders
|
||||
params = (await beforeRequest(params)) || params;
|
||||
}
|
||||
const res = await request(params);
|
||||
|
||||
const resultTotal = res[totalField] || 0;
|
||||
const currentPage = res[pageField];
|
||||
|
||||
// 如果数据异常,需获取正确的页码再次执行
|
||||
if (resultTotal) {
|
||||
if (page > resultTotal) {
|
||||
setPagination({
|
||||
[pageField]: resultTotal,
|
||||
});
|
||||
fetch(opt);
|
||||
}
|
||||
}
|
||||
let resultInfo = res[listField] ? res[listField] : [];
|
||||
if (afterRequest && isFunction(afterRequest)) {
|
||||
// can modify the data returned by the interface for processing
|
||||
resultInfo = (await afterRequest(resultInfo)) || resultInfo;
|
||||
}
|
||||
dataSourceRef.value = resultInfo;
|
||||
setPagination({
|
||||
[pageField]: currentPage,
|
||||
[totalField]: resultTotal,
|
||||
});
|
||||
if (opt && opt[pageField]) {
|
||||
setPagination({
|
||||
[pageField]: opt[pageField] || 1,
|
||||
});
|
||||
}
|
||||
emit('fetch-success', {
|
||||
items: unref(resultInfo),
|
||||
resultTotal,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
emit('fetch-error', error);
|
||||
dataSourceRef.value = [];
|
||||
// setPagination({
|
||||
// pageCount: 0,
|
||||
// });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
fetch();
|
||||
}, 16);
|
||||
});
|
||||
|
||||
function setTableData(values) {
|
||||
dataSourceRef.value = values;
|
||||
}
|
||||
|
||||
function getDataSource(): any[] {
|
||||
return getDataSourceRef.value;
|
||||
}
|
||||
|
||||
async function reload(opt?) {
|
||||
await fetch(opt);
|
||||
}
|
||||
|
||||
return {
|
||||
fetch,
|
||||
getRowKey,
|
||||
getDataSourceRef,
|
||||
getDataSource,
|
||||
setTableData,
|
||||
reload,
|
||||
};
|
||||
}
|
21
web/src/components/Table/src/hooks/useLoading.ts
Normal file
21
web/src/components/Table/src/hooks/useLoading.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ref, ComputedRef, unref, computed, watch } from 'vue';
|
||||
import type { BasicTableProps } from '../types/table';
|
||||
|
||||
export function useLoading(props: ComputedRef<BasicTableProps>) {
|
||||
const loadingRef = ref(unref(props).loading);
|
||||
|
||||
watch(
|
||||
() => unref(props).loading,
|
||||
(loading) => {
|
||||
loadingRef.value = loading;
|
||||
}
|
||||
);
|
||||
|
||||
const getLoading = computed(() => unref(loadingRef));
|
||||
|
||||
function setLoading(loading: boolean) {
|
||||
loadingRef.value = loading;
|
||||
}
|
||||
|
||||
return { getLoading, setLoading };
|
||||
}
|
50
web/src/components/Table/src/hooks/usePagination.ts
Normal file
50
web/src/components/Table/src/hooks/usePagination.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { PaginationProps } from '../types/pagination';
|
||||
import type { BasicTableProps } from '../types/table';
|
||||
import { computed, unref, ref, ComputedRef } from 'vue';
|
||||
|
||||
import { isBoolean } from '@/utils/is';
|
||||
import { APISETTING, DEFAULTPAGESIZE, PAGESIZES } from '../const';
|
||||
|
||||
export function usePagination(refProps: ComputedRef<BasicTableProps>) {
|
||||
const configRef = ref<PaginationProps>({});
|
||||
const show = ref(true);
|
||||
|
||||
const getPaginationInfo = computed((): PaginationProps | boolean => {
|
||||
const { pagination } = unref(refProps);
|
||||
if (!unref(show) || (isBoolean(pagination) && !pagination)) {
|
||||
return false;
|
||||
}
|
||||
const { totalField } = APISETTING;
|
||||
return {
|
||||
pageSize: DEFAULTPAGESIZE,
|
||||
pageSizes: PAGESIZES,
|
||||
showSizePicker: true,
|
||||
showQuickJumper: true,
|
||||
...(isBoolean(pagination) ? {} : pagination),
|
||||
...unref(configRef),
|
||||
pageCount: unref(configRef)[totalField],
|
||||
};
|
||||
});
|
||||
|
||||
function setPagination(info: Partial<PaginationProps>) {
|
||||
const paginationInfo = unref(getPaginationInfo);
|
||||
configRef.value = {
|
||||
...(!isBoolean(paginationInfo) ? paginationInfo : {}),
|
||||
...info,
|
||||
};
|
||||
}
|
||||
|
||||
function getPagination() {
|
||||
return unref(getPaginationInfo);
|
||||
}
|
||||
|
||||
function getShowPagination() {
|
||||
return unref(show);
|
||||
}
|
||||
|
||||
async function setShowPagination(flag: boolean) {
|
||||
show.value = flag;
|
||||
}
|
||||
|
||||
return { getPagination, getPaginationInfo, setShowPagination, getShowPagination, setPagination };
|
||||
}
|
22
web/src/components/Table/src/hooks/useTableContext.ts
Normal file
22
web/src/components/Table/src/hooks/useTableContext.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Ref } from 'vue';
|
||||
import type { BasicTableProps, TableActionType } from '../types/table';
|
||||
import { provide, inject, ComputedRef } from 'vue';
|
||||
|
||||
const key = Symbol('s-table');
|
||||
|
||||
type Instance = TableActionType & {
|
||||
wrapRef: Ref<Nullable<HTMLElement>>;
|
||||
getBindValues: ComputedRef<Recordable>;
|
||||
};
|
||||
|
||||
type RetInstance = Omit<Instance, 'getBindValues'> & {
|
||||
getBindValues: ComputedRef<BasicTableProps>;
|
||||
};
|
||||
|
||||
export function createTableContext(instance: Instance) {
|
||||
provide(key, instance);
|
||||
}
|
||||
|
||||
export function useTableContext(): RetInstance {
|
||||
return inject(key) as RetInstance;
|
||||
}
|
59
web/src/components/Table/src/props.ts
Normal file
59
web/src/components/Table/src/props.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
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,
|
||||
},
|
||||
titleTooltip: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'medium',
|
||||
},
|
||||
dataSource: {
|
||||
type: [Object],
|
||||
default: () => [],
|
||||
},
|
||||
columns: {
|
||||
type: [Array] as PropType<BasicColumn[]>,
|
||||
default: () => [],
|
||||
required: true,
|
||||
},
|
||||
beforeRequest: {
|
||||
type: Function as PropType<(...arg: any[]) => void | Promise<any>>,
|
||||
default: null,
|
||||
},
|
||||
request: {
|
||||
type: Function as PropType<(...arg: any[]) => Promise<any>>,
|
||||
default: null,
|
||||
},
|
||||
afterRequest: {
|
||||
type: Function as PropType<(...arg: any[]) => void | Promise<any>>,
|
||||
default: null,
|
||||
},
|
||||
rowKey: {
|
||||
type: [String, Function] as PropType<string | ((record) => string)>,
|
||||
default: undefined,
|
||||
},
|
||||
pagination: {
|
||||
type: [Object, Boolean],
|
||||
default: () => {},
|
||||
},
|
||||
//废弃
|
||||
showPagination: {
|
||||
type: [String, Boolean],
|
||||
default: 'auto',
|
||||
},
|
||||
actionColumn: {
|
||||
type: Object as PropType<BasicColumn>,
|
||||
default: null,
|
||||
},
|
||||
canResize: propTypes.bool.def(true),
|
||||
resizeHeightOffset: propTypes.number.def(0),
|
||||
};
|
8
web/src/components/Table/src/types/componentType.ts
Normal file
8
web/src/components/Table/src/types/componentType.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type ComponentType =
|
||||
| 'NInput'
|
||||
| 'NInputNumber'
|
||||
| 'NSelect'
|
||||
| 'NCheckbox'
|
||||
| 'NSwitch'
|
||||
| 'NDatePicker'
|
||||
| 'NTimePicker';
|
8
web/src/components/Table/src/types/pagination.ts
Normal file
8
web/src/components/Table/src/types/pagination.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export interface PaginationProps {
|
||||
page?: number;
|
||||
pageCount?: number;
|
||||
pageSize?: number;
|
||||
pageSizes?: number[];
|
||||
showSizePicker?: boolean;
|
||||
showQuickJumper?: boolean;
|
||||
}
|
38
web/src/components/Table/src/types/table.ts
Normal file
38
web/src/components/Table/src/types/table.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { TableBaseColumn } from 'naive-ui/lib/data-table/src/interface';
|
||||
import { ComponentType } from './componentType';
|
||||
export interface BasicColumn extends TableBaseColumn {
|
||||
//编辑表格
|
||||
edit?: boolean;
|
||||
editRow?: boolean;
|
||||
editable?: boolean;
|
||||
editComponent?: ComponentType;
|
||||
editComponentProps?: Recordable;
|
||||
editRule?: boolean | ((text: string, record: Recordable) => Promise<string>);
|
||||
editValueMap?: (value: any) => string;
|
||||
onEditRow?: () => void;
|
||||
// 权限编码控制是否显示
|
||||
auth?: string[];
|
||||
// 业务控制是否显示
|
||||
ifShow?: boolean | ((column: BasicColumn) => boolean);
|
||||
// 控制是否支持拖拽,默认支持
|
||||
draggable?: boolean;
|
||||
}
|
||||
|
||||
export interface TableActionType {
|
||||
reload: (opt) => Promise<void>;
|
||||
emit?: any;
|
||||
getColumns: (opt?) => BasicColumn[];
|
||||
setColumns: (columns: BasicColumn[] | string[]) => void;
|
||||
}
|
||||
|
||||
export interface BasicTableProps {
|
||||
title?: string;
|
||||
dataSource: Function;
|
||||
columns: any[];
|
||||
pagination: object;
|
||||
showPagination: boolean;
|
||||
actionColumn: any[];
|
||||
canResize: boolean;
|
||||
resizeHeightOffset: number;
|
||||
loading: boolean;
|
||||
}
|
27
web/src/components/Table/src/types/tableAction.ts
Normal file
27
web/src/components/Table/src/types/tableAction.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NButton } from 'naive-ui';
|
||||
import type { Component } from 'vue';
|
||||
import { PermissionsEnum } from '@/enums/permissionsEnum';
|
||||
export interface ActionItem extends Partial<InstanceType<typeof NButton>> {
|
||||
onClick?: Fn;
|
||||
label?: string;
|
||||
type?: 'success' | 'error' | 'warning' | 'info' | 'primary' | 'default';
|
||||
// 设定 color 后会覆盖 type 的样式
|
||||
color?: string;
|
||||
icon?: Component;
|
||||
popConfirm?: PopConfirm;
|
||||
disabled?: boolean;
|
||||
divider?: boolean;
|
||||
// 权限编码控制是否显示
|
||||
auth?: PermissionsEnum | PermissionsEnum[] | string | string[];
|
||||
// 业务控制是否显示
|
||||
ifShow?: boolean | ((action: ActionItem) => boolean);
|
||||
}
|
||||
|
||||
export interface PopConfirm {
|
||||
title: string;
|
||||
okText?: string;
|
||||
cancelText?: string;
|
||||
confirm: Fn;
|
||||
cancel?: Fn;
|
||||
icon?: Component;
|
||||
}
|
1
web/src/components/Upload/index.ts
Normal file
1
web/src/components/Upload/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as BasicUpload } from './src/BasicUpload.vue';
|
330
web/src/components/Upload/src/BasicUpload.vue
Normal file
330
web/src/components/Upload/src/BasicUpload.vue
Normal file
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<div class="upload">
|
||||
<div class="upload-card">
|
||||
<!--图片列表-->
|
||||
<div
|
||||
class="upload-card-item"
|
||||
:style="getCSSProperties"
|
||||
v-for="(item, index) in imgList"
|
||||
:key="`img_${index}`"
|
||||
>
|
||||
<div class="upload-card-item-info">
|
||||
<div class="img-box">
|
||||
<img :src="item" />
|
||||
</div>
|
||||
<div class="img-box-actions">
|
||||
<n-icon size="18" class="mx-2 action-icon" @click="preview(item)">
|
||||
<EyeOutlined />
|
||||
</n-icon>
|
||||
<n-icon size="18" class="mx-2 action-icon" @click="remove(index)">
|
||||
<DeleteOutlined />
|
||||
</n-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--上传图片-->
|
||||
<div
|
||||
class="upload-card-item upload-card-item-select-picture"
|
||||
:style="getCSSProperties"
|
||||
v-if="imgList.length < maxNumber"
|
||||
>
|
||||
<n-upload
|
||||
v-bind="$props"
|
||||
:file-list-style="{ display: 'none' }"
|
||||
@before-upload="beforeUpload"
|
||||
@finish="finish"
|
||||
>
|
||||
<div class="flex flex-col justify-center">
|
||||
<n-icon size="18" class="m-auto">
|
||||
<PlusOutlined />
|
||||
</n-icon>
|
||||
<span class="upload-title">上传图片</span>
|
||||
</div>
|
||||
</n-upload>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--上传图片-->
|
||||
<n-space>
|
||||
<n-alert title="提示" type="info" v-if="helpText" class="flex w-full">
|
||||
{{ helpText }}
|
||||
</n-alert>
|
||||
</n-space>
|
||||
</div>
|
||||
|
||||
<!--预览图片-->
|
||||
<n-modal
|
||||
v-model:show="showModal"
|
||||
preset="card"
|
||||
title="预览"
|
||||
:bordered="false"
|
||||
:style="{ width: '520px' }"
|
||||
>
|
||||
<img :src="previewUrl" />
|
||||
</n-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, toRefs, reactive, computed, watch } from 'vue';
|
||||
import { EyeOutlined, DeleteOutlined, PlusOutlined } from '@vicons/antd';
|
||||
import { basicProps } from './props';
|
||||
import { useMessage, useDialog } from 'naive-ui';
|
||||
import { ResultEnum } from '@/enums/httpEnum';
|
||||
import componentSetting from '@/settings/componentSetting';
|
||||
import { useGlobSetting } from '@/hooks/setting';
|
||||
import { isString } from '@/utils/is';
|
||||
|
||||
const globSetting = useGlobSetting();
|
||||
|
||||
export default defineComponent({
|
||||
name: 'BasicUpload',
|
||||
|
||||
components: { EyeOutlined, DeleteOutlined, PlusOutlined },
|
||||
props: {
|
||||
...basicProps,
|
||||
},
|
||||
emits: ['uploadChange', 'delete'],
|
||||
setup(props, { emit }) {
|
||||
const getCSSProperties = computed(() => {
|
||||
return {
|
||||
width: `${props.width}px`,
|
||||
height: `${props.height}px`,
|
||||
};
|
||||
});
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
|
||||
const state = reactive({
|
||||
showModal: false,
|
||||
previewUrl: '',
|
||||
originalImgList: [] as string[],
|
||||
imgList: [] as string[],
|
||||
});
|
||||
|
||||
//赋值默认图片显示
|
||||
watch(
|
||||
() => props.value,
|
||||
() => {
|
||||
// console.log('props.value:' + props.value);
|
||||
// 单图模式
|
||||
if (typeof props.value === 'string') {
|
||||
let data: string[] = [];
|
||||
if (props.value !== '') {
|
||||
data.push(props.value);
|
||||
}
|
||||
|
||||
state.imgList = data.map((item) => {
|
||||
return getImgUrl(item);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 多图模式
|
||||
state.imgList = props.value.map((item) => {
|
||||
return getImgUrl(item);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
//预览
|
||||
function preview(url: string) {
|
||||
state.showModal = true;
|
||||
state.previewUrl = url;
|
||||
}
|
||||
|
||||
//删除
|
||||
function remove(index: number) {
|
||||
dialog.info({
|
||||
title: '提示',
|
||||
content: '你确定要删除吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: () => {
|
||||
state.imgList.splice(index, 1);
|
||||
state.originalImgList.splice(index, 1);
|
||||
emit('uploadChange', state.originalImgList);
|
||||
emit('delete', state.originalImgList);
|
||||
},
|
||||
onNegativeClick: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
//组装完整图片地址
|
||||
function getImgUrl(url: string): string {
|
||||
const { imgUrl } = globSetting;
|
||||
return /(^http|https:\/\/)/g.test(url) ? url : `${imgUrl}${url}`;
|
||||
}
|
||||
|
||||
function checkFileType(fileType: string) {
|
||||
return componentSetting.upload.fileType.includes(fileType);
|
||||
}
|
||||
|
||||
//上传之前
|
||||
function beforeUpload({ file }) {
|
||||
const fileInfo = file.file;
|
||||
const { maxSize, accept } = props;
|
||||
const acceptRef = (isString(accept) && accept.split(',')) || [];
|
||||
|
||||
// 设置最大值,则判断
|
||||
if (maxSize && fileInfo.size / 1024 / 1024 >= maxSize) {
|
||||
message.error(`上传文件最大值不能超过${maxSize}M`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 设置类型,则判断
|
||||
const fileType = componentSetting.upload.fileType;
|
||||
if (acceptRef.length > 0 && !checkFileType(fileInfo.type)) {
|
||||
message.error(`只能上传文件类型为${fileType.join(',')}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//上传结束
|
||||
function finish({ event: Event }) {
|
||||
const res = eval('(' + Event.target.response + ')');
|
||||
const infoField = componentSetting.upload.apiSetting.infoField;
|
||||
const imgField = componentSetting.upload.apiSetting.imgField;
|
||||
const { code } = res;
|
||||
const msg = res.msg || res.message || '上传失败';
|
||||
const result = res[infoField];
|
||||
//成功
|
||||
if (code === ResultEnum.SUCCESS) {
|
||||
let imgUrl: string = getImgUrl(result[imgField]);
|
||||
state.imgList.push(imgUrl);
|
||||
state.originalImgList.push(result[imgField]);
|
||||
emit('uploadChange', state.originalImgList);
|
||||
} else {
|
||||
message.error(msg);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
finish,
|
||||
preview,
|
||||
remove,
|
||||
beforeUpload,
|
||||
getCSSProperties,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.n-upload {
|
||||
width: auto; /** 居中 */
|
||||
}
|
||||
|
||||
.upload {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
|
||||
&-card {
|
||||
width: auto;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
&-item {
|
||||
margin: 0 8px 8px 0;
|
||||
position: relative;
|
||||
padding: 8px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background: 0 0;
|
||||
|
||||
.upload-card-item-info::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&-info::before {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&-info {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
.img-box-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
.img-box {
|
||||
position: relative;
|
||||
//padding: 8px;
|
||||
//border: 1px solid #d9d9d9;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.img-box-actions {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
z-index: 10;
|
||||
white-space: nowrap;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0;
|
||||
transition: all 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
background: 0 0;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-item-select-picture {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
background: #fafafa;
|
||||
color: #666;
|
||||
|
||||
.upload-title {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
38
web/src/components/Upload/src/props.ts
Normal file
38
web/src/components/Upload/src/props.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { PropType } from 'vue';
|
||||
import { NUpload } from 'naive-ui';
|
||||
|
||||
export const basicProps = {
|
||||
...NUpload.props,
|
||||
accept: {
|
||||
type: String,
|
||||
default: '.jpg,.png,.jpeg,.svg,.gif',
|
||||
},
|
||||
helpText: {
|
||||
type: String as PropType<string>,
|
||||
default: '',
|
||||
},
|
||||
maxSize: {
|
||||
type: Number as PropType<number>,
|
||||
default: 2,
|
||||
},
|
||||
maxNumber: {
|
||||
type: Number as PropType<number>,
|
||||
default: Infinity,
|
||||
},
|
||||
value: {
|
||||
type: String as PropType<string>,
|
||||
default: () => '',
|
||||
},
|
||||
values: {
|
||||
type: (Array as PropType<string[]>) || (String as PropType<string>),
|
||||
default: () => [],
|
||||
},
|
||||
width: {
|
||||
type: Number as PropType<number>,
|
||||
default: 104,
|
||||
},
|
||||
height: {
|
||||
type: Number as PropType<number>,
|
||||
default: 104, //建议不小于这个尺寸 太小页面可能显示有异常
|
||||
},
|
||||
};
|
7
web/src/components/Upload/src/type/index.ts
Normal file
7
web/src/components/Upload/src/type/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface BasicProps {
|
||||
title?: string;
|
||||
dataSource: Function;
|
||||
columns: any[];
|
||||
pagination: object;
|
||||
showPagination: boolean;
|
||||
}
|
Reference in New Issue
Block a user