plain-design 1.0.0-beta.78 → 1.0.0-beta.79

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plain-design",
3
- "version": "1.0.0-beta.78",
3
+ "version": "1.0.0-beta.79",
4
4
  "description": "",
5
5
  "main": "dist/plain-design.min.js",
6
6
  "module": "dist/plain-design.commonjs.min.js",
@@ -0,0 +1,32 @@
1
+ import {designPage} from "plain-design-composition";
2
+ import {i18n} from "../i18n";
3
+
4
+ export const SearchFooter = designPage(() => {
5
+ return () => (
6
+ <>
7
+ <svg width="15" height="15" aria-label="Enter key" role="img">
8
+ <g fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.2">
9
+ <path d="M12 3.53088v3c0 1-1 2-2 2H4M7 11.53088l-3-3 3-3"></path>
10
+ </g>
11
+ </svg>
12
+ <span>{i18n.$it('search.select').d('选择')}</span>
13
+ <svg width="15" height="15" aria-label="Arrow down" role="img">
14
+ <g fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.2">
15
+ <path d="M7.5 3.5v8M10.5 8.5l-3 3-3-3"></path>
16
+ </g>
17
+ </svg>
18
+ <svg width="15" height="15" aria-label="Arrow up" role="img">
19
+ <g fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.2">
20
+ <path d="M7.5 11.5v-8M10.5 6.5l-3-3-3 3"></path>
21
+ </g>
22
+ </svg>
23
+ <span>{i18n.$it('search.switch').d('切换')}</span>
24
+ <svg width="15" height="15" aria-label="Escape key" role="img">
25
+ <g fill="none" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.2">
26
+ <path d="M13.6167 8.936c-.1065.3583-.6883.962-1.4875.962-.7993 0-1.653-.9165-1.653-2.1258v-.5678c0-1.2548.7896-2.1016 1.653-2.1016.8634 0 1.3601.4778 1.4875 1.0724M9 6c-.1352-.4735-.7506-.9219-1.46-.8972-.7092.0246-1.344.57-1.344 1.2166s.4198.8812 1.3445.9805C8.465 7.3992 8.968 7.9337 9 8.5c.032.5663-.454 1.398-1.4595 1.398C6.6593 9.898 6 9 5.963 8.4851m-1.4748.5368c-.2635.5941-.8099.876-1.5443.876s-1.7073-.6248-1.7073-2.204v-.4603c0-1.0416.721-2.131 1.7073-2.131.9864 0 1.6425 1.031 1.5443 2.2492h-2.956"></path>
27
+ </g>
28
+ </svg>
29
+ <span>{i18n.$it('base.close').d('关闭')}</span>
30
+ </>
31
+ );
32
+ });
@@ -0,0 +1,206 @@
1
+ import {computed, designComponent, iMouseEvent, PropType, reactive, useRefs} from "plain-design-composition";
2
+ import {iSearchDataMeta, iSearchServiceConfig, SearchOptionButton, SearchTreeIcon, SearchType2Icon} from "./search.utils";
3
+ import {createListUtils} from "../../utils/createListUtils";
4
+ import {delay} from "plain-utils/utils/delay";
5
+ import VirtualList from "../VirtualList";
6
+
7
+ export const SearchList = designComponent({
8
+ props: {
9
+ data: { type: Array as PropType<iSearchDataMeta[]>, required: true },
10
+ config: { type: Object as PropType<iSearchServiceConfig>, required: true }
11
+ },
12
+ slots: ['default'],
13
+ emits: {
14
+ /*选中选项事件*/
15
+ onSelect: (val: iSearchDataMeta) => true,
16
+ /*删除选项事件,选项可能是历史选项,也可能是收藏选项*/
17
+ onRemoveItem: (val: iSearchDataMeta) => true,
18
+ /*添加选项为收藏选项*/
19
+ onAddFavorite: (val: iSearchDataMeta) => true,
20
+ },
21
+ setup({ props, slots, event: { emit } }) {
22
+
23
+ const { refs, onRef } = useRefs({ virtual: VirtualList });
24
+
25
+ const state = reactive({
26
+ /*当前选中的选项位置索引*/
27
+ selectIndex: null as null | number
28
+ });
29
+
30
+ /*有效可选中的选项数组*/
31
+ const availableList = computed(() => props.data.filter(i => i.type !== 'group'));
32
+ /*选中选项在有效选项数组中的位置索引*/
33
+ const availableIndex = computed(() => state.selectIndex == null ? null : availableList.value.indexOf(props.data[state.selectIndex]));
34
+
35
+ const listUtils = createListUtils({ getList: () => availableList.value, current: () => availableIndex.value, });
36
+
37
+ const methods = {
38
+ /**
39
+ * 选中上一个节点
40
+ * @author 韦胜健
41
+ * @date 2024.6.23 21:27
42
+ */
43
+ selectPrev: async () => {
44
+ const newAvailableIndex = listUtils.prevIndex();
45
+ if (newAvailableIndex == -1) {return;}
46
+ const newSelectIndex = props.data.indexOf(availableList.value[newAvailableIndex]);
47
+ if (!!refs.virtual && availableIndex.value == 0 && newAvailableIndex == availableList.value.length - 1) {
48
+ /*滚动到底部再选中选项*/
49
+ refs.virtual.refs.scroll?.methods.scrollEnd();
50
+ await delay(78);
51
+ } else {
52
+ refs.virtual?.refs.scroll?.methods.showElement(`[data-index="_${newSelectIndex}"]`);
53
+ }
54
+ state.selectIndex = newSelectIndex;
55
+ },
56
+ /**
57
+ * 选中下一个节点
58
+ * @author 韦胜健
59
+ * @date 2024.6.23 21:28
60
+ */
61
+ selectNext: async () => {
62
+ const newAvailableIndex = listUtils.nextIndex();
63
+ if (newAvailableIndex == -1) {return;}
64
+ const newSelectIndex = props.data.indexOf(availableList.value[newAvailableIndex]);
65
+ if (!!refs.virtual && availableIndex.value == availableList.value.length - 1 && newAvailableIndex == 0) {
66
+ /*滚动到顶部再选中选项*/
67
+ refs.virtual.refs.scroll?.methods.scrollTop(0);
68
+ await delay(78);
69
+ } else {
70
+ refs.virtual?.refs.scroll?.methods.showElement(`[data-index="_${newSelectIndex}"]`);
71
+ }
72
+ state.selectIndex = newSelectIndex;
73
+ },
74
+ /**
75
+ * 获取当前选中的节点
76
+ * @author 韦胜健
77
+ * @date 2024.6.23 21:28
78
+ */
79
+ getSelected: (): iSearchDataMeta | null => {
80
+ return state.selectIndex == null ? null : props.data[state.selectIndex];
81
+ },
82
+ /**
83
+ * 选中节点
84
+ * @author 韦胜健
85
+ * @date 2024.6.23 21:28
86
+ */
87
+ selectItem: (dataMeta: iSearchDataMeta) => {
88
+ emit.onSelect(dataMeta);
89
+ },
90
+ /**
91
+ * 删除收藏或者历史
92
+ * @author 韦胜健
93
+ * @date 2024.6.23 21:58
94
+ */
95
+ removeItem: (e: iMouseEvent, dataMeta: iSearchDataMeta) => {
96
+ e.stopPropagation();
97
+ emit.onRemoveItem(dataMeta);
98
+ },
99
+ /**
100
+ * 添加收藏
101
+ * @author 韦胜健
102
+ * @date 2024.6.23 21:58
103
+ */
104
+ addFavorite: (e: iMouseEvent, dataMeta: iSearchDataMeta) => {
105
+ e.stopPropagation();
106
+ emit.onAddFavorite(dataMeta);
107
+ },
108
+ /**
109
+ * 重置选中节点
110
+ * @author 韦胜健
111
+ * @date 2024.6.23 23:42
112
+ */
113
+ resetSelectIndex: async () => {
114
+ await delay();
115
+ state.selectIndex = !availableList.value.length ? null : props.data.indexOf(availableList.value[0]);
116
+ },
117
+ };
118
+
119
+ const renderItem = (
120
+ { item, vid, index }: {
121
+ item: iSearchDataMeta, vid: string, index: number
122
+ }
123
+ ) => {
124
+
125
+ /**
126
+ * 是否为最后一个subHeader,如果返回值为null,表明不是sub_header,否则true为最后一个sub_header,false为普通sub_header
127
+ * @author 韦胜健
128
+ * @date 2024.6.23 23:49
129
+ */
130
+ const isLastSubHeader = (() => {
131
+ if (item.type !== 'sub_header') {return null;}
132
+ const nextItem = index + 1 > props.data.length - 1 ? null : props.data[index + 1];
133
+ return !(!!nextItem && nextItem.type === 'sub_header');
134
+ })();
135
+
136
+ return (
137
+ <div
138
+ className={`search-service-option-item`}
139
+ data-active={String(state.selectIndex === index)}
140
+ key={vid}
141
+ data-vid={vid}
142
+ data-index={`_${index}`}
143
+ onMouseEnter={() => item.type != 'group' && (state.selectIndex = index)}
144
+ >
145
+ {(props.config.render || ((item: iSearchDataMeta) => {
146
+ return (
147
+ <div className="search-service-option-item-default" data-service-item-type={item.type}>
148
+ {item.type == 'group' ? (
149
+ <div className="search-service-option-item-default-title">{item.title}</div>
150
+ ) : (
151
+ <div className="search-service-option-item-default-box" onClick={() => methods.selectItem(item)}>
152
+ {isLastSubHeader != null && SearchTreeIcon[isLastSubHeader ? 'last' : 'normal']()}
153
+ {SearchType2Icon[item.type]()}
154
+ <div className="search-service-option-item-default-label">
155
+ {item.title && <span>{item.title}</span>}
156
+ {item.desc && <span>{item.desc}</span>}
157
+ </div>
158
+ {item.type === 'favorite' ?
159
+ SearchOptionButton.remove((e) => methods.removeItem(e, item)) :
160
+ item.type === 'history' ? <>
161
+ {SearchOptionButton.favorite((e) => methods.addFavorite(e, item))}
162
+ {SearchOptionButton.remove((e) => methods.removeItem(e, item))}
163
+ </> : SearchOptionButton.normal()}
164
+ </div>
165
+ )}
166
+ </div>
167
+ );
168
+ }))(item)}
169
+ </div>
170
+ );
171
+ };
172
+
173
+ return {
174
+ refer: {
175
+ refs,
176
+ methods,
177
+ },
178
+ render: () => (
179
+ !props.data.length ? (
180
+ <div className="search-service-empty" key="empty">
181
+ {slots.default()}
182
+ </div>
183
+ ) : (
184
+ <div className="search-service-list" key="list">
185
+ {availableList.value.length <= 6 ? (
186
+ /*不超过6个元素,就直接渲染选项数组*/
187
+ props.data.map((item, index) => (renderItem({ item, vid: `_${(item.title || String(index)) + (item.desc || String(index))}`, index })))
188
+ ) : (
189
+ /*超过6个就渲染虚拟列表*/
190
+ <div className="search-service-option-virtual-list">
191
+ <VirtualList
192
+ ref={onRef.virtual}
193
+ size={56}
194
+ dynamicSize
195
+ data={props.data}
196
+ alwaysShowScrollbar
197
+ v-slots={{ default: ({ item, vid, index }) => renderItem({ item, vid, index }) }}
198
+ />
199
+ </div>
200
+ )}
201
+ </div>
202
+ )
203
+ )
204
+ };
205
+ },
206
+ });
@@ -0,0 +1,233 @@
1
+ import {computed, createStore, designComponent, getComponentCls, onMounted, PropType, reactive, useClassCache, useRefs} from "plain-design-composition";
2
+ import {iSearchDataMeta, iSearchServiceConfig} from "./search.utils";
3
+ import {debounce} from "plain-utils/utils/debounce";
4
+ import {handleKeyboard} from "../KeyboardService";
5
+ import {Box} from "../Box";
6
+ import Input from "../Input";
7
+ import {SearchList} from "./SearchList";
8
+ import {SearchFooter} from "./SearchFooter";
9
+ import ApplicationConfigurationProvider from "../ApplicationConfigurationProvider";
10
+ import {i18n} from "../../i18n";
11
+
12
+ export const SearchServicePanel = designComponent({
13
+ props: {
14
+ config: { type: Object as PropType<iSearchServiceConfig>, required: true }
15
+ },
16
+ emits: {
17
+ onClose: () => true,
18
+ },
19
+ setup({ props, event: { emit } }) {
20
+
21
+ const configuration = ApplicationConfigurationProvider.inject();
22
+
23
+ /*微调输入框以及footer在黑白主题下的颜色*/
24
+ const inputBackgroundColor = computed(() => ({ backgroundColor: configuration.value.theme.vars[configuration.value.theme.dark ? "bg-1" : 'bg-2'] }));
25
+
26
+ /**
27
+ * 搜索缓存
28
+ * @author 韦胜健
29
+ * @date 2024.6.23 21:21
30
+ */
31
+ const searchCache = createStore({
32
+ initialState: {
33
+ history: [] as iSearchDataMeta[],
34
+ favorite: [] as iSearchDataMeta[],
35
+ },
36
+ getCacheConfig: () => ({ envName: getComponentCls(''), cacheName: props.config.cacheName || `@@search_service}` })
37
+ });
38
+
39
+ const { refs, onRef } = useRefs({ list: SearchList });
40
+
41
+ const state = reactive({
42
+ /*当前输入的搜索关键词*/
43
+ searchText: '',
44
+ /*展示的选项数据*/
45
+ data: [] as iSearchDataMeta[],
46
+ /*当前是否处于加载状态*/
47
+ loading: false,
48
+ /*当前是否处于输入状态*/
49
+ editing: false,
50
+ });
51
+
52
+ const historyData = computed(() => {
53
+ const data: iSearchDataMeta[] = [];
54
+ if (!!searchCache.value.history.length) {
55
+ data.push({ title: i18n.$it('search.searchHistory').d('搜索历史'), type: 'group' });
56
+ data.push(...searchCache.value.history);
57
+ }
58
+ if (!!searchCache.value.favorite.length) {
59
+ data.push({ title: i18n.$it('search.favorite').d('收藏'), type: 'group' });
60
+ data.push(...searchCache.value.favorite);
61
+ }
62
+ return data;
63
+ });
64
+
65
+ const classes = useClassCache(() => [
66
+ getComponentCls('search-service-panel')
67
+ ]);
68
+
69
+ const handler = {
70
+ /**
71
+ * 每次输入搜索关键词的时候都重置数据,防抖执行
72
+ * @author 韦胜健
73
+ * @date 2024.6.23 21:23
74
+ */
75
+ resetData: debounce(async () => {
76
+ if (!state.searchText.trim().length) {
77
+ state.data = [];
78
+ state.editing = false;
79
+ refs.list?.methods.resetSelectIndex();
80
+ return;
81
+ }
82
+ state.loading = true;
83
+ try {
84
+ state.data = await props.config.getData(state.searchText);
85
+ } catch (e) {
86
+ state.data = [];
87
+ throw e;
88
+ } finally {
89
+ state.loading = false;
90
+ state.editing = false;
91
+ refs.list?.methods.resetSelectIndex();
92
+ }
93
+ }, 300),
94
+ /**
95
+ * 搜索关键词变化的话立即标识处于输入状态
96
+ * @author 韦胜健
97
+ * @date 2024.6.23 21:24
98
+ */
99
+ onInputChange: () => {
100
+ state.editing = true;
101
+ handler.resetData();
102
+ },
103
+ /**
104
+ * 处理上下以及enter,esc快捷键
105
+ * @author 韦胜健
106
+ * @date 2024.6.23 21:24
107
+ */
108
+ onKeydown: handleKeyboard({
109
+ up: async (e) => {
110
+ e.preventDefault();
111
+ e.stopPropagation();
112
+ refs.list?.methods.selectPrev();
113
+ },
114
+ down: async (e) => {
115
+ e.preventDefault();
116
+ e.stopPropagation();
117
+ refs.list?.methods.selectNext();
118
+ },
119
+ enter: (e) => {
120
+ e.preventDefault();
121
+ e.stopPropagation();
122
+ const target = refs.list?.methods.getSelected();
123
+ if (!target) {return;}
124
+ handler.onSelectItem(target);
125
+ },
126
+ esc: (e) => {
127
+ e.preventDefault();
128
+ e.stopPropagation();
129
+ emit.onClose();
130
+ },
131
+ }),
132
+ /**
133
+ * 处理选中某个选项的动作
134
+ * @author 韦胜健
135
+ * @date 2024.6.23 21:24
136
+ */
137
+ onSelectItem: (dataMeta: iSearchDataMeta) => {
138
+ if (!historyData.value.find(i => i.title === dataMeta.title && i.desc === dataMeta.desc)) {
139
+ dataMeta = { ...dataMeta, type: 'history' };
140
+ searchCache.value.history.unshift(dataMeta);
141
+ if (searchCache.value.history.length > 5) {searchCache.value.history.pop();}
142
+ searchCache.value = { ...searchCache.value };
143
+ }
144
+ props.config.onSelect(dataMeta);
145
+ emit.onClose();
146
+ },
147
+ onRemoveItem: (dataMeta: iSearchDataMeta) => {
148
+ const historyIndex = searchCache.value.history.findIndex(i => i.title === dataMeta.title && i.desc === dataMeta.desc && i.type === 'history');
149
+ if (historyIndex > -1) {
150
+ searchCache.value.history.splice(historyIndex, 1);
151
+ searchCache.value = { ...searchCache.value };
152
+ return;
153
+ }
154
+ const favoriteIndex = searchCache.value.favorite.findIndex(i => i.title === dataMeta.title && i.desc === dataMeta.desc && i.type === 'favorite');
155
+ if (favoriteIndex > -1) {
156
+ searchCache.value.favorite.splice(favoriteIndex, 1);
157
+ searchCache.value = { ...searchCache.value };
158
+ return;
159
+ }
160
+ },
161
+ onAddFavorite: (dataMeta: iSearchDataMeta) => {
162
+ handler.onRemoveItem(dataMeta);
163
+ dataMeta = { ...dataMeta, type: 'favorite' };
164
+ searchCache.value.favorite.unshift(dataMeta);
165
+ searchCache.value = { ...searchCache.value };
166
+ },
167
+ };
168
+
169
+ const renderList = (data: iSearchDataMeta[], empty: () => any) => {
170
+ return (
171
+ <SearchList
172
+ ref={onRef.list}
173
+ data={data}
174
+ config={props.config}
175
+ onSelect={handler.onSelectItem}
176
+ onRemoveItem={handler.onRemoveItem}
177
+ onAddFavorite={handler.onAddFavorite}
178
+ >
179
+ {empty()}
180
+ </SearchList>
181
+ );
182
+ };
183
+
184
+ onMounted(() => {
185
+ refs.list?.methods.resetSelectIndex();
186
+ });
187
+
188
+ return () => (
189
+ <Box className={classes.value}>
190
+ <div className="search-service-input-box">
191
+ <Input
192
+ style={inputBackgroundColor.value}
193
+ autoFocus
194
+ prefixIcon="pi-search"
195
+ v-model={state.searchText}
196
+ placeholder={props.config.placeholder}
197
+ inputMode="stroke"
198
+ onChange={handler.onInputChange}
199
+ loading={state.loading}
200
+ loadingType="kappa"
201
+ onKeyDown={handler.onKeydown}
202
+ />
203
+ </div>
204
+ <div className="search-service-body">
205
+ {!state.searchText.trim().length || (state.editing && !state.data.length) ? (
206
+ /*如果没有搜索关键词,或者处于输入关键词状态并且没有数据的话,显示搜索历史*/
207
+ renderList(historyData.value, () => (
208
+ <div className="search-service-panel-history" key="history">
209
+ {i18n.$it('search.noHistory').d('没有搜索历史')}
210
+ </div>))
211
+ ) : (
212
+ /*否则显示具体的数据*/
213
+ renderList(state.data, () => {
214
+ return (
215
+ <div key="no_match">
216
+ <svg width="40" height="40" viewBox="0 0 20 20" fill="none" fillRule="evenodd" stroke="currentColor" strokeLinecap="round" strokeLinejoin="round">
217
+ <path d="M15.5 4.8c2 3 1.7 7-1 9.7h0l4.3 4.3-4.3-4.3a7.8 7.8 0 01-9.8 1m-2.2-2.2A7.8 7.8 0 0113.2 2.4M2 18L18 2"></path>
218
+ </svg>
219
+ <div>
220
+ {i18n.$it('search.noMatch', { val: state.searchText }).d(`无法找到搜索结果 "${state.searchText}"`)}
221
+ </div>
222
+ </div>
223
+ );
224
+ })
225
+ )}
226
+ </div>
227
+ <div className="search-service-foot" style={inputBackgroundColor.value}>
228
+ {props.config.footer ? props.config.footer() : (<SearchFooter/>)}
229
+ </div>
230
+ </Box>
231
+ );
232
+ },
233
+ });
@@ -0,0 +1,43 @@
1
+ import {iSearchServiceConfig, iSearchServiceCustomConfig, iSearchServiceDefaultConfig} from "./search.utils";
2
+ import $dialog from "../$dialog";
3
+ import {getComponentCls} from "plain-design-composition";
4
+ import './search-service.scss';
5
+ import {i18n} from "../i18n";
6
+
7
+ import {SearchServicePanel} from "./SearchServicePanel";
8
+
9
+ export function createSearchService(defaultConfig?: Partial<iSearchServiceDefaultConfig>) {
10
+
11
+ const _defaultConfig: iSearchServiceDefaultConfig = {
12
+ width: defaultConfig?.width || 560,
13
+ footer: defaultConfig?.footer,
14
+ render: defaultConfig?.render,
15
+ placeholder: defaultConfig?.placeholder || i18n.$it('table.pleaseEnterSearchKey').d('请输入搜索关键词')
16
+ };
17
+
18
+ return (customConfig: iSearchServiceCustomConfig & Partial<iSearchServiceDefaultConfig>) => {
19
+
20
+ const config: iSearchServiceConfig = {
21
+ ..._defaultConfig,
22
+ ...customConfig,
23
+ };
24
+
25
+ const closeDialog = $dialog({
26
+ width: config.width,
27
+ noHead: true,
28
+ noFoot: true,
29
+ externalClass: getComponentCls('search-service'),
30
+ noContentPadding: true,
31
+ render: () => {
32
+ return (
33
+ <SearchServicePanel config={config} onClose={closeDialog}/>
34
+ );
35
+ },
36
+ });
37
+
38
+ return {
39
+ close: closeDialog,
40
+ };
41
+ };
42
+ }
43
+
@@ -0,0 +1,6 @@
1
+ import {createSearchService} from "./createSearchService";
2
+
3
+ export const $search = createSearchService();
4
+
5
+ export default $search;
6
+