|
|
@@ -0,0 +1,314 @@
|
|
|
+<script lang="ts" setup>
|
|
|
+import type { Recordable } from '@vben/types';
|
|
|
+
|
|
|
+import type { ApiConfig } from '../form/types';
|
|
|
+
|
|
|
+import { type PropType, reactive, ref } from 'vue';
|
|
|
+
|
|
|
+import { useVbenForm, useVbenModal } from '@vben/common-ui';
|
|
|
+import { isFunction } from '@vben/utils';
|
|
|
+
|
|
|
+import { Checkbox, message, Pagination } from 'ant-design-vue';
|
|
|
+
|
|
|
+import { EnumApi, QueryApi } from '#/api';
|
|
|
+import { requestClient } from '#/api/request';
|
|
|
+import { get } from '#/utils';
|
|
|
+
|
|
|
+import SelectCardItem from './select-card-item.vue';
|
|
|
+
|
|
|
+defineOptions({
|
|
|
+ name: 'SelectCard',
|
|
|
+});
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ multiple: {
|
|
|
+ type: Boolean as PropType<boolean>,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+ title: {
|
|
|
+ type: String as PropType<string>,
|
|
|
+ default: '选择数据',
|
|
|
+ },
|
|
|
+ showTree: {
|
|
|
+ type: Boolean as PropType<boolean>,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
+ immediate: {
|
|
|
+ type: Boolean,
|
|
|
+ default: true,
|
|
|
+ },
|
|
|
+});
|
|
|
+const emit = defineEmits(['success']);
|
|
|
+const cardData = ref<Recordable<any>[]>([]);
|
|
|
+
|
|
|
+const state = reactive<{
|
|
|
+ api: ApiConfig;
|
|
|
+ config: {
|
|
|
+ bgcolor: string;
|
|
|
+ color: string;
|
|
|
+ fillColor: string;
|
|
|
+ type: string;
|
|
|
+ };
|
|
|
+ disabledIds: number[] | string[];
|
|
|
+ fieldNames: Recordable<any>[];
|
|
|
+ key: number | string;
|
|
|
+ keyName: string;
|
|
|
+ page: { pageIndex: number; pageSize: number; total: number };
|
|
|
+ searchName: string;
|
|
|
+ selectedIds: number[];
|
|
|
+ selectedList: Recordable<any>[];
|
|
|
+}>({
|
|
|
+ page: {
|
|
|
+ pageIndex: 1,
|
|
|
+ pageSize: 9,
|
|
|
+ total: 0,
|
|
|
+ },
|
|
|
+ selectedIds: [],
|
|
|
+ selectedList: [],
|
|
|
+ disabledIds: [],
|
|
|
+ api: {
|
|
|
+ type: 'none',
|
|
|
+ method: 'get',
|
|
|
+ params: {},
|
|
|
+ result: 'items',
|
|
|
+ url: null,
|
|
|
+ },
|
|
|
+ keyName: 'id',
|
|
|
+ key: '',
|
|
|
+ searchName: '',
|
|
|
+ fieldNames: [
|
|
|
+ { title: '名称', value: 'title', maxLength: 12 },
|
|
|
+ { title: '电话', value: 'phone', maxLength: 8 },
|
|
|
+ ],
|
|
|
+ config: {
|
|
|
+ type: 'none',
|
|
|
+ fillColor: '#f1ecfe',
|
|
|
+ bgcolor: '#f5f1fd',
|
|
|
+ color: '#b389ff',
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const fetch = async () => {
|
|
|
+ const api: any =
|
|
|
+ typeof state.api.url === 'string' || !state.api.url
|
|
|
+ ? (params: any) => {
|
|
|
+ Object.assign(params, state.page);
|
|
|
+ if (state.api.type === 'enum') {
|
|
|
+ return EnumApi.getList(params);
|
|
|
+ } else if (state.api.type === 'api') {
|
|
|
+ return QueryApi.postOptions(params);
|
|
|
+ } else
|
|
|
+ return (requestClient as any)[state.api.method](
|
|
|
+ state.api.url as any,
|
|
|
+ state.api.method === 'get' ? { params } : params,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ : state.api.url;
|
|
|
+ if (!api || !isFunction(api)) return;
|
|
|
+ try {
|
|
|
+ const res = await api(state.api.params);
|
|
|
+ if (Array.isArray(res)) {
|
|
|
+ cardData.value = res;
|
|
|
+ } else {
|
|
|
+ cardData.value = state.api.result ? get(res, state.api.result) : [];
|
|
|
+ state.page.total = res.total || 0;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.warn(error);
|
|
|
+ } finally {
|
|
|
+ // setState({ loading: false });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const [Form, { resetForm }] = useVbenForm({
|
|
|
+ layout: 'horizontal',
|
|
|
+ commonConfig: {
|
|
|
+ componentProps: {
|
|
|
+ class: 'w-full',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ schema: [
|
|
|
+ {
|
|
|
+ component: 'Input',
|
|
|
+ componentProps: {
|
|
|
+ placeholder: '请输入',
|
|
|
+ },
|
|
|
+ fieldName: 'keyword',
|
|
|
+ label: '关键字',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ submitButtonOptions: {
|
|
|
+ content: '查询',
|
|
|
+ },
|
|
|
+ wrapperClass: 'grid-cols-2',
|
|
|
+ handleSubmit: async (values: Record<string, any>) => {
|
|
|
+ if (state.searchName) {
|
|
|
+ state.page.pageIndex = 1;
|
|
|
+ state.api.params[state.searchName] = values.keyword;
|
|
|
+ await fetch();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ handleReset: async () => {
|
|
|
+ resetForm();
|
|
|
+ state.page.pageIndex = 1;
|
|
|
+ state.api.params[state.searchName] = '';
|
|
|
+ await fetch();
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const [Modal, { close, setState, getData }] = useVbenModal({
|
|
|
+ onConfirm: async () => {
|
|
|
+ try {
|
|
|
+ close();
|
|
|
+ emit('success', {
|
|
|
+ key: state.key,
|
|
|
+ ids: [...state.selectedIds],
|
|
|
+ list: [...state.selectedList],
|
|
|
+ });
|
|
|
+ } catch {
|
|
|
+ message.error('操作失败');
|
|
|
+ } finally {
|
|
|
+ setState({ confirmLoading: false });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onOpenChange: async (isOpen: boolean) => {
|
|
|
+ if (isOpen) {
|
|
|
+ setState({ loading: true });
|
|
|
+ const data = getData<Record<string, any>>();
|
|
|
+ state.key = data.baseData.key;
|
|
|
+ state.searchName = data.baseData.searchName || '';
|
|
|
+ state.selectedIds = data.baseData.selectedIds || [];
|
|
|
+ state.disabledIds = data.baseData.disabledIds || [];
|
|
|
+ state.fieldNames = data.baseData.fieldNames || [];
|
|
|
+ Object.assign(state.api, data.baseData.api);
|
|
|
+ Object.assign(state.config, data.baseData.config);
|
|
|
+ state.page.pageIndex = 1;
|
|
|
+ await fetch();
|
|
|
+ setState({ loading: false });
|
|
|
+ }
|
|
|
+ },
|
|
|
+});
|
|
|
+
|
|
|
+const hancelClick = (item: any) => {
|
|
|
+ if (
|
|
|
+ state.disabledIds &&
|
|
|
+ Array.isArray(state.disabledIds) &&
|
|
|
+ state.disabledIds.includes(item[state.keyName] as never)
|
|
|
+ ) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (props.multiple) {
|
|
|
+ if (state.selectedIds.includes(item[state.keyName])) {
|
|
|
+ state.selectedIds.splice(
|
|
|
+ state.selectedIds.indexOf(item[state.keyName]),
|
|
|
+ 1,
|
|
|
+ );
|
|
|
+ state.selectedList.splice(
|
|
|
+ state.selectedList.findIndex(
|
|
|
+ (ele) => ele[state.keyName] === item[state.keyName],
|
|
|
+ ),
|
|
|
+ 1,
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ state.selectedIds.push(item[state.keyName]);
|
|
|
+ state.selectedList.push(item);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (state.selectedIds.includes(item[state.keyName])) {
|
|
|
+ state.selectedIds = [];
|
|
|
+ state.selectedList = [];
|
|
|
+ } else {
|
|
|
+ state.selectedIds = [item[state.keyName]];
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const handelPageChange = async (pageIndex: number) => {
|
|
|
+ setState({ loading: true });
|
|
|
+ state.page.pageIndex = pageIndex;
|
|
|
+ await fetch();
|
|
|
+ setState({ loading: false });
|
|
|
+};
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <Modal :title="`选择${title}`" class="w-[1000px]">
|
|
|
+ <div class="select-card flex justify-between">
|
|
|
+ <div class="ml-4 flex-1">
|
|
|
+ <div class="select-card-header">
|
|
|
+ <div class="flex justify-between">
|
|
|
+ <div class="text-foreground text-lg font-semibold">
|
|
|
+ {{ title }}列表
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <Form class="mt-4" />
|
|
|
+ </div>
|
|
|
+ <div class="select-card-body">
|
|
|
+ <SelectCardItem
|
|
|
+ v-for="(item, index) in cardData"
|
|
|
+ :key="index"
|
|
|
+ :class="
|
|
|
+ state.selectedIds.includes(item[state.keyName])
|
|
|
+ ? 'picked'
|
|
|
+ : 'not-picked'
|
|
|
+ "
|
|
|
+ :config="state.config"
|
|
|
+ :disabled="
|
|
|
+ state.disabledIds &&
|
|
|
+ state.disabledIds.includes(item[state.keyName] as never)
|
|
|
+ ? true
|
|
|
+ : false
|
|
|
+ "
|
|
|
+ :field-names="state.fieldNames"
|
|
|
+ :model="item"
|
|
|
+ @click="hancelClick(item)"
|
|
|
+ >
|
|
|
+ <template #check>
|
|
|
+ <Checkbox
|
|
|
+ :checked="state.selectedIds.includes(item[state.keyName])"
|
|
|
+ size="small"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
+ </SelectCardItem>
|
|
|
+ <div class="select-card-pagination">
|
|
|
+ <Pagination
|
|
|
+ v-model:current="state.page.pageIndex"
|
|
|
+ :page-size="state.page.pageSize"
|
|
|
+ :total="state.page.total"
|
|
|
+ show-less-items
|
|
|
+ show-quick-jumper
|
|
|
+ size="small"
|
|
|
+ @change="handelPageChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Modal>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style lang="less" scoped>
|
|
|
+.select-card {
|
|
|
+ display: flex;
|
|
|
+ height: 500px;
|
|
|
+ margin: 10px;
|
|
|
+ overflow: auto;
|
|
|
+ position: relative;
|
|
|
+
|
|
|
+ &-body {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ padding: 10px 0;
|
|
|
+ overflow-y: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ &-pagination {
|
|
|
+ // width: 100%;
|
|
|
+ position: absolute;
|
|
|
+ text-align: right;
|
|
|
+ bottom: 10px;
|
|
|
+ right: 10px;
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|