feat(M5): P0 契约层 + 审批/应用设置下线
- types/homeworkV2.ts:V2 全量类型契约(五态/枚举/矩阵/版本/通知)
- teacherHomework/studentHomework/notification 三客户端(28 端点)
- §6 审批功能下线:删 ApprovalList/ApprovalSetting/ApprovalFlowEditor,
types/api/menu/menuRoute/App/mock 同步清理,DROPPED_MOD_IDS={5,6}
- §7 应用设置下线:删 HomeworkSetting/TypeSetBox/UserCheckInput,
SettingApi 与 dict/contacts 死代码清理,DROPPED_MOD_IDS 扩 {21}
- 新增 V2 组件:TeacherHomeworks(已发布+草稿箱)、ReviewModal(批阅弹窗)、
AttachmentUpload(附件上传)——尚未挂路由,页面切换随后续阶段
This commit is contained in:
@@ -5,10 +5,18 @@ 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 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 role = computed(() => (route.meta as any).role || "student");
|
||||
const isParent = computed(() => role.value === "parent");
|
||||
|
||||
const TABS = [
|
||||
{ name: "all", label: "全部" },
|
||||
@@ -48,34 +56,63 @@ const filtered = computed(() => {
|
||||
if (tab.value === "all") return list;
|
||||
return list.filter((x) => bucketOf(x) === tab.value);
|
||||
});
|
||||
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: "default", submitted: "warning", graded: "success", needRedo: "error", redone: "primary" };
|
||||
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"];
|
||||
}
|
||||
|
||||
function dueInfo(deadline: string | null) {
|
||||
if (!deadline) return { text: "无截止", overdue: false };
|
||||
const d = new Date(String(deadline).replace(/-/g, "/")).getTime();
|
||||
const now = Date.now();
|
||||
const diffH = (d - now) / 3600000;
|
||||
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 };
|
||||
}
|
||||
|
||||
/* ============ 提交 / 重交 ============ */
|
||||
const sub = reactive({ open: false, serid: 0, title: "", text: "", fileids: [] as number[], isRedo: false, submitting: false });
|
||||
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) {
|
||||
sub.open = true;
|
||||
sub.serid = 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;
|
||||
}
|
||||
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, attachments: sub.fileids.join(",") }).send();
|
||||
await homeworkApi.submit({ serid: sub.serid, text: sub.text, images: sub.images, attachments: sub.fileids.map(String) }).send();
|
||||
Message.success("提交成功");
|
||||
sub.open = false;
|
||||
load();
|
||||
@@ -93,215 +130,482 @@ function openFeedback(row: StudentHomeworkVo) {
|
||||
fb.row = row;
|
||||
}
|
||||
|
||||
/* ============ 家长提醒 ============ */
|
||||
async function nudge(row: StudentHomeworkVo) {
|
||||
try {
|
||||
await homeworkApi.parentNudge(row.id).send();
|
||||
Message.success("已提醒孩子");
|
||||
} catch (e: any) {
|
||||
Message.error(e.message || "提醒失败");
|
||||
}
|
||||
/* ============ 图片放大预览 ============ */
|
||||
const imgPreview = reactive({ open: false, url: "" });
|
||||
function zoom(url: string) {
|
||||
imgPreview.url = url;
|
||||
imgPreview.open = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-wrapper">
|
||||
<div class="main-container">
|
||||
<div class="head fix mb-12">
|
||||
<span class="h-title">{{ isParent ? "孩子作业" : "我的作业" }}</span>
|
||||
<span class="h-sub" v-if="data">{{ isParent ? "(只读查看 + 提醒)" : "待完成 " + data.buckets.todo + " · 待订正 " + data.buckets.redo }}</span>
|
||||
<div class="main-container stu">
|
||||
<!-- 顶部概览 -->
|
||||
<div class="stu-head">
|
||||
<div class="sh-left">
|
||||
<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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
<Tabs :value="tab" @on-click="(n: string) => (tab = n)" class="mb-12">
|
||||
<TabPane v-for="t in TABS" :key="t.name" :name="t.name" :label="t.label + (t.name !== 'all' && data ? ' ' + (t.name==='todo'?data.buckets.todo:t.name==='submitted'?data.buckets.submitted:t.name==='graded'?data.buckets.graded:data.buckets.redo) : '')"></TabPane>
|
||||
<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>
|
||||
|
||||
<Spin v-if="loading" fix />
|
||||
<p v-if="!loading && !filtered.length" class="tip">该分类下暂无作业。</p>
|
||||
<div v-if="loading" class="state"><Spin fix /><div style="height: 120px"></div></div>
|
||||
<div v-else-if="!filtered.length" class="state empty">
|
||||
<Icon type="ios-albums-outline" size="52" color="#c5c8ce" />
|
||||
<p>该分类下暂无作业</p>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card" v-for="row in filtered" :key="row.id">
|
||||
<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>
|
||||
<span class="c-title">{{ row.title }}</span>
|
||||
<Tag v-if="row.viewOnly" color="default">仅查阅</Tag>
|
||||
<Tag v-else :color="STATUS_COLOR[row.my ? row.my.submitStatus : 'unsubmitted']">{{ STATUS_TEXT[row.my ? row.my.submitStatus : "unsubmitted"] }}</Tag>
|
||||
<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>
|
||||
</div>
|
||||
<div class="c-meta">
|
||||
<span>{{ row.publisherName }} · {{ row.deptName }}</span>
|
||||
<span :class="{ overdue: dueInfo(row.deadline).overdue && bucketOf(row) === 'todo' }">截止 {{ row.deadline ? String(row.deadline).slice(5, 16) : "—" }}({{ dueInfo(row.deadline).text }})</span>
|
||||
<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>
|
||||
</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>
|
||||
<div class="c-grade warn" v-else-if="row.my && row.my.submitStatus === 'needRedo'">
|
||||
老师要求订正:{{ row.my.comment }}
|
||||
<Icon type="md-refresh" color="#ed4014" size="16" /> 老师要求订正:{{ row.my.comment }}
|
||||
</div>
|
||||
|
||||
<div class="c-actions">
|
||||
<template v-if="!isParent">
|
||||
<Button v-if="bucketOf(row) === 'todo'" type="primary" size="small" @click="openSubmit(row)">提交作业</Button>
|
||||
<Button v-else-if="bucketOf(row) === 'redo'" type="error" size="small" @click="openSubmit(row)">重新提交</Button>
|
||||
<Button
|
||||
v-if="row.my && (row.my.submitStatus === 'graded' || row.my.submitStatus === 'redone' || row.my.submitStatus === 'needRedo')"
|
||||
size="small"
|
||||
@click="openFeedback(row)"
|
||||
>查看批改</Button
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Button
|
||||
v-if="row.my && (row.my.submitStatus === 'graded' || row.my.submitStatus === 'redone' || row.my.submitStatus === 'needRedo')"
|
||||
size="small"
|
||||
@click="openFeedback(row)"
|
||||
>查看批改</Button
|
||||
>
|
||||
<Button v-if="bucketOf(row) === 'todo' || bucketOf(row) === 'redo'" type="primary" size="small" ghost @click="nudge(row)">提醒孩子</Button>
|
||||
</template>
|
||||
<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
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提交弹窗 -->
|
||||
<Modal v-model="sub.open" :title="(sub.isRedo ? '重新提交 · ' : '提交作业 · ') + sub.title" width="560px">
|
||||
<Form :label-width="72">
|
||||
<FormItem label="文字作答">
|
||||
<Input v-model="sub.text" type="textarea" :rows="4" placeholder="输入作答内容"></Input>
|
||||
</FormItem>
|
||||
<FormItem label="图片/附件">
|
||||
<HomeworkUpload v-model:fileids="sub.fileids"></HomeworkUpload>
|
||||
</FormItem>
|
||||
</Form>
|
||||
<Modal v-model="sub.open" :title="(sub.isRedo ? '重新提交 · ' : '提交作业 · ') + sub.title" width="560px" class-name="submit-modal">
|
||||
<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>
|
||||
<template #footer>
|
||||
<Button @click="sub.open = false">取消</Button>
|
||||
<Button type="primary" :loading="sub.submitting" @click="submit">提交</Button>
|
||||
<Button type="primary" :loading="sub.submitting" icon="md-send" @click="submit">提交</Button>
|
||||
</template>
|
||||
</Modal>
|
||||
|
||||
<!-- 批改反馈弹窗 -->
|
||||
<Modal v-model="fb.open" title="批改反馈" width="480px">
|
||||
<Modal v-model="fb.open" title="批改反馈" width="520px">
|
||||
<div v-if="fb.row && fb.row.my" class="fb">
|
||||
<div class="fb-row"><span>学生提交</span><span>{{ fb.row.my.submittedAt ? String(fb.row.my.submittedAt).slice(0, 16) : "—" }}</span></div>
|
||||
<div class="fb-row"><span>作答内容</span><span class="fb-text">{{ fb.row.my.text || "(仅附件)" }}</span></div>
|
||||
<div class="fb-row"><span>得分</span><span class="fb-score">{{ fb.row.my.score ?? "—" }}</span></div>
|
||||
<div class="fb-row"><span>等级</span><span>{{ fb.row.my.level || "—" }}</span></div>
|
||||
<div class="fb-row"><span>评语</span><span>{{ fb.row.my.comment || "—" }}</span></div>
|
||||
<div class="fb-row" v-if="fb.row.my.isExcellent"><span></span><Tag color="success">优秀作业</Tag></div>
|
||||
<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>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.head {
|
||||
.h-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #17233d;
|
||||
.stu {
|
||||
max-width: 960px;
|
||||
}
|
||||
.stu-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: linear-gradient(135deg, #2d8cf0, #5cadff);
|
||||
border-radius: 10px;
|
||||
padding: 16px 20px;
|
||||
color: #fff;
|
||||
margin-bottom: 14px;
|
||||
box-shadow: 0 4px 14px rgba(45, 140, 240, 0.25);
|
||||
.sh-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.h-sub {
|
||||
margin-left: 10px;
|
||||
.sh-icon {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.sh-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.sh-sub {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.sh-chips {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
.chip {
|
||||
min-width: 66px;
|
||||
text-align: center;
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
b {
|
||||
display: block;
|
||||
font-size: 20px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
span {
|
||||
font-size: 12px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.stu-tabs {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.state {
|
||||
padding: 30px 0;
|
||||
text-align: center;
|
||||
&.empty {
|
||||
color: #a6aab0;
|
||||
p {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.cards .card {
|
||||
background: #fff;
|
||||
border: 1px solid #eceef1;
|
||||
border-left: 4px solid #c5c8ce;
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 12px;
|
||||
transition: box-shadow 0.2s;
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.c-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
.c-subj {
|
||||
background: #eaf3ff;
|
||||
color: #2d8cf0;
|
||||
border-radius: 4px;
|
||||
padding: 2px 9px;
|
||||
font-size: 12px;
|
||||
flex: none;
|
||||
}
|
||||
.c-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #17233d;
|
||||
}
|
||||
.c-flag {
|
||||
margin-left: auto;
|
||||
}
|
||||
.c-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-left: auto;
|
||||
}
|
||||
.c-status {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
.c-meta {
|
||||
display: flex;
|
||||
gap: 18px;
|
||||
margin-top: 8px;
|
||||
color: #808695;
|
||||
font-size: 13px;
|
||||
.m-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
.c-grade {
|
||||
margin-top: 10px;
|
||||
padding: 8px 12px;
|
||||
background: #f6ffed;
|
||||
border-radius: 6px;
|
||||
color: #389e0d;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
&.warn {
|
||||
background: #fff1f0;
|
||||
color: #cf1322;
|
||||
}
|
||||
.g-badge {
|
||||
background: #ff9900;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
.c-actions {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
.mb-12 {
|
||||
margin-bottom: 12px;
|
||||
/* 提交弹窗 */
|
||||
.sm-field {
|
||||
margin-bottom: 14px;
|
||||
.sm-label {
|
||||
font-size: 13px;
|
||||
color: #515a6e;
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
.tip {
|
||||
color: #808695;
|
||||
padding: 16px 0;
|
||||
}
|
||||
.cards {
|
||||
.card {
|
||||
.img-wall {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
.img-cell {
|
||||
position: relative;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 1px solid #e8eaec;
|
||||
border-radius: 6px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 12px;
|
||||
background: #fff;
|
||||
.c-top {
|
||||
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;
|
||||
gap: 10px;
|
||||
.c-subj {
|
||||
background: #e8f3ff;
|
||||
color: #2d8cf0;
|
||||
border-radius: 3px;
|
||||
padding: 1px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.c-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #17233d;
|
||||
}
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
.c-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: #808695;
|
||||
font-size: 13px;
|
||||
margin-top: 6px;
|
||||
.overdue {
|
||||
color: #ed4014;
|
||||
}
|
||||
}
|
||||
.c-body {
|
||||
margin-top: 8px;
|
||||
color: #515a6e;
|
||||
line-height: 1.6;
|
||||
max-height: 76px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.c-grade {
|
||||
margin-top: 8px;
|
||||
padding: 6px 10px;
|
||||
background: #f6ffed;
|
||||
border-radius: 4px;
|
||||
color: #389e0d;
|
||||
font-size: 13px;
|
||||
&.warn {
|
||||
background: #fff2f0;
|
||||
color: #cf1322;
|
||||
}
|
||||
.g-badge {
|
||||
background: #ff9900;
|
||||
color: #fff;
|
||||
border-radius: 3px;
|
||||
padding: 0 6px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
.c-actions {
|
||||
margin-top: 12px;
|
||||
button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
.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;
|
||||
span:first-child {
|
||||
width: 80px;
|
||||
.fb-k {
|
||||
width: 76px;
|
||||
color: #808695;
|
||||
}
|
||||
.fb-score {
|
||||
font-weight: 600;
|
||||
color: #ed4014;
|
||||
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 {
|
||||
max-width: 100%;
|
||||
max-height: 62vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user