- Added new styles for buttons in the adult verification filters to enhance appearance and usability. - Set minimum height, padding, and font size for consistency across both views.
256 lines
8.7 KiB
Vue
256 lines
8.7 KiB
Vue
<template>
|
|
<div class="contenthidden">
|
|
<div class="contentscroll">
|
|
<div class="adult-verification">
|
|
<section class="adult-verification__hero surface-card">
|
|
<span class="adult-verification__eyebrow">Administration</span>
|
|
<h1>{{ $t('admin.eroticModeration.title') }}</h1>
|
|
<p>{{ $t('admin.eroticModeration.intro') }}</p>
|
|
</section>
|
|
|
|
<section class="adult-verification__filters surface-card">
|
|
<button
|
|
v-for="option in filterOptions"
|
|
:key="option.value"
|
|
type="button"
|
|
:class="{ active: statusFilter === option.value }"
|
|
@click="changeFilter(option.value)"
|
|
>
|
|
{{ option.label }}
|
|
</button>
|
|
</section>
|
|
|
|
<section class="adult-verification__list surface-card">
|
|
<div v-if="loading" class="adult-verification__state">{{ $t('general.loading') }}</div>
|
|
<div v-else-if="rows.length === 0" class="adult-verification__state">{{ $t('admin.eroticModeration.empty') }}</div>
|
|
<table v-else>
|
|
<thead>
|
|
<tr>
|
|
<th>{{ $t('admin.eroticModeration.target') }}</th>
|
|
<th>{{ $t('admin.eroticModeration.owner') }}</th>
|
|
<th>{{ $t('admin.eroticModeration.reporter') }}</th>
|
|
<th>{{ $t('admin.eroticModeration.reason') }}</th>
|
|
<th>{{ $t('admin.eroticModeration.statusLabel') }}</th>
|
|
<th>{{ $t('admin.eroticModeration.meta') }}</th>
|
|
<th>{{ $t('admin.eroticModeration.actions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="row in rows" :key="row.id">
|
|
<td class="adult-verification__request">
|
|
<strong>{{ row.targetType === 'image' ? $t('admin.eroticModeration.image') : $t('admin.eroticModeration.video') }}</strong>
|
|
<span>{{ row.target?.title || '—' }}</span>
|
|
<span v-if="row.target?.isModeratedHidden" class="adult-verification__badge adult-verification__badge--rejected">
|
|
{{ $t('admin.eroticModeration.hidden') }}
|
|
</span>
|
|
<button v-if="row.target" type="button" class="secondary" @click="previewTarget(row)">
|
|
{{ $t('admin.eroticModeration.preview') }}
|
|
</button>
|
|
</td>
|
|
<td>{{ row.owner?.username || '—' }}</td>
|
|
<td>{{ row.reporter?.username || '—' }}</td>
|
|
<td class="adult-verification__request">
|
|
<strong>{{ $t(`socialnetwork.erotic.reportReasons.${row.reason}`) }}</strong>
|
|
<span v-if="row.note">{{ row.note }}</span>
|
|
</td>
|
|
<td>
|
|
<span class="adult-verification__badge" :class="`adult-verification__badge--${row.status}`">
|
|
{{ $t(`admin.eroticModeration.status.${row.status}`) }}
|
|
</span>
|
|
</td>
|
|
<td class="adult-verification__request">
|
|
<span>{{ formatDate(row.createdAt) }}</span>
|
|
<span v-if="row.actionTaken">{{ $t(`admin.eroticModeration.actionLabels.${row.actionTaken}`) }}</span>
|
|
<span v-if="row.handledAt">{{ formatDate(row.handledAt) }}</span>
|
|
</td>
|
|
<td class="adult-verification__actions">
|
|
<button type="button" @click="applyAction(row, 'dismiss')">{{ $t('admin.eroticModeration.dismiss') }}</button>
|
|
<button type="button" class="secondary" @click="applyAction(row, 'hide_content')">{{ $t('admin.eroticModeration.hide') }}</button>
|
|
<button type="button" class="secondary" @click="applyAction(row, 'restore_content')">{{ $t('admin.eroticModeration.restore') }}</button>
|
|
<button type="button" class="secondary" @click="applyAction(row, 'delete_content')">{{ $t('admin.eroticModeration.delete') }}</button>
|
|
<button type="button" class="secondary" @click="applyAction(row, 'block_uploads')">{{ $t('admin.eroticModeration.blockUploads') }}</button>
|
|
<button type="button" class="secondary" @click="applyAction(row, 'revoke_access')">{{ $t('admin.eroticModeration.revokeAccess') }}</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import apiClient from '@/utils/axios.js';
|
|
import { showApiError, showSuccess } from '@/utils/feedback.js';
|
|
|
|
export default {
|
|
name: 'EroticModerationView',
|
|
data() {
|
|
return {
|
|
loading: false,
|
|
statusFilter: 'open',
|
|
rows: []
|
|
};
|
|
},
|
|
computed: {
|
|
filterOptions() {
|
|
return [
|
|
{ value: 'open', label: this.$t('admin.eroticModeration.filters.open') },
|
|
{ value: 'actioned', label: this.$t('admin.eroticModeration.filters.actioned') },
|
|
{ value: 'dismissed', label: this.$t('admin.eroticModeration.filters.dismissed') },
|
|
{ value: 'all', label: this.$t('admin.eroticModeration.filters.all') }
|
|
];
|
|
}
|
|
},
|
|
methods: {
|
|
async load() {
|
|
this.loading = true;
|
|
try {
|
|
const response = await apiClient.get('/api/admin/users/erotic-moderation', {
|
|
params: { status: this.statusFilter }
|
|
});
|
|
this.rows = response.data || [];
|
|
} catch (error) {
|
|
this.rows = [];
|
|
showApiError(this, error, this.$t('admin.eroticModeration.loadError'));
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
async changeFilter(filter) {
|
|
this.statusFilter = filter;
|
|
await this.load();
|
|
},
|
|
async applyAction(row, action) {
|
|
const note = window.prompt(this.$t('admin.eroticModeration.notePrompt'), row.note || '') || '';
|
|
try {
|
|
await apiClient.put(`/api/admin/users/erotic-moderation/${row.id}`, { action, note });
|
|
showSuccess(this, this.$t('admin.eroticModeration.actionSuccess'));
|
|
await this.load();
|
|
} catch (error) {
|
|
showApiError(this, error, this.$t('admin.eroticModeration.actionError'));
|
|
}
|
|
},
|
|
async previewTarget(row) {
|
|
try {
|
|
const response = await apiClient.get(`/api/admin/users/erotic-moderation/preview/${row.targetType}/${row.targetId}`, {
|
|
responseType: 'blob'
|
|
});
|
|
const url = window.URL.createObjectURL(response.data);
|
|
window.open(url, '_blank', 'noopener');
|
|
window.setTimeout(() => window.URL.revokeObjectURL(url), 10000);
|
|
} catch (error) {
|
|
showApiError(this, error, this.$t('admin.eroticModeration.previewError'));
|
|
}
|
|
},
|
|
formatDate(value) {
|
|
if (!value) return '—';
|
|
return new Intl.DateTimeFormat(undefined, {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short'
|
|
}).format(new Date(value));
|
|
}
|
|
},
|
|
async mounted() {
|
|
await this.load();
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
.adult-verification {
|
|
display: grid;
|
|
gap: 18px;
|
|
padding-bottom: 24px;
|
|
}
|
|
|
|
.adult-verification__hero,
|
|
.adult-verification__filters,
|
|
.adult-verification__list {
|
|
background: linear-gradient(180deg, rgba(255, 252, 247, 0.97), rgba(250, 244, 235, 0.95));
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-soft);
|
|
padding: 22px 24px;
|
|
}
|
|
|
|
.adult-verification__eyebrow {
|
|
display: inline-flex;
|
|
margin-bottom: 8px;
|
|
padding: 4px 10px;
|
|
border-radius: var(--radius-pill);
|
|
background: var(--color-secondary-soft);
|
|
color: var(--color-text-secondary);
|
|
font-size: 0.78rem;
|
|
font-weight: 700;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.adult-verification__hero p,
|
|
.adult-verification__state {
|
|
margin: 0;
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.adult-verification__filters {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.adult-verification__filters button.active {
|
|
background: var(--color-primary);
|
|
color: #fff;
|
|
}
|
|
|
|
.adult-verification__filters button {
|
|
min-height: 34px;
|
|
padding: 0 14px;
|
|
font-size: 0.92rem;
|
|
}
|
|
|
|
.adult-verification table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.adult-verification th,
|
|
.adult-verification td {
|
|
padding: 12px 10px;
|
|
border-bottom: 1px solid rgba(93, 64, 55, 0.08);
|
|
text-align: left;
|
|
}
|
|
|
|
.adult-verification__badge {
|
|
display: inline-flex;
|
|
padding: 4px 10px;
|
|
border-radius: var(--radius-pill);
|
|
font-weight: 700;
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.adult-verification__badge--open {
|
|
background: rgba(216, 167, 65, 0.16);
|
|
}
|
|
|
|
.adult-verification__badge--actioned {
|
|
background: rgba(92, 156, 106, 0.18);
|
|
}
|
|
|
|
.adult-verification__badge--dismissed {
|
|
background: rgba(176, 88, 88, 0.16);
|
|
}
|
|
|
|
.adult-verification__actions,
|
|
.adult-verification__request {
|
|
display: grid;
|
|
gap: 6px;
|
|
}
|
|
|
|
.adult-verification__actions .secondary {
|
|
background: transparent;
|
|
}
|
|
</style>
|