feat(diy-card): 默认操作删除+恢复(2.19.2) — normalizeActDeleted + acts 双重过滤 + 透传

This commit is contained in:
2026-10-24 09:42:32 +08:00
parent 6977b914d3
commit 1b6114ef58
5 changed files with 42 additions and 12 deletions

View File

@@ -396,6 +396,20 @@ export function normalizeActVis(vis) {
return out; return out;
} }
/**
* 默认动作删除(2.19.2): actDeleted = { type: 0|1 }, 1=已删除(C端不渲染, 面板行隐藏,
* 经「恢复」chips 找回)。与 actVis(眼睛临时隐藏)独立 —— 删除与隐藏 C 端均不渲染。
* 缺键/非法 = 未删除(0), 历史配置无此字段 → 零变化
*/
export function normalizeActDeleted(deleted) {
const src = deleted && typeof deleted === 'object' ? deleted : {};
const out = {};
ACT_TYPES.forEach(t => {
out[t] = String(src[t]) === '1' ? 1 : 0;
});
return out;
}
/** /**
* 默认动作文案(2.19): actLabels = { type: string }, 空/缺 = 内置默认 * 默认动作文案(2.19): actLabels = { type: string }, 空/缺 = 内置默认
* 历史配置无此字段 → 全默认, 渲染零变化 * 历史配置无此字段 → 全默认, 渲染零变化

View File

@@ -192,7 +192,7 @@ import {
WAP_LINE_ICONS, svgLineIconBg, WAP_LINE_ICONS, svgLineIconBg,
ACT_GRID_PRESETS, ACT_GRID_PRESET_FALLBACK, hexToRgba, ACT_GRID_PRESETS, ACT_GRID_PRESET_FALLBACK, hexToRgba,
// 2.19: 显隐/文案/自定义动作归一化 + 素材图标库(与后端 CardIconLibrary 同源) // 2.19: 显隐/文案/自定义动作归一化 + 素材图标库(与后端 CardIconLibrary 同源)
normalizeActVis, normalizeActLabels, normalizeCustomActs, normalizeActOrder, libIconBg, normalizeActVis, normalizeActDeleted, normalizeActLabels, normalizeCustomActs, normalizeActOrder, libIconBg,
} from './diy-card-common.js'; } from './diy-card-common.js';
export default { export default {
@@ -388,6 +388,7 @@ export default {
acts() { acts() {
const raw = this.card && Array.isArray(this.card.acts) ? this.card.acts : []; const raw = this.card && Array.isArray(this.card.acts) ? this.card.acts : [];
const vis = normalizeActVis(this.c.actVis); const vis = normalizeActVis(this.c.actVis);
const del = normalizeActDeleted(this.c.actDeleted);
const labels = normalizeActLabels(this.c.actLabels); const labels = normalizeActLabels(this.c.actLabels);
const icons = this.c.actIcons && typeof this.c.actIcons === 'object' ? this.c.actIcons : {}; const icons = this.c.actIcons && typeof this.c.actIcons === 'object' ? this.c.actIcons : {};
const order = normalizeActOrder(this.c.actOrder); const order = normalizeActOrder(this.c.actOrder);
@@ -409,12 +410,12 @@ export default {
}); });
const out = []; const out = [];
order.forEach(t => { order.forEach(t => {
if (byType[t] && vis[t] !== 0) out.push(merge(byType[t])); if (byType[t] && vis[t] !== 0 && del[t] !== 1) out.push(merge(byType[t]));
}); });
raw.forEach(a => { raw.forEach(a => {
if (!a || !a.type) return; if (!a || !a.type) return;
const t = String(a.type); const t = String(a.type);
if (order.indexOf(t) >= 0 || vis[t] === 0) return; if (order.indexOf(t) >= 0 || vis[t] === 0 || del[t] === 1) return;
out.push(merge(a)); out.push(merge(a));
}); });
normalizeCustomActs(this.c.customActs).forEach(ca => { normalizeCustomActs(this.c.customActs).forEach(ca => {

View File

@@ -123,6 +123,7 @@ export default {
qrImage: v.qrImage, qrImage: v.qrImage,
// 2.19: 默认动作显隐/文案 + 自定义动作(基件 acts computed 合成) // 2.19: 默认动作显隐/文案 + 自定义动作(基件 acts computed 合成)
actVis: v.actVis, actVis: v.actVis,
actDeleted: v.actDeleted,
actLabels: v.actLabels, actLabels: v.actLabels,
actOrder: v.actOrder, actOrder: v.actOrder,
customActs: v.customActs, customActs: v.customActs,

View File

@@ -86,6 +86,7 @@ export default {
qrImage: v.qrImage, qrImage: v.qrImage,
// 2.19: 默认动作显隐/文案 + 自定义动作(基件 acts computed 合成) // 2.19: 默认动作显隐/文案 + 自定义动作(基件 acts computed 合成)
actVis: v.actVis, actVis: v.actVis,
actDeleted: v.actDeleted,
actLabels: v.actLabels, actLabels: v.actLabels,
actOrder: v.actOrder, actOrder: v.actOrder,
customActs: v.customActs, customActs: v.customActs,

View File

@@ -363,7 +363,7 @@ test('反例自测: 新形态标记不得出现在未改造组件中', () => {
* 2.19 快捷操作: 显示控制 + 自定义操作 + 素材图标库 + 图层式管理 * 2.19 快捷操作: 显示控制 + 自定义操作 + 素材图标库 + 图层式管理
* ------------------------------------------------------------------ */ * ------------------------------------------------------------------ */
const V19_FIELDS = { actVis: 'object', actLabels: 'object', customActs: 'array', actOrder: 'array' }; const V19_FIELDS = { actVis: 'object', actDeleted: 'object', actLabels: 'object', customActs: 'array', actOrder: 'array' };
const V19_ACT_LABELS = { tel: '打电话', wx: '微信号', mail: '发邮件', web: '网站', save: '存通讯录' }; const V19_ACT_LABELS = { tel: '打电话', wx: '微信号', mail: '发邮件', web: '网站', save: '存通讯录' };
test('diy_view.php: Show/Self value 均含 2.19 字段(默认值=全显/默认文案/空数组/历史顺序=零变化)', () => { test('diy_view.php: Show/Self value 均含 2.19 字段(默认值=全显/默认文案/空数组/历史顺序=零变化)', () => {
@@ -379,6 +379,8 @@ test('diy_view.php: Show/Self value 均含 2.19 字段(默认值=全显/默认
assert.deepEqual(v.customActs, [], comp + '.customActs 默认空数组'); assert.deepEqual(v.customActs, [], comp + '.customActs 默认空数组');
// actOrder(2.19.1): 历史渲染顺序 // actOrder(2.19.1): 历史渲染顺序
assert.deepEqual(v.actOrder, ['tel', 'wx', 'mail', 'web', 'save'], comp + '.actOrder 默认=历史顺序'); assert.deepEqual(v.actOrder, ['tel', 'wx', 'mail', 'web', 'save'], comp + '.actOrder 默认=历史顺序');
// actDeleted(2.19.2): 全 0=无删除(历史零变化)
assert.deepEqual(v.actDeleted, { tel: 0, wx: 0, mail: 0, web: 0, save: 0 }, comp + '.actDeleted 默认全 0');
} }
}); });
@@ -432,13 +434,16 @@ test('common.js: normalizeActVis / normalizeActLabels / normalizeCustomActs / no
js.match(/export const ACT_ORDER_DEFAULT = [^;]+;/)[0].replace('export const', 'const'), js.match(/export const ACT_ORDER_DEFAULT = [^;]+;/)[0].replace('export const', 'const'),
].join('\n'); ].join('\n');
const grab = (name) => js.match(new RegExp('export function ' + name + '[\\s\\S]*?\\n}'))[0].replace('export function ', 'function '); const grab = (name) => js.match(new RegExp('export function ' + name + '[\\s\\S]*?\\n}'))[0].replace('export function ', 'function ');
const body = consts + '\n' + ['normalizeActVis', 'normalizeActLabels', 'normalizeCustomActs', 'normalizeActOrder'].map(grab).join('\n') + '\n' const body = consts + '\n' + ['normalizeActVis', 'normalizeActDeleted', 'normalizeActLabels', 'normalizeCustomActs', 'normalizeActOrder'].map(grab).join('\n') + '\n'
+ 'return [normalizeActVis, normalizeActLabels, normalizeCustomActs, normalizeActOrder];'; + 'return [normalizeActVis, normalizeActDeleted, normalizeActLabels, normalizeCustomActs, normalizeActOrder];';
const [visFn, labelFn, customFn, orderFn] = new Function(body)(); const [visFn, delFn, labelFn, customFn, orderFn] = new Function(body)();
// actVis: 缺省全显(历史零变化); '0' → 0; 未知键忽略 // actVis: 缺省全显(历史零变化); '0' → 0; 未知键忽略
assert.deepEqual(visFn(undefined), { tel: 1, wx: 1, mail: 1, web: 1, save: 1 }, 'actVis 缺省全显'); assert.deepEqual(visFn(undefined), { tel: 1, wx: 1, mail: 1, web: 1, save: 1 }, 'actVis 缺省全显');
assert.deepEqual(visFn({ tel: '0', wx: 0 }), { tel: 0, wx: 0, mail: 1, web: 1, save: 1 }, 'actVis 0=隐藏/缺键=显'); assert.deepEqual(visFn({ tel: '0', wx: 0 }), { tel: 0, wx: 0, mail: 1, web: 1, save: 1 }, 'actVis 0=隐藏/缺键=显');
// actDeleted(2.19.2): 缺省全 0(历史零变化); '1' → 1
assert.deepEqual(delFn(undefined), { tel: 0, wx: 0, mail: 0, web: 0, save: 0 }, 'actDeleted 缺省全 0');
assert.deepEqual(delFn({ tel: '1', save: 1 }), { tel: 1, wx: 0, mail: 0, web: 0, save: 1 }, 'actDeleted 1=已删/缺键=0');
// actLabels: 缺省=内置文案; 空串回落默认 // actLabels: 缺省=内置文案; 空串回落默认
assert.deepEqual(labelFn(undefined), V19_ACT_LABELS, 'actLabels 缺省=内置默认'); assert.deepEqual(labelFn(undefined), V19_ACT_LABELS, 'actLabels 缺省=内置默认');
assert.deepEqual(labelFn({ tel: '', wx: '加微信' }).wx, '加微信', 'actLabels 改名生效'); assert.deepEqual(labelFn({ tel: '', wx: '加微信' }).wx, '加微信', 'actLabels 改名生效');
@@ -492,8 +497,11 @@ test('mini.vue: acts 合成(显隐过滤 + 次序 + 文案覆盖 + 自定义动
assert.match(src, /acts\(\)\s*\{[\s\S]*?normalizeCustomActs\(this\.c\.customActs\)/, 'acts 消费 normalizeCustomActs'); assert.match(src, /acts\(\)\s*\{[\s\S]*?normalizeCustomActs\(this\.c\.customActs\)/, 'acts 消费 normalizeCustomActs');
// 2.19.1 次序: 按 normalizeActOrder 输出默认动作 + order 外扩展类型尾接兜底 // 2.19.1 次序: 按 normalizeActOrder 输出默认动作 + order 外扩展类型尾接兜底
assert.match(src, /acts\(\)\s*\{[\s\S]*?normalizeActOrder\(this\.c\.actOrder\)/, 'acts 消费 normalizeActOrder'); assert.match(src, /acts\(\)\s*\{[\s\S]*?normalizeActOrder\(this\.c\.actOrder\)/, 'acts 消费 normalizeActOrder');
assert.match(src, /acts\(\)\s*\{[\s\S]*?const byType = \{\};[\s\S]*?order\.forEach\(t => \{[\s\S]*?if \(byType\[t\] && vis\[t\] !== 0\) out\.push\(merge\(byType\[t\]\)\);/, '按 order 次序取默认动作(vis=0 过滤)'); assert.match(src, /acts\(\)\s*\{[\s\S]*?const byType = \{\};[\s\S]*?order\.forEach\(t => \{/, 'acts 按 order 次序取默认动作');
assert.match(src, /acts\(\)\s*\{[\s\S]*?if \(order\.indexOf\(t\) >= 0 \|\| vis\[t\] === 0\) return;/, 'order 外扩展类型尾接且隐藏不渲染'); assert.match(src, /acts\(\)\s*\{[\s\S]*?if \(order\.indexOf\(t\) >= 0 \|\| vis\[t\] === 0 \|\| del\[t\] === 1\) return;/, 'order 外扩展类型尾接且隐藏/已删不渲染');
// 2.19.2 删除: 按 order 取默认动作时 deleted=1 的不渲染
assert.match(src, /acts\(\)\s*\{[\s\S]*?normalizeActDeleted\(this\.c\.actDeleted\)/, 'acts 消费 normalizeActDeleted');
assert.match(src, /acts\(\)\s*\{[\s\S]*?if \(byType\[t\] && vis\[t\] !== 0 && del\[t\] !== 1\) out\.push\(merge\(byType\[t\]\)\);/, 'order 取默认动作时 vis=0/deleted=1 均过滤');
assert.match(src, /labels\[t\] \|\| a\.label/, 'actLabels 覆盖文案(空回落服务端)'); assert.match(src, /labels\[t\] \|\| a\.label/, 'actLabels 覆盖文案(空回落服务端)');
assert.match(src, /cfgImg: String\(cfg\.img \|\| ''\)\.trim\(\)/, '默认动作合并 cfgImg'); assert.match(src, /cfgImg: String\(cfg\.img \|\| ''\)\.trim\(\)/, '默认动作合并 cfgImg');
assert.match(src, /cfgKey: String\(cfg\.iconKey \|\| ''\)\.trim\(\)/, '默认动作合并 cfgKey(iconKey)'); assert.match(src, /cfgKey: String\(cfg\.iconKey \|\| ''\)\.trim\(\)/, '默认动作合并 cfgKey(iconKey)');
@@ -503,11 +511,11 @@ test('mini.vue: acts 合成(显隐过滤 + 次序 + 文案覆盖 + 自定义动
assert.match(src, /actLibStyle\(act\)\s*\{[\s\S]*?libIconBg\(this\.actLibOf\(act\), this\.actIconColor\(act\)\)/, 'actLibStyle 消费 libIconBg + 四级色口径'); assert.match(src, /actLibStyle\(act\)\s*\{[\s\S]*?libIconBg\(this\.actLibOf\(act\), this\.actIconColor\(act\)\)/, 'actLibStyle 消费 libIconBg + 四级色口径');
assert.equal((src.match(/v-else-if="actLibOf\(act\)" class="act-grid-svg" :style="actLibStyle\(act\)"/g) || []).length, 2, '上/下两份宫格副本均插入素材库分支'); assert.equal((src.match(/v-else-if="actLibOf\(act\)" class="act-grid-svg" :style="actLibStyle\(act\)"/g) || []).length, 2, '上/下两份宫格副本均插入素材库分支');
assert.match(src, /actSvgOf\(act\)\s*\{[\s\S]*?!this\.actImgOf\(act\) && !this\.actLibOf\(act\) && !!WAP_LINE_ICONS/, '线稿档被 img/素材库优先占位后不再进入'); assert.match(src, /actSvgOf\(act\)\s*\{[\s\S]*?!this\.actImgOf\(act\) && !this\.actLibOf\(act\) && !!WAP_LINE_ICONS/, '线稿档被 img/素材库优先占位后不再进入');
assert.match(src, /import \{[\s\S]*?normalizeActVis, normalizeActLabels, normalizeCustomActs, normalizeActOrder, libIconBg/, 'mini.vue 引入 2.19+2.19.1 依赖'); assert.match(src, /import \{[\s\S]*?normalizeActVis, normalizeActDeleted, normalizeActLabels, normalizeCustomActs, normalizeActOrder, libIconBg/, 'mini.vue 引入 2.19+2.19.1+2.19.2 依赖');
// 透传 // 透传
for (const file of ['diy-card-show.vue', 'diy-card-self.vue']) { for (const file of ['diy-card-show.vue', 'diy-card-self.vue']) {
const vue = readFileSync(resolve(FE, file), 'utf8'); const vue = readFileSync(resolve(FE, file), 'utf8');
for (const f of ['actVis', 'actLabels', 'actOrder', 'customActs']) { for (const f of ['actVis', 'actDeleted', 'actLabels', 'actOrder', 'customActs']) {
assert.match(vue, new RegExp(f + ': v\\.' + f + ','), file + ' 透传 ' + f); assert.match(vue, new RegExp(f + ': v\\.' + f + ','), file + ' 透传 ' + f);
} }
} }
@@ -540,13 +548,18 @@ test('design.html ×2: 图层式管理面板 + 素材图标库 + 预览显隐/
assert.match(html, /nc\.customActs\.splice\(ci, 1\)/, comp + ' 删除自定义动作'); assert.match(html, /nc\.customActs\.splice\(ci, 1\)/, comp + ' 删除自定义动作');
// 2.19.1 默认动作次序: 面板行按 actOrder 渲染 + ↑↓ 委托调整 // 2.19.1 默认动作次序: 面板行按 actOrder 渲染 + ↑↓ 委托调整
assert.equal((html.match(/<template v-for="k in \(nc\.actOrder && nc\.actOrder\.length \? nc\.actOrder : \['tel','wx','mail','web','save'\]\)"><div class="diy-card-layer"/g) || []).length, 1, comp + ' 面板默认行按 actOrder 次序渲染(嵌套 v-for + v-if 匹配)'); assert.equal((html.match(/<template v-for="k in \(nc\.actOrder && nc\.actOrder\.length \? nc\.actOrder : \['tel','wx','mail','web','save'\]\)"><div class="diy-card-layer"/g) || []).length, 1, comp + ' 面板默认行按 actOrder 次序渲染(嵌套 v-for + v-if 匹配)');
// 2.19.2 删除: 行过滤 + 删除按钮 + 恢复 chips + 预览过滤
assert.match(html, /v-if="a\.k === k && !\(nc\.actDeleted && nc\.actDeleted\[a\.k\] === 1\)"/, comp + ' 面板行过滤已删除动作');
assert.match(html, /diy-card-layer__delact" title="删除此默认操作\(可在下方恢复\)" @click="\$set\(nc, 'actDeleted', nc\.actDeleted \|\| \{ tel: 0, wx: 0, mail: 0, web: 0, save: 0 \}\); \$set\(nc\.actDeleted, a\.k, 1\);"/, comp + ' 删除按钮防御式写回 actDeleted');
assert.match(html, /diy-card-layer__restore" v-if="nc\.actDeleted && \(nc\.actDeleted\.tel === 1/, comp + ' 恢复 chips 行(有删除项才显示)');
assert.match(html, /v-if="nc\.actDeleted\[t\.k\] === 1" :key="t\.k" @click="\$set\(nc\.actDeleted, t\.k, 0\);"/, comp + ' 恢复 chip 点击写回 0');
assert.equal((html.match(/v-if="a\.k === k && !\(nc\.actVis && nc\.actVis\[a\.k\] === 0\) && !\(nc\.actDeleted && nc\.actDeleted\[a\.k\] === 1\)"/g) || []).length, 2, comp + ' 预览显隐+删除双重过滤(上/下两副本)');
assert.equal((html.match(/js-act-move" title="上移" :data-key="a\.k" data-dir="up"/g) || []).length, 1, comp + ' 默认行上移按钮(委托)'); assert.equal((html.match(/js-act-move" title="上移" :data-key="a\.k" data-dir="up"/g) || []).length, 1, comp + ' 默认行上移按钮(委托)');
assert.equal((html.match(/js-act-move" title="下移" :data-key="a\.k" data-dir="down"/g) || []).length, 1, comp + ' 默认行下移按钮(委托)'); assert.equal((html.match(/js-act-move" title="下移" :data-key="a\.k" data-dir="down"/g) || []).length, 1, comp + ' 默认行下移按钮(委托)');
assert.match(html, /nc\.customActs\.push\(\{ label: '自定义', type: 'copy'[\s\S]*?svg: ''[\s\S]*?vis: 1 \}\)/, comp + ' 添加自定义操作(全键补齐含 svg)'); assert.match(html, /nc\.customActs\.push\(\{ label: '自定义', type: 'copy'[\s\S]*?svg: ''[\s\S]*?vis: 1 \}\)/, comp + ' 添加自定义操作(全键补齐含 svg)');
assert.match(html, /c\.vis = c\.vis === 0 \? 1 : 0;/, comp + ' 自定义动作眼睛开关'); assert.match(html, /c\.vis = c\.vis === 0 \? 1 : 0;/, comp + ' 自定义动作眼睛开关');
// 预览消费: 次序 + 显隐过滤 + 文案覆盖 + 自定义动作单元格 // 预览消费: 次序 + 显隐过滤 + 文案覆盖 + 自定义动作单元格
assert.equal((html.match(/<template v-for="k in \(nc\.actOrder && nc\.actOrder\.length \? nc\.actOrder : \['tel','wx','mail','web','save'\]\)"><template v-for="a in \[\{k:'tel',ch:'打',lb:'打电话'/g) || []).length, 2, comp + ' 预览默认 cell 按 actOrder 次序渲染(上/下两副本)'); assert.equal((html.match(/<template v-for="k in \(nc\.actOrder && nc\.actOrder\.length \? nc\.actOrder : \['tel','wx','mail','web','save'\]\)"><template v-for="a in \[\{k:'tel',ch:'打',lb:'打电话'/g) || []).length, 2, comp + ' 预览默认 cell 按 actOrder 次序渲染(上/下两副本)');
assert.equal((html.match(/v-if="a\.k === k && !\(nc\.actVis && nc\.actVis\[a\.k\] === 0\)"/g) || []).length, 2, comp + ' 预览显隐过滤(嵌套 key 匹配)');
assert.match(html, /\{\{ \(nc\.actLabels && nc\.actLabels\[a\.k\]\) \|\| a\.lb \}\}/, comp + ' 预览文案覆盖'); assert.match(html, /\{\{ \(nc\.actLabels && nc\.actLabels\[a\.k\]\) \|\| a\.lb \}\}/, comp + ' 预览文案覆盖');
// svg 快照口径: 预览只读纯数据字段(委托 JS 选图/改色时预计算 data URI 写入), // svg 快照口径: 预览只读纯数据字段(委托 JS 选图/改色时预计算 data URI 写入),
// 模板内严禁调用全局函数(模板作用域函数解析不可靠, 实测 typeof 恒 false) // 模板内严禁调用全局函数(模板作用域函数解析不可靠, 实测 typeof 恒 false)