Qwen3-VL:30B前端开发:Vue.js实现实时交互界面
Qwen3-VL:30B前端开发:Vue.js实现实时交互界面
1. 为什么需要一个能“看懂”图片的前端界面
你有没有遇到过这样的场景:上传一张产品截图,想立刻知道它属于哪个品类、有什么功能特点;或者把会议白板照片发给团队,希望系统自动提炼出待办事项;又或者在设计评审中,快速对比两张UI稿的差异点?这些需求背后,其实都指向同一个技术能力——多模态理解。
Qwen3-VL:30B作为当前性能突出的多模态大模型,不仅能读懂文字,更能理解图像内容。但光有强大的后端能力还不够,真正让技术落地的是那个你每天打开、点击、输入的前端界面。一个好用的前端,应该像一位默契的助手:你随手拖张图进去,它就安静地分析、思考、给出答案,整个过程自然得就像和人对话一样。
这篇文章不讲复杂的模型原理,也不堆砌部署命令,而是聚焦在最实际的问题上:怎么用Vue.js搭建一个真正好用的交互界面?我们会从零开始,一步步实现状态管理、异步加载、图表可视化和响应式适配,让你亲手做出一个能和Qwen3-VL:30B流畅对话的前端应用。不需要你已经是Vue专家,只要会写几行HTML和JavaScript,就能跟着做出来。
2. 环境准备与项目初始化
2.1 创建Vue项目骨架
我们使用Vue官方推荐的Vite工具来快速搭建项目。打开终端,执行以下命令:
# 创建新项目(选择Vue + JavaScript模板)
npm create vite@latest qwen-vue-interface -- --template vue
# 进入项目目录
cd qwen-vue-interface
# 安装依赖
npm install
# 启动开发服务器
npm run dev
启动成功后,浏览器访问 http://localhost:5173,你应该能看到Vue的欢迎页面。这个干净的起点,就是我们接下来要构建交互界面的基础。
2.2 安装必要的依赖库
为了让界面具备完整的交互能力,我们需要几个关键的辅助库:
# 安装Axios用于HTTP请求
npm install axios
# 安装Chart.js用于数据可视化
npm install chart.js vue-chartjs
# 安装Element Plus提供现成UI组件(按钮、表单、卡片等)
npm install element-plus
# 安装FileSaver用于下载生成结果
npm install file-saver
安装完成后,在 src/main.js 中添加Element Plus的全局注册:
import { createApp } from 'vue'
import { ElButton, ElCard, ElInput, ElUpload, ElProgress, ElMessage } from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.component('ElButton', ElButton)
app.component('ElCard', ElCard)
app.component('ElInput', ElInput)
app.component('ElUpload', ElUpload)
app.component('ElProgress', ElProgress)
app.component('ElMessage', ElMessage)
app.mount('#app')
这样我们就有了一个功能完备的开发环境,所有UI组件和工具都已就位,可以开始构建核心功能了。
3. 核心功能模块设计与实现
3.1 多模态交互状态管理
前端界面的核心是状态管理。我们需要跟踪用户上传的图片、正在处理的状态、模型返回的结果,以及可能的错误信息。在Vue中,我们使用组合式API配合ref和reactive来管理这些状态。
创建 src/composables/useQwenInteraction.js 文件:
import { ref, reactive } from 'vue'
import axios from 'axios'
export function useQwenInteraction() {
// 交互状态
const state = reactive({
isProcessing: false,
uploadStatus: 'idle', // idle, uploading, success, error
result: null,
error: null,
progress: 0
})
// 用户输入数据
const input = reactive({
image: null,
textPrompt: '',
analysisType: 'general' // general, product, diagram, document
})
// 清除所有状态
const resetState = () => {
state.isProcessing = false
state.uploadStatus = 'idle'
state.result = null
state.error = null
state.progress = 0
input.image = null
input.textPrompt = ''
}
// 模拟调用Qwen3-VL:30B API(实际项目中替换为真实API地址)
const callQwenApi = async () => {
if (!input.image && !input.textPrompt) {
state.error = '请至少上传一张图片或输入文字描述'
return
}
state.isProcessing = true
state.error = null
state.result = null
try {
// 模拟API调用过程(实际项目中替换为真实请求)
// 这里使用setTimeout模拟网络延迟
await new Promise(resolve => setTimeout(resolve, 2000))
// 模拟不同分析类型的返回结果
const mockResults = {
general: {
description: '这是一张现代简约风格的网页设计稿,主色调为浅灰和蓝色,包含导航栏、轮播图区域和三个功能卡片',
tags: ['网页设计', 'UI界面', '用户体验'],
confidence: 0.92
},
product: {
description: '这是一款无线蓝牙耳机的产品图,展示其充电盒和耳机本体,具有IPX5防水等级和30小时续航能力',
features: ['主动降噪', '空间音频', '触控操作'],
confidence: 0.87
},
diagram: {
description: '这是一个UML类图,展示了用户管理系统的三个核心类:User、Role和Permission,以及它们之间的关联关系',
elements: ['4个类', '3种关系', '2个继承'],
confidence: 0.95
}
}
state.result = {
...mockResults[input.analysisType],
timestamp: new Date().toLocaleString(),
inputImage: input.image ? URL.createObjectURL(input.image) : null,
prompt: input.textPrompt
}
state.uploadStatus = 'success'
} catch (err) {
state.error = err.response?.data?.message || '分析失败,请检查网络连接'
state.uploadStatus = 'error'
} finally {
state.isProcessing = false
}
}
return {
state,
input,
resetState,
callQwenApi
}
}
这个自定义Hook封装了所有与Qwen3-VL:30B交互相关的逻辑,包括状态管理、错误处理和模拟API调用。它遵循Vue的最佳实践,将业务逻辑从组件中抽离出来,让代码更易维护和测试。
3.2 图片上传与预览组件
用户需要一个直观的方式上传图片并看到预览效果。我们创建一个独立的上传组件 src/components/ImageUploader.vue:
<template>
<div class="image-uploader">
<el-upload
class="upload-area"
drag
:auto-upload="false"
:show-file-list="false"
:on-change="handleFileChange"
:on-remove="handleRemove"
:on-error="handleError"
accept="image/*"
>
<i class="el-icon-upload"></i>
<div class="el-upload__text">
<em>点击上传</em> 或拖拽图片到此区域
</div>
<div class="el-upload__tip" slot="tip">
支持 JPG/PNG/GIF 格式,建议尺寸不超过 2000x2000 像素
</div>
</el-upload>
<!-- 预览区域 -->
<div v-if="previewUrl" class="preview-container">
<h3>上传预览</h3>
<div class="preview-image">
<img :src="previewUrl" :alt="fileName" />
</div>
<div class="preview-info">
<p><strong>文件名:</strong>{{ fileName }}</p>
<p><strong>大小:</strong>{{ formatFileSize(fileSize) }}</p>
<el-button type="danger" size="small" @click="removeImage">
移除图片
</el-button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, defineEmits, defineProps } from 'vue'
const props = defineProps({
modelValue: {
type: File,
default: null
}
})
const emit = defineEmits(['update:modelValue', 'file-change'])
const previewUrl = ref('')
const fileName = ref('')
const fileSize = ref(0)
const handleFileChange = (file) => {
if (file.raw) {
const reader = new FileReader()
reader.onload = (e) => {
previewUrl.value = e.target.result
fileName.value = file.name
fileSize.value = file.size
emit('update:modelValue', file.raw)
emit('file-change', file.raw)
}
reader.readAsDataURL(file.raw)
}
}
const handleRemove = () => {
previewUrl.value = ''
fileName.value = ''
fileSize.value = 0
emit('update:modelValue', null)
}
const handleError = (err) => {
console.error('上传错误:', err)
}
const removeImage = () => {
previewUrl.value = ''
fileName.value = ''
fileSize.value = 0
emit('update:modelValue', null)
}
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
</script>
<style scoped>
.image-uploader {
margin-bottom: 24px;
}
.upload-area {
border: 2px dashed #d9d9d9;
border-radius: 8px;
padding: 40px 20px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
}
.upload-area:hover {
border-color: #409eff;
background-color: #f5f7fa;
}
.preview-container {
margin-top: 24px;
padding: 16px;
background-color: #f9f9f9;
border-radius: 8px;
}
.preview-image {
text-align: center;
margin-bottom: 16px;
}
.preview-image img {
max-width: 100%;
max-height: 300px;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.preview-info p {
margin: 8px 0;
color: #606266;
}
</style>
这个组件提供了完整的图片上传体验:支持拖拽、文件选择、实时预览、文件信息显示和移除功能。它使用Vue 3的组合式API编写,通过props和emits与父组件通信,符合Vue的最佳实践。
3.3 实时分析结果展示与交互
当Qwen3-VL:30B返回分析结果后,我们需要一个清晰、直观的界面来展示这些信息。创建 src/components/AnalysisResult.vue 组件:
<template>
<div class="analysis-result" v-if="result">
<el-card class="result-card">
<template #header>
<div class="card-header">
<h2>分析结果</h2>
<span class="timestamp">{{ result.timestamp }}</span>
</div>
</template>
<!-- 输入图片预览 -->
<div v-if="result.inputImage" class="input-preview">
<h3>您上传的图片</h3>
<div class="image-wrapper">
<img :src="result.inputImage" :alt="result.prompt || '分析输入'" />
</div>
</div>
<!-- 文字描述 -->
<div class="description-section">
<h3>内容描述</h3>
<p class="description-text">{{ result.description }}</p>
</div>
<!-- 标签云 -->
<div v-if="result.tags" class="tags-section">
<h3>识别标签</h3>
<div class="tag-cloud">
<span
v-for="(tag, index) in result.tags"
:key="index"
class="tag-item"
>
{{ tag }}
</span>
</div>
</div>
<!-- 特征列表 -->
<div v-if="result.features" class="features-section">
<h3>关键特征</h3>
<ul class="feature-list">
<li v-for="(feature, index) in result.features" :key="index">
{{ feature }}
</li>
</ul>
</div>
<!-- 置信度指示器 -->
<div class="confidence-section">
<h3>分析置信度</h3>
<div class="confidence-bar">
<el-progress
:percentage="Math.round(result.confidence * 100)"
:stroke-width="20"
:color="getConfidenceColor(result.confidence)"
/>
<span class="confidence-value">{{ Math.round(result.confidence * 100) }}%</span>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<el-button type="primary" @click="copyToClipboard">
复制结果
</el-button>
<el-button @click="downloadResult">
下载报告
</el-button>
</div>
</el-card>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
import { ElMessage } from 'element-plus'
import { saveAs } from 'file-saver'
const props = defineProps({
result: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['copy', 'download'])
const getConfidenceColor = (confidence) => {
if (confidence >= 0.9) return '#67c23a' // 绿色
if (confidence >= 0.7) return '#e6a23c' // 橙色
return '#f56c6c' // 红色
}
const copyToClipboard = () => {
const text = `【Qwen3-VL分析结果】\n\n${props.result.description}\n\n标签:${props.result.tags?.join('、') || '无'}\n置信度:${Math.round(props.result.confidence * 100)}%`
navigator.clipboard.writeText(text)
ElMessage.success('结果已复制到剪贴板')
}
const downloadResult = () => {
const content = `Qwen3-VL分析报告\n\n时间:${props.result.timestamp}\n\n内容描述:${props.result.description}\n\n识别标签:${props.result.tags?.join('、') || '无'}\n\n置信度:${Math.round(props.result.confidence * 100)}%\n\n---\n由Qwen3-VL:30B多模态大模型生成`
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' })
saveAs(blob, `qwen-analysis-${new Date().toISOString().slice(0,10)}.txt`)
}
</script>
<style scoped>
.analysis-result {
margin-top: 24px;
}
.result-card {
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.timestamp {
font-size: 14px;
color: #909399;
}
.input-preview {
margin-bottom: 24px;
}
.input-preview h3 {
margin-bottom: 12px;
color: #303133;
}
.image-wrapper {
text-align: center;
}
.image-wrapper img {
max-width: 100%;
max-height: 400px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.description-section {
margin-bottom: 24px;
}
.description-section h3 {
margin-bottom: 12px;
color: #303133;
}
.description-text {
line-height: 1.6;
color: #606266;
font-size: 16px;
}
.tags-section {
margin-bottom: 24px;
}
.tags-section h3 {
margin-bottom: 12px;
color: #303133;
}
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.tag-item {
background-color: #ecf5ff;
color: #409eff;
padding: 6px 12px;
border-radius: 20px;
font-size: 14px;
border: 1px solid #d9ecff;
}
.features-section {
margin-bottom: 24px;
}
.features-section h3 {
margin-bottom: 12px;
color: #303133;
}
.feature-list {
list-style: none;
padding: 0;
}
.feature-list li {
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
color: #606266;
}
.confidence-section {
margin-bottom: 24px;
}
.confidence-section h3 {
margin-bottom: 12px;
color: #303133;
}
.confidence-bar {
display: flex;
align-items: center;
gap: 12px;
}
.confidence-value {
font-weight: bold;
font-size: 16px;
min-width: 50px;
text-align: right;
}
.action-buttons {
display: flex;
gap: 12px;
justify-content: center;
}
@media (max-width: 768px) {
.card-header {
flex-direction: column;
gap: 8px;
}
.action-buttons {
flex-direction: column;
}
}
</style>
这个组件以卡片形式展示分析结果,包含了图片预览、文字描述、标签云、特征列表、置信度指示器和操作按钮。样式上采用了响应式设计,确保在移动设备上也能良好显示。
4. 可视化图表集成与数据呈现
4.1 分析结果统计图表
除了展示单次分析结果,我们还可以为用户提供历史分析的统计视图,帮助他们了解自己的使用习惯和模型表现。我们使用Chart.js来创建一个简单的饼图,展示不同分析类型的分布情况。
创建 src/components/AnalysisStats.vue 组件:
<template>
<div class="stats-container">
<h2>使用统计</h2>
<div class="chart-container">
<canvas ref="chartCanvas"></canvas>
</div>
<div class="stats-summary">
<div class="stat-item">
<h3>总分析次数</h3>
<p class="stat-value">{{ totalAnalyses }}</p>
</div>
<div class="stat-item">
<h3>平均置信度</h3>
<p class="stat-value">{{ averageConfidence }}%</p>
</div>
<div class="stat-item">
<h3>最常用类型</h3>
<p class="stat-value">{{ mostCommonType }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { Chart, registerables } from 'chart.js'
// 注册所有Chart.js组件
Chart.register(...registerables)
const props = defineProps({
analyses: {
type: Array,
default: () => []
}
})
const chartCanvas = ref(null)
let chartInstance = null
const totalAnalyses = computed(() => props.analyses.length)
const averageConfidence = computed(() => {
if (props.analyses.length === 0) return 0
const sum = props.analyses.reduce((acc, item) => acc + (item.confidence || 0), 0)
return Math.round((sum / props.analyses.length) * 100)
})
const mostCommonType = computed(() => {
if (props.analyses.length === 0) return '暂无数据'
const typeCount = {}
props.analyses.forEach(item => {
const type = item.analysisType || 'general'
typeCount[type] = (typeCount[type] || 0) + 1
})
return Object.keys(typeCount).reduce((a, b) =>
typeCount[a] > typeCount[b] ? a : b
)
})
onMounted(() => {
initChart()
})
watch(() => props.analyses, () => {
if (chartInstance) {
chartInstance.destroy()
}
initChart()
})
const initChart = () => {
if (!chartCanvas.value) return
// 计算各类型分析次数
const typeCount = {}
props.analyses.forEach(item => {
const type = item.analysisType || 'general'
typeCount[type] = (typeCount[type] || 0) + 1
})
const labels = Object.keys(typeCount)
const data = Object.values(typeCount)
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c']
chartInstance = new Chart(chartCanvas.value, {
type: 'pie',
data: {
labels: labels.map(type => {
const typeMap = {
'general': '通用分析',
'product': '产品识别',
'diagram': '图表解析',
'document': '文档理解'
}
return typeMap[type] || type
}),
datasets: [{
data: data,
backgroundColor: colors.slice(0, labels.length),
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: {
padding: 20,
usePointStyle: true,
pointStyle: 'circle'
}
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || ''
const value = context.parsed || 0
const total = context.dataset.data.reduce((a, b) => a + b, 0)
const percentage = Math.round((value / total) * 100)
return `${label}: ${value}次 (${percentage}%)`
}
}
}
}
}
})
}
</script>
<style scoped>
.stats-container {
margin-top: 32px;
padding: 24px;
background-color: #f9f9f9;
border-radius: 12px;
}
.stats-container h2 {
margin-bottom: 24px;
color: #303133;
text-align: center;
}
.chart-container {
height: 300px;
margin-bottom: 24px;
}
.stats-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
.stat-item {
text-align: center;
padding: 16px;
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.stat-item h3 {
margin-bottom: 8px;
color: #606266;
font-size: 14px;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #409eff;
}
@media (max-width: 768px) {
.stats-summary {
grid-template-columns: 1fr;
}
}
</style>
这个组件展示了用户历史分析的统计信息,包括饼图和关键指标摘要。它使用了Vue的响应式特性,当分析历史数据变化时,图表会自动更新。
4.2 实时处理进度可视化
在等待Qwen3-VL:30B处理图片时,用户需要明确的反馈来了解当前进度。我们创建一个进度指示器组件 src/components/ProcessingIndicator.vue:
<template>
<div class="processing-indicator" v-if="isVisible">
<div class="indicator-content">
<div class="spinner"></div>
<h3>正在分析中...</h3>
<p class="status-text">{{ statusText }}</p>
<el-progress
v-if="showProgress"
:percentage="progress"
:stroke-width="12"
:color="progressColor"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
const props = defineProps({
isVisible: {
type: Boolean,
default: false
},
progress: {
type: Number,
default: 0
},
statusText: {
type: String,
default: '正在理解图片内容...'
}
})
const showProgress = computed(() => props.progress > 0 && props.progress < 100)
const progressColor = computed(() => {
if (props.progress < 30) return '#e6a23c'
if (props.progress < 70) return '#409eff'
return '#67c23a'
})
// 模拟进度动画(实际项目中由API返回真实进度)
watch(() => props.isVisible, (newVal) => {
if (newVal) {
// 重置进度条
// 在实际项目中,这里会监听WebSocket或Server-Sent Events
}
})
</script>
<style scoped>
.processing-indicator {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0,0,0,0.5);
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
}
.indicator-content {
background-color: white;
padding: 32px;
border-radius: 12px;
text-align: center;
max-width: 400px;
width: 90%;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid #f0f0f0;
border-top: 4px solid #409eff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 24px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.indicator-content h3 {
margin-bottom: 12px;
color: #303133;
}
.status-text {
color: #606266;
margin-bottom: 24px;
line-height: 1.5;
}
@media (max-width: 480px) {
.indicator-content {
padding: 24px;
}
}
</style>
这个组件提供了一个全屏的处理指示器,包含旋转动画和进度条,让用户清楚地知道系统正在工作,并且可以大致估计还需要等待多久。
5. 响应式设计与用户体验优化
5.1 移动端适配策略
现代Web应用必须在各种设备上都能良好运行。我们的界面需要在手机、平板和桌面设备上都有出色的体验。我们采用移动优先的设计策略,在 src/assets/styles/main.css 中添加响应式样式:
/* 基础重置和移动端样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #303133;
background-color: #f5f7fa;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
/* 移动端断点 */
@media (max-width: 768px) {
.container {
padding: 0 16px;
}
.main-content {
padding: 16px;
}
.analysis-section {
margin-top: 16px;
}
.control-panel {
display: flex;
flex-direction: column;
gap: 16px;
}
.upload-section {
margin-bottom: 16px;
}
.result-section {
margin-top: 16px;
}
.stats-container {
padding: 16px;
}
}
/* 平板断点 */
@media (min-width: 769px) and (max-width: 1024px) {
.container {
max-width: 960px;
}
.main-grid {
display: grid;
grid-template-columns: 1fr;
gap: 24px;
}
.control-panel {
grid-column: 1;
}
.result-section {
grid-column: 1;
}
}
/* 桌面断点 */
@media (min-width: 1025px) {
.main-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
}
.control-panel {
grid-column: 1;
}
.result-section {
grid-column: 2;
}
.stats-container {
grid-column: 1 / -1;
}
}
这些CSS规则确保了我们的应用在不同屏幕尺寸下都能有良好的布局和间距。我们使用了现代的CSS Grid和Flexbox布局,避免了过时的浮动布局。
5.2 交互细节优化
好的用户体验往往体现在细节上。我们在 src/App.vue 中添加一些交互优化:
<template>
<div id="app">
<header class="app-header">
<div class="container">
<h1>Qwen3-VL多模态分析平台</h1>
<p class="subtitle">用Vue.js构建的实时交互界面</p>
</div>
</header>
<main class="main-content">
<div class="container">
<div class="main-grid">
<!-- 控制面板 -->
<div class="control-panel">
<el-card class="control-card">
<template #header>
<h2>分析设置</h2>
</template>
<div class="upload-section">
<h3>上传图片</h3>
<ImageUploader
v-model="input.image"
@file-change="onFileChange"
/>
</div>
<div class="prompt-section">
<h3>补充说明(可选)</h3>
<el-input
v-model="input.textPrompt"
type="textarea"
:rows="3"
placeholder="例如:请重点分析这张图中的产品功能特点..."
maxlength="200"
show-word-limit
/>
</div>
<div class="type-section">
<h3>分析类型</h3>
<el-radio-group v-model="input.analysisType">
<el-radio label="general">通用分析</el-radio>
<el-radio label="product">产品识别</el-radio>
<el-radio label="diagram">图表解析</el-radio>
<el-radio label="document">文档理解</el-radio>
</el-radio-group>
</div>
<div class="action-section">
<el-button
type="primary"
size="large"
:loading="state.isProcessing"
@click="handleAnalyze"
style="width: 100%; margin-top: 16px;"
>
{{ state.isProcessing ? '分析中...' : '开始分析' }}
</el-button>
<el-button
@click="resetAll"
style="width: 100%; margin-top: 12px;"
>
重置所有
</el-button>
</div>
</el-card>
</div>
<!-- 结果展示区 -->
<div class="result-section">
<div v-if="!state.result && !state.error">
<el-card class="placeholder-card">
<template #header>
<h2>分析结果</h2>
</template>
<div class="placeholder-content">
<i class="el-icon-picture"></i>
<h3>等待您的分析请求</h3>
<p>上传一张图片,选择分析类型,然后点击"开始分析"按钮</p>
</div>
</el-card>
</div>
<AnalysisResult
v-else-if="state.result"
:result="state.result"
@copy="handleCopy"
@download="handleDownload"
/>
<div v-else-if="state.error" class="error-section">
<el-alert
:title="state.error"
type="error"
show-icon
:closable="false"
/>
<el-button @click="resetAll" style="margin-top: 16px;">
重新尝试
</el-button>
</div>
</div>
</div>
<!-- 统计图表 -->
<AnalysisStats :analyses="analysisHistory" />
</div>
</main>
<ProcessingIndicator
:is-visible="state.isProcessing"
:progress="state.progress"
:status-text="getProcessingStatus()"
/>
<footer class="app-footer">
<div class="container">
<p>© {{ new Date().getFullYear() }} Qwen3-VL Vue前端界面示例 | 基于Vue 3和Element Plus构建</p>
</div>
</footer>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { ElMessage } from 'element-plus'
import { useQwenInteraction } from './composables/useQwenInteraction'
import ImageUploader from './components/ImageUploader.vue'
import AnalysisResult from './components/AnalysisResult.vue'
import AnalysisStats from './components/AnalysisStats.vue'
import ProcessingIndicator from './components/ProcessingIndicator.vue'
const { state, input, resetState, callQwenApi } = useQwenInteraction()
// 模拟历史分析记录(实际项目中从localStorage或API获取)
const analysisHistory = ref([
{ analysisType: 'general', confidence: 0.92, timestamp: '2024-更多推荐

所有评论(0)