feat: 全面优化前端界面和增强用户体验
- 新增科技风格的抽屉组件样式和主题系统,支持健康监测、设备管理、用户管理、告警配置等多个业务主题 - 优化TenantInfo组件的事件处理,修复Vue 3兼容性问题 - 重构路由器创建逻辑,提升代码可维护性和扩展性 - 统一所有抽屉弹窗的UI风格,增加操作横幅、分组布局、图标提示等现代化设计元素 - 修复customer租户管理权限控制逻辑,改进按钮状态管理和权限验证流程 - 新增批量数据上传测试工具,支持curl和Python接口测试 - 优化设备信息编辑界面,增加分组显示、帮助文本、状态指示等功能 - 创建完整的界面增强指南和自动化脚本,便于后续维护和扩展 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
# Drawer 组件美化指南
|
||||
|
||||
## 📋 概述
|
||||
|
||||
本指南旨在统一美化 ljwx-admin 项目中所有的 `*-operate-drawer.vue` 组件,提供一致的用户体验和现代化的界面设计。
|
||||
|
||||
## 🎯 设计目标
|
||||
|
||||
- **统一性**: 所有 drawer 组件保持一致的视觉风格
|
||||
- **现代化**: 采用现代设计语言,渐变、阴影、动画等
|
||||
- **可用性**: 提升用户体验,清晰的信息层次
|
||||
- **响应式**: 适配不同屏幕尺寸
|
||||
- **可维护性**: 通过统一样式文件易于维护
|
||||
|
||||
## 🎨 设计特色
|
||||
|
||||
### 1. 渐变色彩系统
|
||||
- **主要渐变**: `linear-gradient(135deg, #667eea 0%, #764ba2 100%)`
|
||||
- **用户管理**: 蓝色系渐变
|
||||
- **调度任务**: 橙色系渐变
|
||||
- **设备管理**: 紫色系渐变
|
||||
- **告警配置**: 红色系渐变
|
||||
|
||||
### 2. 操作横幅
|
||||
每个 drawer 顶部都有一个操作横幅,包含:
|
||||
- 彩色图标
|
||||
- 操作标题
|
||||
- 描述文字
|
||||
- 渐变背景
|
||||
|
||||
### 3. 表单分组
|
||||
表单字段按功能分组:
|
||||
- 基本信息
|
||||
- 联系信息
|
||||
- 工作信息
|
||||
- 设备绑定
|
||||
- 执行配置
|
||||
- 触发器配置
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 步骤 1: 导入样式文件
|
||||
|
||||
在你的 drawer 组件的 `<style>` 部分添加:
|
||||
|
||||
```vue
|
||||
<style scoped>
|
||||
@import '@/styles/drawer.scss';
|
||||
|
||||
/* 你的自定义样式 */
|
||||
</style>
|
||||
```
|
||||
|
||||
### 步骤 2: 添加 CSS 类
|
||||
|
||||
给 `NDrawer` 组件添加 `enhanced-drawer` 类:
|
||||
|
||||
```vue
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="420" class="enhanced-drawer">
|
||||
```
|
||||
|
||||
### 步骤 3: 增强标题
|
||||
|
||||
在 script 部分添加增强标题:
|
||||
|
||||
```typescript
|
||||
const enhancedTitle = computed(() => {
|
||||
const icon = isAdd.value ? '👤' : '✏️';
|
||||
return `${icon} ${title.value}`;
|
||||
});
|
||||
```
|
||||
|
||||
### 步骤 4: 添加操作横幅
|
||||
|
||||
在 `NDrawerContent` 内添加操作横幅:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="420" class="enhanced-drawer">
|
||||
<NDrawerContent :title="enhancedTitle" :native-scrollbar="false" closable>
|
||||
<!-- 操作提示横幅 -->
|
||||
<div class="operation-banner">
|
||||
<div class="banner-icon">
|
||||
<i class="i-material-symbols:person-add" v-if="isAdd"></i>
|
||||
<i class="i-material-symbols:person-edit" v-else></i>
|
||||
</div>
|
||||
<div class="banner-content">
|
||||
<h3 class="banner-title">{{ isAdd ? '新增用户' : '编辑用户' }}</h3>
|
||||
<p class="banner-desc">{{ isAdd ? '请填写完整的用户信息,标有 * 的为必填项' : '修改用户信息,部分字段不可编辑' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表单内容 -->
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<!-- 表单字段 -->
|
||||
</NForm>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</template>
|
||||
```
|
||||
|
||||
### 步骤 5: 表单分组
|
||||
|
||||
将表单字段按功能分组:
|
||||
|
||||
```vue
|
||||
<!-- 基本信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="i-material-symbols:badge"></i>
|
||||
基本信息
|
||||
</div>
|
||||
|
||||
<NFormItem label="用户名" path="userName">
|
||||
<NInput v-model:value="model.userName" placeholder="请输入用户名">
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:account-circle"></i>
|
||||
</template>
|
||||
</NInput>
|
||||
</NFormItem>
|
||||
|
||||
<!-- 更多字段 -->
|
||||
</div>
|
||||
```
|
||||
|
||||
### 步骤 6: 增强按钮
|
||||
|
||||
在 footer 部分使用增强的按钮样式:
|
||||
|
||||
```vue
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<NButton @click="closeDrawer" class="cancel-btn">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:close"></i>
|
||||
</template>
|
||||
{{ $t('common.cancel') }}
|
||||
</NButton>
|
||||
<NButton type="primary" @click="handleSubmit" class="submit-btn" :loading="submitting">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:check" v-if="!submitting"></i>
|
||||
</template>
|
||||
{{ isAdd ? $t('common.add') : $t('common.update') }}
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## 🎨 自定义样式
|
||||
|
||||
### 操作横幅颜色
|
||||
|
||||
根据不同功能模块,自定义横幅颜色:
|
||||
|
||||
```scss
|
||||
/* 用户管理 - 蓝色 */
|
||||
.operation-banner {
|
||||
background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%);
|
||||
border-bottom: 1px solid #e0f2fe;
|
||||
|
||||
.banner-icon {
|
||||
background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
|
||||
box-shadow: 0 4px 12px rgba(14, 165, 233, 0.3);
|
||||
}
|
||||
|
||||
.banner-title {
|
||||
color: #0c4a6e;
|
||||
}
|
||||
|
||||
.banner-desc {
|
||||
color: #0369a1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 调度任务 - 橙色 */
|
||||
.operation-banner {
|
||||
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
||||
border-bottom: 1px solid #fde68a;
|
||||
|
||||
.banner-icon {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
|
||||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
|
||||
.banner-title {
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.banner-desc {
|
||||
color: #a16207;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 分组标题图标
|
||||
|
||||
为不同的表单分组使用不同的图标:
|
||||
|
||||
```scss
|
||||
.form-section .section-title {
|
||||
/* 基本信息 */
|
||||
&:has(i[class*="badge"]) { /* 用户基本信息 */ }
|
||||
&:has(i[class*="contact"]) { /* 联系信息 */ }
|
||||
&:has(i[class*="work"]) { /* 工作信息 */ }
|
||||
&:has(i[class*="devices"]) { /* 设备信息 */ }
|
||||
&:has(i[class*="schedule"]) { /* 调度信息 */ }
|
||||
}
|
||||
```
|
||||
|
||||
## 📱 响应式设计
|
||||
|
||||
样式文件已包含完整的响应式设计:
|
||||
|
||||
- **桌面端**: 420px 宽度,多列布局
|
||||
- **平板端**: 适中宽度,优化布局
|
||||
- **移动端**: 全屏显示,单列布局,大按钮
|
||||
|
||||
## ✨ 动画效果
|
||||
|
||||
包含以下动画效果:
|
||||
|
||||
1. **滑入动画**: Drawer 打开时从右侧滑入
|
||||
2. **渐显动画**: 表单项逐个渐显
|
||||
3. **悬浮效果**: 按钮和卡片的悬浮效果
|
||||
4. **过渡动画**: 所有交互的平滑过渡
|
||||
|
||||
## 🛠️ 高级功能
|
||||
|
||||
### 1. 状态指示器
|
||||
|
||||
为状态字段添加可视化指示器:
|
||||
|
||||
```vue
|
||||
<NRadio v-for="item in dictOptions('status')" :key="item.value" :value="item.value">
|
||||
<span class="radio-label">
|
||||
<span class="status-dot" :class="item.value === '1' ? 'active' : 'inactive'"></span>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</NRadio>
|
||||
```
|
||||
|
||||
### 2. 帮助文本
|
||||
|
||||
为复杂字段添加帮助说明:
|
||||
|
||||
```vue
|
||||
<NFormItem label="Cron表达式" path="cronExpression">
|
||||
<NInput v-model:value="model.cronExpression" placeholder="请输入Cron表达式">
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:timer"></i>
|
||||
</template>
|
||||
</NInput>
|
||||
<div class="help-text">
|
||||
Cron表达式,例如: 0 0/5 * * * ? (每5分钟执行一次)
|
||||
<a href="https://cron.qqe2.com/" target="_blank" class="cron-help">在线生成器</a>
|
||||
</div>
|
||||
</NFormItem>
|
||||
```
|
||||
|
||||
### 3. 动态输入增强
|
||||
|
||||
为动态输入组件添加标题和描述:
|
||||
|
||||
```vue
|
||||
<NFormItem span="24" label="任务参数">
|
||||
<div class="dynamic-input-wrapper">
|
||||
<div class="dynamic-input-header">
|
||||
<span class="input-label">任务参数</span>
|
||||
<span class="input-desc">键值对形式的任务参数</span>
|
||||
</div>
|
||||
<NDynamicInput v-model:value="model.jobData" preset="pair">
|
||||
<template #action="{ index, create, remove }">
|
||||
<NSpace class="ml-8px">
|
||||
<NButton size="small" type="primary" @click="() => create(index)">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:add"></i>
|
||||
</template>
|
||||
</NButton>
|
||||
<NButton size="small" type="error" @click="() => remove(index)">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:remove"></i>
|
||||
</template>
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NDynamicInput>
|
||||
</div>
|
||||
</NFormItem>
|
||||
```
|
||||
|
||||
## 🔧 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **样式不生效**
|
||||
- 确保正确导入了 `@/styles/drawer.scss`
|
||||
- 检查是否添加了 `enhanced-drawer` 类
|
||||
|
||||
2. **图标不显示**
|
||||
- 确保图标库已正确安装
|
||||
- 检查图标名称是否正确
|
||||
|
||||
3. **动画卡顿**
|
||||
- 检查是否有CSS冲突
|
||||
- 确保浏览器支持CSS动画
|
||||
|
||||
### 调试技巧
|
||||
|
||||
```scss
|
||||
/* 临时调试样式 */
|
||||
.enhanced-drawer {
|
||||
* {
|
||||
border: 1px solid red !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 性能优化
|
||||
|
||||
1. **按需导入**: 只导入需要的组件和样式
|
||||
2. **缓存优化**: 利用浏览器缓存机制
|
||||
3. **动画优化**: 使用 CSS transform 而非修改布局属性
|
||||
|
||||
## 🌟 最佳实践
|
||||
|
||||
1. **保持一致性**: 所有 drawer 使用相同的设计模式
|
||||
2. **语义化**: 使用语义化的图标和文本
|
||||
3. **可访问性**: 确保键盘导航和屏幕阅读器支持
|
||||
4. **国际化**: 所有文本使用 i18n
|
||||
5. **错误处理**: 优雅处理表单验证和提交错误
|
||||
|
||||
## 🔄 更新日志
|
||||
|
||||
### v1.0.0 (2024-01-XX)
|
||||
- 初始版本发布
|
||||
- 支持基础美化功能
|
||||
- 包含用户管理和调度器示例
|
||||
|
||||
### 未来计划
|
||||
- [ ] 添加更多组件示例
|
||||
- [ ] 支持深色主题
|
||||
- [ ] 增加更多动画效果
|
||||
- [ ] 优化移动端体验
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
如果你想为这个美化方案贡献代码:
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建特性分支
|
||||
3. 提交你的修改
|
||||
4. 创建 Pull Request
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
|
||||
@@ -0,0 +1,480 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Drawer 组件批量美化脚本
|
||||
*
|
||||
* 用法: node scripts/enhance-drawers.js
|
||||
*
|
||||
* 功能:
|
||||
* 1. 扫描所有 *-operate-drawer.vue 文件
|
||||
* 2. 自动应用美化方案
|
||||
* 3. 生成备份文件
|
||||
* 4. 输出美化报告
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
|
||||
// 配置
|
||||
const CONFIG = {
|
||||
// 源文件目录
|
||||
sourceDir: 'src/views',
|
||||
// 备份目录
|
||||
backupDir: 'backup/drawers',
|
||||
// 样式文件导入语句
|
||||
styleImport: "@import '@/styles/drawer.scss';",
|
||||
// 是否生成备份
|
||||
createBackup: true,
|
||||
// 是否强制覆盖
|
||||
forceOverwrite: false
|
||||
};
|
||||
|
||||
// 颜色配置映射
|
||||
const COLOR_THEMES = {
|
||||
'user': {
|
||||
name: '用户管理',
|
||||
bannerBg: 'linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%)',
|
||||
iconBg: 'linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%)',
|
||||
iconShadow: 'rgba(14, 165, 233, 0.3)',
|
||||
titleColor: '#0c4a6e',
|
||||
descColor: '#0369a1',
|
||||
borderColor: '#e0f2fe'
|
||||
},
|
||||
'scheduler': {
|
||||
name: '调度任务',
|
||||
bannerBg: 'linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)',
|
||||
iconBg: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)',
|
||||
iconShadow: 'rgba(245, 158, 11, 0.3)',
|
||||
titleColor: '#92400e',
|
||||
descColor: '#a16207',
|
||||
borderColor: '#fde68a'
|
||||
},
|
||||
'device': {
|
||||
name: '设备管理',
|
||||
bannerBg: 'linear-gradient(135deg, #f3e8ff 0%, #e9d5ff 100%)',
|
||||
iconBg: 'linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%)',
|
||||
iconShadow: 'rgba(139, 92, 246, 0.3)',
|
||||
titleColor: '#581c87',
|
||||
descColor: '#6b21a8',
|
||||
borderColor: '#e9d5ff'
|
||||
},
|
||||
'alert': {
|
||||
name: '告警配置',
|
||||
bannerBg: 'linear-gradient(135deg, #fef2f2 0%, #fecaca 100%)',
|
||||
iconBg: 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)',
|
||||
iconShadow: 'rgba(239, 68, 68, 0.3)',
|
||||
titleColor: '#7f1d1d',
|
||||
descColor: '#991b1b',
|
||||
borderColor: '#fecaca'
|
||||
},
|
||||
'health': {
|
||||
name: '健康数据',
|
||||
bannerBg: 'linear-gradient(135deg, #ecfdf5 0%, #d1fae5 100%)',
|
||||
iconBg: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
iconShadow: 'rgba(16, 185, 129, 0.3)',
|
||||
titleColor: '#064e3b',
|
||||
descColor: '#065f46',
|
||||
borderColor: '#d1fae5'
|
||||
}
|
||||
};
|
||||
|
||||
// 组件图标映射
|
||||
const COMPONENT_ICONS = {
|
||||
'user': 'i-material-symbols:person',
|
||||
'scheduler': 'i-material-symbols:schedule',
|
||||
'device': 'i-material-symbols:devices',
|
||||
'alert': 'i-material-symbols:warning',
|
||||
'health': 'i-material-symbols:health-and-safety',
|
||||
'menu': 'i-material-symbols:menu',
|
||||
'role': 'i-material-symbols:admin-panel-settings',
|
||||
'org': 'i-material-symbols:corporate-fare',
|
||||
'dict': 'i-material-symbols:book',
|
||||
'notice': 'i-material-symbols:notifications',
|
||||
'position': 'i-material-symbols:work',
|
||||
'customer': 'i-material-symbols:business',
|
||||
'interface': 'i-material-symbols:api',
|
||||
'geofence': 'i-material-symbols:fence',
|
||||
'message': 'i-material-symbols:message'
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取组件主题
|
||||
*/
|
||||
function getComponentTheme(filePath) {
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
// 根据文件名判断组件类型
|
||||
for (const [key, theme] of Object.entries(COLOR_THEMES)) {
|
||||
if (fileName.includes(key)) {
|
||||
return { type: key, ...theme };
|
||||
}
|
||||
}
|
||||
|
||||
// 默认主题
|
||||
return {
|
||||
type: 'default',
|
||||
...COLOR_THEMES.user,
|
||||
name: '通用组件'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件图标
|
||||
*/
|
||||
function getComponentIcon(filePath, operationType = 'add') {
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
for (const [key, icon] of Object.entries(COMPONENT_ICONS)) {
|
||||
if (fileName.includes(key)) {
|
||||
return operationType === 'add' ? `${icon}-add` : `${icon}-edit`;
|
||||
}
|
||||
}
|
||||
|
||||
return operationType === 'add' ? 'i-material-symbols:add' : 'i-material-symbols:edit';
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成操作横幅代码
|
||||
*/
|
||||
function generateOperationBanner(theme, componentName) {
|
||||
const addIcon = getComponentIcon('', 'add');
|
||||
const editIcon = getComponentIcon('', 'edit');
|
||||
|
||||
return `
|
||||
<!-- 操作提示横幅 -->
|
||||
<div class="operation-banner">
|
||||
<div class="banner-icon">
|
||||
<i class="${addIcon}" v-if="isAdd"></i>
|
||||
<i class="${editIcon}" v-else></i>
|
||||
</div>
|
||||
<div class="banner-content">
|
||||
<h3 class="banner-title">{{ isAdd ? '新增${componentName}' : '编辑${componentName}' }}</h3>
|
||||
<p class="banner-desc">{{ isAdd ? '请填写完整的${componentName}信息,标有 * 的为必填项' : '修改${componentName}信息,部分字段不可编辑' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成表单分组代码
|
||||
*/
|
||||
function generateFormSection(title, icon, content) {
|
||||
return `
|
||||
<!-- ${title} -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="${icon}"></i>
|
||||
${title}
|
||||
</div>
|
||||
${content}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成增强的底部按钮
|
||||
*/
|
||||
function generateFooter() {
|
||||
return `
|
||||
<template #footer>
|
||||
<div class="drawer-footer">
|
||||
<NButton @click="closeDrawer" class="cancel-btn">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:close"></i>
|
||||
</template>
|
||||
{{ $t('common.cancel') }}
|
||||
</NButton>
|
||||
<NButton type="primary" @click="handleSubmit" class="submit-btn" :loading="submitting">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:check" v-if="!submitting"></i>
|
||||
</template>
|
||||
{{ isAdd ? $t('common.add') : $t('common.update') }}
|
||||
</NButton>
|
||||
</div>
|
||||
</template>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成主题样式
|
||||
*/
|
||||
function generateThemeStyles(theme) {
|
||||
return `
|
||||
/* 操作提示横幅 - ${theme.name} */
|
||||
.operation-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
margin: -24px -24px 24px -24px;
|
||||
background: ${theme.bannerBg};
|
||||
border-bottom: 1px solid ${theme.borderColor};
|
||||
}
|
||||
|
||||
.banner-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background: ${theme.iconBg};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 24px;
|
||||
box-shadow: 0 4px 12px ${theme.iconShadow};
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.banner-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: ${theme.titleColor};
|
||||
margin: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.banner-desc {
|
||||
font-size: 14px;
|
||||
color: ${theme.descColor};
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 底部按钮样式 */
|
||||
.drawer-footer {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: flex-end;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
min-width: 120px;
|
||||
}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个drawer文件
|
||||
*/
|
||||
function enhanceDrawerFile(filePath) {
|
||||
console.log(`处理文件: ${filePath}`);
|
||||
|
||||
try {
|
||||
// 读取文件内容
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// 如果已经美化过,跳过
|
||||
if (content.includes('enhanced-drawer') && !CONFIG.forceOverwrite) {
|
||||
console.log(` ⏭️ 已美化,跳过`);
|
||||
return { status: 'skipped', reason: 'already_enhanced' };
|
||||
}
|
||||
|
||||
// 创建备份
|
||||
if (CONFIG.createBackup) {
|
||||
const backupPath = path.join(CONFIG.backupDir, path.relative(CONFIG.sourceDir, filePath));
|
||||
const backupDir = path.dirname(backupPath);
|
||||
if (!fs.existsSync(backupDir)) {
|
||||
fs.mkdirSync(backupDir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(backupPath, content);
|
||||
console.log(` 📁 创建备份: ${backupPath}`);
|
||||
}
|
||||
|
||||
// 获取组件主题
|
||||
const theme = getComponentTheme(filePath);
|
||||
const componentName = theme.name;
|
||||
|
||||
let newContent = content;
|
||||
|
||||
// 1. 添加 enhanced-drawer 类
|
||||
newContent = newContent.replace(
|
||||
/<NDrawer([^>]*?)>/g,
|
||||
(match, attrs) => {
|
||||
if (!attrs.includes('class=')) {
|
||||
return `<NDrawer${attrs} class="enhanced-drawer">`;
|
||||
} else {
|
||||
return match.replace(/class="([^"]*)"/, 'class="$1 enhanced-drawer"');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 2. 增加drawer宽度
|
||||
newContent = newContent.replace(
|
||||
/:width="360"/g,
|
||||
':width="420"'
|
||||
);
|
||||
|
||||
// 3. 添加增强标题(在script部分)
|
||||
if (!newContent.includes('enhancedTitle')) {
|
||||
const titleAddition = `
|
||||
const enhancedTitle = computed(() => {
|
||||
const icon = isAdd.value ? '✨' : '✏️';
|
||||
return \`\${icon} \${title.value}\`;
|
||||
});`;
|
||||
|
||||
newContent = newContent.replace(
|
||||
/(const isAdd = computed.*?;)/s,
|
||||
'$1\n' + titleAddition
|
||||
);
|
||||
}
|
||||
|
||||
// 4. 添加submitting状态
|
||||
if (!newContent.includes('submitting')) {
|
||||
newContent = newContent.replace(
|
||||
/import.*from 'vue';/,
|
||||
match => match.replace('} from \'vue\';', ', ref } from \'vue\';')
|
||||
);
|
||||
|
||||
newContent = newContent.replace(
|
||||
/(const.*?= computed.*?;)/s,
|
||||
'$1\nconst submitting = ref(false);'
|
||||
);
|
||||
}
|
||||
|
||||
// 5. 增强handleSubmit函数
|
||||
newContent = newContent.replace(
|
||||
/async function handleSubmit\(\) \{[\s\S]*?\}/,
|
||||
`async function handleSubmit() {
|
||||
try {
|
||||
submitting.value = true;
|
||||
await validate();
|
||||
|
||||
// request
|
||||
const func = isAdd.value ? fetchAdd : fetchUpdate;
|
||||
const { error, data } = await func(model);
|
||||
if (!error && data) {
|
||||
window.$message?.success(isAdd.value ? $t('common.addSuccess') : $t('common.updateSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Submit error:', error);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}`
|
||||
);
|
||||
|
||||
// 6. 添加操作横幅到模板
|
||||
const bannerCode = generateOperationBanner(theme, componentName);
|
||||
newContent = newContent.replace(
|
||||
/(<NDrawerContent[^>]*>)/,
|
||||
'$1' + bannerCode
|
||||
);
|
||||
|
||||
// 7. 更新footer
|
||||
const footerCode = generateFooter();
|
||||
newContent = newContent.replace(
|
||||
/<template #footer>[\s\S]*?<\/template>/,
|
||||
footerCode
|
||||
);
|
||||
|
||||
// 8. 添加或更新样式
|
||||
const themeStyles = generateThemeStyles(theme);
|
||||
const styleImport = `@import '${CONFIG.styleImport}';`;
|
||||
|
||||
if (newContent.includes('<style scoped>')) {
|
||||
newContent = newContent.replace(
|
||||
/<style scoped>([\s\S]*?)<\/style>/,
|
||||
`<style scoped>\n${styleImport}\n\n${themeStyles}\n$1\n</style>`
|
||||
);
|
||||
} else {
|
||||
newContent = newContent.replace(
|
||||
/<\/template>$/,
|
||||
`</template>\n\n<style scoped>\n${styleImport}\n\n${themeStyles}\n</style>`
|
||||
);
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
fs.writeFileSync(filePath, newContent);
|
||||
console.log(` ✅ 美化完成`);
|
||||
|
||||
return { status: 'success', theme: theme.name };
|
||||
|
||||
} catch (error) {
|
||||
console.error(` ❌ 处理失败: ${error.message}`);
|
||||
return { status: 'error', error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主函数
|
||||
*/
|
||||
function main() {
|
||||
console.log('🎨 开始批量美化 Drawer 组件...\n');
|
||||
|
||||
// 确保备份目录存在
|
||||
if (CONFIG.createBackup && !fs.existsSync(CONFIG.backupDir)) {
|
||||
fs.mkdirSync(CONFIG.backupDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 查找所有drawer文件
|
||||
const pattern = path.join(CONFIG.sourceDir, '**/*-operate-drawer.vue');
|
||||
const files = glob.sync(pattern);
|
||||
|
||||
console.log(`📁 发现 ${files.length} 个 drawer 文件\n`);
|
||||
|
||||
// 统计信息
|
||||
const stats = {
|
||||
total: files.length,
|
||||
success: 0,
|
||||
skipped: 0,
|
||||
error: 0,
|
||||
themes: {}
|
||||
};
|
||||
|
||||
// 处理每个文件
|
||||
files.forEach((file, index) => {
|
||||
console.log(`[${index + 1}/${files.length}] ${file}`);
|
||||
const result = enhanceDrawerFile(file);
|
||||
|
||||
stats[result.status]++;
|
||||
|
||||
if (result.theme) {
|
||||
stats.themes[result.theme] = (stats.themes[result.theme] || 0) + 1;
|
||||
}
|
||||
|
||||
console.log('');
|
||||
});
|
||||
|
||||
// 输出统计结果
|
||||
console.log('📊 处理统计:');
|
||||
console.log(` 总计: ${stats.total}`);
|
||||
console.log(` 成功: ${stats.success}`);
|
||||
console.log(` 跳过: ${stats.skipped}`);
|
||||
console.log(` 失败: ${stats.error}`);
|
||||
|
||||
if (Object.keys(stats.themes).length > 0) {
|
||||
console.log('\n🎨 主题分布:');
|
||||
for (const [theme, count] of Object.entries(stats.themes)) {
|
||||
console.log(` ${theme}: ${count}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n✨ 美化完成!');
|
||||
|
||||
if (CONFIG.createBackup) {
|
||||
console.log(`📁 备份文件保存在: ${CONFIG.backupDir}`);
|
||||
}
|
||||
|
||||
console.log('\n📖 查看美化指南: docs/DRAWER_ENHANCEMENT_GUIDE.md');
|
||||
}
|
||||
|
||||
// 运行主函数
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
enhanceDrawerFile,
|
||||
getComponentTheme,
|
||||
generateOperationBanner,
|
||||
generateFormSection,
|
||||
generateFooter,
|
||||
generateThemeStyles
|
||||
};
|
||||
@@ -146,7 +146,7 @@ onMounted(() => {
|
||||
trigger="click"
|
||||
size="medium"
|
||||
:show-arrow="false"
|
||||
@show="handleDropdownShow"
|
||||
@update:show="(show: boolean) => show && handleDropdownShow()"
|
||||
@select="handleSelect"
|
||||
>
|
||||
<template #default>
|
||||
|
||||
@@ -11,10 +11,14 @@ const historyCreatorMap: Record<Env.RouterHistoryMode, (base?: string) => Router
|
||||
memory: createMemoryHistory
|
||||
};
|
||||
|
||||
export const router = createRouter({
|
||||
history: historyCreatorMap[VITE_ROUTER_HISTORY_MODE](VITE_BASE_URL),
|
||||
routes: createBuiltinVueRoutes()
|
||||
});
|
||||
function createAppRouter() {
|
||||
return createRouter({
|
||||
history: historyCreatorMap[VITE_ROUTER_HISTORY_MODE](VITE_BASE_URL),
|
||||
routes: createBuiltinVueRoutes()
|
||||
});
|
||||
}
|
||||
|
||||
export const router = createAppRouter();
|
||||
|
||||
/** Setup Vue Router */
|
||||
export async function setupRouter(app: App) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import './reset.css';
|
||||
@import './nprogress.css';
|
||||
@import './transition.css';
|
||||
@import '../enhanced-drawer.scss';
|
||||
|
||||
html,
|
||||
body,
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
/**
|
||||
* 统一的 Drawer 组件美化样式
|
||||
* 适用于所有 *-operate-drawer.vue 组件
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
Drawer 整体样式
|
||||
========================================================================== */
|
||||
|
||||
.enhanced-drawer {
|
||||
/* Drawer 内容区域 */
|
||||
:deep(.n-drawer-content) {
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Drawer 头部样式 */
|
||||
:deep(.n-drawer-header) {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 20px 24px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.n-drawer-header)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="rgba(255,255,255,0.1)"/><circle cx="75" cy="75" r="1" fill="rgba(255,255,255,0.05)"/><circle cx="50" cy="10" r="0.5" fill="rgba(255,255,255,0.08)"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
:deep(.n-drawer-header__main) {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
:deep(.n-drawer-title) {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
:deep(.n-drawer-title)::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* 关闭按钮样式 */
|
||||
:deep(.n-drawer-close) {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s ease;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:deep(.n-drawer-close):hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Drawer 内容区域 */
|
||||
:deep(.n-drawer-body) {
|
||||
padding: 24px;
|
||||
background: linear-gradient(135deg, #f8fafc 0%, #ffffff 100%);
|
||||
}
|
||||
|
||||
/* Drawer 底部区域 */
|
||||
:deep(.n-drawer-footer) {
|
||||
background: #fafafa;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding: 16px 24px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
表单样式优化
|
||||
========================================================================== */
|
||||
|
||||
.enhanced-drawer {
|
||||
/* 表单整体样式 */
|
||||
:deep(.n-form) {
|
||||
.n-form-item {
|
||||
margin-bottom: 20px;
|
||||
|
||||
/* 表单项标签 */
|
||||
.n-form-item-label {
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
/* 必填标记 */
|
||||
.n-form-item-label__asterisk {
|
||||
color: #ef4444;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 表单项反馈 */
|
||||
.n-form-item-feedback {
|
||||
color: #ef4444;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
:deep(.n-input) {
|
||||
border-radius: 8px;
|
||||
border: 2px solid #e5e7eb;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
|
||||
&:hover {
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&.n-input--focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
|
||||
.n-input__input-el {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
|
||||
&::placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 选择框样式 */
|
||||
:deep(.n-select) {
|
||||
.n-base-selection {
|
||||
border-radius: 8px;
|
||||
border: 2px solid #e5e7eb;
|
||||
transition: all 0.3s ease;
|
||||
background: white;
|
||||
|
||||
&:hover {
|
||||
border-color: #d1d5db;
|
||||
}
|
||||
|
||||
&.n-base-selection--focus {
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.n-base-selection-placeholder {
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
|
||||
/* 单选按钮组样式 */
|
||||
:deep(.n-radio-group) {
|
||||
.n-radio {
|
||||
margin-right: 16px;
|
||||
|
||||
.n-radio__dot {
|
||||
border: 2px solid #d1d5db;
|
||||
|
||||
&::before {
|
||||
background: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
&.n-radio--checked {
|
||||
.n-radio__dot {
|
||||
border-color: #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
.n-radio__label {
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 动态输入组件样式 */
|
||||
:deep(.n-dynamic-input) {
|
||||
.n-dynamic-input-item {
|
||||
background: #f9fafb;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.n-input {
|
||||
border: 1px solid #d1d5db;
|
||||
}
|
||||
}
|
||||
|
||||
.n-dynamic-input-preset-pair {
|
||||
.n-dynamic-input-pair-input:first-child {
|
||||
.n-input {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.n-dynamic-input-pair-input:last-child {
|
||||
.n-input {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
按钮样式优化
|
||||
========================================================================== */
|
||||
|
||||
.enhanced-drawer {
|
||||
/* 主要按钮 */
|
||||
:deep(.n-button--primary-type) {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
padding: 0 20px;
|
||||
height: 36px;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 次要按钮 */
|
||||
:deep(.n-button--default-type),
|
||||
:deep(.n-button--quaternary-type) {
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
padding: 0 20px;
|
||||
height: 36px;
|
||||
background: white;
|
||||
color: #6b7280;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #d1d5db;
|
||||
color: #374151;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* 小按钮样式 */
|
||||
:deep(.n-button--medium-type) {
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
|
||||
&.n-button--primary-type {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
box-shadow: 0 1px 4px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
/* 图标按钮 */
|
||||
:deep(.n-button) {
|
||||
.text-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
分组和分隔样式
|
||||
========================================================================== */
|
||||
|
||||
.enhanced-drawer {
|
||||
/* 表单分组 */
|
||||
.form-section {
|
||||
margin-bottom: 24px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 2px solid #f3f4f6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 4px;
|
||||
height: 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 分隔线 */
|
||||
.form-divider {
|
||||
margin: 24px 0;
|
||||
border: none;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, #e5e7eb, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
状态和提示样式
|
||||
========================================================================== */
|
||||
|
||||
.enhanced-drawer {
|
||||
/* 成功状态 */
|
||||
.status-success {
|
||||
color: #10b981;
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #d1fae5;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 警告状态 */
|
||||
.status-warning {
|
||||
color: #f59e0b;
|
||||
background: #fffbeb;
|
||||
border: 1px solid #fed7aa;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.status-error {
|
||||
color: #ef4444;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 帮助提示 */
|
||||
.help-text {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
margin-top: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
响应式设计
|
||||
========================================================================== */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.enhanced-drawer {
|
||||
:deep(.n-drawer) {
|
||||
.n-drawer-content {
|
||||
width: 100vw !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.n-drawer-body) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
:deep(.n-drawer-footer) {
|
||||
padding: 12px 16px;
|
||||
flex-direction: column-reverse;
|
||||
gap: 8px;
|
||||
|
||||
.n-button {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
动画效果
|
||||
========================================================================== */
|
||||
|
||||
.enhanced-drawer {
|
||||
/* 打开动画 */
|
||||
:deep(.n-drawer-content) {
|
||||
animation: slideInRight 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* 表单项动画 */
|
||||
:deep(.n-form-item) {
|
||||
animation: fadeInUp 0.4s ease-out;
|
||||
}
|
||||
|
||||
:deep(.n-form-item:nth-child(2)) { animation-delay: 0.1s; }
|
||||
:deep(.n-form-item:nth-child(3)) { animation-delay: 0.15s; }
|
||||
:deep(.n-form-item:nth-child(4)) { animation-delay: 0.2s; }
|
||||
:deep(.n-form-item:nth-child(5)) { animation-delay: 0.25s; }
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
暗色主题支持
|
||||
========================================================================== */
|
||||
|
||||
.dark {
|
||||
.enhanced-drawer {
|
||||
:deep(.n-drawer-content) {
|
||||
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
|
||||
}
|
||||
|
||||
:deep(.n-drawer-body) {
|
||||
background: linear-gradient(135deg, #1f2937 0%, #111827 100%);
|
||||
}
|
||||
|
||||
:deep(.n-drawer-footer) {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
}
|
||||
|
||||
:deep(.n-form-item-label) {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
:deep(.n-input) {
|
||||
background: #4b5563;
|
||||
border-color: #6b7280;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,702 @@
|
||||
/**
|
||||
* 高级 Drawer 美化样式 v2.0
|
||||
* 现代化、科技感、专业的界面设计
|
||||
* 设计理念:简洁、现代、高效、专业
|
||||
*/
|
||||
|
||||
/* ==========================================================================
|
||||
CSS 变量定义 - 统一的设计令牌
|
||||
========================================================================== */
|
||||
|
||||
.enhanced-drawer {
|
||||
/* 基础色彩系统 */
|
||||
--primary: #6366f1;
|
||||
--primary-light: #818cf8;
|
||||
--primary-dark: #4f46e5;
|
||||
--secondary: #8b5cf6;
|
||||
--accent: #06b6d4;
|
||||
|
||||
/* 中性色调 */
|
||||
--white: #ffffff;
|
||||
--gray-50: #f8fafc;
|
||||
--gray-100: #f1f5f9;
|
||||
--gray-200: #e2e8f0;
|
||||
--gray-300: #cbd5e1;
|
||||
--gray-400: #94a3b8;
|
||||
--gray-500: #64748b;
|
||||
--gray-600: #475569;
|
||||
--gray-700: #334155;
|
||||
--gray-800: #1e293b;
|
||||
--gray-900: #0f172a;
|
||||
|
||||
/* 功能色彩 */
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
--error: #ef4444;
|
||||
--info: #3b82f6;
|
||||
|
||||
/* 阴影系统 */
|
||||
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
||||
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
|
||||
/* 间距系统 */
|
||||
--space-xs: 4px;
|
||||
--space-sm: 8px;
|
||||
--space-md: 16px;
|
||||
--space-lg: 24px;
|
||||
--space-xl: 32px;
|
||||
--space-2xl: 48px;
|
||||
|
||||
/* 圆角系统 */
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 8px;
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--radius-2xl: 24px;
|
||||
|
||||
/* 字体系统 */
|
||||
--font-sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
--font-weight-normal: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
/* 过渡动画 */
|
||||
--transition-fast: 150ms ease-out;
|
||||
--transition-base: 250ms ease-out;
|
||||
--transition-slow: 350ms ease-out;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Drawer 容器样式 - 现代化设计
|
||||
========================================================================== */
|
||||
|
||||
.enhanced-drawer {
|
||||
/* Drawer 内容区域 */
|
||||
.n-drawer-content {
|
||||
background: var(--white) !important;
|
||||
border-radius: 0 !important;
|
||||
box-shadow: var(--shadow-2xl) !important;
|
||||
border-left: 1px solid var(--gray-200) !important;
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
/* 顶部装饰线 */
|
||||
.n-drawer-content::before {
|
||||
content: '' !important;
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
height: 3px !important;
|
||||
background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 50%, var(--accent) 100%) !important;
|
||||
z-index: 10 !important;
|
||||
}
|
||||
|
||||
/* Drawer 头部 - 简洁专业 */
|
||||
.n-drawer-header {
|
||||
background: var(--white) !important;
|
||||
color: var(--gray-900) !important;
|
||||
border: none !important;
|
||||
border-bottom: 1px solid var(--gray-200) !important;
|
||||
padding: var(--space-xl) !important;
|
||||
position: relative !important;
|
||||
backdrop-filter: blur(8px) !important;
|
||||
}
|
||||
|
||||
.n-drawer-title {
|
||||
font-size: 20px !important;
|
||||
font-weight: var(--font-weight-semibold) !important;
|
||||
color: var(--gray-900) !important;
|
||||
font-family: var(--font-sans) !important;
|
||||
letter-spacing: -0.025em !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: var(--space-md) !important;
|
||||
}
|
||||
|
||||
.n-drawer-title::before {
|
||||
content: '' !important;
|
||||
width: 4px !important;
|
||||
height: 20px !important;
|
||||
background: linear-gradient(180deg, var(--primary) 0%, var(--secondary) 100%) !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
/* 关闭按钮优化 */
|
||||
.n-drawer-close {
|
||||
color: var(--gray-500) !important;
|
||||
background: var(--gray-100) !important;
|
||||
border: 1px solid var(--gray-200) !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
transition: all var(--transition-base) !important;
|
||||
}
|
||||
|
||||
.n-drawer-close:hover {
|
||||
background: var(--gray-200) !important;
|
||||
border-color: var(--gray-300) !important;
|
||||
color: var(--gray-700) !important;
|
||||
transform: scale(1.05) !important;
|
||||
}
|
||||
|
||||
/* Drawer 内容区域 */
|
||||
.n-drawer-body {
|
||||
padding: var(--space-xl) !important;
|
||||
background: var(--gray-50) !important;
|
||||
min-height: 100% !important;
|
||||
}
|
||||
|
||||
/* Drawer 底部区域 */
|
||||
.n-drawer-footer {
|
||||
background: var(--white) !important;
|
||||
border-top: 1px solid var(--gray-200) !important;
|
||||
padding: var(--space-lg) var(--space-xl) !important;
|
||||
display: flex !important;
|
||||
justify-content: flex-end !important;
|
||||
gap: var(--space-md) !important;
|
||||
backdrop-filter: blur(8px) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
表单分组 - 卡片式设计
|
||||
========================================================================== */
|
||||
|
||||
.enhanced-drawer .form-section {
|
||||
background: var(--white) !important;
|
||||
border: 1px solid var(--gray-200) !important;
|
||||
border-radius: var(--radius-xl) !important;
|
||||
padding: var(--space-xl) !important;
|
||||
margin-bottom: var(--space-lg) !important;
|
||||
box-shadow: var(--shadow-sm) !important;
|
||||
transition: all var(--transition-base) !important;
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .form-section:hover {
|
||||
box-shadow: var(--shadow-md) !important;
|
||||
border-color: var(--gray-300) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .form-section::before {
|
||||
content: '' !important;
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
height: 2px !important;
|
||||
background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 100%) !important;
|
||||
opacity: 0 !important;
|
||||
transition: opacity var(--transition-base) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .form-section:hover::before {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .section-title {
|
||||
font-size: 16px !important;
|
||||
font-weight: var(--font-weight-semibold) !important;
|
||||
color: var(--gray-800) !important;
|
||||
margin-bottom: var(--space-lg) !important;
|
||||
padding-bottom: var(--space-md) !important;
|
||||
border-bottom: 1px solid var(--gray-200) !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: var(--space-md) !important;
|
||||
font-family: var(--font-sans) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .section-title i {
|
||||
font-size: 18px !important;
|
||||
color: var(--primary) !important;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
表单控件优化 - 现代化输入体验
|
||||
========================================================================== */
|
||||
|
||||
/* 表单项间距 */
|
||||
.enhanced-drawer .n-form-item {
|
||||
margin-bottom: var(--space-lg) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .n-form-item:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* 标签样式 */
|
||||
.enhanced-drawer .n-form-item-label {
|
||||
font-weight: var(--font-weight-medium) !important;
|
||||
color: var(--gray-700) !important;
|
||||
font-size: 14px !important;
|
||||
margin-bottom: var(--space-sm) !important;
|
||||
font-family: var(--font-sans) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .n-form-item-label__asterisk {
|
||||
color: var(--error) !important;
|
||||
margin-right: var(--space-xs) !important;
|
||||
}
|
||||
|
||||
/* 输入框样式 */
|
||||
.enhanced-drawer .n-input {
|
||||
border-radius: var(--radius-md) !important;
|
||||
border: 1px solid var(--gray-300) !important;
|
||||
transition: all var(--transition-base) !important;
|
||||
background: var(--white) !important;
|
||||
box-shadow: var(--shadow-sm) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .n-input:hover {
|
||||
border-color: var(--gray-400) !important;
|
||||
box-shadow: var(--shadow-md) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .n-input:focus,
|
||||
.enhanced-drawer .n-input.n-input--focus {
|
||||
border-color: var(--primary) !important;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1), var(--shadow-md) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .n-input .n-input__input-el {
|
||||
font-size: 14px !important;
|
||||
color: var(--gray-800) !important;
|
||||
font-family: var(--font-sans) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .n-input .n-input__input-el::placeholder {
|
||||
color: var(--gray-400) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .n-input .n-input__prefix {
|
||||
color: var(--gray-500) !important;
|
||||
margin-right: var(--space-sm) !important;
|
||||
}
|
||||
|
||||
/* 选择框样式 */
|
||||
.enhanced-drawer .n-select .n-base-selection {
|
||||
border-radius: var(--radius-md) !important;
|
||||
border: 1px solid var(--gray-300) !important;
|
||||
transition: all var(--transition-base) !important;
|
||||
background: var(--white) !important;
|
||||
box-shadow: var(--shadow-sm) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .n-select .n-base-selection:hover {
|
||||
border-color: var(--gray-400) !important;
|
||||
box-shadow: var(--shadow-md) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .n-select .n-base-selection.n-base-selection--focus {
|
||||
border-color: var(--primary) !important;
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1), var(--shadow-md) !important;
|
||||
}
|
||||
|
||||
/* 数字输入框 */
|
||||
.enhanced-drawer .n-input-number .n-input {
|
||||
border-radius: var(--radius-md) !important;
|
||||
}
|
||||
|
||||
/* 文本域 */
|
||||
.enhanced-drawer .n-input--textarea {
|
||||
border-radius: var(--radius-md) !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
按钮系统 - 现代化交互
|
||||
========================================================================== */
|
||||
|
||||
/* 主要按钮 */
|
||||
.enhanced-drawer .n-button--primary-type {
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%) !important;
|
||||
border: none !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
font-weight: var(--font-weight-medium) !important;
|
||||
padding: 0 var(--space-lg) !important;
|
||||
height: 40px !important;
|
||||
box-shadow: var(--shadow-md) !important;
|
||||
transition: all var(--transition-base) !important;
|
||||
font-family: var(--font-sans) !important;
|
||||
letter-spacing: 0.025em !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .n-button--primary-type:hover {
|
||||
transform: translateY(-1px) !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
background: linear-gradient(135deg, var(--primary-dark) 0%, var(--primary) 100%) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .n-button--primary-type:active {
|
||||
transform: translateY(0) !important;
|
||||
box-shadow: var(--shadow-md) !important;
|
||||
}
|
||||
|
||||
/* 次要按钮 */
|
||||
.enhanced-drawer .n-button--default-type,
|
||||
.enhanced-drawer .n-button--quaternary-type {
|
||||
border: 1px solid var(--gray-300) !important;
|
||||
border-radius: var(--radius-md) !important;
|
||||
font-weight: var(--font-weight-medium) !important;
|
||||
padding: 0 var(--space-lg) !important;
|
||||
height: 40px !important;
|
||||
background: var(--white) !important;
|
||||
color: var(--gray-700) !important;
|
||||
transition: all var(--transition-base) !important;
|
||||
font-family: var(--font-sans) !important;
|
||||
box-shadow: var(--shadow-sm) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .n-button--default-type:hover,
|
||||
.enhanced-drawer .n-button--quaternary-type:hover {
|
||||
border-color: var(--gray-400) !important;
|
||||
color: var(--gray-800) !important;
|
||||
transform: translateY(-1px) !important;
|
||||
box-shadow: var(--shadow-md) !important;
|
||||
background: var(--gray-50) !important;
|
||||
}
|
||||
|
||||
/* 底部按钮容器 */
|
||||
.enhanced-drawer .drawer-footer {
|
||||
display: flex !important;
|
||||
gap: var(--space-md) !important;
|
||||
justify-content: flex-end !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .cancel-btn {
|
||||
min-width: 100px !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .submit-btn {
|
||||
min-width: 120px !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
操作横幅 - 信息化设计
|
||||
========================================================================== */
|
||||
|
||||
.enhanced-drawer .operation-banner {
|
||||
background: linear-gradient(135deg, var(--gray-50) 0%, var(--white) 100%) !important;
|
||||
border: 1px solid var(--gray-200) !important;
|
||||
border-radius: var(--radius-xl) !important;
|
||||
padding: var(--space-xl) !important;
|
||||
margin: calc(-1 * var(--space-xl)) calc(-1 * var(--space-xl)) var(--space-xl) calc(-1 * var(--space-xl)) !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: var(--space-lg) !important;
|
||||
position: relative !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .operation-banner::before {
|
||||
content: '' !important;
|
||||
position: absolute !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
height: 2px !important;
|
||||
background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 50%, var(--accent) 100%) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .banner-icon {
|
||||
width: 48px !important;
|
||||
height: 48px !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%) !important;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: center !important;
|
||||
color: var(--white) !important;
|
||||
font-size: 20px !important;
|
||||
box-shadow: var(--shadow-lg) !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .banner-content {
|
||||
flex: 1 !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .banner-title {
|
||||
font-size: 18px !important;
|
||||
font-weight: var(--font-weight-semibold) !important;
|
||||
color: var(--gray-900) !important;
|
||||
margin: 0 0 var(--space-xs) 0 !important;
|
||||
font-family: var(--font-sans) !important;
|
||||
letter-spacing: -0.025em !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .banner-desc {
|
||||
font-size: 14px !important;
|
||||
color: var(--gray-600) !important;
|
||||
margin: 0 !important;
|
||||
line-height: 1.5 !important;
|
||||
font-family: var(--font-sans) !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
帮助文本和状态指示器
|
||||
========================================================================== */
|
||||
|
||||
.enhanced-drawer .help-text {
|
||||
font-size: 12px !important;
|
||||
color: var(--gray-500) !important;
|
||||
margin-top: var(--space-xs) !important;
|
||||
line-height: 1.4 !important;
|
||||
font-family: var(--font-sans) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .status-dot {
|
||||
width: 8px !important;
|
||||
height: 8px !important;
|
||||
border-radius: 50% !important;
|
||||
display: inline-block !important;
|
||||
margin-right: var(--space-xs) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .status-dot.active {
|
||||
background: var(--success) !important;
|
||||
box-shadow: 0 0 8px rgba(16, 185, 129, 0.4) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .status-dot.inactive {
|
||||
background: var(--error) !important;
|
||||
box-shadow: 0 0 8px rgba(239, 68, 68, 0.4) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .radio-label {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: var(--space-sm) !important;
|
||||
font-family: var(--font-sans) !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
动态输入组件
|
||||
========================================================================== */
|
||||
|
||||
.enhanced-drawer .dynamic-input-wrapper {
|
||||
border: 1px solid var(--gray-200) !important;
|
||||
border-radius: var(--radius-lg) !important;
|
||||
padding: var(--space-lg) !important;
|
||||
background: var(--gray-50) !important;
|
||||
margin-top: var(--space-sm) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .dynamic-input-header {
|
||||
display: flex !important;
|
||||
justify-content: space-between !important;
|
||||
align-items: center !important;
|
||||
margin-bottom: var(--space-md) !important;
|
||||
padding-bottom: var(--space-sm) !important;
|
||||
border-bottom: 1px solid var(--gray-200) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .input-label {
|
||||
font-weight: var(--font-weight-medium) !important;
|
||||
color: var(--gray-800) !important;
|
||||
font-size: 14px !important;
|
||||
font-family: var(--font-sans) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .input-desc {
|
||||
font-size: 12px !important;
|
||||
color: var(--gray-500) !important;
|
||||
font-family: var(--font-sans) !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
主题变体 - 精致的色彩系统
|
||||
========================================================================== */
|
||||
|
||||
/* 调度任务主题 - 橙色 */
|
||||
.enhanced-drawer.scheduler-theme {
|
||||
--primary: #f59e0b;
|
||||
--primary-light: #fbbf24;
|
||||
--primary-dark: #d97706;
|
||||
--secondary: #f97316;
|
||||
}
|
||||
|
||||
/* 设备管理主题 - 紫色 */
|
||||
.enhanced-drawer.device-theme {
|
||||
--primary: #8b5cf6;
|
||||
--primary-light: #a78bfa;
|
||||
--primary-dark: #7c3aed;
|
||||
--secondary: #a855f7;
|
||||
}
|
||||
|
||||
/* 告警管理主题 - 红色 */
|
||||
.enhanced-drawer.alert-theme {
|
||||
--primary: #ef4444;
|
||||
--primary-light: #f87171;
|
||||
--primary-dark: #dc2626;
|
||||
--secondary: #f43f5e;
|
||||
}
|
||||
|
||||
/* 健康数据主题 - 绿色 */
|
||||
.enhanced-drawer.health-theme {
|
||||
--primary: #10b981;
|
||||
--primary-light: #34d399;
|
||||
--primary-dark: #059669;
|
||||
--secondary: #06b6d4;
|
||||
}
|
||||
|
||||
/* 用户管理主题 - 蓝色 */
|
||||
.enhanced-drawer.user-theme {
|
||||
--primary: #3b82f6;
|
||||
--primary-light: #60a5fa;
|
||||
--primary-dark: #2563eb;
|
||||
--secondary: #6366f1;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
响应式设计
|
||||
========================================================================== */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.enhanced-drawer {
|
||||
.n-drawer .n-drawer-content {
|
||||
width: 100vw !important;
|
||||
}
|
||||
|
||||
.n-drawer-header,
|
||||
.n-drawer-body,
|
||||
.n-drawer-footer {
|
||||
padding-left: var(--space-lg) !important;
|
||||
padding-right: var(--space-lg) !important;
|
||||
}
|
||||
|
||||
.n-drawer-footer {
|
||||
flex-direction: column-reverse !important;
|
||||
gap: var(--space-sm) !important;
|
||||
}
|
||||
|
||||
.n-drawer-footer .n-button {
|
||||
width: 100% !important;
|
||||
height: 44px !important;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: var(--space-lg) !important;
|
||||
margin-bottom: var(--space-md) !important;
|
||||
}
|
||||
|
||||
.operation-banner {
|
||||
flex-direction: column !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
高级动画效果
|
||||
========================================================================== */
|
||||
|
||||
.enhanced-drawer {
|
||||
/* Drawer 打开动画 */
|
||||
.n-drawer-content {
|
||||
animation: slideInRight 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94) !important;
|
||||
}
|
||||
|
||||
/* 表单项渐入动画 */
|
||||
.n-form-item {
|
||||
animation: fadeInUp 0.5s ease-out !important;
|
||||
animation-fill-mode: both !important;
|
||||
}
|
||||
|
||||
.n-form-item:nth-child(1) { animation-delay: 0.1s !important; }
|
||||
.n-form-item:nth-child(2) { animation-delay: 0.15s !important; }
|
||||
.n-form-item:nth-child(3) { animation-delay: 0.2s !important; }
|
||||
.n-form-item:nth-child(4) { animation-delay: 0.25s !important; }
|
||||
.n-form-item:nth-child(5) { animation-delay: 0.3s !important; }
|
||||
.n-form-item:nth-child(6) { animation-delay: 0.35s !important; }
|
||||
|
||||
/* 表单分组动画 */
|
||||
.form-section {
|
||||
animation: fadeInUp 0.6s ease-out !important;
|
||||
animation-fill-mode: both !important;
|
||||
}
|
||||
|
||||
.form-section:nth-child(1) { animation-delay: 0.2s !important; }
|
||||
.form-section:nth-child(2) { animation-delay: 0.3s !important; }
|
||||
.form-section:nth-child(3) { animation-delay: 0.4s !important; }
|
||||
}
|
||||
|
||||
@keyframes slideInRight {
|
||||
from {
|
||||
transform: translateX(100%) !important;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
transform: translateY(20px) !important;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
暗色主题支持
|
||||
========================================================================== */
|
||||
|
||||
.dark .enhanced-drawer {
|
||||
--white: #1e293b;
|
||||
--gray-50: #0f172a;
|
||||
--gray-100: #1e293b;
|
||||
--gray-200: #334155;
|
||||
--gray-300: #475569;
|
||||
--gray-400: #64748b;
|
||||
--gray-500: #94a3b8;
|
||||
--gray-600: #cbd5e1;
|
||||
--gray-700: #e2e8f0;
|
||||
--gray-800: #f1f5f9;
|
||||
--gray-900: #f8fafc;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
自定义滚动条
|
||||
========================================================================== */
|
||||
|
||||
.enhanced-drawer .n-drawer-body::-webkit-scrollbar {
|
||||
width: 6px !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .n-drawer-body::-webkit-scrollbar-track {
|
||||
background: var(--gray-100) !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .n-drawer-body::-webkit-scrollbar-thumb {
|
||||
background: var(--gray-300) !important;
|
||||
border-radius: var(--radius-sm) !important;
|
||||
transition: background var(--transition-base) !important;
|
||||
}
|
||||
|
||||
.enhanced-drawer .n-drawer-body::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--gray-400) !important;
|
||||
}
|
||||
+2
-2
@@ -237,8 +237,8 @@ function getActionText(action: string) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" :width="600" :mask-closable="false">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" :width="600" :mask-closable="false" class="enhanced-drawer alert-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer alert-theme">
|
||||
<!-- 查看模式 -->
|
||||
<template v-if="isView">
|
||||
<NCard>
|
||||
|
||||
@@ -101,8 +101,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="480">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="480" class="enhanced-drawer alert-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer alert-theme">
|
||||
<NForm ref="formRef" :model="model" label-placement="left" :label-width="100">
|
||||
<!-- 企业微信配置 -->
|
||||
<template v-if="props.type === 'enterprise'">
|
||||
|
||||
@@ -112,8 +112,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer alert-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer alert-theme">
|
||||
<NForm ref="formRef" :model="model">
|
||||
<NFormItem span="24 s:8 m:12" :label="$t('page.health.device.message.departmentName')" path="departmentName">
|
||||
<NTreeSelect
|
||||
|
||||
@@ -91,8 +91,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer alert-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer alert-theme">
|
||||
<NForm ref="formRef" :model="model">
|
||||
<NFormItemGi span="24 s:8 m:6" :label="$t('page.health.alert.log.logId')" path="logId">
|
||||
<NInput v-model:value="model.logId" size="small" :placeholder="$t('page.health.alert.log.form.logId')" />
|
||||
|
||||
@@ -215,8 +215,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="480">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="480" class="enhanced-drawer alert-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer alert-theme">
|
||||
<div class="form-container">
|
||||
<!-- 提示信息 -->
|
||||
<NAlert v-if="isAdd" type="info" :show-icon="false" class="mb-4">
|
||||
|
||||
@@ -192,7 +192,8 @@ const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagi
|
||||
);
|
||||
}
|
||||
|
||||
if (hasAuth('t:customer:config:delete') && isAdmin.value) {
|
||||
// 删除按钮 - 根据权限控制
|
||||
if (hasAuth('t:customer:config:delete')) {
|
||||
buttons.push(
|
||||
h(
|
||||
NPopconfirm,
|
||||
@@ -207,7 +208,8 @@ const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagi
|
||||
{
|
||||
type: 'error',
|
||||
quaternary: true,
|
||||
size: 'small'
|
||||
size: 'small',
|
||||
disabled: !isAdmin.value // 非admin用户按钮禁用但可见
|
||||
},
|
||||
() => $t('common.delete')
|
||||
)
|
||||
@@ -225,37 +227,21 @@ const { columns, columnChecks, data, loading, getData, getDataByPage, mobilePagi
|
||||
const { drawerVisible, openDrawer, checkedRowKeys, onDeleted, onBatchDeleted } = useTableOperate(data, getData);
|
||||
|
||||
function handleAdd() {
|
||||
if (!isAdmin.value) {
|
||||
window.$message?.warning('只有管理员才能添加租户配置');
|
||||
return;
|
||||
}
|
||||
operateType.value = 'add';
|
||||
openDrawer();
|
||||
}
|
||||
|
||||
function edit(item: Api.Customer.CustomerConfig) {
|
||||
if (!isAdmin.value) {
|
||||
window.$message?.warning('只有管理员才能编辑租户配置');
|
||||
return;
|
||||
}
|
||||
operateType.value = 'edit';
|
||||
editingData.value = { ...item };
|
||||
openDrawer();
|
||||
}
|
||||
|
||||
async function handleDelete(id: string) {
|
||||
if (!isAdmin.value) {
|
||||
window.$message?.warning('只有管理员才能删除租户配置');
|
||||
return;
|
||||
}
|
||||
await handleDeleteWithPrecheck([id]);
|
||||
}
|
||||
|
||||
async function handleBatchDelete() {
|
||||
if (!isAdmin.value) {
|
||||
window.$message?.warning('只有管理员才能批量删除租户配置');
|
||||
return;
|
||||
}
|
||||
await handleDeleteWithPrecheck(checkedRowKeys.value);
|
||||
}
|
||||
|
||||
@@ -614,8 +600,8 @@ function formatDateTime(dateTime: string | null): string {
|
||||
v-model:columns="columnChecks"
|
||||
:checked-row-keys="checkedRowKeys"
|
||||
:loading="loading"
|
||||
:add-auth="isAdmin ? 't:customer:config:add' : ''"
|
||||
:delete-auth="isAdmin ? 't:customer:config:delete' : ''"
|
||||
:add-auth="'t:customer:config:add'"
|
||||
:delete-auth="'t:customer:config:delete'"
|
||||
@add="handleAdd"
|
||||
@delete="handleBatchDelete"
|
||||
@refresh="getData"
|
||||
|
||||
@@ -272,8 +272,8 @@ function beforeUpload(data: { file: UploadFileInfo; fileList: UploadFileInfo[] }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer">
|
||||
<NForm ref="formRef" :model="model">
|
||||
<NFormItem :label="$t('page.customer.config.customerName')" path="customerName">
|
||||
<NInput v-model:value="model.customerName" :placeholder="$t('page.customer.config.form.customerName')" :disabled="!isAdd" />
|
||||
|
||||
@@ -95,8 +95,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer health-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer health-theme">
|
||||
<NForm ref="formRef" :model="model">
|
||||
<NFormItem :label="$t('page.health.data.config.dataType')" path="dataType">
|
||||
<NInput v-model:value="model.dataType" :placeholder="$t('page.health.data.config.form.dataType')" :disabled="!isAdd" />
|
||||
|
||||
@@ -90,8 +90,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer">
|
||||
<NForm ref="formRef" :model="model">
|
||||
<NFormItem :label="$t('page.customer.interface.name')" path="name">
|
||||
<NInput v-model:value="model.name" :placeholder="$t('page.customer.interface.form.name')" :disabled="!isAdd" filterable />
|
||||
|
||||
@@ -102,8 +102,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer device-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer device-theme">
|
||||
<NForm ref="formRef" :model="model">
|
||||
<NFormItem :label="$t('page.customer.interface.name')" path="name">
|
||||
<NInput v-model:value="model.name" :placeholder="$t('page.customer.interface.form.name')" :disabled="!isAdd" />
|
||||
|
||||
@@ -74,7 +74,11 @@ function handleInitModel() {
|
||||
if (!props.rowData) return;
|
||||
|
||||
if (props.operateType === 'edit' && props.rowData) {
|
||||
Object.assign(model, props.rowData);
|
||||
Object.assign(model, {
|
||||
...props.rowData,
|
||||
networkAccessMode: Number(props.rowData.networkAccessMode) || 0,
|
||||
batteryLevel: Number(props.rowData.batteryLevel) || 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,45 +108,132 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="420" class="enhanced-drawer device-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<!-- 操作横幅 -->
|
||||
<div class="operation-banner">
|
||||
<div class="banner-icon">
|
||||
<i class="i-material-symbols:devices" v-if="isAdd"></i>
|
||||
<i class="i-material-symbols:settings" v-else></i>
|
||||
</div>
|
||||
<div class="banner-content">
|
||||
<h3 class="banner-title">{{ isAdd ? '新增设备' : '编辑设备' }}</h3>
|
||||
<p class="banner-desc">{{ isAdd ? '配置新设备的基本信息和参数' : '修改设备配置信息,确保数据准确性' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NForm ref="formRef" :model="model">
|
||||
<NFormItem :label="$t('page.health.device.info.systemSoftwareVersion')" path="systemSoftwareVersion">
|
||||
<NInput
|
||||
v-model:value="model.systemSoftwareVersion"
|
||||
:placeholder="$t('page.health.device.info.form.systemSoftwareVersion')"
|
||||
:disabled="!isAdd"
|
||||
/>
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.health.device.info.serialNumber')" path="serialNumber">
|
||||
<NInput v-model:value="model.serialNumber" :placeholder="$t('page.health.device.info.form.serialNumber')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.health.device.info.model')" path="model">
|
||||
<NSelect v-model:value="model.model" :options="dictOptions('device_model')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.health.device.info.networkAccessMode')" path="networkAccessMode">
|
||||
<NRadioGroup v-model:value="model.networkAccessMode">
|
||||
<NInputNumber v-model:value="model.networkAccessMode" :placeholder="$t('page.health.device.info.form.networkAccessMode')" />
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.health.device.info.wearableStatus')" path="wearableStatus">
|
||||
<NSelect v-model:value="model.wearableStatus" :options="dictOptions('wear_status')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.health.device.info.batterylevel')" path="batteryLevel">
|
||||
<NInputNumber v-model:value="model.batteryLevel" :placeholder="$t('page.health.device.info.form.batterylevel')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.health.device.info.chargingStatus')" path="chargingStatus">
|
||||
<NSelect v-model:value="model.chargingStatus" :options="dictOptions('charging_status')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.health.device.info.status')" path="status">
|
||||
<NSelect v-model:value="model.status" :options="dictOptions('device_status')" />
|
||||
</NFormItem>
|
||||
<!-- 基本信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="i-material-symbols:info"></i>
|
||||
基本信息
|
||||
</div>
|
||||
|
||||
<NFormItem :label="$t('page.health.device.info.systemSoftwareVersion')" path="systemSoftwareVersion">
|
||||
<NInput
|
||||
v-model:value="model.systemSoftwareVersion"
|
||||
:placeholder="$t('page.health.device.info.form.systemSoftwareVersion')"
|
||||
:disabled="!isAdd"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:system-update"></i>
|
||||
</template>
|
||||
</NInput>
|
||||
<div class="help-text" v-if="!isAdd">系统版本不可修改</div>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="$t('page.health.device.info.serialNumber')" path="serialNumber">
|
||||
<NInput v-model:value="model.serialNumber" :placeholder="$t('page.health.device.info.form.serialNumber')">
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:tag"></i>
|
||||
</template>
|
||||
</NInput>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="$t('page.health.device.info.model')" path="model">
|
||||
<NSelect v-model:value="model.model" :options="dictOptions('device_model')">
|
||||
<template #arrow>
|
||||
<i class="i-material-symbols:smartphone"></i>
|
||||
</template>
|
||||
</NSelect>
|
||||
</NFormItem>
|
||||
</div>
|
||||
|
||||
<!-- 网络配置 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="i-material-symbols:network-wifi"></i>
|
||||
网络配置
|
||||
</div>
|
||||
|
||||
<NFormItem :label="$t('page.health.device.info.networkAccessMode')" path="networkAccessMode">
|
||||
<NInputNumber v-model:value="model.networkAccessMode" :placeholder="$t('page.health.device.info.form.networkAccessMode')">
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:router"></i>
|
||||
</template>
|
||||
</NInputNumber>
|
||||
<div class="help-text">网络访问模式配置</div>
|
||||
</NFormItem>
|
||||
</div>
|
||||
|
||||
<!-- 设备状态 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="i-material-symbols:health-and-safety"></i>
|
||||
设备状态
|
||||
</div>
|
||||
|
||||
<NFormItem :label="$t('page.health.device.info.wearableStatus')" path="wearableStatus">
|
||||
<NSelect v-model:value="model.wearableStatus" :options="dictOptions('wear_status')">
|
||||
<template #arrow>
|
||||
<i class="i-material-symbols:watch"></i>
|
||||
</template>
|
||||
</NSelect>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="$t('page.health.device.info.batterylevel')" path="batteryLevel">
|
||||
<NInputNumber v-model:value="model.batteryLevel" :placeholder="$t('page.health.device.info.form.batterylevel')">
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:battery-full"></i>
|
||||
</template>
|
||||
</NInputNumber>
|
||||
<div class="help-text">电池电量 (0-100%)</div>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="$t('page.health.device.info.chargingStatus')" path="chargingStatus">
|
||||
<NSelect v-model:value="model.chargingStatus" :options="dictOptions('charging_status')">
|
||||
<template #arrow>
|
||||
<i class="i-material-symbols:charging-station"></i>
|
||||
</template>
|
||||
</NSelect>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="$t('page.health.device.info.status')" path="status">
|
||||
<NSelect v-model:value="model.status" :options="dictOptions('device_status')">
|
||||
<template #arrow>
|
||||
<i class="i-material-symbols:power"></i>
|
||||
</template>
|
||||
</NSelect>
|
||||
</NFormItem>
|
||||
</div>
|
||||
</NForm>
|
||||
|
||||
<template #footer>
|
||||
<NSpace>
|
||||
<NButton quaternary @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
|
||||
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
|
||||
</NSpace>
|
||||
<div class="drawer-footer">
|
||||
<NButton @click="closeDrawer" class="cancel-btn">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:close"></i>
|
||||
</template>
|
||||
{{ $t('common.cancel') }}
|
||||
</NButton>
|
||||
<NButton type="primary" @click="handleSubmit" class="submit-btn">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:check"></i>
|
||||
</template>
|
||||
{{ isAdd ? $t('common.add') : $t('common.update') }}
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
|
||||
@@ -305,8 +305,8 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" :width="800" :closable="false">
|
||||
<NDrawerContent :title="title" closable @close="closeDrawer">
|
||||
<NDrawer v-model:show="visible" :width="800" :closable="false" class="enhanced-drawer device-theme">
|
||||
<NDrawerContent :title="title" closable @close="closeDrawer" class="enhanced-drawer device-theme">
|
||||
<NForm ref="formRef" :model="model" :rules="rules" label-placement="left" :label-width="120">
|
||||
<div class="space-y-16px">
|
||||
<!-- 基本信息 -->
|
||||
|
||||
@@ -133,8 +133,8 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer device-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer device-theme">
|
||||
<NForm ref="formRef" :model="model">
|
||||
<NFormItem :label="$t('page.health.device.message.departmentName')" path="orgId">
|
||||
<NTreeSelect
|
||||
|
||||
@@ -94,8 +94,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer user-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer user-theme">
|
||||
<NForm ref="formRef" :model="model">
|
||||
<NFormItemGi span="24 s:8 m:6" :label="$t('page.health.device.user.deviceId')" path="deviceId">
|
||||
<NInput v-model:value="model.deviceId" size="small" :placeholder="$t('page.health.device.user.form.deviceId')" />
|
||||
|
||||
@@ -99,8 +99,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer">
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem :label="$t('page.geofence.geofence.name')" path="name">
|
||||
<NInput v-model:value="model.name" :placeholder="$t('page.geofence.geofence.form.name')" />
|
||||
|
||||
@@ -98,8 +98,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer health-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer health-theme">
|
||||
<NForm ref="formRef" :model="model"></NForm>
|
||||
<template #footer>
|
||||
<NSpace>
|
||||
|
||||
@@ -96,8 +96,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer health-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer health-theme">
|
||||
<NForm ref="formRef" :model="model">
|
||||
<NFormItem :label="$t('page.health.data.config.configId')" path="configId">
|
||||
<NInputNumber v-model:value="model.configId" :placeholder="$t('page.health.data.config.form.configId')" />
|
||||
|
||||
@@ -104,8 +104,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer user-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer user-theme">
|
||||
<NForm ref="formRef" :model="model">
|
||||
<NFormItem :label="$t('page.health.data.info.phonenumber')" path="phoneNumber">
|
||||
<NInput v-model:value="model.phoneNumber" :placeholder="$t('page.health.data.info.form.phoneNumber')" :disabled="!isAdd" />
|
||||
|
||||
@@ -241,8 +241,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" width="720">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" width="720" class="enhanced-drawer user-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer user-theme">
|
||||
<NForm ref="formRef" :model="formModel" :rules="rules" label-placement="top" label-width="auto">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<!-- 基本信息 -->
|
||||
|
||||
@@ -14,8 +14,8 @@ defineEmits<{
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" width="600">
|
||||
<NDrawerContent title="健康档案操作">
|
||||
<NDrawer v-model:show="visible" width="600" class="enhanced-drawer health-theme">
|
||||
<NDrawerContent title="健康档案操作" class="enhanced-drawer health-theme">
|
||||
<div class="p-8 text-center">
|
||||
<NIcon size="48" class="mb-4 text-primary">
|
||||
<icon-ic:baseline-construction />
|
||||
|
||||
+2
-2
@@ -337,8 +337,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" width="800">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" width="800" class="enhanced-drawer health-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer health-theme">
|
||||
<NForm ref="formRef" :model="formModel" :rules="rules" label-placement="top" label-width="auto">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<!-- 基本信息 -->
|
||||
|
||||
@@ -104,8 +104,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer health-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer health-theme">
|
||||
<NForm ref="formRef" :model="model"></NForm>
|
||||
<template #footer>
|
||||
<NSpace>
|
||||
|
||||
@@ -150,8 +150,8 @@ async function handleSubmit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer device-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer device-theme">
|
||||
<NForm ref="formRef" :model="model">
|
||||
<NFormItem :label="$t('page.health.device.message.departmentName')" path="departmentName">
|
||||
<NTreeSelect
|
||||
|
||||
@@ -107,8 +107,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer user-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer user-theme">
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem :label="$t('page.manage.dictItem.value')" path="value">
|
||||
<NInput v-model:value="model.value" :placeholder="$t('page.manage.dictItem.form.value')" />
|
||||
|
||||
@@ -100,8 +100,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer user-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer user-theme">
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem :label="$t('page.manage.dict.name')" path="name">
|
||||
<NInput v-model:value="model.name" :placeholder="$t('page.manage.dict.form.name')" />
|
||||
|
||||
@@ -238,8 +238,8 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" width="sm:450 s:360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" width="sm:450 s:360" class="enhanced-drawer user-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer user-theme">
|
||||
<NForm ref="formRef" :model="model" :rules="rules" label-placement="left" :label-width="80">
|
||||
<NGrid>
|
||||
<NFormItemGi span="12" :label="$t('page.manage.menu.type')" path="type">
|
||||
|
||||
@@ -100,8 +100,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer user-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer user-theme">
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem :label="$t('page.manage.notice.category')" path="category">
|
||||
<NSelect v-model:value="model.category" :options="dictOptions('notice_category')" :placeholder="$t('page.manage.notice.form.category')" />
|
||||
|
||||
@@ -143,8 +143,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer user-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer user-theme">
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem :label="isDeptOperation ? $t('page.manage.orgUnits.dept.name') : $t('page.manage.orgUnits.name')" path="name">
|
||||
<NInput
|
||||
|
||||
@@ -107,8 +107,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer user-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer user-theme">
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem :label="$t('page.manage.position.name')" path="name">
|
||||
<NInput v-model:value="model.name" :placeholder="$t('page.manage.position.form.name')" />
|
||||
|
||||
@@ -104,8 +104,8 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer user-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer user-theme">
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem :label="$t('page.manage.role.roleName')" path="roleName">
|
||||
<NInput v-model:value="model.roleName" :placeholder="$t('page.manage.role.form.roleName')" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import { NSelect } from 'naive-ui';
|
||||
import { computed, reactive, watch, ref } from 'vue';
|
||||
import { NSelect, NInputNumber } from 'naive-ui';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { useAuthStore } from '@/store/modules/auth';
|
||||
import { fetchAddUser, fetchGetEditUserInfo, fetchUpdateUserInfo } from '@/service/api';
|
||||
@@ -79,6 +79,12 @@ const rules: Record<RuleKey, App.Global.FormRule[]> = {
|
||||
};
|
||||
|
||||
const isAdd = computed(() => props.operateType === 'add');
|
||||
const submitting = ref(false);
|
||||
|
||||
const enhancedTitle = computed(() => {
|
||||
const icon = isAdd.value ? '👤' : '✏️';
|
||||
return `${icon} ${title.value}`;
|
||||
});
|
||||
|
||||
async function handleInitModel() {
|
||||
Object.assign(model, createDefaultModel());
|
||||
@@ -96,14 +102,22 @@ function closeDrawer() {
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
// request
|
||||
const func = isAdd.value ? fetchAddUser : fetchUpdateUserInfo;
|
||||
const { error, data } = await func(model);
|
||||
if (!error && data) {
|
||||
window.$message?.success(isAdd.value ? $t('common.addSuccess') : $t('common.updateSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
try {
|
||||
submitting.value = true;
|
||||
await validate();
|
||||
|
||||
// request
|
||||
const func = isAdd.value ? fetchAddUser : fetchUpdateUserInfo;
|
||||
const { error, data } = await func(model);
|
||||
if (!error && data) {
|
||||
window.$message?.success(isAdd.value ? $t('common.addSuccess') : $t('common.updateSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Submit error:', error);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,43 +131,178 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="420" class="enhanced-drawer">
|
||||
<NDrawerContent :title="enhancedTitle" :native-scrollbar="false" closable>
|
||||
<!-- 操作提示 -->
|
||||
<div class="operation-banner">
|
||||
<div class="banner-icon">
|
||||
<i class="i-material-symbols:person-add" v-if="isAdd"></i>
|
||||
<i class="i-material-symbols:person-edit" v-else></i>
|
||||
</div>
|
||||
<div class="banner-content">
|
||||
<h3 class="banner-title">{{ isAdd ? '新增用户' : '编辑用户' }}</h3>
|
||||
<p class="banner-desc">{{ isAdd ? '请填写完整的用户信息,标有 * 的为必填项' : '修改用户信息,部分字段不可编辑' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem :label="$t('page.manage.user.userName')" path="userName">
|
||||
<NInput v-model:value="model.userName" :placeholder="$t('page.manage.user.form.userName')" :disabled="!isAdd" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.manage.user.gender')" path="gender">
|
||||
<NRadioGroup v-model:value="model.gender">
|
||||
<NRadio v-for="item in dictOptions('gender')" :key="item.value" :value="item.value" :label="item.label" />
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.manage.user.userCardNumber')" path="userCardNumber">
|
||||
<NInput v-model:value="model.userCardNumber" :placeholder="$t('page.manage.user.form.userCardNumber')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.manage.user.phone')" path="phone">
|
||||
<NInput v-model:value="model.phone" :placeholder="$t('page.manage.user.form.phone')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.manage.user.workingYears')" path="workingYears">
|
||||
<NInput v-model:value="model.workingYears" :placeholder="$t('page.manage.user.form.workingYears')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.manage.user.deviceSn')" path="deviceSn">
|
||||
<NSelect v-model:value="model.deviceSn" :placeholder="$t('page.manage.user.form.deviceSn')" :options="deviceOptions" filterable />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.manage.user.status')" path="status">
|
||||
<NRadioGroup v-model:value="model.status">
|
||||
<NRadio v-for="item in dictOptions('status')" :key="item.value" :value="item.value" :label="item.label" />
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
<!-- 基本信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="i-material-symbols:badge"></i>
|
||||
基本信息
|
||||
</div>
|
||||
|
||||
<NFormItem :label="$t('page.manage.user.userName')" path="userName">
|
||||
<NInput
|
||||
v-model:value="model.userName"
|
||||
:placeholder="$t('page.manage.user.form.userName')"
|
||||
:disabled="!isAdd"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:account-circle"></i>
|
||||
</template>
|
||||
</NInput>
|
||||
<div class="help-text" v-if="!isAdd">用户名不可修改</div>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="$t('page.manage.user.realName')" path="realName">
|
||||
<NInput
|
||||
v-model:value="model.realName"
|
||||
:placeholder="$t('page.manage.user.form.realName')"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:person"></i>
|
||||
</template>
|
||||
</NInput>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="$t('page.manage.user.gender')" path="gender">
|
||||
<NRadioGroup v-model:value="model.gender">
|
||||
<NRadio v-for="item in dictOptions('gender')" :key="item.value" :value="item.value" :label="item.label" />
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="$t('page.manage.user.userCardNumber')" path="userCardNumber">
|
||||
<NInput
|
||||
v-model:value="model.userCardNumber"
|
||||
:placeholder="$t('page.manage.user.form.userCardNumber')"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:credit-card"></i>
|
||||
</template>
|
||||
</NInput>
|
||||
</NFormItem>
|
||||
</div>
|
||||
|
||||
<!-- 联系信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="i-material-symbols:contact-phone"></i>
|
||||
联系信息
|
||||
</div>
|
||||
|
||||
<NFormItem :label="$t('page.manage.user.phone')" path="phone">
|
||||
<NInput
|
||||
v-model:value="model.phone"
|
||||
:placeholder="$t('page.manage.user.form.phone')"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:phone"></i>
|
||||
</template>
|
||||
</NInput>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="$t('page.manage.user.email')" path="email">
|
||||
<NInput
|
||||
v-model:value="model.email"
|
||||
:placeholder="$t('page.manage.user.form.email')"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:email"></i>
|
||||
</template>
|
||||
</NInput>
|
||||
</NFormItem>
|
||||
</div>
|
||||
|
||||
<!-- 工作信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="i-material-symbols:work"></i>
|
||||
工作信息
|
||||
</div>
|
||||
|
||||
<NFormItem :label="$t('page.manage.user.workingYears')" path="workingYears">
|
||||
<NInputNumber
|
||||
v-model:value="model.workingYears"
|
||||
:placeholder="$t('page.manage.user.form.workingYears')"
|
||||
:min="0"
|
||||
:max="50"
|
||||
style="width: 100%"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:schedule"></i>
|
||||
</template>
|
||||
</NInputNumber>
|
||||
<div class="help-text">工作年限(0-50年)</div>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="$t('page.manage.user.status')" path="status">
|
||||
<NRadioGroup v-model:value="model.status">
|
||||
<NRadio v-for="item in dictOptions('status')" :key="item.value" :value="item.value">
|
||||
<span class="radio-label">
|
||||
<span class="status-dot" :class="item.value === '1' ? 'active' : 'inactive'"></span>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
</NRadio>
|
||||
</NRadioGroup>
|
||||
</NFormItem>
|
||||
</div>
|
||||
|
||||
<!-- 设备绑定 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="i-material-symbols:devices"></i>
|
||||
设备绑定
|
||||
</div>
|
||||
|
||||
<NFormItem :label="$t('page.manage.user.deviceSn')" path="deviceSn">
|
||||
<NSelect
|
||||
v-model:value="model.deviceSn"
|
||||
:placeholder="$t('page.manage.user.form.deviceSn')"
|
||||
:options="deviceOptions"
|
||||
filterable
|
||||
clearable
|
||||
>
|
||||
<template #arrow>
|
||||
<i class="i-material-symbols:smartphone"></i>
|
||||
</template>
|
||||
</NSelect>
|
||||
<div class="help-text">选择要绑定的设备,可以为空</div>
|
||||
</NFormItem>
|
||||
</div>
|
||||
</NForm>
|
||||
|
||||
<template #footer>
|
||||
<NSpace :size="16">
|
||||
<NButton quaternary @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
|
||||
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
|
||||
</NSpace>
|
||||
<div class="drawer-footer">
|
||||
<NButton @click="closeDrawer" class="cancel-btn">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:close"></i>
|
||||
</template>
|
||||
{{ $t('common.cancel') }}
|
||||
</NButton>
|
||||
<NButton type="primary" @click="handleSubmit" class="submit-btn" :loading="submitting">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:check" v-if="!submitting"></i>
|
||||
</template>
|
||||
{{ isAdd ? $t('common.add') : $t('common.update') }}
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
/* 组件特定样式 - 全局美化样式已通过 enhanced-drawer 类自动应用 */
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import { computed, reactive, watch, ref } from 'vue';
|
||||
import { useFormRules, useNaiveForm } from '@/hooks/common/form';
|
||||
import { fetchAddScheduler, fetchGetEditScheduler, fetchUpdateSchedulerInfo } from '@/service/api';
|
||||
import { $t } from '@/locales';
|
||||
@@ -69,6 +69,12 @@ const rules: Record<RuleKey, App.Global.FormRule[]> = {
|
||||
};
|
||||
|
||||
const isEdit = computed(() => props.operateType === 'edit');
|
||||
const submitting = ref(false);
|
||||
|
||||
const enhancedTitle = computed(() => {
|
||||
const icon = !isEdit.value ? '⏰' : '✏️';
|
||||
return `${icon} ${title.value}`;
|
||||
});
|
||||
|
||||
async function handleInitModel() {
|
||||
Object.assign(model, createDefaultModel());
|
||||
@@ -92,14 +98,22 @@ function closeDrawer() {
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
await validate();
|
||||
// request
|
||||
const func = isEdit.value ? fetchUpdateSchedulerInfo : fetchAddScheduler;
|
||||
const { error, data } = await func(model);
|
||||
if (!error && data) {
|
||||
window.$message?.success(isEdit.value ? $t('common.updateSuccess') : $t('common.addSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
try {
|
||||
submitting.value = true;
|
||||
await validate();
|
||||
|
||||
// request
|
||||
const func = isEdit.value ? fetchUpdateSchedulerInfo : fetchAddScheduler;
|
||||
const { error, data } = await func(model);
|
||||
if (!error && data) {
|
||||
window.$message?.success(isEdit.value ? $t('common.updateSuccess') : $t('common.addSuccess'));
|
||||
closeDrawer();
|
||||
emit('submitted');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Submit error:', error);
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,80 +126,251 @@ watch(visible, () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" width="480" class="enhanced-drawer scheduler-theme">
|
||||
<NDrawerContent :title="enhancedTitle" :native-scrollbar="false" closable>
|
||||
<!-- 操作提示 -->
|
||||
<div class="operation-banner">
|
||||
<div class="banner-icon">
|
||||
<i class="i-material-symbols:schedule" v-if="!isEdit"></i>
|
||||
<i class="i-material-symbols:edit-calendar" v-else></i>
|
||||
</div>
|
||||
<div class="banner-content">
|
||||
<h3 class="banner-title">{{ !isEdit ? '新增定时任务' : '编辑定时任务' }}</h3>
|
||||
<p class="banner-desc">{{ !isEdit ? '配置新的定时任务,请仔细填写相关参数' : '修改任务配置,部分核心字段不可修改' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NForm ref="formRef" :model="model" :rules="rules">
|
||||
<NFormItem :label="$t('page.monitor.scheduler.jobName')" path="jobName">
|
||||
<NInput v-model:value="model.jobName" :placeholder="$t('page.monitor.scheduler.form.jobName')" :disabled="isEdit" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.monitor.scheduler.jobGroup')" path="jobGroup">
|
||||
<NInput v-model:value="model.jobGroup" :placeholder="$t('page.monitor.scheduler.form.jobGroup')" :disabled="isEdit" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.monitor.scheduler.jobClassName')" path="jobClassName">
|
||||
<NInput v-model:value="model.jobClassName" :placeholder="$t('page.monitor.scheduler.form.jobClassName')" />
|
||||
</NFormItem>
|
||||
<NFormItem span="24" :label="$t('page.monitor.scheduler.jobData')">
|
||||
<NDynamicInput
|
||||
v-model:value="model.jobData"
|
||||
preset="pair"
|
||||
:key-placeholder="$t('page.monitor.scheduler.form.jobDataKey')"
|
||||
:value-placeholder="$t('page.monitor.scheduler.form.jobDataValue')"
|
||||
>
|
||||
<template #action="{ index, create, remove }">
|
||||
<NSpace class="ml-8px">
|
||||
<NButton size="medium" @click="() => create(index)">
|
||||
<icon-ic:round-plus class="text-icon" />
|
||||
</NButton>
|
||||
<NButton size="medium" @click="() => remove(index)">
|
||||
<icon-ic-round-remove class="text-icon" />
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NDynamicInput>
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.monitor.scheduler.cronExpression')" path="cronExpression">
|
||||
<NInput v-model:value="model.cronExpression" :placeholder="$t('page.monitor.scheduler.form.cronExpression')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.monitor.scheduler.description')" path="description">
|
||||
<NInput v-model:value="model.description" :placeholder="$t('page.monitor.scheduler.form.description')" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.monitor.scheduler.triggerName')" path="triggerName">
|
||||
<NInput v-model:value="model.triggerName" :placeholder="$t('page.monitor.scheduler.form.triggerName')" :disabled="isEdit" />
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.monitor.scheduler.triggerGroup')" path="triggerGroup">
|
||||
<NInput v-model:value="model.triggerGroup" :placeholder="$t('page.monitor.scheduler.form.triggerGroup')" :disabled="isEdit" />
|
||||
</NFormItem>
|
||||
<NFormItem span="24" :label="$t('page.monitor.scheduler.triggerData')">
|
||||
<NDynamicInput
|
||||
v-model:value="model.triggerData"
|
||||
preset="pair"
|
||||
:key-placeholder="$t('page.monitor.scheduler.form.triggerDataKey')"
|
||||
:value-placeholder="$t('page.monitor.scheduler.form.triggerDataValue')"
|
||||
>
|
||||
<template #action="{ index, create, remove }">
|
||||
<NSpace class="ml-8px">
|
||||
<NButton size="medium" @click="() => create(index)">
|
||||
<icon-ic:round-plus class="text-icon" />
|
||||
</NButton>
|
||||
<NButton size="medium" @click="() => remove(index)">
|
||||
<icon-ic-round-remove class="text-icon" />
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NDynamicInput>
|
||||
</NFormItem>
|
||||
<NFormItem :label="$t('page.monitor.scheduler.triggerDescription')" path="triggerDescription">
|
||||
<NInput v-model:value="model.triggerDescription" :placeholder="$t('page.monitor.scheduler.form.triggerDescription')" />
|
||||
</NFormItem>
|
||||
<!-- 任务基本信息 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="i-material-symbols:work"></i>
|
||||
任务基本信息
|
||||
</div>
|
||||
|
||||
<NFormItem :label="$t('page.monitor.scheduler.jobName')" path="jobName">
|
||||
<NInput
|
||||
v-model:value="model.jobName"
|
||||
:placeholder="$t('page.monitor.scheduler.form.jobName')"
|
||||
:disabled="isEdit"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:task"></i>
|
||||
</template>
|
||||
</NInput>
|
||||
<div class="help-text" v-if="isEdit">任务名称不可修改</div>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="$t('page.monitor.scheduler.jobGroup')" path="jobGroup">
|
||||
<NInput
|
||||
v-model:value="model.jobGroup"
|
||||
:placeholder="$t('page.monitor.scheduler.form.jobGroup')"
|
||||
:disabled="isEdit"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:folder"></i>
|
||||
</template>
|
||||
</NInput>
|
||||
<div class="help-text" v-if="isEdit">任务组不可修改</div>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="$t('page.monitor.scheduler.jobClassName')" path="jobClassName">
|
||||
<NInput
|
||||
v-model:value="model.jobClassName"
|
||||
:placeholder="$t('page.monitor.scheduler.form.jobClassName')"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:code"></i>
|
||||
</template>
|
||||
</NInput>
|
||||
<div class="help-text">Java类的全限定名</div>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="$t('page.monitor.scheduler.description')" path="description">
|
||||
<NInput
|
||||
v-model:value="model.description"
|
||||
:placeholder="$t('page.monitor.scheduler.form.description')"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
>
|
||||
</NInput>
|
||||
<div class="help-text">任务描述信息</div>
|
||||
</NFormItem>
|
||||
</div>
|
||||
<!-- 执行配置 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="i-material-symbols:schedule"></i>
|
||||
执行配置
|
||||
</div>
|
||||
|
||||
<NFormItem :label="$t('page.monitor.scheduler.cronExpression')" path="cronExpression">
|
||||
<NInput
|
||||
v-model:value="model.cronExpression"
|
||||
:placeholder="$t('page.monitor.scheduler.form.cronExpression')"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:timer"></i>
|
||||
</template>
|
||||
</NInput>
|
||||
<div class="help-text">
|
||||
Cron表达式,例如: 0 0/5 * * * ? (每5分钟执行一次)
|
||||
<a href="https://cron.qqe2.com/" target="_blank" class="cron-help">在线生成器</a>
|
||||
</div>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem span="24" :label="$t('page.monitor.scheduler.jobData')">
|
||||
<div class="dynamic-input-wrapper">
|
||||
<div class="dynamic-input-header">
|
||||
<span class="input-label">任务参数</span>
|
||||
<span class="input-desc">键值对形式的任务参数</span>
|
||||
</div>
|
||||
<NDynamicInput
|
||||
v-model:value="model.jobData"
|
||||
preset="pair"
|
||||
:key-placeholder="$t('page.monitor.scheduler.form.jobDataKey')"
|
||||
:value-placeholder="$t('page.monitor.scheduler.form.jobDataValue')"
|
||||
>
|
||||
<template #action="{ index, create, remove }">
|
||||
<NSpace class="ml-8px">
|
||||
<NButton size="small" type="primary" @click="() => create(index)">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:add"></i>
|
||||
</template>
|
||||
</NButton>
|
||||
<NButton size="small" type="error" @click="() => remove(index)">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:remove"></i>
|
||||
</template>
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NDynamicInput>
|
||||
</div>
|
||||
</NFormItem>
|
||||
</div>
|
||||
<!-- 触发器配置 -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="i-material-symbols:play-arrow"></i>
|
||||
触发器配置
|
||||
</div>
|
||||
|
||||
<NFormItem :label="$t('page.monitor.scheduler.triggerName')" path="triggerName">
|
||||
<NInput
|
||||
v-model:value="model.triggerName"
|
||||
:placeholder="$t('page.monitor.scheduler.form.triggerName')"
|
||||
:disabled="isEdit"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:label"></i>
|
||||
</template>
|
||||
</NInput>
|
||||
<div class="help-text" v-if="isEdit">触发器名称不可修改</div>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="$t('page.monitor.scheduler.triggerGroup')" path="triggerGroup">
|
||||
<NInput
|
||||
v-model:value="model.triggerGroup"
|
||||
:placeholder="$t('page.monitor.scheduler.form.triggerGroup')"
|
||||
:disabled="isEdit"
|
||||
>
|
||||
<template #prefix>
|
||||
<i class="i-material-symbols:folder"></i>
|
||||
</template>
|
||||
</NInput>
|
||||
<div class="help-text" v-if="isEdit">触发器组不可修改</div>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem :label="$t('page.monitor.scheduler.triggerDescription')" path="triggerDescription">
|
||||
<NInput
|
||||
v-model:value="model.triggerDescription"
|
||||
:placeholder="$t('page.monitor.scheduler.form.triggerDescription')"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
>
|
||||
</NInput>
|
||||
<div class="help-text">触发器描述信息</div>
|
||||
</NFormItem>
|
||||
|
||||
<NFormItem span="24" :label="$t('page.monitor.scheduler.triggerData')">
|
||||
<div class="dynamic-input-wrapper">
|
||||
<div class="dynamic-input-header">
|
||||
<span class="input-label">触发器参数</span>
|
||||
<span class="input-desc">触发器的附加参数配置</span>
|
||||
</div>
|
||||
<NDynamicInput
|
||||
v-model:value="model.triggerData"
|
||||
preset="pair"
|
||||
:key-placeholder="$t('page.monitor.scheduler.form.triggerDataKey')"
|
||||
:value-placeholder="$t('page.monitor.scheduler.form.triggerDataValue')"
|
||||
>
|
||||
<template #action="{ index, create, remove }">
|
||||
<NSpace class="ml-8px">
|
||||
<NButton size="small" type="primary" @click="() => create(index)">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:add"></i>
|
||||
</template>
|
||||
</NButton>
|
||||
<NButton size="small" type="error" @click="() => remove(index)">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:remove"></i>
|
||||
</template>
|
||||
</NButton>
|
||||
</NSpace>
|
||||
</template>
|
||||
</NDynamicInput>
|
||||
</div>
|
||||
</NFormItem>
|
||||
</div>
|
||||
</NForm>
|
||||
<template #footer>
|
||||
<NSpace :size="16">
|
||||
<NButton @click="closeDrawer">{{ $t('common.cancel') }}</NButton>
|
||||
<NButton type="primary" @click="handleSubmit">{{ $t('common.confirm') }}</NButton>
|
||||
</NSpace>
|
||||
<div class="drawer-footer">
|
||||
<NButton @click="closeDrawer" class="cancel-btn">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:close"></i>
|
||||
</template>
|
||||
{{ $t('common.cancel') }}
|
||||
</NButton>
|
||||
<NButton type="primary" @click="handleSubmit" class="submit-btn" :loading="submitting">
|
||||
<template #icon>
|
||||
<i class="i-material-symbols:check" v-if="!submitting"></i>
|
||||
</template>
|
||||
{{ !isEdit ? $t('common.add') : $t('common.update') }}
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</NDrawerContent>
|
||||
</NDrawer>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
/* 组件特定样式 - 调度任务特有的样式 */
|
||||
|
||||
/* Cron 帮助链接 */
|
||||
.cron-help {
|
||||
color: #3b82f6;
|
||||
text-decoration: none;
|
||||
margin-left: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cron-help:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 动态输入组件的按钮样式 */
|
||||
:deep(.n-dynamic-input) {
|
||||
.n-button {
|
||||
border-radius: 6px;
|
||||
|
||||
&.n-button--primary-type {
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
}
|
||||
|
||||
&.n-button--error-type {
|
||||
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -148,8 +148,8 @@ async function handleSubmit() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable>
|
||||
<NDrawer v-model:show="visible" display-directive="show" :width="360" class="enhanced-drawer device-theme">
|
||||
<NDrawerContent :title="title" :native-scrollbar="false" closable class="enhanced-drawer device-theme">
|
||||
<NForm ref="formRef" :model="model">
|
||||
<NFormItem :label="$t('page.health.device.message.departmentName')" path="departmentName">
|
||||
<NTreeSelect
|
||||
|
||||
Executable
+405
@@ -0,0 +1,405 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ljwx-boot 批量上传接口 cURL 测试脚本
|
||||
# 测试三个核心接口:upload_health_data, upload_device_info, upload_common_event
|
||||
|
||||
# 默认配置
|
||||
BASE_URL="http://localhost:8080"
|
||||
CONTENT_TYPE="Content-Type: application/json"
|
||||
|
||||
# 颜色定义
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 工具函数
|
||||
log_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
# 测试健康数据上传接口
|
||||
test_upload_health_data() {
|
||||
log_info "测试健康数据上传接口"
|
||||
|
||||
# 生成测试数据
|
||||
local health_data='[
|
||||
{
|
||||
"device_id": "DEVICE_001",
|
||||
"user_id": "101",
|
||||
"org_id": "1",
|
||||
"customer_id": "8",
|
||||
"heart_rate": 75,
|
||||
"blood_oxygen": 98,
|
||||
"temperature": 36.5,
|
||||
"pressure_high": 120,
|
||||
"pressure_low": 80,
|
||||
"stress": 5,
|
||||
"step": 8500,
|
||||
"distance": 6.2,
|
||||
"calorie": 420.5,
|
||||
"latitude": 39.908823,
|
||||
"longitude": 116.397470,
|
||||
"altitude": 45.2,
|
||||
"create_time": "'$(date '+%Y-%m-%d %H:%M:%S')'"
|
||||
},
|
||||
{
|
||||
"device_id": "DEVICE_002",
|
||||
"user_id": "102",
|
||||
"org_id": "1",
|
||||
"customer_id": "8",
|
||||
"heart_rate": 82,
|
||||
"blood_oxygen": 97,
|
||||
"temperature": 36.8,
|
||||
"pressure_high": 125,
|
||||
"pressure_low": 82,
|
||||
"stress": 3,
|
||||
"step": 12000,
|
||||
"distance": 8.5,
|
||||
"calorie": 680.0,
|
||||
"create_time": "'$(date '+%Y-%m-%d %H:%M:%S')'"
|
||||
}
|
||||
]'
|
||||
|
||||
echo "📊 测试新格式接口: /batch/upload-health-data"
|
||||
local response1=$(curl -s -w "HTTPSTATUS:%{http_code}" \
|
||||
-X POST \
|
||||
-H "$CONTENT_TYPE" \
|
||||
-d "$health_data" \
|
||||
"$BASE_URL/batch/upload-health-data")
|
||||
|
||||
local body1=$(echo $response1 | sed -E 's/HTTPSTATUS\:[0-9]{3}$//')
|
||||
local status1=$(echo $response1 | tr -d '\n' | sed -E 's/.*HTTPSTATUS:([0-9]{3})$/\1/')
|
||||
|
||||
if [ "$status1" -eq 200 ]; then
|
||||
log_success "新格式接口测试通过 (HTTP $status1)"
|
||||
echo " 响应: $(echo $body1 | jq -r '.message // .result.message // "处理成功"')"
|
||||
echo " 处理数据: $(echo $body1 | jq -r '.result.processed // "N/A"') 条"
|
||||
else
|
||||
log_error "新格式接口测试失败 (HTTP $status1)"
|
||||
echo " 错误: $body1"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "📊 测试Python兼容接口: /batch/upload_health_data"
|
||||
local response2=$(curl -s -w "HTTPSTATUS:%{http_code}" \
|
||||
-X POST \
|
||||
-H "$CONTENT_TYPE" \
|
||||
-d "$health_data" \
|
||||
"$BASE_URL/batch/upload_health_data")
|
||||
|
||||
local body2=$(echo $response2 | sed -E 's/HTTPSTATUS\:[0-9]{3}$//')
|
||||
local status2=$(echo $response2 | tr -d '\n' | sed -E 's/.*HTTPSTATUS:([0-9]{3})$/\1/')
|
||||
|
||||
if [ "$status2" -eq 200 ]; then
|
||||
log_success "Python兼容接口测试通过 (HTTP $status2)"
|
||||
echo " 响应: $(echo $body2 | jq -r '.message // .result.message // "处理成功"')"
|
||||
else
|
||||
log_error "Python兼容接口测试失败 (HTTP $status2)"
|
||||
echo " 错误: $body2"
|
||||
fi
|
||||
}
|
||||
|
||||
# 测试设备信息上传接口
|
||||
test_upload_device_info() {
|
||||
log_info "测试设备信息上传接口"
|
||||
|
||||
# 生成测试数据
|
||||
local device_data='[
|
||||
{
|
||||
"device_id": "DEVICE_001",
|
||||
"device_name": "智能手环_001",
|
||||
"device_type": "wearable",
|
||||
"firmware_version": "v2.3.1",
|
||||
"battery_level": 85,
|
||||
"signal_strength": -45,
|
||||
"customer_id": "8",
|
||||
"status": "online",
|
||||
"last_sync": "'$(date '+%Y-%m-%d %H:%M:%S')'"
|
||||
},
|
||||
{
|
||||
"device_id": "DEVICE_002",
|
||||
"device_name": "智能手环_002",
|
||||
"device_type": "wearable",
|
||||
"firmware_version": "v2.3.2",
|
||||
"battery_level": 92,
|
||||
"signal_strength": -38,
|
||||
"customer_id": "8",
|
||||
"status": "charging",
|
||||
"last_sync": "'$(date '+%Y-%m-%d %H:%M:%S')'"
|
||||
}
|
||||
]'
|
||||
|
||||
echo "📱 测试新格式接口: /batch/upload-device-info"
|
||||
local response1=$(curl -s -w "HTTPSTATUS:%{http_code}" \
|
||||
-X POST \
|
||||
-H "$CONTENT_TYPE" \
|
||||
-d "$device_data" \
|
||||
"$BASE_URL/batch/upload-device-info")
|
||||
|
||||
local body1=$(echo $response1 | sed -E 's/HTTPSTATUS\:[0-9]{3}$//')
|
||||
local status1=$(echo $response1 | tr -d '\n' | sed -E 's/.*HTTPSTATUS:([0-9]{3})$/\1/')
|
||||
|
||||
if [ "$status1" -eq 200 ]; then
|
||||
log_success "新格式接口测试通过 (HTTP $status1)"
|
||||
echo " 处理设备: $(echo $body1 | jq -r '.result.processed // "N/A"') 个"
|
||||
else
|
||||
log_error "新格式接口测试失败 (HTTP $status1)"
|
||||
echo " 错误: $body1"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "📱 测试Python兼容接口: /batch/upload_device_info"
|
||||
local response2=$(curl -s -w "HTTPSTATUS:%{http_code}" \
|
||||
-X POST \
|
||||
-H "$CONTENT_TYPE" \
|
||||
-d "$device_data" \
|
||||
"$BASE_URL/batch/upload_device_info")
|
||||
|
||||
local body2=$(echo $response2 | sed -E 's/HTTPSTATUS\:[0-9]{3}$//')
|
||||
local status2=$(echo $response2 | tr -d '\n' | sed -E 's/.*HTTPSTATUS:([0-9]{3})$/\1/')
|
||||
|
||||
if [ "$status2" -eq 200 ]; then
|
||||
log_success "Python兼容接口测试通过 (HTTP $status2)"
|
||||
else
|
||||
log_error "Python兼容接口测试失败 (HTTP $status2)"
|
||||
echo " 错误: $body2"
|
||||
fi
|
||||
}
|
||||
|
||||
# 测试通用事件上传接口
|
||||
test_upload_common_event() {
|
||||
log_info "测试通用事件上传接口"
|
||||
|
||||
# 生成测试数据
|
||||
local event_data='{
|
||||
"health_data": [
|
||||
{
|
||||
"device_id": "DEVICE_001",
|
||||
"user_id": "101",
|
||||
"org_id": "1",
|
||||
"customer_id": "8",
|
||||
"heart_rate": 88,
|
||||
"blood_oxygen": 96,
|
||||
"temperature": 37.0,
|
||||
"create_time": "'$(date '+%Y-%m-%d %H:%M:%S')'"
|
||||
}
|
||||
],
|
||||
"device_info": [
|
||||
{
|
||||
"device_id": "DEVICE_001",
|
||||
"device_name": "智能手环_001",
|
||||
"battery_level": 75,
|
||||
"customer_id": "8",
|
||||
"status": "online"
|
||||
}
|
||||
],
|
||||
"alert_data": [
|
||||
{
|
||||
"alert_type": "heart_rate_abnormal",
|
||||
"device_id": "DEVICE_001",
|
||||
"user_id": "101",
|
||||
"severity": "high",
|
||||
"message": "心率异常:检测到心率持续过高",
|
||||
"timestamp": "'$(date '+%Y-%m-%d %H:%M:%S')'"
|
||||
}
|
||||
]
|
||||
}'
|
||||
|
||||
echo "🔄 测试新格式接口: /batch/upload-common-event"
|
||||
local response1=$(curl -s -w "HTTPSTATUS:%{http_code}" \
|
||||
-X POST \
|
||||
-H "$CONTENT_TYPE" \
|
||||
-d "$event_data" \
|
||||
"$BASE_URL/batch/upload-common-event")
|
||||
|
||||
local body1=$(echo $response1 | sed -E 's/HTTPSTATUS\:[0-9]{3}$//')
|
||||
local status1=$(echo $response1 | tr -d '\n' | sed -E 's/.*HTTPSTATUS:([0-9]{3})$/\1/')
|
||||
|
||||
if [ "$status1" -eq 200 ]; then
|
||||
log_success "新格式接口测试通过 (HTTP $status1)"
|
||||
echo " 健康数据处理: $(echo $body1 | jq -r '.result.health_result.processed // "N/A"') 条"
|
||||
echo " 设备信息处理: $(echo $body1 | jq -r '.result.device_result.processed // "N/A"') 条"
|
||||
else
|
||||
log_error "新格式接口测试失败 (HTTP $status1)"
|
||||
echo " 错误: $body1"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "🔄 测试Python兼容接口: /batch/upload_common_event"
|
||||
local response2=$(curl -s -w "HTTPSTATUS:%{http_code}" \
|
||||
-X POST \
|
||||
-H "$CONTENT_TYPE" \
|
||||
-d "$event_data" \
|
||||
"$BASE_URL/batch/upload_common_event")
|
||||
|
||||
local body2=$(echo $response2 | sed -E 's/HTTPSTATUS\:[0-9]{3}$//')
|
||||
local status2=$(echo $response2 | tr -d '\n' | sed -E 's/.*HTTPSTATUS:([0-9]{3})$/\1/')
|
||||
|
||||
if [ "$status2" -eq 200 ]; then
|
||||
log_success "Python兼容接口测试通过 (HTTP $status2)"
|
||||
else
|
||||
log_error "Python兼容接口测试失败 (HTTP $status2)"
|
||||
echo " 错误: $body2"
|
||||
fi
|
||||
}
|
||||
|
||||
# 测试统计信息接口
|
||||
test_get_stats() {
|
||||
log_info "测试统计信息接口"
|
||||
|
||||
local response=$(curl -s -w "HTTPSTATUS:%{http_code}" \
|
||||
-X GET \
|
||||
"$BASE_URL/batch/stats")
|
||||
|
||||
local body=$(echo $response | sed -E 's/HTTPSTATUS\:[0-9]{3}$//')
|
||||
local status=$(echo $response | tr -d '\n' | sed -E 's/.*HTTPSTATUS:([0-9]{3})$/\1/')
|
||||
|
||||
if [ "$status" -eq 200 ]; then
|
||||
log_success "统计信息接口测试通过 (HTTP $status)"
|
||||
echo " 已处理: $(echo $body | jq -r '.result.processed // "N/A"') 条"
|
||||
echo " 批次数: $(echo $body | jq -r '.result.batches // "N/A"')"
|
||||
echo " 错误数: $(echo $body | jq -r '.result.errors // "N/A"')"
|
||||
echo " 重复数: $(echo $body | jq -r '.result.duplicates // "N/A"')"
|
||||
else
|
||||
log_error "统计信息接口测试失败 (HTTP $status)"
|
||||
echo " 错误: $body"
|
||||
fi
|
||||
}
|
||||
|
||||
# 测试健康检查接口
|
||||
test_health_check() {
|
||||
log_info "测试健康检查接口"
|
||||
|
||||
local response=$(curl -s -w "HTTPSTATUS:%{http_code}" \
|
||||
-X GET \
|
||||
"$BASE_URL/batch/health")
|
||||
|
||||
local body=$(echo $response | sed -E 's/HTTPSTATUS\:[0-9]{3}$//')
|
||||
local status=$(echo $response | tr -d '\n' | sed -E 's/.*HTTPSTATUS:([0-9]{3})$/\1/')
|
||||
|
||||
if [ "$status" -eq 200 ]; then
|
||||
log_success "健康检查接口测试通过 (HTTP $status)"
|
||||
echo " 服务状态: $(echo $body | jq -r '.result.status // "N/A"')"
|
||||
echo " 兼容性: $(echo $body | jq -r '.result.features.python_compatibility // "N/A"')"
|
||||
else
|
||||
log_error "健康检查接口测试失败 (HTTP $status)"
|
||||
echo " 错误: $body"
|
||||
fi
|
||||
}
|
||||
|
||||
# 测试性能测试接口
|
||||
test_performance() {
|
||||
local data_size=${1:-100}
|
||||
log_info "测试性能测试接口 (数据规模: $data_size)"
|
||||
|
||||
local response=$(curl -s -w "HTTPSTATUS:%{http_code}" \
|
||||
-X POST \
|
||||
"$BASE_URL/batch/performance-test?dataSize=$data_size")
|
||||
|
||||
local body=$(echo $response | sed -E 's/HTTPSTATUS\:[0-9]{3}$//')
|
||||
local status=$(echo $response | tr -d '\n' | sed -E 's/.*HTTPSTATUS:([0-9]{3})$/\1/')
|
||||
|
||||
if [ "$status" -eq 200 ]; then
|
||||
log_success "性能测试接口测试通过 (HTTP $status)"
|
||||
echo " 测试数据: $(echo $body | jq -r '.result.test_data_size // "N/A"') 条"
|
||||
echo " 总耗时: $(echo $body | jq -r '.result.total_time_ms // "N/A"') ms"
|
||||
echo " QPS: $(echo $body | jq -r '.result.qps // "N/A"')"
|
||||
echo " 性能评级: $(echo $body | jq -r '.result.performance_rating // "N/A"')"
|
||||
else
|
||||
log_error "性能测试接口测试失败 (HTTP $status)"
|
||||
echo " 错误: $body"
|
||||
fi
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
echo "🚀 ljwx-boot 批量上传接口 cURL 测试"
|
||||
echo " 目标服务: $BASE_URL"
|
||||
echo " 测试时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
||||
echo
|
||||
|
||||
# 检查 jq 是否安装
|
||||
if ! command -v jq &> /dev/null; then
|
||||
log_warning "jq 未安装,JSON 解析可能不完整"
|
||||
echo " 建议安装: brew install jq (macOS) 或 apt-get install jq (Ubuntu)"
|
||||
echo
|
||||
fi
|
||||
|
||||
# 解析命令行参数
|
||||
case "${1:-all}" in
|
||||
"health")
|
||||
test_upload_health_data
|
||||
;;
|
||||
"device")
|
||||
test_upload_device_info
|
||||
;;
|
||||
"event")
|
||||
test_upload_common_event
|
||||
;;
|
||||
"stats")
|
||||
test_get_stats
|
||||
;;
|
||||
"check")
|
||||
test_health_check
|
||||
;;
|
||||
"perf")
|
||||
test_performance "${2:-100}"
|
||||
;;
|
||||
"all"|*)
|
||||
# 运行所有测试
|
||||
test_health_check
|
||||
echo
|
||||
test_upload_health_data
|
||||
echo
|
||||
test_upload_device_info
|
||||
echo
|
||||
test_upload_common_event
|
||||
echo
|
||||
test_get_stats
|
||||
echo
|
||||
test_performance 100
|
||||
;;
|
||||
esac
|
||||
|
||||
echo
|
||||
log_info "测试完成"
|
||||
echo
|
||||
echo "使用方法:"
|
||||
echo " $0 [test_type] [params]"
|
||||
echo
|
||||
echo "测试类型:"
|
||||
echo " all - 运行所有测试 (默认)"
|
||||
echo " health - 仅测试健康数据接口"
|
||||
echo " device - 仅测试设备信息接口"
|
||||
echo " event - 仅测试通用事件接口"
|
||||
echo " stats - 仅测试统计信息接口"
|
||||
echo " check - 仅测试健康检查接口"
|
||||
echo " perf - 仅测试性能接口 (可指定数据规模)"
|
||||
echo
|
||||
echo "示例:"
|
||||
echo " $0 health # 测试健康数据接口"
|
||||
echo " $0 perf 1000 # 测试性能接口 (1000条数据)"
|
||||
}
|
||||
|
||||
# 设置BASE_URL(如果提供了参数)
|
||||
if [[ $1 =~ ^https?:// ]]; then
|
||||
BASE_URL="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
# 运行主函数
|
||||
main "$@"
|
||||
@@ -0,0 +1,349 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ljwx-boot 批量上传接口测试脚本
|
||||
|
||||
测试三个核心接口:
|
||||
1. upload_health_data - 健康数据批量上传
|
||||
2. upload_device_info - 设备信息批量上传
|
||||
3. upload_common_event - 通用事件上传
|
||||
|
||||
兼容原 Python ljwx-bigscreen 接口格式
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Any
|
||||
import argparse
|
||||
|
||||
|
||||
class BatchUploadTester:
|
||||
"""批量上传接口测试器"""
|
||||
|
||||
def __init__(self, base_url: str = "http://localhost:8080"):
|
||||
self.base_url = base_url
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'ljwx-batch-upload-tester/1.0'
|
||||
})
|
||||
|
||||
def generate_health_data(self, count: int = 10) -> List[Dict[str, Any]]:
|
||||
"""生成测试健康数据"""
|
||||
health_data = []
|
||||
|
||||
for i in range(count):
|
||||
# 模拟不同设备和用户
|
||||
device_id = f"DEVICE_{i % 5:03d}"
|
||||
user_id = str(100 + (i % 20))
|
||||
org_id = str(1 + (i % 3))
|
||||
|
||||
data = {
|
||||
"device_id": device_id,
|
||||
"user_id": user_id,
|
||||
"org_id": org_id,
|
||||
"customer_id": "8",
|
||||
|
||||
# 健康指标数据
|
||||
"heart_rate": random.randint(60, 100),
|
||||
"blood_oxygen": random.randint(95, 100),
|
||||
"temperature": round(36.0 + random.uniform(0, 2.0), 1),
|
||||
"pressure_high": random.randint(110, 140),
|
||||
"pressure_low": random.randint(70, 90),
|
||||
"stress": random.randint(1, 10),
|
||||
"step": random.randint(1000, 15000),
|
||||
"distance": round(random.uniform(0.5, 10.0), 2),
|
||||
"calorie": round(random.uniform(200, 800), 1),
|
||||
|
||||
# 位置信息
|
||||
"latitude": round(39.9 + random.uniform(-0.1, 0.1), 6),
|
||||
"longitude": round(116.4 + random.uniform(-0.1, 0.1), 6),
|
||||
"altitude": round(random.uniform(40, 60), 1),
|
||||
|
||||
# 时间戳
|
||||
"create_time": (datetime.now() - timedelta(minutes=random.randint(1, 60))).strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
health_data.append(data)
|
||||
|
||||
return health_data
|
||||
|
||||
def generate_device_data(self, count: int = 5) -> List[Dict[str, Any]]:
|
||||
"""生成测试设备数据"""
|
||||
device_data = []
|
||||
|
||||
for i in range(count):
|
||||
data = {
|
||||
"device_id": f"DEVICE_{i:03d}",
|
||||
"device_name": f"智能手环_{i}",
|
||||
"device_type": "wearable",
|
||||
"firmware_version": f"v2.{random.randint(1, 9)}.{random.randint(0, 9)}",
|
||||
"battery_level": random.randint(10, 100),
|
||||
"signal_strength": random.randint(-80, -30),
|
||||
"customer_id": "8",
|
||||
"status": random.choice(["online", "offline", "charging"]),
|
||||
"last_sync": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
device_data.append(data)
|
||||
|
||||
return device_data
|
||||
|
||||
def generate_common_event_data(self) -> Dict[str, Any]:
|
||||
"""生成通用事件测试数据"""
|
||||
return {
|
||||
"health_data": self.generate_health_data(5),
|
||||
"device_info": self.generate_device_data(2),
|
||||
"alert_data": [
|
||||
{
|
||||
"alert_type": "heart_rate_abnormal",
|
||||
"device_id": "DEVICE_001",
|
||||
"user_id": "101",
|
||||
"severity": "high",
|
||||
"message": "心率异常:检测到心率持续过高",
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def test_upload_health_data(self, data_count: int = 10) -> Dict[str, Any]:
|
||||
"""测试健康数据上传接口"""
|
||||
print(f"\n🩺 测试健康数据上传接口 (数据量: {data_count})")
|
||||
|
||||
# 生成测试数据
|
||||
health_data = self.generate_health_data(data_count)
|
||||
|
||||
# 测试新格式接口
|
||||
url = f"{self.base_url}/batch/upload-health-data"
|
||||
result1 = self._make_request("POST", url, health_data, "upload-health-data")
|
||||
|
||||
# 测试Python兼容接口
|
||||
url_compat = f"{self.base_url}/batch/upload_health_data"
|
||||
result2 = self._make_request("POST", url_compat, health_data, "upload_health_data (兼容)")
|
||||
|
||||
return {"modern": result1, "compatible": result2}
|
||||
|
||||
def test_upload_device_info(self, device_count: int = 5) -> Dict[str, Any]:
|
||||
"""测试设备信息上传接口"""
|
||||
print(f"\n📱 测试设备信息上传接口 (设备数: {device_count})")
|
||||
|
||||
# 生成测试数据
|
||||
device_data = self.generate_device_data(device_count)
|
||||
|
||||
# 测试新格式接口
|
||||
url = f"{self.base_url}/batch/upload-device-info"
|
||||
result1 = self._make_request("POST", url, device_data, "upload-device-info")
|
||||
|
||||
# 测试Python兼容接口
|
||||
url_compat = f"{self.base_url}/batch/upload_device_info"
|
||||
result2 = self._make_request("POST", url_compat, device_data, "upload_device_info (兼容)")
|
||||
|
||||
return {"modern": result1, "compatible": result2}
|
||||
|
||||
def test_upload_common_event(self) -> Dict[str, Any]:
|
||||
"""测试通用事件上传接口"""
|
||||
print(f"\n🔄 测试通用事件上传接口")
|
||||
|
||||
# 生成测试数据
|
||||
event_data = self.generate_common_event_data()
|
||||
|
||||
# 测试新格式接口
|
||||
url = f"{self.base_url}/batch/upload-common-event"
|
||||
result1 = self._make_request("POST", url, event_data, "upload-common-event")
|
||||
|
||||
# 测试Python兼容接口
|
||||
url_compat = f"{self.base_url}/batch/upload_common_event"
|
||||
result2 = self._make_request("POST", url_compat, event_data, "upload_common_event (兼容)")
|
||||
|
||||
return {"modern": result1, "compatible": result2}
|
||||
|
||||
def test_get_stats(self) -> Dict[str, Any]:
|
||||
"""测试统计信息接口"""
|
||||
print(f"\n📊 测试统计信息接口")
|
||||
|
||||
url = f"{self.base_url}/batch/stats"
|
||||
return self._make_request("GET", url, None, "stats")
|
||||
|
||||
def test_health_check(self) -> Dict[str, Any]:
|
||||
"""测试健康检查接口"""
|
||||
print(f"\n💚 测试健康检查接口")
|
||||
|
||||
url = f"{self.base_url}/batch/health"
|
||||
return self._make_request("GET", url, None, "health")
|
||||
|
||||
def test_performance(self, data_size: int = 1000) -> Dict[str, Any]:
|
||||
"""测试性能测试接口"""
|
||||
print(f"\n⚡ 测试性能测试接口 (数据规模: {data_size})")
|
||||
|
||||
url = f"{self.base_url}/batch/performance-test?dataSize={data_size}"
|
||||
return self._make_request("POST", url, None, "performance-test")
|
||||
|
||||
def _make_request(self, method: str, url: str, data: Any, operation: str) -> Dict[str, Any]:
|
||||
"""发送HTTP请求"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
if method.upper() == "GET":
|
||||
response = self.session.get(url, timeout=30)
|
||||
else:
|
||||
response = self.session.post(url, json=data, timeout=30)
|
||||
|
||||
elapsed_time = (time.time() - start_time) * 1000
|
||||
|
||||
result = {
|
||||
"operation": operation,
|
||||
"method": method,
|
||||
"url": url,
|
||||
"status_code": response.status_code,
|
||||
"elapsed_ms": round(elapsed_time, 2),
|
||||
"success": response.status_code == 200
|
||||
}
|
||||
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
response_data = response.json()
|
||||
result["response"] = response_data
|
||||
print(f" ✅ {operation}: {response.status_code} ({elapsed_time:.1f}ms)")
|
||||
|
||||
# 显示关键统计信息
|
||||
if "result" in response_data and isinstance(response_data["result"], dict):
|
||||
stats = response_data["result"]
|
||||
if "processed" in stats:
|
||||
print(f" 📈 处理: {stats.get('processed', 0)} 条")
|
||||
if "duplicates" in stats:
|
||||
print(f" 🔄 重复: {stats.get('duplicates', 0)} 条")
|
||||
if "processing_time_ms" in stats:
|
||||
print(f" ⏱️ 耗时: {stats.get('processing_time_ms', 0)} ms")
|
||||
|
||||
except json.JSONDecodeError:
|
||||
result["response"] = response.text
|
||||
print(f" ⚠️ {operation}: {response.status_code} (响应格式异常)")
|
||||
|
||||
else:
|
||||
result["error"] = response.text
|
||||
print(f" ❌ {operation}: {response.status_code} - {response.text[:100]}")
|
||||
|
||||
except Exception as e:
|
||||
elapsed_time = (time.time() - start_time) * 1000
|
||||
result = {
|
||||
"operation": operation,
|
||||
"method": method,
|
||||
"url": url,
|
||||
"error": str(e),
|
||||
"elapsed_ms": round(elapsed_time, 2),
|
||||
"success": False
|
||||
}
|
||||
print(f" 💥 {operation}: 请求异常 - {str(e)}")
|
||||
|
||||
return result
|
||||
|
||||
def run_all_tests(self, health_count: int = 10, device_count: int = 5, perf_size: int = 1000):
|
||||
"""运行所有测试"""
|
||||
print("🚀 开始 ljwx-boot 批量上传接口测试")
|
||||
print(f" 目标服务: {self.base_url}")
|
||||
print(f" 测试时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
results = {}
|
||||
|
||||
# 1. 健康检查
|
||||
results["health_check"] = self.test_health_check()
|
||||
|
||||
# 2. 健康数据上传测试
|
||||
results["health_data"] = self.test_upload_health_data(health_count)
|
||||
|
||||
# 3. 设备信息上传测试
|
||||
results["device_info"] = self.test_upload_device_info(device_count)
|
||||
|
||||
# 4. 通用事件上传测试
|
||||
results["common_event"] = self.test_upload_common_event()
|
||||
|
||||
# 5. 统计信息查询
|
||||
results["stats"] = self.test_get_stats()
|
||||
|
||||
# 6. 性能测试
|
||||
results["performance"] = self.test_performance(perf_size)
|
||||
|
||||
print(f"\n📋 测试完成总结:")
|
||||
self._print_test_summary(results)
|
||||
|
||||
return results
|
||||
|
||||
def _print_test_summary(self, results: Dict[str, Any]):
|
||||
"""打印测试总结"""
|
||||
total_tests = 0
|
||||
passed_tests = 0
|
||||
|
||||
for test_name, test_result in results.items():
|
||||
if isinstance(test_result, dict):
|
||||
if "success" in test_result:
|
||||
# 单个测试结果
|
||||
total_tests += 1
|
||||
if test_result["success"]:
|
||||
passed_tests += 1
|
||||
print(f" ✅ {test_name}: 通过 ({test_result.get('elapsed_ms', 0):.1f}ms)")
|
||||
else:
|
||||
print(f" ❌ {test_name}: 失败")
|
||||
else:
|
||||
# 多个测试结果 (如health_data的modern和compatible)
|
||||
for sub_test, sub_result in test_result.items():
|
||||
if isinstance(sub_result, dict) and "success" in sub_result:
|
||||
total_tests += 1
|
||||
if sub_result["success"]:
|
||||
passed_tests += 1
|
||||
print(f" ✅ {test_name}.{sub_test}: 通过 ({sub_result.get('elapsed_ms', 0):.1f}ms)")
|
||||
else:
|
||||
print(f" ❌ {test_name}.{sub_test}: 失败")
|
||||
|
||||
success_rate = (passed_tests / total_tests * 100) if total_tests > 0 else 0
|
||||
print(f"\n🎯 测试通过率: {passed_tests}/{total_tests} ({success_rate:.1f}%)")
|
||||
|
||||
if success_rate == 100:
|
||||
print("🎉 所有测试通过!接口运行正常")
|
||||
elif success_rate >= 80:
|
||||
print("⚠️ 大部分测试通过,部分功能可能存在问题")
|
||||
else:
|
||||
print("🚨 多个测试失败,建议检查服务状态")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(description="ljwx-boot 批量上传接口测试")
|
||||
parser.add_argument("--url", default="http://localhost:8080", help="服务基础URL")
|
||||
parser.add_argument("--health-count", type=int, default=10, help="健康数据测试条数")
|
||||
parser.add_argument("--device-count", type=int, default=5, help="设备信息测试条数")
|
||||
parser.add_argument("--perf-size", type=int, default=1000, help="性能测试数据规模")
|
||||
|
||||
# 添加单独测试选项
|
||||
parser.add_argument("--test-health", action="store_true", help="仅测试健康数据接口")
|
||||
parser.add_argument("--test-device", action="store_true", help="仅测试设备信息接口")
|
||||
parser.add_argument("--test-event", action="store_true", help="仅测试通用事件接口")
|
||||
parser.add_argument("--test-stats", action="store_true", help="仅测试统计信息接口")
|
||||
parser.add_argument("--test-check", action="store_true", help="仅测试健康检查接口")
|
||||
parser.add_argument("--test-perf", action="store_true", help="仅测试性能接口")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 初始化测试器
|
||||
tester = BatchUploadTester(args.url)
|
||||
|
||||
# 运行指定的测试
|
||||
if args.test_health:
|
||||
tester.test_upload_health_data(args.health_count)
|
||||
elif args.test_device:
|
||||
tester.test_upload_device_info(args.device_count)
|
||||
elif args.test_event:
|
||||
tester.test_upload_common_event()
|
||||
elif args.test_stats:
|
||||
tester.test_get_stats()
|
||||
elif args.test_check:
|
||||
tester.test_health_check()
|
||||
elif args.test_perf:
|
||||
tester.test_performance(args.perf_size)
|
||||
else:
|
||||
# 运行所有测试
|
||||
tester.run_all_tests(args.health_count, args.device_count, args.perf_size)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user