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

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 +1001 -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 +587 -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 +42 -42
  14. package/dist/components.css +1 -1
  15. package/dist/components.es.js +11437 -10379
  16. package/dist/index.d.ts +1 -0
  17. package/package.json +1 -1
  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 +448 -418
  25. package/src/components/search/OSearchInput.vue +503 -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 +498 -0
  32. package/src/components/search/internal/SearchPanel.vue +431 -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,357 @@ 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;
130
185
 
131
- if (props.storeHistory && props.storeHistory) {
132
- localStorage.setItem(props.storageKey, JSON.stringify(searchHistoryItems.value));
186
+ if (!keyword && !imageUrl) return;
187
+
188
+ if (props.autoSaveHistory && keyword) {
189
+ history.push(keyword);
133
190
  }
134
- emit('search', input);
191
+
192
+ emit('search', { keyword, imageUrl: imageUrl || undefined });
193
+ inputRef.value?.blur?.();
194
+ isShowDrawer.value = false;
135
195
 
136
196
  if (props.searchUrl) {
137
- window.open(props.searchUrl + input, props.searchUrlOpenBlank ? '_blank' : '_self', 'noopener noreferrer');
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
+ }
138
205
  }
139
206
  };
140
207
 
141
- const onClear = () => {
142
- searchInput.value = '';
208
+ const handleClear = () => {
209
+ innerValue.value = '';
143
210
  emit('clear');
144
- if (!lePadV.value) {
145
- isShowDrawer.value = false;
146
- }
211
+ inputRef.value?.focus?.();
147
212
  };
148
213
 
149
- const onDeleteHistory = () => {
150
- const history = [...searchHistoryItems.value];
151
- searchHistoryItems.value = [];
152
- if (props.storeHistory && props.storeHistory) {
153
- localStorage.removeItem(props.storageKey);
154
- }
214
+ const handleSuggestClick = (item: OSearchRecommendItem) => {
215
+ emit('recommend-click', item);
216
+ innerValue.value = item.key;
217
+ runSearch();
218
+ };
155
219
 
156
- emit('delete-history', history);
220
+ const handleRecommendClick = (val: string) => {
221
+ emit('recommend-click', val);
222
+ innerValue.value = val;
223
+ runSearch();
157
224
  };
158
225
 
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
- }
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');
167
230
  }
231
+ };
232
+
233
+ const handleHistoryClick = (val: string) => {
234
+ emit('history-click', val);
235
+ innerValue.value = val;
236
+ runSearch();
237
+ };
168
238
 
239
+ const handleHotClick = (val: string) => {
240
+ emit('hot-click', val);
241
+ innerValue.value = val;
242
+ runSearch();
243
+ };
244
+
245
+ const handleHistoryRemove = (val: string) => {
246
+ history.remove(val);
169
247
  emit('delete-history-item', val);
170
248
  };
171
249
 
172
- const onWordSearch = (val: string) => {
173
- searchInput.value = val;
174
- onSearch();
250
+ const handleHistoryClear = () => {
251
+ const removed = [...history.items.value];
252
+ history.clearAll();
253
+ emit('delete-history', removed);
175
254
  };
176
255
 
177
- const onBack = () => {
178
- searchInput.value = '';
256
+ const handleBack = () => {
257
+ innerValue.value = '';
258
+ internalImageStaged.value = false;
259
+ emit('update:imageUrl', '');
179
260
  isShowDrawer.value = false;
180
261
  };
181
262
 
182
- const posWrapper = ref();
183
- onClickOutside(posWrapper, onClear);
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
+ });
184
294
  </script>
185
295
 
186
296
  <template>
187
- <div class="o-header-search">
297
+ <div class="o-header-search" :class="{ 'is-mobile-mode': isMobileMode }">
188
298
  <div
189
- ref="posWrapper"
299
+ ref="wrapperRef"
300
+ class="o-header-search-pos"
190
301
  :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,
302
+ 'is-pc': !isMobileMode,
303
+ 'is-mobile': isMobileMode,
304
+ 'is-left': expandDirection === 'left',
305
+ 'is-right': expandDirection === 'right',
306
+ 'is-focus': isShowDrawer,
196
307
  }"
197
308
  >
198
- <div class="o-header-search-input-wrapper" :class="{ focus: isShowDrawer }">
199
- <OIcon v-if="lePadV && isShowDrawer" class="o-header-search-icon" @click="onBack">
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">
200
311
  <IconBack />
201
312
  </OIcon>
