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

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