Files
ruoyi-vue/src/views/public/CommentGenerator.vue
2026-01-17 22:34:32 +08:00

1256 lines
30 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="mobile-container">
<div class="mobile-card">
<div class="mobile-form">
<!-- 访问统计区域 -->
<div class="form-section usage-statistics-section">
<div class="usage-stats-row">
<div class="usage-stat-item">
<div class="usage-stat-label">今天</div>
<div class="usage-stat-number">{{ usageStatistics.today || 0 }}</div>
</div>
<div class="usage-stat-item">
<div class="usage-stat-label">近7天</div>
<div class="usage-stat-number">{{ usageStatistics.last7Days || 0 }}</div>
</div>
<div class="usage-stat-item">
<div class="usage-stat-label">近30天</div>
<div class="usage-stat-number">{{ usageStatistics.last30Days || 0 }}</div>
</div>
<div class="usage-stat-item">
<div class="usage-stat-label">累计</div>
<div class="usage-stat-number">{{ usageStatistics.total || 0 }}</div>
</div>
</div>
</div>
<!-- 产品类型选择区域 -->
<div class="form-section">
<div class="form-label">型号/类型</div>
<div class="word-sea">
<div v-if="Object.keys(groupedByLetter).length === 0" class="empty-hint">暂无数据</div>
<div v-else v-for="(items, ltr) in groupedByLetter" :key="ltr" class="group">
<div class="group-head">{{ ltr }}</div>
<div class="group-items">
<span
v-for="it in items"
:key="it.name"
class="item-tag"
:class="{ active: form.productType === it.name }"
@click="selectType(it)"
>{{ it.name }}</span>
</div>
</div>
</div>
</div>
<!-- 生成按钮区域 -->
<div class="form-section">
<el-button
type="primary"
size="large"
class="generate-btn"
:class="{ 'disabled-btn': isGenerateButtonDisabled }"
@click="generate"
:loading="loading"
:disabled="isGenerateButtonDisabled"
icon="el-icon-magic-stick"
>
{{ getButtonText() }}
</el-button>
<!-- 冷却时间提示 -->
<div v-if="remainingCooldownTime > 0 && !loading" class="cooldown-tip">
<i class="el-icon-time"></i>
请等待 {{ remainingCooldownTime }} 秒后再试
</div>
<!-- 按钮状态说明 -->
<div v-if="isButtonDisabled && !loading && remainingCooldownTime === 0" class="button-tip">
<i class="el-icon-warning"></i>
按钮已暂时禁用请稍后重试
</div>
</div>
<!-- 结果展示区域 -->
<div class="form-section">
<div class="form-label">生成结果</div>
<div v-if="comments.length" class="comments-container">
<div v-for="(c, idx) in comments" :key="idx" class="comment-card">
<div class="comment-content">
<div class="comment-text">{{ c.commentText }}</div>
<!-- 图片展示 -->
<div class="image-gallery" v-if="Array.isArray(c.images) && c.images.length">
<div class="image-grid">
<el-image
v-for="(img, ix) in c.images"
:key="ix"
:src="img"
:preview-src-list="c.images"
fit="cover"
class="gallery-image"
lazy
/>
</div>
</div>
<!-- 操作按钮 -->
<div class="comment-actions">
<el-button
size="small"
type="success"
@click="copy(c.commentText)"
icon="el-icon-document-copy"
class="copy-btn"
>
复制文本
</el-button>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<i class="el-icon-document-copy empty-icon"></i>
<p>暂无评论数据</p>
<p class="empty-tip">请选择型号/类型后点击生成评论</p>
</div>
</div>
<!-- 统计信息展示区域 -->
<div v-if="statistics" class="form-section statistics-section">
<div class="form-label">
<i class="el-icon-data-analysis"></i>
评论统计信息
</div>
<div class="statistics-card">
<div class="statistics-header">
<div class="statistics-source">
<span class="source-tag" :class="statistics.source === '京东评论' ? 'jd-tag' : 'tb-tag'">
{{ statistics.source }}
</span>
<span class="product-type">{{ statistics.productType }}</span>
</div>
<div v-if="statistics.lastCommentUpdateTime" class="update-time">
<i class="el-icon-time"></i>
更新日期{{ formatTime(statistics.lastCommentUpdateTime) }}
</div>
</div>
<div class="statistics-content">
<div class="stats-grid">
<div class="stat-item">
<div class="stat-number">{{ statistics.total || 0 }}</div>
<div class="stat-label">总数</div>
</div>
<div class="stat-item">
<div class="stat-number available">{{ statistics.available || 0 }}</div>
<div class="stat-label">可用</div>
</div>
<div class="stat-item">
<div class="stat-number used">{{ statistics.used || 0 }}</div>
<div class="stat-label">已使用</div>
</div>
<div v-if="statistics.newAdded !== undefined" class="stat-item">
<div class="stat-number new-added">{{ statistics.newAdded || 0 }}</div>
<div class="stat-label">新增</div>
</div>
</div>
<!-- 进度条显示使用率 -->
<div class="usage-progress">
<div class="progress-info">
<span>使用率</span>
<span>{{ getUsagePercentage() }}%</span>
</div>
<div class="progress-bar">
<div class="progress-fill" :style="{ width: getUsagePercentage() + '%' }"></div>
</div>
</div>
<!-- 详细统计文本 -->
<div class="statistics-text">
<pre>{{ statistics.statisticsText }}</pre>
</div>
</div>
</div>
</div>
<!-- 历史记录区域 -->
<div class="form-section history-section">
<div class="form-label">
<i class="el-icon-time"></i>
历史记录
<el-button
size="mini"
type="text"
icon="el-icon-refresh"
@click="loadHistory"
:loading="historyLoading"
style="margin-left: 8px;"
>
刷新
</el-button>
</div>
<div v-if="historyLoading" class="history-loading">
<i class="el-icon-loading"></i> 加载中...
</div>
<div v-else-if="historyList.length === 0" class="history-empty">
<i class="el-icon-document-remove"></i>
<p>暂无历史记录</p>
</div>
<div v-else class="history-list">
<div
v-for="(item, idx) in historyList"
:key="idx"
class="history-item"
>
<div class="history-item-header">
<span class="history-type">{{ item.productType || '-' }}</span>
<span class="history-time">{{ formatTime(item.createTime) }}</span>
</div>
<div class="history-item-info">
<span class="history-ip">IP: {{ item.ip || '-' }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 页尾导航 -->
<PublicFooterNav />
</div>
</template>
<script>
import { pinyin } from 'pinyin-pro'
import PublicFooterNav from '@/components/PublicFooterNav'
import { parseTime } from '@/utils/ruoyi'
export default {
name: 'CommentGeneratorPublic',
components: {
PublicFooterNav
},
data() {
return {
form: { productType: '' },
loading: false,
result: null,
typeOptions: [],
comments: [],
statistics: null,
lastGenerateTime: 0,
cooldownTime: 1000, // 5秒冷却时间
isButtonDisabled: false,
currentIP: '',
usageStatistics: {
today: 0,
last7Days: 0,
last30Days: 0,
total: 0
},
historyList: [],
historyLoading: false
}
},
computed: {
pretty() {
try { return this.result ? JSON.stringify(this.result, null, 2) : '' } catch(e) { return '' }
},
groupedByLetter() {
const groups = {}
const items = Array.isArray(this.typeOptions) ? this.typeOptions.slice() : []
items.forEach(it => {
const ltr = this.getInitial(it)
if (!groups[ltr]) groups[ltr] = []
groups[ltr].push(it)
})
Object.keys(groups).forEach(k => {
groups[k].sort((a, b) => {
const an = (a.name || '').toString()
const bn = (b.name || '').toString()
return an.localeCompare(bn)
})
})
// 按字母顺序排序,确保显示顺序一致
const ordered = {}
const letters = Array.from('ABCDEFGHIJKLMNOPQRSTUVWXYZ')
letters.concat('#').forEach(l => {
if (groups[l] && groups[l].length) {
ordered[l] = groups[l]
}
})
// 如果有其他字母不在A-Z范围内也添加进去
Object.keys(groups).forEach(k => {
if (!ordered[k] && groups[k].length) {
ordered[k] = groups[k]
}
})
return ordered
},
isGenerateButtonDisabled() {
// 如果正在加载、手动禁用、没有选择产品类型,或者在冷却时间内,则禁用按钮
return this.loading ||
this.isButtonDisabled ||
!this.form.productType ||
(Date.now() - this.lastGenerateTime < this.cooldownTime && this.lastGenerateTime > 0)
},
remainingCooldownTime() {
if (this.lastGenerateTime === 0) return 0
const remaining = Math.max(0, this.cooldownTime - (Date.now() - this.lastGenerateTime))
return Math.ceil(remaining / 1000)
}
},
mounted() {
this.loadTypes()
this.loadCurrentIP()
this.loadUsageStatistics()
this.loadHistory()
// 启动定时器更新冷却时间显示
this.cooldownTimer = setInterval(() => {
// 检查倒计时是否结束如果结束则清空lastGenerateTime
if (this.lastGenerateTime > 0) {
const remaining = this.cooldownTime - (Date.now() - this.lastGenerateTime)
if (remaining <= 0) {
this.lastGenerateTime = 0
}
}
// 强制更新计算属性
this.$forceUpdate()
}, 1000)
},
beforeDestroy() {
if (this.cooldownTimer) {
clearInterval(this.cooldownTimer)
}
},
methods: {
async loadCurrentIP() {
try {
const res = await this.$axios({ url: '/public/comment/ip', method: 'get' })
if (res && (res.code === 200 || res.msg === '操作成功')) {
this.currentIP = res.data?.ip || res.data || '获取失败'
}
} catch(e) {
console.error('获取IP失败:', e)
this.currentIP = '获取失败'
}
},
async loadUsageStatistics() {
try {
const res = await this.$axios({ url: '/public/comment/usage-statistics', method: 'get' })
if (res && (res.code === 200 || res.msg === '操作成功')) {
this.usageStatistics = {
today: res.data?.today || 0,
last7Days: res.data?.last7Days || res.data?.last7days || 0,
last30Days: res.data?.last30Days || res.data?.last30days || 0,
total: res.data?.total || 0
}
}
} catch(e) {
console.error('获取使用统计失败:', e)
}
},
async loadHistory() {
this.historyLoading = true
try {
const res = await this.$axios({
url: '/public/comment/history',
method: 'get',
params: { pageNum: 1, pageSize: 20 }
})
if (res && (res.code === 200 || res.msg === '操作成功')) {
const list = res.data?.rows || res.data?.list || res.data || []
this.historyList = Array.isArray(list) ? list : []
}
} catch(e) {
console.error('获取历史记录失败:', e)
this.historyList = []
} finally {
this.historyLoading = false
}
},
formatTime(time) {
if (!time) return '-'
return parseTime(time, '{y}-{m}-{d} {h}:{i}:{s}')
},
async loadTypes() {
try {
const res = await this.$axios({ url: '/public/comment/types', method: 'get' })
if (res && (res.code === 200 || res.msg === '操作成功')) {
// 后端返回 [{name,value}] 或纯数组,这里统一转成 [{name}]
const arr = Array.isArray(res.data) ? res.data : []
this.typeOptions = arr.map(it => ({ name: it.name || it }))
}
} catch(e) {}
},
getInitial(it) {
const source = (it && (it.name || it.value) || '').toString().trim()
if (!source) return '#'
const firstChar = source[0]
const upperAscii = firstChar.toUpperCase()
if (upperAscii >= 'A' && upperAscii <= 'Z') return upperAscii
// 中文等非 ASCII 字符,取拼音首字母
try {
const letter = pinyin(firstChar, { toneType: 'none', type: 'array' })[0]
if (!letter || !letter.length) return '#'
const initial = letter[0].toUpperCase()
return (initial >= 'A' && initial <= 'Z') ? initial : '#'
} catch(e) {
return '#'
}
},
selectType(it) {
if (!it) return
// 如果选择的是同一个型号,不重复提交
if (this.form.productType === it.name) {
return
}
this.form.productType = it.name
// 自动提交请求获取评论
this.$nextTick(() => {
this.generate()
})
},
async generate() {
// 检查按钮是否被禁用
if (this.isGenerateButtonDisabled) {
if (this.remainingCooldownTime > 0) {
this.$message.warning(`请等待 ${this.remainingCooldownTime} 秒后再试`)
} else if (!this.form.productType) {
this.$message.error('请选择型号')
}
return
}
// 记录点击时间
this.lastGenerateTime = Date.now()
this.loading = true
this.isButtonDisabled = false
try {
const res = await this.$axios({
url: '/public/comment/generate',
method: 'post',
data: { productType: this.form.productType },
timeout: 30000 // 30秒超时
})
this.loading = false
if (res && (res.code === 200 || res.msg === '操作成功')) {
this.result = res.data
// 检查是否有有效数据
const list = (res.data && res.data.list && Array.isArray(res.data.list)) ? res.data.list : []
if (list.length === 0) {
this.$message.warning('接口返回数据为空,请稍后重试')
this.isButtonDisabled = true
// 10秒后重新启用按钮
setTimeout(() => {
this.isButtonDisabled = false
}, 10000)
return
}
// 结果渲染
this.comments = list.map(it => ({
commentText: (it && it.commentText) ? it.commentText : '',
images: Array.isArray(it && it.images) ? it.images : []
}))
// 解析统计信息
this.statistics = res.data && res.data.statistics ? res.data.statistics : null
// 刷新使用统计和历史记录
this.loadUsageStatistics()
this.loadHistory()
this.$message.success('评论生成成功')
} else {
this.$message.error(res && res.msg ? res.msg : '生成失败')
this.isButtonDisabled = true
// 5秒后重新启用按钮
setTimeout(() => {
this.isButtonDisabled = false
}, 5000)
}
} catch(e) {
this.loading = false
console.error('生成评论失败:', e)
if (e.code === 'ECONNABORTED') {
this.$message.error('请求超时,请检查网络连接')
} else {
this.$message.error('生成失败,请稍后重试')
}
this.isButtonDisabled = true
// 5秒后重新启用按钮
setTimeout(() => {
this.isButtonDisabled = false
}, 5000)
}
},
copy(text) {
if (!text) return
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(() => this.$message.success('已复制'))
} else {
const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select();
try { document.execCommand('copy'); this.$message.success('已复制') } catch(e) { this.$message.error('复制失败') }
document.body.removeChild(ta)
}
},
getUsagePercentage() {
if (!this.statistics || !this.statistics.total || this.statistics.total === 0) {
return 0
}
const used = this.statistics.used || 0
const total = this.statistics.total
return Math.round((used / total) * 100)
},
getButtonText() {
if (this.loading) {
return '生成中...'
}
if (this.remainingCooldownTime > 0) {
return `等待中 (${this.remainingCooldownTime}s)`
}
if (this.isButtonDisabled) {
return '暂时禁用'
}
if (!this.form.productType) {
return '请先选择型号'
}
return '生成评论'
}
}
}
</script>
<style scoped>
/* 移动端容器样式 */
.mobile-container {
min-height: 100vh;
background: #f5f5f5;
padding: 16px;
padding-bottom: calc(80px + 16px); /* 为页尾导航留出空间 */
}
.mobile-card {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
/* 表单样式 */
.mobile-form {
padding: 12px 16px;
}
.form-section {
margin-bottom: 16px;
}
.form-label {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 6px;
}
.form-label i {
font-size: 16px;
color: #409eff;
}
/* 选择器行样式 */
.select-row {
display: flex;
gap: 12px;
align-items: center;
}
.mobile-select {
flex: 1;
min-width: 0;
}
/* 词海分组样式 */
.word-sea .group { margin-bottom: 12px; }
.word-sea .group-head { font-weight: 600; margin: 6px 0; color: #606266; }
.word-sea .group-items { display: flex; flex-wrap: wrap; }
.word-sea .item-tag {
display: inline-block;
padding: 8px 14px;
margin: 4px 8px 4px 0;
border: 1px solid #dcdfe6;
border-radius: 16px;
cursor: pointer;
color: #606266;
user-select: none;
transition: all .15s ease;
background: #fff;
font-size: 14px;
}
.word-sea .item-tag:hover { border-color: #409eff; color: #409eff; }
.word-sea .item-tag.active { background: #409eff; border-color: #409eff; color: #fff; }
.word-sea .empty-hint { color: #909399; }
/* 生成按钮样式 */
.generate-btn {
width: 100%;
height: 48px;
border-radius: 12px;
font-size: 16px;
font-weight: 600;
background: linear-gradient(135deg, #409EFF 0%, #67C23A 100%);
border: none;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
transition: all 0.3s ease;
}
.generate-btn:hover:not(:disabled):not(.disabled-btn) {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(64, 158, 255, 0.4);
}
.generate-btn:disabled,
.generate-btn.disabled-btn {
background: #c0c4cc !important;
border-color: #c0c4cc !important;
cursor: not-allowed !important;
transform: none !important;
box-shadow: none !important;
opacity: 0.6;
}
.generate-btn:disabled:hover,
.generate-btn.disabled-btn:hover {
transform: none !important;
box-shadow: none !important;
}
/* 冷却时间和按钮提示样式 */
.cooldown-tip,
.button-tip {
margin-top: 12px;
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
animation: fadeIn 0.3s ease;
}
.cooldown-tip {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
}
.cooldown-tip i {
color: #f39c12;
animation: pulse 1s infinite;
}
.button-tip {
background: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.button-tip i {
color: #e74c3c;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
/* 评论容器样式 */
.comments-container {
margin-top: 16px;
}
.comment-card {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 12px;
margin-bottom: 16px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.comment-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
transform: translateY(-2px);
}
.comment-content {
padding: 16px;
}
.comment-text {
font-size: 14px;
line-height: 1.6;
color: #303133;
margin-bottom: 16px;
word-break: break-word;
white-space: pre-wrap;
}
/* 图片画廊样式 */
.image-gallery {
margin: 16px 0;
}
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 8px;
}
.gallery-image {
width: 100%;
height: 100px;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: transform 0.2s ease;
}
.gallery-image:hover {
transform: scale(1.05);
}
/* 操作按钮样式 */
.comment-actions {
display: flex;
justify-content: flex-end;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
}
.copy-btn {
border-radius: 20px;
padding: 8px 16px;
font-size: 12px;
}
/* 空状态样式 */
.empty-state {
text-align: center;
padding: 40px 20px;
color: #909399;
}
.empty-icon {
font-size: 48px;
color: #c0c4cc;
margin-bottom: 16px;
}
.empty-state p {
margin: 8px 0;
font-size: 14px;
}
.empty-tip {
font-size: 12px;
color: #c0c4cc;
}
/* 响应式适配 */
@media (max-width: 480px) {
.mobile-container {
padding: 8px;
}
.mobile-form {
padding: 16px 12px;
}
.select-row {
flex-direction: column;
gap: 8px;
}
.image-grid {
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 6px;
}
.gallery-image {
height: 80px;
}
.generate-btn {
height: 44px;
font-size: 15px;
}
/* 移动端型号选择按钮 */
.word-sea .item-tag {
padding: 6px 12px;
margin: 4px 6px 4px 0;
font-size: 13px;
border-radius: 14px;
}
}
@media (max-width: 360px) {
.mobile-container {
padding: 4px;
}
.image-grid {
grid-template-columns: repeat(3, 1fr);
}
.gallery-image {
height: 70px;
}
}
/* 平板适配 */
@media (min-width: 768px) and (max-width: 1024px) {
.mobile-container {
padding: 24px;
max-width: 600px;
margin: 0 auto;
}
.image-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
}
.gallery-image {
height: 120px;
}
}
/* 桌面端适配 */
@media (min-width: 1025px) {
.mobile-container {
padding: 32px;
max-width: 800px;
margin: 0 auto;
}
.select-row {
max-width: 400px;
}
.generate-btn {
max-width: 300px;
margin: 0 auto;
display: block;
}
.image-grid {
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
}
.gallery-image {
height: 140px;
}
}
/* 统计信息样式 */
.statistics-section {
margin-top: 32px;
border-top: 2px solid #f0f0f0;
padding-top: 24px;
}
.statistics-card {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%);
border: 1px solid #e1e8ff;
border-radius: 16px;
padding: 20px;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.1);
}
.statistics-header {
margin-bottom: 20px;
}
.statistics-source {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 8px;
}
.update-time {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #909399;
margin-top: 8px;
}
.update-time i {
font-size: 14px;
color: #409eff;
}
.source-tag {
display: inline-block;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
color: #fff;
}
.jd-tag {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
}
.tb-tag {
background: linear-gradient(135deg, #ff9500 0%, #ff7300 100%);
}
.product-type {
font-size: 14px;
font-weight: 600;
color: #303133;
background: #fff;
padding: 6px 12px;
border-radius: 12px;
border: 1px solid #e4e7ed;
}
.statistics-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(80px, 1fr));
gap: 16px;
}
.stat-item {
text-align: center;
background: #fff;
padding: 16px 12px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
transition: transform 0.2s ease;
}
.stat-item:hover {
transform: translateY(-2px);
}
.stat-number {
font-size: 24px;
font-weight: 700;
margin-bottom: 4px;
color: #303133;
}
.stat-number.available {
color: #67c23a;
}
.stat-number.used {
color: #909399;
}
.stat-number.new-added {
color: #409eff;
}
.stat-label {
font-size: 12px;
color: #909399;
font-weight: 500;
}
.usage-progress {
background: #fff;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.progress-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: #303133;
}
.progress-bar {
height: 8px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
border-radius: 4px;
transition: width 0.3s ease;
}
.statistics-text {
background: #fff;
padding: 16px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.statistics-text pre {
margin: 0;
font-family: 'Monaco', 'Menlo', monospace;
font-size: 13px;
line-height: 1.5;
color: #606266;
white-space: pre-wrap;
word-break: break-word;
}
/* 统计信息响应式适配 */
@media (max-width: 480px) {
.statistics-section {
margin-top: 24px;
padding-top: 20px;
}
.statistics-card {
padding: 16px;
}
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-item {
padding: 12px 8px;
}
.stat-number {
font-size: 20px;
}
.statistics-source {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
@media (max-width: 360px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.stat-item {
padding: 10px 6px;
}
.stat-number {
font-size: 18px;
}
.stat-label {
font-size: 11px;
}
}
/* 访问统计样式 */
.usage-statistics-section {
background: linear-gradient(135deg, #f8f9ff 0%, #f0f4ff 100%);
border: 1px solid #e1e8ff;
border-radius: 8px;
padding: 8px 10px;
margin-bottom: 16px;
}
.usage-stats-row {
display: flex;
justify-content: space-around;
align-items: center;
gap: 4px;
}
.usage-stat-item {
flex: 1;
text-align: center;
background: #fff;
padding: 6px 4px;
border-radius: 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.usage-stat-label {
font-size: 10px;
color: #909399;
font-weight: 500;
margin-bottom: 2px;
line-height: 1.2;
}
.usage-stat-number {
font-size: 16px;
font-weight: 700;
color: #409eff;
line-height: 1.2;
}
/* 历史记录样式 */
.history-section {
margin-top: 24px;
border-top: 2px solid #f0f0f0;
padding-top: 24px;
}
.history-loading,
.history-empty {
text-align: center;
padding: 40px 20px;
color: #909399;
}
.history-loading i,
.history-empty i {
font-size: 32px;
margin-bottom: 12px;
display: block;
color: #c0c4cc;
}
.history-empty p {
margin: 0;
font-size: 14px;
}
.history-list {
max-height: 400px;
overflow-y: auto;
}
.history-item {
background: #fff;
border: 1px solid #e4e7ed;
border-radius: 8px;
padding: 12px;
margin-bottom: 8px;
transition: all 0.3s ease;
}
.history-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transform: translateY(-1px);
}
.history-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.history-type {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.history-time {
font-size: 12px;
color: #909399;
}
.history-item-info {
display: flex;
align-items: center;
gap: 12px;
}
.history-ip {
font-size: 12px;
color: #606266;
}
/* 响应式适配 */
@media (max-width: 480px) {
.usage-statistics-section {
padding: 6px 8px;
margin-bottom: 12px;
}
.usage-stats-row {
gap: 3px;
}
.usage-stat-item {
padding: 5px 3px;
}
.usage-stat-number {
font-size: 14px;
}
.usage-stat-label {
font-size: 9px;
}
.history-item-header {
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
}
</style>