Skip to content

Commit

Permalink
feat(InputFile): 增加文件选择组件 (#698)
Browse files Browse the repository at this point in the history
  • Loading branch information
1zumii authored Mar 25, 2024
1 parent 1eba6ea commit c9743d7
Show file tree
Hide file tree
Showing 16 changed files with 580 additions and 1 deletion.
1 change: 1 addition & 0 deletions components/_style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ import './avatar/style';
import './avatarGroup/style';
import './progress/style';
import './transfer/style';
import './input-file/style';
1 change: 1 addition & 0 deletions components/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ export * from './avatar';
export * from './avatarGroup';
export * from './progress';
export * from './transfer';
export * from './input-file';
3 changes: 3 additions & 0 deletions components/input-file/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const COMPONENT_NAME = 'FInputFile';

export const COMPONENT_NAME_DRAGGER = `${COMPONENT_NAME}Dragger`;
19 changes: 19 additions & 0 deletions components/input-file/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { withInstall } from '../_util/withInstall';
import { type SFCWithInstall } from '../_util/interface';
import InputFile from './inputFile';
import InputFileDragger from './inputFileDragger';

export { inputFileProps, type InputFileProps } from './props';

type InputFileType = SFCWithInstall<typeof InputFile>;
export const FInputFile = withInstall<InputFileType>(
InputFile as InputFileType,
);

export { inputFileDraggerProps, type InputFileDraggerProps } from './props';
type InputFileDraggerType = SFCWithInstall<typeof InputFileDragger>;
export const FInputFileDragger = withInstall<InputFileDraggerType>(
InputFileDragger as InputFileDraggerType,
);

export default FInputFile;
84 changes: 84 additions & 0 deletions components/input-file/inputFile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { defineComponent, type VNodeChild, type StyleValue } from 'vue';
import { useTheme } from '../_theme/useTheme';
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '../_util/constants';
import getPrefixCls from '../_util/getPrefixCls';
import Button from '../button';
import { UploadOutlined } from '../icon';
import { COMPONENT_NAME } from './const';
import { type InputFileSlots, inputFileProps } from './props';
import { useInputFile } from './useInputFile';

const prefixCls = getPrefixCls('input-file');
const cls = (appendClass: string): string => `${prefixCls}-${appendClass}`;

const InputFile = defineComponent({
name: COMPONENT_NAME,
props: inputFileProps,
emits: [UPDATE_MODEL_EVENT, CHANGE_EVENT],
slots: Object as InputFileSlots,
setup: (props, { emit, slots, attrs }) => {
useTheme();

const {
currentFiles,
inputRef,
disabled,
multiple,
acceptStr,
openFileExplorer,
handleInputFileChange,
} = useInputFile(props, emit);

const renderTrigger = (): VNodeChild => {
if (slots.default) {
return slots.default({});
}
return (
<Button
class={cls('trigger-button')}
disabled={disabled.value}
v-slots={{ icon: () => <UploadOutlined /> }}
>
选择文件
</Button>
);
};

const renderFileList = (files: File[]): VNodeChild => {
if (files.length === 0) return null;

if (slots.fileList) {
return slots.fileList({ files });
}

return files.length > 1 ? `${files.length} 个文件` : files[0].name;
};

return () => (
<div class={prefixCls}>
<div
class={cls('visible-content')}
style={attrs.style as StyleValue}
>
<div class={cls('trigger')} onClick={openFileExplorer}>
{renderTrigger()}
</div>
<div class={cls('file-list')}>
{renderFileList(currentFiles.value)}
</div>
</div>
<input
ref={inputRef}
class={cls('input')}
type={'file'}
accept={acceptStr.value}
multiple={multiple.value}
onChange={handleInputFileChange}
onClick={(e) => e.stopPropagation()}
/>
</div>
);
},
});

export default InputFile;
104 changes: 104 additions & 0 deletions components/input-file/inputFileDragger.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { defineComponent, type StyleValue, type VNodeChild } from 'vue';
import { useTheme } from '../_theme/useTheme';
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '../_util/constants';
import getPrefixCls from '../_util/getPrefixCls';
import Message from '../message';
import { useLocale } from '../config-provider/useLocale';
import { COMPONENT_NAME_DRAGGER } from './const';
import { type InputFileSlots, inputFileDraggerProps } from './props';
import { useInputFile } from './useInputFile';
import { useFileDrop } from './useFileDrop';

const prefixCls = getPrefixCls('input-file-dragger');
const cls = (appendClass: string): string => `${prefixCls}-${appendClass}`;

const InputFileDragger = defineComponent({
name: COMPONENT_NAME_DRAGGER,
props: inputFileDraggerProps,
emits: [UPDATE_MODEL_EVENT, CHANGE_EVENT],
slots: Object as InputFileSlots,
setup: (props, { emit, slots, attrs }) => {
useTheme();

const { t } = useLocale();

const {
currentFiles,
updateCurrentFiles,
inputRef,
disabled,
multiple,
accept,
acceptStr,
openFileExplorer,
handleInputFileChange,
} = useInputFile(props, emit);

const handleFileTypeInvalid = (files: File[]): void => {
if (!props.onFileTypeInvalid) {
Message.error(t('upload.fileTypeInvalidTip'));
}
props.onFileTypeInvalid(files);
};

const { isHovering, handleEnter, handleLeave, handleOver, handleDrop } =
useFileDrop({
disabled,
multiple,
accept,
afterDrop: (files) => {
updateCurrentFiles(files);
emit(CHANGE_EVENT, files);
},
onFileTypeInvalid: handleFileTypeInvalid,
});

const renderFileList = (files: File[]): VNodeChild => {
if (files.length === 0) return null;

if (slots.fileList) {
return slots.fileList({ files });
}

return files.length > 1 ? `${files.length} 个文件` : files[0].name;
};

return () => (
<div class={prefixCls}>
<div
class={cls('visible-content')}
style={attrs.style as StyleValue}
>
<div
class={[
cls('droppable'),
isHovering.value && 'is-hovering',
disabled.value && 'is-disabled',
]}
onDragenter={handleEnter}
onDragleave={handleLeave}
onDrop={handleDrop}
onDragover={handleOver}
onClick={openFileExplorer}
>
{slots.default?.({})}
</div>
<div class={cls('file-list')}>
{renderFileList(currentFiles.value)}
</div>
</div>
<input
ref={inputRef}
class={cls('input')}
type={'file'}
accept={acceptStr.value}
multiple={multiple.value}
onChange={handleInputFileChange}
onClick={(e) => e.stopPropagation()}
/>
</div>
);
},
});

export default InputFileDragger;
64 changes: 64 additions & 0 deletions components/input-file/props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
type PropType,
type ComponentObjectPropsOptions,
type SlotsType,
} from 'vue';
import { CHANGE_EVENT, UPDATE_MODEL_EVENT } from '../_util/constants';
import {
type ComponentEmit,
type ExtractPublicPropTypes,
} from '../_util/interface';

