LiRen-qiu 4 months ago
parent 53af4689fc
commit 6c3bfe54f5

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

@ -0,0 +1,22 @@
<template>
<div id="app">
<main-layout>
<router-view />
</main-layout>
</div>
</template>
<script>
import MainLayout from './components/layout/MainLayout.vue'
export default {
name: 'App',
components: {
MainLayout
}
}
</script>
<style>
/* 全局样式已经移至assets/css/global.css */
</style>

@ -0,0 +1,71 @@
import request from './index'
import Mock from '../utils/mock'
// 是否使用模拟数据
const useMock = process.env.NODE_ENV === 'development'
// 获取词频列表
export function getTermFrequencies() {
if (useMock) {
return new Promise(resolve => {
setTimeout(() => {
resolve(Mock.getTermFrequencies())
}, 800)
})
}
return request({
url: '/analysis/terms',
method: 'get'
})
}
// 获取词的上下文
export function getTermContexts(term) {
if (useMock) {
return new Promise(resolve => {
setTimeout(() => {
resolve(Mock.getTermContexts(term))
}, 800)
})
}
return request({
url: '/analysis/contexts',
method: 'get',
params: { term }
})
}
// 获取词的搭配词
export function getTermCollocates(term) {
if (useMock) {
return new Promise(resolve => {
setTimeout(() => {
resolve(Mock.getTermCollocates(term))
}, 800)
})
}
return request({
url: '/analysis/collocates',
method: 'get',
params: { term }
})
}
// 获取文档段落数据
export function getDocumentSegments() {
if (useMock) {
return new Promise(resolve => {
setTimeout(() => {
resolve(Mock.getDocumentSegments())
}, 800)
})
}
return request({
url: '/analysis/segments',
method: 'get'
})
}

@ -0,0 +1,37 @@
import axios from 'axios'
import store from '../store'
// 创建axios实例
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API || '/api',
timeout: 10000
})
// 请求拦截器
service.interceptors.request.use(
config => {
// 在发送请求前显示加载状态
store.dispatch('app/setLoading', true)
return config
},
error => {
store.dispatch('app/setLoading', false)
return Promise.reject(error)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
store.dispatch('app/setLoading', false)
return response.data
},
error => {
store.dispatch('app/setLoading', false)
const errorMsg = error.response?.data?.message || error.message
store.dispatch('app/setError', errorMsg)
return Promise.reject(error)
}
)
export default service

@ -0,0 +1,96 @@
import request from './index'
import Mock from '../utils/mock'
// 是否使用模拟数据
const useMock = process.env.NODE_ENV === 'development'
// 上传文件API
export function uploadFile(file) {
if (useMock) {
return new Promise(resolve => {
setTimeout(() => {
resolve(Mock.getUploadData())
}, 1000)
})
}
const formData = new FormData()
formData.append('file', file)
return request({
url: '/upload',
method: 'post',
data: formData,
headers: {
'Content-Type': 'multipart/form-data'
}
})
}
// 预处理数据API
export function preprocessData(data) {
if (useMock) {
return new Promise(resolve => {
setTimeout(() => {
resolve(Mock.getPreprocessData())
}, 1500)
})
}
return request({
url: '/preprocess',
method: 'post',
data
})
}
// 合并格式API
export function mergeFormat(data) {
if (useMock) {
return new Promise(resolve => {
setTimeout(() => {
resolve(Mock.getMergeData())
}, 1500)
})
}
return request({
url: '/merge',
method: 'post',
data
})
}
// 单词纠错API
export function correctWords(data) {
if (useMock) {
return new Promise(resolve => {
setTimeout(() => {
resolve(Mock.getCorrectionData())
}, 1500)
})
}
return request({
url: '/correct',
method: 'post',
data
})
}
// 大模型分析API
export function analyzeText(data) {
if (useMock) {
return new Promise(resolve => {
setTimeout(() => {
resolve(Mock.getAnalysisData())
}, 2000)
})
}
return request({
url: '/analyze',
method: 'post',
data
})
}

@ -0,0 +1,34 @@
/* 全局样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body, #app {
height: 100%;
width: 100%;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
}
body {
background-image: url('../images/background.png');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* 过渡动画 */
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

@ -0,0 +1,58 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

@ -0,0 +1,61 @@
<template>
<el-dialog
title="错误提示"
:visible.sync="dialogVisible"
width="30%"
@close="handleClose"
>
<div class="error-content">
<i class="el-icon-error error-icon"></i>
<p class="error-text">{{ error }}</p>
</div>
<span slot="footer" class="dialog-footer">
<el-button @click="handleClose"></el-button>
</span>
</el-dialog>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
name: 'ErrorMessage',
computed: {
...mapState('app', ['error']),
dialogVisible() {
return !!this.error
}
},
methods: {
...mapActions('app', ['clearError']),
handleClose() {
this.clearError()
}
}
}
</script>
<style scoped>
.error-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 10px 0;
}
.error-icon {
font-size: 48px;
color: #f56c6c;
margin-bottom: 15px;
}
.error-text {
font-size: 16px;
color: #606266;
text-align: center;
word-break: break-word;
}
</style>

@ -0,0 +1,58 @@
<template>
<div v-if="loading" class="global-loading">
<div class="loading-mask"></div>
<div class="loading-content">
<el-progress type="circle" :percentage="0" :indeterminate="true"></el-progress>
<p class="loading-text">处理中请稍候...</p>
</div>
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'GlobalLoading',
computed: {
...mapState('app', ['loading'])
}
}
</script>
<style scoped>
.global-loading {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
display: flex;
justify-content: center;
align-items: center;
}
.loading-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
}
.loading-content {
position: relative;
z-index: 10000;
background-color: #fff;
padding: 30px;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
text-align: center;
}
.loading-text {
margin-top: 15px;
color: #606266;
}
</style>

@ -0,0 +1,63 @@
<template>
<div class="app-header">
<div class="header-container">
<div class="logo">
<router-link to="/" style="text-decoration: none;">
<h1>航班数据处理及分析系统</h1>
</router-link>
</div>
<div class="nav-menu">
<router-link to="/process" active-class="active">
<el-button type="text">处理流程</el-button>
</router-link>
<router-link to="/analysis" active-class="active">
<el-button type="text">分析结果</el-button>
</router-link>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'AppHeader'
}
</script>
<style scoped>
.app-header {
background-color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
.header-container {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
height: 60px;
padding: 0 20px;
}
.logo h1 {
margin: 0;
font-size: 20px;
color: #1890ff;
}
.nav-menu {
display: flex;
gap: 20px;
}
.active button {
color: #1890ff;
font-weight: bold;
}
</style>

@ -0,0 +1,42 @@
<template>
<div class="main-layout">
<app-header />
<div class="main-content">
<slot></slot>
</div>
<el-backtop :right="20" :bottom="20"></el-backtop>
<global-loading />
<error-message />
</div>
</template>
<script>
import AppHeader from './Header.vue'
import GlobalLoading from '../base/GlobalLoading.vue'
import ErrorMessage from '../base/ErrorMessage.vue'
export default {
name: 'MainLayout',
components: {
AppHeader,
GlobalLoading,
ErrorMessage
}
}
</script>
<style scoped>
.main-layout {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.main-content {
flex: 1;
margin-top: 60px;
padding-top: 20px;
background-color: #f0f2f5;
min-height: calc(100vh - 60px);
}
</style>

@ -0,0 +1,627 @@
<template>
<div class="data-preprocess">
<process-step
title="数据预处理"
:status="preprocessStatus"
:loading="loading"
operationButtonText="开始预处理"
processingText="数据预处理中..."
:processingPercentage="preprocessProgress"
errorText="预处理失败,请重试"
@start="handleStartPreprocess"
@retry="handleRetry"
>
<template #pre-operation>
<div class="preprocess-config">
<h4>预处理配置</h4>
<el-form ref="configForm" :model="configForm" label-width="120px" size="small">
<el-form-item label="处理方式">
<el-radio-group v-model="configForm.processType">
<el-radio label="auto">自动处理</el-radio>
<el-radio label="manual">手动配置</el-radio>
</el-radio-group>
</el-form-item>
<template v-if="configForm.processType === 'manual'">
<el-form-item label="缺失值处理">
<el-select v-model="configForm.missingValueStrategy" placeholder="请选择缺失值处理方式">
<el-option label="删除包含缺失值的行" value="remove_row"></el-option>
<el-option label="删除包含缺失值的列" value="remove_column"></el-option>
<el-option label="使用平均值填充" value="fill_mean"></el-option>
<el-option label="使用中位数填充" value="fill_median"></el-option>
<el-option label="使用众数填充" value="fill_mode"></el-option>
<el-option label="使用指定值填充" value="fill_value"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="configForm.missingValueStrategy === 'fill_value'" label="填充值">
<el-input v-model="configForm.fillValue" placeholder="请输入填充值"></el-input>
</el-form-item>
<el-form-item label="异常值处理">
<el-select v-model="configForm.outlierStrategy" placeholder="请选择异常值处理方式">
<el-option label="不处理" value="none"></el-option>
<el-option label="删除异常值所在行" value="remove"></el-option>
<el-option label="使用均值替换" value="replace_mean"></el-option>
<el-option label="使用中位数替换" value="replace_median"></el-option>
<el-option label="使用上/下限替换" value="replace_boundary"></el-option>
</el-select>
</el-form-item>
<el-form-item label="数据标准化">
<el-select v-model="configForm.normalizationMethod" placeholder="请选择标准化方法">
<el-option label="不标准化" value="none"></el-option>
<el-option label="Z-Score标准化" value="z_score"></el-option>
<el-option label="Min-Max标准化" value="min_max"></el-option>
<el-option label="最大绝对值标准化" value="max_abs"></el-option>
</el-select>
</el-form-item>
</template>
<el-form-item v-if="configForm.processType === 'auto'" label="自动处理说明">
<div class="auto-process-info">
<p>自动处理将</p>
<ol>
<li>删除所有包含过多缺失值的列</li>
<li>使用均值填充数值型缺失值</li>
<li>使用众数填充分类型缺失值</li>
<li>使用IQR方法检测并处理异常值</li>
<li>对数值型数据进行Z-Score标准化</li>
</ol>
</div>
</el-form-item>
</el-form>
</div>
</template>
<template #result-summary>
<div class="preprocess-result-summary">
<i class="el-icon-check"></i>
<h4>数据预处理完成</h4>
<div class="summary-stats">
<div class="stat-item">
<span class="stat-value">{{ resultData.rowCount }}</span>
<span class="stat-label">处理后行数</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ resultData.columnCount }}</span>
<span class="stat-label">处理后列数</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ resultData.missingValueCount }}</span>
<span class="stat-label">处理的缺失值</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ resultData.outlierCount }}</span>
<span class="stat-label">处理的异常值</span>
</div>
</div>
</div>
</template>
<template #result-detail>
<div class="preprocess-result-detail">
<el-tabs type="border-card">
<el-tab-pane label="处理结果对比">
<div class="compare-container">
<div class="compare-item">
<h5>处理前原始数据</h5>
<el-table :data="resultData.originalSample.rows" border size="small" height="300">
<el-table-column
v-for="(col, index) in resultData.originalSample.columns"
:key="'orig-' + index"
:prop="col.prop"
:label="col.label"
></el-table-column>
</el-table>
<div class="data-stats">
<p>原始行数: <span>{{ resultData.originalRowCount }}</span></p>
<p>原始列数: <span>{{ resultData.originalColumnCount }}</span></p>
</div>
</div>
<div class="compare-item">
<h5>处理后数据</h5>
<el-table :data="resultData.processedSample.rows" border size="small" height="300">
<el-table-column
v-for="(col, index) in resultData.processedSample.columns"
:key="'proc-' + index"
:prop="col.prop"
:label="col.label"
></el-table-column>
</el-table>
<div class="data-stats">
<p>处理后行数: <span>{{ resultData.rowCount }}</span></p>
<p>处理后列数: <span>{{ resultData.columnCount }}</span></p>
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="处理详情">
<div class="process-details">
<el-timeline>
<el-timeline-item
v-for="(activity, index) in resultData.processingSteps"
:key="index"
:type="activity.type"
:color="activity.color"
:timestamp="activity.timestamp"
>
{{ activity.content }}
</el-timeline-item>
</el-timeline>
</div>
</el-tab-pane>
<el-tab-pane label="数据统计">
<div class="data-statistics">
<el-row :gutter="20">
<el-col :span="12">
<div class="statistic-card">
<h5>数值型字段统计</h5>
<el-table :data="resultData.numericStats" border size="small">
<el-table-column prop="column" label="列名"></el-table-column>
<el-table-column prop="mean" label="均值"></el-table-column>
<el-table-column prop="median" label="中位数"></el-table-column>
<el-table-column prop="std" label="标准差"></el-table-column>
<el-table-column prop="min" label="最小值"></el-table-column>
<el-table-column prop="max" label="最大值"></el-table-column>
</el-table>
</div>
</el-col>
<el-col :span="12">
<div class="statistic-card">
<h5>分类型字段统计</h5>
<div v-for="(item, index) in resultData.categoricalStats" :key="index" class="categorical-stats">
<h6>{{ item.column }}</h6>
<el-table :data="item.values" border size="small">
<el-table-column prop="value" label="取值"></el-table-column>
<el-table-column prop="count" label="计数"></el-table-column>
<el-table-column prop="percentage" label="比例"></el-table-column>
</el-table>
</div>
</div>
</el-col>
</el-row>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
</process-step>
</div>
</template>
<script>
import ProcessStep from './ProcessStep'
import { mapState, mapActions } from 'vuex'
export default {
name: 'DataPreprocessComponent',
components: {
ProcessStep
},
data() {
return {
loading: false,
preprocessStatus: 'pending',
preprocessProgress: 0,
preprocessTimer: null,
configForm: {
processType: 'auto',
missingValueStrategy: 'fill_mean',
fillValue: '0',
outlierStrategy: 'remove',
normalizationMethod: 'z_score'
},
resultData: {
rowCount: 0,
columnCount: 0,
originalRowCount: 0,
originalColumnCount: 0,
missingValueCount: 0,
outlierCount: 0,
originalSample: {
columns: [],
rows: []
},
processedSample: {
columns: [],
rows: []
},
processingSteps: [],
numericStats: [],
categoricalStats: []
}
}
},
computed: {
...mapState({
fileData: state => state.process.fileData,
fileUploaded: state => state.process.fileUploaded,
preprocessed: state => state.process.preprocessed,
preprocessData: state => state.process.preprocessData
}),
canProcess() {
return this.fileUploaded && !this.preprocessed
}
},
watch: {
preprocessed(newVal) {
if (newVal) {
this.preprocessStatus = 'completed'
this.resultData = this.preprocessData
}
}
},
mounted() {
if (this.fileUploaded && !this.preprocessed) {
this.initializeResultData()
} else if (this.preprocessed) {
this.preprocessStatus = 'completed'
this.resultData = this.preprocessData
}
},
methods: {
...mapActions('process', [
'preprocessDataAction',
'setPreprocessData'
]),
initializeResultData() {
if (this.fileData && this.fileData.previewData) {
this.resultData.originalSample = JSON.parse(JSON.stringify(this.fileData.previewData))
this.resultData.originalRowCount = this.fileData.rowCount
this.resultData.originalColumnCount = this.fileData.columnCount
}
},
handleStartPreprocess() {
if (!this.fileUploaded) {
this.$message.warning('请先上传文件')
return
}
this.loading = true
this.preprocessStatus = 'processing'
this.preprocessProgress = 0
//
this.preprocessTimer = setInterval(() => {
this.preprocessProgress += 5
if (this.preprocessProgress >= 100) {
clearInterval(this.preprocessTimer)
this.simulatePreprocessComplete()
}
}, 200)
// API
this.preprocessDataAction({
config: this.configForm
}).catch(error => {
clearInterval(this.preprocessTimer)
this.preprocessStatus = 'error'
this.loading = false
this.$message.error('数据预处理失败:' + (error.message || '未知错误'))
})
},
simulatePreprocessComplete() {
//
setTimeout(() => {
this.loading = false
//
const mockResult = this.generateMockResult()
// Vuex
this.setPreprocessData(mockResult)
this.resultData = mockResult
this.preprocessStatus = 'completed'
}, 500)
},
handleRetry() {
this.preprocessStatus = 'pending'
this.preprocessProgress = 0
},
generateMockResult() {
//
const originalRowCount = this.fileData.rowCount || 1000
const originalColumnCount = this.fileData.columnCount || 8
//
const processedRowCount = Math.floor(originalRowCount * 0.95)
const processedColumnCount = originalColumnCount - 1
//
const processingSteps = [
{
content: `移除了2列包含超过50%缺失值的字段`,
timestamp: '10:21:30',
type: 'primary',
color: '#409EFF'
},
{
content: `使用均值填充了${Math.floor(originalRowCount * 0.15)}个数值型缺失值`,
timestamp: '10:21:40',
type: 'success',
color: '#67C23A'
},
{
content: `使用众数填充了${Math.floor(originalRowCount * 0.08)}个分类型缺失值`,
timestamp: '10:21:45',
type: 'success',
color: '#67C23A'
},
{
content: `检测并处理了${Math.floor(originalRowCount * 0.03)}个异常值`,
timestamp: '10:21:55',
type: 'warning',
color: '#E6A23C'
},
{
content: `对所有数值型数据进行了${this.configForm.normalizationMethod === 'z_score' ? 'Z-Score' : 'Min-Max'}标准化`,
timestamp: '10:22:05',
type: 'info',
color: '#909399'
}
]
//
const numericStats = []
for (let i = 0; i < Math.min(4, processedColumnCount); i++) {
numericStats.push({
column: `数值列${i + 1}`,
mean: (Math.random() * 100).toFixed(2),
median: (Math.random() * 100).toFixed(2),
std: (Math.random() * 20).toFixed(2),
min: (Math.random() * 10).toFixed(2),
max: (Math.random() * 200 + 50).toFixed(2)
})
}
//
const categoricalStats = []
for (let i = 0; i < 2; i++) {
const values = []
const categories = ['A', 'B', 'C', 'D', 'E']
let totalCount = 0
categories.forEach(cat => {
const count = Math.floor(Math.random() * 500) + 100
totalCount += count
values.push({
value: cat,
count: count
})
})
//
values.forEach(v => {
v.percentage = ((v.count / totalCount) * 100).toFixed(2) + '%'
})
categoricalStats.push({
column: `分类列${i + 1}`,
values: values
})
}
//
const originalSample = {
columns: [],
rows: []
}
//
const processedSample = {
columns: [],
rows: []
}
//
for (let i = 0; i < originalColumnCount; i++) {
originalSample.columns.push({
prop: 'col' + i,
label: `${i + 1}`
})
}
for (let i = 0; i < processedColumnCount; i++) {
processedSample.columns.push({
prop: 'col' + i,
label: `${i + 1}`
})
}
//
const sampleRowCount = 5
for (let i = 0; i < sampleRowCount; i++) {
const originalRow = {}
const processedRow = {}
for (let j = 0; j < originalColumnCount; j++) {
//
if (i === 1 && j === 2) {
originalRow['col' + j] = null //
} else if (i === 3 && j === 1) {
originalRow['col' + j] = 9999 //
} else {
originalRow['col' + j] = `数据 ${i + 1}-${j + 1}`
}
}
for (let j = 0; j < processedColumnCount; j++) {
//
processedRow['col' + j] = `数据 ${i + 1}-${j + 1}`
}
originalSample.rows.push(originalRow)
processedSample.rows.push(processedRow)
}
return {
rowCount: processedRowCount,
columnCount: processedColumnCount,
originalRowCount: originalRowCount,
originalColumnCount: originalColumnCount,
missingValueCount: Math.floor(originalRowCount * 0.23),
outlierCount: Math.floor(originalRowCount * 0.03),
originalSample: originalSample,
processedSample: processedSample,
processingSteps: processingSteps,
numericStats: numericStats,
categoricalStats: categoricalStats
}
}
},
beforeDestroy() {
if (this.preprocessTimer) {
clearInterval(this.preprocessTimer)
}
}
}
</script>
<style scoped>
.data-preprocess {
margin-bottom: 20px;
}
.preprocess-config {
margin-bottom: 20px;
}
.preprocess-config h4 {
margin-bottom: 15px;
font-size: 16px;
color: #303133;
}
.auto-process-info {
color: #606266;
font-size: 14px;
line-height: 1.5;
background-color: #f5f7fa;
padding: 10px 15px;
border-radius: 4px;
}
.auto-process-info ol {
margin-top: 5px;
margin-bottom: 0;
padding-left: 20px;
}
.preprocess-result-summary {
text-align: center;
padding: 15px 0;
}
.preprocess-result-summary i {
font-size: 40px;
color: #67c23a;
margin-bottom: 10px;
}
.preprocess-result-summary h4 {
margin: 10px 0;
font-size: 18px;
color: #303133;
}
.summary-stats {
display: flex;
justify-content: center;
gap: 30px;
margin-top: 20px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #409EFF;
}
.stat-label {
font-size: 14px;
color: #606266;
margin-top: 5px;
}
.compare-container {
display: flex;
gap: 20px;
}
.compare-item {
flex: 1;
}
.compare-item h5 {
margin: 0 0 10px 0;
font-size: 16px;
color: #303133;
}
.data-stats {
margin-top: 10px;
color: #606266;
font-size: 13px;
}
.data-stats p {
margin: 5px 0;
}
.data-stats span {
font-weight: bold;
color: #303133;
}
.process-details {
padding: 10px;
}
.data-statistics {
margin-top: 10px;
}
.statistic-card {
margin-bottom: 20px;
}
.statistic-card h5 {
margin: 0 0 10px 0;
font-size: 16px;
color: #303133;
}
.categorical-stats {
margin-bottom: 15px;
}
.categorical-stats h6 {
margin: 5px 0;
font-size: 14px;
color: #303133;
}
</style>

