feat(ui): 新增请求方法及表格

This commit is contained in:
廖彦棋
2024-03-06 13:55:38 +08:00
parent 06fa54fd25
commit 6399d13a49
19 changed files with 667 additions and 90 deletions

View File

@@ -0,0 +1,117 @@
<script lang="ts" setup>
import { computed, ref, type PropType } from "vue";
import { getDefaultFormData, useComponentConfig } from "./utils";
import { ValueType } from "./type.d";
import type { SearchTableColumns, SearchColumns } from "./type";
const props = defineProps({
modelValue: {
type: Object,
default: () => ({}),
},
columns: {
type: Array as PropType<SearchTableColumns[]>,
default: () => [],
},
submitting: {
type: Boolean,
default: false,
},
});
const emits = defineEmits(["update:modelValue", "request"]);
const size = "small";
const collapsed = ref(false);
const formData = computed({
get: () => props.modelValue,
set(value) {
emits("update:modelValue", value);
},
});
const searchColumns = computed(() => {
return props.columns?.filter(
(item) => item.dataIndex && item.search
) as (SearchColumns & { dataIndex: string })[];
});
const optionsEvent = {
onReset: () => {
formData.value = getDefaultFormData(props.columns);
emits("request");
},
onSearch: () => emits("request"),
onCollapse: (value: boolean) => {
collapsed.value = value ?? !collapsed.value;
},
};
</script>
<template>
<AForm
v-if="searchColumns?.length"
class="search-form-conteiner"
:model="formData"
:size="size"
:label-col-props="{ span: 0 }"
:wrapper-col-props="{ span: 24 }"
@submit="optionsEvent.onSearch"
>
<AGrid
:cols="{ md: 1, lg: 2, xl: 3, xxl: 5 }"
:row-gap="12"
:col-gap="12"
:collapsed="collapsed"
>
<AGridItem
v-for="item in searchColumns"
:key="item.dataIndex"
style="transition: all 0.3s ease-in-out"
>
<AFormItem :field="item.dataIndex" :label="(item.title as string)">
<slot :name="item.search.slotsName">
<component
v-model="formData[item.dataIndex]"
:is="
ValueType[item.search.valueType ?? 'input'] ??
item.search.render
"
v-bind="useComponentConfig(size, item)"
/>
</slot>
</AFormItem>
</AGridItem>
<AGridItem suffix>
<ASpace>
<slot name="search-options" :option="optionsEvent">
<AButton
type="primary"
html-type="submit"
:size="size"
:loading="submitting"
>
<icon-search />
<span>查询</span>
</AButton>
<AButton
:size="size"
@click="optionsEvent.onReset"
:loading="submitting"
>
<icon-refresh />
<span>重置</span>
</AButton>
</slot>
<slot name="search-extra" />
</ASpace>
</AGridItem>
</AGrid>
</AForm>
</template>
<style scoped>
.search-form-conteiner {
padding: 16px 0;
}
</style>

View File

