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

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 (37) hide show
  1. package/dist/chunk-OElCookieNotice.cjs.js +1 -1
  2. package/dist/chunk-OElCookieNotice.es.js +66 -46
  3. package/dist/components/OHeaderSearch.vue.d.ts +812 -534
  4. package/dist/components/search/OSearchInput.vue.d.ts +1003 -0
  5. package/dist/components/search/composables/useImageSearch.d.ts +48 -0
  6. package/dist/components/search/composables/useKeywordHighlight.d.ts +2 -0
  7. package/dist/components/search/composables/useSearchHistory.d.ts +14 -0
  8. package/dist/components/search/index.d.ts +590 -0
  9. package/dist/components/search/internal/HighlightText.vue.d.ts +9 -0
  10. package/dist/components/search/internal/SearchImageInput.vue.d.ts +716 -0
  11. package/dist/components/search/internal/SearchPanel.vue.d.ts +100 -0
  12. package/dist/components/search/types.d.ts +20 -0
  13. package/dist/components.cjs.js +41 -41
  14. package/dist/components.css +1 -1
  15. package/dist/components.es.js +11296 -10253
  16. package/dist/index.d.ts +1 -0
  17. package/package.json +5 -3
  18. package/src/assets/svg-icons/icon-delete-hover.svg +4 -0
  19. package/src/assets/svg-icons/icon-delete.svg +5 -1
  20. package/src/assets/svg-icons/icon-image-close.svg +4 -0
  21. package/src/assets/svg-icons/icon-image-upload.svg +3 -0
  22. package/src/assets/svg-icons/icon-image-zoomin.svg +3 -0
  23. package/src/assets/svg-icons/icon-refresh.svg +3 -0
  24. package/src/components/OHeaderSearch.vue +407 -425
  25. package/src/components/search/OSearchInput.vue +463 -0
  26. package/src/components/search/composables/useImageSearch.ts +157 -0
  27. package/src/components/search/composables/useKeywordHighlight.ts +30 -0
  28. package/src/components/search/composables/useSearchHistory.ts +75 -0
  29. package/src/components/search/index.ts +23 -0
  30. package/src/components/search/internal/HighlightText.vue +37 -0
  31. package/src/components/search/internal/SearchImageInput.vue +488 -0
  32. package/src/components/search/internal/SearchPanel.vue +430 -0
  33. package/src/components/search/types.ts +25 -0
  34. package/src/i18n/en.ts +10 -0
  35. package/src/i18n/zh.ts +10 -0
  36. package/src/index.ts +1 -0
  37. package/vite.config.ts +4 -0
@@ -1,18 +1,24 @@
1
1
  <script setup lang="ts">
2
- import { computed, onMounted, ref, watch } from 'vue';
3
- import { OIcon, OInput, ODivider } from '@opensig/opendesign';
4
- import { onClickOutside } from '@vueuse/core';
2
+ import { computed, ref, toRef, watch } from 'vue';
3
+ import { OIcon } from '@opensig/opendesign';
4
+ import { onClickOutside, useDebounceFn } from '@vueuse/core';
5
5
  import { useScreen } from '@opendesign-plus/composables';
6
6
 
7
- import IconClose from '~icons/components/icon-close.svg';
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
+
8
17
  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';
11
18
  import IconBack from '~icons/components/icon-header-back.svg';
12
19
 