202
313
 
203
- <OInput
314
+ <SearchImageInput
204
315
  ref="inputRef"
205
- v-model="searchInput"
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"
206
327
  class="o-header-search-input"
207
- :placeholder="isShowDrawer ? expandedPlaceholder ?? t('search.expandedPlaceholder') : placeholder ?? t('search.placeholder')"
208
- @focus="onShowDrawer"
209
- @keyup.enter="onSearch"
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)"
210
339
  >
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>
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" />
225
344
  </template>
226
- </OInput>
345
+ </SearchImageInput>
227
346
 
228
- <span v-if="lePadV && isShowDrawer" class="o-header-search-text" @click="onSearch">{{ searchTextMobile ?? t('search') }}</span>
347
+ <span v-if="isMobileMode && isShowDrawer" class="o-header-search-text" @click="runSearch">
348
+ {{ searchTextMobile ?? t('search') }}
349
+ </span>
229
350
  </div>
230
351
 
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>
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>
282
421
  </div>
283
422
 
284
- <OIcon v-if="lePadV" class="o-header-search-icon-mobile" @click="onShowDrawer">
423
+ <OIcon v-if="isMobileMode" class="o-header-search-mobile-icon" @click="openDrawer">
285
424
  <IconSearch />
286
425
  </OIcon>
287
426
  </div>
@@ -293,133 +432,132 @@ onClickOutside(posWrapper, onClear);
293
432
  width: 160px;
294
433
  height: 32px;
295
434
 
296
- @include respond('<=laptop') {
435
+ @include respond-to('<=laptop') {
297
436
  width: 120px;
298
437
  }
299
438
 
300
- @media screen and (max-width: 1080px) {
439
+ @include respond-to('<=pad_v') {
301
440
  width: 24px;
302
441
  height: 24px;
442
+ margin-left: auto;
303
443
  }
304
444
  }
305
445
 
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 {
446
+ .o-header-search-pos {
333
447
  position: absolute;
334
- right: 0;
335
448
  top: 0;
336
449
  width: fit-content;
337
450
  background-color: var(--o-color-fill2);
338
451
  z-index: 100;
339
- }
340
452
 
341
- .o-header-search-input-pc-wrapper-left {
342
- right: 0;
343
- }
453
+ &.is-left {
454
+ right: 0;
455
+ }
456
+ &.is-right {
457
+ left: 0;
458
+ }
459
+ &.is-mobile {
460
+ display: none;
461
+ }
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);
466
+ }
344
467
 
345
- .o-header-search-input-pc-wrapper-right {
346
- left: 0;
468
+ &.is-mobile.is-focus {
469
+ position: fixed;
470
+ top: 0;
471
+ right: 0;
472
+ bottom: 0;
473
+ left: 0;
474
+ width: 100%;
475
+ display: block;
476
+ height: 100vh;
477
+ background-color: var(--o-color-fill2);
478
+ z-index: 100;
479
+ overflow-y: auto;
480
+ }
347
481
  }
348
482
 
349
- .o-header-search-input-pc-wrapper.focus {
350
- box-shadow: var(--o-shadow-2);
351
- top: calc(-1 * var(--o-gap-4));
352
- }
483
+ .o-header-search-row {
484
+ display: flex;
485
+ align-items: start;
353
486
 
354
- .o-header-search-input-mobile-wrapper {
355
- display: none;
356
- }
487
+ &.is-focus {
488
+ padding: var(--o-gap-4);
489
+ border-radius: var(--o-radius-xs);
357
490
 
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;
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
+ }
369
497
  }
370
498
 
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);
499
+ .o-header-search-input {
500
+ width: 160px;
501
+ transition: width var(--o-duration-m2) var(--o-easing-standard-in);
375
502
 
376
- @include respond('<=laptop') {
377
- width: 120px;
378
- }
503
+ @include respond-to('<=laptop') {
504
+ width: 120px;
379
505
  }
380
506
  }
381
507
 
