This commit is contained in:
2026-04-08 21:26:18 +08:00
commit 8fdc7ac0c3
401 changed files with 53093 additions and 0 deletions

20
.editorconfig Normal file
View File

@@ -0,0 +1,20 @@
# EditorConfig is awesome: https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[Makefile]
indent_style = tab

View File

@@ -0,0 +1,60 @@
name: Get Version
description: 从 package.json 读取版本号,与最新 tag 对比决定使用哪个版本
outputs:
version:
description: 最终确定的版本号 (带 v 前缀)
value: ${{ steps.get_version.outputs.version }}
runs:
using: composite
steps:
- name: Get version from package.json
id: get_version
shell: bash
run: |
PKG_VERSION=$(node -p "require('./package.json').version")
echo "Package.json version: ${PKG_VERSION}"
# 获取最新的tag按版本号排序
LATEST_TAG=$(git ls-remote --tags origin 'refs/tags/v*' 2>/dev/null | sed 's/.*refs\/tags\///' | sed 's/\^{}//' | sort -V | tail -n 1 || echo "")
echo "Latest tag: ${LATEST_TAG}"
# 版本比较函数
version_compare() {
# 返回: 0 = 相等, 1 = v1 > v2, 2 = v1 < v2
if [[ "$1" == "$2" ]]; then echo 0; return; fi
local IFS=.
local i v1=($1) v2=($2)
for ((i=0; i<${#v1[@]} || i<${#v2[@]}; i++)); do
local n1=${v1[i]:-0} n2=${v2[i]:-0}
if ((n1 > n2)); then echo 1; return; fi
if ((n1 < n2)); then echo 2; return; fi
done
echo 0
}
if [[ -z "${LATEST_TAG}" ]]; then
# 没有任何tag直接使用package.json版本
NEW_TAG="v${PKG_VERSION}"
echo "No existing tags, using package.json version: ${NEW_TAG}"
else
# 去掉v前缀进行比较
LATEST_VERSION="${LATEST_TAG#v}"
COMPARE_RESULT=$(version_compare "${PKG_VERSION}" "${LATEST_VERSION}")
if [[ "${COMPARE_RESULT}" == "1" ]]; then
# package.json版本更大使用它
NEW_TAG="v${PKG_VERSION}"
echo "Package.json version is greater, using: ${NEW_TAG}"
else
# 最新tag版本更大或相等在其基础上递增patch
IFS='.' read -r MAJOR MINOR PATCH <<< "${LATEST_VERSION}"
PATCH=$((PATCH + 1))
NEW_TAG="v${MAJOR}.${MINOR}.${PATCH}"
echo "Incrementing from latest tag, new version: ${NEW_TAG}"
fi
fi
echo "version=${NEW_TAG}" >> $GITHUB_OUTPUT
echo "Final Version: ${NEW_TAG}"

View File

@@ -0,0 +1,124 @@
name: WeChat Work Notification
description: 发送企业微信群机器人通知
author: Your Team
inputs:
webhook_url:
description: 企业微信群机器人 Webhook URL
required: true
status:
description: 构建状态 (success/failure/cancelled)
required: true
default: success
title:
description: 通知标题
required: true
repository:
description: 仓库名称
required: false
default: ${{ github.repository }}
branch:
description: 分支名称
required: false
default: ''
version:
description: 版本号
required: false
default: ''
actor:
description: 构建触发人
required: false
default: ${{ github.actor }}
run_number:
description: 运行序号
required: false
default: ${{ github.run_number }}
run_url:
description: 运行详情 URL
required: false
default: ''
failed_step:
description: 失败的步骤名称
required: false
default: ''
extra_content:
description: 额外的通知内容 (markdown 格式)
required: false
default: ''
runs:
using: composite
steps:
- name: Send WeChat Work Notification
shell: bash
run: |
# 设置状态图标和文本
if [ "${{ inputs.status }}" = "success" ]; then
STATUS_ICON="✅"
STATUS_TEXT="成功"
elif [ "${{ inputs.status }}" = "cancelled" ]; then
STATUS_ICON="⚠️"
STATUS_TEXT="已取消"
else
STATUS_ICON="❌"
STATUS_TEXT="失败"
fi
# 构建通知内容
CONTENT="## ${STATUS_ICON} ${{ inputs.title }} ${STATUS_TEXT}"
# 添加仓库信息
if [ -n "${{ inputs.repository }}" ]; then
CONTENT="${CONTENT}\n> **仓库**: ${{ inputs.repository }}"
fi
# 添加分支信息
if [ -n "${{ inputs.branch }}" ]; then
CONTENT="${CONTENT}\n> **分支**: ${{ inputs.branch }}"
fi
# 添加版本信息
if [ -n "${{ inputs.version }}" ]; then
CONTENT="${CONTENT}\n> **版本**: ${{ inputs.version }}"
fi
# 添加构建人
if [ -n "${{ inputs.actor }}" ]; then
CONTENT="${CONTENT}\n> **构建人**: ${{ inputs.actor }}"
fi
# 添加运行序号
if [ -n "${{ inputs.run_number }}" ]; then
CONTENT="${CONTENT}\n> **运行序号**: #${{ inputs.run_number }}"
fi
# 添加失败步骤信息
if [ -n "${{ inputs.failed_step }}" ]; then
CONTENT="${CONTENT}\n> **失败步骤**: ${{ inputs.failed_step }}"
fi
# 添加额外内容
if [ -n "${{ inputs.extra_content }}" ]; then
CONTENT="${CONTENT}\n> ${{ inputs.extra_content }}"
fi
# 添加查看详情链接
if [ -n "${{ inputs.run_url }}" ]; then
CONTENT="${CONTENT}\n> [查看详情](${{ inputs.run_url }})"
fi
echo "========== 企业微信通知 =========="
echo "状态: ${STATUS_ICON} ${STATUS_TEXT}"
echo "标题: ${{ inputs.title }}"
echo "Webhook URL: ${{ inputs.webhook_url }}"
echo "=================================="
# 发送通知
curl -s -X POST "${{ inputs.webhook_url }}" \
-H "Content-Type: application/json" \
-d "{
\"msgtype\": \"markdown\",
\"markdown\": {
\"content\": \"${CONTENT}\"
}
}"

View File

@@ -0,0 +1,58 @@
name: Deploy Dev
on:
workflow_dispatch:
inputs:
# 分支
target_branch:
description: 请输入要发布的分支名称
required: true
default: dev
type: string
jobs:
build-and-deploy:
runs-on: node-22
steps:
- uses: https://gitee.com/youtellme/checkout@v4
with:
ref: ${{ inputs.target_branch }}
fetch-depth: 1
- name: Setup pnpm
id: setup_pnpm
uses: https://gitee.com/youtellme/pnpm-action-setup@v4
- name: Install dependencies
id: install
run: |
rm -rf pnpm-lock.yaml node_modules
pnpm install
- run: pnpm run build:web
# 压缩 dist 文件夹
- name: zip dist folder
run: |
TIMESTAMP=$(date +%Y%m%d%H%M%S)
BRANCH_NAME="${{ inputs.target_branch }}"
SAFE_BRANCH_NAME="${BRANCH_NAME//\//_}"
mkdir -p zip-output
cd apps/web/dist
zip -r -q ../../../zip-output/cslab-dcs-web_${SAFE_BRANCH_NAME}_${TIMESTAMP}.zip .
cd -
ls -lh zip-output/
# 上传 zip 文件到 RustFS
- name: Upload zip to RustFS (S3)
uses: jakejarvis/s3-sync-action@master
with:
args: --acl public-read
env:
AWS_S3_BUCKET: ${{ vars.RUSTFS_AWS_S3_BUCKET }}
AWS_ACCESS_KEY_ID: ${{ vars.RUSTFS_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ vars.RUSTFS_SECRET_KEY }}
AWS_REGION: us-east-1
AWS_S3_ENDPOINT: ${{ vars.RUSTFS_AWS_S3_ENDPOINT }}
SOURCE_DIR: zip-output

49
.gitignore vendored Normal file
View File

@@ -0,0 +1,49 @@
# Dependencies
node_modules
.pnpm-store
# Build outputs
dist
out
*.local
# Electron
release
# Tauri
**/src-tauri/target
**/src-tauri/gen
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/launch.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*~
# Environment files
# Test coverage
coverage
*.lcov
# TypeScript cache
*.tsbuildinfo
# Vite
.vite

3
.npmrc Normal file
View File

@@ -0,0 +1,3 @@
shamefully-hoist=true
strict-peer-dependencies=false
auto-install-peers=true

3
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}

39
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,39 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Main Process",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd"
},
"runtimeArgs": ["--sourcemap"],
"env": {
"REMOTE_DEBUGGING_PORT": "9222"
}
},
{
"name": "Debug Renderer Process",
"port": 9222,
"request": "attach",
"type": "chrome",
"webRoot": "${workspaceFolder}/src/renderer",
"timeout": 60000,
"presentation": {
"hidden": true
}
}
],
"compounds": [
{
"name": "Debug All",
"configurations": ["Debug Main Process", "Debug Renderer Process"],
"presentation": {
"order": 1
}
}
]
}

50
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,50 @@
{
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in your IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
]
}

139
Jenkinsfile.dev110 Normal file
View File