@ -0,0 +1,283 @@
<template>
<div class="data-preprocessing">
<ProcessStep
title="数据预处理"
:status="status"
:loading="loading"
:progress="progress"
@prev="$emit('prev')"
@next="$emit('next')"
>
<template #pre-operation>
<div class="file-info" v-if="fileData">
<h3>待处理文件信息</h3>
<el-descriptions border>
<el-descriptions-item label="文件名">{{ fileData.fileName }}</el-descriptions-item>
<el-descriptions-item label="文件大小">{{ formatFileSize(fileData.fileSize) }}</el-descriptions-item>
<el-descriptions-item label="记录数量">{{ fileData.rowCount }}</el-descriptions-item>
<el-descriptions-item label="上传时间">{{ formatDateTime(fileData.uploadTime) }}</el-descriptions-item>
</el-descriptions>
</div>
<el-empty v-else description="请先完成文件上传"></el-empty>
</template>
<template #operation>
<div class="preprocessing-config">
<h3>预处理配置</h3>
<el-form :model="preprocessConfig" label-width="180px" :disabled="status === 'running'">
<el-form-item label="数据清洗级别">
<el-select v-model="preprocessConfig.cleanLevel" placeholder="请选择清洗级别">
<el-option label="基本清洗" value="basic"></el-option>
<el-option label="标准清洗" value="standard"></el-option>
<el-option label="深度清洗" value="deep"></el-option>
</el-select>
</el-form-item>
<el-form-item label="去除重复项">
<el-switch v-model="preprocessConfig.removeDuplicates"></el-switch>
</el-form-item>
<el-form-item label="标准化文本">
<el-switch v-model="preprocessConfig.normalizeText"></el-switch>
</el-form-item>
<el-form-item label="移除缺失值">
<el-select v-model="preprocessConfig.missingValueStrategy" placeholder="选择处理策略">
<el-option label="保留" value="keep"></el-option>
<el-option label="移除行" value="remove"></el-option>
<el-option label="填充默认值" value="fillDefault"></el-option>
</el-select>
</el-form-item>
<el-form-item label="字段选择">
<el-checkbox-group v-model="preprocessConfig.selectedFields">
<el-checkbox label="title">标题</el-checkbox>
<el-checkbox label="content">内容</el-checkbox>
<el-checkbox label="category">分类</el-checkbox>
<el-checkbox label="tags">标签</el-checkbox>
<el-checkbox label="timestamp">时间戳</el-checkbox>
</el-checkbox-group>
</el-form-item>
</el-form>
<div class="operation-buttons">
<el-button
type="primary"
@click="startPreprocess"
:disabled="!fileData || status === 'running' || status === 'completed'"
:loading="status === 'running'"
>
{{ status === 'running' ? '处理中...' : '开始预处理' }}
</el-button>
</div>
</div>
</template>
<template #result-summary v-if="status === 'completed'">
<div class="preprocessing-summary">
<h3>预处理完成</h3>
<el-descriptions border>
<el-descriptions-item label="处理前记录">{{ fileData.rowCount }}</el-descriptions-item>
<el-descriptions-item label="处理后记录">{{ processedData.rowCount }}</el-descriptions-item>
<el-descriptions-item label="移除重复项">{{ preprocessConfig.removeDuplicates ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="清洗级别">{{ preprocessLevelText }}</el-descriptions-item>
<el-descriptions-item label="处理用时">{{ processingTime }}</el-descriptions-item>
</el-descriptions>
</div>
</template>
<template #result-detail v-if="status === 'completed'">
<div class="preprocessing-detail">
<h3>数据预览</h3>
<el-table
:data="previewData"
border
style="width: 100%"
max-height="300px"
>
<el-table-column
v-for="field in preprocessConfig.selectedFields"
:key="field"
:prop="field"
:label="getFieldLabel(field)"
min-width="120"
></el-table-column>
</el-table>
</div>
</template>
</ProcessStep>
</div>
</template>
<script>
import ProcessStep from './ProcessStep.vue';
export default {
name: 'DataPreprocessing',
components: {
ProcessStep
},
props: {
status: {
type: String,
default: 'waiting',
validator: value => ['waiting', 'running', 'completed', 'error'].includes(value)
},
loading: {
type: Boolean,
default: false
},
progress: {
type: Number,
default: 0
},
fileData: {
type: Object,
default: null
}
},
data() {
return {
preprocessConfig: {
cleanLevel: 'standard',
removeDuplicates: true,
normalizeText: true,
missingValueStrategy: 'remove',
selectedFields: ['title', 'content', 'category']
},
processedData: null,
previewData: [],
processingStartTime: null,
processingEndTime: null
};
},
computed: {
preprocessLevelText() {
const levels = {
basic: '基本清洗',
standard: '标准清洗',
deep: '深度清洗'
};
return levels[this.preprocessConfig.cleanLevel] || '未选择';
},
processingTime() {
if (!this.processingStartTime || !this.processingEndTime) return 0;
return ((this.processingEndTime - this.processingStartTime) / 1000).toFixed(2);
}
},
watch: {
status(newStatus) {
if (newStatus === 'completed' && this.fileData) {
this.generateProcessedData();
}
}
},
methods: {
startPreprocess() {
this.processingStartTime = Date.now();
//
this.$emit('start-preprocess', { ...this.preprocessConfig });
},
generateProcessedData() {
this.processingEndTime = Date.now();
//
//
if (this.fileData) {
this.processedData = {
rowCount: this.fileData.rowCount - Math.floor(Math.random() * 50),
processedAt: new Date().toISOString()
};
//
this.previewData = this.generatePreviewData();
}
},
generatePreviewData() {
//
const sampleData = [];
const fieldValues = {
title: ['项目报告', '季度总结', '市场分析', '用户调研', '功能规划'],
content: ['这是一份详细的项目报告...', '本季度业绩表现良好...', '市场竞争激烈,需要...', '根据用户反馈,我们...', '未来三个月的功能开发计划...'],
category: ['报告', '总结', '分析', '调研', '规划'],
tags: ['项目,重要', '季度,业绩', '市场,竞争', '用户,反馈', '功能,开发'],
timestamp: ['2023-10-01', '2023-10-15', '2023-11-01', '2023-11-15', '2023-12-01']
};
for (let i = 0; i < 5; i++) {
const row = {};
this.preprocessConfig.selectedFields.forEach(field => {
if (fieldValues[field]) {
row[field] = fieldValues[field][i % fieldValues[field].length];
}
});
sampleData.push(row);
}
return sampleData;
},
formatFileSize(size) {
if (!size) return '0 B';
const units = ['B', 'KB', 'MB', 'GB'];
let i = 0;
while (size >= 1024 && i < units.length - 1) {
size /= 1024;
i++;
}
return `${size.toFixed(2)} ${units[i]}`;
},
formatDateTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp);
return date.toLocaleString();
},
getFieldLabel(field) {
const labels = {
title: '标题',
content: '内容',
category: '分类',
tags: '标签',
timestamp: '时间戳'
};
return labels[field] || field;
}
}
};
</script>
<style scoped>
.data-preprocessing {
width: 100%;
}
.file-info {
margin-bottom: 20px;
}
.preprocessing-config {
margin-bottom: 20px;
}
.preprocessing-config h3,
.file-info h3,
.preprocessing-summary h3,
.preprocessing-detail h3 {
margin-bottom: 15px;
font-weight: 500;
color: #303133;
}
.operation-buttons {
margin-top: 20px;
display: flex;
justify-content: center;
}
.preprocessing-summary {
margin-bottom: 20px;
}
.preprocessing-detail {
margin-top: 20px;
}
</style>

