帮助文档
{{userInfo.nickname}}
用户设置 退出登录

{{wikiTitle}}

页面添加说明

CRMEB多店系统 - 添加页面说明

一、概述

本文档详细介绍在CRMEB多店系统中添加新页面的完整开发流程,包括前端Vue页面创建、后端API接口开发、路由配置、菜单权限设置等环节。

二、前端项目结构

2.1 后台管理前端目录

view/admin/src/
├── api/              # API接口定义
├── assets/           # 静态资源(图片、字体等)
├── components/       # 公共组件
├── filters/          # 过滤器
├── i18n/             # 国际化
├── layouts/          # 布局组件
├── libs/             # 工具库
├── menu/             # 菜单配置
├── mixins/           # 混入
├── pages/            # 页面组件(核心开发目录)
├── plugins/          # 插件配置
├── router/           # 路由配置
│   ├── index.js      # 路由入口
│   └── modules/      # 模块路由
├── store/            # Vuex状态管理
├── styles/           # 样式文件
├── utils/            # 工具函数
├── App.vue           # 根组件
├── main.js           # 入口文件
└── setting.js        # 配置文件

2.2 技术栈

  • Vue 2.x - 前端框架
  • Vue Router - 路由管理
  • Vuex - 状态管理
  • iView/ViewUI - UI组件库
  • Axios - HTTP请求库
  • form-create - 动态表单生成

三、添加新页面完整流程

3.1 第一步:创建API接口文件

src/api/ 目录下创建或修改API文件。

示例:创建自定义模块API

// src/api/custom.js

import request from '@/plugins/request';

/**
 * 获取自定义列表
 * @param {Object} params - 查询参数
 */
export function getCustomList(params) {
    return request({
        url: 'custom/list',
        method: 'get',
        params
    });
}

/**
 * 获取自定义详情
 * @param {Number} id - 数据ID
 */
export function getCustomInfo(id) {
    return request({
        url: `custom/info/${id}`,
        method: 'get'
    });
}

/**
 * 保存自定义数据
 * @param {Number} id - 数据ID(新增时为0)
 * @param {Object} data - 表单数据
 */
export function saveCustom(id, data) {
    return request({
        url: `custom/save/${id}`,
        method: 'post',
        data
    });
}

/**
 * 删除自定义数据
 * @param {Number} id - 数据ID
 */
export function deleteCustom(id) {
    return request({
        url: `custom/delete/${id}`,
        method: 'delete'
    });
}

/**
 * 修改状态
 * @param {Number} id - 数据ID
 * @param {Number} status - 状态值
 */
export function setCustomStatus(id, status) {
    return request({
        url: `custom/set_status/${id}/${status}`,
        method: 'put'
    });
}

3.2 第二步:创建页面组件

src/pages/ 目录下创建页面文件夹和组件。

目录结构示例:

src/pages/custom/
├── customList/           # 列表页面
│   └── index.vue
├── customAdd/            # 添加/编辑页面
│   └── index.vue
├── components/           # 页面专用组件
│   └── customForm.vue
└── index.vue             # 模块入口(可选)

列表页面示例:

