表单控件参考
FormPlatform 输入、布局、媒体和可视化控件的详细行为、契约与实现指南。
Pagination 分页组件
用途
`Pagination` 是可复用的多步骤表单容器,负责页面切换、每页验证、错误摘要,以及“上一页 / 下一页 / 提交”导航。它**不**拥有数据库 Provider、问卷部署或 Server Action;外围 `FormReader` 宿主接收普通提交事件并决定如何持久化。
因此,同一个组件可用于问卷、普通 Data Model 表单、功能表单,或完全由 Action Code 控制的流程。
流程
1. **上一页**:切换到上一个可见页面,不触发验证。
2. **下一页**:按本页验证方式处理:阻止进入下一页、不触发、触发但允许继续。
3. 当允许继续时,组件可选择先保存当前表单状态,再切换页面。
4. **提交**:验证全部可见页面,运行 `onSubmit`,再执行配置的最终完成动作。
显示出的页签是进度导航,不是绕过验证的入口。用户可以回到已到达页面;未来页签在 Next 到达前保持不可点击。
通用设置
| 设置 | 含义 |
|---|---|
| 显示页面页签头 | 在 Preview / Viewer 显示页签。Designer 始终显示,便于编辑页面。 |
| 页签头纵向排列 | 使用左侧页签栏;关闭时为可换行的横向页签。 |
| 点击下一页时自动提交数据 | 页面允许继续后保存当前数据。 |
| 下一页提交方式 | `普通表单提交` 发送普通提交;`问卷进度保存` 发送 `completionStatus: in_progress`。 |
| 最后一页完成方式 | 指定全部验证成功后的动作。 |
最后一页完成方式:
- **普通表单提交**:普通的已验证 FormReader 提交;适用于系统表单、Data Model 表单和功能表单。
- **问卷提交(已完成)**:发送带 `completionStatus: completed` 的提交;适用于已部署问卷。
- **仅执行 Submit 事件链**:不自动保存,由 `onSubmit` 处理流程。
- **自动(旧表单兼容)**:保留新属性出现前的 Pagination 行为。
新建 Pagination 默认使用普通表单提交,且不在 Next 保存。旧 schema 没有这些属性时仍保留原来的问卷兼容行为。
Events Tab
| 事件 | 运行时机 | 边界 |
|---|---|---|
| `onPrevious` | Previous 切换页面前 | 不验证。 |
| `onNext` | 当前页允许继续时 | “触发且阻止”验证失败时不运行;“触发但允许继续”时可能收到 `valid: false`。 |
| `onSubmit` | 所有可见页面验证成功后 | 在自动最终提交之前运行。 |
事件函数会获得正常 Form Action Context 和 `event.detail`:
{
pageId: 'page_2',
pageTitle: '联系人信息',
pageIndex: 1, // 从 0 开始
pageNumber: 2, // 从 1 开始
isLastPage: false,
valid: true,
validationMode: 'block'
}
`onSubmit` 还会收到 `completionMode`。同步全局 Action Code 函数可以返回 `false` 取消自动后续动作。异步 Form Action 模块函数异步运行,必须自行处理失败结果。
示例
普通业务表单
选择“普通表单提交”,保持 Next 自动保存关闭。最后一页走普通 Form Viewer 保存流程,再按提交按钮配置的 redirect 跳转。
已部署问卷
启用 Next 自动保存,选择“问卷进度保存(进行中)”,最后一页选择“问卷提交(已完成)”。问卷宿主在中间保存收到 `in_progress`,最后提交收到 `completed`。
Action Code 控制完成
选择“仅执行 Submit 事件链”,然后在 Events 中启用 `onSubmit`,填写 `completeRegistration`:
export default {
async completeRegistration({ api, event, data }) {
const result = await api.fetch('/api/registration/complete', {
method: 'POST',
body: JSON.stringify({ data, page: event.detail.pageNumber })
})
api.notify({ type: 'success', message: result.message || '已完成。' })
await api.navigate('/registration/thank-you')
}
}
若 Action Code 要改用普通 FormPlatform 持久化:
api.reader.submit({ redirectUrl: '/registration/thank-you' })
它会再次经由 FormReader 验证,然后向宿主发出普通提交事件。
限制
- Pagination 只验证按正常表单规则当前可见的控件。隐藏控件不会被 FormReader 提交。
- 导航事件属于 Pagination 控件;三个虚拟按钮不是独立保存的 Button 控件。外观在 General 配置,行为在 Events 配置。
- 问卷完成状态只在问卷部署宿主解释时有意义;普通 Viewer 会把它视为未使用的提交选项。
---
Calendar 控件与中国历法计算
English:CALENDAR_COMPONENT.md
架构与数据流
`FormCalendar.vue` 仍是标准的 `value` / `update:value` 表单控件。仅日期值保持公历民用字符串(`YYYY-MM-DD`);可选时间保持既有的本地 `YYYY-MM-DD HH:mm` 展示契约。月份网格将数值化的民用年、月、日传给纯函数模块 `lunarCalendar.js`,由该模块返回农历标签、节气标签和干支;它不解析 ISO 时间点,也不读取浏览器时区状态。
`LUNAR_INFO` 是 1900–2100 年农历数据的唯一依据。`0x8000` 至 `0x10` 位编码十二个普通月的大小,低四位给出闰月,`0x10000` 位给出闰月大小。换算从已知的 1900-01-31/农历 1900 正月初一开始,逐年、逐月扣减这些编码天数;不再推测大小月交替,也不再维护另一份春节或闰月表。
二十四节气按太阳视黄经每隔 15° 的边界定义。实现以紧凑的天文太阳模型对每个黄经边界进行数值求解,再通过儒略日运算把所得时刻转换为 UTC+8 民用日期。对于香港天文台指出的 1901–2100 年间四个极接近午夜的节气,代码采用其已发布的民用日期作为明确校正,防止分钟级模型误差跨到相邻日期;浏览器时区和固定节气间隔均不参与结果。年柱在立春的民用日期切换;月柱只在十二个“节”(小寒、立春、惊蛰……大雪)切换,并按立春后的有效年干通过五虎遁确定月干。
安全边界
这些计算仅在客户端用于显示;不发起网络请求,不依赖数据库或 API,也不接受可执行输入。表单值在正常提交边界仍是不可信输入;历法计算不承担提交验证或授权责任。
兼容性与限制
Vue props、Schema 契约、`v-model` 事件、样式及导航行为均不变。年份选择和箭头导航限制在 1900–2100 年;已经保存的越界值仍保留在输入框中,但历法页脚安全降级为破折号。`getSolarTerm(year, month)` 保持页脚所需的返回形式,并可传入可选日参数以标记日期格。`STEM_BRANCH_YEAR(year)` 为旧的公历年份调用保留;日期感知调用应使用 `yearStemBranch(year, month, day)` 和 `monthStemBranch(year, month, day)`。
干支页脚在表单已有选中值时跟随该值;尚未选择且正在浏览当前月份时使用今天的民用日期,导航到其他月份后才使用该月 1 日。控件不会为了显示今天的干支而擅自把今天写入表单值。
农历和节气对于无效日期或 1900–2100 以外的日期返回 `null`。所附农历数据不能表示 1900-01-31 之前的日期。日柱刻意接受任意有效的预推格里高利民用日期,仅对无效值返回 `null`,以保持不受时区影响的契约。年柱和月柱的边界依赖节气,因此要求日期位于支持范围内。
验证
在 `src/FormPlatform.Host/ClientApp` 执行 `npm run test:unit -- src/components/lunarCalendar.test.js`,然后执行 `npm run build`。直接测试覆盖普通月大小、闰月起止、2024 全部 24 节气、2021/2051/2083/2084 临近午夜的节气日期、立春和逐月“节”边界、跨年代日柱及无效/越界输入。农历和节气预期值以香港天文台 1901–2100 年公历与农历日期对照表为依据:<https://www.hko.gov.hk/tc/gts/time/conversion.htm>。
---
图表控件
FormPlatform 提供六种基于 Chart.js 的只读数据控件:**柱状图、折线图、散点图、圆环图、饼图、雷达图**。图表可以显示静态 JSON、API 返回值或表单 / 数据模型中的 JSON 字段;图表本身不会修改已绑定的数据。
添加与配置
1. 在表单设计器展开 **图表**,拖入所需图表。
2. 在 **通用** 标签页设置标题、标题字号、图例位置、响应式行为和高度。
3. 选择数据来源:
- **静态 JSON**:直接保存在表单 schema 中,适合固定说明图或小型演示。
- **API**:同源 JSON 接口。设计器不会调用真实 API,而会显示本地示例图,避免设计时产生业务请求。
- **表单数据绑定**:使用当前表单数据。需要持久化时,在 **其它 → 属性名** 填写字段,并映射到数据模型的 JSON 属性。
4. 勾选 **简化数据集** 时只绘制一个数据集;取消勾选则原样接收 Chart.js 的 `{ labels, datasets }` 格式。
简化数据格式
柱状图、折线图、圆环图、饼图、雷达图使用数值数组,标签在 **数据标签** 属性中设置:
[12, 19, 8, 15]
["Q1", "Q2", "Q3", "Q4"]
散点图使用 X/Y 点数组,通常不需要数据标签:
[
{ "x": 4, "y": 12 },
{ "x": 9, "y": 5 },
{ "x": 17, "y": 18 }
]
**数据集标签** 与 **数据集背景颜色** 设置该唯一数据集的显示信息。颜色可用 `#2563eb`、`rgba(37,99,235,.7)`,也可填写 JSON 颜色数组。
完整 Chart.js 格式
需要多个数据集、数据集级别选项或高级配色时,关闭 **简化数据集** 并传入 Chart.js 原生对象:
{
"labels": ["Q1", "Q2", "Q3", "Q4"],
"datasets": [
{ "label": "订单", "data": [12, 19, 8, 15], "backgroundColor": "#2563eb" },
{ "label": "退货", "data": [2, 4, 1, 3], "backgroundColor": "#f59e0b" }
]
}
此对象也可以存入数据模型的 JSON 列,并通过表单与数据映射读取。自动生成表时,图表控件会被推断为 JSON 字段。
API 数据来源
主应用提供仅用于测试的匿名示例接口:
| URL | 用途 |
|---|---|
| `/api/examples/charts/sales` | 简化数值数据:`{ "data": [12,19,8,15] }` |
| `/api/examples/charts/sales-full` | 完整的柱状 / 折线 / 雷达 Chart.js 数据 |
| `/api/examples/charts/scatter` | 简化散点数据 |
| `/api/examples/charts/scatter-full` | 完整散点 Chart.js 数据 |
生产模块的接口可以直接返回图表值,也可以返回 `{ "data": ... }`。示例:
api.MapGet("/monthly-sales", async (IOrderReport report, CancellationToken ct) =>
{
var rows = await report.MonthlySalesAsync(ct);
return Results.Ok(new
{
labels = rows.Select(x => x.MonthName),
datasets = new[]
{
new { label = "销售额", data = rows.Select(x => x.Total), backgroundColor = "#2563eb" }
}
});
}).RequireAuthorization();
不要因为图表需要数据而公开无限制报表接口。生产接口应使用模块自己的授权策略,并在服务器端验证全部查询参数。
表单数据绑定
例如,要显示记录中的 `monthlySales` JSON 字段:
1. 新建 **折线图**,选择 **表单数据绑定**,取消 **简化数据集**。
2. 在 **其它 → 属性名** 填写 `monthlySales`。
3. 在数据模型中新增同名 JSON 属性,并设置为可读映射。
4. 数据库中保存完整 Chart.js 数据对象。
图表只读。要编辑图表源数据,应使用 DataGrid、可编辑表格、Action 或独立输入表单。
响应式布局
每个图表都会观察它实际渲染所在的 FormPlatform 容器,包括 flex 横向布局、Tab/Page 切换、GridLayout 面板以及可收缩侧栏。默认开启 **响应式** 时,Chart.js 会按可用宽度重排绘制区域。控件外层始终设置 `min-width: 0`、`max-width: 100%` 并裁剪溢出,因此图表不能把所在表单行撑得比父容器更宽。
小屏幕建议使用适中的固定**高度**、将图例放在 `top` 或 `bottom`,并避免过长的分类标签。Chart.js 会在适当时自动跳过部分分类刻度。除非外层容器也有同样的响应式约束,否则不要在样式标签中设置固定宽度。
柱宽
柱状图在 General 标签页提供 **柱宽(px)**。`0`(或留空)表示让 Chart.js 自动计算,这是响应式场景推荐的默认值。正数会固定所有数据集柱子的像素宽度,包括完整 Chart.js 数据格式中的数据集。图表可能较窄时请使用适中的数值;标签很多时,过大的固定宽度可能令柱子重叠或被裁切。
安装说明
客户端新增了 `chart.js` 依赖。请在 `src/FormPlatform.Host/ClientApp` 执行一次 `npm install`,使 `package-lock.json` 同步;之后按原流程执行 `npm run build` 与主应用发布。使用 `npm ci` 的发布流程必须提交更新后的锁文件。
---
富文本编辑器控件
`富文本编辑器` 是一个可提交数据的表单控件,适用于由用户输入的短文章、格式化备注、说明、条款等 HTML 文本。它位于 Designer 的 **控件** 面板中,且不依赖 CMS 模块。
保存的内容
控件值是一个经过净化的 HTML 字符串,例如:
{
"articleBody": "<h2>欢迎</h2><p>请阅读<a href=\"/terms\">服务条款</a>。</p><ul><li>第一项</li></ul>"
}
在 **其它 → 属性名称** 中填写 `articleBody`(或数据库目标字段名)。之后它走普通表单数据链路:
- 已映射的表单会通过 ORM 写入字符串属性;
- 通用提交会写入 submission 的 JSON 数据;
- 问卷调查会写入自动生成的答卷表;
- 从表单生成数据表时,会生成文本/字符串列。
关系型 Data Model 建议使用足够大的文本类型:PostgreSQL 用 `text`、SQL Server 用 `nvarchar(max)`、MySQL 用 `longtext`。除非业务刻意限制长度,否则不要映射到很短的 `varchar`。
在设计器中配置
1. 从 **控件** 拖入 **富文本编辑器**。
2. 在 **通用** 设置标签、标签位置、占位文字、最小高度、是否显示工具栏、只读/禁用状态和变更防抖时间。
3. 在 **其它** 设置属性名称、默认值、必填规则、验证、可见/只读条件和事件;它与其它数据型控件一致。
4. 使用 Data Model 时,建立同名的字符串属性,再建立或更新表单映射。
新控件的初始值为空。只有确实需要默认文档时,才在 **其它 → 默认值** 中填写;它会像其它默认表单值一样被提交。
编辑体验
内置工具栏支持粗体、斜体、下划线、段落、二级标题、无序列表、有序列表、引用、链接和清除格式。控件支持标准的 `onChange`、`onFocus`、`onBlur` 事件;`onChange timeout` 会合并连续输入后再调用 Action Chain。
只读模式会以格式化后的安全 HTML 显示同一份内容,因此同一个控件可用于编辑表单和查看表单。
安全模型
富文本被当作**数据**而不是可执行页面代码。浏览器端和服务器端都会在显示或保存前应用同一套小型白名单。
允许的元素包括段落、换行、标题、强调、下划线/删除线、列表、引用、代码/预格式化文本和链接。链接只保留安全的 `href`:`http`、`https`、`mailto`、以 `/` 开头的站内链接及 `#` 锚点。
脚本、事件属性、内联样式、嵌入式 frame、表单、SVG/MathML、图片、任意 CSS class 和危险 URL scheme 都会被移除。因此本控件**不**支持 Tailwind class 或自定义 HTML 布局。页面布局请使用 FormPlatform 容器、样式标签、CMS 区块,或专门的自定义控件。
服务器端净化会在已映射实体保存、通用提交、问卷提交和经 ACL 授权的系统用户代填问卷提交前执行。自定义模块/API 若直接接收 HTML,仍必须把输入视为不可信;希望采用同一规则时可调用 `FormRichTextSanitizer.SanitizeHtml(value)`。
Schema 片段示例
{
"id": "articleBodyControl",
"type": "richText",
"props": {
"label": "文章正文",
"placeholder": "请输入公告内容…",
"minHeight": 280,
"toolbar": true,
"fluid": true
},
"other": {
"propertyName": "articleBody",
"required": true,
"validationTrigger": "blurAndDebouncedInput"
}
}
有意保留的边界
- 它不是 CMS 页面构建器,不能替代结构化 CMS 区块。
- 它不上传图片或文件;请使用文件上传控件或共享媒体服务,再按需要插入已批准的链接。
- 从 Word/Google Docs 粘贴时,不保证保留所有格式;不支持的格式会被刻意删除。
- 它不是协作编辑器;多人同时修改时沿用其它表单字段的版本/最后写入规则。
---
签名与照相控件
`签名`与`照相`是生成图片的正式数据控件。图片内容使用现有的 FormPlatform 媒体服务保存,而不是把图片二进制直接写入表单提交记录。
添加控件
在 Designer 的**输入控件**中拖入**签名**或**照相**。如果控件参与 Data Model 映射、表单生成数据表、通用提交记录或问卷答卷表,请在**其它**页指定唯一的**属性名**。
二者均支持标准的必填、自定义验证、只读、可见性、样式和 `onChange` 设置。
签名
受访者在支持鼠标、触控笔和触摸的画布上签名。只有点击**上传签名**时,画布才会生成 PNG 并上传。
- **清空画布**:清除尚未上传的笔迹。
- **取消**:丢弃尚未上传的笔迹。
- **上传签名**:上传 PNG,并将服务器返回的媒体引用写进表单值。
清空和取消不会删除已经保存的旧签名;上传替换签名时只会更新字段引用,旧的未引用媒体由正常的保留/清理策略处理。
通用页可设置画布高度、笔画颜色、背景色和笔画宽度。
照相
用户明确点击**打开相机**后才会请求浏览器相机。控件可选择前置/后置相机、拍摄 JPEG,或通过**从本地选择**上传已有图片。只有点击**上传照片**才会保存。
浏览器相机需要 HTTPS(或 `localhost`)及用户授权。如果相机不可用或用户拒绝授权,仍可从本地选择图片。
图片上传模式
通用页中的图片上传模式:
| 模式 | 行为 |
|---|---|
| 自动 | 问卷部署中使用问卷附件;其他已保存表单使用 FormPlatform 共享媒体。 |
| 平台表单共享媒体 | 使用普通表单媒体服务,适合映射表单、通用表单和功能表单。 |
| 问卷附件 | 写入 `survey_files`,仅适用于已部署问卷。 |
| 第三方自定义 API | 将图片发送到设置的 API URL;该 API 必须返回 ID 字符串或包含 `id` 的对象。如需控件立即显示上传后的图片,也应返回 `downloadUrl`。 |
共享媒体可设为**私有**或**公开**。普通表单必须先保存,才可上传共享媒体;Designer 中不会上传图片。
保存和映射
运行时字段值是媒体引用对象;标准关系映射保存其稳定的 `id` 到字符串列。因此“根据表单生成数据表”会为签名和照相生成字符串字段。手工设计 Data Model 时,请使用 string/varchar 属性(当前 FormPlatform ID 约定通常为 36 长度)。
问卷答卷表中保存附件 ID;再次加载答卷时,系统会将 ID 解析回下载引用,因此两个控件都会显示已保存图片。
安全边界
标准 `/api/forms/{formId}/media` 接口会验证目标 control ID 是否真的是允许共享媒体上传的签名或照相控件,不接受任意控件 ID。问卷上传仍执行部署、表单和受访者归属校验。自定义 API 的认证和验证由表单/模块开发者负责。
Schema 例子
{
"id": "customerSignature",
"type": "signature",
"props": {
"label": "客户签名",
"height": 180,
"penColor": "#0f172a",
"backgroundColor": "#ffffff",
"strokeWidth": 2,
"imageUploadMode": "auto",
"imageVisibility": "private"
},
"other": { "propertyName": "customer_signature", "required": true }
}
照相控件使用 `type: "camera"`,并可设置 `preferredCamera: "environment"` 和 `imageQuality: 0.9`。
---
FileUpload 文件上传控件
`FileUpload` 是普通表单和问卷调查表单使用的标准文件数据控件。它不同于旧的 `Input` + `type = file`:支持上传队列、多选、拖放、可配置文件信息行、图片缩略图以及平台持久化媒体引用。
加入表单
1. 在 Designer 的数据控件区拖入 **文件上传**。
2. 设置 **Property name**。它是表单数据属性;若有 Data Model Mapping,也是映射属性名。
3. 在 **General** 设置下述显示和存储属性。
4. 测试平台托管上传前先保存表单。共享媒体上传需要表单 ID 才能授权及归属。
通过 FormPlatform 上传时,表单值保存的是媒体引用而不是文件二进制内容。单文件控件产生一个引用;多文件控件产生引用数组。这样表单/答卷表保持精简,并由媒体服务统一控制下载授权。
通用属性
| 属性 | 含义 |
|---|---|
| 标签 / 标签定位 | 标准数据控件标签。 |
| 按钮文字 | 未选中 **Use DropZone** 时的文件选择按钮文字。 |
| 允许的文件类型 | 浏览器 `accept` 提示,例如 `.pdf,image/*`。只改善选择体验,不是安全策略;服务器/媒体 Provider 才是最终校验者。 |
| 图标图片文件类型 | 逗号分隔的文件名模式,例如 `*.png,*.jpg,*.gif`。当 MIME type 不可用时,用于判断图片图标/预览。 |
| 显示文件类型图标 | 为每个已选择或已上传文件显示紧凑图标。 |
| 自动处理队列 | 选择后立即上传。关闭后文件留在队列,用户点击 **上传队列** 才上传。 |
| 使用 DropZone | 用可点击的拖放区域替换选择按钮;接受相同文件和设置。 |
| 显示图片预览 | 本地浏览器已有预览或 Provider 返回下载 URL 时,显示图片缩略图。 |
| 允许安全预览 | 为图片、PDF、纯文本、CSV、JSON 增加单独的**预览**链接,并在新标签页打开。控件不会预览 HTML、Office 文档、可执行文件或未知类型。 |
| 允许多选 | 启用浏览器多选,值保存为引用数组。表单生成数据库表时,该字段自动为 JSON。 |
| 只读 / 禁用 | 标准行为。只读仍显示已上传项目和下载链接;两种状态均不允许选择或移除文件。 |
| ID 字段 | 自定义上传 API 返回对象时,作为唯一标识读取的字段。FormPlatform 媒体使用 `id`。可用嵌套路径,例如 `result.fileId`。 |
文件属性
文件属性表只控制每个上传项如何显示,不会把文件重复写入表单记录。
- **文件属性**:逻辑行(`name`、`length`、`contentType`、`token`)。
- **列标题**:显示标签。
- **表格字段**:从媒体引用对象读取的属性,例如 `fileName`、`fileSize`、`contentType`、`id`。
- **显示属性**:启用该行。只显示 Name 可形成简洁附件列表;增加 Length、Content type 可显示详细信息。
样式区域
常规的 **Style** Tab 设置仍控制 FileUpload 控件外层。FileUpload 还在同一个 Tab 中提供语义化内部区域,因此无需依赖脆弱的 DOM 选择器:
| 区域 | 被设置样式的元素 |
|---|---|
| 标签 | 顶部、左侧或右侧的 FileUpload 标签。 |
| 文件框 | 带边框的文件控件框。 |
| 选择/队列按钮 | 文件选择按钮,以及队列的上传/清空按钮。 |
| DropZone | 拖放选择区域。 |
| 文件列表 | 已保存和队列文件行的容器。 |
| 文件项目 | 每一条已保存或队列文件行。 |
| 文件信息 | 每行中可伸缩的文件名/元数据区域。 |
| 文件操作 | 预览、下载、清除操作。 |
| 缩略图 | 启用图片预览时的图片缩略图。 |
| 队列操作区 | 手动上传/清空队列的操作条。 |
每个区域可输入 Tailwind utility 或自定义 CSS 类。例如,**文件框**填 `rounded-xl border-slate-300 shadow-sm`,**选择/队列按钮**填 `bg-blue-600 text-white`。这些字段以 `*Class` 名称保存,因此表单 schema 保存时会被收集为 Tailwind 候选类。控件刻意保留这些语义结构元素;删除它们会使列表布局和分区样式定制不可靠。
存储模式
| 模式 | 使用场景 | 结果 |
|---|---|---|
| 自动 | 默认 | 问卷部署中上传为问卷附件;其他已保存表单上传为 FormPlatform 共享表单媒体。 |
| 平台表单共享媒体 | 普通受保护表单附件 | 通过表单媒体端点上传。私有媒体只能经表单授权下载;公开媒体可使用公开 URL。 |
| 问卷附件 | 已部署问卷 | 通过问卷附件端点上传;提交时会核对 deployment、form 和 respondent。 |
| 第三方自定义 API | 模块/Provider 自行管理文件 | 发送给配置的 API URL。API 应返回 ID 字符串,或带配置 ID 字段的对象,并可附带 `fileName`、`fileSize`、`contentType`、`downloadUrl`。 |
| 手动(Action Code) | 自定义浏览器流程 | 不自动上传。所选 `File`(多选为 `File[]`)可从表单 runtime file API 供 Action Code 处理。Action Code 未替换值时,普通提交值为文件名。 |
平台托管模式的 **媒体可见性** 默认是 `private`。只有确实应允许表单授权范围外下载的文件才设为 `public`。
下载与预览行为
内置 FileUpload 控件的每一个**下载**链接都会要求服务器返回 `Content-Disposition: attachment`,因此浏览器会下载文件,而不是直接以内嵌方式打开;图片、PDF、文本文件也同样如此。
**预览**是单独的可选属性。它只对保守白名单中的安全类型显示,并以新标签页打开,同时不让预览页面访问原页面。自定义上传 API 自己管理下载 URL 和行为;FormPlatform 不会向外部 URL 添加自身的下载参数。
Data Model Mapping 与建表
- 单文件 Property name 映射到可空 string/varchar 属性;存储值是媒体 ID/引用 ID。
- 多文件 Property name 映射到 JSON 属性。由表单生成数据表时,选择 **允许多选** 会自动使用 JSON。
- 这是标准数据控件:必填校验会确认至少有一个上传引用;`onChange`、`onFocus`、`onBlur` 进入正常事件链。
- 问卷的答卷表只存文件 ID。加载答卷时,服务器验证当前 respondent 或管理员权限后才将 ID 转成带下载 URL 的引用对象。
修改 **允许多选** 会改变答卷字段形态,应在 deployment 收集数据前完成。已有数据的问卷答卷表不会被系统自动重建。
自定义 API 示例
设置为 **第三方自定义 API**,Upload API URL 为 `/api/acme/documents`。兼容的返回如下:
{
"id": "9c601af0-3b28-4f18-a8b1-6f6136d9d2b9",
"fileName": "proposal.pdf",
"fileSize": 128004,
"contentType": "application/pdf",
"downloadUrl": "/api/acme/documents/9c601af0-3b28-4f18-a8b1-6f6136d9d2b9/download"
}
自定义端点须自行完成认证、授权、文件大小/内容校验,并把文件存放在表单数据记录之外;只返回控件显示所需的元数据。
安全说明
- `accept` 和扩展名判断只是界面辅助,不能作为信任边界。
- 平台表单媒体端点只允许已保存 schema 中、上传模式为 `auto` 或 `form` 的 `FileUpload` 控件调用。
- 问卷附件 ID 在提交时再次校验,因此不能通过提交任意 ID 盗用其他受访者的文件。
- 除非业务明确需要公开下载,否则不要把文件设为公开。
- 不要预览不可信的 HTML 或 Office 文档。除非表单确有需要,否则保持**允许安全预览**关闭。
旧 Input 文件控件
现有 `Input` + `type = file` 表单继续可用。所有新的多文件、队列、DropZone、缩略图或可配置文件信息行场景,建议使用 `FileUpload`。
---
CustomBlock 自定义区块控件
`CustomBlock` 是一个容器:既可以拥有普通子控件,也可以从已保存的表单或 JSON 渲染可复用组件树。适用于标准提示、共享的静态导航区块或预定义布局等可复用展示片段。
来源类型
| 来源类型 | 行为 |
|---|---|
| 放置控件 | 默认容器模式。可在区块内拖入、排序、配置子控件;子控件保存到当前表单 schema。 |
| 引用表单 | 按 **Form Name** 读取已保存表单的 `schema.components`,在当前表单内只读渲染。 |
| JSON 来源 | 解析组件数组、`{ "components": [...] }` 或 `{ "schema": { "components": [...] } }`,并只读渲染。 |
引用表单授权
引用表单通过既有 `GET /api/forms/by-name/{name}` API 读取。当前用户必须拥有该来源表单的读取权限。这样设计是为了确保嵌入表单不会绕过 Form Access 规则。
此 API 要求已认证,因此匿名/公开页面宜使用 **JSON 来源** 或普通本地区块子控件;除非应用流程本身已提供授权读取来源。
Designer 行为
- **放置控件** 模式下,CustomBlock 就是普通可编辑 Designer 容器。
- **引用表单**、**JSON 来源** 模式下,来源是只读可复用片段。若在父表单中直接编辑会悄悄产生副本、导致所有权不清,所以应编辑来源表单或 JSON 本身。
- JSON 无效、来源表单不存在或无权限时,界面会显示错误,不会静默变成空区块。
JSON 示例
[
{
"id": "welcome-title",
"type": "header",
"props": { "content": "欢迎", "size": 2 }
},
{
"id": "welcome-note",
"type": "staticContent",
"props": { "value": "此区块由 CustomBlock 提供。" }
}
]
引用区块最多嵌套六层,可避免表单间接引用自身而无限渲染。
---
SystemValue / 只读系统数据控件
`SystemValue` 是一种可提交的数据控件:界面只读,真正的值由 FormPlatform 在写入数据前的最后一刻由服务器赋值。它适合记录答卷/提交时间、当前身份、客户端 IP 等审计和请求上下文信息。
它支持普通 Data Model 映射、通用提交记录、问卷部署和经 ACL 授权的系统用户代填问卷;该控件不会参与字段验证。
添加和配置
1. 从控件区拖入 **系统值**。
2. 在 **Other** Tab 设置唯一的 **Property Name**,例如 `submitted_at`。
3. 在 **General** Tab 选择数据来源和写入时机。
4. 若表单使用 Data Model,请将该属性映射到对应 Attribute。只有希望由数据库本身维护字段时,才使用数据库计算列或默认值。
新记录在 Viewer、已部署问卷和已保存表单的设计器预览中加载时,会请求一次短生命周期的服务器显示快照,因此能显示真实的服务器时间、请求 IP、当前身份等。该快照绝不作为保存依据:真正提交时,服务器会再次计算每一个 SystemValue 并覆盖浏览器 payload。已保存记录显示数据库中的历史值。新建或尚未保存的 SystemValue 控件则保留“将在保存时由服务器记录”的非权威占位提示,因为服务器没有可计算的已保存表单定义。
浏览或编辑已有记录时,若 SystemValue 配置为“仅更新记录时”或“建立和更新时”,主区域继续显示数据库已保存的值,并额外以蓝色文字显示“下次保存时将更新为”的服务器实时预览值。这个第二值不会写入浏览器提交模型;提交前服务器仍会重算。这样可同时看到旧值和即将写入的值,而不会把未保存状态伪装成已保存。
数据来源
| 来源 | 写入值 | 说明 |
|---|---|---|
| 服务器时间 | ISO UTC、日期、Unix 秒数或自定义 .NET 格式 | `updated_at` 建议用“建立和更新时”;`submitted_at` 建议用“仅建立新记录时”。自定义格式保存为文本。 |
| 客户端 IP 地址 | 服务器解析后的远程 IP | IIS/Nginx 后必须正确配置 forwarded headers;IPv4、IPv6 都会保留。 |
| 当前系统用户 ID / 用户名 | 当前已登录的系统身份 | 匿名和仅受访者请求为空。 |
| 当前受访者 ID | 问卷受访者 id | 部署问卷中可用;普通系统表单为空。 |
| 浏览器 User-Agent | 请求头中的 user-agent | 仅辅助信息,客户端可伪造。 |
| 请求路径 | 当前公开请求路径 | 为避免记录敏感查询参数,故意不包含 query string。 |
| 请求追踪 ID | ASP.NET Core TraceIdentifier | 可用于把记录关联到服务器日志。 |
| 浏览器时区 | 浏览器提供的 IANA 时区 | 仅辅助信息;浏览器通过 `X-FormPlatform-Time-Zone` 请求头发送。 |
| 固定值 | 控件中配置的文本 | 仍会由服务器覆盖,不能通过提交 JSON 替换。 |
写入时机
| 选项 | 行为 |
|---|---|
| 仅建立新记录时 | 只在 insert 写入;以后修改记录时保留原来的值。 |
| 仅更新记录时 | 只在更新已有记录时写入。 |
| 建立和更新时 | 每次保存都重新写入。 |
推荐审计字段
| Property Name | 来源 | 写入时机 | 推荐数据类型 |
|---|---|---|---|
| `submitted_at` | 服务器时间 / ISO | 仅建立新记录时 | `DateTimeOffset` / timestamp with time zone |
| `updated_at` | 服务器时间 / ISO | 建立和更新时 | `DateTimeOffset` / timestamp with time zone |
| `submitted_by` | 当前系统用户 ID | 仅建立新记录时 | Guid / 数据库原生 UUID |
| `updated_by` | 当前系统用户 ID | 建立和更新时 | Guid / 数据库原生 UUID |
| `respondent_id` | 当前受访者 ID | 仅建立新记录时 | Guid / 数据库原生 UUID |
| `client_ip` | 客户端 IP 地址 | 建立和更新时 | string / varchar(45) |
| `request_id` | 请求追踪 ID | 建立和更新时 | string |
问卷部署的数据表生成中,内置服务器时间格式会生成 Date、Int64 或 DateTimeOffset 字段;自定义时间格式和其它来源生成文本字段。
选择“自定义格式”后,可使用 .NET `DateTimeOffset` 自定义格式,例如 `yyyy-MM-dd HH:mm:ss`。格式化的基础始终是 UTC 服务器时间。
信任模型
服务器先复制浏览器提交的 JSON,再在 Trigger、Server Action、通用提交、Data Model 写入和问卷写入之前,覆盖全部 SystemValue 属性。因此即使用户修改开发者工具、伪造 POST JSON,也无法替换服务器控制的字段。
`browserTimeZone` 和 `userAgent` 是客户端描述,不是安全凭据。`clientIp` 是否可靠取决于反向代理配置:只应信任 IIS/Nginx 等已知代理转发的头,不能信任任意客户端伪造的 forwarded header。
表单定义示例
{
"id": "comp_submitted_at",
"type": "systemValue",
"props": {
"label": "提交时间",
"labelPosition": "top",
"source": "serverTime",
"captureMode": "insert",
"timeFormat": "iso",
"fluid": true
},
"other": { "propertyName": "submitted_at" }
}
不要把 SystemValue 设置为必填。该值由服务器在持久化边界赋值。
---
控件语义化样式标准
目的
表单定义应能配置控件每个有意义的可见区域,而不依赖脆弱的 DOM 选择器。这是以后新增 FormPlatform 控件必须遵守的标准。
规则
1. `style.customClass` 作用于控件最外层包装元素。
2. 一个控件有多个可见区域时,使用一个命名样式对象,例如 `fileUploadStyles`、`chartStyles` 或 `selectionStyles`。
3. 对象成员必须以 `Class` 结尾,并使用语义名称,例如 `labelClass`、`frameClass`、`toolbarClass`、`optionClass`、`emptyClass`、`errorClass`;不要暴露具体 DOM 层级。
4. Vue 模板必须把该类直接添加到所描述的元素上,并且在稳定的语义类之后添加用户类。
5. 控件默认 CSS 尽量使用 `:where(...)`,使作者写入的 Tailwind utility 不需 `!important` 即可覆盖。
6. Canvas 所绘制的内容应通过显式渲染属性配置(颜色、字体、网格、图例等),因为 CSS 无法直接修改 Canvas 像素。
现有控件
- 输入类:`inputStyles`、`textareaStyles`、`richTextStyles`、`systemValueStyles`。
- 上传/设备类:`fileUploadStyles`、GeoLocation 的直接类字段、`signatureStyles`、`cameraStyles`。
- 选择类:Dropdown、AsyncSelect、Tree、Radio、Checkbox、Calendar 均使用 `selectionStyles`。
- 展示类:`headerStyles`、`imageStyles`、`qrCodeStyles`,以及外层 `customClass`。
- 图表类:`chartStyles` 与图表绘制属性。
- DataGrid、ItemRenderer、Menu、Message、Statistic、Breadcrumb、Tabs、Pagination 使用 `semanticStyles`,并保留已有的列、窗格、项目、操作、布局样式设置。
- 表格、可编辑表格和 GridLayout 使用 `semanticStyles` 配置外框、表格/窗格、标题、正文、行/单元格、空状态及适用时的缩放手柄;已有的细粒度单元格、窗格、布局设置仍保留。
- 容器和简单内容控件使用外层 `customClass`;子控件仍各自拥有样式。
新控件检查表
- 对多个可见区域新增 `...Styles` prop,默认空对象。
- 在 Style tab、`ComponentPanel.vue`、`store/formDesigner.js` 和组件契约中声明所有 `...Class` 字段。
- 在 `i18n.js` 补齐中英文标签。
- 直接把类应用到实际元素,不能要求作者了解内部 DOM。
- 默认样式使用低优先级 `:where`。
- 字段以 `Class` 结尾,保证表单 CSS 收集器可以发现 Tailwind 类。
- 有交互状态时添加组件契约或单元测试。