@yxhl/specter-pui-vtk 1.0.0
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 +122 -0
- package/dist/specter-pui-vtk.css +1 -0
- package/dist/specter-pui.es.js +3289 -0
- package/dist/specter-pui.es.js.map +1 -0
- package/dist/specter-pui.umd.js +2 -0
- package/dist/specter-pui.umd.js.map +1 -0
- package/package.json +65 -0
- package/src/assets/css/globals.scss +250 -0
- package/src/assets/css/index.scss +271 -0
- package/src/assets/css/settings.scss +10 -0
- package/src/assets/css/variables.scss +0 -0
- package/src/assets/icon/logo.svg +9 -0
- package/src/assets/img/background.png +0 -0
- package/src/assets/img/dtx.png +0 -0
- package/src/commons/filters/dictionary.js +75 -0
- package/src/commons/filters/format.js +112 -0
- package/src/commons/filters/mask.js +25 -0
- package/src/commons/index.js +17 -0
- package/src/commons/request.js +89 -0
- package/src/commons/storage.js +41 -0
- package/src/commons/themes.js +153 -0
- package/src/commons/validation.js +72 -0
- package/src/components/README.md +35 -0
- package/src/components/assembly/VtkArea.vue +259 -0
- package/src/components/assembly/VtkCheckbox.vue +168 -0
- package/src/components/assembly/VtkCount.vue +403 -0
- package/src/components/assembly/VtkDatePicker.vue +326 -0
- package/src/components/assembly/VtkEmpty.vue +107 -0
- package/src/components/assembly/VtkFab.vue +78 -0
- package/src/components/assembly/VtkFormItem.vue +166 -0
- package/src/components/assembly/VtkImg.vue +372 -0
- package/src/components/assembly/VtkPage.vue +156 -0
- package/src/components/assembly/VtkPdf.vue +424 -0
- package/src/components/assembly/VtkProj.vue +539 -0
- package/src/components/assembly/VtkRadio.vue +82 -0
- package/src/components/assembly/VtkSearch.vue +145 -0
- package/src/components/assembly/VtkSelect.vue +104 -0
- package/src/components/assembly/VtkStepper.vue +160 -0
- package/src/components/message/alert.vue +31 -0
- package/src/components/message/confirm.vue +44 -0
- package/src/components/message/index.js +55 -0
- package/src/components/message/loading.vue +33 -0
- package/src/components/message/prompt.vue +57 -0
- package/src/components/message/toast.vue +45 -0
- package/src/components/message/vtkMessage.vue +27 -0
- package/src/composables/useMixins.js +2 -0
- package/src/composables/usePage.js +311 -0
- package/src/index.js +109 -0
- package/src/stores/message.js +79 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<!-- 空页面 -->
|
|
2
|
+
<template>
|
|
3
|
+
<v-empty-state
|
|
4
|
+
:headline="headline"
|
|
5
|
+
:title="title"
|
|
6
|
+
:text="text"
|
|
7
|
+
:image="image"
|
|
8
|
+
:icon="icon"
|
|
9
|
+
:rounded="rounded"
|
|
10
|
+
:elevation="elevation"
|
|
11
|
+
>
|
|
12
|
+
<!-- 图像插槽 -->
|
|
13
|
+
<template v-if="$slots.media" #media>
|
|
14
|
+
<slot name="media"></slot>
|
|
15
|
+
</template>
|
|
16
|
+
|
|
17
|
+
<!-- 标题插槽 -->
|
|
18
|
+
<template v-if="$slots.title" #title>
|
|
19
|
+
<slot name="title"></slot>
|
|
20
|
+
</template>
|
|
21
|
+
|
|
22
|
+
<!-- 文本插槽 -->
|
|
23
|
+
<template v-if="$slots.text" #text>
|
|
24
|
+
<slot name="text"></slot>
|
|
25
|
+
</template>
|
|
26
|
+
|
|
27
|
+
<!-- 操作按钮插槽 -->
|
|
28
|
+
<template v-if="$slots.actions" #actions>
|
|
29
|
+
<slot name="actions"></slot>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<!-- 底部插槽 -->
|
|
33
|
+
<template v-if="$slots.bottom" #bottom>
|
|
34
|
+
<slot name="bottom"></slot>
|
|
35
|
+
</template>
|
|
36
|
+
</v-empty-state>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<script setup>
|
|
40
|
+
// 定义组件名称
|
|
41
|
+
defineOptions({
|
|
42
|
+
name: "VtkEmpty",
|
|
43
|
+
inheritAttrs: false,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// 定义 props
|
|
47
|
+
const props = defineProps({
|
|
48
|
+
// 标题
|
|
49
|
+
title: {
|
|
50
|
+
type: String,
|
|
51
|
+
default: ''
|
|
52
|
+
},
|
|
53
|
+
// 副标题/文本
|
|
54
|
+
text: {
|
|
55
|
+
type: String,
|
|
56
|
+
default: ''
|
|
57
|
+
},
|
|
58
|
+
// 头部文本(更大字体)
|
|
59
|
+
headline: {
|
|
60
|
+
type: String,
|
|
61
|
+
default: ''
|
|
62
|
+
},
|
|
63
|
+
// 图标
|
|
64
|
+
icon: {
|
|
65
|
+
type: String,
|
|
66
|
+
default: ''
|
|
67
|
+
},
|
|
68
|
+
// 图像
|
|
69
|
+
image: {
|
|
70
|
+
type: String,
|
|
71
|
+
default: ''
|
|
72
|
+
},
|
|
73
|
+
// 圆角
|
|
74
|
+
rounded: {
|
|
75
|
+
type: [Boolean, String, Number],
|
|
76
|
+
default: 'circle'
|
|
77
|
+
},
|
|
78
|
+
// 阴影
|
|
79
|
+
elevation: {
|
|
80
|
+
type: [Number, String],
|
|
81
|
+
default: 0
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<style scoped>
|
|
87
|
+
/* 可以添加自定义样式 */
|
|
88
|
+
:deep(.v-empty-state) {
|
|
89
|
+
padding: 24px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
:deep(.v-empty-state__media) {
|
|
93
|
+
margin-bottom: 16px;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
:deep(.v-empty-state__headline) {
|
|
97
|
+
margin-bottom: 8px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
:deep(.v-empty-state__title) {
|
|
101
|
+
margin-bottom: 8px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
:deep(.v-empty-state__actions) {
|
|
105
|
+
margin-top: 16px;
|
|
106
|
+
}
|
|
107
|
+
</style>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
<!-- 悬浮按钮 -->
|
|
2
|
+
<template>
|
|
3
|
+
<transition name="slide-up">
|
|
4
|
+
<v-fab
|
|
5
|
+
v-if="showFab"
|
|
6
|
+
:class="fabClass"
|
|
7
|
+
color="primary"
|
|
8
|
+
icon="mdi-arrow-up"
|
|
9
|
+
@click="scrollToTop"
|
|
10
|
+
></v-fab>
|
|
11
|
+
</transition>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup>
|
|
15
|
+
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
|
16
|
+
|
|
17
|
+
// 定义组件名称
|
|
18
|
+
defineOptions({
|
|
19
|
+
name: "VtkFab",
|
|
20
|
+
inheritAttrs: false,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// 控制悬浮按钮显示状态
|
|
24
|
+
const showFab = ref(false)
|
|
25
|
+
// 自定义类名用于样式调整
|
|
26
|
+
const fabClass = ref('scroll-to-top-fab')
|
|
27
|
+
|
|
28
|
+
// 监听滚动事件
|
|
29
|
+
const handleScroll = () => {
|
|
30
|
+
showFab.value = window.scrollY > 100
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 滚动到顶部
|
|
34
|
+
const scrollToTop = () => {
|
|
35
|
+
window.scrollTo({
|
|
36
|
+
top: 0,
|
|
37
|
+
behavior: 'smooth'
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 组件挂载时添加滚动监听
|
|
42
|
+
onMounted(() => {
|
|
43
|
+
window.addEventListener('scroll', handleScroll)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// 组件卸载前移除监听
|
|
47
|
+
onBeforeUnmount(() => {
|
|
48
|
+
window.removeEventListener('scroll', handleScroll)
|
|
49
|
+
})
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<style scoped>
|
|
53
|
+
.scroll-to-top-fab {
|
|
54
|
+
position: fixed;
|
|
55
|
+
bottom: 24px;
|
|
56
|
+
right: 24px;
|
|
57
|
+
z-index: 1004;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* 滑动动画 */
|
|
61
|
+
.slide-up-enter-active {
|
|
62
|
+
transition: all 0.2s ease-out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.slide-up-leave-active {
|
|
66
|
+
transition: all 0.2s ease-in;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.slide-up-enter-from {
|
|
70
|
+
opacity: 0;
|
|
71
|
+
transform: translateY(30px);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.slide-up-leave-to {
|
|
75
|
+
opacity: 0;
|
|
76
|
+
transform: translateY(30px);
|
|
77
|
+
}
|
|
78
|
+
</style>
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
class="mb-2"
|
|
4
|
+
:class="{
|
|
5
|
+
'form-item--vertical': top,
|
|
6
|
+
'form-item--horizontal': !top,
|
|
7
|
+
...rootClasses
|
|
8
|
+
}"
|
|
9
|
+
:style="rootStyles"
|
|
10
|
+
>
|
|
11
|
+
<div
|
|
12
|
+
v-if="label && !top"
|
|
13
|
+
class="d-inline-block vtk-width-2 align-middle pb-5"
|
|
14
|
+
:class="{ 'text-right': !left, 'text-left': left }"
|
|
15
|
+
style="min-width: 120px; vertical-align: middle;"
|
|
16
|
+
>
|
|
17
|
+
<span v-if="must" :class="left ? 'vtk-width-2' : ''" class="text-red mr-1">*</span>
|
|
18
|
+
<span>{{ label }}</span>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div
|
|
22
|
+
v-if="label && top"
|
|
23
|
+
class="pb-2 text-left"
|
|
24
|
+
>
|
|
25
|
+
<span v-if="must" class="text-red mr-1">*</span>
|
|
26
|
+
<span>{{ label }}</span>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div
|
|
30
|
+
v-if="!top"
|
|
31
|
+
class="d-inline-block vtk-width-6 align-middle"
|
|
32
|
+
:class="left ? 'vtk-width-8' : ''"
|
|
33
|
+
style="min-width: 300px; vertical-align: middle;"
|
|
34
|
+
>
|
|
35
|
+
<slot></slot>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div
|
|
39
|
+
v-if="top"
|
|
40
|
+
:style="contentStyles"
|
|
41
|
+
>
|
|
42
|
+
<slot></slot>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<script setup>
|
|
48
|
+
import { computed, useAttrs } from 'vue';
|
|
49
|
+
|
|
50
|
+
// 定义组件名称和选项
|
|
51
|
+
defineOptions({
|
|
52
|
+
name: "VtkFormItem",
|
|
53
|
+
inheritAttrs: false,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 获取所有传递的属性
|
|
57
|
+
const attrs = useAttrs();
|
|
58
|
+
|
|
59
|
+
// 定义props
|
|
60
|
+
const props = defineProps({
|
|
61
|
+
label: {
|
|
62
|
+
type: String,
|
|
63
|
+
default: null,
|
|
64
|
+
},
|
|
65
|
+
must: {
|
|
66
|
+
type: Boolean,
|
|
67
|
+
default: false,
|
|
68
|
+
},
|
|
69
|
+
left: {
|
|
70
|
+
type: Boolean,
|
|
71
|
+
default: false,
|
|
72
|
+
},
|
|
73
|
+
// 标签在上方的垂直布局
|
|
74
|
+
top: {
|
|
75
|
+
type: Boolean,
|
|
76
|
+
default: false,
|
|
77
|
+
},
|
|
78
|
+
// 控制top模式下内容的最大宽度
|
|
79
|
+
maxWidth: {
|
|
80
|
+
type: [String, Number],
|
|
81
|
+
default: '100%'
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// 根元素的类名
|
|
86
|
+
const rootClasses = computed(() => {
|
|
87
|
+
const classes = {};
|
|
88
|
+
if (attrs.class) {
|
|
89
|
+
// 如果是字符串,直接添加
|
|
90
|
+
if (typeof attrs.class === 'string') {
|
|
91
|
+
attrs.class.split(' ').forEach(cls => {
|
|
92
|
+
if (cls) classes[cls] = true;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
// 如果是对象,合并
|
|
96
|
+
else if (typeof attrs.class === 'object') {
|
|
97
|
+
Object.assign(classes, attrs.class);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return classes;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// 根元素的样式
|
|
104
|
+
const rootStyles = computed(() => {
|
|
105
|
+
// 合并传入的样式和组件内部样式
|
|
106
|
+
const styles = {};
|
|
107
|
+
|
|
108
|
+
// 应用父组件传递的样式
|
|
109
|
+
if (attrs.style) {
|
|
110
|
+
if (typeof attrs.style === 'string') {
|
|
111
|
+
// 解析字符串样式
|
|
112
|
+
attrs.style.split(';').forEach(style => {
|
|
113
|
+
const [key, value] = style.split(':');
|
|
114
|
+
if (key && value) {
|
|
115
|
+
styles[key.trim()] = value.trim();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
} else if (typeof attrs.style === 'object') {
|
|
119
|
+
// 直接使用对象样式
|
|
120
|
+
Object.assign(styles, attrs.style);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return styles;
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// 处理maxWidth属性
|
|
128
|
+
const processedMaxWidth = computed(() => {
|
|
129
|
+
if (typeof props.maxWidth === 'number') {
|
|
130
|
+
return props.maxWidth + 'px';
|
|
131
|
+
}
|
|
132
|
+
return props.maxWidth;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// 内容样式(仅在top模式下应用maxWidth)
|
|
136
|
+
const contentStyles = computed(() => {
|
|
137
|
+
const styles = {};
|
|
138
|
+
|
|
139
|
+
// 在top模式下应用maxWidth
|
|
140
|
+
if (props.top && props.maxWidth) {
|
|
141
|
+
styles.maxWidth = processedMaxWidth.value;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 在非top模式下保持原有最小宽度
|
|
145
|
+
if (!props.top) {
|
|
146
|
+
styles.minWidth = '300px';
|
|
147
|
+
styles.verticalAlign = 'middle';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return styles;
|
|
151
|
+
});
|
|
152
|
+
</script>
|
|
153
|
+
|
|
154
|
+
<style scoped>
|
|
155
|
+
.mb-2 {
|
|
156
|
+
margin-bottom: 0.5rem;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.form-item--vertical {
|
|
160
|
+
display: block;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.form-item--horizontal {
|
|
164
|
+
display: block;
|
|
165
|
+
}
|
|
166
|
+
</style>
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="vtk-img-container">
|
|
3
|
+
<VCard
|
|
4
|
+
:class="['vtk-img-card', { 'vtk-img-card--preview': preview }]"
|
|
5
|
+
@click="openPreview"
|
|
6
|
+
>
|
|
7
|
+
<v-img
|
|
8
|
+
v-bind="$attrs"
|
|
9
|
+
:src="srcWithToken"
|
|
10
|
+
class="vtk-img"
|
|
11
|
+
:aspect-ratio="aspectRatio"
|
|
12
|
+
cover
|
|
13
|
+
>
|
|
14
|
+
<template v-slot:placeholder>
|
|
15
|
+
<div class="d-flex align-center justify-center fill-height">
|
|
16
|
+
<v-progress-circular indeterminate color="grey-lighten-1"></v-progress-circular>
|
|
17
|
+
</div>
|
|
18
|
+
</template>
|
|
19
|
+
</v-img>
|
|
20
|
+
|
|
21
|
+
<!-- 图片标题和描述 -->
|
|
22
|
+
<div class="vtk-img-info" v-if="showTitle || showDescription">
|
|
23
|
+
<VCardTitle v-if="showTitle" class="vtk-img-title text-subtitle-2 pa-2">
|
|
24
|
+
{{ title }}
|
|
25
|
+
</VCardTitle>
|
|
26
|
+
<v-card-subtitle v-if="showDescription" class="vtk-img-description text-caption pa-2">
|
|
27
|
+
{{ description }}
|
|
28
|
+
</v-card-subtitle>
|
|
29
|
+
</div>
|
|
30
|
+
</VCard>
|
|
31
|
+
|
|
32
|
+
<!-- 使用 v-dialog 的图片预览 -->
|
|
33
|
+
<VDialog
|
|
34
|
+
v-model="showPreview"
|
|
35
|
+
max-width="90%"
|
|
36
|
+
fullscreen
|
|
37
|
+
@click:outside="closePreview"
|
|
38
|
+
>
|
|
39
|
+
<VCard class="image-viewer-card">
|
|
40
|
+
<v-toolbar dark color="primary">
|
|
41
|
+
<VBtn icon dark @click="closePreview">
|
|
42
|
+
<VIcon>mdi-close</VIcon>
|
|
43
|
+
</VBtn>
|
|
44
|
+
<v-toolbar-title>{{ currentImageTitle }}</v-toolbar-title>
|
|
45
|
+
<VSpacer></VSpacer>
|
|
46
|
+
<v-toolbar-items>
|
|
47
|
+
<VBtn icon dark @click="rotateImage(-90)">
|
|
48
|
+
<VIcon>mdi-rotate-left</VIcon>
|
|
49
|
+
</VBtn>
|
|
50
|
+
<VBtn icon dark @click="rotateImage(90)">
|
|
51
|
+
<VIcon>mdi-rotate-right</VIcon>
|
|
52
|
+
</VBtn>
|
|
53
|
+
</v-toolbar-items>
|
|
54
|
+
</v-toolbar>
|
|
55
|
+
|
|
56
|
+
<div class="image-viewer-content">
|
|
57
|
+
<VBtn
|
|
58
|
+
v-if="hasPrev"
|
|
59
|
+
icon
|
|
60
|
+
class="nav-button prev-button"
|
|
61
|
+
@click="prevImage"
|
|
62
|
+
>
|
|
63
|
+
<VIcon>mdi-chevron-left</VIcon>
|
|
64
|
+
</VBtn>
|
|
65
|
+
|
|
66
|
+
<div class="image-container">
|
|
67
|
+
<v-img
|
|
68
|
+
:src="currentImageUrl"
|
|
69
|
+
class="viewer-image"
|
|
70
|
+
:style="{ transform: `rotate(${imageRotation}deg)` }"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<VBtn
|
|
75
|
+
v-if="hasNext"
|
|
76
|
+
icon
|
|
77
|
+
class="nav-button next-button"
|
|
78
|
+
@click="nextImage"
|
|
79
|
+
>
|
|
80
|
+
<VIcon>mdi-chevron-right</VIcon>
|
|
81
|
+
</VBtn>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<div class="image-info">
|
|
85
|
+
<p class="text-center">{{ currentImageDescription }}</p>
|
|
86
|
+
<p class="text-center text--secondary">{{ currentIndex + 1 }} / {{ imageList.length }}</p>
|
|
87
|
+
</div>
|
|
88
|
+
</VCard>
|
|
89
|
+
</VDialog>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
92
|
+
|
|
93
|
+
<script setup>
|
|
94
|
+
import { ref, computed, watch } from 'vue';
|
|
95
|
+
|
|
96
|
+
defineOptions({
|
|
97
|
+
name: 'VtkImg',
|
|
98
|
+
inheritAttrs: true
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const props = defineProps({
|
|
102
|
+
src: {
|
|
103
|
+
type: String,
|
|
104
|
+
default: null,
|
|
105
|
+
},
|
|
106
|
+
error: {
|
|
107
|
+
type: String,
|
|
108
|
+
default: new URL('../../assets/img/wxsq.png', import.meta.url).href,
|
|
109
|
+
},
|
|
110
|
+
preview: {
|
|
111
|
+
type: Boolean,
|
|
112
|
+
default: true
|
|
113
|
+
},
|
|
114
|
+
title: {
|
|
115
|
+
type: String,
|
|
116
|
+
default: ''
|
|
117
|
+
},
|
|
118
|
+
description: {
|
|
119
|
+
type: String,
|
|
120
|
+
default: ''
|
|
121
|
+
},
|
|
122
|
+
// 图片列表,用于导航功能
|
|
123
|
+
imageList: {
|
|
124
|
+
type: Array,
|
|
125
|
+
default: () => []
|
|
126
|
+
},
|
|
127
|
+
// 当前图片索引
|
|
128
|
+
index: {
|
|
129
|
+
type: Number,
|
|
130
|
+
default: 0
|
|
131
|
+
},
|
|
132
|
+
// 是否显示标题
|
|
133
|
+
showTitle: {
|
|
134
|
+
type: Boolean,
|
|
135
|
+
default: true
|
|
136
|
+
},
|
|
137
|
+
// 是否显示描述
|
|
138
|
+
showDescription: {
|
|
139
|
+
type: Boolean,
|
|
140
|
+
default: true
|
|
141
|
+
},
|
|
142
|
+
// 图片宽高比
|
|
143
|
+
aspectRatio: {
|
|
144
|
+
type: [String, Number],
|
|
145
|
+
default: 16/9
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// 预览状态
|
|
150
|
+
const showPreview = ref(false);
|
|
151
|
+
// 图片旋转角度
|
|
152
|
+
const imageRotation = ref(0);
|
|
153
|
+
// 当前图片索引(预览模式下使用)
|
|
154
|
+
const currentIndex = ref(0);
|
|
155
|
+
|
|
156
|
+
// 使用计算属性处理带token的URL
|
|
157
|
+
const srcWithToken = computed(() => {
|
|
158
|
+
if (!props.src) return props.src;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
// 首先尝试使用 $vtk.storage
|
|
162
|
+
if (window.$vtk && typeof window.$vtk.storage?.get === 'function') {
|
|
163
|
+
const token = window.$vtk.storage.get('token');
|
|
164
|
+
return token ? `${props.src}?stoken=${token}` : props.src;
|
|
165
|
+
}
|
|
166
|
+
// 如果 $vtk 不存在,尝试从 localStorage 获取 token
|
|
167
|
+
else {
|
|
168
|
+
const token = localStorage.getItem('token');
|
|
169
|
+
return token ? `${props.src}?stoken=${token}` : props.src;
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
// 发生错误时返回原始src
|
|
173
|
+
console.warn('VtkImg: Failed to get token, using src without token', error);
|
|
174
|
+
return props.src;
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// 当前预览图片的URL
|
|
179
|
+
const currentImageUrl = computed(() => {
|
|
180
|
+
if (props.imageList.length > 0 && currentIndex.value < props.imageList.length) {
|
|
181
|
+
const currentImage = props.imageList[currentIndex.value];
|
|
182
|
+
if (currentImage && currentImage.url) {
|
|
183
|
+
try {
|
|
184
|
+
// 为图片列表中的图片也添加token
|
|
185
|
+
if (window.$vtk && typeof window.$vtk.storage?.get === 'function') {
|
|
186
|
+
const token = window.$vtk.storage.get('token');
|
|
187
|
+
return token ? `${currentImage.url}?stoken=${token}` : currentImage.url;
|
|
188
|
+
} else {
|
|
189
|
+
const token = localStorage.getItem('token');
|
|
190
|
+
return token ? `${currentImage.url}?stoken=${token}` : currentImage.url;
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
return currentImage.url;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return srcWithToken.value;
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// 当前图片标题
|
|
201
|
+
const currentImageTitle = computed(() => {
|
|
202
|
+
if (props.imageList.length > 0 && currentIndex.value < props.imageList.length) {
|
|
203
|
+
const currentImage = props.imageList[currentIndex.value];
|
|
204
|
+
return currentImage?.title || '';
|
|
205
|
+
}
|
|
206
|
+
return props.title;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// 当前图片描述
|
|
210
|
+
const currentImageDescription = computed(() => {
|
|
211
|
+
if (props.imageList.length > 0 && currentIndex.value < props.imageList.length) {
|
|
212
|
+
const currentImage = props.imageList[currentIndex.value];
|
|
213
|
+
return currentImage?.description || '';
|
|
214
|
+
}
|
|
215
|
+
return props.description;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// 是否有上一张图片
|
|
219
|
+
const hasPrev = computed(() => {
|
|
220
|
+
return props.imageList.length > 1 && currentIndex.value > 0;
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// 是否有下一张图片
|
|
224
|
+
const hasNext = computed(() => {
|
|
225
|
+
return props.imageList.length > 1 && currentIndex.value < props.imageList.length - 1;
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// 监听预览状态变化
|
|
229
|
+
watch(showPreview, (newVal) => {
|
|
230
|
+
if (newVal) {
|
|
231
|
+
// 打开预览时重置旋转角度和当前索引
|
|
232
|
+
imageRotation.value = 0;
|
|
233
|
+
currentIndex.value = props.index;
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// 打开预览
|
|
238
|
+
const openPreview = () => {
|
|
239
|
+
if (props.preview) {
|
|
240
|
+
showPreview.value = true;
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// 关闭预览
|
|
245
|
+
const closePreview = () => {
|
|
246
|
+
showPreview.value = false;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// 上一张图片
|
|
250
|
+
const prevImage = () => {
|
|
251
|
+
if (hasPrev.value) {
|
|
252
|
+
currentIndex.value--;
|
|
253
|
+
imageRotation.value = 0; // 切换图片时重置旋转
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// 下一张图片
|
|
258
|
+
const nextImage = () => {
|
|
259
|
+
if (hasNext.value) {
|
|
260
|
+
currentIndex.value++;
|
|
261
|
+
imageRotation.value = 0; // 切换图片时重置旋转
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// 旋转图片
|
|
266
|
+
const rotateImage = (degrees) => {
|
|
267
|
+
imageRotation.value += degrees;
|
|
268
|
+
};
|
|
269
|
+
</script>
|
|
270
|
+
|
|
271
|
+
<style lang="scss" scoped>
|
|
272
|
+
.vtk-img-container {
|
|
273
|
+
position: relative;
|
|
274
|
+
display: inline-block;
|
|
275
|
+
width: 100%;
|
|
276
|
+
height: 100%;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.vtk-img-card {
|
|
280
|
+
height: 100%;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.vtk-img-card--preview {
|
|
284
|
+
cursor: pointer;
|
|
285
|
+
transition: transform 0.2s ease-in-out;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.vtk-img-card--preview:hover {
|
|
289
|
+
transform: translateY(-5px);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.vtk-img {
|
|
293
|
+
width: 100%;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.vtk-img-info {
|
|
297
|
+
padding: 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.vtk-img-title {
|
|
301
|
+
word-break: break-word;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.vtk-img-description {
|
|
305
|
+
word-break: break-word;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* 图片查看器样式 */
|
|
309
|
+
.image-viewer-card {
|
|
310
|
+
background-color: rgba(0, 0, 0, 0.9);
|
|
311
|
+
color: white;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.image-viewer-content {
|
|
315
|
+
display: flex;
|
|
316
|
+
align-items: center;
|
|
317
|
+
justify-content: center;
|
|
318
|
+
height: calc(100vh - 150px);
|
|
319
|
+
position: relative;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.image-container {
|
|
323
|
+
flex: 1;
|
|
324
|
+
display: flex;
|
|
325
|
+
align-items: center;
|
|
326
|
+
justify-content: center;
|
|
327
|
+
overflow: hidden;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.viewer-image {
|
|
331
|
+
max-width: 90%;
|
|
332
|
+
max-height: 80vh;
|
|
333
|
+
transition: transform 0.3s ease;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.nav-button {
|
|
337
|
+
position: absolute;
|
|
338
|
+
top: 50%;
|
|
339
|
+
transform: translateY(-50%);
|
|
340
|
+
z-index: 10;
|
|
341
|
+
background-color: rgba(0, 0, 0, 0.5);
|
|
342
|
+
color: white;
|
|
343
|
+
width: 50px;
|
|
344
|
+
height: 50px;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.prev-button {
|
|
348
|
+
left: 20px;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.next-button {
|
|
352
|
+
right: 20px;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.image-info {
|
|
356
|
+
padding: 20px;
|
|
357
|
+
background-color: rgba(0, 0, 0, 0.7);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.image-info p {
|
|
361
|
+
margin: 5px 0;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
:deep(.v-img__img) {
|
|
365
|
+
background: v-bind('props.error') no-repeat center;
|
|
366
|
+
background-size: cover;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
:deep(.v-img__placeholder) {
|
|
370
|
+
filter: blur(0px);
|
|
371
|
+
}
|
|
372
|
+
</style>
|