<!-- src/pages/custom/customList/index.vue -->
<template>
  <div>
    <!-- 搜索表单 -->
    <Card :bordered="false" dis-hover class="ivu-mt">
      <Form
        ref="searchForm"
        :model="searchForm"
        :label-width="85"
        label-position="right"
        @submit.native.prevent
      >
        <Row :gutter="24">
          <Col span="6">
            <FormItem label="关键字:">
              <Input
                v-model="searchForm.keyword"
                placeholder="请输入关键字"
                clearable
                @on-enter="handleSearch"
              />
            </FormItem>
          </Col>
          <Col span="6">
            <FormItem label="状态:">
              <Select v-model="searchForm.status" clearable placeholder="请选择状态">
                <Option :value="1">启用</Option>
                <Option :value="0">禁用</Option>
              </Select>
            </FormItem>
          </Col>
          <Col span="6">
            <FormItem>
              <Button type="primary" @click="handleSearch">搜索</Button>
              <Button class="ivu-ml-8" @click="handleReset">重置</Button>
            </FormItem>
          </Col>
        </Row>
      </Form>
    </Card>

    <!-- 操作按钮 -->
    <Card :bordered="false" dis-hover class="ivu-mt">
      <Button type="primary" icon="md-add" @click="handleAdd">添加</Button>
      <Button type="error" class="ivu-ml-8" @click="handleBatchDelete">批量删除</Button>
    </Card>

    <!-- 数据表格 -->
    <Card :bordered="false" dis-hover class="ivu-mt">
      <Table
        ref="table"
        :loading="loading"
        :columns="columns"
        :data="tableData"
        @on-selection-change="handleSelectionChange"
      >
        <!-- 状态列 -->
        <template slot-scope="{ row }" slot="status">
          <i-switch
            :value="row.status"
            :true-value="1"
            :false-value="0"
            @on-change="handleStatusChange(row)"
          />
        </template>

        <!-- 操作列 -->
        <template slot-scope="{ row }" slot="action">
          <a @click="handleEdit(row)">编辑</a>
          <Divider type="vertical" />
          <a class="text-danger" @click="handleDelete(row)">删除</a>
        </template>
      </Table>

      <!-- 分页 -->
      <div class="acea-row row-right ivu-mt">
        <Page
          :total="total"
          :current.sync="searchForm.page"
          :page-size="searchForm.limit"
          show-elevator
          show-total
          @on-change="handlePageChange"
          @on-page-size-change="handlePageSizeChange"
        />
      </div>
    </Card>
  </div>
</template>

<script>
import { getCustomList, deleteCustom, setCustomStatus } from '@/api/custom';

export default {
  name: 'CustomList',
  data() {
    return {
      loading: false,
      searchForm: {
        keyword: '',
        status: '',
        page: 1,
        limit: 15
      },
      tableData: [],
      total: 0,
      selectedIds: [],
      columns: [
        {
          type: 'selection',
          width: 60,
          align: 'center'
        },
        {
          title: 'ID',
          key: 'id',
          width: 80
        },
        {
          title: '名称',
          key: 'name',
          minWidth: 150
        },
        {
          title: '状态',
          slot: 'status',
          width: 100
        },
        {
          title: '创建时间',
          key: 'create_time',
          width: 180
        },
        {
          title: '操作',
          slot: 'action',
          width: 150,
          fixed: 'right'
        }
      ]
    };
  },
  created() {
    this.getList();
  },
  methods: {
    // 获取列表数据
    async getList() {
      this.loading = true;
      try {
        const { data } = await getCustomList(this.searchForm);
        this.tableData = data.list;
        this.total = data.count;
      } catch (error) {
        this.$Message.error(error.msg || '获取数据失败');
      }
      this.loading = false;
    },

    // 搜索
    handleSearch() {
      this.searchForm.page = 1;
      this.getList();
    },

    // 重置
    handleReset() {
      this.searchForm = {
        keyword: '',
        status: '',
        page: 1,
        limit: 15
      };
      this.getList();
    },

    // 分页变化
    handlePageChange(page) {
      this.searchForm.page = page;
      this.getList();
    },

    // 每页条数变化
    handlePageSizeChange(limit) {
      this.searchForm.limit = limit;
      this.searchForm.page = 1;
      this.getList();
    },

    // 选择变化
    handleSelectionChange(selection) {
      this.selectedIds = selection.map(item => item.id);
    },

    // 添加
    handleAdd() {
      this.$router.push({ name: 'custom_customAdd' });
    },

    // 编辑
    handleEdit(row) {
      this.$router.push({
        name: 'custom_customAdd',
        params: { id: row.id }
      });
    },

    // 删除
    handleDelete(row) {
      this.$Modal.confirm({
        title: '提示',
        content: '确定要删除该数据吗?',
        onOk: async () => {
          try {
            await deleteCustom(row.id);
            this.$Message.success('删除成功');
            this.getList();
          } catch (error) {
            this.$Message.error(error.msg || '删除失败');
          }
        }
      });
    },

    // 批量删除
    handleBatchDelete() {
      if (this.selectedIds.length === 0) {
        return this.$Message.warning('请先选择要删除的数据');
      }
      this.$Modal.confirm({
        title: '提示',
        content: `确定要删除选中的 ${this.selectedIds.length} 条数据吗?`,
        onOk: async () => {
          // 批量删除逻辑
        }
      });
    },

    // 修改状态
    async handleStatusChange(row) {
      try {
        const newStatus = row.status === 1 ? 0 : 1;
        await setCustomStatus(row.id, newStatus);
        row.status = newStatus;
        this.$Message.success('修改成功');
      } catch (error) {
        this.$Message.error(error.msg || '修改失败');
      }
    }
  }
};
</script>

