feat(projects): 新增项目、执行、任务等功能
This commit is contained in:
314
src/components/custom/table-search-fields.vue
Normal file
314
src/components/custom/table-search-fields.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { ElButton, ElCol, ElDatePicker, ElFormItem, ElInput, ElOption, ElRow, ElSelect } from 'element-plus';
|
||||
import DictSelect from './dict-select.vue';
|
||||
|
||||
defineOptions({ name: 'TableSearchFields' });
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface SearchField {
|
||||
/** 字段键名 */
|
||||
key: string;
|
||||
/** 字段标签 */
|
||||
label: string;
|
||||
/** 字段类型 */
|
||||
type: 'input' | 'select' | 'date' | 'dateRange' | 'dict';
|
||||
/** 占位列数,默认 1 */
|
||||
span?: number;
|
||||
/** select 类型的选项 */
|
||||
options?: Option[];
|
||||
/** dict 类型的字典编码 */
|
||||
dictCode?: string;
|
||||
/** 占位提示文本 */
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
/** 绑定表单数据对象 */
|
||||
modelValue: Record<string, any>;
|
||||
/** 查询字段定义数组 */
|
||||
fields: SearchField[];
|
||||
/** 每行格子数(按钮占 1 格) */
|
||||
columns: number;
|
||||
/** 表单标签宽度 */
|
||||
labelWidth?: string | number;
|
||||
/** 格子间距 */
|
||||
gutter?: number;
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
labelWidth: 80,
|
||||
gutter: 16,
|
||||
disabled: false
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
(e: 'search'): void;
|
||||
(e: 'reset'): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// 折叠/展开状态
|
||||
const expanded = ref(false);
|
||||
|
||||
// 是否需要折叠(字段数 > columns - 1)
|
||||
const needsCollapse = computed(() => props.fields.length > props.columns - 1);
|
||||
|
||||
// 第一行字段数(留一个位置给按钮)
|
||||
const firstRowFieldCount = computed(() => props.columns - 1);
|
||||
|
||||
// 计算第一行字段
|
||||
const firstRowFields = computed(() => {
|
||||
if (expanded.value || !needsCollapse.value) {
|
||||
return props.fields.slice(0, firstRowFieldCount.value);
|
||||
}
|
||||
return props.fields.slice(0, firstRowFieldCount.value);
|
||||
});
|
||||
|
||||
// 计算后续行字段(用于展开后显示)
|
||||
const remainingFields = computed(() => {
|
||||
if (expanded.value || !needsCollapse.value) {
|
||||
return props.fields.slice(firstRowFieldCount.value);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const firstRowButtonSpan = computed(() => {
|
||||
return Math.floor(24 / props.columns);
|
||||
});
|
||||
|
||||
// 计算第一行字段的 span(字段和按钮区保持同一列宽)
|
||||
const firstRowFieldSpan = computed(() => {
|
||||
return firstRowButtonSpan.value;
|
||||
});
|
||||
|
||||
// 计算每个字段的 span(用于后续行)
|
||||
const fieldSpan = computed(() => {
|
||||
return Math.floor(24 / props.columns);
|
||||
});
|
||||
|
||||
// 字段不足时补足首行空列,确保按钮区始终落在 columns 定义的最后一格。
|
||||
const firstRowPlaceholderSpan = computed(() => {
|
||||
const emptySlotCount = Math.max(props.columns - 1 - firstRowFields.value.length, 0);
|
||||
return emptySlotCount * fieldSpan.value;
|
||||
});
|
||||
|
||||
function handleToggle() {
|
||||
expanded.value = !expanded.value;
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
emit('reset');
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
emit('search');
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable vue/no-mutating-props -->
|
||||
<template>
|
||||
<ElCard class="card-wrapper">
|
||||
<ElForm :model="props.modelValue" :label-width="props.labelWidth" @submit.prevent @keyup.enter="handleSearch">
|
||||
<!-- 第一行:fields + 按钮 -->
|
||||
<ElRow :gutter="props.gutter">
|
||||
<ElCol
|
||||
v-for="field in firstRowFields"
|
||||
:key="field.key"
|
||||
class="table-search-fields__col"
|
||||
:span="firstRowFieldSpan"
|
||||
>
|
||||
<ElFormItem :label="field.label">
|
||||
<ElInput
|
||||
v-if="field.type === 'input'"
|
||||
:model-value="props.modelValue[field.key]"
|
||||
:placeholder="field.placeholder"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||
/>
|
||||
<ElSelect
|
||||
v-else-if="field.type === 'select'"
|
||||
:model-value="props.modelValue[field.key]"
|
||||
:placeholder="field.placeholder"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||
>
|
||||
<ElOption v-for="opt in field.options" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</ElSelect>
|
||||
<ElDatePicker
|
||||
v-else-if="field.type === 'date'"
|
||||
:model-value="props.modelValue[field.key]"
|
||||
type="date"
|
||||
:placeholder="field.placeholder"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
value-format="YYYY-MM-DD"
|
||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||
/>
|
||||
<ElDatePicker
|
||||
v-else-if="field.type === 'dateRange'"
|
||||
:model-value="props.modelValue[field.key]"
|
||||
type="daterange"
|
||||
:placeholder="field.placeholder"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||
/>
|
||||
<DictSelect
|
||||
v-else-if="field.type === 'dict'"
|
||||
:model-value="props.modelValue[field.key]"
|
||||
:dict-code="field.dictCode!"
|
||||
:placeholder="field.placeholder"
|
||||
:disabled="props.disabled"
|
||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
|
||||
<ElCol
|
||||
v-if="firstRowPlaceholderSpan > 0"
|
||||
class="table-search-fields__col table-search-fields__placeholder-col"
|
||||
:span="firstRowPlaceholderSpan"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<!-- 按钮区域 -->
|
||||
<ElCol class="table-search-fields__col table-search-fields__action-col" :span="firstRowButtonSpan">
|
||||
<ElFormItem class="table-search-fields__actions" label-width="0">
|
||||
<ElButton
|
||||
v-if="needsCollapse"
|
||||
circle
|
||||
:title="expanded ? '收起' : '展开'"
|
||||
:aria-label="expanded ? '收起查询条件' : '展开查询条件'"
|
||||
:disabled="props.disabled"
|
||||
@click="handleToggle"
|
||||
>
|
||||
<icon-mdi-chevron-double-up v-if="expanded" />
|
||||
<icon-mdi-chevron-double-down v-else />
|
||||
</ElButton>
|
||||
<ElButton :disabled="props.disabled" @click="handleReset">
|
||||
<template #icon>
|
||||
<icon-ic-round-refresh class="text-icon" />
|
||||
</template>
|
||||
重置
|
||||
</ElButton>
|
||||
<ElButton type="primary" :disabled="props.disabled" @click="handleSearch">
|
||||
<template #icon>
|
||||
<icon-ic-round-search class="text-icon" />
|
||||
</template>
|
||||
查询
|
||||
</ElButton>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 展开后的后续行 -->
|
||||
<ElRow v-if="expanded && remainingFields.length > 0" :gutter="props.gutter">
|
||||
<ElCol v-for="field in remainingFields" :key="field.key" class="table-search-fields__col" :span="fieldSpan">
|
||||
<ElFormItem :label="field.label">
|
||||
<ElInput
|
||||
v-if="field.type === 'input'"
|
||||
:model-value="props.modelValue[field.key]"
|
||||
:placeholder="field.placeholder"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||
/>
|
||||
<ElSelect
|
||||
v-else-if="field.type === 'select'"
|
||||
:model-value="props.modelValue[field.key]"
|
||||
:placeholder="field.placeholder"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||
>
|
||||
<ElOption v-for="opt in field.options" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||
</ElSelect>
|
||||
<ElDatePicker
|
||||
v-else-if="field.type === 'date'"
|
||||
:model-value="props.modelValue[field.key]"
|
||||
type="date"
|
||||
:placeholder="field.placeholder"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
value-format="YYYY-MM-DD"
|
||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||
/>
|
||||
<ElDatePicker
|
||||
v-else-if="field.type === 'dateRange'"
|
||||
:model-value="props.modelValue[field.key]"
|
||||
type="daterange"
|
||||
:placeholder="field.placeholder"
|
||||
clearable
|
||||
:disabled="props.disabled"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||
/>
|
||||
<DictSelect
|
||||
v-else-if="field.type === 'dict'"
|
||||
:model-value="props.modelValue[field.key]"
|
||||
:dict-code="field.dictCode!"
|
||||
:placeholder="field.placeholder"
|
||||
:disabled="props.disabled"
|
||||
@update:model-value="val => (props.modelValue[field.key] = val)"
|
||||
/>
|
||||
</ElFormItem>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
</ElForm>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-form-item) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.table-search-fields__col {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.table-search-fields__placeholder-col {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.table-search-fields__actions {
|
||||
:deep(.el-form-item__content) {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-form-item__content) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.el-input),
|
||||
:deep(.el-select),
|
||||
:deep(.el-date-editor) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user