Compare commits

...

2 Commits

Author SHA1 Message Date
cf4d6afbc8 feat: 应用配置项定义功能
All checks were successful
continuous-integration/drone/push Build is passing
- 后端: App 模型添加 config_schema 字段,支持配置项定义
- 后端: apps API 支持 config_schema 的增删改查
- 前端: 应用管理页面支持定义配置项(text/radio/select/switch类型)
- 前端: 租户订阅页面根据应用 schema 动态渲染对应表单控件
- 支持设置选项、默认值、必填等属性
2026-01-27 17:26:49 +08:00
e37466a7cc feat: 租户应用配置支持自定义参数
All checks were successful
continuous-integration/drone/push Build is passing
- 后端: TenantApp 模型添加 custom_configs 字段 (LONGTEXT)
- 后端: tenant_apps API 支持自定义配置的增删改查
- 前端: 应用订阅编辑对话框增加自定义配置编辑区域
- 支持 key-value-备注 三字段结构
- value 使用 textarea 支持超长文本(如提示词)
2026-01-27 17:15:19 +08:00
9 changed files with 1673 additions and 1164 deletions

View File

@@ -18,6 +18,11 @@ class App(Base):
# [{"code": "brainstorm", "name": "头脑风暴", "path": "/brainstorm"}, ...] # [{"code": "brainstorm", "name": "头脑风暴", "path": "/brainstorm"}, ...]
tools = Column(Text) tools = Column(Text)
# 配置项定义JSON 数组)- 定义租户可配置的参数
# [{"key": "industry", "label": "行业类型", "type": "radio", "options": [...], "default": "...", "required": false}, ...]
# type: text(文本) | radio(单选) | select(下拉多选) | switch(开关)
config_schema = Column(Text)
# 是否需要企微JS-SDK # 是否需要企微JS-SDK
require_jssdk = Column(SmallInteger, default=0) # 0-不需要 1-需要 require_jssdk = Column(SmallInteger, default=0) # 0-不需要 1-需要

View File

@@ -23,6 +23,10 @@ class TenantApp(Base):
# 功能权限 # 功能权限
allowed_tools = Column(Text) # JSON 数组 allowed_tools = Column(Text) # JSON 数组
# 自定义配置JSON 数组)
# [{"key": "industry", "value": "medical_beauty", "remark": "医美行业"}, ...]
custom_configs = Column(Text)
status = Column(SmallInteger, default=1) status = Column(SmallInteger, default=1)
created_at = Column(TIMESTAMP, default=datetime.now) created_at = Column(TIMESTAMP, default=datetime.now)
updated_at = Column(TIMESTAMP, default=datetime.now, onupdate=datetime.now) updated_at = Column(TIMESTAMP, default=datetime.now, onupdate=datetime.now)

View File

@@ -1,21 +1,21 @@
"""租户工具配置模型""" """租户工具配置模型"""
from datetime import datetime from datetime import datetime
from sqlalchemy import Column, Integer, String, Text, SmallInteger, TIMESTAMP from sqlalchemy import Column, Integer, String, Text, SmallInteger, TIMESTAMP
from ..database import Base from ..database import Base
class ToolConfig(Base): class ToolConfig(Base):
"""租户工具配置表""" """租户工具配置表"""
__tablename__ = "platform_tool_configs" __tablename__ = "platform_tool_configs"
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
tenant_id = Column(String(50), nullable=False, comment="租户ID") tenant_id = Column(String(50), nullable=False, comment="租户ID")
tool_code = Column(String(50), nullable=True, comment="工具代码NULL 表示租户级共享配置)") tool_code = Column(String(50), nullable=True, comment="工具代码NULL 表示租户级共享配置)")
config_type = Column(String(30), nullable=False, comment="配置类型datasource / jssdk / webhook / params") config_type = Column(String(30), nullable=False, comment="配置类型datasource / jssdk / webhook / params")
config_key = Column(String(100), nullable=False, comment="配置键名") config_key = Column(String(100), nullable=False, comment="配置键名")
config_value = Column(Text, comment="配置值(明文或加密)") config_value = Column(Text, comment="配置值(明文或加密)")
is_encrypted = Column(SmallInteger, default=0, comment="是否加密存储") is_encrypted = Column(SmallInteger, default=0, comment="是否加密存储")
description = Column(String(255), comment="配置说明") description = Column(String(255), comment="配置说明")
status = Column(SmallInteger, default=1) status = Column(SmallInteger, default=1)
created_at = Column(TIMESTAMP, default=datetime.now) created_at = Column(TIMESTAMP, default=datetime.now)
updated_at = Column(TIMESTAMP, default=datetime.now, onupdate=datetime.now) updated_at = Column(TIMESTAMP, default=datetime.now, onupdate=datetime.now)

View File

