Просмотр исходного кода

feat(web): 新增后台菜单管理功能

后端:
- 新建 AdminMenu 实体/Mapper/Service/Controller
- 迁移230: admin_menu表 + 18条默认菜单种子数据
- schema.sql同步新增admin_menu建表DDL及INSERT
- 接口: POST /api/admin/menu/tree|list|add|edit|delete/{id}|sort

前端:
- 新增 admin/MenuManage.vue 树形表格+增删改弹窗
- router/index.js 注册 /menu-manage 路由(system:menu权限)
iwt 1 месяц назад
Родитель
Сommit
6d8019af00

+ 48 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -8855,5 +8855,53 @@ private void runMigration100() {
 		} catch (Exception ex) {
 			log.warn("插入GROWTH_COACH权益种子数据失败: {}", ex.getMessage());
 		}
+
+		// 迁移230: 创建 admin_menu 表(后台菜单管理)
+		try {
+			jdbcTemplate.execute(
+				"CREATE TABLE IF NOT EXISTS admin_menu (" +
+				"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+				"parent_id BIGINT DEFAULT 0 COMMENT '父菜单ID,0=顶级', " +
+				"name VARCHAR(50) NOT NULL COMMENT '菜单名称', " +
+				"path VARCHAR(200) DEFAULT NULL COMMENT '路由路径', " +
+				"icon VARCHAR(50) DEFAULT NULL COMMENT '图标类名', " +
+				"sort_order INT DEFAULT 0 COMMENT '排序号', " +
+				"perm VARCHAR(100) DEFAULT NULL COMMENT '权限标识', " +
+				"component VARCHAR(200) DEFAULT NULL COMMENT '前端组件路径', " +
+				"visible TINYINT DEFAULT 1 COMMENT '是否显示:0隐藏1显示', " +
+				"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+				"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+				"INDEX idx_parent (parent_id)" +
+				") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台管理菜单'"
+			);
+			log.info("已创建admin_menu表");
+		} catch (Exception ex) {
+			log.warn("创建admin_menu表失败: {}", ex.getMessage());
+		}
+		try {
+			jdbcTemplate.execute("INSERT IGNORE INTO admin_menu (parent_id, name, path, icon, sort_order, perm, component, visible) VALUES " +
+				"(0, '首页', '/dashboard', 'el-icon-house', 0, 'dashboard', 'admin/Dashboard', 1), " +
+				"(0, '家庭管理', NULL, 'el-icon-user', 10, 'family:list', NULL, 1), " +
+				"(1, '家庭列表', '/families', 'el-icon-office-building', 1, 'family:list', 'Families', 1), " +
+				"(1, '孩子管理', '/children', 'el-icon-child', 2, 'family:children', 'Children', 1), " +
+				"(0, '积分管理', NULL, 'el-icon-ticket', 20, 'family:points', NULL, 1), " +
+				"(3, '积分记录', '/points-log', 'el-icon-document', 1, 'family:points', 'admin/PointsLog', 1), " +
+				"(0, '会员管理', NULL, 'el-icon-v-ip', 30, 'system:config', NULL, 1), " +
+				"(4, '会员中心', '/membership-center', 'el-icon-setting', 1, 'system:config', 'admin/MembershipCenter', 1), " +
+				"(0, '订单管理', NULL, 'el-icon-shopping-cart-2', 40, 'product:list', NULL, 1), " +
+				"(6, '商品订单', '/product-orders', 'el-icon-s-order', 1, 'product:list', 'admin/ProductManage', 1), " +
+				"(0, '内容管理', NULL, 'el-icon-document-checked', 50, 'article:list', NULL, 1), " +
+				"(8, '文章管理', '/article-manage', 'el-icon-edit', 1, 'article:list', 'admin/ArticleManage', 1), " +
+				"(8, '分类管理', '/article-category', 'el-icon-folder', 2, 'article:list', 'admin/ArticleCategory', 1), " +
+				"(0, '测评管理', NULL, 'el-icon-reading', 60, 'assessment:list', NULL, 1), " +
+				"(10, '测评订单', '/assessment-orders', 'el-icon-tickets', 1, 'assessment:list', 'admin/AssessmentOrders', 1), " +
+				"(10, '测评产品', '/assessment-products', 'el-icon-price-tag', 2, 'assessment:product', 'admin/AssessmentProducts', 1), " +
+				"(0, '系统配置', NULL, 'el-icon-setting', 90, 'system:config', NULL, 1), " +
+				"(14, '菜单管理', '/menu-manage', 'el-icon-menu', 1, 'system:menu', 'admin/MenuManage', 1), " +
+				"(14, '系统配置', '/sys-config', 'el-icon-setting', 2, 'system:config', 'admin/SysConfig', 1)");
+			log.info("已插入admin_menu种子数据");
+		} catch (Exception ex) {
+			log.warn("插入admin_menu种子数据失败: {}", ex.getMessage());
+		}
 	}
 }

