refactor: 整理组件目录结构
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 4m47s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
Some checks failed
Docker Build & Deploy / Build Docker Image (push) Failing after 4m47s
Docker Build & Deploy / Deploy to Production (push) Has been skipped
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
- TransactionDetail, CategoryBillPopup 移入 Transaction/ - BudgetTypeTabs 移入 Budget/ - GlassBottomNav, ModernEmpty 移入 Global/ - Icon, IconSelector, ClassifySelector 等 8 个通用组件移入 Common/ - 更新所有相关引用路径
This commit is contained in:
260
Web/src/components/Common/ClassifySelector.vue
Normal file
260
Web/src/components/Common/ClassifySelector.vue
Normal file
@@ -0,0 +1,260 @@
|
||||
<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(() => {
|
||||
if (props.options) {
|
||||
return props.options
|
||||
}
|
||||
return innerOptions.value
|
||||
})
|
||||
|
||||
const fetchOptions = async () => {
|
||||
if (props.options) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await getCategoryList(props.type)
|
||||
if (response.success) {
|
||||
innerOptions.value = (response.data || []).map((item) => ({
|
||||
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
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
showToast('分类创建成功')
|
||||
// 刷新列表
|
||||
await fetchOptions()
|
||||
|
||||
// 如果是单选模式,且当前没有选值或就是为了新增,则自动选中
|
||||
if (!props.multiple) {
|
||||
emit('update:modelValue', categoryName)
|
||||
emit('change', categoryName)
|
||||
}
|
||||
} else {
|
||||
showToast(response.message || '创建分类失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('ClassifySelector 创建分类出错:', error)
|
||||
showToast('创建分类失败')
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.type,
|
||||
() => {
|
||||
fetchOptions()
|
||||
}
|
||||
)
|
||||
|
||||
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(() => {
|
||||
if (!props.multiple || displayOptions.value.length === 0) {
|
||||
return false
|
||||
}
|
||||
return displayOptions.value.every((item) => props.modelValue.includes(item.text))
|
||||
})
|
||||
|
||||
// 是否有任何选中
|
||||
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 = () => {
|
||||
if (!props.multiple) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isAllSelected.value) {
|
||||
emit('update:modelValue', [])
|
||||
emit('change', [])
|
||||
} else {
|
||||
const allValues = displayOptions.value.map((item) => item.text)
|
||||
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>
|
||||
Reference in New Issue
Block a user