工作台 8 项指标全部改为明确中文标题。
ISO 8601 时间统一转换为北京时间,例如 2026/07/23 22:29。 审计日志、缴费确认时间、通知发布时间统一使用时间格式化。 管理员层级、流程类型、录取状态等建立统一中文映射。 工作台、流程设计、学校管理等管理页面的装饰性英文标题改为中文。 历史审计记录中的 plan、draft 等枚举也会自动翻译。 更新静态资源版本号,避免浏览器继续使用旧缓存。
This commit is contained in:
@@ -196,6 +196,7 @@ internal static class AdminPublicationProjector
|
||||
["summary"] = summary,
|
||||
["author"] = "系统自动发布",
|
||||
["publishedAt"] = publishedAt,
|
||||
["publicVisible"] = visible,
|
||||
["visible"] = visible,
|
||||
["status"] = visible ? "visible" : "hidden"
|
||||
};
|
||||
|
||||
@@ -82,13 +82,13 @@ async function copyCodes() {
|
||||
<div class="security-stack">
|
||||
<div v-if="error" class="form-error">{{ error }}</div>
|
||||
<section v-if="recoveryCodes.length" class="recovery-code-panel">
|
||||
<div><p>RECOVERY CODES</p><h2>立即保存恢复码</h2><span>每个恢复码只能使用一次,关闭页面后系统不会再次展示本组代码。</span></div>
|
||||
<div><p>账户恢复</p><h2>立即保存恢复码</h2><span>每个恢复码只能使用一次,关闭页面后系统不会再次展示本组代码。</span></div>
|
||||
<div class="recovery-code-grid"><code v-for="code in recoveryCodes" :key="code">{{ code }}</code></div>
|
||||
<button class="app-button app-button--primary" type="button" @click="copyCodes">复制全部恢复码</button>
|
||||
</section>
|
||||
|
||||
<form class="business-form security-card" @submit.prevent="changePassword">
|
||||
<header><div><p>LOGIN PASSWORD</p><h2>修改登录密码</h2></div><StatusBadge value="active" /></header>
|
||||
<header><div><p>登录安全</p><h2>修改登录密码</h2></div><StatusBadge value="active" /></header>
|
||||
<span>账号:{{ sessionStore.state.user?.username || sessionStore.state.user?.candidateNumber }}。新密码至少 8 位,并应与当前密码不同。</span>
|
||||
<label><span>当前密码</span><input v-model="password.currentPassword" type="password" autocomplete="current-password" required></label>
|
||||
<label><span>新密码</span><input v-model="password.newPassword" type="password" autocomplete="new-password" minlength="8" required></label>
|
||||
@@ -97,7 +97,7 @@ async function copyCodes() {
|
||||
</form>
|
||||
|
||||
<section class="business-form security-card">
|
||||
<header><div><p>TWO-STEP VERIFICATION</p><h2>TOTP 二次验证</h2></div><StatusBadge :value="status.enabled ? 'active' : 'disabled'" /></header>
|
||||
<header><div><p>二次验证</p><h2>动态验证码(TOTP)</h2></div><StatusBadge :value="status.enabled ? 'active' : 'disabled'" /></header>
|
||||
<template v-if="!status.enabled && !setup">
|
||||
<span>使用验证器应用生成的动态验证码,为账号增加独立于密码的第二层保护。</span>
|
||||
<form class="inline-security-form" @submit.prevent="beginSetup"><label><span>确认当前密码</span><input v-model="setupPassword" type="password" autocomplete="current-password" required></label><button class="app-button app-button--primary" :disabled="busy">开始绑定验证器</button></form>
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
<script setup>
|
||||
import { statusLabel } from '@/lib/business-labels';
|
||||
|
||||
const props = defineProps({ value: { type: [String, Boolean, Number], default: '' } });
|
||||
const labels = {
|
||||
approved: '已通过', pending: '待处理', rejected: '已退回', draft: '草稿', published: '已发布',
|
||||
open: '开放中', upcoming: '即将开放', closed: '已结束', archived: '已归档', completed: '已完成',
|
||||
active: '正常', disabled: '已停用', unpaid: '未缴费', paid: '已缴费', final: '正式录取',
|
||||
school_review: '学校审核', withdrawal_pending: '退档待审', reported: '已报到', not_reported: '未报到'
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span :class="['status-badge', `is-${String(props.value).replaceAll('_', '-')}`]">{{ labels[props.value] || props.value || '—' }}</span>
|
||||
<span :class="['status-badge', `is-${String(props.value).replaceAll('_', '-')}`]">{{ statusLabel(props.value) }}</span>
|
||||
</template>
|
||||
|
||||
@@ -28,7 +28,7 @@ async function logout() {
|
||||
<template>
|
||||
<div class="portal-shell">
|
||||
<aside :class="['portal-shell__sidebar', { 'is-open': open }]">
|
||||
<RouterLink class="portal-shell__brand" to="/"><span>衡</span><div><strong>衡准考试服务</strong><small>OPERATIONS CONSOLE</small></div></RouterLink>
|
||||
<RouterLink class="portal-shell__brand" to="/"><span>衡</span><div><strong>衡准考试服务</strong><small>业务管理工作台</small></div></RouterLink>
|
||||
<button class="portal-shell__close" type="button" aria-label="关闭菜单" @click="open = false">×</button>
|
||||
<p class="portal-shell__role">{{ roleTitle }}</p>
|
||||
<nav aria-label="业务导航">
|
||||
@@ -49,7 +49,7 @@ async function logout() {
|
||||
<div class="portal-shell__user"><i>{{ String(user.displayName || '用').slice(0, 1) }}</i><span><strong>{{ user.displayName || user.username }}</strong><small>{{ scope }}</small></span><button type="button" title="退出登录" @click="logout">退出</button></div>
|
||||
</header>
|
||||
<section class="portal-shell__content">
|
||||
<header class="portal-page-heading"><div><p>{{ role === 'candidate' ? 'CANDIDATE SERVICE' : role === 'admission_school' ? 'SCHOOL ADMISSION' : 'EXAM OPERATIONS' }}</p><h1>{{ title }}</h1><span>{{ description }}</span></div><slot name="actions" /></header>
|
||||
<header class="portal-page-heading"><div><p>{{ role === 'candidate' ? '考生服务' : role === 'admission_school' ? '招生学校工作台' : '考试业务管理' }}</p><h1>{{ title }}</h1><span>{{ description }}</span></div><slot name="actions" /></header>
|
||||
<slot />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
const adminLevelLabels = {
|
||||
super: "超级管理员",
|
||||
school: "校级管理员",
|
||||
class: "班级管理员",
|
||||
};
|
||||
|
||||
const workflowTypeLabels = {
|
||||
profile_change: "考生资料变更",
|
||||
registration_review: "考试报名审核",
|
||||
center_change: "考点考场变更",
|
||||
candidate_account_batch: "批量报名号申领",
|
||||
score_appeal: "考生成绩复议",
|
||||
};
|
||||
|
||||
const businessTypeLabels = {
|
||||
plan: "招生计划",
|
||||
qualification: "指标资格",
|
||||
admission: "录取结果",
|
||||
cutoff: "录取分数线",
|
||||
reporting: "报到情况",
|
||||
};
|
||||
|
||||
const statusLabels = {
|
||||
approved: "已通过",
|
||||
pending: "待处理",
|
||||
rejected: "已退回",
|
||||
draft: "草稿",
|
||||
published: "已发布",
|
||||
open: "开放中",
|
||||
upcoming: "即将开放",
|
||||
closed: "已结束",
|
||||
archived: "已归档",
|
||||
completed: "已完成",
|
||||
active: "正常",
|
||||
disabled: "已停用",
|
||||
unpaid: "未缴费",
|
||||
paid: "已缴费",
|
||||
final: "正式录取",
|
||||
admitted: "已接收",
|
||||
school_review: "学校审核",
|
||||
withdrawal_pending: "退档待审",
|
||||
withdrawn: "已退档",
|
||||
reported: "已报到",
|
||||
not_reported: "未报到",
|
||||
submitted: "已提交",
|
||||
filling: "志愿填报中",
|
||||
supplementary: "补录中",
|
||||
locked: "已锁定",
|
||||
visible: "已公开",
|
||||
hidden: "已隐藏",
|
||||
arranged: "已编排",
|
||||
};
|
||||
|
||||
export function adminLevelLabel(value) {
|
||||
return adminLevelLabels[value] || value || "—";
|
||||
}
|
||||
|
||||
export function workflowTypeLabel(value) {
|
||||
return workflowTypeLabels[value] || value || "—";
|
||||
}
|
||||
|
||||
export function statusLabel(value) {
|
||||
if (value === true) return "是";
|
||||
if (value === false) return "否";
|
||||
return statusLabels[value] || value || "—";
|
||||
}
|
||||
|
||||
export function businessValueLabel(value) {
|
||||
return (
|
||||
adminLevelLabels[value] ||
|
||||
workflowTypeLabels[value] ||
|
||||
businessTypeLabels[value] ||
|
||||
statusLabels[value] ||
|
||||
value ||
|
||||
"—"
|
||||
);
|
||||
}
|
||||
|
||||
export function auditDetailLabel(value) {
|
||||
if (!value) return "—";
|
||||
return String(value)
|
||||
.split(" · ")
|
||||
.map((part) => businessValueLabel(part))
|
||||
.join(" · ");
|
||||
}
|
||||
@@ -3,6 +3,7 @@ export function formatDate(value, withTime = false) {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return String(value);
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
@@ -18,6 +19,7 @@ export function dateRange(start, end) {
|
||||
if (!endDate || Number.isNaN(endDate.getTime())) return formatDate(start);
|
||||
|
||||
const day = value => new Intl.DateTimeFormat('zh-CN', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric', month: '2-digit', day: '2-digit'
|
||||
}).format(value);
|
||||
return `${day(startDate)} — ${day(endDate)}`;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -456,7 +456,7 @@ hydrateSetting();
|
||||
<div v-if="error" class="form-error">{{ error }}</div>
|
||||
<section v-if="issued" class="issued-credential">
|
||||
<div>
|
||||
<span>ONE-TIME CREDENTIAL</span>
|
||||
<span>一次性登录凭据</span>
|
||||
<h2>招生学校临时密码</h2>
|
||||
<p>关闭后不再展示,请安全交付。</p>
|
||||
</div>
|
||||
@@ -474,7 +474,7 @@ hydrateSetting();
|
||||
</section>
|
||||
<section class="admission-command-banner">
|
||||
<div>
|
||||
<span>ADMISSION COMMAND</span>
|
||||
<span>招生工作总览</span>
|
||||
<h2>中考招生录取控制台</h2>
|
||||
<p>
|
||||
志愿内容仅超级管理员可见且不可代改;投档和录取变更全部进入审计日志。
|
||||
@@ -487,7 +487,7 @@ hydrateSetting();
|
||||
class="business-form admission-admin-setting"
|
||||
@submit.prevent="saveSetting"
|
||||
>
|
||||
<p>EXAM PREFERENCE SETTING</p>
|
||||
<p>考试志愿设置</p>
|
||||
<h2>考试志愿与录取阶段</h2>
|
||||
<label
|
||||
><span>考试</span
|
||||
@@ -612,7 +612,7 @@ hydrateSetting();
|
||||
|
||||
<template v-else-if="page === 'admission-accounts'">
|
||||
<form class="business-form" @submit.prevent="createAccount">
|
||||
<p>ADMISSION SCHOOL ACCOUNT</p>
|
||||
<p>招生学校账号</p>
|
||||
<h2>创建招生学校账户</h2>
|
||||
<div class="form-grid">
|
||||
<label
|
||||
@@ -751,7 +751,7 @@ hydrateSetting();
|
||||
|
||||
<template v-else-if="page === 'admission-plans'">
|
||||
<form class="business-form" @submit.prevent="submitPlan">
|
||||
<p>PLAN ON BEHALF</p>
|
||||
<p>代报招生计划</p>
|
||||
<h2>代上传招生计划</h2>
|
||||
<div class="form-grid">
|
||||
<label
|
||||
|
||||
@@ -7,7 +7,9 @@ import StatusBadge from "@/components/common/StatusBadge.vue";
|
||||
import QualificationLedger from "@/components/admin/QualificationLedger.vue";
|
||||
import { useLedger } from "@/composables/useLedger";
|
||||
import { api } from "@/lib/api";
|
||||
import { adminLevelLabel, auditDetailLabel } from "@/lib/business-labels";
|
||||
import { downloadSelectedExcel } from "@/lib/export-selection";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import { sessionStore } from "@/stores/session";
|
||||
import { uiStore } from "@/stores/ui";
|
||||
|
||||
@@ -47,6 +49,24 @@ const candidateSelected = ref([]);
|
||||
const registrationSelected = ref([]);
|
||||
const paymentSelected = ref([]);
|
||||
|
||||
const dashboardMetricDefinitions = [
|
||||
["candidates", "考生总数"],
|
||||
["pendingCandidates", "资料待审核"],
|
||||
["registrations", "报名记录"],
|
||||
["pendingRegistrations", "报名待审核"],
|
||||
["pendingPayments", "待缴费"],
|
||||
["pendingFlows", "待办流程"],
|
||||
["publishedExams", "已发布考试"],
|
||||
["notices", "通知公告"],
|
||||
];
|
||||
const dashboardMetrics = computed(() =>
|
||||
dashboardMetricDefinitions.map(([key, label]) => ({
|
||||
key,
|
||||
label,
|
||||
value: props.data.metrics?.[key] ?? 0,
|
||||
})),
|
||||
);
|
||||
|
||||
const classById = computed(
|
||||
() => new Map((props.data.classes || []).map((item) => [item.id, item])),
|
||||
);
|
||||
@@ -634,7 +654,7 @@ async function importExcel(resource, payload) {
|
||||
<div v-if="error" class="form-error">{{ error }}</div>
|
||||
<section v-if="issued" class="issued-credential">
|
||||
<div>
|
||||
<span>ONE-TIME CREDENTIAL</span>
|
||||
<span>一次性登录凭据</span>
|
||||
<h2>{{ issued.title }}</h2>
|
||||
<p>请通过线下安全渠道交付;关闭后不再展示。</p>
|
||||
</div>
|
||||
@@ -653,16 +673,16 @@ async function importExcel(resource, payload) {
|
||||
|
||||
<template v-if="page === 'dashboard'">
|
||||
<section class="scope-banner-vue">
|
||||
<span>{{ sessionStore.state.user?.adminLevel }}</span>
|
||||
<span>{{ adminLevelLabel(sessionStore.state.user?.adminLevel) }}</span>
|
||||
<div>
|
||||
<strong>{{ data.scopeLabel }}</strong
|
||||
><small>以下指标已按当前管理员数据范围过滤</small>
|
||||
</div>
|
||||
</section>
|
||||
<section class="record-metrics">
|
||||
<article v-for="(value, key) in data.metrics" :key="key">
|
||||
<span>{{ key }}</span
|
||||
><strong>{{ value }}</strong>
|
||||
<article v-for="metric in dashboardMetrics" :key="metric.key">
|
||||
<span>{{ metric.label }}</span
|
||||
><strong>{{ metric.value }}</strong>
|
||||
</article>
|
||||
</section>
|
||||
<section class="record-panel audit-ledger">
|
||||
@@ -675,9 +695,9 @@ async function importExcel(resource, payload) {
|
||||
<div v-for="log in data.logs" :key="log.id" class="dashboard-row">
|
||||
<b>{{ String(log.actorName || "系").slice(0, 1) }}</b
|
||||
><span
|
||||
><strong>{{ log.actorName }} · {{ log.action }}</strong
|
||||
><small>{{ log.detail }}</small></span
|
||||
><time>{{ log.createdAt }}</time>
|
||||
><strong>{{ log.actorName || "系统" }} · {{ log.action }}</strong
|
||||
><small>{{ auditDetailLabel(log.detail) }}</small></span
|
||||
><time>{{ formatDate(log.createdAt, true) }}</time>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
@@ -689,7 +709,7 @@ async function importExcel(resource, payload) {
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
<p>ORGANIZATION</p>
|
||||
<p>组织机构</p>
|
||||
<h2>新增学校档案</h2>
|
||||
</div>
|
||||
</header>
|
||||
@@ -910,7 +930,7 @@ async function importExcel(resource, payload) {
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
<p>ACCOUNT AUTHORITY</p>
|
||||
<p>账号权限</p>
|
||||
<h2>创建管理员</h2>
|
||||
</div>
|
||||
<button
|
||||
@@ -1019,7 +1039,7 @@ async function importExcel(resource, payload) {
|
||||
<tr v-for="item in adminLedger.rows" :key="item.id">
|
||||
<td>{{ item.displayName }}</td>
|
||||
<td>{{ item.username }}</td>
|
||||
<td>{{ item.levelName || item.adminLevel }}</td>
|
||||
<td>{{ item.levelName || adminLevelLabel(item.adminLevel) }}</td>
|
||||
<td>
|
||||
{{ item.schoolName || "全局" }} {{ item.className || "" }}
|
||||
</td>
|
||||
@@ -1060,7 +1080,7 @@ async function importExcel(resource, payload) {
|
||||
@import="importExcel('account_quotas', $event)"
|
||||
/>
|
||||
<form class="business-form" @submit.prevent="submitBatch">
|
||||
<p>SCHOOL ACCOUNT REQUEST</p>
|
||||
<p>报名号申领</p>
|
||||
<h2>按班级申领报名号</h2>
|
||||
<div class="quota-grid-vue">
|
||||
<label v-for="item in data.classes" :key="item.id"
|
||||
@@ -1159,7 +1179,7 @@ async function importExcel(resource, payload) {
|
||||
@submit.prevent="archiveCandidates"
|
||||
>
|
||||
<div>
|
||||
<p>SCHOOL ACCOUNT ARCHIVE</p>
|
||||
<p>账号归档</p>
|
||||
<h2>按班级或年级归档账户</h2>
|
||||
<span>只冻结登录,不删除报名、准考证、成绩和审计记录。</span>
|
||||
</div>
|
||||
@@ -1731,7 +1751,7 @@ async function importExcel(resource, payload) {
|
||||
<td>¥ {{ Number(item.amountDue || 0).toFixed(2) }}</td>
|
||||
<td><StatusBadge :value="item.paymentStatus" /></td>
|
||||
<td>
|
||||
{{ item.paidAt || "—" }}<small>{{ item.paidByName }}</small>
|
||||
{{ formatDate(item.paidAt, true) }}<small>{{ item.paidByName }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<template
|
||||
|
||||
@@ -636,7 +636,7 @@ onMounted(() => {
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
<p>NEW EXAM</p>
|
||||
<p>新建考试</p>
|
||||
<h2>创建考试与科目</h2>
|
||||
<span>先定义科目计分,再选择整场合格判定方式。</span>
|
||||
</div>
|
||||
@@ -824,7 +824,7 @@ onMounted(() => {
|
||||
v-if="data.canArrange"
|
||||
class="business-form arrangement-console-vue"
|
||||
>
|
||||
<p>ADMIT ARRANGEMENT</p>
|
||||
<p>准考证编排</p>
|
||||
<h2>整场准考证编排</h2>
|
||||
<div class="form-grid">
|
||||
<label
|
||||
|
||||
@@ -6,6 +6,7 @@ import LedgerPager from "@/components/common/LedgerPager.vue";
|
||||
import StatusBadge from "@/components/common/StatusBadge.vue";
|
||||
import { useLedger } from "@/composables/useLedger";
|
||||
import { api } from "@/lib/api";
|
||||
import { adminLevelLabel, workflowTypeLabel } from "@/lib/business-labels";
|
||||
import { downloadSelectedExcel } from "@/lib/export-selection";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import { sessionStore } from "@/stores/session";
|
||||
@@ -140,7 +141,9 @@ const noticeLedger = useLedger(() => props.data.notices || [], {
|
||||
const publicationLedger = useLedger(() => props.data.publications || [], {
|
||||
filters: {
|
||||
visibility: (item, value) =>
|
||||
value === "visible" ? item.publicVisible : !item.publicVisible,
|
||||
value === "visible"
|
||||
? isPublicationVisible(item)
|
||||
: !isPublicationVisible(item),
|
||||
type: (item, value) => item.sourceType === value,
|
||||
},
|
||||
searchText: (item) =>
|
||||
@@ -176,6 +179,11 @@ function publicationTypeLabel(value) {
|
||||
}[value] || value
|
||||
);
|
||||
}
|
||||
function isPublicationVisible(item) {
|
||||
if (typeof item?.publicVisible === "boolean") return item.publicVisible;
|
||||
if (typeof item?.visible === "boolean") return item.visible;
|
||||
return item?.status === "visible";
|
||||
}
|
||||
const flowTypes = computed(() => [
|
||||
...new Set(
|
||||
(props.data.instances || [])
|
||||
@@ -285,13 +293,14 @@ function toggleNotice(item) {
|
||||
);
|
||||
}
|
||||
function togglePublication(item) {
|
||||
const currentlyVisible = isPublicationVisible(item);
|
||||
act(
|
||||
() =>
|
||||
api(`/api/admin/publications/${item.sourceType}/${item.id}`, {
|
||||
method: "PATCH",
|
||||
body: { publicVisible: !item.publicVisible },
|
||||
body: { publicVisible: !currentlyVisible },
|
||||
}),
|
||||
item.publicVisible ? "系统公示已隐藏" : "系统公示已公开",
|
||||
currentlyVisible ? "系统公示已隐藏" : "系统公示已公开",
|
||||
);
|
||||
}
|
||||
function addRoom() {
|
||||
@@ -571,7 +580,7 @@ if (props.page === "number-rules" && props.data.activeRule) {
|
||||
>
|
||||
<header class="notice-editor-studio__header">
|
||||
<div>
|
||||
<p>OFFICIAL NOTICE DESK</p>
|
||||
<p>官方通知发布台</p>
|
||||
<h2>{{ notice.id ? "编辑人工通知" : "起草新的通知公告" }}</h2>
|
||||
<span
|
||||
>正文由 CKEditor 编辑,发布前由服务端统一清理不安全内容。</span
|
||||
@@ -835,8 +844,8 @@ if (props.page === "number-rules" && props.data.activeRule) {
|
||||
<tr>
|
||||
<th>标题</th>
|
||||
<th>类型</th>
|
||||
<th>状态</th>
|
||||
<th>公开</th>
|
||||
<th>生成方式</th>
|
||||
<th>公开状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -847,15 +856,27 @@ if (props.page === "number-rules" && props.data.activeRule) {
|
||||
>
|
||||
<td>{{ item.title }}</td>
|
||||
<td>{{ publicationTypeLabel(item.sourceType) }}</td>
|
||||
<td><StatusBadge :value="item.status" /></td>
|
||||
<td>{{ item.publicVisible ? "公开" : "隐藏" }}</td>
|
||||
<td>系统自动生成</td>
|
||||
<td>
|
||||
<span
|
||||
class="status-badge"
|
||||
:class="
|
||||
isPublicationVisible(item)
|
||||
? 'is-approved'
|
||||
: 'is-archived'
|
||||
"
|
||||
>{{
|
||||
isPublicationVisible(item) ? "正在公开" : "已隐藏"
|
||||
}}</span
|
||||
>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
type="button"
|
||||
class="table-action"
|
||||
@click="togglePublication(item)"
|
||||
>
|
||||
{{ item.publicVisible ? "隐藏" : "公开" }}
|
||||
{{ isPublicationVisible(item) ? "隐藏" : "公开" }}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -881,7 +902,7 @@ if (props.page === "number-rules" && props.data.activeRule) {
|
||||
class="business-form center-editor-vue"
|
||||
@submit.prevent="saveCenter"
|
||||
>
|
||||
<p>CONTROLLED DOSSIER</p>
|
||||
<p>受控考点档案</p>
|
||||
<h2>提交新考点档案</h2>
|
||||
<span>提交后进入考点考场变更审批,通过前不会改动正式档案。</span
|
||||
><label v-if="sessionStore.state.user?.adminLevel === 'super'"
|
||||
@@ -1238,7 +1259,7 @@ if (props.page === "number-rules" && props.data.activeRule) {
|
||||
><select v-model="flowLedger.filters.type">
|
||||
<option value="">全部类型</option>
|
||||
<option v-for="type in flowTypes" :key="type" :value="type">
|
||||
{{ type }}
|
||||
{{ workflowTypeLabel(type) }}
|
||||
</option>
|
||||
</select></label
|
||||
><label
|
||||
@@ -1261,7 +1282,7 @@ if (props.page === "number-rules" && props.data.activeRule) {
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
<span>{{ item.businessType }}</span>
|
||||
<span>{{ workflowTypeLabel(item.businessType) }}</span>
|
||||
<h2>
|
||||
{{
|
||||
item.candidateName ||
|
||||
@@ -1288,7 +1309,7 @@ if (props.page === "number-rules" && props.data.activeRule) {
|
||||
step.position < item.currentStep ? "✓" : step.position
|
||||
}}</i
|
||||
><b>{{ step.name }}</b
|
||||
><small>{{ step.adminLevel }}</small></span
|
||||
><small>{{ adminLevelLabel(step.adminLevel) }}</small></span
|
||||
>
|
||||
</div>
|
||||
<footer>
|
||||
@@ -1337,7 +1358,7 @@ if (props.page === "number-rules" && props.data.activeRule) {
|
||||
>
|
||||
<header>
|
||||
<div>
|
||||
<p>{{ workflow.businessType }}</p>
|
||||
<p>{{ workflowTypeLabel(workflow.businessType) }}</p>
|
||||
<h2>{{ workflow.name }}</h2>
|
||||
</div>
|
||||
<button
|
||||
@@ -1374,7 +1395,7 @@ if (props.page === "number-rules" && props.data.activeRule) {
|
||||
|
||||
<template v-else-if="page === 'number-rules'"
|
||||
><section class="account-number-principle-vue">
|
||||
<span>ONE CANDIDATE · ONE NUMBER</span>
|
||||
<span>一名考生 · 一个报名号</span>
|
||||
<h2>超级管理员只设计号码规则</h2>
|
||||
<p>学校按班级提交申领,最终批准后系统才创建长期考生账户。</p>
|
||||
</section>
|
||||
@@ -1401,7 +1422,7 @@ if (props.page === "number-rules" && props.data.activeRule) {
|
||||
><input v-model="rule.school_code" type="checkbox" />
|
||||
学校代码</label
|
||||
><label
|
||||
><input v-model="rule.gender" type="checkbox" /> 性别 M/F/X</label
|
||||
><input v-model="rule.gender" type="checkbox" /> 性别代码(M/F/X)</label
|
||||
><label
|
||||
><input v-model="rule.literal" type="checkbox" /> 固定值
|
||||
<input v-model="rule.literalValue" /></label
|
||||
|
||||
@@ -8,6 +8,7 @@ import ProfileFields from '@/components/common/ProfileFields.vue';
|
||||
import AccountSecurity from '@/components/common/AccountSecurity.vue';
|
||||
import { api } from '@/lib/api';
|
||||
import { dateRange, formatDate, money, passPolicyText } from '@/lib/format';
|
||||
import { statusLabel } from '@/lib/business-labels';
|
||||
import { sessionStore } from '@/stores/session';
|
||||
import { uiStore } from '@/stores/ui';
|
||||
|
||||
@@ -186,13 +187,13 @@ onMounted(load);
|
||||
|
||||
<div v-else-if="page === 'exams'" class="business-card-list"><article v-for="exam in data.exams" :key="exam.id" class="exam-apply-card"><header><span>{{ exam.code }}</span><StatusBadge :value="exam.registrationState" /></header><h2>{{ exam.name }}</h2><p>{{ exam.description }}</p><dl><div><dt>报名期限</dt><dd>{{ dateRange(exam.registrationStart, exam.registrationEnd) }}</dd></div><div><dt>考试时间</dt><dd>{{ dateRange(exam.examStart, exam.examEnd) }}</dd></div><div><dt>计分规则</dt><dd>总分 {{ exam.totalScore }} · {{ passPolicyText(exam) }}</dd></div></dl><div class="subject-choice-grid"><label v-for="subject in exam.subjects" :key="subject.id"><input v-model="selectedSubjects[exam.id]" type="checkbox" :value="subject.id" :disabled="Boolean(exam.registration)"><span><strong>{{ subject.name }}</strong><small>{{ subject.date }} {{ subject.start }} · 满分 {{ subject.fullScore }}</small><em>{{ money(subject.fee) }}</em></span></label></div><footer v-if="exam.registration"><span>已提交 {{ exam.registration.subjectIds?.length || 0 }} 个科目</span><StatusBadge :value="exam.registration.status" /></footer><button v-else class="app-button app-button--primary" type="button" :disabled="saving || exam.registrationState !== 'open' || data.profileStatus !== 'approved'" @click="registerExam(exam)">{{ data.profileStatus !== 'approved' ? '资料审核通过后可报名' : '提交考试报名' }}</button></article></div>
|
||||
|
||||
<div v-else-if="page === 'registrations'" class="business-card-list"><article v-for="item in registrations" :key="item.id" class="registration-vue-card"><header><div><span>{{ item.exam?.code }}</span><h2>{{ item.exam?.name }}</h2></div><StatusBadge :value="item.exam?.archivedAt ? 'archived' : item.status" /></header><dl><div><dt>账户报名号</dt><dd>{{ item.registrationNumber || sessionStore.state.user?.candidateNumber }}</dd></div><div><dt>当前审批</dt><dd>{{ item.workflow?.currentStepDetail?.name || item.workflow?.status || '待提交' }}</dd></div><div><dt>应缴金额</dt><dd>{{ money(item.amountDue) }}</dd></div><div><dt>缴费状态</dt><dd><StatusBadge :value="item.paymentStatus" /></dd></div></dl><div class="chip-list"><span v-for="subject in item.subjects" :key="subject.id">{{ subject.name }}<small>{{ subject.date }} {{ subject.start }}</small></span></div><p v-if="item.reviewNote">审核意见:{{ item.reviewNote }}</p></article><div v-if="!registrations.length" class="page-state page-state--empty"><strong>还没有考试报名</strong><p>资料审核通过后,可在“考试报名”中选择考试与科目。</p></div></div>
|
||||
<div v-else-if="page === 'registrations'" class="business-card-list"><article v-for="item in registrations" :key="item.id" class="registration-vue-card"><header><div><span>{{ item.exam?.code }}</span><h2>{{ item.exam?.name }}</h2></div><StatusBadge :value="item.exam?.archivedAt ? 'archived' : item.status" /></header><dl><div><dt>账户报名号</dt><dd>{{ item.registrationNumber || sessionStore.state.user?.candidateNumber }}</dd></div><div><dt>当前审批</dt><dd>{{ item.workflow?.currentStepDetail?.name || (item.workflow?.status ? statusLabel(item.workflow.status) : '待提交') }}</dd></div><div><dt>应缴金额</dt><dd>{{ money(item.amountDue) }}</dd></div><div><dt>缴费状态</dt><dd><StatusBadge :value="item.paymentStatus" /></dd></div></dl><div class="chip-list"><span v-for="subject in item.subjects" :key="subject.id">{{ subject.name }}<small>{{ subject.date }} {{ subject.start }}</small></span></div><p v-if="item.reviewNote">审核意见:{{ item.reviewNote }}</p></article><div v-if="!registrations.length" class="page-state page-state--empty"><strong>还没有考试报名</strong><p>资料审核通过后,可在“考试报名”中选择考试与科目。</p></div></div>
|
||||
|
||||
<div v-else-if="page === 'admit'" class="business-card-list"><article v-for="item in registrations.filter(row => row.admitCard)" :key="item.id" class="admit-card-vue"><header><span>{{ item.exam.code }}</span><StatusBadge :value="item.exam.archivedAt ? 'archived' : 'open'" /></header><h2>{{ item.exam.name }}</h2><div><small>准考证号</small><strong>{{ item.admitCard.number }}</strong></div><dl><div><dt>固定考点</dt><dd>{{ item.admitCard.testCenter }}</dd></div><div><dt>下载时间</dt><dd>{{ dateRange(item.exam.admitDownloadStart, item.exam.admitDownloadEnd) }}</dd></div></dl><button class="app-button app-button--primary" type="button" :disabled="Boolean(item.exam.archivedAt)" @click="downloadAdmit(item.id)">下载准考证</button></article><div v-if="!registrations.some(row => row.admitCard)" class="page-state page-state--empty"><strong>准考证尚未生成</strong><p>管理员统一编排后会显示在这里。</p></div></div>
|
||||
|
||||
<div v-else-if="page === 'results'" class="business-card-list"><section v-for="items in groupedResults" :key="items[0].examId" class="record-panel result-group"><header><div><span>{{ items[0].examCode }}</span><h2>{{ items[0].examName }}</h2></div><button class="app-button app-button--primary" type="button" @click="downloadScore(items)">下载 PDF 成绩单</button></header><div class="result-card-grid"><article v-for="result in items" :key="result.id"><span>{{ result.subjectName }}</span><strong>{{ result.score }}<small>/ {{ result.fullScore }}</small></strong><em>{{ result.grade }} · 第 {{ result.rank }} / {{ result.cohortSize }} 名</em><StatusBadge v-if="result.appeal" :value="result.appeal.status" /><form v-else @submit.prevent="submitAppeal(result)"><textarea v-model="appealReasons[result.id]" rows="2" placeholder="填写成绩复议理由"></textarea><button type="submit">申请复议</button></form></article></div></section><div v-if="!groupedResults.length" class="page-state page-state--empty"><strong>暂时没有已发布成绩</strong><p>成绩发布后会显示在这里。</p></div></div>
|
||||
|
||||
<div v-else-if="page === 'admissions'" class="business-card-list"><section v-for="item in data.admissions" :key="item.examId" class="record-panel admission-candidate-vue"><header><div><span>{{ item.exam.code }} · 第 {{ item.payload?.round || 1 }} 轮</span><h2>{{ item.exam.name }}</h2></div><StatusBadge :value="item.status" /></header><div class="record-metrics"><article><span>本场总成绩</span><strong>{{ item.totalScore ?? '未完整发布' }}</strong></article><article><span>特征分</span><strong>{{ item.featureScore || 0 }}</strong></article><article><span>已提交志愿</span><strong>{{ item.submissionCount || 0 }} / {{ item.maxSubmissions || item.payload?.maxSubmissions || 0 }}</strong></article></div><div v-if="item.placement" class="form-callout"><strong>当前录取结果:{{ item.placementSchool?.name || item.placement.schoolName || '招生学校' }}</strong><p>{{ item.placement.payload?.categoryName }} · {{ item.placement.status }}</p><button v-if="item.placement.status === 'final'" class="app-button app-button--primary" type="button" @click="downloadAdmission(item)">下载录取通知书 PDF</button></div><form v-if="['filling','supplementary'].includes(item.status) && !item.preferenceLocked && item.supplementEligible !== false" class="preference-editor" @submit.prevent="savePreferences(item)"><div v-for="(choice, index) in preferenceDrafts[item.examId]" :key="index" class="preference-row"><b>{{ choice.preferenceType === 'indicator' ? '指标' : index }}</b><select v-model="choice.schoolId" @change="choice.categoryCode = ''"><option value="">选择招生学校</option><option v-for="plan in availablePlans(item, choice.preferenceType)" :key="plan.schoolId" :value="plan.schoolId">{{ plan.schoolCode }} · {{ plan.schoolName }}</option></select><select v-model="choice.categoryCode" :disabled="!choice.schoolId"><option value="">选择招生类别</option><option v-for="category in categoriesFor(item, choice)" :key="category.code" :value="category.code">{{ category.name }}</option></select></div><button class="app-button app-button--primary">保存本人志愿</button></form><p v-else>{{ item.supplementIneligibilityReason || item.payload?.progress || '当前阶段不能修改志愿。' }}</p></section><div v-if="!data.admissions?.length" class="page-state page-state--empty"><strong>暂无志愿填报安排</strong><p>成绩发布且考试启用志愿后会显示在这里。</p></div></div>
|
||||
<div v-else-if="page === 'admissions'" class="business-card-list"><section v-for="item in data.admissions" :key="item.examId" class="record-panel admission-candidate-vue"><header><div><span>{{ item.exam.code }} · 第 {{ item.payload?.round || 1 }} 轮</span><h2>{{ item.exam.name }}</h2></div><StatusBadge :value="item.status" /></header><div class="record-metrics"><article><span>本场总成绩</span><strong>{{ item.totalScore ?? '未完整发布' }}</strong></article><article><span>特征分</span><strong>{{ item.featureScore || 0 }}</strong></article><article><span>已提交志愿</span><strong>{{ item.submissionCount || 0 }} / {{ item.maxSubmissions || item.payload?.maxSubmissions || 0 }}</strong></article></div><div v-if="item.placement" class="form-callout"><strong>当前录取结果:{{ item.placementSchool?.name || item.placement.schoolName || '招生学校' }}</strong><p>{{ item.placement.payload?.categoryName }} · {{ statusLabel(item.placement.status) }}</p><button v-if="item.placement.status === 'final'" class="app-button app-button--primary" type="button" @click="downloadAdmission(item)">下载录取通知书 PDF</button></div><form v-if="['filling','supplementary'].includes(item.status) && !item.preferenceLocked && item.supplementEligible !== false" class="preference-editor" @submit.prevent="savePreferences(item)"><div v-for="(choice, index) in preferenceDrafts[item.examId]" :key="index" class="preference-row"><b>{{ choice.preferenceType === 'indicator' ? '指标' : index }}</b><select v-model="choice.schoolId" @change="choice.categoryCode = ''"><option value="">选择招生学校</option><option v-for="plan in availablePlans(item, choice.preferenceType)" :key="plan.schoolId" :value="plan.schoolId">{{ plan.schoolCode }} · {{ plan.schoolName }}</option></select><select v-model="choice.categoryCode" :disabled="!choice.schoolId"><option value="">选择招生类别</option><option v-for="category in categoriesFor(item, choice)" :key="category.code" :value="category.code">{{ category.name }}</option></select></div><button class="app-button app-button--primary">保存本人志愿</button></form><p v-else>{{ item.supplementIneligibilityReason || item.payload?.progress || '当前阶段不能修改志愿。' }}</p></section><div v-if="!data.admissions?.length" class="page-state page-state--empty"><strong>暂无志愿填报安排</strong><p>成绩发布且考试启用志愿后会显示在这里。</p></div></div>
|
||||
|
||||
<section v-else-if="page === 'notices'" class="record-panel notice-list-vue"><button v-for="notice in data.notices" :key="notice.id" type="button" @click="openNotice(notice.id)"><time>{{ formatDate(notice.publishAt) }}</time><span><em>{{ notice.category }}</em><strong>{{ notice.title }}</strong><small>{{ notice.summary }}</small></span><i>→</i></button></section>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<meta name="theme-color" content="#0d2d54" />
|
||||
<meta name="description" content="衡准考试信息管理系统——考试通知、考生报名、准考证与成绩查询一站式服务。" />
|
||||
<title>衡准 · 考试信息管理系统</title>
|
||||
<link rel="stylesheet" href="/vue-app/app.css?v=20260723" />
|
||||
<link rel="stylesheet" href="/vue-app/app.css?v=20260723-2" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" aria-live="polite">
|
||||
@@ -16,6 +16,6 @@
|
||||
<span>系统正在加载</span>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/vue-app/app.js?v=20260723"></script>
|
||||
<script type="module" src="/vue-app/app.js?v=20260723-2"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -26,4 +26,27 @@ public sealed class AdminNoticeServiceTests
|
||||
new JsonObject { ["publicVisible"] = "false" },
|
||||
out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PublicationProjector_ExposesConsistentVisibilityFields()
|
||||
{
|
||||
var payload = new JsonObject
|
||||
{
|
||||
["publicVisible"] = false,
|
||||
["reviewedAt"] = "2026-07-23T00:00:00.000Z"
|
||||
};
|
||||
var snapshot = new AdminNoticeManagementSnapshot(
|
||||
[],
|
||||
[new AdminPublicationRecord(
|
||||
"plan_1", "plan", "exam_1", null, "school_1", "approved", payload,
|
||||
"2026-07-23T00:00:00.000Z", "2026-07-23T00:00:00.000Z")],
|
||||
new Dictionary<string, string> { ["exam_1"] = "统一考试" },
|
||||
new Dictionary<string, string> { ["school_1"] = "第一中学" });
|
||||
|
||||
var publication = Assert.Single(AdminPublicationProjector.Build(snapshot));
|
||||
|
||||
Assert.False(publication["publicVisible"]!.GetValue<bool>());
|
||||
Assert.False(publication["visible"]!.GetValue<bool>());
|
||||
Assert.Equal("hidden", publication["status"]!.GetValue<string>());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user