+ 77 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminMenuController.java

@@ -0,0 +1,77 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.AdminMenu;
+import com.etotem.cfc.service.AdminMenuService;
+import io.swagger.v3.oas.annotations.Operation;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/admin/menu")
+public class AdminMenuController {
+
+    @Resource
+    private AdminMenuService menuService;
+
+    @Operation(summary = "获取菜单树")
+    @PostMapping("/tree")
+    public Result<List<Map<String, Object>>> tree() {
+        return Result.success(menuService.buildTree());
+    }
+
+    @Operation(summary = "获取所有菜单(平铺)")
+    @PostMapping("/list")
+    public Result<List<AdminMenu>> list() {
+        return Result.success(menuService.list());
+    }
+
+    @Operation(summary = "新增菜单")
+    @PostMapping("/add")
+    public Result<Void> add(@RequestBody AdminMenu menu) {
+        menu.setCreatedAt(new Date());
+        menu.setUpdatedAt(new Date());
+        menuService.save(menu);
+        return Result.success();
+    }
+
+    @Operation(summary = "更新菜单")
+    @PostMapping("/edit")
+    public Result<Void> edit(@RequestBody AdminMenu menu) {
+        AdminMenu existing = menuService.getById(menu.getId());
+        if (existing == null) return Result.error("菜单不存在");
+        menu.setUpdatedAt(new Date());
+        menuService.updateById(menu);
+        return Result.success();
+    }
+
+    @Operation(summary = "删除菜单")
+    @PostMapping("/delete/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        List<AdminMenu> children = menuService.list(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<AdminMenu>()
+                        .eq(AdminMenu::getParentId, id));
+        if (!children.isEmpty()) {
+            return Result.error("存在子菜单,无法删除");
+        }
+        menuService.removeById(id);
+        return Result.success();
+    }
+
+    @Operation(summary = "调整排序")
+    @PostMapping("/sort")
+    public Result<Void> sort(@RequestBody Map<String, Object> payload) {
+        Long id = Long.valueOf(payload.get("id").toString());
+        Integer sortOrder = (Integer) payload.get("sortOrder");
+        AdminMenu menu = menuService.getById(id);
+        if (menu == null) return Result.error("菜单不存在");
+        menu.setSortOrder(sortOrder);
+        menu.setUpdatedAt(new Date());
+        menuService.updateById(menu);
+        return Result.success();
+    }
+}

+ 37 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/AdminMenu.java

@@ -0,0 +1,37 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("admin_menu")
+public class AdminMenu implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long parentId;
+
+    private String name;
+
+    private String path;
+
+    private String icon;
+
+    private Integer sortOrder;
+
+    private String perm;
+
+    private String component;
+
+    private Integer visible;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/AdminMenuMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.AdminMenu;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface AdminMenuMapper extends BaseMapper<AdminMenu> {
+}

+ 57 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/AdminMenuService.java

