/
creator
/
ecstasy
Обзор
Документация
Войти
/
creator
/
ecstasy
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
frontend/src/components/ConfigFiles.vue
363 строки
14 KB
ig-rudenko
v 17.6.0 Страница device info с использованием TypeScript
10 янв 2024, 11:31
10 янв 2024, 11:31
5632d97
Код
Авторство
О чём код?
<template> <div class="py-4"> <div class="card rounded-2 shadow"> <div class="card-body"> <!-- HEADER--> <div v-if="!collectNew.active" class="py-2 d-flex justify-content-md-between align-items-center"> <h4 class="m-0">Конфигурации</h4> <!-- ADD NEW --> <div> <button @click="collectConfig" class="btn btn-outline-success me-2"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="me-2" viewBox="0 0 16 16"> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"></path> <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path> </svg> <span>Собрать новую</span> </button> <button @click="showDiffDialog=true" class="btn btn-outline-primary"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="me-2" viewBox="0 0 16 16"> <path d="M0 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v2h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-2H2a2 2 0 0 1-2-2V2zm5 10v2a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V6a1 1 0 0 0-1-1h-2v5a2 2 0 0 1-2 2H5zm6-8V2a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h2V6a2 2 0 0 1 2-2h5z"/> </svg> <span>Сравнение</span> </button> </div> </div> <!-- --> <!-- ELSE | COLLECTING--> <div v-else style="padding-bottom: 5px;"> <div style="padding-bottom: 5px;"> <h4>Собираем текущую конфигурацию</h4> </div> <div class="progress" role="progressbar" aria-label="Animated striped example" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100"> <div class="progress-bar progress-bar-striped progress-bar-animated" style="width: 100%;"></div> </div> </div> <!-- --> <!-- ALERT--> <div v-if="collectNew.display" :class="alertClasses"> <div style="padding-right: 0;"> <span class="me-2"> {{collectNew.text}} </span> <button @click="collectNew.display=false" type="button" class="btn-close" style="position: sticky;vertical-align: middle;left: 100%;"></button> </div> </div> <!-- --> <div class="table-responsive-lg"> <table class="table"> <tbody style="vertical-align: middle;"> <template v-for="file in files"> <tr> <td style="width: 100px; text-align: right;"> <div class="button-group"> <!-- DOWNLOAD--> <div @click="downloadFile(file)" class="button-element"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-download" viewBox="0 0 16 16"> <path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"></path> <path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"></path> </svg> </div> <!-- DELETE--> <div @click="selectedFile=file" class="button-element" data-bs-toggle="modal" data-bs-target="#delete-config-file-modal"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="red" class="bi bi-x-lg" viewBox="0 0 16 16"> <path d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854Z"></path> </svg> </div> </div> </td> <td> <span v-html="fileIcon(file.name)"></span> <span @click="toggleFileDisplay(file)" style="cursor: pointer;">{{ file.name }}</span> </td> <td>{{formatBytes(file.size)}}</td> <td> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-clock me-2" viewBox="0 0 16 16"> <path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71V3.5z"></path> <path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16zm7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0z"></path> </svg> <span>{{ file.modTime }}</span> </td> </tr> <tr v-if="file.display && file.content"> <td colspan="4" style="max-width: 1rem;"> <div style="font-family: monospace; padding: 1rem" v-html="formatToHtml(file.content)"></div> </td> </tr> </template> </tbody> </table> </div> </div> </div> </div> <!-- Delete Modal --> <div class="modal fade" id="delete-config-file-modal" tabindex="-1"> <div class="modal-dialog modal-dialog-centered"> <div class="modal-content"> <div class="modal-header"> <h1 class="modal-title fs-5" id="exampleModalLabel"> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="red" class="me-2" viewBox="0 0 16 16"> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"></path> <path d="M7.002 11a1 1 0 1 1 2 0 1 1 0 0 1-2 0zM7.1 4.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 4.995z"></path> </svg> <span>Внимание!</span> </h1> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> </div> <div class="modal-body text-center"> <p>Вы уверены, что хотите удалить конфигурацию?</p> <p v-if="selectedFile">{{selectedFile.name}}</p> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Нет</button> <button v-if="selectedFile" type="button" class="btn btn-danger" @click="deleteFile(selectedFile)" data-bs-dismiss="modal">Удалить</button> </div> </div> </div> </div> <Dialog v-model:visible="showDiffDialog" modal header="Сравнение конфигураций"> <ConfigFileDiff :config-files="files" :device-name="deviceName" :formatted-config-function="formatConfigFile" /> </Dialog> </template> <script lang="ts"> import {defineComponent} from "vue"; import {AxiosResponse} from "axios"; import Dialog from "primevue/dialog"; import ConfigFileDiff from "../pages/deviceInfo/components/ConfigFileDiff.vue"; import api_request from "../api_request"; interface ConfigFile { name: string size: number, modTime: string, content?: string, display?: boolean } class CollectNewConfig { constructor( public active: boolean = false, public status: "success"|"error" = "success", public display: boolean = false, public text: string = "" ) {} setFree() { this.display = true this.active = false } } export default defineComponent({ name: "ConfigFiles", components: { Dialog, ConfigFileDiff, }, props: { deviceName: {required: true, type: String}, }, data() { return { files: [] as ConfigFile[], selectedFile: null as ConfigFile|null, collectNew: new CollectNewConfig(), showDiffDialog: false, } }, mounted() { this.getFiles() }, computed: { alertClasses(){ if (this.collectNew.status === "success"){ return ["alert", "alert-success"] } else if (this.collectNew.status === "error"){ return ["alert", "alert-danger"] } } }, methods: { fileIcon(fileName: string): string { if (fileName && fileName.endsWith(".txt")) { return `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="currentColor" class="bi bi-file-earmark-text me-1" viewBox="0 0 16 16" style="cursor: pointer;"> <path d="M5.5 7a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1h-5zM5 9.5a.5.5 0 0 1 .5-.5h5a.5.5 0 0 1 0 1h-5a.5.5 0 0 1-.5-.5zm0 2a.5.5 0 0 1 .5-.5h2a.5.5 0 0 1 0 1h-2a.5.5 0 0 1-.5-.5z"></path> <path d="M9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V4.5L9.5 0zm0 1v2A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"></path> </svg>` } if (fileName && fileName.endsWith(".zip")) { return `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="currentColor" class="bi bi-file-earmark-zip me-1" viewBox="0 0 16 16"> <path d="M5 7.5a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v.938l.4 1.599a1 1 0 0 1-.416 1.074l-.93.62a1 1 0 0 1-1.11 0l-.929-.62a1 1 0 0 1-.415-1.074L5 8.438V7.5zm2 0H6v.938a1 1 0 0 1-.03.243l-.4 1.598.93.62.929-.62-.4-1.598A1 1 0 0 1 7 8.438V7.5z"/> <path d="M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5L14 4.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1h-2v1h-1v1h1v1h-1v1h1v1H6V5H5V4h1V3H5V2h1V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5h-2z"/> </svg>` } return `<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" fill="currentColor" class="bi bi-file-binary me-1" viewBox="0 0 16 16"> <path d="M5.526 13.09c.976 0 1.524-.79 1.524-2.205 0-1.412-.548-2.203-1.524-2.203-.978 0-1.526.79-1.526 2.203 0 1.415.548 2.206 1.526 2.206zm-.832-2.205c0-1.05.29-1.612.832-1.612.358 0 .607.247.733.721L4.7 11.137a6.749 6.749 0 0 1-.006-.252zm.832 1.614c-.36 0-.606-.246-.732-.718l1.556-1.145c.003.079.005.164.005.249 0 1.052-.29 1.614-.829 1.614zm5.329.501v-.595H9.73V8.772h-.69l-1.19.786v.688L8.986 9.5h.05v2.906h-1.18V13h3z"/> <path d="M4 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H4zm0 1h8a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1z"/> </svg>` }, formatBytes(bytes: number): string { let marker = 1024; // Измените на 1000 при необходимости let decimal = 1; let kiloBytes = marker; // Один килобайт - это 1024 байта let megaBytes = kiloBytes * marker; // Один мегабайт - это 1024 килобайта if (bytes < kiloBytes) return bytes + " Б"; else if (bytes < megaBytes) return (bytes / kiloBytes).toFixed(decimal) + " КБ"; else if (bytes < marker * megaBytes) return (bytes / megaBytes).toFixed(decimal) + " МБ"; return "1+ ГБ" }, /** * Превращаем строку в html, для корректного отображения * @param str Строка, для форматирования. * Заменяем перенос строки на `<br>` пробелы на ` ` */ formatToHtml(str: string): string { let space_re = new RegExp(' ', 'g'); let n_re = new RegExp('\n', 'g'); str = str.replace(space_re, ' ').replace(n_re, '<br>') return str }, getFiles(){ api_request.get("/device/api/" + this.deviceName + "/configs") .then( (value: AxiosResponse<ConfigFile[]>) => { this.files = value.data } ) }, collectConfig(){ // Это проверка для предотвращения множественных запросов к серверу. if (this.collectNew.active) return this.collectNew.active = true api_request.post("/device/api/" + this.deviceName + "/collect-config") .then( (value: AxiosResponse) => { this.getFiles(); this.collectNew.status = "success"; this.collectNew.text = value.data.status; this.collectNew.setFree(); }, (reason: any) => { this.collectNew.status = "error"; this.collectNew.text = reason.response.data.error; this.collectNew.setFree(); } ) .catch( () => { this.collectNew.status = "error"; this.collectNew.text = "Ошибка во время сбора новой конфигурации"; this.collectNew.setFree(); } ) }, deleteFile(file: ConfigFile){ api_request.delete("/device/api/" + this.deviceName + "/config/" + file.name) .then( (value: AxiosResponse) => { if (value.status === 204) this.getFiles(); } ) }, toggleFileDisplay(file: ConfigFile) { if (file.size > 1024 * 1024) return if (!file.content) { api_request.get("/device/api/" + this.deviceName + "/config/" + file.name, {responseType: 'blob'}) .then( (value: AxiosResponse<Blob>) => {return value.data.text()} ) .then(value => { if (!value.length) return file.content = this.formatConfigFile(value) }) } file.display = !file.display; }, formatConfigFile(content: string): string { let formattedContent = "" for (const line of content.split("\n")) { const formattedLine = line.replace(/^\s+|\s+$|(\r\n|\n|\r)/gm, ""); if (!formattedLine.length) continue; formattedContent += formattedLine + "\n"; } return formattedContent }, downloadFile(file: ConfigFile){ api_request.get("/device/api/" + this.deviceName + "/config/" + file.name, {responseType: 'blob'}) .then((response) => { // create file link in browser's memory const href = URL.createObjectURL(response.data); // create "a" HTML element with href to file & click const link = document.createElement('a'); link.href = href; link.setAttribute('download', 'file.pdf'); //or any other extension document.body.appendChild(link); link.click(); // clean up "a" element & remove ObjectURL document.body.removeChild(link); URL.revokeObjectURL(href); }); } } }) </script> <style scoped> .button-group { display: flex!important; justify-content: flex-end!important; } .button-element { margin: 10px 5px; cursor: pointer; user-select: none; } </style>