@@ -0,0 +1,139 @@
pipeline {
agent any
environment {
// 项目信息
PROJECT_NAME = 'DCS 画布编辑器'
APP_NAME = 'cslab-dcs-web'
BUILD_DIR = 'apps/web/dist'
// 部署目标 - 110开发环境 (内网 110)
SSH_SERVER = 'ssh-dev-110'
DEPLOY_HOST = '192.168.1.110'
REMOTE_DIR = 'D:/www/chemlab/jenkins-dir'
DEPLOY_BASE = 'D:/www/chemlab'
DEPLOY_DIR = "${DEPLOY_BASE}/${APP_NAME}"
// 企业微信 Webhook从 Jenkins 凭据读取)
WEBHOOK_URL = credentials('webhook-url')
// Node 版本(需在 Jenkins 全局工具配置中添加同名 NodeJS 安装)
NODE_HOME = tool(name: 'NodeJS-22', type: 'nodejs')
PATH = "${NODE_HOME}/bin:${env.PATH}"
}
options {
timeout(time: 15, unit: 'MINUTES')
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '10'))
}
stages {
stage('环境检查') {
steps {
echo "📋 项目: ${PROJECT_NAME}"
echo "🌿 分支: ${env.BRANCH_NAME ?: env.GIT_BRANCH ?: 'unknown'}"
echo "🔧 环境: 110开发 (${DEPLOY_HOST})"
sh '''
echo "Node 版本: $(node -v)"
echo "pnpm 版本: $(pnpm -v)"
'''
}
}
stage('安装依赖') {
steps {
echo '📦 安装项目依赖...'
sh 'ELECTRON_SKIP_BINARY_DOWNLOAD=1 pnpm install'
}
}
stage('构建') {
steps {
echo '🔨 构建110开发环境版本...'
sh 'pnpm build:web'
}
}
stage('部署') {
steps {
echo "🚀 部署到110开发环境: ${DEPLOY_HOST}"
sshPublisher(
publishers: [
sshPublisherDesc(
configName: SSH_SERVER,
transfers: [
sshTransfer(
sourceFiles: "${BUILD_DIR}/**",
removePrefix: BUILD_DIR,
remoteDirectory: APP_NAME,
execCommand: """powershell -NoProfile -Command "Remove-Item '${DEPLOY_DIR}' -Recurse -Force -ErrorAction SilentlyContinue; Move-Item '${REMOTE_DIR}/${APP_NAME}' '${DEPLOY_DIR}'"
"""
)
]
)
]
)
}
}
}
post {
success {
echo '✅ 110开发环境部署成功'
script {
def branch = env.BRANCH_NAME ?: env.GIT_BRANCH ?: 'unknown'
def timestamp = new Date().format('yyyy-MM-dd HH:mm:ss', TimeZone.getTimeZone('Asia/Shanghai'))
def content = """## 🚀 DCS 画布编辑器 自动部署通知<font color=\\"warning\\"> 110开发环境</font>
**部署时间**: ${timestamp}
**部署项目**: **${PROJECT_NAME}**
**分支**: ${branch}
**构建编号**: #${env.BUILD_NUMBER}
> Jenkins 自动构建部署完成"""
sh """
curl -s -X POST \
-H 'Content-Type: application/json' \
-d '{"msgtype":"markdown","markdown":{"content":"${content}"}}' \
'${WEBHOOK_URL}' || true
"""
}
}
failure {
echo '❌ 110开发环境部署失败'
script {
def branch = env.BRANCH_NAME ?: env.GIT_BRANCH ?: 'unknown'
def timestamp = new Date().format('yyyy-MM-dd HH:mm:ss', TimeZone.getTimeZone('Asia/Shanghai'))
def content = """## ❌ DCS 画布编辑器 部署失败通知<font color=\\"warning\\"> 110开发环境</font>
**时间**: ${timestamp}
**项目**: **${PROJECT_NAME}**
**分支**: ${branch}
**构建编号**: [#${env.BUILD_NUMBER}](${env.BUILD_URL})
> 请检查 Jenkins 构建日志"""
sh """
curl -s -X POST \
-H 'Content-Type: application/json' \
-d '{"msgtype":"markdown","markdown":{"content":"${content}"}}' \
'${WEBHOOK_URL}' || true
"""
}
}
always {
// 清理构建产物
sh "rm -rf ${BUILD_DIR}"
}
}
}

155
Jenkinsfile.dev139 Normal file
View File

@@ -0,0 +1,155 @@
pipeline {
agent any
environment {
// 项目信息
PROJECT_NAME = 'DCS 画布编辑器'
APP_NAME = 'cslab-dcs-web'
BUILD_DIR = 'apps/web/dist'
// 部署目标 - 139测试环境 (内网 139)
SSH_SERVER = 'ssh-test-139'
DEPLOY_HOST = '192.168.1.139'
REMOTE_DIR = '/root/jenkins-dir'
DEPLOY_BASE = '/root/chemlab/www/chemlab'
DEPLOY_DIR = "${DEPLOY_BASE}/${APP_NAME}"
// 企业微信 Webhook从 Jenkins 凭据读取)
WEBHOOK_URL = credentials('webhook-url')
// Node 版本(需在 Jenkins 全局工具配置中添加同名 NodeJS 安装)
NODE_HOME = tool(name: 'NodeJS-22', type: 'nodejs')
PATH = "${NODE_HOME}/bin:${env.PATH}"
}
options {
timeout(time: 15, unit: 'MINUTES')
disableConcurrentBuilds()
buildDiscarder(logRotator(numToKeepStr: '10'))
}
stages {
stage('环境检查') {
steps {
echo "📋 项目: ${PROJECT_NAME}"
echo "🌿 分支: ${env.BRANCH_NAME ?: env.GIT_BRANCH ?: 'unknown'}"
echo "🔧 环境: 139测试 (${DEPLOY_HOST})"
sh '''
echo "Node 版本: $(node -v)"
echo "pnpm 版本: $(pnpm -v)"
'''
}
}
stage('安装依赖') {
steps {
echo '📦 安装项目依赖...'
sh 'ELECTRON_SKIP_BINARY_DOWNLOAD=1 pnpm install'
}
}
stage('构建') {
steps {
echo '🔨 构建139测试环境版本...'
sh 'pnpm build:web'
}
}
stage('部署') {
steps {
echo "🚀 部署到139测试环境: ${DEPLOY_HOST}"
sshPublisher(
publishers: [
sshPublisherDesc(
configName: SSH_SERVER,
transfers: [
sshTransfer(
sourceFiles: "${BUILD_DIR}/**",
removePrefix: BUILD_DIR,
remoteDirectory: APP_NAME,
execCommand: """
TS=\$(date +%Y%m%d%H%M%S)
# 备份旧版本
if [ -d "${DEPLOY_DIR}" ]; then
mv "${DEPLOY_DIR}" "${DEPLOY_DIR}.backup.\$TS"
fi
mkdir -p "${DEPLOY_BASE}"
# 移动到部署目录
mv "${REMOTE_DIR}/${APP_NAME}" "${DEPLOY_DIR}"
# 设置权限
chmod -R 755 "${DEPLOY_DIR}"
# 清理超过 7 天的旧备份
find "${DEPLOY_BASE}" -maxdepth 1 -name "${APP_NAME}.backup.*" -mtime +7 -exec rm -rf {} +
"""
)
]
)
]
)
}
}
}
post {
success {
echo '✅ 139测试环境部署成功'
script {
def branch = env.BRANCH_NAME ?: env.GIT_BRANCH ?: 'unknown'
def timestamp = new Date().format('yyyy-MM-dd HH:mm:ss', TimeZone.getTimeZone('Asia/Shanghai'))
def content = """## 🚀 DCS 画布编辑器 自动部署通知<font color=\\"warning\\"> 139测试环境</font>
**部署时间**: ${timestamp}
**部署项目**: **${PROJECT_NAME}**
**分支**: ${branch}
**构建编号**: #${env.BUILD_NUMBER}
> Jenkins 自动构建部署完成"""
sh """
curl -s -X POST \
-H 'Content-Type: application/json' \
-d '{"msgtype":"markdown","markdown":{"content":"${content}"}}' \
'${WEBHOOK_URL}' || true
"""
}
}
failure {
echo '❌ 139测试环境部署失败'
script {
def branch = env.BRANCH_NAME ?: env.GIT_BRANCH ?: 'unknown'
def timestamp = new Date().format('yyyy-MM-dd HH:mm:ss', TimeZone.getTimeZone('Asia/Shanghai'))
def content = """## ❌ DCS 画布编辑器 部署失败通知<font color=\\"warning\\"> 139测试环境</font>
**时间**: ${timestamp}
**项目**: **${PROJECT_NAME}**
**分支**: ${branch}
**构建编号**: [#${env.BUILD_NUMBER}](${env.BUILD_URL})
> 请检查 Jenkins 构建日志"""
sh """
curl -s -X POST \
-H 'Content-Type: application/json' \
-d '{"msgtype":"markdown","markdown":{"content":"${content}"}}' \
'${WEBHOOK_URL}' || true
"""
}
}
always {
// 清理构建产物
sh "rm -rf ${BUILD_DIR}"
}
}
}

463
README.md Normal file
View File

