u
This commit is contained in:
1 parent
1b73170f78
commit
104e0a78c6
14 files changed
+417
-390
No files matched your search
@@ -13,7 +13,6 @@
|
||||
@use './drawer/index.scss' as *;
|
||||
@use './tooltip/index.scss' as *;
|
||||
@use './tag/index.scss' as *;
|
||||
@use './tag-group/index.scss' as *;
|
||||
@use './space/index.scss' as *;
|
||||
@use './separator/index.scss' as *;
|
||||
@use './radio/index.scss' as *;
|
||||
|
||||
@@ -2,11 +2,12 @@ import type { InjectionKey, Slots } from 'vue'
|
||||
|
||||
export interface RadioGroupContext {
|
||||
name: 'AoRadioGroup'
|
||||
value: string | number | boolean
|
||||
value: string | number | boolean | undefined
|
||||
disabled: boolean
|
||||
slots: Slots
|
||||
allowUncheck: boolean
|
||||
type?: 'default' | 'filled' | 'primaryFilled' | 'card' | 'pureCard' | 'outline'
|
||||
handleChange: (value: string | number | boolean, e: Event) => void
|
||||
handleChange: (value: string | number | boolean | undefined, e: Event) => void
|
||||
}
|
||||
|
||||
export const radioGroupKey: InjectionKey<RadioGroupContext>
|
||||
|
||||
@@ -54,6 +54,7 @@ function toggleThemeDark() {
|
||||
extra-field="note"
|
||||
type="filled"
|
||||
size="small"
|
||||
:allow-uncheck="false"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -22,7 +22,7 @@ export default defineComponent({
|
||||
},
|
||||
defaultValue: {
|
||||
type: [String, Number, Boolean] as PropType<string | number | boolean>,
|
||||
default: '',
|
||||
default: undefined,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
@@ -56,21 +56,23 @@ export default defineComponent({
|
||||
type: String as PropType<'small' | 'medium' | 'large'>,
|
||||
default: 'medium',
|
||||
},
|
||||
allowUncheck: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
setup(props, { emit, slots }) {
|
||||
const prefixCls = getPrefixCls('radio-group')
|
||||
|
||||
// 内部值,优先使用 v-model
|
||||
const _value = ref(props.modelValue ?? props.defaultValue)
|
||||
const _value = ref(props.modelValue ?? props.defaultValue ?? undefined)
|
||||
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (newVal !== undefined) {
|
||||
_value.value = newVal
|
||||
}
|
||||
_value.value = newVal ?? undefined
|
||||
})
|
||||
|
||||
const handleChange = (val: string | number | boolean, e: Event) => {
|
||||
const handleChange = (val: string | number | boolean | undefined, e: Event) => {
|
||||
_value.value = val
|
||||
emit('update:modelValue', val)
|
||||
emit('change', val, e)
|
||||
@@ -84,6 +86,7 @@ export default defineComponent({
|
||||
disabled: groupDisabled,
|
||||
type: groupType,
|
||||
slots,
|
||||
allowUncheck: props.allowUncheck,
|
||||
handleChange,
|
||||
}))
|
||||
|
||||
@@ -126,6 +129,7 @@ export default defineComponent({
|
||||
modelValue={_value.value === opt[props.valueField]}
|
||||
extra={extraContent}
|
||||
type={props.type}
|
||||
allowUncheck={props.allowUncheck}
|
||||
>
|
||||
{labelContent}
|
||||
</Radio>
|
||||
|
||||
@@ -52,18 +52,20 @@ export default defineComponent({
|
||||
return props.modelValue ?? _checked.value
|
||||
})
|
||||
|
||||
const isAllowUncheck = computed(() => props.allowUncheck || radioGroupCtx?.allowUncheck)
|
||||
|
||||
const handleClick = (e: Event) => {
|
||||
if (isDisabled.value)
|
||||
return
|
||||
if (isChecked.value && !props.allowUncheck)
|
||||
if (isChecked.value && !isAllowUncheck.value)
|
||||
return
|
||||
|
||||
if (inGroup.value) {
|
||||
const newValue = isChecked.value && props.allowUncheck ? '' : props.value
|
||||
const newValue = isChecked.value && isAllowUncheck.value ? undefined : props.value
|
||||
radioGroupCtx?.handleChange(newValue, e)
|
||||
}
|
||||
else {
|
||||
const newValue = props.allowUncheck ? !isChecked.value : true
|
||||
const newValue = isAllowUncheck.value ? !isChecked.value : true
|
||||
_checked.value = newValue
|
||||
emit('update:modelValue', newValue)
|
||||
emit('change', props.value, e)
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface TagOption {
|
||||
label: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
const tagOptions: TagOption[] = [
|
||||
{ label: '标签1', value: 1 },
|
||||
{ label: '标签2', value: 2 },
|
||||
{ label: '标签3', value: 3 },
|
||||
]
|
||||
|
||||
const customOptions: TagOption[] = [
|
||||
{ label: '成功', value: 'success' },
|
||||
{ label: '警告', value: 'warning' },
|
||||
{ label: '错误', value: 'error' },
|
||||
]
|
||||
|
||||
// 多选示例
|
||||
const multiSelected = ref<(string | number)[]>([1, 2])
|
||||
function handleMultiChange(selectedOptions: TagOption[]) {
|
||||
console.log('多选 change:', selectedOptions)
|
||||
}
|
||||
|
||||
// 单选示例
|
||||
const singleSelected = ref<(string | number)[]>([2])
|
||||
function handleSingleChange(selectedOptions: TagOption[]) {
|
||||
console.log('单选 change:', selectedOptions)
|
||||
}
|
||||
|
||||
// slot 渲染示例
|
||||
const slotSelected = ref<(string | number)[]>(['b'])
|
||||
|
||||
// 自定义样式示例
|
||||
const customSelected = ref<(string | number)[]>(['success'])
|
||||
function handleCustomChange(selectedOptions: TagOption[]) {
|
||||
console.log('自定义样式 change:', selectedOptions)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="padding: 20px;">
|
||||
<h2>ATagGroup 使用示例</h2>
|
||||
|
||||
<!-- 1. 多选 options 渲染 -->
|
||||
<section style="margin-bottom: 20px;">
|
||||
<h3>多选 - Options 渲染</h3>
|
||||
<ATagGroup
|
||||
v-model="multiSelected"
|
||||
:options="tagOptions"
|
||||
multiple
|
||||
@change="handleMultiChange"
|
||||
/>
|
||||
<p>选中值: {{ multiSelected }}</p>
|
||||
</section>
|
||||
|
||||
<!-- 2. 单选 options 渲染 -->
|
||||
<section style="margin-bottom: 20px;">
|
||||
<h3>单选 - Options 渲染</h3>
|
||||
<ATagGroup
|
||||
v-model="singleSelected"
|
||||
:options="tagOptions"
|
||||
:multiple="false"
|
||||
@change="handleSingleChange"
|
||||
/>
|
||||
<p>选中值: {{ singleSelected }}</p>
|
||||
</section>
|
||||
|
||||
<!-- 3. Slot 渲染 -->
|
||||
<section style="margin-bottom: 20px;">
|
||||
<h3>Slot 渲染</h3>
|
||||
<ATagGroup v-model="slotSelected">
|
||||
<ATag value="a">
|
||||
自定义A
|
||||
</ATag>
|
||||
<ATag value="b" checked>
|
||||
自定义B
|
||||
</ATag>
|
||||
<ATag value="c" disabled>
|
||||
自定义C
|
||||
</ATag>
|
||||
</ATagGroup>
|
||||
<p>选中值: {{ slotSelected }}</p>
|
||||
</section>
|
||||
|
||||
<!-- 4. 自定义 checkedProps / uncheckedProps -->
|
||||
<section>
|
||||
<h3>自定义样式</h3>
|
||||
<ATagGroup
|
||||
v-model="customSelected"
|
||||
:options="customOptions"
|
||||
:checked-props="{ type: 'solid', status: 'success' }"
|
||||
:unchecked-props="{ type: 'solid', status: 'default' }"
|
||||
@change="handleCustomChange"
|
||||
/>
|
||||
<p>选中值: {{ customSelected }}</p>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,12 +0,0 @@
|
||||
@use '../_styles/_variables.scss' as *;
|
||||
@use 'sass:color';
|
||||
|
||||
$tag-prefix: '#{$prefix}-tag';
|
||||
|
||||
$tag-group-prefix: '#{$prefix}-tag-group';
|
||||
|
||||
.#{$tag-group-prefix} {
|
||||
> .#{$tag-prefix}:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import type { PropType, VNode } from 'vue'
|
||||
import { cloneVNode, defineComponent } from 'vue'
|
||||
import { useSelectable } from '../_hooks/useSelectable'
|
||||
import { getPrefixCls } from '../_utils/getPrefixCls'
|
||||
import Tag from '../tag/tag'
|
||||
|
||||
interface TagOption {
|
||||
label?: string
|
||||
value: string | number
|
||||
disabled?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ATagGroup',
|
||||
props: {
|
||||
modelValue: { type: Array as PropType<(string | number)[]>, default: () => [] },
|
||||
multiple: { type: Boolean, default: true },
|
||||
options: { type: Array as PropType<TagOption[]>, default: () => [] },
|
||||
labelField: { type: String, default: 'label' },
|
||||
valueField: { type: String, default: 'value' },
|
||||
checkedProps: { type: Object as PropType<Record<string, any>>, default: () => ({ type: 'solid', status: 'primary' }) },
|
||||
uncheckedProps: { type: Object as PropType<Record<string, any>>, default: () => ({ type: 'solid', status: 'default' }) },
|
||||
class: { type: [String, Array, Object], default: '' },
|
||||
style: { type: Object, default: () => ({}) },
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
setup(props, { slots, emit }) {
|
||||
const prefixCls = getPrefixCls('tag-group')
|
||||
const { selectedValues, updateValue } = useSelectable(
|
||||
{
|
||||
modelValue: props.modelValue,
|
||||
multiple: props.multiple,
|
||||
options: props.options,
|
||||
valueField: props.valueField,
|
||||
},
|
||||
(newVal, selectedOptions) => {
|
||||
emit('update:modelValue', newVal)
|
||||
emit('change', selectedOptions)
|
||||
},
|
||||
)
|
||||
|
||||
const renderTag = (option: TagOption, vnode?: VNode) => {
|
||||
const val = option[props.valueField]
|
||||
const checked = selectedValues.value.includes(val)
|
||||
const baseProps = {
|
||||
'key': val,
|
||||
'value': val,
|
||||
checked,
|
||||
'checkable': true,
|
||||
'checkedProps': props.checkedProps,
|
||||
'uncheckedProps': props.uncheckedProps,
|
||||
'disabled': option.disabled,
|
||||
'onUpdate:checked': (valChecked: boolean) => updateValue(option, valChecked),
|
||||
}
|
||||
return vnode ? cloneVNode(vnode, baseProps) : <Tag {...baseProps}>{option[props.labelField]}</Tag>
|
||||
}
|
||||
|
||||
const renderOptionsTags = () => props.options.map(opt => renderTag(opt))
|
||||
|
||||
const renderSlotTags = () => {
|
||||
const slotNodes = slots.default?.() || []
|
||||
return slotNodes.map((vnode: VNode) => {
|
||||
// 判断 vnode.type 是否等于 Tag 组件本身
|
||||
if (vnode.type === Tag) {
|
||||
const vnodeProps = vnode.props || {}
|
||||
const option = {
|
||||
value: vnodeProps.value,
|
||||
label: vnodeProps.label ?? vnodeProps.value,
|
||||
disabled: vnodeProps.disabled,
|
||||
}
|
||||
return renderTag(option, vnode)
|
||||
}
|
||||
return vnode
|
||||
})
|
||||
}
|
||||
|
||||
return () => {
|
||||
const children = props.options.length ? renderOptionsTags() : renderSlotTags()
|
||||
return <div class={[prefixCls, props.class]} style={props.style}>{children}</div>
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import type { InjectionKey, Slots } from 'vue'
|
||||
|
||||
export interface TagGroupContext {
|
||||
name: 'AoTagGroup'
|
||||
value: Array<string | number | boolean>
|
||||
value: string | number | boolean | (string | number | boolean)[] | undefined
|
||||
disabled: boolean
|
||||
checkable: boolean
|
||||
slots: Slots
|
||||
|
||||
@@ -204,6 +204,8 @@
|
||||
</ATag>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -220,3 +222,107 @@
|
||||
gap: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
<!-- <script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface TagOption {
|
||||
label: string
|
||||
value: string | number
|
||||
}
|
||||
|
||||
const tagOptions: TagOption[] = [
|
||||
{ label: '标签1', value: 1 },
|
||||
{ label: '标签2', value: 2 },
|
||||
{ label: '标签3', value: 3 },
|
||||
]
|
||||
|
||||
const customOptions: TagOption[] = [
|
||||
{ label: '成功', value: 'success' },
|
||||
{ label: '警告', value: 'warning' },
|
||||
{ label: '错误', value: 'error' },
|
||||
]
|
||||
|
||||
// 多选示例
|
||||
const multiSelected = ref<(string | number)[]>([1, 2])
|
||||
function handleMultiChange(selectedOptions: TagOption[]) {
|
||||
console.log('多选 change:', selectedOptions)
|
||||
}
|
||||
|
||||
// 单选示例
|
||||
const singleSelected = ref<(string | number)[]>([2])
|
||||
function handleSingleChange(selectedOptions: TagOption[]) {
|
||||
console.log('单选 change:', selectedOptions)
|
||||
}
|
||||
|
||||
// slot 渲染示例
|
||||
const slotSelected = ref<(string | number)[]>(['b'])
|
||||
|
||||
// 自定义样式示例
|
||||
const customSelected = ref<(string | number)[]>(['success'])
|
||||
function handleCustomChange(selectedOptions: TagOption[]) {
|
||||
console.log('自定义样式 change:', selectedOptions)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="padding: 20px;">
|
||||
<h2>ATagGroup 使用示例</h2>
|
||||
|
||||
<!-- 1. 多选 options 渲染 -->
|
||||
<section style="margin-bottom: 20px;">
|
||||
<h3>多选 - Options 渲染</h3>
|
||||
<ATagGroup
|
||||
v-model="multiSelected"
|
||||
:options="tagOptions"
|
||||
multiple
|
||||
@change="handleMultiChange"
|
||||
/>
|
||||
<p>选中值: {{ multiSelected }}</p>
|
||||
</section>
|
||||
|
||||
<!-- 2. 单选 options 渲染 -->
|
||||
<section style="margin-bottom: 20px;">
|
||||
<h3>单选 - Options 渲染</h3>
|
||||
<ATagGroup
|
||||
v-model="singleSelected"
|
||||
:options="tagOptions"
|
||||
:multiple="false"
|
||||
@change="handleSingleChange"
|
||||
/>
|
||||
<p>选中值: {{ singleSelected }}</p>
|
||||
</section>
|
||||
|
||||
<!-- 3. Slot 渲染 -->
|
||||
<section style="margin-bottom: 20px;">
|
||||
<h3>Slot 渲染</h3>
|
||||
<ATagGroup v-model="slotSelected">
|
||||
<ATag value="a">
|
||||
自定义A
|
||||
</ATag>
|
||||
<ATag value="b">
|
||||
自定义B
|
||||
</ATag>
|
||||
<ATag value="c" disabled>
|
||||
自定义C
|
||||
</ATag>
|
||||
</ATagGroup>
|
||||
<p>选中值: {{ slotSelected }}</p>
|
||||
</section>
|
||||
|
||||
<!-- 4. 自定义 checkedProps / uncheckedProps -->
|
||||
<section>
|
||||
<h3>自定义样式</h3>
|
||||
<ATagGroup
|
||||
v-model="customSelected"
|
||||
:options="customOptions"
|
||||
:checked-props="{ type: 'solid', status: 'success' }"
|
||||
:unchecked-props="{ type: 'solid', status: 'default' }"
|
||||
@change="handleCustomChange"
|
||||
/>
|
||||
<p>选中值: {{ customSelected }}</p>
|
||||
</section>
|
||||
</div>
|
||||
</template> -->
|
||||
@@ -2,6 +2,13 @@
|
||||
@use 'sass:color';
|
||||
|
||||
$tag-prefix: '#{$prefix}-tag';
|
||||
$tag-group-prefix: '#{$prefix}-tag-group';
|
||||
|
||||
.#{$tag-group-prefix} {
|
||||
> .#{$tag-prefix}:not(:last-child) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.#{$tag-prefix} {
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -15,72 +15,93 @@ export default defineComponent({
|
||||
name: 'ATagGroup',
|
||||
props: {
|
||||
modelValue: {
|
||||
type: Array as PropType<Array<string | number | boolean>>,
|
||||
default: () => [],
|
||||
type: [String, Number, Boolean, Array] as PropType<
|
||||
string | number | boolean | (string | number | boolean)[]
|
||||
>,
|
||||
default: undefined,
|
||||
},
|
||||
defaultValue: {
|
||||
type: Array as PropType<Array<string | number | boolean>>,
|
||||
default: () => [],
|
||||
type: [String, Number, Boolean, Array] as PropType<
|
||||
string | number | boolean | (string | number | boolean)[]
|
||||
>,
|
||||
default: undefined,
|
||||
},
|
||||
multiple: { type: Boolean, default: true }, // 单选也用数组,只限制长度为1即可
|
||||
multiple: { type: Boolean, default: true },
|
||||
options: { type: Array as PropType<TagOption[]>, default: () => [] },
|
||||
labelField: { type: String, default: 'label' },
|
||||
valueField: { type: String, default: 'value' },
|
||||
checkedProps: { type: Object as PropType<Record<string, any>>, default: () => ({ type: 'solid', status: 'primary' }) },
|
||||
uncheckedProps: { type: Object as PropType<Record<string, any>>, default: () => ({ type: 'solid', status: 'default' }) },
|
||||
checkedProps: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => ({ type: 'solid', status: 'primary' }),
|
||||
},
|
||||
uncheckedProps: {
|
||||
type: Object as PropType<Record<string, any>>,
|
||||
default: () => ({ type: 'solid', status: 'default' }),
|
||||
},
|
||||
disabled: { type: Boolean, default: false },
|
||||
class: { type: [String, Array, Object], default: '' },
|
||||
style: { type: Object, default: () => ({}) },
|
||||
},
|
||||
emits: ['update:modelValue', 'change'],
|
||||
setup(props, { slots, emit }) {
|
||||
// 统一为数组
|
||||
const _value = ref<Array<string | number | boolean>>(
|
||||
Array.isArray(props.modelValue) ? props.modelValue : props.defaultValue ?? [],
|
||||
)
|
||||
const _value = ref(props.modelValue ?? props.defaultValue ?? (props.multiple ? [] : undefined))
|
||||
|
||||
const prefixCls = getPrefixCls('tag-group')
|
||||
const groupDisabled = computed(() => props.disabled)
|
||||
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (Array.isArray(newVal)) {
|
||||
_value.value = newVal
|
||||
}
|
||||
})
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(newVal) => {
|
||||
_value.value = newVal ?? (props.multiple ? [] : undefined)
|
||||
},
|
||||
)
|
||||
|
||||
const handleChange = (val: string | number | boolean, e: Event) => {
|
||||
const arr = [..._value.value]
|
||||
|
||||
const idx = arr.indexOf(val)
|
||||
if (idx >= 0) {
|
||||
arr.splice(idx, 1)
|
||||
// 外部 v-model 类型决定内部 value 的类型
|
||||
const isArrayMode = Array.isArray(_value.value)
|
||||
if (isArrayMode) {
|
||||
const arr = [...(_value.value as (string | number | boolean)[])]
|
||||
const idx = arr.indexOf(val)
|
||||
if (idx >= 0) {
|
||||
arr.splice(idx, 1)
|
||||
}
|
||||
else {
|
||||
if (!props.multiple)
|
||||
arr.length = 0 // 单选时只保留最后一个
|
||||
arr.push(val)
|
||||
}
|
||||
_value.value = arr
|
||||
emit('update:modelValue', arr)
|
||||
emit('change', arr, e)
|
||||
}
|
||||
else {
|
||||
if (!props.multiple)
|
||||
arr.length = 0 // 单选清空已有值
|
||||
arr.push(val)
|
||||
// 非数组模式:单选或多选时非数组 v-model
|
||||
const newVal = _value.value === val ? undefined : val
|
||||
_value.value = newVal
|
||||
emit('update:modelValue', newVal)
|
||||
emit('change', newVal, e)
|
||||
}
|
||||
|
||||
_value.value = arr
|
||||
emit('update:modelValue', _value.value)
|
||||
emit('change', _value.value, e)
|
||||
}
|
||||
|
||||
provide(tagGroupKey, reactive({
|
||||
name: 'AoTagGroup' as const,
|
||||
value: _value,
|
||||
disabled: groupDisabled,
|
||||
slots,
|
||||
checkable: true,
|
||||
multiple: props.multiple,
|
||||
handleChange,
|
||||
}))
|
||||
provide(
|
||||
tagGroupKey,
|
||||
reactive({
|
||||
name: 'AoTagGroup' as const,
|
||||
value: _value,
|
||||
disabled: groupDisabled,
|
||||
slots,
|
||||
checkable: true,
|
||||
multiple: props.multiple,
|
||||
handleChange,
|
||||
}),
|
||||
)
|
||||
|
||||
const renderOptions = () => {
|
||||
return props.options.map((option) => {
|
||||
const opt = typeof option === 'string' || typeof option === 'number'
|
||||
? { [props.labelField]: String(option), [props.valueField]: option }
|
||||
: option
|
||||
const opt
|
||||
= typeof option === 'string' || typeof option === 'number'
|
||||
? { [props.labelField]: String(option), [props.valueField]: option }
|
||||
: option
|
||||
|
||||
const labelContent = slots.label
|
||||
? slots.label({ data: opt })
|
||||
@@ -88,7 +109,10 @@ export default defineComponent({
|
||||
? opt[props.labelField]()
|
||||
: opt[props.labelField]
|
||||
|
||||
const isChecked = _value.value.includes(opt[props.valueField])
|
||||
const isChecked = props.multiple
|
||||
? Array.isArray(_value.value)
|
||||
&& _value.value.includes(opt[props.valueField])
|
||||
: _value.value === opt[props.valueField]
|
||||
|
||||
return (
|
||||
<Tag
|
||||
|
||||
@@ -13,9 +13,8 @@ export default defineComponent({
|
||||
type: { type: String as () => 'solid' | 'light' | 'outline' | 'outline-light', default: 'solid' },
|
||||
status: { type: String as () => 'primary' | 'danger' | 'warning' | 'success' | 'default', default: 'default' },
|
||||
checkable: { type: Boolean, default: false },
|
||||
value: { type: [String, Number, Boolean] as PropType<string | number | boolean>, default: true },
|
||||
value: { type: [String, Number, Boolean] as PropType<string | number | boolean>, required: true },
|
||||
checked: { type: Boolean, default: undefined },
|
||||
modelValue: { type: Boolean, default: undefined },
|
||||
defaultChecked: { type: Boolean, default: false },
|
||||
checkedProps: { type: Object as PropType<any>, default: () => ({ type: 'solid', status: 'primary' }) },
|
||||
uncheckedProps: { type: Object as PropType<any>, default: () => ({ type: 'solid', status: 'default' }) },
|
||||
@@ -25,7 +24,7 @@ export default defineComponent({
|
||||
class: { type: [String, Array, Object], default: '' },
|
||||
style: { type: Object, default: () => ({}) },
|
||||
},
|
||||
emits: ['click', 'close', 'update:checked', 'update:modelValue'],
|
||||
emits: ['click', 'close', 'update:checked'],
|
||||
setup(props, { slots, emit }) {
|
||||
const prefixCls = getPrefixCls('tag')
|
||||
const tagGroupCtx = inject<TagGroupContext | undefined>(tagGroupKey, undefined)
|
||||
@@ -34,38 +33,33 @@ export default defineComponent({
|
||||
const visible = ref(true)
|
||||
|
||||
const isDisabled = computed(() => props.disabled || tagGroupCtx?.disabled)
|
||||
const isChecked = computed(() => {
|
||||
if (inGroup.value) {
|
||||
const groupValue = tagGroupCtx!.value
|
||||
return groupValue.includes(props.value)
|
||||
}
|
||||
return props.checked ?? props.modelValue ?? _checked.value
|
||||
})
|
||||
|
||||
const isCheckable = computed(() => props.checkable || tagGroupCtx?.checkable)
|
||||
|
||||
const isChecked = computed(() => {
|
||||
if (inGroup.value && tagGroupCtx) {
|
||||
const groupValue = tagGroupCtx.value
|
||||
if (Array.isArray(groupValue)) {
|
||||
return groupValue.includes(props.value)
|
||||
}
|
||||
return groupValue === props.value
|
||||
}
|
||||
return props.checked ?? _checked.value
|
||||
})
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
if (isDisabled.value)
|
||||
return
|
||||
|
||||
if (inGroup.value) {
|
||||
// 多选:切换
|
||||
if (tagGroupCtx!.multiple) {
|
||||
tagGroupCtx!.handleChange(props.value, event)
|
||||
}
|
||||
else {
|
||||
// 单选:清空已有值或选中新值
|
||||
const newValue = isChecked.value ? '' : props.value
|
||||
tagGroupCtx!.handleChange(newValue, event)
|
||||
}
|
||||
if (inGroup.value && tagGroupCtx) {
|
||||
tagGroupCtx.handleChange(props.value, event)
|
||||
}
|
||||
else {
|
||||
// 独立 Tag 自己控制
|
||||
const newChecked = !isChecked.value
|
||||
_checked.value = newChecked
|
||||
emit('update:modelValue', newChecked)
|
||||
emit('update:checked', newChecked)
|
||||
}
|
||||
emit('click', { value: props.value, event })
|
||||
}
|
||||
|
||||
const handleClose = (event: MouseEvent) => {
|
||||
@@ -75,42 +69,28 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
return () =>
|
||||
visible.value && (() => {
|
||||
const renderType = isCheckable.value
|
||||
? isChecked.value
|
||||
? props.checkedProps.type
|
||||
: props.uncheckedProps.type
|
||||
: props.type
|
||||
|
||||
const renderStatus = isCheckable.value
|
||||
? isChecked.value
|
||||
? props.checkedProps.status
|
||||
: props.uncheckedProps.status
|
||||
: props.status
|
||||
|
||||
return (
|
||||
<div
|
||||
class={[
|
||||
prefixCls,
|
||||
`${prefixCls}-size-${props.size}`,
|
||||
`${prefixCls}-shape-${props.shape}`,
|
||||
`${prefixCls}-type-${renderType}`,
|
||||
`${prefixCls}-status-${renderStatus}`,
|
||||
{ [`${prefixCls}-check`]: isCheckable.value },
|
||||
{ [`${prefixCls}-checked`]: isChecked.value },
|
||||
{ [`${prefixCls}-disabled`]: isDisabled.value },
|
||||
props.class,
|
||||
]}
|
||||
style={{ ...props.style }}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{props.icon && <Icon icon={props.icon} class={`${prefixCls}-icon`} />}
|
||||
<span>{slots.default?.()}</span>
|
||||
{!props.disabled && props.showClose && (
|
||||
<Icon icon="ix:close" class={`${prefixCls}-close`} onClick={handleClose} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})()
|
||||
visible.value && (
|
||||
<div
|
||||
class={[
|
||||
prefixCls,
|
||||
`${prefixCls}-size-${props.size}`,
|
||||
`${prefixCls}-shape-${props.shape}`,
|
||||
`${prefixCls}-type-${isCheckable.value ? (isChecked.value ? props.checkedProps.type : props.uncheckedProps.type) : props.type}`,
|
||||
`${prefixCls}-status-${isCheckable.value ? (isChecked.value ? props.checkedProps.status : props.uncheckedProps.status) : props.status}`,
|
||||
{ [`${prefixCls}-check`]: isCheckable.value },
|
||||
{ [`${prefixCls}-checked`]: isChecked.value },
|
||||
{ [`${prefixCls}-disabled`]: isDisabled.value },
|
||||
props.class,
|
||||
]}
|
||||
style={{ ...props.style }}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{props.icon && <Icon icon={props.icon} class={`${prefixCls}-icon`} />}
|
||||
<span>{slots.default?.()}</span>
|
||||
{!props.disabled && props.showClose && (
|
||||
<Icon icon="ix:close" class={`${prefixCls}-close`} onClick={handleClose} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -1,101 +1,200 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface TagOption {
|
||||
label: string
|
||||
value: string | number
|
||||
}
|
||||
const selectedValue = ref()
|
||||
|
||||
const tagOptions: TagOption[] = [
|
||||
{ label: '标签1', value: 1 },
|
||||
{ label: '标签2', value: 2 },
|
||||
{ label: '标签3', value: 3 },
|
||||
// 自定义字段名
|
||||
const options = [
|
||||
{ name: '选项一', id: 1, note: '这是额外信息 1' },
|
||||
{ name: '选项二', id: 2, note: '这是额外信息 2' },
|
||||
{ name: '选项三', id: 3 },
|
||||
]
|
||||
|
||||
const customOptions: TagOption[] = [
|
||||
{ label: '成功', value: 'success' },
|
||||
{ label: '警告', value: 'warning' },
|
||||
{ label: '错误', value: 'error' },
|
||||
]
|
||||
|
||||
// 多选示例
|
||||
const multiSelected = ref<(string | number)[]>([1, 2])
|
||||
function handleMultiChange(selectedOptions: TagOption[]) {
|
||||
console.log('多选 change:', selectedOptions)
|
||||
function handleChange(val: string | number | boolean) {
|
||||
console.log('当前选中值:', val)
|
||||
}
|
||||
|
||||
// 单选示例
|
||||
const singleSelected = ref<(string | number)[]>([2])
|
||||
function handleSingleChange(selectedOptions: TagOption[]) {
|
||||
console.log('单选 change:', selectedOptions)
|
||||
function toggleThemeLight() {
|
||||
document.documentElement.setAttribute('theme-mode', 'light')
|
||||
}
|
||||
|
||||
// slot 渲染示例
|
||||
const slotSelected = ref<(string | number)[]>(['b'])
|
||||
|
||||
// 自定义样式示例
|
||||
const customSelected = ref<(string | number)[]>(['success'])
|
||||
function handleCustomChange(selectedOptions: TagOption[]) {
|
||||
console.log('自定义样式 change:', selectedOptions)
|
||||
function toggleThemeDark() {
|
||||
document.documentElement.setAttribute('theme-mode', 'dark')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="padding: 20px;">
|
||||
<h2>ATagGroup 使用示例</h2>
|
||||
<AButton status="primary" @click="toggleThemeLight">
|
||||
白天
|
||||
</AButton>
|
||||
<AButton status="primary" @click="toggleThemeDark">
|
||||
黑天
|
||||
</AButton>
|
||||
|
||||
<!-- 1. 多选 options 渲染 -->
|
||||
<section style="margin-bottom: 20px;">
|
||||
<h3>多选 - Options 渲染</h3>
|
||||
<ATagGroup
|
||||
v-model="multiSelected"
|
||||
:options="tagOptions"
|
||||
multiple
|
||||
@change="handleMultiChange"
|
||||
<div class="demo-wrapper">
|
||||
<div class="demo-container">
|
||||
<h3>Default</h3>
|
||||
<ARadioGroup
|
||||
v-model="selectedValue"
|
||||
:options="options"
|
||||
label-field="name"
|
||||
value-field="id"
|
||||
extra-field="note"
|
||||
type="default"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<p>选中值: {{ multiSelected }}</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 2. 单选 options 渲染 -->
|
||||
<section style="margin-bottom: 20px;">
|
||||
<h3>单选 - Options 渲染</h3>
|
||||
<ATagGroup
|
||||
v-model="singleSelected"
|
||||
:options="tagOptions"
|
||||
:multiple="false"
|
||||
@change="handleSingleChange"
|
||||
<div class="demo-container">
|
||||
<h3>filled</h3>
|
||||
<ARadioGroup
|
||||
v-model="selectedValue"
|
||||
:options="options"
|
||||
label-field="name"
|
||||
value-field="id"
|
||||
extra-field="note"
|
||||
type="filled"
|
||||
size="small"
|
||||
:allow-uncheck="false"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<p>选中值: {{ singleSelected }}</p>
|
||||
</section>
|
||||
|
||||
<!-- 3. Slot 渲染 -->
|
||||
<section style="margin-bottom: 20px;">
|
||||
<h3>Slot 渲染</h3>
|
||||
<ATagGroup v-model="slotSelected">
|
||||
<ATag value="a">
|
||||
自定义A
|
||||
</ATag>
|
||||
<ATag value="b">
|
||||
自定义B
|
||||
</ATag>
|
||||
<ATag value="c" disabled>
|
||||
自定义C
|
||||
</ATag>
|
||||
</ATagGroup>
|
||||
<p>选中值: {{ slotSelected }}</p>
|
||||
</section>
|
||||
|
||||
<!-- 4. 自定义 checkedProps / uncheckedProps -->
|
||||
<section>
|
||||
<h3>自定义样式</h3>
|
||||
<ATagGroup
|
||||
v-model="customSelected"
|
||||
:options="customOptions"
|
||||
:checked-props="{ type: 'solid', status: 'success' }"
|
||||
:unchecked-props="{ type: 'solid', status: 'default' }"
|
||||
@change="handleCustomChange"
|
||||
</div>
|
||||
<div class="demo-container">
|
||||
<h3>filled</h3>
|
||||
<ARadioGroup
|
||||
v-model="selectedValue"
|
||||
:options="options"
|
||||
label-field="name"
|
||||
value-field="id"
|
||||
extra-field="note"
|
||||
type="filled"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<p>选中值: {{ customSelected }}</p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="demo-container">
|
||||
<h3>filled</h3>
|
||||
<ARadioGroup
|
||||
v-model="selectedValue"
|
||||
:options="options"
|
||||
label-field="name"
|
||||
value-field="id"
|
||||
extra-field="note"
|
||||
type="filled"
|
||||
size="large"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="demo-container">
|
||||
<h3>primaryFilled</h3>
|
||||
<ARadioGroup
|
||||
v-model="selectedValue"
|
||||
:options="options"
|
||||
label-field="name"
|
||||
value-field="id"
|
||||
extra-field="note"
|
||||
type="primaryFilled"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="demo-container">
|
||||
<h3>outline</h3>
|
||||
<ARadioGroup
|
||||
v-model="selectedValue"
|
||||
:options="options"
|
||||
label-field="name"
|
||||
value-field="id"
|
||||
extra-field="note"
|
||||
type="outline"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="demo-container">
|
||||
<h3>card</h3>
|
||||
<ARadioGroup
|
||||
v-model="selectedValue"
|
||||
:options="options"
|
||||
label-field="name"
|
||||
value-field="id"
|
||||
extra-field="note"
|
||||
type="card"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="demo-container">
|
||||
<h3>pureCard</h3>
|
||||
<ARadioGroup
|
||||
v-model="selectedValue"
|
||||
:options="options"
|
||||
label-field="name"
|
||||
value-field="id"
|
||||
extra-field="note"
|
||||
type="pureCard"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="demo-container">
|
||||
<h3>自定义插槽radio</h3>
|
||||
<ARadioGroup
|
||||
v-model="selectedValue" :options="options"
|
||||
value-field="id"
|
||||
>
|
||||
<template #radio="{ data, checked }">
|
||||
<div
|
||||
:style="{
|
||||
padding: '4px 8px',
|
||||
border: checked ? '2px solid #409eff' : '1px solid #dcdfe6',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
marginRight: '4px',
|
||||
}"
|
||||
>
|
||||
{{ checked ? '选中' : '未选中' }} - {{ data.name }}
|
||||
</div>
|
||||
</template>
|
||||
</ARadioGroup>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.demo-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 24px 16px;
|
||||
min-height: 100vh;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 主题背景色 */
|
||||
:root[theme-mode='light'] .demo-wrapper {
|
||||
}
|
||||
:root[theme-mode='dark'] .demo-wrapper {
|
||||
background-color: #1e1e1e;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user