feat: 提交课程作业前端工程源码(Vue3 + Vite + view-ui-plus)

This commit is contained in:
lijing
2026-09-21 12:14:00 +08:00
parent 0749e0b9c5
commit 66cdf7c5a1
358 changed files with 40943 additions and 2 deletions
@@ -0,0 +1,299 @@
import { Method } from "alova";
import { alovaInstance } from "@/utils/alova/alova-iview";
import conf from "@/assets/config/host.config";
import type {
HomeworkVo,
HomeworkDetailVo,
HomeworkPageResult,
BasePageResult,
UpFileResultVo,
UpFileVo,
InitializeRedVo,
NextPrevVo,
ReadGroup,
HomeworkStatisticsVo,
HomeworkRequest,
HomeworkQuery,
DictOption,
DeptManageItem,
ContactGroupPage,
ContactUserPage,
SchoolTreeNode,
ClassTreeNode,
SubmissionsResultVo,
SubmissionDetailVo,
GradeRequest,
MyHomeworksVo,
MySubmissionVo,
SubmitRequest,
} from "@/types/homework";
/**
* 课程作业接口封装(P0 标准化后新契约,P1-7 TS 契约化)。
*
* 前缀说明:conf.serviceHost 本地为空(走 vite 代理),线上为基础平台域名;
* platform 固定使用 "api",与后端 Config.API_PATH 中的 api 匹配。
* 统一响应由 alova-iview 解包为 data(code==0),页面直接使用返回值。
*
* 变更要点(P0):
* 1. 作业域去掉 Homework_list 二级段(/api/homework/Homework_list/List → /api/homework/List)
* 2. 文件域从 base/ 迁移到 file/(/api/base/FileDown → /api/file/Down)
* 3. 字典域从 base/ 迁移到 dict/,GET 写操作改 POST(/api/base/DelDept → /api/dict/Delete)
* 4. postJSON 用于 @RequestBody 接口;postForm 用于 @RequestParam 接口(表单提交)
* 5. 参数更名:chaoSend→fileIds、topmost→smsFlag
* 6. Delete 入参从 ids 逗号串改为 List<Integer> JSON 数组
*
* 变更要点(P1-7):
* 7. 全部方法加返回值泛型标注,替换 Record<string, any>
* 8. 列表分页 content 字段对齐后端 BasePageResult(非旧 list)
*/
const HOMEWORK = "/api/homework";
const BASE = "/api/base";
const DICT = "/api/dict";
const FILE = "/api/file";
const CONTACTS = "/api/contacts";
/** GET 请求,T 为返回数据类型 */
function get<T = any>(url: string, params?: Record<string, any>): Method<T> {
return new Method<T>("GET", alovaInstance, url, { params });
}
/** POST JSON body(与后端 @RequestBody 对齐),T 为返回数据类型 */
function postJSON<T = any>(url: string, data?: Record<string, any> | any[]): Method<T> {
return new Method<T>(
"POST",
alovaInstance,
url,
{ headers: { "Content-Type": "application/json;charset=UTF-8" } },
data ? JSON.stringify(data) : undefined
);
}
/** POST 表单(与后端 @RequestParam 对齐,application/x-www-form-urlencoded),T 为返回数据类型 */
function postForm<T = any>(url: string, data?: Record<string, any>): Method<T> {
const pairs = data
? Object.entries(data).filter(([, v]) => v !== undefined && v !== null)
: [];
const form = pairs.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
// 同时把参数拼到 URL query:后端 @RequestParam 表单/查询参数都能接收,
// 且本地 vite-plugin-mock 不解析 urlencoded body,靠 query 才能拿到参数(不影响线上)。
const fullUrl = form ? `${url}${url.includes("?") ? "&" : "?"}${form}` : url;
return new Method<T>(
"POST",
alovaInstance,
fullUrl,
{ headers: { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" } },
form || undefined
);
}
/** 附件下载直链(浏览器 location.href 跳转) */
function fileDownUrl(fileId: number): string {
return `${conf.serviceHost}${FILE}/Down?id=${fileId}`;
}
/** 作业全部附件打包下载直链 */
function allFileDownUrl(dataid: number): string {
return `${conf.serviceHost}${HOMEWORK}/AllFileDown?dataid=${dataid}`;
}
class BaseApi {
// ========== 用户信息 & 模块权限 ==========
/** 获取当前登录用户信息 */
getUserInfo() {
return get<SchoolTreeNode>(`${BASE}/GetUserInfo`);
}
/** 获取当前用户已授权的模块列表 */
getUserMod() {
return get<SchoolTreeNode[]>(`${BASE}/GetUserMod`);
}
/** 当前学校组织树(部门→教师,用于选择用户) */
getSchoolAll() {
return get<SchoolTreeNode[]>(`${BASE}/GetSchoolAll`);
}
/** 年级→班级树(学生场景:选择通知对象到班级一级) */
getClassTree() {
return get<ClassTreeNode[]>(`${BASE}/GetClassTree`);
}
/** 端分离入口:切换当前视角(teacher / student / parent),需在拉取用户信息前调用 */
setRole(as: "teacher" | "student" | "parent") {
return postJSON<{ role: string }>(`${BASE}/SetRole`, { as });
}
}
class HomeworkApi {
// ========== 收件箱 ==========
/** 已收作业列表 */
list(params: HomeworkQuery) {
return get<HomeworkPageResult<HomeworkVo>>(`${HOMEWORK}/List`, params);
}
/** 清单:一键全部已读 */
initializeRed() {
return postForm<InitializeRedVo>(`${HOMEWORK}/InitializeRed`);
}
// ========== 已发/全部作业 ==========
/** 已发/全部作业列表(scope="user" 只看自己,不传=全部) */
sendList(params: HomeworkQuery) {
return get<BasePageResult<HomeworkVo>>(`${HOMEWORK}/SendList`, params);
}
// ========== 详情 ==========
/** 查看详情(type=1 标读;type=0 编辑不标读) */
modify(serid: number, type = 1) {
return get<HomeworkDetailVo>(`${HOMEWORK}/Detail`, { serid, type });
}
/** 编辑回显(不标读) */
getHomeworkDetails(serid: number) {
return get<HomeworkDetailVo>(`${HOMEWORK}/GetDetail`, { serid });
}
/** 上一篇/下一篇 */
getNextPrevHomework(serid: number, type: string) {
return get<NextPrevVo>(`${HOMEWORK}/GetNextPrev`, { serid, type });
}
/** 阅读情况(已读/未读分组) */
orgizeModify(serid: number) {
return get<ReadGroup[]>(`${HOMEWORK}/ReadDetail`, { serid });
}
/** 追加接收人 */
receiveUsers(serid: number, users: string) {
return postForm<void>(`${HOMEWORK}/ReceiveUsers`, { serid, users });
}
// ========== 发布/编辑/删除 ==========
/** 新建作业(JSON body,fileIds 为附件ID逗号串,smsFlag 为短信标记) */
create(data: HomeworkRequest) {
return postJSON<number>(`${HOMEWORK}/Create`, data);
}
/** 编辑作业(JSON body,含 id) */
editHomework(data: HomeworkRequest) {
return postJSON<void>(`${HOMEWORK}/Edit`, data);
}
/** 删除作业(ids 数组,支持批量) */
deleteHomework(ids: number | string) {
// 兼容页面传单个 id 或逗号串,统一转成数组
const idArr = String(ids).split(",").map(Number).filter(Boolean);
return postJSON<void>(`${HOMEWORK}/Delete`, idArr);
}
/** 撤回 */
userPull(serid: number) {
return postForm<void>(`${HOMEWORK}/UserPull`, { serid });
}
/** 置顶/取消置顶 */
setTopmost(serid: number, topmost = 1) {
return postForm<void>(`${HOMEWORK}/SetTopmost`, { serid, topmost });
}
/** 催收未读 */
expedite(serid: number) {
return postForm<void>(`${HOMEWORK}/Expedite`, { serid });
}
// ========== 课程作业 v1:提交情况 / 批改 / 催交 ==========
/** 某作业的提交名单(按班级分组)+ 汇总;submitStatus: all/unsubmitted/submitted/pending/graded/needRedo */
submissions(serid: number, submitStatus = "all") {
return get<SubmissionsResultVo>(`${HOMEWORK}/Submissions`, { serid, submitStatus });
}
/** 单份提交详情(批改工作台右侧) */
submissionDetail(subId: number) {
return get<SubmissionDetailVo>(`${HOMEWORK}/SubmissionDetail`, { subId });
}
/** 批改(JSON body):action=grade/redo/excellent */
grade(data: GradeRequest) {
return postJSON<void>(`${HOMEWORK}/Grade`, data);
}
/** 催交未交学生 */
remind(serid: number) {
return postForm<{ reminded: number }>(`${HOMEWORK}/Remind`, { serid });
}
// ========== v2 学生端 ==========
/** 今日/我的作业(当前学生视角,含提交与批改态 + 分桶计数) */
myHomeworks() {
return get<MyHomeworksVo>(`${HOMEWORK}/MyHomeworks`);
}
/** 我对某作业的详情(作业信息 + 我的提交/批改) */
mySubmission(serid: number) {
return get<MySubmissionVo>(`${HOMEWORK}/MySubmission`, { serid });
}
/** 提交 / 订正重交(JSON body:serid, text, images, attachments) */
submit(data: SubmitRequest) {
return postJSON<{ subId: number; submitStatus: string }>(`${HOMEWORK}/Submit`, data);
}
// ========== 附件 ==========
fileDownUrl = fileDownUrl;
allFileDownUrl = allFileDownUrl;
// ========== 部门字典 ==========
/** 发文部门下拉 */
getSysDic() {
return get<DictOption[]>(`${DICT}/List`, { type: 1 });
}
/** 是否开通短信(迁移到 dict/Sync 查询,保持旧调用名不变) */
getSchModuleIsSms() {
return get<boolean>(`${DICT}/Sync`);
}
/** 作业统计 */
statistics(params?: { starttime?: string; endtime?: string }) {
return get<HomeworkStatisticsVo>(`${HOMEWORK}/Statis`, params);
}
}
class SettingApi {
// ========== 发文部门管理 ==========
/** 获取发文部门管理列表 */
getDictMangeList() {
return get<DeptManageItem[]>(`${DICT}/ManageList`, { type: "dept" });
}
/** 新增发文部门 */
addDeptDic(name: string) {
return postForm<void>(`${DICT}/Add`, { name });
}
/** 更新发文部门(名称、启用状态) */
updateDeptDic(SerID: number, Name: string, Enable = 1) {
return postForm<void>(`${DICT}/Update`, { SerID, Name, Enable });
}
/** 删除发文部门(原 GET DelDept,已 POST 化) */
delDept(serid: number) {
return postForm<void>(`${DICT}/Delete`, { serid });
}
/** 发文部门排序(原 GET OrdbyDicIds,已 POST 化) */
sortDeptDic(ids: string) {
return postForm<void>(`${DICT}/Sort`, { ids });
}
// ========== 自定义组管理 ==========
/** 获取联系组列表 */
getGroup(page: number, count: number) {
return get<ContactGroupPage>(`${CONTACTS}/GetGroup`, { page, count });
}
/** 新增联系组 */
addGroup(name: string) {
return postForm<void>(`${CONTACTS}/AddGroup`, { name });
}
/** 编辑联系组 */
editGroup(id: number, name: string) {
return postForm<void>(`${CONTACTS}/EditGroup`, { name, id });
}
/** 删除联系组 */
delGroup(ids: string) {
return postForm<void>(`${CONTACTS}/DelGroup`, { ids });
}
/** 获取联系组用户 */
getContactsTea(gid: number, page = 0, count = 0) {
return get<ContactUserPage>(`${CONTACTS}/GetContactsTea`, { gid, page, count });
}
/** 设置联系组用户 */
setContactsTea(gid: number, ids: string) {
return postForm<void>(`${CONTACTS}/SetContactsTea`, { gid, ids });
}
/** 联系组用户树(选择用户-自定义组 tab) */
getGroupTree() {
return get<SchoolTreeNode[]>(`${CONTACTS}/GetGroupTree`, { type: 1 });
}
}
export const baseApi = new BaseApi();
export const homeworkApi = new HomeworkApi();
export const settingApi = new SettingApi();
export default homeworkApi;
@@ -0,0 +1,18 @@
import type { IModule } from "@/store";
/**
* 课程作业左侧菜单数据(对照后端 GetUserMod 现行模块)。
* 当前由 layout 直接使用;接入后端 GetUserMod 后,以接口返回为准。
* 注:课程作业不做审批,故不含「审批列表/审核设置」;平台模块配置若仍下发 modId 5/6,
* 由 utils/menuRoute 的 DROPPED_MOD_IDS 统一剔除(见 classwork-frontend-m5-plan.md §6)。
*/
export function getMenuList(): Promise<IModule[]> {
const menu: IModule[] = [
{ id: 1, modId: 1, name: "已收到", page: "/" },
{ id: 2, modId: 2, name: "已发送", page: "/list" },
{ id: 3, modId: 3, name: "全部作业", page: "/homeworklist" },
{ id: 4, modId: 4, name: "作业统计", page: "/stats" },
{ id: 21, modId: 21, name: "应用设置", page: "/setting" },
];
return Promise.resolve(menu);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,169 @@
<template>
<div class="type_set-box">
<div class="type_name">
<i class="iconfont icon-stop fl mr-10" v-if="!isEdit && !active" style="color: #ff5959"></i>
<Input v-if="isEdit" v-model="type.name" ref="type_input" @on-blur="update" @on-enter="update" />
<span v-else class="fl">{{ type.name }}</span>
</div>
<div class="type_action flex">
<div class="action_item" @click="open">
<span v-if="active">停用</span>
<span v-else>启用</span>
</div>
<div class="action_edit" @click="edit">
<span>编辑</span>
</div>
<div class="action_delete" @click="remove">
<span>删除</span>
</div>
</div>
<div class="divider"></div>
<div class="divider2"></div>
</div>
</template>
<script>
export default {
name: "TypeSetBox",
props: {
type: {
name: { type: String, default: "" },
id: { type: String, default: "" },
},
active: { type: Boolean, default: true },
},
data() {
return {
isEdit: false,
eventType: "",
};
},
watch: {
type: {
deep: true,
handler(newVal) {
if (this.isEdit) {
this.isEdit = false;
}
},
},
"type.name"(newVal) {
if (this.isEdit) {
this.isEdit = false;
}
},
},
methods: {
open() {
this.$emit("active", this.type.id);
},
edit() {
this.isEdit = !this.isEdit;
if (this.isEdit) this.$nextTick(() => this.$refs["type_input"].focus());
},
update(e) {
if (!this.type.name) {
this.$Message.error("值不得为空!");
return;
}
if (this.eventType == "") {
this.eventType = e.type;
this.isEdit = false;
this.$emit("edit", this.type.id, this.type.name);
setTimeout(() => {
this.eventType = "";
}, 500);
}
},
remove() {
this.$emit("remove", this.type.id);
},
},
};
</script>
<style lang="scss" scoped>
.flex {
display: flex;
}
.type_set-box {
width: 222px;
background: #ffffff;
margin-right: 15px;
margin-bottom: 15px;
.type_name {
height: 66px;
border: 1px solid #e9e9e9;
border-bottom: none;
padding-left: 15px;
padding-right: 15px;
line-height: 66px;
display: flex;
justify-content: center;
align-items: center;
span {
height: 24px;
font-size: 15px;
font-family: PingFangSC-Semibold, PingFang SC;
font-weight: 500;
color: #272829;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 222px;
display: inline-block;
}
.iconfont {
font-size: 26px;
}
}
.type_action {
flex-wrap: wrap;
justify-content: center;
align-items: center;
width: 222px;
height: 30px;
line-height: 30px;
background: #f7f7f7;
border-radius: 0px 0px 2px 2px;
border-top: none;
border: 1px solid #e9e9e9;
span {
font-size: 12px;
}
div {
cursor: pointer;
flex: 1;
text-align: center;
}
div:hover {
color: #6c91f0;
}
}
.divider {
// position: relative;
height: 14px;
width: 1px;
background: #e8e8e8;
// left: 73px;
// top: -23px;
float: left;
margin-left: 71px;
margin-top: -22px;
}
.divider2 {
// position: relative;
// left: 146px;
// top: -37px;
height: 14px;
width: 1px;
background: #e8e8e8;
float: left;
margin-left: 147px;
margin-top: -22px;
}
}
</style>
@@ -0,0 +1,165 @@
<template>
<!-- 作业对象选择:全部用户 / 指定用户(自旧前端 user_check_input.vue 适配) -->
<div class="check-user-wrap">
<div class="check-user-box">
<RadioGroup v-model="isCheckUserAll" class="radio" @on-change="userAllCheck">
<Radio :disabled="editDisabled" :label="0">{{ isEdit ? "现有" : "全部" }}用户</Radio>
<Radio :label="1">指定用户</Radio>
</RadioGroup>
<Input
type="text"
v-model="userNameStr"
readonly
:disabled="!isCheckUserAll"
placeholder="选择指定的用户"
class="input"
></Input>
<Button icon="md-add" @click="checkUserShow" :disabled="!isCheckUserAll"></Button>
<Button icon="md-trash" @click="resetUser" :disabled="!isCheckUserAll"></Button>
</div>
<UserCheckModal
ref="userCheckModal"
:sources="sources"
:userids="tempUserIds"
@on-submit="submitUser"
/>
</div>
</template>
<script lang="ts">
/*
* 作业对象选择输入(自旧前端 oldfront/src/components/user_check_input.vue 适配 Vue3)。
* v-model 值为 "all" 或用户 id 逗号串;切换到「指定用户」直接弹出选择用户弹窗。
*/
import UserCheckModal from "@/components/UserCheckModal.vue";
import { baseApi, settingApi } from "@/api/module/homework";
export default {
name: "UserCheckInput",
components: { UserCheckModal },
props: {
modelValue: {
type: String,
default: "all",
},
/** 编辑态:全部用户选项禁用为「现有用户」 */
isEdit: {
type: Boolean,
default: false,
},
},
emits: ["update:modelValue"],
data() {
return {
isCheckUserAll: 0 as number,
tempUserIds: [] as any[],
userNameStr: "",
editDisabled: false,
sources: [
{
name: "基础平台",
loader: () => baseApi.getSchoolAll().send() as Promise<any[]>,
},
{
name: "自定义组",
loader: () => settingApi.getGroupTree().send() as Promise<any[]>,
},
],
};
},
created() {
if (this.modelValue === "all") {
this.isCheckUserAll = 0;
this.userNameStr = this.isEdit ? "现有用户" : "全部用户";
} else {
this.isCheckUserAll = 1;
this.tempUserIds = this.modelValue.split(",").map((x) => parseInt(x)).filter((x) => !isNaN(x));
}
},
methods: {
checkUserShow() {
(this.$refs.userCheckModal as any).show();
},
resetUser() {
//清除用户
this.tempUserIds = [];
this.userNameStr = "";
this.$emit("update:modelValue", "");
},
userAllCheck(type: number) {
let tempIds: any = String(this.modelValue || "")
.split(",")
.map((x) => parseInt(x))
.filter((x) => !isNaN(x));
if (type == 0) {
if (this.isEdit) {
this.tempUserIds = tempIds;
this.userNameStr = "现有用户";
this.$emit("update:modelValue", String(tempIds));
} else {
this.tempUserIds = [];
this.userNameStr = "全部用户";
this.$emit("update:modelValue", "all");
}
} else {
if (this.isEdit) {
this.editDisabled = true;
}
this.tempUserIds = tempIds;
this.userNameStr = "";
this.checkUserShow();
}
},
submitUser(arr: any[]) {
//确认用户
this.userNameStr = arr.map((x) => x.name).join("、");
const ids = arr.map((x) => x.id).join(",");
this.$emit("update:modelValue", ids || "all");
if (!arr.length) {
// 清空后回到全部用户
this.isCheckUserAll = 0;
this.userNameStr = "全部用户";
this.$emit("update:modelValue", "all");
}
},
},
watch: {
modelValue(val: string) {
if (val === "all") {
this.isCheckUserAll = 0;
this.userNameStr = this.isEdit ? "现有用户" : "全部用户";
} else if (val === "") {
this.userNameStr = "";
} else {
// 编辑回显:指定用户 id 串
this.isCheckUserAll = 1;
this.tempUserIds = val
.split(",")
.map((x) => parseInt(x))
.filter((x) => !isNaN(x));
}
},
},
};
</script>
<style lang="scss" scoped>
.check-user-wrap {
width: 100%;
.check-user-box {
display: flex;
align-items: center;
gap: 8px;
.radio {
width: 180px;
flex: none;
font-size: 12px;
vertical-align: middle;
line-height: 32px;
}
.input {
flex: 1;
}
}
}
</style>
@@ -0,0 +1,124 @@
import type { RouteRecordRaw } from "vue-router";
/**
* 课程作业模块路由(扁平短路由,与基础平台模块 path 一致)。
* 页面间参数通过 query 传递:
* - /list/new?serid= (编辑)
* - /list/new?fid= (转发)
* - /list/new?tempid=(模板引用)
* - /list/detail?serid=&type=、/homeworklist/detail?serid=&type=
* type: 0=编辑视角(不标读) 1=普通查看(标读)
*
* modId 是菜单 ↔ 路由的稳定契约(对照后端 GetUserMod 现行数据):
* 1=已收到 2=已发送 3=全部作业 4=作业统计 21=应用设置
* 发作业为「已发送」的深层子页(modId 22 不在菜单树,meta.parentId=2 关联高亮),
* 与 UserBase「应用管理>应用类型管理」范式一致。
*/
const routes: RouteRecordRaw[] = [
// v1 教师默认落地:我布置的
{ path: "/", redirect: "/list" },
// 学生收件箱(v2 保留,不在菜单)
{
path: "/accept",
name: "homework_accept",
component: () => import("@/views/homework/HomeworkAccept.vue"),
meta: { title: "已收到", modId: 1 },
},
// 我布置的(教师端作业列表)
{
path: "/list",
name: "homework_list",
component: () => import("@/views/homework/HomeworkList.vue"),
meta: { title: "我布置的", modId: 2 },
},
// 提交情况(某作业的已交/未交名单;从列表行或菜单进入,无 serid 时页内选择作业)
{
path: "/submissions",
name: "homework_submissions",
component: () => import("@/views/homework/Submissions.vue"),
meta: { title: "提交情况", modId: 25 },
},
// 批改中心(待批作业→逐份批改工作台;无 serid 时先看待批作业列表)
{
path: "/grade",
name: "homework_grade",
component: () => import("@/views/homework/Grade.vue"),
meta: { title: "批改中心", modId: 26 },
},
// 发作业(新建/编辑/转发/模板):已发送的深层子页,不在菜单里,
// layout.vue 按.meta.parentId 推导面包屑「已发送>发作业」并高亮父菜单
{
path: "/list/new",
name: "new_homework",
component: () => import("@/views/homework/NewHomework.vue"),
meta: { title: "发作业", modId: 22, parentId: 2 },
},
// 全部作业(管理端)
{
path: "/homeworklist",
name: "homework_manage",
component: () => import("@/views/homework/HomeworkList.vue"),
meta: { title: "全部作业", modId: 3 },
},
// 作业详情(同一组件挂两个路由,面包屑/菜单高亮由 meta.parentId 确定父节点:
// 已发送下 /list/detail,全部作业下 /homeworklist/detail;type 区分是否标读)
{
path: "/list/detail",
name: "homework_list_detail",
component: () => import("@/views/homework/HomeworkDetails.vue"),
meta: { title: "作业详情", modId: 23, parentId: 2 },
},
{
path: "/homeworklist/detail",
name: "homework_manage_detail",
component: () => import("@/views/homework/HomeworkDetails.vue"),
meta: { title: "作业详情", modId: 24, parentId: 3 },
},
// 作业统计(旧版行为:整页跳转外部统计系统)
{
path: "/stats",
name: "homework_stats",
component: () => import("@/views/homework/HomeworkStats.vue"),
meta: { title: "作业统计", modId: 4 },
},
// 应用设置(发文部门 + 自定义组)
{
path: "/setting",
name: "homework_setting",
component: () => import("@/views/homework/HomeworkSetting.vue"),
meta: { title: "应用设置", modId: 21 },
},
/* ---- v2 学生端 ---- */
// 学生·今日作业
{
path: "/stu/today",
name: "student_today",
component: () => import("@/views/homework/StudentHomeworks.vue"),
meta: { title: "今日作业", modId: 31, role: "student" },
},
// 学生·待订正(复用同一页,默认过滤待订正)
{
path: "/stu/redo",
name: "student_redo",
component: () => import("@/views/homework/StudentHomeworks.vue"),
meta: { title: "待订正", modId: 32, role: "student", initTab: "redo" },
},
/* ---- 旧版路径兼容重定向(书签、残留的平台模块配置) ---- */
{ path: "/details", redirect: "/list/detail" },
{ path: "/newhomework", redirect: "/list/new" },
{ path: "/edit", redirect: "/list/new" },
{ path: "/manage", redirect: "/homeworklist" },
{ path: "/homework", redirect: "/accept" },
{ path: "/homework/newhomework", redirect: "/list/new" },
{ path: "/homework/edit", redirect: "/list/new" },
{ path: "/homework/homework_accept", redirect: "/accept" },
{ path: "/homework/homework_list", redirect: "/list" },
{ path: "/homework/manage", redirect: "/homeworklist" },
{ path: "/homework/homework_details", redirect: "/list/detail" },
{ path: "/homework/statistics", redirect: "/stats" },
{ path: "/homework/setting", redirect: "/setting" },
];
export default routes;
@@ -0,0 +1,492 @@
/**
* 课程作业前端类型契约(P1-7)
*
* 与后端 VO/DTO 一一对齐:
* - HomeworkVo → 后端 model/homework/HomeworkVo.java(Jackson 默认驼峰序列化)
* - HomeworkDetailVo → 后端 model/homework/HomeworkDetailVo.java(@JsonProperty 保留旧字段名)
* - BasePageResult → 后端 model/BasePageResult.java
* - HomeworkPageResult extends BasePageResult → 后端 model/homework/HomeworkPageResult.java
* - UpFileResultVo → 后端 model/homework/UpFileResultVo.java(@JsonProperty)
* - UpFileVo → 后端 model/homework/UpFileVo.java
* - InitializeRedVo → 后端 model/homework/InitializeRedVo.java(@JsonProperty)
* - HomeworkStatisticsVo → 后端 model/homework/HomeworkStatisticsVo.java
* - HomeworkRequest → 后端 model/homework/HomeworkRequest.java
* - HomeworkQuery → 后端 model/homework/HomeworkQuery.java + BasePageQuery.java
*/
// ============================ 分页外壳 ============================
/** 统一分页响应外壳(对齐 BasePageResult.java) */
export interface BasePageResult<T> {
total: number;
content: T[];
pageIndex: number;
pageSize: number;
totalPages: number;
hasNext: boolean;
}
/** 作业分页结果(对齐 HomeworkPageResult.java,扩展 unreadCount) */
export interface HomeworkPageResult<T> extends BasePageResult<T> {
/** 当前用户未读作业总数 */
unreadCount: number;
}
// ============================ 作业列表项 ============================
/** 作业视图对象(对齐 HomeworkVo.java,Jackson 默认驼峰序列化) */
export interface HomeworkVo {
id: number;
title: string;
content: string;
type: number;
typeDesc: string;
publisherAccount: string;
publisherName: string;
deptId: string;
deptName: string;
status: number;
statusDesc: string;
topmost: number;
/** 已读人数 */
readCount: number;
/** 应读人数 */
allowCount: number;
/** 当前用户是否已读 */
read: boolean;
/** 是否撤回:0 否 / 1 是 */
isPulled: number;
/** 定时发送时间 */
timingSendTime: string | null;
/** 父作业ID(转发时关联原作业) */
parentId: number | null;
/** 短信发送标记 */
smsFlag: number;
/** 接收对象类型:class=班级(学生场景) */
receiveType?: string;
/** 接收列表:"all" 或 班级 id 逗号串 */
receiveList: string | null;
/** ===== 课程作业 v1(教师核心闭环)===== */
/** 学科 */
subject?: string;
/** 截止时间 */
deadline?: string | null;
/** 提交方式:text/image/attach/none */
submitType?: string[];
/** 作业流程状态:draft 草稿 / collecting 收集中 / closed 已截止 / done 已完成 */
hwStatus?: HomeworkStatus;
hwStatusDesc?: string;
/** 提交/批改进度(草稿或仅查阅为 null) */
progress?: HomeworkProgress | null;
createBy: string | null;
createTime: string | null;
updateBy: string | null;
updateTime: string | null;
}
// ============================ 作业详情 ============================
/** 作业详情视图对象(对齐 HomeworkDetailVo.java,@JsonProperty 保留旧字段名) */
export interface HomeworkDetailVo {
serid: number;
title: string;
/** 作业内容(旧字段名 Contents) */
Contents: string;
/** 发布部门ID(旧字段名 DeptID) */
DeptID: string;
/** 发布部门名称(旧字段名 DeptName) */
DeptName: string;
/** 发布人姓名(旧字段名 TeaName) */
TeaName: string;
/** 发布时间(旧字段名 EditDate,已格式化为字符串) */
EditDate: string;
/** 应读人数 */
numcount: number;
/** 已读人数 */
readnumcount: number;
/** 接收人列表 */
ReceiveList: string | null;
/** 定时发送时间(旧字段名 DsfsTime,已格式化) */
DsfsTime: string | null;
/** 是否定时发送 */
isDsfs: boolean;
/** ===== 课程作业 v1 ===== */
subject?: string;
deadline?: string | null;
submitType?: string[];
hwStatus?: HomeworkStatus;
hwStatusDesc?: string;
progress?: HomeworkProgress | null;
/** 附件列表(旧字段名 ChaoSend) */
ChaoSend: HomeworkDetailFileItem[];
/** 接收对象列表:"all" 或 班级 id 数组 */
tlist: string | string[];
/** 接收对象类型:class=班级 */
receiveType?: string;
/** 接收对象可读文本(全部班级 / 班级名串) */
receiveText?: string;
}
/** 作业详情附件项(对齐 HomeworkDetailVo.FileItem) */
export interface HomeworkDetailFileItem {
id: number;
name: string;
size: number;
fileid: number;
filename: string;
}
// ============================ 文件上传 ============================
/** 文件上传结果(对齐 UpFileResultVo.java,@JsonProperty) */
export interface UpFileResultVo {
fileid: number;
name: string;
fileurl: string;
host: string;
type: string;
size: number;
}
/** 附件视图对象(对齐 UpFileVo.java) */
export interface UpFileVo {
id: number;
fileTrueName: string;
fileName: string;
fileSize: number;
fileType: string;
topicId: number;
topicType: string;
}
// ============================ 一键已读 ============================
/** 一键已读结果(对齐 InitializeRedVo.java,@JsonProperty) */
export interface InitializeRedVo {
count: number;
}
// ============================ 上一篇/下一篇 ============================
/** 上一篇/下一篇作业ID */
export interface NextPrevVo {
prevId: number;
nextId: number;
}
// ============================ 阅读情况 ============================
/** 阅读情况分组项(后端 Map 返回,字段名固定) */
export interface ReadGroup {
GroupName: string;
teaslist: ReadGroupUser[];
}
/** 阅读情况用户项(后端 Map 返回) */
export interface ReadGroupUser {
account: string;
teaname: string;
states: 'read' | 'unread';
}
// ============================ 统计 ============================
/** 作业统计视图(对齐 HomeworkStatisticsVo.java) */
export interface HomeworkStatisticsVo {
total: number;
publishedCount: number;
totalReadRecords: number;
unreadForCurrentUser: number;
byPublisher: { publisherName: string; count: number }[];
byMonth: { year: number; month: number; count: number }[];
// ===== 课程作业口径(v1,可选) =====
/** 我布置的作业数 */
myAssignCount?: number;
/** 待批改总量 */
pendingGrade?: number;
/** 平均提交率(%) */
avgSubmitRate?: number;
/** 按作业进度 */
byHomework?: { id: number; title: string; subject: string; hwStatus: HomeworkStatus } & HomeworkProgress[];
/** 分数分布 */
scoreDist?: { label: string; key: string; count: number }[];
}
// ============================ 课程作业 v1:提交 / 批改 ============================
/** 作业流程状态 */
export type HomeworkStatus = "draft" | "collecting" | "closed" | "done";
/** 单个提交状态 */
export type SubmitStatus = "unsubmitted" | "submitted" | "graded" | "needRedo" | "redone";
/** 提交/批改进度 */
export interface HomeworkProgress {
/** 应交人数 */
should: number;
/** 已交人数 */
submitted: number;
/** 已批改人数 */
graded: number;
/** 待批改人数 */
pendingGrade: number;
/** 待订正人数 */
needRedo: number;
/** 已订正人数 */
redone: number;
/** 未交人数 */
unsubmitted: number;
/** 提交率(%) */
submitRate: number;
/** 批改率(%) */
gradeRate: number;
}
/** 提交名单项(Submissions 接口返回,精简) */
export interface SubmissionBrief {
subId: number;
studentId: number;
studentName: string;
submitStatus: SubmitStatus;
submittedAt: string | null;
hasGrade: boolean;
score: number | null;
level: string | null;
isExcellent: boolean;
}
/** 提交名单按班级分组 */
export interface SubmissionGroup {
className: string;
students: SubmissionBrief[];
}
/** 某作业提交情况结果 */
export interface SubmissionsResultVo {
homeworkId: number;
title: string;
subject: string;
deadline: string | null;
hwStatus: HomeworkStatus;
hwStatusDesc: string;
summary: HomeworkProgress;
groups: SubmissionGroup[];
}
/** 批改结果 */
export interface GradeResult {
score: number | null;
level: string | null;
comment: string;
isExcellent: boolean;
markedImages: string[];
}
/** 单份提交详情(批改工作台用) */
export interface SubmissionDetailVo {
subId: number;
homeworkId: number;
homeworkTitle: string;
subject: string;
studentId: number;
studentName: string;
classId: string;
className: string;
submitStatus: SubmitStatus;
submittedAt: string | null;
text: string;
images: string[];
attachments: string[];
grade: GradeResult | null;
}
/** 批改入参 */
export interface GradeRequest {
subId: number;
/** grade 保存批改 / redo 要求订正 / excellent 标为优秀 */
action?: "grade" | "redo" | "excellent";
score?: number | string;
level?: string;
comment?: string;
isExcellent?: boolean;
markedImages?: string[];
}
// ============================ 请求入参 ============================
/** 作业发布/编辑入参(对齐 HomeworkRequest.java) */
export interface HomeworkRequest {
title: string;
content?: string;
/** 学科 */
subject?: string;
/** 截止时间 yyyy-MM-dd HH:mm:ss */
deadline?: string;
/** 提交方式:text/image/attach/none */
submitType?: string[] | string;
type?: number;
deptId?: string;
/** 是否允许逾期补交 */
allowLate?: boolean;
/** 截止提醒:如 ["1d","1h"] */
remindBefore?: string[];
/** 存草稿:true 则不发布 */
draft?: boolean;
deptName?: string;
topmost?: number;
/** 接收对象:"all" 或 班级 id 逗号串(receiveType=class) */
receiveList?: string;
/** 接收对象类型:class=班级 */
receiveType?: string;
/** 附件文件ID列表(逗号分隔串) */
fileIds?: string;
timingSendTime?: string;
/** 短信发送标记 */
smsFlag?: number;
/** 转发源作业ID */
sourceId?: number;
}
/** 作业列表查询参数(对齐 HomeworkQuery.java + BasePageQuery.java) */
export interface HomeworkQuery {
pageIndex?: number;
pageSize?: number;
keyword?: string;
status?: number;
type?: number;
/** 列表范围(管理端):"user" 只看自己发布的,不传=全部 */
scope?: string;
deptId?: string;
/** 学科筛选 */
subject?: string;
/** 作业流程状态筛选:draft/collecting/closed/done,缺省或 all=全部 */
hwStatus?: string;
publisherAccount?: string;
/** 开始时间(yyyy-MM-dd 短日期) */
startTime?: string;
/** 结束时间(yyyy-MM-dd 短日期) */
endTime?: string;
/** 阅读状态过滤(仅用户端):0 全部 / 1 未读 / 2 已读 */
readType?: number;
/** 排序:1 时间倒序 / 2 时间正序 */
orderByTime?: string;
}
// ============================ 字典/联系组 ============================
/** 系统字典下拉项 */
export interface DictOption {
k: number | string;
v: string | null;
[key: string]: any;
}
/** 发文部门管理列表项 */
export interface DeptManageItem {
SerID: number;
CodeID: string;
Name: string;
Enable: number;
}
/** 联系组列表项 */
export interface ContactGroupItem {
serid: number;
GroupName: string;
}
/** 联系组分页结果 */
export interface ContactGroupPage {
list: ContactGroupItem[];
total: number;
}
/** 联系组用户项 */
export interface ContactUser {
account: string;
name: string;
userId?: number;
id?: number;
}
/** 联系组用户分页结果 */
export interface ContactUserPage {
list: ContactUser[];
total: number;
}
/** 组织树节点(基础平台返回) */
export interface SchoolTreeNode {
[key: string]: any;
}
// ============================ 班级 ============================
/** 年级→班级树节点(叶子=班级,id 为数字班级 id,附学生数) */
export interface ClassTreeNode {
id: number;
name: string;
isParent: boolean;
studentCount?: number;
children?: ClassTreeNode[];
[key: string]: any;
}
// ============================ v2 学生端 ============================
/** 当前视角角色(家长即登录学生账号,不单列) */
export type ViewerRole = "teacher" | "student";
/** 学生对某作业的提交/批改态 */
export interface StudentSubmissionInfo {
subId: number;
submitStatus: SubmitStatus;
submittedAt: string | null;
text: string;
score: number | null;
level: string | null;
comment: string;
isExcellent: boolean;
images?: string[];
attachments?: string[];
grade?: GradeResult | null;
}
/** 学生视角作业项 */
export interface StudentHomeworkVo {
id: number;
title: string;
subject: string;
publisherName: string;
deptName: string;
deadline: string | null;
createTime: string | null;
content: string;
submitType: string[];
viewOnly: boolean;
hwStatus?: HomeworkStatus;
allowLate?: boolean;
my: StudentSubmissionInfo | null;
}
/** 今日/我的作业列表 */
export interface MyHomeworksVo {
buckets: { todo: number; submitted: number; graded: number; redo: number; all: number };
list: StudentHomeworkVo[];
}
/** 我的某作业详情(作业信息 + 我的提交/批改) */
export interface MySubmissionVo {
homework: { id: number; title: string; subject: string; content: string; deadline: string | null; submitType: string[]; publisherName: string; deptName: string };
my: StudentSubmissionInfo | null;
}
/** 学生提交作业入参 */
export interface SubmitRequest {
serid: number;
text?: string;
/** 图片 URL / dataURL 列表(逗号串或数组) */
images?: string[] | string;
/** 附件 id 列表(逗号串或数组) */
attachments?: string[] | string;
}
@@ -0,0 +1,482 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, nextTick } from "vue";
import { Message } from "view-ui-plus";
import draggable from "vuedraggable";
import MyConfirm from "@/components/MyConfirm";
import UserCheckModal from "@/components/UserCheckModal.vue";
import TypeSetBox from "@/components/TypeSetBox.vue";
import { settingApi, baseApi } from "@/api/module/homework";
import type { DeptManageItem, ContactGroupPage, ContactUserPage, SchoolTreeNode } from "@/types/homework";
import Utils from "@/utils";
/* ============ Tab 切换(规范 §1:tabs-wrapper + Tabs type=card,切换只 v-show 内容区) ============ */
const currentType = ref(1);
function tabChange(name: string) {
currentType.value = Number(name);
if (currentType.value === 1) {
getDeptList();
} else if (currentType.value === 3) {
getGroupList();
}
}
/* ============ 发文部门(直接复用 UserBase 同源 TypeSetBox 卡片组件) ============ */
interface DeptItem {
SerID: number;
CodeID: string;
Name: string;
Enable: number;
}
const deptData = ref<DeptItem[]>([]);
const deptModal = ref(false);
const deptForm = reactive({ name: "" });
const deptFormRef = ref();
const deptRule = {
name: [{ required: true, message: "请输入发文部门名称", trigger: "blur" }],
};
async function getDeptList() {
try {
const data: DeptManageItem[] = await settingApi.getDictMangeList().send();
deptData.value = (data || []).map((d) => ({
SerID: d.SerID,
CodeID: d.CodeID,
Name: d.Name,
Enable: d.Enable ?? 1,
}));
} catch (e: any) {
Message.error(e.message || "获取发文部门列表失败");
}
}
function addDeptShow() {
deptForm.name = "";
deptModal.value = true;
}
async function deptModalSave() {
const valid = await deptFormRef.value?.validate();
if (!valid) {
Message.info("请填写红色星号内容!");
return;
}
try {
await settingApi.addDeptDic(deptForm.name.trim()).send();
Message.success("操作成功!");
deptModal.value = false;
await getDeptList();
} catch (e: any) {
Message.error(e.message || "操作失败");
}
}
function findDept(id: string): DeptItem | undefined {
return deptData.value.find((x) => String(x.SerID) === id);
}
/** TypeSetBox 启用/停用(保留确认弹窗) */
function onDeptToggle(id: string) {
const item = findDept(id);
if (item) toggleDeptStatus(item);
}
/** TypeSetBox 内联编辑部门名(组件已拦截空值) */
async function onDeptRename(id: string, name: string) {
const item = findDept(id);
if (!item) return;
try {
await settingApi.updateDeptDic(item.SerID, name.trim(), item.Enable).send();
Message.success("操作成功!");
await getDeptList();
} catch (e: any) {
Message.error(e.message || "操作失败");
}
}
function onDeptRemove(id: string) {
const item = findDept(id);
if (item) deleteDept(item);
}
async function deleteDept(item: DeptItem) {
await MyConfirm({
title: "删除",
msg: `确定删除“ ${item.Name} ”这个发文部门吗?`,
info: "删除后不可恢复",
async onOk() {
try {
await settingApi.delDept(item.SerID).send();
Message.success("操作成功!");
await getDeptList();
} catch (e: any) {
Message.error(e.message || "删除失败");
}
},
});
}
async function toggleDeptStatus(item: DeptItem) {
const enable = item.Enable === 1;
await MyConfirm({
msg: `确定${enable ? "停用" : "启用"}发文部门「${item.Name}」吗?`,
title: "修改状态",
async onOk() {
try {
await settingApi.updateDeptDic(item.SerID, item.Name, enable ? 0 : 1).send();
item.Enable = enable ? 0 : 1;
Message.success("操作成功!");
} catch (e: any) {
Message.error(e.message || "操作失败");
}
},
});
}
/* 拖动排序:提交当前顺序的 SerID 数组 */
async function deptSortSave() {
const ids = deptData.value.map((x: DeptItem) => x.SerID);
try {
await settingApi.sortDeptDic(ids.join(",")).send();
Message.success("排序成功");
} catch (e: any) {
Message.error(e.message || "排序失败");
}
}
/* ============ 自定义组(规范 §5 联系组管理页形态) ============ */
interface GroupItem {
serid: number;
GroupName: string;
count: number;
}
const groupData = ref<GroupItem[]>([]);
const groupTotal = ref(0);
const groupPage = ref(1);
const groupCount = ref(parseInt(localStorage.getItem("customPageCount") || "10"));
async function getGroupList() {
try {
const data: any = await settingApi.getGroup(groupPage.value, groupCount.value).send();
groupData.value = (data.list || []).map((d: any) => ({
serid: d.serid,
GroupName: d.GroupName,
count: d.count ?? 0,
}));
groupTotal.value = data.total || 0;
} catch (e: any) {
Message.error(e.message || "获取自定义组列表失败");
}
}
const groupModal = ref(false);
const groupEditingId = ref<number | null>(null);
const groupForm = reactive({ name: "" });
const groupFormRef = ref();
const groupRule = {
name: [{ required: true, message: "请输入联系组名称", trigger: "blur" }],
};
function addGroupShow() {
groupEditingId.value = null;
groupForm.name = "";
groupModal.value = true;
}
function editGroupShow(row: GroupItem) {
groupEditingId.value = row.serid;
groupForm.name = row.GroupName;
groupModal.value = true;
}
async function groupModalSave() {
const valid = await groupFormRef.value?.validate();
if (!valid) {
Message.info("请填写红色星号内容!");
return;
}
try {
if (groupEditingId.value == null) {
await settingApi.addGroup(groupForm.name.trim()).send();
} else {
await settingApi.editGroup(groupEditingId.value, groupForm.name.trim()).send();
}
Message.success("操作成功!");
groupModal.value = false;
await getGroupList();
} catch (e: any) {
Message.error(e.message || "操作失败");
}
}
async function deleteGroup(row: GroupItem) {
await MyConfirm({
title: "删除",
msg: `确定删除“ ${row.GroupName} ”这个组吗?`,
info: "删除后不可恢复",
async onOk() {
try {
await settingApi.delGroup(String(row.serid)).send();
Message.success("操作成功!");
// 删除后智能翻页:当前页仅剩一条则回退一页
if (groupData.value.length <= 1 && groupPage.value > 1) groupPage.value -= 1;
await getGroupList();
} catch (e: any) {
Message.error(e.message || "删除失败");
}
},
});
}
/* ---------- 查看联系组用户 ---------- */
const seeUserModal = ref(false);
const seeUserGid = ref(0);
const seeUserGroupName = ref("");
const seeUserData = ref<{ account: string; name: string }[]>([]);
const seeUserTotal = ref(0);
const seeUserPage = ref(1);
const seeUserColumns = [
{ title: "序号", type: "index", width: 70 },
{ title: "账号", key: "account", align: "center" },
{ title: "真实姓名", key: "name", width: 160, align: "center" },
];
async function seeUserShow(row: GroupItem) {
seeUserGid.value = row.serid;
seeUserGroupName.value = row.GroupName;
seeUserPage.value = 1;
await loadSeeUser();
seeUserModal.value = true;
}
async function loadSeeUser() {
try {
const data: ContactUserPage = await settingApi.getContactsTea(seeUserGid.value, seeUserPage.value, 5).send();
seeUserData.value = (data.list || []).map((d) => ({ account: d.account, name: d.name }));
seeUserTotal.value = data.total || 0;
} catch (e: any) {
Message.error(e.message || "获取用户列表失败");
}
}
/* ---------- 设置联系组用户(规范选人组件,打开前回显已有成员) ---------- */
const setUserCheckRef = ref();
const setUserGid = ref(0);
const setUserIds = ref<number[]>([]);
// 设置自定义组成员只需要基础平台组织树,无需「基础平台/自定义组」两个 tab
const setUserSources = [
{
name: "基础平台",
loader: () => baseApi.getSchoolAll().send(),
},
];
async function settingUserShow(row: GroupItem) {
setUserGid.value = row.serid;
setUserIds.value = [];
try {
const data: ContactUserPage = await settingApi.getContactsTea(row.serid, 0, 0).send();
setUserIds.value = (data.list || [])
.map((d) => Number(d.userId ?? d.id))
.filter((n: number) => Number.isFinite(n) && n > 0);
} catch (e: any) {
Message.error(e.message || "获取已有用户失败");
}
await nextTick();
setUserCheckRef.value.show();
}
async function settingUserSubmit(users: SchoolTreeNode[]) {
if (!users.length) {
Message.info("请选择用户!");
return;
}
// on-submit 返回的是用户对象数组({id,name,group}),取 id 拼接
const ids = users.map((x) => (typeof x === "object" && x != null ? x.id : x)).join(",");
try {
await settingApi.setContactsTea(setUserGid.value, ids).send();
Message.success("设置成功");
// 刷新组列表,组员数即时更新
await getGroupList();
} catch (e: any) {
Message.error(e.message || "设置失败");
}
}
onMounted(() => {
getDeptList();
});
/* ---------- 自定义组表格(规范 §5:序号/组名/操作图标列 width 250) ---------- */
const groupColumns = [
{ title: "序号", type: "index", width: 80, align: "center" },
{ title: "组名", key: "GroupName", align: "left" },
{ title: "组员数", key: "count", width: 100, align: "center" },
{
title: "操作",
width: 250,
align: "center",
render: (h: any, params: any) => {
const row: GroupItem = params.row;
return [
Utils.operatingHandle(h, "查看用户", "icon-browse", () => seeUserShow(row)),
Utils.operatingHandle(h, "编辑", "icon-edit", () => editGroupShow(row)),
Utils.operatingHandle(h, "删除", "icon-ashbin", () => deleteGroup(row)),
Utils.operatingHandle(h, "设置用户", "icon-user-filling", () => settingUserShow(row)),
];
},
},
];
const groupTableHeight = computed(() => Utils.useTableHeight({ isTabs: true, isSearch: true, isPage: true }));
</script>
<template>
<div class="tabs-wrapper">
<Tabs class="tabs-panel" type="card" :value="String(currentType)" @on-click="tabChange">
<TabPane label="发文部门" name="1"></TabPane>
<TabPane label="自定义组" name="3"></TabPane>
</Tabs>
<!-- 发文部门 -->
<div class="main-container" v-show="currentType === 1">
<Alert show-icon>设置说明:拖动卡片调整部门排序;停用的部门不出现在「作业发送」的部门下拉中</Alert>
<div class="search-wrap fix">
<Button class="fl" type="primary" @click="addDeptShow">新增部门</Button>
</div>
<draggable
class="type_set-wrap fix"
tag="ul"
v-model="deptData"
group="homeworkDept"
item-key="SerID"
@change="deptSortSave"
>
<template #item="{ element }">
<li>
<TypeSetBox
:type="{ id: String(element.SerID), name: element.Name }"
:active="element.Enable === 1"
@active="onDeptToggle"
@edit="onDeptRename"
@remove="onDeptRemove"
/>
</li>
</template>
</draggable>
</div>
<!-- 自定义组 -->
<div class="main-container" v-show="currentType === 3">
<div class="search-wrap fix">
<Button class="fl" type="primary" @click="addGroupShow">新建联系组</Button>
</div>
<Table
:columns="groupColumns"
:data="groupData"
class="link-table table-bottom-box"
:height="groupTableHeight"
no-data-text="暂无数据"
></Table>
<div class="page-panel">
<div class="fl ml-15 align-middle"></div>
<Page
class="page-box fr"
:total="groupTotal"
:current="groupPage"
:page-size="groupCount"
:page-size-opts="[10, 20, 30, 40]"
@on-page-size-change="
(size: number) => {
groupPage = 1;
groupCount = size;
localStorage.setItem('customPageCount', String(size));
getGroupList();
}
"
@on-change="
(p: number) => {
groupPage = p;
getGroupList();
}
"
placement="top"
show-total
show-sizer
></Page>
</div>
</div>
<!-- 新增发文部门 Modal(编辑走 TypeSetBox 内联编辑) -->
<Modal v-model="deptModal" :mask-closable="false" width="450px" title="新增部门">
<Form ref="deptFormRef" :model="deptForm" :rules="deptRule" :label-width="110">
<FormItem label="发文部门名称:" prop="name">
<Input placeholder="请输入发文部门名称" v-model="deptForm.name" />
</FormItem>
</Form>
<template #footer>
<Button type="primary" @click="deptModalSave">提交</Button>
<Button @click="deptModal = false">取消</Button>
</template>
</Modal>
<!-- 新建/编辑联系组 Modal(规范 §5:450px + label-width 100 + 提交/取消) -->
<Modal v-model="groupModal" :mask-closable="false" width="450px" :title="groupEditingId == null ? '新建联系组' : '编辑联系组'">
<Form ref="groupFormRef" :model="groupForm" :rules="groupRule" :label-width="100">
<FormItem label="联系组名称:" prop="name">
<Input placeholder="请输入联系组名称" v-model="groupForm.name" />
</FormItem>
</Form>
<template #footer>
<Button type="primary" @click="groupModalSave">提交</Button>
<Button @click="groupModal = false">取消</Button>
</template>
</Modal>
<!-- 查看联系组用户 Modal(规范 §5:Modal 内小表格 + small 分页) -->
<Modal v-model="seeUserModal" :title="`查看联系组用户${seeUserGroupName ? ' - ' + seeUserGroupName : ''}`">
<Table height="260" :columns="seeUserColumns" :data="seeUserData" no-data-text="暂无用户"></Table>
<div class="small-pager">
<Page
:current="seeUserPage"
:page-size="5"
:total="seeUserTotal"
@on-change="
(p: number) => {
seeUserPage = p;
loadSeeUser();
}
"
size="small"
show-total
/>
</div>
<template #footer>
<Button @click="seeUserModal = false">关闭</Button>
</template>
</Modal>
<!-- 选择用户弹窗(设置联系组用户) -->
<UserCheckModal ref="setUserCheckRef" :sources="setUserSources" :userids="setUserIds" @on-submit="settingUserSubmit" />
</div>
</template>
<style lang="scss" scoped>
.tab-body {
min-height: 300px;
}
.type_set-wrap {
margin: 12px 0 16px;
display: flex;
flex-wrap: wrap;
}
/* 拖动排序手型(与 UserBase AppCategory 页面级样式一致,组件内不含) */
.type_set-wrap :deep(.type_name) {
cursor: move;
}
.small-pager {
margin-top: 8px;
display: flex;
justify-content: flex-end;
}
</style>