@opendesign-plus-test/components 0.0.1-rc.46 → 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 (78) hide show
  1. package/dist/chunk-OElCookieNotice.cjs.js +1 -1
  2. package/dist/chunk-OElCookieNotice.es.js +67 -47
  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 +4 -2
  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/OBanner.vue +18 -18
  25. package/src/components/OCookieNotice.vue +21 -21
  26. package/src/components/OFooter.vue +18 -17
  27. package/src/components/OHeaderSearch.vue +402 -420
  28. package/src/components/OHeaderUser.vue +3 -2
  29. package/src/components/OSection.vue +4 -4
  30. package/src/components/activity/OActivityApproval.vue +4 -4
  31. package/src/components/activity/OActivityForm.vue +2 -2
  32. package/src/components/activity/OMyActivityCalendar.vue +26 -26
  33. package/src/components/common/ContentWrapper.vue +3 -3
  34. package/src/components/element-plus/OElCookieNotice.vue +26 -26
  35. package/src/components/events/OEventsApply.vue +44 -44
  36. package/src/components/events/OEventsCalendar.vue +14 -14
  37. package/src/components/events/OEventsList.vue +16 -16
  38. package/src/components/header/OHeader.vue +2 -2
  39. package/src/components/header/components/HeaderContent.vue +60 -60
  40. package/src/components/header/components/HeaderNav.vue +4 -4
  41. package/src/components/header/components/HeaderNavMobile.vue +3 -3
  42. package/src/components/meeting/OMeetingCalendar.vue +27 -27
  43. package/src/components/meeting/OMeetingForm.vue +16 -16
  44. package/src/components/meeting/OMeetingPlayback.vue +4 -4
  45. package/src/components/meeting/OMyMeetingCalendar.vue +25 -25
  46. package/src/components/meeting/OSigMeetingCalendar.vue +3 -3
  47. package/src/components/meeting/components/OMeetingCalendarList.vue +9 -9
  48. package/src/components/meeting/components/OMeetingDetail.vue +2 -2
  49. package/src/components/meeting/components/OMeetingPlaybackSubtitles.vue +1 -1
  50. package/src/components/meeting/components/OMeetingPlaybackVideo.vue +5 -5
  51. package/src/components/meeting/components/OSigMeetingAside.vue +6 -6
  52. package/src/components/search/OSearchInput.vue +463 -0
  53. package/src/components/search/composables/useImageSearch.ts +157 -0
  54. package/src/components/search/composables/useKeywordHighlight.ts +30 -0
  55. package/src/components/search/composables/useSearchHistory.ts +75 -0
  56. package/src/components/search/index.ts +23 -0
  57. package/src/components/search/internal/HighlightText.vue +37 -0
  58. package/src/components/search/internal/SearchImageInput.vue +488 -0
  59. package/src/components/search/internal/SearchPanel.vue +430 -0
  60. package/src/components/search/types.ts +25 -0
  61. package/src/draft/Banner.vue +6 -6
  62. package/src/draft/ButtonCards.vue +1 -1
  63. package/src/draft/Feature.vue +6 -6
  64. package/src/draft/Footer.vue +29 -22
  65. package/src/draft/HorizontalAnchor.vue +4 -4
  66. package/src/draft/ItemSwiper.vue +2 -2
  67. package/src/draft/Logo.vue +3 -3
  68. package/src/draft/LogoCard.vue +2 -2
  69. package/src/draft/MultiCard.vue +1 -1
  70. package/src/draft/MultiIconCard.vue +1 -1
  71. package/src/draft/OInfoCard.vue +4 -4
  72. package/src/draft/Section.vue +4 -4
  73. package/src/draft/SingleTabCard.vue +1 -1
  74. package/src/draft/SliderCard.vue +4 -3
  75. package/src/i18n/en.ts +10 -0
  76. package/src/i18n/zh.ts +10 -0
  77. package/src/index.ts +1 -0
  78. package/vite.config.ts +5 -1
@@ -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,27 +26,36 @@ 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; // 手机端搜索按钮文字,默认为搜索
32
- }
33
-
34
- export interface OHeaderSearchEmitsT {
35
- (e: 'update:modelValue', value: string): void;
36
- (e: 'update:historyItems', value: string[]): void;
37
- (e: 'clear'): void;
38
- (e: 'search', value: string): void;
39
- (e: 'delete-history', value: string[]): void;
40
- (e: 'delete-history-item', value: string): void;
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[];
41
58
  }
