Merge pull request #334 from sean908/main

feat: Add tag management for images
This commit is contained in:
叁月柒
2025-10-14 10:34:49 +08:00
committed by GitHub
122 changed files with 695 additions and 36 deletions
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.
+2
View File
@@ -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 (
+192
View File
@@ -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}`);
}
}
+109
View File
@@ -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' }
});
}
}
+172
View File
@@ -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' }
});
}
}
+1
View File
@@ -217,6 +217,7 @@ async function handleChannelBasedMerge(context, uploadId, totalChunks, originalF
TimeStamp: Date.now(),
Label: "None",
Directory: normalizedFolder === '' ? '' : normalizedFolder + '/',
Tags: []
};
// 更新进度
+1
View File
@@ -146,6 +146,7 @@ async function processFileUpload(context, formdata = null) {
TimeStamp: time,
Label: "None",
Directory: normalizedFolder === '' ? '' : normalizedFolder + '/',
Tags: []
};
let fileExt = fileName.split('.').pop(); // 文件扩展名
+25 -4
View File
@@ -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;
});
}
+161
View File
@@ -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
View File
@@ -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;
BIN
View File
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 -1
View File
@@ -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 -1
View File
@@ -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