<style lang="less" scoped>
.text-danger {
  color: #ed4014;
}
</style>

添加/编辑页面示例:

<!-- src/pages/custom/customAdd/index.vue -->
<template>
  <div class="custom-add">
    <Card :bordered="false" dis-hover>
      <div slot="title">
        <span>{{ isEdit ? '编辑' : '添加' }}数据</span>
      </div>

      <Form
        ref="form"
        :model="formData"
        :rules="rules"
        :label-width="120"
        label-position="right"
      >
        <FormItem label="名称:" prop="name">
          <Input
            v-model="formData.name"
            placeholder="请输入名称"
            style="width: 400px"
          />
        </FormItem>

        <FormItem label="描述:" prop="desc">
          <Input
            v-model="formData.desc"
            type="textarea"
            :rows="4"
            placeholder="请输入描述"
            style="width: 400px"
          />
        </FormItem>

        <FormItem label="排序:" prop="sort">
          <InputNumber
            v-model="formData.sort"
            :min="0"
            :max="9999"
            style="width: 200px"
          />
        </FormItem>

        <FormItem label="状态:" prop="status">
          <RadioGroup v-model="formData.status">
            <Radio :label="1">启用</Radio>
            <Radio :label="0">禁用</Radio>
          </RadioGroup>
        </FormItem>

        <FormItem>
          <Button type="primary" :loading="submitting" @click="handleSubmit">
            提交
          </Button>
          <Button class="ivu-ml-8" @click="handleBack">返回</Button>
        </FormItem>
      </Form>
    </Card>
  </div>
</template>

<script>
import { getCustomInfo, saveCustom } from '@/api/custom';

export default {
  name: 'CustomAdd',
  data() {
    return {
      id: 0,
      isEdit: false,
      submitting: false,
      formData: {
        name: '',
        desc: '',
        sort: 0,
        status: 1
      },
      rules: {
        name: [
          { required: true, message: '请输入名称', trigger: 'blur' },
          { max: 50, message: '名称不能超过50个字符', trigger: 'blur' }
        ]
      }
    };
  },
  created() {
    this.id = parseInt(this.$route.params.id) || 0;
    this.isEdit = this.id > 0;
    if (this.isEdit) {
      this.getInfo();
    }
  },
  methods: {
    // 获取详情
    async getInfo() {
      try {
        const { data } = await getCustomInfo(this.id);
        this.formData = {
          name: data.name,
          desc: data.desc,
          sort: data.sort,
          status: data.status
        };
      } catch (error) {
        this.$Message.error(error.msg || '获取数据失败');
      }
    },

    // 提交
    handleSubmit() {
      this.$refs.form.validate(async (valid) => {
        if (!valid) return;

        this.submitting = true;
        try {
          await saveCustom(this.id, this.formData);
          this.$Message.success(this.isEdit ? '修改成功' : '添加成功');
          this.$router.push({ name: 'custom_customList' });
        } catch (error) {
          this.$Message.error(error.msg || '提交失败');
        }
        this.submitting = false;
      });
    },

    // 返回
    handleBack() {
      this.$router.go(-1);
    }
  }
};
</script>

<style lang="less" scoped>
.custom-add {
  padding: 20px;
}
</style>

3.3 第三步:配置前端路由

src/router/modules/ 目录下创建路由配置文件。

// src/router/modules/custom.js

import BasicLayout from '@/layouts/basic-layout';
import Setting from '@/setting';

const pre = 'custom_';

export default {
    path: `${Setting.roterPre}/custom`,
    name: 'custom',
    header: 'custom',
    meta: {
        auth: ['admin-custom-index']  // 权限标识
    },
    component: BasicLayout,
    children: [
        {
            path: 'list',
            name: `${pre}customList`,
            meta: {
                title: '自定义列表',
                keepAlive: true,
                auth: ['admin-custom-list']
            },
            component: () => import('@/pages/custom/customList')
        },
        {
            path: 'add/:id?',
            name: `${pre}customAdd`,
            meta: {
                title: '添加数据',
                auth: ['admin-custom-add']
            },
            component: () => import('@/pages/custom/customAdd')
        }
    ]
};

