3v324v23's picture
Initial commit with robust upload and demo data
e15a3ce
<!DOCTYPE html>
<html lang="zh-CN" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能数据炼油厂 (Smart Data Refinery)</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: '#3b82f6',
secondary: '#10b981',
dark: '#111827',
darker: '#0f172a',
panel: '#1e293b'
}
}
}
}
</script>
<style>
body { font-family: 'Inter', sans-serif; }
[v-cloak] { display: none !important; }
.glass {
background: rgba(30, 41, 59, 0.7);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: #1e293b; }
::-webkit-scrollbar-thumb { background: #475569; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #64748b; }
</style>
</head>
<body class="bg-darker text-gray-200 min-h-screen flex flex-col">
<div id="app" class="flex flex-col h-screen" v-cloak>
<!-- Header -->
<header class="h-16 border-b border-gray-700 bg-panel flex items-center justify-between px-6 shrink-0">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center font-bold text-white">D</div>
<h1 class="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-purple-400">智能数据炼油厂</h1>
</div>
<div class="flex items-center gap-4">
<button @click="loadDemoData" class="px-3 py-1.5 bg-gray-600 hover:bg-gray-700 rounded text-sm font-medium transition flex items-center gap-2" :disabled="loading">
<span>🧪 加载演示数据</span>
</button>
<button @click="exportData('csv')" class="px-3 py-1.5 bg-green-600 hover:bg-green-700 rounded text-sm font-medium transition flex items-center gap-2" :disabled="!filename">
<span>导出 CSV</span>
</button>
<button @click="exportData('json')" class="px-3 py-1.5 bg-yellow-600 hover:bg-yellow-700 rounded text-sm font-medium transition flex items-center gap-2" :disabled="!filename">
<span>导出 JSON</span>
</button>
</div>
</header>
<!-- Main Content -->
<main class="flex-1 flex overflow-hidden">
<!-- Sidebar (Pipeline) -->
<aside class="w-80 bg-panel border-r border-gray-700 flex flex-col shrink-0">
<div class="p-4 border-b border-gray-700">
<h2 class="font-semibold text-gray-300 mb-2">处理流水线 (Pipeline)</h2>
<div class="text-xs text-gray-500">按顺序执行以下操作</div>
</div>
<div class="flex-1 overflow-y-auto p-4 space-y-3">
<div v-if="operations.length === 0" class="text-center text-gray-500 py-10 border-2 border-dashed border-gray-700 rounded-lg">
暂无操作
</div>
<div v-for="(op, index) in operations" :key="index" class="bg-dark p-3 rounded border border-gray-600 relative group">
<button @click="removeOperation(index)" class="absolute top-2 right-2 text-gray-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition"></button>
<div class="text-sm font-bold text-blue-400 mb-1">${ getOpName(op.type) }</div>
<!-- Dynamic Params Display -->
<div class="text-xs text-gray-400 space-y-1">
<div v-if="op.type === 'filter'">
${ op.params.column } ${ op.params.operator } ${ op.params.value }
</div>
<div v-if="op.type === 'fillna'">
${ op.params.subset ? op.params.subset : '所有列' } -> ${ op.params.method || op.params.value }
</div>
<div v-if="op.type === 'drop_duplicates'">
${ op.params.subset ? '依据: ' + op.params.subset : '完全重复' }
</div>
<div v-if="op.type === 'sort'">
${ op.params.column } (${ op.params.ascending ? '升序' : '降序' })
</div>
<div v-if="op.type === 'select_columns'">
保留: ${ op.params.columns.join(', ') }
</div>
<div v-if="op.type === 'rename'">
重命名: ${ JSON.stringify(op.params.mapping) }
</div>
</div>
</div>
</div>
<!-- Add Operation Button -->
<div class="p-4 border-t border-gray-700 bg-panel">
<button @click="showAddOpModal = true" class="w-full py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm font-medium transition" :disabled="!filename">
+ 添加操作
</button>
</div>
</aside>
<!-- Main Area -->
<div class="flex-1 flex flex-col bg-darker overflow-hidden relative">
<!-- Upload / Empty State -->
<div v-if="!filename" class="absolute inset-0 flex items-center justify-center z-10 bg-darker/90 backdrop-blur-sm">
<div
class="w-96 h-64 border-2 border-dashed border-gray-600 rounded-xl flex flex-col items-center justify-center cursor-pointer hover:border-blue-500 hover:bg-blue-500/5 transition group"
@click="triggerFileInput"
@dragover.prevent
@drop.prevent="handleDrop"
>
<input type="file" ref="fileInput" class="hidden" @change="handleFileSelect" accept=".csv,.json,.xlsx">
<div class="text-4xl mb-4 group-hover:scale-110 transition">📂</div>
<div class="text-lg font-medium text-gray-300">点击或拖拽上传文件</div>
<div class="text-sm text-gray-500 mt-2">支持 CSV, JSON, Excel (< 16MB)</div>
</div>
</div>
<!-- Data Table -->
<div class="flex-1 overflow-auto p-0 relative">
<div v-if="loading" class="absolute inset-0 flex items-center justify-center bg-darker/50 z-20">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
</div>
<table v-if="previewData" class="w-full text-left border-collapse">
<thead class="bg-panel sticky top-0 z-10 shadow-md">
<tr>
<th v-for="col in previewColumns" :key="col" class="p-3 text-xs font-medium text-gray-400 uppercase tracking-wider border-b border-gray-700 whitespace-nowrap">
${ col }
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-800">
<tr v-for="(row, idx) in previewData" :key="idx" class="hover:bg-gray-800/50 transition">
<td v-for="col in previewColumns" :key="col" class="p-3 text-sm text-gray-300 whitespace-nowrap border-r border-gray-800 last:border-r-0">
${ row[col] }
</td>
</tr>
</tbody>
</table>
</div>
<!-- Footer Stats -->
<div class="h-10 bg-panel border-t border-gray-700 flex items-center px-4 gap-6 text-xs text-gray-400 shrink-0">
<div v-if="stats">
<span>行数: <span class="text-white">${ stats.rows }</span></span>
<span class="ml-4">列数: <span class="text-white">${ stats.columns }</span></span>
<span class="ml-4">缺失值: <span class="text-yellow-500">${ stats.missing_values }</span></span>
<span class="ml-4">重复行: <span class="text-red-500">${ stats.duplicates }</span></span>
</div>
<div class="ml-auto">
<span v-if="filename" class="text-blue-400">${ filename }</span>
</div>
</div>
</div>
</main>
<!-- Add Operation Modal -->
<div v-if="showAddOpModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50">
<div class="bg-panel border border-gray-600 rounded-lg w-[500px] shadow-2xl p-6">
<h3 class="text-lg font-bold mb-4 text-white">添加操作</h3>
<div class="mb-4">
<label class="block text-sm text-gray-400 mb-1">操作类型</label>
<select v-model="newOp.type" class="w-full bg-dark border border-gray-600 rounded px-3 py-2 text-white focus:outline-none focus:border-blue-500">
<option value="filter">筛选 (Filter)</option>
<option value="sort">排序 (Sort)</option>
<option value="fillna">填充缺失值 (Fill NA)</option>
<option value="drop_duplicates">去重 (Drop Duplicates)</option>
<option value="select_columns">选择列 (Select Columns)</option>
<option value="rename">重命名列 (Rename)</option>
</select>
</div>
<!-- Dynamic Inputs based on Type -->
<div class="space-y-3 mb-6">
<!-- Filter -->
<div v-if="newOp.type === 'filter'">
<select v-model="newOp.params.column" class="w-full bg-dark border border-gray-600 rounded px-3 py-2 text-white mb-2">
<option v-for="col in previewColumns" :value="col">${ col }</option>
</select>
<div class="flex gap-2 mb-2">
<select v-model="newOp.params.operator" class="w-1/3 bg-dark border border-gray-600 rounded px-3 py-2 text-white">
<option value="==">等于</option>
<option value="!=">不等于</option>
<option value=">">大于</option>
<option value="<">小于</option>
<option value="contains">包含</option>
</select>
<input v-model="newOp.params.value" placeholder="值" class="w-2/3 bg-dark border border-gray-600 rounded px-3 py-2 text-white">
</div>
</div>
<!-- Sort -->
<div v-if="newOp.type === 'sort'">
<select v-model="newOp.params.column" class="w-full bg-dark border border-gray-600 rounded px-3 py-2 text-white mb-2">
<option v-for="col in previewColumns" :value="col">${ col }</option>
</select>
<label class="flex items-center gap-2 text-sm text-gray-300">
<input type="checkbox" v-model="newOp.params.ascending"> 升序 (Ascending)
</label>
</div>
<!-- FillNA -->
<div v-if="newOp.type === 'fillna'">
<select v-model="newOp.params.subset" class="w-full bg-dark border border-gray-600 rounded px-3 py-2 text-white mb-2">
<option value="">所有列</option>
<option v-for="col in previewColumns" :value="col">${ col }</option>
</select>
<div class="flex gap-2">
<input v-model="newOp.params.value" placeholder="填充值 (e.g. 0, Unknown)" class="flex-1 bg-dark border border-gray-600 rounded px-3 py-2 text-white">
<select v-model="newOp.params.method" class="w-1/3 bg-dark border border-gray-600 rounded px-3 py-2 text-white">
<option value="">指定值</option>
<option value="ffill">前向填充</option>
<option value="bfill">后向填充</option>
</select>
</div>
</div>
<!-- Drop Duplicates -->
<div v-if="newOp.type === 'drop_duplicates'">
<select v-model="newOp.params.subset" class="w-full bg-dark border border-gray-600 rounded px-3 py-2 text-white mb-2">
<option value="">所有列 (完全重复)</option>
<option v-for="col in previewColumns" :value="col">${ col }</option>
</select>
</div>
<!-- Select Columns -->
<div v-if="newOp.type === 'select_columns'">
<div class="h-32 overflow-y-auto border border-gray-600 rounded p-2 bg-dark">
<label v-for="col in previewColumns" :key="col" class="flex items-center gap-2 text-sm text-gray-300 mb-1">
<input type="checkbox" :value="col" v-model="newOp.params.columns"> ${ col }
</label>
</div>
</div>
<!-- Rename -->
<div v-if="newOp.type === 'rename'">
<select v-model="tempRenameCol" class="w-full bg-dark border border-gray-600 rounded px-3 py-2 text-white mb-2">
<option v-for="col in previewColumns" :value="col">${ col }</option>
</select>
<input v-model="tempRenameVal" placeholder="新列名" class="w-full bg-dark border border-gray-600 rounded px-3 py-2 text-white">
</div>
</div>
<div class="flex justify-end gap-3">
<button @click="showAddOpModal = false" class="px-4 py-2 text-gray-400 hover:text-white transition">取消</button>
<button @click="addOperation" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded text-white font-medium transition">确认添加</button>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, reactive } = Vue
createApp({
delimiters: ['${', '}'],
setup() {
const filename = ref('')
const previewData = ref(null)
const previewColumns = ref([])
const stats = ref(null)
const operations = ref([])
const loading = ref(false)
const showAddOpModal = ref(false)
// Add Op Form
const newOp = reactive({
type: 'filter',
params: {
columns: [], // for select_columns
ascending: true
}
})
const tempRenameCol = ref('')
const tempRenameVal = ref('')
const fileInput = ref(null)
const triggerFileInput = () => fileInput.value.click()
const handleFileSelect = (e) => {
const file = e.target.files[0]
if (file) uploadFile(file)
}
const handleDrop = (e) => {
const file = e.dataTransfer.files[0]
if (file) uploadFile(file)
}
const loadDemoData = async () => {
loading.value = true
try {
const res = await axios.post('/api/load_demo')
filename.value = res.data.filename
previewData.value = res.data.preview.data
previewColumns.value = res.data.preview.columns
stats.value = res.data.preview.stats
operations.value = []
} catch (e) {
alert('Demo load failed: ' + (e.response?.data?.error || e.message))
} finally {
loading.value = false
}
}
const uploadFile = async (file) => {
// Backend limit is 50MB now, frontend warning at 50MB
if (file.size > 50 * 1024 * 1024) {
alert('文件过大,建议小于 50MB')
}
const formData = new FormData()
formData.append('file', file)
loading.value = true
try {
const res = await axios.post('/api/upload', formData)
filename.value = res.data.filename
previewData.value = res.data.preview.data
previewColumns.value = res.data.preview.columns
stats.value = res.data.preview.stats
operations.value = [] // Reset operations
} catch (e) {
alert('Upload failed: ' + (e.response?.data?.error || e.message))
} finally {
loading.value = false
}
}
const addOperation = () => {
const op = JSON.parse(JSON.stringify(newOp)) // Deep copy
// Specific logic fixes
if (op.type === 'rename') {
if (!tempRenameCol.value || !tempRenameVal.value) return
op.params.mapping = { [tempRenameCol.value]: tempRenameVal.value }
}
if (op.type === 'select_columns' && op.params.columns.length === 0) return
operations.value.push(op)
showAddOpModal.value = false
// Reset specialized params
newOp.params = { columns: [], ascending: true }
tempRenameCol.value = ''
tempRenameVal.value = ''
// Trigger process
processPipeline()
}
const removeOperation = (index) => {
operations.value.splice(index, 1)
processPipeline()
}
const processPipeline = async () => {
loading.value = true
try {
const res = await axios.post('/api/process', {
filename: filename.value,
operations: operations.value
})
previewData.value = res.data.preview.data
previewColumns.value = res.data.preview.columns
stats.value = res.data.preview.stats
} catch (e) {
alert('Processing failed: ' + (e.response?.data?.error || e.message))
} finally {
loading.value = false
}
}
const exportData = async (format) => {
try {
const res = await axios.post('/api/export', {
filename: filename.value,
operations: operations.value,
format: format
}, { responseType: 'blob' })
const url = window.URL.createObjectURL(new Blob([res.data]))
const link = document.createElement('a')
link.href = url
link.setAttribute('download', `processed_${filename.value.split('.')[0]}.${format}`)
document.body.appendChild(link)
link.click()
} catch (e) {
alert('Export failed')
}
}
const getOpName = (type) => {
const map = {
'filter': '筛选 (Filter)',
'sort': '排序 (Sort)',
'fillna': '填充缺失 (Fill NA)',
'drop_duplicates': '去重 (Dedupe)',
'select_columns': '列选择 (Select)',
'rename': '重命名 (Rename)'
}
return map[type] || type
}
return {
filename, previewData, previewColumns, stats, operations, loading,
showAddOpModal, newOp, tempRenameCol, tempRenameVal, fileInput,
triggerFileInput, handleFileSelect, handleDrop,
addOperation, removeOperation, exportData, getOpName,
loadDemoData
}
}
}).mount('#app')
</script>
</body>
</html>