feat(rating): 结束服务、星级评价与管理员回看统计
结束会话后可提交 1-5 星与文字评价,重复提交更新原记录;管理端回看接待人与评分。
This commit is contained in:
1 parent
ad90b830c4
commit
2adf288cd2
8 files changed
+3815
No files matched your search
@@ -0,0 +1,702 @@
|
||||
<!--
|
||||
Copyright 2024-2026 ChatterMate
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="sentiment-analytics-container">
|
||||
<div v-if="error" class="error-state">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="isLoading" class="loading-state">
|
||||
正在加载情感分析数据…
|
||||
</div>
|
||||
|
||||
<div v-else-if="!sentimentData?.total_analyzed" class="no-data">
|
||||
所选时间段暂无情感分析数据
|
||||
</div>
|
||||
|
||||
<div v-else class="sentiment-analytics-content">
|
||||
<!-- Summary Cards -->
|
||||
<div class="metrics-overview">
|
||||
<div class="metric-card">
|
||||
<h3>已分析消息数</h3>
|
||||
<div class="metric-value">{{ sentimentData.total_analyzed }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<h3>平均情感分</h3>
|
||||
<div class="metric-value">
|
||||
{{ sentimentData.avg_score.toFixed(2) }}
|
||||
<span class="change" :class="{ positive: sentimentData.score_change >= 0 }">
|
||||
{{ sentimentData.score_change >= 0 ? '+' : '' }}{{ sentimentData.score_change.toFixed(1) }}%
|
||||
<i :class="sentimentData.score_trend === 'up' ? 'trend-up' : 'trend-down'"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="metric-sub">{{ scoreLabel(sentimentData.avg_score) }}</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<h3>正面</h3>
|
||||
<div class="metric-value positive-text">{{ sentimentData.distribution.positive }}</div>
|
||||
<div class="metric-sub">占总量 {{ percentOf(sentimentData.distribution.positive) }}%</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<h3>负面</h3>
|
||||
<div class="metric-value negative-text">{{ sentimentData.distribution.negative }}</div>
|
||||
<div class="metric-sub">占总量 {{ percentOf(sentimentData.distribution.negative) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Charts -->
|
||||
<div class="charts-grid">
|
||||
<div class="chart-container">
|
||||
<h3>情感分布</h3>
|
||||
<div v-if="!hasDistribution" class="no-data">
|
||||
暂无分布数据
|
||||
</div>
|
||||
<apexchart
|
||||
v-else
|
||||
type="donut"
|
||||
height="300"
|
||||
:options="distributionOptions"
|
||||
:series="distributionSeries"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<h3>情感趋势</h3>
|
||||
<div v-if="!sentimentData.trend?.data?.length" class="no-data">
|
||||
暂无趋势数据
|
||||
</div>
|
||||
<apexchart
|
||||
v-else
|
||||
type="area"
|
||||
height="300"
|
||||
:options="trendOptions"
|
||||
:series="trendSeries"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Negative Sessions Needing Attention -->
|
||||
<div class="negative-sessions-section">
|
||||
<h3>需关注的会话</h3>
|
||||
<div v-if="!sentimentData.negative_sessions?.length" class="no-data small">
|
||||
该时间段没有负面情感会话 🎉
|
||||
</div>
|
||||
<table v-else class="sentiment-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>会话</th>
|
||||
<th>情感</th>
|
||||
<th>分值</th>
|
||||
<th>状态</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="session in sentimentData.negative_sessions" :key="session.session_id">
|
||||
<td>
|
||||
<router-link
|
||||
class="session-link"
|
||||
:to="{ name: 'conversations', query: { session: session.session_id, status: session.status } }"
|
||||
title="查看会话记录"
|
||||
>
|
||||
{{ shortId(session.session_id) }}
|
||||
</router-link>
|
||||
</td>
|
||||
<td>
|
||||
<span class="sentiment-badge" :class="session.sentiment_label">
|
||||
{{ session.sentiment_label }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ session.sentiment_score !== null ? session.sentiment_score.toFixed(2) : 'N/A' }}</td>
|
||||
<td><span class="status-text">{{ session.status }}</span></td>
|
||||
<td>
|
||||
<button class="view-details-btn" @click="showSessionDetails(session.session_id)">
|
||||
查看详情
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Session Detail Modal -->
|
||||
<div v-if="selectedSession" class="session-details-modal" @click.self="selectedSession = null">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>会话情感明细</h3>
|
||||
<button class="close-btn" @click="selectedSession = null">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="session-overall">
|
||||
<span class="overall-label">整体:</span>
|
||||
<span class="sentiment-badge" :class="selectedSession.overall_sentiment.label || 'neutral'">
|
||||
{{ selectedSession.overall_sentiment.label || 'N/A' }}
|
||||
</span>
|
||||
<span v-if="selectedSession.overall_sentiment.score !== null" class="overall-score">
|
||||
({{ selectedSession.overall_sentiment.score.toFixed(2) }})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="detailLoading" class="loading-state">正在加载消息…</div>
|
||||
<div v-else-if="!selectedSession.messages?.length" class="no-data small">
|
||||
该会话暂无已分析的客户消息
|
||||
</div>
|
||||
<div v-else class="message-list">
|
||||
<div
|
||||
v-for="msg in selectedSession.messages"
|
||||
:key="msg.id"
|
||||
class="message-item"
|
||||
:class="msg.sentiment_label"
|
||||
>
|
||||
<div class="message-header">
|
||||
<span class="sentiment-badge" :class="msg.sentiment_label">
|
||||
{{ msg.sentiment_label }}
|
||||
</span>
|
||||
<span class="message-score">
|
||||
{{ msg.sentiment_score !== null ? msg.sentiment_score.toFixed(2) : '' }}
|
||||
</span>
|
||||
<span class="message-date">{{ formatDate(msg.created_at) }}</span>
|
||||
</div>
|
||||
<p class="message-text">{{ msg.message || '无内容' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import api from '@/services/api'
|
||||
|
||||
interface SentimentDistribution {
|
||||
positive: number
|
||||
neutral: number
|
||||
negative: number
|
||||
}
|
||||
|
||||
interface SentimentTrend {
|
||||
data: number[]
|
||||
labels: string[]
|
||||
message_counts: number[]
|
||||
}
|
||||
|
||||
interface NegativeSession {
|
||||
session_id: string
|
||||
sentiment_label: string
|
||||
sentiment_score: number | null
|
||||
status: string
|
||||
}
|
||||
|
||||
interface SentimentData {
|
||||
distribution: SentimentDistribution
|
||||
total_analyzed: number
|
||||
avg_score: number
|
||||
score_change: number
|
||||
score_trend: 'up' | 'down'
|
||||
trend: SentimentTrend
|
||||
negative_sessions: NegativeSession[]
|
||||
time_range: string
|
||||
}
|
||||
|
||||
interface SessionMessage {
|
||||
id: number
|
||||
message: string | null
|
||||
sentiment_label: string
|
||||
sentiment_score: number | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface SessionDetail {
|
||||
session_id: string
|
||||
overall_sentiment: { label: string | null; score: number | null }
|
||||
messages: SessionMessage[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
timeRange: string
|
||||
}>()
|
||||
|
||||
const isLoading = ref(true)
|
||||
const error = ref<string | null>(null)
|
||||
const sentimentData = ref<SentimentData | null>(null)
|
||||
|
||||
const selectedSession = ref<SessionDetail | null>(null)
|
||||
const detailLoading = ref(false)
|
||||
|
||||
// Theme-ish colors for the three sentiment classes
|
||||
const COLOR_POSITIVE = '#16a34a'
|
||||
const COLOR_NEUTRAL = '#9ca3af'
|
||||
const COLOR_NEGATIVE = '#ef4444'
|
||||
|
||||
const hasDistribution = computed(() => {
|
||||
const d = sentimentData.value?.distribution
|
||||
return !!d && (d.positive + d.neutral + d.negative) > 0
|
||||
})
|
||||
|
||||
const distributionSeries = computed(() => {
|
||||
const d = sentimentData.value?.distribution
|
||||
if (!d) return []
|
||||
return [d.positive, d.neutral, d.negative]
|
||||
})
|
||||
|
||||
const distributionOptions = computed(() => ({
|
||||
chart: { type: 'donut', toolbar: { show: false } },
|
||||
labels: ['正面', '中性', '负面'],
|
||||
colors: [COLOR_POSITIVE, COLOR_NEUTRAL, COLOR_NEGATIVE],
|
||||
legend: { position: 'bottom' },
|
||||
dataLabels: { enabled: true, formatter: (val: number) => `${val.toFixed(0)}%` },
|
||||
stroke: { width: 0 },
|
||||
plotOptions: {
|
||||
pie: {
|
||||
donut: {
|
||||
labels: {
|
||||
show: true,
|
||||
total: { show: true, label: '总计', formatter: () => `${sentimentData.value?.total_analyzed || 0}` }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
const trendSeries = computed(() => {
|
||||
const t = sentimentData.value?.trend
|
||||
if (!t?.data) return []
|
||||
return [{
|
||||
name: '平均情感分',
|
||||
data: t.data.map((value, index) => ({
|
||||
x: new Date(t.labels[index]).getTime(),
|
||||
y: value
|
||||
}))
|
||||
}]
|
||||
})
|
||||
|
||||
const trendOptions = computed(() => ({
|
||||
chart: { type: 'area', toolbar: { show: false }, zoom: { enabled: false } },
|
||||
colors: ['#6366f1'],
|
||||
fill: {
|
||||
type: 'gradient',
|
||||
gradient: { shadeIntensity: 1, opacityFrom: 0.6, opacityTo: 0.1, stops: [0, 90, 100] }
|
||||
},
|
||||
dataLabels: { enabled: false },
|
||||
stroke: { curve: 'smooth', width: 2 },
|
||||
xaxis: { type: 'datetime' },
|
||||
yaxis: {
|
||||
min: -1,
|
||||
max: 1,
|
||||
tickAmount: 4,
|
||||
labels: { formatter: (val: number) => val.toFixed(1) }
|
||||
},
|
||||
annotations: {
|
||||
yaxis: [{ y: 0, borderColor: '#9ca3af', strokeDashArray: 4 }]
|
||||
},
|
||||
tooltip: { x: { format: 'MMM dd, yyyy' } }
|
||||
}))
|
||||
|
||||
const percentOf = (count: number): string => {
|
||||
const total = sentimentData.value?.total_analyzed || 0
|
||||
if (!total) return '0'
|
||||
return ((count / total) * 100).toFixed(0)
|
||||
}
|
||||
|
||||
const scoreLabel = (score: number): string => {
|
||||
if (score > 0.1) return '正面'
|
||||
if (score < -0.1) return '负面'
|
||||
return '中性'
|
||||
}
|
||||
|
||||
const shortId = (id: string): string => id.slice(0, 8)
|
||||
|
||||
const formatDate = (dateString: string): string => {
|
||||
if (!dateString) return 'N/A'
|
||||
return new Date(dateString).toLocaleString()
|
||||
}
|
||||
|
||||
const fetchSentimentData = async () => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
error.value = null
|
||||
const response = await api.get('/analytics/sentiment', {
|
||||
params: { time_range: props.timeRange }
|
||||
})
|
||||
sentimentData.value = response.data
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || '获取情感分析数据失败'
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const showSessionDetails = async (sessionId: string) => {
|
||||
try {
|
||||
detailLoading.value = true
|
||||
selectedSession.value = {
|
||||
session_id: sessionId,
|
||||
overall_sentiment: { label: null, score: null },
|
||||
messages: []
|
||||
}
|
||||
const response = await api.get(`/analytics/session-sentiment/${sessionId}`)
|
||||
selectedSession.value = response.data
|
||||
} catch (err: any) {
|
||||
error.value = err.response?.data?.detail || '获取会话情感数据失败'
|
||||
selectedSession.value = null
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.timeRange, () => {
|
||||
fetchSentimentData()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
fetchSentimentData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sentiment-analytics-container {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.metrics-overview {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: var(--space-lg);
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
background: var(--background-soft);
|
||||
padding: var(--space-lg);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.metric-card h3 {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-sm);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.metric-value.positive-text {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.metric-value.negative-text {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.metric-sub {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.change {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--error-color);
|
||||
}
|
||||
|
||||
.change.positive {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.trend-up, .trend-down {
|
||||
display: inline-block;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin-left: 4px;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
}
|
||||
|
||||
.trend-up {
|
||||
border-bottom: 4px solid currentColor;
|
||||
}
|
||||
|
||||
.trend-down {
|
||||
border-top: 4px solid currentColor;
|
||||
}
|
||||
|
||||
.charts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: var(--space-lg);
|
||||
margin-bottom: var(--space-xl);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
padding: var(--space-lg);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
background: var(--background-soft);
|
||||
}
|
||||
|
||||
.chart-container h3 {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-sm);
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.negative-sessions-section {
|
||||
background: var(--background-soft);
|
||||
padding: var(--space-lg);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.negative-sessions-section h3 {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-sm);
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.sentiment-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.sentiment-table th,
|
||||
.sentiment-table td {
|
||||
padding: var(--space-md);
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sentiment-table th {
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.sentiment-table td {
|
||||
font-size: var(--text-md);
|
||||
}
|
||||
|
||||
.session-link {
|
||||
font-family: monospace;
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.session-link:hover {
|
||||
border-bottom-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.status-text {
|
||||
text-transform: capitalize;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
.sentiment-badge {
|
||||
display: inline-block;
|
||||
padding: 2px var(--space-sm);
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.sentiment-badge.positive {
|
||||
background-color: rgba(22, 163, 74, 0.12);
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.sentiment-badge.neutral {
|
||||
background-color: rgba(156, 163, 175, 0.18);
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.sentiment-badge.negative {
|
||||
background-color: rgba(239, 68, 68, 0.12);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.view-details-btn {
|
||||
background-color: var(--accent-solid);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: var(--text-sm);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.view-details-btn:hover {
|
||||
background-color: var(--accent-solid);
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.session-details-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--background-color);
|
||||
border-radius: var(--radius-lg);
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow-lg);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: var(--space-lg);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: var(--text-xl);
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: var(--text-xl);
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
.session-overall {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.overall-label {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.overall-score {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.message-item {
|
||||
background: var(--background-soft);
|
||||
padding: var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
border-left: 3px solid var(--border-color);
|
||||
}
|
||||
|
||||
.message-item.positive {
|
||||
border-left-color: #16a34a;
|
||||
}
|
||||
|
||||
.message-item.neutral {
|
||||
border-left-color: #9ca3af;
|
||||
}
|
||||
|
||||
.message-item.negative {
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.message-score {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.message-date {
|
||||
margin-left: auto;
|
||||
font-size: var(--text-xs);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.message-text {
|
||||
margin: 0;
|
||||
font-size: var(--text-md);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.error-state {
|
||||
background-color: var(--error-color);
|
||||
color: white;
|
||||
padding: var(--space-md);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: var(--space-lg);
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.no-data {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 250px;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--text-md);
|
||||
}
|
||||
|
||||
.no-data.small {
|
||||
height: 80px;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user