13
- import { useI18n } from '@/i18n';
14
-
15
20
  export interface OHeaderSearchPropsT {
21
+ /** ---- Backward-compatible props ---- */
16
22
  modelValue?: string;
17
23
  placeholder?: string; // 搜索框默认提示
18
24
  expandedPlaceholder?: string; // 搜索框展开后提示
@@ -20,28 +26,37 @@ export interface OHeaderSearchPropsT {
20
26
  clearable?: boolean; // 是否显示清除按钮,默认显示
21
27
  historyItems?: string[]; // 搜索历史记录
22
28
  maxHistoryCount?: number; // 最多保存的搜索历史记录数,默认 6 条
23
- storeHistory?: boolean; // 是否使用 localStorage 存储搜索历史记录,存储之后初始化时会自动加载搜索历史记录,默认为 false
29
+ storeHistory?: boolean; // 是否使用 localStorage 存储搜索历史记录,存储之后初始化时会自动加载搜索历史记录,默认为 false
24
30
  historyTitle?: string; // 历史记录标题
25
31
  storageKey?: string; // localStorage 存储搜索历史记录的 key,默认为 search-history
26
32
  hotItems?: string[]; // 热门搜索
27
33
  hotTitle?: string; // 推荐搜索标题
28
- recommendItems?: string[]; // 推荐搜索
29
- searchUrl?: string; // 搜索页面 url,不为空点击热门搜索、历史记录、推荐搜索和回车搜索时自动打开页面
30
- searchUrlOpenBlank?: boolean; // 是否在新窗口打开搜索页面,默认为 true
31
- searchTextMobile?: 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[];
32
58
  }
33
59
 
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
-
45
60
  const props = withDefaults(defineProps<OHeaderSearchPropsT>(), {
46
61
  modelValue: '',
47
62
  expandDirection: 'left',
@@ -53,235 +68,349 @@ const props = withDefaults(defineProps<OHeaderSearchPropsT>(), {
53
68
  hotItems: () => [],
54
69
  recommendItems: () => [],
55
70
  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,
56
80
  });
57
81
 
58
- const emit = defineEmits<OHeaderSearchEmitsT>();
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();
59
107
 
60
- const searchInput = ref(props.modelValue);
61
- const searchHistoryItems = ref(props.historyItems);
108
+ const wrapperRef = ref<HTMLElement | null>(null);
109
+ const inputRef = ref<InstanceType<typeof SearchImageInput>>();
62
110
  const isShowDrawer = ref(false);
63
- const inputRef = ref();
111
+ const internalImageStaged = ref(false);
112
+ const internalImageUrl = ref('');
113
+ const hasImage = computed(() => !!props.imageUrl || internalImageStaged.value);
64
114
 
65
- const isShowClearIcon = computed(() => {
66
- return (!lePadV.value && isShowDrawer.value) || (lePadV.value && searchInput.value);
115
+ const innerValue = computed({
116
+ get: () => props.modelValue,
117
+ set: (val: string) => emit('update:modelValue', val),
67
118
  });
68
119
 
69
- watch(
70
- () => props.modelValue,
71
- (val) => {
72
- if (searchInput.value !== val) {
73
- searchInput.value = val;
74
- }
75
- }
76
- );
77
-
78
- watch(
79
- () => searchInput.value,
80
- (val) => {
81
- emit('update:modelValue', val);
82
- }
83
- );
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),
131
+ });
84
132
 
85
- watch(
86
- () => props.historyItems,
87
- (val) => {
88
- if (searchHistoryItems.value !== val) {
89
- searchHistoryItems.value = val;
90
- }
133
+ const placeholder = computed(() => {
134
+ if (props.imageUrl) {
135
+ return props.imagePlaceholder ?? t('search.extendedPlaceholder');
91
136
  }
92
- );
93
-
94
- watch(
95
- () => searchHistoryItems.value,
96
- (val) => {
97
- emit('update:historyItems', val);
137
+ if (isShowDrawer.value && props.enableImageSearch) {
138
+ return props.imagePlaceholder ?? t('search.imagePlaceholder');
98
139
  }
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
- }
140
+ if (isShowDrawer.value) {
141
+ return props.expandedPlaceholder ?? t('search.expandedPlaceholder');
111
142
  }
143
+ return props.placeholder ?? t('search.placeholder');
144
+ });
145
+
146
+ // debounce input event
147
+ const emitInputDebounced = useDebounceFn((val: string) => emit('input', val), () => props.debounce);
148
+ watch(innerValue, (val) => emitInputDebounced(val));
149
+
150
+ // click outside (desktop only)
151
+ onClickOutside(wrapperRef, () => {
152
+ if (lePadV.value) return;
153
+ closeDrawer();
112
154
  });
113
155
 
114
- const onShowDrawer = () => {
156
+ const openDrawer = () => {
115
157
  isShowDrawer.value = true;
116
158
  };
117
159
 
118
- const onSearch = () => {
119
- const input = searchInput.value.trim();
120
- if (!input) {
121
- return;
122
- }
123
-
160
+ const closeDrawer = () => {
161
+ if (lePadV.value) return;
124
162
  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();
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?.();
129
177
  }
178
+ const keyword = innerValue.value.trim();
179
+ const imageUrl = internalImageUrl.value || inputRef.value?.getUploadedUrl?.() || props.imageUrl;
180
+
181
+ if (!keyword && !imageUrl) return;
130
182
 
131
- if (props.storeHistory && props.storeHistory) {
132
- localStorage.setItem(props.storageKey, JSON.stringify(searchHistoryItems.value));
183
+ if (props.autoSaveHistory && keyword) {
184
+ history.push(keyword);
133
185
  }
134
- emit('search', input);
186
+
187
+ isShowDrawer.value = false;
188
+ emit('search', { keyword, imageUrl: imageUrl || undefined });
135
189
 
136
190
  if (props.searchUrl) {
137
- window.open(props.searchUrl + input, props.searchUrlOpenBlank ? '_blank' : '_self', 'noopener noreferrer');
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
+ }
138
199
  }
139
200
  };
140
201
 
141
- const onClear = () => {
142
- searchInput.value = '';
202
+ const handleClear = () => {
203
+ innerValue.value = '';
143
204
  emit('clear');
144
- if (!lePadV.value) {
145
- isShowDrawer.value = false;
146
- }
205
+ inputRef.value?.focus?.();
147
206
  };
148
207
 
149
- const onDeleteHistory = () => {
150
- const history = [...searchHistoryItems.value];
151
- searchHistoryItems.value = [];
152
- if (props.storeHistory && props.storeHistory) {
153
- localStorage.removeItem(props.storageKey);
154
- }
208
+ const handleSuggestClick = (item: OSearchRecommendItem) => {
209
+ emit('recommend-click', item);
210
+ innerValue.value = item.key;
211
+ runSearch();
212
+ };
155
213
 
156
- emit('delete-history', history);
214
+ const handleRecommendClick = (val: string) => {
215
+ emit('recommend-click', val);
216
+ innerValue.value = val;
217
+ runSearch();
157
218
  };
158
219
 
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
- }
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');
167
224
  }
225
+ };
226
+
227
+ const handleHistoryClick = (val: string) => {
228
+ emit('history-click', val);
229
+ };
230
+
231
+ const handleHotClick = (val: string) => {
232
+ emit('hot-click', val);
233
+ };
168
234
 
235
+ const handleHistoryRemove = (val: string) => {
236
+ history.remove(val);
169
237
  emit('delete-history-item', val);
170
238
  };
171
239
 
172
- const onWordSearch = (val: string) => {
173
- searchInput.value = val;
174
- onSearch();
240
+ const handleHistoryClear = () => {
241
+ const removed = [...history.items.value];
242
+ history.clearAll();
243
+ emit('delete-history', removed);
175
244
  };
176
245
 
177
- const onBack = () => {
178
- searchInput.value = '';
246
+ const handleBack = () => {
247
+ innerValue.value = '';
248
+ internalImageStaged.value = false;
249
+ emit('update:imageUrl', '');
179
250
  isShowDrawer.value = false;
180
251
  };
181
252
 
182
- const posWrapper = ref();
183
- onClickOutside(posWrapper, onClear);
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
+ });
184
284
  </script>
185
285
 
186
286
  <template>
187
287
  <div class="o-header-search">
188
288
  <div
189
- ref="posWrapper"
289
+ ref="wrapperRef"
290
+ class="o-header-search-pos"
190
291
  :class="{
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,
292
+ 'is-pc': !lePadV,
293
+ 'is-mobile': lePadV,
294
+ 'is-left': expandDirection === 'left',
295
+ 'is-right': expandDirection === 'right',
296
+ 'is-focus': isShowDrawer,
196
297
  }"
197
298
  >
198
- <div class="o-header-search-input-wrapper" :class="{ focus: isShowDrawer }">
199
- <OIcon v-if="lePadV && isShowDrawer" class="o-header-search-icon" @click="onBack">
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">
200
301
  <IconBack />
201
302
  </OIcon>
202
303
 
203
- <OInput
304
+ <SearchImageInput
204
305
  ref="inputRef"
205
- v-model="searchInput"
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"
206
317
  class="o-header-search-input"
207
- :placeholder="isShowDrawer ? expandedPlaceholder ?? t('search.expandedPlaceholder') : placeholder ?? t('search.placeholder')"
208
- @focus="onShowDrawer"
209
- @keyup.enter="onSearch"
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)"
210
329
  >
211
- <template #prefix>
212
- <slot name="input-prefix">
213
- <OIcon class="o-header-search-icon">
214
- <IconSearch />
215
- </OIcon>
216
- </slot>
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" />
217
334
  </template>
335
+ </SearchImageInput>
218
336
 
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>
337
+ <span v-if="lePadV && isShowDrawer" class="o-header-search-text" @click="runSearch">
338
+ {{ searchTextMobile ?? t('search') }}
339
+ </span>
229
340
  </div>
230
341
 
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>
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>
282
411
  </div>
283
412
 
284
- <OIcon v-if="lePadV" class="o-header-search-icon-mobile" @click="onShowDrawer">
413
+ <OIcon v-if="lePadV" class="o-header-search-mobile-icon" @click="openDrawer">
285
414
  <IconSearch />
286
415
  </OIcon>
287
416
  </div>
@@ -293,117 +422,110 @@ onClickOutside(posWrapper, onClear);
293
422
  width: 160px;
294
423
  height: 32px;
295
424
 
296
- @include respond('<=laptop') {
425
+ @include respond-to('<=laptop') {
297
426
  width: 120px;
298
427
  }
299
428
 
300
- @media screen and (max-width: 1080px) {
429
+ @include respond-to('<=pad_v') {
301
430
  width: 24px;
302
431
  height: 24px;
432
+ margin-left: auto;
303
433
  }
304
434
  }
305
435
 
306
- .o-header-search-icon {
307
- cursor: pointer;
308
- color: var(--o-color-info1);
309
- @include h4;
310
-
311
- @include respond('<=pad_v') {
312
- font-size: 20px;
313
- }
314
-
315
- .close {
316
- @include x-svg-hover;
317
- }
318
- }
319
-
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') {
328
- display: block;
329
- }
330
- }
331
-
332
- .o-header-search-input-pc-wrapper {
436
+ .o-header-search-pos {
333
437
  position: absolute;
334
- right: 0;
335
438
  top: 0;
336
439
  width: fit-content;
337
440
  background-color: var(--o-color-fill2);
338
441
  z-index: 100;
339
- }
340
-
341
- .o-header-search-input-pc-wrapper-left {
342
- right: 0;
343
- }
344
442
 
345
- .o-header-search-input-pc-wrapper-right {
346
- left: 0;
347
- }
348
-
349
- .o-header-search-input-pc-wrapper.focus {
350
- box-shadow: var(--o-shadow-2);
351
- top: calc(-1 * var(--o-gap-4));
352
- }
443
+ &.is-left {
444
+ right: 0;
445
+ }
446
+ &.is-right {
447
+ left: 0;
448
+ }
449
+ &.is-mobile {
450
+ display: none;
451
+ }
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);
456
+ }
353
457
 
