@opendesign-plus-test/components 0.0.1-rc.45 → 0.0.1-rc.47

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.
Files changed (36) hide show
  1. package/dist/chunk-OElCookieNotice.cjs.js +1 -1
  2. package/dist/chunk-OElCookieNotice.es.js +46 -66
  3. package/dist/components/OHeaderSearch.vue.d.ts +534 -812
  4. package/dist/components.cjs.js +41 -41
  5. package/dist/components.css +1 -1
  6. package/dist/components.es.js +10253 -11228
  7. package/dist/index.d.ts +0 -1
  8. package/package.json +2 -2
  9. package/src/assets/svg-icons/icon-delete.svg +1 -5
  10. package/src/components/OHeaderSearch.vue +425 -407
  11. package/src/i18n/en.ts +0 -10
  12. package/src/i18n/zh.ts +0 -10
  13. package/src/index.ts +0 -1
  14. package/dist/components/search/OSearchInput.vue.d.ts +0 -1003
  15. package/dist/components/search/composables/useImageSearch.d.ts +0 -48
  16. package/dist/components/search/composables/useKeywordHighlight.d.ts +0 -2
  17. package/dist/components/search/composables/useSearchHistory.d.ts +0 -14
  18. package/dist/components/search/index.d.ts +0 -590
  19. package/dist/components/search/internal/HighlightText.vue.d.ts +0 -9
  20. package/dist/components/search/internal/SearchImageInput.vue.d.ts +0 -716
  21. package/dist/components/search/internal/SearchPanel.vue.d.ts +0 -100
  22. package/dist/components/search/types.d.ts +0 -20
  23. package/src/assets/svg-icons/icon-delete-hover.svg +0 -4
  24. package/src/assets/svg-icons/icon-image-close.svg +0 -4
  25. package/src/assets/svg-icons/icon-image-upload.svg +0 -3
  26. package/src/assets/svg-icons/icon-image-zoomin.svg +0 -3
  27. package/src/assets/svg-icons/icon-refresh.svg +0 -3
  28. package/src/components/search/OSearchInput.vue +0 -463
  29. package/src/components/search/composables/useImageSearch.ts +0 -157
  30. package/src/components/search/composables/useKeywordHighlight.ts +0 -30
  31. package/src/components/search/composables/useSearchHistory.ts +0 -75
  32. package/src/components/search/index.ts +0 -23
  33. package/src/components/search/internal/HighlightText.vue +0 -37
  34. package/src/components/search/internal/SearchImageInput.vue +0 -488
  35. package/src/components/search/internal/SearchPanel.vue +0 -430
  36. package/src/components/search/types.ts +0 -25
@@ -1,24 +1,18 @@
1
1
  <script setup lang="ts">
2
- import { computed, ref, toRef, watch } from 'vue';
3
- import { OIcon } from '@opensig/opendesign';
4
- import { onClickOutside, useDebounceFn } from '@vueuse/core';
2
+ import { computed, onMounted, ref, watch } from 'vue';
3
+ import { OIcon, OInput, ODivider } from '@opensig/opendesign';
4
+ import { onClickOutside } from '@vueuse/core';
5
5
  import { useScreen } from '@opendesign-plus/composables';
6
6
 
7
- import SearchImageInput from './search/internal/SearchImageInput.vue';
8
- import SearchPanel from './search/internal/SearchPanel.vue';
9
- import { useSearchHistory } from './search/composables/useSearchHistory';
10
- import { useI18n } from '@/i18n';
11
- import type {
12
- OSearchPayload,
13
- OSearchRecommendItem,
14
- OSearchUploadImageFn,
15
- } from './search/types';
16
-
7
+ import IconClose from '~icons/components/icon-close.svg';
17
8
  import IconSearch from '~icons/components/icon-header-search.svg';
9
+ import IconDelete from '~icons/components/icon-header-delete.svg';
10
+ import IconDeleteAll from '~icons/components/icon-delete.svg';
18
11
  import IconBack from '~icons/components/icon-header-back.svg';
19
12
 