@@ -0,0 +1,463 @@
# CSLAB-DCS-Web
**DCS分布式控制系统跨平台可视化组态编辑器**
面向工业控制场景的画布组态设计、数据绑定、实时仿真与运行监控平台,支持 Web、Electron、Tauri 三端部署。
---
## 目录
- [项目简介](#项目简介)
- [技术栈](#技术栈)
- [项目结构](#项目结构)
- [环境要求](#环境要求)
- [快速开始](#快速开始)
- [开发命令](#开发命令)
- [构建与部署](#构建与部署)
- [核心架构](#核心架构)
- [功能模块](#功能模块)
- [数据模型](#数据模型)
- [API 层](#api-层)
- [测试](#测试)
- [CI/CD](#cicd)
---
## 项目简介
CSLAB-DCS-Web 是一个 **工业分布式控制系统可视化组态编辑器**,采用 pnpm monorepo 架构。用户可在画布上拖放、编排工业控制组件(如 PID 控制器、阀门控制器、仪表等),配置数据绑定与表达式,并通过 WebSocket 实现运行时的实时数据推送和仿真监控。
### 核心能力
- **画布组态设计**:基于可视化画布的工业控制界面设计,支持多图层、组件拖放、对齐辅助线、缩放漫游
- **丰富组件库**矩形、文本、数字显示、柱状图、按钮、PID 控制器、阀门控制器、画布切换器等
- **数据绑定与表达式**支持将组件属性绑定到运行时变量内置表达式引擎tokenizer → parser → evaluator
- **条件样式**:根据运行时条件动态切换组件样式
- **事件与动作**:组件支持 click/dblclick 事件触发,执行自定义动作
- **实时运行监控**:通过 WebSocket 连接运行时服务RAF 帧批处理数据更新
- **仿真执行**:对接 chemical-chaos 微服务,支持任务创建、启停、暂停/恢复
- **导入导出**:画布/项目的导入导出Zod schema 校验数据完整性
- **跨平台**Web 浏览器 / Electron 桌面 / Tauri 轻量桌面三端统一
---
## 技术栈
| 类别 | 技术 |
|------|------|
| **框架** | Vue 3 (Composition API + `<script setup>`) |
| **路由** | Vue Router 4Hash 模式,兼容桌面端) |
| **状态管理** | Pinia 3 |
| **UI 组件库** | Element Plus中文本地化 |
| **原子化 CSS** | UnoCSS |
| **组合式工具** | VueUse |
| **HTTP 客户端** | ofetch |
| **Schema 校验** | Zod |
| **构建工具** | Vite 6 / electron-vite |
| **桌面框架** | Electron 22 / Tauri 2 |
| **包管理器** | pnpmworkspace |
| **TypeScript** | 5.9+(严格模式) |
| **代码规范** | ESLint + @antfu/eslint-config |
| **单元测试** | Vitest + @vue/test-utils + happy-dom |
| **E2E 测试** | PlaywrightChromium |
| **样式预处理** | SCSS |
| **图标** | FontAwesome 7 |
---
## 项目结构
```
cslab-dcs-web/
├── apps/ # 应用入口层
│ ├── web/ # Web 浏览器版
│ ├── electron/ # Electron 桌面版
│ └── tauri/ # Tauri 桌面版
├── packages/ # 共享包层
│ ├── core/ # 核心业务逻辑与 UI
│ │ └── src/
│ │ ├── api/ # API 服务层
│ │ ├── assets/ # 静态资源(字体、图片、样式)
│ │ ├── bootstrap/ # 应用初始化Bridge、编辑器、路由状态同步
│ │ ├── components/ # 组件库
│ │ │ └── editor/ # 编辑器组件
│ │ │ ├── canvas/ # 画布引擎Stage、图层面板、属性面板、组件面板
│ │ │ ├── components/# 可渲染组件rect、text、number、bar、button、PID…
│ │ │ ├── controls/ # UI 控件(输入框、颜色选择器、开关…)
│ │ │ ├── header/ # 顶部工具栏
│ │ │ └── ... # 运行时控制台、状态栏等
│ │ ├── composables/ # 组合式函数
│ │ ├── config/ # 运行时配置
│ │ ├── constants/ # 常量定义
│ │ ├── layout/ # 布局组件
│ │ ├── request/ # 请求层HTTP / IndexedDB 双适配器)
│ │ ├── router/ # 路由配置
│ │ ├── stores/ # Pinia 状态管理
│ │ ├── utils/ # 工具函数
│ │ └── views/ # 页面视图
│ │
│ ├── bridge/ # 平台抽象桥接层
│ │ └── src/
│ │ ├── adapters/ # Web / Electron / Tauri 适配器
│ │ └── types.ts # IPlatformBridge 接口定义
│ │
│ └── schema/ # 数据模型与校验
│ └── src/
│ ├── dcs.ts # DCS 配置类型
│ ├── types.ts # 通用类型
│ └── validators.ts # Zod 校验器
├── tests/ # 测试
│ ├── e2e/ # Playwright E2E 测试
│ └── unit/ # Vitest 单元测试
├── references/ # 参考文档与竞品截图
├── eslint.config.js
├── vitest.config.ts
├── playwright.config.ts
├── tsconfig.base.json
├── pnpm-workspace.yaml
└── package.json
```
### 包职责说明
| 包名 | 描述 |
|------|------|
| `@cslab-dcs/core` | 核心业务层——路由、Store、API、视图、组件、组合式函数是应用的主体 |
| `@cslab-dcs/bridge` | 平台抽象层——定义 `IPlatformBridge` 接口,提供文件操作、对话框、系统信息的跨平台实现 |
| `@cslab-dcs/schema` | 数据模型层——Canvas、Layer、Component、Variable 的 Zod Schema 定义与校验 |
| `@cslab-dcs/web` | Web 入口——挂载 Vue 应用到浏览器 |
| `@cslab-dcs/electron` | Electron 入口——主进程 IPC、窗口管理、文件授权 |
| `@cslab-dcs/tauri` | Tauri 入口——Tauri Plugin API 绑定 |
---
## 环境要求
| 依赖 | 版本 |
|------|------|
| Node.js | >= 20.0.0 |
| pnpm | >= 9.0.0 |
| Rust仅 Tauri | latest stable |
---
## 快速开始
```bash
# 1. 安装依赖
pnpm install
# 2. 启动 Web 开发服务
pnpm dev:web
# 访问 http://localhost:5173/dcs-web#/
# 3. 或启动 Electron 桌面版
pnpm dev:electron
# 4. 或启动 Tauri 桌面版
pnpm dev:tauri
```
---
## 开发命令
### 开发
| 命令 | 说明 |
|------|------|
| `pnpm dev:web` | 启动 Web 版开发服务器(端口 5173 |
| `pnpm dev:electron` | 启动 Electron 桌面版 |
| `pnpm dev:tauri` | 启动 Tauri 桌面版 |
### 构建
| 命令 | 说明 |
|------|------|
| `pnpm build:web` | 构建 Web 生产包 |
| `pnpm build:electron:win` | 构建 Windows Electron 安装包 |
| `pnpm build:tauri` | 构建 Tauri 跨平台安装包 |
### 质量保障
| 命令 | 说明 |
|------|------|
| `pnpm format` | ESLint 自动修复 |
| `pnpm typecheck` | 全包 TypeScript 类型检查 |
| `pnpm test` | 运行单元测试 |
| `pnpm test:watch` | 监听模式单元测试 |
| `pnpm test:coverage` | 生成覆盖率报告 |
| `pnpm test:e2e` | 运行 E2E 测试 |
| `pnpm test:e2e:ui` | E2E 测试(带 UI |
---
## 构建与部署
### Web 构建产物
```bash
pnpm build:web
# 输出目录apps/web/dist/
```
构建产物为纯静态资源HTML/JS/CSS可直接部署到任意 Web 服务器或 CDN。
### 环境变量
| 变量 | 说明 |
|------|------|
| `VITE_API_BASE_URL` | 后端 API 地址 |
| `VITE_BASE_URL` | 前端部署路径 |
---
## 核心架构
### 分层架构
```
┌────────────────────────────────────────────┐
│ Views页面视图
├────────────────────────────────────────────┤
│ ComponentsUI 组件层) │
├────────────────────────────────────────────┤
│ Composables组合式函数层
├────────────────────────────────────────────┤
│ Pinia Stores状态管理层
├────────────────────────────────────────────┤
│ API Layer服务接口层
├────────────────────────────────────────────┤
│ Request AdaptersHTTP / IndexedDB 适配) │
├────────────────────────────────────────────┤
│ Platform BridgeWeb / Electron / Tauri
└────────────────────────────────────────────┘
```
### 路由
| 路由 | 页面 | 说明 |
|------|------|------|
| `/canvases` | 画布列表 | 项目下的画布管理 |
| `/editor` | 画布编辑器 | 核心组态编辑界面 |
| `/preview` | 预览模式 | 只读画布预览 |
| `/runtime` | 运行模式 | 实时仿真与监控 |
| `/settings` | 设置 | 桌面端专属 |
| `/welcome` | 欢迎页 | 桌面端专属 |
### 状态管理Pinia Stores
| Store | 职责 | 持久化 |
|-------|------|--------|
| `useAppStore` | 主题、平台信息、应用版本 | — |
| `useUserStore` | JWT Token、设备类型、用户信息 | localStorage |
| `useProjectStore` | 项目列表、当前项目、最近项目 | sessionStorage |
| `useCanvasStore` | 画布列表、缩放级别、视口状态 | sessionStorage |
| `useLayerStore` | 选中图层、图层列表、基础图层 | sessionStorage |
### 平台桥接Bridge
```typescript
interface IPlatformBridge {
platform: 'web' | 'electron' | 'tauri'
file: IFileService // 读写文件、打开/保存对话框
dialog: IDialogService // 消息弹窗、确认弹窗
system: ISystemService // 应用版本、平台信息、打开外部链接
}
```
三端适配器各自实现该接口,上层业务代码无需关心平台差异。
---
## 功能模块
### 画布编辑器
- **画布引擎**:支持缩放(最大 300%、平移、网格对齐10px
- **画布尺寸**:默认 1920×1080最小 360×240最大 20000×20000
- **图层管理**:多选、分组、排序、锁定
- **属性面板**:根据选中组件类型动态展示配置项
- **对齐辅助线**:智能吸附对齐
- **Minimap**:画布缩略图导航
- **撤销/重做**:完整的操作历史记录
- **自动保存**:编辑内容自动持久化
### 可渲染组件
| 组件 | 说明 |
|------|------|
| `rect` | 矩形/容器 |
| `text` | 文本标签 |
| `number` | 数字显示 |
| `bar` | 柱状图 |
| `button` | 可交互按钮 |
| `pidController` | PID 控制器面板 |
| `valveController` | 阀门控制器 |
| `canvasSwitcher` | 画布切换导航 |
| `custom` | 自定义组件 |
### 数据绑定与表达式引擎
组件属性支持绑定到运行时变量,内置完整的表达式解析引擎:
```
表达式字符串 → Tokenizer词法分析→ Parser语法分析→ Evaluator求值
```
支持条件样式:根据表达式结果动态切换组件外观,支持优先级配置。
### 运行时监控
- WebSocket 实时数据推送
- RequestAnimationFrame 帧批处理,保证渲染性能
- 运行时控制台输出
- 手动控制面板
- PID 参数调节面板
- 阀门开度控制面板
### 组合式函数Composables
| 函数 | 职责 |
|------|------|
| `useBridge()` | 获取平台桥接实例 |
| `useOperationLog()` | 操作日志记录(上限 500 条) |
| `useRuntimeSocket()` | WebSocket 运行时连接与帧批更新 |
| `useDataConnection()` | 数据连接管理与写入命令 |
| `useExportImport()` | 画布/项目导入导出 |
| `useRuntimeConsole()` | 运行时控制台日志 |
| `useComponentTemplates()` | 组件模板存取localStorage |
---
## 数据模型
### Canvas画布
```typescript
{
id: string
projectId: string
name: string
description?: string
width: number // 默认 1920
height: number // 默认 1080
type: CanvasType
version: string
components: Layer[] // 组件/图层列表
vars?: Variable[] // 运行时变量
config?: CanvasConfig
thumbnail?: string // 缩略图
updatedAt: string
}
```
### Layer图层/组件)
```typescript
{
id: string // UUID
type: ComponentType // rect | number | text | bar | button | ...
x: number // X 坐标
y: number // Y 坐标
width: number
height: number
config?: object // 组件特定配置
style?: object // CSS 样式
bindings?: Record<string, string | BindingExpression> // 数据绑定
tagNumber?: string // 设备标签号
events?: EventConfig[] // 事件处理器
conditionalStyles?: ConditionalStyle[] // 条件样式
}
```
所有数据模型均使用 Zod Schema 定义,提供运行时校验和 TypeScript 类型推导。
---
## API 层
### 请求架构
采用 **Repository 模式** + **双适配器**
- **HTTP 适配器** → 对接 `/cslab-server` 后端
- **IndexedDB 适配器** → 浏览器本地存储(离线模式)
HTTP 客户端基于 `ofetch`,支持 JWT Token 自动刷新、401 会话过期处理、错误提示。
### API 模块
| 模块 | 接口 |
|------|------|
| **Project** | `getProjectListApi()` / `createProjectApi()` / `updateProjectApi()` |
| **Canvas** | `listCanvasesApi()` / `getCanvasByIdApi()` / `createCanvasApi()` / `updateCanvasApi()` / `saveCanvasComponentsApi()` / `duplicateCanvasApi()` |
| **Runtime** | `addJobApi()` / `stopJobApi()` / `pauseJobApi()` / `resumeJobApi()` / `getProjectJobApi()` / `pushFullDataApi()` / `getExecSequenceApi()` |
| **Dynamic Project** | 动态项目更新 |
### 运行时 RPC
对接 `chemical-chaos` 微服务,通过 `/v1/rpc/common/``/v1/rpc/zero_rpc/` 端点进行仿真调度。
---
## 测试
### 单元测试
- **框架**Vitest + happy-dom + @vue/test-utils
- **位置**`tests/unit/`
- **覆盖范围**Composables、Stores、Schema 校验、表达式引擎
```bash
pnpm test # 运行全部单元测试
pnpm test:watch # 监听模式
pnpm test:coverage # 覆盖率报告
```
### E2E 测试
- **框架**PlaywrightChromium
- **位置**`tests/e2e/`
- **测试场景**:画布编辑器操作、图层交互、页面导航
```bash
pnpm test:e2e # 运行 E2E 测试
pnpm test:e2e:ui # 带 UI 的 E2E 测试
```
---
## CI/CD
项目配置了 Jenkins Pipeline支持两套部署环境
| 环境 | 配置文件 | 目标 |
|------|----------|------|
| 开发环境110 | `Jenkinsfile.dev110` | Windows 服务器部署 |
| 测试环境139 | `Jenkinsfile.dev139` | Linux 服务器部署 |
### Pipeline 流程
1. **环境检查** → 验证 Node 22 / pnpm 版本
2. **安装依赖**`pnpm install`
3. **构建**`pnpm build:web`
4. **部署** → SSH 拷贝静态资源到目标服务器
5. **通知** → 企业微信 Webhook 通知构建结果
### 部署要点
- 构建产物:`apps/web/dist/` 下的静态文件
- 构建超时15 分钟
- 跳过 Electron 二进制下载:`ELECTRON_SKIP_BINARY_DOWNLOAD=1`
- 测试环境支持旧版本自动清理(>7 天)
---
## License
Private — 内部项目

View File

@@ -0,0 +1,3 @@
{
"globals": {}
}

View File

@@ -0,0 +1,29 @@
appId: com.cslab.dcs.editor
productName: cslab-dcs-editor
directories:
output: dist
buildResources: resources
files:
- out/**/*
- '!**/*.map'
extraMetadata:
main: out/main/index.js
nsis:
oneClick: false
perMachine: false
allowToChangeInstallationDirectory: true
deleteAppDataOnUninstall: false
mac:
target: dmg
hardenedRuntime: true
gatekeeperAssess: false
win:
icon: resources/icon.ico
target:
- target: nsis
arch:
- x64
- ia32
artifactName: '${productName}_${version}_win7-setup.${ext}'
linux:
target: AppImage

View File

@@ -0,0 +1,28 @@
import { resolve } from 'node:path'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import { createSharedViteConfig } from '../../packages/core/vite.shared'
// Core 包的根目录
const coreRoot = resolve(__dirname, '../../packages/core')
// 使用 any 断言绕过 Vite 版本差异electron: vite@5, core: vite@6
const sharedConfig = createSharedViteConfig(coreRoot) as any
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
},
preload: {
plugins: [externalizeDepsPlugin()],
},
renderer: {
...sharedConfig,
root: resolve(__dirname, 'src/renderer'),
build: {
rollupOptions: {
input: {
index: resolve(__dirname, 'src/renderer/index.html'),
},
},
},
},
})

View File

@@ -0,0 +1,36 @@
{
"name": "@cslab-dcs/electron",
"version": "1.0.0",
"description": "DCS Editor Electron App",
"author": "cslab-dcs",
"main": "./out/main/index.js",
"scripts": {
"lint": "eslint .",
"typecheck": "tsc --noEmit",
"dev": "electron-vite dev",
"build": "electron-vite build",
"postinstall": "electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "npm run build && electron-builder --mac",
"build:linux": "npm run build && electron-builder --linux"
},
"dependencies": {
"@cslab-dcs/bridge": "workspace:*",
"@cslab-dcs/core": "workspace:*",
"@electron-toolkit/preload": "2.0.0",
"@electron-toolkit/utils": "2.0.0",
"electron-updater": "6.3.9"
},
"devDependencies": {
"@vitejs/plugin-vue": "6.0.1",
"@vitejs/plugin-vue-jsx": "4.1.1",
"electron": "22.3.27",
"electron-builder": "25.1.8",
"electron-vite": "2.3.0",
"typescript": "5.9.3",
"vite": "5.4.11",
"vue": "3.5.13",
"vue-tsc": "2.2.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

1
apps/electron/src/main/env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="electron-vite/node" />

View File

@@ -0,0 +1,201 @@
/* eslint-disable node/prefer-global/process */
import fs from 'node:fs/promises'
import os from 'node:os'
import { join } from 'node:path'
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron'
import icon from '../../resources/icon.png?asset'
function createWindow(): void {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload: join(__dirname, '../preload/index.js'),
sandbox: true,
contextIsolation: true,
nodeIntegration: false,
},
})
mainWindow.on('ready-to-show', () => {
mainWindow.show()
})
mainWindow.webContents.setWindowOpenHandler((details) => {
if (isUrlAllowed(details.url)) {
shell.openExternal(details.url)
}
return { action: 'deny' }
})
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL)
}
else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
}
}
// ── 文件路径授权管理 ──
// 只允许访问经用户通过原生对话框选择的文件路径
const authorizedPaths = new Set<string>()
const MAX_AUTHORIZED_PATHS = 200
function authorizePath(filePath: string) {
if (authorizedPaths.size >= MAX_AUTHORIZED_PATHS) {
const oldest = authorizedPaths.values().next().value
if (oldest)
authorizedPaths.delete(oldest)
}
authorizedPaths.add(filePath)
}
function isPathAuthorized(filePath: string): boolean {
return authorizedPaths.has(filePath)
}
// ── URL 协议白名单 ──
const ALLOWED_URL_PROTOCOLS = new Set(['https:', 'http:'])
function isUrlAllowed(url: string): boolean {
try {
return ALLOWED_URL_PROTOCOLS.has(new URL(url).protocol)
}
catch {
return false
}
}
// IPC Handlers
function registerIpcHandlers() {
// File — 仅允许经对话框授权的路径
ipcMain.handle('file:read', async (_, path) => {
if (!isPathAuthorized(path)) {
throw new Error('文件访问被拒绝:路径未经用户选择授权')
}
return await fs.readFile(path, 'utf-8')
})
ipcMain.handle('file:exists', async (_, path) => {
if (!isPathAuthorized(path)) {
return false
}
try {
await fs.access(path)
return true
}
catch {
return false
}
})
ipcMain.handle('file:write', async (_, path, content) => {
if (!isPathAuthorized(path)) {
throw new Error('文件写入被拒绝:路径未经用户选择授权')
}
await fs.writeFile(path, content, 'utf-8')
})
// Dialog — 记录用户选择的路径
ipcMain.handle('dialog:open', async (_, options) => {
const { filePaths } = await dialog.showOpenDialog({
title: options?.title,
defaultPath: options?.defaultPath,
filters: options?.filters,
properties: [
options?.multiple ? 'multiSelections' : 'openFile',
options?.directory ? 'openDirectory' : 'openFile',
],
})
filePaths.forEach(p => authorizePath(p))
return options?.multiple ? filePaths : filePaths[0] || null
})
ipcMain.handle('dialog:save', async (_, options) => {
const { filePath } = await dialog.showSaveDialog({
title: options?.title,
defaultPath: options?.defaultPath,
filters: options?.filters,
})
if (filePath)
authorizePath(filePath)
return filePath || null
})
ipcMain.handle('dialog:message', async (_, options) => {
await dialog.showMessageBox({
title: options?.title,
message: options?.message,
type: options?.type || 'info',
})
})
ipcMain.handle('dialog:confirm', async (_, options) => {
const { response } = await dialog.showMessageBox({
title: options?.title,
message: options?.message,
type: options?.type || 'info',
buttons: [options?.okLabel || 'Yes', options?.cancelLabel || 'No'],
defaultId: 0,
cancelId: 1,
})
return response === 0
})
// System
ipcMain.handle('app:version', () => app.getVersion())
ipcMain.handle('app:info', () => ({
name: 'electron',
version: process.versions.electron,
os: process.platform,
osVersion: os.release(),
arch: process.arch,
}))
ipcMain.handle('shell:open', (_, url) => {
if (!isUrlAllowed(url)) {
throw new Error('不允许打开此类型的链接,仅支持 HTTP/HTTPS')
}
return shell.openExternal(url)
})
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
// Set app user model id for windows
electronApp.setAppUserModelId('com.cslab.dcs')
// Default open or close DevTools by F12 in development
// and ignore CommandOrControl + R in production.
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
app.on('browser-window-created', (_, window) => {
optimizer.watchWindowShortcuts(window)
})
registerIpcHandlers()
createWindow()
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0)
createWindow()
})
})
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})

8
apps/electron/src/preload/index.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
import type { ElectronAPI } from '@electron-toolkit/preload'
declare global {
interface Window {
electron: ElectronAPI
api: Record<string, unknown>
}
}

View File

@@ -0,0 +1,22 @@
/* eslint-disable node/prefer-global/process */
import { electronAPI } from '@electron-toolkit/preload'
import { contextBridge } from 'electron'
// Custom APIs for renderer
const api = {}
// Use `contextBridge` APIs to expose IPC renderer to the renderer process.
// Read more at https://www.electronjs.org/docs/latest/tutorial/context-isolation
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)
contextBridge.exposeInMainWorld('api', api)
}
catch (error) {
console.error(error)
}
}
else {
window.electron = electronAPI
window.api = api
}

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>DCS Editor (Electron)</title>
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:"> -->
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

