Forráskód Böngészése

⚙️ 修改上传图片,编辑器细节问题

快乐的梦鱼 2 napja
szülő
commit
625bc961c3

+ 19 - 0
src/api/collect/AssessmentContent.ts

@@ -531,6 +531,25 @@ export class AssessmentContentApi extends AppServerRequestModule<DataModel> {
   }
 
   /**
+   * 下载传承协议PDF
+   */
+  async downloadAgreementPdf(id: number) {
+    return new Promise<string>((resolve, reject) => {
+      uni.downloadFile({
+        url: `${this.config.baseUrl}/pdf/nationalContract?id=${id}`,
+        success: (res) => {
+          if (res.statusCode !== 200)
+            throw new Error('下载失败,状态码:' + res.statusCode);
+          resolve(res.tempFilePath);
+        },
+        fail: (err) => {
+          reject(err);
+        },
+      });
+    });
+  }
+
+  /**
    * 证明材料修改与新增
    * POST `/ich/check/saveAnnex`
    */

+ 9 - 0
src/common/components/dynamicf/ComponentRender.vue

@@ -9,6 +9,14 @@
       v-bind="params"
     />
   </template>
+  <template v-if="item.type === 'richtext2'">
+    <RichTextEditor2
+      ref="itemRef"
+      :modelValue="modelValue"
+      @update:modelValue="onValueChanged"
+      v-bind="params"
+    />
+  </template>
   <template v-else-if="item.type === 'recorder'">
     <Recorder
       ref="itemRef"
@@ -28,6 +36,7 @@ import { ref, type PropType } from 'vue';
 import type { IDynamicFormItem } from '@/components/dynamic';
 import RichTextEditor from '@/common/components/form/RichTextEditor.vue';
 import Recorder from '@/common/components/form/Recorder.vue';