在路由入口文件中注册:

// src/router/index.js

import Vue from 'vue';
import VueRouter from 'vue-router';
import custom from './modules/custom';  // 引入自定义路由

Vue.use(VueRouter);

// 路由列表
const routes = [
    // ... 其他路由
    custom,  // 添加自定义路由
];

export default new VueRouter({
    routes
});

3.4 第四步:配置菜单(可选)

如果需要在侧边栏显示菜单,可以在 src/menu/ 目录下配置。

// src/menu/modules/custom.js

export default {
    path: '/admin/custom',
    title: '自定义管理',
    header: 'custom',
    icon: 'ios-apps',
    children: [
        {
            title: '数据列表',
            path: '/admin/custom/list',
            auth: ['admin-custom-list']
        },
        {
            title: '添加数据',
            path: '/admin/custom/add',
            auth: ['admin-custom-add']
        }
    ]
};

3.5 第五步:创建后端接口

参考前面的路由添加说明文档,在后端创建对应的控制器和路由。

控制器示例:

// app/controller/admin/v1/custom/CustomController.php
<?php
namespace app\controller\admin\v1\custom;

use app\controller\admin\AuthController;
use app\services\custom\CustomServices;
use think\facade\App;

class CustomController extends AuthController
{
    protected CustomServices $services;

    public function __construct(App $app, CustomServices $services)
    {
        parent::__construct($app);
        $this->services = $services;
    }

    /**
     * 列表
     */
    public function index()
    {
        $where = $this->request->getMore([
            ['keyword', ''],
            ['status', ''],
            ['page', 1],
            ['limit', 15]
        ]);
        return app('json')->success($this->services->getList($where));
    }

    /**
     * 详情
     */
    public function info($id)
    {
        return app('json')->success($this->services->getInfo((int)$id));
    }

    /**
     * 保存
     */
    public function save($id = 0)
    {
        $data = $this->request->postMore([
            ['name', ''],
            ['desc', ''],
            ['sort', 0],
            ['status', 1]
        ]);

        $this->validate($data, \app\validate\admin\custom\CustomValidate::class);

        $this->services->saveData((int)$id, $data);

        return app('json')->success('保存成功');
    }

    /**
     * 删除
     */
    public function delete($id)
    {
        $this->services->delete((int)$id);
        return app('json')->success('删除成功');
    }

    /**
     * 修改状态
     */
    public function setStatus($id, $status)
    {
        $this->services->setStatus((int)$id, (int)$status);
        return app('json')->success('修改成功');
    }
}

后端路由配置:

// route/admin.php 中添加

Route::group('custom', function () {
    Route::get('list', 'v1.custom.CustomController/index')
        ->option(['real_name' => '自定义列表']);
    Route::get('info/:id', 'v1.custom.CustomController/info')
        ->option(['real_name' => '自定义详情']);
    Route::post('save/:id', 'v1.custom.CustomController/save')
        ->option(['real_name' => '保存自定义数据']);
    Route::delete('delete/:id', 'v1.custom.CustomController/delete')
        ->option(['real_name' => '删除自定义数据']);
    Route::put('set_status/:id/:status', 'v1.custom.CustomController/setStatus')
        ->option(['real_name' => '修改自定义状态']);
})->middleware([
    \app\http\middleware\admin\AdminAuthTokenMiddleware::class,
    \app\http\middleware\admin\AdminCkeckRoleMiddleware::class
])->middleware(\app\http\middleware\SystemLogMiddleware::class, 'admin');

3.6 第六步:配置后台菜单权限

登录后台管理系统,进行权限配置:

  1. 设置 → 权限管理 → 菜单管理
  2. 添加一级菜单:自定义管理
  3. 添加二级菜单:数据列表、添加数据
  4. 配置菜单对应的API路径和权限标识

四、常用组件使用

4.1 表格组件