@ -0,0 +1,326 @@
<template>
<div class="file-upload">
<process-step
title="文件上传"
:status="uploadStatus"
:loading="loading"
:progress="uploadProgress"
:operationButtonText="'上传文件'"
:processingText="'正在上传文件...'"
:errorText="'文件上传失败,请重试'"
@start="handleUpload"
@retry="handleRetry"
@prev="$emit('prev')"
@next="$emit('next')"
>
<template #pre-operation>
<div class="upload-area">
<el-upload
action="#"
:auto-upload="false"
:on-change="handleFileChange"
:on-remove="handleFileRemove"
:limit="1"
:file-list="fileList"
drag
ref="upload"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">拖拽文件到此处或<em>点击上传</em></div>
<div class="el-upload__tip" slot="tip">
支持上传CSV或Excel文件最大50MB
</div>
</el-upload>
<div class="file-info" v-if="file">
<div class="file-detail">
<span>文件名称{{ file.name }}</span>
<span>文件大小{{ formatFileSize(file.size) }}</span>
<span>上传时间{{ formatDate(file.lastModified) }}</span>
</div>
</div>
</div>
</template>
<template #result-summary>
<div class="upload-success">
<i class="el-icon-success"></i>
<h4>文件上传成功</h4>
<p>文件已准备好进行后续处理</p>
</div>
</template>
<template #result-detail>
<div class="file-preview">
<div class="file-metadata">
<el-descriptions :column="2" border>
<el-descriptions-item label="文件名称">{{ fileData.fileName }}</el-descriptions-item>
<el-descriptions-item label="文件大小">{{ fileData.fileSize }}</el-descriptions-item>
<el-descriptions-item label="总行数">{{ fileData.rowCount }}</el-descriptions-item>
<el-descriptions-item label="总列数">{{ fileData.columnCount }}</el-descriptions-item>
<el-descriptions-item label="上传时间">{{ fileData.uploadTime }}</el-descriptions-item>
<el-descriptions-item label="文件类型">{{ fileData.fileType }}</el-descriptions-item>
</el-descriptions>
</div>
<div class="data-preview" v-if="fileData.previewData">
<h4>数据预览</h4>
<el-table :data="fileData.previewData.rows" border style="width: 100%">
<el-table-column
v-for="(col, index) in fileData.previewData.columns"
:key="index"
:prop="col.prop"
:label="col.label"
width="180"
></el-table-column>
</el-table>
</div>
</div>
</template>
</process-step>
</div>
</template>
<script>
import ProcessStep from './ProcessStep'
import { mapState, mapActions } from 'vuex'
export default {
name: 'FileUploadComponent',
components: {
ProcessStep
},
data() {
return {
fileList: [],
file: null,
loading: false,
uploadProgress: 0,
uploadStatus: 'waiting',
uploadTimer: null
}
},
computed: {
...mapState({
fileData: state => state.process.fileData,
fileUploaded: state => state.process.fileUploaded
})
},
watch: {
fileUploaded(newVal) {
if (newVal) {
this.uploadStatus = 'completed'
}
}
},
methods: {
...mapActions('process', [
'uploadFile',
'setFileData'
]),
handleFileChange(file) {
this.file = file.raw
this.fileList = [file]
},
handleFileRemove() {
this.file = null
this.fileList = []
},
handleUpload() {
if (!this.file) {
this.$message.warning('请先选择要上传的文件')
return
}
this.loading = true
this.uploadStatus = 'running'
this.uploadProgress = 0
//
this.uploadTimer = setInterval(() => {
this.uploadProgress += 10
if (this.uploadProgress >= 100) {
clearInterval(this.uploadTimer)
this.simulateUploadComplete()
}
}, 300)
// API
const formData = new FormData()
formData.append('file', this.file)
this.uploadFile(formData)
.then(() => {
// simulateUploadComplete
})
.catch(error => {
clearInterval(this.uploadTimer)
this.uploadStatus = 'error'
this.loading = false
this.$message.error('文件上传失败:' + (error.message || '未知错误'))
})
},
simulateUploadComplete() {
//
setTimeout(() => {
//
const mockFileData = {
fileName: this.file.name,
fileSize: this.formatFileSize(this.file.size),
rowCount: Math.floor(Math.random() * 1000) + 500,
columnCount: Math.floor(Math.random() * 10) + 5,
uploadTime: this.formatDate(new Date()),
fileType: this.getFileType(this.file.name),
previewData: this.generatePreviewData()
}
// Vuex
this.setFileData(mockFileData)
//
this.loading = false
this.uploadStatus = 'completed'
console.log('文件上传完成,状态已设置为:', this.uploadStatus)
//
this.$message.success('文件上传成功,请点击下一步按钮继续')
//
this.$emit('file-uploaded', mockFileData)
console.log('触发file-uploaded事件文件数据:', mockFileData)
}, 500)
},
handleRetry() {
this.uploadStatus = 'waiting'
this.uploadProgress = 0
},
formatFileSize(size) {
if (size < 1024) {
return size + ' B'
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + ' KB'
} else {
return (size / (1024 * 1024)).toFixed(2) + ' MB'
}
},
formatDate(timestamp) {
const date = new Date(timestamp)
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
},
getFileType(filename) {
const ext = filename.split('.').pop().toLowerCase()
if (ext === 'csv') {
return 'CSV文件'
} else if (['xls', 'xlsx'].includes(ext)) {
return 'Excel文件'
} else {
return '未知类型'
}
},
generatePreviewData() {
//
const columns = []
const rows = []
//
const colCount = Math.floor(Math.random() * 5) + 3
for (let i = 0; i < colCount; i++) {
columns.push({
prop: 'col' + i,
label: '列 ' + (i + 1)
})
}
//
const rowCount = 5
for (let i = 0; i < rowCount; i++) {
const row = {}
for (let j = 0; j < colCount; j++) {
row['col' + j] = `数据 ${i + 1}-${j + 1}`
}
rows.push(row)
}
return { columns, rows }
}
},
beforeDestroy() {
if (this.uploadTimer) {
clearInterval(this.uploadTimer)
}
}
}
</script>
<style scoped>
.file-upload {
margin-bottom: 20px;
}
.upload-area {
padding: 20px 0;
}
.file-info {
margin-top: 20px;
padding: 15px;
background-color: #f5f7fa;
border-radius: 4px;
}
.file-detail {
display: flex;
flex-direction: column;
gap: 8px;
}
.upload-success {
text-align: center;
padding: 15px 0;
}
.upload-success i {
font-size: 40px;
color: #67c23a;
margin-bottom: 10px;
}
.upload-success h4 {
margin: 10px 0;
font-size: 18px;
color: #303133;
}
.upload-success p {
color: #606266;
}
.file-preview {
margin-top: 20px;
}
.file-metadata {
margin-bottom: 20px;
}
.data-preview h4 {
margin: 15px 0;
font-size: 16px;
color: #303133;
}
</style>

