|
@@ -86,6 +86,29 @@
|
|
|
<p @click="viewDetails(i)" class="content">
|
|
<p @click="viewDetails(i)" class="content">
|
|
|
{{ i.content }}
|
|
{{ i.content }}
|
|
|
</p>
|
|
</p>
|
|
|
|
|
+ <div class="img-preview-list" style="height: 92px;">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="img-item"
|
|
|
|
|
+ v-for="(url, index) in i.imageLists"
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ style="height: 80px;margin-bottom: 10px;"
|
|
|
|
|
+ >
|
|
|
|
|
+ <video
|
|
|
|
|
+ v-if="isMp4Link(url)"
|
|
|
|
|
+ class="preview-img"
|
|
|
|
|
+ :src="url"
|
|
|
|
|
+ controls
|
|
|
|
|
+ ></video>
|
|
|
|
|
+ <img
|
|
|
|
|
+ v-else
|
|
|
|
|
+ :src="url"
|
|
|
|
|
+ alt="图片预览"
|
|
|
|
|
+ class="preview-img"
|
|
|
|
|
+ v-viewer
|
|
|
|
|
+ @click="handlePreview(i.imageLists, index)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
<div class="dianzan">
|
|
<div class="dianzan">
|
|
|
<div class="icon">
|
|
<div class="icon">
|
|
|
<img src="../../assets/img/pinglun.png" alt="" />
|
|
<img src="../../assets/img/pinglun.png" alt="" />
|
|
@@ -218,6 +241,44 @@
|
|
|
type="textarea"
|
|
type="textarea"
|
|
|
/>
|
|
/>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
|
|
+ <el-form-item label="照片 :" prop="imageLists">
|
|
|
|
|
+ <div class="img-preview-list">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="img-item"
|
|
|
|
|
+ v-for="(url, index) in ruleForm.imageLists"
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ >
|
|
|
|
|
+ <video
|
|
|
|
|
+ v-if="isMp4Link(url)"
|
|
|
|
|
+ class="preview-img"
|
|
|
|
|
+ :src="url"
|
|
|
|
|
+ controls
|
|
|
|
|
+ ></video>
|
|
|
|
|
+ <img
|
|
|
|
|
+ v-else
|
|
|
|
|
+ :src="url"
|
|
|
|
|
+ alt="图片预览"
|
|
|
|
|
+ class="preview-img"
|
|
|
|
|
+ v-viewer
|
|
|
|
|
+ @click="handlePreview(ruleForm.imageLists, index)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <span class="delete-btn" @click="handleDelete(index)">
|
|
|
|
|
+ <el-icon><Delete /></el-icon>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-upload
|
|
|
|
|
+ action="#"
|
|
|
|
|
+ :auto-upload="false"
|
|
|
|
|
+ :on-change="handleFileChange"
|
|
|
|
|
+ :before-upload="beforeUpload"
|
|
|
|
|
+ class="upload-btn"
|
|
|
|
|
+ accept=".jpg,.jpeg,.png,.gif,.mp4,.avi,.mov,.wmv,.flv"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
|
|
+ <span class="upload-txt">点击图片或视频</span>
|
|
|
|
|
+ </el-upload>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-form-item>
|
|
|
<el-form-item class="options">
|
|
<el-form-item class="options">
|
|
|
<el-button @click="cancelAdd">取消</el-button>
|
|
<el-button @click="cancelAdd">取消</el-button>
|
|
|
<el-button
|
|
<el-button
|
|
@@ -304,13 +365,35 @@
|
|
|
<p style="text-indent: 2em; line-height: 1.8">
|
|
<p style="text-indent: 2em; line-height: 1.8">
|
|
|
{{ detailData.content }}
|
|
{{ detailData.content }}
|
|
|
</p>
|
|
</p>
|
|
|
- <p class="comment-count">{{ detailData.commentNum }}条评论数</p>
|
|
|
|
|
- <div class="card-footer" v-loading="commentLoading">
|
|
|
|
|
|
|
+ <div class="img-preview-list" style="margin-bottom: 15px;">
|
|
|
<div
|
|
<div
|
|
|
- class="user"
|
|
|
|
|
- v-for="(i, index) in commentData"
|
|
|
|
|
- :key="i.id"
|
|
|
|
|
|
|
+ class="img-item"
|
|
|
|
|
+ v-for="(url, index) in detailData.imageLists"
|
|
|
|
|
+ :key="index"
|
|
|
>
|
|
>
|
|
|
|
|
+ <video
|
|
|
|
|
+ v-if="isMp4Link(url)"
|
|
|
|
|
+ class="preview-img"
|
|
|
|
|
+ :src="url"
|
|
|
|
|
+ controls
|
|
|
|
|
+ ></video>
|
|
|
|
|
+ <img
|
|
|
|
|
+ v-else
|
|
|
|
|
+ :src="url"
|
|
|
|
|
+ alt="图片预览"
|
|
|
|
|
+ class="preview-img"
|
|
|
|
|
+ v-viewer
|
|
|
|
|
+ @click="handlePreview(detailData.imageLists, index)"
|
|
|
|
|
+ />
|
|
|
|
|
+ <span class="delete-btn" @click="handleDelete(index)">
|
|
|
|
|
+ <el-icon><Delete /></el-icon>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <p class="comment-count">{{ detailData.commentNum }}条评论数</p>
|
|
|
|
|
+ <div class="card-footer" v-loading="commentLoading">
|
|
|
|
|
+ <div class="user" v-for="(i, index) in commentData" :key="i.id">
|
|
|
<img style="border-radius: 0" :src="i.image" alt="" />
|
|
<img style="border-radius: 0" :src="i.image" alt="" />
|
|
|
<div class="user-info">
|
|
<div class="user-info">
|
|
|
<div class="upvote">
|
|
<div class="upvote">
|
|
@@ -346,7 +429,11 @@
|
|
|
v-for="j in i.children"
|
|
v-for="j in i.children"
|
|
|
:key="j.id"
|
|
:key="j.id"
|
|
|
>
|
|
>
|
|
|
- <img style="border-radius: 0;margin: 10px 10px 0 0;" :src="j.image" alt="" />
|
|
|
|
|
|
|
+ <img
|
|
|
|
|
+ style="border-radius: 0; margin: 10px 10px 0 0"
|
|
|
|
|
+ :src="j.image"
|
|
|
|
|
+ alt=""
|
|
|
|
|
+ />
|
|
|
<div class="user-info">
|
|
<div class="user-info">
|
|
|
<div class="upvote">
|
|
<div class="upvote">
|
|
|
<span class="username">{{ j.commentName }}</span>
|
|
<span class="username">{{ j.commentName }}</span>
|
|
@@ -402,7 +489,12 @@ import { useRouter } from "vue-router";
|
|
|
import { genFileId, ElMessage, ElMessageBox } from "element-plus";
|
|
import { genFileId, ElMessage, ElMessageBox } from "element-plus";
|
|
|
import { dayjs } from "element-plus";
|
|
import { dayjs } from "element-plus";
|
|
|
import lodash from "lodash";
|
|
import lodash from "lodash";
|
|
|
-import { MoreFilled, ArrowRightBold } from "@element-plus/icons-vue";
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ MoreFilled,
|
|
|
|
|
+ ArrowRightBold,
|
|
|
|
|
+ Delete,
|
|
|
|
|
+ Plus,
|
|
|
|
|
+} from "@element-plus/icons-vue";
|
|
|
import { storeToRefs } from "pinia";
|
|
import { storeToRefs } from "pinia";
|
|
|
import { useCounterStore } from "@/stores/index";
|
|
import { useCounterStore } from "@/stores/index";
|
|
|
import {
|
|
import {
|
|
@@ -413,6 +505,8 @@ import {
|
|
|
queryPostsPage,
|
|
queryPostsPage,
|
|
|
queryCommetns,
|
|
queryCommetns,
|
|
|
} from "@/api/alumni-square";
|
|
} from "@/api/alumni-square";
|
|
|
|
|
+import { uploadFile } from "@/api/uploadFile";
|
|
|
|
|
+import { api as viewerApi } from "v-viewer";
|
|
|
|
|
|
|
|
const router = useRouter();
|
|
const router = useRouter();
|
|
|
const store = useCounterStore();
|
|
const store = useCounterStore();
|
|
@@ -447,6 +541,7 @@ const ruleFormRef = ref();
|
|
|
const ruleForm = reactive({
|
|
const ruleForm = reactive({
|
|
|
categoryId: "",
|
|
categoryId: "",
|
|
|
content: "",
|
|
content: "",
|
|
|
|
|
+ imageLists: [],
|
|
|
id: "",
|
|
id: "",
|
|
|
});
|
|
});
|
|
|
// 表单验证
|
|
// 表单验证
|
|
@@ -455,6 +550,7 @@ const rules = reactive({
|
|
|
{ required: true, message: "组织分类不能为空", trigger: "blur" },
|
|
{ required: true, message: "组织分类不能为空", trigger: "blur" },
|
|
|
],
|
|
],
|
|
|
content: [{ required: true, message: "内容不能为空", trigger: "blur" }],
|
|
content: [{ required: true, message: "内容不能为空", trigger: "blur" }],
|
|
|
|
|
+ // imageLists: [{ required: true, message: "图片或视频不能为空", trigger: "blur" }],
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const detailsVisible = ref(false); // 详情弹窗
|
|
const detailsVisible = ref(false); // 详情弹窗
|
|
@@ -499,6 +595,97 @@ const getList = async () => {
|
|
|
});
|
|
});
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+// 完整的MP4链接判断函数
|
|
|
|
|
+const isMp4Link = (url) => {
|
|
|
|
|
+ if (!url || typeof url !== "string") return false;
|
|
|
|
|
+
|
|
|
|
|
+ // 步骤1:去掉URL中的参数和哈希(? 或 # 之后的内容)
|
|
|
|
|
+ const pureUrl = url.split(/[?#]/)[0]; // 按 ? 或 # 拆分,取第一部分
|
|
|
|
|
+
|
|
|
|
|
+ // 步骤2:转为小写 + 判断是否以 .mp4 结尾
|
|
|
|
|
+ return pureUrl.toLowerCase().endsWith(".mp4");
|
|
|
|
|
+};
|
|
|
|
|
+const handleDelete = (index) => {
|
|
|
|
|
+ ruleForm.imageLists.splice(index, 1);
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+const handlePreview = (images, index) => {
|
|
|
|
|
+ // 核心:复制数组,把点击的图片移到第一位
|
|
|
|
|
+ const newImages = [...images]; // 拷贝原数组,避免修改原数据
|
|
|
|
|
+ const currentImg = newImages.splice(index, 1)[0]; // 取出当前点击的图片
|
|
|
|
|
+ newImages.unshift(currentImg); // 放到数组开头
|
|
|
|
|
+
|
|
|
|
|
+ // 传给预览器,此时插件显示的第一张就是点击的图片
|
|
|
|
|
+ // viewerApi({
|
|
|
|
|
+ // images: newImages,
|
|
|
|
|
+ // zIndex: 3000,
|
|
|
|
|
+ // });
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 可选:限制上传文件大小(单位:MB)
|
|
|
|
|
+const MAX_SIZE = 50; // 图片/视频最大50MB
|
|
|
|
|
+// 上传前的校验
|
|
|
|
|
+const beforeUpload = (file) => {
|
|
|
|
|
+ // 定义允许的文件类型(MIME类型 + 后缀名双重校验)
|
|
|
|
|
+ // const allowTypes = [
|
|
|
|
|
+ // // 图片类型
|
|
|
|
|
+ // 'image/jpeg', 'image/png', 'image/gif', 'image/jpg',
|
|
|
|
|
+ // // 视频类型
|
|
|
|
|
+ // 'video/mp4', 'video/avi', 'video/quicktime', 'video/x-ms-wmv', 'video/x-flv'
|
|
|
|
|
+ // ]
|
|
|
|
|
+ // const allowExts = ['jpg', 'jpeg', 'png', 'gif', 'mp4', 'avi', 'mov', 'wmv', 'flv']
|
|
|
|
|
+
|
|
|
|
|
+ const allowTypes = [
|
|
|
|
|
+ // 图片类型
|
|
|
|
|
+ "image/jpeg",
|
|
|
|
|
+ "image/png",
|
|
|
|
|
+ "image/gif",
|
|
|
|
|
+ "image/jpg",
|
|
|
|
|
+ // 视频类型
|
|
|
|
|
+ "video/mp4",
|
|
|
|
|
+ ];
|
|
|
|
|
+ const allowExts = ["jpg", "jpeg", "png", "gif", "mp4"];
|
|
|
|
|
+
|
|
|
|
|
+ // 获取文件后缀名(小写)
|
|
|
|
|
+ const fileExt = file.name.split(".").pop().toLowerCase();
|
|
|
|
|
+ // 获取文件MIME类型
|
|
|
|
|
+ const fileType = file.type;
|
|
|
|
|
+
|
|
|
|
|
+ // 校验格式
|
|
|
|
|
+ if (!allowTypes.includes(fileType) && !allowExts.includes(fileExt)) {
|
|
|
|
|
+ ElMessage.error("仅支持上传 jpg/jpeg/png/gif 图片或 mp4 视频!");
|
|
|
|
|
+ return false; // 阻止上传
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 校验大小(可选)
|
|
|
|
|
+ // const fileSize = file.size / 1024 / 1024 // 转MB
|
|
|
|
|
+ // if (fileSize > MAX_SIZE) {
|
|
|
|
|
+ // ElMessage.error(`文件大小不能超过 ${MAX_SIZE}MB!`)
|
|
|
|
|
+ // return false // 阻止上传
|
|
|
|
|
+ // }
|
|
|
|
|
+
|
|
|
|
|
+ return true; // 校验通过
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 文件选择变化时触发
|
|
|
|
|
+const handleFileChange = async (file, newFileList) => {
|
|
|
|
|
+ console.log(file);
|
|
|
|
|
+ let formData = new FormData();
|
|
|
|
|
+ formData.append("file", file.raw);
|
|
|
|
|
+ let res = await uploadFile(formData);
|
|
|
|
|
+ console.log(res);
|
|
|
|
|
+ if (res.code == 200) {
|
|
|
|
|
+ ruleForm.imageLists.push(res.data.fileUrl); // 赋值给响应式fileList,页面自动刷新
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage({
|
|
|
|
|
+ type: "error",
|
|
|
|
|
+ showClose: true,
|
|
|
|
|
+ message: res.message,
|
|
|
|
|
+ center: true,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
const categoryData = ref();
|
|
const categoryData = ref();
|
|
|
const categoryList = async () => {
|
|
const categoryList = async () => {
|
|
|
let res = await queryCategoryDatas();
|
|
let res = await queryCategoryDatas();
|
|
@@ -534,6 +721,7 @@ const addlist = () => {
|
|
|
ruleForm.categoryId = "";
|
|
ruleForm.categoryId = "";
|
|
|
ruleForm.content = "";
|
|
ruleForm.content = "";
|
|
|
ruleForm.id = "";
|
|
ruleForm.id = "";
|
|
|
|
|
+ ruleForm.imageLists = [];
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const deleteS = async (row) => {
|
|
const deleteS = async (row) => {
|
|
@@ -577,6 +765,7 @@ const submitAdd = lodash.debounce(async (formEl) => {
|
|
|
let data = {
|
|
let data = {
|
|
|
categoryId: ruleForm.categoryId,
|
|
categoryId: ruleForm.categoryId,
|
|
|
content: ruleForm.content,
|
|
content: ruleForm.content,
|
|
|
|
|
+ imageLists: ruleForm.imageLists,
|
|
|
};
|
|
};
|
|
|
let res = await insertPosts(data);
|
|
let res = await insertPosts(data);
|
|
|
if (res.code == 200) {
|
|
if (res.code == 200) {
|
|
@@ -843,10 +1032,10 @@ onUnmounted(() => {
|
|
|
padding: 0 20px 10px;
|
|
padding: 0 20px 10px;
|
|
|
font-size: 13px;
|
|
font-size: 13px;
|
|
|
.content {
|
|
.content {
|
|
|
- height: 78px;
|
|
|
|
|
|
|
+ height: 50px;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
display: -webkit-box;
|
|
display: -webkit-box;
|
|
|
- -webkit-line-clamp: 3;
|
|
|
|
|
|
|
+ -webkit-line-clamp: 2;
|
|
|
-webkit-box-orient: vertical;
|
|
-webkit-box-orient: vertical;
|
|
|
line-height: 1.6;
|
|
line-height: 1.6;
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
@@ -1013,12 +1202,12 @@ onUnmounted(() => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
.comment-count {
|
|
.comment-count {
|
|
|
- padding: 10px ;
|
|
|
|
|
- margin: 0;
|
|
|
|
|
- font-weight: 800;
|
|
|
|
|
- background-color: #f5f7fa;
|
|
|
|
|
- border-bottom: 0.5px solid #e0e0e0;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+ background-color: #f5f7fa;
|
|
|
|
|
+ border-bottom: 0.5px solid #e0e0e0;
|
|
|
|
|
+ }
|
|
|
.card-footer {
|
|
.card-footer {
|
|
|
max-height: 450px;
|
|
max-height: 450px;
|
|
|
overflow: auto;
|
|
overflow: auto;
|