- 新 api/module/anon.ts:两把探针走原生 fetch(alova 的 302 拦截对全部 /api/* 生效且没有
per-request 豁免位,用它问"有没有会话"会当场整页跳 CAS,C15 的代码事实);带 yschapirequest
头让墙内未登录回 JSON 的 code=302 信封而不是 HTML 跳转。提交回落 alova(/anon 面无 302 语义)。
- Fill.vue 引导改双探针:probeSession → 有会话走登录通道并取"我的提交"段;无会话再 probeAnonForm
→ 命中即匿名渲染(匿名人无任何查询能力,编辑/删除段整条不出现);两跳都落空才 guideToLogin。
- F1 顺修(取"改判通道"那一支,不动全局 errorHandle):旧判据 e?.code===4 是死分支——alova 抛的是
不带 code 的 Error,embed 的 needLogin 从来没发出去过;现在未登录引导只由探针结论触发。
- F2 顺修:slot desc → #desc 两处(Vue 3 下旧写法静默不进 Alert 描述插槽,报名号那行从未渲染)。
- App.vue:/f/ 前缀路由先以原生探针判会话,无会话则裸渲染且一个鉴权请求都不发(P124——否则无会话者
还没走到填报页的探针就被带去 CAS,"匿名表单 page=直接可填"根本兑现不了);探到会话时结果直接复用,
登录面不多发一次请求。其余路由引导一字未动。
- upload/image 两个附件控件的上传端点改 inject("fcUploadUrl", 默认 /api/form/upload),由 Fill.vue
按通道 provide;设计器预览等调用点行为不变。vite.config.ts 代理加 /anon → 8086。
门禁:npx vue-tsc --noEmit exit 0;npm run build ✓ built in 11.61s。
142 lines
4.1 KiB
Vue
142 lines
4.1 KiB
Vue
<script setup lang="ts">
|
||
/**
|
||
* upload 附件(值 [{id,fileName,key}];单文件 ≤50MB、每字段 ≤9 个是契约 §3 的硬上限,
|
||
* 字段级 accept/maxSize/maxFiles 只能往更严收,不能往宽放)。
|
||
* accept 不含 .svg 由设计器把门(P28),这里只如实呈现配置。
|
||
*/
|
||
import { computed, inject, ref } from "vue";
|
||
import type { ComputedRef } 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";
|
||
|
||
/**
|
||
* 上传端点由填报页按通道注入(匿名面必须走 `/anon/form/upload?code=`,契约 v2 C6:
|
||
* 只有表单能定 form_file.sch_id=表单归属校)。没有注入=登录面默认,设计器预览等调用点行为不变。
|
||
*/
|
||
const uploadUrl = inject<ComputedRef<string>>("fcUploadUrl", computed(() => "/api/form/upload"));
|
||
|
||
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 }>(
|
||
uploadUrl.value,
|
||
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> |