HarmonyOS 实战小项目开发(二)


日常逼逼叨

在上期实战项目一中,已经对于练手项目的背景,后端搭建等做了一定的简述,那么本期将结合HarmonyOS 页面搭建个人性格测试的移动端。如有一些错误,希望观众老爷们批评指正


首先放一个图片,是整个的一个效果,至于页面布局配色方面的问题,希望大家勿喷,就我个人而言做不出具有美感的一些东西...,大家领会精神就行。

移动端搭建

移动端的app主要包含四个界面:首页、答题、空界面,结果展示界面,整个界面做的比较简洁,但是有覆盖到前面所说的一些知识点

项目文件夹展示

那首先呢,还是老规矩,在这里带领大家了解一下我的项目路径

  • /ets/component 主要是自定义的一些组件

    • ImageComponent 结果展示中图片组件
    • ProgressComponent 答题过程中进度条组件
    • PropertiesPanelComponent 结果展示属性面板组件
    • TitleComponent 各个页面菜单栏组件
    • TopicBodyComponent 题目展示答题区域组件
  • /ets/data 自定义的实体类
    • RES 后端数据返回解析到的实体类
  • /ets/pages 各个页面
    • EmptyPage 空界面
    • Index 主界面
    • IsHidden 自己之前测试的,在整个项目中没啥用途
    • ShowResPage 测试结果展示界面
    • Topic 题目展示界面
  • /ets/resources 资源文件 配置文件等,在这里不在过多赘述
主界面搭建

主界面的搭建比较粗糙,页面中没有做一些组件化的东西,大致就是采用行,列布局进行的堆叠,通过图片与单选按钮进行逻辑处理,确定用户选择了什么性别以及什么样的测试方式

在整个页面组件最初声明变量,分别用来保存各个单选按钮是否被选中的状态,以及用户选择性别等的状态以性别这个列布局为例子,进行相关代码中的布局以及判定逻辑的展示

  //最开始的变量声明
@State ischeckedA: boolean = false //按钮A的初始化状态,默认不选中
@State ischeckedB: boolean = false //按钮B的初始化状态,默认不选中
@State ischeckedC: boolean = false //按钮C的初始化状态,默认不选中
@State ischeckedD: boolean = false //按钮D的初始化状态,默认不选中
@State sex: number = -1; //性别 初始化状态为-1 0代表女性,1代表男性
@State type: string = ''; //测试名称,初始化为空字符串,
//性别选择模块
Column() {
Row() {
Column() {
Text($r('app.string.EntryAbility_tips'))//请选择您的性别 提示
.fontSize(15)
.fontWeight(FontWeight.Bold)
}
.width('100%')
}
.border({ width: 1 })
.height('5%') Row() {
Column() {
Image($r('app.media.sex_boy'))
.width(this.imageWidth)
.height(this.imageHeigt)
.border({ width: 1 })
.onClick(() => {
this.ischeckedA = true;
this.ischeckedB = false;
}) Radio({ value: 'sex_boy', group: 'sex' }).checked(this.ischeckedA)
.height(10)
.width(10)
.onChange((isChecked: boolean) => { if (isChecked) {
console.log('boy')
this.sex = 1;
}
})
}
.width('50%') Column() {
Image($r('app.media.sex_girl'))
.border({ width: 1 })
.width(this.imageHeigt)
.height((this.imageHeigt))
.onClick(() => {
this.ischeckedA = false;
this.ischeckedB = true;
})
Radio({ value: 'sex_girl', group: 'sex' }).checked(this.ischeckedB)
.height(10)
.width(10)
.onChange((isChecked: boolean) => {
if (isChecked) {
console.log('girl')
this.sex = 0;
}
})
}
.width('50%')
}
.height('20%')
}

可以通过用户的选择进入不同的界面,不论男女,如果选择了mbti,则进入mbti测试界面,如果选择了disc,则跳转至空界面,当然用户如果不进行选择就开始测试,页面也会提醒用户进行选择。

