Appearance
56|前端页面实现(下)
上半部分实现了登录页和资产列表。下半部分完成资产表单页(创建/编辑)、任务管理页、事件日志页,以及错误处理和加载状态。
一、资产表单页
vue
<!-- src/views/AssetForm.vue -->
<template>
<div>
<h2>{{ isEdit ? "编辑资产" : "新增资产" }}</h2>
<form @submit.prevent="handleSubmit">
<div class="form-item">
<label>主机名 *</label>
<input v-model="form.hostname" type="text" required>
</div>
<div class="form-item">
<label>IP 地址 *</label>
<input v-model="form.ip" type="text" required>
</div>
<div class="form-item">
<label>环境</label>
<select v-model="form.env">
<option value="dev">开发</option>
<option value="test">测试</option>
<option value="prod">生产</option>
</select>
</div>
<p v-if="error" class="error">{{ error }}</p>
<div class="actions">
<button type="button" @click="$router.back()">取消</button>
<button type="submit" :disabled="saving">{{ saving ? "保存中..." : "保存" }}</button>
</div>
</form>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { api } from "../api/request.js";
const route = useRoute();
const router = useRouter();
const isEdit = ref(!!route.params.id);
const form = reactive({
hostname: "",
ip: "",
env: "dev",
});
const saving = ref(false);
const error = ref("");
async function loadAsset() {
if (!isEdit.value) return;
try {
const data = await api.get(`/assets/${route.params.id}`);
Object.assign(form, data);
} catch (err) {
error.value = err.message;
}
}
async function handleSubmit() {
saving.value = true;
error.value = "";
try {
if (isEdit.value) {
await api.put(`/assets/${route.params.id}`, form);
} else {
await api.post("/assets", form);
}
router.push("/assets");
} catch (err) {
error.value = err.message;
} finally {
saving.value = false;
}
}
onMounted(loadAsset);
</script>二、任务管理页
vue
<!-- src/views/TaskList.vue -->
<template>
<div>
<h2>任务管理</h2>
<div class="toolbar">
<select v-model="newTask.asset_id">
<option v-for="a in assets" :key="a.id" :value="a.id">{{ a.hostname }}</option>
</select>
<select v-model="newTask.action">
<option value="check">健康检查</option>
<option value="restart">重启</option>
</select>
<button @click="createTask" :disabled="creating">创建任务</button>
</div>
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>资产</th>
<th>操作</th>
<th>状态</th>
<th>结果</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
<tr v-for="t in tasks" :key="t.id">
<td>{{ t.id }}</td>
<td>{{ t.asset?.hostname || t.asset_id }}</td>
<td>{{ t.action }}</td>
<td><span :class="t.status">{{ t.status }}</span></td>
<td>{{ t.result || "-" }}</td>
<td>{{ formatDate(t.created_at) }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import { api } from "../api/request.js";
const assets = ref([]);
const tasks = ref([]);
const creating = ref(false);
const newTask = reactive({ asset_id: "", action: "check" });
async function loadData() {
assets.value = await api.get("/assets");
tasks.value = await api.get("/tasks");
if (assets.value.length > 0 && !newTask.asset_id) {
newTask.asset_id = assets.value[0].id;
}
}
async function createTask() {
creating.value = true;
try {
await api.post("/tasks", { asset_id: newTask.asset_id, action: newTask.action });
await loadData();
} catch (err) {
alert(err.message);
} finally {
creating.value = false;
}
}
function formatDate(iso) {
return new Date(iso).toLocaleString("zh-CN");
}
onMounted(loadData);
</script>三、事件日志页
vue
<!-- src/views/EventLog.vue -->
<template>
<div>
<h2>事件日志</h2>
<table class="data-table">
<thead>
<tr>
<th>时间</th>
<th>用户</th>
<th>操作</th>
<th>对象</th>
<th>详情</th>
</tr>
</thead>
<tbody>
<tr v-for="e in events" :key="e.id">
<td>{{ formatDate(e.created_at) }}</td>
<td>{{ e.user?.username || "-" }}</td>
<td>{{ e.action }}</td>
<td>{{ e.target_type }} #{{ e.target_id }}</td>
<td>{{ e.detail }}</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { api } from "../api/request.js";
const events = ref([]);
async function loadEvents() {
events.value = await api.get("/events");
}
function formatDate(iso) {
return new Date(iso).toLocaleString("zh-CN");
}
onMounted(loadEvents);
</script>四、加载与错误状态
vue
<!-- 通用加载组件 src/components/Loading.vue -->
<template>
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="error" class="error">{{ error }}</div>
<div v-else><slot /></div>
</template>
<script setup>
defineProps({ loading: Boolean, error: String });
</script>使用:
vue
<Loading :loading="loading" :error="error">
<table>...数据...</table>
</Loading>