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,135 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* upload 附件(值 [{id,fileName,key}];单文件 ≤50MB、每字段 ≤9 个是契约 §3 的硬上限,
|
||||
* 字段级 accept/maxSize/maxFiles 只能往更严收,不能往宽放)。
|
||||
* accept 不含 .svg 由设计器把门(P28),这里只如实呈现配置。
|
||||
*/
|
||||
import { computed, ref } from "vue";
|
||||
import type { FieldWidgetProps } from "../types";
|
||||
import type { FileValue } from "@/types/formSchema";
|
||||
import { Upload } from "view-ui-plus";
|
||||
import { alovaInstance } from "@/utils/alova/alova-iview";
|
||||
|
||||
const props = withDefaults(defineProps<FieldWidgetProps & {
|
||||
modelValue?: FileValue[];
|
||||
readonly?: boolean;
|
||||
}>(), {
|
||||
modelValue: () => [],
|
||||
readonly: true
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:modelValue", value: FileValue[]): void;
|
||||
}>();
|
||||
|
||||
const fileList = computed(() => props.modelValue || []);
|
||||
const uploading = ref(false);
|
||||
|
||||
const hints = computed(() => {
|
||||
const f = props.field;
|
||||
return [
|
||||
f.accept ? `格式 ${f.accept}` : "",
|
||||
f.maxSize ? `单个 ≤${f.maxSize}MB` : "",
|
||||
f.maxFiles ? `最多 ${f.maxFiles} 个` : ""
|
||||
].filter(Boolean);
|
||||
});
|
||||
|
||||
const accept = computed(() => props.field.accept || "");
|
||||
|
||||
async function beforeUpload(file: File) {
|
||||
const maxSize = (props.field.maxSize || 50) * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
return false;
|
||||
}
|
||||
const maxFiles = props.field.maxFiles || 9;
|
||||
if (fileList.value.length >= maxFiles) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleUpload(file: File) {
|
||||
uploading.value = true;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
const result = await alovaInstance.Post<{ id: number; fileName: string; key: string }>(
|
||||
"/api/form/upload",
|
||||
formData,
|
||||
{ headers: { "Content-Type": "multipart/form-data" } }
|
||||
).send();
|
||||
const newValue = [...fileList.value, { id: result.id, fileName: result.fileName, key: result.key }];
|
||||
emit("update:modelValue", newValue);
|
||||
} catch (e) {
|
||||
console.error("Upload failed:", e);
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemove(index: number) {
|
||||
const newValue = fileList.value.filter((_, i) => i !== index);
|
||||
emit("update:modelValue", newValue);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="attach">
|
||||
<div class="file-list">
|
||||
<div v-for="(item, idx) in fileList" :key="item.id" class="file-item">
|
||||
<Icon type="ios-document" class="file-icon" />
|
||||
<span class="file-name">{{ item.fileName }}</span>
|
||||
<Icon v-if="!readonly" type="ios-close-circle" class="file-remove" @click="handleRemove(idx)" />
|
||||
</div>
|
||||
</div>
|
||||
<Upload v-if="!readonly" :before-upload="beforeUpload" :handle-upload="handleUpload" :accept="accept"
|
||||
:disabled="uploading">
|
||||
<Button icon="ios-cloud-upload-outline" :loading="uploading">上传附件</Button>
|
||||
</Upload>
|
||||
<div v-if="hints.length" class="attach-hint">{{ hints.join("|") }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.attach {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 8px;
|
||||
background: #f7f7f7;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.file-icon {
|
||||
font-size: 16px;
|
||||
color: #515a6e;
|
||||
}
|
||||
.file-name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
color: #515a6e;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.file-remove {
|
||||
font-size: 16px;
|
||||
color: #ed4014;
|
||||
cursor: pointer;
|
||||
}
|
||||
.attach-hint {
|
||||
color: #808695;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user