@@ -23,6 +23,18 @@ class ToolItem(BaseModel):
path: str 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): class AppCreate(BaseModel):
"""创建应用""" """创建应用"""
app_code: str app_code: str
@@ -30,6 +42,7 @@ class AppCreate(BaseModel):
base_url: Optional[str] = None base_url: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
tools: Optional[List[ToolItem]] = None tools: Optional[List[ToolItem]] = None
config_schema: Optional[List[ConfigSchemaItem]] = None
require_jssdk: bool = False require_jssdk: bool = False
@@ -39,6 +52,7 @@ class AppUpdate(BaseModel):
base_url: Optional[str] = None base_url: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
tools: Optional[List[ToolItem]] = None tools: Optional[List[ToolItem]] = None
config_schema: Optional[List[ConfigSchemaItem]] = None
require_jssdk: Optional[bool] = None require_jssdk: Optional[bool] = None
status: Optional[int] = None status: Optional[int] = None
@@ -119,6 +133,7 @@ async def create_app(
base_url=data.base_url, base_url=data.base_url,
description=data.description, description=data.description,
tools=json.dumps([t.model_dump() for t in data.tools], ensure_ascii=False) if data.tools else None, 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, require_jssdk=1 if data.require_jssdk else 0,
status=1 status=1
) )
@@ -150,6 +165,13 @@ async def update_app(
else: else:
update_data['tools'] = None 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 # 处理 require_jssdk
if 'require_jssdk' in update_data: if 'require_jssdk' in update_data:
update_data['require_jssdk'] = 1 if update_data['require_jssdk'] else 0 update_data['require_jssdk'] = 1 if update_data['require_jssdk'] else 0
@@ -259,6 +281,21 @@ async def get_app_tools(
return 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: def format_app(app: App) -> dict:
"""格式化应用数据""" """格式化应用数据"""
return { return {
@@ -268,6 +305,7 @@ def format_app(app: App) -> dict:
"base_url": app.base_url, "base_url": app.base_url,
"description": app.description, "description": app.description,
"tools": json.loads(app.tools) if app.tools else [], "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), "require_jssdk": bool(app.require_jssdk),
"status": app.status, "status": app.status,
"created_at": app.created_at, "created_at": app.created_at,

View File

@@ -1,235 +1,254 @@
"""租户应用配置路由""" """租户应用配置路由"""
import json import json
import secrets import secrets
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, List from typing import Optional, List
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..database import get_db from ..database import get_db
from ..models.tenant_app import TenantApp from ..models.tenant_app import TenantApp
from ..models.app import App from ..models.app import App
from .auth import get_current_user, require_operator from .auth import get_current_user, require_operator
from ..models.user import User from ..models.user import User
router = APIRouter(prefix="/tenant-apps", tags=["应用配置"]) router = APIRouter(prefix="/tenant-apps", tags=["应用配置"])
# Schemas # Schemas
class TenantAppCreate(BaseModel): class CustomConfigItem(BaseModel):
tenant_id: str """自定义配置项"""
app_code: str = "tools" key: str # 配置键
app_name: Optional[str] = None value: str # 配置值
wechat_app_id: Optional[int] = None # 关联的企微应用ID remark: Optional[str] = None # 备注说明
access_token: Optional[str] = None # 如果不传则自动生成
allowed_origins: Optional[List[str]] = None
allowed_tools: Optional[List[str]] = None class TenantAppCreate(BaseModel):
tenant_id: str
app_code: str = "tools"
class TenantAppUpdate(BaseModel): app_name: Optional[str] = None
app_name: Optional[str] = None wechat_app_id: Optional[int] = None # 关联的企微应用ID
wechat_app_id: Optional[int] = None # 关联的企微应用ID access_token: Optional[str] = None # 如果不传则自动生成
access_token: Optional[str] = None allowed_origins: Optional[List[str]] = None
allowed_origins: Optional[List[str]] = None allowed_tools: Optional[List[str]] = None
allowed_tools: Optional[List[str]] = None custom_configs: Optional[List[CustomConfigItem]] = None # 自定义配置
status: Optional[int] = None
class TenantAppUpdate(BaseModel):
# API Endpoints app_name: Optional[str] = None
wechat_app_id: Optional[int] = None # 关联的企微应用ID
@router.get("") access_token: Optional[str] = None
async def list_tenant_apps( allowed_origins: Optional[List[str]] = None
page: int = Query(1, ge=1), allowed_tools: Optional[List[str]] = None
size: int = Query(20, ge=1, le=1000), custom_configs: Optional[List[CustomConfigItem]] = None # 自定义配置
tenant_id: Optional[str] = None, status: Optional[int] = None
app_code: Optional[str] = None,
user: User = Depends(get_current_user),
db: Session = Depends(get_db) # API Endpoints
):
"""获取应用配置列表""" @router.get("")
query = db.query(TenantApp) async def list_tenant_apps(
page: int = Query(1, ge=1),
if tenant_id: size: int = Query(20, ge=1, le=1000),
query = query.filter(TenantApp.tenant_id == tenant_id) tenant_id: Optional[str] = None,
if app_code: app_code: Optional[str] = None,
query = query.filter(TenantApp.app_code == app_code) user: User = Depends(get_current_user),
db: Session = Depends(get_db)
total = query.count() ):
apps = query.order_by(TenantApp.id.desc()).offset((page - 1) * size).limit(size).all() """获取应用配置列表"""
query = db.query(TenantApp)
return {
"total": total, if tenant_id:
"page": page, query = query.filter(TenantApp.tenant_id == tenant_id)
"size": size, if app_code:
"items": [format_tenant_app(app, mask_secret=True, db=db) for app in apps] query = query.filter(TenantApp.app_code == app_code)
}
total = query.count()
apps = query.order_by(TenantApp.id.desc()).offset((page - 1) * size).limit(size).all()
@router.get("/{app_id}")
async def get_tenant_app( return {
app_id: int, "total": total,
user: User = Depends(get_current_user), "page": page,
db: Session = Depends(get_db) "size": size,
): "items": [format_tenant_app(app, mask_secret=True, db=db) for app in apps]
"""获取应用配置详情""" }
app = db.query(TenantApp).filter(TenantApp.id == app_id).first()
if not app:
raise HTTPException(status_code=404, detail="应用配置不存在") @router.get("/{app_id}")
async def get_tenant_app(
return format_tenant_app(app, mask_secret=True, db=db) app_id: int,
user: User = Depends(get_current_user),
db: Session = Depends(get_db)
@router.post("") ):
async def create_tenant_app( """获取应用配置详情"""
data: TenantAppCreate, app = db.query(TenantApp).filter(TenantApp.id == app_id).first()
user: User = Depends(require_operator), if not app:
db: Session = Depends(get_db) raise HTTPException(status_code=404, detail="应用配置不存在")
):
"""创建应用配置""" return format_tenant_app(app, mask_secret=True, db=db)
# 验证 app_code 是否存在于应用管理中
app_exists = db.query(App).filter(App.app_code == data.app_code, App.status == 1).first()
if not app_exists: @router.post("")
raise HTTPException(status_code=400, detail=f"应用 '{data.app_code}' 不存在,请先在应用管理中创建") async def create_tenant_app(
data: TenantAppCreate,
# 检查是否重复 user: User = Depends(require_operator),
exists = db.query(TenantApp).filter( db: Session = Depends(get_db)
TenantApp.tenant_id == data.tenant_id, ):
TenantApp.app_code == data.app_code """创建应用配置"""
).first() # 验证 app_code 是否存在于应用管理中
if exists: app_exists = db.query(App).filter(App.app_code == data.app_code, App.status == 1).first()
raise HTTPException(status_code=400, detail="该租户应用配置已存在") if not app_exists:
raise HTTPException(status_code=400, detail=f"应用 '{data.app_code}' 不存在,请先在应用管理中创建")
# 自动生成 access_token
access_token = data.access_token or secrets.token_hex(32) # 检查是否重复
exists = db.query(TenantApp).filter(
app = TenantApp( TenantApp.tenant_id == data.tenant_id,
tenant_id=data.tenant_id, TenantApp.app_code == data.app_code
app_code=data.app_code, ).first()
app_name=data.app_name, if exists:
wechat_app_id=data.wechat_app_id, raise HTTPException(status_code=400, detail="该租户应用配置已存在")
access_token=access_token,
allowed_origins=json.dumps(data.allowed_origins) if data.allowed_origins else None, # 自动生成 access_token
allowed_tools=json.dumps(data.allowed_tools) if data.allowed_tools else None, access_token = data.access_token or secrets.token_hex(32)
status=1
) app = TenantApp(
db.add(app) tenant_id=data.tenant_id,
db.commit() app_code=data.app_code,
db.refresh(app) app_name=data.app_name,
wechat_app_id=data.wechat_app_id,
return {"success": True, "id": app.id, "access_token": access_token} access_token=access_token,
allowed_origins=json.dumps(data.allowed_origins) if data.allowed_origins else None,
allowed_tools=json.dumps(data.allowed_tools) if data.allowed_tools else None,
@router.put("/{app_id}") custom_configs=json.dumps([c.model_dump() for c in data.custom_configs], ensure_ascii=False) if data.custom_configs else None,
async def update_tenant_app( status=1
app_id: int, )
data: TenantAppUpdate, db.add(app)
user: User = Depends(require_operator), db.commit()
db: Session = Depends(get_db) db.refresh(app)
):
"""更新应用配置""" return {"success": True, "id": app.id, "access_token": access_token}
app = db.query(TenantApp).filter(TenantApp.id == app_id).first()
if not app:
raise HTTPException(status_code=404, detail="应用配置不存在") @router.put("/{app_id}")
async def update_tenant_app(
update_data = data.model_dump(exclude_unset=True) app_id: int,
data: TenantAppUpdate,
# 处理 JSON 字段 user: User = Depends(require_operator),
if 'allowed_origins' in update_data: db: Session = Depends(get_db)
update_data['allowed_origins'] = json.dumps(update_data['allowed_origins']) if update_data['allowed_origins'] else None ):
if 'allowed_tools' in update_data: """更新应用配置"""
update_data['allowed_tools'] = json.dumps(update_data['allowed_tools']) if update_data['allowed_tools'] else None app = db.query(TenantApp).filter(TenantApp.id == app_id).first()
if not app:
for key, value in update_data.items(): raise HTTPException(status_code=404, detail="应用配置不存在")
setattr(app, key, value)
update_data = data.model_dump(exclude_unset=True)
db.commit()
return {"success": True} # 处理 JSON 字段
if 'allowed_origins' in update_data:
update_data['allowed_origins'] = json.dumps(update_data['allowed_origins']) if update_data['allowed_origins'] else None
@router.delete("/{app_id}") if 'allowed_tools' in update_data:
async def delete_tenant_app( update_data['allowed_tools'] = json.dumps(update_data['allowed_tools']) if update_data['allowed_tools'] else None
app_id: int, if 'custom_configs' in update_data:
user: User = Depends(require_operator), if update_data['custom_configs']:
db: Session = Depends(get_db) update_data['custom_configs'] = json.dumps(
): [c.model_dump() if hasattr(c, 'model_dump') else c for c in update_data['custom_configs']],
"""删除应用配置""" ensure_ascii=False
app = db.query(TenantApp).filter(TenantApp.id == app_id).first() )
if not app: else:
raise HTTPException(status_code=404, detail="应用配置不存在") update_data['custom_configs'] = None
db.delete(app) for key, value in update_data.items():
db.commit() setattr(app, key, value)
return {"success": True} db.commit()
return {"success": True}
@router.get("/{app_id}/token")
async def get_token( @router.delete("/{app_id}")
app_id: int, async def delete_tenant_app(
user: User = Depends(require_operator), app_id: int,
db: Session = Depends(get_db) user: User = Depends(require_operator),
): db: Session = Depends(get_db)
"""获取真实的 access_token仅管理员可用""" ):
app = db.query(TenantApp).filter(TenantApp.id == app_id).first() """删除应用配置"""
if not app: app = db.query(TenantApp).filter(TenantApp.id == app_id).first()
raise HTTPException(status_code=404, detail="应用配置不存在") if not app:
raise HTTPException(status_code=404, detail="应用配置不存在")
# 获取应用的 base_url
app_info = db.query(App).filter(App.app_code == app.app_code).first() db.delete(app)
base_url = app_info.base_url if app_info else "" db.commit()
return { return {"success": True}
"access_token": app.access_token,
"base_url": base_url
} @router.get("/{app_id}/token")
async def get_token(
app_id: int,
@router.post("/{app_id}/regenerate-token") user: User = Depends(require_operator),
async def regenerate_token( db: Session = Depends(get_db)
app_id: int, ):
user: User = Depends(require_operator), """获取真实的 access_token仅管理员可用"""
db: Session = Depends(get_db) app = db.query(TenantApp).filter(TenantApp.id == app_id).first()
): if not app:
"""重新生成 access_token""" raise HTTPException(status_code=404, detail="应用配置不存在")
app = db.query(TenantApp).filter(TenantApp.id == app_id).first()
if not app: # 获取应用的 base_url
raise HTTPException(status_code=404, detail="应用配置不存在") app_info = db.query(App).filter(App.app_code == app.app_code).first()
base_url = app_info.base_url if app_info else ""
new_token = secrets.token_hex(32)
app.access_token = new_token return {
db.commit() "access_token": app.access_token,
"base_url": base_url
return {"success": True, "access_token": new_token} }
def format_tenant_app(app: TenantApp, mask_secret: bool = True, db: Session = None) -> dict: @router.post("/{app_id}/regenerate-token")
"""格式化应用配置""" async def regenerate_token(
# 获取关联的企微应用信息 app_id: int,
wechat_app_info = None user: User = Depends(require_operator),
if app.wechat_app_id and db: db: Session = Depends(get_db)
from ..models.tenant_wechat_app import TenantWechatApp ):
wechat_app = db.query(TenantWechatApp).filter(TenantWechatApp.id == app.wechat_app_id).first() """重新生成 access_token"""
if wechat_app: app = db.query(TenantApp).filter(TenantApp.id == app_id).first()
wechat_app_info = { if not app:
"id": wechat_app.id, raise HTTPException(status_code=404, detail="应用配置不存在")
"name": wechat_app.name,
"corp_id": wechat_app.corp_id, new_token = secrets.token_hex(32)
"agent_id": wechat_app.agent_id app.access_token = new_token
} db.commit()
result = { return {"success": True, "access_token": new_token}
"id": app.id,
"tenant_id": app.tenant_id,
"app_code": app.app_code, def format_tenant_app(app: TenantApp, mask_secret: bool = True, db: Session = None) -> dict:
"app_name": app.app_name, """格式化应用配置"""
"wechat_app_id": app.wechat_app_id, # 获取关联的企微应用信息
"wechat_app": wechat_app_info, wechat_app_info = None
"access_token": "******" if mask_secret and app.access_token else app.access_token, if app.wechat_app_id and db:
"allowed_origins": json.loads(app.allowed_origins) if app.allowed_origins else [], from ..models.tenant_wechat_app import TenantWechatApp
"allowed_tools": json.loads(app.allowed_tools) if app.allowed_tools else [], wechat_app = db.query(TenantWechatApp).filter(TenantWechatApp.id == app.wechat_app_id).first()
"status": app.status, if wechat_app:
"created_at": app.created_at, wechat_app_info = {
"updated_at": app.updated_at "id": wechat_app.id,
} "name": wechat_app.name,
return result "corp_id": wechat_app.corp_id,
"agent_id": wechat_app.agent_id
}
result = {
"id": app.id,
"tenant_id": app.tenant_id,
"app_code": app.app_code,
"app_name": app.app_name,
"wechat_app_id": app.wechat_app_id,
"wechat_app": wechat_app_info,
"access_token": "******" if mask_secret and app.access_token else app.access_token,
"allowed_origins": json.loads(app.allowed_origins) if app.allowed_origins else [],
"allowed_tools": json.loads(app.allowed_tools) if app.allowed_tools else [],
"custom_configs": json.loads(app.custom_configs) if app.custom_configs else [],
"status": app.status,
"created_at": app.created_at,
"updated_at": app.updated_at
}
return result

View File

@@ -1,391 +1,391 @@
"""租户工具配置路由""" """租户工具配置路由"""
import json import json
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import or_ from sqlalchemy import or_
from ..database import get_db from ..database import get_db
from ..models.tool_config import ToolConfig from ..models.tool_config import ToolConfig
from .auth import get_current_user from .auth import get_current_user
from ..models.user import User from ..models.user import User
router = APIRouter(prefix="/tool-configs", tags=["工具配置"]) router = APIRouter(prefix="/tool-configs", tags=["工具配置"])
# ======================================== # ========================================
# Schemas # Schemas
# ======================================== # ========================================
class ToolConfigCreate(BaseModel): class ToolConfigCreate(BaseModel):
"""创建配置""" """创建配置"""
tenant_id: str tenant_id: str
tool_code: Optional[str] = None # NULL 表示租户级共享配置 tool_code: Optional[str] = None # NULL 表示租户级共享配置
config_type: str # datasource / jssdk / webhook / params config_type: str # datasource / jssdk / webhook / params
config_key: str config_key: str
config_value: Optional[str] = None config_value: Optional[str] = None
is_encrypted: int = 0 is_encrypted: int = 0
description: Optional[str] = None description: Optional[str] = None
class ToolConfigUpdate(BaseModel): class ToolConfigUpdate(BaseModel):
"""更新配置""" """更新配置"""
config_value: Optional[str] = None config_value: Optional[str] = None
is_encrypted: Optional[int] = None is_encrypted: Optional[int] = None
description: Optional[str] = None description: Optional[str] = None
status: Optional[int] = None status: Optional[int] = None
class ToolConfigBatchCreate(BaseModel): class ToolConfigBatchCreate(BaseModel):
"""批量创建配置""" """批量创建配置"""
tenant_id: str tenant_id: str
tool_code: Optional[str] = None tool_code: Optional[str] = None
configs: List[Dict[str, Any]] # [{config_type, config_key, config_value, description}] configs: List[Dict[str, Any]] # [{config_type, config_key, config_value, description}]
# ======================================== # ========================================
# 工具函数 # 工具函数
# ======================================== # ========================================
def format_config(config: ToolConfig, mask_secret: bool = True) -> dict: def format_config(config: ToolConfig, mask_secret: bool = True) -> dict:
"""格式化配置输出""" """格式化配置输出"""
value = config.config_value value = config.config_value
# 如果需要掩码且是加密字段 # 如果需要掩码且是加密字段
if mask_secret and config.is_encrypted and value: if mask_secret and config.is_encrypted and value:
value = value[:4] + "****" + value[-4:] if len(value) > 8 else "****" value = value[:4] + "****" + value[-4:] if len(value) > 8 else "****"
return { return {
"id": config.id, "id": config.id,
"tenant_id": config.tenant_id, "tenant_id": config.tenant_id,
"tool_code": config.tool_code, "tool_code": config.tool_code,
"config_type": config.config_type, "config_type": config.config_type,
"config_key": config.config_key, "config_key": config.config_key,
"config_value": value, "config_value": value,
"is_encrypted": config.is_encrypted, "is_encrypted": config.is_encrypted,
"description": config.description, "description": config.description,
"status": config.status, "status": config.status,
"created_at": config.created_at.isoformat() if config.created_at else None, "created_at": config.created_at.isoformat() if config.created_at else None,
"updated_at": config.updated_at.isoformat() if config.updated_at else None "updated_at": config.updated_at.isoformat() if config.updated_at else None
} }
# ======================================== # ========================================
# API Endpoints # API Endpoints
# ======================================== # ========================================
@router.get("") @router.get("")
async def list_tool_configs( async def list_tool_configs(
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
size: int = Query(20, ge=1, le=100), size: int = Query(20, ge=1, le=100),
tenant_id: Optional[str] = None, tenant_id: Optional[str] = None,
tool_code: Optional[str] = None, tool_code: Optional[str] = None,
config_type: Optional[str] = None, config_type: Optional[str] = None,
keyword: Optional[str] = None, keyword: Optional[str] = None,
user: User = Depends(get_current_user), user: User = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""获取配置列表""" """获取配置列表"""
query = db.query(ToolConfig).filter(ToolConfig.status == 1) query = db.query(ToolConfig).filter(ToolConfig.status == 1)
if tenant_id: if tenant_id:
query = query.filter(ToolConfig.tenant_id == tenant_id) query = query.filter(ToolConfig.tenant_id == tenant_id)
if tool_code: if tool_code:
if tool_code == "__shared__": if tool_code == "__shared__":
# 特殊标记:只查租户级共享配置 # 特殊标记:只查租户级共享配置
query = query.filter(ToolConfig.tool_code.is_(None)) query = query.filter(ToolConfig.tool_code.is_(None))
else: else:
query = query.filter(ToolConfig.tool_code == tool_code) query = query.filter(ToolConfig.tool_code == tool_code)
if config_type: if config_type:
query = query.filter(ToolConfig.config_type == config_type) query = query.filter(ToolConfig.config_type == config_type)
if keyword: if keyword:
query = query.filter( query = query.filter(
or_( or_(
ToolConfig.config_key.like(f"%{keyword}%"), ToolConfig.config_key.like(f"%{keyword}%"),
ToolConfig.description.like(f"%{keyword}%") ToolConfig.description.like(f"%{keyword}%")
) )
) )
total = query.count() total = query.count()
configs = query.order_by(ToolConfig.tool_code, ToolConfig.config_type, ToolConfig.config_key)\ configs = query.order_by(ToolConfig.tool_code, ToolConfig.config_type, ToolConfig.config_key)\
.offset((page - 1) * size).limit(size).all() .offset((page - 1) * size).limit(size).all()
return { return {
"total": total, "total": total,
"page": page, "page": page,
"size": size, "size": size,
"items": [format_config(c) for c in configs] "items": [format_config(c) for c in configs]
} }
@router.get("/tenant/{tenant_id}") @router.get("/tenant/{tenant_id}")
async def get_tenant_configs( async def get_tenant_configs(
tenant_id: str, tenant_id: str,
tool_code: Optional[str] = None, tool_code: Optional[str] = None,
user: User = Depends(get_current_user), user: User = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
获取租户完整配置(合并层级) 获取租户完整配置(合并层级)
返回结构: 返回结构:
{ {
"__shared__": {"config_key": "value", ...}, # 租户级共享配置 "__shared__": {"config_key": "value", ...}, # 租户级共享配置
"customer-profile": {"config_key": "value", ...}, # 工具级配置 "customer-profile": {"config_key": "value", ...}, # 工具级配置
... ...
} }
""" """
query = db.query(ToolConfig).filter( query = db.query(ToolConfig).filter(
ToolConfig.tenant_id == tenant_id, ToolConfig.tenant_id == tenant_id,
ToolConfig.status == 1 ToolConfig.status == 1
) )
if tool_code: if tool_code:
# 只查指定工具 + 共享配置 # 只查指定工具 + 共享配置
query = query.filter( query = query.filter(
or_( or_(
ToolConfig.tool_code == tool_code, ToolConfig.tool_code == tool_code,
ToolConfig.tool_code.is_(None) ToolConfig.tool_code.is_(None)
) )
) )
configs = query.all() configs = query.all()
result = {"__shared__": {}} result = {"__shared__": {}}
for config in configs: for config in configs:
tool = config.tool_code or "__shared__" tool = config.tool_code or "__shared__"
if tool not in result: if tool not in result:
result[tool] = {} result[tool] = {}
result[tool][config.config_key] = { result[tool][config.config_key] = {
"value": config.config_value, "value": config.config_value,
"type": config.config_type, "type": config.config_type,
"encrypted": config.is_encrypted == 1, "encrypted": config.is_encrypted == 1,
"description": config.description "description": config.description
} }
return result return result
@router.get("/merged/{tenant_id}/{tool_code}") @router.get("/merged/{tenant_id}/{tool_code}")
async def get_merged_config( async def get_merged_config(
tenant_id: str, tenant_id: str,
tool_code: str, tool_code: str,
user: User = Depends(get_current_user), user: User = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
""" """
获取合并后的工具配置(工具级 > 租户级 > 默认值) 获取合并后的工具配置(工具级 > 租户级 > 默认值)
返回扁平化的配置字典: 返回扁平化的配置字典:
{"config_key": "value", ...} {"config_key": "value", ...}
""" """
# 查询租户级配置 # 查询租户级配置
shared_configs = db.query(ToolConfig).filter( shared_configs = db.query(ToolConfig).filter(
ToolConfig.tenant_id == tenant_id, ToolConfig.tenant_id == tenant_id,
ToolConfig.tool_code.is_(None), ToolConfig.tool_code.is_(None),
ToolConfig.status == 1 ToolConfig.status == 1
).all() ).all()
# 查询工具级配置 # 查询工具级配置
tool_configs = db.query(ToolConfig).filter( tool_configs = db.query(ToolConfig).filter(
ToolConfig.tenant_id == tenant_id, ToolConfig.tenant_id == tenant_id,
ToolConfig.tool_code == tool_code, ToolConfig.tool_code == tool_code,
ToolConfig.status == 1 ToolConfig.status == 1
).all() ).all()
# 合并:工具级覆盖租户级 # 合并:工具级覆盖租户级
result = {} result = {}
for config in shared_configs: for config in shared_configs:
result[config.config_key] = config.config_value result[config.config_key] = config.config_value
for config in tool_configs: for config in tool_configs:
result[config.config_key] = config.config_value result[config.config_key] = config.config_value
return result return result
@router.get("/{config_id}") @router.get("/{config_id}")
async def get_tool_config( async def get_tool_config(
config_id: int, config_id: int,
user: User = Depends(get_current_user), user: User = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""获取配置详情""" """获取配置详情"""
config = db.query(ToolConfig).filter(ToolConfig.id == config_id).first() config = db.query(ToolConfig).filter(ToolConfig.id == config_id).first()
if not config: if not config:
raise HTTPException(status_code=404, detail="配置不存在") raise HTTPException(status_code=404, detail="配置不存在")
return format_config(config, mask_secret=False) return format_config(config, mask_secret=False)
@router.post("") @router.post("")
async def create_tool_config( async def create_tool_config(
data: ToolConfigCreate, data: ToolConfigCreate,
user: User = Depends(get_current_user), user: User = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""创建配置""" """创建配置"""
# 检查是否已存在 # 检查是否已存在
existing = db.query(ToolConfig).filter( existing = db.query(ToolConfig).filter(
ToolConfig.tenant_id == data.tenant_id, ToolConfig.tenant_id == data.tenant_id,
ToolConfig.tool_code == data.tool_code if data.tool_code else ToolConfig.tool_code.is_(None), ToolConfig.tool_code == data.tool_code if data.tool_code else ToolConfig.tool_code.is_(None),
ToolConfig.config_key == data.config_key ToolConfig.config_key == data.config_key
).first() ).first()
if existing: if existing:
raise HTTPException(status_code=400, detail="配置已存在") raise HTTPException(status_code=400, detail="配置已存在")
config = ToolConfig( config = ToolConfig(
tenant_id=data.tenant_id, tenant_id=data.tenant_id,
tool_code=data.tool_code, tool_code=data.tool_code,
config_type=data.config_type, config_type=data.config_type,
config_key=data.config_key, config_key=data.config_key,
config_value=data.config_value, config_value=data.config_value,
is_encrypted=data.is_encrypted, is_encrypted=data.is_encrypted,
description=data.description description=data.description
) )
db.add(config) db.add(config)
db.commit() db.commit()
db.refresh(config) db.refresh(config)
return format_config(config) return format_config(config)
@router.post("/batch") @router.post("/batch")
async def batch_create_configs( async def batch_create_configs(
data: ToolConfigBatchCreate, data: ToolConfigBatchCreate,
user: User = Depends(get_current_user), user: User = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""批量创建配置""" """批量创建配置"""
created = [] created = []
skipped = [] skipped = []
for item in data.configs: for item in data.configs:
config_key = item.get("config_key") config_key = item.get("config_key")
if not config_key: if not config_key:
continue continue
# 检查是否已存在 # 检查是否已存在
existing = db.query(ToolConfig).filter( existing = db.query(ToolConfig).filter(
ToolConfig.tenant_id == data.tenant_id, ToolConfig.tenant_id == data.tenant_id,
ToolConfig.tool_code == data.tool_code if data.tool_code else ToolConfig.tool_code.is_(None), ToolConfig.tool_code == data.tool_code if data.tool_code else ToolConfig.tool_code.is_(None),
ToolConfig.config_key == config_key ToolConfig.config_key == config_key
).first() ).first()
if existing: if existing:
skipped.append(config_key) skipped.append(config_key)
continue continue
config = ToolConfig( config = ToolConfig(
tenant_id=data.tenant_id, tenant_id=data.tenant_id,
tool_code=data.tool_code, tool_code=data.tool_code,
config_type=item.get("config_type", "params"), config_type=item.get("config_type", "params"),
config_key=config_key, config_key=config_key,
config_value=item.get("config_value"), config_value=item.get("config_value"),
is_encrypted=item.get("is_encrypted", 0), is_encrypted=item.get("is_encrypted", 0),
description=item.get("description") description=item.get("description")
) )
db.add(config) db.add(config)
created.append(config_key) created.append(config_key)
db.commit() db.commit()
return { return {
"created": created, "created": created,
"skipped": skipped, "skipped": skipped,
"message": f"成功创建 {len(created)} 条配置,跳过 {len(skipped)} 条已存在配置" "message": f"成功创建 {len(created)} 条配置,跳过 {len(skipped)} 条已存在配置"
} }
@router.put("/{config_id}") @router.put("/{config_id}")
async def update_tool_config( async def update_tool_config(
config_id: int, config_id: int,
data: ToolConfigUpdate, data: ToolConfigUpdate,
user: User = Depends(get_current_user), user: User = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""更新配置""" """更新配置"""
config = db.query(ToolConfig).filter(ToolConfig.id == config_id).first() config = db.query(ToolConfig).filter(ToolConfig.id == config_id).first()
if not config: if not config:
raise HTTPException(status_code=404, detail="配置不存在") raise HTTPException(status_code=404, detail="配置不存在")
if data.config_value is not None: if data.config_value is not None:
config.config_value = data.config_value config.config_value = data.config_value
if data.is_encrypted is not None: if data.is_encrypted is not None:
config.is_encrypted = data.is_encrypted config.is_encrypted = data.is_encrypted
if data.description is not None: if data.description is not None:
config.description = data.description config.description = data.description
if data.status is not None: if data.status is not None:
config.status = data.status config.status = data.status
db.commit() db.commit()
db.refresh(config) db.refresh(config)
return format_config(config) return format_config(config)
@router.delete("/{config_id}") @router.delete("/{config_id}")
async def delete_tool_config( async def delete_tool_config(
config_id: int, config_id: int,
user: User = Depends(get_current_user), user: User = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""删除配置(软删除)""" """删除配置(软删除)"""
config = db.query(ToolConfig).filter(ToolConfig.id == config_id).first() config = db.query(ToolConfig).filter(ToolConfig.id == config_id).first()
if not config: if not config:
raise HTTPException(status_code=404, detail="配置不存在") raise HTTPException(status_code=404, detail="配置不存在")
config.status = 0 config.status = 0
db.commit() db.commit()
return {"message": "删除成功"} return {"message": "删除成功"}
# ======================================== # ========================================
# 配置类型和键名定义(供前端使用) # 配置类型和键名定义(供前端使用)
# ======================================== # ========================================
@router.get("/schema/types") @router.get("/schema/types")
async def get_config_types(): async def get_config_types():
"""获取支持的配置类型""" """获取支持的配置类型"""
return { return {
"types": [ "types": [
{"code": "datasource", "name": "数据源配置", "description": "数据库连接等"}, {"code": "datasource", "name": "数据源配置", "description": "数据库连接等"},
{"code": "jssdk", "name": "JS-SDK 配置", "description": "企微侧边栏等"}, {"code": "jssdk", "name": "JS-SDK 配置", "description": "企微侧边栏等"},
{"code": "webhook", "name": "Webhook 配置", "description": "n8n 工作流地址等"}, {"code": "webhook", "name": "Webhook 配置", "description": "n8n 工作流地址等"},
{"code": "params", "name": "工具参数", "description": "各工具的自定义参数"} {"code": "params", "name": "工具参数", "description": "各工具的自定义参数"}
] ]
} }
@router.get("/schema/keys") @router.get("/schema/keys")
async def get_config_keys(): async def get_config_keys():
"""获取预定义的配置键(供前端下拉选择)""" """获取预定义的配置键(供前端下拉选择)"""
return { return {
"datasource": [ "datasource": [
{"key": "scrm_db_host", "name": "SCRM 数据库地址", "encrypted": False}, {"key": "scrm_db_host", "name": "SCRM 数据库地址", "encrypted": False},
{"key": "scrm_db_port", "name": "SCRM 数据库端口", "encrypted": False}, {"key": "scrm_db_port", "name": "SCRM 数据库端口", "encrypted": False},
{"key": "scrm_db_user", "name": "SCRM 数据库用户", "encrypted": False}, {"key": "scrm_db_user", "name": "SCRM 数据库用户", "encrypted": False},
{"key": "scrm_db_password", "name": "SCRM 数据库密码", "encrypted": True}, {"key": "scrm_db_password", "name": "SCRM 数据库密码", "encrypted": True},
{"key": "scrm_db_name", "name": "SCRM 数据库名", "encrypted": False} {"key": "scrm_db_name", "name": "SCRM 数据库名", "encrypted": False}
], ],
"jssdk": [ "jssdk": [
{"key": "corp_id", "name": "企业ID", "encrypted": False}, {"key": "corp_id", "name": "企业ID", "encrypted": False},
{"key": "agent_id", "name": "应用ID", "encrypted": False}, {"key": "agent_id", "name": "应用ID", "encrypted": False},
{"key": "secret", "name": "应用密钥", "encrypted": True} {"key": "secret", "name": "应用密钥", "encrypted": True}
], ],
"webhook": [ "webhook": [
{"key": "n8n_base_url", "name": "n8n 基础地址", "encrypted": False}, {"key": "n8n_base_url", "name": "n8n 基础地址", "encrypted": False},
{"key": "webhook_brainstorm", "name": "头脑风暴 Webhook", "encrypted": False}, {"key": "webhook_brainstorm", "name": "头脑风暴 Webhook", "encrypted": False},
{"key": "webhook_high_eq", "name": "高情商回复 Webhook", "encrypted": False}, {"key": "webhook_high_eq", "name": "高情商回复 Webhook", "encrypted": False},
{"key": "webhook_customer_profile", "name": "客户画像 Webhook", "encrypted": False}, {"key": "webhook_customer_profile", "name": "客户画像 Webhook", "encrypted": False},
{"key": "webhook_consultation", "name": "面诊方案 Webhook", "encrypted": False}, {"key": "webhook_consultation", "name": "面诊方案 Webhook", "encrypted": False},
{"key": "webhook_medical_compliance", "name": "医疗合规 Webhook", "encrypted": False} {"key": "webhook_medical_compliance", "name": "医疗合规 Webhook", "encrypted": False}
], ],
"params": [ "params": [
{"key": "default_data_tenant_id", "name": "默认数据租户ID", "encrypted": False}, {"key": "default_data_tenant_id", "name": "默认数据租户ID", "encrypted": False},
{"key": "enable_deep_thinking", "name": "启用深度思考", "encrypted": False}, {"key": "enable_deep_thinking", "name": "启用深度思考", "encrypted": False},
{"key": "max_history_rounds", "name": "最大历史轮数", "encrypted": False} {"key": "max_history_rounds", "name": "最大历史轮数", "encrypted": False}
] ]
} }

View File

@@ -1,6 +1,7 @@
<script setup> <script setup>
import { ref, reactive, onMounted, computed, watch } from 'vue' import { ref, reactive, onMounted, computed, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Plus } from '@element-plus/icons-vue'
import api from '@/api' import api from '@/api'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
@@ -23,6 +24,7 @@ const tenantList = ref([])
const appList = ref([]) const appList = ref([])
const appRequireJssdk = ref({}) // app_code -> require_jssdk const appRequireJssdk = ref({}) // app_code -> require_jssdk
const appBaseUrl = ref({}) // app_code -> base_url const appBaseUrl = ref({}) // app_code -> base_url
const appConfigSchema = ref({}) // app_code -> config_schema
// 企微应用列表(按租户) // 企微应用列表(按租户)
const wechatAppList = ref([]) const wechatAppList = ref([])
@@ -36,7 +38,8 @@ const form = reactive({
tenant_id: '', tenant_id: '',
app_code: '', app_code: '',
app_name: '', app_name: '',
wechat_app_id: null wechat_app_id: null,
custom_configs: [] // 自定义配置 [{key, value, remark}]
}) })
// 当前选择的应用是否需要 JS-SDK // 当前选择的应用是否需要 JS-SDK
@@ -44,6 +47,44 @@ const currentAppRequireJssdk = computed(() => {
return appRequireJssdk.value[form.app_code] || false 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 必须是有效的应用 // 验证 app_code 必须是有效的应用
const validateAppCode = (rule, value, callback) => { const validateAppCode = (rule, value, callback) => {
if (!value) { if (!value) {
@@ -70,6 +111,14 @@ watch(() => form.tenant_id, async (newVal) => {
form.wechat_app_id = null form.wechat_app_id = null
}) })
// 监听应用选择变化,初始化配置默认值
watch(() => form.app_code, (newVal) => {
if (newVal && !editingId.value) {
// 新建时,根据 schema 初始化默认值
initConfigDefaults()
}
})
// 查看 Token 对话框 // 查看 Token 对话框
const tokenDialogVisible = ref(false) const tokenDialogVisible = ref(false)
const currentToken = ref('') const currentToken = ref('')
@@ -93,12 +142,25 @@ async function fetchApps() {
for (const app of apps) { for (const app of apps) {
appRequireJssdk.value[app.app_code] = app.require_jssdk || false appRequireJssdk.value[app.app_code] = app.require_jssdk || false
appBaseUrl.value[app.app_code] = app.base_url || '' appBaseUrl.value[app.app_code] = app.base_url || ''
appConfigSchema.value[app.app_code] = app.config_schema || []
} }
} catch (e) { } catch (e) {
console.error('获取应用列表失败:', 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) { async function fetchWechatApps(tenantId) {
if (!tenantId) { if (!tenantId) {
wechatAppList.value = [] wechatAppList.value = []
@@ -142,7 +204,8 @@ function handleCreate() {
tenant_id: '', tenant_id: '',
app_code: '', app_code: '',
app_name: '', app_name: '',
wechat_app_id: null wechat_app_id: null,
custom_configs: []
}) })
wechatAppList.value = [] wechatAppList.value = []
dialogVisible.value = true dialogVisible.value = true
@@ -151,16 +214,49 @@ function handleCreate() {
async function handleEdit(row) { async function handleEdit(row) {
editingId.value = row.id editingId.value = row.id
dialogTitle.value = '编辑应用订阅' 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, { Object.assign(form, {
tenant_id: row.tenant_id, tenant_id: row.tenant_id,
app_code: row.app_code, app_code: row.app_code,
app_name: row.app_name || '', app_name: row.app_name || '',
wechat_app_id: row.wechat_app_id || null wechat_app_id: row.wechat_app_id || null,
custom_configs: mergedConfigs
}) })
await fetchWechatApps(row.tenant_id) await fetchWechatApps(row.tenant_id)
dialogVisible.value = true dialogVisible.value = true
} }
// 自定义配置管理(用于没有 schema 定义时的手动添加)
function addCustomConfig() {
form.custom_configs.push({ key: '', value: '', remark: '' })
}
function removeCustomConfig(index) {
form.custom_configs.splice(index, 1)
}
async function handleSubmit() { async function handleSubmit() {
await formRef.value.validate() await formRef.value.validate()
@@ -301,6 +397,14 @@ onMounted(() => {
<el-tag v-else type="danger" size="small">未生成</el-tag> <el-tag v-else type="danger" size="small">未生成</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="自定义配置" width="100">
<template #default="{ row }">
<el-tag v-if="row.custom_configs && row.custom_configs.length > 0" type="primary" size="small">
{{ row.custom_configs.length }}
</el-tag>
<span v-else style="color: #909399; font-size: 12px">-</span>
</template>
</el-table-column>
<el-table-column label="状态" width="80"> <el-table-column label="状态" width="80">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small"> <el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
@@ -330,7 +434,7 @@ onMounted(() => {
</div> </div>
<!-- 编辑对话框 --> <!-- 编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="550px"> <el-dialog v-model="dialogVisible" :title="dialogTitle" width="750px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px"> <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="租户" prop="tenant_id"> <el-form-item label="租户" prop="tenant_id">
<el-select <el-select
@@ -379,6 +483,119 @@ onMounted(() => {
</div> </div>
</el-form-item> </el-form-item>
</template> </template>
<!-- 自定义配置 -->
<template v-if="currentConfigSchema.length > 0 || form.custom_configs.length > 0">
<el-divider content-position="left">应用配置</el-divider>
<div class="custom-configs-section">
<!-- 根据 schema 渲染表单 -->
<template v-for="(schema, index) in currentConfigSchema" :key="schema.key">
<div class="config-item-schema">
<div class="config-label">
{{ schema.label }}
<span v-if="schema.required" class="required-star">*</span>
</div>
<!-- text 类型 -->
<template v-if="schema.type === 'text'">
<el-input
v-model="form.custom_configs[index].value"
type="textarea"
:rows="2"
:autosize="{ minRows: 2, maxRows: 12 }"
:placeholder="schema.placeholder || '请输入'"
/>
</template>
<!-- radio 类型 -->
<template v-else-if="schema.type === 'radio'">
<el-radio-group v-model="form.custom_configs[index].value">
<el-radio
v-for="opt in schema.options"
:key="opt"
:value="opt"
>
{{ getOptionLabel(schema, opt) }}
</el-radio>
</el-radio-group>
</template>
<!-- select 类型 -->
<template v-else-if="schema.type === 'select'">
<el-select v-model="form.custom_configs[index].value" placeholder="请选择" style="width: 100%">
<el-option
v-for="opt in schema.options"
:key="opt"
:label="getOptionLabel(schema, opt)"
:value="opt"
/>
</el-select>
</template>
<!-- switch 类型 -->
<template v-else-if="schema.type === 'switch'">
<el-switch
v-model="form.custom_configs[index].value"
active-value="true"
inactive-value="false"
/>
</template>
<!-- 备注输入 -->
<el-input
v-model="form.custom_configs[index].remark"
placeholder="备注(可选)"
style="margin-top: 8px; width: 300px"
size="small"
/>
</div>
</template>
<!-- 没有 schema 定义时显示手动配置 -->
<template v-if="currentConfigSchema.length === 0">
<div v-for="(config, index) in form.custom_configs" :key="index" class="config-item">
<div class="config-row">
<el-input
v-model="config.key"
placeholder="配置键 (如: industry)"
style="width: 150px"
/>
<el-input
v-model="config.remark"
placeholder="备注说明"
style="width: 180px; margin-left: 8px"
/>
<el-button
type="danger"
:icon="Delete"
circle
size="small"
style="margin-left: 8px"
@click="removeCustomConfig(index)"
/>
</div>
<el-input
v-model="config.value"
type="textarea"
:rows="3"
:autosize="{ minRows: 2, maxRows: 10 }"
placeholder="配置值(支持超长文本,如提示词等)"
style="margin-top: 8px"
/>
</div>
<el-button type="primary" plain @click="addCustomConfig" style="margin-top: 8px">
<el-icon><Plus /></el-icon>
添加配置项
</el-button>
<div v-if="form.custom_configs.length === 0" class="config-empty-tip">
该应用暂无配置项定义
</div>
</template>
</div>
</template>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="dialogVisible = false">取消</el-button> <el-button @click="dialogVisible = false">取消</el-button>
@@ -449,4 +666,47 @@ onMounted(() => {
margin-bottom: 8px; margin-bottom: 8px;
color: #303133; color: #303133;
} }
/* 自定义配置样式 */
.custom-configs-section {
padding: 0 10px;
}
.config-item {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.config-item-schema {
background: #f5f7fa;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
}
.config-label {
font-weight: 500;
color: #303133;
margin-bottom: 10px;
font-size: 14px;
}
.required-star {
color: #f56c6c;
margin-left: 4px;
}
.config-row {
display: flex;
align-items: center;
}
.config-empty-tip {
color: #909399;
font-size: 13px;
text-align: center;
padding: 20px 0;
}
</style> </style>

View File

@@ -1,6 +1,7 @@
<script setup> <script setup>
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Plus } from '@element-plus/icons-vue'
import api from '@/api' import api from '@/api'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
@@ -14,6 +15,14 @@ const query = reactive({
size: 20 size: 20
}) })
// 配置项类型选项
const configTypes = [
{ value: 'text', label: '文本输入' },
{ value: 'radio', label: '单选' },
{ value: 'select', label: '下拉选择' },
{ value: 'switch', label: '开关' }
]
// 对话框 // 对话框
const dialogVisible = ref(false) const dialogVisible = ref(false)
const dialogTitle = ref('') const dialogTitle = ref('')
@@ -24,7 +33,8 @@ const form = reactive({
app_name: '', app_name: '',
base_url: '', base_url: '',
description: '', description: '',
require_jssdk: false require_jssdk: false,
config_schema: [] // 配置项定义
}) })
const rules = { const rules = {
@@ -63,7 +73,8 @@ function handleCreate() {
app_name: '', app_name: '',
base_url: '', base_url: '',
description: '', description: '',
require_jssdk: false require_jssdk: false,
config_schema: []
}) })
dialogVisible.value = true dialogVisible.value = true
} }
@@ -76,11 +87,55 @@ function handleEdit(row) {
app_name: row.app_name, app_name: row.app_name,
base_url: row.base_url || '', base_url: row.base_url || '',
description: row.description || '', description: row.description || '',
require_jssdk: row.require_jssdk || false require_jssdk: row.require_jssdk || false,
config_schema: row.config_schema ? row.config_schema.map(c => ({
...c,
options: c.options || [],
option_labels: c.option_labels || {}
})) : []
}) })
dialogVisible.value = true dialogVisible.value = true
} }
// 配置项管理
function addConfigItem() {
form.config_schema.push({
key: '',
label: '',
type: 'text',
options: [],
option_labels: {},
default: '',
placeholder: '',
required: false
})
}
function removeConfigItem(index) {
form.config_schema.splice(index, 1)
}
// 选项管理radio/select 类型)
function addOption(config) {
const optionKey = `option_${config.options.length + 1}`
config.options.push(optionKey)
config.option_labels[optionKey] = ''
}
function removeOption(config, index) {
const optionKey = config.options[index]
config.options.splice(index, 1)
delete config.option_labels[optionKey]
}
function updateOptionKey(config, index, newKey) {
const oldKey = config.options[index]
const oldLabel = config.option_labels[oldKey]
delete config.option_labels[oldKey]
config.options[index] = newKey
config.option_labels[newKey] = oldLabel || ''
}
async function handleSubmit() { async function handleSubmit() {
await formRef.value.validate() await formRef.value.validate()
@@ -154,6 +209,14 @@ onMounted(() => {
<el-table-column prop="app_code" label="应用代码" width="150" /> <el-table-column prop="app_code" label="应用代码" width="150" />
<el-table-column prop="app_name" label="应用名称" width="180" /> <el-table-column prop="app_name" label="应用名称" width="180" />
<el-table-column prop="base_url" label="访问地址" min-width="300" show-overflow-tooltip /> <el-table-column prop="base_url" label="访问地址" min-width="300" show-overflow-tooltip />
<el-table-column label="配置项" width="90">
<template #default="{ row }">
<el-tag v-if="row.config_schema && row.config_schema.length > 0" type="primary" size="small">
{{ row.config_schema.length }}
</el-tag>
<span v-else style="color: #909399; font-size: 12px">-</span>
</template>
</el-table-column>
<el-table-column label="JS-SDK" width="90"> <el-table-column label="JS-SDK" width="90">
<template #default="{ row }"> <template #default="{ row }">
<el-tag :type="row.require_jssdk ? 'warning' : 'info'" size="small"> <el-tag :type="row.require_jssdk ? 'warning' : 'info'" size="small">
@@ -191,7 +254,7 @@ onMounted(() => {
</div> </div>
<!-- 编辑对话框 --> <!-- 编辑对话框 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="550px"> <el-dialog v-model="dialogVisible" :title="dialogTitle" width="800px">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px"> <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="应用代码" prop="app_code"> <el-form-item label="应用代码" prop="app_code">
<el-input v-model="form.app_code" :disabled="!!editingId" placeholder="唯一标识,如: brainstorm" /> <el-input v-model="form.app_code" :disabled="!!editingId" placeholder="唯一标识,如: brainstorm" />
@@ -212,6 +275,69 @@ onMounted(() => {
<el-switch v-model="form.require_jssdk" /> <el-switch v-model="form.require_jssdk" />
<span style="margin-left: 12px; color: #909399; font-size: 12px">开启后租户需关联企微应用</span> <span style="margin-left: 12px; color: #909399; font-size: 12px">开启后租户需关联企微应用</span>
</el-form-item> </el-form-item>
<!-- 配置项定义 -->
<el-divider content-position="left">配置项定义</el-divider>
<div class="config-schema-section">
<div class="config-schema-tip">
定义租户订阅时可配置的参数如行业类型提示词等
</div>
<div v-for="(config, index) in form.config_schema" :key="index" class="config-schema-item">
<div class="config-header">
<span class="config-index">#{{ index + 1 }}</span>
<el-button type="danger" :icon="Delete" circle size="small" @click="removeConfigItem(index)" />
</div>
<div class="config-row">
<el-input v-model="config.key" placeholder="配置键(如:industry)" style="width: 140px" />
<el-input v-model="config.label" placeholder="显示标签(如:行业类型)" style="width: 160px" />
<el-select v-model="config.type" placeholder="类型" style="width: 120px">
<el-option v-for="t in configTypes" :key="t.value" :label="t.label" :value="t.value" />
</el-select>
<el-checkbox v-model="config.required">必填</el-checkbox>
</div>
<!-- text 类型显示 placeholder -->
<div v-if="config.type === 'text'" class="config-row" style="margin-top: 8px">
<el-input v-model="config.placeholder" placeholder="输入提示文字" style="width: 300px" />
<el-input v-model="config.default" placeholder="默认值" style="width: 200px" />
</div>
<!-- switch 类型显示默认值 -->
<div v-if="config.type === 'switch'" class="config-row" style="margin-top: 8px">
<span style="color: #606266; margin-right: 8px">默认值</span>
<el-switch v-model="config.default" active-value="true" inactive-value="false" />
</div>
<!-- radio/select 类型显示选项编辑 -->
<div v-if="config.type === 'radio' || config.type === 'select'" class="config-options">
<div class="options-label">选项列表</div>
<div v-for="(opt, optIndex) in config.options" :key="optIndex" class="option-row">
<el-input
:model-value="opt"
@update:model-value="v => updateOptionKey(config, optIndex, v)"
placeholder="选项值(:medical)"
style="width: 140px"
/>
<el-input
v-model="config.option_labels[opt]"
placeholder="显示名(:医美)"
style="width: 140px"
/>
<el-radio v-model="config.default" :value="opt">默认</el-radio>
<el-button type="danger" :icon="Delete" circle size="small" @click="removeOption(config, optIndex)" />
</div>
<el-button type="primary" plain size="small" @click="addOption(config)">
<el-icon><Plus /></el-icon> 添加选项
</el-button>
</div>
</div>
<el-button type="primary" plain @click="addConfigItem" style="margin-top: 12px">
<el-icon><Plus /></el-icon> 添加配置项
</el-button>
</div>
</el-form> </el-form>
<template #footer> <template #footer>
<el-button @click="dialogVisible = false">取消</el-button> <el-button @click="dialogVisible = false">取消</el-button>
@@ -225,4 +351,61 @@ onMounted(() => {
.page-tip { .page-tip {
margin-bottom: 16px; margin-bottom: 16px;
} }
/* 配置项定义样式 */
.config-schema-section {
padding: 0 10px;
}
.config-schema-tip {
color: #909399;
font-size: 13px;
margin-bottom: 12px;
}
.config-schema-item {
background: #f5f7fa;
border-radius: 8px;
padding: 12px;
margin-bottom: 12px;
}
.config-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.config-index {
font-weight: 600;
color: #409eff;
}
.config-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.config-options {
margin-top: 10px;
padding: 10px;
background: #fff;
border-radius: 6px;
}
.options-label {
font-size: 13px;
color: #606266;
margin-bottom: 8px;
}
.option-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
</style> </style>

File diff suppressed because it is too large Load Diff