17
apps/electron/src/renderer/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
/// <reference types="vite/client" />
/// <reference types="element-plus/global" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<object, object, any>
export default component
}
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

View File

@@ -0,0 +1,5 @@
import { bridge } from '@cslab-dcs/bridge'
import { createDCSApp } from '@cslab-dcs/core'
const app = createDCSApp(bridge)
app.mount('#app')

View File

@@ -0,0 +1,8 @@
{
"references": [
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.preload.json" },
{ "path": "./tsconfig.web.json" }
],
"files": []
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"types": ["electron-vite/node"],
"outDir": "out"
},
"include": ["src/main/**/*"],
"exclude": ["node_modules", "out", "dist"]
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"types": ["electron-vite/node"],
"outDir": "out"
},
"include": ["src/preload/**/*"],
"exclude": ["node_modules", "out", "dist"]
}

View File

@@ -0,0 +1,23 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"paths": {
"@/*": ["../../packages/core/src/*"],
"@cslab-dcs/core": ["../../packages/core/src"],
"@cslab-dcs/bridge": ["../../packages/bridge/src"],
"@cslab-dcs/schema": ["../../packages/schema/src"]
},
"types": ["element-plus/global"]
},
"include": [
"src/renderer/**/*",
"../../packages/core/src/**/*",
"../../packages/bridge/src/**/*",
"../../packages/request/src/**/*",
"../../packages/schema/src/**/*",
"../../packages/utils/src/**/*"
],
"exclude": ["node_modules", "out", "dist"]
}

View File

@@ -0,0 +1,3 @@
{
"globals": {}
}

13
apps/tauri/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DCS Editor (Tauri)</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

30
apps/tauri/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "@cslab-dcs/tauri",
"type": "module",
"version": "1.0.0",
"private": true,
"scripts": {
"dev:web": "vite dev",
"dev": "tauri dev",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"tauri": "tauri"
},
"dependencies": {
"@cslab-dcs/bridge": "workspace:*",
"@cslab-dcs/core": "workspace:*",
"@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-opener": "^2.2.5",
"vue": "^3.5.13"
},
"devDependencies": {
"@tauri-apps/cli": "2.10.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"typescript": "^5.9.3",
"vite": "^6.0.3",
"vue-tsc": "^2.2.0"
}
}

