style: 移除多余空行并统一代码格式

修复代码中的多余空行问题,统一字符串引号使用双引号,调整对象和数组的格式,优化代码缩进和换行,提升代码可读性和一致性
This commit is contained in:
zhangtao
2025-11-14 01:45:22 +08:00
parent 66c345d590
commit d588abb386
100 changed files with 9192 additions and 5181 deletions
+204 -172
View File
@@ -16,8 +16,23 @@
</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>
<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>
@@ -30,8 +45,10 @@
<el-icon size="64"><ChatDotRound /></el-icon>
</div>
<h1>FA智能助手</h1>
<p class="welcome-subtitle">我是您的专属AI助手,可以帮您回答问题、处理任务和进行智能对话</p>
<p class="welcome-subtitle">
我是您的专属AI助手,可以帮您回答问题、处理任务和进行智能对话
</p>
<div class="example-prompts">
<div class="prompt-card" @click="setPrompt('请介绍一下FastApiAdmin系统')">
<h4>系统介绍</h4>
@@ -55,9 +72,9 @@
<!-- 消息列表 -->
<div v-else class="messages-list">
<div
v-for="message in messages"
:key="message.id"
<div
v-for="message in messages"
:key="message.id"
:class="['message-group', message.type]"
>
<div class="message-avatar">
@@ -71,11 +88,14 @@
<div class="message-content">
<div class="message-header">
<strong class="sender-name">
{{ message.type === 'user' ? 'You' : 'FA助手' }}
{{ message.type === "user" ? "You" : "FA助手" }}
</strong>
</div>
<div class="message-body">
<div v-if="message.type === 'assistant' && message.loading" class="typing-indicator">
<div
v-if="message.type === 'assistant' && message.loading"
class="typing-indicator"
>
<div class="typing-dots">
<span></span>
<span></span>
@@ -85,8 +105,18 @@
<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>
<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>
@@ -94,13 +124,7 @@
<!-- 错误提示 -->
<div v-if="error" class="error-banner">
<el-alert
:title="error"
type="error"
:closable="true"
show-icon
@close="error = ''"
/>
<el-alert :title="error" type="error" :closable="true" show-icon @close="error = ''" />
</div>
</div>
@@ -132,9 +156,7 @@
</el-button>
</div>
<div class="input-footer">
<span class="input-hint">
按 Enter 发送消息,Shift + Enter 换行
</span>
<span class="input-hint">按 Enter 发送消息,Shift + Enter 换行</span>
</div>
</div>
</div>
@@ -143,8 +165,8 @@
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref, onMounted, onUnmounted, nextTick, computed } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import {
ChatDotRound,
User,
@@ -155,262 +177,259 @@ import {
Warning,
Setting,
CopyDocument,
RefreshLeft
} from '@element-plus/icons-vue'
RefreshLeft,
} from "@element-plus/icons-vue";
// 消息接口
interface ChatMessage {
id: string
type: 'user' | 'assistant'
content: string
timestamp: number
loading?: boolean
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>()
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'
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 '未知状态'
case "connected":
return "已连接";
case "connecting":
return "连接中...";
case "disconnected":
return "未连接";
default:
return "未知状态";
}
})
});
// WebSocket 连接管理
const connectWebSocket = () => {
if (ws?.readyState === WebSocket.OPEN) {
return
return;
}
connectionStatus.value = 'connecting'
error.value = ''
connectionStatus.value = "connecting";
error.value = "";
try {
ws = new WebSocket(WS_URL)
ws = new WebSocket(WS_URL);
ws.onopen = () => {
console.log('WebSocket 连接已建立')
isConnected.value = true
connectionStatus.value = 'connected'
ElMessage.success('连接成功')
}
console.log("WebSocket 连接已建立");
isConnected.value = true;
connectionStatus.value = "connected";
ElMessage.success("连接成功");
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
handleWebSocketMessage(data)
const data = JSON.parse(event.data);
handleWebSocketMessage(data);
} catch (err) {
console.error('解析消息失败:', err)
handleWebSocketMessage({ content: event.data })
console.error("解析消息失败:", err);
handleWebSocketMessage({ content: event.data });
}
}
};
ws.onclose = (event) => {
console.log('WebSocket 连接已关闭', event.code, event.reason)
isConnected.value = false
connectionStatus.value = 'disconnected'
}
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('连接失败,请检查服务器状态')
}
console.error("WebSocket 错误:", error);
isConnected.value = false;
connectionStatus.value = "disconnected";
ElMessage.error("连接失败,请检查服务器状态");
};
} catch (err) {
console.error('创建 WebSocket 连接失败:', err)
connectionStatus.value = 'disconnected'
error.value = '无法创建连接'
console.error("创建 WebSocket 连接失败:", err);
connectionStatus.value = "disconnected";
error.value = "无法创建连接";
}
}
};
// 断开连接
const disconnectWebSocket = () => {
if (ws) {
ws.close(1000, '用户主动断开')
ws = null
ws.close(1000, "用户主动断开");
ws = null;
}
isConnected.value = false
connectionStatus.value = 'disconnected'
}
isConnected.value = false;
connectionStatus.value = "disconnected";
};
// 切换连接状态
const toggleConnection = () => {
if (isConnected.value) {
disconnectWebSocket()
ElMessage.info('已断开连接')
disconnectWebSocket();
ElMessage.info("已断开连接");
} else {
connectWebSocket()
connectWebSocket();
}
}
};
// 处理 WebSocket 消息
const handleWebSocketMessage = (data: any) => {
// 查找最后一个助手消息
const lastMessage = messages.value[messages.value.length - 1]
if (lastMessage && lastMessage.type === 'assistant' && lastMessage.loading) {
const lastMessage = messages.value[messages.value.length - 1];
if (lastMessage && lastMessage.type === "assistant" && lastMessage.loading) {
// 更新加载中的消息
lastMessage.content = data.content || data.message || '收到回复'
lastMessage.loading = false
lastMessage.content = data.content || data.message || "收到回复";
lastMessage.loading = false;
} else {
// 添加新的助手消息
addMessage('assistant', data.content || data.message || '收到回复')
addMessage("assistant", data.content || data.message || "收到回复");
}
scrollToBottom()
}
scrollToBottom();
};
// 发送消息
const sendMessage = async () => {
const message = inputMessage.value.trim()
const message = inputMessage.value.trim();
if (!message || !isConnected.value || sending.value) {
return
return;
}
// 添加用户消息
addMessage('user', message)
inputMessage.value = ''
addMessage("user", message);
inputMessage.value = "";
// 添加加载中的助手消息
const loadingMessage: ChatMessage = {
id: generateId(),
type: 'assistant',
content: '',
type: "assistant",
content: "",
timestamp: Date.now(),
loading: true
}
messages.value.push(loadingMessage)
loading: true,
};
messages.value.push(loadingMessage);
sending.value = true
scrollToBottom()
sending.value = true;
scrollToBottom();
try {
// 发送消息到 WebSocket
if (ws?.readyState === WebSocket.OPEN) {
const payload = {
message,
timestamp: Date.now()
}
ws.send(JSON.stringify(payload))
timestamp: Date.now(),
};
ws.send(JSON.stringify(payload));
} else {
throw new Error('WebSocket 连接未建立')
throw new Error("WebSocket 连接未建立");
}
} catch (err) {
console.error('发送消息失败:', err)
console.error("发送消息失败:", err);
// 移除加载消息并显示错误
messages.value.pop()
error.value = '发送消息失败,请检查连接状态'
ElMessage.error('发送失败')
messages.value.pop();
error.value = "发送消息失败,请检查连接状态";
ElMessage.error("发送失败");
} finally {
sending.value = false
sending.value = false;
}
}
};
// 添加消息
const addMessage = (type: 'user' | 'assistant', content: string) => {
const addMessage = (type: "user" | "assistant", content: string) => {
const message: ChatMessage = {
id: generateId(),
type,
content,
timestamp: Date.now()
}
messages.value.push(message)
nextTick(() => scrollToBottom())
}
timestamp: Date.now(),
};
messages.value.push(message);
nextTick(() => scrollToBottom());
};
const clearCurrentChat = async () => {
try {
await ElMessageBox.confirm(
'确定要清空当前对话吗?此操作不可恢复。',
'确认清空',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
messages.value = []
ElMessage.success('对话已清空')
await ElMessageBox.confirm("确定要清空当前对话吗?此操作不可恢复。", "确认清空", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
messages.value = [];
ElMessage.success("对话已清空");
} catch {
// 用户取消
}
}
};
// 设置提示词
const setPrompt = (prompt: string) => {
inputMessage.value = prompt
}
inputMessage.value = prompt;
};
// 复制消息
const copyMessage = async (content: string) => {
try {
await navigator.clipboard.writeText(content)
ElMessage.success('已复制到剪贴板')
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 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
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight;
}
})
}
});
};
// 格式化消息内容
const formatMessage = (content: string) => {
if (!content) return ''
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>')
}
.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)
}
return Date.now().toString(36) + Math.random().toString(36).substr(2);
};
// 生命周期
onMounted(() => {
connectWebSocket()
})
connectWebSocket();
});
onUnmounted(() => {
disconnectWebSocket()
})
disconnectWebSocket();
});
</script>
<style lang="scss" scoped>
@@ -458,9 +477,15 @@ onUnmounted(() => {
font-size: 12px;
.status-icon {
&.connected { color: var(--el-color-success); }
&.connecting { color: var(--el-color-warning); }
&.disconnected { color: var(--el-color-danger); }
&.connected {
color: var(--el-color-success);
}
&.connecting {
color: var(--el-color-warning);
}
&.disconnected {
color: var(--el-color-danger);
}
}
.status-text {
@@ -609,7 +634,7 @@ onUnmounted(() => {
:deep(p) {
margin: 0 0 12px;
&:last-child {
margin-bottom: 0;
}
@@ -619,7 +644,7 @@ onUnmounted(() => {
background: var(--el-fill-color-light);
padding: 2px 6px;
border-radius: 4px;
font-family: 'JetBrains Mono', 'Courier New', monospace;
font-family: "JetBrains Mono", "Courier New", monospace;
font-size: 14px;
}
@@ -644,7 +669,8 @@ onUnmounted(() => {
font-style: italic;
}
:deep(ul), :deep(ol) {
:deep(ul),
:deep(ol) {
padding-left: 20px;
margin: 12px 0;
}
@@ -671,8 +697,12 @@ onUnmounted(() => {
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; }
&:nth-child(2) {
animation-delay: 0.2s;
}
&:nth-child(3) {
animation-delay: 0.4s;
}
}
}
}
@@ -774,15 +804,17 @@ onUnmounted(() => {
}
@keyframes typing {
0%, 20% {
0%,
20% {
opacity: 0.4;
transform: scale(0.8);
}
50% {
50% {
opacity: 1;
transform: scale(1);
}
80%, 100% {
80%,
100% {
opacity: 0.4;
transform: scale(0.8);
}
@@ -1,21 +1,33 @@
<!-- 任务日志抽屉 -->
<template>
<el-drawer v-model="drawerVisible" :title="'【' + props.jobName + '】任务日志'" :size="drawerSize">
<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
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-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"
/>
<DatePicker v-model="dateRange" @update:model-value="handleDateRangeChange" />
</el-form-item>
<!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons">
@@ -44,7 +56,9 @@
<template #header>
<div class="card-header">
<span>
<el-tooltip content="任务执行日志记录每次定时任务的执行情况,包括成功、失败状态及错误信息。">
<el-tooltip
content="任务执行日志记录每次定时任务的执行情况,包括成功、失败状态及错误信息。"
>
<QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip>
任务执行日志列表
@@ -57,7 +71,14 @@
<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-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>
@@ -82,7 +103,17 @@
</div>
<!-- 表格区域:任务日志列表 -->
<el-table ref="dataTableRef" v-loading="loading" :data="pageTableData" highlight-current-row class="data-table__content" height="460" border stripe @selection-change="handleSelectionChange">
<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>
@@ -101,49 +132,116 @@
</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_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="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_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>
<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" />
<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">
<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="日志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-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>
@@ -164,13 +262,13 @@
const props = defineProps({
jobId: {
type: Number,
required: true
required: true,
},
jobName: {
type: String,
required: true
}
})
required: true,
},
});
import JobAPI, { JobLogPageQuery, JobLogTable } from "@/api/module_application/job";
import { useAppStore } from "@/store/modules/app.store";
@@ -216,7 +314,7 @@ const queryFormData = reactive<JobLogPageQuery>({
const dialogVisible = reactive({
title: "",
visible: false,
type: 'detail',
type: "detail",
});
// 日期范围临时变量
@@ -235,9 +333,9 @@ function handleDateRangeChange(range: [Date, Date]) {
}
// 列表刷新
async function handleRefresh () {
async function handleRefresh() {
await loadingData();
};
}
// 加载表格数据
async function loadingData() {
@@ -247,11 +345,9 @@ async function loadingData() {
const response = await JobAPI.getJobLogList(queryFormData);
pageTableData.value = response.data.data.items;
total.value = response.data.data.total;
}
catch (error: any) {
} catch (error: any) {
console.error(error);
}
finally {
} finally {
loading.value = false;
}
}
@@ -290,7 +386,7 @@ async function handleCloseDialog() {
}
// 打开详情弹窗
async function handleOpenDialog(type: 'detail', id?: number) {
async function handleOpenDialog(type: "detail", id?: number) {
dialogVisible.type = type;
if (id) {
const response = await JobAPI.getJobLogDetail(id);
@@ -310,21 +406,23 @@ async function handleDelete(ids: number[]) {
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();
});
})
.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();
});
}
// 清空日志
@@ -333,19 +431,21 @@ async function handleClearLog() {
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();
});
})
.then(async () => {
try {
loading.value = true;
await JobAPI.clearJobLog();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
})
.catch(() => {
ElMessageBox.close();
});
}
// 打开导出弹窗
@@ -355,22 +455,22 @@ function handleOpenExportsModal() {
// 导出字段
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: '创建时间' },
{ 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',
permPrefix: "application:job_log",
cols: exportColumns as any,
exportsAction: async (params: any) => {
const query: any = { ...params };
@@ -402,7 +502,7 @@ function closeDrawer() {
// 暴露方法给父组件
defineExpose({
openDrawer,
closeDrawer
closeDrawer,
});
onMounted(() => {
@@ -11,11 +11,7 @@
@submit.prevent="handleQuery"
>
<el-form-item prop="name" label="任务名称">
<el-input
v-model="queryFormData.name"
placeholder="请输入任务名称"
clearable
/>
<el-input v-model="queryFormData.name" placeholder="请输入任务名称" clearable />
</el-form-item>
<el-form-item prop="status" label="状态">
<el-select
@@ -30,30 +26,35 @@
</el-form-item>
<!-- 时间范围,收起状态下隐藏 -->
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
<DatePicker
v-model="dateRange"
@update:model-value="handleDateRangeChange"
/>
<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"
v-model="queryFormData.creator"
@confirm-click="handleConfirm"
@clear-click="handleQuery"
/>
</el-form-item>
<!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons">
<el-button v-hasPerm="['module_application:job:query']" type="primary" icon="search" native-type="submit">查询</el-button>
<el-button v-hasPerm="['module_application:job:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
<el-button
v-hasPerm="['module_application:job:query']"
type="primary"
icon="search"
native-type="submit"
>
查询
</el-button>
<el-button
v-hasPerm="['module_application:job:query']"
icon="refresh"
@click="handleResetQuery"
>
重置
</el-button>
<!-- 展开/收起 -->
<template v-if="isExpandable">
<el-link
class="ml-3"
type="primary"
underline="never"
@click="isExpand = !isExpand"
>
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
{{ isExpand ? "收起" : "展开" }}
<el-icon>
<template v-if="isExpand">
@@ -92,7 +93,9 @@
type="success"
icon="plus"
@click="handleOpenDialog('create')"
>新增</el-button>
>
新增
</el-button>
</el-col>
<el-col :span="1.5">
<el-button
@@ -101,7 +104,9 @@
icon="delete"
:disabled="selectIds.length === 0"
@click="handleDelete(selectIds)"
>批量删除</el-button>
>
批量删除
</el-button>
</el-col>
</el-row>
</div>
@@ -120,7 +125,13 @@
</el-col>
<el-col :span="1.5">
<el-tooltip content="清除">
<el-button v-hasPerm="['module_application:job:clear']" type="danger" icon="delete" circle @click="handleClear"/>
<el-button
v-hasPerm="['module_application:job:clear']"
type="danger"
icon="delete"
circle
@click="handleClear"
/>
</el-tooltip>
</el-col>
<el-col :span="1.5">
@@ -160,24 +171,29 @@
</template>
</el-table-column>
<el-table-column label="任务名称" prop="name" min-width="140" />
<el-table-column label="执行函数" prop="func" 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 }}
{{
(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" show-overflow-tooltip>
<template #default="scope">
{{ (dictStore.getDictLabel('sys_job_trigger',scope.row.trigger) as any)?.dict_label }}
{{ (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" show-overflow-tooltip>
<template #default="scope">
{{ (dictStore.getDictLabel('sys_job_store',scope.row.jobstore) as any)?.dict_label }}
{{ (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 }}
{{
(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">
@@ -195,18 +211,8 @@
</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
/>
<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">
@@ -295,30 +301,50 @@
<!-- 详情 -->
<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">
{{ 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 ? "是" : "否" }}
@@ -329,30 +355,30 @@
{{ 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-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>
<!-- 新增、编辑表单 -->
@@ -366,55 +392,43 @@
inline
>
<el-form-item label="任务名称" prop="name" style="width: 40%">
<el-input
v-model="formData.name"
placeholder="请输入任务名称"
:maxlength="50"
/>
<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-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-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-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-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-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">
@@ -422,11 +436,7 @@
<el-radio :value="false">否</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="最大实例数"
prop="max_instances"
style="width: 40%"
>
<el-form-item label="最大实例数" prop="max_instances" style="width: 40%">
<el-input-number
v-model="formData.max_instances"
controls-position="right"
@@ -436,13 +446,13 @@
</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-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
@@ -464,9 +474,7 @@
v-else-if="formData.trigger === 'interval'"
label="间隔时间"
prop="trigger_args"
:rules="[
{ required: true, message: '请输入间隔时间', trigger: 'change' },
]"
:rules="[{ required: true, message: '请输入间隔时间', trigger: 'change' }]"
style="width: 40%"
>
<el-popover
@@ -532,9 +540,7 @@
v-if="formData.trigger && formData.trigger != 'date'"
label="开始日期"
prop="start_date"
:rules="[
{ required: false, message: '请选择开始日期', trigger: 'blur' },
]"
:rules="[{ required: false, message: '请选择开始日期', trigger: 'blur' }]"
style="width: 40%"
>
<el-date-picker
@@ -549,9 +555,7 @@
v-if="formData.trigger && formData.trigger != 'date'"
label="结束日期"
prop="end_date"
:rules="[
{ required: false, message: '请选择结束日期', trigger: 'blur' },
]"
:rules="[{ required: false, message: '请选择结束日期', trigger: 'blur' }]"
style="width: 40%"
>
<el-date-picker
@@ -580,12 +584,26 @@
<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="['module_application:job:detail']" type="primary" @click="handleCloseDialog" >确定</el-button>
<el-button v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">
确定
</el-button>
<el-button
v-else
v-hasPerm="['module_application: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" />
<JobLogDrawer
v-if="drawerVisible"
v-model="drawerVisible"
:job-id="currentJobId"
:job-name="currentJobName"
/>
<ExportModal
v-model="exportsDialogVisible"
:content-config="curdContentConfig"
@@ -607,7 +625,7 @@ 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 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";
@@ -690,7 +708,6 @@ const rules = reactive({
executor: [{ required: true, message: "请选择执行器", trigger: "blur" }],
});
// 日期范围临时变量
const dateRange = ref<[Date, Date] | []>([]);
@@ -764,7 +781,7 @@ const initialFormData: JobForm = {
end_date: undefined,
status: undefined,
description: undefined,
}
};
// 重置表单
async function resetForm() {
@@ -789,10 +806,7 @@ async function handleCloseDialog() {
}
// 打开弹窗
async function handleOpenDialog(
type: "create" | "update" | "detail",
id?: number
) {
async function handleOpenDialog(type: "create" | "update" | "detail", id?: number) {
dialogVisible.type = type;
if (id) {
const response = await JobAPI.getJobDetail(id);
@@ -928,25 +942,25 @@ function handleOpenLogDrawer(jobId: number, jobName: string) {
// 导出字段
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: '更新时间' },
{ 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: 'module_application:job',
permPrefix: "module_application:job",
cols: exportColumns as any,
exportsAction: async (params: any) => {
const query: any = { ...params };
if (typeof query.status === 'string') query.status = query.status === 'true';
if (typeof query.status === "string") query.status = query.status === "true";
query.page_no = 1;
query.page_size = 1000;
const all: any[] = [];
@@ -964,11 +978,15 @@ const curdContentConfig = {
onMounted(async () => {
// 加载字典数据
await dictStore.getDict(['sys_job_function','sys_job_executor','sys_job_store', 'sys_job_trigger']);
await dictStore.getDict([
"sys_job_function",
"sys_job_executor",
"sys_job_store",
"sys_job_trigger",
]);
// 加载表格数据
loadingData();
});
</script>
<style lang="scss" scoped>
</style>
<style lang="scss" scoped></style>
@@ -2,9 +2,9 @@
<template>
<div class="internal-app-container">
<div class="internal-app-content">
<iframe
<iframe
ref="iframeRef"
:src="appUrl"
:src="appUrl"
class="internal-app-iframe"
frameborder="0"
allowfullscreen
@@ -21,9 +21,9 @@
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router';
import { Loading } from '@element-plus/icons-vue';
import { useTagsViewStore } from '@/store';
import { useRoute } from "vue-router";
import { Loading } from "@element-plus/icons-vue";
import { useTagsViewStore } from "@/store";
defineOptions({
name: "InternalApp",
@@ -45,19 +45,22 @@ function handleIframeLoad() {
}
// 监听路由变化,更新iframe
watch(() => route.query.url, (newUrl) => {
if (newUrl && iframeRef.value) {
loading.value = true;
iframeRef.value.src = newUrl as string;
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);
const currentTag = tagsViewStore.visitedViews.find((tag) => tag.path === route.path);
if (currentTag && currentTag.title !== appName.value) {
tagsViewStore.updateVisitedView({
...currentTag,
@@ -71,19 +74,22 @@ onMounted(() => {
});
// 监听应用名称变化,更新标签标题
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,
});
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>
@@ -118,13 +124,13 @@ watch(() => appName.value, (newAppName) => {
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;
@@ -1,169 +1,189 @@
<!-- 我的应用管理 -->
<template>
<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>
<!-- 顶部搜索和操作区域 -->
<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-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>
</template>
</el-card>
</div>
<!-- 应用网格 -->
<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 v-if="applicationList.length === 0 && !loading">
<el-empty :image-size="80" description="暂无数据" />
</div>
<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>
<!-- 分页区域 -->
<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
@@ -183,22 +203,22 @@
<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"
@@ -231,8 +251,12 @@ 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 { 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();
@@ -245,7 +269,7 @@ const formRef = ref();
const loading = ref(false);
const total = ref(0);
const dialogVisible = ref(false);
const dialogType = ref<'create' | 'edit'>('create');
const dialogType = ref<"create" | "edit">("create");
const currentApp = ref<ApplicationInfo | null>(null);
const isExpand = ref(false);
const isExpandable = ref(true);
@@ -264,37 +288,38 @@ const applicationList = ref<ApplicationInfo[]>([]);
// 表单数据
const formData = reactive<ApplicationForm>({
name: '',
access_url: '',
icon_url: '',
name: "",
access_url: "",
icon_url: "",
status: true,
description: '',
description: "",
});
// 表单验证规则
const formRules = reactive({
name: [
{ required: true, message: "请输入应用名称", trigger: "blur" },
{ min: 2, max: 30, message: "长度在 2 到 30 个字符", 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" }
{ type: "url" as const, message: "请输入正确的URL格式", trigger: "blur" },
],
icon_url: [
{ required: true, message: "请输入图标地址", trigger: "blur" },
{ type: 'url' as const, message: "请输入正确的URL格式", 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 dialogTitle = computed(() => (dialogType.value === "create" ? "创建应用" : "编辑应用"));
// 格式化时间
const formatTime = (time: string | undefined) => {
if (!time) return '未知';
return formatToDateTime(time, 'YYYY-MM-DD HH:mm:ss');
if (!time) return "未知";
return formatToDateTime(time, "YYYY-MM-DD HH:mm:ss");
};
// 加载应用列表
@@ -305,7 +330,7 @@ async function loadApplicationList() {
applicationList.value = response.data.data.items;
total.value = response.data.data.total;
} catch (error) {
console.error('加载应用列表失败:', error);
console.error("加载应用列表失败:", error);
} finally {
loading.value = false;
}
@@ -331,15 +356,15 @@ async function handleResetQuery() {
// 创建应用
function handleCreateApp() {
console.log('handleCreateApp');
dialogType.value = 'create';
console.log("handleCreateApp");
dialogType.value = "create";
resetForm();
dialogVisible.value = true;
}
// 编辑应用
function handleEditApp(app: ApplicationInfo) {
dialogType.value = 'edit';
dialogType.value = "edit";
currentApp.value = app;
Object.assign(formData, app);
dialogVisible.value = true;
@@ -348,17 +373,17 @@ function handleEditApp(app: ApplicationInfo) {
// 删除应用
async function handleDeleteApp(app: ApplicationInfo) {
try {
await ElMessageBox.confirm('确认删除该应用?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
await ElMessageBox.confirm("确认删除该应用?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
});
await ApplicationAPI.deleteApplication([app.id!]);
await loadApplicationList();
} catch (error) {
if (error !== 'cancel') {
console.error('删除应用失败:', error);
if (error !== "cancel") {
console.error("删除应用失败:", error);
}
}
}
@@ -366,10 +391,10 @@ async function handleDeleteApp(app: ApplicationInfo) {
// 应用操作
async function handleAppAction(command: string, app: ApplicationInfo) {
switch (command) {
case 'edit':
case "edit":
handleEditApp(app);
break;
case 'delete':
case "delete":
await handleDeleteApp(app);
break;
}
@@ -378,68 +403,72 @@ async function handleAppAction(command: string, app: ApplicationInfo) {
// 外部打开应用
function openAppExternal(url: string | undefined) {
if (url) {
window.open(url, '_blank');
window.open(url, "_blank");
}
}
// 内部打开应用
function openAppInternal(app: ApplicationInfo) {
if (!app.status) {
ElMessage.warning('应用已停用,无法打开');
ElMessage.warning("应用已停用,无法打开");
return;
}
if (!app.access_url) {
ElMessage.warning('应用访问地址不存在');
ElMessage.warning("应用访问地址不存在");
return;
}
// 创建一个动态路由路径
const appPath = `/internal-app/${app.id}`;
const appName = `InternalApp${app.id}`;
const appTitle = app.name || '未命名应用';
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 },
});
}
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: '',
name: "",
access_url: "",
icon_url: "",
status: true,
description: '',
description: "",
});
formRef.value?.resetFields();
}
@@ -454,18 +483,18 @@ function handleCloseDialog() {
async function handleSubmit() {
try {
await formRef.value?.validate();
if (dialogType.value === 'create') {
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);
console.error("提交失败:", error);
}
}
@@ -481,7 +510,7 @@ onMounted(() => {
position: relative;
display: flex;
flex-direction: column;
:deep(.el-card__footer) {
margin-top: auto;
display: flex;
@@ -496,10 +525,10 @@ onMounted(() => {
}
.app-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
flex: 1;
}
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
flex: 1;
}
.app-card-el {
border: 1px solid var(--el-border-color-lighter);
@@ -521,7 +550,6 @@ onMounted(() => {
min-width: 0;
}
.app-name-el {
font-size: 16px;
font-weight: 600;
@@ -533,8 +561,6 @@ onMounted(() => {
text-overflow: ellipsis;
}
.app-card-content {
margin: 12px 0;
}
@@ -600,7 +626,7 @@ onMounted(() => {
display: flex;
gap: 8px;
margin-top: 12px;
.action-btn {
flex: 1;
font-size: 12px;
@@ -608,17 +634,14 @@ onMounted(() => {
}
}
.pagination-container {
display: flex;
justify-content: flex-end;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>
@@ -1,209 +1,349 @@
<!-- 演示示例 -->
<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="暂无数据" />
<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>
<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>
<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,
name: "Workflow",
inheritAttrs: false,
});
import { ref, reactive, onMounted } from "vue";
import ExampleAPI, { ExampleTable, ExampleForm, ExamplePageQuery } from "@/api/module_generator/demo";
import ExampleAPI, {
ExampleTable,
ExampleForm,
ExamplePageQuery,
} from "@/api/module_generator/demo";
import DatePicker from "@/components/DatePicker/index.vue";
import { formatToDateTime } from "@/utils/dateUtil";
@@ -220,16 +360,16 @@ 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 }
])
{ 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>({});
@@ -239,216 +379,218 @@ 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;
}
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,
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,
})
id: undefined,
name: "",
status: true,
description: undefined,
});
// 弹窗状态
const dialogVisible = reactive({
title: "",
visible: false,
type: 'create' as 'create' | 'update' | 'detail',
title: "",
visible: false,
type: "create" as "create" | "update" | "detail",
});
// 表单验证规则
const rules = reactive({
name: [{ required: true, message: "请输入名称", trigger: "blur" }],
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
name: [{ required: true, message: "请输入名称", trigger: "blur" }],
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
});
// 列表刷新
async function handleRefresh() {
await loadingData();
};
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;
}
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();
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();
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: '',
}
id: undefined,
name: "",
status: true,
description: "",
};
// 重置表单
async function resetForm() {
if (dataFormRef.value) {
dataFormRef.value.resetFields();
dataFormRef.value.clearValidate();
}
// 完全重置 formData 为初始状态
Object.assign(formData, initialFormData);
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);
selectIds.value = selection.map((item: any) => item.id);
}
// 关闭弹窗
async function handleCloseDialog() {
dialogVisible.visible = false;
resetForm();
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;
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);
}
dialogVisible.visible = true;
} 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;
}
}
// 表单校验
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();
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();
});
}
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();
loadingData();
});
</script>