Compare commits

...

3 Commits

Author SHA1 Message Date
Kseninia Mikhaylova 8335b16bad all result tables 2025-06-25 13:50:14 +03:00
Kseninia Mikhaylova 289e256286 show results 2025-06-25 12:52:50 +03:00
Kseninia Mikhaylova 9c025628ff loading status 2025-06-25 12:11:55 +03:00
7 changed files with 382 additions and 106 deletions

View File

@ -0,0 +1,58 @@
<script setup>
defineProps({
drawingData: {
type: Array,
required: true
}
})
// Форматируем чертежи под TableItem
function formatDrawings(item) {
return item.drawings.reduce((acc, drawing) => {
acc[drawing.name] = drawing.path
return acc
}, {})
}
// Проверяем, есть ли спецификации
function hasSpecifications(item) {
return item.specifications && Array.isArray(item.specifications) && item.specifications.length > 0
}
// Форматируем спецификации под TableItem
function formatSpecifications(item) {
if (!hasSpecifications(item)) return {}
return item.specifications.reduce((acc, spec) => {
acc[spec.name] = spec.path
return acc
}, {})
}
</script>
<template>
<div class="mt-6">
<h3 class="text-xl font-semibold mb-4">Результат: Чертежи и спецификации</h3>
<div v-for="item in drawingData" :key="item.document_name" class="mb-6 border p-4 rounded shadow-sm bg-white">
<h4 class="font-medium text-lg">{{ item.document_name }}</h4>
<!-- Чертежи -->
<TableItem
caption="Чертежи:"
:items="formatDrawings(item)"
label-key="Название"
label-value="Путь"
/>
<!-- Спецификации -->
<TableItem
v-if="hasSpecifications(item)"
caption="Спецификации:"
:items="formatSpecifications(item)"
label-key="Название"
label-value="Путь"
/>
</div>
</div>
</template>

View File

@ -0,0 +1,41 @@
<template>
<div>
<h2 class="text-xl font-bold mb-4">Результаты сохранения в IGES</h2>
<table class="min-w-full border-collapse mt-4">
<thead class="bg-gray-100">
<tr>
<th class="border px-4 py-2 text-left">Документ</th>
<th class="border px-4 py-2 text-left">Файл</th>
<th class="border px-4 py-2 text-left">Успешно</th>
<th class="border px-4 py-2 text-left">Ошибка</th>
</tr>
</thead>
<tbody>
<tr v-for="file in files" :key="file.timestamp" class="hover:bg-gray-50">
<td class="border px-4 py-2">{{ file.document_name }}</td>
<td class="border px-4 py-2">
<a :href="`file://${file.file}`" target="_blank">{{ file.file }}</a>
</td>
<td class="border px-4 py-2">
<span :class="file.success ? 'text-green-500' : 'text-red-500'">
{{ file.success ? '✅ Да' : '❌ Нет' }}
</span>
</td>
<td class="border px-4 py-2">
{{ file.error || '-' }}
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
defineProps({
files: {
type: Array,
required: true
}
})
</script>

View File

@ -0,0 +1,37 @@
<script setup lang="ts">
defineProps({
caption: {
type: String,
default: ''
},
items: {
type: Object,
required: true
},
labelKey: {
type: String,
default: 'Ключ'
},
labelValue: {
type: String,
default: 'Значение'
}
})
</script>
<template>
<table class="min-w-full mt-4 border-collapse">
<caption v-if="caption" class="caption-top font-medium text-left">{{ caption }}</caption>
<thead class="bg-gray-100">
<tr>
<th class="border px-4 py-2 text-left">{{ labelKey }}</th>
<th class="border px-4 py-2 text-left">{{ labelValue }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, key) in items" :key="key">
<td class="border px-4 py-2">{{ key }}</td>
<td class="border px-4 py-2">{{ value }}</td>
</tr>
</tbody>
</table>
</template>

View File