+import RichTextEditor2 from '../form/RichTextEditor2.vue';
 
 const props = defineProps({	
   modelValue: {

+ 154 - 0
src/common/components/form/RichTextEditor2.vue

@@ -0,0 +1,154 @@
+<template>
+  <Popup 
+    :show="show"
+    position="bottom"
+    size="90vh"
+  >
+    <ActionSheetTitle 
+      title="编辑" 
+      cancelText="取消"
+      confirmText="保存"
+      @cancel="cancel" 
+      @confirm="save" 
+    />
+    <sp-editor
+      :toolbar-config="{
+        excludeKeys: ['direction', 'date', 'lineHeight', 'letterSpacing', 'listCheck'],
+        iconSize: '18px'
+      }"
+      @init="initEditor"
+      @input="inputOver"
+      @upinImage="upinImage"
+      @overMax="overMax"
+    ></sp-editor>
+  </Popup>
+  <view class="d-flex flex-col">
+    <view class="richtext-preview-box" @click.stop="edit">
+      <Parse v-if="modelValue" :content="modelValue" contentStyle="max-height:400px;overflow:hidden;" />
+      <Text v-else color="text.second">{{placeholder}}</Text>
+      <div class="richtext-preview-box-fade"></div>
+    </view>
+    <view class="d-flex flex-row gap-sss align-center mt-2">
+      <Button icon="edit" text="编辑" size="small" type="primary" @click.stop="edit" />
+    </view>
+  </view>
+</template>
+
+<script setup lang="ts">
+import Popup from '@/components/dialog/Popup.vue';
+import spEditor from '@/uni_modules/sp-editor/components/sp-editor/sp-editor.vue';
+import ActionSheetTitle from '@/components/dialog/ActionSheetTitle.vue';
+import { ref } from 'vue';
+import { confirm, toast } from '@/components/utils/DialogAction';
+import CommonContent from '@/api/CommonContent';
+import Parse from '@/components/display/parse/Parse.vue';
+import Text from '@/components/basic/Text.vue';
+import Button from '@/components/basic/Button.vue';
+
+const show = ref(false);
+
+const props = defineProps({	
+  modelValue: { 
+    type: String,
+    default: null 
+  },
+  maxLength: {
+    type: Number,
+    default: -1,
+  },
+  placeholder: {
+    type: String,
+    default: '未编写内容,点击编写',
+  },
+})
+const emit = defineEmits(['update:modelValue'])
+
+function edit() {
+  show.value = true;
+  setTimeout(() => {
+
+  }, 200);
+}
+function cancel() {
+  confirm({
+    title: '提示',
+    content: '是否放弃编辑?',
+  }).then((res) => {
+    if (res)
+      show.value = false;
+  })
+}
+function save() {
+  emit('update:modelValue', currentContent);
+  show.value = false;
+}
+
+let currentContent = '';
+let currentEditor: any = null;
+
+/**
+* 获取输入内容
+*/
+function inputOver(e: { html: string; text: string; }) {
+  // 可以在此处获取到编辑器已编辑的内容
+  currentContent = e.html;
+}
+/**
+ * 超出最大内容限制
+ * @param {Object} e {html,text} 内容的html文本,和text文本
+ */
+function overMax(e: { html: string; text: string; }) {
+  // 若设置了最大字数限制,可在此处触发超出限制的回调
+  console.log('==== overMax :', e)
+}
+function initEditor(editor: any) {
+  editor.setContents({
+    html: props.modelValue
+  })
+}
+/**
+ * 直接运行示例工程插入图片无法正常显示的看这里
+ * 因为插件默认采用云端存储图片的方式
+ * 以$emit('upinImage', tempFiles, this.editorCtx)的方式回调
+ * @param {Object} tempFiles
+ * @param {Object} editorCtx
+ */
+function upinImage(tempFiles: any, editorCtx: any) {
+  CommonContent.uploadFile(
+  // #ifdef MP-WEIXIN
+  // 注意微信小程序的图片路径是在tempFilePath字段中
+    tempFiles[0].tempFilePath
+  // #endif
+  // #ifndef MP-WEIXIN
+    tempFiles[0].path
+    // #endif
+    , 'image', 'file').then((res) => {
+      editorCtx.insertImage({
+        src: res.fullurl,
+        width: '80%', // 默认不建议铺满宽度100%,预留一点空隙以便用户编辑
+        success: function () {}
+      })
+    }).catch((err) => {
+      toast('上传图片失败')
+      console.error('uploadFile error', err);
+    });
+    return;
+}
+</script>
+
+<style lang="scss" scoped>
+.richtext-preview-box {
+  position: relative;
+  flex: 1;
+  min-height: 400rpx;
+
+  .richtext-preview-box-fade {
+    position: absolute;
+    bottom: 0;
+    left: 0;
+    width: 100%;
+    height: 100rpx;
+    background: linear-gradient(to bottom, rgba(255, 255, 255, 0), rgba(255, 255, 255, 1));
+  }
+}
+</style>

+ 6 - 4
src/common/components/upload/AliOssUploadCo.ts

@@ -62,10 +62,12 @@ function uploadOSS(uploadPath: string, filePath: string, cancelHandler: { cancel
         if (res.statusCode === 200 || res.statusCode === 204) {
           resolve(`https://${client.bucket}.${client.region}.aliyuncs.com/${uploadPath}`)
         } else {
-          reject(new Error('上传失败' + res.statusCode))
+          reject('上传失败' + res.statusCode)
         }
       },
-      fail: reject,
+      fail: (e) => {
+        reject('上传失败:' + e.errMsg)
+      },
       complete: () => {}
     })
     task.onProgressUpdate(({ progress }) => onProgress?.(progress));
@@ -81,8 +83,8 @@ export function useAliOssUploadCo(
   onFinish?: (res: string, item: UploaderItem) => void
 ) {
   return (action: UploaderAction) => {
-    const name = StringUtils.path.getFileName(action.item.filePath);
-    const uploadPath = `${subPath}/${name.split('.')[0]}-${RandomUtils.genNonDuplicateID(6)}.${StringUtils.path.getFileExt(name)}`;  
+    const name = action.item.name || StringUtils.path.getFileName(action.item.filePath);
+    const uploadPath = `${subPath}/${name.split('.')[0]}-${RandomUtils.genNonDuplicateID(6)}.${StringUtils.path.getFileExt(name)}`;     
     const cancelHandler: { cancel: () => void } = {
       cancel: () => {},
     };    

+ 8 - 1
src/components/form/Uploader.ts

@@ -1,5 +1,11 @@
+import { StringUtils } from "@imengyu/imengyu-utils";
+
 export interface UploaderItem {
   /**
+   * 文件名称
+   */
+  name: string;
+  /**
    * 上传文件源路径
    */
   filePath: string;
@@ -74,8 +80,9 @@ export interface UploaderAction {
   }, message?: string) => void;
 }
 
-export function stringUrlToUploaderItem(url: string): UploaderItem {
+export function stringUrlToUploaderItem(url: string, name?: string): UploaderItem {
   return {
+    name: name || StringUtils.path.getFileName(url) || '',
     filePath: url,
     uploadedPath: url,
     state: 'success',

+ 30 - 6
src/components/form/Uploader.vue

@@ -75,7 +75,7 @@
 <script setup lang="ts">
 import { computed, reactive, ref } from 'vue';
 import { propGetThemeVar, useTheme, type TextStyle, type ViewStyle } from '../theme/ThemeDefine';
-import { Debounce, LogUtils } from '@imengyu/imengyu-utils';
+import { Debounce, LogUtils, StringUtils } from '@imengyu/imengyu-utils';
 import { actionSheet } from '../dialog/CommonRoot';
 import type { ToastInstance } from '../feedback/Toast.vue';
 import type { UploaderAction, UploaderItem } from './Uploader';
@@ -330,6 +330,7 @@ function getUploadItemDisplayKind(item: UploaderItem): UploadDisplayKind {
 
 function makeUploadTitleItem(label: string): UploaderItem {
   return {
+    name: label,
     filePath: label,
     isTitle: true,
     state: 'success',
@@ -381,6 +382,7 @@ function onUploadPress() {
       function handleFiles(res: {
         path: string;
         size: number;
+        name: string;
       }[]) {
         resolve(res.map((item) => {
           let isImage = typeof (item as any).type === 'string' ? (item as any).type.startsWith('image/') : false;
@@ -388,6 +390,7 @@ function onUploadPress() {
             isImage = isImagePath(item.path);
           }
           return {
+            name: item.name,
             filePath: item.path,
             previewPath: item.path,
             size: item.size,
@@ -420,11 +423,11 @@ function onUploadPress() {
             chooseLocal();
           } else if (index === 1) {
             uni.chooseMessageFile({
-              type: props.chooseType || 'all',
+              type: props.chooseType === 'file' ? 'all' :(props.chooseType || 'all'),
               count: props.maxUploadCount - currentUpladList.value.length,
               success: (res) => {
                 LogUtils.printLog(TAG, 'info', 'chooseMessageFile', res);
-                handleFiles(res.tempFiles as { path: string; size: number; }[])
+                handleFiles(res.tempFiles as { path: string; name: string; size: number; }[])
               },
               fail: (e) => {
                 LogUtils.printLog(TAG, 'error', 'chooseMessageFile', e);
@@ -447,18 +450,29 @@ function onUploadPress() {
             uni.chooseVideo().then((res) => handleFiles([
               {
                 path: res.tempFilePath,
+                name: res.name || StringUtils.path.getFileName(res.tempFilePath) || '',
                 size: res.size,
               }
             ])).catch(reject);
             break;
+          //#ifdef H5
           case 'file':
-            uni.chooseFile().then((res) => handleFiles(res.tempFiles as { path: string; size: number; }[])).catch(reject);
+            uni.chooseFile().then((res) => handleFiles((res.tempFiles as any []).map((item) => ({
+              path: item.path,
+              name: item.name || StringUtils.path.getFileName(item.path) || '',
+              size: item.size,
+            })))).catch(reject);
             break;
+          //#endif
           default:
           case 'image':
             uni.chooseImage({
               count: props.maxUploadCount - currentUpladList.value.length,
-            }).then((res) => handleFiles(res.tempFiles as { path: string; size: number; }[])).catch(reject);
+            }).then((res) => handleFiles((res.tempFiles as any[]).map((item) => ({
+              path: item.path,
+              name: item.name || StringUtils.path.getFileName(item.path) || '',
+              size: item.size,
+            })))).catch(reject);
             break;
         }
       }
@@ -562,6 +576,16 @@ function deleteListItem(item: UploaderItem) {
   }
 }
 
+function formatError(error: unknown) {
+  if (error instanceof Error)
+    return error.message;
+  if (typeof error === 'string')
+    return error;
+  if (typeof error === 'object')
+    return JSON.stringify(error);
+  return '' + error;
+}
+
 //开始上传条目
 function startUploadItem(item: UploaderItem) {
   if (item.state === 'uploading')
@@ -584,7 +608,7 @@ function startUploadItem(item: UploaderItem) {
       item,
       onError(error) {
         item.state = 'fail';
-        item.message = ('' + error) || '上传失败';
+        item.message = formatError(error) || '上传失败';
         updateListItem(item);
         reject(error);
         LogUtils.printLog(TAG, 'error', `上传文件 ${item.filePath} 失败,错误信息:${error}`);

+ 1 - 1
src/components/form/UploaderListItem.vue

@@ -59,7 +59,7 @@
     <FlexRow v-if="isListStyle" :flex="1" align="center">
       <Width :size="20" />
       <FlexCol :flex="1">
-        <Text :fontSize="26" :text="StringUtils.path.getFileName(item.filePath)" wrap wordBreak="break-all" />
+        <Text :fontSize="26" :text="item.name" wrap wordBreak="break-all" />
         <Text :fontSize="22" :text="item.message" wrap wordBreak="break-all" />
         <Height :size="10" /> 
         <Progress :progressColor="selectStyleType(item.state, 'notstart', {

+ 17 - 19
src/pages/article/editor/editor.vue

@@ -21,11 +21,12 @@
 
 <script setup lang="ts">
 import { showError } from '@/common/composeabe/ErrorDisplay';
-import { confirm } from '@/components/utils/DialogAction';
+import { confirm, toast } from '@/components/utils/DialogAction';
 import { back, backAndCallOnPageBack } from '@/components/utils/PageAction';
 import Button from '@/components/basic/Button.vue';
 import XBarSpace from '@/components/layout/space/XBarSpace.vue';
 import spEditor from '@/uni_modules/sp-editor/components/sp-editor/sp-editor.vue';
+import CommonContent from '@/api/CommonContent';
 
 function cancel() {
   confirm({
@@ -82,27 +83,24 @@ function initEditor(editor: any) {
  * @param {Object} editorCtx
  */
 function upinImage(tempFiles: any, editorCtx: any) {
-  /**
-   * 本地临时插入图片预览
-   * 注意:这里仅是示例本地图片预览,因为需要将图片先上传到云端,再将图片插入到编辑器中
-   * 正式开发时,还请将此处注释,并解开下面 使用 uniCloud.uploadFile 上传图片的示例方法 的注释
-   * @tutorial https://uniapp.dcloud.net.cn/api/media/editor-context.html#editorcontext-insertimage
-   */
+  CommonContent.uploadFile(
   // #ifdef MP-WEIXIN
   // 注意微信小程序的图片路径是在tempFilePath字段中
-  editorCtx.insertImage({
-    src: tempFiles[0].tempFilePath,
-    width: '80%', // 默认不建议铺满宽度100%,预留一点空隙以便用户编辑
-    success: function () {}
-  })
+    tempFiles[0].tempFilePath
   // #endif
-
   // #ifndef MP-WEIXIN
-  editorCtx.insertImage({
-    src: tempFiles[0].path,
-    width: '80%', // 默认不建议铺满宽度100%,预留一点空隙以便用户编辑
-    success: function () {}
-  })
-  // #endif
+    tempFiles[0].path
+    // #endif
+    , 'image', 'file').then((res) => {
+      editorCtx.insertImage({
+        src: res.fullurl,
+        width: '80%', // 默认不建议铺满宽度100%,预留一点空隙以便用户编辑
+        success: function () {}
+      })
+    }).catch((err) => {
+      toast('上传图片失败')
+      console.error('uploadFile error', err);
+    });
+    return;
 }
 </script>

+ 13 - 10
src/pages/collect/assessment/argeement-sign.vue

@@ -268,20 +268,23 @@ async function saveAgreement() {
   submitLoading.value = false;
 }
 async function downloadAgreement() {
-  alert({
-    title: '抱歉',
-    content: '抱歉,暂未开放导出功能,有需要打印传承协议的请联系客服,我们会为您打印。',
-  });
-  /*
-  const detail = currentAgreement.value;
+  if (!currentAgreement.value?.id) {
+    toast('请先保存评估表后再下载PDF');
+    return;
+  }
   try {
-    assertNotNull(detail, 'currentAgreement is null');
-    throw new Error('抱歉');
+    assertNotNull(currentAgreement.value, 'currentForm is null');
+    const pdfPath = await AssessmentContentApi.downloadAgreementPdf(currentAgreement.value.id);
+    uni.openDocument({
+      filePath: pdfPath,
+      fileType: 'pdf',
+      showMenu: true,
+    });
   } catch (error) {
     alert({
-      title: '导出失败',
+      title: '下载评估表失败',
       content: formatError(error),
     });
-  }*/
+  }
 }
 </script>

+ 15 - 11
src/pages/collect/assessment/evaluation-form.vue

@@ -26,8 +26,8 @@
             <Divider />
             <FlexCol gap="gap.lg">
               <FlexCol v-for="(title, secIdx) in externalReviewSectionTitles" :key="secIdx" gap="gap.sm">
-                <Text bold :text="title" />
-                <Text text="评分:(待终审填写)" />
+                <Text bold :text="title.title" />
+                <Field v-model="title.suggestion" :disabled="title.disabled" placeholder="(待终审填写)" />
                 <FlexRow wrap align="center" gap="gap.md">
                   <CheckBox
                     v-for="(label, i) in externalReviewScoreRow1"
@@ -61,9 +61,10 @@
               v-else
               ref="uploaderRef"
               :upload="assessmentAnnexUpload"
-              :max-upload-count="9"
+              :max-upload-count="100"
               :max-file-size="20 * 1024 * 1024"
               :group-type="true"
+              chooseType="file"
               list-type="list"
             />
             <Height :height="30" />
@@ -113,13 +114,14 @@ import { getMimeType } from '@/common/components/upload/mimes';
 import Divider from '@/components/display/Divider.vue';
 import { stringUrlToUploaderItem } from '@/components/form/Uploader';
 import CheckBox from '@/components/form/CheckBox.vue';
+import Field from '@/components/form/Field.vue';
 
 /** 评估表下方展示用:各级审核意见(不接数据、禁用) */
-const externalReviewSectionTitles = [
-  '1. 项目保护单位意见',
-  '2. 县(区)文旅部门审核意见',
-  '3. 设区市文旅部门、省非遗中心审核意见',
-] as const;
+const externalReviewSectionTitles = ref([
+  { title: '1. 项目保护单位意见', suggestion: '', disabled: false },
+  { title: '2. 县(区)文旅部门审核意见', suggestion: '', disabled: true },
+  { title: '3. 设区市文旅部门、省非遗中心审核意见', suggestion: '', disabled: true },
+]);
 const externalReviewScoreRow1 = ['优秀', '合格', '不合格'] as const;
 const externalReviewScoreRow2 = ['丧失传承能力', '取消资格'] as const;
 
@@ -143,7 +145,7 @@ const assessmentAnnexUpload = useAliOssUploadCo('assessment/annex', async (res,
   assertNotNull(currentForm.value, 'currentForm is null');
   const mimetype = getMimeType(item.filePath);
   await AssessmentContentApi.saveAnnex({
-    name: '佐证资料' + StringUtils.path.getFileName(item.filePath),
+    name: item.name,
     formId: currentForm.value.id,
     url: res,
     type: getCheckAnnexType(mimetype),
@@ -235,7 +237,7 @@ const formOptions : IDynamicFormOptions = {
         {
           label: '自评报告',
           name: 'content',
-          type: 'textarea',
+          type: 'richtext2',
           additionalProps: { 
             placeholder: '请填写自评报告',
             rows: 10,
@@ -363,7 +365,9 @@ async function loadAnnexList() {
   console.log('awardTime', currentForm.value.awardTime);
   const annexList = await AssessmentContentApi.getAnnexList(currentForm.value.id);
   setTimeout(() => {
-    uploaderRef.value!.setList(annexList.data.map((item) => stringUrlToUploaderItem(item.url)));
+    if (uploaderRef.value) {  
+      uploaderRef.value.setList(annexList.data.map((item) => stringUrlToUploaderItem(item.url, item.name)));
+    }
   }, 1000);
 }