MapManage.vue 6.69 KB
<template>
  <el-card shadow="never" class="box">
    <div class="toolbar">
      <el-button type="primary" @click="openEdit()">新增锚点</el-button>
    </div>
    <el-table v-loading="loading" :data="list" stripe>
      <el-table-column label="缩略" width="72">
        <template #default="{ row }">
          <el-image v-if="row.thumbnail" :src="fullUrl(row.thumbnail)" fit="cover" class="thumb" />
          <span v-else class="muted">—</span>
        </template>
      </el-table-column>
      <el-table-column prop="name" label="名称" width="120" />
      <el-table-column prop="lat" label="纬度" width="110" />
      <el-table-column prop="lng" label="经度" width="110" />
      <el-table-column prop="photos_count" label="作品数" width="90" />
      <el-table-column prop="pin_left_pct" label="图钉左%" width="100" />
      <el-table-column prop="pin_top_pct" label="图钉上%" width="100" />
      <el-table-column prop="sort" label="排序" width="70" />
      <el-table-column label="状态" width="90">
        <template #default="{ row }">
          <el-tag :type="row.status === 1 ? 'success' : 'info'">
            {{ row.status === 1 ? "显示" : "隐藏" }}
          </el-tag>
        </template>
      </el-table-column>
      <el-table-column label="操作" width="160" fixed="right">
        <template #default="{ row }">
          <el-button type="primary" link @click="openEdit(row)">编辑</el-button>
          <el-button type="danger" link @click="onDelete(row)">删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <el-dialog
      v-model="visible"
      :title="form.id ? '编辑锚点' : '新增锚点'"
      width="560px"
      align-center
      destroy-on-close
    >
      <el-form ref="formRef" :model="form" :rules="rules" label-width="112px">
        <el-form-item label="名称" prop="name">
          <el-input v-model="form.name" />
        </el-form-item>
        <el-form-item label="纬度" prop="lat">
          <el-input-number v-model="form.lat" :precision="6" :step="0.000001" class="w-num" />
        </el-form-item>
        <el-form-item label="经度" prop="lng">
          <el-input-number v-model="form.lng" :precision="6" :step="0.000001" class="w-num" />
        </el-form-item>
        <el-form-item label="描述" prop="description">
          <el-input v-model="form.description" type="textarea" :rows="3" />
        </el-form-item>
        <el-form-item label="缩略图路径" prop="thumbnail">
          <el-input v-model="form.thumbnail" placeholder="/static/xxx.png" />
        </el-form-item>
        <el-form-item label="作品数量" prop="photos_count">
          <el-input-number v-model="form.photos_count" :min="0" />
        </el-form-item>
        <el-form-item label="图钉左%" prop="pin_left_pct">
          <el-input-number v-model="form.pin_left_pct" :min="0" :max="100" :precision="3" class="w-num" />
        </el-form-item>
        <el-form-item label="图钉上%" prop="pin_top_pct">
          <el-input-number v-model="form.pin_top_pct" :min="0" :max="100" :precision="3" class="w-num" />
        </el-form-item>
        <el-form-item label="排序" prop="sort">
          <el-input-number v-model="form.sort" :min="0" />
        </el-form-item>
        <el-form-item label="状态" prop="status">
          <el-radio-group v-model="form.status">
            <el-radio :value="1">显示</el-radio>
            <el-radio :value="0">隐藏</el-radio>
          </el-radio-group>
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="visible = false">取消</el-button>
        <el-button type="primary" :loading="saving" @click="submit">保存</el-button>
      </template>
    </el-dialog>
  </el-card>
</template>

<script setup lang="ts">
import { onMounted, reactive, ref } from "vue";
import type { FormInstance, FormRules } from "element-plus";
import { ElMessageBox } from "element-plus";
import { deleteMapSpot, fetchMapSpots, saveMapSpot, type MapSpotRow } from "@/api/admin";

const loading = ref(false);
const list = ref<MapSpotRow[]>([]);
const visible = ref(false);
const saving = ref(false);
const formRef = ref<FormInstance>();

const form = reactive({
  id: 0,
  name: "",
  lat: 29.05,
  lng: 100.35,
  description: "",
  thumbnail: "",
  photos_count: 0,
  pin_left_pct: 50,
  pin_top_pct: 50,
  sort: 0,
  status: 1,
});

const rules: FormRules = {
  name: [{ required: true, message: "必填", trigger: "blur" }],
};

function apiOrigin(): string {
  const v = import.meta.env.VITE_API_BASE;
  if (typeof v === "string" && v.trim()) {
    return v.trim().replace(/\/+$/, "");
  }
  return "";
}

function fullUrl(path: string): string {
  if (!path) return "";
  if (path.startsWith("http")) return path;
  if (path.startsWith("/uploads/")) {
    const o = apiOrigin();
    return o ? `${o}${path}` : path;
  }
  return path;
}

async function load() {
  loading.value = true;
  try {
    const res = await fetchMapSpots();
    list.value = res.data.list;
  } finally {
    loading.value = false;
  }
}

function openEdit(row?: MapSpotRow) {
  if (row) {
    form.id = row.id;
    form.name = row.name;
    form.lat = row.lat;
    form.lng = row.lng;
    form.description = row.description;
    form.thumbnail = row.thumbnail;
    form.photos_count = row.photos_count;
    form.pin_left_pct = row.pin_left_pct;
    form.pin_top_pct = row.pin_top_pct;
    form.sort = row.sort;
    form.status = row.status;
  } else {
    form.id = 0;
    form.name = "";
    form.lat = 29.05;
    form.lng = 100.35;
    form.description = "";
    form.thumbnail = "";
    form.photos_count = 0;
    form.pin_left_pct = 50;
    form.pin_top_pct = 50;
    form.sort = list.value.length + 1;
    form.status = 1;
  }
  visible.value = true;
}

async function submit() {
  await formRef.value?.validate().catch(() => Promise.reject());
  saving.value = true;
  try {
    await saveMapSpot({
      id: form.id,
      name: form.name,
      lat: form.lat,
      lng: form.lng,
      description: form.description,
      thumbnail: form.thumbnail,
      photos_count: form.photos_count,
      pin_left_pct: form.pin_left_pct,
      pin_top_pct: form.pin_top_pct,
      sort: form.sort,
      status: form.status,
    });
    visible.value = false;
    await load();
  } finally {
    saving.value = false;
  }
}

async function onDelete(row: MapSpotRow) {
  await ElMessageBox.confirm("确认删除该锚点?", "提示", { type: "warning" });
  await deleteMapSpot(row.id);
  await load();
}

onMounted(() => {
  void load();
});
</script>

<style scoped>
.box {
  border-radius: 12px;
}

.toolbar {
  margin-bottom: 16px;
}

.thumb {
  width: 48px;
  height: 48px;
  border-radius: 6px;
}

.muted {
  color: #94a3b8;
}

.w-num {
  width: 100%;
  max-width: 240px;
}
</style>