2025-09-17 10:19:20 +08:00

332 lines
7.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<!-- 遮罩层 -->
<div
v-if="visible"
class="dialog-mask"
@click="handleCancel"
></div>
<!-- 弹窗主体 -->
<div
v-if="visible"
class="dialog-container"
>
<div class="dialog-box">
<!-- 标题区域 -->
<div class="dialog-header">
<h3 class="dialog-title">{{ title }}</h3>
<button
class="dialog-close"
@click="handleCancel"
aria-label="关闭"
>
<span>×</span>
</button>
</div>
<!-- 内容区域 -->
<div class="dialog-body">
<!-- 搜索框 -->
<el-input
v-model="searchKey"
placeholder="搜索(支持名称、编码、简拼)..."
class="search-input"
clearable
@clear="filterDictData"
@input="filterDictData"
/>
<!-- 字典列表 -->
<el-table
:data="filteredDictData"
height="300px"
border
@row-click="selectItem"
@row-dblclick="selectItem"
:loading="isDictLoading"
>
<el-table-column prop="value" label="编码" width="100" />
<el-table-column prop="label" label="名称" width="200" />
<el-table-column prop="pinyin" label="简拼" width="150" />
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button size="small" type="primary" @click="selectItem(scope.row)">选择</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { getFirstLetter } from '@/api/pinyin.js'; // 拼音处理工具
// 组件参数
const props = defineProps({
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: '选择字典项'
},
modelValue: {
type: [String, Number],
default: ''
},
dictType: {
type: String,
required: true
},
placeholder: {
type: String,
default: '请输入或双击选择'
},
clearable: {
type: Boolean,
default: true
},
fetchDict: {
type: Function,
required: true
},
disabled: {
type: Boolean,
default: false
}
});
// 组件事件
const emit = defineEmits([
'update:modelValue',
'update:labelValue',
'select',
'update:visible',
'onCancel'
]);
// 内部状态管理
const tempValue = ref('');
const labelValue = ref('');
const dictData = ref([]); // 存储带简拼的字典数据
const filteredDictData = ref([]);
const isDictLoading = ref(true);
const searchKey = ref('');
const dialogVisible = ref(props.visible); // 内部弹窗状态
// 显示值计算属性
const displayValue = computed({
get() {
return labelValue.value || tempValue.value || props.modelValue || '';
},
set(newValue) {
tempValue.value = newValue;
if (labelValue.value && newValue !== labelValue.value) {
labelValue.value = '';
emit('update:labelValue', '');
}
}
});
// 加载字典数据并添加简拼字段
const loadDictData = async () => {
try {
isDictLoading.value = true;
const rawData = await props.fetchDict(props.dictType);
// 为每个字典项添加简拼字段
dictData.value = (rawData || []).map(item => ({
...item,
pinyin: getFirstLetter(item.label) || ''
}));
filteredDictData.value = [...dictData.value];
syncLabelWithValue();
} catch (error) {
console.error(`加载${props.dictType}字典失败:`, error);
dictData.value = [];
filteredDictData.value = [];
} finally {
isDictLoading.value = false;
}
};
// 根据当前值同步显示标签
const syncLabelWithValue = () => {
if (props.modelValue) {
const matched = dictData.value.find(
item => String(item.value) === String(props.modelValue)
);
labelValue.value = matched?.label || props.modelValue;
emit('update:labelValue', labelValue.value);
}
};
// 字典搜索过滤逻辑
const filterDictData = () => {
const key = searchKey.value.trim().toLowerCase();
if (!key) {
filteredDictData.value = [...dictData.value];
return;
}
filteredDictData.value = dictData.value.filter(item => {
const matchLabel = item.label?.toLowerCase().includes(key) || false;
const matchValue = String(item.value).toLowerCase().includes(key);
const matchPinyin = item.pinyin?.toLowerCase().includes(key) || false;
return matchLabel || matchValue || matchPinyin;
});
};
// 打开弹窗
const openDictSelector = () => {
if (!props.disabled && !isDictLoading.value) {
dialogVisible.value = true;
searchKey.value = '';
filterDictData();
}
};
// 选择字典项
const selectItem = (item) => {
if (!item) return;
emit('update:modelValue', item.value);
emit('update:labelValue', item.label);
emit('select', item);
tempValue.value = item.value;
labelValue.value = item.label;
dialogVisible.value = false;
emit('update:visible', false);
};
// 处理取消
const handleCancel = () => {
emit('onCancel');
dialogVisible.value = false;
emit('update:visible', false);
};
// 监听visible属性变化
watch(() => props.visible, (newVal) => {
dialogVisible.value = newVal;
if (newVal) {
searchKey.value = '';
filterDictData();
}
});
// 监听初始值变化
watch(() => props.modelValue, (newVal) => {
tempValue.value = newVal || '';
if (newVal && !isDictLoading.value) {
syncLabelWithValue();
}
}, { immediate: true });
// 监听字典类型变化,重新加载数据
watch(() => props.dictType, loadDictData, { immediate: false });
// 组件挂载时加载字典
onMounted(async () => {
await loadDictData();
});
</script>
<style scoped>
/* 遮罩层 */
.dialog-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
transition: opacity 0.3s;
}
/* 弹窗容器 */
.dialog-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1001;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
/* 弹窗主体 */
.dialog-box {
width: 100%;
max-width: 600px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
overflow: hidden;
}
/* 标题区域 */
.dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
background-color: #f9fafb;
}
.dialog-title {
margin: 0;
font-size: 16px;
font-weight: 600;
color: #333;
}
.dialog-close {
padding: 0;
background: transparent;
border: none;
font-size: 20px;
color: #999;
cursor: pointer;
transition: color 0.2s;
}
.dialog-close:hover {
color: #333;
}
/* 内容区域 */
.dialog-body {
padding: 20px;
}
.search-input {
margin-bottom: 16px;
}
/* 表格样式优化 */
::v-deep .el-table {
border-radius: 4px;
}
::v-deep .el-table__empty-text {
color: #909399;
}
/* 按钮样式优化 */
::v-deep .el-button--small {
padding: 4px 12px;
}
</style>