feat: LLM多阶段交互式Schema生成 + UI布局优化
- AI Service: 多阶段对话模型(理解→澄清→生成→校验→确认),支持对话历史和当前Schema上下文 - Backend: 适配多阶段AI模型,Gateway/Service/Controller传递history和currentSchema - Frontend: AiDesignerChat组件重写,支持阶段徽章、快速回复、Schema校验展示 - Frontend: 表单/流程设计器布局优化,AI助手和Schema预览作为独立列 - Frontend: 流程设计器元数据和模式选择器合并为一行 - Keycloak: 自定义登录主题(中文化) - 修复CSS媒体查询括号平衡问题
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { api } from './api';
|
||||
import type { Field, ControlType } from './schema';
|
||||
|
||||
export type DesignerStage = 'UNDERSTANDING' | 'CLARIFYING' | 'GENERATING' | 'VALIDATING' | 'CONFIRMING';
|
||||
|
||||
export type DesignerSuggestion = {
|
||||
stage: DesignerStage;
|
||||
formTitle?: string;
|
||||
formKey?: string;
|
||||
fields: {
|
||||
key: string; label: string; control: string; required: boolean;
|
||||
placeholder?: string; options?: string[]; helperText?: string;
|
||||
}[];
|
||||
process?: {
|
||||
mode: string;
|
||||
steps: { name: string; assigneeVariable: string }[];
|
||||
conditionThresholdDays?: number | null;
|
||||
};
|
||||
summary: string;
|
||||
understanding: string;
|
||||
assumptions: string[];
|
||||
needsClarification: string[];
|
||||
validationIssues?: { field: string; issue: string; severity: string }[];
|
||||
schemaReady: boolean;
|
||||
};
|
||||
|
||||
type ChatMessage = {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
suggestion?: DesignerSuggestion;
|
||||
};
|
||||
|
||||
export type ApplySuggestion = (s: DesignerSuggestion) => void;
|
||||
|
||||
const stageLabels: Record<DesignerStage, string> = {
|
||||
UNDERSTANDING: '🧠 理解需求',
|
||||
CLARIFYING: '❓ 澄清确认',
|
||||
GENERATING: '📋 生成 Schema',
|
||||
VALIDATING: '✅ 校验 Schema',
|
||||
CONFIRMING: '🎯 等待确认',
|
||||
};
|
||||
|
||||
export function AiDesignerChat({ onApply, contextLabel, getCurrentSchema }: { onApply: ApplySuggestion; contextLabel: string; getCurrentSchema?: () => string }) {
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||
{ role: 'assistant', content: '你好!我是 AI 设计助手。用自然语言描述你想要的表单或审批流程,我来帮你生成。\n\n例如:\n• "报销申请表单,包含报销人、金额、日期、事由"\n• "报销审批:先部门主管审批,超过5000元加财务复核,最后OA管理员审批"' },
|
||||
]);
|
||||
const [input, setInput] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentStage, setCurrentStage] = useState<DesignerStage>('UNDERSTANDING');
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' });
|
||||
}, [messages, loading]);
|
||||
|
||||
function buildHistory(msgs: ChatMessage[]) {
|
||||
return msgs.slice(1).map(m => ({ role: m.role, content: m.content }));
|
||||
}
|
||||
|
||||
async function send(presetText?: string) {
|
||||
const text = (presetText ?? input).trim();
|
||||
if (!text || loading) return;
|
||||
setInput('');
|
||||
const next = [...messages, { role: 'user' as const, content: text }];
|
||||
setMessages(next);
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await api<{ suggestion: DesignerSuggestion; model: string }>('/ai/designer-suggestions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
message: text,
|
||||
history: buildHistory(next.slice(0, -1)),
|
||||
currentSchema: getCurrentSchema?.() ?? null,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || 'Asia/Shanghai',
|
||||
}),
|
||||
});
|
||||
const s = result.suggestion;
|
||||
setCurrentStage(s.stage);
|
||||
const parts = formatSuggestionMessage(s);
|
||||
setMessages([...next, { role: 'assistant', content: parts, suggestion: s }]);
|
||||
} catch (e) {
|
||||
setMessages([...next, { role: 'assistant', content: `⚠️ ${e instanceof Error ? e.message : 'AI 服务暂时不可用'}` }]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="aiChatPanel">
|
||||
<div className="aiChatHeader">
|
||||
<span>✨ AI 设计助手</span>
|
||||
<small>{contextLabel}</small>
|
||||
<span className="aiStageBadge">{stageLabels[currentStage]}</span>
|
||||
</div>
|
||||
<div className="aiChatMessages" ref={scrollRef}>
|
||||
{messages.map((m, i) => <div key={i} className={`aiMsg ${m.role}`}>
|
||||
<div className="aiMsgContent">{m.content}</div>
|
||||
{m.suggestion && m.suggestion.fields.length > 0 && (m.suggestion.stage === 'GENERATING' || m.suggestion.stage === 'VALIDATING' || m.suggestion.stage === 'CONFIRMING') && (
|
||||
<button className="aiApplyBtn" onClick={() => onApply(m.suggestion!)}>应用到设计器 →</button>
|
||||
)}
|
||||
{m.suggestion && m.suggestion.needsClarification.length > 0 && m.suggestion.stage === 'CLARIFYING' && (
|
||||
<div className="aiQuickReplies">
|
||||
{m.suggestion.needsClarification.map((q, qi) => <button key={qi} className="aiQuickReply" onClick={() => void send(q)}>{q}</button>)}
|
||||
</div>
|
||||
)}
|
||||
{m.suggestion && m.suggestion.schemaReady && (
|
||||
<div className="aiSchemaReady">✅ Schema 已确认,可应用到设计器</div>
|
||||
)}
|
||||
</div>)}
|
||||
{loading && <div className="aiMsg assistant"><div className="aiMsgContent aiTyping">思考中…</div></div>}
|
||||
</div>
|
||||
<div className="aiChatInput">
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); void send(); } }}
|
||||
placeholder={currentStage === 'CONFIRMING' ? '输入"确认"或描述调整…' : '描述你想要的表单或流程…'}
|
||||
rows={2}
|
||||
disabled={loading}
|
||||
/>
|
||||
<button onClick={() => void send()} disabled={loading || !input.trim()}>发送</button>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function formatSuggestionMessage(s: DesignerSuggestion): string {
|
||||
const parts: string[] = [];
|
||||
if (s.understanding) parts.push(`🧠 我的理解:${s.understanding}`);
|
||||
if (s.needsClarification.length > 0) parts.push(`❓ 需要澄清:\n${s.needsClarification.map((q, i) => `${i + 1}. ${q}`).join('\n')}`);
|
||||
if (s.fields.length > 0) parts.push(`📋 表单字段(${s.fields.length} 个):\n${s.fields.map(f => ` • ${f.label}(${f.control}${f.required ? ',必填' : ''})`).join('\n')}`);
|
||||
if (s.process) parts.push(`🔄 审批流程(${s.process.mode}):${s.process.steps.map(st => st.name).join(' → ')}`);
|
||||
if (s.validationIssues && s.validationIssues.length > 0) parts.push(`⚠️ 校验问题:\n${s.validationIssues.map(v => ` • [${v.severity}] ${v.field}: ${v.issue}`).join('\n')}`);
|
||||
if (s.assumptions.length > 0) parts.push(`💡 假设:${s.assumptions.join(';')}`);
|
||||
if (s.summary) parts.push(`📝 ${s.summary}`);
|
||||
if (s.schemaReady) parts.push(`✅ Schema 已就绪,请确认或调整`);
|
||||
return parts.join('\n\n') || '已处理';
|
||||
}
|
||||
|
||||
export function suggestionToFields(s: DesignerSuggestion): Field[] {
|
||||
return s.fields.map(f => ({
|
||||
id: crypto.randomUUID(),
|
||||
key: f.key,
|
||||
label: f.label,
|
||||
control: (['text', 'textArea', 'select', 'dateTime', 'number'].includes(f.control) ? f.control : 'text') as ControlType,
|
||||
required: f.required,
|
||||
placeholder: f.placeholder,
|
||||
options: f.options,
|
||||
helperText: f.helperText,
|
||||
}));
|
||||
}
|
||||
+12
-3
@@ -4,6 +4,7 @@ import { userManager } from './auth';
|
||||
import { buildSchemas, type ControlType, type Field } from './schema';
|
||||
import { ProcessBindings } from './ProcessBindings';
|
||||
import { ProcessDesigner } from './ProcessDesigner';
|
||||
import { AiDesignerChat, suggestionToFields, type DesignerSuggestion } from './AiDesignerChat';
|
||||
|
||||
type FormVersion = { form_key: string; version: number; status: string; published_at?: string };
|
||||
const palette: { control: ControlType; label: string }[] = [
|
||||
@@ -46,11 +47,17 @@ function Designer() {
|
||||
const [selected, setSelected] = useState(fields[0].id);
|
||||
const [versions, setVersions] = useState<FormVersion[]>([]);
|
||||
const [message, setMessage] = useState('');
|
||||
const [showAI, setShowAI] = useState(false);
|
||||
const schemas = useMemo(() => buildSchemas(title, fields), [title, fields]);
|
||||
const selectedField = fields.find(f => f.id === selected);
|
||||
const loadVersions = () => api<FormVersion[]>('/admin/process-configuration/forms').then(setVersions).catch(e => setMessage(e.message));
|
||||
useEffect(() => { void loadVersions(); }, []);
|
||||
|
||||
function applySuggestion(s: DesignerSuggestion) {
|
||||
if (s.formTitle) setTitle(s.formTitle);
|
||||
if (s.formKey) setFormKey(s.formKey);
|
||||
if (s.fields.length) { const newFields = suggestionToFields(s); setFields(newFields); setSelected(newFields[0]?.id ?? ''); }
|
||||
}
|
||||
function add(control: ControlType) {
|
||||
const index = fields.length + 1;
|
||||
const field: Field = { id: crypto.randomUUID(), key: `field${index}`, label: `字段 ${index}`, control, required: false, ...(control === 'select' ? { options: ['OPTION_1', 'OPTION_2'] } : {}) };
|
||||
@@ -69,13 +76,15 @@ function Designer() {
|
||||
}
|
||||
|
||||
return <div className="appShell">
|
||||
<div className="subHeader"><div><strong>可视化表单设计器</strong><span>Schema 驱动移动端卡片</span></div><button onClick={save}>保存草稿</button></div>
|
||||
<div className="subHeader"><div><strong>可视化表单设计器</strong><span>Schema 驱动移动端卡片</span></div><div className="subHeaderActions"><button className="aiToggleBtn" onClick={() => setShowAI(!showAI)}>✨ AI 助手</button><button onClick={save}>保存草稿</button></div></div>
|
||||
{message && <div className="toast" onClick={() => setMessage('')}>{message}</div>}
|
||||
<section className="meta"><label>表单 Key<input value={formKey} onChange={e => setFormKey(e.target.value)} /></label><label>表单标题<input value={title} onChange={e => setTitle(e.target.value)} /></label></section>
|
||||
<main className="designer">
|
||||
<main className={`designer ${showAI ? 'designerWithAI' : ''}`}>
|
||||
<aside><h2>控件</h2>{palette.map(p => <button className="palette" key={p.control} onClick={() => add(p.control)}>+ {p.label}</button>)}<h2>版本</h2>{versions.filter(v => v.form_key === formKey).map(v => <div className="version" key={v.version}><span>v{v.version} · {v.status}</span>{v.status === 'DRAFT' && <button onClick={() => publish(v.version)}>发布</button>}</div>)}</aside>
|
||||
<section className="canvas"><div className="phone"><div className="phoneHeader">{title}</div>{fields.map((f, i) => <div className={`field ${selected === f.id ? 'selected' : ''}`} key={f.id} draggable onDragStart={e => e.dataTransfer.setData('field', f.id)} onDragOver={e => e.preventDefault()} onDrop={e => dropField(e, f.id)} onClick={() => setSelected(f.id)}><label>{f.label}{f.required && ' *'}</label>{f.control === 'textArea' ? <textarea disabled placeholder={f.placeholder} /> : f.control === 'select' ? <select disabled><option>{f.options?.[0] ?? '请选择'}</option></select> : <input disabled placeholder={f.control === 'dateTime' ? '请选择日期和时间' : f.placeholder} />}<div className="fieldActions"><button onClick={e => { e.stopPropagation(); move(f.id, -1); }}>↑</button><button onClick={e => { e.stopPropagation(); move(f.id, 1); }}>↓</button><button onClick={e => { e.stopPropagation(); setFields(fields.filter(x => x.id !== f.id)); }}>×</button></div><small>{i + 1}</small></div>)}</div></section>
|
||||
<aside className="properties"><h2>字段属性</h2>{selectedField ? <><label>字段 Key<input value={selectedField.key} onChange={e => update({ key: e.target.value })} /></label><label>显示名称<input value={selectedField.label} onChange={e => update({ label: e.target.value })} /></label><label>占位提示<input value={selectedField.placeholder ?? ''} onChange={e => update({ placeholder: e.target.value })} /></label><label className="check"><input type="checkbox" checked={selectedField.required} onChange={e => update({ required: e.target.checked })} />必填</label>{selectedField.control === 'select' && <label>选项(每行一个)<textarea value={(selectedField.options ?? []).join('\n')} onChange={e => update({ options: e.target.value.split('\n') })} /></label>}</> : <p>请选择字段</p>}<details><summary>生成的 Schema</summary><pre>{JSON.stringify(schemas, null, 2)}</pre></details></aside>
|
||||
{showAI && <div className="aiChatContainer"><AiDesignerChat onApply={applySuggestion} contextLabel="表单设计" getCurrentSchema={() => JSON.stringify(schemas)} /></div>}
|
||||
{showAI && <aside className="schemaPreview"><h2>生成的 Schema</h2><pre>{JSON.stringify(schemas, null, 2)}</pre></aside>}
|
||||
<aside className="properties"><h2>字段属性</h2>{selectedField ? <><label>字段 Key<input value={selectedField.key} onChange={e => update({ key: e.target.value })} /></label><label>显示名称<input value={selectedField.label} onChange={e => update({ label: e.target.value })} /></label><label>占位提示<input value={selectedField.placeholder ?? ''} onChange={e => update({ placeholder: e.target.value })} /></label><label className="check"><input type="checkbox" checked={selectedField.required} onChange={e => update({ required: e.target.checked })} />必填</label>{selectedField.control === 'select' && <label>选项(每行一个)<textarea value={(selectedField.options ?? []).join('\n')} onChange={e => update({ options: e.target.value.split('\n') })} /></label>}</> : <p>请选择字段</p>}</aside>
|
||||
</main>
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { api } from './api';
|
||||
import { AiDesignerChat, type DesignerSuggestion } from './AiDesignerChat';
|
||||
|
||||
type Mode = 'SERIAL' | 'PARALLEL' | 'CONDITIONAL';
|
||||
type Assignee = 'approverId' | 'oaAdministratorId' | 'hrReviewerId';
|
||||
@@ -28,6 +29,7 @@ export function ProcessDesigner() {
|
||||
]);
|
||||
const [selected, setSelected] = useState(steps[0].id);
|
||||
const [message, setMessage] = useState('');
|
||||
const [showAI, setShowAI] = useState(false);
|
||||
const [rules, setRules] = useState<ApprovalRule[]>(fallbackRules);
|
||||
const [history, setHistory] = useState<ProcessHistory[]>([]);
|
||||
async function loadConfiguration() {
|
||||
@@ -46,6 +48,18 @@ export function ProcessDesigner() {
|
||||
const duplicateRestrictedRule = mode !== 'SERIAL' && new Set(effectiveSteps.map(step => step.assigneeVariable)).size !== effectiveSteps.length;
|
||||
const selectedRule = rules.find(rule => rule.variable === selectedStep?.assigneeVariable);
|
||||
|
||||
function applySuggestion(s: DesignerSuggestion) {
|
||||
if (s.process) {
|
||||
const p = s.process;
|
||||
const m = (['SERIAL', 'PARALLEL', 'CONDITIONAL'].includes(p.mode) ? p.mode : 'SERIAL') as Mode;
|
||||
setMode(m);
|
||||
if (p.steps.length) {
|
||||
const newSteps = p.steps.map(st => ({ id: crypto.randomUUID(), name: st.name, assigneeVariable: (['approverId', 'oaAdministratorId', 'hrReviewerId'].includes(st.assigneeVariable) ? st.assigneeVariable : 'approverId') as Assignee }));
|
||||
setSteps(newSteps); setSelected(newSteps[0]?.id ?? '');
|
||||
}
|
||||
if (p.conditionThresholdDays != null) setThresholdDays(p.conditionThresholdDays);
|
||||
}
|
||||
}
|
||||
function changeMode(next: Mode) {
|
||||
setMode(next);
|
||||
if (next === 'CONDITIONAL' && steps.length < 2) setSteps([...steps, { id: crypto.randomUUID(), name: 'OA 管理员复核', assigneeVariable: 'oaAdministratorId' }]);
|
||||
@@ -79,16 +93,17 @@ export function ProcessDesigner() {
|
||||
|
||||
return <section className="processDesignerPage">
|
||||
{message && <div className="toast" onClick={() => setMessage('')}>{message}</div>}
|
||||
<div className="processTop"><div><h1>可视化流程设计器</h1><p>使用安全模板设计串行、并行和条件审批,并直接发布到 Flowable。</p></div><button onClick={deploy} disabled={duplicateRestrictedRule}>校验并部署</button></div>
|
||||
<section className="processMeta"><label>流程 Key<input value={key} onChange={e => setKey(e.target.value)} /></label><label>流程名称<input value={name} onChange={e => setName(e.target.value)} /></label></section>
|
||||
<div className="modeSelector">{modes.map(item => <button key={item.value} className={mode === item.value ? 'modeActive' : 'modeButton'} onClick={() => changeMode(item.value)}><strong>{item.label}</strong><span>{item.hint}</span></button>)}</div>
|
||||
<div className="processWorkspace">
|
||||
<div className="processTop"><div><h1>可视化流程设计器</h1><p>使用安全模板设计串行、并行和条件审批,并直接发布到 Flowable。</p></div><div className="subHeaderActions"><button className="aiToggleBtn" onClick={() => setShowAI(!showAI)}>✨ AI 助手</button><button onClick={deploy} disabled={duplicateRestrictedRule}>校验并部署</button></div></div>
|
||||
<section className="processMetaRow"><div className="processMeta"><label>流程 Key<input value={key} onChange={e => setKey(e.target.value)} /></label><label>流程名称<input value={name} onChange={e => setName(e.target.value)} /></label></div><div className="modeSelector">{modes.map(item => <button key={item.value} className={mode === item.value ? 'modeActive' : 'modeButton'} onClick={() => changeMode(item.value)}><strong>{item.label}</strong><span>{item.hint}</span></button>)}</div></section>
|
||||
<div className={`processWorkspace ${showAI ? 'processWorkspaceWithAI' : ''}`}>
|
||||
<div className="processCanvas"><div className="templateHint"><strong>{modes.find(item => item.value === mode)?.label}</strong><span>{summary}</span></div>
|
||||
<div className={`processGraph ${mode.toLowerCase()}`}><ProcessNode kind="start" title="开始" subtitle="业务已提交" /><Arrow />
|
||||
{mode === 'PARALLEL' && <><ProcessNode kind="gateway" title="并行拆分" subtitle="同时创建任务" /><Arrow /></>}
|
||||
<div className={mode === 'PARALLEL' ? 'parallelBranches' : 'linearNodes'}>{effectiveSteps.map((step, index) => <div className="graphStep" key={step.id}><button className={`approvalNode ${selected === step.id ? 'nodeSelected' : ''}`} onClick={() => setSelected(step.id)}><span>{index + 1}</span><strong>{step.name}</strong><small>{rules.find(item => item.variable === step.assigneeVariable)?.label}</small></button>{mode !== 'PARALLEL' && index < effectiveSteps.length - 1 && <Arrow label={mode === 'CONDITIONAL' ? `>${thresholdDays}天` : undefined} />}</div>)}</div>
|
||||
{mode === 'PARALLEL' && <><Arrow /><ProcessNode kind="gateway" title="并行汇聚" subtitle="全部通过" /></>}<Arrow /><ProcessNode kind="end" title="结束" subtitle="批准 / 驳回" /></div>
|
||||
</div>
|
||||
{showAI && <div className="aiChatContainer"><AiDesignerChat onApply={applySuggestion} contextLabel="流程设计" getCurrentSchema={() => JSON.stringify({ key, name, mode, steps: effectiveSteps.map(({ name: n, assigneeVariable }) => ({ name: n, assigneeVariable })), thresholdDays })} /></div>}
|
||||
{showAI && <aside className="schemaPreview"><h2>生成的 Schema</h2><pre>{JSON.stringify({ key, name, mode, steps: effectiveSteps.map(({ name: n, assigneeVariable }) => ({ name: n, assigneeVariable })), thresholdDays }, null, 2)}</pre></aside>}
|
||||
<aside className="processProperties"><h2>流程属性</h2>{mode === 'CONDITIONAL' && <label>时长阈值(天)<input type="number" min="0" step="0.5" value={thresholdDays} onChange={e => setThresholdDays(Number(e.target.value))} /></label>}<h2>审批节点</h2>{selectedStep ? <><label>节点名称<input value={selectedStep.name} onChange={e => update({ name: e.target.value })} /></label><label>审批人规则<select value={selectedStep.assigneeVariable} onChange={e => update({ assigneeVariable: e.target.value as Assignee })}>{rules.map(item => <option key={item.variable} value={item.variable}>{item.label}</option>)}</select></label>{selectedRule && <div className="ruleDetail"><div><span>来源</span><strong>{selectedRule.sourceType === 'POSITION' ? '岗位' : '角色'} · {selectedRule.selector}</strong></div><div><span>范围</span><strong>{selectedRule.scope === 'TENANT' ? '当前租户' : '发起人所在部门'}</strong></div><p>{selectedRule.description}</p><small>无人匹配时:拒绝提交,不自动跳过</small></div>}<div className="nodeActions"><button onClick={() => move(selectedStep.id, -1)}>上移</button><button onClick={() => move(selectedStep.id, 1)}>下移</button><button className="dangerGhost" disabled={effectiveSteps.length <= (mode === 'CONDITIONAL' ? 2 : 1)} onClick={() => setSteps(steps.filter(step => step.id !== selectedStep.id))}>删除</button></div></> : <p>请选择审批节点</p>}{mode !== 'CONDITIONAL' && steps.length < 6 && <button className="primaryWide" onClick={addStep}>+ 添加审批节点</button>}{duplicateRestrictedRule && <div className="validationError">并行会签或条件复核必须选择不同审批人规则,以满足职责分离。</div>}<div className="securityNote"><strong>安全约束</strong><p>审批规则由后端提供并在流程启动时根据有效组织、岗位和角色解析;无人匹配或职责冲突时拒绝提交。</p></div><div className="processHistory"><h2>已部署版本</h2>{history.length === 0 ? <p>暂无由设计器部署的版本</p> : history.map(item => <button key={`${item.key}:${item.version}`} onClick={() => loadTemplate(item)}><span><strong>{item.name}</strong><small>{item.key} · v{item.version} · {item.mode}</small></span><b>加载</b></button>)}</div></aside>
|
||||
</div>
|
||||
</section>;
|
||||
|
||||
@@ -8,6 +8,7 @@ export const userManager = new UserManager({
|
||||
post_logout_redirect_uri: window.location.origin,
|
||||
response_type: 'code',
|
||||
scope: 'openid profile email',
|
||||
extraQueryParams: { ui_locales: 'zh-CN' },
|
||||
userStore: new WebStorageStateStore({ store: window.sessionStorage }),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export type ControlType = 'text' | 'textArea' | 'select' | 'dateTime';
|
||||
export type Field = { id: string; key: string; label: string; control: ControlType; required: boolean; placeholder?: string; options?: string[] };
|
||||
export type ControlType = 'text' | 'textArea' | 'select' | 'dateTime' | 'number';
|
||||
export type Field = { id: string; key: string; label: string; control: ControlType; required: boolean; placeholder?: string; options?: string[]; helperText?: string };
|
||||
|
||||
export function buildSchemas(title: string, fields: Field[]) {
|
||||
const properties: Record<string, unknown> = {};
|
||||
@@ -8,7 +8,9 @@ export function buildSchemas(title: string, fields: Field[]) {
|
||||
? { type: 'string', enum: field.options?.filter(Boolean) ?? [] }
|
||||
: field.control === 'dateTime'
|
||||
? { type: 'string', format: 'date-time' }
|
||||
: { type: 'string', minLength: field.required ? 1 : 0, maxLength: field.control === 'textArea' ? 2000 : 255 };
|
||||
: field.control === 'number'
|
||||
? { type: 'number' }
|
||||
: { type: 'string', minLength: field.required ? 1 : 0, maxLength: field.control === 'textArea' ? 2000 : 255 };
|
||||
}
|
||||
return {
|
||||
dataSchema: { $id: `${title.toLowerCase().replace(/\s+/g, '-')}-draft`, title, type: 'object', required: fields.filter(f => f.required).map(f => f.key), properties },
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user