5295
apps/tauri/src-tauri/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,31 @@
[package]
name = "cslab-dcs-editor"
version = "1.0.0"
description = "DCS Editor - CSLAB DCS 编辑器"
authors = [ "cslab" ]
edition = "2021"
[lib]
name = "cslab_dcs_tauri_lib"
crate-type = [
"staticlib",
"cdylib",
"rlib"
]
[build-dependencies]
tauri-build = { version = "2.5.4", features = [] }
[dependencies]
tauri = { version = "2.10.0", features = [] }
tauri-plugin-opener = "2.2.5"
tauri-plugin-dialog = "2.2.0"
tauri-plugin-fs = "2.2.0"
tauri-plugin-os = "2"
serde = { version = "1", features = [ "derive" ] }
serde_json = "1"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = [ "tauri/custom-protocol" ]

View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:default",
"opener:default",
"fs:default",
"os:default"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

View File

@@ -0,0 +1,10 @@
// #[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_os::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
cslab_dcs_tauri_lib::run()
}

View File

@@ -0,0 +1,40 @@
{
"identifier": "com.cslab.dcs.editor",
"build": {
"beforeDevCommand": "pnpm dev:web",
"beforeBuildCommand": "pnpm build:web",
"devUrl": "http://localhost:1420",
"frontendDist": "../dist"
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
},
"app": {
"withGlobalTauri": false,
"windows": [
{
"title": "DCS Editor",
"width": 800,
"height": 600,
"dragDropEnabled": false
}
],
"security": {
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self' https:; font-src 'self' data:"
}
},
"plugins": {
"fs": {}
}
}

9
apps/tauri/src/main.ts Normal file
View File

@@ -0,0 +1,9 @@
import { WebBridge } from '@cslab-dcs/bridge'
import { TauriBridge } from '@cslab-dcs/bridge/tauri'
import { createDCSApp } from '@cslab-dcs/core'
const isTauri = !!(window as any).__TAURI_INTERNALS__ || !!(window as any).__TAURI__
const bridge = isTauri ? new TauriBridge() : new WebBridge()
const app = createDCSApp(bridge)
app.mount('#app')

23
apps/tauri/tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["../../packages/core/src/*"],
"@cslab-dcs/core": ["../../packages/core/src"],
"@cslab-dcs/bridge": ["../../packages/bridge/src"],
"@cslab-dcs/schema": ["../../packages/schema/src"]
},
"types": ["element-plus/global"]
},
"include": [
"src/**/*",
"../../packages/core/src/**/*",
"../../packages/core/env.d.ts",
"../../packages/bridge/src/**/*",
"../../packages/request/src/**/*",
"../../packages/schema/src/**/*",
"../../packages/utils/src/**/*"
],
"exclude": ["node_modules", "dist", "src-tauri"]
}

36
apps/tauri/vite.config.ts Normal file
View File

@@ -0,0 +1,36 @@
import type { UserConfig } from 'vite'
import { resolve } from 'node:path'
import process from 'node:process'
import { defineConfig } from 'vite'
import { createSharedViteConfig } from '../../packages/core/vite.shared'
const coreRoot = resolve(__dirname, '../../packages/core')
const host = process.env.VITE_DEV_SERVER_HOST
// 使用类型断言绕过 pnpm 导致的 Vite 依赖重复安装类型不兼容问题
export default defineConfig({
...createSharedViteConfig(coreRoot) as UserConfig,
root: __dirname,
clearScreen: false,
server: {
port: 1420,
strictPort: true,
host: host || false,
hmr: host
? {
protocol: 'ws',
host,
port: 1421,
}
: undefined,
watch: {
// 3. tell vite to ignore watching `src-tauri`
ignored: ['**/src-tauri/**'],
},
},
build: {
outDir: 'dist',
emptyOutDir: true,
},
})

3
apps/web/.env Normal file
View File

@@ -0,0 +1,3 @@
# 默认环境变量配置(会被 .env.development 和 .env.production 覆盖)
# VITE_API_BASE_URL=https://cslab.oberyun.com
VITE_API_BASE_URL=http://192.168.1.110:8001

View File

@@ -0,0 +1,2 @@
# 开发环境配置
VITE_API_BASE_URL=http://192.168.1.110:8001

2
apps/web/.env.production Normal file
View File

@@ -0,0 +1,2 @@
# 生产环境配置
VITE_API_BASE_URL=https://cslab.oberyun.com

View File

@@ -0,0 +1,3 @@
{
"globals": {}
}

13
apps/web/index.html Normal file
View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>DCS Editor (Web)</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

24
apps/web/package.json Normal file
View File

@@ -0,0 +1,24 @@
{
"name": "@cslab-dcs/web",
"type": "module",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vite --host",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"@cslab-dcs/bridge": "workspace:*",
"@cslab-dcs/core": "workspace:*",
"vue": "^3.5.13"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"typescript": "^5.9.3",
"vite": "^6.0.3",
"vue-tsc": "^2.2.0"
}
}

16
apps/web/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<object, object, any>
export default component
}
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}

5
apps/web/src/main.ts Normal file
View File

@@ -0,0 +1,5 @@
import { bridge } from '@cslab-dcs/bridge'
import { createDCSApp } from '@cslab-dcs/core'
const app = createDCSApp(bridge)
app.mount('#app')

24
apps/web/tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["../../packages/core/src/*"],
"@cslab-dcs/core": ["../../packages/core/src"],
"@cslab-dcs/bridge": ["../../packages/bridge/src"],
"@cslab-dcs/schema": ["../../packages/schema/src"]
},
"types": ["element-plus/global"]
},
"include": [
"src/**/*",
"../../packages/core/src/**/*",
"../../packages/core/src/auto-imports.d.ts",
"../../packages/core/env.d.ts",
"../../packages/bridge/src/**/*",
"../../packages/request/src/**/*",
"../../packages/schema/src/**/*",
"../../packages/utils/src/**/*"
],
"exclude": ["node_modules", "dist"]
}

31
apps/web/unocss.config.ts Normal file
View File

@@ -0,0 +1,31 @@
import {
defineConfig,
presetAttributify,
presetIcons,
presetTypography,
presetUno,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
import presetChinese from 'unocss-preset-chinese'
import presetEase from 'unocss-preset-ease'
export default defineConfig({
presets: [
presetUno(),
presetAttributify(),
presetChinese(),
presetEase(),
presetTypography(),
presetIcons({
scale: 1.2,
warn: true,
}),
],
shortcuts: [
['flex-center', 'flex items-center justify-center'],
['flex-between', 'flex items-center justify-between'],
['flex-end', 'flex items-end justify-between'],
],
transformers: [transformerDirectives(), transformerVariantGroup()],
})

25
apps/web/vite.config.ts Normal file
View File

@@ -0,0 +1,25 @@
import type { UserConfig } from 'vite'
import { resolve } from 'node:path'
import { defineConfig, loadEnv, mergeConfig } from 'vite'
import { createSharedViteConfig } from '../../packages/core/vite.shared'
// Core 包的根目录
const coreRoot = resolve(__dirname, '../../packages/core')
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, coreRoot, 'VITE_')
const sharedConfig = createSharedViteConfig(coreRoot, env) as UserConfig
return mergeConfig(sharedConfig, {
root: __dirname,
base: env.VITE_BASE_URL, // 确保相对路径,方便部署
build: {
outDir: 'dist',
emptyOutDir: true,
},
server: {
port: 5173,
},
})
})

3
docs/README.md Normal file
View File

@@ -0,0 +1,3 @@
# README
本目录下记录与AI的prompt和AI出的计划和实际操作等等。

8
eslint.config.js Normal file
View File

@@ -0,0 +1,8 @@
import antfu from '@antfu/eslint-config'
export default antfu({
rules: {
'no-console': 'off',
},
ignores: ['**/skills/**', 'research.md'],
})

39
package.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "cslab-dcs",
"type": "module",
"version": "1.0.0",
"private": true,
"packageManager": "pnpm@9.15.0",
"description": "DCS Editor - A cross-platform configuration editor",
"engines": {
"node": ">=20.0.0",
"pnpm": ">=9.0.0"
},
"scripts": {
"dev:web": "pnpm -F @cslab-dcs/web dev",
"dev:electron": "pnpm -F @cslab-dcs/electron dev",
"dev:tauri": "pnpm -F @cslab-dcs/tauri tauri dev",
"build:web": "pnpm -F @cslab-dcs/web build",
"build:electron:win": "pnpm -F @cslab-dcs/electron build:win",
"build:tauri": "pnpm -F @cslab-dcs/tauri tauri build",
"format": "eslint . --fix",
"typecheck": "pnpm -r typecheck",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"clean:all": "pnpm -r exec rm -rf dist out node_modules && rm -rf node_modules"
},
"devDependencies": {
"@antfu/eslint-config": "^7.2.0",
"@playwright/test": "^1.58.2",
"@types/node": "^22.19.7",
"@vitest/coverage-v8": "^4.1.0",
"@vue/test-utils": "^2.4.6",
"eslint": "^9.39.2",
"happy-dom": "^20.8.4",
"typescript": "^5.9.3",
"vitest": "^4.1.0"
}
}

View File

@@ -0,0 +1,32 @@
{
"name": "@cslab-dcs/bridge",
"type": "module",
"version": "1.0.0",
"private": true,
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
},
"./tauri": {
"types": "./src/tauri.ts",
"import": "./src/tauri.ts"
}
},
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@tauri-apps/api": "2.10.1",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-opener": "^2.2.5",
"@tauri-apps/plugin-os": "^2.3.2"
},
"devDependencies": {
"electron": "^33.2.1",
"typescript": "^5.9.3"
}
}

View File

@@ -0,0 +1,70 @@
import type {
ConfirmOptions,
FileDialogOptions,
IDialogService,
IFileService,
IPlatformBridge,
ISystemService,
MessageOptions,
PlatformType,
SaveDialogOptions,
} from '../types'
// Electron IPC 接口定义(需要在 electron preload 中实现)
interface IElectronAPI {
ipcRenderer: {
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>
send: (channel: string, ...args: unknown[]) => void
on: (channel: string, listener: (event: unknown, ...args: unknown[]) => void) => void
removeListener: (channel: string, listener: (...args: unknown[]) => void) => void
}
}
declare global {
interface Window {
electron: IElectronAPI
}
}
export class ElectronBridge implements IPlatformBridge {
readonly platform: PlatformType = 'electron'
file: IFileService = {
async read(path: string): Promise<string> {
return (await window.electron.ipcRenderer.invoke('file:read', path)) as string
},
async write(path: string, content: string): Promise<void> {
await window.electron.ipcRenderer.invoke('file:write', path, content)
},
async exists(path: string): Promise<boolean> {
return (await window.electron.ipcRenderer.invoke('file:exists', path)) as boolean
},
async openDialog(options?: FileDialogOptions): Promise<string | string[] | null> {
return (await window.electron.ipcRenderer.invoke('dialog:open', options)) as string | string[] | null
},
async saveDialog(options?: SaveDialogOptions): Promise<string | null> {
return (await window.electron.ipcRenderer.invoke('dialog:save', options)) as string | null
},
}
dialog: IDialogService = {
async message(options: MessageOptions): Promise<void> {
await window.electron.ipcRenderer.invoke('dialog:message', options)
},
async confirm(options: ConfirmOptions): Promise<boolean> {
return (await window.electron.ipcRenderer.invoke('dialog:confirm', options)) as boolean
},
}
system: ISystemService = {
async getAppVersion(): Promise<string> {
return (await window.electron.ipcRenderer.invoke('app:version')) as string
},
async getPlatformInfo() {
return (await window.electron.ipcRenderer.invoke('app:info')) as any
},
async openExternal(url: string): Promise<void> {
await window.electron.ipcRenderer.invoke('shell:open', url)
},
}
}

