NCF 实战功能(三)
- 2021 年 12 月 12 日
本文字数:14270 字
阅读完需:约 47 分钟
概述
本节主要说的内容是,如何使用 Xncf 实现自己的业务逻辑,帮助你完成增删改查等基本的功能,会写了增删改查,其他的功能也就都可以根据实际的需要衍生出来了。
建立需求
下面我们举例来实现一下 User 的功能
数据库
首先我们先确定一下表所需要的字段
Senparc_Admin_User(用户)
Senparc_Admin_User(用户表)
创建 Model
在路径自定义的 Xncf Module 下
创建\Models\DatabaseModel\User.cs
源码如下:
using Senparc.Ncf.Core.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
using Senparc.Xncf.Admin.Models.DatabaseModel.Dto;
namespace Senparc.Xncf.Admin.Models.DatabaseModel
{
/// <summary>
/// User 实体类
/// </summary>
[Table(Register.DATABASE_PREFIX + nameof(User))]//必须添加前缀,防止全系统中发生冲突
[Serializable]
public class User : EntityBase<string>
{
public User()
{
Id = Guid.NewGuid().ToString();
AddTime = DateTime.Now;
this.LastUpdateTime = AddTime;
}
public User(UserDto userDto) : this()
{
LastUpdateTime = userDto.LastUpdateTime;
UnionId = userDto.UnionId;
WxOpenId = userDto.WxOpenId;
WxNickName = userDto.WxNickName;
Thumb = userDto.Thumb;
Gender = userDto.Gender;
Country = userDto.Country;
Province = userDto.Province;
City = userDto.City;
}
public void Update(UserDto userDto)
{
LastUpdateTime = userDto.LastUpdateTime;
UnionId = userDto.UnionId;
WxOpenId = userDto.WxOpenId;
WxNickName = userDto.WxNickName;
Thumb = userDto.Thumb;
Gender = userDto.Gender;
Country = userDto.Country;
Province = userDto.Province;
City = userDto.City;
}
/// <summary>
/// 微信UnionId
/// </summary>
[MaxLength(50)]
public string UnionId { get; set; }
/// <summary>
/// 微信OpenId
/// </summary>
[MaxLength(50)]
public string WxOpenId { get; set; }
/// <summary>
/// 微信昵称
/// </summary>
[MaxLength(100)]
public string WxNickName { get; set; }
/// <summary>
/// 头像
/// </summary>
[MaxLength(200)]
public string Thumb { get; set; }
/// <summary>
/// 性别(1-男;2-女;)
/// </summary>
public int Gender { get; set; }
/// <summary>
/// 国家
/// </summary>
[MaxLength(100)]
public string Country { get; set; }
/// <summary>
/// 省份
/// </summary>
[MaxLength(100)]
public string Province { get; set; }
/// <summary>
/// 城市
/// </summary>
[MaxLength(100)]
public string City { get; set; }
}
}
创建 \Models\DatabaseModel\Dto\UserDto.cs
源码如下:
using Senparc.Ncf.Core.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text;
namespace Senparc.Xncf.Admin.Models.DatabaseModel.Dto
{
public class UserDto : DtoBase
{
public UserDto()
{
}
public UserDto(string id, string unionId, string wxOpenId, string wxNickName, string thumb, int gender, string country, string province, string city)
{
Id = id;
UnionId = unionId;
WxOpenId = wxOpenId;
WxNickName = wxNickName;
Thumb = thumb;
Gender = gender;
Country = country;
Province = province;
City = city;
}
public string Id { get; set; }
/// <summary>
/// 微信UnionId
/// </summary>
[MaxLength(50)]
public string UnionId { get; set; }
/// <summary>
/// 微信OpenId
/// </summary>
[MaxLength(50)]
public string WxOpenId { get; set; }
/// <summary>
/// 微信昵称
/// </summary>
[MaxLength(100)]
public string WxNickName { get; set; }
/// <summary>
/// 头像
/// </summary>
[MaxLength(200)]
public string Thumb { get; set; }
/// <summary>
/// 性别(1-男;2-女;)
/// </summary>
public int Gender { get; set; }
/// <summary>
/// 国家
/// </summary>
[MaxLength(100)]
public string Country { get; set; }
/// <summary>
/// 省份
/// </summary>
[MaxLength(100)]
public string Province { get; set; }
/// <summary>
/// 城市
/// </summary>
[MaxLength(100)]
public string City { get; set; }
}
}
创建 \Models\DatabaseModel\Mapping\Admin_UserConfigurationMapping.cs
源码如下:
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Senparc.Ncf.Core.Models.DataBaseModel;
using Senparc.Ncf.XncfBase.Attributes;
using Senparc.Xncf.Admin.Models.DatabaseModel;
namespace Senparc.Xncf.Admin.Models
{
[XncfAutoConfigurationMapping]
public class Admin_UserConfigurationMapping : ConfigurationMappingWithIdBase<User, string>
{
public override void Configure(EntityTypeBuilder<User> builder)
{
// do something
}
}
}
修改 \Models\DatabaseModel\AdminSenparcEntities.cs
using Microsoft.EntityFrameworkCore;
using Senparc.Ncf.Database;
using Senparc.Ncf.Core.Models;
using Senparc.Ncf.XncfBase.Database;
namespace Senparc.Xncf.Admin.Models.DatabaseModel
{
public class AdminSenparcEntities : XncfDatabaseDbContext
{
public AdminSenparcEntities(DbContextOptions dbContextOptions) : base(dbContextOptions)
{
}
//DOT REMOVE OR MODIFY THIS LINE 请勿移除或修改本行 - Entities Point
//ex. public DbSet<Color> Colors { get; set; }
public DbSet<User> Users { get; set; }
//如无特殊需需要,OnModelCreating 方法可以不用写,已经在 Register 中要求注册
//protected override void OnModelCreating(ModelBuilder modelBuilder)
//{
//}
}
}
网页部分
模块中的网页部分在
页面可以根据自己的实际需要去排版
html
index.cshtml 源码:
@page
@model Senparc.Xncf.Admin.Areas.Admin.Pages.User.IndexModel
@{
ViewData["Title"] = "用户页面";
Layout = "_Layout_Vue";
}
@section Style{
<link href="~/css/Admin/User/User.css" rel="stylesheet" />
}
@section breadcrumbs {
<el-breadcrumb-item>扩展模块</el-breadcrumb-item>
<el-breadcrumb-item>用户管理</el-breadcrumb-item>
<el-breadcrumb-item>用户列表</el-breadcrumb-item>
}
<div>
<div class="admin-role">
<el-row class="filter-condition" :gutter="18">
<el-col :span="4"><el-input v-model="keyword" placeholder="请输入关键字"></el-input></el-col>
<el-col :span="6">
<el-button type="primary" @@click="handleSearch()">查询</el-button>
<el-button type="primary" @@click="resetCondition()">重置</el-button>
</el-col>
</el-row>
<div class="filter-container">
<el-button class="filter-item" size="mini" type="primary" icon="el-icon-plus" @@click="handleEdit('','','add')">新增</el-button>
</div>
<el-table :data="tableData"
style="width: 100%;margin-bottom: 20px;"
row-key="id"
border
ref="multipleTable"
@@selection-change="handleSelectionChange">
<el-table-column label="序号" width="65">
<template scope="scope">
<el-radio :label="scope.$index" v-model="radio" @@change.native="getCurrentRow(scope.row)"></el-radio>
</template>
</el-table-column>
<el-table-column prop="unionId" align="left" label="微信UnionId"></el-table-column>
<el-table-column prop="wxOpenId" align="left" label="微信OpenId"></el-table-column>
<el-table-column prop="wxNickName" align="left" label="微信昵称"></el-table-column>
<el-table-column prop="thumb" align="center" label="头像">
<template slot-scope="scope">
<a :href="scope.row.thumb ? scope.row.thumb : 'demo.png'" target="_blank">
<img :src="scope.row.thumb ? scope.row.thumb : 'demo.png'">
</a>
</template>
</el-table-column>
<el-table-column prop="gender" align="left" label="性别"></el-table-column>
<el-table-column prop="country" align="left" label="国家"></el-table-column>
<el-table-column prop="province" align="left" label="省份"></el-table-column>
<el-table-column prop="city" align="left" label="城市"></el-table-column>
<el-table-column align="center"
label="添加时间">
<template slot-scope="scope">
{{formaTableTime(scope.row.addTime)}}
</template>
</el-table-column>
<el-table-column label="操作" align="center" fixed="right" width="150">
<template slot-scope="scope">
<el-button size="mini"
type="primary"
@@click="handleEdit(scope.$index, scope.row,'edit')">编辑</el-button>
<el-popconfirm placement="top" title="确认删除此用户吗?" @@on-confirm="handleDelete(scope.$index, scope.row)">
<el-button size="mini" type="danger" slot="reference">删除</el-button>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<pagination :total="paginationQuery.total"
:page.sync="listQuery.pageIndex"
:limit.sync="listQuery.pageSize"
@@pagination="getList"></pagination>
<!--编辑、新增-->
<el-dialog :title="dialog.title"
:visible.sync="dialog.visible"
:close-on-click-modal="false"
width="700px">
<el-form ref="dataForm"
:rules="dialog.rules"
:model="dialog.data"
:disabled="dialog.disabled"
label-position="left"
label-width="100px"
style="max-width: 200px; margin-left:50px;">
<el-form-item label="微信UnionId" prop="unionId">
<el-input v-model="dialog.data.unionId" clearable placeholder="请输入微信UnionId" />
</el-form-item>
<el-form-item label="微信OpenId" prop="wxOpenId">
<el-input v-model="dialog.data.wxOpenId" clearable placeholder="请输入微信OpenId" />
</el-form-item>
<el-form-item label="微信昵称" prop="wxNickName">
<el-input v-model="dialog.data.wxNickName" clearable placeholder="请输入微信昵称" />
</el-form-item>
<el-form-item label="头像">
<el-upload action="@Model.UpFileUrl"
list-type="picture-card"
show-file-list="true"
accept="image/png, image/jpeg"
:on-success="uploadSuccess"
:on-error="uploadError"
:on-preview="handlePictureCardPreview"
:on-remove="handleRemove">
<i class="el-icon-plus"></i>
<div class="el-upload__tip" slot="tip">不能超过100MB</div>
</el-upload>
<img width="100%" :src="dialogImageUrl" alt="">
<el-input class="hidden" v-model="dialog.data.thumb" clearable placeholder="头像" />
</el-form-item>
<el-form-item label="性别">
<el-input v-model="dialog.data.gender" clearable placeholder="请输入性别" />
</el-form-item>
<el-form-item label="国家" prop="country">
<el-input v-model="dialog.data.country" clearable placeholder="请输入国家" />
</el-form-item>
<el-form-item label="省份" prop="province">
<el-input v-model="dialog.data.province" clearable placeholder="请输入省份" />
</el-form-item>
<el-form-item label="城市" prop="city">
<el-input v-model="dialog.data.city" clearable placeholder="请输入城市" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @@click="dialog.visible=false">取消</el-button>
<el-button :loading="dialog.updateLoading" :disabled="dialog.disabled" type="primary" @@click="updateData">确认</el-button>
</div>
</el-dialog>
</div>
</div>
@section scripts
{
<script src="~/js/Admin/Pages/User/user.js"></script>
}
index.cshtml.cs 源码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Senparc.Ncf.Service;
using Microsoft.Extensions.DependencyInjection;
using Senparc.Ncf.Core.Models;
using Senparc.CO2NET.Trace;
using Senparc.Ncf.Utility;
using Senparc.Xncf.Admin.Models.DatabaseModel.Dto;
using Senparc.Xncf.Admin.Services;
namespace Senparc.Xncf.Admin.Areas.Admin.Pages.User
{
public class IndexModel : Senparc.Ncf.AreaBase.Admin.AdminXncfModulePageModelBase
{
private readonly UserService _userService;
private readonly IServiceProvider _serviceProvider;
public UserDto userDto { get; set; }
public string Token { get; set; }
public string UpFileUrl { get; set; }
public string BaseUrl { get; set; }
public IndexModel(Lazy<XncfModuleService> xncfModuleService, UserService userService, IServiceProvider serviceProvider) : base(xncfModuleService)
{
CurrentMenu = "User";
this._userService = userService;
this._serviceProvider = serviceProvider;
}
[BindProperty(SupportsGet = true)]
public int PageIndex { get; set; } = 1;
public PagedList<Models.DatabaseModel.User> User { get; set; }
public Task OnGetAsync()
{
BaseUrl = $"{Request.Scheme}://{Request.Host.Value}";
UpFileUrl = $"{BaseUrl}/api/v1/common/upload";
return Task.CompletedTask;
}
public async Task<IActionResult> OnGetUserAsync(string keyword, string orderField, int pageIndex, int pageSize)
{
var seh = new SenparcExpressionHelper<Models.DatabaseModel.User>();
seh.ValueCompare.AndAlso(!string.IsNullOrEmpty(keyword), _ => _.WxNickName.Contains(keyword));
var where = seh.BuildWhereExpression();
var response = await _userService.GetObjectListAsync(pageIndex, pageSize, where, orderField);
return Ok(new
{
response.TotalCount,
response.PageIndex,
List = response.Select(_ => new
{
_.Id,
_.LastUpdateTime,
_.Remark,
_.UnionId,
_.WxOpenId,
_.WxNickName,
_.Thumb,
_.Gender,
_.Country,
_.Province,
_.City,
_.AddTime
})
});
}
}
}
Edit.cshtml 源码
@page
@model Senparc.Xncf.Admin.Areas.Admin.Pages.User.EditModel
@{
ViewData["Title"] = $"{ (!string.IsNullOrEmpty(Model.Id.ToString()) ? "编辑" : "新增")}用户";
}
Edit.cshtml.cs 源码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Senparc.Ncf.Service;
using Senparc.CO2NET.Trace;
using Senparc.CO2NET.Extensions;
using Senparc.Xncf.Admin.Models.DatabaseModel.Dto;
using Senparc.Xncf.Admin.Services;
namespace Senparc.Xncf.Admin.Areas.Admin.Pages.User
{
public class EditModel : Senparc.Ncf.AreaBase.Admin.AdminXncfModulePageModelBase
{
private readonly UserService _userService;
public EditModel(UserService userService, Lazy<XncfModuleService> xncfModuleService) : base(xncfModuleService)
{
CurrentMenu = "User";
_userService = userService;
}
[BindProperty(SupportsGet = true)]
public string Id { get; set; }
public UserDto UserDto { get; set; }
/// <summary>
/// Handler=Save
/// </summary>
/// <returns></returns>
public async Task<IActionResult> OnPostSaveAsync([FromBody] UserDto userDto)
{
if (userDto == null)
{
return Ok(false);
}
await _userService.CreateOrUpdateAsync(userDto);
return Ok(true);
}
public async Task<IActionResult> OnPostDeleteAsync([FromBody] string[] ids)
{
var entity = await _userService.GetFullListAsync(_ => ids.Contains(_.Id));
await _userService.DeleteAllAsync(entity);
IEnumerable<string> unDeleteIds = ids.Except(entity.Select(_ => _.Id));
return Ok(unDeleteIds);
}
}
}
style
创建 \wwwroot\css\Admin\User\User.css
源码如下:
.el-dialog .el-form-item .el-input,
.el-dialog .el-form-item .el-textarea {
width: 30rem;
}
.el-form-item__content {
width: 30rem;
}
.filter-condition {
margin-bottom: 1rem;
}
.hidden {
display: none;
}
.col-thumb img {
width: 6rem;
height: 4rem;
}
.col-file video {
width: 8rem;
height: 5rem;
}
.col-file audio {
width: 10rem;
height: 5rem;
}
.item .item-left {
text-align: right;
padding-right: 1rem;
background: #909399;
color: #fff;
height: 2rem;
line-height: 2rem;
}
.item .item-right {
text-align: left;
padding-left: 1rem;
/*background: #909399;*/
border:1px solid #909399;
color: #000;
height: 2rem;
line-height: 2rem;
}
.item-right img, .item-right video {
width: 15rem;
-webkit-filter: drop-shadow(10px 10px 10px rgba(0,0,0,.5)); /*考虑浏览器兼容性:兼容 Chrome, Safari, Opera */
filter: drop-shadow(10px 10px 10px rgba(0,0,0,.5));
}
.el-row{padding-bottom:1rem;}
.el-form-item{margin-bottom:5px;}
javascript
创建 \wwwroot\js\Admin\Pages\User\user.js
源码如下:
new Vue({
el: "#app",
data() {
var validateCode = (rule, value, callback) => {
callback();
};
return {
defaultMSG: null,
editorData: '',
form:
{
content: ''
},
config:
{
initialFrameHeight: 500
},
//分页参数
paginationQuery:
{
total: 5
},
//分页接口传参
listQuery: {
pageIndex: 1,
pageSize: 20,
keyword: '',
orderField: ''
},
keyword:'',
multipleSelection: '',
radio: '',
props: { multiple: true },
// 表格数据
tableData:[],
uid: '',
fileList:[],
dialogImageUrl: '',
dialogVisible: false,
dialog:
{
title: '新增用户',
visible: false,
data:
{
id:'',unionId: '', wxOpenId: '', wxNickName: '', thumb: '', gender: 0, country: '', province: '', city: ''
},
rules:
{
name:
[
{ required: true, message: "用户名称为必填项", trigger: "blur" }
]
},
updateLoading: false,
disabled: false,
checkStrictly: true // 是否严格的遵守父子节点不互相关联
}
}
},
created: function() {
let that = this
that.getList();
},
watch:
{
'dialog.visible': function(val, old) {
// 关闭dialog,清空
if (!val)
{
this.dialog.data = {
id:'',unionId: '', wxOpenId: '', wxNickName: '', thumb: '', gender: 0, country: '', province: '', city: ''
};
this.dialog.updateLoading = false;
this.dialog.disabled = false;
}
}
},
methods:
{
handleChange(value) {
console.log(value);
},
handleRemove(file, fileList) {
log(file, fileList,2);
},
handlePictureCardPreview(file) {
let that = this
that.dialogImageUrl = file.url;
that.dialogVisible = true;
},
uploadSuccess(res, file, fileList) {
let that = this
that.fileList = fileList;
if (res.code == 200)
{
that.$notify({
title: '成功',
message: '恭喜你,上传成功',
type: 'success'
});
that.dialog.data.cover = res.data;
}
else
{
that.$notify.error({
title: '失败',
message: '上传失败,请重新上传'
});
}
},
uploadError() {
let that = this
that.$notify.error({
title: '失败',
message: '上传失败,请重新上传'
});
},
// 获取列表
async getList()
{
let that = this
let { pageIndex, pageSize, keyword, orderField } = that.listQuery;
if (orderField == '' || orderField == undefined)
{
orderField = 'AddTime Desc';
}
if (that.keyword != '' && that.keyword != undefined) {
keyword = that.keyword;
}
await service.get(`/Admin/User/Index?handler=User&pageIndex=${pageIndex}&pageSize=${pageSize}&keyword=${keyword}&orderField=${orderField}`).then(res => {
that.tableData = res.data.data.list;
that.paginationQuery.total = res.data.data.totalCount;
});
},
// 编辑 // 新增用户 // 增加下一级
handleEdit(index, row, flag)
{
let that = this
that.dialog.visible = true;
if (flag === 'add')
{
// 新增
that.dialog.title = '新增用户';
that.dialogImageUrl = '';
return;
}
// 编辑
let { id,unionId, wxOpenId, wxNickName, thumb, gender, country, province, city } = row;
that.dialog.data = {
id,unionId, wxOpenId, wxNickName, thumb, gender, country, province, city
};
if (flag === 'edit')
{
that.dialog.title = '编辑用户';
}
},
// 设置父级菜单默认显示 递归
recursionFunc(row, source, dest)
{
if (row.categoryId === null)
{
return;
}
for (let i in source)
{
let ele = source[i];
if (row.categoryId === ele.id)
{
this.recursionFunc(ele, this.categoryData, dest);
dest.push(ele.id);
}
else
{
this.recursionFunc(row, ele.children, dest);
}
}
},
// 更新新增、编辑
updateData()
{
let that = this
that.dialog.updateLoading = true;
that.$refs['dataForm'].validate(valid => {
// 表单校验
if (valid)
{
that.dialog.updateLoading = true;
let data = {
Id: that.dialog.data.id,
UnionId: that.dialog.data.unionId,
WxOpenId: that.dialog.data.wxOpenId,
WxNickName: that.dialog.data.wxNickName,
Thumb: that.dialog.data.thumb,
Gender: that.dialog.data.gender,
Country: that.dialog.data.country,
Province: that.dialog.data.province,
City: that.dialog.data.city
};
service.post("/Admin/User/Edit?handler=Save", data).then(res => {
if (res.data.success)
{
that.getList();
that.$notify({
title: "Success",
message: "成功",
type: "success",
duration: 2000
});
that.dialog.visible = false;
}
});
}
});
},
// 删除
handleDelete(index, row) {
let that = this
let ids = [row.id];
service.post("/Admin/User/edit?handler=Delete", ids).then(res => {
if (res.data.success)
{
that.getList();
that.$notify({
title: "Success",
message: "删除成功",
type: "success",
duration: 2000
});
}
});
},
getCurrentRow(row) {
let that = this
that.multipleSelection = row;
},
handleSearch() {
let that = this
that.getList();
},
resetCondition() {
let that = this
that.keyword = '';
}
}
});
权限配置
修改 \Register.Area.cs
源码如下:
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Senparc.CO2NET.Trace;
using Senparc.Ncf.Core.Areas;
using Senparc.Ncf.Core.Config;
using System;
using Senparc.Ncf.XncfBase;
using System.Collections.Generic;
using System.IO;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Builder;
using Senparc.CO2NET.RegisterServices;
using Microsoft.Extensions.FileProviders;
using System.Reflection;
namespace Senparc.Xncf.Admin
{
public partial class Register : IAreaRegister, //注册 XNCF 页面接口(按需选用)
IXncfRazorRuntimeCompilation //赋能 RazorPage 运行时编译
{
#region IAreaRegister 接口
public string HomeUrl => "/Admin/Admin/Index";
public List<AreaPageMenuItem> AareaPageMenuItems => new List<AreaPageMenuItem>() {
new AreaPageMenuItem(GetAreaHomeUrl(),"首页","fa fa-laptop"),
//新增的菜单
new AreaPageMenuItem(GetAreaUrl($"/Admin/User/Index"),"用户","fa fa-bookmark-o"),
};
public IMvcBuilder AuthorizeConfig(IMvcBuilder builder, IHostEnvironment env)
{
builder.AddRazorPagesOptions(options =>
{
//此处可配置页面权限
});
SenparcTrace.SendCustomLog("Admin 启动", "完成 Area:AllTheCode.Xncf.Admin 注册");
return builder;
}
public override IApplicationBuilder UseXncfModule(IApplicationBuilder app, IRegisterService registerService)
{
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new ManifestEmbeddedFileProvider(Assembly.GetExecutingAssembly(), "wwwroot")
});
return base.UseXncfModule(app, registerService);
}
#endregion
#region IXncfRazorRuntimeCompilation 接口
public string LibraryPath => Path.GetFullPath(Path.Combine(SiteConfig.WebRootPath, "..", "..", "Senparc.Xncf.Admin"));
#endregion
}
}
根据以上的创建方法可以完成系统中任何需要的功能
版权声明: 本文为 InfoQ 作者【MartyZane】的原创文章。
原文链接:【http://xie.infoq.cn/article/2eb4ced38ad38c0646c7d1f7a】。文章转载请联系作者。
MartyZane
坚持不懈,直到成功 2021.03.15 加入
技术痴一枚,资深开发,喜欢交流,热爱开源,希望能与更多优秀的开发者一共为社会的进步贡献一份自己的力量
评论