Compare commits

...

3 Commits

Author SHA1 Message Date
zhanglei 2132e07d2f 1 1 week ago
zhanglei fe8eca1480 python 脚手架 1 week ago
zhanglei 9af85065aa python 脚手架 1 week ago
  1. 1
      .gitignore
  2. 179
      README.md
  3. 88
      cvideo2.py
  4. 655
      python_scaffolding.py
  5. 1
      templates/py/__init__.py.j2
  6. 31
      templates/py/config.py.j2
  7. 33
      templates/py/database.py.j2
  8. 44
      templates/py/main.py.j2
  9. 26
      templates/py/model.py.j2
  10. 22
      templates/py/requirements.txt.j2
  11. 53
      templates/py/result.py.j2
  12. 120
      templates/py/router.py.j2
  13. 33
      templates/py/schema.py.j2
  14. 132
      templates/py/service.py.j2
  15. 9
      vue-vben-admin/LICENSE
  16. 531
      vue-vben-admin/apps/web-antd/mock/health_fun.ts
  17. 511
      vue-vben-admin/apps/web-antd/mock/health_msg.ts
  18. 69
      vue-vben-admin/apps/web-antd/src/api/health_device.ts
  19. 69
      vue-vben-admin/apps/web-antd/src/api/health_fun.ts
  20. 69
      vue-vben-admin/apps/web-antd/src/api/health_msg.ts
  21. 30
      vue-vben-admin/apps/web-antd/src/router/routes/modules/health_device.ts
  22. 30
      vue-vben-admin/apps/web-antd/src/router/routes/modules/health_fun.ts
  23. 30
      vue-vben-admin/apps/web-antd/src/router/routes/modules/health_msg.ts
  24. 253
      vue-vben-admin/apps/web-antd/src/views/health_fun/data.ts
  25. 276
      vue-vben-admin/apps/web-antd/src/views/health_fun/form.ts
  26. 691
      vue-vben-admin/apps/web-antd/src/views/health_fun/index.vue
  27. 242
      vue-vben-admin/apps/web-antd/src/views/health_msg/data.ts
  28. 264
      vue-vben-admin/apps/web-antd/src/views/health_msg/form.ts
  29. 691
      vue-vben-admin/apps/web-antd/src/views/health_msg/index.vue

1
.gitignore

@ -6,6 +6,7 @@ __pycache__/
# C extensions
*.so
.idea
# Distribution / packaging
.Python

179
README.md

@ -2,15 +2,16 @@
## 项目描述:
1. JAVA和VUE脚手架生成器,可以生成可执行的完整项目。
1. JAVA、Python和VUE脚手架生成器,可以生成可执行的完整项目。
2. 用于初始创建空项目也可以根据数据库的表生成可执行的完整项目 。
3. VUE脚手架以vue-vben-admin框架为基础的脚手架,根据数据库的表和JAVA脚手架生成列表界面。能自动适配枚举,适配图片并增加上传组件。
4. Python脚手架基于FastAPI + SQLAlchemy + Pydantic,复用JAVA脚手架的config.yml配置文件。
## 操作方式:
**注** 运行环境 Python `3.13.5` ;浏览器 `Chrome 80+` ; npm `11.6.1+`; node `v22.20.0+` ; java `8`/java `21`; maven `3.8.0`
~~~shell
```shell
# -------------------打包-------------------
Windows 运行命令:
python.exe -m pip install pyinstaller
@ -40,17 +41,37 @@
C:\Users\DELL\AppData\Local\Programs\Python\Python313\python.exe .\generator.py --model swagger # 不打包也可以直接运行
#----------vue脚手架---------------------------
C:\Users\DELL\AppData\Local\Programs\Python\Python313\python.exe .\frontend_vue.py health_fun
~~~
#------------Python脚手架执行操作----------------
python_scaffolding.py -h
options:
-h, --help #show this help message and exit
-c, --config #配置文件路径(默认: config.yml),复用JAVA脚手架配置
-o, --output #输出目录(默认: ./output)
-t, --tables #指定要生成的表名(默认: 全部)
--no-db #不连接数据库,使用示例数据
python python_scaffolding.py -c config.yml #连接数据库,生成全部表
python python_scaffolding.py -c config.yml -t user #生成指定表
python python_scaffolding.py -c config.yml --no-db #不连接数据库,使用示例数据
python python_scaffolding.py -c config.yml -o ./my_dir #指定输出目录
#------------生成的Python项目运行----------------
cd ./output/{mainModule}/{moduleName}
pip install -r requirements.txt
python main.py #启动FastAPI服务,默认 http://127.0.0.1:8000
# Swagger文档: http://127.0.0.1:8000/docs
```
**目前已集成的组件:**
基础组件 : java8(jdk corretto-1.8/21) + maven(3.8.0) + springboot + mybatis + mybatisplus
可选组件 : xxlJob + swagger + redis + mysql + minio + satoken
前端组件 : vue-vben-admin 框架
Python组件 : FastAPI + SQLAlchemy + Pydantic + Uvicorn
### 生成项目目录结构:
~~~shell
```shell
#java 脚手架目录
src
├── main
@ -93,10 +114,36 @@
│ └── api
│ └── ApplicationTests.java
└── pom.xml
~~~
```
```shell
#Python 脚手架目录 (FastAPI + SQLAlchemy + Pydantic)
{mainModule}/{moduleName}/
├── main.py # FastAPI应用入口
├── requirements.txt # 依赖包
├── app/
│ ├── __init__.py
│ ├── config.py # 应用配置
│ ├── database.py # 数据库配置
│ ├── models/ # SQLAlchemy数据模型层
│ │ ├── __init__.py
│ │ └── {table_name}.py # 每张表对应一个Model
│ ├── schemas/ # Pydantic数据验证层
│ │ ├── __init__.py
│ │ └── {table_name}.py # 每张表对应Schema
│ ├── services/ # 业务逻辑层
│ │ ├── __init__.py
│ │ └── {table_name}_service.py
│ ├── routers/ # API路由层(控制器)
│ │ ├── __init__.py
│ │ └── {table_name}.py # 每张表对应Router
│ └── utils/
│ └── __init__.py
└── tests/
└── __init__.py
```
~~~shell
```shell
vue-vben-admin/
├── apps/ # 应用目录(不同 UI 框架的成品)
│ ├── web-antd/ # Ant Design Vue 版本
@ -141,25 +188,115 @@ vue-vben-admin/
├── docs/ # 项目文档
└── scripts/ # 脚本工具
~~~
```
## Python API 接口调用方式
生成的Python项目使用FastAPI框架,所有接口统一使用 `api_version` Header替代 `version`(避免与业务字段冲突)。
```shell
# 启动服务后访问 Swagger 文档
http://127.0.0.1:8000/docs
# 分页查询 - POST /api/{table_name}/page
curl -X POST "http://127.0.0.1:8000/api/health_fun/page?page=1&page_size=10" \
-H "token: your_token" \
-H "api_version: 1.0"
# 详情查询 - POST /api/{table_name}/info?id=1
curl -X POST "http://127.0.0.1:8000/api/health_fun/info?id=1" \
-H "token: your_token" \
-H "api_version: 1.0"
# 新增 - POST /api/{table_name}/add
curl -X POST "http://127.0.0.1:8000/api/health_fun/add" \
-H "token: your_token" \
-H "Content-Type: application/json" \
-d '{"name": "test"}'
# 修改 - POST /api/{table_name}/modify
curl -X POST "http://127.0.0.1:8000/api/health_fun/modify" \
-H "token: your_token" \
-H "Content-Type: application/json" \
-d '{"id": 1, "name": "updated"}'
# 删除单个 - GET /api/{table_name}/remove/{id}
curl "http://127.0.0.1:8000/api/health_fun/remove/1" \
-H "token: your_token"
# 批量删除 - POST /api/{table_name}/removes
curl -X POST "http://127.0.0.1:8000/api/health_fun/removes" \
-H "token: your_token" \
-H "Content-Type: application/json" \
-d "[1, 2, 3]"
```
### Python API 返回 JSON 格式
```json
{
"code": 200,
"message": "success",
"data": {
"id": 1,
"name": "test",
"created_at": "2024-01-01T00:00:00",
"updated_at": "2024-01-01T00:00:00"
}
}
// 分页返回格式
{
"code": 200,
"message": "success",
"data": [],
"total": 100,
"page": 1,
"page_size": 10
}
```
## 脚手架目录结构:
~~~shell
```shell
codegen/
├── generator.py java主程序
├── frontend_vue.py vue主程序
├── config.yml 项目配置文件
├── templates/ 模板文件
├── ├── java/
├── generator.py # java主程序
├── frontend_vue.py # vue主程序
├── python_scaffolding.py # python主程序
├── config.yml # 项目配置文件(JAVA/Python共用)
├── templates/ # 模板文件
│ ├── java/
│ │ ├── entity.java.j2
│ │ ├── controller.java.j2
│ │ └── ...
├── └── vue/
│ ├── api.ts.j2
│ ├── data.ts.j2
│ └── ...
├── utils.py 公共 方法
├── db.py 数据库操作
└── requirements.txt 依赖包
~~~
│ ├── vue/
│ │ ├── api.ts.j2
│ │ ├── data.ts.j2
│ │ └── ...
│ └── py/
│ ├── main.py.j2
│ ├── model.py.j2
│ ├── schema.py.j2
│ ├── service.py.j2
│ ├── router.py.j2
│ ├── config.py.j2
│ ├── database.py.j2
│ └── requirements.txt.j2
├── utils.py # 公共方法
├── db.py # 数据库操作
└── requirements.txt # 依赖包
```
### api 返回 json 格式
```json
{
"success": true,
"message": "",
"code": 200,
"result": {
},
"timestamp": 1783590380098
}
```

88
cvideo2.py

@ -0,0 +1,88 @@
import cv2
import numpy as np
import sys
def convert_to_red_pseudo_color(input_path, output_path):
cap = cv2.VideoCapture(input_path)
if not cap.isOpened():
print("❌ 无法打开视频")
return
fps = cap.get(cv2.CAP_PROP_FPS)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
# 基础色 #f21762 (BGR 顺序)
#base_b, base_g, base_r = 98, 23, 242
# f80b5b
base_b, base_g, base_r = 91, 11, 248
# f31347
#base_b, base_g, base_r = 71, 19, 243
##b60430
#base_b, base_g, base_r = 48, 4, 182
# # 锐化核(锐度70)
# sharpen_kernel = np.array([[-0.7, -0.7, -0.7],
# [-0.7, 6.6, -0.7],
# [-0.7, -0.7, -0.7]], dtype=np.float32)
frame_count = 0
while True:
ret, frame = cap.read()
if not ret:
break
# 1. 灰度 + 明暗反转(亮变暗,暗变亮)
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
gray_inverted = 255 - gray
# 2. 归一化亮度 0~1
t = gray_inverted / 255.0
# 3. 关键:双向渐变 黑色 → #f21762 → 白色
# t < 0.5: 黑色到基础色的渐变
# t >= 0.5: 基础色到白色的渐变
pseudo = np.zeros_like(frame, dtype=np.float32)
# 4. 亮度提升100% + 对比度70
pseudo = cv2.convertScaleAbs(pseudo, alpha=2.0, beta=1000)
# 双向插值公式
mask_low = t < 0.5 # 暗区:黑 → 基础色
mask_high = t >= 0.5 # 亮区:基础色 → 白
# 暗区:从黑色(0)渐变到基础色
pseudo[mask_low, 0] = base_b * (t[mask_low] / 0.5)
pseudo[mask_low, 1] = base_g * (t[mask_low] / 0.5)
pseudo[mask_low, 2] = base_r * (t[mask_low] / 0.5)
# 亮区:从基础色渐变到白色(255)
t_high = (t[mask_high] - 0.5) / 0.5 # 重新映射到 0~1
pseudo[mask_high, 0] = base_b + t_high * (255 - base_b)
pseudo[mask_high, 1] = base_g + t_high * (255 - base_g)
pseudo[mask_high, 2] = base_r + t_high * (255 - base_r)
pseudo = np.clip(pseudo, 0, 255).astype(np.uint8)
# 5. 锐度70
# pseudo = cv2.filter2D(pseudo, -1, sharpen_kernel)
out.write(pseudo)
frame_count += 1
if frame_count % 100 == 0:
print(f"处理进度: {frame_count}/{total_frames}")
cap.release()
out.release()
print("🎉 完成 | #f80b5b 平滑双向渐变 + 反转 + 亮度/对比度/锐度")
if __name__ == "__main__":
if len(sys.argv) != 3:
print("用法:python 脚本.py 输入.mp4 输出.mp4")
else:
convert_to_red_pseudo_color(sys.argv[1], sys.argv[2])

655
python_scaffolding.py

