v-uni-app-ui 1.0.0 → 1.0.2

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 (61) hide show
  1. package/components/config.js +123 -0
  2. package/components/layout/v-card/v-card.vue +108 -0
  3. package/components/layout/v-grid/v-grid.vue +162 -0
  4. package/components/layout/v-icon-grid/v-icon-grid.vue +195 -0
  5. package/components/layout/v-infinite-scroll/v-infinite-scroll.vue +172 -0
  6. package/components/layout/v-list/v-list.vue +43 -0
  7. package/components/layout/v-row/v-row.vue +142 -0
  8. package/components/layout/v-waterfall/v-waterfall.vue +79 -0
  9. package/components/model/compound/v-checkbox-group/v-checkbox-group.vue +96 -0
  10. package/components/model/compound/v-console/v-console.js +20 -0
  11. package/components/model/compound/v-console/v-console.vue +299 -0
  12. package/components/model/compound/v-date-time/v-date-time.vue +261 -0
  13. package/components/model/compound/v-dialog/v-dialog.vue +178 -0
  14. package/components/model/compound/v-drum-select-picker/v-drum-select-picker.vue +83 -0
  15. package/components/model/compound/v-form/v-form.vue +226 -0
  16. package/components/model/compound/v-form-item/v-form-item.vue +255 -0
  17. package/components/model/compound/v-image/v-image.vue +357 -0
  18. package/components/model/compound/v-input-desensitize/v-input-desensitize.vue +101 -0
  19. package/components/model/compound/v-page/v-page.vue +11 -0
  20. package/components/model/compound/v-pages/v-pages.vue +141 -0
  21. package/components/model/compound/v-picker-list/v-picker-list.vue +109 -0
  22. package/components/model/compound/v-popup/v-popup.vue +151 -0
  23. package/components/model/compound/v-radio-group/v-radio-group.vue +86 -0
  24. package/components/model/compound/v-select-picker/v-select-picker.vue +202 -0
  25. package/components/model/compound/v-series-picker-list/v-series-picker-list.vue +221 -0
  26. package/components/model/compound/v-series-select-picker/v-series-select-picker.vue +203 -0
  27. package/components/model/compound/v-switch/v-switch.vue +136 -0
  28. package/components/model/compound/v-tabs-page/v-tabs-page.vue +138 -0
  29. package/components/model/native/v-badge/v-badge.vue +143 -0
  30. package/components/model/native/v-button/v-button.vue +273 -0
  31. package/components/model/native/v-carousel/v-carousel.vue +138 -0
  32. package/components/model/native/v-checkbox/v-checkbox.vue +215 -0
  33. package/components/model/native/v-collapse/v-collapse.vue +190 -0
  34. package/components/model/native/v-header-navigation-bar/v-header-navigation-bar.vue +92 -0
  35. package/components/model/native/v-input/v-input.vue +352 -0
  36. package/components/model/native/v-input-code/v-input-code.vue +146 -0
  37. package/components/model/native/v-loading/v-loading.vue +206 -0
  38. package/components/model/native/v-menu/v-menu.vue +222 -0
  39. package/components/model/native/v-menu-slide/v-menu-slide.vue +364 -0
  40. package/components/model/native/v-min-loading/v-min-loading.vue +80 -0
  41. package/components/model/native/v-null/v-null.vue +97 -0
  42. package/components/model/native/v-overlay/v-overlay.vue +96 -0
  43. package/components/model/native/v-pull-up-refresh/v-pull-up-refresh.vue +157 -0
  44. package/components/model/native/v-radio/v-radio.vue +138 -0
  45. package/components/model/native/v-scroll-list/v-scroll-list.vue +169 -0
  46. package/components/model/native/v-steps/v-steps.vue +253 -0
  47. package/components/model/native/v-table/v-table.vue +203 -0
  48. package/components/model/native/v-tabs/v-tabs.vue +235 -0
  49. package/components/model/native/v-tag/v-tag.vue +206 -0
  50. package/components/model/native/v-text/v-text.vue +187 -0
  51. package/components/model/native/v-text-button/v-text-button.vue +139 -0
  52. package/components/model/native/v-textarea/v-textarea.vue +178 -0
  53. package/components/model/native/v-title/v-title.vue +91 -0
  54. package/components/model/native/v-toast/info.png +0 -0
  55. package/components/model/native/v-toast/success.png +0 -0
  56. package/components/model/native/v-toast/v-toast.vue +198 -0
  57. package/components/model/native/v-toast/warn.png +0 -0
  58. package/components/model/native/v-upload-file-button/v-upload-file-button.vue +296 -0
  59. package/components/model/native/v-video/v-video.vue +175 -0
  60. package/components/model/native/v-window/v-window.vue +158 -0
  61. package/package.json +18 -94
