Compare commits
5 Commits
c727492da2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| f28b5dc50a | |||
| 1d0d39995e | |||
| 669ee2dcd3 | |||
| eb27d72927 | |||
| 8248767b89 |
@@ -0,0 +1,40 @@
|
||||
'use strict'
|
||||
|
||||
const silenceLegacySassApi = {
|
||||
sassOptions: {
|
||||
silenceDeprecations: ['legacy-js-api']
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scss: {
|
||||
loader: 'sass-loader',
|
||||
options: silenceLegacySassApi
|
||||
},
|
||||
sass: {
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
indentedSyntax: true,
|
||||
...silenceLegacySassApi
|
||||
}
|
||||
},
|
||||
vueScssLoaders: [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: silenceLegacySassApi
|
||||
}
|
||||
],
|
||||
vueSassLoaders: [
|
||||
'vue-style-loader',
|
||||
'css-loader',
|
||||
{
|
||||
loader: 'sass-loader',
|
||||
options: {
|
||||
indentedSyntax: true,
|
||||
...silenceLegacySassApi
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -12,6 +12,7 @@ const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const { VueLoaderPlugin } = require('vue-loader')
|
||||
const sassLoaders = require('./sass-loader-config')
|
||||
|
||||
/**
|
||||
* List of node_modules to include in webpack bundle
|
||||
@@ -45,11 +46,11 @@ let rendererConfig = {
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: ['vue-style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
|
||||
use: ['vue-style-loader', 'css-loader', 'postcss-loader', sassLoaders.scss]
|
||||
},
|
||||
{
|
||||
test: /\.sass$/,
|
||||
use: ['vue-style-loader', 'css-loader', 'postcss-loader', 'sass-loader?indentedSyntax']
|
||||
use: ['vue-style-loader', 'css-loader', 'postcss-loader', sassLoaders.sass]
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
@@ -79,8 +80,8 @@ let rendererConfig = {
|
||||
options: {
|
||||
extractCSS: process.env.NODE_ENV === 'production',
|
||||
loaders: {
|
||||
sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
|
||||
scss: 'vue-style-loader!css-loader!sass-loader',
|
||||
sass: sassLoaders.vueSassLoaders,
|
||||
scss: sassLoaders.vueScssLoaders,
|
||||
less: 'vue-style-loader!css-loader!less-loader'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const { VueLoaderPlugin } = require('vue-loader')
|
||||
const sassLoaders = require('./sass-loader-config')
|
||||
|
||||
let webConfig = {
|
||||
devtool: '#cheap-module-eval-source-map',
|
||||
@@ -33,11 +34,11 @@ let webConfig = {
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: ['vue-style-loader', 'css-loader', 'postcss-loader', 'sass-loader']
|
||||
use: ['vue-style-loader', 'css-loader', 'postcss-loader', sassLoaders.scss]
|
||||
},
|
||||
{
|
||||
test: /\.sass$/,
|
||||
use: ['vue-style-loader', 'css-loader', 'postcss-loader', 'sass-loader?indentedSyntax']
|
||||
use: ['vue-style-loader', 'css-loader', 'postcss-loader', sassLoaders.sass]
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
@@ -64,8 +65,8 @@ let webConfig = {
|
||||
options: {
|
||||
extractCSS: true,
|
||||
loaders: {
|
||||
sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
|
||||
scss: 'vue-style-loader!css-loader!sass-loader',
|
||||
sass: sassLoaders.vueSassLoaders,
|
||||
scss: sassLoaders.vueScssLoaders,
|
||||
less: 'vue-style-loader!css-loader!less-loader'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,35 @@
|
||||
# OS
|
||||
.DS_Store
|
||||
thumbs.db
|
||||
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Webpack / electron-vue build output
|
||||
dist/electron/*
|
||||
dist/web/*
|
||||
!dist/electron/.gitkeep
|
||||
!dist/web/.gitkeep
|
||||
|
||||
# electron-builder pack output (keep icons for packaging)
|
||||
build/*
|
||||
!build/icons
|
||||
node_modules/
|
||||
!build/icons/**
|
||||
|
||||
# Logs
|
||||
npm-debug.log
|
||||
npm-debug.log.*
|
||||
thumbs.db
|
||||
!.gitkeep
|
||||
yarn-error.log
|
||||
yarn-debug.log*
|
||||
|
||||
# Local env (secrets)
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Runtime / deploy assets (not source)
|
||||
/CardsoonServer
|
||||
/cardsoonServer
|
||||
/User Templates
|
||||
/docs
|
||||
/.cursor
|
||||
|
||||
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 648 B |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 257 KiB After Width: | Height: | Size: 350 KiB |
@@ -1,13 +0,0 @@
|
||||
[system_setting]
|
||||
downDevicePath=/dev/sr1
|
||||
downDeviceType=0
|
||||
end_time=cWL0ocEfX1NTQW0+hs3SGnKIgYkMeNru
|
||||
logLevel=1
|
||||
retryFreq=2
|
||||
serverPath=/home/test/CDCache
|
||||
server_name=127.0.0.1
|
||||
server_port=10010
|
||||
topDevicePath=/dev/sr0
|
||||
topDeviceType=0
|
||||
update_time=iWfJMJHeTnMCacgifeF9oACX0fK8UsnmNBUJgTgF2cs=
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
|
||||
# 路径
|
||||
HOME=$(cd $(dirname $0);pwd)
|
||||
|
||||
# 启动
|
||||
start(){
|
||||
cd $HOME
|
||||
# 将C++程序后台启动,并将启动日志指向springBootThree-1.0.log中
|
||||
spid=$(pgrep -f "controlServer") #$(ps -ef | grep "[m]controlServer" | grep -v grep | awk '{print $2}')
|
||||
if [ -z "$spid" ]
|
||||
then
|
||||
./controlServer > /dev/null 2>&1 &
|
||||
# 打印字符串
|
||||
echo "启动成功"
|
||||
else
|
||||
echo Application is already started
|
||||
fi
|
||||
}
|
||||
|
||||
# 停止
|
||||
stop(){
|
||||
# 查询到项目的pid
|
||||
tpid=$(pgrep -f "controlServer") #$(ps -ef | grep "[m]controlServer" | grep -v grep | awk '{print $2}')
|
||||
# 判断是否为空
|
||||
if [ -z "$tpid" ]
|
||||
then
|
||||
# 如果为空 则打印这句话
|
||||
echo Application is already stoped
|
||||
else
|
||||
# 如果不为空则执行
|
||||
echo kill $tpid
|
||||
# 关闭进程
|
||||
kill -9 $tpid
|
||||
|
||||
echo "程序已关闭"
|
||||
fi
|
||||
}
|
||||
|
||||
# 查看状态
|
||||
status(){
|
||||
# 获取pid的
|
||||
pid=$(pgrep -f "controlServer") #$(ps -ef | grep "[m]controlServer" | grep -v grep)
|
||||
if [ -z "$pid" ]
|
||||
then
|
||||
echo "程序不存在"
|
||||
else
|
||||
echo $pid
|
||||
fi
|
||||
}
|
||||
|
||||
# 查看日志信息
|
||||
log(){
|
||||
tail -f $HOME/CSPServer.log
|
||||
}
|
||||
|
||||
case $1 in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
status)
|
||||
status
|
||||
;;
|
||||
log)
|
||||
log
|
||||
;;
|
||||
restart)
|
||||
$0 stop
|
||||
sleep 5
|
||||
$0 start
|
||||
;;
|
||||
*)
|
||||
echo "只能是{start|stop|log|restart}"
|
||||
;;
|
||||
esac
|
||||
exit 0
|
||||
@@ -1,5 +0,0 @@
|
||||
功能:统一相对路径导入 fileSize 工具,修复打包别名解析问题
|
||||
|
||||
- 将 '@/utils/fileSize' 全部改为相对路径导入,兼容打包环境
|
||||
- 修复 store 与页面中别名解析失败导致的模块找不到问题
|
||||
- 保持逻辑不变,仅调整导入路径,降低打包风险
|
||||
@@ -1,5 +1,7 @@
|
||||
module.exports = {
|
||||
NODE_ENV: '"development"',
|
||||
BASE_API: '"https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin"',
|
||||
VUE_APP_SOCKET_API: '"ws://127.0.0.1:10010"'
|
||||
VUE_APP_SOCKET_API: '"ws://127.0.0.1:10010"',
|
||||
VUE_APP_CONSOLE_V2: '"true"',
|
||||
VUE_APP_CONSOLE_DESIGN_ONLY: '"true"'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
module.exports = {
|
||||
NODE_ENV: '"production"',
|
||||
BASE_API: '"https://easy-mock.com/mock/5950a2419adc231f356a6636/vue-admin"',
|
||||
VUE_APP_SOCKET_API: '"ws://127.0.0.1:10010"'
|
||||
VUE_APP_SOCKET_API: '"ws://127.0.0.1:10010"',
|
||||
VUE_APP_CONSOLE_V2: '"false"',
|
||||
VUE_APP_CONSOLE_DESIGN_ONLY: '"false"'
|
||||
}
|
||||
|
||||
@@ -1,226 +0,0 @@
|
||||
# 宝塔部署 Web 端说明
|
||||
|
||||
## 一、构建前配置
|
||||
|
||||
### 1. 配置后端 API 地址
|
||||
|
||||
编辑 `config/prod.env.js`,将 `BASE_API` 改为实际的后端接口地址:
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
NODE_ENV: '"production"',
|
||||
BASE_API: '"https://your-api-domain.com/api"', // 改为实际后端地址
|
||||
VUE_APP_SOCKET_API: '"ws://127.0.0.1:10010"' // WebSocket 默认地址(用户可在登录页修改)
|
||||
}
|
||||
```
|
||||
|
||||
**注意:**
|
||||
- `BASE_API` 是 HTTP 接口地址(登录、数据请求等)
|
||||
- `VUE_APP_SOCKET_API` 是 WebSocket 默认地址,用户可在登录页修改,所以这里可以保持默认或设为公网地址
|
||||
|
||||
### 2. 执行构建
|
||||
|
||||
在项目根目录执行:
|
||||
|
||||
```bash
|
||||
npm run build:web
|
||||
```
|
||||
|
||||
构建完成后,产物在 `dist/web/` 目录下,包含:
|
||||
- `index.html`(入口文件)
|
||||
- `web.js`(主 JS 文件,包含所有代码和样式)
|
||||
- `1.js` ~ `7.js`(代码分割的 chunk 文件)
|
||||
- `fonts/`(字体文件)
|
||||
- `imgs/`(图片资源)
|
||||
|
||||
**注意**:CSS 样式被打包进 `web.js` 中,运行时动态注入,无需单独引用 CSS 文件。
|
||||
|
||||
---
|
||||
|
||||
## 二、宝塔部署步骤
|
||||
|
||||
### 1. 创建网站
|
||||
|
||||
1. 登录宝塔面板
|
||||
2. 点击「网站」→「添加站点」
|
||||
3. 填写:
|
||||
- **域名**:例如 `your-domain.com` 或 `192.168.1.100`(内网 IP)
|
||||
- **根目录**:例如 `/www/wwwroot/your-domain.com`
|
||||
- **PHP 版本**:选择「纯静态」或任意版本(前端不需要 PHP)
|
||||
|
||||
### 2. 上传文件
|
||||
|
||||
将 `dist/web/` 目录下的**所有文件**上传到网站根目录:
|
||||
|
||||
```
|
||||
/www/wwwroot/your-domain.com/
|
||||
├── index.html
|
||||
├── web.js
|
||||
├── styles.css
|
||||
└── static/
|
||||
├── imgs/
|
||||
└── fonts/
|
||||
```
|
||||
|
||||
**上传方式:**
|
||||
- 方式一:在宝塔「文件」中直接上传压缩包并解压
|
||||
- 方式二:使用 FTP/SFTP 工具上传
|
||||
- 方式三:使用宝塔「终端」执行 `scp` 或 `rsync` 命令
|
||||
|
||||
### 3. 配置 Nginx(重要)
|
||||
|
||||
由于是 Vue Router 的 hash 模式(`#/login`、`#/manage`),通常不需要特殊配置。但建议添加以下配置以确保正常访问:
|
||||
|
||||
在宝塔「网站」→ 选择站点 →「设置」→「配置文件」中,在 `server` 块内添加:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
root /www/wwwroot/your-domain.com;
|
||||
index index.html;
|
||||
|
||||
# 前端路由支持(hash 模式通常不需要,但加上更稳妥)
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# 静态资源缓存
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# 禁止访问隐藏文件
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 配置 HTTPS(可选但推荐)
|
||||
|
||||
如果使用域名且需要 HTTPS:
|
||||
|
||||
1. 在宝塔「网站」→ 选择站点 →「SSL」→「Let's Encrypt」申请免费证书
|
||||
2. 开启「强制 HTTPS」
|
||||
3. **注意**:如果后端 API 或 WebSocket 使用 HTTPS/WSS,确保:
|
||||
- `BASE_API` 使用 `https://`
|
||||
- WebSocket 地址使用 `wss://`(或在登录页填写 `wss://` 地址)
|
||||
|
||||
### 5. 配置跨域(如需要)
|
||||
|
||||
如果后端 API 与前端不在同一域名,后端需要配置 CORS:
|
||||
|
||||
- 允许的源:`https://your-domain.com`(或 `*` 用于开发)
|
||||
- 允许的请求头:`Content-Type, X-Token`(根据实际后端要求)
|
||||
- 允许的方法:`GET, POST, PUT, DELETE` 等
|
||||
|
||||
---
|
||||
|
||||
## 三、WebSocket 配置
|
||||
|
||||
### 情况 1:WebSocket 服务在服务器本机
|
||||
|
||||
如果 WebSocket 服务运行在**宝塔服务器本机**(例如 `ws://127.0.0.1:10010`):
|
||||
|
||||
- 用户访问网页时,浏览器会尝试连接**用户电脑**的 `127.0.0.1`,**无法连接**
|
||||
- **解决方案**:
|
||||
1. 在登录页填写 `ws://服务器公网IP:10010` 或 `ws://服务器域名:10010`
|
||||
2. 或配置 Nginx 反向代理 WebSocket(见下方)
|
||||
|
||||
### 情况 2:使用 Nginx 反向代理 WebSocket(推荐)
|
||||
|
||||
在宝塔「网站」→ 选择站点 →「设置」→「配置文件」中添加:
|
||||
|
||||
```nginx
|
||||
# WebSocket 代理(假设后端 WebSocket 在 127.0.0.1:10010)
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:10010;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
```
|
||||
|
||||
然后在登录页填写:`wss://your-domain.com/ws`(HTTPS 用 `wss://`,HTTP 用 `ws://`)
|
||||
|
||||
### 情况 3:WebSocket 服务在公网服务器
|
||||
|
||||
如果 WebSocket 服务部署在**公网服务器**(例如 `wss://ws-api.xxx.com`):
|
||||
|
||||
- 在登录页填写该地址即可
|
||||
- 或在构建时设置 `VUE_APP_SOCKET_API` 为 `wss://ws-api.xxx.com`
|
||||
|
||||
---
|
||||
|
||||
## 四、访问测试
|
||||
|
||||
1. 打开浏览器访问:`http://your-domain.com` 或 `https://your-domain.com`
|
||||
2. 应能看到登录页
|
||||
3. 点击「设置 WebSocket 服务地址」,填写实际可访问的 WebSocket 地址
|
||||
4. 输入账号密码登录
|
||||
5. 检查:
|
||||
- 登录是否成功
|
||||
- WebSocket 是否连接成功(查看浏览器控制台 Network → WS)
|
||||
- 各功能页面是否正常加载
|
||||
|
||||
---
|
||||
|
||||
## 五、常见问题
|
||||
|
||||
### 1. 页面空白
|
||||
|
||||
- **检查**:浏览器控制台是否有 JS 错误
|
||||
- **解决**:确认 `index.html` 中引用的 `web.js`、`styles.css` 路径正确;检查 Nginx 配置是否正确
|
||||
|
||||
### 2. 接口请求失败(404 或 CORS)
|
||||
|
||||
- **检查**:`BASE_API` 是否正确;后端是否允许跨域
|
||||
- **解决**:修改 `config/prod.env.js` 中的 `BASE_API` 后重新构建;后端配置 CORS
|
||||
|
||||
### 3. WebSocket 连接失败
|
||||
|
||||
- **检查**:浏览器控制台 Network → WS,查看连接地址和错误信息
|
||||
- **解决**:
|
||||
- 确认 WebSocket 服务已启动
|
||||
- 在登录页填写正确的地址(本机用 `ws://127.0.0.1:10010`,服务器用 `ws://服务器IP:10010`,HTTPS 用 `wss://`)
|
||||
- 如使用 Nginx 代理,确认代理配置正确
|
||||
|
||||
### 4. 静态资源 404
|
||||
|
||||
- **检查**:`static/` 目录是否上传完整
|
||||
- **解决**:重新上传 `dist/web/static/` 目录
|
||||
|
||||
### 5. 路由跳转异常
|
||||
|
||||
- **检查**:Nginx 配置中是否有 `try_files $uri $uri/ /index.html;`
|
||||
- **解决**:添加该配置后重启 Nginx
|
||||
|
||||
---
|
||||
|
||||
## 六、更新部署
|
||||
|
||||
当代码更新后:
|
||||
|
||||
1. 修改 `config/prod.env.js`(如需要)
|
||||
2. 执行 `npm run build:web`
|
||||
3. 将 `dist/web/` 下的文件**覆盖上传**到宝塔网站根目录
|
||||
4. 清除浏览器缓存或强制刷新(Ctrl+F5)
|
||||
|
||||
---
|
||||
|
||||
## 七、总结
|
||||
|
||||
- **构建产物**:`dist/web/` 目录
|
||||
- **上传位置**:宝塔网站根目录
|
||||
- **配置要点**:
|
||||
- `BASE_API`:后端 HTTP 接口地址
|
||||
- WebSocket:用户可在登录页配置,或通过 Nginx 反向代理
|
||||
- **访问方式**:`http(s)://your-domain.com`
|
||||
|
||||
部署完成后,用户访问网站即可使用,首次登录时记得在登录页配置 WebSocket 服务地址。
|
||||
@@ -1,82 +0,0 @@
|
||||
# WebSocket 与网页端部署说明
|
||||
|
||||
## 问题说明
|
||||
|
||||
程序通过 **WebSocket** 与后端服务通信(设备状态、作业列表、系统配置等)。该服务在**桌面端**场景下是 Windows 本机运行的服务(默认 `ws://127.0.0.1:10010`)。
|
||||
|
||||
**网页端打包部署后能否连接,取决于“浏览器连的是谁”:**
|
||||
|
||||
- 网页运行在**用户浏览器**里,`new WebSocket("ws://127.0.0.1:10010")` 会连接的是**当前用户所在设备**的 127.0.0.1,而不是你放网页的那台服务器。
|
||||
- 因此:
|
||||
- **用户和 Windows 服务在同一台电脑**:例如在本机浏览器打开部署在本机的页面,用 `ws://127.0.0.1:10010` 可以连到本机服务,**可以连接**。
|
||||
- **用户和 Windows 服务不在同一台电脑**:例如网页部署在服务器 A,用户在手机或另一台电脑 B 打开,此时 127.0.0.1 指向的是 B,连不到 A 上的服务,**无法连接**。
|
||||
|
||||
所以:**把程序打包放到网页端后,只有在“能访问到 WebSocket 服务”的情况下才可以连接**;默认写死 127.0.0.1 时,只适合“网页和服务在同一台机器”或“用户本机也跑了该服务”的场景。
|
||||
|
||||
---
|
||||
|
||||
## 已做改造:可配置 WebSocket 地址
|
||||
|
||||
为支持不同部署方式,已做两点改动:
|
||||
|
||||
### 1. 连接时优先读“用户配置”
|
||||
|
||||
- 连接 WebSocket 时**优先使用**本地存储中的 **`WS_SOCKET_API`**(localStorage)。
|
||||
- 若未配置,再使用构建时注入的 **`process.env.VUE_APP_SOCKET_API`**(默认 `ws://127.0.0.1:10010`)。
|
||||
|
||||
因此同一套网页包可以:
|
||||
|
||||
- 部署在服务器上,由用户在登录页填写“实际要连的 WebSocket 地址”;
|
||||
- 或由部署方在构建时通过环境变量写死一个公网/内网地址。
|
||||
|
||||
### 2. 登录页可填写并保存地址
|
||||
|
||||
- 在**登录页**增加 **“设置 WebSocket 服务地址”**(可展开/收起)。
|
||||
- 用户可输入例如:
|
||||
- `ws://127.0.0.1:10010`(本机服务)
|
||||
- `ws://192.168.1.100:10010`(内网某台机器上的服务)
|
||||
- `wss://your-domain.com/ws`(通过域名 + HTTPS 的 WSS 服务)
|
||||
- 失焦或保存后写入 **localStorage**,下次进入或刷新后,连接会使用该地址。
|
||||
|
||||
这样**网页端打包部署后**,只要用户(或管理员)在登录页把地址改成“实际可访问的 WebSocket 服务地址”,就可以正常连接。
|
||||
|
||||
---
|
||||
|
||||
## 常见部署方式与是否可连接
|
||||
|
||||
| 部署方式 | 网页访问方式 | WebSocket 地址建议 | 能否连接 |
|
||||
|----------|--------------|--------------------|----------|
|
||||
| 网页和 Windows 服务都在**同一台电脑** | 本机浏览器打开(如 http://localhost:9081 或 file://) | `ws://127.0.0.1:10010`(默认即可) | 可以 |
|
||||
| 网页在**服务器**,服务仍在**用户本机** | 用户在本机浏览器打开网站 | 在登录页设为 `ws://127.0.0.1:10010`(本机须已运行该服务) | 可以(仅限本机也跑服务时) |
|
||||
| 网页在**服务器**,服务在**内网另一台机器** | 用户在内网任意电脑打开网站 | 在登录页设为 `ws://内网机器IP:10010`(或该机域名) | 可以(需网络可达、无策略拦截) |
|
||||
| 网页在**公网**,服务在**公网服务器** | 任意设备打开网站 | 构建时设 `VUE_APP_SOCKET_API=wss://api.xxx.com/ws`,或在登录页填写该地址 | 可以(需后端提供 wss 与跨域策略) |
|
||||
|
||||
---
|
||||
|
||||
## 使用步骤(网页端部署后)
|
||||
|
||||
1. 打开登录页,点击 **“设置 WebSocket 服务地址”**。
|
||||
2. 填入实际可访问的地址,例如:
|
||||
- 本机:`ws://127.0.0.1:10010`
|
||||
- 内网机器:`ws://192.168.1.100:10010`
|
||||
- 公网:`wss://your-api.com/ws`
|
||||
3. 失焦后会自动保存到当前浏览器;之后登录或刷新页面,会使用该地址连接。
|
||||
4. 若更换环境(换电脑/换网络),在同一浏览器重新打开登录页,修改并保存新地址即可。
|
||||
|
||||
---
|
||||
|
||||
## 构建时写死地址(可选)
|
||||
|
||||
若部署环境固定(例如始终连同一台服务器),可在构建网页时注入默认地址,减少用户手动填写:
|
||||
|
||||
- 开发/测试:在 `config/dev.env.js` 中设置 `VUE_APP_SOCKET_API`。
|
||||
- 生产:在 `config/prod.env.js` 或 CI 中设置 `VUE_APP_SOCKET_API`(如 `wss://api.xxx.com/ws`)。
|
||||
|
||||
未在登录页保存过地址时,会使用该默认值;保存过后以登录页配置为准。
|
||||
|
||||
---
|
||||
|
||||
## 总结
|
||||
|
||||
- **默认 `ws://127.0.0.1:10010` 只适合“浏览器所在机器 = 运行 WebSocket 服务的机器”的情况。**
|
||||
- **网页端打包部署后**:通过**登录页可配置 WebSocket 地址**并写入 localStorage,连接时**优先使用该配置**,因此只要用户填写的地址在浏览器侧可达(本机/内网/公网),就可以正常连接。
|
||||
@@ -1,154 +0,0 @@
|
||||
# DiscWorker 网页端功能清单与实现情况
|
||||
|
||||
本文档列出全部功能,并标明:**无改动**(桌面/网页一致)、**有改动**(为兼容网页做了适配)、**网页端无法实现**(仅桌面端或需后端扩展)。
|
||||
|
||||
---
|
||||
|
||||
## 一、全局与基础设施
|
||||
|
||||
| 功能 | 实现情况 | 说明 |
|
||||
|------|----------|------|
|
||||
| 路由(/、/login、/manage、/set、/user、/404) | 无改动 | 使用 Vue Router,hash 模式,网页端可直接部署 |
|
||||
| 登录鉴权(permission.js) | 无改动 | 基于 token 与 store,与运行环境无关 |
|
||||
| 路由守卫(白名单、未登录跳转登录) | 无改动 | 纯前端逻辑 |
|
||||
| HTTP 请求(request.js、BASE_API) | 有改动 | 网页构建通过 DefinePlugin 注入 BASE_API,需配置后端地址 |
|
||||
| 多语言(vue-i18n) | 无改动 | 纯前端 |
|
||||
| Element UI、主题与尺寸 | 无改动 | 纯前端 |
|
||||
| Vuex Store(user、chat、app 等) | 有改动 | store 仅桌面端条件引入 vuex-electron,网页端不引入 |
|
||||
|
||||
---
|
||||
|
||||
## 二、登录页(/login)
|
||||
|
||||
| 功能 | 实现情况 | 说明 |
|
||||
|------|----------|------|
|
||||
| 用户名/密码输入、记住账号 | 无改动 | 纯表单 + localStorage |
|
||||
| 登录请求(/user/login) | 无改动 | 走 axios + BASE_API,网页端需后端可用 |
|
||||
| 新手引导(第一步) | 无改动 | 纯前端弹窗与步骤 |
|
||||
| **WebSocket 服务地址设置** | **有改动** | 登录页可展开「设置 WebSocket 服务地址」,填写后写入 localStorage(key: `WS_SOCKET_API`),连接时优先使用该地址,便于网页端部署后连接本机/内网/公网服务。详见 [WEBSOCKET_DEPLOY.md](./WEBSOCKET_DEPLOY.md) |
|
||||
|
||||
---
|
||||
|
||||
## 三、首页 / 看板(/)
|
||||
|
||||
### 3.1 顶部与状态
|
||||
|
||||
| 功能 | 实现情况 | 说明 |
|
||||
|------|----------|------|
|
||||
| 回到主界面、系统设置入口 | 无改动 | 路由跳转 |
|
||||
| 服务启停开关(打开/关闭服务) | **网页端无法实现** | 依赖桌面端执行 `control.sh`,网页端显示「网页端」占位与 tooltip「仅桌面端支持启停服务」 |
|
||||
| 完成刻录数/废弃刻录数/错误数 | 无改动 | 来自 WebSocket 的 task_list,与端无关 |
|
||||
| 试用版天数、前往激活 | 有改动 | 「前往激活」仅桌面端显示(依赖 runCmd 执行 regist.sh);网页端不显示该按钮 |
|
||||
| 用户信息组件 | 无改动 | 展示 store 中的用户信息 |
|
||||
|
||||
### 3.2 设备与作业
|
||||
|
||||
| 功能 | 实现情况 | 说明 |
|
||||
|------|----------|------|
|
||||
| 设备状态展示(图示、状态文案) | 无改动 | 数据来自 WebSocket printer_info |
|
||||
| 运行选项(重启设备、清除错误、设置光盘桶、刷新光盘桶等) | 无改动 | 通过 WebSocket 下发,网页端在已连接服务时可用 |
|
||||
| 作业列表(表格、重试/取消) | 无改动 | WebSocket task_list + websocketsend |
|
||||
| 新建作业、打开作业 | 有改动 | **打开作业**:桌面端用系统对话框选 .dwk 后读文件;网页端用 `<input type="file">` 选文件并 FileReader 读内容,再解析填入 workAdd,**可正常使用**。新建作业为打开弹窗,无改动 |
|
||||
| 保存作业(workAdd 内) | 有改动 | 桌面端:系统保存对话框 + 写入本地路径;网页端:无保存对话框,直接触发浏览器下载 .dwk 文件,**可正常使用** |
|
||||
| 动态/日志(最新日志、全部日志) | 无改动 | 数据来自 WebSocket log,与端无关 |
|
||||
|
||||
### 3.3 帮助与其它
|
||||
|
||||
| 功能 | 实现情况 | 说明 |
|
||||
|------|----------|------|
|
||||
| 帮助(打开帮助文档) | 有改动 | 桌面端:IPC 打开本机 PDF;网页端:`window.open(HELP_PDF_URL)`,需部署或配置帮助 PDF 地址 |
|
||||
| 关于我们弹窗 | 无改动 | 纯前端 |
|
||||
|
||||
---
|
||||
|
||||
## 四、作业弹窗(WorkAdd 组件)
|
||||
|
||||
### 4.1 作业配置
|
||||
|
||||
| 功能 | 实现情况 | 说明 |
|
||||
|------|----------|------|
|
||||
| 任务名称、CD 类型、刻录类型、标签、UDF、关联任务等表单 | 无改动 | 纯表单与校验 |
|
||||
| 模板下拉(从 User Templates 加载) | 有改动 | 桌面端:读本地「User Templates」目录;网页端:readdir 不可用,模板列表为空,**网页端无法使用本地模板列表**;若需网页端模板,需后端提供列表接口 |
|
||||
| 选择模板(加载 .soon 文件) | 有改动 | 仅桌面端可用(依赖本地路径 + readFile);网页端无模板列表,该入口不适用 |
|
||||
| 「···」选择作业文件(.soon) | 有改动 | 桌面端:系统对话框选文件;网页端:可用平台 showOpenFileDialog(input file),**单文件选择可用**;多选仍仅桌面端 |
|
||||
| 新建标签(打开 SoonDesign) | **网页端无法实现** | 依赖桌面端 exec('soondesign'),网页端按钮改为灰色 + tooltip「仅桌面端支持」 |
|
||||
|
||||
### 4.2 文件与路径
|
||||
|
||||
| 功能 | 实现情况 | 说明 |
|
||||
|------|----------|------|
|
||||
| 添加文件(单/多选) | 有改动 | 桌面端:系统对话框 + 本地路径 + stat;网页端:input file 单文件,以「web://」虚拟路径加入列表,**可添加单文件** |
|
||||
| 添加文件夹 | **网页端无法实现** | 依赖系统选择目录,网页端 showOpenDirectoryDialog 拒绝,提示「仅桌面端支持」 |
|
||||
| 拖放添加文件/文件夹 | 有改动 | 桌面端:支持路径与 calcSize;网页端:仅支持 File 对象(无 path),用 name 与 size,不计算目录大小,**可拖放文件** |
|
||||
| 选择路径(归档路径) | **网页端无法实现** | 依赖系统选择目录,网页端不支持 |
|
||||
| 保存作业为 .dwk | 见「三、首页」保存作业 | 网页端为下载 |
|
||||
|
||||
### 4.3 提交与打包
|
||||
|
||||
| 功能 | 实现情况 | 说明 |
|
||||
|------|----------|------|
|
||||
| 提交任务(刻录/打印) | 有改动 | 桌面端:提交本地路径 + mtime 等,由本机服务访问本地文件;网页端:当前提交的为「web://」虚拟路径,**后端无法按路径读文件,故网页端提交任务需后端提供文件上传接口**,否则仅桌面端可完整使用 |
|
||||
| 复制文件到某路径(file-back 复制) | **网页端无法实现** | 依赖 fs.cp,仅桌面端;网页端点击会提示「仅桌面端支持」 |
|
||||
| 压缩为 zip/加密 zip(file-back) | **网页端无法实现** | 依赖 fs stream + archiver,仅桌面端;网页端会提示「仅桌面端支持」 |
|
||||
|
||||
---
|
||||
|
||||
## 五、系统管理(/manage)
|
||||
|
||||
| 功能 | 实现情况 | 说明 |
|
||||
|------|----------|------|
|
||||
| 设置光驱(手动设置光驱、设置光盘桶类型) | 无改动 | 全部通过 WebSocket 与后端/设备通信,网页端在连接服务后**可正常使用** |
|
||||
| 校准设备、维护设备 | 无改动 | WebSocket 下发指令,网页端可用 |
|
||||
| 刷新时间、执行 | 无改动 | WebSocket,网页端可用 |
|
||||
| 清除任务(服务重启时是否删除已完成/已取消任务) | 无改动 | WebSocket/配置,网页端可用 |
|
||||
| 新手引导开关 | 无改动 | 本地存储,网页端可用 |
|
||||
| 光盘类型列表(getCurrentOSDiscTypes) | 有改动 | fileSize 在网页端返回固定 1024 与 web 类型,manage 仅读配置列表,**网页端可正常使用** |
|
||||
|
||||
---
|
||||
|
||||
## 六、系统配置(/set)
|
||||
|
||||
| 功能 | 实现情况 | 说明 |
|
||||
|------|----------|------|
|
||||
| LogLevel、AutoRetryTimes、PrintQuality、CacheDir、SkipSr0Check 等 | 无改动 | 通过 WebSocket 获取/提交系统配置,网页端**可正常使用** |
|
||||
| 保存 | 无改动 | websocketsend,网页端可用 |
|
||||
|
||||
---
|
||||
|
||||
## 七、用户管理(/user)
|
||||
|
||||
| 功能 | 实现情况 | 说明 |
|
||||
|------|----------|------|
|
||||
| 用户列表展示(当前为静态示例数据) | 无改动 | 纯前端表格,若后续接真实接口则与端无关 |
|
||||
|
||||
---
|
||||
|
||||
## 八、其它
|
||||
|
||||
| 功能 | 实现情况 | 说明 |
|
||||
|------|----------|------|
|
||||
| 404 页 | 无改动 | 纯静态 |
|
||||
| TabBar 导航 | 无改动 | 路由跳转 |
|
||||
| WebSocket 连接(chat 模块) | 有改动 | 网页端通过 VUE_APP_SOCKET_API 连接,需后端或本机服务提供 WebSocket;**网页端需能访问该地址**(同源或 CORS/代理) |
|
||||
| 文件大小显示(filterSize、fileSize) | 有改动 | fileSize 在网页端 getOS 返回 'web'、getBaseSize 固定 1024,**显示正常** |
|
||||
| 右键菜单(show-context-menu) | 有改动 | 桌面端 IPC;网页端 no-op,无影响 |
|
||||
|
||||
---
|
||||
|
||||
## 九、总结表
|
||||
|
||||
| 类型 | 数量 | 说明 |
|
||||
|------|------|------|
|
||||
| **无改动** | 绝大多数 | 登录、配置、设备指令、作业列表、重试/取消、日志、系统管理、系统配置等均与运行环境无关或仅依赖 WebSocket/HTTP |
|
||||
| **有改动且网页端可用** | 若干 | 打开作业(选文件 + 读内容)、保存作业(下载)、帮助(新标签打开 PDF)、添加单文件、拖放文件、服务开关/激活 UI 占位与提示 |
|
||||
| **网页端无法实现** | 少量 | 服务启停(执行 control.sh)、前往激活(执行 regist.sh)、新建标签(打开 SoonDesign)、选择目录、复制到本地路径、本地 zip 打包;提交任务在无上传接口时仅桌面端完整可用 |
|
||||
|
||||
---
|
||||
|
||||
## 十、网页端使用前提
|
||||
|
||||
1. **后端与 WebSocket**:配置 `BASE_API`、`VUE_APP_SOCKET_API`,确保登录接口与 WebSocket 可访问(同源或代理/CORS)。
|
||||
2. **帮助文档**:如需帮助入口,部署帮助 PDF 并配置 `HELP_PDF_URL`(或使用默认 `/help/User Manual.pdf`)。
|
||||
3. **提交任务**:若要在网页端完整使用「提交刻录/打印任务」,需后端提供文件上传接口,前端改为先上传再提交任务参数;当前实现仍为提交路径,仅桌面端可被本机服务访问。
|
||||
|
||||
按当前实现,网页端可正常完成:登录、看板查看、设备状态与指令、作业列表与重试/取消、打开/保存作业(选文件与下载)、系统管理、系统配置、日志查看等;仅上述「网页端无法实现」项在网页端为禁用或提示。
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "discworker",
|
||||
"version": "0.0.1",
|
||||
"version": "1.1.1.10",
|
||||
"author": "24kycj <1637269896@qq.com>",
|
||||
"description": "KaShu SoonWorkerDisk",
|
||||
"homepage": "https://www.cardsoon.com/index.php/cn",
|
||||
@@ -15,6 +15,7 @@
|
||||
"dev:web": "cross-env NODE_ENV=development node node_modules/webpack-dev-server/bin/webpack-dev-server.js --config .electron-vue/webpack.web.config.js --open",
|
||||
"electron:linux": "vue-cli-service electron:build -l --x64",
|
||||
"electron:arm": "vue-cli-service electron:build -l --arm64",
|
||||
"build:deb:arm64": "bash scripts/build-deb-arm64.sh",
|
||||
"dev": "node .electron-vue/dev-runner.js",
|
||||
"lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter src",
|
||||
"lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src",
|
||||
@@ -24,20 +25,20 @@
|
||||
"postinstall": "npm run lint:fix"
|
||||
},
|
||||
"build": {
|
||||
"productName": "discworker",
|
||||
"productName": "DiscWorker",
|
||||
"appId": "com.kashu.soonworkerdisk",
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "./User Templates",
|
||||
"to": "."
|
||||
"to": "User Templates"
|
||||
},
|
||||
{
|
||||
"from": "./cardsoonServer",
|
||||
"to": "."
|
||||
"from": "./CardsoonServer",
|
||||
"to": "CardsoonServer"
|
||||
},
|
||||
{
|
||||
"from": "./help",
|
||||
"to": "."
|
||||
"to": "help"
|
||||
}
|
||||
],
|
||||
"directories": {
|
||||
@@ -68,11 +69,13 @@
|
||||
"icon": "build/icons/icon.ico"
|
||||
},
|
||||
"linux": {
|
||||
"icon": "build/icons"
|
||||
"icon": "build/icons",
|
||||
"executableName": "DiscWorker"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.1.2",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"archiver": "^7.0.1",
|
||||
"archiver-zip-encrypted": "^2.0.0",
|
||||
"axios": "^0.18.0",
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
# 构建 Linux arm64 deb,并保证安装路径与 productName/executableName=DiscWorker 一致
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$ROOT"
|
||||
|
||||
EXE="DiscWorker"
|
||||
SANITIZED="DiscWorker"
|
||||
OPT_DIR="/opt/${SANITIZED}"
|
||||
BIN_LINK="/usr/bin/${EXE}"
|
||||
|
||||
echo "[build-deb-arm64] executable: ${EXE}"
|
||||
echo "[build-deb-arm64] opt dir: ${OPT_DIR}"
|
||||
|
||||
node .electron-vue/build.js
|
||||
npx electron-builder --linux deb --arm64
|
||||
|
||||
shopt -s nullglob
|
||||
mapfile -t DEBS < <(ls -t build/*.deb 2>/dev/null || true)
|
||||
if [[ ${#DEBS[@]} -eq 0 ]]; then
|
||||
echo "[build-deb-arm64] 未找到 build/*.deb" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DEB="${DEBS[0]}"
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
|
||||
echo "[build-deb-arm64] patching: $DEB"
|
||||
dpkg-deb -R "$DEB" "$WORK/pkg"
|
||||
CONTROL_DIR="$WORK/pkg/DEBIAN"
|
||||
mkdir -p "$CONTROL_DIR"
|
||||
|
||||
# 若 builder 已生成钩子,把旧小写路径替换为 DiscWorker;否则写入默认钩子
|
||||
patch_or_write() {
|
||||
local file="$1"
|
||||
local kind="$2"
|
||||
if [[ -f "$file" ]]; then
|
||||
sed -i \
|
||||
-e "s|/opt/discworker|/opt/${SANITIZED}|g" \
|
||||
-e "s|/usr/bin/discworker|/usr/bin/${EXE}|g" \
|
||||
-e "s|discworker/chrome-sandbox|${SANITIZED}/chrome-sandbox|g" \
|
||||
-e "s|/opt/discworker/discworker|${OPT_DIR}/${EXE}|g" \
|
||||
"$file"
|
||||
chmod 755 "$file"
|
||||
echo "[build-deb-arm64] patched existing ${kind}"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ "$kind" == "postinst" ]]; then
|
||||
cat > "$file" <<EOF
|
||||
#!/bin/bash
|
||||
set -e
|
||||
if [ -f ${OPT_DIR}/chrome-sandbox ]; then
|
||||
chown root:root ${OPT_DIR}/chrome-sandbox
|
||||
chmod 4755 ${OPT_DIR}/chrome-sandbox
|
||||
fi
|
||||
if [ -x ${OPT_DIR}/${EXE} ]; then
|
||||
ln -sfn ${OPT_DIR}/${EXE} ${BIN_LINK}
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
else
|
||||
cat > "$file" <<EOF
|
||||
#!/bin/bash
|
||||
set -e
|
||||
if [ "\$1" = "remove" ] || [ "\$1" = "purge" ]; then
|
||||
rm -f ${BIN_LINK}
|
||||
fi
|
||||
exit 0
|
||||
EOF
|
||||
fi
|
||||
chmod 755 "$file"
|
||||
echo "[build-deb-arm64] wrote default ${kind}"
|
||||
}
|
||||
|
||||
patch_or_write "$CONTROL_DIR/postinst" "postinst"
|
||||
patch_or_write "$CONTROL_DIR/postrm" "postrm"
|
||||
|
||||
OUT="build/${SANITIZED}-arm64-patched.deb"
|
||||
dpkg-deb -b "$WORK/pkg" "$OUT"
|
||||
echo "[build-deb-arm64] done: $OUT"
|
||||
@@ -2,7 +2,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>DiscWorker V1.01</title>
|
||||
<title>DiscWorker</title>
|
||||
<link rel="icon" href="<%= process.env.NODE_ENV === 'development' ? '/static/favicon.ico' : './static/favicon.ico' %>" type="image/x-icon">
|
||||
<% if (typeof isWeb === 'undefined' ? htmlWebpackPlugin.options.nodeModules : !isWeb && htmlWebpackPlugin.options.nodeModules) { %>
|
||||
<!-- Add `node_modules/` to global paths so `require` works properly in development (Electron only) -->
|
||||
<script>
|
||||
|
||||
@@ -23,6 +23,13 @@ const root = process.env.NODE_ENV === "development"
|
||||
? path.resolve(__dirname, '../../')
|
||||
: path.dirname(app.getPath("exe"))
|
||||
|
||||
function getAppIcon() {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
return path.join(root, 'src/renderer/assets/images/logo.png')
|
||||
}
|
||||
return path.join(__dirname, 'static/icon.png')
|
||||
}
|
||||
|
||||
// 日志函数
|
||||
function writeLog(message) {
|
||||
console.log(`[${new Date().toISOString()}] ${message}`)
|
||||
@@ -38,6 +45,7 @@ function createWindow () {
|
||||
useContentSize: true,
|
||||
show: false,
|
||||
fullscreen: false,
|
||||
icon: getAppIcon(),
|
||||
webPreferences: {
|
||||
webSecurity: false,
|
||||
nodeIntegration: true,
|
||||
@@ -46,8 +54,10 @@ function createWindow () {
|
||||
})
|
||||
require("@electron/remote/main").initialize();
|
||||
require("@electron/remote/main").enable(mainWindow.webContents);
|
||||
// 禁用默认菜单
|
||||
// Menu.setApplicationMenu(null);
|
||||
// dev 保留 Electron 默认菜单;打包后隐藏
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
Menu.setApplicationMenu(null)
|
||||
}
|
||||
// mainWindow.maximize()
|
||||
mainWindow.show()
|
||||
mainWindow.loadURL(winURL)
|
||||
@@ -56,7 +66,12 @@ function createWindow () {
|
||||
})
|
||||
}
|
||||
|
||||
app.on('ready', createWindow)
|
||||
app.on('ready', () => {
|
||||
if (process.platform === 'darwin' && app.dock) {
|
||||
app.dock.setIcon(getAppIcon())
|
||||
}
|
||||
createWindow()
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
|
||||
|
Before Width: | Height: | Size: 307 KiB After Width: | Height: | Size: 478 KiB |
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 462 KiB |
|
Before Width: | Height: | Size: 205 KiB After Width: | Height: | Size: 513 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 425 KiB |
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 9.2 KiB After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,781 @@
|
||||
<template>
|
||||
|
||||
<dw-card class="device-panel" flex-body :title="''">
|
||||
|
||||
<template #header>
|
||||
|
||||
<div class="device-panel__head">
|
||||
|
||||
<console-icon name="device" :size="16" />
|
||||
|
||||
<span>设备状态</span>
|
||||
|
||||
</div>
|
||||
|
||||
<dw-status-pill :variant="tagVariant" :label="tagLabel" />
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<div v-if="loading" class="device-panel__skeleton">
|
||||
|
||||
<div class="dw-skeleton device-panel__sk-preview" />
|
||||
|
||||
<div class="dw-skeleton" style="height:88px;margin-bottom:12px" />
|
||||
|
||||
<div class="dw-skeleton" style="height:96px" />
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<template v-else>
|
||||
|
||||
<div class="device-panel__preview" :class="{ 'device-panel__preview--offline': !connected }">
|
||||
|
||||
<span class="device-panel__online-dot" :class="{ 'device-panel__online-dot--off': !online }" />
|
||||
|
||||
<img class="device-panel__img" :src="deviceImg" alt="device" />
|
||||
|
||||
<div class="device-panel__name">{{ deviceTitle }}</div>
|
||||
|
||||
<div class="device-panel__sn">序列号: {{ serialLabel }}</div>
|
||||
|
||||
<div v-if="!connected" class="device-panel__offline-tip">
|
||||
|
||||
<console-icon name="wifi" :size="14" />
|
||||
|
||||
请确认服务已启动并已联机
|
||||
|
||||
<button type="button" class="device-panel__refresh" @click="$emit('refresh')">刷新</button>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="device-panel__section">
|
||||
|
||||
<div class="device-panel__section-title">
|
||||
|
||||
<console-icon name="ink" :size="14" />
|
||||
|
||||
墨水余量
|
||||
|
||||
</div>
|
||||
|
||||
<ink-levels
|
||||
|
||||
v-if="connected && hasInkData"
|
||||
|
||||
:blue="printerInfo.blue_last"
|
||||
|
||||
:red="printerInfo.red_last"
|
||||
|
||||
:yellow="printerInfo.yellow_last"
|
||||
|
||||
/>
|
||||
|
||||
<div v-else class="device-panel__placeholder">联机后显示墨水余量</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="device-panel__section">
|
||||
|
||||
<div class="device-panel__section-title">
|
||||
|
||||
<console-icon name="bin" :size="14" />
|
||||
|
||||
盘仓监控
|
||||
|
||||
</div>
|
||||
|
||||
<disc-bins v-if="discBins.length" :bins="discBins" />
|
||||
|
||||
<div v-else class="device-panel__placeholder">联机后显示盘仓容量</div>
|
||||
|
||||
<div v-if="showSureBar && changeCD.req_info" class="device-panel__cd-types">
|
||||
|
||||
<div
|
||||
|
||||
v-for="(item, index) in changeCD.req_info.strong_list"
|
||||
|
||||
:key="index"
|
||||
|
||||
class="device-panel__cd-type-row"
|
||||
|
||||
>
|
||||
|
||||
<span>盘仓 {{ item.strong_pos }}</span>
|
||||
|
||||
<el-select
|
||||
|
||||
size="mini"
|
||||
|
||||
:value="item.strong_type"
|
||||
|
||||
@change="(v) => $emit('command-cd', v, item.strong_pos)"
|
||||
|
||||
>
|
||||
|
||||
<el-option
|
||||
|
||||
v-for="opt in cdTypeOptions"
|
||||
|
||||
:key="opt.value"
|
||||
|
||||
:label="opt.label"
|
||||
|
||||
:value="opt.value"
|
||||
|
||||
/>
|
||||
|
||||
</el-select>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div v-if="showSureBar" class="device-panel__sure">
|
||||
|
||||
<span>{{ sureText }}</span>
|
||||
|
||||
<div>
|
||||
|
||||
<button type="button" class="dw-btn dw-btn--ghost" @click="$emit('cancel-cd')">取消</button>
|
||||
|
||||
<button type="button" class="dw-btn dw-btn--primary" @click="$emit('confirm-cd')">确定</button>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<details v-if="connected" class="device-panel__details">
|
||||
|
||||
<summary>设备详情</summary>
|
||||
|
||||
<div class="device-panel__meta">
|
||||
|
||||
<div>主机名:{{ printerInfo.system_name || '—' }}</div>
|
||||
|
||||
<div>主机IP:{{ printerInfo.system_ip || '—' }}</div>
|
||||
|
||||
<div>缓存:{{ cacheText }}</div>
|
||||
|
||||
<div>状态:{{ runText }}</div>
|
||||
|
||||
</div>
|
||||
|
||||
</details>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<template #footer>
|
||||
|
||||
<button type="button" class="dw-btn dw-btn--primary device-panel__cta" @click="$emit('new-work')">
|
||||
|
||||
<console-icon name="plus" :size="16" />
|
||||
|
||||
新建刻录作业
|
||||
|
||||
</button>
|
||||
|
||||
<el-dropdown trigger="click" @command="(cmd) => $emit('maintain', cmd)">
|
||||
|
||||
<button type="button" class="dw-btn dw-btn--outline device-panel__more">
|
||||
|
||||
<console-icon name="settings" :size="14" />
|
||||
|
||||
更多维护
|
||||
|
||||
</button>
|
||||
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
|
||||
<el-dropdown-item v-for="item in runList" :key="item.value" :command="item.value">
|
||||
|
||||
{{ item.label }}
|
||||
|
||||
</el-dropdown-item>
|
||||
|
||||
</el-dropdown-menu>
|
||||
|
||||
</el-dropdown>
|
||||
|
||||
</template>
|
||||
|
||||
</dw-card>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import DwCard from '../ui/DwCard.vue'
|
||||
|
||||
import DwStatusPill from '../ui/DwStatusPill.vue'
|
||||
|
||||
import ConsoleIcon from '../icons/ConsoleIcon.vue'
|
||||
|
||||
import InkLevels from './InkLevels.vue'
|
||||
|
||||
import DiscBins from './DiscBins.vue'
|
||||
|
||||
import { getDeviceImages, resolveDeviceImageKey } from '@/utils/console/constants'
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
|
||||
name: 'DevicePanel',
|
||||
|
||||
components: { DwCard, DwStatusPill, ConsoleIcon, InkLevels, DiscBins },
|
||||
|
||||
props: {
|
||||
|
||||
printerInfo: { type: Object, default: () => ({}) },
|
||||
|
||||
runText: { type: String, default: '' },
|
||||
|
||||
tagVariant: { type: String, default: 'idle' },
|
||||
|
||||
tagLabel: { type: String, default: '空闲' },
|
||||
|
||||
online: { type: Boolean, default: false },
|
||||
|
||||
loading: { type: Boolean, default: false },
|
||||
|
||||
runList: { type: Array, default: () => [] },
|
||||
|
||||
currentRunVal: { type: [Number, String], default: 0 },
|
||||
|
||||
sureText: { type: String, default: '' },
|
||||
|
||||
changeCD: { type: Object, default: () => ({}) },
|
||||
|
||||
cdTypeOptions: { type: Array, default: () => [] },
|
||||
|
||||
CDTypes: { type: Object, default: () => ({}) }
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
connected() {
|
||||
|
||||
return !!(this.printerInfo && this.printerInfo.printer_name)
|
||||
|
||||
},
|
||||
|
||||
hasInkData() {
|
||||
|
||||
const p = this.printerInfo
|
||||
|
||||
return p.blue_last != null || p.red_last != null || p.yellow_last != null
|
||||
|
||||
},
|
||||
|
||||
deviceImages() {
|
||||
|
||||
return getDeviceImages()
|
||||
|
||||
},
|
||||
|
||||
deviceImg() {
|
||||
|
||||
const key = resolveDeviceImageKey(this.printerInfo.printer_name)
|
||||
|
||||
const imgs = this.deviceImages[key]
|
||||
|
||||
if (!imgs) return ''
|
||||
|
||||
return this.connected && this.printerInfo.cover_flag ? imgs.img2 : imgs.img1
|
||||
|
||||
},
|
||||
|
||||
deviceTitle() {
|
||||
|
||||
const p = this.printerInfo.printer_path
|
||||
|
||||
if (!this.connected) return 'TF-Publish 4200 系列'
|
||||
|
||||
const n = this.printerInfo.printer_name || ''
|
||||
|
||||
if (n === '4200' || n === 'DP-4200') {
|
||||
|
||||
return `TF-Publish 4200${p ? ` (${p})` : ''}`
|
||||
|
||||
}
|
||||
|
||||
return `国产光盘刻录一体机 ${n}${p ? ` (${p})` : ''}`
|
||||
|
||||
},
|
||||
|
||||
serialLabel() {
|
||||
|
||||
return (this.connected && this.printerInfo.serial_no) || '—'
|
||||
|
||||
},
|
||||
|
||||
cacheText() {
|
||||
|
||||
const free = this.printerInfo.free_cache_space
|
||||
|
||||
const total = this.printerInfo.total_cache_space
|
||||
|
||||
if (!total) return '—'
|
||||
|
||||
return `${this.$filterSize(free)} / ${this.$filterSize(total)}`
|
||||
|
||||
},
|
||||
|
||||
showSureBar() {
|
||||
|
||||
return parseInt(this.currentRunVal) === 6
|
||||
|
||||
},
|
||||
|
||||
discBins() {
|
||||
|
||||
if (!this.connected) return []
|
||||
|
||||
const info = this.printerInfo
|
||||
|
||||
const key = resolveDeviceImageKey(info.printer_name)
|
||||
|
||||
const limits = this.deviceImages[key] || this.deviceImages['4200']
|
||||
|
||||
const sl = info.strongList || this.buildStrongList(info.strong_list)
|
||||
|
||||
const build = (pos, labelKey) => {
|
||||
|
||||
const row = sl[pos]
|
||||
|
||||
if (!row) return null
|
||||
|
||||
const count = row.cd_num || 0
|
||||
|
||||
const percent = Math.min(100, (count / 50) * 100)
|
||||
|
||||
let levelClass = ''
|
||||
|
||||
if (count < limits.num2) levelClass = 'disc-bins__stack--warn'
|
||||
|
||||
if (count < limits.num1) levelClass = 'disc-bins__stack--danger'
|
||||
|
||||
const typeLabel = this.CDTypes[row.cd_type] || '光盘'
|
||||
|
||||
return { key: labelKey, label: typeLabel, count, percent, levelClass }
|
||||
|
||||
}
|
||||
|
||||
return [build(1, 'l'), build(2, 'r')].filter(Boolean)
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
buildStrongList(list) {
|
||||
|
||||
if (!list || !list.length) return {}
|
||||
|
||||
const out = {}
|
||||
|
||||
list.forEach((item) => {
|
||||
|
||||
out[item.strong_pos] = item
|
||||
|
||||
})
|
||||
|
||||
return out
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.device-panel {
|
||||
|
||||
height: 100%;
|
||||
|
||||
box-shadow: var(--dw-shadow-card);
|
||||
|
||||
|
||||
|
||||
&__head {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
font-size: 15px;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
color: var(--dw-text);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__sk-preview {
|
||||
|
||||
height: 160px;
|
||||
|
||||
margin-bottom: 12px;
|
||||
|
||||
border-radius: var(--dw-radius-md);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__preview {
|
||||
|
||||
position: relative;
|
||||
|
||||
text-align: center;
|
||||
|
||||
padding: 18px 16px 14px;
|
||||
|
||||
margin-bottom: 14px;
|
||||
|
||||
background: var(--dw-gradient-device);
|
||||
|
||||
border-radius: var(--dw-radius-md);
|
||||
|
||||
border: 1px solid rgba(30, 79, 217, 0.08);
|
||||
|
||||
|
||||
|
||||
&--offline {
|
||||
|
||||
.device-panel__img {
|
||||
|
||||
opacity: 0.72;
|
||||
|
||||
filter: grayscale(0.25);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__online-dot {
|
||||
|
||||
position: absolute;
|
||||
|
||||
top: 12px;
|
||||
|
||||
right: 12px;
|
||||
|
||||
width: 10px;
|
||||
|
||||
height: 10px;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
background: var(--dw-success);
|
||||
|
||||
box-shadow: 0 0 0 3px rgba(5, 150, 105, 0.15);
|
||||
|
||||
animation: dw-pulse 2s infinite;
|
||||
|
||||
|
||||
|
||||
&--off {
|
||||
|
||||
background: var(--dw-text-muted);
|
||||
|
||||
box-shadow: none;
|
||||
|
||||
animation: none;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__img {
|
||||
|
||||
max-width: 168px;
|
||||
|
||||
max-height: 120px;
|
||||
|
||||
object-fit: contain;
|
||||
|
||||
display: block;
|
||||
|
||||
margin: 0 auto;
|
||||
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__name {
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
margin-top: 10px;
|
||||
|
||||
line-height: 1.35;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__sn {
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: var(--dw-text-secondary);
|
||||
|
||||
margin-top: 4px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__offline-tip {
|
||||
|
||||
display: inline-flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
flex-wrap: wrap;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
gap: 6px;
|
||||
|
||||
margin-top: 10px;
|
||||
|
||||
padding: 4px 10px;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: var(--dw-warning);
|
||||
|
||||
background: var(--dw-warning-soft);
|
||||
|
||||
border-radius: 999px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__refresh {
|
||||
|
||||
border: none;
|
||||
|
||||
background: none;
|
||||
|
||||
color: var(--dw-primary);
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
padding: 0 2px;
|
||||
|
||||
text-decoration: underline;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__section {
|
||||
|
||||
margin-bottom: 14px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__section-title {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 6px;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
margin-bottom: 10px;
|
||||
|
||||
color: var(--dw-text);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__placeholder {
|
||||
|
||||
padding: 14px 12px;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: var(--dw-text-muted);
|
||||
|
||||
text-align: center;
|
||||
|
||||
background: #f8fafc;
|
||||
|
||||
border: 1px dashed var(--dw-border);
|
||||
|
||||
border-radius: var(--dw-radius-sm);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__cd-types {
|
||||
|
||||
margin-top: 10px;
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__cd-type-row {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: var(--dw-text-secondary);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__sure {
|
||||
|
||||
display: flex;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
align-items: center;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
background: var(--dw-primary-soft);
|
||||
|
||||
border-radius: var(--dw-radius-sm);
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
margin-bottom: 10px;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__details {
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: var(--dw-text-secondary);
|
||||
|
||||
margin-bottom: 8px;
|
||||
|
||||
|
||||
|
||||
summary {
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
color: var(--dw-primary);
|
||||
|
||||
margin-bottom: 6px;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__meta div {
|
||||
|
||||
line-height: 1.8;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__cta {
|
||||
|
||||
width: 100%;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
margin-bottom: 8px;
|
||||
|
||||
padding: 10px 14px;
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__more {
|
||||
|
||||
width: 100%;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
|
||||
<div class="disc-bins">
|
||||
|
||||
<div v-for="bin in bins" :key="bin.key" class="disc-bins__item">
|
||||
|
||||
<div class="disc-bins__box">
|
||||
|
||||
<div
|
||||
|
||||
class="disc-bins__stack"
|
||||
|
||||
:style="{ height: bin.percent + '%' }"
|
||||
|
||||
:class="bin.levelClass"
|
||||
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="disc-bins__label">{{ bin.label }}</div>
|
||||
|
||||
<div class="disc-bins__count">{{ bin.count }} 张</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
|
||||
name: 'DiscBins',
|
||||
|
||||
props: {
|
||||
|
||||
bins: { type: Array, default: () => [] }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.disc-bins {
|
||||
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
gap: 14px;
|
||||
|
||||
|
||||
|
||||
&__item {
|
||||
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__box {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: flex-end;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
height: 92px;
|
||||
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
|
||||
|
||||
border: 1px solid var(--dw-border);
|
||||
|
||||
border-radius: var(--dw-radius-md);
|
||||
|
||||
padding: 10px 14px 8px;
|
||||
|
||||
box-shadow: inset 0 1px 2px rgba(15, 23, 42, 0.04);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__stack {
|
||||
|
||||
width: 56%;
|
||||
|
||||
background: var(--dw-gradient-disc);
|
||||
|
||||
border-radius: 6px 6px 4px 4px;
|
||||
|
||||
min-height: 6px;
|
||||
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.25);
|
||||
|
||||
transition: height 0.35s ease;
|
||||
|
||||
position: relative;
|
||||
|
||||
|
||||
|
||||
&::after {
|
||||
|
||||
content: '';
|
||||
|
||||
position: absolute;
|
||||
|
||||
top: -3px;
|
||||
|
||||
left: 8%;
|
||||
|
||||
right: 8%;
|
||||
|
||||
height: 5px;
|
||||
|
||||
background: #94a3b8;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&--warn {
|
||||
|
||||
background: linear-gradient(180deg, #fde68a 0%, #f59e0b 100%);
|
||||
|
||||
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.25);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&--danger {
|
||||
|
||||
background: linear-gradient(180deg, #fecaca 0%, #ef4444 100%);
|
||||
|
||||
box-shadow: 0 2px 8px rgba(239, 68, 68, 0.25);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__label {
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
color: var(--dw-text);
|
||||
|
||||
margin-top: 8px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__count {
|
||||
|
||||
font-size: 11px;
|
||||
|
||||
color: var(--dw-text-secondary);
|
||||
|
||||
margin-top: 2px;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
|
||||
<div class="ink-levels">
|
||||
|
||||
<div v-for="item in items" :key="item.key" class="ink-levels__row">
|
||||
|
||||
<div class="ink-levels__label">
|
||||
|
||||
<span class="ink-levels__swatch" :style="{ background: item.color }" />
|
||||
|
||||
<span>{{ item.label }}</span>
|
||||
|
||||
<span class="ink-levels__pct">{{ item.value }}%</span>
|
||||
|
||||
</div>
|
||||
|
||||
<dw-progress-bar :value="item.value" :color="item.color" />
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import DwProgressBar from '../ui/DwProgressBar.vue'
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
|
||||
name: 'InkLevels',
|
||||
|
||||
components: { DwProgressBar },
|
||||
|
||||
props: {
|
||||
|
||||
blue: { type: Number, default: 0 },
|
||||
|
||||
red: { type: Number, default: 0 },
|
||||
|
||||
yellow: { type: Number, default: 0 }
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
items() {
|
||||
|
||||
return [
|
||||
|
||||
{ key: 'c', label: 'Cyan (青)', value: parseInt(this.blue) || 0, color: '#06b6d4' },
|
||||
|
||||
{ key: 'm', label: 'Magenta (品红)', value: parseInt(this.red) || 0, color: '#ec4899' },
|
||||
|
||||
{ key: 'y', label: 'Yellow (黄)', value: parseInt(this.yellow) || 0, color: '#eab308' }
|
||||
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.ink-levels {
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: 14px;
|
||||
|
||||
|
||||
|
||||
&__row {
|
||||
|
||||
width: 100%;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__label {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: var(--dw-text-secondary);
|
||||
|
||||
margin-bottom: 6px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__swatch {
|
||||
|
||||
width: 10px;
|
||||
|
||||
height: 10px;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.8), 0 1px 2px rgba(0, 0, 0, 0.12);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__pct {
|
||||
|
||||
margin-left: auto;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
color: var(--dw-text);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,387 @@
|
||||
<template>
|
||||
|
||||
<div class="drive-card" :class="{ 'drive-card--burning': burning }">
|
||||
|
||||
<div class="drive-card__icon" :class="{ 'drive-card__icon--spin': burning }">
|
||||
|
||||
<console-icon v-if="burning" name="spinner" :size="22" spin />
|
||||
|
||||
<console-icon v-else name="disc" :size="22" />
|
||||
|
||||
</div>
|
||||
|
||||
<div class="drive-card__body">
|
||||
|
||||
<div class="drive-card__top">
|
||||
|
||||
<div class="drive-card__title">{{ title }}</div>
|
||||
|
||||
<dw-status-pill :variant="statusVariant" :label="statusLabel" />
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="discTypeLabel" class="drive-card__type">{{ discTypeLabel }}</div>
|
||||
|
||||
<div v-if="burning" class="drive-card__progress">
|
||||
|
||||
<dw-progress-bar :value="progress" color="#059669" />
|
||||
|
||||
<div class="drive-card__meta">
|
||||
|
||||
已刻录 {{ usedSize }} / {{ totalSize }}
|
||||
|
||||
<span v-if="speed"> · 速度 {{ speed }}X</span>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="taskId" class="drive-card__task">
|
||||
|
||||
<span class="drive-card__task-label">任务</span>
|
||||
|
||||
{{ taskId }}
|
||||
|
||||
</div>
|
||||
|
||||
<div v-if="sim" class="drive-card__sim">模拟刻录</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import ConsoleIcon from '../icons/ConsoleIcon.vue'
|
||||
|
||||
import DwStatusPill from '../ui/DwStatusPill.vue'
|
||||
|
||||
import DwProgressBar from '../ui/DwProgressBar.vue'
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
|
||||
name: 'DriveCard',
|
||||
|
||||
components: { ConsoleIcon, DwStatusPill, DwProgressBar },
|
||||
|
||||
props: {
|
||||
|
||||
drive: { type: Object, default: () => ({}) },
|
||||
|
||||
CDStatus: { type: Object, default: () => ({}) },
|
||||
|
||||
CDName: { type: Object, default: () => ({}) },
|
||||
|
||||
CDTypes: { type: Object, default: () => ({}) },
|
||||
|
||||
diskTypes: { type: Object, default: () => ({}) }
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
title() {
|
||||
|
||||
const path = this.drive.dev_path || '光驱'
|
||||
|
||||
const pos = this.CDName[this.drive.cd_pos] || this.fallbackPosLabel
|
||||
|
||||
return `${path} (${pos})`
|
||||
|
||||
},
|
||||
|
||||
fallbackPosLabel() {
|
||||
|
||||
return this.drive.cd_pos === 1 ? '下光驱' : this.drive.cd_pos === 2 ? '上光驱' : '光驱'
|
||||
|
||||
},
|
||||
|
||||
burning() {
|
||||
|
||||
return this.drive.copy_scheduler > 0
|
||||
|
||||
},
|
||||
|
||||
progress() {
|
||||
|
||||
return parseFloat(this.drive.copy_scheduler) || 0
|
||||
|
||||
},
|
||||
|
||||
statusLabel() {
|
||||
|
||||
if (this.burning) return `刻录中 ${this.progress}%`
|
||||
|
||||
return this.CDStatus[this.drive.cd_status] || '未知'
|
||||
|
||||
},
|
||||
|
||||
statusVariant() {
|
||||
|
||||
if (this.burning) return 'busy'
|
||||
|
||||
if (this.drive.cd_status === 1) return 'neutral'
|
||||
|
||||
if (this.drive.cd_status === 4) return 'online'
|
||||
|
||||
return 'idle'
|
||||
|
||||
},
|
||||
|
||||
discTypeLabel() {
|
||||
|
||||
if (!this.drive.cd_type) return ''
|
||||
|
||||
return this.CDTypes[this.drive.cd_type] || this.diskTypes[this.drive.cd_type] || ''
|
||||
|
||||
},
|
||||
|
||||
usedSize() {
|
||||
|
||||
return this.$filterSize(this.drive.use_size || 0)
|
||||
|
||||
},
|
||||
|
||||
totalSize() {
|
||||
|
||||
return this.$filterSize(this.drive.total_size || 0)
|
||||
|
||||
},
|
||||
|
||||
speed() {
|
||||
|
||||
return this.drive.burn_speed ? this.drive.burn_speed.toFixed(2) : ''
|
||||
|
||||
},
|
||||
|
||||
taskId() {
|
||||
|
||||
return this.drive.cd_taskid && this.burning ? this.drive.cd_taskid : ''
|
||||
|
||||
},
|
||||
|
||||
sim() {
|
||||
|
||||
return this.drive.copy_scheduler && this.drive.is_sim
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.drive-card {
|
||||
|
||||
display: flex;
|
||||
|
||||
gap: 14px;
|
||||
|
||||
padding: 16px;
|
||||
|
||||
background: #fff;
|
||||
|
||||
border: 1px solid var(--dw-border);
|
||||
|
||||
border-radius: var(--dw-radius-md);
|
||||
|
||||
box-shadow: var(--dw-shadow-sm);
|
||||
|
||||
transition: box-shadow 0.2s ease, border-color 0.2s ease;
|
||||
|
||||
|
||||
|
||||
&:hover {
|
||||
|
||||
box-shadow: var(--dw-shadow-md);
|
||||
|
||||
border-color: rgba(30, 79, 217, 0.15);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&--burning {
|
||||
|
||||
border-color: rgba(5, 150, 105, 0.25);
|
||||
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__icon {
|
||||
|
||||
width: 52px;
|
||||
|
||||
height: 52px;
|
||||
|
||||
border-radius: var(--dw-radius-md);
|
||||
|
||||
background: linear-gradient(145deg, #eff6ff 0%, #dbeafe 100%);
|
||||
|
||||
color: var(--dw-primary);
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
border: 1px solid rgba(30, 79, 217, 0.12);
|
||||
|
||||
|
||||
|
||||
&--spin {
|
||||
|
||||
color: var(--dw-success);
|
||||
|
||||
background: linear-gradient(145deg, #ecfdf5 0%, #d1fae5 100%);
|
||||
|
||||
border-color: rgba(5, 150, 105, 0.2);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__body {
|
||||
|
||||
flex: 1;
|
||||
|
||||
min-width: 0;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__top {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: flex-start;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
margin-bottom: 4px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__title {
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
line-height: 1.35;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__type {
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: var(--dw-text-secondary);
|
||||
|
||||
margin-bottom: 6px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__progress {
|
||||
|
||||
margin-top: 8px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__meta {
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: var(--dw-text-secondary);
|
||||
|
||||
margin-top: 6px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__task {
|
||||
|
||||
margin-top: 6px;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: var(--dw-text-secondary);
|
||||
|
||||
word-break: break-all;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__task-label {
|
||||
|
||||
display: inline-block;
|
||||
|
||||
padding: 1px 6px;
|
||||
|
||||
margin-right: 4px;
|
||||
|
||||
background: #f3f4f6;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
font-size: 11px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__sim {
|
||||
|
||||
display: inline-block;
|
||||
|
||||
margin-top: 8px;
|
||||
|
||||
font-size: 11px;
|
||||
|
||||
padding: 2px 8px;
|
||||
|
||||
background: var(--dw-warning-soft);
|
||||
|
||||
color: var(--dw-warning);
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
|
||||
<dw-card class="drive-monitor" :title="''">
|
||||
|
||||
<template #header>
|
||||
|
||||
<div class="drive-monitor__head">
|
||||
|
||||
<console-icon name="drive" :size="16" />
|
||||
|
||||
<span>光驱监控</span>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<div v-if="loading" class="drive-monitor__grid">
|
||||
|
||||
<div class="dw-skeleton" style="height:120px" />
|
||||
|
||||
<div class="dw-skeleton" style="height:120px" />
|
||||
|
||||
</div>
|
||||
|
||||
<dw-empty v-else-if="!cdList.length" scene="drives" title="无光驱数据" description="联机后将显示上下光驱状态" />
|
||||
|
||||
<div v-else class="drive-monitor__grid">
|
||||
|
||||
<drive-card
|
||||
|
||||
v-for="(item, index) in cdList"
|
||||
|
||||
:key="index"
|
||||
|
||||
:drive="item"
|
||||
|
||||
:CDStatus="CDStatus"
|
||||
|
||||
:CDName="CDName"
|
||||
|
||||
:CDTypes="CDTypes"
|
||||
|
||||
:diskTypes="diskTypes"
|
||||
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
</dw-card>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import DwCard from '../ui/DwCard.vue'
|
||||
|
||||
import DwEmpty from '../ui/DwEmpty.vue'
|
||||
|
||||
import ConsoleIcon from '../icons/ConsoleIcon.vue'
|
||||
|
||||
import DriveCard from './DriveCard.vue'
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
|
||||
name: 'DriveMonitor',
|
||||
|
||||
components: { DwCard, DwEmpty, ConsoleIcon, DriveCard },
|
||||
|
||||
props: {
|
||||
|
||||
cdList: { type: Array, default: () => [] },
|
||||
|
||||
CDStatus: { type: Object, default: () => ({}) },
|
||||
|
||||
CDName: { type: Object, default: () => ({}) },
|
||||
|
||||
CDTypes: { type: Object, default: () => ({}) },
|
||||
|
||||
diskTypes: { type: Object, default: () => ({}) },
|
||||
|
||||
loading: { type: Boolean, default: false }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.drive-monitor {
|
||||
|
||||
box-shadow: var(--dw-shadow-card);
|
||||
|
||||
|
||||
|
||||
&__head {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
font-size: 15px;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__grid {
|
||||
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
gap: 14px;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<i
|
||||
class="dw-icon"
|
||||
:class="iconClasses"
|
||||
:style="iconStyle"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const FA_MAP = {
|
||||
disc: 'fa-compact-disc',
|
||||
logo: 'fa-compact-disc',
|
||||
drive: 'fa-hdd',
|
||||
device: 'fa-print',
|
||||
server: 'fa-server',
|
||||
ink: 'fa-tint',
|
||||
bin: 'fa-box',
|
||||
folder: 'fa-folder-open',
|
||||
settings: 'fa-cog',
|
||||
plus: 'fa-plus',
|
||||
tasks: 'fa-list',
|
||||
logs: 'fa-stream',
|
||||
spinner: 'fa-spinner',
|
||||
offline: 'fa-circle-exclamation',
|
||||
wifi: 'fa-wifi',
|
||||
help: 'fa-circle-question',
|
||||
user: 'fa-user',
|
||||
refresh: 'fa-rotate-right'
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'ConsoleIcon',
|
||||
props: {
|
||||
name: { type: String, required: true },
|
||||
size: { type: [Number, String], default: 16 },
|
||||
spin: { type: Boolean, default: false },
|
||||
fixed: { type: Boolean, default: true }
|
||||
},
|
||||
computed: {
|
||||
faClass() {
|
||||
return FA_MAP[this.name] || 'fa-circle'
|
||||
},
|
||||
iconClasses() {
|
||||
return [
|
||||
this.fixed ? 'fa-solid' : 'fas',
|
||||
this.faClass,
|
||||
{ 'fa-spin': this.spin }
|
||||
]
|
||||
},
|
||||
iconStyle() {
|
||||
const n = parseInt(this.size, 10) || 16
|
||||
return { fontSize: `${n}px`, width: `${n}px`, lineHeight: `${n}px` }
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dw-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,372 @@
|
||||
<template>
|
||||
|
||||
<div class="console-header">
|
||||
|
||||
<div class="console-header__brand">
|
||||
|
||||
<console-icon name="logo" :size="22" class="console-header__logo-icon" />
|
||||
<img class="console-header__logo" src="@/assets/images/logo.png" alt="logo" />
|
||||
<div>
|
||||
<div class="console-header__title">DiscWorker 控制台</div>
|
||||
<div class="console-header__sub">国产光盘打印刻录系统 v2.0</div>
|
||||
<div v-if="previewMode" class="console-header__preview-tag">预览模式</div>
|
||||
<div v-else-if="demoMode" class="console-header__preview-tag console-header__preview-tag--demo">设计模式 · Mock 数据</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="console-header__center">
|
||||
|
||||
<dw-status-pill :variant="connectionVariant" :label="connectionLabel" dot />
|
||||
|
||||
<dw-kpi-chip label="完成" :value="taskNum[0]" variant="success" icon="check" />
|
||||
|
||||
<dw-kpi-chip label="废弃" :value="taskNum[1]" variant="muted" />
|
||||
|
||||
<dw-kpi-chip label="错误" :value="taskNum[2]" variant="danger" icon="alert" />
|
||||
|
||||
</div>
|
||||
|
||||
<div class="console-header__actions">
|
||||
|
||||
<template v-if="printerInfo.test_use > -1 && printerInfo.test_use !== 255">
|
||||
|
||||
<span class="console-header__trial">试用版 {{ printerInfo.test_use }} 天</span>
|
||||
|
||||
<button v-if="hasRunCmd" type="button" class="dw-btn dw-btn--ghost" @click="$emit('activate')">激活</button>
|
||||
|
||||
</template>
|
||||
|
||||
<button type="button" class="dw-btn dw-btn--outline" @click="$emit('open-work')">
|
||||
|
||||
<console-icon name="folder" :size="15" />
|
||||
|
||||
打开文件
|
||||
|
||||
</button>
|
||||
|
||||
<button type="button" class="dw-btn dw-btn--outline" @click="$emit('go-settings')">
|
||||
|
||||
<console-icon name="settings" :size="15" />
|
||||
|
||||
设置
|
||||
|
||||
</button>
|
||||
|
||||
<el-dropdown trigger="click" @command="handleUserCommand">
|
||||
|
||||
<button type="button" class="console-header__user">
|
||||
|
||||
<span class="console-header__avatar">{{ userInitial }}</span>
|
||||
|
||||
<span>{{ userName }}</span>
|
||||
|
||||
<i class="el-icon-arrow-down" />
|
||||
|
||||
</button>
|
||||
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
|
||||
<el-dropdown-item v-if="hasRunCmd" command="toggle-service">
|
||||
|
||||
{{ workStatus ? '关闭服务' : '开启服务' }}
|
||||
|
||||
</el-dropdown-item>
|
||||
|
||||
<el-dropdown-item v-else disabled>网页端(无服务控制)</el-dropdown-item>
|
||||
|
||||
<el-dropdown-item command="legacy">切换旧版界面</el-dropdown-item>
|
||||
|
||||
<el-dropdown-item divided command="logout">登出</el-dropdown-item>
|
||||
|
||||
</el-dropdown-menu>
|
||||
|
||||
</el-dropdown>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import ConsoleIcon from '../icons/ConsoleIcon.vue'
|
||||
|
||||
import DwStatusPill from '../ui/DwStatusPill.vue'
|
||||
|
||||
import DwKpiChip from '../ui/DwKpiChip.vue'
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
|
||||
name: 'ConsoleHeader',
|
||||
|
||||
components: { ConsoleIcon, DwStatusPill, DwKpiChip },
|
||||
|
||||
props: {
|
||||
|
||||
userName: { type: String, default: 'Admin' },
|
||||
|
||||
taskNum: { type: Array, default: () => [0, 0, 0] },
|
||||
|
||||
workStatus: { type: Boolean, default: false },
|
||||
|
||||
hasRunCmd: { type: Boolean, default: false },
|
||||
|
||||
previewMode: { type: Boolean, default: false },
|
||||
demoMode: { type: Boolean, default: false },
|
||||
printerInfo: { type: Object, default: () => ({}) },
|
||||
|
||||
isConnect: { type: Boolean, default: false },
|
||||
|
||||
serviceStatus: { type: Boolean, default: false }
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
userInitial() {
|
||||
|
||||
return (this.userName || 'A').charAt(0).toUpperCase()
|
||||
|
||||
},
|
||||
|
||||
connectionVariant() {
|
||||
|
||||
if (!this.isConnect || !this.serviceStatus) return 'error'
|
||||
|
||||
return 'online'
|
||||
|
||||
},
|
||||
|
||||
connectionLabel() {
|
||||
if (this.demoMode && this.isConnect && this.serviceStatus) return '系统联机'
|
||||
if (!this.hasRunCmd) return '网页端'
|
||||
if (!this.isConnect || !this.serviceStatus) return '未联机'
|
||||
return '系统联机'
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
handleUserCommand(cmd) {
|
||||
|
||||
this.$emit('user-command', cmd)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.console-header {
|
||||
|
||||
height: var(--dw-header-h);
|
||||
|
||||
padding: 0 20px;
|
||||
|
||||
background: var(--dw-surface);
|
||||
|
||||
border-bottom: 1px solid var(--dw-border);
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
box-shadow: var(--dw-shadow-sm);
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
|
||||
|
||||
&__brand {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 12px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__logo-icon {
|
||||
color: var(--dw-primary);
|
||||
}
|
||||
|
||||
&__logo {
|
||||
|
||||
width: 36px;
|
||||
|
||||
height: 36px;
|
||||
|
||||
object-fit: contain;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__title {
|
||||
|
||||
font-size: 16px;
|
||||
|
||||
font-weight: 700;
|
||||
|
||||
color: var(--dw-primary);
|
||||
|
||||
line-height: 1.2;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__sub {
|
||||
|
||||
font-size: 11px;
|
||||
|
||||
color: var(--dw-text-muted);
|
||||
|
||||
margin-top: 1px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__preview-tag {
|
||||
display: inline-block;
|
||||
margin-top: 2px;
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
color: var(--dw-warning);
|
||||
background: var(--dw-warning-soft);
|
||||
|
||||
&--demo {
|
||||
color: var(--dw-info);
|
||||
background: var(--dw-info-soft);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__center {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__actions {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__trial {
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: var(--dw-text-secondary);
|
||||
|
||||
padding: 4px 8px;
|
||||
|
||||
background: #f9fafb;
|
||||
|
||||
border-radius: var(--dw-radius-sm);
|
||||
|
||||
margin-right: 2px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__user {
|
||||
|
||||
display: inline-flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
border: 1px solid var(--dw-border);
|
||||
|
||||
background: #fff;
|
||||
|
||||
border-radius: var(--dw-radius-sm);
|
||||
|
||||
padding: 4px 10px 4px 4px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
transition: border-color 0.15s ease;
|
||||
|
||||
|
||||
|
||||
&:hover {
|
||||
|
||||
border-color: rgba(30, 79, 217, 0.35);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__avatar {
|
||||
|
||||
width: 28px;
|
||||
|
||||
height: 28px;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
background: linear-gradient(135deg, #dbeafe 0%, #eff6ff 100%);
|
||||
|
||||
color: var(--dw-primary);
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
font-weight: 700;
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
border: 1px solid rgba(30, 79, 217, 0.15);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div class="console-shell">
|
||||
<console-header
|
||||
:user-name="userName"
|
||||
:task-num="taskNum"
|
||||
:work-status="workStatus"
|
||||
:has-run-cmd="hasRunCmd"
|
||||
:preview-mode="previewMode"
|
||||
:demo-mode="demoMode"
|
||||
:printer-info="printerInfo"
|
||||
:is-connect="isConnect"
|
||||
:service-status="serviceStatus"
|
||||
@open-work="$emit('open-work')"
|
||||
@go-settings="$emit('go-settings')"
|
||||
@activate="$emit('activate')"
|
||||
@user-command="$emit('user-command', $event)"
|
||||
/>
|
||||
<main class="console-main">
|
||||
<div class="console-main__device">
|
||||
<device-panel
|
||||
:printer-info="printerInfo"
|
||||
:run-text="runText"
|
||||
:tag-variant="tagVariant"
|
||||
:tag-label="tagLabel"
|
||||
:online="workStatus"
|
||||
:loading="loading"
|
||||
:run-list="runList"
|
||||
:current-run-val="currentRunVal"
|
||||
:sure-text="sureText"
|
||||
:change-c-d="changeCD"
|
||||
:cd-type-options="cdTypeOptions"
|
||||
:CDTypes="CDTypes"
|
||||
@new-work="$emit('new-work')"
|
||||
@maintain="$emit('maintain', $event)"
|
||||
@command-cd="(e, type) => $emit('command-cd', e, type)"
|
||||
@confirm-cd="$emit('confirm-cd')"
|
||||
@cancel-cd="$emit('cancel-cd')"
|
||||
@refresh="$emit('refresh')"
|
||||
/>
|
||||
</div>
|
||||
<div class="console-main__drive">
|
||||
<drive-monitor
|
||||
:cd-list="cdList"
|
||||
:CDStatus="CDStatus"
|
||||
:CDName="CDName"
|
||||
:CDTypes="CDTypes"
|
||||
:diskTypes="diskTypes"
|
||||
:loading="loading"
|
||||
/>
|
||||
</div>
|
||||
<div class="console-main__task">
|
||||
<task-workspace
|
||||
:task-list="taskList"
|
||||
:log-list="logList"
|
||||
:show-all-logs="showAllLogs"
|
||||
@retry="$emit('retry', $event)"
|
||||
@cancel="$emit('cancel-task', $event)"
|
||||
@toggle-logs="$emit('toggle-logs')"
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<console-status-bar
|
||||
:serial-no="printerInfo.serial_no"
|
||||
:cache-text="cacheText"
|
||||
:version-text="versionText"
|
||||
@help="$emit('help')"
|
||||
@about="$emit('about')"
|
||||
/>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConsoleHeader from './ConsoleHeader.vue'
|
||||
import ConsoleStatusBar from './ConsoleStatusBar.vue'
|
||||
import DevicePanel from '../device/DevicePanel.vue'
|
||||
import DriveMonitor from '../drive/DriveMonitor.vue'
|
||||
import TaskWorkspace from '../task/TaskWorkspace.vue'
|
||||
|
||||
export default {
|
||||
name: 'ConsoleShell',
|
||||
components: {
|
||||
ConsoleHeader,
|
||||
ConsoleStatusBar,
|
||||
DevicePanel,
|
||||
DriveMonitor,
|
||||
TaskWorkspace
|
||||
},
|
||||
props: {
|
||||
previewMode: { type: Boolean, default: false },
|
||||
demoMode: { type: Boolean, default: false },
|
||||
userName: { type: String, default: '' },
|
||||
taskNum: { type: Array, default: () => [0, 0, 0] },
|
||||
workStatus: { type: Boolean, default: false },
|
||||
hasRunCmd: { type: Boolean, default: false },
|
||||
isConnect: { type: Boolean, default: false },
|
||||
serviceStatus: { type: Boolean, default: false },
|
||||
printerInfo: { type: Object, default: () => ({}) },
|
||||
runText: { type: String, default: '' },
|
||||
tagVariant: { type: String, default: 'idle' },
|
||||
tagLabel: { type: String, default: '' },
|
||||
loading: { type: Boolean, default: false },
|
||||
runList: { type: Array, default: () => [] },
|
||||
currentRunVal: { type: [Number, String], default: 0 },
|
||||
sureText: { type: String, default: '' },
|
||||
changeCD: { type: Object, default: () => ({}) },
|
||||
cdTypeOptions: { type: Array, default: () => [] },
|
||||
cdList: { type: Array, default: () => [] },
|
||||
CDStatus: { type: Object, default: () => ({}) },
|
||||
CDName: { type: Object, default: () => ({}) },
|
||||
CDTypes: { type: Object, default: () => ({}) },
|
||||
diskTypes: { type: Object, default: () => ({}) },
|
||||
taskList: { type: Array, default: () => [] },
|
||||
logList: { type: Array, default: () => [] },
|
||||
showAllLogs: { type: Boolean, default: false },
|
||||
versionText: { type: String, default: '' }
|
||||
},
|
||||
computed: {
|
||||
cacheText() {
|
||||
const p = this.printerInfo
|
||||
if (!p.total_cache_space) return ''
|
||||
return `缓存 ${this.$filterSize(p.free_cache_space)} / ${this.$filterSize(p.total_cache_space)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.console-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="console-statusbar">
|
||||
<div class="console-statusbar__left">
|
||||
<span class="console-statusbar__version">{{ displayVersion }}</span>
|
||||
<span v-if="serialNo" class="console-statusbar__item">SN: {{ serialNo }}</span>
|
||||
<span v-if="cacheText" class="console-statusbar__item">{{ cacheText }}</span>
|
||||
</div>
|
||||
<div class="console-statusbar__links">
|
||||
<a href="#" @click.prevent="$emit('help')">
|
||||
<console-icon name="help" :size="13" />
|
||||
帮助
|
||||
</a>
|
||||
<span class="console-statusbar__sep">|</span>
|
||||
<a href="#" @click.prevent="$emit('about')">关于</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ConsoleIcon from '../icons/ConsoleIcon.vue'
|
||||
import { loadAppVersionLine } from '@/utils/console/appVersion'
|
||||
|
||||
export default {
|
||||
name: 'ConsoleStatusBar',
|
||||
components: { ConsoleIcon },
|
||||
props: {
|
||||
serialNo: { type: String, default: '' },
|
||||
cacheText: { type: String, default: '' },
|
||||
versionText: { type: String, default: '' }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localVersionText: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayVersion() {
|
||||
return this.versionText || this.localVersionText || 'DiscWorker'
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (!this.versionText) {
|
||||
loadAppVersionLine().then((text) => {
|
||||
this.localVersionText = text
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.console-statusbar {
|
||||
height: var(--dw-statusbar-h);
|
||||
padding: 0 20px;
|
||||
background: var(--dw-surface);
|
||||
border-top: 1px solid var(--dw-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--dw-text-muted);
|
||||
flex-shrink: 0;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__version {
|
||||
color: var(--dw-text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__item {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__links {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: var(--dw-text-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--dw-primary);
|
||||
}
|
||||
}
|
||||
|
||||
&__sep {
|
||||
margin: 0 6px;
|
||||
opacity: 0.45;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="activity-log">
|
||||
<div class="activity-log__toolbar">
|
||||
<button type="button" class="dw-btn dw-btn--ghost" @click="$emit('toggle-all')">
|
||||
{{ showAll ? '最新日志' : '全部日志' }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="activity-log__list">
|
||||
<div
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
class="activity-log__item"
|
||||
:class="`activity-log__item--${(item.status || 'INFO').toLowerCase()}`"
|
||||
>
|
||||
<span class="activity-log__badge">{{ item.status || 'INFO' }}</span>
|
||||
<span class="activity-log__text">{{ item.text }}</span>
|
||||
<span class="activity-log__time">{{ item.time }}</span>
|
||||
</div>
|
||||
<dw-empty v-if="!list.length" scene="logs" title="暂无日志" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DwEmpty from '../ui/DwEmpty.vue'
|
||||
|
||||
export default {
|
||||
name: 'ActivityLog',
|
||||
components: { DwEmpty },
|
||||
props: {
|
||||
list: { type: Array, default: () => [] },
|
||||
showAll: { type: Boolean, default: false }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.activity-log {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
|
||||
&__toolbar {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
&__list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-radius: var(--dw-radius-sm);
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--dw-border);
|
||||
|
||||
&--error {
|
||||
background: var(--dw-danger-soft);
|
||||
border-color: rgba(220, 38, 38, 0.15);
|
||||
}
|
||||
|
||||
&--debug {
|
||||
background: var(--dw-warning-soft);
|
||||
border-color: rgba(217, 119, 6, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
&__badge {
|
||||
flex-shrink: 0;
|
||||
min-width: 44px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--dw-info-soft);
|
||||
color: var(--dw-info);
|
||||
}
|
||||
|
||||
&__item--error &__badge {
|
||||
background: rgba(220, 38, 38, 0.12);
|
||||
color: var(--dw-danger);
|
||||
}
|
||||
|
||||
&__item--debug &__badge {
|
||||
background: rgba(217, 119, 6, 0.12);
|
||||
color: var(--dw-warning);
|
||||
}
|
||||
|
||||
&__text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
&__time {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,331 @@
|
||||
<template>
|
||||
|
||||
<div class="task-table-wrap">
|
||||
|
||||
<div class="task-table-wrap__toolbar">
|
||||
|
||||
<el-input
|
||||
|
||||
v-model="keyword"
|
||||
|
||||
size="small"
|
||||
|
||||
clearable
|
||||
|
||||
placeholder="搜索任务名称或 ID"
|
||||
|
||||
prefix-icon="el-icon-search"
|
||||
|
||||
class="task-table-wrap__search"
|
||||
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
|
||||
row-key="id"
|
||||
|
||||
default-expand-all
|
||||
|
||||
class="task-table dw-console-table"
|
||||
|
||||
:data="filteredList"
|
||||
|
||||
height="100%"
|
||||
|
||||
stripe
|
||||
|
||||
:tree-props="{ children: 'ass_task', hasChildren: 'hasChildren' }"
|
||||
|
||||
>
|
||||
|
||||
<template slot="empty">
|
||||
|
||||
<dw-empty scene="tasks" title="暂无作业列表" description="点击左侧「新建刻录作业」开始第一个任务" />
|
||||
|
||||
</template>
|
||||
|
||||
<el-table-column prop="task_uuid" label="作业ID" min-width="130" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="task_name" label="任务名称" min-width="150" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="cdType" label="类型" width="88">
|
||||
|
||||
<template slot-scope="scope">
|
||||
|
||||
<span v-if="scope.row.cdType" class="task-table__type">{{ scope.row.cdType }}</span>
|
||||
|
||||
<span v-else>—</span>
|
||||
|
||||
</template>
|
||||
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="大小" width="96">
|
||||
|
||||
<template slot-scope="scope">{{ $filterSize(scope.row.task_size) }}</template>
|
||||
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="start_time" label="开始时间" width="150" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="finish_time" label="结束时间" width="150" show-overflow-tooltip />
|
||||
|
||||
<el-table-column label="完成度" width="120">
|
||||
|
||||
<template slot-scope="scope">
|
||||
|
||||
<template v-if="scope.row.task_uuid && isRunning(scope.row.task_status)">
|
||||
|
||||
<dw-progress-bar :value="taskProgress(scope.row)" color="#0284c7" />
|
||||
|
||||
</template>
|
||||
|
||||
<span v-else-if="scope.row.task_uuid">{{ scope.row.finish_freq }}/{{ scope.row.total_freq }}</span>
|
||||
|
||||
<span v-else>—</span>
|
||||
|
||||
</template>
|
||||
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="状态" width="148" fixed="right">
|
||||
|
||||
<template slot-scope="scope">
|
||||
|
||||
<div v-if="scope.row.task_uuid && scope.row.task_status == 5" class="task-table__actions">
|
||||
|
||||
<dw-status-pill variant="error" :label="statusLabel(scope.row.task_status)" />
|
||||
|
||||
<el-button type="text" size="mini" icon="el-icon-refresh-right" @click="$emit('retry', scope.row)" />
|
||||
|
||||
<el-button type="text" size="mini" icon="el-icon-circle-close" @click="$emit('cancel', scope.row)" />
|
||||
|
||||
</div>
|
||||
|
||||
<dw-status-pill
|
||||
|
||||
v-else-if="scope.row.task_uuid"
|
||||
|
||||
:variant="statusVariant(scope.row.task_status)"
|
||||
|
||||
:label="statusLabel(scope.row.task_status)"
|
||||
|
||||
/>
|
||||
|
||||
<span v-else class="task-table__sub">{{ statusLabel(scope.row.task_status) }}</span>
|
||||
|
||||
</template>
|
||||
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
|
||||
<div class="task-table-wrap__footer">
|
||||
|
||||
共 <strong>{{ filteredList.length }}</strong> 条作业
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import DwEmpty from '../ui/DwEmpty.vue'
|
||||
|
||||
import DwStatusPill from '../ui/DwStatusPill.vue'
|
||||
|
||||
import DwProgressBar from '../ui/DwProgressBar.vue'
|
||||
|
||||
import { TASK_STATUS_LABELS, taskStatusVariant } from '@/utils/console/constants'
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
|
||||
name: 'TaskTable',
|
||||
|
||||
components: { DwEmpty, DwStatusPill, DwProgressBar },
|
||||
|
||||
props: {
|
||||
|
||||
taskList: { type: Array, default: () => [] }
|
||||
|
||||
},
|
||||
|
||||
data() {
|
||||
|
||||
return { keyword: '' }
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
filteredList() {
|
||||
|
||||
const kw = (this.keyword || '').trim().toLowerCase()
|
||||
|
||||
if (!kw) return this.taskList
|
||||
|
||||
return this.taskList.filter((row) => {
|
||||
|
||||
const name = (row.task_name || '').toLowerCase()
|
||||
|
||||
const id = (row.task_uuid || '').toLowerCase()
|
||||
|
||||
return name.includes(kw) || id.includes(kw)
|
||||
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
statusLabel(status) {
|
||||
|
||||
return TASK_STATUS_LABELS[status] || status
|
||||
|
||||
},
|
||||
|
||||
statusVariant(status) {
|
||||
|
||||
return taskStatusVariant(status)
|
||||
|
||||
},
|
||||
|
||||
isRunning(status) {
|
||||
|
||||
return [3, 4, 6].includes(parseInt(status, 10))
|
||||
|
||||
},
|
||||
|
||||
taskProgress(row) {
|
||||
|
||||
if (!row.total_freq) return 0
|
||||
|
||||
return Math.min(100, Math.round((row.finish_freq / row.total_freq) * 100))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.task-table-wrap {
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
|
||||
min-height: 0;
|
||||
|
||||
|
||||
|
||||
&__toolbar {
|
||||
|
||||
padding: 0 0 10px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__search {
|
||||
|
||||
max-width: 280px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__footer {
|
||||
|
||||
padding-top: 10px;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: var(--dw-text-muted);
|
||||
|
||||
|
||||
|
||||
strong {
|
||||
|
||||
color: var(--dw-text);
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.task-table {
|
||||
|
||||
flex: 1;
|
||||
|
||||
min-height: 0;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.task-table__type {
|
||||
|
||||
display: inline-block;
|
||||
|
||||
padding: 2px 8px;
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
color: var(--dw-primary);
|
||||
|
||||
background: var(--dw-primary-soft);
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.task-table__sub {
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: var(--dw-text-secondary);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
.task-table__actions {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 4px;
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
<template>
|
||||
|
||||
<dw-card class="task-workspace" flex-body flush>
|
||||
|
||||
<template #header>
|
||||
|
||||
<el-tabs v-model="activeTab" class="task-workspace__tabs">
|
||||
|
||||
<el-tab-pane name="tasks">
|
||||
|
||||
<span slot="label" class="task-workspace__tab-label">
|
||||
|
||||
<console-icon name="tasks" :size="14" />
|
||||
|
||||
作业列表
|
||||
|
||||
</span>
|
||||
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane name="logs">
|
||||
|
||||
<span slot="label" class="task-workspace__tab-label">
|
||||
|
||||
<console-icon name="logs" :size="14" />
|
||||
|
||||
动态日志
|
||||
|
||||
</span>
|
||||
|
||||
</el-tab-pane>
|
||||
|
||||
</el-tabs>
|
||||
|
||||
</template>
|
||||
|
||||
<div class="task-workspace__body">
|
||||
|
||||
<task-table
|
||||
|
||||
v-show="activeTab === 'tasks'"
|
||||
|
||||
:task-list="taskList"
|
||||
|
||||
@retry="$emit('retry', $event)"
|
||||
|
||||
@cancel="$emit('cancel', $event)"
|
||||
|
||||
/>
|
||||
|
||||
<activity-log
|
||||
|
||||
v-show="activeTab === 'logs'"
|
||||
|
||||
:list="logList"
|
||||
|
||||
:show-all="showAllLogs"
|
||||
|
||||
@toggle-all="$emit('toggle-logs')"
|
||||
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
</dw-card>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import DwCard from '../ui/DwCard.vue'
|
||||
|
||||
import ConsoleIcon from '../icons/ConsoleIcon.vue'
|
||||
|
||||
import TaskTable from './TaskTable.vue'
|
||||
|
||||
import ActivityLog from './ActivityLog.vue'
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
|
||||
name: 'TaskWorkspace',
|
||||
|
||||
components: { DwCard, ConsoleIcon, TaskTable, ActivityLog },
|
||||
|
||||
props: {
|
||||
|
||||
taskList: { type: Array, default: () => [] },
|
||||
|
||||
logList: { type: Array, default: () => [] },
|
||||
|
||||
showAllLogs: { type: Boolean, default: false }
|
||||
|
||||
},
|
||||
|
||||
data() {
|
||||
|
||||
return { activeTab: 'tasks' }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.task-workspace {
|
||||
|
||||
height: 100%;
|
||||
|
||||
box-shadow: var(--dw-shadow-card);
|
||||
|
||||
|
||||
|
||||
&__tabs {
|
||||
|
||||
::v-deep .el-tabs__header {
|
||||
|
||||
margin: 0;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
::v-deep .el-tabs__nav-wrap::after {
|
||||
|
||||
display: none;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
::v-deep .el-tabs__item {
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
height: 44px;
|
||||
|
||||
line-height: 44px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
::v-deep .el-tabs__item.is-active {
|
||||
|
||||
color: var(--dw-primary);
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__tab-label {
|
||||
|
||||
display: inline-flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 6px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__body {
|
||||
|
||||
padding: 12px 16px 16px;
|
||||
|
||||
flex: 1;
|
||||
|
||||
min-height: 0;
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
height: calc(100% - 0px);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="dw-card" :class="{ 'dw-card--flush': flush }">
|
||||
<div v-if="$slots.header || title" class="dw-card__header">
|
||||
<slot name="header">
|
||||
<span class="dw-card__title">{{ title }}</span>
|
||||
</slot>
|
||||
<div v-if="$slots.extra" class="dw-card__extra">
|
||||
<slot name="extra" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="dw-card__body" :class="{ 'dw-card__body--flex': flexBody }">
|
||||
<slot />
|
||||
</div>
|
||||
<div v-if="$slots.footer" class="dw-card__footer">
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DwCard',
|
||||
props: {
|
||||
title: { type: String, default: '' },
|
||||
flush: { type: Boolean, default: false },
|
||||
flexBody: { type: Boolean, default: false }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dw-card {
|
||||
background: var(--dw-surface);
|
||||
border-radius: var(--dw-radius-lg);
|
||||
border: 1px solid rgba(0, 0, 0, 0.04);
|
||||
box-shadow: var(--dw-shadow-card);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--dw-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--dw-text);
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 16px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
|
||||
&--flex {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
&--flush .dw-card__body {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--dw-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
|
||||
<div class="dw-empty" :class="`dw-empty--${scene}`">
|
||||
|
||||
<div class="dw-empty__visual">
|
||||
|
||||
<img
|
||||
|
||||
v-if="scene === 'offline'"
|
||||
|
||||
class="dw-empty__device-img"
|
||||
|
||||
:src="placeholderDeviceImg"
|
||||
|
||||
alt=""
|
||||
|
||||
/>
|
||||
|
||||
<console-icon v-else :name="iconName" :size="iconSize" />
|
||||
|
||||
</div>
|
||||
|
||||
<div class="dw-empty__title">{{ title }}</div>
|
||||
|
||||
<div v-if="description" class="dw-empty__desc">{{ description }}</div>
|
||||
|
||||
<button v-if="actionText" type="button" class="dw-btn dw-btn--outline" @click="$emit('action')">
|
||||
|
||||
<console-icon v-if="scene === 'offline'" name="wifi" :size="14" />
|
||||
|
||||
{{ actionText }}
|
||||
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
import ConsoleIcon from '../icons/ConsoleIcon.vue'
|
||||
|
||||
import { getDeviceImages } from '@/utils/console/constants'
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
|
||||
name: 'DwEmpty',
|
||||
|
||||
components: { ConsoleIcon },
|
||||
|
||||
props: {
|
||||
|
||||
title: { type: String, default: '暂无数据' },
|
||||
|
||||
description: { type: String, default: '' },
|
||||
|
||||
actionText: { type: String, default: '' },
|
||||
|
||||
scene: {
|
||||
|
||||
type: String,
|
||||
|
||||
default: 'default',
|
||||
|
||||
validator: (v) => ['default', 'offline', 'tasks', 'drives', 'logs'].includes(v)
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
iconName() {
|
||||
|
||||
const map = { tasks: 'tasks', drives: 'drive', logs: 'tasks', default: 'device', offline: 'offline' }
|
||||
|
||||
return map[this.scene] || 'device'
|
||||
|
||||
},
|
||||
|
||||
iconSize() {
|
||||
|
||||
return this.scene === 'offline' ? 32 : 40
|
||||
|
||||
},
|
||||
|
||||
placeholderDeviceImg() {
|
||||
|
||||
return getDeviceImages()['4200'].img1
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.dw-empty {
|
||||
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
padding: 28px 16px;
|
||||
|
||||
text-align: center;
|
||||
|
||||
color: var(--dw-text-secondary);
|
||||
|
||||
|
||||
|
||||
&__visual {
|
||||
|
||||
width: 88px;
|
||||
|
||||
height: 88px;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
background: linear-gradient(145deg, #f8fafc 0%, #eef2ff 100%);
|
||||
|
||||
border: 1px solid var(--dw-border);
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
margin-bottom: 14px;
|
||||
|
||||
color: var(--dw-primary);
|
||||
|
||||
box-shadow: var(--dw-shadow-sm);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__device-img {
|
||||
|
||||
max-width: 72px;
|
||||
|
||||
max-height: 56px;
|
||||
|
||||
object-fit: contain;
|
||||
|
||||
opacity: 0.55;
|
||||
|
||||
filter: grayscale(0.35);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&--offline &__visual {
|
||||
|
||||
width: auto;
|
||||
|
||||
height: auto;
|
||||
|
||||
padding: 12px 16px;
|
||||
|
||||
border-radius: var(--dw-radius-md);
|
||||
|
||||
background: var(--dw-gradient-device);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__title {
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
color: var(--dw-text);
|
||||
|
||||
margin-bottom: 4px;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__desc {
|
||||
|
||||
font-size: 13px;
|
||||
|
||||
margin-bottom: 14px;
|
||||
|
||||
max-width: 280px;
|
||||
|
||||
line-height: 1.5;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
|
||||
<div class="dw-kpi" :class="`dw-kpi--${variant}`">
|
||||
|
||||
<span v-if="icon === 'check'" class="dw-kpi__dot dw-kpi__dot--success" />
|
||||
|
||||
<span v-else-if="icon === 'alert'" class="dw-kpi__dot dw-kpi__dot--danger" />
|
||||
|
||||
<span class="dw-kpi__label">{{ label }}</span>
|
||||
|
||||
<span class="dw-kpi__value">{{ value }}</span>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
|
||||
name: 'DwKpiChip',
|
||||
|
||||
props: {
|
||||
|
||||
label: { type: String, required: true },
|
||||
|
||||
value: { type: [Number, String], default: 0 },
|
||||
|
||||
variant: {
|
||||
|
||||
type: String,
|
||||
|
||||
default: 'default',
|
||||
|
||||
validator: (v) => ['default', 'success', 'muted', 'danger'].includes(v)
|
||||
|
||||
},
|
||||
|
||||
icon: { type: String, default: '' }
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.dw-kpi {
|
||||
|
||||
display: flex;
|
||||
|
||||
align-items: center;
|
||||
|
||||
gap: 8px;
|
||||
|
||||
padding: 5px 12px;
|
||||
|
||||
border-radius: 999px;
|
||||
|
||||
background: #fff;
|
||||
|
||||
border: 1px solid var(--dw-border);
|
||||
|
||||
box-shadow: var(--dw-shadow-sm);
|
||||
|
||||
|
||||
|
||||
&__dot {
|
||||
|
||||
width: 8px;
|
||||
|
||||
height: 8px;
|
||||
|
||||
border-radius: 50%;
|
||||
|
||||
flex-shrink: 0;
|
||||
|
||||
|
||||
|
||||
&--success { background: var(--dw-success); }
|
||||
|
||||
&--danger { background: var(--dw-danger); }
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__label {
|
||||
|
||||
font-size: 12px;
|
||||
|
||||
color: var(--dw-text-secondary);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&__value {
|
||||
|
||||
font-size: 14px;
|
||||
|
||||
font-weight: 700;
|
||||
|
||||
min-width: 18px;
|
||||
|
||||
text-align: center;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
&--success .dw-kpi__value { color: var(--dw-success); }
|
||||
|
||||
&--muted .dw-kpi__value { color: var(--dw-text-muted); }
|
||||
|
||||
&--danger .dw-kpi__value { color: var(--dw-danger); }
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<div class="dw-progress">
|
||||
<div v-if="showLabel" class="dw-progress__label">
|
||||
<span>{{ labelLeft }}</span>
|
||||
<span>{{ labelRight }}</span>
|
||||
</div>
|
||||
<div class="dw-progress__track">
|
||||
<div
|
||||
class="dw-progress__fill"
|
||||
:class="{ 'dw-progress__fill--warning': value >= 90 && value < 100, 'dw-progress__fill--danger': value >= 100 }"
|
||||
:style="{ width: clamped + '%', background: color || undefined }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DwProgressBar',
|
||||
props: {
|
||||
value: { type: Number, default: 0 },
|
||||
color: { type: String, default: '' },
|
||||
showLabel: { type: Boolean, default: false },
|
||||
labelLeft: { type: String, default: '' },
|
||||
labelRight: { type: String, default: '' }
|
||||
},
|
||||
computed: {
|
||||
clamped() {
|
||||
return Math.min(100, Math.max(0, this.value))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dw-progress {
|
||||
&__label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
color: var(--dw-text-secondary);
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
&__track {
|
||||
height: 8px;
|
||||
background: #e5e7eb;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__fill {
|
||||
height: 100%;
|
||||
background: var(--dw-primary);
|
||||
border-radius: 4px;
|
||||
transition: width 0.3s ease;
|
||||
|
||||
&--warning { background: var(--dw-warning) !important; }
|
||||
&--danger { background: var(--dw-danger) !important; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<span class="dw-pill" :class="`dw-pill--${variant}`">
|
||||
<span v-if="dot" class="dw-pill__dot" />
|
||||
<slot>{{ label }}</slot>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DwStatusPill',
|
||||
props: {
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'idle',
|
||||
validator: (v) => ['online', 'idle', 'busy', 'error', 'warning', 'info', 'neutral'].includes(v)
|
||||
},
|
||||
label: { type: String, default: '' },
|
||||
dot: { type: Boolean, default: false }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dw-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
|
||||
&__dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
&--online {
|
||||
background: var(--dw-success-soft);
|
||||
color: var(--dw-success);
|
||||
.dw-pill__dot { animation: dw-pulse 2s infinite; }
|
||||
}
|
||||
|
||||
&--idle { background: #f3f4f6; color: var(--dw-text-secondary); }
|
||||
&--busy { background: var(--dw-info-soft); color: var(--dw-info); }
|
||||
&--error { background: var(--dw-danger-soft); color: var(--dw-danger); }
|
||||
&--warning { background: var(--dw-warning-soft); color: var(--dw-warning); }
|
||||
&--info { background: var(--dw-info-soft); color: var(--dw-info); }
|
||||
&--neutral { background: #f3f4f6; color: #4b5563; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,230 @@
|
||||
<template>
|
||||
<div v-if="previewMode" class="work-add-modal-v2">
|
||||
<div v-if="visible" class="work-add-modal-v2__mask">
|
||||
<div class="work-add-modal-v2__dialog">
|
||||
<div class="work-add-modal-v2__header">
|
||||
<span>新建刻录作业</span>
|
||||
<button type="button" class="work-add-modal-v2__close" @click="close">×</button>
|
||||
</div>
|
||||
<div class="work-add-modal-v2__body">
|
||||
<div class="work-add-modal-v2__left">
|
||||
<h3>1. 数据内容</h3>
|
||||
<div class="work-add-modal-v2__row">
|
||||
<el-select v-model="previewForm.cd_type" size="small" style="width:130px">
|
||||
<el-option v-for="item in previewCdTypes" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
<el-input v-model="previewForm.label" size="small" placeholder="卷标" />
|
||||
</div>
|
||||
<div class="work-add-modal-v2__drop">
|
||||
<p>点击或拖放文件到此处</p>
|
||||
<span>支持 ISO, IMG, 文件夹</span>
|
||||
</div>
|
||||
<dw-progress-bar show-label :value="previewPercent" label-left="已选: 0 B" label-right="上限: 4.7 GB" />
|
||||
</div>
|
||||
<work-label-panel
|
||||
:templates="previewTemplates"
|
||||
:preview-title="previewForm.label"
|
||||
preview-date="2025-11-23"
|
||||
/>
|
||||
</div>
|
||||
<div class="work-add-modal-v2__footer">
|
||||
<div class="work-add-modal-v2__freq">
|
||||
<label>份数</label>
|
||||
<el-input-number v-model="previewForm.task_freq" size="small" :min="1" />
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="dw-btn dw-btn--outline" @click="stub">保存草稿</button>
|
||||
<button type="button" class="dw-btn dw-btn--primary" @click="stub">开始刻录</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<work-add
|
||||
v-else
|
||||
ref="legacy"
|
||||
class="console-work-add-bridge"
|
||||
:is-new="isNew"
|
||||
:save-work-list="saveWorkList"
|
||||
@close="$emit('close')"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WorkAdd from '@/components/workAdd/workAdd.vue'
|
||||
import WorkLabelPanel from './WorkLabelPanel.vue'
|
||||
import DwProgressBar from '../ui/DwProgressBar.vue'
|
||||
|
||||
export default {
|
||||
name: 'WorkAddModalV2',
|
||||
components: { WorkAdd, WorkLabelPanel, DwProgressBar },
|
||||
props: {
|
||||
previewMode: { type: Boolean, default: false },
|
||||
isNew: { type: Boolean, default: true },
|
||||
saveWorkList: { type: Object, default: undefined }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
previewForm: { cd_type: 2, copy_type: 1, label: '2025-11-23_Archive', task_freq: 1 },
|
||||
previewPercent: 0,
|
||||
previewCdTypes: [
|
||||
{ value: 1, label: 'CD (700MB)' },
|
||||
{ value: 2, label: 'DVD (4.7GB)' }
|
||||
],
|
||||
previewTemplates: [{ value: 1, label: '标准日期模板' }]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
if (this.previewMode) {
|
||||
this.visible = true
|
||||
return
|
||||
}
|
||||
this.$refs.legacy.show()
|
||||
},
|
||||
close() {
|
||||
if (this.previewMode) {
|
||||
this.visible = false
|
||||
this.$emit('close')
|
||||
return
|
||||
}
|
||||
this.$refs.legacy.close()
|
||||
},
|
||||
stub() {
|
||||
this.$message.info('预览模式')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.work-add-modal-v2 {
|
||||
&__mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 3000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__dialog {
|
||||
width: 960px;
|
||||
max-width: calc(100vw - 32px);
|
||||
height: 600px;
|
||||
max-height: calc(100vh - 32px);
|
||||
background: #fff;
|
||||
border-radius: var(--dw-radius-lg);
|
||||
box-shadow: var(--dw-shadow-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
border-bottom: 1px solid var(--dw-border);
|
||||
background: #f8fafc;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__close {
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 22px;
|
||||
cursor: pointer;
|
||||
color: var(--dw-text-secondary);
|
||||
}
|
||||
|
||||
&__body {
|
||||
flex: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 0.8fr;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
> * {
|
||||
padding: 16px 20px;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
> *:first-child {
|
||||
border-right: 1px solid var(--dw-border);
|
||||
}
|
||||
}
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&__row {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__drop {
|
||||
flex: 1;
|
||||
min-height: 140px;
|
||||
border: 2px dashed #bfdbfe;
|
||||
background: #f0f9ff;
|
||||
border-radius: var(--dw-radius-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--dw-text-secondary);
|
||||
font-size: 13px;
|
||||
|
||||
p {
|
||||
margin: 0 0 4px;
|
||||
color: var(--dw-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
&__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid var(--dw-border);
|
||||
}
|
||||
|
||||
&__freq {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--dw-text-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.console-app .console-work-add-bridge.add_box .el-dialog {
|
||||
width: 960px !important;
|
||||
max-width: calc(100vw - 40px);
|
||||
border-radius: var(--dw-radius-lg, 14px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.console-app .console-work-add-bridge .work_top_text,
|
||||
.console-app .console-work-add-bridge .work_left_top_label,
|
||||
.console-app .console-work-add-bridge .work_right_top_label {
|
||||
color: var(--dw-text, #111827);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div class="work-label-panel">
|
||||
<div class="work-label-panel__head">
|
||||
<h3 class="work-label-panel__title">2. 盘面标签</h3>
|
||||
<button v-if="hasRunCmd" type="button" class="dw-btn dw-btn--ghost" @click="$emit('new-label')">新建标签</button>
|
||||
<span v-else class="work-label-panel__hint">仅桌面端支持新建标签</span>
|
||||
</div>
|
||||
<div class="work-label-panel__field">
|
||||
<label>选择模板</label>
|
||||
<el-select v-model="currentTemplate" size="small" placeholder="选择模板" @change="$emit('template-change', $event)">
|
||||
<el-option v-for="item in templates" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</div>
|
||||
<div class="work-label-panel__preview">
|
||||
<img v-if="soonImg" class="work-label-panel__img" :src="soonImg" alt="label preview" />
|
||||
<div v-else class="work-label-panel__disc">
|
||||
<div class="work-label-panel__disc-title">{{ previewTitle }}</div>
|
||||
<div class="work-label-panel__disc-date">{{ previewDate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-for="(item, index) in soonList" :key="index" class="work-label-panel__field">
|
||||
<label>{{ item.name }}</label>
|
||||
<el-input v-if="item.type == 3 || item.type == 4 || item.type == 5" v-model="item.val" size="small" clearable :disabled="csvIsExist" />
|
||||
</div>
|
||||
<div class="work-label-panel__warn">
|
||||
请确保打印机已装载光盘托架,且墨盒余量充足。
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'WorkLabelPanel',
|
||||
props: {
|
||||
templates: { type: Array, default: () => [] },
|
||||
currentTemplate: { type: [String, Number], default: '' },
|
||||
soonImg: { type: String, default: '' },
|
||||
soonList: { type: Array, default: () => [] },
|
||||
csvIsExist: { type: Boolean, default: false },
|
||||
hasRunCmd: { type: Boolean, default: false },
|
||||
previewTitle: { type: String, default: '同方档案' },
|
||||
previewDate: { type: String, default: '' }
|
||||
},
|
||||
model: {
|
||||
prop: 'currentTemplate',
|
||||
event: 'update:currentTemplate'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.work-label-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
height: 100%;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
font-size: 12px;
|
||||
color: var(--dw-text-muted);
|
||||
}
|
||||
|
||||
&__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
|
||||
label {
|
||||
font-size: 12px;
|
||||
color: var(--dw-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
&__preview {
|
||||
flex: 1;
|
||||
min-height: 140px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--dw-border);
|
||||
border-radius: var(--dw-radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__img {
|
||||
max-width: 100%;
|
||||
max-height: 180px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
&__disc {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: 50%;
|
||||
background: conic-gradient(from 45deg, #e2e8f0, #fff, #e2e8f0, #cbd5e1, #e2e8f0);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
box-shadow: var(--dw-shadow-md);
|
||||
}
|
||||
|
||||
&__disc-title {
|
||||
font-weight: 700;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
&__disc-date {
|
||||
margin-top: 36px;
|
||||
color: var(--dw-text-secondary);
|
||||
}
|
||||
|
||||
&__warn {
|
||||
font-size: 12px;
|
||||
color: #9a3412;
|
||||
background: #fff7ed;
|
||||
border: 1px solid #ffedd5;
|
||||
border-radius: var(--dw-radius-sm);
|
||||
padding: 8px 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,178 @@
|
||||
export const MOCK_PRINTER_STATUS = {
|
||||
I: '空闲',
|
||||
B: '忙碌',
|
||||
P: '正在打印'
|
||||
}
|
||||
|
||||
export function getConsoleMock() {
|
||||
return {
|
||||
isConnect: true,
|
||||
serviceStatus: true,
|
||||
taskNum: [12, 2, 1],
|
||||
printer_info: {
|
||||
blue_last: 78,
|
||||
red_last: 65,
|
||||
yellow_last: 72,
|
||||
calibration_flag: 0,
|
||||
lack_ink: 0,
|
||||
cover_flag: false,
|
||||
printer_name: '4200',
|
||||
printer_path: '/dev/usb/lp0',
|
||||
printer_status: 'I',
|
||||
printer_tray: 'I',
|
||||
system_ip: '192.168.1.100',
|
||||
system_name: 'discworker-host',
|
||||
serial_no: 'TF-2025-X88',
|
||||
free_cache_space: 51587919872,
|
||||
total_cache_space: 105152176128,
|
||||
test_use: 255,
|
||||
is_set_cddev: 0,
|
||||
strongList: {
|
||||
1: { strong_pos: 1, cd_type: 1, cd_num: 38 },
|
||||
2: { strong_pos: 2, cd_type: 2, cd_num: 25 }
|
||||
},
|
||||
strong_list: [
|
||||
{ strong_pos: 1, cd_type: 1, cd_num: 38 },
|
||||
{ strong_pos: 2, cd_type: 2, cd_num: 25 }
|
||||
]
|
||||
},
|
||||
cd_list: [
|
||||
{
|
||||
cd_pos: 2,
|
||||
cd_status: 1,
|
||||
cd_taskid: '',
|
||||
dev_path: '/dev/sr0',
|
||||
dev_type: 1,
|
||||
cd_type: 0,
|
||||
cd_assid: '',
|
||||
total_size: 4700000000,
|
||||
use_size: 0,
|
||||
copy_scheduler: 0,
|
||||
burn_speed: 0,
|
||||
is_sim: false
|
||||
},
|
||||
{
|
||||
cd_pos: 1,
|
||||
cd_status: 4,
|
||||
cd_taskid: '1763431953099',
|
||||
dev_path: '/dev/sr1',
|
||||
dev_type: 2,
|
||||
cd_type: 2,
|
||||
cd_assid: 'Archive_2025',
|
||||
total_size: 4700000000,
|
||||
use_size: 2115000000,
|
||||
copy_scheduler: 45,
|
||||
burn_speed: 4.2,
|
||||
is_sim: false
|
||||
}
|
||||
],
|
||||
CDName: { 1: '下光驱', 2: '上光驱' },
|
||||
CDStatus: {
|
||||
0: '无法确定光盘状态',
|
||||
1: '没有光盘',
|
||||
2: '光驱托盘打开',
|
||||
3: '光驱未准备好',
|
||||
4: '光盘状态正常,光盘已插入',
|
||||
5: '校验中'
|
||||
},
|
||||
CDTypes: { 1: 'CD', 2: 'DVD', 3: 'DVD_DL', 4: 'BD' },
|
||||
diskTypes: { 1: 'CD', 2: 'DVD', 3: 'BD' },
|
||||
task_list: [
|
||||
{
|
||||
task_uuid: '1763452409668',
|
||||
task_name: 'MU_FU_QIANG_Project_A',
|
||||
finish_freq: 1,
|
||||
total_freq: 1,
|
||||
task_status: 100,
|
||||
start_time: '2025-11-18 15:54:00',
|
||||
finish_time: '2025-11-18 16:10:00',
|
||||
task_size: 1771674009,
|
||||
cdType: 'DVD',
|
||||
id: '1763452409668',
|
||||
ass_task: [
|
||||
{
|
||||
id: '1763452409668_1',
|
||||
cd_type: 2,
|
||||
task_size: 1771674009,
|
||||
task_status: 100,
|
||||
start_time: '2025-11-18 15:54:00',
|
||||
finish_time: '2025-11-18 16:10:00',
|
||||
cdType: 'DVD_1'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
task_uuid: '1763447668606',
|
||||
task_name: 'System_Backup_Weekly',
|
||||
finish_freq: 0,
|
||||
total_freq: 1,
|
||||
task_status: 5,
|
||||
start_time: '2025-11-18 14:35:00',
|
||||
finish_time: '',
|
||||
task_size: 3435973836,
|
||||
cdType: 'DVD',
|
||||
id: '1763447668606',
|
||||
ass_task: [
|
||||
{
|
||||
id: '1763447668606_1',
|
||||
cd_type: 2,
|
||||
task_size: 3435973836,
|
||||
task_status: 5,
|
||||
start_time: '2025-11-18 14:35:00',
|
||||
finish_time: '',
|
||||
cdType: 'DVD_1'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
task_uuid: '1763446831835',
|
||||
task_name: 'MU_FU_QIANG_Docs',
|
||||
finish_freq: 0,
|
||||
total_freq: 1,
|
||||
task_status: 7,
|
||||
start_time: '2025-11-18 14:21:00',
|
||||
finish_time: '',
|
||||
task_size: 434865438,
|
||||
cdType: 'CD',
|
||||
id: '1763446831835',
|
||||
ass_task: []
|
||||
},
|
||||
{
|
||||
task_uuid: '1763431953073',
|
||||
task_name: 'WuGuiSheng_Media',
|
||||
finish_freq: 0,
|
||||
total_freq: 1,
|
||||
task_status: 7,
|
||||
start_time: '2025-11-18 10:38:00',
|
||||
finish_time: '',
|
||||
task_size: 1771674009,
|
||||
cdType: 'DVD',
|
||||
id: '1763431953073',
|
||||
ass_task: []
|
||||
},
|
||||
{
|
||||
task_uuid: '1763431953099',
|
||||
task_name: 'Auto_Archive_2025',
|
||||
finish_freq: 0,
|
||||
total_freq: 1,
|
||||
task_status: 3,
|
||||
start_time: '2025-11-18 16:01:00',
|
||||
finish_time: '',
|
||||
task_size: 1771674009,
|
||||
cdType: 'DVD',
|
||||
id: '1763431953099',
|
||||
ass_task: []
|
||||
}
|
||||
],
|
||||
logList: [
|
||||
{ time: '16:01:02', text: '任务 Auto_Archive_2025 开始刻录', status: 'INFO' },
|
||||
{ time: '15:54:10', text: '任务 MU_FU_QIANG_Project_A 已完成', status: 'INFO' },
|
||||
{ time: '14:35:22', text: 'System_Backup_Weekly 刻录失败', status: 'ERROR' },
|
||||
{ time: '14:21:05', text: '用户取消任务 MU_FU_QIANG_Docs', status: 'DEBUG' }
|
||||
],
|
||||
allLogList: [
|
||||
{ time: '10:00:00', text: '系统启动完成', status: 'INFO' },
|
||||
{ time: '09:58:12', text: '设备 TF-2025-X88 已注册', status: 'INFO' }
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,14 @@ import store from './store'
|
||||
import NProgress from 'nprogress' // Progress 进度条
|
||||
import 'nprogress/nprogress.css'// Progress 进度条样式
|
||||
import { Message } from 'element-ui'
|
||||
import { getDefaultHomeRoute } from '@/utils/console/homeRoute'
|
||||
|
||||
const whiteList = ['/login'] // 不重定向白名单
|
||||
const whiteList = ['/login', '/console/preview'] // 不重定向白名单
|
||||
router.beforeEach((to, from, next) => {
|
||||
NProgress.start()
|
||||
if (store.getters.token) {
|
||||
if (to.path === '/login') {
|
||||
next({ path: '/' })
|
||||
next({ path: getDefaultHomeRoute() })
|
||||
NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
|
||||
} else {
|
||||
next()
|
||||
|
||||