//开始测试按钮
Row() {
Column() {
Button('开始测试').type(ButtonType.Capsule)
.fontSize(15)
.fontWeight(FontWeight.Bold)
.onClick(() => { if (this.sex === -1 || this.type === '') {
AlertDialog.show(
{
title: '提示:',
message: '请选择性别或者测试方式!',
autoCancel: true,
alignment: DialogAlignment.Bottom,
offset: { dx: 0, dy: -200 },
gridCount: 4,
}
)
} else if (this.type === 'disc') {
console.log(this.sex.toString())
console.log(this.type.toString())
// router.pushUrl({ url: 'pages/EmptyPage' })
this.clickNext('pages/EmptyPage')
}
else {
//携带参数 进行页面跳转
console.log('' + this.sex)
console.log('' + this.type)
this.clickNext('pages/Topic')
}
})
}
.width('100%')
} //页面跳转自定义函数
clickNext(url:string) {
router.pushUrl({
url: url, //页面路由url
params: { //跳转时的携带的参数
args1: this.sex,
args2: this.type,
}
}, router.RouterMode.Single //单实例模式。
//如果目标页面的url在页面栈中已经存在同url页面,离栈顶最近的页面会被移动到栈顶,移动后的页面为新建页。
//如目标页面的url在页面栈中不存在同url页面,按标准模式跳转。
)
}

特别注意:如果想进行页面的跳转,你得让他知道你的基本路由,所以在添加新的界面的时候,一定要在 src/main/resources/base/profile下的main_pages.json进行路由配置,将新建界面的路由加进去才可以进行所谓的页面跳转

//main_pages.json
{
"src": [
"pages/Index",
"pages/EmptyPage",
"pages/Topic",
"pages/ShowResPage"
]
}
答题界面搭建

答题界面采用了组件的方式,将一个个模块按照组件的方式进行封装,然后进行调用,这样的话,在一定程度上可以精简页面,提高代码的复用性。相比index界面,答题界面的代码明显比较简洁,其中标题,进度条,题目答题区域都被封装成组件提取出去,在本页面使用时只需import进来,传入相关的参数即可。

//Topic.ets
import { TitleComponent } from '../component/TitleComponent'
import { ProgressComponent } from '../component/ProgressComponent'
import { TopicBodyComponent } from '../component/TopicBodyComponent'
import router from '@ohos.router' @Entry
@Component
struct Topic {
@State paramsFromIndex: object = router.getParams()
@State finishTopic: number = 0
@State allTopic: number = 0 build() {
Column({ space: 20 }) {
//标题
TitleComponent({ title: this.paramsFromIndex?.['args2'] + "测试 " })
//进度条
ProgressComponent({ finishTopic: this.finishTopic, allTopic: this.allTopic })
//答题界面
TopicBodyComponent({finishTopic:$finishTopic,allTopic:$allTopic})
}
.width('100%')
.height('100%')
.backgroundColor("#f0f0f0")
}
}

那么对于答题界面而言,topic.ets为父组件,标题,进度条,答题区域都为子组件。

进入该界面之后呢 首先得进行首页传值的接收,接收所传递的是哪种测试,同时将标题中界面展示字样进行修改。由于标题字样仅仅是做展示使用,也不会对这个值进行修改,父组件怎么传递,标题怎么渲染即可,所以父子组件之间传递值是属于单向传递,所以对于子组件中变量的监听与接收使用注解@Prop即可

/***
* 顶部状态栏
*/
import router from '@ohos.router' @Component
export struct TitleComponent {
@Prop title: string //父子传递为单向 父--->子 build() {
Row({ space: 22 }) {
Image($r('app.media.left')).width(50).height(50).fillColor(Color.White)
.margin({ left: 15 })
.onClick(() => {
router.back()
})
Text(this.title).fontSize(20)
}.height("10%")
.width('100%')
.border({ width: 1 }) }
}

在题目展示及答题区组件加载之前,先去加载后台数据,获取题目列表数据进行解析,获取到题目列表和题目总数。在答题的过程中,每当用户答完一道题后,已完成数目便会+1,由于父组件与该组件之间是@Link进行的双向数据传递,所以在该组件中获取到的数据,以及对于变量的修改,在父组件重视可以被感知的。