@@ -0,0 +1,91 @@
1
+ <template>
2
+ <view :class="['v-title', `v-title--level-${level}`]" :style="titleStyle">
3
+ <slot></slot>
4
+ </view>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import { computed,inject } from 'vue';
9
+
10
+ const props = defineProps({
11
+ level: {
12
+ type: Number,
13
+ default: 1,
14
+ validator: (val: number) => val >= 1 && val <= 6
15
+ },
16
+ fontSize: {
17
+ type: String,
18
+ default: ''
19
+ },
20
+ color: {
21
+ type: String,
22
+ default: ''
23
+ }
24
+ });
25
+
26
+ const config = inject<any>('config');
27
+ const titleStyle = computed(() => {
28
+ const style: Record<string, string> = {};
29
+ if (props.fontSize) {
30
+ style['font-size'] = props.fontSize;
31
+ }
32
+ if (props.color) {
33
+ style['color'] = props.color;
34
+ }
35
+ return style;
36
+ });
37
+ </script>
38
+
39
+ <style lang="scss" scoped>
40
+ .v-title {
41
+ font-weight: bold;
42
+
43
+ &--level-1 {
44
+ font-size: v-bind("config.fontSize.largeTitle");
45
+ color: v-bind("config.fontColor.mianTitle");
46
+ }
47
+
48
+ &--level-2 {
49
+ font-size: v-bind("config.fontSize.mediumTitle");
50
+ color: v-bind("config.fontColor.mianTitle");
51
+ }
52
+
53
+ &--level-3 {
54
+ font-size: v-bind("config.fontSize.smallTitle");
55
+ color: v-bind("config.fontColor.mianTitle");
56
+ }
57
+
58
+ &--level-4 {
59
+ font-size: v-bind("config.fontSize.largeText");
60
+ color: v-bind("config.fontColor.subTitle");
61
+ }
62
+
63
+ &--level-5 {
64
+ font-size: v-bind("config.fontSize.mediumText");
65
+ color: v-bind("config.fontColor.subTitle");
66
+ }
67
+
68
+ &--level-6 {
69
+ font-size: v-bind("config.fontSize.smallText");
70
+ color: v-bind("config.fontColor.subTitle");
71
+ }
72
+
73
+ @media (prefers-color-scheme: dark) {
74
+ &--level-1,
75
+ &--level-2,
76
+ &--level-3 {
77
+ color: #e0e0e0;
78
+ }
79
+
80
+ &--level-4,
81
+ &--level-5,
82
+ &--level-6 {
83
+ color: #b0b0b0;
84
+ }
85
+
86
+ :deep(.v-title) {
87
+ color: #e0e0e0;
88
+ }
89
+ }
90
+ }
91
+ </style>
@@ -0,0 +1,198 @@
1
+ <template>
2
+ <view v-if="toastList.length > 0" class="toast-container">
3
+ <view v-for="(toast, index) in toastList" :key="index" class="toast-content-wrapper" :style="{ top: `${20 + index * 10}%` }">
4
+ <view class="toast-content" :class="[toast.typeClass, toast.customClass]" v-if="toast.isVisible">
5
+ <!-- 信息图标 -->
6
+ <text v-if="toast.type === 'info'" class="icon">
7
+ <image src="./info.png" />
8
+ </text>
9
+ <!-- 警告图标 -->
10
+ <text v-else-if="toast.type === 'warn'" class="icon"><image src="./warn.png" /></text>
11
+ <!-- 加载中图标 -->
12
+ <view v-else-if="toast.type === 'loading'" class="loading-icon"></view>
13
+ <!-- 成功图标 -->
14
+ <text v-else-if="toast.type === 'success'" class="icon"><image src="./success.png" /></text>
15
+ <!-- 错误图标 -->
16
+ <text v-else-if="toast.type === 'error'" class="icon"><view class="text-icon">✕</view></text>
17
+ <!-- 消息内容 -->
18
+ <text class="toast-message">{{ toast.message }}</text>
19
+ </view>
20
+ </view>
21
+ </view>
22
+ </template>
23
+
24
+ <script lang="ts" setup>
25
+ import { ref, computed, onUnmounted } from 'vue';
26
+ import { config } from '../../../config';
27
+
28
+ type ToastType = 'info' | 'success' | 'error' | 'loading' | 'warn';
29
+ type ToastPosition = 'top' | 'center' | 'bottom';
30
+
31
+ interface ToastItem {
32
+ message: string;
33
+ type: ToastType;
34
+ position: ToastPosition;
35
+ duration: number;
36
+ customClass: string;
37
+ typeClass: string;
38
+ isVisible: boolean;
39
+ isLoaded: boolean;
40
+ timer?: NodeJS.Timeout;
41
+ }
42
+
43
+ // Toast 列表
44
+ const toastList = ref<ToastItem[]>([]);
45
+
46
+ const show = (options: { message: string; type?: ToastType; duration?: number; position?: ToastPosition; customClass?: string }) => {
47
+ // 如果超过3个,则清空全部
48
+ if (toastList.value.length >= 3) {
49
+ clearAll();
50
+ }
51
+
52
+ // 添加新的 Toast
53
+ const newToast: ToastItem = {
54
+ message: options.message,
55
+ type: options.type || 'info',
56
+ duration: options.duration || 2000,
57
+ position: options.position || 'top',
58
+ customClass: options.customClass || '',
59
+ typeClass: `toast-${options.type || 'info'}`,
60
+ isVisible: true,
61
+ isLoaded: false
62
+ };
63
+
64
+ toastList.value.push(newToast);
65
+ if (newToast.duration > 0) {
66
+ newToast.timer = setTimeout(() => {
67
+ removeToast(newToast);
68
+ }, newToast.duration);
69
+ }
70
+ };
71
+
72
+ const removeToast = (toast: ToastItem) => {
73
+ if (!toast.timer) {
74
+ return;
75
+ }
76
+ clearTimeout(toast.timer);
77
+ toast.timer = undefined;
78
+ toast.isVisible = false;
79
+ console.log(JSON.stringify(toast));
80
+ toastList.value = toastList.value.filter((t) => t !== toast);
81
+ };
82
+
83
+ const clearAll = () => {
84
+ // 清除所有定时器
85
+ toastList.value.forEach((toast) => {
86
+ if (toast.timer) {
87
+ clearTimeout(toast.timer);
88
+ toast.timer = undefined;
89
+ }
90
+ });
91
+ toastList.value = [];
92
+ };
93
+
94
+ onUnmounted(() => {
95
+ clearAll();
96
+ });
97
+
98
+ defineExpose({ show, clearAll });
99
+ </script>
100
+
101
+ <style lang="scss">
102
+ .toast-container {
103
+ position: fixed;
104
+ left: 0;
105
+ right: 0;
106
+ top: 10%;
107
+ bottom: 0;
108
+ display: flex;
109
+ flex-direction: column;
110
+ justify-content: flex-start;
111
+ pointer-events: none;
112
+ z-index: 9999;
113
+ }
114
+
115
+ .toast-content-wrapper {
116
+ margin-top: 2.5%;
117
+ display: flex;
118
+ justify-content: center;
119
+ }
120
+
121
+ .toast-content {
122
+ min-width: 65%;
123
+ padding: 15rpx 25rpx;
124
+ background-color: #000000;
125
+ opacity: v-bind('config.opacity.toast');
126
+ border-radius: 8rpx;
127
+ color: v-bind('config.fontColor.reversal');
128
+ font-size: 28rpx;
129
+ display: flex;
130
+ align-items: center;
131
+ max-width: 70%;
132
+ box-sizing: border-box;
133
+ line-height: 1.5;
134
+
135
+ .icon {
136
+ margin-right: 16rpx;
137
+ font-size: 36rpx;
138
+ image {
139
+ margin: auto;
140
+ width: 32rpx;
141
+ height: 32rpx;
142
+ margin-top: 10rpx;
143
+ }
144
+ .text-icon {
145
+ display: block;
146
+ margin-top: -8rpx;
147
+ }
148
+ }
149
+ .toast-message {
150
+ margin-left: 20rpx;
151
+ text-align: left;
152
+ display: flex;
153
+ align-items: center;
154
+ }
155
+
156
+ .loading-icon {
157
+ width: 36rpx;
158
+ height: 36rpx;
159
+ margin-right: 16rpx;
160
+ border: 4rpx solid #fff;
161
+ border-top-color: transparent;
162
+ border-radius: 50%;
163
+ animation: rotating 1s linear infinite;
164
+ display: flex;
165
+ align-items: center;
166
+ }
167
+
168
+ &.toast-info {
169
+ background-color: v-bind('config.backgroundColor.info');
170
+ }
171
+
172
+ &.toast-success {
173
+ background-color: v-bind('config.backgroundColor.succeed');
174
+ }
175
+
176
+ &.toast-error {
177
+ background-color: v-bind('config.backgroundColor.delete');
178
+ }
179
+
180
+ &.toast-warn {
181
+ background-color: v-bind('config.backgroundColor.warn');
182
+ }
183
+
184
+ &.toast-loading {
185
+ background-color: #000000;
186
+ opacity: v-bind('config.opacity.toast');
187
+ }
188
+ }
189
+
190
+ @keyframes rotating {
191
+ from {
192
+ transform: rotate(0deg);
193
+ }
194
+ to {
195
+ transform: rotate(360deg);
196
+ }
197
+ }
198
+ </style>
@@ -0,0 +1,296 @@
1
+ <template>
2
+ <button @click="handleFileChange" :class="['upload-button', { uploading: uploading }]" :disabled="disabled">
3
+ <template v-if="!uploading && !fileSelected">
4
+ <slot>
5
+ <view class="upload-icon">
6
+ <text class="icon-upload"></text>
7
+ </view>
8
+ <view class="upload-text">
9
+ {{ buttonText }}
10
+ </view>
11
+ </slot>
12
+ </template>
13
+ <template v-else>
14
+ <view v-if="uploading" class="upload-progress">
15
+ <slot name="loading" :progress="progress">
16
+ <view class="progress-bar">
17
+ <view class="progress-bar-filled" :style="{ width: `${progress}%` }"></view>
18
+ </view>
19
+ <view class="progress-text">{{ progress }}%</view>
20
+ </slot>
21
+ </view>
22
+ <view v-else-if="fileSelected" class="selected-file">
23
+ <slot name="selected-file" :file="selectedFile" :file-type="fileFormat" :file-size="fileSize">
24
+ <view class="file-name">{{ selectedFile.name }}</view>
25
+ <view class="file-info">{{ fileFormat }} {{ fileSize }}</view>
26
+ </slot>
27
+ </view>
28
+ </template>
29
+ </button>
30
+ </template>
31
+
32
+ <script setup lang="ts">
33
+ import { ref, computed, inject, watchEffect } from 'vue';
34
+
35
+ const props = defineProps({
36
+ buttonText: {
37
+ type: String,
38
+ default: '上传文件'
39
+ },
40
+ accept: {
41
+ type: String,
42
+ default: '*'
43
+ },
44
+ disabled: {
45
+ type: Boolean,
46
+ default: false
47
+ },
48
+ uploadUrl: {
49
+ type: String,
50
+ required: true
51
+ },
52
+ toKen: {
53
+ type: String,
54
+ default: null
55
+ },
56
+ requestMode: {
57
+ type: String,
58
+ default: 'POST'
59
+ }
60
+ });
61
+
62
+ const emit = defineEmits(['success', 'error', 'cancel']);
63
+
64
+ const config = inject<any>('config');
65
+ const selectedFile = ref(null);
66
+ const progress = ref<number>(0);
67
+ const uploading = ref<boolean>(false);
68
+ const fileSelected = ref<boolean>(false);
69
+
70
+ const fileFormat = computed(() => {
71
+ if (selectedFile.value) {
72
+ const name = selectedFile.value.name;
73
+ const extension = name.split('.').pop().toLowerCase(); // 获取文件扩展名
74
+
75
+ if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(extension)) {
76
+ return '图片';
77
+ }
78
+ if (extension === 'pdf') {
79
+ return 'PDF';
80
+ }
81
+ return '文件';
82
+ }
83
+ return '';
84
+ });
85
+
86
+ const fileSize = computed(() => {
87
+ if (selectedFile.value) {
88
+ const size = selectedFile.value.size;
89
+ if (size < 1024) return `${size} B`;
90
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(2)} KB`;
91
+ return `${(size / 1024 / 1024).toFixed(2)} MB`;
92
+ }
93
+ return '';
94
+ });
95
+
96
+ watchEffect(() => {
97
+ if (props.disabled) {
98
+ uploading.value = false;
99
+ }
100
+ });
101
+ const handleFileChange = () => {
102
+ if (typeof uni !== 'undefined') {
103
+ // 使用 uni.chooseImage 选择图片
104
+ uni.chooseImage({
105
+ count: 1,
106
+ sizeType: ['original', 'compressed'],
107
+ sourceType: ['album', 'camera'],
108
+ success: (res) => {
109
+ const tempFilePath = res.tempFilePaths[0];
110
+ selectedFile.value = { path: tempFilePath, name: tempFilePath.split('/').pop() };
111
+ fileSelected.value = true;
112
+ uploadFile(tempFilePath); // 直接传递文件路径
113
+ },
114
+ fail: (err) => {
115
+ console.error('选择图片失败:', err);
116
+ }
117
+ });
118
+ } else {
119
+ // 浏览器环境使用 HTML File API
120
+ const input = document.createElement('input');
121
+ input.type = 'file';
122
+ input.accept = props.accept;
123
+ input.onchange = (e) => {
124
+ const files = e.target.files;
125
+ if (files && files.length > 0) {
126
+ selectedFile.value = files[0];
127
+ fileSelected.value = true;
128
+ uploadFile(files[0]);
129
+ }
130
+ };
131
+ input.click();
132
+ }
133
+ };
134
+
135
+ const uploadFile = (file) => {
136
+ if (!file) return;
137
+ uploading.value = true;
138
+ progress.value = 0;
139
+
140
+ if (typeof uni !== 'undefined') {
141
+ // uni-app 环境
142
+ const uploadTask = uni.uploadFile({
143
+ url: props.uploadUrl,
144
+ filePath: file,
145
+ name: 'file',
146
+ header: props.toKen
147
+ ? {
148
+ Authorization: `Bearer ${props.toKen}`
149
+ }
150
+ : {},
151
+ formData: {},
152
+ timeout: 60000, // 设置超时时间为60秒
153
+ success: (res) => {
154
+ emit('success', JSON.parse(res.data));
155
+ },
156
+ fail: (err) => {
157
+ console.error('上传失败:', err);
158
+ emit('error', err);
159
+ },
160
+ complete: () => {
161
+ uploading.value = false;
162
+ }
163
+ });
164
+
165
+ // 监听上传进度
166
+ uploadTask.onProgressUpdate((res) => {
167
+ progress.value = res.progress;
168
+ });
169
+ } else {
170
+ // 浏览器环境
171
+ const formData = new FormData();
172
+ formData.append('file', file);
173
+
174
+ const xhr = new XMLHttpRequest();
175
+ xhr.open(props.requestMode, props.uploadUrl, true);
176
+ if (props.toKen) {
177
+ xhr.setRequestHeader('Authorization', `Bearer ${props.toKen}`);
178
+ }
179
+
180
+ xhr.upload.onprogress = (event) => {
181
+ if (event.lengthComputable) {
182
+ progress.value = (event.loaded / event.total) * 100;
183
+ }
184
+ };
185
+
186
+ xhr.onload = () => {
187
+ if (xhr.status >= 200 && xhr.status < 300) {
188
+ emit('success', JSON.parse(xhr.responseText));
189
+ } else {
190
+ console.error('上传失败:', xhr.statusText);
191
+ emit('error', { errMsg: '上传失败' });
192
+ }
193
+ uploading.value = false;
194
+ };
195
+
196
+ xhr.onerror = () => {
197
+ console.error('上传出错:', xhr.statusText);
198
+ emit('error', { errMsg: '上传出错' });
199
+ uploading.value = false;
200
+ };
201
+
202
+ xhr.send(formData);
203
+ }
204
+ };
205
+ </script>
206
+
207
+ <style lang="scss" scoped>
208
+ .upload-button {
209
+ position: relative;
210
+ display: flex;
211
+ flex-direction: column;
212
+ align-items: center;
213
+ justify-content: center;
214
+ width: 100%;
215
+ min-height: 140rpx;
216
+ border: 4rpx dashed;
217
+ border-color: v-bind('config.border.color');
218
+ border-radius: v-bind('config.borderRadius.semicircle');
219
+ background-color: v-bind('config.VUploadFileButton.backgroundColor');
220
+ color: v-bind('config.fontColor.mainText');
221
+ cursor: pointer;
222
+ transition: all 0.3s;
223
+
224
+ &:hover {
225
+ border-color: v-bind('config.border.default');
226
+ background-color: v-bind('config.VUploadFileButton.backgroundColor');
227
+ color: v-bind('config.fontColor.default');
228
+ }
229
+
230
+ &:disabled {
231
+ cursor: not-allowed;
232
+ opacity: 0.6;
233
+ }
234
+
235
+ &.uploading {
236
+ border-color: v-bind('config.border.default');
237
+ background-color: v-bind('config.VUploadFileButton.backgroundColor');
238
+ color: v-bind('config.fontColor.default');
239
+ }
240
+
241
+ .upload-icon {
242
+ margin-bottom: 8rpx;
243
+ .icon-upload {
244
+ font-size: v-bind('config.fontSize.mediumText');
245
+ }
246
+ }
247
+
248
+ .upload-text {
249
+ font-size: v-bind('config.fontSize.mediumText');
250
+ }
251
+
252
+ .upload-progress {
253
+ width: 100%;
254
+ text-align: center;
255
+ }
256
+
257
+ .progress-bar {
258
+ width: 100%;
259
+ height: 16rpx;
260
+ background-color: v-bind('config.VUploadFileButton.backgroundColor');
261
+ border-radius: 4rpx;
262
+ overflow: hidden;
263
+ margin-bottom: 8rpx;
264
+ }
265
+
266
+ .progress-bar-filled {
267
+ height: 100%;
268
+ background-color: v-bind('config.backgroundColor.default');
269
+ width: 0;
270
+ transition: width 0.3s;
271
+ }
272
+
273
+ .progress-text {
274
+ font-size: v-bind('config.fontSize.mediumText');
275
+ color: v-bind('config.fontColor.default');
276
+ }
277
+
278
+ .selected-file {
279
+ width: 100%;
280
+ text-align: center;
281
+ }
282
+
283
+ .file-name {
284
+ font-size: v-bind('config.fontSize.mediumText');
285
+ margin-bottom: 6rpx;
286
+ white-space: nowrap;
287
+ overflow: hidden;
288
+ text-overflow: ellipsis;
289
+ }
290
+
291
+ .file-info {
292
+ font-size: v-bind('config.fontSize.mediumText');
293
+ color: v-bind('config.fontColor.info');
294
+ }
295
+ }
296
+ </style>