<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>小说分割工具</title>
<script src="javascript/tailwindcss.js"></script>
<script src="javascript/vue.global.js"></script>
<style>
[v-cloak] {
display: none;
}
/* 禁用数字输入框滚轮事件 */
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
body,
html {
margin: 0;
padding: 0;
height: 100vh;
overflow: hidden;
}
#app {
height: 100vh;
overflow-y: auto;
}
</style>
</head>
<body class="bg-gray-50">
<div id="app" v-cloak>
<div class="w-full h-full px-6 py-6">
<!-- Main Card -->
<div class="bg-white rounded-lg shadow p-8 mb-6">
<h2 class="text-2xl font-bold mb-6 text-gray-800">上传文件或输入内容</h2>
<!-- Upload Area -->
<div @click="triggerFileInput" @dragover.prevent="isDragging = true" @dragleave="isDragging = false"
@drop.prevent="handleFileDrop"
:class="['border-2 border-dashed rounded-lg p-16 text-center cursor-pointer transition-all mb-6',
isDragging ? 'border-purple-600 bg-purple-50' : 'border-purple-400 hover:border-purple-600 hover:bg-purple-50']">
<div v-if="!selectedFile">
<svg class="w-20 h-20 mx-auto mb-4 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<p class="text-lg text-gray-600 mb-2">点击选择文件或拖拽文件到这里</p>
<p class="text-sm text-gray-400">支持 .txt 文件</p>
</div>
<div v-else>
<svg class="w-20 h-20 mx-auto mb-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p class="text-lg text-gray-700 mb-2">已选择: {{ selectedFile.name }}</p>
<p class="text-sm text-gray-400">{{ (selectedFile.size / 1024).toFixed(2) }} KB</p>
</div>
<input type="file" ref="fileInput" @change="handleFileSelect" accept=".txt" class="hidden">
</div>
<!-- Options -->
<div class="mb-6">
<label class="block text-sm font-semibold text-gray-700 mb-2">最大字符数</label>
<input v-model.number="maxChars" type="number" min="100" max="5000" @wheel.prevent
class="w-full px-4 py-3 border-2 border-gray-200 rounded-lg focus:outline-none focus:border-purple-500 transition-colors">
<p class="text-sm text-gray-500 mt-2">将自动创建项目并初始化改写任务</p>
</div>
<!-- Content Input -->
<div class="mb-6">
<label class="block text-sm font-semibold text-gray-700 mb-2">或者直接输入内容</label>
<textarea v-model="contentInput" placeholder="在这里粘贴或输入小说内容..."
class="w-full px-4 py-3 border-2 border-gray-200 rounded-lg focus:outline-none focus:border-purple-500 transition-colors font-mono min-h-[200px] resize-y"></textarea>
</div>
<!-- Action Button -->
<button @click="processSplit" :disabled="isProcessing"
:class="['w-full py-4 rounded-lg font-bold text-lg transition-all flex items-center justify-center gap-3',
isProcessing ? 'bg-gray-400 cursor-not-allowed' : 'bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-700 hover:to-indigo-700 text-white shadow-lg hover:shadow-xl transform hover:-translate-y-0.5']">
<svg v-if="!isProcessing" class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<svg v-else class="w-6 h-6 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
{{ isProcessing ? '处理中...' : '开始分割' }}
</button>
<!-- Error Message -->
<div v-if="errorMessage" class="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg flex items-start gap-3">
<svg class="w-6 h-6 text-red-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p class="text-red-700">{{ errorMessage }}</p>
</div>
</div>
<!-- Results -->
<div v-if="result" class="bg-white rounded-lg shadow p-8">
<h2 class="text-2xl font-bold mb-6 text-gray-800 flex items-center gap-3">
<svg class="w-8 h-8 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
分割结果
</h2>
<!-- Stats -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div class="bg-gradient-to-br from-purple-50 to-purple-100 p-4 rounded-lg">
<h3 class="text-sm text-gray-600 mb-1">总段落数</h3>
<p class="text-3xl font-bold text-purple-600">{{ result.metadata.totalParagraphs }}</p>
</div>
<div class="bg-gradient-to-br from-blue-50 to-blue-100 p-4 rounded-lg">
<h3 class="text-sm text-gray-600 mb-1">总 Token 数</h3>
<p class="text-3xl font-bold text-blue-600">{{ result.metadata.totalTokens }}</p>
</div>
<div class="bg-gradient-to-br from-green-50 to-green-100 p-4 rounded-lg">
<h3 class="text-sm text-gray-600 mb-1">平均每段 Token</h3>
<p class="text-3xl font-bold text-green-600">{{ result.metadata.averageTokensPerParagraph }}</p>
</div>
<div class="bg-gradient-to-br from-orange-50 to-orange-100 p-4 rounded-lg">
<h3 class="text-sm text-gray-600 mb-1">最大字符数</h3>
<p class="text-3xl font-bold text-orange-600">{{ result.metadata.maxChars }}</p>
</div>
</div>
<!-- Encoding Info -->
<div v-if="result.encoding"
class="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg flex items-start gap-3">
<svg class="w-6 h-6 text-blue-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p class="text-blue-800 font-semibold">文件编码: {{ result.encoding.toUpperCase() }}</p>
<p class="text-blue-600 text-sm mt-1">已自动识别并正确解码</p>
</div>
</div>
<!-- Task Init Info -->
<div v-if="result.task_init_success" class="mb-6 p-4 bg-green-50 border border-green-200 rounded-lg">
<div class="flex items-start gap-3 mb-3">
<svg class="w-6 h-6 text-green-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div class="flex-1">
<p class="text-green-800 font-semibold">{{ result.task_init_message }}</p>
<p class="text-green-600 text-sm mt-1">项目: {{ result.project_name }}</p>
</div>
</div>
<a @click.prevent="goToTasks(result.project_name)"
class="inline-flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-lg transition-colors cursor-pointer">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
进入任务管理
</a>
</div>
<div v-else-if="result.task_init_success === false"
class="mb-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg flex items-start gap-3">
<svg class="w-6 h-6 text-yellow-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div>
<p class="text-yellow-800 font-semibold">任务初始化提示</p>
<p class="text-yellow-700 text-sm mt-1">{{ result.task_init_message }}</p>
</div>
</div>
<!-- Paragraphs -->
<h3 class="text-xl font-bold mb-4 text-gray-800">段落列表</h3>
<div class="max-h-[600px] overflow-y-auto space-y-3">
<div v-for="para in result.paragraphs" :key="para.index"
class="bg-gray-50 border-l-4 border-purple-500 p-4 rounded-r-lg hover:shadow transition-shadow">
<h4 class="text-sm font-semibold text-gray-600 mb-2">
段落 {{ para.index }} - {{ para.length }} 字符 / {{ para.tokens }} tokens
</h4>
<p class="text-gray-700 whitespace-pre-wrap leading-relaxed">{{ para.content }}</p>
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref } = Vue;
// 如果不在 iframe 中,提示用户访问主页面
if (window.self === window.top) {
const useMainPage = confirm('建议使用主页面以获得更好的体验,是否跳转?');
if (useMainPage) {
window.location.href = 'index.html?tab=split';
}
}
createApp({
setup() {
// 响应式数据
const selectedFile = ref(null);
const contentInput = ref('');
const maxChars = ref(500);
const isProcessing = ref(false);
const isDragging = ref(false);
const errorMessage = ref('');
const result = ref(null);
const fileInput = ref(null);
// 方法
const triggerFileInput = () => {
fileInput.value.click();
};
const handleFileSelect = (e) => {
const file = e.target.files[0];
if (file && file.name.endsWith('.txt')) {
selectedFile.value = file;
contentInput.value = '';
errorMessage.value = '';
} else {
showError('请上传 .txt 文件');
}
};
const handleFileDrop = (e) => {
isDragging.value = false;
const file = e.dataTransfer.files[0];
if (file && file.name.endsWith('.txt')) {
selectedFile.value = file;
contentInput.value = '';
errorMessage.value = '';
} else {
showError('请上传 .txt 文件');
}
};
const processSplit = async () => {
if (!selectedFile.value && !contentInput.value.trim()) {
showError('请选择文件或输入内容');
return;
}
if (maxChars.value < 100 || maxChars.value > 5000) {
showError('最大字符数必须在 100-5000 之间');
return;
}
isProcessing.value = true;
errorMessage.value = '';
result.value = null;
try {
const formData = new FormData();
if (selectedFile.value) {
formData.append('file', selectedFile.value);
} else {
formData.append('content', contentInput.value);
}
formData.append('max_chars', maxChars.value.toString());
// 第一步:分割小说
const response = await fetch('/api/split', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
result.value = data;
// 第二步:自动初始化任务
const initResponse = await fetch('/api/init_tasks', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
project_name: data.project_name,
source_dir: data.output_dir,
force: false
})
});
const initData = await initResponse.json();
if (initData.success) {
result.value.task_init_message = initData.message;
result.value.task_init_success = true;
} else {
result.value.task_init_message = initData.message || '任务初始化失败';
result.value.task_init_success = false;
}
} else {
showError(data.error || '处理失败');
}
} catch (error) {
showError('网络错误: ' + error.message);
} finally {
isProcessing.value = false;
}
};
const showError = (message) => {
errorMessage.value = message;
setTimeout(() => {
errorMessage.value = '';
}, 5000);
};
const goToTasks = (projectName) => {
// 如果在 iframe 中,通知父窗口切换到任务管理页面
if (window.parent !== window) {
window.parent.postMessage({ action: 'switchTab', tab: 'tasks', project: projectName }, '*');
} else {
// 如果不在 iframe 中,直接跳转
window.location.href = `tasks.html?project=${projectName}`;
}
};
return {
selectedFile,
contentInput,
maxChars,
isProcessing,
isDragging,
errorMessage,
result,
fileInput,
triggerFileInput,
handleFileSelect,
handleFileDrop,
processSplit,
showError,
goToTasks
};
}
}).mount('#app');
</script>
</body>
</html>