@ -0,0 +1,618 @@
<template>
<div class="format-merge">
<process-step
title="格式合并"
:status="mergeStatus"
:loading="loading"
operationButtonText="开始合并"
processingText="数据格式合并中..."
:processingPercentage="mergeProgress"
errorText="格式合并失败,请重试"
@start="handleStartMerge"
@retry="handleRetry"
>
<template #pre-operation>
<div class="merge-config">
<h4>合并配置</h4>
<el-alert
v-if="!preprocessed"
title="请先完成数据预处理步骤"
type="warning"
show-icon
:closable="false"
/>
<el-form v-else ref="configForm" :model="configForm" label-width="120px" size="small">
<el-form-item label="目标格式">
<el-radio-group v-model="configForm.targetFormat">
<el-radio label="json">JSON格式</el-radio>
<el-radio label="xml">XML格式</el-radio>
<el-radio label="csv">CSV格式</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="数据结构">
<el-radio-group v-model="configForm.dataStructure">
<el-radio label="flat">扁平结构</el-radio>
<el-radio label="nested">嵌套结构</el-radio>
</el-radio-group>
</el-form-item>
<template v-if="configForm.dataStructure === 'nested'">
<el-form-item label="嵌套字段">
<el-select
v-model="configForm.nestedFields"
multiple
placeholder="请选择要嵌套的字段"
>
<el-option
v-for="(column, index) in availableColumns"
:key="index"
:label="column.label"
:value="column.prop"
/>
</el-select>
</el-form-item>
<el-form-item label="嵌套层级名称">
<el-input v-model="configForm.nestedName" placeholder="请输入嵌套层级名称"></el-input>
</el-form-item>
</template>
<el-form-item label="字段映射">
<el-button size="small" type="primary" @click="showMappingDialog = true">配置字段映射</el-button>
</el-form-item>
</el-form>
<el-dialog
title="字段映射配置"
:visible.sync="showMappingDialog"
width="60%"
>
<el-table :data="fieldMappings" size="small">
<el-table-column label="原字段名" prop="originalField"></el-table-column>
<el-table-column label="目标字段名">
<template slot-scope="scope">
<el-input v-model="scope.row.targetField" size="small" placeholder="请输入目标字段名"></el-input>
</template>
</el-table-column>
<el-table-column label="是否包含">
<template slot-scope="scope">
<el-switch v-model="scope.row.include"></el-switch>
</template>
</el-table-column>
</el-table>
<span slot="footer" class="dialog-footer">
<el-button @click="showMappingDialog = false">取消</el-button>
<el-button type="primary" @click="handleSaveMapping"></el-button>
</span>
</el-dialog>
</div>
</template>
<template #result-summary>
<div class="merge-result-summary">
<i class="el-icon-check"></i>
<h4>数据格式合并完成</h4>
<div class="summary-stats">
<div class="stat-item">
<span class="stat-value">{{ resultData.targetFormat }}</span>
<span class="stat-label">目标格式</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ resultData.dataStructure }}</span>
<span class="stat-label">数据结构</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ resultData.recordCount }}</span>
<span class="stat-label">记录数量</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ resultData.fileSize }}</span>
<span class="stat-label">文件大小</span>
</div>
</div>
</div>
</template>
<template #result-detail>
<div class="merge-result-detail">
<el-tabs type="border-card">
<el-tab-pane label="数据预览">
<div class="preview-container">
<div class="preview-header">
<h5>{{ formatTitle }}</h5>
<el-button
size="mini"
type="primary"
icon="el-icon-download"
@click="handleDownload"
>
下载转换后数据
</el-button>
</div>
<div class="preview-content">
<pre v-if="configForm.targetFormat === 'json'">{{ jsonPreview }}</pre>
<pre v-else-if="configForm.targetFormat === 'xml'">{{ xmlPreview }}</pre>
<el-table
v-else
:data="resultData.previewData"
border
size="small"
style="width: 100%"
height="300"
>
<el-table-column
v-for="(column, index) in previewColumns"
:key="index"
:prop="column.prop"
:label="column.label"
/>
</el-table>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="字段映射详情">
<div class="mapping-details">
<el-table :data="fieldMappings" border size="small">
<el-table-column label="原字段名" prop="originalField"></el-table-column>
<el-table-column label="目标字段名" prop="targetField"></el-table-column>
<el-table-column label="是否包含" width="120">
<template slot-scope="scope">
<el-tag :type="scope.row.include ? 'success' : 'info'">
{{ scope.row.include ? '是' : '否' }}
</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<el-tab-pane label="转换日志">
<div class="conversion-logs">
<el-timeline>
<el-timeline-item
v-for="(log, index) in resultData.conversionLogs"
:key="index"
:type="log.type"
:color="log.color"
:timestamp="log.timestamp"
>
{{ log.content }}
</el-timeline-item>
</el-timeline>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
</process-step>
</div>
</template>
<script>
import ProcessStep from './ProcessStep'
import { mapState, mapActions } from 'vuex'
export default {
name: 'FormatMergeComponent',
components: {
ProcessStep
},
data() {
return {
loading: false,
mergeStatus: 'pending',
mergeProgress: 0,
mergeTimer: null,
showMappingDialog: false,
configForm: {
targetFormat: 'json',
dataStructure: 'flat',
nestedFields: [],
nestedName: 'data'
},
fieldMappings: [],
resultData: {
targetFormat: '',
dataStructure: '',
recordCount: 0,
fileSize: '',
previewData: [],
conversionLogs: []
}
}
},
computed: {
...mapState({
fileData: state => state.process.fileData,
fileUploaded: state => state.process.fileUploaded,
preprocessed: state => state.process.preprocessed,
preprocessData: state => state.process.preprocessData,
formatMerged: state => state.process.formatMerged,
mergeData: state => state.process.mergeData
}),
availableColumns() {
if (this.preprocessed && this.preprocessData && this.preprocessData.processedSample) {
return this.preprocessData.processedSample.columns || []
}
return []
},
previewColumns() {
//
if (this.configForm.targetFormat === 'csv') {
return this.fieldMappings
.filter(m => m.include)
.map(m => ({ prop: m.targetField, label: m.targetField }))
}
return []
},
formatTitle() {
const format = this.configForm.targetFormat.toUpperCase()
const structure = this.configForm.dataStructure === 'flat' ? '扁平结构' : '嵌套结构'
return `${format}格式(${structure}`
},
jsonPreview() {
if (!this.resultData.previewData || this.resultData.previewData.length === 0) {
return '{}'
}
try {
return JSON.stringify(this.resultData.previewData.slice(0, 5), null, 2)
} catch (e) {
return '{}'
}
},
xmlPreview() {
if (!this.resultData.previewData || this.resultData.previewData.length === 0) {
return '<root></root>'
}
try {
// XML
let xml = '<root>\n'
this.resultData.previewData.slice(0, 3).forEach((item, index) => {
xml += ` <item id="${index + 1}">\n`
Object.entries(item).forEach(([key, value]) => {
xml += ` <${key}>${value}</${key}>\n`
})
xml += ' </item>\n'
})
xml += '</root>'
return xml
} catch (e) {
return '<root></root>'
}
}
},
watch: {
preprocessData: {
handler(newVal) {
if (newVal && newVal.processedSample && newVal.processedSample.columns) {
this.initFieldMappings()
}
},
immediate: true
},
formatMerged(newVal) {
if (newVal) {
this.mergeStatus = 'completed'
this.resultData = this.mergeData
}
}
},
mounted() {
if (this.preprocessed && !this.formatMerged) {
this.initFieldMappings()
} else if (this.formatMerged) {
this.mergeStatus = 'completed'
this.resultData = this.mergeData
}
},
methods: {
...mapActions('process', [
'mergeFormatAction',
'setMergeData'
]),
initFieldMappings() {
if (this.preprocessData && this.preprocessData.processedSample && this.preprocessData.processedSample.columns) {
this.fieldMappings = this.preprocessData.processedSample.columns.map(column => ({
originalField: column.label,
targetField: column.label,
include: true
}))
}
},
handleSaveMapping() {
this.showMappingDialog = false
},
handleStartMerge() {
if (!this.preprocessed) {
this.$message.warning('请先完成数据预处理步骤')
return
}
this.loading = true
this.mergeStatus = 'processing'
this.mergeProgress = 0
//
this.mergeTimer = setInterval(() => {
this.mergeProgress += 7
if (this.mergeProgress >= 100) {
clearInterval(this.mergeTimer)
this.simulateMergeComplete()
}
}, 200)
// API
this.mergeFormatAction({
config: this.configForm,
fieldMappings: this.fieldMappings
}).catch(error => {
clearInterval(this.mergeTimer)
this.mergeStatus = 'error'
this.loading = false
this.$message.error('格式合并失败:' + (error.message || '未知错误'))
})
},
simulateMergeComplete() {
//
setTimeout(() => {
this.loading = false
//
const mockResult = this.generateMockResult()
// Vuex
this.setMergeData(mockResult)
this.resultData = mockResult
this.mergeStatus = 'completed'
}, 500)
},
handleRetry() {
this.mergeStatus = 'pending'
this.mergeProgress = 0
},
handleDownload() {
this.$message.success('转换后的数据已开始下载')
//
const format = this.configForm.targetFormat
const filename = `converted_data.${format}`
let content = ''
if (format === 'json') {
content = JSON.stringify(this.resultData.previewData, null, 2)
} else if (format === 'xml') {
content = this.xmlPreview
} else {
// CSV
const headers = this.previewColumns.map(col => col.label).join(',')
const rows = this.resultData.previewData.map(row => {
return this.previewColumns.map(col => row[col.prop]).join(',')
}).join('\n')
content = headers + '\n' + rows
}
const blob = new Blob([content], { type: 'text/plain' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
link.click()
URL.revokeObjectURL(link.href)
},
generateMockResult() {
//
const recordCount = this.preprocessData ? this.preprocessData.rowCount : 1000
const fileSize = (recordCount * 0.2).toFixed(1) + ' KB'
//
let previewData = []
const includedMappings = this.fieldMappings.filter(m => m.include)
// 5
for (let i = 0; i < 5; i++) {
const item = {}
includedMappings.forEach(mapping => {
item[mapping.targetField] = `示例数据 ${i + 1}-${mapping.originalField}`
})
previewData.push(item)
}
//
if (this.configForm.dataStructure === 'nested' && this.configForm.nestedFields.length > 0) {
previewData = previewData.map(item => {
const nestedItem = {}
const nestedData = {}
Object.keys(item).forEach(key => {
if (this.configForm.nestedFields.includes(key)) {
nestedData[key] = item[key]
} else {
nestedItem[key] = item[key]
}
})
nestedItem[this.configForm.nestedName] = nestedData
return nestedItem
})
}
//
const conversionLogs = [
{
content: '开始格式转换处理',
timestamp: '10:30:15',
type: 'primary',
color: '#409EFF'
},
{
content: `从CSV格式转换为${this.configForm.targetFormat.toUpperCase()}格式`,
timestamp: '10:30:20',
type: 'info',
color: '#909399'
},
{
content: `应用了${includedMappings.length}个字段映射`,
timestamp: '10:30:25',
type: 'success',
color: '#67C23A'
},
{
content: this.configForm.dataStructure === 'nested'
? `创建了嵌套结构,嵌套层级名为"${this.configForm.nestedName}"`
: '应用了扁平数据结构',
timestamp: '10:30:35',
type: 'info',
color: '#909399'
},
{
content: `成功转换了${recordCount}条记录`,
timestamp: '10:30:45',
type: 'success',
color: '#67C23A'
}
]
return {
targetFormat: this.configForm.targetFormat.toUpperCase(),
dataStructure: this.configForm.dataStructure === 'flat' ? '扁平结构' : '嵌套结构',
recordCount: recordCount,
fileSize: fileSize,
previewData: previewData,
conversionLogs: conversionLogs
}
}
},
beforeDestroy() {
if (this.mergeTimer) {
clearInterval(this.mergeTimer)
}
}
}
</script>
<style scoped>
.format-merge {
margin-bottom: 20px;
}
.merge-config {
margin-bottom: 20px;
}
.merge-config h4 {
margin-bottom: 15px;
font-size: 16px;
color: #303133;
}
.merge-result-summary {
text-align: center;
padding: 15px 0;
}
.merge-result-summary i {
font-size: 40px;
color: #67c23a;
margin-bottom: 10px;
}
.merge-result-summary h4 {
margin: 10px 0;
font-size: 18px;
color: #303133;
}
.summary-stats {
display: flex;
justify-content: center;
gap: 30px;
margin-top: 20px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #409EFF;
}
.stat-label {
font-size: 14px;
color: #606266;
margin-top: 5px;
}
.preview-container {
margin-bottom: 20px;
}
.preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.preview-header h5 {
margin: 0;
font-size: 16px;
color: #303133;
}
.preview-content {
border: 1px solid #e4e7ed;
border-radius: 4px;
background-color: #f5f7fa;
min-height: 300px;
max-height: 500px;
overflow: auto;
}
.preview-content pre {
margin: 0;
padding: 10px;
font-family: Consolas, Monaco, 'Andale Mono', monospace;
font-size: 14px;
line-height: 1.5;
color: #333;
white-space: pre-wrap;
word-wrap: break-word;
}
.mapping-details {
margin-top: 10px;
}
.conversion-logs {
padding: 10px;
}
</style>

@ -0,0 +1,289 @@
<template>
<div class="merge-format">
<process-step
title="格式合并"
:status="stepStatus"
:loading="loading"
operationButtonText="开始合并格式"
processingText="正在合并数据格式..."
:processingPercentage="processingPercentage"
errorText="格式合并出错,请重试"
@start="startProcess"
@retry="startProcess"
>
<template #pre-operation>
<div class="info-box">
<p><i class="el-icon-info"></i> 合并格式将统一数据表示形式便于后续处理和分析</p>
<el-alert
v-if="!preprocessResults"
title="请先完成数据预处理步骤"
type="warning"
:closable="false"
show-icon
>
</el-alert>
<div v-else class="options-section">
<div class="section-title">合并选项</div>
<el-form :model="mergeOptions" label-width="120px" size="small">
<el-form-item label="目标格式">
<el-radio-group v-model="mergeOptions.format">
<el-radio label="standardized">标准化格式</el-radio>
<el-radio label="simplified">简化格式</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="合并相似内容">
<el-switch v-model="mergeOptions.mergeSimilar"></el-switch>
</el-form-item>
<el-form-item label="相似度阈值">
<el-slider
v-model="mergeOptions.similarityThreshold"
:disabled="!mergeOptions.mergeSimilar"
:min="0"
:max="100"
:format-tooltip="val => val + '%'"
></el-slider>
</el-form-item>
</el-form>
</div>
</div>
</template>
<template #result-summary>
<el-alert
title="格式合并完成"
type="success"
:closable="false"
description="数据格式已成功合并"
show-icon
>
</el-alert>
<div class="result-stats">
<el-statistic title="合并后条目" :value="resultStats.merged"></el-statistic>
<el-statistic title="目标格式" :value="resultStats.format === 'standardized' ? '标准化' : '简化'"></el-statistic>
</div>
</template>
<template #result-detail>
<div class="result-preview">
<h4>格式合并结果预览</h4>
<el-table
:data="resultPreview"
border
style="width: 100%"
max-height="250"
>
<el-table-column
v-for="(col, index) in tableColumns"
:key="index"
:prop="col.prop"
:label="col.label"
:width="col.width || ''"
>
</el-table-column>
</el-table>
</div>
</template>
</process-step>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
import { mergeFormat } from '../../api/process'
import ProcessStep from './ProcessStep.vue'
export default {
name: 'MergeFormatComponent',
components: {
ProcessStep
},
data() {
return {
loading: false,
stepStatus: 'pending',
processingPercentage: 0,
mergeOptions: {
format: 'standardized',
mergeSimilar: false,
similarityThreshold: 75
},
resultStats: {
merged: 0,
format: 'standardized'
},
resultPreview: [],
tableColumns: [],
processTimer: null
}
},
computed: {
...mapState('process', ['preprocessResults', 'mergeResults', 'stepStatus']),
currentStepStatus() {
return this.stepStatus.merge
}
},
watch: {
currentStepStatus(newStatus) {
if (newStatus === 'completed' && this.stepStatus === 'pending') {
this.stepStatus = 'completed'
this.setResultData(this.mergeResults)
}
},
mergeResults(newResults) {
if (newResults && this.stepStatus === 'completed') {
this.setResultData(newResults)
}
}
},
mounted() {
if (this.currentStepStatus === 'completed' && this.mergeResults) {
this.stepStatus = 'completed'
this.setResultData(this.mergeResults)
}
},
methods: {
...mapActions('process', ['processMerge']),
startProcess() {
if (!this.preprocessResults) {
this.$message.error('请先完成数据预处理步骤')
return
}
this.loading = true
this.stepStatus = 'processing'
//
this.processingPercentage = 0
this.processTimer = setInterval(() => {
this.processingPercentage += 8
if (this.processingPercentage >= 100) {
clearInterval(this.processTimer)
}
}, 300)
//
const requestData = {
...this.preprocessResults,
options: this.mergeOptions
}
// API
mergeFormat(requestData).then(response => {
if (response.success) {
clearInterval(this.processTimer)
this.processingPercentage = 100
// Vuex
this.processMerge(response.data)
//
setTimeout(() => {
this.stepStatus = 'completed'
this.setResultData(response.data)
this.loading = false
}, 500)
} else {
this.handleError('格式合并失败')
}
}).catch(error => {
this.handleError(`格式合并错误: ${error.message}`)
})
},
setResultData(data) {
if (!data) return
this.resultStats = {
merged: data.merged || 0,
format: data.format || 'standardized'
}
this.resultPreview = data.preview || []
if (this.resultPreview.length > 0) {
const firstRow = this.resultPreview[0]
this.tableColumns = Object.keys(firstRow).map(key => {
return {
prop: key,
label: key.charAt(0).toUpperCase() + key.slice(1),
width: key === 'id' ? '70' : ''
}
})
}
},
handleError(message) {
clearInterval(this.processTimer)
this.stepStatus = 'error'
this.loading = false
this.$message.error(message)
}
},
beforeDestroy() {
if (this.processTimer) {
clearInterval(this.processTimer)
}
}
}
</script>
<style scoped>
.merge-format {
width: 100%;
}
.info-box {
background-color: #f5f7fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 15px;
}
.info-box p {
color: #606266;
margin-bottom: 15px;
}
.options-section {
margin-top: 20px;
background-color: #fff;
padding: 15px;
border-radius: 4px;
}
.section-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
color: #303133;
}
.result-stats {
display: flex;
justify-content: space-around;
margin: 20px 0;
}
.result-preview {
margin-top: 15px;
}
.result-preview h4 {
margin-bottom: 10px;
color: #303133;
}
</style>

@ -0,0 +1,813 @@
<template>
<div class="model-analysis">
<process-step
title="大模型分析"
:status="analysisStatus"
:progress="analysisProgress"
:loading="isLoading"
@prev="$emit('prev')"
@next="$emit('next')"
@start="startAnalysis"
>
<template #pre-operation>
<el-form :model="configForm" label-width="140px" size="small">
<el-form-item label="分析模型选择">
<el-select v-model="configForm.model" placeholder="请选择分析模型">
<el-option label="GPT-4" value="gpt4"></el-option>
<el-option label="GPT-3.5" value="gpt3.5"></el-option>
<el-option label="BERT" value="bert"></el-option>
<el-option label="自定义模型" value="custom"></el-option>
</el-select>
</el-form-item>
<el-form-item label="分析目标">
<el-select
v-model="configForm.analysisTargets"
multiple
placeholder="请选择分析目标"
>
<el-option label="文本分类" value="classification"></el-option>
<el-option label="情感分析" value="sentiment"></el-option>
<el-option label="关键信息提取" value="extraction"></el-option>
<el-option label="摘要生成" value="summarization"></el-option>
<el-option label="实体识别" value="ner"></el-option>
</el-select>
</el-form-item>
<el-form-item label="分析模式">
<el-radio-group v-model="configForm.analysisMode">
<el-radio label="auto">自动分析</el-radio>
<el-radio label="manual">人工确认</el-radio>
</el-radio-group>
</el-form-item>
<el-collapse v-model="activeNames">
<el-collapse-item title="高级设置" name="advanced">
<el-form-item label="置信度阈值">
<el-slider
v-model="configForm.confidenceThreshold"
:min="0"
:max="100"
:format-tooltip="formatConfidence"
></el-slider>
</el-form-item>
<el-form-item label="分析模板">
<el-select v-model="configForm.template" placeholder="请选择分析模板">
<el-option label="标准模板" value="standard"></el-option>
<el-option label="详细模板" value="detailed"></el-option>
<el-option label="简洁模板" value="concise"></el-option>
<el-option label="自定义模板" value="custom"></el-option>
</el-select>
</el-form-item>
<el-form-item label="自定义提示词" v-if="configForm.template === 'custom'">
<el-input
type="textarea"
v-model="configForm.customPrompt"
:rows="3"
placeholder="输入自定义提示词以指导模型分析..."
></el-input>
</el-form-item>
</el-collapse-item>
</el-collapse>
</el-form>
</template>
<template #operation v-if="analysisStatus === 'running'">
<div class="analysis-progress">
<div class="progress-info">
<span>已完成分析: {{ analyzedItems }} / {{ totalItems }}</span>
<el-progress :percentage="analysisProgress" :stroke-width="15"></el-progress>
</div>
<div class="current-task">
<span>当前任务: {{ currentTask }}</span>
</div>
</div>
</template>
<template #result-summary v-if="analysisStatus === 'completed'">
<div class="analysis-summary">
<el-alert
title="分析完成"
type="success"
:closable="false"
show-icon
>
<div class="summary-content">
<div class="summary-item">
<span>分析文档总数:</span>
<span class="value">{{ analysisSummary.totalDocuments }}</span>
</div>
<div class="summary-item">
<span>成功分析:</span>
<span class="value">{{ analysisSummary.successful }}</span>
</div>
<div class="summary-item">
<span>分析失败:</span>
<span class="value">{{ analysisSummary.failed }}</span>
</div>
<div class="summary-item">
<span>平均置信度:</span>
<span class="value">{{ analysisSummary.avgConfidence }}%</span>
</div>
<div class="summary-item">
<span>处理时间:</span>
<span class="value">{{ analysisSummary.processingTime }}</span>
</div>
</div>
</el-alert>
<div class="analysis-actions">
<el-button type="primary" @click="downloadResults"></el-button>
<el-button @click="resetAnalysis"></el-button>
</div>
</div>
</template>
<template #result-detail v-if="analysisStatus === 'completed' || analysisStatus === 'reviewing'">
<div class="analysis-results">
<el-tabs v-model="activeTab">
<el-tab-pane label="分析结果概览" name="overview">
<div class="chart-container">
<div class="chart-item">
<h4>文档分类分布</h4>
<div class="chart-placeholder">
[分类饼图]
</div>
</div>
<div class="chart-item">
<h4>情感分析分布</h4>
<div class="chart-placeholder">
[情感分析条形图]
</div>
</div>
<div class="chart-item">
<h4>关键词云</h4>
<div class="chart-placeholder">
[关键词词云]
</div>
</div>
<div class="chart-item">
<h4>置信度分布</h4>
<div class="chart-placeholder">
[置信度直方图]
</div>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="详细分析结果" name="details">
<el-table :data="analysisResults" style="width: 100%" height="400">
<el-table-column prop="documentName" label="文档名称" width="180"></el-table-column>
<el-table-column prop="category" label="分类" width="120"></el-table-column>
<el-table-column prop="sentiment" label="情感" width="100">
<template slot-scope="scope">
<el-tag
:type="getSentimentType(scope.row.sentiment)"
size="mini"
>
{{ scope.row.sentiment }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="confidence" label="置信度" width="100">
<template slot-scope="scope">
<el-progress
:percentage="scope.row.confidence"
:color="getConfidenceColor(scope.row.confidence)"
></el-progress>
</template>
</el-table-column>
<el-table-column prop="keyEntities" label="关键实体" width="200">
<template slot-scope="scope">
<el-tag
v-for="(entity, index) in scope.row.keyEntities"
:key="index"
size="mini"
class="entity-tag"
>
{{ entity }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="summary" label="摘要" min-width="250"></el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template slot-scope="scope">
<el-button type="text" @click="viewDetail(scope.row)"></el-button>
<el-button type="text" @click="editResult(scope.row)"></el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="分析日志" name="logs">
<div class="logs-container">
<div v-for="(log, index) in analysisLogs" :key="index" class="log-item">
<span class="log-time">{{ log.time }}</span>
<span :class="['log-level', 'log-level-' + log.level]">{{ log.level }}</span>
<span class="log-message">{{ log.message }}</span>
</div>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
</process-step>
<el-dialog
title="分析详情"
:visible.sync="detailDialogVisible"
width="70%"
>
<div v-if="selectedResult" class="result-detail-dialog">
<div class="detail-header">
<h3>{{ selectedResult.documentName }}</h3>
<div class="detail-meta">
<span class="meta-item">
<label>分类:</label> {{ selectedResult.category }}
</span>
<span class="meta-item">
<label>情感:</label>
<el-tag :type="getSentimentType(selectedResult.sentiment)" size="mini">
{{ selectedResult.sentiment }}
</el-tag>
</span>
<span class="meta-item">
<label>置信度:</label> {{ selectedResult.confidence }}%
</span>
</div>
</div>
<el-divider></el-divider>
<div class="detail-section">
<h4>文档摘要</h4>
<p>{{ selectedResult.summary }}</p>
</div>
<div class="detail-section">
<h4>关键实体</h4>
<div class="entities-container">
<el-tag
v-for="(entity, index) in selectedResult.keyEntities"
:key="index"
class="entity-tag"
>
{{ entity }}
</el-tag>
</div>
</div>
<div class="detail-section">
<h4>关键信息提取</h4>
<el-table :data="selectedResult.keyInformation" border style="width: 100%">
<el-table-column prop="type" label="信息类型" width="120"></el-table-column>
<el-table-column prop="content" label="内容"></el-table-column>
<el-table-column prop="confidence" label="置信度" width="100">
<template slot-scope="scope">
<el-progress
:percentage="scope.row.confidence"
:color="getConfidenceColor(scope.row.confidence)"
></el-progress>
</template>
</el-table-column>
</el-table>
</div>
<div class="detail-section">
<h4>原文内容与注解</h4>
<div class="annotated-content">
<!-- 这里可以添加带有高亮和注解的原文内容 -->
<p>原文内容将在这里展示带有高亮和注解...</p>
</div>
</div>
</div>
</el-dialog>
<el-dialog
title="编辑分析结果"
:visible.sync="editDialogVisible"
width="60%"
>
<div v-if="editingResult" class="edit-result-form">
<el-form :model="editingResult" label-width="100px">
<el-form-item label="文档分类">
<el-select v-model="editingResult.category" placeholder="请选择文档分类">
<el-option label="商务文档" value="business"></el-option>
<el-option label="技术文档" value="technical"></el-option>
<el-option label="法律文档" value="legal"></el-option>
<el-option label="学术文档" value="academic"></el-option>
<el-option label="其他" value="other"></el-option>
</el-select>
</el-form-item>
<el-form-item label="情感分析">
<el-select v-model="editingResult.sentiment" placeholder="请选择情感分析结果">
<el-option label="积极" value="positive"></el-option>
<el-option label="中性" value="neutral"></el-option>
<el-option label="消极" value="negative"></el-option>
</el-select>
</el-form-item>
<el-form-item label="摘要">
<el-input
type="textarea"
v-model="editingResult.summary"
:rows="4"
></el-input>
</el-form-item>
<el-form-item label="关键实体">
<el-tag
:key="tag"
v-for="tag in editingResult.keyEntities"
closable
:disable-transitions="false"
@close="handleEntityRemove(tag)">
{{tag}}
</el-tag>
<el-input
class="input-new-tag"
v-if="inputVisible"
v-model="inputValue"
ref="saveTagInput"
size="small"
@keyup.enter.native="handleEntityInputConfirm"
@blur="handleEntityInputConfirm"
>
</el-input>
<el-button v-else class="button-new-tag" size="small" @click="showInput">+ </el-button>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveEditedResult"></el-button>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
name: 'ModelAnalysis',
props: {
status: {
type: String,
default: 'waiting'
},
loading: {
type: Boolean,
default: false
},
progress: {
type: Number,
default: 0
},
correctedData: {
type: Object,
default: () => null
}
},
data() {
return {
analysisStatus: 'waiting', // waiting, running, completed, reviewing
analysisProgress: 0,
activeNames: [], //
activeTab: 'overview',
isLoading: false, // loadingprops
//
configForm: {
model: 'gpt3.5',
analysisTargets: ['classification', 'sentiment', 'extraction'],
analysisMode: 'auto',
confidenceThreshold: 70,
template: 'standard',
customPrompt: ''
},
//
analyzedItems: 0,
totalItems: 0,
currentTask: '准备分析...',
//
analysisSummary: {
totalDocuments: 0,
successful: 0,
failed: 0,
avgConfidence: 0,
processingTime: 0
},
//
analysisResults: [],
analysisLogs: [],
//
detailDialogVisible: false,
selectedResult: null,
//
editDialogVisible: false,
editingResult: null,
inputVisible: false,
inputValue: ''
}
},
watch: {
//
status(newVal) {
this.analysisStatus = newVal;
},
progress(newVal) {
this.analysisProgress = newVal;
},
loading(newVal) {
this.isLoading = newVal;
}
},
methods: {
//
startAnalysis() {
this.isLoading = true; // 使
this.analysisStatus = 'running';
this.analysisProgress = 0;
this.analyzedItems = 0;
// 使correctedData
if (this.correctedData) {
console.log('开始分析已纠错的数据:', this.correctedData);
this.totalItems = this.correctedData.errorsCorrected ? 100 + this.correctedData.errorsCorrected : 100;
} else {
this.totalItems = 100; // 100
}
//
this.$emit('start-analysis', this.configForm);
//
const interval = setInterval(() => {
this.analysisProgress += Math.random() * 10;
this.analyzedItems = Math.floor(this.analysisProgress * this.totalItems / 100);
//
const tasks = [
'加载文档...',
'预处理文本...',
'执行文本分类...',
'进行情感分析...',
'提取关键信息...',
'生成文档摘要...',
'识别命名实体...',
'计算分析结果...'
];
this.currentTask = tasks[Math.floor(Math.random() * tasks.length)];
//
if (Math.random() > 0.7) {
this.analysisLogs.unshift({
time: new Date().toLocaleTimeString(),
level: ['info', 'warning', 'error'][Math.floor(Math.random() * 3)],
message: `处理文档 ${this.analyzedItems}: ${this.currentTask}`
});
}
if (this.analysisProgress >= 100) {
clearInterval(interval);
this.analysisProgress = 100;
this.analyzedItems = this.totalItems;
this.completeAnalysis();
}
}, 500);
},
//
completeAnalysis() {
setTimeout(() => {
this.isLoading = false; // 使
this.analysisStatus = 'completed';
//
this.analysisSummary = {
totalDocuments: this.totalItems,
successful: Math.floor(this.totalItems * 0.95),
failed: Math.floor(this.totalItems * 0.05),
avgConfidence: 85,
processingTime: 45.3
};
//
this.generateMockResults();
//
this.analysisLogs.unshift({
time: new Date().toLocaleTimeString(),
level: 'info',
message: '分析任务完成!'
});
//
this.$emit('finish');
}, 1000);
},
//
generateMockResults() {
const categories = ['商务文档', '技术文档', '法律文档', '学术文档', '其他'];
const sentiments = ['积极', '中性', '消极'];
this.analysisResults = [];
for (let i = 1; i <= 20; i++) {
const confidence = 60 + Math.floor(Math.random() * 40);
const category = categories[Math.floor(Math.random() * categories.length)];
const sentiment = sentiments[Math.floor(Math.random() * sentiments.length)];
const entities = [
'公司', '产品', '技术', '人员', '地点', '时间', '金额', '法规',
'合同', '项目', '部门', '客户', '供应商', '竞争对手'
];
// 3-5
const keyEntities = [];
const entityCount = 3 + Math.floor(Math.random() * 3);
for (let j = 0; j < entityCount; j++) {
const entity = entities[Math.floor(Math.random() * entities.length)];
if (!keyEntities.includes(entity)) {
keyEntities.push(entity);
}
}
//
const keyInformation = [
{ type: '主题', content: `这是文档${i}的主题内容`, confidence: 70 + Math.floor(Math.random() * 30) },
{ type: '时间', content: '2023年10月15日', confidence: 80 + Math.floor(Math.random() * 20) },
{ type: '地点', content: '北京市海淀区', confidence: 75 + Math.floor(Math.random() * 25) },
];
this.analysisResults.push({
id: i,
documentName: `文档${i}.docx`,
category,
sentiment,
confidence,
summary: `这是文档${i}的摘要内容,通过大模型分析生成。主要内容涉及${keyEntities.join('、')}等方面的信息...`,
keyEntities,
keyInformation
});
}
},
//
formatConfidence(val) {
return `${val}%`;
},
//
getConfidenceColor(confidence) {
if (confidence < 60) return '#F56C6C';
if (confidence < 80) return '#E6A23C';
return '#67C23A';
},
//
getSentimentType(sentiment) {
if (sentiment === '积极') return 'success';
if (sentiment === '消极') return 'danger';
return 'info';
},
//
viewDetail(row) {
this.selectedResult = row;
this.detailDialogVisible = true;
},
//
editResult(row) {
this.editingResult = JSON.parse(JSON.stringify(row)); //
this.editDialogVisible = true;
},
//
saveEditedResult() {
//
const index = this.analysisResults.findIndex(item => item.id === this.editingResult.id);
if (index !== -1) {
this.analysisResults.splice(index, 1, this.editingResult);
}
this.editDialogVisible = false;
},
//
handleEntityRemove(tag) {
this.editingResult.keyEntities.splice(this.editingResult.keyEntities.indexOf(tag), 1);
},
//
showInput() {
this.inputVisible = true;
this.$nextTick(() => {
this.$refs.saveTagInput.$el.querySelector('input').focus();
});
},
//
handleEntityInputConfirm() {
const inputValue = this.inputValue;
if (inputValue && !this.editingResult.keyEntities.includes(inputValue)) {
this.editingResult.keyEntities.push(inputValue);
}
this.inputVisible = false;
this.inputValue = '';
},
//
downloadResults() {
//
this.$message.success('分析结果下载已开始');
},
//
resetAnalysis() {
this.analysisStatus = 'waiting';
this.analysisProgress = 0;
this.analysisResults = [];
this.analysisLogs = [];
}
}
}
</script>
<style scoped>
.model-analysis {
width: 100%;
}
.analysis-progress {
padding: 20px;
background-color: #f5f7fa;
border-radius: 4px;
}
.progress-info {
margin-bottom: 10px;
}
.current-task {
margin-top: 10px;
font-style: italic;
color: #606266;
}
.analysis-summary {
margin-bottom: 20px;
}
.summary-content {
display: flex;
flex-wrap: wrap;
margin-top: 10px;
}
.summary-item {
width: 33%;
margin-bottom: 8px;
}
.summary-item .value {
font-weight: bold;
margin-left: 5px;
}
.analysis-actions {
margin-top: 15px;
text-align: right;
}
.chart-container {
display: flex;
flex-wrap: wrap;
margin-top: 15px;
}
.chart-item {
width: 48%;
margin: 1%;
height: 200px;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 10px;
}
.chart-placeholder {
height: 150px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f7fa;
color: #909399;
font-size: 14px;
}
.entity-tag {
margin-right: 5px;
margin-bottom: 5px;
}
.logs-container {
height: 400px;
overflow-y: auto;
padding: 10px;
background-color: #f5f7fa;
border-radius: 4px;
}
.log-item {
padding: 5px 0;
border-bottom: 1px dashed #e0e0e0;
}
.log-time {
color: #909399;
margin-right: 10px;
}
.log-level {
display: inline-block;
width: 60px;
text-align: center;
margin-right: 10px;
border-radius: 3px;
padding: 2px 5px;
font-size: 12px;
}
.log-level-info {
background-color: #e1f3d8;
color: #67c23a;
}
.log-level-warning {
background-color: #faecd8;
color: #e6a23c;
}
.log-level-error {
background-color: #fde2e2;
color: #f56c6c;
}
.detail-header {
margin-bottom: 20px;
}
.detail-meta {
display: flex;
flex-wrap: wrap;
margin-top: 10px;
}
.meta-item {
margin-right: 20px;
margin-bottom: 10px;
}
.meta-item label {
font-weight: bold;
margin-right: 5px;
}
.detail-section {
margin-bottom: 20px;
}
.entities-container {
display: flex;
flex-wrap: wrap;
margin-top: 10px;
}
.annotated-content {
padding: 15px;
background-color: #f5f7fa;
border-radius: 4px;
margin-top: 10px;
}
.input-new-tag {
width: 120px;
margin-left: 10px;
vertical-align: top;
}
.button-new-tag {
margin-left: 10px;
height: 32px;
line-height: 30px;
padding-top: 0;
padding-bottom: 0;
}
</style>

@ -0,0 +1,243 @@
<template>
<div class="pre-process">
<process-step
title="数据预处理"
:status="stepStatus"
:loading="loading"
operationButtonText="开始预处理"
processingText="正在对数据进行预处理..."
:processingPercentage="processingPercentage"
errorText="预处理出错,请重试"
@start="startProcess"
@retry="startProcess"
>
<template #pre-operation>
<div class="info-box">
<p><i class="el-icon-info"></i> 预处理将规范化数据格式清除无效字段并准备用于后续处理的数据结构</p>
<div v-if="fileData" class="file-info">
<el-descriptions title="文件信息" :column="2" border>
<el-descriptions-item label="文件名">{{ fileData.fileName }}</el-descriptions-item>
<el-descriptions-item label="行数">{{ fileData.rows }}</el-descriptions-item>
<el-descriptions-item label="列数">{{ fileData.columns }}</el-descriptions-item>
</el-descriptions>
</div>
</div>
</template>
<template #result-summary>
<el-alert
title="预处理完成"
type="success"
:closable="false"
description="数据已成功预处理"
show-icon
>
</el-alert>
<div class="result-stats">
<el-statistic title="处理行数" :value="resultStats.processed"></el-statistic>
<el-statistic title="无效行数" :value="resultStats.invalidRows"></el-statistic>
<el-statistic title="规范化字段" :value="resultStats.normalizedFields ? resultStats.normalizedFields.length : 0"></el-statistic>
</div>
</template>
<template #result-detail>
<div class="result-preview">
<h4>预处理结果预览</h4>
<el-table
:data="resultPreview"
border
style="width: 100%"
max-height="250"
>
<el-table-column
v-for="(col, index) in tableColumns"
:key="index"
:prop="col.prop"
:label="col.label"
:width="col.width || ''"
>
</el-table-column>
</el-table>
</div>
</template>
</process-step>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
import { preprocessData } from '../../api/process'
import ProcessStep from './ProcessStep.vue'
export default {
name: 'PreProcessComponent',
components: {
ProcessStep
},
data() {
return {
loading: false,
stepStatus: 'pending',
processingPercentage: 0,
resultStats: {
processed: 0,
invalidRows: 0,
normalizedFields: []
},
resultPreview: [],
tableColumns: [],
processTimer: null
}
},
computed: {
...mapState('process', ['fileData', 'preprocessResults', 'stepStatus']),
currentStepStatus() {
return this.stepStatus.preprocess
}
},
watch: {
currentStepStatus(newStatus) {
if (newStatus === 'completed' && this.stepStatus === 'pending') {
this.stepStatus = 'completed'
this.setResultData(this.preprocessResults)
}
},
preprocessResults(newResults) {
if (newResults && this.stepStatus === 'completed') {
this.setResultData(newResults)
}
}
},
mounted() {
if (this.currentStepStatus === 'completed' && this.preprocessResults) {
this.stepStatus = 'completed'
this.setResultData(this.preprocessResults)
}
},
methods: {
...mapActions('process', ['processPreprocess']),
startProcess() {
if (!this.fileData) {
this.$message.error('没有可处理的数据,请先上传文件')
return
}
this.loading = true
this.stepStatus = 'processing'
//
this.processingPercentage = 0
this.processTimer = setInterval(() => {
this.processingPercentage += 10
if (this.processingPercentage >= 100) {
clearInterval(this.processTimer)
}
}, 300)
// API
preprocessData(this.fileData).then(response => {
if (response.success) {
clearInterval(this.processTimer)
this.processingPercentage = 100
// Vuex
this.processPreprocess(response.data)
//
setTimeout(() => {
this.stepStatus = 'completed'
this.setResultData(response.data)
this.loading = false
}, 500)
} else {
this.handleError('预处理失败')
}
}).catch(error => {
this.handleError(`预处理错误: ${error.message}`)
})
},
setResultData(data) {
if (!data) return
this.resultStats = {
processed: data.processed || 0,
invalidRows: data.invalidRows || 0,
normalizedFields: data.normalizedFields || []
}
this.resultPreview = data.preview || []
if (this.resultPreview.length > 0) {
const firstRow = this.resultPreview[0]
this.tableColumns = Object.keys(firstRow).map(key => {
return {
prop: key,
label: key.charAt(0).toUpperCase() + key.slice(1),
width: key === 'id' ? '70' : ''
}
})
}
},
handleError(message) {
clearInterval(this.processTimer)
this.stepStatus = 'error'
this.loading = false
this.$message.error(message)
}
},
beforeDestroy() {
if (this.processTimer) {
clearInterval(this.processTimer)
}
}
}
</script>
<style scoped>
.pre-process {
width: 100%;
}
.info-box {
background-color: #f5f7fa;
padding: 15px;
border-radius: 4px;
margin-bottom: 15px;
}
.info-box p {
color: #606266;
margin-bottom: 15px;
}
.file-info {
margin-top: 15px;
}
.result-stats {
display: flex;
justify-content: space-around;
margin: 20px 0;
}
.result-preview {
margin-top: 15px;
}
.result-preview h4 {
margin-bottom: 10px;
color: #303133;
}
</style>

@ -0,0 +1,284 @@
<template>
<div class="process-page">
<div class="process-header">
<h2>数据处理流程</h2>
<el-steps :active="currentStep" finish-status="success" align-center>
<el-step title="文件上传" description="上传需要处理的文件"></el-step>
<el-step title="数据预处理" description="清洗与标准化数据"></el-step>
<el-step title="格式合并" description="统一数据格式"></el-step>
<el-step title="单词纠错" description="修正文本错误"></el-step>
<el-step title="大模型分析" description="AI分析与处理"></el-step>
</el-steps>
</div>
<div class="process-container">
<!-- 步骤1: 文件上传 -->
<div v-show="currentStep === 1">
<FileUpload
:status="stepStatus[0]"
:loading="loadingStatus[0]"
:progress="progressStatus[0]"
@prev="navToPrev"
@next="completeStep(1)"
@file-uploaded="handleFileUploaded"
/>
</div>
<!-- 步骤2: 数据预处理 -->
<div v-show="currentStep === 2">
<DataPreprocessing
:status="stepStatus[1]"
:loading="loadingStatus[1]"
:progress="progressStatus[1]"
:fileData="processData.fileData"
@prev="navToPrev"
@next="completeStep(2)"
@start-preprocess="handlePreprocess"
/>
</div>
<!-- 步骤3: 格式合并 -->
<div v-show="currentStep === 3">
<FormatMerge
:status="stepStatus[2]"
:loading="loadingStatus[2]"
:progress="progressStatus[2]"
:preprocessedData="processData.preprocessedData"
@prev="navToPrev"
@next="completeStep(3)"
@start-merge="handleFormatMerge"
/>
</div>
<!-- 步骤4: 单词纠错 -->
<div v-show="currentStep === 4">
<WordCorrection
:status="stepStatus[3]"
:loading="loadingStatus[3]"
:progress="progressStatus[3]"
:mergedData="processData.mergedData"
@prev="navToPrev"
@next="completeStep(4)"
@start-correction="handleWordCorrection"
/>
</div>
<!-- 步骤5: 大模型分析 -->
<div v-show="currentStep === 5">
<ModelAnalysis
:status="stepStatus[4]"
:loading="loadingStatus[4]"
:progress="progressStatus[4]"
:correctedData="processData.correctedData"
@prev="navToPrev"
@finish="finishProcess"
@start-analysis="handleModelAnalysis"
/>
</div>
</div>
</div>
</template>
<script>
import FileUpload from './FileUpload.vue';
import DataPreprocessing from './DataPreprocessing.vue';
import FormatMerge from './FormatMerge.vue';
import WordCorrection from './WordCorrection.vue';
import ModelAnalysis from './ModelAnalysis.vue';
export default {
name: 'ProcessPage',
components: {
FileUpload,
DataPreprocessing,
FormatMerge,
WordCorrection,
ModelAnalysis
},
data() {
return {
currentStep: 1,
stepStatus: ['waiting', 'waiting', 'waiting', 'waiting', 'waiting'],
loadingStatus: [false, false, false, false, false],
progressStatus: [0, 0, 0, 0, 0],
processData: {
fileData: null,
preprocessedData: null,
mergedData: null,
correctedData: null,
analysisResult: null
},
// (ms)
simulatedProcessingTime: {
preprocess: 3000,
formatMerge: 2500,
wordCorrection: 4000,
modelAnalysis: 6000
}
};
},
methods: {
navToPrev() {
if (this.currentStep > 1) {
console.log('导航到上一步', this.currentStep, '->', this.currentStep - 1);
this.currentStep--;
}
},
completeStep(step) {
console.log('完成步骤', step, '准备导航到下一步');
//
this.stepStatus[step - 1] = 'completed';
//
if (step < 5) {
this.currentStep++;
console.log('导航到下一步', step, '->', this.currentStep);
//
this.stepStatus[step] = 'waiting';
}
},
simulateProgress(stepIndex, duration) {
//
this.progressStatus[stepIndex] = 0;
//
this.stepStatus[stepIndex] = 'running';
//
this.loadingStatus[stepIndex] = true;
const startTime = Date.now();
const endTime = startTime + duration;
const updateProgress = () => {
const now = Date.now();
const elapsed = now - startTime;
if (elapsed < duration) {
//
this.progressStatus[stepIndex] = Math.floor((elapsed / duration) * 100);
requestAnimationFrame(updateProgress);
} else {
//
this.progressStatus[stepIndex] = 100;
this.loadingStatus[stepIndex] = false;
this.stepStatus[stepIndex] = 'completed';
}
};
updateProgress();
},
handleFileUploaded(fileData) {
console.log('文件上传完成,数据:', fileData)
//
this.processData.fileData = fileData;
//
this.stepStatus[0] = 'completed';
this.loadingStatus[0] = false;
this.progressStatus[0] = 100;
//
this.$message({
message: '文件上传成功,请点击"下一步"按钮继续',
type: 'success',
duration: 5000, // 便
showClose: true
});
console.log('文件上传步骤状态已设置为:', this.stepStatus[0]);
},
handlePreprocess(preprocessConfig) {
console.log('开始预处理数据,配置:', preprocessConfig);
//
this.simulateProgress(1, this.simulatedProcessingTime.preprocess);
//
setTimeout(() => {
this.processData.preprocessedData = {
...this.processData.fileData,
rowCount: this.processData.fileData.rowCount - Math.floor(Math.random() * 200),
processedAt: new Date().toISOString(),
preprocessConfig
};
}, this.simulatedProcessingTime.preprocess);
},
handleFormatMerge(mergeConfig) {
console.log('开始格式合并,配置:', mergeConfig);
//
this.simulateProgress(2, this.simulatedProcessingTime.formatMerge);
//
setTimeout(() => {
this.processData.mergedData = {
...this.processData.preprocessedData,
format: mergeConfig.targetFormat,
mergedAt: new Date().toISOString(),
mergeConfig
};
}, this.simulatedProcessingTime.formatMerge);
},
handleWordCorrection(correctionConfig) {
console.log('开始单词纠错,配置:', correctionConfig);
//
this.simulateProgress(3, this.simulatedProcessingTime.wordCorrection);
//
setTimeout(() => {
this.processData.correctedData = {
...this.processData.mergedData,
errorsCorrected: Math.floor(Math.random() * 50),
correctedAt: new Date().toISOString(),
correctionConfig
};
}, this.simulatedProcessingTime.wordCorrection);
},
handleModelAnalysis(analysisConfig) {
console.log('开始大模型分析,配置:', analysisConfig);
//
this.simulateProgress(4, this.simulatedProcessingTime.modelAnalysis);
//
setTimeout(() => {
this.processData.analysisResult = {
...this.processData.correctedData,
analysisCompletedAt: new Date().toISOString(),
insightsGenerated: Math.floor(Math.random() * 20) + 5,
confidence: Math.random() * 0.3 + 0.7, // 0.7-1.0
analysisConfig
};
}, this.simulatedProcessingTime.modelAnalysis);
},
finishProcess() {
this.$message.success('所有处理步骤已完成!');
//
console.log('处理完成,最终结果:', this.processData);
}
}
};
</script>
<style scoped>
.process-page {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.process-header {
margin-bottom: 30px;
}
.process-header h2 {
text-align: center;
margin-bottom: 20px;
color: #303133;
}
.process-container {
margin-top: 40px;
}
</style>

@ -0,0 +1,225 @@
<template>
<div class="process-step">
<div class="step-header">
<h3>{{ title }}</h3>
<el-tag :type="tagType" effect="dark">{{ statusText }}</el-tag>
</div>
<!-- 进度条 -->
<el-progress
v-if="loading"
:percentage="progress"
:status="progressStatus"
:stroke-width="15"
class="step-progress"
></el-progress>
<!-- 预操作区域 -->
<div class="pre-operation" v-if="!isCompleted">
<slot name="pre-operation"></slot>
</div>
<!-- 操作区域 -->
<div class="operation" v-if="!isCompleted">
<slot name="operation"></slot>
<div v-if="status === 'waiting' || status === 'error'" class="operation-buttons">
<el-button
type="primary"
@click="startOperation"
:loading="loading"
>
{{ operationButtonText || '开始操作' }}
</el-button>
<el-button
v-if="status === 'error'"
@click="retryOperation"
>
{{ retryButtonText || '重试' }}
</el-button>
</div>
<div v-if="status === 'running'" class="processing-info">
<p>{{ processingText || '处理中...' }}</p>
</div>
</div>
<!-- 结果区域 - 只在完成后显示 -->
<div class="result" v-if="isCompleted">
<div class="result-summary">
<slot name="result-summary"></slot>
</div>
<div class="result-detail">
<slot name="result-detail"></slot>
</div>
</div>
<!-- 步骤操作按钮 -->
<div class="step-actions">
<el-button @click="prevStep" :disabled="loading" type="default">{{ '上一步' }}</el-button>
<el-button
type="primary"
@click="nextStep"
:disabled="!allowNext || loading"
>{{ '下一步' }}</el-button>
</div>
</div>
</template>
<script>
export default {
name: 'ProcessStep',
props: {
title: {
type: String,
required: true
},
status: {
type: String,
default: 'waiting',
validator: value => ['waiting', 'running', 'completed', 'reviewing', 'error', 'pending', 'processing'].includes(value)
},
loading: {
type: Boolean,
default: false
},
progress: {
type: Number,
default: 0
},
operationButtonText: {
type: String,
default: '开始操作'
},
processingText: {
type: String,
default: '处理中...'
},
processingPercentage: {
type: Number,
default: 0
},
errorText: {
type: String,
default: '操作失败'
},
retryButtonText: {
type: String,
default: '重试'
}
},
computed: {
statusText() {
const statusMap = {
waiting: '等待中',
pending: '等待中',
running: '处理中',
processing: '处理中',
completed: '已完成',
reviewing: '检查中',
error: '失败'
};
return statusMap[this.status] || '未知状态';
},
tagType() {
const typeMap = {
waiting: 'info',
pending: 'info',
running: 'warning',
processing: 'warning',
completed: 'success',
reviewing: 'primary',
error: 'danger'
};
return typeMap[this.status] || 'info';
},
progressStatus() {
if (this.status === 'completed') return 'success';
if (this.progress >= 100) return 'success';
if (this.status === 'error') return 'exception';
return '';
},
isCompleted() {
return this.status === 'completed' || this.status === 'reviewing';
},
allowNext() {
// completed
return this.status === 'completed';
}
},
methods: {
prevStep() {
console.log('触发prev事件');
this.$emit('prev');
},
nextStep() {
console.log('触发next事件当前状态:', this.status);
if (this.allowNext) {
this.$emit('next');
} else {
console.warn('当前状态不允许进入下一步:', this.status);
this.$message.warning('请先完成当前步骤');
}
},
startOperation() {
this.$emit('start');
},
retryOperation() {
this.$emit('retry');
}
}
};
</script>
<style scoped>
.process-step {
width: 100%;
padding: 20px;
border-radius: 4px;
background: #fff;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.step-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.step-header h3 {
margin: 0;
font-size: 18px;
color: #303133;
}
.step-progress {
margin: 20px 0;
}
.pre-operation, .operation, .result {
margin-bottom: 25px;
}
.operation-buttons {
margin-top: 15px;
text-align: center;
}
.processing-info {
margin-top: 15px;
text-align: center;
color: #909399;
}
.result-summary, .result-detail {
margin-bottom: 15px;
}
.step-actions {
display: flex;
justify-content: space-between;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #EBEEF5;
}
</style>

@ -0,0 +1,831 @@
<template>
<div class="word-correction">
<process-step
title="单词纠错"
:status="correctionStatus"
:loading="loading"
operationButtonText="开始单词纠错"
processingText="单词纠错处理中..."
:processingPercentage="correctionProgress"
errorText="单词纠错失败,请重试"
@start="handleStartCorrection"
@retry="handleRetry"
>
<template #pre-operation>
<div class="correction-config">
<h4>纠错配置</h4>
<el-alert
v-if="!formatMerged"
title="请先完成格式合并步骤"
type="warning"
show-icon
:closable="false"
/>
<el-form v-else ref="configForm" :model="configForm" label-width="120px" size="small">
<el-form-item label="纠错方法">
<el-radio-group v-model="configForm.correctionMethod">
<el-radio label="auto">自动纠错</el-radio>
<el-radio label="manual">手动确认</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="纠错字段">
<el-select
v-model="configForm.targetFields"
multiple
placeholder="请选择需要纠错的字段"
>
<el-option
v-for="field in availableFields"
:key="field.value"
:label="field.label"
:value="field.value"
/>
</el-select>
</el-form-item>
<el-form-item label="字典选择">
<el-select v-model="configForm.dictionary" placeholder="请选择参考字典">
<el-option label="标准英语词典" value="en_standard"></el-option>
<el-option label="专业术语词典" value="technical"></el-option>
<el-option label="自定义词典" value="custom"></el-option>
</el-select>
</el-form-item>
<el-form-item v-if="configForm.dictionary === 'custom'" label="上传词典">
<el-upload
action="#"
:auto-upload="false"
:on-change="handleDictionaryUpload"
:limit="1"
>
<el-button size="small" type="primary">点击上传</el-button>
<div slot="tip" class="el-upload__tip">请上传TXT或CSV文件每行一个词汇</div>
</el-upload>
</el-form-item>
<el-form-item label="拼写相似度阈值">
<el-slider
v-model="configForm.similarityThreshold"
:min="50"
:max="100"
:format-tooltip="formatThreshold"
></el-slider>
</el-form-item>
<el-form-item label="高级选项">
<el-checkbox v-model="configForm.ignoreCase"></el-checkbox>
<el-checkbox v-model="configForm.ignorePunctuation"></el-checkbox>
<el-checkbox v-model="configForm.useContextCheck">使</el-checkbox>
</el-form-item>
</el-form>
</div>
</template>
<template #result-summary>
<div class="correction-result-summary">
<i class="el-icon-check"></i>
<h4>单词纠错完成</h4>
<div class="summary-stats">
<div class="stat-item">
<span class="stat-value">{{ resultData.totalProcessed }}</span>
<span class="stat-label">处理词汇总数</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ resultData.correctedCount }}</span>
<span class="stat-label">已纠正词汇</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ resultData.correctionRate }}%</span>
<span class="stat-label">纠正率</span>
</div>
<div class="stat-item">
<span class="stat-value">{{ resultData.processingTime }}</span>
<span class="stat-label">处理时间</span>
</div>
</div>
</div>
</template>
<template #result-detail>
<div class="correction-result-detail">
<el-tabs type="border-card">
<el-tab-pane label="纠错详情">
<div class="correction-table">
<div class="table-actions">
<el-input
placeholder="搜索词汇"
v-model="searchWord"
prefix-icon="el-icon-search"
size="small"
style="width: 200px; margin-right: 15px;"
></el-input>
<el-select
v-model="filterStatus"
placeholder="筛选状态"
size="small"
style="width: 150px; margin-right: 15px;"
>
<el-option label="全部" value=""></el-option>
<el-option label="已纠正" value="corrected"></el-option>
<el-option label="未纠正" value="unchanged"></el-option>
<el-option label="待确认" value="pending"></el-option>
</el-select>
<el-button
size="small"
type="primary"
@click="confirmAllPending"
:disabled="!hasPendingCorrections"
>
确认所有待修改项
</el-button>
</div>
<el-table
:data="filteredCorrectionResults"
border
size="small"
style="width: 100%"
height="350"
>
<el-table-column label="原词" prop="original" width="180"></el-table-column>
<el-table-column label="纠正建议" width="180">
<template slot-scope="scope">
<span v-if="scope.row.status === 'pending'">
<el-select v-model="scope.row.suggested" size="small" style="width: 100%">
<el-option
v-for="(suggestion, idx) in scope.row.suggestions"
:key="idx"
:label="suggestion"
:value="suggestion"
></el-option>
</el-select>
</span>
<span v-else>{{ scope.row.suggested }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template slot-scope="scope">
<el-tag
:type="scope.row.status === 'corrected' ? 'success' :
scope.row.status === 'pending' ? 'warning' : 'info'"
>
{{
scope.row.status === 'corrected' ? '已纠正' :
scope.row.status === 'pending' ? '待确认' : '未纠正'
}}
</el-tag>
</template>
</el-table-column>
<el-table-column label="相似度" width="100">
<template slot-scope="scope">
<span>{{ scope.row.similarity }}%</span>
</template>
</el-table-column>
<el-table-column label="上下文" show-overflow-tooltip>
<template slot-scope="scope">
<span v-html="highlightWord(scope.row.context, scope.row.original)"></span>
</template>
</el-table-column>
<el-table-column label="操作" width="150" v-if="configForm.correctionMethod === 'manual'">
<template slot-scope="scope">
<el-button
size="mini"
type="success"
@click="confirmCorrection(scope.row)"
v-if="scope.row.status === 'pending'"
>
确认
</el-button>
<el-button
size="mini"
type="info"
@click="ignoreCorrection(scope.row)"
v-if="scope.row.status === 'pending'"
>
忽略
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="currentPage"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="filteredCorrectionResults.length"
>
</el-pagination>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="统计分析">
<div class="stats-charts">
<el-row :gutter="20">
<el-col :span="12">
<div class="chart-container">
<h5>纠错类型分布</h5>
<div class="mock-chart pie-chart"></div>
<div class="chart-legend">
<div class="legend-item">
<span class="color-box" style="background-color: #409EFF;"></span>
<span>拼写错误{{ resultData.errorTypeStats.spelling }}%</span>
</div>
<div class="legend-item">
<span class="color-box" style="background-color: #67C23A;"></span>
<span>大小写错误{{ resultData.errorTypeStats.capitalization }}%</span>
</div>
<div class="legend-item">
<span class="color-box" style="background-color: #E6A23C;"></span>
<span>重复单词{{ resultData.errorTypeStats.duplication }}%</span>
</div>
<div class="legend-item">
<span class="color-box" style="background-color: #F56C6C;"></span>
<span>语法错误{{ resultData.errorTypeStats.grammar }}%</span>
</div>
<div class="legend-item">
<span class="color-box" style="background-color: #909399;"></span>
<span>其他错误{{ resultData.errorTypeStats.other }}%</span>
</div>
</div>
</div>
</el-col>
<el-col :span="12">
<div class="chart-container">
<h5>纠错率按字段统计</h5>
<div class="mock-chart bar-chart"></div>
</div>
</el-col>
</el-row>
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="24">
<div class="chart-container">
<h5>常见错误单词排行</h5>
<el-table
:data="resultData.commonErrors"
border
size="small"
style="width: 100%"
>
<el-table-column label="排名" type="index" width="80"></el-table-column>
<el-table-column label="错误单词" prop="word"></el-table-column>
<el-table-column label="出现次数" prop="count" width="120"></el-table-column>
<el-table-column label="纠正为" prop="correctedTo"></el-table-column>
<el-table-column label="错误类型" prop="errorType" width="120"></el-table-column>
</el-table>
</div>
</el-col>
</el-row>
</div>
</el-tab-pane>
<el-tab-pane label="处理日志">
<div class="processing-logs">
<el-timeline>
<el-timeline-item
v-for="(log, index) in resultData.logs"
:key="index"
:type="log.type"
:color="log.color"
:timestamp="log.timestamp"
>
{{ log.content }}
</el-timeline-item>
</el-timeline>
</div>
</el-tab-pane>
</el-tabs>
</div>
</template>
</process-step>
</div>
</template>
<script>
import ProcessStep from './ProcessStep'
import { mapState, mapActions } from 'vuex'
export default {
name: 'WordCorrectionComponent',
components: {
ProcessStep
},
data() {
return {
loading: false,
correctionStatus: 'pending',
correctionProgress: 0,
correctionTimer: null,
configForm: {
correctionMethod: 'auto',
targetFields: [],
dictionary: 'en_standard',
similarityThreshold: 80,
ignoreCase: true,
ignorePunctuation: true,
useContextCheck: true
},
resultData: {
totalProcessed: 0,
correctedCount: 0,
correctionRate: 0,
processingTime: 0,
correctionResults: [],
errorTypeStats: {
spelling: 0,
capitalization: 0,
duplication: 0,
grammar: 0,
other: 0
},
commonErrors: [],
logs: []
},
searchWord: '',
filterStatus: '',
currentPage: 1,
pageSize: 10
}
},
computed: {
...mapState({
fileData: state => state.process.fileData,
fileUploaded: state => state.process.fileUploaded,
preprocessed: state => state.process.preprocessed,
preprocessData: state => state.process.preprocessData,
formatMerged: state => state.process.formatMerged,
mergeData: state => state.process.mergeData,
wordCorrected: state => state.process.wordCorrected,
wordCorrectionData: state => state.process.wordCorrectionData
}),
availableFields() {
//
if (this.formatMerged && this.mergeData && this.mergeData.previewData && this.mergeData.previewData.length > 0) {
const sampleData = this.mergeData.previewData[0]
return Object.keys(sampleData).map(key => ({
label: key,
value: key
}))
}
return []
},
filteredCorrectionResults() {
if (!this.resultData.correctionResults) return []
return this.resultData.correctionResults.filter(item => {
const matchesSearch = this.searchWord ?
(item.original.toLowerCase().includes(this.searchWord.toLowerCase()) ||
item.suggested.toLowerCase().includes(this.searchWord.toLowerCase())) :
true
const matchesStatus = this.filterStatus ?
item.status === this.filterStatus :
true
return matchesSearch && matchesStatus
})
},
hasPendingCorrections() {
return this.resultData.correctionResults &&
this.resultData.correctionResults.some(item => item.status === 'pending')
}
},
watch: {
wordCorrected(newVal) {
if (newVal) {
this.correctionStatus = 'completed'
this.resultData = this.wordCorrectionData
}
}
},
mounted() {
if (this.wordCorrected) {
this.correctionStatus = 'completed'
this.resultData = this.wordCorrectionData
}
},
methods: {
...mapActions('process', [
'wordCorrectionAction',
'setWordCorrectionData'
]),
formatThreshold(val) {
return val + '%'
},
handleDictionaryUpload(file) {
//
if (file) {
this.$message.success('自定义词典上传成功:' + file.name)
}
},
handleStartCorrection() {
if (!this.formatMerged) {
this.$message.warning('请先完成格式合并步骤')
return
}
if (this.configForm.targetFields.length === 0) {
this.$message.warning('请选择至少一个需要纠错的字段')
return
}
this.loading = true
this.correctionStatus = 'processing'
this.correctionProgress = 0
//
this.correctionTimer = setInterval(() => {
this.correctionProgress += 5
if (this.correctionProgress >= 100) {
clearInterval(this.correctionTimer)
this.simulateCorrectionComplete()
}
}, 200)
// API
this.wordCorrectionAction({
config: this.configForm
}).catch(error => {
clearInterval(this.correctionTimer)
this.correctionStatus = 'error'
this.loading = false
this.$message.error('单词纠错失败:' + (error.message || '未知错误'))
})
},
simulateCorrectionComplete() {
//
setTimeout(() => {
this.loading = false
//
const mockResult = this.generateMockResult()
// Vuex
this.setWordCorrectionData(mockResult)
this.resultData = mockResult
this.correctionStatus = 'completed'
}, 800)
},
handleRetry() {
this.correctionStatus = 'pending'
this.correctionProgress = 0
},
confirmCorrection(row) {
//
row.status = 'corrected'
this.updateCorrectionStats()
},
ignoreCorrection(row) {
//
row.status = 'unchanged'
this.updateCorrectionStats()
},
confirmAllPending() {
//
this.resultData.correctionResults.forEach(item => {
if (item.status === 'pending') {
item.status = 'corrected'
}
})
this.updateCorrectionStats()
this.$message.success('已确认所有待修改项')
},
updateCorrectionStats() {
//
const correctedCount = this.resultData.correctionResults.filter(
item => item.status === 'corrected'
).length
this.resultData.correctedCount = correctedCount
this.resultData.correctionRate = Math.round(
(correctedCount / this.resultData.totalProcessed) * 100
)
// Vuex
this.setWordCorrectionData({...this.resultData})
},
highlightWord(context, word) {
if (!context || !word) return ''
try {
const regex = new RegExp(`(${word})`, 'gi')
return context.replace(regex, '<span class="highlight-word">$1</span>')
} catch (e) {
return context
}
},
handleSizeChange(val) {
this.pageSize = val
},
handleCurrentChange(val) {
this.currentPage = val
},
generateMockResult() {
//
const totalProcessed = Math.floor(Math.random() * 500) + 1000 // 1000-1500
const correctedCount = Math.floor(totalProcessed * (Math.random() * 0.2 + 0.1)) // 10%-30%
const correctionRate = Math.round((correctedCount / totalProcessed) * 100)
const processingTime = (Math.random() * 10 + 5).toFixed(1) // 5-15
//
const errorTypeStats = {
spelling: Math.round(Math.random() * 30 + 40), // 40-70%
capitalization: Math.round(Math.random() * 15 + 10), // 10-25%
duplication: Math.round(Math.random() * 10 + 5), // 5-15%
grammar: Math.round(Math.random() * 10 + 5), // 5-15%
other: 0
}
// 100%
const sum = Object.values(errorTypeStats).reduce((acc, val) => acc + val, 0)
errorTypeStats.other = 100 - sum + errorTypeStats.other
//
const commonErrors = [
{ word: 'teh', count: Math.floor(Math.random() * 50) + 30, correctedTo: 'the', errorType: '拼写错误' },
{ word: 'langauge', count: Math.floor(Math.random() * 40) + 20, correctedTo: 'language', errorType: '拼写错误' },
{ word: 'recieve', count: Math.floor(Math.random() * 30) + 15, correctedTo: 'receive', errorType: '拼写错误' },
{ word: 'seperate', count: Math.floor(Math.random() * 25) + 10, correctedTo: 'separate', errorType: '拼写错误' },
{ word: 'definate', count: Math.floor(Math.random() * 20) + 10, correctedTo: 'definite', errorType: '拼写错误' },
{ word: 'occured', count: Math.floor(Math.random() * 15) + 5, correctedTo: 'occurred', errorType: '拼写错误' },
{ word: 'goverment', count: Math.floor(Math.random() * 15) + 5, correctedTo: 'government', errorType: '拼写错误' },
{ word: 'accomodate', count: Math.floor(Math.random() * 12) + 3, correctedTo: 'accommodate', errorType: '拼写错误' },
{ word: 'begining', count: Math.floor(Math.random() * 10) + 3, correctedTo: 'beginning', errorType: '拼写错误' },
{ word: 'beleive', count: Math.floor(Math.random() * 10) + 3, correctedTo: 'believe', errorType: '拼写错误' }
]
//
const correctionResults = []
const statuses = ['corrected', 'unchanged', 'pending']
const contexts = [
'这是一个示例句子,其中包含{word}单词.',
'在这个上下文中,我们可以看到{word}出现在中间位置.',
'{word}可能会出现在句子的开头,这也是常见情况.',
'文本分析通常会处理类似{word}这样的单词.',
'数据中的{word}需要进行纠正处理.'
]
// 100
for (let i = 0; i < 100; i++) {
const original = `example_word_${i + 1}`
const suggested = `corrected_word_${i + 1}`
const statusIndex = Math.floor(Math.random() * 3) //
const status = statuses[statusIndex]
const similarity = Math.floor(Math.random() * 20) + 80 // 80-100
//
const contextTemplate = contexts[Math.floor(Math.random() * contexts.length)]
const context = contextTemplate.replace('{word}', original)
//
const suggestions = [
suggested,
`alt_suggestion_1_${i + 1}`,
`alt_suggestion_2_${i + 1}`
]
correctionResults.push({
original,
suggested,
status,
similarity,
context,
suggestions
})
}
//
const logs = [
{
content: '开始单词纠错处理',
timestamp: '11:15:10',
type: 'primary',
color: '#409EFF'
},
{
content: `加载${this.configForm.dictionary === 'en_standard' ? '标准英语词典' :
this.configForm.dictionary === 'technical' ? '专业术语词典' : '自定义词典'}`,
timestamp: '11:15:15',
type: 'info',
color: '#909399'
},
{
content: `设置相似度阈值为${this.configForm.similarityThreshold}%`,
timestamp: '11:15:20',
type: 'info',
color: '#909399'
},
{
content: `处理${this.configForm.targetFields.join(', ')}字段中的单词`,
timestamp: '11:15:25',
type: 'info',
color: '#909399'
},
{
content: `发现${correctedCount}个需要纠正的单词`,
timestamp: '11:15:40',
type: 'warning',
color: '#E6A23C'
},
{
content: this.configForm.correctionMethod === 'auto' ?
'自动完成所有单词纠正' :
'等待手动确认纠正建议',
timestamp: '11:15:45',
type: 'info',
color: '#909399'
},
{
content: `单词纠错完成,处理了${totalProcessed}个单词,纠正了${correctedCount}个单词`,
timestamp: '11:16:00',
type: 'success',
color: '#67C23A'
}
]
return {
totalProcessed,
correctedCount,
correctionRate,
processingTime,
correctionResults,
errorTypeStats,
commonErrors,
logs
}
}
},
beforeDestroy() {
if (this.correctionTimer) {
clearInterval(this.correctionTimer)
}
}
}
</script>
<style scoped>
.word-correction {
margin-bottom: 20px;
}
.correction-config {
margin-bottom: 20px;
}
.correction-config h4 {
margin-bottom: 15px;
font-size: 16px;
color: #303133;
}
.correction-result-summary {
text-align: center;
padding: 15px 0;
}
.correction-result-summary i {
font-size: 40px;
color: #67c23a;
margin-bottom: 10px;
}
.correction-result-summary h4 {
margin: 10px 0;
font-size: 18px;
color: #303133;
}
.summary-stats {
display: flex;
justify-content: center;
gap: 30px;
margin-top: 20px;
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #409EFF;
}
.stat-label {
font-size: 14px;
color: #606266;
margin-top: 5px;
}
.table-actions {
display: flex;
align-items: center;
margin-bottom: 15px;
}
.pagination-container {
margin-top: 15px;
text-align: right;
}
.chart-container {
background-color: #fff;
border-radius: 4px;
border: 1px solid #ebeef5;
padding: 20px;
margin-bottom: 20px;
}
.chart-container h5 {
margin: 0 0 15px 0;
font-size: 16px;
color: #303133;
}
.mock-chart {
height: 250px;
margin-bottom: 15px;
background-color: #f5f7fa;
border-radius: 4px;
position: relative;
}
.mock-chart.pie-chart::before {
content: "饼图 - 纠错类型分布";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #909399;
}
.mock-chart.bar-chart::before {
content: "柱状图 - 字段纠错率统计";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #909399;
}
.chart-legend {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.legend-item {
display: flex;
align-items: center;
margin-right: 15px;
}
.color-box {
width: 15px;
height: 15px;
margin-right: 5px;
border-radius: 3px;
}
.processing-logs {
padding: 10px;
}
.highlight-word {
background-color: rgba(255, 230, 0, 0.3);
font-weight: bold;
}
</style>

@ -0,0 +1,16 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import './assets/css/global.css'
Vue.config.productionTip = false
Vue.use(ElementUI)
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')

@ -0,0 +1,33 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
{
path: '/',
redirect: '/process'
},
{
path: '/process',
name: 'ProcessView',
component: () => import('../views/Process.vue')
},
{
path: '/analysis',
name: 'AnalysisView',
component: () => import('../views/Analysis.vue')
},
{
path: '*',
redirect: '/process'
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router

@ -0,0 +1,15 @@
import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import process from './modules/process'
import analysis from './modules/analysis'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
app,
process,
analysis
}
})

