Files
ruoyi-vue/src/views/public/CommentGenerator.vue
2025-09-07 17:35:42 +08:00

890 lines
20 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-header">
<h3>评论生成公开</h3>
</div>
<div class="mobile-form">
<!-- 产品类型选择区域 -->
<div class="form-section">
<div class="form-label">型号/类型</div>
<div class="select-row">
<el-select
v-model="form.productType"
filterable
placeholder="请选择型号/类型"
class="mobile-select"
size="medium"
>
<el-option v-for="it in typeOptions" :key="it.name" :label="it.name" :value="it.name" />
</el-select>
<el-button
type="primary"
size="medium"
class="refresh-btn"
@click="loadTypes"
icon="el-icon-refresh"
>
刷新
</el-button>
</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>
<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>
</div>
</div>
</template>
<script>
export default {
name: 'CommentGeneratorPublic',
data() {
return {
form: { productType: '' },
loading: false,
result: null,
typeOptions: [],
comments: [],
statistics: null,
lastGenerateTime: 0,
cooldownTime: 3000, // 5秒冷却时间
isButtonDisabled: false
}
},
computed: {
pretty() {
try { return this.result ? JSON.stringify(this.result, null, 2) : '' } catch(e) { return '' }
},
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.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 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) {}
},
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.$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;
}
.mobile-card {
background: #fff;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
/* 头部样式 */
.mobile-header {
background: linear-gradient(135deg, #409EFF 0%, #67C23A 100%);
color: #fff;
padding: 20px 16px;
text-align: center;
}
.mobile-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
/* 表单样式 */
.mobile-form {
padding: 20px 16px;
}
.form-section {
margin-bottom: 24px;
}
.form-label {
font-size: 14px;
font-weight: 600;
color: #303133;
margin-bottom: 12px;
}
/* 选择器行样式 */
.select-row {
display: flex;
gap: 12px;
align-items: center;
}
.mobile-select {
flex: 1;
min-width: 0;
}
.refresh-btn {
flex-shrink: 0;
border-radius: 8px;
}
/* 生成按钮样式 */
.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;
}
.refresh-btn {
width: 100%;
}
.image-grid {
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 6px;
}
.gallery-image {
height: 80px;
}
.mobile-header h3 {
font-size: 16px;
}
.generate-btn {
height: 44px;
font-size: 15px;
}
}
@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;
}
.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;
}
}
</style>