v-uni-app-ui 1.0.0 → 1.0.4

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 (86) hide show
  1. package/README.md +147 -0
  2. package/components/config/css/basic.scss +19 -0
  3. package/components/config/interface/basic-type.js +16 -0
  4. package/components/config/interface/components-interface.ts +0 -0
  5. package/components/config/interface/monitor/components/input-monitor.js +0 -0
  6. package/components/config/interface/monitor/property-monitor.ts +136 -0
  7. package/components/config/interface/props/basic-props.ts +88 -0
  8. package/components/config/interface/props/components/button-props.ts +85 -0
  9. package/components/config/interface/props/components/input-props.ts +69 -0
  10. package/components/config/interface/props/props-tools.ts +64 -0
  11. package/components/config/style/basic.js +346 -0
  12. package/components/config/style/component-registry.js +142 -0
  13. package/components/config/style/components/button-style.js +160 -0
  14. package/components/config/style/components/input-style.js +98 -0
  15. package/components/config/style/components-style.js +622 -0
  16. package/components/config/style/property-mapper.js +377 -0
  17. package/components/config/style/pseudo-processor.js +213 -0
  18. package/components/config.js +123 -0
  19. package/components/icon/iconfont.css +87 -0
  20. package/components/icon/iconfont.js +1 -0
  21. package/components/icon/iconfont.json +135 -0
  22. package/components/icon/iconfont.ttf +0 -0
  23. package/components/icon/iconfont.woff +0 -0
  24. package/components/icon/iconfont.woff2 +0 -0
  25. package/components/layout/v-card/v-card.vue +108 -0
  26. package/components/layout/v-grid/v-grid.vue +162 -0
  27. package/components/layout/v-icon-grid/v-icon-grid.vue +195 -0
  28. package/components/layout/v-infinite-scroll/v-infinite-scroll.vue +172 -0
  29. package/components/layout/v-list/v-list.vue +43 -0
  30. package/components/layout/v-row/v-row.vue +142 -0
  31. package/components/layout/v-waterfall/v-waterfall.vue +79 -0
  32. package/components/model/compound/v-checkbox-group/v-checkbox-group.vue +96 -0
  33. package/components/model/compound/v-console/v-console.js +20 -0
  34. package/components/model/compound/v-console/v-console.vue +299 -0
  35. package/components/model/compound/v-date-time/v-date-time.vue +261 -0
  36. package/components/model/compound/v-dialog/v-dialog.vue +178 -0
  37. package/components/model/compound/v-drum-select-picker/v-drum-select-picker.vue +83 -0
  38. package/components/model/compound/v-form/v-form.vue +226 -0
  39. package/components/model/compound/v-form-item/v-form-item.vue +255 -0
  40. package/components/model/compound/v-image/v-image.vue +357 -0
  41. package/components/model/compound/v-input-desensitize/v-input-desensitize.vue +101 -0
  42. package/components/model/compound/v-page/v-page.vue +11 -0
  43. package/components/model/compound/v-pages/v-pages.vue +141 -0
  44. package/components/model/compound/v-picker-list/v-picker-list.vue +109 -0
  45. package/components/model/compound/v-popup/v-popup.vue +151 -0
  46. package/components/model/compound/v-radio-group/v-radio-group.vue +86 -0
  47. package/components/model/compound/v-select-picker/v-select-picker.vue +202 -0
  48. package/components/model/compound/v-series-picker-list/v-series-picker-list.vue +221 -0
  49. package/components/model/compound/v-series-select-picker/v-series-select-picker.vue +203 -0
  50. package/components/model/compound/v-switch/v-switch.vue +136 -0
  51. package/components/model/compound/v-tabs-page/v-tabs-page.vue +138 -0
  52. package/components/model/native/v-badge/v-badge.vue +143 -0
  53. package/components/model/native/v-button/v-button.vue +81 -0
  54. package/components/model/native/v-carousel/v-carousel.vue +138 -0
  55. package/components/model/native/v-checkbox/v-checkbox.vue +215 -0
  56. package/components/model/native/v-collapse/v-collapse.vue +190 -0
  57. package/components/model/native/v-header-navigation-bar/v-header-navigation-bar.vue +92 -0
  58. package/components/model/native/v-input/v-input.vue +163 -0
  59. package/components/model/native/v-input-code/v-input-code.vue +146 -0
  60. package/components/model/native/v-loading/v-loading.vue +206 -0
  61. package/components/model/native/v-menu/v-menu.vue +222 -0
  62. package/components/model/native/v-menu-slide/v-menu-slide.vue +364 -0
  63. package/components/model/native/v-min-loading/v-min-loading.vue +80 -0
  64. package/components/model/native/v-null/v-null.vue +97 -0
  65. package/components/model/native/v-overlay/v-overlay.vue +96 -0
  66. package/components/model/native/v-pull-up-refresh/v-pull-up-refresh.vue +157 -0
  67. package/components/model/native/v-radio/v-radio.vue +138 -0
  68. package/components/model/native/v-scroll-list/v-scroll-list.vue +169 -0
  69. package/components/model/native/v-steps/v-steps.vue +253 -0
  70. package/components/model/native/v-table/v-table.vue +203 -0
  71. package/components/model/native/v-tabs/v-tabs.vue +235 -0
  72. package/components/model/native/v-tag/v-tag.vue +206 -0
  73. package/components/model/native/v-text/v-text.vue +187 -0
  74. package/components/model/native/v-textarea/v-textarea.vue +178 -0
  75. package/components/model/native/v-title/v-title.vue +91 -0
  76. package/components/model/native/v-toast/info.png +0 -0
  77. package/components/model/native/v-toast/success.png +0 -0
  78. package/components/model/native/v-toast/v-toast.vue +198 -0
  79. package/components/model/native/v-toast/warn.png +0 -0
  80. package/components/model/native/v-upload-file-button/v-upload-file-button.vue +296 -0
  81. package/components/model/native/v-video/v-video.vue +175 -0
  82. package/components/model/native/v-window/v-window.vue +158 -0
  83. package/components/utils/event-modifiers.ts +139 -0
  84. package/components/utils/validator.ts +451 -0
  85. package/index.js +372 -0
  86. package/package.json +25 -93