382
- .o-header-search-input-wrapper.focus {
383
- padding: var(--o-gap-4);
508
+ .o-header-search-row.is-focus .o-header-search-input {
509
+ width: 480px;
384
510
 
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);
511
+ @include respond-to('laptop') {
512
+ width: 360px;
390
513
  }
391
514
 
392
- .o-header-search-input {
393
- width: 480px;
394
-
395
- @include respond('<=laptop') {
396
- width: 240px;
397
- }
515
+ @include respond-to('pad_h') {
516
+ width: 272px;
517
+ }
398
518
 
399
- @include respond('<=pad_v') {
400
- flex: 1;
401
- }
519
+ @include respond-to('<=pad_v') {
520
+ flex: 1;
521
+ width: auto;
402
522
  }
403
523
  }
404
524
 
405
- .o-header-search-icon.close {
406
- @include x-svg-hover;
525
+ .o-header-search-back-icon {
526
+ cursor: pointer;
527
+ color: var(--o-color-info1);
528
+ font-size: 20px;
529
+ height: 30px;
530
+ }
531
+
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;
539
+
540
+ @include hover {
541
+ color: var(--o-color-primary1);
542
+ }
407
543
  }
408
544
 
409
545
  .o-header-search-drawer {
410
546
  position: absolute;
411
547
  width: 100%;
412
- padding: var(--o-gap-5);
413
- padding-top: var(--o-gap-2);
548
+ padding: var(--o-gap-4);
549
+ padding-top: 0;
414
550
  background-color: var(--o-color-fill2);
415
551
  box-shadow: var(--o-shadow-2);
552
+ border-radius: 0 0 var(--o-radius-xs) var(--o-radius-xs);
416
553
 
417
- @include respond('<=pad_v') {
554
+ @include respond-to('<=pad_v') {
418
555
  position: static;
419
556
  height: calc(100vh - 50px);
420
557
  padding-top: 0;
421
558
  overflow-y: auto;
422
559
  box-shadow: unset;
560
+ border-radius: unset;
423
561
  }
424
562
  }
425
563
 
@@ -428,174 +566,66 @@ onClickOutside(posWrapper, onClear);
428
566
  position: absolute;
429
567
  left: 0;
430
568
  right: 0;
431
- top: -14px;
432
- height: 14px;
569
+ top: calc(-1 * var(--o-gap-4));
570
+ height: calc(var(--o-gap-4) + 1px);
433
571
  background-color: var(--o-color-fill2);
434
- box-shadow: unset;
435
572
 
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') {
573
+ @include respond-to('<=pad_v') {
447
574
  display: none;
448
575
  }
449
576
  }
450
577
 
451
- .o-header-search-recommend-container {
578
+ .o-header-search-mobile-icon {
579
+ font-size: 24px;
580
+ line-height: 28px;
452
581
  color: var(--o-color-info1);
453
- margin-bottom: var(--o-gap-3);
454
- }
455
-
456
- .o-header-search-recommend-item {
457
582
  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);
583
+ display: none;
489
584
 
490
- @include respond('<=pad_v') {
491
- @include text2;
492
- color: var(--o-color-info1);
585
+ @include respond-to('<=pad_v') {
586
+ display: block;
493
587
  }
494
588
  }
495
589
 
496
- .o-header-search-history-item-container {
497
- display: flex;
498
- gap: 8px;
499
- flex-wrap: wrap;
500
- margin-top: var(--o-gap-2);
590
+ .o-header-search-drawer-enter-active {
591
+ transition: opacity var(--o-duration-m1);
501
592
  }
502
593
 
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
- }
594
+ .o-header-search-drawer-enter-from {
595
+ opacity: 0;
520
596
  }
521
597
 
522
- .o-header-search-history-item {
523
- position: relative;
524
- display: flex;
525
- align-items: center;
526
- max-width: 224px;
598
+ // 外部传入 mobile=true 时,复制 <=pad_v 的全部样式,不依赖媒体查询
599
+ .o-header-search.is-mobile-mode {
600
+ width: 24px;
527
601
  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
602
 
555
- .o-header-search-drawer-divider {
556
- --o-divider-gap: var(--o-gap-4);
557
-
558
- @include respond('<=pad_v') {
559
- display: none;
603
+ .o-header-search-mobile-icon {
604
+ display: block;
560
605
  }
561
- }
562
606
 
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;
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;
570
611
  }
571
- }
572
612
 
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;
613
+ .o-header-search-row.is-focus .o-header-search-input {
614
+ flex: 1;
615
+ width: auto;
585
616
  }
586
- }
587
617
 
588
- .o-header-search-hot-item {
589
- color: var(--o-color-info1);
590
- cursor: pointer;
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;
591
625
 
592
- @include hover {
593
- color: var(--o-color-primary1);
626
+ &::before {
627
+ display: none;
628
+ }
594
629
  }
595
630
  }
596
-
597
- .o-header-search-text {
598
- font-size: 16px;
599
- line-height: 24px;
600
- }
601
631
  </style>