Skip to content

Commit

Permalink
完成 Input 和 TextArea 的基本功能 (#2)
Browse files Browse the repository at this point in the history
* feat: 新增输入框组件

* perf: 优化 storybook 组件配置

* feat: 新增 TextField 的 Filled 样式

* fix: 修复 type 始终为 text 的问题

* feat: 新增 TextField 的 Standard 样式

* feat: 准备分离 Input 子组件

* feat: 新增 Textarea 子组件

* feat: 新增 Textarea Outlined 样式

* perf: 移除 Standard Input 的无效样式
feat: 新增 Standard Textarea

* perf: 调整 Dom 结构
feat: 新增 Helper 插槽

* refactor: 重构 Textarea
feat: 新增最大行数限制

* perf: 优化焦点判断方法

* refactor: 合并 Input 和 Textarea 的样式

* perf: 优化累赘 CSS 规则

* feat: 新增下拉展示组件
feat: 新增 fade、fade-scale 动效

* fix: 修复当 slotClass 未提供时的报错问题

* fix: 修复全局 CSS 不生效问题

* feat: 新增出现时机控制字段

* feat: 新增 Standard Select

* feat: 新增 Outlined Select

* feat: 新增 Filled Select

* fix: 修复 Outlined Input 样式错误
fix: 修复 Input 点击外部不会自动聚焦
  • Loading branch information
Hanmo123 authored Jul 11, 2023
1 parent 4c41ad3 commit 10cd049
Show file tree
Hide file tree
Showing 21 changed files with 701 additions and 10 deletions.
2 changes: 2 additions & 0 deletions packages/component-vue/.storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Preview } from '@storybook/vue3'

import '../style/ACommon.css';

const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
Expand Down
26 changes: 26 additions & 0 deletions packages/component-vue/components/ADropDown.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Meta, StoryObj } from "@storybook/vue3";
import ADropDown from "./ADropDown.vue";
import { ref } from "vue";

const meta: Meta<typeof ADropDown> = {
title: 'Components/ADropDown',
component: ADropDown
};

const model = ref(false);
const updateModel = (value: any) => model.value = value

export default meta;
type Story = StoryObj<typeof ADropDown>;

export const Standard: Story = {
render: (args) => ({
components: { ADropDown },
setup: () => ({ args, model, updateModel }),
template: '<ADropDown v-bind="args" v-model="model"><template #visible>Visible Slot</template><template #hidden>Hidden Slot<br />Hidden Slot 2</template></ADropDown>'
}),
args: {
transition: 'fade-scale',
when: 'hover'
}
};
49 changes: 49 additions & 0 deletions packages/component-vue/components/ADropDown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script lang="ts" setup>
import '../style/components/ADropDown.css'
import type { IADropDownProps } from '@/interfaces/IADropDownProps'
import { onClickOutside, useElementHover, useVModel } from '@vueuse/core'
import { ref, watch, type Ref } from 'vue'
const props = withDefaults(defineProps<IADropDownProps>(), {
when: 'click'
})
const emits = defineEmits(['update:modelValue'])
const active = useVModel(props, 'modelValue', emits)
let onClick: () => void,
hidden: Ref<HTMLElement | undefined>,
dropdown: Ref<HTMLElement | undefined>
if (props.when == 'click') {
hidden = ref()
onClick = () => (active.value = !active.value)
onClickOutside(hidden, (e: PointerEvent) => {
active.value = false
e.stopPropagation()
})
} else {
dropdown = ref()
const hover = useElementHover(dropdown)
watch(
() => hover.value,
(v: boolean) => (active.value = v)
)
}
</script>

<template>
<div class="a-dropdown" ref="dropdown">
<div @click="onClick" class="a-dropdown-visible" :class="props.slotClass?.visible">
<slot name="visible" />
</div>
<Transition :name="props.transition">
<div ref="hidden" v-if="active" class="a-dropdown-hidden" :class="props.slotClass?.hidden">
<slot name="hidden" />
</div>
</Transition>
</div>
</template>
169 changes: 169 additions & 0 deletions packages/component-vue/components/AInput.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import type { Meta, StoryObj } from "@storybook/vue3";
import AInput from "./AInput.vue";
import { ref } from "vue";

const meta: Meta<typeof AInput> = {
title: 'Components/AInput',
component: AInput
};

const model = ref('');
const updateModel = (value: any) => model.value = value

export default meta;
type Story = StoryObj<typeof AInput>;

export const Standard: Story = {
render: (args) => ({
components: { AInput },
setup: () => ({ args, model, updateModel }),
template: '<AInput v-bind="args" v-model="model"><template #helper>{{ model }}</template></AInput>'
}),
args: {
label: '用户名',
type: 'text'
},
};

export const Outlined: Story = {
render: (args) => ({
components: { AInput },
setup: () => ({ args, model, updateModel }),
template: '<AInput v-bind="args" v-model="model"><template #helper>{{ model }}</template></AInput>'
}),
args: {
label: '用户名',
type: 'password',
outlined: true
},
};

export const Filled: Story = {
render: (args) => ({
components: { AInput },
setup: () => ({ args, model, updateModel }),
template: '<AInput v-bind="args" v-model="model"><template #helper>{{ model }}</template></AInput>'
}),
args: {
label: '用户名',
type: 'text',
filled: true
},
};

export const SelectStandard: Story = {
render: (args) => ({
components: { AInput },
setup: () => ({ args, model, updateModel }),
template: '<AInput v-bind="args" v-model="model"><template #helper>{{ model }}</template></AInput>'
}),
args: {
label: '用户名',
type: 'select',
options: {
value1: 'label1',
value2: 'label2'
}
},
};

