From c04e60ffb8606c6fd75740fa4332ed95336145ed Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Sun, 4 Oct 2026 13:19:25 +0700 Subject: [PATCH] up --- proto/main.proto | 149 ++++++++++++---- .../crabs/evening_detective_server/index.ts | 106 ++++++++++- src/components/GamePage.vue | 168 ++++++++++++++++++ src/components/ScreenPage.vue | 1 + src/components/ScreenPreview.vue | 1 + 5 files changed, 385 insertions(+), 40 deletions(-) diff --git a/proto/main.proto b/proto/main.proto index 4ccb105..31e76aa 100644 --- a/proto/main.proto +++ b/proto/main.proto @@ -685,6 +685,36 @@ service EveningDetectiveServer { }; } + rpc GetScreenPlan(GetScreenPlanReq) returns (GetScreenPlanRsp) { + option (google.api.http) = { get: "/api/games/{gameID}/screen-plan" }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Экраны"; + summary: "Получить план экранов"; + }; + } + + rpc SaveScreenPlan(SaveScreenPlanReq) returns (SaveScreenPlanRsp) { + option (google.api.http) = { + put: "/api/games/{gameID}/screen-plan" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Экраны"; + summary: "Сохранить план экранов"; + }; + } + + rpc SetScreenPlanStep(SetScreenPlanStepReq) returns (SetScreenPlanStepRsp) { + option (google.api.http) = { + post: "/api/games/{gameID}/screen-plan/step" + body: "*" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags: "Экраны"; + summary: "Перейти на шаг плана экранов"; + }; + } + rpc GetScreenResults(GetScreenResultsReq) returns (GetScreenResultsRsp) { option (google.api.http) = { post: "/api/screens/results" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { @@ -1045,19 +1075,19 @@ message DeleteAccountRsp { message GetTermsReq {} message GetTermsRsp { - string text = 1; + string text = 1; } message GetPrivacyReq {} message GetPrivacyRsp { - string text = 1; + string text = 1; } message GetConsentReq {} message GetConsentRsp { - string text = 1; + string text = 1; } message GetUsersReq {} @@ -1080,13 +1110,13 @@ message GetUserByIDReq { } message GetUserByIDRsp { - User user = 1; + User user = 1; } message GetMeReq {} message GetMeRsp { - User user = 1; + User user = 1; } message ChangeUserPasswordReq { @@ -1139,7 +1169,7 @@ message AddScenarioReq { } message AddScenarioRsp { - int32 ID = 1; + int32 ID = 1; } message GetMyScenariosReq {} @@ -1179,11 +1209,11 @@ message Scenario { } message Story { - repeated Place places = 1; - Introduction introduction = 2; - repeated Question questions = 3; - string background = 4; - Conclusion conclusion = 5; // Только в полном сценарии, игрокам не передаётся. + repeated Place places = 1; + Introduction introduction = 2; + repeated Question questions = 3; + string background = 4; + Conclusion conclusion = 5; // Только в полном сценарии, игрокам не передаётся. } message Conclusion { @@ -1365,20 +1395,20 @@ message DownloadScenarioArchiveReq { } message UploadScenarioArchiveRsp { - int32 ID = 1; + int32 ID = 1; } message AddGameReq { string name = 1; string description = 2; google.protobuf.Timestamp start_at = 3; - int32 scenarioID = 4; + int32 scenarioID = 4; string format = 5; string location = 6; } message AddGameRsp { - int32 ID = 1; + int32 ID = 1; } message GetGamesReq {} @@ -1410,6 +1440,7 @@ enum ScreenMode { SCREEN_MODE_CONCLUSION_DONE = 5; SCREEN_MODE_RESULTS = 6; SCREEN_MODE_TIMER = 7; + SCREEN_MODE_TEXT = 8; } enum ScreenTimerState { @@ -1438,7 +1469,7 @@ enum ScreenCommand { message ScreenState { string ID = 1; string status = 2; - optional int32 gameID = 3; + optional int32 gameID = 3; ScreenMode mode = 4; bool sound_enabled = 5; string error = 6; @@ -1455,12 +1486,13 @@ message ScreenState { int64 timer_duration_seconds = 17; int64 timer_remaining_seconds = 18; ScreenTimerState timer_state = 19; + string text = 20; } message CreateScreenSessionReq {} message CreateScreenSessionRsp { - string screenID = 1; + string screenID = 1; string access_token = 2; } @@ -1508,7 +1540,7 @@ message SetScreenSoundRsp { message MapItem { string kind = 1; - int32 itemID = 2; + int32 itemID = 2; string label = 3; int32 x = 4; int32 y = 5; @@ -1563,6 +1595,45 @@ message SendScreenCommandReq { message SendScreenCommandRsp { } +message ScreenPlanStep { + ScreenMode mode = 1; + string text = 2; + string timer_label = 3; + int64 timer_duration_seconds = 4; +} + +message ScreenPlan { + repeated ScreenPlanStep steps = 1; + int32 current_step = 2; +} + +message GetScreenPlanReq { + int32 gameID = 1; +} + +message GetScreenPlanRsp { + ScreenPlan plan = 1; +} + +message SaveScreenPlanReq { + int32 gameID = 1; + repeated ScreenPlanStep steps = 2; + int32 current_step = 3; +} + +message SaveScreenPlanRsp { + ScreenPlan plan = 1; +} + +message SetScreenPlanStepReq { + int32 gameID = 1; + int32 current_step = 2; +} + +message SetScreenPlanStepRsp { + ScreenPlan plan = 1; +} + message GetScreenResultsReq { string screenID = 1; string accessToken = 2; @@ -1602,22 +1673,22 @@ message ParticipationRsp { } message Team { - int32 ID = 1; - string name = 2; - string status = 3; - string password = 4; - int32 actions_count = 5; - repeated Application applications = 6; - string captain_email = 7; - int32 participants_count = 8; + int32 ID = 1; + string name = 2; + string status = 3; + string password = 4; + int32 actions_count = 5; + repeated Application applications = 6; + string captain_email = 7; + int32 participants_count = 8; // Статус участия: pending, confirmed или cancelled. Поле панели организатора. - string participation_status = 9; + string participation_status = 9; // Источник решения: captain, automatic либо пустая строка до ответа. string participation_source = 10; // Последняя ошибка отправки; пустая строка, если ошибки нет. string participation_email_error = 11; // Письмо принято SMTP-сервером; не означает прочтение или доставку во входящие. - bool participation_email_sent = 12; + bool participation_email_sent = 12; } message GetGameReq { @@ -1625,7 +1696,7 @@ message GetGameReq { } message GetGameRsp { - Game game = 1; + Game game = 1; } message GetGameAnswersReq { @@ -1669,8 +1740,8 @@ message GetGameResultsRsp { message GameReview { int32 ID = 1; - int32 gameID = 2; - int32 teamID = 3; + int32 gameID = 2; + int32 teamID = 3; string team_name = 4; int32 rating = 5; string text = 6; @@ -1681,11 +1752,11 @@ message GameReview { } message CreateGameReviewReq { - int32 gameID = 1; - int32 teamID = 2; - string password = 3; - int32 rating = 4; - string text = 5; + int32 gameID = 1; + int32 teamID = 2; + string password = 3; + int32 rating = 4; + string text = 5; int32 difficulty = 6; } @@ -1723,7 +1794,7 @@ message UpdateGameReq { string name = 2; string description = 3; google.protobuf.Timestamp start_at = 4; - int32 scenarioID = 5; + int32 scenarioID = 5; string format = 6; string location = 7; } @@ -1774,9 +1845,9 @@ message DeleteGameRsp { } message AddTeamReq { - int32 gameID = 1; - string name = 2; - string captain_email = 3; + int32 gameID = 1; + string name = 2; + string captain_email = 3; int32 participants_count = 4; } diff --git a/src/api/generated/crabs/evening_detective_server/index.ts b/src/api/generated/crabs/evening_detective_server/index.ts index 3efdfce..5406cb5 100644 --- a/src/api/generated/crabs/evening_detective_server/index.ts +++ b/src/api/generated/crabs/evening_detective_server/index.ts @@ -10,7 +10,8 @@ export type ScreenMode = | "SCREEN_MODE_CONCLUSION" | "SCREEN_MODE_CONCLUSION_DONE" | "SCREEN_MODE_RESULTS" - | "SCREEN_MODE_TIMER"; + | "SCREEN_MODE_TIMER" + | "SCREEN_MODE_TEXT"; export type ScreenTimerState = | "SCREEN_TIMER_STATE_UNSPECIFIED" | "SCREEN_TIMER_STATE_STOPPED" @@ -502,6 +503,7 @@ export type ScreenState = { timerDurationSeconds: number | undefined; timerRemainingSeconds: number | undefined; timerState: ScreenTimerState | undefined; + text: string | undefined; }; export type CreateScreenSessionReq = { @@ -612,6 +614,45 @@ export type SendScreenCommandReq = { export type SendScreenCommandRsp = { }; +export type ScreenPlanStep = { + mode: ScreenMode | undefined; + text: string | undefined; + timerLabel: string | undefined; + timerDurationSeconds: number | undefined; +}; + +export type ScreenPlan = { + steps: ScreenPlanStep[] | undefined; + currentStep: number | undefined; +}; + +export type GetScreenPlanReq = { + gameID: number | undefined; +}; + +export type GetScreenPlanRsp = { + plan: ScreenPlan | undefined; +}; + +export type SaveScreenPlanReq = { + gameID: number | undefined; + steps: ScreenPlanStep[] | undefined; + currentStep: number | undefined; +}; + +export type SaveScreenPlanRsp = { + plan: ScreenPlan | undefined; +}; + +export type SetScreenPlanStepReq = { + gameID: number | undefined; + currentStep: number | undefined; +}; + +export type SetScreenPlanStepRsp = { + plan: ScreenPlan | undefined; +}; + export type GetScreenResultsReq = { screenID: string | undefined; accessToken: string | undefined; @@ -1017,6 +1058,9 @@ export interface EveningDetectiveServer { SetScreenTimer(request: SetScreenTimerReq): Promise; // Отправить общую команду экранам игры. SendScreenCommand(request: SendScreenCommandReq): Promise; + GetScreenPlan(request: GetScreenPlanReq): Promise; + SaveScreenPlan(request: SaveScreenPlanReq): Promise; + SetScreenPlanStep(request: SetScreenPlanStepReq): Promise; GetScreenResults(request: GetScreenResultsReq): Promise; DownloadTeamQR(request: GetGameReq): Promise; GetGameAnswers(request: GetGameAnswersReq): Promise; @@ -2167,6 +2211,66 @@ export function createEveningDetectiveServerClient( method: "SendScreenCommand", }) as Promise; }, + GetScreenPlan(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/screen-plan`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetScreenPlan", + }) as Promise; + }, + SaveScreenPlan(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/screen-plan`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "SaveScreenPlan", + }) as Promise; + }, + SetScreenPlanStep(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/screen-plan/step`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "SetScreenPlanStep", + }) as Promise; + }, GetScreenResults(request) { // eslint-disable-line @typescript-eslint/no-unused-vars const path = `api/screens/results`; // eslint-disable-line quotes const body = JSON.stringify(request); diff --git a/src/components/GamePage.vue b/src/components/GamePage.vue index 0d05f68..8103cdc 100644 --- a/src/components/GamePage.vue +++ b/src/components/GamePage.vue @@ -29,6 +29,8 @@ import type { ResultsQuestion, ScreenCommand, ScreenMode, + ScreenPlan, + ScreenPlanStep, ScreenState, Team, TeamAnswers, @@ -106,6 +108,10 @@ const teamsNeedingApplications = computed( ), ) const screens = ref([]) +const screenPlan = ref() +const screenPlanDraft = ref([]) +const screenPlanDirty = ref(false) +const screenPlanSaving = ref(false) const previewQuestions = ref([]) const previewTeams = ref([]) const mapItems = ref([]) @@ -123,6 +129,16 @@ const currentScreenMode = computed(() => { const timerScreen = computed(() => screens.value[0]) const previewScreen = computed(() => screens.value.find((screen) => screen.status === 'connected') || screens.value[0]) const hasDisconnectedScreen = computed(() => screens.value.some((screen) => screen.status === 'disconnected')) +const screenPlanModeOptions = [ + { label: 'Обложка', value: 'SCREEN_MODE_COVER' }, + { label: 'Введение', value: 'SCREEN_MODE_INTRO' }, + { label: 'Успехов', value: 'SCREEN_MODE_INTRO_DONE' }, + { label: 'Закрывашка', value: 'SCREEN_MODE_CONCLUSION' }, + { label: 'Спасибо за игру', value: 'SCREEN_MODE_CONCLUSION_DONE' }, + { label: 'Результаты', value: 'SCREEN_MODE_RESULTS' }, + { label: 'Таймер', value: 'SCREEN_MODE_TIMER' }, + { label: 'Текст', value: 'SCREEN_MODE_TEXT' }, +] const timerStateLabel = computed(() => { switch (timerScreen.value?.timerState) { case 'SCREEN_TIMER_STATE_RUNNING': @@ -196,6 +212,77 @@ async function getGameScreens() { } getGameScreens() +async function getScreenPlan() { + if (screenPlanDirty.value) return + const res = await client.GetScreenPlan({ gameID: Number(gameId) }) + screenPlan.value = res.plan + screenPlanDraft.value = res.plan?.steps?.map((step) => ({ ...step })) || [] +} +getScreenPlan() + +function addScreenPlanStep() { + screenPlanDraft.value.push({ + mode: 'SCREEN_MODE_TEXT', + text: 'Новый экран', + timerLabel: 'Перерыв', + timerDurationSeconds: 600, + }) + screenPlanDirty.value = true +} + +function removeScreenPlanStep(index: number) { + screenPlanDraft.value.splice(index, 1) + screenPlanDirty.value = true +} + +function moveScreenPlanStep(index: number, offset: number) { + const target = index + offset + if (target < 0 || target >= screenPlanDraft.value.length) return + const [step] = screenPlanDraft.value.splice(index, 1) + screenPlanDraft.value.splice(target, 0, step) + screenPlanDirty.value = true +} + +function markScreenPlanDirty() { + screenPlanDirty.value = true +} + +async function saveScreenPlan() { + if (screenPlanSaving.value) return + screenPlanSaving.value = true + try { + const currentStep = Math.min( + screenPlan.value?.currentStep || 0, + Math.max(0, screenPlanDraft.value.length - 1), + ) + const res = await client.SaveScreenPlan({ + gameID: Number(gameId), + steps: screenPlanDraft.value, + currentStep, + }) + screenPlan.value = res.plan + screenPlanDraft.value = res.plan?.steps?.map((step) => ({ ...step })) || [] + screenPlanDirty.value = false + await getGameScreens() + message.success(res.plan ? 'План экранов сохранён' : 'План экранов удалён') + } catch (err) { + message.error(err instanceof Error ? err.message : 'Не удалось сохранить план экранов') + } finally { + screenPlanSaving.value = false + } +} + +async function setScreenPlanStep(index: number) { + if (screenPlanDirty.value || !screenPlan.value) return + try { + const res = await client.SetScreenPlanStep({ gameID: Number(gameId), currentStep: index }) + screenPlan.value = res.plan + await getGameScreens() + } catch (err) { + message.error(err instanceof Error ? err.message : 'Не удалось перейти на шаг плана') + } +} + function stopScreenScanner() { cancelAnimationFrame(screenScannerFrame) screenScannerStream?.getTracks().forEach((track) => track.stop()) @@ -607,6 +694,7 @@ async function issueTickets() { useSimplePolling(() => { getGame(Number(gameId)) getGameScreens() + getScreenPlan() getGameMap() }, 2000) @@ -952,6 +1040,86 @@ useSimplePolling(() => { >Перемотать +
+ +

План экранов

+ + + Создать план + + + Сохранить план + + +
+ +
diff --git a/src/components/ScreenPage.vue b/src/components/ScreenPage.vue index e8a6ca9..8062018 100644 --- a/src/components/ScreenPage.vue +++ b/src/components/ScreenPage.vue @@ -281,6 +281,7 @@ onUnmounted(() => {
Видео закрывашки не задано
Успехов
Спасибо за игру!
+
{{ state.text }}

{{ state.timerLabel || 'Перерыв' }}

{{ timerDisplay }}
diff --git a/src/components/ScreenPreview.vue b/src/components/ScreenPreview.vue index f3bc593..38976b3 100644 --- a/src/components/ScreenPreview.vue +++ b/src/components/ScreenPreview.vue @@ -87,6 +87,7 @@ onUnmounted(() => {
Спасибо за игру!
+
{{ state.text }}

Результаты