@ -0,0 +1,84 @@
// 分析页面相关状态模块
const state = {
selectedTerms: [],
termFrequencies: [],
documentSegments: [],
contexts: {},
collocates: {}
}
const mutations = {
SET_TERM_FREQUENCIES(state, terms) {
state.termFrequencies = terms
},
SET_SELECTED_TERMS(state, terms) {
state.selectedTerms = terms
},
ADD_SELECTED_TERM(state, term) {
if (!state.selectedTerms.includes(term)) {
state.selectedTerms.push(term)
}
},
REMOVE_SELECTED_TERM(state, term) {
state.selectedTerms = state.selectedTerms.filter(t => t !== term)
},
SET_DOCUMENT_SEGMENTS(state, segments) {
state.documentSegments = segments
},
SET_CONTEXTS(state, { term, contexts }) {
state.contexts = { ...state.contexts, [term]: contexts }
},
SET_COLLOCATES(state, { term, collocates }) {
state.collocates = { ...state.collocates, [term]: collocates }
}
}
const actions = {
setTermFrequencies({ commit }, terms) {
commit('SET_TERM_FREQUENCIES', terms)
},
setSelectedTerms({ commit }, terms) {
commit('SET_SELECTED_TERMS', terms)
},
addSelectedTerm({ commit }, term) {
commit('ADD_SELECTED_TERM', term)
},
removeSelectedTerm({ commit }, term) {
commit('REMOVE_SELECTED_TERM', term)
},
setDocumentSegments({ commit }, segments) {
commit('SET_DOCUMENT_SEGMENTS', segments)
},
fetchContexts({ commit }, term) {
// 这里会有API调用暂时模拟
const contexts = [
{ left: '...文本', keyword: term, right: '文本...' },
{ left: '...另一段', keyword: term, right: '文本...' }
]
commit('SET_CONTEXTS', { term, contexts })
},
fetchCollocates({ commit }, term) {
// 这里会有API调用暂时模拟
const collocates = [
{ word: '相关词1', frequency: 10 },
{ word: '相关词2', frequency: 8 }
]
commit('SET_COLLOCATES', { term, collocates })
}
}
const getters = {
termFrequencies: state => state.termFrequencies,
selectedTerms: state => state.selectedTerms,
documentSegments: state => state.documentSegments,
contexts: state => term => state.contexts[term] || [],
collocates: state => term => state.collocates[term] || []
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}

