无需编译:
2type/admin 可以作为管理后台框架,也可以作为前端快速开发的原型框架.
无需 node webpack gulp 构建工具,真正意义的下载即用,能降低复杂度.
对后端友好,不是很熟悉前端的后端只需要复制粘贴即可完成管理后台的前端开发. 复杂的需求让前端在 2type/admin 的框架之上开发即可.
编译在前端正式项目中是必须的,但是在管理后台就显得非常繁琐.反而会降低开发效率,管理后台应该是让后端能参与的.
基于Vue生态
使用非常成熟且易用的 vue2 作为基础框架. 既能快速开发,又能使用 element 这样成熟的组件库.
vue3 不适合管理后台这样无需编译的场景,并且 vue2 的生态目前比 vue3 成熟
灵活自由
提供了 _list _submit _find 等方法来解决常用的 列表页面, 表单页面, 字典查询等需求
在实际角度出发,管理后台框架的重点不在于组件库有多少,而是要快速的配置简单的增删查改页面
安全性
建议不要采用 vue-router 开发内部管理后台,因为 vue-router 会让页面大部分代码在js中. 一旦被发现内部管理后台的页面内容,会导致业务运作模式与商业机密泄露.
下载 https:/2type/admin/archive/refs/heads/main.zip
- project.js 项目配置
- project.auth.js 项目配置
授权(登录)后才能访问的项目配置
- project.css 项目样式
- 2type/** 项目所需静态资源,不要改动这里的代码
- view/layout/page.html 模板布局文件,可根据项目后端语言修改成对应的后端模板
- view/login.html 登录页面
- view/home.html 首页
- view/demo_list.html 列表页面
- view/demo_form.html 创建和编辑页面 通过 __RENDER_DATA.formKind 控制创建和编辑的区别
项目中可直接使用 import
导入js模块,但要注意一定要在 <script type="module" >
标签的起始处, 必须有 type="module"
.
在线 esm 模块: https://www.skypack.dev/
国内加速镜像: https://esm.2type.cn/
因为 Vue 模板是写在html中,而不是通过编译生成.所以应该确保每个标签都是有起始和结束.
<!--正确-->
<el-switch></el-switch>
<!--错误-->
<el-switch/>
<!--错误-->
<el-switch>
如果不按照正确的语法,会导致页面无法正常显示,或显示错误.
页面中 __RENDER_DATA
变量是用来存放后端渲染数据的
前端开发人员可以跳过后端数据这一部分内容,让后端同事看这一部分内容
某后端模板
<script>
window.__RENDER_DATA = [[raw(xjson(.))]]
</script>
某后端模板引擎渲染语法
var data = {
list: [{name:"2type"}]
}
var t = view.getTemplate("list.html")
t.render(data)
渲染结果
<script>
window.__RENDER_DATA = {list: [{name:"2type"}]}
</script>
vm 指的是
new Vue({...})
创建的实例
将 __RENDER_DATA 的数据导入 vm.data
{
...__RENDER_DATA,
}
将 TA.m 的数据导入 vm.methods
methods: {
...TA.m,
},
如果你误删了
...__RENDER_DATA,
或者...TA.m,
可能会导致某些功能失效
- main.go go 语言示例环境,可以安装 https://golang.org/ 后在当前目录运行 go run main.go
页面
<ta-page :header="header">
</ta-page>
const header = [
{
title: "首页",
url: TA.m.url_home(),
},
{
title: "列表",
// 一般最后一个参数 url 可留空
},
]
导航部分在 ./project.js 中通过 TA.nav 控制
TA.nav = {
// 头部
top: {
logo: 'https://2type.nimo.run/icon/logo.svg',
},
// 退出登录
logoutURL: TA.m.url_logout(),
// 导航
items: [
{
url: TA.m.url_home(),
// 可在 https://icon.2type.cn/ 中寻找 icon
icon: 'display',
title: "首页",
},
{
icon: 'shopping',
title: "演示",
items: [
{
// 可在 http://www.fontawesome.com.cn/faicons/ 中寻找fontawesome
fontawesome: 'list-alt',
url: TA.m.url_demo_list(),
title: '列表',
},
{
fontawesome: 'pencil-square-o',
url: TA.m.url_demo_create(),
title: '创建',
},
]
},
]
}
底部部分在 ./project.js 中通过 TA.footer 控制
TA.footer = {
link: {
text: "@2type/admin",
href: "https:/2type/admin",
}
}
盒子
<ta-box title="商品列表">
内容
</ta-box>
你可以在一个页面放多个 ta-box
<el-row :gutter="20">
<el-col :span="12">
<ta-box title="Top5 客户">
内容1
</ta-box>
</el-col>
<el-col :span="12">
<ta-box title="Top5 员工">
内容2
</ta-box>
</el-col>
</el-row>
还可在右上角增加一些元素
<ta-box title="商品列表">
<template slot="tools">
<el-button @click="_jump(url_demo_create())" type="primary" >创建</el-button>
</template>
内容
</ta-box>
富文本编辑器
<ta-editor v-model="form.content" photo="/admin/upload/photo" > ></ta-editor>
photo 参数用来配置图片上传的地址
可以在 ./project.js 中修改 insertImage
匹配后端响应并插入图片
// 配置富文本编辑器的上传图片插入
TA.hook.editor.insertImage = function (res, insert) {
insert(res.data.src)
}
<ta-upload v-model="form.photo" action="/admin/upload/photo" ></ta-upload>
<ta-upload v-model="form.file" action="/admin/upload/file" ></ta-upload>
{
form: {
title: 'abc',
file: {
filename: "abc.csv",
id: "some_uuid",
},
photo: {
src: "https://picsum.photos/100",
id: "https://picsum.photos/100",
},
}
}
通过响应 src 来实现上传图片
POST /admin/upload/photo
{
"src": "https://picsum.photos/100",
"id": "https://picsum.photos/100",
"error": {
"code": 0,
"message": ""
}
}
通过响应 filename 来实现上传文件
POST /admin/upload/file
{
"filename": "abc.pdf",
"id": "some_id",
"error": {
"code": 0,
"message": ""
}
}
与 upload 类似,区别是可以上传多个文件
<ta-upload-list v-model="form.photoList" action="/admin/upload/photo" ></ta-upload-list>
<ta-upload-list v-model="form.fileList" action="/admin/upload/file" ></ta-upload-list>
{
form: {
photoList: [
{
src: "https://picsum.photos/100",
id: "https://picsum.photos/100",
},
],
fileList: [
{
filename: "abc.csv",
id: "some_uuid",
}
]
}
}
地区选择
<ta-region v-model="form.region"></ta-region>
地区限制通过
- ♾️不限地区
- ✅包含区域
- 🚫排除区域
- 🔗组合模式
四种模式加上地区选择器,可以简单灵活的配置地区限制,如果后端使用mongo则可以快速完成地区筛选.
<ta-lbs-limit v-model="form.area"></ta-lbs-limit>
不同选择 form.area
对应的值
<!-- 可以通过 :debug="true" 查看所有省市区结构 --> <ta-lbs-limit :debug="true" v-model="form.area"></ta-lbs-limit>
它们的ID存储的是 adcode 行政区划
// ♾️不限地区
{"type":"unlimited","inverse":[],"selected":[]}
// ✅包含区域
{"type":"selected","inverse":[],"selected":["110000","110101"]}
🚫排除区域
{"type":"inverse","inverse":["120102","120103"],"selected":[]}
// 🔗组合模式
{"type":"selectedAndInverse","inverse":["110101"],"selected":["110000","120000"]}
对应的 mongoDB 代码
集合的数据结构
{
"advertisingID": 1,
"areaLimit": {
"type": "unlimited",
"inverse": [],
"selected": []
}
}
假设用户的位置是 310113
查询1,2这两个广告
advertisingID:{$in: [1,2]}
并且$and
它们的限制规则必须至少满足以下4项的其中一项$or
- 包含模式的包含区域中存在
310112
- 排除模式的排除区域中不存在
310112
- 组合模式的包含区域中存在
310112
并且 排除区域中不存在310112
- 不限地区
// adcode = ["310112"]
// adcode = []
var areaLimit = [{"areaLimit.type": "unlimited",}]
if (adcode.length != 0) {
areaLimit.push({
"areaLimit.type": "selected",
"areaLimit.selected": {$in: adcode},
})
areaLimit.push({
"areaLimit.type": "inverse",
"areaLimit.inverse": {$in: adcode},
})
areaLimit.push({
"areaLimit.type": "selectedAndInverse",
"areaLimit.selected": {$in: adcode},
"areaLimit.inverse": {$in: adcode},
})
}
db.advertisingRule.find({
$and: [{
"advertisingID": {
$in: [1,2,]
}
},
{
$or: areaLimit,
}]
})
你可能还需要使用 https:/goclub/lbs 来查询一些地区的上下级
ta-input-fen 在用户输入时使用2位数浮点数(元),保存数据时使用整数(分).
<ta-input-fen :min="0" v-model="form.amount" :step="1"></ta-input-fen>元
<span v-if="form.amount">({{form.amount}}分)</span>
ta-input-fen 底层使用 el-input-number, 你可以在 ta-input-fen 上配置 el-input-number 的所有属性
2type/admin 中已经集成了 element-ui 可直接使用无需引用
<div ref="mktChartNode" style="height:300px" ></div>
mounted() {
const vm = this
var chart= TA.echarts.init(vm.$refs.mktChartNode);
// 配置参考 https://echarts.apache.org/examples/zh/editor.html?c=line-stack
const option = {tooltip: {trigger: 'axis'}, grid: {left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: {type: 'category', boundaryGap: false, data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] }, yAxis: {type: 'value'}, series: [{name: '邮件', type: 'line', stack: 'Total', data: [120, 132, 101, 134, 90, 230, 210] }, {name: '广告', type: 'line', stack: 'Total', data: [220, 182, 191, 234, 290, 330, 310] }, {name: '视频', type: 'line', stack: 'Total', data: [150, 232, 201, 154, 190, 330, 410] }, {name: '短信', type: 'line', stack: 'Total', data: [320, 332, 301, 334, 390, 330, 320] }, {name: '搜索引擎', type: 'line', stack: 'Total', data: [820, 932, 901, 934, 1290, 1330, 1320] } ] };
chart.setOption(option);
},
时间处理 https://dayjs.gitee.io/docs/zh-CN/manipulate/manipulate
TA.dayjs = dayjs
URL query 解析 https://www.npmjs.com/package/query-string/v/7.0.0
TA.qs = qs
http 请求(建议使用) https://axios-http.com/zh/docs/api_intro
TA.axios = axios
TA.m 会挂载在 vm 中的 methods, 这样在模板中就可以直接使用
new Vue({
// ...
methods: {
...TA.m,
}
})
例如 TA.m 中定义了 _jump
方法
<el-button @click="_jump('https:/2type/admin')" >创建</el-button>
在页面点击创建按钮后即可跳转至 https:/2type/admin
在模板中使用
<el-button @click="_jump('https:/2type/admin')" >创建</el-button>
在 methods 中使用
new Vue({
methods: {
...TA.m,
clickBtn() {
const vm = this
vm._jump('https:/2type/admin')
}
}
})
与 TA.m._jump()
方法类似,不同处在于 TA.m._open()
会打开新页面
在 ./project.js 中以 TA.m.url_
作为前缀配置项目路由,用于同一管理跳转路径
TA.m.url_home = function () {
return "/admin/home"
}
TA.m.url_demo_update = function (id) {
return "/admin/demo_update?id=" + id
}
这样在模板中可以直接使用
无参数
<el-button @click="_jump(url_demo_create())" >创建</el-button>
有参数
<el-table-column label="操作">
<template slot-scope="scope">
<el-button size="mini" @click="_jump(url_demo_update(scope.row.id))" >编辑</el-button>
</template>
</el-table-column>
页面初始参数
例如有些列表页面需要时间范围默认选中最近7天,则可以通过如下代码实现.
TA.m.url_demo_list = function () {
var value= TA.m._encodeJSONQuery({
daterange: [
TA.dayjs().subtract(6, 'day').format("YYYY-MM-DD"),
TA.dayjs().format("YYYY-MM-DD")
]
})
return "/admin/demo_list?json=" + value
}
返回页面 GET 参数
模板中使用
在 https://domain.com/path?id=abc
页面中
<div>
ID: {{_query().id}}
</div>
在 methods 中使用
new Vue({
methods: {
...TA.m,
some() {
const vm = this
console.log("id:", vm._query().id)
}
}
})
在创建和编辑页面需要配置 formKind
,值可以是"create"
或 "update"
.
可以使用 :disabled="formKind == 'update'"
让某些组件在编辑页面中无法编辑
<el-select v-model="form.type" :disabled="formKind == 'update'" >
返回 formKind 对应的中文
直接看源码吧
TA.m._formKindLabel = function() {
let map = {
'create': "创建",
'update': "编辑",
}
let vm = this
let query = qs.parse(location.search)
let RenderData = __RENDER_DATA
let key = vm.formKind || query.formKind || RenderData.formKind
return map[key] || '提交'
}
由前端从 url query 的 json 中获取搜索参数
let url = "http://domain.com/admin/demo_list?json=%7B%22title%22%3A%22%E6%A0%87%E9%A2%981%22%7D"
encodeURIComponent('{"title": "标题1"}')) == "%7B%22title%22%3A%22%E6%A0%87%E9%A2%981%22%7D"
new Vue({
// ...
data: function () {
const out = {
...__RENDER_DATA,
search:TA.m._readSearch(),
header: header,
}
return out
},
})
<el-form :inline="true" >
<el-form-item label="标题">
<el-input v-model="search.title"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="_list(search)">查询</el-button>
</el-form-item>
</el-form>
源码也比较简单
TA.m._readSearch = function() {
if (!qs.parse(location.search).json) {
return {}
}
return JSON.parse(qs.parse(location.search).json)
}
发起 HTTP 请求
使用示例
TA.m._req({
$loading: false, // 可以通过 $loading: false 取消 loading 遮罩层
method:"get",
url: vm.url_mobile_home(),
}, function passCallback (res) {
console.log(res)
})
TA.m._req({
$loading: false, // 可以通过 $loading: false 取消 loading 遮罩层
method:"get",
url: vm.url_mobile_home(),
}, function passCallback (res) {
console.log(res)
}, function failCallback() {
}, function alwaysCallback() {
})
TA.m._req({
$loading: false, // 可以通过 $loading: false 取消 loading 遮罩层
method:"post",
url: vm.url_mobile_home(),
data:{
name:"nimo",
age:18
}
}, function passCallback (res) {
console.log(res)
}, function failCallback() {
}, function alwaysCallback() {
})
config 参数是 axios 的参数.
passCallback
failCallback
是请求成功失败的回调,可以通过 在 ./project.js 中修改
TA.hook.req.handleError
来匹配后端接口.
后端操作成功响应:
{"error": {"code": 0, "message": ""}}
后端操作失败响应:
{"error": {"code": 1, "message": "标题重复"}}
TA.hook.req.handleError = function (res, passCallback, failCallback) {
// 数据格式补全
res.data.error = res.data.error || {}
res.data.error.code = res.data.error.code || 0
// 失败判断
if (res.data.error.code) {
failCallback(res)
return true
} else {
res.data.error = undefined
passCallback(res)
return false
}
}
passCallback
和 failCallback
都有默认参数,在控制台中输入下面的代码查看
console.log(TA.hook.req.passCallback.toString())
console.log(TA.hook.req.failCallback.toString())
默认的 passCallback
根据响应参数提供了一些便捷方法
控制跳转到任意地址
{"jump":"https:/2type/admin", "code":0, "message":""}
控制跳转到 TA.m 中的函数
{"jump":"url_home()", "code":0, "message":""}
控制跳转到 TA.m 中的函数 带参数
{"jump":"url_demo_update()", "jumpArgs": [1], "code":0, "message":""}
成功提示
{"successMessage": "创建成功","code":0, "message":""}
提交数据到当前页面
2type/admin 对常见的创建编辑进行了封装,配合
__submit
可以非常方便的实现创建编辑
在模板中使用
<el-form-item>
<el-button type="primary" @click="_submit(form)">提交</el-button>
</el-form-item>
如上代码在点击提交后可提交 vm.form 到当前页面
自定义 passCallback
<el-button type="primary" @click="_submit(form, submitPass)">提交</el-button>
new Vue({
methods: {
...TA.m,
submitPass: function (res) {
console.log(res)
}
},
})
与 TA.m._submit
相同,区别是可以通过 url 配置请求地址
2type/admin 对常见的列表分页进行了封装,配合
_list
可以非常方便的实现列表分页.
列表跳转专用(请求当前页)
_list(search, page, perPage)
会获取 data 跳转至 path?json=data
,后端获取 URL query 中的 json 作为查询条件, query 中会包含 page(页码).
按条件查询
<el-button type="primary" @click="_list(search)">查询</el-button>
按条件查询并翻页
<el-pagination
:total="total"
:current-page="Number(search.page)"
@current-change="_list(search, $event, null)"
@size-change="_list(search, null, $event)"
:page-size="Number(search.perPage) || 10"
style="text-align: center;padding:1em;"
background
layout="prev, pager, next, sizes"
>
</el-pagination>
与 TA.m._list
相同,区别是可以通过 path 配置请求地址
导出
某些页码除了要实现列表翻页还需要实现导出搜索结果,此时可以使用 _export(search)
.
它与 _list(search)
功能类似,区别在于会在新页面打开页面便于后端返回文件下载.
并且 URL query json 参数中会包含 "export":true
, 后端可根据 export 参数判断是导出数据还是搜索分页
<el-form-item>
<el-button type="info" @click="_export(search)">导出</el-button>
</el-form-item>
与 TA.m._export
相同,区别是可以通过 path 配置请求地址
TA.m._enum = function () {
return TA.enum
}
// project.auth.js 中配置各种 enum
TA.enum.skuType = [
{
key: 'digit',
value: 1,
label: '虚拟',
},
{
key: 'object',
value: 2,
label: '实物',
},
]
_enum()
最常用的场景是渲染 select
<el-select v-model="form.type" :disabled="formKind == 'update'" >
<!-- _enum().skuType 在 project.js 中配置 -->
<el-option v-for="(item, key) in _enum().skuType" :key="item.key" :value="item.value" :label="item.label"></el-option>
</el-select>
_find 可配置 enum 使用,例如 enum 配置如下:
TA.enum.skuType = [
{
key: 'digit',
value: 1,
label: '虚拟',
},
{
key: 'object',
value: 2,
label: '实物',
},
]
_find("skuType", 2).label // 实物
_find("skuType", 2).key // object
_find("skuType", 2).id // 2
_find("skuType", "object").label // 实物
_find("skuType", 'object').key // object
_find("skuType", 'object').id // 2
在 el-table 中将 1
2
转换为"虚拟""
"实物"
_find("skuType", scope.row.type).label
<el-table-column label="类型">
<template slot-scope="scope">
{{_find("skuType", scope.row.type).label}}
</template>
</el-table-column>
你也可以直接使用 vue data 中定义的数据
{
option: {
user: [
{
userID: 1,
userName: "张三",
},
{
userID: 2,
userName: "李四",
}
]
}
}
_find(option.user, 1).userName // 张三
_find(option.user, 2).userName // 李四
在 el-table 中将 1
2
转换为"张三""
"李四"
_find(option.user, scope.row.userID).userName
<el-table-column label="用户">
<template slot-scope="scope">
{{_find(option.user, scope.row.userID).userName}}
</template>
</el-table-column>
将对象转换为 URL query 值
var url = `/list?json=${TA.m._encodeJSONQuery({'name':'2type'})}`
# '/list?json=%7B%22name%22%3A%222type%22%7D'
MongoDB ObjectID 转换为时间
{{_objectIDToDate("507f1f77bcf86cd799439011")}}
# Thu Oct 18 2012 05:13:27 GMT+0800 (中国标准时间)
Date 对象或时间字符串格式化
{{_dateFormat("Thu Oct 18 2012 05:13:27 GMT+0800 (中国标准时间)")}}
# 2012-10-18 05:13:27
配合_objectIDToDate使用可将ObjectID转换为格式化后的日期字符串
{{_dateFormat(_objectIDToDate("507f1f77bcf86cd799439011"))}}
# 2012-10-18 05:13:27