Restore lightweight form data selectors

This commit is contained in:
2026-06-09 23:41:12 +08:00
parent d48db4aad4
commit ce5a34e3b0
5 changed files with 1055 additions and 31 deletions
@@ -1,23 +1,492 @@
<script lang="ts" setup>
defineOptions({ name: 'FormSelector' });
import type { FormSelectorEmits, FormSelectorProps } from './types';
defineProps<{
disabled?: boolean;
modelValue?: unknown;
placeholder?: string;
}>();
import { computed, ref, watch } from 'vue';
const emit = defineEmits<{
'update:modelValue': [value: unknown];
}>();
import { Table2 } from '@vben/icons';
import { $t } from '@vben/locales';
function clearValue() {
emit('update:modelValue', undefined);
import {
ElButton,
ElCheckbox,
ElEmpty,
ElInput,
ElOption,
ElPagination,
ElRadio,
ElSelect,
ElTable,
ElTableColumn,
} from 'element-plus';
import { requestClient } from '#/api/request';
import { ZqDialog } from '#/components/zq-dialog';
defineOptions({
name: 'FormSelector',
inheritAttrs: false,
});
const props = withDefaults(defineProps<FormSelectorProps>(), {
multiple: false,
collapseTags: false,
maxCollapseTags: 1,
placeholder: () => $t('form-design.attribute.clickToSelect'),
disabled: false,
clearable: true,
dialogTitle: () => $t('form-design.attribute.selectData'),
dialogWidth: '1200px',
valueField: 'id',
labelField: 'name',
expandMultipleToRows: false,
externalSelectedValues: () => [],
initialFilters: undefined,
});
const emit = defineEmits<FormSelectorEmits>();
const dialogVisible = ref(false);
const loading = ref(false);
const labelsLoading = ref(false);
const tableData = ref<Array<Record<string, any>>>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(10);
const searchKeyword = ref('');
const selectedValues = ref<Set<string>>(new Set());
const selectedItemsMap = ref<Map<string, Record<string, any>>>(new Map());
function normalizeValues(value: null | string | string[] | undefined) {
if (!value) return [];
return Array.isArray(value) ? value.map(String) : [String(value)];
}
function toStringArray(value: unknown) {
if (!value) return [];
return Array.isArray(value) ? value.map(String) : [String(value)];
}
function getItemLabel(item: undefined | Record<string, any>, value: string) {
if (!item) return value;
return (
item[props.labelField] ??
item.name ??
item.title ??
item.label ??
item.text ??
value
);
}
function pickList(response: any) {
return Array.isArray(response)
? response
: response?.items || response?.list || response?.data || [];
}
function normalizeFilters() {
const params: Record<string, any> = {};
for (const [field, filter] of Object.entries(props.initialFilters || {})) {
if (filter?.value === undefined || filter.value === null || filter.value === '') {
continue;
}
const op = filter.type || 'eq';
params[op === 'eq' ? field : `${field}__${op}`] = filter.value;
}
return params;
}
async function loadInitialLabels(values: string[]) {
if (!props.formCode || values.length === 0) return;
try {
const response = await requestClient.get(
`/api/online_dev/form-data/${props.formCode}/list`,
{
params: {
...normalizeFilters(),
[`${props.valueField}__in`]: values.join(','),
pageSize: values.length,
},
},
);
for (const item of pickList(response)) {
selectedItemsMap.value.set(String(item[props.valueField]), item);
}
} catch (error) {
console.error('load form selector labels failed:', error);
}
}
function emitSelectItem() {
const items = [...selectedValues.value]
.map((value) => selectedItemsMap.value.get(value))
.filter(Boolean);
emit('select-item', props.multiple ? items : items[0]);
}
watch(
() => props.modelValue,
async (value) => {
const values = normalizeValues(value);
selectedValues.value = new Set(values);
const missingValues = values.filter((v) => !selectedItemsMap.value.has(v));
if (missingValues.length > 0) {
labelsLoading.value = true;
await loadInitialLabels(missingValues);
labelsLoading.value = false;
if (!props.expandMultipleToRows) emitSelectItem();
}
},
{ immediate: true },
);
watch(
() => props.formCode,
async () => {
const values = normalizeValues(props.modelValue);
const missingValues = values.filter((v) => !selectedItemsMap.value.has(v));
if (missingValues.length > 0) {
labelsLoading.value = true;
await loadInitialLabels(missingValues);
labelsLoading.value = false;
if (!props.expandMultipleToRows) emitSelectItem();
}
},
);
const selectValue = computed(() => {
if (labelsLoading.value) return props.multiple ? [] : '';
if (props.expandMultipleToRows) {
const values = normalizeValues(props.modelValue);
return props.multiple ? values : values[0] || '';
}
const values = [...selectedValues.value];
return props.multiple ? values : values[0] || '';
});
const selectOptions = computed(() => {
const values = props.expandMultipleToRows
? normalizeValues(props.modelValue)
: [...selectedValues.value];
return values.map((value) => ({
value,
label: getItemLabel(selectedItemsMap.value.get(value), value),
}));
});
async function loadData() {
if (!props.formCode) {
tableData.value = [];
total.value = 0;
return;
}
loading.value = true;
try {
const params: Record<string, any> = {
...normalizeFilters(),
page: currentPage.value,
pageSize: pageSize.value,
};
if (searchKeyword.value) {
params[`${props.labelField}__like`] = searchKeyword.value;
}
const response = await requestClient.get(
`/api/online_dev/form-data/${props.formCode}/list`,
{ params },
);
const rows = pickList(response);
tableData.value = rows;
total.value = Number(response?.total ?? rows.length);
for (const row of rows) {
const value = String(row[props.valueField]);
if (selectedValues.value.has(value)) selectedItemsMap.value.set(value, row);
}
} catch (error) {
console.error('load form selector data failed:', error);
tableData.value = [];
total.value = 0;
} finally {
loading.value = false;
}
}
function openDialog() {
if (props.disabled) return;
if (props.expandMultipleToRows) {
for (const value of toStringArray(props.externalSelectedValues)) {
selectedValues.value.add(value);
}
selectedValues.value = new Set(selectedValues.value);
}
dialogVisible.value = true;
loadData();
}
function handleSearch() {
currentPage.value = 1;
loadData();
}
function handlePageChange(page: number) {
currentPage.value = page;
loadData();
}
function handleSizeChange(size: number) {
pageSize.value = size;
currentPage.value = 1;
loadData();
}
function getRowValue(row: Record<string, any>) {
return String(row[props.valueField]);
}
function isRowSelected(row: Record<string, any>) {
return selectedValues.value.has(getRowValue(row));
}
function toggleRow(row: Record<string, any>, checked?: boolean | number | string) {
const value = getRowValue(row);
if (props.multiple) {
const nextChecked = checked ?? !selectedValues.value.has(value);
if (nextChecked) {
selectedValues.value.add(value);
selectedItemsMap.value.set(value, row);
} else {
selectedValues.value.delete(value);
selectedItemsMap.value.delete(value);
}
selectedValues.value = new Set(selectedValues.value);
return;
}
selectedValues.value = new Set([value]);
selectedItemsMap.value.clear();
selectedItemsMap.value.set(value, row);
}
const isAllSelected = computed(
() =>
tableData.value.length > 0 &&
tableData.value.every((row) => isRowSelected(row)),
);
const isIndeterminate = computed(() => {
const selectedCount = tableData.value.filter((row) => isRowSelected(row)).length;
return selectedCount > 0 && selectedCount < tableData.value.length;
});
function handleSelectAll(checked: boolean | number | string) {
for (const row of tableData.value) toggleRow(row, checked);
}
function handleConfirm() {
const values = [...selectedValues.value];
const result = props.multiple ? values : values[0] || null;
emit('update:modelValue', result);
emit('change', result);
emitSelectItem();
dialogVisible.value = false;
}
function handleClear() {
selectedValues.value = new Set();
selectedItemsMap.value.clear();
emit('update:modelValue', props.multiple ? [] : null);
emit('change', props.multiple ? [] : null);
emit('select-item', undefined);
}
function handleRemoveTag(value: string) {
selectedValues.value.delete(value);
selectedItemsMap.value.delete(value);
selectedValues.value = new Set(selectedValues.value);
const values = [...selectedValues.value];
const result = props.multiple ? values : values[0] || null;
emit('update:modelValue', result);
emit('change', result);
emitSelectItem();
}
</script>
<template>
<ElButton :disabled="disabled" plain @click="clearValue">
{{ placeholder || 'Form selector unavailable' }}
</ElButton>
<div class="form-selector">
<ElSelect
:model-value="selectValue"
:multiple="multiple"
:placeholder="labelsLoading ? $t('common.loading') : placeholder"
:disabled="disabled"
:clearable="clearable"
:loading="labelsLoading"
:collapse-tags="collapseTags"
:collapse-tags-tooltip="collapseTags"
:max-collapse-tags="maxCollapseTags"
:suffix-icon="Table2"
class="w-full"
popper-class="form-selector-popper-hidden"
@click="openDialog"
@clear="handleClear"
@remove-tag="handleRemoveTag"
>
<ElOption
v-for="item in selectOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
<ZqDialog
v-model="dialogVisible"
:title="dialogTitle"
:width="dialogWidth"
append-to-body
destroy-on-close
:show-fullscreen-button="false"
class="form-selector-dialog h-[90%]"
>
<div v-if="formCode" class="form-selector-content flex h-[650px] flex-col">
<div class="mb-4 flex shrink-0 items-center gap-2">
<ElInput
v-model="searchKeyword"
:placeholder="$t('form-design.search')"
clearable
class="w-64"
@change="handleSearch"
@keyup.enter="handleSearch"
/>
<ElButton type="primary" @click="handleSearch">
{{ $t('common.search') }}
</ElButton>
</div>
<div class="min-h-0 flex-1">
<ElTable
v-loading="loading"
:data="tableData"
border
stripe
height="100%"
:row-class-name="
({ row }: any) => (isRowSelected(row) ? 'selected-row' : '')
"
style="width: 100%"
@row-click="toggleRow"
>
<ElTableColumn v-if="!multiple" width="55" align="center">
<template #default="{ row }">
<ElRadio
:model-value="isRowSelected(row) ? getRowValue(row) : ''"
:value="getRowValue(row)"
@click.stop
@change="toggleRow(row)"
>
<span></span>
</ElRadio>
</template>
</ElTableColumn>
<ElTableColumn v-else width="55" align="center">
<template #header>
<ElCheckbox
:model-value="isAllSelected"
:indeterminate="isIndeterminate"
@change="handleSelectAll"
/>
</template>
<template #default="{ row }">
<ElCheckbox
:model-value="isRowSelected(row)"
@click.stop
@change="toggleRow(row, $event)"
/>
</template>
</ElTableColumn>
<ElTableColumn :prop="labelField" :label="$t('form-design.attribute.nodeLabel')" />
<ElTableColumn :prop="valueField" :label="$t('form-design.attribute.nodeValue')" width="180" />
<template #empty>
<ElEmpty :description="$t('common.noData')" />
</template>
</ElTable>
</div>
</div>
<div v-else class="no-form-code">
{{ $t('form-design.attribute.selectFormFirst') }}
</div>
<template #footer>
<div class="flex w-full items-center">
<div class="flex-shrink-0">
<ElPagination
v-if="formCode"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
small
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
<div class="flex flex-1 justify-center text-sm text-[var(--el-text-color-secondary)]">
{{
$t('form-design.attribute.selectedCount', {
count: selectedValues.size,
})
}}
</div>
<div class="flex flex-shrink-0 gap-2">
<ElButton @click="dialogVisible = false">
{{ $t('common.cancel') }}
</ElButton>
<ElButton type="primary" @click="handleConfirm">
{{ $t('common.ok') }}
</ElButton>
</div>
</div>
</template>
</ZqDialog>
</div>
</template>
<style scoped>
.form-selector {
width: 100%;
}
.form-selector-content :deep(.selected-row) {
background-color: var(--el-color-primary-light-9) !important;
}
.form-selector-content :deep(.el-table__row) {
cursor: pointer;
}
.no-form-code {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
color: var(--el-text-color-secondary);
}
</style>
<style>
.form-selector-popper-hidden {
display: none !important;
}
.form-selector-dialog .el-dialog__body {
padding: 16px;
}
</style>
@@ -1,5 +1,23 @@
export interface FormSelectorProps {
disabled?: boolean;
modelValue?: null | string | string[];
formCode?: string;
valueField?: string;
labelField?: string;
multiple?: boolean;
collapseTags?: boolean;
maxCollapseTags?: number;
placeholder?: string;
disabled?: boolean;
clearable?: boolean;
dialogTitle?: string;
dialogWidth?: string;
expandMultipleToRows?: boolean;
externalSelectedValues?: string[];
initialFilters?: Record<string, { type: string; value: any }>;
}
export interface FormSelectorEmits {
(e: 'update:modelValue', value: null | string | string[]): void;
(e: 'change', value: null | string | string[]): void;
(e: 'select-item', item: any): void;
}
@@ -1,23 +1,525 @@
<script lang="ts" setup>
defineOptions({ name: 'TableSelector' });
import type {
TableSelectorColumn,
TableSelectorEmits,
TableSelectorProps,
} from './types';
defineProps<{
disabled?: boolean;
modelValue?: unknown;
placeholder?: string;
}>();
import { computed, ref, watch } from 'vue';
const emit = defineEmits<{
'update:modelValue': [value: unknown];
}>();
import { Table2 } from '@vben/icons';
import { $t } from '@vben/locales';
function clearValue() {
emit('update:modelValue', undefined);
import {
ElButton,
ElCheckbox,
ElEmpty,
ElInput,
ElOption,
ElPagination,
ElRadio,
ElSelect,
ElTable,
ElTableColumn,
} from 'element-plus';
import { requestClient } from '#/api/request';
import { ZqDialog } from '#/components/zq-dialog';
defineOptions({
name: 'TableSelector',
inheritAttrs: false,
});
const props = withDefaults(defineProps<TableSelectorProps>(), {
multiple: false,
placeholder: () => $t('form-design.attribute.clickToSelect'),
disabled: false,
clearable: true,
dialogTitle: () => $t('form-design.attribute.selectData'),
dialogWidth: '800px',
columns: () => [],
labelField: 'label',
valueField: 'value',
dataSourceType: 'static',
apiMethod: 'GET',
searchFields: () => [],
collapseTags: false,
options: () => [],
});
const emit = defineEmits<TableSelectorEmits>();
const dialogVisible = ref(false);
const loading = ref(false);
const tableData = ref<Array<Record<string, any>>>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(10);
const searchKeyword = ref('');
const selectedValues = ref<Set<string>>(new Set());
const selectedItemsMap = ref<Map<string, Record<string, any>>>(new Map());
const labelsLoading = ref(false);
function normalizeValues(value: undefined | string | string[]) {
if (!value) return [];
return Array.isArray(value) ? value.map(String) : [String(value)];
}
function pickList(response: any) {
return Array.isArray(response)
? response
: response?.items || response?.list || response?.data || [];
}
function pickTotal(response: any, fallback: number) {
return Number(response?.total ?? response?.count ?? fallback);
}
function getItemLabel(item: undefined | Record<string, any>, value: string) {
if (!item) return value;
return (
item[props.labelField] ??
item.name ??
item.title ??
item.label ??
item.text ??
value
);
}
async function loadInitialLabels(values: string[]) {
if (values.length === 0) return;
try {
if (
props.dataSourceType === 'static' ||
(!props.dataSourceType && props.options.length > 0)
) {
for (const item of props.options) {
const value = String(item[props.valueField]);
if (values.includes(value)) selectedItemsMap.value.set(value, item);
}
return;
}
if (props.dataSourceType === 'formData' && props.formCode) {
const response = await requestClient.get(
`/api/online_dev/form-data/${props.formCode}/list`,
{
params: {
[`${props.valueField}__in`]: values.join(','),
pageSize: values.length,
},
},
);
for (const item of pickList(response)) {
selectedItemsMap.value.set(String(item[props.valueField]), item);
}
return;
}
if (props.dataSourceType === 'dict' && props.dictCode) {
const response = await requestClient.get(
`/api/core/dict_item/by/dict_code/${props.dictCode}`,
);
for (const item of pickList(response)) {
const value = String(item[props.valueField]);
if (values.includes(value)) selectedItemsMap.value.set(value, item);
}
return;
}
if (props.dataSourceType === 'dataSource' && props.dataSourceCode) {
const response = await requestClient.get(
`/api/core/data-source/execute/${props.dataSourceCode}`,
);
for (const item of pickList(response)) {
const value = String(item[props.valueField]);
if (values.includes(value)) selectedItemsMap.value.set(value, item);
}
}
} catch (error) {
console.error('load selector labels failed:', error);
}
}
function emitSelectItem() {
const selectedItems = [...selectedValues.value]
.map((value) => selectedItemsMap.value.get(value))
.filter(Boolean);
emit('select-item', props.multiple ? selectedItems : selectedItems[0]);
}
watch(
() => props.modelValue,
async (value) => {
const values = normalizeValues(value);
selectedValues.value = new Set(values);
const missingValues = values.filter((v) => !selectedItemsMap.value.has(v));
if (missingValues.length > 0) {
labelsLoading.value = true;
await loadInitialLabels(missingValues);
labelsLoading.value = false;
emitSelectItem();
}
},
{ immediate: true },
);
const displayTags = computed(() =>
[...selectedValues.value].map((value) => ({
value,
label: getItemLabel(selectedItemsMap.value.get(value), value),
})),
);
const selectDisplayValue = computed(() => {
const values = [...selectedValues.value];
return props.multiple ? values : values[0];
});
const showPagination = computed(
() =>
props.dataSourceType === 'formData' ||
props.dataSourceType === 'api' ||
total.value > pageSize.value,
);
const displayColumns = computed<TableSelectorColumn[]>(() => {
if (props.columns.length > 0) return props.columns;
return [
{ field: props.labelField, label: $t('form-design.attribute.nodeLabel') },
{ field: props.valueField, label: $t('form-design.attribute.nodeValue') },
];
});
async function loadData() {
loading.value = true;
try {
let data: Array<Record<string, any>> = [];
let totalCount = 0;
if (
props.dataSourceType === 'static' ||
(!props.dataSourceType && props.options.length > 0)
) {
const keyword = searchKeyword.value.trim().toLowerCase();
data = keyword
? props.options.filter((item) =>
[props.labelField, props.valueField].some((field) =>
String(item[field] ?? '')
.toLowerCase()
.includes(keyword),
),
)
: props.options;
totalCount = data.length;
} else if (props.dataSourceType === 'formData' && props.formCode) {
const params: Record<string, any> = {
page: currentPage.value,
pageSize: pageSize.value,
};
if (searchKeyword.value) {
const searchField = props.searchFields[0] || props.labelField;
params[`${searchField}__like`] = searchKeyword.value;
}
const response = await requestClient.get(
`/api/online_dev/form-data/${props.formCode}/list`,
{ params },
);
data = pickList(response);
totalCount = pickTotal(response, data.length);
} else if (props.dataSourceType === 'dict' && props.dictCode) {
const response = await requestClient.get(
`/api/core/dict_item/by/dict_code/${props.dictCode}`,
);
data = pickList(response);
totalCount = data.length;
} else if (props.dataSourceType === 'dataSource' && props.dataSourceCode) {
const response = await requestClient.get(
`/api/core/data-source/execute/${props.dataSourceCode}`,
);
data = pickList(response);
totalCount = data.length;
} else if (props.dataSourceType === 'api' && props.apiUrl) {
const query = {
page: currentPage.value,
pageSize: pageSize.value,
keyword: searchKeyword.value,
};
const response =
props.apiMethod === 'POST'
? await requestClient.post(props.apiUrl, query)
: await requestClient.get(props.apiUrl, { params: query });
data = pickList(response);
totalCount = pickTotal(response, data.length);
}
tableData.value = data;
total.value = totalCount;
for (const item of data) {
const value = String(item[props.valueField]);
if (selectedValues.value.has(value)) selectedItemsMap.value.set(value, item);
}
} catch (error) {
console.error('load selector data failed:', error);
tableData.value = [];
total.value = 0;
} finally {
loading.value = false;
}
}
function openDialog() {
if (props.disabled) return;
dialogVisible.value = true;
loadData();
}
function handleSearch() {
currentPage.value = 1;
loadData();
}
function handlePageChange(page: number) {
currentPage.value = page;
loadData();
}
function handleSizeChange(size: number) {
pageSize.value = size;
currentPage.value = 1;
loadData();
}
function isRowSelected(row: Record<string, any>) {
return selectedValues.value.has(String(row[props.valueField]));
}
function getRowValue(row: Record<string, any>) {
return String(row[props.valueField]);
}
function toggleRow(row: Record<string, any>, checked?: boolean | number | string) {
const value = getRowValue(row);
if (props.multiple) {
const nextChecked = checked ?? !selectedValues.value.has(value);
if (nextChecked) {
selectedValues.value.add(value);
selectedItemsMap.value.set(value, row);
} else {
selectedValues.value.delete(value);
selectedItemsMap.value.delete(value);
}
selectedValues.value = new Set(selectedValues.value);
return;
}
selectedValues.value = new Set([value]);
selectedItemsMap.value.clear();
selectedItemsMap.value.set(value, row);
}
const isAllSelected = computed(
() =>
tableData.value.length > 0 &&
tableData.value.every((row) => isRowSelected(row)),
);
const isIndeterminate = computed(() => {
const selectedCount = tableData.value.filter((row) => isRowSelected(row)).length;
return selectedCount > 0 && selectedCount < tableData.value.length;
});
function handleSelectAll(checked: boolean | number | string) {
for (const row of tableData.value) toggleRow(row, checked);
}
function handleConfirm() {
const values = [...selectedValues.value];
const result = props.multiple ? values : values[0];
emit('update:modelValue', result);
emit('change', result);
emit('blur');
emitSelectItem();
dialogVisible.value = false;
}
function handleClear() {
selectedValues.value = new Set();
selectedItemsMap.value.clear();
emit('update:modelValue', props.multiple ? [] : undefined);
emit('change', props.multiple ? [] : undefined);
emit('select-item', undefined);
}
function handleRemoveTag(value: string) {
selectedValues.value.delete(value);
selectedItemsMap.value.delete(value);
selectedValues.value = new Set(selectedValues.value);
const values = [...selectedValues.value];
const result = props.multiple ? values : values[0];
emit('update:modelValue', result);
emit('change', result);
emitSelectItem();
}
</script>
<template>
<ElButton :disabled="disabled" plain @click="clearValue">
{{ placeholder || 'Table selector unavailable' }}
</ElButton>
<div class="table-selector">
<ElSelect
:model-value="labelsLoading ? undefined : selectDisplayValue"
:placeholder="labelsLoading ? $t('common.loading') : placeholder"
:disabled="disabled"
:clearable="clearable"
:multiple="multiple"
:collapse-tags="multiple && collapseTags"
:collapse-tags-tooltip="multiple && collapseTags"
:loading="labelsLoading"
:suffix-icon="Table2"
class="w-full"
popper-class="table-selector-popper-hidden"
@click="openDialog"
@clear="handleClear"
@remove-tag="handleRemoveTag"
>
<ElOption
v-for="tag in displayTags"
:key="tag.value"
:label="tag.label"
:value="tag.value"
/>
</ElSelect>
<ZqDialog v-model="dialogVisible" :title="dialogTitle" :width="dialogWidth">
<div class="table-selector-content flex h-[600px] flex-col">
<div class="mb-4 flex shrink-0 items-center gap-2">
<ElInput
v-model="searchKeyword"
:placeholder="$t('form-design.search')"
clearable
class="w-64"
@change="handleSearch"
@keyup.enter="handleSearch"
/>
<ElButton type="primary" @click="handleSearch">
{{ $t('common.search') }}
</ElButton>
</div>
<div class="min-h-0 flex-1">
<ElTable
v-loading="loading"
:data="tableData"
border
stripe
height="100%"
:row-class-name="
({ row }: any) => (isRowSelected(row) ? 'selected-row' : '')
"
style="width: 100%"
@row-click="toggleRow"
>
<ElTableColumn v-if="!multiple" width="55" align="center">
<template #default="{ row }">
<ElRadio
:model-value="isRowSelected(row) ? getRowValue(row) : ''"
:value="getRowValue(row)"
@click.stop
@change="toggleRow(row)"
>
<span></span>
</ElRadio>
</template>
</ElTableColumn>
<ElTableColumn v-else width="55" align="center">
<template #header>
<ElCheckbox
:model-value="isAllSelected"
:indeterminate="isIndeterminate"
@change="handleSelectAll"
/>
</template>
<template #default="{ row }">
<ElCheckbox
:model-value="isRowSelected(row)"
@click.stop
@change="toggleRow(row, $event)"
/>
</template>
</ElTableColumn>
<ElTableColumn
v-for="col in displayColumns"
:key="col.field"
:prop="col.field"
:label="col.label"
:width="col.width"
/>
<template #empty>
<ElEmpty :description="$t('common.noData')" />
</template>
</ElTable>
</div>
</div>
<template #footer>
<div class="flex w-full items-center">
<div class="flex-shrink-0">
<ElPagination
v-if="showPagination"
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
small
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
<div class="flex flex-1 justify-center">
<span
v-if="selectedValues.size > 0"
class="text-sm text-[var(--el-text-color-secondary)]"
>
{{
$t('form-design.attribute.selectedCount', {
count: selectedValues.size,
})
}}
</span>
</div>
<div class="flex flex-shrink-0 gap-2">
<ElButton @click="dialogVisible = false">
{{ $t('common.cancel') }}
</ElButton>
<ElButton type="primary" @click="handleConfirm">
{{ $t('common.confirm') }}
</ElButton>
</div>
</div>
</template>
</ZqDialog>
</div>
</template>
<style scoped>
.table-selector {
width: 100%;
}
.table-selector-content :deep(.selected-row) {
background-color: var(--el-color-primary-light-9) !important;
}
.table-selector-content :deep(.el-table__row) {
cursor: pointer;
}
</style>
<style>
.table-selector-popper-hidden {
display: none !important;
}
</style>
@@ -0,0 +1,37 @@
export interface TableSelectorColumn {
field: string;
label: string;
width?: number | string;
}
export interface TableSelectorProps {
modelValue?: string | string[];
multiple?: boolean;
placeholder?: string;
disabled?: boolean;
clearable?: boolean;
dialogTitle?: string;
dialogWidth?: string;
columns?: TableSelectorColumn[];
labelField?: string;
valueField?: string;
dataSourceType?: 'api' | 'dataSource' | 'dict' | 'formData' | 'static';
dictCode?: string;
dataSourceCode?: string;
formCode?: string;
apiUrl?: string;
apiMethod?: 'GET' | 'POST';
searchFields?: string[];
collapseTags?: boolean;
options?: Array<Record<string, any>>;
}
export interface TableSelectorEmits {
(e: 'update:modelValue', value: string | string[] | undefined): void;
(e: 'change', value: string | string[] | undefined): void;
(e: 'blur'): void;
(
e: 'select-item',
item: Record<string, any> | Record<string, any>[] | undefined,
): void;
}