@@ -0,0 +1,57 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.etotem.cfc.entity.AdminMenu;
+import com.etotem.cfc.mapper.AdminMenuMapper;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Service
+public class AdminMenuService extends ServiceImpl<AdminMenuMapper, AdminMenu> {
+
+    public List<Map<String, Object>> buildTree() {
+        List<AdminMenu> all = list();
+        all.sort(Comparator.comparingInt(m -> m.getSortOrder() != null ? m.getSortOrder() : 0));
+
+        Map<Long, List<AdminMenu>> byParent = all.stream()
+                .collect(Collectors.groupingBy(m -> m.getParentId() != null ? m.getParentId() : 0L));
+
+        List<AdminMenu> roots = all.stream()
+                .filter(m -> m.getParentId() == null || m.getParentId() == 0)
+                .collect(Collectors.toList());
+
+        return buildNodes(roots, byParent);
+    }
+
+    private List<Map<String, Object>> buildNodes(List<AdminMenu> nodes, Map<Long, List<AdminMenu>> byParent) {
+        List<Map<String, Object>> result = new ArrayList<>();
+        for (AdminMenu menu : nodes) {
+            Map<String, Object> map = new java.util.HashMap<>();
+            map.put("id", menu.getId());
+            map.put("parentId", menu.getParentId());
+            map.put("name", menu.getName());
+            map.put("path", menu.getPath());
+            map.put("icon", menu.getIcon());
+            map.put("sortOrder", menu.getSortOrder());
+            map.put("perm", menu.getPerm());
+            map.put("component", menu.getComponent());
+            map.put("visible", menu.getVisible());
+            map.put("createdAt", menu.getCreatedAt());
+            map.put("updatedAt", menu.getUpdatedAt());
+
+            List<AdminMenu> children = byParent.getOrDefault(menu.getId(), new ArrayList<>());
+            if (!children.isEmpty()) {
+                map.put("children", buildNodes(children, byParent));
+            } else {
+                map.put("children", new ArrayList<>());
+            }
+            result.add(map);
+        }
+        return result;
+    }
+}

+ 39 - 0
cfc-backend/src/main/resources/schema.sql

