select-pagination-element-plus 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # SelectPagination (Element Plus)
2
+
3
+ [中文文档](./README.zh-CN.md)
4
+
5
+ `SelectPagination` is a Vue 3 + Element Plus `<el-select>` wrapper that supports remote search + paginated loading (infinite load) with optional cache and value echo (detail API).
6
+
7
+ ## Features
8
+
9
+ - Remote search + pagination (`api`, `pageKey`, `sizeKey`, `keyword`, `responsePath`)
10
+ - Infinite loading via `IntersectionObserver` (no scroll listener / no Element Plus internal class dependency)
11
+ - Optional cache (`enableCache`, `cacheKey`)
12
+ - Optional “value echo” (`detailApi`)
13
+ - Supports single / multiple select
14
+ - Footer slot to fully customize “loading / no more / load more”
15
+
16
+ ## Requirements
17
+
18
+ - Vue 3
19
+ - Element Plus
20
+
21
+ ## Install
22
+
23
+ Replace `YOUR_PACKAGE_NAME` with your published npm package name.
24
+
25
+ ```bash
26
+ npm i YOUR_PACKAGE_NAME
27
+ ```
28
+
29
+ ## Basic Usage
30
+
31
+ ```vue
32
+ <template>
33
+ <SelectPagination v-model="value" :api="fetchUsers" placeholder="Type to search" />
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import { ref } from 'vue'
38
+ import SelectPagination from 'YOUR_PACKAGE_NAME'
39
+
40
+ const value = ref('')
41
+
42
+ const fetchUsers = async (params: any) => {
43
+ // params = { page, size, name, ...yourExtraParams }
44
+ // Must return an object that contains { content, totalElements } at responsePath (default: data.data)
45
+ return {
46
+ data: {
47
+ data: {
48
+ content: [
49
+ { id: 'u-1', label: 'User 1', value: 'u-1' },
50
+ { id: 'u-2', label: 'User 2', value: 'u-2' }
51
+ ],
52
+ totalElements: 2
53
+ }
54
+ }
55
+ }
56
+ }
57
+ </script>
58
+ ```
59
+
60
+ ## API Response Format
61
+
62
+ By default `responsePath = "data.data"`, which should point to an object like:
63
+
64
+ ```ts
65
+ {
66
+ content: any[]
67
+ totalElements: number
68
+ }
69
+ ```
70
+
71
+ If your backend returns a different shape, set `responsePath` accordingly.
72
+
73
+ ## Props
74
+
75
+ | Prop | Type | Default | Description |
76
+ | --- | --- | --- | --- |
77
+ | modelValue | `string \| number \| any[]` | `''` | v-model |
78
+ | isRemote | `boolean` | `true` | Enables Element Plus remote mode (`:remote`) |
79
+ | api | `(params) => any` or `() => (params) => any` | - | Fetch list API |
80
+ | apiAsync | `boolean` | `false` | If `true`, treat `api` as a factory (`() => apiFn`) |
81
+ | params | `object \| () => object` | `{}` | Extra query params merged into request |
82
+ | labelField | `string` | `'label'` | Option label field |
83
+ | valueField | `string` | `'value'` | Option value field |
84
+ | pageSize | `number` | `10` | Page size |
85
+ | pageKey | `string` | `'page'` | Page number param key |
86
+ | sizeKey | `string` | `'size'` | Page size param key |
87
+ | keyword | `string` | `'name'` | Search keyword param key |
88
+ | responsePath | `string` | `'data.data'` | Path to `{ content, totalElements }` |
89
+ | enableCache | `boolean` | `true` | Cache options/paging/keyword |
90
+ | cacheKey | `string` | `''` | Cache namespace; if empty, generated from `api.name + params` |
91
+ | changeDetail | `boolean` | `false` | Emit selected option detail(s) |
92
+ | detailApi | `(value) => any` | `null` | Fetch option detail for echo |
93
+ | initOptions | `any[]` | `[]` | Initial options appended into options list |
94
+ | popperClass | `string` | `''` | Extra popper class (scoped styles rely on `select-pagination-popper`) |
95
+ | footerSentinelId | `string` | `''` | Custom sentinel element id for infinite loading |
96
+
97
+ ## Events
98
+
99
+ | Event | Payload | Description |
100
+ | --- | --- | --- |
101
+ | update:modelValue | `any` | v-model update |
102
+ | change | `any` | same as Element Plus change |
103
+ | change-detail | `any \| any[] \| null` | selected option detail(s) when `changeDetail = true` |
104
+
105
+ ## Slots
106
+
107
+ ### footer
108
+
109
+ Slot props:
110
+
111
+ - `loading`: `boolean`
112
+ - `hasMore`: `boolean`
113
+ - `options`: `any[]`
114
+ - `loadMore`: `() => void`
115
+
116
+ Example:
117
+
118
+ ```vue
119
+ <SelectPagination v-model="value" :api="fetchUsers" footer-sentinel-id="user-select-footer">
120
+ <template #footer="{ loading, hasMore, loadMore }">
121
+ <div v-if="loading" style="padding: 8px 0; text-align: center">Loading...</div>
122
+ <div v-else-if="!hasMore" style="padding: 8px 0; text-align: center">No more</div>
123
+ <div v-else style="padding: 8px 0; text-align: center; cursor: pointer" @click="loadMore">
124
+ Click to load more
125
+ </div>
126
+ </template>
127
+ </SelectPagination>
128
+ ```
129
+
130
+ ## Exposed Methods
131
+
132
+ Access via template ref:
133
+
134
+ - `loadData(isReset?: boolean)`
135
+ - `clearCache()`
136
+ - `restoreCache()`
137
+ - `updateValue(val)`
138
+ - `loadEchoData()`
139
+ - `setPage(page: number)`
140
+ - `setOptions(options: any[])`
141
+ - `options` (ref)
142
+ - `currentPage` (ref)
143
+
@@ -0,0 +1,143 @@
1
+ # SelectPagination(Element Plus)
2
+
3
+ [English](./README.md)
4
+
5
+ `SelectPagination` 是一个基于 Vue 3 + Element Plus 的 `<el-select>` 封装组件,支持远程搜索 + 分页加载(无限滚动/触底加载),并提供可选缓存与回显能力(detailApi)。
6
+
7
+ ## 特性
8
+
9
+ - 远程搜索 + 分页(`api`, `pageKey`, `sizeKey`, `keyword`, `responsePath`)
10
+ - 使用 `IntersectionObserver` 实现触底加载(不绑定 scroll 事件 / 不依赖 Element Plus 内部 class)
11
+ - 可选缓存(`enableCache`, `cacheKey`)
12
+ - 可选回显(`detailApi`)
13
+ - 支持单选 / 多选
14
+ - `footer` slot 可完全自定义“加载中/没有更多/加载更多”
15
+
16
+ ## 环境要求
17
+
18
+ - Vue 3
19
+ - Element Plus
20
+
21
+ ## 安装
22
+
23
+ 把 `YOUR_PACKAGE_NAME` 替换成你发布到 npm 的包名。
24
+
25
+ ```bash
26
+ npm i YOUR_PACKAGE_NAME
27
+ ```
28
+
29
+ ## 基础使用
30
+
31
+ ```vue
32
+ <template>
33
+ <SelectPagination v-model="value" :api="fetchUsers" placeholder="输入关键词搜索" />
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import { ref } from 'vue'
38
+ import SelectPagination from 'YOUR_PACKAGE_NAME'
39
+
40
+ const value = ref('')
41
+
42
+ const fetchUsers = async (params: any) => {
43
+ // params = { page, size, name, ...你额外的 params }
44
+ // 必须在 responsePath(默认:data.data)对应的位置返回 { content, totalElements }
45
+ return {
46
+ data: {
47
+ data: {
48
+ content: [
49
+ { id: 'u-1', label: '用户 1', value: 'u-1' },
50
+ { id: 'u-2', label: '用户 2', value: 'u-2' }
51
+ ],
52
+ totalElements: 2
53
+ }
54
+ }
55
+ }
56
+ }
57
+ </script>
58
+ ```
59
+
60
+ ## 接口返回格式
61
+
62
+ 默认 `responsePath = "data.data"`,该路径需要指向如下对象:
63
+
64
+ ```ts
65
+ {
66
+ content: any[]
67
+ totalElements: number
68
+ }
69
+ ```
70
+
71
+ 如果你的后端返回结构不同,调整 `responsePath` 即可。
72
+
73
+ ## Props
74
+
75
+ | 参数 | 类型 | 默认值 | 说明 |
76
+ | --- | --- | --- | --- |
77
+ | modelValue | `string \| number \| any[]` | `''` | v-model |
78
+ | isRemote | `boolean` | `true` | 是否启用 Element Plus 的 remote 模式(`:remote`) |
79
+ | api | `(params) => any` 或 `() => (params) => any` | - | 拉取列表数据的 API |
80
+ | apiAsync | `boolean` | `false` | 为 `true` 时,`api` 视为工厂函数(`() => apiFn`) |
81
+ | params | `object \| () => object` | `{}` | 额外查询参数(会合并进请求参数) |
82
+ | labelField | `string` | `'label'` | label 字段名 |
83
+ | valueField | `string` | `'value'` | value 字段名 |
84
+ | pageSize | `number` | `10` | 每页数量 |
85
+ | pageKey | `string` | `'page'` | 页码参数名 |
86
+ | sizeKey | `string` | `'size'` | 每页大小参数名 |
87
+ | keyword | `string` | `'name'` | 搜索关键词参数名 |
88
+ | responsePath | `string` | `'data.data'` | 响应中 `{ content, totalElements }` 的路径 |
89
+ | enableCache | `boolean` | `true` | 是否启用缓存 |
90
+ | cacheKey | `string` | `''` | 缓存命名空间;为空时由 `api.name + params` 生成 |
91
+ | changeDetail | `boolean` | `false` | 是否在 change 时额外返回选中项详情 |
92
+ | detailApi | `(value) => any` | `null` | 回显用详情接口(通过 value 拉取对应 option 数据) |
93
+ | initOptions | `any[]` | `[]` | 初始 options,会合并进 options 列表 |
94
+ | popperClass | `string` | `''` | 额外 popper class(组件样式基于 `select-pagination-popper` 作用域) |
95
+ | footerSentinelId | `string` | `''` | 自定义 footer sentinel 的 DOM id(用于触底加载定位) |
96
+
97
+ ## 事件
98
+
99
+ | 事件 | 参数 | 说明 |
100
+ | --- | --- | --- |
101
+ | update:modelValue | `any` | v-model 更新 |
102
+ | change | `any` | 选中值变化 |
103
+ | change-detail | `any \| any[] \| null` | `changeDetail = true` 时返回选中项详情(单选/多选) |
104
+
105
+ ## Slots
106
+
107
+ ### footer
108
+
109
+ slot 参数:
110
+
111
+ - `loading`: `boolean`
112
+ - `hasMore`: `boolean`
113
+ - `options`: `any[]`
114
+ - `loadMore`: `() => void`
115
+
116
+ 示例:
117
+
118
+ ```vue
119
+ <SelectPagination v-model="value" :api="fetchUsers" footer-sentinel-id="user-select-footer">
120
+ <template #footer="{ loading, hasMore, loadMore }">
121
+ <div v-if="loading" style="padding: 8px 0; text-align: center">加载中...</div>
122
+ <div v-else-if="!hasMore" style="padding: 8px 0; text-align: center">没有更多了</div>
123
+ <div v-else style="padding: 8px 0; text-align: center; cursor: pointer" @click="loadMore">
124
+ 点击加载更多
125
+ </div>
126
+ </template>
127
+ </SelectPagination>
128
+ ```
129
+
130
+ ## 暴露方法(ref)
131
+
132
+ 通过组件 ref 访问:
133
+
134
+ - `loadData(isReset?: boolean)`
135
+ - `clearCache()`
136
+ - `restoreCache()`
137
+ - `updateValue(val)`
138
+ - `loadEchoData()`
139
+ - `setPage(page: number)`
140
+ - `setOptions(options: any[])`
141
+ - `options`(ref)
142
+ - `currentPage`(ref)
143
+
@@ -0,0 +1,215 @@
1
+ import { PropType } from 'vue';
2
+ type ApiParams = Record<string, any>;
3
+ type ApiFunction = (params: ApiParams) => any;
4
+ type ApiFactory = () => ApiFunction | Promise<ApiFunction>;
5
+ type ApiProp = ApiFunction | ApiFactory;
6
+ type ParamsSource = ApiParams | (() => ApiParams);
7
+ type DetailApiFunction = (value: any) => any;
8
+ declare var __VLS_16: {
9
+ loading: boolean;
10
+ hasMore: boolean;
11
+ options: any[];
12
+ loadMore: () => void;
13
+ };
14
+ type __VLS_Slots = {} & {
15
+ footer?: (props: typeof __VLS_16) => any;
16
+ };
17
+ declare const __VLS_component: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
18
+ isRemote: {
19
+ type: BooleanConstructor;
20
+ default: boolean;
21
+ };
22
+ modelValue: {
23
+ type: PropType<string | number | Array<any>>;
24
+ default: string;
25
+ };
26
+ api: {
27
+ type: PropType<ApiProp>;
28
+ required: false;
29
+ };
30
+ apiAsync: {
31
+ type: BooleanConstructor;
32
+ default: boolean;
33
+ };
34
+ params: {
35
+ type: PropType<ParamsSource>;
36
+ default: () => {};
37
+ };
38
+ labelField: {
39
+ type: StringConstructor;
40
+ default: string;
41
+ };
42
+ valueField: {
43
+ type: StringConstructor;
44
+ default: string;
45
+ };
46
+ pageSize: {
47
+ type: NumberConstructor;
48
+ default: number;
49
+ };
50
+ pageKey: {
51
+ type: StringConstructor;
52
+ default: string;
53
+ };
54
+ sizeKey: {
55
+ type: StringConstructor;
56
+ default: string;
57
+ };
58
+ keyword: {
59
+ type: StringConstructor;
60
+ default: string;
61
+ };
62
+ responsePath: {
63
+ type: StringConstructor;
64
+ default: string;
65
+ };
66
+ enableCache: {
67
+ type: BooleanConstructor;
68
+ default: boolean;
69
+ };
70
+ cacheKey: {
71
+ type: StringConstructor;
72
+ default: string;
73
+ };
74
+ changeDetail: {
75
+ type: BooleanConstructor;
76
+ default: boolean;
77
+ };
78
+ detailApi: {
79
+ type: PropType<DetailApiFunction | null>;
80
+ default: null;
81
+ };
82
+ initOptions: {
83
+ type: PropType<any[]>;
84
+ default: () => never[];
85
+ };
86
+ popperClass: {
87
+ type: StringConstructor;
88
+ default: string;
89
+ };
90
+ footerSentinelId: {
91
+ type: StringConstructor;
92
+ default: string;
93
+ };
94
+ }>, {
95
+ loadData: (isReset?: boolean) => Promise<void>;
96
+ clearCache: () => void;
97
+ restoreCache: () => boolean;
98
+ updateValue: (val: any) => void;
99
+ loadEchoData: () => Promise<void>;
100
+ options: import("vue").Ref<any[], any[]>;
101
+ currentPage: import("vue").Ref<number, number>;
102
+ setPage: (page: number) => void;
103
+ setOptions: (newOptions: any[]) => void;
104
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
105
+ "update:modelValue": (...args: any[]) => void;
106
+ change: (...args: any[]) => void;
107
+ "change-detail": (...args: any[]) => void;
108
+ }, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
109
+ isRemote: {
110
+ type: BooleanConstructor;
111
+ default: boolean;
112
+ };
113
+ modelValue: {
114
+ type: PropType<string | number | Array<any>>;
115
+ default: string;
116
+ };
117
+ api: {
118
+ type: PropType<ApiProp>;
119
+ required: false;
120
+ };
121
+ apiAsync: {
122
+ type: BooleanConstructor;
123
+ default: boolean;
124
+ };
125
+ params: {
126
+ type: PropType<ParamsSource>;
127
+ default: () => {};
128
+ };
129
+ labelField: {
130
+ type: StringConstructor;
131
+ default: string;
132
+ };
133
+ valueField: {
134
+ type: StringConstructor;
135
+ default: string;
136
+ };
137
+ pageSize: {
138
+ type: NumberConstructor;
139
+ default: number;
140
+ };
141
+ pageKey: {
142
+ type: StringConstructor;
143
+ default: string;
144
+ };
145
+ sizeKey: {
146
+ type: StringConstructor;
147
+ default: string;
148
+ };
149
+ keyword: {
150
+ type: StringConstructor;
151
+ default: string;
152
+ };
153
+ responsePath: {
154
+ type: StringConstructor;
155
+ default: string;
156
+ };
157
+ enableCache: {
158
+ type: BooleanConstructor;
159
+ default: boolean;
160
+ };
161
+ cacheKey: {
162
+ type: StringConstructor;
163
+ default: string;
164
+ };
165
+ changeDetail: {
166
+ type: BooleanConstructor;
167
+ default: boolean;
168
+ };
169
+ detailApi: {
170
+ type: PropType<DetailApiFunction | null>;
171
+ default: null;
172
+ };
173
+ initOptions: {
174
+ type: PropType<any[]>;
175
+ default: () => never[];
176
+ };
177
+ popperClass: {
178
+ type: StringConstructor;
179
+ default: string;
180
+ };
181
+ footerSentinelId: {
182
+ type: StringConstructor;
183
+ default: string;
184
+ };
185
+ }>> & Readonly<{
186
+ "onUpdate:modelValue"?: ((...args: any[]) => any) | undefined;
187
+ onChange?: ((...args: any[]) => any) | undefined;
188
+ "onChange-detail"?: ((...args: any[]) => any) | undefined;
189
+ }>, {
190
+ isRemote: boolean;
191
+ modelValue: string | number | any[];
192
+ apiAsync: boolean;
193
+ params: ParamsSource;
194
+ labelField: string;
195
+ valueField: string;
196
+ pageSize: number;
197
+ pageKey: string;
198
+ sizeKey: string;
199
+ keyword: string;
200
+ responsePath: string;
201
+ enableCache: boolean;
202
+ cacheKey: string;
203
+ changeDetail: boolean;
204
+ detailApi: DetailApiFunction | null;
205
+ initOptions: any[];
206
+ popperClass: string;
207
+ footerSentinelId: string;
208
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
209
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
210
+ export default _default;
211
+ type __VLS_WithSlots<T, S> = T & {
212
+ new (): {
213
+ $slots: S;
214
+ };
215
+ };
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});const a=require("vue"),le=require("lodash-es"),ne={style:{width:"100%"}},oe=["id"],re={key:0,class:"loading-more"},se={key:1,class:"no-more"},ie="select-pagination-popper",ue=a.defineComponent({__name:"SelectPagination",props:{isRemote:{type:Boolean,default:!0},modelValue:{type:[String,Number,Array],default:""},api:{type:Function,required:!1},apiAsync:{type:Boolean,default:!1},params:{type:[Object,Function],default:()=>({})},labelField:{type:String,default:"label"},valueField:{type:String,default:"value"},pageSize:{type:Number,default:10},pageKey:{type:String,default:"page"},sizeKey:{type:String,default:"size"},keyword:{type:String,default:"name"},responsePath:{type:String,default:"data.data"},enableCache:{type:Boolean,default:!0},cacheKey:{type:String,default:""},changeDetail:{type:Boolean,default:!1},detailApi:{type:Function,default:null},initOptions:{type:Array,default:()=>[]},popperClass:{type:String,default:""},footerSentinelId:{type:String,default:""}},emits:["update:modelValue","change","change-detail"],setup(v,{expose:C,emit:y}){var K;const V=new Map,t=v,g=y,_=a.useAttrs(),z=((K=a.getCurrentInstance())==null?void 0:K.uid)??Date.now(),I=a.computed(()=>t.footerSentinelId||`select-pagination-footer-${z}`),T=a.computed(()=>{const e=_["popper-class"]??_.popperClass;return typeof e=="string"?e:""}),L=a.computed(()=>[T.value,t.popperClass,ie].filter(Boolean).join(" ")),k=e=>{if(S.value=!0,g("update:modelValue",e),g("change",e),e&&t.changeDetail){let l=null;Array.isArray(e)?l=r.value.filter(n=>e.find(o=>n[d.value]===o)):l=r.value.find(n=>n[d.value]===e),g("change-detail",l)}},r=a.ref([]),s=a.ref(!1),i=a.ref(!0),c=a.ref(1),m=a.ref(""),D=a.ref(!1),S=a.ref(!1),B=V,O=()=>{var n;if(t.cacheKey)return t.cacheKey;const e=((n=t==null?void 0:t.api)==null?void 0:n.name)||"anonymous",l=JSON.stringify(t.params);return`${e}_${btoa(l)}`},R=()=>{if(!t.enableCache)return;const e=O();B.set(e,{options:[...r.value],currentPage:c.value,hasMore:i.value,keyword:m.value})},P=()=>{if(!t.enableCache)return!1;const e=O(),l=B.get(e);return l?(r.value=[...l.options],c.value=l.currentPage,i.value=l.hasMore,m.value=l.keyword,!0):!1},b=()=>{if(!t.enableCache)return;const e=O();B.delete(e)},j=a.computed(()=>t.labelField),d=a.computed(()=>t.valueField),U=(e,l)=>l.split(".").reduce((n,o)=>{if(n!=null)return n[o]},e),E=a.computed(()=>t.isRemote&&typeof t.api=="function");let p=null,h=null;const A=()=>{h&&(h.disconnect(),h=null),p=null},H=e=>{if(!e)return null;let l=e.parentElement;for(;l;){const o=window.getComputedStyle(l).overflowY;if((o==="auto"||o==="scroll"||o==="overlay")&&l.scrollHeight>l.clientHeight)return l;l=l.parentElement}return null},Y=async()=>{if(A(),!E.value)return;await a.nextTick();const e=document.getElementById(I.value);e&&(p=H(e),h=new IntersectionObserver(l=>{const n=l[0];n!=null&&n.isIntersecting&&E.value&&(s.value||i.value&&f())},{root:p,rootMargin:"0px 0px 120px 0px",threshold:0}),h.observe(e))},J=(e=!0)=>{r.value=e&&t.initOptions&&t.initOptions.length>0?[...t.initOptions]:[],c.value=1,i.value=!0,m.value="",s.value=!1},f=async(e=!1)=>{if(s.value)return;let l=0;e||p&&(l=p.scrollTop),e&&(r.value=[],c.value=1,i.value=!0,b()),s.value=!0;try{const o={...(typeof t.params=="function"?t.params():t.params)||{},[t.pageKey]:c.value,[t.sizeKey]:t.pageSize,[t.keyword]:m.value};if(!t.api)return;const u=await(t.apiAsync?await t.api():t.api)(o),F=U(u,t.responsePath);if(F){const q=F.content??[],ee=F.totalElements??0,te=r.value.length;if(e)r.value=q||[];else{const ae=W(q||[]);r.value=[...r.value,...ae]}i.value=r.value.length<ee,i.value&&c.value++,R(),!e&&p&&te>0&&(await a.nextTick(),requestAnimationFrame(()=>{p&&(p.scrollTop=l)}))}else console.error("响应数据格式不正确",u)}catch(n){console.error("加载数据失败",n)}finally{s.value=!1}},w=a.ref(!1),G=e=>{w.value||m.value!==e&&(m.value=e,f(!0))},Q=e=>{e?(!P()&&r.value.length===0&&f(!0),Y()):A()},M=async()=>{const e=Array.isArray(t.modelValue)?t.modelValue.length>0:!!t.modelValue;if(!(!t.detailApi||!e||D.value))try{s.value=!0;let l=[];if(Array.isArray(t.modelValue)){for(const n of t.modelValue)if(n){const o=await t.detailApi(n);o&&o.data&&l.push(o.data)}}else{let o=await t.detailApi(t.modelValue);for(;o&&!o.id&&o.data;)o=o.data;o&&o.id&&(l=[o])}l.length>0&&(r.value=[...l,...r.value]),D.value=!0}catch(l){console.error("加载回显数据失败",l)}finally{s.value=!1}},W=e=>{const l=new Set(r.value.map(n=>n[d.value]));return e.filter(n=>!l.has(n[d.value]))},X=async()=>{t.initOptions&&t.initOptions.length>0&&(r.value=[...t.initOptions]),P()||(await M(),t.modelValue&&r.value.length===0&&await f(!0))};a.onMounted(()=>{X()}),a.watch(()=>t.api,(e,l)=>{e!==l&&(J(!0),b(),l&&!e&&(g("update:modelValue",""),g("change","")),t.modelValue&&e&&f(!0))},{immediate:!1}),a.watch(()=>t.params,(e,l)=>{le.isEqual(e,l)||(b(),t.initOptions&&t.initOptions.length>0&&(r.value=[...t.initOptions]),f(!0),_.multiple?k([]):k(""))},{deep:!0}),a.watch(()=>t.modelValue,async(e,l)=>{e&&e!==l&&t.detailApi&&!S.value&&await M(),S.value&&(S.value=!1)},{immediate:!1});const N=()=>{!s.value&&i.value&&f()},Z=()=>{w.value&&(w.value=!1)};return a.watch(()=>t.initOptions,e=>{if(e&&e.length>0){const l=new Set(r.value.map(o=>o[d.value])),n=e.filter(o=>!l.has(o[d.value]));r.value=[...n,...r.value]}},{immediate:!0,deep:!0}),C({loadData:f,clearCache:b,restoreCache:P,updateValue:k,loadEchoData:M,options:r,currentPage:c,setPage:e=>{c.value=e},setOptions:e=>{r.value=[...e]}}),a.onBeforeUnmount(()=>{A()}),(e,l)=>{const n=a.resolveComponent("el-option"),o=a.resolveComponent("el-select"),x=a.resolveDirective("loading");return a.withDirectives((a.openBlock(),a.createElementBlock("div",ne,[a.createVNode(o,a.mergeProps({"model-value":v.modelValue,"onUpdate:modelValue":k},e.$attrs,{"popper-class":L.value,"remote-method":G,filterable:!0,remote:v.isRemote,onVisibleChange:Q,onCompositionstart:l[0]||(l[0]=u=>w.value=!0),onCompositionend:Z,clearable:"","remote-show-suffix":""}),{default:a.withCtx(()=>[(a.openBlock(!0),a.createElementBlock(a.Fragment,null,a.renderList(r.value,u=>(a.openBlock(),a.createBlock(n,{key:u[d.value]??u.id,label:u[j.value],value:u[d.value],disabled:u.disabled},null,8,["label","value","disabled"]))),128)),a.createElementVNode("div",{id:I.value},[a.renderSlot(e.$slots,"footer",{loading:s.value,hasMore:i.value,options:r.value,loadMore:N},()=>[E.value&&r.value.length>0?(a.openBlock(),a.createElementBlock(a.Fragment,{key:0},[s.value?(a.openBlock(),a.createElementBlock("div",re,"加载中...")):i.value?(a.openBlock(),a.createElementBlock("div",{key:2,class:"more-hint",onClick:N},[...l[1]||(l[1]=[a.createElementVNode("span",{class:"more-text"},"点击或下拉加载更多",-1)])])):(a.openBlock(),a.createElementBlock("div",se,"没有更多数据"))],64)):a.createCommentVNode("",!0)],!0)],8,oe)]),_:3},16,["model-value","popper-class","remote"])])),[[x,s.value]])}}}),ce=(v,C)=>{const y=v.__vccOpts||v;for(const[V,t]of C)y[V]=t;return y},$=ce(ue,[["__scopeId","data-v-88571e50"]]);exports.SelectPagination=$;exports.default=$;
@@ -0,0 +1,3 @@
1
+ import SelectPagination from './SelectPagination.vue';
2
+ export { SelectPagination };
3
+ export default SelectPagination;
package/dist/index.js ADDED
@@ -0,0 +1,287 @@
1
+ import { defineComponent as ce, useAttrs as de, getCurrentInstance as pe, computed as h, ref as f, onMounted as fe, watch as k, onBeforeUnmount as ve, resolveComponent as T, resolveDirective as me, withDirectives as ge, openBlock as v, createElementBlock as y, createVNode as he, mergeProps as ye, withCtx as Ce, Fragment as U, renderList as be, createBlock as Se, createElementVNode as j, renderSlot as we, createCommentVNode as Ve, nextTick as H } from "vue";
2
+ import { isEqual as Oe } from "lodash-es";
3
+ const _e = { style: { width: "100%" } }, ke = ["id"], Pe = {
4
+ key: 0,
5
+ class: "loading-more"
6
+ }, Ae = {
7
+ key: 1,
8
+ class: "no-more"
9
+ }, Me = "select-pagination-popper", xe = /* @__PURE__ */ ce({
10
+ __name: "SelectPagination",
11
+ props: {
12
+ isRemote: { type: Boolean, default: !0 },
13
+ modelValue: {
14
+ type: [String, Number, Array],
15
+ default: ""
16
+ },
17
+ api: { type: Function, required: !1 },
18
+ apiAsync: { type: Boolean, default: !1 },
19
+ params: {
20
+ type: [Object, Function],
21
+ default: () => ({})
22
+ },
23
+ labelField: { type: String, default: "label" },
24
+ valueField: { type: String, default: "value" },
25
+ pageSize: { type: Number, default: 10 },
26
+ pageKey: { type: String, default: "page" },
27
+ sizeKey: { type: String, default: "size" },
28
+ keyword: { type: String, default: "name" },
29
+ responsePath: { type: String, default: "data.data" },
30
+ enableCache: { type: Boolean, default: !0 },
31
+ cacheKey: { type: String, default: "" },
32
+ changeDetail: { type: Boolean, default: !1 },
33
+ detailApi: {
34
+ type: Function,
35
+ default: null
36
+ },
37
+ initOptions: { type: Array, default: () => [] },
38
+ popperClass: { type: String, default: "" },
39
+ footerSentinelId: { type: String, default: "" }
40
+ },
41
+ emits: ["update:modelValue", "change", "change-detail"],
42
+ setup(m, { expose: P, emit: S }) {
43
+ var L;
44
+ const A = /* @__PURE__ */ new Map(), t = m, C = S, M = de(), Y = ((L = pe()) == null ? void 0 : L.uid) ?? Date.now(), $ = h(
45
+ () => t.footerSentinelId || `select-pagination-footer-${Y}`
46
+ ), J = h(() => {
47
+ const e = M["popper-class"] ?? M.popperClass;
48
+ return typeof e == "string" ? e : "";
49
+ }), G = h(() => [J.value, t.popperClass, Me].filter(Boolean).join(" ")), w = (e) => {
50
+ if (V.value = !0, C("update:modelValue", e), C("change", e), e && t.changeDetail) {
51
+ let a = null;
52
+ Array.isArray(e) ? a = o.value.filter((l) => e.find((n) => l[c.value] === n)) : a = o.value.find((l) => l[c.value] === e), C("change-detail", a);
53
+ }
54
+ }, o = f([]), r = f(!1), s = f(!0), u = f(1), g = f(""), z = f(!1), V = f(!1), x = A, I = () => {
55
+ var l;
56
+ if (t.cacheKey)
57
+ return t.cacheKey;
58
+ const e = ((l = t == null ? void 0 : t.api) == null ? void 0 : l.name) || "anonymous", a = JSON.stringify(t.params);
59
+ return `${e}_${btoa(a)}`;
60
+ }, Q = () => {
61
+ if (!t.enableCache) return;
62
+ const e = I();
63
+ x.set(e, {
64
+ options: [...o.value],
65
+ currentPage: u.value,
66
+ hasMore: s.value,
67
+ keyword: g.value
68
+ });
69
+ }, E = () => {
70
+ if (!t.enableCache) return !1;
71
+ const e = I(), a = x.get(e);
72
+ return a ? (o.value = [...a.options], u.value = a.currentPage, s.value = a.hasMore, g.value = a.keyword, !0) : !1;
73
+ }, O = () => {
74
+ if (!t.enableCache) return;
75
+ const e = I();
76
+ x.delete(e);
77
+ }, W = h(() => t.labelField), c = h(() => t.valueField), X = (e, a) => a.split(".").reduce((l, n) => {
78
+ if (l != null)
79
+ return l[n];
80
+ }, e), F = h(() => t.isRemote && typeof t.api == "function");
81
+ let d = null, b = null;
82
+ const B = () => {
83
+ b && (b.disconnect(), b = null), d = null;
84
+ }, Z = (e) => {
85
+ if (!e) return null;
86
+ let a = e.parentElement;
87
+ for (; a; ) {
88
+ const n = window.getComputedStyle(a).overflowY;
89
+ if ((n === "auto" || n === "scroll" || n === "overlay") && a.scrollHeight > a.clientHeight) return a;
90
+ a = a.parentElement;
91
+ }
92
+ return null;
93
+ }, ee = async () => {
94
+ if (B(), !F.value) return;
95
+ await H();
96
+ const e = document.getElementById($.value);
97
+ e && (d = Z(e), b = new IntersectionObserver(
98
+ (a) => {
99
+ const l = a[0];
100
+ l != null && l.isIntersecting && F.value && (r.value || s.value && p());
101
+ },
102
+ {
103
+ root: d,
104
+ rootMargin: "0px 0px 120px 0px",
105
+ threshold: 0
106
+ }
107
+ ), b.observe(e));
108
+ }, te = (e = !0) => {
109
+ o.value = e && t.initOptions && t.initOptions.length > 0 ? [...t.initOptions] : [], u.value = 1, s.value = !0, g.value = "", r.value = !1;
110
+ }, p = async (e = !1) => {
111
+ if (r.value) return;
112
+ let a = 0;
113
+ e || d && (a = d.scrollTop), e && (o.value = [], u.value = 1, s.value = !0, O()), r.value = !0;
114
+ try {
115
+ const n = {
116
+ ...(typeof t.params == "function" ? t.params() : t.params) || {},
117
+ [t.pageKey]: u.value,
118
+ [t.sizeKey]: t.pageSize,
119
+ [t.keyword]: g.value
120
+ };
121
+ if (!t.api) return;
122
+ const i = await (t.apiAsync ? await t.api() : t.api)(n), K = X(i, t.responsePath);
123
+ if (K) {
124
+ const R = K.content ?? [], se = K.totalElements ?? 0, ie = o.value.length;
125
+ if (e)
126
+ o.value = R || [];
127
+ else {
128
+ const ue = ne(R || []);
129
+ o.value = [...o.value, ...ue];
130
+ }
131
+ s.value = o.value.length < se, s.value && u.value++, Q(), !e && d && ie > 0 && (await H(), requestAnimationFrame(() => {
132
+ d && (d.scrollTop = a);
133
+ }));
134
+ } else
135
+ console.error("响应数据格式不正确", i);
136
+ } catch (l) {
137
+ console.error("加载数据失败", l);
138
+ } finally {
139
+ r.value = !1;
140
+ }
141
+ }, _ = f(!1), ae = (e) => {
142
+ _.value || g.value !== e && (g.value = e, p(!0));
143
+ }, le = (e) => {
144
+ e ? (!E() && o.value.length === 0 && p(!0), ee()) : B();
145
+ }, D = async () => {
146
+ const e = Array.isArray(t.modelValue) ? t.modelValue.length > 0 : !!t.modelValue;
147
+ if (!(!t.detailApi || !e || z.value))
148
+ try {
149
+ r.value = !0;
150
+ let a = [];
151
+ if (Array.isArray(t.modelValue)) {
152
+ for (const l of t.modelValue)
153
+ if (l) {
154
+ const n = await t.detailApi(l);
155
+ n && n.data && a.push(n.data);
156
+ }
157
+ } else {
158
+ let n = await t.detailApi(t.modelValue);
159
+ for (; n && !n.id && n.data; )
160
+ n = n.data;
161
+ n && n.id && (a = [n]);
162
+ }
163
+ a.length > 0 && (o.value = [...a, ...o.value]), z.value = !0;
164
+ } catch (a) {
165
+ console.error("加载回显数据失败", a);
166
+ } finally {
167
+ r.value = !1;
168
+ }
169
+ }, ne = (e) => {
170
+ const a = new Set(o.value.map((l) => l[c.value]));
171
+ return e.filter((l) => !a.has(l[c.value]));
172
+ }, oe = async () => {
173
+ t.initOptions && t.initOptions.length > 0 && (o.value = [...t.initOptions]), E() || (await D(), t.modelValue && o.value.length === 0 && await p(!0));
174
+ };
175
+ fe(() => {
176
+ oe();
177
+ }), k(
178
+ () => t.api,
179
+ (e, a) => {
180
+ e !== a && (te(!0), O(), a && !e && (C("update:modelValue", ""), C("change", "")), t.modelValue && e && p(!0));
181
+ },
182
+ { immediate: !1 }
183
+ ), k(
184
+ () => t.params,
185
+ (e, a) => {
186
+ Oe(e, a) || (O(), t.initOptions && t.initOptions.length > 0 && (o.value = [...t.initOptions]), p(!0), M.multiple ? w([]) : w(""));
187
+ },
188
+ { deep: !0 }
189
+ ), k(
190
+ () => t.modelValue,
191
+ async (e, a) => {
192
+ e && e !== a && t.detailApi && !V.value && await D(), V.value && (V.value = !1);
193
+ },
194
+ { immediate: !1 }
195
+ );
196
+ const q = () => {
197
+ !r.value && s.value && p();
198
+ }, re = () => {
199
+ _.value && (_.value = !1);
200
+ };
201
+ return k(
202
+ () => t.initOptions,
203
+ (e) => {
204
+ if (e && e.length > 0) {
205
+ const a = new Set(o.value.map((n) => n[c.value])), l = e.filter(
206
+ (n) => !a.has(n[c.value])
207
+ );
208
+ o.value = [...l, ...o.value];
209
+ }
210
+ },
211
+ { immediate: !0, deep: !0 }
212
+ ), P({
213
+ loadData: p,
214
+ clearCache: O,
215
+ restoreCache: E,
216
+ updateValue: w,
217
+ loadEchoData: D,
218
+ options: o,
219
+ currentPage: u,
220
+ setPage: (e) => {
221
+ u.value = e;
222
+ },
223
+ setOptions: (e) => {
224
+ o.value = [...e];
225
+ }
226
+ }), ve(() => {
227
+ B();
228
+ }), (e, a) => {
229
+ const l = T("el-option"), n = T("el-select"), N = me("loading");
230
+ return ge((v(), y("div", _e, [
231
+ he(n, ye({
232
+ "model-value": m.modelValue,
233
+ "onUpdate:modelValue": w
234
+ }, e.$attrs, {
235
+ "popper-class": G.value,
236
+ "remote-method": ae,
237
+ filterable: !0,
238
+ remote: m.isRemote,
239
+ onVisibleChange: le,
240
+ onCompositionstart: a[0] || (a[0] = (i) => _.value = !0),
241
+ onCompositionend: re,
242
+ clearable: "",
243
+ "remote-show-suffix": ""
244
+ }), {
245
+ default: Ce(() => [
246
+ (v(!0), y(U, null, be(o.value, (i) => (v(), Se(l, {
247
+ key: i[c.value] ?? i.id,
248
+ label: i[W.value],
249
+ value: i[c.value],
250
+ disabled: i.disabled
251
+ }, null, 8, ["label", "value", "disabled"]))), 128)),
252
+ j("div", { id: $.value }, [
253
+ we(e.$slots, "footer", {
254
+ loading: r.value,
255
+ hasMore: s.value,
256
+ options: o.value,
257
+ loadMore: q
258
+ }, () => [
259
+ F.value && o.value.length > 0 ? (v(), y(U, { key: 0 }, [
260
+ r.value ? (v(), y("div", Pe, "加载中...")) : s.value ? (v(), y("div", {
261
+ key: 2,
262
+ class: "more-hint",
263
+ onClick: q
264
+ }, [...a[1] || (a[1] = [
265
+ j("span", { class: "more-text" }, "点击或下拉加载更多", -1)
266
+ ])])) : (v(), y("div", Ae, "没有更多数据"))
267
+ ], 64)) : Ve("", !0)
268
+ ], !0)
269
+ ], 8, ke)
270
+ ]),
271
+ _: 3
272
+ }, 16, ["model-value", "popper-class", "remote"])
273
+ ])), [
274
+ [N, r.value]
275
+ ]);
276
+ };
277
+ }
278
+ }), Ie = (m, P) => {
279
+ const S = m.__vccOpts || m;
280
+ for (const [A, t] of P)
281
+ S[A] = t;
282
+ return S;
283
+ }, Ne = /* @__PURE__ */ Ie(xe, [["__scopeId", "data-v-88571e50"]]);
284
+ export {
285
+ Ne as SelectPagination,
286
+ Ne as default
287
+ };
package/dist/style.css ADDED
@@ -0,0 +1 @@
1
+ .loading-more[data-v-88571e50],.no-more[data-v-88571e50],.more-hint[data-v-88571e50]{padding:8px 0;font-size:12px;color:#909399;text-align:center;background-color:#fff;border-top:rgba(144,147,153,.1) solid 1px}.more-hint[data-v-88571e50]{font-size:12px;color:#409eff;cursor:pointer;transition:background-color .3s}.more-hint[data-v-88571e50]:hover{background-color:#f5f7fa}.more-text[data-v-88571e50]{display:inline-block;padding:2px 0}[data-v-88571e50] .select-pagination-popper .el-scrollbar__wrap{max-height:274px;overflow:auto;scroll-behavior:smooth;transition:height .2s ease}[data-v-88571e50] .select-pagination-popper .el-scrollbar__view{padding-bottom:8px}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "select-pagination-element-plus",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "files": [
6
+ "dist",
7
+ "README.md",
8
+ "README.zh-CN.md"
9
+ ],
10
+ "main": "./dist/index.cjs",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js",
17
+ "require": "./dist/index.cjs"
18
+ }
19
+ },
20
+ "scripts": {
21
+ "build": "vite build && vue-tsc -p tsconfig.build.json --declaration --emitDeclarationOnly --declarationDir dist",
22
+ "typecheck": "vue-tsc --noEmit",
23
+ "prepublishOnly": "npm run build"
24
+ },
25
+ "dependencies": {
26
+ "lodash-es": "^4.17.21"
27
+ },
28
+ "peerDependencies": {
29
+ "element-plus": "^2.0.0",
30
+ "vue": "^3.0.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^22.7.5",
34
+ "@vitejs/plugin-vue": "^5.1.4",
35
+ "element-plus": "^2.7.8",
36
+ "typescript": "^5.6.3",
37
+ "vite": "^5.4.8",
38
+ "vue": "^3.5.13",
39
+ "vue-tsc": "^2.1.6"
40
+ }
41
+ }
42
+