Initial commit: finish basic WebUI interface

This commit is contained in:
2026-03-09 21:17:22 +08:00
parent d2a2a4de36
commit 2f6cfe7704
23 changed files with 5028 additions and 1 deletions

266
src/views/Instances.vue Normal file
View File

@@ -0,0 +1,266 @@
<template>
<div class="instances-page">
<div class="page-header">
<h2>Instance Management</h2>
</div>
<div class="instances-grid">
<div
v-for="instance in filteredInstances"
:key="instance.name"
class="instance-card"
@click="goToDetail(instance.name)"
>
<div class="card-header">
<h3 class="instance-name">{{ highlightText(instance.name, searchQuery) }}</h3>
<span :class="['status-badge', instance.status]">
{{ instance.status === 'running' ? 'Running' : 'Stopped' }}
</span>
</div>
<div class="card-body">
<div class="info-row" v-if="instance.webuiPort">
<span class="label">WebUI Port:</span>
<span class="value">{{ instance.webuiPort }}</span>
</div>
<div class="info-row">
<span class="label">Created At:</span>
<span class="value">{{ formatDate(instance.createdAt) }}</span>
</div>
<div class="info-row">
<span class="label">Last Active:</span>
<span class="value">{{ formatDate(instance.lastActive) }}</span>
</div>
</div>
<div class="card-footer" @click.stop>
<button
v-if="instance.status !== 'running'"
class="action-btn start-btn"
@click="startInstance(instance.name)"
>
Start
</button>
<button
v-else
class="action-btn stop-btn"
@click="stopInstance(instance.name)"
>
Stop
</button>
</div>
</div>
</div>
<div v-if="filteredInstances.length === 0" class="empty-state">
<p>No instances available</p>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue';
import { useRouter } from 'vue-router';
import { instanceApi } from '../api/index.js';
import { showNotification, formatDate, highlightText } from '../utils/functions.js';
export default {
name: 'Instances',
props: {
searchQuery: {
type: String,
default: ''
}
},
setup(props) {
const router = useRouter();
const instances = ref([]);
const filteredInstances = computed(() => {
if (!props.searchQuery) return instances.value;
return instances.value.filter(instance =>
instance.name.toLowerCase().includes(props.searchQuery.toLowerCase())
);
});
const loadInstances = async () => {
try {
const result = await instanceApi.listInstances();
instances.value = result.data.map(instance => ({
...instance,
status: 'stopped'
}));
} catch (error) {
showNotification('Load instance list failed', 'error');
}
};
const startInstance = async (instanceName) => {
try {
await instanceApi.startInstance(instanceName);
showNotification('Instance started successfully', 'success');
await loadInstances();
} catch (error) {
showNotification(error.message || 'Start instance failed', 'error');
}
};
const stopInstance = async (instanceName) => {
try {
await instanceApi.stopInstance(instanceName);
showNotification('Instance stopped successfully', 'success');
await loadInstances();
} catch (error) {
showNotification(error.message || 'Stop instance failed', 'error');
}
};
const goToDetail = (instanceName) => {
router.push(`/instances/${instanceName}`);
};
onMounted(() => {
loadInstances();
});
return {
instances,
filteredInstances,
startInstance,
stopInstance,
goToDetail,
formatDate,
highlightText
};
}
};
</script>
<style scoped>
.instances-page {
padding: 24px;
}
.page-header {
margin-bottom: 24px;
}
.page-header h2 {
font-size: 24px;
color: var(--text-color);
margin: 0;
transition: color 0.3s;
}
.instances-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
}
.instance-card {
background: var(--card-bg);
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
cursor: pointer;
transition: all 0.3s;
}
.instance-card:hover {
transform: translateY(-4px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.instance-name {
font-size: 18px;
font-weight: 600;
color: var(--text-color);
margin: 0;
transition: color 0.3s;
}
.status-badge {
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.status-badge.running {
background: #f6ffed;
color: #52c41a;
}
.status-badge.stopped {
background: #fff1f0;
color: #ff4d4f;
}
.card-body {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 16px;
}
.info-row {
display: flex;
justify-content: space-between;
font-size: 14px;
}
.info-row .label {
color: #666;
}
.info-row .value {
color: var(--text-color);
font-weight: 500;
transition: color 0.3s;
}
.card-footer {
display: flex;
gap: 8px;
}
.action-btn {
flex: 1;
padding: 8px 16px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
}
.start-btn {
background: #52c41a;
color: white;
}
.start-btn:hover {
background: #45a049;
}
.stop-btn {
background: #ff4d4f;
color: white;
}
.stop-btn:hover {
background: #f04142;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #999;
font-size: 16px;
}
</style>