2026-01-08 14:41:50 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="classify-selector">
|
|
|
|
|
|
<div class="classify-buttons">
|
|
|
|
|
|
<!-- 全选按钮 (仅多选模式) -->
|
|
|
|
|
|
<van-button
|
|
|
|
|
|
v-if="multiple && showAll"
|
|
|
|
|
|
:type="isAllSelected ? 'primary' : 'default'"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
class="classify-btn all-btn"
|
|
|
|
|
|
@click="toggleAll"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ isAllSelected ? '取消全选' : '全选' }}
|
|
|
|
|
|
</van-button>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 新增按钮 -->
|
|
|
|
|
|
<van-button
|
|
|
|
|
|
v-if="showAdd"
|
|
|
|
|
|
type="success"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
class="classify-btn"
|
|
|
|
|
|
@click="openAddDialog"
|
|
|
|
|
|
>
|
|
|
|
|
|
+ 新增
|
|
|
|
|
|
</van-button>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 分类项按钮 -->
|
|
|
|
|
|
<van-button
|
|
|
|
|
|
v-for="item in displayOptions"
|
|
|
|
|
|
:key="item.id || item.text"
|
|
|
|
|
|
:type="isSelected(item) ? 'primary' : 'default'"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
class="classify-btn"
|
|
|
|
|
|
@click="toggleItem(item)"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ item.text }}
|
|
|
|
|
|
</van-button>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 清空按钮 -->
|
|
|
|
|
|
<van-button
|
|
|
|
|
|
v-if="showClear && hasSelection"
|
|
|
|
|
|
type="warning"
|
|
|
|
|
|
size="small"
|
|
|
|
|
|
class="classify-btn"
|
|
|
|
|
|
@click="clear"
|
|
|
|
|
|
>
|
|
|
|
|
|
清空
|
|
|
|
|
|
</van-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 新增分类对话框 -->
|
|
|
|
|
|
<AddClassifyDialog
|
|
|
|
|
|
ref="addClassifyDialogRef"
|
|
|
|
|
|
@confirm="handleAddConfirm"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, computed, watch, onMounted } from 'vue'
|
|
|
|
|
|
import { showToast } from 'vant'
|
|
|
|
|
|
import { getCategoryList, createCategory } from '@/api/transactionCategory'
|
|
|
|
|
|
import AddClassifyDialog from './AddClassifyDialog.vue'
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
modelValue: {
|
|
|
|
|
|
type: [String, Array],
|
|
|
|
|
|
default: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
options: {
|
|
|
|
|
|
type: Array,
|
|
|
|
|
|
default: null
|
|
|
|
|
|
},
|
|
|
|
|
|
type: {
|
|
|
|
|
|
type: [Number, String],
|
|
|
|
|
|
default: null
|
|
|
|
|
|
},
|
|
|
|
|
|
multiple: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
},
|
|
|
|
|
|
showAdd: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: true
|
|
|
|
|
|
},
|
|
|
|
|
|
showClear: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: true
|
|
|
|
|
|
},
|
|
|
|
|
|
showAll: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: true
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits(['update:modelValue', 'add', 'change'])
|
|
|
|
|
|
|
|
|
|
|
|
const innerOptions = ref([])
|
|
|
|
|
|
const addClassifyDialogRef = ref()
|
|
|
|
|
|
|
|
|
|
|
|
const displayOptions = computed(() => {
|
2026-01-16 11:15:44 +08:00
|
|
|
|
if (props.options) {
|
|
|
|
|
|
return props.options
|
|
|
|
|
|
}
|
2026-01-08 14:41:50 +08:00
|
|
|
|
return innerOptions.value
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const fetchOptions = async () => {
|
2026-01-16 11:15:44 +08:00
|
|
|
|
if (props.options) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 14:41:50 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const response = await getCategoryList(props.type)
|
|
|
|
|
|
if (response.success) {
|
2026-01-16 11:15:44 +08:00
|
|
|
|
innerOptions.value = (response.data || []).map((item) => ({
|
2026-01-08 14:41:50 +08:00
|
|
|
|
text: item.name,
|
|
|
|
|
|
value: item.name,
|
|
|
|
|
|
id: item.id
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('ClassifySelector 加载分类失败:', error)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 打开新增对话框
|
|
|
|
|
|
const openAddDialog = () => {
|
|
|
|
|
|
addClassifyDialogRef.value?.open()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理新增确认
|
|
|
|
|
|
const handleAddConfirm = async (categoryName) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 调用API创建分类
|
|
|
|
|
|
const response = await createCategory({
|
|
|
|
|
|
name: categoryName,
|
|
|
|
|
|
type: props.type
|
|
|
|
|
|
})
|
2026-01-16 11:15:44 +08:00
|
|
|
|
|
2026-01-08 14:41:50 +08:00
|
|
|
|
if (response.success) {
|
|
|
|
|
|
showToast('分类创建成功')
|
|
|
|
|
|
// 刷新列表
|
|
|
|
|
|
await fetchOptions()
|
2026-01-16 11:15:44 +08:00
|
|
|
|
|
2026-01-08 14:41:50 +08:00
|
|
|
|
// 如果是单选模式,且当前没有选值或就是为了新增,则自动选中
|
|
|
|
|
|
if (!props.multiple) {
|
|
|
|
|
|
emit('update:modelValue', categoryName)
|
|
|
|
|
|
emit('change', categoryName)
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showToast(response.message || '创建分类失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('ClassifySelector 创建分类出错:', error)
|
|
|
|
|
|
showToast('创建分类失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-16 11:15:44 +08:00
|
|
|
|
watch(
|
|
|
|
|
|
() => props.type,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
fetchOptions()
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
2026-01-08 14:41:50 +08:00
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
fetchOptions()
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 公开刷新方法
|
|
|
|
|
|
defineExpose({
|
|
|
|
|
|
refresh: fetchOptions
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 是否选中
|
|
|
|
|
|
const isSelected = (item) => {
|
|
|
|
|
|
if (props.multiple) {
|
|
|
|
|
|
return Array.isArray(props.modelValue) && props.modelValue.includes(item.text)
|
|
|
|
|
|
}
|
|
|
|
|
|
return props.modelValue === item.text
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 是否全部选中
|
|
|
|
|
|
const isAllSelected = computed(() => {
|
2026-01-16 11:15:44 +08:00
|
|
|
|
if (!props.multiple || displayOptions.value.length === 0) {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
return displayOptions.value.every((item) => props.modelValue.includes(item.text))
|
2026-01-08 14:41:50 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 是否有任何选中
|
|
|
|
|
|
const hasSelection = computed(() => {
|
|
|
|
|
|
if (props.multiple) {
|
|
|
|
|
|
return Array.isArray(props.modelValue) && props.modelValue.length > 0
|
|
|
|
|
|
}
|
|
|
|
|
|
return !!props.modelValue
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 切换选中状态
|
|
|
|
|
|
const toggleItem = (item) => {
|
|
|
|
|
|
if (props.multiple) {
|
|
|
|
|
|
const newValue = Array.isArray(props.modelValue) ? [...props.modelValue] : []
|
|
|
|
|
|
const index = newValue.indexOf(item.text)
|
|
|
|
|
|
if (index > -1) {
|
|
|
|
|
|
newValue.splice(index, 1)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newValue.push(item.text)
|
|
|
|
|
|
}
|
|
|
|
|
|
emit('update:modelValue', newValue)
|
|
|
|
|
|
emit('change', newValue)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const newValue = props.modelValue === item.text ? '' : item.text
|
|
|
|
|
|
emit('update:modelValue', newValue)
|
|
|
|
|
|
emit('change', newValue)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 切换全选
|
|
|
|
|
|
const toggleAll = () => {
|
2026-01-16 11:15:44 +08:00
|
|
|
|
if (!props.multiple) {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-08 14:41:50 +08:00
|
|
|
|
if (isAllSelected.value) {
|
|
|
|
|
|
emit('update:modelValue', [])
|
|
|
|
|
|
emit('change', [])
|
|
|
|
|
|
} else {
|
2026-01-16 11:15:44 +08:00
|
|
|
|
const allValues = displayOptions.value.map((item) => item.text)
|
2026-01-08 14:41:50 +08:00
|
|
|
|
emit('update:modelValue', allValues)
|
|
|
|
|
|
emit('change', allValues)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清空
|
|
|
|
|
|
const clear = () => {
|
|
|
|
|
|
const newValue = props.multiple ? [] : ''
|
|
|
|
|
|
emit('update:modelValue', newValue)
|
|
|
|
|
|
emit('change', newValue)
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.classify-buttons {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.classify-btn {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
min-width: 70px;
|
|
|
|
|
|
border-radius: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.all-btn {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
border-style: dashed;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|