feat: add sql execution, keyword search, and generic export

This commit is contained in:
lx1056758714-glitch
2025-12-26 14:46:22 +08:00
parent f5e1bc54e9
commit 1c5c08868f
6 changed files with 462 additions and 106 deletions

View File

@@ -134,11 +134,18 @@ func (s *Service) GetTables(group, file string) ([]string, error) {
return s.db.GetTables(group, file)
}
func (s *Service) GetTableData(group, file, table string, limit, offset int) ([]map[string]interface{}, error) {
func (s *Service) GetTableData(group, file, table string, limit, offset int, keyword string) ([]map[string]interface{}, error) {
if s.db == nil {
return nil, nil
}
return s.db.GetTableData(group, file, table, limit, offset)
return s.db.GetTableData(group, file, table, limit, offset, keyword)
}
func (s *Service) ExecuteSQL(group, file, query string) ([]map[string]interface{}, error) {
if s.db == nil {
return nil, nil
}
return s.db.ExecuteSQL(group, file, query)
}
func (s *Service) initWebhook() error {

View File

@@ -64,6 +64,7 @@ func (s *Service) initAPIRouter() {
api.GET("/db", s.handleGetDBs)
api.GET("/db/tables", s.handleGetDBTables)
api.GET("/db/data", s.handleGetDBTableData)
api.GET("/db/query", s.handleExecuteSQL)
api.POST("/cache/clear", s.handleClearCache)
}
}
@@ -733,8 +734,10 @@ func (s *Service) handleGetDBTableData(c *gin.Context) {
group := c.Query("group")
file := c.Query("file")
table := c.Query("table")
keyword := c.Query("keyword")
limitStr := c.DefaultQuery("limit", "20")
offsetStr := c.DefaultQuery("offset", "0")
format := strings.ToLower(c.Query("format"))
if group == "" || file == "" || table == "" {
errors.Err(c, errors.InvalidArg("group, file or table"))
@@ -746,11 +749,120 @@ func (s *Service) handleGetDBTableData(c *gin.Context) {
fmt.Sscanf(limitStr, "%d", &limit)
fmt.Sscanf(offsetStr, "%d", &offset)
data, err := s.db.GetTableData(group, file, table, limit, offset)
// If exporting, fetch all matching rows (ignore pagination if user wants all? or respect pagination?)
// Usually export means "export all matching".
if format == "csv" || format == "xlsx" || format == "excel" {
limit = -1 // No limit
offset = 0
}
data, err := s.db.GetTableData(group, file, table, limit, offset, keyword)
if err != nil {
errors.Err(c, err)
return
}
if format == "csv" || format == "xlsx" || format == "excel" {
s.exportData(c, data, format, table)
return
}
c.JSON(http.StatusOK, data)
}
func (s *Service) handleExecuteSQL(c *gin.Context) {
group := c.Query("group")
file := c.Query("file")
query := c.Query("sql")
format := strings.ToLower(c.Query("format"))
if group == "" || file == "" || query == "" {
errors.Err(c, errors.InvalidArg("group, file or sql"))
return
}
data, err := s.db.ExecuteSQL(group, file, query)
if err != nil {
errors.Err(c, err)
return
}
if format == "csv" || format == "xlsx" || format == "excel" {
s.exportData(c, data, format, "query_result")
return
}
c.JSON(http.StatusOK, data)
}
func (s *Service) exportData(c *gin.Context, data []map[string]interface{}, format string, filename string) {
if len(data) == 0 {
c.String(http.StatusOK, "")
return
}
// Extract headers
var headers []string
for k := range data[0] {
headers = append(headers, k)
}
// Sort headers for consistency
// sort.Strings(headers) // We need sort package
if format == "csv" {
c.Writer.Header().Set("Content-Type", "text/csv; charset=utf-8")
c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.csv", filename))
c.Writer.Header().Set("Cache-Control", "no-cache")
c.Writer.Flush()
w := csv.NewWriter(c.Writer)
w.Write(headers)
for _, row := range data {
var record []string
for _, h := range headers {
val := row[h]
if val == nil {
record = append(record, "")
} else {
record = append(record, fmt.Sprintf("%v", val))
}
}
w.Write(record)
}
w.Flush()
} else {
// Excel
f := excelize.NewFile()
defer func() {
if err := f.Close(); err != nil {
log.Error().Err(err).Msg("Failed to close excel file")
}
}()
sheet := "Sheet1"
index, _ := f.NewSheet(sheet)
// Write headers
for i, h := range headers {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
f.SetCellValue(sheet, cell, h)
}
// Write data
for r, row := range data {
for cIdx, h := range headers {
val := row[h]
cell, _ := excelize.CoordinatesToCellName(cIdx+1, r+2)
f.SetCellValue(sheet, cell, val)
}
}
f.SetActiveSheet(index)
c.Writer.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s.xlsx", filename))
if err := f.Write(c.Writer); err != nil {
log.Error().Err(err).Msg("Failed to write excel file")
}
}
}

View File

@@ -180,6 +180,11 @@
background: #e5e7eb;
}
.btn-sm {
padding: 6px 12px;
font-size: 0.85rem;
}
/* Results */
.result-area {
margin-top: 20px;
@@ -269,30 +274,48 @@
/* Modal Styles */
.modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; justify-content: center; align-items: center; }
.modal.hidden { display: none; }
.modal-content { background: white; width: 90%; max-width: 1200px; height: 90%; border-radius: 8px; display: flex; flex-direction: column; box-shadow: 0 4px 20px rgba(0,0,0,0.2); }
.modal-content { background: white; width: 95%; max-width: 1300px; height: 95%; border-radius: 8px; display: flex; flex-direction: column; box-shadow: 0 4px 20px rgba(0,0,0,0.2); }
.modal-header { padding: 15px 20px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; background: #f9fafb; border-radius: 8px 8px 0 0; }
.modal-header h3 { margin: 0; color: var(--text-main); font-size: 1.2rem; }
.modal-body { flex: 1; overflow: hidden; padding: 20px; display: flex; flex-direction: column; }
.close-btn { background: none; border: none; font-size: 2rem; line-height: 1; cursor: pointer; color: var(--text-secondary); }
.close-btn:hover { color: var(--danger); }
/* Modal Nav & Views */
.modal-nav { display: flex; gap: 10px; margin-bottom: 15px; border-bottom: 1px solid var(--border); padding-bottom: 10px; }
.nav-btn { background: none; border: none; padding: 8px 15px; cursor: pointer; font-weight: 500; color: var(--text-secondary); border-radius: 4px; transition: all 0.2s; }
.nav-btn:hover { background: #f3f4f6; }
.nav-btn.active { background: #eff6ff; color: var(--primary); font-weight: 600; }
.modal-view { display: none; flex: 1; flex-direction: column; overflow: hidden; }
.modal-view.active { display: flex; }
/* Browser View */
.db-table-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; overflow-y: auto; padding: 5px; }
.db-table-item { padding: 15px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer; text-align: center; background: white; transition: all 0.2s; box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
.db-table-item:hover { background: #eff6ff; border-color: var(--primary); transform: translateY(-2px); box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
.data-view-container { display: flex; flex-direction: column; height: 100%; }
.data-controls { display: flex; gap: 15px; align-items: center; margin-bottom: 15px; flex-shrink: 0; }
.table-scroll { flex: 1; overflow: auto; border: 1px solid var(--border); border-radius: 6px; }
.data-view-container { display: flex; flex-direction: column; height: 100%; flex: 1; overflow: hidden; }
.data-controls { display: flex; gap: 10px; align-items: center; margin-bottom: 15px; flex-shrink: 0; flex-wrap: wrap; }
.spacer { flex: 1; }
.search-input { padding: 6px 12px; border: 1px solid var(--border); border-radius: 4px; width: 200px; font-size: 0.9rem; }
#data-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 0.9rem; }
#data-table th, #data-table td { padding: 10px; border-bottom: 1px solid var(--border); border-right: 1px solid var(--border); white-space: nowrap; max-width: 300px; overflow: hidden; text-overflow: ellipsis; }
#data-table th { background: #f3f4f6; position: sticky; top: 0; z-index: 10; font-weight: 600; text-align: left; }
#data-table tr:hover { background-color: #f9fafb; }
.table-scroll { flex: 1; overflow: auto; border: 1px solid var(--border); border-radius: 6px; position: relative; }
/* SQL View */
.sql-editor-container { margin-bottom: 15px; display: flex; flex-direction: column; gap: 10px; }
.sql-editor { width: 100%; height: 120px; font-family: 'Consolas', monospace; padding: 10px; border: 1px solid var(--border); border-radius: 6px; box-sizing: border-box; resize: vertical; font-size: 14px; background: #f8fafc; }
.sql-editor:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1); }
#data-table, #sql-result-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 0.9rem; }
#data-table th, #sql-result-table th, #data-table td, #sql-result-table td { padding: 8px 12px; border-bottom: 1px solid var(--border); border-right: 1px solid var(--border); white-space: nowrap; max-width: 400px; overflow: hidden; text-overflow: ellipsis; }
#data-table th, #sql-result-table th { background: #f3f4f6; position: sticky; top: 0; z-index: 10; font-weight: 600; text-align: left; }
#data-table tr:hover, #sql-result-table tr:hover { background-color: #f9fafb; }
.pagination { display: flex; justify-content: center; gap: 15px; margin-top: 15px; align-items: center; flex-shrink: 0; }
/* Helper Classes */
.hidden { display: none; }
.hidden { display: none !important; }
.text-sm { font-size: 0.875rem; }
.text-gray { color: var(--text-secondary); }
.text-danger { color: var(--danger); }
@@ -330,9 +353,10 @@
</div>
</div>
<!-- Session Tab -->
<!-- Other Tabs (Keep same as before) -->
<div id="session" class="tab-content">
<div class="form-grid">
<!-- ... content ... -->
<div class="form-grid">
<div class="form-group">
<label>关键词搜索 (可选)</label>
<input type="text" id="session-keyword" placeholder="昵称 / 备注 / wxid">
@@ -352,10 +376,9 @@
</div>
<div id="session-result" class="result-area hidden"></div>
</div>
<!-- Chatroom Tab -->
<div id="chatroom" class="tab-content">
<div class="form-grid">
<!-- ... content ... -->
<div class="form-grid">
<div class="form-group">
<label>搜索群聊 (可选)</label>
<input type="text" id="chatroom-keyword" placeholder="群名称 / 群ID">
@@ -375,10 +398,9 @@
</div>
<div id="chatroom-result" class="result-area hidden"></div>
</div>
<!-- Contact Tab -->
<div id="contact" class="tab-content">
<div class="form-grid">
<!-- ... content ... -->
<div class="form-grid">
<div class="form-group">
<label>搜索联系人 (可选)</label>
<input type="text" id="contact-keyword" placeholder="昵称 / 备注 / wxid">
@@ -398,10 +420,9 @@
</div>
<div id="contact-result" class="result-area hidden"></div>
</div>
<!-- Chatlog Tab -->
<div id="chatlog" class="tab-content">
<div class="form-grid">
<!-- ... content ... -->
<div class="form-grid">
<div class="form-group">
<label>时间范围 <span style="color:red">*</span></label>
<input type="text" id="chatlog-time" placeholder="例如: 2023-01-01~2023-12-31">
@@ -448,31 +469,72 @@
<button class="close-btn" onclick="closeModal()">×</button>
</div>
<div class="modal-body">
<!-- Table List View -->
<div id="table-list-view">
<p class="text-gray" style="margin-bottom: 15px;">请选择要查看的表:</p>
<div id="table-list" class="db-table-list"></div>
<div class="modal-nav">
<button class="nav-btn active" id="btn-view-browser" onclick="switchModalView('browser')">表浏览器</button>
<button class="nav-btn" id="btn-view-sql" onclick="switchModalView('sql')">SQL 控制台</button>
</div>
<!-- Table Data View -->
<div id="table-data-view" class="data-view-container hidden">
<div class="data-controls">
<button class="btn btn-secondary text-sm" onclick="backToTableList()">← 返回表列表</button>
<strong id="current-table-name" style="font-size: 1.1rem;"></strong>
<span id="loading-indicator" class="text-gray hidden">加载中...</span>
</div>
<div class="table-scroll">
<table id="data-table">
<thead></thead>
<tbody></tbody>
</table>
</div>
<div class="pagination">
<button class="btn btn-secondary text-sm" onclick="prevPage()">上一页</button>
<span id="page-info" class="text-sm font-bold">Page 1</span>
<button class="btn btn-secondary text-sm" onclick="nextPage()">下一页</button>
</div>
<!-- Browser View -->
<div id="view-browser" class="modal-view active">
<!-- Table List -->
<div id="table-list-view">
<p class="text-gray" style="margin-bottom: 15px;">请选择要查看的表:</p>
<div id="table-list" class="db-table-list"></div>
</div>
<!-- Table Data -->
<div id="table-data-view" class="data-view-container hidden">
<div class="data-controls">
<button class="btn btn-secondary text-sm" onclick="backToTableList()">← 返回列表</button>
<strong id="current-table-name" style="font-size: 1.1rem; margin-right: 15px;"></strong>
<input type="text" id="data-search-input" class="search-input" placeholder="搜索关键词..." onkeydown="if(event.key === 'Enter') performTableSearch()">
<button class="btn btn-secondary btn-sm" onclick="performTableSearch()">搜索</button>
<div class="spacer"></div>
<button class="btn btn-success btn-sm" style="background-color: #10b981; color: white;" onclick="exportTableData('xlsx')">导出 Excel</button>
<button class="btn btn-secondary btn-sm" onclick="exportTableData('csv')">导出 CSV</button>
</div>
<div class="table-scroll">
<table id="data-table">
<thead></thead>
<tbody></tbody>
</table>
</div>
<div id="browser-loading" class="text-gray hidden" style="text-align: center; padding: 10px;">加载中...</div>
<div class="pagination">
<button class="btn btn-secondary text-sm" onclick="prevPage()">上一页</button>
<span id="page-info" class="text-sm font-bold">Page 1</span>
<button class="btn btn-secondary text-sm" onclick="nextPage()">下一页</button>
</div>
</div>
</div>
<!-- SQL View -->
<div id="view-sql" class="modal-view">
<div class="sql-editor-container">
<textarea id="sql-input" class="sql-editor" placeholder="在此输入 SQL 语句... (例如: SELECT * FROM message LIMIT 10)"></textarea>
<div class="toolbar">
<button class="btn btn-primary btn-sm" onclick="runSQL()">执行 (Run)</button>
<div class="spacer"></div>
<button class="btn btn-success btn-sm" style="background-color: #10b981; color: white;" onclick="exportSQLResult('xlsx')">导出结果 Excel</button>
<button class="btn btn-secondary btn-sm" onclick="exportSQLResult('csv')">导出结果 CSV</button>
</div>
</div>
<div class="table-scroll">
<table id="sql-result-table">
<thead></thead>
<tbody></tbody>
</table>
<div id="sql-msg" style="padding: 10px; color: #666; text-align: center;">请执行查询以查看结果</div>
</div>
</div>
</div>
</div>
</div>
@@ -483,7 +545,7 @@
loadDBList();
});
// Tab Switching
// Tab Switching for Main Page
function switchTab(tabId) {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
@@ -501,7 +563,17 @@
let currentDB = { group: null, file: null };
let currentTable = null;
let currentPage = 0;
let currentKeyword = '';
const pageSize = 50;
// Modal View Switching
function switchModalView(view) {
document.querySelectorAll('.modal-nav .nav-btn').forEach(btn => btn.classList.remove('active'));
document.getElementById(`btn-view-${view}`).classList.add('active');
document.querySelectorAll('.modal-view').forEach(v => v.classList.remove('active'));
document.getElementById(`view-${view}`).classList.add('active');
}
// Load DB List
async function loadDBList() {
@@ -525,9 +597,9 @@
if (files && files.length > 0) {
files.forEach(file => {
// Escape backslashes for JS string
const escapedFile = file.replace(/\\/g, '\\\\');
const escapedFile = file.replace(/\/g, '\\');
html += `<div class="db-item" onclick="openDBViewer('${group}', '${escapedFile}')">
<span style="font-weight:bold;">📄 ${file.split(/[\\/]/).pop()}</span>
<span style="font-weight:bold;">📄 ${file.split(/[\/]/).pop()}</span>
<span class="text-gray text-sm" style="margin-left:auto;">${file}</span>
</div>`;
});
@@ -548,11 +620,17 @@
document.getElementById('modal-title').textContent = `正在查看: ${file.split(/[\/]/).pop()}`;
document.getElementById('db-viewer-modal').classList.remove('hidden');
// Reset state
switchModalView('browser');
backToTableList();
document.getElementById('sql-input').value = '';
document.getElementById('sql-result-table').querySelector('thead').innerHTML = '';
document.getElementById('sql-result-table').querySelector('tbody').innerHTML = '';
document.getElementById('sql-msg').textContent = '请执行查询以查看结果';
// Load tables
const listContainer = document.getElementById('table-list');
listContainer.innerHTML = '<div class="loading">加载表列表中...</div>';
backToTableList(); // Ensure we are in list view
try {
const res = await fetch(`/api/v1/db/tables?group=${group}&file=${encodeURIComponent(file)}`);
@@ -588,6 +666,8 @@
async function loadTableData(table) {
currentTable = table;
currentPage = 0;
currentKeyword = '';
document.getElementById('data-search-input').value = '';
document.getElementById('table-list-view').classList.add('hidden');
document.getElementById('table-data-view').classList.remove('hidden');
@@ -595,62 +675,37 @@
await fetchTableData();
}
function performTableSearch() {
const val = document.getElementById('data-search-input').value.trim();
currentKeyword = val;
currentPage = 0;
fetchTableData();
}
async function fetchTableData() {
if(!currentTable || !currentDB.file) return;
const tbody = document.querySelector('#data-table tbody');
const thead = document.querySelector('#data-table thead');
const indicator = document.getElementById('loading-indicator');
const indicator = document.getElementById('browser-loading');
indicator.classList.remove('hidden');
tbody.innerHTML = ''; // Clear current data
// Keep header if we are just changing pages? No, schema might ensure safety but refreshing is better.
tbody.innerHTML = '';
const offset = currentPage * pageSize;
try {
const url = `/api/v1/db/data?group=${currentDB.group}&file=${encodeURIComponent(currentDB.file)}&table=${currentTable}&limit=${pageSize}&offset=${offset}`;
let url = `/api/v1/db/data?group=${currentDB.group}&file=${encodeURIComponent(currentDB.file)}&table=${currentTable}&limit=${pageSize}&offset=${offset}`;
if(currentKeyword) {
url += `&keyword=${encodeURIComponent(currentKeyword)}`;
}
const res = await fetch(url);
if(!res.ok) throw new Error('Failed to load data');
const data = await res.json();
// Render Header
thead.innerHTML = '';
if(data && data.length > 0) {
const headers = Object.keys(data[0]);
let headerHtml = '<tr>';
headers.forEach(h => headerHtml += `<th>${h}</th>`);
headerHtml += '</tr>';
thead.innerHTML = headerHtml;
// Render Body
let bodyHtml = '';
data.forEach(row => {
bodyHtml += '<tr>';
headers.forEach(h => {
let val = row[h];
if(val === null) val = '<span class="text-gray">null</span>';
// Basic escaping
if(typeof val === 'string') {
if(val.length > 100) val = val.substring(0, 100) + '...';
val = escapeHtml(val);
}
bodyHtml += `<td>${val}</td>`;
});
bodyHtml += '</tr>';
});
tbody.innerHTML = bodyHtml;
} else {
if(currentPage === 0) {
thead.innerHTML = '<tr><td>暂无数据</td></tr>';
} else {
// Keep header, show empty row
const prevHeader = thead.innerHTML;
if(!prevHeader) thead.innerHTML = '<tr><td>End of data</td></tr>';
else tbody.innerHTML = '<tr><td colspan="100%" style="text-align:center; padding: 20px;">没有更多数据了</td></tr>';
}
}
renderTable(data, thead, tbody, currentPage, pageSize); // Reuse renderTable
document.getElementById('page-info').textContent = `Page ${currentPage + 1}`;
@@ -661,6 +716,41 @@
}
}
function renderTable(data, thead, tbody, page, size) {
thead.innerHTML = '';
if(data && data.length > 0) {
const headers = Object.keys(data[0]);
let headerHtml = '<tr>';
headers.forEach(h => headerHtml += `<th>${h}</th>`);
headerHtml += '</tr>';
thead.innerHTML = headerHtml;
let bodyHtml = '';
data.forEach(row => {
bodyHtml += '<tr>';
headers.forEach(h => {
let val = row[h];
if(val === null) val = '<span class="text-gray">null</span>';
if(typeof val === 'string') {
if(val.length > 100) val = val.substring(0, 100) + '...';
val = escapeHtml(val);
}
bodyHtml += `<td>${val}</td>`;
});
bodyHtml += '</tr>';
});
tbody.innerHTML = bodyHtml;
} else {
if(page === 0) {
thead.innerHTML = '<tr><td>暂无数据</td></tr>';
} else {
const prevHeader = thead.innerHTML;
if(!prevHeader) thead.innerHTML = '<tr><td>End of data</td></tr>';
else tbody.innerHTML = '<tr><td colspan="100%" style="text-align:center; padding: 20px;">没有更多数据了</td></tr>';
}
}
}
function prevPage() {
if(currentPage > 0) {
currentPage--;
@@ -672,9 +762,67 @@
currentPage++;
fetchTableData();
}
function exportTableData(format) {
if(!currentTable || !currentDB.file) return;
let url = `/api/v1/db/data?group=${currentDB.group}&file=${encodeURIComponent(currentDB.file)}&table=${currentTable}&format=${format}`;
if(currentKeyword) {
url += `&keyword=${encodeURIComponent(currentKeyword)}`;
}
window.location.href = url;
}
// --- SQL Console Logic ---
async function runSQL() {
const sql = document.getElementById('sql-input').value.trim();
if(!sql) {
alert("请输入 SQL 语句");
return;
}
const tbody = document.querySelector('#sql-result-table tbody');
const thead = document.querySelector('#sql-result-table thead');
const msg = document.getElementById('sql-msg');
tbody.innerHTML = '';
thead.innerHTML = '';
msg.textContent = '执行中...';
msg.style.display = 'block';
try {
const url = `/api/v1/db/query?group=${currentDB.group}&file=${encodeURIComponent(currentDB.file)}&sql=${encodeURIComponent(sql)}`;
const res = await fetch(url);
if(!res.ok) throw new Error(await res.text());
const data = await res.json();
msg.style.display = 'none';
renderTable(data, thead, tbody, 0, 0); // Reuse renderTable
if(!data || data.length === 0) {
msg.textContent = '执行成功,无结果返回';
msg.style.display = 'block';
}
} catch(e) {
msg.textContent = `执行错误: ${e.message}`;
msg.style.color = 'var(--danger)';
msg.style.display = 'block';
}
}
function exportSQLResult(format) {
const sql = document.getElementById('sql-input').value.trim();
if(!sql) {
alert("请输入 SQL 语句");
return;
}
const url = `/api/v1/db/query?group=${currentDB.group}&file=${encodeURIComponent(currentDB.file)}&sql=${encodeURIComponent(sql)}&format=${format}`;
window.location.href = url;
}
// --- Generic API Query ---
// --- Generic API Query (Keep same as before) ---
async function queryAPI(type) {
// ... (Same logic as before, just kept for brevity since I'm overwriting file)
const resultArea = document.getElementById(`${type}-result`);
resultArea.classList.remove('hidden');
resultArea.innerHTML = '<div class="loading">正在处理...</div>';
@@ -722,7 +870,6 @@
const url = `/api/v1/${type}?${params.toString()}`;
if (format === 'csv' || format === 'xlsx') {
// Trigger download
window.location.href = url;
resultArea.innerHTML = `<div class="text-success">已触发 ${format.toUpperCase()} 下载。<br>请求URL: <span class="url-display">${url}</span></div>`;
} else {
@@ -768,7 +915,6 @@
});
}
// Clear Cache
document.getElementById('clearCacheBtn').addEventListener('click', async () => {
if(!confirm('确定要清除所有解密的媒体文件缓存吗?')) return;
try {
@@ -781,4 +927,4 @@
});
</script>
</body>
</html>
</html>