@ -0,0 +1,39 @@
// 全局应用状态模块
const state = {
loading: false,
error: null
}
const mutations = {
SET_LOADING(state, loading) {
state.loading = loading
},
SET_ERROR(state, error) {
state.error = error
}
}
const actions = {
setLoading({ commit }, loading) {
commit('SET_LOADING', loading)
},
setError({ commit }, error) {
commit('SET_ERROR', error)
},
clearError({ commit }) {
commit('SET_ERROR', null)
}
}
const getters = {
isLoading: state => state.loading,
error: state => state.error
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}

@ -0,0 +1,107 @@
// 处理流程相关状态模块
const state = {
currentStep: 1,
uploadedFiles: [],
fileData: null,
preprocessResults: null,
mergeResults: null,
correctionResults: null,
analysisResults: null,
stepStatus: {
upload: 'pending',
preprocess: 'pending',
merge: 'pending',
correction: 'pending',
analysis: 'pending'
}
}
const mutations = {
SET_CURRENT_STEP(state, step) {
state.currentStep = step
},
SET_UPLOADED_FILES(state, files) {
state.uploadedFiles = files
},
SET_FILE_DATA(state, data) {
state.fileData = data
},
SET_PREPROCESS_RESULTS(state, results) {
state.preprocessResults = results
},
SET_MERGE_RESULTS(state, results) {
state.mergeResults = results
},
SET_CORRECTION_RESULTS(state, results) {
state.correctionResults = results
},
SET_ANALYSIS_RESULTS(state, results) {
state.analysisResults = results
},
UPDATE_STEP_STATUS(state, { step, status }) {
state.stepStatus[step] = status
}
}
const actions = {
setCurrentStep({ commit }, step) {
commit('SET_CURRENT_STEP', step)
},
uploadFiles({ commit }, files) {
commit('SET_UPLOADED_FILES', files)
commit('UPDATE_STEP_STATUS', { step: 'upload', status: 'completed' })
},
setFileData({ commit }, data) {
commit('SET_FILE_DATA', data)
},
processPreprocess({ commit }, results) {
commit('SET_PREPROCESS_RESULTS', results)
commit('UPDATE_STEP_STATUS', { step: 'preprocess', status: 'completed' })
},
processMerge({ commit }, results) {
commit('SET_MERGE_RESULTS', results)
commit('UPDATE_STEP_STATUS', { step: 'merge', status: 'completed' })
},
processCorrection({ commit }, results) {
commit('SET_CORRECTION_RESULTS', results)
commit('UPDATE_STEP_STATUS', { step: 'correction', status: 'completed' })
},
processAnalysis({ commit }, results) {
commit('SET_ANALYSIS_RESULTS', results)
commit('UPDATE_STEP_STATUS', { step: 'analysis', status: 'completed' })
},
resetProcess({ commit }) {
commit('SET_CURRENT_STEP', 1)
commit('SET_UPLOADED_FILES', [])
commit('SET_FILE_DATA', null)
commit('SET_PREPROCESS_RESULTS', null)
commit('SET_MERGE_RESULTS', null)
commit('SET_CORRECTION_RESULTS', null)
commit('SET_ANALYSIS_RESULTS', null)
const steps = ['upload', 'preprocess', 'merge', 'correction', 'analysis']
steps.forEach(step => {
commit('UPDATE_STEP_STATUS', { step, status: 'pending' })
})
}
}
const getters = {
currentStep: state => state.currentStep,
uploadedFiles: state => state.uploadedFiles,
fileData: state => state.fileData,
preprocessResults: state => state.preprocessResults,
mergeResults: state => state.mergeResults,
correctionResults: state => state.correctionResults,
analysisResults: state => state.analysisResults,
stepStatus: state => state.stepStatus,
isStepCompleted: state => step => state.stepStatus[step] === 'completed'
}
export default {
namespaced: true,
state,
mutations,
actions,
getters
}

