前言

以前写过 form 表单, 但很不齐全, 这篇想做一个大整理. 主要讲讲在网站中使用原生 Form 的功能, 不足和扩展.

前端是原生的 HTML/JS, 后端是 ASP.NET Core Razor Pages.

Simplest Form Overview

form 的职责是让 user 可以把信息传递到服务端. 常见的使用场景是 contact / enquiry form.

结构大概长这样

<form method="post">
<input type="text" name="username">
<button type="submit">Submit</button>
</form>

一个 form tag 把所有信息包裹在里面

input text 作为 accessor 信息读写器. 还有很多种 accessor 用来输出输入不同种类的信息. 后面会详细讲到.

一个 submit button.

ASP.NET Core

public class FormData
{
public string Username { get; set; } = "";
}
public class IndexModel : PageModel
{
public void OnGet()
{ } public void OnPost([FromForm] FormData formData)
{
var value = formData.Username;
}
}

一个对象从 form 获取信息, 然后就可以做各做操作了, 比如存入数据库, 发电邮等等.

Form Attribute

form 有 3 个常用的 attribute

method

有 3 种 get, post, dialog

get 我没有用过, 也不知道什么时候会用到.

post 是每次用的

dialog 很新, Safari 15.4 (14-03-2022) 才支持, 我没有用过这篇就不介绍了 (看这篇)

action

action 用来声明 post 去服务端的地址. 没有填写的话就是和当前页面相同地址.

比如 current url : /contact, 那么就是 post to /contact

Razor Pages 注意事项

在 Razor Pages, 如果 post to other page 需要加多一个 attribute asp-antiforgery, 不然会报错 400 error 哦, 详情可以看这篇 ASP.NET Core – CSRF

<form method="post" action="/other-page" asp-antiforgery="true">

在 Razor Pages, 如果 post to same page 但不同 method 的话, 要添加 query params handler

<form method="post" action="?Handler=ExternalLogin" asp-antiforgery="true">

注: 直接添加 handler 会把其它 query params 给覆盖掉哦. 更好的方式是用 QueryHelpersQueryBuilder 做一个完整的

enctype

声明 form 的格式, 类似 Content-Type

application/x-www-form-urlencoded (默认): 信息会以 key-value encodeURIComponent 方式发送出去, 这个格式不支持文件上传哦.

multipart/form-data: 想上传文件就要用这个 (即使没有文件也是可以用的, 只是会有一些多余的信息, 比如分隔符号, 那个是为了 upload file 才需要的)

例子

<form action="/" method="post" enctype="multipart/form-data" asp-antiforgery="true">
<input type="text" name="username">
<input type="file" name="attachment">
<input type="submit" value="Submit">
</form>

ASP.Net Core

public class FormData
{
public string Username { get; set; } = "";
public IFormFile Attachment { get; set; } = null!;
}
public class IndexModel : PageModel
{
public void OnGet()
{ } public void OnPost([FromForm] FormData formData)
{
var value = formData.Username;
var fileSize = formData.Attachment.Length;
}
}

Form Submission

Submit Button

<button>Submit</button>
<button type="submit">Submit</button>
<input type="submit" value="Submit">

这 3 个效果是一样的.

button 默认的 type 就是 submit, 所以 1 和 2 是一样的.

input:submit 和 button 也是一样的, style 都一样.

通常我会用第 2 个. 比较明确.

multiple button in form

复杂的 form 里面会有多个 button 出现. 但通常只会有一个用来 submit.

所以其余的记得要写上 type="button".

input enter trigger submit button click

在任何一个 input (accessor) 按 enter 键, 游览器会找到 form 里面第一个 submit button (type=submit / image / no defined) 点击.

multiple submit button

一个常见的例子是让用户做简单的选择提交. 比如 external login.

<form method="post">
<button type="submit" name="provider" value="Google">Login with Google</button>
<button type="submit" name="provider" value="Microsoft">Login with Microsoft Account</button>
</form>

在 button 声明 name 和 value, 用户点击后, 被点击的那一个 submit value 会被放入信息中, 一起发送出去

还有一个用法是这样

<form method="post">
<input type="text" name="dataName">
<button type="submit" name="deleteType" value="SoftDelete">Soft Delete Data</button>
<button type="submit" name="deleteType" value="HardDelete">Hard Delete Data</button>
</form>

