diff --git a/backend/app/models/app.py b/backend/app/models/app.py index f388851..caade74 100644 --- a/backend/app/models/app.py +++ b/backend/app/models/app.py @@ -18,6 +18,11 @@ class App(Base): # [{"code": "brainstorm", "name": "头脑风暴", "path": "/brainstorm"}, ...] tools = Column(Text) + # 配置项定义(JSON 数组)- 定义租户可配置的参数 + # [{"key": "industry", "label": "行业类型", "type": "radio", "options": [...], "default": "...", "required": false}, ...] + # type: text(文本) | radio(单选) | select(下拉多选) | switch(开关) + config_schema = Column(Text) + # 是否需要企微JS-SDK require_jssdk = Column(SmallInteger, default=0) # 0-不需要 1-需要 diff --git a/backend/app/routers/apps.py b/backend/app/routers/apps.py index 871d809..b1c6dd0 100644 --- a/backend/app/routers/apps.py +++ b/backend/app/routers/apps.py @@ -23,6 +23,18 @@ class ToolItem(BaseModel): path: str +class ConfigSchemaItem(BaseModel): + """配置项定义""" + key: str # 配置键 + label: str # 显示标签 + type: str # text | radio | select | switch + options: Optional[List[str]] = None # radio/select 的选项值 + option_labels: Optional[dict] = None # 选项显示名称 {"value": "显示名"} + default: Optional[str] = None # 默认值 + placeholder: Optional[str] = None # 输入提示(text类型) + required: bool = False # 是否必填 + + class AppCreate(BaseModel): """创建应用""" app_code: str @@ -30,6 +42,7 @@ class AppCreate(BaseModel): base_url: Optional[str] = None description: Optional[str] = None tools: Optional[List[ToolItem]] = None + config_schema: Optional[List[ConfigSchemaItem]] = None require_jssdk: bool = False @@ -39,6 +52,7 @@ class AppUpdate(BaseModel): base_url: Optional[str] = None description: Optional[str] = None tools: Optional[List[ToolItem]] = None + config_schema: Optional[List[ConfigSchemaItem]] = None require_jssdk: Optional[bool] = None status: Optional[int] = None @@ -119,6 +133,7 @@ async def create_app( base_url=data.base_url, description=data.description, tools=json.dumps([t.model_dump() for t in data.tools], ensure_ascii=False) if data.tools else None, + config_schema=json.dumps([c.model_dump() for c in data.config_schema], ensure_ascii=False) if data.config_schema else None, require_jssdk=1 if data.require_jssdk else 0, status=1 ) @@ -150,6 +165,13 @@ async def update_app( else: update_data['tools'] = None + # 处理 config_schema JSON + if 'config_schema' in update_data: + if update_data['config_schema']: + update_data['config_schema'] = json.dumps([c.model_dump() if hasattr(c, 'model_dump') else c for c in update_data['config_schema']], ensure_ascii=False) + else: + update_data['config_schema'] = None + # 处理 require_jssdk if 'require_jssdk' in update_data: update_data['require_jssdk'] = 1 if update_data['require_jssdk'] else 0 @@ -259,6 +281,21 @@ async def get_app_tools( return tools +@router.get("/{app_code}/config-schema") +async def get_app_config_schema( + app_code: str, + user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取应用的配置项定义(用于租户订阅时渲染表单)""" + app = db.query(App).filter(App.app_code == app_code).first() + if not app: + raise HTTPException(status_code=404, detail="应用不存在") + + config_schema = json.loads(app.config_schema) if app.config_schema else [] + return config_schema + + def format_app(app: App) -> dict: """格式化应用数据""" return { @@ -268,6 +305,7 @@ def format_app(app: App) -> dict: "base_url": app.base_url, "description": app.description, "tools": json.loads(app.tools) if app.tools else [], + "config_schema": json.loads(app.config_schema) if app.config_schema else [], "require_jssdk": bool(app.require_jssdk), "status": app.status, "created_at": app.created_at, diff --git a/frontend/src/views/app-config/index.vue b/frontend/src/views/app-config/index.vue index 70a8702..40dc5ec 100644 --- a/frontend/src/views/app-config/index.vue +++ b/frontend/src/views/app-config/index.vue @@ -24,6 +24,7 @@ const tenantList = ref([]) const appList = ref([]) const appRequireJssdk = ref({}) // app_code -> require_jssdk const appBaseUrl = ref({}) // app_code -> base_url +const appConfigSchema = ref({}) // app_code -> config_schema // 企微应用列表(按租户) const wechatAppList = ref([]) @@ -46,6 +47,44 @@ const currentAppRequireJssdk = computed(() => { return appRequireJssdk.value[form.app_code] || false }) +// 当前应用的配置项定义 +const currentConfigSchema = computed(() => { + return appConfigSchema.value[form.app_code] || [] +}) + +// 配置值映射(方便读写) +const configValues = computed(() => { + const map = {} + form.custom_configs.forEach(c => { + map[c.key] = c + }) + return map +}) + +// 获取配置值 +function getConfigValue(key) { + return configValues.value[key]?.value || '' +} + +// 设置配置值 +function setConfigValue(key, value, remark = '') { + const existing = form.custom_configs.find(c => c.key === key) + if (existing) { + existing.value = value + if (remark) existing.remark = remark + } else { + form.custom_configs.push({ key, value, remark }) + } +} + +// 获取选项显示名称 +function getOptionLabel(schema, optionValue) { + if (schema.option_labels && schema.option_labels[optionValue]) { + return schema.option_labels[optionValue] + } + return optionValue +} + // 验证 app_code 必须是有效的应用 const validateAppCode = (rule, value, callback) => { if (!value) { @@ -72,6 +111,14 @@ watch(() => form.tenant_id, async (newVal) => { form.wechat_app_id = null }) +// 监听应用选择变化,初始化配置默认值 +watch(() => form.app_code, (newVal) => { + if (newVal && !editingId.value) { + // 新建时,根据 schema 初始化默认值 + initConfigDefaults() + } +}) + // 查看 Token 对话框 const tokenDialogVisible = ref(false) const currentToken = ref('') @@ -95,12 +142,25 @@ async function fetchApps() { for (const app of apps) { appRequireJssdk.value[app.app_code] = app.require_jssdk || false appBaseUrl.value[app.app_code] = app.base_url || '' + appConfigSchema.value[app.app_code] = app.config_schema || [] } } catch (e) { console.error('获取应用列表失败:', e) } } +// 根据 schema 初始化配置默认值 +function initConfigDefaults() { + const schema = currentConfigSchema.value + if (!schema.length) return + + form.custom_configs = schema.map(s => ({ + key: s.key, + value: s.default || '', + remark: '' + })) +} + async function fetchWechatApps(tenantId) { if (!tenantId) { wechatAppList.value = [] @@ -154,18 +214,41 @@ function handleCreate() { async function handleEdit(row) { editingId.value = row.id dialogTitle.value = '编辑应用订阅' + + // 先获取 schema + const schema = appConfigSchema.value[row.app_code] || [] + + // 合并已有配置和 schema 默认值 + const existingConfigs = row.custom_configs || [] + const existingMap = {} + existingConfigs.forEach(c => { existingMap[c.key] = c }) + + // 构建完整的配置列表(包含 schema 中的所有配置项) + const mergedConfigs = schema.map(s => ({ + key: s.key, + value: existingMap[s.key]?.value ?? s.default ?? '', + remark: existingMap[s.key]?.remark ?? '' + })) + + // 添加 schema 中没有但已存在的配置(兼容旧数据) + existingConfigs.forEach(c => { + if (!schema.find(s => s.key === c.key)) { + mergedConfigs.push({ ...c }) + } + }) + Object.assign(form, { tenant_id: row.tenant_id, app_code: row.app_code, app_name: row.app_name || '', wechat_app_id: row.wechat_app_id || null, - custom_configs: row.custom_configs ? [...row.custom_configs] : [] + custom_configs: mergedConfigs }) await fetchWechatApps(row.tenant_id) dialogVisible.value = true } -// 自定义配置管理 +// 自定义配置管理(用于没有 schema 定义时的手动添加) function addCustomConfig() { form.custom_configs.push({ key: '', value: '', remark: '' }) } @@ -402,49 +485,117 @@ onMounted(() => { - 自定义配置 - -
-
-
- - - -
- -
+