@ -0,0 +1,176 @@
// 模拟数据生成工具
// 文件上传模拟数据
export function getUploadData() {
return {
success: true,
data: {
fileName: 'sample.xlsx',
rows: 150,
columns: 5,
preview: [
{ id: 1, content: '示例内容1', date: '2023-05-10', source: '来源A', type: '类型1' },
{ id: 2, content: '示例内容2', date: '2023-05-11', source: '来源B', type: '类型2' },
{ id: 3, content: '示例内容3', date: '2023-05-12', source: '来源A', type: '类型1' }
]
}
}
}
// 预处理模拟数据
export function getPreprocessData() {
return {
success: true,
data: {
processed: 150,
invalidRows: 2,
normalizedFields: ['date', 'content'],
preview: [
{ id: 1, content: '规范化后的内容1', date: '2023-05-10', source: '来源A', type: '类型1' },
{ id: 2, content: '规范化后的内容2', date: '2023-05-11', source: '来源B', type: '类型2' },
{ id: 3, content: '规范化后的内容3', date: '2023-05-12', source: '来源A', type: '类型1' }
]
}
}
}
// 合并格式模拟数据
export function getMergeData() {
return {
success: true,
data: {
merged: 148,
format: 'standardized',
preview: [
{ id: 1, content: '合并格式后的内容1', date: '2023-05-10', source: '来源A', type: '类型1' },
{ id: 2, content: '合并格式后的内容2', date: '2023-05-11', source: '来源B', type: '类型2' },
{ id: 3, content: '合并格式后的内容3', date: '2023-05-12', source: '来源A', type: '类型1' }
]
}
}
}
// 纠错模拟数据
export function getCorrectionData() {
return {
success: true,
data: {
corrected: 148,
corrections: 25,
preview: [
{ id: 1, content: '纠错后的内容1', date: '2023-05-10', source: '来源A', type: '类型1' },
{ id: 2, content: '纠错后的内容2', date: '2023-05-11', source: '来源B', type: '类型2' },
{ id: 3, content: '纠错后的内容3', date: '2023-05-12', source: '来源A', type: '类型1' }
]
}
}
}
// 分析模拟数据
export function getAnalysisData() {
return {
success: true,
data: {
termFrequencies: getTermFrequencies().data,
documentSegments: getDocumentSegments().data,
summary: {
totalWords: 12568,
uniqueWords: 3452,
averageSentenceLength: 15.7,
documentCount: 148
}
}
}
}
// 词频列表模拟数据
export function getTermFrequencies() {
return {
success: true,
data: [
{ term: '数据', frequency: 189, rank: 1 },
{ term: '分析', frequency: 156, rank: 2 },
{ term: '模型', frequency: 143, rank: 3 },
{ term: '学习', frequency: 122, rank: 4 },
{ term: '算法', frequency: 118, rank: 5 },
{ term: '机器', frequency: 105, rank: 6 },
{ term: '深度', frequency: 92, rank: 7 },
{ term: '训练', frequency: 87, rank: 8 },
{ term: '人工智能', frequency: 76, rank: 9 },
{ term: '神经网络', frequency: 65, rank: 10 },
{ term: '预测', frequency: 58, rank: 11 },
{ term: '精度', frequency: 51, rank: 12 },
{ term: '特征', frequency: 45, rank: 13 },
{ term: '计算', frequency: 40, rank: 14 },
{ term: '优化', frequency: 35, rank: 15 }
]
}
}
// 上下文模拟数据
export function getTermContexts(term) {
return {
success: true,
data: [
{ left: '使用各种', keyword: term, right: '方法处理大规模数据集' },
{ left: '通过深度学习改进', keyword: term, right: '结果的准确性和可靠性' },
{ left: '传统的统计学习方法在某些', keyword: term, right: '任务中仍然表现良好' },
{ left: '研究表明,增加训练数据可以提高', keyword: term, right: '模型的泛化能力' },
{ left: '在数据处理过程中,', keyword: term, right: '步骤起到了关键作用' }
]
}
}
// 搭配词模拟数据
export function getTermCollocates(term) {
// 根据不同的term返回不同的搭配词
const baseData = [
{ word: '深度', frequency: 23, score: 0.85 },
{ word: '机器', frequency: 19, score: 0.82 },
{ word: '数据', frequency: 18, score: 0.78 },
{ word: '模型', frequency: 17, score: 0.75 },
{ word: '算法', frequency: 15, score: 0.72 },
{ word: '训练', frequency: 12, score: 0.68 },
{ word: '优化', frequency: 10, score: 0.65 },
{ word: '预测', frequency: 9, score: 0.62 },
{ word: '特征', frequency: 8, score: 0.58 },
{ word: '精度', frequency: 7, score: 0.55 }
]
// 在数据中添加关联到term的信息
const result = baseData.map(item => ({
...item,
relatedTo: term // 使用term参数
}))
return {
success: true,
data: result
}
}
// 文档段落模拟数据
export function getDocumentSegments() {
return {
success: true,
data: [
{ id: 1, name: '段落1', wordCount: 850 },
{ id: 2, name: '段落2', wordCount: 920 },
{ id: 3, name: '段落3', wordCount: 780 },
{ id: 4, name: '段落4', wordCount: 860 },
{ id: 5, name: '段落5', wordCount: 910 }
]
}
}
export default {
getUploadData,
getPreprocessData,
getMergeData,
getCorrectionData,
getAnalysisData,
getTermFrequencies,
getTermContexts,
getTermCollocates,
getDocumentSegments
}

