@opendesign-plus-test/components 0.0.1-rc.54 → 0.0.1-rc.55

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