vpagecrud

0.8.0 • Public • Published

vPageCRUD

介绍

基于vue3和elementPlus的可快速通过json配置的形式,开发增删改查页面的项目。

更新日志
v0.6.5 - v0.7.4  2024-12-22
   1, 添加上传文件时即时删除功能。
   2,弹框组件支持展示 table 类型数据
   3, 优化 table 组件操作列中按钮样式
   4, 添加_matchType_字段收集在query配置时 prop 字段的匹配值:like/eq/..,请求时处理的默认参数
   5, 解决表单重复添加问题,itemSlotIdx 至少配置为 1
   6, 添加支持 table组件,前端分页功能

v0.4.1 - v0.6.4 2024-12-16
   1, 适配项目, fetch钩子添加responseStatusKey/successCode/resultMsg/successLogic/responseType(json)字段
   2, 优化 emit(up)处理函数
   3,修复 table 组件翻页 bug、操作栏单独渲染并添加宽度和默认固定右侧配置
   4, 修复 dialog 多层弹框 bug

v0.3.9 - 0.4.0 2024-11-30
   1, 修复在没有声明 slot 时,意外添加formItem的 bug
   2, 各组件operate中添加 hookFn 配置,直接调用通过最外层组件传入的hooks对象中的函数
   3,修复 dialog 组件数据不跟随点击更新的 bug

v0.3.4 - v0.3.8 2024-11-29
   1, 添加对外提供/query、closeDialog、openDialog、getDialogData、updateTableData、getTableSelection,   	 getTableLength, refreshTable, updateTableData方法。
   2,dailog组件修改getDialogData方法为 getData
   3,添加监听 form 组件的按钮
   4,对外提供的 event 监听方法,添加更多监听项:query组件、dialog组件、table组件的operation、pagination(实时)

v0.3.3 2024-11-11
   1,对外开放 getTableSelection/getTableLength/getQueryData方法,支持外部使用
   2, 添加v3Operation的插槽功能,支持配置items:表单结构(array)、operationSlotIdx:插入表单位置,支持 array
   3, 新增配置文件中 config 字段,可区别配置 table /query /operation /dialog

v0.3.1 - v0.3.2  2024-11-01
   1,添加v3Form的插槽功能,支持配置items:表单结构(array)、itemSlotIdx:插入表单位置,支持 array
   2,添加v3Table的插槽功能,支持配置columns:表格结构(array)、columnSlotIdx:插入列位置,支持 array
   3,添加v3Query的插槽功能,支持配置items:表单结构(array)、querySlotIdx(内部使用时为querySlotIdx字段,区分于v3Form的itemSlotIdx):插入表单位置,支持 array
   4,开放v3Form/v3Table/v3Query/pageInfo,支持外部使用
   5,v3Dialog 无法支持 Slot,dialog组件是动态生成的,一个视图下面可能会包含 n 多个 dialog,插槽位置无法固定,所以不支持

v0.3.0 2024-08-05
   1,添加支持tabs下,标签页根据配置的逻辑(showLogic)显示

v0.2.7 - v0.2.9 2024-07-11
   1, 解决el-input配置clearable时,再输入内容后,自动变长的问题
   2,优化点击查询按钮后,自动查询第一页数据
   3,添加自动解析get请求api中包含${}中的内容
   4, table组件中添加pageInfo的storage和assert数据,补充列数据
   5,添加支持operatios中配置dropdownMenus字段(按钮下拉的形式<dropdown>)
   6,form/table组件添加支持prop字段的x.xx.xxx取值方式。form组件提交时自动转换为{x:{xx:{xxx:1}}}


v0.2.6 2024-07-08
   1, 优化fields中字段配置,select组件和table组件共享config中的enums属性,避免冗余
   2, 优化select的选中逻辑,改为优先选中enums中的key,config中配置reqVal的值为“value"时,选中enums中的value
   3, 优化form表单的checkbox组件
   4, 添加rangeFnExcuteJs方法,参数是配置rangeFn函数的返回值,增强rangeFn的额外执行能力
   5, fetch请求默认拦截['0', '400', '401', '403', '404', '500']中的code