//渲染之前进行网络请求
aboutToAppear(): void {
//初始化数据
let list = [] //获取网络请求
let httpRequest = http.createHttp();
httpRequest.request("localhost:8899/homp/getAll", (err, data) => {
if (!err) {
//数据解析
const response = data.result.toString();
const res = JSON.parse(response).data for (let i = 0; i < res.length; i++) {
let item = res[i];
list.push({
id: item.sequenceNumber,
name: item.name,
optionA: item.optionA,
valA: item.valA,
optionB: item.optionB,
valB: item.valB,
});
// console.log(list[i].id)
}
this.allTopic = res.length //进行总题目数的修改
} else {
console.info('error:' + JSON.stringify(err));
}
}); this.data = new MyDataSource(list)
}

特别注意:由于网络请求这块不算做是默认存在的,得手动开启网络访问权限之后,才可以进行网络数据的获取,主要是对src/main/路径下的module.json5文件进行添加如下代码

//网路权限
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"usedScene": {
"when": "always"
}
}
]

而在主体答题区呢 主要是用到了一个Swiper组件,类似于实现答一题进行自动翻页的效果,并将获取到的数据进行循环渲染。而在游标到达最后一题,并且已经答完时,跳转至结果展示界面等待后端计算返回性格测评的结果

//答题区
Swiper(this.swiperController) {
LazyForEach(this.data, (item: Topic) => { //嵌套组件 显示题目
Column() {
//题干
Text(item.id + ". " + item.name) //选项A
Button() {
Row() {
Text(item.optionA)
}
}
.type(ButtonType.Normal)
.optionStyle()
.onClick(() => {
let index = Number(item.id)
this.finishTopic = index
if (index === this.data.totalCount()) {
//1-保存值
this.res.push(item.valA)
let ans = this.res.join('')
console.log("ans:" + ans)
//跳转界面(携带拼接好的选项字符串)
router.replaceUrl({
url: 'pages/ShowResPage',
params: {
ans: ans,
}
}) } else {
//1-保存值
this.res.push(item.valA)
//2-换到下一题
this.swiperController.showNext()
} }) //选项B
Button() {
Row() {
Text(item.optionB)
}
}.type(ButtonType.Normal)
.optionStyle()
.onClick(() => {
let index = Number(item.id)
this.finishTopic = index
if (index === this.data.totalCount()) {
//如果到最后一题了 显示提交按钮
//1-保存值
this.res.push(item.valB)
let ans = this.res.join('')
console.log("ans:" + ans)
//跳转界面
router.replaceUrl({
url: 'pages/ShowResPage',
params: {
ans: ans,
}
}) } else {
//1-保存值
this.res.push(item.valB)
//2-换到下一题
this.swiperController.showNext()
} }) }.width('90%')
.height(180)
.justifyContent(FlexAlign.SpaceEvenly)
}, item => item) }
.cardStyle() //自定义卡片样式
.cachedCount(2)
.index(0)
.interval(4000)
.indicator(false)
.loop(false)
.duration(1000)
.itemSpace(0)
.disableSwipe(true)
.curve(Curve.Linear)
.onChange((index: number) => {
// console.info(index.toString() + this.res.join(''))
})

答题进度中,由于父组件与展示题目子组件之间数据类似于双向绑定,在答题组件进行操作,答题后,会对已完成题目这个变量进行修改,而变量的变化能够被子组件感知并单向传递给进度组件,所以整个过程中,进度组件的显示也会随答题而发生变化

/***
* 题目列表中的进度模块
*/
@Component
export struct ProgressComponent {
@Prop finishTopic: number
@Prop allTopic: number build() {
Column() {
Row() {
Text('答题进度:')
.fontSize(20)
.fontWeight(FontWeight.Bold)
Stack() {//堆叠组件,将一个进度条和两个文本框进行堆叠,展示出比较好看的效果
Progress(
{
value: this.finishTopic,
total: this.allTopic,
type: ProgressType.Ring }).width(80)
Row() {
Text(this.finishTopic.toString())
.fontWeight(18)
.fontColor("#36D")
Text(' / ' + this.allTopic.toString())
.fontWeight(18)
.fontColor(Color.Black)
}
} }.cardStyle()
.margin({ top: 15, left: 10, right: 10 })
.justifyContent(FlexAlign.SpaceEvenly)
.backgroundColor("#FAEBD7") } }
}
//自定义卡片样式
@Styles function cardStyle() {
.width("95%")
.padding(20)
.backgroundColor(Color.White)
.borderRadius(15)
.shadow({ radius: 6, color: '#1F000000', offsetX: 2, offsetY: 4 })
}

