|
@@ -7,13 +7,18 @@
|
|
|
.search-bar {
|
|
.search-bar {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
gap: var(--spacing-sm);
|
|
gap: var(--spacing-sm);
|
|
|
- flex-wrap: wrap;
|
|
|
|
|
|
|
+ flex-wrap: nowrap;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ min-width: 380px;
|
|
|
|
|
+ max-width: 420px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.search-bar .input-group {
|
|
.search-bar .input-group {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: stretch;
|
|
align-items: stretch;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ min-width: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.search-bar .form-select {
|
|
.search-bar .form-select {
|
|
@@ -134,6 +139,78 @@
|
|
|
margin-bottom: var(--spacing-md);
|
|
margin-bottom: var(--spacing-md);
|
|
|
opacity: 0.5;
|
|
opacity: 0.5;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ .error-indicator {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ width: 18px;
|
|
|
|
|
+ height: 18px;
|
|
|
|
|
+ background: linear-gradient(135deg, #dc3545, #c82333);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ font-size: 0.7rem;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ margin-left: 4px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ box-shadow: 0 2px 6px rgba(220, 53, 69, 0.3);
|
|
|
|
|
+ transition: all 0.2s ease;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ top: -1px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .error-indicator:hover {
|
|
|
|
|
+ transform: scale(1.2);
|
|
|
|
|
+ box-shadow: 0 3px 10px rgba(220, 53, 69, 0.5);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .error-popover {
|
|
|
|
|
+ display: none;
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ z-index: 1000;
|
|
|
|
|
+ background: var(--color-bg-card);
|
|
|
|
|
+ border: 1px solid rgba(220, 53, 69, 0.3);
|
|
|
|
|
+ border-radius: var(--radius-md);
|
|
|
|
|
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
|
|
|
|
+ padding: var(--spacing-md);
|
|
|
|
|
+ min-width: 280px;
|
|
|
|
|
+ max-width: 400px;
|
|
|
|
|
+ margin-top: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .error-popover.show {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .error-popover-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: var(--spacing-xs);
|
|
|
|
|
+ color: #dc3545;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ margin-bottom: var(--spacing-sm);
|
|
|
|
|
+ padding-bottom: var(--spacing-xs);
|
|
|
|
|
+ border-bottom: 1px solid rgba(220, 53, 69, 0.1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .error-popover-content {
|
|
|
|
|
+ font-size: 0.85rem;
|
|
|
|
|
+ color: var(--color-text-secondary);
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ white-space: pre-wrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .error-popover-arrow {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: -6px;
|
|
|
|
|
+ left: 12px;
|
|
|
|
|
+ width: 12px;
|
|
|
|
|
+ height: 12px;
|
|
|
|
|
+ background: var(--color-bg-card);
|
|
|
|
|
+ border-left: 1px solid rgba(220, 53, 69, 0.3);
|
|
|
|
|
+ border-top: 1px solid rgba(220, 53, 69, 0.3);
|
|
|
|
|
+ transform: rotate(45deg);
|
|
|
|
|
+ }
|
|
|
</style>
|
|
</style>
|
|
|
{% endblock %}
|
|
{% endblock %}
|
|
|
|
|
|
|
@@ -160,10 +237,10 @@
|
|
|
value="{{ search_name or '' }}">
|
|
value="{{ search_name or '' }}">
|
|
|
<button class="btn btn-primary" type="submit"><i class="bi bi-search"></i></button>
|
|
<button class="btn btn-primary" type="submit"><i class="bi bi-search"></i></button>
|
|
|
</div>
|
|
</div>
|
|
|
- {% if search_name %}
|
|
|
|
|
- <a href="{{ url_for('members') }}" class="btn btn-secondary">重置</a>
|
|
|
|
|
- {% endif %}
|
|
|
|
|
</form>
|
|
</form>
|
|
|
|
|
+ {% if search_name %}
|
|
|
|
|
+ <a href="{{ url_for('members') }}" class="btn btn-secondary">重置</a>
|
|
|
|
|
+ {% endif %}
|
|
|
<a href="{{ url_for('add_member') }}" class="btn btn-primary">
|
|
<a href="{{ url_for('add_member') }}" class="btn btn-primary">
|
|
|
<i class="bi bi-plus-lg"></i> 录入新成员
|
|
<i class="bi bi-plus-lg"></i> 录入新成员
|
|
|
</a>
|
|
</a>
|
|
@@ -201,10 +278,13 @@
|
|
|
<tbody>
|
|
<tbody>
|
|
|
{% for member in members %}
|
|
{% for member in members %}
|
|
|
<tr>
|
|
<tr>
|
|
|
- <td class="px-4 text-muted small">{{ loop.index + (page - 1) * 20 }}</td>
|
|
|
|
|
|
|
+ <td class="px-4 text-muted small">{{ loop.index + (page - 1) * per_page }}</td>
|
|
|
<td>
|
|
<td>
|
|
|
<div class="member-name">
|
|
<div class="member-name">
|
|
|
{{ member.name }}{% if member.simplified_name %}({{ member.simplified_name }}){% endif %}
|
|
{{ member.name }}{% if member.simplified_name %}({{ member.simplified_name }}){% endif %}
|
|
|
|
|
+ {% if member.suspected_error and member.suspected_error.strip() %}
|
|
|
|
|
+ <span class="error-indicator" data-error="{{ member.suspected_error.strip() | e }}">!</span>
|
|
|
|
|
+ {% endif %}
|
|
|
</div>
|
|
</div>
|
|
|
<div class="member-meta">
|
|
<div class="member-meta">
|
|
|
{{ '男' if member.sex == 1 else '女' }} | {{ member.birthday_str or '未知' }}
|
|
{{ '男' if member.sex == 1 else '女' }} | {{ member.birthday_str or '未知' }}
|
|
@@ -375,5 +455,68 @@ function confirmDelete(id, name) {
|
|
|
form.submit();
|
|
form.submit();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+let currentPopover = null;
|
|
|
|
|
+
|
|
|
|
|
+document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
|
+ document.querySelectorAll('.error-indicator').forEach(indicator => {
|
|
|
|
|
+ indicator.addEventListener('mouseenter', function(e) {
|
|
|
|
|
+ showErrorPopover(this, e);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ indicator.addEventListener('mouseleave', function() {
|
|
|
|
|
+ hideErrorPopover();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ indicator.addEventListener('click', function(e) {
|
|
|
|
|
+ e.stopPropagation();
|
|
|
|
|
+ if (currentPopover && currentPopover.dataset.visible === 'true') {
|
|
|
|
|
+ hideErrorPopover();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ showErrorPopover(this, e);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ document.addEventListener('click', function(e) {
|
|
|
|
|
+ if (currentPopover && !currentPopover.contains(e.target) && !e.target.classList.contains('error-indicator')) {
|
|
|
|
|
+ hideErrorPopover();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+function showErrorPopover(indicator, event) {
|
|
|
|
|
+ hideErrorPopover();
|
|
|
|
|
+
|
|
|
|
|
+ const errorText = indicator.dataset.error;
|
|
|
|
|
+ if (!errorText) return;
|
|
|
|
|
+
|
|
|
|
|
+ const popover = document.createElement('div');
|
|
|
|
|
+ popover.className = 'error-popover show';
|
|
|
|
|
+ popover.dataset.visible = 'true';
|
|
|
|
|
+
|
|
|
|
|
+ popover.innerHTML = `
|
|
|
|
|
+ <div class="error-popover-arrow"></div>
|
|
|
|
|
+ <div class="error-popover-header">
|
|
|
|
|
+ <i class="bi bi-exclamation-circle"></i>
|
|
|
|
|
+ <span>疑似错误</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="error-popover-content">${errorText}</div>
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ const rect = indicator.getBoundingClientRect();
|
|
|
|
|
+ popover.style.left = rect.left + 'px';
|
|
|
|
|
+ popover.style.top = (rect.bottom + 8) + 'px';
|
|
|
|
|
+
|
|
|
|
|
+ document.body.appendChild(popover);
|
|
|
|
|
+ currentPopover = popover;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function hideErrorPopover() {
|
|
|
|
|
+ if (currentPopover) {
|
|
|
|
|
+ currentPopover.remove();
|
|
|
|
|
+ currentPopover = null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
</script>
|
|
</script>
|
|
|
{% endblock %}
|
|
{% endblock %}
|