无需编译:
2type/admin 可以作为管理后台框架,也可以作为前端快速开发的原型框架.
无需 node webpack gulp 构建工具,真正意义的下载即用,能降低复杂度.
对后端友好,不是很熟悉前端的后端只需要复制粘贴即可完成管理后台的前端开发. 复杂的需求让前端在 2type/admin 的框架之上开发即可.
编译在前端正式项目中是必须的,但是在管理后台就显得非常繁琐.反而会降低开发效率,管理后台应该是让后端能参与的.
基于Vue生态
使用非常成熟且易用的 vue2 作为基础框架. 既能快速开发,又能使用 element 这样成熟的组件库.
vue3 不适合管理后台这样无需编译的场景,并且 vue2 的生态目前比 vue3 成熟
灵活自由
提供了 _list _submit _find 等方法来解决常用的 列表页面, 表单页面, 字典查询等需求
在实际角度出发,管理后台框架的重点不在于组件库有多少,而是要快速的配置简单的增删查改页面
安全性
建议不要采用 vue-router 开发内部管理后台,因为 vue-router 会让页面大部分代码在js中. 一旦被发现内部管理后台的页面内容,会导致业务运作模式与商业机密泄露.
下载 https://github.com/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://esm.run/
因为 Vue 模板是写在html中,而不是通过编译生成.所以应该确保每个标签都是有起始和结束.
<!--正确--><el-switch></el-switch><!--错误--><el-switch/><!--错误--><el-switch>如果不按照正确的语法,会导致页面无法正常显示,或显示错误.
页面中 __RENDER_DATA 变量是用来存放后端渲染数据的
前端开发人员可以跳过后端数据这一部分内容,让后端同事看这一部分内容
某后端模板
<script>window.__RENDER_DATA=[[raw(xjson(.))]]</script>某后端模板引擎渲染语法
vardata={list: [{name:"2type"}]}vart=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>constheader=[{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/ 中寻找 iconicon: 'display',title: "首页",},{icon: 'shopping',title: "演示",items: [{// 可在 http://www.fontawesome.com.cn/faicons/ 中寻找fontawesomefontawesome: '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://github.com/2type/admin",}}盒子
<ta-boxtitle="商品列表">
内容
</ta-box>你可以在一个页面放多个 ta-box
<el-row:gutter="20"><el-col:span="12"><ta-boxtitle="Top5 客户">
内容1
</ta-box></el-col><el-col:span="12"><ta-boxtitle="Top5 员工">
内容2
</ta-box></el-col></el-row>还可在右上角增加一些元素
<ta-boxtitle="商品列表"><templateslot="tools"><el-button@click="_jump(url_demo_create())" type="primary" >创建</el-button></template>
内容
</ta-box>富文本编辑器
<ta-editorv-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-uploadv-model="form.photo" action="/admin/upload/photo" ></ta-upload><ta-uploadv-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": ""
}
}需要直接存储文件网址可以使用 only-id="true 让 v-model 的值是,字符串格式"id"
而不是 {id:"id",src:"src",filename:"filename"} 格式
<ta-uploadv-model="form.uploadOnlyId" only-id="true" action="/admin/upload/photo" ></ta-upload>与 upload 类似,区别是可以上传多个文件
<ta-upload-listv-model="form.photoList" action="/admin/upload/photo" ></ta-upload-list><ta-upload-listv-model="form.fileList" action="/admin/upload/file" ></ta-upload-list><ta-upload-listonly-id="true" v-model="form.uploadOnlyIdList" action="/admin/upload/photo" ></ta-upload-list>{form: {photoList: [{src: "https://picsum.photos/100",id: "https://picsum.photos/100",},],fileList: [{filename: "abc.csv",id: "some_uuid",}],uploadOnlyIdList:[],}}地区选择
<ta-region v-model="form.region"></ta-region>
地区限制通过
- ♾️不限地区
- ✅包含区域
- 🚫排除区域
- 🔗组合模式
四种模式加上地区选择器,可以简单灵活的配置地区限制,如果后端使用mongo则可以快速完成地区筛选.
<ta-lbs-limitv-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://github.com/goclub/lbs 来查询一些地区的上下级
ta-input-fen 在用户输入时使用2位数浮点数(元),保存数据时使用整数(分).
<ta-input-fen:min="0" v-model="form.amount" :step="1"></ta-input-fen>元
<spanv-if="form.amount">({{form.amount}}分)</span>ta-input-fen 底层使用 el-input-number, 你可以在 ta-input-fen 上配置 el-input-number 的所有属性
2type/admin 中已经集成了 element-ui 可直接使用无需引用
<divref="mktChartNode" style="height:300px" ></div>mounted(){constvm=thisvarchart=TA.echarts.init(vm.$refs.mktChartNode);// 配置参考 https://echarts.apache.org/examples/zh/editor.html?c=line-stackconstoption={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=dayjsURL query 解析 https://www.npmjs.com/package/query-string/v/7.0.0
TA.qs=qshttp 请求(建议使用) https://axios-http.com/zh/docs/api_intro
TA.axios=axiosTA.m 会挂载在 vm 中的 methods, 这样在模板中就可以直接使用
newVue({// ...methods: {
...TA.m,}})例如 TA.m 中定义了 _jump 方法
<el-button@click="_jump('https://github.com/2type/admin')" >创建</el-button>在页面点击创建按钮后即可跳转至 https://github.com/2type/admin
在模板中使用
<el-button@click="_jump('https://github.com/2type/admin')" >创建</el-button>在 methods 中使用
newVue({methods: {
...TA.m,clickBtn(){constvm=thisvm._jump('https://github.com/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-columnlabel="操作"><templateslot-scope="scope"><el-buttonsize="mini" @click="_jump(url_demo_update(scope.row.id))" >编辑</el-button></template></el-table-column>页面初始参数
例如有些列表页面需要时间范围默认选中最近7天,则可以通过如下代码实现.
TA.m.url_demo_list=function(){varvalue=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 中使用
newVue({methods: {
...TA.m,some(){constvm=thisconsole.log("id:",vm._query().id)}}})在创建和编辑页面需要配置 formKind ,值可以是"create" 或 "update".
可以使用 :disabled="formKind == 'update'" 让某些组件在编辑页面中无法编辑
<el-selectv-model="form.type" :disabled="formKind == 'update'" >返回 formKind 对应的中文
直接看源码吧
TA.m._formKindLabel=function(){letmap={'create': "创建",'update': "编辑",}letvm=thisletquery=qs.parse(location.search)letRenderData=__RENDER_DATAletkey=vm.formKind||query.formKind||RenderData.formKindreturnmap[key]||'提交'}由前端从 url query 的 json 中获取搜索参数
leturl="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"newVue({// ...data: function(){constout={
...__RENDER_DATA,search:TA.m._readSearch(),header: header,}returnout},})<el-form:inline="true" ><el-form-itemlabel="标题"><el-inputv-model="search.title"></el-input></el-form-item><el-form-item><el-buttontype="primary" @click="_list(search)">查询</el-button></el-form-item></el-form>源码也比较简单
TA.m._readSearch=function(){if(!qs.parse(location.search).json){return{}}returnJSON.parse(qs.parse(location.search).json)}发起 HTTP 请求
使用示例
TA.m._req({$loading: false,// 可以通过 $loading: false 取消 loading 遮罩层method:"get",url: vm.url_mobile_home(),},functionpassCallback(res){console.log(res)})TA.m._req({$loading: false,// 可以通过 $loading: false 取消 loading 遮罩层method:"get",url: vm.url_mobile_home(),},functionpassCallback(res){console.log(res)},functionfailCallback(){},functionalwaysCallback(){})TA.m._req({$loading: false,// 可以通过 $loading: false 取消 loading 遮罩层method:"post",url: vm.url_mobile_home(),data:{name:"nimo",age:18}},functionpassCallback(res){console.log(res)},functionfailCallback(){},functionalwaysCallback(){})config 参数是 axios 的参数.
passCallbackfailCallback 是请求成功失败的回调,可以通过 在 ./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)returntrue}else{res.data.error=undefinedpassCallback(res)returnfalse}}passCallback 和 failCallback 都有默认参数,在控制台中输入下面的代码查看
console.log(TA.hook.req.passCallback.toString())console.log(TA.hook.req.failCallback.toString())默认的 passCallback 根据响应参数提供了一些便捷方法
控制跳转到任意地址
{"jump":"https://github.com/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-buttontype="primary" @click="_submit(form)">提交</el-button></el-form-item>如上代码在点击提交后可提交 vm.form 到当前页面
自定义 passCallback
<el-buttontype="primary" @click="_submit(form, submitPass)">提交</el-button>newVue({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-buttontype="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;"
backgroundlayout="prev, pager, next, sizes"
></el-pagination>与 TA.m._list 相同,区别是可以通过 path 配置请求地址
导出
某些页码除了要实现列表翻页还需要实现导出搜索结果,此时可以使用 _export(search) .
它与 _list(search) 功能类似,区别在于会在新页面打开页面便于后端返回文件下载.
并且 URL query json 参数中会包含 "export":true , 后端可根据 export 参数判断是导出数据还是搜索分页
<el-form-item><el-buttontype="info" @click="_export(search)">导出</el-button></el-form-item>与 TA.m._export 相同,区别是可以通过 path 配置请求地址
TA.m._enum=function(){returnTA.enum}// project.auth.js 中配置各种 enumTA.enum.skuType=[{key: 'digit',value: 1,label: '虚拟',},{key: 'object',value: 2,label: '实物',},]_enum() 最常用的场景是渲染 select
<el-selectv-model="form.type" :disabled="formKind == 'update'" ><!-- _enum().skuType 在 project.js 中配置 --><el-optionv-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 中将 12 转换为"虚拟"""实物"
_find("skuType", scope.row.type).label
<el-table-columnlabel="类型"><templateslot-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 中将 12 转换为"张三"""李四"
_find(option.user, scope.row.userID).userName
<el-table-columnlabel="用户"><templateslot-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