@yxhl/specter-pui-vtk 1.0.91 → 1.0.93

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yxhl/specter-pui-vtk",
3
- "version": "1.0.91",
3
+ "version": "1.0.93",
4
4
  "description": "雅心互联 Vue 3 + Vuetify3 组件库",
5
5
  "type": "module",
6
6
  "main": "./dist/specter-pui.umd.js",
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="vtk-date-selector">
2
+ <div class="vtk-date-selector" :style="rootStyle">
3
3
  <VMenu
4
4
  v-if="!inline"
5
5
  v-model="showPicker"
@@ -26,7 +26,7 @@
26
26
  </VTextField>
27
27
  </div>
28
28
  </template>
29
- <div class="date-picker-dropdown" @click.stop @mousedown.stop>
29
+ <div class="date-picker-dropdown" :style="popupStyle" @click.stop @mousedown.stop>
30
30
  <!-- 头部控制区(非日期/月份范围选择模式时显示) -->
31
31
  <div v-if="!(mode === 'range' && (displayMode === 'day' || displayMode === 'month'))" class="picker-header">
32
32
  <button class="nav-btn" @click="prevPeriod">‹</button>
@@ -222,7 +222,7 @@
222
222
  </div>
223
223
  </div>
224
224
  </VMenu>
225
- <div v-if="inline" class="date-picker-dropdown inline-mode" @click.stop @mousedown.stop>
225
+ <div v-if="inline" class="date-picker-dropdown inline-mode" :style="popupStyle" @click.stop @mousedown.stop>
226
226
  <!-- 头部控制区(非日期/月份范围选择模式时显示) -->
227
227
  <div v-if="!(mode === 'range' && (displayMode === 'day' || displayMode === 'month'))" class="picker-header">
228
228
  <button class="nav-btn" @click="prevPeriod">‹</button>
