add catalog

This commit is contained in:
2026-07-26 03:02:05 +07:00
parent 4663493c66
commit 6d5766092b
14 changed files with 338 additions and 5 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -5,8 +5,8 @@
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Вечерний детектив</title>
<script type="module" crossorigin src="/assets/index-D4LqqFgY.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BiYEczz-.css">
<script type="module" crossorigin src="/assets/index-Cv18iwiG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BuOna_tK.css">
</head>
<body>
<div id="app"></div>
+27
View File
@@ -219,6 +219,26 @@ service EveningDetectiveServer {
};
}
rpc GetScenariosCatalog(GetScenariosCatalogReq) returns (GetScenariosCatalogRsp) {
option (google.api.http) = {
get: "/api/catalog"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Сценарии";
summary: "Получить каталог сценариев";
};
}
rpc GetFullScenario(GetScenarioReq) returns (GetScenarioRsp) {
option (google.api.http) = {
get: "/api/scenarios/{id}/full"
};
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
tags : "Сценарии";
summary: "Получить сценарий со всеми деталями по id";
};
}
rpc GetScenario(GetScenarioReq) returns (GetScenarioRsp) {
option (google.api.http) = {
get: "/api/scenarios/{id}"
@@ -442,6 +462,13 @@ message GetMyScenariosRsp {
repeated Scenario scenarios = 2;
}
message GetScenariosCatalogReq {}
message GetScenariosCatalogRsp {
string error = 1;
repeated Scenario scenarios = 2;
}
message GetScenarioReq {
int32 id = 1;
}
@@ -193,6 +193,14 @@ export type Key = {
name: string | undefined;
};
export type GetScenariosCatalogReq = {
};
export type GetScenariosCatalogRsp = {
error: string | undefined;
scenarios: Scenario[] | undefined;
};
export type GetScenarioReq = {
id: number | undefined;
};
@@ -282,6 +290,8 @@ export interface EveningDetectiveServer {
DownloadFile(request: DownloadFileReq): Promise<googleapi_HttpBody>;
AddScenario(request: AddScenarioReq): Promise<AddScenarioRsp>;
GetMyScenarios(request: GetMyScenariosReq): Promise<GetMyScenariosRsp>;
GetScenariosCatalog(request: GetScenariosCatalogReq): Promise<GetScenariosCatalogRsp>;
GetFullScenario(request: GetScenarioReq): Promise<GetScenarioRsp>;
GetScenario(request: GetScenarioReq): Promise<GetScenarioRsp>;
UpdateScenario(request: UpdateScenarioReq): Promise<UpdateScenarioRsp>;
PublicScenario(request: PublicScenarioReq): Promise<PublicScenarioRsp>;
@@ -591,6 +601,43 @@ export function createEveningDetectiveServerClient(
method: "GetMyScenarios",
}) as Promise<GetMyScenariosRsp>;
},
GetScenariosCatalog(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
const path = `api/catalog`; // 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: "GetScenariosCatalog",
}) as Promise<GetScenariosCatalogRsp>;
},
GetFullScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
}
const path = `api/scenarios/${request.id}/full`; // 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: "GetFullScenario",
}) as Promise<GetScenarioRsp>;
},
GetScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars
if (!request.id) {
throw new Error("missing required field request.id");
+8
View File
@@ -37,6 +37,14 @@ body {
height: 80vh;
}
.center-block-custom {
margin-top: 70px;
}
.center-block-custom-big {
margin-top: 70px;
}
@media (width >=1024px) {
.center-block-custom {
width: 700px;
+138
View File
@@ -0,0 +1,138 @@
<script setup lang="ts">
import { NTag } from 'naive-ui'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { getAuthClient } from '@/api/auth_client'
import type { Scenario } from '@/api/generated/crabs/evening_detective_server'
import HeaderMenu from '@/components/HeaderMenu.vue'
const client = getAuthClient()
const scenarios = ref<Scenario[]>([])
const router = useRouter()
async function getScenarios() {
scenarios.value = []
const res = await client.GetScenariosCatalog({})
scenarios.value = res.scenarios!
}
async function toScenarioInfo(id: number) {
router.push('/scenarios/' + id)
}
getScenarios()
</script>
<template>
<div class="center-block-custom-big content-block">
<div class="scenarios-container">
<div class="scenario-block" v-for="scenario in scenarios" @click="toScenarioInfo(scenario.id!)"
v-bind:key="scenario.id">
<div class="scenario-image-block scenario-image" :style="{ backgroundImage: `url(${scenario.image})` }">
</div>
<div class="scenario-content-block">
<p class="scenario-title">{{ scenario.name }}</p>
<p class="scenario-description">{{ scenario.description }}</p>
<p class="scenario-label">
<n-tag :bordered="false" type="success" class="status-block">Автор: {{ scenario.author?.username
}}</n-tag>
</p>
</div>
</div>
</div>
</div>
<HeaderMenu :add-logout="true" active="catalog" />
</template>
<style scoped>
.scenarios-container {
display: flex;
flex-flow: row wrap;
justify-content: center;
overflow-y: auto;
padding: 0;
height: calc(100vh - 150px);
/* border: 1px solid red; */
}
.scenario-block {
position: relative;
margin: 10px;
width: 160px;
height: 300px;
cursor: pointer;
border-radius: 12px;
/* background-color: #553333; */
background-color: #333;
border: 2px solid #333;
}
.scenario-content-block {
padding: 2px 10px;
}
.scenario-block:hover {
color: #63e2b7;
border: 2px solid #63e2b7;
}
.scenario-image-block {
background-color: #111;
height: 160px;
border-radius: 10px;
}
.scenario-image {
background-size: cover;
background-position: center;
}
.scenario-title {
padding: 5px 0;
}
.scenario-description {
color: #aaa;
display: none;
}
.status-block {
position: absolute;
bottom: 15px;
right: 15px;
}
@media (width >=1024px) {
.scenarios-container {
height: calc(100vh - 70px);
}
.scenario-block {
width: 250px;
height: 400px;
}
.scenario-image-block {
height: 250px;
}
.scenario-content-block {
padding: 4px 10px;
}
.scenario-description {
color: #aaa;
display: block;
}
}
</style>
+4
View File
@@ -70,6 +70,10 @@ const openMenu = ref(false)
@click="router.push('/games')">
Игры
</div>
<div class="menu-item-block" :class="{ active: props.active == 'catalog' }" v-if="hasPermission('catalog_page')"
@click="router.push('/catalog')">
Каталог
</div>
<div class="menu-item-block" :class="{ active: props.active == 'scenarios' }"
v-if="hasPermission('scenarios_page')" @click="router.push('/scenarios')">
Мои сценарии
+78
View File
@@ -0,0 +1,78 @@
<script setup lang="ts">
import { getAuthClient } from '@/api/auth_client'
import type { Scenario } from '@/api/generated/crabs/evening_detective_server'
import HeaderMenu from '@/components/HeaderMenu.vue'
import { useMessage } from 'naive-ui'
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { NTag } from 'naive-ui'
const message = useMessage()
const client = getAuthClient()
const route = useRoute()
const scenarioId = route.params.id
const scenario = ref<Scenario>({
id: undefined,
name: undefined,
story: undefined,
author: undefined,
status: undefined,
updatedAt: undefined,
createdAt: undefined,
publishedAt: undefined,
})
async function getScenario(id: number) {
const res = await client.GetScenario({ id: id })
if (res.error != '') {
message.error(res.error!)
return
}
scenario.value = res.scenario!
}
getScenario(Number(scenarioId))
</script>
<template>
<div class="center-block-custom">
<div class="scenario-container">
<div class="scenario-title">
{{ scenario.name }}
</div>
<img class="scenario-image" :src="scenario.image">
<div class="scenario-description">
{{ scenario.description }}
</div>
<div class="scenario-author">
<n-tag :bordered="false" type="success" class="status-block">Автор: {{ scenario.author?.username
}}</n-tag>
</div>
</div>
</div>
<HeaderMenu :add-logout="true" active="catalog" />
</template>
<style scoped>
.scenario-container {
padding: 20px;
}
.scenario-title {
font-size: 24px;
margin-bottom: 10px;
}
.scenario-image {
width: 100%;
max-width: 400px;
margin-bottom: 10px;
}
.scenario-description {
color: #aaa;
margin-bottom: 10px;
}
</style>
+2 -1
View File
@@ -29,13 +29,14 @@ const scenario = ref<Scenario>({
name: undefined,
story: undefined,
author: undefined,
status: undefined,
updatedAt: undefined,
createdAt: undefined,
publishedAt: undefined,
})
async function getScenario(id: number) {
const res = await client.GetScenario({ id: id })
const res = await client.GetFullScenario({ id: id })
if (res.error != '') {
message.error(res.error!)
return
+12
View File
@@ -9,6 +9,8 @@ import ScenariosEditorView from '../views/ScenariosEditorView.vue'
import ScenariosView from '../views/ScenariosView.vue'
import UserAgreementView from '../views/UserAgreementView.vue'
import UsersView from '../views/UsersView.vue'
import CatalogView from '../views/CatalogView.vue'
import ScenarioView from '../views/ScenarioView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -48,11 +50,21 @@ const router = createRouter({
name: 'scenarios',
component: ScenariosView,
},
{
path: '/catalog',
name: 'CatalogView',
component: CatalogView,
},
{
path: '/scenarios/:id/editor', // Динамический параметр id
name: 'ScenariosEditorView',
component: ScenariosEditorView,
},
{
path: '/scenarios/:id', // Динамический параметр id
name: 'ScenarioView',
component: ScenarioView,
},
{
path: '/users',
name: 'users',
+9
View File
@@ -0,0 +1,9 @@
<script setup lang="ts">
import CatalogPage from '@/components/CatalogPage.vue'
</script>
<template>
<CatalogPage />
</template>
<style lang="css"></style>
+9
View File
@@ -0,0 +1,9 @@
<script setup lang="ts">
import ScenarioPage from '@/components/ScenarioPage.vue'
</script>
<template>
<ScenarioPage />
</template>
<style lang="css"></style>