refactor: 按功能分类重组 components 目录结构
This commit is contained in:
750
src/components/panels/DiffAnnotationPanel.vue
Normal file
750
src/components/panels/DiffAnnotationPanel.vue
Normal file
@@ -0,0 +1,750 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user