@@ -449,10 +449,20 @@ const props = defineProps({
449
449
  validator: (value) => ['single', 'range'].includes(value)
450
450
  },
451
451
  // 占位符
452
- placeholder: {
453
- type: String,
454
- default: '请选择日期'
455
- },
452
+ placeholder: {
453
+ type: String,
454
+ default: '请选择日期'
455
+ },
456
+ // 输入框宽度,数字按 px 处理,也支持 100%、12rem 等 CSS 宽度
457
+ width: {
458
+ type: [String, Number],
459
+ default: ''
460
+ },
461
+ // 日期弹层宽度,数字按 px 处理,也支持 100%、24rem 等 CSS 宽度
462
+ popupWidth: {
463
+ type: [String, Number],
464
+ default: ''
465
+ },
456
466
  // 禁用状态
457
467
  disabled: {
458
468
  type: Boolean,
@@ -533,12 +543,29 @@ const YEAR_PANEL_CENTER_OFFSET = 4;
533
543
  const weekDays = ['日', '一', '二', '三', '四', '五', '六'];
534
544
  const monthList = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
535
545
 
536
- // 显示值
537
- const menuAttach = computed(() => (props.noTeleport ? true : false));
538
-
539
- const menuLocation = computed(() => props.placement === 'right' ? 'bottom end' : 'bottom start');
540
-
541
- const menuOrigin = computed(() => props.placement === 'right' ? 'top end' : 'top start');
546
+ // 显示值
547
+ const menuAttach = computed(() => (props.noTeleport ? true : false));
548
+
549
+ const menuLocation = computed(() => props.placement === 'right' ? 'bottom end' : 'bottom start');
550
+
551
+ const menuOrigin = computed(() => props.placement === 'right' ? 'top end' : 'top start');
552
+
553
+ const normalizeCssSize = (value) => {
554
+ if (value === null || value === undefined || value === '') return '';
555
+ if (typeof value === 'number') return `${value}px`;
556
+ const nextValue = String(value).trim();
557
+ return /^\d+(\.\d+)?$/.test(nextValue) ? `${nextValue}px` : nextValue;
558
+ };
559
+
560
+ const rootStyle = computed(() => {
561
+ const width = normalizeCssSize(props.width);
562
+ return width ? { width } : {};
563
+ });
564
+
565
+ const popupStyle = computed(() => {
566
+ const width = normalizeCssSize(props.popupWidth);
567
+ return width ? { width } : {};
568
+ });
542
569
 
543
570
  const displayValue = computed(() => {
544
571
  if (!selectedValue.value) return '';
@@ -1206,10 +1233,13 @@ watch(() => props.modelValue, (newValue) => {
1206
1233
  color: rgb(var(--v-theme-on-surface), 0.87);
1207
1234
  }
1208
1235
 
1209
- .date-picker-dropdown {
1210
- background: rgb(var(--v-theme-surface));
1211
- border-radius: 8px;
1212
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
1236
+ .date-picker-dropdown {
1237
+ box-sizing: border-box;
1238
+ max-width: calc(100vw - 24px);
1239
+ overflow-x: auto;
1240
+ background: rgb(var(--v-theme-surface));
1241
+ border-radius: 8px;
1242
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
1213
1243
  border: 1px solid rgb(var(--v-theme-on-surface), 0.12);
1214
1244
  }
1215
1245
 
@@ -1,20 +1,18 @@
1
- <template>
2
- <v-card class="pa-4 mx-auto text-center pt-10" color="transparent" elevation="0" style="border: none !important;box-shadow: none !important;">
3
- <v-img contain width="120" class="mx-auto" src="@/assets/img/empty.svg" alt="" />
4
- <p class="text-grey mt-1">{{ text || "暂无数据" }}</p>
5
- </v-card>
6
- </template>
7
-
8
- <script setup>
9
- import { useAttrs } from 'vue';
10
-
11
- const attrs = useAttrs();
12
-
13
- // 定义 props
14
- const props = defineProps({
15
- text: {
16
- type: String,
17
- default: null,
1
+ <template>
2
+ <v-card class="pa-4 mx-auto text-center pt-10" color="transparent" elevation="0" style="border: none !important;box-shadow: none !important;">
3
+ <v-img contain width="120" class="mx-auto" :src="emptyImg" alt="" />
4
+ <p class="text-grey mt-1">{{ text || "暂无数据" }}</p>
5
+ </v-card>
6
+ </template>
7
+
8
+ <script setup>
9
+ import emptyImg from '@/assets/img/empty.svg';
10
+
11
+ // 定义 props
12
+ defineProps({
13
+ text: {
14
+ type: String,
15
+ default: null,
18
16
  },
19
17
  items: {
20
18
  type: Array,
@@ -23,4 +21,4 @@ const props = defineProps({
23
21
  });
24
22
  </script>
25
23
 
26
- <style lang="scss" scoped></style>
24
+ <style lang="scss" scoped></style>
@@ -26,7 +26,7 @@
26
26
  :key="file.uid"
27
27
  class="vtk-upload__picture-card-item"
28
28
  >
29
- <v-img :src="file.url || file.preview" cover class="fill-height" />
29
+ <v-img :key="file.url || file.preview || file.uid" :src="file.url || file.preview" cover class="fill-height" />
30
30
  <div class="vtk-upload__picture-card-mask">
31
31
  <VBtn icon size="x-small" variant="text" color="white" title="查看" @click.stop="handlePreview(file)">
32
32
  <VIcon>mdi-eye</VIcon>
@@ -297,9 +297,10 @@ const previewTranslateX = ref(0);
297
297
  const previewTranslateY = ref(0);
298
298
  const previewDragState = ref(null);
299
299
 
300
- // 内部维护文件列表
301
- const fileList = ref([]);
302
- let isSyncingModel = false;
300
+ // 内部维护文件列表
301
+ const fileList = ref([]);
302
+ let isSyncingModel = false;
303
+ const localPreviewUrls = new Set();
303
304
 
304
305
  watch(
305
306
  () => props.modelValue,
@@ -309,10 +310,11 @@ watch(
309
310
  return;
310
311
  }
311
312
 
312
- // Accept external URL lists or file object lists.
313
- fileList.value = normalizeFileList(val || []);
314
- },
315
- );
313
+ // Accept external URL lists or file object lists.
314
+ revokeAllLocalPreviews();
315
+ fileList.value = normalizeFileList(val || []);
316
+ },
317
+ );
316
318
 
317
319
  /* -------------------- 工具函数 -------------------- */
318
320
  let uidCounter = 0;
@@ -384,7 +386,21 @@ const fileIcon = (file) => {
384
386
  return 'mdi-file-outline';
385
387
  };
386
388
 
387
- const getPreviewSrc = (file) => file?.url || file?.preview || '';
389
+ const getPreviewSrc = (file) => file?.url || file?.preview || '';
390
+
391
+ const revokeLocalPreview = (file) => {
392
+ const preview = file?.preview;
393
+ if (preview && localPreviewUrls.has(preview)) {
394
+ URL.revokeObjectURL(preview);
395
+ localPreviewUrls.delete(preview);
396
+ }
397
+ };
398
+
399
+ const revokeAllLocalPreviews = () => {
400
+ fileList.value.forEach(revokeLocalPreview);
401
+ localPreviewUrls.forEach((url) => URL.revokeObjectURL(url));
402
+ localPreviewUrls.clear();
403
+ };
388
404
 
389
405
  const previewImageList = computed(() => fileList.value.filter((file) => isImage(file) && getPreviewSrc(file)));
390
406
 
@@ -444,12 +460,20 @@ const processFiles = (rawFiles) => {
444
460
 
445
461
  const file = buildFile(raw, 'ready');
446
462
 
447
- // 生成预览
448
- if (isImage(raw)) {
449
- const reader = new FileReader();
450
- reader.onload = (e) => { file.preview = e.target.result; };
451
- reader.readAsDataURL(raw);
452
- }
463
+ // 生成本地预览地址,保证 picture-card 选图后立即回显缩略图。
464
+ if (isImage(raw)) {
465
+ if (typeof URL !== 'undefined' && URL.createObjectURL) {
466
+ file.preview = URL.createObjectURL(raw);
467
+ localPreviewUrls.add(file.preview);
468
+ } else {
469
+ const reader = new FileReader();
470
+ reader.onload = (e) => {
471
+ file.preview = e.target.result;
472
+ emit('change', file, fileList.value);
473
+ };
474
+ reader.readAsDataURL(raw);
475
+ }
476
+ }
453
477
 
454
478
  const beforeHook = props.beforeUpload ? props.beforeUpload(raw) : true;
455
479
  Promise.resolve(beforeHook).then((result) => {
@@ -512,9 +536,12 @@ const uploadFile = (file) => {
512
536
  Request.http(props.action, formData, 'POST', headers).then((res) => {
513
537
  file.status = 'success';
514
538
  file.response = res;
515
- // 兼容 { data: 'url' } 和 { data: { url: '...' } } 两种结构
516
- const url = typeof res?.data === 'string' ? res.data : (res?.data?.url || res?.url || '');
517
- if (url) file.url = url;
539
+ // 兼容 { data: 'url' } 和 { data: { url: '...' } } 两种结构
540
+ const url = typeof res?.data === 'string' ? res.data : (res?.data?.url || res?.url || '');
541
+ if (url) {
542
+ file.url = url;
543
+ revokeLocalPreview(file);
544
+ }
518
545
  console.log('[VtkUpload] res:', res, '| file.url:', file.url, '| fileList:', JSON.parse(JSON.stringify(fileList.value)));
519
546
  syncModel();
520
547
  emit('success', res, file, fileList.value);
@@ -529,10 +556,11 @@ const uploadFile = (file) => {
529
556
 
530
557
  /* -------------------- 移除文件 -------------------- */
531
558
  const handleRemove = (file) => {
532
- const doRemove = () => {
533
- fileList.value = fileList.value.filter((f) => f.uid !== file.uid);
534
- syncModel();
535
- emit('remove', file, fileList.value);
559
+ const doRemove = () => {
560
+ revokeLocalPreview(file);
561
+ fileList.value = fileList.value.filter((f) => f.uid !== file.uid);
562
+ syncModel();
563
+ emit('remove', file, fileList.value);
536
564
  };
537
565
 
538
566
  if (props.beforeRemove) {
@@ -687,9 +715,10 @@ watch(previewVisible, (visible) => {
687
715
  resetPreviewTransform();
688
716
  });
689
717
 
690
- onBeforeUnmount(() => {
691
- window.removeEventListener('keydown', handlePreviewKeydown);
692
- });
718
+ onBeforeUnmount(() => {
719
+ window.removeEventListener('keydown', handlePreviewKeydown);
720
+ revokeAllLocalPreviews();
721
+ });
693
722
 
694
723
  /* -------------------- 对外暴露方法 -------------------- */
695
724
  /** 手动触发未上传文件的上传 */
@@ -700,10 +729,11 @@ const submit = () => {
700
729
  };
701
730
 
702
731
  /** 清空文件列表 */
703
- const clearFiles = () => {
704
- fileList.value = [];
705
- syncModel();
706
- };
732
+ const clearFiles = () => {
733
+ revokeAllLocalPreviews();
734
+ fileList.value = [];
735
+ syncModel();
736
+ };
707
737
 
708
738
  /** 手动移除某个文件 */
709
739
  const remove = (file) => handleRemove(file);
package/src/index.js CHANGED
@@ -11,9 +11,10 @@ export { default as VtkDatePicker } from "./components/assembly/VtkDatePicker.vu
11
11
  export { default as VtkDateSelector } from "./components/assembly/VtkDateSelector.vue";
12
12
  export { default as VtkDateTimePicker } from "./components/assembly/VtkDateTimePicker.vue";
13
13
  export { default as VtkDept } from "./components/assembly/VtkDept.vue";
14
- export { default as VtkEmpty } from "./components/assembly/VtkEmpty.vue";
15
- export { default as VtkFab } from "./components/assembly/VtkFab.vue";
16
- export { default as VtkFormItem } from "./components/assembly/VtkFormItem.vue";
14
+ export { default as VtkEmpty } from "./components/assembly/VtkEmpty.vue";
15
+ export { default as VtkEmptyNew } from "./components/assembly/VtkEmptyNew.vue";
16
+ export { default as VtkFab } from "./components/assembly/VtkFab.vue";
17
+ export { default as VtkFormItem } from "./components/assembly/VtkFormItem.vue";
17
18
  export { default as VtkImg } from "./components/assembly/VtkImg.vue";
18
19
  export { default as VtkPage } from "./components/assembly/VtkPage.vue";
19
20
  export { default as VtkPdf } from "./components/assembly/VtkPdf.vue";
@@ -55,9 +56,10 @@ import VtkDatePicker from "./components/assembly/VtkDatePicker.vue";
55
56
  import VtkDateSelector from "./components/assembly/VtkDateSelector.vue";
56
57
  import VtkDateTimePicker from "./components/assembly/VtkDateTimePicker.vue";
57
58
  import VtkDept from "./components/assembly/VtkDept.vue";
58
- import VtkEmpty from "./components/assembly/VtkEmpty.vue";
59
- import VtkFab from "./components/assembly/VtkFab.vue";
60
- import VtkFormItem from "./components/assembly/VtkFormItem.vue";
59
+ import VtkEmpty from "./components/assembly/VtkEmpty.vue";
60
+ import VtkEmptyNew from "./components/assembly/VtkEmptyNew.vue";
61
+ import VtkFab from "./components/assembly/VtkFab.vue";
62
+ import VtkFormItem from "./components/assembly/VtkFormItem.vue";
61
63
  import VtkImg from "./components/assembly/VtkImg.vue";
62
64
  import VtkPage from "./components/assembly/VtkPage.vue";
63
65
  import VtkPdf from "./components/assembly/VtkPdf.vue";
@@ -80,9 +82,10 @@ function install(app, options = {}) {
80
82
  VtkDateSelector,
81
83
  VtkDateTimePicker,
82
84
  VtkDept,
83
- VtkEmpty,
84
- VtkFab,
85
- VtkFormItem,
85
+ VtkEmpty,
86
+ VtkEmptyNew,
87
+ VtkFab,
88
+ VtkFormItem,
86
89
  VtkImg,
87
90
  VtkPage,
88
91
  VtkPdf,
@@ -128,8 +131,9 @@ export default {
128
131
  VtkDateSelector,
129
132
  VtkDateTimePicker,
130
133
  VtkDept,
131
- VtkEmpty,
132
- VtkFab,
134
+ VtkEmpty,
135
+ VtkEmptyNew,
136
+ VtkFab,
133
137
  VtkFormItem,
134
138
  VtkImg,
135
139
  VtkPage,