354
- .o-header-search-input-mobile-wrapper {
355
- display: none;
458
+ &.is-mobile.is-focus {
459
+ position: fixed;
460
+ top: 0;
461
+ right: 0;
462
+ bottom: 0;
463
+ left: 0;
464
+ width: 100%;
465
+ display: block;
466
+ height: 100vh;
467
+ background-color: var(--o-color-fill2);
468
+ z-index: 100;
469
+ overflow-y: auto;
470
+ }
356
471
  }
357
472
 
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
- }
473
+ .o-header-search-row {
474
+ display: flex;
475
+ align-items: start;
370
476
 
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);
477
+ &.is-focus {
478
+ padding: var(--o-gap-4);
479
+ border-radius: var(--o-radius-xs);
375
480
 
376
- @include respond('<=laptop') {
377
- width: 120px;
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;
378
485
  }
379
486
  }
380
487
  }
381
488
 
382
- .o-header-search-input-wrapper.focus {
383
- padding: var(--o-gap-4);
489
+ .o-header-search-input {
490
+ width: 160px;
491
+ transition: width var(--o-duration-m2) var(--o-easing-standard-in);
384
492
 
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);
493
+ @include respond-to('<=laptop') {
494
+ width: 120px;
390
495
  }
496
+ }
391
497
 
