LingTropy/lingtropy-client/src/renderer/screens/TextRewritingTool.vue

1578 lines
34 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="text-rewriter-container">
<div class="text-rewriter-layout">
<!-- 左侧输入区域 -->
<div class="input-section">
<div class="header-section">
<div class="logo-container">
<div class="logo-icon">AI</div>
</div>
<div class="header-content">
<h1 class="title">文案生成工具</h1>
<p class="subtitle">智能改写提升文案质量</p>
</div>
<button @click="toggleSettings" class="settings-button">
<span class="settings-icon"></span>
</button>
</div>
<div class="model-selection-section">
<h2 class="section-title">
<span class="section-icon">🤖</span>
AI 模型选择
</h2>
<div class="model-input-group">
<div ref="modelDropdown" @click="toggleModelList" class="model-dropdown">
<input
v-model="selectedModel"
type="text"
class="model-input"
placeholder="选择或输入模型名称"
@input="onModelInput"
/>
<span :class="{ 'arrow-up': showModelList }" class="dropdown-arrow">▼</span>
<!-- 模型列表下拉框 -->
<div v-show="showModelList" class="model-list">
<div
v-for="model in filteredModels"
:key="model.name"
:class="{ active: model.name === selectedModel }"
@click="selectModel(model)"
class="model-item"
>
<span class="model-name">{{ model.name }}</span>
<span class="model-description">{{ model.description }}</span>
</div>
</div>
</div>
</div>
</div>
<div class="rewrite-mode-section">
<h2 class="section-title">
<span class="section-icon">⚙️</span>
改写模式
</h2>
<div class="mode-tabs">
<button
:class="{ active: mode === 'single' }"
@click="switchMode('single')"
class="mode-tab"
>
<span class="mode-icon">🔄</span>
单次改写
</button>
<button
:class="{ active: mode === 'batch' }"
@click="switchMode('batch')"
class="mode-tab"
>
<span class="mode-icon">🔀</span>
批量改写
</button>
</div>
<transition name="fade">
<div v-if="mode === 'batch'" class="generation-count">
<label for="count-input">生成数量:</label>
<div class="count-control">
<button
:disabled="generationCount <= 1"
@click="decrementCount"
class="count-button"
>-</button>
<input
id="count-input"
v-model="generationCount"
min="1"
max="10"
type="number"
class="count-input"
/>
<button
:disabled="generationCount >= 10"
@click="incrementCount"
class="count-button"
>+</button>
</div>
</div>
</transition>
</div>
<div class="original-text-section">
<h2 class="section-title">
<span class="section-icon">📝</span>
原始文案
<span v-if="originalText" class="char-count">{{ originalText.length }} 字</span>
</h2>
<textarea
v-model="originalText"
class="text-input"
placeholder="请在此输入需要改写的文本..."
></textarea>
</div>
<button
:disabled="isGenerating"
@click="rewriteText"
class="rewrite-button"
>
<span v-if="isGenerating" class="loading-spinner"></span>
<span v-else>{{ mode === 'single' ? '生成改写' : '批量生成' }}</span>
</button>
</div>
<!-- 右侧结果区域 -->
<transition name="fade">
<div v-if="rewrittenText.length > 0 || isGenerating" class="result-section">
<div class="result-header-bar">
<h2 class="section-title">
<span class="section-icon">✨</span>
改写结果
</h2>
<div class="result-stats">
<span class="model-badge">{{ selectedModel }}</span>
<span class="count-badge">{{ rewrittenText.length }} 个结果</span>
<button
@click="toggleLayout"
class="layout-toggle-button"
:title="settings.layoutMode === 'list' ? '切换到网格视图' : '切换到列表视图'"
>
<span v-if="settings.layoutMode === 'list'" >▦</span>
<span v-else>☰</span>
</button>
</div>
</div>
<div v-if="isGenerating && rewrittenText.length === 0" class="generating-placeholder">
<div class="generating-animation">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
<p>正在使用 {{ selectedModel }} 生成改写结果...</p>
</div>
<div v-else class="results-container">
<div
:class="{
'results-list': settings.layoutMode === 'list',
'results-grid': settings.layoutMode === 'grid'
}"
>
<div
v-for="(text, index) in rewrittenText"
:key="index"
:class="{
'expanded-result': expandedResults[index],
'grid-item': settings.layoutMode === 'grid'
}"
:style="{ animationDelay: `${index * 0.1}s` }"
class="result-block"
>
<div class="result-header">
<div class="result-number">
结果 #{{ index + 1 }}
<span class="char-count">{{ text.length }} 字</span>
</div>
<div class="result-actions">
<button @click="copyText(text, index)" class="action-button copy-button">
<span v-if="copiedIndex === index">已复制!</span>
<span v-else>复制</span>
</button>
<button
@click="toggleExpand(index)"
class="action-button expand-button"
>
{{ expandedResults[index] ? '收起' : '展开' }}
</button>
</div>
</div>
<div
:class="{ 'expanded': expandedResults[index] }"
class="result-content"
>
{{ text }}
</div>
</div>
</div>
</div>
</div>
<div v-else class="empty-result-section">
<div class="empty-state">
<div class="empty-icon">✨</div>
<h3>等待生成结果</h3>
<p>请在左侧输入文本并点击生成按钮</p>
</div>
</div>
</transition>
</div>
<!-- 设置弹窗 -->
<div v-if="showSettings" @click="closeSettingsModal" class="modal-overlay">
<div @click.stop class="settings-modal">
<div class="modal-header">
<h2>设置</h2>
<button @click="toggleSettings" class="close-button">×</button>
</div>
<div class="modal-content">
<div class="settings-section">
<h3>界面设置</h3>
<div class="setting-item">
<label class="setting-label">
<input v-model="settings.darkMode" type="checkbox">
深色模式
</label>
</div>
<div class="setting-item">
<label class="setting-label">
<input v-model="settings.compactMode" type="checkbox">
紧凑视图
</label>
</div>
<div class="setting-item">
<label class="setting-label">结果布局</label>
<select v-model="settings.layoutMode" class="select-input">
<option value="list">列表视图</option>
<option value="grid">网格视图</option>
</select>
</div>
</div>
<div class="settings-section">
<h3>生成设置</h3>
<div class="setting-item">
<label class="setting-label">温度值</label>
<div class="slider-container">
<input
v-model="settings.temperature"
min="0"
max="100"
type="range"
class="slider"
>
<span class="slider-value">{{ settings.temperature / 100 }}</span>
</div>
</div>
<div class="setting-item">
<label class="setting-label">最大长度</label>
<select v-model="settings.maxLength" class="select-input">
<option value="500">500 字</option>
<option value="1000">1000 字</option>
<option value="2000">2000 字</option>
<option value="3000">3000 字</option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button @click="toggleSettings" class="cancel-button">取消</button>
<button @click="saveSettings" class="save-button">保存设置</button>
</div>
</div>
</div>
<!-- 背景装饰元素 -->
<div class="background-decoration">
<div class="bg-circle bg-circle-1"></div>
<div class="bg-circle bg-circle-2"></div>
<div class="bg-circle bg-circle-3"></div>
<div class="bg-dots"></div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted, computed } from 'vue'
// AI 模型列表
const availableModels = [
{ name: 'gpt-4o', description: '最新的 GPT-4o 模型,支持更复杂的任务' },
{ name: 'gpt-4', description: '强大的 GPT-4 模型,适合高质量内容生成' },
{ name: 'gpt-3.5-turbo', description: '高效的 GPT-3.5 模型,适合一般任务' },
{ name: 'claude-3-opus', description: 'Claude 3 Opus 模型,擅长长文本处理' },
{ name: 'claude-3-sonnet', description: 'Claude 3 Sonnet 模型,平衡性能与效率' },
{ name: 'gemini-pro', description: 'Google Gemini Pro 模型' },
{ name: 'llama-3', description: 'Meta 最新开源大模型' },
]
// 基础状态
const mode = ref('single')
const originalText = ref('')
const rewrittenText = ref([])
const generationCount = ref(3)
const expandedResults = reactive({})
const isGenerating = ref(false)
const copiedIndex = ref(null)
// 模型选择相关
const selectedModel = ref('gpt-3.5-turbo')
const showModelList = ref(false)
const modelDropdown = ref(null)
const filteredModels = computed(() => {
if (!selectedModel.value) return availableModels
const searchTerm = selectedModel.value.toLowerCase()
return availableModels.filter(model =>
model.name.toLowerCase().includes(searchTerm)
)
})
// 设置相关
const showSettings = ref(false)
const settings = reactive({
darkMode: false,
compactMode: false,
temperature: 70, // 0.7
maxLength: '2000',
layoutMode: 'list'
})
// 切换布局模式
const toggleLayout = () => {
settings.layoutMode = settings.layoutMode === 'list' ? 'grid' : 'list'
}
// 监听点击事件,关闭模型列表
const handleClickOutside = (event) => {
if (modelDropdown.value && !modelDropdown.value.contains(event.target)) {
showModelList.value = false
}
}
// 设置弹窗相关
const toggleSettings = () => {
showSettings.value = !showSettings.value
}
const closeSettingsModal = (event) => {
if (event.target.classList.contains('modal-overlay')) {
showSettings.value = false
}
}
const saveSettings = () => {
// 这里可以添加保存设置的逻辑,例如存储到 localStorage
showToast('设置已保存')
showSettings.value = false
}
onMounted(() => {
document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
// 切换模型列表显示
const toggleModelList = () => {
showModelList.value = !showModelList.value
}
// 选择模型
const selectModel = (model) => {
selectedModel.value = model.name
showModelList.value = false
}
// 模型输入过滤 - 已移至计算属性
const onModelInput = () => {
// 过滤逻辑已移至计算属性
}
// 其他方法
const switchMode = (newMode) => {
if (mode.value !== newMode) {
mode.value = newMode
}
}
const incrementCount = () => {
if (generationCount.value < 10) {
generationCount.value++
}
}
const decrementCount = () => {
if (generationCount.value > 1) {
generationCount.value--
}
}
const rewriteText = () => {
if (!originalText.value.trim()) {
showToast('请输入需要改写的文本')
return
}
Object.keys(expandedResults).forEach(key => {
delete expandedResults[key]
})
isGenerating.value = true
rewrittenText.value = []
setTimeout(() => {
if (mode.value === 'single') {
const longText = `使用 ${selectedModel.value} 模型改写:\n\n${originalText.value}\n\n这是改写后的内容保留了原文的核心信息同时进行了适当的润色和优化。这个版本更加流畅自然同时保持了专业性和可读性。\n\n我们对句式结构进行了调整使表达更加清晰。同时我们也优化了用词使整体风格更加一致和专业。`
rewrittenText.value = [longText]
} else {
const count = parseInt(generationCount.value) || 1
for (let i = 1; i <= count; i++) {
let style = ''
switch (i % 3) {
case 1:
style = '正式商务风格'
break
case 2:
style = '简洁明了风格'
break
case 0:
style = '生动活泼风格'
break
}
const longText = `使用 ${selectedModel.value} 模型改写:\n【${style}\n${originalText.value}\n\n这是第 ${i} 个改写版本,采用了${style}。我们对原文进行了重新组织和表达,使其更符合目标风格和受众需求。\n\n这个版本保留了原文的核心信息同时进行了适当的润色和优化使表达更加自然流畅。`
rewrittenText.value.push(longText)
}
}
isGenerating.value = false
}, 1500)
}
const copyText = (text, index) => {
navigator.clipboard.writeText(text)
.then(() => {
copiedIndex.value = index
setTimeout(() => {
copiedIndex.value = null
}, 2000)
})
.catch(err => {
console.error('复制失败:', err)
showToast('复制失败,请重试')
})
}
const toggleExpand = (index) => {
expandedResults[index] = !expandedResults[index]
}
const showToast = (message) => {
const toast = document.createElement('div')
toast.className = 'toast-message'
toast.textContent = message
document.body.appendChild(toast)
setTimeout(() => {
toast.classList.add('show')
setTimeout(() => {
toast.classList.remove('show')
setTimeout(() => {
document.body.removeChild(toast)
}, 300)
}, 2000)
}, 10)
}
</script>
<style scoped>
/* 基础布局 */
.text-rewriter-container {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
display: flex;
justify-content: center;
align-items: flex-start;
padding: 20px;
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
/* 背景装饰 */
.background-decoration {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: -1;
overflow: hidden;
}
.bg-circle {
position: absolute;
border-radius: 50%;
opacity: 0.5;
filter: blur(60px);
}
.bg-circle-1 {
width: 400px;
height: 400px;
background: linear-gradient(45deg, rgba(46, 204, 113, 0.3), rgba(52, 152, 219, 0.3));
top: -100px;
left: -100px;
animation: float 15s ease-in-out infinite alternate;
}
.bg-circle-2 {
width: 600px;
height: 600px;
background: linear-gradient(45deg, rgba(155, 89, 182, 0.2), rgba(41, 128, 185, 0.2));
bottom: -200px;
right: -200px;
animation: float 20s ease-in-out infinite alternate-reverse;
}
.bg-circle-3 {
width: 300px;
height: 300px;
background: linear-gradient(45deg, rgba(241, 196, 15, 0.2), rgba(231, 76, 60, 0.2));
top: 40%;
left: 60%;
animation: float 18s ease-in-out infinite alternate;
}
.bg-dots {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: radial-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px);
background-size: 30px 30px;
opacity: 0.3;
}
@keyframes float {
0% {
transform: translate(0, 0);
}
100% {
transform: translate(50px, 50px);
}
}
.text-rewriter-layout {
display: flex;
gap: 24px;
width: 100%;
max-width: 1800px;
height: calc(100vh - 40px);
position: relative;
z-index: 1;
}
/* 左侧输入区域 */
.input-section {
flex: 0 0 40%;
max-width: 600px;
min-width: 320px;
background-color: white;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 28px;
display: flex;
flex-direction: column;
overflow-y: auto;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
background-color: rgba(255, 255, 255, 0.95);
}
/* 右侧结果区域 */
.result-section, .empty-result-section {
flex: 1;
background-color: white;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
padding: 28px;
display: flex;
flex-direction: column;
overflow: hidden;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
background-color: rgba(255, 255, 255, 0.95);
}
.result-header-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid #f0f0f0;
}
.result-stats {
display: flex;
gap: 12px;
align-items: center;
}
.model-badge, .count-badge {
padding: 6px 12px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
}
.model-badge {
background-color: #e8f5e9;
color: #2e7d32;
}
.count-badge {
background-color: #e3f2fd;
color: #1565c0;
}
/* Logo 样式 */
.header-section {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 28px;
position: relative;
}
.header-content {
flex: 1;
}
.logo-container {
display: flex;
align-items: center;
justify-content: center;
}
.logo-icon {
width: 48px;
height: 48px;
background: linear-gradient(135deg, #2ecc71, #27ae60);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 20px;
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.3);
}
/* 设置按钮 */
.settings-button {
width: 40px;
height: 40px;
border-radius: 10px;
background-color: #f5f5f5;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
}
.settings-button:hover {
background-color: #e0e0e0;
transform: rotate(30deg);
}
.settings-icon {
font-size: 20px;
}
/* 标题样式 */
.title {
font-size: 24px;
font-weight: 700;
margin: 0 0 4px 0;
background: linear-gradient(90deg, #2ecc71, #27ae60);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.subtitle {
color: #666;
margin: 0;
font-size: 14px;
}
.section-title {
font-size: 18px;
font-weight: 600;
margin: 0 0 16px 0;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
.section-icon {
font-size: 20px;
}
/* 字数统计 */
.char-count {
font-size: 14px;
color: #666;
font-weight: normal;
margin-left: auto;
background-color: #f0f0f0;
padding: 2px 8px;
border-radius: 12px;
}
/* 模型选择样式 */
.model-selection-section {
margin-bottom: 24px;
}
.model-input-group {
position: relative;
}
.model-dropdown {
position: relative;
width: 100%;
}
.model-input {
width: 100%;
padding: 12px 16px;
padding-right: 36px;
border: 1px solid #e0e0e0;
border-radius: 10px;
font-size: 16px;
transition: all 0.3s ease;
background-color: #f9f9f9;
}
.model-input:focus {
outline: none;
border-color: #2ecc71;
box-shadow: 0 0 0 3px rgba(46, 204, 113, 0.2);
background-color: white;
}
.dropdown-arrow {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
color: #666;
pointer-events: none;
transition: transform 0.3s ease;
}
.arrow-up {
transform: translateY(-50%) rotate(180deg);
}
.model-list {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 10px;
margin-top: 8px;
max-height: 300px;
overflow-y: auto;
z-index: 1000;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.model-item {
padding: 14px 16px;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 4px;
transition: all 0.2s ease;
border-bottom: 1px solid #f0f0f0;
}
.model-item:last-child {
border-bottom: none;
}
.model-item:hover {
background-color: #f5f5f5;
}
.model-item.active {
background-color: #e8f5e9;
}
.model-name {
font-weight: 600;
color: #333;
}
.model-description {
font-size: 14px;
color: #666;
}
/* 模式选择样式 */
.rewrite-mode-section {
margin-bottom: 24px;
}
.mode-tabs {
display: flex;
gap: 16px;
margin-bottom: 16px;
}
.mode-tab {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 10px;
color: #333;
cursor: pointer;
flex: 1;
font-size: 16px;
padding: 14px;
text-align: center;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.mode-tab:hover {
border-color: #2ecc71;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.mode-tab.active {
background: linear-gradient(135deg, #2ecc71, #27ae60);
border-color: transparent;
color: white;
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.3);
}
.mode-icon {
font-size: 18px;
}
/* 生成数量控制样式 */
.generation-count {
display: flex;
align-items: center;
margin-bottom: 24px;
gap: 16px;
padding: 16px;
background-color: #f9f9f9;
border-radius: 10px;
border-left: 4px solid #2ecc71;
}
.count-control {
display: flex;
align-items: center;
}
.count-button {
width: 36px;
height: 36px;
border: 1px solid #e0e0e0;
background-color: white;
color: #333;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.count-button:first-child {
border-radius: 8px 0 0 8px;
}
.count-button:last-child {
border-radius: 0 8px 8px 0;
}
.count-button:hover:not(:disabled) {
background-color: #f0f0f0;
}
.count-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.count-input {
border: 1px solid #e0e0e0;
border-left: none;
border-right: none;
padding: 8px 0;
width: 50px;
font-size: 16px;
text-align: center;
height: 36px;
}
/* 输入区域样式 */
.original-text-section {
margin-bottom: 24px;
flex-grow: 1;
display: flex;
flex-direction: column;
}
.text-input {
border: 1px solid #e0e0e0;
border-radius: 10px;
font-family: inherit;
font-size: 16px;
padding: 16px;
resize: none;
width: 100%;
flex-grow: 1;
min-height: 150px;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
transition: border-color 0.3s, box-shadow 0.3s;
background-color: #f9f9f9;
}
.text-input:focus {
outline: none;
border-color: #2ecc71;
box-shadow: 0 0 0 3px rgba(46, 204, 113, 0.2);
background-color: white;
}
/* 按钮样式 */
.rewrite-button {
background: linear-gradient(135deg, #2ecc71, #27ae60);
border: none;
border-radius: 10px;
color: white;
cursor: pointer;
font-size: 16px;
font-weight: 600;
margin-top: 16px;
padding: 16px;
transition: all 0.3s ease;
width: 100%;
position: relative;
overflow: hidden;
box-shadow: 0 4px 12px rgba(46, 204, 113, 0.3);
}
.rewrite-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(46, 204, 113, 0.4);
}
.rewrite-button:active:not(:disabled) {
transform: translateY(1px);
box-shadow: 0 2px 8px rgba(46, 204, 113, 0.3);
}
.rewrite-button:disabled {
opacity: 0.7;
cursor: not-allowed;
}
/* 加载动画 */
.loading-spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 结果区域样式 */
.results-container {
flex: 1;
overflow-y: auto;
padding: 4px;
}
.results-list {
display: flex;
flex-direction: column;
gap: 20px;
padding: 4px;
}
.results-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
padding: 4px;
}
.grid-item {
height: 100%;
display: flex;
flex-direction: column;
}
.result-block {
background-color: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
overflow: hidden;
border: 1px solid #e8e8e8;
width: 100%;
transition: all 0.3s ease;
animation: fadeIn 0.5s ease forwards;
opacity: 0;
transform: translateY(20px);
display: flex;
flex-direction: column;
}
.result-block:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
.result-header {
background-color: #f9f9f9;
padding: 14px 16px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e8e8e8;
}
.result-number {
font-weight: 600;
color: #2ecc71;
display: flex;
align-items: center;
gap: 10px;
}
.result-number::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
background-color: #2ecc71;
border-radius: 50%;
}
.result-actions {
display: flex;
gap: 8px;
}
.action-button {
background-color: white;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 6px 12px;
font-size: 14px;
cursor: pointer;
transition: all 0.2s;
position: relative;
min-width: 60px;
text-align: center;
}
.action-button:hover {
background-color: #f0f0f0;
border-color: #ccc;
}
.copy-button {
color: #2ecc71;
border-color: #2ecc71;
}
.copy-button:hover {
background-color: rgba(46, 204, 113, 0.1);
border-color: #2ecc71;
}
.expand-button {
color: #666;
}
.result-content {
padding: 16px;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
line-height: 1.6;
max-height: 200px;
overflow: hidden;
position: relative;
text-align: justify;
hyphens: auto;
transition: max-height 0.5s ease;
flex: 1;
}
.result-content:not(.expanded)::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 60px;
background: linear-gradient(transparent, white);
pointer-events: none;
transition: opacity 0.3s;
}
.result-content.expanded {
max-height: none;
}
/* 生成中占位符 */
.generating-placeholder {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px;
text-align: center;
color: #666;
}
.generating-placeholder p {
margin-top: 20px;
font-size: 16px;
}
.generating-animation {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.dot {
width: 12px;
height: 12px;
background-color: #2ecc71;
border-radius: 50%;
animation: bounce 1.4s infinite ease-in-out both;
}
.dot:nth-child(1) {
animation-delay: -0.32s;
}
.dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1.0);
}
}
/* 空状态 */
.empty-result-section {
display: flex;
align-items: center;
justify-content: center;
}
.empty-state {
text-align: center;
padding: 40px;
color: #666;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
color: #ccc;
}
.empty-state h3 {
font-size: 20px;
margin-bottom: 8px;
color: #333;
}
.empty-state p {
font-size: 16px;
color: #666;
}
/* 设置弹窗 */
.modal-overlay {
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;
animation: fadeIn 0.3s ease;
}
.settings-modal {
background-color: white;
border-radius: 16px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
animation: slideUp 0.3s ease;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
border-bottom: 1px solid #f0f0f0;
}
.modal-header h2 {
margin: 0;
font-size: 20px;
color: #333;
}
.close-button {
background: none;
border: none;
font-size: 24px;
color: #666;
cursor: pointer;
transition: color 0.2s;
}
.close-button:hover {
color: #333;
}
.modal-content {
padding: 20px;
}
.settings-section {
margin-bottom: 24px;
}
.settings-section h3 {
font-size: 18px;
margin: 0 0 16px 0;
color: #333;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.setting-item {
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
.setting-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
color: #333;
}
.slider-container {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
max-width: 200px;
}
.slider {
flex: 1;
height: 6px;
-webkit-appearance: none;
appearance: none;
background: #e0e0e0;
border-radius: 3px;
outline: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: #2ecc71;
cursor: pointer;
transition: all 0.2s;
}
.slider::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
.slider-value {
min-width: 40px;
text-align: center;
font-size: 14px;
color: #666;
}
.select-input {
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
background-color: white;
min-width: 120px;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
}
.cancel-button, .save-button {
padding: 10px 20px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: all 0.2s;
}
.cancel-button {
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
color: #666;
}
.cancel-button:hover {
background-color: #e0e0e0;
}
.save-button {
background-color: #2ecc71;
border: none;
color: white;
}
.save-button:hover {
background-color: #27ae60;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 动画效果 */
@keyframes fadeIn {
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInOut {
0% { opacity: 0; }
20% { opacity: 1; }
80% { opacity: 1; }
100% { opacity: 0; }
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s, transform 0.3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(20px);
}
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from {
opacity: 0;
transform: translateY(30px);
}
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* 提示框样式 */
:global(.toast-message) {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%) translateY(100px);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
z-index: 1000;
transition: transform 0.3s ease;
}
:global(.toast-message.show) {
transform: translateX(-50%) translateY(0);
}
/* 布局切换按钮 */
.layout-toggle-button {
background-color: #f0f0f0;
border: none;
border-radius: 50%;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
}
.layout-toggle-button:hover {
background-color: #e0e0e0;
transform: scale(1.1);
}
/* 响应式设计 */
@media (max-width: 1200px) {
.text-rewriter-layout {
flex-direction: column;
}
.input-section {
flex: none;
width: 100%;
max-width: 100%;
}
.result-section, .empty-result-section {
width: 100%;
}
}
@media (max-width: 768px) {
.text-rewriter-container {
padding: 12px;
}
.text-rewriter-layout {
gap: 16px;
}
.input-section, .result-section, .empty-result-section {
padding: 20px;
border-radius: 12px;
}
.mode-tabs {
flex-direction: column;
}
.generation-count {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.header-section {
flex-direction: column;
text-align: center;
}
.settings-button {
position: absolute;
top: 0;
right: 0;
}
.results-grid {
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
}
}
@media (max-width: 480px) {
.text-rewriter-container {
padding: 8px;
}
.input-section, .result-section, .empty-result-section {
padding: 16px;
}
.result-header {
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
.result-actions {
width: 100%;
justify-content: space-between;
}
.results-grid {
grid-template-columns: 1fr;
}
}
</style>