vue程序中使用Pyodide 详解

1. 完整的vue3组件代码
<template>
<div class="pyodide-demo">
<h2>🐍 Pyodide Python 代码执行器</h2>
<div class="demo-container">
<!-- 加载状态 -->
<div v-if="isLoading" class="loading">
<div class="spinner"></div>
<p>正在加载 Pyodide... 请稍候</p>
</div>
<!-- 主界面 -->
<div v-else class="main-interface">
<!-- 代码编辑器 -->
<div class="editor-section">
<h3>Python 代码编辑器</h3>
<textarea
v-model="pythonCode"
class="code-editor"
placeholder="在这里输入 Python 代码..."
@keydown.ctrl.enter="runCode"
@keydown.meta.enter="runCode"
></textarea>
<div class="editor-toolbar">
<button @click="runCode" :disabled="isRunning" class="run-btn">
{{ isRunning ? '运行中...' : '运行代码 (Ctrl+Enter)' }}
</button>
<button @click="clearOutput" class="clear-btn">清空输出</button>
<button @click="loadExample" class="example-btn">加载示例</button>
</div>
</div>
<!-- 输出区域 -->
<div class="output-section">
<h3>输出结果</h3>
<div class="output-container">
<pre v-if="output" class="output">{{ output }}</pre>
<pre v-if="error" class="error">{{ error }}</pre>
<div v-if="!output && !error" class="placeholder">
运行 Python 代码后,结果将显示在这里
</div>
</div>
</div>
</div>
<!-- 功能说明 -->
<div class="features">
<h3>✨ 功能特性</h3>
<ul>
<li>在浏览器中直接运行 Python 代码</li>
<li>支持 NumPy、Pandas、Matplotlib 等科学计算库</li>
<li>实时代码执行和结果显示</li>
<li>错误处理和调试信息</li>
</ul>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const isLoading = ref(true)
const isRunning = ref(false)
const pythonCode = ref('')
const output = ref('')
const error = ref('')
interface PyodideInterface {
loadPackage: (packages: string[]) => Promise<unknown>
runPython: (code: string) => unknown
}
let pyodide: PyodideInterface | null = null
const initPyodide = async () => {
try {
const script = document.createElement('script')
script.src = 'https://cdn.jsdelivr.net/pyodide/v0.24.1/full/pyodide.js'
document.head.appendChild(script)
await new Promise((resolve) => {
script.onload = resolve
})
pyodide = await window.loadPyodide({
indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.24.1/full/',
})
if (pyodide) {
await pyodide.loadPackage(['numpy', 'matplotlib', 'pandas'])
}
console.log('Pyodide 加载完成')
isLoading.value = false
} catch (err) {
console.error('Pyodide 加载失败:', err)
error.value = `Pyodide 加载失败: ${err}`
isLoading.value = false
}
}
const runCode = async () => {
if (!pyodide || !pythonCode.value.trim()) {
return
}
isRunning.value = true
output.value = ''
error.value = ''
try {
pyodide.runPython(`
import sys
from io import StringIO
sys.stdout = StringIO()
sys.stderr = StringIO()
`)
const result = pyodide.runPython(pythonCode.value)
const stdout = pyodide.runPython('sys.stdout.getvalue()')
const stderr = pyodide.runPython('sys.stderr.getvalue()')
pyodide.runPython(`
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
`)
let outputText = ''
if (stdout) outputText += stdout
if (result !== undefined && result !== null) {
outputText += (outputText ? '\n' : '') + `返回值: ${result}`
}
if (stderr) {
error.value = String(stderr)
}
output.value = outputText || '代码执行完成,无输出'
} catch (err) {
error.value = `执行错误: ${err}`
} finally {
isRunning.value = false
}
}
const clearOutput = () => {
output.value = ''
error.value = ''
}
const loadExample = () => {
pythonCode.value = `# Python 在浏览器中运行示例
import numpy as np
import matplotlib.pyplot as plt
# 创建数据
x = np.linspace(0, 2 * np.pi, 100)
y1 = np.sin(x)
y2 = np.cos(x)
print("NumPy 数组操作:")
print(f"x 数组长度: {len(x)}")
print(f"sin(π/2) = {np.sin(np.pi/2):.2f}")
print(f"cos(π) = {np.cos(np.pi):.2f}")
# Pandas 示例
import pandas as pd
data = {
'x': x[:10],
'sin_x': y1[:10],
'cos_x': y2[:10]
}
df = pd.DataFrame(data)
print("\\nPandas DataFrame (前5行):")
print(df.head())
# 计算统计信息
print(f"\\nsin(x) 的平均值: {df['sin_x'].mean():.3f}")
print(f"cos(x) 的标准差: {df['cos_x'].std():.3f}")
`
}
onMounted(() => {
initPyodide()
loadExample()
})
</script>
<style scoped>
.pyodide-demo {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.demo-container {
margin-top: 20px;
}
.loading {
text-align: center;
padding: 50px;
}
.spinner {
border: 4px solid #f3f3f3;
border-top: 4px solid #42b883;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.main-interface {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
.editor-section,
.output-section {
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
background: #f9f9f9;
}
.editor-section h3,
.output-section h3 {
margin-top: 0;
color: #2c3e50;
}
.code-editor {
width: 100%;
height: 300px;
padding: 15px;
border: 1px solid #ccc;
border-radius: 4px;
font-family: 'Courier New', Monaco, monospace;
font-size: 14px;
resize: vertical;
background: white;
}
.editor-toolbar {
margin-top: 10px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.run-btn,
.clear-btn,
.example-btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.run-btn {
background: #42b883;
color: white;
}
.run-btn:hover:not(:disabled) {
background: #369870;
}
.run-btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.clear-btn {
background: #f56565;
color: white;
}
.clear-btn:hover {
background: #e53e3e;
}
.example-btn {
background: #4299e1;
color: white;
}
.example-btn:hover {
background: #3182ce;
}
.output-container {
height: 300px;
overflow-y: auto;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
}
.output {
padding: 15px;
margin: 0;
font-family: 'Courier New', Monaco, monospace;
font-size: 14px;
white-space: pre-wrap;
color: #2c3e50;
}
.error {
padding: 15px;
margin: 0;
font-family: 'Courier New', Monaco, monospace;
font-size: 14px;
white-space: pre-wrap;
color: #e53e3e;
background: #fed7d7;
}
.placeholder {
padding: 15px;
color: #999;
font-style: italic;
text-align: center;
}
.features {
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
padding: 20px;
}
.features h3 {
margin-top: 0;
color: #0369a1;
}
.features ul {
margin: 10px 0;
padding-left: 20px;
}
.features li {
margin: 8px 0;
color: #374151;
}
@media (max-width: 768px) {
.main-interface {
grid-template-columns: 1fr;
}
.editor-toolbar {
justify-content: center;
}
}
</style>