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

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/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
+