- Submissions 提交情况表格「附件」列:四源判定(fileName 扩展名 → URL 扩展名 → 行级 contentTypes → img onerror 兜底),非图片渲染为与图片缩略图同尺寸(32×32) 的文件图标格,图标按扩展名取 icon-excel / icon-word / icon-pdf1 等 - 点文件格可在线预览(有 fileId 走 FilePreviewModal),title 显示学生上传的真实文件名 - Utils 新增 isImageName / fileIcon / attIsImage 三个工具方法 - HomeworkAccept、HomeworkDetails 两处完全重复的「扩展名→图标」映射收敛到 Utils.fileIcon, 兜底图标由语义不符的 icon-weizhi 改为 icon-attachment - ReviewWorkbench 批阅工作台:图片/文件分流改走 Utils.attIsImage(原判定只认 fileType==='image',后端不填该字段时图片会被整体误判为文件),并给文件附件补上「查看」 - StudentMatrixVo 兼容 attachments: AttachmentItem[] 新契约,缺失时退回 thumbnails; 后端上线即自动获得精确文件类型图标与在线预览,前端无需再发版 - mock 的 thumbnails 改为返回全部附件 URL(原先只挑 fileType==='image', 比契约更宽容,导致该缺陷本地无法复现)
This commit is contained in:
@@ -275,7 +275,26 @@ function genMatrix(n: any): any[] {
|
||||
isLate: cur ? cur.isLate : 0,
|
||||
reviewStatus: cur ? cur.reviewStatus : "PENDING",
|
||||
score: cur ? cur.score : null,
|
||||
thumbnails: cur ? (cur.attachments || []).filter((a: any) => (a.fileType || "").toLowerCase() === "image").map((a: any) => a.fileUrl) : [],
|
||||
// 契约:thumbnails 是「最新提交附件 URL 列表」——**不按类型过滤**,
|
||||
// 线上就是这么返回的(学生交 excel 也会进来,前端必须自己分流)。
|
||||
// 早先这里 filter(fileType==='image') 把非图片附件滤掉了,
|
||||
// 于是「附件列把 excel 当图片渲染成裂图」这个缺陷在本地永远复现不出来。
|
||||
thumbnails: cur ? (cur.attachments || []).map((a: any) => a.fileUrl) : [],
|
||||
// 契约增强:附件明细(含 fileType/fileName/fileId),列表页据此精确出图标并在线预览
|
||||
attachments: cur ? cur.attachments || [] : [],
|
||||
// 提交内容类型摘要
|
||||
contentTypes: cur
|
||||
? [
|
||||
...(cur.text ? ["text"] : []),
|
||||
...Array.from(
|
||||
new Set(
|
||||
(cur.attachments || []).map((a: any) =>
|
||||
(a.fileType || "").toLowerCase() === "image" ? "image" : "attach"
|
||||
)
|
||||
)
|
||||
),
|
||||
]
|
||||
: [],
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
</div>
|
||||
<div class="rv-files" v-if="fileAtts.length">
|
||||
<span class="rv-file" v-for="(a, i) in fileAtts" :key="i">
|
||||
<i class="iconfont file-type-icon" :class="Utils.fileIcon(a.fileName || a.fileUrl)"></i>
|
||||
{{ a.fileName || "附件" + (i + 1) }}
|
||||
<a @click="openFilePreview(a)">查看</a>
|
||||
<a :href="attDownUrl(a)" target="_blank">下载</a>
|
||||
</span>
|
||||
</div>
|
||||
@@ -84,6 +86,9 @@
|
||||
<Modal v-model="imgZoom.open" footer-hide title="作答图片" width="680px">
|
||||
<div class="zoom-wrap"><img :src="imgZoom.url" /></div>
|
||||
</Modal>
|
||||
|
||||
<!-- 非图片附件预览(xlsx/docx/pdf/txt 等) -->
|
||||
<FilePreviewModal v-model="filePreview.show" :file-id="filePreview.fileId" :file-name="filePreview.fileName" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -92,6 +97,8 @@ import { Message } from "view-ui-plus";
|
||||
import { teacherApi } from "@/api/module/teacherHomework";
|
||||
import type { AttachmentItem, SubmissionVersionVo, StudentMatrixVo } from "@/types/homeworkV2";
|
||||
import config from "@/assets/config/host.config";
|
||||
import Utils from "@/utils";
|
||||
import FilePreviewModal from "@/components/FilePreviewModal.vue";
|
||||
|
||||
/** 附件地址前缀:本地 dev/mock 走同源代理(空),线上为基础平台域名(含后端 contextPath) */
|
||||
const serviceBase = config.isMock ? "" : config.serviceHost;
|
||||
@@ -153,8 +160,8 @@ async function load() {
|
||||
}
|
||||
}
|
||||
|
||||
const imageAtts = computed(() => (cur.value?.attachments || []).filter((a) => (a.fileType || "").toLowerCase() === "image"));
|
||||
const fileAtts = computed(() => (cur.value?.attachments || []).filter((a) => (a.fileType || "").toLowerCase() !== "image"));
|
||||
const imageAtts = computed(() => (cur.value?.attachments || []).filter((a) => Utils.attIsImage(a)));
|
||||
const fileAtts = computed(() => (cur.value?.attachments || []).filter((a) => !Utils.attIsImage(a)));
|
||||
|
||||
/** 附件展示地址:fileUrl 可能是 /api/file/Preview?id= 相对路径(需拼 serviceHost),绝对地址原样 */
|
||||
function attUrl(a: AttachmentItem): string {
|
||||
@@ -171,6 +178,22 @@ function zoom(url: string) {
|
||||
imgZoom.url = url;
|
||||
imgZoom.open = true;
|
||||
}
|
||||
/**
|
||||
* 非图片附件的在线预览(学生交的就是 excel 这类文件,老师批阅时必须能打开看)。
|
||||
* 有 fileId 走 open-file-viewer;没有则退回新窗口打开。
|
||||
*/
|
||||
const filePreview = ref<{ show: boolean; fileId: number | null; fileName: string }>({
|
||||
show: false,
|
||||
fileId: null,
|
||||
fileName: "",
|
||||
});
|
||||
function openFilePreview(a: AttachmentItem) {
|
||||
if (!a.fileId) {
|
||||
window.open(attUrl(a), "_blank");
|
||||
return;
|
||||
}
|
||||
filePreview.value = { show: true, fileId: a.fileId, fileName: a.fileName || "" };
|
||||
}
|
||||
|
||||
async function doReview() {
|
||||
if (!props.student) return;
|
||||
@@ -300,9 +323,14 @@ async function doReturn() {
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
color: #515a6e;
|
||||
.file-type-icon {
|
||||
font-size: 14px;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
a {
|
||||
margin-left: 6px;
|
||||
color: #2d8cf0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-1
@@ -198,8 +198,23 @@ export interface StudentMatrixVo {
|
||||
isLate: number;
|
||||
reviewStatus: ReviewStatus;
|
||||
score?: number | null;
|
||||
/** 最新提交附件 URL 列表(缩略图) */
|
||||
/**
|
||||
* 最新提交附件的 URL 列表。
|
||||
*
|
||||
* ⚠️ 这是**裸 URL**,不含 fileType/fileName/fileId,所以:
|
||||
* - 无法判断每个 URL 是不是图片(平台附件地址形如 `/api/file/Preview?id=9001`,无扩展名);
|
||||
* - 拿到非图片附件时前端只能靠「加载失败兜底」降级。
|
||||
* 新契约请用下面的 `attachments`,本字段仅作向后兼容。
|
||||
*/
|
||||
thumbnails?: string[];
|
||||
/**
|
||||
* 最新提交附件明细(**契约增强字段**,与模块内其余接口的附件结构一致)。
|
||||
*
|
||||
* 有它就能精确拿到 fileType/fileName/fileId:按扩展名出对应文件图标、
|
||||
* 走 `FilePreviewModal` 在线预览 xlsx/docx/pdf。
|
||||
* 后端未返回时前端自动退回 `thumbnails`。
|
||||
*/
|
||||
attachments?: AttachmentItem[];
|
||||
/** 提交内容类型摘要:image=拍照/图片、attach=上传附件、text=文字作答(未交为空) */
|
||||
contentTypes?: string[];
|
||||
}
|
||||
|
||||
@@ -192,6 +192,74 @@ export default class Utils {
|
||||
);
|
||||
}
|
||||
|
||||
/* ================= 作业模块:附件类型判定与图标 ================= */
|
||||
|
||||
/**
|
||||
* 可直接当图片渲染的扩展名白名单。
|
||||
*
|
||||
* ⚠️ 判定「能不能塞进 <img>」必须走白名单,不能用「非图片后缀」的黑名单:
|
||||
* 未知后缀(.xlsx/.docx/无后缀)走黑名单会被误判成图片 → 裂图。
|
||||
*/
|
||||
public static readonly IMG_EXT: string[] = [
|
||||
"jpg", "jpeg", "png", "gif", "bmp", "webp", "svg", "avif", "heic", "heif", "ico",
|
||||
];
|
||||
|
||||
/** 取扩展名(小写、不含点)。自动剥掉 URL 的 query/hash,取不到返回 "" */
|
||||
public static fileExt(name?: string): string {
|
||||
const s = String(name || "").split("?")[0].split("#")[0];
|
||||
const m = /\.([A-Za-z0-9]{1,8})$/.exec(s);
|
||||
return m ? m[1].toLowerCase() : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* 扩展名 → 本地文件类型图标(iconfont 类名,含 `icon-` 前缀)。
|
||||
*
|
||||
* 原先 `HomeworkAccept` / `HomeworkDetails` 各写了一份完全相同的映射,是重复代码;
|
||||
* 收敛到这里后新增类型只改一处。未知类型的兜底从 `icon-weizhi`(定位图标,语义不符)
|
||||
* 改为 `icon-attachment`。
|
||||
*/
|
||||
public static fileIcon(name?: string): string {
|
||||
const e = Utils.fileExt(name);
|
||||
if (["doc", "docx", "wps"].includes(e)) return "icon-word";
|
||||
if (["xls", "xlsx", "csv"].includes(e)) return "icon-excel";
|
||||
if (["ppt", "pptx"].includes(e)) return "icon-ppt";
|
||||
if (["pdf"].includes(e)) return "icon-pdf1";
|
||||
if (["txt", "md"].includes(e)) return "icon-txt";
|
||||
if (Utils.IMG_EXT.includes(e)) return "icon-tupian1";
|
||||
if (["zip", "rar", "7z"].includes(e)) return "icon-yasuo";
|
||||
return "icon-attachment";
|
||||
}
|
||||
|
||||
/**
|
||||
* 按「名称」判断是否图片。
|
||||
*
|
||||
* @returns true=图片 / false=确定不是图片 / null=**判不出来**(名称里没有扩展名)
|
||||
*
|
||||
* 返回 null 是刻意设计的第三态:平台附件地址形如 `/api/file/Preview?id=9001`,
|
||||
* 路径里根本没有扩展名,此时任何「按后缀判定」都不成立,调用方必须另想办法
|
||||
* (行级 contentTypes 或加载失败兜底),不能擅自当成图片。
|
||||
*/
|
||||
public static isImageName(name?: string): boolean | null {
|
||||
const e = Utils.fileExt(name);
|
||||
if (!e) return null;
|
||||
return Utils.IMG_EXT.includes(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否图片附件。两个判据互为补充,缺一都会出错:
|
||||
* - 只认 `fileType === 'image'`:后端若没填 fileType(或填 MIME),图片会被整体判成文件;
|
||||
* - 只认扩展名:附件地址可能是无扩展名的 `/api/file/Preview?id=9001`。
|
||||
*
|
||||
* 故 fileType 优先(含 MIME 形式 `image/png`),拿不到结论时按 fileName/fileUrl 扩展名兜底,
|
||||
* 两者都判不出时**保守判为「非图片」**(显示成文件条目,不会裂图)。
|
||||
*/
|
||||
public static attIsImage(a: { fileType?: string; fileName?: string; fileUrl?: string } | null | undefined): boolean {
|
||||
if (!a) return false;
|
||||
const t = String(a.fileType || "").toLowerCase();
|
||||
if (t === "image" || t.startsWith("image/")) return true;
|
||||
return Utils.isImageName(a.fileName || a.fileUrl) === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算表格高度
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,7 @@ import AttachmentUpload from "@/components/AttachmentUpload.vue";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import FilePreviewModal from "@/components/FilePreviewModal.vue";
|
||||
import config from "@/assets/config/host.config";
|
||||
import Utils from "@/utils";
|
||||
|
||||
/**
|
||||
* 学生作业详情页(M5 P3,PRD 学生页 2/3):
|
||||
@@ -187,16 +188,9 @@ function zoom(u: string) {
|
||||
imgPreview.url = u;
|
||||
imgPreview.open = true;
|
||||
}
|
||||
/* 附件图标(按扩展名)——统一走 Utils.fileIcon,避免各页各写一份映射 */
|
||||
function getFileIcon(name: string) {
|
||||
const suffix = String(name || "").split(".").pop()?.toLowerCase() || "";
|
||||
if (["doc", "docx", "wps"].includes(suffix)) return "icon-word";
|
||||
if (["xls", "xlsx"].includes(suffix)) return "icon-excel";
|
||||
if (["ppt", "pptx"].includes(suffix)) return "icon-ppt";
|
||||
if (["pdf"].includes(suffix)) return "icon-pdf1";
|
||||
if (["txt"].includes(suffix)) return "icon-txt";
|
||||
if (["jpg", "jpeg", "png", "gif", "bmp"].includes(suffix)) return "icon-tupian1";
|
||||
if (["zip", "rar"].includes(suffix)) return "icon-yasuo";
|
||||
return "icon-weizhi";
|
||||
return Utils.fileIcon(name);
|
||||
}
|
||||
const previewState = ref<{ show: boolean; fileId: number | null; fileName: string }>({ show: false, fileId: null, fileName: "" });
|
||||
function openPreview(a: AttachmentItem) {
|
||||
@@ -259,7 +253,7 @@ function openPreview(a: AttachmentItem) {
|
||||
<div class="my-text empty" v-else>(无文字作答)</div>
|
||||
<div class="my-atts" v-if="detail.myAttachments && detail.myAttachments.length">
|
||||
<template v-for="a in detail.myAttachments" :key="a.fileUrl">
|
||||
<div class="th" v-if="(a.fileType || '').toLowerCase() === 'image'" @click="zoom(previewUrl(a))">
|
||||
<div class="th" v-if="Utils.attIsImage(a)" @click="zoom(previewUrl(a))">
|
||||
<img :src="previewUrl(a)" />
|
||||
</div>
|
||||
<div class="file-chip" v-else>
|
||||
@@ -320,7 +314,7 @@ function openPreview(a: AttachmentItem) {
|
||||
<div class="v-text" v-if="v.text">{{ v.text }}</div>
|
||||
<div class="v-atts" v-if="v.attachments && v.attachments.length">
|
||||
<template v-for="a in v.attachments" :key="a.fileUrl">
|
||||
<div class="th" v-if="(a.fileType || '').toLowerCase() === 'image'" @click="zoom(previewUrl(a))"><img :src="previewUrl(a)" /></div>
|
||||
<div class="th" v-if="Utils.attIsImage(a)" @click="zoom(previewUrl(a))"><img :src="previewUrl(a)" /></div>
|
||||
<div class="file-chip" v-else>
|
||||
<Icon type="md-document" />
|
||||
{{ a.fileName || "附件" }}
|
||||
|
||||
@@ -180,17 +180,9 @@ function downAll() {
|
||||
function displayName(item: { name?: string; filename?: string }) {
|
||||
return item.name || item.filename || "";
|
||||
}
|
||||
/* 附件图标(按扩展名) */
|
||||
/* 附件图标(按扩展名)——统一走 Utils.fileIcon,避免各页各写一份映射 */
|
||||
function getFileIcon(name: string) {
|
||||
const suffix = String(name || "").split(".").pop()?.toLowerCase() || "";
|
||||
if (["doc", "docx", "wps"].includes(suffix)) return "icon-word";
|
||||
if (["xls", "xlsx"].includes(suffix)) return "icon-excel";
|
||||
if (["ppt", "pptx"].includes(suffix)) return "icon-ppt";
|
||||
if (["pdf"].includes(suffix)) return "icon-pdf1";
|
||||
if (["txt"].includes(suffix)) return "icon-txt";
|
||||
if (["jpg", "jpeg", "png", "gif", "bmp"].includes(suffix)) return "icon-tupian1";
|
||||
if (["zip", "rar"].includes(suffix)) return "icon-yasuo";
|
||||
return "icon-weizhi";
|
||||
return Utils.fileIcon(name);
|
||||
}
|
||||
/* 可在线预览(open-file-viewer 支持:图片/pdf/txt/docx/xlsx/pptx;doc/zip 等仅下载) */
|
||||
function getIsPreview(name: string) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
SubmitStatusV2,
|
||||
} from "@/types/homeworkV2";
|
||||
import ReviewModal from "@/components/ReviewModal.vue";
|
||||
import FilePreviewModal from "@/components/FilePreviewModal.vue";
|
||||
import MyConfirm from "@/components/MyConfirm";
|
||||
import config from "@/assets/config/host.config";
|
||||
import Utils from "@/utils";
|
||||
@@ -161,6 +162,86 @@ function previewUrl(u: string) {
|
||||
return `${serviceBase}${u}`;
|
||||
}
|
||||
|
||||
/* ============ 附件列:图片 / 文件分流 ============ */
|
||||
|
||||
/** 列表里最多展示几个附件格(超出只报数量) */
|
||||
const ATT_MAX = 3;
|
||||
|
||||
/**
|
||||
* 已确认加载失败的附件 URL。
|
||||
*
|
||||
* 这是「拿不到类型信息」时的最后一道防线:旧契约的 `thumbnails` 只有裸 URL,
|
||||
* 平台地址又是 `/api/file/Preview?id=9001` 这种无扩展名的形式,前端**无法预判**
|
||||
* 它是不是图片。与其赌对,不如让 <img> 失败一次后立刻换成文件格。
|
||||
*/
|
||||
const brokenThumbs = ref<Set<string>>(new Set());
|
||||
function markThumbBroken(url: string) {
|
||||
if (brokenThumbs.value.has(url)) return;
|
||||
const next = new Set(brokenThumbs.value);
|
||||
next.add(url);
|
||||
brokenThumbs.value = next; // 换新 Set 才会触发响应式
|
||||
}
|
||||
|
||||
/**
|
||||
* 该附件能否按 <img> 渲染。四源判定,从可靠到兜底:
|
||||
* 1. 有 fileName(契约增强的 attachments)→ 按扩展名精确判定,最可靠,
|
||||
* 非图片直接判掉,省掉一个注定失败的图片请求;
|
||||
* 2. URL 自带扩展名 → 以扩展名为准(`a.png` 是图、`a.xlsx` 不是);
|
||||
* 3. URL 无扩展名(线上附件地址就是 `/api/file/Preview?id=9001` 这种)→ 退回行级
|
||||
* `contentTypes`:该行不含 image 就判为文件,省掉裂图闪烁;
|
||||
* 4. 连 `contentTypes` 也没有(更旧的后端)→ 只能先按图片试,失败由 onerror 降级。
|
||||
*/
|
||||
function thumbIsImage(row: StudentMatrixVo, it: { url: string; name: string }): boolean {
|
||||
if (brokenThumbs.value.has(it.url)) return false;
|
||||
if (it.name) {
|
||||
const byName = Utils.isImageName(it.name);
|
||||
if (byName !== null) return byName;
|
||||
}
|
||||
const byUrl = Utils.isImageName(it.url);
|
||||
if (byUrl !== null) return byUrl;
|
||||
const ct = row.contentTypes || [];
|
||||
if (!ct.length) return true;
|
||||
return ct.includes("image");
|
||||
}
|
||||
|
||||
/**
|
||||
* 归一化附件列表:优先用契约增强的 `attachments`(含 fileName/fileId,
|
||||
* 能精确出图标并在线预览),没有则退回裸 URL 的 `thumbnails`。
|
||||
*/
|
||||
function attItems(row: StudentMatrixVo): { url: string; name: string; fileId: number | null }[] {
|
||||
const list = row.attachments || [];
|
||||
if (list.length) {
|
||||
return list
|
||||
.map((a) => ({ url: a.fileUrl || "", name: a.fileName || "", fileId: Number(a.fileId) || null }))
|
||||
.filter((x) => x.url);
|
||||
}
|
||||
return (row.thumbnails || []).map((u) => ({ url: u, name: "", fileId: null }));
|
||||
}
|
||||
|
||||
/* 图片放大 */
|
||||
const imgPreview = reactive({ open: false, url: "" });
|
||||
function zoom(url: string) {
|
||||
imgPreview.url = url;
|
||||
imgPreview.open = true;
|
||||
}
|
||||
/* 文件预览:非图片附件走 open-file-viewer(xlsx/docx/pdf/txt 可在线看) */
|
||||
const filePreviewState = ref<{ show: boolean; fileId: number | null; fileName: string }>({
|
||||
show: false,
|
||||
fileId: null,
|
||||
fileName: "",
|
||||
});
|
||||
/**
|
||||
* 点击附件格的统一入口:有 fileId 走预览弹窗(能在线看 excel/word/pdf),
|
||||
* 只有裸 URL 时新窗口打开,由后端决定 inline 展示还是下载。
|
||||
*/
|
||||
function openAttachment(it: { url: string; name: string; fileId: number | null }) {
|
||||
if (it.fileId) {
|
||||
filePreviewState.value = { show: true, fileId: it.fileId, fileName: it.name || "" };
|
||||
return;
|
||||
}
|
||||
window.open(previewUrl(it.url), "_blank");
|
||||
}
|
||||
|
||||
/* ============ 批阅弹窗 / 定向提醒 ============ */
|
||||
const reviewModal = reactive({ open: false, row: null as StudentMatrixVo | null });
|
||||
function openReview(row: StudentMatrixVo) {
|
||||
@@ -259,22 +340,36 @@ const columns = computed(() => [
|
||||
},
|
||||
{
|
||||
title: "附件",
|
||||
width: 110,
|
||||
// 3 格 × 32px + 2 × 4px 间距 + 单元格左右 padding 36px = 140
|
||||
width: 140,
|
||||
align: "left",
|
||||
render: (h: any, p: any) => {
|
||||
const th: string[] = p.row.thumbnails || [];
|
||||
if (!th.length) return h("span", { style: "color:#c5c8ce;" }, "—");
|
||||
return h(
|
||||
"div",
|
||||
{ style: "display:flex;gap:4px;" },
|
||||
th.slice(0, 3).map((u) =>
|
||||
h("img", {
|
||||
src: previewUrl(u),
|
||||
style: "width:32px;height:32px;object-fit:cover;border-radius:3px;border:1px solid #e8eaec;cursor:zoom-in;",
|
||||
onClick: () => zoom(previewUrl(u)),
|
||||
})
|
||||
)
|
||||
);
|
||||
const row = p.row as StudentMatrixVo;
|
||||
const items = attItems(row);
|
||||
if (!items.length) return h("span", { style: "color:#c5c8ce;" }, "—");
|
||||
|
||||
const cells = items.slice(0, ATT_MAX).map((it) => {
|
||||
if (thumbIsImage(row, it)) {
|
||||
return h("img", {
|
||||
src: previewUrl(it.url),
|
||||
class: "hw-att-thumb",
|
||||
title: it.name || "图片",
|
||||
onError: () => markThumbBroken(it.url),
|
||||
onClick: () => zoom(previewUrl(it.url)),
|
||||
});
|
||||
}
|
||||
// 非图片:同尺寸的文件格,图标按扩展名(拿不到扩展名时用通用附件图标)
|
||||
const icon = it.name ? Utils.fileIcon(it.name) : "icon-attachment";
|
||||
return h("div", {
|
||||
class: `hw-att-file iconfont ${icon}`,
|
||||
title: it.name || "附件",
|
||||
onClick: () => openAttachment(it),
|
||||
});
|
||||
});
|
||||
|
||||
const wrap: any[] = [h("div", { class: "hw-att-box" }, cells)];
|
||||
if (items.length > ATT_MAX) wrap.push(h("span", { class: "hw-att-more" }, `+${items.length - ATT_MAX}`));
|
||||
return h("div", { class: "hw-att-wrap" }, wrap);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -303,13 +398,6 @@ function calcTableHeight() {
|
||||
tableHeight.value = Math.max(Utils.useTableHeight({ isPage: true }) - topH - 16, 200);
|
||||
}
|
||||
watch(total, () => nextTick(calcTableHeight));
|
||||
|
||||
/* 图片放大 */
|
||||
const imgPreview = reactive({ open: false, url: "" });
|
||||
function zoom(url: string) {
|
||||
imgPreview.url = url;
|
||||
imgPreview.open = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -398,6 +486,12 @@ function zoom(url: string) {
|
||||
<Modal v-model="imgPreview.open" footer-hide title="作答图片" width="680px">
|
||||
<div class="zoom-wrap"><img :src="imgPreview.url" /></div>
|
||||
</Modal>
|
||||
<!-- 非图片附件预览(xlsx/docx/pdf/txt 等,open-file-viewer) -->
|
||||
<FilePreviewModal
|
||||
v-model="filePreviewState.show"
|
||||
:file-id="filePreviewState.fileId"
|
||||
:file-name="filePreviewState.fileName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -454,3 +548,64 @@ function zoom(url: string) {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--
|
||||
附件格样式必须是**非 scoped**:这两个元素由 columns 的 render 函数用 h() 创建,
|
||||
而手写 render 不会带入 scoped 的 data-v 属性(`<style scoped>` 选择器匹配不上,
|
||||
之前就因此退化成了 .iconfont 默认的 16px)。故改为带 hw- 前缀的全局类,避免串页。
|
||||
-->
|
||||
<style lang="scss">
|
||||
.hw-att-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.hw-att-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
/* 图片缩略图与文件图标格同尺寸,保证列内观感整齐 */
|
||||
/* flex:none 必须有:flex 子项默认可压缩,列宽一旦吃紧就会被挤成窄条 */
|
||||
.hw-att-thumb {
|
||||
flex: none;
|
||||
display: block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
object-fit: cover;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: 3px;
|
||||
cursor: zoom-in;
|
||||
transition: border-color 0.2s;
|
||||
&:hover {
|
||||
border-color: #9e89cf;
|
||||
}
|
||||
}
|
||||
.hw-att-file {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 18px;
|
||||
color: #808695;
|
||||
background: #f8f8f9;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
/* hover 色与列表操作按钮(.operating-btn)一致 */
|
||||
&:hover {
|
||||
color: #9e89cf;
|
||||
border-color: #9e89cf;
|
||||
background: #f6f3fc;
|
||||
}
|
||||
}
|
||||
.hw-att-more {
|
||||
flex: none;
|
||||
font-size: 12px;
|
||||
color: #808695;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user