From c799752f636fb999fa4207c03f7d987deb8fa66c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E6=A2=93=E5=BD=AD?= Date: Sun, 6 Sep 2026 22:36:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=93=E5=88=86=E7=B3=BB=E7=BB=9F=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8F=B0=E9=83=A8=E5=88=86=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/account-management-contract-test.mjs | 59 +++++ scripts/workbench-business-contract-test.mjs | 61 +++++ src/api/match/unitaccount.js | 8 +- src/api/match/workbench.js | 60 +++++ src/router/backEnd.ts | 24 +- src/router/index.ts | 2 +- src/router/route.ts | 19 ++ src/views/match/manage/index.vue | 13 + .../permission/account/scoring-dialog.vue | 52 ++++ src/views/scoring/checkin-screen/index.vue | 70 ++++++ src/views/scoring/launcher/index.vue | 46 ++++ src/views/scoring/orderbook/index.vue | 38 +++ src/views/scoring/screen/index.vue | 74 ++++++ src/views/scoring/workbench/index.vue | 179 ++++++++++++++ src/views/system/unit/index.vue | 230 ++++-------------- 15 files changed, 743 insertions(+), 192 deletions(-) create mode 100644 scripts/account-management-contract-test.mjs create mode 100644 scripts/workbench-business-contract-test.mjs create mode 100644 src/api/match/workbench.js create mode 100644 src/views/permission/account/scoring-dialog.vue create mode 100644 src/views/scoring/checkin-screen/index.vue create mode 100644 src/views/scoring/launcher/index.vue create mode 100644 src/views/scoring/orderbook/index.vue create mode 100644 src/views/scoring/screen/index.vue create mode 100644 src/views/scoring/workbench/index.vue diff --git a/scripts/account-management-contract-test.mjs b/scripts/account-management-contract-test.mjs new file mode 100644 index 0000000..f9a8781 --- /dev/null +++ b/scripts/account-management-contract-test.mjs @@ -0,0 +1,59 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const read = (path) => readFileSync(resolve(process.cwd(), path), 'utf8'); +const accountApi = read('src/api/match/account.js'); +const unitApi = read('src/api/match/unitaccount.js'); +const page = read('src/views/permission/account/index.vue'); +const scoringDialog = read('src/views/permission/account/scoring-dialog.vue'); +const sysUserService = read('../YD_Event.Admin/YD_Event.Application/User/SysUserService.cs'); +const unitService = read('../YD_Event.Admin/YD_Event.Application/Unit/UnitAccountService.cs'); +const deploymentSql = read('../YD_Event.Admin/二期需求/dbo.sql'); +const menuSeed = JSON.parse(read('../YD_Event.Admin/YD_Event.Core/InitData/sysmenu.txt')); +const roleMenuSeed = JSON.parse(read('../YD_Event.Admin/YD_Event.Core/InitData/sysrolemenu.txt')); + +const checks = [ + ['报名账号查询', accountApi, "http.get('/sysuser/page'"], + ['报名账号新增', accountApi, "http.post('/sysuser/user'"], + ['报名账号编辑', accountApi, "http.put('/sysuser/user'"], + ['报名账号删除', accountApi, "http.delete('/sysuser'"], + ['报名账号状态', accountApi, "http.post('/sysuser/setstatus'"], + ['报名账号重置密码', accountApi, "http.post('/sysuser/reset'"], + ['打分账号查询', unitApi, "http.get('/unitaccount/page'"], + ['打分账号新增', unitApi, "http.post('/unitaccount/add'"], + ['打分账号编辑', unitApi, "http.put('/unitaccount/edit'"], + ['打分账号删除', unitApi, "http.delete('/unitaccount/delete'"], + ['打分账号状态', unitApi, "http.post('/unitaccount/setStatus'"], + ['打分账号重置密码', unitApi, "http.post('/unitaccount/resetPassword'"], + ['报名平台页签', page, 'label="报名平台"'], + ['打分系统页签', page, 'label="打分系统"'], + ['打分查询固定平台', page, 'Platform: SCORING_PLATFORM'], + ['打分禁用确认', page, "confirmDisable('打分系统')"], + ['新增打分账号固定平台', scoringDialog, 'platform: SCORING_PLATFORM'], + ['新增打分账号自动角色', unitService, 'RoleId = GetRoleId(input.Platform)'], + ['报名账号数据域过滤', sysUserService, 'x.UnitPlatform == UnitAccountPlatform.EventSystem'], + ['报名账号详情隔离', sysUserService, 'await GetEventSystemUserAsync(id)'], + ['报名账号状态隔离', sysUserService, 'public override async Task SetStatus'], + ['单位接口阻止普通账号', unitService, '该账号不是单位账号'], + ['全局账号查重', unitService, 'x.Account == input.Account'], + ['打分账号密码加盐', unitService, '_idGenerator.Encode(user.Id)'], + ['上线SQL挂载打分账号按钮', deploymentSql, "(1081,N'打分账号-新增',2,N'system:unit:add',1047"], + ['上线SQL停用旧单位菜单', deploymentSql, "WHERE Id=1080 AND Code=N'system:unit' AND DeleteMark=0"], + ['上线SQL校验账号管理父级', deploymentSql, 'ParentId=1047 AND DeleteMark=0)=5'], +]; + +for (const [name, source, expected] of checks) { + assert.ok(source.includes(expected), `${name}:未找到 ${expected}`); + process.stdout.write(`PASS ${name}\n`); +} + +const scoringButtonIds = new Set([1081, 1082, 1083, 1084, 1085]); +const scoringButtons = menuSeed.filter((item) => scoringButtonIds.has(item.Id)); +assert.equal(scoringButtons.length, 5, '初始化菜单:打分账号按钮数量应为 5'); +assert.ok(scoringButtons.every((item) => item.ParentId === 1047), '初始化菜单:打分账号按钮必须挂到账号管理'); +assert.ok(!menuSeed.some((item) => item.Id === 1080), '初始化菜单:不应再包含单位管理菜单'); +assert.ok(!roleMenuSeed.some((item) => item.MenuId === 1080), '初始化授权:不应再授权单位管理菜单'); +process.stdout.write('PASS 初始化菜单与角色授权一致性\n'); + +process.stdout.write(`账号管理契约测试通过:${checks.length + 1}/${checks.length + 1}\n`); diff --git a/scripts/workbench-business-contract-test.mjs b/scripts/workbench-business-contract-test.mjs new file mode 100644 index 0000000..72066dc --- /dev/null +++ b/scripts/workbench-business-contract-test.mjs @@ -0,0 +1,61 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const read = (path) => readFileSync(resolve(process.cwd(), path), 'utf8').replace(/\r\n/g, '\n'); +const controller = read('../YD_Event.Admin/YD_Event.Application/Workbench/WorkbenchController.cs'); +const dtos = read('../YD_Event.Admin/YD_Event.Application/Workbench/Dtos/WorkbenchDtos.cs'); +const orderBook = read('../YD_Event.Admin/YD_Event.Application/OrderBook/OrderBookService.cs'); +const calculation = read('../YD_Event.Admin/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs'); +const sql = read('../YD_Event.Admin/二期需求/dbo.sql'); +const launcher = read('src/views/scoring/launcher/index.vue'); +const workbench = read('src/views/scoring/workbench/index.vue'); +const orderBookPage = read('src/views/scoring/orderbook/index.vue'); +const scoreScreen = read('src/views/scoring/screen/index.vue'); +const checkinScreen = read('src/views/scoring/checkin-screen/index.vue'); +const menuSeed = read('../YD_Event.Admin/YD_Event.Core/InitData/sysmenu.txt'); +const roleMenuSeed = read('../YD_Event.Admin/YD_Event.Core/InitData/sysrolemenu.txt'); +const backEndRoutes = read('src/router/backEnd.ts'); + +const checks = [ + ['工作台接口不重复控制器前缀', controller, '[HttpGet("events")]'], + ['秩序册接口使用文档约定路由', orderBook, 'Name = "order-book"'], + ['投屏校验成绩与出场关联', controller, 'result.SessionEntryId != entry.Id'], + ['投屏校验成绩版本', controller, 'input.ResultVersion.Value != result.ResultVersion'], + ['投屏校验评分单收齐', controller, 'submittedJudgeCount < expectedJudgeCount'], + ['投屏后进入状态30', controller, 'result.ResultStatus = 30'], + ['成绩主表与版本表状态同步', controller, 'SyncResultVersionStatusAsync(result, 30'], + ['更正成绩确认后进入40', controller, 'ScoreCorrectionId.HasValue == true ? 40 : 20'], + ['聚合跳过成绩同步作废当前版本', calculation, 'version.ResultStatus = ResultStatusVoided'], + ['排名排除DQ', controller, '!r.IsDisqualified'], + ['排名不包含作废状态', controller, 'r.ResultStatus == 20 || r.ResultStatus == 30 || r.ResultStatus == 40'], + ['投屏操作事务化', controller, '[UnitOfWork]\n public async Task CastResult'], + ['重赛按组别定位末位', controller, 'e.GroupName == originalEntry.GroupName'], + ['重赛后续出场顺延', controller, 'laterEntry.PlayOrder++'], + ['批量确认必须显式选择', controller, 'results.Count != requestedIds.Count'], + ['原因服务端规范化', controller, 'NormalizeReason(input.VoidReason, "作废原因")'], + ['动态评分项接口', dtos, 'List ScoreItems'], + ['秩序册支持xls和xlsx', orderBook, 'ext is not ".xlsx" and not ".xls"'], + ['单一当前投屏唯一索引', sql, 'UX_EventResultScreenCast_Current'], + ['SQL状态30为已投屏', sql, "WHEN 30 THEN N''已投屏''"], + ['报名与秩序册数据源', launcher, "switchSource('orderbook')"], + ['赛事列表分页', launcher, 'v-model:current-page="pageNo"'], + ['工作台保存赛事上下文', launcher, "sessionStorage.setItem('scoring-context'"], + ['秩序册具有独立页面', orderBookPage, 'orderBookImportPost'], + ['成绩管理与投屏使用独立菜单路由', menuSeed, '"Path": "/scoring/cast"'], + ['秩序册直达路由具有兼容兜底', backEndRoutes, 'ensureScoringOperationalRoutes'], + ['超级管理员具有秩序册菜单授权', roleMenuSeed, '"MenuId": 1088'], + ['工作台动态评分列', workbench, 'v-for="col in detailColumns"'], + ['规定赛隐藏难度', scoreScreen, "includes('PRESCRIBED')"], + ['评分卡列数随规则动态调整', scoreScreen, 'Math.max(scoreCards.length,1)'], + ['成绩轮询防重入', scoreScreen, 'castRequesting'], + ['检录轮询防重入', checkinScreen, 'requesting=false'], + ['检录时间使用服务端基线', checkinScreen, 'serverBaseMs+(Date.now()-syncedAtMs)'], +]; + +for (const [name, source, expected] of checks) { + assert.ok(source.includes(expected), `${name}:缺少 ${expected}`); + process.stdout.write(`PASS ${name}\n`); +} + +process.stdout.write(`工作台业务契约测试通过:${checks.length}/${checks.length}\n`); diff --git a/src/api/match/unitaccount.js b/src/api/match/unitaccount.js index 2c25e9c..8c0a4bd 100644 --- a/src/api/match/unitaccount.js +++ b/src/api/match/unitaccount.js @@ -72,8 +72,8 @@ export const unitaccountEditPut = (data) => { * @param {number} data.id - 单位账号ID * @param {string} data.password - 新密码 (6-32位) */ -export const unitaccountResetPasswordPatch = (data) => { - return http.patch('/unitaccount/resetPassword', data); +export const unitaccountResetPasswordPost = (data) => { + return http.post('/unitaccount/resetPassword', data); }; /** @@ -82,8 +82,8 @@ export const unitaccountResetPasswordPatch = (data) => { * @param {number} data.id - 单位账号ID * @param {number} data.status - 状态 (0=启用, 1=禁用) */ -export const unitaccountSetstatusPatch = (data) => { - return http.patch('/unitaccount/setStatus', data); +export const unitaccountSetstatusPost = (data) => { + return http.post('/unitaccount/setStatus', data); }; /** diff --git a/src/api/match/workbench.js b/src/api/match/workbench.js new file mode 100644 index 0000000..f10d801 --- /dev/null +++ b/src/api/match/workbench.js @@ -0,0 +1,60 @@ +import { http } from '../../utils/request'; + +export const workbenchEventsGet = (params) => http.get('/workbench/events', { params }); + +export const workbenchSessionsGet = (params) => http.get('/workbench/sessions', { params }); + +export const workbenchSessionEntriesGet = (sessionId, params = {}) => + http.get(`/workbench/sessions/${sessionId}/entries`, { params }); + +export const workbenchScreenCastPost = (data) => http.post('/workbench/screencast', data); + +export const workbenchScreenCastCurrentGet = (params) => + http.get('/workbench/screencast/current', { params }); + +export const workbenchScreenCastWithdrawPost = (screenCastId) => + http.post('/workbench/screencast/withdraw', { screenCastId }); + +export const workbenchCorrectionsGet = (params) => http.get('/workbench/corrections', { params }); + +export const workbenchCorrectionDetailGet = (correctionId) => + http.get(`/workbench/corrections/${correctionId}`); + +export const workbenchCorrectionApprovePost = (data) => + http.post('/workbench/corrections/approve', data); + +export const workbenchResultConfirmPost = (resultId) => + http.post('/workbench/results/confirm', { resultId }); + +export const workbenchResultsBatchConfirmPost = (data) => + http.post('/workbench/results/batch-confirm', data); + +export const workbenchEntryRematchPost = (data) => + http.post('/workbench/entries/rematch', data); + +export const workbenchResultVoidPost = (data) => http.post('/workbench/results/void', data); + +export const judgeScoreCorrectionInitiatePost = (data) => + http.post('/judge-score/judge-scores/corrections/initiate', data); + +export const checkinScreenCastGet = (params) => http.get('/checkin/screen-cast', { params }); + +export const workbenchResultsExportGet = (params) => + http.get('/workbench/results/export', { params, responseType: 'blob' }); + +export const workbenchScoreSheetsExportGet = (params) => + http.get('/workbench/score-sheets/export', { params, responseType: 'blob' }); + +export const orderBookTemplateGet = () => + http.get('/order-book/template', { responseType: 'blob' }); + +export const orderBookImportPost = (eventId, file) => { + const data = new FormData(); + data.append('eventId', String(eventId)); + data.append('importMode', 'overwrite'); + data.append('file', file); + return http.post('/order-book/import', data, { headers: { 'Content-Type': 'multipart/form-data' } }); +}; + +export const orderBookConfirmPost = (batchId) => + http.post('/order-book/confirm', { batchId, generateJudgeAccount: false }); diff --git a/src/router/backEnd.ts b/src/router/backEnd.ts index ac0512e..20002ed 100644 --- a/src/router/backEnd.ts +++ b/src/router/backEnd.ts @@ -101,7 +101,10 @@ export async function initBackEndControlRoutes() { }, }, ]; - dynamicRoutes[0].children = await backEndComponent([...routeList, ...staticRoute]); + // 工作台业务页需要携带赛事上下文,不能依赖菜单接口是否已经下发全部子菜单。 + // 这些路由只作为隐藏的运行路由注册,左侧菜单仍以后端权限菜单为准。 + const routesWithScoringFallback = ensureScoringOperationalRoutes(routeList); + dynamicRoutes[0].children = await backEndComponent([...routesWithScoringFallback, ...staticRoute]); // dynamicRoutes[0].children = await backEndComponent(routeList); // 添加动态路由 await setAddRoute(); @@ -163,6 +166,25 @@ export function getBackEndControlRoutes() { return SysMenuApi.getMenus(); } +function ensureScoringOperationalRoutes(routes: any[]) { + const paths = new Set(); + const collectPaths = (items: any[]) => items.forEach((item) => { + if (item?.path) paths.add(item.path); + if (item?.children?.length) collectPaths(item.children); + }); + collectPaths(routes); + const scoringEnabled = paths.has('/scoring') || paths.has('/scoring/launcher') || paths.has('/scoring/workbench') || paths.has('/scoring/orderbook'); + if (!scoringEnabled) return routes; + + const fallbacks = [ + { path: '/scoring/launcher', name: 'scoringLauncherFallback', component: 'scoring/launcher/index', meta: { title: '工作台', isKeepAlive: true } }, + { path: '/scoring/orderbook', name: 'scoringOrderBookFallback', component: 'scoring/orderbook/index', meta: { title: '秩序册', isHide: true, isKeepAlive: true } }, + { path: '/scoring/workbench', name: 'scoringWorkbenchFallback', component: 'scoring/workbench/index', meta: { title: '成绩管理', isHide: true, isKeepAlive: true } }, + { path: '/scoring/cast', name: 'scoringCastFallback', component: 'scoring/workbench/index', meta: { title: '成绩投屏', isHide: true, isKeepAlive: true } }, + ].filter((item) => !paths.has(item.path)); + return [...routes, ...fallbacks]; +} + /** * 重新请求后端路由菜单接口 * @description 用于菜单管理界面刷新菜单(未进行测试) diff --git a/src/router/index.ts b/src/router/index.ts index cf866cc..87d5f68 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -96,7 +96,7 @@ router.beforeEach(async (to, from, next) => { NProgress.configure({ showSpinner: false }); if (to.meta.title) NProgress.start(); const token = Session.get(accessTokenKey); - if (to.path === '/login' && !token) { + if ((to.path === '/login' || to.meta.isPublic) && !token) { next(); NProgress.done(); } else { diff --git a/src/router/route.ts b/src/router/route.ts index 82cb51d..523a35f 100644 --- a/src/router/route.ts +++ b/src/router/route.ts @@ -27,6 +27,7 @@ declare module 'vue-router' { isIframe?: boolean; roles?: string[]; icon?: string; + isPublic?: boolean; } } @@ -601,6 +602,24 @@ export const staticRoutes: Array = [ title: '登录', }, }, + { + path: '/scoring/screen', + name: 'scoringScreen', + component: () => import('/@/views/scoring/screen/index.vue'), + meta: { + title: '成绩大屏', + isPublic: true, + }, + }, + { + path: '/scoring/checkin-screen', + name: 'scoringCheckinScreen', + component: () => import('/@/views/scoring/checkin-screen/index.vue'), + meta: { + title: '检录进度大屏', + isPublic: true, + }, + }, /** * 提示:写在这里的为全屏界面,不建议写在这里 * 请写在 `dynamicRoutes` 路由数组中 diff --git a/src/views/match/manage/index.vue b/src/views/match/manage/index.vue index c18536b..8889403 100644 --- a/src/views/match/manage/index.vue +++ b/src/views/match/manage/index.vue @@ -48,6 +48,9 @@ 赛事工作台 + 打分工作台 + @@ -58,6 +61,7 @@ diff --git a/src/views/scoring/checkin-screen/index.vue b/src/views/scoring/checkin-screen/index.vue new file mode 100644 index 0000000..2755bb9 --- /dev/null +++ b/src/views/scoring/checkin-screen/index.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/src/views/scoring/launcher/index.vue b/src/views/scoring/launcher/index.vue new file mode 100644 index 0000000..246106c --- /dev/null +++ b/src/views/scoring/launcher/index.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/src/views/scoring/orderbook/index.vue b/src/views/scoring/orderbook/index.vue new file mode 100644 index 0000000..2572b91 --- /dev/null +++ b/src/views/scoring/orderbook/index.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/src/views/scoring/screen/index.vue b/src/views/scoring/screen/index.vue new file mode 100644 index 0000000..dea9308 --- /dev/null +++ b/src/views/scoring/screen/index.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/src/views/scoring/workbench/index.vue b/src/views/scoring/workbench/index.vue new file mode 100644 index 0000000..a8935db --- /dev/null +++ b/src/views/scoring/workbench/index.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/src/views/system/unit/index.vue b/src/views/system/unit/index.vue index 367b46c..7e8c380 100644 --- a/src/views/system/unit/index.vue +++ b/src/views/system/unit/index.vue @@ -1,211 +1,69 @@ - -