feat: add feedback review notification UI
This commit is contained in:
311
frontend/src/features/feedback/views/MyFeedbackListView.vue
Normal file
311
frontend/src/features/feedback/views/MyFeedbackListView.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { FEEDBACK_STATUSES, FEEDBACK_TYPES } from '@/features/feedback/stores/developerFeedbackStore.js';
|
||||
import { useMyFeedbackStore } from '@/features/feedback/stores/myFeedbackStore.js';
|
||||
import { mdiFilterOffOutline, mdiRefresh, mdiTagOutline } from '@mdi/js';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const feedbackStore = useMyFeedbackStore();
|
||||
|
||||
const sortOptions = computed(() => [
|
||||
{ title: t('feedback.review.sort.lastActivity'), value: 'lastActivity' },
|
||||
{ title: t('feedback.review.sort.newest'), value: 'newest' },
|
||||
]);
|
||||
|
||||
const summary = computed(() => ({
|
||||
active: feedbackStore.reports.filter(report => ['New', 'Planned'].includes(report.status)).length,
|
||||
unread: feedbackStore.reports.filter(report => feedbackStore.unreadReportIds.has(report.id)).length,
|
||||
visible: feedbackStore.filteredReports.length,
|
||||
}));
|
||||
|
||||
onMounted(() => {
|
||||
feedbackStore.loadReports();
|
||||
});
|
||||
|
||||
function openReport(report) {
|
||||
router.push({ name: 'my-feedback-detail', params: { id: report.id } });
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
return value ? new Date(value).toLocaleString() : t('feedback.review.emptyValue');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="my-feedback-page">
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<div class="eyebrow">{{ t('feedback.mine.eyebrow') }}</div>
|
||||
<h1>{{ t('feedback.mine.title') }}</h1>
|
||||
<p>{{ t('feedback.mine.description') }}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="icon-button"
|
||||
type="button"
|
||||
:title="t('feedback.mine.refresh')"
|
||||
@click="feedbackStore.loadReports"
|
||||
>
|
||||
<v-icon :icon="mdiRefresh" />
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<section class="metric-grid">
|
||||
<article class="metric">
|
||||
<span>{{ t('feedback.mine.metrics.active') }}</span>
|
||||
<strong>{{ summary.active }}</strong>
|
||||
</article>
|
||||
<article class="metric">
|
||||
<span>{{ t('feedback.mine.metrics.unread') }}</span>
|
||||
<strong>{{ summary.unread }}</strong>
|
||||
</article>
|
||||
<article class="metric">
|
||||
<span>{{ t('feedback.mine.metrics.visible') }}</span>
|
||||
<strong>{{ summary.visible }}</strong>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="filter-panel">
|
||||
<v-select
|
||||
v-model="feedbackStore.filters.type"
|
||||
:items="FEEDBACK_TYPES"
|
||||
:label="t('feedback.review.filters.type')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
/>
|
||||
|
||||
<v-select
|
||||
v-model="feedbackStore.filters.status"
|
||||
:items="FEEDBACK_STATUSES"
|
||||
:label="t('feedback.review.filters.status')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
clearable
|
||||
/>
|
||||
|
||||
<v-select
|
||||
v-model="feedbackStore.filters.sort"
|
||||
:items="sortOptions"
|
||||
:label="t('feedback.review.filters.sort')"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
hide-details
|
||||
/>
|
||||
|
||||
<button
|
||||
class="icon-button"
|
||||
type="button"
|
||||
:title="t('feedback.review.filters.clear')"
|
||||
@click="feedbackStore.resetFilters"
|
||||
>
|
||||
<v-icon :icon="mdiFilterOffOutline" />
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<div
|
||||
v-if="feedbackStore.isLoading"
|
||||
class="page-message"
|
||||
>
|
||||
{{ t('feedback.review.loading') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="feedbackStore.error"
|
||||
class="page-message page-message-error"
|
||||
>
|
||||
{{ t(feedbackStore.error) }}
|
||||
</div>
|
||||
|
||||
<section
|
||||
v-else
|
||||
class="report-list"
|
||||
>
|
||||
<button
|
||||
v-for="report in feedbackStore.filteredReports"
|
||||
:key="report.id"
|
||||
class="report-row"
|
||||
:class="{ 'report-row-unread': feedbackStore.unreadReportIds.has(report.id) }"
|
||||
type="button"
|
||||
@click="openReport(report)"
|
||||
>
|
||||
<span
|
||||
v-if="feedbackStore.unreadReportIds.has(report.id)"
|
||||
class="unread-dot"
|
||||
:title="t('feedback.mine.unread')"
|
||||
></span>
|
||||
<span class="report-main">
|
||||
<span class="report-title">
|
||||
<strong>{{ report.type }}</strong>
|
||||
<em>{{ report.status }}</em>
|
||||
</span>
|
||||
<span class="report-description">{{ report.description }}</span>
|
||||
<span class="report-tags">
|
||||
<span
|
||||
v-for="tag in report.tags"
|
||||
:key="tag"
|
||||
>
|
||||
<v-icon :icon="mdiTagOutline" />
|
||||
{{ tag }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<span class="report-activity">
|
||||
<span>{{ t('feedback.review.lastActivity') }}</span>
|
||||
<strong>{{ formatDate(report.lastActivityAt) }}</strong>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="!feedbackStore.filteredReports.length"
|
||||
class="page-message"
|
||||
>
|
||||
{{ t('feedback.mine.empty') }}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.my-feedback-page {
|
||||
@apply mx-auto flex w-full max-w-6xl flex-col gap-5 px-5 py-8 md:px-8;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
@apply flex flex-col justify-between gap-4 md:flex-row md:items-start;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
@apply text-xs font-bold uppercase tracking-[0.22em];
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
@apply mt-2 text-3xl font-black md:text-4xl;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
@apply mt-2 max-w-3xl text-sm leading-6;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
@apply grid gap-3 md:grid-cols-3;
|
||||
}
|
||||
|
||||
.metric,
|
||||
.filter-panel,
|
||||
.report-row,
|
||||
.page-message {
|
||||
@apply rounded-lg border;
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.metric {
|
||||
@apply p-4;
|
||||
}
|
||||
|
||||
.metric span {
|
||||
@apply text-xs font-bold uppercase tracking-[0.16em];
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.metric strong {
|
||||
@apply mt-2 block text-3xl font-black;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.filter-panel {
|
||||
@apply grid gap-3 p-4 md:grid-cols-[repeat(3,minmax(10rem,1fr))_auto];
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
@apply inline-flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-lg border transition-colors;
|
||||
border-color: rgba(23, 32, 51, 0.12);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
background: #172033;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.report-list {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.report-row {
|
||||
@apply grid gap-4 p-4 text-left transition-colors md:grid-cols-[auto_minmax(0,1fr)_minmax(12rem,0.35fr)] md:items-center;
|
||||
}
|
||||
|
||||
.report-row:hover,
|
||||
.report-row-unread {
|
||||
border-color: rgba(15, 118, 110, 0.36);
|
||||
background: white;
|
||||
}
|
||||
|
||||
.unread-dot {
|
||||
@apply h-2.5 w-2.5 rounded-full;
|
||||
background: #0f766e;
|
||||
}
|
||||
|
||||
.report-main,
|
||||
.report-activity {
|
||||
@apply flex min-w-0 flex-col gap-1;
|
||||
}
|
||||
|
||||
.report-title {
|
||||
@apply flex flex-wrap items-center gap-2;
|
||||
}
|
||||
|
||||
.report-title strong,
|
||||
.report-activity strong {
|
||||
@apply text-sm font-black;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.report-title em {
|
||||
@apply rounded-md px-2 py-1 text-xs font-bold not-italic;
|
||||
background: rgba(15, 118, 110, 0.08);
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.report-description {
|
||||
@apply line-clamp-2 text-sm leading-6;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.report-tags {
|
||||
@apply mt-1 flex flex-wrap gap-1.5;
|
||||
}
|
||||
|
||||
.report-tags span {
|
||||
@apply inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-semibold;
|
||||
background: rgba(23, 32, 51, 0.06);
|
||||
color: #44516a;
|
||||
}
|
||||
|
||||
.report-activity span {
|
||||
@apply text-xs;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.page-message {
|
||||
@apply p-4 text-sm font-semibold;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.page-message-error {
|
||||
border-color: rgba(220, 38, 38, 0.24);
|
||||
color: #b91c1c;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user