mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-06-18 20:10:13 +02:00
Add files via upload
This commit is contained in:
+132
-1
@@ -11153,6 +11153,124 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.section-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.conversation-sort-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.conversation-sort-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.conversation-sort-btn:hover,
|
||||
.conversation-sort-btn[aria-expanded="true"] {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.conversation-sort-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
right: 0;
|
||||
z-index: 200;
|
||||
min-width: 156px;
|
||||
padding: 4px;
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.conversation-sort-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 7px 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
text-align: left;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.conversation-sort-option:hover {
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.conversation-sort-option.is-selected {
|
||||
background: rgba(0, 102, 255, 0.08);
|
||||
color: var(--accent-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.conversation-sort-option.is-selected:hover {
|
||||
background: rgba(0, 102, 255, 0.12);
|
||||
}
|
||||
|
||||
.conversation-sort-option-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 7px;
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--text-muted);
|
||||
transition: background 0.15s ease, color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.conversation-sort-option:hover .conversation-sort-option-icon {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.conversation-sort-option.is-selected .conversation-sort-option-icon {
|
||||
background: rgba(0, 102, 255, 0.12);
|
||||
color: var(--accent-color);
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 102, 255, 0.16);
|
||||
}
|
||||
|
||||
.conversation-sort-option-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.conversation-sort-option-check {
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
color: var(--accent-color);
|
||||
opacity: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.conversation-sort-option.is-selected .conversation-sort-option-check {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
@@ -13818,10 +13936,23 @@ tr.mcp-stats-tool-row[data-tool-name]:focus-visible {
|
||||
}
|
||||
|
||||
.batch-task-header .batch-task-edit-btn {
|
||||
/* 仅把“编辑”(首个操作按钮)推到最右,避免多个按钮都 margin-left:auto 导致挤压/错位 */
|
||||
/* 仅把「编辑」(首个操作按钮)推到最右,避免多个按钮都 margin-left:auto 导致挤压/错位 */
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.batch-task-header .batch-task-edit-btn--push {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.batch-task-header .batch-task-run-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.batch-task-header .batch-task-run-btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.batch-task-header .batch-task-delete-btn {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@@ -436,6 +436,9 @@
|
||||
"conversationGroups": "Conversation groups",
|
||||
"addGroup": "New group",
|
||||
"recentConversations": "Recent conversations",
|
||||
"sortConversations": "Sort",
|
||||
"sortByCreatedAt": "Created time",
|
||||
"sortByUpdatedAt": "Updated time",
|
||||
"batchManage": "Batch manage",
|
||||
"paginationShow": "Show {{start}}-{{end}} of {{total}}",
|
||||
"paginationRange": "{{start}}-{{end}}/{{total}}",
|
||||
@@ -676,7 +679,12 @@
|
||||
"viewConversation": "View conversation",
|
||||
"viewVulnerabilities": "View vulnerabilities",
|
||||
"viewVulnerabilitiesQueueTitle": "View vulnerabilities: open management filtered to this queue",
|
||||
"retryTask": "Retry",
|
||||
"runSingleTask": "Run task",
|
||||
"confirmRunSingleTask": "Run this task only? The queue will pause when it finishes and will not continue other pending items.",
|
||||
"runSingleTaskFailed": "Failed to run task",
|
||||
"runSingleTaskUnavailable": "Unavailable while the queue or a task is running",
|
||||
"runSingleTaskUnavailableSelf": "This task is running",
|
||||
"runSingleTaskUnavailableQueue": "Queue is running; pause it before running another task individually",
|
||||
"conversationIdLabel": "Conversation ID",
|
||||
"statusPending": "Pending",
|
||||
"statusPaused": "Paused",
|
||||
|
||||
@@ -424,6 +424,9 @@
|
||||
"conversationGroups": "对话分组",
|
||||
"addGroup": "新建分组",
|
||||
"recentConversations": "最近对话",
|
||||
"sortConversations": "排序",
|
||||
"sortByCreatedAt": "创建时间",
|
||||
"sortByUpdatedAt": "更新时间",
|
||||
"batchManage": "批量管理",
|
||||
"paginationShow": "显示 {{start}}-{{end}} / 共 {{total}}",
|
||||
"paginationRange": "{{start}}-{{end}}/{{total}}",
|
||||
@@ -664,7 +667,12 @@
|
||||
"viewConversation": "查看对话",
|
||||
"viewVulnerabilities": "查看漏洞",
|
||||
"viewVulnerabilitiesQueueTitle": "查看漏洞:打开漏洞管理并筛选本队列",
|
||||
"retryTask": "重试",
|
||||
"runSingleTask": "单条执行",
|
||||
"confirmRunSingleTask": "确定执行该任务?仅运行这一条,完成后队列会自动暂停,不会继续执行其他待执行项。",
|
||||
"runSingleTaskFailed": "单条执行失败",
|
||||
"runSingleTaskUnavailable": "队列或任务执行中,暂无法单条执行",
|
||||
"runSingleTaskUnavailableSelf": "该任务正在执行中",
|
||||
"runSingleTaskUnavailableQueue": "队列批量执行中,请暂停后再单条执行其它任务",
|
||||
"conversationIdLabel": "对话ID",
|
||||
"statusPending": "待执行",
|
||||
"statusPaused": "已暂停",
|
||||
|
||||
+98
-9
@@ -5763,6 +5763,95 @@ let conversationGroupMappingCache = {};
|
||||
let pendingGroupMappings = {}; // 待保留的分组映射(用于处理后端API延迟的情况)
|
||||
let conversationsListLoadSeq = 0; // 对话列表加载序号,避免并发请求导致重复渲染
|
||||
const CONVERSATIONS_PAGE_SIZE_KEY = 'cyberstrike.conversations_page_size';
|
||||
const CONVERSATIONS_SORT_KEY = 'cyberstrike.conversations_sort_by';
|
||||
|
||||
function getConversationSortBy() {
|
||||
try {
|
||||
const saved = localStorage.getItem(CONVERSATIONS_SORT_KEY);
|
||||
if (saved === 'created_at' || saved === 'updated_at') return saved;
|
||||
} catch (e) { /* ignore */ }
|
||||
return 'updated_at';
|
||||
}
|
||||
|
||||
let conversationSortBy = getConversationSortBy();
|
||||
|
||||
function getConversationSortTime(conv) {
|
||||
const field = conversationSortBy === 'created_at' ? 'createdAt' : 'updatedAt';
|
||||
const raw = conv && conv[field];
|
||||
if (!raw) return new Date(0);
|
||||
const date = new Date(raw);
|
||||
return isNaN(date.getTime()) ? new Date(0) : date;
|
||||
}
|
||||
|
||||
function updateConversationSortMenuUI() {
|
||||
const menu = document.getElementById('conversation-sort-menu');
|
||||
const btn = document.getElementById('conversation-sort-btn');
|
||||
if (!menu) return;
|
||||
menu.querySelectorAll('.conversation-sort-option').forEach((option) => {
|
||||
const selected = option.dataset.sort === conversationSortBy;
|
||||
option.classList.toggle('is-selected', selected);
|
||||
option.setAttribute('aria-checked', selected ? 'true' : 'false');
|
||||
});
|
||||
if (btn) {
|
||||
btn.setAttribute('aria-expanded', menu.hidden ? 'false' : 'true');
|
||||
}
|
||||
}
|
||||
|
||||
function closeConversationSortMenu() {
|
||||
const menu = document.getElementById('conversation-sort-menu');
|
||||
const btn = document.getElementById('conversation-sort-btn');
|
||||
if (menu) menu.hidden = true;
|
||||
if (btn) btn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
function toggleConversationSortMenu(event) {
|
||||
if (event) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
const menu = document.getElementById('conversation-sort-menu');
|
||||
const btn = document.getElementById('conversation-sort-btn');
|
||||
if (!menu || !btn) return;
|
||||
const willOpen = menu.hidden;
|
||||
closeConversationSortMenu();
|
||||
if (willOpen) {
|
||||
menu.hidden = false;
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
updateConversationSortMenuUI();
|
||||
}
|
||||
}
|
||||
|
||||
function setConversationSortBy(sortBy) {
|
||||
const next = sortBy === 'created_at' ? 'created_at' : 'updated_at';
|
||||
if (next === conversationSortBy) {
|
||||
closeConversationSortMenu();
|
||||
return;
|
||||
}
|
||||
conversationSortBy = next;
|
||||
try {
|
||||
localStorage.setItem(CONVERSATIONS_SORT_KEY, next);
|
||||
} catch (e) { /* ignore */ }
|
||||
updateConversationSortMenuUI();
|
||||
closeConversationSortMenu();
|
||||
conversationsPagination.page = 1;
|
||||
loadConversationsWithGroups(conversationsSearchQuery);
|
||||
}
|
||||
|
||||
if (!window.__conversationSortMenuBound) {
|
||||
window.__conversationSortMenuBound = true;
|
||||
document.addEventListener('click', (event) => {
|
||||
const dropdown = document.getElementById('conversation-sort-dropdown');
|
||||
if (!dropdown || dropdown.contains(event.target)) return;
|
||||
closeConversationSortMenu();
|
||||
});
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') closeConversationSortMenu();
|
||||
});
|
||||
}
|
||||
|
||||
window.toggleConversationSortMenu = toggleConversationSortMenu;
|
||||
window.setConversationSortBy = setConversationSortBy;
|
||||
window.closeConversationSortMenu = closeConversationSortMenu;
|
||||
|
||||
function getConversationsPageSize() {
|
||||
try {
|
||||
@@ -6025,6 +6114,9 @@ async function loadConversationsWithGroups(searchQuery = '') {
|
||||
const pageSize = conversationsPagination.pageSize;
|
||||
const offset = (conversationsPagination.page - 1) * pageSize;
|
||||
const convParams = new URLSearchParams({ limit: String(pageSize), offset: String(offset) });
|
||||
if (conversationSortBy === 'created_at') {
|
||||
convParams.set('sort_by', 'created_at');
|
||||
}
|
||||
if (searchQuery && searchQuery.trim()) {
|
||||
convParams.set('search', searchQuery.trim());
|
||||
} else {
|
||||
@@ -6114,11 +6206,7 @@ async function loadConversationsWithGroups(searchQuery = '') {
|
||||
});
|
||||
|
||||
// 按时间排序
|
||||
const sortByTime = (a, b) => {
|
||||
const timeA = a.updatedAt ? new Date(a.updatedAt) : new Date(0);
|
||||
const timeB = b.updatedAt ? new Date(b.updatedAt) : new Date(0);
|
||||
return timeB - timeA;
|
||||
};
|
||||
const sortByTime = (a, b) => getConversationSortTime(b) - getConversationSortTime(a);
|
||||
|
||||
pinnedConvs.sort(sortByTime);
|
||||
normalConvs.sort(sortByTime);
|
||||
@@ -6146,8 +6234,8 @@ async function loadConversationsWithGroups(searchQuery = '') {
|
||||
};
|
||||
|
||||
normalConvs.forEach(conv => {
|
||||
const dateObj = conv.updatedAt ? new Date(conv.updatedAt) : new Date();
|
||||
const validDate = isNaN(dateObj.getTime()) ? new Date() : dateObj;
|
||||
const dateObj = getConversationSortTime(conv);
|
||||
const validDate = dateObj.getTime() === 0 ? new Date() : dateObj;
|
||||
const groupKey = getConversationGroup(validDate, todayStart, sevenDaysCutoff, yesterdayStart);
|
||||
groups[groupKey].push({
|
||||
...conv,
|
||||
@@ -6159,8 +6247,8 @@ async function loadConversationsWithGroups(searchQuery = '') {
|
||||
|
||||
if (pinnedConvs.length > 0) {
|
||||
pinnedConvs.forEach(conv => {
|
||||
const dateObj = conv.updatedAt ? new Date(conv.updatedAt) : new Date();
|
||||
const validDate = isNaN(dateObj.getTime()) ? new Date() : dateObj;
|
||||
const dateObj = getConversationSortTime(conv);
|
||||
const validDate = dateObj.getTime() === 0 ? new Date() : dateObj;
|
||||
fragment.appendChild(createConversationListItemWithMenu({
|
||||
...conv,
|
||||
_timeText: formatConversationTimestamp(validDate, todayStart, yesterdayStart),
|
||||
@@ -8508,6 +8596,7 @@ function clearGroupSearch() {
|
||||
|
||||
// 初始化时加载分组
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
updateConversationSortMenuUI();
|
||||
await loadGroups();
|
||||
await loadConversationsWithGroups();
|
||||
|
||||
|
||||
+30
-26
@@ -83,6 +83,21 @@ function batchQueueAllowsSubtaskMutation(queue) {
|
||||
return queue.status === 'pending' || queue.status === 'paused' || queue.status === 'completed' || queue.status === 'cancelled';
|
||||
}
|
||||
|
||||
/** 是否允许对指定子任务发起单条执行(与后端 queueAllowsSingleTaskRunLocked 对齐) */
|
||||
function batchQueueCanRunSingleTask(queue, task) {
|
||||
if (!queue || !task) return false;
|
||||
if (task.status === 'running') return false;
|
||||
if (queue.status === 'running') return false;
|
||||
return queue.status === 'pending' || queue.status === 'paused' || queue.status === 'completed' || queue.status === 'cancelled';
|
||||
}
|
||||
|
||||
function batchQueueRunSingleTaskDisabledReason(queue, task) {
|
||||
if (!queue || !task) return _t('tasks.runSingleTaskUnavailable');
|
||||
if (task.status === 'running') return _t('tasks.runSingleTaskUnavailableSelf');
|
||||
if (queue.status === 'running') return _t('tasks.runSingleTaskUnavailableQueue');
|
||||
return _t('tasks.runSingleTaskUnavailable');
|
||||
}
|
||||
|
||||
// HTML转义函数(如果未定义)
|
||||
if (typeof escapeHtml === 'undefined') {
|
||||
function escapeHtml(text) {
|
||||
@@ -1497,6 +1512,8 @@ async function showBatchQueueDetail(queueId) {
|
||||
${queue.tasks.map((task, index) => {
|
||||
const taskStatus = taskStatusMap[task.status] || { text: task.status, class: 'batch-task-status-unknown' };
|
||||
const canEdit = allowSubtaskMutation && task.status !== 'running';
|
||||
const canRunSingle = batchQueueCanRunSingleTask(queue, task);
|
||||
const runSingleUnavailableTitle = escapeHtml(batchQueueRunSingleTaskDisabledReason(queue, task));
|
||||
const taskMessageEscaped = escapeHtml(task.message).replace(/'/g, "'").replace(/"/g, """).replace(/\n/g, "\\n");
|
||||
return `
|
||||
<div class="batch-task-item ${task.status === 'running' ? 'batch-task-item-active' : ''}" data-queue-id="${queue.id}" data-task-id="${task.id}" data-task-message="${taskMessageEscaped}">
|
||||
@@ -1504,10 +1521,10 @@ async function showBatchQueueDetail(queueId) {
|
||||
<span class="batch-task-index">#${index + 1}</span>
|
||||
<span class="batch-task-status ${taskStatus.class}">${taskStatus.text}</span>
|
||||
<span class="batch-task-message" title="${escapeHtml(task.message)}">${escapeHtml(task.message)}</span>
|
||||
<button class="btn-secondary btn-small batch-task-run-btn" ${canRunSingle ? `onclick="runSingleBatchTask('${queue.id}', '${task.id}'); event.stopPropagation();"` : `disabled title="${runSingleUnavailableTitle}"`}>` + _t('tasks.runSingleTask') + `</button>
|
||||
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewBatchTaskConversation('${task.conversationId}'); event.stopPropagation();">` + _t('tasks.viewConversation') + `</button>` : ''}
|
||||
${canEdit ? `<button class="btn-secondary btn-small batch-task-edit-btn" onclick="editBatchTaskFromElement(this); event.stopPropagation();">` + _t('common.edit') + `</button>` : ''}
|
||||
${canEdit ? `<button class="btn-secondary btn-small btn-danger batch-task-delete-btn" onclick="deleteBatchTaskFromElement(this); event.stopPropagation();">` + _t('common.delete') + `</button>` : ''}
|
||||
${allowSubtaskMutation && task.status === 'failed' ? `<button class="btn-secondary btn-small" onclick="retryBatchTask('${queue.id}', '${task.id}'); event.stopPropagation();">` + _t('tasks.retryTask') + `</button>` : ''}
|
||||
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewBatchTaskConversation('${task.conversationId}'); event.stopPropagation();">` + _t('tasks.viewConversation') + `</button>` : ''}
|
||||
</div>
|
||||
${task.startedAt ? `<div class="batch-task-time">` + _t('batchQueueDetailModal.startLabel') + `: ${new Date(task.startedAt).toLocaleString()}</div>` : ''}
|
||||
${task.completedAt ? `<div class="batch-task-time">` + _t('batchQueueDetailModal.completeLabel') + `: ${new Date(task.completedAt).toLocaleString()}</div>` : ''}
|
||||
@@ -2270,38 +2287,25 @@ async function saveInlineAgentMode() {
|
||||
}
|
||||
}
|
||||
|
||||
// --- 重试失败任务 ---
|
||||
async function retryBatchTask(queueId, taskId) {
|
||||
// --- 单条执行 ---
|
||||
async function runSingleBatchTask(queueId, taskId) {
|
||||
if (!queueId || !taskId) return;
|
||||
if (!confirm(_t('tasks.confirmRunSingleTask'))) return;
|
||||
try {
|
||||
// 获取任务消息
|
||||
const detailResp = await apiFetch(`/api/batch-tasks/${queueId}`);
|
||||
if (!detailResp.ok) throw new Error(_t('tasks.getQueueDetailFailed'));
|
||||
const detail = await detailResp.json();
|
||||
const task = detail.queue.tasks.find(t => t.id === taskId);
|
||||
if (!task) throw new Error(_t('tasks.taskNotFound') || 'Task not found');
|
||||
const message = task.message;
|
||||
|
||||
// 先添加新任务(pending),再删除旧任务 — 避免先删后加失败导致任务丢失
|
||||
const addResp = await apiFetch(`/api/batch-tasks/${queueId}/tasks`, {
|
||||
const response = await apiFetch(`/api/batch-tasks/${queueId}/tasks/${taskId}/run`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message }),
|
||||
});
|
||||
if (!addResp.ok) {
|
||||
const r = await addResp.json().catch(() => ({}));
|
||||
throw new Error(r.error || _t('tasks.addTaskFailed'));
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || _t('tasks.runSingleTaskFailed'));
|
||||
}
|
||||
// 新任务添加成功后才删除旧任务
|
||||
const delResp = await apiFetch(`/api/batch-tasks/${queueId}/tasks/${taskId}`, { method: 'DELETE' });
|
||||
if (!delResp.ok) {
|
||||
// 删除失败不阻塞(新任务已添加,旧任务保留也不影响)
|
||||
console.warn('删除旧任务失败,但新任务已添加');
|
||||
if (result.autoStarted === false && result.message) {
|
||||
alert(result.message);
|
||||
}
|
||||
showBatchQueueDetail(queueId);
|
||||
refreshBatchQueues();
|
||||
} catch (e) {
|
||||
console.error('重试任务失败:', e);
|
||||
console.error('单条执行失败:', e);
|
||||
alert(e.message);
|
||||
}
|
||||
}
|
||||
@@ -2437,7 +2441,7 @@ window.startInlineEditRole = startInlineEditRole;
|
||||
window.saveInlineRole = saveInlineRole;
|
||||
window.startInlineEditAgentMode = startInlineEditAgentMode;
|
||||
window.saveInlineAgentMode = saveInlineAgentMode;
|
||||
window.retryBatchTask = retryBatchTask;
|
||||
window.runSingleBatchTask = runSingleBatchTask;
|
||||
window.startInlineEditSchedule = startInlineEditSchedule;
|
||||
window.toggleInlineScheduleCron = toggleInlineScheduleCron;
|
||||
window.saveInlineSchedule = saveInlineSchedule;
|
||||
|
||||
+43
-10
@@ -808,16 +808,49 @@
|
||||
<div class="recent-conversations-section">
|
||||
<div class="section-header">
|
||||
<span class="section-title" data-i18n="chat.recentConversations">最近对话</span>
|
||||
<button class="batch-manage-btn" onclick="showBatchManageModal()" data-i18n="chat.batchManage" data-i18n-attr="title" data-i18n-skip-text="true" title="批量管理">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="3" y1="12" x2="21" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="3" y1="18" x2="21" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="8" cy="6" r="1" fill="currentColor"/>
|
||||
<circle cx="8" cy="12" r="1" fill="currentColor"/>
|
||||
<circle cx="8" cy="18" r="1" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="section-header-actions">
|
||||
<div class="conversation-sort-dropdown" id="conversation-sort-dropdown">
|
||||
<button type="button" class="conversation-sort-btn" id="conversation-sort-btn" onclick="toggleConversationSortMenu(event)" aria-haspopup="menu" aria-expanded="false" aria-controls="conversation-sort-menu" data-i18n="chat.sortConversations" data-i18n-attr="title" data-i18n-skip-text="true" title="排序">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M3 6h18M7 12h10M10 18h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="conversation-sort-menu" id="conversation-sort-menu" role="menu" hidden>
|
||||
<button type="button" class="conversation-sort-option" role="menuitemradio" data-sort="created_at" onclick="setConversationSortBy('created_at')">
|
||||
<span class="conversation-sort-option-icon" aria-hidden="true">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="5" width="18" height="16" rx="2.5" stroke="currentColor" stroke-width="1.75"/>
|
||||
<path d="M3 10h18" stroke="currentColor" stroke-width="1.75"/>
|
||||
<path d="M8 3v3M16 3v3" stroke="currentColor" stroke-width="1.75" stroke-linecap="round"/>
|
||||
<circle cx="12" cy="15" r="1.75" fill="currentColor"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="conversation-sort-option-label" data-i18n="chat.sortByCreatedAt">创建时间</span>
|
||||
<span class="conversation-sort-option-check" aria-hidden="true">✓</span>
|
||||
</button>
|
||||
<button type="button" class="conversation-sort-option" role="menuitemradio" data-sort="updated_at" onclick="setConversationSortBy('updated_at')">
|
||||
<span class="conversation-sort-option-icon" aria-hidden="true">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="8" stroke="currentColor" stroke-width="1.75"/>
|
||||
<path d="M12 8v4.5l3 2" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="conversation-sort-option-label" data-i18n="chat.sortByUpdatedAt">更新时间</span>
|
||||
<span class="conversation-sort-option-check" aria-hidden="true">✓</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="batch-manage-btn" onclick="showBatchManageModal()" data-i18n="chat.batchManage" data-i18n-attr="title" data-i18n-skip-text="true" title="批量管理">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<line x1="3" y1="12" x2="21" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="3" y1="18" x2="21" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<circle cx="8" cy="6" r="1" fill="currentColor"/>
|
||||
<circle cx="8" cy="12" r="1" fill="currentColor"/>
|
||||
<circle cx="8" cy="18" r="1" fill="currentColor"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="conversations-list" class="conversations-list"></div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user