教师档案增加“激活账号”:管理员设置初始密码,服务端校验管理权限、学院数据范围、在职状态、工号冲突,并自动关联教师角色与档案。
已发布课表增加“发布后修改”:创建修订草稿,旧课表继续生效;修订版重新发布后再替换旧版本。 修复 390px 窄屏下课表页头按钮溢出。
This commit is contained in:
@@ -5,6 +5,7 @@ using Jiaowu.Api.Domain.Identity;
|
|||||||
using Jiaowu.Api.Infrastructure.Auth;
|
using Jiaowu.Api.Infrastructure.Auth;
|
||||||
using Jiaowu.Api.Infrastructure.Persistence;
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
@@ -15,7 +16,8 @@ namespace Jiaowu.Api.Controllers;
|
|||||||
[Route("api/personnel")]
|
[Route("api/personnel")]
|
||||||
public sealed class PersonnelController(
|
public sealed class PersonnelController(
|
||||||
AppDbContext db,
|
AppDbContext db,
|
||||||
ICurrentUserDataScope currentUserDataScope) : ControllerBase
|
ICurrentUserDataScope currentUserDataScope,
|
||||||
|
UserManager<ApplicationUser> userManager) : ControllerBase
|
||||||
{
|
{
|
||||||
private const string ReadRoles =
|
private const string ReadRoles =
|
||||||
SystemRoles.SuperAdmin + "," +
|
SystemRoles.SuperAdmin + "," +
|
||||||
@@ -149,6 +151,61 @@ public sealed class PersonnelController(
|
|||||||
return await SaveNoContentAsync(cancellationToken);
|
return await SaveNoContentAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("teachers/{id:guid}/activate-account")]
|
||||||
|
[Authorize(Roles = WriteRoles)]
|
||||||
|
public async Task<ActionResult> ActivateTeacherAccount(
|
||||||
|
Guid id,
|
||||||
|
TeacherAccountActivationRequest request,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var teacher = await db.Teachers.FindAsync([id], cancellationToken);
|
||||||
|
if (teacher is null) return NotFound();
|
||||||
|
if (!CanAccessCollege(teacher.CollegeId)) return Forbid();
|
||||||
|
if (teacher.Status != TeacherStatus.Active)
|
||||||
|
{
|
||||||
|
return ConflictProblem("仅在职教师可以激活登录账号。");
|
||||||
|
}
|
||||||
|
if (teacher.UserId.HasValue)
|
||||||
|
{
|
||||||
|
return ConflictProblem("该教师档案已经关联登录账号。");
|
||||||
|
}
|
||||||
|
|
||||||
|
var userName = teacher.TeacherNumber.Trim();
|
||||||
|
if (await userManager.FindByNameAsync(userName) is not null)
|
||||||
|
{
|
||||||
|
return ConflictProblem("该工号已有登录账号但未正确关联,请到账号管理中核对。");
|
||||||
|
}
|
||||||
|
|
||||||
|
await using var transaction = await db.Database.BeginTransactionAsync(cancellationToken);
|
||||||
|
var user = new ApplicationUser
|
||||||
|
{
|
||||||
|
UserName = userName,
|
||||||
|
DisplayName = teacher.Name,
|
||||||
|
StaffNumber = userName,
|
||||||
|
CollegeId = teacher.CollegeId,
|
||||||
|
IsEnabled = true,
|
||||||
|
LockoutEnabled = true
|
||||||
|
};
|
||||||
|
var result = await userManager.CreateAsync(user, request.Password);
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(cancellationToken);
|
||||||
|
return IdentityValidationProblem(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = await userManager.AddToRoleAsync(user, SystemRoles.Teacher);
|
||||||
|
if (!result.Succeeded)
|
||||||
|
{
|
||||||
|
await transaction.RollbackAsync(cancellationToken);
|
||||||
|
return IdentityValidationProblem(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
teacher.UserId = user.Id;
|
||||||
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
return Ok(new { user.Id, UserName = userName });
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("students")]
|
[HttpGet("students")]
|
||||||
public async Task<ActionResult<PagedResult<object>>> GetStudents(
|
public async Task<ActionResult<PagedResult<object>>> GetStudents(
|
||||||
[FromQuery] PersonnelQuery query,
|
[FromQuery] PersonnelQuery query,
|
||||||
@@ -393,6 +450,13 @@ public sealed class PersonnelController(
|
|||||||
Status = StatusCodes.Status409Conflict
|
Status = StatusCodes.Status409Conflict
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private ActionResult IdentityValidationProblem(IdentityResult result)
|
||||||
|
{
|
||||||
|
foreach (var error in result.Errors)
|
||||||
|
ModelState.AddModelError(error.Code, error.Description);
|
||||||
|
return ValidationProblem(ModelState);
|
||||||
|
}
|
||||||
|
|
||||||
private static string? Normalize(string? value) =>
|
private static string? Normalize(string? value) =>
|
||||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
|
||||||
@@ -436,3 +500,6 @@ public sealed record StudentRequest(
|
|||||||
[MaxLength(30)] string? Phone,
|
[MaxLength(30)] string? Phone,
|
||||||
[EmailAddress, MaxLength(100)] string? Email,
|
[EmailAddress, MaxLength(100)] string? Email,
|
||||||
[MaxLength(500)] string? Notes);
|
[MaxLength(500)] string? Notes);
|
||||||
|
|
||||||
|
public sealed record TeacherAccountActivationRequest(
|
||||||
|
[Required, MinLength(8), MaxLength(100)] string Password);
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using Jiaowu.Api.Controllers;
|
||||||
|
using Jiaowu.Api.Domain.Academic;
|
||||||
|
using Jiaowu.Api.Domain.Identity;
|
||||||
|
using Jiaowu.Api.Infrastructure.Auth;
|
||||||
|
using Jiaowu.Api.Infrastructure.Persistence;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Jiaowu.Api.Tests;
|
||||||
|
|
||||||
|
public sealed class PersonnelControllerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ActivateTeacherAccount_CreatesAndLinksTeacherLogin()
|
||||||
|
{
|
||||||
|
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||||
|
await connection.OpenAsync();
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddLogging();
|
||||||
|
services.AddDbContext<AppDbContext>(options => options.UseSqlite(connection));
|
||||||
|
services
|
||||||
|
.AddIdentityCore<ApplicationUser>(options =>
|
||||||
|
{
|
||||||
|
options.Password.RequiredLength = 8;
|
||||||
|
options.Password.RequireDigit = true;
|
||||||
|
options.Password.RequireLowercase = true;
|
||||||
|
options.Password.RequireUppercase = true;
|
||||||
|
options.Password.RequireNonAlphanumeric = true;
|
||||||
|
})
|
||||||
|
.AddRoles<ApplicationRole>()
|
||||||
|
.AddEntityFrameworkStores<AppDbContext>();
|
||||||
|
|
||||||
|
await using var provider = services.BuildServiceProvider();
|
||||||
|
await using var scope = provider.CreateAsyncScope();
|
||||||
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
await db.Database.EnsureCreatedAsync();
|
||||||
|
var roleManager = scope.ServiceProvider
|
||||||
|
.GetRequiredService<RoleManager<ApplicationRole>>();
|
||||||
|
Assert.True((await roleManager.CreateAsync(new ApplicationRole
|
||||||
|
{
|
||||||
|
Name = SystemRoles.Teacher,
|
||||||
|
Description = "教师"
|
||||||
|
})).Succeeded);
|
||||||
|
|
||||||
|
var college = new College { Code = "CS", Name = "计算机学院" };
|
||||||
|
var teacher = new Teacher
|
||||||
|
{
|
||||||
|
TeacherNumber = "T2026999",
|
||||||
|
Name = "测试教师",
|
||||||
|
CollegeId = college.Id,
|
||||||
|
Status = TeacherStatus.Active
|
||||||
|
};
|
||||||
|
db.AddRange(college, teacher);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var userManager = scope.ServiceProvider
|
||||||
|
.GetRequiredService<UserManager<ApplicationUser>>();
|
||||||
|
var controller = new PersonnelController(
|
||||||
|
db,
|
||||||
|
new TestDataScope(college.Id),
|
||||||
|
userManager);
|
||||||
|
|
||||||
|
var result = await controller.ActivateTeacherAccount(
|
||||||
|
teacher.Id,
|
||||||
|
new TeacherAccountActivationRequest("Teacher@123"),
|
||||||
|
CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.IsType<OkObjectResult>(result);
|
||||||
|
var user = await userManager.FindByNameAsync(teacher.TeacherNumber);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
Assert.Equal(teacher.Name, user.DisplayName);
|
||||||
|
Assert.Equal(teacher.CollegeId, user.CollegeId);
|
||||||
|
Assert.True(await userManager.IsInRoleAsync(user, SystemRoles.Teacher));
|
||||||
|
await db.Entry(teacher).ReloadAsync();
|
||||||
|
Assert.Equal(user.Id, teacher.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestDataScope(Guid collegeId) : ICurrentUserDataScope
|
||||||
|
{
|
||||||
|
public CurrentUserScope Current { get; } = new(
|
||||||
|
Guid.NewGuid(),
|
||||||
|
"测试管理员",
|
||||||
|
collegeId,
|
||||||
|
DataScope.College,
|
||||||
|
new HashSet<string>([SystemRoles.CollegeAdmin]));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1070,6 +1070,8 @@ button { cursor: pointer; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
|
.page-intro { align-items: flex-start; flex-direction: column; }
|
||||||
|
.page-actions { width: 100%; flex-wrap: wrap; }
|
||||||
.option-filter-grid.course,
|
.option-filter-grid.course,
|
||||||
.option-filter-grid.course.compact,
|
.option-filter-grid.course.compact,
|
||||||
.option-filter-grid.classes { grid-template-columns: 1fr; }
|
.option-filter-grid.classes { grid-template-columns: 1fr; }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { Document, Download, Plus, Refresh, Search, Upload } from '@element-plus/icons-vue'
|
import { Document, Download, Key, Plus, Refresh, Search, Upload } from '@element-plus/icons-vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import http, { apiErrorMessage } from '../api/http'
|
import http, { apiErrorMessage } from '../api/http'
|
||||||
import { downloadApiFile, importExcel } from '../api/excel'
|
import { downloadApiFile, importExcel } from '../api/excel'
|
||||||
@@ -25,6 +25,9 @@ const fileInput = ref<HTMLInputElement>()
|
|||||||
const rows = ref<any[]>([])
|
const rows = ref<any[]>([])
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
|
const activationDialog = ref(false)
|
||||||
|
const activationLoading = ref(false)
|
||||||
|
const activatingTeacher = ref<any | null>(null)
|
||||||
const editingId = ref('')
|
const editingId = ref('')
|
||||||
const colleges = ref<any[]>([])
|
const colleges = ref<any[]>([])
|
||||||
const majors = ref<any[]>([])
|
const majors = ref<any[]>([])
|
||||||
@@ -40,6 +43,10 @@ const query = reactive({
|
|||||||
status: undefined as string | undefined,
|
status: undefined as string | undefined,
|
||||||
})
|
})
|
||||||
const form = reactive<Record<string, any>>({})
|
const form = reactive<Record<string, any>>({})
|
||||||
|
const activationForm = reactive({
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
})
|
||||||
|
|
||||||
const canManage = computed(() =>
|
const canManage = computed(() =>
|
||||||
auth.user?.roles.some((role) =>
|
auth.user?.roles.some((role) =>
|
||||||
@@ -293,6 +300,47 @@ async function remove(row: any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openActivation(row: any) {
|
||||||
|
activatingTeacher.value = row
|
||||||
|
activationForm.password = ''
|
||||||
|
activationForm.confirmPassword = ''
|
||||||
|
activationDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function activateTeacherAccount() {
|
||||||
|
if (!activationForm.password) {
|
||||||
|
ElMessage.warning('请设置教师的初始密码。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (activationForm.password.length < 8 ||
|
||||||
|
!/[a-z]/.test(activationForm.password) ||
|
||||||
|
!/[A-Z]/.test(activationForm.password) ||
|
||||||
|
!/\d/.test(activationForm.password) ||
|
||||||
|
!/[^A-Za-z0-9]/.test(activationForm.password)) {
|
||||||
|
ElMessage.warning('密码至少 8 位,并包含大写字母、小写字母、数字和特殊字符。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (activationForm.password !== activationForm.confirmPassword) {
|
||||||
|
ElMessage.warning('两次输入的密码不一致。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
activationLoading.value = true
|
||||||
|
try {
|
||||||
|
const { data } = await http.post(
|
||||||
|
`/personnel/teachers/${activatingTeacher.value.id}/activate-account`,
|
||||||
|
{ password: activationForm.password },
|
||||||
|
)
|
||||||
|
activationDialog.value = false
|
||||||
|
ElMessage.success(`教师账号 ${data.userName} 已激活`)
|
||||||
|
await load()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(apiErrorMessage(error))
|
||||||
|
} finally {
|
||||||
|
activationLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([loadReferences(), load()])
|
await Promise.all([loadReferences(), load()])
|
||||||
})
|
})
|
||||||
@@ -417,8 +465,23 @@ watch(active, async () => {
|
|||||||
<el-table-column label="联系方式" min-width="170">
|
<el-table-column label="联系方式" min-width="170">
|
||||||
<template #default="{ row }">{{ row.phone || row.email || '—' }}</template>
|
<template #default="{ row }">{{ row.phone || row.email || '—' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="canManage" label="操作" width="145" fixed="right">
|
<el-table-column
|
||||||
|
v-if="canManage"
|
||||||
|
label="操作"
|
||||||
|
:width="active === 'teachers' ? 225 : 145"
|
||||||
|
fixed="right"
|
||||||
|
>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<el-button
|
||||||
|
v-if="active === 'teachers' && !row.userId"
|
||||||
|
link
|
||||||
|
type="success"
|
||||||
|
:icon="Key"
|
||||||
|
:disabled="row.status !== 'Active'"
|
||||||
|
@click="openActivation(row)"
|
||||||
|
>
|
||||||
|
激活账号
|
||||||
|
</el-button>
|
||||||
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
|
||||||
<el-button link type="danger" @click="remove(row)">删除</el-button>
|
<el-button link type="danger" @click="remove(row)">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
@@ -514,5 +577,63 @@ watch(active, async () => {
|
|||||||
<el-button type="primary" @click="save">保存档案</el-button>
|
<el-button type="primary" @click="save">保存档案</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
v-model="activationDialog"
|
||||||
|
title="激活教师账号"
|
||||||
|
width="520px"
|
||||||
|
@closed="activatingTeacher = null"
|
||||||
|
>
|
||||||
|
<el-alert
|
||||||
|
title="账号激活后,教师可使用工号和初始密码登录。请通过安全方式将密码交给教师。"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<el-form v-if="activatingTeacher" label-position="top" class="entity-form activation-account-form">
|
||||||
|
<div class="form-grid">
|
||||||
|
<el-form-item label="教师">
|
||||||
|
<el-input :model-value="activatingTeacher.name" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="登录账号">
|
||||||
|
<el-input :model-value="activatingTeacher.teacherNumber" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="初始密码" required>
|
||||||
|
<el-input
|
||||||
|
v-model="activationForm.password"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
autocomplete="new-password"
|
||||||
|
placeholder="至少 8 位,含大小写字母、数字和特殊字符"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认初始密码" required>
|
||||||
|
<el-input
|
||||||
|
v-model="activationForm.confirmPassword"
|
||||||
|
type="password"
|
||||||
|
show-password
|
||||||
|
autocomplete="new-password"
|
||||||
|
@keyup.enter="activateTeacherAccount"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="activationDialog = false">取消</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
:loading="activationLoading"
|
||||||
|
@click="activateTeacherAccount"
|
||||||
|
>
|
||||||
|
确认激活
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.activation-account-form {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
||||||
import { CopyDocument, Plus, Promotion, Refresh, Search, Setting } from '@element-plus/icons-vue'
|
import { CopyDocument, EditPen, Plus, Promotion, Refresh, Search, Setting } from '@element-plus/icons-vue'
|
||||||
import http, { apiErrorMessage } from '../api/http'
|
import http, { apiErrorMessage } from '../api/http'
|
||||||
|
|
||||||
const plans = ref<any[]>([])
|
const plans = ref<any[]>([])
|
||||||
@@ -29,6 +29,7 @@ const keyword = ref('')
|
|||||||
const termId = ref<string | undefined>()
|
const termId = ref<string | undefined>()
|
||||||
const planForm = reactive<Record<string, any>>({})
|
const planForm = reactive<Record<string, any>>({})
|
||||||
const cloneForm = reactive<Record<string, any>>({})
|
const cloneForm = reactive<Record<string, any>>({})
|
||||||
|
const revisingPublishedPlan = ref(false)
|
||||||
const entryForm = reactive<Record<string, any>>({})
|
const entryForm = reactive<Record<string, any>>({})
|
||||||
const constraintForm = reactive<Record<string, any>>({})
|
const constraintForm = reactive<Record<string, any>>({})
|
||||||
const constraintBatchForm = reactive<Record<string, any>>({})
|
const constraintBatchForm = reactive<Record<string, any>>({})
|
||||||
@@ -478,8 +479,9 @@ async function savePlan() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openClone() {
|
function openClone() {
|
||||||
|
revisingPublishedPlan.value = selected.value.status === 'Published'
|
||||||
Object.assign(cloneForm, {
|
Object.assign(cloneForm, {
|
||||||
name: `${selected.value.name}(调整版)`,
|
name: `${selected.value.name}(修订版)`,
|
||||||
version: `V${plans.value.length + 1}`,
|
version: `V${plans.value.length + 1}`,
|
||||||
})
|
})
|
||||||
cloneDialog.value = true
|
cloneDialog.value = true
|
||||||
@@ -487,10 +489,18 @@ function openClone() {
|
|||||||
|
|
||||||
async function clonePlan() {
|
async function clonePlan() {
|
||||||
try {
|
try {
|
||||||
await http.post(`/schedules/plans/${selected.value.id}/clone`, cloneForm)
|
const { data } = await http.post(
|
||||||
|
`/schedules/plans/${selected.value.id}/clone`,
|
||||||
|
cloneForm,
|
||||||
|
)
|
||||||
cloneDialog.value = false
|
cloneDialog.value = false
|
||||||
ElMessage.success('已复制为可调整的草稿版本')
|
ElMessage.success(
|
||||||
|
revisingPublishedPlan.value
|
||||||
|
? '已建立修订草稿,原已发布课表继续生效'
|
||||||
|
: '已复制为可调整的草稿版本',
|
||||||
|
)
|
||||||
await loadPlans(false)
|
await loadPlans(false)
|
||||||
|
if (data.id && selected.value?.id !== data.id) await loadDetail(data.id)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(apiErrorMessage(error))
|
ElMessage.error(apiErrorMessage(error))
|
||||||
}
|
}
|
||||||
@@ -499,7 +509,7 @@ async function clonePlan() {
|
|||||||
async function publishPlan() {
|
async function publishPlan() {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(
|
await ElMessageBox.confirm(
|
||||||
'系统将再次检查全部教师、行政班和教室冲突;发布后本版本锁定,并归档旧课表。',
|
'系统将再次检查全部教师、行政班和教室冲突;发布后本版本锁定并替换旧课表,后续仍可通过“发布后修改”建立修订版。',
|
||||||
'发布课表',
|
'发布课表',
|
||||||
{ type: 'warning', confirmButtonText: '检查并发布', cancelButtonText: '取消' },
|
{ type: 'warning', confirmButtonText: '检查并发布', cancelButtonText: '取消' },
|
||||||
)
|
)
|
||||||
@@ -631,7 +641,23 @@ onBeforeUnmount(clearAutoSchedulePoll)
|
|||||||
</div>
|
</div>
|
||||||
<div class="plan-actions">
|
<div class="plan-actions">
|
||||||
<el-button v-if="isDraft" :disabled="autoLoading" @click="openPlan(selected)">编辑版本</el-button>
|
<el-button v-if="isDraft" :disabled="autoLoading" @click="openPlan(selected)">编辑版本</el-button>
|
||||||
<el-button :icon="CopyDocument" :disabled="autoLoading" @click="openClone">复制调整</el-button>
|
<el-button
|
||||||
|
v-if="selected.status === 'Published'"
|
||||||
|
type="warning"
|
||||||
|
plain
|
||||||
|
:icon="EditPen"
|
||||||
|
@click="openClone"
|
||||||
|
>
|
||||||
|
发布后修改
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
v-else
|
||||||
|
:icon="CopyDocument"
|
||||||
|
:disabled="autoLoading"
|
||||||
|
@click="openClone"
|
||||||
|
>
|
||||||
|
复制调整
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
v-if="isDraft"
|
v-if="isDraft"
|
||||||
type="warning"
|
type="warning"
|
||||||
@@ -723,12 +749,28 @@ onBeforeUnmount(clearAutoSchedulePoll)
|
|||||||
<template #footer><el-button @click="planDialog = false">取消</el-button><el-button type="primary" @click="savePlan">保存</el-button></template>
|
<template #footer><el-button @click="planDialog = false">取消</el-button><el-button type="primary" @click="savePlan">保存</el-button></template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="cloneDialog" title="复制排课版本" width="520px">
|
<el-dialog
|
||||||
|
v-model="cloneDialog"
|
||||||
|
:title="revisingPublishedPlan ? '发布后修改课表' : '复制排课版本'"
|
||||||
|
width="520px"
|
||||||
|
>
|
||||||
|
<el-alert
|
||||||
|
v-if="revisingPublishedPlan"
|
||||||
|
title="系统会建立一份可编辑的修订草稿;原课表在修订版重新发布前继续生效,不会影响教师和学生查课。"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
<el-form label-position="top">
|
<el-form label-position="top">
|
||||||
<el-form-item label="新版本名称" required><el-input v-model="cloneForm.name" /></el-form-item>
|
<el-form-item label="新版本名称" required><el-input v-model="cloneForm.name" /></el-form-item>
|
||||||
<el-form-item label="新版本号" required><el-input v-model="cloneForm.version" /></el-form-item>
|
<el-form-item label="新版本号" required><el-input v-model="cloneForm.version" /></el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer><el-button @click="cloneDialog = false">取消</el-button><el-button type="primary" @click="clonePlan">复制</el-button></template>
|
<template #footer>
|
||||||
|
<el-button @click="cloneDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="clonePlan">
|
||||||
|
{{ revisingPublishedPlan ? '建立修订草稿' : '复制' }}
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog v-model="entryDialog" :title="editingEntryId ? '调整排课' : '添加排课'" width="680px">
|
<el-dialog v-model="entryDialog" :title="editingEntryId ? '调整排课' : '添加排课'" width="680px">
|
||||||
|
|||||||
Reference in New Issue
Block a user