@studio-west/component-sw 0.11.9 → 0.11.11
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/README.md +5 -5
- package/dist/SwButton-yS_tKW9w.js +4 -0
- package/dist/{SwButton-jKDKwFV9.js → SwButton.vue_vue_type_script_setup_true_lang-aODPwFa6.js} +11 -11
- package/dist/{SwDatePicker-CJjKSM8k.js → SwDatePicker-CpmdOhuc.js} +1 -1
- package/dist/{SwDropdownItem-CtlMVgsX.js → SwDropdownItem-BE6ZRWT1.js} +1 -1
- package/dist/{SwGide-DbSSyZ-y.js → SwGide-_a5-3g_f.js} +2 -2
- package/dist/SwInput-CbNd7Vin.js +90 -0
- package/dist/{SwMessage-CovKkpf6.js → SwMessage-DdUbYQet.js} +6 -6
- package/dist/SwSection-CQe2kE0O.js +34 -0
- package/dist/SwSelect-BxbCfof-.js +1883 -0
- package/dist/{SwSlider-YncjYKPw.js → SwSlider-jWTzzPZg.js} +1 -1
- package/dist/SwSwitch-DeMdyD0-.js +47 -0
- package/dist/index-C3iiqwEz.js +188 -0
- package/dist/index.cjs +6 -1
- package/dist/index.js +1 -1
- package/package.json +6 -2
- package/src/Alert.ts +65 -0
- package/src/components/SwAlert.vue +70 -0
- package/src/components/SwButton.vue +50 -0
- package/src/components/SwButtonGroup.vue +67 -0
- package/src/components/SwCollapse.vue +36 -0
- package/src/components/SwDatePicker.vue +375 -0
- package/src/components/SwDropdown.vue +202 -0
- package/src/components/SwDropdownItem.vue +26 -0
- package/src/components/SwDropdownNew.vue +175 -0
- package/src/components/SwFormItem.vue +21 -0
- package/src/components/SwGide.vue +128 -0
- package/src/components/SwIcon.vue +16 -0
- package/src/components/SwInput.vue +100 -0
- package/src/components/SwMessage.vue +53 -0
- package/src/components/SwSection.vue +17 -0
- package/src/components/SwSelect.vue +151 -0
- package/src/components/SwSkeleton.vue +13 -0
- package/src/components/SwSkeletonItem.vue +27 -0
- package/src/components/SwSlider.vue +281 -0
- package/src/components/SwSwitch.vue +51 -0
- package/src/components/SwTable.vue +239 -0
- package/src/components/SwTableColumn.vue +25 -0
- package/src/components/SwTabs.vue +41 -0
- package/src/components/SwTabsPane.vue +44 -0
- package/src/index.ts +43 -0
- package/src/utils/index.ts +149 -0
- package/types/components.d.ts +72 -0
- package/types/index.d.ts +82 -70
- package/dist/SwInput-DCV1rrWa.js +0 -89
- package/dist/SwSection-D8ooQ21I.js +0 -37
- package/dist/SwSelect-C2RKinez.js +0 -72
- package/dist/SwSwitch-6rl1IT4p.js +0 -47
- package/dist/index-B5koqczP.js +0 -190
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<li :class="style">
|
|
3
|
+
<sw-icon :icon-class="iconBefore" v-if="iconBefore.length > 0"/>
|
|
4
|
+
<slot></slot>
|
|
5
|
+
</li>
|
|
6
|
+
</template>
|
|
7
|
+
|
|
8
|
+
<script setup>
|
|
9
|
+
import { computed } from "vue"
|
|
10
|
+
import SwIcon from "@/components/SwIcon.vue"
|
|
11
|
+
|
|
12
|
+
const props = defineProps({
|
|
13
|
+
class: {type: String, default: ''},
|
|
14
|
+
size: {type: String, default: ''},
|
|
15
|
+
type: {type: String, default: ''},
|
|
16
|
+
iconBefore: {type: String, default: ''},
|
|
17
|
+
})
|
|
18
|
+
const style = computed(() =>{
|
|
19
|
+
let s = ['sw-dropdown-item']
|
|
20
|
+
if(props.size.length > 0) s.push('sw-' + props.size)
|
|
21
|
+
if(props.type.length > 0) s.push('sw-' + props.type)
|
|
22
|
+
if(props.class.length > 0) s.push(props.class)
|
|
23
|
+
return s
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
</script>
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:class="style"
|
|
4
|
+
@click="toggleClick()"
|
|
5
|
+
@mouseover.stop="toggleHover()"
|
|
6
|
+
@mouseleave.stop="toggleHover()"
|
|
7
|
+
@contextmenu.stop="toggleContext()"
|
|
8
|
+
ref="dropdownRef"
|
|
9
|
+
>
|
|
10
|
+
<slot></slot>
|
|
11
|
+
<Teleport to="body">
|
|
12
|
+
<ul
|
|
13
|
+
popover
|
|
14
|
+
ref="popupRef"
|
|
15
|
+
:id ="id"
|
|
16
|
+
class="sw-dropdown-popup"
|
|
17
|
+
:class="props.class"
|
|
18
|
+
v-if="manual || false"
|
|
19
|
+
:style="popupStyle"
|
|
20
|
+
tabindex="-1"
|
|
21
|
+
>
|
|
22
|
+
<slot name="dropdown"></slot>
|
|
23
|
+
</ul>
|
|
24
|
+
</Teleport>
|
|
25
|
+
</div>
|
|
26
|
+
</template>
|
|
27
|
+
|
|
28
|
+
<script setup>
|
|
29
|
+
import { computed, ref, onMounted, onUnmounted, watchEffect } from "vue"
|
|
30
|
+
|
|
31
|
+
const props = defineProps({
|
|
32
|
+
class: {type: String, default: ''},
|
|
33
|
+
type: {type: String, default: ''},
|
|
34
|
+
trigger: {type: String, default: "click"},
|
|
35
|
+
placement: {type: String, default: 'bottom-left'},
|
|
36
|
+
maxWidth: {type: Number, default: 0}
|
|
37
|
+
})
|
|
38
|
+
const manual = defineModel()
|
|
39
|
+
|
|
40
|
+
const dropdownRef = ref(null)
|
|
41
|
+
const popupRef = ref(null)
|
|
42
|
+
const popupStyle = ref({})
|
|
43
|
+
const id = Math.ceil(Math.random() * 1000)
|
|
44
|
+
const repositionTrigger = ref(0)
|
|
45
|
+
const style = computed(() =>{
|
|
46
|
+
let s = ['sw-dropdown']
|
|
47
|
+
if(props.type.length > 0) s.push('sw-' + props.type)
|
|
48
|
+
return s
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
52
|
+
if (manual.value) repositionTrigger.value++
|
|
53
|
+
if(repositionTrigger.value > 99 ) repositionTrigger.value = 1
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
watchEffect( () => {
|
|
57
|
+
// console.log('watchEffect', repositionTrigger.value)
|
|
58
|
+
// Если popup не активен — ничего не делаем
|
|
59
|
+
if (!manual.value || !popupRef.value || !dropdownRef.value) {
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
if(repositionTrigger.value < 2) resizeObserver.observe(popupRef.value)
|
|
63
|
+
let scrollTop = window.pageYOffset || document.documentElement.scrollTop
|
|
64
|
+
let scrollLeft = window.pageXOffset || document.documentElement.scrollLeft
|
|
65
|
+
document.body.style.position = 'fixed'
|
|
66
|
+
document.body.style.top = `-${scrollTop}px`
|
|
67
|
+
document.body.style.left = `-${scrollLeft}px`
|
|
68
|
+
document.body.style.position = ''
|
|
69
|
+
document.body.style.top = ''
|
|
70
|
+
document.body.style.left = ''
|
|
71
|
+
document.documentElement.scrollTop = scrollTop
|
|
72
|
+
document.documentElement.scrollLeft = scrollLeft
|
|
73
|
+
const buttonRect = dropdownRef.value.getBoundingClientRect()
|
|
74
|
+
let viewportWidth = window.innerWidth
|
|
75
|
+
let viewportHeight = window.innerHeight
|
|
76
|
+
// Получаем высоту и ширину popup
|
|
77
|
+
let popupHeight = popupRef.value.offsetHeight;
|
|
78
|
+
let popupWidth = popupRef.value.offsetWidth;
|
|
79
|
+
// Расчет позиции popup (например, снизу кнопки)
|
|
80
|
+
// Центр по горизонтали
|
|
81
|
+
let left = buttonRect.left + (buttonRect.width / 2) + scrollLeft
|
|
82
|
+
// Центр по вертикали
|
|
83
|
+
let top = buttonRect.top + (buttonRect.height / 2) + scrollTop
|
|
84
|
+
let deltaTop = (buttonRect.height / 2) + 8
|
|
85
|
+
const [mainPlacement, subPlacement] = props.placement.split('-', 2)
|
|
86
|
+
|
|
87
|
+
// Основное позиционирование
|
|
88
|
+
switch (mainPlacement) {
|
|
89
|
+
case "bottom":
|
|
90
|
+
top = (scrollTop + viewportHeight - popupHeight - deltaTop - 8 < top)? top - deltaTop - popupHeight : top + deltaTop
|
|
91
|
+
if(subPlacement === undefined ) left -= popupWidth / 2
|
|
92
|
+
break
|
|
93
|
+
case "top":
|
|
94
|
+
top = (scrollTop + popupHeight + deltaTop + 8 > top)? top + deltaTop : top - deltaTop - popupHeight
|
|
95
|
+
if(subPlacement === undefined ) left -= popupWidth / 2
|
|
96
|
+
break
|
|
97
|
+
case "left":
|
|
98
|
+
if(left - popupWidth - (buttonRect.width / 2) - 8 < 0) {
|
|
99
|
+
if(left + (buttonRect.width / 2) + 8 + popupWidth < viewportWidth) { // справа
|
|
100
|
+
left = left + (buttonRect.width / 2) + 8
|
|
101
|
+
top = top - (popupHeight / 2)
|
|
102
|
+
} else {
|
|
103
|
+
top = (scrollTop + popupHeight + deltaTop + 8 > top)? top + deltaTop : top - deltaTop - popupHeight
|
|
104
|
+
left = (left + (buttonRect.width / 2) < viewportWidth )? left + (buttonRect.width / 2) - popupWidth: left - (popupWidth / 2)
|
|
105
|
+
}
|
|
106
|
+
} else { // слева
|
|
107
|
+
left = left - popupWidth - (buttonRect.width / 2) - 8
|
|
108
|
+
top = top - (popupHeight / 2)
|
|
109
|
+
}
|
|
110
|
+
break
|
|
111
|
+
case "right":
|
|
112
|
+
if(left + (buttonRect.width / 2) + 8 + popupWidth > viewportWidth ) {
|
|
113
|
+
if(left - popupWidth - (buttonRect.width / 2) - 8 > 0){
|
|
114
|
+
left = left - popupWidth - (buttonRect.width / 2) - 8 // слева
|
|
115
|
+
top = top - (popupHeight / 2)
|
|
116
|
+
} else {
|
|
117
|
+
top = (scrollTop + popupHeight + deltaTop + 8 > top)? top + deltaTop : top - deltaTop - popupHeight
|
|
118
|
+
left = left - (buttonRect.width / 2) // влево
|
|
119
|
+
}
|
|
120
|
+
} else { // справа
|
|
121
|
+
left = left + (buttonRect.width / 2) + 8
|
|
122
|
+
top = top - (popupHeight / 2)
|
|
123
|
+
}
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
switch (subPlacement) {
|
|
127
|
+
case 'left':
|
|
128
|
+
left = (scrollLeft + viewportWidth - 8 > left - (buttonRect.width / 2) + popupWidth) ? left - (buttonRect.width / 2) : left - popupWidth + (buttonRect.width / 2)
|
|
129
|
+
break
|
|
130
|
+
case 'right':
|
|
131
|
+
left = (8< left + (buttonRect.width / 2) - popupWidth) ? left - popupWidth + (buttonRect.width / 2) : left - (buttonRect.width / 2)
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
// Убедимся, что попап не выходит за экран
|
|
135
|
+
if (left < 0) left = 0
|
|
136
|
+
if (top < 0) top = 0
|
|
137
|
+
|
|
138
|
+
popupStyle.value = {
|
|
139
|
+
position: 'absolute',
|
|
140
|
+
top: `${top}px`,
|
|
141
|
+
left: `${left}px`,
|
|
142
|
+
maxWidth: props.maxWidth > 0 ? `${props.maxWidth}px` : ''
|
|
143
|
+
}
|
|
144
|
+
}, {flush: 'post'}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
const toggleClick = () => {
|
|
148
|
+
if(props.trigger === 'click') manual.value = !manual.value
|
|
149
|
+
}
|
|
150
|
+
const toggleHover = () => {
|
|
151
|
+
if(props.trigger === 'hover') manual.value = !manual.value
|
|
152
|
+
}
|
|
153
|
+
const toggleContext = () => {
|
|
154
|
+
if (props.trigger === 'context') manual.value = !manual.value
|
|
155
|
+
}
|
|
156
|
+
const handleResize = () => {
|
|
157
|
+
if (manual.value) repositionTrigger.value++
|
|
158
|
+
if(repositionTrigger.value > 99 ) repositionTrigger.value = 1
|
|
159
|
+
}
|
|
160
|
+
defineExpose({ handleResize })
|
|
161
|
+
|
|
162
|
+
onMounted(() => {
|
|
163
|
+
window.addEventListener("resize", handleResize)
|
|
164
|
+
if (window.onscrollend !== undefined) window.addEventListener("scrollend", handleResize)
|
|
165
|
+
else window.addEventListener("scroll", handleResize)
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
onUnmounted(() => {
|
|
169
|
+
resizeObserver.disconnect()
|
|
170
|
+
window.removeEventListener("resize", handleResize)
|
|
171
|
+
if (window.onscrollend !== undefined) window.removeEventListener("scrollend", handleResize)
|
|
172
|
+
else window.removeEventListener("scroll", handleResize)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
</script>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="style">
|
|
3
|
+
<slot></slot>
|
|
4
|
+
</div>
|
|
5
|
+
</template>
|
|
6
|
+
|
|
7
|
+
<script setup>
|
|
8
|
+
import {computed} from "vue"
|
|
9
|
+
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
class: {type:String, default: ''},
|
|
12
|
+
type: {type:String, default: ''},
|
|
13
|
+
})
|
|
14
|
+
const style = computed(() =>{
|
|
15
|
+
let s = ['sw-form-item']
|
|
16
|
+
if(props.type.length > 0) s.push('sw-' + props.type)
|
|
17
|
+
if(props.class.length > 0) s.push(props.class)
|
|
18
|
+
return s
|
|
19
|
+
}
|
|
20
|
+
)
|
|
21
|
+
</script>
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="sw-gide" ref="swGide">
|
|
3
|
+
<div class="overlay0"/>
|
|
4
|
+
<div class="overlay1"/>
|
|
5
|
+
<sw-dropdown
|
|
6
|
+
class="gide"
|
|
7
|
+
trigger="none"
|
|
8
|
+
:placement="props.steps[step].placement"
|
|
9
|
+
v-model="visual"
|
|
10
|
+
:maxWidth='props.maxWidth'
|
|
11
|
+
>
|
|
12
|
+
<div class="hole"/>
|
|
13
|
+
<template #dropdown>
|
|
14
|
+
<header><slot name="header">{{ props.steps[step].header }} <sw-button type="primary" link @click="exitModule"><sw-icon :icon-class="props.iconClose" /></sw-button></slot></header>
|
|
15
|
+
<slot>
|
|
16
|
+
<div>{{ props.steps[step].text }}</div>
|
|
17
|
+
<img :class="(/left/.test(props.steps[step]?.placement))? 'reflect': null" :src="props.steps[step].img" />
|
|
18
|
+
</slot>
|
|
19
|
+
<footer>
|
|
20
|
+
<slot name="footer">
|
|
21
|
+
<sw-button :type="(step === 0)? 'info': 'warning'" text link @click="walk(-1)"><slot name="arrow">‹</slot></sw-button>
|
|
22
|
+
<sw-button type="success" @click="walk(1)" text link class="sw-revers"><slot name="arrow">‹</slot></sw-button>
|
|
23
|
+
</slot>
|
|
24
|
+
</footer>
|
|
25
|
+
</template>
|
|
26
|
+
</sw-dropdown>
|
|
27
|
+
<div class="overlay2"/>
|
|
28
|
+
<div class="overlay3"/>
|
|
29
|
+
</div>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script setup>
|
|
33
|
+
import SwDropdown from "@/components/SwDropdown.vue"
|
|
34
|
+
import SwButton from "@/components/SwButton.vue"
|
|
35
|
+
import {watchEffect, ref, onMounted} from "vue"
|
|
36
|
+
import SwIcon from "@/components/SwIcon.vue"
|
|
37
|
+
const swGide = ref(null)
|
|
38
|
+
const visual = ref(true)
|
|
39
|
+
const block = ref(null)
|
|
40
|
+
const step = defineModel({ type: Number, default: 0 })
|
|
41
|
+
const emit = defineEmits(['close'])
|
|
42
|
+
const props = defineProps({
|
|
43
|
+
steps: { type: Array, default: () => [] },
|
|
44
|
+
iconClose: { type: String, default: '' },
|
|
45
|
+
maxWidth: { type: Number, default: 250 }
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const exitModule = () => emit('close')
|
|
49
|
+
|
|
50
|
+
const walk = (e) => {
|
|
51
|
+
const nextStep = step.value + e
|
|
52
|
+
if (nextStep < 0) step.value = 0
|
|
53
|
+
else if (nextStep >= props.steps.length) exitModule()
|
|
54
|
+
else step.value = nextStep
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Флаг: игнорировать следующее событие скролла (после автопрокрутки)
|
|
58
|
+
let ignoreNextScroll = false
|
|
59
|
+
|
|
60
|
+
// Функция прокрутки к элементу
|
|
61
|
+
const scrollToElement = (el) => {
|
|
62
|
+
const rect = el.getBoundingClientRect()
|
|
63
|
+
const viewportHeight = window.innerHeight
|
|
64
|
+
|
|
65
|
+
if (rect.top < 5) {
|
|
66
|
+
ignoreNextScroll = true
|
|
67
|
+
document.documentElement.scrollTop += rect.top - 5
|
|
68
|
+
} else if (rect.bottom > viewportHeight - 5) {
|
|
69
|
+
ignoreNextScroll = true
|
|
70
|
+
document.documentElement.scrollTop += rect.bottom - viewportHeight + 5
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Обновление позиции оверлея (без прокрутки!)
|
|
75
|
+
const update = () => {
|
|
76
|
+
if (!block.value || !swGide.value) return
|
|
77
|
+
const r = block.value.getBoundingClientRect()
|
|
78
|
+
const sT = window.pageYOffset || document.documentElement.scrollTop
|
|
79
|
+
const sL = window.pageXOffset || document.documentElement.scrollLeft
|
|
80
|
+
|
|
81
|
+
swGide.value.style.gridTemplateRows = `${r.top + sT - 5}px ${r.height + 10}px 1fr`
|
|
82
|
+
swGide.value.style.gridTemplateColumns = `${r.left + sL - 5}px ${r.width + 10}px 1fr`
|
|
83
|
+
swGide.value.style.height = document.documentElement.scrollHeight + 'px'
|
|
84
|
+
}
|
|
85
|
+
watchEffect((onInvalidate) => {
|
|
86
|
+
if (!swGide.value) return
|
|
87
|
+
const selector = props.steps[step.value]?.tag
|
|
88
|
+
if (!selector || !selector.trim()) {
|
|
89
|
+
// Сброс стилей, если нет селектора
|
|
90
|
+
swGide.value.style.gridTemplateRows = `40% 0 1fr`
|
|
91
|
+
swGide.value.style.gridTemplateColumns = `1fr 0 1fr`
|
|
92
|
+
swGide.value.style.height = document.documentElement.scrollHeight + 'px'
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
block.value = document.querySelector(selector)
|
|
96
|
+
if (!block.value) {
|
|
97
|
+
console.warn(`[SwGuide] Элемент не найден: ${selector}`)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
// === Автопрокрутка при смене шага ===
|
|
101
|
+
// Выполняем один раз после установки нового блока
|
|
102
|
+
scrollToElement(block.value)
|
|
103
|
+
// === Обновление оверлея при ресайзе/скролле ===
|
|
104
|
+
const onResizeOrScroll = () => {
|
|
105
|
+
if (ignoreNextScroll) {
|
|
106
|
+
ignoreNextScroll = false // сброс флага
|
|
107
|
+
return // игнорируем "нашу" прокрутку
|
|
108
|
+
}
|
|
109
|
+
update()
|
|
110
|
+
}
|
|
111
|
+
// Инициализация
|
|
112
|
+
update()
|
|
113
|
+
// Наблюдатели
|
|
114
|
+
const observer = new ResizeObserver(update)
|
|
115
|
+
observer.observe(block.value)
|
|
116
|
+
|
|
117
|
+
window.addEventListener('resize', onResizeOrScroll)
|
|
118
|
+
window.addEventListener('scroll', onResizeOrScroll, { passive: true })
|
|
119
|
+
|
|
120
|
+
// Очистка
|
|
121
|
+
onInvalidate(() => {
|
|
122
|
+
observer.disconnect()
|
|
123
|
+
window.removeEventListener('resize', onResizeOrScroll)
|
|
124
|
+
window.removeEventListener('scroll', onResizeOrScroll)
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
onMounted(()=> window.onload = () => block.value = document.querySelector(props.steps[step.value]?.tag))
|
|
128
|
+
</script>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="isExt" :style="'mask: url(' + props.iconClass + ') no-repeat 50% 50%;'" class="sw-external-icon svg-icon" v-bind="$attrs" />
|
|
3
|
+
<svg v-else :class="'sw-icon icon-' + (props.className || props.iconClass)" aria-hidden="true" v-bind="$attrs">
|
|
4
|
+
<use :href="'#' + props.prefix +'-' + props.iconClass" />
|
|
5
|
+
</svg>
|
|
6
|
+
</template>
|
|
7
|
+
<script setup>
|
|
8
|
+
import { isExternal } from '@/utils/index.js'
|
|
9
|
+
|
|
10
|
+
const props = defineProps({
|
|
11
|
+
prefix: {type: String, default: 'icon'},
|
|
12
|
+
iconClass: {type: String, required: true},
|
|
13
|
+
className: {type: String, default: ''}
|
|
14
|
+
})
|
|
15
|
+
const isExt = isExternal(props.iconClass)
|
|
16
|
+
</script>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, ref, watch } from "vue"
|
|
3
|
+
import SwIcon from "@/components/SwIcon.vue"
|
|
4
|
+
import { formatPhoneNumber } from "@/utils/index"
|
|
5
|
+
|
|
6
|
+
// Define prop types - separate model from props
|
|
7
|
+
interface InputProps {
|
|
8
|
+
before?: string
|
|
9
|
+
after?: string
|
|
10
|
+
placeholder?: string
|
|
11
|
+
label?: string
|
|
12
|
+
name: string
|
|
13
|
+
class?: string
|
|
14
|
+
size?: string
|
|
15
|
+
type?: string
|
|
16
|
+
inputMode?: string
|
|
17
|
+
required?: boolean
|
|
18
|
+
maxlength?: number
|
|
19
|
+
minlength?: number
|
|
20
|
+
inputSize?: number
|
|
21
|
+
autofocus?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Use defineModel for the input value
|
|
25
|
+
const modelValue = defineModel<string>()
|
|
26
|
+
|
|
27
|
+
// Define props separately
|
|
28
|
+
const props = withDefaults(defineProps<InputProps>(), {
|
|
29
|
+
before: '',
|
|
30
|
+
after: '',
|
|
31
|
+
placeholder: '',
|
|
32
|
+
label: '',
|
|
33
|
+
class: '',
|
|
34
|
+
size: '',
|
|
35
|
+
type: 'text',
|
|
36
|
+
inputMode: 'text',
|
|
37
|
+
required: false,
|
|
38
|
+
maxlength: 128,
|
|
39
|
+
minlength: 1,
|
|
40
|
+
inputSize: 60,
|
|
41
|
+
autofocus: false
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const lab = ref<HTMLLabelElement | null>(null)
|
|
45
|
+
const emit = defineEmits(['suffix','prefix','focusInput'])
|
|
46
|
+
const onfocus = (e: FocusEvent) => {
|
|
47
|
+
// console.log(e)
|
|
48
|
+
emit('focusInput')
|
|
49
|
+
const target = e.target as HTMLInputElement
|
|
50
|
+
target.setSelectionRange(modelValue.value?.length || 0, modelValue.value?.length || 0)
|
|
51
|
+
if(lab.value !== null) lab.value.style.top = '-.6em'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const onblur = (e: FocusEvent) => {
|
|
55
|
+
if((modelValue.value?.length || 0) === 0 && lab.value !== null) lab.value.removeAttribute("style")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Set initial label position if there's a value
|
|
59
|
+
if((modelValue.value?.length || 0) > 0 && lab.value !== null) lab.value.style.top = '-.6em'
|
|
60
|
+
|
|
61
|
+
const suffix = ()=>{ emit('suffix') }
|
|
62
|
+
const prefix = ()=>{ emit('prefix') }
|
|
63
|
+
const style = computed(() =>{
|
|
64
|
+
let s = ['sw-input']
|
|
65
|
+
if(props.size.length > 0) s.push('sw-' + props.size)
|
|
66
|
+
if(props.class.length > 0) s.push(props.class)
|
|
67
|
+
return s
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
watch(modelValue, (newValue, oldValue) => {
|
|
71
|
+
if(props.type === 'phone' && newValue) {
|
|
72
|
+
modelValue.value = formatPhoneNumber(newValue, oldValue)
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<template>
|
|
78
|
+
<div :class="style">
|
|
79
|
+
<sw-icon v-if="props.before.length > 0" :icon-class="props.before" @click="prefix"/>
|
|
80
|
+
<slot v-else name="prefix"></slot>
|
|
81
|
+
<label v-if="(props.label.length > 0)" :for="props.name" ref="lab">{{ props.label }}<span title="Это поле обязательно для заполнения." v-if="props.required">*</span></label>
|
|
82
|
+
<input
|
|
83
|
+
v-model="model"
|
|
84
|
+
:required="props.required"
|
|
85
|
+
:placeholder="(props.required && props.label.length === 0)? props.placeholder + `*`: props.placeholder"
|
|
86
|
+
:type="props.type"
|
|
87
|
+
:id="props.name"
|
|
88
|
+
:name="props.name"
|
|
89
|
+
:size="props.inputSize"
|
|
90
|
+
@focus="onfocus"
|
|
91
|
+
@blur="onblur"
|
|
92
|
+
:maxlength="props.maxlength"
|
|
93
|
+
:minlength="props.minlength"
|
|
94
|
+
:autofocus="props.autofocus"
|
|
95
|
+
v-bind="$attrs"
|
|
96
|
+
>
|
|
97
|
+
<sw-icon v-if="props.after.length > 0" :icon-class="props.after" @click="suffix"/>
|
|
98
|
+
<slot v-else name="suffix"></slot>
|
|
99
|
+
</div>
|
|
100
|
+
</template>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<dialog :class="'sw-message ' + props.class" ref="messageRef" >
|
|
3
|
+
<header v-if="props.name.length > 0 || !!slots?.header" ><slot name="header">{{ props.name }} <sw-button link type="primary" @click="model = false"><sw-icon :icon-class="props.iconAfter" /></sw-button></slot></header>
|
|
4
|
+
<slot></slot>
|
|
5
|
+
<footer><slot name="footer"></slot></footer>
|
|
6
|
+
</dialog>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup>
|
|
10
|
+
import {nextTick, onUnmounted, ref, watch, useSlots} from "vue"
|
|
11
|
+
import SwButton from "@/components/SwButton.vue"
|
|
12
|
+
import SwIcon from "@/components/SwIcon.vue"
|
|
13
|
+
|
|
14
|
+
const model = defineModel()
|
|
15
|
+
const props = defineProps({
|
|
16
|
+
name: {type: String, default: ''},
|
|
17
|
+
class: {type:String, default: ''},
|
|
18
|
+
iconAfter: {type:String, default: ''}
|
|
19
|
+
})
|
|
20
|
+
const messageRef = ref(null)
|
|
21
|
+
const slots = useSlots();
|
|
22
|
+
const handleClickOutside = (e) => {
|
|
23
|
+
if (messageRef.value && model.value) {
|
|
24
|
+
const dialogRect = messageRef.value.getBoundingClientRect()
|
|
25
|
+
const isClickInsideDialog = (
|
|
26
|
+
e.clientX >= dialogRect.left &&
|
|
27
|
+
e.clientX <= dialogRect.right &&
|
|
28
|
+
e.clientY >= dialogRect.top &&
|
|
29
|
+
e.clientY <= dialogRect.bottom
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
// Если клик вне диалога, закрываем его
|
|
33
|
+
if (!isClickInsideDialog) {
|
|
34
|
+
model.value = false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Когда модель становится true — добавляем обработчик
|
|
39
|
+
watch(() => model.value, (newVal) => {
|
|
40
|
+
if (newVal) {
|
|
41
|
+
messageRef.value.showModal()
|
|
42
|
+
|
|
43
|
+
nextTick() // ждём, пока DOM обновится
|
|
44
|
+
window.addEventListener('click', handleClickOutside, true)
|
|
45
|
+
} else {
|
|
46
|
+
messageRef.value.close()
|
|
47
|
+
window.removeEventListener('click', handleClickOutside)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
onUnmounted(() => {
|
|
51
|
+
window.removeEventListener('click', handleClickOutside)
|
|
52
|
+
})
|
|
53
|
+
</script>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
const props = defineProps({
|
|
3
|
+
name: {type: String, default: ''},
|
|
4
|
+
class: {type:String, default: ''},
|
|
5
|
+
iconAfter: {type:String, default: ''}
|
|
6
|
+
})
|
|
7
|
+
const emit = defineEmits(['header'])
|
|
8
|
+
const header = () => {emit('header')}
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<template>
|
|
12
|
+
<section :class="'sw-section ' + props.class">
|
|
13
|
+
<header @click="header"><slot name="header">{{ props.name }} <svg-icon :icon-class="props.iconAfter" /></slot></header>
|
|
14
|
+
<slot></slot>
|
|
15
|
+
<footer><slot name="footer"></slot></footer>
|
|
16
|
+
</section>
|
|
17
|
+
</template>
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch } from "vue"
|
|
3
|
+
import axios from 'axios'
|
|
4
|
+
|
|
5
|
+
// Define types
|
|
6
|
+
interface AddressData {
|
|
7
|
+
fullText: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Suggestion {
|
|
11
|
+
value: string
|
|
12
|
+
data: {
|
|
13
|
+
geo_lat: string
|
|
14
|
+
geo_lon: string
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface GeoData {
|
|
19
|
+
text: string
|
|
20
|
+
coords: [number, number]
|
|
21
|
+
suggestion: any
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const address = ref<AddressData>({ fullText: '' })
|
|
25
|
+
const visible = ref<boolean>(false)
|
|
26
|
+
const options = ref<Suggestion[]>([])
|
|
27
|
+
|
|
28
|
+
const model = defineModel<GeoData>()
|
|
29
|
+
|
|
30
|
+
interface Props {
|
|
31
|
+
class?: string
|
|
32
|
+
size?: 'large' | 'small'
|
|
33
|
+
placeholder?: string
|
|
34
|
+
token?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
38
|
+
class: '',
|
|
39
|
+
size: 'large',
|
|
40
|
+
placeholder: '',
|
|
41
|
+
token: ''
|
|
42
|
+
})
|
|
43
|
+
watch(() => model.value?.text, (newValue) => {
|
|
44
|
+
if (newValue && newValue.length > 3) address.value.fullText = newValue
|
|
45
|
+
})
|
|
46
|
+
watch(address.value.fullText,(newValue, oldValue) => {
|
|
47
|
+
visible.value = (newValue.length > 3 && newValue.length > oldValue.length)
|
|
48
|
+
if (newValue.length >3) getDadata()
|
|
49
|
+
})
|
|
50
|
+
const getDadata = () => {
|
|
51
|
+
let url='https://suggestions.dadata.ru/suggestions/api/4_1/rs/suggest/address'
|
|
52
|
+
console.log(address.value.fullText)
|
|
53
|
+
axios.post(url, {
|
|
54
|
+
query: address.value.fullText },
|
|
55
|
+
{
|
|
56
|
+
headers: {
|
|
57
|
+
"Content-Type": "application/json",
|
|
58
|
+
"Accept": "application/json",
|
|
59
|
+
"Authorization": 'Token ' + props.token
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
.then(response => {
|
|
63
|
+
console.log(response.data.suggestions)
|
|
64
|
+
options.value = response.data.suggestions
|
|
65
|
+
// this.visible = (this.options.length > 1)
|
|
66
|
+
if (options.value.length === 1) clickLi(options.value[0])
|
|
67
|
+
// this.geo.coords = [Number(response.data.result[0]), Number(response.data.result[1])]
|
|
68
|
+
})
|
|
69
|
+
.catch(error => {
|
|
70
|
+
// handle error
|
|
71
|
+
console.log(error);
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
const clickLi = (item) => {
|
|
75
|
+
address.value.fullText = item.value
|
|
76
|
+
visible.value = false
|
|
77
|
+
const geo = {}
|
|
78
|
+
geo.text = item.value
|
|
79
|
+
geo.coords = [Number(item.data.geo_lat), Number(item.data.geo_lon)]
|
|
80
|
+
geo.suggestion = item.data
|
|
81
|
+
model.value = geo
|
|
82
|
+
}
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<template>
|
|
86
|
+
<div class="popup">
|
|
87
|
+
<sw-input
|
|
88
|
+
:size="props.size"
|
|
89
|
+
v-model="address.fullText"
|
|
90
|
+
:placeholder="props.placeholder"
|
|
91
|
+
/>
|
|
92
|
+
<div class="wrapper" v-show="visible">
|
|
93
|
+
<ul class="scrollbar">
|
|
94
|
+
<li tabindex="1" v-for="(item, key) in options" :key="key" @click="clickLi(item)">{{ item.value }}</li>
|
|
95
|
+
</ul>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</template>
|
|
99
|
+
|
|
100
|
+
<style lang="scss">
|
|
101
|
+
.popup{
|
|
102
|
+
position: relative;
|
|
103
|
+
.wrapper{
|
|
104
|
+
position: absolute;
|
|
105
|
+
z-index: 1;
|
|
106
|
+
list-style: none;
|
|
107
|
+
font-size: .8rem;
|
|
108
|
+
line-height: 1.2;
|
|
109
|
+
word-break: break-word;
|
|
110
|
+
padding: 0;
|
|
111
|
+
margin: 10px 0 0;
|
|
112
|
+
:before{
|
|
113
|
+
content: '';
|
|
114
|
+
border-bottom-color: transparent !important;
|
|
115
|
+
border-left-color: transparent !important;
|
|
116
|
+
border: 1px solid var(--el-border-color-light);
|
|
117
|
+
background: var(--el-bg-color-overlay);
|
|
118
|
+
right: 50%;
|
|
119
|
+
top: -6px;
|
|
120
|
+
position: absolute;
|
|
121
|
+
width: 10px;
|
|
122
|
+
height: 10px;
|
|
123
|
+
z-index: 1;
|
|
124
|
+
transform: rotate(-45deg);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
.scrollbar {
|
|
128
|
+
box-shadow: 0 0 0 1px var(--el-border-color);
|
|
129
|
+
border-radius: var(--el-border-radius-base);
|
|
130
|
+
background: #ffffff;
|
|
131
|
+
padding: .5rem 0;
|
|
132
|
+
margin: 0;
|
|
133
|
+
li{display: flex;
|
|
134
|
+
align-items: start;
|
|
135
|
+
white-space: nowrap;
|
|
136
|
+
list-style: none;
|
|
137
|
+
line-height: 22px;
|
|
138
|
+
padding: 5px 16px;
|
|
139
|
+
margin:0;
|
|
140
|
+
font-size: var(--sw-font-size);
|
|
141
|
+
color: var(--sw-text-color-secondary);
|
|
142
|
+
cursor:pointer;
|
|
143
|
+
outline: none;
|
|
144
|
+
&:not(.is-disabled):focus{
|
|
145
|
+
background: var(--el-dropdown-menuItem-hover-fill);
|
|
146
|
+
color:var(--el-dropdown-menuItem-hover-color);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
</style>
|