-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
21 changed files
with
701 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.