Files
coursehomework-front/src/views/homework/TeacherHomeworks.vue
T
lijing bb7a832290 feat(theme): 接入云海子主题通道(A 类免插件,规范 v1.12 §2/§3)
本次同时包含会话前遗留的本地未提交改动(作业模块 M5 相关页面/接口/样式调整、
host.config 的 SSO 兜底推导、.gitignore 等),按团队确认一并提交。

主题通道部分:
- index.html 插 A 类 loader:唯一接入点,FOUC 门/manifest 预取/主题 link 追加与排序
  全由通道 yh-theme-init.js 承担,主题升级走 /static 通道,本应用无需重新构建
- vite.config 加 /static dev 代理(YH_THEME_PROXY_TARGET 可覆盖,默认线上通道)
- host.config 回写 __APP_CONFIG__.serviceHost(仅同源):init 的 appearance 请求有硬性
  同源限制,跨源会被直接跳过;本地 dev 保持空串走同源 /api 代理
- 顶栏头像接平台键 yh-avatar,监听 yh-appearance-updated,userInfo 兜底
- view-ui-plus 声明锁 1.3.24(对齐通道皮肤生成版本)
- .gitea/workflows/build.yml:CI 平台统一更新(构建容器名唯一化 + npm 走 npmmirror)
2026-10-21 17:38:11 +08:00