@@ -4770,3 +4770,42 @@ CREATE TABLE IF NOT EXISTS ai_q_profile (
   UNIQUE KEY uk_session (session_id),
   INDEX idx_member (member_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI 动态问卷-画像结果';
+
+-- =============================================
+-- 后台菜单管理
+-- =============================================
+CREATE TABLE IF NOT EXISTS admin_menu (
+  id BIGINT AUTO_INCREMENT PRIMARY KEY,
+  parent_id BIGINT DEFAULT 0 COMMENT '父菜单ID,0=顶级',
+  name VARCHAR(50) NOT NULL COMMENT '菜单名称',
+  path VARCHAR(200) DEFAULT NULL COMMENT '路由路径',
+  icon VARCHAR(50) DEFAULT NULL COMMENT '图标类名',
+  sort_order INT DEFAULT 0 COMMENT '排序号',
+  perm VARCHAR(100) DEFAULT NULL COMMENT '权限标识',
+  component VARCHAR(200) DEFAULT NULL COMMENT '前端组件路径',
+  visible TINYINT DEFAULT 1 COMMENT '是否显示:0隐藏1显示',
+  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  INDEX idx_parent (parent_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台管理菜单';
+
+INSERT IGNORE INTO admin_menu (parent_id, name, path, icon, sort_order, perm, component, visible) VALUES
+(0, '首页', '/dashboard', 'el-icon-house', 0, 'dashboard', 'admin/Dashboard', 1),
+(0, '家庭管理', NULL, 'el-icon-user', 10, 'family:list', NULL, 1),
+(1, '家庭列表', '/families', 'el-icon-office-building', 1, 'family:list', 'Families', 1),
+(1, '孩子管理', '/children', 'el-icon-child', 2, 'family:children', 'Children', 1),
+(0, '积分管理', NULL, 'el-icon-ticket', 20, 'family:points', NULL, 1),
+(3, '积分记录', '/points-log', 'el-icon-document', 1, 'family:points', 'admin/PointsLog', 1),
+(0, '会员管理', NULL, 'el-icon-v-ip', 30, 'system:config', NULL, 1),
+(4, '会员中心', '/membership-center', 'el-icon-setting', 1, 'system:config', 'admin/MembershipCenter', 1),
+(0, '订单管理', NULL, 'el-icon-shopping-cart-2', 40, 'product:list', NULL, 1),
+(6, '商品订单', '/product-orders', 'el-icon-s-order', 1, 'product:list', 'admin/ProductManage', 1),
+(0, '内容管理', NULL, 'el-icon-document-checked', 50, 'article:list', NULL, 1),
+(8, '文章管理', '/article-manage', 'el-icon-edit', 1, 'article:list', 'admin/ArticleManage', 1),
+(8, '分类管理', '/article-category', 'el-icon-folder', 2, 'article:list', 'admin/ArticleCategory', 1),
+(0, '测评管理', NULL, 'el-icon-reading', 60, 'assessment:list', NULL, 1),
+(10, '测评订单', '/assessment-orders', 'el-icon-tickets', 1, 'assessment:list', 'admin/AssessmentOrders', 1),
+(10, '测评产品', '/assessment-products', 'el-icon-price-tag', 2, 'assessment:product', 'admin/AssessmentProducts', 1),
+(0, '系统配置', NULL, 'el-icon-setting', 90, 'system:config', NULL, 1),
+(14, '菜单管理', '/menu-manage', 'el-icon-menu', 1, 'system:menu', 'admin/MenuManage', 1),
+(14, '系统配置', '/sys-config', 'el-icon-setting', 2, 'system:config', 'admin/SysConfig', 1);

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-7de18d4657c3b30567501fbb2c0644eb5d361c28
+b05b420adf35f8c20e94e5f04fa781b1cbe9d531

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1033",
+  "version": "1.0.1034",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1033",
+      "version": "1.0.1034",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1034",
+  "version": "1.0.1035",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 6 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,12 @@
 
 ---
 
+## v1.0.1035 (2026-08-14)
+
+### 文档
+- 小程序改版设计+分阶段实施计划(需求驱动·逐步解锁,C类问题域闭环优先)
+
+
 ## v1.0.1034 (2026-08-14)
 
 ### 文档

+ 7 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1034
+> 当前版本: v1.0.1035
 
 ## 历史版本
 
@@ -8,6 +8,12 @@
 
 ---
 
+## v1.0.1035 (2026-08-14)
+
+### 文档
+- 小程序改版设计+分阶段实施计划(需求驱动·逐步解锁,C类问题域闭环优先)
+
+
 ## v1.0.1034 (2026-08-14)
 
 ### 文档

+ 6 - 0
cfc-web/src/router/index.js

@@ -294,6 +294,12 @@ const routes = [
       component: () => import('@/views/admin/SysConfig.vue'),
       meta: { title: '系统配置', perm: 'system:config' }
     },
+    {
+      path: 'menu-manage',
+      name: 'MenuManage',
+      component: () => import('@/views/admin/MenuManage.vue'),
+      meta: { title: '菜单管理', perm: 'system:menu' }
+    },
     {
       path: 'energy-sandbox',
       name: 'EnergySandbox',

+ 187 - 0
cfc-web/src/views/admin/MenuManage.vue

@@ -0,0 +1,187 @@
+<template>
+  <div class="menu-manage admin-page">
+    <div class="header admin-page-header">
+      <h2 class="admin-page-title">菜单管理</h2>
+      <div class="flex items-center gap-sm">
+        <el-button type="primary" size="small" @click="handleAdd" icon="el-icon-plus">新增菜单</el-button>
+        <el-button size="small" @click="loadData" icon="el-icon-refresh">刷新</el-button>
+      </div>
+    </div>
+    <el-table :data="treeData" row-key="id" border default-expand-all :tree-props="{children: 'children'}" v-loading="loading" style="width:100%">
+      <el-table-column prop="name" label="菜单名称" min-width="160" />
+      <el-table-column prop="path" label="路由路径" min-width="160" show-overflow-tooltip />
+      <el-table-column prop="icon" label="图标" width="120" show-overflow-tooltip>
+        <template slot-scope="{row}">{{ row.icon || '-' }}</template>
+      </el-table-column>
+      <el-table-column prop="perm" label="权限标识" min-width="140" show-overflow-tooltip />
+      <el-table-column prop="component" label="组件路径" min-width="160" show-overflow-tooltip />
+      <el-table-column prop="sortOrder" label="排序" width="70" align="center" />
+      <el-table-column prop="visible" label="可见" width="70" align="center">
+        <template slot-scope="{row}">
+          <el-tag :type="row.visible ? 'success' : 'info'" size="mini">{{ row.visible ? '是' : '否' }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="160" fixed="right">
+        <template slot-scope="{row}">
+          <el-button size="mini" type="primary" @click="handleEdit(row)">编辑</el-button>
+          <el-button size="mini" type="danger" @click="handleDelete(row)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <!-- 编辑弹窗 -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="560px" :close-on-click-modal="false">
+      <el-form :model="form" :rules="rules" ref="menuForm" label-width="100px">
+        <el-form-item label="父菜单" prop="parentId">
+          <el-tree-select
+            v-model="form.parentId"
+            :data="parentOptions"
+            :props="{label:'name',value:'id',children:'children'}"
+            check-strictly
+            placeholder="顶级菜单"
+            style="width:100%"
+          />
+        </el-form-item>
+        <el-form-item label="菜单名称" prop="name">
+          <el-input v-model="form.name" placeholder="如:订单管理" />
+        </el-form-item>
+        <el-form-item label="路由路径" prop="path">
+          <el-input v-model="form.path" placeholder="如:/order-manage(子菜单必填)" />
+        </el-form-item>
+        <el-form-item label="图标">
+          <el-input v-model="form.icon" placeholder="如:el-icon-setting" />
+        </el-form-item>
+        <el-form-item label="权限标识" prop="perm">
+          <el-input v-model="form.perm" placeholder="如:product:list" />
+        </el-form-item>
+        <el-form-item label="组件路径" prop="component">
+          <el-input v-model="form.component" placeholder="如:admin/ProductManage" />
+        </el-form-item>
+        <el-form-item label="排序">
+          <el-input-number v-model="form.sortOrder" :min="0" :max="999" />
+        </el-form-item>
+        <el-form-item label="是否可见">
+          <el-switch v-model="form.visible" :active-value="1" :inactive-value="0" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="confirmSave" :loading="saving">保存</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+
+export default {
+  data() {
+    return {
+      loading: false,
+      treeData: [],
+      allFlat: [],
+      dialogVisible: false,
+      dialogTitle: '新增菜单',
+      saving: false,
+      form: {
+        id: null, parentId: 0, name: '', path: '', icon: '',
+        perm: '', component: '', sortOrder: 0, visible: 1
+      },
+      rules: {
+        name: [{ required: true, message: '请输入菜单名称', trigger: 'blur' }],
+        perm: [{ required: true, message: '请输入权限标识', trigger: 'blur' }]
+      }
+    }
+  },
+  computed: {
+    parentOptions() {
+      var opts = [{ id: 0, name: '顶级菜单', children: [] }]
+      this.allFlat.forEach(function(m) {
+        opts.push({ id: m.id, name: m.name, children: [] })
+      })
+      return opts
+    }
+  },
+  created() {
+    this.loadData()
+  },
+  methods: {
+    async loadData() {
+      this.loading = true
+      try {
+        var res = await axios.post('/api/admin/menu/tree')
+        if (res.data.code === 200) {
+          this.treeData = res.data.data || []
+          this.allFlat = this.flatten(this.treeData)
+        }
+      } catch (e) {
+        this.$message.error('加载菜单失败')
+      } finally {
+        this.loading = false
+      }
+    },
+    flatten(nodes) {
+      var result = []
+      nodes.forEach(function(n) {
+        result.push(n)
+        if (n.children && n.children.length > 0) {
+          result = result.concat(this.flatten(n.children))
+        }
+      }.bind(this))
+      return result
+    },
+    handleAdd() {
+      this.form = { id: null, parentId: 0, name: '', path: '', icon: '', perm: '', component: '', sortOrder: 0, visible: 1 }
+      this.dialogTitle = '新增菜单'
+      this.dialogVisible = true
+    },
+    handleEdit(row) {
+      this.form = { id: row.id, parentId: row.parentId || 0, name: row.name, path: row.path || '', icon: row.icon || '', perm: row.perm || '', component: row.component || '', sortOrder: row.sortOrder || 0, visible: row.visible || 1 }
+      this.dialogTitle = '编辑菜单'
+      this.dialogVisible = true
+    },
+    async confirmSave() {
+      this.$refs.menuForm.validate(async (valid) => {
+        if (!valid) return
+        this.saving = true
+        try {
+          var url = this.form.id ? '/api/admin/menu/edit' : '/api/admin/menu/add'
+          var res = await axios.post(url, this.form)
+          if (res.data.code === 200) {
+            this.$message.success('保存成功')
+            this.dialogVisible = false
+            this.loadData()
+          } else {
+            this.$message.error(res.data.message || '保存失败')
+          }
+        } catch (e) {
+          this.$message.error('保存失败')
+        } finally {
+          this.saving = false
+        }
+      })
+    },
+    handleDelete(row) {
+      var self = this
+      this.$confirm('确认删除菜单「' + row.name + '」?', '提示', { type: 'warning' }).then(async function() {
+        try {
+          var res = await axios.post('/api/admin/menu/delete/' + row.id)
+          if (res.data.code === 200) {
+            self.$message.success('删除成功')
+            self.loadData()
+          } else {
+            self.$message.error(res.data.message || '删除失败')
+          }
+        } catch (e) {
+          self.$message.error('删除失败')
+        }
+      }).catch(function() {})
+    }
+  }
+}
+</script>
+
+<style scoped>
+.menu-manage { padding: 20px; }
+</style>