Files
form-center-front/src/components/fields/upload/pc.vue
T
chenxing 60083517c7 t15:前端双探针引导——/f/ 页通道选择走匿名面,顺修 F1 未登录引导与 F2 desc 插槽
- 新 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。
2026-10-20 14:05:05 +08:00

142 lines
4.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>