732 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { ref, reactive, computed, watch, onMounted, onBeforeUnmount, nextTick } from "vue";
import { useRouter } from "vue-router";
import { useRequest } from "alova/client";
import { Message } from "view-ui-plus";
import { teacherApi } from "@/api/module/teacherHomework";
import type { HomeworkV2Vo, TeacherHomeworkQuery } from "@/types/homeworkV2";
import type { BasePageResult, SubjectVo } from "@/types/homeworkV2";
import MyConfirm from "@/components/MyConfirm";
import Utils from "@/utils";
const router = useRouter();
/* 学科下拉绑定 /subjects/assigned(布置过 ∪ 任教,subId=筛选值与列表 subject 精确匹配);
* 接口失败或为空时降级为内置清单,保证筛选始终可用 */
const SUBJECTS = ["语文", "数学", "英语", "物理", "化学", "生物", "道德与法治", "历史", "地理", "科学", "综合实践"];
const subjectOpts = ref<SubjectVo[]>(SUBJECTS.map((s) => ({ subId: s, subName: s })));
async function loadSubjects() {
try {
const list = await teacherApi.assignedSubjects().send();
if (Array.isArray(list) && list.length) subjectOpts.value = list;
} catch {
/* 降级用内置清单 */
}
}
loadSubjects();
/* ============ 视图:单行 Tab(已发布/进行中/已截止/已归档 + 草稿箱,草稿箱用分隔线隔开,带数量) ============ */
type ViewMode = "published" | "drafts";
const viewMode = ref<ViewMode>("published");
/* 已发布状态筛选(空 = 已发布全部状态) */
const pubStatus = ref("");
const TAB_DEFS = [
{ name: "", label: "已发布" },
{ name: "PUBLISHED", label: "进行中" },
{ name: "CLOSED", label: "已截止" },
{ name: "ARCHIVED", label: "已归档" },
{ name: "drafts", label: "草稿箱" },
];
/* Tab 数量(接口无汇总端点,用 pageSize=1 的分页 total 汇出;随关键字/学科筛选刷新) */
const tabCounts = ref<Record<string, number>>({});
async function loadCounts() {
const base = { keyword: keyword.value || undefined, subject: subject.value || undefined, pageIndex: 1, pageSize: 1 };
try {
const [pub, closed, arch, draft] = await Promise.all([
teacherApi.published({ ...base, hwStatus: "PUBLISHED" }).send(),
teacherApi.published({ ...base, hwStatus: "CLOSED" }).send(),
teacherApi.published({ ...base, hwStatus: "ARCHIVED" }).send(),
teacherApi.drafts(base).send(),
]);
tabCounts.value = {
"": pub.total + closed.total + arch.total,
PUBLISHED: pub.total,
CLOSED: closed.total,
ARCHIVED: arch.total,
drafts: draft.total,
};
} catch {
/* 数量加载失败不影响主列表 */
}
}
/* Tab 标签用 render:文字 + 计数徽标(数量未加载时只显示文字) */
const PUB_TABS = computed(() =>
TAB_DEFS.map((t) => ({
name: t.name,
label: (h: any) =>
h("span", { class: "hw-tab" }, [
h("span", { class: "hw-tab-text" }, t.label),
tabCounts.value[t.name] === undefined
? null
: h("span", { class: "hw-tab-count" }, String(tabCounts.value[t.name])),
]),
}))
);
const tabValue = computed(() => (viewMode.value === "drafts" ? "drafts" : pubStatus.value));
/* ============ 搜索条件 ============ */
const keyword = ref("");
const subject = ref("");
/* ============ 分页 ============ */
const pageNum = ref(1);
const pageSize = ref(parseInt(localStorage.getItem("customPageCount") || "10"));
const EMPTY_PAGE: BasePageResult<HomeworkV2Vo> = { content: [], total: 0, pageIndex: 1, pageSize: 10, totalPages: 0, hasNext: false };
const { data: page, loading, send: reload } = useRequest((params: TeacherHomeworkQuery, mode: ViewMode) =>
mode === "drafts" ? teacherApi.drafts(params) : teacherApi.published(params),
{ immediate: false, initialData: EMPTY_PAGE });
async function run() {
const params: TeacherHomeworkQuery = {
keyword: keyword.value || undefined,
subject: subject.value || undefined,
pageIndex: pageNum.value,
pageSize: pageSize.value,
};
if (viewMode.value === "published" && pubStatus.value) params.hwStatus = pubStatus.value;
loadCounts();
try {
await reload(params, viewMode.value);
} catch (e: any) {
Message.error(e.message || "获取作业列表失败");
}
}
function doSearch() {
pageNum.value = 1;
run();
}
function onTab(name: string) {
if (name === "drafts") {
viewMode.value = "drafts";
} else {
viewMode.value = "published";
pubStatus.value = name;
}
doSearch();
}
function pageUpdate(p: number) {
pageNum.value = p;
run();
}
function pageSizeUpdate(size: number) {
pageNum.value = 1;
pageSize.value = size;
localStorage.setItem("customPageCount", String(size));
run();
}
run();
watch(
() => router.currentRoute.value.path,
() => {
pageNum.value = 1;
keyword.value = "";
subject.value = "";
pubStatus.value = "";
viewMode.value = "published";
run();
}
);
/* ============ 状态列 & 操作列(与「全部作业」共用 Utils 里那一套口径) ============ */
/**
* 状态:统一走 Utils.hwStatusPill(状态图标 + 文字 + 同色浅底)。
* 之前这里用 iview Tag、全部作业用圆点,同一个模块两套观感;现在两边同一张状态表,
* 颜色/图标/文案都只在 utils/index.ts 的 HW_STATUS_META 里定义一次。
*/
function statusCell(h: any, code: string | undefined, text?: string) {
return Utils.hwStatusPill(h, code, text);
}
function fmtTime(v?: string | null): string {
return v ? String(v).slice(0, 16) : "—";
}
/** 短字段单元格:强制单行(时间、状态这类短文本折行很难看,且会顶破 48px 行高) */
function nowrapCell(h: any, text: string, style = "") {
return h("span", { style: `white-space:nowrap;${style}` }, text);
}
/** 长字段单元格:单行 + 省略号,悬停由原生 title 看全文(与宿舍通知「接收宿舍」同一手法) */
function ellipsisCell(h: any, text: string) {
return h(
"span",
{
title: text,
style: "display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle;",
},
text
);
}
/* ============ 行操作 ============ */
function viewDetail(row: HomeworkV2Vo) {
router.push({ path: "/list/detail", query: { serid: String(row.id) } });
}
function viewSubmissions(row: HomeworkV2Vo) {
router.push({ path: "/submissions", query: { serid: String(row.id) } });
}
function toGrade(row: HomeworkV2Vo) {
router.push({ path: "/grade", query: { serid: String(row.id) } });
}
/** 勾选了任一提交渠道(拍照/图片、上传附件、文字作答)=教师需批改 → 显示批阅入口;
* 仅查阅(none)=学生只预览,不显示批阅 */
function needGrade(row: HomeworkV2Vo): boolean {
return (row.submitType || []).some((t) => t !== "none");
}
function edit(row: HomeworkV2Vo) {
router.push({ path: "/list/new", query: { serid: String(row.id) } });
}
function remind(row: HomeworkV2Vo, type: "NOT_READ" | "NOT_SUBMITTED") {
const label = type === "NOT_READ" ? "未读" : "未交";
MyConfirm({
title: "提醒",
msg: `向“ ${row.title} ”的${label}学生发送提醒?`,
async onOk() {
try {
const r = await teacherApi.remind(row.id, type).send();
Message.success(`已提醒 ${r?.reminded ?? 0} 位${label}学生`);
} catch (e: any) {
Message.error(e.message || "提醒失败");
}
},
});
}
/* 延长截止(仅进行中;新截止须晚于 now 与原截止) */
const extendModal = reactive({ open: false, row: null as HomeworkV2Vo | null, time: null as Date | null, saving: false });
function openExtend(row: HomeworkV2Vo) {
extendModal.row = row;
extendModal.time = null;
extendModal.open = true;
}
async function doExtend() {
const row = extendModal.row;
if (!row) return;
if (!extendModal.time) {
Message.info("请选择新的截止时间");
return;
}
const d = extendModal.time;
const pad = (n: number) => String(n).padStart(2, "0");
const at = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:00`;
if (row.deadlineAt && at <= String(row.deadlineAt)) {
Message.info("新截止时间必须晚于原截止时间");
return;
}
extendModal.saving = true;
try {
await teacherApi.extend(row.id, at).send();
Message.success("截止时间已延长");
extendModal.open = false;
run();
} catch (e: any) {
Message.error(e.message || "延长截止失败");
} finally {
extendModal.saving = false;
}
}
function copyRow(row: HomeworkV2Vo) {
MyConfirm({
title: "复制",
msg: `复制“ ${row.title} ”为新草稿?`,
info: "复制作业内容与班级设置,不含提交与阅读数据",
async onOk() {
try {
const newId = await teacherApi.copy(row.id).send();
Message.success("已复制为新草稿");
router.push({ path: "/list/new", query: { serid: String(newId) } });
} catch (e: any) {
Message.error(e.message || "复制失败");
}
},
});
}
function archive(row: HomeworkV2Vo) {
MyConfirm({
title: "归档",
msg: `归档“ ${row.title} ”?`,
info: "归档后作业只读,不再触发任何提醒",
async onOk() {
try {
await teacherApi.archive(row.id).send();
Message.success("已归档");
run();
} catch (e: any) {
Message.error(e.message || "归档失败");
}
},
});
}
function doExport(row: HomeworkV2Vo) {
window.location.href = teacherApi.exportUrl(row.id);
}
/* 草稿箱操作 */
function publishDraft(row: HomeworkV2Vo) {
MyConfirm({
title: "发布",
msg: `立即发布“ ${row.title} ”?`,
info: "发布后参与班级学生将收到作业通知",
async onOk() {
try {
await teacherApi.publish(row.id).send();
Message.success("发布成功");
viewMode.value = "published";
pubStatus.value = "";
pageNum.value = 1;
run();
} catch (e: any) {
Message.error(e.message || "发布失败");
}
},
});
}
function cancelSchedule(row: HomeworkV2Vo) {
MyConfirm({
title: "取消定时",
msg: `取消“ ${row.title} ”的定时发布?`,
info: "取消后作业回到草稿状态",
async onOk() {
try {
await teacherApi.schedule(row.id, { cancel: true }).send();
Message.success("已取消定时");
run();
} catch (e: any) {
Message.error(e.message || "取消定时失败");
}
},
});
}
function del(row: HomeworkV2Vo) {
MyConfirm({
title: "删除",
msg: `确定删除“ ${row.title} ”这条作业吗?`,
info: "删除后不可恢复",
async onOk() {
try {
await teacherApi.remove(row.id).send();
Message.success("删除成功");
if ((page.value?.content?.length || 0) <= 1 && pageNum.value > 1) pageNum.value -= 1;
run();
} catch (e: any) {
Message.error(e.message || "删除失败");
}
},
});
}
/* ============ 已发布列表列 ============ */
/**
* 列宽口径(对齐宿舍通知 NoticeList):
* - 序号列 ≥ 76:iview 单元格左右各 18px padding,「序号」两字约 36px,给 60 会被挤成「序/号」两行;
* - 「未读未交」「待批阅」「提交率」这类 3~4 字表头各自留足一字不折行的宽度;
* - 时间列 150 + nowrap:130 会把「2026-09-15 10:00」折成两行;
* - 班级单行省略,悬停由原生 title 看全文;
* - 列宽合计 ~1700px,宽屏也保留横向滚动(滚动条 8px 圆角的样式见 App.vue 的
* .ivu-table-overflowX 覆写)。**不要再为了「收进一屏」去压列宽或合并列** ——
* 把列压窄只会让表头折行,越改越难看。
*/
const pubColumns = computed(() => [
{ title: "序号", type: "index", width: 76, align: "center" },
{
title: "标题",
key: "title",
minWidth: 300,
align: "left",
tooltip: true,
/* 标题是长自由文本,列宽再大也会超 —— 单行 + 省略号,悬停用原生 title 看全文
* (宿舍通知 NoticeList 的 titleCell 就是「蓝色链接 + title 属性」,这里补上省略号,
* 否则长标题会折成两行、和「全部作业」页的单行样式不一致) */
render: (h: any, p: any) =>
h(
"a",
{
style: "display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;vertical-align:middle;color:#2d8cf0;",
title: p.row.title,
onClick: () => viewDetail(p.row),
},
() => p.row.title
),
},
{ title: "学科", key: "subjectName", width: 120, align: "center" },
{
title: "班级",
width: 190,
align: "left",
render: (h: any, p: any) => ellipsisCell(h, (p.row.classes || []).map((c: any) => c.className).join("、") || "—"),
},
{ title: "发布时间", width: 170, align: "left", render: (h: any, p: any) => nowrapCell(h, fmtTime(p.row.publishedAt)) },
{ title: "截止时间", width: 170, align: "left", render: (h: any, p: any) => nowrapCell(h, fmtTime(p.row.deadlineAt)) },
{
title: "应交",
width: 70,
align: "center",
render: (h: any, p: any) => h("span", {}, String(p.row.progress?.should ?? "—")),
},
{
title: "已读",
width: 70,
align: "center",
render: (h: any, p: any) => h("span", {}, String(p.row.progress?.read ?? "—")),
},
{
title: "已交",
width: 70,
align: "center",
render: (h: any, p: any) => h("span", {}, String(p.row.progress?.submitted ?? "—")),
},
{
title: "未读未交",
width: 100,
align: "center",
render: (h: any, p: any) => {
const g = p.row.progress;
if (!g) return h("span", { style: "color:#999;" }, "—");
return h("span", { style: g.unreadUndone > 0 ? "color:#ed4014;font-weight:600;" : "color:#999;" }, String(g.unreadUndone));
},
},
{
title: "提交率",
width: 90,
align: "center",
render: (h: any, p: any) => {
const g = p.row.progress;
if (!g) return h("span", { style: "color:#999;" }, "—");
return h("span", {}, `${g.submitRate}%`);
},
},
{
title: "待批阅",
width: 95,
align: "center",
render: (h: any, p: any) => {
const g = p.row.progress;
if (!g) return h("span", { style: "color:#999;" }, "—");
return h("span", { style: g.pendingGrade > 0 ? "color:#f90;font-weight:600;" : "color:#999;" }, String(g.pendingGrade));
},
},
{
title: "状态",
width: 120,
align: "center",
render: (h: any, p: any) => statusCell(h, p.row.hwStatus, p.row.hwStatusDesc),
},
{
title: "操作",
slot: "action",
/* 3 个图标按钮:32px + tooltip 左右各 2px = 36px → 108px,再留单元格左右 padding
各 18px(36px) 与少量余量 → 170 恰好一行;给 150 时最后一个会被挤到第二行 */
width: 170,
align: "center",
fixed: "right",
},
]);
/* 更多菜单分发 */
function onPubMore(name: string, row: HomeworkV2Vo) {
if (name === "notRead") remind(row, "NOT_READ");
else if (name === "notSubmitted") remind(row, "NOT_SUBMITTED");
else if (name === "extend") openExtend(row);
else if (name === "copy") copyRow(row);
else if (name === "archive") archive(row);
else if (name === "export") doExport(row);
}
/* ============ 草稿箱列 ============ */
const draftColumns = computed(() => [
{ title: "序号", type: "index", width: 76, align: "center" },
{ title: "标题", key: "title", minWidth: 300, align: "left", tooltip: true },
{ title: "学科", key: "subjectName", width: 120, align: "center" },
{
title: "班级",
width: 190,
align: "left",
render: (h: any, p: any) => ellipsisCell(h, (p.row.classes || []).map((c: any) => c.className).join("、") || "—"),
},
{ title: "更新时间", width: 170, align: "left", render: (h: any, p: any) => nowrapCell(h, fmtTime(p.row.updateTime)) },
{ title: "截止时间", width: 170, align: "left", render: (h: any, p: any) => nowrapCell(h, fmtTime(p.row.deadlineAt)) },
{
title: "状态",
/* 「定时 2026-09-18 08:00」+ 状态图标 + 胶囊内边距实测 185px,单元格左右 padding 各 18px
→ 列宽给 230 才有余量(220 实测只剩 0~1px,改字号/字体就会截掉一截) */
width: 230,
align: "center",
render: (h: any, p: any) => {
const row = p.row as HomeworkV2Vo;
const scheduled = row.hwStatus === "DRAFT_SCHEDULED";
return statusCell(h, row.hwStatus, scheduled ? `定时 ${fmtTime(row.scheduledAt)}` : undefined);
},
},
{
title: "操作",
slot: "draftAction",
/* 最多 4 个图标按钮(编辑/发布/取消定时/删除):4 × 36 + padding 36 + 余量 = 200 */
width: 200,
align: "center",
fixed: "right",
},
]);
const columns = computed(() => (viewMode.value === "drafts" ? draftColumns.value : pubColumns.value));
/* ============ 表格高度:量真实 DOM,不用常量估算 ============ */
/** 表格外层容器(量它的顶边位置) */
const tableWrap = ref<HTMLElement | null>(null);
const tableHeight = ref(400);
/**
* 表格高度 = 分页条顶边 − 表格顶边 − 12px 呼吸位(列宽超出容器时再扣掉横向滚动条约 15px)。
*
* 原先用 `Utils.useTableHeight({isSearch,isOperat,isPage})` 按 body 高度减固定值估算:
* 它既不认本页把状态 Tab 嵌在搜索行里的实际高度,也不认容器内边距,算出的值与真实可用
* 空间对不齐;而 `.page-panel` 是 absolute 吸底定位,表格一旦比可用空间高就压到分页上,
* 横向滚动条也没给它留位置(最后一行会被滚动条切掉)。
* 这里直接量 DOM —— 加减筛选条件、切 Tab、改字号都不会再失配;窗口 resize 时重算。
*/
function calcTableHeight() {
const wrap = tableWrap.value;
if (!wrap) return;
const rect = wrap.getBoundingClientRect();
const panel = wrap.parentElement?.querySelector(".page-panel") as HTMLElement | null;
const limit = panel ? panel.getBoundingClientRect().top : window.innerHeight;
// 列宽合计超过容器宽度 → 会出现横向滚动条,它会吃掉表体 ~15px 高度,一并预留
const colsWidth = (columns.value as any[]).reduce((s, c) => s + (c.width || c.minWidth || 100), 0);
const hBar = colsWidth > wrap.clientWidth ? 15 : 0;
tableHeight.value = Math.max(240, Math.floor(limit - rect.top - 12 - hBar));
}
onMounted(() => {
nextTick(calcTableHeight);
window.addEventListener("resize", calcTableHeight);
});
onBeforeUnmount(() => window.removeEventListener("resize", calcTableHeight));
/* 已发布 / 草稿箱两套列宽不同 → 横向滚动条的有无会变,切 Tab 后重算 */
watch(columns, () => nextTick(calcTableHeight));
</script>
<template>
<div class="page-wrapper">
<div class="main-container">
<!-- 1. 操作 -->
<div class="fix mb-12">
<Button class="fl" type="primary" size="large" @click="router.push('/list/new')">布置作业</Button>
</div>
<!-- 2. 状态筛选(单行 Tab) + 搜索区 -->
<div class="search-wrap fix">
<div class="fl">
<Tabs :value="tabValue" @on-click="onTab" size="small" class="inner-tabs">
<TabPane v-for="t in PUB_TABS" :key="t.name" :name="t.name" :label="t.label"></TabPane>
</Tabs>
</div>
<Select v-model="subject" class="search-wrap_sel ml-12 fl" clearable placeholder="学科" style="width: 120px" @on-change="doSearch">
<Option v-for="s in subjectOpts" :key="s.subId" :value="s.subId">{{ s.subName }}</Option>
</Select>
<Input
class="fr"
style="width: 200px"
search
suffix="ios-search"
placeholder="输入关键字搜索"
v-model="keyword"
@on-search="doSearch"
@on-clear="doSearch"
/>
</div>
<!-- 3. 表格 + 分页(tableWrap 供 calcTableHeight 量真实可用高度) -->
<div ref="tableWrap">
<Table
:columns="columns"
:data="page.content"
class="link-table table-bottom-box"
:height="tableHeight"
:loading="loading"
no-data-text="暂无数据"
>
<!-- 已发布行操作:图标按钮 + 悬浮说明
(提交情况=icon-chakan、批阅=icon-shenpi,与「全部作业」「提交情况」页同一套语义;
其余低频操作仍收在「更多」里,不丢功能) -->
<template #action="{ row }">
<Tooltip content="提交情况" placement="bottom" transfer>
<div class="operating-btn iconfont icon-chakan" @click="viewSubmissions(row)"></div>
</Tooltip>
<Tooltip v-if="needGrade(row)" content="批阅" placement="bottom" transfer>
<div class="operating-btn iconfont icon-shenpi" @click="toGrade(row)"></div>
</Tooltip>
<Dropdown transfer @on-click="(name: string) => onPubMore(name, row)">
<!-- 触发器不再包 Tooltip:Dropdown 默认 hover 展开,悬停时两个气泡会打架,
这里用原生 title 兜底说明 -->
<div class="operating-btn operating-btn-more" title="更多"><Icon type="ios-more" size="20" /></div>
<template #list>
<DropdownMenu>
<DropdownItem v-if="row.hwStatus === 'PUBLISHED' && (row.progress?.unread ?? 0) > 0" name="notRead">提醒未读</DropdownItem>
<DropdownItem v-if="row.hwStatus === 'PUBLISHED' && (row.progress?.unsubmitted ?? 0) > 0" name="notSubmitted">催交</DropdownItem>
<DropdownItem v-if="row.hwStatus === 'PUBLISHED'" name="extend">延长截止</DropdownItem>
<DropdownItem name="copy">复制</DropdownItem>
<DropdownItem v-if="row.hwStatus === 'PUBLISHED' || row.hwStatus === 'CLOSED'" name="archive">归档</DropdownItem>
<DropdownItem name="export">导出报表</DropdownItem>
</DropdownMenu>
</template>
</Dropdown>
</template>
<!-- 草稿箱行操作:同一套图标口径(编辑/发布/取消定时/删除) -->
<template #draftAction="{ row }">
<Tooltip content="编辑" placement="bottom" transfer>
<div class="operating-btn iconfont icon-edit" @click="edit(row)"></div>
</Tooltip>
<Tooltip content="发布" placement="bottom" transfer>
<div class="operating-btn iconfont icon-fasong" @click="publishDraft(row)"></div>
</Tooltip>
<Tooltip v-if="row.hwStatus === 'DRAFT_SCHEDULED'" content="取消定时" placement="bottom" transfer>
<div class="operating-btn iconfont icon-stop" @click="cancelSchedule(row)"></div>
</Tooltip>
<Tooltip content="删除" placement="bottom" transfer>
<div class="operating-btn iconfont icon-ashbin" @click="del(row)"></div>
</Tooltip>
</template>
</Table>
</div>
<div class="page-panel">
<div class="fl ml-15 align-middle"></div>
<Page
class="page-box fr"
:total="page.total"
:current="pageNum"
:page-size="pageSize"
:page-size-opts="[10, 20, 30, 40]"
@on-page-size-change="pageSizeUpdate"
@on-change="pageUpdate"
placement="top"
show-total
show-sizer
></Page>
</div>
</div>
<!-- 延长截止 -->
<Modal v-model="extendModal.open" title="延长截止时间" width="420px">
<Form :label-width="100">
<FormItem label="新截止时间:">
<DatePicker
type="datetime"
transfer
v-model="extendModal.time"
format="yyyy-MM-dd HH:mm"
placeholder="选择新的截止时间"
style="width: 240px"
></DatePicker>
<div class="extend-tip" v-if="extendModal.row?.deadlineAt">
原截止时间:{{ extendModal.row.deadlineAt }}
</div>
</FormItem>
</Form>
<template #footer>
<Button type="primary" :loading="extendModal.saving" @click="doExtend">保存</Button>
<Button @click="extendModal.open = false" style="margin-left: 8px">取消</Button>
</template>
</Modal>
</div>
</template>
<style scoped lang="scss">
.search-wrap {
.inner-tabs {
margin-bottom: -8px;
:deep(.ivu-tabs-bar) {
margin-bottom: 0;
}
/* ink bar 由组件 JS 按前序 tab 宽度累计定位,自定义 margin 间距会使其偏移,
* 故隐藏之,改用激活项伪元素画下划线 */
:deep(.ivu-tabs-ink-bar) {
display: none;
}
:deep(.ivu-tabs-tab) {
position: relative;
margin-right: 28px;
padding: 10px 2px;
transition: color 0.2s ease;
}
/* 激活下划线:加粗 + 圆角,随 tab 盒子对齐 */
:deep(.ivu-tabs-tab-active)::after,
:deep(.ivu-tabs-tab-active:hover)::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 3px;
border-radius: 3px 3px 0 0;
background: #2d8cf0;
}
/* 标签文字 + 计数徽标 */
:deep(.hw-tab) {
display: inline-flex;
align-items: center;
.hw-tab-text {
font-size: 14px;
}
.hw-tab-count {
min-width: 16px;
margin-left: 6px;
padding: 0 6px;
font-size: 12px;
line-height: 16px;
text-align: center;
border-radius: 8px;
background: #f2f3f5;
color: #808695;
transition: all 0.2s ease;
}
}
:deep(.ivu-tabs-tab:hover .hw-tab-count) {
background: #e8f3ff;
color: #2d8cf0;
}
:deep(.ivu-tabs-tab-active .hw-tab-count),
:deep(.ivu-tabs-tab-active:hover .hw-tab-count) {
background: #2d8cf0;
color: #fff;
}
/* 末位(草稿箱)与状态组之间用竖线隔开 */
:deep(.ivu-tabs-tab:last-child) {
margin-right: 0;
margin-left: 26px;
&::before {
content: "";
position: absolute;
left: -14px;
top: 50%;
transform: translateY(-50%);
width: 1px;
height: 16px;
background: #dcdee2;
}
}
}
}
.extend-tip {
color: #808695;
font-size: 12px;
margin-top: 4px;
}
/* 操作列图标按钮组:Tooltip / Dropdown 两种容器都是 inline-block,
默认 baseline 对齐会让「更多」比另两个偏低 2~3px,统一按中线对齐 */
:deep(.ivu-table-cell) {
.ivu-tooltip,
.ivu-dropdown {
vertical-align: middle;
}
}
</style>