这个比较少见. 而且交互有点复杂. 不鼓励使用.

ASP.NET Core

public enum DeleteType
{
SoftDelete,
HardDelete
}
public class FormData
{
public string? DataName { get; set; }
public DeleteType? DeleteType { get; set; }
}
public class IndexModel : PageModel
{
public void OnGet()
{ } public void OnPost([FromForm] FormData formData)
{
}
}

listen to submission event

通过 JS 可以拦截 submit event

document.querySelector("form").addEventListener("submit", (e) => {
e.preventDefault();
alert("submit");
});

通过 preventDefault 可以阻止游览器发送到服务端. 通常目的是想改成使用 Ajax 发送, 下面会说到细节.

JS trigger submission

document.querySelector("form").submit();
document.querySelector("form").dispatchEvent(new Event("submit"));

.submit() 不会触发 submission event, 它会直接发送到服务端.

相反, dispatch 只会触发 submit event, 而不会发送到服务端 (不管有没有 prevent default).

所以 2 个的用法是不同的哦, 要依据场景来运用. 一般上 Ajax Form 会用 dispatchEvent, 刷新 form 会用 .submit().

Ajax Form

所谓 Ajax Form 就是替代原本的 form submit 刷新体验, 改成通过 Ajax 发送 form 的信息.

XMLHttpRequest

这是平常用来发 Ajax 的方式, Content-Type: application/json

var request = new XMLHttpRequest();
request.onreadystatechange = () => {
if (request.readyState == 4 && request.status == 200) {
alert("done");
}
};
request.open("POST", "/api/create-enquiry");
request.setRequestHeader("Content-Type", "application/json");
const dataJson = JSON.stringify({ username: "Derrick" });
request.send(dataJson);

ASP.NET Core

public class CreateEnquiryData
{
public string Username { get; set; } = "";
}
public class EnquiryController
{
[HttpPost("api/create-enquiry")]
public void CreateEnquiry([FromBody] CreateEnquiryData createEnquiryData)
{ }
}

form 的话, 它不是 JSON

request.open("POST", "/api/create-enquiry");
const searchParams = new URLSearchParams({ username: "Derrick" });
request.send(searchParams);

直接发送 URLSearchParams 相等于是 application/x-www-form-urlencoded.

multipart/form-data 则是发送 FormData

request.open("POST", "/api/create-enquiry");
const formData = new FormData();
formData.append("username", "Derrick");
request.send(formData);

注:

1.在发送 URLSearchParams 和 FormData 时, 不要去设置 Content-Type Header, 游览器会依据类型自动设定好. 诺我们自己去设置反而会破坏掉游览器的机制, 比如 FormData 会自动创建分隔符, 如果手动设置 Content-Type 则不会.

2.JSON 则需要手动设置 Content-Type, 不然会是 text/plain.

3. 小总结

<form> 必须去手动设置 application/x-www-form-urlencoded 或者 multipart/form-data

JSON 必须手动设置 application/json

request.send(formData or searchParams) 不要手动设置, 让游览器自己判断.

FormData From Form Tag

const formData = new FormData(document.querySelector("form"));

直接把 element 丢进去就可以了. (注: disabled 的 input 会自动过滤掉, 不会放入 FormData 的 key value 里)

要添加额外的信息也可以

formData.append('extraInfo', 'value');

有一种 upload 体验是这样的

它的原理是 HTML file 不要放 name, 这样它就排除在 submit 信息里

然后用 JS 去拦截它, 存取来

const files = [];
document.querySelector('input[type="file"]').addEventListener("input", (e) => {
files.push(e.target.files[0]); // save the file
});

最后, 通过 extra info 的方式 append 进去 formData

const formData = new FormData(document.querySelector("form"));
for (const file of files) {
formData.append("Attachments", file);
}
request.send(formData);

ASP.NET Core

public class CreateEnquiryData
{
public string Username { get; set; } = "";
public List<IFormFile> Attachments { get; set; } = new List<IFormFile>();
}
public class EnquiryController
{
[HttpPost("api/create-enquiry")]
public void CreateEnquiry([FromForm] CreateEnquiryData createEnquiryData)
{ }
}

FormData value

FormData 的 value 只能是 string | File

通常服务端会 convert from string, 比如 parse string number, parse string date