13
+ import { useI18n } from '@/i18n';
14
+
20
15
  export interface OHeaderSearchPropsT {
21
- /** ---- Backward-compatible props ---- */
22
16
  modelValue?: string;
23
17
  placeholder?: string; // 搜索框默认提示
24
18
  expandedPlaceholder?: string; // 搜索框展开后提示
@@ -26,37 +20,28 @@ export interface OHeaderSearchPropsT {
26
20
  clearable?: boolean; // 是否显示清除按钮,默认显示
27
21
  historyItems?: string[]; // 搜索历史记录
28
22
  maxHistoryCount?: number; // 最多保存的搜索历史记录数,默认 6 条
29
- storeHistory?: boolean; // 是否使用 localStorage 存储搜索历史记录,存储之后初始化时会自动加载搜索历史记录,默认为 false
23
+ storeHistory?: boolean; // 是否使用 localStorage 存储搜索历史记录,存储之后初始化时会自动加载搜索历史记录,默认为 false
30
24
  historyTitle?: string; // 历史记录标题
31
25
  storageKey?: string; // localStorage 存储搜索历史记录的 key,默认为 search-history
32
26
  hotItems?: string[]; // 热门搜索
33
27
  hotTitle?: string; // 推荐搜索标题
34
- /** Legacy plain-string recommend list. Shown when input is empty. */
35
- recommendItems?: string[];
36
- searchUrl?: string;
37
- searchUrlOpenBlank?: boolean;
38
- searchTextMobile?: string;
39
- imagePlaceholder?: string;
40
- enableImageSearch?: boolean;
41
- imageUrl?: string;
42
- uploadImage?: OSearchUploadImageFn;
43
- maxImageSize?: number;
44
- imageUploadTooltip?: string;
45
- suggestItems?: OSearchRecommendItem[];
46
- onestepItems?: OSearchRecommendItem[];
47
- suggestTitle?: string;
48
- onestepTitle?: string;
49
- noDataText?: string;
50
- highlightKeyword?: boolean;
51
- /** Debounce ms for the `input` event */
52
- debounce?: number;
53
- /** Auto-record history on search; default true */
54
- autoSaveHistory?: boolean;
55
- /** Show "no data" empty state in suggest section while typing */
56
- showSuggestEmpty?: boolean;
57
- allowedImageTypes?: string[];
28
+ recommendItems?: string[]; // 推荐搜索
29
+ searchUrl?: string; // 搜索页面 url,不为空点击热门搜索、历史记录、推荐搜索和回车搜索时自动打开页面
30
+ searchUrlOpenBlank?: boolean; // 是否在新窗口打开搜索页面,默认为 true
31
+ searchTextMobile?: string; // 手机端搜索按钮文字,默认为搜索
58
32
  }
59
33
 
34
+ export interface OHeaderSearchEmitsT {
35
+ (e: 'update:modelValue', value: string): void;
36
+ (e: 'update:historyItems', value: string[]): void;
37
+ (e: 'clear'): void;
38
+ (e: 'search', value: string): void;
39
+ (e: 'delete-history', value: string[]): void;
40
+ (e: 'delete-history-item', value: string): void;
41
+ }
42
+ const { lePadV } = useScreen();
43
+ const { t } = useI18n();
44
+
60
45
  const props = withDefaults(defineProps<OHeaderSearchPropsT>(), {
61
46
  modelValue: '',
62
47
  expandDirection: 'left',
@@ -68,349 +53,235 @@ const props = withDefaults(defineProps<OHeaderSearchPropsT>(), {
68
53
  hotItems: () => [],
69
54
  recommendItems: () => [],
70
55
  searchUrlOpenBlank: true,
71
- enableImageSearch: false,
72
- imageUrl: '',
73
- maxImageSize: 10 * 1024 * 1024,
74
- suggestItems: () => [],
75
- onestepItems: () => [],
76
- highlightKeyword: true,
77
- debounce: 300,
78
- autoSaveHistory: true,
79
- showSuggestEmpty: true,
80
56
  });
81
57
 
82
- const emit = defineEmits<{
83
- (e: 'update:modelValue', value: string): void;
84
- (e: 'update:historyItems', value: string[]): void;
85
- (e: 'update:imageUrl', url: string): void;
86
- (e: 'focus'): void;
87
- (e: 'blur'): void;
88
- (e: 'input', val: string): void;
89
- (e: 'clear'): void;
90
- /** Backward compatible: previously `(val: string)`. Now emits payload. */
91
- (e: 'search', payload: OSearchPayload): void;
92
- (e: 'recommend-click', item: OSearchRecommendItem | string): void;
93
- (e: 'onestep-click', item: OSearchRecommendItem): void;
94
- (e: 'history-click', val: string): void;
95
- (e: 'hot-click', val: string): void;
96
- (e: 'hot-refresh'): void;
97
- (e: 'delete-history', value: string[]): void;
98
- (e: 'delete-history-item', value: string): void;
99
- (e: 'image-upload-start', file: File): void;
100
- (e: 'image-upload-success', url: string, file: File): void;
101
- (e: 'image-upload-error', error: unknown, file: File): void;
102
- (e: 'image-validate-error', reason: 'size' | 'type', file: File): void;
103
- }>();
104
-
105
- const { lePadV } = useScreen();
106
- const { t } = useI18n();
58
+ const emit = defineEmits<OHeaderSearchEmitsT>();
107
59
 
108
- const wrapperRef = ref<HTMLElement | null>(null);
109
- const inputRef = ref<InstanceType<typeof SearchImageInput>>();
60
+ const searchInput = ref(props.modelValue);
61
+ const searchHistoryItems = ref(props.historyItems);
110
62
  const isShowDrawer = ref(false);
111
- const internalImageStaged = ref(false);
112
- const internalImageUrl = ref('');
113
- const hasImage = computed(() => !!props.imageUrl || internalImageStaged.value);
114
-
115
- const innerValue = computed({
116
- get: () => props.modelValue,
117
- set: (val: string) => emit('update:modelValue', val),
118
- });
63
+ const inputRef = ref();
119
64
 
120
- const historyItemsRef = toRef(props, 'historyItems');
121
- const storageKeyRef = toRef(props, 'storageKey');
122
- const storeHistoryRef = toRef(props, 'storeHistory');
123
- const maxHistoryRef = toRef(props, 'maxHistoryCount');
124
-
125
- const history = useSearchHistory({
126
- initial: historyItemsRef,
127
- storageKey: storageKeyRef,
128
- storeHistory: storeHistoryRef,
129
- maxHistoryCount: maxHistoryRef,
130
- onChange: (items) => emit('update:historyItems', items),
65
+ const isShowClearIcon = computed(() => {
66
+ return (!lePadV.value && isShowDrawer.value) || (lePadV.value && searchInput.value);
131
67
  });
132
68
 
133
- const placeholder = computed(() => {
134
- if (props.imageUrl) {
135
- return props.imagePlaceholder ?? t('search.extendedPlaceholder');
136
- }
137
- if (isShowDrawer.value && props.enableImageSearch) {
138
- return props.imagePlaceholder ?? t('search.imagePlaceholder');
69
+ watch(
70
+ () => props.modelValue,
71
+ (val) => {
72
+ if (searchInput.value !== val) {
73
+ searchInput.value = val;
74
+ }
139
75
  }
140
- if (isShowDrawer.value) {
141
- return props.expandedPlaceholder ?? t('search.expandedPlaceholder');
76
+ );
77
+
78
+ watch(
79
+ () => searchInput.value,
80
+ (val) => {
81
+ emit('update:modelValue', val);
142
82
  }
143
- return props.placeholder ?? t('search.placeholder');
144
- });
83
+ );
145
84
 
146
- // debounce input event
147
- const emitInputDebounced = useDebounceFn((val: string) => emit('input', val), () => props.debounce);
148
- watch(innerValue, (val) => emitInputDebounced(val));
85
+ watch(
86
+ () => props.historyItems,
87
+ (val) => {
88
+ if (searchHistoryItems.value !== val) {
89
+ searchHistoryItems.value = val;
90
+ }
91
+ }
92
+ );
149
93
 
150
- // click outside (desktop only)
151
- onClickOutside(wrapperRef, () => {
152
- if (lePadV.value) return;
153
- closeDrawer();
94
+ watch(
95
+ () => searchHistoryItems.value,
96
+ (val) => {
97
+ emit('update:historyItems', val);
98
+ }
99
+ );
100
+
101
+ onMounted(() => {
102
+ if (props.storeHistory && props.storageKey) {
103
+ try {
104
+ const history = JSON.parse(localStorage.getItem(props.storageKey) || '[]');
105
+ if (Array.isArray(history) && history.length) {
106
+ searchHistoryItems.value = Array.from(new Set([...searchHistoryItems.value, ...history]));
107
+ }
108
+ } catch {
109
+ // nothing
110
+ }
111
+ }
154
112
  });
155
113
 
156
- const openDrawer = () => {
114
+ const onShowDrawer = () => {
157
115
  isShowDrawer.value = true;
158
116
  };
159
117
 
160
- const closeDrawer = () => {
161
- if (lePadV.value) return;
162
- isShowDrawer.value = false;
163
- };
164
-
165
- const handleFocus = () => {
166
- openDrawer();
167
- emit('focus');
168
- };
169
-
170
- const handleBlur = () => {
171
- emit('blur');
172
- };
173
-
174
- const runSearch = async () => {
175
- if (inputRef.value?.getIsUploading?.()) {
176
- await inputRef.value.awaitUpload?.();
118
+ const onSearch = () => {
119
+ const input = searchInput.value.trim();
120
+ if (!input) {
121
+ return;
177
122
  }
178
- const keyword = innerValue.value.trim();
179
- const imageUrl = internalImageUrl.value || inputRef.value?.getUploadedUrl?.() || props.imageUrl;
180
-
181
- if (!keyword && !imageUrl) return;
182
123
 
183
- if (props.autoSaveHistory && keyword) {
184
- history.push(keyword);
124
+ isShowDrawer.value = false;
125
+ searchHistoryItems.value.unshift(input);
126
+ searchHistoryItems.value = Array.from(new Set(searchHistoryItems.value));
127
+ if (searchHistoryItems.value.length > props.maxHistoryCount) {
128
+ searchHistoryItems.value.pop();
185
129
  }
186
130
 
187
- isShowDrawer.value = false;
188
- emit('search', { keyword, imageUrl: imageUrl || undefined });
131
+ if (props.storeHistory && props.storeHistory) {
132
+ localStorage.setItem(props.storageKey, JSON.stringify(searchHistoryItems.value));
133
+ }
134
+ emit('search', input);
189
135
 
190
136
  if (props.searchUrl) {
191
- const params = new URLSearchParams();
192
- if (keyword) params.set('q', keyword);
193
- if (imageUrl) params.set('imageUrl', imageUrl);
194
- const sep = props.searchUrl.includes('?') ? '&' : '?';
195
- const url = `${props.searchUrl}${sep}${params.toString()}`;
196
- if (typeof window !== 'undefined') {
197
- window.open(url, props.searchUrlOpenBlank ? '_blank' : '_self', 'noopener,noreferrer');
198
- }
137
+ window.open(props.searchUrl + input, props.searchUrlOpenBlank ? '_blank' : '_self', 'noopener noreferrer');
199
138
  }
200
139
  };
201
140
 
202
- const handleClear = () => {
203
- innerValue.value = '';
141
+ const onClear = () => {
142
+ searchInput.value = '';
204
143
  emit('clear');
205
- inputRef.value?.focus?.();
206
- };
207
-
208
- const handleSuggestClick = (item: OSearchRecommendItem) => {
209
- emit('recommend-click', item);
210
- innerValue.value = item.key;
211
- runSearch();
212
- };
213
-
214
- const handleRecommendClick = (val: string) => {
215
- emit('recommend-click', val);
216
- innerValue.value = val;
217
- runSearch();
144
+ if (!lePadV.value) {
145
+ isShowDrawer.value = false;
146
+ }
218
147
  };
219
148
 
220
- const handleOnestepClick = (item: OSearchRecommendItem) => {
221
- emit('onestep-click', item);
222
- if (item.path && typeof window !== 'undefined') {
223
- window.open(item.path, '_blank', 'noopener,noreferrer');
149
+ const onDeleteHistory = () => {
150
+ const history = [...searchHistoryItems.value];
151
+ searchHistoryItems.value = [];
152
+ if (props.storeHistory && props.storeHistory) {
153
+ localStorage.removeItem(props.storageKey);
224
154
  }
225
- };
226
155
 
227
- const handleHistoryClick = (val: string) => {
228
- emit('history-click', val);
156
+ emit('delete-history', history);
229
157
  };
230
158
 
231
- const handleHotClick = (val: string) => {
232
- emit('hot-click', val);
233
- };
159
+ const onDeleteHistoryItem = (val: string) => {
160
+ searchHistoryItems.value = searchHistoryItems.value.filter((item) => item !== val);
161
+ if (props.storeHistory && props.storeHistory) {
162
+ if (searchHistoryItems.value.length) {
163
+ localStorage.setItem(props.storageKey, JSON.stringify(searchHistoryItems.value));
164
+ } else {
165
+ localStorage.removeItem(props.storageKey);
166
+ }
167
+ }
234
168
 
235
- const handleHistoryRemove = (val: string) => {
236
- history.remove(val);
237
169
  emit('delete-history-item', val);
238
170
  };
239
171
 
240
- const handleHistoryClear = () => {
241
- const removed = [...history.items.value];
242
- history.clearAll();
243
- emit('delete-history', removed);
172
+ const onWordSearch = (val: string) => {
173
+ searchInput.value = val;
174
+ onSearch();
244
175
  };
245
176
 
246
- const handleBack = () => {
247
- innerValue.value = '';
248
- internalImageStaged.value = false;
249
- emit('update:imageUrl', '');
177
+ const onBack = () => {
178
+ searchInput.value = '';
250
179
  isShowDrawer.value = false;
251
180
  };
252
181
 
253
- const handleImageUploadStart = (file: File) => {
254
- internalImageStaged.value = true;
255
- internalImageUrl.value = '';
256
- emit('image-upload-start', file);
257
- };
258
-
259
- const handleImageUploadSuccess = (url: string, file: File) => {
260
- internalImageUrl.value = url;
261
- emit('update:imageUrl', url);
262
- emit('image-upload-success', url, file);
263
- };
264
-
265
- const handleImageClear = () => {
266
- internalImageStaged.value = false;
267
- internalImageUrl.value = '';
268
- emit('update:imageUrl', '');
269
- };
270
-
271
- const showPanel = computed(() => {
272
- if (!isShowDrawer.value) return false;
273
- if (hasImage.value) return false;
274
- return true;
275
- });
276
-
277
- defineExpose({
278
- focus: () => inputRef.value?.focus?.(),
279
- blur: () => inputRef.value?.blur?.(),
280
- open: openDrawer,
281
- close: closeDrawer,
282
- search: runSearch,
283
- });
182
+ const posWrapper = ref();
183
+ onClickOutside(posWrapper, onClear);
284
184
  </script>
285
185
 
286
186
  <template>
287
187
  <div class="o-header-search">
288
188
  <div
289
- ref="wrapperRef"
290
- class="o-header-search-pos"
189
+ ref="posWrapper"
291
190
  :class="{
292
- 'is-pc': !lePadV,
293
- 'is-mobile': lePadV,
294
- 'is-left': expandDirection === 'left',
295
- 'is-right': expandDirection === 'right',
296
- 'is-focus': isShowDrawer,
191
+ 'o-header-search-input-pc-wrapper': !lePadV,
192
+ 'o-header-search-input-pc-wrapper-left': !lePadV && expandDirection === 'left',
193
+ 'o-header-search-input-pc-wrapper-right': !lePadV && expandDirection === 'right',
194
+ 'o-header-search-input-mobile-wrapper': lePadV,
195
+ focus: isShowDrawer,
297
196
  }"
298
197
  >
299
- <div class="o-header-search-row" :class="{ 'is-focus': isShowDrawer }">
300
- <OIcon v-if="lePadV && isShowDrawer" class="o-header-search-back-icon" @click="handleBack">
198
+ <div class="o-header-search-input-wrapper" :class="{ focus: isShowDrawer }">
199
+ <OIcon v-if="lePadV && isShowDrawer" class="o-header-search-icon" @click="onBack">
301
200
  <IconBack />
302
201
  </OIcon>
303
202
 
304
- <SearchImageInput
203
+ <OInput
305
204
  ref="inputRef"
306
- v-model="innerValue"
307
- :image-url="imageUrl"
308
- :placeholder="placeholder"
309
- :enable-image-search="enableImageSearch"
310
- :upload-image="uploadImage"
311
- :max-image-size="maxImageSize"
312
- :image-upload-tooltip="imageUploadTooltip"
313
- :clearable="clearable && isShowDrawer"
314
- :expanded="isShowDrawer && hasImage"
315
- :allowed-image-types="allowedImageTypes"
316
- size="medium"
205
+ v-model="searchInput"
317
206
  class="o-header-search-input"
318
- :class="{ 'is-collapsed': !isShowDrawer }"
319
- @update:imageUrl="(url: string) => emit('update:imageUrl', url)"
320
- @focus="handleFocus"
321
- @blur="handleBlur"
322
- @enter="runSearch"
323
- @clear="handleClear"
324
- @image-clear="handleImageClear"
325
- @image-upload-start="handleImageUploadStart"
326
- @image-upload-success="handleImageUploadSuccess"
327
- @image-upload-error="(error: unknown, file: File) => emit('image-upload-error', error, file)"
328
- @image-validate-error="(reason: 'size' | 'type', file: File) => emit('image-validate-error', reason, file)"
207
+ :placeholder="isShowDrawer ? expandedPlaceholder ?? t('search.expandedPlaceholder') : placeholder ?? t('search.placeholder')"
208
+ @focus="onShowDrawer"
209
+ @keyup.enter="onSearch"
329
210
  >
330
- <template #prefix><slot name="input-prefix" /></template>
331
- <template #suffix="slotProps"><slot name="input-suffix" v-bind="slotProps" /></template>
332
- <template v-if="$slots['image-preview']" #preview="slotProps">
333
- <slot name="image-preview" v-bind="slotProps" />
211
+ <template #prefix>
212
+ <slot name="input-prefix">
213
+ <OIcon class="o-header-search-icon">
214
+ <IconSearch />
215
+ </OIcon>
216
+ </slot>
334
217
  </template>
335
- </SearchImageInput>
336
218
 
337
- <span v-if="lePadV && isShowDrawer" class="o-header-search-text" @click="runSearch">
338
- {{ searchTextMobile ?? t('search') }}
339
- </span>
219
+ <template #suffix>
220
+ <slot name="input-suffix">
221
+ <OIcon v-if="clearable && isShowClearIcon" class="o-header-search-icon close" @click="onClear">
222
+ <IconClose />
223
+ </OIcon>
224
+ </slot>
225
+ </template>
226
+ </OInput>
227
+
228
+ <span v-if="lePadV && isShowDrawer" class="o-header-search-text" @click="onSearch">{{ searchTextMobile ?? t('search') }}</span>
340
229
  </div>
341
230
 
342
- <Transition name="o-header-search-drawer">
343
- <div v-if="showPanel" class="o-header-search-drawer">
344
- <slot
345
- name="drawer"
346
- :recommend-items="recommendItems"
347
- :history-items="history.items.value"
348
- :hot-items="hotItems"
349
- :suggest-items="suggestItems"
350
- :onestep-items="onestepItems"
351
- :keyword="modelValue"
352
- >
353
- <SearchPanel
354
- :keyword="modelValue"
355
- :onestep-items="onestepItems"
356
- :onestep-title="onestepTitle"
357
- :suggest-items="suggestItems"
358
- :suggest-title="suggestTitle"
359
- :recommend-items="recommendItems"
360
- :history-items="history.items.value"
361
- :history-title="historyTitle"
362
- :hot-items="hotItems"
363
- :hot-title="hotTitle"
364
- :no-data-text="noDataText"
365
- :highlight-keyword="highlightKeyword"
366
- :hide-on-keyword="true"
367
- :show-suggest-empty="showSuggestEmpty"
368
- @onestep-click="handleOnestepClick"
369
- @suggest-click="handleSuggestClick"
370
- @recommend-click="handleRecommendClick"
371
- @history-click="handleHistoryClick"
372
- @history-remove="handleHistoryRemove"
373
- @history-clear="handleHistoryClear"
374
- @hot-click="handleHotClick"
375
- @hot-refresh="emit('hot-refresh')"
376
- >
377
- <template v-if="$slots['recommend-header']" #recommend-header="slotProps">
378
- <slot name="recommend-header" v-bind="slotProps" />
379
- </template>
380
- <template v-if="$slots['recommend-content']" #recommend-content="slotProps">
381
- <slot name="recommend-content" v-bind="slotProps" />
382
- </template>
383
- <template v-if="$slots['onestep-header']" #onestep-header="slotProps">
384
- <slot name="onestep-header" v-bind="slotProps" />
385
- </template>
386
- <template v-if="$slots['onestep-content']" #onestep-content="slotProps">
387
- <slot name="onestep-content" v-bind="slotProps" />
388
- </template>
389
- <template v-if="$slots['suggest-header']" #suggest-header="slotProps">
390
- <slot name="suggest-header" v-bind="slotProps" />
391
- </template>
392
- <template v-if="$slots['suggest-content']" #suggest-content="slotProps">
393
- <slot name="suggest-content" v-bind="slotProps" />
394
- </template>
395
- <template v-if="$slots['history-header']" #history-header="slotProps">
396
- <slot name="history-header" v-bind="slotProps" />
397
- </template>
398
- <template v-if="$slots['history-content']" #history-content="slotProps">
399
- <slot name="history-content" v-bind="slotProps" />
400
- </template>
401
- <template v-if="$slots['hot-header']" #hot-header="slotProps">
402
- <slot name="hot-header" v-bind="slotProps" />
403
- </template>
404
- <template v-if="$slots['hot-content']" #hot-content="slotProps">
405
- <slot name="hot-content" v-bind="slotProps" />
406
- </template>
407
- </SearchPanel>
408
- </slot>
409
- </div>
410
- </Transition>
231
+ <div v-show="isShowDrawer" class="o-header-search-drawer">
232
+ <slot name="drawer" :recommend-items="recommendItems" :history-items="searchHistoryItems" :hot-items="hotItems">
233
+ <!-- 搜索推荐 -->
234
+ <div v-if="recommendItems.length" class="o-header-search-recommend-container">
235
+ <slot name="recommend-header" :recommend="recommendItems" />
236
+ <slot name="recommend-content" :recommend="recommendItems">
237
+ <div v-for="item in recommendItems" class="o-header-search-recommend-item" :key="item" @click="onWordSearch(item)">
238
+ {{ item }}
239
+ </div>
240
+ </slot>
241
+ </div>
242
+
243
+ <!-- 历史记录 -->
244
+ <div v-else-if="searchHistoryItems.length" class="o-header-search-history-container">
245
+ <slot name="history-header" :history="searchHistoryItems">
246
+ <div class="o-header-search-history-header">
247
+ <span class="o-header-search-history-header-title">{{ historyTitle ?? t('search.history') }}</span>
248
+ <OIcon class="o-header-search-icon" @click="onDeleteHistory">
249
+ <IconDeleteAll />
250
+ </OIcon>
251
+ </div>
252
+ </slot>
253
+
254
+ <slot name="history-content" :history="searchHistoryItems">
255
+ <div class="o-header-search-history-item-container">
256
+ <div v-for="item in searchHistoryItems" :key="item" class="o-header-search-history-item" @click="onWordSearch(item)">
257
+ <span class="o-header-search-history-item-text">{{ item }}</span>
258
+ <OIcon class="o-header-search-history-item-icon" @click.stop="onDeleteHistoryItem(item)">
259
+ <IconDelete class="icon-delete" />
260
+ </OIcon>
261
+ </div>
262
+ </div>
263
+ </slot>
264
+ </div>
265
+
266
+ <ODivider v-if="(recommendItems.length || searchHistoryItems.length) && hotItems.length" class="o-header-search-drawer-divider" />
267
+
268
+ <!-- 热门搜索 -->
269
+ <div v-if="hotItems.length" class="o-header-search-hot-container">
270
+ <slot name="hot-header" :hot="hotItems">
271
+ <div class="o-header-search-hot-header">{{ hotTitle ?? t('search.hot') }}</div>
272
+ </slot>
273
+
274
+ <slot name="hot-content" :hot="hotItems">
275
+ <div class="o-header-search-hot-item-container">
276
+ <div v-for="item in hotItems" :key="item" class="o-header-search-hot-item" @click="onWordSearch(item)">{{ item }}</div>
277
+ </div>
278
+ </slot>
279
+ </div>
280
+ </slot>
281
+ </div>
411
282
  </div>
412
283
 
413
- <OIcon v-if="lePadV" class="o-header-search-mobile-icon" @click="openDrawer">
284
+ <OIcon v-if="lePadV" class="o-header-search-icon-mobile" @click="onShowDrawer">
414
285
  <IconSearch />
415
286
  </OIcon>
416
287
  </div>
@@ -422,112 +293,119 @@ defineExpose({
422
293
  width: 160px;
423
294
  height: 32px;
424
295
 
425
- @include respond-to('<=laptop') {
296
+ @include respond('<=laptop') {
426
297
  width: 120px;
427
298
  }
428
299
 
429
- @include respond-to('<=pad_v') {
300
+ @media screen and (max-width: 1080px) {
430
301
  width: 24px;
431
302
  height: 24px;
432
- margin-left: auto;
433
303
  }
434
304
  }
435
305
 
436
- .o-header-search-pos {
437
- position: absolute;
438
- top: 0;
439
- width: fit-content;
440
- background-color: var(--o-color-fill2);
441
- z-index: 100;
306
+ .o-header-search-icon {
307
+ cursor: pointer;
308
+ color: var(--o-color-info1);
309
+ @include h4;
442
310
 
443
- &.is-left {
444
- right: 0;
445
- }
446
- &.is-right {
447
- left: 0;
448
- }
449
- &.is-mobile {
450
- display: none;
311
+ @include respond('<=pad_v') {
312
+ font-size: 20px;
451
313
  }
452
- &.is-pc.is-focus {
453
- box-shadow: var(--o-shadow-2);
454
- top: calc(-1 * var(--o-gap-4));
455
- border-radius: var(--o-radius-xs);
314
+
315
+ .close {
316
+ @include x-svg-hover;
456
317
  }
318
+ }
457
319
 
458
- &.is-mobile.is-focus {
459
- position: fixed;
460
- top: 0;
461
- right: 0;
462
- bottom: 0;
463
- left: 0;
464
- width: 100%;
320
+ .o-header-search-icon-mobile {
321
+ font-size: 24px;
322
+ line-height: 28px;
323
+ color: var(--o-color-info1);
324
+ cursor: pointer;
325
+ display: none;
326
+
327
+ @include respond('<=pad_v') {
465
328
  display: block;
466
- height: 100vh;
467
- background-color: var(--o-color-fill2);
468
- z-index: 100;
469
- overflow-y: auto;
470
329
  }
471
330
  }
472
331
 
473
- .o-header-search-row {
474
- display: flex;
475
- align-items: start;
332
+ .o-header-search-input-pc-wrapper {
333
+ position: absolute;
334
+ right: 0;
335
+ top: 0;
336
+ width: fit-content;
337
+ background-color: var(--o-color-fill2);
338
+ z-index: 100;
339
+ }
476
340
 
477
- &.is-focus {
478
- padding: var(--o-gap-4);
479
- border-radius: var(--o-radius-xs);
341
+ .o-header-search-input-pc-wrapper-left {
342
+ right: 0;
343
+ }
480
344
 
481
- @include respond-to('<=pad_v') {
482
- gap: var(--o-gap-4);
483
- padding: 10px var(--o-gap-4) var(--o-gap-4) var(--o-gap-4);
484
- border-radius: unset;
485
- }
486
- }
345
+ .o-header-search-input-pc-wrapper-right {
346
+ left: 0;
487
347
  }
488
348
 
489
- .o-header-search-input {
490
- width: 160px;
491
- transition: width var(--o-duration-m2) var(--o-easing-standard-in);
349
+ .o-header-search-input-pc-wrapper.focus {
350
+ box-shadow: var(--o-shadow-2);
351
+ top: calc(-1 * var(--o-gap-4));
352
+ }
492
353
 
493
- @include respond-to('<=laptop') {
494
- width: 120px;
495
- }
354
+ .o-header-search-input-mobile-wrapper {
355
+ display: none;
496
356
  }
497
357
 
498
- .o-header-search-row.is-focus .o-header-search-input {
499
- width: 480px;
358
+ .o-header-search-input-mobile-wrapper.focus {
359
+ position: fixed;
360
+ top: 0;
361
+ right: 0;
362
+ bottom: 0;
363
+ left: 0;
364
+ display: block;
365
+ height: 100vh;
366
+ background-color: var(--o-color-fill2);
367
+ z-index: 100;
368
+ overflow: hidden;
369
+ }
500
370
 
501
- @include respond-to('<=laptop') {
502
- width: 240px;
503
- }
371
+ .o-header-search-input-wrapper {
372
+ .o-header-search-input {
373
+ width: 160px;
374
+ transition: width var(--o-easing-standard-in) var(--o-duration-m2);
504
375
 
505
- @include respond-to('<=pad_v') {
506
- flex: 1;
507
- width: auto;
376
+ @include respond('<=laptop') {
377
+ width: 120px;
378
+ }
508
379
  }
509
380
  }
510
381
 
511
- .o-header-search-back-icon {
512
- cursor: pointer;
513
- color: var(--o-color-info1);
514
- font-size: 20px;
515
- height: 30px;
516
- }
382
+ .o-header-search-input-wrapper.focus {
383
+ padding: var(--o-gap-4);
517
384
 
518
- .o-header-search-text {
519
- white-space: nowrap;
520
- font-size: 16px;
521
- line-height: 24px;
522
- cursor: pointer;
523
- color: var(--o-color-info1);
524
- height: 30px;
385
+ @include respond('<=pad_v') {
386
+ display: flex;
387
+ align-items: center;
388
+ gap: var(--o-gap-4);
389
+ padding: 10px var(--o-gap-4) var(--o-gap-4) var(--o-gap-4);
390
+ }
525
391
 
526
- @include hover {
527
- color: var(--o-color-primary1);
392
+ .o-header-search-input {
393
+ width: 480px;
394
+
395
+ @include respond('<=laptop') {
396
+ width: 240px;
397
+ }
398
+
399
+ @include respond('<=pad_v') {
400
+ flex: 1;
401
+ }
528
402
  }
529
403
  }
530
404
 
405
+ .o-header-search-icon.close {
406
+ @include x-svg-hover;
407
+ }
408
+
531
409
  .o-header-search-drawer {
532
410
  position: absolute;
533
411
  width: 100%;
@@ -535,15 +413,13 @@ defineExpose({
535
413
  padding-top: var(--o-gap-2);
536
414
  background-color: var(--o-color-fill2);
537
415
  box-shadow: var(--o-shadow-2);
538
- border-radius: 0 0 var(--o-radius-xs) var(--o-radius-xs);
539
416
 
540
- @include respond-to('<=pad_v') {
417
+ @include respond('<=pad_v') {
541
418
  position: static;
542
419
  height: calc(100vh - 50px);
543
420
  padding-top: 0;
544
421
  overflow-y: auto;
545
422
  box-shadow: unset;
546
- border-radius: unset;
547
423
  }
548
424
  }
549
425
 
@@ -555,29 +431,171 @@ defineExpose({
555
431
  top: -14px;
556
432
  height: 14px;
557
433
  background-color: var(--o-color-fill2);
434
+ box-shadow: unset;
558
435
 
559
- @include respond-to('<=pad_v') {
436
+ @include respond('<=laptop') {
437
+ top: -10px;
438
+ height: 10px;
439
+ }
440
+
441
+ @include respond('<=pad') {
442
+ top: -8px;
443
+ height: 8px;
444
+ }
445
+
446
+ @include respond('<=pad_v') {
560
447
  display: none;
561
448
  }
562
449
  }
563
450
 
564
- .o-header-search-mobile-icon {
565
- font-size: 24px;
566
- line-height: 28px;
451
+ .o-header-search-recommend-container {
567
452
  color: var(--o-color-info1);
453
+ margin-bottom: var(--o-gap-3);
454
+ }
455
+
456
+ .o-header-search-recommend-item {
568
457
  cursor: pointer;
458
+ @include tip2;
459
+
460
+ @include hover {
461
+ color: var(--o-color-primary1);
462
+ }
463
+
464
+ @include respond('<=pad_v') {
465
+ font-size: 12px;
466
+ line-height: 18px;
467
+ }
468
+
469
+ & + & {
470
+ margin-top: var(--o-gap-3);
471
+ }
472
+ }
473
+
474
+ .o-header-search-history-container {
475
+ @include respond('<=pad_v') {
476
+ margin-bottom: var(--o-gap-5);
477
+ }
478
+ }
479
+
480
+ .o-header-search-history-header {
481
+ display: flex;
482
+ align-items: center;
483
+ justify-content: space-between;
484
+ }
485
+
486
+ .o-header-search-history-header-title {
487
+ @include tip2;
488
+ color: var(--o-color-info3);
489
+
490
+ @include respond('<=pad_v') {
491
+ @include text2;
492
+ color: var(--o-color-info1);
493
+ }
494
+ }
495
+
496
+ .o-header-search-history-item-container {
497
+ display: flex;
498
+ gap: 8px;
499
+ flex-wrap: wrap;
500
+ margin-top: var(--o-gap-2);
501
+ }
502
+
503
+ .o-header-search-history-item-icon {
504
+ position: absolute;
505
+ right: -8px;
506
+ top: -8px;
569
507
  display: none;
508
+ align-items: center;
509
+ justify-content: center;
510
+ width: 16px;
511
+ height: 16px;
512
+ border-radius: 50%;
513
+ background-color: rgb(var(--o-grey-9));
514
+
515
+ .icon-delete {
516
+ height: 16px;
517
+ width: 16px;
518
+ color: var(--o-color-white);
519
+ }
520
+ }
570
521
 
571
- @include respond-to('<=pad_v') {
572
- display: block;
522
+ .o-header-search-history-item {
523
+ position: relative;
524
+ display: flex;
525
+ align-items: center;
526
+ max-width: 224px;
527
+ height: 24px;
528
+ padding: 0 var(--o-gap-3);
529
+ background-color: var(--o-color-fill3);
530
+ border-radius: var(--o-radius-xs);
531
+ cursor: pointer;
532
+
533
+ @include hover {
534
+ background-color: var(--o-color-control2-light);
535
+ color: var(--o-color-primary1);
536
+
537
+ .o-header-search-history-item-icon {
538
+ display: flex;
539
+ }
573
540
  }
574
541
  }
575
542
 
576
- .o-header-search-drawer-enter-active {
577
- transition: opacity var(--o-duration-m1);
543
+ .o-header-search-history-item-text {
544
+ max-width: 200px;
545
+ overflow: hidden;
546
+ text-overflow: ellipsis;
547
+ white-space: nowrap;
548
+ @include tip2;
549
+
550
+ @include respond('<=pad_v') {
551
+ @include text1;
552
+ }
578
553
  }
579
554
 
580
- .o-header-search-drawer-enter-from {
581
- opacity: 0;
555
+ .o-header-search-drawer-divider {
556
+ --o-divider-gap: var(--o-gap-4);
557
+
558
+ @include respond('<=pad_v') {
559
+ display: none;
560
+ }
561
+ }
562
+
563
+ .o-header-search-hot-header {
564
+ color: var(--o-color-info3);
565
+ @include tip2;
566
+
567
+ @include respond('<=pad_v') {
568
+ margin-bottom: var(--o-gap-3);
569
+ @include text2;
570
+ }
571
+ }
572
+
573
+ .o-header-search-hot-item-container {
574
+ display: flex;
575
+ flex-wrap: wrap;
576
+ gap: var(--o-gap-4);
577
+ margin-top: var(--o-gap-3);
578
+ @include tip2;
579
+
580
+ @include respond('<=pad_v') {
581
+ flex-direction: column;
582
+ gap: 12px;
583
+ font-size: 12px;
584
+ line-height: 18px;
585
+ }
586
+ }
587
+
588
+ .o-header-search-hot-item {
589
+ color: var(--o-color-info1);
590
+ cursor: pointer;
591
+
592
+ @include hover {
593
+ color: var(--o-color-primary1);
594
+ }
595
+ }
596
+
597
+ .o-header-search-text {
598
+ font-size: 16px;
599
+ line-height: 24px;
582
600
  }
583
601
  </style>