HarmonyOS 多设备粘贴表格的列映射与错误回显【共创稿事节】
HarmonyOS 多设备粘贴表格的列映射与错误回显【共创稿事节】
从桌面表格复制一批书目,再粘贴到手机或平板上的入库页面,最容易出错的地方往往是列的位置。有人把“数量”放在第二列,有人把“名称”放在第二列,还有人只复制了数据区。直接按数组下标取值,界面可能看起来正常,实际入库字段却已经错位。窗口变宽以后,更多列同时可见,也不会自动解决这个数据问题。
这里为图书收货表建立一个明确的粘贴协议:首行必须含有书号、名称、数量三个表头;顺序允许变化;数据使用制表符分列、换行分行。程序先完成列映射,再逐行核对字段,把错误定位到原始行号和业务列。所有行都通过以后,才形成可提交的数据集合。

把粘贴入口与数据协议分开
页面使用TextArea接收用户粘贴的文本,软键盘和物理键盘都进入同一条解析路径。华为的TextArea输入绑定说明给出了text与onChange的配合方式。输入回调中保存原文,不把制表符改成空格,也不按屏幕上的视觉换行拆行。输入框自动折行属于排版,只有原文里的换行字符才是数据边界。
示例使用API 12及以上的ArkUI基础组件,无需剪贴板读取权限。用户主动粘贴到输入框,与应用读取系统剪贴板是不同的交互路径。布局依据当前应用窗口宽度决定内容上限,而不是根据设备名称判断“手机模式”或“平板模式”。多设备适配入口可用于继续检查窗口、键盘和不同交互方式。
协议刻意保持紧凑:不接收带引号转义的CSV、不接收单元格内换行和制表符。若数据来源需要这些内容,应新增相应格式的解析器,并在入口明确格式。把所有分隔文本都交给一个split表达式,会让用户无法区分格式问题与业务问题。
先找到列,再解释单元格
前三段代码按顺序放入ets/model/PasteTable.ts,第四段是ets/pages/PasteTable.ets。列映射只读取表头,返回名称到位置的对应关系。表头前后空格允许清理,但数据单元格的原文仍然保留,便于回显。首字符的BOM单独处理,避免第一列表头带上不可见字符。
export class BookRow {
isbn: string = '';
name: string = '';
count: number = 0;
}
export class PasteResult {
rows: BookRow[] = [];
errors: string[] = [];
}
export function mapHeader(line: string): number[] {
const header: string[] = line.replace(/^\uFEFF/, '')
.split('\t').map((cell: string) => cell.trim());
const required: string[] = ['书号', '名称', '数量'];
if (header.length !== required.length ||
new Set<string>(header).size !== header.length) {
throw new Error('表头必须包含三个不同的业务列');
}
return required.map((name: string) => {
const position: number = header.indexOf(name);
if (position < 0) {
throw new Error('缺少表头:' + name);
}
return position;
});
}
只判断“包含数量”还不够,还要拒绝重复表头。两个数量列同时存在时,indexOf会取第一个,另一个被静默忽略。这里要求列数严格等于三,并且每个名称只出现一次。遇到额外备注列也会提示表头错误,避免用户以为备注已经被保存。后续若扩展可选列,应明确列的保留或丢弃策略。
书号采用13位数字字符,作为标识保存为字符串,前导零不会被数值转换丢掉。示例只核对长度和字符集合,若业务要求ISBN校验位、目录查询或书目去重,应在此基础上增加对应规则;不同层次的错误应分开呈现。
错误必须回到来源位置
数据区逐行检查列数、书号、名称和数量。数量限制为1至9999的十进制整数,拒绝小数、指数记法与带符号输入。行号从原始文本第一行开始计算,首行表头为第1行,因此首条数据的错误位置是第2行。尾部空行通常来自复制动作,可以移除;中间空行保留为错误,避免行号漂移。
export function parseTable(text: string): PasteResult {
const result = new PasteResult();
if (text.length > 100000) {
result.errors.push('文本超过长度上限');
return result;
}
const lines: string[] = text.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n').split('\n');
while (lines.length > 1 && lines[lines.length - 1] === '') {
lines.pop();
}
if (lines.length < 2 || lines.length > 501) {
result.errors.push('请提供表头及1至500行数据');
return result;
}
let columns: number[];
try {
columns = mapHeader(lines[0]);
} catch {
result.errors.push('表头错误:请核对书号、名称、数量');
return result;
}
for (let i: number = 1; i < lines.length; i++) {
const cells: string[] = lines[i].split('\t');
const prefix: string = '第' + (i + 1).toString() + '行:';
if (cells.length !== 3) {
result.errors.push(prefix + '列数应为3');
continue;
}
const isbn: string = cells[columns[0]].trim();
const name: string = cells[columns[1]].trim();
const count: string = cells[columns[2]].trim();
const before: number = result.errors.length;
if (!/^\d{13}$/.test(isbn)) {
result.errors.push(prefix + '书号应为13位数字');
}
if (name.length === 0 || name.length > 80) {
result.errors.push(prefix + '名称应为1至80个字符');
}
if (!/^[1-9]\d{0,3}$/.test(count)) {
result.errors.push(prefix + '数量应为1至9999的整数');
}
if (result.errors.length === before) {
const row = new BookRow();
row.isbn = isbn;
row.name = name;
row.count = Number(count);
result.rows.push(row);
}
}
if (result.errors.length > 0) {
result.rows = [];
}
return result;
}
这里采取整表通过的策略。错误行不会混入提交集合,其他有效行也暂时不提交,用户修正原文后重新检查。若改为允许部分入库,必须为每一行建立稳定身份和已处理状态,否则用户再次粘贴同一段内容,容易重复导入已经成功的行。错误提示本身也不应成为数据源,提交仍以解析得到的BookRow对象为准。
500行和十万字符是入口保护阈值,不是性能测量结论。解析复杂度随单元格数量增长;更大的批量数据可以走文件导入与分段检查。输入变化时只保存草稿,按钮触发解析,避免每按一次键就重新处理整张表。