另外, ASP.NET Core parse boolean 是 "True", "False", 而不是 "0", "1" 哦.

Accessor

原生 form accessor 虽然挺多的, 但是一般上网站的 form 不会太复杂 (不像 control panel), 所以常用到的就那几个.

input: text, email, number, date, checkbox, file

textarea, select

radio group

checkbox group

input text

<input type="text" name="username" autofocus autocomplete="on" readonly disabled placeholder="e.g. example.com">

排除 validation attribute (下面会详细讲 validation part), 常用的 attribute 有

autofocus 进入页面后自动 focus 到当前的 input

autocomplete 游览器会缓存之前填写过的记入, name, phone, email, address 都会, 如果不希望这样就通过 off 把它关掉, 也可以直接在 <form autocomplete="off" > 把所有的关掉.

readonly & disabled

这 2 个共同点是让用户只能看无法修改, 比较大的区别是 readonly 的信息会提交到服务端, 而 disabled 则不会. (注: select 是没有 readonly 的哦).

另外 CSS display none 只能让元素看不到, 依然会提交到服务端哦, 只有 disabled 或者把 name 去掉才能阻止提交.

placeholder 用来写提示的

maxlength 通过 UI 限制 value length, 注: 它不是 validation 哦, 如果通过 JS input.value = 'value' 是可以超过 max length 的, 而且 submit 也不会有 validation error.

size 用来设置 input 的 width, size="4" 不等于 style: 4ch 哦, 游览器有自己的算法, 大约是 8ch.

input text + datalist

类似 autocomplete 的效果, 但是资料由自己设定而不是游览器缓存.

  <input list="browsers">
<datalist id="browsers">
<option value="Internet Explorer">
<option value="Firefox">
<option value="Chrome">
<option value="Opera">
<option value="Safari">
</datalist>

input email

email 的特色就是自动加了一个 email 的 validation, 其余的和 text 一样.

input number

number 限制了 value 只能是数字, a-z, 符号都不能输入, JS input.value 也输入不了. 但它接收 e, 因为这个是合格的数字, 但大部分情况业务是不允许 e 的.

右边多了一个上下箭头, 可以 increase 和 decrease value, 很方便

step 设置上下箭头按一下 +- 多少. 比如 step="30" 那么按 2 下加 value 就是 60.

input date

min, max 限制 UI 不能选大过或效果 min, max, 同时 validation 不允许大过或小过 min, max.

注: 限制往往有 2 种方式, 一种是 block from UI, 操作上无法输入不允许的值, 另一种是 validation, 可以输入, 但是提交的时候会 error.

注: date format 是 yyyy-mm-dd 只支持一种 format, 输入其它的会被无视. 参考这篇

input file

multiple 允许一次 select 多个文件

accept 支持的类型, 常用的有:

<input type="file" name="attachment" accept="image/png, image/jpeg, image/*, .sql">

用逗号做分隔符 (可以放空格, 比较好看, 它会清掉的), 也可以写 extension 哦

textarea

<textarea name="text" cols="20" rows="2"></textarea>

rows, cols 类似 input 的 size, 用来控制 width, height

textarea 默认是可以 resize 的, 想关掉可以通过 CSS style resize:none; 或者 vertical / horizontal 表示只允许某一边 resize.

select

<select name="cars">
<option value="">--Select--</option>
<optgroup label="Swedish Cars">
<option value="volvo">Volvo</option>
<option value="saab">Saab</option>
</optgroup>
<optgroup label="German Cars">
<option value="mercedes">Mercedes</option>
<option value="audi">Audi</option>
</optgroup>
</select>

select 有几个局限, 所以不是很好用, 如果内容不多建议用 radio group 替代.

1. --select-- 由于它不能 cancel 所以只能通过一个 select empty 让用户清空.

2. value must be string, 不接受 null, number

3. multiple 在 PC 很丑, 手机还可以

4. search 体验差

checkbox

<input type="checkbox" name="rememberMe" value="True" checked>

for ASP.NET Core value 放 "True" 去到 C# 会 binding 成 bool. 当 checked 时, 它才会有 key 传到服务器. 所以服务端不可以 set default true 哦.

