参照 ProfessionView/src/views/profession/RecordEdit.vue(权威契约)逐字段重写:
- 正文字段名由 content 改为 remark;按模块页补齐「年级段 / 学科 / 是否公开 / 栏目动态字段 / 添加附件」
- 新增「栏目动态字段」机制:这组字段由**栏目的后台配置**决定,后端构建期无法枚举 ⇒
后端只下发「去哪儿取」(含 {{占位符}} 依赖映射),前端带用户登录态取回后就地渲染;
提交时按模块页口径打包成 contents = JSON.stringify([{fieldId,value}])
- 支持七种栏目字段类型:text(多行转 textarea) / number / date / radio / checkbox / select / province_city(级联)
- 分区渲染在「正文内容」之前,与模块页一致;取不到字段定义时保留下拉并给「重新加载」
- 单行文本控件补 maxlength 与 placeholder(原先只有 textarea 认 placeholder)
- vite dev 补 /profession 代理,让「路径拼错」在本地就暴露
This commit is contained in:
+336
-11
@@ -60,9 +60,223 @@ const summary = ref<{ label: string; text: string }[]>([]);
|
||||
* 自动填入并隐藏 —— 模块页就是 {@code v-if="myClasses.length > 1"}`。
|
||||
* 只在真的取到唯一选项时才隐藏;选项没取到(302/超时)时照常显示,让用户能自己填。
|
||||
*/
|
||||
const renderFields = computed(() =>
|
||||
(props.form?.fields || []).filter(f => !f.mirrorOf && !hiddenSingle[f.name])
|
||||
);
|
||||
/* ==================== 栏目动态字段(业务模块按所选栏目下发) ==================== */
|
||||
/**
|
||||
* 「栏目动态字段」——由栏目配置决定、后端构建期**枚举不出来**的一组字段。
|
||||
*
|
||||
* <p>典型场景:教师专业发展的每个栏目(教学设计 / 课题研究…)可以挂管理员自定义字段,
|
||||
* 模块页在「发布」表单里把它们渲染在正文之前,提交时打包成 `contents`
|
||||
* (`JSON.stringify([{fieldId, value}])`)。要真正对齐就必须在**选好栏目之后**
|
||||
* 把这组定义取回来 —— 与下拉字典同源:模块接口只认浏览器 CAS 会话,
|
||||
* 所以后端只下发「去哪儿取」,这里带用户登录态取回后就地生成控件。
|
||||
*
|
||||
* <p>字段名统一加 {@link EXTRA_PREFIX} 前缀:既不与工具自己的参数撞名,
|
||||
* 也让提交前能把它们**摘出来**单独打包(它们不能混进 args 直接发给后端)。
|
||||
*/
|
||||
const EXTRA_PREFIX = "__x_";
|
||||
|
||||
/** 当前栏目下的动态字段(本地生成,不在后端下发的字段列表里) */
|
||||
const extraFields = ref<FormField[]>([]);
|
||||
/** idle=未选栏目 / loading / ok / fail(取不到只是少填自定义字段,不阻塞发布) */
|
||||
const extraState = ref<"idle" | "loading" | "ok" | "fail">("idle");
|
||||
/** 级联字段的区划树缓存(按层级:2 省市 / 3 省市区),同模块页 areaTreeCache */
|
||||
const areaTree = reactive<Record<number, any[]>>({});
|
||||
|
||||
function isExtraField(f?: FormField) {
|
||||
return !!f && f.name.startsWith(EXTRA_PREFIX);
|
||||
}
|
||||
|
||||
/** 栏目字段 id(打包 contents 时要用回原 id) */
|
||||
function extraFieldId(name: string) {
|
||||
return Number(name.slice(EXTRA_PREFIX.length));
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染哪几个字段。
|
||||
*
|
||||
* <p>栏目动态字段要插在**正文之前**(模块页的顺序是「标题 / 年级段学科 / 是否公开 →
|
||||
* 栏目动态字段 → 正文 → 附件」),所以按后端给的锚点({@code extraFields.before})插入,
|
||||
* 锚点不存在时退化为追加到末尾。
|
||||
*/
|
||||
const renderFields = computed(() => {
|
||||
const base = (props.form?.fields || []).filter(f => !f.mirrorOf && !hiddenSingle[f.name]);
|
||||
const ext = extraFields.value.filter(f => !hiddenSingle[f.name]);
|
||||
if (!ext.length) return base;
|
||||
const anchor = props.form?.extraFields?.before;
|
||||
const idx = anchor ? base.findIndex(f => f.name === anchor) : -1;
|
||||
if (idx < 0) return [...base, ...ext];
|
||||
return [...base.slice(0, idx), ...ext, ...base.slice(idx)];
|
||||
});
|
||||
|
||||
/** 栏目字段的选项形如 [{fleid, Name}](模块侧自有命名),转成通用 {value,label} */
|
||||
function extraOptions(raw: any): { value: string; label: string }[] {
|
||||
let arr: any[] = [];
|
||||
try {
|
||||
arr = raw?.extText ? JSON.parse(raw.extText) : [];
|
||||
} catch {
|
||||
arr = [];
|
||||
}
|
||||
if (!Array.isArray(arr)) return [];
|
||||
return arr
|
||||
.filter(o => o && o.fleid !== undefined && o.fleid !== null)
|
||||
.map(o => ({ value: String(o.fleid), label: o.Name ?? String(o.fleid) }));
|
||||
}
|
||||
|
||||
/** 级联字段的展开层级(2=省市 / 3=省市区,容错默认 3) */
|
||||
function extraAreaLevel(raw: any): 2 | 3 {
|
||||
try {
|
||||
const cfg = raw?.extText ? JSON.parse(raw.extText) : {};
|
||||
return Number(cfg?.level) === 2 ? 2 : 3;
|
||||
} catch {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
/** 栏目字段定义 → 表单控件(与模块页 RecordEdit.vue 的 fieldType 分支一一对应) */
|
||||
function mapExtraField(raw: any): FormField | null {
|
||||
const id = Number(raw?.id);
|
||||
if (!id) return null;
|
||||
const base: Partial<FormField> = {
|
||||
name: `${EXTRA_PREFIX}${id}`,
|
||||
label: raw.fieldName || `字段${id}`,
|
||||
required: !!raw.ifRequired,
|
||||
type: "string",
|
||||
};
|
||||
switch (String(raw.fieldType || "text")) {
|
||||
case "text": {
|
||||
const rows = Number(raw.rowNum || 1);
|
||||
return {
|
||||
...base,
|
||||
widget: rows > 1 ? "textarea" : "text",
|
||||
rows: rows > 1 ? rows : undefined,
|
||||
maxLength: Number(raw.maxLength) || undefined,
|
||||
placeholder: raw.defaultValue || "",
|
||||
} as FormField;
|
||||
}
|
||||
case "number":
|
||||
return { ...base, widget: "number", type: "number" } as FormField;
|
||||
case "date":
|
||||
return { ...base, widget: "date" } as FormField;
|
||||
case "radio":
|
||||
return { ...base, widget: "radio", options: extraOptions(raw) } as FormField;
|
||||
case "checkbox":
|
||||
// 模块页是 Checkbox 组,值本身是数组 —— 用多选下拉呈现同一语义,提交时仍是数组
|
||||
return { ...base, widget: "multiselect", options: extraOptions(raw) } as FormField;
|
||||
case "select":
|
||||
return { ...base, widget: "select", options: extraOptions(raw) } as FormField;
|
||||
case "province_city":
|
||||
return { ...base, widget: "cascader", areaLevel: extraAreaLevel(raw) } as FormField;
|
||||
default:
|
||||
// 模块端新加了未知类型:退成文本框,至少不丢数据
|
||||
return { ...base, widget: "text" } as FormField;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把地址里的 {@code {{占位符}}} 换成对应表单字段的当前值。
|
||||
* <p>占位符名与字段名**并不相同**(模块侧的栏目字段接口查询参数叫 SerID,
|
||||
* 表单字段叫 moduleId),所以按后端给的 dependsOn 映射替换,而不是同名替换。
|
||||
*/
|
||||
function fillExtraPlaceholders(tpl: string): string {
|
||||
let out = String(tpl || "");
|
||||
const map = props.form?.extraFields?.dependsOn || {};
|
||||
Object.entries(map).forEach(([ph, field]) => {
|
||||
out = out.split(`{{${ph}}}`).join(encodeURIComponent(String(values[field] ?? "")));
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 依赖字段(如「所属栏目」)是否都已填好 —— 没选栏目就不该去拉它的字段定义 */
|
||||
function extraDepsReady(): boolean {
|
||||
const deps = Object.values(props.form?.extraFields?.dependsOn || {});
|
||||
return deps.every(d => {
|
||||
const v = values[d];
|
||||
return v !== undefined && v !== null && String(v).trim() !== "";
|
||||
});
|
||||
}
|
||||
|
||||
/** 换栏目时把上一栏目的字段与取值一起清掉(否则会带着旧栏目的值提交) */
|
||||
function clearExtra() {
|
||||
extraFields.value.forEach(f => {
|
||||
delete values[f.name];
|
||||
delete errors[f.name];
|
||||
});
|
||||
extraFields.value = [];
|
||||
}
|
||||
|
||||
/** 取回「当前栏目」的动态字段定义并生成控件 */
|
||||
async function loadExtraFields(force = false) {
|
||||
const spec = props.form?.extraFields;
|
||||
if (!spec) {
|
||||
extraState.value = "idle";
|
||||
return;
|
||||
}
|
||||
if (!extraDepsReady()) {
|
||||
clearExtra();
|
||||
extraState.value = "idle";
|
||||
return;
|
||||
}
|
||||
clearExtra();
|
||||
extraState.value = "loading";
|
||||
const call = { ...spec.call, pathAndQuery: fillExtraPlaceholders(spec.call.pathAndQuery) };
|
||||
const res = await callModuleApi(call, undefined, { forceWarm: force });
|
||||
let payload: any = null;
|
||||
try {
|
||||
payload = res.error ? null : JSON.parse(res.body || "");
|
||||
} catch {
|
||||
payload = null;
|
||||
}
|
||||
if (payload && typeof payload === "object" && "code" in payload) {
|
||||
payload = Number(payload.code) === 0 ? payload.data : null;
|
||||
}
|
||||
const raw = payload ? payload[spec.fieldsKey || "fields"] : null;
|
||||
if (!Array.isArray(raw)) {
|
||||
extraState.value = "fail";
|
||||
return;
|
||||
}
|
||||
const mapped = raw.map(mapExtraField).filter((f): f is FormField => !!f);
|
||||
mapped.forEach(f => {
|
||||
values[f.name] = f.widget === "multiselect" ? [] : "";
|
||||
});
|
||||
extraFields.value = mapped;
|
||||
extraState.value = "ok";
|
||||
// 级联字段:按层级懒加载区划树(多个同级字段共用一份)
|
||||
const levels = Array.from(new Set(mapped.filter(f => f.widget === "cascader").map(f => f.areaLevel || 3)));
|
||||
for (const lv of levels) await ensureAreaTree(lv);
|
||||
}
|
||||
|
||||
/** 按层级取一次区划树(模块页 areaTreeCache 的同款做法,一次请求多字段共用) */
|
||||
async function ensureAreaTree(level: number) {
|
||||
const areaCall = props.form?.extraFields?.areaCall;
|
||||
if (!areaCall || (areaTree[level] && areaTree[level].length)) return;
|
||||
const call = {
|
||||
...areaCall,
|
||||
pathAndQuery: String(areaCall.pathAndQuery || "").split("{{level}}").join(String(level)),
|
||||
};
|
||||
const res = await callModuleApi(call);
|
||||
let payload: any = null;
|
||||
try {
|
||||
payload = res.error ? null : JSON.parse(res.body || "");
|
||||
} catch {
|
||||
payload = null;
|
||||
}
|
||||
if (payload && typeof payload === "object" && "code" in payload) {
|
||||
payload = Number(payload.code) === 0 ? payload.data : null;
|
||||
}
|
||||
if (Array.isArray(payload) && payload.length) areaTree[level] = payload;
|
||||
}
|
||||
|
||||
/** 级联选择:v-model 存编码数组,与模块页 contents 的 value:string[] 契约一致 */
|
||||
function onExtraAreaChange(f: FormField, v: any) {
|
||||
values[f.name] = Array.isArray(v) ? v : v ? [v] : [];
|
||||
}
|
||||
|
||||
/** 用户点「重新加载」:强制再走一遍(含模块会话预热) */
|
||||
async function reloadExtraFields() {
|
||||
if (props.disabled || props.submitted) return;
|
||||
await loadExtraFields(true);
|
||||
if (extraState.value === "fail") Message.warning("该栏目的自定义字段还是没取到,可稍后重试,或先不填这几项");
|
||||
}
|
||||
|
||||
/* ==================== 附件上传(widget=file) ==================== */
|
||||
/** 已上传附件的文件名(按字段名),仅用于界面展示 */
|
||||
@@ -444,6 +658,10 @@ function initValues() {
|
||||
Object.keys(switchOn).forEach(k => delete switchOn[k]);
|
||||
Object.keys(dailyRows).forEach(k => delete dailyRows[k]);
|
||||
Object.keys(hiddenSingle).forEach(k => delete hiddenSingle[k]);
|
||||
// 栏目动态字段:换表单就整体丢弃(取值、区划树缓存都不跨表单复用)
|
||||
extraFields.value = [];
|
||||
extraState.value = "idle";
|
||||
Object.keys(areaTree).forEach(k => delete areaTree[Number(k)]);
|
||||
resetOptionState();
|
||||
// 「全字段表单」把已识别的参数直接预填进控件(不再只做只读回显)
|
||||
const preset = props.form?.preset || {};
|
||||
@@ -479,6 +697,8 @@ function initValues() {
|
||||
syncOptions();
|
||||
// 预填里可能已经勾了「连续请假」(如模型已识别)→ 立刻展开逐天时段
|
||||
for (const f of props.form?.fields || []) if (f.dailyExpand) syncDailyRows(f);
|
||||
// 栏目动态字段:只有「所属栏目」已经有值(模型识别出来了)才立刻去取
|
||||
loadExtraFields();
|
||||
}
|
||||
|
||||
initValues();
|
||||
@@ -493,6 +713,18 @@ watch(
|
||||
() => syncOptions()
|
||||
);
|
||||
|
||||
/**
|
||||
* 依赖字段(「所属栏目」)一变就重新取栏目动态字段。
|
||||
* 签名只由依赖字段组成 —— 用户在其它字段上打字不会触发模块请求。
|
||||
*/
|
||||
watch(
|
||||
() =>
|
||||
Object.values(props.form?.extraFields?.dependsOn || {})
|
||||
.map(d => `${d}=${values[d] ?? ""}`)
|
||||
.join("&"),
|
||||
() => loadExtraFields()
|
||||
);
|
||||
|
||||
/**
|
||||
* 「连续请假」的逐天时段:基准起止时间 / 天数 / 勾选态任一变化就重建。
|
||||
* <p>签名只由这三个基准组成 —— 用户逐天微调过的行不会被自己的编辑动作覆盖。
|
||||
@@ -603,7 +835,8 @@ function onSubmit() {
|
||||
if (props.submitted || props.disabled) return;
|
||||
|
||||
let ok = true;
|
||||
for (const f of props.form?.fields || []) {
|
||||
// 必填校验要连栏目动态字段一起看(它们是模块侧标了 ifRequired 的字段)
|
||||
for (const f of [...(props.form?.fields || []), ...extraFields.value]) {
|
||||
const v = values[f.name];
|
||||
const empty = v === undefined || v === null || String(v).trim() === "";
|
||||
if (f.required && empty) {
|
||||
@@ -703,6 +936,40 @@ function onSubmit() {
|
||||
Message.warning("发布方式选了「提交审批」,请先在「审批流程」里勾选并选择审批人");
|
||||
return;
|
||||
}
|
||||
// 栏目动态字段:单独打包成 contents(不是普通参数,绝不能混进 args 直接发)
|
||||
const extraSpec = props.form?.extraFields;
|
||||
if (extraSpec?.param) {
|
||||
const packed: { fieldId: number; value: any }[] = [];
|
||||
for (const f of extraFields.value) {
|
||||
if (hiddenSingle[f.name]) continue;
|
||||
const v = values[f.name];
|
||||
if (Array.isArray(v)) {
|
||||
// 模块页的 checkbox 恒带数组(未勾选也是 []),这里保持一致
|
||||
packed.push({ fieldId: extraFieldId(f.name), value: v });
|
||||
} else if (v === undefined || v === null || String(v).trim() === "") {
|
||||
// 文本/日期/单选没填就不带这一项(与模块页「只提交碰过的字段」同口径)
|
||||
continue;
|
||||
} else {
|
||||
packed.push({ fieldId: extraFieldId(f.name), value: normalize(f, v) });
|
||||
}
|
||||
}
|
||||
args[extraSpec.param] = JSON.stringify(packed);
|
||||
if (packed.length) {
|
||||
rows.push({
|
||||
label: extraSpec.title || "栏目动态字段",
|
||||
text: packed
|
||||
.map(p => {
|
||||
const f = extraFields.value.find(x => extraFieldId(x.name) === p.fieldId);
|
||||
const opt = f ? (f.options || []).find(o => String(o.value) === String(p.value)) : undefined;
|
||||
const shown = Array.isArray(p.value)
|
||||
? p.value.map(t => (f?.options || []).find(o => String(o.value) === String(t))?.label || t).join("、") || "(空)"
|
||||
: opt?.label || String(p.value);
|
||||
return `${f?.label || p.fieldId}:${shown}`;
|
||||
})
|
||||
.join(";"),
|
||||
});
|
||||
}
|
||||
}
|
||||
summary.value = rows;
|
||||
emit("submit", { tool: props.form.tool, args });
|
||||
}
|
||||
@@ -745,12 +1012,13 @@ function reset() {
|
||||
|
||||
<!-- 填写区 -->
|
||||
<div v-if="!submitted" class="form-card__fields">
|
||||
<div
|
||||
v-for="f in renderFields"
|
||||
:key="f.name"
|
||||
class="field"
|
||||
:class="{ 'field--wide': isWide(f) }"
|
||||
>
|
||||
<template v-for="(f, fi) in renderFields" :key="f.name">
|
||||
<!-- 栏目动态字段的分区标题:只在「第一张栏目字段」之前出现一次 -->
|
||||
<div v-if="isExtraField(f) && !isExtraField(renderFields[fi - 1])" class="fields-divider">
|
||||
<span class="fields-divider__text">{{ form.extraFields?.title || "栏目动态字段" }}</span>
|
||||
<span class="fields-divider__hint">由「所属栏目」的配置决定</span>
|
||||
</div>
|
||||
<div class="field" :class="{ 'field--wide': isWide(f) }">
|
||||
<label v-if="widgetOf(f) !== 'switch'" class="field__label">
|
||||
<span v-if="f.required" class="field__star">*</span>{{ f.label }}
|
||||
</label>
|
||||
@@ -798,6 +1066,19 @@ function reset() {
|
||||
<Option v-for="o in fieldOptions(f)" :key="o.value" :value="o.value">{{ o.label }}</Option>
|
||||
</Select>
|
||||
|
||||
<div v-else-if="widgetOf(f) === 'cascader'" class="field__cascader">
|
||||
<Cascader
|
||||
:data="areaTree[f.areaLevel || 3] || []"
|
||||
:model-value="values[f.name]"
|
||||
:change-on-select="true"
|
||||
:placeholder="f.placeholder || '请选择省市区'"
|
||||
:disabled="disabled"
|
||||
transfer
|
||||
style="width: 100%"
|
||||
@on-change="(v: any) => onExtraAreaChange(f, v)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="widgetOf(f) === 'file'" class="field__file">
|
||||
<label class="file-btn" :class="{ 'file-btn--busy': uploadState[f.name] === 'uploading' }">
|
||||
<input
|
||||
@@ -892,7 +1173,8 @@ function reset() {
|
||||
<Input
|
||||
v-else
|
||||
v-model="values[f.name]"
|
||||
:placeholder="f.example || f.description || '请输入'"
|
||||
:maxlength="f.maxLength"
|
||||
:placeholder="f.placeholder || f.example || f.description || '请输入'"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</div>
|
||||
@@ -949,6 +1231,13 @@ function reset() {
|
||||
<p v-if="f.description && f.example" class="field__tip">{{ f.description }}</p>
|
||||
<p v-if="errors[f.name]" class="field__error">{{ errors[f.name] }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 栏目自定义字段取不到:停在原地给「重新加载」,不静默少填 -->
|
||||
<p v-if="extraState === 'fail'" class="field__warn field__warn--extra">
|
||||
这个栏目的自定义字段暂时没取到(模块接口未通或需要重新登录)。
|
||||
<a class="field__retry-btn" @click="reloadExtraFields()">重新加载</a>
|
||||
<span class="field__warn-sub">也可以先不填这几项直接发布,或到系统里补齐。</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 提交后:只读摘要 -->
|
||||
@@ -1081,6 +1370,42 @@ function reset() {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
/* ------------------------------ 栏目动态字段(业务模块按栏目下发) ------------------------------ */
|
||||
/* 分区标题要横跨整行(网格是 auto-fill 多列,否则会挤在某一格里) */
|
||||
.fields-divider {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
margin-top: 2px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed var(--agent-border);
|
||||
}
|
||||
|
||||
.fields-divider__text {
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
color: var(--agent-text);
|
||||
}
|
||||
|
||||
.fields-divider__hint {
|
||||
font-size: 11px;
|
||||
color: var(--agent-text-mute);
|
||||
}
|
||||
|
||||
.field__warn--extra {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.field__warn-sub {
|
||||
margin-left: 6px;
|
||||
color: var(--agent-text-mute);
|
||||
}
|
||||
|
||||
.field__cascader {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field__label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
|
||||
+40
-1
@@ -50,7 +50,9 @@ export interface FormField {
|
||||
/** 单选按钮组(平铺,对齐模块页的 RadioGroup,如「请假类型」) */
|
||||
| "radio"
|
||||
/** 勾选框(对齐模块页的 Checkbox,如「连续请假」) */
|
||||
| "switch";
|
||||
| "switch"
|
||||
/** 级联选择(栏目动态字段的 province_city:省市区,值存编码数组) */
|
||||
| "cascader";
|
||||
type?: string;
|
||||
required: boolean;
|
||||
/** 字段说明(含义与格式) */
|
||||
@@ -109,6 +111,8 @@ export interface FormField {
|
||||
rows?: number;
|
||||
/** 文本最大长度(显示字数统计) */
|
||||
maxLength?: number;
|
||||
/** widget=cascader:区划树展开层级(2 省市 / 3 省市区),来自栏目字段配置 */
|
||||
areaLevel?: number;
|
||||
/** 下拉选项 */
|
||||
options?: { value: string; label: string }[];
|
||||
/**
|
||||
@@ -161,6 +165,41 @@ export interface FormRequest {
|
||||
/** 缺失字段的中文名 */
|
||||
missing?: string[];
|
||||
hint?: string;
|
||||
/**
|
||||
* 「栏目动态字段」——由业务模块的**栏目配置**决定、后端构建期无法枚举的一组字段。
|
||||
*
|
||||
* <p>典型场景:教师专业发展每个栏目(教学设计/课题研究…)可以挂管理员自定义字段,
|
||||
* 模块页在「发布」表单里把它们渲染在正文之前,提交时打包成 `contents`
|
||||
* (`JSON.stringify([{fieldId, value}])`)。助手要真正对齐,就必须在选好栏目后
|
||||
* 把这组字段取回来渲染 —— 步骤与「下拉字典代取」一致。
|
||||
*/
|
||||
extraFields?: ExtraFieldsSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* 「栏目动态字段」的代取描述。
|
||||
*
|
||||
* <p>与下拉字典同源:模块的字典接口只认浏览器 CAS 会话,服务端取不到,
|
||||
* 所以后端只下发「去哪儿取」,前端带用户登录态取回后就地渲染。
|
||||
*/
|
||||
export interface ExtraFieldsSpec {
|
||||
/** 取字段定义的调用;地址里含 `{{占位符}}`,依赖有值后替换再发 */
|
||||
call: BrowserToolCall;
|
||||
/**
|
||||
* 占位符 → 表单字段名(如 `SerID → moduleId`)。
|
||||
* 模块侧的查询参数习惯用老式命名(`SerID`),与表单字段名(`moduleId`)并不一致。
|
||||
*/
|
||||
dependsOn?: Record<string, string>;
|
||||
/** 渲染结果打包后写进哪个提交参数(专业发展是 contents) */
|
||||
param: string;
|
||||
/** 区块标题(默认「栏目动态字段」) */
|
||||
title?: string;
|
||||
/** 渲染在哪个字段**之前**(对齐模块页:栏目字段在正文与附件之上) */
|
||||
before?: string;
|
||||
/** 响应里字段数组所在的键(默认 fields,即 data.fields) */
|
||||
fieldsKey?: string;
|
||||
/** 「省市区」级联字段的区划树调用(地址里含 `{{level}}`,按字段配置取 2 或 3 级) */
|
||||
areaCall?: BrowserToolCall;
|
||||
}
|
||||
|
||||
/** 表单提交体 */
|
||||
|
||||
@@ -101,6 +101,9 @@ export default defineConfig((): UserConfig => {
|
||||
"/dormnotice": { target: "http://localhost:8093", changeOrigin: true },
|
||||
"/familyactivity": { target: "http://localhost:8093", changeOrigin: true },
|
||||
"/coursehomework": { target: "http://localhost:8093", changeOrigin: true },
|
||||
// 教师专业发展:前缀与模块 key 同名(profession),但按同一条铁律显式列出,
|
||||
// 免得以后改了 key 又出现「本地测不出、上线 404」。
|
||||
"/profession": { target: "http://localhost:8093", changeOrigin: true },
|
||||
},
|
||||
},
|
||||
base: "./",
|
||||
|
||||
Reference in New Issue
Block a user