Compare commits
No commits in common. "main" and "97541f1fd5b87e54e25e2875e1620c0d8ced1534" have entirely different histories.
main
...
97541f1fd5
80
README.md
80
README.md
@ -1,2 +1,80 @@
|
||||
# portial
|
||||
# Portal — 企业官网CMS独立Web应用
|
||||
|
||||
基于Sage/bricks-framework的独立Web应用,通过pip install加载cms业务模块。
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
Portal (Web应用壳) CMS (业务模块)
|
||||
┌─────────────────────┐ ┌──────────────────────┐
|
||||
│ app/portal.py │────>│ cms/init.py │
|
||||
│ load_cms() │ │ load_cms() │
|
||||
│ │ │ - CMS CRUD │
|
||||
│ wwwroot/ │ │ - DD审批 │
|
||||
│ index.ui (官网) │ │ │
|
||||
│ news.ui │ │ wwwroot/ │
|
||||
│ api/ (公开API) │ │ admin.ui │
|
||||
│ │ │ api/ (管理API) │
|
||||
│ conf/config.json │ │ │
|
||||
│ build.sh │ │ models/ json/ init/ │
|
||||
│ deploy.sh │ │ data.yaml │
|
||||
└─────────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 一键部署
|
||||
cd ~/repos/portal && ./deploy.sh
|
||||
|
||||
# 或分步执行:
|
||||
./build.sh # 构建
|
||||
mysql -h db -u test -p ocai_cms < cms.ddl.sql # 建表
|
||||
py3/bin/python init_data.py # 初始数据
|
||||
py3/bin/python init_superuser_permissions.py # 权限
|
||||
py3/bin/python init_any_permissions.py
|
||||
py3/bin/python ~/repos/cms/scripts/load_path.py
|
||||
./start.sh # 启动
|
||||
```
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
portal/
|
||||
├── app/
|
||||
│ ├── portal.py # 主入口 (from cms.init import load_cms)
|
||||
│ └── global_func.py # 全局函数
|
||||
├── conf/config.json # 应用配置 (数据库ocai_cms, 端口9090)
|
||||
├── wwwroot/ # 公开页面
|
||||
│ ├── index.ui # 官网首页
|
||||
│ ├── products.ui # 产品架构
|
||||
│ ├── news.ui / news_detail.ui
|
||||
│ ├── cases.ui # 成功案例
|
||||
│ ├── admin.ui # 管理后台入口
|
||||
│ └── api/ # 公开只读API
|
||||
│ ├── get_published_content.dspy
|
||||
│ ├── get_content_detail.dspy
|
||||
│ ├── get_config.dspy
|
||||
│ ├── get_sections.dspy
|
||||
│ └── submit_lead.dspy
|
||||
├── build.sh # 构建脚本
|
||||
├── deploy.sh # 一键部署
|
||||
├── init_data.py # 加载初始数据
|
||||
├── init_any_permissions.py # 匿名权限
|
||||
└── init_superuser_permissions.py
|
||||
```
|
||||
|
||||
## CMS模块
|
||||
|
||||
CMS业务模块位于 `~/repos/cms/`,通过 `pip install -e` 安装。
|
||||
详见 [CMS模块README](../cms/README.md)。
|
||||
|
||||
## 访问地址
|
||||
|
||||
| 页面 | URL |
|
||||
|------|-----|
|
||||
| 官网首页 | http://localhost:9090/ |
|
||||
| 产品架构 | /products.ui |
|
||||
| 新闻动态 | /news.ui |
|
||||
| 成功案例 | /cases.ui |
|
||||
| 管理后台 | /cms/admin.ui |
|
||||
|
||||
58
app/global_func.py
Normal file
58
app/global_func.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""
|
||||
Portal全局函数 — 注册到ServerEnv供.dspy和.ui调用
|
||||
"""
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from appPublic.jsonConfig import getConfig
|
||||
|
||||
def get_module_dbname(mname):
|
||||
"""Portal应用统一使用ocai_cms数据库"""
|
||||
return 'ocai_cms'
|
||||
|
||||
def UiWindow(title, icon, content, cheight=10, cwidth=15):
|
||||
return {
|
||||
"widgettype": "PopupWindow",
|
||||
"options": {
|
||||
"author": "portal",
|
||||
"cwidth": cwidth,
|
||||
"cheight": cheight,
|
||||
"title": title,
|
||||
"content": content,
|
||||
"icon": icon or entire_url('/bricks/imgs/app.png'),
|
||||
"movable": True,
|
||||
"auto_open": True
|
||||
}
|
||||
}
|
||||
|
||||
def UiError(title="出错", message="出错啦", timeout=5):
|
||||
return {
|
||||
"widgettype": "Error",
|
||||
"options": {
|
||||
"author": "portal",
|
||||
"timeout": timeout,
|
||||
"cwidth": 15,
|
||||
"cheight": 10,
|
||||
"title": title,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
def UiMessage(title="消息", message="后台消息", timeout=5):
|
||||
return {
|
||||
"widgettype": "Message",
|
||||
"options": {
|
||||
"author": "portal",
|
||||
"timeout": timeout,
|
||||
"cwidth": 15,
|
||||
"cheight": 10,
|
||||
"title": title,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
def set_globalvariable():
|
||||
g = ServerEnv()
|
||||
g.getConfig = getConfig
|
||||
g.get_module_dbname = get_module_dbname
|
||||
g.UiError = UiError
|
||||
g.UiMessage = UiMessage
|
||||
g.UiWindow = UiWindow
|
||||
58
app/portal.py
Normal file
58
app/portal.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""
|
||||
Portal Web应用主入口 — CMS业务壳
|
||||
启动: py3/bin/python app/portal.py -p 9090 -w $(pwd)
|
||||
|
||||
Portal是一个轻量级Web应用壳,通过pip install加载cms业务模块。
|
||||
类似pipeline-app模式:app壳负责基础设施初始化,业务逻辑在模块中。
|
||||
"""
|
||||
import os, sys
|
||||
|
||||
# 添加应用根目录到Python路径
|
||||
app_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
root_dir = os.path.dirname(app_dir)
|
||||
sys.path.insert(0, root_dir)
|
||||
|
||||
# Ensure app/ is in path for local imports
|
||||
sys.path.insert(0, app_dir)
|
||||
|
||||
from appPublic.log import MyLogger, info
|
||||
from appPublic.folderUtils import ProgramPath
|
||||
from appPublic.jsonConfig import getConfig
|
||||
from appPublic.registerfunction import RegisterFunction
|
||||
from bricks_for_python.init import load_pybricks
|
||||
from ahserver.webapp import webapp
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from sqlor.dbpools import DBPools
|
||||
|
||||
# CMS业务模块 (通过pip install -e ~/repos/cms安装)
|
||||
from cms.init import load_cms
|
||||
|
||||
# RBAC认证(复用sage的rbac模块)
|
||||
from rbac.init import load_rbac
|
||||
from appbase.init import load_appbase
|
||||
|
||||
# 全局函数
|
||||
from global_func import set_globalvariable
|
||||
|
||||
__version__ = '1.0.0'
|
||||
|
||||
def get_module_dbname(m):
|
||||
return 'ocai_cms'
|
||||
|
||||
def init():
|
||||
rf = RegisterFunction()
|
||||
set_globalvariable()
|
||||
env = ServerEnv()
|
||||
env.get_module_dbname = get_module_dbname
|
||||
|
||||
# Initialize DBPools and register db on ServerEnv for dspy use
|
||||
config = getConfig('.')
|
||||
env.db = DBPools(config.databases)
|
||||
|
||||
load_pybricks()
|
||||
load_appbase()
|
||||
load_rbac()
|
||||
load_cms()
|
||||
|
||||
if __name__ == '__main__':
|
||||
webapp(init)
|
||||
171
build.sh
Executable file
171
build.sh
Executable file
@ -0,0 +1,171 @@
|
||||
#!/usr/bin/env bash
|
||||
# Portal Web应用 — 构建脚本
|
||||
# Portal是CMS业务的独立Web应用壳,通过pip install加载cms模块
|
||||
# 用法: cd ~/repos/portal && ./build.sh
|
||||
set -e
|
||||
|
||||
cdir=$(pwd)
|
||||
uname=$(id -un)
|
||||
gname=$(id -gn)
|
||||
|
||||
echo "============================================"
|
||||
echo " Portal Web应用 — 构建"
|
||||
echo "============================================"
|
||||
|
||||
# ===========================================
|
||||
# Step 1: Python虚拟环境
|
||||
# ===========================================
|
||||
echo ""
|
||||
echo "--- Step 1: 创建Python虚拟环境 ---"
|
||||
if [ ! -d "py3" ]; then
|
||||
python3 -m venv py3
|
||||
fi
|
||||
source py3/bin/activate
|
||||
|
||||
# ===========================================
|
||||
# Step 2: 核心基础设施包
|
||||
# ===========================================
|
||||
echo ""
|
||||
echo "--- Step 2: 安装核心基础设施包 ---"
|
||||
mkdir -p pkgs
|
||||
|
||||
for m in apppublic sqlor ahserver bricks-for-python xls2ddl
|
||||
do
|
||||
echo " install $m..."
|
||||
cd $cdir/pkgs
|
||||
if [ ! -d "$m" ]; then
|
||||
git clone https://git.opencomputing.cn/yumoqing/$m
|
||||
fi
|
||||
cd $m
|
||||
$cdir/py3/bin/pip install . 2>/dev/null || echo " WARN: $m install failed"
|
||||
done
|
||||
|
||||
# bricks前端
|
||||
echo " install bricks..."
|
||||
cd $cdir/pkgs
|
||||
if [ ! -d "bricks" ]; then
|
||||
git clone https://git.opencomputing.cn/yumoqing/bricks
|
||||
fi
|
||||
cd bricks/bricks
|
||||
./build.sh 2>/dev/null || echo " WARN: bricks build skipped"
|
||||
|
||||
# bricks符号链接
|
||||
mkdir -p $cdir/bricks
|
||||
if [ -d "$cdir/pkgs/bricks/dist" ]; then
|
||||
rm -f $cdir/bricks
|
||||
ln -sf $cdir/pkgs/bricks/dist $cdir/bricks
|
||||
fi
|
||||
|
||||
# ===========================================
|
||||
# Step 3: RBAC + AppBase + checklang
|
||||
# ===========================================
|
||||
echo ""
|
||||
echo "--- Step 3: 安装RBAC/AppBase模块 ---"
|
||||
for m in appbase rbac checklang
|
||||
do
|
||||
echo " install $m..."
|
||||
cd $cdir/pkgs
|
||||
if [ ! -d "$m" ]; then
|
||||
git clone https://git.opencomputing.cn/yumoqing/$m
|
||||
fi
|
||||
cd $m
|
||||
$cdir/py3/bin/pip install . 2>/dev/null || echo " WARN: $m install failed"
|
||||
done
|
||||
|
||||
# ===========================================
|
||||
# Step 4: CMS业务模块 (editable mode)
|
||||
# ===========================================
|
||||
echo ""
|
||||
echo "--- Step 4: 安装CMS业务模块 ---"
|
||||
|
||||
CMS_DIR=~/repos/cms
|
||||
echo " install cms (editable)..."
|
||||
$cdir/py3/bin/pip install -e $CMS_DIR 2>/dev/null || echo " WARN: cms install failed"
|
||||
|
||||
# ===========================================
|
||||
# Step 5: 数据库DDL (从cms模块的models目录)
|
||||
# ===========================================
|
||||
echo ""
|
||||
echo "--- Step 5: 生成数据库DDL ---"
|
||||
|
||||
if [ -d "$CMS_DIR/models" ]; then
|
||||
cd $CMS_DIR/models
|
||||
echo " 生成 CMS DDL..."
|
||||
$cdir/py3/bin/json2ddl mysql . > $cdir/cms.ddl.sql 2>/dev/null || echo " WARN: json2ddl failed"
|
||||
echo " DDL已生成: cms.ddl.sql"
|
||||
fi
|
||||
|
||||
# ===========================================
|
||||
# Step 6: CRUD UI生成 (从cms模块的json目录)
|
||||
# ===========================================
|
||||
echo ""
|
||||
echo "--- Step 6: 生成CRUD UI ---"
|
||||
|
||||
if [ -d "$CMS_DIR/json" ]; then
|
||||
cd $CMS_DIR/json
|
||||
echo " 生成 CMS CRUD UI..."
|
||||
for f in *.json; do
|
||||
[ -f "$f" ] || continue
|
||||
echo " $f"
|
||||
$cdir/py3/bin/xls2ui -m ../models -o $CMS_DIR/wwwroot cms $f 2>/dev/null || echo " WARN: xls2ui failed for $f"
|
||||
done
|
||||
fi
|
||||
|
||||
# ===========================================
|
||||
# Step 7: 日志和文件目录
|
||||
# ===========================================
|
||||
echo ""
|
||||
echo "--- Step 7: 创建运行时目录 ---"
|
||||
mkdir -p $cdir/logs
|
||||
mkdir -p $cdir/files
|
||||
|
||||
# ===========================================
|
||||
# Step 8: systemd服务文件
|
||||
# ===========================================
|
||||
echo ""
|
||||
echo "--- Step 8: 生成systemd服务文件 ---"
|
||||
cat > $cdir/portal.service <<EOF
|
||||
[Unit]
|
||||
Description=Portal CMS Web Application
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=$uname
|
||||
Group=$gname
|
||||
Type=forking
|
||||
WorkingDirectory=$cdir
|
||||
ExecStart=$cdir/start.sh
|
||||
ExecStop=$cdir/stop.sh
|
||||
StandardOutput=append:$cdir/logs/portal.log
|
||||
StandardError=append:$cdir/logs/portal.log
|
||||
SyslogIdentifier=portal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
echo " portal.service 已生成"
|
||||
|
||||
# ===========================================
|
||||
# Done
|
||||
# ===========================================
|
||||
cd $cdir
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " 构建完成!"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
echo "后续步骤:"
|
||||
echo " 1. 编辑 conf/config.json 填入数据库密码"
|
||||
echo " 2. 执行DDL创建CMS业务表:"
|
||||
echo " mysql -h HOST -u USER -pPASS ocai_cms < cms.ddl.sql"
|
||||
echo " 3. 初始化数据(appcodes/分类/栏目/配置):"
|
||||
echo " py3/bin/python init_data.py"
|
||||
echo " 4. 初始化权限:"
|
||||
echo " py3/bin/python init_superuser_permissions.py"
|
||||
echo " py3/bin/python init_any_permissions.py"
|
||||
echo " py3/bin/python ~/repos/cms/scripts/load_path.py"
|
||||
echo " 5. 启动应用:"
|
||||
echo " ./start.sh"
|
||||
echo ""
|
||||
echo "访问地址: http://localhost:9090/"
|
||||
echo "管理后台: http://localhost:9090/admin.ui"
|
||||
76
conf/config.json
Normal file
76
conf/config.json
Normal file
@ -0,0 +1,76 @@
|
||||
{
|
||||
"password_key": "!@#$%^&*(*&^%$QWERTYUIqwertyui234567",
|
||||
"logger": {
|
||||
"name": "portal",
|
||||
"levelname": "info",
|
||||
"logfile": "$[workdir]$/logs/portal.log"
|
||||
},
|
||||
"filesroot": "$[workdir]$/files",
|
||||
"databases": {
|
||||
"ocai_cms": {
|
||||
"driver": "mysql",
|
||||
"async_mode": true,
|
||||
"coding": "utf8",
|
||||
"dbname": "ocai_cms",
|
||||
"kwargs": {
|
||||
"user": "test",
|
||||
"db": "ocai_cms",
|
||||
"password": "SS+C1MDMJrslBwGzYIv3nQ==",
|
||||
"host": "db"
|
||||
}
|
||||
}
|
||||
},
|
||||
"website": {
|
||||
"paths": [
|
||||
[
|
||||
"$[workdir]$/wwwroot",
|
||||
""
|
||||
],
|
||||
[
|
||||
"$[workdir]$/../cms/wwwroot",
|
||||
"/cms"
|
||||
]
|
||||
],
|
||||
"host": "0.0.0.0",
|
||||
"port": 9090,
|
||||
"coding": "utf-8",
|
||||
"session_redis": {
|
||||
"url": "redis://127.0.0.1:6379/0"
|
||||
},
|
||||
"indexes": [
|
||||
"index.ui",
|
||||
"index.html",
|
||||
"index.tmpl"
|
||||
],
|
||||
"processors": [
|
||||
[
|
||||
".xlsxds",
|
||||
"xlsxds"
|
||||
],
|
||||
[
|
||||
".sqlds",
|
||||
"sqlds"
|
||||
],
|
||||
[
|
||||
".tmpl",
|
||||
"tmpl"
|
||||
],
|
||||
[
|
||||
".dspy",
|
||||
"dspy"
|
||||
],
|
||||
[
|
||||
".ui",
|
||||
"bui"
|
||||
],
|
||||
[
|
||||
".md",
|
||||
"md"
|
||||
]
|
||||
]
|
||||
},
|
||||
"langMapping": {
|
||||
"zh-Hans-CN": "zh-cn",
|
||||
"en-US": "en"
|
||||
}
|
||||
}
|
||||
88
deploy.sh
Executable file
88
deploy.sh
Executable file
@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
# Portal 一键部署脚本
|
||||
# 完成从构建到启动的全流程
|
||||
# 用法: cd ~/repos/portal && ./deploy.sh
|
||||
set -e
|
||||
|
||||
cdir=$(cd "$(dirname "$0")" && pwd)
|
||||
cd "$cdir"
|
||||
|
||||
echo "╔══════════════════════════════════════════╗"
|
||||
echo "║ Portal CMS — 一键部署 ║"
|
||||
echo "╚══════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# Step 1: Build
|
||||
echo "=== Step 1/5: 构建 ==="
|
||||
bash "$cdir/build.sh"
|
||||
echo ""
|
||||
|
||||
# Step 2: Database DDL
|
||||
echo "=== Step 2/5: 创建数据库表 ==="
|
||||
if [ -f "$cdir/cms.ddl.sql" ]; then
|
||||
# 从config.json读取数据库连接信息
|
||||
DB_HOST=$(python3 -c "import json; c=json.load(open('$cdir/conf/config.json')); print(c['databases']['ocai_cms']['kwargs']['host'])" 2>/dev/null || echo "db")
|
||||
DB_USER=$(python3 -c "import json; c=json.load(open('$cdir/conf/config.json')); print(c['databases']['ocai_cms']['kwargs']['user'])" 2>/dev/null || echo "test")
|
||||
DB_PASS=$(python3 -c "
|
||||
import json, sys
|
||||
sys.path.insert(0, '$cdir')
|
||||
from appPublic.password import decode
|
||||
c=json.load(open('$cdir/conf/config.json'))
|
||||
print(decode(c['databases']['ocai_cms']['kwargs']['password'], c['password_key']))
|
||||
" 2>/dev/null || echo "")
|
||||
DB_NAME=$(python3 -c "import json; c=json.load(open('$cdir/conf/config.json')); print(c['databases']['ocai_cms']['kwargs']['db'])" 2>/dev/null || echo "ocai_cms")
|
||||
|
||||
if [ -n "$DB_PASS" ]; then
|
||||
echo " 执行DDL: $DB_HOST/$DB_NAME ..."
|
||||
mysql -h "$DB_HOST" -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" < "$cdir/cms.ddl.sql" 2>/dev/null && \
|
||||
echo " ✓ DDL执行成功" || \
|
||||
echo " ⚠ DDL执行失败(表可能已存在),继续..."
|
||||
else
|
||||
echo " ⚠ 无法读取数据库密码,请手动执行:"
|
||||
echo " mysql -h $DB_HOST -u $DB_USER -p $DB_NAME < cms.ddl.sql"
|
||||
fi
|
||||
else
|
||||
echo " ⚠ cms.ddl.sql 不存在,跳过(请先运行 build.sh)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 3: Init data
|
||||
echo "=== Step 3/5: 加载初始数据 ==="
|
||||
if [ -f "$cdir/py3/bin/python" ]; then
|
||||
"$cdir/py3/bin/python" "$cdir/init_data.py" 2>&1 || echo " ⚠ 初始数据加载失败,继续..."
|
||||
else
|
||||
echo " ⚠ py3未构建,跳过"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 4: Init permissions
|
||||
echo "=== Step 4/5: 初始化权限 ==="
|
||||
if [ -f "$cdir/py3/bin/python" ]; then
|
||||
"$cdir/py3/bin/python" "$cdir/init_superuser_permissions.py" 2>&1 || echo " ⚠ superuser权限失败"
|
||||
"$cdir/py3/bin/python" "$cdir/init_any_permissions.py" 2>&1 || echo " ⚠ any权限失败"
|
||||
"$cdir/py3/bin/python" ~/repos/cms/scripts/load_path.py 2>&1 || echo " ⚠ CMS模块权限失败"
|
||||
echo " ✓ 权限初始化完成"
|
||||
else
|
||||
echo " ⚠ py3未构建,跳过"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Step 5: Start
|
||||
echo "=== Step 5/5: 启动应用 ==="
|
||||
if [ -f "$cdir/start.sh" ]; then
|
||||
# 先停掉旧进程
|
||||
bash "$cdir/stop.sh" 2>/dev/null || true
|
||||
sleep 1
|
||||
bash "$cdir/start.sh"
|
||||
echo " ✓ 应用已启动"
|
||||
else
|
||||
echo " ⚠ start.sh 不存在"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "╔══════════════════════════════════════════╗"
|
||||
echo "║ 部署完成! ║"
|
||||
echo "╠══════════════════════════════════════════╣"
|
||||
echo "║ 官网: http://localhost:9090/ ║"
|
||||
echo "║ 管理: http://localhost:9090/cms/admin.ui ║"
|
||||
echo "╚══════════════════════════════════════════╝"
|
||||
139
docs/architecture.md
Normal file
139
docs/architecture.md
Normal file
@ -0,0 +1,139 @@
|
||||
# Portal系统架构
|
||||
|
||||
## 项目概述
|
||||
|
||||
Portal是企业官网CMS系统的独立Web应用壳,采用与pipeline-app相同的"壳+模块"架构模式。
|
||||
Portal负责Web服务器启动、基础设施加载和公开前端页面;CMS业务逻辑通过pip install以模块方式引入。
|
||||
|
||||
## 架构模式: 壳+模块
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Portal (Web壳) │
|
||||
│ │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ ahserver │ │ rbac │ │ appbase │ │
|
||||
│ │ (Web服务) │ │ (认证) │ │ (基础) │ │
|
||||
│ └──────────┘ └──────────┘ └──────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────┐ │
|
||||
│ │ wwwroot/ (公开前端) │ │
|
||||
│ │ index.ui products.ui news.ui ... │ │
|
||||
│ │ api/get_published_content.dspy ... │ │
|
||||
│ │ dingdingflow/index.ui ... │ │
|
||||
│ └──────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────────┐ │
|
||||
│ │ CMS模块 (pip install -e ~/repos/cms) │ │
|
||||
│ │ │ │
|
||||
│ │ ┌─────────┐ ┌──────────────┐ │ │
|
||||
│ │ │ entcms │ │ dingdingflow │ │ │
|
||||
│ │ │ 内容管理 │ │ 钉钉审批 │ │ │
|
||||
│ │ │ CRUD页面│ │ CRUD页面 │ │ │
|
||||
│ │ │ 数据模型 │ │ 数据模型 │ │ │
|
||||
│ │ └─────────┘ └──────────────┘ │ │
|
||||
│ └──────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 初始化流程
|
||||
|
||||
Portal启动时的加载顺序:
|
||||
|
||||
```
|
||||
1. set_globalvariable() — 注册全局函数 (get_module_dbname, UiError等)
|
||||
2. load_pybricks() — 加载bricks前端框架
|
||||
3. load_appbase() — 加载应用基础模块 (公共函数注册)
|
||||
4. load_rbac() — 加载RBAC认证模块 (角色权限控制)
|
||||
5. load_cms() — 加载CMS业务模块 (entcms + dingdingflow)
|
||||
```
|
||||
|
||||
所有模块注册到 `ServerEnv`,供.dspy和.ui文件在运行时调用。
|
||||
|
||||
## 数据库
|
||||
|
||||
统一使用 `ocai_cms` 数据库:
|
||||
|
||||
| 模块 | 表 |
|
||||
|------|-----|
|
||||
| entcms | cms_content, cms_categories, cms_sections, cms_leads, cms_site_config |
|
||||
| dingdingflow | dd_approvals, dd_approval_configs |
|
||||
|
||||
`get_module_dbname()` 对所有模块返回 `'ocai_cms'`。
|
||||
|
||||
## 前端页面分层
|
||||
|
||||
### 公开页面 (Portal wwwroot/)
|
||||
| 页面 | 权限 | 说明 |
|
||||
|------|------|------|
|
||||
| index.ui | any | 官网首页 (导航/Hero/产品/案例/新闻/页脚/浮动入口) |
|
||||
| products.ui | any | 产品架构列表 |
|
||||
| news.ui | any | 新闻列表 |
|
||||
| news_detail.ui | any | 新闻详情 |
|
||||
| cases.ui | any | 案例列表 |
|
||||
| admin.ui | logined | 管理后台仪表盘 (入口) |
|
||||
| menu.ui | logined | 管理菜单 |
|
||||
|
||||
### 公开API (Portal wwwroot/api/)
|
||||
| API | 权限 | 说明 |
|
||||
|-----|------|------|
|
||||
| get_published_content.dspy | any | 获取已发布内容 |
|
||||
| get_content_detail.dspy | any | 获取内容详情 |
|
||||
| get_config.dspy | any | 获取站点配置 |
|
||||
| get_sections.dspy | any | 获取栏目列表 |
|
||||
| submit_lead.dspy | any | 提交商机线索 |
|
||||
|
||||
### 管理CRUD页面 (CMS模块wwwroot/)
|
||||
由cms模块提供,不在portal/wwwroot中:
|
||||
- cms_content_list (内容管理)
|
||||
- cms_categories_list (分类管理)
|
||||
- cms_sections_list (栏目管理)
|
||||
- cms_leads_list (线索管理)
|
||||
- cms_site_config_list (站点配置)
|
||||
|
||||
### 钉钉审批 (Portal wwwroot/dingdingflow/)
|
||||
| 页面 | 权限 | 说明 |
|
||||
|------|------|------|
|
||||
| dingdingflow/index.ui | any | 审批列表入口 |
|
||||
| dingdingflow/menu.ui | any | 审批菜单 |
|
||||
| dingdingflow/api/*.dspy | any | 审批API |
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 层 | 技术 |
|
||||
|----|------|
|
||||
| Web服务器 | ahserver (异步HTTP) |
|
||||
| 前端框架 | bricks-framework (JSON UI DSL) |
|
||||
| 数据库 | MySQL (async, sqlor连接池) |
|
||||
| 认证 | RBAC (角色权限控制) |
|
||||
| 基础设施 | appbase, apppublic, checklang |
|
||||
| 业务模块 | cms (entcms + dingdingflow) |
|
||||
|
||||
## 部署架构
|
||||
|
||||
```
|
||||
Nginx/LB (80/443)
|
||||
│
|
||||
▼
|
||||
Portal (ahserver :9090)
|
||||
│
|
||||
├── / wwwroot/ (公开页面 + API)
|
||||
├── /bricks/ → pkgs/bricks/dist (前端框架)
|
||||
└── CMS模块 (pip installed)
|
||||
└── CRUD页面 + 业务逻辑
|
||||
│
|
||||
▼
|
||||
MySQL (ocai_cms)
|
||||
Redis (session)
|
||||
```
|
||||
|
||||
## 与pipeline-app对比
|
||||
|
||||
| 特征 | pipeline-app | portal |
|
||||
|------|-------------|--------|
|
||||
| 业务模块数 | 3 (core/ops/dist) | 1 (cms) |
|
||||
| 模块安装方式 | 本地子目录 | editable pip install |
|
||||
| 公开页面 | index.ui | index.ui + products + news + cases |
|
||||
| 公开API | 无 | 5个只读接口 |
|
||||
| 数据库 | pipeline_* | ocai_cms |
|
||||
| 端口 | 8080 | 9090 |
|
||||
114
init_any_permissions.py
Normal file
114
init_any_permissions.py
Normal file
@ -0,0 +1,114 @@
|
||||
"""
|
||||
Portal RBAC权限初始化 — any (匿名) 角色
|
||||
扫描 wwwroot 和 bricks 下的公开页面,授予 any 角色权限
|
||||
|
||||
规则:
|
||||
- wwwroot/* → /<file> (公开页面和API)
|
||||
- bricks/* → /bricks/<file>
|
||||
- /cms/* 由 cms/scripts/load_path.py 管理(需要登录)
|
||||
|
||||
用法: cd ~/repos/portal && py3/bin/python init_any_permissions.py
|
||||
"""
|
||||
import os, sys, subprocess
|
||||
|
||||
def find_app_root():
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
app_root = find_app_root()
|
||||
sage_root = None
|
||||
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]:
|
||||
if os.path.isdir(os.path.join(c, "py3", "bin")):
|
||||
sage_root = c
|
||||
break
|
||||
if not sage_root:
|
||||
print("ERROR: 找不到Sage,无法初始化权限")
|
||||
sys.exit(1)
|
||||
|
||||
py = os.path.join(sage_root, "py3", "bin", "python")
|
||||
sp = os.path.join(sage_root, "set_role_perm.py")
|
||||
if not os.path.exists(sp):
|
||||
print("ERROR: 找不到set_role_perm.py")
|
||||
sys.exit(1)
|
||||
|
||||
SKIP_DIRS = {".git", "__pycache__", "node_modules", ".svn"}
|
||||
SKIP_EXTS = {".pyc", ".pyo", ".swp", ".swo", ".bak", ".orig", ".log", ".pid", ".lock"}
|
||||
|
||||
def scan_dir(base_dir, url_prefix):
|
||||
paths = []
|
||||
if not os.path.isdir(base_dir):
|
||||
return paths
|
||||
for root, dirs, files in os.walk(base_dir):
|
||||
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
|
||||
for f in sorted(files):
|
||||
_, ext = os.path.splitext(f)
|
||||
if ext.lower() in SKIP_EXTS or f.startswith("."):
|
||||
continue
|
||||
full_path = os.path.join(root, f)
|
||||
if os.path.islink(full_path):
|
||||
link_target = os.path.realpath(full_path)
|
||||
if not link_target.startswith(app_root):
|
||||
continue
|
||||
rel_path = os.path.relpath(full_path, base_dir)
|
||||
url = url_prefix + "/" + rel_path.replace(os.sep, "/")
|
||||
paths.append(url)
|
||||
return paths
|
||||
|
||||
def set_any_perms(paths):
|
||||
count = 0
|
||||
env = os.environ.copy()
|
||||
env['SAGE_RBAC_DB'] = 'ocai_cms'
|
||||
for p in paths:
|
||||
result = subprocess.run(
|
||||
[py, sp, "any", p],
|
||||
cwd=sage_root, capture_output=True, text=True, env=env
|
||||
)
|
||||
status = "✓" if result.returncode == 0 else "✗"
|
||||
print(f" {status} any {p}")
|
||||
count += 1
|
||||
return count
|
||||
|
||||
print("=== Portal RBAC权限初始化 — any (匿名访问) ===")
|
||||
print(f"Sage: {sage_root}")
|
||||
print()
|
||||
|
||||
# 1. wwwroot/ 公开页面和API
|
||||
wwwroot_dir = os.path.join(app_root, "wwwroot")
|
||||
root_paths = scan_dir(wwwroot_dir, "")
|
||||
root_paths.append("/") # 根路径
|
||||
print(f"--- wwwroot/ → / ({len(root_paths)} 个路径) ---")
|
||||
n1 = set_any_perms(root_paths)
|
||||
|
||||
# 2. bricks/
|
||||
bricks_dir = os.path.join(app_root, "bricks")
|
||||
bricks_paths = scan_dir(bricks_dir, "/bricks")
|
||||
if bricks_paths:
|
||||
print(f"\n--- bricks → /bricks ({len(bricks_paths)} 个文件) ---")
|
||||
n2 = set_any_perms(bricks_paths)
|
||||
else:
|
||||
n2 = 0
|
||||
print(f"\n--- bricks → /bricks (未构建,跳过) ---")
|
||||
|
||||
# 3. rbac模块公开路径 (登录页、注册、验证码等)
|
||||
# 从rbac的load_path.py导入PATHS_ANY列表
|
||||
rbac_load_path = os.path.join(os.path.dirname(app_root), "rbac", "scripts", "load_path.py")
|
||||
rbac_any_paths = []
|
||||
if os.path.exists(rbac_load_path):
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("rbac_load_path", rbac_load_path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
# 阻止register_paths自动执行
|
||||
mod.__name__ = "rbac_load_path"
|
||||
spec.loader.exec_module(mod)
|
||||
rbac_any_paths = getattr(mod, 'PATHS_ANY', [])
|
||||
else:
|
||||
print("WARNING: 找不到rbac/scripts/load_path.py,跳过rbac路径注册")
|
||||
|
||||
if rbac_any_paths:
|
||||
print(f"\n--- rbac模块 → any ({len(rbac_any_paths)} 个路径) ---")
|
||||
n3 = set_any_perms(rbac_any_paths)
|
||||
else:
|
||||
n3 = 0
|
||||
print("\n--- rbac模块 → any (无路径,跳过) ---")
|
||||
|
||||
total = n1 + n2 + n3
|
||||
print(f"\n=== 完成: 共设置 {total} 个any权限 ===")
|
||||
179
init_data.py
Normal file
179
init_data.py
Normal file
@ -0,0 +1,179 @@
|
||||
"""
|
||||
Portal 一键部署 — 初始化数据
|
||||
从 cms 模块的 init/data.yaml 加载所有初始数据:
|
||||
- appcodes (枚举编码)
|
||||
- appcodes_kv (枚举值)
|
||||
- cms_categories (默认分类)
|
||||
- cms_site_config (默认站点配置)
|
||||
- cms_sections (默认栏目配置)
|
||||
- dd_approval_configs (默认审批配置)
|
||||
|
||||
用法: cd ~/repos/portal && py3/bin/python init_data.py
|
||||
"""
|
||||
import os, sys, json
|
||||
|
||||
app_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
root_dir = app_dir
|
||||
sys.path.insert(0, root_dir)
|
||||
|
||||
# 读取YAML
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
# 简单YAML解析(fallback)
|
||||
yaml = None
|
||||
|
||||
CMS_DIR = os.path.expanduser("~/cms")
|
||||
DATA_FILE = os.path.join(CMS_DIR, "init", "data.yaml")
|
||||
|
||||
def load_yaml_simple(path):
|
||||
"""简单YAML加载(仅支持本文件用到的结构)"""
|
||||
if yaml:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f)
|
||||
else:
|
||||
# 用json做fallback - 要求data文件也有json版本
|
||||
json_path = path.replace('.yaml', '.json')
|
||||
if os.path.exists(json_path):
|
||||
with open(json_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
raise ImportError("需要PyYAML: pip install pyyaml")
|
||||
|
||||
|
||||
def main():
|
||||
print("=== Portal 一键部署 — 初始化数据 ===")
|
||||
print(f"CMS模块: {CMS_DIR}")
|
||||
print(f"数据文件: {DATA_FILE}")
|
||||
print()
|
||||
|
||||
if not os.path.exists(DATA_FILE):
|
||||
print(f"ERROR: 数据文件不存在: {DATA_FILE}")
|
||||
sys.exit(1)
|
||||
|
||||
# 加载YAML
|
||||
data = load_yaml_simple(DATA_FILE)
|
||||
|
||||
# 连接数据库
|
||||
from sqlor.dbpools import DBPools
|
||||
from appPublic.uniqueID import getID
|
||||
from ahserver.serverenv import ServerEnv
|
||||
from appPublic.jsonConfig import getConfig
|
||||
from appPublic.log import MyLogger
|
||||
|
||||
# 初始化日志和配置
|
||||
MyLogger(getConfig())
|
||||
db = DBPools(getConfig().databases)
|
||||
dbname = 'ocai_cms'
|
||||
|
||||
import asyncio
|
||||
|
||||
async def insert_data():
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
|
||||
# 1. appcodes
|
||||
appcodes = data.get('appcodes', [])
|
||||
if appcodes:
|
||||
print(f"\n--- appcodes ({len(appcodes)} 条) ---")
|
||||
for item in appcodes:
|
||||
item.setdefault('id', getID())
|
||||
item.setdefault('org_id', '0')
|
||||
try:
|
||||
# 检查是否已存在
|
||||
existing = await sor.R('appcodes', {'id': item['id']})
|
||||
if not existing:
|
||||
await sor.C('appcodes', item)
|
||||
print(f" ✓ {item['id']} - {item.get('name', '')}")
|
||||
else:
|
||||
print(f" · {item['id']} (已存在)")
|
||||
except Exception as e:
|
||||
print(f" ✗ {item.get('id', '?')}: {e}")
|
||||
|
||||
# 2. appcodes_kv
|
||||
appcodes_kv = data.get('appcodes_kv', [])
|
||||
if appcodes_kv:
|
||||
print(f"\n--- appcodes_kv ({len(appcodes_kv)} 条) ---")
|
||||
for item in appcodes_kv:
|
||||
item.setdefault('id', getID())
|
||||
item.setdefault('org_id', '0')
|
||||
try:
|
||||
existing = await sor.R('appcodes_kv', {'id': item['id']})
|
||||
if not existing:
|
||||
await sor.C('appcodes_kv', item)
|
||||
print(f" ✓ {item['id']} ({item.get('parentid', '')}.{item.get('k', '')} = {item.get('v', '')})")
|
||||
else:
|
||||
print(f" · {item['id']} (已存在)")
|
||||
except Exception as e:
|
||||
print(f" ✗ {item.get('id', '?')}: {e}")
|
||||
|
||||
# 3. cms_categories
|
||||
categories = data.get('cms_categories', [])
|
||||
if categories:
|
||||
print(f"\n--- cms_categories ({len(categories)} 条) ---")
|
||||
for item in categories:
|
||||
item.setdefault('id', getID())
|
||||
try:
|
||||
existing = await sor.R('cms_categories', {'id': item['id']})
|
||||
if not existing:
|
||||
await sor.C('cms_categories', item)
|
||||
print(f" ✓ {item['id']} - {item.get('name', '')} [{item.get('content_type', '')}]")
|
||||
else:
|
||||
print(f" · {item['id']} (已存在)")
|
||||
except Exception as e:
|
||||
print(f" ✗ {item.get('id', '?')}: {e}")
|
||||
|
||||
# 4. cms_site_config
|
||||
configs = data.get('cms_site_config', [])
|
||||
if configs:
|
||||
print(f"\n--- cms_site_config ({len(configs)} 条) ---")
|
||||
for item in configs:
|
||||
item.setdefault('id', getID())
|
||||
try:
|
||||
existing = await sor.R('cms_site_config', {'id': item['id']})
|
||||
if not existing:
|
||||
await sor.C('cms_site_config', item)
|
||||
print(f" ✓ {item['id']} ({item.get('config_group', '')}.{item.get('config_key', '')})")
|
||||
else:
|
||||
print(f" · {item['id']} (已存在)")
|
||||
except Exception as e:
|
||||
print(f" ✗ {item.get('id', '?')}: {e}")
|
||||
|
||||
# 5. cms_sections
|
||||
sections = data.get('cms_sections', [])
|
||||
if sections:
|
||||
print(f"\n--- cms_sections ({len(sections)} 条) ---")
|
||||
for item in sections:
|
||||
item.setdefault('id', getID())
|
||||
try:
|
||||
existing = await sor.R('cms_sections', {'id': item['id']})
|
||||
if not existing:
|
||||
await sor.C('cms_sections', item)
|
||||
print(f" ✓ {item['id']} - {item.get('title', '')} [{item.get('section_type', '')}]")
|
||||
else:
|
||||
print(f" · {item['id']} (已存在)")
|
||||
except Exception as e:
|
||||
print(f" ✗ {item.get('id', '?')}: {e}")
|
||||
|
||||
# 6. dd_approval_configs
|
||||
dd_configs = data.get('dd_approval_configs', [])
|
||||
if dd_configs:
|
||||
print(f"\n--- dd_approval_configs ({len(dd_configs)} 条) ---")
|
||||
for item in dd_configs:
|
||||
item.setdefault('id', getID())
|
||||
try:
|
||||
existing = await sor.R('dd_approval_configs', {'id': item['id']})
|
||||
if not existing:
|
||||
await sor.C('dd_approval_configs', item)
|
||||
print(f" ✓ {item['id']} - {item.get('biz_type', '')}")
|
||||
else:
|
||||
print(f" · {item['id']} (已存在)")
|
||||
except Exception as e:
|
||||
print(f" ✗ {item.get('id', '?')}: {e}")
|
||||
|
||||
total = len(appcodes) + len(appcodes_kv) + len(categories) + len(configs) + len(sections) + len(dd_configs)
|
||||
print(f"\n=== 完成: 处理 {total} 条初始数据 ===")
|
||||
|
||||
asyncio.run(insert_data())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
143
init_superuser_permissions.py
Normal file
143
init_superuser_permissions.py
Normal file
@ -0,0 +1,143 @@
|
||||
"""
|
||||
Portal RBAC权限初始化 — superuser角色
|
||||
为owner.superuser授予Portal所有权限
|
||||
|
||||
Portal包含:
|
||||
- 公开页面 (wwwroot下的.ui和静态文件)
|
||||
- CMS管理CRUD页面 (cms模块wwwroot,路由到/cms/)
|
||||
- appbase系统基础模块
|
||||
|
||||
用法: cd ~/repos/portal && py3/bin/python init_superuser_permissions.py
|
||||
"""
|
||||
import os, sys, subprocess
|
||||
|
||||
def find_app_root():
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
app_root = find_app_root()
|
||||
sage_root = None
|
||||
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/sage")]:
|
||||
if os.path.isdir(os.path.join(c, "py3", "bin")):
|
||||
sage_root = c
|
||||
break
|
||||
if not sage_root:
|
||||
sage_root = app_root
|
||||
|
||||
py = os.path.join(sage_root, "py3", "bin", "python")
|
||||
sp = os.path.join(sage_root, "set_role_perm.py") if os.path.exists(os.path.join(sage_root, "set_role_perm.py")) else None
|
||||
|
||||
if not sp:
|
||||
print("ERROR: 找不到set_role_perm.py")
|
||||
sys.exit(1)
|
||||
|
||||
def run(role, paths):
|
||||
env = os.environ.copy()
|
||||
env['SAGE_RBAC_DB'] = 'ocai_cms'
|
||||
for p in paths:
|
||||
print(f" {role:30s} {p}")
|
||||
subprocess.run([py, sp, role, p], cwd=sage_root, capture_output=True, env=env)
|
||||
|
||||
# ─── superuser — 所有权限 ───
|
||||
superuser_paths = [
|
||||
# 公开页面
|
||||
"/index.ui", "/news.ui", "/news_detail.ui",
|
||||
"/cases.ui", "/products.ui",
|
||||
"/cms_styles.css", "/cms_scripts.js",
|
||||
"/menu.ui", "/admin.ui",
|
||||
|
||||
# 公开API
|
||||
"/api/get_published_content.dspy",
|
||||
"/api/get_content_detail.dspy",
|
||||
"/api/get_config.dspy",
|
||||
"/api/get_sections.dspy",
|
||||
"/api/submit_lead.dspy",
|
||||
|
||||
# CMS管理 — 由cms模块提供,路由到 /cms/
|
||||
"/cms",
|
||||
"/cms/admin.ui", "/cms/menu.ui",
|
||||
|
||||
# CMS Content CRUD
|
||||
"/cms/cms_content_list", "/cms/cms_content_list/%",
|
||||
"/cms/api/cms_content_create.dspy",
|
||||
"/cms/api/cms_content_update.dspy",
|
||||
"/cms/api/cms_content_delete.dspy",
|
||||
"/cms/api/cms_content_list.dspy",
|
||||
"/cms/api/submit_content_approval.dspy",
|
||||
|
||||
# CMS Categories
|
||||
"/cms/cms_categories_list", "/cms/cms_categories_list/%",
|
||||
"/cms/api/cms_categories_create.dspy",
|
||||
"/cms/api/cms_categories_update.dspy",
|
||||
"/cms/api/cms_categories_delete.dspy",
|
||||
"/cms/api/cms_categories_list.dspy",
|
||||
"/cms/api/category_options.dspy",
|
||||
|
||||
# CMS Sections
|
||||
"/cms/cms_sections_list", "/cms/cms_sections_list/%",
|
||||
"/cms/api/cms_sections_create.dspy",
|
||||
"/cms/api/cms_sections_update.dspy",
|
||||
"/cms/api/cms_sections_delete.dspy",
|
||||
"/cms/api/cms_sections_list.dspy",
|
||||
|
||||
# CMS Site Config
|
||||
"/cms/cms_site_config_list", "/cms/cms_site_config_list/%",
|
||||
"/cms/api/cms_site_config_create.dspy",
|
||||
"/cms/api/cms_site_config_update.dspy",
|
||||
"/cms/api/cms_site_config_delete.dspy",
|
||||
"/cms/api/cms_site_config_list.dspy",
|
||||
|
||||
# CMS Leads
|
||||
"/cms/cms_leads_list", "/cms/cms_leads_list/%",
|
||||
"/cms/api/cms_leads_create.dspy",
|
||||
"/cms/api/cms_leads_update.dspy",
|
||||
"/cms/api/cms_leads_delete.dspy",
|
||||
"/cms/api/cms_leads_list.dspy",
|
||||
|
||||
# DingTalk Approvals (cms模块内)
|
||||
"/cms/api/submit_approval.dspy",
|
||||
"/cms/api/dingtalk_callback.dspy",
|
||||
"/cms/dd_approvals", "/cms/dd_approvals/%",
|
||||
"/cms/api/dd_approvals_create.dspy",
|
||||
"/cms/api/dd_approvals_update.dspy",
|
||||
"/cms/api/dd_approvals_delete.dspy",
|
||||
"/cms/api/dd_approvals_list.dspy",
|
||||
"/cms/dd_approval_configs", "/cms/dd_approval_configs/%",
|
||||
"/cms/api/dd_approval_configs_create.dspy",
|
||||
"/cms/api/dd_approval_configs_update.dspy",
|
||||
"/cms/api/dd_approval_configs_delete.dspy",
|
||||
"/cms/api/dd_approval_configs_list.dspy",
|
||||
|
||||
# appbase 系统基础模块
|
||||
"/appbase/appcodes_kv", "/appbase/appcodes_kv/%",
|
||||
"/appbase/appcodes", "/appbase/appcodes/%",
|
||||
"/appbase/params", "/appbase/params/%",
|
||||
"/appbase/svgicon", "/appbase/svgicon/%",
|
||||
"/appbase/cron/index.ui",
|
||||
|
||||
# rbac模块 (登录后管理页面)
|
||||
"/rbac",
|
||||
"/rbac/index.ui", "/rbac/admin_menu.ui", "/rbac/usermenu.ui",
|
||||
"/rbac/add_adminuser.dspy", "/rbac/add_adminuser.ui",
|
||||
"/rbac/add_provider.dspy", "/rbac/add_provider.ui",
|
||||
"/rbac/add_reseller.dspy", "/rbac/add_superuser.dspy",
|
||||
"/rbac/find_unauth_files.dspy",
|
||||
"/rbac/get_all_roles.dspy", "/rbac/get_normal_roles.dspy",
|
||||
"/rbac/get_provider.dspy", "/rbac/get_reseller.dspy",
|
||||
"/rbac/list_path_roles.dspy", "/rbac/list_path_roles.ui",
|
||||
"/rbac/organization", "/rbac/orgtypes",
|
||||
"/rbac/permission", "/rbac/provider", "/rbac/reseller",
|
||||
"/rbac/refresh_userperm.dspy",
|
||||
"/rbac/role", "/rbac/rolepermission",
|
||||
"/rbac/stat_active_users.ui", "/rbac/stat_total_orgs.ui", "/rbac/stat_total_users.ui",
|
||||
"/rbac/user", "/rbac/user/myrole.ui", "/rbac/user/user.ui", "/rbac/user/user_panel.ui",
|
||||
"/rbac/user/userapikey", "/rbac/user/userapikey/%",
|
||||
"/rbac/user/userinfo.ui", "/rbac/user/edit_profile.dspy", "/rbac/user/save_profile.dspy",
|
||||
"/rbac/user/wechat_login.ui",
|
||||
"/rbac/userapp", "/rbac/userdepartment", "/rbac/userrole",
|
||||
"/rbac/users", "/rbac/usersync", "/rbac/usersync/index.dspy",
|
||||
]
|
||||
|
||||
print("=== Portal RBAC权限初始化 — superuser ===")
|
||||
print(f"\n--- owner.superuser (超级管理员) ---")
|
||||
run("owner.superuser", superuser_paths)
|
||||
print("\n完成")
|
||||
18
start.sh
Executable file
18
start.sh
Executable file
@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
WORKDIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$WORKDIR"
|
||||
|
||||
if [ -f portal.pid ]; then
|
||||
pid=$(cat portal.pid)
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
echo "Already running (PID $pid)"
|
||||
exit 0
|
||||
fi
|
||||
rm -f portal.pid
|
||||
fi
|
||||
|
||||
echo "Starting portal on port 9090..."
|
||||
$WORKDIR/py3/bin/python $WORKDIR/app/portal.py -p 9090 -w $WORKDIR >> $WORKDIR/logs/portal.log 2>&1 &
|
||||
echo $! > portal.pid
|
||||
echo "Started PID $(cat portal.pid)"
|
||||
16
stop.sh
Executable file
16
stop.sh
Executable file
@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
WORKDIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$WORKDIR"
|
||||
|
||||
if [ -f portal.pid ]; then
|
||||
pid=$(cat portal.pid)
|
||||
if kill -0 "$pid" 2>/dev/null; then
|
||||
kill "$pid"
|
||||
echo "Stopped PID $pid"
|
||||
else
|
||||
echo "Process $pid not running"
|
||||
fi
|
||||
rm -f portal.pid
|
||||
else
|
||||
echo "No pid file found"
|
||||
fi
|
||||
322
wwwroot/admin.ui
Normal file
322
wwwroot/admin.ui
Normal file
@ -0,0 +1,322 @@
|
||||
{
|
||||
"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('/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('/cms_sections_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('/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('/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('/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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
18
wwwroot/api/get_config.dspy
Normal file
18
wwwroot/api/get_config.dspy
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('cms')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
|
||||
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', '')
|
||||
return {'status': 'ok', 'data': result}
|
||||
16
wwwroot/api/get_content_detail.dspy
Normal file
16
wwwroot/api/get_content_detail.dspy
Normal file
@ -0,0 +1,16 @@
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('cms')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
|
||||
_id = params_kw.get('id', '')
|
||||
if not _id:
|
||||
return {'status': 'error', 'message': '缺少ID'}
|
||||
else:
|
||||
ns = {'id': _id}
|
||||
rows = await sor.R('cms_content', ns)
|
||||
if rows:
|
||||
return {'status': 'ok', 'data': rows[0]}
|
||||
else:
|
||||
return {'status': 'error', 'message': '内容不存在'}
|
||||
15
wwwroot/api/get_published_content.dspy
Normal file
15
wwwroot/api/get_published_content.dspy
Normal file
@ -0,0 +1,15 @@
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('cms')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
|
||||
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]
|
||||
return {'status': 'ok', 'rows': rows, 'total': len(rows)}
|
||||
18
wwwroot/api/get_sections.dspy
Normal file
18
wwwroot/api/get_sections.dspy
Normal file
@ -0,0 +1,18 @@
|
||||
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('cms')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
|
||||
ns = {'is_visible': '1', 'sort': 'sort_order asc'}
|
||||
rows = await sor.R('cms_sections', ns)
|
||||
# Parse JSON fields
|
||||
for r in rows:
|
||||
for field in ['display_config', 'style_config', 'static_content']:
|
||||
v = r.get(field, None)
|
||||
if v and isinstance(v, str):
|
||||
try:
|
||||
r[field] = json.loads(v)
|
||||
except:
|
||||
pass
|
||||
return {'status': 'ok', 'rows': rows, 'total': len(rows)}
|
||||
22
wwwroot/api/submit_lead.dspy
Normal file
22
wwwroot/api/submit_lead.dspy
Normal file
@ -0,0 +1,22 @@
|
||||
config = getConfig('.')
|
||||
DBPools(config.databases)
|
||||
dbname = get_module_dbname('cms')
|
||||
async with db.sqlorContext(dbname) as sor:
|
||||
|
||||
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)
|
||||
return {
|
||||
'widgettype': 'Message',
|
||||
'options': {'text': '感谢您的留言,我们会尽快联系您!', 'messagetype': 'success'}
|
||||
}
|
||||
13
wwwroot/cases.ui
Normal file
13
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
wwwroot/cms_scripts.js
Normal file
153
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
wwwroot/cms_styles.css
Normal file
640
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
wwwroot/index.ui
Normal file
17
wwwroot/index.ui
Normal file
File diff suppressed because one or more lines are too long
44
wwwroot/menu.ui
Normal file
44
wwwroot/menu.ui
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"widgettype": "Menu",
|
||||
"id": "entcms_menu",
|
||||
"options": {
|
||||
"items": [
|
||||
{
|
||||
"name": "cms_content_list",
|
||||
"label": "内容管理",
|
||||
"url": "{{entire_url('/cms_content_list')}}",
|
||||
"target": "app.sage_main_content"
|
||||
},
|
||||
{
|
||||
"name": "cms_sections_list",
|
||||
"label": "栏目管理",
|
||||
"url": "{{entire_url('/cms_sections_list')}}",
|
||||
"target": "app.sage_main_content"
|
||||
},
|
||||
{
|
||||
"name": "cms_categories_list",
|
||||
"label": "内容分类",
|
||||
"url": "{{entire_url('/cms_categories_list')}}",
|
||||
"target": "app.sage_main_content"
|
||||
},
|
||||
{
|
||||
"name": "cms_leads_list",
|
||||
"label": "商机线索",
|
||||
"url": "{{entire_url('/cms_leads_list')}}",
|
||||
"target": "app.sage_main_content"
|
||||
},
|
||||
{
|
||||
"name": "cms_site_config_list",
|
||||
"label": "站点配置",
|
||||
"url": "{{entire_url('/cms_site_config_list')}}",
|
||||
"target": "app.sage_main_content"
|
||||
},
|
||||
{
|
||||
"name": "public_site",
|
||||
"label": "官网预览",
|
||||
"url": "{{entire_url('/index.ui')}}",
|
||||
"target": "app.sage_main_content"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
13
wwwroot/news.ui
Normal file
13
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
wwwroot/news_detail.ui
Normal file
13
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>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
13
wwwroot/products.ui
Normal file
13
wwwroot/products.ui
Normal file
@ -0,0 +1,13 @@
|
||||
{% set products = get_published_content('product', 50) %}
|
||||
{
|
||||
"widgettype": "VBox",
|
||||
"options": {"width": "100%", "css": "site-root"},
|
||||
"subwidgets": [
|
||||
{
|
||||
"widgettype": "Html",
|
||||
"options": {
|
||||
"html": "<nav class=\"nav-bar\"><a class=\"nav-logo\" href=\"{{entire_url('index.ui')}}\">开元云科技<\/a><ul class=\"nav-links\"><li><a href=\"{{entire_url('index.ui')}}#products\">产品架构<\/a><\/li><li><a href=\"{{entire_url('index.ui')}}#cases\">成功案例<\/a><\/li><li><a href=\"{{entire_url('news.ui')}}\">企业动态<\/a><\/li><\/ul><\/nav><section class=\"section\" style=\"padding-top:100px\"><h2 class=\"section-title\">产品架构<\/h2><p class=\"section-desc\">AI基础设施全栈解决方案<\/p><div class=\"cases-grid\">{% for p in products %}<div class=\"case-card\"><div class=\"case-tag\">{{p.tags or '核心产品'}}<\/div><div class=\"case-title\">{{p.title}}<\/div><div class=\"case-desc\">{{p.summary_text}}<\/div><\/div>{% endfor %}<\/div><div class=\"cta-banner\" style=\"margin-top:40px\"><div class=\"cta-text\">需要定制化方案?<\/div><a class=\"btn-primary\" href=\"{{entire_url('index.ui')}}#contact\">联系解决方案团队 →<\/a><\/div><\/section><footer class=\"site-footer\"><p>© 2026 开元云科技 · 国家级高新技术企业 · 专精特新企业<\/p><\/footer>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user