export const SelectOutlined: Story = {
render: (args) => ({
components: { AInput },
setup: () => ({ args, model, updateModel }),
template: '<AInput v-bind="args" v-model="model"><template #helper>{{ model }}</template></AInput>'
}),
args: {
label: '用户名',
type: 'select',
options: {
value1: 'label1',
value2: 'label2'
},
outlined: true
},
};

export const SelectFilled: Story = {
render: (args) => ({
components: { AInput },
setup: () => ({ args, model, updateModel }),
template: '<AInput v-bind="args" v-model="model"><template #helper>{{ model }}</template></AInput>'
}),
args: {
label: '用户名',
type: 'select',
options: {
value1: 'label1',
value2: 'label2'
},
filled: true
},
};

export const TextareaStandard: Story = {
render: (args) => ({
components: { AInput },
setup: () => ({ args, model, updateModel }),
template: '<AInput v-bind="args" v-model="model"><template #helper>{{ model }}</template></AInput>'
}),
args: {
label: '个人简介',
type: 'textarea'
},
};

export const TextareaOutlined: Story = {
render: (args) => ({
components: { AInput },
setup: () => ({ args, model, updateModel }),
template: '<AInput v-bind="args" v-model="model"><template #helper>{{ model }}</template></AInput>'
}),
args: {
label: '个人简介',
type: 'textarea',
outlined: true
},
};

export const TextareaFilled: Story = {
render: (args) => ({
components: { AInput },
setup: () => ({ args, model, updateModel }),
template: '<AInput v-bind="args" v-model="model"><template #helper>{{ model }}</template></AInput>'
}),
args: {
label: '个人简介',
type: 'textarea',
filled: true
},
};

export const MaxRow: Story = {
render: (args) => ({
components: { AInput },
setup: () => ({ args, model, updateModel }),
template: '<AInput v-bind="args" v-model="model"><template #helper>{{ model }}</template></AInput>'
}),
args: {
label: '个人简介',
type: 'textarea',
maxrow: 4,
outlined: false,
filled: false
},
};

export const HelperText: Story = {
render: (args) => ({
components: { AInput },
setup: () => ({ args, model, updateModel }),
template: '<AInput v-bind="args" v-model="model"><template #helper>帮助文本</template></AInput>'
}),
args: {
label: '用户名',
type: 'text',
outlined: false,
filled: false
},
};
112 changes: 112 additions & 0 deletions packages/component-vue/components/AInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<script lang="ts" setup>
import '../style/components/AInput.css'
import { computed, ref, watch } from 'vue'
import { onClickOutside } from '@vueuse/core'
import AInputInput from './AInput/AInputInput.vue'
import type { IAInputProps } from '@/interfaces/IAInputProps'
import AInputTextarea from './AInput/AInputTextarea.vue'
import AInputHelper from './AInput/AInputHelper.vue'
import AInputDrop from './AInput/AInputDrop.vue'
const props = defineProps<IAInputProps>()
const emits = defineEmits(['update:modelValue'])
const target = ref<HTMLElement>()
// 封装外部 v-model 的双向绑定
const outerModel = computed({
get: () => props.modelValue,
set: (v: string) => emits('update:modelValue', v)
})
// 初始化内部 v-model 的值
const innerModel = ref(props.modelValue)
// 判断 Label 是否应该上浮
const focused = ref(false)
const triggerFocus = (v: boolean) => (focused.value = v)
const active = computed(() => focused.value || !!innerModel.value)
// 监听失去焦点
// 在 click 时 focus 输入框会导致 focus-within 短暂变为 false 故使用此方法
onClickOutside(target, () => triggerFocus(false))
// 监听外部 v-model 的变化并同步
watch(
() => outerModel.value,
(v: string) => (innerModel.value = v)
)
// v-model 正常更新模式 内部 model 变化时同步
watch(
() => innerModel.value,
(v) => {
if (!props.modelModifiers?.lazy) outerModel.value = v
}
)
// v-model 懒更新模式 失去焦点事件时同步
watch(
() => focused.value,
(v) => {
if (!v && props.modelModifiers?.lazy) outerModel.value = innerModel.value
}
)
// 判断组件类型
const inputType = computed(
() =>
({
text: 'input',
password: 'input',
textarea: 'textarea',
select: 'drop'
}[props.type])
)
// 判断组件样式类别
const inputStyle = computed(
() =>
({
input: 'common',
textarea: 'common',
drop: 'common'
}[inputType.value])
)
// 计算绑定的属性
const status = computed(() => ({
ref: 'target',
[inputType.value]: '',
[inputStyle.value!]: '',
focused: focused.value ? '' : null,
active: active.value ? '' : null,
standard: props.filled || props.outlined ? null : '',
outlined: props.outlined ? '' : null,
filled: props.filled ? '' : null
}))
const binds = computed(() => ({
class: 'a-input-fields',
type: props.type,
label: props.label,
maxrow: props.maxrow,
options: props.options,
active: active.value,
focused: focused.value,
modelValue: innerModel.value,
'onUpdate:focused': (v: boolean) => (focused.value = v),
'onUpdate:modelValue': (v: string) => (innerModel.value = v)
}))
</script>

<template>
<div v-bind="status" class="a-input" @click="triggerFocus(true)">
<AInputInput v-bind="binds" v-if="inputType == 'input'" />
<AInputTextarea v-bind="binds" v-else-if="inputType == 'textarea'" />
<AInputDrop v-bind="binds" v-else-if="inputType == 'drop'" />

<AInputHelper v-if="!!$slots?.helper">
<slot name="helper" />
</AInputHelper>
</div>
</template>
Loading

0 comments on commit 10cd049

Please sign in to comment.