@ -0,0 +1,655 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Python API脚手架生成器
基于FastAPI + SQLAlchemy + Pydantic的三层架构代码生成
复用Java脚手架的config.yml配置文件
@author: Auto Generated
@date: 2024
"""
import os
import sys
import yaml
from urllib.parse import quote_plus
import argparse
from datetime import datetime
from typing import List, Dict, Any, Optional
from jinja2 import Environment, FileSystemLoader
# 添加当前目录到路径
sys.path.insert(0, os.path.dirname(__file__))
# 字段类型映射 (MySQL -> Python/SQLAlchemy)
TYPE_MAPPING = {
"int": {"python": "int", "sqlalchemy": "Integer"},
"bigint": {"python": "int", "sqlalchemy": "BigInteger"},
"smallint": {"python": "int", "sqlalchemy": "SmallInteger"},
"tinyint": {"python": "int", "sqlalchemy": "SmallInteger"},
"float": {"python": "float", "sqlalchemy": "Float"},
"double": {"python": "float", "sqlalchemy": "Float"},
"decimal": {"python": "float", "sqlalchemy": "Numeric"},
"varchar": {"python": "str", "sqlalchemy": "String"},
"char": {"python": "str", "sqlalchemy": "String"},
"text": {"python": "str", "sqlalchemy": "Text"},
"longtext": {"python": "str", "sqlalchemy": "Text"},
"date": {"python": "datetime", "sqlalchemy": "Date"},
"datetime": {"python": "datetime", "sqlalchemy": "DateTime"},
"timestamp": {"python": "datetime", "sqlalchemy": "DateTime"},
"boolean": {"python": "bool", "sqlalchemy": "Boolean"},
"bool": {"python": "bool", "sqlalchemy": "Boolean"},
"json": {"python": "dict", "sqlalchemy": "JSON"},
}
# 需要排除的系统字段
EXCLUDE_FIELDS = [
"id",
"created_at",
"created_by",
"updated_at",
"updated_by",
"deleted_flag",
"is_deleted",
]
class DatabaseInspector:
"""数据库结构检查器"""
def __init__(self, host: str, port: int, user: str, password: str, database: str):
self.host = host
self.port = port
self.user = user
self.password = password
self.database = database
self.connection = None
def connect(self):
"""连接数据库"""
try:
import pymysql
self.connection = pymysql.connect(
host=self.host,
port=self.port,
user=self.user,
password=self.password,
database=self.database,
charset="utf8mb4",
)
print(f"成功连接到数据库: {self.host}:{self.port}/{self.database}")
return True
except ImportError:
print("错误: 请安装 pymysql: pip install pymysql")
return False
except Exception as e:
print(f"数据库连接失败: {e}")
return False
def get_tables(self, table_names: List[str] = None) -> List[Dict[str, Any]]:
"""获取表信息"""
if not self.connection:
return []
tables = []
cursor = self.connection.cursor()
try:
# 获取所有表
if table_names:
placeholders = ",".join(["%s"] * len(table_names))
cursor.execute(
f"""
SELECT TABLE_NAME, TABLE_COMMENT
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = %s AND TABLE_NAME IN ({placeholders})
""",
[self.database] + table_names,
)
else:
cursor.execute(
"""
SELECT TABLE_NAME, TABLE_COMMENT
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = %s
""",
[self.database],
)
for table_name, table_comment in cursor.fetchall():
fields = self._get_table_fields(table_name)
tables.append(
{
"name": table_name,
"comment": table_comment or table_name,
"fields": fields,
}
)
except Exception as e:
print(f"获取表信息失败: {e}")
finally:
cursor.close()
return tables
def _get_table_fields(self, table_name: str) -> List[Dict[str, Any]]:
"""获取表字段信息"""
cursor = self.connection.cursor()
fields = []
try:
cursor.execute(
"""
SELECT COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT, IS_NULLABLE, COLUMN_KEY
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s
ORDER BY ORDINAL_POSITION
""",
[self.database, table_name],
)
for (
col_name,
col_type,
col_comment,
is_nullable,
col_key,
) in cursor.fetchall():
# 排除系统字段
if col_name.lower() in EXCLUDE_FIELDS:
continue
# 解析类型
base_type = col_type.split("(")[0].lower()
type_info = TYPE_MAPPING.get(
base_type, {"python": "str", "sqlalchemy": "String"}
)
fields.append(
{
"name": col_name,
"type": col_type,
"comment": col_comment or col_name,
"nullable": is_nullable == "YES",
"is_primary": col_key == "PRI",
"searchable": col_key != "PRI", # 非主键默认可搜索
"python_type": type_info["python"],
"sqlalchemy_type": self._get_sqlalchemy_type(col_type),
}
)
except Exception as e:
print(f"获取字段信息失败 {table_name}: {e}")
finally:
cursor.close()
return fields
@staticmethod
def _get_sqlalchemy_type(db_type: str) -> str:
"""获取SQLAlchemy类型字符串"""
base_type = db_type.lower().split("(")[0]
if "(" in db_type and base_type in ["varchar", "char"]:
length = db_type.split("(")[1].rstrip(")")
return f"String({length})"
elif base_type in ["decimal", "numeric"]:
return "Numeric(precision=10, scale=2)"
else:
type_info = TYPE_MAPPING.get(
base_type, {"python": "str", "sqlalchemy": "String"}
)
return type_info["sqlalchemy"]
def close(self):
"""关闭连接"""
if self.connection:
self.connection.close()
class PythonScaffoldingGenerator:
"""Python API脚手架生成器"""
def __init__(self, template_dir: str = None):
"""初始化生成器"""
if template_dir is None:
template_dir = os.path.join(os.path.dirname(__file__), "templates", "py")
self.template_dir = template_dir
self.env = Environment(
loader=FileSystemLoader(template_dir), trim_blocks=True, lstrip_blocks=True
)
# 添加自定义过滤器
self.env.filters["snake_case"] = self._to_snake_case
self.env.filters["camel_case"] = self._to_camel_case
self.env.filters["pascal_case"] = self._to_pascal_case
@staticmethod
def _to_snake_case(name: str) -> str:
"""转换为snake_case"""
result = []
for i, char in enumerate(name):
if char.isupper() and i > 0:
result.append("_")
result.append(char.lower())
return "".join(result)
@staticmethod
def _to_camel_case(name: str) -> str:
"""转换为camelCase"""
parts = name.split("_")
return parts[0].lower() + "".join(p.capitalize() for p in parts[1:])
@staticmethod
def _to_pascal_case(name: str) -> str:
"""转换为PascalCase"""
return "".join(p.capitalize() for p in name.split("_"))
def _process_table(self, table: Dict[str, Any]) -> Dict[str, Any]:
"""处理表信息"""
name = table.get("name", "")
return {
**table,
"entity": self._to_pascal_case(name),
"name_lower": name.lower(),
"comment": table.get("comment", name),
}
def generate_project(
self,
project_name: str,
module_name: str,
tables: List[Dict[str, Any]],
output_dir: str,
author: str = "Auto Generated",
db_config: Dict[str, Any] = None,
**kwargs,
):
"""生成完整项目"""
print(f"开始生成Python API项目: {project_name}/{module_name}")
# 创建项目目录
project_dir = os.path.join(output_dir, project_name, module_name)
os.makedirs(project_dir, exist_ok=True)
# 创建子目录
dirs = [
"app",
"app/models",
"app/schemas",
"app/services",
"app/routers",
"app/utils",
"tests",
]
for d in dirs:
os.makedirs(os.path.join(project_dir, d), exist_ok=True)
# 处理表信息
processed_tables = [self._process_table(t) for t in tables]
# 构建数据库URL
if db_config:
db_host = db_config.get("host", "localhost")
db_port = db_config.get("port", 3306)
db_user = db_config.get("user", "root")
db_password = db_config.get("password", "")
db_database = db_config.get("database", "")
database_url = f"mysql+pymysql://{db_user}:{quote_plus(db_password)}@{db_host}:{db_port}/{db_database}?charset=utf8mb4"
else:
database_url = "sqlite:///./sql_app.db"
# 通用上下文
context = {
"project_name": project_name,
"module_name": module_name,
"author": author,
"date": datetime.now().strftime("%Y-%m-%d"),
"database_url": database_url,
"database_type": "mysql" if db_config else "sqlite",
"tables": processed_tables,
**kwargs,
}
# 生成主文件
self._render_template(
"main.py.j2", context, os.path.join(project_dir, "main.py")
)
self._render_template(
"config.py.j2", context, os.path.join(project_dir, "app", "config.py")
)
self._render_template(
"database.py.j2", context, os.path.join(project_dir, "app", "database.py")
)
self._render_template(
"requirements.txt.j2",
context,
os.path.join(project_dir, "requirements.txt"),
)
self._render_template(
"result.py.j2",
context,
os.path.join(project_dir, "app", "utils", "result.py"),
)
# 生成__init__.py文件
init_dirs = [
"app",
"app/models",
"app/schemas",
"app/services",
"app/routers",
"app/utils",
"tests",
]
for d in init_dirs:
self._render_template(
"__init__.py.j2",
{"package_name": d.replace("/", ".")},
os.path.join(project_dir, d, "__init__.py"),
)
# 为每个表生成代码
for table in processed_tables:
table_context = {
**context,
"table": table,
"fields": table.get("fields", []),
}
# 生成Model
self._render_template(
"model.py.j2",
table_context,
os.path.join(project_dir, "app", "models", f"{table['name_lower']}.py"),
)
# 生成Schema
self._render_template(
"schema.py.j2",
table_context,
os.path.join(
project_dir, "app", "schemas", f"{table['name_lower']}.py"
),
)
# 生成Service
self._render_template(
"service.py.j2",
table_context,
os.path.join(
project_dir, "app", "services", f"{table['name_lower']}_service.py"
),
)
# 生成Router
self._render_template(
"router.py.j2",
table_context,
os.path.join(
project_dir, "app", "routers", f"{table['name_lower']}.py"
),
)
print(f"项目生成完成: {project_dir}")
print(f"\n项目结构:")
self._print_tree(project_dir)
return project_dir
def _render_template(
self, template_name: str, context: Dict[str, Any], output_path: str
):
"""渲染模板并写入文件"""
try:
template = self.env.get_template(template_name)
content = template.render(**context)
with open(output_path, "w", encoding="utf-8") as f:
f.write(content)
print(
f" 生成: {os.path.relpath(output_path, os.path.dirname(output_path))}"
)
except Exception as e:
print(f" 生成失败 {template_name}: {str(e)}")
@staticmethod
def _print_tree(directory: str, prefix: str = ""):
"""打印目录树"""
entries = sorted(os.listdir(directory))
entries = [e for e in entries if not e.startswith("__pycache__")]
for i, entry in enumerate(entries):
path = os.path.join(directory, entry)
is_last = i == len(entries) - 1
connector = "└── " if is_last else "├── "
print(f"{prefix}{connector}{entry}")
if os.path.isdir(path):
extension = " " if is_last else ""
PythonScaffoldingGenerator._print_tree(path, prefix + extension)
def load_config(config_path: str) -> Dict[str, Any]:
"""加载YAML配置文件"""
with open(config_path, "r", encoding="utf-8") as f:
config = yaml.safe_load(f)
return config
def resolve_variables(config: Dict[str, Any]) -> Dict[str, Any]:
"""解析配置中的变量引用"""
config_str = yaml.dump(config)
# 简单的变量替换
replacements = {
"${mainModule}": config.get("mainModule", ""),
"${moduleName}": config.get("moduleName", ""),
"${package.Base}": config.get("package", {}).get("Base", ""),
"${package.Models}": config.get("package", {}).get("Models", ""),
}
for key, value in replacements.items():
config_str = config_str.replace(key, value)
return yaml.safe_load(config_str)
def main():
"""主函数"""
parser = argparse.ArgumentParser(
description="Python API脚手架生成器 - 复用Java配置"
)
parser.add_argument(
"-c", "--config", default="config.yml", help="配置文件路径(默认: config.yml)"
)
parser.add_argument(
"-o", "--output", default="./output", help="输出目录(默认: ./output)"
)
parser.add_argument(
"-t", "--tables", nargs="+", help="指定要生成的表名(默认: 全部)"
)
parser.add_argument(
"--no-db", action="store_true", help="不连接数据库,使用示例数据"
)
args = parser.parse_args()
# 检查配置文件
if not os.path.exists(args.config):
print(f"错误: 配置文件不存在: {args.config}")
return
# 加载配置
print(f"加载配置文件: {args.config}")
config = load_config(args.config)
config = resolve_variables(config)
# 获取基础信息
main_module = config.get("mainModule", "project")
module_name = config.get("moduleName", "api")
author = config.get("author", "Auto Generated")
db_config = config.get("db", {})
app_config = config.get("application", {})
print(f"项目: {main_module}/{module_name}")
print(f"作者: {author}")
# 获取表结构
tables = []
if not args.no_db and db_config:
print(f"\n连接数据库: {db_config.get('host')}:{db_config.get('port')}")
inspector = DatabaseInspector(
host=db_config.get("host", "localhost"),
port=db_config.get("port", 3306),
user=db_config.get("user", "root"),
password=db_config.get("password", ""),
database=db_config.get("database", ""),
)
if inspector.connect():
tables = inspector.get_tables(args.tables)
inspector.close()
print(f"获取到 {len(tables)} 个表")
else:
print("数据库连接失败,使用示例数据")
tables = _get_sample_tables()
else:
print("\n使用示例数据")
tables = _get_sample_tables()
if not tables:
print("没有要生成的表")
return
# 初始化生成器
generator = PythonScaffoldingGenerator()
# 生成项目
generator.generate_project(
project_name=main_module,
module_name=module_name,
tables=tables,
output_dir=args.output,
author=author,
db_config=db_config if not args.no_db else None,
redis_config=app_config.get("redis"),
minio_config=app_config.get("minio"),
)
print("\n生成完成!")
print(f"\n下一步:")
print(f" 1. cd {args.output}/{main_module}/{module_name}")
print(f" 2. pip install -r requirements.txt")
print(f" 3. python main.py")
def _get_sample_tables() -> List[Dict[str, Any]]:
"""获取示例表数据"""
return [
{
"name": "user",
"comment": "用户表",
"fields": [
{
"name": "username",
"type": "varchar(50)",
"comment": "用户名",
"nullable": False,
"searchable": True,
"python_type": "str",
"sqlalchemy_type": "String(50)",
},
{
"name": "password",
"type": "varchar(100)",
"comment": "密码",
"nullable": False,
"searchable": False,
"python_type": "str",
"sqlalchemy_type": "String(100)",
},
{
"name": "email",
"type": "varchar(100)",
"comment": "邮箱",
"nullable": True,
"searchable": True,
"python_type": "str",
"sqlalchemy_type": "String(100)",
},
{
"name": "phone",
"type": "varchar(20)",
"comment": "手机号",
"nullable": True,
"searchable": True,
"python_type": "str",
"sqlalchemy_type": "String(20)",
},
{
"name": "status",
"type": "int",
"comment": "状态",
"nullable": True,
"searchable": True,
"python_type": "int",
"sqlalchemy_type": "Integer",
},
],
},
{
"name": "product",
"comment": "商品表",
"fields": [
{
"name": "name",
"type": "varchar(100)",
"comment": "商品名称",
"nullable": False,
"searchable": True,
"python_type": "str",
"sqlalchemy_type": "String(100)",
},
{
"name": "description",
"type": "text",
"comment": "商品描述",
"nullable": True,
"searchable": False,
"python_type": "str",
"sqlalchemy_type": "Text",
},
{
"name": "price",
"type": "decimal(10,2)",
"comment": "价格",
"nullable": False,
"searchable": True,
"python_type": "float",
"sqlalchemy_type": "Numeric(precision=10, scale=2)",
},
{
"name": "stock",
"type": "int",
"comment": "库存",
"nullable": True,
"searchable": True,
"python_type": "int",
"sqlalchemy_type": "Integer",
},
{
"name": "category_id",
"type": "int",
"comment": "分类ID",
"nullable": True,
"searchable": True,
"python_type": "int",
"sqlalchemy_type": "Integer",
},
],
},
]
if __name__ == "__main__":
main()

1
templates/py/__init__.py.j2

@ -0,0 +1 @@
# {{ package_name }} package

31
templates/py/config.py.j2

@ -0,0 +1,31 @@
"""
Application Configuration
@author: {{ author }}
@date: {{ date }}
"""
import os
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
"""Application Settings"""
APP_NAME: str = "{{ project_name }}"
APP_VERSION: str = "1.0.0"
DEBUG: bool = True
# Database
DATABASE_URL: str = "{{ database_url | default('sqlite:///./sql_app.db') }}"
# JWT
SECRET_KEY: str = "{{ secret_key | default('your-secret-key-change-in-production') }}"
ALGORITHM: str = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
# CORS
ALLOWED_ORIGINS: list = ["*"]
class Config:
env_file = ".env"
case_sensitive = True
settings = Settings()

33
templates/py/database.py.j2

@ -0,0 +1,33 @@
"""
Database configuration
@author: {{ author }}
@date: {{ date }}
"""
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
# Database URL - configure according to your database
SQLALCHEMY_DATABASE_URL = "{{ database_url | default('sqlite:///./sql_app.db') }}"
engine = create_engine(
SQLALCHEMY_DATABASE_URL,
{% if database_url and 'sqlite' not in database_url %}
pool_pre_ping=True,
pool_size=10,
max_overflow=20
{% endif %}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
def get_db():
"""Dependency to get database session"""
db = SessionLocal()
try:
yield db
finally:
db.close()

44
templates/py/main.py.j2

@ -0,0 +1,44 @@
"""
{{ project_name }} - FastAPI Application
@author: {{ author }}
@date: {{ date }}
"""
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.database import engine, Base
{% for table in tables %}
from app.routers import {{ table.name_lower }}
{% endfor %}
# Create database tables
Base.metadata.create_all(bind=engine)
app = FastAPI(
title="{{ project_name }}",
description="{{ project_description }}",
version="1.0.0"
)
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include routers
{% for table in tables %}
app.include_router({{ table.name_lower }}.router, prefix="/api/{{ table.name_lower }}", tags=["{{ table.comment }}"])
{% endfor %}
@app.get("/")
async def root():
return {"message": "Welcome to {{ project_name }}"}
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)

26
templates/py/model.py.j2

@ -0,0 +1,26 @@
"""
{{ table.comment }} Model - 对齐JAVA Entity
@author: {{ author }}
@date: {{ date }}
"""
from sqlalchemy import Column, Integer, BigInteger, SmallInteger, String, DateTime, Boolean, Float, Text, Numeric, JSON, ForeignKey
from sqlalchemy.sql import func
from app.database import Base
class {{ table.entity }}Model(Base):
"""
{{ table.comment }} - 对齐JAVA Entity
"""
__tablename__ = "{{ table.name }}"
id = Column(Integer, primary_key=True, index=True, autoincrement=True, comment="主键ID")
{% for field in fields %}
{{ field.name }} = Column({{ field.sqlalchemy_type }}, {% if field.nullable %}nullable=True{% else %}nullable=False{% endif %}, comment="{{ field.comment }}")
{% endfor %}
created_at = Column(DateTime, server_default=func.now(), comment="创建时间")
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now(), comment="更新时间")
created_by = Column(String(50), comment="创建人")
updated_by = Column(String(50), comment="更新人")
is_deleted = Column(Boolean, default=False, comment="是否删除")

22
templates/py/requirements.txt.j2

@ -0,0 +1,22 @@
# {{ project_name }} Dependencies
# @author: {{ author }}
# @date: {{ date }}
fastapi==0.104.1
uvicorn[standard]==0.24.0
sqlalchemy==2.0.23
pydantic==2.5.2
python-multipart==0.0.6
{% if database_type == 'mysql' %}
pymysql==1.1.0
cryptography==41.0.7
{% elif database_type == 'postgresql' %}
psycopg2-binary==2.9.9
{% elif database_type == 'sqlite' %}
# SQLite is included in Python standard library
{% endif %}
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
httpx==0.25.2
pytest==7.4.3
pytest-asyncio==0.23.2

53
templates/py/result.py.j2

@ -0,0 +1,53 @@
"""
统一返回结果类 - 对齐JAVA Result
@author: {{ author }}
@date: {{ date }}
"""
from typing import TypeVar, Generic, Optional, Any
from pydantic import BaseModel, Field
from datetime import datetime
import time
T = TypeVar('T')
class Result(BaseModel, Generic[T]):
"""接口返回数据格式 - 对齐JAVA Result<T>"""
success: bool = Field(True, description="成功标志")
message: str = Field("", description="返回处理消息")
code: int = Field(0, description="返回代码, 0=成功, 500=错误")
result: Optional[T] = Field(None, description="返回数据对象")
timestamp: int = Field(default_factory=lambda: int(time.time() * 1000), description="时间戳")
@staticmethod
def OK(data: Any = None, message: str = "") -> dict:
"""成功返回"""
return {
"success": True,
"message": message,
"code": 200,
"result": data,
"timestamp": int(time.time() * 1000)
}
@staticmethod
def error(message: str, code: int = 500) -> dict:
"""错误返回"""
return {
"success": False,
"message": message,
"code": code,
"result": None,
"timestamp": int(time.time() * 1000)
}
@staticmethod
def noauth(message: str = "无权限访问") -> dict:
"""无权限返回"""
return {
"success": False,
"message": message,
"code": 210,
"result": None,
"timestamp": int(time.time() * 1000)
}

120
templates/py/router.py.j2

@ -0,0 +1,120 @@
"""
{{ table.comment }} Router - 对齐JAVA Controller
@author: {{ author }}
@date: {{ date }}
"""
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, Header
from sqlalchemy.orm import Session
from typing import List, Optional
from app.database import get_db
from app.schemas.{{ table.name_lower }} import {{ table.entity }}
from app.services.{{ table.name_lower }}_service import {{ table.entity }}Service
from app.utils.result import Result
router = APIRouter()
@router.post("/page", summary="{{ table.comment }}分页列表查询")
async def page(
param: {{ table.entity }},
token: str = Header(..., description="认证token"),
version: str = Header("1.0", description="版本号"),
db: Session = Depends(get_db)
):
"""{{ table.comment }}分页列表查询 - 对齐JAVA: @PostMapping("/page")"""
if not token:
return Result.error("token不能为空")
page_num = param.pageNum if param.pageNum else 1
page_size = param.pageSize if param.pageSize else 10
items, total = {{ table.entity }}Service.get_list(
db=db,
param=param,
page=page_num,
page_size=page_size
)
page_data = {
"records": [item.__dict__ for item in items],
"total": total,
"size": page_size,
"current": page_num,
"pages": (total + page_size - 1) // page_size
}
return Result.OK(page_data)
@router.post("/info", summary="{{ table.comment }}根据条件查询")
async def info(
param: {{ table.entity }},
token: str = Header(..., description="认证token"),
version: str = Header("1.0", description="版本号"),
db: Session = Depends(get_db)
):
"""{{ table.comment }}根据条件查询 - 对齐JAVA: @PostMapping("/info")"""
if not token:
return Result.error("token不能为空")
data = {{ table.entity }}Service.info(db=db, param=param)
return Result.OK(data)
@router.post("/add", summary="{{ table.comment }}新增")
async def add(
param: {{ table.entity }},
token: str = Header(..., description="认证token"),
version: str = Header("1.0", description="版本号"),
db: Session = Depends(get_db)
):
"""{{ table.comment }}新增 - 对齐JAVA: @PostMapping("/add")"""
if not token:
return Result.error("token不能为空")
{{ table.entity }}Service.add(db=db, param=param)
return Result.OK()
@router.post("/modify", summary="{{ table.comment }}修改")
async def modify(
param: {{ table.entity }},
token: str = Header(..., description="认证token"),
version: str = Header("1.0", description="版本号"),
db: Session = Depends(get_db)
):
"""{{ table.comment }}修改 - 对齐JAVA: @PostMapping("/modify")"""
if not token:
return Result.error("token不能为空")
info = {{ table.entity }}Service.get_by_id(db=db, id=param.id)
if info is None:
return Result.error(f"[{param.id}]记录不存在")
{{ table.entity }}Service.modify(db=db, param=param)
return Result.OK()
@router.get("/remove/{id}", summary="{{ table.comment }}删除(单个条目)")
async def remove(
id: int,
token: str = Header(..., description="认证token"),
version: str = Header("1.0", description="版本号"),
db: Session = Depends(get_db)
):
"""{{ table.comment }}删除(单个条目) - 对齐JAVA: @GetMapping("/remove/{id}")"""
{{ table.entity }}Service.remove(db=db, id=id)
return Result.OK()
@router.post("/removes", summary="{{ table.comment }}删除(多个条目)")
async def removes(
ids: List[int],
token: str = Header(..., description="认证token"),
version: str = Header("1.0", description="版本号"),
db: Session = Depends(get_db)
):
"""{{ table.comment }}删除(多个条目) - 对齐JAVA: @PostMapping("/removes")"""
{{ table.entity }}Service.removes(db=db, ids=ids)
return Result.OK()

33
templates/py/schema.py.j2

@ -0,0 +1,33 @@
"""
{{ table.comment }} Pydantic Schemas - 对齐JAVA Entity
@author: {{ author }}
@date: {{ date }}
"""
from pydantic import BaseModel, Field
from typing import Optional, List
from datetime import datetime
class {{ table.entity }}(BaseModel):
"""
{{ table.comment }} - 对齐JAVA Entity
所有接口共用同一个Schema作为入参
"""
id: Optional[int] = Field(None, description="主键ID")
{% for field in fields %}
{{ field.name }}: Optional[{{ field.python_type }}] = Field(None, description="{{ field.comment }}")
{% endfor %}
# 分页字段 - 对齐JAVA BaseEntity
pageNum: Optional[int] = Field(None, description="当前页码")
pageSize: Optional[int] = Field(None, description="每页数量")
# 审计字段
created_at: Optional[datetime] = Field(None, description="创建时间")
updated_at: Optional[datetime] = Field(None, description="更新时间")
created_by: Optional[str] = Field(None, description="创建人")
updated_by: Optional[str] = Field(None, description="更新人")
is_deleted: Optional[bool] = Field(False, description="是否删除")
# 用户信息
user_id: Optional[str] = Field(None, description="用户ID")
class Config:
from_attributes = True

132
templates/py/service.py.j2

@ -0,0 +1,132 @@
"""
{{ table.comment }} Service - 对齐JAVA Service
@author: {{ author }}
@date: {{ date }}
"""
from sqlalchemy.orm import Session
from sqlalchemy import and_
from typing import List, Optional, Tuple
from datetime import datetime
from app.models.{{ table.name_lower }} import {{ table.entity }}Model
from app.schemas.{{ table.name_lower }} import {{ table.entity }}
class {{ table.entity }}Service:
"""{{ table.comment }} Service Class - 对齐JAVA Service"""
@staticmethod
def get_by_id(db: Session, id: int) -> Optional[{{ table.entity }}Model]:
"""根据ID查询{{ table.comment }}"""
return db.query({{ table.entity }}Model).filter(
and_({{ table.entity }}Model.id == id, {{ table.entity }}Model.is_deleted == False)
).first()
@staticmethod
def info(db: Session, param: {{ table.entity }}) -> Optional[{{ table.entity }}Model]:
"""{{ table.comment }}详情 - 对齐JAVA: info({{ table.entity }} param)"""
query = db.query({{ table.entity }}Model).filter({{ table.entity }}Model.is_deleted == False)
if param.id is not None:
query = query.filter({{ table.entity }}Model.id == param.id)
{% for field in fields %}
{% if field.searchable %}
if param.{{ field.name }} is not None:
query = query.filter({{ table.entity }}Model.{{ field.name }} == param.{{ field.name }})
{% endif %}
{% endfor %}
return query.first()
@staticmethod
def get_list(
db: Session,
param: {{ table.entity }},
page: int = 1,
page_size: int = 10
) -> Tuple[List[{{ table.entity }}Model], int]:
"""{{ table.comment }}分页列表 - 对齐JAVA: page({{ table.entity }} param)"""
query = db.query({{ table.entity }}Model).filter({{ table.entity }}Model.is_deleted == False)
# 根据参数条件过滤 - 对齐JAVA MPJLambdaWrapper条件查询
{% for field in fields %}
{% if field.searchable %}
if param.{{ field.name }} is not None:
query = query.filter({{ table.entity }}Model.{{ field.name }} == param.{{ field.name }})
{% endif %}
{% endfor %}
total = query.count()
items = query.offset((page - 1) * page_size).limit(page_size).all()
return items, total
@staticmethod
def list(db: Session, param: {{ table.entity }}) -> List[{{ table.entity }}Model]:
"""{{ table.comment }}列表 - 对齐JAVA: list({{ table.entity }} param)"""
query = db.query({{ table.entity }}Model).filter({{ table.entity }}Model.is_deleted == False)
{% for field in fields %}
{% if field.searchable %}
if param.{{ field.name }} is not None:
query = query.filter({{ table.entity }}Model.{{ field.name }} == param.{{ field.name }})
{% endif %}
{% endfor %}
return query.all()
@staticmethod
def add(db: Session, param: {{ table.entity }}) -> None:
"""{{ table.comment }}新增 - 对齐JAVA: add({{ table.entity }} param)"""
db_obj = {{ table.entity }}Model(
{% for field in fields %}
{{ field.name }}=param.{{ field.name }},
{% endfor %}
created_by=param.user_id,
created_at=datetime.now()
)
db.add(db_obj)
db.commit()
db.refresh(db_obj)
@staticmethod
def modify(db: Session, param: {{ table.entity }}) -> None:
"""{{ table.comment }}修改 - 对齐JAVA: modify({{ table.entity }} param)"""
db_obj = db.query({{ table.entity }}Model).filter(
and_({{ table.entity }}Model.id == param.id, {{ table.entity }}Model.is_deleted == False)
).first()
if db_obj is None:
return
# 更新非空字段
{% for field in fields %}
if param.{{ field.name }} is not None:
db_obj.{{ field.name }} = param.{{ field.name }}
{% endfor %}
db_obj.updated_by = param.user_id
db_obj.updated_at = datetime.now()
db.commit()
@staticmethod
def remove(db: Session, id: int) -> None:
"""{{ table.comment }}删除(单个条目) - 对齐JAVA: remove(Integer id)"""
db_obj = db.query({{ table.entity }}Model).filter(
and_({{ table.entity }}Model.id == id, {{ table.entity }}Model.is_deleted == False)
).first()
if db_obj:
db_obj.is_deleted = True
db_obj.updated_at = datetime.now()
db.commit()
@staticmethod
def removes(db: Session, ids: List[int]) -> None:
"""{{ table.comment }}删除(多个条目) - 对齐JAVA: removes(List<Integer> ids)"""
db.query({{ table.entity }}Model).filter(
and_({{ table.entity }}Model.id.in_(ids), {{ table.entity }}Model.is_deleted == False)
).update({
{{ table.entity }}Model.is_deleted: True,
{{ table.entity }}Model.updated_at: datetime.now()
}, synchronize_session=False)
db.commit()

9
vue-vben-admin/LICENSE

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2024-present, Vben
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

531
vue-vben-admin/apps/web-antd/mock/health_fun.ts

@ -0,0 +1,531 @@
/**
* Mock
*/
import { MockMethod } from 'vite-plugin-mock';
const list = [
{
id: 'id_0',
funParentId: 'funParentId_0',
funName: 'funName_0',
funCode: 'funCode_0',
funValue: 'funValue_0',
funWarnLevel: 'funWarnLevel_0',
funWarnType: 'funWarnType_0',
funIndex: 'funIndex_0',
createdAt: 'createdAt_0',
createdBy: 'createdBy_0',
updatedAt: 'updatedAt_0',
updatedBy: 'updatedBy_0',
deletedFlag: 'deletedFlag_0',
userId: 'userId_0',
deviceCode: 'deviceCode_0',
funImg: 'funImg_0',
deviceType: 'deviceType_0',
funStatus: 'funStatus_0',
graduallyIntervalTime: 'graduallyIntervalTime_0',
funMsgTitle: 'funMsgTitle_0',
funStatisticsTimes: 'funStatisticsTimes_0',
funShortMsg: 'funShortMsg_0',
},
{
id: 'id_1',
funParentId: 'funParentId_1',
funName: 'funName_1',
funCode: 'funCode_1',
funValue: 'funValue_1',
funWarnLevel: 'funWarnLevel_1',
funWarnType: 'funWarnType_1',
funIndex: 'funIndex_1',
createdAt: 'createdAt_1',
createdBy: 'createdBy_1',
updatedAt: 'updatedAt_1',
updatedBy: 'updatedBy_1',
deletedFlag: 'deletedFlag_1',
userId: 'userId_1',
deviceCode: 'deviceCode_1',
funImg: 'funImg_1',
deviceType: 'deviceType_1',
funStatus: 'funStatus_1',
graduallyIntervalTime: 'graduallyIntervalTime_1',
funMsgTitle: 'funMsgTitle_1',
funStatisticsTimes: 'funStatisticsTimes_1',
funShortMsg: 'funShortMsg_1',
},
{
id: 'id_2',
funParentId: 'funParentId_2',
funName: 'funName_2',
funCode: 'funCode_2',
funValue: 'funValue_2',
funWarnLevel: 'funWarnLevel_2',
funWarnType: 'funWarnType_2',
funIndex: 'funIndex_2',
createdAt: 'createdAt_2',
createdBy: 'createdBy_2',
updatedAt: 'updatedAt_2',
updatedBy: 'updatedBy_2',
deletedFlag: 'deletedFlag_2',
userId: 'userId_2',
deviceCode: 'deviceCode_2',
funImg: 'funImg_2',
deviceType: 'deviceType_2',
funStatus: 'funStatus_2',
graduallyIntervalTime: 'graduallyIntervalTime_2',
funMsgTitle: 'funMsgTitle_2',
funStatisticsTimes: 'funStatisticsTimes_2',
funShortMsg: 'funShortMsg_2',
},
{
id: 'id_3',
funParentId: 'funParentId_3',
funName: 'funName_3',
funCode: 'funCode_3',
funValue: 'funValue_3',
funWarnLevel: 'funWarnLevel_3',
funWarnType: 'funWarnType_3',
funIndex: 'funIndex_3',
createdAt: 'createdAt_3',
createdBy: 'createdBy_3',
updatedAt: 'updatedAt_3',
updatedBy: 'updatedBy_3',
deletedFlag: 'deletedFlag_3',
userId: 'userId_3',
deviceCode: 'deviceCode_3',
funImg: 'funImg_3',
deviceType: 'deviceType_3',
funStatus: 'funStatus_3',
graduallyIntervalTime: 'graduallyIntervalTime_3',
funMsgTitle: 'funMsgTitle_3',
funStatisticsTimes: 'funStatisticsTimes_3',
funShortMsg: 'funShortMsg_3',
},
{
id: 'id_4',
funParentId: 'funParentId_4',
funName: 'funName_4',
funCode: 'funCode_4',
funValue: 'funValue_4',
funWarnLevel: 'funWarnLevel_4',
funWarnType: 'funWarnType_4',
funIndex: 'funIndex_4',
createdAt: 'createdAt_4',
createdBy: 'createdBy_4',
updatedAt: 'updatedAt_4',
updatedBy: 'updatedBy_4',
deletedFlag: 'deletedFlag_4',
userId: 'userId_4',
deviceCode: 'deviceCode_4',
funImg: 'funImg_4',
deviceType: 'deviceType_4',
funStatus: 'funStatus_4',
graduallyIntervalTime: 'graduallyIntervalTime_4',
funMsgTitle: 'funMsgTitle_4',
funStatisticsTimes: 'funStatisticsTimes_4',
funShortMsg: 'funShortMsg_4',
},
{
id: 'id_5',
funParentId: 'funParentId_5',
funName: 'funName_5',
funCode: 'funCode_5',
funValue: 'funValue_5',
funWarnLevel: 'funWarnLevel_5',
funWarnType: 'funWarnType_5',
funIndex: 'funIndex_5',
createdAt: 'createdAt_5',
createdBy: 'createdBy_5',
updatedAt: 'updatedAt_5',
updatedBy: 'updatedBy_5',
deletedFlag: 'deletedFlag_5',
userId: 'userId_5',
deviceCode: 'deviceCode_5',
funImg: 'funImg_5',
deviceType: 'deviceType_5',
funStatus: 'funStatus_5',
graduallyIntervalTime: 'graduallyIntervalTime_5',
funMsgTitle: 'funMsgTitle_5',
funStatisticsTimes: 'funStatisticsTimes_5',
funShortMsg: 'funShortMsg_5',
},
{
id: 'id_6',
funParentId: 'funParentId_6',
funName: 'funName_6',
funCode: 'funCode_6',
funValue: 'funValue_6',
funWarnLevel: 'funWarnLevel_6',
funWarnType: 'funWarnType_6',
funIndex: 'funIndex_6',
createdAt: 'createdAt_6',
createdBy: 'createdBy_6',
updatedAt: 'updatedAt_6',
updatedBy: 'updatedBy_6',
deletedFlag: 'deletedFlag_6',
userId: 'userId_6',
deviceCode: 'deviceCode_6',
funImg: 'funImg_6',
deviceType: 'deviceType_6',
funStatus: 'funStatus_6',
graduallyIntervalTime: 'graduallyIntervalTime_6',
funMsgTitle: 'funMsgTitle_6',
funStatisticsTimes: 'funStatisticsTimes_6',
funShortMsg: 'funShortMsg_6',
},
{
id: 'id_7',
funParentId: 'funParentId_7',
funName: 'funName_7',
funCode: 'funCode_7',
funValue: 'funValue_7',
funWarnLevel: 'funWarnLevel_7',
funWarnType: 'funWarnType_7',
funIndex: 'funIndex_7',
createdAt: 'createdAt_7',
createdBy: 'createdBy_7',
updatedAt: 'updatedAt_7',
updatedBy: 'updatedBy_7',
deletedFlag: 'deletedFlag_7',
userId: 'userId_7',
deviceCode: 'deviceCode_7',
funImg: 'funImg_7',
deviceType: 'deviceType_7',
funStatus: 'funStatus_7',
graduallyIntervalTime: 'graduallyIntervalTime_7',
funMsgTitle: 'funMsgTitle_7',
funStatisticsTimes: 'funStatisticsTimes_7',
funShortMsg: 'funShortMsg_7',
},
{
id: 'id_8',
funParentId: 'funParentId_8',
funName: 'funName_8',
funCode: 'funCode_8',
funValue: 'funValue_8',
funWarnLevel: 'funWarnLevel_8',
funWarnType: 'funWarnType_8',
funIndex: 'funIndex_8',
createdAt: 'createdAt_8',
createdBy: 'createdBy_8',
updatedAt: 'updatedAt_8',
updatedBy: 'updatedBy_8',
deletedFlag: 'deletedFlag_8',
userId: 'userId_8',
deviceCode: 'deviceCode_8',
funImg: 'funImg_8',
deviceType: 'deviceType_8',
funStatus: 'funStatus_8',
graduallyIntervalTime: 'graduallyIntervalTime_8',
funMsgTitle: 'funMsgTitle_8',
funStatisticsTimes: 'funStatisticsTimes_8',
funShortMsg: 'funShortMsg_8',
},
{
id: 'id_9',
funParentId: 'funParentId_9',
funName: 'funName_9',
funCode: 'funCode_9',
funValue: 'funValue_9',
funWarnLevel: 'funWarnLevel_9',
funWarnType: 'funWarnType_9',
funIndex: 'funIndex_9',
createdAt: 'createdAt_9',
createdBy: 'createdBy_9',
updatedAt: 'updatedAt_9',
updatedBy: 'updatedBy_9',
deletedFlag: 'deletedFlag_9',
userId: 'userId_9',
deviceCode: 'deviceCode_9',
funImg: 'funImg_9',
deviceType: 'deviceType_9',
funStatus: 'funStatus_9',
graduallyIntervalTime: 'graduallyIntervalTime_9',
funMsgTitle: 'funMsgTitle_9',
funStatisticsTimes: 'funStatisticsTimes_9',
funShortMsg: 'funShortMsg_9',
},
];
export default [
{
url: '/api/health_fun/page',
method: 'get',
response: () => {
return {
code: 0,
data: {
records: list,
total: list.length,
},
};
},
},
{
url: '/api/health_fun',
method: 'post',
response: () => {
return {
code: 0,
message: 'success',
};
},
},
{
url: '/api/health_fun/:id',
method: 'delete',
response: () => {
return {
code: 0,
message: 'deleted',
};
},
},
] as MockMethod[];

511
vue-vben-admin/apps/web-antd/mock/health_msg.ts

@ -0,0 +1,511 @@
/**
* Mock
*/
import { MockMethod } from 'vite-plugin-mock';
const list = [
{
id: 'id_0',
msgNote: 'msgNote_0',
msgType: 'msgType_0',
sceneId: 'sceneId_0',
userId: 'userId_0',
url: 'url_0',
messageSendStatus: 'messageSendStatus_0',
createdAt: 'createdAt_0',
createdBy: 'createdBy_0',
updatedAt: 'updatedAt_0',
updatedBy: 'updatedBy_0',
deletedFlag: 'deletedFlag_0',
deviceId: 'deviceId_0',
roomId: 'roomId_0',
msgRead: 'msgRead_0',
funId: 'funId_0',
funParentId: 'funParentId_0',
funWarnLevel: 'funWarnLevel_0',
familyId: 'familyId_0',
statisticsDuration: 'statisticsDuration_0',
msgSourceId: 'msgSourceId_0',
},
{
id: 'id_1',
msgNote: 'msgNote_1',
msgType: 'msgType_1',
sceneId: 'sceneId_1',
userId: 'userId_1',
url: 'url_1',
messageSendStatus: 'messageSendStatus_1',
createdAt: 'createdAt_1',
createdBy: 'createdBy_1',
updatedAt: 'updatedAt_1',
updatedBy: 'updatedBy_1',
deletedFlag: 'deletedFlag_1',
deviceId: 'deviceId_1',
roomId: 'roomId_1',
msgRead: 'msgRead_1',
funId: 'funId_1',
funParentId: 'funParentId_1',
funWarnLevel: 'funWarnLevel_1',
familyId: 'familyId_1',
statisticsDuration: 'statisticsDuration_1',
msgSourceId: 'msgSourceId_1',
},
{
id: 'id_2',
msgNote: 'msgNote_2',
msgType: 'msgType_2',
sceneId: 'sceneId_2',
userId: 'userId_2',
url: 'url_2',
messageSendStatus: 'messageSendStatus_2',
createdAt: 'createdAt_2',
createdBy: 'createdBy_2',
updatedAt: 'updatedAt_2',
updatedBy: 'updatedBy_2',
deletedFlag: 'deletedFlag_2',
deviceId: 'deviceId_2',
roomId: 'roomId_2',
msgRead: 'msgRead_2',
funId: 'funId_2',
funParentId: 'funParentId_2',
funWarnLevel: 'funWarnLevel_2',
familyId: 'familyId_2',
statisticsDuration: 'statisticsDuration_2',
msgSourceId: 'msgSourceId_2',
},
{
id: 'id_3',
msgNote: 'msgNote_3',
msgType: 'msgType_3',
sceneId: 'sceneId_3',
userId: 'userId_3',
url: 'url_3',
messageSendStatus: 'messageSendStatus_3',
createdAt: 'createdAt_3',
createdBy: 'createdBy_3',
updatedAt: 'updatedAt_3',
updatedBy: 'updatedBy_3',
deletedFlag: 'deletedFlag_3',
deviceId: 'deviceId_3',
roomId: 'roomId_3',
msgRead: 'msgRead_3',
funId: 'funId_3',
funParentId: 'funParentId_3',
funWarnLevel: 'funWarnLevel_3',
familyId: 'familyId_3',
statisticsDuration: 'statisticsDuration_3',
msgSourceId: 'msgSourceId_3',
},
{
id: 'id_4',
msgNote: 'msgNote_4',
msgType: 'msgType_4',
sceneId: 'sceneId_4',
userId: 'userId_4',
url: 'url_4',
messageSendStatus: 'messageSendStatus_4',
createdAt: 'createdAt_4',
createdBy: 'createdBy_4',
updatedAt: 'updatedAt_4',
updatedBy: 'updatedBy_4',
deletedFlag: 'deletedFlag_4',
deviceId: 'deviceId_4',
roomId: 'roomId_4',
msgRead: 'msgRead_4',
funId: 'funId_4',
funParentId: 'funParentId_4',
funWarnLevel: 'funWarnLevel_4',
familyId: 'familyId_4',
statisticsDuration: 'statisticsDuration_4',
msgSourceId: 'msgSourceId_4',
},
{
id: 'id_5',
msgNote: 'msgNote_5',
msgType: 'msgType_5',
sceneId: 'sceneId_5',
userId: 'userId_5',
url: 'url_5',
messageSendStatus: 'messageSendStatus_5',
createdAt: 'createdAt_5',
createdBy: 'createdBy_5',
updatedAt: 'updatedAt_5',
updatedBy: 'updatedBy_5',
deletedFlag: 'deletedFlag_5',
deviceId: 'deviceId_5',
roomId: 'roomId_5',
msgRead: 'msgRead_5',
funId: 'funId_5',
funParentId: 'funParentId_5',
funWarnLevel: 'funWarnLevel_5',
familyId: 'familyId_5',
statisticsDuration: 'statisticsDuration_5',
msgSourceId: 'msgSourceId_5',
},
{
id: 'id_6',
msgNote: 'msgNote_6',
msgType: 'msgType_6',
sceneId: 'sceneId_6',
userId: 'userId_6',
url: 'url_6',
messageSendStatus: 'messageSendStatus_6',
createdAt: 'createdAt_6',
createdBy: 'createdBy_6',
updatedAt: 'updatedAt_6',
updatedBy: 'updatedBy_6',
deletedFlag: 'deletedFlag_6',
deviceId: 'deviceId_6',
roomId: 'roomId_6',
msgRead: 'msgRead_6',
funId: 'funId_6',
funParentId: 'funParentId_6',
funWarnLevel: 'funWarnLevel_6',
familyId: 'familyId_6',
statisticsDuration: 'statisticsDuration_6',
msgSourceId: 'msgSourceId_6',
},
{
id: 'id_7',
msgNote: 'msgNote_7',
msgType: 'msgType_7',
sceneId: 'sceneId_7',
userId: 'userId_7',
url: 'url_7',
messageSendStatus: 'messageSendStatus_7',
createdAt: 'createdAt_7',
createdBy: 'createdBy_7',
updatedAt: 'updatedAt_7',
updatedBy: 'updatedBy_7',
deletedFlag: 'deletedFlag_7',
deviceId: 'deviceId_7',
roomId: 'roomId_7',
msgRead: 'msgRead_7',
funId: 'funId_7',
funParentId: 'funParentId_7',
funWarnLevel: 'funWarnLevel_7',
familyId: 'familyId_7',
statisticsDuration: 'statisticsDuration_7',
msgSourceId: 'msgSourceId_7',
},
{
id: 'id_8',
msgNote: 'msgNote_8',
msgType: 'msgType_8',
sceneId: 'sceneId_8',
userId: 'userId_8',
url: 'url_8',
messageSendStatus: 'messageSendStatus_8',
createdAt: 'createdAt_8',
createdBy: 'createdBy_8',
updatedAt: 'updatedAt_8',
updatedBy: 'updatedBy_8',
deletedFlag: 'deletedFlag_8',
deviceId: 'deviceId_8',
roomId: 'roomId_8',
msgRead: 'msgRead_8',
funId: 'funId_8',
funParentId: 'funParentId_8',
funWarnLevel: 'funWarnLevel_8',
familyId: 'familyId_8',
statisticsDuration: 'statisticsDuration_8',
msgSourceId: 'msgSourceId_8',
},
{
id: 'id_9',
msgNote: 'msgNote_9',
msgType: 'msgType_9',
sceneId: 'sceneId_9',
userId: 'userId_9',
url: 'url_9',
messageSendStatus: 'messageSendStatus_9',
createdAt: 'createdAt_9',
createdBy: 'createdBy_9',
updatedAt: 'updatedAt_9',
updatedBy: 'updatedBy_9',
deletedFlag: 'deletedFlag_9',
deviceId: 'deviceId_9',
roomId: 'roomId_9',
msgRead: 'msgRead_9',
funId: 'funId_9',
funParentId: 'funParentId_9',
funWarnLevel: 'funWarnLevel_9',
familyId: 'familyId_9',
statisticsDuration: 'statisticsDuration_9',
msgSourceId: 'msgSourceId_9',
},
];
export default [
{
url: '/api/health_msg/page',
method: 'get',
response: () => {
return {
code: 0,
data: {
records: list,
total: list.length,
},
};
},
},
{
url: '/api/health_msg',
method: 'post',
response: () => {
return {
code: 0,
message: 'success',
};
},
},
{
url: '/api/health_msg/:id',
method: 'delete',
response: () => {
return {
code: 0,
message: 'deleted',
};
},
},
] as MockMethod[];

69
vue-vben-admin/apps/web-antd/src/api/health_device.ts

@ -0,0 +1,69 @@
/**
* API
*
*/
import { requestClient } from '#/api/request';
import { useAppConfig } from '@vben/hooks';
import { useAccessStore } from '@vben/stores';
export namespace health_deviceApi {
const applicationConfig = useAppConfig(import.meta.env, import.meta.env.PROD);
console.log('=== 接口域名 ===', applicationConfig.javaURL)
/**
*
*/
export function page(params: any) {
return requestClient.post(applicationConfig.javaURL+'/health-device/page', params,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
*
*/
export function get(id: number) {
return requestClient.get(applicationConfig.javaURL+'/health-device/' + id);
}
/**
*
*/
export function add(data: any) {
return requestClient.post(applicationConfig.javaURL+'/health-device/add', data,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
*
*/
export function save(data: any) {
return requestClient.post(applicationConfig.javaURL+'/health-device/modify', data,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
*
*/
export function remove(id: number) {
return requestClient.delete(applicationConfig.javaURL+'/health-device/' + id);
}
/**
*
*/
export function enumList(params: any) {
return requestClient.post(applicationConfig.javaURL+'/health-enums/optionList', params,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
*
*/
export function upload(params: any) {
return requestClient.post(applicationConfig.javaURL+'/file/up', params,
{ headers: {'Content-Type': 'multipart/form-data', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
}

69
vue-vben-admin/apps/web-antd/src/api/health_fun.ts

@ -0,0 +1,69 @@
/**
* API
*
*/
import { requestClient } from '#/api/request';
import { useAppConfig } from '@vben/hooks';
import { useAccessStore } from '@vben/stores';
export namespace health_funApi {
const applicationConfig = useAppConfig(import.meta.env, import.meta.env.PROD);
console.log('=== 接口域名 ===', applicationConfig.javaURL)
/**
*
*/
export function page(params: any) {
return requestClient.post(applicationConfig.javaURL+'/health-fun/page', params,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
*
*/
export function get(id: number) {
return requestClient.get(applicationConfig.javaURL+'/health-fun/' + id);
}
/**
*
*/
export function add(data: any) {
return requestClient.post(applicationConfig.javaURL+'/health-fun/add', data,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
*
*/
export function save(data: any) {
return requestClient.post(applicationConfig.javaURL+'/health-fun/modify', data,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
*
*/
export function remove(id: number) {
return requestClient.delete(applicationConfig.javaURL+'/health-fun/' + id);
}
/**
*
*/
export function enumList(params: any) {
return requestClient.post(applicationConfig.javaURL+'/health-enums/optionList', params,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
*
*/
export function upload(params: any) {
return requestClient.post(applicationConfig.javaURL+'/file/up', params,
{ headers: {'Content-Type': 'multipart/form-data', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
}

69
vue-vben-admin/apps/web-antd/src/api/health_msg.ts

@ -0,0 +1,69 @@
/**
* API
*
*/
import { requestClient } from '#/api/request';
import { useAppConfig } from '@vben/hooks';
import { useAccessStore } from '@vben/stores';
export namespace health_msgApi {
const applicationConfig = useAppConfig(import.meta.env, import.meta.env.PROD);
console.log('=== 接口域名 ===', applicationConfig.javaURL)
/**
*
*/
export function page(params: any) {
return requestClient.post(applicationConfig.javaURL+'/health-msg/page', params,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
*
*/
export function get(id: number) {
return requestClient.get(applicationConfig.javaURL+'/health-msg/' + id);
}
/**
*
*/
export function add(data: any) {
return requestClient.post(applicationConfig.javaURL+'/health-msg/add', data,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
*
*/
export function save(data: any) {
return requestClient.post(applicationConfig.javaURL+'/health-msg/modify', data,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
*
*/
export function remove(id: number) {
return requestClient.delete(applicationConfig.javaURL+'/health-msg/' + id);
}
/**
*
*/
export function enumList(params: any) {
return requestClient.post(applicationConfig.javaURL+'/health-enums/optionList', params,
{ headers: {'Content-Type': 'application/json', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
/**
*
*/
export function upload(params: any) {
return requestClient.post(applicationConfig.javaURL+'/file/up', params,
{ headers: {'Content-Type': 'multipart/form-data', Token: useAccessStore().accessToken, version: '1.0.1'}});
}
}

30
vue-vben-admin/apps/web-antd/src/router/routes/modules/health_device.ts

@ -0,0 +1,30 @@
/**
*
*/
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/health_device',
name: '设备表模块',
meta: {
icon: 'ic:baseline-view-list',
order: 1000,
keepAlive: true,
title: '设备表',
},
children: [
{
meta: {
title: "设备表列表",
icon: 'ic:baseline-menu',
},
name: 'health_deviceList',
path: '/health_device',
component: () => import('#/views/health_device/index.vue'),
},
],
}
];

30
vue-vben-admin/apps/web-antd/src/router/routes/modules/health_fun.ts

@ -0,0 +1,30 @@
/**
*
*/
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/health_fun',
name: '功能配置表模块',
meta: {
icon: 'ic:baseline-view-list',
order: 1000,
keepAlive: true,
title: '功能配置表',
},
children: [
{
meta: {
title: "功能配置表列表",
icon: 'ic:baseline-menu',
},
name: 'health_funList',
path: '/health_fun',
component: () => import('#/views/health_fun/index.vue'),
},
],
}
];

30
vue-vben-admin/apps/web-antd/src/router/routes/modules/health_msg.ts

@ -0,0 +1,30 @@
/**
*
*/
import type { RouteRecordRaw } from 'vue-router';
const routes: RouteRecordRaw[] = [
{
path: '/health_msg',
name: '推送消息表(每个人场景维度保存,这里的UID是送达的用户)模块',
meta: {
icon: 'ic:baseline-view-list',
order: 1000,
keepAlive: true,
title: '推送消息表(每个人场景维度保存,这里的UID是送达的用户)',
},
children: [
{
meta: {
title: "推送消息表(每个人场景维度保存,这里的UID是送达的用户)列表",
icon: 'ic:baseline-menu',
},
name: 'health_msgList',
path: '/health_msg',
component: () => import('#/views/health_msg/index.vue'),
},
],
}
];

253
vue-vben-admin/apps/web-antd/src/views/health_fun/data.ts

@ -0,0 +1,253 @@
/**
*
*/
import type { VxeGridProps } from '#/adapter/vxe-table';
export const columns: VxeGridProps['columns'] = [
{
// 列标题
title: '主键ID',
// 对应字段
field: 'id',
// 宽度
width: 150
},
{
// 列标题
title: '父类ID',
// 对应字段
field: 'funParentId',
// 宽度
width: 150
},
{
// 列标题
title: '功能名称',
// 对应字段
field: 'funName',
// 宽度
width: 150
},
{
// 列标题
title: '功能码:功能码对外做映射用',
// 对应字段
field: 'funCode',
// 宽度
width: 150
},
{
// 列标题
title: '功能数值',
// 对应字段
field: 'funValue',
// 宽度
width: 150
},
{
// 列标题
title: '告警级别: 0默认,1级,2级,3级',
// 对应字段
field: 'funWarnLevel',
// 宽度
width: 150
},
{
// 列标题
title: '用位运算组合',
// 对应字段
field: 'funWarnType',
// 宽度
width: 150
},
{
// 列标题
title: '首页展示: 0展示,1不展示',
// 对应字段
field: 'funIndex',
// 宽度
width: 150
},
{
// 列标题
title: '创建时间',
// 对应字段
field: 'createdAt',
// 宽度
width: 150
},
{
// 列标题
title: '创建人',
// 对应字段
field: 'createdBy',
// 宽度
width: 150
},
{
// 列标题
title: '更新时间',
// 对应字段
field: 'updatedAt',
// 宽度
width: 150
},
{
// 列标题
title: '更新人',
// 对应字段
field: 'updatedBy',
// 宽度
width: 150
},
{
// 列标题
title: '删除: 0 默认 1删除',
// 对应字段
field: 'deletedFlag',
// 宽度
width: 150
},
{
// 列标题
title: '用户ID',
// 对应字段
field: 'userId',
// 宽度
width: 150
},
{
// 列标题
title: '设备唯一标识',
// 对应字段
field: 'deviceCode',
// 宽度
width: 150
},
{
// 列标题
title: '功能图',
// 对应字段
field: 'funImg',
// 宽度
width: 150
},
{
// 列标题
title: '设备类型',
// 对应字段
field: 'deviceType',
// 宽度
width: 150
},
{
// 列标题
title: '功能状态: 默认 0开通,1关闭,2隐藏',
// 对应字段
field: 'funStatus',
// 宽度
width: 150
},
{
// 列标题
title: '逐级告警:完成10,30,60逐级提升告警持续',
// 对应字段
field: 'graduallyIntervalTime',
// 宽度
width: 150
},
{
// 列标题
title: '消息展示语',
// 对应字段
field: 'funMsgTitle',
// 宽度
width: 150
},
{
// 列标题
title: '持续时间默认值',
// 对应字段
field: 'funStatisticsTimes',
// 宽度
width: 150
},
{
// 列标题
title: '提示语言:短消息提示语言',
// 对应字段
field: 'funShortMsg',
// 宽度
width: 150
},
];

276
vue-vben-admin/apps/web-antd/src/views/health_fun/form.ts

@ -0,0 +1,276 @@
/**
* schema
* component parse_component()
*/
import type { VbenFormSchema } from '#/adapter/form';
export const formSchema: VbenFormSchema[] = [
{
// 字段名
fieldName: 'id',
// label
label: '主键ID',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'funParentId',
// label
label: '父类ID',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'funName',
// label
label: '功能名称',
// 自动组件
component: 'Input'
},
{
// 字段名
fieldName: 'funCode',
// label
label: '功能码:功能码对外做映射用',
// 自动组件
component: 'Input'
},
{
// 字段名
fieldName: 'funValue',
// label
label: '功能数值',
// 自动组件
component: 'Input'
},
{
// 字段名
fieldName: 'funWarnLevel',
// label
label: '告警级别: 0默认,1级,2级,3级',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'funWarnType',
// label
label: '用位运算组合',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'funIndex',
// label
label: '首页展示: 0展示,1不展示',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'createdAt',
// label
label: '创建时间',
// 自动组件
component: 'DatePicker'
},
{
// 字段名
fieldName: 'createdBy',
// label
label: '创建人',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'updatedAt',
// label
label: '更新时间',
// 自动组件
component: 'DatePicker'
},
{
// 字段名
fieldName: 'updatedBy',
// label
label: '更新人',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'deletedFlag',
// label
label: '删除: 0 默认 1删除',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'userId',
// label
label: '用户ID',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'deviceCode',
// label
label: '设备唯一标识',
// 自动组件
component: 'Input'
},
{
// 字段名
fieldName: 'funImg',
// label
label: '功能图',
// 自动组件
component: 'Input'
},
{
// 字段名
fieldName: 'deviceType',
// label
label: '设备类型',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'funStatus',
// label
label: '功能状态: 默认 0开通,1关闭,2隐藏',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'graduallyIntervalTime',
// label
label: '逐级告警:完成10,30,60逐级提升告警持续',
// 自动组件
component: 'Input'
},
{
// 字段名
fieldName: 'funMsgTitle',
// label
label: '消息展示语',
// 自动组件
component: 'Input'
},
{
// 字段名
fieldName: 'funStatisticsTimes',
// label
label: '持续时间默认值',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'funShortMsg',
// label
label: '提示语言:短消息提示语言',
// 自动组件
component: 'Input'
},
];

691
vue-vben-admin/apps/web-antd/src/views/health_fun/index.vue

@ -0,0 +1,691 @@
<script setup lang="ts">
/**
* health_fun管理页面
*/
import {ref, reactive, onMounted, watch, h} from 'vue';
import {useVbenVxeGrid} from '#/adapter/vxe-table';
import {useVbenModal} from '@vben/common-ui';
import {useVbenForm} from '#/adapter/form';
import {Upload, message, Image} from 'ant-design-vue';
import dayjs from 'dayjs';
import {columns} from './data';
import {formSchema} from './form';
import { health_funApi } from '#/api/health_fun';
// ========== API URL ==========
const API_BASE_URL = import.meta.env.VITE_GLOB_API_URL || '';
// ========== ==========
const currentRow = ref(null);
const isEdit = ref(false);
const modalTitle = ref('新增');
const uploadFieldName = ref('');
const uploadImageUrl = ref('');
const uploadedUrl = ref(''); //
const formUploadUrls = ref<Record<string, string>>({}); // URL
// ========== Schema ==========
const querySchema = ref([]);
// ========== ==========
const enumData = reactive({
loading: true,
list: [], //
});
//todo
const hiddenColumns = ref(['fun_code', 'graduallyIntervalTime', 'funMsgTitle', 'funImg', 'createdAt', 'updatedBy', 'updatedAt', 'userPassword', 'userId', 'userSys', 'deletedFlag', "userFace"])
const editFields = ['userId','createdAt','createdBy','updatedAt','updatedBy','deletedFlag'];
// ========== ==========
function isImageField(field: string) {
return /img|face|picture/i.test(field);
}
// ========== ==========
async function initEnumData() {
try {
enumData.loading = true;
//
const res = await health_funApi.enumList({});
const enums = res.result || res;
//
enumData.list = enums;
// queryFields schema`
querySchema.value = formSchema.filter(formItem => {
// 2.
if (hiddenColumns.value.includes(formItem.fieldName)) {
console.log('跳过隐藏字段:', formItem.fieldName)
return false;
}
return true;
}).map(formSchemaTmp => {
//
const matchedEnums = enums.filter(item => item.fieldName === formSchemaTmp.fieldName);
// fieldName options
const allOptions = matchedEnums.reduce((acc, item) => {
if (item.options && Array.isArray(item.options)) {
return [...acc, ...item.options];
}
return acc;
}, []);
//
if (allOptions.length > 0) {
return {
component: 'Select',
fieldName: formSchemaTmp.fieldName,
label: formSchemaTmp.label,
componentProps: {
placeholder: `请选择`,
allowClear: true,
options: allOptions.map(opt => ({
label: opt.label,
value: String(opt.value), // value
})),
},
};
} else {
// 使 Input
return {
component: 'Input',
fieldName: formSchemaTmp.fieldName,
label: formSchemaTmp.label,
componentProps: {
placeholder: `请输入${formSchemaTmp.label}`,
allowClear: true,
},
};
}
});
} catch (error) {
} finally {
enumData.loading = false;
}
}
//
onMounted(() => {
initEnumData();
});
// ========== ( schema) ==========
const [QueryForm, queryFormApi] = useVbenForm({
schema: [], //
layout: 'inline',
showDefaultActions: false,
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-5',
})
// ========== querySchema ==========
watch(
querySchema,
(newSchema) => {
if (newSchema && newSchema.length > 0) {
console.log('🔄 更新查询表单 schema:', newSchema);
// 使 setState schema
queryFormApi.setState({
schema: newSchema,
});
}
},
{immediate: false}
);
// ========== ==========
function getFullUrl(url: string) {
if (!url) return '';
if (url.startsWith('http')) return url;
return `${API_BASE_URL}${url}`;
}
const gridOptions = {
columns: [
...columns.map(col => {
const baseCol = {
...col,
fixed: col.fixed ?? null,
};
// ( img, face, picture)使 +
if (isImageField(col.field)) {
return {
...baseCol,
width: 200,
align: 'center',
slots: {
default: ({row}: any) => {
const imgUrl = row[col.field];
const handleClick = (e: Event) => {
e.stopPropagation();
currentRow.value = row;
isEdit.value = true;
openUploadDialog(col.field, imgUrl);
};
if (!imgUrl) {
return h('div', { class: 'flex items-center justify-center gap-2 h-[50px]' }, [
h('span', { class: 'text-gray-400 text-xs' }, '无图片'),
h('button', { onClick: handleClick, class: 'text-blue-500 text-xs' }, '上传'),
]);
}
return h('div', { class: 'flex items-center gap-2 justify-center' }, [
h(Image, {
src: getFullUrl(imgUrl),
width: 80,
height: 60,
preview: true,
}),
h('button', { onClick: handleClick, class: 'text-blue-500 text-xs' }, '更换'),
]);
}
}
};
}
return baseCol;
}),
{
field: 'action',
title: '操作',
width: 120,
fixed: 'right',
slots: {default: 'action'},
},
],
customConfig: {
storage: true, // /
allowFixed: true, // 🔥
},
proxyConfig: {
ajax: {
query: async ({page}) => {
try {
//
const queryValues = await queryFormApi.getValues();
const res = await health_funApi.page({
pageNum: page.currentPage || 1,
pageSize: page?.pageSize || 10,
...queryValues, //
})
const data = res.error?.result || res.result || res
return {
items: data.records || [],
total: data.total || 0
}
} catch (error) {
console.error('查询列表失败:', error)
throw error
}
}
},
response: {
result: 'items',
total: 'total'
}
},
pagerConfig: {
enabled: true,
pageSize: 10,
},
showOverflow: true,
minHeight: '100%',
maxHeight: 'auto',
showHeaderOverflow: true,
}
const [Grid, gridApi] = useVbenVxeGrid({gridOptions})
// ========== Schema ==========
const editFormSchema = formSchema.filter(item => !editFields.includes(item.fieldName));
// ========== ==========
const [Modal, modalApi] = useVbenModal({
centered: true,
closable: true,
maskClosable: false,
draggable: true,
class: 'w-[60vw]',
onCancel() {
modalApi.close();
},
onConfirm: async () => {
let loadingMessage: any = null;
try {
const values = await formApi.validateAndSubmitForm();
if (!values) return;
// URL
const submitValues = !isEdit.value
? {...values, ...formUploadUrls.value}
: values;
// Day.js
const finalSubmitValues = {
...submitValues,
userBirthday: submitValues.userBirthday ? dayjs(submitValues.userBirthday).format('YYYY-MM-DD') : null,
};
// loading
loadingMessage = message.loading(isEdit.value ? '保存中...' : '新增中...', 0);
if (isEdit.value && currentRow.value?.id) {
await health_funApi.save({...finalSubmitValues, id: currentRow.value.id});
Object.assign(currentRow.value, finalSubmitValues);
modalApi.close();
loadingMessage();
isEdit.value = false;
message.success('保存成功!');
} else {
await health_funApi.add(finalSubmitValues);
// reload
modalApi.close();
gridApi.reload();
formUploadUrls.value = {};
loadingMessage();
message.success('新增成功!');
}
} catch (error) {
console.error('保存失败:', error);
if (loadingMessage) {
loadingMessage();
}
}
},
onOpenChange(isOpen: boolean) {
if (!isOpen && !isEdit.value) {
formApi.resetForm();
formUploadUrls.value = {};
}
},
})
// ========== ==========
const uploadFileList = ref<any[]>([]);
const [UploadModal, uploadModalApi] = useVbenModal({
centered: true,
closable: true,
maskClosable: false,
draggable: true,
width: 600,
title: '上传图片',
onCancel() {
uploadModalApi.close();
uploadFileList.value = [];
uploadedUrl.value = '';
},
onConfirm: async () => {
let loadingMessage: any = null;
console.log('=== 点击确认保存 ===', {
uploadedUrl: uploadedUrl.value,
isEdit: isEdit.value,
currentRowId: currentRow.value?.id,
uploadFieldName: uploadFieldName.value,
currentRowData: currentRow.value,
});
// URL
if (uploadedUrl.value) {
try {
// URL
if (currentRow.value) {
currentRow.value[uploadFieldName.value] = uploadedUrl.value;
console.log('✅ 已更新当前行图片字段:', {
fieldName: uploadFieldName.value,
newUrl: uploadedUrl.value,
updatedRow: currentRow.value,
});
}
// save
if (isEdit.value && currentRow.value?.id) {
const submitData = {
...currentRow.value,
id: currentRow.value.id
};
console.log('📤 提交保存数据:', submitData);
// loading
loadingMessage = message.loading('保存图片中...', 0);
await health_funApi.save(submitData);
message.success('保存成功!');
//
uploadModalApi.close();
// gridApi.reload();
loadingMessage()
//
uploadFileList.value = [];
uploadedUrl.value = '';
} else {
console.warn('⚠️ 不满足保存条件:', {
isEdit: isEdit.value,
hasId: !!currentRow.value?.id,
});
message.warning('非编辑模式或无 ID,仅更新预览');
//
uploadModalApi.close();
// gridApi.reload();
}
} catch (error: any) {
console.error('❌ 保存失败:', error);
if (loadingMessage) {
loadingMessage();
}
message.error(error?.message || '保存失败');
}
} else {
message.warning('请先选择并上传图片');
}
},
})
// ========== Schema ==========
// id
// id
const getEditFormSchema = () => {
return formSchema.filter(item => {
// editFields
if (!editFields.includes(item.fieldName)) {
// id
if (!isEdit.value && item.fieldName === 'id') {
return false;
}
return true;
}
return false;
}).map(item => {
//
if (!isEdit.value && isImageField(item.fieldName)) {
// 使 formUploadUrls URL使
const currentImageUrl = formUploadUrls.value[item.fieldName] || '';
return {
...item,
component: 'Upload',
componentProps: {
class: 'w-full',
accept: 'image/*',
maxCount: 1,
listType: 'picture-card',
fileList: currentImageUrl && typeof currentImageUrl === 'string' ? [{
uid: '-1',
name: 'image.png',
status: 'done',
url: currentImageUrl,
}] : (Array.isArray(currentImageUrl) ? currentImageUrl : []),
beforeUpload: async (file: any) => {
console.log('📦 新增表单内上传文件:', file);
try {
const resultUrl = await uploadFile(file);
console.log(item.fieldName + ' 新增表单内上传成功,URL:', resultUrl);
message.success('上传成功!');
// URL
formUploadUrls.value[item.fieldName] = resultUrl;
// schema fileList
setTimeout(() => {
formApi.setState({
schema: getEditFormSchema(),
});
}, 0);
// Deleted:formApi.resetForm();
// false
return false;
} catch (error: any) {
console.error('❌ 新增表单内上传失败:', error);
message.error(error?.message || '上传失败');
return false;
}
},
},
};
}
return item;
});
};
// ========== ==========
const [Form, formApi] = useVbenForm({
schema: editFormSchema,
showDefaultActions: false,
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
})
// ========== ==========
async function uploadFile(file: File) {
let loadingMessage: any = null;
// loading
loadingMessage = message.loading('图片上传中...', 0);
const formData = new FormData();
formData.append('file', file);
const res = await health_funApi.upload(formData);
loadingMessage();
return res.result || res.message || res.url || res;
}
// ========== ==========
function openUploadDialog(fieldName: string, currentUrl: string) {
console.log('=== 打开上传对话框 ===', {
fieldName,
currentUrl,
isEdit: isEdit.value,
currentRowId: currentRow.value?.id,
currentRowFull: currentRow.value,
});
uploadFieldName.value = fieldName;
uploadImageUrl.value = currentUrl || '';
uploadedUrl.value = ''; // URL
uploadFileList.value = []; //
uploadModalApi.open();
}
// ========== - 使 custom-request ==========
async function handleCustomRequest(options: any) {
const {file, onSuccess, onError} = options;
try {
console.log('📦 开始上传文件:', file);
console.log('📦 文件详情:', {
name: file.name,
type: file.type,
size: file.size,
lastModified: file.lastModified,
});
const resultUrl = await uploadFile(file);
console.log('✅ 上传成功,URL:', resultUrl);
message.success('上传成功!');
// URL
uploadedUrl.value = resultUrl;
console.log('=== uploadedUrl 赋值后 ===', uploadedUrl.value);
//
uploadFileList.value = [{
uid: file.uid,
name: file.name,
status: 'done',
url: resultUrl,
}];
//
onSuccess({url: resultUrl});
} catch (error: any) {
console.error('❌ 上传失败:', error);
message.error(error?.message || '上传失败');
onError(error);
}
}
// ========== ==========
function handleAdd() {
isEdit.value = false;
modalTitle.value = '功能配置表新增';
// URL
formUploadUrls.value = {};
// schema ( id)
formApi.setState({
schema: getEditFormSchema(),
});
formApi.resetForm();
modalApi.open();
}
// ========== ==========
function handleEdit(row: any) {
isEdit.value = true;
currentRow.value = row;
modalTitle.value = '功能配置表编辑';
// schema ( id)
formApi.setState({
schema: getEditFormSchema(),
});
// editFields
const formValues: any = {};
editFormSchema.forEach(item => {
const field = item.fieldName;
if (row[field] !== undefined && row[field] !== null) {
formValues[field] = row[field];
}
});
formApi.setValues(formValues);
modalApi.open();
}
// ========== ==========
function handleDelete(row: any) {
if (!row.id) return;
window.confirm(`确定要删除 "${row.userName}" 吗?`) &&
health_funApi.remove(row.id).then(() => {
gridApi.reload();
});
}
// ========== ==========
function handleSearch() {
console.log('=== 点击查询按钮 ===')
//
gridApi.query();
}
// ========== ==========
function handleReset() {
queryFormApi.resetForm();
gridApi.reload();
}
// ========== ==========
function openColumnSetting() {
const $grid = gridApi?.grid || gridApi?.getVxeGrid?.();
$grid?.openCustom();
}
</script>
<template>
<div
style="height: 100vh; padding: 16px; box-sizing: border-box; display: flex; flex-direction: column;">
<!-- 查询表单 -->
<div class="bg-card mb-4 p-4 rounded shadow flex-shrink-0">
<h3 class="text-lg font-semibold mb-3">查询条件</h3>
<div v-if="enumData.loading" class="text-center py-4 text-gray-500">
正在加载查询条件...
</div>
<QueryForm v-else/>
<div class="mt-3 flex gap-2">
<button @click="handleSearch"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
🔍 查询
</button>
<button @click="handleReset"
class="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600">
🔄 重置
</button>
</div>
</div>
<!-- 数据表格 (可滚动区域) -->
<div class="flex-1 overflow-hidden bg-card rounded shadow">
<Grid>
<template #toolbar-tools>
<button @click="handleAdd" class="vxe-button type--button size--small is--circle" title="新增">
<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 24 24">
<path fill="currentColor" d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"></path>
</svg>
</button>
<button @click="openColumnSetting" class="vxe-button type--button vxe-toolbar-custom-target size--small is--circle" title="列设置">
<i class="vxe-button--item vxe-button--prefix-icon vxe-table-icon-custom-column"></i>
</button>
<button @click="() => gridApi.reload()" class="vxe-button type--button size--small is--circle" title="刷新" type="button">
<i class="vxe-button--item vxe-button--prefix-icon vxe-table-icon-repeat"></i>
</button>
</template>
<template #action="{ row }">
<div class="flex gap-2">
<button @click="() => handleEdit(row)" class="text-blue-500 hover:text-blue-700"> 编辑
</button>
<button @click="() => handleDelete(row)" class="text-red-500 hover:text-red-700">🗑 删除
</button>
</div>
</template>
</Grid>
</div>
<Modal :title="modalTitle">
<Form/>
</Modal>
<!-- 上传对话框 -->
<UploadModal>
<div style="padding: 20px;margin-top: 16px; text-align: center;">
<Upload
name="file"
:file-list="uploadFileList"
:custom-request="handleCustomRequest"
:show-upload-list="true"
accept="image/*" style="width: 100%;"
>
<div
style="width: 100%; height: 150px; border: 2px dashed #d9d9d9; border-radius: 8px; background: #fafafa; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer;">
<!-- 使用 SVG 图标代替 -->
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#1890ff"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="margin-bottom: 8px;">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<span style="color: #666;">点击或拖拽图片到此上传</span>
<span style="color: #999; font-size: 12px; margin-top: 4px;">支持 JPGPNG 格式</span>
</div>
</Upload>
<!-- 展示上传成功后的图片 -->
<div v-if="uploadedUrl" style="margin-top: 16px; text-align: center;">
<p style="color: #67c23a; font-size: 14px; margin-bottom: 8px;"> 上传成功</p>
<p style="color: #999; font-size: 12px; margin-bottom: 8px;">新图片预览:</p>
<Image
:src="uploadedUrl"
style="max-width: 200px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"
:preview="true"
/>
<p style="color: #999; font-size: 12px; margin-top: 8px; word-break: break-all;"></p>
</div>
<!-- 展示原图如果有 -->
<div v-if="uploadImageUrl && !uploadedUrl" style="margin-top: 16px; text-align: center;">
<p style="color: #999; font-size: 12px; margin-bottom: 8px;">当前图片:</p>
<Image
:src="getFullUrl(uploadImageUrl)"
style="max-width: 200px; border-radius: 4px;"
:preview="true"
/>
</div>
</div>
</UploadModal>
</div>
</template>
<style scoped>
:deep(.vxe-pager--wrapper) {
margin-bottom: 0.5rem;
}
</style>

242
vue-vben-admin/apps/web-antd/src/views/health_msg/data.ts

@ -0,0 +1,242 @@
/**
*
*/
import type { VxeGridProps } from '#/adapter/vxe-table';
export const columns: VxeGridProps['columns'] = [
{
// 列标题
title: '主键ID',
// 对应字段
field: 'id',
// 宽度
width: 150
},
{
// 列标题
title: '信息详情',
// 对应字段
field: 'msgNote',
// 宽度
width: 150
},
{
// 列标题
title: '消息类型: 0 告警信息 1恢复消息,2推送消息',
// 对应字段
field: 'msgType',
// 宽度
width: 150
},
{
// 列标题
title: '场景ID',
// 对应字段
field: 'sceneId',
// 宽度
width: 150
},
{
// 列标题
title: '用户ID',
// 对应字段
field: 'userId',
// 宽度
width: 150
},
{
// 列标题
title: '图片路径',
// 对应字段
field: 'url',
// 宽度
width: 150
},
{
// 列标题
title: '推送状态 0 未推送 1成功 2失败',
// 对应字段
field: 'messageSendStatus',
// 宽度
width: 150
},
{
// 列标题
title: '创建时间',
// 对应字段
field: 'createdAt',
// 宽度
width: 150
},
{
// 列标题
title: '创建人',
// 对应字段
field: 'createdBy',
// 宽度
width: 150
},
{
// 列标题
title: '更新时间',
// 对应字段
field: 'updatedAt',
// 宽度
width: 150
},
{
// 列标题
title: '更新人',
// 对应字段
field: 'updatedBy',
// 宽度
width: 150
},
{
// 列标题
title: '删除 0 默认 1删除',
// 对应字段
field: 'deletedFlag',
// 宽度
width: 150
},
{
// 列标题
title: '设备ID',
// 对应字段
field: 'deviceId',
// 宽度
width: 150
},
{
// 列标题
title: '房间ID',
// 对应字段
field: 'roomId',
// 宽度
width: 150
},
{
// 列标题
title: '0默认未读;已读1',
// 对应字段
field: 'msgRead',
// 宽度
width: 150
},
{
// 列标题
title: '功能方法ID',
// 对应字段
field: 'funId',
// 宽度
width: 150
},
{
// 列标题
title: '功能父类ID',
// 对应字段
field: 'funParentId',
// 宽度
width: 150
},
{
// 列标题
title: '功能告警等级',
// 对应字段
field: 'funWarnLevel',
// 宽度
width: 150
},
{
// 列标题
title: '家庭ID',
// 对应字段
field: 'familyId',
// 宽度
width: 150
},
{
// 列标题
title: '统计时长/毫秒',
// 对应字段
field: 'statisticsDuration',
// 宽度
width: 150
},
{
// 列标题
title: '消息原表ID',
// 对应字段
field: 'msgSourceId',
// 宽度
width: 150
},
];

264
vue-vben-admin/apps/web-antd/src/views/health_msg/form.ts

@ -0,0 +1,264 @@
/**
* schema
* component parse_component()
*/
import type { VbenFormSchema } from '#/adapter/form';
export const formSchema: VbenFormSchema[] = [
{
// 字段名
fieldName: 'id',
// label
label: '主键ID',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'msgNote',
// label
label: '信息详情',
// 自动组件
component: 'Input'
},
{
// 字段名
fieldName: 'msgType',
// label
label: '消息类型: 0 告警信息 1恢复消息,2推送消息',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'sceneId',
// label
label: '场景ID',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'userId',
// label
label: '用户ID',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'url',
// label
label: '图片路径',
// 自动组件
component: 'Input'
},
{
// 字段名
fieldName: 'messageSendStatus',
// label
label: '推送状态 0 未推送 1成功 2失败',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'createdAt',
// label
label: '创建时间',
// 自动组件
component: 'DatePicker'
},
{
// 字段名
fieldName: 'createdBy',
// label
label: '创建人',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'updatedAt',
// label
label: '更新时间',
// 自动组件
component: 'DatePicker'
},
{
// 字段名
fieldName: 'updatedBy',
// label
label: '更新人',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'deletedFlag',
// label
label: '删除 0 默认 1删除',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'deviceId',
// label
label: '设备ID',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'roomId',
// label
label: '房间ID',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'msgRead',
// label
label: '0默认未读;已读1',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'funId',
// label
label: '功能方法ID',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'funParentId',
// label
label: '功能父类ID',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'funWarnLevel',
// label
label: '功能告警等级',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'familyId',
// label
label: '家庭ID',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'statisticsDuration',
// label
label: '统计时长/毫秒',
// 自动组件
component: 'InputNumber'
},
{
// 字段名
fieldName: 'msgSourceId',
// label
label: '消息原表ID',
// 自动组件
component: 'InputNumber'
},
];

691
vue-vben-admin/apps/web-antd/src/views/health_msg/index.vue

@ -0,0 +1,691 @@
<script setup lang="ts">
/**
* health_msg管理页面
*/
import {ref, reactive, onMounted, watch, h} from 'vue';
import {useVbenVxeGrid} from '#/adapter/vxe-table';
import {useVbenModal} from '@vben/common-ui';
import {useVbenForm} from '#/adapter/form';
import {Upload, message, Image} from 'ant-design-vue';
import dayjs from 'dayjs';
import {columns} from './data';
import {formSchema} from './form';
import { health_msgApi } from '#/api/health_msg';
// ========== API URL ==========
const API_BASE_URL = import.meta.env.VITE_GLOB_API_URL || '';
// ========== ==========
const currentRow = ref(null);
const isEdit = ref(false);
const modalTitle = ref('新增');
const uploadFieldName = ref('');
const uploadImageUrl = ref('');
const uploadedUrl = ref(''); //
const formUploadUrls = ref<Record<string, string>>({}); // URL
// ========== Schema ==========
const querySchema = ref([]);
// ========== ==========
const enumData = reactive({
loading: true,
list: [], //
});
//todo
const hiddenColumns = ref(['fun_code', 'graduallyIntervalTime', 'funMsgTitle', 'funImg', 'createdAt', 'updatedBy', 'updatedAt', 'userPassword', 'userId', 'userSys', 'deletedFlag', "userFace"])
const editFields = ['userId','createdAt','createdBy','updatedAt','updatedBy','deletedFlag'];
// ========== ==========
function isImageField(field: string) {
return /img|face|picture/i.test(field);
}
// ========== ==========
async function initEnumData() {
try {
enumData.loading = true;
//
const res = await health_msgApi.enumList({});
const enums = res.result || res;
//
enumData.list = enums;
// queryFields schema`
querySchema.value = formSchema.filter(formItem => {
// 2.
if (hiddenColumns.value.includes(formItem.fieldName)) {
console.log('跳过隐藏字段:', formItem.fieldName)
return false;
}
return true;
}).map(formSchemaTmp => {
//
const matchedEnums = enums.filter(item => item.fieldName === formSchemaTmp.fieldName);
// fieldName options
const allOptions = matchedEnums.reduce((acc, item) => {
if (item.options && Array.isArray(item.options)) {
return [...acc, ...item.options];
}
return acc;
}, []);
//
if (allOptions.length > 0) {
return {
component: 'Select',
fieldName: formSchemaTmp.fieldName,
label: formSchemaTmp.label,
componentProps: {
placeholder: `请选择`,
allowClear: true,
options: allOptions.map(opt => ({
label: opt.label,
value: String(opt.value), // value
})),
},
};
} else {
// 使 Input
return {
component: 'Input',
fieldName: formSchemaTmp.fieldName,
label: formSchemaTmp.label,
componentProps: {
placeholder: `请输入${formSchemaTmp.label}`,
allowClear: true,
},
};
}
});
} catch (error) {
} finally {
enumData.loading = false;
}
}
//
onMounted(() => {
initEnumData();
});
// ========== ( schema) ==========
const [QueryForm, queryFormApi] = useVbenForm({
schema: [], //
layout: 'inline',
showDefaultActions: false,
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-5',
})
// ========== querySchema ==========
watch(
querySchema,
(newSchema) => {
if (newSchema && newSchema.length > 0) {
console.log('🔄 更新查询表单 schema:', newSchema);
// 使 setState schema
queryFormApi.setState({
schema: newSchema,
});
}
},
{immediate: false}
);
// ========== ==========
function getFullUrl(url: string) {
if (!url) return '';
if (url.startsWith('http')) return url;
return `${API_BASE_URL}${url}`;
}
const gridOptions = {
columns: [
...columns.map(col => {
const baseCol = {
...col,
fixed: col.fixed ?? null,
};
// ( img, face, picture)使 +
if (isImageField(col.field)) {
return {
...baseCol,
width: 200,
align: 'center',
slots: {
default: ({row}: any) => {
const imgUrl = row[col.field];
const handleClick = (e: Event) => {
e.stopPropagation();
currentRow.value = row;
isEdit.value = true;
openUploadDialog(col.field, imgUrl);
};
if (!imgUrl) {
return h('div', { class: 'flex items-center justify-center gap-2 h-[50px]' }, [
h('span', { class: 'text-gray-400 text-xs' }, '无图片'),
h('button', { onClick: handleClick, class: 'text-blue-500 text-xs' }, '上传'),
]);
}
return h('div', { class: 'flex items-center gap-2 justify-center' }, [
h(Image, {
src: getFullUrl(imgUrl),
width: 80,
height: 60,
preview: true,
}),
h('button', { onClick: handleClick, class: 'text-blue-500 text-xs' }, '更换'),
]);
}
}
};
}
return baseCol;
}),
{
field: 'action',
title: '操作',
width: 120,
fixed: 'right',
slots: {default: 'action'},
},
],
customConfig: {
storage: true, // /
allowFixed: true, // 🔥
},
proxyConfig: {
ajax: {
query: async ({page}) => {
try {
//
const queryValues = await queryFormApi.getValues();
const res = await health_msgApi.page({
pageNum: page.currentPage || 1,
pageSize: page?.pageSize || 10,
...queryValues, //
})
const data = res.error?.result || res.result || res
return {
items: data.records || [],
total: data.total || 0
}
} catch (error) {
console.error('查询列表失败:', error)
throw error
}
}
},
response: {
result: 'items',
total: 'total'
}
},
pagerConfig: {
enabled: true,
pageSize: 10,
},
showOverflow: true,
minHeight: '100%',
maxHeight: 'auto',
showHeaderOverflow: true,
}
const [Grid, gridApi] = useVbenVxeGrid({gridOptions})
// ========== Schema ==========
const editFormSchema = formSchema.filter(item => !editFields.includes(item.fieldName));
// ========== ==========
const [Modal, modalApi] = useVbenModal({
centered: true,
closable: true,
maskClosable: false,
draggable: true,
class: 'w-[60vw]',
onCancel() {
modalApi.close();
},
onConfirm: async () => {
let loadingMessage: any = null;
try {
const values = await formApi.validateAndSubmitForm();
if (!values) return;
// URL
const submitValues = !isEdit.value
? {...values, ...formUploadUrls.value}
: values;
// Day.js
const finalSubmitValues = {
...submitValues,
userBirthday: submitValues.userBirthday ? dayjs(submitValues.userBirthday).format('YYYY-MM-DD') : null,
};
// loading
loadingMessage = message.loading(isEdit.value ? '保存中...' : '新增中...', 0);
if (isEdit.value && currentRow.value?.id) {
await health_msgApi.save({...finalSubmitValues, id: currentRow.value.id});
Object.assign(currentRow.value, finalSubmitValues);
modalApi.close();
loadingMessage();
isEdit.value = false;
message.success('保存成功!');
} else {
await health_msgApi.add(finalSubmitValues);
// reload
modalApi.close();
gridApi.reload();
formUploadUrls.value = {};
loadingMessage();
message.success('新增成功!');
}
} catch (error) {
console.error('保存失败:', error);
if (loadingMessage) {
loadingMessage();
}
}
},
onOpenChange(isOpen: boolean) {
if (!isOpen && !isEdit.value) {
formApi.resetForm();
formUploadUrls.value = {};
}
},
})
// ========== ==========
const uploadFileList = ref<any[]>([]);
const [UploadModal, uploadModalApi] = useVbenModal({
centered: true,
closable: true,
maskClosable: false,
draggable: true,
width: 600,
title: '上传图片',
onCancel() {
uploadModalApi.close();
uploadFileList.value = [];
uploadedUrl.value = '';
},
onConfirm: async () => {
let loadingMessage: any = null;
console.log('=== 点击确认保存 ===', {
uploadedUrl: uploadedUrl.value,
isEdit: isEdit.value,
currentRowId: currentRow.value?.id,
uploadFieldName: uploadFieldName.value,
currentRowData: currentRow.value,
});
// URL
if (uploadedUrl.value) {
try {
// URL
if (currentRow.value) {
currentRow.value[uploadFieldName.value] = uploadedUrl.value;
console.log('✅ 已更新当前行图片字段:', {
fieldName: uploadFieldName.value,
newUrl: uploadedUrl.value,
updatedRow: currentRow.value,
});
}
// save
if (isEdit.value && currentRow.value?.id) {
const submitData = {
...currentRow.value,
id: currentRow.value.id
};
console.log('📤 提交保存数据:', submitData);
// loading
loadingMessage = message.loading('保存图片中...', 0);
await health_msgApi.save(submitData);
message.success('保存成功!');
//
uploadModalApi.close();
// gridApi.reload();
loadingMessage()
//
uploadFileList.value = [];
uploadedUrl.value = '';
} else {
console.warn('⚠️ 不满足保存条件:', {
isEdit: isEdit.value,
hasId: !!currentRow.value?.id,
});
message.warning('非编辑模式或无 ID,仅更新预览');
//
uploadModalApi.close();
// gridApi.reload();
}
} catch (error: any) {
console.error('❌ 保存失败:', error);
if (loadingMessage) {
loadingMessage();
}
message.error(error?.message || '保存失败');
}
} else {
message.warning('请先选择并上传图片');
}
},
})
// ========== Schema ==========
// id
// id
const getEditFormSchema = () => {
return formSchema.filter(item => {
// editFields
if (!editFields.includes(item.fieldName)) {
// id
if (!isEdit.value && item.fieldName === 'id') {
return false;
}
return true;
}
return false;
}).map(item => {
//
if (!isEdit.value && isImageField(item.fieldName)) {
// 使 formUploadUrls URL使
const currentImageUrl = formUploadUrls.value[item.fieldName] || '';
return {
...item,
component: 'Upload',
componentProps: {
class: 'w-full',
accept: 'image/*',
maxCount: 1,
listType: 'picture-card',
fileList: currentImageUrl && typeof currentImageUrl === 'string' ? [{
uid: '-1',
name: 'image.png',
status: 'done',
url: currentImageUrl,
}] : (Array.isArray(currentImageUrl) ? currentImageUrl : []),
beforeUpload: async (file: any) => {
console.log('📦 新增表单内上传文件:', file);
try {
const resultUrl = await uploadFile(file);
console.log(item.fieldName + ' 新增表单内上传成功,URL:', resultUrl);
message.success('上传成功!');
// URL
formUploadUrls.value[item.fieldName] = resultUrl;
// schema fileList
setTimeout(() => {
formApi.setState({
schema: getEditFormSchema(),
});
}, 0);
// Deleted:formApi.resetForm();
// false
return false;
} catch (error: any) {
console.error('❌ 新增表单内上传失败:', error);
message.error(error?.message || '上传失败');
return false;
}
},
},
};
}
return item;
});
};
// ========== ==========
const [Form, formApi] = useVbenForm({
schema: editFormSchema,
showDefaultActions: false,
wrapperClass: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
})
// ========== ==========
async function uploadFile(file: File) {
let loadingMessage: any = null;
// loading
loadingMessage = message.loading('图片上传中...', 0);
const formData = new FormData();
formData.append('file', file);
const res = await health_msgApi.upload(formData);
loadingMessage();
return res.result || res.message || res.url || res;
}
// ========== ==========
function openUploadDialog(fieldName: string, currentUrl: string) {
console.log('=== 打开上传对话框 ===', {
fieldName,
currentUrl,
isEdit: isEdit.value,
currentRowId: currentRow.value?.id,
currentRowFull: currentRow.value,
});
uploadFieldName.value = fieldName;
uploadImageUrl.value = currentUrl || '';
uploadedUrl.value = ''; // URL
uploadFileList.value = []; //
uploadModalApi.open();
}
// ========== - 使 custom-request ==========
async function handleCustomRequest(options: any) {
const {file, onSuccess, onError} = options;
try {
console.log('📦 开始上传文件:', file);
console.log('📦 文件详情:', {
name: file.name,
type: file.type,
size: file.size,
lastModified: file.lastModified,
});
const resultUrl = await uploadFile(file);
console.log('✅ 上传成功,URL:', resultUrl);
message.success('上传成功!');
// URL
uploadedUrl.value = resultUrl;
console.log('=== uploadedUrl 赋值后 ===', uploadedUrl.value);
//
uploadFileList.value = [{
uid: file.uid,
name: file.name,
status: 'done',
url: resultUrl,
}];
//
onSuccess({url: resultUrl});
} catch (error: any) {
console.error('❌ 上传失败:', error);
message.error(error?.message || '上传失败');
onError(error);
}
}
// ========== ==========
function handleAdd() {
isEdit.value = false;
modalTitle.value = '功能配置表新增';
// URL
formUploadUrls.value = {};
// schema ( id)
formApi.setState({
schema: getEditFormSchema(),
});
formApi.resetForm();
modalApi.open();
}
// ========== ==========
function handleEdit(row: any) {
isEdit.value = true;
currentRow.value = row;
modalTitle.value = '功能配置表编辑';
// schema ( id)
formApi.setState({
schema: getEditFormSchema(),
});
// editFields
const formValues: any = {};
editFormSchema.forEach(item => {
const field = item.fieldName;
if (row[field] !== undefined && row[field] !== null) {
formValues[field] = row[field];
}
});
formApi.setValues(formValues);
modalApi.open();
}
// ========== ==========
function handleDelete(row: any) {
if (!row.id) return;
window.confirm(`确定要删除 "${row.userName}" 吗?`) &&
health_msgApi.remove(row.id).then(() => {
gridApi.reload();
});
}
// ========== ==========
function handleSearch() {
console.log('=== 点击查询按钮 ===')
//
gridApi.query();
}
// ========== ==========
function handleReset() {
queryFormApi.resetForm();
gridApi.reload();
}
// ========== ==========
function openColumnSetting() {
const $grid = gridApi?.grid || gridApi?.getVxeGrid?.();
$grid?.openCustom();
}
</script>
<template>
<div
style="height: 100vh; padding: 16px; box-sizing: border-box; display: flex; flex-direction: column;">
<!-- 查询表单 -->
<div class="bg-card mb-4 p-4 rounded shadow flex-shrink-0">
<h3 class="text-lg font-semibold mb-3">查询条件</h3>
<div v-if="enumData.loading" class="text-center py-4 text-gray-500">
正在加载查询条件...
</div>
<QueryForm v-else/>
<div class="mt-3 flex gap-2">
<button @click="handleSearch"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
🔍 查询
</button>
<button @click="handleReset"
class="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600">
🔄 重置
</button>
</div>
</div>
<!-- 数据表格 (可滚动区域) -->
<div class="flex-1 overflow-hidden bg-card rounded shadow">
<Grid>
<template #toolbar-tools>
<button @click="handleAdd" class="vxe-button type--button size--small is--circle" title="新增">
<svg xmlns="http://www.w3.org/2000/svg" width="1.2em" height="1.2em" viewBox="0 0 24 24">
<path fill="currentColor" d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2z"></path>
</svg>
</button>
<button @click="openColumnSetting" class="vxe-button type--button vxe-toolbar-custom-target size--small is--circle" title="列设置">
<i class="vxe-button--item vxe-button--prefix-icon vxe-table-icon-custom-column"></i>
</button>
<button @click="() => gridApi.reload()" class="vxe-button type--button size--small is--circle" title="刷新" type="button">
<i class="vxe-button--item vxe-button--prefix-icon vxe-table-icon-repeat"></i>
</button>
</template>
<template #action="{ row }">
<div class="flex gap-2">
<button @click="() => handleEdit(row)" class="text-blue-500 hover:text-blue-700"> 编辑
</button>
<button @click="() => handleDelete(row)" class="text-red-500 hover:text-red-700">🗑 删除
</button>
</div>
</template>
</Grid>
</div>
<Modal :title="modalTitle">
<Form/>
</Modal>
<!-- 上传对话框 -->
<UploadModal>
<div style="padding: 20px;margin-top: 16px; text-align: center;">
<Upload
name="file"
:file-list="uploadFileList"
:custom-request="handleCustomRequest"
:show-upload-list="true"
accept="image/*" style="width: 100%;"
>
<div
style="width: 100%; height: 150px; border: 2px dashed #d9d9d9; border-radius: 8px; background: #fafafa; display: flex; flex-direction: column; justify-content: center; align-items: center; cursor: pointer;">
<!-- 使用 SVG 图标代替 -->
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="#1890ff"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
style="margin-bottom: 8px;">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
<span style="color: #666;">点击或拖拽图片到此上传</span>
<span style="color: #999; font-size: 12px; margin-top: 4px;">支持 JPGPNG 格式</span>
</div>
</Upload>
<!-- 展示上传成功后的图片 -->
<div v-if="uploadedUrl" style="margin-top: 16px; text-align: center;">
<p style="color: #67c23a; font-size: 14px; margin-bottom: 8px;"> 上传成功</p>
<p style="color: #999; font-size: 12px; margin-bottom: 8px;">新图片预览:</p>
<Image
:src="uploadedUrl"
style="max-width: 200px; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"
:preview="true"
/>
<p style="color: #999; font-size: 12px; margin-top: 8px; word-break: break-all;"></p>
</div>
<!-- 展示原图如果有 -->
<div v-if="uploadImageUrl && !uploadedUrl" style="margin-top: 16px; text-align: center;">
<p style="color: #999; font-size: 12px; margin-bottom: 8px;">当前图片:</p>
<Image
:src="getFullUrl(uploadImageUrl)"
style="max-width: 200px; border-radius: 4px;"
:preview="true"
/>
</div>
</div>
</UploadModal>
</div>
</template>
<style scoped>
:deep(.vxe-pager--wrapper) {
margin-bottom: 0.5rem;
}
</style>
Loading…
Cancel
Save