Files
mono/components/desktop/extensions/capital/widgets/CommitsListWidget/CommitsListWidget.vue
T
2026-03-28 21:09:57 +05:00

479 lines
14 KiB
Vue

<template lang="pug">
q-card(flat)
q-table(
:rows='commits?.items || []',
:columns='columns',
row-key='commit_hash',
:pagination='pagination',
@request='onRequest',
flat,
square,
:no-data-label='"Нет коммитов"'
)
template(#body='props')
q-tr(
:props='props',
@click.prevent.stop='handleCommitClick(props.row.commit_hash)',
)
q-td(style='width: 35px')
ExpandToggleButton(
:expanded='expanded[props.row.commit_hash]',
@click='handleToggleExpand(props.row.commit_hash)'
)
// Проекты и компоненты с возможностью перехода
q-td(style='max-width: 250px; word-wrap: break-word; white-space: normal')
.projects-info
.project-link(
v-if='props.row.project?.parent_title',
@click.stop='navigateToProject(props.row.project.parent_hash)'
)
q-icon(name='fa-regular fa-folder', size='xs').q-mr-xs
span.list-item-title {{ props.row.project.parent_title }}
.component-link(
v-if='props.row.project?.title',
@click.stop='navigateToComponent(props.row.project_hash)'
)
q-icon(name='fa-regular fa-folder-open', size='xs').q-mr-xs
span.list-item-title {{ props.row.project.title }}
// Пользователь
q-td(style='width: 120px')
.text-grey-7 {{ props.row.display_name || props.row.username || 'Неизвестный' }}
// Статус
q-td(style='width: 120px')
q-chip(
:color='getStatusColor(props.row.status)',
:text-color="'white'",
dense,
size='sm'
) {{ getStatusLabel(props.row.status) }}
// Затраченное время
q-td.text-right(style='width: 100px')
.stats-info
ColorCard(color='blue')
.card-value {{ formatHours(Number(props.row.amounts?.creators_hours) || 0) }}
// Стоимость часа
q-td.text-right(style='width: 120px')
.stats-info
ColorCard(color='orange')
.card-value {{ formatCurrency(props.row.amounts?.hour_cost) }} / час
// Сумма
q-td.text-right(style='width: 150px')
.stats-info
ColorCard(color='green')
.card-value {{ formatCurrency(props.row.amounts?.creators_base_pool) }}
// Кнопки действий
q-td(style='width: 120px')
.row.items-center.q-gutter-xs.justify-end(v-if='props.row.project?.master === session.username')
ApproveCommitButton(
v-if='props.row.status === Zeus.CommitStatus.CREATED'
:commit-hash='props.row.commit_hash'
mini
)
DeclineCommitButton(
v-if='props.row.status === Zeus.CommitStatus.CREATED'
:commit-hash='props.row.commit_hash'
mini
)
// Разворачиваемый контент с деталями коммита
q-tr.q-virtual-scroll--with-prev(
no-hover,
v-if='expanded[props.row.commit_hash]',
:key='`e_${props.row.commit_hash}`'
)
q-td(colspan='9', style='padding: 16px;')
.commit-details
.row.q-gutter-md
.col
.text-subtitle2.text-grey-7 Дата:
.text-body2 {{ formatDate(props.row.created_at) }}
.q-mt-md
.q-mt-md(v-if='getGitData(props.row.data)?.url')
.text-subtitle2.text-grey-7 Ссылка:
a(:href='getGitData(props.row.data).url', target='_blank', rel='noopener noreferrer').commit-url {{ getGitData(props.row.data).url }}
.q-mt-md(v-if='getGitData(props.row.data)?.diff')
.text-subtitle2.text-grey-7 Изменения:
DiffViewer(:diff='getGitData(props.row.data).diff')
</template>
<script lang="ts" setup>
import { ref, onMounted, watch, computed } from 'vue';
import { useRouter } from 'vue-router';
import type { QTableProps } from 'quasar';
import { useSessionStore } from 'src/entities/Session';
import { useSystemStore } from 'src/entities/System/model';
import { FailAlert } from 'src/shared/api';
import { useCommitStore } from 'app/extensions/capital/entities/Commit/model';
import type { IGetCommitsFilter } from 'app/extensions/capital/entities/Commit/model';
import { ApproveCommitButton } from 'app/extensions/capital/features/Commit/ApproveCommit';
import { DeclineCommitButton } from 'app/extensions/capital/features/Commit/DeclineCommit';
import { ExpandToggleButton } from 'src/shared/ui/ExpandToggleButton';
import { Zeus } from '@coopenomics/sdk';
import { ColorCard } from 'src/shared/ui/ColorCard/ui';
import { formatAsset2Digits } from 'src/shared/lib/utils/formatAsset2Digits';
import { formatHours } from 'src/shared/lib/utils';
import { DiffViewer } from 'src/shared/ui/DiffViewer';
const props = defineProps<{
filter?: IGetCommitsFilter;
expanded?: Record<string, boolean>;
}>();
const emit = defineEmits<{
toggleExpand: [commitHash: string];
commitClick: [commitHash: string];
dataLoaded: [commitHashes: string[]];
paginationChanged: [pagination: { page: number; rowsPerPage: number; sortBy: string; descending: boolean }];
}>();
const router = useRouter();
const session = useSessionStore();
const { info } = useSystemStore();
const commitStore = useCommitStore();
const commits = ref<any>(null);
const loading = ref(false);
// Используем computed для реактивной связи с props.expanded
const expanded = computed({
get: () => props.expanded || {},
set: () => {
// Сеттер не используется напрямую, изменения происходят через emit
}
});
// Следим за изменениями в commitStore и обновляем локальные данные
watch(() => commitStore.commits, (newCommits) => {
if (newCommits) {
commits.value = newCommits;
pagination.value.rowsNumber = newCommits.totalCount || 0;
// Эмитим событие загрузки данных
const commitHashes = newCommits.items?.map(item => item.commit_hash) || [];
emit('dataLoaded', commitHashes);
}
}, { deep: true });
// Пагинация
const pagination = ref({
sortBy: 'created_at',
descending: true,
page: 1,
rowsPerPage: 100,
rowsNumber: 0,
});
// Загрузка коммитов
const loadCommits = async (paginationData?: any) => {
const paginationToUse = paginationData || pagination.value;
loading.value = true;
try {
const filter: IGetCommitsFilter = {
coopname: info.coopname,
...props.filter,
};
await commitStore.loadCommits({
filter,
options: {
page: paginationToUse.page,
limit: paginationToUse.rowsPerPage,
sortBy: paginationToUse.sortBy,
sortOrder: paginationToUse.descending ? 'DESC' : 'ASC',
},
});
commits.value = commitStore.commits;
pagination.value.rowsNumber = commitStore.commits?.totalCount || 0;
// Эмитим событие загрузки данных с актуальными ключами коммитов
const commitHashes = commitStore.commits?.items?.map(
(commit: any) => commit.commit_hash,
) || [];
emit('dataLoaded', commitHashes);
} catch (error) {
console.error('Ошибка при загрузке коммитов:', error);
FailAlert('Не удалось загрузить список коммитов');
} finally {
loading.value = false;
}
};
// Обработчик запросов пагинации и сортировки
const onRequest = async (requestProps: any) => {
const { page, rowsPerPage, sortBy, descending } = requestProps.pagination;
pagination.value.page = page;
pagination.value.rowsPerPage = rowsPerPage;
pagination.value.sortBy = sortBy;
pagination.value.descending = descending;
// Эмитим изменение пагинации для родительской страницы
emit('paginationChanged', {
page,
rowsPerPage,
sortBy,
descending,
});
await loadCommits(pagination.value);
};
const handleToggleExpand = (commitHash: string) => {
emit('toggleExpand', commitHash);
};
const handleCommitClick = (commitHash: string) => {
emit('commitClick', commitHash);
};
// Вспомогательные функции
const formatDate = (dateString: string) => {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
};
// Функции для работы со статусами
const getStatusColor = (status: string) => {
switch (status) {
case Zeus.CommitStatus.CREATED:
return 'orange';
case Zeus.CommitStatus.APPROVED:
return 'green';
case Zeus.CommitStatus.DECLINED:
return 'red';
case Zeus.CommitStatus.UNDEFINED:
return 'grey';
default:
return 'grey';
}
};
const getStatusLabel = (status: string) => {
switch (status) {
case Zeus.CommitStatus.CREATED:
return 'Ожидает';
case Zeus.CommitStatus.APPROVED:
return 'Одобрен';
case Zeus.CommitStatus.DECLINED:
return 'Отклонен';
case Zeus.CommitStatus.UNDEFINED:
return 'Не определен';
default:
return status;
}
};
// Функция форматирования валюты
const formatCurrency = (value?: string) => {
if (!value) return '0.00';
return formatAsset2Digits(value);
};
// Функция для извлечения Git-данных из массива данных коммита
const getGitData = (data: any[] | null | undefined) => {
if (!data || !Array.isArray(data)) return undefined;
const gitItem = data.find(item => item?.type === 'git');
return gitItem?.data;
};
// Функция навигации к проекту (родительскому)
const navigateToProject = (projectHash: string) => {
if (projectHash) {
router.push({
name: 'project-description',
params: { project_hash: projectHash },
query: { _useHistoryBack: 'true' }
});
}
};
// Функция навигации к компоненту
const navigateToComponent = (projectHash: string) => {
if (projectHash) {
router.push({
name: 'component-description',
params: { project_hash: projectHash },
query: { _useHistoryBack: 'true' }
});
}
};
// Загружаем данные при монтировании
onMounted(async () => {
await loadCommits();
});
// Определяем столбцы таблицы
const columns: QTableProps['columns'] = [
{
name: 'expand',
label: '',
align: 'center' as const,
field: '' as const,
sortable: false,
},
{
name: 'projects',
label: 'Проект / Компонент',
align: 'left' as const,
field: 'project.title' as const,
sortable: false,
},
{
name: 'user',
label: 'Пользователь',
align: 'left' as const,
field: 'username' as const,
sortable: true,
},
{
name: 'status',
label: 'Статус',
align: 'center' as const,
field: 'status' as const,
sortable: true,
},
{
name: 'hours',
label: 'Время',
align: 'right' as const,
field: 'amounts.creators_hours' as const,
sortable: true,
},
{
name: 'hour_rate',
label: 'Стоимость часа',
align: 'right' as const,
field: 'amounts.hour_cost' as const,
sortable: true,
},
{
name: 'total',
label: 'Сумма',
align: 'right' as const,
field: 'amounts.total_contribution' as const,
sortable: true,
},
{
name: 'actions',
label: 'Действия',
align: 'center' as const,
field: '' as const,
sortable: false,
},
];
</script>
<style lang="scss" scoped>
.projects-info {
.project-link {
display: block;
cursor: pointer;
margin-bottom: 4px;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.2s ease;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.project-title {
font-size: 0.875rem;
font-weight: 500;
word-wrap: break-word;
white-space: normal;
line-height: 1.3;
}
}
.component-link {
display: block;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
margin-left: 16px;
border-left: 2px solid #1976d2;
transition: background-color 0.2s ease;
&:hover {
background-color: rgba(25, 118, 210, 0.08);
}
.component-title {
font-size: 0.875rem;
word-wrap: break-word;
white-space: normal;
line-height: 1.3;
}
}
}
.stats-info {
display: flex;
justify-content: center;
align-items: center;
}
.commit-details {
.text-subtitle2 {
margin-bottom: 4px;
}
.text-body2 {
font-family: 'Courier New', monospace;
font-size: 0.875rem;
}
.commit-text {
font-family: 'Courier New', monospace;
font-size: 0.875rem;
word-break: break-all;
flex: 1;
a {
color: var(--q-primary);
text-decoration: underline;
&:hover {
text-decoration: none;
}
}
}
.commit-url {
color: var(--q-primary);
text-decoration: underline;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
&:hover {
text-decoration: none;
}
}
}
</style>