v0.2.2-v0.2.5: 2024-07-03
   1,优化报错提示信息
   2,添加按钮的显示逻辑operationLogic,可根据上下文控制是否显示该字段(上下文具体是_assert_、_storage_所携带的字段)
   3,弹框的fileds添加showLogin属性,可根据上下文控制弹框内字段在初次渲染时,是否显示
   4, operations对象下的query数组的操作属性添加支持notImmediate: true,打开页面时,不立即执行请求操作
   5,上下文信息是从地址栏和浏览器缓存中获取请注意编码问题

   
v0.2.1:2024-03-15
    1,修改查询时携带地址栏的_query_数据bug
    2,dialog添加模板解析,添加字段:headTip/footTip,支持字符串、html代码:<div>我是底部${data}</div>
    3,添加对外暴露event事件,补充组件的完整性

软件架构

项目整体采用widget、component、page三层结构:

page 解析schema对象,针对解析结果做页面级的效果,比如:是否需要标签页、解析地址栏参数、添加字段的展示逻辑、处理各个子组件之间的调用等;

range 主要集成了各个widget,并根据schema解析后的数据控制widget的展示隐藏、默认数据(config)提前处理.

widget 主要是对elementPlus中表单组件的增强和融合,对外提供各个零碎功能;如:asyncSelect、datePicker、selectTree等;

安装教程

  1. yarn add vpagecrud
  2. import vPagecrud from vpagecrud
  3. app.use(vpagecrud)

使用说明

  1. vPagecrud组件接收schema对象(对象字段如下展示),对外暴露fetch方法处理请求错误时的特殊需求。
  2. 页面包含搜索区、页码区、table展示区、操作按钮区。
  3. 暴露tool对象,可方便项目其他地方使用。
  4. fetch请求使用Axios插件,baseURL配置为 VITE_BASE_URL || BASE_URL || '/',可灵活使用
结构
page: 整个页面,组装range
range: page下的各个区块,组装widget;值包含:table/dialog/query/operation
    |- table: 表格, 提供的方法:update--更新当前table,getSelection--获取勾选的值,getLength--获取勾选的长度,delete--删除当前项
    |- dialog:弹框,提供的方法:close--关闭当前dialog,open--打开当前dialog,submit--提交弹框内formData,getData--获取弹框内formData
    |- query:查询,提供的方法:getData--获取所有查询formdata, reset--重置查询formdata
    |- operation:操作,提供的方法:
widget: 完成某一功能的具体(最小)单元

title:标题(单页面时可省,多标签页面时必填)
showLogic:显示逻辑,用于控制在初次渲染时是否显示当前tab页,支持两种方式:
    1. 直接赋值一个布尔值,表示是否显示该部分,默认展示;
    2. 赋值一个函数体,该函数返回一个布尔值,取值是地址栏中的_assert_对象和浏览器的storage对象。
fields:一级fields对象
operations: 操作按钮
    |- default: 默认操作区(列表头部所有按钮)
        |- label:按钮名称
        |- actOn:按钮触发的range,值为 range 中的某一项,表示:点击按钮后,触发该range中的某一项的rangeFn
        |- rangeFn:欲触发range中的某方法
        |- request:请求
    |- query: 查询区
        字段同default
    |- table: 列表区
        字段同default
注意
  • 导出配置为数组时,需配置 title属性,用于显示在标签页;导出配置为对象时,则为单页面,不需要配置 title属性。
  • 配置中,大部分(form)属性遵循elementPlus的配置字段,如:rules规则;