<Table
  :loading="loading"
  :columns="columns"
  :data="tableData"
  border
  stripe
  @on-selection-change="handleSelectionChange"
  @on-sort-change="handleSortChange"
>
  <!-- 自定义列 -->
  <template slot-scope="{ row }" slot="customSlot">
    <span>{{ row.customField }}</span>
  </template>
</Table>

4.2 表单组件

<Form ref="form" :model="formData" :rules="rules" :label-width="100">
  <FormItem label="输入框:" prop="input">
    <Input v-model="formData.input" />
  </FormItem>

  <FormItem label="选择器:" prop="select">
    <Select v-model="formData.select">
      <Option v-for="item in options" :key="item.value" :value="item.value">
        {{ item.label }}
      </Option>
    </Select>
  </FormItem>

  <FormItem label="日期:" prop="date">
    <DatePicker v-model="formData.date" type="date" />
  </FormItem>
</Form>

4.3 图片上传组件

<template>
  <upload-from
    v-model="formData.image"
    :limit="1"
    @change="handleImageChange"
  />
</template>

<script>
import uploadFrom from '@/components/uploadPictures/uploadFrom';

export default {
  components: { uploadFrom },
  // ...
};
</script>

4.4 富文本编辑器

<template>
  <editor v-model="formData.content" />
</template>

<script>
import editor from '@/components/editor';

export default {
  components: { editor },
  // ...
};
</script>

五、开发规范

5.1 文件命名规范

  • 页面组件文件夹使用 camelCasecustomList
  • Vue文件使用 index.vuecamelCase
  • API文件使用 camelCasecustom.js
  • 路由模块使用 camelCasecustom.js

5.2 代码规范

  • 组件 name 属性使用 PascalCase
  • 路由 name 使用统一前缀:custom_customList
  • API函数使用动词开头:getList, saveData, deleteItem

5.3 接口返回格式

// 成功响应
{
    status: 200,
    msg: 'success',
    data: { ... }
}

// 列表响应
{
    status: 200,
    msg: 'success',
    data: {
        list: [...],
        count: 100
    }
}

// 错误响应
{
    status: 400,
    msg: '错误信息'
}

六、调试技巧

6.1 API调试

// 在API文件中添加日志
export function getCustomList(params) {
    console.log('请求参数:', params);
    return request({
        url: 'custom/list',
        method: 'get',
        params
    }).then(res => {
        console.log('响应数据:', res);
        return res;
    });
}

6.2 Vue Devtools

使用浏览器Vue Devtools扩展查看组件状态和Vuex数据。

6.3 网络请求

使用浏览器开发者工具Network面板查看API请求和响应。

七、常见问题

Q1: 页面路由404?

解决方案:

  1. 检查路由配置是否正确注册
  2. 检查路由路径是否正确
  3. 检查组件导入路径是否正确

Q2: API请求失败?

解决方案:

  1. 检查API地址是否正确
  2. 检查请求方法是否匹配
  3. 检查后端接口是否存在
  4. 查看控制台错误信息

Q3: 表单验证不生效?

解决方案:

  1. 确保FormItem有prop属性
  2. 确保rules中定义了对应的验证规则
  3. 确保v-model绑定正确

Q4: 页面缓存问题?

解决方案:

// 路由配置中设置keepAlive
meta: {
    keepAlive: true  // 启用缓存
}

// 或在组件中手动刷新
activated() {
    this.getList();
}
{{cateWiki.like_num}}人点赞
0人点赞
评论({{cateWiki.comment_num}}) {{commentWhere.order ? '评论从旧到新':'评论从新到旧'}} {{cateWiki.page_view_num}}人看过该文档
评论(0) {{commentWhere.order ? '评论从旧到新':'评论从新到旧'}} 9人看过该文档
评论
{{item.user ? item.user.nickname : ''}} (自评)
{{item.content}}
{{item.create_time}} 删除
{{item.like ? item.like.like_num : 0}} {{replyIndex == index ? '取消回复' : '回复'}}
评论
{{items.user ? items.user.nickname : '暂无昵称'}} (自评)
{{items.content}}
{{items.create_time}} 删除
{{items.like ? items.like.like_num : 0}} {{replyIndexJ == (index+'|'+indexJ) ? '取消回复' : '回复'}}
评论
目录
  • {{item}}