u
This commit is contained in:
1 parent
1b73170f78
commit
104e0a78c6
14 files changed
+379
-352
No files matched your search
@@ -13,7 +13,6 @@
|
|||||||
@use './drawer/index.scss' as *;
|
@use './drawer/index.scss' as *;
|
||||||
@use './tooltip/index.scss' as *;
|
@use './tooltip/index.scss' as *;
|
||||||
@use './tag/index.scss' as *;
|
@use './tag/index.scss' as *;
|
||||||
@use './tag-group/index.scss' as *;
|
|
||||||
@use './space/index.scss' as *;
|
@use './space/index.scss' as *;
|
||||||
@use './separator/index.scss' as *;
|
@use './separator/index.scss' as *;
|
||||||
@use './radio/index.scss' as *;
|
@use './radio/index.scss' as *;
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import type { InjectionKey, Slots } from 'vue'
|
|||||||
|
|
||||||
export interface RadioGroupContext {
|
export interface RadioGroupContext {
|
||||||
name: 'AoRadioGroup'
|
name: 'AoRadioGroup'
|
||||||
value: string | number | boolean
|
value: string | number | boolean | undefined
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
slots: Slots
|
slots: Slots
|
||||||
|
allowUncheck: boolean
|
||||||
type?: 'default' | 'filled' | 'primaryFilled' | 'card' | 'pureCard' | 'outline'
|
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>
|
export const radioGroupKey: InjectionKey<RadioGroupContext>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ function toggleThemeDark() {
|
|||||||
extra-field="note"
|
extra-field="note"
|
||||||
type="filled"
|
type="filled"
|
||||||
size="small"
|
size="small"
|
||||||
|
:allow-uncheck="false"
|
||||||
@change="handleChange"
|
@change="handleChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
type: [String, Number, Boolean] as PropType<string | number | boolean>,
|
type: [String, Number, Boolean] as PropType<string | number | boolean>,
|
||||||
default: '',
|
default: undefined,
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@@ -56,21 +56,23 @@ export default defineComponent({
|
|||||||
type: String as PropType<'small' | 'medium' | 'large'>,
|
type: String as PropType<'small' | 'medium' | 'large'>,
|
||||||
default: 'medium',
|
default: 'medium',
|
||||||
},
|
},
|
||||||
|
allowUncheck: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue', 'change'],
|
emits: ['update:modelValue', 'change'],
|
||||||
setup(props, { emit, slots }) {
|
setup(props, { emit, slots }) {
|
||||||
const prefixCls = getPrefixCls('radio-group')
|
const prefixCls = getPrefixCls('radio-group')
|
||||||
|
|
||||||
// 内部值,优先使用 v-model
|
// 内部值,优先使用 v-model
|
||||||
const _value = ref(props.modelValue ?? props.defaultValue)
|
const _value = ref(props.modelValue ?? props.defaultValue ?? undefined)
|
||||||
|
|
||||||
watch(() => props.modelValue, (newVal) => {
|
watch(() => props.modelValue, (newVal) => {
|
||||||
if (newVal !== undefined) {
|
_value.value = newVal ?? undefined
|
||||||
_value.value = newVal
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleChange = (val: string | number | boolean, e: Event) => {
|
const handleChange = (val: string | number | boolean | undefined, e: Event) => {
|
||||||
_value.value = val
|
_value.value = val
|
||||||
emit('update:modelValue', val)
|
emit('update:modelValue', val)
|
||||||
emit('change', val, e)
|
emit('change', val, e)
|
||||||
@@ -84,6 +86,7 @@ export default defineComponent({
|
|||||||
disabled: groupDisabled,
|
disabled: groupDisabled,
|
||||||
type: groupType,
|
type: groupType,
|
||||||
slots,
|
slots,
|
||||||
|
allowUncheck: props.allowUncheck,
|
||||||
handleChange,
|
handleChange,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
@@ -126,6 +129,7 @@ export default defineComponent({
|
|||||||
modelValue={_value.value === opt[props.valueField]}
|
modelValue={_value.value === opt[props.valueField]}
|
||||||
extra={extraContent}
|
extra={extraContent}
|
||||||
type={props.type}
|
type={props.type}
|
||||||
|
allowUncheck={props.allowUncheck}
|
||||||
>
|
>
|
||||||
{labelContent}
|
{labelContent}
|
||||||
</Radio>
|
</Radio>
|
||||||
|
|||||||
@@ -52,18 +52,20 @@ export default defineComponent({
|
|||||||
return props.modelValue ?? _checked.value
|
return props.modelValue ?? _checked.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isAllowUncheck = computed(() => props.allowUncheck || radioGroupCtx?.allowUncheck)
|
||||||
|
|
||||||
const handleClick = (e: Event) => {
|
const handleClick = (e: Event) => {
|
||||||
if (isDisabled.value)
|
if (isDisabled.value)
|
||||||
return
|
return
|
||||||
if (isChecked.value && !props.allowUncheck)
|
if (isChecked.value && !isAllowUncheck.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
if (inGroup.value) {
|
if (inGroup.value) {
|
||||||
const newValue = isChecked.value && props.allowUncheck ? '' : props.value
|
const newValue = isChecked.value && isAllowUncheck.value ? undefined : props.value
|
||||||
radioGroupCtx?.handleChange(newValue, e)
|
radioGroupCtx?.handleChange(newValue, e)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const newValue = props.allowUncheck ? !isChecked.value : true
|
const newValue = isAllowUncheck.value ? !isChecked.value : true
|
||||||
_checked.value = newValue
|
_checked.value = newValue
|
||||||
emit('update:modelValue', newValue)
|
emit('update:modelValue', newValue)
|
||||||
emit('change', props.value, e)
|
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 {
|
export interface TagGroupContext {
|
||||||
name: 'AoTagGroup'
|
name: 'AoTagGroup'
|
||||||
value: Array<string | number | boolean>
|
value: string | number | boolean | (string | number | boolean)[] | undefined
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
checkable: boolean
|
checkable: boolean
|
||||||
slots: Slots
|
slots: Slots
|
||||||
|
|||||||
@@ -204,6 +204,8 @@
|
|||||||
</ATag>
|
</ATag>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -220,3 +222,107 @@
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
</style>
|
</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';
|
@use 'sass:color';
|
||||||
|
|
||||||
$tag-prefix: '#{$prefix}-tag';
|
$tag-prefix: '#{$prefix}-tag';
|
||||||
|
$tag-group-prefix: '#{$prefix}-tag-group';
|
||||||
|
|
||||||
|
.#{$tag-group-prefix} {
|
||||||
|
> .#{$tag-prefix}:not(:last-child) {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.#{$tag-prefix} {
|
.#{$tag-prefix} {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
@@ -15,58 +15,77 @@ export default defineComponent({
|
|||||||
name: 'ATagGroup',
|
name: 'ATagGroup',
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: Array as PropType<Array<string | number | boolean>>,
|
type: [String, Number, Boolean, Array] as PropType<
|
||||||
default: () => [],
|
string | number | boolean | (string | number | boolean)[]
|
||||||
|
>,
|
||||||
|
default: undefined,
|
||||||
},
|
},
|
||||||
defaultValue: {
|
defaultValue: {
|
||||||
type: Array as PropType<Array<string | number | boolean>>,
|
type: [String, Number, Boolean, Array] as PropType<
|
||||||
default: () => [],
|
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: () => [] },
|
options: { type: Array as PropType<TagOption[]>, default: () => [] },
|
||||||
labelField: { type: String, default: 'label' },
|
labelField: { type: String, default: 'label' },
|
||||||
valueField: { type: String, default: 'value' },
|
valueField: { type: String, default: 'value' },
|
||||||
checkedProps: { type: Object as PropType<Record<string, any>>, default: () => ({ type: 'solid', status: 'primary' }) },
|
checkedProps: {
|
||||||
uncheckedProps: { type: Object as PropType<Record<string, any>>, default: () => ({ type: 'solid', status: 'default' }) },
|
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 },
|
disabled: { type: Boolean, default: false },
|
||||||
class: { type: [String, Array, Object], default: '' },
|
class: { type: [String, Array, Object], default: '' },
|
||||||
style: { type: Object, default: () => ({}) },
|
style: { type: Object, default: () => ({}) },
|
||||||
},
|
},
|
||||||
emits: ['update:modelValue', 'change'],
|
emits: ['update:modelValue', 'change'],
|
||||||
setup(props, { slots, emit }) {
|
setup(props, { slots, emit }) {
|
||||||
// 统一为数组
|
const _value = ref(props.modelValue ?? props.defaultValue ?? (props.multiple ? [] : undefined))
|
||||||
const _value = ref<Array<string | number | boolean>>(
|
|
||||||
Array.isArray(props.modelValue) ? props.modelValue : props.defaultValue ?? [],
|
|
||||||
)
|
|
||||||
|
|
||||||
const prefixCls = getPrefixCls('tag-group')
|
const prefixCls = getPrefixCls('tag-group')
|
||||||
const groupDisabled = computed(() => props.disabled)
|
const groupDisabled = computed(() => props.disabled)
|
||||||
|
|
||||||
watch(() => props.modelValue, (newVal) => {
|
watch(
|
||||||
if (Array.isArray(newVal)) {
|
() => props.modelValue,
|
||||||
_value.value = newVal
|
(newVal) => {
|
||||||
}
|
_value.value = newVal ?? (props.multiple ? [] : undefined)
|
||||||
})
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const handleChange = (val: string | number | boolean, e: Event) => {
|
const handleChange = (val: string | number | boolean, e: Event) => {
|
||||||
const arr = [..._value.value]
|
// 外部 v-model 类型决定内部 value 的类型
|
||||||
|
const isArrayMode = Array.isArray(_value.value)
|
||||||
|
if (isArrayMode) {
|
||||||
|
const arr = [...(_value.value as (string | number | boolean)[])]
|
||||||
const idx = arr.indexOf(val)
|
const idx = arr.indexOf(val)
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
arr.splice(idx, 1)
|
arr.splice(idx, 1)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (!props.multiple)
|
if (!props.multiple)
|
||||||
arr.length = 0 // 单选清空已有值
|
arr.length = 0 // 单选时只保留最后一个
|
||||||
arr.push(val)
|
arr.push(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
_value.value = arr
|
_value.value = arr
|
||||||
emit('update:modelValue', _value.value)
|
emit('update:modelValue', arr)
|
||||||
emit('change', _value.value, e)
|
emit('change', arr, e)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 非数组模式:单选或多选时非数组 v-model
|
||||||
|
const newVal = _value.value === val ? undefined : val
|
||||||
|
_value.value = newVal
|
||||||
|
emit('update:modelValue', newVal)
|
||||||
|
emit('change', newVal, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
provide(tagGroupKey, reactive({
|
provide(
|
||||||
|
tagGroupKey,
|
||||||
|
reactive({
|
||||||
name: 'AoTagGroup' as const,
|
name: 'AoTagGroup' as const,
|
||||||
value: _value,
|
value: _value,
|
||||||
disabled: groupDisabled,
|
disabled: groupDisabled,
|
||||||
@@ -74,11 +93,13 @@ export default defineComponent({
|
|||||||
checkable: true,
|
checkable: true,
|
||||||
multiple: props.multiple,
|
multiple: props.multiple,
|
||||||
handleChange,
|
handleChange,
|
||||||
}))
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
const renderOptions = () => {
|
const renderOptions = () => {
|
||||||
return props.options.map((option) => {
|
return props.options.map((option) => {
|
||||||
const opt = typeof option === 'string' || typeof option === 'number'
|
const opt
|
||||||
|
= typeof option === 'string' || typeof option === 'number'
|
||||||
? { [props.labelField]: String(option), [props.valueField]: option }
|
? { [props.labelField]: String(option), [props.valueField]: option }
|
||||||
: option
|
: option
|
||||||
|
|
||||||
@@ -88,7 +109,10 @@ export default defineComponent({
|
|||||||
? opt[props.labelField]()
|
? opt[props.labelField]()
|
||||||
: 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 (
|
return (
|
||||||
<Tag
|
<Tag
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ export default defineComponent({
|
|||||||
type: { type: String as () => 'solid' | 'light' | 'outline' | 'outline-light', default: 'solid' },
|
type: { type: String as () => 'solid' | 'light' | 'outline' | 'outline-light', default: 'solid' },
|
||||||
status: { type: String as () => 'primary' | 'danger' | 'warning' | 'success' | 'default', default: 'default' },
|
status: { type: String as () => 'primary' | 'danger' | 'warning' | 'success' | 'default', default: 'default' },
|
||||||
checkable: { type: Boolean, default: false },
|
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 },
|
checked: { type: Boolean, default: undefined },
|
||||||
modelValue: { type: Boolean, default: undefined },
|
|
||||||
defaultChecked: { type: Boolean, default: false },
|
defaultChecked: { type: Boolean, default: false },
|
||||||
checkedProps: { type: Object as PropType<any>, default: () => ({ type: 'solid', status: 'primary' }) },
|
checkedProps: { type: Object as PropType<any>, default: () => ({ type: 'solid', status: 'primary' }) },
|
||||||
uncheckedProps: { type: Object as PropType<any>, default: () => ({ type: 'solid', status: 'default' }) },
|
uncheckedProps: { type: Object as PropType<any>, default: () => ({ type: 'solid', status: 'default' }) },
|
||||||
@@ -25,7 +24,7 @@ export default defineComponent({
|
|||||||
class: { type: [String, Array, Object], default: '' },
|
class: { type: [String, Array, Object], default: '' },
|
||||||
style: { type: Object, default: () => ({}) },
|
style: { type: Object, default: () => ({}) },
|
||||||
},
|
},
|
||||||
emits: ['click', 'close', 'update:checked', 'update:modelValue'],
|
emits: ['click', 'close', 'update:checked'],
|
||||||
setup(props, { slots, emit }) {
|
setup(props, { slots, emit }) {
|
||||||
const prefixCls = getPrefixCls('tag')
|
const prefixCls = getPrefixCls('tag')
|
||||||
const tagGroupCtx = inject<TagGroupContext | undefined>(tagGroupKey, undefined)
|
const tagGroupCtx = inject<TagGroupContext | undefined>(tagGroupKey, undefined)
|
||||||
@@ -34,38 +33,33 @@ export default defineComponent({
|
|||||||
const visible = ref(true)
|
const visible = ref(true)
|
||||||
|
|
||||||
const isDisabled = computed(() => props.disabled || tagGroupCtx?.disabled)
|
const isDisabled = computed(() => props.disabled || tagGroupCtx?.disabled)
|
||||||
|
const isCheckable = computed(() => props.checkable || tagGroupCtx?.checkable)
|
||||||
|
|
||||||
const isChecked = computed(() => {
|
const isChecked = computed(() => {
|
||||||
if (inGroup.value) {
|
if (inGroup.value && tagGroupCtx) {
|
||||||
const groupValue = tagGroupCtx!.value
|
const groupValue = tagGroupCtx.value
|
||||||
|
if (Array.isArray(groupValue)) {
|
||||||
return groupValue.includes(props.value)
|
return groupValue.includes(props.value)
|
||||||
}
|
}
|
||||||
return props.checked ?? props.modelValue ?? _checked.value
|
return groupValue === props.value
|
||||||
|
}
|
||||||
|
return props.checked ?? _checked.value
|
||||||
})
|
})
|
||||||
|
|
||||||
const isCheckable = computed(() => props.checkable || tagGroupCtx?.checkable)
|
|
||||||
|
|
||||||
const handleClick = (event: MouseEvent) => {
|
const handleClick = (event: MouseEvent) => {
|
||||||
if (isDisabled.value)
|
if (isDisabled.value)
|
||||||
return
|
return
|
||||||
|
|
||||||
if (inGroup.value) {
|
if (inGroup.value && tagGroupCtx) {
|
||||||
// 多选:切换
|
tagGroupCtx.handleChange(props.value, event)
|
||||||
if (tagGroupCtx!.multiple) {
|
|
||||||
tagGroupCtx!.handleChange(props.value, event)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// 单选:清空已有值或选中新值
|
|
||||||
const newValue = isChecked.value ? '' : props.value
|
|
||||||
tagGroupCtx!.handleChange(newValue, event)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// 独立 Tag 自己控制
|
// 独立 Tag 自己控制
|
||||||
const newChecked = !isChecked.value
|
const newChecked = !isChecked.value
|
||||||
_checked.value = newChecked
|
_checked.value = newChecked
|
||||||
emit('update:modelValue', newChecked)
|
|
||||||
emit('update:checked', newChecked)
|
emit('update:checked', newChecked)
|
||||||
}
|
}
|
||||||
|
emit('click', { value: props.value, event })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClose = (event: MouseEvent) => {
|
const handleClose = (event: MouseEvent) => {
|
||||||
@@ -75,27 +69,14 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return () =>
|
return () =>
|
||||||
visible.value && (() => {
|
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
|
<div
|
||||||
class={[
|
class={[
|
||||||
prefixCls,
|
prefixCls,
|
||||||
`${prefixCls}-size-${props.size}`,
|
`${prefixCls}-size-${props.size}`,
|
||||||
`${prefixCls}-shape-${props.shape}`,
|
`${prefixCls}-shape-${props.shape}`,
|
||||||
`${prefixCls}-type-${renderType}`,
|
`${prefixCls}-type-${isCheckable.value ? (isChecked.value ? props.checkedProps.type : props.uncheckedProps.type) : props.type}`,
|
||||||
`${prefixCls}-status-${renderStatus}`,
|
`${prefixCls}-status-${isCheckable.value ? (isChecked.value ? props.checkedProps.status : props.uncheckedProps.status) : props.status}`,
|
||||||
{ [`${prefixCls}-check`]: isCheckable.value },
|
{ [`${prefixCls}-check`]: isCheckable.value },
|
||||||
{ [`${prefixCls}-checked`]: isChecked.value },
|
{ [`${prefixCls}-checked`]: isChecked.value },
|
||||||
{ [`${prefixCls}-disabled`]: isDisabled.value },
|
{ [`${prefixCls}-disabled`]: isDisabled.value },
|
||||||
@@ -111,6 +92,5 @@ export default defineComponent({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})()
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -1,101 +1,200 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
interface TagOption {
|
const selectedValue = ref()
|
||||||
label: string
|
|
||||||
value: string | number
|
|
||||||
}
|
|
||||||
|
|
||||||
const tagOptions: TagOption[] = [
|
// 自定义字段名
|
||||||
{ label: '标签1', value: 1 },
|
const options = [
|
||||||
{ label: '标签2', value: 2 },
|
{ name: '选项一', id: 1, note: '这是额外信息 1' },
|
||||||
{ label: '标签3', value: 3 },
|
{ name: '选项二', id: 2, note: '这是额外信息 2' },
|
||||||
|
{ name: '选项三', id: 3 },
|
||||||
]
|
]
|
||||||
|
|
||||||
const customOptions: TagOption[] = [
|
function handleChange(val: string | number | boolean) {
|
||||||
{ label: '成功', value: 'success' },
|
console.log('当前选中值:', val)
|
||||||
{ label: '警告', value: 'warning' },
|
|
||||||
{ label: '错误', value: 'error' },
|
|
||||||
]
|
|
||||||
|
|
||||||
// 多选示例
|
|
||||||
const multiSelected = ref<(string | number)[]>([1, 2])
|
|
||||||
function handleMultiChange(selectedOptions: TagOption[]) {
|
|
||||||
console.log('多选 change:', selectedOptions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 单选示例
|
function toggleThemeLight() {
|
||||||
const singleSelected = ref<(string | number)[]>([2])
|
document.documentElement.setAttribute('theme-mode', 'light')
|
||||||
function handleSingleChange(selectedOptions: TagOption[]) {
|
|
||||||
console.log('单选 change:', selectedOptions)
|
|
||||||
}
|
}
|
||||||
|
function toggleThemeDark() {
|
||||||
// slot 渲染示例
|
document.documentElement.setAttribute('theme-mode', 'dark')
|
||||||
const slotSelected = ref<(string | number)[]>(['b'])
|
|
||||||
|
|
||||||
// 自定义样式示例
|
|
||||||
const customSelected = ref<(string | number)[]>(['success'])
|
|
||||||
function handleCustomChange(selectedOptions: TagOption[]) {
|
|
||||||
console.log('自定义样式 change:', selectedOptions)
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="padding: 20px;">
|
<AButton status="primary" @click="toggleThemeLight">
|
||||||
<h2>ATagGroup 使用示例</h2>
|
白天
|
||||||
|
</AButton>
|
||||||
|
<AButton status="primary" @click="toggleThemeDark">
|
||||||
|
黑天
|
||||||
|
</AButton>
|
||||||
|
|
||||||
<!-- 1. 多选 options 渲染 -->
|
<div class="demo-wrapper">
|
||||||
<section style="margin-bottom: 20px;">
|
<div class="demo-container">
|
||||||
<h3>多选 - Options 渲染</h3>
|
<h3>Default</h3>
|
||||||
<ATagGroup
|
<ARadioGroup
|
||||||
v-model="multiSelected"
|
v-model="selectedValue"
|
||||||
:options="tagOptions"
|
:options="options"
|
||||||
multiple
|
label-field="name"
|
||||||
@change="handleMultiChange"
|
value-field="id"
|
||||||
|
extra-field="note"
|
||||||
|
type="default"
|
||||||
|
@change="handleChange"
|
||||||
/>
|
/>
|
||||||
<p>选中值: {{ multiSelected }}</p>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- 2. 单选 options 渲染 -->
|
<div class="demo-container">
|
||||||
<section style="margin-bottom: 20px;">
|
<h3>filled</h3>
|
||||||
<h3>单选 - Options 渲染</h3>
|
<ARadioGroup
|
||||||
<ATagGroup
|
v-model="selectedValue"
|
||||||
v-model="singleSelected"
|
:options="options"
|
||||||
:options="tagOptions"
|
label-field="name"
|
||||||
:multiple="false"
|
value-field="id"
|
||||||
@change="handleSingleChange"
|
extra-field="note"
|
||||||
|
type="filled"
|
||||||
|
size="small"
|
||||||
|
:allow-uncheck="false"
|
||||||
|
@change="handleChange"
|
||||||
/>
|
/>
|
||||||
<p>选中值: {{ singleSelected }}</p>
|
</div>
|
||||||
</section>
|
<div class="demo-container">
|
||||||
|
<h3>filled</h3>
|
||||||
<!-- 3. Slot 渲染 -->
|
<ARadioGroup
|
||||||
<section style="margin-bottom: 20px;">
|
v-model="selectedValue"
|
||||||
<h3>Slot 渲染</h3>
|
:options="options"
|
||||||
<ATagGroup v-model="slotSelected">
|
label-field="name"
|
||||||
<ATag value="a">
|
value-field="id"
|
||||||
自定义A
|
extra-field="note"
|
||||||
</ATag>
|
type="filled"
|
||||||
<ATag value="b">
|
@change="handleChange"
|
||||||
自定义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>
|
</div>
|
||||||
</section>
|
<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>
|
</div>
|
||||||
</template>
|
</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