radio group

  <input type="radio" id="html" name="fav_language" value="HTML">
  <label for="html">HTML</label><br>
  <input type="radio" id="css" name="fav_language" value="CSS">
  <label for="css">CSS</label><br>
  <input type="radio" id="javascript" name="fav_language" value="JavaScript">
  <label for="javascript">JavaScript</label>

same name 表示 same group, 最终被选中的 radio 会成为唯一的 fav_language 值

checkbox group

<input type="checkbox" name="phones" value="iPhone">
<input type="checkbox" name="phones" value="HuaWei">

当多选时, 最终的会有多个 phones key-vlaue, 游览器其实并没有正真的 checkbox group, 它只是允许放重复名字的 checkbox 而已.

ASP.NET Core 会把这些放入 List 中

public class FormData
{
public List<string> Phones { get; set; } = new List<string>();
}

Semantic HTML

参考: MDN – How to structure a web form

所有内容包裹在 form 里, form 不要嵌套 (以前好像 ok, 现在不鼓励了)

<form></form>

分组使用 section

<form>
<section>
<h2>Contact Information</h2>
<!-- accessor here -->
</section>
<section>
<h2>Contact Information</h2>
<!-- accessor here -->
</section>
<p><button type="submit">Submit</button></p>
</form>

submit button 用 p 抱着. 如果没有分组则可以不需要 section (不过我觉得 p > button 感觉怪怪的)

accessor 和 label 用 p 包裹 (也有人用 div 或者 ul > li 来包, Exabyte 是用 p 哦)

参考: stackoverflow1 和 stackoverflow2

<p>
<label for="number">
<span>Card number:</span>
<strong><abbr title="required">*</abbr></strong>
</label>
<input type="tel" id="number" name="cardnumber">
</p>

input, select 做法一样.

radio, checkbox list 用 fieldset > ul > li

<fieldset>
<legend>Title</legend>
<ul>
<li>
<label for="title_1">
<input type="radio" id="title_1" name="title" value="K">
King
</label>
</li>
<li>
<label for="title_2">
<input type="radio" id="title_2" name="title" value="Q">
Queen
</label>
</li>
<li>
<label for="title_3">
<input type="radio" id="title_3" name="title" value="J">
Joker
</label>
</li>
</ul>
</fieldset>

fieldset 长这样, 一个大筐筐加一个 legend title

Validation

参考: W3Schools – JavaScript Validation API

Overview

validation 的玩法大概是这样:

1. 声明条件, 比如在 input 写上 required, pattern="\d" type="number | email" 这些都是条件.

2. 游览器会依据条件来限制用户的输入, 比如 type="number" keydown 不接受 a-z (除了 e) 和符号.

3. 除了阻止用户输入, 另一种方式是 popup error message 告诉用户虽然你成功输入了值, 但值我不接受请你修改, 不然就无法提交.

虽然游览器 build-in 了许多条件, 限制,验证 error, 但依然满足不了所有的需求, 所以我们可以通过 JS 去完善它.

manual trigger validation

虽然可以 manual trigger, 但有些时候会失灵, 比如 minlength, maxlength 只能通过 UI 才会有 error (挺奇葩的)

const valid = input.checkValidity(); // boolean

get error message

console.log(input.validationMessage);

set error or error message

不管是 custom error 还是想修改原本的 error message 都可以用这个接口

input.setCustomValidity('error message');

popup error message

input.reportValidity();

check validation state

console.log('input.validity', input.validity); // validation info

所有条件都在这里了

interface ValidityState {
readonly badInput: boolean;
readonly customError: boolean;
readonly patternMismatch: boolean;
readonly rangeOverflow: boolean;
readonly rangeUnderflow: boolean;
readonly stepMismatch: boolean;
readonly tooLong: boolean;
readonly tooShort: boolean;
readonly typeMismatch: boolean;
readonly valid: boolean;
readonly valueMissing: boolean;
}

badInput input type="number" UI 输入 e 就会是 badInput. 但是 input.value = 'e' 这样是输入不到值的哦, 只有通过 UI 才可以输入 e.

customError 如果有调用 setCustomValidity 就会是 true

patternMismatch 对应 pattern=“\d” input text 的正则验证

rangeOverflowrangeUnderflow 对应 min, max (date or number)

stepMismatch 对应 type="number" step="20" value="18"

tooLong 和 tooShort 对应 maxlength 和 minlength

typeMismatch 对应 type="email"

valueMissing 对应 required。