@ -0,0 +1,197 @@
<template>
<div class="analysis-container">
<h1 class="analysis-title">文本分析结果</h1>
<div class="analysis-panels">
<div class="grid-container">
<!-- 左侧词频面板 -->
<div class="panel terms-panel">
<div class="panel-header">
<h3>词频列表</h3>
</div>
<div class="panel-content">
<p>词频列表组件将在这里显示</p>
</div>
</div>
<!-- 中间词云面板 -->
<div class="panel wordcloud-panel">
<div class="panel-header">
<h3>词云图</h3>
</div>
<div class="panel-content">
<p>词云组件将在这里显示</p>
</div>
</div>
<!-- 右上角趋势图面板 -->
<div class="panel trends-panel">
<div class="panel-header">
<h3>趋势分析</h3>
</div>
<div class="panel-content">
<p>趋势图组件将在这里显示</p>
</div>
</div>
<!-- 底部详情面板 -->
<div class="panel details-panel">
<div class="panel-header">
<h3>详细信息</h3>
</div>
<div class="panel-content">
<el-tabs type="border-card">
<el-tab-pane label="摘要">
<p>摘要内容将在这里显示</p>
</el-tab-pane>
<el-tab-pane label="上下文">
<p>上下文组件将在这里显示</p>
</el-tab-pane>
<el-tab-pane label="搭配词">
<p>搭配词组件将在这里显示</p>
</el-tab-pane>
<el-tab-pane label="文档">
<p>文档视图将在这里显示</p>
</el-tab-pane>
</el-tabs>
</div>
</div>
</div>
</div>
<div class="analysis-actions">
<el-button
icon="el-icon-back"
@click="returnToProcess"
>返回处理页面</el-button>
<el-button
type="primary"
icon="el-icon-download"
@click="exportResults"
>导出结果</el-button>
</div>
</div>
</template>
<script>
import { mapState, mapActions } from 'vuex'
export default {
name: 'AnalysisView',
computed: {
...mapState('analysis', ['termFrequencies', 'selectedTerms'])
},
methods: {
...mapActions('analysis', [
'setTermFrequencies',
'setSelectedTerms'
]),
returnToProcess() {
this.$router.push('/process')
},
exportResults() {
//
this.$message({
message: '结果导出功能将在后续版本中实现',
type: 'info'
})
}
},
created() {
// storeAPI
// API
if (this.termFrequencies.length === 0) {
//
setTimeout(() => {
this.setTermFrequencies([
{ term: '示例词1', frequency: 100 },
{ term: '示例词2', frequency: 80 },
{ term: '示例词3', frequency: 60 }
])
}, 500)
}
}
}
</script>
<style scoped>
.analysis-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.analysis-title {
text-align: center;
margin-bottom: 30px;
color: #1890ff;
}
.grid-container {
display: grid;
grid-template-columns: 250px 1fr 300px;
grid-template-rows: 300px 1fr;
grid-gap: 15px;
height: calc(100vh - 200px);
min-height: 600px;
}
.panel {
background-color: #f5f7fa;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.05);
display: flex;
flex-direction: column;
}
.panel-header {
padding: 10px 15px;
border-bottom: 1px solid #e6e6e6;
background-color: #f9f9f9;
}
.panel-header h3 {
margin: 0;
font-size: 16px;
color: #606266;
}
.panel-content {
padding: 15px;
flex: 1;
overflow: auto;
}
.terms-panel {
grid-column: 1;
grid-row: 1 / span 2;
}
.wordcloud-panel {
grid-column: 2;
grid-row: 1 / span 2;
}
.trends-panel {
grid-column: 3;
grid-row: 1;
}
.details-panel {
grid-column: 3;
grid-row: 2;
}
.analysis-actions {
display: flex;
justify-content: space-between;
margin-top: 20px;
padding: 10px 0;
}
</style>

@ -0,0 +1,130 @@
<template>
<div class="process-container">
<h1 class="process-title">文件处理流程</h1>
<div class="process-steps">
<el-steps :active="currentStep" finish-status="success" align-center>
<el-step title="文件上传" description="上传Excel或CSV文件"></el-step>
<el-step title="数据预处理" description="规范化文本格式"></el-step>
<el-step title="格式合并" description="统一数据格式"></el-step>
<el-step title="单词纠错" description="纠正错误拼写"></el-step>
<el-step title="大模型分析" description="使用AI分析内容"></el-step>
</el-steps>
</div>
<div class="step-content">
<!-- 步骤内容区域 -->
<div v-if="currentStep === 1">
<file-upload />
</div>
<div v-else-if="currentStep === 2">
<pre-process />
</div>
<div v-else-if="currentStep === 3">
<merge-format />
</div>
<div v-else-if="currentStep === 4">
<word-correction />
</div>
<div v-else-if="currentStep === 5">
<model-analysis />
</div>
</div>
</div>
</template>
<script>
import { mapState, mapActions, mapGetters } from 'vuex'
import FileUpload from '../components/process/FileUpload.vue'
import PreProcess from '../components/process/PreProcess.vue'
import MergeFormat from '../components/process/MergeFormat.vue'
import WordCorrection from '../components/process/WordCorrection.vue'
import ModelAnalysis from '../components/process/ModelAnalysis.vue'
export default {
name: 'ProcessView',
components: {
FileUpload,
PreProcess,
MergeFormat,
WordCorrection,
ModelAnalysis
},
computed: {
...mapState('process', ['currentStep', 'uploadedFiles']),
...mapGetters('process', ['isStepCompleted']),
canProceedToNextStep() {
//
switch (this.currentStep) {
case 1:
return this.isStepCompleted('upload')
case 2:
return this.isStepCompleted('preprocess')
case 3:
return this.isStepCompleted('merge')
case 4:
return this.isStepCompleted('correction')
case 5:
return this.isStepCompleted('analysis')
default:
return false
}
}
},
methods: {
...mapActions('process', ['setCurrentStep']),
prevStep() {
if (this.currentStep > 1) {
this.setCurrentStep(this.currentStep - 1)
}
},
nextStep() {
if (this.currentStep < 5 && this.canProceedToNextStep) {
this.setCurrentStep(this.currentStep + 1)
}
},
completeProcess() {
//
this.$router.push('/analysis')
}
}
}
</script>
<style scoped>
.process-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.process-title {
text-align: center;
margin-bottom: 30px;
color: #1890ff;
}
.process-steps {
margin-bottom: 40px;
}
.step-content {
background-color: #f5f7fa;
border-radius: 4px;
padding: 30px;
min-height: 300px;
margin-bottom: 20px;
}
.process-actions {
display: flex;
justify-content: space-between;
padding: 10px 0;
}
</style>
Loading…
Cancel
Save