Files
managed-portal/web/src/views/ManagedServices.vue
2026-04-27 10:04:36 +08:00

313 lines
7.2 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="managed-services-page">
<div class="page-toolbar">
<p class="page-desc">
统一管理两个 Docker 子服务RTSP容器状态与子项目结构化结果摘要
</p>
<el-button type="primary" @click="loadServices" :loading="loading">
刷新
</el-button>
</div>
<el-row v-loading="loading" :gutter="20">
<el-col
v-for="service in services"
:key="service.id"
:xs="24"
:md="12"
:xl="8"
>
<el-card class="service-card" shadow="never">
<template #header>
<div class="card-header">
<div>
<div class="service-name">{{ service.display_name }}</div>
<div class="service-type">{{ service.project_type }}</div>
</div>
<el-tag :type="statusType(service.status)">
{{ statusText(service.status) }}
</el-tag>
</div>
</template>
<div class="service-section">
<span class="section-label">RTSP</span>
<div class="rtsp-value">
{{ service.rtsp || service.config_error || "-" }}
</div>
</div>
<div class="service-section">
<span class="section-label">最近结果</span>
<div>{{ formatTime(service.summary?.last_result_time) }}</div>
</div>
<div class="service-section">
<span class="section-label">摘要</span>
<div class="summary-text">
{{
service.summary?.headline ||
service.result_error ||
"暂无摘要"
}}
</div>
</div>
<div
v-if="
service.config_error ||
service.service_error ||
service.result_error
"
class="service-errors"
>
<el-tag v-if="service.config_error" type="warning">
配置异常
</el-tag>
<el-tag v-if="service.service_error" type="danger">
服务状态异常
</el-tag>
<el-tag v-if="service.result_error" type="info">
结果读取异常
</el-tag>
</div>
<div class="service-actions">
<el-button @click="openEdit(service)">编辑 RTSP</el-button>
<el-button
type="warning"
@click="handleRestart(service)"
:loading="restartTarget === service.id"
>
重启容器
</el-button>
<el-button type="primary" @click="goDetail(service)">
查看详情
</el-button>
</div>
</el-card>
</el-col>
</el-row>
<el-empty
v-if="!loading && services.length === 0"
description="暂无被管理服务"
/>
<el-dialog v-model="editVisible" width="680px" title="编辑 RTSP">
<el-form label-width="100px">
<el-form-item label="服务">
<span>{{ editForm.display_name }}</span>
</el-form-item>
<el-form-item label="RTSP">
<el-input
v-model="editForm.rtsp"
type="textarea"
:rows="3"
placeholder="rtsp://user:password@host:554/stream"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editVisible = false">取消</el-button>
<el-button type="primary" @click="saveConfig(false)" :loading="saving">
保存
</el-button>
<el-button type="warning" @click="saveConfig(true)" :loading="saving">
保存并重启
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import {
getManagedServices,
restartManagedService,
updateManagedServiceConfig,
} from "@/api";
const router = useRouter();
const services = ref([]);
const loading = ref(false);
const saving = ref(false);
const restartTarget = ref("");
const editVisible = ref(false);
const editForm = ref({
id: "",
display_name: "",
rtsp: "",
});
const loadServices = async () => {
loading.value = true;
try {
const res = await getManagedServices();
services.value = res.services || [];
} catch (error) {
ElMessage.error(error.message || "加载被管理服务失败");
} finally {
loading.value = false;
}
};
const openEdit = (service) => {
editForm.value = {
id: service.id,
display_name: service.display_name,
rtsp: service.rtsp || "",
};
editVisible.value = true;
};
const saveConfig = async (restartAfter) => {
if (!editForm.value.id) {
return;
}
saving.value = true;
try {
await updateManagedServiceConfig(editForm.value.id, {
rtsp_url: editForm.value.rtsp,
});
if (restartAfter) {
await restartManagedService(editForm.value.id);
}
ElMessage.success(restartAfter ? "配置已保存并重启容器" : "配置已保存");
editVisible.value = false;
await loadServices();
} catch (error) {
ElMessage.error(error.message || "保存配置失败");
} finally {
saving.value = false;
}
};
const handleRestart = async (service) => {
restartTarget.value = service.id;
try {
await restartManagedService(service.id);
ElMessage.success("容器已重启");
await loadServices();
} catch (error) {
ElMessage.error(error.message || "重启服务失败");
} finally {
restartTarget.value = "";
}
};
const goDetail = (service) => {
router.push(`/managed-services/${service.id}`);
};
const statusType = (status) => {
const map = {
running: "success",
stopped: "info",
failed: "danger",
unknown: "warning",
};
return map[status] || "warning";
};
const statusText = (status) => {
const map = {
running: "运行中",
stopped: "已停止",
failed: "失败",
unknown: "未知",
};
return map[status] || status || "未知";
};
const formatTime = (value) => {
if (!value) {
return "-";
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
return date.toLocaleString();
};
onMounted(loadServices);
</script>
<style scoped>
.managed-services-page {
width: 100%;
}
.page-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
gap: 16px;
}
.page-desc {
margin: 0;
color: #606266;
}
.service-card {
margin-bottom: 20px;
min-height: 320px;
}
.card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
}
.service-name {
font-size: 18px;
font-weight: 600;
}
.service-type {
margin-top: 4px;
color: #909399;
font-size: 13px;
}
.service-section {
margin-bottom: 16px;
}
.section-label {
display: block;
margin-bottom: 6px;
color: #909399;
font-size: 13px;
}
.rtsp-value,
.summary-text {
line-height: 1.6;
word-break: break-word;
}
.service-errors {
display: flex;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.service-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
</style>