View File

@@ -0,0 +1,86 @@
import type {
ConfirmOptions,
FileDialogOptions,
IDialogService,
IFileService,
IPlatformBridge,
ISystemService,
MessageOptions,
PlatformType,
SaveDialogOptions,
} from '../types'
import { getTauriVersion, getVersion } from '@tauri-apps/api/app'
import { ask, message, open, save } from '@tauri-apps/plugin-dialog'
import { exists, readTextFile, writeTextFile } from '@tauri-apps/plugin-fs'
import { openUrl } from '@tauri-apps/plugin-opener'
import { arch, version as osVersion, platform } from '@tauri-apps/plugin-os'
export class TauriBridge implements IPlatformBridge {
readonly platform: PlatformType = 'tauri'
file: IFileService = {
async read(path: string): Promise<string> {
return await readTextFile(path)
},
async write(path: string, content: string): Promise<void> {
await writeTextFile(path, content)
},
async exists(path: string): Promise<boolean> {
return await exists(path)
},
async openDialog(options?: FileDialogOptions): Promise<string | string[] | null> {
const selected = await open({
multiple: options?.multiple ?? false,
directory: options?.directory ?? false,
filters: options?.filters,
defaultPath: options?.defaultPath,
})
if (selected === null)
return null
// 这里的类型转换取决于 multiple 选项
return selected as string | string[]
},
async saveDialog(options?: SaveDialogOptions): Promise<string | null> {
return await save({
defaultPath: options?.defaultPath,
filters: options?.filters,
})
},
}
dialog: IDialogService = {
async message(options: MessageOptions): Promise<void> {
await message(options.message, {
title: options.title,
kind: options.type === 'error' ? 'error' : options.type === 'warning' ? 'warning' : 'info',
})
},
async confirm(options: ConfirmOptions): Promise<boolean> {
return await ask(options.message, {
title: options.title,
kind: options.type === 'error' ? 'error' : options.type === 'warning' ? 'warning' : 'info',
okLabel: options.okLabel || 'Yes',
cancelLabel: options.cancelLabel || 'No',
})
},
}
system: ISystemService = {
async getAppVersion(): Promise<string> {
return await getVersion()
},
async getPlatformInfo() {
return {
name: 'tauri',
version: await getTauriVersion(),
os: await platform(),
osVersion: await osVersion(),
arch: await arch(),
}
},
async openExternal(url: string): Promise<void> {
await openUrl(url)
},
}
}

View File

@@ -0,0 +1,96 @@
import type {
ConfirmOptions,
FileDialogOptions,
IDialogService,
IFileService,
IPlatformBridge,
ISystemService,
MessageOptions,
PlatformType,
SaveDialogOptions,
} from '../types'
const MAC_OS_VERSION_RE = /Mac OS X ([\d_]+)/
const WINDOWS_VERSION_RE = /Windows NT ([\d.]+)/
const UNDERSCORE_RE = /_/g
function parseOsVersion(userAgent: string): string | undefined {
const macMatch = userAgent.match(MAC_OS_VERSION_RE)
if (macMatch)
return macMatch[1].replace(UNDERSCORE_RE, '.')
const winMatch = userAgent.match(WINDOWS_VERSION_RE)
if (winMatch)
return winMatch[1]
return undefined
}
export class WebBridge implements IPlatformBridge {
readonly platform: PlatformType = 'web'
file: IFileService = {
async read(_path: string): Promise<string> {
console.warn('Web environment does not support file system access directly.')
return ''
},
async write(_path: string, _content: string): Promise<void> {
console.warn('Web environment does not support file system access directly.')
},
async exists(_path: string): Promise<boolean> {
return false
},
async openDialog(_options?: FileDialogOptions): Promise<string | string[] | null> {
return new Promise((resolve) => {
const input = document.createElement('input')
input.type = 'file'
// Web端模拟仅用于演示
input.onchange = (e) => {
const files = (e.target as HTMLInputElement).files
if (files && files.length > 0) {
// Web端通常不能获取完整路径这里只是模拟
resolve(files[0].name)
}
else {
resolve(null)
}
}
input.click()
})
},
async saveDialog(_options?: SaveDialogOptions): Promise<string | null> {
console.warn('Web environment save dialog not fully supported.')
return null
},
}
dialog: IDialogService = {
async message(options: MessageOptions): Promise<void> {
// eslint-disable-next-line no-alert
alert(`${options.title ? `${options.title}\n` : ''}${options.message}`)
},
async confirm(options: ConfirmOptions): Promise<boolean> {
// eslint-disable-next-line no-alert
return confirm(`${options.title ? `${options.title}\n` : ''}${options.message}`)
},
}
system: ISystemService = {
async getAppVersion(): Promise<string> {
return '1.0.0'
},
async getPlatformInfo() {
const osVersion = parseOsVersion(navigator.userAgent)
return {
name: 'web',
version: navigator.userAgent,
os: navigator.platform,
osVersion,
arch: 'unknown',
}
},
async openExternal(url: string): Promise<void> {
window.open(url, '_blank')
},
}
}

View File

@@ -0,0 +1,24 @@
import type { IPlatformBridge } from './types'
import { ElectronBridge } from './adapters/electron'
import { WebBridge } from './adapters/web'
export { ElectronBridge } from './adapters/electron'
export { WebBridge } from './adapters/web'
export * from './types'
export function createBridge(): IPlatformBridge {
if (typeof window !== 'undefined') {
const win = window as any
if (win.electron && win.electron.ipcRenderer) {
return new ElectronBridge()
}
// Tauri 检测逻辑 - 注意:这里不能引用 TauriBridge否则会破坏 Electron 构建
// Tauri App 必须手动初始化 Bridge 并通过 provide 注入,或者我们依赖 userAgent 判断
// 但 instantiation 必须在 app 层做。
}
return new WebBridge()
}
export const bridge = createBridge()

View File

@@ -0,0 +1 @@
export * from './adapters/tauri'

View File

@@ -0,0 +1,70 @@
/**
* 平台桥接类型定义
*/
export type PlatformType = 'web' | 'electron' | 'tauri'
export interface FileDialogOptions {
title?: string
defaultPath?: string
filters?: {
name: string
extensions: string[]
}[]
multiple?: boolean
directory?: boolean
}
export interface SaveDialogOptions {
title?: string
defaultPath?: string
filters?: {
name: string
extensions: string[]
}[]
}
export interface MessageOptions {
title?: string
message: string
type?: 'info' | 'warning' | 'error'
}
export interface ConfirmOptions extends MessageOptions {
okLabel?: string
cancelLabel?: string
}
export interface PlatformInfo {
name: PlatformType
version: string
os: string
osVersion?: string
arch: string
}
export interface IFileService {
read: (path: string) => Promise<string>
write: (path: string, content: string) => Promise<void>
exists: (path: string) => Promise<boolean>
openDialog: (options?: FileDialogOptions) => Promise<string | string[] | null>
saveDialog: (options?: SaveDialogOptions) => Promise<string | null>
}
export interface IDialogService {
message: (options: MessageOptions) => Promise<void>
confirm: (options: ConfirmOptions) => Promise<boolean>
}
export interface ISystemService {
getAppVersion: () => Promise<string>
getPlatformInfo: () => Promise<PlatformInfo>
openExternal: (url: string) => Promise<void>
}
export interface IPlatformBridge {
readonly platform: PlatformType
file: IFileService
dialog: IDialogService
system: ISystemService
}

View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"rootDir": "./src",
"types": ["electron", "node"],
"outDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

4
packages/core/.env Normal file
View File

@@ -0,0 +1,4 @@
# 默认环境变量配置(会被 .env.development 和 .env.production 覆盖)
VITE_API_BASE_URL=https://cslab.oberyun.com
VITE_BASE_URL=/dcs-web/

View File

@@ -0,0 +1,3 @@
# 开发环境配置
VITE_API_BASE_URL=http://192.168.1.110:8001
VITE_WS_DOMAIN=ws://192.168.1.110:6600

View File

@@ -0,0 +1,2 @@
# 生产环境配置
VITE_API_BASE_URL=https://cslab.oberyun.com

22
packages/core/env.d.ts vendored Normal file
View File

@@ -0,0 +1,22 @@
/// <reference types="vite/client" />
/// <reference types="element-plus/global" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<object, object, any>
export default component
}
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string
// 更多环境变量可以在这里添加...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
interface Window {
// 可以扩展 window 类型
}

View File

@@ -0,0 +1,50 @@
{
"name": "@cslab-dcs/core",
"type": "module",
"version": "1.0.0",
"private": true,
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
},
"./vite.shared": {
"types": "./vite.shared.d.ts",
"import": "./vite.shared.ts"
}
},
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"typecheck": "vue-tsc --noEmit"
},
"dependencies": {
"@cslab-dcs/bridge": "workspace:*",
"@cslab-dcs/schema": "workspace:*",
"@fortawesome/fontawesome-free": "^7.1.0",
"@vueuse/core": "^12.0.0",
"dayjs": "^1.11.19",
"element-plus": "^2.9.0",
"es-toolkit": "^1.44.0",
"mitt": "^3.0.1",
"ofetch": "^1.5.1",
"pinia": "^3.0.0",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"zod": "^3.24.1"
},
"devDependencies": {
"@unocss/preset-attributify": "^66.6.0",
"@vitejs/plugin-vue": "^6.0.1",
"@vitejs/plugin-vue-jsx": "^4.1.1",
"sass": "^1.83.0",
"typescript": "^5.9.3",
"unocss": "^66.6.0",
"unocss-preset-chinese": "^0.3.3",
"unocss-preset-ease": "^0.0.4",
"unplugin-auto-import": "^19.0.0",
"unplugin-vue-components": "^28.0.0",
"vite": "^6.0.3",
"vue-tsc": "^2.2.0"
}
}

32
packages/core/src/App.vue Normal file
View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import { useBridge } from '@/bootstrap'
useBridge()
</script>
<template>
<ElConfigProvider :locale="zhCn">
<router-view />
</ElConfigProvider>
</template>
<style lang="scss">
/* 全局样式 */
html, body, #app {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
overflow: auto;
position: relative;
font-family: 'Helvetica Neue', Helvetica, 'PingFang SC', 'Hiragino Sans GB',
'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
}
/* 确保 Element Plus 弹窗层级正确 */
.el-overlay {
position: fixed !important;
z-index: 2000 !important;
}
</style>