@@ -0,0 +1,87 @@
<script lang="ts" setup>
import { computed, ref, onActivated } from "vue";
import useAsyncTable from "./useAsyncTable";
import FormSection from "./FormSection.vue";
import type { SearchTableProps } from "./type";
import { useTableScroll, getDefaultFormData, useRequestParams } from "./utils";
import { Message } from "@arco-design/web-vue";
const props = defineProps<SearchTableProps>();
const formData = ref({ ...getDefaultFormData(props.columns) });
const tableContainerRef = ref<HTMLElement>();
// 表格请求参数
const requestParams = computed(() => ({
...useRequestParams(props.columns, formData.value),
...props.params,
}));
const [tableConfig, getList] = useAsyncTable(props.request, requestParams);
const _columns = computed(() => {
return props.columns.map((item) => ({
ellipsis: true,
tooltip: true,
...item,
}));
});
const handleSearch = async (tips?: boolean) => {
tips && Message.success("操作成功");
await getList();
};
onActivated(handleSearch);
</script>
<template>
<div class="search-table">
<div class="search-table-header">
<div>
<slot name="header-title">{{ props.headerTitle }}</slot>
</div>
<div class="header-option">
<slot
name="header-option"
:formData="formData"
:reload="handleSearch"
/>
</div>
</div>
<FormSection
v-model="formData"
:columns="columns"
:submitting="(tableConfig.loading as boolean)"
@request="handleSearch"
/>
<div ref="tableContainerRef" class="search-table-container">
<ATable
v-bind="{
...$attrs,
...tableConfig,
...props,
scroll: useTableScroll(_columns, tableContainerRef as HTMLElement),
columns: _columns
}"
>
<template v-for="slot in Object.keys($slots)" #[slot]="config">
<slot :name="slot" v-bind="{ ...config, reload: handleSearch }" />
</template>
</ATable>
</div>
</div>
</template>
<style scoped>
.search-table {
display: flex;
flex-direction: column;
height: 100%;
}
.search-table-container {
flex: 1;
}
.search-table-header {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,49 @@
import type { Component } from "vue";
import type { JsxElement } from "typescript";
import {
DatePicker,
Input,
InputNumber,
RadioGroup,
RangePicker,
Select,
Switch,
type TableColumnData,
} from "@arco-design/web-vue";
import type { TableOriginalProps, TableRequest } from "./useAsyncTable";
type Object = Record<string, unknown>;
export enum ValueType {
"input" = Input,
"select" = Select,
"number" = InputNumber,
"date" = DatePicker,
"range" = RangePicker,
"radio" = RadioGroup,
"switch" = Switch,
}
export type SearchConfig = {
valueType?: keyof typeof ValueType;
fieldProps?: Object;
render?: Component | JsxElement;
slotsName?: string;
defaultValue?: any;
transform?: (value) => Record<string, any>;
};
export interface SearchTableColumns extends TableColumnData {
search?: SearchConfig;
hideInTable?: boolean;
[key: string]: any;
}
export type SearchColumns = SearchTableColumns & { search: SearchConfig };
export interface SearchTableProps extends /* @vue-ignore */ TableOriginalProps {
request: TableRequest<Object>;
params?: Object;
columns: SearchTableColumns[];
headerTitle?: string;
}

View File

@@ -0,0 +1,63 @@
import { computed, onMounted, reactive, unref, type Ref } from "vue";
import type { TableInstance } from "@arco-design/web-vue";
import type { BaseResponse, ListResponse } from "@gpt-vue/packages/type";
export type TableOriginalProps = TableInstance["$props"];
export type TableRequest<T extends Record<string, unknown>> = (params?: any) => Promise<BaseResponse<ListResponse<T>>>
export type TableReturn = [TableOriginalProps, () => Promise<void>];
function useAsyncTable<T extends Record<string, unknown>>(
request: TableRequest<T>,
params?: Ref<Record<string, unknown>>
): TableReturn {
const paginationState = reactive({
current: 1,
pageSize: 10,
total: 0,
});
const tableState = reactive({
loading: false,
data: []
})
const tableConfig = computed<TableOriginalProps>(() => {
return {
...tableState,
rowKey: "id",
pagination: {
...paginationState,
showTotal: true,
showPageSize: true,
},
onPageChange: (page) => {
paginationState.current = page;
getTableData();
},
onPageSizeChange(pageSize) {
paginationState.pageSize = pageSize;
getTableData();
},
};
});
const getTableData = async () => {
tableState.loading = true
try {
const { data } = await request({
...unref(params ?? {}),
page: paginationState.current,
pageSize: paginationState.pageSize,
});
tableState.data = data?.items;
paginationState.total = data.total;
} finally {
tableState.loading = false
}
};
onMounted(getTableData);
return [tableConfig, getTableData] as TableReturn;
}
export default useAsyncTable;

View File

@@ -0,0 +1,53 @@
import type { TableColumnData } from "@arco-design/web-vue";
import type { SearchTableColumns, SearchColumns } from "./type";
export function useTableXScroll(columns: TableColumnData[]) {
return columns.reduce((prev, curr) => {
const width = curr.width ?? 150;
return prev + width;
}, 0);
}
export function useTableScroll(columns: SearchTableColumns[], container?: HTMLElement) {
const x = columns.reduce((prev, curr) => {
const width = curr.hideInTable ? 0 : curr.width ?? 150;
return prev + width;
}, 0);
const y = container?.clientHeight ?? undefined;
return { x, y };
}
export function getDefaultFormData(columns: SearchTableColumns[]) {
return columns?.reduce((field, curr) => {
if (curr.dataIndex && curr?.search?.defaultValue) {
field[curr.dataIndex] = curr.search.defaultValue;
}
return field;
}, {});
}
export function useRequestParams(
columns: SearchTableColumns[],
originFormData: Record<string, any>
) {
const filterFormData = columns?.reduce((prev, curr) => {
if (!curr.dataIndex || !curr.search) {
return prev;
}
if (curr?.search?.transform) {
const filters = curr.search.transform(originFormData[curr.dataIndex]);
return Object.assign(prev, filters);
}
return Object.assign(prev, { [curr.dataIndex]: originFormData[curr.dataIndex] });
}, {});
return filterFormData as Record<string, any>;
}
export function useComponentConfig(size: string, item: SearchColumns) {
return {
size,
placeholder: item.search.valueType === "range" ? ["开始时间", "结束时间"] : item.title,
allowClear: true,
...(item.search.fieldProps ?? {}),
};
}