那为了方便大家对于这块的理解,我做了如下的图示,方便大家理解父子组件传值在本案例中的应用

结果展示界面搭建

结果展示界面也是将一些布局以组件的形式提取出去,方便以后的复用等,为了防止一些内容显示不下,整个页面是在一个滚动组件中进行的布局,滚动布局中还是包括三段,顶部提示栏,图片展示栏,以及底部的属性面板;

在进入该页面时,在界面加载渲染之前同样是接收答题界面传递过来的答题选项字符串,在接收到这个答题选项字符串后进行后端数据的请求,请求采用post请求,携带结果字符串,经过后端接口计算后返回测试结果以及性格属性值

//post请求
aboutToAppear() {
//获取网络请求
let httpRequest = http.createHttp();
httpRequest.request("localhost:8899/homp/submit",
{
method: http.RequestMethod.POST,
extraData: {
"ans": this.paramsFromIndex?.['ans'],
}
},
(err, data) => {
if (!err) {
const response = data.result.toString();
const res = JSON.parse(response).data this.resShow = res;
console.log("this.resShow.res:" + this.resShow.res)
this.resStr = this.resShow.res
console.log("resStr:" + this.resStr) }
})
}

这三部分分别对应着三个自定义组件,标题组件之前有提到过,在这不做过多的赘述,我们重点看一下其他两个组件

图片组件

在结果展示页面获取到后端返回的测试结果后,解析成为一个结果对象,获取到对象的性格简称,传递给图片组件,图片组件在拿到性格简称之后,通过字符串拼接获取到资源文件下的相关性格图片进行展示。同时还为图片添加了简单的动画效果

@Component
// @Entry
export struct ImageComponent {
@State clicked: boolean = false;
@Prop resStr: string // @State resStr: string = 'ENTP' build() {
Row() {
Column() {
Image($rawfile(this.resStr + ".png"))
.width("60%")
.height("60%")
.objectFit(ImageFit.Contain)
// .border({ width: 1 })
.rotate({
x: 0,
y: 1,
z: 0,
angle: this.clicked ? 360 : 0
})
.scale(
this.clicked
? { x: 1.25, y: 1.25 }
: { x: 1, y: 1 }
)
.opacity(this.clicked ? 0.6 : 1)
.onClick(() => {
this.clicked = !this.clicked;
})
.animation(
{
delay: 10,
duration: 1000,
iterations: 1,
curve: Curve.Smooth,
playMode: PlayMode.Normal
}
)
Text(this.resStr)
.fontSize(25)
.width("90%")
.height("20%")
.decoration({
type: TextDecorationType.Underline,
color: Color.Orange
})
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Center) }.justifyContent(FlexAlign.SpaceEvenly) }
.height("30%")
.cardStyle()
.margin({ top: 15 })
// .border({ width: 1 })
.alignItems(VerticalAlign.Center)
.justifyContent(FlexAlign.Center)
}
}

属性面板组件

在结果展示页面获取到后端返回的测试结果后,解析成为一个结果对象,将这个对象使用@ObjectLink整体作为数据进行传递到属性面板组件,在拿到这个对象后,获取相关的一些字段进行渲染即可

在进行一些占比数据的渲染时,使用到了HarmonyOs内置滑动条组件( Slider),展示一些占比情况

