同步6/16
This commit is contained in:
@@ -84,7 +84,7 @@ const handleClick = (item) => {
|
||||
};
|
||||
|
||||
// 暴露方法给父组件调用
|
||||
defineExpose({ open });
|
||||
defineExpose({ open, close });
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue';
|
||||
import { ref, onMounted, onBeforeUnmount, watch, nextTick, shallowRef } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import type { EChartsOption, ECharts } from 'echarts';
|
||||
|
||||
@@ -19,7 +19,6 @@ interface Props {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
options: () => ({}) as EChartsOption,
|
||||
theme: '',
|
||||
autoResize: true,
|
||||
immediate: true
|
||||
@@ -27,36 +26,38 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
|
||||
const emit = defineEmits<{
|
||||
ready: [chart: ECharts];
|
||||
click: [params: any];
|
||||
click: [params: echarts.ECElementEvent];
|
||||
rendered: [];
|
||||
}>();
|
||||
|
||||
const chartRef = ref<HTMLDivElement>();
|
||||
let chartInstance: ECharts | null = null;
|
||||
// 3. 使用 shallowRef 优化性能,因为 ECharts 实例不需要深层响应式
|
||||
const chartInstance = shallowRef<ECharts | null>(null);
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const initChart = () => {
|
||||
if (!chartRef.value) return;
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.dispose();
|
||||
chartInstance.value = null;
|
||||
}
|
||||
|
||||
chartInstance = echarts.init(chartRef.value, props.theme);
|
||||
chartInstance.setOption(props.options);
|
||||
const instance = echarts.init(chartRef.value, props.theme);
|
||||
chartInstance.value = instance;
|
||||
chartInstance.value.setOption(props.options);
|
||||
|
||||
emit('ready', chartInstance);
|
||||
emit('ready', instance);
|
||||
emit('rendered');
|
||||
|
||||
chartInstance.on('click', (params: any) => {
|
||||
instance.on('click', (params: any) => {
|
||||
emit('click', params);
|
||||
});
|
||||
};
|
||||
|
||||
const updateChart = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.setOption(props.options, {
|
||||
if (chartInstance.value) {
|
||||
chartInstance.value.setOption(props.options, {
|
||||
notMerge: false,
|
||||
lazyUpdate: false
|
||||
});
|
||||
@@ -66,15 +67,15 @@ const updateChart = () => {
|
||||
|
||||
// ✅ 核心修复:支持 scale 缩放后重绘
|
||||
const resizeChart = () => {
|
||||
if (!chartInstance) return;
|
||||
chartInstance.resize();
|
||||
if (!chartInstance.value) return;
|
||||
chartInstance.value.resize();
|
||||
};
|
||||
|
||||
const getInstance = () => chartInstance;
|
||||
const getInstance = () => chartInstance.value;
|
||||
|
||||
const exportAsImage = (type: 'png' | 'svg' | 'jpeg' = 'png') => {
|
||||
return (
|
||||
chartInstance?.getDataURL({
|
||||
chartInstance.value?.getDataURL({
|
||||
type,
|
||||
pixelRatio: 2,
|
||||
backgroundColor: '#fff'
|
||||
@@ -86,23 +87,18 @@ watch(() => props.options, updateChart, { deep: true });
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => initChart());
|
||||
|
||||
if (props.autoResize && chartRef.value) {
|
||||
resizeObserver = new ResizeObserver(resizeChart);
|
||||
resizeObserver.observe(chartRef.value);
|
||||
}
|
||||
});
|
||||
|
||||
// ✅ 关键:监听 window.resize(scale 缩放会触发 window.resize)
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', resizeChart);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', resizeChart);
|
||||
resizeObserver?.disconnect();
|
||||
chartInstance?.dispose();
|
||||
chartInstance = null;
|
||||
chartInstance.value?.dispose();
|
||||
chartInstance.value = null;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
|
||||
304
src/components/Holder/CarUser.vue
Normal file
304
src/components/Holder/CarUser.vue
Normal file
@@ -0,0 +1,304 @@
|
||||
<template>
|
||||
<slot name="default" />
|
||||
<el-dialog append-to-body v-model="dialog.dialogVisible" :title="dialog.dialogTitle" width="1050">
|
||||
<el-table ref="multipleTableRef" :data="tableData" border @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" width="55" type="index" />
|
||||
<el-table-column label="业主" align="center">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.residentName ? scope.row.residentName : '---' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="性别" align="center">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_user_sex" :value="scope.row.gender"></dict-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="手机号" align="center" prop="phone" />
|
||||
<el-table-column label="车位" align="center" prop="parkingName" />
|
||||
</el-table>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer flex flex-items-center flex-justify-between">
|
||||
<pagination
|
||||
style="margin-top: 0"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getlist"
|
||||
/>
|
||||
<div>
|
||||
<el-button @click="conback">退出</el-button>
|
||||
<el-button type="primary" @click="confirm"> 确定 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getBuildinglistAPI, getHouselistAPI } from '@/api/system/house';
|
||||
import { BillUserlist, queryResidentList_Cars, queryResidentList_holder } from '@/api/system/SdbBill';
|
||||
import { ref, nextTick, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_user_sex } = toRefs<any>(proxy?.useDict('sys_user_sex'));
|
||||
|
||||
const props = defineProps({
|
||||
returnvalue: {
|
||||
type: String,
|
||||
default: 'parkingId'
|
||||
},
|
||||
titleName: {
|
||||
type: String,
|
||||
default: '选择业主'
|
||||
},
|
||||
/** 是否多选 false 单选 */
|
||||
isMultiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const { titleName, returnvalue } = toRefs(props);
|
||||
|
||||
const userid = ref([]);
|
||||
// ✅ 手动设置默认值(替代 withDefaults)
|
||||
const emit = defineEmits<{
|
||||
change: [value: string | number | (string | number)[] | BillUserlist | BillUserlist[]]; // 支持返回数组
|
||||
}>();
|
||||
|
||||
const dialog = ref({
|
||||
dialogVisible: false,
|
||||
dialogTitle: titleName.value
|
||||
});
|
||||
|
||||
const queryParams = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10
|
||||
});
|
||||
|
||||
const multipleTableRef = ref();
|
||||
const tableData = ref<BillUserlist[]>([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const isFetching = ref(false);
|
||||
// 存储选中项
|
||||
const multipleSelection = ref<BillUserlist[]>([]);
|
||||
|
||||
const isMultiple = computed(() => props.isMultiple ?? false);
|
||||
|
||||
// 优化 handleUserSelection,避免重复查找
|
||||
const handleUserSelection = () => {
|
||||
isFetching.value = true;
|
||||
if (!multipleTableRef.value || !tableData.value.length) {
|
||||
isFetching.value = false;
|
||||
return;
|
||||
}
|
||||
const rowsToSelect = tableData.value.filter((row) => {
|
||||
return multipleSelection.value.some((selected) => selected.parkingId === row.parkingId);
|
||||
});
|
||||
multipleTableRef.value.clearSelection();
|
||||
if (rowsToSelect.length > 0) {
|
||||
nextTick(() => {
|
||||
rowsToSelect.forEach((row) => {
|
||||
multipleTableRef.value?.toggleRowSelection(row, true);
|
||||
});
|
||||
isFetching.value = false;
|
||||
});
|
||||
} else {
|
||||
isFetching.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ====================== 方法 ======================
|
||||
|
||||
// 获取列表
|
||||
function getlist() {
|
||||
isFetching.value = true; // 加锁
|
||||
loading.value = true;
|
||||
queryResidentList_Cars(queryParams.value)
|
||||
.then((res) => {
|
||||
tableData.value = res.rows;
|
||||
total.value = res.total;
|
||||
handleUserSelection();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('获取用户列表失败:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
isFetching.value = false; // 加锁
|
||||
});
|
||||
}
|
||||
|
||||
/** 楼栋列表 */
|
||||
const buildinglist = ref([]);
|
||||
/** 房屋列表 */
|
||||
const unitlist = ref([]);
|
||||
|
||||
/** 筛选房屋类型 */
|
||||
function handleChangeHouse(val: string) {
|
||||
getBuildinglistAPI().then((res) => {
|
||||
buildinglist.value = res.data;
|
||||
});
|
||||
}
|
||||
/** 单元列表 */
|
||||
function handleChangebuilding(val: string) {
|
||||
getHouselistAPI(val).then((res) => {
|
||||
unitlist.value = res.data.units;
|
||||
});
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// ✅ 新增:处理 selection-change(多选专用)
|
||||
// ==============================================
|
||||
function handleSelectionChange(selection: BillUserlist[]) {
|
||||
if (isFetching.value) return;
|
||||
if (isMultiple.value) {
|
||||
const currentSelectedIds = new Set(selection.map((item) => item.parkingId));
|
||||
const otherPageSelections = multipleSelection.value.filter((item) => {
|
||||
const isCurrentPageItem = tableData.value.some((row) => row.parkingId === item.parkingId);
|
||||
if (isCurrentPageItem) {
|
||||
return currentSelectedIds.has(item.parkingId);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
// 3. 合并:其他页保留的 + 当前页新选中的
|
||||
const finalSelection = [...otherPageSelections, ...selection];
|
||||
// 4. 去重(以防万一)
|
||||
const uniqueMap = new Map();
|
||||
finalSelection.forEach((item) => uniqueMap.set(item.parkingId, item));
|
||||
multipleSelection.value = Array.from(uniqueMap.values());
|
||||
console.log(multipleSelection.value.map((item) => item.parkingId));
|
||||
} else {
|
||||
// --- 单选逻辑 ---
|
||||
if (selection.length > 0) {
|
||||
const lastSelected = selection[selection.length - 1];
|
||||
multipleSelection.value = [lastSelected];
|
||||
nextTick(() => {
|
||||
if (selection.length > 1) {
|
||||
multipleTableRef.value?.clearSelection();
|
||||
multipleTableRef.value?.toggleRowSelection(lastSelected, true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
multipleSelection.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
// ==============================================
|
||||
// 最佳单选方案:点击行选中,无报错、无循环、最稳定
|
||||
// ==============================================
|
||||
// 优化 handleRowClick,避免重复
|
||||
function handleRowClick(row: BillUserlist) {
|
||||
if (!multipleTableRef.value) return;
|
||||
|
||||
if (isMultiple.value) {
|
||||
const index = multipleSelection.value.findIndex((item) => item.parkingId === row.parkingId);
|
||||
if (index > -1) {
|
||||
// 取消选中
|
||||
multipleSelection.value.splice(index, 1);
|
||||
multipleTableRef.value?.toggleRowSelection(row, false);
|
||||
} else {
|
||||
// 选中
|
||||
multipleSelection.value.push(row);
|
||||
multipleTableRef.value?.toggleRowSelection(row, true);
|
||||
}
|
||||
} else {
|
||||
// 【单选模式】先清空,再选中当前行
|
||||
multipleSelection.value = [];
|
||||
multipleTableRef.value.clearSelection();
|
||||
multipleSelection.value = [row];
|
||||
multipleTableRef.value.toggleRowSelection(row, true);
|
||||
}
|
||||
}
|
||||
// 重置查询
|
||||
function resetQuery() {
|
||||
queryParams.value = {
|
||||
pageNum: 1,
|
||||
pageSize: 10
|
||||
};
|
||||
getlist();
|
||||
}
|
||||
|
||||
// 退出
|
||||
function conback() {
|
||||
dialog.value.dialogVisible = false;
|
||||
resetQuery();
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// ✅ 核心修改:根据 returnvalue 返回对应的值
|
||||
// ==============================================
|
||||
function confirm() {
|
||||
dialog.value.dialogVisible = false;
|
||||
if (isMultiple.value) {
|
||||
// 多选
|
||||
if (returnvalue.value === '*') {
|
||||
// ✅ 返回整个对象数组
|
||||
emit('change', multipleSelection.value);
|
||||
} else {
|
||||
// 返回指定字段数组
|
||||
emit(
|
||||
'change',
|
||||
multipleSelection.value.map((item) => item[returnvalue.value as keyof BillUserlist])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 单选
|
||||
if (returnvalue.value === '*') {
|
||||
// ✅ 返回整个对象
|
||||
emit('change', multipleSelection.value[0]);
|
||||
} else {
|
||||
// 返回指定字段
|
||||
emit('change', multipleSelection.value[0][returnvalue.value as keyof BillUserlist]);
|
||||
}
|
||||
}
|
||||
|
||||
multipleSelection.value = [];
|
||||
multipleTableRef.value.clearSelection();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open: (arr: string[] = []) => {
|
||||
userid.value = arr;
|
||||
if (arr && arr.length > 0) {
|
||||
multipleSelection.value = arr.map(
|
||||
(id) =>
|
||||
({
|
||||
parkingId: id
|
||||
}) as BillUserlist
|
||||
);
|
||||
console.log(multipleSelection.value);
|
||||
} else {
|
||||
multipleSelection.value = [];
|
||||
}
|
||||
getlist();
|
||||
dialog.value.dialogVisible = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.residentBox {
|
||||
width: 385px;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
padding: 0 10px;
|
||||
.defaultbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: #f4f8ff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
251
src/components/Holder/appUser.vue
Normal file
251
src/components/Holder/appUser.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<slot name="default" />
|
||||
<el-dialog append-to-body v-model="dialog.dialogVisible" :title="dialog.dialogTitle" width="1050">
|
||||
<el-table v-loading="loading" ref="multipleTableRef" :data="tableData" border @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="业主" align="center">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.user_name ? scope.row.user_name : '---' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="手机号" align="center" prop="user_phone" />
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<div class="dialog-footer flex flex-items-center flex-justify-between">
|
||||
<pagination
|
||||
style="margin-top: 0"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
v-show="total > 0"
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getlist"
|
||||
/>
|
||||
<div>
|
||||
<el-button @click="conback">退出</el-button>
|
||||
<el-button type="primary" @click="confirm"> 确定 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { BillUserlist } from '@/api/system/SdbBill';
|
||||
import { ref, nextTick, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
|
||||
import { getAppUser } from '@/api/system/cars';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const props = defineProps({
|
||||
returnvalue: {
|
||||
type: String,
|
||||
default: '*'
|
||||
},
|
||||
titleName: {
|
||||
type: String,
|
||||
default: '选择业主'
|
||||
},
|
||||
/** 是否多选 false 单选 */
|
||||
isMultiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const { titleName, returnvalue } = toRefs(props);
|
||||
|
||||
const userid = ref([]);
|
||||
// ✅ 手动设置默认值(替代 withDefaults)
|
||||
const emit = defineEmits<{
|
||||
change: [value: string | number | (string | number)[] | BillUserlist | BillUserlist[]]; // 支持返回数组
|
||||
}>();
|
||||
|
||||
const dialog = ref({
|
||||
dialogVisible: false,
|
||||
dialogTitle: titleName.value
|
||||
});
|
||||
|
||||
const queryParams = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10
|
||||
});
|
||||
|
||||
const multipleTableRef = ref();
|
||||
const tableData = ref<BillUserlist[]>([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const isFetching = ref(false);
|
||||
// 存储选中项
|
||||
const multipleSelection = ref<BillUserlist[]>([]);
|
||||
|
||||
const isMultiple = computed(() => props.isMultiple ?? false);
|
||||
|
||||
// 优化 handleUserSelection,避免重复查找
|
||||
const handleUserSelection = () => {
|
||||
isFetching.value = true;
|
||||
if (!multipleTableRef.value || !tableData.value.length) {
|
||||
isFetching.value = false;
|
||||
return;
|
||||
}
|
||||
const rowsToSelect = tableData.value.filter((row) => {
|
||||
return multipleSelection.value.some((selected) => selected.parkingId === row.parkingId);
|
||||
});
|
||||
multipleTableRef.value.clearSelection();
|
||||
if (rowsToSelect.length > 0) {
|
||||
nextTick(() => {
|
||||
rowsToSelect.forEach((row) => {
|
||||
multipleTableRef.value?.toggleRowSelection(row, true);
|
||||
});
|
||||
isFetching.value = false;
|
||||
});
|
||||
} else {
|
||||
isFetching.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ====================== 方法 ======================
|
||||
|
||||
// 获取列表
|
||||
function getlist() {
|
||||
isFetching.value = true; // 加锁
|
||||
loading.value = true;
|
||||
getAppUser(queryParams.value)
|
||||
.then((res) => {
|
||||
tableData.value = res.rows;
|
||||
total.value = res.total;
|
||||
handleUserSelection();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('获取用户列表失败:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
isFetching.value = false; // 加锁
|
||||
});
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// ✅ 新增:处理 selection-change(多选专用)
|
||||
// ==============================================
|
||||
function handleSelectionChange(selection: BillUserlist[]) {
|
||||
if (isFetching.value) return;
|
||||
if (isMultiple.value) {
|
||||
const currentSelectedIds = new Set(selection.map((item) => item.parkingId));
|
||||
const otherPageSelections = multipleSelection.value.filter((item) => {
|
||||
const isCurrentPageItem = tableData.value.some((row) => row.parkingId === item.parkingId);
|
||||
if (isCurrentPageItem) {
|
||||
return currentSelectedIds.has(item.parkingId);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
// 3. 合并:其他页保留的 + 当前页新选中的
|
||||
const finalSelection = [...otherPageSelections, ...selection];
|
||||
// 4. 去重(以防万一)
|
||||
const uniqueMap = new Map();
|
||||
finalSelection.forEach((item) => uniqueMap.set(item.parkingId, item));
|
||||
multipleSelection.value = Array.from(uniqueMap.values());
|
||||
console.log(multipleSelection.value.map((item) => item.parkingId));
|
||||
} else {
|
||||
// --- 单选逻辑 ---
|
||||
if (selection.length > 0) {
|
||||
const lastSelected = selection[selection.length - 1];
|
||||
multipleSelection.value = [lastSelected];
|
||||
nextTick(() => {
|
||||
if (selection.length > 1) {
|
||||
multipleTableRef.value?.clearSelection();
|
||||
multipleTableRef.value?.toggleRowSelection(lastSelected, true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
multipleSelection.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
function resetQuery() {
|
||||
queryParams.value = {
|
||||
pageNum: 1,
|
||||
pageSize: 10
|
||||
};
|
||||
getlist();
|
||||
}
|
||||
|
||||
// 退出
|
||||
function conback() {
|
||||
dialog.value.dialogVisible = false;
|
||||
resetQuery();
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// ✅ 核心修改:根据 returnvalue 返回对应的值
|
||||
// ==============================================
|
||||
function confirm() {
|
||||
dialog.value.dialogVisible = false;
|
||||
if (isMultiple.value) {
|
||||
// 多选
|
||||
if (returnvalue.value === '*') {
|
||||
// ✅ 返回整个对象数组
|
||||
emit('change', multipleSelection.value);
|
||||
} else {
|
||||
// 返回指定字段数组
|
||||
emit(
|
||||
'change',
|
||||
multipleSelection.value.map((item) => item[returnvalue.value as keyof BillUserlist])
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 单选
|
||||
if (returnvalue.value === '*') {
|
||||
// ✅ 返回整个对象
|
||||
emit('change', multipleSelection.value[0]);
|
||||
} else {
|
||||
// 返回指定字段
|
||||
emit('change', multipleSelection.value[0][returnvalue.value as keyof BillUserlist]);
|
||||
}
|
||||
}
|
||||
|
||||
multipleSelection.value = [];
|
||||
multipleTableRef.value.clearSelection();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open: (arr: string[] = []) => {
|
||||
userid.value = arr;
|
||||
if (arr && arr.length > 0) {
|
||||
multipleSelection.value = arr.map(
|
||||
(id) =>
|
||||
({
|
||||
parkingId: id
|
||||
}) as BillUserlist
|
||||
);
|
||||
console.log(multipleSelection.value);
|
||||
} else {
|
||||
multipleSelection.value = [];
|
||||
}
|
||||
getlist();
|
||||
dialog.value.dialogVisible = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.residentBox {
|
||||
width: 385px;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
padding: 0 10px;
|
||||
.defaultbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: #f4f8ff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<slot name="default" />
|
||||
|
||||
<el-dialog append-to-body v-model="dialog.dialogVisible" :title="dialog.dialogTitle" width="1250">
|
||||
<nav class="flex flex-items-center mb-8">
|
||||
<el-dialog append-to-body v-model="dialog.dialogVisible" :title="dialog.dialogTitle" width="1400">
|
||||
<nav class="flex flex-items-center mb-8 justify-between">
|
||||
<!-- <div class="flex flex-items-center">
|
||||
<div style="width: 70px; text-align: right; padding-right: 10px">房屋类型</div>
|
||||
<div class="flex-1">
|
||||
@@ -12,32 +12,40 @@
|
||||
</div>
|
||||
</div> -->
|
||||
<div class="flex flex-items-center">
|
||||
<div style="width: 50px; text-align: right; padding-right: 10px">楼栋</div>
|
||||
<div class="flex-1"></div>
|
||||
<el-select @change="handleChangebuilding" clearable style="width: 140px" v-model="queryParams.buildingId">
|
||||
<el-option v-for="(item, index) in buildinglist" :key="index" :label="item.buildingName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="flex flex-items-center mr-20px">
|
||||
<div style="width: 50px; text-align: right; padding-right: 10px">单元</div>
|
||||
<div class="flex-1">
|
||||
<el-select @change="handleChangeUnit" clearable style="width: 140px" v-model="queryParams.unitNoId">
|
||||
<el-option v-for="(item, index) in unitlist" :key="index" :label="item.unitNoName" :value="item.unitNoId"></el-option>
|
||||
<div class="flex flex-items-center">
|
||||
<div style="width: 50px; text-align: right; padding-right: 10px">楼栋</div>
|
||||
<div class="flex-1"></div>
|
||||
<el-select @change="handleChangebuilding" clearable style="width: 120px" v-model="queryParams.buildingId">
|
||||
<el-option v-for="(item, index) in buildinglist" :key="index" :label="item.buildingName" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-items-center mr-20px">
|
||||
<div style="width: 80px; text-align: right; padding-right: 10px">房屋类型</div>
|
||||
<div class="flex-1">
|
||||
<el-select clearable @change="handleHouseUseChange" style="width: 140px" v-model="queryParams.houseUseTypeId">
|
||||
<el-option v-for="(item, index) in houseUseTypeList" :key="index" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
<div class="flex flex-items-center mr-20px">
|
||||
<div style="width: 50px; text-align: right; padding-right: 10px">单元</div>
|
||||
<div class="flex-1">
|
||||
<el-select @change="handleChangeUnit" clearable style="width: 120px" v-model="queryParams.unitNoId">
|
||||
<el-option v-for="(item, index) in unitlist" :key="index" :label="item.unitNoName" :value="item.unitNoId"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-items-center mr-20px">
|
||||
<div style="width: 80px; text-align: right; padding-right: 10px">业主名称</div>
|
||||
<div class="flex-1">
|
||||
<el-input clearable @clear="handleClear" v-model="queryParams.residentName" placeholder="查找业主"></el-input>
|
||||
<div class="flex flex-items-center mr-20px">
|
||||
<div style="width: 80px; text-align: right; padding-right: 10px">房屋类型</div>
|
||||
<div class="flex-1">
|
||||
<el-select clearable @change="handleHouseUseChange" style="width: 120px" v-model="queryParams.houseUseTypeId">
|
||||
<el-option v-for="(item, index) in houseUseTypeList" :key="index" :label="item.name" :value="item.id"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-items-center mr-20px">
|
||||
<div style="width: 60px; text-align: right; padding-right: 10px">房屋号</div>
|
||||
<div class="flex-1">
|
||||
<el-input clearable @clear="handleClear" style="width: 120px" v-model="queryParams.houseNo" placeholder="请输入房屋号"></el-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-items-center mr-20px">
|
||||
<div style="width: 80px; text-align: right; padding-right: 10px">业主名称</div>
|
||||
<div class="flex-1">
|
||||
<el-input clearable @clear="handleClear" style="width: 120px" v-model="queryParams.residentName" placeholder="请输入业主名称"></el-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -45,14 +53,7 @@
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
</nav>
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
@row-click="handleRowClick"
|
||||
ref="multipleTableRef"
|
||||
:data="tableData"
|
||||
border
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table row-key="houseId" v-loading="loading" ref="multipleTableRef" :data="tableData" border @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" width="55" type="index" />
|
||||
<el-table-column label="小区" align="center" prop="villageName" />
|
||||
@@ -87,7 +88,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getBuildinglistAPI, getBuildingOnly, getHouselistAPI } from '@/api/system/house';
|
||||
import { listHouseUseType } from '@/api/system/houseUse';
|
||||
import { listHouseUseType, listHouseUseType_add_house } from '@/api/system/houseUse';
|
||||
import { BillHouse, listBillHouse } from '@/api/system/SdbBill';
|
||||
import { ref, watch, nextTick, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
|
||||
|
||||
@@ -128,7 +129,7 @@ const dialog = ref({
|
||||
const queryParams = ref({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
// houseUseTypeId: null,
|
||||
houseNo: '',
|
||||
buildingId: null,
|
||||
unitNoId: null,
|
||||
houseUseTypeId: null,
|
||||
@@ -147,29 +148,35 @@ const isMultiple = computed(() => props.isMultiple ?? false);
|
||||
|
||||
// 优化 handleUserSelection,避免重复查找
|
||||
const handleUserSelection = () => {
|
||||
if (!tableData.value.length) return;
|
||||
multipleSelection.value = [];
|
||||
if (showids.value.length > 0) {
|
||||
const arr = showids.value;
|
||||
arr.forEach((item) => {
|
||||
const target = tableData.value.find((row) => row.houseId == item);
|
||||
target && multipleSelection.value.push(target);
|
||||
});
|
||||
isFetching.value = true;
|
||||
if (!multipleTableRef.value || !tableData.value.length) {
|
||||
isFetching.value = false;
|
||||
return;
|
||||
}
|
||||
// 1. 获取当前页所有行的 houseId 集合,用于快速判断
|
||||
// const currentPageIds = new Set(tableData.value.map((item) => item.houseId));
|
||||
const rowsToSelect = tableData.value.filter((row) => {
|
||||
return multipleSelection.value.some((selected) => selected.houseId === row.houseId);
|
||||
});
|
||||
multipleTableRef.value.clearSelection();
|
||||
if (rowsToSelect.length > 0) {
|
||||
nextTick(() => {
|
||||
if (multipleTableRef.value) {
|
||||
multipleTableRef.value.clearSelection();
|
||||
multipleSelection.value.forEach((item) => {
|
||||
multipleTableRef.value.toggleRowSelection(item, true);
|
||||
});
|
||||
}
|
||||
rowsToSelect.forEach((row) => {
|
||||
multipleTableRef.value?.toggleRowSelection(row, true);
|
||||
});
|
||||
isFetching.value = false;
|
||||
});
|
||||
} else {
|
||||
isFetching.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ====================== 方法 ======================
|
||||
const houseUseTypeList = ref([]);
|
||||
const isFetching = ref(false);
|
||||
// 获取列表
|
||||
function getlist() {
|
||||
isFetching.value = true; // 🔒 加锁:禁止 selection-change 更新数据
|
||||
loading.value = true;
|
||||
listBillHouse(queryParams.value)
|
||||
.then((res) => {
|
||||
@@ -182,6 +189,7 @@ function getlist() {
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
isFetching.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -225,34 +233,44 @@ function handleChangeUnit() {
|
||||
// ✅ 新增:处理 selection-change(多选专用)
|
||||
// ==============================================
|
||||
function handleSelectionChange(selection: BillHouse[]) {
|
||||
// 🛑 如果正在获取数据/恢复状态,直接返回,不要更新 multipleSelection
|
||||
if (isFetching.value) return;
|
||||
if (isMultiple.value) {
|
||||
multipleSelection.value = selection;
|
||||
}
|
||||
}
|
||||
// ==============================================
|
||||
// 最佳单选方案:点击行选中,无报错、无循环、最稳定
|
||||
// ==============================================
|
||||
// 优化 handleRowClick,避免重复
|
||||
function handleRowClick(row: BillHouse) {
|
||||
if (!multipleTableRef.value) return;
|
||||
|
||||
if (isMultiple.value) {
|
||||
const index = multipleSelection.value.findIndex((item) => item.houseId === row.houseId);
|
||||
if (index > -1) {
|
||||
// 取消选中
|
||||
multipleSelection.value.splice(index, 1);
|
||||
multipleTableRef.value?.toggleRowSelection(row, false);
|
||||
} else {
|
||||
// 选中
|
||||
multipleSelection.value.push(row);
|
||||
multipleTableRef.value?.toggleRowSelection(row, true);
|
||||
}
|
||||
// 1. 获取当前表格中所有选中行的 ID
|
||||
const currentSelectedIds = new Set(selection.map((item) => item.houseId));
|
||||
const otherPageSelections = multipleSelection.value.filter((item) => {
|
||||
// 判断 item 是否属于当前页
|
||||
const isCurrentPageItem = tableData.value.some((row) => row.houseId === item.houseId);
|
||||
if (isCurrentPageItem) {
|
||||
// 如果是当前页的数据,检查是否在当前 selection 中
|
||||
return currentSelectedIds.has(item.houseId);
|
||||
} else {
|
||||
// 如果不是当前页的数据,保留(假设是之前页选的)
|
||||
return true;
|
||||
}
|
||||
});
|
||||
// 去重合并
|
||||
const finalSelection = [...otherPageSelections, ...selection];
|
||||
// 再次去重以防万一
|
||||
const uniqueMap = new Map();
|
||||
finalSelection.forEach((item) => uniqueMap.set(item.houseId, item));
|
||||
multipleSelection.value = Array.from(uniqueMap.values());
|
||||
console.log(multipleSelection.value.map((item) => item.houseId));
|
||||
} else {
|
||||
// 【单选模式】先清空,再选中当前行
|
||||
multipleSelection.value = [];
|
||||
multipleTableRef.value.clearSelection();
|
||||
multipleSelection.value = [row];
|
||||
multipleTableRef.value.toggleRowSelection(row, true);
|
||||
// ✅ 补充:单选模式下,selection-change 触发时,直接取最新的一项
|
||||
// 这样配合 handleRowClick 的 clearSelection + toggle 就能实现完美单选
|
||||
if (selection.length > 0) {
|
||||
const lastSelected = selection[selection.length - 1];
|
||||
multipleSelection.value = [lastSelected];
|
||||
nextTick(() => {
|
||||
if (selection.length > 1) {
|
||||
multipleTableRef.value?.clearSelection();
|
||||
multipleTableRef.value?.toggleRowSelection(lastSelected, true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
multipleSelection.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
// 重置查询
|
||||
@@ -263,6 +281,7 @@ function resetQuery() {
|
||||
houseUseTypeId: null,
|
||||
buildingId: null,
|
||||
unitNoId: null,
|
||||
houseNo: '',
|
||||
residentName: ''
|
||||
};
|
||||
getlist();
|
||||
@@ -308,15 +327,20 @@ function confirm() {
|
||||
|
||||
defineExpose({
|
||||
open: (showidArray: string[] = []) => {
|
||||
multipleSelection.value = [];
|
||||
showids.value = showidArray;
|
||||
if (showidArray && showidArray.length > 0) {
|
||||
multipleSelection.value = showidArray.map(
|
||||
(id) =>
|
||||
({
|
||||
houseId: id
|
||||
}) as BillHouse
|
||||
);
|
||||
}
|
||||
getlist();
|
||||
dialog.value.dialogVisible = true;
|
||||
|
||||
listHouseUseType({
|
||||
pageNum: 1,
|
||||
pageSize: 99999
|
||||
}).then((res) => {
|
||||
houseUseTypeList.value = res.rows;
|
||||
listHouseUseType_add_house().then((res) => {
|
||||
houseUseTypeList.value = res.data;
|
||||
});
|
||||
handleChangeHouse();
|
||||
}
|
||||
|
||||
@@ -28,8 +28,7 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- 关键:去掉 @selection-change,改用 row-click 实现单选,永不报错! -->
|
||||
<el-table ref="multipleTableRef" :data="tableData" border @row-click="handleRowClick" @selection-change="handleSelectionChange">
|
||||
<el-table ref="multipleTableRef" row-key="id" :data="tableData" border @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" width="55" type="index" />
|
||||
<el-table-column label="姓名" align="center" prop="name" />
|
||||
@@ -38,6 +37,11 @@
|
||||
<dict-tag :options="com_resident_type" :value="row.type" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="与业主关系" align="center" width="95" prop="ownerRelationship">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="com_resident_relationship" :value="row.ownerRelationship" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" width="85" prop="status">
|
||||
<template #default="{ row }">
|
||||
<dict-tag :options="com_certification_status" :value="row.status" />
|
||||
@@ -45,7 +49,7 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="证件号码" align="center" prop="idCard" />
|
||||
<el-table-column label="手机号码" align="center" prop="phone" />
|
||||
<el-table-column label="房间" align="center">
|
||||
<el-table-column label="房间" show-overflow-tooltip align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.housePath }}
|
||||
</template>
|
||||
@@ -75,7 +79,7 @@
|
||||
import { listResident } from '@/api/system/resident';
|
||||
import { ResidentVO } from '@/api/system/resident/type';
|
||||
import { getHousePathById } from '@/utils/house';
|
||||
import { ref, watch, nextTick, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
|
||||
import { ref, nextTick, getCurrentInstance, ComponentInternalInstance, toRefs } from 'vue';
|
||||
import { useHouseStore } from '@/store/modules/house';
|
||||
|
||||
const houseStore = useHouseStore();
|
||||
@@ -111,7 +115,7 @@ const props = defineProps<{
|
||||
|
||||
// ✅ 手动设置默认值(替代 withDefaults)
|
||||
const isMultiple = computed(() => props.isMultiple ?? false);
|
||||
const returnvalue = computed(() => props.returnvalue ?? 'id'); // 👈 默认 userId
|
||||
const returnvalue = computed(() => props.returnvalue ?? '*'); // 👈 默认 userId
|
||||
|
||||
const { userid } = toRefs(props);
|
||||
|
||||
@@ -140,50 +144,66 @@ const total = ref(0);
|
||||
const loading = ref(false);
|
||||
|
||||
// ==============================================
|
||||
// ✅ 新增:处理 selection-change(多选专用)
|
||||
// ✅ 优化:处理 selection-change
|
||||
// ==============================================
|
||||
function handleSelectionChange(selection: tableType[]) {
|
||||
if (isMultiple.value) {
|
||||
multipleSelection.value = selection;
|
||||
}
|
||||
}
|
||||
// 多选逻辑:合并当前页选中项和之前页选中项
|
||||
const currentSelectedIds = new Set(selection.map((item) => item.id));
|
||||
|
||||
// ==============================================
|
||||
// 最佳单选方案:点击行选中,无报错、无循环、最稳定
|
||||
// ==============================================
|
||||
function handleRowClick(row) {
|
||||
if (isMultiple.value) {
|
||||
// 多选:点击行切换选中状态
|
||||
const isSelected = multipleSelection.value.some((item) => item.id === row.id);
|
||||
if (isSelected) {
|
||||
multipleSelection.value = multipleSelection.value.filter((item) => item.id !== row.id);
|
||||
multipleTableRef.value?.toggleRowSelection(row, false);
|
||||
} else {
|
||||
multipleSelection.value.push(row);
|
||||
multipleTableRef.value?.toggleRowSelection(row, true);
|
||||
}
|
||||
// 过滤掉当前页未选中的旧数据
|
||||
const otherPageSelections = multipleSelection.value.filter((item) => {
|
||||
const isCurrentPageItem = tableData.value.some((row) => row.id === item.id);
|
||||
if (isCurrentPageItem) {
|
||||
return currentSelectedIds.has(item.id);
|
||||
}
|
||||
return true; // 保留其他页的数据
|
||||
});
|
||||
|
||||
// 合并当前页新选中的数据
|
||||
const finalSelection = [...otherPageSelections, ...selection];
|
||||
|
||||
// 去重
|
||||
const uniqueMap = new Map();
|
||||
finalSelection.forEach((item) => uniqueMap.set(item.id, item));
|
||||
multipleSelection.value = Array.from(uniqueMap.values());
|
||||
} else {
|
||||
// 单选:原有逻辑保持不变
|
||||
multipleTableRef.value?.clearSelection();
|
||||
multipleTableRef.value?.toggleRowSelection(row, true);
|
||||
multipleSelection.value = [row];
|
||||
// 单选逻辑:始终只保留最后一个选中的项
|
||||
if (selection.length > 0) {
|
||||
// 获取最后选中的那一项
|
||||
const lastSelected = selection[selection.length - 1];
|
||||
multipleSelection.value = [lastSelected];
|
||||
|
||||
// 关键:如果 selection 长度大于1(比如之前选了一个,现在又选一个,el-table可能暂时上报两个),
|
||||
// 我们需要手动清除表格中其他的勾选状态,只保留最后一个
|
||||
nextTick(() => {
|
||||
if (selection.length > 1) {
|
||||
multipleTableRef.value?.clearSelection();
|
||||
multipleTableRef.value?.toggleRowSelection(lastSelected, true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
multipleSelection.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// ✅ 修改:支持多选回显
|
||||
// ==============================================
|
||||
// ✅ 优化:回显逻辑
|
||||
const handleUserSelection = () => {
|
||||
if (!userid.value || !tableData.value.length) return;
|
||||
if (!multipleTableRef.value) return;
|
||||
|
||||
// 先清空所有选择
|
||||
multipleTableRef.value.clearSelection();
|
||||
multipleSelection.value = [];
|
||||
|
||||
if (!props.userid && props.userid !== 0) return;
|
||||
|
||||
nextTick(() => {
|
||||
multipleTableRef.value?.clearSelection();
|
||||
|
||||
if (isMultiple.value && Array.isArray(userid.value)) {
|
||||
if (isMultiple.value && Array.isArray(props.userid)) {
|
||||
// 多选回显
|
||||
const selectedItems: tableType[] = [];
|
||||
userid.value.forEach((id) => {
|
||||
const findItem = tableData.value.find((item) => item.userId == id);
|
||||
props.userid.forEach((id) => {
|
||||
const findItem = tableData.value.find((item) => item.id == id || item.userId == id);
|
||||
if (findItem) {
|
||||
selectedItems.push(findItem);
|
||||
multipleTableRef.value?.toggleRowSelection(findItem, true);
|
||||
@@ -191,38 +211,43 @@ const handleUserSelection = () => {
|
||||
});
|
||||
multipleSelection.value = selectedItems;
|
||||
} else {
|
||||
// 单选回显(原有逻辑)
|
||||
const findItem = tableData.value.find((item) => item.id == userid.value);
|
||||
// 单选回显
|
||||
// 注意:props.userid 可能是单个 ID
|
||||
const targetId = Array.isArray(props.userid) ? props.userid[0] : props.userid;
|
||||
const findItem = tableData.value.find((item) => item.id == targetId || item.userId == targetId);
|
||||
|
||||
if (findItem) {
|
||||
multipleSelection.value = [findItem];
|
||||
multipleTableRef.value?.toggleRowSelection(findItem, true);
|
||||
multipleSelection.value = [findItem];
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
watch(userid, handleUserSelection, { immediate: true });
|
||||
watch(tableData, handleUserSelection);
|
||||
|
||||
// 打开选择框
|
||||
function choiceResidentFun() {
|
||||
// 初始化
|
||||
getlist();
|
||||
dialog.value.dialogVisible = true;
|
||||
}
|
||||
|
||||
// 获取列表
|
||||
function getlist() {
|
||||
loading.value = true;
|
||||
listResident(queryParams.value).then((res) => {
|
||||
tableData.value = res.rows;
|
||||
tableData.value.forEach((item) => {
|
||||
if (item.houseId !== null) {
|
||||
const houselist = getHousePathById(treedata.value, item.houseId, 'label');
|
||||
item.housePath = houselist?.join('-') || '';
|
||||
}
|
||||
listResident(queryParams.value)
|
||||
.then((res) => {
|
||||
tableData.value = res.rows;
|
||||
tableData.value.forEach((item) => {
|
||||
if (item.houseId !== null) {
|
||||
const houselist = getHousePathById(treedata.value, item.houseId, 'label');
|
||||
item.housePath = houselist?.join('-') || '';
|
||||
}
|
||||
});
|
||||
handleUserSelection();
|
||||
total.value = res.total;
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
@@ -268,6 +293,7 @@ function confirm() {
|
||||
}
|
||||
} else {
|
||||
// 单选
|
||||
console.log(multipleSelection.value[0], multipleSelection.value);
|
||||
if (returnvalue.value === '*') {
|
||||
// ✅ 返回整个对象
|
||||
emit('change', multipleSelection.value[0]);
|
||||
@@ -276,13 +302,21 @@ function confirm() {
|
||||
emit('change', multipleSelection.value[0][returnvalue.value as keyof tableType]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
emit('change', null);
|
||||
}
|
||||
}
|
||||
// 初始化
|
||||
getlist();
|
||||
|
||||
defineExpose({
|
||||
open: () => {
|
||||
open: (arr) => {
|
||||
if (arr && arr.length > 0) {
|
||||
multipleSelection.value = arr.map((id) => ({
|
||||
id: id
|
||||
}));
|
||||
} else {
|
||||
multipleSelection.value = [];
|
||||
}
|
||||
getlist();
|
||||
dialog.value.dialogVisible = true;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
<slot v-if="$slots.default" />
|
||||
|
||||
<div v-else class="residentBox" @click="choiceResidentFun">
|
||||
<div class="defaultbox" v-if="multipleSelection.length === 0">
|
||||
<div class="defaultbox" v-if="multipleSelection.length === 0 && props.username === null">
|
||||
<span>请选择</span>
|
||||
<el-icon><Search /></el-icon>
|
||||
</div>
|
||||
<div v-else class="overflow-auto overflow-hidden w-full text-ellipsis text-nowrap">
|
||||
<span>姓名:{{ multipleSelection[0]?.name }}</span>
|
||||
<span>姓名:{{ multipleSelection[0]?.nickName ?? props.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<el-button @click="resetQuery">重置</el-button>
|
||||
</div>
|
||||
</nav>
|
||||
<el-table @row-click="handleRowClick" ref="multipleTableRef" :data="tableData" border @selection-change="handleSelectionChange">
|
||||
<el-table ref="multipleTableRef" :data="tableData" border @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" width="55" type="index" />
|
||||
<el-table-column label="姓名" align="center" prop="nickName" />
|
||||
@@ -80,9 +80,13 @@ const props = defineProps({
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
username: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
returnvalue: {
|
||||
type: String,
|
||||
default: 'id'
|
||||
default: 'userId'
|
||||
},
|
||||
titleName: {
|
||||
type: String,
|
||||
@@ -121,6 +125,7 @@ const multipleTableRef = ref();
|
||||
const tableData = ref<tableType[]>([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
const isFetching = ref(false); // 加锁,防止重复触发 selection-change
|
||||
|
||||
// 存储选中项
|
||||
const multipleSelection = ref<tableType[]>([]);
|
||||
@@ -129,29 +134,31 @@ const isMultiple = computed(() => props.isMultiple ?? false);
|
||||
|
||||
// 优化 handleUserSelection,避免重复查找
|
||||
const handleUserSelection = () => {
|
||||
if (!userid.value || !tableData.value.length) return;
|
||||
|
||||
multipleSelection.value = [];
|
||||
if (userid.value.trim() !== '') {
|
||||
const arr = userid.value.split(',');
|
||||
arr.forEach((item) => {
|
||||
const target = tableData.value.find((row) => row.userId == item);
|
||||
target && multipleSelection.value.push(target);
|
||||
});
|
||||
isFetching.value = true;
|
||||
console.log('获取列表');
|
||||
if (tableData.value.length <= 0 || !multipleTableRef.value) {
|
||||
isFetching.value = false;
|
||||
return;
|
||||
}
|
||||
const rowsToSelect = tableData.value.filter((row) => {
|
||||
return multipleSelection.value.some((selected) => selected.userId === row.userId);
|
||||
});
|
||||
multipleTableRef.value.clearSelection();
|
||||
if (rowsToSelect.length > 0) {
|
||||
nextTick(() => {
|
||||
if (multipleTableRef.value) {
|
||||
multipleTableRef.value.clearSelection();
|
||||
multipleSelection.value.forEach((item) => {
|
||||
multipleTableRef.value.toggleRowSelection(item, true);
|
||||
});
|
||||
}
|
||||
rowsToSelect.forEach((row) => {
|
||||
multipleTableRef.value?.toggleRowSelection(row, true);
|
||||
});
|
||||
isFetching.value = false;
|
||||
});
|
||||
} else {
|
||||
isFetching.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听
|
||||
// 监听优化:只在 userid 变化时触发
|
||||
// watch(userid, handleUserSelection, { immediate: true });
|
||||
watch(userid, handleUserSelection, { immediate: true });
|
||||
// watch(tableData, () => {
|
||||
// if (userid.value) handleUserSelection(); // 仅在有 userid 时重新处理
|
||||
// });
|
||||
@@ -160,6 +167,17 @@ const handleUserSelection = () => {
|
||||
// 打开选择框
|
||||
function choiceResidentFun() {
|
||||
dialog.value.dialogVisible = true;
|
||||
multipleSelection.value = []; // 打开时清空之前的选择
|
||||
if (typeof props.userid === 'string' && props.userid.trim() !== '') {
|
||||
multipleSelection.value = [
|
||||
{
|
||||
userId: props.userid
|
||||
} as ResidentVO
|
||||
];
|
||||
} else if (Array.isArray(props.userid)) {
|
||||
multipleSelection.value = props.userid.map((id) => ({ userId: id })) as tableType[];
|
||||
}
|
||||
getlist();
|
||||
}
|
||||
|
||||
// 获取列表
|
||||
@@ -167,8 +185,9 @@ function getlist() {
|
||||
loading.value = true;
|
||||
getUserByRoleKey(userType.value)
|
||||
.then((res) => {
|
||||
tableData.value = res.data;
|
||||
tableData.value = res.rows;
|
||||
total.value = res.total;
|
||||
handleUserSelection();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('获取用户列表失败:', error);
|
||||
@@ -177,40 +196,45 @@ function getlist() {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// ✅ 新增:处理 selection-change(多选专用)
|
||||
// ==============================================
|
||||
function handleSelectionChange(selection: tableType[]) {
|
||||
if (isFetching.value) return;
|
||||
if (isMultiple.value) {
|
||||
multipleSelection.value = selection;
|
||||
}
|
||||
}
|
||||
// ==============================================
|
||||
// 最佳单选方案:点击行选中,无报错、无循环、最稳定
|
||||
// ==============================================
|
||||
// 优化 handleRowClick,避免重复
|
||||
function handleRowClick(row: tableType) {
|
||||
if (!multipleTableRef.value) return;
|
||||
|
||||
if (isMultiple.value) {
|
||||
const index = multipleSelection.value.findIndex((item) => item.id === row.id);
|
||||
if (index > -1) {
|
||||
// 取消选中
|
||||
multipleSelection.value.splice(index, 1);
|
||||
multipleTableRef.value?.toggleRowSelection(row, false);
|
||||
} else {
|
||||
// 选中
|
||||
multipleSelection.value.push(row);
|
||||
multipleTableRef.value?.toggleRowSelection(row, true);
|
||||
}
|
||||
const currentSelectedIds = new Set(selection.map((item) => item.id));
|
||||
const otherPageSelections = multipleSelection.value.filter((item) => {
|
||||
const isCurrentPageItem = tableData.value.some((row) => row.id === item.id);
|
||||
if (isCurrentPageItem) {
|
||||
return currentSelectedIds.has(item.id);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
// 3. 合并:其他页保留的 + 当前页新选中的
|
||||
const finalSelection = [...otherPageSelections, ...selection];
|
||||
// 4. 去重(以防万一)
|
||||
const uniqueMap = new Map();
|
||||
finalSelection.forEach((item) => uniqueMap.set(item.id, item));
|
||||
multipleSelection.value = Array.from(uniqueMap.values());
|
||||
console.log(multipleSelection.value.map((item) => item.id));
|
||||
} else {
|
||||
// 【单选模式】先清空,再选中当前行
|
||||
multipleSelection.value = [];
|
||||
multipleTableRef.value.clearSelection();
|
||||
multipleSelection.value = [row];
|
||||
multipleTableRef.value.toggleRowSelection(row, true);
|
||||
// --- 单选逻辑 ---
|
||||
if (selection.length > 0) {
|
||||
const lastSelected = selection[selection.length - 1];
|
||||
multipleSelection.value = [lastSelected];
|
||||
nextTick(() => {
|
||||
if (selection.length > 1) {
|
||||
multipleTableRef.value?.clearSelection();
|
||||
multipleTableRef.value?.toggleRowSelection(lastSelected, true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
multipleSelection.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
console.log('handleSelectionChange', selection);
|
||||
}
|
||||
// 重置查询
|
||||
function resetQuery() {
|
||||
@@ -250,23 +274,21 @@ function confirm() {
|
||||
// 单选
|
||||
if (returnvalue.value === '*') {
|
||||
// ✅ 返回整个对象
|
||||
emit('change', multipleSelection.value[0]);
|
||||
emit('change', multipleSelection.value[multipleSelection.value.length - 1]);
|
||||
} else {
|
||||
// 返回指定字段
|
||||
emit('change', multipleSelection.value[0][returnvalue.value as keyof tableType]);
|
||||
emit('change', multipleSelection.value[multipleSelection.value.length - 1][returnvalue.value as keyof tableType]);
|
||||
}
|
||||
}
|
||||
|
||||
multipleSelection.value = [];
|
||||
multipleTableRef.value.clearSelection();
|
||||
}
|
||||
// 初始化
|
||||
getlist();
|
||||
onMounted(() => {
|
||||
// 初始化
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
open: () => {
|
||||
dialog.value.dialogVisible = true;
|
||||
handleUserSelection();
|
||||
getlist();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<slot name="default" />
|
||||
<el-dialog append-to-body v-model="dialog.dialogVisible" :title="dialog.dialogTitle" width="1050">
|
||||
<el-table @row-click="handleRowClick" ref="multipleTableRef" :data="tableData" border @selection-change="handleSelectionChange">
|
||||
<el-table ref="multipleTableRef" :data="tableData" border @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" align="center" width="55" type="index" />
|
||||
<el-table-column label="业主" align="center">
|
||||
@@ -49,7 +49,7 @@ const { sys_user_sex } = toRefs<any>(proxy?.useDict('sys_user_sex'));
|
||||
const props = defineProps({
|
||||
returnvalue: {
|
||||
type: String,
|
||||
default: 'houseId'
|
||||
default: 'residentId'
|
||||
},
|
||||
titleName: {
|
||||
type: String,
|
||||
@@ -84,7 +84,7 @@ const multipleTableRef = ref();
|
||||
const tableData = ref<BillUserlist[]>([]);
|
||||
const total = ref(0);
|
||||
const loading = ref(false);
|
||||
|
||||
const isFetching = ref(false);
|
||||
// 存储选中项
|
||||
const multipleSelection = ref<BillUserlist[]>([]);
|
||||
|
||||
@@ -92,29 +92,32 @@ const isMultiple = computed(() => props.isMultiple ?? false);
|
||||
|
||||
// 优化 handleUserSelection,避免重复查找
|
||||
const handleUserSelection = () => {
|
||||
if (!tableData.value.length) return;
|
||||
|
||||
multipleSelection.value = [];
|
||||
const arr = userid.value;
|
||||
if (!arr.length) return;
|
||||
arr.forEach((item) => {
|
||||
const target = tableData.value.find((row) => row.residentId == item);
|
||||
target && multipleSelection.value.push(target);
|
||||
isFetching.value = true;
|
||||
if (!multipleTableRef.value || !tableData.value.length) {
|
||||
isFetching.value = false;
|
||||
return;
|
||||
}
|
||||
const rowsToSelect = tableData.value.filter((row) => {
|
||||
return multipleSelection.value.some((selected) => selected.residentId === row.residentId);
|
||||
});
|
||||
nextTick(() => {
|
||||
if (multipleTableRef.value) {
|
||||
multipleTableRef.value.clearSelection();
|
||||
multipleSelection.value.forEach((item) => {
|
||||
multipleTableRef.value.toggleRowSelection(item, true);
|
||||
multipleTableRef.value.clearSelection();
|
||||
if (rowsToSelect.length > 0) {
|
||||
nextTick(() => {
|
||||
rowsToSelect.forEach((row) => {
|
||||
multipleTableRef.value?.toggleRowSelection(row, true);
|
||||
});
|
||||
}
|
||||
});
|
||||
isFetching.value = false;
|
||||
});
|
||||
} else {
|
||||
isFetching.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// ====================== 方法 ======================
|
||||
|
||||
// 获取列表
|
||||
function getlist() {
|
||||
isFetching.value = true; // 加锁
|
||||
loading.value = true;
|
||||
queryResidentList_holder(queryParams.value)
|
||||
.then((res) => {
|
||||
@@ -127,6 +130,7 @@ function getlist() {
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
isFetching.value = false; // 加锁
|
||||
});
|
||||
}
|
||||
|
||||
@@ -152,8 +156,31 @@ function handleChangebuilding(val: string) {
|
||||
// ✅ 新增:处理 selection-change(多选专用)
|
||||
// ==============================================
|
||||
function handleSelectionChange(selection: BillUserlist[]) {
|
||||
if (isFetching.value) return;
|
||||
if (isMultiple.value) {
|
||||
multipleSelection.value = selection;
|
||||
const currentSelectedIds = new Set(selection.map((item) => item.residentId));
|
||||
const otherPageSelections = multipleSelection.value.filter((item) => {
|
||||
const isCurrentPageItem = tableData.value.some((row) => row.residentId === item.residentId);
|
||||
if (isCurrentPageItem) {
|
||||
return currentSelectedIds.has(item.residentId);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
// 3. 合并:其他页保留的 + 当前页新选中的
|
||||
const finalSelection = [...otherPageSelections, ...selection];
|
||||
// 4. 去重(以防万一)
|
||||
const uniqueMap = new Map();
|
||||
finalSelection.forEach((item) => uniqueMap.set(item.residentId, item));
|
||||
multipleSelection.value = Array.from(uniqueMap.values());
|
||||
console.log(multipleSelection.value.map((item) => item.residentId));
|
||||
} else {
|
||||
// --- 单选逻辑 ---
|
||||
if (selection.length > 0) {
|
||||
multipleSelection.value = [selection[selection.length - 1]];
|
||||
} else {
|
||||
multipleSelection.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
// ==============================================
|
||||
@@ -232,6 +259,17 @@ function confirm() {
|
||||
defineExpose({
|
||||
open: (arr: string[] = []) => {
|
||||
userid.value = arr;
|
||||
if (arr && arr.length > 0) {
|
||||
multipleSelection.value = arr.map(
|
||||
(id) =>
|
||||
({
|
||||
residentId: id
|
||||
}) as BillUserlist
|
||||
);
|
||||
} else {
|
||||
multipleSelection.value = [];
|
||||
}
|
||||
|
||||
getlist();
|
||||
dialog.value.dialogVisible = true;
|
||||
}
|
||||
|
||||
@@ -22,17 +22,24 @@
|
||||
@paste="onPaste"
|
||||
@focus="activeIndex = index"
|
||||
@blur="onBlur"
|
||||
@compositionend="onCompositionEnd(index)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 错误提示(和 Element Plus 原生完全一致) -->
|
||||
<!-- 错误提示 -->
|
||||
<transition name="el-fade-in-linear">
|
||||
<div v-if="hasError && errorMessage" class="plate-error">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { LicensePlateValidator } from '@/utils/Reg';
|
||||
import { ref, watch, nextTick } from 'vue';
|
||||
import { LicensePlateValidator } from '@/utils/Reg';
|
||||
|
||||
// 省份简称
|
||||
const PROVINCES = [
|
||||
'京',
|
||||
'津',
|
||||
@@ -66,7 +73,12 @@ const PROVINCES = [
|
||||
'宁',
|
||||
'琼'
|
||||
];
|
||||
const VALID_CHARS = /^[A-HJ-NP-Z0-9]$/i;
|
||||
|
||||
// 合法基础字符:字母 (排除I,O) 和 数字
|
||||
const VALID_CHAR_REGEX = /^[A-HJ-NP-Z0-9]$/i;
|
||||
|
||||
// 车牌特殊结尾字符
|
||||
const SPECIAL_CHARS = ['警', '学', '挂', '临', '超', '港', '澳'];
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: string;
|
||||
@@ -85,22 +97,23 @@ const chars = ref<string[]>(Array(8).fill(''));
|
||||
const hasError = ref(false);
|
||||
const errorMessage = ref('');
|
||||
|
||||
// 外部值同步
|
||||
// 外部值同步到内部
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
const currentValue = chars.value.join('').trim();
|
||||
if (val !== currentValue) {
|
||||
const cleanVal = (val || '').toUpperCase().trim();
|
||||
const currentJoined = chars.value.join('').trim();
|
||||
|
||||
if (cleanVal !== currentJoined) {
|
||||
const newChars = Array(8).fill('');
|
||||
if (val) {
|
||||
val
|
||||
.toUpperCase()
|
||||
.split('')
|
||||
.forEach((c, i) => {
|
||||
if (i < 8) newChars[i] = c;
|
||||
});
|
||||
if (cleanVal) {
|
||||
const arr = cleanVal.split('');
|
||||
for (let i = 0; i < Math.min(arr.length, 8); i++) {
|
||||
newChars[i] = arr[i];
|
||||
}
|
||||
}
|
||||
chars.value = newChars;
|
||||
if (cleanVal) clearError();
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -109,43 +122,76 @@ watch(
|
||||
// 内部值同步到外部
|
||||
watch(
|
||||
chars,
|
||||
() => {
|
||||
const value = chars.value.join('').trim();
|
||||
(newChars) => {
|
||||
const value = newChars.join('').trim();
|
||||
emit('update:modelValue', value);
|
||||
emit('change', value);
|
||||
// 输入时清除错误
|
||||
clearError();
|
||||
|
||||
if (hasError.value) {
|
||||
clearError();
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
function onInput(index: number) {
|
||||
const char = chars.value[index].toUpperCase();
|
||||
let char = chars.value[index];
|
||||
|
||||
if (index === 0 && !PROVINCES.includes(char)) {
|
||||
chars.value[index] = '';
|
||||
return;
|
||||
if (!char) return;
|
||||
|
||||
// 统一转大写 (汉字不受影响)
|
||||
char = char.toUpperCase();
|
||||
|
||||
let isValid = false;
|
||||
|
||||
// 1. 第一位:必须是省份
|
||||
if (index === 0) {
|
||||
if (PROVINCES.includes(char)) {
|
||||
isValid = true;
|
||||
}
|
||||
}
|
||||
// 2. 特殊字符处理 (警/学/挂/临/超/港/澳)
|
||||
else if (SPECIAL_CHARS.includes(char)) {
|
||||
// 特殊字符通常出现在最后一位,或者倒数第二位(如临时车牌的"临"可能在最后,也可能在倒数第二?不,通常是最后)
|
||||
// 为了用户体验,允许在任何非第一位的位置输入,但最终校验会判断位置是否正确
|
||||
isValid = true;
|
||||
}
|
||||
// 3. 常规字符:字母或数字
|
||||
else {
|
||||
if (VALID_CHAR_REGEX.test(char)) {
|
||||
isValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (index > 0 && !VALID_CHARS.test(char)) {
|
||||
if (!isValid) {
|
||||
chars.value[index] = '';
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
chars.value[index] = char;
|
||||
|
||||
chars.value[index] = char;
|
||||
|
||||
if (char && index < 7) {
|
||||
nextTick(() => inputRefs.value[index + 1]?.focus());
|
||||
// 如果输入的是特殊字符,通常意味着车牌结束,不再自动跳转
|
||||
// 如果输入的是常规字符,且不是最后一格,则跳转
|
||||
if (!SPECIAL_CHARS.includes(char) && char && index < 7) {
|
||||
nextTick(() => {
|
||||
inputRefs.value[index + 1]?.focus();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onCompositionEnd(index: number) {
|
||||
onInput(index);
|
||||
}
|
||||
|
||||
function onKeydown(e: KeyboardEvent, index: number) {
|
||||
if (e.key === 'Backspace' && !chars.value[index] && index > 0) {
|
||||
e.preventDefault();
|
||||
nextTick(() => {
|
||||
inputRefs.value[index - 1]?.focus();
|
||||
inputRefs.value[index - 1]?.select();
|
||||
});
|
||||
if (e.key === 'Backspace') {
|
||||
if (!chars.value[index] && index > 0) {
|
||||
e.preventDefault();
|
||||
nextTick(() => {
|
||||
inputRefs.value[index - 1]?.focus();
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'ArrowLeft' && index > 0) {
|
||||
@@ -160,20 +206,37 @@ function onKeydown(e: KeyboardEvent, index: number) {
|
||||
|
||||
function onPaste(e: ClipboardEvent) {
|
||||
e.preventDefault();
|
||||
const text = e.clipboardData?.getData('text')?.trim().toUpperCase() || '';
|
||||
const text = e.clipboardData?.getData('text')?.toUpperCase().trim() || '';
|
||||
if (!text) return;
|
||||
|
||||
const newChars = Array(8).fill('');
|
||||
text.split('').forEach((c, i) => {
|
||||
if (i >= 8) return;
|
||||
if (i === 0 && PROVINCES.includes(c)) newChars[i] = c;
|
||||
if (i > 0 && VALID_CHARS.test(c)) newChars[i] = c;
|
||||
const filledChars = [...chars.value];
|
||||
let insertPos = activeIndex.value >= 0 ? activeIndex.value : 0;
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (insertPos >= 8) break;
|
||||
const char = text[i];
|
||||
|
||||
let canInsert = false;
|
||||
if (insertPos === 0) {
|
||||
if (PROVINCES.includes(char)) canInsert = true;
|
||||
} else if (SPECIAL_CHARS.includes(char)) {
|
||||
canInsert = true;
|
||||
} else {
|
||||
if (VALID_CHAR_REGEX.test(char)) canInsert = true;
|
||||
}
|
||||
|
||||
if (canInsert) {
|
||||
filledChars[insertPos] = char;
|
||||
insertPos++;
|
||||
}
|
||||
}
|
||||
|
||||
chars.value = filledChars;
|
||||
|
||||
nextTick(() => {
|
||||
const focusIdx = Math.min(insertPos, 7);
|
||||
inputRefs.value[focusIdx]?.focus();
|
||||
});
|
||||
|
||||
chars.value = newChars;
|
||||
|
||||
const lastIndex = Math.min(text.length - 1, 6);
|
||||
nextTick(() => inputRefs.value[lastIndex + 1]?.focus());
|
||||
}
|
||||
|
||||
function onBlur() {
|
||||
@@ -181,49 +244,50 @@ function onBlur() {
|
||||
const hasFocus = inputRefs.value.some((input) => document.activeElement === input);
|
||||
if (!hasFocus) {
|
||||
activeIndex.value = -1;
|
||||
validate();
|
||||
}
|
||||
}, 10);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// ✅ 手动验证方法(核心)
|
||||
function validate(): { valid: boolean; message?: string } {
|
||||
const value = chars.value.join('').trim();
|
||||
|
||||
// 必填验证
|
||||
if (props.required && !value) {
|
||||
hasError.value = true;
|
||||
errorMessage.value = '请输入车牌号';
|
||||
return { valid: false, message: errorMessage.value };
|
||||
setError('请输入车牌号');
|
||||
return { valid: false, message: '请输入车牌号' };
|
||||
}
|
||||
|
||||
// 格式验证
|
||||
if (value && !isValidPlate(value)) {
|
||||
hasError.value = true;
|
||||
errorMessage.value = '请输入正确的车牌号格式';
|
||||
return { valid: false, message: errorMessage.value };
|
||||
if (!value) {
|
||||
clearError();
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
if (!LicensePlateValidator.isValidPlate(value)) {
|
||||
setError('车牌号格式不正确');
|
||||
return { valid: false, message: '车牌号格式不正确' };
|
||||
}
|
||||
|
||||
// 验证通过
|
||||
clearError();
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// 车牌格式验证(内置,不依赖外部工具)
|
||||
function isValidPlate(plate: string): boolean {
|
||||
return LicensePlateValidator.isValidPlate(plate);
|
||||
function setError(msg: string) {
|
||||
hasError.value = true;
|
||||
errorMessage.value = msg;
|
||||
}
|
||||
|
||||
// 清除错误
|
||||
function clearError() {
|
||||
hasError.value = false;
|
||||
errorMessage.value = '';
|
||||
}
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
focus: () => nextTick(() => inputRefs.value[0]?.focus()),
|
||||
clear: () => {
|
||||
chars.value = Array(8).fill('');
|
||||
clearError();
|
||||
emit('update:modelValue', '');
|
||||
emit('change', '');
|
||||
},
|
||||
validate,
|
||||
clearError
|
||||
@@ -235,6 +299,7 @@ defineExpose({
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.plate_line {
|
||||
@@ -242,15 +307,17 @@ defineExpose({
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.plate-item {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.plate-item.active {
|
||||
@@ -265,6 +332,7 @@ defineExpose({
|
||||
.plate-item.disabled {
|
||||
background-color: #f5f7fa;
|
||||
cursor: not-allowed;
|
||||
color: #a8abb2;
|
||||
}
|
||||
|
||||
.plate-item input {
|
||||
@@ -272,10 +340,13 @@ defineExpose({
|
||||
height: 100%;
|
||||
border: none;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
background: transparent;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.plate-error {
|
||||
@@ -283,5 +354,6 @@ defineExpose({
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
margin-top: 2px;
|
||||
min-height: 18px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user