You've already forked think-plugs-recorder
recorder
This commit is contained in:
227
view/recorder/components/data_operators.html
Normal file
227
view/recorder/components/data_operators.html
Normal file
@@ -0,0 +1,227 @@
|
||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-size: 14px; line-height: 1.5; color: #262626;">
|
||||
<div class="recorder-operators {$options.css_class|default=''}" style="background: #ffffff; border: 1px solid #e6e6e6; border-radius: 6px; padding: 15px;">
|
||||
{if isset($operators) && !empty($operators)}
|
||||
<div style="margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #f0f0f0;">
|
||||
<h5 style="margin: 0; color: #333; font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 6px;">
|
||||
<i class="layui-icon layui-icon-user" style="color: #52c41a; font-size: 16px;"></i>
|
||||
操作用户 ({$operators|count})
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 15px;">
|
||||
{volist name="operators" id="operator"}
|
||||
<div class="recorder-operator recorder-slide-in" data-user-id="{$operator.user_id}"
|
||||
style="display: flex; align-items: flex-start; padding: 15px; background: #fafafa; border-radius: 6px; border-left: 3px solid #52c41a; transition: all 0.3s ease; cursor: pointer;"
|
||||
onmouseover="this.style.background='#f6ffed'; this.style.borderLeftColor='#73d13d'; this.style.boxShadow='0 2px 8px rgba(82, 196, 26, 0.1)'; this.style.transform='translateX(2px)';"
|
||||
onmouseout="this.style.background='#fafafa'; this.style.borderLeftColor='#52c41a'; this.style.boxShadow='none'; this.style.transform='translateX(0)';">
|
||||
<div style="width: 40px; height: 40px; background: linear-gradient(135deg, #52c41a, #73d13d); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-right: 15px; color: #ffffff; flex-shrink: 0; box-shadow: 0 2px 4px rgba(82, 196, 26, 0.2);">
|
||||
<i class="layui-icon layui-icon-user" style="font-size: 18px;"></i>
|
||||
</div>
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div style="font-size: 14px; font-weight: 600; color: #333; margin-bottom: 4px;">{$operator.user_nickname}</div>
|
||||
<div style="font-size: 12px; color: #52c41a; font-weight: 500; margin-bottom: 8px; padding: 2px 6px; background: rgba(82, 196, 26, 0.1); border-radius: 3px; display: inline-block;">{$operator.operation_summary}</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 15px; font-size: 12px; color: #666; margin-bottom: 6px;">
|
||||
<span style="display: flex; align-items: center; color: #52c41a; font-weight: 500;">
|
||||
<i class="layui-icon layui-icon-edit" style="font-size: 12px; margin-right: 3px;"></i>
|
||||
操作 {$operator.total_operations} 次
|
||||
</span>
|
||||
<span style="display: flex; align-items: center;">
|
||||
<i class="layui-icon layui-icon-time" style="font-size: 12px; margin-right: 3px;"></i>
|
||||
最后: {$operator.last_operation_at_relative}
|
||||
</span>
|
||||
</div>
|
||||
<div style="color: #999; font-size: 11px; line-height: 1.4; margin-bottom: 8px;">
|
||||
<small>
|
||||
首次: {$operator.first_operation_at_formatted}
|
||||
| 最后: {$operator.last_operation_at_formatted}
|
||||
</small>
|
||||
</div>
|
||||
{if isset($operator.operations) && !empty($operator.operations)}
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 4px; margin-top: 8px;">
|
||||
{volist name="operator.operations" id="op"}
|
||||
<span class="operation-tag operation-tag--{$op.type}"
|
||||
style="font-size: 10px; padding: 2px 6px; border-radius: 3px; font-weight: 500; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); cursor: pointer;
|
||||
{switch name="op.type"}
|
||||
{case value="创建"}background: #e6f7ff; color: #1890ff;{/case}
|
||||
{case value="更新"}background: #fff7e6; color: #fa8c16;{/case}
|
||||
{case value="删除"}background: #fff2f0; color: #f5222d;{/case}
|
||||
{case value="导出"}background: #f6ffed; color: #52c41a;{/case}
|
||||
{case value="审核"}background: #f9f0ff; color: #722ed1;{/case}
|
||||
{case value="发布"}background: #e6fffb; color: #13c2c2;{/case}
|
||||
{default}background: #f0f0f0; color: #666;{/case}
|
||||
{/switch}"
|
||||
onmouseover="this.style.transform='scale(1.05)';"
|
||||
onmouseout="this.style.transform='scale(1)';">
|
||||
{$op.type} ({$op.count})
|
||||
</span>
|
||||
{/volist}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/volist}
|
||||
</div>
|
||||
{else}
|
||||
<div style="text-align: center; padding: 50px 20px; color: #999;">
|
||||
<i class="layui-icon layui-icon-face-cry" style="font-size: 48px; margin-bottom: 12px; color: #ddd; display: block;"></i>
|
||||
<p style="margin: 0; font-size: 14px;">暂无用户操作过此数据</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes recorderSlideIn {
|
||||
from { opacity: 0; transform: translateX(-20px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
.recorder-slide-in {
|
||||
animation: recorderSlideIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.recorder-operators {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.recorder-operator {
|
||||
padding: 12px !important;
|
||||
flex-direction: column !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.recorder-operator > div:first-child {
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.recorder-operator > div:last-child > div:nth-child(3) {
|
||||
flex-direction: column !important;
|
||||
gap: 4px !important;
|
||||
}
|
||||
|
||||
.recorder-operator > div:last-child > div:last-child {
|
||||
margin-top: 6px !important;
|
||||
justify-content: center !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 操作用户点击处理
|
||||
document.addEventListener('click', function(e) {
|
||||
var operator = e.target.closest('.recorder-operator');
|
||||
if (operator) {
|
||||
// 移除其他选中状态
|
||||
document.querySelectorAll('.recorder-operator.selected').forEach(function(el) {
|
||||
el.classList.remove('selected');
|
||||
el.style.background = '#fafafa';
|
||||
el.style.borderLeftColor = '#52c41a';
|
||||
});
|
||||
|
||||
// 添加选中状态
|
||||
operator.classList.add('selected');
|
||||
operator.style.background = '#f6ffed';
|
||||
operator.style.borderLeftColor = '#73d13d';
|
||||
|
||||
// 触发自定义事件
|
||||
var event = new CustomEvent('recorderOperatorSelect', {
|
||||
detail: {
|
||||
userId: operator.dataset.userId,
|
||||
element: operator
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
|
||||
// 操作标签点击处理
|
||||
var operationTag = e.target.closest('.operation-tag');
|
||||
if (operationTag) {
|
||||
e.stopPropagation(); // 阻止冒泡到operator
|
||||
|
||||
var operationType = operationTag.textContent.split('(')[0].trim();
|
||||
|
||||
// 触发筛选事件
|
||||
var event = new CustomEvent('recorderOperationFilter', {
|
||||
detail: {
|
||||
operationType: operationType,
|
||||
element: operationTag
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
// 为新添加的元素添加动画
|
||||
function addAnimation() {
|
||||
var operators = document.querySelectorAll('.recorder-operator:not(.animated)');
|
||||
operators.forEach(function(operator, index) {
|
||||
operator.style.animationDelay = (index * 150) + 'ms';
|
||||
operator.classList.add('recorder-slide-in', 'animated');
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化动画
|
||||
setTimeout(addAnimation, 100);
|
||||
|
||||
// 响应式处理
|
||||
function handleResize() {
|
||||
var isMobile = window.innerWidth <= 768;
|
||||
var operators = document.querySelectorAll('.recorder-operator');
|
||||
|
||||
operators.forEach(function(operator) {
|
||||
if (isMobile) {
|
||||
operator.style.padding = '12px';
|
||||
operator.style.flexDirection = 'column';
|
||||
operator.style.textAlign = 'center';
|
||||
|
||||
var avatar = operator.querySelector('div:first-child');
|
||||
if (avatar) {
|
||||
avatar.style.marginRight = '0';
|
||||
avatar.style.marginBottom = '8px';
|
||||
}
|
||||
|
||||
var details = operator.querySelector('div:last-child > div:nth-child(3)');
|
||||
if (details) {
|
||||
details.style.flexDirection = 'column';
|
||||
details.style.gap = '4px';
|
||||
}
|
||||
|
||||
var tags = operator.querySelector('div:last-child > div:last-child');
|
||||
if (tags) {
|
||||
tags.style.marginTop = '6px';
|
||||
tags.style.justifyContent = 'center';
|
||||
}
|
||||
} else {
|
||||
operator.style.padding = '15px';
|
||||
operator.style.flexDirection = 'row';
|
||||
operator.style.textAlign = 'left';
|
||||
|
||||
var avatar = operator.querySelector('div:first-child');
|
||||
if (avatar) {
|
||||
avatar.style.marginRight = '15px';
|
||||
avatar.style.marginBottom = '0';
|
||||
}
|
||||
|
||||
var details = operator.querySelector('div:last-child > div:nth-child(3)');
|
||||
if (details) {
|
||||
details.style.flexDirection = 'row';
|
||||
details.style.gap = '15px';
|
||||
}
|
||||
|
||||
var tags = operator.querySelector('div:last-child > div:last-child');
|
||||
if (tags) {
|
||||
tags.style.marginTop = '8px';
|
||||
tags.style.justifyContent = 'flex-start';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleResize();
|
||||
})();
|
||||
</script>
|
180
view/recorder/components/data_readers.html
Normal file
180
view/recorder/components/data_readers.html
Normal file
@@ -0,0 +1,180 @@
|
||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-size: 14px; line-height: 1.5; color: #262626;">
|
||||
<div class="recorder-readers {$options.css_class|default=''}" style="background: #ffffff; border: 1px solid #e6e6e6; border-radius: 6px; padding: 15px;">
|
||||
{if isset($readers) && !empty($readers)}
|
||||
<div style="margin-bottom: 15px; padding-bottom: 10px; border-bottom: 1px solid #f0f0f0;">
|
||||
<h5 style="margin: 0; color: #333; font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 6px;">
|
||||
<i class="layui-icon layui-icon-username" style="color: #1890ff; font-size: 16px;"></i>
|
||||
读取用户 ({$readers|count})
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; flex-direction: column; gap: 12px;">
|
||||
{volist name="readers" id="reader"}
|
||||
<div class="recorder-reader recorder-slide-in" data-user-id="{$reader.user_id}"
|
||||
style="display: flex; align-items: flex-start; padding: 12px; background: #fafafa; border-radius: 6px; border-left: 3px solid #1890ff; transition: all 0.3s ease; cursor: pointer;"
|
||||
onmouseover="this.style.background='#f0f9ff'; this.style.borderLeftColor='#40a9ff'; this.style.transform='translateX(2px)';"
|
||||
onmouseout="this.style.background='#fafafa'; this.style.borderLeftColor='#1890ff'; this.style.transform='translateX(0)';">
|
||||
<div style="width: 36px; height: 36px; background: linear-gradient(135deg, #1890ff, #40a9ff); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin-right: 12px; color: #ffffff; flex-shrink: 0; box-shadow: 0 2px 4px rgba(24, 144, 255, 0.2);">
|
||||
<i class="layui-icon layui-icon-username" style="font-size: 16px;"></i>
|
||||
</div>
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<div style="font-size: 14px; font-weight: 600; color: #333; margin-bottom: 6px;">{$reader.user_nickname}</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 12px; font-size: 12px; color: #666; margin-bottom: 4px;">
|
||||
<span style="display: flex; align-items: center; color: #1890ff; font-weight: 500;">
|
||||
<i class="layui-icon layui-icon-read" style="font-size: 12px; margin-right: 2px;"></i>
|
||||
读取 {$reader.read_count} 次
|
||||
</span>
|
||||
{if $reader.first_read_at != $reader.last_read_at}
|
||||
<span style="display: flex; align-items: center;">
|
||||
<i class="layui-icon layui-icon-time" style="font-size: 12px; margin-right: 2px;"></i>
|
||||
最后: {$reader.last_read_at_relative}
|
||||
</span>
|
||||
{else/}
|
||||
<span style="display: flex; align-items: center;">
|
||||
<i class="layui-icon layui-icon-time" style="font-size: 12px; margin-right: 2px;"></i>
|
||||
{$reader.first_read_at_relative}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div style="color: #999; font-size: 11px; line-height: 1.4;">
|
||||
<small>
|
||||
首次: {$reader.first_read_at_formatted}
|
||||
{if $reader.first_read_at != $reader.last_read_at}
|
||||
| 最后: {$reader.last_read_at_formatted}
|
||||
{/if}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/volist}
|
||||
</div>
|
||||
{else}
|
||||
<div style="text-align: center; padding: 50px 20px; color: #999;">
|
||||
<i class="layui-icon layui-icon-face-cry" style="font-size: 48px; margin-bottom: 12px; color: #ddd; display: block;"></i>
|
||||
<p style="margin: 0; font-size: 14px;">暂无用户读取过此数据</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes recorderSlideIn {
|
||||
from { opacity: 0; transform: translateX(-20px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
.recorder-slide-in {
|
||||
animation: recorderSlideIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.recorder-readers {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.recorder-reader {
|
||||
padding: 10px !important;
|
||||
flex-direction: column !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.recorder-reader > div:first-child {
|
||||
margin-right: 0 !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
|
||||
.recorder-reader > div:last-child > div:nth-child(2) {
|
||||
flex-direction: column !important;
|
||||
gap: 4px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 读取用户点击处理
|
||||
document.addEventListener('click', function(e) {
|
||||
var reader = e.target.closest('.recorder-reader');
|
||||
if (reader) {
|
||||
// 移除其他选中状态
|
||||
document.querySelectorAll('.recorder-reader.selected').forEach(function(el) {
|
||||
el.classList.remove('selected');
|
||||
el.style.background = '#fafafa';
|
||||
el.style.borderLeftColor = '#1890ff';
|
||||
});
|
||||
|
||||
// 添加选中状态
|
||||
reader.classList.add('selected');
|
||||
reader.style.background = '#e6f7ff';
|
||||
reader.style.borderLeftColor = '#40a9ff';
|
||||
|
||||
// 触发自定义事件
|
||||
var event = new CustomEvent('recorderReaderSelect', {
|
||||
detail: {
|
||||
userId: reader.dataset.userId,
|
||||
element: reader
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
// 为新添加的元素添加动画
|
||||
function addAnimation() {
|
||||
var readers = document.querySelectorAll('.recorder-reader:not(.animated)');
|
||||
readers.forEach(function(reader, index) {
|
||||
reader.style.animationDelay = (index * 100) + 'ms';
|
||||
reader.classList.add('recorder-slide-in', 'animated');
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化动画
|
||||
setTimeout(addAnimation, 100);
|
||||
|
||||
// 响应式处理
|
||||
function handleResize() {
|
||||
var isMobile = window.innerWidth <= 768;
|
||||
var readers = document.querySelectorAll('.recorder-reader');
|
||||
|
||||
readers.forEach(function(reader) {
|
||||
if (isMobile) {
|
||||
reader.style.padding = '10px';
|
||||
reader.style.flexDirection = 'column';
|
||||
reader.style.textAlign = 'center';
|
||||
|
||||
var avatar = reader.querySelector('div:first-child');
|
||||
if (avatar) {
|
||||
avatar.style.marginRight = '0';
|
||||
avatar.style.marginBottom = '8px';
|
||||
}
|
||||
|
||||
var details = reader.querySelector('div:last-child > div:nth-child(2)');
|
||||
if (details) {
|
||||
details.style.flexDirection = 'column';
|
||||
details.style.gap = '4px';
|
||||
}
|
||||
} else {
|
||||
reader.style.padding = '12px';
|
||||
reader.style.flexDirection = 'row';
|
||||
reader.style.textAlign = 'left';
|
||||
|
||||
var avatar = reader.querySelector('div:first-child');
|
||||
if (avatar) {
|
||||
avatar.style.marginRight = '12px';
|
||||
avatar.style.marginBottom = '0';
|
||||
}
|
||||
|
||||
var details = reader.querySelector('div:last-child > div:nth-child(2)');
|
||||
if (details) {
|
||||
details.style.flexDirection = 'row';
|
||||
details.style.gap = '12px';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleResize();
|
||||
})();
|
||||
</script>
|
301
view/recorder/components/record_item_default.html
Normal file
301
view/recorder/components/record_item_default.html
Normal file
@@ -0,0 +1,301 @@
|
||||
<div class="recorder-item-single {$options.css_class|default=''}" data-id="{$record.id|default=''}">
|
||||
{if isset($record) && !empty($record)}
|
||||
<div class="recorder-item-single__header">
|
||||
<div class="recorder-item-single__type-wrapper">
|
||||
<span class="recorder-item-single__type recorder-item-single__type--{$record.operation_type_label.class}">
|
||||
<i class="layui-icon layui-icon-{$record.operation_type_label.icon|default='circle'}"></i>
|
||||
{$record.operation_type_label.text}
|
||||
</span>
|
||||
</div>
|
||||
<div class="recorder-item-single__time-wrapper">
|
||||
<div class="recorder-item-single__time" title="{$record.created_at_formatted|default=''}">
|
||||
{$record.created_at_relative|default=''}
|
||||
</div>
|
||||
<div class="recorder-item-single__datetime">
|
||||
{$record.created_at_formatted|default=''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recorder-item-single__content">
|
||||
<div class="recorder-item-single__desc">
|
||||
{$record.operation_desc|default=''}
|
||||
</div>
|
||||
|
||||
<div class="recorder-item-single__meta">
|
||||
{if $options.show_user|default=true && !empty($record.user_info)}
|
||||
<div class="recorder-item-single__meta-item">
|
||||
<i class="layui-icon layui-icon-username"></i>
|
||||
<span class="recorder-item-single__meta-label">操作人:</span>
|
||||
<span class="recorder-item-single__meta-value">{$record.user_info}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{if !empty($record.data_info)}
|
||||
<div class="recorder-item-single__meta-item">
|
||||
<i class="layui-icon layui-icon-template"></i>
|
||||
<span class="recorder-item-single__meta-label">操作对象:</span>
|
||||
<span class="recorder-item-single__meta-value">{$record.data_info}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{if !empty($record.related_info)}
|
||||
<div class="recorder-item-single__meta-item">
|
||||
<i class="layui-icon layui-icon-link"></i>
|
||||
<span class="recorder-item-single__meta-label">关联数据:</span>
|
||||
<span class="recorder-item-single__meta-value">{$record.related_info}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{if $options.show_ip|default=false && !empty($record.request_ip)}
|
||||
<div class="recorder-item-single__meta-item">
|
||||
<i class="layui-icon layui-icon-location"></i>
|
||||
<span class="recorder-item-single__meta-label">IP地址:</span>
|
||||
<span class="recorder-item-single__meta-value">{$record.request_ip}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{if !empty($record.request_url)}
|
||||
<div class="recorder-item-single__meta-item">
|
||||
<i class="layui-icon layui-icon-link"></i>
|
||||
<span class="recorder-item-single__meta-label">请求地址:</span>
|
||||
<span class="recorder-item-single__meta-value recorder-item-single__meta-url" title="{$record.request_url}">
|
||||
{$record.request_url}
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{if $options.show_extra|default=false && !empty($record.extra_data)}
|
||||
<div class="recorder-item-single__extra">
|
||||
<details>
|
||||
<summary>
|
||||
<i class="layui-icon layui-icon-more"></i>
|
||||
查看详细信息
|
||||
</summary>
|
||||
<div class="recorder-item-single__extra-content">
|
||||
<pre>{:json_encode($record.extra_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)}</pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{else}
|
||||
<div class="recorder-item-single__empty">
|
||||
<i class="layui-icon layui-icon-face-cry"></i>
|
||||
<p>记录不存在</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.recorder-item-single {
|
||||
background: #fff;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.recorder-item-single__header {
|
||||
padding: 12px 15px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.recorder-item-single__type {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 16px;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recorder-item-single__type .layui-icon {
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.recorder-item-single__type--success { background-color: #52c41a; }
|
||||
.recorder-item-single__type--info { background-color: #1890ff; }
|
||||
.recorder-item-single__type--warning { background-color: #faad14; }
|
||||
.recorder-item-single__type--danger { background-color: #f5222d; }
|
||||
.recorder-item-single__type--primary { background-color: #722ed1; }
|
||||
.recorder-item-single__type--default { background-color: #8c8c8c; }
|
||||
|
||||
.recorder-item-single__time-wrapper {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.recorder-item-single__time {
|
||||
font-size: 12px;
|
||||
color: #1890ff;
|
||||
background: #e6f7ff;
|
||||
padding: 2px 6px;
|
||||
border-radius: 10px;
|
||||
display: inline-block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.recorder-item-single__datetime {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.recorder-item-single__content {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.recorder-item-single__desc {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.recorder-item-single__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.recorder-item-single__meta-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.recorder-item-single__meta-item .layui-icon {
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
margin-top: 2px;
|
||||
color: #999;
|
||||
width: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.recorder-item-single__meta-label {
|
||||
margin-right: 6px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
min-width: 60px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.recorder-item-single__meta-value {
|
||||
color: #333;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.recorder-item-single__meta-url {
|
||||
word-break: break-all;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.recorder-item-single__extra {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding-top: 15px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.recorder-item-single__extra details {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.recorder-item-single__extra summary {
|
||||
color: #1890ff;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.recorder-item-single__extra summary:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
|
||||
.recorder-item-single__extra summary .layui-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.recorder-item-single__extra-content {
|
||||
margin-top: 8px;
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #1890ff;
|
||||
}
|
||||
|
||||
.recorder-item-single__extra-content pre {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
line-height: 1.4;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.recorder-item-single__empty {
|
||||
padding: 60px 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.recorder-item-single__empty .layui-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.recorder-item-single__empty p {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 紧凑模式 */
|
||||
.recorder-item-single.recorder-item-single--compact .recorder-item-single__content {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.recorder-item-single.recorder-item-single--compact .recorder-item-single__meta {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.recorder-item-single.recorder-item-single--compact .recorder-item-single__meta-item {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.recorder-item-single__header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.recorder-item-single__time-wrapper {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.recorder-item-single__meta-label {
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.recorder-item-single__meta-url {
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
</style>
|
200
view/recorder/components/record_list_card.html
Normal file
200
view/recorder/components/record_list_card.html
Normal file
@@ -0,0 +1,200 @@
|
||||
<div class="recorder-list recorder-list--card {$options.css_class|default=''}">
|
||||
{if isset($records) && !empty($records)}
|
||||
<div class="recorder-cards">
|
||||
{volist name="records" id="record"}
|
||||
<div class="recorder-card" data-id="{$record.id}">
|
||||
<div class="recorder-card__header">
|
||||
<div class="recorder-card__type recorder-card__type--{$record.operation_type_label.class}">
|
||||
<i class="layui-icon layui-icon-{$record.operation_type_label.icon|default='circle'}"></i>
|
||||
{$record.operation_type_label.text}
|
||||
</div>
|
||||
<div class="recorder-card__time">
|
||||
{$record.created_at_relative}
|
||||
</div>
|
||||
</div>
|
||||
<div class="recorder-card__body">
|
||||
<div class="recorder-card__desc">{$record.operation_desc}</div>
|
||||
<div class="recorder-card__meta">
|
||||
{if $options.show_user && !empty($record.user_info)}
|
||||
<div class="recorder-card__meta-item">
|
||||
<i class="layui-icon layui-icon-username"></i>
|
||||
<span>{$record.user_info}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{if !empty($record.data_info)}
|
||||
<div class="recorder-card__meta-item">
|
||||
<i class="layui-icon layui-icon-template"></i>
|
||||
<span>{$record.data_info}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{if $options.show_ip && !empty($record.request_ip)}
|
||||
<div class="recorder-card__meta-item">
|
||||
<i class="layui-icon layui-icon-location"></i>
|
||||
<span>{$record.request_ip}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="recorder-card__footer">
|
||||
<time class="recorder-card__datetime" datetime="{$record.created_at}">
|
||||
{$record.created_at_formatted}
|
||||
</time>
|
||||
{if !empty($record.related_info)}
|
||||
<div class="recorder-card__related">
|
||||
关联: {$record.related_info}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/volist}
|
||||
</div>
|
||||
{else}
|
||||
<div class="recorder-empty recorder-empty--card">
|
||||
<i class="layui-icon layui-icon-face-cry"></i>
|
||||
<p>暂无操作记录</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.recorder-list--card {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.recorder-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.recorder-cards {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.recorder-card {
|
||||
background: #fff;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.recorder-card:hover {
|
||||
border-color: #1890ff;
|
||||
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.15);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.recorder-card__header {
|
||||
padding: 12px 15px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.recorder-card__type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 16px;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.recorder-card__type .layui-icon {
|
||||
font-size: 12px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.recorder-card__type--success { background-color: #52c41a; }
|
||||
.recorder-card__type--info { background-color: #1890ff; }
|
||||
.recorder-card__type--warning { background-color: #faad14; }
|
||||
.recorder-card__type--danger { background-color: #f5222d; }
|
||||
.recorder-card__type--primary { background-color: #722ed1; }
|
||||
.recorder-card__type--default { background-color: #8c8c8c; }
|
||||
|
||||
.recorder-card__time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.recorder-card__body {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.recorder-card__desc {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.recorder-card__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.recorder-card__meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.recorder-card__meta-item .layui-icon {
|
||||
font-size: 12px;
|
||||
margin-right: 6px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.recorder-card__footer {
|
||||
padding: 10px 15px;
|
||||
background: #fafafa;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.recorder-card__datetime {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.recorder-card__related {
|
||||
max-width: 150px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.recorder-empty--card {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 60px 0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.recorder-empty--card .layui-icon {
|
||||
font-size: 64px;
|
||||
margin-bottom: 15px;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.recorder-empty--card p {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
206
view/recorder/components/record_list_default.html
Normal file
206
view/recorder/components/record_list_default.html
Normal file
@@ -0,0 +1,206 @@
|
||||
<div class="recorder-container" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-size: 14px; line-height: 1.5; color: #262626;">
|
||||
<div class="recorder-list {$options.css_class|default=''} {$options.compact ? 'recorder-list--compact' : ''}" style="margin: 0; padding: 0; background: transparent;">
|
||||
{if isset($records) && !empty($records)}
|
||||
{volist name="records" id="record"}
|
||||
<div class="recorder-item {$options.theme == 'card' ? 'recorder-item--card' : ''} recorder-fade-in" data-id="{$record.id}"
|
||||
style="background: #ffffff; border: 1px solid #e6e6e6; border-radius: 6px; margin-bottom: 12px; padding: 16px; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); position: relative; overflow: hidden; {$options.theme == 'card' ? 'border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.06); border: none;' : ''}"
|
||||
onmouseover="this.style.borderColor='#1890ff'; this.style.boxShadow='0 2px 8px rgba(24, 144, 255, 0.1)'; this.style.transform='translateY(-1px)';"
|
||||
onmouseout="this.style.borderColor='#e6e6e6'; this.style.boxShadow='none'; this.style.transform='translateY(0)';">
|
||||
|
||||
{if $options.theme == 'card'}
|
||||
<div style="position: absolute; top: 0; left: 0; right: 0; height: 3px; background: linear-gradient(90deg, #1890ff, #52c41a);"></div>
|
||||
{/if}
|
||||
|
||||
<div class="recorder-item__header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; padding-bottom: 8px; border-bottom: 1px solid #f0f0f0;">
|
||||
<span class="recorder-item__type recorder-item__type--{$record.operation_type_label.class}"
|
||||
style="font-size: 10px; padding: 4px 8px; border-radius: 12px; color: #ffffff; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
{switch name="record.operation_type_label.class"}
|
||||
{case value="success"}background: linear-gradient(135deg, #52c41a, #73d13d);{/case}
|
||||
{case value="info"}background: linear-gradient(135deg, #1890ff, #40a9ff);{/case}
|
||||
{case value="warning"}background: linear-gradient(135deg, #faad14, #ffc53d);{/case}
|
||||
{case value="danger"}background: linear-gradient(135deg, #f5222d, #ff7875);{/case}
|
||||
{case value="primary"}background: linear-gradient(135deg, #722ed1, #9254de);{/case}
|
||||
{default}background: linear-gradient(135deg, #8c8c8c, #a6a6a6);{/case}
|
||||
{/switch}">
|
||||
{$record.operation_type_label.text}
|
||||
</span>
|
||||
<span class="recorder-item__time" title="{$record.created_at_formatted}" style="font-size: 12px; color: #8c8c8c; font-weight: 400;">
|
||||
{$record.created_at_relative}
|
||||
</span>
|
||||
</div>
|
||||
<div class="recorder-item__content" style="font-size: 12px; line-height: 1.6;">
|
||||
<div class="recorder-item__desc" style="color: #262626; font-weight: 500; margin-bottom: 4px; font-size: 14px;">{$record.operation_desc}</div>
|
||||
{if $options.show_user && !empty($record.user_info)}
|
||||
<div class="recorder-item__user" style="font-size: 12px; color: #8c8c8c; margin-bottom: 4px; display: flex; align-items: center; gap: 4px;">
|
||||
<i class="layui-icon layui-icon-username" style="font-size: 12px; color: #1890ff;"></i> {$record.user_info}
|
||||
</div>
|
||||
{/if}
|
||||
{if !empty($record.data_info)}
|
||||
<div class="recorder-item__data" style="font-size: 12px; color: #8c8c8c; margin-bottom: 4px; display: flex; align-items: center; gap: 4px;">
|
||||
<i class="layui-icon layui-icon-template" style="font-size: 12px; color: #1890ff;"></i> {$record.data_info}
|
||||
</div>
|
||||
{/if}
|
||||
{if !empty($record.related_info)}
|
||||
<div class="recorder-item__related" style="font-size: 12px; color: #8c8c8c; margin-bottom: 4px; display: flex; align-items: center; gap: 4px;">
|
||||
<i class="layui-icon layui-icon-link" style="font-size: 12px; color: #1890ff;"></i> 关联:{$record.related_info}
|
||||
</div>
|
||||
{/if}
|
||||
{if $options.show_ip && !empty($record.request_ip)}
|
||||
<div class="recorder-item__ip" style="font-size: 12px; color: #8c8c8c; margin-bottom: 4px; display: flex; align-items: center; gap: 4px;">
|
||||
<i class="layui-icon layui-icon-location" style="font-size: 12px; color: #1890ff;"></i> {$record.request_ip}
|
||||
</div>
|
||||
{/if}
|
||||
{if $options.show_extra && !empty($record.extra_data)}
|
||||
<div class="recorder-item__extra" style="margin-top: 8px; font-size: 12px;">
|
||||
<i class="layui-icon layui-icon-more"></i>
|
||||
<details style="margin-top: 4px; border: 1px solid #f0f0f0; border-radius: 4px; overflow: hidden;">
|
||||
<summary style="color: #1890ff; cursor: pointer; outline: none; padding: 8px; background: #fafafa; font-weight: 500; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);"
|
||||
onmouseover="this.style.background='#f5f5f5';"
|
||||
onmouseout="this.style.background='#fafafa';">额外信息</summary>
|
||||
<pre style="background: #f5f5f5; padding: 12px; margin: 0; font-size: 10px; max-height: 200px; overflow-y: auto; border: none; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;">{:json_encode($record.extra_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)}</pre>
|
||||
</details>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/volist}
|
||||
{else}
|
||||
<div class="recorder-empty" style="text-align: center; padding: 20px; color: #8c8c8c; background: #fafafa; border-radius: 6px;">
|
||||
<i class="layui-icon layui-icon-face-cry" style="font-size: 48px; margin-bottom: 12px; color: #d9d9d9; display: block;"></i>
|
||||
<p style="margin: 0; font-size: 14px; color: #8c8c8c;">暂无操作记录</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes recorderFadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.recorder-fade-in {
|
||||
animation: recorderFadeIn 0.3s ease-in-out;
|
||||
}
|
||||
{if $options.compact}
|
||||
.recorder-list--compact .recorder-item {
|
||||
padding: 8px 12px !important;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
.recorder-list--compact .recorder-item__header {
|
||||
margin-bottom: 4px !important;
|
||||
padding-bottom: 4px !important;
|
||||
}
|
||||
.recorder-list--compact .recorder-item__desc {
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
.recorder-list--compact .recorder-item__user,
|
||||
.recorder-list--compact .recorder-item__data,
|
||||
.recorder-list--compact .recorder-item__related {
|
||||
display: none !important;
|
||||
}
|
||||
{/if}
|
||||
@media (max-width: 768px) {
|
||||
.recorder-item {
|
||||
padding: 12px !important;
|
||||
margin-bottom: 8px !important;
|
||||
}
|
||||
.recorder-item__header {
|
||||
flex-direction: column !important;
|
||||
align-items: flex-start !important;
|
||||
gap: 4px !important;
|
||||
}
|
||||
.recorder-item__type {
|
||||
margin-bottom: 4px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 记录项点击处理
|
||||
document.addEventListener('click', function(e) {
|
||||
var item = e.target.closest('.recorder-item');
|
||||
if (item) {
|
||||
// 移除其他选中状态
|
||||
document.querySelectorAll('.recorder-item.selected').forEach(function(el) {
|
||||
el.classList.remove('selected');
|
||||
el.style.borderColor = '#e6e6e6';
|
||||
el.style.backgroundColor = '#ffffff';
|
||||
});
|
||||
|
||||
// 添加选中状态
|
||||
item.classList.add('selected');
|
||||
item.style.borderColor = '#1890ff';
|
||||
item.style.backgroundColor = '#f0f9ff';
|
||||
|
||||
// 触发自定义事件
|
||||
var event = new CustomEvent('recorderItemSelect', {
|
||||
detail: {
|
||||
recordId: item.dataset.id,
|
||||
element: item
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
// 详情展开/收起处理
|
||||
document.addEventListener('toggle', function(e) {
|
||||
if (e.target.tagName === 'DETAILS') {
|
||||
var summary = e.target.querySelector('summary');
|
||||
if (e.target.open) {
|
||||
summary.style.color = '#1890ff';
|
||||
} else {
|
||||
summary.style.color = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 为新添加的元素添加动画
|
||||
function addAnimation() {
|
||||
var items = document.querySelectorAll('.recorder-item:not(.animated)');
|
||||
items.forEach(function(item, index) {
|
||||
item.style.animationDelay = (index * 50) + 'ms';
|
||||
item.classList.add('recorder-fade-in', 'animated');
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化动画
|
||||
setTimeout(addAnimation, 100);
|
||||
|
||||
// 响应式处理
|
||||
function handleResize() {
|
||||
var isMobile = window.innerWidth <= 768;
|
||||
var items = document.querySelectorAll('.recorder-item');
|
||||
|
||||
items.forEach(function(item) {
|
||||
if (isMobile) {
|
||||
item.style.padding = '12px';
|
||||
item.style.marginBottom = '8px';
|
||||
|
||||
var header = item.querySelector('.recorder-item__header');
|
||||
if (header) {
|
||||
header.style.flexDirection = 'column';
|
||||
header.style.alignItems = 'flex-start';
|
||||
header.style.gap = '4px';
|
||||
}
|
||||
} else {
|
||||
item.style.padding = '16px';
|
||||
item.style.marginBottom = '12px';
|
||||
|
||||
var header = item.querySelector('.recorder-item__header');
|
||||
if (header) {
|
||||
header.style.flexDirection = 'row';
|
||||
header.style.alignItems = 'center';
|
||||
header.style.gap = '0';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleResize();
|
||||
})();
|
||||
</script>
|
245
view/recorder/components/record_timeline.html
Normal file
245
view/recorder/components/record_timeline.html
Normal file
@@ -0,0 +1,245 @@
|
||||
<div style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; font-size: 14px; line-height: 1.5; color: #262626;">
|
||||
<div class="recorder-timeline {$options.css_class|default=''} {$options.compact ? 'recorder-timeline--compact' : ''}" style="position: relative; padding: 0; margin: 0;">
|
||||
{if isset($records) && !empty($records)}
|
||||
<div style="position: relative; padding-left: 30px;">
|
||||
<!-- 时间线主线 -->
|
||||
<div style="content: ''; position: absolute; left: 15px; top: 0; bottom: 0; width: 2px; background: linear-gradient(to bottom, #e6e6e6 0%, #e6e6e6 100%); z-index: 1;"></div>
|
||||
|
||||
{volist name="records" id="record"}
|
||||
<div class="recorder-timeline__item recorder-fade-in" style="position: relative; padding-bottom: {$options.compact ? '16px' : '24px'}; margin-bottom: {$options.compact ? '12px' : '16px'};">
|
||||
<!-- 时间线标记点 -->
|
||||
<div style="position: absolute; left: -22px; top: 4px; width: 16px; height: 16px; border-radius: 50%; border: 2px solid #fff; display: flex; align-items: center; justify-content: center; z-index: 2; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
{switch name="record.operation_type_label.class"}
|
||||
{case value="success"}background-color: #52c41a;{/case}
|
||||
{case value="info"}background-color: #1890ff;{/case}
|
||||
{case value="warning"}background-color: #faad14;{/case}
|
||||
{case value="danger"}background-color: #f5222d;{/case}
|
||||
{case value="primary"}background-color: #722ed1;{/case}
|
||||
{default}background-color: #8c8c8c;{/case}
|
||||
{/switch}">
|
||||
<i class="layui-icon layui-icon-{$record.operation_type_label.icon|default='circle'}" style="font-size: 10px; color: #fff;"></i>
|
||||
</div>
|
||||
|
||||
<!-- 时间线内容 -->
|
||||
<div style="background: #fff; border: 1px solid #e6e6e6; border-radius: 6px; padding: 0; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); transition: all 0.3s ease; cursor: pointer;"
|
||||
onmouseover="this.style.borderColor='#d9d9d9'; this.style.boxShadow='0 2px 6px rgba(0, 0, 0, 0.1)';"
|
||||
onmouseout="this.style.borderColor='#e6e6e6'; this.style.boxShadow='0 1px 3px rgba(0, 0, 0, 0.05)';">
|
||||
|
||||
<!-- 头部 -->
|
||||
<div style="padding: 12px 15px; background: #fafafa; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center;">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<span style="font-weight: 500; font-size: 13px; color: #333;">{$record.operation_type_label.text}</span>
|
||||
<span style="font-size: 12px; color: #1890ff; background: #e6f7ff; padding: 2px 6px; border-radius: 10px;">{$record.created_at_relative}</span>
|
||||
</div>
|
||||
<div style="font-size: 11px; color: #999;" title="{$record.created_at_formatted}">{$record.created_at_formatted}</div>
|
||||
</div>
|
||||
|
||||
<!-- 主体内容 -->
|
||||
<div style="padding: {$options.compact ? '10px 15px' : '15px'};">
|
||||
<div style="font-size: 14px; color: #333; margin-bottom: 12px; line-height: 1.5;">{$record.operation_desc}</div>
|
||||
|
||||
{if !$options.compact}
|
||||
<div style="display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px;">
|
||||
{if $options.show_user && !empty($record.user_info)}
|
||||
<div style="display: flex; align-items: center; font-size: 12px; color: #666;">
|
||||
<i class="layui-icon layui-icon-username" style="font-size: 12px; margin-right: 6px; color: #999; width: 14px;"></i>
|
||||
<span style="margin-right: 4px; font-weight: 500;">操作人:</span>
|
||||
<span style="color: #333;">{$record.user_info}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{if !empty($record.data_info)}
|
||||
<div style="display: flex; align-items: center; font-size: 12px; color: #666;">
|
||||
<i class="layui-icon layui-icon-template" style="font-size: 12px; margin-right: 6px; color: #999; width: 14px;"></i>
|
||||
<span style="margin-right: 4px; font-weight: 500;">操作对象:</span>
|
||||
<span style="color: #333;">{$record.data_info}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{if !empty($record.related_info)}
|
||||
<div style="display: flex; align-items: center; font-size: 12px; color: #666;">
|
||||
<i class="layui-icon layui-icon-link" style="font-size: 12px; margin-right: 6px; color: #999; width: 14px;"></i>
|
||||
<span style="margin-right: 4px; font-weight: 500;">关联数据:</span>
|
||||
<span style="color: #333;">{$record.related_info}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{if $options.show_ip && !empty($record.request_ip)}
|
||||
<div style="display: flex; align-items: center; font-size: 12px; color: #666;">
|
||||
<i class="layui-icon layui-icon-location" style="font-size: 12px; margin-right: 6px; color: #999; width: 14px;"></i>
|
||||
<span style="margin-right: 4px; font-weight: 500;">IP地址:</span>
|
||||
<span style="color: #333;">{$record.request_ip}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{if $options.show_extra && !empty($record.extra_data)}
|
||||
<div style="border-top: 1px solid #f0f0f0; padding-top: 12px; margin-top: 12px;">
|
||||
<details style="font-size: 12px;">
|
||||
<summary style="color: #1890ff; cursor: pointer; outline: none; display: flex; align-items: center; padding: 4px 0;"
|
||||
onmouseover="this.style.color='#40a9ff';"
|
||||
onmouseout="this.style.color='#1890ff';">
|
||||
<i class="layui-icon layui-icon-more" style="margin-right: 4px;"></i>
|
||||
查看详细信息
|
||||
</summary>
|
||||
<div style="margin-top: 8px; padding: 8px; background: #f5f5f5; border-radius: 4px;">
|
||||
<pre style="margin: 0; font-size: 11px; max-height: 200px; overflow-y: auto; font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;">{:json_encode($record.extra_data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)}</pre>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/volist}
|
||||
</div>
|
||||
{else}
|
||||
<div style="position: relative; padding-left: 30px; text-align: center;">
|
||||
<div style="position: absolute; left: 7px; top: 50%; transform: translateY(-50%); width: 16px; height: 16px; border-radius: 50%; background-color: #ddd; border: 2px solid #fff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);"></div>
|
||||
<div style="padding: 40px 20px; color: #999;">
|
||||
<i class="layui-icon layui-icon-face-cry" style="font-size: 48px; margin-bottom: 10px; color: #ddd; display: block;"></i>
|
||||
<p style="margin: 0; font-size: 14px;">暂无操作记录</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes recorderFadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
.recorder-fade-in {
|
||||
animation: recorderFadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* 最后一个时间线项目的特殊处理 */
|
||||
.recorder-timeline__item:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.recorder-timeline {
|
||||
font-size: 13px !important;
|
||||
}
|
||||
|
||||
.recorder-timeline__item > div:nth-child(2) > div:first-child {
|
||||
flex-direction: column !important;
|
||||
align-items: flex-start !important;
|
||||
gap: 6px !important;
|
||||
}
|
||||
|
||||
.recorder-timeline__item > div:nth-child(2) > div:first-child > div:last-child {
|
||||
align-self: flex-end !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// 时间线项目点击处理
|
||||
document.addEventListener('click', function(e) {
|
||||
var timelineItem = e.target.closest('.recorder-timeline__item');
|
||||
if (timelineItem) {
|
||||
var content = timelineItem.querySelector('div:nth-child(2)');
|
||||
if (content) {
|
||||
// 移除其他选中状态
|
||||
document.querySelectorAll('.recorder-timeline__item.selected').forEach(function(el) {
|
||||
var itemContent = el.querySelector('div:nth-child(2)');
|
||||
if (itemContent) {
|
||||
el.classList.remove('selected');
|
||||
itemContent.style.borderColor = '#e6e6e6';
|
||||
itemContent.style.backgroundColor = '#fff';
|
||||
}
|
||||
});
|
||||
|
||||
// 添加选中状态
|
||||
timelineItem.classList.add('selected');
|
||||
content.style.borderColor = '#1890ff';
|
||||
content.style.backgroundColor = '#f0f9ff';
|
||||
|
||||
// 触发自定义事件
|
||||
var event = new CustomEvent('recorderTimelineSelect', {
|
||||
detail: {
|
||||
element: timelineItem,
|
||||
content: content
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 详情展开/收起处理
|
||||
document.addEventListener('toggle', function(e) {
|
||||
if (e.target.tagName === 'DETAILS') {
|
||||
var summary = e.target.querySelector('summary');
|
||||
if (e.target.open) {
|
||||
summary.style.color = '#1890ff';
|
||||
} else {
|
||||
summary.style.color = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 时间线标记hover提示
|
||||
document.addEventListener('mouseover', function(e) {
|
||||
var marker = e.target.closest('.recorder-timeline__item > div:first-child');
|
||||
if (marker) {
|
||||
var item = marker.closest('.recorder-timeline__item');
|
||||
var typeText = item.querySelector('div:nth-child(2) span:first-child');
|
||||
var timeText = item.querySelector('div:nth-child(2) span:nth-child(2)');
|
||||
|
||||
if (typeText && timeText) {
|
||||
marker.setAttribute('title', typeText.textContent + ' - ' + timeText.textContent);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 为新添加的元素添加动画
|
||||
function addAnimation() {
|
||||
var items = document.querySelectorAll('.recorder-timeline__item:not(.animated)');
|
||||
items.forEach(function(item, index) {
|
||||
item.style.animationDelay = (index * 100) + 'ms';
|
||||
item.classList.add('recorder-fade-in', 'animated');
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化动画
|
||||
setTimeout(addAnimation, 100);
|
||||
|
||||
// 响应式处理
|
||||
function handleResize() {
|
||||
var isMobile = window.innerWidth <= 768;
|
||||
var headers = document.querySelectorAll('.recorder-timeline__item > div:nth-child(2) > div:first-child');
|
||||
|
||||
headers.forEach(function(header) {
|
||||
if (isMobile) {
|
||||
header.style.flexDirection = 'column';
|
||||
header.style.alignItems = 'flex-start';
|
||||
header.style.gap = '6px';
|
||||
|
||||
var datetime = header.querySelector('div:last-child');
|
||||
if (datetime) {
|
||||
datetime.style.alignSelf = 'flex-end';
|
||||
}
|
||||
} else {
|
||||
header.style.flexDirection = 'row';
|
||||
header.style.alignItems = 'center';
|
||||
header.style.gap = '12px';
|
||||
|
||||
var datetime = header.querySelector('div:last-child');
|
||||
if (datetime) {
|
||||
datetime.style.alignSelf = 'auto';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleResize();
|
||||
})();
|
||||
</script>
|
Reference in New Issue
Block a user