feat(M5-P3): 学生端闭环切换 V2

- StudentHomeworks 切 V2 列表(PRD 学生页 1):tab=all/todo/done/closed 服务端
  分页 + keyword;五态徽标 + 逾期角标 + 截止倒计时(逾期/临期变色);todo 桶内
  RETURNED 红色强化置顶(/stu/redo initTab=todo);快捷提交弹窗改
  AttachmentUpload + studentApi(resubmit 走 PUT action)
- HomeworkAccept 改造为学生详情页挂 /stu/detail(PRD 学生页 2/3):作业说明/附件、
  进入 3 秒阅读上报(onUnmounted 清理)、我的提交 + 批阅结果 + 历史版本各版评语、
  按 submitType 动态提交表单、截止/allowLate 分流、修改提交生成新版本、
  截止前撤回、打回订正重交
- 菜单:静态兜底菜单对齐 V2(今日作业/待订正/批改中心),已收作业别名导向 /stu/today
This commit is contained in:
wanghu
2026-09-16 02:42:18 +08:00
parent 23d67161e4
commit 6258ebb173
4 changed files with 732 additions and 724 deletions
+218 -331
View File
@@ -1,118 +1,168 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch } from "vue";
import { useRoute } from "vue-router";
import { useRoute, useRouter } from "vue-router";
import { Message } from "view-ui-plus";
import { homeworkApi } from "@/api/module/homework";
import type { StudentHomeworkVo, MyHomeworksVo } from "@/types/homework";
import HomeworkUpload from "@/components/HomeworkUpload.vue";
import { studentApi } from "@/api/module/studentHomework";
import type { BasePageResult, StudentHomeworkV2Vo, StudentHomeworkDetailV2Vo, AttachmentItem } from "@/types/homeworkV2";
import AttachmentUpload from "@/components/AttachmentUpload.vue";
import MyConfirm from "@/components/MyConfirm";
import config from "@/assets/config/host.config";
const serviceBase = config.isMock ? "" : config.serviceHost;
const imgUploadUrl = serviceBase + "/api/file/UploadImage";
/** 提交图片:可直接显示的 URL/dataURL,或后端 fileid(回退拼预览地址) */
function previewUrl(v: number | string) {
const s = String(v);
if (s.startsWith("data:") || s.startsWith("http")) return s;
return `${serviceBase}/api/file/Preview?id=${s}`;
}
const route = useRoute();
const router = useRouter();
/* V2 tab:all/todo/done/closed(redo 桶已并入 todo,RETURNED 红色强化) */
const TABS = [
{ name: "all", label: "全部" },
{ name: "todo", label: "待完成" },
{ name: "submitted", label: "已交待批" },
{ name: "graded", label: "已批改" },
{ name: "redo", label: "待订正" },
{ name: "done", label: "已完成" },
{ name: "closed", label: "已截止" },
];
const tab = ref<string>((route.meta as any).initTab || "all");
const keyword = ref("");
const pageNum = ref(1);
const pageSize = ref(10);
const data = ref<MyHomeworksVo | null>(null);
const EMPTY_PAGE: BasePageResult<StudentHomeworkV2Vo> = { content: [], total: 0, pageIndex: 1, pageSize: 10, totalPages: 0, hasNext: false };
const page = ref<BasePageResult<StudentHomeworkV2Vo>>(EMPTY_PAGE);
const loading = ref(false);
async function load() {
loading.value = true;
try {
data.value = await homeworkApi.myHomeworks().send();
page.value = await studentApi
.list(tab.value, {
keyword: keyword.value || undefined,
pageIndex: pageNum.value,
pageSize: pageSize.value,
})
.send();
} catch (e: any) {
Message.error(e.message || "加载作业失败");
page.value = EMPTY_PAGE;
} finally {
loading.value = false;
}
}
function onTab(name: string) {
tab.value = name;
pageNum.value = 1;
load();
}
function doSearch() {
pageNum.value = 1;
load();
}
function pageUpdate(p: number) {
pageNum.value = p;
load();
}
onMounted(load);
watch(() => route.fullPath, load);
watch(
() => route.fullPath,
() => {
tab.value = (route.meta as any).initTab || "all";
keyword.value = "";
pageNum.value = 1;
load();
}
);
function bucketOf(x: StudentHomeworkVo): string {
if (x.viewOnly) return "view";
const st = x.my ? x.my.submitStatus : "unsubmitted";
if (st === "unsubmitted") return "todo";
if (st === "submitted") return "submitted";
if (st === "needRedo") return "redo";
return "graded";
}
const filtered = computed(() => {
const list = data.value?.list || [];
if (tab.value === "all") return list;
return list.filter((x) => bucketOf(x) === tab.value);
/* todo 桶内待订正(RETURNED)排前,红色强化 */
const rows = computed(() => {
const list = page.value.content || [];
if (tab.value === "todo") {
return [...list].sort((a, b) => Number(b.my?.submitStatus === "RETURNED") - Number(a.my?.submitStatus === "RETURNED"));
}
return list;
});
function tabCount(name: string) {
const b = data.value?.buckets;
if (!b) return 0;
return name === "all" ? b.all : name === "todo" ? b.todo : name === "submitted" ? b.submitted : name === "graded" ? b.graded : b.redo;
}
const STATUS_TEXT: Record<string, string> = { unsubmitted: "未提交", submitted: "已交·待批", graded: "已批改", needRedo: "待订正", redone: "已订正·待批" };
const STATUS_COLOR: Record<string, string> = { unsubmitted: "#c5c8ce", submitted: "#ff9900", graded: "#19be6b", needRedo: "#ed4014", redone: "#2d8cf0" };
function statusColor(x: StudentHomeworkVo) {
if (x.viewOnly) return "#808695";
return STATUS_COLOR[x.my ? x.my.submitStatus : "unsubmitted"];
/* ============ 状态展示 ============ */
const STATUS_TEXT: Record<string, string> = {
NOT_SUBMITTED: "未提交",
SUBMITTED: "已提交·待批",
LATE_SUBMITTED: "逾期补交·待批",
RETURNED: "待订正",
REVIEWED: "已批阅",
};
const STATUS_COLOR: Record<string, string> = {
NOT_SUBMITTED: "#c5c8ce",
SUBMITTED: "#2d8cf0",
LATE_SUBMITTED: "#ff9900",
RETURNED: "#ed4014",
REVIEWED: "#19be6b",
};
function myStatus(x: StudentHomeworkV2Vo) {
return x.my?.submitStatus || "NOT_SUBMITTED";
}
function dueInfo(deadline: string | null) {
if (!deadline) return { text: "无截止", overdue: false };
function statusColor(x: StudentHomeworkV2Vo) {
return STATUS_COLOR[myStatus(x)];
}
/** 仅查阅型(submitType 含 none) */
function isViewOnly(x: StudentHomeworkV2Vo) {
return (x.submitType || []).includes("none") || (x.submitType || []).length === 0;
}
/** 是否可提交:PUBLISHED 期内,或 CLOSED 且允许补交 */
function canSubmit(x: StudentHomeworkV2Vo) {
if (isViewOnly(x)) return false;
if (x.hwStatus === "PUBLISHED") return true;
if (x.hwStatus === "CLOSED") return x.allowLate === 1;
return false;
}
function dueInfo(deadline: string | null | undefined) {
if (!deadline) return { text: "无截止", overdue: false, urgent: false };
const d = new Date(String(deadline).replace(/-/g, "/")).getTime();
if (isNaN(d)) return { text: "—", overdue: false, urgent: false };
const diffH = (d - Date.now()) / 3600000;
if (diffH < 0) return { text: "已截止", overdue: true };
if (diffH < 24) return { text: `剩 ${Math.max(1, Math.round(diffH))} 小时`, overdue: false };
return { text: `剩 ${Math.round(diffH / 24)} 天`, overdue: false };
if (diffH < 0) return { text: "已截止", overdue: true, urgent: false };
if (diffH < 24) return { text: `剩 ${Math.max(1, Math.round(diffH))} 小时`, overdue: false, urgent: true };
return { text: `剩 ${Math.round(diffH / 24)} 天`, overdue: false, urgent: false };
}
function goDetail(row: StudentHomeworkV2Vo) {
router.push({ path: "/stu/detail", query: { serid: String(row.id) } });
}
/* ============ 提交 / 重交 ============ */
const sub = reactive({ open: false, serid: 0, title: "", text: "", fileids: [] as number[], images: [] as string[], isRedo: false, submitting: false, uploading: false });
function openSubmit(row: StudentHomeworkVo) {
/* ============ 提交 / 重交 / 撤回(快捷弹窗,主提交面在 /stu/detail) ============ */
const sub = reactive({
open: false,
id: 0,
title: "",
mode: "submit" as "submit" | "resubmit",
submissionId: 0,
text: "",
attachments: [] as AttachmentItem[],
loadingEcho: false,
submitting: false,
});
async function openSubmit(row: StudentHomeworkV2Vo) {
sub.open = true;
sub.serid = row.id;
sub.id = row.id;
sub.title = row.title;
sub.text = row.my?.text || "";
sub.fileids = [];
sub.images = (row.my?.images || []).map((v: any) => String(v)).filter(Boolean);
sub.isRedo = !!row.my && row.my.submitStatus === "needRedo";
}
function onImgSuccess(file: any) {
sub.uploading = false;
const resp = file?.response;
if (!resp || resp.code !== 0) {
Message.error(resp?.msg || "图片上传失败");
return;
sub.mode = row.my?.submitStatus === "RETURNED" ? "resubmit" : "submit";
sub.submissionId = row.my?.submissionId || 0;
sub.text = "";
sub.attachments = [];
// 回显当前版本内容(修改提交 / 订正重交场景)
sub.loadingEcho = true;
try {
const d: StudentHomeworkDetailV2Vo = await studentApi.detail(row.id).send();
sub.text = d.myText || "";
sub.attachments = [...(d.myAttachments || [])];
} catch {
/* 回显失败不阻塞提交 */
} finally {
sub.loadingEcho = false;
}
const item = Array.isArray(resp.data) ? resp.data[0] : resp.data;
const url = item?.url || (item?.id != null ? previewUrl(item.id) : "");
if (url) {
if (sub.images.length >= 9) {
Message.warning("最多上传 9 张图片");
return;
}
sub.images.push(url);
}
}
function removeImg(url: string) {
sub.images = sub.images.filter((x) => x !== url);
}
async function submit() {
sub.submitting = true;
try {
await homeworkApi.submit({ serid: sub.serid, text: sub.text, images: sub.images, attachments: sub.fileids.map(String) }).send();
if (sub.mode === "resubmit" && sub.submissionId) {
await studentApi.action(sub.id, sub.submissionId, { action: "resubmit", text: sub.text, attachments: sub.attachments }).send();
} else {
await studentApi.submit(sub.id, { text: sub.text, attachments: sub.attachments }).send();
}
Message.success("提交成功");
sub.open = false;
load();
@@ -122,16 +172,30 @@ async function submit() {
sub.submitting = false;
}
}
/* ============ 批改反馈 ============ */
const fb = reactive({ open: false, row: null as StudentHomeworkVo | null });
function openFeedback(row: StudentHomeworkVo) {
fb.open = true;
fb.row = row;
function withdraw(row: StudentHomeworkV2Vo) {
MyConfirm({
title: "撤回提交",
msg: `撤回“ ${row.title} ”的提交?`,
info: "撤回后可重新编辑提交(截止前有效)",
async onOk() {
try {
await studentApi.action(row.id, row.my!.submissionId, { action: "withdraw" }).send();
Message.success("已撤回,可重新提交");
load();
} catch (e: any) {
Message.error(e.message || "撤回失败");
}
},
});
}
/* ============ 图片放大预览 ============ */
const imgPreview = reactive({ open: false, url: "" });
function previewUrl(a: AttachmentItem) {
const u = a.fileUrl || "";
if (/^(https?:)?\/\//.test(u) || u.startsWith("data:")) return u;
return `${serviceBase}${u}`;
}
function zoom(url: string) {
imgPreview.url = url;
imgPreview.open = true;
@@ -147,113 +211,91 @@ function zoom(url: string) {
<div class="sh-icon"><Icon type="md-book" size="26" color="#fff" /></div>
<div>
<div class="sh-title">我的作业</div>
<div class="sh-sub">按时提交,查看老师批改与订正</div>
<div class="sh-sub">按时提交,查看老师批阅与订正</div>
</div>
</div>
<div class="sh-chips">
<div class="chip todo"><b>{{ tabCount("todo") }}</b><span>待完成</span></div>
<div class="chip redo"><b>{{ tabCount("redo") }}</b><span>待订正</span></div>
<div class="chip graded"><b>{{ tabCount("graded") }}</b><span>已批改</span></div>
<div class="chip"><b>{{ page.total }}</b><span>本页作业</span></div>
</div>
</div>
<Tabs :value="tab" @on-click="(n: string) => (tab = n)" class="stu-tabs">
<TabPane v-for="t in TABS" :key="t.name" :name="t.name" :label="t.name === 'all' ? t.label : t.label + ' ' + tabCount(t.name)"></TabPane>
</Tabs>
<div class="fix mb-12">
<Tabs :value="tab" @on-click="onTab" class="fl stu-tabs">
<TabPane v-for="t in TABS" :key="t.name" :name="t.name" :label="t.label"></TabPane>
</Tabs>
<Input
class="fr"
style="width: 220px"
search
suffix="ios-search"
placeholder="搜索作业标题"
v-model="keyword"
@on-search="doSearch"
@on-clear="doSearch"
/>
</div>
<div v-if="loading" class="state"><Spin fix /><div style="height: 120px"></div></div>
<div v-else-if="!filtered.length" class="state empty">
<div v-else-if="!rows.length" class="state empty">
<Icon type="ios-albums-outline" size="52" color="#c5c8ce" />
<p>该分类下暂无作业</p>
</div>
<div class="cards" v-else>
<div class="card" v-for="row in filtered" :key="row.id" :style="{ borderLeftColor: statusColor(row) }">
<div class="c-top">
<span class="c-subj">{{ row.subject }}</span>
<div class="card" v-for="row in rows" :key="row.id" :class="{ redo: myStatus(row) === 'RETURNED' }" :style="{ borderLeftColor: statusColor(row) }">
<div class="c-top" @click="goDetail(row)">
<span class="c-subj">{{ row.subject || "作业" }}</span>
<span class="c-title">{{ row.title }}</span>
<Tag v-if="row.viewOnly" color="default" class="c-flag">仅查阅</Tag>
<span v-else class="c-dot" :style="{ background: statusColor(row) }"></span>
<span v-if="!row.viewOnly" class="c-status" :style="{ color: statusColor(row) }">{{ STATUS_TEXT[row.my ? row.my.submitStatus : "unsubmitted"] }}</span>
<Tag v-if="isViewOnly(row)" color="default" class="c-flag">仅查阅</Tag>
<template v-else>
<span v-if="row.my?.isLate" class="c-late">逾期</span>
<span class="c-status" :style="{ color: statusColor(row) }">{{ STATUS_TEXT[myStatus(row)] }}</span>
</template>
</div>
<div class="c-meta">
<span class="m-item"><Icon type="md-person" size="14" /> {{ row.publisherName }} · {{ row.deptName }}</span>
<span class="m-item" :class="{ overdue: dueInfo(row.deadline).overdue && bucketOf(row) === 'todo' }">
<Icon type="md-time" size="14" /> 截止 {{ row.deadline ? String(row.deadline).slice(5, 16) : "—" }}({{ dueInfo(row.deadline).text }})
<span class="m-item"><Icon type="md-person" size="14" /> {{ row.teacherName || "—" }}</span>
<span class="m-item" :class="{ overdue: dueInfo(row.deadlineAt).overdue && myStatus(row) !== 'REVIEWED', urgent: dueInfo(row.deadlineAt).urgent && myStatus(row) === 'NOT_SUBMITTED' }">
<Icon type="md-time" size="14" /> 截止 {{ row.deadlineAt ? String(row.deadlineAt).slice(5, 16) : "—" }}({{ dueInfo(row.deadlineAt).text }})
</span>
</div>
<div class="c-body" v-html="row.content"></div>
<div class="c-imgs" v-if="row.my && row.my.images && row.my.images.length">
<div class="th" v-for="(im, ii) in row.my.images" :key="ii" @click="zoom(previewUrl(im))"><img :src="previewUrl(im)" /></div>
</div>
<div class="c-grade" v-if="row.my && (row.my.submitStatus === 'graded' || row.my.submitStatus === 'redone')">
<span class="g-badge" v-if="row.my.isExcellent">优秀</span>
<Icon type="md-checkmark-circle" color="#19be6b" size="16" />
得分 {{ row.my.score ?? "—" }} · 等级 {{ row.my.level ?? "—" }} · {{ row.my.comment }}
<!-- 待订正:老师评语强化展示 -->
<div class="c-grade warn" v-if="myStatus(row) === 'RETURNED'">
<Icon type="md-refresh" color="#ed4014" size="16" /> 老师要求订正:{{ row.my?.comment || "请订正后重新提交" }}
</div>
<div class="c-grade warn" v-else-if="row.my && row.my.submitStatus === 'needRedo'">
<Icon type="md-refresh" color="#ed4014" size="16" /> 老师要求订正:{{ row.my.comment }}
<!-- 已批阅:得分摘要 -->
<div class="c-grade" v-else-if="myStatus(row) === 'REVIEWED'">
<Icon type="md-checkmark-circle" color="#19be6b" size="16" />
得分 {{ row.my?.score ?? "—" }}<template v-if="row.my?.comment"> · {{ row.my.comment }}</template>
</div>
<div class="c-actions">
<Button
v-if="bucketOf(row) === 'todo' && (row.hwStatus !== 'closed' || row.allowLate)"
type="primary"
icon="md-cloud-upload"
size="small"
@click="openSubmit(row)"
>{{ row.hwStatus === "closed" && row.allowLate ? "补交作业" : "提交作业" }}</Button
>
<Button v-else-if="bucketOf(row) === 'todo'" disabled size="small">已截止</Button>
<Button v-else-if="bucketOf(row) === 'redo'" type="error" icon="md-refresh" size="small" @click="openSubmit(row)">重新提交</Button>
<Button v-if="bucketOf(row) === 'submitted'" ghost icon="md-create" size="small" @click="openSubmit(row)">修改提交</Button>
<Button
v-if="row.my && ['graded', 'redone', 'needRedo', 'submitted'].includes(row.my.submitStatus)"
icon="md-eye"
size="small"
@click="openFeedback(row)"
>{{ row.my.submitStatus === "graded" || row.my.submitStatus === "redone" ? "查看批改" : "查看提交" }}</Button
>
<Button v-if="['NOT_SUBMITTED'].includes(myStatus(row)) && canSubmit(row)" type="primary" icon="md-cloud-upload" size="small" @click="openSubmit(row)">
{{ row.hwStatus === "CLOSED" ? "补交作业" : "提交作业" }}</Button>
<Button v-else-if="myStatus(row) === 'RETURNED'" type="error" icon="md-refresh" size="small" @click="openSubmit(row)">重新提交</Button>
<template v-else-if="['SUBMITTED', 'LATE_SUBMITTED'].includes(myStatus(row)) && row.hwStatus === 'PUBLISHED'">
<Button ghost icon="md-create" size="small" @click="openSubmit(row)">修改提交</Button>
<Button icon="md-undo" size="small" @click="withdraw(row)">撤回</Button>
</template>
<Button icon="md-eye" size="small" @click="goDetail(row)">{{ myStatus(row) === "REVIEWED" || myStatus(row) === "RETURNED" ? "查看批阅" : "查看详情" }}</Button>
</div>
</div>
</div>
<!-- 提交弹窗 -->
<Modal v-model="sub.open" :title="(sub.isRedo ? '重新提交 · ' : '提交作业 · ') + sub.title" width="560px" class-name="submit-modal">
<div class="page-panel" v-if="page.total > pageSize">
<Page class="page-box fr" :total="page.total" :current="pageNum" :page-size="pageSize" @on-change="pageUpdate" show-total></Page>
</div>
<!-- 快捷提交弹窗 -->
<Modal v-model="sub.open" :title="(sub.mode === 'resubmit' ? '重新提交 · ' : '提交作业 · ') + sub.title" width="560px" class-name="submit-modal">
<Spin v-if="sub.loadingEcho" fix />
<div class="sm-field">
<div class="sm-label"><Icon type="md-create" size="16" /> 文字作答</div>
<Input v-model="sub.text" type="textarea" :rows="4" placeholder="输入作答内容"></Input>
</div>
<div class="sm-field">
<div class="sm-label"><Icon type="md-image" size="16" /> 图片(拍照 / 相册,最多 9 张)</div>
<div class="img-wall">
<div class="img-cell" v-for="(u, ui) in sub.images" :key="ui">
<img :src="previewUrl(u)" />
<span class="img-del" @click="removeImg(u)"><Icon type="md-close" size="14" /></span>
</div>
<Upload
class="img-add"
:action="imgUploadUrl"
:show-upload-list="false"
:format="['jpg', 'jpeg', 'png', 'gif']"
accept="image/*"
multiple
name="file"
:before-upload="() => (sub.uploading = true)"
:on-success="onImgSuccess"
:on-error="() => { sub.uploading = false; Message.error('图片上传失败'); }"
>
<div class="img-add-btn" :class="{ spin: sub.uploading }">
<Icon :type="sub.uploading ? 'ios-loading' : 'ios-camera'" :size="24" :class="{ 'iv-load': sub.uploading }" />
<span>{{ sub.uploading ? "上传中" : "拍照/上传" }}</span>
</div>
</Upload>
</div>
</div>
<div class="sm-field">
<div class="sm-label"><Icon type="md-attach" size="16" /> 附件</div>
<HomeworkUpload v-model:fileids="sub.fileids"></HomeworkUpload>
<div class="sm-label"><Icon type="md-image" size="16" /> 图片 / 附件(先上传后提交)</div>
<AttachmentUpload v-model="sub.attachments"></AttachmentUpload>
</div>
<template #footer>
<Button @click="sub.open = false">取消</Button>
@@ -261,30 +303,6 @@ function zoom(url: string) {
</template>
</Modal>
<!-- 批改反馈弹窗 -->
<Modal v-model="fb.open" title="批改反馈" width="520px">
<div v-if="fb.row && fb.row.my" class="fb">
<div class="fb-row"><span class="fb-k">提交时间</span><span>{{ fb.row.my.submittedAt ? String(fb.row.my.submittedAt).slice(0, 16) : "—" }}</span></div>
<div class="fb-row"><span class="fb-k">我的作答</span><span class="fb-text">{{ fb.row.my.text || "(仅附件)" }}</span></div>
<div class="fb-row">
<span class="fb-k">提交图片</span>
<span class="fb-imgs">
<template v-if="fb.row.my.images && fb.row.my.images.length">
<img v-for="(im, ii) in fb.row.my.images" :key="ii" :src="previewUrl(im)" @click="zoom(previewUrl(im))" />
</template>
<template v-else>无</template>
</span>
</div>
<div class="fb-score-box" :class="fb.row.my.isExcellent ? 'ex' : ''">
<div class="sc"><b>{{ fb.row.my.score ?? "—" }}</b><i>得分</i></div>
<div class="sc"><b>{{ fb.row.my.level || "—" }}</b><i>等级</i></div>
<div class="cm">{{ fb.row.my.comment || "—" }}</div>
<Tag v-if="fb.row.my.isExcellent" color="success">优秀作业</Tag>
</div>
</div>
<template #footer><Button type="primary" @click="fb.open = false">关闭</Button></template>
</Modal>
<!-- 图片放大 -->
<Modal v-model="imgPreview.open" footer-hide title="图片预览" width="680px">
<div class="zoom-wrap"><img :src="imgPreview.url" /></div>
@@ -352,7 +370,10 @@ function zoom(url: string) {
}
}
.stu-tabs {
margin-bottom: 6px;
margin-bottom: 0;
:deep(.ivu-tabs-bar) {
margin-bottom: 0;
}
}
.state {
padding: 30px 0;
@@ -375,10 +396,16 @@ function zoom(url: string) {
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
}
&.redo {
border-color: #f5b0a6;
border-left-color: #ed4014;
background: #fffaf9;
}
.c-top {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
.c-subj {
background: #eaf3ff;
color: #2d8cf0;
@@ -395,15 +422,19 @@ function zoom(url: string) {
.c-flag {
margin-left: auto;
}
.c-dot {
width: 8px;
height: 8px;
border-radius: 50%;
margin-left: auto;
.c-late {
background: #ff9900;
color: #fff;
border-radius: 3px;
font-size: 11px;
padding: 0 4px;
flex: none;
}
.c-status {
margin-left: auto;
font-size: 13px;
font-weight: 600;
flex: none;
}
}
.c-meta {
@@ -420,30 +451,8 @@ function zoom(url: string) {
.overdue {
color: #ed4014;
}
}
.c-body {
margin-top: 8px;
color: #515a6e;
line-height: 1.6;
max-height: 72px;
overflow: hidden;
}
.c-imgs {
display: flex;
gap: 8px;
margin-top: 10px;
.th {
width: 72px;
height: 72px;
border-radius: 6px;
overflow: hidden;
cursor: zoom-in;
border: 1px solid #eee;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.urgent {
color: #ff9900;
}
}
.c-grade {
@@ -460,12 +469,6 @@ function zoom(url: string) {
background: #fff1f0;
color: #cf1322;
}
.g-badge {
background: #ff9900;
color: #fff;
border-radius: 4px;
padding: 0 6px;
}
}
.c-actions {
margin-top: 12px;
@@ -485,122 +488,6 @@ function zoom(url: string) {
gap: 4px;
}
}
.img-wall {
display: flex;
flex-wrap: wrap;
gap: 8px;
.img-cell {
position: relative;
width: 80px;
height: 80px;
border: 1px solid #e8eaec;
border-radius: 6px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.img-del {
position: absolute;
top: 0;
right: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.55);
color: #fff;
cursor: pointer;
border-bottom-left-radius: 6px;
}
}
.img-add-btn {
width: 80px;
height: 80px;
border: 1px dashed #c5c8ce;
border-radius: 6px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
color: #808695;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: #2d8cf0;
color: #2d8cf0;
}
}
}
.iv-load {
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 反馈 */
.fb {
.fb-row {
display: flex;
padding: 8px 0;
border-bottom: 1px dashed #eee;
.fb-k {
width: 76px;
color: #808695;
flex: none;
}
.fb-text {
color: #515a6e;
}
.fb-imgs {
display: flex;
gap: 6px;
img {
width: 60px;
height: 60px;
object-fit: cover;
border-radius: 6px;
border: 1px solid #eee;
cursor: zoom-in;
}
}
}
.fb-score-box {
margin-top: 12px;
padding: 12px;
background: #f7faff;
border-radius: 8px;
display: flex;
align-items: center;
gap: 20px;
&.ex {
background: #fff8ec;
}
.sc {
text-align: center;
b {
font-size: 22px;
color: #ed4014;
}
i {
display: block;
font-style: normal;
font-size: 12px;
color: #808695;
}
}
.cm {
flex: 1;
color: #515a6e;
}
}
}
.zoom-wrap {
text-align: center;
img {