| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701 |
- <template>
- <div class="strategy-log-container">
- <!-- 页面头部 -->
- <div class="page-header">
- <h1 class="page-title">
- <i class="el-icon-document"></i>
- 策略执行日志
- </h1>
- <div class="header-actions">
- <el-date-picker
- v-model="dateRange"
- type="daterange"
- range-separator="至"
- start-placeholder="开始日期"
- end-placeholder="结束日期"
- value-format="yyyy-MM-dd"
- @change="handleDateChange"
- style="margin-right: 12px"
- />
- <el-button icon="el-icon-refresh" @click="refreshList">刷新</el-button>
- </div>
- </div>
- <div class="main-content">
- <!-- 左侧筛选面板 -->
- <div class="filter-panel">
- <div class="filter-section">
- <div class="filter-title">策略筛选</div>
- <el-select
- v-model="queryParams.strategyCode"
- placeholder="选择策略"
- filterable
- clearable
- style="width: 100%"
- @change="getList"
- >
- <el-option
- v-for="strategy in strategyList"
- :key="strategy.strategyCode"
- :label="strategy.strategyName"
- :value="strategy.strategyCode"
- />
- </el-select>
- </div>
- <div class="filter-section">
- <div class="filter-title">执行状态</div>
- <div class="status-tags">
- <el-tag
- v-for="status in execStatusOptions"
- :key="status.value"
- :effect="queryParams.execStatus === status.value ? 'dark' : 'plain'"
- :type="status.type"
- @click="handleStatusFilter(status.value)"
- class="status-tag"
- >
- {{ status.label }}
- </el-tag>
- </div>
- </div>
- <div class="filter-section">
- <div class="filter-title">触发类型</div>
- <el-radio-group v-model="queryParams.triggerType" @change="getList">
- <el-radio label="">全部</el-radio>
- <el-radio label="EVENT">事件</el-radio>
- <el-radio label="TIME">定时</el-radio>
- <el-radio label="MANUAL">手动</el-radio>
- <el-radio label="CONDITION">条件</el-radio>
- <el-radio label="ATTR">属性</el-radio>
- </el-radio-group>
- </div>
- <div class="filter-section">
- <div class="filter-title">执行统计</div>
- <div class="stats-summary">
- <div class="stat-item">
- <span class="stat-value">{{ stats.total || 0 }}</span>
- <span class="stat-label">总执行</span>
- </div>
- <div class="stat-item success">
- <span class="stat-value">{{ stats.success || 0 }}</span>
- <span class="stat-label">成功</span>
- </div>
- <div class="stat-item fail">
- <span class="stat-value">{{ stats.fail || 0 }}</span>
- <span class="stat-label">失败</span>
- </div>
- <div class="stat-item timeout">
- <span class="stat-value">{{ stats.timeout || 0 }}</span>
- <span class="stat-label">超时</span>
- </div>
- </div>
- </div>
- </div>
- <!-- 右侧日志列表 -->
- <div class="log-panel">
- <el-table :data="logList" v-loading="loading" border stripe>
- <el-table-column label="执行ID" prop="execId" width="200" show-overflow-tooltip>
- <template slot-scope="{ row }">
- <el-button type="text" @click="viewLogDetail(row)">
- {{ row.execId.slice(0, 8) }}...
- </el-button>
- </template>
- </el-table-column>
- <el-table-column label="策略" min-width="150">
- <template slot-scope="{ row }">
- <div class="strategy-info">
- <span class="strategy-name">{{ row.strategyName || row.strategyCode }}</span>
- <span class="strategy-code">{{ row.strategyCode }}</span>
- </div>
- </template>
- </el-table-column>
- <el-table-column label="触发类型" width="100" align="center">
- <template slot-scope="{ row }">
- <el-tag size="small" :type="getTriggerTagType(row.triggerType)">
- {{ getTriggerTypeName(row.triggerType) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="触发源" prop="triggerSource" width="120" show-overflow-tooltip />
- <el-table-column label="执行状态" width="100" align="center">
- <template slot-scope="{ row }">
- <el-tag :type="getExecStatusType(row.execStatus)" size="small">
- <i :class="getExecStatusIcon(row.execStatus)"></i>
- {{ getExecStatusText(row.execStatus) }}
- </el-tag>
- </template>
- </el-table-column>
- <el-table-column label="开始时间" width="160">
- <template slot-scope="{ row }">
- {{ parseTime(row.startTime) }}
- </template>
- </el-table-column>
- <el-table-column label="耗时" width="100" align="center">
- <template slot-scope="{ row }">
- <span :class="getDurationClass(row.duration)">
- {{ formatDuration(row.duration) }}
- </span>
- </template>
- </el-table-column>
- <el-table-column label="执行人" prop="execBy" width="100" align="center">
- <template slot-scope="{ row }">
- {{ row.execBy || '-' }}
- </template>
- </el-table-column>
- <el-table-column label="操作" width="120" align="center" fixed="right">
- <template slot-scope="{ row }">
- <el-button size="mini" type="text" @click="viewLogDetail(row)">
- <i class="el-icon-view"></i> 详情
- </el-button>
- </template>
- </el-table-column>
- </el-table>
- <!-- 分页 -->
- <div class="pagination-wrapper">
- <el-pagination
- background
- layout="total, sizes, prev, pager, next, jumper"
- :total="total"
- :page-size.sync="queryParams.pageSize"
- :current-page.sync="queryParams.pageNum"
- :page-sizes="[20, 50, 100]"
- @size-change="getList"
- @current-change="getList"
- />
- </div>
- </div>
- </div>
- <!-- 日志详情抽屉 -->
- <el-drawer
- title="执行详情"
- :visible.sync="detailDrawerVisible"
- size="60%"
- direction="rtl"
- >
- <div class="log-detail" v-if="currentLog">
- <!-- 基本信息 -->
- <div class="detail-section">
- <h3 class="section-title">基本信息</h3>
- <el-descriptions :column="2" border size="small">
- <el-descriptions-item label="执行ID">{{ currentLog.execId }}</el-descriptions-item>
- <el-descriptions-item label="策略代码">{{ currentLog.strategyCode }}</el-descriptions-item>
- <el-descriptions-item label="策略名称">{{ currentLog.strategyName || '-' }}</el-descriptions-item>
- <el-descriptions-item label="触发类型">{{ getTriggerTypeName(currentLog.triggerType) }}</el-descriptions-item>
- <el-descriptions-item label="触发源">{{ currentLog.triggerSource || '-' }}</el-descriptions-item>
- <el-descriptions-item label="执行状态">
- <el-tag :type="getExecStatusType(currentLog.execStatus)" size="small">
- {{ getExecStatusText(currentLog.execStatus) }}
- </el-tag>
- </el-descriptions-item>
- <el-descriptions-item label="执行人">{{ currentLog.execBy || '系统' }}</el-descriptions-item>
- <el-descriptions-item label="开始时间">{{ parseTime(currentLog.startTime) }}</el-descriptions-item>
- <el-descriptions-item label="结束时间">{{ parseTime(currentLog.endTime) || '-' }}</el-descriptions-item>
- <el-descriptions-item label="执行耗时">{{ formatDuration(currentLog.duration) }}</el-descriptions-item>
- </el-descriptions>
- </div>
- <!-- 错误信息 -->
- <div class="detail-section" v-if="currentLog.errorMessage">
- <h3 class="section-title error-title">
- <i class="el-icon-warning"></i> 错误信息
- </h3>
- <div class="error-content">
- {{ currentLog.errorMessage }}
- </div>
- </div>
- <!-- 步骤执行时间线 -->
- <div class="detail-section">
- <h3 class="section-title">执行步骤</h3>
- <el-timeline v-if="stepLogs.length > 0">
- <el-timeline-item
- v-for="step in stepLogs"
- :key="step.id"
- :type="getStepTimelineType(step.execStatus)"
- :icon="getStepTimelineIcon(step.execStatus)"
- >
- <div class="step-log-item">
- <div class="step-header">
- <span class="step-name">{{ step.stepName }}</span>
- <span class="step-index">#{{ step.stepIndex }}</span>
- <el-tag :type="getExecStatusType(step.execStatus)" size="mini">
- {{ getExecStatusText(step.execStatus) }}
- </el-tag>
- <span class="step-duration">{{ formatDuration(step.duration) }}</span>
- </div>
- <div class="step-time">
- {{ parseTime(step.startTime) }} - {{ parseTime(step.endTime) || '执行中' }}
- </div>
- <div class="step-params" v-if="step.inputParam">
- <el-collapse>
- <el-collapse-item title="输入参数">
- <pre>{{ formatJson(step.inputParam) }}</pre>
- </el-collapse-item>
- </el-collapse>
- </div>
- <div class="step-result" v-if="step.outputResult">
- <el-collapse>
- <el-collapse-item title="输出结果">
- <pre>{{ formatJson(step.outputResult) }}</pre>
- </el-collapse-item>
- </el-collapse>
- </div>
- <div class="step-error" v-if="step.errorMessage">
- <span class="error-label">错误:</span>
- {{ step.errorMessage }}
- </div>
- </div>
- </el-timeline-item>
- </el-timeline>
- <el-empty v-else description="暂无步骤执行记录" />
- </div>
- <!-- ✅ 已删除"上下文数据"区块 -->
- </div>
- </el-drawer>
- </div>
- </template>
- <script>
- import { listEnergyStrategy, getExecLogList, getExecLog, getStepExecLog } from '@/api/mgr/energyStrategy';
- export default {
- name: 'StrategyLog',
- data() {
- return {
- loading: false,
- logList: [],
- total: 0,
- strategyList: [],
- dateRange: [],
- queryParams: {
- pageNum: 1,
- pageSize: 20,
- strategyCode: '',
- execStatus: null,
- triggerType: '',
- startTimeBegin: '',
- startTimeEnd: ''
- },
- stats: {
- total: 0,
- success: 0,
- fail: 0,
- timeout: 0
- },
- execStatusOptions: [
- { value: null, label: '全部', type: '' },
- { value: 0, label: '执行中', type: 'info' },
- { value: 1, label: '成功', type: 'success' },
- { value: 2, label: '失败', type: 'danger' },
- { value: 3, label: '超时', type: 'warning' }
- ],
- detailDrawerVisible: false,
- currentLog: null,
- stepLogs: []
- };
- },
- created() {
- this.loadStrategies();
- this.getList();
- },
- methods: {
- // 加载策略列表(用于筛选)
- async loadStrategies() {
- try {
- const response = await listEnergyStrategy({ pageSize: 1000 });
- this.strategyList = response.rows || [];
- } catch (error) {
- console.error('加载策略列表失败', error);
- this.$message.error('加载策略列表失败');
- }
- },
- // 获取日志列表
- async getList() {
- this.loading = true;
- try {
- const params = { ...this.queryParams };
- // 处理日期范围
- if (this.dateRange && this.dateRange.length === 2) {
- params.startTimeBegin = this.dateRange[0] + ' 00:00:00';
- params.startTimeEnd = this.dateRange[1] + ' 23:59:59';
- } else {
- params.startTimeBegin = '';
- params.startTimeEnd = '';
- }
- // 清理空值参数
- Object.keys(params).forEach(key => {
- if (params[key] === '' || params[key] === null) {
- delete params[key];
- }
- });
- const response = await getExecLogList(params);
- this.logList = response.rows || [];
- this.total = response.total || 0;
- // 计算统计数据
- this.calculateStats();
- } catch (error) {
- console.error('获取日志列表失败', error);
- this.$message.error('获取日志列表失败');
- this.logList = [];
- this.total = 0;
- } finally {
- this.loading = false;
- }
- },
- calculateStats() {
- this.stats = {
- total: this.total,
- success: this.logList.filter(l => l.execStatus === 1).length,
- fail: this.logList.filter(l => l.execStatus === 2).length,
- timeout: this.logList.filter(l => l.execStatus === 3).length
- };
- },
- refreshList() {
- this.queryParams.pageNum = 1;
- this.getList();
- this.$message.success('刷新成功');
- },
- handleDateChange() {
- this.queryParams.pageNum = 1;
- this.getList();
- },
- handleStatusFilter(status) {
- this.queryParams.execStatus = this.queryParams.execStatus === status ? null : status;
- this.queryParams.pageNum = 1;
- this.getList();
- },
- // 查看日志详情
- async viewLogDetail(log) {
- this.currentLog = log;
- this.detailDrawerVisible = true;
- this.stepLogs = [];
- // 加载步骤执行日志
- try {
- const response = await getStepExecLog(log.execId);
- this.stepLogs = (response.data || []).sort((a, b) => a.stepIndex - b.stepIndex);
- } catch (error) {
- console.error('加载步骤日志失败', error);
- this.$message.warning('步骤日志加载失败');
- }
- },
- // 辅助方法
- getTriggerTypeName(type) {
- const map = {
- 'EVENT': '事件',
- 'TIME': '定时',
- 'MANUAL': '手动',
- 'CONDITION': '条件',
- 'ATTR': '属性' // ✅ 新增属性触发类型
- };
- return map[type] || type || '-';
- },
- getTriggerTagType(type) {
- const map = {
- 'EVENT': 'danger',
- 'TIME': '',
- 'MANUAL': 'info',
- 'CONDITION': 'success',
- 'ATTR': 'warning' // ✅ 新增属性触发类型
- };
- return map[type] || '';
- },
- getExecStatusType(status) {
- const map = { 0: 'info', 1: 'success', 2: 'danger', 3: 'warning' };
- return map[status] || '';
- },
- getExecStatusText(status) {
- const map = { 0: '执行中', 1: '成功', 2: '失败', 3: '超时' };
- return map[status] || '-';
- },
- getExecStatusIcon(status) {
- const map = { 0: 'el-icon-loading', 1: 'el-icon-success', 2: 'el-icon-error', 3: 'el-icon-time' };
- return map[status] || '';
- },
- getStepTimelineType(status) {
- const map = { 0: 'info', 1: 'success', 2: 'danger', 3: 'warning' };
- return map[status] || 'info';
- },
- getStepTimelineIcon(status) {
- const map = { 0: 'el-icon-loading', 1: 'el-icon-check', 2: 'el-icon-close', 3: 'el-icon-time' };
- return map[status] || 'el-icon-more';
- },
- getDurationClass(duration) {
- if (!duration) return '';
- if (duration > 10000) return 'duration-slow';
- if (duration > 5000) return 'duration-normal';
- return 'duration-fast';
- },
- formatDuration(ms) {
- if (!ms && ms !== 0) return '-';
- if (ms < 1000) return ms + 'ms';
- if (ms < 60000) return (ms / 1000).toFixed(1) + 's';
- return (ms / 60000).toFixed(1) + 'min';
- },
- parseTime(time) {
- if (!time) return '';
- return new Date(time).toLocaleString('zh-CN', { hour12: false });
- },
- formatJson(str) {
- if (!str) return '';
- try {
- const obj = typeof str === 'string' ? JSON.parse(str) : str;
- return JSON.stringify(obj, null, 2);
- } catch (e) {
- return str;
- }
- }
- }
- };
- </script>
- <style lang="scss" scoped>
- .strategy-log-container {
- min-height: 100vh;
- background: #f0f2f5;
- padding: 20px;
- }
- .page-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 20px;
- padding: 20px 24px;
- background: #fff;
- border-radius: 12px;
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
- .page-title {
- margin: 0;
- font-size: 20px;
- font-weight: 600;
- i {
- margin-right: 10px;
- color: #409eff;
- }
- }
- }
- .main-content {
- display: flex;
- gap: 20px;
- }
- .filter-panel {
- width: 260px;
- flex-shrink: 0;
- background: #fff;
- border-radius: 12px;
- padding: 20px;
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
- .filter-section {
- margin-bottom: 24px;
- &:last-child {
- margin-bottom: 0;
- }
- .filter-title {
- font-size: 14px;
- font-weight: 600;
- color: #303133;
- margin-bottom: 12px;
- }
- }
- .status-tags {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
- .status-tag {
- cursor: pointer;
- transition: all 0.2s;
- &:hover {
- transform: scale(1.05);
- }
- }
- }
- .stats-summary {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 12px;
- .stat-item {
- text-align: center;
- padding: 12px;
- background: #f5f7fa;
- border-radius: 8px;
- .stat-value {
- display: block;
- font-size: 24px;
- font-weight: 600;
- color: #303133;
- }
- .stat-label {
- font-size: 12px;
- color: #909399;
- }
- &.success .stat-value { color: #67c23a; }
- &.fail .stat-value { color: #f56c6c; }
- &.timeout .stat-value { color: #e6a23c; }
- }
- }
- }
- .log-panel {
- flex: 1;
- background: #fff;
- border-radius: 12px;
- padding: 20px;
- box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
- .strategy-info {
- .strategy-name {
- display: block;
- font-weight: 500;
- }
- .strategy-code {
- font-size: 12px;
- color: #909399;
- }
- }
- .duration-fast { color: #67c23a; }
- .duration-normal { color: #e6a23c; }
- .duration-slow { color: #f56c6c; }
- }
- .pagination-wrapper {
- display: flex;
- justify-content: flex-end;
- margin-top: 20px;
- }
- // 日志详情
- .log-detail {
- padding: 0 20px 20px;
- .detail-section {
- margin-bottom: 24px;
- .section-title {
- font-size: 16px;
- font-weight: 600;
- color: #303133;
- margin-bottom: 16px;
- padding-bottom: 8px;
- border-bottom: 1px solid #ebeef5;
- &.error-title {
- color: #f56c6c;
- }
- }
- }
- .error-content {
- padding: 12px 16px;
- background: #fef0f0;
- border-radius: 6px;
- color: #f56c6c;
- font-family: monospace;
- white-space: pre-wrap;
- word-break: break-all;
- }
- .step-log-item {
- .step-header {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 8px;
- .step-name {
- font-weight: 600;
- color: #303133;
- }
- .step-index {
- font-size: 12px;
- color: #909399;
- }
- .step-duration {
- margin-left: auto;
- font-size: 12px;
- color: #909399;
- }
- }
- .step-time {
- font-size: 12px;
- color: #909399;
- margin-bottom: 8px;
- }
- .step-error {
- padding: 8px 12px;
- background: #fef0f0;
- border-radius: 4px;
- font-size: 12px;
- color: #f56c6c;
- margin-top: 8px;
- .error-label {
- font-weight: 600;
- }
- }
- pre {
- background: #f5f7fa;
- padding: 12px;
- border-radius: 6px;
- font-size: 12px;
- overflow-x: auto;
- }
- }
- }
- </style>
|