42
- const { lePadV } = useScreen();
43
- const { t } = useI18n();
44
59
 
45
60
  const props = withDefaults(defineProps<OHeaderSearchPropsT>(), {
46
61
  modelValue: '',
@@ -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');
112
144
  });
113
145
 
114
- const onShowDrawer = () => {
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();
154
+ });
155
+
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
+ };
168
230
 
231
+ const handleHotClick = (val: string) => {
232
+ emit('hot-click', val);
233
+ };
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>
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>
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" />
225
334
  </template>
226
- </OInput>
335
+ </SearchImageInput>
227
336
 
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>
@@ -297,115 +426,108 @@ onClickOutside(posWrapper, onClear);
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-to('<=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-to('<=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
442
 
341
- .o-header-search-input-pc-wrapper-left {
342
- right: 0;
343
- }
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
+ }
344
457
 
345
- .o-header-search-input-pc-wrapper-right {
346
- left: 0;
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
+ }
347
471
  }
348
472
 
349
- .o-header-search-input-pc-wrapper.focus {
350
- box-shadow: var(--o-shadow-2);
351
- top: calc(-1 * var(--o-gap-4));
352
- }
473
+ .o-header-search-row {
474
+ display: flex;
475
+ align-items: start;
353
476
 
354
- .o-header-search-input-mobile-wrapper {
355
- display: none;
356
- }
477
+ &.is-focus {
478
+ padding: var(--o-gap-4);
479
+ border-radius: var(--o-radius-xs);
357
480
 
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;
481
+ @include respond-to('<=pad_v') {
482
+ gap: var(--o-gap-4);
483
+ padding: 10px var(--o-gap-4) var(--o-gap-4) var(--o-gap-4);
484
+ border-radius: unset;
485
+ }
486
+ }
369
487
  }
370
488
 
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);
489
+ .o-header-search-input {
490
+ width: 160px;
491
+ transition: width var(--o-duration-m2) var(--o-easing-standard-in);
375
492
 
376
- @include respond-to('<=laptop') {
377
- width: 120px;
378
- }
493
+ @include respond-to('<=laptop') {
494
+ width: 120px;
379
495
  }
380
496
  }
381
497
 
382
- .o-header-search-input-wrapper.focus {
383
- padding: var(--o-gap-4);
498
+ .o-header-search-row.is-focus .o-header-search-input {
499
+ width: 480px;
500
+
501
+ @include respond-to('<=laptop') {
502
+ width: 240px;
503
+ }
384
504
 
385
505
  @include respond-to('<=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);
506
+ flex: 1;
507
+ width: auto;
390
508
  }
509
+ }
391
510
 
392
- .o-header-search-input {
393
- width: 480px;
511
+ .o-header-search-back-icon {
512
+ cursor: pointer;
513
+ color: var(--o-color-info1);
514
+ font-size: 20px;
515
+ height: 30px;
516
+ }
394
517
 
395
- @include respond-to('<=laptop') {
396
- width: 240px;
397
- }
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;
398
525
 
399
- @include respond-to('<=pad_v') {
400
- flex: 1;
401
- }
526
+ @include hover {
527
+ color: var(--o-color-primary1);
402
528
  }
403
529
  }
404
530
 
405
- .o-header-search-icon.close {
406
- @include x-svg-hover;
407
- }
408
-
409
531
  .o-header-search-drawer {
410
532
  position: absolute;
411
533
  width: 100%;
@@ -413,6 +535,7 @@ 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
540
  @include respond-to('<=pad_v') {
418
541
  position: static;
@@ -420,6 +543,7 @@ onClickOutside(posWrapper, onClear);
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
-
436
- @include respond-to('<=laptop') {
437
- top: -10px;
438
- height: 10px;
439
- }
440
-
441
- @include respond-to('<=pad') {
442
- top: -8px;
443
- height: 8px;
444
- }
445
558
 
446
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-to('<=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-to('<=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-to('<=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-mixedgray-9));
514
-
515
- .icon-delete {
516
- height: 16px;
517
- width: 16px;
518
- color: var(--o-color-white);
519
- }
520
- }
521
-
522
- .o-header-search-history-item {
523
- position: relative;
524
- display: flex;
525
- align-items: center;
526
- max-width: 224px;
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
570
 
550
571
  @include respond-to('<=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-to('<=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-to('<=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-to('<=pad_v') {
581
- flex-direction: column;
582
- gap: 12px;
583
- font-size: 12px;
584
- line-height: 18px;
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>