Initial commit

This commit is contained in:
2026-01-31 19:51:16 +10:00
commit 836665f1ed
23 changed files with 984 additions and 0 deletions

39
.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo
.eslintcache
# Cypress
/cypress/videos/
/cypress/screenshots/
# Vitest
__screenshots__/
test-results/
playwright-report/

6
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,6 @@
{
"recommendations": [
"Vue.volar",
"ms-playwright.playwright"
]
}

57
README.md Normal file
View File

@@ -0,0 +1,57 @@
# test
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```
### Run End-to-End Tests with [Playwright](https://playwright.dev)
```sh
# Install browsers for the first run
npx playwright install
# When testing on CI, must build the project first
npm run build
# Runs the end-to-end tests
npm run test:e2e
# Runs the tests only on Chromium
npm run test:e2e -- --project=chromium
# Runs the tests of a specific file
npm run test:e2e -- tests/example.spec.ts
# Runs the tests in debug mode
npm run test:e2e -- --debug
```

8
e2e/vue.spec.js Normal file
View File

@@ -0,0 +1,8 @@
import { test, expect } from '@playwright/test';
// See here how to get started:
// https://playwright.dev/docs/intro
test('visits the app root url', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toHaveText('You did it!');
})

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/src/assets/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
jsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

0
package-lock.json generated Normal file
View File

27
package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "test",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test:e2e": "playwright test"
},
"dependencies": {
"axios": "^1.13.2",
"tailwindcss": "^4.1.18",
"vue": "^3.5.25"
},
"devDependencies": {
"@playwright/test": "^1.57.0",
"@tailwindcss/vite": "^4.1.18",
"@vitejs/plugin-vue": "^6.0.2",
"vite": "^7.3.0",
"vite-plugin-vue-devtools": "^8.0.5"
}
}

110
playwright.config.js Normal file
View File

@@ -0,0 +1,110 @@
import process from 'node:process'
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './e2e',
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: process.env.CI ? 'http://localhost:4173' : 'http://localhost:5173',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
/* Only on CI systems run the tests headless */
headless: !!process.env.CI,
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
},
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox'],
},
},
{
name: 'webkit',
use: {
...devices['Desktop Safari'],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
webServer: {
/**
* Use the dev server by default for faster feedback loop.
* Use the preview server on CI for more realistic testing.
* Playwright will re-use the local server if there is already a dev-server running.
*/
command: process.env.CI ? 'npm run preview' : 'npm run dev',
port: process.env.CI ? 4173 : 5173,
reuseExistingServer: !process.env.CI,
},
})

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

59
src/App.vue Normal file
View File

@@ -0,0 +1,59 @@
<script setup>
import { onMounted, ref, watch } from "vue";
import My_naw from "./components/My_naw.vue";
import Section from "./components/Section.vue";
import Setings from "./components/Setings.vue";
import Authe from "./components/Authe.vue";
import axios from "axios";
// Состояния страницы и данные для входа
const currentPage = ref("admin-panel");
const items = ref([]);
// Универсальная функция для получения данных
const fetchData = async (url, targetRef) => {
try {
const { data } = await axios.get(url)
targetRef.value = data
} catch (err) {
console.error(`Ошибка при получении данных с ${url}:`, err)
}
}
// Обертки для вызова
const loadItems = () => fetchData("http://45.129.78.228:8002/records_all", items) //http://127.0.0.1:8002/records_all http://45.129.78.228:8002/records_all
onMounted(() => {
loadItems()
})
watch(items, loadItems)
function handleUpdate(newValue) {
currentPage.value = newValue; // изменение значения в родителе
}
</script>
<template>
<Authe :currentPage="currentPage" @update="handleUpdate"/>
<div v-if="currentPage === 'rezylt'">
<div class="sm:flex">
<My_naw :currentPage="currentPage" @update="handleUpdate"/>
<Section :items="items"/>
</div>
</div>
<div v-if="currentPage === 'setings'">
<div class="sm:flex">
<My_naw :currentPage="currentPage" @update="handleUpdate"/>
<Setings/>
</div>
</div>
</template>
<style scoped>
/* Добавьте стили по необходимости */
</style>

BIN
src/assets/href.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

1
src/assets/logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

16
src/assets/main.css Normal file
View File

@@ -0,0 +1,16 @@
@import "tailwindcss";
/* Для темной темы */
@media (prefers-color-scheme: dark) {
body {
background: #111;
/* другие стили для темной темы */
}
}
/* Для светлой темы (по умолчанию или явно) */
@media (prefers-color-scheme: light) {
body {
background: #ccc; /* или любой другой цвет */
}
}

92
src/components/Authe.vue Normal file
View File

@@ -0,0 +1,92 @@
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import axios from "axios";
// Получение props
const props = defineProps({
currentPage: String,
});
// Объявление события для обновления страницы
const emit = defineEmits(["update"]);
const login = ref("");
const password = ref("");
const authError = ref(false);
// Функция авторизации
const auth_my = async () => {
try {
const response = await axios.post("http://45.129.78.228:8004/login", {
username: login.value,
password: password.value,
});
if (response.data.message === "Login successful") {
authError.value = false;
emit("update", "rezylt");
} else {
authError.value = true;
}
} catch (err) {
authError.value = true;
console.log(err);
}
};
// Обработка глобального нажатия Enter
const handleKeyDown = (event) => {
if (event.key === "Enter") {
auth_my();
}
};
// Добавляем глобальный слушатель при монтировании
onMounted(() => {
window.addEventListener("keydown", handleKeyDown);
});
// Удаляем при размонтировании
onUnmounted(() => {
window.removeEventListener("keydown", handleKeyDown);
});
// Переход на стартовую страницу (сброс данных)
function showStartPage() {
emit("update", "admin-panel");
login.value = "";
password.value = "";
authError.value = false;
}
</script>
<template>
<div
v-if="props.currentPage === 'admin-panel'"
class="p-4 bg-white rounded shadow max-w-md mx-auto mt-10 dark:bg-gray-800 dark:text-neutral-300"
>
<h2>Вход для администратора</h2>
<div class="mb-2">
<label>Логин:</label>
<input v-model="login" type="text" class="border p-1 w-full" />
</div>
<div class="mb-2">
<label>Пароль:</label>
<input v-model="password" type="password" class="border p-1 w-full" />
</div>
<button
@click="auth_my"
class="mr-2 bg-teal-600 hover:bg-teal-800 text-white px-4 py-2 rounded cursor-pointer"
>
Войти
</button>
<button
@click="showStartPage"
class="dark:bg-gray-600 dark:hover:bg-gray-700 bg-gray-500 hover:bg-gray-600 px-4 py-2 rounded cursor-pointer"
>
Отмена
</button>
<div v-if="authError" class="mt-2 text-red-600">
Неверный логин или пароль
</div>
</div>
</template>

View File

@@ -0,0 +1,48 @@
<template>
<div>
<button class="rounded-md bg-white/10 px-2.5 py-1.5 text-sm font-semibold text-white inset-ring inset-ring-white/5 hover:bg-white/20" @click="open = true">Open drawer</button>
<TransitionRoot as="template" :show="open">
<Dialog class="relative z-10" @close="open = false">
<TransitionChild as="template" enter="ease-in-out duration-500" enter-from="opacity-0" enter-to="" leave="ease-in-out duration-500" leave-from="" leave-to="opacity-0">
<div class="fixed inset-0 bg-gray-900/50 transition-opacity"></div>
</TransitionChild>
<div class="fixed inset-0 overflow-hidden">
<div class="absolute inset-0 overflow-hidden">
<div class="pointer-events-none fixed inset-y-0 right-0 flex max-w-full pl-10 sm:pl-16">
<TransitionChild as="template" enter="transform transition ease-in-out duration-500 sm:duration-700" enter-from="translate-x-full" enter-to="translate-x-0" leave="transform transition ease-in-out duration-500 sm:duration-700" leave-from="translate-x-0" leave-to="translate-x-full">
<DialogPanel class="pointer-events-auto relative w-screen max-w-md">
<TransitionChild as="template" enter="ease-in-out duration-500" enter-from="opacity-0" enter-to="" leave="ease-in-out duration-500" leave-from="" leave-to="opacity-0">
<div class="absolute top-0 left-0 -ml-8 flex pt-4 pr-2 sm:-ml-10 sm:pr-4">
<button type="button" class="relative rounded-md text-gray-400 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500" @click="open = false">
<span class="absolute -inset-2.5"></span>
<span class="sr-only">Close panel</span>
<XMarkIcon class="size-6" aria-hidden="true" />
</button>
</div>
</TransitionChild>
<div class="relative flex h-full flex-col overflow-y-auto bg-gray-800 py-6 shadow-xl after:absolute after:inset-y-0 after:left-0 after:w-px after:bg-white/10">
<div class="px-4 sm:px-6">
<DialogTitle class="text-base font-semibold text-white">Panel title</DialogTitle>
</div>
<div class="relative mt-6 flex-1 px-4 sm:px-6">
<!-- Your content -->
</div>
</div>
</DialogPanel>
</TransitionChild>
</div>
</div>
</div>
</Dialog>
</TransitionRoot>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
import { XMarkIcon } from '@heroicons/vue/24/outline'
const open = ref(true)
</script>

53
src/components/My_naw.vue Normal file
View File

@@ -0,0 +1,53 @@
<template>
<div class=" sticky top-0 w-full sm:w-1/5 sm:h-screen bg-white z-10 pt-5 md:p-5 dark:bg-gray-800">
<!-- Фиксированный левый блок -->
<div v-if="currentPage === 'rezylt'" class="flex sm:flex-col">
<button
@click="ValueRezylt"
class="bg-gray-600 w-full text-white min-w-30 px-4 py-2 rounded cursor-pointer"
>
Результат
</button>
<button
@click="ValueSeting"
class="bg-gray-300 sm:mt-3 w-full min-w-30 hover:text-white hover:bg-gray-600 px-4 py-2 rounded cursor-pointer"
>
Настройка
</button>
</div>
<div v-if="currentPage === 'setings'" class="flex sm:flex-col">
<button
@click="ValueRezylt"
class="bg-gray-300 w-full min-w-30 hover:text-white hover:bg-gray-600 px-4 py-2 rounded cursor-pointer"
>
Результат
</button>
<button
@click="ValueSeting"
class="bg-gray-600 sm:mt-3 w-full text-white min-w-30 px-4 py-2 rounded cursor-pointer"
>
Настройка
</button>
</div>
</div>
</template>
<script setup>
const props = defineProps({
currentPage: String,
});
// Макросы Vue 3.3+ не требуют явного импорта
const emit = defineEmits(["update"]);
function ValueSeting() {
emit("update", "setings");
}
function ValueRezylt() {
emit("update", "rezylt");
}
</script>

View File

@@ -0,0 +1,35 @@
<!-- AdminLogin.vue -->
<template>
<div class="w-full sm:w-4/5 dark:text-neutral-300">
<div class="bg-white flex justify-between p-5 dark:bg-gray-800 ">
<h1>Результат:</h1>
<Time />
</div>
<div class="p-4">
<Stat
v-for="item in items"
:translation_text="item.translation_text"
:original_text="item.original_text"
:url="item.url"
:title="item.title"
:article_date="item.article_date"
:short_text="item.short_text"
:category="item.category"
:parsed_at="item.parsed_at"
:status = "item.status"
:viewed = "item.viewed"
:other = "item.other"
/>
</div>
</div>
</template>
<script setup>
import Stat from "./Stat.vue";
import Time from "./Time.vue";
defineProps({
items: Array,
});
</script>

240
src/components/Setings.vue Normal file
View File

@@ -0,0 +1,240 @@
<template>
<div class="w-full sm:w-4/5 dark:text-neutral-300">
<!-- Ввод времени -->
<div class="bg-white p-4 mb-4 flex-colum sm:flex dark:bg-gray-800">
<div class="w-full sm:max-w-100 flex">
<input
v-model="time"
type="text"
placeholder="01.01.2026"
class="dark:bg-gray-900 min-w-30 border-slate-100 shadow rounded-xl w-1/2 p-3"
/>
<button
class="dark:bg-orange-500 hover:dark:bg-orange-600 ml-4 shadow text-white bg-sky-700 w-1/2 hover:bg-sky-900 rounded-xl px-2 min-h-11 cursor-pointer"
@click="start_parser_1"
>
Парсить 1 источник
</button>
</div>
<button
class="dark:bg-orange-500 hover:dark:bg-orange-600 sm:ml-2 shadow mt-1 sm:mt-0 text-white w-full sm:max-w-50 bg-sky-700 hover:bg-sky-900 rounded-xl px-2 min-h-11 cursor-pointer"
@click="start_parser_2"
>
Парсить 2 источник
</button>
</div>
<!-- Список источников и промт -->
<div
class="dark:bg-gray-800 sm:m-5 bg-white p-4 hover:-translate-y-2 hover:shadow-2xl border-slate-100 rounded-xl transition"
>
<div
class="flex-colum sm:flex items-center mb-4 border-slate-900 justify-between"
>
<div class="w-full sm:max-w-60 flex">
<button
v-for="(source, index) in sources"
:key="source.name"
@click="selectSource(index)"
class="dark:bg-neutral-500 hover:dark:bg-neutral-600 p-2 mr-2 rounded-xl bg-sky-700 hover:bg-sky-900 w-1/2 text-white cursor-pointer"
>
{{ source.name }}
</button>
</div>
<!-- Кнопка сохранения источников -->
<button
class="shadow mt-1 sm:mt-0 text-white bg-green-500 hover:bg-green-600 rounded-xl w-full sm:max-w-60 px-2 min-h-11 cursor-pointer"
@click="saveSources"
>
Сохранить изменения
</button>
</div>
<!-- Редактируемый промт -->
<div class=" ">
<textarea
v-model="promt"
class="w-full min-h-100 rounded-xl p-2 border-2 border-neutral-300"
></textarea>
</div>
</div>
<!-- Список логов -->
<div
class="dark:bg-gray-800 mt-5 sm:m-5 bg-white p-4 hover:-translate-y-2 hover:shadow-2xl border-slate-100 rounded-xl transition"
>
<!-- Кнопка для переключения -->
<button
@click="toggleLogs"
class="dark:bg-orange-500 hover:dark:bg-orange-600 shadow text-white bg-sky-700 w-full hover:bg-sky-900 rounded-xl px-2 min-h-11 cursor-pointer"
>
{{ showLogs ? "Скрыть логи" : "Показать логи" }}
</button>
<!-- Блок логов, показывается или скрывается -->
<div
class="border-1 border-neutral-300 p-2 mt-2 rounded-xl"
v-if="showLogs"
>
<div v-for="(log, index) in logs" :key="index">
{{ log }}
</div>
</div>
</div>
<div
class="dark:bg-gray-800 mt-5 sm:m-5 bg-white p-4 hover:-translate-y-2 hover:shadow-2xl border-slate-100 rounded-xl transition"
>
<!-- Кнопка для переключения -->
<button
@click="toggletask"
class="dark:bg-orange-500 hover:dark:bg-orange-600 shadow text-white bg-sky-700 w-full hover:bg-sky-900 rounded-xl px-2 min-h-11 cursor-pointer"
>
{{ showtask ? "Скрыть задачи" : "Показать задачи" }}
</button>
<div
class="border-1 border-neutral-300 p-2 mt-2 rounded-xl overflow-x-auto"
v-if="showtask"
>
<table class="w-full min-w-max">
<thead>
<tr>
<th class="px-2 py-2 text-left">Status</th>
<th class="px-2 py-2 text-left">Source URL</th>
</tr>
</thead>
<tbody>
<tr v-for="task in tasks" :key="task.id">
<td
class="px-2 py-2 whitespace-nowrap overflow-hidden overflow-ellipsis max-w-full"
>
{{ task.status }}
</td>
<td
class="px-2 py-2 whitespace-nowrap overflow-hidden overflow-ellipsis max-w-full"
>
{{ task.source_url }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from "vue";
import axios from "axios";
const sources = ref([
{ name: "source1", url: "eee", prompt: "" },
{ name: "source2", url: "https://example2.com/api", prompt: "" },
]);
const currentSource = ref(sources.value[0]);
const time = ref("");
const promt = ref("Начальный текст");
const logs = ref([]);
const tasks = ref([]);
const showLogs = ref(false); // скрыто по умолчанию
const showtask = ref(false); // скрыто по умолчанию
// функция для переключения видимости логов
function toggleLogs() {
showLogs.value = !showLogs.value;
if (showLogs.value) {
load_log();
}
}
const load_log = async () => {
try {
const response = await axios.get("http://45.129.78.228:8001/logs");
logs.value = response.data.logs;
} catch (error) {
console.error("Ошибка при загрузке настроек:", error);
}
};
// функция для переключения видимости задач
function toggletask() {
showtask.value = !showtask.value;
if (showtask.value) {
loadTasks();
}
}
const loadTasks = async () => {
try {
const response = await axios.get(
"http://45.129.78.228:8001/get_tasks_offset",
);
// Предположим, что ответ уже массив задач
tasks.value = response.data; // или response.data если это массив
} catch (error) {
console.error("Ошибка при загрузке задач:", error);
}
};
// Загрузка настроек при монтировании
onMounted(() => {
loadSettings();
// load_log();
});
// watch(logs, load_log);
// Получение настроек с сервера
const loadSettings = async () => {
try {
const response = await axios.get("http://45.129.78.228:8001/settings");
sources.value = response.data.sources;
currentSource.value = sources.value[0];
promt.value = currentSource.value.prompt;
} catch (error) {
console.error("Ошибка при загрузке настроек:", error);
}
};
// Выбор источника
const selectSource = (index) => {
currentSource.value = sources.value[index];
promt.value = currentSource.value.prompt;
};
// Сохранение источников
const saveSources = async () => {
try {
await axios.post("http://45.129.78.228:8001/settings", {
name: currentSource.value.name,
url: "",
prompt: promt.value,
});
// Здесь можно отправить на сервер, если нужно
loadSettings();
} catch (error) {
console.error("Ошибка при сохранении источников:", error);
}
};
// Запуск парсинга 1
const start_parser_1 = async () => {
try {
await axios.post("http://45.129.78.228:8001/parser_1", {
time: time.value,
});
} catch (error) {
console.error("Ошибка запроса parser_1:", error);
}
};
// Запуск парсинга 2
const start_parser_2 = async () => {
try {
await axios.post("http://45.129.78.228:8001/parser_2");
} catch (error) {
console.error("Ошибка запроса parser_2:", error);
}
};
</script>

106
src/components/Stat.vue Normal file
View File

@@ -0,0 +1,106 @@
<script setup>
import { ref, onMounted, onBeforeUnmount } from "vue";
import axios from "axios";
const props = defineProps({
url: String,
parsed_at: String,
title: String,
category: String,
short_text: String,
article_date: String,
original_text: String,
translation_text: String,
other: String,
viewed: Boolean,
status: Boolean,
});
//Код для раскрытия/скрытия карточки
const isOpen = ref(false);
const cardRef = ref(null);
function toggle() {
isOpen.value = true; //!isOpen.value;
}
function handleClickOutside(event) {
if (cardRef.value && !cardRef.value.contains(event.target)) {
isOpen.value = false;
}
}
const viewed_b = async () => {
try {
const response = await axios.post(
"http://45.129.78.228:8002/update_viewed_status",
null,
{
params: {
url: props.url,
viewed: true,
},
},
);
} catch (err) {
console.log(err);
}
};
onMounted(() => {
document.addEventListener("click", handleClickOutside);
});
onBeforeUnmount(() => {
document.removeEventListener("click", handleClickOutside);
});
</script>
<template>
<div
class="bg-white dark:bg-gray-800 hover:-translate-y-2 hover:shadow-2xl rounded-xl transition pb-1 mb-3"
:class="{ 'opacity-50': props.viewed }"
><!--:class="{ 'opacity-50': props.viewed }" обработчик прочитанности-->
<div ref="cardRef" class="cursor-pointer" @click="toggle">
<div class="flex justify-between p-3">
<p>{{ article_date }}</p>
<a
:href="url"
class="max-w-100 text-blue-600 dark:text-gray-400 hover:text-blue-800 underline flex"
style="overflow: hidden; white-space: nowrap; text-overflow: ellipsis"
>
<img
src="/src/assets/href.webp"
class="w-5 h-6 mr-2"
/>
{{ url }}
</a>
</div>
<div class="m-3 dark:bg-gray-900 shadow rounded-xl p-3">
<p>{{ title }}</p>
</div>
<!-- Вариант с Tailwind: показывать/скрывать по состоянию -->
<div v-show="isOpen" class="transition-all">
<div class="m-3 dark:bg-gray-900 shadow rounded-xl p-3">
<p>{{ short_text }}</p>
</div>
<div class="m-3 dark:bg-gray-900 shadow rounded-xl p-3">
<p>{{ translation_text }}</p>
</div>
<div class="m-3 dark:bg-gray-900 shadow rounded-xl p-3">
<p>{{ category }}</p>
</div>
<div class="m-3 dark:bg-gray-900 shadow rounded-xl p-3">
<p>{{ original_text }}</p>
</div>
</div>
</div>
<button
class="ml-4 shadow mb-5 bg-lime-500 dark:bg-orange-500 hover:dark:bg-orange-600 hover:dark:text-neutral-50 hover:bg-lime-300 hover:text-stone-900 rounded-xl w-40 h-10 text-white cursor-pointer"
@click="viewed_b"
>
Прочитано
</button>
</div>
</template>

45
src/components/Time.vue Normal file
View File

@@ -0,0 +1,45 @@
<template>
<div>
<p>Дата и время в Пекине: <br>{{ currentDateTime }}</p>
</div>
</template>
<script>
export default {
data() {
return {
currentDateTime: '', // строка с текущими датой и временем
timer: null,
};
},
methods: {
updateTime() {
const options = {
timeZone: 'Asia/Shanghai',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
};
const formatter = new Intl.DateTimeFormat('zh-CN', options);
const parts = formatter.formatToParts(new Date());
const dateParts = {};
parts.forEach(({ type, value }) => {
dateParts[type] = value;
});
this.currentDateTime = `${dateParts.year}-${dateParts.month}-${dateParts.day} ${dateParts.hour}:${dateParts.minute}:${dateParts.second}`;
},
},
mounted() {
this.updateTime();
this.timer = setInterval(this.updateTime, 1000);
},
beforeDestroy() {
clearInterval(this.timer);
},
};
</script>

6
src/main.js Normal file
View File

@@ -0,0 +1,6 @@
import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

15
vite.config.js Normal file
View File

@@ -0,0 +1,15 @@
import { fileURLToPath, URL } from "node:url";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueDevTools from "vite-plugin-vue-devtools";
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), vueDevTools(), tailwindcss()],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});