import { Res } from '../data/RES'
//属性面板
@Component
// @Entry
export struct PropertiesPanelComponent {
@ObjectLink resReturn: Res build() {
Column() {
Column() {
Row() {
Text('erate').fontSize(12)
Slider({
value: Number(this.resReturn.erate),
min: 0,
max: 40,
style: SliderStyle.InSet
}).panelStyle() Text(this.resReturn.erate + " %").fontSize(12)
}.width('80%')
.justifyContent(FlexAlign.SpaceAround) Row() {
Text('irate').fontSize(12)
Slider({
value: Number(this.resReturn.irate),
min: 0,
max: 40,
style: SliderStyle.InSet
}).panelStyle() Text(this.resReturn.irate + " %").fontSize(12)
}.width('80%')
.justifyContent(FlexAlign.SpaceAround) Row() {
Text('srate').fontSize(12)
Slider({
value: Number(this.resReturn.srate),
min: 0,
max: 40,
style: SliderStyle.InSet
}).panelStyle() Text(this.resReturn.srate + " %").fontSize(12)
}.width('80%')
.justifyContent(FlexAlign.SpaceAround) Row() {
Text('nrate').fontSize(12)
Slider({
value: Number(this.resReturn.nrate),
min: 0,
max: 40,
style: SliderStyle.InSet
}).panelStyle() Text(this.resReturn.nrate + " %").fontSize(12)
}.width('80%')
.justifyContent(FlexAlign.SpaceAround) Row() {
Text('trate').fontSize(12)
Slider({
value: Number(this.resReturn.trate),
min: 0,
max: 40,
style: SliderStyle.InSet
}).panelStyle() Text(this.resReturn.trate + " %").fontSize(12)
}.width('80%')
.justifyContent(FlexAlign.SpaceAround) Row() {
Text('frate').fontSize(12)
Slider({
value: Number(this.resReturn.frate),
min: 0,
max: 40,
style: SliderStyle.InSet
}).panelStyle() Text(this.resReturn.frate + " %").fontSize(12)
}.width('80%')
.justifyContent(FlexAlign.SpaceAround) Row() {
Text('jrate').fontSize(12)
Slider({
value: Number(this.resReturn.jrate),
min: 0,
max: 40,
style: SliderStyle.InSet
}).panelStyle() Text(this.resReturn.jrate + " %").fontSize(12)
}.width('80%')
.justifyContent(FlexAlign.SpaceAround) Row() {
Text('prate').fontSize(12)
Slider({
value: Number(this.resReturn.prate),
min: 0,
max: 40,
style: SliderStyle.InSet
}).panelStyle() Text(this.resReturn.prate + " %").fontSize(12)
}.width('80%')
.justifyContent(FlexAlign.SpaceAround)
}.cardStyle()
.margin({ top: 15 }) Column() {
Text('disc').fontSize(12).margin({ bottom: 15 })
Text(this.resReturn.disc)
.fontSize(14)
.maxLines(15)
.lineHeight(20)
}.cardStyle()
.margin({ top: 15 })
.justifyContent(FlexAlign.SpaceAround)
}.width("100%")
.height("80%")
.alignItems(HorizontalAlign.Center)
}
} @Extend(Slider) function panelStyle() { //自定义滑动条样式
.width("50%")
.blockColor('#ffefeff5')
.trackColor('#ADD8E6')
.selectedColor('#ff168a86')
.showTips(true)
.enabled(false)
}

总结

那到这里呢,移动端的一部分功能就搭建完成了,个人在页面的布局,配色上比较随便,做的也比较简单,后续有时间可以在进行细化吧;

对于mbti测试呢现在就告一段落,相信认真看的小伙伴们一定记得还有一个disc测评没有做,其逻辑等应该和mbti大同小异,有兴趣的小伙伴可以加入进来,对这个项目进行优化升级等。


主要是对于HarmonyOS 的移动端开发基础知识的巩固,所以整体结构等都比较简单,就当练手了吧,项目已经上传至gitee,并且全部开源,有兴趣的小伙伴可以加入进行后续的开发

前端:mtbiTest

后端:homp

希望有兴趣的小伙伴加入一起,我们做大做强,再创辉煌

