refactor: 从独立webapp重构为纯Sage模块
- cms/: Python包(合并原entcms+dingdingflow) - init.py: 791行,load_cms()注册所有CRUD+审批函数 - dingtalk_client.py: 钉钉API客户端 - models/: 7个表定义JSON(5个CMS+2个DD) - json/: 7个CRUD定义JSON - wwwroot/: 管理后台CRUD页面和API(37个dspy) - init/data.yaml: 模块初始数据(appcodes/appcodes_kv/分类/栏目/配置) - scripts/load_path.py: RBAC权限配置 - pyproject.toml: pip-installable包定义 - 删除: app/, conf/, build.sh, entcms/, dingdingflow/等webapp文件 - 数据库访问统一为DBPools()+_get_dbname()动态模式
This commit is contained in:
parent
f70e8e4d26
commit
4495e9589b
88
README.md
88
README.md
@ -1,58 +1,60 @@
|
|||||||
# 开元云科技 - 企业官网CMS系统
|
# CMS 内容管理模块
|
||||||
|
|
||||||
企业官网内容管理系统 + 钉钉审批流程,基于Sage/bricks-framework开发。
|
企业官网内容管理与钉钉审批工作流模块,基于Sage/bricks-framework开发。
|
||||||
|
|
||||||
## 模块
|
## 功能
|
||||||
|
|
||||||
| 模块 | 说明 |
|
- **内容管理**: 新闻/案例/产品/Banner的统一CRUD,带发布审批状态流
|
||||||
|
- **分类管理**: 按content_type分组的层级分类
|
||||||
|
- **栏目管理**: 官网页面栏目配置
|
||||||
|
- **商机线索**: 网站访客提交 + AI抽取
|
||||||
|
- **站点配置**: Hero标语、页脚等KV配置
|
||||||
|
- **钉钉审批**: 内容发布审批工作流
|
||||||
|
|
||||||
|
## 数据库表
|
||||||
|
|
||||||
|
| 表名 | 用途 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **entcms** | 企业CMS - 新闻/案例/产品/Banner/线索管理 |
|
| cms_content | 统一内容表 |
|
||||||
| **dingdingflow** | 钉钉审批流程 - 内容发布审批工作流 |
|
| cms_categories | 内容分类 |
|
||||||
|
| cms_sections | 栏目管理 |
|
||||||
|
| cms_leads | 商机线索 |
|
||||||
|
| cms_site_config | 站点配置 |
|
||||||
|
| dd_approvals | 审批记录 |
|
||||||
|
| dd_approval_configs | 审批流程配置 |
|
||||||
|
|
||||||
## 目录结构
|
## 安装
|
||||||
|
|
||||||
```
|
|
||||||
cms/
|
|
||||||
├── conf/config.json # 应用配置
|
|
||||||
├── wwwroot/ # 前端静态文件(统一目录)
|
|
||||||
│ ├── index.ui, news.ui, ... # 企业官网页面
|
|
||||||
│ ├── api/*.dspy # CMS后端API
|
|
||||||
│ └── dingdingflow/ # 钉钉审批模块前端
|
|
||||||
│ ├── index.ui, menu.ui
|
|
||||||
│ └── api/*.dspy
|
|
||||||
├── entcms/ # 企业CMS Python模块
|
|
||||||
├── dingdingflow/ # 钉钉审批Python模块
|
|
||||||
├── bricks -> pkgs/bricks/dist # 前端框架(符号链接)
|
|
||||||
├── build.sh # 构建脚本
|
|
||||||
├── start.sh / stop.sh # 启停脚本
|
|
||||||
├── init_superuser_permissions.py # superuser权限初始化
|
|
||||||
├── init_any_permissions.py # any权限初始化
|
|
||||||
└── scripts/init_superuser.py # 超级用户账号初始化
|
|
||||||
```
|
|
||||||
|
|
||||||
## 快速开始
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. 构建并安装
|
pip install -e ~/repos/cms
|
||||||
cd ~/repos/cms && ./build.sh
|
|
||||||
|
|
||||||
# 2. 配置RBAC权限
|
|
||||||
cd ~/repos/sage
|
|
||||||
./py3/bin/python ~/repos/cms/entcms/scripts/load_path.py
|
|
||||||
./py3/bin/python ~/repos/cms/dingdingflow/scripts/load_path.py
|
|
||||||
|
|
||||||
# 3. 重启Sage
|
|
||||||
./stop.sh && ./start.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 文档
|
## 集成
|
||||||
- [系统架构](docs/architecture.md)
|
|
||||||
- [测试用例](docs/test-cases.md)
|
在Web应用(app/portal.py)中:
|
||||||
- [开发日志](docs/)
|
|
||||||
|
```python
|
||||||
|
from cms.init import load_cms
|
||||||
|
|
||||||
|
def init():
|
||||||
|
load_cms()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 初始数据
|
||||||
|
|
||||||
|
`init/data.yaml` 包含:
|
||||||
|
- appcodes/appcodes_kv: 枚举编码(content_type, content_status, lead_status等)
|
||||||
|
- cms_categories: 默认分类
|
||||||
|
- cms_site_config: 默认站点配置
|
||||||
|
- cms_sections: 默认栏目配置
|
||||||
|
- dd_approval_configs: 默认审批配置
|
||||||
|
|
||||||
|
## 环境变量 (钉钉审批)
|
||||||
|
|
||||||
## 环境变量 (dingdingflow)
|
|
||||||
```
|
```
|
||||||
DINGTALK_APP_KEY=xxx
|
DINGTALK_APP_KEY=xxx
|
||||||
DINGTALK_APP_SECRET=xxx
|
DINGTALK_APP_SECRET=xxx
|
||||||
DINGTALK_AGENT_ID=xxx
|
DINGTALK_AGENT_ID=xxx
|
||||||
```
|
```
|
||||||
|
|
||||||
|
缺少环境变量时自动使用mock响应。
|
||||||
|
|||||||
52
app/cms.py
52
app/cms.py
@ -1,52 +0,0 @@
|
|||||||
"""
|
|
||||||
开元云科技CMS — 独立Web应用主入口
|
|
||||||
启动: py3/bin/python app/cms.py -p 9090 -w $(pwd)
|
|
||||||
"""
|
|
||||||
import os, sys
|
|
||||||
|
|
||||||
# 添加应用根目录到Python路径
|
|
||||||
app_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
root_dir = os.path.dirname(app_dir)
|
|
||||||
sys.path.insert(0, root_dir)
|
|
||||||
|
|
||||||
# Ensure app/ is in path for local imports
|
|
||||||
sys.path.insert(0, app_dir)
|
|
||||||
|
|
||||||
from appPublic.log import MyLogger, info
|
|
||||||
from appPublic.folderUtils import ProgramPath
|
|
||||||
from appPublic.jsonConfig import getConfig
|
|
||||||
from appPublic.registerfunction import RegisterFunction
|
|
||||||
from bricks_for_python.init import load_pybricks
|
|
||||||
from ahserver.webapp import webapp
|
|
||||||
from ahserver.serverenv import ServerEnv
|
|
||||||
from sqlor.dbpools import DBPools
|
|
||||||
|
|
||||||
# CMS业务模块
|
|
||||||
from entcms.init import load_entcms
|
|
||||||
from dingdingflow.init import load_dingdingflow
|
|
||||||
|
|
||||||
# RBAC认证(复用sage的rbac模块)
|
|
||||||
from rbac.init import load_rbac
|
|
||||||
from appbase.init import load_appbase
|
|
||||||
|
|
||||||
# 全局函数
|
|
||||||
from global_func import set_globalvariable
|
|
||||||
|
|
||||||
__version__ = '1.0.0'
|
|
||||||
|
|
||||||
def get_module_dbname(m):
|
|
||||||
return 'ocai_cms'
|
|
||||||
|
|
||||||
def init():
|
|
||||||
rf = RegisterFunction()
|
|
||||||
set_globalvariable()
|
|
||||||
env = ServerEnv()
|
|
||||||
env.get_module_dbname = get_module_dbname
|
|
||||||
load_pybricks()
|
|
||||||
load_appbase()
|
|
||||||
load_rbac()
|
|
||||||
load_entcms()
|
|
||||||
load_dingdingflow()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
webapp(init)
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
"""
|
|
||||||
CMS全局函数 — 注册到ServerEnv供.dspy和.ui调用
|
|
||||||
"""
|
|
||||||
from ahserver.serverenv import ServerEnv
|
|
||||||
|
|
||||||
def get_module_dbname(mname):
|
|
||||||
"""CMS应用统一使用ocai_cms数据库"""
|
|
||||||
return 'ocai_cms'
|
|
||||||
|
|
||||||
def UiWindow(title, icon, content, cheight=10, cwidth=15):
|
|
||||||
return {
|
|
||||||
"widgettype": "PopupWindow",
|
|
||||||
"options": {
|
|
||||||
"author": "cms",
|
|
||||||
"cwidth": cwidth,
|
|
||||||
"cheight": cheight,
|
|
||||||
"title": title,
|
|
||||||
"content": content,
|
|
||||||
"icon": icon or entire_url('/bricks/imgs/app.png'),
|
|
||||||
"movable": True,
|
|
||||||
"auto_open": True
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def UiError(title="出错", message="出错啦", timeout=5):
|
|
||||||
return {
|
|
||||||
"widgettype": "Error",
|
|
||||||
"options": {
|
|
||||||
"author": "cms",
|
|
||||||
"timeout": timeout,
|
|
||||||
"cwidth": 15,
|
|
||||||
"cheight": 10,
|
|
||||||
"title": title,
|
|
||||||
"message": message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def UiMessage(title="消息", message="后台消息", timeout=5):
|
|
||||||
return {
|
|
||||||
"widgettype": "Message",
|
|
||||||
"options": {
|
|
||||||
"author": "cms",
|
|
||||||
"timeout": timeout,
|
|
||||||
"cwidth": 15,
|
|
||||||
"cheight": 10,
|
|
||||||
"title": title,
|
|
||||||
"message": message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def set_globalvariable():
|
|
||||||
g = ServerEnv()
|
|
||||||
g.get_module_dbname = get_module_dbname
|
|
||||||
g.UiError = UiError
|
|
||||||
g.UiMessage = UiMessage
|
|
||||||
g.UiWindow = UiWindow
|
|
||||||
198
build.sh
198
build.sh
@ -1,198 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# 开元云科技CMS — 独立Web应用构建脚本
|
|
||||||
# 用法: cd ~/repos/cms && ./build.sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
cdir=$(pwd)
|
|
||||||
uname=$(id -un)
|
|
||||||
gname=$(id -gn)
|
|
||||||
|
|
||||||
echo "============================================"
|
|
||||||
echo " 开元云科技CMS — 独立Web应用构建"
|
|
||||||
echo "============================================"
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# Step 1: Python虚拟环境
|
|
||||||
# ===========================================
|
|
||||||
echo ""
|
|
||||||
echo "--- Step 1: 创建Python虚拟环境 ---"
|
|
||||||
if [ ! -d "py3" ]; then
|
|
||||||
python3 -m venv py3
|
|
||||||
fi
|
|
||||||
source py3/bin/activate
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# Step 2: 核心依赖
|
|
||||||
# ===========================================
|
|
||||||
echo ""
|
|
||||||
echo "--- Step 2: 安装核心依赖 ---"
|
|
||||||
mkdir -p pkgs
|
|
||||||
|
|
||||||
# 核心框架包
|
|
||||||
for m in apppublic sqlor ahserver bricks-for-python xls2ddl
|
|
||||||
do
|
|
||||||
echo " install $m..."
|
|
||||||
cd $cdir/pkgs
|
|
||||||
if [ ! -d "$m" ]; then
|
|
||||||
git clone https://git.opencomputing.cn/yumoqing/$m
|
|
||||||
fi
|
|
||||||
cd $m
|
|
||||||
$cdir/py3/bin/pip install . 2>/dev/null || echo " WARN: $m install failed"
|
|
||||||
done
|
|
||||||
|
|
||||||
# bricks前端
|
|
||||||
echo " install bricks..."
|
|
||||||
cd $cdir/pkgs
|
|
||||||
if [ ! -d "bricks" ]; then
|
|
||||||
git clone https://git.opencomputing.cn/yumoqing/bricks
|
|
||||||
fi
|
|
||||||
cd bricks/bricks
|
|
||||||
./build.sh 2>/dev/null || echo " WARN: bricks build skipped"
|
|
||||||
|
|
||||||
# bricks符号链接
|
|
||||||
mkdir -p $cdir/bricks
|
|
||||||
if [ -d "$cdir/pkgs/bricks/dist" ]; then
|
|
||||||
rm -f $cdir/bricks
|
|
||||||
ln -sf $cdir/pkgs/bricks/dist $cdir/bricks
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# Step 3: RBAC + AppBase模块(认证依赖)
|
|
||||||
# ===========================================
|
|
||||||
echo ""
|
|
||||||
echo "--- Step 3: 安装RBAC/AppBase模块 ---"
|
|
||||||
for m in appbase rbac checklang
|
|
||||||
do
|
|
||||||
echo " install $m..."
|
|
||||||
cd $cdir/pkgs
|
|
||||||
if [ ! -d "$m" ]; then
|
|
||||||
git clone https://git.opencomputing.cn/yumoqing/$m
|
|
||||||
fi
|
|
||||||
cd $m
|
|
||||||
$cdir/py3/bin/pip install . 2>/dev/null || echo " WARN: $m install failed"
|
|
||||||
done
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# Step 4: CMS业务模块
|
|
||||||
# ===========================================
|
|
||||||
echo ""
|
|
||||||
echo "--- Step 4: 安装CMS业务模块 ---"
|
|
||||||
|
|
||||||
# entcms模块
|
|
||||||
echo " install entcms..."
|
|
||||||
cd $cdir/entcms
|
|
||||||
$cdir/py3/bin/pip install . 2>/dev/null || echo " WARN: entcms install failed"
|
|
||||||
|
|
||||||
# dingdingflow模块
|
|
||||||
echo " install dingdingflow..."
|
|
||||||
cd $cdir/dingdingflow
|
|
||||||
$cdir/py3/bin/pip install . 2>/dev/null || echo " WARN: dingdingflow install failed"
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# Step 5: 数据库DDL(CMS业务表)
|
|
||||||
# ===========================================
|
|
||||||
echo ""
|
|
||||||
echo "--- Step 5: 生成数据库DDL ---"
|
|
||||||
|
|
||||||
# entcms表DDL
|
|
||||||
if [ -d "$cdir/entcms/models" ]; then
|
|
||||||
cd $cdir/entcms/models
|
|
||||||
echo " 生成 entcms DDL..."
|
|
||||||
$cdir/py3/bin/json2ddl mysql . > $cdir/entcms/mysql.ddl.sql 2>/dev/null || echo " WARN: json2ddl failed for entcms"
|
|
||||||
echo " DDL已生成: entcms/mysql.ddl.sql"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# dingdingflow表DDL
|
|
||||||
if [ -d "$cdir/dingdingflow/models" ]; then
|
|
||||||
cd $cdir/dingdingflow/models
|
|
||||||
echo " 生成 dingdingflow DDL..."
|
|
||||||
$cdir/py3/bin/json2ddl mysql . > $cdir/dingdingflow/mysql.ddl.sql 2>/dev/null || echo " WARN: json2ddl failed for dingdingflow"
|
|
||||||
echo " DDL已生成: dingdingflow/mysql.ddl.sql"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# Step 6: CRUD UI生成
|
|
||||||
# ===========================================
|
|
||||||
echo ""
|
|
||||||
echo "--- Step 6: 生成CRUD UI ---"
|
|
||||||
|
|
||||||
# entcms CRUD
|
|
||||||
if [ -d "$cdir/entcms/json" ]; then
|
|
||||||
cd $cdir/entcms/json
|
|
||||||
echo " 生成 entcms CRUD UI..."
|
|
||||||
for f in *.json; do
|
|
||||||
[ -f "$f" ] || continue
|
|
||||||
echo " $f"
|
|
||||||
$cdir/py3/bin/xls2ui -m ../models -o ../wwwroot entcms $f 2>/dev/null || echo " WARN: xls2ui failed for $f"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# dingdingflow CRUD
|
|
||||||
if [ -d "$cdir/dingdingflow/json" ]; then
|
|
||||||
cd $cdir/dingdingflow/json
|
|
||||||
echo " 生成 dingdingflow CRUD UI..."
|
|
||||||
for f in *.json; do
|
|
||||||
[ -f "$f" ] || continue
|
|
||||||
echo " $f"
|
|
||||||
$cdir/py3/bin/xls2ui -m ../models -o ../wwwroot dingdingflow $f 2>/dev/null || echo " WARN: xls2ui failed for $f"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# Step 7: 日志和文件目录
|
|
||||||
# ===========================================
|
|
||||||
echo ""
|
|
||||||
echo "--- Step 7: 创建运行时目录 ---"
|
|
||||||
mkdir -p $cdir/logs
|
|
||||||
mkdir -p $cdir/files
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# Step 8: systemd服务文件
|
|
||||||
# ===========================================
|
|
||||||
echo ""
|
|
||||||
echo "--- Step 8: 生成systemd服务文件 ---"
|
|
||||||
cat > $cdir/cms.service <<EOF
|
|
||||||
[Unit]
|
|
||||||
Description=KaiYuan Cloud CMS Web Application
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
User=$uname
|
|
||||||
Group=$gname
|
|
||||||
Type=forking
|
|
||||||
WorkingDirectory=$cdir
|
|
||||||
ExecStart=$cdir/start.sh
|
|
||||||
ExecStop=$cdir/stop.sh
|
|
||||||
StandardOutput=append:$cdir/logs/cms.log
|
|
||||||
StandardError=append:$cdir/logs/cms.log
|
|
||||||
SyslogIdentifier=cms
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
EOF
|
|
||||||
echo " cms.service 已生成"
|
|
||||||
|
|
||||||
# ===========================================
|
|
||||||
# Done
|
|
||||||
# ===========================================
|
|
||||||
cd $cdir
|
|
||||||
echo ""
|
|
||||||
echo "============================================"
|
|
||||||
echo " 构建完成!"
|
|
||||||
echo "============================================"
|
|
||||||
echo ""
|
|
||||||
echo "后续步骤:"
|
|
||||||
echo " 1. 编辑 conf/config.json 填入数据库密码"
|
|
||||||
echo " 2. 执行DDL创建CMS业务表:"
|
|
||||||
echo " mysql -h HOST -u USER -pPASS sage < entcms/mysql.ddl.sql"
|
|
||||||
echo " mysql -h HOST -u USER -pPASS sage < dingdingflow/mysql.ddl.sql"
|
|
||||||
echo " 3. 初始化权限:"
|
|
||||||
echo " py3/bin/python init_superuser_permissions.py"
|
|
||||||
echo " py3/bin/python init_any_permissions.py"
|
|
||||||
echo " 4. 初始化超级用户:"
|
|
||||||
echo " py3/bin/python scripts/init_superuser.py"
|
|
||||||
echo " 5. 启动应用:"
|
|
||||||
echo " ./start.sh"
|
|
||||||
echo ""
|
|
||||||
echo "访问地址: http://localhost:9090/"
|
|
||||||
echo "管理后台: http://localhost:9090/admin.ui"
|
|
||||||
8
cms/__init__.py
Normal file
8
cms/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
"""
|
||||||
|
CMS - 企业官网内容管理与审批工作流模块
|
||||||
|
合并原 entcms + dingdingflow 两个子模块
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .init import load_cms
|
||||||
|
|
||||||
|
__all__ = ['load_cms']
|
||||||
791
cms/init.py
Normal file
791
cms/init.py
Normal file
@ -0,0 +1,791 @@
|
|||||||
|
"""
|
||||||
|
cms - 企业CMS内容管理与钉钉审批工作流模块
|
||||||
|
合并原 entcms + dingdingflow 两个子模块
|
||||||
|
|
||||||
|
提供:
|
||||||
|
- CMS Content CRUD (cms_content_*)
|
||||||
|
- CMS Categories CRUD (cms_categories_*)
|
||||||
|
- CMS Sections CRUD (cms_sections_*)
|
||||||
|
- CMS Leads CRUD (cms_leads_*)
|
||||||
|
- CMS Site Config CRUD (cms_site_config_*)
|
||||||
|
- 公开API (get_published_content, get_latest_news, get_content_detail, submit_lead, get_visible_sections, get_site_config, get_category_options)
|
||||||
|
- 钉钉审批 CRUD (dd_approvals_*, dd_approval_configs_*)
|
||||||
|
- 审批业务逻辑 (submit_approval, get_approval_status, handle_dingtalk_callback)
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from ahserver.serverenv import ServerEnv
|
||||||
|
from appPublic.uniqueID import getID
|
||||||
|
from sqlor.dbpools import DBPools
|
||||||
|
from .dingtalk_client import get_dingtalk_client
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MODULE_NAME = 'cms'
|
||||||
|
MODULE_VERSION = '2.0.0'
|
||||||
|
|
||||||
|
|
||||||
|
def _get_dbname():
|
||||||
|
"""Get the database name for this module (dynamic, not hardcoded)."""
|
||||||
|
env = ServerEnv()
|
||||||
|
return env.get_module_dbname(MODULE_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# CMS Content CRUD
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
async def cms_content_list(ns=None):
|
||||||
|
"""查询内容列表"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
ns = ns or {}
|
||||||
|
ns.setdefault('sort', 'sort_order asc, created_at desc')
|
||||||
|
rows = await sor.R('cms_content', ns)
|
||||||
|
total = len(rows)
|
||||||
|
return {'rows': rows, 'total': total}
|
||||||
|
|
||||||
|
|
||||||
|
async def cms_content_create(data):
|
||||||
|
"""创建内容"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
data['id'] = getID()
|
||||||
|
await sor.C('cms_content', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def cms_content_update(data):
|
||||||
|
"""更新内容"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.U('cms_content', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def cms_content_delete(data):
|
||||||
|
"""删除内容"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.D('cms_content', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# CMS Categories CRUD
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
async def cms_categories_list(ns=None):
|
||||||
|
"""查询分类列表"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
ns = ns or {}
|
||||||
|
ns.setdefault('sort', 'sort_order asc')
|
||||||
|
rows = await sor.R('cms_categories', ns)
|
||||||
|
return {'rows': rows, 'total': len(rows)}
|
||||||
|
|
||||||
|
|
||||||
|
async def cms_categories_create(data):
|
||||||
|
"""创建分类"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
data['id'] = getID()
|
||||||
|
await sor.C('cms_categories', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def cms_categories_update(data):
|
||||||
|
"""更新分类"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.U('cms_categories', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def cms_categories_delete(data):
|
||||||
|
"""删除分类"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.D('cms_categories', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def get_category_options(content_type=None):
|
||||||
|
"""获取分类下拉选项"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
ns = {'sort': 'sort_order asc'}
|
||||||
|
if content_type:
|
||||||
|
ns['content_type'] = content_type
|
||||||
|
rows = await sor.R('cms_categories', ns)
|
||||||
|
options = [{'value': r['id'], 'text': r['name']} for r in rows]
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# CMS Sections CRUD
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
async def cms_sections_list(ns=None):
|
||||||
|
"""查询栏目列表"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
ns = ns or {}
|
||||||
|
ns.setdefault('sort', 'sort_order asc')
|
||||||
|
rows = await sor.R('cms_sections', ns)
|
||||||
|
return {'rows': rows, 'total': len(rows)}
|
||||||
|
|
||||||
|
|
||||||
|
async def cms_sections_create(data):
|
||||||
|
"""创建栏目"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
data['id'] = getID()
|
||||||
|
await sor.C('cms_sections', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def cms_sections_update(data):
|
||||||
|
"""更新栏目"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.U('cms_sections', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def cms_sections_delete(data):
|
||||||
|
"""删除栏目"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.D('cms_sections', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def get_visible_sections():
|
||||||
|
"""获取所有可见栏目(公开接口)"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
ns = {'is_visible': '1', 'sort': 'sort_order asc'}
|
||||||
|
rows = await sor.R('cms_sections', ns)
|
||||||
|
for r in rows:
|
||||||
|
for field in ['display_config', 'style_config', 'static_content']:
|
||||||
|
v = r.get(field, None)
|
||||||
|
if v and isinstance(v, str):
|
||||||
|
try:
|
||||||
|
r[field] = json.loads(v)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# CMS Leads CRUD
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
async def cms_leads_list(ns=None):
|
||||||
|
"""查询线索列表"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
ns = ns or {}
|
||||||
|
ns.setdefault('sort', 'created_at desc')
|
||||||
|
rows = await sor.R('cms_leads', ns)
|
||||||
|
return {'rows': rows, 'total': len(rows)}
|
||||||
|
|
||||||
|
|
||||||
|
async def cms_leads_create(data):
|
||||||
|
"""创建线索"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
data['id'] = getID()
|
||||||
|
await sor.C('cms_leads', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def cms_leads_update(data):
|
||||||
|
"""更新线索"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.U('cms_leads', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def cms_leads_delete(data):
|
||||||
|
"""删除线索"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.D('cms_leads', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def submit_lead(data):
|
||||||
|
"""公开接口 - 网站访客提交线索"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
data['id'] = getID()
|
||||||
|
data.setdefault('status', 'new')
|
||||||
|
data.setdefault('source', 'website')
|
||||||
|
await sor.C('cms_leads', data)
|
||||||
|
return {'status': 'ok', 'id': data['id']}
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# CMS Site Config CRUD
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
async def cms_site_config_list(ns=None):
|
||||||
|
"""查询站点配置列表"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
ns = ns or {}
|
||||||
|
ns.setdefault('sort', 'config_group asc, sort_order asc')
|
||||||
|
rows = await sor.R('cms_site_config', ns)
|
||||||
|
return {'rows': rows, 'total': len(rows)}
|
||||||
|
|
||||||
|
|
||||||
|
async def cms_site_config_create(data):
|
||||||
|
"""创建站点配置"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
data['id'] = getID()
|
||||||
|
await sor.C('cms_site_config', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def cms_site_config_update(data):
|
||||||
|
"""更新站点配置"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.U('cms_site_config', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def cms_site_config_delete(data):
|
||||||
|
"""删除站点配置"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.D('cms_site_config', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def get_site_config(group=None):
|
||||||
|
"""获取站点配置(公开接口)"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
ns = {'sort': 'sort_order asc'}
|
||||||
|
if group:
|
||||||
|
ns['config_group'] = group
|
||||||
|
rows = await sor.R('cms_site_config', ns)
|
||||||
|
result = {}
|
||||||
|
for r in rows:
|
||||||
|
g = r.get('config_group', '')
|
||||||
|
if g not in result:
|
||||||
|
result[g] = {}
|
||||||
|
result[g][r.get('config_key', '')] = r.get('config_value', '')
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Public Content APIs
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
async def get_published_content(content_type=None, limit=10):
|
||||||
|
"""获取已发布内容(公开接口)"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
ns = {'status': 'published', 'sort': 'sort_order asc, published_at desc'}
|
||||||
|
if content_type:
|
||||||
|
ns['content_type'] = content_type
|
||||||
|
rows = await sor.R('cms_content', ns)
|
||||||
|
if limit:
|
||||||
|
rows = rows[:limit]
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
async def get_latest_news(limit=2):
|
||||||
|
"""获取最新新闻(公开接口)"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
ns = {'status': 'published', 'content_type': 'news', 'sort': 'published_at desc'}
|
||||||
|
rows = await sor.R('cms_content', ns)
|
||||||
|
return rows[:limit]
|
||||||
|
|
||||||
|
|
||||||
|
async def get_content_detail(content_id):
|
||||||
|
"""获取内容详情(公开接口)"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
ns = {'id': content_id, 'status': 'published'}
|
||||||
|
rows = await sor.R('cms_content', ns)
|
||||||
|
return rows[0] if rows else None
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Content Approval Integration
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
async def submit_content_for_approval(content_id, title, applicant_id):
|
||||||
|
"""提交内容审批(调用钉钉审批流程)"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
# 更新内容状态为pending
|
||||||
|
await sor.U('cms_content', {'id': content_id, 'status': 'pending'})
|
||||||
|
# 调用审批流程
|
||||||
|
result = await submit_approval('content_publish', content_id, title, applicant_id)
|
||||||
|
# 保存审批ID
|
||||||
|
if result and result.get('approval_id'):
|
||||||
|
await sor.U('cms_content', {'id': content_id, 'approval_id': result['approval_id']})
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# DD Approvals CRUD (原 dingdingflow)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
async def dd_approvals_create(data):
|
||||||
|
"""创建审批记录"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
data['id'] = getID()
|
||||||
|
if 'org_id' not in data:
|
||||||
|
data['org_id'] = '0'
|
||||||
|
if 'status' not in data:
|
||||||
|
data['status'] = 'pending'
|
||||||
|
if 'created_at' not in data:
|
||||||
|
data['created_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.C('dd_approvals', data)
|
||||||
|
return {'id': data['id']}
|
||||||
|
|
||||||
|
|
||||||
|
async def dd_approvals_update(data):
|
||||||
|
"""更新审批记录"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
record_id = data.get('id')
|
||||||
|
if not record_id:
|
||||||
|
raise ValueError('id is required for update')
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.U('dd_approvals', data)
|
||||||
|
return {'id': record_id}
|
||||||
|
|
||||||
|
|
||||||
|
async def dd_approvals_delete(data):
|
||||||
|
"""删除审批记录"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.D('dd_approvals', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def dd_approvals_list(ns=None):
|
||||||
|
"""查询审批记录列表"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
ns = ns or {}
|
||||||
|
ns.setdefault('sort', 'created_at desc')
|
||||||
|
rows = await sor.R('dd_approvals', ns)
|
||||||
|
return {'rows': rows, 'total': len(rows)}
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# DD Approval Configs CRUD (原 dingdingflow)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
async def dd_approval_configs_create(data):
|
||||||
|
"""创建审批配置"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
data['id'] = getID()
|
||||||
|
if 'org_id' not in data:
|
||||||
|
data['org_id'] = '0'
|
||||||
|
if 'is_active' not in data:
|
||||||
|
data['is_active'] = '1'
|
||||||
|
now_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
if 'created_at' not in data:
|
||||||
|
data['created_at'] = now_str
|
||||||
|
data['updated_at'] = now_str
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.C('dd_approval_configs', data)
|
||||||
|
return {'id': data['id']}
|
||||||
|
|
||||||
|
|
||||||
|
async def dd_approval_configs_update(data):
|
||||||
|
"""更新审批配置"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
record_id = data.get('id')
|
||||||
|
if not record_id:
|
||||||
|
raise ValueError('id is required for update')
|
||||||
|
data['updated_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.U('dd_approval_configs', data)
|
||||||
|
return {'id': record_id}
|
||||||
|
|
||||||
|
|
||||||
|
async def dd_approval_configs_delete(data):
|
||||||
|
"""删除审批配置"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.D('dd_approval_configs', data)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
async def dd_approval_configs_list(ns=None):
|
||||||
|
"""查询审批配置列表"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
ns = ns or {}
|
||||||
|
ns.setdefault('sort', 'biz_type')
|
||||||
|
rows = await sor.R('dd_approval_configs', ns)
|
||||||
|
return {'rows': rows, 'total': len(rows)}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_approval_config_by_type(org_id, biz_type):
|
||||||
|
"""根据org_id和biz_type获取审批配置"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
rows = await sor.R('dd_approval_configs', {'org_id': org_id, 'biz_type': biz_type})
|
||||||
|
if rows:
|
||||||
|
return rows[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Approval Workflow Business Logic (原 dingdingflow)
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
async def submit_approval(biz_type, biz_id, title, applicant_id, org_id='0'):
|
||||||
|
"""
|
||||||
|
提交审批请求:
|
||||||
|
1. 查找审批配置
|
||||||
|
2. 创建审批记录
|
||||||
|
3. 调用钉钉API创建审批实例
|
||||||
|
4. 保存钉钉实例ID
|
||||||
|
"""
|
||||||
|
client = get_dingtalk_client()
|
||||||
|
|
||||||
|
# 查找审批配置
|
||||||
|
config = await get_approval_config_by_type(org_id, biz_type)
|
||||||
|
if not config:
|
||||||
|
logger.error('No approval config found for org_id=%s, biz_type=%s', org_id, biz_type)
|
||||||
|
return {'success': False, 'message': f'No approval config found for biz_type={biz_type}'}
|
||||||
|
|
||||||
|
process_code = config.get('process_code', '') if isinstance(config, dict) else getattr(config, 'process_code', '') or ''
|
||||||
|
form_config_raw = config.get('form_config', '') if isinstance(config, dict) else getattr(config, 'form_config', '') or ''
|
||||||
|
|
||||||
|
# 从form_config构建表单数据
|
||||||
|
form_data = []
|
||||||
|
if form_config_raw:
|
||||||
|
try:
|
||||||
|
form_config = json.loads(form_config_raw) if isinstance(form_config_raw, str) else form_config_raw
|
||||||
|
if isinstance(form_config, list):
|
||||||
|
form_data = form_config
|
||||||
|
except (json.JSONDecodeError, TypeError) as e:
|
||||||
|
logger.warning('Failed to parse form_config: %s', str(e))
|
||||||
|
|
||||||
|
# 无表单数据时创建最小表单
|
||||||
|
if not form_data:
|
||||||
|
form_data = [
|
||||||
|
{'name': '审批标题', 'value': title},
|
||||||
|
{'name': '业务类型', 'value': biz_type},
|
||||||
|
]
|
||||||
|
|
||||||
|
# 调用钉钉API
|
||||||
|
result = client.create_approval_instance(process_code, form_data, applicant_id)
|
||||||
|
|
||||||
|
if not result['success']:
|
||||||
|
# API失败仍然创建记录
|
||||||
|
approval_data = {
|
||||||
|
'biz_type': biz_type,
|
||||||
|
'biz_id': biz_id,
|
||||||
|
'title': title,
|
||||||
|
'applicant_id': applicant_id,
|
||||||
|
'org_id': org_id,
|
||||||
|
'status': 'pending',
|
||||||
|
'dingtalk_instance_id': '',
|
||||||
|
'comment': f"DingTalk API error: {result.get('errmsg', '')}",
|
||||||
|
}
|
||||||
|
approval = await dd_approvals_create(approval_data)
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': f"DingTalk API failed: {result.get('errmsg', '')}",
|
||||||
|
'approval_id': approval['id'],
|
||||||
|
}
|
||||||
|
|
||||||
|
# 创建审批记录
|
||||||
|
approval_data = {
|
||||||
|
'biz_type': biz_type,
|
||||||
|
'biz_id': biz_id,
|
||||||
|
'title': title,
|
||||||
|
'applicant_id': applicant_id,
|
||||||
|
'org_id': org_id,
|
||||||
|
'status': 'pending',
|
||||||
|
'dingtalk_instance_id': result['instance_id'],
|
||||||
|
}
|
||||||
|
approval = await dd_approvals_create(approval_data)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
'Approval submitted: id=%s, instance=%s, biz=%s/%s',
|
||||||
|
approval['id'], result['instance_id'], biz_type, biz_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': 'Approval submitted successfully',
|
||||||
|
'approval_id': approval['id'],
|
||||||
|
'instance_id': result['instance_id'],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def get_approval_status(approval_id):
|
||||||
|
"""查询钉钉审批最新状态并同步到本地"""
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
|
||||||
|
# 获取本地记录
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
rows = await sor.R('dd_approvals', {'id': approval_id})
|
||||||
|
if not rows:
|
||||||
|
return {'success': False, 'message': 'Approval record not found'}
|
||||||
|
|
||||||
|
record = rows[0]
|
||||||
|
instance_id = record.get('dingtalk_instance_id', '') if isinstance(record, dict) else getattr(record, 'dingtalk_instance_id', '')
|
||||||
|
current_status = record.get('status', '') if isinstance(record, dict) else getattr(record, 'status', '')
|
||||||
|
|
||||||
|
# 已完成无需再查
|
||||||
|
if current_status in ('approved', 'rejected', 'cancelled'):
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'status': current_status,
|
||||||
|
'approval_id': approval_id,
|
||||||
|
'instance_id': instance_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not instance_id:
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'status': current_status,
|
||||||
|
'approval_id': approval_id,
|
||||||
|
'instance_id': '',
|
||||||
|
'message': 'No DingTalk instance ID, cannot sync',
|
||||||
|
}
|
||||||
|
|
||||||
|
# 查询钉钉
|
||||||
|
client = get_dingtalk_client()
|
||||||
|
dt_result = client.get_approval_instance(instance_id)
|
||||||
|
|
||||||
|
if not dt_result['success']:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': f"DingTalk query failed: {dt_result.get('errmsg', '')}",
|
||||||
|
'status': current_status,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 映射钉钉状态
|
||||||
|
dt_status = dt_result.get('status', '')
|
||||||
|
dt_result_val = dt_result.get('result', '')
|
||||||
|
|
||||||
|
new_status = current_status
|
||||||
|
if dt_status == 'COMPLETED':
|
||||||
|
if dt_result_val == 'agree':
|
||||||
|
new_status = 'approved'
|
||||||
|
elif dt_result_val == 'refuse':
|
||||||
|
new_status = 'rejected'
|
||||||
|
elif dt_status == 'TERMINATED':
|
||||||
|
new_status = 'cancelled'
|
||||||
|
|
||||||
|
# 更新本地记录
|
||||||
|
if new_status != current_status:
|
||||||
|
update_data = {'id': approval_id, 'status': new_status}
|
||||||
|
if new_status in ('approved', 'rejected', 'cancelled'):
|
||||||
|
update_data['completed_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
await dd_approvals_update(update_data)
|
||||||
|
logger.info('Approval %s status synced: %s -> %s', approval_id, current_status, new_status)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'status': new_status,
|
||||||
|
'approval_id': approval_id,
|
||||||
|
'instance_id': instance_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_dingtalk_callback(data):
|
||||||
|
"""
|
||||||
|
处理钉钉webhook回调。
|
||||||
|
钉钉在审批状态变化时发送回调。
|
||||||
|
"""
|
||||||
|
logger.info('DingTalk callback received: %s', json.dumps(data, ensure_ascii=False))
|
||||||
|
|
||||||
|
instance_id = data.get('processInstanceId', '')
|
||||||
|
if not instance_id:
|
||||||
|
return {'success': False, 'message': 'Missing processInstanceId'}
|
||||||
|
|
||||||
|
callback_type = data.get('type', '')
|
||||||
|
if callback_type != 'bpms_instance_change':
|
||||||
|
logger.info('Ignoring callback type: %s', callback_type)
|
||||||
|
return {'success': True, 'message': f'Ignored callback type: {callback_type}'}
|
||||||
|
|
||||||
|
# 查找本地审批记录
|
||||||
|
dbname = _get_dbname()
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
rows = await sor.R('dd_approvals', {'dingtalk_instance_id': instance_id})
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
logger.warning('No local approval found for instance_id=%s', instance_id)
|
||||||
|
return {'success': False, 'message': f'No approval found for instance {instance_id}'}
|
||||||
|
|
||||||
|
record = rows[0]
|
||||||
|
record_id = record.get('id', '') if isinstance(record, dict) else getattr(record, 'id', '')
|
||||||
|
current_status = record.get('status', '') if isinstance(record, dict) else getattr(record, 'status', '')
|
||||||
|
|
||||||
|
# 映射回调状态
|
||||||
|
dt_result = data.get('result', '')
|
||||||
|
new_status = current_status
|
||||||
|
if dt_result == 'agree':
|
||||||
|
new_status = 'approved'
|
||||||
|
elif dt_result == 'refuse':
|
||||||
|
new_status = 'rejected'
|
||||||
|
elif callback_type == 'terminate':
|
||||||
|
new_status = 'cancelled'
|
||||||
|
|
||||||
|
# 更新记录
|
||||||
|
if new_status != current_status:
|
||||||
|
update_data = {
|
||||||
|
'id': record_id,
|
||||||
|
'status': new_status,
|
||||||
|
'comment': data.get('remark', ''),
|
||||||
|
}
|
||||||
|
if new_status in ('approved', 'rejected', 'cancelled'):
|
||||||
|
update_data['completed_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
await dd_approvals_update(update_data)
|
||||||
|
logger.info('Callback: approval %s updated to %s', record_id, new_status)
|
||||||
|
|
||||||
|
# 通知CMS内容状态变更
|
||||||
|
biz_type = record.get('biz_type', '') if isinstance(record, dict) else getattr(record, 'biz_type', '')
|
||||||
|
biz_id = record.get('biz_id', '') if isinstance(record, dict) else getattr(record, 'biz_id', '')
|
||||||
|
if biz_type == 'content_publish' and biz_id:
|
||||||
|
content_status = 'published' if new_status == 'approved' else 'draft' if new_status == 'rejected' else ''
|
||||||
|
if content_status:
|
||||||
|
content_update = {'id': biz_id, 'status': content_status}
|
||||||
|
if content_status == 'published':
|
||||||
|
content_update['published_at'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
await sor.U('cms_content', content_update)
|
||||||
|
logger.info('Callback: cms_content %s updated to %s', biz_id, content_status)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': f'Approval {record_id} updated to {new_status}',
|
||||||
|
'approval_id': record_id,
|
||||||
|
'status': new_status,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
# Module Loader
|
||||||
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
def load_cms():
|
||||||
|
"""注册所有CMS模块函数到ServerEnv"""
|
||||||
|
env = ServerEnv()
|
||||||
|
|
||||||
|
# Content CRUD
|
||||||
|
env.cms_content_list = cms_content_list
|
||||||
|
env.cms_content_create = cms_content_create
|
||||||
|
env.cms_content_update = cms_content_update
|
||||||
|
env.cms_content_delete = cms_content_delete
|
||||||
|
|
||||||
|
# Categories CRUD
|
||||||
|
env.cms_categories_list = cms_categories_list
|
||||||
|
env.cms_categories_create = cms_categories_create
|
||||||
|
env.cms_categories_update = cms_categories_update
|
||||||
|
env.cms_categories_delete = cms_categories_delete
|
||||||
|
env.get_category_options = get_category_options
|
||||||
|
|
||||||
|
# Sections CRUD
|
||||||
|
env.cms_sections_list = cms_sections_list
|
||||||
|
env.cms_sections_create = cms_sections_create
|
||||||
|
env.cms_sections_update = cms_sections_update
|
||||||
|
env.cms_sections_delete = cms_sections_delete
|
||||||
|
env.get_visible_sections = get_visible_sections
|
||||||
|
|
||||||
|
# Leads CRUD
|
||||||
|
env.cms_leads_list = cms_leads_list
|
||||||
|
env.cms_leads_create = cms_leads_create
|
||||||
|
env.cms_leads_update = cms_leads_update
|
||||||
|
env.cms_leads_delete = cms_leads_delete
|
||||||
|
env.submit_lead = submit_lead
|
||||||
|
|
||||||
|
# Site Config CRUD
|
||||||
|
env.cms_site_config_list = cms_site_config_list
|
||||||
|
env.cms_site_config_create = cms_site_config_create
|
||||||
|
env.cms_site_config_update = cms_site_config_update
|
||||||
|
env.cms_site_config_delete = cms_site_config_delete
|
||||||
|
env.get_site_config = get_site_config
|
||||||
|
|
||||||
|
# Public Content APIs
|
||||||
|
env.get_published_content = get_published_content
|
||||||
|
env.get_latest_news = get_latest_news
|
||||||
|
env.get_content_detail = get_content_detail
|
||||||
|
env.submit_content_for_approval = submit_content_for_approval
|
||||||
|
|
||||||
|
# DD Approvals CRUD
|
||||||
|
env.dd_approvals_create = dd_approvals_create
|
||||||
|
env.dd_approvals_update = dd_approvals_update
|
||||||
|
env.dd_approvals_delete = dd_approvals_delete
|
||||||
|
env.dd_approvals_list = dd_approvals_list
|
||||||
|
|
||||||
|
# DD Approval Configs CRUD
|
||||||
|
env.dd_approval_configs_create = dd_approval_configs_create
|
||||||
|
env.dd_approval_configs_update = dd_approval_configs_update
|
||||||
|
env.dd_approval_configs_delete = dd_approval_configs_delete
|
||||||
|
env.dd_approval_configs_list = dd_approval_configs_list
|
||||||
|
env.get_approval_config_by_type = get_approval_config_by_type
|
||||||
|
|
||||||
|
# Approval Business Logic
|
||||||
|
env.submit_approval = submit_approval
|
||||||
|
env.get_approval_status = get_approval_status
|
||||||
|
env.handle_dingtalk_callback = handle_dingtalk_callback
|
||||||
|
|
||||||
|
# DingTalk Client
|
||||||
|
env.get_dingtalk_client = get_dingtalk_client
|
||||||
|
|
||||||
|
logger.info('cms module loaded (v%s)', MODULE_VERSION)
|
||||||
|
return True
|
||||||
@ -1,76 +0,0 @@
|
|||||||
{
|
|
||||||
"password_key":"!@#$%^&*(*&^%$QWERTYUIqwertyui234567",
|
|
||||||
"logger": {
|
|
||||||
"name": "cms",
|
|
||||||
"levelname": "info",
|
|
||||||
"logfile": "$[workdir]$/logs/cms.log"
|
|
||||||
},
|
|
||||||
"filesroot": "$[workdir]$/files",
|
|
||||||
"databases": {
|
|
||||||
"ocai_cms": {
|
|
||||||
"driver": "mysql",
|
|
||||||
"async_mode": true,
|
|
||||||
"coding": "utf8",
|
|
||||||
"dbname": "ocai_cms",
|
|
||||||
"kwargs": {
|
|
||||||
"user": "test",
|
|
||||||
"db": "ocai_cms",
|
|
||||||
"password": "SS+C1MDMJrslBwGzYIv3nQ==",
|
|
||||||
"host": "db"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"website": {
|
|
||||||
"paths": [
|
|
||||||
[
|
|
||||||
"$[workdir]$/wwwroot",
|
|
||||||
""
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"$[workdir]$/bricks",
|
|
||||||
"/bricks"
|
|
||||||
]
|
|
||||||
],
|
|
||||||
"host": "0.0.0.0",
|
|
||||||
"port": 9090,
|
|
||||||
"coding": "utf-8",
|
|
||||||
"session_redis": {
|
|
||||||
"url": "redis://127.0.0.1:6379/0"
|
|
||||||
},
|
|
||||||
"indexes": [
|
|
||||||
"index.ui",
|
|
||||||
"index.html",
|
|
||||||
"index.tmpl"
|
|
||||||
],
|
|
||||||
"processors": [
|
|
||||||
[
|
|
||||||
".xlsxds",
|
|
||||||
"xlsxds"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
".sqlds",
|
|
||||||
"sqlds"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
".tmpl",
|
|
||||||
"tmpl"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
".dspy",
|
|
||||||
"dspy"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
".ui",
|
|
||||||
"bui"
|
|
||||||
],
|
|
||||||
[
|
|
||||||
".md",
|
|
||||||
"md"
|
|
||||||
]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"langMapping": {
|
|
||||||
"zh-Hans-CN": "zh-cn",
|
|
||||||
"en-US": "en"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
# dingdingflow - 钉钉审批流程
|
|
||||||
|
|
||||||
为CMS内容发布提供钉钉审批工作流。
|
|
||||||
|
|
||||||
## 数据表
|
|
||||||
- dd_approvals: 审批记录
|
|
||||||
- dd_approval_configs: 审批流程配置
|
|
||||||
|
|
||||||
## 环境变量
|
|
||||||
```
|
|
||||||
DINGTALK_APP_KEY=钉钉应用AppKey
|
|
||||||
DINGTALK_APP_SECRET=钉钉应用AppSecret
|
|
||||||
DINGTALK_AGENT_ID=钉钉应用AgentId
|
|
||||||
```
|
|
||||||
|
|
||||||
未配置时自动进入开发模式(mock响应)。
|
|
||||||
|
|
||||||
## API
|
|
||||||
- POST /dingdingflow/api/submit_approval.dspy - 提交审批
|
|
||||||
- POST /dingdingflow/api/dingtalk_callback.dspy - 钉钉回调(公开)
|
|
||||||
@ -1 +0,0 @@
|
|||||||
# dingdingflow module
|
|
||||||
@ -1,436 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
dingdingflow module initialization.
|
|
||||||
Registers all module functions with ServerEnv for use in .ui and .dspy files.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from ahserver.serverenv import ServerEnv
|
|
||||||
from appPublic.uniqueID import getID
|
|
||||||
from dingdingflow.dingtalk_client import get_dingtalk_client
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
MODULE_NAME = "dingdingflow"
|
|
||||||
MODULE_VERSION = "1.0.0"
|
|
||||||
|
|
||||||
|
|
||||||
def _get_dbname():
|
|
||||||
"""Get the database name for this module."""
|
|
||||||
env = ServerEnv()
|
|
||||||
return env.get_module_dbname(MODULE_NAME)
|
|
||||||
|
|
||||||
|
|
||||||
# ─── CRUD: dd_approvals ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
async def create_dd_approval(data):
|
|
||||||
"""Create a new approval record."""
|
|
||||||
new_id = getID()
|
|
||||||
data["id"] = new_id
|
|
||||||
if "org_id" not in data:
|
|
||||||
data["org_id"] = "0"
|
|
||||||
if "status" not in data:
|
|
||||||
data["status"] = "pending"
|
|
||||||
if "created_at" not in data:
|
|
||||||
data["created_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
dbname = _get_dbname()
|
|
||||||
db = ServerEnv().db
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
await sor.C("dd_approvals", data)
|
|
||||||
return {"id": new_id}
|
|
||||||
|
|
||||||
|
|
||||||
async def update_dd_approval(data):
|
|
||||||
"""Update an existing approval record."""
|
|
||||||
record_id = data.get("id")
|
|
||||||
if not record_id:
|
|
||||||
raise ValueError("id is required for update")
|
|
||||||
|
|
||||||
dbname = _get_dbname()
|
|
||||||
db = ServerEnv().db
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
await sor.U("dd_approvals", {"id": record_id}, data)
|
|
||||||
return {"id": record_id}
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_dd_approval(record_id):
|
|
||||||
"""Delete an approval record by ID."""
|
|
||||||
dbname = _get_dbname()
|
|
||||||
db = ServerEnv().db
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
await sor.D("dd_approvals", {"id": record_id})
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def get_dd_approval(record_id):
|
|
||||||
"""Get a single approval record by ID."""
|
|
||||||
dbname = _get_dbname()
|
|
||||||
db = ServerEnv().db
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
rows = await sor.R("dd_approvals", {"id": record_id})
|
|
||||||
if rows:
|
|
||||||
return rows[0]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def list_dd_approvals(filters=None, page=1, rows=20, sort="created_at desc"):
|
|
||||||
"""List approval records with optional filters."""
|
|
||||||
dbname = _get_dbname()
|
|
||||||
db = ServerEnv().db
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
query_filters = filters or {}
|
|
||||||
ns = {"page": page, "rows": rows, "sort": sort}
|
|
||||||
ns.update(query_filters)
|
|
||||||
result = await sor.R("dd_approvals", query_filters, page=page, rows=rows, sort=sort)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# ─── CRUD: dd_approval_configs ────────────────────────────────────────────────
|
|
||||||
|
|
||||||
async def create_dd_approval_config(data):
|
|
||||||
"""Create a new approval config record."""
|
|
||||||
new_id = getID()
|
|
||||||
data["id"] = new_id
|
|
||||||
if "org_id" not in data:
|
|
||||||
data["org_id"] = "0"
|
|
||||||
if "is_active" not in data:
|
|
||||||
data["is_active"] = "1"
|
|
||||||
if "created_at" not in data:
|
|
||||||
data["created_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
data["updated_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
dbname = _get_dbname()
|
|
||||||
db = ServerEnv().db
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
await sor.C("dd_approval_configs", data)
|
|
||||||
return {"id": new_id}
|
|
||||||
|
|
||||||
|
|
||||||
async def update_dd_approval_config(data):
|
|
||||||
"""Update an existing approval config record."""
|
|
||||||
record_id = data.get("id")
|
|
||||||
if not record_id:
|
|
||||||
raise ValueError("id is required for update")
|
|
||||||
|
|
||||||
data["updated_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
|
|
||||||
dbname = _get_dbname()
|
|
||||||
db = ServerEnv().db
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
await sor.U("dd_approval_configs", {"id": record_id}, data)
|
|
||||||
return {"id": record_id}
|
|
||||||
|
|
||||||
|
|
||||||
async def delete_dd_approval_config(record_id):
|
|
||||||
"""Delete an approval config record by ID."""
|
|
||||||
dbname = _get_dbname()
|
|
||||||
db = ServerEnv().db
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
await sor.D("dd_approval_configs", {"id": record_id})
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
async def get_dd_approval_config(record_id):
|
|
||||||
"""Get a single approval config record by ID."""
|
|
||||||
dbname = _get_dbname()
|
|
||||||
db = ServerEnv().db
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
rows = await sor.R("dd_approval_configs", {"id": record_id})
|
|
||||||
if rows:
|
|
||||||
return rows[0]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
async def get_approval_config_by_type(org_id, biz_type):
|
|
||||||
"""Get approval config by org_id and biz_type (unique constraint)."""
|
|
||||||
dbname = _get_dbname()
|
|
||||||
db = ServerEnv().db
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
rows = await sor.R("dd_approval_configs", {"org_id": org_id, "biz_type": biz_type})
|
|
||||||
if rows:
|
|
||||||
return rows[0]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# ─── Business Logic: Approval Workflow ────────────────────────────────────────
|
|
||||||
|
|
||||||
async def submit_approval(biz_type, biz_id, title, applicant_id, org_id="0"):
|
|
||||||
"""
|
|
||||||
Submit a new approval request.
|
|
||||||
|
|
||||||
1. Look up the approval config for this biz_type
|
|
||||||
2. Create a dd_approvals record
|
|
||||||
3. Call DingTalk API to create the approval instance
|
|
||||||
4. Store the DingTalk instance_id back in the record
|
|
||||||
|
|
||||||
Returns: dict with approval record details
|
|
||||||
"""
|
|
||||||
client = get_dingtalk_client()
|
|
||||||
|
|
||||||
# Look up config
|
|
||||||
config = await get_approval_config_by_type(org_id, biz_type)
|
|
||||||
if not config:
|
|
||||||
logger.error("No approval config found for org_id=%s, biz_type=%s", org_id, biz_type)
|
|
||||||
return {"success": False, "message": f"No approval config found for biz_type={biz_type}"}
|
|
||||||
|
|
||||||
process_code = getattr(config, "process_code", "") or ""
|
|
||||||
agent_id = getattr(config, "agent_id", "") or ""
|
|
||||||
form_config_raw = getattr(config, "form_config", "") or ""
|
|
||||||
|
|
||||||
# Build form data from form_config
|
|
||||||
form_data = []
|
|
||||||
if form_config_raw:
|
|
||||||
try:
|
|
||||||
form_config = json.loads(form_config_raw) if isinstance(form_config_raw, str) else form_config_raw
|
|
||||||
if isinstance(form_config, list):
|
|
||||||
form_data = form_config
|
|
||||||
except (json.JSONDecodeError, TypeError) as e:
|
|
||||||
logger.warning("Failed to parse form_config: %s", str(e))
|
|
||||||
|
|
||||||
# If no form_data, create minimal form with title
|
|
||||||
if not form_data:
|
|
||||||
form_data = [
|
|
||||||
{"name": "审批标题", "value": title},
|
|
||||||
{"name": "业务类型", "value": biz_type},
|
|
||||||
]
|
|
||||||
|
|
||||||
# Call DingTalk API
|
|
||||||
result = client.create_approval_instance(process_code, form_data, applicant_id)
|
|
||||||
|
|
||||||
if not result["success"]:
|
|
||||||
# Still create the record with failed status
|
|
||||||
approval_data = {
|
|
||||||
"biz_type": biz_type,
|
|
||||||
"biz_id": biz_id,
|
|
||||||
"title": title,
|
|
||||||
"applicant_id": applicant_id,
|
|
||||||
"org_id": org_id,
|
|
||||||
"status": "pending",
|
|
||||||
"dingtalk_instance_id": "",
|
|
||||||
"comment": f"DingTalk API error: {result.get('errmsg', '')}",
|
|
||||||
}
|
|
||||||
approval = await create_dd_approval(approval_data)
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"message": f"DingTalk API failed: {result.get('errmsg', '')}",
|
|
||||||
"approval_id": approval["id"],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Create approval record with instance_id
|
|
||||||
approval_data = {
|
|
||||||
"biz_type": biz_type,
|
|
||||||
"biz_id": biz_id,
|
|
||||||
"title": title,
|
|
||||||
"applicant_id": applicant_id,
|
|
||||||
"org_id": org_id,
|
|
||||||
"status": "pending",
|
|
||||||
"dingtalk_instance_id": result["instance_id"],
|
|
||||||
}
|
|
||||||
approval = await create_dd_approval(approval_data)
|
|
||||||
|
|
||||||
logger.info(
|
|
||||||
"Approval submitted: id=%s, instance=%s, biz=%s/%s",
|
|
||||||
approval["id"],
|
|
||||||
result["instance_id"],
|
|
||||||
biz_type,
|
|
||||||
biz_id,
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"message": "Approval submitted successfully",
|
|
||||||
"approval_id": approval["id"],
|
|
||||||
"instance_id": result["instance_id"],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def get_approval_status(approval_id):
|
|
||||||
"""
|
|
||||||
Query DingTalk for the latest approval status and sync to local DB.
|
|
||||||
|
|
||||||
Returns: dict with current status info
|
|
||||||
"""
|
|
||||||
# Get local record
|
|
||||||
record = await get_dd_approval(approval_id)
|
|
||||||
if not record:
|
|
||||||
return {"success": False, "message": "Approval record not found"}
|
|
||||||
|
|
||||||
instance_id = getattr(record, "dingtalk_instance_id", "")
|
|
||||||
current_status = getattr(record, "status", "")
|
|
||||||
|
|
||||||
# If already completed, no need to check DingTalk
|
|
||||||
if current_status in ("approved", "rejected", "cancelled"):
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"status": current_status,
|
|
||||||
"approval_id": approval_id,
|
|
||||||
"instance_id": instance_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
if not instance_id:
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"status": current_status,
|
|
||||||
"approval_id": approval_id,
|
|
||||||
"instance_id": "",
|
|
||||||
"message": "No DingTalk instance ID, cannot sync",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Query DingTalk
|
|
||||||
client = get_dingtalk_client()
|
|
||||||
dt_result = client.get_approval_instance(instance_id)
|
|
||||||
|
|
||||||
if not dt_result["success"]:
|
|
||||||
return {
|
|
||||||
"success": False,
|
|
||||||
"message": f"DingTalk query failed: {dt_result.get('errmsg', '')}",
|
|
||||||
"status": current_status,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Map DingTalk status to local status
|
|
||||||
dt_status = dt_result.get("status", "")
|
|
||||||
dt_result_val = dt_result.get("result", "")
|
|
||||||
|
|
||||||
new_status = current_status
|
|
||||||
if dt_status == "COMPLETED":
|
|
||||||
if dt_result_val == "agree":
|
|
||||||
new_status = "approved"
|
|
||||||
elif dt_result_val == "refuse":
|
|
||||||
new_status = "rejected"
|
|
||||||
elif dt_status == "TERMINATED":
|
|
||||||
new_status = "cancelled"
|
|
||||||
|
|
||||||
# Update local record if status changed
|
|
||||||
if new_status != current_status:
|
|
||||||
update_data = {"status": new_status}
|
|
||||||
if new_status in ("approved", "rejected", "cancelled"):
|
|
||||||
update_data["completed_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
await update_dd_approval({"id": approval_id, **update_data})
|
|
||||||
logger.info("Approval %s status synced: %s -> %s", approval_id, current_status, new_status)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"status": new_status,
|
|
||||||
"approval_id": approval_id,
|
|
||||||
"instance_id": instance_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def handle_dingtalk_callback(data):
|
|
||||||
"""
|
|
||||||
Process DingTalk webhook callback.
|
|
||||||
|
|
||||||
DingTalk sends callbacks when approval status changes.
|
|
||||||
Expected data format:
|
|
||||||
{
|
|
||||||
"processInstanceId": "xxx",
|
|
||||||
"processCode": "xxx",
|
|
||||||
"type": "bpms_instance_change",
|
|
||||||
"result": "agree" / "refuse",
|
|
||||||
"staffId": "xxx",
|
|
||||||
...
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
logger.info("DingTalk callback received: %s", json.dumps(data, ensure_ascii=False))
|
|
||||||
|
|
||||||
instance_id = data.get("processInstanceId", "")
|
|
||||||
if not instance_id:
|
|
||||||
return {"success": False, "message": "Missing processInstanceId"}
|
|
||||||
|
|
||||||
callback_type = data.get("type", "")
|
|
||||||
if callback_type != "bpms_instance_change":
|
|
||||||
logger.info("Ignoring callback type: %s", callback_type)
|
|
||||||
return {"success": True, "message": f"Ignored callback type: {callback_type}"}
|
|
||||||
|
|
||||||
# Find local approval record by DingTalk instance ID
|
|
||||||
dbname = _get_dbname()
|
|
||||||
db = ServerEnv().db
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
rows = await sor.R("dd_approvals", {"dingtalk_instance_id": instance_id})
|
|
||||||
|
|
||||||
if not rows:
|
|
||||||
logger.warning("No local approval found for instance_id=%s", instance_id)
|
|
||||||
return {"success": False, "message": f"No approval found for instance {instance_id}"}
|
|
||||||
|
|
||||||
record = rows[0]
|
|
||||||
record_id = getattr(record, "id", "")
|
|
||||||
current_status = getattr(record, "status", "")
|
|
||||||
|
|
||||||
# Map callback to status
|
|
||||||
dt_result = data.get("result", "")
|
|
||||||
new_status = current_status
|
|
||||||
if dt_result == "agree":
|
|
||||||
new_status = "approved"
|
|
||||||
elif dt_result == "refuse":
|
|
||||||
new_status = "rejected"
|
|
||||||
elif callback_type == "terminate":
|
|
||||||
new_status = "cancelled"
|
|
||||||
|
|
||||||
# Update record
|
|
||||||
if new_status != current_status:
|
|
||||||
update_data = {
|
|
||||||
"id": record_id,
|
|
||||||
"status": new_status,
|
|
||||||
"comment": data.get("remark", ""),
|
|
||||||
}
|
|
||||||
if new_status in ("approved", "rejected", "cancelled"):
|
|
||||||
update_data["completed_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
await update_dd_approval(update_data)
|
|
||||||
logger.info("Callback: approval %s updated to %s", record_id, new_status)
|
|
||||||
|
|
||||||
# Notify entcms module about status change
|
|
||||||
biz_type = getattr(record, "biz_type", "")
|
|
||||||
biz_id = getattr(record, "biz_id", "")
|
|
||||||
if biz_type == "content_publish" and biz_id:
|
|
||||||
content_status = "published" if new_status == "approved" else "draft" if new_status == "rejected" else ""
|
|
||||||
if content_status:
|
|
||||||
content_update = {"id": biz_id, "status": content_status}
|
|
||||||
if content_status == "published":
|
|
||||||
content_update["published_at"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
||||||
async with db.sqlorContext(dbname) as sor:
|
|
||||||
await sor.U("cms_content", content_update)
|
|
||||||
logger.info("Callback: cms_content %s updated to %s", biz_id, content_status)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"message": f"Approval {record_id} updated to {new_status}",
|
|
||||||
"approval_id": record_id,
|
|
||||||
"status": new_status,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# ─── Module Loader ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
def load_dingdingflow():
|
|
||||||
"""Register all dingdingflow functions with ServerEnv."""
|
|
||||||
env = ServerEnv()
|
|
||||||
|
|
||||||
# CRUD functions for dd_approvals
|
|
||||||
env.create_dd_approval = create_dd_approval
|
|
||||||
env.update_dd_approval = update_dd_approval
|
|
||||||
env.delete_dd_approval = delete_dd_approval
|
|
||||||
env.get_dd_approval = get_dd_approval
|
|
||||||
env.list_dd_approvals = list_dd_approvals
|
|
||||||
|
|
||||||
# CRUD functions for dd_approval_configs
|
|
||||||
env.create_dd_approval_config = create_dd_approval_config
|
|
||||||
env.update_dd_approval_config = update_dd_approval_config
|
|
||||||
env.delete_dd_approval_config = delete_dd_approval_config
|
|
||||||
env.get_dd_approval_config = get_dd_approval_config
|
|
||||||
env.get_approval_config_by_type = get_approval_config_by_type
|
|
||||||
|
|
||||||
# Business logic functions
|
|
||||||
env.submit_approval = submit_approval
|
|
||||||
env.get_approval_status = get_approval_status
|
|
||||||
env.handle_dingtalk_callback = handle_dingtalk_callback
|
|
||||||
|
|
||||||
# DingTalk client accessor
|
|
||||||
env.get_dingtalk_client = get_dingtalk_client
|
|
||||||
|
|
||||||
logger.info("dingdingflow module loaded (v%s)", MODULE_VERSION)
|
|
||||||
return True
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
[build-system]
|
|
||||||
requires = ["setuptools>=45", "wheel"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
|
|
||||||
[project]
|
|
||||||
name = "dingdingflow"
|
|
||||||
version = "1.0.0"
|
|
||||||
description = "钉钉审批流程模块 - 内容发布审批工作流"
|
|
||||||
requires-python = ">=3.8"
|
|
||||||
dependencies = [
|
|
||||||
"sqlor",
|
|
||||||
"bricks_for_python",
|
|
||||||
"requests",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
|
||||||
where = ["."]
|
|
||||||
include = ["dingdingflow*"]
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
"""
|
|
||||||
dingdingflow RBAC权限配置 — 已废弃
|
|
||||||
|
|
||||||
dingdingflow模块的wwwroot内容已移到应用根目录的 wwwroot/dingdingflow/ 下。
|
|
||||||
请使用:
|
|
||||||
cd ~/repos/cms && py3/bin/python init_any_permissions.py
|
|
||||||
cd ~/repos/cms && py3/bin/python init_superuser_permissions.py
|
|
||||||
"""
|
|
||||||
@ -1,175 +0,0 @@
|
|||||||
# 开元云科技官网系统架构
|
|
||||||
|
|
||||||
## 项目概述
|
|
||||||
企业官网 + CMS内容管理 + 钉钉审批流程系统,基于Sage平台开发。
|
|
||||||
|
|
||||||
## 模块组成
|
|
||||||
|
|
||||||
### 1. entcms - 企业CMS系统
|
|
||||||
管理官网所有内容:新闻、案例、产品、Banner、商机线索。
|
|
||||||
|
|
||||||
**数据库表 (entcms)**:
|
|
||||||
| 表名 | 用途 |
|
|
||||||
|------|------|
|
|
||||||
| cms_content | 统一内容表(新闻/案例/产品/Banner),带发布审批状态流 |
|
|
||||||
| cms_categories | 内容分类(支持层级,按content_type分组) |
|
|
||||||
| cms_sections | 栏目管理 |
|
|
||||||
| cms_leads | 商机线索(网站访客提交 + 未来AI抽取) |
|
|
||||||
| cms_site_config | 站点配置(Hero标语、页脚信息等KV配置) |
|
|
||||||
|
|
||||||
**目录结构**:
|
|
||||||
```
|
|
||||||
wwwroot/ # 统一前端目录
|
|
||||||
├── index.ui # 官网首页(7个模块:导航/Hero/产品/案例/新闻/页脚/浮动入口)
|
|
||||||
├── products.ui # 产品架构列表
|
|
||||||
├── news.ui # 新闻列表
|
|
||||||
├── news_detail.ui # 新闻详情
|
|
||||||
├── cases.ui # 案例列表
|
|
||||||
├── admin.ui # 管理后台仪表盘
|
|
||||||
├── api/*.dspy # CMS后端API
|
|
||||||
└── dingdingflow/ # 钉钉审批模块
|
|
||||||
├── index.ui
|
|
||||||
├── menu.ui
|
|
||||||
└── api/*.dspy
|
|
||||||
```
|
|
||||||
|
|
||||||
**公开页面 (any权限)**:
|
|
||||||
- `/index.ui` - 官网首页(7个模块:导航/Hero/产品/案例/新闻/页脚/浮动入口)
|
|
||||||
- `/products.ui` - 产品架构列表
|
|
||||||
- `/news.ui` - 新闻列表
|
|
||||||
- `/news_detail.ui` - 新闻详情
|
|
||||||
- `/cases.ui` - 案例列表
|
|
||||||
|
|
||||||
**管理页面 (logined权限)**:
|
|
||||||
- `/admin.ui` - 管理后台仪表盘
|
|
||||||
- `/cms_content_list` - 内容CRUD
|
|
||||||
- `/cms_categories_list` - 分类CRUD
|
|
||||||
- `/cms_sections_list` - 栏目CRUD
|
|
||||||
- `/cms_leads_list` - 线索CRUD
|
|
||||||
- `/cms_site_config_list` - 配置CRUD
|
|
||||||
|
|
||||||
**内容审批流程**:
|
|
||||||
编辑创建内容(草稿) → 提交审批(status=pending) → 钉钉审批 → 审批通过(status=approved) → 发布(status=published)
|
|
||||||
|
|
||||||
### 2. dingdingflow - 钉钉审批流程
|
|
||||||
对接钉钉审批API,为CMS内容发布提供审批工作流。
|
|
||||||
|
|
||||||
**数据库表 (dingdingflow)**:
|
|
||||||
| 表名 | 用途 |
|
|
||||||
|------|------|
|
|
||||||
| dd_approvals | 审批记录(关联业务类型和ID,记录钉钉审批实例ID) |
|
|
||||||
| dd_approval_configs | 审批流程配置(按biz_type配置钉钉模板编码等) |
|
|
||||||
|
|
||||||
**环境变量**:
|
|
||||||
- `DINGTALK_APP_KEY` - 钉钉应用AppKey
|
|
||||||
- `DINGTALK_APP_SECRET` - 钉钉应用AppSecret
|
|
||||||
- `DINGTALK_AGENT_ID` - 钉钉应用AgentId
|
|
||||||
- `DINGTALK_CALLBACK_TOKEN` - 钉钉回调Token
|
|
||||||
|
|
||||||
**开发模式**: 缺少环境变量时自动使用mock响应,不影响CMS功能。
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
|
|
||||||
| 层 | 技术 |
|
|
||||||
|----|------|
|
|
||||||
| 前端 | bricks-framework (JSON UI) + 自定义CSS/JS |
|
|
||||||
| 后端 | ahserver + sqlor + apppublic |
|
|
||||||
| 认证 | rbac (角色权限控制) |
|
|
||||||
| 基础设施 | appbase (公共函数) |
|
|
||||||
| 审批 | 钉钉开放API (预留接口) |
|
|
||||||
| AI能力 | 预留Agent接口(商机抽取) |
|
|
||||||
|
|
||||||
## 前端设计
|
|
||||||
|
|
||||||
### 官网视觉规范
|
|
||||||
- 风格: 极简科技感(参考OpenAI官网)
|
|
||||||
- 主色: #6C5CE7 (紫色)
|
|
||||||
- 渐变: #6C5CE7 → #A29BFE → #74B9FF
|
|
||||||
- 暗色背景: #0a0a0a
|
|
||||||
- 卡片背景: #1A1A1A
|
|
||||||
- 字体: Noto Sans SC
|
|
||||||
- 最大宽度: 1100px
|
|
||||||
- 响应式断点: 768px
|
|
||||||
- 云宝形象: SVG线稿占位符
|
|
||||||
|
|
||||||
### 官网页面结构
|
|
||||||
1. **导航栏** - 固定顶部,毛玻璃效果
|
|
||||||
2. **Hero区** - 品牌Slogan + 脉冲呼吸灯 + 双按钮 + 云宝占位
|
|
||||||
3. **1+N+X产品架构** - 3张可展开卡片
|
|
||||||
4. **成功案例** - 3列网格 + CTA横幅
|
|
||||||
5. **企业动态** - 2条最新新闻 + 查看全部链接
|
|
||||||
6. **页脚** - 版权信息
|
|
||||||
7. **浮动入口** - 云宝头像 + 联系面板(表单提交线索)
|
|
||||||
|
|
||||||
## 目录结构
|
|
||||||
```
|
|
||||||
~/repos/cms/
|
|
||||||
├── entcms/ # CMS模块
|
|
||||||
│ ├── entcms/
|
|
||||||
│ │ ├── __init__.py
|
|
||||||
│ │ └── init.py # 模块初始化 + ServerEnv注册
|
|
||||||
│ ├── wwwroot/
|
|
||||||
│ │ ├── index.ui # 官网首页
|
|
||||||
│ │ ├── news.ui # 新闻列表
|
|
||||||
│ │ ├── news_detail.ui # 新闻详情
|
|
||||||
│ │ ├── cases.ui # 案例列表
|
|
||||||
│ │ ├── admin.ui # 管理后台
|
|
||||||
│ │ ├── menu.ui # 管理菜单
|
|
||||||
│ │ ├── cms_styles.css # 官网样式
|
|
||||||
│ │ ├── cms_scripts.js # 官网交互脚本
|
|
||||||
│ │ └── api/ # 22个.dspy API文件
|
|
||||||
│ ├── models/ # 4个表定义JSON
|
|
||||||
│ ├── json/ # 4个CRUD定义JSON
|
|
||||||
│ ├── init/data.json # 初始化数据
|
|
||||||
│ ├── scripts/load_path.py # RBAC权限配置
|
|
||||||
│ └── pyproject.toml
|
|
||||||
├── dingdingflow/ # 审批模块
|
|
||||||
│ ├── dingdingflow/
|
|
||||||
│ │ ├── __init__.py
|
|
||||||
│ │ ├── init.py
|
|
||||||
│ │ └── dingtalk_client.py # 钉钉API客户端
|
|
||||||
│ ├── wwwroot/
|
|
||||||
│ │ ├── index.ui
|
|
||||||
│ │ ├── menu.ui
|
|
||||||
│ │ └── api/ # 10个.dspy API文件
|
|
||||||
│ ├── models/ # 2个表定义JSON
|
|
||||||
│ ├── json/ # 2个CRUD定义JSON
|
|
||||||
│ ├── scripts/load_path.py
|
|
||||||
│ └── pyproject.toml
|
|
||||||
├── build.sh # 构建脚本
|
|
||||||
└── docs/ # 文档目录
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sage集成步骤
|
|
||||||
|
|
||||||
### 1. app/sage.py
|
|
||||||
```python
|
|
||||||
from entcms.init import load_entcms
|
|
||||||
from dingdingflow.init import load_dingdingflow
|
|
||||||
# 在init()函数中:
|
|
||||||
load_entcms()
|
|
||||||
load_dingdingflow()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. build.sh
|
|
||||||
```bash
|
|
||||||
for m in ... entcms dingdingflow
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. RBAC权限
|
|
||||||
```bash
|
|
||||||
cd ~/repos/sage
|
|
||||||
./py3/bin/python ~/repos/cms/entcms/scripts/load_path.py
|
|
||||||
./py3/bin/python ~/repos/cms/dingdingflow/scripts/load_path.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 数据库
|
|
||||||
```bash
|
|
||||||
cd ~/repos/cms/entcms && cat mysql.ddl.sql | mysql -u root -p sage
|
|
||||||
cd ~/repos/cms/dingdingflow && cat mysql.ddl.sql | mysql -u root -p sage
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 重启
|
|
||||||
```bash
|
|
||||||
cd ~/repos/sage && ./stop.sh && ./start.sh
|
|
||||||
```
|
|
||||||
@ -1,93 +0,0 @@
|
|||||||
# 测试用例
|
|
||||||
|
|
||||||
## 一、entcms模块测试
|
|
||||||
|
|
||||||
### 1.1 数据库表验证
|
|
||||||
| # | 测试项 | 预期 | 状态 |
|
|
||||||
|---|--------|------|------|
|
|
||||||
| T01 | cms_content表创建 | DDL执行成功 | ⬜ 待执行 |
|
|
||||||
| T02 | cms_categories表创建 | DDL执行成功 | ⬜ 待执行 |
|
|
||||||
| T03 | cms_leads表创建 | DDL执行成功 | ⬜ 待执行 |
|
|
||||||
| T04 | cms_site_config表创建 | DDL执行成功 | ⬜ 待执行 |
|
|
||||||
| T05 | 初始化数据导入 | 10条分类+5条配置写入成功 | ⬜ 待执行 |
|
|
||||||
|
|
||||||
### 1.2 CRUD API测试
|
|
||||||
| # | 测试项 | 预期 | 状态 |
|
|
||||||
|---|--------|------|------|
|
|
||||||
| T06 | 创建新闻内容 | 返回Message成功 | ⬜ 待执行 |
|
|
||||||
| T07 | 创建产品内容 | 返回Message成功 | ⬜ 待执行 |
|
|
||||||
| T08 | 创建案例内容 | 返回Message成功 | ⬜ 待执行 |
|
|
||||||
| T09 | 查询内容列表 | 返回rows+total | ⬜ 待执行 |
|
|
||||||
| T10 | 按content_type筛选 | 只返回指定类型 | ⬜ 待执行 |
|
|
||||||
| T11 | 按status筛选 | 只返回指定状态 | ⬜ 待执行 |
|
|
||||||
| T12 | data_filter搜索 | LIKE/=操作符正常 | ⬜ 待执行 |
|
|
||||||
| T13 | 更新内容 | 字段更新成功 | ⬜ 待执行 |
|
|
||||||
| T14 | 删除内容 | 记录删除 | ⬜ 待执行 |
|
|
||||||
| T15 | 创建分类 | 返回成功 | ⬜ 待执行 |
|
|
||||||
| T16 | 分类下拉选项API | 返回value/text数组 | ⬜ 待执行 |
|
|
||||||
| T17 | 创建线索 | 返回成功 | ⬜ 待执行 |
|
|
||||||
| T18 | 线索列表 | 返回rows+total | ⬜ 待执行 |
|
|
||||||
| T19 | 更新线索状态 | 状态更新成功 | ⬜ 待执行 |
|
|
||||||
| T20 | 站点配置CRUD | 增删改查正常 | ⬜ 待执行 |
|
|
||||||
|
|
||||||
### 1.3 公开API测试
|
|
||||||
| # | 测试项 | 预期 | 状态 |
|
|
||||||
|---|--------|------|------|
|
|
||||||
| T21 | 提交线索(无需登录) | 返回成功消息 | ⬜ 待执行 |
|
|
||||||
| T22 | 获取已发布内容 | 只返回status=published | ⬜ 待执行 |
|
|
||||||
| T23 | 获取最新新闻 | 按时间倒序,limit生效 | ⬜ 待执行 |
|
|
||||||
| T24 | 获取内容详情 | 返回单条完整数据 | ⬜ 待执行 |
|
|
||||||
| T25 | 获取站点配置 | 按group分组返回 | ⬜ 待执行 |
|
|
||||||
|
|
||||||
### 1.4 前端页面测试
|
|
||||||
| # | 测试项 | 预期 | 状态 |
|
|
||||||
|---|--------|------|------|
|
|
||||||
| T26 | 首页加载 | 所有7个section渲染正常 | ⬜ 待执行 |
|
|
||||||
| T27 | Hero呼吸灯动画 | CSS动画正常运行 | ⬜ 待执行 |
|
|
||||||
| T28 | 产品卡片点击展开 | 点击展开/收起详情 | ⬜ 待执行 |
|
|
||||||
| T29 | 案例卡片hover效果 | 上移4px+边框变色 | ⬜ 待执行 |
|
|
||||||
| T30 | 浮动入口交互 | 悬停气泡+点击面板 | ⬜ 待执行 |
|
|
||||||
| T31 | 线索表单提交 | 数据写入cms_leads | ⬜ 待执行 |
|
|
||||||
| T32 | 导航锚点跳转 | 平滑滚动到目标section | ⬜ 待执行 |
|
|
||||||
| T33 | 新闻列表页 | 显示所有新闻 | ⬜ 待执行 |
|
|
||||||
| T34 | 新闻详情页 | 显示单条文章 | ⬜ 待执行 |
|
|
||||||
| T35 | 案例列表页 | 显示所有案例 | ⬜ 待执行 |
|
|
||||||
| T36 | 响应式-桌面端 | 3列grid,1100px最大宽度 | ⬜ 待执行 |
|
|
||||||
| T37 | 响应式-移动端 | 单列堆叠,32px标题 | ⬜ 待执行 |
|
|
||||||
| T38 | 滚动动画 | fade-in元素可见时出现 | ⬜ 待执行 |
|
|
||||||
|
|
||||||
### 1.5 RBAC权限测试
|
|
||||||
| # | 测试项 | 预期 | 状态 |
|
|
||||||
|---|--------|------|------|
|
|
||||||
| T39 | 未登录访问首页 | 200正常显示 | ⬜ 待执行 |
|
|
||||||
| T40 | 未登录提交线索 | 200正常写入 | ⬜ 待执行 |
|
|
||||||
| T41 | 未登录访问管理页 | 401拒绝 | ⬜ 待执行 |
|
|
||||||
| T42 | 已登录访问管理页 | 200正常显示 | ⬜ 待执行 |
|
|
||||||
| T43 | 已登录CRUD操作 | 正常执行 | ⬜ 待执行 |
|
|
||||||
|
|
||||||
## 二、dingdingflow模块测试
|
|
||||||
|
|
||||||
### 2.1 审批流程测试
|
|
||||||
| # | 测试项 | 预期 | 状态 |
|
|
||||||
|---|--------|------|------|
|
|
||||||
| T44 | 提交审批(dd_approvals写入) | 记录创建,status=pending | ⬜ 待执行 |
|
|
||||||
| T45 | 开发模式(无钉钉凭证) | mock响应,不影响流程 | ⬜ 待执行 |
|
|
||||||
| T46 | 获取审批状态 | 返回当前状态 | ⬜ 待执行 |
|
|
||||||
| T47 | 钉钉回调(审批通过) | 状态更新为approved | ⬜ 待执行 |
|
|
||||||
| T48 | 钉钉回调(审批拒绝) | 状态更新为rejected | ⬜ 待执行 |
|
|
||||||
| T49 | 审批配置CRUD | 增删改查正常 | ⬜ 待执行 |
|
|
||||||
|
|
||||||
### 2.2 集成测试
|
|
||||||
| # | 测试项 | 预期 | 状态 |
|
|
||||||
|---|--------|------|------|
|
|
||||||
| T50 | CMS提交审批→dingdingflow | 内容状态变pending,审批记录创建 | ⬜ 待执行 |
|
|
||||||
| T51 | 审批通过→CMS状态更新 | 内容状态变approved | ⬜ 待执行 |
|
|
||||||
| T52 | 审批拒绝→CMS状态不变 | 内容保持pending | ⬜ 待执行 |
|
|
||||||
| T53 | dingdingflow未安装→CMS降级 | CMS提示审批模块未安装 | ⬜ 待执行 |
|
|
||||||
|
|
||||||
## 测试汇总
|
|
||||||
- 总用例数: 53
|
|
||||||
- 通过: 0
|
|
||||||
- 失败: 0
|
|
||||||
- 待执行: 53
|
|
||||||
- 通过率: 待部署后统计
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
# 开发日志
|
|
||||||
|
|
||||||
## 2026-05-27 - 项目初始化与核心开发
|
|
||||||
|
|
||||||
### 范围
|
|
||||||
企业官网CMS系统 (entcms + dingdingflow) 从零搭建。
|
|
||||||
|
|
||||||
### 完成内容
|
|
||||||
|
|
||||||
**entcms模块**:
|
|
||||||
- 4个数据库表定义 (cms_content, cms_categories, cms_leads, cms_site_config)
|
|
||||||
- init.py 模块初始化 + 25个ServerEnv注册函数
|
|
||||||
- 4个CRUD JSON定义
|
|
||||||
- 22个.dspy API文件 (含公开API和data_filter支持)
|
|
||||||
- 4个公开页面 (index.ui, news.ui, news_detail.ui, cases.ui)
|
|
||||||
- 1个管理后台 (admin.ui)
|
|
||||||
- 1个菜单 (menu.ui)
|
|
||||||
- 完整营销站点CSS (cms_styles.css) + 交互JS (cms_scripts.js)
|
|
||||||
- RBAC权限配置脚本
|
|
||||||
- 初始化数据 (10条分类 + 5条站点配置)
|
|
||||||
|
|
||||||
**dingdingflow模块**:
|
|
||||||
- 2个数据库表定义 (dd_approvals, dd_approval_configs)
|
|
||||||
- init.py + dingtalk_client.py (钉钉API客户端)
|
|
||||||
- 2个CRUD JSON定义
|
|
||||||
- 10个.dspy API文件 (含公开回调endpoint)
|
|
||||||
- 管理UI (index.ui, menu.ui)
|
|
||||||
- RBAC权限配置脚本
|
|
||||||
- 开发模式: 无凭证时自动mock
|
|
||||||
|
|
||||||
**基础设施**:
|
|
||||||
- build.sh 构建脚本
|
|
||||||
- pyproject.toml x2
|
|
||||||
- 架构文档
|
|
||||||
- 53条测试用例
|
|
||||||
|
|
||||||
### 技术决策
|
|
||||||
1. 官网前端使用bricks框架 + Html widget渲染营销页面内容
|
|
||||||
2. 自定义CSS/JS实现营销设计(暗色主题、渐变、动画)
|
|
||||||
3. 统一cms_content表存储所有内容类型,通过content_type区分
|
|
||||||
4. 钉钉API凭证从环境变量获取,开发模式mock响应
|
|
||||||
5. 线索表预留raw_text字段用于未来AI商机抽取
|
|
||||||
|
|
||||||
### 当前状态
|
|
||||||
- 代码完整,待部署到Sage进行集成测试
|
|
||||||
- 分支: main (首次提交)
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
# entcms - 企业CMS系统
|
|
||||||
|
|
||||||
管理开元云科技官网所有内容。
|
|
||||||
|
|
||||||
## 数据表
|
|
||||||
- cms_content: 内容(新闻/案例/产品/Banner)
|
|
||||||
- cms_categories: 分类
|
|
||||||
- cms_leads: 商机线索
|
|
||||||
- cms_site_config: 站点配置
|
|
||||||
|
|
||||||
## 公开页面 (无需登录)
|
|
||||||
- /entcms/index.ui - 官网首页
|
|
||||||
- /entcms/news.ui - 新闻列表
|
|
||||||
- /entcms/news_detail.ui - 新闻详情
|
|
||||||
- /entcms/cases.ui - 案例列表
|
|
||||||
|
|
||||||
## 管理页面 (需登录)
|
|
||||||
- /entcms/admin.ui - 管理后台
|
|
||||||
- /entcms/cms_content_list - 内容管理
|
|
||||||
- /entcms/cms_categories_list - 分类管理
|
|
||||||
- /entcms/cms_leads_list - 线索管理
|
|
||||||
- /entcms/cms_site_config_list - 配置管理
|
|
||||||
@ -1,328 +0,0 @@
|
|||||||
"""
|
|
||||||
entcms - 企业CMS系统模块
|
|
||||||
企业官网内容管理:新闻、案例、产品、Banner、商机线索
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
from ahserver.serverenv import ServerEnv
|
|
||||||
from appPublic.uniqueID import getID
|
|
||||||
from sqlor.dbpools import DBPools
|
|
||||||
|
|
||||||
MODULE_NAME = "entcms"
|
|
||||||
MODULE_VERSION = "1.0.0"
|
|
||||||
|
|
||||||
DBNAME = "ocai_cms"
|
|
||||||
|
|
||||||
|
|
||||||
# ===== CMS Content CRUD =====
|
|
||||||
async def cms_content_list(ns=None):
|
|
||||||
"""查询内容列表"""
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
ns = ns or {}
|
|
||||||
ns.setdefault('sort', 'sort_order asc, created_at desc')
|
|
||||||
rows = await sor.R('cms_content', ns)
|
|
||||||
total = len(rows)
|
|
||||||
return {'rows': rows, 'total': total}
|
|
||||||
|
|
||||||
|
|
||||||
async def cms_content_create(data):
|
|
||||||
"""创建内容"""
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
data['id'] = getID()
|
|
||||||
await sor.C('cms_content', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def cms_content_update(data):
|
|
||||||
"""更新内容"""
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
await sor.U('cms_content', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def cms_content_delete(data):
|
|
||||||
"""删除内容"""
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
await sor.D('cms_content', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
# ===== CMS Categories CRUD =====
|
|
||||||
async def cms_categories_list(ns=None):
|
|
||||||
"""查询分类列表"""
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
ns = ns or {}
|
|
||||||
ns.setdefault('sort', 'sort_order asc')
|
|
||||||
rows = await sor.R('cms_categories', ns)
|
|
||||||
return {'rows': rows, 'total': len(rows)}
|
|
||||||
|
|
||||||
|
|
||||||
async def cms_categories_create(data):
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
data['id'] = getID()
|
|
||||||
await sor.C('cms_categories', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def cms_categories_update(data):
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
await sor.U('cms_categories', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def cms_categories_delete(data):
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
await sor.D('cms_categories', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def get_category_options(content_type=None):
|
|
||||||
"""获取分类下拉选项"""
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
ns = {'sort': 'sort_order asc'}
|
|
||||||
if content_type:
|
|
||||||
ns['content_type'] = content_type
|
|
||||||
rows = await sor.R('cms_categories', ns)
|
|
||||||
options = [{'value': r['id'], 'text': r['name']} for r in rows]
|
|
||||||
return options
|
|
||||||
|
|
||||||
|
|
||||||
# ===== CMS Leads CRUD =====
|
|
||||||
async def cms_leads_list(ns=None):
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
ns = ns or {}
|
|
||||||
ns.setdefault('sort', 'created_at desc')
|
|
||||||
rows = await sor.R('cms_leads', ns)
|
|
||||||
return {'rows': rows, 'total': len(rows)}
|
|
||||||
|
|
||||||
|
|
||||||
async def cms_leads_create(data):
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
data['id'] = getID()
|
|
||||||
await sor.C('cms_leads', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def cms_leads_update(data):
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
await sor.U('cms_leads', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def cms_leads_delete(data):
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
await sor.D('cms_leads', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def submit_lead(data):
|
|
||||||
"""公开接口 - 网站访客提交线索"""
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
data['id'] = getID()
|
|
||||||
data.setdefault('status', 'new')
|
|
||||||
data.setdefault('source', 'website')
|
|
||||||
await sor.C('cms_leads', data)
|
|
||||||
return {'status': 'ok', 'id': data['id']}
|
|
||||||
|
|
||||||
|
|
||||||
# ===== CMS Sections CRUD =====
|
|
||||||
async def cms_sections_list(ns=None):
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
ns = ns or {}
|
|
||||||
ns.setdefault('sort', 'sort_order asc')
|
|
||||||
rows = await sor.R('cms_sections', ns)
|
|
||||||
return {'rows': rows, 'total': len(rows)}
|
|
||||||
|
|
||||||
async def cms_sections_create(data):
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
data['id'] = getID()
|
|
||||||
await sor.C('cms_sections', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
async def cms_sections_update(data):
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
await sor.U('cms_sections', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
async def cms_sections_delete(data):
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
await sor.D('cms_sections', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
async def get_visible_sections():
|
|
||||||
"""获取所有可见栏目(公开接口)"""
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
ns = {'is_visible': '1', 'sort': 'sort_order asc'}
|
|
||||||
rows = await sor.R('cms_sections', ns)
|
|
||||||
import json as _json
|
|
||||||
for r in rows:
|
|
||||||
for field in ['display_config', 'style_config', 'static_content']:
|
|
||||||
v = r.get(field, None)
|
|
||||||
if v and isinstance(v, str):
|
|
||||||
try: r[field] = _json.loads(v)
|
|
||||||
except: pass
|
|
||||||
return rows
|
|
||||||
|
|
||||||
# ===== CMS Site Config CRUD =====
|
|
||||||
async def cms_site_config_list(ns=None):
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
ns = ns or {}
|
|
||||||
ns.setdefault('sort', 'config_group asc, sort_order asc')
|
|
||||||
rows = await sor.R('cms_site_config', ns)
|
|
||||||
return {'rows': rows, 'total': len(rows)}
|
|
||||||
|
|
||||||
|
|
||||||
async def cms_site_config_create(data):
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
data['id'] = getID()
|
|
||||||
await sor.C('cms_site_config', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def cms_site_config_update(data):
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
await sor.U('cms_site_config', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def cms_site_config_delete(data):
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
await sor.D('cms_site_config', data)
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
async def get_site_config(group=None):
|
|
||||||
"""获取站点配置(公开接口)"""
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
ns = {'sort': 'sort_order asc'}
|
|
||||||
if group:
|
|
||||||
ns['config_group'] = group
|
|
||||||
rows = await sor.R('cms_site_config', ns)
|
|
||||||
result = {}
|
|
||||||
for r in rows:
|
|
||||||
g = r.get('config_group', '')
|
|
||||||
if g not in result:
|
|
||||||
result[g] = {}
|
|
||||||
result[g][r.get('config_key', '')] = r.get('config_value', '')
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# ===== Public Content APIs =====
|
|
||||||
async def get_published_content(content_type=None, limit=10):
|
|
||||||
"""获取已发布内容(公开接口)"""
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
ns = {'status': 'published', 'sort': 'sort_order asc, published_at desc'}
|
|
||||||
if content_type:
|
|
||||||
ns['content_type'] = content_type
|
|
||||||
rows = await sor.R('cms_content', ns)
|
|
||||||
if limit:
|
|
||||||
rows = rows[:limit]
|
|
||||||
return rows
|
|
||||||
|
|
||||||
|
|
||||||
async def get_latest_news(limit=2):
|
|
||||||
"""获取最新新闻(公开接口)"""
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
ns = {'status': 'published', 'content_type': 'news', 'sort': 'published_at desc'}
|
|
||||||
rows = await sor.R('cms_content', ns)
|
|
||||||
return rows[:limit]
|
|
||||||
|
|
||||||
|
|
||||||
async def get_content_detail(content_id):
|
|
||||||
"""获取内容详情(公开接口)"""
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
ns = {'id': content_id, 'status': 'published'}
|
|
||||||
rows = await sor.R('cms_content', ns)
|
|
||||||
return rows[0] if rows else None
|
|
||||||
|
|
||||||
|
|
||||||
# ===== Submit for approval =====
|
|
||||||
async def submit_content_for_approval(content_id, title, applicant_id):
|
|
||||||
"""提交内容审批(调用dingdingflow)"""
|
|
||||||
db = DBPools()
|
|
||||||
async with db.sqlorContext(DBNAME) as sor:
|
|
||||||
# 更新内容状态为pending
|
|
||||||
await sor.U('cms_content', {'id': content_id, 'status': 'pending'})
|
|
||||||
# 调用dingdingflow的submit_approval
|
|
||||||
try:
|
|
||||||
from dingdingflow.init import submit_approval
|
|
||||||
result = await submit_approval('content_publish', content_id, title, applicant_id)
|
|
||||||
# 保存审批ID
|
|
||||||
if result and result.get('approval_id'):
|
|
||||||
await sor.U('cms_content', {'id': content_id, 'approval_id': result['approval_id']})
|
|
||||||
return result
|
|
||||||
except ImportError:
|
|
||||||
return {'status': 'error', 'message': 'dingdingflow模块未安装'}
|
|
||||||
|
|
||||||
|
|
||||||
def load_entcms():
|
|
||||||
"""注册所有函数到ServerEnv"""
|
|
||||||
env = ServerEnv()
|
|
||||||
|
|
||||||
# Content CRUD
|
|
||||||
env.cms_content_list = cms_content_list
|
|
||||||
env.cms_content_create = cms_content_create
|
|
||||||
env.cms_content_update = cms_content_update
|
|
||||||
env.cms_content_delete = cms_content_delete
|
|
||||||
|
|
||||||
# Categories CRUD
|
|
||||||
env.cms_categories_list = cms_categories_list
|
|
||||||
env.cms_categories_create = cms_categories_create
|
|
||||||
env.cms_categories_update = cms_categories_update
|
|
||||||
env.cms_categories_delete = cms_categories_delete
|
|
||||||
env.get_category_options = get_category_options
|
|
||||||
|
|
||||||
# Leads CRUD
|
|
||||||
env.cms_leads_list = cms_leads_list
|
|
||||||
env.cms_leads_create = cms_leads_create
|
|
||||||
env.cms_leads_update = cms_leads_update
|
|
||||||
env.cms_leads_delete = cms_leads_delete
|
|
||||||
env.submit_lead = submit_lead
|
|
||||||
|
|
||||||
# Site Config CRUD
|
|
||||||
env.cms_site_config_list = cms_site_config_list
|
|
||||||
env.cms_site_config_create = cms_site_config_create
|
|
||||||
env.cms_site_config_update = cms_site_config_update
|
|
||||||
env.cms_site_config_delete = cms_site_config_delete
|
|
||||||
env.get_site_config = get_site_config
|
|
||||||
|
|
||||||
# Sections CRUD
|
|
||||||
env.cms_sections_list = cms_sections_list
|
|
||||||
env.cms_sections_create = cms_sections_create
|
|
||||||
env.cms_sections_update = cms_sections_update
|
|
||||||
env.cms_sections_delete = cms_sections_delete
|
|
||||||
env.get_visible_sections = get_visible_sections
|
|
||||||
|
|
||||||
# Public APIs
|
|
||||||
env.get_published_content = get_published_content
|
|
||||||
env.get_latest_news = get_latest_news
|
|
||||||
env.get_content_detail = get_content_detail
|
|
||||||
env.submit_content_for_approval = submit_content_for_approval
|
|
||||||
|
|
||||||
return True
|
|
||||||
@ -1,198 +0,0 @@
|
|||||||
{
|
|
||||||
"cms_categories": [
|
|
||||||
{
|
|
||||||
"id": "cat_product_platform",
|
|
||||||
"org_id": "0",
|
|
||||||
"name": "AI平台",
|
|
||||||
"content_type": "product",
|
|
||||||
"sort_order": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cat_product_model",
|
|
||||||
"org_id": "0",
|
|
||||||
"name": "行业模型",
|
|
||||||
"content_type": "product",
|
|
||||||
"sort_order": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cat_product_agent",
|
|
||||||
"org_id": "0",
|
|
||||||
"name": "智能体",
|
|
||||||
"content_type": "product",
|
|
||||||
"sort_order": 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cat_case_mfg",
|
|
||||||
"org_id": "0",
|
|
||||||
"name": "智能制造",
|
|
||||||
"content_type": "case",
|
|
||||||
"sort_order": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cat_case_finance",
|
|
||||||
"org_id": "0",
|
|
||||||
"name": "金融科技",
|
|
||||||
"content_type": "case",
|
|
||||||
"sort_order": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cat_case_healthcare",
|
|
||||||
"org_id": "0",
|
|
||||||
"name": "医疗健康",
|
|
||||||
"content_type": "case",
|
|
||||||
"sort_order": 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cat_case_education",
|
|
||||||
"org_id": "0",
|
|
||||||
"name": "教育培训",
|
|
||||||
"content_type": "case",
|
|
||||||
"sort_order": 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cat_news_company",
|
|
||||||
"org_id": "0",
|
|
||||||
"name": "公司动态",
|
|
||||||
"content_type": "news",
|
|
||||||
"sort_order": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cat_news_industry",
|
|
||||||
"org_id": "0",
|
|
||||||
"name": "行业资讯",
|
|
||||||
"content_type": "news",
|
|
||||||
"sort_order": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cat_news_product",
|
|
||||||
"org_id": "0",
|
|
||||||
"name": "产品更新",
|
|
||||||
"content_type": "news",
|
|
||||||
"sort_order": 3
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cms_site_config": [
|
|
||||||
{
|
|
||||||
"id": "cfg_hero_slogan",
|
|
||||||
"org_id": "0",
|
|
||||||
"config_group": "hero",
|
|
||||||
"config_key": "slogan",
|
|
||||||
"config_value": "一个平台,千行百业 智能跃迁",
|
|
||||||
"config_type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cfg_hero_subtitle",
|
|
||||||
"org_id": "0",
|
|
||||||
"config_group": "hero",
|
|
||||||
"config_key": "subtitle",
|
|
||||||
"config_value": "基于东数西算国家战略,打造新一代AI智能体服务平台",
|
|
||||||
"config_type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cfg_hero_tag",
|
|
||||||
"org_id": "0",
|
|
||||||
"config_group": "hero",
|
|
||||||
"config_key": "tag_text",
|
|
||||||
"config_value": "AI 智能体服务平台",
|
|
||||||
"config_type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cfg_footer_copyright",
|
|
||||||
"org_id": "0",
|
|
||||||
"config_group": "footer",
|
|
||||||
"config_key": "copyright",
|
|
||||||
"config_value": "© 2026 开元云科技 · 国家级高新技术企业 · 专精特新企业",
|
|
||||||
"config_type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "cfg_contact_company",
|
|
||||||
"org_id": "0",
|
|
||||||
"config_group": "contact",
|
|
||||||
"config_key": "company_name",
|
|
||||||
"config_value": "开元云科技",
|
|
||||||
"config_type": "text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"cms_sections": [
|
|
||||||
{
|
|
||||||
"id": "sec_hero",
|
|
||||||
"org_id": "0",
|
|
||||||
"section_key": "hero",
|
|
||||||
"title": "首屏Hero",
|
|
||||||
"section_type": "hero",
|
|
||||||
"content_type": "",
|
|
||||||
"sort_order": 1,
|
|
||||||
"is_visible": "1",
|
|
||||||
"display_config": "{\"layout\": \"fullscreen\", \"height\": \"100vh\", \"bg_glow\": true}",
|
|
||||||
"style_config": "{\"gradient\": \"#6C5CE7,#A29BFE,#74B9FF\", \"title_size\": \"56px\"}",
|
|
||||||
"static_content": "{\"tag_text\": \"AI \\u667a\\u80fd\\u4f53\\u670d\\u52a1\\u5e73\\u53f0\", \"slogan\": \"\\u4e00\\u4e2a\\u5e73\\u53f0\\uff0c\\u5343\\u884c\\u767e\\u4e1a \\u667a\\u80fd\\u8dc3\\u8fc1\", \"subtitle\": \"\\u57fa\\u4e8e\\u4e1c\\u6570\\u897f\\u7b97\\u56fd\\u5bb6\\u6218\\u7565\\uff0c\\u6253\\u9020\\u65b0\\u4e00\\u4ee3AI\\u667a\\u80fd\\u4f53\\u670d\\u52a1\\u5e73\\u53f0\", \"btn_primary\": \"\\u8054\\u7cfb\\u9500\\u552e\", \"btn_secondary\": \"\\u4e86\\u89e3\\u4ea7\\u54c1\\u67b6\\u6784\"}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "sec_products",
|
|
||||||
"org_id": "0",
|
|
||||||
"section_key": "products",
|
|
||||||
"title": "1+N+X 产品架构",
|
|
||||||
"subtitle": "一个AI平台 + N个行业模型 + X个智能体",
|
|
||||||
"section_type": "cards",
|
|
||||||
"content_type": "product",
|
|
||||||
"sort_order": 2,
|
|
||||||
"is_visible": "1",
|
|
||||||
"display_config": "{\"columns\": 3, \"expandable\": true, \"icon_position\": \"top\"}",
|
|
||||||
"style_config": "{\"card_bg\": \"#1A1A1A\", \"card_border\": \"#222\", \"hover_border\": \"#6C5CE7\"}",
|
|
||||||
"static_content": "{\"cards\": [{\"icon\": \"\\ud83e\\udde0\", \"title\": \"1 \\u4e2a AI \\u5e73\\u53f0\", \"desc\": \"\\u7edf\\u4e00AI\\u57fa\\u7840\\u8bbe\\u65bd\\u5e73\\u53f0\\uff0c\\u63d0\\u4f9b\\u7b97\\u529b\\u8c03\\u5ea6\\u3001\\u6a21\\u578b\\u7ba1\\u7406\\u3001\\u667a\\u80fd\\u4f53\\u7f16\\u6392\\u7b49\\u6838\\u5fc3\\u80fd\\u529b\", \"detail\": \"\\u57fa\\u4e8e\\u4e1c\\u6570\\u897f\\u7b97\\u56fd\\u5bb6\\u6218\\u7565\\u90e8\\u7f72\\uff0c\\u63d0\\u4f9b\\u9ad8\\u6027\\u80fd\\u3001\\u4f4e\\u6210\\u672c\\u7684AI\\u7b97\\u529b\\u670d\\u52a1\\u3002\"}, {\"icon\": \"\\ud83c\\udfed\", \"title\": \"N \\u4e2a\\u884c\\u4e1a\\u6a21\\u578b\", \"desc\": \"\\u9488\\u5bf9\\u5236\\u9020\\u3001\\u91d1\\u878d\\u3001\\u533b\\u7597\\u3001\\u6559\\u80b2\\u7b49\\u884c\\u4e1a\\u6df1\\u5ea6\\u5b9a\\u5236\\u7684\\u4e13\\u4e1aAI\\u6a21\\u578b\", \"detail\": \"\\u6bcf\\u4e2a\\u884c\\u4e1a\\u6a21\\u578b\\u90fd\\u7ecf\\u8fc7\\u5927\\u91cf\\u884c\\u4e1a\\u6570\\u636e\\u8bad\\u7ec3\\u548c\\u5fae\\u8c03\\uff0c\\u7406\\u89e3\\u884c\\u4e1a\\u672f\\u8bed\\u548c\\u4e1a\\u52a1\\u6d41\\u7a0b\\u3002\"}, {\"icon\": \"\\ud83e\\udd16\", \"title\": \"X \\u4e2a\\u667a\\u80fd\\u4f53\", \"desc\": \"\\u7075\\u6d3b\\u7ec4\\u5408\\u7684\\u667a\\u80fd\\u4f53\\u5e94\\u7528\\uff0c\\u8986\\u76d6\\u5ba2\\u670d\\u3001\\u5199\\u4f5c\\u3001\\u5206\\u6790\\u3001\\u7f16\\u7a0b\\u7b49\\u591a\\u79cd\\u573a\\u666f\", \"detail\": \"\\u667a\\u80fd\\u4f53\\u652f\\u6301\\u591a\\u6a21\\u6001\\u4ea4\\u4e92\\u3001\\u5de5\\u5177\\u8c03\\u7528\\u3001\\u591aAgent\\u534f\\u4f5c\\u3002\"}]}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "sec_cases",
|
|
||||||
"org_id": "0",
|
|
||||||
"section_key": "cases",
|
|
||||||
"title": "成功案例",
|
|
||||||
"subtitle": "看看AI如何改变这些行业",
|
|
||||||
"section_type": "grid",
|
|
||||||
"content_type": "case",
|
|
||||||
"sort_order": 3,
|
|
||||||
"is_visible": "1",
|
|
||||||
"display_config": "{\"columns\": 3, \"hover_effect\": \"lift\", \"show_cta\": true}",
|
|
||||||
"style_config": "{\"card_bg\": \"#1A1A1A\", \"hover_border\": \"#6C5CE7\"}",
|
|
||||||
"static_content": "{\"cta_text\": \"\\u60f3\\u4e86\\u89e3\\u8fd9\\u4e9b\\u65b9\\u6848\\u5982\\u4f55\\u843d\\u5730\\uff1f\", \"cta_btn\": \"\\u4e86\\u89e3\\u66f4\\u591a \\u2192 \\u8054\\u7cfb\\u9500\\u552e\"}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "sec_news",
|
|
||||||
"org_id": "0",
|
|
||||||
"section_key": "news",
|
|
||||||
"title": "企业动态",
|
|
||||||
"section_type": "list",
|
|
||||||
"content_type": "news",
|
|
||||||
"sort_order": 4,
|
|
||||||
"is_visible": "1",
|
|
||||||
"display_config": "{\"limit\": 2, \"show_view_all\": true}",
|
|
||||||
"style_config": "{\"item_bg\": \"#1A1A1A\", \"item_border\": \"#222\"}",
|
|
||||||
"static_content": ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "sec_footer",
|
|
||||||
"org_id": "0",
|
|
||||||
"section_key": "footer",
|
|
||||||
"title": "页脚",
|
|
||||||
"section_type": "footer",
|
|
||||||
"content_type": "",
|
|
||||||
"sort_order": 99,
|
|
||||||
"is_visible": "1",
|
|
||||||
"display_config": "{\"show_qrcode\": false}",
|
|
||||||
"style_config": "{\"border_top\": \"1px solid rgba(255,255,255,0.06)\"}",
|
|
||||||
"static_content": "{\"copyright\": \"\\u00a9 2026 \\u5f00\\u5143\\u4e91\\u79d1\\u6280 \\u00b7 \\u56fd\\u5bb6\\u7ea7\\u9ad8\\u65b0\\u6280\\u672f\\u4f01\\u4e1a \\u00b7 \\u4e13\\u7cbe\\u7279\\u65b0\\u4f01\\u4e1a\"}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "sec_float",
|
|
||||||
"org_id": "0",
|
|
||||||
"section_key": "float_contact",
|
|
||||||
"title": "浮动商机入口",
|
|
||||||
"section_type": "float",
|
|
||||||
"content_type": "",
|
|
||||||
"sort_order": 100,
|
|
||||||
"is_visible": "1",
|
|
||||||
"display_config": "{\"position\": \"fixed\", \"right\": 24, \"bottom\": 24}",
|
|
||||||
"style_config": "{\"avatar_bg\": \"linear-gradient(135deg, #6C5CE7, #A29BFE)\", \"size\": \"56px\"}",
|
|
||||||
"static_content": "{\"bubble_text\": \"\\u6709\\u4ec0\\u4e48\\u53ef\\u4ee5\\u5e2e\\u60a8\\uff1f\", \"panel_title\": \"\\u4e91\\u5b9d\\u5546\\u673a\\u52a9\\u624b\", \"options\": [\"\\u60a8\\u5bf9\\u54ea\\u4e9b\\u4ea7\\u54c1\\u611f\\u5174\\u8da3\\uff1f\", \"\\u7ed9\\u6211\\u4eec\\u7559\\u8a00\", \"\\u7559\\u4e0b\\u8054\\u7cfb\\u65b9\\u5f0f\"]}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
{
|
|
||||||
"tblname": "cms_sections",
|
|
||||||
"alias": "cms_sections_list",
|
|
||||||
"title": "栏目管理",
|
|
||||||
"params": {
|
|
||||||
"sortby": [
|
|
||||||
"sort_order asc"
|
|
||||||
],
|
|
||||||
"logined_userorgid": "org_id",
|
|
||||||
"browserfields": {
|
|
||||||
"exclouded": [
|
|
||||||
"display_config",
|
|
||||||
"style_config",
|
|
||||||
"static_content"
|
|
||||||
],
|
|
||||||
"alters": {
|
|
||||||
"section_type": {
|
|
||||||
"uitype": "code",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"value": "hero",
|
|
||||||
"text": "首屏Hero"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "cards",
|
|
||||||
"text": "卡片(1+N+X)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "grid",
|
|
||||||
"text": "网格(案例)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "list",
|
|
||||||
"text": "列表(新闻)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "banner",
|
|
||||||
"text": "横幅(CTA)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "float",
|
|
||||||
"text": "浮动入口"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "footer",
|
|
||||||
"text": "页脚"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"content_type": {
|
|
||||||
"uitype": "code",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"value": "",
|
|
||||||
"text": "(静态)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "product",
|
|
||||||
"text": "产品"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "case",
|
|
||||||
"text": "案例"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "news",
|
|
||||||
"text": "新闻"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"is_visible": {
|
|
||||||
"uitype": "code",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"value": "1",
|
|
||||||
"text": "显示"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "0",
|
|
||||||
"text": "隐藏"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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')}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,63 +0,0 @@
|
|||||||
{
|
|
||||||
"tblname": "cms_site_config",
|
|
||||||
"alias": "cms_site_config_list",
|
|
||||||
"title": "站点配置",
|
|
||||||
"params": {
|
|
||||||
"sortby": [
|
|
||||||
"config_group asc",
|
|
||||||
"sort_order asc"
|
|
||||||
],
|
|
||||||
"logined_userorgid": "org_id",
|
|
||||||
"browserfields": {
|
|
||||||
"alters": {
|
|
||||||
"config_group": {
|
|
||||||
"uitype": "code",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"value": "hero",
|
|
||||||
"text": "首屏Hero"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "footer",
|
|
||||||
"text": "页脚"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "contact",
|
|
||||||
"text": "联系信息"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "seo",
|
|
||||||
"text": "SEO设置"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"config_type": {
|
|
||||||
"uitype": "code",
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"value": "text",
|
|
||||||
"text": "文本"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "image",
|
|
||||||
"text": "图片"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "html",
|
|
||||||
"text": "HTML"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "json",
|
|
||||||
"text": "JSON"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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')}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
[build-system]
|
|
||||||
requires = ["setuptools>=45", "wheel"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
|
|
||||||
[project]
|
|
||||||
name = "entcms"
|
|
||||||
version = "1.0.0"
|
|
||||||
description = "企业CMS系统 - 开元云科技官网内容管理"
|
|
||||||
requires-python = ">=3.8"
|
|
||||||
dependencies = [
|
|
||||||
"sqlor",
|
|
||||||
"bricks_for_python",
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
|
||||||
where = ["."]
|
|
||||||
include = ["entcms*"]
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
"""
|
|
||||||
entcms RBAC权限配置 — 已废弃
|
|
||||||
|
|
||||||
entcms模块的wwwroot内容已移到应用根目录的 wwwroot/ 下。
|
|
||||||
请使用:
|
|
||||||
cd ~/repos/cms && py3/bin/python init_any_permissions.py
|
|
||||||
cd ~/repos/cms && py3/bin/python init_superuser_permissions.py
|
|
||||||
"""
|
|
||||||
295
init/data.yaml
Normal file
295
init/data.yaml
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
appcodes:
|
||||||
|
- id: content_type
|
||||||
|
name: 内容类型
|
||||||
|
hierarchy_flg: 0
|
||||||
|
- id: content_status
|
||||||
|
name: 内容状态
|
||||||
|
hierarchy_flg: 0
|
||||||
|
- id: lead_status
|
||||||
|
name: 线索状态
|
||||||
|
hierarchy_flg: 0
|
||||||
|
- id: lead_source
|
||||||
|
name: 线索来源
|
||||||
|
hierarchy_flg: 0
|
||||||
|
- id: config_type
|
||||||
|
name: 配置类型
|
||||||
|
hierarchy_flg: 0
|
||||||
|
- id: approval_status
|
||||||
|
name: 审批状态
|
||||||
|
hierarchy_flg: 0
|
||||||
|
|
||||||
|
appcodes_kv:
|
||||||
|
# content_type
|
||||||
|
- id: content_type_news
|
||||||
|
parentid: content_type
|
||||||
|
k: news
|
||||||
|
v: 新闻
|
||||||
|
- id: content_type_case
|
||||||
|
parentid: content_type
|
||||||
|
k: case
|
||||||
|
v: 案例
|
||||||
|
- id: content_type_product
|
||||||
|
parentid: content_type
|
||||||
|
k: product
|
||||||
|
v: 产品
|
||||||
|
- id: content_type_banner
|
||||||
|
parentid: content_type
|
||||||
|
k: banner
|
||||||
|
v: Banner
|
||||||
|
|
||||||
|
# content_status
|
||||||
|
- id: content_status_draft
|
||||||
|
parentid: content_status
|
||||||
|
k: draft
|
||||||
|
v: 草稿
|
||||||
|
- id: content_status_pending
|
||||||
|
parentid: content_status
|
||||||
|
k: pending
|
||||||
|
v: 待审批
|
||||||
|
- id: content_status_approved
|
||||||
|
parentid: content_status
|
||||||
|
k: approved
|
||||||
|
v: 已审批
|
||||||
|
- id: content_status_published
|
||||||
|
parentid: content_status
|
||||||
|
k: published
|
||||||
|
v: 已发布
|
||||||
|
- id: content_status_rejected
|
||||||
|
parentid: content_status
|
||||||
|
k: rejected
|
||||||
|
v: 已拒绝
|
||||||
|
|
||||||
|
# lead_status
|
||||||
|
- id: lead_status_new
|
||||||
|
parentid: lead_status
|
||||||
|
k: new
|
||||||
|
v: 新线索
|
||||||
|
- id: lead_status_contacted
|
||||||
|
parentid: lead_status
|
||||||
|
k: contacted
|
||||||
|
v: 已联系
|
||||||
|
- id: lead_status_qualified
|
||||||
|
parentid: lead_status
|
||||||
|
k: qualified
|
||||||
|
v: 已确认
|
||||||
|
- id: lead_status_converted
|
||||||
|
parentid: lead_status
|
||||||
|
k: converted
|
||||||
|
v: 已转化
|
||||||
|
- id: lead_status_closed
|
||||||
|
parentid: lead_status
|
||||||
|
k: closed
|
||||||
|
v: 已关闭
|
||||||
|
|
||||||
|
# lead_source
|
||||||
|
- id: lead_source_website
|
||||||
|
parentid: lead_source
|
||||||
|
k: website
|
||||||
|
v: 官网
|
||||||
|
- id: lead_source_referral
|
||||||
|
parentid: lead_source
|
||||||
|
k: referral
|
||||||
|
v: 转介绍
|
||||||
|
- id: lead_source_event
|
||||||
|
parentid: lead_source
|
||||||
|
k: event
|
||||||
|
v: 活动
|
||||||
|
- id: lead_source_ai_extract
|
||||||
|
parentid: lead_source
|
||||||
|
k: ai_extract
|
||||||
|
v: AI抽取
|
||||||
|
|
||||||
|
# config_type
|
||||||
|
- id: config_type_text
|
||||||
|
parentid: config_type
|
||||||
|
k: text
|
||||||
|
v: 文本
|
||||||
|
- id: config_type_html
|
||||||
|
parentid: config_type
|
||||||
|
k: html
|
||||||
|
v: HTML
|
||||||
|
- id: config_type_json
|
||||||
|
parentid: config_type
|
||||||
|
k: json
|
||||||
|
v: JSON
|
||||||
|
- id: config_type_image
|
||||||
|
parentid: config_type
|
||||||
|
k: image
|
||||||
|
v: 图片
|
||||||
|
|
||||||
|
# approval_status
|
||||||
|
- id: approval_status_pending
|
||||||
|
parentid: approval_status
|
||||||
|
k: pending
|
||||||
|
v: 待审批
|
||||||
|
- id: approval_status_approved
|
||||||
|
parentid: approval_status
|
||||||
|
k: approved
|
||||||
|
v: 已通过
|
||||||
|
- id: approval_status_rejected
|
||||||
|
parentid: approval_status
|
||||||
|
k: rejected
|
||||||
|
v: 已拒绝
|
||||||
|
- id: approval_status_cancelled
|
||||||
|
parentid: approval_status
|
||||||
|
k: cancelled
|
||||||
|
v: 已取消
|
||||||
|
|
||||||
|
cms_categories:
|
||||||
|
- id: cat_product_platform
|
||||||
|
org_id: "0"
|
||||||
|
name: AI平台
|
||||||
|
content_type: product
|
||||||
|
sort_order: 1
|
||||||
|
- id: cat_product_model
|
||||||
|
org_id: "0"
|
||||||
|
name: 行业模型
|
||||||
|
content_type: product
|
||||||
|
sort_order: 2
|
||||||
|
- id: cat_product_agent
|
||||||
|
org_id: "0"
|
||||||
|
name: 智能体
|
||||||
|
content_type: product
|
||||||
|
sort_order: 3
|
||||||
|
- id: cat_case_mfg
|
||||||
|
org_id: "0"
|
||||||
|
name: 智能制造
|
||||||
|
content_type: case
|
||||||
|
sort_order: 1
|
||||||
|
- id: cat_case_finance
|
||||||
|
org_id: "0"
|
||||||
|
name: 金融科技
|
||||||
|
content_type: case
|
||||||
|
sort_order: 2
|
||||||
|
- id: cat_case_healthcare
|
||||||
|
org_id: "0"
|
||||||
|
name: 医疗健康
|
||||||
|
content_type: case
|
||||||
|
sort_order: 3
|
||||||
|
- id: cat_case_education
|
||||||
|
org_id: "0"
|
||||||
|
name: 教育培训
|
||||||
|
content_type: case
|
||||||
|
sort_order: 4
|
||||||
|
- id: cat_news_company
|
||||||
|
org_id: "0"
|
||||||
|
name: 公司动态
|
||||||
|
content_type: news
|
||||||
|
sort_order: 1
|
||||||
|
- id: cat_news_industry
|
||||||
|
org_id: "0"
|
||||||
|
name: 行业资讯
|
||||||
|
content_type: news
|
||||||
|
sort_order: 2
|
||||||
|
- id: cat_news_product
|
||||||
|
org_id: "0"
|
||||||
|
name: 产品更新
|
||||||
|
content_type: news
|
||||||
|
sort_order: 3
|
||||||
|
|
||||||
|
cms_site_config:
|
||||||
|
- id: cfg_hero_slogan
|
||||||
|
org_id: "0"
|
||||||
|
config_group: hero
|
||||||
|
config_key: slogan
|
||||||
|
config_value: 一个平台,千行百业 智能跃迁
|
||||||
|
config_type: text
|
||||||
|
- id: cfg_hero_subtitle
|
||||||
|
org_id: "0"
|
||||||
|
config_group: hero
|
||||||
|
config_key: subtitle
|
||||||
|
config_value: 基于东数西算国家战略,打造新一代AI智能体服务平台
|
||||||
|
config_type: text
|
||||||
|
- id: cfg_hero_tag
|
||||||
|
org_id: "0"
|
||||||
|
config_group: hero
|
||||||
|
config_key: tag_text
|
||||||
|
config_value: AI 智能体服务平台
|
||||||
|
config_type: text
|
||||||
|
- id: cfg_footer_copyright
|
||||||
|
org_id: "0"
|
||||||
|
config_group: footer
|
||||||
|
config_key: copyright
|
||||||
|
config_value: "© 2026 开元云科技 · 国家级高新技术企业 · 专精特新企业"
|
||||||
|
config_type: text
|
||||||
|
- id: cfg_contact_company
|
||||||
|
org_id: "0"
|
||||||
|
config_group: contact
|
||||||
|
config_key: company_name
|
||||||
|
config_value: 开元云科技
|
||||||
|
config_type: text
|
||||||
|
|
||||||
|
cms_sections:
|
||||||
|
- id: sec_hero
|
||||||
|
org_id: "0"
|
||||||
|
section_key: hero
|
||||||
|
title: 首屏Hero
|
||||||
|
section_type: hero
|
||||||
|
content_type: ""
|
||||||
|
sort_order: 1
|
||||||
|
is_visible: "1"
|
||||||
|
display_config: '{"layout": "fullscreen", "height": "100vh", "bg_glow": true}'
|
||||||
|
style_config: '{"gradient": "#6C5CE7,#A29BFE,#74B9FF", "title_size": "56px"}'
|
||||||
|
static_content: '{"tag_text": "AI 智能体服务平台", "slogan": "一个平台,千行百业 智能跃迁", "subtitle": "基于东数西算国家战略,打造新一代AI智能体服务平台", "btn_primary": "联系销售", "btn_secondary": "了解产品架构"}'
|
||||||
|
- id: sec_products
|
||||||
|
org_id: "0"
|
||||||
|
section_key: products
|
||||||
|
title: 1+N+X 产品架构
|
||||||
|
subtitle: 一个AI平台 + N个行业模型 + X个智能体
|
||||||
|
section_type: cards
|
||||||
|
content_type: product
|
||||||
|
sort_order: 2
|
||||||
|
is_visible: "1"
|
||||||
|
display_config: '{"columns": 3, "expandable": true, "icon_position": "top"}'
|
||||||
|
style_config: '{"card_bg": "#1A1A1A", "card_border": "#222", "hover_border": "#6C5CE7"}'
|
||||||
|
- id: sec_cases
|
||||||
|
org_id: "0"
|
||||||
|
section_key: cases
|
||||||
|
title: 成功案例
|
||||||
|
subtitle: 看看AI如何改变这些行业
|
||||||
|
section_type: grid
|
||||||
|
content_type: case
|
||||||
|
sort_order: 3
|
||||||
|
is_visible: "1"
|
||||||
|
display_config: '{"columns": 3, "hover_effect": "lift", "show_cta": true}'
|
||||||
|
style_config: '{"card_bg": "#1A1A1A", "hover_border": "#6C5CE7"}'
|
||||||
|
- id: sec_news
|
||||||
|
org_id: "0"
|
||||||
|
section_key: news
|
||||||
|
title: 企业动态
|
||||||
|
section_type: list
|
||||||
|
content_type: news
|
||||||
|
sort_order: 4
|
||||||
|
is_visible: "1"
|
||||||
|
display_config: '{"limit": 2, "show_view_all": true}'
|
||||||
|
style_config: '{"item_bg": "#1A1A1A", "item_border": "#222"}'
|
||||||
|
- id: sec_footer
|
||||||
|
org_id: "0"
|
||||||
|
section_key: footer
|
||||||
|
title: 页脚
|
||||||
|
section_type: footer
|
||||||
|
content_type: ""
|
||||||
|
sort_order: 99
|
||||||
|
is_visible: "1"
|
||||||
|
display_config: '{"show_qrcode": false}'
|
||||||
|
style_config: '{"border_top": "1px solid rgba(255,255,255,0.06)"}'
|
||||||
|
- id: sec_float
|
||||||
|
org_id: "0"
|
||||||
|
section_key: float_contact
|
||||||
|
title: 浮动商机入口
|
||||||
|
section_type: float
|
||||||
|
content_type: ""
|
||||||
|
sort_order: 100
|
||||||
|
is_visible: "1"
|
||||||
|
display_config: '{"position": "fixed", "right": 24, "bottom": 24}'
|
||||||
|
style_config: '{"avatar_bg": "linear-gradient(135deg, #6C5CE7, #A29BFE)", "size": "56px"}'
|
||||||
|
|
||||||
|
dd_approval_configs:
|
||||||
|
- id: apvcfg_content_publish
|
||||||
|
org_id: "0"
|
||||||
|
biz_type: content_publish
|
||||||
|
biz_type_title: 内容发布审批
|
||||||
|
process_code: ""
|
||||||
|
agent_id: ""
|
||||||
|
form_config: '[{"name": "审批类型", "value": "内容发布"}]'
|
||||||
|
is_active: "1"
|
||||||
@ -1,158 +0,0 @@
|
|||||||
"""
|
|
||||||
CMS RBAC权限初始化 — any (匿名) 角色
|
|
||||||
自动扫描 wwwroot 和 bricks 下所有文件,授予 any 角色权限
|
|
||||||
|
|
||||||
规则:
|
|
||||||
- wwwroot/* → /<file>
|
|
||||||
- wwwroot/dingdingflow/* → /dingdingflow/<file>
|
|
||||||
- bricks/* → /bricks/<file>
|
|
||||||
- 排除: .pyc, __pycache__, .git, 指向其他模块的符号链接
|
|
||||||
|
|
||||||
用法: cd ~/repos/cms && py3/bin/python init_any_permissions.py
|
|
||||||
"""
|
|
||||||
import os, sys, subprocess, re
|
|
||||||
|
|
||||||
def find_app_root():
|
|
||||||
return os.path.dirname(os.path.abspath(__file__))
|
|
||||||
|
|
||||||
app_root = find_app_root()
|
|
||||||
sage_root = None
|
|
||||||
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]:
|
|
||||||
if os.path.isdir(os.path.join(c, "py3", "bin")):
|
|
||||||
sage_root = c
|
|
||||||
break
|
|
||||||
if not sage_root:
|
|
||||||
print("ERROR: 找不到Sage,无法初始化权限")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
py = os.path.join(sage_root, "py3", "bin", "python")
|
|
||||||
sp = os.path.join(sage_root, "set_role_perm.py")
|
|
||||||
if not os.path.exists(sp):
|
|
||||||
print("ERROR: 找不到set_role_perm.py")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
SKIP_DIRS = {".git", "__pycache__", "node_modules", ".svn"}
|
|
||||||
SKIP_EXTS = {".pyc", ".pyo", ".swp", ".swo", ".bak", ".orig", ".log", ".pid", ".lock"}
|
|
||||||
|
|
||||||
def scan_wwwroot(wwwroot_dir, url_prefix):
|
|
||||||
"""扫描wwwroot目录下所有文件,返回URL路径列表"""
|
|
||||||
paths = []
|
|
||||||
if not os.path.isdir(wwwroot_dir):
|
|
||||||
return paths
|
|
||||||
for root, dirs, files in os.walk(wwwroot_dir):
|
|
||||||
# 排除不需要扫描的目录
|
|
||||||
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
|
|
||||||
for f in sorted(files):
|
|
||||||
# 跳过不需要的文件
|
|
||||||
_, ext = os.path.splitext(f)
|
|
||||||
if ext.lower() in SKIP_EXTS:
|
|
||||||
continue
|
|
||||||
if f.startswith("."):
|
|
||||||
continue
|
|
||||||
# 计算相对路径
|
|
||||||
rel_path = os.path.relpath(os.path.join(root, f), wwwroot_dir)
|
|
||||||
# 检查是否是符号链接指向其他模块
|
|
||||||
full_path = os.path.join(root, f)
|
|
||||||
if os.path.islink(full_path):
|
|
||||||
link_target = os.path.realpath(full_path)
|
|
||||||
# 如果链接目标不在CMS目录下,跳过
|
|
||||||
if not link_target.startswith(app_root):
|
|
||||||
print(f" SKIP (外部链接): {rel_path} -> {link_target}")
|
|
||||||
continue
|
|
||||||
url = url_prefix + "/" + rel_path.replace(os.sep, "/")
|
|
||||||
paths.append(url)
|
|
||||||
return paths
|
|
||||||
|
|
||||||
def scan_bricks(bricks_dir):
|
|
||||||
"""扫描bricks目录下所有文件,返回URL路径列表"""
|
|
||||||
paths = []
|
|
||||||
if not os.path.isdir(bricks_dir):
|
|
||||||
return paths
|
|
||||||
# 检查bricks是否为符号链接
|
|
||||||
real_bricks = os.path.realpath(bricks_dir)
|
|
||||||
for root, dirs, files in os.walk(bricks_dir):
|
|
||||||
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
|
|
||||||
for f in sorted(files):
|
|
||||||
_, ext = os.path.splitext(f)
|
|
||||||
if ext.lower() in SKIP_EXTS:
|
|
||||||
continue
|
|
||||||
if f.startswith("."):
|
|
||||||
continue
|
|
||||||
full_path = os.path.join(root, f)
|
|
||||||
# 跳过指向其他模块的符号链接
|
|
||||||
if os.path.islink(full_path):
|
|
||||||
link_target = os.path.realpath(full_path)
|
|
||||||
if not link_target.startswith(real_bricks) and not link_target.startswith(app_root):
|
|
||||||
continue
|
|
||||||
rel_path = os.path.relpath(full_path, bricks_dir)
|
|
||||||
url = "/bricks/" + rel_path.replace(os.sep, "/")
|
|
||||||
paths.append(url)
|
|
||||||
return paths
|
|
||||||
|
|
||||||
def set_any_perms(paths):
|
|
||||||
"""为路径列表设置any权限"""
|
|
||||||
count = 0
|
|
||||||
env = os.environ.copy()
|
|
||||||
env['SAGE_RBAC_DB'] = 'ocai_cms'
|
|
||||||
for p in paths:
|
|
||||||
result = subprocess.run(
|
|
||||||
[py, sp, "any", p],
|
|
||||||
cwd=sage_root,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
env=env
|
|
||||||
)
|
|
||||||
status = "✓" if result.returncode == 0 else "✗"
|
|
||||||
print(f" {status} any {p}")
|
|
||||||
count += 1
|
|
||||||
return count
|
|
||||||
|
|
||||||
print("=== CMS RBAC权限初始化 — any (匿名访问) ===")
|
|
||||||
print(f"Sage: {sage_root}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# 1. wwwroot/ 根目录文件 → / (排除dingdingflow子目录)
|
|
||||||
wwwroot_root = os.path.join(app_root, "wwwroot")
|
|
||||||
root_paths = []
|
|
||||||
if os.path.isdir(wwwroot_root):
|
|
||||||
for f in sorted(os.listdir(wwwroot_root)):
|
|
||||||
fpath = os.path.join(wwwroot_root, f)
|
|
||||||
if os.path.isfile(fpath) and not f.startswith("."):
|
|
||||||
_, ext = os.path.splitext(f)
|
|
||||||
if ext.lower() not in SKIP_EXTS:
|
|
||||||
root_paths.append("/" + f)
|
|
||||||
# api 子目录
|
|
||||||
api_dir = os.path.join(wwwroot_root, "api")
|
|
||||||
if os.path.isdir(api_dir):
|
|
||||||
for f in sorted(os.listdir(api_dir)):
|
|
||||||
fpath = os.path.join(api_dir, f)
|
|
||||||
if os.path.isfile(fpath) and not f.startswith("."):
|
|
||||||
_, ext = os.path.splitext(f)
|
|
||||||
if ext.lower() not in SKIP_EXTS:
|
|
||||||
root_paths.append("/api/" + f)
|
|
||||||
|
|
||||||
print(f"--- wwwroot/ → / ({len(root_paths)} 个文件) ---")
|
|
||||||
# 确保根路径 / 也有权限(访问根路径时RBAC检查的是 '/' 而非 index.ui)
|
|
||||||
root_paths.append("/")
|
|
||||||
n1 = set_any_perms(root_paths)
|
|
||||||
|
|
||||||
# 2. wwwroot/dingdingflow/ → /dingdingflow/
|
|
||||||
dd_paths = scan_wwwroot(
|
|
||||||
os.path.join(app_root, "wwwroot", "dingdingflow"),
|
|
||||||
"/dingdingflow"
|
|
||||||
)
|
|
||||||
print(f"\n--- wwwroot/dingdingflow/ → /dingdingflow ({len(dd_paths)} 个文件) ---")
|
|
||||||
n2 = set_any_perms(dd_paths)
|
|
||||||
|
|
||||||
# 3. bricks → /bricks
|
|
||||||
bricks_dir = os.path.join(app_root, "bricks")
|
|
||||||
bricks_paths = scan_bricks(bricks_dir)
|
|
||||||
if bricks_paths:
|
|
||||||
print(f"\n--- bricks → /bricks ({len(bricks_paths)} 个文件) ---")
|
|
||||||
n3 = set_any_perms(bricks_paths)
|
|
||||||
else:
|
|
||||||
n3 = 0
|
|
||||||
print(f"\n--- bricks → /bricks (目录不存在或未构建,跳过) ---")
|
|
||||||
|
|
||||||
total = n1 + n2 + n3
|
|
||||||
print(f"\n=== 完成: 共设置 {total} 个any权限 ===")
|
|
||||||
@ -1,147 +0,0 @@
|
|||||||
"""
|
|
||||||
CMS RBAC权限初始化 — superuser角色
|
|
||||||
为owner.superuser授予CMS所有权限
|
|
||||||
|
|
||||||
用法: cd ~/repos/cms && py3/bin/python init_superuser_permissions.py
|
|
||||||
"""
|
|
||||||
import os, sys, subprocess
|
|
||||||
|
|
||||||
def find_app_root():
|
|
||||||
"""查找CMS应用根目录"""
|
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
return script_dir
|
|
||||||
|
|
||||||
app_root = find_app_root()
|
|
||||||
sage_root = None
|
|
||||||
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]:
|
|
||||||
if os.path.isdir(os.path.join(c, "py3", "bin")):
|
|
||||||
sage_root = c
|
|
||||||
break
|
|
||||||
if not sage_root:
|
|
||||||
sage_root = app_root
|
|
||||||
|
|
||||||
py = os.path.join(sage_root, "py3", "bin", "python")
|
|
||||||
sp = os.path.join(sage_root, "set_role_perm.py") if os.path.exists(os.path.join(sage_root, "set_role_perm.py")) else None
|
|
||||||
|
|
||||||
if not sp:
|
|
||||||
print("ERROR: 找不到set_role_perm.py,请确保Sage或CMS已构建")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def run(role, paths):
|
|
||||||
assert sp is not None, "set_role_perm.py not found"
|
|
||||||
env = os.environ.copy()
|
|
||||||
env['SAGE_RBAC_DB'] = 'ocai_cms'
|
|
||||||
for p in paths:
|
|
||||||
print(f" {role:30s} {p}")
|
|
||||||
subprocess.run([py, sp, role, p], cwd=sage_root, capture_output=True, env=env)
|
|
||||||
|
|
||||||
# ─── superuser — 所有权限 ───
|
|
||||||
superuser_paths = [
|
|
||||||
# entcms 公开页面
|
|
||||||
"/index.ui",
|
|
||||||
"/news.ui",
|
|
||||||
"/news_detail.ui",
|
|
||||||
"/cases.ui",
|
|
||||||
"/products.ui",
|
|
||||||
"/cms_styles.css",
|
|
||||||
"/cms_scripts.js",
|
|
||||||
"/menu.ui",
|
|
||||||
"/admin.ui",
|
|
||||||
|
|
||||||
# entcms 内容管理
|
|
||||||
"/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",
|
|
||||||
"/api/submit_content_approval.dspy",
|
|
||||||
|
|
||||||
# entcms 分类管理
|
|
||||||
"/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",
|
|
||||||
"/api/category_options.dspy",
|
|
||||||
|
|
||||||
# entcms 栏目管理
|
|
||||||
"/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",
|
|
||||||
|
|
||||||
# entcms 站点配置
|
|
||||||
"/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",
|
|
||||||
|
|
||||||
# entcms 线索管理
|
|
||||||
"/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",
|
|
||||||
|
|
||||||
# entcms 其他API
|
|
||||||
"/api/submit_lead.dspy",
|
|
||||||
"/api/get_config.dspy",
|
|
||||||
"/api/get_published_content.dspy",
|
|
||||||
"/api/get_content_detail.dspy",
|
|
||||||
"/api/get_sections.dspy",
|
|
||||||
|
|
||||||
# dingdingflow
|
|
||||||
"/dingdingflow",
|
|
||||||
"/dingdingflow/index.ui",
|
|
||||||
"/dingdingflow/menu.ui",
|
|
||||||
"/dingdingflow/api/dingtalk_callback.dspy",
|
|
||||||
"/dingdingflow/api/submit_approval.dspy",
|
|
||||||
|
|
||||||
# dingdingflow 审批配置
|
|
||||||
"/dingdingflow/dd_approval_configs", "/dingdingflow/dd_approval_configs/%",
|
|
||||||
"/dingdingflow/api/dd_approval_configs_create.dspy",
|
|
||||||
"/dingdingflow/api/dd_approval_configs_update.dspy",
|
|
||||||
"/dingdingflow/api/dd_approval_configs_delete.dspy",
|
|
||||||
"/dingdingflow/api/dd_approval_configs_list.dspy",
|
|
||||||
|
|
||||||
# dingdingflow 审批单
|
|
||||||
"/dingdingflow/dd_approvals", "/dingdingflow/dd_approvals/%",
|
|
||||||
"/dingdingflow/api/dd_approvals_create.dspy",
|
|
||||||
"/dingdingflow/api/dd_approvals_update.dspy",
|
|
||||||
"/dingdingflow/api/dd_approvals_delete.dspy",
|
|
||||||
"/dingdingflow/api/dd_approvals_list.dspy",
|
|
||||||
|
|
||||||
# appbase 系统基础模块
|
|
||||||
"/appbase/appcodes_kv",
|
|
||||||
"/appbase/appcodes_kv/get_appcodes_kv.dspy",
|
|
||||||
"/appbase/appcodes_kv/add_appcodes_kv.dspy",
|
|
||||||
"/appbase/appcodes_kv/update_appcodes_kv.dspy",
|
|
||||||
"/appbase/appcodes_kv/index.ui",
|
|
||||||
"/appbase/appcodes_kv/delete_appcodes_kv.dspy",
|
|
||||||
"/appbase/cron/index.ui",
|
|
||||||
"/appbase/appcodes",
|
|
||||||
"/appbase/appcodes/get_appcodes.dspy",
|
|
||||||
"/appbase/appcodes/add_appcodes.dspy",
|
|
||||||
"/appbase/appcodes/index.ui",
|
|
||||||
"/appbase/appcodes/update_appcodes.dspy",
|
|
||||||
"/appbase/appcodes/delete_appcodes.dspy",
|
|
||||||
"/appbase/params",
|
|
||||||
"/appbase/params/update_params.dspy",
|
|
||||||
"/appbase/params/get_params.dspy",
|
|
||||||
"/appbase/params/index.ui",
|
|
||||||
"/appbase/params/add_params.dspy",
|
|
||||||
"/appbase/params/delete_params.dspy",
|
|
||||||
"/appbase/svgicon",
|
|
||||||
"/appbase/svgicon/get_svgicon.dspy",
|
|
||||||
"/appbase/svgicon/delete_svgicon.dspy",
|
|
||||||
"/appbase/svgicon/add_svgicon.dspy",
|
|
||||||
"/appbase/svgicon/update_svgicon.dspy",
|
|
||||||
"/appbase/svgicon/index.ui",
|
|
||||||
]
|
|
||||||
|
|
||||||
print("=== CMS RBAC权限初始化 — superuser ===")
|
|
||||||
print(f"\\n--- owner.superuser (超级管理员) ---")
|
|
||||||
run("owner.superuser", superuser_paths)
|
|
||||||
print("\\n完成")
|
|
||||||
@ -12,18 +12,9 @@
|
|||||||
"content_type": {
|
"content_type": {
|
||||||
"uitype": "code",
|
"uitype": "code",
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{"value": "product", "text": "产品"},
|
||||||
"value": "product",
|
{"value": "case", "text": "案例"},
|
||||||
"text": "产品"
|
{"value": "news", "text": "新闻"}
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "case",
|
|
||||||
"text": "案例"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "news",
|
|
||||||
"text": "新闻"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,4 +25,4 @@
|
|||||||
"delete_data_url": "{{entire_url('api/cms_categories_delete.dspy')}}"
|
"delete_data_url": "{{entire_url('api/cms_categories_delete.dspy')}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -36,47 +36,20 @@
|
|||||||
"content_type": {
|
"content_type": {
|
||||||
"uitype": "code",
|
"uitype": "code",
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{"value": "banner", "text": "Banner"},
|
||||||
"value": "banner",
|
{"value": "product", "text": "产品"},
|
||||||
"text": "Banner"
|
{"value": "case", "text": "案例"},
|
||||||
},
|
{"value": "news", "text": "新闻"}
|
||||||
{
|
|
||||||
"value": "product",
|
|
||||||
"text": "产品"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "case",
|
|
||||||
"text": "案例"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "news",
|
|
||||||
"text": "新闻"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"uitype": "code",
|
"uitype": "code",
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{"value": "draft", "text": "草稿"},
|
||||||
"value": "draft",
|
{"value": "pending", "text": "待审批"},
|
||||||
"text": "草稿"
|
{"value": "approved", "text": "已审批"},
|
||||||
},
|
{"value": "published", "text": "已发布"},
|
||||||
{
|
{"value": "archived", "text": "已归档"}
|
||||||
"value": "pending",
|
|
||||||
"text": "待审批"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "approved",
|
|
||||||
"text": "已审批"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "published",
|
|
||||||
"text": "已发布"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "archived",
|
|
||||||
"text": "已归档"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"category_id": {
|
"category_id": {
|
||||||
@ -94,4 +67,4 @@
|
|||||||
"delete_data_url": "{{entire_url('api/cms_content_delete.dspy')}}"
|
"delete_data_url": "{{entire_url('api/cms_content_delete.dspy')}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -39,47 +39,20 @@
|
|||||||
"status": {
|
"status": {
|
||||||
"uitype": "code",
|
"uitype": "code",
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{"value": "new", "text": "新线索"},
|
||||||
"value": "new",
|
{"value": "contacted", "text": "已联系"},
|
||||||
"text": "新线索"
|
{"value": "qualified", "text": "已确认"},
|
||||||
},
|
{"value": "converted", "text": "已转化"},
|
||||||
{
|
{"value": "closed", "text": "已关闭"}
|
||||||
"value": "contacted",
|
|
||||||
"text": "已联系"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "qualified",
|
|
||||||
"text": "已确认"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "converted",
|
|
||||||
"text": "已转化"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "closed",
|
|
||||||
"text": "已关闭"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"source": {
|
"source": {
|
||||||
"uitype": "code",
|
"uitype": "code",
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{"value": "website", "text": "官网"},
|
||||||
"value": "website",
|
{"value": "phone", "text": "电话"},
|
||||||
"text": "官网"
|
{"value": "referral", "text": "转介绍"},
|
||||||
},
|
{"value": "ai_extract", "text": "AI抽取"}
|
||||||
{
|
|
||||||
"value": "phone",
|
|
||||||
"text": "电话"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "referral",
|
|
||||||
"text": "转介绍"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"value": "ai_extract",
|
|
||||||
"text": "AI抽取"
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,4 +63,4 @@
|
|||||||
"delete_data_url": "{{entire_url('api/cms_leads_delete.dspy')}}"
|
"delete_data_url": "{{entire_url('api/cms_leads_delete.dspy')}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
53
json/cms_sections_list.json
Normal file
53
json/cms_sections_list.json
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"tblname": "cms_sections",
|
||||||
|
"alias": "cms_sections_list",
|
||||||
|
"title": "栏目管理",
|
||||||
|
"params": {
|
||||||
|
"sortby": [
|
||||||
|
"sort_order asc"
|
||||||
|
],
|
||||||
|
"logined_userorgid": "org_id",
|
||||||
|
"browserfields": {
|
||||||
|
"exclouded": [
|
||||||
|
"display_config",
|
||||||
|
"style_config",
|
||||||
|
"static_content"
|
||||||
|
],
|
||||||
|
"alters": {
|
||||||
|
"section_type": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "hero", "text": "首屏Hero"},
|
||||||
|
{"value": "cards", "text": "卡片(1+N+X)"},
|
||||||
|
{"value": "grid", "text": "网格(案例)"},
|
||||||
|
{"value": "list", "text": "列表(新闻)"},
|
||||||
|
{"value": "banner", "text": "横幅(CTA)"},
|
||||||
|
{"value": "float", "text": "浮动入口"},
|
||||||
|
{"value": "footer", "text": "页脚"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content_type": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "", "text": "(静态)"},
|
||||||
|
{"value": "product", "text": "产品"},
|
||||||
|
{"value": "case", "text": "案例"},
|
||||||
|
{"value": "news", "text": "新闻"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"is_visible": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "1", "text": "显示"},
|
||||||
|
{"value": "0", "text": "隐藏"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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')}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
json/cms_site_config_list.json
Normal file
39
json/cms_site_config_list.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"tblname": "cms_site_config",
|
||||||
|
"alias": "cms_site_config_list",
|
||||||
|
"title": "站点配置",
|
||||||
|
"params": {
|
||||||
|
"sortby": [
|
||||||
|
"config_group asc",
|
||||||
|
"sort_order asc"
|
||||||
|
],
|
||||||
|
"logined_userorgid": "org_id",
|
||||||
|
"browserfields": {
|
||||||
|
"alters": {
|
||||||
|
"config_group": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "hero", "text": "首屏Hero"},
|
||||||
|
{"value": "footer", "text": "页脚"},
|
||||||
|
{"value": "contact", "text": "联系信息"},
|
||||||
|
{"value": "seo", "text": "SEO设置"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"config_type": {
|
||||||
|
"uitype": "code",
|
||||||
|
"data": [
|
||||||
|
{"value": "text", "text": "文本"},
|
||||||
|
{"value": "image", "text": "图片"},
|
||||||
|
{"value": "html", "text": "HTML"},
|
||||||
|
{"value": "json", "text": "JSON"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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')}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,9 +17,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"editable": {
|
"editable": {
|
||||||
"new_data_url": "{{entire_url('../api/dd_approval_configs_create.dspy')}}",
|
"new_data_url": "{{entire_url('../api/dingdingflow/dd_approval_configs_create.dspy')}}",
|
||||||
"update_data_url": "{{entire_url('../api/dd_approval_configs_update.dspy')}}",
|
"update_data_url": "{{entire_url('../api/dingdingflow/dd_approval_configs_update.dspy')}}",
|
||||||
"delete_data_url": "{{entire_url('../api/dd_approval_configs_delete.dspy')}}"
|
"delete_data_url": "{{entire_url('../api/dingdingflow/dd_approval_configs_delete.dspy')}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,9 +34,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"editable": {
|
"editable": {
|
||||||
"new_data_url": "{{entire_url('../api/dd_approvals_create.dspy')}}",
|
"new_data_url": "{{entire_url('../api/dingdingflow/dd_approvals_create.dspy')}}",
|
||||||
"update_data_url": "{{entire_url('../api/dd_approvals_update.dspy')}}",
|
"update_data_url": "{{entire_url('../api/dingdingflow/dd_approvals_update.dspy')}}",
|
||||||
"delete_data_url": "{{entire_url('../api/dd_approvals_delete.dspy')}}"
|
"delete_data_url": "{{entire_url('../api/dingdingflow/dd_approvals_delete.dspy')}}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,4 +82,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -151,4 +151,4 @@
|
|||||||
"textfield": "name"
|
"textfield": "name"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -134,4 +134,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -113,4 +113,4 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"codes": []
|
"codes": []
|
||||||
}
|
}
|
||||||
@ -72,4 +72,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -82,4 +82,4 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"codes": []
|
"codes": []
|
||||||
}
|
}
|
||||||
@ -111,4 +111,4 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -4,14 +4,15 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "kaiyuan-cms"
|
name = "kaiyuan-cms"
|
||||||
version = "1.0.0"
|
version = "2.0.0"
|
||||||
description = "开元云科技企业官网CMS系统 — 独立Web应用"
|
description = "CMS内容管理模块 - 企业官网内容管理与钉钉审批工作流"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"sqlor",
|
"sqlor",
|
||||||
"bricks_for_python",
|
"bricks_for_python",
|
||||||
|
"requests",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
where = ["."]
|
where = ["."]
|
||||||
include = ["entcms*", "dingdingflow*"]
|
include = ["cms*"]
|
||||||
|
|||||||
@ -1,126 +0,0 @@
|
|||||||
"""
|
|
||||||
初始化超级用户
|
|
||||||
用法:
|
|
||||||
CMS环境: py3/bin/python scripts/init_superuser.py [username] [password]
|
|
||||||
Sage环境: cd ~/repos/sage && ./py3/bin/python ~/repos/cms/scripts/init_superuser.py [username] [password]
|
|
||||||
默认: admin / admin123
|
|
||||||
"""
|
|
||||||
import os, sys, asyncio
|
|
||||||
|
|
||||||
# 自动检测运行环境
|
|
||||||
def find_workdir():
|
|
||||||
"""查找应用根目录"""
|
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
cms_root = os.path.dirname(script_dir)
|
|
||||||
|
|
||||||
# 检查是否是CMS独立环境
|
|
||||||
if os.path.isdir(os.path.join(cms_root, "py3", "bin")) and \
|
|
||||||
os.path.isfile(os.path.join(cms_root, "conf", "config.json")):
|
|
||||||
return cms_root
|
|
||||||
|
|
||||||
# 检查Sage环境
|
|
||||||
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]:
|
|
||||||
if os.path.isdir(os.path.join(c, "py3", "bin")):
|
|
||||||
return c
|
|
||||||
return None
|
|
||||||
|
|
||||||
workdir = find_workdir()
|
|
||||||
if not workdir:
|
|
||||||
print("ERROR: 找不到CMS或Sage应用目录")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# 确保Python路径
|
|
||||||
sys.path.insert(0, os.path.join(workdir, "py3", "lib"))
|
|
||||||
os.chdir(workdir)
|
|
||||||
|
|
||||||
from sqlor.dbpools import DBPools
|
|
||||||
from appPublic.jsonConfig import getConfig
|
|
||||||
from appPublic.uniqueID import getID
|
|
||||||
from appPublic.password import password_encode
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
username = sys.argv[1] if len(sys.argv) > 1 else "admin"
|
|
||||||
password = sys.argv[2] if len(sys.argv) > 2 else "admin123"
|
|
||||||
|
|
||||||
config = getConfig('.')
|
|
||||||
db = DBPools(config.databases)
|
|
||||||
async with db.sqlorContext('sage') as sor:
|
|
||||||
# 检查用户是否存在
|
|
||||||
existing = await sor.R('users', {'username': username})
|
|
||||||
if existing:
|
|
||||||
print(f"用户 {username} 已存在 (id={existing[0]['id']})")
|
|
||||||
await sor.U('users', {
|
|
||||||
'id': existing[0]['id'],
|
|
||||||
'passwd': password_encode(password)
|
|
||||||
})
|
|
||||||
print(f"密码已更新为: {password}")
|
|
||||||
else:
|
|
||||||
user_id = getID()
|
|
||||||
await sor.C('users', {
|
|
||||||
'id': user_id,
|
|
||||||
'username': username,
|
|
||||||
'passwd': password_encode(password),
|
|
||||||
'orgid': '0',
|
|
||||||
'orgtypeid': 'owner',
|
|
||||||
'status': '1',
|
|
||||||
})
|
|
||||||
print(f"用户已创建: {username} (id={user_id})")
|
|
||||||
|
|
||||||
# 查找或创建superuser角色
|
|
||||||
roles = await sor.R('role', {'orgtypeid': 'owner', 'name': 'superuser'})
|
|
||||||
if not roles:
|
|
||||||
role_id = getID()
|
|
||||||
await sor.C('role', {
|
|
||||||
'id': role_id,
|
|
||||||
'orgtypeid': 'owner',
|
|
||||||
'name': 'superuser'
|
|
||||||
})
|
|
||||||
print(f"角色 owner.superuser 已创建 (id={role_id})")
|
|
||||||
else:
|
|
||||||
role_id = roles[0]['id']
|
|
||||||
print(f"角色 owner.superuser 已存在 (id={role_id})")
|
|
||||||
|
|
||||||
# 获取用户ID
|
|
||||||
users = await sor.R('users', {'username': username})
|
|
||||||
uid = users[0]['id']
|
|
||||||
|
|
||||||
# 分配角色
|
|
||||||
try:
|
|
||||||
ur = await sor.R('userrole', {'userid': uid, 'roleid': role_id})
|
|
||||||
if not ur:
|
|
||||||
await sor.C('userrole', {
|
|
||||||
'id': getID(),
|
|
||||||
'userid': uid,
|
|
||||||
'roleid': role_id,
|
|
||||||
})
|
|
||||||
print(f"已分配角色 owner.superuser 给用户 {username}")
|
|
||||||
else:
|
|
||||||
print(f"用户 {username} 已拥有 owner.superuser 角色")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"注意: userrole表操作异常: {e}")
|
|
||||||
print("可能需要手动分配角色")
|
|
||||||
|
|
||||||
# 给superuser分配全部权限
|
|
||||||
try:
|
|
||||||
all_perms = await sor.R('permission', {})
|
|
||||||
for perm in all_perms:
|
|
||||||
existing_rp = await sor.R('rolepermission', {
|
|
||||||
'roleid': role_id,
|
|
||||||
'permid': perm['id']
|
|
||||||
})
|
|
||||||
if not existing_rp:
|
|
||||||
await sor.C('rolepermission', {
|
|
||||||
'id': getID(),
|
|
||||||
'roleid': role_id,
|
|
||||||
'permid': perm['id']
|
|
||||||
})
|
|
||||||
print(f"已将全部 {len(all_perms)} 条权限分配给 owner.superuser")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"注意: 权限分配异常: {e}")
|
|
||||||
|
|
||||||
print(f"\n登录信息:")
|
|
||||||
print(f" 用户名: {username}")
|
|
||||||
print(f" 密码: {password}")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
asyncio.get_event_loop().run_until_complete(main())
|
|
||||||
112
scripts/load_path.py
Normal file
112
scripts/load_path.py
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
"""
|
||||||
|
cms 模块 RBAC 权限配置
|
||||||
|
CMS管理后台的CRUD页面和API,挂载在 /cms/ 路径下
|
||||||
|
|
||||||
|
用法:
|
||||||
|
cd ~/repos/portal
|
||||||
|
py3/bin/python ~/repos/cms/scripts/load_path.py
|
||||||
|
"""
|
||||||
|
import subprocess, os, sys
|
||||||
|
|
||||||
|
def find_portal_root():
|
||||||
|
candidates = [
|
||||||
|
os.path.expanduser("~/repos/portal"),
|
||||||
|
os.path.expanduser("~/portal"),
|
||||||
|
]
|
||||||
|
for c in candidates:
|
||||||
|
if os.path.isdir(os.path.join(c, "py3")):
|
||||||
|
return c
|
||||||
|
# fallback: sage
|
||||||
|
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]:
|
||||||
|
if os.path.isdir(os.path.join(c, "py3")):
|
||||||
|
return c
|
||||||
|
return None
|
||||||
|
|
||||||
|
PORTAL_ROOT = find_portal_root()
|
||||||
|
if not PORTAL_ROOT:
|
||||||
|
print("ERROR: Cannot find Portal or Sage root")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
PYTHON = os.path.join(PORTAL_ROOT, "py3", "bin", "python")
|
||||||
|
SET_PERM = os.path.join(PORTAL_ROOT, "set_role_perm.py")
|
||||||
|
if not os.path.exists(SET_PERM):
|
||||||
|
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]:
|
||||||
|
sp = os.path.join(c, "set_role_perm.py")
|
||||||
|
if os.path.exists(sp):
|
||||||
|
SET_PERM = sp
|
||||||
|
break
|
||||||
|
|
||||||
|
MOD = "cms"
|
||||||
|
env = os.environ.copy()
|
||||||
|
env['SAGE_RBAC_DB'] = 'ocai_cms'
|
||||||
|
|
||||||
|
# CMS管理后台所有路径
|
||||||
|
PATHS = [
|
||||||
|
f"/{MOD}",
|
||||||
|
f"/{MOD}/admin.ui",
|
||||||
|
f"/{MOD}/menu.ui",
|
||||||
|
# Content
|
||||||
|
f"/{MOD}/cms_content_list", f"/{MOD}/cms_content_list/%",
|
||||||
|
f"/{MOD}/api/cms_content_create.dspy",
|
||||||
|
f"/{MOD}/api/cms_content_update.dspy",
|
||||||
|
f"/{MOD}/api/cms_content_delete.dspy",
|
||||||
|
f"/{MOD}/api/cms_content_list.dspy",
|
||||||
|
f"/{MOD}/api/submit_content_approval.dspy",
|
||||||
|
# Categories
|
||||||
|
f"/{MOD}/cms_categories_list", f"/{MOD}/cms_categories_list/%",
|
||||||
|
f"/{MOD}/api/cms_categories_create.dspy",
|
||||||
|
f"/{MOD}/api/cms_categories_update.dspy",
|
||||||
|
f"/{MOD}/api/cms_categories_delete.dspy",
|
||||||
|
f"/{MOD}/api/cms_categories_list.dspy",
|
||||||
|
f"/{MOD}/api/category_options.dspy",
|
||||||
|
# Sections
|
||||||
|
f"/{MOD}/cms_sections_list", f"/{MOD}/cms_sections_list/%",
|
||||||
|
f"/{MOD}/api/cms_sections_create.dspy",
|
||||||
|
f"/{MOD}/api/cms_sections_update.dspy",
|
||||||
|
f"/{MOD}/api/cms_sections_delete.dspy",
|
||||||
|
f"/{MOD}/api/cms_sections_list.dspy",
|
||||||
|
# Leads
|
||||||
|
f"/{MOD}/cms_leads_list", f"/{MOD}/cms_leads_list/%",
|
||||||
|
f"/{MOD}/api/cms_leads_create.dspy",
|
||||||
|
f"/{MOD}/api/cms_leads_update.dspy",
|
||||||
|
f"/{MOD}/api/cms_leads_delete.dspy",
|
||||||
|
f"/{MOD}/api/cms_leads_list.dspy",
|
||||||
|
# Site Config
|
||||||
|
f"/{MOD}/cms_site_config_list", f"/{MOD}/cms_site_config_list/%",
|
||||||
|
f"/{MOD}/api/cms_site_config_create.dspy",
|
||||||
|
f"/{MOD}/api/cms_site_config_update.dspy",
|
||||||
|
f"/{MOD}/api/cms_site_config_delete.dspy",
|
||||||
|
f"/{MOD}/api/cms_site_config_list.dspy",
|
||||||
|
# DD Approvals
|
||||||
|
f"/{MOD}/dd_approvals", f"/{MOD}/dd_approvals/%",
|
||||||
|
f"/{MOD}/api/dd_approvals_create.dspy",
|
||||||
|
f"/{MOD}/api/dd_approvals_update.dspy",
|
||||||
|
f"/{MOD}/api/dd_approvals_delete.dspy",
|
||||||
|
f"/{MOD}/api/dd_approvals_list.dspy",
|
||||||
|
# DD Approval Configs
|
||||||
|
f"/{MOD}/dd_approval_configs", f"/{MOD}/dd_approval_configs/%",
|
||||||
|
f"/{MOD}/api/dd_approval_configs_create.dspy",
|
||||||
|
f"/{MOD}/api/dd_approval_configs_update.dspy",
|
||||||
|
f"/{MOD}/api/dd_approval_configs_delete.dspy",
|
||||||
|
f"/{MOD}/api/dd_approval_configs_list.dspy",
|
||||||
|
# DingTalk
|
||||||
|
f"/{MOD}/api/submit_approval.dspy",
|
||||||
|
f"/{MOD}/api/dingtalk_callback.dspy",
|
||||||
|
]
|
||||||
|
|
||||||
|
def run(role, paths):
|
||||||
|
count = 0
|
||||||
|
for p in paths:
|
||||||
|
result = subprocess.run(
|
||||||
|
[PYTHON, SET_PERM, role, p],
|
||||||
|
cwd=PORTAL_ROOT, capture_output=True, text=True, env=env
|
||||||
|
)
|
||||||
|
status = "✓" if result.returncode == 0 else "✗"
|
||||||
|
print(f" {status} {role:20s} {p}")
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
print(f"=== CMS模块 RBAC权限初始化 ===")
|
||||||
|
print(f"Portal: {PORTAL_ROOT}")
|
||||||
|
n = run("owner.superuser", PATHS)
|
||||||
|
print(f"\n完成: {n} 个权限已设置")
|
||||||
11
start.sh
11
start.sh
@ -1,11 +0,0 @@
|
|||||||
#!/usr/bin/bash
|
|
||||||
# CMS独立应用启动脚本
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
WORKDIR="$(pwd)"
|
|
||||||
PIDFILE="$WORKDIR/cms.pid"
|
|
||||||
|
|
||||||
echo "启动 CMS Web Application (port 9090)..."
|
|
||||||
$WORKDIR/py3/bin/python $WORKDIR/app/cms.py -p 9090 -w $WORKDIR &
|
|
||||||
echo $! > $PIDFILE
|
|
||||||
echo "CMS started (PID: $(cat $PIDFILE))"
|
|
||||||
exit 0
|
|
||||||
22
stop.sh
22
stop.sh
@ -1,22 +0,0 @@
|
|||||||
#!/usr/bin/bash
|
|
||||||
# CMS独立应用停止脚本
|
|
||||||
cd "$(dirname "$0")"
|
|
||||||
PIDFILE="$(pwd)/cms.pid"
|
|
||||||
|
|
||||||
if [ -f "$PIDFILE" ]; then
|
|
||||||
PID=$(cat "$PIDFILE")
|
|
||||||
if kill -0 "$PID" 2>/dev/null; then
|
|
||||||
echo "停止 CMS (PID: $PID)..."
|
|
||||||
kill "$PID"
|
|
||||||
sleep 2
|
|
||||||
if kill -0 "$PID" 2>/dev/null; then
|
|
||||||
kill -9 "$PID"
|
|
||||||
fi
|
|
||||||
echo "CMS已停止"
|
|
||||||
else
|
|
||||||
echo "CMS进程已不存在"
|
|
||||||
fi
|
|
||||||
rm -f "$PIDFILE"
|
|
||||||
else
|
|
||||||
echo "未找到PID文件"
|
|
||||||
fi
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
content_type = params_kw.get('content_type', None)
|
content_type = params_kw.get('content_type', None)
|
||||||
|
|||||||
@ -1,34 +1,14 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
data = {'id': getID()}
|
data = {'id': getID()}
|
||||||
|
for field in ['org_id', 'name', 'parent_id', 'content_type', 'description', 'sort_order', 'display_config']:
|
||||||
v = params_kw.get('org_id', None)
|
v = params_kw.get(field, None)
|
||||||
if v is not None:
|
if v is not None:
|
||||||
data['org_id'] = v
|
data[field] = v
|
||||||
|
|
||||||
v = params_kw.get('name', None)
|
|
||||||
if v is not None:
|
|
||||||
data['name'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('parent_id', None)
|
|
||||||
if v is not None:
|
|
||||||
data['parent_id'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('content_type', None)
|
|
||||||
if v is not None:
|
|
||||||
data['content_type'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('description', None)
|
|
||||||
if v is not None:
|
|
||||||
data['description'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('sort_order', None)
|
|
||||||
if v is not None:
|
|
||||||
data['sort_order'] = v
|
|
||||||
|
|
||||||
await sor.C('cms_categories', data)
|
await sor.C('cms_categories', data)
|
||||||
return {'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}
|
return {'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
_id = params_kw.get('id', '')
|
_id = params_kw.get('id', '')
|
||||||
|
|||||||
@ -1,30 +1,9 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
ns = {'sort': 'sort_order asc'}
|
ns = {'sort': 'sort_order asc'}
|
||||||
|
|
||||||
# data_filter support
|
|
||||||
filter_json = params_kw.get('data_filter', None)
|
|
||||||
if filter_json:
|
|
||||||
from sqlor.filter import DBFilter
|
|
||||||
try:
|
|
||||||
filter_def = json.loads(filter_json) if isinstance(filter_json, str) else filter_json
|
|
||||||
dbf = DBFilter(filter_def)
|
|
||||||
conds = dbf.gen(params_kw)
|
|
||||||
ns.update(conds)
|
|
||||||
ns.update(dbf.consts)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Manual filter params
|
|
||||||
|
|
||||||
_content_type = params_kw.get('content_type', None)
|
|
||||||
if _content_type:
|
|
||||||
ns['content_type'] = _content_type
|
|
||||||
|
|
||||||
rows = await sor.R('cms_categories', ns)
|
rows = await sor.R('cms_categories', ns)
|
||||||
total = len(rows)
|
return {'status': 'ok', 'rows': rows, 'total': len(rows)}
|
||||||
return {'status': 'ok', 'rows': rows, 'total': total}
|
|
||||||
|
|||||||
@ -1,36 +1,17 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
data = {'id': params_kw.get('id', '')}
|
data = {'id': params_kw.get('id', '')}
|
||||||
if not data['id']:
|
if not data['id']:
|
||||||
return {'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}
|
return {'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}
|
||||||
|
|
||||||
v = params_kw.get('org_id', None)
|
for field in ['org_id', 'name', 'parent_id', 'content_type', 'description', 'sort_order', 'display_config']:
|
||||||
if v is not None:
|
v = params_kw.get(field, None)
|
||||||
data['org_id'] = v
|
if v is not None:
|
||||||
|
data[field] = v
|
||||||
v = params_kw.get('name', None)
|
|
||||||
if v is not None:
|
|
||||||
data['name'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('parent_id', None)
|
|
||||||
if v is not None:
|
|
||||||
data['parent_id'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('content_type', None)
|
|
||||||
if v is not None:
|
|
||||||
data['content_type'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('description', None)
|
|
||||||
if v is not None:
|
|
||||||
data['description'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('sort_order', None)
|
|
||||||
if v is not None:
|
|
||||||
data['sort_order'] = v
|
|
||||||
|
|
||||||
await sor.U('cms_categories', data)
|
await sor.U('cms_categories', data)
|
||||||
return {'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}
|
return {'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
data = {'id': getID()}
|
data = {'id': getID()}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
_id = params_kw.get('id', '')
|
_id = params_kw.get('id', '')
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
ns = {'sort': 'sort_order asc, created_at desc'}
|
ns = {'sort': 'sort_order asc, created_at desc'}
|
||||||
@ -20,7 +20,6 @@ async with db.sqlorContext(dbname) as sor:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Manual filter params
|
# Manual filter params
|
||||||
|
|
||||||
_content_type = params_kw.get('content_type', None)
|
_content_type = params_kw.get('content_type', None)
|
||||||
if _content_type:
|
if _content_type:
|
||||||
ns['content_type'] = _content_type
|
ns['content_type'] = _content_type
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
data = {'id': params_kw.get('id', '')}
|
data = {'id': params_kw.get('id', '')}
|
||||||
|
|||||||
@ -1,66 +1,16 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
data = {'id': getID()}
|
data = {'id': getID()}
|
||||||
|
for field in ['org_id', 'source', 'name', 'company', 'phone', 'email',
|
||||||
v = params_kw.get('org_id', None)
|
'industry', 'region', 'interest_products', 'message',
|
||||||
if v is not None:
|
'status', 'assigned_to', 'notes']:
|
||||||
data['org_id'] = v
|
v = params_kw.get(field, None)
|
||||||
|
if v is not None:
|
||||||
v = params_kw.get('source', None)
|
data[field] = v
|
||||||
if v is not None:
|
|
||||||
data['source'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('name', None)
|
|
||||||
if v is not None:
|
|
||||||
data['name'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('company', None)
|
|
||||||
if v is not None:
|
|
||||||
data['company'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('phone', None)
|
|
||||||
if v is not None:
|
|
||||||
data['phone'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('email', None)
|
|
||||||
if v is not None:
|
|
||||||
data['email'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('industry', None)
|
|
||||||
if v is not None:
|
|
||||||
data['industry'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('region', None)
|
|
||||||
if v is not None:
|
|
||||||
data['region'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('interest_products', None)
|
|
||||||
if v is not None:
|
|
||||||
data['interest_products'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('message', None)
|
|
||||||
if v is not None:
|
|
||||||
data['message'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('raw_text', None)
|
|
||||||
if v is not None:
|
|
||||||
data['raw_text'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('status', None)
|
|
||||||
if v is not None:
|
|
||||||
data['status'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('assigned_to', None)
|
|
||||||
if v is not None:
|
|
||||||
data['assigned_to'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('notes', None)
|
|
||||||
if v is not None:
|
|
||||||
data['notes'] = v
|
|
||||||
|
|
||||||
await sor.C('cms_leads', data)
|
await sor.C('cms_leads', data)
|
||||||
return {'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}
|
return {'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
_id = params_kw.get('id', '')
|
_id = params_kw.get('id', '')
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
ns = {'sort': 'created_at desc'}
|
ns = {'sort': 'created_at desc'}
|
||||||
@ -19,16 +19,5 @@ async with db.sqlorContext(dbname) as sor:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Manual filter params
|
|
||||||
|
|
||||||
_status = params_kw.get('status', None)
|
|
||||||
if _status:
|
|
||||||
ns['status'] = _status
|
|
||||||
|
|
||||||
_source = params_kw.get('source', None)
|
|
||||||
if _source:
|
|
||||||
ns['source'] = _source
|
|
||||||
|
|
||||||
rows = await sor.R('cms_leads', ns)
|
rows = await sor.R('cms_leads', ns)
|
||||||
total = len(rows)
|
return {'status': 'ok', 'rows': rows, 'total': len(rows)}
|
||||||
return {'status': 'ok', 'rows': rows, 'total': total}
|
|
||||||
|
|||||||
@ -1,68 +1,19 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
data = {'id': params_kw.get('id', '')}
|
data = {'id': params_kw.get('id', '')}
|
||||||
if not data['id']:
|
if not data['id']:
|
||||||
return {'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}
|
return {'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}
|
||||||
|
|
||||||
v = params_kw.get('org_id', None)
|
for field in ['org_id', 'source', 'name', 'company', 'phone', 'email',
|
||||||
if v is not None:
|
'industry', 'region', 'interest_products', 'message',
|
||||||
data['org_id'] = v
|
'status', 'assigned_to', 'notes']:
|
||||||
|
v = params_kw.get(field, None)
|
||||||
v = params_kw.get('source', None)
|
if v is not None:
|
||||||
if v is not None:
|
data[field] = v
|
||||||
data['source'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('name', None)
|
|
||||||
if v is not None:
|
|
||||||
data['name'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('company', None)
|
|
||||||
if v is not None:
|
|
||||||
data['company'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('phone', None)
|
|
||||||
if v is not None:
|
|
||||||
data['phone'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('email', None)
|
|
||||||
if v is not None:
|
|
||||||
data['email'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('industry', None)
|
|
||||||
if v is not None:
|
|
||||||
data['industry'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('region', None)
|
|
||||||
if v is not None:
|
|
||||||
data['region'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('interest_products', None)
|
|
||||||
if v is not None:
|
|
||||||
data['interest_products'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('message', None)
|
|
||||||
if v is not None:
|
|
||||||
data['message'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('raw_text', None)
|
|
||||||
if v is not None:
|
|
||||||
data['raw_text'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('status', None)
|
|
||||||
if v is not None:
|
|
||||||
data['status'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('assigned_to', None)
|
|
||||||
if v is not None:
|
|
||||||
data['assigned_to'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('notes', None)
|
|
||||||
if v is not None:
|
|
||||||
data['notes'] = v
|
|
||||||
|
|
||||||
await sor.U('cms_leads', data)
|
await sor.U('cms_leads', data)
|
||||||
return {'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}
|
return {'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}
|
||||||
|
|||||||
@ -1,42 +1,15 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
data = {'id': getID()}
|
data = {'id': getID()}
|
||||||
v = params_kw.get('org_id', None)
|
for field in ['org_id', 'section_key', 'title', 'subtitle', 'section_type', 'content_type',
|
||||||
if v is not None:
|
'sort_order', 'is_visible', 'display_config', 'style_config', 'static_content']:
|
||||||
data['org_id'] = v
|
v = params_kw.get(field, None)
|
||||||
v = params_kw.get('section_key', None)
|
if v is not None:
|
||||||
if v is not None:
|
data[field] = v
|
||||||
data['section_key'] = v
|
|
||||||
v = params_kw.get('title', None)
|
|
||||||
if v is not None:
|
|
||||||
data['title'] = v
|
|
||||||
v = params_kw.get('subtitle', None)
|
|
||||||
if v is not None:
|
|
||||||
data['subtitle'] = v
|
|
||||||
v = params_kw.get('section_type', None)
|
|
||||||
if v is not None:
|
|
||||||
data['section_type'] = v
|
|
||||||
v = params_kw.get('content_type', None)
|
|
||||||
if v is not None:
|
|
||||||
data['content_type'] = v
|
|
||||||
v = params_kw.get('sort_order', None)
|
|
||||||
if v is not None:
|
|
||||||
data['sort_order'] = v
|
|
||||||
v = params_kw.get('is_visible', None)
|
|
||||||
if v is not None:
|
|
||||||
data['is_visible'] = v
|
|
||||||
v = params_kw.get('display_config', None)
|
|
||||||
if v is not None:
|
|
||||||
data['display_config'] = v
|
|
||||||
v = params_kw.get('style_config', None)
|
|
||||||
if v is not None:
|
|
||||||
data['style_config'] = v
|
|
||||||
v = params_kw.get('static_content', None)
|
|
||||||
if v is not None:
|
|
||||||
data['static_content'] = v
|
|
||||||
await sor.C('cms_sections', data)
|
await sor.C('cms_sections', data)
|
||||||
return {'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}
|
return {'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
_id = params_kw.get('id', '')
|
_id = params_kw.get('id', '')
|
||||||
|
|||||||
@ -1,20 +1,9 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
ns = {'sort': 'sort_order asc'}
|
ns = {'sort': 'sort_order asc'}
|
||||||
filter_json = params_kw.get('data_filter', None)
|
|
||||||
if filter_json:
|
|
||||||
from sqlor.filter import DBFilter
|
|
||||||
try:
|
|
||||||
filter_def = json.loads(filter_json) if isinstance(filter_json, str) else filter_json
|
|
||||||
dbf = DBFilter(filter_def)
|
|
||||||
conds = dbf.gen(params_kw)
|
|
||||||
ns.update(conds)
|
|
||||||
ns.update(dbf.consts)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
rows = await sor.R('cms_sections', ns)
|
rows = await sor.R('cms_sections', ns)
|
||||||
return {'status': 'ok', 'rows': rows, 'total': len(rows)}
|
return {'status': 'ok', 'rows': rows, 'total': len(rows)}
|
||||||
|
|||||||
@ -1,44 +1,18 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
data = {'id': params_kw.get('id', '')}
|
data = {'id': params_kw.get('id', '')}
|
||||||
if not data['id']:
|
if not data['id']:
|
||||||
return {'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}
|
return {'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}
|
||||||
v = params_kw.get('org_id', None)
|
|
||||||
if v is not None:
|
for field in ['org_id', 'section_key', 'title', 'subtitle', 'section_type', 'content_type',
|
||||||
data['org_id'] = v
|
'sort_order', 'is_visible', 'display_config', 'style_config', 'static_content']:
|
||||||
v = params_kw.get('section_key', None)
|
v = params_kw.get(field, None)
|
||||||
if v is not None:
|
if v is not None:
|
||||||
data['section_key'] = v
|
data[field] = v
|
||||||
v = params_kw.get('title', None)
|
|
||||||
if v is not None:
|
|
||||||
data['title'] = v
|
|
||||||
v = params_kw.get('subtitle', None)
|
|
||||||
if v is not None:
|
|
||||||
data['subtitle'] = v
|
|
||||||
v = params_kw.get('section_type', None)
|
|
||||||
if v is not None:
|
|
||||||
data['section_type'] = v
|
|
||||||
v = params_kw.get('content_type', None)
|
|
||||||
if v is not None:
|
|
||||||
data['content_type'] = v
|
|
||||||
v = params_kw.get('sort_order', None)
|
|
||||||
if v is not None:
|
|
||||||
data['sort_order'] = v
|
|
||||||
v = params_kw.get('is_visible', None)
|
|
||||||
if v is not None:
|
|
||||||
data['is_visible'] = v
|
|
||||||
v = params_kw.get('display_config', None)
|
|
||||||
if v is not None:
|
|
||||||
data['display_config'] = v
|
|
||||||
v = params_kw.get('style_config', None)
|
|
||||||
if v is not None:
|
|
||||||
data['style_config'] = v
|
|
||||||
v = params_kw.get('static_content', None)
|
|
||||||
if v is not None:
|
|
||||||
data['static_content'] = v
|
|
||||||
await sor.U('cms_sections', data)
|
await sor.U('cms_sections', data)
|
||||||
return {'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}
|
return {'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}
|
||||||
|
|||||||
@ -1,34 +1,14 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
data = {'id': getID()}
|
data = {'id': getID()}
|
||||||
|
for field in ['org_id', 'config_group', 'config_key', 'config_value', 'config_type', 'sort_order']:
|
||||||
v = params_kw.get('org_id', None)
|
v = params_kw.get(field, None)
|
||||||
if v is not None:
|
if v is not None:
|
||||||
data['org_id'] = v
|
data[field] = v
|
||||||
|
|
||||||
v = params_kw.get('config_group', None)
|
|
||||||
if v is not None:
|
|
||||||
data['config_group'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('config_key', None)
|
|
||||||
if v is not None:
|
|
||||||
data['config_key'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('config_value', None)
|
|
||||||
if v is not None:
|
|
||||||
data['config_value'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('config_type', None)
|
|
||||||
if v is not None:
|
|
||||||
data['config_type'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('sort_order', None)
|
|
||||||
if v is not None:
|
|
||||||
data['sort_order'] = v
|
|
||||||
|
|
||||||
await sor.C('cms_site_config', data)
|
await sor.C('cms_site_config', data)
|
||||||
return {'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}
|
return {'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
_id = params_kw.get('id', '')
|
_id = params_kw.get('id', '')
|
||||||
|
|||||||
@ -1,30 +1,9 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
ns = {'sort': 'config_group asc, sort_order asc'}
|
ns = {'sort': 'config_group asc, sort_order asc'}
|
||||||
|
|
||||||
# data_filter support
|
|
||||||
filter_json = params_kw.get('data_filter', None)
|
|
||||||
if filter_json:
|
|
||||||
from sqlor.filter import DBFilter
|
|
||||||
try:
|
|
||||||
filter_def = json.loads(filter_json) if isinstance(filter_json, str) else filter_json
|
|
||||||
dbf = DBFilter(filter_def)
|
|
||||||
conds = dbf.gen(params_kw)
|
|
||||||
ns.update(conds)
|
|
||||||
ns.update(dbf.consts)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Manual filter params
|
|
||||||
|
|
||||||
_config_group = params_kw.get('config_group', None)
|
|
||||||
if _config_group:
|
|
||||||
ns['config_group'] = _config_group
|
|
||||||
|
|
||||||
rows = await sor.R('cms_site_config', ns)
|
rows = await sor.R('cms_site_config', ns)
|
||||||
total = len(rows)
|
return {'status': 'ok', 'rows': rows, 'total': len(rows)}
|
||||||
return {'status': 'ok', 'rows': rows, 'total': total}
|
|
||||||
|
|||||||
@ -1,36 +1,17 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
data = {'id': params_kw.get('id', '')}
|
data = {'id': params_kw.get('id', '')}
|
||||||
if not data['id']:
|
if not data['id']:
|
||||||
return {'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}
|
return {'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}
|
||||||
|
|
||||||
v = params_kw.get('org_id', None)
|
for field in ['org_id', 'config_group', 'config_key', 'config_value', 'config_type', 'sort_order']:
|
||||||
if v is not None:
|
v = params_kw.get(field, None)
|
||||||
data['org_id'] = v
|
if v is not None:
|
||||||
|
data[field] = v
|
||||||
v = params_kw.get('config_group', None)
|
|
||||||
if v is not None:
|
|
||||||
data['config_group'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('config_key', None)
|
|
||||||
if v is not None:
|
|
||||||
data['config_key'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('config_value', None)
|
|
||||||
if v is not None:
|
|
||||||
data['config_value'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('config_type', None)
|
|
||||||
if v is not None:
|
|
||||||
data['config_type'] = v
|
|
||||||
|
|
||||||
v = params_kw.get('sort_order', None)
|
|
||||||
if v is not None:
|
|
||||||
data['sort_order'] = v
|
|
||||||
|
|
||||||
await sor.U('cms_site_config', data)
|
await sor.U('cms_site_config', data)
|
||||||
return {'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}
|
return {'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
group = params_kw.get('group', None)
|
group = params_kw.get('group', None)
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
_id = params_kw.get('id', '')
|
_id = params_kw.get('id', '')
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
content_type = params_kw.get('content_type', None)
|
content_type = params_kw.get('content_type', None)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
ns = {'is_visible': '1', 'sort': 'sort_order asc'}
|
ns = {'is_visible': '1', 'sort': 'sort_order asc'}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
|
|
||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
content_id = params_kw.get('content_id', '')
|
content_id = params_kw.get('content_id', '')
|
||||||
@ -10,10 +10,9 @@ async with db.sqlorContext(dbname) as sor:
|
|||||||
else:
|
else:
|
||||||
# Update status to pending
|
# Update status to pending
|
||||||
await sor.U('cms_content', {'id': content_id, 'status': 'pending'})
|
await sor.U('cms_content', {'id': content_id, 'status': 'pending'})
|
||||||
|
|
||||||
# Try to call dingdingflow
|
# Call submit_approval from cms module
|
||||||
try:
|
try:
|
||||||
from dingdingflow.init import submit_approval
|
|
||||||
user_id = await get_user()
|
user_id = await get_user()
|
||||||
ns_detail = {'id': content_id}
|
ns_detail = {'id': content_id}
|
||||||
rows = await sor.R('cms_content', ns_detail)
|
rows = await sor.R('cms_content', ns_detail)
|
||||||
@ -22,5 +21,5 @@ async with db.sqlorContext(dbname) as sor:
|
|||||||
if result and result.get('approval_id'):
|
if result and result.get('approval_id'):
|
||||||
await sor.U('cms_content', {'id': content_id, 'approval_id': result['approval_id']})
|
await sor.U('cms_content', {'id': content_id, 'approval_id': result['approval_id']})
|
||||||
return {'widgettype': 'Message', 'options': {'text': '已提交审批', 'messagetype': 'success'}}
|
return {'widgettype': 'Message', 'options': {'text': '已提交审批', 'messagetype': 'success'}}
|
||||||
except ImportError:
|
except Exception as e:
|
||||||
return {'widgettype': 'Message', 'options': {'text': '审批模块未安装,状态已改为待审批', 'messagetype': 'warning'}}
|
return {'widgettype': 'Message', 'options': {'text': f'审批提交失败: {str(e)},状态已改为待审批', 'messagetype': 'warning'}}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
config = getConfig('.')
|
config = getConfig('.')
|
||||||
DBPools(config.databases)
|
DBPools(config.databases)
|
||||||
dbname = get_module_dbname('entcms')
|
dbname = get_module_dbname('cms')
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
@ -9,7 +9,7 @@ async with db.sqlorContext(dbname) as sor:
|
|||||||
'status': 'new',
|
'status': 'new',
|
||||||
'org_id': '0'
|
'org_id': '0'
|
||||||
}
|
}
|
||||||
for field in ['name', 'company', 'phone', 'email', 'industry', 'region',
|
for field in ['name', 'company', 'phone', 'email', 'industry', 'region',
|
||||||
'interest_products', 'message']:
|
'interest_products', 'message']:
|
||||||
v = params_kw.get(field, None)
|
v = params_kw.get(field, None)
|
||||||
if v is not None:
|
if v is not None:
|
||||||
@ -17,6 +17,6 @@ async with db.sqlorContext(dbname) as sor:
|
|||||||
|
|
||||||
await sor.C('cms_leads', data)
|
await sor.C('cms_leads', data)
|
||||||
return {
|
return {
|
||||||
'widgettype': 'Message',
|
'widgettype': 'Message',
|
||||||
'options': {'text': '感谢您的留言,我们会尽快联系您!', 'messagetype': 'success'}
|
'options': {'text': '感谢您的留言,我们会尽快联系您!', 'messagetype': 'success'}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
{% set all_cases = get_published_content('case', 20) %}
|
|
||||||
{
|
|
||||||
"widgettype": "VBox",
|
|
||||||
"options": {"width": "100%", "css": "site-root"},
|
|
||||||
"subwidgets": [
|
|
||||||
{
|
|
||||||
"widgettype": "Html",
|
|
||||||
"options": {
|
|
||||||
"html": "<nav class=\"nav-bar\"><a class=\"nav-logo\" href=\"{{entire_url('index.ui')}}\">开元云科技<\/a><ul class=\"nav-links\"><li><a href=\"{{entire_url('index.ui')}}#products\">产品架构<\/a><\/li><li><a href=\"{{entire_url('index.ui')}}#cases\">成功案例<\/a><\/li><li><a href=\"{{entire_url('news.ui')}}\">企业动态<\/a><\/li><\/ul><\/nav><section class=\"section\" style=\"padding-top:100px\"><h2 class=\"section-title\">成功案例<\/h2><p class=\"section-desc\">AI正在改变千行百业<\/p><div class=\"cases-grid\">{% for c in all_cases %}<div class=\"case-card\"><div class=\"case-tag\">{{c.tags or '行业案例'}}<\/div><div class=\"case-title\">{{c.title}}<\/div><div class=\"case-desc\">{{c.summary_text}}<\/div><\/div>{% endfor %}<\/div><div class=\"cta-banner\" style=\"margin-top:40px\"><div class=\"cta-text\">想了解这些方案如何落地?<\/div><a class=\"btn-primary\" href=\"{{entire_url('index.ui')}}#contact\">了解更多 → 联系销售<\/a><\/div><\/section><footer class=\"site-footer\"><p>© 2026 开元云科技 · 国家级高新技术企业 · 专精特新企业<\/p><\/footer>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,153 +0,0 @@
|
|||||||
/* ===== 开元云科技 官网交互脚本 ===== */
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
initScrollAnimations();
|
|
||||||
initProductCards();
|
|
||||||
initFloatingWidget();
|
|
||||||
initSmoothScroll();
|
|
||||||
});
|
|
||||||
|
|
||||||
/* === Scroll Fade-in Animations === */
|
|
||||||
function initScrollAnimations() {
|
|
||||||
var elements = document.querySelectorAll('.fade-in');
|
|
||||||
if (!elements.length) return;
|
|
||||||
|
|
||||||
var observer = new IntersectionObserver(function(entries) {
|
|
||||||
entries.forEach(function(entry) {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
entry.target.classList.add('visible');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, { threshold: 0.1 });
|
|
||||||
|
|
||||||
elements.forEach(function(el) { observer.observe(el); });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Product Card Expand === */
|
|
||||||
function initProductCards() {
|
|
||||||
var cards = document.querySelectorAll('.product-card');
|
|
||||||
cards.forEach(function(card) {
|
|
||||||
card.addEventListener('click', function() {
|
|
||||||
var wasActive = card.classList.contains('active');
|
|
||||||
// Close all
|
|
||||||
cards.forEach(function(c) { c.classList.remove('active'); });
|
|
||||||
// Toggle current
|
|
||||||
if (!wasActive) {
|
|
||||||
card.classList.add('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Floating Contact Widget === */
|
|
||||||
function initFloatingWidget() {
|
|
||||||
var avatar = document.querySelector('.float-avatar');
|
|
||||||
var panel = document.querySelector('.float-panel');
|
|
||||||
if (!avatar || !panel) return;
|
|
||||||
|
|
||||||
avatar.addEventListener('click', function(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
panel.classList.toggle('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close panel on outside click
|
|
||||||
document.addEventListener('click', function(e) {
|
|
||||||
if (!panel.contains(e.target) && !avatar.contains(e.target)) {
|
|
||||||
panel.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Panel option clicks -> show form
|
|
||||||
var options = panel.querySelectorAll('.panel-option');
|
|
||||||
options.forEach(function(opt) {
|
|
||||||
opt.addEventListener('click', function() {
|
|
||||||
var formType = opt.getAttribute('data-form');
|
|
||||||
var panelBody = panel.querySelector('.panel-body');
|
|
||||||
var panelForm = panel.querySelector('.panel-form');
|
|
||||||
if (panelBody) panelBody.style.display = 'none';
|
|
||||||
if (panelForm) {
|
|
||||||
panelForm.classList.add('active');
|
|
||||||
panelForm.setAttribute('data-type', formType);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Back button
|
|
||||||
var backBtn = panel.querySelector('.back-btn');
|
|
||||||
if (backBtn) {
|
|
||||||
backBtn.addEventListener('click', function() {
|
|
||||||
var panelBody = panel.querySelector('.panel-body');
|
|
||||||
var panelForm = panel.querySelector('.panel-form');
|
|
||||||
if (panelBody) panelBody.style.display = 'block';
|
|
||||||
if (panelForm) panelForm.classList.remove('active');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Form submit
|
|
||||||
var submitBtn = panel.querySelector('.submit-btn');
|
|
||||||
if (submitBtn) {
|
|
||||||
submitBtn.addEventListener('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
var form = panel.querySelector('.panel-form');
|
|
||||||
var name = form.querySelector('[name="name"]');
|
|
||||||
var phone = form.querySelector('[name="phone"]');
|
|
||||||
var company = form.querySelector('[name="company"]');
|
|
||||||
var message = form.querySelector('[name="message"]');
|
|
||||||
|
|
||||||
if (!phone || !phone.value.trim()) {
|
|
||||||
alert('请填写联系电话');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var data = {
|
|
||||||
name: name ? name.value : '',
|
|
||||||
phone: phone ? phone.value : '',
|
|
||||||
company: company ? company.value : '',
|
|
||||||
message: message ? message.value : '',
|
|
||||||
interest_products: form.getAttribute('data-type') || ''
|
|
||||||
};
|
|
||||||
|
|
||||||
// Submit via fetch
|
|
||||||
var submitUrl = form.getAttribute('data-submit-url');
|
|
||||||
if (!submitUrl) {
|
|
||||||
alert('提交成功,我们会尽快联系您!');
|
|
||||||
panel.classList.remove('active');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(submitUrl, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
body: JSON.stringify(data)
|
|
||||||
})
|
|
||||||
.then(function(r) { return r.json(); })
|
|
||||||
.then(function(resp) {
|
|
||||||
alert('感谢您的留言,我们会尽快联系您!');
|
|
||||||
panel.classList.remove('active');
|
|
||||||
// Reset form
|
|
||||||
if (name) name.value = '';
|
|
||||||
if (phone) phone.value = '';
|
|
||||||
if (company) company.value = '';
|
|
||||||
if (message) message.value = '';
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
console.error('Submit error:', err);
|
|
||||||
alert('提交失败,请稍后重试');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Smooth Scroll for Nav Links === */
|
|
||||||
function initSmoothScroll() {
|
|
||||||
var links = document.querySelectorAll('.nav-links a[href^="#"]');
|
|
||||||
links.forEach(function(link) {
|
|
||||||
link.addEventListener('click', function(e) {
|
|
||||||
var target = document.querySelector(this.getAttribute('href'));
|
|
||||||
if (target) {
|
|
||||||
e.preventDefault();
|
|
||||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,640 +0,0 @@
|
|||||||
/* ===== 开元云科技 官网样式系统 ===== */
|
|
||||||
/* 设计参考: OpenAI风格, 极简科技感 */
|
|
||||||
|
|
||||||
/* === CSS Variables === */
|
|
||||||
:root {
|
|
||||||
--brand-primary: #6C5CE7;
|
|
||||||
--brand-light: #A29BFE;
|
|
||||||
--brand-sky: #74B9FF;
|
|
||||||
--brand-gradient: linear-gradient(135deg, #6C5CE7, #A29BFE, #74B9FF);
|
|
||||||
--bg-dark: #0a0a0a;
|
|
||||||
--bg-card: #1A1A1A;
|
|
||||||
--bg-card-hover: #222222;
|
|
||||||
--border-dark: #222;
|
|
||||||
--border-active: #6C5CE7;
|
|
||||||
--text-primary: #FFFFFF;
|
|
||||||
--text-secondary: #999999;
|
|
||||||
--text-muted: #666666;
|
|
||||||
--max-width: 1100px;
|
|
||||||
--font-family: 'Noto Sans SC', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
||||||
--transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Reset & Base === */
|
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
||||||
|
|
||||||
body, html {
|
|
||||||
font-family: var(--font-family);
|
|
||||||
background: var(--bg-dark);
|
|
||||||
color: var(--text-primary);
|
|
||||||
line-height: 1.6;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
a { color: inherit; text-decoration: none; }
|
|
||||||
|
|
||||||
/* === Navigation === */
|
|
||||||
.nav-bar {
|
|
||||||
position: fixed;
|
|
||||||
top: 0; left: 0; right: 0;
|
|
||||||
z-index: 100;
|
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
background: rgba(10, 10, 10, 0.8);
|
|
||||||
border-bottom: 1px solid rgba(255,255,255,0.06);
|
|
||||||
padding: 0 48px;
|
|
||||||
height: 64px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-logo {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 700;
|
|
||||||
background: var(--brand-gradient);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links {
|
|
||||||
display: flex;
|
|
||||||
gap: 32px;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links a {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-links a:hover { color: var(--text-primary); }
|
|
||||||
|
|
||||||
.nav-cta {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 8px 20px;
|
|
||||||
background: var(--brand-primary);
|
|
||||||
color: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: var(--transition);
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-cta:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 20px rgba(108, 92, 231, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Hero Section === */
|
|
||||||
.hero-section {
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 120px 48px 80px;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-bg-glow {
|
|
||||||
position: absolute;
|
|
||||||
width: 800px;
|
|
||||||
height: 800px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: radial-gradient(circle, rgba(108, 92, 231, 0.15) 0%, transparent 70%);
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-content {
|
|
||||||
max-width: var(--max-width);
|
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-tag {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 6px 16px;
|
|
||||||
background: rgba(108, 92, 231, 0.15);
|
|
||||||
border: 1px solid rgba(108, 92, 231, 0.3);
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--brand-light);
|
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-tag .pulse-dot {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
background: var(--brand-primary);
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: pulse 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%, 100% { opacity: 1; }
|
|
||||||
50% { opacity: 0.4; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title {
|
|
||||||
font-size: 56px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.2;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-title .gradient-text {
|
|
||||||
background: var(--brand-gradient);
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-subtitle {
|
|
||||||
font-size: 18px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-weight: 300;
|
|
||||||
max-width: 600px;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 28px;
|
|
||||||
background: var(--brand-primary);
|
|
||||||
color: white;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: var(--transition);
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 24px rgba(108, 92, 231, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px 28px;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-primary);
|
|
||||||
border: 1px solid rgba(255,255,255,0.2);
|
|
||||||
border-radius: 10px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: var(--transition);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline:hover {
|
|
||||||
border-color: var(--brand-primary);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hero-mascot {
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 10%;
|
|
||||||
opacity: 0.12;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Section Common === */
|
|
||||||
.section {
|
|
||||||
padding: 80px 48px;
|
|
||||||
max-width: var(--max-width);
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 36px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-desc {
|
|
||||||
font-size: 16px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: 48px;
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Products (1+N+X) === */
|
|
||||||
.products-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border-dark);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 32px 24px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: var(--transition);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0; top: 0; bottom: 0;
|
|
||||||
width: 3px;
|
|
||||||
background: transparent;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card:hover {
|
|
||||||
transform: translateX(4px);
|
|
||||||
border-color: var(--brand-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card:hover::before {
|
|
||||||
background: var(--brand-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card.active {
|
|
||||||
background: rgba(108, 92, 231, 0.1);
|
|
||||||
border-color: var(--brand-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card.active::before {
|
|
||||||
background: var(--brand-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card .card-icon {
|
|
||||||
font-size: 32px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card .card-title {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card .card-desc {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-detail {
|
|
||||||
display: none;
|
|
||||||
margin-top: 16px;
|
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 1px solid var(--border-dark);
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: 1.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.product-card.active .product-detail {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Cases === */
|
|
||||||
.cases-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
gap: 20px;
|
|
||||||
margin-bottom: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.case-card {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border-dark);
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 24px;
|
|
||||||
transition: var(--transition);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.case-card:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
border-color: var(--brand-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.case-tag {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--brand-light);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.case-title {
|
|
||||||
font-size: 17px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.case-desc {
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === CTA Banner === */
|
|
||||||
.cta-banner {
|
|
||||||
background: linear-gradient(135deg, rgba(108, 92, 231, 0.15), rgba(116, 185, 255, 0.1));
|
|
||||||
border: 1px solid rgba(108, 92, 231, 0.2);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cta-text {
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === News === */
|
|
||||||
.news-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.news-view-all {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--brand-light);
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.news-view-all:hover { color: var(--brand-primary); }
|
|
||||||
|
|
||||||
.news-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.news-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--border-dark);
|
|
||||||
border-radius: 12px;
|
|
||||||
transition: var(--transition);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.news-item:hover {
|
|
||||||
border-color: var(--brand-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.news-date {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
white-space: nowrap;
|
|
||||||
min-width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.news-title {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Footer === */
|
|
||||||
.site-footer {
|
|
||||||
border-top: 1px solid rgba(255,255,255,0.06);
|
|
||||||
padding: 40px 48px;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Floating Contact Widget === */
|
|
||||||
.float-contact {
|
|
||||||
position: fixed;
|
|
||||||
right: 24px;
|
|
||||||
bottom: 24px;
|
|
||||||
z-index: 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.float-avatar {
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--brand-gradient);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 4px 20px rgba(108, 92, 231, 0.3);
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.float-avatar:hover {
|
|
||||||
transform: translateY(-3px) scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.float-avatar svg {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.float-bubble {
|
|
||||||
position: absolute;
|
|
||||||
right: 68px;
|
|
||||||
bottom: 12px;
|
|
||||||
background: white;
|
|
||||||
color: #333;
|
|
||||||
padding: 10px 16px;
|
|
||||||
border-radius: 12px;
|
|
||||||
font-size: 14px;
|
|
||||||
white-space: nowrap;
|
|
||||||
box-shadow: 0 4px 16px rgba(0,0,0,0.15);
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(10px);
|
|
||||||
transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.float-contact:hover .float-bubble {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.float-panel {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 70px;
|
|
||||||
right: 0;
|
|
||||||
width: 300px;
|
|
||||||
background: white;
|
|
||||||
border-radius: 16px;
|
|
||||||
box-shadow: 0 8px 40px rgba(0,0,0,0.2);
|
|
||||||
overflow: hidden;
|
|
||||||
display: none;
|
|
||||||
animation: panelSlideUp 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.float-panel.active { display: block; }
|
|
||||||
|
|
||||||
@keyframes panelSlideUp {
|
|
||||||
from { opacity: 0; transform: translateY(20px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-header {
|
|
||||||
background: var(--brand-gradient);
|
|
||||||
padding: 20px;
|
|
||||||
color: white;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-body {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-option {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: #f5f5f5;
|
|
||||||
border-radius: 10px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: var(--transition);
|
|
||||||
color: #333;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-option:hover {
|
|
||||||
background: var(--brand-primary);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-option svg {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-form {
|
|
||||||
padding: 16px;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-form.active { display: block; }
|
|
||||||
|
|
||||||
.panel-form input,
|
|
||||||
.panel-form textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border: 1px solid #e0e0e0;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
font-family: var(--font-family);
|
|
||||||
outline: none;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-form input:focus,
|
|
||||||
.panel-form textarea:focus {
|
|
||||||
border-color: var(--brand-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-form textarea { resize: vertical; min-height: 60px; }
|
|
||||||
|
|
||||||
.panel-form .submit-btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
background: var(--brand-primary);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-form .submit-btn:hover {
|
|
||||||
background: #5a4bd1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-form .back-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--brand-primary);
|
|
||||||
font-size: 13px;
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Responsive === */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.nav-bar { padding: 0 20px; }
|
|
||||||
.nav-links { display: none; }
|
|
||||||
|
|
||||||
.hero-section { padding: 100px 20px 60px; }
|
|
||||||
.hero-title { font-size: 32px; }
|
|
||||||
.hero-subtitle { font-size: 16px; }
|
|
||||||
.hero-buttons { flex-direction: column; }
|
|
||||||
|
|
||||||
.section { padding: 60px 20px; }
|
|
||||||
.section-title { font-size: 28px; }
|
|
||||||
|
|
||||||
.products-grid { grid-template-columns: 1fr; }
|
|
||||||
.cases-grid { grid-template-columns: 1fr; }
|
|
||||||
|
|
||||||
.cta-banner {
|
|
||||||
flex-direction: column;
|
|
||||||
text-align: center;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.news-item { flex-direction: column; gap: 8px; }
|
|
||||||
|
|
||||||
.site-footer { padding: 30px 20px; }
|
|
||||||
|
|
||||||
.float-panel { width: 280px; right: -8px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* === Scroll Animations === */
|
|
||||||
.fade-in {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
transition: opacity 0.6s ease, transform 0.6s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-in.visible {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
@ -1,77 +0,0 @@
|
|||||||
{
|
|
||||||
"widgettype": "VBox",
|
|
||||||
"options": {"width": "100%", "height": "100%", "padding": "20px"},
|
|
||||||
"subwidgets": [
|
|
||||||
{
|
|
||||||
"widgettype": "Text",
|
|
||||||
"options": {"label": "钉钉审批管理", "fontSize": "24px", "fontWeight": "bold", "marginBottom": "20px"}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"widgettype": "ResponsableBox",
|
|
||||||
"options": {"gap": "16px", "minWidth": "280px"},
|
|
||||||
"subwidgets": [
|
|
||||||
{
|
|
||||||
"widgettype": "VBox",
|
|
||||||
"options": {
|
|
||||||
"backgroundColor": "#FFFFFF",
|
|
||||||
"padding": "20px",
|
|
||||||
"borderRadius": "8px",
|
|
||||||
"cursor": "pointer",
|
|
||||||
"boxShadow": "0 2px 8px rgba(0,0,0,0.1)"
|
|
||||||
},
|
|
||||||
"binds": [{
|
|
||||||
"wid": "self",
|
|
||||||
"event": "click",
|
|
||||||
"actiontype": "urlwidget",
|
|
||||||
"target": "app.dingdingflow_content",
|
|
||||||
"options": {"url": "{{entire_url('dd_approvals/index.ui')}}"},
|
|
||||||
"mode": "replace"
|
|
||||||
}],
|
|
||||||
"subwidgets": [
|
|
||||||
{
|
|
||||||
"widgettype": "Text",
|
|
||||||
"options": {"label": "📋 审批记录", "fontSize": "18px", "fontWeight": "bold", "marginBottom": "8px"}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"widgettype": "Text",
|
|
||||||
"options": {"label": "查看和管理所有审批申请记录,包括待审批、已通过、已拒绝的审批", "fontSize": "13px", "color": "#666"}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"widgettype": "VBox",
|
|
||||||
"options": {
|
|
||||||
"backgroundColor": "#FFFFFF",
|
|
||||||
"padding": "20px",
|
|
||||||
"borderRadius": "8px",
|
|
||||||
"cursor": "pointer",
|
|
||||||
"boxShadow": "0 2px 8px rgba(0,0,0,0.1)"
|
|
||||||
},
|
|
||||||
"binds": [{
|
|
||||||
"wid": "self",
|
|
||||||
"event": "click",
|
|
||||||
"actiontype": "urlwidget",
|
|
||||||
"target": "app.dingdingflow_content",
|
|
||||||
"options": {"url": "{{entire_url('dd_approval_configs/index.ui')}}"},
|
|
||||||
"mode": "replace"
|
|
||||||
}],
|
|
||||||
"subwidgets": [
|
|
||||||
{
|
|
||||||
"widgettype": "Text",
|
|
||||||
"options": {"label": "⚙️ 审批流程配置", "fontSize": "18px", "fontWeight": "bold", "marginBottom": "8px"}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"widgettype": "Text",
|
|
||||||
"options": {"label": "配置不同业务类型的钉钉审批模板,设置审批流程参数", "fontSize": "13px", "color": "#666"}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"widgettype": "VBox",
|
|
||||||
"id": "app.dingdingflow_content",
|
|
||||||
"options": {"width": "100%", "flex": "1", "marginTop": "20px"}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"widgettype": "Menu",
|
|
||||||
"options": {
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"title": "钉钉审批",
|
|
||||||
"icon": "icon-approve",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"title": "审批管理",
|
|
||||||
"url": "{{entire_url('dingdingflow/index.ui')}}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "审批记录",
|
|
||||||
"url": "{{entire_url('dingdingflow/dd_approvals/index.ui')}}"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "流程配置",
|
|
||||||
"url": "{{entire_url('dingdingflow/dd_approval_configs/index.ui')}}"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
@ -1,13 +0,0 @@
|
|||||||
{% set news_items = get_published_content('news', 50) %}
|
|
||||||
{
|
|
||||||
"widgettype": "VBox",
|
|
||||||
"options": {"width": "100%", "css": "site-root"},
|
|
||||||
"subwidgets": [
|
|
||||||
{
|
|
||||||
"widgettype": "Html",
|
|
||||||
"options": {
|
|
||||||
"html": "<nav class=\"nav-bar\"><a class=\"nav-logo\" href=\"{{entire_url('index.ui')}}\">开元云科技<\/a><ul class=\"nav-links\"><li><a href=\"{{entire_url('index.ui')}}#products\">产品架构<\/a><\/li><li><a href=\"{{entire_url('index.ui')}}#cases\">成功案例<\/a><\/li><li><a href=\"{{entire_url('index.ui')}}#news\">企业动态<\/a><\/li><\/ul><\/nav><section class=\"section\" style=\"padding-top:100px\"><h2 class=\"section-title\">企业动态<\/h2><p class=\"section-desc\">了解开元云最新资讯与行业洞察<\/p><div class=\"news-list\">{% for item in news_items %}<a class=\"news-item\" href=\"{{entire_url('news_detail.ui')}}?id={{item.id}}\"><span class=\"news-date\">{{item.published_at or item.created_at}}<\/span><div><span class=\"news-title\">{{item.title}}<\/span>{% if item.summary_text %}<p style=\"font-size:13px;color:#666;margin-top:4px\">{{item.summary_text}}<\/p>{% endif %}<\/div><\/a>{% endfor %}<\/div><\/section><footer class=\"site-footer\"><p>© 2026 开元云科技 · 国家级高新技术企业 · 专精特新企业<\/p><\/footer>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
{% set article = get_content_detail(params_kw.get('id', '')) %}
|
|
||||||
{
|
|
||||||
"widgettype": "VBox",
|
|
||||||
"options": {"width": "100%", "css": "site-root"},
|
|
||||||
"subwidgets": [
|
|
||||||
{
|
|
||||||
"widgettype": "Html",
|
|
||||||
"options": {
|
|
||||||
"html": "<nav class=\"nav-bar\"><a class=\"nav-logo\" href=\"{{entire_url('index.ui')}}\">开元云科技<\/a><ul class=\"nav-links\"><li><a href=\"{{entire_url('index.ui')}}#products\">产品架构<\/a><\/li><li><a href=\"{{entire_url('index.ui')}}#cases\">成功案例<\/a><\/li><li><a href=\"{{entire_url('news.ui')}}\">企业动态<\/a><\/li><\/ul><\/nav><section class=\"section\" style=\"padding-top:100px;max-width:800px\">{% if article %}<a href=\"{{entire_url('news.ui')}}\" style=\"color:#A29BFE;font-size:14px;margin-bottom:24px;display:inline-block\">← 返回新闻列表<\/a><h1 class=\"section-title\" style=\"margin-bottom:12px\">{{article.title}}<\/h1><p style=\"font-size:13px;color:#666;margin-bottom:32px\">{{article.published_at or article.created_at}}{% if article.tags %} · {{article.tags}}{% endif %}<\/p>{% if article.image_url %}<img src=\"{{article.image_url}}\" style=\"width:100%;border-radius:12px;margin-bottom:32px\" /><\/img>{% endif %}<div style=\"font-size:16px;line-height:1.8;color:#ccc\">{{article.body or article.summary_text or ''}}<\/div>{% else %}<p style=\"color:#999\">文章不存在或已下线<\/p><a href=\"{{entire_url('news.ui')}}\" style=\"color:#A29BFE\">← 返回新闻列表<\/a>{% endif %}<\/section><footer class=\"site-footer\"><p>© 2026 开元云科技 · 国家级高新技术企业 · 专精特新企业<\/p><\/footer>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
{% set products = get_published_content('product', 50) %}
|
|
||||||
{
|
|
||||||
"widgettype": "VBox",
|
|
||||||
"options": {"width": "100%", "css": "site-root"},
|
|
||||||
"subwidgets": [
|
|
||||||
{
|
|
||||||
"widgettype": "Html",
|
|
||||||
"options": {
|
|
||||||
"html": "<nav class=\"nav-bar\"><a class=\"nav-logo\" href=\"{{entire_url('index.ui')}}\">开元云科技<\/a><ul class=\"nav-links\"><li><a href=\"{{entire_url('index.ui')}}#products\">产品架构<\/a><\/li><li><a href=\"{{entire_url('index.ui')}}#cases\">成功案例<\/a><\/li><li><a href=\"{{entire_url('news.ui')}}\">企业动态<\/a><\/li><\/ul><\/nav><section class=\"section\" style=\"padding-top:100px\"><h2 class=\"section-title\">产品架构<\/h2><p class=\"section-desc\">AI基础设施全栈解决方案<\/p><div class=\"cases-grid\">{% for p in products %}<div class=\"case-card\"><div class=\"case-tag\">{{p.tags or '核心产品'}}<\/div><div class=\"case-title\">{{p.title}}<\/div><div class=\"case-desc\">{{p.summary_text}}<\/div><\/div>{% endfor %}<\/div><div class=\"cta-banner\" style=\"margin-top:40px\"><div class=\"cta-text\">需要定制化方案?<\/div><a class=\"btn-primary\" href=\"{{entire_url('index.ui')}}#contact\">联系解决方案团队 →<\/a><\/div><\/section><footer class=\"site-footer\"><p>© 2026 开元云科技 · 国家级高新技术企业 · 专精特新企业<\/p><\/footer>"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user