P76(T8 验收遗留):快应用上下文的导出按钮此前恒按中心会话树判"导出"锚点 (Data.vue exportable 读 store.modules),锚点判定不生效(fail-closed 方向, 有授权也不画按钮)。本轮把 AppContext 拉到的**目标应用树**经 modules prop 下传, Data.vue 的权限判据改为 permsTree(app=应用树 / center=会话树),canExport 纯函数不动。 Stats.vue 未接:本页无锚点判定也无 store.modules 引用,加 prop 是死代码(差异记 T9 报告)。 P77(T8 验收遗留):应用上下文内的"详情"不再跳回中心路由 /admin/data/detail。 形态二选一取**子路由 /app/:appCode/data/detail**(同一个 AnswerDetail.vue): 地址保留 uuid 可深链/刷新、详情页只有一份实现、back() 本就按 app 回应用数据视图, 写 parentPath 反而造假(menuRoute 的 ROUTE_PATHS 只收静态 path,应用段导航在页内轻导航)。 goDetail 按 props.context 分派;back() 改读 route.params.appCode,撤掉 query.app 双轨。 观察项①(T8 验收记档):确认跨触底页勾选**会被清空**——view-ui-plus 的 data 监听器 用 makeObjBaseData 重建 objData,_isChecked 只认行上的 _checked(点选不回写行), 整组替换后勾选视觉与 getSelection 归零而"已选 N 条"仍累计。新增 withSelectionState() 在 reload/loadMore 两条装载路径按 selected 回填 _checked,selected 为唯一事实源。 演练桩(本地件,dev:mock 生效):baseapi 增 fc-drill 快应用模块树(方案 §6.2 五行含 导入/导出两个 path 空锚点),dataapi 增 /api/form/resolve 桩(快应用数据视图唯一的 合规取数路径是 code→resolve→uuid,契约 §1)。其余外指 appCode 仍旧返回空树(P3 口径)。 门禁:npm run build ✓ / npm run type-check 0 错 ✓; t1~t9 全量复跑:t5 13/0/1、t6 22/0/2、t7 12/0/0、t8 21/0/1、t9 12/0/1 (skip 均为 headless Edge --dump-dom 吐 0 字节的 P73 环境事实,不假通过)。 本提交含 T6~T8 三轮堆在工作区的既有改动(沿 T8 验收 §5 推送分层)。
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 答卷值的只读呈现(数据详情页 / 我的填写详情 / 编辑弹层的"原值"对照共用)。
|
||||
*
|
||||
* 三条纪律:
|
||||
* 1. **三态空值与已删字段降级**(契约 §3、V8):值在 schema 且为空 → "—";
|
||||
* values 无此 key(提交时字段还不存在)→ "—";values 有 key 但传入的 schema 已删该字段 →
|
||||
* "(已移除字段)原始key: 原始值",并在表尾单列一节,不静默丢弃;
|
||||
* 2. **默认转义**(方案 §2.3 红线):全部走文本插值与 CSS(textarea 用 pre-wrap 保换行),
|
||||
* <b>本组件永不含 v-html</b>,冒烟脚本按全仓 grep 断言零命中;
|
||||
* 3. **只读态才脱敏**(契约 §3 phone):手机号在只读呈现打码,编辑态由弹层自己传原值。
|
||||
*
|
||||
* 附件链接的 URL 前缀与 T7 的图片控件同口径(值里只有相对 key,绝对地址渲染期拼——契约 §3、V24);
|
||||
* "经 FILE_WEB_PATH 真能取到文件"属部署环境断言,按 T3/T9 的分工留到 160 复验。
|
||||
*/
|
||||
import { computed } from "vue";
|
||||
import { EMPTY_TEXT, cellText, flattenFields, isFileArray } from "@/utils/dataValue";
|
||||
import type { DesignField, FileValue } from "@/types/formSchema";
|
||||
import conf from "@/assets/config/host.config";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
/** 钉住版本(或当前发布版本)内联展开后的 groups——后端读接口下发的形状 */
|
||||
groups?: any;
|
||||
/** 答卷值文档 */
|
||||
values?: Record<string, unknown> | null;
|
||||
/** 手机号是否脱敏(只读呈现默认脱敏) */
|
||||
maskPhone?: boolean;
|
||||
}>(),
|
||||
{ groups: () => [], values: null, maskPhone: true }
|
||||
);
|
||||
|
||||
interface FileLink {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface ValueRow {
|
||||
key: string;
|
||||
label: string;
|
||||
text: string;
|
||||
multiline: boolean;
|
||||
files: FileLink[];
|
||||
}
|
||||
|
||||
interface GroupView {
|
||||
key: string;
|
||||
label: string;
|
||||
rows: ValueRow[];
|
||||
}
|
||||
|
||||
const schemaFields = computed<DesignField[]>(() => flattenFields(props.groups));
|
||||
|
||||
/** 附件地址由渲染期拼(值里只存相对 key,绝对 URL 不进答卷——契约 §3、方案 §4.3②) */
|
||||
function fileLinks(value: unknown): FileLink[] {
|
||||
if (!isFileArray(value)) return [];
|
||||
const prefix = `${conf.serviceHost || ""}/static/`;
|
||||
return (value as FileValue[]).map((f) => ({
|
||||
name: String(f.fileName ?? f.key ?? ""),
|
||||
url: prefix + String(f.key ?? "")
|
||||
}));
|
||||
}
|
||||
|
||||
const views = computed<GroupView[]>(() => {
|
||||
const groups = Array.isArray(props.groups) ? (props.groups as any[]) : [];
|
||||
return groups.map((group) => {
|
||||
const fields = Array.isArray(group?.fields) ? (group.fields as DesignField[]) : [];
|
||||
return {
|
||||
key: String(group?.key ?? ""),
|
||||
label: String(group?.label ?? ""),
|
||||
rows: fields.map((field) => ({
|
||||
key: field.key,
|
||||
label: field.label || field.key,
|
||||
text: cellText(field, field.key, props.values, { maskPhone: props.maskPhone }),
|
||||
multiline: field.type === "textarea",
|
||||
files: fileLinks(props.values?.[field.key])
|
||||
}))
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
/** schema 已删、值文档里还留着的 key(V8 的降级半边) */
|
||||
const removedRows = computed<ValueRow[]>(() => {
|
||||
const known = new Set(schemaFields.value.map((f) => f.key));
|
||||
const values = props.values || {};
|
||||
return Object.keys(values)
|
||||
.filter((key) => !known.has(key))
|
||||
.map((key) => {
|
||||
const raw = values[key];
|
||||
const blank = raw === null || raw === undefined || (typeof raw === "string" && raw.trim() === "");
|
||||
return {
|
||||
key,
|
||||
label: key,
|
||||
text: blank ? EMPTY_TEXT : cellText(null, key, values),
|
||||
multiline: false,
|
||||
files: []
|
||||
};
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="answer-values">
|
||||
<section v-for="group in views" :key="group.key" class="value-group">
|
||||
<h5 v-if="group.label" class="group-title">{{ group.label }}</h5>
|
||||
<div v-for="row in group.rows" :key="row.key" class="value-row">
|
||||
<span class="value-label">{{ row.label }}</span>
|
||||
<span :class="['value-text', row.multiline ? 'pre-wrap' : '']">{{ row.text }}</span>
|
||||
<span v-if="row.files.length" class="value-files">
|
||||
<a v-for="file in row.files" :key="file.url" :href="file.url" target="_blank" rel="noopener">
|
||||
{{ file.name }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="removedRows.length" class="value-group removed-group">
|
||||
<h5 class="group-title">历史遗留字段(当前版本 schema 已删除)</h5>
|
||||
<div v-for="row in removedRows" :key="row.key" class="value-row">
|
||||
<span class="value-label">{{ row.label }}</span>
|
||||
<span class="value-text pre-wrap">{{ row.text }}</span>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.answer-values {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.value-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.group-title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #17233d;
|
||||
border-left: 3px solid #2d8cf0;
|
||||
padding-left: 8px;
|
||||
}
|
||||
.value-row {
|
||||
display: grid;
|
||||
grid-template-columns: 180px 1fr;
|
||||
gap: 12px;
|
||||
align-items: start;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px dashed #e8eaec;
|
||||
}
|
||||
.value-label {
|
||||
color: #808695;
|
||||
font-size: 13px;
|
||||
}
|
||||
.value-text {
|
||||
color: #17233d;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.value-text.pre-wrap {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.value-files {
|
||||
grid-column: 2;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
.value-files a {
|
||||
font-size: 12px;
|
||||
color: #2d8cf0;
|
||||
}
|
||||
.removed-group .group-title {
|
||||
border-left-color: #ff9900;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user