@opendesign-plus-test/components 0.0.1-rc.50 → 0.0.1-rc.51

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