feat: 开元云科技官网CMS系统初始版本
entcms模块: - 4个数据表(cms_content/cms_categories/cms_leads/cms_site_config) - 22个.dspy API(含公开API和data_filter) - 4个公开页面(首页/新闻/案例)+管理后台 - 完整营销站点CSS/JS(暗色主题/渐变/动画/响应式) - 云宝SVG线稿占位符 - RBAC权限配置 dingdingflow模块: - 2个数据表(dd_approvals/dd_approval_configs) - 10个.dspy API(含钉钉回调endpoint) - 钉钉API客户端(环境变量配置,开发模式mock) - 管理UI 文档: 架构设计/53条测试用例/开发日志
This commit is contained in:
commit
5cfb0e867b
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
*.pyc
|
||||
__pycache__/
|
||||
mysql.ddl.sql
|
||||
37
README.md
Normal file
37
README.md
Normal file
@ -0,0 +1,37 @@
|
||||
# 开元云科技 - 企业官网CMS系统
|
||||
|
||||
企业官网内容管理系统 + 钉钉审批流程,基于Sage/bricks-framework开发。
|
||||
|
||||
## 模块
|
||||
|
||||
| 模块 | 说明 |
|
||||
|------|------|
|
||||
| **entcms** | 企业CMS - 新闻/案例/产品/Banner/线索管理 |
|
||||
| **dingdingflow** | 钉钉审批流程 - 内容发布审批工作流 |
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 1. 构建并安装
|
||||
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)
|
||||
- [开发日志](docs/)
|
||||
|
||||
## 环境变量 (dingdingflow)
|
||||
```
|
||||
DINGTALK_APP_KEY=xxx
|
||||
DINGTALK_APP_SECRET=xxx
|
||||
DINGTALK_AGENT_ID=xxx
|
||||
```
|
||||
87
build.sh
Executable file
87
build.sh
Executable file
@ -0,0 +1,87 @@
|
||||
#!/bin/bash
|
||||
# CMS项目构建脚本
|
||||
# 构建 entcms + dingdingflow 模块并集成到Sage系统
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
echo "=== CMS项目构建 ==="
|
||||
echo "Script dir: $SCRIPT_DIR"
|
||||
|
||||
# 查找Sage根目录
|
||||
SAGE_ROOT=""
|
||||
for candidate in "$SCRIPT_DIR/../.." "$HOME/repos/sage" "$HOME/sage"; do
|
||||
if [ -d "$candidate/wwwroot" ] && [ -d "$candidate/py3/bin" ]; then
|
||||
SAGE_ROOT="$(cd "$candidate" && pwd)"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$SAGE_ROOT" ]; then
|
||||
echo "ERROR: 找不到Sage根目录"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Sage root: $SAGE_ROOT"
|
||||
PY="$SAGE_ROOT/py3/bin/python"
|
||||
PIP="$SAGE_ROOT/py3/bin/pip"
|
||||
|
||||
# 安装模块
|
||||
for mod in entcms dingdingflow; do
|
||||
echo ""
|
||||
echo "=== 安装 $mod ==="
|
||||
cd "$SCRIPT_DIR/$mod"
|
||||
$PIP install -e .
|
||||
|
||||
# 生成DDL
|
||||
if [ -d "models" ] && [ "$(ls models/*.json 2>/dev/null)" ]; then
|
||||
echo "生成DDL..."
|
||||
cd models
|
||||
$PY -c "from sqlor.ddl_template_mysql import DDLTemplate; print(DDLTemplate().generate('.'))" > ../mysql.ddl.sql 2>/dev/null || json2ddl mysql . > ../mysql.ddl.sql 2>/dev/null || echo "DDL generation skipped (json2ddl not available)"
|
||||
cd ..
|
||||
fi
|
||||
|
||||
# 生成CRUD UI
|
||||
if [ -d "json" ] && [ "$(ls json/*.json 2>/dev/null)" ]; then
|
||||
echo "生成CRUD UI..."
|
||||
cd json
|
||||
xls2ui -m ../models -o ../wwwroot $mod *.json 2>/dev/null || echo "CRUD UI generation skipped (xls2ui not available)"
|
||||
cd ..
|
||||
fi
|
||||
|
||||
# 链接wwwroot到Sage
|
||||
MODULE_WWWROOT="$SAGE_ROOT/wwwroot/$mod"
|
||||
mkdir -p "$MODULE_WWWROOT/api"
|
||||
|
||||
# 链接UI/CSS/JS文件
|
||||
for f in "$SCRIPT_DIR/$mod/wwwroot"/*.ui "$SCRIPT_DIR/$mod/wwwroot"/*.css "$SCRIPT_DIR/$mod/wwwroot"/*.js; do
|
||||
[ -f "$f" ] || continue
|
||||
fname=$(basename "$f")
|
||||
ln -sf "$f" "$MODULE_WWWROOT/$fname"
|
||||
done
|
||||
|
||||
# 链接api目录下的.dspy文件
|
||||
for f in "$SCRIPT_DIR/$mod/wwwroot/api"/*.dspy; do
|
||||
[ -f "$f" ] || continue
|
||||
fname=$(basename "$f")
|
||||
ln -sf "$f" "$MODULE_WWWROOT/api/$fname"
|
||||
done
|
||||
|
||||
# 链接生成的CRUD目录
|
||||
for d in "$SCRIPT_DIR/$mod/wwwroot"/*/; do
|
||||
[ -d "$d" ] || continue
|
||||
dname=$(basename "$d")
|
||||
case "$dname" in api|styles|scripts) continue ;; esac
|
||||
ln -sf "$d" "$MODULE_WWWROOT/$dname"
|
||||
done
|
||||
|
||||
echo "$mod 安装完成"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "=== 构建完成 ==="
|
||||
echo "请执行以下步骤完成集成:"
|
||||
echo "1. 编辑 $SAGE_ROOT/app/sage.py 添加模块导入"
|
||||
echo "2. 编辑 $SAGE_ROOT/build.sh 添加模块到安装循环"
|
||||
echo "3. 执行 RBAC 权限配置"
|
||||
echo "4. 重启Sage服务"
|
||||
20
dingdingflow/README.md
Normal file
20
dingdingflow/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
# 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
dingdingflow/dingdingflow/__init__.py
Normal file
1
dingdingflow/dingdingflow/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
# dingdingflow module
|
||||
240
dingdingflow/dingdingflow/dingtalk_client.py
Normal file
240
dingdingflow/dingdingflow/dingtalk_client.py
Normal file
@ -0,0 +1,240 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
DingTalk API Client for approval workflow integration.
|
||||
Reads credentials from environment variables (DINGTALK_APP_KEY, DINGTALK_APP_SECRET, DINGTALK_AGENT_ID).
|
||||
Gracefully handles missing credentials by returning mock responses in dev mode.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DingTalkClient:
|
||||
"""Client for DingTalk Open API - approval workflow operations."""
|
||||
|
||||
BASE_URL = "https://oapi.dingtalk.com"
|
||||
|
||||
def __init__(self):
|
||||
self.app_key = os.environ.get("DINGTALK_APP_KEY", "")
|
||||
self.app_secret = os.environ.get("DINGTALK_APP_SECRET", "")
|
||||
self.agent_id = os.environ.get("DINGTALK_AGENT_ID", "")
|
||||
self.callback_token = os.environ.get("DINGTALK_CALLBACK_TOKEN", "")
|
||||
self._access_token = None
|
||||
self._token_expires_at = 0
|
||||
|
||||
if not self.app_key or not self.app_secret:
|
||||
logger.warning(
|
||||
"DingTalk credentials not configured (DINGTALK_APP_KEY / DINGTALK_APP_SECRET). "
|
||||
"Running in dev/mock mode - API calls will return mock responses."
|
||||
)
|
||||
|
||||
@property
|
||||
def is_dev_mode(self):
|
||||
"""Return True if credentials are missing (dev/mock mode)."""
|
||||
return not self.app_key or not self.app_secret
|
||||
|
||||
def _http_post(self, url, data=None, params=None):
|
||||
"""Make HTTP POST request and return parsed JSON response."""
|
||||
if params:
|
||||
query = "&".join(f"{k}={v}" for k, v in params.items())
|
||||
url = f"{url}?{query}"
|
||||
|
||||
body = json.dumps(data).encode("utf-8") if data else b""
|
||||
req = urllib.request.Request(
|
||||
url,
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json"},
|
||||
method="POST",
|
||||
)
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
resp_data = resp.read().decode("utf-8")
|
||||
return json.loads(resp_data)
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode("utf-8", errors="replace")
|
||||
logger.error("DingTalk API HTTP error %s: %s", e.code, error_body)
|
||||
return {"errcode": e.code, "errmsg": f"HTTP {e.code}: {error_body}"}
|
||||
except urllib.error.URLError as e:
|
||||
logger.error("DingTalk API connection error: %s", str(e))
|
||||
return {"errcode": -1, "errmsg": f"Connection error: {str(e)}"}
|
||||
except Exception as e:
|
||||
logger.error("DingTalk API unexpected error: %s", str(e))
|
||||
return {"errcode": -1, "errmsg": str(e)}
|
||||
|
||||
def _http_get(self, url, params=None):
|
||||
"""Make HTTP GET request and return parsed JSON response."""
|
||||
if params:
|
||||
query = "&".join(f"{k}={v}" for k, v in params.items())
|
||||
url = f"{url}?{query}"
|
||||
|
||||
req = urllib.request.Request(url, headers={"Content-Type": "application/json"})
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
resp_data = resp.read().decode("utf-8")
|
||||
return json.loads(resp_data)
|
||||
except urllib.error.HTTPError as e:
|
||||
error_body = e.read().decode("utf-8", errors="replace")
|
||||
logger.error("DingTalk API HTTP error %s: %s", e.code, error_body)
|
||||
return {"errcode": e.code, "errmsg": f"HTTP {e.code}: {error_body}"}
|
||||
except Exception as e:
|
||||
logger.error("DingTalk API unexpected error: %s", str(e))
|
||||
return {"errcode": -1, "errmsg": str(e)}
|
||||
|
||||
def get_access_token(self):
|
||||
"""
|
||||
Get DingTalk access token. Caches token until expiry.
|
||||
Returns token string or empty string on failure.
|
||||
"""
|
||||
if self.is_dev_mode:
|
||||
logger.info("Dev mode: returning mock access token")
|
||||
return "mock_access_token_dev"
|
||||
|
||||
# Return cached token if still valid (with 5-min buffer)
|
||||
now = time.time()
|
||||
if self._access_token and now < self._token_expires_at - 300:
|
||||
return self._access_token
|
||||
|
||||
url = f"{self.BASE_URL}/gettoken"
|
||||
params = {"appkey": self.app_key, "appsecret": self.app_secret}
|
||||
result = self._http_get(url, params=params)
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
self._access_token = result.get("access_token", "")
|
||||
expires_in = result.get("expires_in", 7200)
|
||||
self._token_expires_at = now + expires_in
|
||||
logger.info("DingTalk access token obtained, expires in %ss", expires_in)
|
||||
return self._access_token
|
||||
else:
|
||||
logger.error(
|
||||
"Failed to get DingTalk access token: %s",
|
||||
result.get("errmsg", "unknown error"),
|
||||
)
|
||||
return ""
|
||||
|
||||
def create_approval_instance(self, process_code, form_data, originator_user_id):
|
||||
"""
|
||||
Create a DingTalk approval instance.
|
||||
|
||||
Args:
|
||||
process_code: DingTalk approval template code (from dd_approval_configs)
|
||||
form_data: List of form component values, e.g.:
|
||||
[{"name": "审批类型", "value": "内容发布"}, ...]
|
||||
originator_user_id: DingTalk user ID of the applicant
|
||||
|
||||
Returns:
|
||||
dict with keys:
|
||||
- success (bool)
|
||||
- instance_id (str) - DingTalk process instance ID
|
||||
- errmsg (str) - error message if failed
|
||||
"""
|
||||
if self.is_dev_mode:
|
||||
mock_instance_id = f"mock_instance_{int(time.time())}"
|
||||
logger.info(
|
||||
"Dev mode: mock approval instance created: %s (process_code=%s)",
|
||||
mock_instance_id,
|
||||
process_code,
|
||||
)
|
||||
return {
|
||||
"success": True,
|
||||
"instance_id": mock_instance_id,
|
||||
"errmsg": "",
|
||||
}
|
||||
|
||||
token = self.get_access_token()
|
||||
if not token:
|
||||
return {"success": False, "instance_id": "", "errmsg": "Failed to get access token"}
|
||||
|
||||
url = f"{self.BASE_URL}/topapi/processinstance/create"
|
||||
payload = {
|
||||
"agent_id": int(self.agent_id) if self.agent_id else 0,
|
||||
"process_code": process_code,
|
||||
"originator_user_id": originator_user_id,
|
||||
"dept_id": -1,
|
||||
"form_component_values": form_data,
|
||||
}
|
||||
|
||||
result = self._http_post(url, data=payload, params={"access_token": token})
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
instance_id = result.get("process_instance_id", "")
|
||||
logger.info("DingTalk approval instance created: %s", instance_id)
|
||||
return {"success": True, "instance_id": instance_id, "errmsg": ""}
|
||||
else:
|
||||
errmsg = result.get("errmsg", "unknown error")
|
||||
logger.error("Failed to create DingTalk approval: %s", errmsg)
|
||||
return {"success": False, "instance_id": "", "errmsg": errmsg}
|
||||
|
||||
def get_approval_instance(self, instance_id):
|
||||
"""
|
||||
Get DingTalk approval instance details.
|
||||
|
||||
Args:
|
||||
instance_id: DingTalk process instance ID
|
||||
|
||||
Returns:
|
||||
dict with keys:
|
||||
- success (bool)
|
||||
- status (str) - NEW/RUNNING/COMPLETED/TERMINATED
|
||||
- result (str) - agree/refuse (only when status=COMPLETED)
|
||||
- data (dict) - full instance data
|
||||
- errmsg (str) - error message if failed
|
||||
"""
|
||||
if self.is_dev_mode:
|
||||
logger.info("Dev mode: mock get approval instance: %s", instance_id)
|
||||
return {
|
||||
"success": True,
|
||||
"status": "COMPLETED",
|
||||
"result": "agree",
|
||||
"data": {
|
||||
"process_instance_id": instance_id,
|
||||
"status": "COMPLETED",
|
||||
"result": "agree",
|
||||
},
|
||||
"errmsg": "",
|
||||
}
|
||||
|
||||
token = self.get_access_token()
|
||||
if not token:
|
||||
return {"success": False, "status": "", "result": "", "data": {}, "errmsg": "Failed to get access token"}
|
||||
|
||||
url = f"{self.BASE_URL}/topapi/processinstance/get"
|
||||
payload = {"process_instance_id": instance_id}
|
||||
|
||||
result = self._http_post(url, data=payload, params={"access_token": token})
|
||||
|
||||
if result.get("errcode") == 0:
|
||||
pi = result.get("process_instance", {})
|
||||
status = pi.get("status", "")
|
||||
pi_result = pi.get("result", "")
|
||||
return {
|
||||
"success": True,
|
||||
"status": status,
|
||||
"result": pi_result,
|
||||
"data": pi,
|
||||
"errmsg": "",
|
||||
}
|
||||
else:
|
||||
errmsg = result.get("errmsg", "unknown error")
|
||||
logger.error("Failed to get DingTalk approval instance: %s", errmsg)
|
||||
return {"success": False, "status": "", "result": "", "data": {}, "errmsg": errmsg}
|
||||
|
||||
|
||||
# Module-level singleton
|
||||
_client_instance = None
|
||||
|
||||
|
||||
def get_dingtalk_client():
|
||||
"""Get or create the DingTalkClient singleton."""
|
||||
global _client_instance
|
||||
if _client_instance is None:
|
||||
_client_instance = DingTalkClient()
|
||||
return _client_instance
|
||||
432
dingdingflow/dingdingflow/init.py
Normal file
432
dingdingflow/dingdingflow/init.py
Normal file
@ -0,0 +1,432 @@
|
||||
#!/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)
|
||||
|
||||
# TODO: Notify entcms module about status change
|
||||
# This would trigger content status update in the CMS
|
||||
biz_type = getattr(record, "biz_type", "")
|
||||
biz_id = getattr(record, "biz_id", "")
|
||||
logger.info(
|
||||
"Callback: should notify entcms: biz_type=%s, biz_id=%s, status=%s",
|
||||
biz_type, biz_id, new_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
|
||||
25
dingdingflow/json/dd_approval_configs.json
Normal file
25
dingdingflow/json/dd_approval_configs.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"tblname": "dd_approval_configs",
|
||||
"alias": "dd_approval_configs",
|
||||
"title": "审批流程配置",
|
||||
"params": {
|
||||
"sortby": ["biz_type"],
|
||||
"browserfields": {
|
||||
"exclouded": ["id", "form_config"],
|
||||
"alters": {
|
||||
"is_active": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{"value": "1", "text": "启用"},
|
||||
{"value": "0", "text": "停用"}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/dd_approval_configs_create.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/dd_approval_configs_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/dd_approval_configs_delete.dspy')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
42
dingdingflow/json/dd_approvals.json
Normal file
42
dingdingflow/json/dd_approvals.json
Normal file
@ -0,0 +1,42 @@
|
||||
{
|
||||
"tblname": "dd_approvals",
|
||||
"alias": "dd_approvals",
|
||||
"title": "审批记录",
|
||||
"params": {
|
||||
"sortby": ["created_at desc"],
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{"field": "status", "op": "=", "var": "status_filter"},
|
||||
{"field": "biz_type", "op": "=", "var": "biz_type_filter"},
|
||||
{"field": "title", "op": "LIKE", "var": "title_filter"}
|
||||
]
|
||||
},
|
||||
"browserfields": {
|
||||
"exclouded": ["id"],
|
||||
"alters": {
|
||||
"status": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{"value": "pending", "text": "待审批"},
|
||||
{"value": "approved", "text": "已通过"},
|
||||
{"value": "rejected", "text": "已拒绝"},
|
||||
{"value": "cancelled", "text": "已取消"}
|
||||
]
|
||||
},
|
||||
"biz_type": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{"value": "content_publish", "text": "内容发布"},
|
||||
{"value": "content_update", "text": "内容修改"},
|
||||
{"value": "content_delete", "text": "内容删除"}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/dd_approvals_create.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/dd_approvals_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/dd_approvals_delete.dspy')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
85
dingdingflow/models/dd_approval_configs.json
Normal file
85
dingdingflow/models/dd_approval_configs.json
Normal file
@ -0,0 +1,85 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "dd_approval_configs",
|
||||
"title": "审批流程配置表",
|
||||
"primary": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "组织ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "biz_type",
|
||||
"title": "业务类型",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "biz_type_title",
|
||||
"title": "业务类型名称",
|
||||
"type": "str",
|
||||
"length": 100
|
||||
},
|
||||
{
|
||||
"name": "process_code",
|
||||
"title": "钉钉审批模板编码",
|
||||
"type": "str",
|
||||
"length": 100
|
||||
},
|
||||
{
|
||||
"name": "agent_id",
|
||||
"title": "钉钉应用AgentId",
|
||||
"type": "str",
|
||||
"length": 100
|
||||
},
|
||||
{
|
||||
"name": "form_config",
|
||||
"title": "表单字段配置JSON",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "is_active",
|
||||
"title": "是否启用(1/0)",
|
||||
"type": "str",
|
||||
"length": 1,
|
||||
"default": "1"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "更新时间",
|
||||
"type": "timestamp"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_apvcfg_org_type",
|
||||
"idxtype": "unique",
|
||||
"idxfields": [
|
||||
"org_id",
|
||||
"biz_type"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": []
|
||||
}
|
||||
114
dingdingflow/models/dd_approvals.json
Normal file
114
dingdingflow/models/dd_approvals.json
Normal file
@ -0,0 +1,114 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "dd_approvals",
|
||||
"title": "审批记录表",
|
||||
"primary": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "组织ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "biz_type",
|
||||
"title": "业务类型(content_publish等)",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "biz_id",
|
||||
"title": "业务数据ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"title": "审批标题",
|
||||
"type": "str",
|
||||
"length": 255,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "applicant_id",
|
||||
"title": "申请人ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "approver_id",
|
||||
"title": "审批人ID",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "dingtalk_instance_id",
|
||||
"title": "钉钉审批实例ID",
|
||||
"type": "str",
|
||||
"length": 100
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"title": "状态(pending/approved/rejected/cancelled)",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "pending"
|
||||
},
|
||||
{
|
||||
"name": "comment",
|
||||
"title": "审批意见",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp"
|
||||
},
|
||||
{
|
||||
"name": "completed_at",
|
||||
"title": "完成时间",
|
||||
"type": "datetime"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_approval_biz",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"biz_type",
|
||||
"biz_id"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_approval_status",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"org_id",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_approval_applicant",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"applicant_id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
18
dingdingflow/pyproject.toml
Normal file
18
dingdingflow/pyproject.toml
Normal file
@ -0,0 +1,18 @@
|
||||
[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*"]
|
||||
60
dingdingflow/scripts/load_path.py
Normal file
60
dingdingflow/scripts/load_path.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""
|
||||
dingdingflow RBAC权限配置
|
||||
用法: cd ~/repos/sage && ./py3/bin/python ~/repos/cms/dingdingflow/scripts/load_path.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
def find_sage_root():
|
||||
for candidate in [
|
||||
os.path.expanduser("~/repos/sage"),
|
||||
os.path.expanduser("~/sage"),
|
||||
]:
|
||||
if os.path.isdir(os.path.join(candidate, "wwwroot")) and \
|
||||
os.path.isdir(os.path.join(candidate, "py3")):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
sage_root = find_sage_root()
|
||||
if not sage_root:
|
||||
print("ERROR: Cannot find Sage root directory")
|
||||
sys.exit(1)
|
||||
|
||||
python = os.path.join(sage_root, "py3", "bin", "python")
|
||||
set_perm = os.path.join(sage_root, "set_role_perm.py")
|
||||
|
||||
paths_any = [
|
||||
# 钉钉回调是公开endpoint
|
||||
"/dingdingflow/api/dingtalk_callback.dspy",
|
||||
"/dingdingflow/menu.ui",
|
||||
]
|
||||
|
||||
paths_logined = [
|
||||
"/dingdingflow",
|
||||
"/dingdingflow/index.ui",
|
||||
"/dingdingflow/dd_approvals",
|
||||
"/dingdingflow/dd_approvals/%",
|
||||
"/dingdingflow/dd_approval_configs",
|
||||
"/dingdingflow/dd_approval_configs/%",
|
||||
"/dingdingflow/api/dd_approvals_create.dspy",
|
||||
"/dingdingflow/api/dd_approvals_update.dspy",
|
||||
"/dingdingflow/api/dd_approvals_delete.dspy",
|
||||
"/dingdingflow/api/dd_approvals_list.dspy",
|
||||
"/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/api/submit_approval.dspy",
|
||||
]
|
||||
|
||||
def set_perms(role, paths):
|
||||
for path in paths:
|
||||
cmd = [python, set_perm, role, path]
|
||||
print(f" {role:20s} {path}")
|
||||
subprocess.run(cmd, cwd=sage_root, capture_output=True)
|
||||
|
||||
print("=== dingdingflow RBAC权限配置 ===")
|
||||
set_perms("any", paths_any)
|
||||
set_perms("logined", paths_logined)
|
||||
print("完成")
|
||||
41
dingdingflow/wwwroot/api/dd_approval_configs_create.dspy
Normal file
41
dingdingflow/wwwroot/api/dd_approval_configs_create.dspy
Normal file
@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""dd_approval_configs create API"""
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
biz_type = params_kw.get('biz_type', '')
|
||||
biz_type_title = params_kw.get('biz_type_title', '')
|
||||
process_code = params_kw.get('process_code', '')
|
||||
|
||||
if not biz_type:
|
||||
result['options'] = {'title': 'Error', 'message': 'biz_type is required', 'type': 'error'}
|
||||
else:
|
||||
new_id = uuid()
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
now_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.sqlExe(
|
||||
"INSERT INTO dd_approval_configs (id, org_id, biz_type, biz_type_title, process_code, agent_id, form_config, is_active, created_at, updated_at) VALUES (${id}$, ${org_id}$, ${biz_type}$, ${biz_type_title}$, ${process_code}$, ${agent_id}$, ${form_config}$, ${is_active}$, ${created_at}$, ${updated_at}$)",
|
||||
{
|
||||
'id': new_id,
|
||||
'org_id': org_id,
|
||||
'biz_type': biz_type,
|
||||
'biz_type_title': biz_type_title,
|
||||
'process_code': process_code,
|
||||
'agent_id': params_kw.get('agent_id', ''),
|
||||
'form_config': params_kw.get('form_config', ''),
|
||||
'is_active': params_kw.get('is_active', '1'),
|
||||
'created_at': now_str,
|
||||
'updated_at': now_str
|
||||
}
|
||||
)
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批配置创建成功', 'type': 'success'}}
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'创建失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
20
dingdingflow/wwwroot/api/dd_approval_configs_delete.dspy
Normal file
20
dingdingflow/wwwroot/api/dd_approval_configs_delete.dspy
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""dd_approval_configs delete API"""
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
record_id = params_kw.get('id', '')
|
||||
if not record_id:
|
||||
result['options'] = {'title': 'Error', 'message': 'ID is required', 'type': 'error'}
|
||||
else:
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.sqlExe("DELETE FROM dd_approval_configs WHERE id=${id}$", {'id': record_id})
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批配置删除成功', 'type': 'success'}}
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'删除失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
58
dingdingflow/wwwroot/api/dd_approval_configs_list.dspy
Normal file
58
dingdingflow/wwwroot/api/dd_approval_configs_list.dspy
Normal file
@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""dd_approval_configs list API"""
|
||||
|
||||
result = {'success': False, 'rows': [], 'total': 0}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
where_clauses = []
|
||||
where_ns = {}
|
||||
|
||||
# Optional filtering
|
||||
is_active = params_kw.get('is_active', '')
|
||||
if is_active:
|
||||
where_clauses.append("is_active=${is_active}$")
|
||||
where_ns['is_active'] = is_active
|
||||
|
||||
biz_type = params_kw.get('biz_type', '')
|
||||
if biz_type:
|
||||
where_clauses.append("biz_type=${biz_type}$")
|
||||
where_ns['biz_type'] = biz_type
|
||||
|
||||
where_sql = " AND ".join(where_clauses)
|
||||
where_prefix = " WHERE " if where_clauses else ""
|
||||
|
||||
# Count query
|
||||
count_sql = "SELECT count(*) rcnt FROM dd_approval_configs" + where_prefix + where_sql
|
||||
count_rows = await sor.sqlExe(count_sql, where_ns)
|
||||
total = 0
|
||||
if count_rows and len(count_rows) > 0:
|
||||
r = count_rows[0]
|
||||
total = getattr(r, 'rcnt', 0)
|
||||
|
||||
if total > 0:
|
||||
ns = {
|
||||
'page': int(params_kw.get('page', 1)),
|
||||
'rows': int(params_kw.get('rows', 20)),
|
||||
'sort': params_kw.get('sort', 'biz_type')
|
||||
}
|
||||
sql = "SELECT id, org_id, biz_type, biz_type_title, process_code, agent_id, form_config, is_active, created_at, updated_at FROM dd_approval_configs" + where_prefix + where_sql
|
||||
query_ns = dict(list(ns.items()) + list(where_ns.items()))
|
||||
rows = await sor.sqlExe(sql, query_ns)
|
||||
|
||||
if isinstance(rows, dict):
|
||||
result['rows'] = rows.get('rows', [])
|
||||
result['total'] = rows.get('total', total)
|
||||
elif rows:
|
||||
result['rows'] = [dict(r) if hasattr(r, 'keys') else r for r in rows]
|
||||
result['total'] = total
|
||||
else:
|
||||
result['total'] = 0
|
||||
|
||||
result['success'] = True
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
36
dingdingflow/wwwroot/api/dd_approval_configs_update.dspy
Normal file
36
dingdingflow/wwwroot/api/dd_approval_configs_update.dspy
Normal file
@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""dd_approval_configs update API"""
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
record_id = params_kw.get('id', '')
|
||||
if not record_id:
|
||||
result['options'] = {'title': 'Error', 'message': 'ID is required', 'type': 'error'}
|
||||
else:
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
update_fields = []
|
||||
update_ns = {'id': record_id}
|
||||
|
||||
for field in ['biz_type', 'biz_type_title', 'process_code', 'agent_id', 'form_config', 'is_active']:
|
||||
val = params_kw.get(field)
|
||||
if val is not None:
|
||||
update_fields.append(f"{field}=${field}$")
|
||||
update_ns[field] = val
|
||||
|
||||
# Always update updated_at
|
||||
now_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
update_fields.append("updated_at=${updated_at}$")
|
||||
update_ns['updated_at'] = now_str
|
||||
|
||||
if update_fields:
|
||||
set_clause = ", ".join(update_fields)
|
||||
await sor.sqlExe(f"UPDATE dd_approval_configs SET {set_clause} WHERE id=${id}$", update_ns)
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批配置更新成功', 'type': 'success'}}
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'更新失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
43
dingdingflow/wwwroot/api/dd_approvals_create.dspy
Normal file
43
dingdingflow/wwwroot/api/dd_approvals_create.dspy
Normal file
@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""dd_approvals create API"""
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
biz_type = params_kw.get('biz_type', '')
|
||||
biz_id = params_kw.get('biz_id', '')
|
||||
title = params_kw.get('title', '')
|
||||
applicant_id = params_kw.get('applicant_id', '')
|
||||
|
||||
if not biz_type or not title:
|
||||
result['options'] = {'title': 'Error', 'message': 'biz_type and title are required', 'type': 'error'}
|
||||
else:
|
||||
new_id = uuid()
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
now_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.sqlExe(
|
||||
"INSERT INTO dd_approvals (id, org_id, biz_type, biz_id, title, applicant_id, approver_id, dingtalk_instance_id, status, comment, created_at) VALUES (${id}$, ${org_id}$, ${biz_type}$, ${biz_id}$, ${title}$, ${applicant_id}$, ${approver_id}$, ${dingtalk_instance_id}$, ${status}$, ${comment}$, ${created_at}$)",
|
||||
{
|
||||
'id': new_id,
|
||||
'org_id': org_id,
|
||||
'biz_type': biz_type,
|
||||
'biz_id': biz_id,
|
||||
'title': title,
|
||||
'applicant_id': applicant_id,
|
||||
'approver_id': params_kw.get('approver_id', ''),
|
||||
'dingtalk_instance_id': params_kw.get('dingtalk_instance_id', ''),
|
||||
'status': params_kw.get('status', 'pending'),
|
||||
'comment': params_kw.get('comment', ''),
|
||||
'created_at': now_str
|
||||
}
|
||||
)
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批记录创建成功', 'type': 'success'}}
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'创建失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
20
dingdingflow/wwwroot/api/dd_approvals_delete.dspy
Normal file
20
dingdingflow/wwwroot/api/dd_approvals_delete.dspy
Normal file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""dd_approvals delete API"""
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
record_id = params_kw.get('id', '')
|
||||
if not record_id:
|
||||
result['options'] = {'title': 'Error', 'message': 'ID is required', 'type': 'error'}
|
||||
else:
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
await sor.sqlExe("DELETE FROM dd_approvals WHERE id=${id}$", {'id': record_id})
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批记录删除成功', 'type': 'success'}}
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'删除失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
76
dingdingflow/wwwroot/api/dd_approvals_list.dspy
Normal file
76
dingdingflow/wwwroot/api/dd_approvals_list.dspy
Normal file
@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""dd_approvals list API with data_filter support"""
|
||||
|
||||
result = {'success': False, 'rows': [], 'total': 0}
|
||||
|
||||
try:
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
# Build WHERE clause from data_filter or direct params
|
||||
where_clauses = []
|
||||
where_ns = {}
|
||||
|
||||
# Support data_filter from CRUD search popup
|
||||
data_filter_str = params_kw.get('data_filter', '')
|
||||
if data_filter_str:
|
||||
# Individual filter values passed alongside data_filter
|
||||
status_filter = params_kw.get('status_filter', '')
|
||||
biz_type_filter = params_kw.get('biz_type_filter', '')
|
||||
title_filter = params_kw.get('title_filter', '')
|
||||
|
||||
if status_filter:
|
||||
where_clauses.append("status=${status_filter}$")
|
||||
where_ns['status_filter'] = status_filter
|
||||
if biz_type_filter:
|
||||
where_clauses.append("biz_type=${biz_type_filter}$")
|
||||
where_ns['biz_type_filter'] = biz_type_filter
|
||||
if title_filter:
|
||||
where_clauses.append("title LIKE ${title_filter}$")
|
||||
where_ns['title_filter'] = f'%{title_filter}%'
|
||||
else:
|
||||
# Direct param filtering
|
||||
status = params_kw.get('status', '')
|
||||
if status:
|
||||
where_clauses.append("status=${status}$")
|
||||
where_ns['status'] = status
|
||||
biz_type = params_kw.get('biz_type', '')
|
||||
if biz_type:
|
||||
where_clauses.append("biz_type=${biz_type}$")
|
||||
where_ns['biz_type'] = biz_type
|
||||
|
||||
where_sql = " AND ".join(where_clauses)
|
||||
where_prefix = " WHERE " if where_clauses else ""
|
||||
|
||||
# Count query
|
||||
count_sql = "SELECT count(*) rcnt FROM dd_approvals" + where_prefix + where_sql
|
||||
count_rows = await sor.sqlExe(count_sql, where_ns)
|
||||
total = 0
|
||||
if count_rows and len(count_rows) > 0:
|
||||
r = count_rows[0]
|
||||
total = getattr(r, 'rcnt', 0)
|
||||
|
||||
if total > 0:
|
||||
ns = {
|
||||
'page': int(params_kw.get('page', 1)),
|
||||
'rows': int(params_kw.get('rows', 20)),
|
||||
'sort': params_kw.get('sort', 'created_at desc')
|
||||
}
|
||||
sql = "SELECT id, org_id, biz_type, biz_id, title, applicant_id, approver_id, dingtalk_instance_id, status, comment, created_at, completed_at FROM dd_approvals" + where_prefix + where_sql
|
||||
query_ns = dict(list(ns.items()) + list(where_ns.items()))
|
||||
rows = await sor.sqlExe(sql, query_ns)
|
||||
|
||||
if isinstance(rows, dict):
|
||||
result['rows'] = rows.get('rows', [])
|
||||
result['total'] = rows.get('total', total)
|
||||
elif rows:
|
||||
result['rows'] = [dict(r) if hasattr(r, 'keys') else r for r in rows]
|
||||
result['total'] = total
|
||||
else:
|
||||
result['total'] = 0
|
||||
|
||||
result['success'] = True
|
||||
except Exception as e:
|
||||
result['error'] = str(e)
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
36
dingdingflow/wwwroot/api/dd_approvals_update.dspy
Normal file
36
dingdingflow/wwwroot/api/dd_approvals_update.dspy
Normal file
@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""dd_approvals update API"""
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Error', 'message': 'Invalid request', 'type': 'error'}}
|
||||
|
||||
try:
|
||||
record_id = params_kw.get('id', '')
|
||||
if not record_id:
|
||||
result['options'] = {'title': 'Error', 'message': 'ID is required', 'type': 'error'}
|
||||
else:
|
||||
dbname = get_module_dbname('dingdingflow')
|
||||
async with DBPools().sqlorContext(dbname) as sor:
|
||||
update_fields = []
|
||||
update_ns = {'id': record_id}
|
||||
|
||||
for field in ['biz_type', 'biz_id', 'title', 'applicant_id', 'approver_id', 'dingtalk_instance_id', 'status', 'comment']:
|
||||
val = params_kw.get(field)
|
||||
if val is not None:
|
||||
update_fields.append(f"{field}=${field}$")
|
||||
update_ns[field] = val
|
||||
|
||||
completed_at = params_kw.get('completed_at')
|
||||
if completed_at:
|
||||
update_fields.append("completed_at=${completed_at}$")
|
||||
update_ns['completed_at'] = completed_at
|
||||
|
||||
if update_fields:
|
||||
set_clause = ", ".join(update_fields)
|
||||
await sor.sqlExe(f"UPDATE dd_approvals SET {set_clause} WHERE id=${id}$", update_ns)
|
||||
|
||||
result = {'widgettype': 'Message', 'options': {'title': 'Success', 'message': '审批记录更新成功', 'type': 'success'}}
|
||||
except Exception as e:
|
||||
result['options'] = {'title': 'Error', 'message': f'更新失败: {str(e)}', 'type': 'error'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False)
|
||||
62
dingdingflow/wwwroot/api/dingtalk_callback.dspy
Normal file
62
dingdingflow/wwwroot/api/dingtalk_callback.dspy
Normal file
@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
DingTalk webhook callback endpoint.
|
||||
PUBLIC endpoint (any permission) - DingTalk servers call this when approval status changes.
|
||||
|
||||
Expected POST body:
|
||||
{
|
||||
"processInstanceId": "xxx",
|
||||
"processCode": "xxx",
|
||||
"type": "bpms_instance_change",
|
||||
"result": "agree" / "refuse",
|
||||
"staffId": "xxx",
|
||||
"remark": "optional comment"
|
||||
}
|
||||
"""
|
||||
|
||||
result = {'success': False, 'message': 'Invalid callback'}
|
||||
|
||||
try:
|
||||
# DingTalk callback data comes via params_kw (POST body auto-parsed)
|
||||
callback_data = {}
|
||||
|
||||
# params_kw contains the parsed POST body fields
|
||||
process_instance_id = params_kw.get('processInstanceId', '')
|
||||
callback_type = params_kw.get('type', '')
|
||||
callback_result = params_kw.get('result', '')
|
||||
staff_id = params_kw.get('staffId', '')
|
||||
process_code = params_kw.get('processCode', '')
|
||||
remark = params_kw.get('remark', '')
|
||||
|
||||
# Also handle nested JSON body case where entire body is under a key
|
||||
if not process_instance_id:
|
||||
body = params_kw.get('body', None)
|
||||
if isinstance(body, dict):
|
||||
process_instance_id = body.get('processInstanceId', '')
|
||||
callback_type = body.get('type', '')
|
||||
callback_result = body.get('result', '')
|
||||
staff_id = body.get('staffId', '')
|
||||
process_code = body.get('processCode', '')
|
||||
remark = body.get('remark', '')
|
||||
|
||||
callback_data = {
|
||||
'processInstanceId': process_instance_id,
|
||||
'type': callback_type,
|
||||
'result': callback_result,
|
||||
'staffId': staff_id,
|
||||
'processCode': process_code,
|
||||
'remark': remark,
|
||||
}
|
||||
|
||||
if not process_instance_id:
|
||||
result = {'success': False, 'message': 'Missing processInstanceId'}
|
||||
else:
|
||||
# Call the handle_dingtalk_callback function registered via load_dingdingflow()
|
||||
callback_result_data = await handle_dingtalk_callback(callback_data)
|
||||
result = callback_result_data
|
||||
|
||||
except Exception as e:
|
||||
result = {'success': False, 'message': f'Callback processing error: {str(e)}'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
36
dingdingflow/wwwroot/api/submit_approval.dspy
Normal file
36
dingdingflow/wwwroot/api/submit_approval.dspy
Normal file
@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Submit approval API - triggered by entcms when content needs approval.
|
||||
Creates a dd_approvals record and calls DingTalk API to start the approval process.
|
||||
"""
|
||||
|
||||
result = {'success': False, 'message': 'Invalid request'}
|
||||
|
||||
try:
|
||||
biz_type = params_kw.get('biz_type', '')
|
||||
biz_id = params_kw.get('biz_id', '')
|
||||
title = params_kw.get('title', '')
|
||||
applicant_id = params_kw.get('applicant_id', '')
|
||||
|
||||
if not biz_type:
|
||||
result = {'success': False, 'message': 'biz_type is required'}
|
||||
elif not biz_id:
|
||||
result = {'success': False, 'message': 'biz_id is required'}
|
||||
elif not title:
|
||||
result = {'success': False, 'message': 'title is required'}
|
||||
else:
|
||||
# Use current user as applicant if not specified
|
||||
if not applicant_id:
|
||||
applicant_id = get_user() or ''
|
||||
|
||||
org_id = (await get_userorgid()) or '0'
|
||||
|
||||
# Call the submit_approval function registered via load_dingdingflow()
|
||||
approval_result = await submit_approval(biz_type, biz_id, title, applicant_id, org_id)
|
||||
result = approval_result
|
||||
|
||||
except Exception as e:
|
||||
result = {'success': False, 'message': f'提交审批失败: {str(e)}'}
|
||||
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
77
dingdingflow/wwwroot/index.ui
Normal file
77
dingdingflow/wwwroot/index.ui
Normal file
@ -0,0 +1,77 @@
|
||||
{
|
||||
"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"}
|
||||
}
|
||||
]
|
||||
}
|
||||
25
dingdingflow/wwwroot/menu.ui
Normal file
25
dingdingflow/wwwroot/menu.ui
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"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')}}"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
156
docs/architecture.md
Normal file
156
docs/architecture.md
Normal file
@ -0,0 +1,156 @@
|
||||
# 开元云科技官网系统架构
|
||||
|
||||
## 项目概述
|
||||
企业官网 + CMS内容管理 + 钉钉审批流程系统,基于Sage平台开发。
|
||||
|
||||
## 模块组成
|
||||
|
||||
### 1. entcms - 企业CMS系统
|
||||
管理官网所有内容:新闻、案例、产品、Banner、商机线索。
|
||||
|
||||
**数据库表 (entcms)**:
|
||||
| 表名 | 用途 |
|
||||
|------|------|
|
||||
| cms_content | 统一内容表(新闻/案例/产品/Banner),带发布审批状态流 |
|
||||
| cms_categories | 内容分类(支持层级,按content_type分组) |
|
||||
| cms_leads | 商机线索(网站访客提交 + 未来AI抽取) |
|
||||
| cms_site_config | 站点配置(Hero标语、页脚信息等KV配置) |
|
||||
|
||||
**公开页面 (any权限)**:
|
||||
- `/entcms/index.ui` - 官网首页(7个模块:导航/Hero/产品/案例/新闻/页脚/浮动入口)
|
||||
- `/entcms/news.ui` - 新闻列表
|
||||
- `/entcms/news_detail.ui` - 新闻详情
|
||||
- `/entcms/cases.ui` - 案例列表
|
||||
|
||||
**管理页面 (logined权限)**:
|
||||
- `/entcms/admin.ui` - 管理后台仪表盘
|
||||
- `/entcms/cms_content_list` - 内容CRUD
|
||||
- `/entcms/cms_categories_list` - 分类CRUD
|
||||
- `/entcms/cms_leads_list` - 线索CRUD
|
||||
- `/entcms/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
|
||||
```
|
||||
93
docs/test-cases.md
Normal file
93
docs/test-cases.md
Normal file
@ -0,0 +1,93 @@
|
||||
# 测试用例
|
||||
|
||||
## 一、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
|
||||
- 通过率: 待部署后统计
|
||||
46
docs/work-log-2026-05-27.md
Normal file
46
docs/work-log-2026-05-27.md
Normal file
@ -0,0 +1,46 @@
|
||||
# 开发日志
|
||||
|
||||
## 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 (首次提交)
|
||||
22
entcms/README.md
Normal file
22
entcms/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
# 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 - 配置管理
|
||||
0
entcms/entcms/__init__.py
Normal file
0
entcms/entcms/__init__.py
Normal file
260
entcms/entcms/init.py
Normal file
260
entcms/entcms/init.py
Normal file
@ -0,0 +1,260 @@
|
||||
"""
|
||||
entcms - 企业CMS系统模块
|
||||
企业官网内容管理:新闻、案例、产品、Banner、商机线索
|
||||
"""
|
||||
import json
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
MODULE_NAME = "entcms"
|
||||
MODULE_VERSION = "1.0.0"
|
||||
|
||||
DBNAME = "entcms"
|
||||
|
||||
|
||||
def _get_db():
|
||||
"""获取数据库上下文"""
|
||||
from sqlor import DBPools
|
||||
return DBPools().sqlorContext(DBNAME)
|
||||
|
||||
|
||||
# ===== CMS Content CRUD =====
|
||||
async def cms_content_list(ns=None):
|
||||
"""查询内容列表"""
|
||||
sor = _get_db()
|
||||
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):
|
||||
"""创建内容"""
|
||||
sor = _get_db()
|
||||
data['id'] = getID()
|
||||
await sor.C('cms_content', data)
|
||||
return data
|
||||
|
||||
|
||||
async def cms_content_update(data):
|
||||
"""更新内容"""
|
||||
sor = _get_db()
|
||||
await sor.U('cms_content', data)
|
||||
return data
|
||||
|
||||
|
||||
async def cms_content_delete(data):
|
||||
"""删除内容"""
|
||||
sor = _get_db()
|
||||
await sor.D('cms_content', data)
|
||||
return data
|
||||
|
||||
|
||||
# ===== CMS Categories CRUD =====
|
||||
async def cms_categories_list(ns=None):
|
||||
"""查询分类列表"""
|
||||
sor = _get_db()
|
||||
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):
|
||||
sor = _get_db()
|
||||
data['id'] = getID()
|
||||
await sor.C('cms_categories', data)
|
||||
return data
|
||||
|
||||
|
||||
async def cms_categories_update(data):
|
||||
sor = _get_db()
|
||||
await sor.U('cms_categories', data)
|
||||
return data
|
||||
|
||||
|
||||
async def cms_categories_delete(data):
|
||||
sor = _get_db()
|
||||
await sor.D('cms_categories', data)
|
||||
return data
|
||||
|
||||
|
||||
async def get_category_options(content_type=None):
|
||||
"""获取分类下拉选项"""
|
||||
sor = _get_db()
|
||||
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):
|
||||
sor = _get_db()
|
||||
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):
|
||||
sor = _get_db()
|
||||
data['id'] = getID()
|
||||
await sor.C('cms_leads', data)
|
||||
return data
|
||||
|
||||
|
||||
async def cms_leads_update(data):
|
||||
sor = _get_db()
|
||||
await sor.U('cms_leads', data)
|
||||
return data
|
||||
|
||||
|
||||
async def cms_leads_delete(data):
|
||||
sor = _get_db()
|
||||
await sor.D('cms_leads', data)
|
||||
return data
|
||||
|
||||
|
||||
async def submit_lead(data):
|
||||
"""公开接口 - 网站访客提交线索"""
|
||||
sor = _get_db()
|
||||
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):
|
||||
sor = _get_db()
|
||||
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):
|
||||
sor = _get_db()
|
||||
data['id'] = getID()
|
||||
await sor.C('cms_site_config', data)
|
||||
return data
|
||||
|
||||
|
||||
async def cms_site_config_update(data):
|
||||
sor = _get_db()
|
||||
await sor.U('cms_site_config', data)
|
||||
return data
|
||||
|
||||
|
||||
async def cms_site_config_delete(data):
|
||||
sor = _get_db()
|
||||
await sor.D('cms_site_config', data)
|
||||
return data
|
||||
|
||||
|
||||
async def get_site_config(group=None):
|
||||
"""获取站点配置(公开接口)"""
|
||||
sor = _get_db()
|
||||
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):
|
||||
"""获取已发布内容(公开接口)"""
|
||||
sor = _get_db()
|
||||
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):
|
||||
"""获取最新新闻(公开接口)"""
|
||||
sor = _get_db()
|
||||
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):
|
||||
"""获取内容详情(公开接口)"""
|
||||
sor = _get_db()
|
||||
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)"""
|
||||
# 更新内容状态为pending
|
||||
sor = _get_db()
|
||||
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
|
||||
|
||||
# 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
|
||||
116
entcms/init/data.json
Normal file
116
entcms/init/data.json
Normal file
@ -0,0 +1,116 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
37
entcms/json/cms_categories_list.json
Normal file
37
entcms/json/cms_categories_list.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"tblname": "cms_categories",
|
||||
"alias": "cms_categories_list",
|
||||
"title": "内容分类",
|
||||
"params": {
|
||||
"sortby": [
|
||||
"sort_order asc"
|
||||
],
|
||||
"logined_userorgid": "org_id",
|
||||
"browserfields": {
|
||||
"alters": {
|
||||
"content_type": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{
|
||||
"value": "product",
|
||||
"text": "产品"
|
||||
},
|
||||
{
|
||||
"value": "case",
|
||||
"text": "案例"
|
||||
},
|
||||
{
|
||||
"value": "news",
|
||||
"text": "新闻"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/cms_categories_create.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/cms_categories_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/cms_categories_delete.dspy')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
97
entcms/json/cms_content_list.json
Normal file
97
entcms/json/cms_content_list.json
Normal file
@ -0,0 +1,97 @@
|
||||
{
|
||||
"tblname": "cms_content",
|
||||
"alias": "cms_content_list",
|
||||
"title": "内容管理",
|
||||
"params": {
|
||||
"sortby": [
|
||||
"sort_order asc",
|
||||
"created_at desc"
|
||||
],
|
||||
"logined_userorgid": "org_id",
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{
|
||||
"field": "title",
|
||||
"op": "LIKE",
|
||||
"var": "title"
|
||||
},
|
||||
{
|
||||
"field": "content_type",
|
||||
"op": "=",
|
||||
"var": "content_type"
|
||||
},
|
||||
{
|
||||
"field": "status",
|
||||
"op": "=",
|
||||
"var": "status"
|
||||
}
|
||||
]
|
||||
},
|
||||
"browserfields": {
|
||||
"exclouded": [
|
||||
"body",
|
||||
"extra_json"
|
||||
],
|
||||
"alters": {
|
||||
"content_type": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{
|
||||
"value": "banner",
|
||||
"text": "Banner"
|
||||
},
|
||||
{
|
||||
"value": "product",
|
||||
"text": "产品"
|
||||
},
|
||||
{
|
||||
"value": "case",
|
||||
"text": "案例"
|
||||
},
|
||||
{
|
||||
"value": "news",
|
||||
"text": "新闻"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{
|
||||
"value": "draft",
|
||||
"text": "草稿"
|
||||
},
|
||||
{
|
||||
"value": "pending",
|
||||
"text": "待审批"
|
||||
},
|
||||
{
|
||||
"value": "approved",
|
||||
"text": "已审批"
|
||||
},
|
||||
{
|
||||
"value": "published",
|
||||
"text": "已发布"
|
||||
},
|
||||
{
|
||||
"value": "archived",
|
||||
"text": "已归档"
|
||||
}
|
||||
]
|
||||
},
|
||||
"category_id": {
|
||||
"uitype": "code",
|
||||
"dataurl": "{{entire_url('../api/category_options.dspy')}}",
|
||||
"data_field": "options",
|
||||
"textField": "text",
|
||||
"valueField": "value"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/cms_content_create.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/cms_content_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/cms_content_delete.dspy')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
93
entcms/json/cms_leads_list.json
Normal file
93
entcms/json/cms_leads_list.json
Normal file
@ -0,0 +1,93 @@
|
||||
{
|
||||
"tblname": "cms_leads",
|
||||
"alias": "cms_leads_list",
|
||||
"title": "商机线索",
|
||||
"params": {
|
||||
"sortby": [
|
||||
"created_at desc"
|
||||
],
|
||||
"logined_userorgid": "org_id",
|
||||
"data_filter": {
|
||||
"AND": [
|
||||
{
|
||||
"field": "name",
|
||||
"op": "LIKE",
|
||||
"var": "name"
|
||||
},
|
||||
{
|
||||
"field": "company",
|
||||
"op": "LIKE",
|
||||
"var": "company"
|
||||
},
|
||||
{
|
||||
"field": "status",
|
||||
"op": "=",
|
||||
"var": "status"
|
||||
},
|
||||
{
|
||||
"field": "source",
|
||||
"op": "=",
|
||||
"var": "source"
|
||||
}
|
||||
]
|
||||
},
|
||||
"browserfields": {
|
||||
"exclouded": [
|
||||
"raw_text"
|
||||
],
|
||||
"alters": {
|
||||
"status": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{
|
||||
"value": "new",
|
||||
"text": "新线索"
|
||||
},
|
||||
{
|
||||
"value": "contacted",
|
||||
"text": "已联系"
|
||||
},
|
||||
{
|
||||
"value": "qualified",
|
||||
"text": "已确认"
|
||||
},
|
||||
{
|
||||
"value": "converted",
|
||||
"text": "已转化"
|
||||
},
|
||||
{
|
||||
"value": "closed",
|
||||
"text": "已关闭"
|
||||
}
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"uitype": "code",
|
||||
"data": [
|
||||
{
|
||||
"value": "website",
|
||||
"text": "官网"
|
||||
},
|
||||
{
|
||||
"value": "phone",
|
||||
"text": "电话"
|
||||
},
|
||||
{
|
||||
"value": "referral",
|
||||
"text": "转介绍"
|
||||
},
|
||||
{
|
||||
"value": "ai_extract",
|
||||
"text": "AI抽取"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"editable": {
|
||||
"new_data_url": "{{entire_url('../api/cms_leads_create.dspy')}}",
|
||||
"update_data_url": "{{entire_url('../api/cms_leads_update.dspy')}}",
|
||||
"delete_data_url": "{{entire_url('../api/cms_leads_delete.dspy')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
63
entcms/json/cms_site_config_list.json
Normal file
63
entcms/json/cms_site_config_list.json
Normal file
@ -0,0 +1,63 @@
|
||||
{
|
||||
"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')}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
80
entcms/models/cms_categories.json
Normal file
80
entcms/models/cms_categories.json
Normal file
@ -0,0 +1,80 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "cms_categories",
|
||||
"title": "CMS内容分类表",
|
||||
"primary": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "组织ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"title": "分类名称",
|
||||
"type": "str",
|
||||
"length": 100,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "parent_id",
|
||||
"title": "父分类ID",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "content_type",
|
||||
"title": "所属内容类型",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"title": "分类描述",
|
||||
"type": "str",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"name": "sort_order",
|
||||
"title": "排序号",
|
||||
"type": "int",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_cat_org_type",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"org_id",
|
||||
"content_type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_cat_parent",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"parent_id"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
154
entcms/models/cms_content.json
Normal file
154
entcms/models/cms_content.json
Normal file
@ -0,0 +1,154 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "cms_content",
|
||||
"title": "CMS内容表",
|
||||
"primary": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "组织ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "content_type",
|
||||
"title": "内容类型(banner/product/case/news)",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "category_id",
|
||||
"title": "分类ID",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "title",
|
||||
"title": "标题",
|
||||
"type": "str",
|
||||
"length": 255,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "subtitle",
|
||||
"title": "副标题",
|
||||
"type": "str",
|
||||
"length": 255
|
||||
},
|
||||
{
|
||||
"name": "summary_text",
|
||||
"title": "摘要",
|
||||
"type": "str",
|
||||
"length": 500
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"title": "正文内容",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "image_url",
|
||||
"title": "封面图片URL",
|
||||
"type": "str",
|
||||
"length": 500
|
||||
},
|
||||
{
|
||||
"name": "tags",
|
||||
"title": "标签(逗号分隔)",
|
||||
"type": "str",
|
||||
"length": 500
|
||||
},
|
||||
{
|
||||
"name": "sort_order",
|
||||
"title": "排序号",
|
||||
"type": "int",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"title": "状态(draft/pending/approved/published/archived)",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "draft"
|
||||
},
|
||||
{
|
||||
"name": "approval_id",
|
||||
"title": "审批单ID",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "published_at",
|
||||
"title": "发布时间",
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"name": "extra_json",
|
||||
"title": "扩展JSON",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "created_by",
|
||||
"title": "创建人",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "更新时间",
|
||||
"type": "timestamp"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_content_type_status",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"content_type",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_content_org",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"org_id",
|
||||
"content_type"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_content_sort",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"sort_order"
|
||||
]
|
||||
}
|
||||
],
|
||||
"codes": [
|
||||
{
|
||||
"field": "category_id",
|
||||
"table": "cms_categories",
|
||||
"valuefield": "id",
|
||||
"textfield": "name"
|
||||
}
|
||||
]
|
||||
}
|
||||
137
entcms/models/cms_leads.json
Normal file
137
entcms/models/cms_leads.json
Normal file
@ -0,0 +1,137 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "cms_leads",
|
||||
"title": "商机线索表",
|
||||
"primary": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "组织ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "source",
|
||||
"title": "来源(website/phone/referral/ai_extract)",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"title": "联系人姓名",
|
||||
"type": "str",
|
||||
"length": 100
|
||||
},
|
||||
{
|
||||
"name": "company",
|
||||
"title": "公司名称",
|
||||
"type": "str",
|
||||
"length": 200
|
||||
},
|
||||
{
|
||||
"name": "phone",
|
||||
"title": "联系电话",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "email",
|
||||
"title": "邮箱",
|
||||
"type": "str",
|
||||
"length": 100
|
||||
},
|
||||
{
|
||||
"name": "industry",
|
||||
"title": "所属行业",
|
||||
"type": "str",
|
||||
"length": 100
|
||||
},
|
||||
{
|
||||
"name": "region",
|
||||
"title": "地区",
|
||||
"type": "str",
|
||||
"length": 100
|
||||
},
|
||||
{
|
||||
"name": "interest_products",
|
||||
"title": "感兴趣的产品",
|
||||
"type": "str",
|
||||
"length": 500
|
||||
},
|
||||
{
|
||||
"name": "message",
|
||||
"title": "留言内容",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "raw_text",
|
||||
"title": "原始文本(AI抽取用)",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "status",
|
||||
"title": "状态(new/contacted/qualified/converted/closed)",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "new"
|
||||
},
|
||||
{
|
||||
"name": "assigned_to",
|
||||
"title": "负责人",
|
||||
"type": "str",
|
||||
"length": 32
|
||||
},
|
||||
{
|
||||
"name": "notes",
|
||||
"title": "跟进备注",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"title": "创建时间",
|
||||
"type": "timestamp"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "更新时间",
|
||||
"type": "timestamp"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_leads_org_status",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"org_id",
|
||||
"status"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_leads_source",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"source"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "idx_leads_assigned",
|
||||
"idxtype": "index",
|
||||
"idxfields": [
|
||||
"assigned_to"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
75
entcms/models/cms_site_config.json
Normal file
75
entcms/models/cms_site_config.json
Normal file
@ -0,0 +1,75 @@
|
||||
{
|
||||
"summary": [
|
||||
{
|
||||
"name": "cms_site_config",
|
||||
"title": "站点配置表",
|
||||
"primary": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"name": "id",
|
||||
"title": "主键ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "org_id",
|
||||
"title": "组织ID",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "config_group",
|
||||
"title": "配置组(hero/footer/contact/seo)",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "config_key",
|
||||
"title": "配置键",
|
||||
"type": "str",
|
||||
"length": 100,
|
||||
"nullable": "no"
|
||||
},
|
||||
{
|
||||
"name": "config_value",
|
||||
"title": "配置值",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"name": "config_type",
|
||||
"title": "值类型(text/image/html/json)",
|
||||
"type": "str",
|
||||
"length": 32,
|
||||
"default": "text"
|
||||
},
|
||||
{
|
||||
"name": "sort_order",
|
||||
"title": "排序号",
|
||||
"type": "int",
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"title": "更新时间",
|
||||
"type": "timestamp"
|
||||
}
|
||||
],
|
||||
"indexes": [
|
||||
{
|
||||
"name": "idx_config_org_group",
|
||||
"idxtype": "unique",
|
||||
"idxfields": [
|
||||
"org_id",
|
||||
"config_group",
|
||||
"config_key"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
17
entcms/pyproject.toml
Normal file
17
entcms/pyproject.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[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*"]
|
||||
89
entcms/scripts/load_path.py
Normal file
89
entcms/scripts/load_path.py
Normal file
@ -0,0 +1,89 @@
|
||||
"""
|
||||
entcms RBAC权限配置
|
||||
用法: cd ~/repos/sage && ./py3/bin/python ~/repos/cms/entcms/scripts/load_path.py
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
|
||||
# 查找Sage根目录
|
||||
def find_sage_root():
|
||||
for candidate in [
|
||||
os.path.expanduser("~/repos/sage"),
|
||||
os.path.expanduser("~/sage"),
|
||||
]:
|
||||
if os.path.isdir(os.path.join(candidate, "wwwroot")) and \
|
||||
os.path.isdir(os.path.join(candidate, "py3")):
|
||||
return candidate
|
||||
return None
|
||||
|
||||
sage_root = find_sage_root()
|
||||
if not sage_root:
|
||||
print("ERROR: Cannot find Sage root directory")
|
||||
sys.exit(1)
|
||||
|
||||
python = os.path.join(sage_root, "py3", "bin", "python")
|
||||
set_perm = os.path.join(sage_root, "set_role_perm.py")
|
||||
|
||||
# 权限配置
|
||||
paths_any = [
|
||||
# 公开页面和静态资源
|
||||
"/entcms/index.ui",
|
||||
"/entcms/news.ui",
|
||||
"/entcms/news_detail.ui",
|
||||
"/entcms/cases.ui",
|
||||
"/entcms/products.ui",
|
||||
"/entcms/cms_styles.css",
|
||||
"/entcms/cms_scripts.js",
|
||||
"/entcms/menu.ui",
|
||||
# 公开API
|
||||
"/entcms/api/submit_lead.dspy",
|
||||
"/entcms/api/get_config.dspy",
|
||||
"/entcms/api/get_published_content.dspy",
|
||||
"/entcms/api/get_content_detail.dspy",
|
||||
]
|
||||
|
||||
paths_logined = [
|
||||
# 管理后台
|
||||
"/entcms",
|
||||
"/entcms/admin.ui",
|
||||
# CRUD页面
|
||||
"/entcms/cms_content_list",
|
||||
"/entcms/cms_content_list/%",
|
||||
"/entcms/cms_categories_list",
|
||||
"/entcms/cms_categories_list/%",
|
||||
"/entcms/cms_leads_list",
|
||||
"/entcms/cms_leads_list/%",
|
||||
"/entcms/cms_site_config_list",
|
||||
"/entcms/cms_site_config_list/%",
|
||||
# 管理API
|
||||
"/entcms/api/cms_content_create.dspy",
|
||||
"/entcms/api/cms_content_update.dspy",
|
||||
"/entcms/api/cms_content_delete.dspy",
|
||||
"/entcms/api/cms_content_list.dspy",
|
||||
"/entcms/api/cms_categories_create.dspy",
|
||||
"/entcms/api/cms_categories_update.dspy",
|
||||
"/entcms/api/cms_categories_delete.dspy",
|
||||
"/entcms/api/cms_categories_list.dspy",
|
||||
"/entcms/api/category_options.dspy",
|
||||
"/entcms/api/cms_leads_create.dspy",
|
||||
"/entcms/api/cms_leads_update.dspy",
|
||||
"/entcms/api/cms_leads_delete.dspy",
|
||||
"/entcms/api/cms_leads_list.dspy",
|
||||
"/entcms/api/cms_site_config_create.dspy",
|
||||
"/entcms/api/cms_site_config_update.dspy",
|
||||
"/entcms/api/cms_site_config_delete.dspy",
|
||||
"/entcms/api/cms_site_config_list.dspy",
|
||||
"/entcms/api/submit_content_approval.dspy",
|
||||
]
|
||||
|
||||
def set_perms(role, paths):
|
||||
for path in paths:
|
||||
cmd = [python, set_perm, role, path]
|
||||
print(f" {role:20s} {path}")
|
||||
subprocess.run(cmd, cwd=sage_root, capture_output=True)
|
||||
|
||||
print("=== entcms RBAC权限配置 ===")
|
||||
set_perms("any", paths_any)
|
||||
set_perms("logined", paths_logined)
|
||||
print("完成")
|
||||
267
entcms/wwwroot/admin.ui
Normal file
267
entcms/wwwroot/admin.ui
Normal file
@ -0,0 +1,267 @@
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"height": "100%",
|
||||
"padding": "20px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "企业CMS管理后台",
|
||||
"fontSize": "24px",
|
||||
"fontWeight": "bold",
|
||||
"css": "title"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "管理官网内容、分类、商机线索和站点配置",
|
||||
"fontSize": "14px",
|
||||
"color": "#999",
|
||||
"css": "subtitle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "ResponsableBox",
|
||||
"options": {
|
||||
"gap": "16px",
|
||||
"minWidth": "220px",
|
||||
"css": "admin-cards"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Button",
|
||||
"options": {
|
||||
"css": "card",
|
||||
"padding": "20px",
|
||||
"borderRadius": "12px",
|
||||
"border": "none"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.sage_main_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('/entcms/cms_content_list')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"alignItems": "flex-start",
|
||||
"gap": "8px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "📝",
|
||||
"fontSize": "32px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "内容管理",
|
||||
"fontSize": "18px",
|
||||
"fontWeight": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "新闻、案例、产品、Banner",
|
||||
"fontSize": "13px",
|
||||
"color": "#999"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "Button",
|
||||
"options": {
|
||||
"css": "card",
|
||||
"padding": "20px",
|
||||
"borderRadius": "12px",
|
||||
"border": "none"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.sage_main_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('/entcms/cms_categories_list')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"alignItems": "flex-start",
|
||||
"gap": "8px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "📂",
|
||||
"fontSize": "32px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "内容分类",
|
||||
"fontSize": "18px",
|
||||
"fontWeight": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "管理产品分类、案例行业、新闻栏目",
|
||||
"fontSize": "13px",
|
||||
"color": "#999"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "Button",
|
||||
"options": {
|
||||
"css": "card",
|
||||
"padding": "20px",
|
||||
"borderRadius": "12px",
|
||||
"border": "none"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.sage_main_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('/entcms/cms_leads_list')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"alignItems": "flex-start",
|
||||
"gap": "8px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "🎯",
|
||||
"fontSize": "32px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "商机线索",
|
||||
"fontSize": "18px",
|
||||
"fontWeight": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "访客留言、AI抽取商机",
|
||||
"fontSize": "13px",
|
||||
"color": "#999"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "Button",
|
||||
"options": {
|
||||
"css": "card",
|
||||
"padding": "20px",
|
||||
"borderRadius": "12px",
|
||||
"border": "none"
|
||||
},
|
||||
"binds": [
|
||||
{
|
||||
"wid": "self",
|
||||
"event": "click",
|
||||
"actiontype": "urlwidget",
|
||||
"target": "app.sage_main_content",
|
||||
"options": {
|
||||
"url": "{{entire_url('/entcms/cms_site_config_list')}}"
|
||||
},
|
||||
"mode": "replace"
|
||||
}
|
||||
],
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {
|
||||
"alignItems": "flex-start",
|
||||
"gap": "8px"
|
||||
},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "⚙️",
|
||||
"fontSize": "32px"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "站点配置",
|
||||
"fontSize": "18px",
|
||||
"fontWeight": "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
"widgettype": "Text",
|
||||
"options": {
|
||||
"text": "首屏标语、页脚信息、联系方式",
|
||||
"fontSize": "13px",
|
||||
"color": "#999"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"id": "sage_main_content",
|
||||
"options": {
|
||||
"width": "100%",
|
||||
"flex": "1",
|
||||
"marginTop": "20px"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
15
entcms/wwwroot/api/category_options.dspy
Normal file
15
entcms/wwwroot/api/category_options.dspy
Normal file
@ -0,0 +1,15 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
content_type = params_kw.get('content_type', None)
|
||||
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]
|
||||
print(json.dumps({'status': 'ok', 'data': {'options': options}}, ensure_ascii=False))
|
||||
36
entcms/wwwroot/api/cms_categories_create.dspy
Normal file
36
entcms/wwwroot/api/cms_categories_create.dspy
Normal file
@ -0,0 +1,36 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
data = {'id': getID()}
|
||||
|
||||
v = params_kw.get('org_id', None)
|
||||
if v is not None:
|
||||
data['org_id'] = 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)
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}, ensure_ascii=False))
|
||||
14
entcms/wwwroot/api/cms_categories_delete.dspy
Normal file
14
entcms/wwwroot/api/cms_categories_delete.dspy
Normal file
@ -0,0 +1,14 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
_id = params_kw.get('id', '')
|
||||
if not _id:
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}, ensure_ascii=False))
|
||||
return
|
||||
await sor.D('cms_categories', {'id': _id})
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '删除成功', 'messagetype': 'success'}}, ensure_ascii=False))
|
||||
32
entcms/wwwroot/api/cms_categories_list.dspy
Normal file
32
entcms/wwwroot/api/cms_categories_list.dspy
Normal file
@ -0,0 +1,32 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
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)
|
||||
total = len(rows)
|
||||
print(json.dumps({'status': 'ok', 'rows': rows, 'total': total}, ensure_ascii=False))
|
||||
39
entcms/wwwroot/api/cms_categories_update.dspy
Normal file
39
entcms/wwwroot/api/cms_categories_update.dspy
Normal file
@ -0,0 +1,39 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
data = {'id': params_kw.get('id', '')}
|
||||
if not data['id']:
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}, ensure_ascii=False))
|
||||
return
|
||||
|
||||
v = params_kw.get('org_id', None)
|
||||
if v is not None:
|
||||
data['org_id'] = 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)
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}, ensure_ascii=False))
|
||||
60
entcms/wwwroot/api/cms_content_create.dspy
Normal file
60
entcms/wwwroot/api/cms_content_create.dspy
Normal file
@ -0,0 +1,60 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
data = {'id': getID()}
|
||||
|
||||
v = params_kw.get('org_id', None)
|
||||
if v is not None:
|
||||
data['org_id'] = v
|
||||
|
||||
v = params_kw.get('content_type', None)
|
||||
if v is not None:
|
||||
data['content_type'] = v
|
||||
|
||||
v = params_kw.get('category_id', None)
|
||||
if v is not None:
|
||||
data['category_id'] = 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('summary_text', None)
|
||||
if v is not None:
|
||||
data['summary_text'] = v
|
||||
|
||||
v = params_kw.get('body', None)
|
||||
if v is not None:
|
||||
data['body'] = v
|
||||
|
||||
v = params_kw.get('image_url', None)
|
||||
if v is not None:
|
||||
data['image_url'] = v
|
||||
|
||||
v = params_kw.get('tags', None)
|
||||
if v is not None:
|
||||
data['tags'] = v
|
||||
|
||||
v = params_kw.get('sort_order', None)
|
||||
if v is not None:
|
||||
data['sort_order'] = v
|
||||
|
||||
v = params_kw.get('status', None)
|
||||
if v is not None:
|
||||
data['status'] = v
|
||||
|
||||
v = params_kw.get('extra_json', None)
|
||||
if v is not None:
|
||||
data['extra_json'] = v
|
||||
|
||||
await sor.C('cms_content', data)
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}, ensure_ascii=False))
|
||||
14
entcms/wwwroot/api/cms_content_delete.dspy
Normal file
14
entcms/wwwroot/api/cms_content_delete.dspy
Normal file
@ -0,0 +1,14 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
_id = params_kw.get('id', '')
|
||||
if not _id:
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}, ensure_ascii=False))
|
||||
return
|
||||
await sor.D('cms_content', {'id': _id})
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '删除成功', 'messagetype': 'success'}}, ensure_ascii=False))
|
||||
36
entcms/wwwroot/api/cms_content_list.dspy
Normal file
36
entcms/wwwroot/api/cms_content_list.dspy
Normal file
@ -0,0 +1,36 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
ns = {'sort': 'sort_order asc, created_at desc'}
|
||||
|
||||
# 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
|
||||
|
||||
_status = params_kw.get('status', None)
|
||||
if _status:
|
||||
ns['status'] = _status
|
||||
|
||||
rows = await sor.R('cms_content', ns)
|
||||
total = len(rows)
|
||||
print(json.dumps({'status': 'ok', 'rows': rows, 'total': total}, ensure_ascii=False))
|
||||
71
entcms/wwwroot/api/cms_content_update.dspy
Normal file
71
entcms/wwwroot/api/cms_content_update.dspy
Normal file
@ -0,0 +1,71 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
data = {'id': params_kw.get('id', '')}
|
||||
if not data['id']:
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}, ensure_ascii=False))
|
||||
return
|
||||
|
||||
v = params_kw.get('org_id', None)
|
||||
if v is not None:
|
||||
data['org_id'] = v
|
||||
|
||||
v = params_kw.get('content_type', None)
|
||||
if v is not None:
|
||||
data['content_type'] = v
|
||||
|
||||
v = params_kw.get('category_id', None)
|
||||
if v is not None:
|
||||
data['category_id'] = 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('summary_text', None)
|
||||
if v is not None:
|
||||
data['summary_text'] = v
|
||||
|
||||
v = params_kw.get('body', None)
|
||||
if v is not None:
|
||||
data['body'] = v
|
||||
|
||||
v = params_kw.get('image_url', None)
|
||||
if v is not None:
|
||||
data['image_url'] = v
|
||||
|
||||
v = params_kw.get('tags', None)
|
||||
if v is not None:
|
||||
data['tags'] = v
|
||||
|
||||
v = params_kw.get('sort_order', None)
|
||||
if v is not None:
|
||||
data['sort_order'] = v
|
||||
|
||||
v = params_kw.get('status', None)
|
||||
if v is not None:
|
||||
data['status'] = v
|
||||
|
||||
v = params_kw.get('extra_json', None)
|
||||
if v is not None:
|
||||
data['extra_json'] = v
|
||||
|
||||
v = params_kw.get('approval_id', None)
|
||||
if v is not None:
|
||||
data['approval_id'] = v
|
||||
|
||||
v = params_kw.get('published_at', None)
|
||||
if v is not None:
|
||||
data['published_at'] = v
|
||||
|
||||
await sor.U('cms_content', data)
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}, ensure_ascii=False))
|
||||
68
entcms/wwwroot/api/cms_leads_create.dspy
Normal file
68
entcms/wwwroot/api/cms_leads_create.dspy
Normal file
@ -0,0 +1,68 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
data = {'id': getID()}
|
||||
|
||||
v = params_kw.get('org_id', None)
|
||||
if v is not None:
|
||||
data['org_id'] = v
|
||||
|
||||
v = params_kw.get('source', None)
|
||||
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)
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}, ensure_ascii=False))
|
||||
14
entcms/wwwroot/api/cms_leads_delete.dspy
Normal file
14
entcms/wwwroot/api/cms_leads_delete.dspy
Normal file
@ -0,0 +1,14 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
_id = params_kw.get('id', '')
|
||||
if not _id:
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}, ensure_ascii=False))
|
||||
return
|
||||
await sor.D('cms_leads', {'id': _id})
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '删除成功', 'messagetype': 'success'}}, ensure_ascii=False))
|
||||
36
entcms/wwwroot/api/cms_leads_list.dspy
Normal file
36
entcms/wwwroot/api/cms_leads_list.dspy
Normal file
@ -0,0 +1,36 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
ns = {'sort': 'created_at desc'}
|
||||
|
||||
# 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
|
||||
|
||||
_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)
|
||||
total = len(rows)
|
||||
print(json.dumps({'status': 'ok', 'rows': rows, 'total': total}, ensure_ascii=False))
|
||||
71
entcms/wwwroot/api/cms_leads_update.dspy
Normal file
71
entcms/wwwroot/api/cms_leads_update.dspy
Normal file
@ -0,0 +1,71 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
data = {'id': params_kw.get('id', '')}
|
||||
if not data['id']:
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}, ensure_ascii=False))
|
||||
return
|
||||
|
||||
v = params_kw.get('org_id', None)
|
||||
if v is not None:
|
||||
data['org_id'] = v
|
||||
|
||||
v = params_kw.get('source', None)
|
||||
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.U('cms_leads', data)
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}, ensure_ascii=False))
|
||||
36
entcms/wwwroot/api/cms_site_config_create.dspy
Normal file
36
entcms/wwwroot/api/cms_site_config_create.dspy
Normal file
@ -0,0 +1,36 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
data = {'id': getID()}
|
||||
|
||||
v = params_kw.get('org_id', None)
|
||||
if v is not None:
|
||||
data['org_id'] = 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)
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '创建成功', 'messagetype': 'success'}}, ensure_ascii=False))
|
||||
14
entcms/wwwroot/api/cms_site_config_delete.dspy
Normal file
14
entcms/wwwroot/api/cms_site_config_delete.dspy
Normal file
@ -0,0 +1,14 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
_id = params_kw.get('id', '')
|
||||
if not _id:
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}, ensure_ascii=False))
|
||||
return
|
||||
await sor.D('cms_site_config', {'id': _id})
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '删除成功', 'messagetype': 'success'}}, ensure_ascii=False))
|
||||
32
entcms/wwwroot/api/cms_site_config_list.dspy
Normal file
32
entcms/wwwroot/api/cms_site_config_list.dspy
Normal file
@ -0,0 +1,32 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
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)
|
||||
total = len(rows)
|
||||
print(json.dumps({'status': 'ok', 'rows': rows, 'total': total}, ensure_ascii=False))
|
||||
39
entcms/wwwroot/api/cms_site_config_update.dspy
Normal file
39
entcms/wwwroot/api/cms_site_config_update.dspy
Normal file
@ -0,0 +1,39 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
data = {'id': params_kw.get('id', '')}
|
||||
if not data['id']:
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '缺少ID', 'messagetype': 'error'}}, ensure_ascii=False))
|
||||
return
|
||||
|
||||
v = params_kw.get('org_id', None)
|
||||
if v is not None:
|
||||
data['org_id'] = 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)
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '更新成功', 'messagetype': 'success'}}, ensure_ascii=False))
|
||||
20
entcms/wwwroot/api/get_config.dspy
Normal file
20
entcms/wwwroot/api/get_config.dspy
Normal file
@ -0,0 +1,20 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
group = params_kw.get('group', None)
|
||||
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', '')
|
||||
print(json.dumps({'status': 'ok', 'data': result}, ensure_ascii=False))
|
||||
18
entcms/wwwroot/api/get_content_detail.dspy
Normal file
18
entcms/wwwroot/api/get_content_detail.dspy
Normal file
@ -0,0 +1,18 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
_id = params_kw.get('id', '')
|
||||
if not _id:
|
||||
print(json.dumps({'status': 'error', 'message': '缺少ID'}, ensure_ascii=False))
|
||||
else:
|
||||
ns = {'id': _id}
|
||||
rows = await sor.R('cms_content', ns)
|
||||
if rows:
|
||||
print(json.dumps({'status': 'ok', 'data': rows[0]}, ensure_ascii=False))
|
||||
else:
|
||||
print(json.dumps({'status': 'error', 'message': '内容不存在'}, ensure_ascii=False))
|
||||
17
entcms/wwwroot/api/get_published_content.dspy
Normal file
17
entcms/wwwroot/api/get_published_content.dspy
Normal file
@ -0,0 +1,17 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
content_type = params_kw.get('content_type', None)
|
||||
limit = int(params_kw.get('limit', '10'))
|
||||
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]
|
||||
print(json.dumps({'status': 'ok', 'rows': rows, 'total': len(rows)}, ensure_ascii=False))
|
||||
28
entcms/wwwroot/api/submit_content_approval.dspy
Normal file
28
entcms/wwwroot/api/submit_content_approval.dspy
Normal file
@ -0,0 +1,28 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
content_id = params_kw.get('content_id', '')
|
||||
if not content_id:
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '缺少内容ID', 'messagetype': 'error'}}, ensure_ascii=False))
|
||||
else:
|
||||
# Update status to pending
|
||||
await sor.U('cms_content', {'id': content_id, 'status': 'pending'})
|
||||
|
||||
# Try to call dingdingflow
|
||||
try:
|
||||
from dingdingflow.init import submit_approval
|
||||
user_id = await get_user()
|
||||
ns_detail = {'id': content_id}
|
||||
rows = await sor.R('cms_content', ns_detail)
|
||||
title = rows[0].get('title', '内容审批') if rows else '内容审批'
|
||||
result = await submit_approval('content_publish', content_id, title, user_id)
|
||||
if result and result.get('approval_id'):
|
||||
await sor.U('cms_content', {'id': content_id, 'approval_id': result['approval_id']})
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '已提交审批', 'messagetype': 'success'}}, ensure_ascii=False))
|
||||
except ImportError:
|
||||
print(json.dumps({'widgettype': 'Message', 'options': {'text': '审批模块未安装,状态已改为待审批', 'messagetype': 'warning'}}, ensure_ascii=False))
|
||||
25
entcms/wwwroot/api/submit_lead.dspy
Normal file
25
entcms/wwwroot/api/submit_lead.dspy
Normal file
@ -0,0 +1,25 @@
|
||||
import json
|
||||
from appPublic.uniqueID import getID
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('entcms')
|
||||
sor = DBPools().sqlorContext(dbname)
|
||||
|
||||
data = {
|
||||
'id': getID(),
|
||||
'source': 'website',
|
||||
'status': 'new',
|
||||
'org_id': '0'
|
||||
}
|
||||
for field in ['name', 'company', 'phone', 'email', 'industry', 'region',
|
||||
'interest_products', 'message']:
|
||||
v = params_kw.get(field, None)
|
||||
if v is not None:
|
||||
data[field] = v
|
||||
|
||||
await sor.C('cms_leads', data)
|
||||
print(json.dumps({
|
||||
'widgettype': 'Message',
|
||||
'options': {'text': '感谢您的留言,我们会尽快联系您!', 'messagetype': 'success'}
|
||||
}, ensure_ascii=False))
|
||||
13
entcms/wwwroot/cases.ui
Normal file
13
entcms/wwwroot/cases.ui
Normal file
@ -0,0 +1,13 @@
|
||||
{% 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>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
153
entcms/wwwroot/cms_scripts.js
Normal file
153
entcms/wwwroot/cms_scripts.js
Normal file
@ -0,0 +1,153 @@
|
||||
/* ===== 开元云科技 官网交互脚本 ===== */
|
||||
|
||||
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' });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
640
entcms/wwwroot/cms_styles.css
Normal file
640
entcms/wwwroot/cms_styles.css
Normal file
@ -0,0 +1,640 @@
|
||||
/* ===== 开元云科技 官网样式系统 ===== */
|
||||
/* 设计参考: 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);
|
||||
}
|
||||
17
entcms/wwwroot/index.ui
Normal file
17
entcms/wwwroot/index.ui
Normal file
File diff suppressed because one or more lines are too long
38
entcms/wwwroot/menu.ui
Normal file
38
entcms/wwwroot/menu.ui
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"widgettype": "Menu",
|
||||
"id": "entcms_menu",
|
||||
"options": {
|
||||
"items": [
|
||||
{
|
||||
"name": "cms_content_list",
|
||||
"label": "内容管理",
|
||||
"url": "{{entire_url('/entcms/cms_content_list')}}",
|
||||
"target": "app.sage_main_content"
|
||||
},
|
||||
{
|
||||
"name": "cms_categories_list",
|
||||
"label": "内容分类",
|
||||
"url": "{{entire_url('/entcms/cms_categories_list')}}",
|
||||
"target": "app.sage_main_content"
|
||||
},
|
||||
{
|
||||
"name": "cms_leads_list",
|
||||
"label": "商机线索",
|
||||
"url": "{{entire_url('/entcms/cms_leads_list')}}",
|
||||
"target": "app.sage_main_content"
|
||||
},
|
||||
{
|
||||
"name": "cms_site_config_list",
|
||||
"label": "站点配置",
|
||||
"url": "{{entire_url('/entcms/cms_site_config_list')}}",
|
||||
"target": "app.sage_main_content"
|
||||
},
|
||||
{
|
||||
"name": "public_site",
|
||||
"label": "官网预览",
|
||||
"url": "{{entire_url('/entcms/index.ui')}}",
|
||||
"target": "app.sage_main_content"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
13
entcms/wwwroot/news.ui
Normal file
13
entcms/wwwroot/news.ui
Normal file
@ -0,0 +1,13 @@
|
||||
{% 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>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
13
entcms/wwwroot/news_detail.ui
Normal file
13
entcms/wwwroot/news_detail.ui
Normal file
@ -0,0 +1,13 @@
|
||||
{% 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>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user