thainv-dev: tạo lại cấu trúc folder và làm UI
This commit is contained in:
@@ -1,56 +1,110 @@
|
||||
<script setup lang="ts">
|
||||
import { usePollStore } from '~/stores/poll'
|
||||
import { usePollOptionStore } from '~/stores/poll-option'
|
||||
import type { Poll } from '~/server/models/poll';
|
||||
import { usePollStore } from "~/stores/poll";
|
||||
import { usePollOptionStore } from "~/stores/poll-option";
|
||||
import { usePollResponseStore } from "~/stores/poll-response";
|
||||
import type { Poll } from "~/server/models/poll";
|
||||
import type { PollResponse } from "~/server/models/poll-response";
|
||||
import type { PollOption } from "~/server/models/poll-option";
|
||||
const props = defineProps<{ dataId?: string }>();
|
||||
|
||||
const store = reactive({
|
||||
poll: usePollStore(),
|
||||
pollOptions: usePollOptionStore()
|
||||
})
|
||||
pollOptions: usePollOptionStore(),
|
||||
pollResponse: usePollResponseStore(),
|
||||
});
|
||||
const { currentPoll } = storeToRefs(store.poll);
|
||||
const { currentPollOptions } = storeToRefs(store.pollOptions);
|
||||
const { currentPollResponses } = storeToRefs(store.pollResponse);
|
||||
const poll = reactive<Poll | any>({});
|
||||
const options = ref<PollOption[]>();
|
||||
async function loadData() {
|
||||
await store.poll.fetchById(String(props.dataId));
|
||||
await store.pollOptions.fetchByPollId(String(props.dataId));
|
||||
await store.pollResponse.fetchByPollId(String(props.dataId));
|
||||
assignData();
|
||||
}
|
||||
|
||||
const poll = reactive<Poll>(await store.poll.fetchById((String(props.dataId))))
|
||||
const options = ref<any []>(await store.pollOptions.fetchByPollId((String(props.dataId))))
|
||||
function assignData() {
|
||||
Object.assign(poll, currentPoll.value);
|
||||
options.value = currentPollOptions.value;
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await loadData();
|
||||
});
|
||||
|
||||
const selectedOption = ref<any>(null);
|
||||
const showResult = ref(false);
|
||||
const alreadyVoted = ref(false);
|
||||
const canShowResult = computed(() => {
|
||||
switch (poll.settings?.resultPublication) {
|
||||
case 0:
|
||||
return false;
|
||||
case 1:
|
||||
return true;
|
||||
case 2:
|
||||
return alreadyVoted.value;
|
||||
case 3:
|
||||
return alreadyVoted.value && (!poll.endTime || new Date() > new Date(poll.endTime));
|
||||
}
|
||||
});
|
||||
|
||||
function submitVote () {
|
||||
console.log(selectedOption, 'id')
|
||||
async function submitVote() {
|
||||
if (selectedOption.value) {
|
||||
let option = options.value?.find((option: PollOption) => option.id === selectedOption.value);
|
||||
if(option) {
|
||||
option.responsesCount = Number(option.responsesCount) + 1
|
||||
|
||||
}
|
||||
const totalResponses = options.value?.reduce((sum, option) => sum + Number(option.responsesCount ?? 0), 0);
|
||||
// const result = await store.pollResponse.create({ optionId: selectedOption.value });
|
||||
// if (result) {
|
||||
// alreadyVoted.value = true;
|
||||
// if (options.value) {
|
||||
// let option = options.value.find((option: PollOption) => option.id === selectedOption.value);
|
||||
// // if (option) option?.responsesCount += 1;
|
||||
// // options.value.filter((option) => {
|
||||
// // option.votePercentage = (option.responsesCount / currentPollResponses.value.length) * 100;
|
||||
// // });
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
const toggleResults = () => {
|
||||
if (canShowResult.value) showResult.value = !showResult.value;
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span class="inline-block px-4 py-2 shadow-xl rounded-lg bg-[#f5f5f5]">
|
||||
<span class="inline-block px-4 py-2 shadow-xl rounded-lg bg-[#f5f5f5]">
|
||||
<span class="block">
|
||||
<span class="underline decoration-gray-500 font-bold">
|
||||
{{ poll?.title }}
|
||||
</span>
|
||||
<!-- <button v-if="showResult && canShowResult" type="button" class="underline text-blue-400" @click="toggleResults">
|
||||
Câu hỏi
|
||||
</button>
|
||||
<button class="underline text-blue-400" v-if="!showResult && canShowResult" type="button" @click="toggleResults">
|
||||
Kết quả
|
||||
</button> -->
|
||||
<button v-if="showResult && canShowResult" type="button" class="underline text-blue-400" @click="toggleResults">Câu hỏi</button>
|
||||
<button class="underline text-blue-400" v-if="!showResult && canShowResult" type="button" @click="toggleResults">Kết quả</button>
|
||||
</span>
|
||||
|
||||
<span class="p-1 block">
|
||||
<span v-for="(option, index) in options" :key="index" class="block">
|
||||
<label class="flex gap-2 m-2">
|
||||
<input type="radio" :value="option.id" v-model="selectedOption" />
|
||||
<span class="font-semibold">{{ option?.title }}</span>
|
||||
</label>
|
||||
</span>
|
||||
<button @click="submitVote" class="bg-primary-500 text-white py-1 px-3 rounded-4px cursor-pointer hover:bg-primary-600 float-right">Bình chọn </button>
|
||||
<span v-if="!showResult" class="p-1 block">
|
||||
<span v-for="(option, index) in options" :key="index" class="block">
|
||||
<label class="flex gap-2 m-2">
|
||||
<input type="radio" :value="option.id" v-model="selectedOption" />
|
||||
<span class="font-semibold">{{ option?.title }}</span>
|
||||
</label>
|
||||
</span>
|
||||
<button @click="submitVote" class="bg-primary-500 text-white py-1 px-3 rounded-4px cursor-pointer hover:bg-primary-600 float-right">Bình chọn</button>
|
||||
</span>
|
||||
|
||||
<!-- <span v-else class="block">
|
||||
<span v-for="(answer, index) in options" :key="index" class="block poll-result relative rounded-3xl overflow-hidden my-3">
|
||||
<span v-else class="block">
|
||||
hoàn thành
|
||||
<!-- <span v-for="(answer, index) in options" :key="index" class="block poll-result relative rounded-3xl overflow-hidden my-3">
|
||||
<span class="absolute top-0 start-0 bottom-0 bg-gradient-to-r from-sky-500 to-indigo-500"
|
||||
:style="{ width: `${calculatePercentage(answer?.voteCount)}%` }"></span>
|
||||
<span class="block relative z-0 ps-1">
|
||||
<span>{{ calculatePercentage(answer?.voteCount).toFixed(2) }}%</span>
|
||||
<span class="">({{ answer?.voteCount }})</span>
|
||||
</span>
|
||||
</span>
|
||||
</span> -->
|
||||
</span> -->
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,310 @@
|
||||
<script setup lang="ts">
|
||||
import { useQuizStore } from "~/stores/quiz";
|
||||
import type { Quiz } from "~/server/models/quiz";
|
||||
|
||||
const props = defineProps<{ dataId?: string }>();
|
||||
|
||||
const store = reactive({
|
||||
quiz: useQuizStore(),
|
||||
});
|
||||
|
||||
const { currentQuiz } = storeToRefs(store.quiz);
|
||||
|
||||
const quiz = reactive<Quiz>({});
|
||||
|
||||
async function loadData() {
|
||||
await store.quiz.fetchById(Number(props.dataId));
|
||||
|
||||
assignData();
|
||||
}
|
||||
|
||||
function assignData() {
|
||||
Object.assign(quiz, currentQuiz.value);
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await loadData();
|
||||
});
|
||||
|
||||
const prevQuestion = () => {
|
||||
if (step.value) {
|
||||
step.value--;
|
||||
}
|
||||
};
|
||||
|
||||
const nextQuestion = () => {
|
||||
if (step.value < 3) {
|
||||
step.value++;
|
||||
}
|
||||
};
|
||||
|
||||
const data = {
|
||||
articles: null,
|
||||
questionGeneral: [
|
||||
{
|
||||
answers: [
|
||||
{
|
||||
id: 260,
|
||||
siteId: 1,
|
||||
quizId: 4,
|
||||
questionId: 511,
|
||||
title: "Con ếch 1",
|
||||
thumbnail: "",
|
||||
description: "Con ếch 1",
|
||||
type: 0,
|
||||
isCorrect: true,
|
||||
order: 1,
|
||||
status: 6,
|
||||
createdBy: 3,
|
||||
createdOn: "2024-06-04T15:27:05.641243",
|
||||
updatedBy: null,
|
||||
updatedOn: null,
|
||||
},
|
||||
{
|
||||
id: 259,
|
||||
siteId: 1,
|
||||
quizId: 4,
|
||||
questionId: 511,
|
||||
title: "Con ếch 2",
|
||||
thumbnail: "",
|
||||
description: "Con ếch 2",
|
||||
type: 0,
|
||||
isCorrect: false,
|
||||
order: 1,
|
||||
status: 6,
|
||||
createdBy: 3,
|
||||
createdOn: "2024-06-04T15:27:05.641243",
|
||||
updatedBy: null,
|
||||
updatedOn: null,
|
||||
},
|
||||
{
|
||||
id: 258,
|
||||
siteId: 1,
|
||||
quizId: 4,
|
||||
questionId: 511,
|
||||
title: "Con ếch 3",
|
||||
thumbnail: "",
|
||||
description: "Con ếch 3",
|
||||
type: 0,
|
||||
isCorrect: false,
|
||||
order: 3,
|
||||
status: 6,
|
||||
createdBy: 3,
|
||||
createdOn: "2024-06-04T15:27:05.641243",
|
||||
updatedBy: null,
|
||||
updatedOn: null,
|
||||
},
|
||||
],
|
||||
responses: null,
|
||||
id: 511,
|
||||
siteId: 1,
|
||||
quizId: 4,
|
||||
title: "Con ếch bạn chọn sẽ tiết lộ bí quyết làm giàu",
|
||||
thumbnail: "https://resource.vpress.vn/resources/1/private/13cee27a2bd93915479f049378cffdd3/caudo1-1717486185.jpg",
|
||||
description: "Con ếch bạn chọn sẽ tiết lộ bí quyết làm giàu",
|
||||
type: 1,
|
||||
order: 1,
|
||||
status: 6,
|
||||
createdBy: 3,
|
||||
createdOn: "2024-06-04T15:27:05.641243",
|
||||
updatedBy: null,
|
||||
updatedOn: null,
|
||||
},
|
||||
{
|
||||
answers: [
|
||||
{
|
||||
id: 257,
|
||||
siteId: 1,
|
||||
quizId: 4,
|
||||
questionId: 510,
|
||||
title: "Băng zôn",
|
||||
thumbnail: "",
|
||||
description: "Băng zôn",
|
||||
type: 1,
|
||||
isCorrect: true,
|
||||
order: 1,
|
||||
status: 6,
|
||||
createdBy: 3,
|
||||
createdOn: "2024-06-04T15:27:05.641243",
|
||||
updatedBy: null,
|
||||
updatedOn: null,
|
||||
},
|
||||
{
|
||||
id: 256,
|
||||
siteId: 1,
|
||||
quizId: 4,
|
||||
questionId: 510,
|
||||
title: "Người đàn ông",
|
||||
thumbnail: "",
|
||||
description: "Người đàn ông",
|
||||
type: 1,
|
||||
isCorrect: true,
|
||||
order: 2,
|
||||
status: 6,
|
||||
createdBy: 3,
|
||||
createdOn: "2024-06-04T15:27:05.641243",
|
||||
updatedBy: null,
|
||||
updatedOn: null,
|
||||
},
|
||||
{
|
||||
id: 255,
|
||||
siteId: 1,
|
||||
quizId: 4,
|
||||
questionId: 510,
|
||||
title: "Bánh sinh nhật",
|
||||
thumbnail: "",
|
||||
description: "Bánh sinh nhật",
|
||||
type: 1,
|
||||
isCorrect: false,
|
||||
order: 3,
|
||||
status: 6,
|
||||
createdBy: 3,
|
||||
createdOn: "2024-06-04T15:27:05.641243",
|
||||
updatedBy: null,
|
||||
updatedOn: null,
|
||||
},
|
||||
{
|
||||
id: 254,
|
||||
siteId: 1,
|
||||
quizId: 4,
|
||||
questionId: 510,
|
||||
title: "Khác",
|
||||
thumbnail: "",
|
||||
description: "Khác",
|
||||
type: 2,
|
||||
isCorrect: false,
|
||||
order: 4,
|
||||
status: 6,
|
||||
createdBy: 3,
|
||||
createdOn: "2024-06-04T15:27:05.641243",
|
||||
updatedBy: null,
|
||||
updatedOn: null,
|
||||
},
|
||||
],
|
||||
responses: null,
|
||||
id: 510,
|
||||
siteId: 1,
|
||||
quizId: 4,
|
||||
title: "Những điều khả nghi nào trong bức hình này?",
|
||||
thumbnail: "https://resource.vpress.vn/resources/1/private/13cee27a2bd93915479f049378cffdd3/câu-đố-2-1717486529.jpg",
|
||||
description: "Đâu là điều khả nghi nhất trong bức hình này",
|
||||
type: 2,
|
||||
order: 2,
|
||||
status: 6,
|
||||
createdBy: 3,
|
||||
createdOn: "2024-06-04T15:27:05.641243",
|
||||
updatedBy: null,
|
||||
updatedOn: null,
|
||||
},
|
||||
],
|
||||
responses: null,
|
||||
id: 4,
|
||||
siteId: 1,
|
||||
title: "câu đố tháng 6",
|
||||
code: "cau-do-thang-6",
|
||||
type: 0,
|
||||
startTime: "2024-06-04T15:00:00",
|
||||
endTime: "2024-06-10T00:00:00",
|
||||
settings: {
|
||||
participantType: 3,
|
||||
resultPublication: 1,
|
||||
},
|
||||
features: "Important;Feature",
|
||||
taxonomy: "Biên tập",
|
||||
keywords: "câu đố;tháng 6;e",
|
||||
thumbnail: "https://resource.vpress.vn/resources/1/private/13cee27a2bd93915479f049378cffdd3/ret-20240603042609106.jpg",
|
||||
description: "câu đố tháng 6 e",
|
||||
order: 1,
|
||||
status: 6,
|
||||
createdBy: 3,
|
||||
createdOn: "2024-06-04T14:40:08.617253",
|
||||
updatedBy: 3,
|
||||
updatedOn: "2024-06-04T15:23:59.964931",
|
||||
};
|
||||
|
||||
const step = ref(0);
|
||||
const beforeWidth = computed(() => (100 / Number(data.questionGeneral.length - 1)) * step.value);
|
||||
|
||||
const selectQuizAnswer = ref<any>([]);
|
||||
|
||||
data.questionGeneral.forEach((question) => {
|
||||
switch (question.type) {
|
||||
case 0:
|
||||
selectQuizAnswer.value.push([]);
|
||||
break;
|
||||
case 1:
|
||||
selectQuizAnswer.value.push(0);
|
||||
break;
|
||||
case 2:
|
||||
selectQuizAnswer.value.push([]);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
async function submitSend() {}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
quiz
|
||||
</template>
|
||||
<div class="inline-block px-4 py-2 shadow-xl rounded-lg bg-[#f5f5f5] !text-black">
|
||||
<h5 class="underline decoration-gray-500 font-bold mb-2">Câu đố: {{ data?.title }}</h5>
|
||||
|
||||
<ul class="px-3">
|
||||
<li v-for="(question, questionIndex) in data.questionGeneral" :key="questionIndex" class="mb-2">
|
||||
<h5 class="mb-1 font-700 text-black">{{ `${questionIndex + 1}. ${question.title}` }}</h5>
|
||||
|
||||
<ul>
|
||||
<li v-for="(answer, answerIndex) in question.answers" :key="answerIndex" class="flex items-center gap-1 py-1">
|
||||
<input :id="`answer-${questionIndex}-${answerIndex}`" :type="question.type === 1 ? 'radio' : 'checkbox'" :value="answerIndex" v-model="selectQuizAnswer[questionIndex]" />
|
||||
<label :for="`answer-${questionIndex}-${answerIndex}`" class="font-semibold">{{ answer.title }}</label>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button @click="submitSend" class="bg-primary-500 text-white py-1 px-3 rounded-4px cursor-pointer hover:bg-primary-600 float-right">Gửi câu trả lời</button>
|
||||
</div>
|
||||
<!-- <div>
|
||||
<h5 class="text-black text-18px font-700">{{ data?.title }}</h5>
|
||||
<template v-if="data.questionGeneral.length > 1">
|
||||
<ul
|
||||
:style="{ '--before-width': beforeWidth + '%' }"
|
||||
class="progress flex items-center justify-between relative after:content-[''] after:absolute after:top-50% after:translate-y--50% after:w-full after:h-1 after:bg-gray-200 before:content-[''] before:absolute before:top-50% before:translate-y--50% before:h-1 before:bg-primary-500 before:z-2 before:transition-all before:ease-linear before:duration-300"
|
||||
>
|
||||
<li
|
||||
v-for="(index, item) in data.questionGeneral.length"
|
||||
:key="index"
|
||||
:class="step >= index - 1 ? 'bg-primary-500 text-white transition-all delay-300' : 'bg-white text-primary-500'"
|
||||
class="relative z-3 w-7 h-7 rounded-full flex items-center justify-center border-2 border-solid border-primary-500"
|
||||
>
|
||||
<template template v-if="step > index - 1"><Icon name="material-symbols:check-rounded" class="text-22px" /></template>
|
||||
<template v-else>{{ item }}</template>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<template v-for="(item, index) in data.questionGeneral" :key="index">
|
||||
<div v-show="step === index">
|
||||
{{ item.title }} => {{ index }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div>
|
||||
<button class="bg-primary-500 text-white px-2 py-2 rounded-4px" @click="prevQuestion()">Câu trước</button>
|
||||
<button class="bg-primary-500 text-white px-2 py-2 rounded-4px" @click="nextQuestion()">Câu tiếp theo</button>
|
||||
</div>
|
||||
</div> -->
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:root {
|
||||
--before-width: 0%;
|
||||
}
|
||||
|
||||
.progress {
|
||||
&::before {
|
||||
width: var(--before-width);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,299 @@
|
||||
<script setup lang="ts">
|
||||
import { useSurveyStore } from "~/stores/survey";
|
||||
import type { Survey } from "~/server/models/survey";
|
||||
|
||||
const props = defineProps<{ dataId?: string }>();
|
||||
|
||||
const store = reactive({
|
||||
survey: useSurveyStore(),
|
||||
});
|
||||
|
||||
const { currentSurvey } = storeToRefs(store.survey);
|
||||
|
||||
const survey = reactive<Survey>({});
|
||||
|
||||
async function loadData() {
|
||||
await store.survey.fetchById(Number(props.dataId));
|
||||
|
||||
assignData();
|
||||
}
|
||||
|
||||
function assignData() {
|
||||
Object.assign(survey, currentSurvey.value);
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
await loadData();
|
||||
});
|
||||
|
||||
const dataSurvey = {
|
||||
"articles": null,
|
||||
"questionGeneral": [
|
||||
{
|
||||
"answers": [
|
||||
{
|
||||
"id": 85,
|
||||
"siteId": 1,
|
||||
"surveyId": 10,
|
||||
"questionId": 84,
|
||||
"title": "Không",
|
||||
"thumbnail": "",
|
||||
"description": "",
|
||||
"type": 1,
|
||||
"isCorrect": false,
|
||||
"order": 2,
|
||||
"status": 6,
|
||||
"createdBy": 3,
|
||||
"createdOn": "2024-06-04T17:13:45.794056",
|
||||
"updatedBy": null,
|
||||
"updatedOn": null
|
||||
},
|
||||
{
|
||||
"id": 84,
|
||||
"siteId": 1,
|
||||
"surveyId": 10,
|
||||
"questionId": 84,
|
||||
"title": "Có",
|
||||
"thumbnail": "",
|
||||
"description": "",
|
||||
"type": 1,
|
||||
"isCorrect": true,
|
||||
"order": 1,
|
||||
"status": 6,
|
||||
"createdBy": 3,
|
||||
"createdOn": "2024-06-04T17:13:45.794056",
|
||||
"updatedBy": null,
|
||||
"updatedOn": null
|
||||
}
|
||||
],
|
||||
"responses": null,
|
||||
"id": 84,
|
||||
"siteId": 1,
|
||||
"surveyId": 10,
|
||||
"title": "Bạn có chọn xe công nghệ để di chuyển trong giờ cao điểm không?",
|
||||
"thumbnail": "",
|
||||
"description": "",
|
||||
"type": 0,
|
||||
"order": 3,
|
||||
"status": 6,
|
||||
"createdBy": 3,
|
||||
"createdOn": "2024-06-04T17:13:45.794056",
|
||||
"updatedBy": null,
|
||||
"updatedOn": null
|
||||
},
|
||||
{
|
||||
"answers": [
|
||||
{
|
||||
"id": 83,
|
||||
"siteId": 1,
|
||||
"surveyId": 10,
|
||||
"questionId": 83,
|
||||
"title": "Xe bus",
|
||||
"thumbnail": "",
|
||||
"description": "",
|
||||
"type": 1,
|
||||
"isCorrect": false,
|
||||
"order": 3,
|
||||
"status": 6,
|
||||
"createdBy": 3,
|
||||
"createdOn": "2024-06-04T17:13:45.794056",
|
||||
"updatedBy": null,
|
||||
"updatedOn": null
|
||||
},
|
||||
{
|
||||
"id": 82,
|
||||
"siteId": 1,
|
||||
"surveyId": 10,
|
||||
"questionId": 83,
|
||||
"title": "Xe đạp",
|
||||
"thumbnail": "",
|
||||
"description": "",
|
||||
"type": 1,
|
||||
"isCorrect": false,
|
||||
"order": 2,
|
||||
"status": 6,
|
||||
"createdBy": 3,
|
||||
"createdOn": "2024-06-04T17:13:45.794056",
|
||||
"updatedBy": null,
|
||||
"updatedOn": null
|
||||
},
|
||||
{
|
||||
"id": 81,
|
||||
"siteId": 1,
|
||||
"surveyId": 10,
|
||||
"questionId": 83,
|
||||
"title": "Xe máy",
|
||||
"thumbnail": "",
|
||||
"description": "",
|
||||
"type": 1,
|
||||
"isCorrect": true,
|
||||
"order": 1,
|
||||
"status": 6,
|
||||
"createdBy": 3,
|
||||
"createdOn": "2024-06-04T17:13:45.794056",
|
||||
"updatedBy": null,
|
||||
"updatedOn": null
|
||||
}
|
||||
],
|
||||
"responses": null,
|
||||
"id": 83,
|
||||
"siteId": 1,
|
||||
"surveyId": 10,
|
||||
"title": "Bạn thường di chuyển bằng phương tiện gì?",
|
||||
"thumbnail": "",
|
||||
"description": "",
|
||||
"type": 1,
|
||||
"order": 2,
|
||||
"status": 6,
|
||||
"createdBy": 3,
|
||||
"createdOn": "2024-06-04T17:13:45.794056",
|
||||
"updatedBy": null,
|
||||
"updatedOn": null
|
||||
},
|
||||
{
|
||||
"answers": [
|
||||
{
|
||||
"id": 80,
|
||||
"siteId": 1,
|
||||
"surveyId": 10,
|
||||
"questionId": 82,
|
||||
"title": "21 lần trở lên",
|
||||
"thumbnail": "",
|
||||
"description": "",
|
||||
"type": 1,
|
||||
"isCorrect": false,
|
||||
"order": 3,
|
||||
"status": 6,
|
||||
"createdBy": 3,
|
||||
"createdOn": "2024-06-04T17:13:45.794056",
|
||||
"updatedBy": null,
|
||||
"updatedOn": null
|
||||
},
|
||||
{
|
||||
"id": 79,
|
||||
"siteId": 1,
|
||||
"surveyId": 10,
|
||||
"questionId": 82,
|
||||
"title": "14 - 21 lần",
|
||||
"thumbnail": "",
|
||||
"description": "",
|
||||
"type": 1,
|
||||
"isCorrect": false,
|
||||
"order": 0,
|
||||
"status": 6,
|
||||
"createdBy": 3,
|
||||
"createdOn": "2024-06-04T17:13:45.794056",
|
||||
"updatedBy": null,
|
||||
"updatedOn": null
|
||||
},
|
||||
{
|
||||
"id": 78,
|
||||
"siteId": 1,
|
||||
"surveyId": 10,
|
||||
"questionId": 82,
|
||||
"title": "7 lần",
|
||||
"thumbnail": "",
|
||||
"description": "",
|
||||
"type": 1,
|
||||
"isCorrect": true,
|
||||
"order": 1,
|
||||
"status": 6,
|
||||
"createdBy": 3,
|
||||
"createdOn": "2024-06-04T17:13:45.794056",
|
||||
"updatedBy": null,
|
||||
"updatedOn": null
|
||||
}
|
||||
],
|
||||
"responses": null,
|
||||
"id": 82,
|
||||
"siteId": 1,
|
||||
"surveyId": 10,
|
||||
"title": "Mỗi tuần bạn di chuyển với tần suất bao nhiêu lần?",
|
||||
"thumbnail": "",
|
||||
"description": "Mỗi tuần bạn di chuyển với tần suất bao nhiêu lần?",
|
||||
"type": 1,
|
||||
"order": 1,
|
||||
"status": 6,
|
||||
"createdBy": 3,
|
||||
"createdOn": "2024-06-04T17:13:45.794056",
|
||||
"updatedBy": null,
|
||||
"updatedOn": null
|
||||
}
|
||||
],
|
||||
"responses": null,
|
||||
"id": 10,
|
||||
"siteId": 1,
|
||||
"title": "Thói quen di chuyển trong giờ cao điểm",
|
||||
"code": "thoi-quen-di-chuyen-trong-gio-cao-diem",
|
||||
"type": 0,
|
||||
"startTime": "2024-06-04T17:18:00",
|
||||
"endTime": "2024-06-20T00:00:00",
|
||||
"settings": {
|
||||
"participantType": 3,
|
||||
"resultPublication": 2
|
||||
},
|
||||
"features": "Feature",
|
||||
"taxonomy": "Biên tập",
|
||||
"keywords": "thoiquendichuyen;giocaodiem",
|
||||
"thumbnail": "https://resource.vpress.vn/resources/1/private/13cee27a2bd93915479f049378cffdd3/thoiquendichuyentronggiocaodiem-20240604100659862.png",
|
||||
"description": "Thói quen di chuyển trong giờ cao điểm",
|
||||
"order": 1,
|
||||
"status": 6,
|
||||
"createdBy": 3,
|
||||
"createdOn": "2024-06-04T17:13:45.653177",
|
||||
"updatedBy": null,
|
||||
"updatedOn": null
|
||||
};
|
||||
|
||||
|
||||
const selectSurveyAnswer = ref<any>([])
|
||||
|
||||
dataSurvey.questionGeneral.forEach((question) => {
|
||||
switch (question.type) {
|
||||
case 0:
|
||||
selectSurveyAnswer.value.push([])
|
||||
break;
|
||||
case 1:
|
||||
selectSurveyAnswer.value.push(0)
|
||||
break;
|
||||
case 2:
|
||||
selectSurveyAnswer.value.push([])
|
||||
break;
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
async function submitSend() {
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
Survey
|
||||
</template>
|
||||
|
||||
<div class="inline-block px-4 py-2 shadow-xl rounded-lg bg-[#f5f5f5] !text-black">
|
||||
<h5 class="underline decoration-gray-500 font-bold mb-2">Khảo sát: {{ dataSurvey?.title }}</h5>
|
||||
|
||||
<ul class="px-3">
|
||||
<li v-for="(question, questionIndex) in dataSurvey.questionGeneral" :key="questionIndex" class="mb-2">
|
||||
<h5 class="mb-1 font-700 text-black">{{ `${questionIndex + 1}. ${question.title}` }}</h5>
|
||||
|
||||
<ul>
|
||||
<li v-for="(answer, answerIndex) in question.answers" :key="answerIndex" class="flex items-center gap-1 py-1">
|
||||
<input :id="`answer-survey-${questionIndex}-${answerIndex}`" :type="question.type === 1 ? 'radio' : 'checkbox'" :value="answerIndex" v-model="selectSurveyAnswer[questionIndex]">
|
||||
<label :for="`answer-survey-${questionIndex}-${answerIndex}`" class="font-semibold">{{ answer.title }}</label>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button @click="submitSend" class="bg-primary-500 text-white py-1 px-3 rounded-4px cursor-pointer hover:bg-primary-600 float-right">Gửi câu trả lời</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:root {
|
||||
--before-width: 0%;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default as ADS_Default } from './layouts/Default.vue';
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||
import { ADS_Default
|
||||
} from "./index";
|
||||
const _props = defineProps<{
|
||||
settings: any;
|
||||
component?: any;
|
||||
}>();
|
||||
|
||||
const definedDynamicComponent: Record<string, any> = {
|
||||
'DEFAULT': ADS_Default,
|
||||
};
|
||||
|
||||
const getCurrentComponent = computed(() => `${_props.settings.layout}`);
|
||||
const GET_PROPS = computed(() => {
|
||||
return () => {
|
||||
let props: any = {};
|
||||
if (_props.settings) {
|
||||
for (const [key, value] of Object.entries(_props.settings)) {
|
||||
props = {
|
||||
...props,
|
||||
[key]: value,
|
||||
};
|
||||
}
|
||||
return props;
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="definedDynamicComponent[getCurrentComponent]" v-bind="GET_PROPS()" />
|
||||
</template>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import ADSDefault from '@/assets/images/ads.jpg'
|
||||
</script>
|
||||
<template>
|
||||
<div class="shadow">
|
||||
<img :src="ADSDefault" alt="quảng cáo" class=" object-cover">
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.content {
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
export { default as Article_Card } from './layouts/Card.vue'
|
||||
export { default as Article_Detail_General } from './layouts/details/General.vue'
|
||||
export { default as Article_Detail_Podcast } from './layouts/details/Podcast.vue'
|
||||
export { default as Article_Detail_Video } from './layouts/details/Video.vue'
|
||||
export { default as Article_Detail_Image } from './layouts/details/Image.vue'
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts" setup>
|
||||
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||
import {
|
||||
Article_Card,
|
||||
Article_Detail_General,
|
||||
Article_Detail_Podcast,
|
||||
Article_Detail_Video,
|
||||
Article_Detail_Image
|
||||
} from "./index";
|
||||
const _props = defineProps<{
|
||||
settings: any;
|
||||
component?: any;
|
||||
}>();
|
||||
|
||||
const definedDynamicComponent: Record<string, any> = {
|
||||
'TYPE:Detail-LAYOUT:default': Article_Detail_General,
|
||||
'TYPE:Detail-LAYOUT:image': Article_Detail_Image,
|
||||
'TYPE:Detail-LAYOUT:video': Article_Detail_Video,
|
||||
'TYPE:Detail-LAYOUT:podcast': Article_Detail_Podcast,
|
||||
};
|
||||
|
||||
const getCurrentComponent = computed(() => `${_props.settings.layout}`);
|
||||
const GET_PROPS = computed(() => {
|
||||
return () => {
|
||||
let props: any = {};
|
||||
if (_props.settings) {
|
||||
for (const [key, value] of Object.entries(_props.settings)) {
|
||||
props = {
|
||||
...props,
|
||||
[key]: value,
|
||||
};
|
||||
}
|
||||
return props;
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="definedDynamicComponent[getCurrentComponent]" v-bind="GET_PROPS()" />
|
||||
</template>
|
||||
@@ -0,0 +1,98 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
|
||||
const props = defineProps<{
|
||||
dataResult?: any;
|
||||
dataType?: any;
|
||||
dataQuery?: any;
|
||||
layout?: string;
|
||||
design?: string;
|
||||
}>();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
vào r</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.basic-article {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
|
||||
&.no-data {
|
||||
gap: 5px !important;
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
&.border-custom {
|
||||
border-color: #e5e5e5 !important;
|
||||
}
|
||||
&.borderLeft {
|
||||
border-left: 2px solid;
|
||||
padding-left: 10px;
|
||||
}
|
||||
&.borderRight {
|
||||
border-right: 2px solid;
|
||||
padding-right: 10px;
|
||||
}
|
||||
&.borderTop {
|
||||
border-top: 2px solid;
|
||||
padding-top: 10px;
|
||||
}
|
||||
&.borderBottom {
|
||||
border-bottom: 2px solid;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
&.horizontal {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
&.reverse {
|
||||
.basic-article_thumbnail {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.basic-article_content {
|
||||
grid-row: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&_thumbnail {
|
||||
flex: 1;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
aspect-ratio: 16/10;
|
||||
}
|
||||
}
|
||||
|
||||
&_content {
|
||||
padding: 10px 0px;
|
||||
|
||||
&.no-data {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-block {
|
||||
background-color: #409eff;
|
||||
height: 100px;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
<script setup lang="ts">
|
||||
import { useArticleStore } from "~/stores/articles";
|
||||
import Poll from "~/components/article/immerse/Poll.vue";
|
||||
import Quiz from "~/components/article/immerse/Quiz.vue";
|
||||
import Survey from "~/components/article/immerse/Survey.vue";
|
||||
import Document from "~/components/article/immerse/Document.vue";
|
||||
import Attachment from "@/components/article/immerse/Attachment.vue";
|
||||
import Tag from "@/components/article/immerse/Tag.vue";
|
||||
const { currentArticle } = storeToRefs(useArticleStore());
|
||||
import { useDynamicPageStore } from "~/stores/dynamic-page";
|
||||
import { useCategoryStore } from "~/stores/category";
|
||||
|
||||
const store = reactive({
|
||||
dynamicPage: useDynamicPageStore(),
|
||||
article: useArticleStore(),
|
||||
category: useCategoryStore(),
|
||||
});
|
||||
|
||||
const { categoryTree } = storeToRefs(store.category);
|
||||
|
||||
|
||||
function increase() {
|
||||
const step = ref(Number(getComputedStyle(document.documentElement).getPropertyValue("--step").trim()));
|
||||
step.value += 2;
|
||||
document.documentElement.style.setProperty("--step", step.value.toString());
|
||||
}
|
||||
|
||||
function decrease() {
|
||||
const step = ref(Number(getComputedStyle(document.documentElement).getPropertyValue("--step").trim()));
|
||||
step.value -= 2;
|
||||
document.documentElement.style.setProperty("--step", step.value.toString());
|
||||
}
|
||||
await store.category.fetchBySiteId();
|
||||
const currentCategoryTree = (store.category.currentCategoryTree = findElementPathById(categoryTree.value, currentArticle.value.categoryId));
|
||||
function findElementPathById(categories: any[], targetId: number, path: any[] = []) {
|
||||
for (const category of categories) {
|
||||
const currentPath = [...path, { title: category.title, code: category.code }];
|
||||
if (category.id === targetId) {
|
||||
return currentPath;
|
||||
}
|
||||
if (category.children) {
|
||||
const result: any = findElementPathById(category.children, targetId, currentPath);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
clickElement("figure", "custom-figure", "data-code");
|
||||
clickElement("author", "author", "data-code");
|
||||
|
||||
let detailEmagazine = document.querySelector('div[layout="ARTICLE_DETAIL_EMAGAZINE"]');
|
||||
let breakcrumb = document.querySelector('div[layout="BREADCRUM_DEFAULT"]');
|
||||
if (detailEmagazine && breakcrumb) {
|
||||
breakcrumb.classList.add("lg:max-w-640px", "mx-auto");
|
||||
}
|
||||
});
|
||||
|
||||
function clickElement(type: string, selector: string, attribute: string) {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
elements.forEach((element) => {
|
||||
element.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
const url = `${window.location.protocol}//${window.location.host}/${type}/${element.getAttribute(attribute)}`;
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const isBookmark = ref(false)
|
||||
const onClickBookmark = () => {
|
||||
isBookmark.value = !isBookmark.value
|
||||
}
|
||||
async function copyLink() {
|
||||
try {
|
||||
const url = window.location.href
|
||||
await navigator.clipboard.writeText(url)
|
||||
alert('copy link thành công')
|
||||
|
||||
} catch (error) {
|
||||
alert(error)
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div id="breakcrumb" class="flex justify-between items-center my-3">
|
||||
<ul class="flex gap-6 items-center text-md">
|
||||
<template v-for="(category, index) in currentCategoryTree" :key="index">
|
||||
<li class="first:text-primary-600 hover:text-primary-600 font-semibold relative after:absolute after:content-['\003E'] last:after:content-[''] after:right--4 after:text-gray-3">
|
||||
<nuxt-link :to="{ name: 'categories', params: { categories: category.code ?? '/' } }">{{ category.title }}</nuxt-link>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<div @click="increase()" class="w-10 h-10 border-1px border-solid shadow rounded-full relative cursor-pointer hover:bg-primary-100 hover:text-primary-600">
|
||||
<svg class="absolute top-50% left-50% translate-y--50% translate-x--55%" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
||||
<path fill="none" d="M0 0h24v24H0z"></path>
|
||||
<path
|
||||
d="M11.246 15H4.75416L2.75416 20H0.600098L7.0001 4H9.0001L15.4001 20H13.246L11.246 15ZM10.446 13L8.0001 6.88516L5.55416 13H10.446ZM21.0001 12.5351V12H23.0001V20H21.0001V19.4649C20.4118 19.8052 19.7287 20 19.0001 20C16.791 20 15.0001 18.2091 15.0001 16C15.0001 13.7909 16.791 12 19.0001 12C19.7287 12 20.4118 12.1948 21.0001 12.5351ZM19.0001 18C20.1047 18 21.0001 17.1046 21.0001 16C21.0001 14.8954 20.1047 14 19.0001 14C17.8955 14 17.0001 14.8954 17.0001 16C17.0001 17.1046 17.8955 18 19.0001 18Z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg class="absolute right-1.5 top-2 w-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M11 11V5H13V11H19V13H13V19H11V13H5V11H11Z"></path></svg>
|
||||
</div>
|
||||
<div @click="decrease()" class="w-10 h-10 border-1px border-solid shadow rounded-full relative cursor-pointer hover:bg-primary-100 hover:text-primary-600">
|
||||
<svg class="absolute top-50% left-50% translate-y--50% translate-x--55%" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
||||
<path fill="none" d="M0 0h24v24H0z"></path>
|
||||
<path
|
||||
d="M11.246 15H4.75416L2.75416 20H0.600098L7.0001 4H9.0001L15.4001 20H13.246L11.246 15ZM10.446 13L8.0001 6.88516L5.55416 13H10.446ZM21.0001 12.5351V12H23.0001V20H21.0001V19.4649C20.4118 19.8052 19.7287 20 19.0001 20C16.791 20 15.0001 18.2091 15.0001 16C15.0001 13.7909 16.791 12 19.0001 12C19.7287 12 20.4118 12.1948 21.0001 12.5351ZM19.0001 18C20.1047 18 21.0001 17.1046 21.0001 16C21.0001 14.8954 20.1047 14 19.0001 14C17.8955 14 17.0001 14.8954 17.0001 16C17.0001 17.1046 17.8955 18 19.0001 18Z"
|
||||
></path>
|
||||
</svg>
|
||||
<svg class="absolute right-1.5 top-2 w-3" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M5 11V13H19V11H5Z"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content" v-if="currentArticle">
|
||||
<h1 id="sub" v-html="currentArticle?.sub" class="font-bold opacity-60 pb-1"></h1>
|
||||
<h3 id="title" class="font-bold pb-1" v-html="currentArticle?.title"></h3>
|
||||
<p id="published-on" class="text-gray-600 mb-3">{{ utils.dateFormat(currentArticle?.publishedOn, "dddd, DD/MM/YYYY - HH:mm") }}</p>
|
||||
<div id="intro" v-if="currentArticle?.intro" v-html="currentArticle?.intro" class="font-semibold tracking-widest pb-1 mb-3"></div>
|
||||
<component :is="{ template: currentArticle.detail, components: { Poll, Quiz, Survey, Document, Attachment, Tag } }" />
|
||||
</div>
|
||||
|
||||
<div class="py-5 flex flex-wrap justify-between items-center">
|
||||
<div class="flex gap-4 items-center">
|
||||
<nuxt-link :to="{ name: 'categories', params: { categories: `${currentCategoryTree[currentCategoryTree.length - 1]?.code}` } }" title="Quay trở lại" class="w-12 h-3rem rounded-full flex items-center justify-center bg-white border-1px shadow hover:bg-primary-100 hover:text-primary-600 cursor-pointer">
|
||||
<Icon name="fa6-solid:arrow-left" />
|
||||
</nuxt-link>
|
||||
<button @click="onClickBookmark()" class="w-8 h-8 rounded-full bg-white hover:bg-primary-100 hover:text-primary-600">
|
||||
<Icon v-show="isBookmark === false" name="fa6-regular:bookmark" />
|
||||
<Icon v-show="isBookmark === true" name="fa6-solid:bookmark" class="text-primary-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-center">
|
||||
<button @click="copyLink()" title="Copy link" class="w-12 h-3rem rounded-full flex items-center justify-center bg-white border-1px shadow hover:bg-primary-100 hover:text-primary-600 cursor-pointer text-2xl">
|
||||
<Icon name="bi:link-45deg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#sub,
|
||||
#intro,
|
||||
#intro + div {
|
||||
font-size: calc(16px + var(--step) * 1px);
|
||||
}
|
||||
|
||||
#title {
|
||||
font-size: calc(28px + var(--step) * 1px);
|
||||
}
|
||||
|
||||
#published-on {
|
||||
font-size: calc(14px + var(--step) * 1px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
import { useArticleStore } from "~/stores/articles";
|
||||
import Poll from "~/components/article/immerse/Poll.vue";
|
||||
import Quiz from "~/components/article/immerse/Quiz.vue";
|
||||
import Survey from "~/components/article/immerse/Survey.vue";
|
||||
import Document from "~/components/article/immerse/Document.vue";
|
||||
import Attachment from "@/components/article/immerse/Attachment.vue";
|
||||
import Tag from "@/components/article/immerse/Tag.vue";
|
||||
const { currentArticle } = storeToRefs(useArticleStore());
|
||||
import { useDynamicPageStore } from "~/stores/dynamic-page";
|
||||
import { useCategoryStore } from "~/stores/category";
|
||||
|
||||
const store = reactive({
|
||||
dynamicPage: useDynamicPageStore(),
|
||||
article: useArticleStore(),
|
||||
category: useCategoryStore(),
|
||||
});
|
||||
|
||||
const { categoryTree } = storeToRefs(store.category);
|
||||
|
||||
await store.category.fetchBySiteId();
|
||||
const currentCategoryTree = (store.category.currentCategoryTree = findElementPathById(categoryTree.value, currentArticle.value.categoryId));
|
||||
function findElementPathById(categories: any[], targetId: number, path: any[] = []) {
|
||||
for (const category of categories) {
|
||||
const currentPath = [...path, { title: category.title, code: category.code }];
|
||||
if (category.id === targetId) {
|
||||
return currentPath;
|
||||
}
|
||||
if (category.children) {
|
||||
const result: any = findElementPathById(category.children, targetId, currentPath);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
clickElement("figure", "custom-figure", "data-code");
|
||||
clickElement("author", "author", "data-code");
|
||||
|
||||
let detailEmagazine = document.querySelector('div[layout="ARTICLE_DETAIL_EMAGAZINE"]');
|
||||
let breakcrumb = document.querySelector('div[layout="BREADCRUM_DEFAULT"]');
|
||||
if (detailEmagazine && breakcrumb) {
|
||||
breakcrumb.classList.add("lg:max-w-640px", "mx-auto");
|
||||
}
|
||||
});
|
||||
|
||||
function clickElement(type: string, selector: string, attribute: string) {
|
||||
const elements = document.querySelectorAll(selector);
|
||||
elements.forEach((element) => {
|
||||
element.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
const url = `${window.location.protocol}//${window.location.host}/${type}/${element.getAttribute(attribute)}`;
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const isBookmark = ref(false);
|
||||
const onClickBookmark = () => {
|
||||
isBookmark.value = !isBookmark.value;
|
||||
};
|
||||
async function copyLink() {
|
||||
try {
|
||||
const url = window.location.href;
|
||||
await navigator.clipboard.writeText(url);
|
||||
alert("copy link thành công");
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(currentArticle, "curetna");
|
||||
</script>
|
||||
<template>
|
||||
<div class="content" v-if="currentArticle">
|
||||
<div id="breakcrumb" class="flex justify-between items-center my-3">
|
||||
<ul class="flex gap-6 items-center text-md">
|
||||
<template v-for="(category, index) in currentCategoryTree" :key="index">
|
||||
<li class="font-semibold relative after:absolute after:content-['\003E'] last:after:content-[''] after:right--4 after:text-gray-3">
|
||||
<nuxt-link :class="index !== 0 ? '!text-black' : ''" class="hover:!text-primary-600" :to="{ name: 'categories', params: { categories: category.code ?? '/' } }">{{ category.title }}</nuxt-link>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<span id="published-on" class="text-gray-600 mb-3">{{ utils.dateFormat(currentArticle?.publishedOn, "dddd, DD/MM/YYYY - HH:mm") }}</span>
|
||||
</div>
|
||||
<h1 id="sub" v-html="currentArticle?.sub" class="font-bold opacity-60 pb-1"></h1>
|
||||
<h3 id="title" class="font-bold pb-1" v-html="currentArticle?.title"></h3>
|
||||
|
||||
<div id="intro" v-if="currentArticle?.intro" v-html="currentArticle?.intro" class="font-semibold tracking-widest pb-1 mb-3"></div>
|
||||
<component :is="{ template: currentArticle.detail, components: { Poll, Quiz, Survey, Document, Attachment, Tag } }" />
|
||||
|
||||
<div id="navigation__bottom" class="py-5 flex flex-wrap justify-between items-center">
|
||||
<div class="flex gap-4 items-center">
|
||||
<nuxt-link
|
||||
:to="{ name: 'categories', params: { categories: `${currentCategoryTree[currentCategoryTree.length - 1]?.code}` } }"
|
||||
title="Quay trở lại"
|
||||
class="w-10 h-10 rounded-8px flex items-center justify-center bg-white border-1px shadow hover:bg-primary-100 hover:text-primary-600 cursor-pointer"
|
||||
>
|
||||
<Icon name="fa6-solid:arrow-left" />
|
||||
</nuxt-link>
|
||||
<button @click="onClickBookmark()" class="w-10 h-10 rounded-8px border-1px bg-white hover:bg-primary-100 hover:text-primary-600">
|
||||
<Icon v-show="isBookmark === false" name="fa6-regular:bookmark" />
|
||||
<Icon v-show="isBookmark === true" name="fa6-solid:bookmark" class="text-primary-600" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 items-center">
|
||||
<button @click="copyLink()" title="Copy link" class="w-10 h-10 rounded-8px flex items-center justify-center bg-white border-1px shadow hover:bg-primary-100 hover:text-primary-600 cursor-pointer text-2xl">
|
||||
<Icon name="bi:link-45deg" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
#sub,
|
||||
#intro,
|
||||
#intro + div {
|
||||
font-size: calc(16px + var(--step) * 1px);
|
||||
}
|
||||
|
||||
#title {
|
||||
font-size: calc(28px + var(--step) * 1px);
|
||||
}
|
||||
|
||||
#published-on {
|
||||
font-size: calc(14px + var(--step) * 1px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,10 @@
|
||||
<script setup lang="ts"></script>
|
||||
<template>
|
||||
<div>
|
||||
postcart</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
div {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script setup lang="ts">
|
||||
import Comment from "@/components/dynamic-page/page-component/templates/other/comments/default.vue";
|
||||
import { useArticleStore } from "~/stores/articles";
|
||||
import Poll from "~/components/article/immerse/Poll.vue";
|
||||
import Quiz from "~/components/article/immerse/Quiz.vue";
|
||||
import Survey from "~/components/article/immerse/Survey.vue";
|
||||
import Document from "~/components/article/immerse/Document.vue";
|
||||
import Attachment from "@/components/article/immerse/Attachment.vue";
|
||||
import Tag from "@/components/article/immerse/Tag.vue";
|
||||
const { currentArticle } = storeToRefs(useArticleStore());
|
||||
import { useDynamicPageStore } from "~/stores/dynamic-page";
|
||||
import { useCategoryStore } from "~/stores/category";
|
||||
|
||||
const store = reactive({
|
||||
dynamicPage: useDynamicPageStore(),
|
||||
article: useArticleStore(),
|
||||
category: useCategoryStore(),
|
||||
});
|
||||
|
||||
console.log(currentArticle.value ,'curenta')
|
||||
</script>
|
||||
<template>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3">
|
||||
<div class="md:col-span-2">
|
||||
<video class="w-full h-full" controls="controls" width="100%" height="100%" data-file-id="149" data-resource="https://acp-api.vpress.vn/Resources/Video/983d2f57-7743-472f-b22d-fc73085af6d5.mp4" data-title="Download.mp4">
|
||||
<source src="" type="video/mp4" />
|
||||
</video>
|
||||
</div>
|
||||
|
||||
<div class="px-4 pt-2 bg-[rgb(248,249,250)]">
|
||||
<h1 class="text-2rem text-#495057 font-700" v-html="currentArticle.title"></h1>
|
||||
<p class="line-clamp-3 font-400" v-html="currentArticle.intro"></p>
|
||||
<h5 class="text-end font-600 opacity-75">Tác giả</h5>
|
||||
|
||||
<p><b class="text-primary-600 fw-bold">Danh mục</b> <span class="ms-2 opacity-25 fw-semibold">{{ utils.dateFormat(currentArticle?.publishedOn, "dddd, DD/MM/YYYY - HH:mm") }}</span></p>
|
||||
<Comment />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped lang="scss">
|
||||
.line-clamp-3 {
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Collection_Article } from './layouts/Article.vue'
|
||||
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
Collection_Article
|
||||
} from "./index";
|
||||
const _props = defineProps<{
|
||||
settings: any;
|
||||
component?: any;
|
||||
}>();
|
||||
|
||||
const definedDynamicComponent: Record<string, any> = {
|
||||
'TYPE:Article-LAYOUT:vertical-DATA:HORIZONTAL': Collection_Article,
|
||||
};
|
||||
|
||||
const getCurrentComponent = computed(() => `${_props.settings.layout}`);
|
||||
const GET_PROPS = computed(() => {
|
||||
return () => {
|
||||
let props: any = {};
|
||||
if (_props.settings) {
|
||||
for (const [key, value] of Object.entries(_props.settings)) {
|
||||
props = {
|
||||
...props,
|
||||
[key]: value,
|
||||
};
|
||||
}
|
||||
return props;
|
||||
}
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component :is="definedDynamicComponent[getCurrentComponent]" v-bind="GET_PROPS()" />
|
||||
</template>
|
||||
@@ -0,0 +1,6 @@
|
||||
<script setup lang="ts"></script>
|
||||
<template>
|
||||
<div>
|
||||
collection
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1,6 @@
|
||||
// Article
|
||||
export { default as Article_BasicCard } from './articles/individuals/Card.vue'
|
||||
|
||||
export { default as Article_BasicCollection } from './articles/collections/BasicCollection.vue'
|
||||
|
||||
// Category
|
||||
@@ -7,4 +8,8 @@ export { default as BasicCategories } from './categories/BasicCategories.vue'
|
||||
export { default as CollectionPaging } from './pageCategories/collection_page.vue'
|
||||
|
||||
|
||||
export { default as Dynamic_Other } from './other/index.vue'
|
||||
export { default as Dynamic_Other } from './other/index.vue'
|
||||
|
||||
export { default as Dynamic_Advertising } from './advertising/index.vue'
|
||||
export { default as Dynamic_Article } from './articles/index.vue'
|
||||
export { default as Dynamic_Collection } from './collections/index.vue'
|
||||
@@ -1,22 +1,23 @@
|
||||
<script lang="ts" setup>
|
||||
import { enumPageComponentTemplates } from "@/definitions/enum";
|
||||
import { Article_BasicCard, BasicCategories, Article_BasicCollection, CollectionPaging, Dynamic_Other } from "./index";
|
||||
|
||||
import { BasicCategories, Dynamic_Collection, CollectionPaging, Dynamic_Other, Dynamic_Advertising, Dynamic_Article } from "./index";
|
||||
const _props = defineProps<{
|
||||
settings: any;
|
||||
component?: any;
|
||||
}>();
|
||||
|
||||
const definedDynamicComponent: Record<string, any> = {
|
||||
[enumPageComponentTemplates.ARTICLE]: Article_BasicCard,
|
||||
[enumPageComponentTemplates.ARTICLE]: Dynamic_Article,
|
||||
[enumPageComponentTemplates.CATEGORY]: BasicCategories,
|
||||
[enumPageComponentTemplates.COLLECTION]: Article_BasicCollection,
|
||||
[enumPageComponentTemplates.COLLECTION]: Dynamic_Collection,
|
||||
[enumPageComponentTemplates.SECTION]: CollectionPaging,
|
||||
[enumPageComponentTemplates.OTHER]: Dynamic_Other
|
||||
[enumPageComponentTemplates.OTHER]: Dynamic_Other,
|
||||
[enumPageComponentTemplates.ADVERTISING]: Dynamic_Advertising
|
||||
};
|
||||
|
||||
const getCurrentComponent = computed(() => `${_props.settings.template}`);
|
||||
|
||||
|
||||
const GET_PROPS = computed(() => {
|
||||
return () => {
|
||||
let props: any = {};
|
||||
|
||||
@@ -1,38 +1,73 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import { useDynamicPageStore } from "~/stores/dynamic-page";
|
||||
import { useCategoryStore } from "~/stores/category";
|
||||
import { useArticleStore } from "~/stores/articles";
|
||||
|
||||
const store = reactive({
|
||||
dynamicPage: useDynamicPageStore(),
|
||||
article: useArticleStore(),
|
||||
category: useCategoryStore()
|
||||
})
|
||||
|
||||
const { currentArticle } = storeToRefs(store.article)
|
||||
const { categoryTree } = storeToRefs(store.category)
|
||||
if(!localStorage.getItem('step')) {
|
||||
localStorage.setItem('step', '0')
|
||||
}
|
||||
useDynamicPageStore().step = Number(localStorage.getItem('step')) ?? 0
|
||||
function increase() {
|
||||
useDynamicPageStore().step = Number(useDynamicPageStore().step) + 2
|
||||
localStorage.setItem('step', useDynamicPageStore().step.toString())
|
||||
|
||||
const step = ref(Number(getComputedStyle(document.documentElement).getPropertyValue('--step').trim()))
|
||||
step.value += 2
|
||||
document.documentElement.style.setProperty('--step', step.value.toString());
|
||||
|
||||
}
|
||||
|
||||
function decrease() {
|
||||
useDynamicPageStore().step = Number(useDynamicPageStore().step) - 2
|
||||
localStorage.setItem('step', useDynamicPageStore().step.toString())
|
||||
const step = ref(Number(getComputedStyle(document.documentElement).getPropertyValue('--step').trim()))
|
||||
step.value -= 2
|
||||
document.documentElement.style.setProperty('--step', step.value.toString());
|
||||
}
|
||||
|
||||
function findElementPathById(categories: any[], targetId: number, path = []) {
|
||||
for (const category of categories) {
|
||||
const currentPath = [...path, { title: category.title, code: category.code }];
|
||||
if (category.id === targetId) {
|
||||
return currentPath;
|
||||
}
|
||||
if (category.children) {
|
||||
const result: any = findElementPathById(category.children, targetId, currentPath);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
await store.category.fetchBySiteId()
|
||||
console.log(store.category.categoryTree, 'tree')
|
||||
if(!categoryTree.value?.length) {
|
||||
}
|
||||
|
||||
const currentCategoryTree = store.category.currentCategoryTree = findElementPathById(categoryTree.value, currentArticle.value.categoryId)
|
||||
onMounted(() => {
|
||||
let detailEmagazine = document.querySelector('div[layout="ARTICLE_DETAIL_EMAGAZINE"]')
|
||||
let breakcrumb = document.querySelector('div[layout="BREADCRUM_DEFAULT"]')
|
||||
if( detailEmagazine && breakcrumb) {
|
||||
breakcrumb.classList.add('lg:max-w-640px','mx-auto')
|
||||
}
|
||||
|
||||
console.log('b')
|
||||
})
|
||||
|
||||
</script>
|
||||
<template>
|
||||
|
||||
<div class="flex justify-between items-center my-3">
|
||||
<ul class="flex gap-6 items-center text-md">
|
||||
<li class="first:text-primary-600 font-semibold">
|
||||
<nuxt-link to="/">Trang chủ</nuxt-link>
|
||||
</li>
|
||||
<template v-for="(category, index) in currentCategoryTree" :key="index">
|
||||
<li class="first:text-primary-600 hover:text-primary-600 font-semibold relative after:absolute after:content-['\003E'] last:after:content-[''] after:right--4 after:text-gray-3">
|
||||
<nuxt-link :to="{ name: 'categories', params: { categories: category.code ?? '/' } }">{{ category.title }}</nuxt-link>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<div class="flex gap-2">
|
||||
@@ -47,12 +82,9 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.empty {
|
||||
border-radius: 6px;
|
||||
background: #409eff;
|
||||
width: 40px;
|
||||
min-height: 20px;
|
||||
<style lang="scss">
|
||||
:root {
|
||||
--step: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useCategoryStore } from '@/stores/category';
|
||||
const store = {
|
||||
category: useCategoryStore()
|
||||
}
|
||||
|
||||
const { currentCategoryTree } = storeToRefs(store.category)
|
||||
const isBookmark = ref(false)
|
||||
const onClickBookmark = () => {
|
||||
isBookmark.value = !isBookmark.value
|
||||
@@ -14,11 +20,12 @@ async function copyLink() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<div class="py-5 flex flex-wrap justify-between items-center">
|
||||
<div class="flex gap-4 items-center">
|
||||
<nuxt-link to="/" title="Quay trở lại" class="w-12 h-3rem rounded-full flex items-center justify-center bg-white border-1px shadow hover:bg-primary-100 hover:text-primary-600 cursor-pointer">
|
||||
<nuxt-link :to="{ name: 'categories', params: { categories: `${currentCategoryTree[currentCategoryTree.length - 1]?.code}` } }" title="Quay trở lại" class="w-12 h-3rem rounded-full flex items-center justify-center bg-white border-1px shadow hover:bg-primary-100 hover:text-primary-600 cursor-pointer">
|
||||
<Icon name="fa6-solid:arrow-left" />
|
||||
</nuxt-link>
|
||||
<button @click="onClickBookmark()" class="w-8 h-8 rounded-full bg-white hover:bg-primary-100 hover:text-primary-600">
|
||||
|
||||
@@ -11,17 +11,57 @@ import Document from '~/components/article/immerse/Document.vue'
|
||||
import Attachment from '@/components/article/immerse/Attachment.vue'
|
||||
import Tag from '@/components/article/immerse/Tag.vue'
|
||||
import Articlerelation from '~/components/article/immerse/ArticleRelation.vue'
|
||||
|
||||
const { currentArticle } = storeToRefs(useArticleStore());
|
||||
const { step } = storeToRefs(useDynamicPageStore());
|
||||
import * as cherrio from 'cheerio'
|
||||
|
||||
const $ = cherrio.load(currentArticle.value.detail)
|
||||
// console.log($, 'cherrip')
|
||||
const router = useRouter()
|
||||
// import * as cherrio from 'cheerio'
|
||||
|
||||
// const $ = cherrio.load(currentArticle.value.detail)
|
||||
// for(let index = 0; index < $('articlerelation').length, index++) {
|
||||
// $('articlerelation')[index]
|
||||
// }
|
||||
// console.log($('articlerelation').length, 'cherrip')
|
||||
// onBeforeMount(async () => {
|
||||
// await useArticleStore().getArticleCondition({ids: [1, 2, 3]})
|
||||
// })
|
||||
// console.log(router,'route')
|
||||
onMounted(() => {
|
||||
// const elements = document.querySelectorAll('custom-figure')
|
||||
// elements.forEach((element) => {
|
||||
// element.addEventListener('click', (event) => {
|
||||
// event.preventDefault();
|
||||
// console.log(element, 'element')
|
||||
// const url = `figure/${element.getAttribute('data-code')}`;
|
||||
|
||||
// const a = document.createElement('a')
|
||||
// a.href = url;
|
||||
// document.body.appendChild(a)
|
||||
// a.click();
|
||||
// document.body.removeChild(a);
|
||||
// })
|
||||
// })
|
||||
|
||||
clickElement('figure', 'custom-figure', 'data-code')
|
||||
clickElement('author', 'author', 'data-code')
|
||||
})
|
||||
|
||||
function clickElement(type: string, selector: string, attribute: string) {
|
||||
const elements = document.querySelectorAll(selector)
|
||||
elements.forEach((element) => {
|
||||
element.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
const url = `${window.location.protocol}//${window.location.host}/${type}/${element.getAttribute(attribute)}`;
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = url;
|
||||
document.body.appendChild(a)
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
})
|
||||
})
|
||||
}
|
||||
// const fileName = ref('')
|
||||
// onMounted(() => {
|
||||
// const documentElements = document.querySelectorAll('document, attachment')
|
||||
|
||||
@@ -50,15 +90,24 @@ const $ = cherrio.load(currentArticle.value.detail)
|
||||
</script>
|
||||
<template>
|
||||
<div class="content" v-if="currentArticle">
|
||||
<h1 id="sub" v-html="currentArticle?.sub" class=" font-bold opacity-60 pb-1" :style="{ 'font-size': `${16 + Number(step)}px`}"></h1>
|
||||
<h3 id="title" :style="{ 'font-size': width > breakpoint.lg ? `${32 + Number(step)}px` : `${20 + Number(step)}px`}" class="font-bold pb-1" v-html="currentArticle?.title"></h3>
|
||||
<p id="published-on" class="text-gray-600 mb-3" :style="{ 'font-size': `${14 + Number(step)}px` }">{{ utils.dateFormat(currentArticle?.publishedOn, "dddd, DD/MM/YYYY - HH:mm") }}</p>
|
||||
<div id="intro" v-if="currentArticle?.intro" v-html="currentArticle?.intro" class="font-semibold tracking-widest pb-1 mb-3" :style="{'font-size': `${16 + Number(step)}px`}"></div>
|
||||
<!-- <div id="article-detail" :class="'tracking-wider'" v-html="currentArticle.detail" class="[&_img]:mx-auto" :style="{ 'font-size': `${16 + Number(step)}px`}"> </div> -->
|
||||
<component :is="{template: currentArticle.detail, components: { Poll, Document, Attachment, Tag} }" />
|
||||
<!-- <component :is="{template: currentArticle.detail, components: { Poll, Quiz, Survey, Document, Attachment, Tag} }" /> -->
|
||||
<h1 id="sub" v-html="currentArticle?.sub" class=" font-bold opacity-60 pb-1"></h1>
|
||||
<h3 id="title" class="font-bold pb-1" v-html="currentArticle?.title"></h3>
|
||||
<p id="published-on" class="text-gray-600 mb-3">{{ utils.dateFormat(currentArticle?.publishedOn, "dddd, DD/MM/YYYY - HH:mm") }}</p>
|
||||
<div id="intro" v-if="currentArticle?.intro" v-html="currentArticle?.intro" class="font-semibold tracking-widest pb-1 mb-3"></div>
|
||||
<component :is="{template: currentArticle.detail, components: { Poll, Quiz, Survey, Document, Attachment, Tag} }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
#sub, #intro, #intro + div {
|
||||
font-size: calc(16px + var(--step) * 1px);
|
||||
}
|
||||
|
||||
#title {
|
||||
font-size: calc(28px + var(--step) * 1px);
|
||||
}
|
||||
|
||||
#published-on {
|
||||
font-size: calc(14px + var(--step) * 1px);
|
||||
}
|
||||
</style>
|
||||
@@ -15,7 +15,7 @@ const definedDynamicComponent: Record<string, any> = {
|
||||
'ARTICLE_DETAIL_EMAGAZINE': Article_Detail_Emagazine,
|
||||
'ADS_DEFAULT': ADS_Default,
|
||||
'ARTICLE_BUTTON': Article_Button,
|
||||
COMMENT: Comment,
|
||||
'COMMENT_DEFAULT': Comment,
|
||||
PODCAST: Article_Detail_Podcast,
|
||||
VIDEO: Article_Detail_Video
|
||||
};
|
||||
|
||||
@@ -37,7 +37,7 @@ const CLASS_FOR_LAYOUT = computed(() => {
|
||||
|
||||
<template>
|
||||
<div :class="[CLASS_FOR_LAYOUT.page_container]">
|
||||
<div :class="[CLASS_FOR_LAYOUT.layout_container]" class="grid-container grid grid-cols-1 gap-20 py-20 style_layout">
|
||||
<div :class="[CLASS_FOR_LAYOUT.layout_container]" class="grid-container grid grid-cols-1 gap-20 py-10 style_layout">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<template>
|
||||
<div class="page_container full-size-page">
|
||||
<div class="layout_container center-layout grid-container">
|
||||
<div class="container-long my-5">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user