@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.
- package/dist/chunk-OElCookieNotice.cjs.js +1 -1
- package/dist/chunk-OElCookieNotice.es.js +67 -47
- package/dist/components/OHeaderSearch.vue.d.ts +812 -534
- package/dist/components/search/OSearchInput.vue.d.ts +1003 -0
- package/dist/components/search/composables/useImageSearch.d.ts +48 -0
- package/dist/components/search/composables/useKeywordHighlight.d.ts +2 -0
- package/dist/components/search/composables/useSearchHistory.d.ts +14 -0
- package/dist/components/search/index.d.ts +590 -0
- package/dist/components/search/internal/HighlightText.vue.d.ts +9 -0
- package/dist/components/search/internal/SearchImageInput.vue.d.ts +716 -0
- package/dist/components/search/internal/SearchPanel.vue.d.ts +100 -0
- package/dist/components/search/types.d.ts +20 -0
- package/dist/components.cjs.js +41 -41
- package/dist/components.css +1 -1
- package/dist/components.es.js +11296 -10253
- package/dist/index.d.ts +1 -0
- package/package.json +4 -2
- package/src/assets/svg-icons/icon-delete-hover.svg +4 -0
- package/src/assets/svg-icons/icon-delete.svg +5 -1
- package/src/assets/svg-icons/icon-image-close.svg +4 -0
- package/src/assets/svg-icons/icon-image-upload.svg +3 -0
- package/src/assets/svg-icons/icon-image-zoomin.svg +3 -0
- package/src/assets/svg-icons/icon-refresh.svg +3 -0
- package/src/components/OBanner.vue +18 -18
- package/src/components/OCookieNotice.vue +21 -21
- package/src/components/OFooter.vue +18 -17
- package/src/components/OHeaderSearch.vue +402 -420
- package/src/components/OHeaderUser.vue +3 -2
- package/src/components/OSection.vue +4 -4
- package/src/components/activity/OActivityApproval.vue +4 -4
- package/src/components/activity/OActivityForm.vue +2 -2
- package/src/components/activity/OMyActivityCalendar.vue +26 -26
- package/src/components/common/ContentWrapper.vue +3 -3
- package/src/components/element-plus/OElCookieNotice.vue +26 -26
- package/src/components/events/OEventsApply.vue +44 -44
- package/src/components/events/OEventsCalendar.vue +14 -14
- package/src/components/events/OEventsList.vue +16 -16
- package/src/components/header/OHeader.vue +2 -2
- package/src/components/header/components/HeaderContent.vue +60 -60
- package/src/components/header/components/HeaderNav.vue +4 -4
- package/src/components/header/components/HeaderNavMobile.vue +3 -3
- package/src/components/meeting/OMeetingCalendar.vue +27 -27
- package/src/components/meeting/OMeetingForm.vue +16 -16
- package/src/components/meeting/OMeetingPlayback.vue +4 -4
- package/src/components/meeting/OMyMeetingCalendar.vue +25 -25
- package/src/components/meeting/OSigMeetingCalendar.vue +3 -3
- package/src/components/meeting/components/OMeetingCalendarList.vue +9 -9
- package/src/components/meeting/components/OMeetingDetail.vue +2 -2
- package/src/components/meeting/components/OMeetingPlaybackSubtitles.vue +1 -1
- package/src/components/meeting/components/OMeetingPlaybackVideo.vue +5 -5
- package/src/components/meeting/components/OSigMeetingAside.vue +6 -6
- package/src/components/search/OSearchInput.vue +463 -0
- package/src/components/search/composables/useImageSearch.ts +157 -0
- package/src/components/search/composables/useKeywordHighlight.ts +30 -0
- package/src/components/search/composables/useSearchHistory.ts +75 -0
- package/src/components/search/index.ts +23 -0
- package/src/components/search/internal/HighlightText.vue +37 -0
- package/src/components/search/internal/SearchImageInput.vue +488 -0
- package/src/components/search/internal/SearchPanel.vue +430 -0
- package/src/components/search/types.ts +25 -0
- package/src/draft/Banner.vue +6 -6
- package/src/draft/ButtonCards.vue +1 -1
- package/src/draft/Feature.vue +6 -6
- package/src/draft/Footer.vue +29 -22
- package/src/draft/HorizontalAnchor.vue +4 -4
- package/src/draft/ItemSwiper.vue +2 -2
- package/src/draft/Logo.vue +3 -3
- package/src/draft/LogoCard.vue +2 -2
- package/src/draft/MultiCard.vue +1 -1
- package/src/draft/MultiIconCard.vue +1 -1
- package/src/draft/OInfoCard.vue +4 -4
- package/src/draft/Section.vue +4 -4
- package/src/draft/SingleTabCard.vue +1 -1
- package/src/draft/SliderCard.vue +4 -3
- package/src/i18n/en.ts +10 -0
- package/src/i18n/zh.ts +10 -0
- package/src/index.ts +1 -0
- package/vite.config.ts +5 -1
|
@@ -652,12 +652,12 @@ defineExpose({
|
|
|
652
652
|
|
|
653
653
|
.vjs-progress-holder {
|
|
654
654
|
margin: 0;
|
|
655
|
-
background: rgba(var(--o-
|
|
655
|
+
background: rgba(var(--o-grey-1), 0.3);
|
|
656
656
|
}
|
|
657
657
|
|
|
658
658
|
.vjs-load-progress {
|
|
659
659
|
div {
|
|
660
|
-
background: rgba(var(--o-
|
|
660
|
+
background: rgba(var(--o-grey-1), 0.5);
|
|
661
661
|
}
|
|
662
662
|
}
|
|
663
663
|
|
|
@@ -724,7 +724,7 @@ defineExpose({
|
|
|
724
724
|
background-repeat: no-repeat;
|
|
725
725
|
background-size: cover;
|
|
726
726
|
border: 0.5px solid rgba(var(--o-white), 0.6);
|
|
727
|
-
background-color: rgba(var(--o-
|
|
727
|
+
background-color: rgba(var(--o-grey-1), 0.2);
|
|
728
728
|
|
|
729
729
|
.vjs-icon-placeholder::before {
|
|
730
730
|
content: none;
|
|
@@ -733,8 +733,8 @@ defineExpose({
|
|
|
733
733
|
|
|
734
734
|
@include hover {
|
|
735
735
|
.vjs-big-play-button {
|
|
736
|
-
border: 0.5px solid rgba(var(--o-
|
|
737
|
-
background-color: rgba(var(--o-
|
|
736
|
+
border: 0.5px solid rgba(var(--o-grey-1), 0.6);
|
|
737
|
+
background-color: rgba(var(--o-grey-1), 0.2);
|
|
738
738
|
}
|
|
739
739
|
}
|
|
740
740
|
}
|
|
@@ -84,17 +84,17 @@ const changeMonth = (step: number) => {
|
|
|
84
84
|
display: flex;
|
|
85
85
|
flex-direction: column;
|
|
86
86
|
border-right: 1px solid var(--o-color-control4);
|
|
87
|
-
@include respond
|
|
87
|
+
@include respond('<=pad') {
|
|
88
88
|
padding: var(--o-gap-4);
|
|
89
89
|
border-right: none;
|
|
90
90
|
}
|
|
91
|
-
@include respond
|
|
91
|
+
@include respond('phone') {
|
|
92
92
|
padding: 16px;
|
|
93
93
|
.title {
|
|
94
94
|
margin-bottom: var(--o-gap-2);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
@include respond
|
|
97
|
+
@include respond('<=pad_v') {
|
|
98
98
|
width: 100%;
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -112,7 +112,7 @@ const changeMonth = (step: number) => {
|
|
|
112
112
|
display: grid;
|
|
113
113
|
grid-template-columns: repeat(2, 1fr);
|
|
114
114
|
|
|
115
|
-
@include respond
|
|
115
|
+
@include respond('<=pad_v') {
|
|
116
116
|
display: flex;
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -185,11 +185,11 @@ const changeMonth = (step: number) => {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
@include respond
|
|
188
|
+
@include respond('<=pad_v') {
|
|
189
189
|
.day-item {
|
|
190
190
|
}
|
|
191
191
|
}
|
|
192
|
-
@include respond
|
|
192
|
+
@include respond('<=pad_v') {
|
|
193
193
|
.day-item {
|
|
194
194
|
width: min(128px, calc((100% - var(--o-gap-2)) / 2));
|
|
195
195
|
}
|
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, toRef, watch } from 'vue';
|
|
3
|
+
import { onClickOutside, useDebounceFn } from '@vueuse/core';
|
|
4
|
+
|
|
5
|
+
import SearchImageInput from './internal/SearchImageInput.vue';
|
|
6
|
+
import SearchPanel from './internal/SearchPanel.vue';
|
|
7
|
+
import { useSearchHistory } from './composables/useSearchHistory';
|
|
8
|
+
import { useI18n } from '@/i18n';
|
|
9
|
+
import type {
|
|
10
|
+
OSearchPayload,
|
|
11
|
+
OSearchRecommendItem,
|
|
12
|
+
OSearchUploadImageFn,
|
|
13
|
+
} from './types';
|
|
14
|
+
|
|
15
|
+
export interface OSearchInputPropsT {
|
|
16
|
+
modelValue?: string;
|
|
17
|
+
imageUrl?: string;
|
|
18
|
+
size?: 'small' | 'medium' | 'large';
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
imagePlaceholder?: string;
|
|
21
|
+
|
|
22
|
+
/** ---- Image search ---- */
|
|
23
|
+
enableImageSearch?: boolean;
|
|
24
|
+
uploadImage?: OSearchUploadImageFn;
|
|
25
|
+
maxImageSize?: number;
|
|
26
|
+
imageUploadTooltip?: string;
|
|
27
|
+
|
|
28
|
+
/** ---- Recommendation lists (controlled by parent) ---- */
|
|
29
|
+
suggestItems?: OSearchRecommendItem[];
|
|
30
|
+
onestepItems?: OSearchRecommendItem[];
|
|
31
|
+
suggestTitle?: string;
|
|
32
|
+
onestepTitle?: string;
|
|
33
|
+
noDataText?: string;
|
|
34
|
+
/** When true, show "no data" placeholder for empty suggest while typing */
|
|
35
|
+
showSuggestEmpty?: boolean;
|
|
36
|
+
highlightKeyword?: boolean;
|
|
37
|
+
debounce?: number;
|
|
38
|
+
|
|
39
|
+
/** ---- History ---- */
|
|
40
|
+
enableHistory?: boolean;
|
|
41
|
+
historyItems?: string[];
|
|
42
|
+
maxHistoryCount?: number;
|
|
43
|
+
storeHistory?: boolean;
|
|
44
|
+
storageKey?: string;
|
|
45
|
+
historyTitle?: string;
|
|
46
|
+
/** Auto-record history on search; default true */
|
|
47
|
+
autoSaveHistory?: boolean;
|
|
48
|
+
|
|
49
|
+
/** ---- "Did you mean" list (below input) ---- */
|
|
50
|
+
suggestList?: string[];
|
|
51
|
+
suggestListLabel?: string;
|
|
52
|
+
/** When true, render suggestList items as HTML (with v-html on inert string) */
|
|
53
|
+
allowHtmlInSuggest?: boolean;
|
|
54
|
+
|
|
55
|
+
/** ---- Misc ---- */
|
|
56
|
+
clearable?: boolean;
|
|
57
|
+
closeOnSearch?: boolean;
|
|
58
|
+
closeOnClickOutside?: boolean;
|
|
59
|
+
/** Show dropdown on focus even when empty (history) */
|
|
60
|
+
openOnFocus?: boolean;
|
|
61
|
+
/** Always show image as inline thumbnail inside the input; never expand preview below */
|
|
62
|
+
alwaysInlineThumbnail?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const props = withDefaults(defineProps<OSearchInputPropsT>(), {
|
|
66
|
+
modelValue: '',
|
|
67
|
+
imageUrl: '',
|
|
68
|
+
size: 'large',
|
|
69
|
+
enableImageSearch: false,
|
|
70
|
+
maxImageSize: 10 * 1024 * 1024,
|
|
71
|
+
suggestItems: () => [],
|
|
72
|
+
onestepItems: () => [],
|
|
73
|
+
showSuggestEmpty: true,
|
|
74
|
+
highlightKeyword: true,
|
|
75
|
+
debounce: 300,
|
|
76
|
+
enableHistory: true,
|
|
77
|
+
historyItems: () => [],
|
|
78
|
+
maxHistoryCount: 6,
|
|
79
|
+
storeHistory: false,
|
|
80
|
+
storageKey: 'search-history',
|
|
81
|
+
autoSaveHistory: true,
|
|
82
|
+
suggestList: () => [],
|
|
83
|
+
allowHtmlInSuggest: false,
|
|
84
|
+
clearable: true,
|
|
85
|
+
closeOnSearch: true,
|
|
86
|
+
closeOnClickOutside: true,
|
|
87
|
+
openOnFocus: true,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const emit = defineEmits<{
|
|
91
|
+
(e: 'update:modelValue', val: string): void;
|
|
92
|
+
(e: 'update:imageUrl', url: string): void;
|
|
93
|
+
(e: 'update:historyItems', items: string[]): void;
|
|
94
|
+
(e: 'focus'): void;
|
|
95
|
+
(e: 'blur'): void;
|
|
96
|
+
(e: 'input', val: string): void;
|
|
97
|
+
(e: 'clear'): void;
|
|
98
|
+
(e: 'search', payload: OSearchPayload): void;
|
|
99
|
+
(e: 'recommend-click', item: OSearchRecommendItem): void;
|
|
100
|
+
(e: 'onestep-click', item: OSearchRecommendItem): void;
|
|
101
|
+
(e: 'history-click', val: string): void;
|
|
102
|
+
(e: 'suggest-list-click', val: string): void;
|
|
103
|
+
(e: 'delete-history', items: string[]): void;
|
|
104
|
+
(e: 'delete-history-item', val: string): void;
|
|
105
|
+
(e: 'image-upload-start', file: File): void;
|
|
106
|
+
(e: 'image-upload-success', url: string, file: File): void;
|
|
107
|
+
(e: 'image-upload-error', error: unknown, file: File): void;
|
|
108
|
+
(e: 'image-validate-error', reason: 'size' | 'type', file: File): void;
|
|
109
|
+
}>();
|
|
110
|
+
|
|
111
|
+
const { t } = useI18n();
|
|
112
|
+
|
|
113
|
+
const inputRef = ref<InstanceType<typeof SearchImageInput>>();
|
|
114
|
+
const wrapperRef = ref<HTMLElement | null>(null);
|
|
115
|
+
|
|
116
|
+
const innerValue = computed({
|
|
117
|
+
get: () => props.modelValue,
|
|
118
|
+
set: (val: string) => emit('update:modelValue', val),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const isFocus = ref(false);
|
|
122
|
+
const isPreviewOpen = ref(false);
|
|
123
|
+
const justClosedPreview = ref(false);
|
|
124
|
+
const hasInternalImage = ref(false);
|
|
125
|
+
|
|
126
|
+
const showDropdown = computed(() => {
|
|
127
|
+
if (!isFocus.value) return false;
|
|
128
|
+
if (props.imageUrl || hasInternalImage.value) return false;
|
|
129
|
+
const hasSuggest =
|
|
130
|
+
!!innerValue.value &&
|
|
131
|
+
(props.suggestItems.length > 0 || props.onestepItems.length > 0 || props.showSuggestEmpty);
|
|
132
|
+
const hasHistory = props.enableHistory && history.items.value.length > 0 && !innerValue.value;
|
|
133
|
+
if (hasSuggest) return true;
|
|
134
|
+
if (props.openOnFocus && hasHistory) return true;
|
|
135
|
+
return false;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// history
|
|
139
|
+
const historyItemsRef = toRef(props, 'historyItems');
|
|
140
|
+
const storageKeyRef = toRef(props, 'storageKey');
|
|
141
|
+
const storeHistoryRef = toRef(props, 'storeHistory');
|
|
142
|
+
const maxHistoryRef = toRef(props, 'maxHistoryCount');
|
|
143
|
+
|
|
144
|
+
const history = useSearchHistory({
|
|
145
|
+
initial: historyItemsRef,
|
|
146
|
+
storageKey: storageKeyRef,
|
|
147
|
+
storeHistory: storeHistoryRef,
|
|
148
|
+
maxHistoryCount: maxHistoryRef,
|
|
149
|
+
onChange: (items) => emit('update:historyItems', items),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// debounced input event
|
|
153
|
+
const emitInputDebounced = useDebounceFn((val: string) => {
|
|
154
|
+
emit('input', val);
|
|
155
|
+
}, () => props.debounce);
|
|
156
|
+
|
|
157
|
+
watch(innerValue, (val) => {
|
|
158
|
+
emitInputDebounced(val);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// click outside
|
|
162
|
+
onClickOutside(wrapperRef, () => {
|
|
163
|
+
if (!props.closeOnClickOutside) return;
|
|
164
|
+
if (isPreviewOpen.value || justClosedPreview.value) return;
|
|
165
|
+
isFocus.value = false;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const handleFocus = () => {
|
|
169
|
+
isFocus.value = true;
|
|
170
|
+
emit('focus');
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const handleBlur = () => {
|
|
174
|
+
emit('blur');
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const runSearch = async () => {
|
|
178
|
+
if (inputRef.value?.getIsUploading?.()) {
|
|
179
|
+
await inputRef.value.awaitUpload?.();
|
|
180
|
+
}
|
|
181
|
+
const keyword = innerValue.value.trim();
|
|
182
|
+
const imageUrl = inputRef.value?.getUploadedUrl?.() || props.imageUrl;
|
|
183
|
+
|
|
184
|
+
if (!keyword && !imageUrl) return;
|
|
185
|
+
|
|
186
|
+
if (props.autoSaveHistory && keyword) {
|
|
187
|
+
history.push(keyword);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (props.closeOnSearch) {
|
|
191
|
+
isFocus.value = false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
emit('search', { keyword, imageUrl: imageUrl || undefined });
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const handleClear = () => {
|
|
198
|
+
innerValue.value = '';
|
|
199
|
+
emit('clear');
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const handleSuggestClick = (item: OSearchRecommendItem) => {
|
|
203
|
+
emit('recommend-click', item);
|
|
204
|
+
innerValue.value = item.key;
|
|
205
|
+
runSearch();
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const handleOnestepClick = (item: OSearchRecommendItem) => {
|
|
209
|
+
emit('onestep-click', item);
|
|
210
|
+
if (item.path) {
|
|
211
|
+
const url = item.path;
|
|
212
|
+
if (typeof window !== 'undefined') window.open(url, '_blank', 'noopener,noreferrer');
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const handleHistoryClick = (val: string) => {
|
|
217
|
+
emit('history-click', val);
|
|
218
|
+
innerValue.value = val;
|
|
219
|
+
runSearch();
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const handleHistoryRemove = (val: string) => {
|
|
223
|
+
history.remove(val);
|
|
224
|
+
emit('delete-history-item', val);
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const handleHistoryClear = () => {
|
|
228
|
+
const removed = [...history.items.value];
|
|
229
|
+
history.clearAll();
|
|
230
|
+
emit('delete-history', removed);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const handleSuggestListClick = (val: string) => {
|
|
234
|
+
// Strip HTML tags when raw HTML is allowed for display
|
|
235
|
+
const text = props.allowHtmlInSuggest ? val.replace(/<[^>]+>/g, '') : val;
|
|
236
|
+
emit('suggest-list-click', text);
|
|
237
|
+
innerValue.value = text;
|
|
238
|
+
runSearch();
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const onPreviewChange = (visible: boolean) => {
|
|
242
|
+
isPreviewOpen.value = visible;
|
|
243
|
+
if (!visible) {
|
|
244
|
+
justClosedPreview.value = true;
|
|
245
|
+
setTimeout(() => {
|
|
246
|
+
justClosedPreview.value = false;
|
|
247
|
+
}, 100);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const showInlineThumbnail = computed(() =>
|
|
252
|
+
!!props.imageUrl && (props.alwaysInlineThumbnail || !isFocus.value)
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
const onImageUploadStart = (file: File) => {
|
|
256
|
+
hasInternalImage.value = true;
|
|
257
|
+
emit('image-upload-start', file);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const onImageUploadSuccess = (url: string, file: File) => {
|
|
261
|
+
hasInternalImage.value = false;
|
|
262
|
+
emit('image-upload-success', url, file);
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const onImageClear = () => {
|
|
266
|
+
hasInternalImage.value = false;
|
|
267
|
+
emit('update:imageUrl', '');
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
defineExpose({
|
|
271
|
+
focus: () => inputRef.value?.focus?.(),
|
|
272
|
+
blur: () => inputRef.value?.blur?.(),
|
|
273
|
+
search: runSearch,
|
|
274
|
+
saveHistory: (val?: string) => history.push(val ?? innerValue.value),
|
|
275
|
+
});
|
|
276
|
+
</script>
|
|
277
|
+
|
|
278
|
+
<template>
|
|
279
|
+
<div ref="wrapperRef" class="o-search-input" :class="{ 'is-focus': isFocus }">
|
|
280
|
+
<div class="o-search-input-box">
|
|
281
|
+
<SearchImageInput
|
|
282
|
+
ref="inputRef"
|
|
283
|
+
v-model="innerValue"
|
|
284
|
+
:image-url="imageUrl"
|
|
285
|
+
:placeholder="placeholder"
|
|
286
|
+
:image-placeholder="imagePlaceholder"
|
|
287
|
+
:size="size"
|
|
288
|
+
:enable-image-search="enableImageSearch"
|
|
289
|
+
:upload-image="uploadImage"
|
|
290
|
+
:max-image-size="maxImageSize"
|
|
291
|
+
:image-upload-tooltip="imageUploadTooltip"
|
|
292
|
+
:expanded="isFocus && !props.alwaysInlineThumbnail"
|
|
293
|
+
:inline-thumbnail="showInlineThumbnail"
|
|
294
|
+
:clearable="clearable"
|
|
295
|
+
class="o-search-input-field"
|
|
296
|
+
@update:imageUrl="(url: string) => emit('update:imageUrl', url)"
|
|
297
|
+
@focus="handleFocus"
|
|
298
|
+
@blur="handleBlur"
|
|
299
|
+
@enter="runSearch"
|
|
300
|
+
@clear="handleClear"
|
|
301
|
+
@image-clear="onImageClear"
|
|
302
|
+
@image-upload-start="onImageUploadStart"
|
|
303
|
+
@image-upload-success="onImageUploadSuccess"
|
|
304
|
+
@image-upload-error="(error: unknown, file: File) => emit('image-upload-error', error, file)"
|
|
305
|
+
@image-validate-error="(reason: 'size' | 'type', file: File) => emit('image-validate-error', reason, file)"
|
|
306
|
+
@preview-change="onPreviewChange"
|
|
307
|
+
>
|
|
308
|
+
<template #prefix><slot name="input-prefix" /></template>
|
|
309
|
+
<template #suffix="slotProps"><slot name="input-suffix" v-bind="slotProps" /></template>
|
|
310
|
+
<template v-if="$slots['image-preview']" #preview="slotProps">
|
|
311
|
+
<slot name="image-preview" v-bind="slotProps" />
|
|
312
|
+
</template>
|
|
313
|
+
</SearchImageInput>
|
|
314
|
+
|
|
315
|
+
<Transition name="o-search-input-dropdown">
|
|
316
|
+
<div v-if="showDropdown" class="o-search-input-dropdown">
|
|
317
|
+
<slot name="dropdown" :keyword="modelValue">
|
|
318
|
+
<SearchPanel
|
|
319
|
+
:keyword="modelValue"
|
|
320
|
+
:onestep-items="onestepItems"
|
|
321
|
+
:onestep-title="onestepTitle"
|
|
322
|
+
:suggest-items="suggestItems"
|
|
323
|
+
:suggest-title="suggestTitle"
|
|
324
|
+
:history-items="enableHistory ? history.items.value : []"
|
|
325
|
+
:history-title="historyTitle"
|
|
326
|
+
:no-data-text="noDataText"
|
|
327
|
+
:highlight-keyword="highlightKeyword"
|
|
328
|
+
:hide-on-keyword="true"
|
|
329
|
+
:show-suggest-empty="showSuggestEmpty"
|
|
330
|
+
history-layout="list"
|
|
331
|
+
@onestep-click="handleOnestepClick"
|
|
332
|
+
@suggest-click="handleSuggestClick"
|
|
333
|
+
@history-click="handleHistoryClick"
|
|
334
|
+
@history-remove="handleHistoryRemove"
|
|
335
|
+
@history-clear="handleHistoryClear"
|
|
336
|
+
>
|
|
337
|
+
<template v-if="$slots['onestep-header']" #onestep-header="slotProps">
|
|
338
|
+
<slot name="onestep-header" v-bind="slotProps" />
|
|
339
|
+
</template>
|
|
340
|
+
<template v-if="$slots['onestep-content']" #onestep-content="slotProps">
|
|
341
|
+
<slot name="onestep-content" v-bind="slotProps" />
|
|
342
|
+
</template>
|
|
343
|
+
<template v-if="$slots['suggest-header']" #suggest-header="slotProps">
|
|
344
|
+
<slot name="suggest-header" v-bind="slotProps" />
|
|
345
|
+
</template>
|
|
346
|
+
<template v-if="$slots['suggest-content']" #suggest-content="slotProps">
|
|
347
|
+
<slot name="suggest-content" v-bind="slotProps" />
|
|
348
|
+
</template>
|
|
349
|
+
<template v-if="$slots['history-header']" #history-header="slotProps">
|
|
350
|
+
<slot name="history-header" v-bind="slotProps" />
|
|
351
|
+
</template>
|
|
352
|
+
<template v-if="$slots['history-content']" #history-content="slotProps">
|
|
353
|
+
<slot name="history-content" v-bind="slotProps" />
|
|
354
|
+
</template>
|
|
355
|
+
</SearchPanel>
|
|
356
|
+
</slot>
|
|
357
|
+
</div>
|
|
358
|
+
</Transition>
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<div v-if="suggestList?.length" class="o-search-input-suggest-list-row">
|
|
362
|
+
<slot name="suggest-list" :items="suggestList">
|
|
363
|
+
<span class="o-search-input-suggest-list-label">{{ suggestListLabel ?? t('search.suggestListLabel') }}</span>
|
|
364
|
+
<ul class="o-search-input-suggest-list">
|
|
365
|
+
<li
|
|
366
|
+
v-for="(item, idx) in suggestList"
|
|
367
|
+
:key="item + idx"
|
|
368
|
+
class="o-search-input-suggest-list-item"
|
|
369
|
+
@click="handleSuggestListClick(item)"
|
|
370
|
+
>
|
|
371
|
+
<span v-if="allowHtmlInSuggest" v-html="item" />
|
|
372
|
+
<span v-else>{{ item }}</span>
|
|
373
|
+
</li>
|
|
374
|
+
</ul>
|
|
375
|
+
</slot>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
</template>
|
|
379
|
+
|
|
380
|
+
<style lang="scss" scoped>
|
|
381
|
+
.o-search-input {
|
|
382
|
+
position: relative;
|
|
383
|
+
width: 100%;
|
|
384
|
+
background-color: var(--o-color-fill2);
|
|
385
|
+
border-radius: var(--o-radius-xs);
|
|
386
|
+
|
|
387
|
+
// Make large size 48px tall
|
|
388
|
+
:deep(.o-input.o_box-large) {
|
|
389
|
+
--_box-height: 48px;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.o-search-input-box {
|
|
394
|
+
position: relative;
|
|
395
|
+
width: 100%;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
.o-search-input-field {
|
|
399
|
+
width: 100%;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.o-search-input-dropdown {
|
|
403
|
+
position: absolute;
|
|
404
|
+
top: calc(100% + 4px);
|
|
405
|
+
left: 0;
|
|
406
|
+
right: 0;
|
|
407
|
+
z-index: 10;
|
|
408
|
+
padding: var(--o-gap-4);
|
|
409
|
+
background-color: var(--o-color-fill2);
|
|
410
|
+
border-radius: var(--o-radius-xs);
|
|
411
|
+
box-shadow: var(--o-shadow-2);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.o-search-input-dropdown-enter-active,
|
|
415
|
+
.o-search-input-dropdown-leave-active {
|
|
416
|
+
transition: opacity var(--o-duration-m1), transform var(--o-duration-m1);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
.o-search-input-dropdown-enter-from,
|
|
420
|
+
.o-search-input-dropdown-leave-to {
|
|
421
|
+
opacity: 0;
|
|
422
|
+
transform: translateY(-4px);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.o-search-input-suggest-list-row {
|
|
426
|
+
display: flex;
|
|
427
|
+
margin-top: 8px;
|
|
428
|
+
align-items: center;
|
|
429
|
+
flex-wrap: wrap;
|
|
430
|
+
@include tip1;
|
|
431
|
+
color: var(--o-color-info1);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
.o-search-input-suggest-list-label {
|
|
435
|
+
color: var(--o-color-info3);
|
|
436
|
+
margin-right: 4px;
|
|
437
|
+
@include tip1;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.o-search-input-suggest-list {
|
|
441
|
+
display: flex;
|
|
442
|
+
flex-wrap: wrap;
|
|
443
|
+
align-items: center;
|
|
444
|
+
padding: 0;
|
|
445
|
+
margin: 0;
|
|
446
|
+
list-style: none;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.o-search-input-suggest-list-item {
|
|
450
|
+
margin-right: 8px;
|
|
451
|
+
cursor: pointer;
|
|
452
|
+
color: var(--o-color-primary1);
|
|
453
|
+
|
|
454
|
+
:deep(em) {
|
|
455
|
+
color: var(--o-color-primary1);
|
|
456
|
+
font-style: normal;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
@include hover {
|
|
460
|
+
text-decoration: underline;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
</style>
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { ref, onBeforeUnmount, type Ref } from 'vue';
|
|
2
|
+
import type { OSearchUploadImageFn } from '../types';
|
|
3
|
+
|
|
4
|
+
export interface UseImageSearchOptions {
|
|
5
|
+
uploadImage?: Ref<OSearchUploadImageFn | undefined>;
|
|
6
|
+
maxImageSize: Ref<number>;
|
|
7
|
+
allowedImageTypes?: Ref<string[]>;
|
|
8
|
+
onUploadStart?: (file: File) => void;
|
|
9
|
+
onUploadSuccess?: (url: string, file: File) => void;
|
|
10
|
+
onUploadError?: (error: unknown, file: File) => void;
|
|
11
|
+
onValidationError?: (reason: 'size' | 'type', file: File) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function useImageSearch(options: UseImageSearchOptions) {
|
|
15
|
+
const previewUrl = ref('');
|
|
16
|
+
const file = ref<File | null>(null);
|
|
17
|
+
const uploadedUrl = ref('');
|
|
18
|
+
const isUploading = ref(false);
|
|
19
|
+
let uploadPromise: Promise<void> | null = null;
|
|
20
|
+
|
|
21
|
+
const validate = (f: File) => {
|
|
22
|
+
const allowed = options.allowedImageTypes?.value;
|
|
23
|
+
const typeOk = allowed && allowed.length > 0 ? allowed.includes(f.type) : f.type?.startsWith('image/');
|
|
24
|
+
if (!f.type || !typeOk) {
|
|
25
|
+
options.onValidationError?.('type', f);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (options.maxImageSize.value > 0 && f.size > options.maxImageSize.value) {
|
|
29
|
+
options.onValidationError?.('size', f);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const reset = () => {
|
|
36
|
+
if (previewUrl.value) {
|
|
37
|
+
try {
|
|
38
|
+
URL.revokeObjectURL(previewUrl.value);
|
|
39
|
+
} catch {
|
|
40
|
+
// ignore
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
previewUrl.value = '';
|
|
44
|
+
file.value = null;
|
|
45
|
+
uploadedUrl.value = '';
|
|
46
|
+
isUploading.value = false;
|
|
47
|
+
uploadPromise = null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const setExternalUrl = (url: string) => {
|
|
51
|
+
if (!url) {
|
|
52
|
+
if (!file.value) reset();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (uploadedUrl.value === url) return;
|
|
56
|
+
if (previewUrl.value && previewUrl.value.startsWith('blob:')) {
|
|
57
|
+
try {
|
|
58
|
+
URL.revokeObjectURL(previewUrl.value);
|
|
59
|
+
} catch {
|
|
60
|
+
// ignore
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
uploadedUrl.value = url;
|
|
64
|
+
previewUrl.value = url;
|
|
65
|
+
file.value = null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const handleImageFile = (f: File) => {
|
|
69
|
+
if (!validate(f)) return;
|
|
70
|
+
|
|
71
|
+
reset();
|
|
72
|
+
file.value = f;
|
|
73
|
+
previewUrl.value = URL.createObjectURL(f);
|
|
74
|
+
options.onUploadStart?.(f);
|
|
75
|
+
|
|
76
|
+
const fn = options.uploadImage?.value;
|
|
77
|
+
if (!fn) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
isUploading.value = true;
|
|
82
|
+
uploadPromise = Promise.resolve()
|
|
83
|
+
.then(() => fn(f))
|
|
84
|
+
.then((url) => {
|
|
85
|
+
if (url && file.value === f) {
|
|
86
|
+
uploadedUrl.value = url;
|
|
87
|
+
options.onUploadSuccess?.(url, f);
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
.catch((err) => {
|
|
91
|
+
options.onUploadError?.(err, f);
|
|
92
|
+
})
|
|
93
|
+
.finally(() => {
|
|
94
|
+
isUploading.value = false;
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const onPaste = (event: ClipboardEvent) => {
|
|
99
|
+
const items = event.clipboardData?.items;
|
|
100
|
+
if (!items) return;
|
|
101
|
+
for (let i = 0; i < items.length; i++) {
|
|
102
|
+
const item = items[i];
|
|
103
|
+
if (item.type.indexOf('image') !== -1) {
|
|
104
|
+
const f = item.getAsFile();
|
|
105
|
+
if (f) handleImageFile(f);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const onDragOver = (event: DragEvent) => {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
event.stopPropagation();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const onDrop = (event: DragEvent) => {
|
|
117
|
+
event.preventDefault();
|
|
118
|
+
event.stopPropagation();
|
|
119
|
+
const f = event.dataTransfer?.files?.[0];
|
|
120
|
+
if (f && f.type.indexOf('image') !== -1) {
|
|
121
|
+
handleImageFile(f);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const onFileChange = (event: Event) => {
|
|
126
|
+
const target = event.target as HTMLInputElement;
|
|
127
|
+
const f = target.files?.[0];
|
|
128
|
+
if (f) handleImageFile(f);
|
|
129
|
+
if (target) target.value = '';
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const pickFile = (input: HTMLInputElement | undefined) => {
|
|
133
|
+
if (!input) return;
|
|
134
|
+
input.value = '';
|
|
135
|
+
input.click();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const awaitUpload = () => uploadPromise ?? Promise.resolve();
|
|
139
|
+
|
|
140
|
+
onBeforeUnmount(reset);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
previewUrl,
|
|
144
|
+
file,
|
|
145
|
+
uploadedUrl,
|
|
146
|
+
isUploading,
|
|
147
|
+
onPaste,
|
|
148
|
+
onDrop,
|
|
149
|
+
onDragOver,
|
|
150
|
+
onFileChange,
|
|
151
|
+
pickFile,
|
|
152
|
+
handleImageFile,
|
|
153
|
+
reset,
|
|
154
|
+
setExternalUrl,
|
|
155
|
+
awaitUpload,
|
|
156
|
+
};
|
|
157
|
+
}
|