diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5788d6b --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# CRUD auto-generated directories (regenerated via xls2crud) +wwwroot/cms_content_list/ +wwwroot/cms_sections_list/ +wwwroot/cms_categories_list/ +wwwroot/cms_leads_list/ +wwwroot/cms_site_config_list/ + +# Python +__pycache__/ +*.pyc +*.pyo + +# Virtual environment +py3/ + +# Logs +logs/*.log + +# PID file +portal.pid + +# IDE +.idea/ +.vscode/ +*.swp diff --git a/json/cms_categories_list.json b/json/cms_categories_list.json new file mode 100644 index 0000000..33810cf --- /dev/null +++ b/json/cms_categories_list.json @@ -0,0 +1,29 @@ +{ + "tblname": "cms_categories", + "alias": "cms_categories_list", + "title": "内容分类", + "uitype": "tree", + "params": { + "idField": "id", + "textField": "name", + "parentField": "parent_id", + "sortby": ["sort_order asc", "created_at desc"], + "logined_userorgid": "org_id", + "browserfields": { + "exclouded": ["display_config"], + "alters": { + "content_type": { + "uitype": "code", + "dataurl": "{{entire_url('../api/get_search_content_type.dspy')}}", + "datamethod": "GET" + } + } + }, + "editexclouded": ["id", "org_id", "created_at"], + "editable": { + "new_data_url": "{{entire_url('../api/cms_categories_create.dspy')}}", + "update_data_url": "{{entire_url('../api/cms_categories_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/cms_categories_delete.dspy')}}" + } + } +} diff --git a/json/cms_content_list.json b/json/cms_content_list.json new file mode 100644 index 0000000..9b7e1a4 --- /dev/null +++ b/json/cms_content_list.json @@ -0,0 +1,38 @@ +{ + "tblname": "cms_content", + "alias": "cms_content_list", + "title": "内容管理", + "params": { + "sortby": ["sort_order asc", "created_at desc"], + "logined_userorgid": "org_id", + "browserfields": { + "exclouded": ["body", "extra_json", "approval_id", "created_by"], + "alters": { + "status": { + "uitype": "code", + "data": [ + {"value": "draft", "text": "草稿"}, + {"value": "published", "text": "已发布"}, + {"value": "archived", "text": "已归档"} + ] + }, + "content_type": { + "uitype": "code", + "dataurl": "{{entire_url('../api/get_search_content_type.dspy')}}", + "datamethod": "GET" + }, + "category_id": { + "uitype": "code", + "dataurl": "{{entire_url('../api/get_search_cms_categories.dspy')}}", + "datamethod": "GET" + } + } + }, + "editexclouded": ["id", "org_id", "created_by", "created_at", "updated_at"], + "editable": { + "new_data_url": "{{entire_url('../api/cms_content_create.dspy')}}", + "update_data_url": "{{entire_url('../api/cms_content_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/cms_content_delete.dspy')}}" + } + } +} diff --git a/json/cms_leads_list.json b/json/cms_leads_list.json new file mode 100644 index 0000000..6e9121d --- /dev/null +++ b/json/cms_leads_list.json @@ -0,0 +1,39 @@ +{ + "tblname": "cms_leads", + "alias": "cms_leads_list", + "title": "商机线索", + "params": { + "sortby": ["created_at desc"], + "logined_userorgid": "org_id", + "browserfields": { + "exclouded": ["raw_text"], + "alters": { + "status": { + "uitype": "code", + "data": [ + {"value": "new", "text": "新线索"}, + {"value": "contacting", "text": "联系中"}, + {"value": "qualified", "text": "已确认"}, + {"value": "converted", "text": "已转化"}, + {"value": "closed", "text": "已关闭"} + ] + }, + "source": { + "uitype": "code", + "data": [ + {"value": "website", "text": "官网"}, + {"value": "form", "text": "表单"}, + {"value": "ai", "text": "AI抽取"}, + {"value": "other", "text": "其他"} + ] + } + } + }, + "editexclouded": ["id", "org_id", "created_at", "updated_at"], + "editable": { + "new_data_url": "{{entire_url('../api/cms_leads_create.dspy')}}", + "update_data_url": "{{entire_url('../api/cms_leads_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/cms_leads_delete.dspy')}}" + } + } +} diff --git a/json/cms_sections_list.json b/json/cms_sections_list.json new file mode 100644 index 0000000..a9d4af9 --- /dev/null +++ b/json/cms_sections_list.json @@ -0,0 +1,32 @@ +{ + "tblname": "cms_sections", + "alias": "cms_sections_list", + "title": "栏目管理", + "params": { + "sortby": ["sort_order asc", "created_at desc"], + "logined_userorgid": "org_id", + "browserfields": { + "exclouded": ["static_content", "style_config"], + "alters": { + "is_visible": { + "uitype": "code", + "data": [ + {"value": "1", "text": "显示"}, + {"value": "0", "text": "隐藏"} + ] + }, + "content_type": { + "uitype": "code", + "dataurl": "{{entire_url('../api/get_search_content_type.dspy')}}", + "datamethod": "GET" + } + } + }, + "editexclouded": ["id", "org_id", "created_at", "updated_at"], + "editable": { + "new_data_url": "{{entire_url('../api/cms_sections_create.dspy')}}", + "update_data_url": "{{entire_url('../api/cms_sections_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/cms_sections_delete.dspy')}}" + } + } +} diff --git a/json/cms_site_config_list.json b/json/cms_site_config_list.json new file mode 100644 index 0000000..155a5cd --- /dev/null +++ b/json/cms_site_config_list.json @@ -0,0 +1,30 @@ +{ + "tblname": "cms_site_config", + "alias": "cms_site_config_list", + "title": "站点配置", + "params": { + "sortby": ["config_group asc", "sort_order asc"], + "logined_userorgid": "org_id", + "browserfields": { + "exclouded": [], + "alters": { + "config_type": { + "uitype": "code", + "data": [ + {"value": "text", "text": "文本"}, + {"value": "number", "text": "数字"}, + {"value": "json", "text": "JSON"}, + {"value": "bool", "text": "布尔"}, + {"value": "image", "text": "图片URL"} + ] + } + } + }, + "editexclouded": ["id", "org_id", "updated_at"], + "editable": { + "new_data_url": "{{entire_url('../api/cms_site_config_create.dspy')}}", + "update_data_url": "{{entire_url('../api/cms_site_config_update.dspy')}}", + "delete_data_url": "{{entire_url('../api/cms_site_config_delete.dspy')}}" + } + } +} diff --git a/load_path.py b/load_path.py new file mode 100644 index 0000000..53d2e70 --- /dev/null +++ b/load_path.py @@ -0,0 +1,138 @@ +#!/usr/bin/env python3 +""" +Portal CMS CRUD RBAC 权限注册脚本 + +注册CMS管理后台的所有CRUD路径权限: +- superuser (owner.superuser): CMS管理页面和API +- any: 公开API(搜索下拉等) + +使用方法: + cd ~/repos/sage + ./py3/bin/python ~/repos/portal/load_path.py +""" + +import subprocess +import os +import sys + + +def find_sage_root(): + candidates = [ + os.path.expanduser("~/repos/sage"), + os.path.expanduser("~/sage"), + ] + for c in candidates: + if os.path.isdir(os.path.join(c, "py3")) and os.path.isdir(os.path.join(c, "wwwroot")): + return c + return None + + +SAGE_ROOT = find_sage_root() +if not SAGE_ROOT: + print("ERROR: Cannot find Sage root directory") + sys.exit(1) + +PYTHON = os.path.join(SAGE_ROOT, "py3", "bin", "python") +SET_PERM_SCRIPT = os.path.join(SAGE_ROOT, "set_role_perm.py") + +# ============================================================ +# 权限路径定义 +# ============================================================ + +# any — 无需登录(公开API: 搜索下拉、内容类型列表) +PATHS_ANY = [ + "/api/get_search_cms_categories.dspy", + "/api/get_search_content_type.dspy", +] + +# owner.superuser — CMS管理CRUD页面和API +PATHS_SUPERUSER = [ + # CMS Content CRUD + "/cms_content_list", "/cms_content_list/%", + "/api/cms_content_create.dspy", + "/api/cms_content_update.dspy", + "/api/cms_content_delete.dspy", + "/api/cms_content_list.dspy", + "/cms_content_list/get_cms_content.dspy", + "/cms_content_list/add_cms_content.dspy", + "/cms_content_list/update_cms_content.dspy", + "/cms_content_list/delete_cms_content.dspy", + + # CMS Sections CRUD + "/cms_sections_list", "/cms_sections_list/%", + "/api/cms_sections_create.dspy", + "/api/cms_sections_update.dspy", + "/api/cms_sections_delete.dspy", + "/api/cms_sections_list.dspy", + "/cms_sections_list/get_cms_sections.dspy", + "/cms_sections_list/add_cms_sections.dspy", + "/cms_sections_list/update_cms_sections.dspy", + "/cms_sections_list/delete_cms_sections.dspy", + + # CMS Categories CRUD + "/cms_categories_list", "/cms_categories_list/%", + "/api/cms_categories_create.dspy", + "/api/cms_categories_update.dspy", + "/api/cms_categories_delete.dspy", + "/api/cms_categories_list.dspy", + "/cms_categories_list/get_cms_categories.dspy", + "/cms_categories_list/add_cms_categories.dspy", + "/cms_categories_list/update_cms_categories.dspy", + "/cms_categories_list/delete_cms_categories.dspy", + + # CMS Leads CRUD + "/cms_leads_list", "/cms_leads_list/%", + "/api/cms_leads_create.dspy", + "/api/cms_leads_update.dspy", + "/api/cms_leads_delete.dspy", + "/api/cms_leads_list.dspy", + "/cms_leads_list/get_cms_leads.dspy", + "/cms_leads_list/add_cms_leads.dspy", + "/cms_leads_list/update_cms_leads.dspy", + "/cms_leads_list/delete_cms_leads.dspy", + + # CMS Site Config CRUD + "/cms_site_config_list", "/cms_site_config_list/%", + "/api/cms_site_config_create.dspy", + "/api/cms_site_config_update.dspy", + "/api/cms_site_config_delete.dspy", + "/api/cms_site_config_list.dspy", + "/cms_site_config_list/get_cms_site_config.dspy", + "/cms_site_config_list/add_cms_site_config.dspy", + "/cms_site_config_list/update_cms_site_config.dspy", + "/cms_site_config_list/delete_cms_site_config.dspy", +] + +# ============================================================ +# 执行注册 +# ============================================================ + +def run_set_perm(role, path): + env = os.environ.copy() + env['SAGE_RBAC_DB'] = 'ocai_cms' + cmd = [PYTHON, SET_PERM_SCRIPT, role, path] + result = subprocess.run(cmd, capture_output=True, text=True, env=env) + return result.returncode == 0 + + +def register_role_paths(role, paths): + count = 0 + for p in paths: + if run_set_perm(role, p): + count += 1 + print(f" {role}: {count}/{len(paths)} paths registered") + return count + + +def main(): + print(f"Sage root: {SAGE_ROOT}") + print(f"RBAC DB: ocai_cms") + total = 0 + total += register_role_paths("any", PATHS_ANY) + total += register_role_paths("owner.superuser", PATHS_SUPERUSER) + print(f"\nDone. Total {total} permission entries registered.") + print("NOTE: Restart Sage after permission changes to reload RBAC cache.") + + +if __name__ == "__main__": + main() diff --git a/models/cms_categories.json b/models/cms_categories.json new file mode 100644 index 0000000..f7f2b80 --- /dev/null +++ b/models/cms_categories.json @@ -0,0 +1,25 @@ +{ + "summary": [ + { + "name": "cms_categories", + "title": "CMS分类表", + "primary": ["id"] + } + ], + "fields": [ + {"name": "id", "title": "ID", "type": "str", "length": 32, "nullable": "no", "comments": "主键ID"}, + {"name": "org_id", "title": "组织ID", "type": "str", "length": 32, "nullable": "no", "comments": "所属组织"}, + {"name": "name", "title": "分类名称", "type": "str", "length": 100, "nullable": "no", "comments": "分类名称"}, + {"name": "parent_id", "title": "父分类ID", "type": "str", "length": 32, "nullable": "yes", "comments": "父分类ID,空表示顶级"}, + {"name": "content_type", "title": "内容类型", "type": "str", "length": 32, "nullable": "yes", "comments": "关联内容类型"}, + {"name": "description", "title": "描述", "type": "str", "length": 500, "nullable": "yes", "comments": "分类描述"}, + {"name": "sort_order", "title": "排序", "type": "int", "nullable": "yes", "default": "0", "comments": "排序序号"}, + {"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no", "comments": "创建时间"}, + {"name": "display_config", "title": "展示配置", "type": "text", "nullable": "yes", "comments": "展示配置JSON"} + ], + "indexes": [ + {"name": "idx_categories_org", "idxtype": "index", "idxfields": ["org_id"]}, + {"name": "idx_categories_parent", "idxtype": "index", "idxfields": ["parent_id"]}, + {"name": "idx_categories_type", "idxtype": "index", "idxfields": ["content_type"]} + ] +} diff --git a/models/cms_content.json b/models/cms_content.json new file mode 100644 index 0000000..3aeb1c8 --- /dev/null +++ b/models/cms_content.json @@ -0,0 +1,35 @@ +{ + "summary": [ + { + "name": "cms_content", + "title": "CMS内容表", + "primary": ["id"] + } + ], + "fields": [ + {"name": "id", "title": "ID", "type": "str", "length": 32, "nullable": "no", "comments": "主键ID"}, + {"name": "org_id", "title": "组织ID", "type": "str", "length": 32, "nullable": "no", "comments": "所属组织"}, + {"name": "content_type", "title": "内容类型", "type": "str", "length": 32, "nullable": "no", "comments": "news/product/case/banner"}, + {"name": "category_id", "title": "分类ID", "type": "str", "length": 32, "nullable": "yes", "comments": "关联分类"}, + {"name": "title", "title": "标题", "type": "str", "length": 200, "nullable": "no", "comments": "内容标题"}, + {"name": "subtitle", "title": "副标题", "type": "str", "length": 200, "nullable": "yes", "comments": "副标题"}, + {"name": "summary_text", "title": "摘要", "type": "text", "nullable": "yes", "comments": "内容摘要"}, + {"name": "body", "title": "正文", "type": "text", "nullable": "yes", "comments": "正文内容(HTML)"}, + {"name": "image_url", "title": "图片URL", "type": "str", "length": 500, "nullable": "yes", "comments": "封面图片URL"}, + {"name": "tags", "title": "标签", "type": "str", "length": 500, "nullable": "yes", "comments": "标签,逗号分隔"}, + {"name": "sort_order", "title": "排序", "type": "int", "nullable": "yes", "default": "0", "comments": "排序序号"}, + {"name": "status", "title": "状态", "type": "str", "length": 20, "nullable": "no", "default": "draft", "comments": "draft/published/archived"}, + {"name": "approval_id", "title": "审批ID", "type": "str", "length": 64, "nullable": "yes", "comments": "钉钉审批实例ID"}, + {"name": "published_at", "title": "发布时间", "type": "timestamp", "nullable": "yes", "comments": "发布时间"}, + {"name": "extra_json", "title": "扩展JSON", "type": "text", "nullable": "yes", "comments": "扩展属性JSON"}, + {"name": "created_by", "title": "创建人", "type": "str", "length": 32, "nullable": "yes", "comments": "创建人ID"}, + {"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no", "comments": "创建时间"}, + {"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no", "comments": "更新时间"} + ], + "indexes": [ + {"name": "idx_content_type", "idxtype": "index", "idxfields": ["content_type"]}, + {"name": "idx_content_status", "idxtype": "index", "idxfields": ["status"]}, + {"name": "idx_content_org", "idxtype": "index", "idxfields": ["org_id"]}, + {"name": "idx_content_category", "idxtype": "index", "idxfields": ["category_id"]} + ] +} diff --git a/models/cms_leads.json b/models/cms_leads.json new file mode 100644 index 0000000..46dd0d9 --- /dev/null +++ b/models/cms_leads.json @@ -0,0 +1,33 @@ +{ + "summary": [ + { + "name": "cms_leads", + "title": "CMS商机线索表", + "primary": ["id"] + } + ], + "fields": [ + {"name": "id", "title": "ID", "type": "str", "length": 32, "nullable": "no", "comments": "主键ID"}, + {"name": "org_id", "title": "组织ID", "type": "str", "length": 32, "nullable": "no", "comments": "所属组织"}, + {"name": "source", "title": "来源", "type": "str", "length": 50, "nullable": "yes", "comments": "来源: website/form/ai等"}, + {"name": "name", "title": "联系人", "type": "str", "length": 100, "nullable": "yes", "comments": "联系人姓名"}, + {"name": "company", "title": "公司", "type": "str", "length": 200, "nullable": "yes", "comments": "公司名称"}, + {"name": "phone", "title": "电话", "type": "str", "length": 30, "nullable": "yes", "comments": "联系电话"}, + {"name": "email", "title": "邮箱", "type": "str", "length": 200, "nullable": "yes", "comments": "电子邮箱"}, + {"name": "industry", "title": "行业", "type": "str", "length": 100, "nullable": "yes", "comments": "所属行业"}, + {"name": "region", "title": "地区", "type": "str", "length": 100, "nullable": "yes", "comments": "所在地区"}, + {"name": "interest_products", "title": "意向产品", "type": "str", "length": 500, "nullable": "yes", "comments": "意向产品"}, + {"name": "message", "title": "留言", "type": "text", "nullable": "yes", "comments": "访客留言内容"}, + {"name": "raw_text", "title": "原始文本", "type": "text", "nullable": "yes", "comments": "AI抽取前的原始文本"}, + {"name": "status", "title": "状态", "type": "str", "length": 20, "nullable": "no", "default": "new", "comments": "new/contacting/qualified/converted/closed"}, + {"name": "assigned_to", "title": "负责人", "type": "str", "length": 32, "nullable": "yes", "comments": "分配给的销售人员ID"}, + {"name": "notes", "title": "备注", "type": "text", "nullable": "yes", "comments": "跟进备注"}, + {"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no", "comments": "创建时间"}, + {"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no", "comments": "更新时间"} + ], + "indexes": [ + {"name": "idx_leads_org", "idxtype": "index", "idxfields": ["org_id"]}, + {"name": "idx_leads_status", "idxtype": "index", "idxfields": ["status"]}, + {"name": "idx_leads_source", "idxtype": "index", "idxfields": ["source"]} + ] +} diff --git a/models/cms_sections.json b/models/cms_sections.json new file mode 100644 index 0000000..9ce0320 --- /dev/null +++ b/models/cms_sections.json @@ -0,0 +1,29 @@ +{ + "summary": [ + { + "name": "cms_sections", + "title": "CMS栏目表", + "primary": ["id"] + } + ], + "fields": [ + {"name": "id", "title": "ID", "type": "str", "length": 32, "nullable": "no", "comments": "主键ID"}, + {"name": "org_id", "title": "组织ID", "type": "str", "length": 32, "nullable": "no", "comments": "所属组织"}, + {"name": "section_key", "title": "栏目Key", "type": "str", "length": 64, "nullable": "no", "comments": "栏目唯一标识"}, + {"name": "title", "title": "标题", "type": "str", "length": 100, "nullable": "no", "comments": "栏目标题"}, + {"name": "subtitle", "title": "副标题", "type": "str", "length": 200, "nullable": "yes", "comments": "栏目副标题"}, + {"name": "section_type", "title": "栏目类型", "type": "str", "length": 32, "nullable": "yes", "comments": "栏目类型: hero/features/list/cta等"}, + {"name": "content_type", "title": "内容类型", "type": "str", "length": 32, "nullable": "yes", "comments": "关联内容类型: news/product/case/banner"}, + {"name": "sort_order", "title": "排序", "type": "int", "nullable": "yes", "default": "0", "comments": "排序序号"}, + {"name": "is_visible", "title": "是否可见", "type": "str", "length": 1, "nullable": "no", "default": "1", "comments": "1=显示,0=隐藏"}, + {"name": "display_config", "title": "展示配置", "type": "text", "nullable": "yes", "comments": "展示配置JSON"}, + {"name": "style_config", "title": "样式配置", "type": "text", "nullable": "yes", "comments": "样式配置JSON"}, + {"name": "static_content", "title": "静态内容", "type": "text", "nullable": "yes", "comments": "静态HTML内容"}, + {"name": "created_at", "title": "创建时间", "type": "timestamp", "nullable": "no", "comments": "创建时间"}, + {"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no", "comments": "更新时间"} + ], + "indexes": [ + {"name": "idx_sections_org", "idxtype": "index", "idxfields": ["org_id"]}, + {"name": "idx_sections_key", "idxtype": "unique", "idxfields": ["org_id", "section_key"]} + ] +} diff --git a/models/cms_site_config.json b/models/cms_site_config.json new file mode 100644 index 0000000..87d23c6 --- /dev/null +++ b/models/cms_site_config.json @@ -0,0 +1,23 @@ +{ + "summary": [ + { + "name": "cms_site_config", + "title": "CMS站点配置表", + "primary": ["id"] + } + ], + "fields": [ + {"name": "id", "title": "ID", "type": "str", "length": 32, "nullable": "no", "comments": "主键ID"}, + {"name": "org_id", "title": "组织ID", "type": "str", "length": 32, "nullable": "no", "comments": "所属组织"}, + {"name": "config_group", "title": "配置组", "type": "str", "length": 50, "nullable": "no", "comments": "配置分组: header/footer/contact/seo等"}, + {"name": "config_key", "title": "配置键", "type": "str", "length": 100, "nullable": "no", "comments": "配置键名"}, + {"name": "config_value", "title": "配置值", "type": "text", "nullable": "yes", "comments": "配置值"}, + {"name": "config_type", "title": "值类型", "type": "str", "length": 20, "nullable": "yes", "default": "text", "comments": "text/number/json/bool/image"}, + {"name": "sort_order", "title": "排序", "type": "int", "nullable": "yes", "default": "0", "comments": "排序序号"}, + {"name": "updated_at", "title": "更新时间", "type": "timestamp", "nullable": "no", "comments": "更新时间"} + ], + "indexes": [ + {"name": "idx_site_config_org", "idxtype": "index", "idxfields": ["org_id"]}, + {"name": "idx_site_config_key", "idxtype": "unique", "idxfields": ["org_id", "config_group", "config_key"]} + ] +} diff --git a/wwwroot/api/cms_categories_create.dspy b/wwwroot/api/cms_categories_create.dspy new file mode 100644 index 0000000..3d5b28e --- /dev/null +++ b/wwwroot/api/cms_categories_create.dspy @@ -0,0 +1,30 @@ +user_id = await get_user() +if not user_id: + return json.dumps({'status': 'error', 'message': '未登录'}, ensure_ascii=False) + +org_id = params_kw.get('org_id', '') +if not org_id: + org_id = (await get_userorgid()) or '0' + +now = curDateString() +data = { + 'id': getID(), + 'org_id': org_id, + 'name': params_kw.get('name', ''), + 'parent_id': params_kw.get('parent_id', ''), + 'content_type': params_kw.get('content_type', ''), + 'description': params_kw.get('description', ''), + 'sort_order': int(params_kw.get('sort_order', '0') or '0'), + 'created_at': now, + 'display_config': params_kw.get('display_config', ''), +} + +if not data['name']: + return json.dumps({'status': 'error', 'message': '分类名称不能为空'}, ensure_ascii=False) + +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + await sor.I('cms_categories', data) + return json.dumps({'status': 'ok', 'id': data['id']}, ensure_ascii=False) +except Exception as e: + return json.dumps({'status': 'error', 'message': str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/cms_categories_delete.dspy b/wwwroot/api/cms_categories_delete.dspy new file mode 100644 index 0000000..c556389 --- /dev/null +++ b/wwwroot/api/cms_categories_delete.dspy @@ -0,0 +1,14 @@ +user_id = await get_user() +if not user_id: + return json.dumps({'status': 'error', 'message': '未登录'}, ensure_ascii=False) + +rec_id = params_kw.get('id', '') +if not rec_id: + return json.dumps({'status': 'error', 'message': '缺少id'}, ensure_ascii=False) + +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + await sor.D('cms_categories', {'id': rec_id}) + return json.dumps({'status': 'ok'}, ensure_ascii=False) +except Exception as e: + return json.dumps({'status': 'error', 'message': str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/cms_categories_update.dspy b/wwwroot/api/cms_categories_update.dspy new file mode 100644 index 0000000..9e0c69c --- /dev/null +++ b/wwwroot/api/cms_categories_update.dspy @@ -0,0 +1,21 @@ +user_id = await get_user() +if not user_id: + return json.dumps({'status': 'error', 'message': '未登录'}, ensure_ascii=False) + +rec_id = params_kw.get('id', '') +if not rec_id: + return json.dumps({'status': 'error', 'message': '缺少id'}, ensure_ascii=False) + +data = {'id': rec_id} +for field in ['name', 'parent_id', 'content_type', 'description', 'display_config']: + if field in params_kw: + data[field] = params_kw[field] +if 'sort_order' in params_kw: + data['sort_order'] = int(params_kw['sort_order'] or '0') + +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + await sor.U('cms_categories', data) + return json.dumps({'status': 'ok'}, ensure_ascii=False) +except Exception as e: + return json.dumps({'status': 'error', 'message': str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/cms_content_create.dspy b/wwwroot/api/cms_content_create.dspy new file mode 100644 index 0000000..fb80969 --- /dev/null +++ b/wwwroot/api/cms_content_create.dspy @@ -0,0 +1,39 @@ +user_id = await get_user() +if not user_id: + return json.dumps({'status': 'error', 'message': '未登录'}, ensure_ascii=False) + +org_id = params_kw.get('org_id', '') +if not org_id: + org_id = (await get_userorgid()) or '0' + +now = curDateString() +data = { + 'id': getID(), + 'org_id': org_id, + 'content_type': params_kw.get('content_type', ''), + 'category_id': params_kw.get('category_id', ''), + 'title': params_kw.get('title', ''), + 'subtitle': params_kw.get('subtitle', ''), + 'summary_text': params_kw.get('summary_text', ''), + 'body': params_kw.get('body', ''), + 'image_url': params_kw.get('image_url', ''), + 'tags': params_kw.get('tags', ''), + 'sort_order': int(params_kw.get('sort_order', '0') or '0'), + 'status': params_kw.get('status', 'draft'), + 'approval_id': params_kw.get('approval_id', ''), + 'published_at': params_kw.get('published_at', '') or None, + 'extra_json': params_kw.get('extra_json', ''), + 'created_by': user_id, + 'created_at': now, + 'updated_at': now, +} + +if not data['title']: + return json.dumps({'status': 'error', 'message': '标题不能为空'}, ensure_ascii=False) + +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + await sor.I('cms_content', data) + return json.dumps({'status': 'ok', 'id': data['id']}, ensure_ascii=False) +except Exception as e: + return json.dumps({'status': 'error', 'message': str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/cms_content_delete.dspy b/wwwroot/api/cms_content_delete.dspy new file mode 100644 index 0000000..5e95a8b --- /dev/null +++ b/wwwroot/api/cms_content_delete.dspy @@ -0,0 +1,14 @@ +user_id = await get_user() +if not user_id: + return json.dumps({'status': 'error', 'message': '未登录'}, ensure_ascii=False) + +rec_id = params_kw.get('id', '') +if not rec_id: + return json.dumps({'status': 'error', 'message': '缺少id'}, ensure_ascii=False) + +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + await sor.D('cms_content', {'id': rec_id}) + return json.dumps({'status': 'ok'}, ensure_ascii=False) +except Exception as e: + return json.dumps({'status': 'error', 'message': str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/cms_content_update.dspy b/wwwroot/api/cms_content_update.dspy new file mode 100644 index 0000000..cf83891 --- /dev/null +++ b/wwwroot/api/cms_content_update.dspy @@ -0,0 +1,24 @@ +user_id = await get_user() +if not user_id: + return json.dumps({'status': 'error', 'message': '未登录'}, ensure_ascii=False) + +rec_id = params_kw.get('id', '') +if not rec_id: + return json.dumps({'status': 'error', 'message': '缺少id'}, ensure_ascii=False) + +data = {'id': rec_id, 'updated_at': curDateString()} +for field in ['content_type', 'category_id', 'title', 'subtitle', 'summary_text', + 'body', 'image_url', 'tags', 'status', 'approval_id', 'extra_json']: + if field in params_kw: + data[field] = params_kw[field] +if 'sort_order' in params_kw: + data['sort_order'] = int(params_kw['sort_order'] or '0') +if 'published_at' in params_kw: + data['published_at'] = params_kw['published_at'] or None + +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + await sor.U('cms_content', data) + return json.dumps({'status': 'ok'}, ensure_ascii=False) +except Exception as e: + return json.dumps({'status': 'error', 'message': str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/cms_leads_create.dspy b/wwwroot/api/cms_leads_create.dspy new file mode 100644 index 0000000..0fb6033 --- /dev/null +++ b/wwwroot/api/cms_leads_create.dspy @@ -0,0 +1,35 @@ +user_id = await get_user() +if not user_id: + return json.dumps({'status': 'error', 'message': '未登录'}, ensure_ascii=False) + +org_id = params_kw.get('org_id', '') +if not org_id: + org_id = (await get_userorgid()) or '0' + +now = curDateString() +data = { + 'id': getID(), + 'org_id': org_id, + 'source': params_kw.get('source', 'website'), + 'name': params_kw.get('name', ''), + 'company': params_kw.get('company', ''), + 'phone': params_kw.get('phone', ''), + 'email': params_kw.get('email', ''), + 'industry': params_kw.get('industry', ''), + 'region': params_kw.get('region', ''), + 'interest_products': params_kw.get('interest_products', ''), + 'message': params_kw.get('message', ''), + 'raw_text': params_kw.get('raw_text', ''), + 'status': params_kw.get('status', 'new'), + 'assigned_to': params_kw.get('assigned_to', ''), + 'notes': params_kw.get('notes', ''), + 'created_at': now, + 'updated_at': now, +} + +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + await sor.I('cms_leads', data) + return json.dumps({'status': 'ok', 'id': data['id']}, ensure_ascii=False) +except Exception as e: + return json.dumps({'status': 'error', 'message': str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/cms_leads_delete.dspy b/wwwroot/api/cms_leads_delete.dspy new file mode 100644 index 0000000..f0e7038 --- /dev/null +++ b/wwwroot/api/cms_leads_delete.dspy @@ -0,0 +1,14 @@ +user_id = await get_user() +if not user_id: + return json.dumps({'status': 'error', 'message': '未登录'}, ensure_ascii=False) + +rec_id = params_kw.get('id', '') +if not rec_id: + return json.dumps({'status': 'error', 'message': '缺少id'}, ensure_ascii=False) + +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + await sor.D('cms_leads', {'id': rec_id}) + return json.dumps({'status': 'ok'}, ensure_ascii=False) +except Exception as e: + return json.dumps({'status': 'error', 'message': str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/cms_leads_update.dspy b/wwwroot/api/cms_leads_update.dspy new file mode 100644 index 0000000..1ad3059 --- /dev/null +++ b/wwwroot/api/cms_leads_update.dspy @@ -0,0 +1,20 @@ +user_id = await get_user() +if not user_id: + return json.dumps({'status': 'error', 'message': '未登录'}, ensure_ascii=False) + +rec_id = params_kw.get('id', '') +if not rec_id: + return json.dumps({'status': 'error', 'message': '缺少id'}, ensure_ascii=False) + +data = {'id': rec_id, 'updated_at': curDateString()} +for field in ['source', 'name', 'company', 'phone', 'email', 'industry', 'region', + 'interest_products', 'message', 'raw_text', 'status', 'assigned_to', 'notes']: + if field in params_kw: + data[field] = params_kw[field] + +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + await sor.U('cms_leads', data) + return json.dumps({'status': 'ok'}, ensure_ascii=False) +except Exception as e: + return json.dumps({'status': 'error', 'message': str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/cms_sections_create.dspy b/wwwroot/api/cms_sections_create.dspy new file mode 100644 index 0000000..164358d --- /dev/null +++ b/wwwroot/api/cms_sections_create.dspy @@ -0,0 +1,35 @@ +user_id = await get_user() +if not user_id: + return json.dumps({'status': 'error', 'message': '未登录'}, ensure_ascii=False) + +org_id = params_kw.get('org_id', '') +if not org_id: + org_id = (await get_userorgid()) or '0' + +now = curDateString() +data = { + 'id': getID(), + 'org_id': org_id, + 'section_key': params_kw.get('section_key', ''), + 'title': params_kw.get('title', ''), + 'subtitle': params_kw.get('subtitle', ''), + 'section_type': params_kw.get('section_type', ''), + 'content_type': params_kw.get('content_type', ''), + 'sort_order': int(params_kw.get('sort_order', '0') or '0'), + 'is_visible': params_kw.get('is_visible', '1'), + 'display_config': params_kw.get('display_config', ''), + 'style_config': params_kw.get('style_config', ''), + 'static_content': params_kw.get('static_content', ''), + 'created_at': now, + 'updated_at': now, +} + +if not data['section_key'] or not data['title']: + return json.dumps({'status': 'error', 'message': '栏目Key和标题不能为空'}, ensure_ascii=False) + +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + await sor.I('cms_sections', data) + return json.dumps({'status': 'ok', 'id': data['id']}, ensure_ascii=False) +except Exception as e: + return json.dumps({'status': 'error', 'message': str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/cms_sections_delete.dspy b/wwwroot/api/cms_sections_delete.dspy new file mode 100644 index 0000000..bd079ec --- /dev/null +++ b/wwwroot/api/cms_sections_delete.dspy @@ -0,0 +1,14 @@ +user_id = await get_user() +if not user_id: + return json.dumps({'status': 'error', 'message': '未登录'}, ensure_ascii=False) + +rec_id = params_kw.get('id', '') +if not rec_id: + return json.dumps({'status': 'error', 'message': '缺少id'}, ensure_ascii=False) + +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + await sor.D('cms_sections', {'id': rec_id}) + return json.dumps({'status': 'ok'}, ensure_ascii=False) +except Exception as e: + return json.dumps({'status': 'error', 'message': str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/cms_sections_update.dspy b/wwwroot/api/cms_sections_update.dspy new file mode 100644 index 0000000..d79d68e --- /dev/null +++ b/wwwroot/api/cms_sections_update.dspy @@ -0,0 +1,22 @@ +user_id = await get_user() +if not user_id: + return json.dumps({'status': 'error', 'message': '未登录'}, ensure_ascii=False) + +rec_id = params_kw.get('id', '') +if not rec_id: + return json.dumps({'status': 'error', 'message': '缺少id'}, ensure_ascii=False) + +data = {'id': rec_id, 'updated_at': curDateString()} +for field in ['section_key', 'title', 'subtitle', 'section_type', 'content_type', + 'is_visible', 'display_config', 'style_config', 'static_content']: + if field in params_kw: + data[field] = params_kw[field] +if 'sort_order' in params_kw: + data['sort_order'] = int(params_kw['sort_order'] or '0') + +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + await sor.U('cms_sections', data) + return json.dumps({'status': 'ok'}, ensure_ascii=False) +except Exception as e: + return json.dumps({'status': 'error', 'message': str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/cms_site_config_create.dspy b/wwwroot/api/cms_site_config_create.dspy new file mode 100644 index 0000000..b2accd2 --- /dev/null +++ b/wwwroot/api/cms_site_config_create.dspy @@ -0,0 +1,29 @@ +user_id = await get_user() +if not user_id: + return json.dumps({'status': 'error', 'message': '未登录'}, ensure_ascii=False) + +org_id = params_kw.get('org_id', '') +if not org_id: + org_id = (await get_userorgid()) or '0' + +now = curDateString() +data = { + 'id': getID(), + 'org_id': org_id, + 'config_group': params_kw.get('config_group', ''), + 'config_key': params_kw.get('config_key', ''), + 'config_value': params_kw.get('config_value', ''), + 'config_type': params_kw.get('config_type', 'text'), + 'sort_order': int(params_kw.get('sort_order', '0') or '0'), + 'updated_at': now, +} + +if not data['config_group'] or not data['config_key']: + return json.dumps({'status': 'error', 'message': '配置组和配置键不能为空'}, ensure_ascii=False) + +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + await sor.I('cms_site_config', data) + return json.dumps({'status': 'ok', 'id': data['id']}, ensure_ascii=False) +except Exception as e: + return json.dumps({'status': 'error', 'message': str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/cms_site_config_delete.dspy b/wwwroot/api/cms_site_config_delete.dspy new file mode 100644 index 0000000..72b7c8e --- /dev/null +++ b/wwwroot/api/cms_site_config_delete.dspy @@ -0,0 +1,14 @@ +user_id = await get_user() +if not user_id: + return json.dumps({'status': 'error', 'message': '未登录'}, ensure_ascii=False) + +rec_id = params_kw.get('id', '') +if not rec_id: + return json.dumps({'status': 'error', 'message': '缺少id'}, ensure_ascii=False) + +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + await sor.D('cms_site_config', {'id': rec_id}) + return json.dumps({'status': 'ok'}, ensure_ascii=False) +except Exception as e: + return json.dumps({'status': 'error', 'message': str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/cms_site_config_update.dspy b/wwwroot/api/cms_site_config_update.dspy new file mode 100644 index 0000000..cc9b9d7 --- /dev/null +++ b/wwwroot/api/cms_site_config_update.dspy @@ -0,0 +1,21 @@ +user_id = await get_user() +if not user_id: + return json.dumps({'status': 'error', 'message': '未登录'}, ensure_ascii=False) + +rec_id = params_kw.get('id', '') +if not rec_id: + return json.dumps({'status': 'error', 'message': '缺少id'}, ensure_ascii=False) + +data = {'id': rec_id, 'updated_at': curDateString()} +for field in ['config_group', 'config_key', 'config_value', 'config_type']: + if field in params_kw: + data[field] = params_kw[field] +if 'sort_order' in params_kw: + data['sort_order'] = int(params_kw['sort_order'] or '0') + +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + await sor.U('cms_site_config', data) + return json.dumps({'status': 'ok'}, ensure_ascii=False) +except Exception as e: + return json.dumps({'status': 'error', 'message': str(e)}, ensure_ascii=False) diff --git a/wwwroot/api/get_search_cms_categories.dspy b/wwwroot/api/get_search_cms_categories.dspy new file mode 100644 index 0000000..d71733b --- /dev/null +++ b/wwwroot/api/get_search_cms_categories.dspy @@ -0,0 +1,9 @@ +result = [{'value': '', 'text': '全部分类'}] +try: + async with get_sor_context(request._run_ns, 'ocai_cms') as sor: + rows = await sor.sqlExe("select id as value, name as text from cms_categories order by sort_order asc, created_at desc", {}) + result = [{'value': '', 'text': '全部分类'}] + list(rows) + return json.dumps(result, ensure_ascii=False) +except Exception as e: + debug(f'search categories error: {e}') +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/api/get_search_content_type.dspy b/wwwroot/api/get_search_content_type.dspy new file mode 100644 index 0000000..d2b63b7 --- /dev/null +++ b/wwwroot/api/get_search_content_type.dspy @@ -0,0 +1,8 @@ +result = [ + {'value': '', 'text': '全部类型'}, + {'value': 'news', 'text': '新闻'}, + {'value': 'product', 'text': '产品'}, + {'value': 'case', 'text': '案例'}, + {'value': 'banner', 'text': 'Banner'} +] +return json.dumps(result, ensure_ascii=False) diff --git a/wwwroot/cms_styles.css b/wwwroot/cms_styles.css index 54e1891..99a55a2 100644 --- a/wwwroot/cms_styles.css +++ b/wwwroot/cms_styles.css @@ -1039,5 +1039,23 @@ a { color: inherit; text-decoration: none; } transform: translateY(0); } +/* === Global bricks widget background reset for dark theme === */ +.site-root .bricks-vbox, +.site-root .bricks-hbox, +.site-root .bricks-text, +.site-root .bricks-title2, +.site-root .bricks-title3, +.site-root .bricks-title4, +.site-root .bricks-filler, +.site-root .bricks-html { + background: transparent; + color: inherit; +} + +.site-root .bricks-button { + background: transparent; + color: var(--text-primary); +} + /* Clickable utility */ .clickable { cursor: pointer; } diff --git a/wwwroot/user_menu.ui b/wwwroot/user_menu.ui index ab2d3ec..b21802b 100644 --- a/wwwroot/user_menu.ui +++ b/wwwroot/user_menu.ui @@ -8,6 +8,13 @@ "label": "个人信息", "submenu": "{{entire_url('/rbac/user/userinfo.ui')}}" }, + {% if has_permission('/admin.ui') %} + { + "name": "admin", + "label": "管理后台", + "submenu": "{{entire_url('/admin.ui')}}" + }, + {% endif %} { "name": "logout", "label": "退出登录",