注: badInput 和 valueMissing 是可能同时发生的,比如 input type number 输入 'e'

由于格式不对,所以 badInput = true,同时格式不对会导致 value = empty,所以 valueMissing 也等于 true。

valid 表示是否全部条件都满足,valid 和 checkValidity 的区别是 checkValidity 会 trigger invalid event,而 valid 只是一个属性。

注: 这些 value 都是 live 的哦, 并不需要调用 checkValidity()。比如我突然 required = true; valueMissing 会立马变成 true,valid 会立马变成 false。

奇葩现象

有时候 validation 会失灵, 唯一确保它可以跑的方式是通过 UI 去操作. JS 输入值会有许多奇葩现象:

JS 无法输入 e

const input = document.createElement('input');
input.type = 'number';
input.value = 'e';
console.log(input.value); // ''
console.log(input.valueAsNumber); // 'NaN'

UI 是可以输入 e 的, 但是会 badInput

stepMismatch 完全正常

const input = document.createElement('input');
input.type = 'number';
input.step = '20';
input.value = '19';
console.log('validity', input.validity.stepMismatch); // true

甚至不需要调用 checkValidity

typeMismatch 完全正常

const input = document.createElement('input');
input.type = 'email';
input.value = 'abc';
console.log('validity', input.validity.typeMismatch); // true

tooShort, tooLong 完成失灵

const input = document.createElement('input');
input.type = 'text';
input.minLength = 3;
input.maxLength = 5;
input.value = 'a';
console.log('validity', input.validity.tooShort); // false
input.value = '123456';
console.log('validity', input.validity.tooLong); // false
input.checkValidity();
console.log(input.value); // 123456
console.log('validity', input.validity.tooLong); // false

如果通过 UI 操作的话是可以弄出 error 的.

替换 error message

原生的 error message 有 3 个大问题,

language problem

它依据游览器的设置来提供语言, 但通常网站的语言是通过 URL 控制的.

consistency problem

不同游览器出的 error message 是不同的. 统一管理会比较方便

checkbox group no build-in

上面有提到, checkbox list 是没有 build-in 的, 它只是没有阻止你 multiple same name 而已

如果想做一个 required 它就不会像 radio group 那样. 所以只能自己处理.

Bootstrap 有 2 种 validation, 一种是原生, 一种是完全用它的 custom

但它的原生也是支持 set error message 的. 由此推断这是很 popular 的需求.

拦截 input event 然后判断当前 validity state 再通过 setCustomValidaity 替换掉 message.

input.setCustomValidity('error message');

注: 如果遇到 conditional validation,那还需要用到 MutationObserver 监听 attribute 变化,比如 required 等等。

ASP.NET Core form string empty become null

踩到一个坑, 前端 submit form string value = '' empty string, 后端接收以后变成了 null

原来是 [FromForm] 搞的鬼, JSON 则不会.

参考:

Asp.Net Core Model binding, how to get empty field to bind as a blank string?

Handling multipart requests with JSON and file uploads in ASP.NET Core

ConvertEmptyStringToNull stopped being honored after upgrading from RC1

2 个 solution, 一个是在 property 上面加 attribute

public class FormData
{
[DisplayFormat(ConvertEmptyStringToNull = false)]
public string Name { get; set; } = "";
}

另一个是在 Program.cs 配置 MvcOptions (参考上面的链接, 这个动作很大, 不推荐)