392
- .o-header-search-input {
393
- width: 480px;
498
+ .o-header-search-row.is-focus .o-header-search-input {
499
+ width: 480px;
394
500
 
395
- @include respond('<=laptop') {
396
- width: 240px;
397
- }
501
+ @include respond-to('<=laptop') {
502
+ width: 240px;
503
+ }
398
504
 
399
- @include respond('<=pad_v') {
400
- flex: 1;
401
- }
505
+ @include respond-to('<=pad_v') {
506
+ flex: 1;
507
+ width: auto;
402
508
  }
403
509
  }
404
510
 
405
- .o-header-search-icon.close {
406
- @include x-svg-hover;
511
+ .o-header-search-back-icon {
512
+ cursor: pointer;
513
+ color: var(--o-color-info1);
514
+ font-size: 20px;
515
+ height: 30px;
516
+ }
517
+
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;
525
+
526
+ @include hover {
527
+ color: var(--o-color-primary1);
528
+ }
407
529
  }
408
530
 
409
531
  .o-header-search-drawer {
@@ -413,13 +535,15 @@ onClickOutside(posWrapper, onClear);
413
535
  padding-top: var(--o-gap-2);
414
536
  background-color: var(--o-color-fill2);
415
537
  box-shadow: var(--o-shadow-2);
538
+ border-radius: 0 0 var(--o-radius-xs) var(--o-radius-xs);
416
539
 
417
- @include respond('<=pad_v') {
540
+ @include respond-to('<=pad_v') {
418
541
  position: static;
419
542
  height: calc(100vh - 50px);
420
543
  padding-top: 0;
421
544
  overflow-y: auto;
422
545
  box-shadow: unset;
546
+ border-radius: unset;
423
547
  }
424
548
  }
425
549
 
@@ -431,171 +555,29 @@ onClickOutside(posWrapper, onClear);
431
555
  top: -14px;
432
556
  height: 14px;
433
557
  background-color: var(--o-color-fill2);
434
- box-shadow: unset;
435
558
 
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') {
559
+ @include respond-to('<=pad_v') {
447
560
  display: none;
448
561
  }
449
562
  }
450
563
 
451
- .o-header-search-recommend-container {
564
+ .o-header-search-mobile-icon {
565
+ font-size: 24px;
566
+ line-height: 28px;
452
567
  color: var(--o-color-info1);
453
- margin-bottom: var(--o-gap-3);
454
- }
455
-
456
- .o-header-search-recommend-item {
457
568
  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;
507
569
  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
570
 
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
- }
540
- }
541
- }
542
-
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
- }
553
- }
554
-
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;
571
+ @include respond-to('<=pad_v') {
572
+ display: block;
585
573
  }
586
574
  }
587
575
 
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
- }
576
+ .o-header-search-drawer-enter-active {
577
+ transition: opacity var(--o-duration-m1);
595
578
  }
596
579
 
597
- .o-header-search-text {
598
- font-size: 16px;
599
- line-height: 24px;
580
+ .o-header-search-drawer-enter-from {
581
+ opacity: 0;
600
582
  }
601
583
  </style>