@opendesign-plus/components 0.0.1-rc.25 → 0.0.1-rc.27

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 +111 -88
  3. package/dist/components/OHeaderSearch.vue.d.ts +814 -534
  4. package/dist/components/OHeaderUser.vue.d.ts +1 -1
  5. package/dist/components/OLanguageSwitcher.vue.d.ts +49 -0
  6. package/dist/components/OThemeSwitcher.vue.d.ts +1 -1
  7. package/dist/components/activity/OActivityMyCalendar.vue.d.ts +4 -4
  8. package/dist/components/activity/index.d.ts +2 -2
  9. package/dist/components/banner/OBanner.vue.d.ts +13 -0
  10. package/dist/components/banner/OBannerContent.vue.d.ts +7 -0
  11. package/dist/components/banner/index.d.ts +68 -0
  12. package/dist/components/banner/types.d.ts +31 -0
  13. package/dist/components/meeting/OMeetingCalendar.vue.d.ts +5 -3
  14. package/dist/components/meeting/OMeetingMyCalendar.vue.d.ts +4 -4
  15. package/dist/components/meeting/OMeetingPlayback.vue.d.ts +50 -1
  16. package/dist/components/meeting/components/OMeetingCalendarSelector.vue.d.ts +1 -1
  17. package/dist/components/meeting/components/OMeetingPlaybackSubtitles.vue.d.ts +16 -1
  18. package/dist/components/meeting/composables/useMeetingConfig.d.ts +1 -1
  19. package/dist/components/meeting/index.d.ts +17 -7
  20. package/dist/components/meeting/types.d.ts +1 -1
  21. package/dist/components/search/OSearchInput.vue.d.ts +1005 -0
  22. package/dist/components/search/composables/useImageSearch.d.ts +48 -0
  23. package/dist/components/search/composables/useKeywordHighlight.d.ts +2 -0
  24. package/dist/components/search/composables/useSearchHistory.d.ts +14 -0
  25. package/dist/components/search/index.d.ts +590 -0
  26. package/dist/components/search/internal/HighlightText.vue.d.ts +9 -0
  27. package/dist/components/search/internal/SearchImageInput.vue.d.ts +716 -0
  28. package/dist/components/search/internal/SearchPanel.vue.d.ts +100 -0
  29. package/dist/components/search/types.d.ts +20 -0
  30. package/dist/components.cjs.js +41 -41
  31. package/dist/components.css +1 -1
  32. package/dist/components.es.js +11383 -10083
  33. package/dist/index.d.ts +4 -2
  34. package/package.json +4 -4
  35. package/scripts/generate-components-index.js +1 -1
  36. package/src/assets/styles/element-plus.scss +16 -9
  37. package/src/assets/svg-icons/icon-delete-hover.svg +4 -0
  38. package/src/assets/svg-icons/icon-delete.svg +5 -1
  39. package/src/assets/svg-icons/icon-image-close.svg +4 -0
  40. package/src/assets/svg-icons/icon-image-upload.svg +3 -0
  41. package/src/assets/svg-icons/icon-image-zoomin.svg +3 -0
  42. package/src/assets/svg-icons/icon-refresh.svg +3 -0
  43. package/src/components/OHeaderSearch.vue +445 -418
  44. package/src/components/OLanguageSwitcher.vue +211 -0
  45. package/src/components/OPlusConfigProvider.vue +2 -2
  46. package/src/components/activity/OActivityForm.vue +7 -3
  47. package/src/components/activity/OActivityMyCalendar.vue +16 -7
  48. package/src/components/banner/OBanner.vue +288 -0
  49. package/src/components/banner/OBannerContent.vue +175 -0
  50. package/src/components/banner/index.ts +18 -0
  51. package/src/components/banner/types.ts +39 -0
  52. package/src/components/header/OHeader.vue +1 -1
  53. package/src/components/meeting/OMeetingCalendar.vue +11 -6
  54. package/src/components/meeting/OMeetingForm.vue +55 -9
  55. package/src/components/meeting/OMeetingMyCalendar.vue +17 -14
  56. package/src/components/meeting/OMeetingPlayback.vue +10 -4
  57. package/src/components/meeting/OMeetingSigCalendar.vue +1 -1
  58. package/src/components/meeting/components/OMeetingCalendarList.vue +57 -21
  59. package/src/components/meeting/components/OMeetingCalendarSelector.vue +11 -8
  60. package/src/components/meeting/components/OMeetingDetail.vue +1 -1
  61. package/src/components/meeting/components/OMeetingPlaybackSubtitles.vue +7 -4
  62. package/src/components/meeting/composables/useMeetingConfig.ts +5 -5
  63. package/src/components/meeting/types.ts +1 -1
  64. package/src/components/search/OSearchInput.vue +526 -0
  65. package/src/components/search/composables/useImageSearch.ts +157 -0
  66. package/src/components/search/composables/useKeywordHighlight.ts +30 -0
  67. package/src/components/search/composables/useSearchHistory.ts +75 -0
  68. package/src/components/search/index.ts +23 -0
  69. package/src/components/search/internal/HighlightText.vue +37 -0
  70. package/src/components/search/internal/SearchImageInput.vue +498 -0
  71. package/src/components/search/internal/SearchPanel.vue +431 -0
  72. package/src/components/search/types.ts +25 -0
  73. package/src/i18n/en.ts +13 -1
  74. package/src/i18n/zh.ts +14 -3
  75. package/src/index.ts +5 -3
  76. package/vite.config.ts +4 -0
  77. package/dist/components/OBanner.vue.d.ts +0 -11
  78. package/src/components/OBanner.vue +0 -398
