bl-common-vue3 3.8.111 → 3.8.114

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": "bl-common-vue3",
3
- "version": "3.8.111",
3
+ "version": "3.8.114",
4
4
  "main": "index.js",
5
5
  "module": "index.js",
6
6
  "description": "bailing vue3 common components lib",
@@ -5,6 +5,54 @@ const DEFAULT_COLUMN_WIDTH = 16;
5
5
  const MAX_COLUMN_WIDTH = 36;
6
6
  const HEADER_ROW_HEIGHT = 30;
7
7
  const DATA_ROW_HEIGHT = 24;
8
+ const XLSX_MIME_TYPE =
9
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
10
+ const INVALID_FILE_NAME_REG = /[\\/:*?"<>|]/g;
11
+
12
+ const getSafeXlsxFileName = (filename) => {
13
+ const safeName =
14
+ String(filename || "excel")
15
+ .replace(INVALID_FILE_NAME_REG, "_")
16
+ .trim() || "excel";
17
+ return /\.xlsx$/i.test(safeName) ? safeName : `${safeName}.xlsx`;
18
+ };
19
+
20
+ const isWeComBrowser = () => {
21
+ const ua = typeof navigator === "undefined" ? "" : navigator.userAgent || "";
22
+ return /wxwork|MicroMessenger/i.test(ua);
23
+ };
24
+
25
+ const saveByLink = (blob, fileName) => {
26
+ const objectUrl = window.URL.createObjectURL(blob);
27
+ const link = document.createElement("a");
28
+ link.style.display = "none";
29
+ link.href = objectUrl;
30
+ link.download = fileName;
31
+ document.body.appendChild(link);
32
+ link.click();
33
+ link.remove();
34
+ window.setTimeout(() => window.URL.revokeObjectURL(objectUrl), 0);
35
+ };
36
+
37
+ const saveXlsxBlob = (blob, filename) => {
38
+ const fileName = getSafeXlsxFileName(filename);
39
+ const file =
40
+ typeof File === "function"
41
+ ? new File([blob], fileName, { type: XLSX_MIME_TYPE })
42
+ : blob;
43
+
44
+ if (window.navigator?.msSaveOrOpenBlob) {
45
+ window.navigator.msSaveOrOpenBlob(file, fileName);
46
+ return;
47
+ }
48
+
49
+ if (isWeComBrowser()) {
50
+ saveByLink(file, fileName);
51
+ return;
52
+ }
53
+
54
+ fs.saveAs(file, fileName);
55
+ };
8
56
 
9
57
  const createBorder = (color) => ({
10
58
  top: { style: "thin", color: { rgb: color } },
@@ -33,56 +81,56 @@ const getStyledExportOptions = (options = {}) => {
33
81
  columnWidths && typeof columnWidths === "object" ? columnWidths : {},
34
82
  };
35
83
  };
36
-
84
+
37
85
  export function getVerticalMergeCells(json, fields, options = {}) {
38
- const { groupField, mergeFields = [] } = options;
39
- if (!groupField || !Array.isArray(mergeFields) || !mergeFields.length) {
40
- return [];
41
- }
42
-
43
- const fieldKeys = Object.keys(fields);
44
- const mergeColumnIndexes = mergeFields
45
- .map((field) => fieldKeys.indexOf(field))
46
- .filter((columnIndex) => columnIndex >= 0);
47
- if (!mergeColumnIndexes.length) {
48
- return [];
49
- }
50
-
51
- const merges = [];
52
- let groupStartIndex = 0;
53
-
54
- const appendGroupMerges = (groupEndIndex) => {
55
- const groupValue = json[groupStartIndex]?.[groupField];
56
- if (
57
- groupEndIndex <= groupStartIndex ||
58
- groupValue === undefined ||
59
- groupValue === null ||
60
- groupValue === ""
61
- ) {
62
- return;
63
- }
64
-
65
- mergeColumnIndexes.forEach((columnIndex) => {
66
- merges.push({
67
- // 工作表第 0 行是表头,数据行需要整体向下偏移一行。
68
- s: { r: groupStartIndex + 1, c: columnIndex },
69
- e: { r: groupEndIndex + 1, c: columnIndex },
70
- });
71
- });
72
- };
73
-
74
- for (let rowIndex = 1; rowIndex <= json.length; rowIndex++) {
75
- const groupChanged =
76
- rowIndex === json.length ||
77
- json[rowIndex]?.[groupField] !== json[groupStartIndex]?.[groupField];
78
- if (!groupChanged) {
79
- continue;
80
- }
81
-
82
- appendGroupMerges(rowIndex - 1);
83
- groupStartIndex = rowIndex;
84
- }
85
-
86
+ const { groupField, mergeFields = [] } = options;
87
+ if (!groupField || !Array.isArray(mergeFields) || !mergeFields.length) {
88
+ return [];
89
+ }
90
+
91
+ const fieldKeys = Object.keys(fields);
92
+ const mergeColumnIndexes = mergeFields
93
+ .map((field) => fieldKeys.indexOf(field))
94
+ .filter((columnIndex) => columnIndex >= 0);
95
+ if (!mergeColumnIndexes.length) {
96
+ return [];
97
+ }
98
+
99
+ const merges = [];
100
+ let groupStartIndex = 0;
101
+
102
+ const appendGroupMerges = (groupEndIndex) => {
103
+ const groupValue = json[groupStartIndex]?.[groupField];
104
+ if (
105
+ groupEndIndex <= groupStartIndex ||
106
+ groupValue === undefined ||
107
+ groupValue === null ||
108
+ groupValue === ""
109
+ ) {
110
+ return;
111
+ }
112
+
113
+ mergeColumnIndexes.forEach((columnIndex) => {
114
+ merges.push({
115
+ // 工作表第 0 行是表头,数据行需要整体向下偏移一行。
116
+ s: { r: groupStartIndex + 1, c: columnIndex },
117
+ e: { r: groupEndIndex + 1, c: columnIndex },
118
+ });
119
+ });
120
+ };
121
+
122
+ for (let rowIndex = 1; rowIndex <= json.length; rowIndex++) {
123
+ const groupChanged =
124
+ rowIndex === json.length ||
125
+ json[rowIndex]?.[groupField] !== json[groupStartIndex]?.[groupField];
126
+ if (!groupChanged) {
127
+ continue;
128
+ }
129
+
130
+ appendGroupMerges(rowIndex - 1);
131
+ groupStartIndex = rowIndex;
132
+ }
133
+
86
134
  return merges;
87
135
  }
88
136
 
@@ -188,14 +236,14 @@ export async function createWorkbook(
188
236
  : null;
189
237
  //导出xlsx
190
238
  json.forEach((item) => {
191
- for (let i in item) {
192
- // eslint-disable-next-line no-prototype-builtins
193
- if (fields.hasOwnProperty(i)) {
194
- item[fields[i]] = item[i];
195
- }
196
- delete item[i]; //删除原先的对象属性
197
- }
198
- });
239
+ for (let i in item) {
240
+ // eslint-disable-next-line no-prototype-builtins
241
+ if (fields.hasOwnProperty(i)) {
242
+ item[fields[i]] = item[i];
243
+ }
244
+ delete item[i]; //删除原先的对象属性
245
+ }
246
+ });
199
247
  let wb = XLSX.utils.book_new(); //工作簿对象包含一SheetNames数组,以及一个表对象映射表名称到表对象。XLSX.utils.book_new实用函数创建一个新的工作簿对象。
200
248
  let ws = XLSX.utils.json_to_sheet(json, { header: Object.values(fields) }); //将JS对象数组转换为工作表。
201
249
  if (merges.length) {
@@ -223,32 +271,32 @@ export async function xlsx(
223
271
  const sheetName = filename.replace(/[\\/?*[\]:]/g, "").slice(0, 31) || "Sheet1"; //excel的文件名称
224
272
  wb.SheetNames.push(sheetName);
225
273
  wb.Sheets[sheetName] = ws;
226
- const defaultCellStyle = {
227
- font: { name: "Verdana", sz: 13, color: "FF00FF88" },
228
- fill: { fgColor: { rgb: "FFFFAA00" } },
229
- }; //设置表格的样式
230
- let wopts = {
231
- bookType: "xlsx",
232
- bookSST: false,
233
- type: "binary",
234
- cellStyles: true,
235
- defaultCellStyle: defaultCellStyle,
236
- showGridLines: false,
237
- }; //写入的样式
238
- let wbout = XLSX.write(wb, wopts);
239
- let blob = new Blob([s2ab(wbout)], { type: "application/octet-stream" });
240
- fs.saveAs(blob, filename + ".xlsx");
241
- }
242
- const s2ab = (s) => {
243
- var buf;
244
- if (typeof ArrayBuffer !== "undefined") {
245
- buf = new ArrayBuffer(s.length);
246
- var view = new Uint8Array(buf);
247
- for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xff;
248
- return buf;
249
- } else {
250
- buf = new Array(s.length);
251
- for (var n = 0; n != s.length; ++n) buf[n] = s.charCodeAt(n) & 0xff;
252
- return buf;
253
- }
254
- };
274
+ const defaultCellStyle = {
275
+ font: { name: "Verdana", sz: 13, color: "FF00FF88" },
276
+ fill: { fgColor: { rgb: "FFFFAA00" } },
277
+ }; //设置表格的样式
278
+ let wopts = {
279
+ bookType: "xlsx",
280
+ bookSST: false,
281
+ type: "binary",
282
+ cellStyles: true,
283
+ defaultCellStyle: defaultCellStyle,
284
+ showGridLines: false,
285
+ }; //写入的样式
286
+ let wbout = XLSX.write(wb, wopts);
287
+ let blob = new Blob([s2ab(wbout)], { type: XLSX_MIME_TYPE });
288
+ saveXlsxBlob(blob, filename);
289
+ }
290
+ const s2ab = (s) => {
291
+ var buf;
292
+ if (typeof ArrayBuffer !== "undefined") {
293
+ buf = new ArrayBuffer(s.length);
294
+ var view = new Uint8Array(buf);
295
+ for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xff;
296
+ return buf;
297
+ } else {
298
+ buf = new Array(s.length);
299
+ for (var n = 0; n != s.length; ++n) buf[n] = s.charCodeAt(n) & 0xff;
300
+ return buf;
301
+ }
302
+ };
@@ -25,6 +25,7 @@
25
25
  :mobileAnnexUploadConfig="mobileAnnexUploadConfig"
26
26
  @handleRequest="handleRequest"
27
27
  @handleAddSuccess="handleAddSuccess"
28
+ @previewFile="previewClick"
28
29
  />
29
30
  </template>
30
31
  <a-table
@@ -52,11 +53,9 @@
52
53
  <a-tooltip v-if="record.annex_type == 2">
53
54
  <template #title>{{ t('AttachmentInfo.index.559728-5') }}</template>
54
55
  <a-button
55
- type="link"
56
- :disabled="record.online_preview && !record.online_preview.previewUrl &&
57
- record.online_preview.documentType !== 'pic'
58
- "
59
- @click="previewClick(record)"
56
+ type="link"
57
+ :disabled="!canPreviewFile(record)"
58
+ @click="previewClick(record)"
60
59
  >
61
60
  <template #icon>
62
61
  <EyeOutlined/>
@@ -86,6 +85,7 @@
86
85
  :mobileAnnexUploadConfig="mobileAnnexUploadConfig"
87
86
  @handleRequest="handleRequest"
88
87
  @handleAddSuccess="handleAddSuccess"
88
+ @previewFile="previewClick"
89
89
  />
90
90
  <a-tooltip v-if="record.operate.includes('Topping')">
91
91
  <template #title>{{ t('AttachmentInfo.index.559728-6') }}</template>
@@ -136,6 +136,23 @@
136
136
  </template>
137
137
  </a-table>
138
138
  </a-card>
139
+
140
+ <!-- 预览图片 -->
141
+ <PreviewImg
142
+ :visible="imgVisible"
143
+ :index="0"
144
+ :list="imgList"
145
+ @close="onCloseImgPreview"
146
+ />
147
+
148
+ <!-- 文档预览 -->
149
+ <PreviewFile
150
+ :visible="previewFileVisible"
151
+ :urlData="previewUrlData"
152
+ :title="previewUrlData.name"
153
+ :esignDetail="{esignCheck: true}"
154
+ @close="onClosePreviewFile"
155
+ />
139
156
  </section>
140
157
  </template>
141
158
 
@@ -166,6 +183,8 @@ import {
166
183
  } from "ant-design-vue";
167
184
  import {t, loadLanguageAsync} from "../../locale";
168
185
  import AddAction from "./modules/AddAction.vue";
186
+ import PreviewImg from "../PreviewImg/index.vue";
187
+ import PreviewFile from "../PreviewFile/index.vue";
169
188
  export default defineComponent({
170
189
  name: "ContractAttachmentInfo",
171
190
  components: {
@@ -181,6 +200,8 @@ export default defineComponent({
181
200
  "a-tooltip": Tooltip,
182
201
  "a-button": Button,
183
202
  "a-space": Space,
203
+ PreviewImg: PreviewImg,
204
+ PreviewFile,
184
205
  },
185
206
  props: {
186
207
  active: {
@@ -315,7 +336,12 @@ export default defineComponent({
315
336
  disabled: false,
316
337
  };
317
338
  },
318
- }
339
+ },
340
+ // 是否自定义预览
341
+ isCustomPreview: {
342
+ type: Boolean,
343
+ default: false,
344
+ },
319
345
  },
320
346
  setup(props, context) {
321
347
  const homeCrumb = { key: "home", parentId: 0, title: t('AttachmentInfo.index.559728-8') };
@@ -345,7 +371,6 @@ export default defineComponent({
345
371
  annexAdd: props.annexAdd,
346
372
  serviceFrom: props.serviceFrom,
347
373
  };
348
- console.log(addParams.value, "!111");
349
374
  if (type != 3) {
350
375
  addVisible.value = true;
351
376
  }
@@ -542,17 +567,124 @@ export default defineComponent({
542
567
  const randomStr = utils.getRandomUid(18);
543
568
  let loaded = false;
544
569
 
570
+ // 获取文件后缀,统一处理带 query/hash 的预览地址
571
+ const getFileSuffix = (file) => {
572
+ if (!file) return "";
573
+ return file.split("?")[0].split("#")[0].split(".").pop().toLowerCase();
574
+ };
575
+
576
+ // 判断是否为图片文件
577
+ const isImageFile = (file) => {
578
+ if (!file) return false;
579
+ const imageExtensions = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"];
580
+ return imageExtensions.includes(getFileSuffix(file));
581
+ };
582
+
583
+ // 判断是否为媒体文件
584
+ const isMediaFile = (file) => {
585
+ if (!file) return false;
586
+ const videoExtensions = ["mp4","mov","m4v","webm","ogv","avi","wmv","mkv","flv","mpeg"];
587
+ const radioExtensions = ["mp3","wav","ogg","m4a","aac","flac","wma"];
588
+ return [...videoExtensions, ...radioExtensions].includes(getFileSuffix(file));
589
+ };
590
+
591
+ // 统一整理附件预览数据,避免不同入口分别判断图片和文档逻辑
592
+ const resolvePreviewData = (record = {}) => {
593
+ const filePath = record.file_path || "";
594
+ const previewInfo = record.preview_url || record.online_preview || {};
595
+ const previewUrl = previewInfo.previewUrl || "";
596
+ const documentType = previewInfo.documentType || "";
597
+ const fileName = record.name || "";
598
+ const sourceUrl = previewUrl || filePath;
599
+
600
+ if (sourceUrl && (documentType === "pic" || isImageFile(sourceUrl))) {
601
+ return {
602
+ type: "image",
603
+ url: sourceUrl,
604
+ name: fileName,
605
+ };
606
+ }
607
+
608
+ if (getFileSuffix(filePath) === "pdf") {
609
+ return {
610
+ type: "file",
611
+ previewUrl: `/public_web/pdf/web/viewer.html?file=${encodeURIComponent(filePath)}`,
612
+ url: filePath,
613
+ name: fileName,
614
+ };
615
+ }
616
+
617
+ if (previewUrl) {
618
+ return {
619
+ type: "file",
620
+ previewUrl,
621
+ url: filePath || previewUrl,
622
+ name: fileName,
623
+ };
624
+ }
625
+
626
+ return null;
627
+ };
628
+
629
+ // 判断是否可预览文件
630
+ const canPreviewFile = (record = {}) => {
631
+ return !!resolvePreviewData(record) || !!record.file_path;
632
+ };
633
+
545
634
  // 在线预览
546
635
  const imgVisible = ref(false);
547
636
  const imgList = ref([]);
548
637
  const previewUrlData = ref({});
638
+ const previewFileVisible = ref(false);
639
+
640
+ // 关闭图片预览
641
+ const onCloseImgPreview = () => {
642
+ imgVisible.value = false;
643
+ imgList.value = [];
644
+ };
645
+
646
+ // 关闭预览文件
647
+ const onClosePreviewFile = () => {
648
+ previewFileVisible.value = false;
649
+ previewUrlData.value = {};
650
+ };
651
+
652
+ // 预览文件
549
653
  const previewClick = (record) => {
550
- window.microApp.forceDispatch({
551
- type: "previewFile",
552
- visible: true,
553
- file: { url: record.file_path },
554
- filePreviewUrl: record.preview_url.previewUrl,
555
- });
654
+ if(props.isCustomPreview){
655
+ context.emit("previewFile", record);
656
+ return;
657
+ }
658
+ // 视频和音频文件直接由浏览器打开,避免进入 iframe 预览导致无法播放
659
+ if (record.file_path && isMediaFile(record?.file_path)) {
660
+ record.file_path && window.open(record.file_path);
661
+ return;
662
+ }
663
+ if (window.__MICRO_APP_ENVIRONMENT__) {
664
+ window.microApp.forceDispatch({
665
+ type: "previewFile",
666
+ visible: true,
667
+ file: {
668
+ url: record?.file_path || '',
669
+ name: record?.name || '',
670
+ },
671
+ filePreviewUrl: record?.preview_url?.previewUrl || record?.online_preview?.previewUrl || '',
672
+ });
673
+ } else {
674
+ const previewData = resolvePreviewData(record);
675
+ if (previewData?.type === "image") {
676
+ imgList.value = [previewData.url];
677
+ imgVisible.value = true;
678
+ return;
679
+ }
680
+ if (previewData?.type === "file") {
681
+ previewUrlData.value = previewData;
682
+ previewFileVisible.value = true;
683
+ return;
684
+ }
685
+ // 没有可预览地址时,保留浏览器原生打开行为,避免误拦截下载型文件
686
+ record.file_path && window.open(record.file_path);
687
+ }
556
688
  };
557
689
 
558
690
  // 附件编辑
@@ -592,7 +724,6 @@ export default defineComponent({
592
724
  if(props.typeFrom){
593
725
  attachParams.typeFrom = props.typeFrom;
594
726
  }
595
- console.log("attachParams", attachParams);
596
727
  getDataList();
597
728
  loaded = true;
598
729
  };
@@ -716,11 +847,15 @@ export default defineComponent({
716
847
  downloadFile,
717
848
  previewClick,
718
849
  previewUrlData,
850
+ previewFileVisible,
851
+ onClosePreviewFile,
719
852
 
720
853
  imgList,
721
854
  imgVisible,
855
+ onCloseImgPreview,
722
856
  multiAddVisible,
723
857
  handleRequest,
858
+ canPreviewFile,
724
859
 
725
860
  getAnnexEdit,
726
861
  mobileAnnexUploadConfigInfo,
@@ -81,6 +81,7 @@
81
81
  :annexAdd="annexAdd"
82
82
  :getFile="getFile"
83
83
  @handleCommit="handleAddSuccess"
84
+ @previewFile="previewClick"
84
85
  @request="handleRequest"/>
85
86
  </template>
86
87
 
@@ -265,6 +266,11 @@ export default defineComponent({
265
266
  return props.getFile ? {} : params;
266
267
  });
267
268
 
269
+ // 预览文件
270
+ const previewClick = (previewData) => {
271
+ context.emit("previewFile", previewData);
272
+ };
273
+
268
274
  return {
269
275
  t,
270
276
  multiAddVisible,
@@ -277,6 +283,7 @@ export default defineComponent({
277
283
  handleAddSuccess,
278
284
  handleRequest,
279
285
  addRequest,
286
+ previewClick,
280
287
  };
281
288
  },
282
289
  });
@@ -245,16 +245,33 @@ export default {
245
245
  const imgVisible = ref(false);
246
246
  const imgList = ref([]);
247
247
  const previewClick = (record) => {
248
+ let fileUrl = record.response.data.fileUrl || '';
249
+ let previewUrl = record.response.data.previewUrl || '';
250
+ const fileName = record?.originFileObj?.name || record?.name || '';
248
251
  if (record.type.startsWith("image")) {
249
- imgList.value = [record.response.data.fileUrl];
252
+ imgList.value = [fileUrl];
250
253
  imgVisible.value = true;
251
254
  } else {
252
- window.microApp.forceDispatch({
253
- type: "previewFile",
254
- visible: true,
255
- file: { url: record.response.data.fileUrl },
256
- filePreviewUrl: record.response.data.previewUrl,
257
- });
255
+ if (window.__MICRO_APP_ENVIRONMENT__) {
256
+ window.microApp.forceDispatch({
257
+ type: "previewFile",
258
+ visible: true,
259
+ file: {
260
+ url: fileUrl,
261
+ name: fileName,
262
+ },
263
+ filePreviewUrl: previewUrl || '',
264
+ });
265
+ } else {
266
+ let previewData = {
267
+ file_path: fileUrl,
268
+ preview_url: {
269
+ previewUrl
270
+ },
271
+ name: fileName,
272
+ }
273
+ context.emit("previewFile", previewData);
274
+ }
258
275
  }
259
276
  };
260
277
 
@@ -111,6 +111,15 @@
111
111
  :config="item"
112
112
  :validateInfo="validateInfos[item.key]"
113
113
  />
114
+ <a-col :span="8" v-if="displayStatus">
115
+ <a-form-item :label="t('common.table.status')">
116
+ <a-switch
117
+ v-model:checked="formState.status"
118
+ :checkedValue="1"
119
+ :unCheckedValue="0"
120
+ />
121
+ </a-form-item>
122
+ </a-col>
114
123
  <a-col :span="24">
115
124
  <div class="text-title">{{t('components.extraManageEdit.enterSetting')}}</div>
116
125
  </a-col>
@@ -272,6 +281,10 @@ export default defineComponent({
272
281
  type: Array,
273
282
  default: () => [],
274
283
  },
284
+ displayStatus:{
285
+ type: Boolean,
286
+ default: true,
287
+ },
275
288
  },
276
289
  components: {
277
290
  BlModal,
@@ -575,6 +588,9 @@ export default defineComponent({
575
588
  const init = () => {
576
589
  gettypelist('extra_field_type')
577
590
  getCompanyList();
591
+ if (props.displayStatus){
592
+ state.formState.status = 1
593
+ }
578
594
  if (props.id) {
579
595
  // 编辑
580
596
  let detailInfo = props.requestAllInfo.detail;
@@ -28,6 +28,14 @@
28
28
  @change="handleCustomConfigChange($event, record, item)"
29
29
  />
30
30
  </template>
31
+ <template #column-status="{record}">
32
+ <a-switch
33
+ :checked="getStatusValue(record)"
34
+ :checkedValue="1"
35
+ :unCheckedValue="0"
36
+ @change="statusChange(record, $event)"
37
+ />
38
+ </template>
31
39
  <template #column-action="{ record }">
32
40
  <div v-if="record.is_system == 1">
33
41
  <a-button @click="handleEdit(record)" type="link" v-if="record?.can_edit">{{t('common.button.edit')}}</a-button>
@@ -52,6 +60,7 @@
52
60
  :requestAllInfo="requestAllInfo"
53
61
  :requestAllParams="requestAllParams"
54
62
  :displayCompany="displayCompany"
63
+ :display-status="displayStatus"
55
64
  :expandFieldsExtraParams="expandFieldsExtraParams"
56
65
  :id="editId"
57
66
  :hasVillageSelect="hasVillageSelect"
@@ -135,6 +144,11 @@ export default defineComponent({
135
144
  type: Array,
136
145
  default: () => [],
137
146
  },
147
+ // 是否展示状态
148
+ displayStatus:{
149
+ type: Boolean,
150
+ default: false,
151
+ },
138
152
  },
139
153
  components: {
140
154
  BlDrawer,
@@ -147,6 +161,7 @@ export default defineComponent({
147
161
  editVisible: false,
148
162
  editId: 0,
149
163
  customTable: null,
164
+ statusMap: {},
150
165
  });
151
166
 
152
167
  // 处理后的自定义配置项
@@ -298,6 +313,15 @@ export default defineComponent({
298
313
  const fillColumnIndex = _deepColumns.findIndex((column) => column.key === 'fill');
299
314
  const customColumns = getColumnsVisibleCustomConfigItems(normalizedCustomConfigItems.value);
300
315
  _deepColumns.splice(fillColumnIndex, 0, ...customColumns);
316
+ if (props.displayStatus){
317
+ let index = _deepColumns.length - 3
318
+ _deepColumns.splice(index, 0, {
319
+ title: t('common.table.status'),
320
+ dataIndex: "status" ,
321
+ key: 'status',
322
+ width: 120,
323
+ })
324
+ }
301
325
  return _deepColumns;
302
326
  });
303
327
 
@@ -346,6 +370,40 @@ export default defineComponent({
346
370
  });
347
371
  };
348
372
 
373
+ const getStatusValue = (record) => {
374
+ const key = record?.id;
375
+ if (Object.prototype.hasOwnProperty.call(state.statusMap, key)) {
376
+ return state.statusMap[key];
377
+ }
378
+ return Number(record?.status) === 1 ? 1 : 0;
379
+ }
380
+
381
+ const statusChange = (record, checked) => {
382
+ const status = checked ? 1 : 0;
383
+ const oldStatus = getStatusValue(record);
384
+ state.statusMap[record.id] = status;
385
+ record.status = status;
386
+ const params = { ...record, status };
387
+ // 编辑
388
+ emit("request", {
389
+ params: {
390
+ method: 'put',
391
+ server: props.requestAllInfo.edit.service,
392
+ url: props.requestAllInfo.edit.serviceUrl.replace(/\${id}/g, record.id),
393
+ extra: {
394
+ ...params,
395
+ },
396
+ },
397
+ success: (res) => {
398
+ message.success(res?.msg || t('common.save.success'));
399
+ },
400
+ fail: () => {
401
+ state.statusMap[record.id] = oldStatus;
402
+ record.status = oldStatus;
403
+ },
404
+ })
405
+ }
406
+
349
407
  return {
350
408
  t,
351
409
  ...toRefs(state),
@@ -361,6 +419,8 @@ export default defineComponent({
361
419
  getCustomConfigEditorProps,
362
420
  handleCustomConfigChange,
363
421
  editableCustomConfigItems,
422
+ getStatusValue,
423
+ statusChange,
364
424
  };
365
425
  },
366
426
  });
@@ -0,0 +1,158 @@
1
+ <template>
2
+ <a-modal v-model:visible="showModal" :title="t('PreviewFile.printModal.title')" @cancel="handleOk">
3
+ <div>
4
+ {{ t('PreviewFile.printModal.noticeStart') }}
5
+ <div
6
+ class="text-blue"
7
+ style="display: inline-block; cursor: pointer"
8
+ @click="downloadExe"
9
+ >
10
+ {{ t('PreviewFile.printModal.download') }}
11
+ <cloud-download-outlined style="font-size: 20px" />
12
+ </div>
13
+ {{ t('PreviewFile.printModal.noticeEnd') }}
14
+ </div>
15
+ <template #footer>
16
+ <div style="text-align: right">
17
+ <a-button type="primary" @click="handleOk">{{ t('PreviewFile.printModal.ok') }}</a-button>
18
+ </div>
19
+ </template>
20
+ </a-modal>
21
+ </template>
22
+
23
+ <script>
24
+ import { onUnmounted, reactive, toRefs, watch } from "vue";
25
+ import { CloudDownloadOutlined } from "@ant-design/icons-vue";
26
+ import { message, Modal, Button } from "ant-design-vue";
27
+ import { t, loadLanguageAsync } from "../../locale";
28
+ import utils from "../../common/utils/util";
29
+
30
+ export default {
31
+ components: {
32
+ CloudDownloadOutlined,
33
+ 'a-button': Button,
34
+ 'a-modal': Modal
35
+ },
36
+ props: {
37
+ usePrint: {
38
+ type: Boolean,
39
+ default: false,
40
+ },
41
+ file_list: {
42
+ type: Array,
43
+ default: () => [],
44
+ },
45
+ },
46
+ setup(props, context) {
47
+ const state = reactive({
48
+ showModal: false,
49
+ });
50
+
51
+ const printHost = "http://127.0.0.1:19889/api";
52
+
53
+ // 带超时的本地接口请求,避免插件无响应时页面一直挂起
54
+ const requestWithTimeout = async (url, options = {}, timeout = 2000) => {
55
+ const controller = new AbortController();
56
+ const timer = window.setTimeout(() => controller.abort(), timeout);
57
+ try {
58
+ return await fetch(url, {
59
+ ...options,
60
+ signal: controller.signal,
61
+ });
62
+ } finally {
63
+ window.clearTimeout(timer);
64
+ }
65
+ };
66
+
67
+ // 连接成功后,把文件列表发给本地打印插件
68
+ const commitPrint = async () => {
69
+ const param = {
70
+ file_list: props.file_list,
71
+ };
72
+ console.log(JSON.stringify(param));
73
+ try {
74
+ const response = await requestWithTimeout(
75
+ `${printHost}/printDoc`,
76
+ {
77
+ method: "POST",
78
+ headers: {
79
+ "Content-Type": "application/json",
80
+ },
81
+ body: JSON.stringify(param),
82
+ },
83
+ 2000
84
+ );
85
+ if (!response.ok) {
86
+ throw new Error(response.statusText || "print failed");
87
+ }
88
+ message.success(t("PreviewFile.printModal.success"));
89
+ context.emit("change");
90
+ } catch (error) {
91
+ message.error(error?.message || error);
92
+ }
93
+ };
94
+
95
+ // 先检查本地打印插件是否已连接,未连接时展示下载提示
96
+ const connect = async () => {
97
+ try {
98
+ const response = await requestWithTimeout(`${printHost}/connect`, {
99
+ method: "GET",
100
+ }, 1000);
101
+ if (!response.ok) {
102
+ throw new Error(response.statusText || "connect failed");
103
+ }
104
+ console.log(await response.text());
105
+ await commitPrint();
106
+ } catch (error) {
107
+ console.error(error);
108
+ state.showModal = true;
109
+ }
110
+ };
111
+
112
+ // 下载网页打印插件安装包
113
+ const downloadExe = () => {
114
+ const url =
115
+ "https://bailing-customer-1305744786.cos.ap-nanjing.myqcloud.com/http_printer/%E7%BD%91%E9%A1%B5%E6%89%93%E5%8D%B0%E6%8F%92%E4%BB%B6.exe";
116
+ window.open(url);
117
+ };
118
+
119
+ // 关闭提示弹窗并通知父组件结束本次打印动作
120
+ const handleOk = () => {
121
+ state.showModal = false;
122
+ context.emit("change");
123
+ };
124
+
125
+ // 外部触发打印时,尝试连接本地插件
126
+ watch(() => props.usePrint, (value) => {
127
+ if (value) {
128
+ connect();
129
+ } else {
130
+ state.showModal = false;
131
+ }
132
+ });
133
+
134
+ // 保持当前组件的语言环境和全局语言一致
135
+ const handleLangChange = (event) => {
136
+ loadLanguageAsync(event.newValue);
137
+ };
138
+
139
+ loadLanguageAsync(utils.getLang());
140
+ window.addEventListener("setBLLang", handleLangChange);
141
+ onUnmounted(() => {
142
+ window.removeEventListener("setBLLang", handleLangChange);
143
+ });
144
+
145
+ return {
146
+ ...toRefs(state),
147
+ t,
148
+ downloadExe,
149
+ connect,
150
+ commitPrint,
151
+ handleOk,
152
+ };
153
+ },
154
+ };
155
+ </script>
156
+
157
+ <style lang="less" scoped>
158
+ </style>
@@ -0,0 +1,293 @@
1
+ <template>
2
+ <div>
3
+ <a-modal
4
+ :width="1200"
5
+ :visible="visible"
6
+ centered
7
+ :footer="null"
8
+ :bodyStyle="{
9
+ padding: '0',
10
+ }"
11
+ destroyOnClose
12
+ @cancel="onClose"
13
+ >
14
+ <div class="content">
15
+ <a-spin :tip="t('PreviewFile.loading')" :spinning="spinLoading" :style="{ height: spinHeight }">
16
+ <iframe
17
+ v-if="previewUrl"
18
+ :height="clientHeight * 0.85"
19
+ width="100%"
20
+ :src="previewUrl"
21
+ allowtransparency="true"
22
+ allowfullscreen="true"
23
+ allowfullscreenInteractive="true"
24
+ scrolling="no"
25
+ border="0"
26
+ frameborder="0"
27
+ @load="handleLoad"
28
+ ></iframe>
29
+ </a-spin>
30
+ </div>
31
+ <template #title>
32
+ <a-space>
33
+ <div>{{ displayTitle }}</div>
34
+ <div v-if="esignDetail.esignCheck && (fileUrl || canPrint)">
35
+ <a-space>
36
+ <a-button v-if="fileUrl" :loading="esignLoading" type="primary" @click="operateClick('download')">
37
+ <DownloadOutlined />
38
+ {{ t('PreviewFile.download') }}
39
+ </a-button>
40
+ <a-button v-if="canPrint" type="primary" @click="printFile">
41
+ <PrinterOutlined />
42
+ {{ t('PreviewFile.print') }}
43
+ </a-button>
44
+ </a-space>
45
+ </div>
46
+ </a-space>
47
+ </template>
48
+ </a-modal>
49
+ <!--打印-->
50
+ <print-modal :file_list="printList" :usePrint="showModal" @change="showModal = false" />
51
+ </div>
52
+ </template>
53
+
54
+ <script>
55
+ import { computed, defineComponent, onUnmounted, reactive, toRefs, watch } from "vue";
56
+ import { Modal, Spin, Button, Space } from "ant-design-vue";
57
+ import { DownloadOutlined, PrinterOutlined } from "@ant-design/icons-vue";
58
+ import PrintModal from './PrintModal.vue';
59
+ import { t, loadLanguageAsync } from "../../locale";
60
+ import utils from "../../common/utils/util";
61
+
62
+ export default defineComponent({
63
+ name: "PreviewFile",
64
+ components: {
65
+ PrintModal,
66
+ DownloadOutlined,
67
+ PrinterOutlined,
68
+ "a-modal": Modal,
69
+ "a-spin": Spin,
70
+ "a-button": Button,
71
+ "a-space": Space,
72
+ },
73
+ props: {
74
+ visible: {
75
+ type: Boolean,
76
+ default: false,
77
+ },
78
+ esignDetail: {
79
+ type: Object,
80
+ default: () => {
81
+ return {};
82
+ },
83
+ },
84
+ title: {
85
+ type: String,
86
+ default: () => {
87
+ return t('PreviewFile.defaultTitle')
88
+ },
89
+ },
90
+ urlData: {
91
+ type: Object,
92
+ default: () => {
93
+ return {};
94
+ },
95
+ },
96
+ type: {
97
+ type: String,
98
+ default: "",
99
+ },
100
+ },
101
+ setup(props, {emit}) {
102
+ const state = reactive({
103
+ previewUrl: "",
104
+ fileUrl: "",
105
+ name: "",
106
+ clientHeight: document.body.clientHeight,
107
+ esignLoading: false,
108
+ spinLoading: true,
109
+ showModal: false,
110
+ canPrint: false,
111
+ printList: [],
112
+ });
113
+
114
+ // 预览弹窗标题统一使用多语言兜底,避免父组件不传标题时出现空白
115
+ const displayTitle = computed(() => props.title || t('PreviewFile.defaultTitle'));
116
+
117
+ // 重置所有预览状态,避免关闭后再次打开时残留上一次的文件信息
118
+ const resetPreviewState = () => {
119
+ state.previewUrl = "";
120
+ state.fileUrl = "";
121
+ state.name = "";
122
+ state.esignLoading = false;
123
+ state.spinLoading = true;
124
+ state.showModal = false;
125
+ state.canPrint = false;
126
+ state.printList = [];
127
+ };
128
+
129
+ // 获取文件后缀,统一处理带 query/hash 的预览地址
130
+ const getFileSuffix = (fileUrl = "") => {
131
+ if (!fileUrl) return "";
132
+ return fileUrl.split("?")[0].split("#")[0].split(".").pop().toLowerCase();
133
+ };
134
+
135
+ const operateClick = (action) => {
136
+ // 下载按钮只处理本地可下载的文档类型,其他文件直接交给浏览器打开
137
+ if (action == "download") {
138
+ const fileType = getFileSuffix(state.fileUrl);
139
+ let type = "";
140
+ if (["xlsx", "xls"].includes(fileType)) {
141
+ type = 'xlsx'
142
+ }
143
+ if (["docx", "doc"].includes(fileType)) {
144
+ type = 'word'
145
+ }
146
+ if (["pdf"].includes(fileType)) {
147
+ type = 'pdf'
148
+ }
149
+ if (state.name && ["pdf", "word", "xlsx"].includes(type)) {
150
+ if (state.esignLoading) {
151
+ return;
152
+ }
153
+ state.esignLoading = true;
154
+ fetchDownloadFile({
155
+ url: state.fileUrl,
156
+ name: state.name,
157
+ type,
158
+ });
159
+ } else {
160
+ window.open(state.fileUrl);
161
+ }
162
+ return;
163
+ }
164
+ };
165
+
166
+ // 使用浏览器能力完成本地下载,避免把下载链接再次交给预览页处理
167
+ const fetchDownloadFile = async (data) => {
168
+ if (!data?.url) {
169
+ state.esignLoading = false;
170
+ return;
171
+ }
172
+ let downloadUrl = "";
173
+ try {
174
+ const response = await fetch(data.url, {
175
+ method: "get",
176
+ mode: "cors",
177
+ });
178
+ if (!response.ok) {
179
+ throw new Error("download failed");
180
+ }
181
+ const res = await response.blob();
182
+ const typeObj = {
183
+ pdf: "application/pdf",
184
+ word: "application/msword",
185
+ xlsx: "application/vnd.ms-excel",
186
+ };
187
+ downloadUrl = window.URL.createObjectURL(
188
+ new Blob([res], {
189
+ type: typeObj[data.type] || "",
190
+ })
191
+ );
192
+ const link = document.createElement("a");
193
+ link.href = downloadUrl;
194
+ link.setAttribute("download", data.name);
195
+ document.body.appendChild(link);
196
+ link.click();
197
+ link.remove();
198
+ } catch (error) {
199
+ window.open(data.url);
200
+ } finally {
201
+ if (downloadUrl) {
202
+ window.URL.revokeObjectURL(downloadUrl);
203
+ }
204
+ state.esignLoading = false;
205
+ }
206
+ };
207
+
208
+ const onClose = () => {
209
+ resetPreviewState();
210
+ emit("close");
211
+ };
212
+
213
+ const handleLoad = () => {
214
+ state.spinLoading = false;
215
+ };
216
+
217
+ const spinHeight = computed(() => {
218
+ return state.clientHeight * 0.85 + "px";
219
+ });
220
+
221
+ // 打开弹窗时重建预览状态,关闭时清理现场,避免上一个文件的状态串到下一个文件
222
+ watch(
223
+ () => [props.visible, props.urlData?.previewUrl, props.urlData?.url, props.urlData?.name],
224
+ ([visible]) => {
225
+ if (visible) {
226
+ state.previewUrl = props.urlData?.previewUrl || "";
227
+ state.fileUrl = props.urlData?.url || "";
228
+ state.name = props.urlData?.name || "";
229
+ state.canPrint = state.fileUrl.endsWith(".docx");
230
+ state.spinLoading = !!state.previewUrl;
231
+ state.esignLoading = false;
232
+ state.showModal = false;
233
+ state.printList = [];
234
+ } else {
235
+ resetPreviewState();
236
+ }
237
+ }
238
+ );
239
+
240
+ const printFile = () => {
241
+ state.printList = [state.fileUrl];
242
+ state.showModal = true;
243
+ };
244
+
245
+ // 同步当前系统语言,保证单独使用该组件时多语言也能正常切换
246
+ const handleLangChange = (event) => {
247
+ loadLanguageAsync(event.newValue);
248
+ };
249
+
250
+ loadLanguageAsync(utils.getLang());
251
+ window.addEventListener("setBLLang", handleLangChange);
252
+ onUnmounted(() => {
253
+ window.removeEventListener("setBLLang", handleLangChange);
254
+ });
255
+
256
+ return {
257
+ ...toRefs(state),
258
+ t,
259
+ displayTitle,
260
+ onClose,
261
+ operateClick,
262
+ spinHeight,
263
+ handleLoad,
264
+ printFile,
265
+ };
266
+ },
267
+ });
268
+ </script>
269
+
270
+ <style lang="less" scoped>
271
+ .content {
272
+ width: 100%;
273
+ display: flex;
274
+ justify-content: center;
275
+ }
276
+
277
+ .el-loading {
278
+ position: fixed;
279
+ left: 0;
280
+ right: 0;
281
+ top: 0;
282
+ bottom: 0;
283
+ z-index: 1000;
284
+ display: flex;
285
+ align-items: center;
286
+ justify-content: center;
287
+ background: rgba(255, 255, 255, 0.6);
288
+ }
289
+
290
+ :deep(.ant-spin-nested-loading) {
291
+ width: 100%;
292
+ }
293
+ </style>
@@ -51,3 +51,5 @@ export { default as PublicExtensionField } from "./PublicExtensionField/index.vu
51
51
  export { default as BlCreateChatBtn } from "./BlChat/createChatBtn.vue";//发起讨论按钮
52
52
  export { default as BlModal } from "./BlModal/index.vue";
53
53
  export { default as BlDrawer } from "./BlDrawer/index.vue";
54
+ export { default as BlTinymceEditor } from "./BlTinymceEditor/index.vue";
55
+ export { default as PreviewFile } from "./PreviewFile/index.vue";
@@ -515,5 +515,15 @@
515
515
  "DepartmentUser.index.555981-1": "展開",
516
516
  "DepartmentUser.index.555981-2": "收起",
517
517
  "BlMobileAnnexUpload.index.676543-0": "此 QR Code 於 15 分鐘內有效",
518
- "AttchmentInfo.userShow": "對業戶开放"
518
+ "AttchmentInfo.userShow": "對業戶开放",
519
+ "PreviewFile.loading": "載入中...",
520
+ "PreviewFile.printModal.title": "提示訊息",
521
+ "PreviewFile.printModal.noticeStart": "您目前尚未開啟網頁列印插件,請先開啟或",
522
+ "PreviewFile.printModal.download": "點擊下載",
523
+ "PreviewFile.printModal.noticeEnd": "。電腦需安裝 Microsoft Word 或 WPS Office 才能使用。",
524
+ "PreviewFile.printModal.ok": "好的",
525
+ "PreviewFile.printModal.success": "發送成功",
526
+ "PreviewFile.download": "文件下載",
527
+ "PreviewFile.print": "列印文件",
528
+ "PreviewFile.defaultTitle": "預覽文件"
519
529
  }
@@ -515,5 +515,15 @@
515
515
  "DepartmentUser.index.555981-1": "Expand",
516
516
  "DepartmentUser.index.555981-2": "Collapse",
517
517
  "BlMobileAnnexUpload.index.676543-0": "This QR code is valid for 15 minutes",
518
- "AttchmentInfo.userShow": "Show to Users"
518
+ "AttchmentInfo.userShow": "Show to Users",
519
+ "PreviewFile.loading": "Loading...",
520
+ "PreviewFile.printModal.title": "Notice",
521
+ "PreviewFile.printModal.noticeStart": "The web print plugin is not open. Open it or",
522
+ "PreviewFile.printModal.download": "download it",
523
+ "PreviewFile.printModal.noticeEnd": ". Microsoft Word or WPS Office is also required.",
524
+ "PreviewFile.printModal.ok": "OK",
525
+ "PreviewFile.printModal.success": "Sent",
526
+ "PreviewFile.download": "Download File",
527
+ "PreviewFile.print": "Print File",
528
+ "PreviewFile.defaultTitle": "Preview File"
519
529
  }
@@ -515,5 +515,15 @@
515
515
  "DepartmentUser.index.555981-1": "展開",
516
516
  "DepartmentUser.index.555981-2": "折りたたむ",
517
517
  "BlMobileAnnexUpload.index.676543-0": "このQRコードは15分間有効です",
518
- "AttchmentInfo.userShow": "ユーザー表示"
518
+ "AttchmentInfo.userShow": "ユーザー表示",
519
+ "PreviewFile.loading": "読み込み中...",
520
+ "PreviewFile.printModal.title": "お知らせ",
521
+ "PreviewFile.printModal.noticeStart": "Web 印刷プラグインが起動していません。起動するか、",
522
+ "PreviewFile.printModal.download": "ダウンロードしてください",
523
+ "PreviewFile.printModal.noticeEnd": "。Microsoft Word または WPS Office も必要です。",
524
+ "PreviewFile.printModal.ok": "OK",
525
+ "PreviewFile.printModal.success": "送信しました",
526
+ "PreviewFile.download": "ファイルダウンロード",
527
+ "PreviewFile.print": "ファイル印刷",
528
+ "PreviewFile.defaultTitle": "ファイルをプレビュー"
519
529
  }
@@ -515,5 +515,15 @@
515
515
  "BlMobileAnnexUpload.index-0": "查看二維碼",
516
516
  "BlMobileAnnexUpload.index-1": "使用手機掃碼上載",
517
517
  "modules.ListFieldModal.082665-0": "選項內容不能留空",
518
- "AttchmentInfo.userShow": "對業戶开放"
518
+ "AttchmentInfo.userShow": "對業戶开放",
519
+ "PreviewFile.loading": "載入中...",
520
+ "PreviewFile.printModal.title": "提示訊息",
521
+ "PreviewFile.printModal.noticeStart": "您目前尚未開啟網頁列印插件,請先開啟或",
522
+ "PreviewFile.printModal.download": "點擊下載",
523
+ "PreviewFile.printModal.noticeEnd": "。電腦需安裝 Microsoft Word 或 WPS Office 才能使用。",
524
+ "PreviewFile.printModal.ok": "好的",
525
+ "PreviewFile.printModal.success": "發送成功",
526
+ "PreviewFile.download": "文件下載",
527
+ "PreviewFile.print": "列印文件",
528
+ "PreviewFile.defaultTitle": "預覽文件"
519
529
  }
@@ -515,5 +515,15 @@
515
515
  "DepartmentUser.index.555981-1": "展开",
516
516
  "DepartmentUser.index.555981-2": "收起",
517
517
  "BlMobileAnnexUpload.index.676543-0": "该二维码十五分钟内有效",
518
- "AttchmentInfo.userShow": "对业户开放"
518
+ "AttchmentInfo.userShow": "对业户开放",
519
+ "PreviewFile.loading": "加载中...",
520
+ "PreviewFile.printModal.title": "温馨提示",
521
+ "PreviewFile.printModal.noticeStart": "您当前没有打开网页打印插件,请先打开或者",
522
+ "PreviewFile.printModal.download": "点击下载",
523
+ "PreviewFile.printModal.noticeEnd": "。电脑需要同时安装微软word软件或者wps软件,才能使用。",
524
+ "PreviewFile.printModal.ok": "好的",
525
+ "PreviewFile.printModal.success": "发送成功",
526
+ "PreviewFile.download": "文档下载",
527
+ "PreviewFile.print": "文档打印",
528
+ "PreviewFile.defaultTitle": "预览文档"
519
529
  }