用列顺序变化检查映射
下面的规则检查覆盖交换列顺序、空数据、重复表头、非法数量以及错误行号。测试数据直接包含在代码内,修改协议时可同时更新预期。页面常用的一组输入也在这里给出,名称放在第一列,数量放在第二列,用来确认业务字段没有依赖固定下标。
export const SAMPLE_TABLE: string =
'名称\t数量\t书号\n窗口设计\t2\t9780000000001';
export function checkPasteRules(): void {
const ok: PasteResult = parseTable(SAMPLE_TABLE);
if (ok.errors.length !== 0 || ok.rows.length !== 1 ||
ok.rows[0].count !== 2 || ok.rows[0].name !== '窗口设计') {
throw new Error('交换列顺序后字段不匹配');
}
const bad: PasteResult = parseTable(
'名称\t数量\t书号\n窗口设计\t2.5\t9780000000001');
if (bad.rows.length !== 0 ||
bad.errors[0] !== '第2行:数量应为1至9999的整数') {
throw new Error('数量错误定位不匹配');
}
const samples: string[] = ['', '名称\t名称\t书号\n甲\t乙\t1',
'书号\t名称\t数量\n9780000000001\t\t2',
'书号\t名称\t数量\n9780000000001\t甲\t0'];
samples.forEach((sample: string) => {
if (parseTable(sample).errors.length === 0) {
throw new Error('异常输入应返回错误');
}
});
}
调试时可以分别观察原文、表头位置数组和最终对象。原文不能用空格替换后的日志来代替,否则制表符缺失会被掩盖。遇到不可见字符问题,临时输出字符码或字符串长度比截图更有用;业务记录里则只保留错误类别与行列位置,减少书目原文的无关留存。
检查结果还应明确对应哪一次输入。用户修改草稿以后,原来的“可提交”提示必须失效,否则按钮下方仍显示旧结果,容易让人误以为新文本已经通过检查。本例在onChange中把摘要改成需要重新检查,提交入口也应重新解析当前草稿,或者比较内容版本,保证展示状态与实际提交数据一致。
表头映射的检查也可以独立于业务字段规则开展。将三个表头的六种排列分别输入,同一条书目都应得到相同对象;将表头改成“数量、数量、书号”,则应在读取数据之前被拒绝。这样可以区分映射错误与数量规则错误,减少修改一个分支后连带影响其他分支的机会。
界面回显不宜只用红色表达错误。行号、列名和具体条件已经包含在文字里,颜色只是辅助。窄窗口下应让错误说明自然换行,不能用省略号截掉数量范围。外接键盘粘贴后还要检查滚动位置,确保用户能够找到检查按钮,再返回原文修正对应单元格。
对于重复书号,本例保留输入行次序,不自动合并数量。合并会改变业务含义,例如两行可能属于不同收货批次。若入库流程要求唯一书号,应另加重复标识规则,并同时提示首次出现与再次出现的行号,让用户决定如何整理来源数据。
让窗口变化保持同一份草稿
页面保持一个TextArea组件,宽窗口只限制内容容器的最大宽度,输入草稿不会随窗口缩放重建。Scroll保留键盘弹出后的操作空间,错误摘要放在按钮下面。大量错误适合在独立列表逐项显示,本例用换行文本呈现,方便核对基础流程。
import { parseTable, PasteResult, SAMPLE_TABLE } from '../model/PasteTable';
@Entry
@Component
struct PasteTable {
@State draft: string = SAMPLE_TABLE.replace('\t2\t', '\t2.5\t');
@State summary: string = '待检查';
@State wide: boolean = false;
build() {
Scroll() {
Column({ space: 16 }) {
Text('书目粘贴检查').fontSize(24).fontWeight(FontWeight.Bold)
Text('表头映射:名称 → 数量 → 书号').fontSize(14)
TextArea({ text: this.draft })
.height(180)
.onChange((value: string) => {
this.draft = value;
this.summary = '内容已变更,请重新检查';
})
Button('检查表格').onClick(() => {
const result: PasteResult = parseTable(this.draft);
this.summary = result.errors.length > 0 ?
result.errors.join('\n') :
'可提交 ' + result.rows.length.toString() + ' 行';
}).width('100%')
Text(this.summary).fontSize(18)
}.padding(24).width(this.wide ? 720 : '100%')
}.width('100%').height('100%')
.onAreaChange((_oldArea: Area, area: Area) => {
this.wide = Number(area.width) >= 760;
})
}
}

上架前应检查不同来源的换行方式、末尾空行、列顺序以及错误修复后的再次提交。列映射把输入顺序转换成稳定的业务含义,行号把错误指回原始文本,整表检查保证提交集合清晰。多设备页面共享这套协议以后,窗口大小只影响阅读和操作空间,数据解释始终保持一致。
更多推荐

所有评论(0)