Browse Source

消息修改。

jiaxiaoqiang 8 months ago
parent
commit
e360127102

+ 3 - 0
.env.production

@@ -1,6 +1,9 @@
 ## 生产环境
 NODE_ENV='production'
 
+# 上传文件接口地址
+VITE_APP_UPLOAD_URL = ''
+
 # 代理前缀
 VITE_APP_BASE_API = '/prod-api'
 

+ 1 - 1
public/version.json

@@ -1,3 +1,3 @@
 {
-  "version": "2.6"
+  "version": "2.7"
 }

+ 7 - 2
src/api/file/index.ts

@@ -7,11 +7,16 @@ import { FileInfo } from "./types";
  *
  * @param file
  */
-export function uploadFileApi(file: File): AxiosPromise<FileInfo> {
+export function uploadFileApi(
+  file: File,
+  generatePdf: boolean = false
+): AxiosPromise<FileInfo> {
   const formData = new FormData();
   formData.append("file", file);
+  formData.append("fileName", file.name);
+  formData.append("generatePdf", generatePdf);
   return request({
-    url: "/api/v1/files",
+    url: "/api/v1/base/upload",
     method: "post",
     data: formData,
     headers: {

+ 8 - 0
src/api/message/index.ts

@@ -16,3 +16,11 @@ export function getUserMessageList(type: string): AxiosPromise {
     },
   });
 }
+
+// 获取消息详情
+export function getMessageDetail(id: string): AxiosPromise {
+  return request({
+    url: `/api/v1/sys/message/get/${id}`,
+    method: "get",
+  });
+}

+ 1 - 0
src/api/user/types.ts

@@ -12,6 +12,7 @@ export interface UserInfo {
   sysData: any[];
   canSetPermission: boolean; //是否有设置权限的权限
   canSetIP: boolean; //是否有设置IP的权限
+  canCreateMessage: boolean; //是否有创建消息的权限
 }
 
 /**

+ 19 - 0
src/components/TopTitle.vue

@@ -3,14 +3,33 @@
     <svg-icon :icon-class="icon" size="26" />
 
     <div class="title">{{ title }}</div>
+    <i-ep-plus
+      v-if="messageType && userStore.user.canCreateMessage"
+      style="margin-left: 8px"
+      @click="gotoList"
+    />
   </div>
 </template>
 
 <script lang="ts" setup>
+import { useUserStore } from "@/store";
+
 const props = defineProps<{
   title: string;
   icon: string;
+  messageType: {
+    type: string;
+    default: "";
+    required: false;
+  };
 }>();
+
+const router = useRouter();
+const userStore = useUserStore();
+const gotoList = () => {
+  console.log("gotoList", props.messageType);
+  router.push({ name: "messagesList", params: { type: props.messageType } });
+};
 </script>
 
 <style lang="scss" scoped>

+ 158 - 0
src/components/Upload/FilesUpload.vue

@@ -0,0 +1,158 @@
+<template>
+  <el-upload
+    v-model:file-list="fileList"
+    class="upload-demo"
+    :on-change="handleChange"
+    :limit="limit"
+    :auto-upload="false"
+    :show-file-list="false"
+  >
+    <el-button
+      type="primary"
+      :loading="loading"
+      :disabled="fileList.length >= limit"
+      >点击上传文件</el-button
+    >
+    <template #tip>
+      <div class="tip" v-if="showTip">
+        文件类型限制为.jpg,.jpeg,.png,word文档,pdf,大小不超过{{ size }}M
+      </div>
+    </template>
+    <div>
+      <el-tag
+        class="file-item"
+        v-for="(file, index) in fileList"
+        :key="file.name"
+        closable
+        type="success"
+        @close="deleteFile(index)"
+        @click.stop.prevent="handlePreview(index)"
+      >
+        {{ file.name }}
+      </el-tag>
+    </div>
+  </el-upload>
+  <el-drawer
+    v-model="PDFVisible"
+    :footer="false"
+    :header="false"
+    :show-close="false"
+    destroy-on-close
+    direction="rtl"
+    :append-to-body="true"
+    size="972px"
+  >
+    <VuePdfEmbed :source="pdfSource" annotation-layer text-layer />
+  </el-drawer>
+</template>
+<script lang="ts" setup>
+import { ref } from "vue";
+import { UploadFile, UploadFiles, UploadUserFile } from "element-plus";
+import { uploadFileApi } from "@/api/file";
+import PDFView from "@/components/PDFView/index.vue";
+import VuePdfEmbed from "vue-pdf-embed";
+
+const props = defineProps({
+  size: {
+    type: Number,
+    default: 4,
+  },
+  limit: {
+    type: Number,
+    default: 1,
+  },
+  src: {
+    type: String,
+    default: "",
+  },
+  srcList: {
+    type: Array<string>,
+    default: () => [],
+  },
+  pdfList: {
+    type: Array<string>,
+    default: () => [],
+  },
+  fileNameList: {
+    type: Array<string>,
+    default: () => [],
+  },
+  generatePdf: {
+    type: Boolean,
+    default: false,
+  },
+  showTip: {
+    type: Boolean,
+    default: true,
+  },
+});
+
+const loading = ref(false);
+
+const emit = defineEmits([
+  "update:src",
+  "update:srcList",
+  "update:pdfList",
+  "update:fileNameList",
+  "finished",
+]);
+
+// 上传文件成功返回的值
+const src = useVModel(props, "src", emit); //单文件用这个
+const srcList = useVModel(props, "srcList", emit); //多文件用这个
+const pdfList = useVModel(props, "pdfList", emit); //转换成pdf后的多文件用这个
+const fileNameList = useVModel(props, "fileNameList", emit); //多文件上传要显示文件名
+
+// el-upload 绑定的值
+const fileList = ref<UploadUserFile[]>([]);
+
+const handleChange = async (uploadFile: UploadFile) => {
+  if (uploadFile.size! > props.size * 1048 * 1048) {
+    ElMessage.warning(`上传文件不能大于${props.size}M`);
+    return;
+  }
+  loading.value = true;
+  uploadFileApi(uploadFile.raw as File, props.generatePdf)
+    .then((res: any) => {
+      loading.value = false;
+      src.value = res.data.fileUrl;
+      pdfList.value.push(res.data.pdfUrl);
+      srcList.value.push(res.data.fileUrl);
+      fileNameList.value.push(res.data.fileName);
+      emit("finished");
+    })
+    .catch((err) => {
+      ElMessage.error(err.message);
+      loading.value = false;
+    });
+};
+
+const deleteFile = (index: number) => {
+  src.value = ""; //删除直接清空src即可,不用考虑是哪个,因为单文件不会有多个
+  fileList.value.splice(index, 1);
+  srcList.value.splice(index, 1);
+  pdfList.value.splice(index, 1);
+  fileNameList.value.splice(index, 1);
+};
+
+const PDFVisible = ref(false);
+const pdfSource = ref("");
+const handlePreview = (index: number) => {
+  if (srcList.value.length > index && props.generatePdf) {
+    pdfSource.value =
+      import.meta.env.VITE_APP_UPLOAD_URL + pdfList.value[index];
+    PDFVisible.value = true;
+  }
+};
+</script>
+
+<style scoped lang="scss">
+.tip {
+  font-size: 12px;
+  color: #e6a23c;
+  margin-top: 5px;
+}
+.file-item {
+  margin-left: 10px;
+}
+</style>

+ 67 - 79
src/components/Upload/MultiUpload.vue

@@ -1,36 +1,35 @@
-<!--
-  多图上传组件
-  @author: youlaitech
-  @date 2022/11/20
--->
-
 <template>
-  <el-upload
-    v-model:file-list="fileList"
-    list-type="picture-card"
-    :before-upload="handleBeforeUpload"
-    :http-request="handleUpload"
-    :on-remove="handleRemove"
-    :on-preview="previewImg"
-    :limit="props.limit"
-  >
-    <i-ep-plus />
-  </el-upload>
+  <div class="upload-container">
+    <el-upload
+      v-model:file-list="fileList"
+      :before-upload="handleBeforeUpload"
+      :http-request="handleUpload"
+      :limit="limit"
+      :show-file-list="false"
+      class="img-box"
+      list-type="picture-card"
+    >
+      <i-ep-plus />
+    </el-upload>
+    <div v-for="(item, index) in fileList" :key="index" class="img-container">
+      <img :src="item.url" class="img-box" object-fit="cover" />
+      <Delete class="delete-icon" @click="deleteImg(index)" />
+    </div>
+  </div>
 
   <el-dialog v-model="dialogVisible">
-    <img w-full :src="previewImgUrl" alt="Preview Image" />
+    <img :src="previewImgUrl" alt="Preview Image" w-full />
   </el-dialog>
 </template>
 
-<script setup lang="ts">
+<script lang="ts" setup>
 import {
   UploadRawFile,
   UploadRequestOptions,
   UploadUserFile,
-  UploadFile,
-  UploadProps,
 } from "element-plus";
-import { uploadFileApi, deleteFileApi } from "@/api/file";
+import { Delete } from "@element-plus/icons-vue";
+import { uploadFileApi } from "@/api/file";
 
 const emit = defineEmits(["update:modelValue"]);
 
@@ -55,26 +54,7 @@ const previewImgUrl = ref("");
 const dialogVisible = ref(false);
 
 const fileList = ref([] as UploadUserFile[]);
-watch(
-  () => props.modelValue,
-  (newVal: string[]) => {
-    const filePaths = fileList.value.map((file) => file.url);
-    // 监听modelValue文件集合值未变化时,跳过赋值
-    if (
-      filePaths.length > 0 &&
-      filePaths.length === newVal.length &&
-      filePaths.every((x) => newVal.some((y) => y === x)) &&
-      newVal.every((y) => filePaths.some((x) => x === y))
-    ) {
-      return;
-    }
-
-    fileList.value = newVal.map((filePath) => {
-      return { url: filePath } as UploadUserFile;
-    });
-  },
-  { immediate: true }
-);
+const urlList = ref([] as string[]); // 上传成功后的图片路径集合,要传给父组件
 
 /**
  * 自定义图片上传
@@ -83,39 +63,9 @@ watch(
  */
 async function handleUpload(options: UploadRequestOptions): Promise<any> {
   // 上传API调用
-  const { data: fileInfo } = await uploadFileApi(options.file);
-
-  // 上传成功需手动替换文件路径为远程URL,否则图片地址为预览地址 blob:http://
-  const fileIndex = fileList.value.findIndex(
-    (file) => file.uid == (options.file as any).uid
-  );
-
-  fileList.value.splice(fileIndex, 1, {
-    name: fileInfo.name,
-    url: fileInfo.url,
-  } as UploadUserFile);
-
-  emit(
-    "update:modelValue",
-    fileList.value.map((file) => file.url)
-  );
-}
-
-/**
- * 删除图片
- */
-function handleRemove(removeFile: UploadFile) {
-  const filePath = removeFile.url;
-
-  if (filePath) {
-    deleteFileApi(filePath).then(() => {
-      // 删除成功回调
-      emit(
-        "update:modelValue",
-        fileList.value.map((file) => file.url)
-      );
-    });
-  }
+  const res: any = await uploadFileApi(options.file);
+  urlList.value.push(res.data.fileUrl);
+  emit("update:modelValue", urlList.value);
 }
 
 /**
@@ -129,11 +79,49 @@ function handleBeforeUpload(file: UploadRawFile) {
   return true;
 }
 
+const deleteImg = (index: number) => {
+  fileList.value.splice(index, 1);
+  urlList.value.splice(index, 1);
+  emit("update:modelValue", urlList.value);
+};
+
 /**
  * 预览图片
  */
-const previewImg: UploadProps["onPreview"] = (uploadFile) => {
-  previewImgUrl.value = uploadFile.url!;
-  dialogVisible.value = true;
-};
+// const previewImg: UploadProps["onPreview"] = (uploadFile) => {
+//   previewImgUrl.value = uploadFile.url!;
+//   dialogVisible.value = true;
+// };
 </script>
+
+<style lang="scss" scoped>
+.upload-container {
+  display: flex;
+  justify-content: start;
+  flex-wrap: wrap;
+}
+
+.img-container {
+  position: relative;
+}
+
+.img-box {
+  width: 148px;
+  height: 148px;
+  display: block;
+  margin-right: 10px;
+  margin-top: 10px;
+  border-radius: 4px;
+}
+
+.delete-icon {
+  position: absolute;
+  width: 20px;
+  height: 20px;
+  left: 50%;
+  top: 50%;
+  transform: translate(-50%, -50%);
+  color: red;
+  cursor: pointer;
+}
+</style>

+ 2 - 0
src/store/modules/user.ts

@@ -63,6 +63,8 @@ export const useUserStore = defineStore("user", () => {
                 user.value.canSetPermission = true;
               } else if (menu.id === 7) {
                 user.value.canSetIP = true;
+              } else if (menu.id === 8) {
+                user.value.canCreateMessage = true;
               }
             }
           });

