From 39706c265e7a5ebc3bb702a9df82c4a3cb6ed5d4 Mon Sep 17 00:00:00 2001 From: vaebe <18137693952@163.com> Date: Mon, 17 Nov 2025 20:51:56 +0800 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20input=20=E5=88=9D=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ccui/ui/input/index.ts | 17 + packages/ccui/ui/input/src/input-types.ts | 49 +++ packages/ccui/ui/input/src/input.scss | 234 ++++++++++++++ packages/ccui/ui/input/src/input.tsx | 185 +++++++++++ packages/ccui/ui/input/test/input.test.ts | 211 ++++++++++++ packages/docs/components/input/index.md | 378 ++++++++++++++++++++++ 6 files changed, 1074 insertions(+) create mode 100644 packages/ccui/ui/input/index.ts create mode 100644 packages/ccui/ui/input/src/input-types.ts create mode 100644 packages/ccui/ui/input/src/input.scss create mode 100644 packages/ccui/ui/input/src/input.tsx create mode 100644 packages/ccui/ui/input/test/input.test.ts create mode 100644 packages/docs/components/input/index.md diff --git a/packages/ccui/ui/input/index.ts b/packages/ccui/ui/input/index.ts new file mode 100644 index 00000000..bf039e7e --- /dev/null +++ b/packages/ccui/ui/input/index.ts @@ -0,0 +1,17 @@ +import type { App } from 'vue' +import Input from './src/input' + +Input.install = function (app: App): void { + app.component(Input.name, Input) +} + +export { Input } + +export default { + title: 'Input 输入框', + category: '数据录入', + status: '100%', + install(app: App): void { + app.component(Input.name, Input) + }, +} diff --git a/packages/ccui/ui/input/src/input-types.ts b/packages/ccui/ui/input/src/input-types.ts new file mode 100644 index 00000000..427b3d97 --- /dev/null +++ b/packages/ccui/ui/input/src/input-types.ts @@ -0,0 +1,49 @@ +import type { ExtractPropTypes, PropType } from 'vue' + +export type InputType = 'text' | 'password' | 'number' | 'textarea' +export type InputSize = 'large' | 'default' | 'small' + +export const inputProps = { + type: { + type: String as PropType, + default: 'text', + }, + size: { + type: String as PropType, + default: 'default', + }, + placeholder: { + type: String, + default: '', + }, + disabled: { + type: Boolean, + default: false, + }, + readonly: { + type: Boolean, + default: false, + }, + clearable: { + type: Boolean, + default: false, + }, + showPassword: { + type: Boolean, + default: false, + }, + prepend: { + type: String, + default: '', + }, + append: { + type: String, + default: '', + }, + value: { + type: String, + default: '', + }, +} as const + +export type InputProps = ExtractPropTypes diff --git a/packages/ccui/ui/input/src/input.scss b/packages/ccui/ui/input/src/input.scss new file mode 100644 index 00000000..d97e4644 --- /dev/null +++ b/packages/ccui/ui/input/src/input.scss @@ -0,0 +1,234 @@ +@use '../../style-var/index.scss' as *; + +.#{$cls-prefix}-input { + position: relative; + display: inline-flex; + align-items: center; + width: 100%; + box-sizing: border-box; + vertical-align: middle; + font-size: 14px; + line-height: $ccui-line-height-base; + border-radius: $ccui-border-radius; + transition: $ccui-animation-duration-base; + background-color: $ccui-base-bg; + border: 1px solid $ccui-line; + color: $ccui-text; + + &:hover { + border-color: $ccui-brand-hover; + } + + &__wrapper { + display: inline-flex; + align-items: center; + width: 100%; + border-radius: $ccui-border-radius; + transition: $ccui-animation-duration-base; + background-color: $ccui-base-bg; + border: 1px solid $ccui-line; + + &--disabled { + background-color: $ccui-disabled-bg; + border-color: $ccui-disabled-line; + color: $ccui-disabled-text; + cursor: not-allowed; + + &:hover { + border-color: $ccui-disabled-line; + } + } + + &--focus { + border-color: $ccui-brand; + box-shadow: 0 0 0 2px rgba($ccui-brand, 0.2); + } + + &--hover { + border-color: $ccui-brand-hover; + } + + &--large { + font-size: 16px; + line-height: 1.5; + padding: 12px; + } + + &--default { + font-size: 14px; + line-height: 1.5; + padding: 8px; + } + + &--small { + font-size: 12px; + line-height: 1.5; + padding: 4px; + } + } + + &--disabled { + background-color: $ccui-disabled-bg; + border-color: $ccui-disabled-line; + color: $ccui-disabled-text; + cursor: not-allowed; + + &:hover { + border-color: $ccui-disabled-line; + } + } + + &--readonly { + cursor: default; + } + + &--clearable, + &--suffix { + padding-right: 30px; + } + + &--prefix { + padding-left: 30px; + } + + &__main { + position: relative; + display: inline-flex; + align-items: center; + flex: 1; + width: 100%; + } + + &__prepend, + &__append { + display: inline-flex; + align-items: center; + justify-content: center; + white-space: nowrap; + background-color: $ccui-area; + border: 1px solid $ccui-line; + border-radius: $ccui-border-radius; + color: $ccui-text; + font-size: 14px; + line-height: 1.5; + padding: 0 8px; + } + + &__prepend { + border-right: 0; + border-top-right-radius: 0; + border-bottom-right-radius: 0; + } + + &__append { + border-left: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + } + + &__prefix, + &__suffix { + position: absolute; + top: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + color: $ccui-placeholder; + pointer-events: none; + } + + &__prefix { + left: 10px; + } + + &__suffix { + right: 10px; + } + + &__clear, + &__password-visible, + &__password-hidden { + cursor: pointer; + pointer-events: auto; + color: $ccui-placeholder; + + &:hover { + color: $ccui-text; + } + } + + input, + textarea { + display: inline-block; + width: 100%; + height: 100%; + box-sizing: border-box; + background: none; + border: none; + outline: none; + color: inherit; + font-size: inherit; + line-height: inherit; + padding: 0; + margin: 0; + border-radius: $ccui-border-radius; + + &::placeholder { + color: $ccui-placeholder; + } + + &:disabled { + background: none; + cursor: not-allowed; + color: $ccui-disabled-text; + } + } + + textarea { + resize: vertical; + padding: 8px; + } + + &--large { + font-size: 16px; + line-height: 1.5; + + input, + textarea { + padding: 12px; + } + + textarea { + padding: 12px; + } + } + + &--default { + font-size: 14px; + line-height: 1.5; + + input, + textarea { + padding: 8px; + } + + textarea { + padding: 8px; + } + } + + &--small { + font-size: 12px; + line-height: 1.5; + + input, + textarea { + padding: 4px; + } + + textarea { + padding: 4px; + } + } +} diff --git a/packages/ccui/ui/input/src/input.tsx b/packages/ccui/ui/input/src/input.tsx new file mode 100644 index 00000000..966ad45b --- /dev/null +++ b/packages/ccui/ui/input/src/input.tsx @@ -0,0 +1,185 @@ +import type { InputProps } from './input-types' +import { computed, defineComponent, ref, watch } from 'vue' +import { useNamespace } from '../../shared/hooks/use-namespace' +import { inputProps } from './input-types' +import './input.scss' + +export default defineComponent({ + name: 'CInput', + props: inputProps, + emits: ['update:value', 'input', 'change', 'focus', 'blur', 'clear'], + setup(props: InputProps, { emit, slots }) { + const ns = useNamespace('input') + const inputRef = ref(null) + + // 输入框的值 + const inputValue = ref(props.value) + + // 是否显示密码切换图标 + const showPasswordVisible = computed(() => props.type === 'password' && props.showPassword) + + // 密码是否可见 + const isPasswordVisible = ref(false) + + // 当前输入框类型 + const currentType = computed(() => { + if (props.type === 'password' && showPasswordVisible.value && isPasswordVisible.value) { + return 'text' + } + return props.type + }) + + // 输入框类名 + const inputClass = computed(() => ({ + [ns.b()]: true, + [ns.m(props.size)]: !!props.size, + [ns.m('disabled')]: props.disabled, + [ns.m('readonly')]: props.readonly, + [ns.m('clearable')]: props.clearable, + [ns.m('suffix')]: props.clearable || showPasswordVisible.value, + })) + + // 包装器类名 + const wrapperClass = computed(() => ({ + [ns.e('wrapper')]: true, + [ns.em('wrapper', props.size)]: !!props.size, + [ns.em('wrapper', 'disabled')]: props.disabled, + })) + + // 更新输入框的值 + const updateValue = (value: string) => { + inputValue.value = value + emit('update:value', value) + emit('input', value) + } + + // 处理输入事件 + const handleInput = (e: Event) => { + const target = e.target as HTMLInputElement + updateValue(target.value) + } + + // 处理变化事件 + const handleChange = (e: Event) => { + const target = e.target as HTMLInputElement + emit('change', target.value) + } + + // 处理焦点事件 + const handleFocus = (e: FocusEvent) => { + emit('focus', e) + } + + // 处理失去焦点事件 + const handleBlur = (e: FocusEvent) => { + emit('blur', e) + } + + // 清空输入框 + const clearInput = () => { + updateValue('') + emit('clear') + } + + // 切换密码可见性 + const togglePasswordVisible = () => { + isPasswordVisible.value = !isPasswordVisible.value + } + + // 监听 value 属性变化 + watch(() => props.value, (newVal) => { + if (newVal !== inputValue.value) { + inputValue.value = newVal + } + }) + + return () => { + // 前置内容 + const prependContent = props.prepend || slots.prepend + ? ( +
+ {slots.prepend ? slots.prepend() : {props.prepend}} +
+ ) + : null + + // 后置内容 + const appendContent = props.append || slots.append + ? ( +
+ {slots.append ? slots.append() : {props.append}} +
+ ) + : null + + // 后缀图标内容 + const suffixIconContent = props.clearable || showPasswordVisible.value || slots.suffix + ? ( +
+ {props.clearable && inputValue.value && ( + + )} + {showPasswordVisible.value && ( + + + )} + {slots.suffix && slots.suffix()} +
+ ) + : null + + // 输入框属性 + const baseInputAttrs = { + ref: inputRef, + class: inputClass.value, + placeholder: props.placeholder, + disabled: props.disabled, + readonly: props.readonly, + value: inputValue.value, + onInput: handleInput, + onChange: handleChange, + onFocus: handleFocus, + onBlur: handleBlur, + } + + // 输入框元素 + const inputElement = props.type === 'textarea' + ? ( +