View File

@@ -0,0 +1,127 @@
import type { Layer } from '@cslab-dcs/schema'
import type { CanvasDetail, CanvasSummary } from '@/request'
import { requestClient } from '@/request'
import pinia, { useAppStore, useProjectStore } from '@/stores'
// 画布服务层:对 UI 提供稳定接口,屏蔽 request 底层适配器
export interface CanvasCreatePayload {
name: string
description?: string
thumbnail?: string
width?: number
height?: number
}
export interface CanvasUpdatePayload {
id: string
name: string
description?: string
}
export interface CanvasDuplicatePayload {
id: string
name?: string
}
export interface SaveCanvasComponentsPayload {
id: string
components: Layer[]
}
export interface UpdateCanvasBaseLayerPayload {
id: string
width: number
height: number
thumbnail?: string | null
lockAspectRatio?: boolean
backgroundColor?: string
}
export type CanvasListItem = CanvasSummary
export type CanvasData = CanvasDetail
function getRequestClient() {
const { isWeb } = useAppStore(pinia)
if (!isWeb)
return requestClient.local
return requestClient.local
}
export async function listCanvasesApi(): Promise<CanvasListItem[]> {
const { projectId } = useProjectStore(pinia)
if (!projectId)
return []
return getRequestClient().canvas.list(projectId)
}
export async function getCanvasByIdApi(canvasId: string): Promise<CanvasData | null> {
return getRequestClient().canvas.getById(canvasId)
}
export async function createCanvasApi(payload: CanvasCreatePayload): Promise<CanvasListItem> {
const { projectId } = useProjectStore(pinia)
if (!projectId)
throw new Error('Project ID is required to create a canvas')
return getRequestClient().canvas.create({
projectId,
name: payload.name,
description: payload.description,
thumbnail: payload.thumbnail,
width: payload.width,
height: payload.height,
})
}
export async function updateCanvasApi(payload: CanvasUpdatePayload): Promise<CanvasListItem> {
console.log('payload', payload)
return getRequestClient().canvas.update({
canvasId: payload.id,
name: payload.name,
description: payload.description,
})
}
export async function duplicateCanvasApi(payload: CanvasDuplicatePayload): Promise<CanvasListItem> {
return getRequestClient().canvas.duplicate({
canvasId: payload.id,
name: payload.name,
})
}
export async function deleteCanvasApi(canvasId: string): Promise<void> {
return getRequestClient().canvas.remove({ canvasId })
}
// 批量更新画布排序
export async function reorderCanvasesApi(canvasIds: string[]): Promise<void> {
const { projectId } = useProjectStore(pinia)
if (!projectId)
throw new Error('Project ID is required to reorder canvases')
return getRequestClient().canvas.reorder({ projectId, canvasIds })
}
// 编辑器自动保存组件快照
export async function updateComponentsLayer(payload: SaveCanvasComponentsPayload): Promise<void> {
return getRequestClient().canvas.updateComponentsLayer({
canvasId: payload.id,
components: payload.components,
})
}
// 更新画布背景图层(底图)属性
export async function updateCanvasBaseLayer(payload: UpdateCanvasBaseLayerPayload): Promise<void> {
return getRequestClient().canvas.updateBaseLayer({
canvasId: payload.id,
width: payload.width,
height: payload.height,
thumbnail: payload.thumbnail,
lockAspectRatio: payload.lockAspectRatio,
backgroundColor: payload.backgroundColor,
})
}

View File

@@ -0,0 +1,112 @@
import { createHttpClient } from '@/request/client'
export interface DynamicProjectModuleResponse {
module_pk: string
name: string
describe: string
label: string
classify: string
}
export interface DynamicProjectModule extends DynamicProjectModuleResponse {
displayLabel: string
key: string
}
export interface DynamicProjectModuleProp {
prop_pk: string
name: string
describe: string
t_module_prop: string
classify: string
}
export interface DynamicProjectProperty extends DynamicProjectModuleProp {
displayLabel: string
key: string
defaultValue?: unknown
}
const fetcher = createHttpClient()
export async function getDynamicProjectModulesApi(baseProjectId: string): Promise<DynamicProjectModule[]> {
if (!baseProjectId) {
return []
}
const data = await fetcher<DynamicProjectModuleResponse[]>('/project/module/list/', {
method: 'GET',
headers: {
'Hidden-Error': '1',
'PROJECT': baseProjectId,
},
query: {
pro: baseProjectId,
isEnum: 1,
},
})
console.log('[dynamic-project] modules 原始响应', data)
if (!Array.isArray(data)) {
console.warn('[dynamic-project] modules 响应非数组', { type: typeof data, data })
return []
}
const result: DynamicProjectModule[] = data.map(module => ({
...module,
displayLabel: `${module.name} (${module.describe})`,
key: module.module_pk,
}))
console.log('[dynamic-project] modules 归一化结果', result)
return result
}
export async function getDynamicProjectModulePropsApi(baseProjectId: string, modulePk: string): Promise<DynamicProjectProperty[]> {
if (!baseProjectId || !modulePk) {
return []
}
const data = await fetcher<{ moduleProp: DynamicProjectModuleProp[] }>('/project/module/', {
method: 'GET',
headers: {
'Hidden-Error': '1',
'PROJECT': baseProjectId,
},
query: {
pk: modulePk,
pro: baseProjectId,
isNeedFlow: 0,
},
})
console.log(`[dynamic-project] module "${modulePk}" props 原始响应`, data)
const propList = data?.moduleProp
if (!Array.isArray(propList)) {
console.warn(`[dynamic-project] module "${modulePk}" props 响应非数组`, { type: typeof propList, data })
return []
}
// 按 name 去重API 可能对同一属性返回多条记录,只保留首条
const seen = new Set<string>()
const result: DynamicProjectProperty[] = propList
.filter(prop => prop)
.filter((prop) => {
if (seen.has(prop.name)) {
return false
}
seen.add(prop.name)
return true
})
.map(prop => ({
...prop,
displayLabel: `${prop.name} (${prop.describe})`,
key: prop.prop_pk,
defaultValue: undefined,
}))
console.log(`[dynamic-project] module "${modulePk}" props 归一化结果`, { count: result.length })
return result
}

View File

@@ -0,0 +1,4 @@
export * from './canvas'
export * from './dynamic-project'
export * from './project'
export * from './runtime'

View File

@@ -0,0 +1,15 @@
import type { ProjectSummary } from '@/request'
import { requestClient } from '@/request'
import pinia, { useAppStore } from '@/stores'
function getRequestClient() {
const { isWeb } = useAppStore(pinia)
if (!isWeb)
return requestClient.local
return requestClient.http
}
export async function getProjectListApi(): Promise<ProjectSummary[]> {
return getRequestClient().project.list()
}

View File

@@ -0,0 +1,99 @@
import { ofetch } from 'ofetch'
import { createHttpClient } from '@/request/client'
/** RPC 请求客户端(指向 chemical-chaos 运算服务,响应格式与 cslab-server 不同,不复用通用拦截器) */
const rpcFetcher = ofetch.create({
baseURL: '/chemical-chaos',
timeout: 600_000,
})
/** cslab-server 请求客户端execSequence 等走 Django 后端) */
const serverFetcher = createHttpClient()
// ── 通用 RPC 调用 ──
/** common/ 接口:返回格式为 { message, result } */
function rpcCommon<T = unknown>(clazz: string, method: string, kwargs: Record<string, unknown>) {
return rpcFetcher<T>('/v1/rpc/common/', {
method: 'POST',
body: { clazz, method, args: [], kwargs },
})
}
/** zero_rpc/ 接口:返回格式为 { status, msg, data },自动解包取 data */
async function rpcZero<T = unknown>(method: string, kwargs: Record<string, unknown>) {
const res = await rpcFetcher<{ status: number, msg: string, data: T }>('/v1/rpc/zero_rpc/', {
method: 'POST',
body: { method, kwargs },
})
return res.data
}
// ── 执行顺序接口cslab-server ──
/** 获取执行顺序(运行前的必要前置调用) */
export function getExecSequenceApi(pro: string, callowWay: string) {
return serverFetcher('/project/module/execSequence/', {
method: 'GET',
params: { pro, is_preview: 0, callow_way: callowWay },
headers: { PROJECT: pro },
})
}
// ── 任务管理接口 ──
export interface AddJobParams {
pro: string
addressee: number
callow_way: 'steady' | 'dynamic' | 'design' | 'chemicalPrinciple'
is_only_checked?: boolean
is_custom_sequence?: boolean
is_steady?: boolean
need_converge?: number
current_origin?: string
is_debug?: boolean
cal_label?: string
pk?: string[]
}
export interface AddJobResult {
message: string
result: {
job: { id: string }
msg?: string
}
}
/** 添加运行任务 */
export function addJobApi(params: AddJobParams) {
return rpcCommon<AddJobResult>(
'agent.rpc_client.run.run',
'Run.add_job',
params as unknown as Record<string, unknown>,
)
}
/** 暂停任务 */
export function pauseJobApi(taskId: string, addressee: number) {
return rpcZero('pause_cal_job', { task_id: taskId, addressee })
}
/** 继续运行 */
export function resumeJobApi(taskId: string, addressee: number) {
return rpcZero('unpause_cal_job', { task_id: taskId, addressee })
}
/** 停止任务 */
export function stopJobApi(taskId: string, addressee: number) {
return rpcZero('exit_cal_job', { task_id: taskId, addressee })
}
/** 获取项目下的任务 */
export function getProjectJobApi(addressee: number, pro: string) {
return rpcZero<{ state2: string | null, task: string }>('get_user_job', { addressee, pro })
}
/** 全量推送数据 */
export function pushFullDataApi(taskId: string, addressee: number) {
return rpcZero('push_full_cal_data', { task_id: taskId, addressee })
}

Binary file not shown.

View File

