all
20
.editorconfig
Normal 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
|
||||||
60
.gitea/actions/get-version/action.yaml
Normal 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}"
|
||||||
124
.gitea/actions/wecom-notification/action.yaml
Normal 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}\"
|
||||||
|
}
|
||||||
|
}"
|
||||||
58
.gitea/workflows/deploy-dev.yaml
Normal 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
@@ -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
@@ -0,0 +1,3 @@
|
|||||||
|
shamefully-hoist=true
|
||||||
|
strict-peer-dependencies=false
|
||||||
|
auto-install-peers=true
|
||||||
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["dbaeumer.vscode-eslint"]
|
||||||
|
}
|
||||||
39
.vscode/launch.json
vendored
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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 4(Hash 模式,兼容桌面端) |
|
||||||
|
| **状态管理** | Pinia 3 |
|
||||||
|
| **UI 组件库** | Element Plus(中文本地化) |
|
||||||
|
| **原子化 CSS** | UnoCSS |
|
||||||
|
| **组合式工具** | VueUse |
|
||||||
|
| **HTTP 客户端** | ofetch |
|
||||||
|
| **Schema 校验** | Zod |
|
||||||
|
| **构建工具** | Vite 6 / electron-vite |
|
||||||
|
| **桌面框架** | Electron 22 / Tauri 2 |
|
||||||
|
| **包管理器** | pnpm(workspace) |
|
||||||
|
| **TypeScript** | 5.9+(严格模式) |
|
||||||
|
| **代码规范** | ESLint + @antfu/eslint-config |
|
||||||
|
| **单元测试** | Vitest + @vue/test-utils + happy-dom |
|
||||||
|
| **E2E 测试** | Playwright(Chromium) |
|
||||||
|
| **样式预处理** | 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(页面视图) │
|
||||||
|
├────────────────────────────────────────────┤
|
||||||
|
│ Components(UI 组件层) │
|
||||||
|
├────────────────────────────────────────────┤
|
||||||
|
│ Composables(组合式函数层) │
|
||||||
|
├────────────────────────────────────────────┤
|
||||||
|
│ Pinia Stores(状态管理层) │
|
||||||
|
├────────────────────────────────────────────┤
|
||||||
|
│ API Layer(服务接口层) │
|
||||||
|
├────────────────────────────────────────────┤
|
||||||
|
│ Request Adapters(HTTP / IndexedDB 适配) │
|
||||||
|
├────────────────────────────────────────────┤
|
||||||
|
│ Platform Bridge(Web / 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 测试
|
||||||
|
|
||||||
|
- **框架**:Playwright(Chromium)
|
||||||
|
- **位置**:`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 — 内部项目
|
||||||
3
apps/electron/.eslintrc-auto-import.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"globals": {}
|
||||||
|
}
|
||||||
29
apps/electron/electron-builder.yml
Normal 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
|
||||||
28
apps/electron/electron.vite.config.ts
Normal 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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
36
apps/electron/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
apps/electron/resources/icon.ico
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
apps/electron/resources/icon.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
1
apps/electron/src/main/env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="electron-vite/node" />
|
||||||
201
apps/electron/src/main/index.ts
Normal 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
@@ -0,0 +1,8 @@
|
|||||||
|
import type { ElectronAPI } from '@electron-toolkit/preload'
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
electron: ElectronAPI
|
||||||
|
api: Record<string, unknown>
|
||||||
|
}
|
||||||
|
}
|
||||||
22
apps/electron/src/preload/index.ts
Normal 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
|
||||||
|
}
|
||||||
12
apps/electron/src/renderer/index.html
Normal 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
@@ -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
|
||||||
|
}
|
||||||
5
apps/electron/src/renderer/src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { bridge } from '@cslab-dcs/bridge'
|
||||||
|
import { createDCSApp } from '@cslab-dcs/core'
|
||||||
|
|
||||||
|
const app = createDCSApp(bridge)
|
||||||
|
app.mount('#app')
|
||||||
8
apps/electron/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.node.json" },
|
||||||
|
{ "path": "./tsconfig.preload.json" },
|
||||||
|
{ "path": "./tsconfig.web.json" }
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
}
|
||||||
10
apps/electron/tsconfig.node.json
Normal 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"]
|
||||||
|
}
|
||||||
10
apps/electron/tsconfig.preload.json
Normal 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"]
|
||||||
|
}
|
||||||
23
apps/electron/tsconfig.web.json
Normal 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"]
|
||||||
|
}
|
||||||
3
apps/tauri/.eslintrc-auto-import.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"globals": {}
|
||||||
|
}
|
||||||
13
apps/tauri/index.html
Normal 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
@@ -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
31
apps/tauri/src-tauri/Cargo.toml
Normal 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" ]
|
||||||
3
apps/tauri/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
tauri_build::build()
|
||||||
|
}
|
||||||
13
apps/tauri/src-tauri/capabilities/default.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
apps/tauri/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
apps/tauri/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
apps/tauri/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
apps/tauri/src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
apps/tauri/src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
apps/tauri/src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
apps/tauri/src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
apps/tauri/src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
apps/tauri/src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
apps/tauri/src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
apps/tauri/src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
apps/tauri/src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
apps/tauri/src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
apps/tauri/src-tauri/icons/icon.icns
Normal file
BIN
apps/tauri/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
apps/tauri/src-tauri/icons/icon.png
Executable file
|
After Width: | Height: | Size: 291 KiB |
BIN
apps/tauri/src-tauri/icons/icon_512.png
Normal file
|
After Width: | Height: | Size: 189 KiB |
10
apps/tauri/src-tauri/src/lib.rs
Normal 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");
|
||||||
|
}
|
||||||
6
apps/tauri/src-tauri/src/main.rs
Normal 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()
|
||||||
|
}
|
||||||
40
apps/tauri/src-tauri/tauri.conf.json
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
2
apps/web/.env.development
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# 开发环境配置
|
||||||
|
VITE_API_BASE_URL=http://192.168.1.110:8001
|
||||||
2
apps/web/.env.production
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# 生产环境配置
|
||||||
|
VITE_API_BASE_URL=https://cslab.oberyun.com
|
||||||
3
apps/web/.eslintrc-auto-import.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"globals": {}
|
||||||
|
}
|
||||||
13
apps/web/index.html
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
|
|||||||
|
# README
|
||||||
|
|
||||||
|
本目录下记录与AI的prompt和AI出的计划和实际操作等等。
|
||||||
8
eslint.config.js
Normal 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
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
32
packages/bridge/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
70
packages/bridge/src/adapters/electron.ts
Normal 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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
86
packages/bridge/src/adapters/tauri.ts
Normal 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)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
96
packages/bridge/src/adapters/web.ts
Normal 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')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
24
packages/bridge/src/index.ts
Normal 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()
|
||||||
1
packages/bridge/src/tauri.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './adapters/tauri'
|
||||||
70
packages/bridge/src/types.ts
Normal 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
|
||||||
|
}
|
||||||
11
packages/bridge/tsconfig.json
Normal 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
@@ -0,0 +1,4 @@
|
|||||||
|
# 默认环境变量配置(会被 .env.development 和 .env.production 覆盖)
|
||||||
|
VITE_API_BASE_URL=https://cslab.oberyun.com
|
||||||
|
|
||||||
|
VITE_BASE_URL=/dcs-web/
|
||||||
3
packages/core/.env.development
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# 开发环境配置
|
||||||
|
VITE_API_BASE_URL=http://192.168.1.110:8001
|
||||||
|
VITE_WS_DOMAIN=ws://192.168.1.110:6600
|
||||||
2
packages/core/.env.production
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# 生产环境配置
|
||||||
|
VITE_API_BASE_URL=https://cslab.oberyun.com
|
||||||
22
packages/core/env.d.ts
vendored
Normal 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 类型
|
||||||
|
}
|
||||||
50
packages/core/package.json
Normal 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
@@ -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>
|
||||||
127
packages/core/src/api/canvas.ts
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
112
packages/core/src/api/dynamic-project.ts
Normal 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
|
||||||
|
}
|
||||||
4
packages/core/src/api/index.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export * from './canvas'
|
||||||
|
export * from './dynamic-project'
|
||||||
|
export * from './project'
|
||||||
|
export * from './runtime'
|
||||||
15
packages/core/src/api/project.ts
Normal 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()
|
||||||
|
}
|
||||||
99
packages/core/src/api/runtime.ts
Normal 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 })
|
||||||
|
}
|
||||||
BIN
packages/core/src/assets/fonts/Oxanium.woff2
Normal file
6
packages/core/src/assets/fonts/index.css
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Oxanium';
|
||||||
|
src: url('./Oxanium.woff2') format('woff2'),
|
||||||
|
url('./Oxanium.woff') format('woff');
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
BIN
packages/core/src/assets/images/demo.png
Normal file
|
After Width: | Height: | Size: 404 KiB |
BIN
packages/core/src/assets/images/demo1.png
Normal file
|
After Width: | Height: | Size: 708 KiB |
227
packages/core/src/assets/styles/element-plus-theme.scss
Normal 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};
|
||||||
|
}
|
||||||
41
packages/core/src/assets/styles/index.scss
Normal 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;
|
||||||
|
}
|
||||||
18
packages/core/src/assets/styles/variables.scss
Normal 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
@@ -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']
|
||||||
|
}
|
||||||