const commonProps = {
modelValue: {
type: Array as PropType<File[]>,
default: (): File[] => [],
},
accept: {
type: Array as PropType<string[]>,
default: (): string[] => [],
},
disabled: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
default: false,
},
} as const satisfies ComponentObjectPropsOptions;

// ------ Default Props ------

export const inputFileProps = {
...commonProps,
} as const satisfies ComponentObjectPropsOptions;

export type InputFileProps = ExtractPublicPropTypes<typeof inputFileProps>;

// ------ Dragger Props ------

export const inputFileDraggerProps = {
...commonProps,
onFileTypeInvalid: {
type: Function as PropType<(files: File[]) => void>,
},
} as const satisfies ComponentObjectPropsOptions;

export type InputFileDraggerProps = ExtractPublicPropTypes<
typeof inputFileDraggerProps
>;

// ------ Emit ------
export const EMIT_EVENTS = [UPDATE_MODEL_EVENT, CHANGE_EVENT] as const;
export type InputFileEmit = ComponentEmit<typeof EMIT_EVENTS>;

// ------ Slots ------
export type InputFileSlotsParams = {
default: Record<string, never>;
fileList: {
files: File[];
};
};

export type InputFileSlots = SlotsType<InputFileSlotsParams>;
63 changes: 63 additions & 0 deletions components/input-file/style/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
@import '../../style/themes/index';
@import '../../style/mixins/index';

@input-file: ~'@{cls-prefix}-input-file';
@input-file-dragger: ~'@{cls-prefix}-input-file-dragger';

.@{input-file}, .@{input-file-dragger} {
&-input {
display: none;
}

&-file-list {
.text();
color: var(--f-sub-head-color);
}
}

.@{input-file} {
.@{input-file}-visible-content {
display: flex;
align-items: center;

.@{input-file}-trigger {
cursor: pointer;
}

.@{input-file}-file-list {
margin-left: var(--f-padding-xsmall);
}
}
}

.@{input-file-dragger} {
width: 100%;

.@{input-file-dragger}-visible-content {

.@{input-file-dragger}-droppable {
width: 100%;
padding: var(--f-padding-middle);
text-align: center;
background-color: var(--f-component-bg-color);
border: var(--f-border-width-base) dashed var(--f-border-color-base);
border-radius: var(--f-border-radius-base);
cursor: pointer;
transition: border-color @animation-duration-slow @ease-base-in;

&:hover, &.is-hovering{
border-color: var(--f-primary-color);
}

&.is-disabled {
color: var(--f-text-color-disabled);
border-color: var(--f-border-color-base);
cursor: not-allowed;
}
}

.@{input-file-dragger}-file-list {
margin-top: var(--f-padding-xsmall);
}
}
}
2 changes: 2 additions & 0 deletions components/input-file/style/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import '../../style';
import './index.less';
Loading

0 comments on commit c9743d7

Please sign in to comment.