配置项说明
    1, label:fields对象属性:显示的中文字段
    2, prop:fields对象属性:取值的英文字段 ‘|’后加方法名可对当前取的值进行处理后显示:updateTime | dateFormatter2YMD:表示用dateFormatter2YMD方法对updateTime进行处理
    3, showIn:fields对象属性:显示此字段到什么位置目前支持(table/query);
    4, tag: fields对象属性:渲染标签的类型。可用:input/select/asyncSelect/autocomplete/date/daterange/month/datetimerange/fileUpload/imgUpload/radio/checkbox/tree/button
    5, config:fields对象属性:表单、列表组件的配置项信息,配置elementPlus的配置项,如:rules规则;
    6, showLogic:fields对象属性:显示逻辑,用于控制在初次渲染时是否显示当前字段,支持两种方式:
        1. 直接赋值一个布尔值,表示是否显示该部分,默认展示;
        2. 赋值一个函数体,该函数返回一个布尔值,函数的参数是地址栏中的_assert_对象和浏览器的storage对象。
    7, queryLogic:fields对象中config对象的属性:用于控制何时在当前查询区域中显示该字段,支持两种方式:
        1. 直接赋值一个布尔值,表示是否显示该部分,默认展示;
        2. 赋值一个函数体,该函数返回一个布尔值,函数的参数是当前查询模块所有字段的值对象。
    8, tableLogic: fields对象中config对象的属性:用于控制何时在table表中显示该字段和显示什么值以及值的样式,支持两种方式:
        1. 直接赋值一个布尔值,表示是否显示该部分,默认展示;
        2. 赋值一个函数体,该函数返回一个布尔值,函数的参数是当前table中的row对象:
            1. obj?.groupName == '小组零' # value('优'); obj?.groupName == '小组六' # value('良'); 
            2. obj?.roleName == '管理员' && obj?.postName == '后端开发' # [class('p-1 p-3'), value('优秀'), style('color: red;font-size: 16px;')];
            3. obj?.roleName == '管理员' # [class('p-2 p-4'), value('秀'), style('color: red;font-size: 16px;')]; obj?.roleName == '普通成员' # [class('p-1'), value('好'), style('color: green;font-size: 14px;')]
            三种逻辑是根据row对象中的某字段的值修改展示的内容和样式;注意';'表示语句的间隔;每条语句的value/rowClass/cellClass/style字段切勿重复,否则会样式紊乱。
    9, value/rowClass/cellClass/style: logic中的自定义配置,用于修改table中的当前字段的值/行样式/单元格样式/值样式
    10, afterExcuteJsStr/beforeExcuteJsStr: request对象属性,用于在请求前和请求后对params或response执行string类型的js代码,参数为params或response
        "beforeExcuteJsStr": "function(obj){Object.keys(obj).map(el => {obj[el]+=1;console.log(obj)});return obj;}(obj)";可在请求前对params的各个参数都做+1处理。
        "afterExcuteJsStr": "function(obj){obj.list.map(el => el['phone']+='555');return obj;}(obj)";对手机号后缀都加了'+86'字符
    11, 列表的响应取值为responseKey字段; 也可在afterExcuteJsStr中自行处理,本期不做特殊配置
    12, _query_、_submit_、_assert_:地址栏中的对象
        1. _query_:当前页面的首次查询信息,查询组件的默认值
        2. _submit_:提交到当前页面的对象
        3. _assert_:当前页面的断言信息,配合showLogic字段使用
        4._storage_:浏览器的缓存数据(包括cookie、localstorage和sessionStorage)

字段说明