HarmonyOS 实战小项目开发(二)的更多相关文章

  1. Flask框架的学习与实战(二):实战小项目

    昨天写了一篇flask开发环境搭建,今天继续,进行一个实战小项目-blog系统. blog系统很简单,只有一个页面,然而麻雀虽小五脏俱全.这里目的不是为了做项目而做项目,这篇文章本意是通过这次练习传达 ...

  2. 【实战小项目】python开发自动化运维工具--批量操作主机

    有很多开源自动化运维工具都很好用如ansible/salt stack等,完全不用重复造轮子.只不过,很多运维同学学习Python之后,苦于没小项目训练.本篇就演示用Python写一个批量操作主机的工 ...

  3. Django集成celery实战小项目

    上一篇已经介绍了celery的基本知识,本篇以一个小项目为例,详细说明django框架如何集成celery进行开发. 本系列文章的开发环境: window 7 + python2.7 + pychar ...

  4. 实战小项目BUG纪录

    果然,作为程序员最可爱的女朋友就是各种BUG,解决了你的开发能力和开发效率就会上升到一个新的层次.反之,在你面对BUG的时候,如果轻易的就放弃了,你也就失去了一次自我成长的机会.学习就是这样的,我们有 ...

  5. 第003篇 深入体验C#项目开发(二)

    下半本的5个项目也看完了,还是跳着看,只看大概!        第6章 企业交互系统            作者入职一年,开始带新的2个实习生的项目!一个外资企业内部的OA交互系统,这次又是一个基于w ...

  6. python网页爬虫小项目开发

    这是我最近接的一个小项目,花了是整整四天多时间. 任务是将http://www.examcoo.com/index/detail/mid/7网站下所有的试卷里的试题全部提取出来,首先按照题型进行分类, ...

  7. Android 实践项目开发二

    在地图开发中项目中,我这周主要完成的任务是和遇到的问题是以下几个方面. 1.在本次的项目中主要是利用百度地图的.jar包实现地图的定位与搜索功能,需要在百度地图开发中心网站取得 密钥,并下载相关.ja ...

  8. 跟我一起做一个vue的小项目(二)

    这个vue项目是紧跟着之前的项目跟我一起做一个vue的小项目(一)来的. 我继续后面的开发(写的比较粗糙,边学边记录) 下图是header头部的样式 header组件内容如下 //header.vue ...

  9. C#-Xamarin的Android项目开发(二)——控件应用

    相信我,这不是一篇吐槽文章.... 基础控件 Android的控件和控件样式非常特别,它是一种内联特别高的设计模式,换句话说,它是非常烂的设计.... 但在这种特别的关系里还是有一定的规律的,下面我们 ...

  10. 小程序开发二三事--数据请求head的设置

    wx.request(OBJECT) 发起请求的方法有很多,默认为 GET,有效值:OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT: 一般在 ...

随机推荐

  1. uniapp picker组件实现二级联动

    https://blog.csdn.net/hxh_csdn/article/details/111504951 https://www.cnblogs.com/jstll/p/14149600.ht ...

  2. CF1656F Parametric MST 题解

    为了便于解题,先对 \(a\) 数组从小到大进行排序. 首先,根据定义可以得出总价值的表达式: \[\begin{aligned} W&=\sum\limits_{(u,v)\in E}[a_ ...

  3. R语言—数据基础及练习

    ## 创建leadership数据框 manager <- c(1,2,3,4,5) date <-c("10/24/08","10/28/08", ...

  4. Java开发者的Python进修指南:JSON利器之官方json库、demjson和orjson的实用指南

    JSON JSON作为目前最流行的传输格式,在Python中也有相应的实现方式.由于JSON格式的文本可以跨平台并且简单易用,因此被广泛传播.因此,我们今天的主要讨论内容是如何熟练地应用Python的 ...

  5. 【BUS】动画图解嵌入式常见的通讯协议:SPI、I²C、UART、红外 ......

    SPI传输 SPI数据传输 SPI数据传输 SPI时序信号 I2C传输 2C总线寻址 UART传输 PC-UART-MCU RS-232电平转换 红外控制 红外通信 红外信号接收.放大.整形 红外控制 ...

  6. CSS - 正确解决 float 高度坍塌的问题

    <!DOCTYPE html> <html lang="en" dir="ltr"> <head> <meta cha ...

  7. Android Studio 的 Gradle 面板没有 Task

    问题描述:Android Studio Gradle 窗口没有显示 task 列表的问题,如下图所示: 网上找了好久都没有找到原因,最后自己摸索,找了解决方法. 解决方法:依次点击:File -> ...

  8. Nginx的日志处理

    Nginx的日志处理 背景 之前一直被各种咨询nginx的使用问题. 大部分都是性能, 加模块, 以及一些tcp端口等的问题. 其实这些都还好, 还有一个比较麻烦的问题时日志相关的. nginx的日志 ...

  9. [转帖]TLS/SSL (Schannel SSP) 中的密码套件

    https://learn.microsoft.com/zh-cn/windows/win32/secauthn/cipher-suites-in-schannel 密码套件是一组加密算法. TLS/ ...

  10. [转帖]TiDB的使用限制

    https://docs.pingcap.com/zh/tidb/stable/tidb-limitations 本文会将详细描述 TiDB 中常见的使用限制,包括:标识符长度,最大支持的数据库.表. ...