mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-25 21:57:48 +00:00
refactor(module): 重构模块路径和数据库配置
- 将前端API路径从`system`和`monitor`重命名为`module_system`和`module_monitor` - 移除MongoDB相关配置和依赖 - 统一数据库类型命名为`mysql`和`postgres` - 修复代码生成模板中的字段命名问题 - 更新依赖项并移除不必要的包 - 优化数据库连接配置 - 添加新的文档和Redoc视图组件 - 修复SQL脚本中的字段注释
This commit is contained in:
@@ -0,0 +1,808 @@
|
||||
<template>
|
||||
<div class="chatgpt-container">
|
||||
<!-- 主聊天区域 -->
|
||||
<div class="main-chat">
|
||||
<!-- 顶部导航栏 -->
|
||||
<div class="chat-navbar">
|
||||
<div class="navbar-left">
|
||||
<h2>FA智能助手</h2>
|
||||
</div>
|
||||
<div class="navbar-right">
|
||||
<div class="connection-status">
|
||||
<el-icon :class="['status-icon', connectionStatus]">
|
||||
<Connection v-if="connectionStatus === 'connected'" />
|
||||
<Loading v-else-if="connectionStatus === 'connecting'" />
|
||||
<Warning v-else />
|
||||
</el-icon>
|
||||
<span class="status-text">{{ connectionStatusText }}</span>
|
||||
</div>
|
||||
<el-button v-if="messages.length > 0" v-hasPerm="['ai:mcp:clear']" text :icon="Delete" @click="clearCurrentChat">清空对话</el-button>
|
||||
<el-button v-hasPerm="['ai:mcp:connection']" text :icon="Setting" @click="toggleConnection">{{ isConnected ? '断开连接' : '重新连接' }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 聊天消息区域 -->
|
||||
<div ref="messagesContainer" class="chat-messages">
|
||||
<!-- 欢迎界面 -->
|
||||
<div v-if="messages.length === 0" class="welcome-screen">
|
||||
<div class="welcome-content">
|
||||
<div class="ai-logo">
|
||||
<el-icon size="64"><ChatDotRound /></el-icon>
|
||||
</div>
|
||||
<h1>FA智能助手</h1>
|
||||
<p class="welcome-subtitle">我是您的专属AI助手,可以帮您回答问题、处理任务和进行智能对话</p>
|
||||
|
||||
<div class="example-prompts">
|
||||
<div class="prompt-card" @click="setPrompt('请介绍一下FastApiAdmin系统')">
|
||||
<h4>系统介绍</h4>
|
||||
<p>请介绍一下FastApiAdmin系统</p>
|
||||
</div>
|
||||
<div class="prompt-card" @click="setPrompt('如何在系统中创建新的模块?')">
|
||||
<h4>开发指导</h4>
|
||||
<p>如何在系统中创建新的模块?</p>
|
||||
</div>
|
||||
<div class="prompt-card" @click="setPrompt('系统的权限管理是如何工作的?')">
|
||||
<h4>权限管理</h4>
|
||||
<p>FA系统的权限管理是如何工作的?</p>
|
||||
</div>
|
||||
<div class="prompt-card" @click="setPrompt('如何优化FA系统的性能?')">
|
||||
<h4>性能优化</h4>
|
||||
<p>如何优化系统的性能?</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<div v-else class="messages-list">
|
||||
<div
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
:class="['message-group', message.type]"
|
||||
>
|
||||
<div class="message-avatar">
|
||||
<div v-if="message.type === 'user'" class="user-avatar">
|
||||
<el-icon><User /></el-icon>
|
||||
</div>
|
||||
<div v-else class="ai-avatar">
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="message-content">
|
||||
<div class="message-header">
|
||||
<strong class="sender-name">
|
||||
{{ message.type === 'user' ? 'You' : 'FA助手' }}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="message-body">
|
||||
<div v-if="message.type === 'assistant' && message.loading" class="typing-indicator">
|
||||
<div class="typing-dots">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="message-text" v-html="formatMessage(message.content)"></div>
|
||||
</div>
|
||||
<div v-if="!message.loading" class="message-actions">
|
||||
<el-button text size="small" :icon="CopyDocument" @click="copyMessage(message.content)"></el-button>
|
||||
<el-button v-if="message.type === 'assistant'" text size="small" :icon="RefreshLeft"></el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="error" class="error-banner">
|
||||
<el-alert
|
||||
:title="error"
|
||||
type="error"
|
||||
:closable="true"
|
||||
show-icon
|
||||
@close="error = ''"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<div class="chat-input">
|
||||
<div class="input-wrapper">
|
||||
<div class="input-container">
|
||||
<el-input
|
||||
v-model="inputMessage"
|
||||
:placeholder="isConnected ? '向FA助手发送消息...' : '请先连接到服务器'"
|
||||
:disabled="!isConnected || sending"
|
||||
type="textarea"
|
||||
:rows="1"
|
||||
:autosize="{ minRows: 1, maxRows: 6 }"
|
||||
resize="none"
|
||||
class="message-input"
|
||||
@keydown.enter.exact.prevent="sendMessage"
|
||||
@keydown.shift.enter.exact="inputMessage += '\n'"
|
||||
/>
|
||||
<el-button
|
||||
:disabled="!inputMessage.trim() || !isConnected || sending"
|
||||
:loading="sending"
|
||||
class="send-button"
|
||||
type="primary"
|
||||
circle
|
||||
@click="sendMessage"
|
||||
>
|
||||
<el-icon><Promotion /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="input-footer">
|
||||
<span class="input-hint">
|
||||
按 Enter 发送消息,Shift + Enter 换行
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
ChatDotRound,
|
||||
User,
|
||||
Delete,
|
||||
Promotion,
|
||||
Connection,
|
||||
Loading,
|
||||
Warning,
|
||||
Setting,
|
||||
CopyDocument,
|
||||
RefreshLeft
|
||||
} from '@element-plus/icons-vue'
|
||||
|
||||
// 消息接口
|
||||
interface ChatMessage {
|
||||
id: string
|
||||
type: 'user' | 'assistant'
|
||||
content: string
|
||||
timestamp: number
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const messages = ref<ChatMessage[]>([])
|
||||
const inputMessage = ref('')
|
||||
const sending = ref(false)
|
||||
const isConnected = ref(false)
|
||||
const connectionStatus = ref<'connected' | 'connecting' | 'disconnected'>('disconnected')
|
||||
const error = ref('')
|
||||
const messagesContainer = ref<HTMLElement>()
|
||||
|
||||
// WebSocket 连接
|
||||
let ws: WebSocket | null = null
|
||||
const WS_URL = import.meta.env.VITE_APP_WS_ENDPOINT + '/api/v1/application/ai/ws/chat'
|
||||
|
||||
// 计算属性
|
||||
const connectionStatusText = computed(() => {
|
||||
switch (connectionStatus.value) {
|
||||
case 'connected': return '已连接'
|
||||
case 'connecting': return '连接中...'
|
||||
case 'disconnected': return '未连接'
|
||||
default: return '未知状态'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// WebSocket 连接管理
|
||||
const connectWebSocket = () => {
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
return
|
||||
}
|
||||
|
||||
connectionStatus.value = 'connecting'
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
ws = new WebSocket(WS_URL)
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('WebSocket 连接已建立')
|
||||
isConnected.value = true
|
||||
connectionStatus.value = 'connected'
|
||||
ElMessage.success('连接成功')
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
handleWebSocketMessage(data)
|
||||
} catch (err) {
|
||||
console.error('解析消息失败:', err)
|
||||
handleWebSocketMessage({ content: event.data })
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log('WebSocket 连接已关闭', event.code, event.reason)
|
||||
isConnected.value = false
|
||||
connectionStatus.value = 'disconnected'
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('WebSocket 错误:', error)
|
||||
isConnected.value = false
|
||||
connectionStatus.value = 'disconnected'
|
||||
ElMessage.error('连接失败,请检查服务器状态')
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('创建 WebSocket 连接失败:', err)
|
||||
connectionStatus.value = 'disconnected'
|
||||
error.value = '无法创建连接'
|
||||
}
|
||||
}
|
||||
|
||||
// 断开连接
|
||||
const disconnectWebSocket = () => {
|
||||
if (ws) {
|
||||
ws.close(1000, '用户主动断开')
|
||||
ws = null
|
||||
}
|
||||
isConnected.value = false
|
||||
connectionStatus.value = 'disconnected'
|
||||
}
|
||||
|
||||
// 切换连接状态
|
||||
const toggleConnection = () => {
|
||||
if (isConnected.value) {
|
||||
disconnectWebSocket()
|
||||
ElMessage.info('已断开连接')
|
||||
} else {
|
||||
connectWebSocket()
|
||||
}
|
||||
}
|
||||
|
||||
// 处理 WebSocket 消息
|
||||
const handleWebSocketMessage = (data: any) => {
|
||||
// 查找最后一个助手消息
|
||||
const lastMessage = messages.value[messages.value.length - 1]
|
||||
|
||||
if (lastMessage && lastMessage.type === 'assistant' && lastMessage.loading) {
|
||||
// 更新加载中的消息
|
||||
lastMessage.content = data.content || data.message || '收到回复'
|
||||
lastMessage.loading = false
|
||||
} else {
|
||||
// 添加新的助手消息
|
||||
addMessage('assistant', data.content || data.message || '收到回复')
|
||||
}
|
||||
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const sendMessage = async () => {
|
||||
const message = inputMessage.value.trim()
|
||||
if (!message || !isConnected.value || sending.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// 添加用户消息
|
||||
addMessage('user', message)
|
||||
inputMessage.value = ''
|
||||
|
||||
// 添加加载中的助手消息
|
||||
const loadingMessage: ChatMessage = {
|
||||
id: generateId(),
|
||||
type: 'assistant',
|
||||
content: '',
|
||||
timestamp: Date.now(),
|
||||
loading: true
|
||||
}
|
||||
messages.value.push(loadingMessage)
|
||||
|
||||
sending.value = true
|
||||
scrollToBottom()
|
||||
|
||||
try {
|
||||
// 发送消息到 WebSocket
|
||||
if (ws?.readyState === WebSocket.OPEN) {
|
||||
const payload = {
|
||||
message,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
ws.send(JSON.stringify(payload))
|
||||
} else {
|
||||
throw new Error('WebSocket 连接未建立')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('发送消息失败:', err)
|
||||
// 移除加载消息并显示错误
|
||||
messages.value.pop()
|
||||
error.value = '发送消息失败,请检查连接状态'
|
||||
ElMessage.error('发送失败')
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 添加消息
|
||||
const addMessage = (type: 'user' | 'assistant', content: string) => {
|
||||
const message: ChatMessage = {
|
||||
id: generateId(),
|
||||
type,
|
||||
content,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
messages.value.push(message)
|
||||
nextTick(() => scrollToBottom())
|
||||
}
|
||||
|
||||
const clearCurrentChat = async () => {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
'确定要清空当前对话吗?此操作不可恢复。',
|
||||
'确认清空',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
messages.value = []
|
||||
ElMessage.success('对话已清空')
|
||||
} catch {
|
||||
// 用户取消
|
||||
}
|
||||
}
|
||||
|
||||
// 设置提示词
|
||||
const setPrompt = (prompt: string) => {
|
||||
inputMessage.value = prompt
|
||||
}
|
||||
|
||||
// 复制消息
|
||||
const copyMessage = async (content: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(content)
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
} catch {
|
||||
// 降级方案
|
||||
const textArea = document.createElement('textarea')
|
||||
textArea.value = content
|
||||
document.body.appendChild(textArea)
|
||||
textArea.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(textArea)
|
||||
ElMessage.success('已复制到剪贴板')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 滚动到底部
|
||||
const scrollToBottom = () => {
|
||||
nextTick(() => {
|
||||
if (messagesContainer.value) {
|
||||
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 格式化消息内容
|
||||
const formatMessage = (content: string) => {
|
||||
if (!content) return ''
|
||||
|
||||
// 简单的 Markdown 支持
|
||||
return content
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/\*(.*?)\*/g, '<em>$1</em>')
|
||||
.replace(/`(.*?)`/g, '<code>$1</code>')
|
||||
.replace(/\n/g, '<br>')
|
||||
}
|
||||
|
||||
// 生成唯一ID
|
||||
const generateId = () => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
connectWebSocket()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
disconnectWebSocket()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chatgpt-container {
|
||||
display: flex;
|
||||
height: calc(100vh - 120px);
|
||||
background: var(--el-bg-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// 主聊天区域
|
||||
.main-chat {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
.chat-navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
border-bottom: 1px solid var(--el-border-color-light);
|
||||
background: var(--el-bg-color);
|
||||
|
||||
.navbar-left {
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.connection-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
|
||||
.status-icon {
|
||||
&.connected { color: var(--el-color-success); }
|
||||
&.connecting { color: var(--el-color-warning); }
|
||||
&.disconnected { color: var(--el-color-danger); }
|
||||
}
|
||||
|
||||
.status-text {
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: var(--el-bg-color);
|
||||
padding-bottom: 120px; // 为底部输入框留出空间
|
||||
|
||||
// 欢迎界面
|
||||
.welcome-screen {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
|
||||
.welcome-content {
|
||||
max-width: 800px;
|
||||
|
||||
.ai-logo {
|
||||
margin-bottom: 24px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 16px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.welcome-subtitle {
|
||||
font-size: 16px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin-bottom: 32px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.example-prompts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
max-width: 600px;
|
||||
|
||||
.prompt-card {
|
||||
background: var(--el-bg-color-page);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 消息列表
|
||||
.messages-list {
|
||||
padding: 24px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
|
||||
.message-group {
|
||||
margin-bottom: 32px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.message-avatar {
|
||||
flex-shrink: 0;
|
||||
|
||||
.user-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background: var(--el-color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ai-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background: var(--el-color-success);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.message-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.message-header {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.sender-name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.message-body {
|
||||
.message-text {
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
color: var(--el-text-color-primary);
|
||||
word-wrap: break-word;
|
||||
|
||||
:deep(p) {
|
||||
margin: 0 0 12px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(code) {
|
||||
background: var(--el-fill-color-light);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'JetBrains Mono', 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(pre) {
|
||||
background: var(--el-fill-color-light);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
|
||||
code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(strong) {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(em) {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
:deep(ul), :deep(ol) {
|
||||
padding-left: 20px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
:deep(li) {
|
||||
margin: 4px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.typing-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
.typing-dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
span {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--el-text-color-secondary);
|
||||
animation: typing 1.4s infinite;
|
||||
|
||||
&:nth-child(2) { animation-delay: 0.2s; }
|
||||
&:nth-child(3) { animation-delay: 0.4s; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-top: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
.el-button {
|
||||
padding: 4px 8px;
|
||||
min-height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .message-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.error-banner {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
// 输入区域
|
||||
.chat-input {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 16px 24px 24px;
|
||||
background: var(--el-bg-color);
|
||||
border-top: 1px solid var(--el-border-color-light);
|
||||
z-index: 10;
|
||||
|
||||
.input-wrapper {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
|
||||
.input-container {
|
||||
position: relative;
|
||||
background: var(--el-bg-color-page);
|
||||
border: 1px solid var(--el-border-color);
|
||||
border-radius: 12px;
|
||||
transition: border-color 0.2s ease;
|
||||
box-shadow: var(--el-box-shadow-light);
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--el-color-primary);
|
||||
box-shadow: var(--el-box-shadow);
|
||||
}
|
||||
|
||||
.message-input {
|
||||
:deep(.el-textarea__inner) {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
resize: none;
|
||||
padding: 16px 60px 16px 16px;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
|
||||
&:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.send-button {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-height: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.input-footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
|
||||
.input-hint {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes typing {
|
||||
0%, 20% {
|
||||
opacity: 0.4;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
80%, 100% {
|
||||
opacity: 0.4;
|
||||
transform: scale(0.8);
|
||||
}
|
||||
}
|
||||
|
||||
// 滚动条样式
|
||||
.chat-messages::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.chat-messages::-webkit-scrollbar-thumb {
|
||||
background: var(--el-fill-color);
|
||||
border-radius: 3px;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-dark);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,414 @@
|
||||
<!-- 任务日志抽屉 -->
|
||||
<template>
|
||||
<el-drawer v-model="drawerVisible" :title="'【' + props.jobName + '】任务日志'" :size="drawerSize">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery">
|
||||
<el-form-item prop="status" label="执行状态">
|
||||
<el-select v-model="queryFormData.status" placeholder="请选择执行状态" style="width: 167.5px" clearable>
|
||||
<el-option :value="true" label="成功" />
|
||||
<el-option :value="false" label="失败" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 时间范围,收起状态下隐藏 -->
|
||||
<el-form-item v-if="isExpand" prop="start_time" label="执行时间">
|
||||
<DatePicker
|
||||
v-model="dateRange"
|
||||
@update:model-value="handleDateRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button type="primary" icon="search" @click="handleQuery">查询</el-button>
|
||||
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
<el-tooltip content="任务执行日志记录每次定时任务的执行情况,包括成功、失败状态及错误信息。">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
任务执行日志列表
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" icon="delete" @click="handleClearLog">清空日志</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导出">
|
||||
<!-- 将直接导出改为打开导出弹窗 -->
|
||||
<el-button type="warning" icon="download" circle @click="handleOpenExportsModal" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button type="default" icon="refresh" circle @click="handleRefresh" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域:任务日志列表 -->
|
||||
<el-table ref="dataTableRef" v-loading="loading" :data="pageTableData" highlight-current-row class="data-table__content" height="460" border stripe @selection-change="handleSelectionChange">
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column type="index" fixed label="序号" min-width="60">
|
||||
<template #default="scope">
|
||||
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="任务名称" prop="job_name" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="任务组名" prop="job_group" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="执行状态" prop="status" min-width="100" show-overflow-tooltip>
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === true ? 'success' : 'danger'">
|
||||
{{ scope.row.status ? "成功" : "失败" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="执行信息" prop="job_message" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="异常信息" prop="exception_info" min-width="250" show-overflow-tooltip />
|
||||
<el-table-column label="执行器" prop="job_executor" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="调用目标" prop="invoke_target" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column label="位置参数" prop="job_args" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="关键字参数" prop="job_kwargs" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column label="触发器" prop="job_trigger" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column label="创建时间" prop="create_time" min-width="180" sortable />
|
||||
<el-table-column fixed="right" label="操作" align="center" min-width="150">
|
||||
<template #default="scope">
|
||||
<el-button type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
|
||||
<el-button type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<pagination v-model:total="total" v-model:page="queryFormData.page_no" v-model:limit="queryFormData.page_size" @pagination="loadingData" />
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="2" border label-width="120px">
|
||||
<el-descriptions-item label="日志ID" :span="2">{{ detailFormData.id }}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务名称" :span="2">{{ detailFormData.job_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务组名" :span="2">{{ detailFormData.job_group }}</el-descriptions-item>
|
||||
<el-descriptions-item label="执行状态" :span="2">
|
||||
<el-tag :type="detailFormData.status === true ? 'success' : 'danger'">
|
||||
{{ detailFormData.status ? "成功" : "失败" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="执行信息" :span="2">{{ detailFormData.job_message || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="异常信息" :span="2">{{ detailFormData.exception_info || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="执行器" :span="2">{{ detailFormData.job_executor || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="调用目标" :span="2">{{ detailFormData.invoke_target || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="位置参数" :span="2">{{ detailFormData.job_args || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="关键字参数" :span="2">{{ detailFormData.job_kwargs || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="触发器" :span="2">{{ detailFormData.job_trigger || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{ detailFormData.create_time }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 导出弹窗 -->
|
||||
<ExportModal
|
||||
v-model="exportsDialogVisible"
|
||||
:content-config="curdContentConfig"
|
||||
:query-params="queryFormData"
|
||||
:page-data="pageTableData"
|
||||
:selection-data="selectionRows"
|
||||
/>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 添加 props 来接收 jobId 和 jobName
|
||||
const props = defineProps({
|
||||
jobId: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
jobName: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
import JobAPI, { JobLogPageQuery, JobLogTable } from "@/api/module_application/job";
|
||||
import { useAppStore } from "@/store/modules/app.store";
|
||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||
import ExportModal from "@/components/CURD/ExportModal.vue";
|
||||
import type { IContentConfig } from "@/components/CURD/types";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "80%" : "60%"));
|
||||
|
||||
const queryFormRef = ref();
|
||||
const dataTableRef = ref();
|
||||
const total = ref(0);
|
||||
const selectIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
const drawerVisible = ref<boolean>(false);
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<JobLogTable[]>([]);
|
||||
|
||||
// 导出弹窗显示状态 & 选中行
|
||||
const exportsDialogVisible = ref(false);
|
||||
const selectionRows = ref<JobLogTable[]>([]);
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<JobLogTable>({} as JobLogTable);
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<JobLogPageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
status: undefined,
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
job_id: props.jobId,
|
||||
});
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: 'detail',
|
||||
});
|
||||
|
||||
// 日期范围临时变量
|
||||
const dateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange(range: [Date, Date]) {
|
||||
dateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.start_time = formatToDateTime(range[0]);
|
||||
queryFormData.end_time = formatToDateTime(range[1]);
|
||||
} else {
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh () {
|
||||
await loadingData();
|
||||
};
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
// 调用任务日志列表接口
|
||||
const response = await JobAPI.getJobLogList(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
queryFormData.page_no = 1;
|
||||
queryFormData.status = undefined;
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
dateRange.value = [];
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
// 提取有效的数字ID,过滤掉 null/undefined 并转为 number
|
||||
selectIds.value = selection
|
||||
.map((item: any) => item?.id)
|
||||
.filter((id: any) => id !== null && id !== undefined)
|
||||
.map((id: any) => Number(id));
|
||||
// 记录选中行数据供导出弹窗使用
|
||||
selectionRows.value = selection;
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
}
|
||||
|
||||
// 打开详情弹窗
|
||||
async function handleOpenDialog(type: 'detail', id?: number) {
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await JobAPI.getJobLogDetail(id);
|
||||
dialogVisible.title = "任务日志详情";
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
// 如果没有有效ID,直接返回
|
||||
const validIds = ids.filter((id) => id !== null && id !== undefined) as number[];
|
||||
if (validIds.length === 0) return;
|
||||
|
||||
ElMessageBox.confirm("确认删除该任务日志?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await JobAPI.deleteJobLog(validIds);
|
||||
// 删除后刷新并清空选择状态
|
||||
handleResetQuery();
|
||||
selectIds.value = [];
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
async function handleClearLog() {
|
||||
ElMessageBox.confirm("确认清空所有任务日志?此操作不可恢复!", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await JobAPI.clearJobLog();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 打开导出弹窗
|
||||
function handleOpenExportsModal() {
|
||||
exportsDialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 导出字段
|
||||
const exportColumns = [
|
||||
{ prop: 'job_name', label: '任务名称' },
|
||||
{ prop: 'job_group', label: '任务组名' },
|
||||
{ prop: 'status', label: '执行状态' },
|
||||
{ prop: 'job_message', label: '执行信息' },
|
||||
{ prop: 'exception_info', label: '异常信息' },
|
||||
{ prop: 'job_executor', label: '执行器' },
|
||||
{ prop: 'invoke_target', label: '调用目标' },
|
||||
{ prop: 'job_args', label: '位置参数' },
|
||||
{ prop: 'job_kwargs', label: '关键字参数' },
|
||||
{ prop: 'job_trigger', label: '触发器' },
|
||||
{ prop: 'create_time', label: '创建时间' },
|
||||
];
|
||||
|
||||
// 导出配置(用于导出弹窗)
|
||||
const curdContentConfig = {
|
||||
permPrefix: 'application:job_log',
|
||||
cols: exportColumns as any,
|
||||
exportsAction: async (params: any) => {
|
||||
const query: any = { ...params };
|
||||
query.page_no = 1;
|
||||
query.page_size = 1000;
|
||||
const all: any[] = [];
|
||||
while (true) {
|
||||
const res = await JobAPI.getJobLogList(query);
|
||||
const items = res.data?.data?.items || [];
|
||||
const total = res.data?.data?.total || 0;
|
||||
all.push(...items);
|
||||
if (all.length >= total || items.length === 0) break;
|
||||
query.page_no += 1;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
} as unknown as IContentConfig;
|
||||
|
||||
// 打开抽屉
|
||||
function openDrawer() {
|
||||
drawerVisible.value = true;
|
||||
}
|
||||
|
||||
// 关闭抽屉
|
||||
function closeDrawer() {
|
||||
drawerVisible.value = false;
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
openDrawer,
|
||||
closeDrawer
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
// 抽屉打开时会自动加载数据
|
||||
loadingData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,974 @@
|
||||
<!-- 定时任务 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form
|
||||
ref="queryFormRef"
|
||||
:model="queryFormData"
|
||||
:inline="true"
|
||||
label-suffix=":"
|
||||
@submit.prevent="handleQuery"
|
||||
>
|
||||
<el-form-item prop="name" label="任务名称">
|
||||
<el-input
|
||||
v-model="queryFormData.name"
|
||||
placeholder="请输入任务名称"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="status" label="状态">
|
||||
<el-select
|
||||
v-model="queryFormData.status"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
style="width: 167.5px"
|
||||
>
|
||||
<el-option value="true" label="运行中" />
|
||||
<el-option value="false" label="暂停" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 时间范围,收起状态下隐藏 -->
|
||||
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
|
||||
<DatePicker
|
||||
v-model="dateRange"
|
||||
@update:model-value="handleDateRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isExpand" prop="creator" label="创建人">
|
||||
<UserTableSelect
|
||||
v-model="queryFormData.creator"
|
||||
@confirm-click="handleConfirm"
|
||||
@clear-click="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button v-hasPerm="['app:job:query']" type="primary" icon="search" native-type="submit">查询</el-button>
|
||||
<el-button v-hasPerm="['app:job:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link
|
||||
class="ml-3"
|
||||
type="primary"
|
||||
underline="never"
|
||||
@click="isExpand = !isExpand"
|
||||
>
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
<el-tooltip content="定时任务列表">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
定时任务列表
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-hasPerm="['app:job:create']"
|
||||
type="success"
|
||||
icon="plus"
|
||||
@click="handleOpenDialog('create')"
|
||||
>新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-hasPerm="['app:job:delete']"
|
||||
type="danger"
|
||||
icon="delete"
|
||||
:disabled="selectIds.length === 0"
|
||||
@click="handleDelete(selectIds)"
|
||||
>批量删除</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导出">
|
||||
<el-button
|
||||
v-hasPerm="['app:job:export']"
|
||||
type="warning"
|
||||
icon="download"
|
||||
circle
|
||||
@click="handleOpenExportsModal"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="清除">
|
||||
<el-button v-hasPerm="['app:job:clear']" type="danger" icon="delete" circle @click="handleClear"/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button
|
||||
v-hasPerm="['app:job:refresh']"
|
||||
type="primary"
|
||||
icon="refresh"
|
||||
circle
|
||||
@click="handleRefresh"
|
||||
/>
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域:系统配置列表 -->
|
||||
<el-table
|
||||
ref="dataTableRef"
|
||||
v-loading="loading"
|
||||
:data="pageTableData"
|
||||
class="data-table__content"
|
||||
highlight-current-row
|
||||
height="450"
|
||||
border
|
||||
stripe
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column type="selection" align="center" min-width="55" />
|
||||
<el-table-column type="index" label="序号" fixed min-width="60">
|
||||
<template #default="scope">
|
||||
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="任务名称" prop="name" min-width="140" />
|
||||
<el-table-column label="执行函数" prop="func" min-width="140" >
|
||||
<template #default="scope">
|
||||
{{ (dictStore.getDictLabel('sys_job_function',scope.row.func) as any)?.dict_label || scope.row.func }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="触发器" prop="trigger" min-width="100">
|
||||
<template #default="scope">
|
||||
{{ (dictStore.getDictLabel('sys_job_trigger',scope.row.trigger) as any)?.dict_label }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="存储器" prop="jobstore" min-width="120">
|
||||
<template #default="scope">
|
||||
{{ (dictStore.getDictLabel('sys_job_store',scope.row.jobstore) as any)?.dict_label }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="执行器" prop="executor" min-width="100">
|
||||
<template #default="scope">
|
||||
{{ (dictStore.getDictLabel('sys_job_executor',scope.row.executor) as any)?.dict_label }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="并发执行" prop="coalesce" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.coalesce === true ? 'success' : 'danger'">
|
||||
{{ scope.row.coalesce === true ? "是" : "否" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" prop="status" min-width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === true ? 'success' : 'danger'">
|
||||
{{ scope.row.status === true ? "运行中" : "暂停" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="描述" prop="description" min-width="100" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
prop="created_at"
|
||||
min-width="200"
|
||||
sortable
|
||||
/>
|
||||
<el-table-column
|
||||
label="更新时间"
|
||||
prop="updated_at"
|
||||
min-width="200"
|
||||
sortable
|
||||
/>
|
||||
|
||||
<OperationColumn :list-data-length="pageTableData.length">
|
||||
<template #default="scope">
|
||||
<div class="flex">
|
||||
<el-button
|
||||
type="warning"
|
||||
size="small"
|
||||
link
|
||||
icon="document"
|
||||
@click="handleOpenLogDrawer(scope.row.id, scope.row.name)"
|
||||
>
|
||||
日志
|
||||
</el-button>
|
||||
<el-button
|
||||
type="info"
|
||||
size="small"
|
||||
link
|
||||
icon="document"
|
||||
@click="handleOpenDialog('detail', scope.row.id)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['app:job:update']"
|
||||
type="primary"
|
||||
size="small"
|
||||
link
|
||||
icon="edit"
|
||||
@click="handleOpenDialog('update', scope.row.id)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['app:job:delete']"
|
||||
type="danger"
|
||||
size="small"
|
||||
link
|
||||
icon="delete"
|
||||
@click="handleDelete([scope.row.id])"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
<el-dropdown v-hasPerm="['app:job:status']" trigger="click">
|
||||
<el-button type="warning" size="small" link icon="ArrowDown">更多</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
:disabled="scope.row.status === false"
|
||||
icon="Check"
|
||||
@click="handleOption(scope.row.id, 1)"
|
||||
>
|
||||
暂停
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
:disabled="scope.row.status === true"
|
||||
icon="CircleClose"
|
||||
@click="handleOption(scope.row.id, 2)"
|
||||
>
|
||||
恢复
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
</OperationColumn>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<pagination
|
||||
v-model:total="total"
|
||||
v-model:page="queryFormData.page_no"
|
||||
v-model:limit="queryFormData.page_size"
|
||||
@pagination="loadingData"
|
||||
/>
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-dialog
|
||||
v-model="dialogVisible.visible"
|
||||
:title="dialogVisible.title"
|
||||
@close="handleCloseDialog"
|
||||
>
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="4" border>
|
||||
<el-descriptions-item label="序号" :span="2">{{
|
||||
detailFormData.id
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务名称" :span="2">{{
|
||||
detailFormData.name
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="任务函数" :span="2">{{
|
||||
(detailFormData.func ? dictStore.getDictLabel('sys_job_function', detailFormData.func) as any : undefined)?.dict_label || detailFormData.func
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="存储器" :span="2">{{
|
||||
(detailFormData.jobstore ? dictStore.getDictLabel('sys_job_store', detailFormData.jobstore) as any : undefined)?.dict_label || detailFormData.jobstore
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="执行器" :span="2">{{
|
||||
(detailFormData.executor ? dictStore.getDictLabel('sys_job_executor', detailFormData.executor) as any : undefined)?.dict_label || detailFormData.executor
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="触发器" :span="2">{{
|
||||
(detailFormData.trigger ? dictStore.getDictLabel('sys_job_trigger', detailFormData.trigger) as any : undefined)?.dict_label || detailFormData.trigger
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="位置参数" :span="2">{{
|
||||
detailFormData.args
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="关键字参数" :span="2">{{
|
||||
detailFormData.kwargs
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="并发执行" :span="2">
|
||||
<el-tag :type="detailFormData.coalesce ? 'success' : 'danger'">
|
||||
{{ detailFormData.coalesce ? "是" : "否" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态" :span="2">
|
||||
<el-tag :type="detailFormData.status ? 'success' : 'danger'">
|
||||
{{ detailFormData.status ? "运行中" : "暂停" }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最大实例数" :span="2">{{
|
||||
detailFormData.max_instances
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="触发器参数" :span="2">{{
|
||||
detailFormData.trigger_args
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="开始时间" :span="2">{{
|
||||
detailFormData.start_date
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="结束时间" :span="2">{{
|
||||
detailFormData.end_date
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人" :span="2">{{
|
||||
detailFormData.creator?.name
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">{{
|
||||
detailFormData.created_at
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">{{
|
||||
detailFormData.updated_at
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="4">{{
|
||||
detailFormData.description
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<!-- 新增、编辑表单 -->
|
||||
<template v-else>
|
||||
<el-form
|
||||
ref="dataFormRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-suffix=":"
|
||||
label-width="auto"
|
||||
inline
|
||||
>
|
||||
<el-form-item label="任务名称" prop="name" style="width: 40%">
|
||||
<el-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入任务名称"
|
||||
:maxlength="50"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务函数" prop="func" style="width: 40%">
|
||||
<el-select v-model="formData.func" placeholder="请选择任务函数">
|
||||
<el-option
|
||||
v-for="item in dictStore.getDictArray('sys_job_function')"
|
||||
:key="item.dict_value"
|
||||
:label="item.dict_label"
|
||||
:value="item.dict_value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="存储器" prop="jobstore" style="width: 40%">
|
||||
<el-select v-model="formData.jobstore" placeholder="请选择存储器">
|
||||
<el-option
|
||||
v-for="item in dictStore.getDictArray('sys_job_store')"
|
||||
:key="item.dict_value"
|
||||
:label="item.dict_label"
|
||||
:value="item.dict_value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="执行器" prop="executor" style="width: 40%">
|
||||
<el-select v-model="formData.executor" placeholder="请选择执行器">
|
||||
<el-option
|
||||
v-for="item in dictStore.getDictArray('sys_job_executor')"
|
||||
:key="item.dict_value"
|
||||
:label="item.dict_label"
|
||||
:value="item.dict_value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="位置参数" prop="args" style="width: 40%">
|
||||
<el-input
|
||||
v-model="formData.args"
|
||||
placeholder="请输入位置参数"
|
||||
:maxlength="50"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="关键字参数" prop="kwargs" style="width: 40%">
|
||||
<el-input
|
||||
v-model="formData.kwargs"
|
||||
placeholder="请输入关键字参数"
|
||||
:maxlength="50"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="并发执行" prop="coalesce" style="width: 40%">
|
||||
<el-radio-group v-model="formData.coalesce">
|
||||
<el-radio :value="true">是</el-radio>
|
||||
<el-radio :value="false">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="最大实例数"
|
||||
prop="max_instances"
|
||||
style="width: 40%"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="formData.max_instances"
|
||||
controls-position="right"
|
||||
:min="1"
|
||||
:max="10"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="触发器" prop="trigger" style="width: 40%">
|
||||
<el-select v-model="formData.trigger" placeholder="请选择触发器">
|
||||
<el-option
|
||||
v-for="item in dictStore.getDictArray('sys_job_trigger')"
|
||||
:key="item.dict_value"
|
||||
:label="item.dict_label"
|
||||
:value="item.dict_value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 运行日期、间隔时间或 Cron 表达式 -->
|
||||
<el-form-item
|
||||
v-if="formData.trigger === 'date'"
|
||||
label="运行日期"
|
||||
prop="trigger_args"
|
||||
:rules="[{ required: true, message: '请选择运行日期' }]"
|
||||
style="width: 40%"
|
||||
>
|
||||
<el-date-picker
|
||||
v-model="formData.trigger_args"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择运行日期"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-else-if="formData.trigger === 'interval'"
|
||||
label="间隔时间"
|
||||
prop="trigger_args"
|
||||
:rules="[
|
||||
{ required: true, message: '请输入间隔时间', trigger: 'change' },
|
||||
]"
|
||||
style="width: 40%"
|
||||
>
|
||||
<el-popover
|
||||
:visible="openIntervalTab"
|
||||
width="600px"
|
||||
trigger="click"
|
||||
:persistent="false"
|
||||
>
|
||||
<template #reference>
|
||||
<el-input
|
||||
v-model="formData.trigger_args"
|
||||
placeholder="请输入 秒-分-时-天-周"
|
||||
readonly
|
||||
@click="openIntervalTab = true"
|
||||
/>
|
||||
</template>
|
||||
<IntervalTab
|
||||
ref="intervalTabRef"
|
||||
:cron-value="formData.trigger_args"
|
||||
@confirm="handleIntervalConfirm"
|
||||
@cancel="openIntervalTab = false"
|
||||
/>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-else-if="formData.trigger === 'cron'"
|
||||
label="Cron表达式"
|
||||
prop="trigger_args"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: '请输入Cron表达式',
|
||||
trigger: 'change',
|
||||
},
|
||||
]"
|
||||
style="width: 40%"
|
||||
>
|
||||
<el-popover
|
||||
:visible="openCron"
|
||||
width="600px"
|
||||
trigger="click"
|
||||
:persistent="false"
|
||||
placement="left"
|
||||
>
|
||||
<template #reference>
|
||||
<el-input
|
||||
v-model="formData.trigger_args"
|
||||
placeholder="请输入 * * * * * ? *"
|
||||
readonly
|
||||
@click="openCron = true"
|
||||
/>
|
||||
</template>
|
||||
<vue3CronPlus
|
||||
i18n="cn"
|
||||
max-height="500px"
|
||||
@change="handlechangeCron"
|
||||
@close="openCron = false"
|
||||
/>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
<!-- 开始日期和结束日期 -->
|
||||
<el-form-item
|
||||
v-if="formData.trigger && formData.trigger != 'date'"
|
||||
label="开始日期"
|
||||
prop="start_date"
|
||||
:rules="[
|
||||
{ required: false, message: '请选择开始日期', trigger: 'blur' },
|
||||
]"
|
||||
style="width: 40%"
|
||||
>
|
||||
<el-date-picker
|
||||
v-model="formData.start_date"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择开始日期"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="formData.trigger && formData.trigger != 'date'"
|
||||
label="结束日期"
|
||||
prop="end_date"
|
||||
:rules="[
|
||||
{ required: false, message: '请选择结束日期', trigger: 'blur' },
|
||||
]"
|
||||
style="width: 40%"
|
||||
>
|
||||
<el-date-picker
|
||||
v-model="formData.end_date"
|
||||
type="datetime"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="请选择结束日期"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="描述" prop="description" style="width: 85%">
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
:rows="4"
|
||||
:maxlength="100"
|
||||
show-word-limit
|
||||
type="textarea"
|
||||
placeholder="请输入描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
<el-button v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button v-else v-hasPerm="['app:job:detail']" type="primary" @click="handleCloseDialog" >确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<JobLogDrawer v-if="drawerVisible" v-model="drawerVisible" :job-id="currentJobId" :job-name="currentJobName" />
|
||||
<ExportModal
|
||||
v-model="exportsDialogVisible"
|
||||
:content-config="curdContentConfig"
|
||||
:query-params="queryFormData"
|
||||
:page-data="pageTableData"
|
||||
:selection-data="selectionRows"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineOptions({
|
||||
name: "Job",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import JobAPI, { JobTable, JobForm, JobPageQuery } from "@/api/module_application/job";
|
||||
import IntervalTab from "@/components/IntervalTab/index.vue";
|
||||
import { useDictStore } from "@/store/index";
|
||||
import { vue3CronPlus } from "vue3-cron-plus";
|
||||
import "vue3-cron-plus/dist/index.css"; // 引入样式
|
||||
import JobLogDrawer from "@/views/module_application/job/components/JobLogDrawer.vue"
|
||||
import OperationColumn from "@/components/OperationColumn/index.vue";
|
||||
import ExportModal from "@/components/CURD/ExportModal.vue";
|
||||
import type { IContentConfig } from "@/components/CURD/types";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
const dictStore = useDictStore();
|
||||
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
const total = ref(0);
|
||||
const selectIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
|
||||
const openCron = ref(false);
|
||||
|
||||
const openIntervalTab = ref(false);
|
||||
const intervalTabRef = ref();
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<JobTable[]>([]);
|
||||
|
||||
// 导出弹窗显示状态 & 选中行
|
||||
const exportsDialogVisible = ref(false);
|
||||
const selectionRows = ref<JobTable[]>([]);
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<JobTable>({} as JobTable);
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<JobPageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
name: undefined,
|
||||
status: undefined,
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
// 创建人
|
||||
creator: undefined,
|
||||
});
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive<JobForm>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
func: undefined,
|
||||
trigger: undefined,
|
||||
args: undefined,
|
||||
kwargs: undefined,
|
||||
coalesce: false,
|
||||
max_instances: 1,
|
||||
jobstore: undefined,
|
||||
executor: undefined,
|
||||
trigger_args: undefined,
|
||||
start_date: undefined,
|
||||
end_date: undefined,
|
||||
status: undefined,
|
||||
description: undefined,
|
||||
});
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: "create" as "create" | "update" | "detail" | "log",
|
||||
});
|
||||
|
||||
// 抽屉显隐
|
||||
const drawerVisible = ref(false);
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: "请输入任务名称", trigger: "blur" }],
|
||||
func: [{ required: true, message: "请输入执行函数", trigger: "blur" }],
|
||||
trigger: [{ required: true, message: "请选择触发器", trigger: "blur" }],
|
||||
coalesce: [{ required: true, message: "请选择并发执行", trigger: "blur" }],
|
||||
jobstore: [{ required: true, message: "请选择存储器", trigger: "blur" }],
|
||||
executor: [{ required: true, message: "请选择执行器", trigger: "blur" }],
|
||||
});
|
||||
|
||||
|
||||
// 日期范围临时变量
|
||||
const dateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange(range: [Date, Date]) {
|
||||
dateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.start_time = formatToDateTime(range[0]);
|
||||
queryFormData.end_time = formatToDateTime(range[1]);
|
||||
} else {
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh() {
|
||||
await loadingData();
|
||||
}
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await JobAPI.getJobList(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 选择创建人后触发查询
|
||||
function handleConfirm() {
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
queryFormData.page_no = 1;
|
||||
// 额外清空日期范围与时间查询参数
|
||||
dateRange.value = [];
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 定义初始表单数据常量
|
||||
const initialFormData: JobForm = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
func: undefined,
|
||||
trigger: undefined,
|
||||
args: undefined,
|
||||
kwargs: undefined,
|
||||
coalesce: false,
|
||||
max_instances: 1,
|
||||
jobstore: undefined,
|
||||
executor: undefined,
|
||||
trigger_args: undefined,
|
||||
start_date: undefined,
|
||||
end_date: undefined,
|
||||
status: undefined,
|
||||
description: undefined,
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
// 完全重置 formData 为初始状态
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
selectIds.value = selection.map((item: any) => item.id);
|
||||
selectionRows.value = selection;
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
async function handleOpenDialog(
|
||||
type: "create" | "update" | "detail",
|
||||
id?: number
|
||||
) {
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await JobAPI.getJobDetail(id);
|
||||
if (type === "detail") {
|
||||
dialogVisible.title = "任务详情";
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
} else if (type === "update") {
|
||||
dialogVisible.title = "修改任务";
|
||||
Object.assign(formData, response.data.data);
|
||||
}
|
||||
} else {
|
||||
dialogVisible.title = "新增任务";
|
||||
formData.id = undefined;
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 提交表单(防抖)
|
||||
async function handleSubmit() {
|
||||
// 表单校验
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
|
||||
const id = formData.id;
|
||||
if (id) {
|
||||
try {
|
||||
await JobAPI.updateJob(id, formData);
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await JobAPI.createJob(formData);
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await JobAPI.deleteJob(ids);
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 打开导出弹窗
|
||||
async function handleOpenExportsModal() {
|
||||
exportsDialogVisible.value = true;
|
||||
}
|
||||
|
||||
function handleIntervalConfirm(interval: string) {
|
||||
formData.trigger_args = interval;
|
||||
openIntervalTab.value = false;
|
||||
}
|
||||
|
||||
const handlechangeCron = (cronStr: string) => {
|
||||
// formData.trigger_args = cronStr;
|
||||
if (typeof cronStr == "string") {
|
||||
formData.trigger_args = cronStr;
|
||||
}
|
||||
};
|
||||
|
||||
// 清空按钮操作
|
||||
const handleClear = () => {
|
||||
ElMessageBox.confirm("是否确认清空所有定时任务数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
})
|
||||
.then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await JobAPI.clearJob();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
};
|
||||
|
||||
// 操作按钮:操作类型 1: 暂停 2: 恢复 3: 重启(暂时移除重启)
|
||||
const handleOption = (id: number, option: number) => {
|
||||
JobAPI.OptionJob({ id, option }).then(() => {
|
||||
loadingData();
|
||||
});
|
||||
};
|
||||
|
||||
const currentJobId = ref<number>(0);
|
||||
const currentJobName = ref<string>("");
|
||||
|
||||
function handleOpenLogDrawer(jobId: number, jobName: string) {
|
||||
currentJobId.value = jobId;
|
||||
currentJobName.value = jobName;
|
||||
drawerVisible.value = true;
|
||||
}
|
||||
|
||||
// 导出字段
|
||||
const exportColumns = [
|
||||
{ prop: 'name', label: '任务名称' },
|
||||
{ prop: 'func', label: '执行函数' },
|
||||
{ prop: 'trigger', label: '触发器' },
|
||||
{ prop: 'jobstore', label: '存储器' },
|
||||
{ prop: 'executor', label: '执行器' },
|
||||
{ prop: 'coalesce', label: '并发执行' },
|
||||
{ prop: 'status', label: '状态' },
|
||||
{ prop: 'description', label: '描述' },
|
||||
{ prop: 'created_at', label: '创建时间' },
|
||||
{ prop: 'updated_at', label: '更新时间' },
|
||||
];
|
||||
|
||||
// 导出配置(用于导出弹窗)
|
||||
const curdContentConfig = {
|
||||
permPrefix: 'app:job',
|
||||
cols: exportColumns as any,
|
||||
exportsAction: async (params: any) => {
|
||||
const query: any = { ...params };
|
||||
if (typeof query.status === 'string') query.status = query.status === 'true';
|
||||
query.page_no = 1;
|
||||
query.page_size = 1000;
|
||||
const all: any[] = [];
|
||||
while (true) {
|
||||
const res = await JobAPI.getJobList(query);
|
||||
const items = res.data?.data?.items || [];
|
||||
const total = res.data?.data?.total || 0;
|
||||
all.push(...items);
|
||||
if (all.length >= total || items.length === 0) break;
|
||||
query.page_no += 1;
|
||||
}
|
||||
return all;
|
||||
},
|
||||
} as unknown as IContentConfig;
|
||||
|
||||
onMounted(async () => {
|
||||
// 加载字典数据
|
||||
await dictStore.getDict(['sys_job_function','sys_job_executor','sys_job_store', 'sys_job_trigger']);
|
||||
// 加载表格数据
|
||||
loadingData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<!-- 内部应用展示页面 -->
|
||||
<template>
|
||||
<div class="internal-app-container">
|
||||
<div class="internal-app-content">
|
||||
<iframe
|
||||
ref="iframeRef"
|
||||
:src="appUrl"
|
||||
class="internal-app-iframe"
|
||||
frameborder="0"
|
||||
allowfullscreen
|
||||
@load="handleIframeLoad"
|
||||
></iframe>
|
||||
<div v-if="loading" class="loading-overlay">
|
||||
<el-icon class="loading-icon">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router';
|
||||
import { Loading } from '@element-plus/icons-vue';
|
||||
import { useTagsViewStore } from '@/store';
|
||||
|
||||
defineOptions({
|
||||
name: "InternalApp",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const route = useRoute();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
const iframeRef = ref<HTMLIFrameElement>();
|
||||
const loading = ref(true);
|
||||
|
||||
// 从路由参数获取应用信息
|
||||
const appUrl = computed(() => route.query.url as string);
|
||||
const appName = computed(() => route.query.appName as string);
|
||||
|
||||
// iframe加载完成
|
||||
function handleIframeLoad() {
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
// 监听路由变化,更新iframe
|
||||
watch(() => route.query.url, (newUrl) => {
|
||||
if (newUrl && iframeRef.value) {
|
||||
loading.value = true;
|
||||
iframeRef.value.src = newUrl as string;
|
||||
}
|
||||
});
|
||||
|
||||
// 在组件挂载时设置标签标题
|
||||
onMounted(() => {
|
||||
if (appName.value) {
|
||||
// 查找当前标签并更新标题
|
||||
nextTick(() => {
|
||||
const currentTag = tagsViewStore.visitedViews.find(tag => tag.path === route.path);
|
||||
if (currentTag && currentTag.title !== appName.value) {
|
||||
tagsViewStore.updateVisitedView({
|
||||
...currentTag,
|
||||
title: appName.value,
|
||||
fullPath: route.fullPath,
|
||||
query: route.query,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 监听应用名称变化,更新标签标题
|
||||
watch(() => appName.value, (newAppName) => {
|
||||
if (newAppName) {
|
||||
const currentTag = tagsViewStore.visitedViews.find(tag => tag.path === route.path);
|
||||
if (currentTag) {
|
||||
tagsViewStore.updateVisitedView({
|
||||
...currentTag,
|
||||
title: newAppName,
|
||||
fullPath: route.fullPath,
|
||||
query: route.query,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.internal-app-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.internal-app-content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.internal-app-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--el-bg-color);
|
||||
gap: 12px;
|
||||
|
||||
.loading-icon {
|
||||
font-size: 24px;
|
||||
color: var(--el-color-primary);
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
span {
|
||||
color: var(--el-text-color-regular);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,624 @@
|
||||
<!-- 我的应用管理 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 顶部搜索和操作区域 -->
|
||||
<div class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery">
|
||||
<el-form-item prop="name" label="应用名称">
|
||||
<el-input v-model="queryFormData.name" placeholder="请输入应用名称" clearable/>
|
||||
</el-form-item>
|
||||
<el-form-item prop="status" label="状态">
|
||||
<el-select v-model="queryFormData.status" placeholder="请选择状态" clearable style="width: 170px;">
|
||||
<el-option label="启用" :value="true" />
|
||||
<el-option label="停用" :value="false" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="isExpand" prop="creator" label="创建人">
|
||||
<UserTableSelect
|
||||
v-model="queryFormData.creator"
|
||||
@confirm-click="handleConfirm"
|
||||
@clear-click="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button v-hasPerm="['application:myapp:query']" type="primary" icon="search" native-type="submit">查询</el-button>
|
||||
<el-button v-hasPerm="['application:myapp:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 应用卡片展示区域 -->
|
||||
<el-card shadow="hover" class="app-grid-card" >
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>应用市场</span>
|
||||
<el-button v-hasPerm="['application:myapp:create']" type="primary" icon="plus" @click="handleCreateApp">
|
||||
创建应用
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 应用网格 -->
|
||||
<div v-loading="loading" class="app-grid" >
|
||||
<el-card
|
||||
v-for="app in applicationList"
|
||||
:key="app.id"
|
||||
class="app-card-el"
|
||||
:body-style="{ padding: '12px' }"
|
||||
shadow="hover"
|
||||
>
|
||||
<template #header>
|
||||
<div class="app-card-header-el">
|
||||
<div class="app-info-header">
|
||||
<el-avatar
|
||||
:size="40"
|
||||
:src="app.icon_url"
|
||||
class="app-avatar-el"
|
||||
>
|
||||
<el-icon size="20"><Monitor /></el-icon>
|
||||
</el-avatar>
|
||||
<div class="app-title-section">
|
||||
<h3 class="app-name-el">{{ app.name }}</h3>
|
||||
<el-tag
|
||||
:type="app.status ? 'success' : 'danger'"
|
||||
size="small"
|
||||
class="app-status-tag"
|
||||
>
|
||||
{{ app.status ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
<el-dropdown
|
||||
v-hasPerm="['application:myapp:update']"
|
||||
trigger="click"
|
||||
@command="(command) => handleAppAction(command, app)"
|
||||
>
|
||||
<el-button
|
||||
type="text"
|
||||
icon="MoreFilled"
|
||||
size="small"
|
||||
class="app-menu-btn"
|
||||
/>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="edit" icon="Edit">编辑</el-dropdown-item>
|
||||
<el-dropdown-item command="delete" icon="Delete" divided>删除</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="app-card-content">
|
||||
<p class="app-description">{{ app.description || '暂无描述' }}</p>
|
||||
|
||||
<div class="app-meta-info">
|
||||
<div class="app-meta-row">
|
||||
<div class="meta-item left">
|
||||
<el-icon size="14" class="meta-icon"><User /></el-icon>
|
||||
<span>{{ app.creator?.name || '未知' }}</span>
|
||||
</div>
|
||||
<div class="meta-item right">
|
||||
<el-icon size="14" class="meta-icon"><Clock /></el-icon>
|
||||
<span>{{ formatTime(app.created_at) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-card-actions">
|
||||
<el-button
|
||||
v-hasPerm="['application:myapp:open_external']"
|
||||
type="primary"
|
||||
icon="Link"
|
||||
size="small"
|
||||
:disabled="!app.status"
|
||||
class="action-btn"
|
||||
@click="openAppExternal(app.access_url)"
|
||||
>
|
||||
外部打开
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPerm="['application:myapp:open_internal']"
|
||||
type="default"
|
||||
icon="View"
|
||||
size="small"
|
||||
:disabled="!app.status"
|
||||
class="action-btn"
|
||||
@click="openAppInternal(app)"
|
||||
>
|
||||
内部打开
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="applicationList.length === 0 && !loading" >
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</div>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<!-- 使用卡片 footer 样式右对齐,无需额外容器 -->
|
||||
<pagination
|
||||
v-model:total="total"
|
||||
v-model:page="queryFormData.page_no"
|
||||
v-model:limit="queryFormData.page_size"
|
||||
:page-sizes="[12, 24, 48]"
|
||||
@pagination="loadApplicationList"
|
||||
/>
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 应用创建/编辑弹窗 -->
|
||||
<el-drawer
|
||||
v-model="dialogVisible"
|
||||
:title="dialogTitle"
|
||||
:size="drawerSize"
|
||||
direction="rtl"
|
||||
@close="handleCloseDialog"
|
||||
>
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
label-position="right"
|
||||
>
|
||||
<el-form-item label="应用名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入应用名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="访问地址" prop="access_url">
|
||||
<el-input v-model="formData.access_url" placeholder="请输入访问地址" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="图标地址" prop="icon_url">
|
||||
<el-input v-model="formData.icon_url" placeholder="请输入图标地址" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="应用状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="true">启用</el-radio>
|
||||
<el-radio :value="false">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="应用描述" prop="description">
|
||||
<el-input
|
||||
v-model="formData.description"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入应用描述"
|
||||
maxlength="200"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "MyApplication",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { useAppStore } from "@/store/modules/app.store";
|
||||
import { useTagsViewStore } from "@/store";
|
||||
import { useRouter } from "vue-router";
|
||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||
import { Monitor, User, Clock } from '@element-plus/icons-vue';
|
||||
import ApplicationAPI, { type ApplicationForm, type ApplicationInfo, type ApplicationPageQuery } from "@/api/module_application/myapp";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
const appStore = useAppStore();
|
||||
const tagsViewStore = useTagsViewStore();
|
||||
const router = useRouter();
|
||||
|
||||
// 响应式数据
|
||||
const queryFormRef = ref();
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const total = ref(0);
|
||||
const dialogVisible = ref(false);
|
||||
const dialogType = ref<'create' | 'edit'>('create');
|
||||
const currentApp = ref<ApplicationInfo | null>(null);
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<ApplicationPageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 12,
|
||||
name: undefined,
|
||||
status: undefined,
|
||||
creator: undefined,
|
||||
});
|
||||
|
||||
// 应用列表数据
|
||||
const applicationList = ref<ApplicationInfo[]>([]);
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive<ApplicationForm>({
|
||||
name: '',
|
||||
access_url: '',
|
||||
icon_url: '',
|
||||
status: true,
|
||||
description: '',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = reactive({
|
||||
name: [
|
||||
{ required: true, message: "请输入应用名称", trigger: "blur" },
|
||||
{ min: 2, max: 30, message: "长度在 2 到 30 个字符", trigger: "blur" }],
|
||||
access_url: [
|
||||
{ required: true, message: "请输入访问地址", trigger: "blur" },
|
||||
{ type: 'url' as const, message: "请输入正确的URL格式", trigger: "blur" }
|
||||
],
|
||||
icon_url: [
|
||||
{ required: true, message: "请输入图标地址", trigger: "blur" },
|
||||
{ type: 'url' as const, message: "请输入正确的URL格式", trigger: "blur" }
|
||||
],
|
||||
status: [{ required: true, message: "请选择应用状态", trigger: "change" }],
|
||||
});
|
||||
|
||||
// 计算属性
|
||||
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "500px" : "90%"));
|
||||
const dialogTitle = computed(() => dialogType.value === 'create' ? '创建应用' : '编辑应用');
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (time: string | undefined) => {
|
||||
if (!time) return '未知';
|
||||
return formatToDateTime(time, 'YYYY-MM-DD HH:mm:ss');
|
||||
};
|
||||
|
||||
// 加载应用列表
|
||||
async function loadApplicationList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await ApplicationAPI.getApplicationList(queryFormData);
|
||||
applicationList.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
} catch (error) {
|
||||
console.error('加载应用列表失败:', error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
await loadApplicationList();
|
||||
}
|
||||
|
||||
// 选择创建人后触发查询
|
||||
function handleConfirm() {
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryFormData.page_no = 1;
|
||||
await loadApplicationList();
|
||||
}
|
||||
|
||||
// 创建应用
|
||||
function handleCreateApp() {
|
||||
console.log('handleCreateApp');
|
||||
dialogType.value = 'create';
|
||||
resetForm();
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 编辑应用
|
||||
function handleEditApp(app: ApplicationInfo) {
|
||||
dialogType.value = 'edit';
|
||||
currentApp.value = app;
|
||||
Object.assign(formData, app);
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
|
||||
// 删除应用
|
||||
async function handleDeleteApp(app: ApplicationInfo) {
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该应用?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
|
||||
await ApplicationAPI.deleteApplication([app.id!]);
|
||||
await loadApplicationList();
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除应用失败:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应用操作
|
||||
async function handleAppAction(command: string, app: ApplicationInfo) {
|
||||
switch (command) {
|
||||
case 'edit':
|
||||
handleEditApp(app);
|
||||
break;
|
||||
case 'delete':
|
||||
await handleDeleteApp(app);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 外部打开应用
|
||||
function openAppExternal(url: string | undefined) {
|
||||
if (url) {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
// 内部打开应用
|
||||
function openAppInternal(app: ApplicationInfo) {
|
||||
if (!app.status) {
|
||||
ElMessage.warning('应用已停用,无法打开');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!app.access_url) {
|
||||
ElMessage.warning('应用访问地址不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建一个动态路由路径
|
||||
const appPath = `/internal-app/${app.id}`;
|
||||
const appName = `InternalApp${app.id}`;
|
||||
const appTitle = app.name || '未命名应用';
|
||||
|
||||
// 先导航到路由,这样可以动态设置路由的meta信息
|
||||
router.push({
|
||||
path: appPath,
|
||||
query: { url: app.access_url, appId: app.id?.toString() || '', appName: appTitle }
|
||||
}).then(() => {
|
||||
// 导航完成后,手动添加或更新标签视图
|
||||
nextTick(() => {
|
||||
// 查找是否已存在该标签
|
||||
const existingTag = tagsViewStore.visitedViews.find(tag => tag.path === appPath);
|
||||
|
||||
if (existingTag) {
|
||||
// 如果存在,更新标题
|
||||
tagsViewStore.updateVisitedView({
|
||||
...existingTag,
|
||||
title: appTitle,
|
||||
});
|
||||
} else {
|
||||
// 如果不存在,添加新标签
|
||||
tagsViewStore.addView({
|
||||
name: appName,
|
||||
title: appTitle,
|
||||
path: appPath,
|
||||
fullPath: appPath + `?url=${encodeURIComponent(app.access_url || '')}&appId=${app.id || ''}&appName=${encodeURIComponent(appTitle)}`,
|
||||
icon: 'Monitor',
|
||||
affix: false,
|
||||
keepAlive: false,
|
||||
query: { url: app.access_url, appId: app.id?.toString() || '', appName: appTitle },
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
function resetForm() {
|
||||
Object.assign(formData, {
|
||||
name: '',
|
||||
access_url: '',
|
||||
icon_url: '',
|
||||
status: true,
|
||||
description: '',
|
||||
});
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function handleCloseDialog() {
|
||||
dialogVisible.value = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
|
||||
if (dialogType.value === 'create') {
|
||||
await ApplicationAPI.createApplication(formData);
|
||||
} else {
|
||||
await ApplicationAPI.updateApplication(currentApp.value!.id!, formData);
|
||||
}
|
||||
|
||||
dialogVisible.value = false;
|
||||
resetForm();
|
||||
await loadApplicationList();
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
loadApplicationList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-grid-card {
|
||||
height: calc(100vh - 200px);
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.el-card__footer) {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.app-card-el {
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
border-radius: 6px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.app-card-header-el {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.app-info-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
|
||||
.app-name-el {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
margin: 0 0 4px 0;
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.app-card-content {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.app-description {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-meta-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.app-meta-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.meta-item.left {
|
||||
flex: 1;
|
||||
justify-content: flex-start;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.meta-item.right {
|
||||
flex: 1;
|
||||
justify-content: flex-end;
|
||||
text-align: right;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.meta-icon {
|
||||
color: var(--el-text-color-placeholder);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.meta-item span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.app-card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
|
||||
.action-btn {
|
||||
flex: 1;
|
||||
font-size: 12px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.pagination-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,455 @@
|
||||
<!-- 演示示例 -->
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery">
|
||||
<el-form-item prop="name" label="名称">
|
||||
<el-input v-model="queryFormData.name" placeholder="请输入名称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item prop="status" label="状态">
|
||||
<el-select v-model="queryFormData.status" placeholder="请选择状态" style="width: 167.5px" clearable>
|
||||
<el-option value="true" label="启用" />
|
||||
<el-option value="false" label="停用" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 时间范围,收起状态下隐藏 -->
|
||||
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
|
||||
<DatePicker
|
||||
v-model="dateRange"
|
||||
@update:model-value="handleDateRangeChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 查询、重置、展开/收起按钮 -->
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button type="primary" icon="search" native-type="submit">
|
||||
查询
|
||||
</el-button>
|
||||
<el-button icon="refresh" @click="handleResetQuery">
|
||||
重置
|
||||
</el-button>
|
||||
<!-- 展开/收起 -->
|
||||
<template v-if="isExpandable">
|
||||
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
|
||||
{{ isExpand ? "收起" : "展开" }}
|
||||
<el-icon>
|
||||
<template v-if="isExpand">
|
||||
<ArrowUp />
|
||||
</template>
|
||||
<template v-else>
|
||||
<ArrowDown />
|
||||
</template>
|
||||
</el-icon>
|
||||
</el-link>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<el-card class="data-table">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>
|
||||
<el-tooltip content="流程管理列表">
|
||||
<QuestionFilled class="w-4 h-4 mx-1" />
|
||||
</el-tooltip>
|
||||
流程管理列表
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-button v-hasPerm="['workflow:operator:create']" type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
|
||||
<el-button v-hasPerm="['workflow:operator:delete']" type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
|
||||
<el-dropdown v-hasPerm="['application:myapp:update']" trigger="click">
|
||||
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">更多</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Check" @click="handleMoreClick(true)">批量启用</el-dropdown-item>
|
||||
<el-dropdown-item icon="CircleClose" @click="handleMoreClick(false)">批量停用</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button v-hasPerm="['workflow:operator:refresh']" type="warning" icon="refresh" circle @click="handleRefresh"/>
|
||||
</el-tooltip>
|
||||
<el-popover placement="bottom" trigger="click">
|
||||
<template #reference>
|
||||
<el-button type="danger" icon="operation" circle></el-button>
|
||||
</template>
|
||||
<el-scrollbar max-height="350px">
|
||||
<template v-for="column in tableColumns" :key="column.prop">
|
||||
<el-checkbox v-if="column.prop" v-model="column.show" :label="column.label" />
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</el-popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域:工作流列表 -->
|
||||
<el-table ref="dataTableRef" v-loading="loading" :data="pageTableData" highlight-current-row class="data-table__content" height="450" border stripe @selection-change="handleSelectionChange">
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'selection')?.show" type="selection" min-width="55" align="center" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'index')?.show" fixed label="序号" min-width="60">
|
||||
<template #default="scope">
|
||||
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'name')?.show" label="名称" prop="name" min-width="140" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'status')?.show" label="状态" prop="status" min-width="80">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.status === true ? 'success' : 'danger'">
|
||||
{{ scope.row.status === true ? "启用" : "停用" }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'description')?.show" label="描述" prop="description" min-width="140" />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'created_at')?.show" label="创建时间" prop="created_at" min-width="180" sortable />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'updated_at')?.show" label="更新时间" prop="updated_at" min-width="180" sortable />
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'creator')?.show" key="creator" label="创建人" min-width="100">
|
||||
<template #default="scope">
|
||||
{{ scope.row.creator?.name }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'operation')?.show" fixed="right" label="操作" align="center" min-width="200">
|
||||
<template #default="scope">
|
||||
<el-button v-hasPerm="['workflow:operator:detail']" type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
|
||||
<el-button v-hasPerm="['workflow:operator:update']" type="primary" size="small" link icon="edit" @click="handleOpenDialog('update', scope.row.id)">编辑</el-button>
|
||||
<el-button v-hasPerm="['workflow:operator:delete']" type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页区域 -->
|
||||
<template #footer>
|
||||
<pagination v-model:total="total" v-model:page="queryFormData.page_no" v-model:limit="queryFormData.page_size" @pagination="loadingData" />
|
||||
</template>
|
||||
</el-card>
|
||||
|
||||
<!-- 弹窗区域 -->
|
||||
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
|
||||
<!-- 详情 -->
|
||||
<template v-if="dialogVisible.type === 'detail'">
|
||||
<el-descriptions :column="4" border>
|
||||
<el-descriptions-item label="名称" :span="2">
|
||||
{{ detailFormData.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态" :span="2">
|
||||
<el-tag :type="detailFormData.status ? 'success' : 'danger'">
|
||||
{{ detailFormData.status ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="描述" :span="2">
|
||||
{{ detailFormData.description }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人" :span="2">
|
||||
{{ detailFormData.creator?.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间" :span="2">
|
||||
{{ detailFormData.created_at }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间" :span="2">
|
||||
{{ detailFormData.updated_at }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<!-- 新增、编辑表单 -->
|
||||
<template v-else>
|
||||
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto" label-position="right">
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入名称" :maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="true">
|
||||
启用
|
||||
</el-radio>
|
||||
<el-radio :value="false">
|
||||
停用
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="description">
|
||||
<el-input v-model="formData.description" :rows="4" :maxlength="100" show-word-limit type="textarea" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
<el-button v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button v-else type="primary" @click="handleCloseDialog">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: "Workflow",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
import { ref, reactive, onMounted } from "vue";
|
||||
import ExampleAPI, { ExampleTable, ExampleForm, ExamplePageQuery } from "@/api/module_generator/demo";
|
||||
import DatePicker from "@/components/DatePicker/index.vue";
|
||||
import { formatToDateTime } from "@/utils/dateUtil";
|
||||
|
||||
const queryFormRef = ref();
|
||||
const dataFormRef = ref();
|
||||
const total = ref(0);
|
||||
const selectIds = ref<number[]>([]);
|
||||
const loading = ref(false);
|
||||
const isExpand = ref(false);
|
||||
const isExpandable = ref(true);
|
||||
|
||||
// 分页表单
|
||||
const pageTableData = ref<ExampleTable[]>([]);
|
||||
|
||||
// 表格列配置
|
||||
const tableColumns = ref([
|
||||
{ prop: 'selection', label: '选择框', show: true },
|
||||
{ prop: 'index', label: '序号', show: true },
|
||||
{ prop: 'name', label: '名称', show: true },
|
||||
{ prop: 'status', label: '状态', show: true },
|
||||
{ prop: 'description', label: '描述', show: true },
|
||||
{ prop: 'created_at', label: '创建时间', show: true },
|
||||
{ prop: 'updated_at', label: '更新时间', show: true },
|
||||
{ prop: 'creator', label: '创建人', show: true },
|
||||
{ prop: 'operation', label: '操作', show: true }
|
||||
])
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref<ExampleTable>({});
|
||||
|
||||
// 日期范围临时变量
|
||||
const dateRange = ref<[Date, Date] | []>([]);
|
||||
|
||||
// 处理日期范围变化
|
||||
function handleDateRangeChange(range: [Date, Date]) {
|
||||
dateRange.value = range;
|
||||
if (range && range.length === 2) {
|
||||
queryFormData.start_time = formatToDateTime(range[0]);
|
||||
queryFormData.end_time = formatToDateTime(range[1]);
|
||||
} else {
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 分页查询参数
|
||||
const queryFormData = reactive<ExamplePageQuery>({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
name: undefined,
|
||||
status: undefined,
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
});
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive<ExampleForm>({
|
||||
id: undefined,
|
||||
name: '',
|
||||
status: true,
|
||||
description: undefined,
|
||||
})
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: "",
|
||||
visible: false,
|
||||
type: 'create' as 'create' | 'update' | 'detail',
|
||||
});
|
||||
|
||||
// 表单验证规则
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: "请输入名称", trigger: "blur" }],
|
||||
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
|
||||
});
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh() {
|
||||
await loadingData();
|
||||
};
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await ExampleAPI.getExampleList(queryFormData);
|
||||
pageTableData.value = response.data.data.items;
|
||||
total.value = response.data.data.total;
|
||||
}
|
||||
catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 查询(重置页码后获取数据)
|
||||
async function handleQuery() {
|
||||
queryFormData.page_no = 1;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields();
|
||||
queryFormData.page_no = 1;
|
||||
// 重置日期范围选择器
|
||||
dateRange.value = [];
|
||||
queryFormData.start_time = undefined;
|
||||
queryFormData.end_time = undefined;
|
||||
loadingData();
|
||||
}
|
||||
|
||||
// 定义初始表单数据常量
|
||||
const initialFormData: ExampleForm = {
|
||||
id: undefined,
|
||||
name: '',
|
||||
status: true,
|
||||
description: '',
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
async function resetForm() {
|
||||
if (dataFormRef.value) {
|
||||
dataFormRef.value.resetFields();
|
||||
dataFormRef.value.clearValidate();
|
||||
}
|
||||
// 完全重置 formData 为初始状态
|
||||
Object.assign(formData, initialFormData);
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
async function handleSelectionChange(selection: any) {
|
||||
selectIds.value = selection.map((item: any) => item.id);
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
async function handleCloseDialog() {
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number) {
|
||||
dialogVisible.type = type;
|
||||
if (id) {
|
||||
const response = await ExampleAPI.getExampleDetail(id);
|
||||
if (type === 'detail') {
|
||||
dialogVisible.title = "详情";
|
||||
Object.assign(detailFormData.value, response.data.data);
|
||||
} else if (type === 'update') {
|
||||
dialogVisible.title = "修改";
|
||||
Object.assign(formData, response.data.data);
|
||||
}
|
||||
} else {
|
||||
dialogVisible.title = "新增工作流";
|
||||
formData.id = undefined;
|
||||
}
|
||||
dialogVisible.visible = true;
|
||||
}
|
||||
|
||||
// 提交表单(防抖)
|
||||
async function handleSubmit() {
|
||||
// 表单校验
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
// 根据弹窗传入的参数(deatil\create\update)判断走什么逻辑
|
||||
const id = formData.id;
|
||||
if (id) {
|
||||
try {
|
||||
await ExampleAPI.updateExample(id, { id, ...formData })
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await ExampleAPI.createExample(formData)
|
||||
dialogVisible.visible = false;
|
||||
resetForm();
|
||||
handleCloseDialog();
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm("确认删除该项数据?", "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await ExampleAPI.deleteExample(ids);
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
|
||||
// 批量启用/停用
|
||||
async function handleMoreClick(status: boolean) {
|
||||
if (selectIds.value.length) {
|
||||
ElMessageBox.confirm(`确认${status ? '启用' : '停用'}该项数据?`, "警告", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await ExampleAPI.batchAvailableExample({ ids: selectIds.value, status });
|
||||
handleResetQuery();
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadingData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
Reference in New Issue
Block a user