@@ -0,0 +1,6 @@
@font-face {
font-family: 'Oxanium';
src: url('./Oxanium.woff2') format('woff2'),
url('./Oxanium.woff') format('woff');
font-weight: 400;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 KiB

View File

@@ -0,0 +1,227 @@
@use "sass:color";
@use "sass:math";
/* Element Plus 主题变量覆盖 */
/* 基于 Element UI 变量转换为 Element Plus CSS Variables */
/* Transition
-------------------------- */
$--all-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1) !default;
$--fade-transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) !default;
$--fade-linear-transition: opacity 200ms linear !default;
$--md-fade-transition: transform 300ms cubic-bezier(0.23, 1, 0.32, 1),
opacity 300ms cubic-bezier(0.23, 1, 0.32, 1) !default;
$--border-transition-base: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) !default;
$--color-transition-base: color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1) !default;
/* Color
-------------------------- */
$--color-primary: #1677ff !default;
$--color-white: #ffffff !default;
$--color-black: #000000 !default;
// Primary color variants
$--color-primary-light-1: color.mix($--color-white, $--color-primary, 10%) !default;
$--color-primary-light-2: color.mix($--color-white, $--color-primary, 20%) !default;
$--color-primary-light-3: color.mix($--color-white, $--color-primary, 30%) !default;
$--color-primary-light-4: color.mix($--color-white, $--color-primary, 40%) !default;
$--color-primary-light-5: color.mix($--color-white, $--color-primary, 50%) !default;
$--color-primary-light-6: color.mix($--color-white, $--color-primary, 60%) !default;
$--color-primary-light-7: color.mix($--color-white, $--color-primary, 70%) !default;
$--color-primary-light-8: color.mix($--color-white, $--color-primary, 80%) !default;
$--color-primary-light-9: color.mix($--color-white, $--color-primary, 90%) !default;
// Functional colors
$--color-success: #0ac05e !default;
$--color-warning: #faad14 !default;
$--color-danger: #ff4d4f !default;
$--color-info: #8c8c8c !default;
$--color-success-light: color.mix($--color-white, $--color-success, 80%) !default;
$--color-warning-light: color.mix($--color-white, $--color-warning, 80%) !default;
$--color-danger-light: color.mix($--color-white, $--color-danger, 80%) !default;
$--color-info-light: color.mix($--color-white, $--color-info, 80%) !default;
$--color-success-lighter: color.mix($--color-white, $--color-success, 90%) !default;
$--color-warning-lighter: color.mix($--color-white, $--color-warning, 90%) !default;
$--color-danger-lighter: color.mix($--color-white, $--color-danger, 90%) !default;
$--color-info-lighter: color.mix($--color-white, $--color-info, 90%) !default;
// Text colors
$--color-text-primary: #262626 !default;
$--color-text-regular: #595959 !default;
$--color-text-secondary: #8c8c8c !default;
$--color-text-placeholder: #bfbfbf !default;
// Border colors
$--border-color-base: #d9d9d9 !default;
$--border-color-light: #e8e8e8 !default;
$--border-color-lighter: #e8e8e8 !default;
$--border-color-extra-light: #f5f5f5 !default;
// Background
$--background-color-base: #f5f5f5 !default;
/* Border
-------------------------- */
$--border-width-base: 1px !default;
$--border-style-base: solid !default;
$--border-color-hover: $--color-text-placeholder !default;
$--border-base: $--border-width-base $--border-style-base $--border-color-base !default;
$--border-radius-base: 6px !default;
$--border-radius-small: 4px !default;
$--border-radius-circle: 100% !default;
$--border-radius-zero: 0 !default;
/* Box-shadow
-------------------------- */
$--box-shadow-base: 0 2px 8px rgba(0, 0, 0, 0.08) !default;
$--box-shadow-dark: 0 6px 16px rgba(0, 0, 0, 0.12) !default;
$--box-shadow-light: 0 4px 12px rgba(0, 0, 0, 0.1) !default;
/* Typography
-------------------------- */
$--font-size-extra-large: 20px !default;
$--font-size-large: 18px !default;
$--font-size-medium: 16px !default;
$--font-size-base: 14px !default;
$--font-size-small: 13px !default;
$--font-size-extra-small: 12px !default;
$--font-weight-primary: 500 !default;
$--font-weight-secondary: 100 !default;
$--font-line-height-primary: 24px !default;
$--font-line-height-secondary: 16px !default;
/* z-index
-------------------------- */
$--index-normal: 1 !default;
$--index-top: 1000 !default;
$--index-popper: 2000 !default;
/* Disabled
-------------------------- */
$--disabled-fill-base: $--background-color-base !default;
$--disabled-color-base: $--color-text-placeholder !default;
$--disabled-border-base: $--border-color-light !default;
/* Element Plus CSS Variables Override
-------------------------- */
:root {
// Colors
--el-color-primary: #{$--color-primary};
--el-color-primary-light-3: #{$--color-primary-light-3};
--el-color-primary-light-5: #{$--color-primary-light-5};
--el-color-primary-light-7: #{$--color-primary-light-7};
--el-color-primary-light-8: #{$--color-primary-light-8};
--el-color-primary-light-9: #{$--color-primary-light-9};
--el-color-primary-dark-2: #{color.mix($--color-black, $--color-primary, 20%)};
--el-color-success: #{$--color-success};
--el-color-success-light-3: #{color.mix($--color-white, $--color-success, 30%)};
--el-color-success-light-5: #{color.mix($--color-white, $--color-success, 50%)};
--el-color-success-light-7: #{color.mix($--color-white, $--color-success, 70%)};
--el-color-success-light-8: #{color.mix($--color-white, $--color-success, 80%)};
--el-color-success-light-9: #{color.mix($--color-white, $--color-success, 90%)};
--el-color-warning: #{$--color-warning};
--el-color-warning-light-3: #{color.mix($--color-white, $--color-warning, 30%)};
--el-color-warning-light-5: #{color.mix($--color-white, $--color-warning, 50%)};
--el-color-warning-light-7: #{color.mix($--color-white, $--color-warning, 70%)};
--el-color-warning-light-8: #{color.mix($--color-white, $--color-warning, 80%)};
--el-color-warning-light-9: #{color.mix($--color-white, $--color-warning, 90%)};
--el-color-danger: #{$--color-danger};
--el-color-danger-light-3: #{color.mix($--color-white, $--color-danger, 30%)};
--el-color-danger-light-5: #{color.mix($--color-white, $--color-danger, 50%)};
--el-color-danger-light-7: #{color.mix($--color-white, $--color-danger, 70%)};
--el-color-danger-light-8: #{color.mix($--color-white, $--color-danger, 80%)};
--el-color-danger-light-9: #{color.mix($--color-white, $--color-danger, 90%)};
--el-color-info: #{$--color-info};
--el-color-info-light-3: #{color.mix($--color-white, $--color-info, 30%)};
--el-color-info-light-5: #{color.mix($--color-white, $--color-info, 50%)};
--el-color-info-light-7: #{color.mix($--color-white, $--color-info, 70%)};
--el-color-info-light-8: #{color.mix($--color-white, $--color-info, 80%)};
--el-color-info-light-9: #{color.mix($--color-white, $--color-info, 90%)};
// Text colors
--el-text-color-primary: #{$--color-text-primary};
--el-text-color-regular: #{$--color-text-regular};
--el-text-color-secondary: #{$--color-text-secondary};
--el-text-color-placeholder: #{$--color-text-placeholder};
--el-text-color-disabled: #{$--disabled-color-base};
// Border colors
--el-border-color: #{$--border-color-base};
--el-border-color-light: #{$--border-color-light};
--el-border-color-lighter: #{$--border-color-lighter};
--el-border-color-extra-light: #{$--border-color-extra-light};
--el-border-color-dark: #{color.mix($--color-black, $--border-color-base, 20%)};
--el-border-color-darker: #{color.mix($--color-black, $--border-color-base, 40%)};
// Fill colors
--el-fill-color: #{$--background-color-base};
--el-fill-color-light: #{color.mix($--color-white, $--background-color-base, 30%)};
--el-fill-color-lighter: #{color.mix($--color-white, $--background-color-base, 50%)};
--el-fill-color-extra-light: #{color.mix($--color-white, $--background-color-base, 70%)};
--el-fill-color-dark: #{color.mix($--color-black, $--background-color-base, 10%)};
--el-fill-color-darker: #{color.mix($--color-black, $--background-color-base, 20%)};
--el-fill-color-blank: #{$--color-white};
// Background colors
--el-bg-color: #{$--color-white};
--el-bg-color-page: #{$--background-color-base};
--el-bg-color-overlay: #{$--color-white};
// Border
--el-border-width: #{$--border-width-base};
--el-border-style: #{$--border-style-base};
--el-border-radius-base: #{$--border-radius-base};
--el-border-radius-small: #{$--border-radius-small};
--el-border-radius-round: 20px;
--el-border-radius-circle: #{$--border-radius-circle};
// Box-shadow
--el-box-shadow: #{$--box-shadow-base};
--el-box-shadow-light: #{$--box-shadow-light};
--el-box-shadow-lighter: 0 2px 4px rgba(0, 0, 0, 0.04);
--el-box-shadow-dark: #{$--box-shadow-dark};
// Typography
--el-font-size-extra-large: #{$--font-size-extra-large};
--el-font-size-large: #{$--font-size-large};
--el-font-size-medium: #{$--font-size-medium};
--el-font-size-base: #{$--font-size-base};
--el-font-size-small: #{$--font-size-small};
--el-font-size-extra-small: #{$--font-size-extra-small};
--el-font-weight-primary: #{$--font-weight-primary};
--el-font-line-height-primary: #{$--font-line-height-primary};
// z-index
--el-index-normal: #{$--index-normal};
--el-index-top: #{$--index-top};
--el-index-popper: #{$--index-popper};
// Transition
--el-transition-duration: 0.3s;
--el-transition-duration-fast: 0.2s;
--el-transition-function-ease-in-out-bezier: cubic-bezier(0.645, 0.045, 0.355, 1);
--el-transition-function-fast-bezier: cubic-bezier(0.23, 1, 0.32, 1);
--el-transition-all: #{$--all-transition};
--el-transition-fade: #{$--fade-transition};
--el-transition-fade-linear: #{$--fade-linear-transition};
--el-transition-md-fade: #{$--md-fade-transition};
--el-transition-border: #{$--border-transition-base};
--el-transition-color: #{$--color-transition-base};
// Component size
--el-component-size-large: 40px;
--el-component-size: 32px;
--el-component-size-small: 24px;
// Disabled
--el-disabled-bg-color: #{$--disabled-fill-base};
--el-disabled-text-color: #{$--disabled-color-base};
--el-disabled-border-color: #{$--disabled-border-base};
}

View File

@@ -0,0 +1,41 @@
@use './variables.scss' as *;
@use './element-plus-theme.scss';
@import url('../fonts/index.css');
/* Reset or Base styles */
body {
background-color: $bg-color-page;
color: $text-color-primary;
}
* {
box-sizing: border-box;
}
/* Scrollbar customization */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: $border-color;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: $text-color-secondary;
}
/* DCS 画布组件动画 */
@keyframes dcs-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.2; }
}
.dcs-blink {
animation: dcs-blink 1s ease-in-out infinite;
}

View File

@@ -0,0 +1,18 @@
/* Global SCSS Variables */
$primary-color: #1677ff;
$success-color: #0ac05e;
$warning-color: #faad14;
$danger-color: #ff4d4f;
$info-color: #8c8c8c;
$text-color-primary: #262626;
$text-color-regular: #595959;
$text-color-secondary: #8c8c8c;
$text-color-placeholder: #bfbfbf;
$border-color: #d9d9d9;
$border-color-light: #e8e8e8;
$border-color-lighter: #e8e8e8;
$bg-color: #ffffff;
$bg-color-page: #f5f5f5;

11
packages/core/src/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,11 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const ElMessage: typeof import('element-plus/es')['ElMessage']
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
}

Some files were not shown because too many files have changed in this diff Show More