751 lines
21 KiB
Vue
751 lines
21 KiB
Vue
<template>
|
||
<div class="h-screen w-full flex flex-col bg-slate-950">
|
||
<!-- 顶部工具栏 -->
|
||
<header class="h-14 border-b border-slate-800 flex items-center justify-between px-6 bg-slate-950 shrink-0">
|
||
<div class="flex items-center gap-4">
|
||
<h1 class="text-lg font-bold text-white flex items-center gap-2">
|
||
<IconLibrary name="chart" :size="20" /> 文稿差异标注
|
||
</h1>
|
||
<button
|
||
@click="goBack"
|
||
class="text-xs px-3 py-1 rounded bg-slate-700 text-slate-300 hover:bg-slate-600 transition"
|
||
>
|
||
← 返回写作
|
||
</button>
|
||
</div>
|
||
<div class="flex items-center gap-4">
|
||
<!-- 图例说明 -->
|
||
<div class="text-xs text-slate-500 flex items-center gap-3">
|
||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-amber-500/50"></span> 修改</span>
|
||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-green-500/50"></span> 新增</span>
|
||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-red-500/50"></span> 删除</span>
|
||
</div>
|
||
<!-- 差异统计 -->
|
||
<div v-if="diffStats.total > 0" class="text-xs text-slate-400 flex items-center gap-3 border-l border-slate-700 pl-4">
|
||
<span class="text-amber-400">{{ diffStats.modified }} 处修改</span>
|
||
<span class="text-green-400">{{ diffStats.added }} 处新增</span>
|
||
<span class="text-red-400">{{ diffStats.removed }} 处删除</span>
|
||
</div>
|
||
<!-- 导出按钮 -->
|
||
<button
|
||
@click="exportToWord"
|
||
:disabled="!leftContent || !rightContent"
|
||
class="text-sm px-4 py-2 rounded bg-blue-600 text-white hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition flex items-center gap-2"
|
||
>
|
||
<IconLibrary name="download" :size="14" /> 导出 Word
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 主内容区:左右对比 -->
|
||
<div class="flex-1 flex w-full overflow-hidden">
|
||
<!-- 左侧:原文 -->
|
||
<div class="flex-1 flex flex-col border-r border-slate-700 min-w-0">
|
||
<div class="p-3 bg-slate-800 border-b border-slate-700 flex items-center justify-between shrink-0">
|
||
<h2 class="text-sm font-medium text-amber-400 flex items-center gap-2">
|
||
<IconLibrary name="document" :size="14" /> 原文
|
||
</h2>
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-xs text-slate-500">来源:</span>
|
||
<div class="flex bg-slate-900 rounded p-0.5 border border-slate-700">
|
||
<button
|
||
@click="leftSourceType = 'paste'"
|
||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||
leftSourceType === 'paste' ? 'bg-amber-600 text-white' : 'text-slate-400 hover:text-white']"
|
||
>粘贴</button>
|
||
<button
|
||
@click="showLeftDocSelector = true"
|
||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||
leftSourceType === 'document' ? 'bg-amber-600 text-white' : 'text-slate-400 hover:text-white']"
|
||
>文稿库</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 差异高亮显示区 -->
|
||
<div class="flex-1 overflow-y-auto p-4 bg-slate-900/50 min-h-0" :style="{ minHeight: `calc(100% - ${inputHeight}px - 60px)` }">
|
||
<div v-if="diffSegments.length > 0" class="text-sm leading-relaxed whitespace-pre-wrap">
|
||
<template v-for="(segment, idx) in diffSegments" :key="'left-' + idx">
|
||
<span
|
||
v-if="segment.type === 'unchanged'"
|
||
class="text-slate-300"
|
||
>{{ segment.original }}</span>
|
||
<span
|
||
v-else-if="segment.type === 'modified'"
|
||
class="bg-amber-500/30 text-amber-200 px-0.5 rounded"
|
||
>{{ segment.original }}</span>
|
||
<span
|
||
v-else-if="segment.type === 'removed'"
|
||
class="bg-red-500/30 text-red-200 px-0.5 rounded line-through"
|
||
>{{ segment.original }}</span>
|
||
</template>
|
||
</div>
|
||
<div v-else class="h-full flex flex-col items-center justify-center text-slate-500">
|
||
<div class="w-16 h-16 flex items-center justify-center mb-3 opacity-30">
|
||
<IconLibrary name="edit" :size="40" />
|
||
</div>
|
||
<p class="text-sm">在下方输入原文内容</p>
|
||
<p class="text-xs text-slate-600 mt-1">或从文稿库选择</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 可拖动分割线 -->
|
||
<div
|
||
class="h-2 bg-slate-700 cursor-ns-resize hover:bg-slate-600 flex items-center justify-center transition shrink-0"
|
||
@mousedown="startResize"
|
||
>
|
||
<div class="w-12 h-1 bg-slate-500 rounded"></div>
|
||
</div>
|
||
|
||
<!-- 输入区 -->
|
||
<div class="p-3 border-t border-slate-700 bg-slate-800 shrink-0" :style="{ height: inputHeight + 'px' }">
|
||
<textarea
|
||
v-model="leftContent"
|
||
class="w-full h-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm text-slate-200 focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none resize-none"
|
||
placeholder="粘贴或输入原文内容..."
|
||
></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:修改文 -->
|
||
<div class="flex-1 flex flex-col min-w-0">
|
||
<div class="p-3 bg-slate-800 border-b border-slate-700 flex items-center justify-between shrink-0">
|
||
<h2 class="text-sm font-medium text-blue-400 flex items-center gap-2">
|
||
<IconLibrary name="edit" :size="14" /> 修改文
|
||
</h2>
|
||
<div class="flex items-center gap-2">
|
||
<span class="text-xs text-slate-500">来源:</span>
|
||
<div class="flex bg-slate-900 rounded p-0.5 border border-slate-700">
|
||
<button
|
||
@click="rightSourceType = 'paste'"
|
||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||
rightSourceType === 'paste' ? 'bg-blue-600 text-white' : 'text-slate-400 hover:text-white']"
|
||
>粘贴</button>
|
||
<button
|
||
@click="showRightDocSelector = true"
|
||
:class="['text-xs px-2 py-0.5 rounded transition',
|
||
rightSourceType === 'document' ? 'bg-blue-600 text-white' : 'text-slate-400 hover:text-white']"
|
||
>文稿库</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 差异高亮显示区 -->
|
||
<div class="flex-1 overflow-y-auto p-4 bg-slate-900/50 min-h-0" :style="{ minHeight: `calc(100% - ${inputHeight}px - 60px)` }">
|
||
<div v-if="diffSegments.length > 0" class="text-sm leading-relaxed whitespace-pre-wrap">
|
||
<template v-for="(segment, idx) in diffSegments" :key="'right-' + idx">
|
||
<span
|
||
v-if="segment.type === 'unchanged'"
|
||
class="text-slate-300"
|
||
>{{ segment.rewritten }}</span>
|
||
<span
|
||
v-else-if="segment.type === 'modified'"
|
||
class="bg-amber-500/30 text-amber-200 px-0.5 rounded"
|
||
>{{ segment.rewritten }}</span>
|
||
<span
|
||
v-else-if="segment.type === 'added'"
|
||
class="bg-green-500/30 text-green-200 px-0.5 rounded"
|
||
>{{ segment.rewritten }}</span>
|
||
</template>
|
||
</div>
|
||
<div v-else class="h-full flex flex-col items-center justify-center text-slate-500">
|
||
<div class="w-16 h-16 flex items-center justify-center mb-3 opacity-30">
|
||
<IconLibrary name="edit" :size="40" />
|
||
</div>
|
||
<p class="text-sm">在下方输入修改后的内容</p>
|
||
<p class="text-xs text-slate-600 mt-1">或从文稿库选择</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 可拖动分割线 -->
|
||
<div
|
||
class="h-2 bg-slate-700 cursor-ns-resize hover:bg-slate-600 flex items-center justify-center transition shrink-0"
|
||
@mousedown="startResize"
|
||
>
|
||
<div class="w-12 h-1 bg-slate-500 rounded"></div>
|
||
</div>
|
||
|
||
<!-- 输入区 -->
|
||
<div class="p-3 border-t border-slate-700 bg-slate-800 shrink-0" :style="{ height: inputHeight + 'px' }">
|
||
<textarea
|
||
v-model="rightContent"
|
||
class="w-full h-full bg-slate-900 border border-slate-700 rounded-lg p-3 text-sm text-slate-200 focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none resize-none"
|
||
placeholder="粘贴或输入修改后的内容..."
|
||
></textarea>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文稿选择弹窗 - 左侧 -->
|
||
<DocumentSelectorModal
|
||
:visible="showLeftDocSelector"
|
||
@close="showLeftDocSelector = false"
|
||
@select="handleLeftDocSelect"
|
||
/>
|
||
|
||
<!-- 文稿选择弹窗 - 右侧 -->
|
||
<DocumentSelectorModal
|
||
:visible="showRightDocSelector"
|
||
@close="showRightDocSelector = false"
|
||
@select="handleRightDocSelect"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||
import { useAppStore } from '../../stores/app'
|
||
import { computePreciseDiff, getPreciseDiffStats } from '../../utils/preciseDiff.js'
|
||
import IconLibrary from '../icons/IconLibrary.vue'
|
||
import { Document, Packer, Paragraph, TextRun } from 'docx'
|
||
import { saveAs } from 'file-saver'
|
||
import DocumentSelectorModal from '../modals/DocumentSelectorModal.vue'
|
||
|
||
const appStore = useAppStore()
|
||
|
||
// 内容
|
||
const leftContent = ref('')
|
||
const rightContent = ref('')
|
||
|
||
// 来源类型
|
||
const leftSourceType = ref('paste')
|
||
const rightSourceType = ref('paste')
|
||
|
||
// 文稿选择器
|
||
const showLeftDocSelector = ref(false)
|
||
const showRightDocSelector = ref(false)
|
||
|
||
// 差异计算
|
||
const diffSegments = ref([])
|
||
|
||
// 可拖动分割线状态
|
||
const inputHeight = ref(160)
|
||
const isResizing = ref(false)
|
||
const startY = ref(0)
|
||
const startHeight = ref(0)
|
||
|
||
// 监听内容变化,实时计算差异(使用精确算法)
|
||
watch([leftContent, rightContent], () => {
|
||
if (leftContent.value && rightContent.value) {
|
||
diffSegments.value = computePreciseDiff(leftContent.value, rightContent.value)
|
||
} else {
|
||
diffSegments.value = []
|
||
}
|
||
}, { immediate: true })
|
||
|
||
// 差异统计
|
||
const diffStats = computed(() => {
|
||
return getPreciseDiffStats(diffSegments.value)
|
||
})
|
||
|
||
// 处理左侧文稿选择
|
||
const handleLeftDocSelect = (doc) => {
|
||
leftContent.value = doc.content || ''
|
||
leftSourceType.value = 'document'
|
||
showLeftDocSelector.value = false
|
||
}
|
||
|
||
// 处理右侧文稿选择
|
||
const handleRightDocSelect = (doc) => {
|
||
rightContent.value = doc.content || ''
|
||
rightSourceType.value = 'document'
|
||
showRightDocSelector.value = false
|
||
}
|
||
|
||
// 分割线拖动处理
|
||
const startResize = (e) => {
|
||
isResizing.value = true
|
||
startY.value = e.clientY
|
||
startHeight.value = inputHeight.value
|
||
document.addEventListener('mousemove', doResize)
|
||
document.addEventListener('mouseup', stopResize)
|
||
document.body.style.cursor = 'ns-resize'
|
||
document.body.style.userSelect = 'none'
|
||
}
|
||
|
||
const doResize = (e) => {
|
||
if (!isResizing.value) return
|
||
const delta = startY.value - e.clientY
|
||
const newHeight = Math.max(80, Math.min(400, startHeight.value + delta))
|
||
inputHeight.value = newHeight
|
||
}
|
||
|
||
const stopResize = () => {
|
||
isResizing.value = false
|
||
document.removeEventListener('mousemove', doResize)
|
||
document.removeEventListener('mouseup', stopResize)
|
||
document.body.style.cursor = ''
|
||
document.body.style.userSelect = ''
|
||
}
|
||
|
||
// 清理事件监听
|
||
onUnmounted(() => {
|
||
document.removeEventListener('mousemove', doResize)
|
||
document.removeEventListener('mouseup', stopResize)
|
||
})
|
||
|
||
// 返回写作页面
|
||
const goBack = () => {
|
||
appStore.setCurrentPage('writer')
|
||
}
|
||
|
||
// 导出 Word 文档
|
||
const exportToWord = async () => {
|
||
if (!leftContent.value || !rightContent.value) {
|
||
alert('请先填写原文和修改文内容')
|
||
return
|
||
}
|
||
|
||
try {
|
||
// 创建文档段落
|
||
const children = []
|
||
|
||
// 标题
|
||
children.push(
|
||
new Paragraph({
|
||
children: [
|
||
new TextRun({
|
||
text: '文稿差异对比报告',
|
||
bold: true,
|
||
size: 32
|
||
})
|
||
],
|
||
spacing: { after: 400 }
|
||
})
|
||
)
|
||
|
||
// 统计信息
|
||
children.push(
|
||
new Paragraph({
|
||
children: [
|
||
new TextRun({
|
||
text: `差异统计:修改 ${diffStats.value.modified} 处,新增 ${diffStats.value.added} 处,删除 ${diffStats.value.removed} 处`,
|
||
size: 20,
|
||
color: '666666'
|
||
})
|
||
],
|
||
spacing: { after: 400 }
|
||
})
|
||
)
|
||
|
||
// 分隔线
|
||
children.push(
|
||
new Paragraph({
|
||
children: [new TextRun({ text: '─'.repeat(50) })],
|
||
spacing: { after: 200 }
|
||
})
|
||
)
|
||
|
||
// 原文标题
|
||
children.push(
|
||
new Paragraph({
|
||
children: [
|
||
new TextRun({
|
||
text: '【原文】',
|
||
bold: true,
|
||
size: 24
|
||
})
|
||
],
|
||
spacing: { after: 200 }
|
||
})
|
||
)
|
||
|
||
// 原文内容(带高亮)
|
||
const leftRuns = []
|
||
for (const segment of diffSegments.value) {
|
||
if (segment.type === 'unchanged' && segment.original) {
|
||
leftRuns.push(new TextRun({ text: segment.original, size: 22 }))
|
||
} else if (segment.type === 'modified' && segment.original) {
|
||
leftRuns.push(new TextRun({
|
||
text: segment.original,
|
||
size: 22,
|
||
color: 'D97706', // 橙色
|
||
highlight: 'yellow'
|
||
}))
|
||
} else if (segment.type === 'removed' && segment.original) {
|
||
leftRuns.push(new TextRun({
|
||
text: segment.original,
|
||
size: 22,
|
||
color: 'DC2626', // 红色
|
||
strike: true,
|
||
highlight: 'red'
|
||
}))
|
||
}
|
||
}
|
||
if (leftRuns.length > 0) {
|
||
children.push(new Paragraph({ children: leftRuns, spacing: { after: 400 } }))
|
||
}
|
||
|
||
// 分隔线
|
||
children.push(
|
||
new Paragraph({
|
||
children: [new TextRun({ text: '─'.repeat(50) })],
|
||
spacing: { after: 200 }
|
||
})
|
||
)
|
||
|
||
// 修改文标题
|
||
children.push(
|
||
new Paragraph({
|
||
children: [
|
||
new TextRun({
|
||
text: '【修改文】',
|
||
bold: true,
|
||
size: 24
|
||
})
|
||
],
|
||
spacing: { after: 200 }
|
||
})
|
||
)
|
||
|
||
// 修改文内容(带高亮)
|
||
const rightRuns = []
|
||
for (const segment of diffSegments.value) {
|
||
if (segment.type === 'unchanged' && segment.rewritten) {
|
||
rightRuns.push(new TextRun({ text: segment.rewritten, size: 22 }))
|
||
} else if (segment.type === 'modified' && segment.rewritten) {
|
||
rightRuns.push(new TextRun({
|
||
text: segment.rewritten,
|
||
size: 22,
|
||
color: 'D97706', // 橙色
|
||
highlight: 'yellow'
|
||
}))
|
||
} else if (segment.type === 'added' && segment.rewritten) {
|
||
rightRuns.push(new TextRun({
|
||
text: segment.rewritten,
|
||
size: 22,
|
||
color: '16A34A', // 绿色
|
||
highlight: 'green'
|
||
}))
|
||
}
|
||
}
|
||
if (rightRuns.length > 0) {
|
||
children.push(new Paragraph({ children: rightRuns, spacing: { after: 400 } }))
|
||
}
|
||
|
||
// 创建文档
|
||
const doc = new Document({
|
||
sections: [{
|
||
children: children
|
||
}]
|
||
})
|
||
|
||
// 生成并下载
|
||
const blob = await Packer.toBlob(doc)
|
||
const timestamp = new Date().toISOString().slice(0, 10)
|
||
saveAs(blob, `文稿差异对比_${timestamp}.docx`)
|
||
|
||
alert('导出成功!')
|
||
} catch (error) {
|
||
console.error('导出失败:', error)
|
||
alert('导出失败,请重试')
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* ========== 使用设计令牌系统 ========== */
|
||
|
||
/* 页面容器 */
|
||
.diff-page {
|
||
height: 100vh;
|
||
width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: var(--bg-primary);
|
||
}
|
||
|
||
/* 头部工具栏 */
|
||
.diff-header {
|
||
height: 56px;
|
||
border-bottom: 1px solid var(--border-default);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 0 var(--space-6);
|
||
background: var(--bg-primary);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.diff-header-title {
|
||
font-size: var(--text-lg);
|
||
font-weight: var(--font-semibold);
|
||
color: var(--text-primary);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
}
|
||
|
||
/* 图例说明 */
|
||
.diff-legend {
|
||
font-size: var(--text-xs);
|
||
color: var(--text-muted);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-3);
|
||
}
|
||
|
||
.diff-legend-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-1);
|
||
}
|
||
|
||
.diff-legend-dot {
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: var(--radius-md);
|
||
}
|
||
|
||
.diff-legend-dot.modified {
|
||
background: rgba(245, 158, 11, 0.5);
|
||
}
|
||
|
||
.diff-legend-dot.added {
|
||
background: rgba(34, 197, 94, 0.5);
|
||
}
|
||
|
||
.diff-legend-dot.removed {
|
||
background: rgba(239, 68, 68, 0.5);
|
||
}
|
||
|
||
/* 差异统计 */
|
||
.diff-stats {
|
||
font-size: var(--text-xs);
|
||
color: var(--text-secondary);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-3);
|
||
border-left: 1px solid var(--border-default);
|
||
padding-left: var(--space-4);
|
||
}
|
||
|
||
.diff-stats .modified {
|
||
color: var(--accent-warning);
|
||
}
|
||
|
||
.diff-stats .added {
|
||
color: var(--accent-success);
|
||
}
|
||
|
||
.diff-stats .removed {
|
||
color: var(--accent-danger);
|
||
}
|
||
|
||
/* 主内容区 */
|
||
.diff-main {
|
||
flex: 1;
|
||
display: flex;
|
||
width: 100%;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 面板区域 */
|
||
.diff-panel {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
}
|
||
|
||
.diff-panel-left {
|
||
border-right: 1px solid var(--border-default);
|
||
}
|
||
|
||
/* 面板头部 */
|
||
.diff-panel-header {
|
||
padding: var(--space-3);
|
||
background: var(--bg-secondary);
|
||
border-bottom: 1px solid var(--border-default);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.diff-panel-title {
|
||
font-size: var(--text-sm);
|
||
font-weight: var(--font-medium);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
}
|
||
|
||
.diff-panel-title.left {
|
||
color: var(--accent-warning);
|
||
}
|
||
|
||
.diff-panel-title.right {
|
||
color: var(--accent-primary);
|
||
}
|
||
|
||
/* 差异显示区 */
|
||
.diff-display {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: var(--space-4);
|
||
background: rgba(15, 23, 42, 0.5);
|
||
min-height: 0;
|
||
}
|
||
|
||
.diff-content {
|
||
font-size: var(--text-sm);
|
||
line-height: 1.6;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
/* 差异片段样式 */
|
||
.diff-unchanged {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.diff-modified {
|
||
background: rgba(245, 158, 11, 0.3);
|
||
color: #fed7aa;
|
||
padding: 2px 4px;
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
|
||
.diff-removed {
|
||
background: rgba(239, 68, 68, 0.3);
|
||
color: #fecaca;
|
||
padding: 2px 4px;
|
||
border-radius: var(--radius-sm);
|
||
text-decoration: line-through;
|
||
}
|
||
|
||
.diff-added {
|
||
background: rgba(34, 197, 94, 0.3);
|
||
color: #bbf7d0;
|
||
padding: 2px 4px;
|
||
border-radius: var(--radius-sm);
|
||
}
|
||
|
||
/* 空状态 */
|
||
.diff-empty-state {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.diff-empty-icon {
|
||
font-size: 3rem;
|
||
margin-bottom: var(--space-3);
|
||
opacity: 0.3;
|
||
}
|
||
|
||
/* 可拖动分割线 */
|
||
.diff-resizer {
|
||
height: 8px;
|
||
background: var(--bg-elevated);
|
||
cursor: ns-resize;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
transition: background var(--transition-fast);
|
||
}
|
||
|
||
.diff-resizer:hover {
|
||
background: var(--border-strong);
|
||
}
|
||
|
||
.diff-resizer-handle {
|
||
width: 48px;
|
||
height: 4px;
|
||
background: var(--text-muted);
|
||
border-radius: var(--radius-full);
|
||
}
|
||
|
||
/* 输入区 */
|
||
.diff-input-area {
|
||
padding: var(--space-3);
|
||
border-top: 1px solid var(--border-default);
|
||
background: var(--bg-secondary);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.diff-textarea {
|
||
width: 100%;
|
||
height: 100%;
|
||
background: var(--bg-primary);
|
||
border: 1px solid var(--border-default);
|
||
border-radius: var(--radius-lg);
|
||
padding: var(--space-3);
|
||
font-size: var(--text-sm);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.diff-textarea:focus {
|
||
outline: none;
|
||
border-color: var(--accent-primary);
|
||
box-shadow: 0 0 0 2px var(--info-bg);
|
||
}
|
||
|
||
/* 按钮样式 */
|
||
.btn-diff {
|
||
font-size: var(--text-xs);
|
||
padding: var(--space-2) var(--space-3);
|
||
border-radius: var(--radius-md);
|
||
transition: all var(--transition-fast);
|
||
border: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.btn-diff-secondary {
|
||
background: var(--bg-elevated);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.btn-diff-secondary:hover {
|
||
background: var(--bg-sunken);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.btn-diff-primary {
|
||
background: var(--accent-primary);
|
||
color: var(--text-inverse);
|
||
}
|
||
|
||
.btn-diff-primary:hover {
|
||
background: var(--accent-primary-hover);
|
||
}
|
||
|
||
.btn-diff:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* 来源切换器 */
|
||
.source-toggle {
|
||
display: flex;
|
||
background: var(--bg-primary);
|
||
padding: var(--space-1);
|
||
border-radius: var(--radius-md);
|
||
border: 1px solid var(--border-default);
|
||
}
|
||
|
||
.source-toggle-btn {
|
||
padding: var(--space-2) var(--space-3);
|
||
font-size: var(--text-xs);
|
||
border-radius: var(--radius-sm);
|
||
transition: all var(--transition-fast);
|
||
}
|
||
|
||
.source-toggle-btn.active {
|
||
background: var(--accent-primary);
|
||
color: var(--text-inverse);
|
||
}
|
||
|
||
.source-toggle-btn:not(.active) {
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.source-toggle-btn:not(.active):hover {
|
||
color: var(--text-primary);
|
||
}
|
||
</style>
|