{{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 第六步:配置后台菜单权限
登录后台管理系统,进行权限配置:
- 设置 → 权限管理 → 菜单管理
- 添加一级菜单:自定义管理
- 添加二级菜单:数据列表、添加数据
- 配置菜单对应的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 文件命名规范
- 页面组件文件夹使用 camelCase:
customList - Vue文件使用
index.vue或 camelCase - API文件使用 camelCase:
custom.js - 路由模块使用 camelCase:
custom.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?
解决方案:
- 检查路由配置是否正确注册
- 检查路由路径是否正确
- 检查组件导入路径是否正确
Q2: API请求失败?
解决方案:
- 检查API地址是否正确
- 检查请求方法是否匹配
- 检查后端接口是否存在
- 查看控制台错误信息
Q3: 表单验证不生效?
解决方案:
- 确保FormItem有prop属性
- 确保rules中定义了对应的验证规则
- 确保v-model绑定正确
Q4: 页面缓存问题?
解决方案:
// 路由配置中设置keepAlive
meta: {
keepAlive: true // 启用缓存
}
// 或在组件中手动刷新
activated() {
this.getList();
}
评论({{cateWiki.comment_num}})
{{commentWhere.order ? '评论从旧到新':'评论从新到旧'}}
{{cateWiki.page_view_num}}人看过该文档
评论(0)
{{commentWhere.order ? '评论从旧到新':'评论从新到旧'}}
9人看过该文档
{{item.user ? item.user.nickname : ''}}
(自评)
{{item.content}}
{{item.create_time}}
删除
搜索结果
为您找到{{wikiCount}}条结果
{{item.page_view_num}}
{{item.like ? item.like.like_num : 0}}
{{item.comment ? item.comment.comment_num : 0}}
位置:
{{path.name}}
{{(i+1) == item.catalogue.path_data.length ? '':'/'}}