@@ -0,0 +1,255 @@
1
+ <template>
2
+ <view :class="['form-item', `form-item--border--${borderModel}`, { 'form-item--hint--model': !hintModel }]">
3
+ <view class="title">
4
+ <view class="required" v-if="requiredPosition == 'left'">
5
+ <v-text v-if="isRequired">*</v-text>
6
+ </view>
7
+ <view v-if="label" class="label">
8
+ {{ label }}
9
+ </view>
10
+ <view class="required" v-if="requiredPosition == 'right'">
11
+ <v-text v-if="isRequired">*</v-text>
12
+ </view>
13
+ </view>
14
+ <view class="control">
15
+ <slot></slot>
16
+ <view class="error-message" v-if="hintModel">
17
+ <v-text v-if="error" type="danger">{{ error }}</v-text>
18
+ </view>
19
+ </view>
20
+ <v-toast ref="toast"></v-toast>
21
+ </view>
22
+ </template>
23
+
24
+ <script lang="ts" setup>
25
+ import { ref, inject, onMounted, onUnmounted } from 'vue';
26
+ import { getCurrentInstance } from 'vue';
27
+
28
+ const { proxy } = getCurrentInstance();
29
+
30
+ const props = defineProps({
31
+ label: {
32
+ type: String,
33
+ default: ''
34
+ },
35
+ name: {
36
+ type: String,
37
+ required: true
38
+ },
39
+ requiredPosition: {
40
+ type: String,
41
+ default: 'right'
42
+ },
43
+ hintModel: {
44
+ type: Boolean,
45
+ default: true
46
+ },
47
+ borderModel: {
48
+ type: String,
49
+ default: 'bottom',
50
+ validator: (v) => ['all', 'none', 'bottom', 'top', 'left', 'right', 'ends', 'up-down'].includes(v)
51
+ }
52
+ });
53
+
54
+ const toast = ref(null);
55
+ const error = ref('');
56
+ const isRequired = ref(false);
57
+ const config = inject('config');
58
+ const formContext = inject('form', {
59
+ rules: {},
60
+ formData: {},
61
+ register: (name, validate) => {
62
+ console.log(name, validate);
63
+ },
64
+ unregister: (name) => {
65
+ console.log(name);
66
+ },
67
+ formHasError:{
68
+ value:false
69
+ },
70
+ });
71
+
72
+ const validate = async () => {
73
+ const rules = formContext.rules[props.name] || [];
74
+ const value = formContext.formData[props.name];
75
+ const formData = formContext.formData;
76
+
77
+ for (const rule of rules) {
78
+ // 必填校验
79
+ if (rule.required) {
80
+ let isEmpty = false;
81
+ if (value === null || value === undefined || value === '') {
82
+ isEmpty = true;
83
+ } else if (typeof value === 'string' || typeof value === 'number') {
84
+ if (value != null && value !== undefined) {
85
+ isEmpty = !value.toString().trim();
86
+ }
87
+ } else if (Array.isArray(value)) {
88
+ isEmpty = value.length === 0;
89
+ } else if (typeof value === 'object') {
90
+ isEmpty = Object.keys(value).length === 0;
91
+ }
92
+ if (isEmpty) {
93
+ hintModelMessage(rule.message || `${props.label}不能为空`);
94
+ return false;
95
+ }
96
+ }
97
+
98
+ // 类型转换校验(非空后执行)
99
+ let processedValue = value;
100
+ if ((rule.type === 'number' || rule.type === 'integer') && value !== null && value !== undefined) {
101
+ processedValue = Number(value);
102
+ if (isNaN(processedValue)) {
103
+ hintModelMessage(rule.message || `${props.label}必须为数字`);
104
+ return false;
105
+ }
106
+ if (rule.type === 'integer' && !Number.isInteger(processedValue)) {
107
+ hintModelMessage(rule.message || `${props.label}必须为整数`);
108
+ return false;
109
+ }
110
+ }
111
+
112
+ // 格式校验
113
+ if (value !== null && value !== undefined && value !== '') {
114
+ if (rule.pattern && !rule.pattern.test(value)) {
115
+ hintModelMessage(rule.message || `${props.label}格式错误`);
116
+ return false;
117
+ }
118
+
119
+ // 内置类型校验
120
+ if (rule.type === 'email' && !/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/.test(value)) {
121
+ hintModelMessage(rule.message || `请输入有效的邮箱地址`);
122
+ return false;
123
+ }
124
+
125
+ if (rule.type === 'mobile' && !/^1[3-9]\d{9}$/.test(value)) {
126
+ hintModelMessage(rule.message || `请输入有效的手机号码`);
127
+ return false;
128
+ }
129
+
130
+ // 长度校验
131
+ if (rule.minLength !== undefined && value.length < rule.minLength) {
132
+ hintModelMessage(rule.message || `${props.label}至少需要${rule.minLength}个字符`);
133
+ return false;
134
+ }
135
+
136
+ if (rule.maxLength !== undefined && value.length > rule.maxLength) {
137
+ hintModelMessage(rule.message || `${props.label}不能超过${rule.maxLength}个字符`);
138
+ return false;
139
+ }
140
+
141
+ // 数值范围校验
142
+ if (typeof rule.min === 'number' && processedValue < rule.min) {
143
+ hintModelMessage(rule.message || `${props.label}不能小于${rule.min}`);
144
+ return false;
145
+ }
146
+
147
+ if (typeof rule.max === 'number' && processedValue > rule.max) {
148
+ hintModelMessage(rule.message || `${props.label}不能大于${rule.max}`);
149
+ return false;
150
+ }
151
+
152
+ // 关联字段校验(如密码确认)
153
+ if (rule.compareWith) {
154
+ const compareValue = formData[rule.compareWith];
155
+ if (value !== compareValue) {
156
+ hintModelMessage(rule.message || `${props.label}必须与${rule.compareWith}保持一致`);
157
+ return false;
158
+ }
159
+ }
160
+
161
+ // 自定义校验函数
162
+ if (rule.validator) {
163
+ const result = await rule.validator(value, formData);
164
+ if (!result) {
165
+ hintModelMessage(rule.message || `${props.label}验证未通过`);
166
+ return false;
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ error.value = '';
173
+ return true;
174
+ };
175
+
176
+ const hintModelMessage = (message: string) => {
177
+ if (props.hintModel) {
178
+ error.value = message;
179
+ } else {
180
+ if (formContext && formContext.formHasError.value) {
181
+ return;
182
+ }
183
+ if (toast.value) {
184
+ toast.value.show({
185
+ message: message,
186
+ type: 'error',
187
+ duration: 3000
188
+ });
189
+ formContext.formHasError.value = true;
190
+ }
191
+ }
192
+ };
193
+
194
+ const resetField = () => {
195
+ error.value = '';
196
+ formContext.formData[props.name] = null;
197
+ };
198
+
199
+ // 注册到父表单
200
+ onMounted(() => {
201
+ if (formContext && formContext.register) {
202
+ formContext.register(props.name, { validate, resetField });
203
+ if (formContext.rules && formContext.rules[props.name]) {
204
+ isRequired.value = formContext.rules[props.name].some((r) => r.required);
205
+ }
206
+ }
207
+ });
208
+
209
+ onUnmounted(() => {
210
+ if (formContext && formContext.unregister) {
211
+ formContext.unregister(props.name);
212
+ }
213
+ });
214
+ </script>
215
+ <style lang="scss" scoped>
216
+ .form-item {
217
+ &--hint--model {
218
+ padding-bottom: 20rpx;
219
+ }
220
+
221
+ &--border--all {
222
+ border: 1rpx solid v-bind('config.VFormItem.borderColor');
223
+ }
224
+
225
+ &--border--none {
226
+ border: none;
227
+ }
228
+
229
+ &--border--bottom {
230
+ border-bottom: 1rpx solid v-bind('config.VFormItem.borderColor');
231
+ }
232
+
233
+ &--border--top {
234
+ border-top: 1rpx solid v-bind('config.VFormItem.borderColor');
235
+ }
236
+
237
+ &--border--left {
238
+ border-left: 1rpx solid v-bind('config.VFormItem.borderColor');
239
+ }
240
+
241
+ &--border--right {
242
+ border-right: 1rpx solid v-bind('config.VFormItem.borderColor');
243
+ }
244
+
245
+ &--border--ends {
246
+ border-left: 1rpx solid v-bind('config.VFormItem.borderColor');
247
+ border-right: 1rpx solid v-bind('config.VFormItem.borderColor');
248
+ }
249
+
250
+ &--border--up-down {
251
+ border-top: 1rpx solid v-bind('config.VFormItem.borderColor');
252
+ border-bottom: 1rpx solid v-bind('config.VFormItem.borderColor');
253
+ }
254
+ }
255
+ </style>
@@ -0,0 +1,357 @@
1
+ <template>
2
+ <view class="v-image-container" @click="handleClick">
3
+ <!-- 根据环境选择不同的标签 -->
4
+ <template v-if="!isError">
5
+ <image
6
+ v-if="isAppPlus"
7
+ ref="imgElement"
8
+ :src="finalSrc"
9
+ :class="['v-image', `v-image--${model}`, { 'v-image--loading': isLoading, 'v-image--disabled': disabled }]"
10
+ @click="handlePreview"
11
+ @load="handleLoad"
12
+ @error="handleError"
13
+ />
14
+ <img
15
+ v-else
16
+ ref="imgElement"
17
+ :src="finalSrc"
18
+ :class="['v-image', `v-image--${model}`, { 'v-image--loading': isLoading, 'v-image--disabled': disabled }]"
19
+ @click="handlePreview"
20
+ @load="handleLoad"
21
+ @error="handleError"
22
+ />
23
+ </template>
24
+
25
+ <slot name="loading" v-if="isLoading">
26
+ <view class="v-image-loading">
27
+ <v-loading text="加载中……" show />
28
+ </view>
29
+ </slot>
30
+ <slot name="error" v-if="isError">
31
+ <view class="v-image-error">
32
+ <text class="error-text">加载失败</text>
33
+ </view>
34
+ </slot>
35
+ </view>
36
+ <v-overlay v-model:value="showPreview" @click="closePreview" v-if="showPreview">
37
+ <image v-if="isAppPlus" :src="finalPreviewSrc" :class="['v-image-preview__img', `v-image-preview__img--${previewModel}`]" @click.stop />
38
+ <img v-else :src="finalPreviewSrc" :class="['v-image-preview__img', `v-image-preview__img--${previewModel}`]" @click.stop />
39
+ </v-overlay>
40
+ </template>
41
+
42
+ <script lang="ts" setup>
43
+ import { ref, watch, onMounted, onUnmounted, inject, computed } from 'vue';
44
+ const props = defineProps({
45
+ src: {
46
+ type: String,
47
+ default: '',
48
+ required: true
49
+ },
50
+ width: {
51
+ type: String,
52
+ default: '100%'
53
+ },
54
+ height: {
55
+ type: String,
56
+ default: '100%'
57
+ },
58
+ model: {
59
+ type: String,
60
+ default: 'square',
61
+ validator: (val: string) => ['circle', 'rounded', 'square'].includes(val)
62
+ },
63
+ borderRadius: {
64
+ type: String,
65
+ default: '10rpx'
66
+ },
67
+ previewWidth: {
68
+ type: String,
69
+ default: 'auto'
70
+ },
71
+ previewHeight: {
72
+ type: String,
73
+ default: 'auto'
74
+ },
75
+ enablePreview: {
76
+ type: Boolean,
77
+ default: false
78
+ },
79
+ previewModel: {
80
+ type: String,
81
+ default: 'square',
82
+ validator: (val: string) => ['circle', 'rounded', 'square'].includes(val)
83
+ },
84
+ previewBorderRadius: {
85
+ type: String,
86
+ default: '10rpx'
87
+ },
88
+ loading: {
89
+ type: Boolean,
90
+ default: false
91
+ },
92
+ disabled: {
93
+ type: Boolean,
94
+ default: false
95
+ },
96
+ lazy: {
97
+ type: Boolean,
98
+ default: false
99
+ },
100
+ threshold: {
101
+ type: Number,
102
+ default: 0.1
103
+ },
104
+ delay: {
105
+ type: Number,
106
+ default: 800
107
+ },
108
+ isLoading: {
109
+ type: Boolean,
110
+ default: false
111
+ },
112
+ retryCount: {
113
+ type: Number,
114
+ default: 3
115
+ },
116
+ fit: {
117
+ type: String,
118
+ default: 'cover'
119
+ },
120
+ previewFit: {
121
+ type: String,
122
+ default: ''
123
+ }
124
+ });
125
+
126
+ const emit = defineEmits(['update:enablePreview', 'update:loading', 'handleLoading', 'handError', 'click']);
127
+
128
+ const config = inject<any>('config');
129
+ const showPreview = ref(false);
130
+ const isLoading = ref(props.isLoading);
131
+ const isError = ref(false);
132
+ const imgElement = ref<HTMLElement | null>(null);
133
+ const displaySrc = ref<string | null>(null);
134
+
135
+ let lazyLoadTimer: any = null;
136
+ let scrollHandler: any = null;
137
+ const retryCount = ref(0);
138
+ const maxRetryCount = ref(props.retryCount);
139
+
140
+ // 判断是否是 App 环境
141
+ const isAppPlus = ref(false);
142
+
143
+ // 判断是否是 HTTP 或 HTTPS 链接
144
+ const isHttpOrHttps = (url: string) => {
145
+ return /^https?:\/\//.test(url);
146
+ };
147
+ // 动态计算图片路径
148
+ const finalSrc = computed(() => {
149
+ if (!props.src) return '';
150
+ return isHttpOrHttps(props.src) ? props.src : config.VImage.prefix + props.src;
151
+ });
152
+
153
+ const finalPreviewSrc = computed(() => {
154
+ if (!props.src) return '';
155
+ return isHttpOrHttps(props.src) ? props.src : config.VImage.prefix + props.src;
156
+ });
157
+
158
+ onMounted(() => {
159
+ // 判断当前环境
160
+ if (process.env.VUE_APP_PLATFORM === 'app-plus') {
161
+ isAppPlus.value = true;
162
+ } else {
163
+ isAppPlus.value = false;
164
+ }
165
+
166
+ if (props.lazy && props.src && window) {
167
+ window.addEventListener('scroll', handleScroll);
168
+ handleScroll();
169
+ } else {
170
+ displaySrc.value = finalSrc.value;
171
+ }
172
+ });
173
+
174
+ const handleScroll = () => {
175
+ if (!props.lazy || !props.src || !imgElement.value) return;
176
+
177
+ const rect = imgElement.value.getBoundingClientRect();
178
+ const isVisible = rect.top < (window.innerHeight || document.documentElement.clientHeight) && rect.bottom > 0;
179
+
180
+ if (isVisible) {
181
+ loadImage();
182
+ }
183
+ };
184
+
185
+ const loadImage = () => {
186
+ clearTimeout(lazyLoadTimer);
187
+ lazyLoadTimer = setTimeout(() => {
188
+ displaySrc.value = finalSrc.value;
189
+ if (displaySrc.value && displaySrc.value.trim()) {
190
+ isLoading.value = false
191
+ isError.value = false;
192
+ }
193
+ }, props.delay);
194
+ };
195
+
196
+ const handleLoad = () => {
197
+ isLoading.value = false;
198
+ emit('handleLoading');
199
+ retryCount.value = 0; // 成功后重置重试计数
200
+ };
201
+
202
+ const handleRetry = () => {
203
+ if (retryCount.value < maxRetryCount.value) {
204
+ retryCount.value++;
205
+ displaySrc.value = null;
206
+ isLoading.value = true;
207
+ isError.value = false;
208
+ loadImage();
209
+ emit('handError', retryCount.value);
210
+ } else {
211
+ emit('handError', retryCount.value);
212
+ }
213
+ };
214
+
215
+ const handleError = () => {
216
+ isLoading.value = false;
217
+ isError.value = true;
218
+ handleRetry();
219
+ };
220
+
221
+ const handlePreview = () => {
222
+ if (props.enablePreview && props.src && !props.disabled) {
223
+ showPreview.value = true;
224
+ }
225
+ };
226
+
227
+ const handleClick = () => {
228
+ emit('click');
229
+ };
230
+
231
+ const closePreview = () => {
232
+ showPreview.value = false;
233
+ };
234
+
235
+ watch(
236
+ () => showPreview.value,
237
+ (newValue) => {
238
+ emit('update:enablePreview', newValue);
239
+ }
240
+ );
241
+
242
+ watch(
243
+ () => isLoading.value,
244
+ () => {
245
+ emit('update:loading', isLoading.value);
246
+ }
247
+ );
248
+
249
+ watch(
250
+ () => props.src,
251
+ (newSrc) => {
252
+ if (!props.lazy) {
253
+ displaySrc.value = finalSrc.value;
254
+ if (newSrc) {
255
+ isLoading.value = true;
256
+ isError.value = false;
257
+ }
258
+ } else {
259
+ if (imgElement.value && window) {
260
+ loadImage();
261
+ }
262
+ }
263
+ }
264
+ );
265
+
266
+ onUnmounted(() => {
267
+ clearTimeout(lazyLoadTimer);
268
+ if (window) {
269
+ window.removeEventListener('scroll', handleScroll);
270
+ }
271
+ });
272
+ </script>
273
+
274
+ <style lang="scss" scoped>
275
+ .v-image-container {
276
+ width: 100%;
277
+ height: 100%;
278
+ display: flex;
279
+ justify-content: center;
280
+ align-items: center;
281
+ position: relative;
282
+ }
283
+
284
+ .v-image {
285
+ max-width: 100%;
286
+ max-height: 100%;
287
+ width: v-bind('props.width');
288
+ height: v-bind('props.height');
289
+ object-fit: v-bind('fit');
290
+
291
+ &--circle {
292
+ border-radius: 50%;
293
+ }
294
+
295
+ &--rounded {
296
+ border-radius: v-bind('props.borderRadius');
297
+ }
298
+
299
+ &--square {
300
+ border-radius: 0;
301
+ }
302
+
303
+ &--loading,
304
+ &--disabled {
305
+ opacity: v-bind('config.opacity.disabled');
306
+ }
307
+ }
308
+
309
+ .v-image-preview__img {
310
+ max-width: 90%;
311
+ max-height: 90%;
312
+ object-fit: v-bind('previewFit');
313
+
314
+ &--circle {
315
+ border-radius: 50%;
316
+ }
317
+
318
+ &--rounded {
319
+ border-radius: v-bind('props.previewBorderRadius');
320
+ }
321
+
322
+ &--square {
323
+ border-radius: 0;
324
+ }
325
+ }
326
+
327
+ .v-image-loading {
328
+ position: absolute;
329
+ top: 0;
330
+ left: 0;
331
+ width: 100%;
332
+ height: 100%;
333
+ display: flex;
334
+ justify-content: center;
335
+ align-items: center;
336
+ z-index: 1;
337
+ }
338
+
339
+ .v-image-error {
340
+ position: absolute;
341
+ top: 0;
342
+ left: 0;
343
+ width: 100%;
344
+ height: 100%;
345
+ display: flex;
346
+ flex-direction: column;
347
+ justify-content: center;
348
+ align-items: center;
349
+ z-index: 1;
350
+
351
+ .error-text {
352
+ font-size: v-bind('config.fontSize.smallText');
353
+ color: v-bind('config.fontColor.subTitle');
354
+ margin-top: 12rpx;
355
+ }
356
+ }
357
+ </style>
@@ -0,0 +1,101 @@
1
+ <template>
2
+ <v-input
3
+ :value="displayValue"
4
+ v-bind="$props"
5
+ class="desensitize-input"
6
+ @update:value="onInput"
7
+ @focus="emit('focus', $event)"
8
+ @blur="emit('blur', $event)"
9
+ @confirm="emit('confirm', $event)"
10
+ >
11
+ <template #left><slot name="left" /></template>
12
+ <template #right><slot name="right" /></template>
13
+ </v-input>
14
+ </template>
15
+
16
+ <script setup lang="ts">
17
+ import { ref, watch, computed } from 'vue';
18
+
19
+ type BuiltInRule = 'name' | 'phone' | 'idcard';
20
+ type CustomRule = (raw: string) => string;
21
+ type Rule = BuiltInRule | CustomRule;
22
+
23
+ const props = defineProps({
24
+ modelValue: { type: String, default: '' },
25
+ size: { type: String, default: 'medium' },
26
+ placeholder: { type: [String, Array], default: '' },
27
+ maxlength: { type: Number, default: null },
28
+ disabled: { type: Boolean, default: false },
29
+ showCounter: { type: Boolean, default: false },
30
+ borderModel: { type: String, default: 'all' },
31
+ combinationConfig: { type: Object, default: () => ({ isShow: false }) },
32
+ autoFocus: { type: Boolean, default: false },
33
+ realType: { type: String, default: 'text' },
34
+ desensitize: { type: Boolean, default: true },
35
+ rule: { type: [String, Function] as () => Rule, default: 'phone' }
36
+ });
37
+
38
+ const emit = defineEmits(['update:modelValue', 'focus', 'blur', 'confirm']);
39
+
40
+ /* ========= 内置规则 ========= */
41
+ const builtInRules: Record<BuiltInRule, (v: string) => string> = {
42
+ /* 姓名:保留首字母和末字母,中间全星号 */
43
+ name: (v) => (v.length <= 2 ? v.slice(-1).padStart(v.length, '*') : v[0] + '*'.repeat(v.length - 2) + v.slice(-1)),
44
+
45
+ /* 手机:保留前三后四,中间星号 */
46
+ phone: (v) => {
47
+ const digits = v.replace(/\D/g, '');
48
+ if (digits.length < 7) return v;
49
+
50
+ let result = '';
51
+ let digitIndex = 0;
52
+
53
+ for (let i = 0; i < v.length; i++) {
54
+ if (/\d/.test(v[i])) {
55
+ if (digitIndex < 3) {
56
+ result += v[i];
57
+ } else if (digitIndex >= digits.length - 4) {
58
+ result += v[i];
59
+ } else {
60
+ result += '*';
61
+ }
62
+ digitIndex++;
63
+ } else {
64
+ result += v[i];
65
+ }
66
+ }
67
+ return result;
68
+ },
69
+
70
+ /* 身份证:保留前 4 后 4,中间全星号 */
71
+ idcard: (v) => {
72
+ const len = v.length;
73
+ if (len < 8) return v;
74
+ return v.slice(0, 4) + '*'.repeat(len - 8) + v.slice(-4);
75
+ }
76
+ };
77
+
78
+ /* ========= 脱敏显示值 ========= */
79
+ const displayValue = computed(() => {
80
+ if (!props.desensitize) return props.modelValue;
81
+
82
+ const rule = props.rule;
83
+ if (typeof rule === 'function') {
84
+ return rule(props.modelValue);
85
+ }
86
+ return builtInRules[rule as BuiltInRule](props.modelValue);
87
+ });
88
+
89
+ /* ========= 输入事件:直接传递原始值 ========= */
90
+ const onInput = (val: string) => {
91
+ // 注意:这里传入的val是脱敏后的显示值,我们需要直接更新原始值
92
+ emit('update:modelValue', val);
93
+ };
94
+ </script>
95
+
96
+ <style scoped>
97
+ .desensitize-input :deep(input) {
98
+ font-family: monospace;
99
+ line-height: 1.5;
100
+ }
101
+ </style>
@@ -0,0 +1,11 @@
1
+ <template>
2
+ <view>
3
+
4
+ </view>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ </script>
9
+
10
+ <style lang="scss" scoped>
11
+ </style>