[{
    "title": "用户管理",
    "fields": [
        { "label": "用户名称", "prop": "name.a", "showIn":["table", "query"], "tag": "input", "config":
            {
                "clearable": true, "placeholder": "请输入用户名称"
            }
        },
        { "label": "手机号码", "prop": "phone", "showIn":["table", "query"], 'config': { "tableLogic": "obj?.phone == '3' # rowClass('p-1')"}},
        { "label": "状态", "prop": "status", "showIn":["table", "query"], "tag": "select",
            "config": {
                "reqField": "status",
                "enums": {
                    "active": '激活',
                    "inactive": '禁用',
                    "blocked": '锁定'
                }
            }
        },
        { "label": "邮箱", "prop": "email"},
        { "label": "地址", "prop": "address"},
        { "label": "创建时间", "prop": "createdAt | dateFormatter2YMDHMS", "showIn":["table", "query"], "tag": "datetimerange"},
        { "label": "修改时间", "prop": "updatedAt | dateFormatter2YMDHMS"}
    ],
    "operations": {
        "default": [
            {
                "title": "新增用户", "label": "新增", "actOn": "dialog", "rangeFn": "open", "btnClass": "add",
                "fields": [
                    {"label": "用户名称", "prop": "name", "config": {
                        "rules": [{ "required": true, "message": '请输入用户名称', "trigger": 'blur' }]
                    }},
                    {"label": "昵称", "prop": "nickName"},
                    {"label": "密码", "prop": "password"},
                    {"label": "手机号码", "prop": "phone"},
                    {"label": "邮箱", "prop": "email"},
                    {"label": "地址", "prop": "address"},
                    {"label": "状态", "prop": "status", "tag": "select",
                        "config": {
                            "reqField": "status",
                            "enums": {
                                "active": '激活',
                                "inactive": '禁用',
                                "blocked": '锁定'
                            }
                        }
                    }
                ],
                "operations": [{
                    "label": "确认", "actOn": "dialog", "rangeFn": "commit", "operationLogic": "obj?.name == 2",
                    "request": { "url": "/api/user/create", "method": "post", "successMsg": "创建成功!", "successCb":
                        { "actOn": "table", "rangeFn": "update", "request": {"url": "/api/user/query", "method": "post" }, "params": {}},
                    }
                }, {
                    "label": "取消", "rangeFn": "close", "actOn": "dialog"
                }]
            }, {
                "label": "table处理", "actOn": "table", "rangeFn": "getSelection", "operationLogic": "obj?.name == 2",
                "request": { "url": "/api/user/query", "method": "post", "fields": ["id", "name", "phone", "groupId"] }
            }, {
                "label": "按钮下拉", "actOn": "table", "rangeFn": "getSelection",
                "list": [
                    {
                        "label": "新增", "actOn": "dialog", "rangeFn": "open", 
                        "fields": [{"label": "姓名", "prop": "name", "tag": "input"}, {"label": "年龄", "prop": "age", "tag": "input"}],
                        "operations": [{
                            "label": "确认", "actOn": "dialog", "rangeFn": "commit", "operationLogic": "obj?.name == 2",
                            "request": { "url": "/api/user/query", "method": "post" }
                            }, {
                            "label": "取消", "actOn": "dialog", "rangeFn": "close"
                            }
                        ]
                    }
                ]
            }, {
                "label": "query处理", "actOn": "query", "rangeFn": "getQueryData", "request": { "url": "/api/user/query", "method": "post" }
            }, {
                "label": "table + dialog处理", "actOn": "table", "rangeFn": "getSelection", 
                "next": {
                    "label": "table + dialog处理", "actOn": "dialog", "rangeFn": "open", "headTip": "处理${selectedSize}",
                    "fields": [{"label": "姓名", "prop": "name", "tag": "input"}, {"label": "年龄", "prop": "age", "tag": "input"}, {"label": "性别", "prop": "sex", "tag": 
                        "input"}],
                    "operations": [{
                            "label": "确认", "actOn": "dialog", "rangeFn": "commit",
                            "request": { "url": "/api/user/query", "method": "post" }
                        }, {
                            "label": "取消", "actOn": "dialog", "rangeFn": "close"
                        }
                    ]
                }
            }, {
                "label": "query + dialog处理", "actOn": "query", "rangeFn": "getQueryData",
                "next": {
                    "label": "query + dialog处理", "actOn": "dialog", "rangeFn": "open",
                    "fields": [{"label": "手机号", "prop": "phone", "tag": "input"}, {"label": "邮箱", "prop": "email", "tag": "input"}, {"label": "地址", "prop": "address", "tag": 
                        "input"}],
                    "operations": [{
                            "label": "确认", "actOn": "dialog", "rangeFn": "commit",
                            "request": { "url": "/api/user/query", "method": "post" }
                        }, {
                            "label": "取消", "actOn": "dialog", "rangeFn": "close"
                        }
                    ]
                }
            }, {
                "label": "query + table + dialog处理", "actOn": "query", "rangeFn": "getQueryData",
                "next": {
                    "actOn": "table", "rangeFn": "getSelection", 
                    "next": {
                        "actOn": "dialog", "rangeFn": "open", "label": "query + table + dialog处理",
                        "fields": [
                            {"label": "姓名", "prop": "name", "tag": "input"},
                            {"label": "年龄", "prop": "age", "tag": "input"},
                            {"label": "电话", "prop": "phone", "tag": "input"},
                            {"label": "性别", "prop": "sex", "tag": "input"}
                        ],
                        "operations": [{
                            "label": "确认", "actOn": "dialog", "rangeFn": "commit",
                            "request": { "url": "/api/user/query", "method": "post" }
                            }, {
                            "label": "取消", "actOn": "dialog", "rangeFn": "close"
                            }, {
                                "label": "下一步", "actOn": "dialog", "rangeFn": "getDialogData", "showLogic": "obj?.aaa == 22 && obj?.bbb == 111",
                                "next": {
                                    "actOn": "dialog", "rangeFn": "open",
                                    "fields": [
                                        {"label": "姓名1", "prop": "name1", "tag": "input"},
                                        {"label": "年龄2", "prop": "age1", "tag": "input"},
                                        {"label": "电话3", "prop": "phone1", "tag": "input"},
                                        {"label": "性别4", "prop": "sex1", "tag": "input"}
                                    ],
                                    "operations": [{
                                        "label": "确认", "actOn": "dialog", "rangeFn": "commit",
                                        "request": { "url": "/api/user/query", "method": "post" }
                                        }, {
                                        "label": "取消", "actOn": "dialog", "rangeFn": "close"
                                        },{
                                            "label": "下一步", "actOn": "dialog", "rangeFn": "getDialogData",
                                            "next": {
                                                "actOn": "dialog", "rangeFn": "open",
                                                "fields": [
                                                    {"label": "姓名11", "prop": "name2", "tag": "input"},
                                                    {"label": "年龄22", "prop": "age2", "tag": "input"},
                                                    {"label": "电话33", "prop": "phone2", "tag": "input"},
                                                    {"label": "性别44", "prop": "sex2", "tag": "input"}
                                                ],
                                                "operations": [{
                                                    "label": "确认", "actOn": "dialog", "rangeFn": "commit",
                                                    "request": { "url": "/api/user/query", "method": "post" }
                                                    }, {
                                                    "label": "取消", "actOn": "dialog", "rangeFn": "close"
                                                }]
                                            }
                                        }]
                                }
                            }
                        ],
                    }
                }
            },
            {
                "label": "自定义处理", "actOn": "query", "rangeFn": "getQueryData", "next": {
                    "actOn": "table", "rangeFn": "getSelection", "rangeFnExcuteJs": "function(obj) {console.log(obj);return obj}(obj)", "next": {
                        "isCustom": true, "request": { "url": "/api/user/query", "method": "post" }
                    }
                }
            },
        ],
        "table": [
            { "label": "编辑", "actOn": "dialog", "rangeFn": "open", "showLogic": "obj.permissions.includes(1)", "btnClass": "edit",
                "fields": [
                    {"label": "用户ID", "prop": "id", "config": {
                        "disabled": true
                    }},
                    {"label": "用户名称", "prop": "name.a"},
                    {"label": "手机号码", "prop": "phone"},
                    {"label": "角色", "prop": "roleName", "tag": "select", "config": 
                        {
                            "reqField": "roleId",
                            "enum": {
                                "b578e8c0e8295ec295e7cc89447271ab": '管理员',
                                "9886d03139161f1329dfb6049e3a1910": '普通用户',
                                "70545e008dd490edea15bdd228e566fc": '超级管理员'
                            }
                        }
                    },
                    { "label": "所属部门", "prop": "zoneId", "tag": "asyncSelect", "config":
                        {
                            "url": "/api/zone/select", "method": "post", "fieldKey": "name", "fieldValue": "id",
                        }
                    },
                    { "label": "所属组", "prop": "groupId", "tag": "asyncSelect", "config":
                        {
                            "url": "/api/group/select", "method": "post", "fieldKey": "name", "fieldValue": "id",
                        }
                    },
                    { "label": "所属岗位", "prop": "postId", "tag": "asyncSelect", "config":
                        {
                            "url": "/api/post/select", "method": "post", "fieldKey": "name", "fieldValue": "id",
                        }
                    },
                    { "label": "备注", "prop": "description", "config":
                        {
                            "type": "textarea",
                        }
                    }
                ],
                "operations": [{
                    "label": "确认", "rangeFn": "commit", "actOn": "dialog", "isCloseDialog": true,
                    "request": { "url": "/user/update", "method": "post", "fetchMsg": "创建成功!", "successCb": { "actOn": "table", "rangeFn": "update", "request": {"url": "/user/query", "method": "post" }, "params": {}}}
                }, {
                    "label": "取消", "actOn": "dialog", "rangeFn": "close"
                }]
            },
            { "label": "删除", "actOn": "table", "rangeFn": "delete", "request": { "url": "/api/user/del/${id}", "method": "post", "successCb": { "rangeFn": "update", "request": {"url": "/user/query", "method": "post" }, "params": {}}}}
        ],
        "query": [
            { "label": "重置", "actOn": "query", "rangeFn": "reset", "btnClass": "reset" },
            { "label": "查询", "actOn": "table", "rangeFn": "update", "btnLoading": true, "request": {
                "url": "/api/user/query",
                "method": "post",
                "params": {"pageNum": 1},
                "beforeExcuteJsStr": "function(obj){return obj;}(obj)",
                "afterExcuteJsStr": "function(obj){obj.body.forEach((el,idx) => {el.name = {'a': idx}});console.log(obj);return obj;}(obj)"
            }
        }]
    }
}
        ],
        "table": [
            { "label": "编辑", "actOn": "dialog", "rangeFn": "open", "showLogic": "obj.roleName === '管理员'", "className": "edit",
                "fields": [
                    { "label": "成员ID", "prop": "id", "config": { "disabled": true } },
                    {"label": "成员名称", "prop": "name"},
                    {"label": "手机号码", "prop": "phone"},
                    {"label": "角色", "prop": "roleName", "tag": "select", "config": 
                        {
                            "reqField": "roleId",
                            "enum": {
                                "b578e8c0e8295ec295e7cc89447271ab": '管理员',
                                "9886d03139161f1329dfb6049e3a1910": '普通成员',
                                "70545e008dd490edea15bdd228e566fc": '超级管理员'
                            }
                        }
                    },
                    { "label": "所属部门", "prop": "zoneId", "tag": "asyncSelect", "config":
                        {
                            "url": "/api/zone/select", "method": "post", "fieldKey": "name", "fieldValue": "id", "notMsg": true,
                        }
                    },
                    { "label": "所属组", "prop": "groupId", "tag": "asyncSelect", "config":
                        {
                            "url": "/api/group/select", "method": "post", "fieldKey": "name", "fieldValue": "id", "notMsg": true,
                        }
                    },
                    { "label": "所属岗位", "prop": "postId", "tag": "asyncSelect", "config":
                        {
                            "url": "/api/post/select", "method": "post", "fieldKey": "name", "fieldValue": "id", "notMsg": true,
                        }
                    },
                    { "label": "备注", "prop": "description", "config": { "type": "textarea" } }
                ],
                "operations": [
                    { "label": "确认", "rangeFn": "commit", "actOn": "dialog", "isCloseDialog": true, "request": 
                        {
                            "url": "/member/update", "method": "post", "successCb": { 
                                "actOn": "table", "rangeFn": "update", "request":
                                {
                                    "url": "/member/list", "method": "post"
                                }, "params": {}
                            }
                        }
                    },
                    { "label": "取消", "actOn": "dialog", "rangeFn": "close" }
                ]
            },
            { "label": "删除", "actOn": "table", "rangeFn": "delete", "request": 
                {
                    "url": "/member/delete", "method": "post", "successCb": { "rangeFn": "update", "request": { "url": "/member/list", "method": "post" }, "params": {} }
                }   
            }
        ],
        "query": [
            { "rangeFn": "reset", "label": "重置", "className": "reset"},
            { "label": "查询", "actOn": "table", "rangeFn": "update", "btnLoading": true, "request": {
                "url": "/api/member/list",
                "method": "post",
                "data": {},
                "beforeExcuteJsStr": "function(obj){Object.keys(obj).map(el => {obj[el]+=1;console.log(obj)});return obj;}(obj)",
                "afterExcuteJsStr": "function(obj){obj.list.map(el => el['phone']+='+86');return obj;}(obj)"
            }
            }
        ]
    }
}, {
    "title": "XXXX",
    ...
},{
    "title": "YYYY",
    ...
}]

如上所示,一个完整的schema配置得到的页面包含table,dialog,query,operation四部分,包含了项目内的所有功能。

Versions

Current Tags

VersionDownloads (Last 7 Days)Tag
0.8.061latest

Version History

VersionDownloads (Last 7 Days)Published
0.8.061
0.7.964
0.7.862
0.7.764
0.7.660
0.7.576
0.7.265
0.7.173
0.6.513
0.6.19
0.5.82
0.5.62
0.4.20
0.3.90
0.3.31
0.3.21
0.2.524
0.2.123
0.2.023
0.1.923
0.1.823

Package Sidebar

Install

npm i vpagecrud

Weekly Downloads

1,085

Version

0.8.0

License

MIT

Unpacked Size

149 kB

Total Files

6

Last publish

Collaborators

  • jser_lz