Skip to content

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>