前言
在我之前写的文章你一定没见过这样高度适配的接口,HC小区管理系统后端项目源码难点梳理 中我们一起梳理了后端项目MicroCommunity
的开发流程和难点,当时前端项目MicroCommunityWeb
尚未梳理,而前端项目MicroCommunityWeb
对于
初学者来说也容易懵逼。因为MicroCommunityWeb
项目是一个同时集成了vue
、layui
、vcFramwork
和 bootstrap
等不同的 js
框架,里面开发页面的写法也与 仅仅集成了 Vue
和 Element-UI
框架的项目大部相同。
开发新页面
MicroCommunityWeb
项目中的页面组件几乎全部都在src/public/pages
目录下,我们也可以在这个目录下新建自己的页面组件。
在pages
目录下的任何一个大类子目录下(如fee目录,费用相关)新建一个页面子目录,然后在该子目录下新建页面相关的html
和js
文件。例如申请发票页面对应的两个文件invoiceApply.html
和 invoiceApply.js
页面 html 文件
在html
页面中可以使用vue
语法,而数据和函数则定义在与之对应的js
文件中。invoiceApply.html
文件源码如下:
<div>
<div class="row">
<div class="col-md-2 padding-r-0">
<div class=" border-radius ">
<div class="margin-xs-r treeview attendance-staff">
<ul class="list-group text-center border-radius">
<li class="list-group-item node-orgTree " v-for="(item,index) in invoiceApplyInfo.states" :key="index" @click="swatchState(item)" :class="{'vc-node-selected':invoiceApplyInfo.conditions.state == item.state}">
{{item.stateName}}
</li>
</ul>
</div>
</div>
</div>
<div class="col-md-10">
<div class="row">
<div class="col-lg-12">
<div class="ibox ">
<div class="ibox-title">
<h5>
<vc:i18n name="查询条件"></vc:i18n>
</h5>
<div class="ibox-tools" style="top:10px;"></div>
</div>
<div class="ibox-content">
<div class="row">
<div class="col-sm-2">
<div class="form-group">
<input type="text" :placeholder="vc.i18n('请填写发票号','invoiceApply')" v-model="invoiceApplyInfo.conditions.invoiceCode" class="form-control">
</div>
</div>
<div class="col-sm-2">
<select class="custom-select" v-model="invoiceApplyInfo.conditions.invoiceType">
<option selected value="">
请选择发票类型
</option>
<option value="1001">个人</option>
<option value="2002">企业</option>
</select>
</div>
<div class="col-sm-2">
<div class="form-group">
<input type="text" :placeholder="vc.i18n('请选择业主名称','invoiceApply')" v-model="invoiceApplyInfo.conditions.ownerName" class=" form-control">
</div>
</div>
<div class="col-sm-2">
<div class="form-group">
<input type="text" :placeholder="vc.i18n('请选择申请人','invoiceApply')" v-model="invoiceApplyInfo.conditions.createUserName" class=" form-control">
</div>
</div>
<div class="col-sm-2">
<div class="form-group">
<input type="text" :placeholder="vc.i18n('请选择申请人电话','invoiceApply')" v-model="invoiceApplyInfo.conditions.applyTel" class=" form-control">
</div>
</div>
<div class="col-sm-1">
<button type="button" class="btn btn-primary btn-sm" style="min-width: 50px;" v-on:click="_queryInvoiceApplyMethod()">
<i class="glyphicon glyphicon-search"></i> <span>
<vc:i18n name="查询"></vc:i18n>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div class="ibox">
<div class="ibox-title">
<h5>
<vc:i18n name="申请发票" namespace="invoiceApply"></vc:i18n>
</h5>
<div class="ibox-tools" style="top:10px;">
<button type="button" class="btn btn-primary btn-sm" @click="_invoiceApply">
<vc:i18n name="申请"></vc:i18n>
</button>
</div>
</div>
<div class="ibox-content">
<table class="footable table table-stripped toggle-arrow-tiny" data-page-size="15">
<thead>
<tr>
<th class="text-center">
<vc:i18n name='编号' namespace='invoiceApply'></vc:i18n>
</th>
<th class="text-center">
<vc:i18n name='发票类型' namespace='invoiceApply'></vc:i18n>
</th>
<th class="text-center">
<vc:i18n name='业主名称' namespace='invoiceApply'></vc:i18n>
</th>
<th class="text-center">
<vc:i18n name='申请人' namespace='invoiceApply'></vc:i18n>
</th>
<th class="text-center">
<vc:i18n name='发票名头' namespace='invoiceApply'></vc:i18n>
</th>
<th class="text-center">
<vc:i18n name='纳税人识别号' namespace='invoiceApply'></vc:i18n>
</th>
<th class="text-center">
<vc:i18n name='地址、电话' namespace='invoiceApply'></vc:i18n>
</th>
<th class="text-center">
<vc:i18n name='申请金额' namespace='invoiceApply'></vc:i18n>
</th>
<th class="text-center">
<vc:i18n name='发票号' namespace='invoiceApply'></vc:i18n>
</th>
<th class="text-center">
<vc:i18n name='审核状态' namespace='invoiceApply'></vc:i18n>
</th>
<th class="text-center">
<vc:i18n name='申请时间' namespace='invoiceApply'></vc:i18n>
</th>
<th class="text-center">
<vc:i18n name='操作'></vc:i18n>
</th>
</tr>
</thead>
<tbody>
<tr v-for="invoiceApply in invoiceApplyInfo.invoiceApplys">
<td class="text-center">{{invoiceApply.applyId}}</td>
<td class="text-center">{{invoiceApply.invoiceType == '1001'?'个人':'企业'}}</td>
<td class="text-center">{{invoiceApply.ownerName}}</td>
<td class="text-center">
{{invoiceApply.createUserName}}({{invoiceApply.applyTel}})</td>
<td class="text-center">{{invoiceApply.invoiceName}}</td>
<td class="text-center">{{invoiceApply.invoiceNum}}</td>
<td class="text-center">{{invoiceApply.invoiceAddress}}</td>
<td class="text-center">{{invoiceApply.invoiceAmount}}</td>
<td class="text-center">{{invoiceApply.invoiceCode || '未上传'}}</td>
<td class="text-center">{{invoiceApply.stateName}}</td>
<td class="text-center">{{invoiceApply.createTime}}</td>
<td class="text-center">
<div class="btn-group" v-if="invoiceApply.state == 'W'">
<button class="btn-white btn btn-xs" v-on:click="_openInvoiceAuditModel(invoiceApply)">
<vc:i18n name='审核'></vc:i18n>
</button>
</div>
<div class="btn-group" v-if="invoiceApply.state == 'U'">
<button class="btn-white btn btn-xs" v-on:click="_openInvoice(invoiceApply)">
<vc:i18n name='开票'></vc:i18n>
</button>
</div>
<div class="btn-group">
<button class="btn-white btn btn-xs" v-on:click="_openDeleteInvoiceApplyModel(invoiceApply)">
<vc:i18n name='删除'></vc:i18n>
</button>
</div>
<div class="btn-group">
<button class="btn-white btn btn-xs"
v-on:click="_openInvoiceApplyDetail(invoiceApply)">
<vc:i18n name='详情'></vc:i18n>
</button>
</div>
</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="7">
<ul class="pagination float-right"></ul>
</td>
</tr>
</tfoot>
</table>
<!-- 分页 -->
<vc:create path="frame/pagination"></vc:create>
</div>
</div>
</div>
</div>
</div>
</div>
<vc:create path="fee/deleteInvoiceApply"></vc:create>
<vc:create path="fee/uploadInvoicePhoto"></vc:create>
<vc:create path="fee/wirteInvoiceEvent"></vc:create>
<vc:create path="common/audit" callBackListener="invoiceApply" callBackFunction="notifyAuditInfo"></vc:create>
</div>
v-if
、 v-for
和v-model
等 vue
指令在页面的html
文件中可以无障碍使用。这里可以看到,页面的js
文件并不需要通过script
标签的src
属性引入。因为在vcFramework.js
框架中会自动将页面的js
文件嵌入到相同目录下的页面html
文件中去。
除此之外还能看到很多vc:i18n
和vc:create
标签, vc:i18n
表示国际化标签;而vc:create
表示页面内嵌的弹窗子组件,path
属性值表示位于public/components
目录下的弹窗组件位置,callBackListener
和 callBackFunction
的值分别表示弹窗子组件中的两个构造参数。而 path="common/audit"
代表的子组件为public/components/common/audit
目录下表示的弹窗组件。
audit.html
文件源码如下:
<div id="auditModel" class="modal fade" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-body">
<h3 class="m-t-none m-b ">
<vc:i18n name="审核信息" namespace="audit"></vc:i18n>
</h3>
<div class="ibox-content">
<div class="form-group row">
<label class="col-sm-2 col-form-label">
<vc:i18n name="审核状态" namespace="audit"></vc:i18n>
</label>
<div class="col-sm-10">
<select class="custom-select" v-model="auditInfo.state">
<option selected disabled value="">{{vc.i18n('请审核','audit')}}</option>
<option value="1100">{{vc.i18n('同意','audit')}}</option>
<option value="1200">{{vc.i18n('拒绝','audit')}}</option>
</select>
</div>
</div>
<div class="form-group row">
<label class="col-sm-2 col-form-label">
<span>
<vc:i18n name="原因" namespace="audit"></vc:i18n>
</span>
</label>
<div class="col-sm-10">
<textarea :placeholder="vc.i18n('必填,请填写原因','audit')" class="form-control"
v-model="auditInfo.remark">
</textarea>
</div>
</div>
<div class="ibox-content">
<button class="btn btn-primary float-right" type="button" v-on:click="_auditSubmit()">
<i class="fa fa-check"></i> 提交
</button>
<button type="button" class="btn btn-warning float-right" style="margin-right:20px;"
data-dismiss="modal">
<i class="fa fa-times"></i> 取消
</button>
</div>
</div>
</div>
</div>
</div>
</div>
可以看到html
文件中通过role="dialog"
指定这是一个弹窗,通过aria-hidden="true"
指定弹窗默认隐藏。html
文件中定义的class
的样式都可以在public/css/vc-ui.css
文件中定义。
页面js 文件
定义组件中的数据以及页面按钮绑定的函数都在页面的js
文件中完成,invoiceApply.js
文件源码如下:
(function (vc) {
var DEFAULT_PAGE = 1;
var DEFAULT_ROWS = 10;
vc.extends({
data: {
invoiceApplyInfo: {
invoiceApplys: [],
total: 0,
records: 1,
moreCondition: false,
applyId: '',
states:[{
stateName:'全部',
state:'',
},{
stateName:'待审核',
state:'W',
},{
stateName:'待上传',
state:'U',
},{
stateName:'审核失败',
state:'F',
},{
stateName:'带领用',
state:'G',
},{
stateName:'已领用',
state:'C',
}],
conditions: {
applyId: '',
invoiceCode:'',
invoiceType: '',
ownerName: '',
applyTel: '',
createUserName: '',
state: '',
},
audit:{}
}
},
_initMethod: function () {
$that._listInvoiceApplys(DEFAULT_PAGE, DEFAULT_ROWS);
},
_initEvent: function () {
vc.on('invoiceApply', 'notifyAuditInfo', function (_auditInfo) {
$that._auditInvoiceState(_auditInfo);
});
vc.on('invoiceApply', 'listInvoiceApply', function (_param) {
$that._listInvoiceApplys(DEFAULT_PAGE, DEFAULT_ROWS);
});
vc.on('pagination', 'page_event', function (_currentPage) {
$that._listInvoiceApplys(_currentPage, DEFAULT_ROWS);
});
},
methods: {
_listInvoiceApplys: function (_page, _rows) {
$that.invoiceApplyInfo.conditions.page = _page;
$that.invoiceApplyInfo.conditions.row = _rows;
$that.invoiceApplyInfo.conditions.communityId = vc.getCurrentCommunity().communityId;
let param = {
params: $that.invoiceApplyInfo.conditions
};
//发送get请求
vc.http.apiGet('/invoice.listInvoiceApply',
param,
function (json, res) {
let _json = JSON.parse(json);
$that.invoiceApplyInfo.total = _json.total;
$that.invoiceApplyInfo.records = _json.records;
$that.invoiceApplyInfo.invoiceApplys = _json.data;
vc.emit('pagination', 'init', {
total: $that.invoiceApplyInfo.records,
currentPage: _page
});
}, function (errInfo, error) {
console.log('请求失败处理');
}
);
},
_openDeleteInvoiceApplyModel: function (_invoiceApply) {
vc.emit('deleteInvoiceApply', 'openDeleteInvoiceApplyModal', _invoiceApply);
},
_queryInvoiceApplyMethod: function () {
$that._listInvoiceApplys(DEFAULT_PAGE, DEFAULT_ROWS);
},
_moreCondition: function () {
if ($that.invoiceApplyInfo.moreCondition) {
$that.invoiceApplyInfo.moreCondition = false;
} else {
$that.invoiceApplyInfo.moreCondition = true;
}
},
swatchState:function(_state){
$that.invoiceApplyInfo.conditions.state = _state.state;
$that._listInvoiceApplys(DEFAULT_PAGE, DEFAULT_ROWS);
},
_invoiceApply:function(){
vc.jumpToPage('/#/pages/fee/ownerApplyInvoice')
},
_openInvoiceApplyDetail:function(_invoiceApply){
vc.jumpToPage('/#/pages/fee/invoiceApplyDetail?applyId=' _invoiceApply.applyId);
},
_openInvoiceAuditModel(_invoiceApply) {
$that.invoiceApplyInfo.audit = _invoiceApply;
vc.emit('audit', 'openAuditModal', {});
},
_auditInvoiceState: function (_auditInfo) {
$that.invoiceApplyInfo.audit.state = _auditInfo.state;
$that.invoiceApplyInfo.audit.remark = _auditInfo.remark;
vc.http.apiPost(
'/invoice.auditInvoiceApply',
JSON.stringify( $that.invoiceApplyInfo.audit), {
emulateJSON: true
},
function (json, res) {
let _json = JSON.parse(json);
if (_json.code == 0) {
$that._listInvoiceApplys(DEFAULT_PAGE, DEFAULT_ROWS);
vc.toast("审核成功");
return;
} else {
vc.toast(_json.msg);
}
},
function (errInfo, error) {
vc.toast(errInfo);
});
},
_openUploadInvoicePhoto: function(_apply){
vc.emit('uploadInvoicePhoto', 'openInvoicePhotoModal',_apply);
},
_openUserGetInvoice:function(_apply){
vc.emit('wirteInvoiceEvent', 'openWirteInvoiceModal',_apply);
},
_openInvoice: function(invoiceApply){
vc.jumpToPage('/#/pages/fee/openInvoice');
sessionStorage.setItem('invoiceApply', JSON.stringify(invoiceApply));
}
}
});
})(window.vc);
可以看到invoiceApply.js
文件中定义了一个闭包函数,里面的vc
参数表示public/vc-core/core.js
文件中定义的工具对象。
通过vc.extends
方法定义页面的数据和生命周期函数。
data
对象中定义页面双向绑定的数据;_initMethod
方法中定义页面加载后需要调用的函数;_initEvent
方法中监听关联组件通过vc.emit
方法触发与本组件的通信;methods
对象中则可以自定义页面按钮绑定的函数或其他需要用到的函数。
例如 vc.emit('audit', 'openAuditModal', {});
方法触发打开审批对话框,调用该方法会进入public/components/audit/audit.js
文件中_initEvent
方法中下面这段逻辑。
vc.on('audit', 'openAuditModal', function () {
$('#auditModel').modal('show'); // 显示对话框
});
vc.emit
方法中的前两个参数与vc.on
方法中的前两个参数保持一致,vc.emit
方法中的第三个参数为vc.on
回调函数中的参数。
如:vc.emit('uploadInvoicePhoto', 'openInvoicePhotoModal',_apply);
对应 public/components/fee/uploadInvoicePhoto/uploadInvoicePhoto.js
文件中_initEvent
方法中对应的源码
vc.on('uploadInvoicePhoto', 'openInvoicePhotoModal', function(_param) {
vc.copyObject(_param,$that.uploadInvoicePhotoInfo);
$('#uploadInvoicePhotoModel').modal('show');
});
回调函数中的_param
变量就是 vc.emit
方法中的_apply
参数。除此之外下面几个vc
对象的方法也是常用到的
vc.jumpToPage
方法可以完成页面跳转,传入的参数为目标页面对应的url
;vc.http.post
和vc.http.get
方法可分别完成向后台发送post
和get
类型的http
请求;vc.toast
方法可完成弹出提示消息。
源码分析
vc
对象的源码都写在src/public/vcCore/vc-core.js
文件中可查看到
1)vc.emit
方法源码
/**
事件触发
**/
vc.emit = function () {
var _namespace = "";
var _componentName = "";
var _value = "";
var _param = undefined;
if (arguments.length == 4) {
_namespace = arguments[0];
_componentName = arguments[1];
_value = arguments[2];
_param = arguments[3];
} else if (arguments.length == 3) {
_componentName = arguments[0];
_value = arguments[1];
_param = arguments[2];
} else {
console.error("执行on 异常,vc.on 参数只能是3个 或4个");
return;
}
if (vc.debug) {
console.log("监听emit事件", _namespace, _componentName, _value, _param);
}
if (vc.notNull(_namespace)) {
vc.component.$emit(_namespace "_" _componentName '_' _value, _param);
return;
}
vc.component.$emit(_componentName '_' _value, _param);
};
可以看到在我们的页面js
文件中大多是传了3个参数到vc.emit
方法中,最终是走了vc.component.$emit(_componentName '_' _value, _param);
这行代码。而vc.component
通过下面这段源码可以看到它是一个vue
实例。
(function (vc, vmOptions) {
console.log("vmOptions:", vmOptions);
vc.component = new Vue(vmOptions);
})(window.vc, window.vc.vmOptions);
2)vc.on
方法源码
/**
事件监听
**/
vc.on = function () {
var _namespace = "";
var _componentName = "";
var _value = "";
var _callback = undefined;
if (arguments.length == 4) {
_namespace = arguments[0];
_componentName = arguments[1];
_value = arguments[2];
_callback = arguments[3];
} else if (arguments.length == 3) {
_componentName = arguments[0];
_value = arguments[1];
_callback = arguments[2];
} else {
console.error("执行on 异常,vc.on 参数只能是3个 或4个");
return;
}
if (vc.notNull(_namespace)) {
vc.component.$on(_namespace "_" _componentName '_' _value,
function (param) {
if (vc.debug) {
console.log("监听ON事件", _namespace, _componentName, _value, param);
}
_callback(param);
}
);
return;
}
vc.component.$on(_componentName '_' _value,
function (param) {
if (vc.debug) {
console.log("监听ON事件", _componentName, _value, param);
}
_callback(param);
}
);
};
vc.component.emit 方法会触发vc.on方法,而我们的页面js文件中调用vc.on方法时统一传了3个参数,前两个参数共同组成监听的事件名称,后一个参数为回调函数。最终会走了vue实例的on方法监听事件方法,也就是下面这段代码逻辑。
代码语言:javascript复制vc.component.$on(_componentName '_' _value,
function (param) {
if (vc.debug) {
console.log("监听ON事件", _componentName, _value, param);
}
_callback(param);
}
);
最后会通过回调函数_callback
和传过来的参数param
在监听事件中完成业务逻辑,可以通过在源码中 debugger
断点追踪到。
vue
对象 执行初始化方法的源码在vc-core.js
文件中
(function (vc) {
vc.initEvent.forEach(function (eventMethod) {
eventMethod();
});
vc.initMethod.forEach(function (callback) {
callback();
});
vc.namespace.forEach(function (_param) {
vc[_param.namespace] = vc.component[_param.namespace];
});
})(window.vc);
3)发送http
请求方法源码
源码位置:src/public/vcCore/vcFramework-0.4.js
vcFramework.http = {
apiPost: function (api, param, options, successCallback, errorCallback) {
let _api = '';
if (api.indexOf('/') >= 0) {
_api = '/app' api;
Vue.http.headers.common['APP-ID'] = '8000418004';
Vue.http.headers.common['TRANSACTION-ID'] = vcFramework.uuid();
Vue.http.headers.common['REQ-TIME'] = vcFramework.getDateYYYYMMDDHHMISS();
Vue.http.headers.common['SIGN'] = '';
} else {
_api = '/callComponent/' api;
}
vcFramework.loading('open');
Vue.http.post(_api, param, options)
.then(function (res) {
try {
let _header = res.headers.map;
//console.log('res', res);
if (vcFramework.notNull(_header['location'])) {
window.location.href = _header['location'];
return;
};
successCallback(res.bodyText, res);
} catch (e) {
console.error(e);
} finally {
vcFramework.loading('close');
}
}, function (res) {
try {
if (res.status == 401 && res.headers.map["location"]) {
let _header = res.headers.map;
//console.log('res', res);
window.location.href = _header['location'];
return;
}
if (res.status == 404) {
window.location.href = '/user.html#/pages/frame/login';
return;
}
errorCallback(res.bodyText, res);
} catch (e) {
console.error(e);
} finally {
vcFramework.loading('close');
}
});
},
apiGet: function (api, param, successCallback, errorCallback) {
//加入缓存机制
let _getPath = '';
if (api.indexOf('/') != 0) {
_getPath = '/' api;
}
if (vcFramework.constant.GET_CACHE_URL.includes(_getPath)) {
let _cacheData = vcFramework.getData(_getPath);
//浏览器缓存中能获取到
if (_cacheData != null && _cacheData != undefined) {
successCallback(JSON.stringify(_cacheData), { status: 200 });
return;
}
}
let _api = '';
if (api.indexOf('/') >= 0) {
_api = '/app' api;
Vue.http.headers.common['APP-ID'] = '8000418004';
Vue.http.headers.common['TRANSACTION-ID'] = vcFramework.uuid();
Vue.http.headers.common['REQ-TIME'] = vcFramework.getDateYYYYMMDDHHMISS();
Vue.http.headers.common['SIGN'] = '';
} else {
_api = '/callComponent/' api;
}
if(vcFramework.hasOwnProperty('loading')){
vcFramework.loading('open');
}
Vue.http.get(_api, param)
.then(function (res) {
try {
successCallback(res.bodyText, res);
if (vcFramework.constant.GET_CACHE_URL.includes(_getPath) && res.status == 200) {
vcFramework.saveData(_getPath, JSON.parse(res.bodyText));
}
} catch (e) {
console.error(e);
} finally {
if(vcFramework.hasOwnProperty('loading')){
vcFramework.loading('close');
}
}
}, function (res) {
try {
if (res.status == 401 && res.headers.map["location"]) {
let _header = res.headers.map;
//console.log('res', res);
window.location.href = _header['location'];
return;
}
if (res.status == 404) {
window.location.href = '/user.html#/pages/frame/login';
return;
}
errorCallback(res.bodyText, res);
} catch (e) {
console.error(e);
} finally {
vcFramework.loading('close');
}
});
}
}
可以看到 vcFramework.http
对象中封装了apipost
、apiGet
请求方法,分别对应POST
和GET
类型的 http
请求。
小结
相信有了本文的梳理后,在HcCommunityWeb
项目已有代码的基础上开发新的页面功能也不会有太大的难度了。主要是模仿已有的页面组件开发新的页面功能,但是如果对于一些工具方法不理解它的含义也很难模仿。重要的是我们要做到大体理解vm.emit
和vm.on
这些用于组件之间通信的用法和传入的每个参数的含义。下面是笔者模仿别的页面源码开发出来的在线开票页面,开票和冲红发票的完整功能还有待完善。
需要注意的是后台没新开发一个新的@Java110Cmd
标注的接口都需要将接口信息加入到c_service
和 c_route
表中,否则后台会报没有权限访问接口。示例脚本如下:
insert into c_service(id, service_id, service_code, business_type_cd, `name`, seq, messageQueueName, is_instance, url, method, timeout, retry_count, provide_app_id, create_time, status_cd)
values(1867, '982024032389830038', 'invoice.saveInvoice', 'API', '开电子发票', 1, '', 'CMD', 'http://fee-service', 'POST', 60, 3, '8000418002', '2024-04-23 00:09:17', '0');
insert into c_route(app_id, service_id, order_type_cd, invoke_limit_times, invoke_model, create_time, status_cd)
values('8000418004', '982024032389830038', 'D', '1000', 'S', CURRENT_TIMESTAMP, '0');
c_service
表中service_code
列的值为@Java110Cmd
注解中serviceCode
属性对应的值, is_instance
列为固定值CMD
,method
列的值为接口对应的请求类型GET
或POST
。
HC小区物业系统前端项目MicroCommunityWeb
代码仓库地址如下:
https://gitee.com/java110/MicroCommunityWeb
对源码感兴趣的读者朋友可自行克隆下载。