mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-12 11:32:36 +08:00
增强模块联邦支持,添加动态导入远程模块的声明,更新示例项目以展示新组件结构和配置,调整 Vite 配置以支持更灵活的远程组件加载。
This commit is contained in:
@@ -1,93 +1,128 @@
|
||||
<template>
|
||||
<div class="plugin-component pa-4">
|
||||
<v-card>
|
||||
<v-card-title>{{ title }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-progress-circular v-if="loading" indeterminate color="primary"></v-progress-circular>
|
||||
<div v-else>
|
||||
<v-alert v-if="error" type="error" class="mb-4">{{ error }}</v-alert>
|
||||
<v-simple-table v-if="data && data.stats">
|
||||
<template v-slot:default>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>类型</th>
|
||||
<th>数量</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(value, key) in data.stats" :key="key">
|
||||
<td>{{ key }}</td>
|
||||
<td>{{ value }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</v-simple-table>
|
||||
<div v-if="data">
|
||||
<div><strong>状态:</strong> {{ data.status }}</div>
|
||||
<div><strong>最后更新:</strong> {{ data.last_updated }}</div>
|
||||
</div>
|
||||
<div v-else>无数据</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" @click="refreshData"> 刷新 </v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<div class="app-container">
|
||||
<v-app>
|
||||
<v-app-bar color="primary" app>
|
||||
<v-app-bar-title>MoviePilot插件组件示例</v-app-bar-title>
|
||||
</v-app-bar>
|
||||
|
||||
<v-main>
|
||||
<v-container>
|
||||
<v-tabs v-model="activeTab" bg-color="primary">
|
||||
<v-tab value="page">详情页面</v-tab>
|
||||
<v-tab value="config">配置页面</v-tab>
|
||||
<v-tab value="dashboard">仪表板</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-window v-model="activeTab" class="mt-4">
|
||||
<v-window-item value="page">
|
||||
<h2 class="text-h5 mb-4">Page组件</h2>
|
||||
<div class="component-preview">
|
||||
<page-component @action="handleAction"></page-component>
|
||||
</div>
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="config">
|
||||
<h2 class="text-h5 mb-4">Config组件</h2>
|
||||
<div class="component-preview">
|
||||
<config-component :initial-config="initialConfig" @save="handleConfigSave"></config-component>
|
||||
</div>
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="dashboard">
|
||||
<h2 class="text-h5 mb-4">Dashboard组件</h2>
|
||||
<v-switch v-model="dashboardConfig.attrs.border" label="显示边框" color="primary" class="mb-4"></v-switch>
|
||||
<div class="component-preview">
|
||||
<dashboard-component :config="dashboardConfig" :allow-refresh="true"></dashboard-component>
|
||||
</div>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-container>
|
||||
</v-main>
|
||||
|
||||
<v-footer app color="primary" class="text-center d-flex justify-center">
|
||||
<span class="text-white">MoviePilot 模块联邦示例 ©{{ new Date().getFullYear() }}</span>
|
||||
</v-footer>
|
||||
</v-app>
|
||||
|
||||
<!-- 通知弹窗 -->
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="snackbar.timeout">
|
||||
{{ snackbar.text }}
|
||||
<template v-slot:actions>
|
||||
<v-btn variant="text" @click="snackbar.show = false"> 关闭 </v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
import PageComponent from './components/Page.vue'
|
||||
import ConfigComponent from './components/Config.vue'
|
||||
import DashboardComponent from './components/Dashboard.vue'
|
||||
|
||||
// 组件状态
|
||||
const title = ref('插件数据示例')
|
||||
const loading = ref(true)
|
||||
const data = ref(null)
|
||||
const error = ref(null)
|
||||
// 活动标签页
|
||||
const activeTab = ref('page')
|
||||
|
||||
// 向主应用发送事件
|
||||
const emit = defineEmits(['action'])
|
||||
|
||||
// 获取和刷新数据
|
||||
async function refreshData() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// 模拟API调用 - 实际开发中应使用 fetch 调用真实API
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 模拟数据
|
||||
data.value = {
|
||||
status: 'running',
|
||||
stats: {
|
||||
'电影': Math.floor(Math.random() * 100) + 50,
|
||||
'电视剧': Math.floor(Math.random() * 100) + 30,
|
||||
'动漫': Math.floor(Math.random() * 100) + 20,
|
||||
'纪录片': Math.floor(Math.random() * 100) + 10,
|
||||
'综艺': Math.floor(Math.random() * 100) + 5,
|
||||
},
|
||||
last_updated: new Date().toLocaleString(),
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('获取数据失败:', err)
|
||||
error.value = err.message || '获取数据失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
// 通知主应用组件已更新
|
||||
emit('action')
|
||||
}
|
||||
// 配置初始值
|
||||
const initialConfig = {
|
||||
name: '测试插件',
|
||||
description: '这是一个测试配置',
|
||||
enable_notifications: true,
|
||||
update_interval: 30,
|
||||
api_url: 'https://api.example.com',
|
||||
api_key: 'test_api_key_123',
|
||||
concurrent_tasks: 2,
|
||||
tags: ['电影', '测试'],
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
refreshData()
|
||||
// 仪表板配置
|
||||
const dashboardConfig = reactive({
|
||||
id: 'test_plugin',
|
||||
name: '测试插件',
|
||||
attrs: {
|
||||
title: '仪表板示例',
|
||||
subtitle: '插件数据展示',
|
||||
border: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 通知状态
|
||||
const snackbar = reactive({
|
||||
show: false,
|
||||
text: '',
|
||||
color: 'success',
|
||||
timeout: 3000,
|
||||
})
|
||||
|
||||
// 显示通知
|
||||
function showNotification(text, color = 'success') {
|
||||
snackbar.text = text
|
||||
snackbar.color = color
|
||||
snackbar.show = true
|
||||
}
|
||||
|
||||
// 处理详情页面操作
|
||||
function handleAction() {
|
||||
showNotification('Page组件触发了action事件')
|
||||
}
|
||||
|
||||
// 处理配置保存
|
||||
function handleConfigSave(config) {
|
||||
console.log('配置已保存:', config)
|
||||
showNotification('配置已保存')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.plugin-component {
|
||||
width: 100%;
|
||||
<style>
|
||||
/* 为了使测试应用更美观 */
|
||||
.app-container {
|
||||
block-size: 100vh;
|
||||
inline-size: 100vw;
|
||||
}
|
||||
|
||||
.component-preview {
|
||||
overflow: hidden;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
208
examples/plugin-component/src/components/Config.vue
Normal file
208
examples/plugin-component/src/components/Config.vue
Normal file
@@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div class="plugin-config">
|
||||
<v-card>
|
||||
<v-card-title>插件配置</v-card-title>
|
||||
<v-card-text>
|
||||
<v-alert v-if="error" type="error" class="mb-4">{{ error }}</v-alert>
|
||||
|
||||
<v-form ref="form" v-model="isFormValid" @submit.prevent="saveConfig">
|
||||
<!-- 基本设置区域 -->
|
||||
<div class="text-subtitle-1 font-weight-bold mt-4 mb-2">基本设置</div>
|
||||
|
||||
<v-text-field
|
||||
v-model="config.name"
|
||||
label="插件名称"
|
||||
variant="outlined"
|
||||
:rules="[v => !!v || '名称不能为空']"
|
||||
hint="显示在插件列表中的名称"
|
||||
></v-text-field>
|
||||
|
||||
<v-textarea
|
||||
v-model="config.description"
|
||||
label="插件描述"
|
||||
variant="outlined"
|
||||
rows="3"
|
||||
hint="简要说明插件的功能和用途"
|
||||
></v-textarea>
|
||||
|
||||
<!-- 功能配置区域 -->
|
||||
<div class="text-subtitle-1 font-weight-bold mt-4 mb-2">功能配置</div>
|
||||
|
||||
<v-switch
|
||||
v-model="config.enable_notifications"
|
||||
label="启用通知"
|
||||
color="primary"
|
||||
inset
|
||||
hint="接收插件状态变更通知"
|
||||
persistent-hint
|
||||
></v-switch>
|
||||
|
||||
<v-select
|
||||
v-model="config.update_interval"
|
||||
label="更新频率"
|
||||
:items="updateIntervalOptions"
|
||||
variant="outlined"
|
||||
item-title="text"
|
||||
item-value="value"
|
||||
></v-select>
|
||||
|
||||
<!-- API配置区域 -->
|
||||
<div class="text-subtitle-1 font-weight-bold mt-4 mb-2">API设置</div>
|
||||
|
||||
<v-text-field
|
||||
v-model="config.api_url"
|
||||
label="API地址"
|
||||
variant="outlined"
|
||||
hint="外部服务API地址"
|
||||
:rules="[v => !v || v.startsWith('http') || '请输入有效的URL']"
|
||||
></v-text-field>
|
||||
|
||||
<v-text-field
|
||||
v-model="config.api_key"
|
||||
label="API密钥"
|
||||
variant="outlined"
|
||||
:append-inner-icon="showApiKey ? 'mdi-eye-off' : 'mdi-eye'"
|
||||
:type="showApiKey ? 'text' : 'password'"
|
||||
@click:append-inner="showApiKey = !showApiKey"
|
||||
></v-text-field>
|
||||
|
||||
<!-- 高级选项区域 -->
|
||||
<div class="text-subtitle-1 font-weight-bold mt-4 mb-2">高级选项</div>
|
||||
|
||||
<v-expansion-panels variant="accordion">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title>高级选项</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<v-slider
|
||||
v-model="config.concurrent_tasks"
|
||||
label="并发任务数"
|
||||
min="1"
|
||||
max="10"
|
||||
step="1"
|
||||
thumb-label
|
||||
></v-slider>
|
||||
|
||||
<v-combobox
|
||||
v-model="config.tags"
|
||||
label="标签"
|
||||
variant="outlined"
|
||||
chips
|
||||
multiple
|
||||
closable-chips
|
||||
></v-combobox>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="secondary" variant="outlined" @click="resetForm">重置</v-btn>
|
||||
<v-btn color="primary" :disabled="!isFormValid" @click="saveConfig" :loading="saving">保存配置</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
|
||||
// 接收初始配置
|
||||
const props = defineProps({
|
||||
initialConfig: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
})
|
||||
|
||||
// 表单状态
|
||||
const form = ref(null)
|
||||
const isFormValid = ref(true)
|
||||
const error = ref(null)
|
||||
const saving = ref(false)
|
||||
const showApiKey = ref(false)
|
||||
|
||||
// 更新频率选项
|
||||
const updateIntervalOptions = [
|
||||
{ text: '5分钟', value: 5 },
|
||||
{ text: '15分钟', value: 15 },
|
||||
{ text: '30分钟', value: 30 },
|
||||
{ text: '1小时', value: 60 },
|
||||
{ text: '2小时', value: 120 },
|
||||
{ text: '6小时', value: 360 },
|
||||
{ text: '12小时', value: 720 },
|
||||
{ text: '1天', value: 1440 },
|
||||
]
|
||||
|
||||
// 配置数据,使用默认值和初始配置合并
|
||||
const defaultConfig = {
|
||||
name: '我的插件',
|
||||
description: '',
|
||||
enable_notifications: true,
|
||||
update_interval: 60,
|
||||
api_url: '',
|
||||
api_key: '',
|
||||
concurrent_tasks: 3,
|
||||
tags: [],
|
||||
}
|
||||
|
||||
// 合并默认配置和初始配置
|
||||
const config = reactive({ ...defaultConfig })
|
||||
|
||||
// 初始化配置
|
||||
onMounted(() => {
|
||||
// 加载初始配置
|
||||
if (props.initialConfig) {
|
||||
Object.keys(props.initialConfig).forEach(key => {
|
||||
if (key in config) {
|
||||
config[key] = props.initialConfig[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 自定义事件,用于保存配置
|
||||
const emit = defineEmits(['save'])
|
||||
|
||||
// 保存配置
|
||||
async function saveConfig() {
|
||||
if (!isFormValid.value) {
|
||||
error.value = '请修正表单错误'
|
||||
return
|
||||
}
|
||||
|
||||
saving.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// 模拟API调用等待
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 发送保存事件
|
||||
emit('save', { ...config })
|
||||
} catch (err) {
|
||||
console.error('保存配置失败:', err)
|
||||
error.value = err.message || '保存配置失败'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
Object.keys(defaultConfig).forEach(key => {
|
||||
config[key] = defaultConfig[key]
|
||||
})
|
||||
|
||||
if (form.value) {
|
||||
form.value.resetValidation()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.plugin-config {
|
||||
padding: 16px;
|
||||
inline-size: 100%;
|
||||
}
|
||||
</style>
|
||||
314
examples/plugin-component/src/components/Dashboard.vue
Normal file
314
examples/plugin-component/src/components/Dashboard.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<div class="dashboard-widget">
|
||||
<v-card v-if="!config?.attrs?.border" flat>
|
||||
<v-card-text class="pa-0">
|
||||
<div class="dashboard-content">
|
||||
<!-- 加载中状态 -->
|
||||
<div v-if="loading" class="d-flex justify-center align-center py-4">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
</div>
|
||||
|
||||
<!-- 数据内容 -->
|
||||
<div v-else>
|
||||
<!-- 数据图表 -->
|
||||
<div v-if="chartData" class="chart-container">
|
||||
<v-chart class="chart" :option="chartOptions" autoresize />
|
||||
</div>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<v-list v-if="items.length" density="compact" class="py-0">
|
||||
<v-list-item v-for="(item, index) in items" :key="index" :title="item.title" :subtitle="item.subtitle">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar :color="getStatusColor(item.status)" size="small">
|
||||
<v-icon size="small" color="white">{{ getStatusIcon(item.status) }}</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<template v-slot:append v-if="item.value">
|
||||
<span class="text-caption">{{ item.value }}</span>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- 带边框的卡片 -->
|
||||
<v-card v-else>
|
||||
<v-card-item>
|
||||
<v-card-title>{{ config?.attrs?.title || '仪表板组件' }}</v-card-title>
|
||||
<v-card-subtitle v-if="config?.attrs?.subtitle">{{ config.attrs.subtitle }}</v-card-subtitle>
|
||||
</v-card-item>
|
||||
|
||||
<v-card-text>
|
||||
<!-- 加载中状态 -->
|
||||
<div v-if="loading" class="d-flex justify-center align-center py-4">
|
||||
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||
</div>
|
||||
|
||||
<!-- 数据内容 -->
|
||||
<div v-else>
|
||||
<!-- 数据图表 -->
|
||||
<div v-if="chartData" class="chart-container">
|
||||
<v-chart class="chart" :option="chartOptions" autoresize />
|
||||
</div>
|
||||
|
||||
<!-- 数据列表 -->
|
||||
<v-list v-if="items.length" density="compact" class="rounded pa-0">
|
||||
<v-list-item v-for="(item, index) in items" :key="index" :title="item.title" :subtitle="item.subtitle">
|
||||
<template v-slot:prepend>
|
||||
<v-avatar :color="getStatusColor(item.status)" size="small">
|
||||
<v-icon size="small" color="white">{{ getStatusIcon(item.status) }}</v-icon>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<template v-slot:append v-if="item.value">
|
||||
<span class="text-caption">{{ item.value }}</span>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import VChart from 'vue-echarts'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { LineChart, PieChart } from 'echarts/charts'
|
||||
import { GridComponent, TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components'
|
||||
|
||||
// 注册ECharts组件
|
||||
try {
|
||||
use([CanvasRenderer, LineChart, PieChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent])
|
||||
} catch (e) {
|
||||
console.warn('ECharts components registration failed', e)
|
||||
}
|
||||
|
||||
// 接收仪表板配置
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
allowRefresh: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
})
|
||||
|
||||
// 组件状态
|
||||
const loading = ref(true)
|
||||
const items = ref([])
|
||||
const chartData = ref(null)
|
||||
let refreshTimer = null
|
||||
|
||||
// 获取状态图标
|
||||
function getStatusIcon(status) {
|
||||
const icons = {
|
||||
'success': 'mdi-check-circle',
|
||||
'warning': 'mdi-alert',
|
||||
'error': 'mdi-alert-circle',
|
||||
'info': 'mdi-information',
|
||||
'running': 'mdi-play-circle',
|
||||
'pending': 'mdi-clock-outline',
|
||||
'completed': 'mdi-check-circle-outline',
|
||||
}
|
||||
return icons[status] || 'mdi-help-circle'
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
function getStatusColor(status) {
|
||||
const colors = {
|
||||
'success': 'success',
|
||||
'warning': 'warning',
|
||||
'error': 'error',
|
||||
'info': 'info',
|
||||
'running': 'primary',
|
||||
'pending': 'secondary',
|
||||
'completed': 'success',
|
||||
}
|
||||
return colors[status] || 'grey'
|
||||
}
|
||||
|
||||
// 图表选项
|
||||
const chartOptions = computed(() => {
|
||||
if (!chartData.value) return {}
|
||||
|
||||
const { type, data } = chartData.value
|
||||
|
||||
if (type === 'line') {
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: data.xAxis,
|
||||
axisLabel: {
|
||||
color: '#888',
|
||||
},
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: {
|
||||
color: '#888',
|
||||
},
|
||||
},
|
||||
series: data.series.map(series => ({
|
||||
name: series.name,
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
data: series.data,
|
||||
areaStyle: { opacity: 0.1 },
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'pie') {
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)',
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: data.name,
|
||||
type: 'pie',
|
||||
radius: ['40%', '70%'],
|
||||
avoidLabelOverlap: false,
|
||||
itemStyle: {
|
||||
borderRadius: 10,
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: '12',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: data.items,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
return {}
|
||||
})
|
||||
|
||||
// 获取仪表板数据
|
||||
async function fetchDashboardData() {
|
||||
if (!props.allowRefresh) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 随机决定显示饼图或折线图
|
||||
const showPie = Math.random() > 0.5
|
||||
|
||||
if (showPie) {
|
||||
// 饼图数据
|
||||
chartData.value = {
|
||||
type: 'pie',
|
||||
data: {
|
||||
name: '文件分布',
|
||||
items: [
|
||||
{ value: Math.floor(Math.random() * 50) + 30, name: '电影' },
|
||||
{ value: Math.floor(Math.random() * 40) + 20, name: '电视剧' },
|
||||
{ value: Math.floor(Math.random() * 30) + 10, name: '动漫' },
|
||||
{ value: Math.floor(Math.random() * 20) + 5, name: '纪录片' },
|
||||
],
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// 折线图数据
|
||||
const days = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
|
||||
chartData.value = {
|
||||
type: 'line',
|
||||
data: {
|
||||
xAxis: days,
|
||||
series: [
|
||||
{
|
||||
name: '下载量',
|
||||
data: days.map(() => Math.floor(Math.random() * 10) + 1),
|
||||
},
|
||||
{
|
||||
name: '完成量',
|
||||
data: days.map(() => Math.floor(Math.random() * 8) + 1),
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 生成列表数据
|
||||
const statuses = ['success', 'warning', 'error', 'info', 'running', 'pending', 'completed']
|
||||
items.value = Array.from({ length: 5 }, (_, i) => {
|
||||
const status = statuses[Math.floor(Math.random() * statuses.length)]
|
||||
return {
|
||||
title: `项目 ${i + 1}`,
|
||||
subtitle: `上次更新: ${new Date().toLocaleTimeString()}`,
|
||||
status,
|
||||
value: Math.floor(Math.random() * 100) + '%',
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('获取仪表板数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 设置定时刷新
|
||||
function setupRefreshTimer() {
|
||||
if (props.allowRefresh) {
|
||||
// 每30秒刷新一次
|
||||
refreshTimer = setInterval(() => {
|
||||
fetchDashboardData()
|
||||
}, 30000)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
fetchDashboardData()
|
||||
setupRefreshTimer()
|
||||
})
|
||||
|
||||
// 清理
|
||||
onUnmounted(() => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-widget {
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
block-size: 200px;
|
||||
margin-block-end: 16px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
block-size: 100%;
|
||||
inline-size: 100%;
|
||||
}
|
||||
</style>
|
||||
152
examples/plugin-component/src/components/Page.vue
Normal file
152
examples/plugin-component/src/components/Page.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<div class="plugin-page">
|
||||
<v-card>
|
||||
<v-card-title>{{ title }}</v-card-title>
|
||||
<v-card-text>
|
||||
<v-alert v-if="error" type="error" class="mb-4">{{ error }}</v-alert>
|
||||
<v-skeleton-loader v-if="loading" type="card"></v-skeleton-loader>
|
||||
<div v-else>
|
||||
<!-- 数据统计展示 -->
|
||||
<v-row v-if="stats">
|
||||
<v-col v-for="(value, key) in stats" :key="key" cols="12" sm="6" md="4">
|
||||
<v-card variant="outlined" class="text-center">
|
||||
<v-card-text>
|
||||
<div class="text-h4 font-weight-bold">{{ value }}</div>
|
||||
<div class="text-subtitle-1">{{ key }}</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- 最近记录展示 -->
|
||||
<div v-if="recentItems && recentItems.length" class="mt-4">
|
||||
<div class="text-h6 mb-2">最近记录</div>
|
||||
<v-timeline density="compact">
|
||||
<v-timeline-item
|
||||
v-for="(item, index) in recentItems"
|
||||
:key="index"
|
||||
:dot-color="getItemColor(item.type)"
|
||||
size="small"
|
||||
>
|
||||
<div class="d-flex align-center">
|
||||
<v-icon :color="getItemColor(item.type)" size="small" class="mr-2">
|
||||
{{ getItemIcon(item.type) }}
|
||||
</v-icon>
|
||||
<span class="font-weight-medium">{{ item.title }}</span>
|
||||
</div>
|
||||
<div class="text-caption text-secondary">{{ item.time }}</div>
|
||||
</v-timeline-item>
|
||||
</v-timeline>
|
||||
</div>
|
||||
|
||||
<!-- 当前状态 -->
|
||||
<div class="mt-4 text-subtitle-2">
|
||||
<div>
|
||||
<strong>状态:</strong>
|
||||
<v-chip size="small" :color="status === 'running' ? 'success' : 'warning'">{{ status }}</v-chip>
|
||||
</div>
|
||||
<div><strong>最后更新:</strong> {{ lastUpdated }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" @click="refreshData" :loading="loading">
|
||||
<v-icon left>mdi-refresh</v-icon>
|
||||
刷新数据
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
// 组件状态
|
||||
const title = ref('插件详情页面')
|
||||
const loading = ref(true)
|
||||
const error = ref(null)
|
||||
const stats = ref(null)
|
||||
const recentItems = ref([])
|
||||
const status = ref('running')
|
||||
const lastUpdated = ref('')
|
||||
|
||||
// 自定义事件,用于通知主应用刷新数据
|
||||
const emit = defineEmits(['action'])
|
||||
|
||||
// 获取状态图标
|
||||
function getItemIcon(type) {
|
||||
const icons = {
|
||||
'movie': 'mdi-movie',
|
||||
'tv': 'mdi-television-classic',
|
||||
'download': 'mdi-download',
|
||||
'error': 'mdi-alert-circle',
|
||||
'success': 'mdi-check-circle',
|
||||
}
|
||||
return icons[type] || 'mdi-information'
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
function getItemColor(type) {
|
||||
const colors = {
|
||||
'movie': 'blue',
|
||||
'tv': 'green',
|
||||
'download': 'purple',
|
||||
'error': 'red',
|
||||
'success': 'success',
|
||||
}
|
||||
return colors[type] || 'grey'
|
||||
}
|
||||
|
||||
// 获取和刷新数据
|
||||
async function refreshData() {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
|
||||
try {
|
||||
// 模拟API调用 - 实际开发中应使用 fetch 调用真实API
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
// 模拟数据
|
||||
stats.value = {
|
||||
'电影': Math.floor(Math.random() * 100) + 50,
|
||||
'电视剧': Math.floor(Math.random() * 100) + 30,
|
||||
'动漫': Math.floor(Math.random() * 100) + 20,
|
||||
'纪录片': Math.floor(Math.random() * 100) + 10,
|
||||
'综艺': Math.floor(Math.random() * 100) + 5,
|
||||
}
|
||||
|
||||
// 模拟最近记录
|
||||
recentItems.value = [
|
||||
{ type: 'movie', title: '肖申克的救赎 (1994)', time: '今天 12:30' },
|
||||
{ type: 'tv', title: '绝命毒师 S01E01', time: '昨天 18:45' },
|
||||
{ type: 'download', title: '开始下载:星际穿越', time: '2天前' },
|
||||
{ type: 'success', title: '下载完成:黑客帝国', time: '3天前' },
|
||||
{ type: 'error', title: '下载失败:泰坦尼克号', time: '一周前' },
|
||||
]
|
||||
|
||||
status.value = Math.random() > 0.2 ? 'running' : 'paused'
|
||||
lastUpdated.value = new Date().toLocaleString()
|
||||
} catch (err) {
|
||||
console.error('获取数据失败:', err)
|
||||
error.value = err.message || '获取数据失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
// 通知主应用组件已更新
|
||||
emit('action')
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
refreshData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.plugin-page {
|
||||
padding: 16px;
|
||||
inline-size: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +1,24 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import { createVuetify } from 'vuetify'
|
||||
import * as components from 'vuetify/components'
|
||||
import * as directives from 'vuetify/directives'
|
||||
import 'vuetify/styles'
|
||||
|
||||
// 创建Vuetify实例
|
||||
const vuetify = createVuetify({
|
||||
components,
|
||||
directives,
|
||||
theme: {
|
||||
defaultTheme: 'dark'
|
||||
}
|
||||
})
|
||||
|
||||
// 创建应用
|
||||
const app = createApp(App)
|
||||
|
||||
// 使用插件
|
||||
app.use(vuetify)
|
||||
|
||||
// 挂载应用
|
||||
app.mount('#app')
|
||||
|
||||
Reference in New Issue
Block a user