feat: 办事大厅用户面前端(hall)首版入库——由 app/public 改名去 app 层;build.yml 修并发撞名容exit137
build / build (push) Successful in 3m6s
build / build (push) Successful in 3m6s
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 动态表单渲染器(V2.0 Part B.3:schema → 控件,一期 11 类)。
|
||||
* schemaJson = {"groups":[{title, fields:[{key,label,type,required,pattern,options,...}]}]}。
|
||||
* v-model(formData) 为 {key:value} 对象;upload/image 字段值为 /api/file/upload 返回体数组
|
||||
* [{id,fileName,fileUrl,fileSize,mimeType}],多选类(checkbox/multiselect)为 string[]。
|
||||
*/
|
||||
import { computed } from "vue";
|
||||
import { Message } from "view-ui-plus";
|
||||
import { fileApi } from "@/api/module/svc";
|
||||
|
||||
const props = defineProps<{ schemaJson?: string; formData: Record<string, any> }>();
|
||||
const emit = defineEmits<{ (e: "update:formData", v: Record<string, any>): void }>();
|
||||
|
||||
interface Field {
|
||||
key: string;
|
||||
label: string;
|
||||
type: string;
|
||||
required?: boolean;
|
||||
pattern?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
maxLength?: number;
|
||||
tip?: string;
|
||||
placeholder?: string;
|
||||
options?: string[];
|
||||
rows?: number;
|
||||
defaultValue?: any;
|
||||
accept?: string;
|
||||
maxSize?: number;
|
||||
maxFiles?: number;
|
||||
}
|
||||
|
||||
const groups = computed<{ title: string; fields: Field[] }[]>(() => {
|
||||
try {
|
||||
const parsed = JSON.parse(props.schemaJson || "{}");
|
||||
return (parsed.groups || []).map((g: any) => ({ title: g.title || "", fields: g.fields || [] }));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
function setValue(field: Field, value: any) {
|
||||
emit("update:formData", { ...props.formData, [field.key]: value });
|
||||
}
|
||||
|
||||
function ensureArray(field: Field): any[] {
|
||||
const v = props.formData[field.key];
|
||||
return Array.isArray(v) ? v : [];
|
||||
}
|
||||
|
||||
/** upload/image 上传(maxSize MB、maxFiles 上限;maxFiles 缺省/0=不限;落库为 UploadVo 原样对象) */
|
||||
async function uploadFile(field: Field, ev: Event) {
|
||||
const input = ev.target as HTMLInputElement;
|
||||
const file = input.files?.[0];
|
||||
input.value = "";
|
||||
if (!file) return;
|
||||
if (field.maxSize && file.size > field.maxSize * 1024 * 1024) {
|
||||
Message.error(`单个文件不得超过 ${field.maxSize}MB`);
|
||||
return;
|
||||
}
|
||||
const list = ensureArray(field);
|
||||
if (field.maxFiles && field.maxFiles > 0 && list.length >= field.maxFiles) {
|
||||
Message.error(`附件数超过上限 ${field.maxFiles}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// image 走 image 通道:后端按 type 分目录(/upload/image/** 与 /upload/file/**)
|
||||
const res: any = await fileApi.upload(file, field.type === "image" ? "image" : "file").send();
|
||||
setValue(field, [...list, res]);
|
||||
} catch (e: any) {
|
||||
Message.error(e?.message || "上传失败");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="dyn-form">
|
||||
<div v-for="(group, gi) in groups" :key="gi" class="dyn-group">
|
||||
<div v-if="group.title" class="dyn-group-title">{{ group.title }}</div>
|
||||
<div v-for="field in group.fields" :key="field.key" class="dyn-field">
|
||||
<label class="dyn-label">
|
||||
<span v-if="field.required" class="dyn-required">*</span>
|
||||
{{ field.label }}
|
||||
</label>
|
||||
|
||||
<template v-if="field.type === 'text' || field.type === 'phone'">
|
||||
<Input
|
||||
:model-value="formData[field.key] ?? field.defaultValue ?? ''"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:maxlength="field.maxLength"
|
||||
@update:model-value="(v: string) => setValue(field, v)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === 'textarea'">
|
||||
<Input
|
||||
type="textarea"
|
||||
:rows="field.rows || 3"
|
||||
:model-value="formData[field.key] ?? field.defaultValue ?? ''"
|
||||
:placeholder="field.placeholder || ''"
|
||||
:maxlength="field.maxLength"
|
||||
@update:model-value="(v: string) => setValue(field, v)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === 'number'">
|
||||
<InputNumber
|
||||
:model-value="formData[field.key] ?? field.defaultValue ?? null"
|
||||
:min="field.min"
|
||||
:max="field.max"
|
||||
:placeholder="field.placeholder || ''"
|
||||
style="width: 220px"
|
||||
@update:model-value="(v: number) => setValue(field, v)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === 'date'">
|
||||
<DatePicker
|
||||
type="date"
|
||||
:model-value="formData[field.key] ?? field.defaultValue ?? ''"
|
||||
placeholder="选择日期"
|
||||
style="width: 220px"
|
||||
@update:model-value="(v: string) => setValue(field, v)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === 'select'">
|
||||
<Select
|
||||
:model-value="formData[field.key] ?? field.defaultValue ?? ''"
|
||||
:placeholder="field.placeholder || '请选择'"
|
||||
style="width: 260px"
|
||||
@update:model-value="(v: string) => setValue(field, v)"
|
||||
>
|
||||
<Option v-for="opt in field.options || []" :key="opt" :value="opt">{{ opt }}</Option>
|
||||
</Select>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === 'multiselect'">
|
||||
<Select
|
||||
:model-value="ensureArray(field)"
|
||||
multiple
|
||||
:placeholder="field.placeholder || '请选择'"
|
||||
style="width: 260px"
|
||||
@update:model-value="(v: any[]) => setValue(field, v)"
|
||||
>
|
||||
<Option v-for="opt in field.options || []" :key="opt" :value="opt">{{ opt }}</Option>
|
||||
</Select>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === 'radio'">
|
||||
<RadioGroup
|
||||
:model-value="formData[field.key] ?? field.defaultValue ?? ''"
|
||||
@update:model-value="(v: string) => setValue(field, v)"
|
||||
>
|
||||
<Radio v-for="opt in field.options || []" :key="opt" :label="opt">{{ opt }}</Radio>
|
||||
</RadioGroup>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === 'checkbox'">
|
||||
<CheckboxGroup
|
||||
:model-value="ensureArray(field)"
|
||||
@update:model-value="(v: any[]) => setValue(field, v)"
|
||||
>
|
||||
<Checkbox v-for="opt in field.options || []" :key="opt" :label="opt">{{ opt }}</Checkbox>
|
||||
</CheckboxGroup>
|
||||
</template>
|
||||
|
||||
<template v-else-if="field.type === 'upload' || field.type === 'image'">
|
||||
<div class="dyn-upload">
|
||||
<div v-for="(f, fi) in ensureArray(field)" :key="fi" class="dyn-file">
|
||||
<img
|
||||
v-if="field.type === 'image' && f.fileUrl"
|
||||
:src="f.fileUrl"
|
||||
class="dyn-thumb"
|
||||
alt=""
|
||||
/>
|
||||
<a :href="f.fileUrl" target="_blank" rel="noopener">{{ f.fileName }}</a>
|
||||
<a
|
||||
class="dyn-file-remove"
|
||||
@click.prevent="
|
||||
setValue(
|
||||
field,
|
||||
ensureArray(field).filter((_, i) => i !== fi)
|
||||
)
|
||||
"
|
||||
>移除</a>
|
||||
</div>
|
||||
<label class="dyn-file-btn">
|
||||
<input
|
||||
type="file"
|
||||
style="display: none"
|
||||
:accept="field.accept || (field.type === 'image' ? '.jpg,.jpeg,.png,.gif' : '')"
|
||||
@change="uploadFile(field, $event)"
|
||||
/>
|
||||
<Button size="small" icon="ios-cloud-upload-outline">
|
||||
{{ field.type === 'image' ? '上传图片' : '上传附件' }}
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="field.tip" class="dyn-tip">{{ field.tip }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dyn-group {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.dyn-group-title {
|
||||
font-weight: 600;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.dyn-field {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.dyn-label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: #515a6e;
|
||||
}
|
||||
.dyn-required {
|
||||
color: #ed4014;
|
||||
margin-right: 2px;
|
||||
}
|
||||
.dyn-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.dyn-file {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.dyn-file-remove {
|
||||
color: #ed4014;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
.dyn-thumb {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
object-fit: cover;
|
||||
vertical-align: middle;
|
||||
margin-right: 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user