HTML – Native Form 原生表单功能集的更多相关文章

  1. React利用Antd的Form组件实现表单功能(转载)

    一.构造组件 1.表单一定会包含表单域,表单域可以是输入控件,标准表单域,标签,下拉菜单,文本域等. 这里先引用了封装的表单域 <Form.Item /> 2.使用Form.create处 ...

  2. Flask入门 表单Flask-wtf form原生 Bootstrap渲染(七)

    (1) 原生的表单 模板页面,form表单form.html <form action="{{ url_for('/check/') }}" method='post'> ...

  3. elementui 使用Form表单 的 resetForm表单功能出现的问题

    代码因为在保密机上,这里只进行描述并截取elemen文档中的代码作为参考 今天在开发一个很简单需求的时候遇到的问题,在使用elementui的表单功能,将增和改的表单进行了复用,是在表单的父组件 dr ...

  4. Django form表单功能的引用(注册,复写form.clean方法 增加 验证密码功能)

    1. 在app下 新建 forms.py 定义表单内容,类型models from django import forms class RegisterForm(forms.Form): userna ...

  5. ExtJs 第二章,Ext.form.Basic表单操作

    1.认识Ext.form.Panel表单面板         Ext.form.field.CheckBox 复选框 checkboxfield Ext.form.CheckBoxGroup 复选框组 ...

  6. React Native 导入原生Xcode项目总结与记录

    html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,bi ...

  7. layui的表单功能

    作为一个phper还是非常喜欢这个插件的~虽然在vue的群里面说这个插件好被人怼过..废话不多说, 这次使用到的是layui的表单功能.上次的日历忘记做笔记了非常可惜,大部分其实跟着文档撸就可以,这次 ...

  8. 解析:使用easyui的form提交表单,在IE下出现类似附件下载时提示是否保存的现象

    之前开发时遇到的一个问题,使用easyui的form提交表单,在Chrome下时没问题的,但是在IE下出现类似附件下载时提示是否保存的现象. 这里记录一下如何解决的.其实这个现象不光是easyui的f ...

  9. wemall app商城源码Android之Native(原生)支付模式一demo

    wemall-mobile是基于WeMall的Android app商城,只需要在原商城目录下上传接口文件即可完成服务端的配置,客户端可定制修改.本文分享Native(原生)支付模式一demo,供技术 ...

  10. MySQL表空间集

    --MySQL表空间集 ----------------------2014-09-20 1. 收缩ibdata的方法,目前MySQL依然没有提供收缩ibdata的方法,只能重构,下面是5.7的步骤. ...

随机推荐

  1. PHP中引用的详解(引用计数、写时拷贝)

    转载:https://blog.csdn.net/ljguo212/article/details/8972865 1. PHP中引用的特性 PHP中引用意味着用不同的名字访问同一个变量内容,引用不是 ...

  2. Django REST framework的10个常见组件

    Django REST framework的10个常见组件: 权限组件 认证组件 访问频率限制组件 序列化组件 路由组件 视图组件 分页组件 解析器组件 渲染组件 版本组件

  3. [oeasy]python0010_怎么用命令行保存文件

    编写 py 文件 回忆上次内容 上次 真的输出了 程序员的浪漫 Hello world!   print函数 可以输出 字符串 但是 print这个词 别拼错 就连 大小写 也别能错 错了就改 也没事 ...

  4. oeasy 教您玩转linux 之 010209 装酷利器 hollywood

    我们来回顾一下 上一部分我们都讲了什么? 屏幕故障风格的软件包bb 可以设置音频 这次装一个酷 下个hollywood软件包 apt show hollywood apt search hollywo ...

  5. oeasy 教您玩转linux 010303文件管理器 nautilus

    我们来回顾一下 上一部分我们都讲了什么? 讲了火狐 火狐的位置 用命令行打开多个网址 火狐的升级 火狐桌面建立快捷方式 我们可以知道桌面快捷方式文件的名称么? 从文件管理器到命令行 按住文件 拖动到t ...

  6. 整数-笔记C

    实际情况也确实如此,C语言并没有严格规定 short.int.long 的长度,只做了宽泛的限制: short 至少占用 2 个字节. int 建议为一个机器字长.32 位环境下机器字长为 4 字节, ...

  7. exp解析

    1 #pragma once 2 #include <string> 3 #include <functional> 4 #include <type_traits> ...

  8. .NET 权限工作流框架 TOP 榜

    前言 .NET权限管理及快速开发框架.最好用的权限工作流系统. 基于经典领域驱动设计的权限管理及快速开发框架,源于Martin Fowler企业级应用开发思想及最新技术组合(SqlSugar.EF.Q ...

  9. 压力测试工具httperf使用方法

    目录 压力测试工具httperf使用方法 通过tar zxvf解压httperf-0.9.0.tar.gz 进入目录 安装c++编译环境 开始编译 进入编译后的bin目录 开始测试 压力测试工具htt ...

  10. 【MySQL】全库调整表大小写语句

    统一修改字段成小写+下划线的命名规则: V1上线后,重新看SQL调整的较可行的写法: # = = = = = = = = = = = = = = = 统一更改全库所有字段大小写脚本SQL(会删除字段原 ...