@@ -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,354 @@ 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 (overrideKeyword?: string) => {
180
+ if (inputRef.value?.getIsUploading?.()) {
181
+ await inputRef.value.awaitUpload?.();
129
182
  }
183
+ const keyword = overrideKeyword !== undefined ? overrideKeyword.trim() : 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(item.key ?? '');
218
+ };
155
219
 
156
- emit('delete-history', history);
220
+ const handleRecommendClick = (val: string) => {
221
+ emit('recommend-click', val);
222
+ runSearch(val);
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
+ };
231
+
232
+ const handleHistoryClick = (val: string) => {
233
+ emit('history-click', val);
234
+ runSearch(val);
235
+ };
168
236
 
237
+ const handleHotClick = (val: string) => {
238
+ emit('hot-click', val);
239
+ runSearch(val);
240
+ };
241
+
242
+ const handleHistoryRemove = (val: string) => {
243
+ history.remove(val);
169
244
  emit('delete-history-item', val);
170
245
  };
171
246
 
172
- const onWordSearch = (val: string) => {
173
- searchInput.value = val;
174
- onSearch();
247
+ const handleHistoryClear = () => {
248
+ const removed = [...history.items.value];
249
+ history.clearAll();
250
+ emit('delete-history', removed);
175
251
  };
176
252
 
177
- const onBack = () => {
178
- searchInput.value = '';
253
+ const handleBack = () => {
254
+ innerValue.value = '';
255
+ internalImageStaged.value = false;
256
+ emit('update:imageUrl', '');
179
257
  isShowDrawer.value = false;
180
258
  };
181
259
 
182
- const posWrapper = ref();
183
- onClickOutside(posWrapper, onClear);
260
+ const handleImageUploadStart = (file: File) => {
261
+ internalImageStaged.value = true;
262
+ internalImageUrl.value = '';
263
+ emit('image-upload-start', file);
264
+ };
265
+
266
+ const handleImageUploadSuccess = (url: string, file: File) => {
267
+ internalImageUrl.value = url;
268
+ emit('update:imageUrl', url);
269
+ emit('image-upload-success', url, file);
270
+ };
271
+
272
+ const handleImageClear = () => {
273
+ internalImageStaged.value = false;
274
+ internalImageUrl.value = '';
275
+ emit('update:imageUrl', '');
276
+ };
277
+
278
+ const showPanel = computed(() => {
279
+ if (!isShowDrawer.value) return false;
280
+ if (hasImage.value) return false;
281
+ return true;
282
+ });
283
+
284
+ defineExpose({
285
+ focus: () => inputRef.value?.focus?.(),
286
+ blur: () => inputRef.value?.blur?.(),
287
+ open: openDrawer,
288
+ close: closeDrawer,
289
+ search: runSearch,
290
+ });
184
291
  </script>
185
292
 
186
293
  <template>
187
- <div class="o-header-search">
294
+ <div class="o-header-search" :class="{ 'is-mobile-mode': isMobileMode }">
188
295
  <div
189
- ref="posWrapper"
296
+ ref="wrapperRef"
297
+ class="o-header-search-pos"
190
298
  :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,
299
+ 'is-pc': !isMobileMode,
300
+ 'is-mobile': isMobileMode,
301
+ 'is-left': expandDirection === 'left',
302
+ 'is-right': expandDirection === 'right',
303
+ 'is-focus': isShowDrawer,
196
304
  }"
197
305
  >
198
- <div class="o-header-search-input-wrapper" :class="{ focus: isShowDrawer }">
199
- <OIcon v-if="lePadV && isShowDrawer" class="o-header-search-icon" @click="onBack">
306
+ <div class="o-header-search-row" :class="{ 'is-focus': isShowDrawer }">
307
+ <OIcon v-if="isMobileMode && isShowDrawer" class="o-header-search-back-icon" @click="handleBack">
200
308
  <IconBack />
201
309
  </OIcon>
202
310
 
203
- <OInput
311
+ <SearchImageInput
204
312
  ref="inputRef"
205
- v-model="searchInput"
313
+ v-model="innerValue"
314
+ :image-url="imageUrl"
315
+ :placeholder="placeholder"
316
+ :enable-image-search="enableImageSearch"
317
+ :upload-image="uploadImage"
318
+ :max-image-size="maxImageSize"
319
+ :image-upload-tooltip="imageUploadTooltip"
320
+ :clearable="clearable && isShowDrawer"
321
+ :expanded="isShowDrawer && hasImage"
322
+ :allowed-image-types="allowedImageTypes"
323
+ size="medium"
206
324
  class="o-header-search-input"
207
- :placeholder="isShowDrawer ? expandedPlaceholder ?? t('search.expandedPlaceholder') : placeholder ?? t('search.placeholder')"
208
- @focus="onShowDrawer"
209
- @keyup.enter="onSearch"
325
+ :class="{ 'is-collapsed': !isShowDrawer }"
326
+ @update:imageUrl="(url: string) => emit('update:imageUrl', url)"
327
+ @focus="handleFocus"
328
+ @blur="handleBlur"
329
+ @enter="runSearch"
330
+ @clear="handleClear"
331
+ @image-clear="handleImageClear"
332
+ @image-upload-start="handleImageUploadStart"
333
+ @image-upload-success="handleImageUploadSuccess"
334
+ @image-upload-error="(error: unknown, file: File) => emit('image-upload-error', error, file)"
335
+ @image-validate-error="(reason: 'size' | 'type', file: File) => emit('image-validate-error', reason, file)"
210
336
  >
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>
337
+ <template #prefix><slot name="input-prefix" /></template>
338
+ <template #suffix="slotProps"><slot name="input-suffix" v-bind="slotProps" /></template>
339
+ <template v-if="$slots['image-preview']" #preview="slotProps">
340
+ <slot name="image-preview" v-bind="slotProps" />
225
341
  </template>
226
- </OInput>
342
+ </SearchImageInput>
227
343
 
228
- <span v-if="lePadV && isShowDrawer" class="o-header-search-text" @click="onSearch">{{ searchTextMobile ?? t('search') }}</span>
344
+ <span v-if="isMobileMode && isShowDrawer" class="o-header-search-text" @click="runSearch">
345
+ {{ searchTextMobile ?? t('search') }}
346
+ </span>
229
347
  </div>
230
348
 
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>
349
+ <Transition name="o-header-search-drawer">
350
+ <div v-if="showPanel" class="o-header-search-drawer">
351
+ <slot
352
+ name="drawer"
353
+ :recommend-items="recommendItems"
354
+ :history-items="history.items.value"
355
+ :hot-items="hotItems"
356
+ :suggest-items="suggestItems"
357
+ :onestep-items="onestepItems"
358
+ :keyword="modelValue"
359
+ >
360
+ <SearchPanel
361
+ :keyword="modelValue"
362
+ :onestep-items="onestepItems"
363
+ :onestep-title="onestepTitle"
364
+ :suggest-items="suggestItems"
365
+ :suggest-title="suggestTitle"
366
+ :recommend-items="recommendItems"
367
+ :history-items="history.items.value"
368
+ :history-title="historyTitle"
369
+ :hot-items="hotItems"
370
+ :hot-title="hotTitle"
371
+ :no-data-text="noDataText"
372
+ :highlight-keyword="highlightKeyword"
373
+ :hide-on-keyword="true"
374
+ :show-suggest-empty="showSuggestEmpty"
375
+ @onestep-click="handleOnestepClick"
376
+ @suggest-click="handleSuggestClick"
377
+ @recommend-click="handleRecommendClick"
378
+ @history-click="handleHistoryClick"
379
+ @history-remove="handleHistoryRemove"
380
+ @history-clear="handleHistoryClear"
381
+ @hot-click="handleHotClick"
382
+ @hot-refresh="emit('hot-refresh')"
383
+ >
384
+ <template v-if="$slots['recommend-header']" #recommend-header="slotProps">
385
+ <slot name="recommend-header" v-bind="slotProps" />
386
+ </template>
387
+ <template v-if="$slots['recommend-content']" #recommend-content="slotProps">
388
+ <slot name="recommend-content" v-bind="slotProps" />
389
+ </template>
390
+ <template v-if="$slots['onestep-header']" #onestep-header="slotProps">
391
+ <slot name="onestep-header" v-bind="slotProps" />
392
+ </template>
393
+ <template v-if="$slots['onestep-content']" #onestep-content="slotProps">
394
+ <slot name="onestep-content" v-bind="slotProps" />
395
+ </template>
396
+ <template v-if="$slots['suggest-header']" #suggest-header="slotProps">
397
+ <slot name="suggest-header" v-bind="slotProps" />
398
+ </template>
399
+ <template v-if="$slots['suggest-content']" #suggest-content="slotProps">
400
+ <slot name="suggest-content" v-bind="slotProps" />
401
+ </template>
402
+ <template v-if="$slots['history-header']" #history-header="slotProps">
403
+ <slot name="history-header" v-bind="slotProps" />
404
+ </template>
405
+ <template v-if="$slots['history-content']" #history-content="slotProps">
406
+ <slot name="history-content" v-bind="slotProps" />
407
+ </template>
408
+ <template v-if="$slots['hot-header']" #hot-header="slotProps">
409
+ <slot name="hot-header" v-bind="slotProps" />
410
+ </template>
411
+ <template v-if="$slots['hot-content']" #hot-content="slotProps">
412
+ <slot name="hot-content" v-bind="slotProps" />
413
+ </template>
414
+ </SearchPanel>
415
+ </slot>
416
+ </div>
417
+ </Transition>
282
418
  </div>
283
419
 
284
- <OIcon v-if="lePadV" class="o-header-search-icon-mobile" @click="onShowDrawer">
420
+ <OIcon v-if="isMobileMode" class="o-header-search-mobile-icon" @click="openDrawer">
285
421
  <IconSearch />
286
422
  </OIcon>
287
423
  </div>
@@ -293,133 +429,132 @@ onClickOutside(posWrapper, onClear);
293
429
  width: 160px;
294
430
  height: 32px;
295
431
 
296
- @include respond('<=laptop') {
432
+ @include respond-to('<=laptop') {
297
433
  width: 120px;
298
434
  }
299
435
 
300
- @media screen and (max-width: 1080px) {
436
+ @include respond-to('<=pad_v') {
301
437
  width: 24px;
302
438
  height: 24px;
439
+ margin-left: auto;
303
440
  }
304
441
  }
305
442
 
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 {
443
+ .o-header-search-pos {
333
444
  position: absolute;
334
- right: 0;
335
445
  top: 0;
336
446
  width: fit-content;
337
447
  background-color: var(--o-color-fill2);
338
448
  z-index: 100;
339
- }
340
449
 
341
- .o-header-search-input-pc-wrapper-left {
342
- right: 0;
343
- }
450
+ &.is-left {
451
+ right: 0;
452
+ }
453
+ &.is-right {
454
+ left: 0;
455
+ }
456
+ &.is-mobile {
457
+ display: none;
458
+ }
459
+ &.is-pc.is-focus {
460
+ box-shadow: var(--o-shadow-2);
461
+ top: calc(-1 * var(--o-gap-4));
462
+ border-radius: var(--o-radius-xs);
463
+ }
344
464
 
345
- .o-header-search-input-pc-wrapper-right {
346
- left: 0;
465
+ &.is-mobile.is-focus {
466
+ position: fixed;
467
+ top: 0;
468
+ right: 0;
469
+ bottom: 0;
470
+ left: 0;
471
+ width: 100%;
472
+ display: block;
473
+ height: 100vh;
474
+ background-color: var(--o-color-fill2);
475
+ z-index: 100;
476
+ overflow-y: auto;
477
+ }
347
478
  }
348
479
 
349
- .o-header-search-input-pc-wrapper.focus {
350
- box-shadow: var(--o-shadow-2);
351
- top: calc(-1 * var(--o-gap-4));
352
- }
480
+ .o-header-search-row {
481
+ display: flex;
482
+ align-items: start;
353
483
 
354
- .o-header-search-input-mobile-wrapper {
355
- display: none;
356
- }
484
+ &.is-focus {
485
+ padding: var(--o-gap-4);
486
+ border-radius: var(--o-radius-xs);
357
487
 
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;
488
+ @include respond-to('<=pad_v') {
489
+ gap: var(--o-gap-4);
490
+ padding: 10px var(--o-gap-4) var(--o-gap-4) var(--o-gap-4);
491
+ border-radius: unset;
492
+ }
493
+ }
369
494
  }
370
495
 
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);
496
+ .o-header-search-input {
497
+ width: 160px;
498
+ transition: width var(--o-duration-m2) var(--o-easing-standard-in);
375
499
 
376
- @include respond('<=laptop') {
377
- width: 120px;
378
- }
500
+ @include respond-to('<=laptop') {
501
+ width: 120px;
379
502
  }
380
503
  }
381
504
 
382
- .o-header-search-input-wrapper.focus {
383
- padding: var(--o-gap-4);
505
+ .o-header-search-row.is-focus .o-header-search-input {
506
+ width: 480px;
384
507
 
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);
508
+ @include respond-to('laptop') {
509
+ width: 360px;
390
510
  }
391
511
 
392
- .o-header-search-input {
393
- width: 480px;
394
-
395
- @include respond('<=laptop') {
396
- width: 240px;
397
- }
512
+ @include respond-to('pad_h') {
513
+ width: 272px;
514
+ }
398
515
 
399
- @include respond('<=pad_v') {
400
- flex: 1;
401
- }
516
+ @include respond-to('<=pad_v') {
517
+ flex: 1;
518
+ width: auto;
402
519
  }
403
520
  }
404
521
 
405
- .o-header-search-icon.close {
406
- @include x-svg-hover;
522
+ .o-header-search-back-icon {
523
+ cursor: pointer;
524
+ color: var(--o-color-info1);
525
+ font-size: 20px;
526
+ height: 30px;
527
+ }
528
+
529
+ .o-header-search-text {
530
+ white-space: nowrap;
531
+ font-size: 16px;
532
+ line-height: 24px;
533
+ cursor: pointer;
534
+ color: var(--o-color-info1);
535
+ height: 30px;
536
+
537
+ @include hover {
538
+ color: var(--o-color-primary1);
539
+ }
407
540
  }
408
541
 
409
542
  .o-header-search-drawer {
410
543
  position: absolute;
411
544
  width: 100%;
412
- padding: var(--o-gap-5);
413
- padding-top: var(--o-gap-2);
545
+ padding: var(--o-gap-4);
546
+ padding-top: 0;
414
547
  background-color: var(--o-color-fill2);
415
548
  box-shadow: var(--o-shadow-2);
549
+ border-radius: 0 0 var(--o-radius-xs) var(--o-radius-xs);
416
550
 
417
- @include respond('<=pad_v') {
551
+ @include respond-to('<=pad_v') {
418
552
  position: static;
419
553
  height: calc(100vh - 50px);
420
554
  padding-top: 0;
421
555
  overflow-y: auto;
422
556
  box-shadow: unset;
557
+ border-radius: unset;
423
558
  }
424
559
  }
425
560
 
@@ -428,174 +563,66 @@ onClickOutside(posWrapper, onClear);
428
563
  position: absolute;
429
564
  left: 0;
430
565
  right: 0;
431
- top: -14px;
432
- height: 14px;
566
+ top: calc(-1 * var(--o-gap-4));
567
+ height: calc(var(--o-gap-4) + 1px);
433
568
  background-color: var(--o-color-fill2);
434
- box-shadow: unset;
435
569
 
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') {
570
+ @include respond-to('<=pad_v') {
447
571
  display: none;
448
572
  }
449
573
  }
450
574
 
451
- .o-header-search-recommend-container {
575
+ .o-header-search-mobile-icon {
576
+ font-size: 24px;
577
+ line-height: 28px;
452
578
  color: var(--o-color-info1);
453
- margin-bottom: var(--o-gap-3);
454
- }
455
-
456
- .o-header-search-recommend-item {
457
579
  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);
580
+ display: none;
489
581
 
490
- @include respond('<=pad_v') {
491
- @include text2;
492
- color: var(--o-color-info1);
582
+ @include respond-to('<=pad_v') {
583
+ display: block;
493
584
  }
494
585
  }
495
586
 
496
- .o-header-search-history-item-container {
497
- display: flex;
498
- gap: 8px;
499
- flex-wrap: wrap;
500
- margin-top: var(--o-gap-2);
587
+ .o-header-search-drawer-enter-active {
588
+ transition: opacity var(--o-duration-m1);
501
589
  }
502
590
 
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
- }
591
+ .o-header-search-drawer-enter-from {
592
+ opacity: 0;
520
593
  }
521
594
 
522
- .o-header-search-history-item {
523
- position: relative;
524
- display: flex;
525
- align-items: center;
526
- max-width: 224px;
595
+ // 外部传入 mobile=true 时,复制 <=pad_v 的全部样式,不依赖媒体查询
596
+ .o-header-search.is-mobile-mode {
597
+ width: 24px;
527
598
  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
599
 
555
- .o-header-search-drawer-divider {
556
- --o-divider-gap: var(--o-gap-4);
557
-
558
- @include respond('<=pad_v') {
559
- display: none;
600
+ .o-header-search-mobile-icon {
601
+ display: block;
560
602
  }
561
- }
562
603
 
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;
604
+ .o-header-search-row.is-focus {
605
+ gap: var(--o-gap-4);
606
+ padding: 10px var(--o-gap-4) var(--o-gap-4) var(--o-gap-4);
607
+ border-radius: unset;
570
608
  }
571
- }
572
609
 
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;
610
+ .o-header-search-row.is-focus .o-header-search-input {
611
+ flex: 1;
612
+ width: auto;
585
613
  }
586
- }
587
614
 
588
- .o-header-search-hot-item {
589
- color: var(--o-color-info1);
590
- cursor: pointer;
615
+ .o-header-search-drawer {
616
+ position: static;
617
+ height: calc(100vh - 50px);
618
+ padding-top: 0;
619
+ overflow-y: auto;
620
+ box-shadow: unset;
621
+ border-radius: unset;
591
622
 
592
- @include hover {
593
- color: var(--o-color-primary1);
623
+ &::before {
624
+ display: none;
625
+ }
594
626
  }
595
627
  }
596
-
597
- .o-header-search-text {
598
- font-size: 16px;
599
- line-height: 24px;
600
- }
601
628
  </style>