mirror of
https://github.com/ZSCGR/CloudFlare-ImgBed.git
synced 2026-08-13 04:03:42 +08:00
Merge pull request #334 from sean908/main
feat: Add tag management for images
This commit is contained in:
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS files (
|
||||
tg_chat_id TEXT, -- Telegram聊天ID (从metadata中提取)
|
||||
tg_bot_token TEXT, -- Telegram Bot Token (从metadata中提取)
|
||||
is_chunked BOOLEAN DEFAULT FALSE, -- 是否为分块文件
|
||||
tags TEXT, -- 标签 (从metadata中提取,JSON数组格式)
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
@@ -32,6 +33,7 @@ CREATE INDEX IF NOT EXISTS idx_files_channel ON files(channel);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_file_type ON files(file_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_upload_ip ON files(upload_ip);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_created_at ON files(created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_tags ON files(tags);
|
||||
|
||||
-- 2. 系统配置表 - 存储各种系统配置
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import { purgeCFCache } from "../../../utils/purgeCache.js";
|
||||
import { addFileToIndex } from "../../../utils/indexManager.js";
|
||||
import { getDatabase } from "../../../utils/databaseAdapter.js";
|
||||
import { mergeTags, normalizeTags, validateTag } from "../../../utils/tagHelpers.js";
|
||||
|
||||
/**
|
||||
* Tag Management API for Single Files
|
||||
*
|
||||
* GET /api/manage/tags/{fileId} - Get tags for a file
|
||||
* POST /api/manage/tags/{fileId} - Update tags for a file
|
||||
*
|
||||
* POST body format:
|
||||
* {
|
||||
* action: "set" | "add" | "remove",
|
||||
* tags: ["tag1", "tag2", ...]
|
||||
* }
|
||||
*/
|
||||
export async function onRequest(context) {
|
||||
const {
|
||||
request,
|
||||
env,
|
||||
params,
|
||||
waitUntil,
|
||||
} = context;
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
// Parse file path
|
||||
if (params.path) {
|
||||
params.path = String(params.path).split(',').join('/');
|
||||
}
|
||||
|
||||
// Decode file path
|
||||
const fileId = decodeURIComponent(params.path);
|
||||
|
||||
const db = getDatabase(env);
|
||||
|
||||
try {
|
||||
if (request.method === 'GET') {
|
||||
// Get tags for file
|
||||
return await handleGetTags(db, fileId);
|
||||
} else if (request.method === 'POST') {
|
||||
// Update tags for file
|
||||
return await handleUpdateTags(context, db, fileId, url.hostname);
|
||||
} else {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Method not allowed',
|
||||
allowedMethods: ['GET', 'POST']
|
||||
}), {
|
||||
status: 405,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error in tag management for ${fileId}:`, error);
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Internal server error',
|
||||
message: error.message
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET request - Get tags for a file
|
||||
*/
|
||||
async function handleGetTags(db, fileId) {
|
||||
try {
|
||||
const fileData = await db.getWithMetadata(fileId);
|
||||
|
||||
if (!fileData || !fileData.metadata) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'File not found',
|
||||
fileId: fileId
|
||||
}), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const tags = fileData.metadata.Tags || [];
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
fileId: fileId,
|
||||
tags: tags
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get tags: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST request - Update tags for a file
|
||||
*/
|
||||
async function handleUpdateTags(context, db, fileId, hostname) {
|
||||
const { request, waitUntil } = context;
|
||||
|
||||
try {
|
||||
// Parse request body
|
||||
const body = await request.json();
|
||||
const { action = 'set', tags = [] } = body;
|
||||
|
||||
// Validate action
|
||||
if (!['set', 'add', 'remove'].includes(action)) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Invalid action',
|
||||
message: 'Action must be one of: set, add, remove'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Validate tags array
|
||||
if (!Array.isArray(tags)) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Invalid tags format',
|
||||
message: 'Tags must be an array of strings'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Validate each tag
|
||||
const invalidTags = tags.filter(tag => !validateTag(tag));
|
||||
if (invalidTags.length > 0) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Invalid tag format',
|
||||
message: 'Tags must contain only alphanumeric characters, underscores, hyphens, and CJK characters',
|
||||
invalidTags: invalidTags
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Get file metadata
|
||||
const fileData = await db.getWithMetadata(fileId);
|
||||
|
||||
if (!fileData || !fileData.metadata) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'File not found',
|
||||
fileId: fileId
|
||||
}), {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Get existing tags
|
||||
const existingTags = fileData.metadata.Tags || [];
|
||||
|
||||
// Merge tags based on action
|
||||
const updatedTags = mergeTags(existingTags, tags, action);
|
||||
|
||||
// Update metadata
|
||||
fileData.metadata.Tags = updatedTags;
|
||||
|
||||
// Save to database
|
||||
await db.put(fileId, fileData.value, {
|
||||
metadata: fileData.metadata
|
||||
});
|
||||
|
||||
// Clear CDN cache asynchronously (don't wait for it to complete)
|
||||
const cdnUrl = `https://${hostname}/file/${fileId}`;
|
||||
waitUntil(purgeCFCache(context.env, cdnUrl));
|
||||
|
||||
// Update file index asynchronously
|
||||
waitUntil(addFileToIndex(context, fileId, fileData.metadata));
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
fileId: fileId,
|
||||
action: action,
|
||||
tags: updatedTags,
|
||||
metadata: fileData.metadata
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update tags: ${error.message}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { getDatabase } from "../../../utils/databaseAdapter.js";
|
||||
import { extractUniqueTags, filterTagsByPrefix } from "../../../utils/tagHelpers.js";
|
||||
|
||||
/**
|
||||
* Tag Autocomplete API
|
||||
*
|
||||
* GET /api/manage/tags/autocomplete?prefix=ph - Get tag suggestions
|
||||
*
|
||||
* Returns all tags matching the given prefix, useful for autocomplete functionality
|
||||
*/
|
||||
export async function onRequest(context) {
|
||||
const { request, env } = context;
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (request.method !== 'GET') {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Method not allowed',
|
||||
allowedMethods: ['GET']
|
||||
}), {
|
||||
status: 405,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const db = getDatabase(env);
|
||||
|
||||
try {
|
||||
// Get prefix from query parameters
|
||||
const prefix = url.searchParams.get('prefix') || '';
|
||||
const limit = parseInt(url.searchParams.get('limit') || '20', 10);
|
||||
|
||||
// Validate limit
|
||||
if (limit < 1 || limit > 100) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Invalid limit',
|
||||
message: 'Limit must be between 1 and 100'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Get all files from database
|
||||
const allTags = new Set();
|
||||
let cursor = null;
|
||||
|
||||
while (true) {
|
||||
const response = await db.list({
|
||||
limit: 1000,
|
||||
cursor: cursor
|
||||
});
|
||||
|
||||
for (const item of response.keys) {
|
||||
// Skip non-file entries
|
||||
if (item.name.startsWith('manage@') || item.name.startsWith('chunk_')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Extract tags from metadata
|
||||
if (item.metadata && Array.isArray(item.metadata.Tags)) {
|
||||
item.metadata.Tags.forEach(tag => {
|
||||
if (tag && typeof tag === 'string') {
|
||||
allTags.add(tag.toLowerCase().trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cursor = response.cursor;
|
||||
if (!cursor) break;
|
||||
|
||||
// Limit iterations for performance
|
||||
if (allTags.size > 10000) break;
|
||||
}
|
||||
|
||||
// Convert to array and sort
|
||||
const tagsArray = Array.from(allTags).sort();
|
||||
|
||||
// Filter by prefix
|
||||
const filteredTags = prefix
|
||||
? filterTagsByPrefix(tagsArray, prefix, limit)
|
||||
: tagsArray.slice(0, limit);
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
success: true,
|
||||
prefix: prefix,
|
||||
tags: filteredTags,
|
||||
total: filteredTags.length,
|
||||
hasMore: tagsArray.length > filteredTags.length
|
||||
}), {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'public, max-age=60' // Cache for 1 minute
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in tag autocomplete:', error);
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Internal server error',
|
||||
message: error.message
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { purgeCFCache } from "../../../utils/purgeCache.js";
|
||||
import { batchAddFilesToIndex } from "../../../utils/indexManager.js";
|
||||
import { getDatabase } from "../../../utils/databaseAdapter.js";
|
||||
import { mergeTags, validateTag } from "../../../utils/tagHelpers.js";
|
||||
|
||||
/**
|
||||
* Batch Tag Management API
|
||||
*
|
||||
* POST /api/manage/tags/batch - Update tags for multiple files
|
||||
*
|
||||
* Request body format:
|
||||
* {
|
||||
* fileIds: ["file1", "file2", ...],
|
||||
* action: "set" | "add" | "remove",
|
||||
* tags: ["tag1", "tag2", ...]
|
||||
* }
|
||||
*/
|
||||
export async function onRequest(context) {
|
||||
const {
|
||||
request,
|
||||
env,
|
||||
waitUntil,
|
||||
} = context;
|
||||
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (request.method !== 'POST') {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Method not allowed',
|
||||
allowedMethods: ['POST']
|
||||
}), {
|
||||
status: 405,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const db = getDatabase(env);
|
||||
|
||||
try {
|
||||
// Parse request body
|
||||
const body = await request.json();
|
||||
const { fileIds = [], action = 'set', tags = [] } = body;
|
||||
|
||||
// Validate fileIds
|
||||
if (!Array.isArray(fileIds) || fileIds.length === 0) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Invalid fileIds',
|
||||
message: 'fileIds must be a non-empty array of file identifiers'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Validate action
|
||||
if (!['set', 'add', 'remove'].includes(action)) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Invalid action',
|
||||
message: 'Action must be one of: set, add, remove'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Validate tags array
|
||||
if (!Array.isArray(tags)) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Invalid tags format',
|
||||
message: 'Tags must be an array of strings'
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Validate each tag
|
||||
const invalidTags = tags.filter(tag => !validateTag(tag));
|
||||
if (invalidTags.length > 0) {
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Invalid tag format',
|
||||
message: 'Tags must contain only alphanumeric characters, underscores, hyphens, and CJK characters',
|
||||
invalidTags: invalidTags
|
||||
}), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
// Process files in batch
|
||||
const results = {
|
||||
success: true,
|
||||
total: fileIds.length,
|
||||
updated: 0,
|
||||
errors: []
|
||||
};
|
||||
|
||||
const updatedFiles = [];
|
||||
|
||||
for (const fileId of fileIds) {
|
||||
try {
|
||||
// Get file metadata
|
||||
const fileData = await db.getWithMetadata(fileId);
|
||||
|
||||
if (!fileData || !fileData.metadata) {
|
||||
results.errors.push({
|
||||
fileId: fileId,
|
||||
error: 'File not found'
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get existing tags
|
||||
const existingTags = fileData.metadata.Tags || [];
|
||||
|
||||
// Merge tags based on action
|
||||
const updatedTags = mergeTags(existingTags, tags, action);
|
||||
|
||||
// Update metadata
|
||||
fileData.metadata.Tags = updatedTags;
|
||||
|
||||
// Save to database
|
||||
await db.put(fileId, fileData.value, {
|
||||
metadata: fileData.metadata
|
||||
});
|
||||
|
||||
// Clear CDN cache (async)
|
||||
const cdnUrl = `https://${url.hostname}/file/${fileId}`;
|
||||
waitUntil(purgeCFCache(env, cdnUrl));
|
||||
|
||||
// Track updated file for batch index update
|
||||
updatedFiles.push({
|
||||
fileId: fileId,
|
||||
metadata: fileData.metadata
|
||||
});
|
||||
|
||||
results.updated++;
|
||||
|
||||
} catch (error) {
|
||||
results.errors.push({
|
||||
fileId: fileId,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Batch update file index asynchronously
|
||||
if (updatedFiles.length > 0) {
|
||||
waitUntil(batchAddFilesToIndex(context, updatedFiles, { skipExisting: false }));
|
||||
}
|
||||
|
||||
// Set success to false if there were any errors
|
||||
if (results.errors.length > 0) {
|
||||
results.success = false;
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(results), {
|
||||
status: results.success ? 200 : 207, // 207 = Multi-Status (partial success)
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in batch tag update:', error);
|
||||
return new Response(JSON.stringify({
|
||||
error: 'Internal server error',
|
||||
message: error.message
|
||||
}), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -217,6 +217,7 @@ async function handleChannelBasedMerge(context, uploadId, totalChunks, originalF
|
||||
TimeStamp: Date.now(),
|
||||
Label: "None",
|
||||
Directory: normalizedFolder === '' ? '' : normalizedFolder + '/',
|
||||
Tags: []
|
||||
};
|
||||
|
||||
// 更新进度
|
||||
|
||||
@@ -146,6 +146,7 @@ async function processFileUpload(context, formdata = null) {
|
||||
TimeStamp: time,
|
||||
Label: "None",
|
||||
Directory: normalizedFolder === '' ? '' : normalizedFolder + '/',
|
||||
Tags: []
|
||||
};
|
||||
|
||||
let fileExt = fileName.split('.').pop(); // 文件扩展名
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
*/
|
||||
|
||||
import { getDatabase } from './databaseAdapter.js';
|
||||
import { parseSearchQuery, matchesTags } from './tagHelpers.js';
|
||||
|
||||
const INDEX_KEY = 'manage@index';
|
||||
const INDEX_META_KEY = 'manage@index@meta'; // 索引元数据键
|
||||
@@ -520,12 +521,32 @@ export async function readIndex(context, options = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
// 搜索过滤
|
||||
// 搜索过滤(支持标签和关键字混合搜索)
|
||||
if (search) {
|
||||
const searchLower = search.toLowerCase();
|
||||
// 解析搜索查询,提取标签和关键字
|
||||
const { keywords, tags } = parseSearchQuery(search);
|
||||
|
||||
filteredFiles = filteredFiles.filter(file => {
|
||||
return file.metadata.FileName?.toLowerCase().includes(searchLower) ||
|
||||
file.id.toLowerCase().includes(searchLower);
|
||||
// 标签过滤(必须包含所有指定的标签)
|
||||
if (tags.length > 0) {
|
||||
const fileTags = file.metadata.Tags || [];
|
||||
if (!matchesTags(fileTags, tags)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 关键字过滤(匹配文件名或文件ID)
|
||||
if (keywords) {
|
||||
const keywordsLower = keywords.toLowerCase();
|
||||
const matchesKeyword =
|
||||
file.metadata.FileName?.toLowerCase().includes(keywordsLower) ||
|
||||
file.id.toLowerCase().includes(keywordsLower);
|
||||
if (!matchesKeyword) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Tag Management Helper Functions
|
||||
* Provides utilities for validating, normalizing, and managing tags
|
||||
*/
|
||||
|
||||
/**
|
||||
* Validate tag format
|
||||
* Tags must contain only alphanumeric characters, underscores, and hyphens
|
||||
* @param {string} tag - The tag to validate
|
||||
* @returns {boolean} - Whether the tag is valid
|
||||
*/
|
||||
export function validateTag(tag) {
|
||||
if (!tag || typeof tag !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Allow alphanumeric, underscore, hyphen, and Chinese/Japanese/Korean characters
|
||||
return /^[\w\u4e00-\u9fa5\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af-]+$/.test(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize tags
|
||||
* - Convert to lowercase
|
||||
* - Trim whitespace
|
||||
* - Remove duplicates
|
||||
* - Filter out invalid tags
|
||||
* @param {string[]} tags - Array of tags to normalize
|
||||
* @returns {string[]} - Normalized array of unique tags
|
||||
*/
|
||||
export function normalizeTags(tags) {
|
||||
if (!Array.isArray(tags)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const normalized = tags
|
||||
.filter(tag => tag && typeof tag === 'string')
|
||||
.map(tag => tag.toLowerCase().trim())
|
||||
.filter(tag => validateTag(tag));
|
||||
|
||||
// Remove duplicates while preserving order
|
||||
return [...new Set(normalized)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge tags based on action
|
||||
* @param {string[]} existingTags - Current tags on the file
|
||||
* @param {string[]} newTags - Tags to add/remove/set
|
||||
* @param {string} action - 'set', 'add', or 'remove'
|
||||
* @returns {string[]} - Merged tags array
|
||||
*/
|
||||
export function mergeTags(existingTags, newTags, action) {
|
||||
const existing = Array.isArray(existingTags) ? existingTags : [];
|
||||
const normalized = normalizeTags(newTags);
|
||||
|
||||
switch (action) {
|
||||
case 'set':
|
||||
// Replace all tags with new tags
|
||||
return normalized;
|
||||
|
||||
case 'add':
|
||||
// Add new tags to existing, remove duplicates
|
||||
return normalizeTags([...existing, ...normalized]);
|
||||
|
||||
case 'remove':
|
||||
// Remove specified tags from existing
|
||||
const toRemove = new Set(normalized);
|
||||
return existing.filter(tag => !toRemove.has(tag.toLowerCase()));
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid action: ${action}. Must be 'set', 'add', or 'remove'`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse search query to extract tags and keywords
|
||||
* Input: "vacation #photo #2024"
|
||||
* Output: { keywords: "vacation", tags: ["photo", "2024"] }
|
||||
* @param {string} searchString - The search query string
|
||||
* @returns {Object} - Object with keywords and tags arrays
|
||||
*/
|
||||
export function parseSearchQuery(searchString) {
|
||||
if (!searchString || typeof searchString !== 'string') {
|
||||
return { keywords: '', tags: [] };
|
||||
}
|
||||
|
||||
const tagRegex = /#([\w\u4e00-\u9fa5\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af-]+)/g;
|
||||
const tags = [];
|
||||
let match;
|
||||
|
||||
while ((match = tagRegex.exec(searchString)) !== null) {
|
||||
tags.push(match[1].toLowerCase());
|
||||
}
|
||||
|
||||
// Remove tags from search string to get keywords
|
||||
const keywords = searchString.replace(/#[\w\u4e00-\u9fa5\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af-]+/g, '').trim();
|
||||
|
||||
return { keywords, tags: normalizeTags(tags) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file matches tag filter
|
||||
* @param {string[]} fileTags - Tags on the file
|
||||
* @param {string[]} requiredTags - Tags that must be present
|
||||
* @returns {boolean} - Whether file has all required tags
|
||||
*/
|
||||
export function matchesTags(fileTags, requiredTags) {
|
||||
if (!Array.isArray(requiredTags) || requiredTags.length === 0) {
|
||||
return true; // No tag filter
|
||||
}
|
||||
|
||||
if (!Array.isArray(fileTags) || fileTags.length === 0) {
|
||||
return false; // File has no tags but filter requires tags
|
||||
}
|
||||
|
||||
const fileTagsLower = fileTags.map(t => t.toLowerCase());
|
||||
return requiredTags.every(tag => fileTagsLower.includes(tag.toLowerCase()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all unique tags from an array of files
|
||||
* @param {Array} files - Array of file objects with metadata.Tags
|
||||
* @returns {string[]} - Sorted array of unique tags
|
||||
*/
|
||||
export function extractUniqueTags(files) {
|
||||
if (!Array.isArray(files)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const allTags = new Set();
|
||||
|
||||
files.forEach(file => {
|
||||
if (file && file.metadata && Array.isArray(file.metadata.Tags)) {
|
||||
file.metadata.Tags.forEach(tag => {
|
||||
if (tag && typeof tag === 'string') {
|
||||
allTags.add(tag.toLowerCase().trim());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(allTags).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter tags by prefix (for autocomplete)
|
||||
* @param {string[]} tags - Array of all available tags
|
||||
* @param {string} prefix - Prefix to filter by
|
||||
* @param {number} limit - Maximum number of results
|
||||
* @returns {string[]} - Filtered tags
|
||||
*/
|
||||
export function filterTagsByPrefix(tags, prefix, limit = 20) {
|
||||
if (!Array.isArray(tags) || !prefix || typeof prefix !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const prefixLower = prefix.toLowerCase().trim();
|
||||
|
||||
return tags
|
||||
.filter(tag => tag.toLowerCase().startsWith(prefixLower))
|
||||
.slice(0, limit);
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/logo.png"><link rel="apple-touch-icon" href="/logo.png"><link rel="mask-icon" href="/logo.png" color="#f4b400"><meta name="description" content="Sanyue ImgHub - A modern file hosting platform"><meta name="keywords" content="Sanyue, ImgHub, file hosting, image hosting, cloud storage"><meta name="author" content="SanyueQi"><title>Sanyue ImgHub</title><script defer="defer" src="/js/app.02dbf6ff.js"></script><link href="/css/app.6e9711cc.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but sanyue_imghub doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html><style>/* 下拉菜单样式 */
|
||||
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/logo.png"><link rel="apple-touch-icon" href="/logo.png"><link rel="mask-icon" href="/logo.png" color="#f4b400"><meta name="description" content="Sanyue ImgHub - A modern file hosting platform"><meta name="keywords" content="Sanyue, ImgHub, file hosting, image hosting, cloud storage"><meta name="author" content="SanyueQi"><title>Sanyue ImgHub</title><script defer="defer" src="/js/app.f5fe4030.js"></script><link href="/css/app.ed56be35.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but sanyue_imghub doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html><style>/* 下拉菜单样式 */
|
||||
.el-dropdown__popper.el-popper {
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +1,2 @@
|
||||
"use strict";(self["webpackChunksanyue_imghub"]=self["webpackChunksanyue_imghub"]||[]).push([[548],{4452:function(e,t,s){s.d(t,{A:function(){return v}});var n=s(6975),a=s(47),i=(s(5331),s(9648),s(9623)),l=(s(9092),s(4632)),o=s(3525),r=s(6768),d=s(4232),u=s(5130);const c={class:"login"},h={class:"login-container"},p={class:"login-title",tabindex:"0"},m={class:"input-wrapper"};function f(e,t,s,f,b,g){const y=o.A,k=l.A,w=i.WK,v=a.S2,L=n.A;return(0,r.uX)(),(0,r.CE)("div",c,[(0,r.bF)(y,{class:"toggle-dark"}),(0,r.bF)(k),(0,r.Lk)("div",h,[(0,r.Lk)("h1",p,(0,d.v_)(s.title),1),((0,r.uX)(!0),(0,r.CE)(r.FK,null,(0,r.pI)(s.fields,(e,s)=>((0,r.uX)(),(0,r.CE)("div",{key:e.key,class:"input-container"},[(0,r.Lk)("label",{class:"input-name",ref_for:!0,ref:`inputLabel${s}`,style:(0,d.Tr)({"--underline-width":b.labelUnderlineWidths[s]+"px"})},(0,d.v_)(e.label),5),(0,r.Lk)("div",m,[(0,r.bF)(w,{modelValue:b.formData[e.key],"onUpdate:modelValue":t=>b.formData[e.key]=t,placeholder:e.placeholder,type:e.type||"text","show-password":e.showPassword,class:"password-input",onKeyup:(0,u.jR)(g.handleSubmit,["enter","native"]),onFocus:g.handleInputFocus,onBlur:g.handleInputBlur},null,8,["modelValue","onUpdate:modelValue","placeholder","type","show-password","onKeyup","onFocus","onBlur"]),t[0]||(t[0]=(0,r.Lk)("div",{class:"input-underline"},null,-1))])]))),128)),(0,r.bF)(v,{class:"submit",type:"primary",onClick:g.handleSubmit},{default:(0,r.k6)(()=>[(0,r.eW)((0,d.v_)(s.submitText),1)]),_:1},8,["onClick"])]),(0,r.bF)(L,{class:"footer"})])}s(8111),s(7588);var b=s(782),g=s(8903),y={name:"BaseLogin",mixins:[g.A],props:{title:{type:String,required:!0},fields:{type:Array,required:!0},submitText:{type:String,default:"登录"},backgroundKey:{type:String,required:!0},isAdmin:{type:Boolean,default:!1}},data(){return{formData:{},labelUnderlineWidths:[]}},computed:{...(0,b.L8)(["userConfig"])},watch:{fields:{handler(){this.$nextTick(()=>{this.calculateLabelWidths()})},deep:!0}},components:{Footer:n.A,ToggleDark:o.A,Logo:l.A},mounted(){this.initFormData(),this.initializeBackground(this.backgroundKey,".login",!this.isAdmin,!0),this.$nextTick(()=>{this.calculateLabelWidths()})},methods:{initFormData(){const e={};this.fields.forEach(t=>{e[t.key]=""}),this.formData=e,this.labelUnderlineWidths=new Array(this.fields.length).fill(0)},calculateLabelWidths(){this.$nextTick(()=>{this.fields.forEach((e,t)=>{const s=this.$refs[`inputLabel${t}`];if(s&&s[0]){const n=document.createElement("canvas"),a=n.getContext("2d"),i=s[0],l=window.getComputedStyle(i);a.font=`${l.fontWeight} ${l.fontSize} ${l.fontFamily}`;const o=a.measureText(e.label).width;this.labelUnderlineWidths[t]=Math.ceil(o)+3}})})},handleSubmit(){this.$emit("submit",{...this.formData})},handleInputFocus(e){const t=e.target.closest(".input-container");if(t){const e=t.querySelector(".input-wrapper");e&&e.classList.add("focused")}},handleInputBlur(e){const t=e.target.closest(".input-container");if(t){const e=t.querySelector(".input-wrapper");e&&e.classList.remove("focused")}}}},k=s(1241);const w=(0,k.A)(y,[["render",f],["__scopeId","data-v-61197d6f"]]);var v=w},8351:function(e,t,s){s.r(t),s.d(t,{default:function(){return u}});var n=s(4452),a=s(6768);function i(e,t,s,i,l,o){const r=n.A;return(0,a.uX)(),(0,a.Wv)(r,{title:"管理端登录",fields:l.loginFields,"submit-text":"登录","background-key":"adminLoginBkImg","is-admin":!0,onSubmit:o.handleLogin},null,8,["fields","onSubmit"])}s(4114),s(4979);var l=s(9189),o={data(){return{loginFields:[{key:"username",label:"用户名",placeholder:"请输入用户名",type:"text"},{key:"password",label:"密码",placeholder:"请输入密码",type:"password",showPassword:!0}]}},components:{BaseLogin:n.A},methods:{async handleLogin(e){const{username:t,password:s}=e,n=btoa(`${t}:${s}`);try{const e=await l.A.get("/api/manage/check",{headers:{Authorization:`Basic ${n}`},withCredentials:!0});200===e.status&&(this.$store.commit("setCredentials",n),this.$router.push("/dashboard"))}catch(a){a.response&&401===a.response.status?this.$message.error("用户名或密码错误"):this.$message.error("服务器错误")}}}},r=s(1241);const d=(0,r.A)(o,[["render",i]]);var u=d}}]);
|
||||
//# sourceMappingURL=548.6255b5bc.js.map
|
||||
//# sourceMappingURL=548.872506ac.js.map
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +1,2 @@
|
||||
"use strict";(self["webpackChunksanyue_imghub"]=self["webpackChunksanyue_imghub"]||[]).push([[585],{4452:function(e,t,n){n.d(t,{A:function(){return v}});var s=n(6975),i=n(47),a=(n(5331),n(9648),n(9623)),o=(n(9092),n(4632)),l=n(3525),r=n(6768),u=n(4232),d=n(5130);const c={class:"login"},h={class:"login-container"},p={class:"login-title",tabindex:"0"},m={class:"input-wrapper"};function f(e,t,n,f,g,b){const y=l.A,k=o.A,w=a.WK,v=i.S2,L=s.A;return(0,r.uX)(),(0,r.CE)("div",c,[(0,r.bF)(y,{class:"toggle-dark"}),(0,r.bF)(k),(0,r.Lk)("div",h,[(0,r.Lk)("h1",p,(0,u.v_)(n.title),1),((0,r.uX)(!0),(0,r.CE)(r.FK,null,(0,r.pI)(n.fields,(e,n)=>((0,r.uX)(),(0,r.CE)("div",{key:e.key,class:"input-container"},[(0,r.Lk)("label",{class:"input-name",ref_for:!0,ref:`inputLabel${n}`,style:(0,u.Tr)({"--underline-width":g.labelUnderlineWidths[n]+"px"})},(0,u.v_)(e.label),5),(0,r.Lk)("div",m,[(0,r.bF)(w,{modelValue:g.formData[e.key],"onUpdate:modelValue":t=>g.formData[e.key]=t,placeholder:e.placeholder,type:e.type||"text","show-password":e.showPassword,class:"password-input",onKeyup:(0,d.jR)(b.handleSubmit,["enter","native"]),onFocus:b.handleInputFocus,onBlur:b.handleInputBlur},null,8,["modelValue","onUpdate:modelValue","placeholder","type","show-password","onKeyup","onFocus","onBlur"]),t[0]||(t[0]=(0,r.Lk)("div",{class:"input-underline"},null,-1))])]))),128)),(0,r.bF)(v,{class:"submit",type:"primary",onClick:b.handleSubmit},{default:(0,r.k6)(()=>[(0,r.eW)((0,u.v_)(n.submitText),1)]),_:1},8,["onClick"])]),(0,r.bF)(L,{class:"footer"})])}n(8111),n(7588);var g=n(782),b=n(8903),y={name:"BaseLogin",mixins:[b.A],props:{title:{type:String,required:!0},fields:{type:Array,required:!0},submitText:{type:String,default:"登录"},backgroundKey:{type:String,required:!0},isAdmin:{type:Boolean,default:!1}},data(){return{formData:{},labelUnderlineWidths:[]}},computed:{...(0,g.L8)(["userConfig"])},watch:{fields:{handler(){this.$nextTick(()=>{this.calculateLabelWidths()})},deep:!0}},components:{Footer:s.A,ToggleDark:l.A,Logo:o.A},mounted(){this.initFormData(),this.initializeBackground(this.backgroundKey,".login",!this.isAdmin,!0),this.$nextTick(()=>{this.calculateLabelWidths()})},methods:{initFormData(){const e={};this.fields.forEach(t=>{e[t.key]=""}),this.formData=e,this.labelUnderlineWidths=new Array(this.fields.length).fill(0)},calculateLabelWidths(){this.$nextTick(()=>{this.fields.forEach((e,t)=>{const n=this.$refs[`inputLabel${t}`];if(n&&n[0]){const s=document.createElement("canvas"),i=s.getContext("2d"),a=n[0],o=window.getComputedStyle(a);i.font=`${o.fontWeight} ${o.fontSize} ${o.fontFamily}`;const l=i.measureText(e.label).width;this.labelUnderlineWidths[t]=Math.ceil(l)+3}})})},handleSubmit(){this.$emit("submit",{...this.formData})},handleInputFocus(e){const t=e.target.closest(".input-container");if(t){const e=t.querySelector(".input-wrapper");e&&e.classList.add("focused")}},handleInputBlur(e){const t=e.target.closest(".input-container");if(t){const e=t.querySelector(".input-wrapper");e&&e.classList.remove("focused")}}}},k=n(1241);const w=(0,k.A)(y,[["render",f],["__scopeId","data-v-61197d6f"]]);var v=w},9206:function(e,t,n){n.r(t),n.d(t,{default:function(){return p}});var s=n(4452),i=n(6768);function a(e,t,n,a,o,l){const r=s.A;return(0,i.uX)(),(0,i.Wv)(r,{title:l.loginTitle,fields:o.loginFields,"submit-text":"登录","background-key":"loginBkImg","is-admin":!1,onSubmit:l.handleLogin},null,8,["title","fields","onSubmit"])}n(4114);var o=n(4570),l=n.n(o),r=n(9189),u=n(782),d={data(){return{loginFields:[{key:"password",label:"密码",placeholder:"请输入认证码",type:"password",showPassword:!0}]}},computed:{...(0,u.L8)(["userConfig"]),ownerName(){return this.userConfig?.ownerName||"Sanyue"},loginTitle(){return`登录到 ${this.ownerName} 图床`}},components:{BaseLogin:s.A},methods:{handleLogin(e){const{password:t}=e,n=""===t?"unset":t;r.A.post("/api/login",{authCode:t}).then(e=>{200===e.status?(l().set("authCode",n,"14d"),this.$router.push("/"),this.$message.success("登录成功")):this.$message.error("登录失败,请检查密码是否正确")}).catch(e=>{this.$message.error("登录失败,请检查密码是否正确")})}}},c=n(1241);const h=(0,c.A)(d,[["render",a]]);var p=h}}]);
|
||||
//# sourceMappingURL=585.c628b4e4.js.map
|
||||
//# sourceMappingURL=585.e7104bbc.js.map
|
||||
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user