@ -0,0 +1,32 @@
<template>
<div v-if="item">
<h3 class="text-xl font-bold mb-2">{{ item.name }}</h3>
<p>Количество элементов: {{ item.elements_count }}</p>
<p>Количество гибов: {{ item.bends_count }}</p>
<!-- Имя -->
<TableItem caption="По названию:" :items="item.statistics.Name" label-key="Название" label-value="Количество" />
<!-- Материал -->
<TableItem caption="По материалу:" :items="item.statistics.Material" label-key="Материал"
label-value="Количество" />
<!-- Площадь -->
<TableItem caption="Площадь материалов:" :items="item.statistics.Area" label-key="Материал"
label-value="Площадь (м²)" />
<!-- Сварки -->
<TableItem caption="Сварные швы:" :items="item.statistics.Welding" label-key="Имя сварки"
label-value="Длина (мм)" />
</div>
</template>
<script setup>
defineProps({
item: {
type: Object,
required: true
}
})
</script>

View File

@ -4,26 +4,33 @@ const availableActions = ref<Record<string, { label: string; allowedTypes: numbe
export function useKompasActions() {
const { sendCommandToPython, isReady } = usePythonBridge()
const loading = ref(false)
const error = ref<string | null>(null)
/**
* Загружает список действий с бэкенда
*/
async function loadActions() {
loading.value = true
if (!isReady.value) {
console.warn('Bridge не готов')
return
}
error.value = null
try {
const actions = await sendCommandToPython<{
[key: string]: { label: string; allowed_types: number[] }
// Отправляем команду и ожидаем ответ в новом формате
const response = await sendCommandToPython<{
status: 'success' | 'error'
data: Record<string, { label: string; allowed_types: number[] }> | null
error: string | null
}>('get_available_actions')
if (actions) {
// Преобразуем все ключи `allowed_types` → `allowedTypes`
if (response.status === 'success' && response.data) {
// Преобразуем данные
const convertedActions: Record<string, { label: string; allowedTypes: number[] }> = {}
for (const key in actions) {
const { label, allowed_types } = actions[key]
for (const key in response.data) {
const { label, allowed_types } = response.data[key]
convertedActions[key] = {
label,
allowedTypes: allowed_types
@ -31,18 +38,18 @@ export function useKompasActions() {
}
availableActions.value = convertedActions
} else {
throw new Error(response.error || 'Неизвестная ошибка')
}
} catch (err: any) {
console.error('Ошибка при получении действий:', err)
error.value = 'Не удалось загрузить список действий'
} finally {
loading.value = false
}
}
return {
availableActions,
loading,
error,
loadActions,
}

View File

@ -1,4 +1,4 @@
import { ref, onMounted } from 'vue'
// composables/usePythonBridge.ts
declare global {
interface Window {
@ -12,29 +12,25 @@ declare global {
new(transport: any, ready: (channel: any) => void): void
}
}
interface PythonEvent {
type: string
data: any
}
const pyjs = ref<any>(null)
const isReady = ref<boolean>(false)
// Хранилище обработчиков событий
const pythonEventHandlers = new Map<string, Array<(data: any) => void>>()
function receiveFromPython(eventType: string, data: any) {
console.warn("🟢 Event from Python:", eventType, data)
const handlers = pythonEventHandlers.get(eventType) || []
handlers.forEach(handler => handler(data))
}
export function usePythonBridge() {
const pyjs = useState<any>('bridge_pyjs', () => null)
const isReady = useState<boolean>('bridge_ready', () => false)
const isAvailableOnPlatform = useState<boolean>('bridge_av', () => false)
const loading = useState<boolean>('bridge_loading', () => false)
// Хранилище обработчиков событий
const pythonEventHandlers = new Map<string, Array<(data: any) => void>>()
function receiveFromPython(eventType: string, data: any) {
console.warn("🟢 Event from Python:", eventType, data)
const handlers = pythonEventHandlers.get(eventType) || []
handlers.forEach(handler => handler(data))
}
function setupQWebChannel() {
if (window.QWebChannel && window.qt && window.qt.webChannelTransport) {
new window.QWebChannel(window.qt.webChannelTransport, function (channel) {
isAvailableOnPlatform.value = true
isReady.value = true
pyjs.value = channel.objects.pyjs
@ -47,6 +43,7 @@ export function usePythonBridge() {
}
})
} else {
isAvailableOnPlatform.value = false
console.error('Qt WebChannel недоступен')
}
}
@ -55,24 +52,34 @@ export function usePythonBridge() {
command: string,
data: Record<string, any> = {}
): Promise<T> {
return new Promise((resolve, reject) => {
if (!isReady.value) {
reject(new Error('Мост еще не готов'))
return
}
if (!isReady.value) {
throw new Error('Мост еще не готов')
}
if (!pyjs.value || !pyjs.value.callFromJS) {
reject(new Error('Python недоступен'))
return
}
if (!pyjs.value || !pyjs.value.callFromJS) {
throw new Error('Python недоступен')
}
pyjs.value.callFromJS(command, JSON.stringify(data), (result: string) => {
loading.value = true
return new Promise<T>((resolve, reject) => {
setTimeout(() => {
try {
resolve(JSON.parse(result))
} catch (e) {
reject(e)
pyjs.value.callFromJS(command, JSON.stringify(data), (result: string) => {
try {
const parsed = JSON.parse(result)
resolve(parsed)
} catch (e) {
reject(e)
} finally {
loading.value = false
}
})
} catch (err) {
reject(err)
loading.value = false
}
})
}, 300)
})
}
@ -96,7 +103,7 @@ export function usePythonBridge() {
}
const checkBridge = () => {
if (window.QWebChannel && window.qt?.webChannelTransport) {
if (import.meta.client && window.QWebChannel && window.qt?.webChannelTransport) {
setupQWebChannel()
}
@ -107,12 +114,16 @@ export function usePythonBridge() {
}
}
onMounted(() => {
// Вызываем один раз при первом монтировании
if (!isReady.value) {
checkBridge()
})
}
return {
pyjs,
isReady,
isAvailableOnPlatform,
loading,
sendCommandToPython,
onPythonEvent,
offPythonEvent,

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
const { sendCommandToPython, isReady } = usePythonBridge()
const { sendCommandToPython, isReady, isAvailableOnPlatform, loading } = usePythonBridge()
const {
documents,
@ -21,28 +21,85 @@ watch(selectedAction, () => {
selectedTypes.value = availableActions.value[selectedAction.value].allowedTypes
})
const statsData = ref(null)
const filesData = ref(null)
const drawingData = ref(null)
const resultData = ref(null)
// Синхронизация с КОМПАС
async function syncKompas() {
try {
const docs = await sendCommandToPython<KompasDocument[]>('open_kompas')
// Получаем ответ в новом формате: { status, data, error }
const response = await sendCommandToPython('open_kompas')
if (!docs || !Array.isArray(docs)) {
console.warn('Неожиданный ответ от Python:', docs)
documents.value = []
} else {
documents.value = docs
loadActions()
console.log('Полный ответ от Python:', response)
// Проверяем структуру ответа
if (!response || typeof response !== 'object') {
throw new Error('Некорректный формат ответа: пустой или не объект')
}
console.warn('Документы из КОМПАС:', docs)
if (response.status === 'error') {
throw new Error(response.error || 'Неизвестная ошибка')
}
if (!response.data || !Array.isArray(response.data)) {
throw new Error('Данные от сервера отсутствуют или имеют неверный формат')
}
// Успешно получили документы
documents.value = response.data
loadActions()
console.warn('Документы из КОМПАС:', documents.value)
} catch (err) {
console.error('Ошибка при синхронизации с КОМПАС:', err)
alert('Не удалось получить список документов из КОМПАС')
alert(`Не удалось получить список документов из КОМПАС\n(${err})`)
}
}
const canRunAction = computed(() => {
return selectedAction.value && filteredDocuments.value.length > 0
})
function updateUIWithResult(data: any, action: any) {
// Обнуляем предыдущие данные
statsData.value = null
filesData.value = null
drawingData.value = null
resultData.value = null
// Сохраняем сырые данные на случай непредвиденного типа действия
resultData.value = data
if (!data || !data.result || !Array.isArray(data.result)) {
console.warn('Некорректный формат данных')
return
}
console.warn(action)
console.warn(JSON.stringify(data))
switch (action) {
case 'stats':
// Для статистики
statsData.value = data
break
case 'iges':
case 'export_raster':
filesData.value = data
break
case 'project_support':
drawingData.value = data
break
default:
console.warn(`Неизвестное действие: ${action}`)
break
}
}
async function runSelectedAction() {
if (!selectedAction.value) {
alert('Выберите действие')
@ -51,20 +108,40 @@ async function runSelectedAction() {
try {
// Отправляем команду на бэкенд
// Обнуляем предыдущие данные
statsData.value = null
filesData.value = null
drawingData.value = null
resultData.value = null
const result = await sendCommandToPython(selectedAction.value)
console.log('Результат выполнения:', result)
console.log('Полный ответ от сервера:', result)
if (result && result.status === 'success') {
alert(`Действие "${selectedAction.value}" выполнено успешно!`)
} else if (result && result.status === 'error') {
alert(`Ошибка при выполнении: ${result.error || 'Неизвестная ошибка'}`)
} else {
alert('Не удалось получить результат от сервера')
// Проверяем, что результат это объект и содержит поле status
if (typeof result !== 'object' || result === null) {
// alert('Ошибка: получен некорректный формат данных от сервера')
console.error('Некорректный формат результата:', result)
return
}
// Теперь можно безопасно работать с result.status
if (result.status === 'success') {
// alert(` Действие "${selectedAction.value}" выполнено успешно!`)
updateUIWithResult(result.data, selectedAction.value) // например, выводим данные в интерфейс
}
else if (result.status === 'error') {
const errorMessage = result.error || 'Неизвестная ошибка'
// alert(` Ошибка при выполнении: ${errorMessage}`)
console.error('Ошибка от сервера:', errorMessage)
}
else {
// alert(' Не удалось обработать результат от сервера')
console.warn('Неизвестный статус ответа:', result)
}
} catch (err) {
console.error('Ошибка при выполнении действия:', err)
alert('Произошла ошибка при выполнении действия')
console.error('🚨 Ошибка при выполнении действия:', err)
alert('🔥 Произошла критическая ошибка при выполнении действия')
}
}
</script>
@ -72,16 +149,20 @@ async function runSelectedAction() {
<template>
<div class="flex flex-col gap-4 p-4">
<h1>Компас прослушка событий от Python</h1>
<div v-if="!isAvailableOnPlatform" class="mb-4 p-3 bg-red-100 text-red-800 rounded">
<strong>Мост недоступен на этой платформе.</strong> Возможно, вы запускаете приложение не в Qt/PySide.
</div>
<!-- Статус -->
<div v-if="!isReady" class="mb-4 p-3 bg-yellow-100 text-yellow-800 rounded flex items-center">
<div v-if="!isReady && isAvailableOnPlatform"
class="mb-4 p-3 bg-yellow-100 text-yellow-800 rounded flex items-center">
<strong class="ml-2">Ещё рано отправлять запросы. Идёт инициализация...</strong>
</div>
<template v-else>
<template v-if="isReady && isAvailableOnPlatform">
<div>
<!-- Кнопка синхронизации -->
<button @click="syncKompas"
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded mb-4 transition">
class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded mb-4 transition"
:disabled="loading">
🔁 Синхронизировать КОМПАС
</button>
</div>
@ -91,40 +172,44 @@ async function runSelectedAction() {
Нет данных о документах. Нажмите "Синхронизировать КОМПАС".
</div>
<template v-else>
<!-- Чекбоксы для фильтрации -->
<div class="mb-4 p-3 bg-gray-100 rounded shadow-sm">
<h3 class="font-medium mb-2">Фильтр по типам:</h3>
<div class="flex flex-wrap gap-4">
<label v-for="(label, code) in documentTypeLabels" :key="code" class="flex items-center gap-1">
<input type="checkbox" :value="Number(code)" v-model="selectedTypes"
:disabled="documentCounts[Number(code)] === 0"
class="form-checkbox h-4 w-4 text-blue-600 disabled:opacity-50" />
<span class="text-sm">
{{ label }} ({{ documentCounts[Number(code)] }})
</span>
</label>
</div>
</div>
<!-- Фильтры и действия в одной строке -->
<div class="grid grid-cols-2 gap-6">
<!-- Действия -->
<div class="mb-4 p-3 bg-gray-100 rounded shadow-sm">
<h3 class="font-medium mb-2">Действия:</h3>
<div class="flex flex-col gap-2">
<label v-for="(action, key) in availableActions" :key="key" class="flex items-center gap-2">
<input type="radio" :value="key" v-model="selectedAction"
class="form-radio h-4 w-4 text-blue-600" />
<span class="text-sm">{{ action.label }}</span>
</label>
<!-- Чекбоксы для фильтрации -->
<div class="mb-4 p-3 bg-gray-100 rounded shadow-sm">
<h3 class="font-medium mb-2">Фильтр по типам:</h3>
<div class="flex flex-wrap gap-4">
<label v-for="(label, code) in documentTypeLabels" :key="code"
class="flex items-center gap-1">
<input type="checkbox" :value="Number(code)" v-model="selectedTypes"
:disabled="documentCounts[Number(code)] === 0"
class="form-checkbox h-4 w-4 text-blue-600 disabled:opacity-50" />
<span class="text-sm">
{{ label }} ({{ documentCounts[Number(code)] }})
</span>
</label>
</div>
</div>
<!-- Кнопка выполнения действия -->
<button type="button" @click="runSelectedAction" :disabled="!canRunAction" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600
<!-- Действия -->
<div class="mb-4 p-3 bg-gray-100 rounded shadow-sm">
<h3 class="font-medium mb-2">Действия:</h3>
<div class="flex flex-col gap-2">
<label v-for="(action, key) in availableActions" :key="key" class="flex items-center gap-2">
<input type="radio" :value="key" v-model="selectedAction"
class="form-radio h-4 w-4 text-blue-600" />
<span class="text-sm">{{ action.label }}</span>
</label>
</div>
<!-- Кнопка выполнения действия -->
<button type="button" @click="runSelectedAction" :disabled="!canRunAction || loading" class="mt-4 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600
disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none
transition">
Выполнить действие
</button>
Выполнить действие
</button>
</div>
</div>
<!-- Грид документов -->
<div v-if="filteredDocuments.length" class="mt-4 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div v-for="(doc, index) in filteredDocuments" :key="index"
@ -135,18 +220,23 @@ async function runSelectedAction() {
<strong>Тип:</strong> {{ getDocumentLabel(doc.type) }}
</div>
<div class="mt-1">
<strong>Активен:</strong>
<span class="ml-2">
{{ doc.active ? '✅' : '❌' }}
</span>
</div>
<div class="mt-2 text-xs text-gray-500 truncate">
{{ doc.path }}
</div>
</div>
</div>
<div>
<template v-if="filesData">
<TableFiles :files="(filesData as any).result" />
</template>
<template v-if="statsData">
<TableStats v-for="item in (statsData as any).result" :key="item.name" :item="item" />
</template>
<template v-if="drawingData">
<TableDrawing :drawingData="(drawingData as any).result" />
</template>
</div>
</template>
</template>
</div>