+ 13 - 6
src/views/main/message.vue

@@ -1,19 +1,26 @@
 <template>
   <div class="common-box" style="margin-top: 20px">
-    <TopTitle :title="title" icon="laba" />
-    <div class="list-box" @click="gotoList">
+    <TopTitle :messageType="messageType" :title="title" icon="laba" />
+    <div class="list-box">
       <el-scrollbar>
-        <div v-for="(item, index) in dataList" :key="index" class="line">
+        <div
+          v-for="(item, index) in dataList"
+          :key="index"
+          class="line"
+          @click="showDetail(item)"
+        >
           <div class="title">{{ item.title }}</div>
           <div class="date">{{ item.created }}</div>
         </div>
       </el-scrollbar>
     </div>
+    <MessageDetail ref="messageDetailRef" />
   </div>
 </template>
 
 <script lang="ts" setup>
 import { getUserMessageList } from "@/api/message";
+import MessageDetail from "@/views/messages/MessageDetail.vue";
 
 const props = defineProps({
   // 消息类型 0 系统公告 1 工位消息 2-具体某些人员消息
@@ -35,9 +42,9 @@ onMounted(() => {
   });
 });
 
-const router = useRouter();
-const gotoList = () => {
-  router.push({ name: "messagesList", params: { type: props.messageType } });
+const messageDetailRef = ref<InstanceType<typeof MessageDetail>>();
+const showDetail = (item: any) => {
+  messageDetailRef.value?.handleOpenDrawer(item.id);
 };
 </script>
 

+ 109 - 0
src/views/messages/MessageDetail.vue

@@ -0,0 +1,109 @@
+<template>
+  <el-drawer
+    v-model="drawerVisible"
+    :destroy-on-close="true"
+    :with-header="false"
+    size="800"
+    title="消息详情"
+  >
+    <div class="message-detail">
+      <h3>{{ messageObj.title }}</h3>
+      <p>{{ messageObj.content }}</p>
+      <div class="img-container">
+        <img
+          v-for="(item, index) in messageObj.photoLists"
+          :key="index"
+          :src="getImgUrl(item)"
+          class="img-box"
+          object-fit="cover"
+        />
+      </div>
+      <div
+        v-if="messageObj?.fileLists?.length > 0"
+        class="message-time download-btn"
+        @click="downloadFile(messageObj.fileLists[0])"
+      >
+        相关附件下载
+      </div>
+
+      <div class="message-time">创建时间: {{ messageObj.created }}</div>
+    </div>
+  </el-drawer>
+</template>
+
+<script lang="ts" setup>
+import { getMessageDetail } from "@/api/message";
+
+const drawerVisible = ref(false);
+
+const messageObj = ref<any>({});
+
+const getImgUrl = (url: string) => {
+  return `${import.meta.env.VITE_APP_UPLOAD_URL}${url}`;
+};
+//192.168.101.4:9000/jgfile/2024/10/30/5175213819500.jpg
+const handleOpenDrawer = (id: string) => {
+  drawerVisible.value = true;
+  getMessageDetail(id).then((res) => {
+    messageObj.value = res.data;
+  });
+};
+
+const handleCloseDrawer = () => {
+  drawerVisible.value = false;
+};
+
+const downloadFile = (url: string) => {
+  // 根据链接获取后缀名
+  const suffix = url.split(".").pop();
+
+  const link = document.createElement("a"); // 创建一个链接元素
+  // link.href = url;
+  link.href = import.meta.env.VITE_APP_UPLOAD_URL + url;
+  link.setAttribute("download", `${messageObj.value.title}.${suffix}`); // 可以设置你要下载的文件名和扩展名
+  document.body.appendChild(link);
+  link.click(); // 模拟点击下载
+  link.remove(); // 下载后移除链接元素
+};
+
+defineExpose({ handleOpenDrawer, handleCloseDrawer });
+</script>
+
+<style lang="scss" scoped>
+.message-detail {
+  padding: 20px;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+}
+
+.img-container {
+  display: flex;
+  flex-wrap: wrap;
+  width: 100%;
+  margin-top: 20px;
+  justify-content: start;
+}
+
+.img-box {
+  width: 200px;
+  height: 200px;
+  display: block;
+  margin-right: 10px;
+  margin-top: 10px;
+  border-radius: 4px;
+}
+
+.download-btn {
+  cursor: pointer;
+}
+
+.message-time {
+  font-size: 12px;
+  color: #999;
+  width: 100%;
+  text-align: start;
+  margin-top: 20px;
+}
+</style>

+ 26 - 5
src/views/messages/index.vue

@@ -16,6 +16,12 @@
       @current-change="dataList"
       @selection-change="selectionChange"
     >
+      <template #photoLists-form>
+        <MultiUpload v-model="form.photoLists" />
+      </template>
+      <template #fileLists-form>
+        <FilesUpload v-model:src-list="form.fileLists" :limit="1" />
+      </template>
       <template #menu-left="{ size }">
         <el-button
           :disabled="toDeleteIds.length < 1"
@@ -41,6 +47,8 @@
 import { ref } from "vue";
 import { useCrud } from "@/hooks/userCrud";
 import { useDictionaryStore } from "@/store";
+import MultiUpload from "@/components/Upload/MultiUpload.vue";
+import FilesUpload from "@/components/Upload/FilesUpload.vue";
 
 const userTableRef = ref(null);
 
@@ -150,6 +158,13 @@ option.value = Object.assign(option.value, {
       value: "10",
     },
     {
+      label: "消息内容",
+      prop: "content",
+      search: true,
+      type: "textarea",
+      overHidden: true,
+    },
+    {
       label: "消息类型",
       prop: "type",
       search: false,
@@ -166,12 +181,18 @@ option.value = Object.assign(option.value, {
       ],
       value: "0",
     },
+
     {
-      label: "消息内容",
-      prop: "content",
-      search: true,
-      type: "textarea",
-      overHidden: true,
+      label: "图片列表",
+      prop: "photoLists",
+      type: "array",
+      span: 24,
+    },
+    {
+      label: "文件列表",
+      prop: "fileLists",
+      type: "array",
+      span: 24,
     },
   ],
 });